diff --git a/.github/workflows/typescript-update.yml b/.github/workflows/typescript-update.yml index a72c4f2..98913e6 100644 --- a/.github/workflows/typescript-update.yml +++ b/.github/workflows/typescript-update.yml @@ -70,3 +70,6 @@ jobs: commit-message: "chore: update TypeScript toolchain snapshot" title: "chore: update TypeScript toolchain snapshot" body-path: .tmp/typescript-toolchain-update-pr.md + + - name: Check latest TypeScript major against the peer range + run: npm run check:typescript-peer-latest diff --git a/deploy/package-lib.mjs b/deploy/package-lib.mjs index 8e18e25..0f794ce 100644 --- a/deploy/package-lib.mjs +++ b/deploy/package-lib.mjs @@ -41,6 +41,74 @@ export async function createPackageStages(options = {}) { return summaries; } +const DELIVERED_COMPAT_RESOLUTION_KINDS = new Set([ + "constructor", + "inherited-member", + "member", + "option-property", + "root-availability", + "signature-compat", + "transform-only", + "type-property", +]); +const NON_DECLARATION_COMPAT_RESOLUTION_KINDS = new Set([ + "already-excluded-upstream", + "behavioral", + "not-modeled-upstream", +]); + +/** + * @param {Array<{ includeInTarget: boolean; resolutionKind: string; }>} classifiedCompatRows + */ +export function countIncludedCompatRows(classifiedCompatRows) { + let count = 0; + for (const row of classifiedCompatRows) { + const delivered = DELIVERED_COMPAT_RESOLUTION_KINDS.has(row.resolutionKind); + const nonDeclaration = NON_DECLARATION_COMPAT_RESOLUTION_KINDS.has(row.resolutionKind); + if (!delivered && !nonDeclaration) { + throw new Error(`Unknown compat resolution kind: ${row.resolutionKind}`); + } + if (row.includeInTarget && delivered) { + count++; + } + } + return count; +} + +/** + * @param {string} range + * @param {string[]} versions + */ +export function assertTypeScriptPeerRange(range, versions) { + const numericIdentifier = "(0|[1-9]\\d*)"; + const rangeMatch = typeof range === "string" + ? range.match(new RegExp(`^>=${numericIdentifier} <${numericIdentifier}$`)) + : undefined; + if (!rangeMatch) { + throw new Error(`Unsupported TypeScript peer range: ${range}`); + } + const minimumMajor = Number(rangeMatch[1]); + const maximumMajor = Number(rangeMatch[2]); + if (minimumMajor >= maximumMajor) { + throw new Error(`Unsupported TypeScript peer range: ${range}`); + } + if (!versions.length) { + throw new Error("No TypeScript versions were provided for peer range validation"); + } + for (const version of versions) { + const versionMatch = typeof version === "string" + ? version.match(new RegExp(`^${numericIdentifier}\\.${numericIdentifier}\\.${numericIdentifier}$`)) + : undefined; + if (!versionMatch) { + throw new Error(`Unsupported TypeScript version: ${version}`); + } + const major = Number(versionMatch[1]); + if (major < minimumMajor || major >= maximumMajor) { + throw new Error(`TypeScript ${version} is outside peer range ${range}`); + } + } +} + /** * @param {{ * packageId?: string; @@ -113,6 +181,10 @@ export async function publishReleasePlan(releasePlan, options = {}) { * @param {string} stageDirectory */ async function createPackageStage(packageConfig, snapshot, versionOverride, stageDirectory) { + assertTypeScriptPeerRange(packageConfig.typescriptPeerDependencyRange, [ + snapshot.manifest.snapshot.typescriptStradaVersion, + snapshot.manifest.snapshot.typescriptVersion, + ]); await rm(stageDirectory, { recursive: true, force: true }); await mkdir(stageDirectory, { recursive: true }); @@ -123,6 +195,16 @@ async function createPackageStage(packageConfig, snapshot, versionOverride, stag } const packageVersion = versionOverride ?? await resolveNextPackageVersion(packageConfig); + const includedCompatCount = countIncludedCompatRows(snapshot.classification.classifiedCompatRows); + const snapshotMetadata = { + schemaVersion: 1, + baselineDate: snapshot.manifest.snapshot.baselineDate, + webFeaturesPackageVersion: snapshot.manifest.snapshot.webFeaturesPackageVersion, + webFeaturesGitHead: snapshot.manifest.snapshot.webFeaturesGitHead, + typescriptVersion: snapshot.manifest.snapshot.typescriptVersion, + includedCompatCount, + generatorVersion: snapshot.manifest.snapshot.generatorVersion, + }; const packageJson = { name: packageConfig.name, version: packageVersion, @@ -136,6 +218,14 @@ async function createPackageStage(packageConfig, snapshot, versionOverride, stag bugs: { url: packageConfig.bugsUrl, }, + peerDependencies: { + typescript: packageConfig.typescriptPeerDependencyRange, + }, + peerDependenciesMeta: { + typescript: { + optional: true, + }, + }, publishConfig: { access: "public", }, @@ -144,6 +234,7 @@ async function createPackageStage(packageConfig, snapshot, versionOverride, stag files: [ "index.d.ts", "baseline.d.ts", + "snapshot.json", "reports/", "README.md", "LICENSE", @@ -159,16 +250,21 @@ async function createPackageStage(packageConfig, snapshot, versionOverride, stag path.join(stageDirectory, "package.json"), `${JSON.stringify(packageJson, undefined, 2)}\n`, ); + await writeFile( + path.join(stageDirectory, "snapshot.json"), + `${JSON.stringify(snapshotMetadata, undefined, 2)}\n`, + ); await writeFile( path.join(stageDirectory, "README.md"), renderTemplate(await readFile(packageConfig.readmeTemplatePath, "utf8"), { PACKAGE_NAME: packageConfig.name, PACKAGE_VERSION: packageVersion, + TYPESCRIPT_PEER_DEPENDENCY_RANGE: packageConfig.typescriptPeerDependencyRange, BASELINE_DATE: snapshot.manifest.snapshot.baselineDate, TYPESCRIPT_VERSION: snapshot.manifest.snapshot.typescriptVersion, WEB_FEATURES_VERSION: snapshot.manifest.snapshot.webFeaturesPackageVersion, WEB_FEATURES_GIT_HEAD: snapshot.manifest.snapshot.webFeaturesGitHead, - INCLUDED_COMPAT_COUNT: String(snapshot.classification.summary.includedCompatCount), + INCLUDED_COMPAT_COUNT: String(includedCompatCount), SELECTED_UNIT_COUNT: String(snapshot.generation.summary.selectedUnitCount), TRANSFORMED_UNIT_COUNT: String(snapshot.generation.summary.transformedUnitCount), }), @@ -463,7 +559,7 @@ function renderReleaseNotes(options) { `- TypeScript package: ${snapshot.manifest.snapshot.typescriptVersion}`, `- web-features package: ${snapshot.manifest.snapshot.webFeaturesPackageVersion}`, `- web-features gitHead: ${snapshot.manifest.snapshot.webFeaturesGitHead}`, - `- Included compat rows: ${snapshot.classification.summary.includedCompatCount}`, + `- Included compat rows: ${countIncludedCompatRows(snapshot.classification.classifiedCompatRows)}`, `- Selected declaration units: ${snapshot.generation.summary.selectedUnitCount}`, `- Transformed units: ${snapshot.generation.summary.transformedUnitCount}`, "", @@ -588,6 +684,7 @@ function compareStrings(left, right) { * id: string; * name: string; * description: string; + * typescriptPeerDependencyRange: string; * initialVersion: string; * license: string; * keywords: string[]; diff --git a/deploy/package-registry.mjs b/deploy/package-registry.mjs index 583d254..d51f284 100644 --- a/deploy/package-registry.mjs +++ b/deploy/package-registry.mjs @@ -12,6 +12,7 @@ export const baselinePackage = { id: "baseline", name: "typescript-baseline-lib", description: "Baseline widely available JavaScript declarations for TypeScript.", + typescriptPeerDependencyRange: ">=6 <8", initialVersion: "0.0.1", license: "Apache-2.0", keywords: [ diff --git a/deploy/readmes/baseline.md b/deploy/readmes/baseline.md index d05c9b7..68f268c 100644 --- a/deploy/readmes/baseline.md +++ b/deploy/readmes/baseline.md @@ -6,6 +6,7 @@ This package is produced by the [`TypeScript-Baseline-lib-generator`](https://gi Current snapshot: +- Supported TypeScript versions: `{{TYPESCRIPT_PEER_DEPENDENCY_RANGE}}` - Baseline date: `{{BASELINE_DATE}}` - TypeScript package: `{{TYPESCRIPT_VERSION}}` - web-features package: `{{WEB_FEATURES_VERSION}}` @@ -22,6 +23,10 @@ Stock TypeScript doesn't treat `"baseline"` as a built-in `lib` yet, so install npm install --save-dev {{PACKAGE_NAME}} ``` +TypeScript is an optional peer dependency. Install a supported TypeScript 6.x or 7.x compiler separately if your project does not already provide one. + +The same snapshot facts are available to tools through `{{PACKAGE_NAME}}/snapshot.json`. + ```json { "compilerOptions": { diff --git a/derived/current/classification.json b/derived/current/classification.json index 2dc6a7d..b32e31c 100644 --- a/derived/current/classification.json +++ b/derived/current/classification.json @@ -12,7 +12,7 @@ "lowCompatCount": 77, "falseCompatCount": 323, "includedCompatCount": 750, - "emitCompatCount": 998, + "emitCompatCount": 997, "notModeledUpstreamCount": 64, "alreadyExcludedUpstreamCount": 1, "managedCompatCount": 68, @@ -14753,10 +14753,10 @@ "snapshot": [], "compatRoot": "RegExp", "includeInTarget": true, - "resolutionKind": "inherited-member", + "resolutionKind": "behavioral", "resolvedUnitIds": [], "transforms": [], - "notes": "javascript.builtins.RegExp.toString.escaping is inherited from shared object prototype declarations" + "notes": "javascript.builtins.RegExp.toString.escaping is tracked as behavior on RegExp.toString" }, { "compatKey": "javascript.builtins.RegExp.unicode", diff --git a/lib/classifier.mjs b/lib/classifier.mjs index bc3671c..e7cba93 100644 --- a/lib/classifier.mjs +++ b/lib/classifier.mjs @@ -688,6 +688,15 @@ function classifyMemberRow(options) { ].sort((left, right) => compareStringsCaseSensitive(left.id, right.id)); if (!memberUnits.length) { + if (isBehaviorQualifier(qualifierSegments)) { + return { + resolutionKind: "behavioral", + resolvedUnitIds: [], + notes: `${compatRow.compatKey} is tracked as behavior on ${compatRoot}.${memberName}`, + transforms: [], + }; + } + const inheritedUnits = [ ...[...rootSurface.instanceContainerSymbols].flatMap(symbol => getDeclarationUnits(inventory, symbol) diff --git a/package.json b/package.json index 7b8538f..557802d 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ }, "scripts": { "generate": "node scripts/generate.mjs", + "check:typescript-peer-latest": "node scripts/check-typescript-peer-latest.mjs", "update:typescript-toolchain": "node scripts/update-typescript-toolchain.mjs", "update:web-features": "node scripts/update-web-features.mjs", "checkout:typescript-source": "node scripts/checkout-typescript-source.mjs", diff --git a/scripts/check-typescript-peer-latest.mjs b/scripts/check-typescript-peer-latest.mjs new file mode 100644 index 0000000..74dd6c4 --- /dev/null +++ b/scripts/check-typescript-peer-latest.mjs @@ -0,0 +1,16 @@ +// @ts-check + +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { assertTypeScriptPeerRange } from "../deploy/package-lib.mjs"; +import { baselinePackage } from "../deploy/package-registry.mjs"; +import { npmViewField } from "../lib/installed-package.mjs"; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const latestVersion = npmViewField(repoRoot, "typescript", "dist-tags.latest"); +if (typeof latestVersion !== "string") { + throw new Error("Could not resolve the latest stable TypeScript version"); +} + +assertTypeScriptPeerRange(baselinePackage.typescriptPeerDependencyRange, [latestVersion]); +console.log(`typescript@${latestVersion} is within peer range ${baselinePackage.typescriptPeerDependencyRange}`); diff --git a/scripts/update-typescript-toolchain.mjs b/scripts/update-typescript-toolchain.mjs index a7a00f7..b3c27bc 100644 --- a/scripts/update-typescript-toolchain.mjs +++ b/scripts/update-typescript-toolchain.mjs @@ -15,6 +15,8 @@ import { applyStradaSourcePin, applyTypeScriptGoSourcePin, } from "../lib/typescript-source.mjs"; +import { assertTypeScriptPeerRange } from "../deploy/package-lib.mjs"; +import { baselinePackage } from "../deploy/package-registry.mjs"; const scriptPath = fileURLToPath(import.meta.url); const scriptDirectory = path.dirname(scriptPath); @@ -41,16 +43,14 @@ await main(); async function main() { const typescriptVersion = args.typescriptVersion - ?? String(npmViewField(repoRoot, "typescript", "dist-tags.latest")); + ?? resolveLatestStableVersion( + `typescript@${baselinePackage.typescriptPeerDependencyRange}`, + "supported TypeScript", + ); const stradaVersion = args.stradaVersion ?? resolveLatestStradaVersion(); - assertStableSemver(typescriptVersion, "typescript"); - assertStableSemver(stradaVersion, "typescript-strada"); - if (!typescriptVersion.startsWith("7.") && !args.typescriptVersion) { - throw new Error( - `Resolved typescript dist-tags.latest is ${typescriptVersion}, expected a 7.x release. Pass --typescript-version explicitly to override.`, - ); - } + assertTypeScriptPeerRange(baselinePackage.typescriptPeerDependencyRange, [typescriptVersion]); + assertTypeScriptPeerRange(">=6 <7", [stradaVersion]); execFileSync("npm", [ "install", @@ -110,18 +110,43 @@ async function main() { * Automatically follows any security patch released on 6.0.x. */ function resolveLatestStradaVersion() { - const versions = npmViewField(repoRoot, "typescript@<7.0.0-0", "version"); + return resolveLatestStableVersion("typescript@<7.0.0-0", "Strada (typescript <7)"); +} + +/** + * @param {string} packageSpecifier + * @param {string} label + */ +function resolveLatestStableVersion(packageSpecifier, label) { + const versions = npmViewField(repoRoot, packageSpecifier, "version"); const versionList = Array.isArray(versions) ? versions : [versions]; const stableVersions = versionList - .filter(version => typeof version === "string" && /^\d+\.\d+\.\d+$/u.test(version)); + .filter(version => typeof version === "string" && /^\d+\.\d+\.\d+$/u.test(version)) + .sort(compareStableSemver); const latest = stableVersions.at(-1); if (!latest) { - throw new Error("Could not resolve the latest stable Strada (typescript <7) version from the registry"); + throw new Error(`Could not resolve the latest stable ${label} version from the registry`); } return latest; } +/** + * @param {string} left + * @param {string} right + */ +function compareStableSemver(left, right) { + const leftParts = left.split(".").map(Number); + const rightParts = right.split(".").map(Number); + for (let index = 0; index < 3; index++) { + const difference = leftParts[index] - rightParts[index]; + if (difference) { + return difference; + } + } + return 0; +} + /** * Compute the content hash of lib/*.d.ts from every reference platform's registry * tarball, verify they all match exactly, then return the pin. @@ -197,17 +222,6 @@ function computeCrossPlatformLibSourcePin(typescriptVersion) { }; } -/** - * @param {string} version - * @param {string} label - */ -function assertStableSemver(version, label) { - if (!/^\d+\.\d+\.\d+$/u.test(version)) { - throw new Error(`Resolved ${label} version ${version} is not a stable x.y.z release`); - } -} - - /** * @param {string[]} argv */ diff --git a/test/classifier.test.mjs b/test/classifier.test.mjs index cc36e7e..3dbe9a7 100644 --- a/test/classifier.test.mjs +++ b/test/classifier.test.mjs @@ -268,7 +268,9 @@ test("classifier routes synthetic compat rows to the expected resolution kinds", row("javascript.builtins.Widget.Widget", "high"), row("javascript.builtins.Widget.Widget.options_parameter", "low"), row("javascript.builtins.Widget.Widget.options_size_parameter", "low"), + row("javascript.builtins.Widget.configure.options_size_parameter.extended_values", "high"), row("javascript.builtins.Widget.toy.stable_sorting", "high"), + row("javascript.builtins.Widget.toString.escaping", "high"), row("javascript.builtins.TypedArray.at", "high"), row("javascript.builtins.Iterator.map", "low"), row("javascript.functions.arguments", "high"), @@ -306,10 +308,21 @@ test("classifier routes synthetic compat rows to the expected resolution kinds", assert.equal(optionRow.resolutionKind, "option-property"); assert.ok(optionRow.resolvedUnitIds.some(unitId => unitId.includes("WidgetOptions.size"))); + const qualifiedOptionRow = findRow( + classification, + "javascript.builtins.Widget.configure.options_size_parameter.extended_values", + ); + assert.equal(qualifiedOptionRow.resolutionKind, "option-property"); + assert.ok(qualifiedOptionRow.resolvedUnitIds.some(unitId => unitId.includes("WidgetOptions.size"))); + const behaviorRow = findRow(classification, "javascript.builtins.Widget.toy.stable_sorting"); assert.equal(behaviorRow.resolutionKind, "behavioral"); assert.deepEqual(behaviorRow.resolvedUnitIds, []); + const inheritedBehaviorRow = findRow(classification, "javascript.builtins.Widget.toString.escaping"); + assert.equal(inheritedBehaviorRow.resolutionKind, "behavioral"); + assert.deepEqual(inheritedBehaviorRow.resolvedUnitIds, []); + // Synthetic TypedArray root: BCD's "TypedArray" expands to individual typed array types. // But only typed arrays whose own root row is in the target get mapped. const typedArrayRow = findRow(classification, "javascript.builtins.TypedArray.at"); diff --git a/test/consumer-smoke.test.mjs b/test/consumer-smoke.test.mjs index 0767f2f..bed220d 100644 --- a/test/consumer-smoke.test.mjs +++ b/test/consumer-smoke.test.mjs @@ -1,6 +1,7 @@ // @ts-check import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; import path from "node:path"; import test from "node:test"; import { baselinePackageName } from "../deploy/package-registry.mjs"; @@ -8,6 +9,7 @@ import { renderNegativeProbeSource } from "../lib/negative-probes.mjs"; import { cleanupTempDirectories, loadActiveNegativeProbesFromRepo, + readJsonFile, stageBaselinePackage, createTempDirectory, runNpm, @@ -45,6 +47,30 @@ test("staged consumer smoke: stock tsc accepts supported baseline APIs and rejec }); runNpm(["install", "--no-package-lock", "--no-save", stageDirectory], { cwd: consumerDirectory }); + const stagedSnapshot = readJsonFile(path.join(stageDirectory, "snapshot.json")); + const importProbePath = path.join(consumerDirectory, "snapshot-import.mjs"); + const requireProbePath = path.join(consumerDirectory, "snapshot-require.cjs"); + writeTextFile(importProbePath, [ + `import snapshot from "${baselinePackageName}/snapshot.json" with { type: "json" };`, + "process.stdout.write(JSON.stringify(snapshot));", + "", + ].join("\n")); + writeTextFile(requireProbePath, [ + `const snapshot = require("${baselinePackageName}/snapshot.json");`, + "process.stdout.write(JSON.stringify(snapshot));", + "", + ].join("\n")); + const importedSnapshot = JSON.parse(execFileSync(process.execPath, [importProbePath], { + cwd: consumerDirectory, + encoding: "utf8", + })); + const requiredSnapshot = JSON.parse(execFileSync(process.execPath, [requireProbePath], { + cwd: consumerDirectory, + encoding: "utf8", + })); + assert.deepEqual(importedSnapshot, stagedSnapshot); + assert.deepEqual(requiredSnapshot, stagedSnapshot); + const passPath = path.join(consumerDirectory, "consumer-pass.ts"); writeTextFile(passPath, [ "const reversed = [1, 2, 3].toReversed();", diff --git a/test/package-metadata.test.mjs b/test/package-metadata.test.mjs new file mode 100644 index 0000000..4d522be --- /dev/null +++ b/test/package-metadata.test.mjs @@ -0,0 +1,157 @@ +// @ts-check + +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import test from "node:test"; +import { + assertTypeScriptPeerRange, + countIncludedCompatRows, +} from "../deploy/package-lib.mjs"; +import { resolveInstalledPackageRoot } from "../lib/installed-package.mjs"; +import { + cleanupTempDirectories, + readJsonFile, + repoClassificationPath, + repoManifest, + repoRoot, + stageBaselinePackage, +} from "./helpers.mjs"; + +/** @type {string[]} */ +const tempDirectories = []; + +test.afterEach(() => { + cleanupTempDirectories(tempDirectories); +}); + +test("package metadata stages deterministically with the TypeScript peer contract and snapshot facts", async () => { + const firstStage = await stageBaselinePackage({ tempDirectories }); + const secondStage = await stageBaselinePackage({ tempDirectories }); + + const packageJson = readJsonFile(path.join(firstStage.stageDirectory, "package.json")); + assert.deepEqual(packageJson.peerDependencies, { + typescript: ">=6 <8", + }); + assert.deepEqual(packageJson.peerDependenciesMeta, { + typescript: { + optional: true, + }, + }); + assert.equal(packageJson.exports, undefined); + assert.ok(packageJson.files.includes("snapshot.json")); + + /** @type {{ summary: { includedCompatCount: number; }; classifiedCompatRows: Array<{ includeInTarget: boolean; resolutionKind: string; }>; }} */ + const classification = readJsonFile(repoClassificationPath); + const includedCompatCount = countIncludedCompatRows(classification.classifiedCompatRows); + assert.ok(includedCompatCount < classification.summary.includedCompatCount); + const expectedSnapshot = { + schemaVersion: 1, + baselineDate: repoManifest.snapshot.baselineDate, + webFeaturesPackageVersion: repoManifest.snapshot.webFeaturesPackageVersion, + webFeaturesGitHead: repoManifest.snapshot.webFeaturesGitHead, + typescriptVersion: repoManifest.snapshot.typescriptVersion, + includedCompatCount, + generatorVersion: repoManifest.snapshot.generatorVersion, + }; + assert.deepEqual( + readJsonFile(path.join(firstStage.stageDirectory, "snapshot.json")), + expectedSnapshot, + ); + + const readme = fs.readFileSync(path.join(firstStage.stageDirectory, "README.md"), "utf8"); + assert.match(readme, /Supported TypeScript versions: `>=6 <8`/); + assert.ok(readme.includes(`Baseline date: \`${expectedSnapshot.baselineDate}\``)); + assert.ok(readme.includes(`Included compat rows: \`${expectedSnapshot.includedCompatCount}\``)); + assert.doesNotMatch(readme, /{{[A-Z_]+}}/); + + assert.deepEqual( + readPackageContents(firstStage.stageDirectory), + readPackageContents(secondStage.stageDirectory), + ); +}); + +test("included compat count accepts only declaration-backed resolution kinds", () => { + assert.equal(countIncludedCompatRows([ + { includeInTarget: true, resolutionKind: "member" }, + { includeInTarget: true, resolutionKind: "inherited-member" }, + { includeInTarget: true, resolutionKind: "behavioral" }, + { includeInTarget: true, resolutionKind: "not-modeled-upstream" }, + { includeInTarget: false, resolutionKind: "member" }, + ]), 2); + assert.throws( + () => countIncludedCompatRows([{ includeInTarget: true, resolutionKind: "future-kind" }]), + /Unknown compat resolution kind: future-kind/, + ); + assert.throws( + () => countIncludedCompatRows([{ includeInTarget: false, resolutionKind: "future-kind" }]), + /Unknown compat resolution kind: future-kind/, + ); +}); + +test("TypeScript peer range contains every pinned compiler line", () => { + assert.doesNotThrow(() => assertTypeScriptPeerRange(">=6 <8", ["6.0.3", "7.0.2"])); + assert.throws( + () => assertTypeScriptPeerRange(">=6 <8", ["6.0.3", "8.0.0"]), + /TypeScript 8\.0\.0 is outside peer range >=6 <8/, + ); + assert.throws( + () => assertTypeScriptPeerRange("latest", ["7.0.2"]), + /Unsupported TypeScript peer range: latest/, + ); + assert.throws( + () => assertTypeScriptPeerRange(">=6 <8", ["7.0.0-beta"]), + /Unsupported TypeScript version: 7\.0\.0-beta/, + ); + assert.throws( + () => assertTypeScriptPeerRange(">=6 <8", []), + /No TypeScript versions were provided/, + ); + for (const range of [">=06 <8", ">=8 <8", undefined]) { + assert.throws( + () => assertTypeScriptPeerRange(/** @type {any} */ (range), ["7.0.2"]), + /Unsupported TypeScript peer range/, + ); + } + assert.throws( + () => assertTypeScriptPeerRange(">=6 <8", ["07.0.2"]), + /Unsupported TypeScript version: 07\.0\.2/, + ); +}); + +test("manifest compiler versions match the installed toolchains", () => { + const installedTypeScript = readJsonFile(path.join(resolveInstalledPackageRoot(repoRoot, "typescript"), "package.json")); + const installedStrada = readJsonFile(path.join(resolveInstalledPackageRoot(repoRoot, "typescript-strada"), "package.json")); + assert.equal(repoManifest.snapshot.typescriptVersion, installedTypeScript.version); + assert.equal(repoManifest.snapshot.typescriptStradaVersion, installedStrada.version); +}); + +/** + * @param {string} packageDirectory + */ +function readPackageContents(packageDirectory) { + /** @type {Record} */ + const contents = {}; + + /** + * @param {string} currentDirectory + * @param {string} relativeDirectory + */ + function visit(currentDirectory, relativeDirectory) { + const entries = fs.readdirSync(currentDirectory, { withFileTypes: true }) + .sort((left, right) => left.name.localeCompare(right.name)); + for (const entry of entries) { + const relativePath = relativeDirectory ? path.join(relativeDirectory, entry.name) : entry.name; + const fullPath = path.join(currentDirectory, entry.name); + if (entry.isDirectory()) { + visit(fullPath, relativePath); + } + else { + contents[relativePath.split(path.sep).join(path.posix.sep)] = fs.readFileSync(fullPath); + } + } + } + + visit(packageDirectory, ""); + return contents; +} diff --git a/test/packed-consumer-smoke.test.mjs b/test/packed-consumer-smoke.test.mjs index fa502a5..d6b439e 100644 --- a/test/packed-consumer-smoke.test.mjs +++ b/test/packed-consumer-smoke.test.mjs @@ -10,6 +10,8 @@ import { createBaselinePackageTarball, createTempDirectory, loadActiveNegativeProbesFromRepo, + readJsonFile, + repoManifest, runNpm, runTsc, runTscExpectFailure, @@ -45,6 +47,16 @@ test("packed consumer smoke: npm-packed baseline package typechecks through comp runNpm(["install", "--no-package-lock", "--no-save", tarballPath], { cwd: consumerDirectory }); + const installedPackageDirectory = path.join(consumerDirectory, "node_modules", baselinePackageName); + const installedPackageJson = readJsonFile(path.join(installedPackageDirectory, "package.json")); + assert.deepEqual(installedPackageJson.peerDependencies, { + typescript: ">=6 <8", + }); + assert.equal( + readJsonFile(path.join(installedPackageDirectory, "snapshot.json")).baselineDate, + repoManifest.snapshot.baselineDate, + ); + writeTextFile(path.join(consumerDirectory, "consumer-pass.ts"), [ "const reversed = [1, 2, 3].toReversed();", "const values = Intl.supportedValuesOf(\"currency\");",