diff --git a/README.md b/README.md index e6107ec..28929a9 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ npx tsc --noEmit With this configuration, TypeScript accepts supported JavaScript APIs that are Baseline Widely available. TypeScript reports APIs outside this target as errors. -Examples include `Promise.withResolvers` and `Array.fromAsync` before they reach this Baseline status. The project goal is first-class `--lib baseline` support in TypeScript. +Examples include `Promise.try` and `Intl.Segmenter` before they reach this Baseline status. The project goal is first-class `--lib baseline` support in TypeScript. For more configurations, read the [Usage Guide](docs/USAGE.md). @@ -46,10 +46,10 @@ The generator does not add unavailable runtime APIs to support third-party packa ## Allow a polyfilled feature -If the runtime loads an audited polyfill, add its generated `web-features` entry after the base package. For example, core-js can provide `Promise.withResolvers`: +If the runtime loads an audited polyfill, add its generated `web-features` entry after the base package. For example, core-js can provide `Promise.try`: ```ts -import "core-js/proposals/promise-with-resolvers"; +import "core-js/proposals/promise-try"; ``` ```json @@ -58,7 +58,7 @@ import "core-js/proposals/promise-with-resolvers"; "noLib": true, "types": [ "typescript-baseline-lib", - "typescript-baseline-lib/allow/promise-withresolvers" + "typescript-baseline-lib/allow/promise-try" ] } } diff --git a/datasets/web-features-js-compat.json b/datasets/web-features-js-compat.json index 2b7a7a2..57e6b02 100644 --- a/datasets/web-features-js-compat.json +++ b/datasets/web-features-js-compat.json @@ -1,10 +1,10 @@ { "snapshot": { "name": "baseline-js", - "baselineDate": "2026-07-21", - "extractedDate": "2026-07-21", + "baselineDate": "2026-10-02", + "extractedDate": "2026-10-02", "webFeaturesPackageName": "web-features", - "webFeaturesPackageVersion": "3.34.1" + "webFeaturesPackageVersion": "3.40.1" }, "featureRows": [ { @@ -225,8 +225,9 @@ { "featureId": "array-fromasync", "featureName": "Array.fromAsync()", - "baselineStatus": "low", + "baselineStatus": "high", "baselineLowDate": "2024-01-25", + "baselineHighDate": "2026-07-25", "snapshot": [], "group": [ "arrays" @@ -239,8 +240,9 @@ { "featureId": "array-group", "featureName": "Array grouping", - "baselineStatus": "low", + "baselineStatus": "high", "baselineLowDate": "2024-03-05", + "baselineHighDate": "2026-09-05", "snapshot": [], "group": [ "maps" @@ -761,6 +763,19 @@ "https://github.com/tc39/proposal-import-attributes/tree/abca60286360b47f9a6be25a28f489c2cb157beb" ] }, + { + "featureId": "import-defer", + "featureName": "import defer", + "baselineStatus": false, + "snapshot": [], + "group": [ + "js-modules" + ], + "hasCompatRows": true, + "spec": [ + "https://tc39.es/proposal-defer-import-eval/" + ] + }, { "featureId": "intl", "featureName": "Intl", @@ -838,7 +853,8 @@ { "featureId": "intl-locale-info", "featureName": "Intl.Locale info", - "baselineStatus": false, + "baselineStatus": "low", + "baselineLowDate": "2026-07-21", "snapshot": [], "group": [ "intl" @@ -919,6 +935,19 @@ "https://tc39.es/ecma262/multipage/control-abstraction-objects.html#sec-iterator.concat" ] }, + { + "featureId": "iterator-join", + "featureName": "Iterator join()", + "baselineStatus": false, + "snapshot": [], + "group": [ + "iterators" + ], + "hasCompatRows": true, + "spec": [ + "https://tc39.es/proposal-iterator-join/#sec-iterator.prototype.join" + ] + }, { "featureId": "iterator-methods", "featureName": "Iterator methods", @@ -1321,8 +1350,9 @@ { "featureId": "promise-withresolvers", "featureName": "Promise.withResolvers()", - "baselineStatus": "low", + "baselineStatus": "high", "baselineLowDate": "2024-03-05", + "baselineHighDate": "2026-09-05", "snapshot": [], "group": [ "promises" @@ -1674,6 +1704,19 @@ "https://tc39.es/ecma262/multipage/text-processing.html#sec-string-objects" ] }, + { + "featureId": "string-trim-leftright", + "featureName": "String trimLeft() and trimRight()", + "baselineStatus": false, + "snapshot": [], + "group": [ + "string" + ], + "hasCompatRows": true, + "spec": [ + "https://tc39.es/ecma262/multipage/additional-ecmascript-features-for-web-browsers.html#String.prototype.trimleft" + ] + }, { "featureId": "string-trim-startend", "featureName": "String trimStart() and trimEnd()", @@ -1762,10 +1805,24 @@ "https://tc39.es/proposal-temporal/" ] }, + { + "featureId": "text-modules", + "featureName": "Text import attributes", + "baselineStatus": false, + "snapshot": [], + "group": [ + "js-modules" + ], + "hasCompatRows": true, + "spec": [ + "https://tc39.es/proposal-import-text/" + ] + }, { "featureId": "top-level-await", "featureName": "Top-level await", - "baselineStatus": false, + "baselineStatus": "low", + "baselineLowDate": "2026-09-14", "snapshot": [], "group": [ "javascript" @@ -1778,8 +1835,9 @@ { "featureId": "transferable-arraybuffer", "featureName": "Transferable ArrayBuffer", - "baselineStatus": "low", + "baselineStatus": "high", "baselineLowDate": "2024-03-05", + "baselineHighDate": "2026-09-05", "snapshot": [], "group": [ "javascript" @@ -2380,8 +2438,9 @@ "compatKey": "javascript.builtins.Array.fromAsync", "featureId": "array-fromasync", "featureName": "Array.fromAsync()", - "baselineStatus": "low", + "baselineStatus": "high", "baselineLowDate": "2024-01-25", + "baselineHighDate": "2026-07-25", "snapshot": [], "group": [ "arrays" @@ -2988,8 +3047,9 @@ "compatKey": "javascript.builtins.ArrayBuffer.detached", "featureId": "transferable-arraybuffer", "featureName": "Transferable ArrayBuffer", - "baselineStatus": "low", + "baselineStatus": "high", "baselineLowDate": "2024-03-05", + "baselineHighDate": "2026-09-05", "snapshot": [], "group": [ "javascript" @@ -3078,8 +3138,9 @@ "compatKey": "javascript.builtins.ArrayBuffer.transfer", "featureId": "transferable-arraybuffer", "featureName": "Transferable ArrayBuffer", - "baselineStatus": "low", + "baselineStatus": "high", "baselineLowDate": "2024-03-05", + "baselineHighDate": "2026-09-05", "snapshot": [], "group": [ "javascript" @@ -3092,8 +3153,9 @@ "compatKey": "javascript.builtins.ArrayBuffer.transferToFixedLength", "featureId": "transferable-arraybuffer", "featureName": "Transferable ArrayBuffer", - "baselineStatus": "low", + "baselineStatus": "high", "baselineLowDate": "2024-03-05", + "baselineHighDate": "2026-09-05", "snapshot": [], "group": [ "javascript" @@ -5670,7 +5732,8 @@ "compatKey": "javascript.builtins.FinalizationRegistry.unregister.symbol_as_target", "featureId": "weak-references", "featureName": "Weak references", - "baselineStatus": false, + "baselineStatus": "low", + "baselineLowDate": "2025-12-09", "snapshot": [ "ecmascript-2021" ], @@ -7285,7 +7348,8 @@ "compatKey": "javascript.builtins.Intl.Locale.getCalendars", "featureId": "intl-locale-info", "featureName": "Intl.Locale info", - "baselineStatus": false, + "baselineStatus": "low", + "baselineLowDate": "2026-07-21", "snapshot": [], "group": [ "intl" @@ -7298,7 +7362,8 @@ "compatKey": "javascript.builtins.Intl.Locale.getCollations", "featureId": "intl-locale-info", "featureName": "Intl.Locale info", - "baselineStatus": false, + "baselineStatus": "low", + "baselineLowDate": "2026-07-21", "snapshot": [], "group": [ "intl" @@ -7311,7 +7376,8 @@ "compatKey": "javascript.builtins.Intl.Locale.getHourCycles", "featureId": "intl-locale-info", "featureName": "Intl.Locale info", - "baselineStatus": false, + "baselineStatus": "low", + "baselineLowDate": "2026-07-21", "snapshot": [], "group": [ "intl" @@ -7324,7 +7390,8 @@ "compatKey": "javascript.builtins.Intl.Locale.getNumberingSystems", "featureId": "intl-locale-info", "featureName": "Intl.Locale info", - "baselineStatus": false, + "baselineStatus": "low", + "baselineLowDate": "2026-07-21", "snapshot": [], "group": [ "intl" @@ -7337,7 +7404,8 @@ "compatKey": "javascript.builtins.Intl.Locale.getTextInfo", "featureId": "intl-locale-info", "featureName": "Intl.Locale info", - "baselineStatus": false, + "baselineStatus": "low", + "baselineLowDate": "2026-07-21", "snapshot": [], "group": [ "intl" @@ -7350,7 +7418,8 @@ "compatKey": "javascript.builtins.Intl.Locale.getTimeZones", "featureId": "intl-locale-info", "featureName": "Intl.Locale info", - "baselineStatus": false, + "baselineStatus": "low", + "baselineLowDate": "2026-07-21", "snapshot": [], "group": [ "intl" @@ -7363,7 +7432,8 @@ "compatKey": "javascript.builtins.Intl.Locale.getWeekInfo", "featureId": "intl-locale-info", "featureName": "Intl.Locale info", - "baselineStatus": false, + "baselineStatus": "low", + "baselineLowDate": "2026-07-21", "snapshot": [], "group": [ "intl" @@ -8656,6 +8726,19 @@ "javascript.builtins.Iterator.from" ] }, + { + "compatKey": "javascript.builtins.Iterator.join", + "featureId": "iterator-join", + "featureName": "Iterator join()", + "baselineStatus": false, + "snapshot": [], + "group": [ + "iterators" + ], + "sourceRefs": [ + "javascript.builtins.Iterator.join" + ] + }, { "compatKey": "javascript.builtins.Iterator.map", "featureId": "iterator-methods", @@ -9062,8 +9145,9 @@ "compatKey": "javascript.builtins.Map.groupBy", "featureId": "array-group", "featureName": "Array grouping", - "baselineStatus": "low", + "baselineStatus": "high", "baselineLowDate": "2024-03-05", + "baselineHighDate": "2026-09-05", "snapshot": [], "group": [ "maps" @@ -10492,8 +10576,9 @@ "compatKey": "javascript.builtins.Object.groupBy", "featureId": "array-group", "featureName": "Array grouping", - "baselineStatus": "low", + "baselineStatus": "high", "baselineLowDate": "2024-03-05", + "baselineHighDate": "2026-09-05", "snapshot": [], "group": [ "maps" @@ -11033,8 +11118,9 @@ "compatKey": "javascript.builtins.Promise.withResolvers", "featureId": "promise-withresolvers", "featureName": "Promise.withResolvers()", - "baselineStatus": "low", + "baselineStatus": "high", "baselineLowDate": "2024-03-05", + "baselineHighDate": "2026-09-05", "snapshot": [], "group": [ "promises" @@ -13533,6 +13619,32 @@ "javascript.builtins.String.trimEnd" ] }, + { + "compatKey": "javascript.builtins.String.trimLeft", + "featureId": "string-trim-leftright", + "featureName": "String trimLeft() and trimRight()", + "baselineStatus": false, + "snapshot": [], + "group": [ + "string" + ], + "sourceRefs": [ + "javascript.builtins.String.trimLeft" + ] + }, + { + "compatKey": "javascript.builtins.String.trimRight", + "featureId": "string-trim-leftright", + "featureName": "String trimLeft() and trimRight()", + "baselineStatus": false, + "snapshot": [], + "group": [ + "string" + ], + "sourceRefs": [ + "javascript.builtins.String.trimRight" + ] + }, { "compatKey": "javascript.builtins.String.trimStart", "featureId": "string-trim-startend", @@ -19859,7 +19971,8 @@ "compatKey": "javascript.operators.await.top_level", "featureId": "top-level-await", "featureName": "Top-level await", - "baselineStatus": false, + "baselineStatus": "low", + "baselineLowDate": "2026-09-14", "snapshot": [], "group": [ "javascript" @@ -20323,6 +20436,19 @@ "javascript.operators.import" ] }, + { + "compatKey": "javascript.operators.import.defer", + "featureId": "import-defer", + "featureName": "import defer", + "baselineStatus": false, + "snapshot": [], + "group": [ + "js-modules" + ], + "sourceRefs": [ + "javascript.operators.import.defer" + ] + }, { "compatKey": "javascript.operators.import.worker_support", "featureId": "js-modules-workers", @@ -21908,6 +22034,19 @@ "javascript.statements.import.arbitrary_module_namespace_identifier_names" ] }, + { + "compatKey": "javascript.statements.import.defer", + "featureId": "import-defer", + "featureName": "import defer", + "baselineStatus": false, + "snapshot": [], + "group": [ + "js-modules" + ], + "sourceRefs": [ + "javascript.statements.import.defer" + ] + }, { "compatKey": "javascript.statements.import.import_assertions", "featureId": "import-assertions", @@ -21988,6 +22127,19 @@ "javascript.statements.import.import_attributes.type_json" ] }, + { + "compatKey": "javascript.statements.import.import_attributes.type_text", + "featureId": "text-modules", + "featureName": "Text import attributes", + "baselineStatus": false, + "snapshot": [], + "group": [ + "js-modules" + ], + "sourceRefs": [ + "javascript.statements.import.import_attributes.type_text" + ] + }, { "compatKey": "javascript.statements.import.service_worker_support", "featureId": "js-modules-service-workers", diff --git a/deploy/createPackage.mjs b/deploy/createPackage.mjs index 9c80c1c..819347f 100644 --- a/deploy/createPackage.mjs +++ b/deploy/createPackage.mjs @@ -38,6 +38,7 @@ function parseArgs(argv) { for (let index = 0; index < argv.length; index++) { const current = argv[index]; + switch (current) { case "--package": args.package = requireValue(argv[++index], current); @@ -68,6 +69,7 @@ function requireValue(value, flagName) { if (!value) { throw new Error(`Missing value for ${flagName}`); } + return value; } diff --git a/deploy/createPackageTarball.mjs b/deploy/createPackageTarball.mjs index b84bd7b..e3e8974 100644 --- a/deploy/createPackageTarball.mjs +++ b/deploy/createPackageTarball.mjs @@ -16,6 +16,7 @@ const summaries = await createPackageStages({ /** @type {Array>} */ const tarballSummaries = []; + for (const summary of summaries) { const tarballPath = await createPackageTarball(summary.stageDirectory); tarballSummaries.push({ @@ -45,6 +46,7 @@ function parseArgs(argv) { for (let index = 0; index < argv.length; index++) { const current = argv[index]; + switch (current) { case "--package": args.package = requireValue(argv[++index], current); @@ -75,6 +77,7 @@ function requireValue(value, flagName) { if (!value) { throw new Error(`Missing value for ${flagName}`); } + return value; } diff --git a/deploy/package-lib.mjs b/deploy/package-lib.mjs index be9a479..e4f48a0 100644 --- a/deploy/package-lib.mjs +++ b/deploy/package-lib.mjs @@ -14,6 +14,8 @@ import { import os from "node:os"; import path from "node:path"; import { retryAsync, retrySync } from "../lib/net-retry.mjs"; +import { hasDeclarationSurface } from "../lib/resolution-kinds.mjs"; +import { compareStringsCaseSensitive } from "../lib/shared.mjs"; import { packages, repoRoot } from "./package-registry.mjs"; import { resolveReleaseExecutable } from "./trusted-executable.mjs"; @@ -29,11 +31,13 @@ export async function createPackageStages(options = {}) { if (options.versionOverride !== undefined && options.versionBump !== undefined) { throw new Error("Package staging accepts either versionOverride or versionBump, not both"); } + const selectedPackages = selectPackages(options.packageId); const snapshot = await readCurrentSnapshot(); /** @type {PackageStageSummary[]} */ const summaries = []; + for (const packageConfig of selectedPackages) { summaries.push(await createPackageStage( packageConfig, @@ -47,37 +51,20 @@ 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}`); - } + const delivered = hasDeclarationSurface(row.resolutionKind); + if (row.includeInTarget && delivered) { count++; } } + return count; } @@ -87,28 +74,37 @@ export function countIncludedCompatRows(classifiedCompatRows) { */ 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}`); } @@ -128,6 +124,7 @@ export async function collectReleasePlans(options = {}) { /** @type {ReleasePlan[]} */ const releasePlans = []; + for (const stageSummary of stageSummaries) { releasePlans.push(await buildReleasePlan(stageSummary)); } @@ -158,6 +155,7 @@ async function createPackageStage(packageConfig, snapshot, versionOverride, vers const packageVersion = versionOverride ?? await resolveNextPackageVersion(packageConfig, versionBump); const includedCompatCount = countIncludedCompatRows(snapshot.classification.classifiedCompatRows); + const snapshotMetadata = { schemaVersion: 1, baselineDate: snapshot.manifest.snapshot.baselineDate, @@ -167,6 +165,7 @@ async function createPackageStage(packageConfig, snapshot, versionOverride, vers includedCompatCount, generatorVersion: snapshot.manifest.snapshot.generatorVersion, }; + const packageJson = { name: packageConfig.name, version: packageVersion, @@ -262,7 +261,7 @@ async function buildReleasePlan(stageSummary) { const changedFiles = []; const unchangedFiles = []; - const stagedPaths = [...stagedSnapshot.keys()].sort(compareStrings); + const stagedPaths = [...stagedSnapshot.keys()].sort(compareStringsCaseSensitive); const publishedPaths = new Set(published.snapshot.keys()); for (const relativePath of stagedPaths) { @@ -272,10 +271,11 @@ async function buildReleasePlan(stageSummary) { else { unchangedFiles.push(relativePath); } + publishedPaths.delete(relativePath); } - const removedFiles = [...publishedPaths].sort(compareStrings); + const removedFiles = [...publishedPaths].sort(compareStringsCaseSensitive); assertNoRemovedAllowEntries(removedFiles); assertAllowEntryContractsPreserved( published.snapshot.get("reports/generation.json"), @@ -308,6 +308,7 @@ async function buildReleasePlan(stageSummary) { */ export function assertNoRemovedAllowEntries(removedFiles) { const removedEntries = removedFiles.filter(relativePath => /^allow\/[^/]+\/index\.d\.ts$/.test(relativePath)); + if (removedEntries.length) { throw new Error(`Published allow entry paths cannot be removed: ${removedEntries.join(", ")}`); } @@ -318,6 +319,7 @@ export function assertNoRemovedAllowEntries(removedFiles) { */ export function assertNoRemovedYearEntryPoints(removedFiles) { const removedEntryPoints = removedFiles.filter(relativePath => /^year\/\d{4}\/index\.d\.ts$/.test(relativePath)); + if (removedEntryPoints.length) { throw new Error(`Published Baseline year entrypoints cannot be removed: ${removedEntryPoints.join(", ")}`); } @@ -331,14 +333,17 @@ export function assertAllowEntryContractsPreserved(publishedReportText, stagedRe if (!publishedReportText) { return; } + if (!stagedReportText) { throw new Error("The staged package is missing reports/generation.json"); } const publishedEntries = readAllowEntryContracts(publishedReportText, "published"); const stagedEntries = readAllowEntryContracts(stagedReportText, "staged"); + for (const [entryName, publishedCompatKeys] of publishedEntries) { const stagedCompatKeys = stagedEntries.get(entryName); + if (!stagedCompatKeys || JSON.stringify(stagedCompatKeys) !== JSON.stringify(publishedCompatKeys)) { throw new Error(`Published allow entry contract changed: allow/${entryName}`); } @@ -351,6 +356,7 @@ export function assertAllowEntryContractsPreserved(publishedReportText, stagedRe */ export function incrementPackageVersion(currentVersion, bump) { const current = parseVersion(currentVersion); + switch (bump) { case "major": return `${current.major + 1}.0.0`; @@ -371,25 +377,30 @@ export function incrementPackageVersion(currentVersion, bump) { function parseVersion(value) { const numericIdentifier = "(?:0|[1-9]\\d*)"; const dotSeparatedIdentifiers = "[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*"; + const match = new RegExp( `^(${numericIdentifier})\\.(${numericIdentifier})\\.(${numericIdentifier})` + `(?:-(${dotSeparatedIdentifiers}))?(?:\\+${dotSeparatedIdentifiers})?$`, ).exec(value); + if ( !match || match[4]?.split(".").some(identifier => /^\d+$/.test(identifier) && identifier.length > 1 && identifier[0] === "0") ) { throw new Error(`Unsupported package version format: ${value}`); } + const parsed = { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3]), prerelease: match[4], }; + if (![parsed.major, parsed.minor, parsed.patch].every(Number.isSafeInteger)) { throw new Error(`Unsupported package version format: ${value}`); } + return parsed; } @@ -401,6 +412,7 @@ function readAllowEntryContracts(reportText, label) { /** @type {{ allowEntries?: Array<{ entryName?: unknown; compatKeys?: unknown; }>; }} */ const report = JSON.parse(reportText); const contracts = new Map(); + for (const entry of report.allowEntries ?? []) { if ( typeof entry.entryName !== "string" @@ -410,8 +422,10 @@ function readAllowEntryContracts(reportText, label) { ) { throw new Error(`Invalid ${label} allow entry contract report`); } - contracts.set(entry.entryName, [...entry.compatKeys].sort(compareStrings)); + + contracts.set(entry.entryName, [...entry.compatKeys].sort(compareStringsCaseSensitive)); } + return contracts; } @@ -421,11 +435,13 @@ function readAllowEntryContracts(reportText, label) { */ async function resolveNextPackageVersion(packageConfig, bump) { const currentVersion = await getLatestPublishedVersion(packageConfig.name); + if (!currentVersion) { return bump === "patch" ? packageConfig.initialVersion : incrementPackageVersion("0.0.0", bump); } + return incrementPackageVersion(currentVersion, bump); } @@ -434,6 +450,7 @@ async function resolveNextPackageVersion(packageConfig, bump) { */ async function getLatestPublishedVersion(packageName) { const metadata = await fetchPackageMetadata(packageName); + return metadata?.["dist-tags"]?.latest; } @@ -442,6 +459,7 @@ async function getLatestPublishedVersion(packageName) { */ async function getPublishedPackageState(packageConfig) { const version = await getLatestPublishedVersion(packageConfig.name); + if (!version) { return { version: undefined, @@ -451,15 +469,19 @@ async function getPublishedPackageState(packageConfig) { } const tempDirectory = await mkdtemp(path.join(os.tmpdir(), "ts-baseline-published-package-")); + try { // Fetching the published tarball is read-only, so it's safe to retry through registry flake. const npm = resolveReleaseExecutable(repoRoot, "RELEASE_NPM_EXECUTABLE", "npm"); + const packOutput = retrySync(`npm pack ${packageConfig.name}@${version}`, () => execFileSync(npm.executable, ["pack", `${packageConfig.name}@${version}`, "--silent"], { cwd: tempDirectory, encoding: "utf8", env: npm.environment, })).trim(); + const tarballName = packOutput.split(/\r?\n/).filter(Boolean).at(-1); + if (!tarballName) { throw new Error(`npm pack did not return a tarball name for ${packageConfig.name}@${version}`); } @@ -473,6 +495,7 @@ async function getPublishedPackageState(packageConfig) { const packageDirectory = path.join(tempDirectory, "package"); const snapshot = await readComparablePackageSnapshot(packageDirectory); + return { version, snapshot, @@ -493,17 +516,22 @@ async function fetchPackageMetadata(packageName) { // A 404 means "not published yet" — a normal case, so don't retry it. const response = await retryAsync(`fetch npm metadata for ${packageName}`, async () => { const result = await fetch(`https://registry.npmjs.org/${encodeURIComponent(packageName)}`); + if (!result.ok && result.status !== 404) { throw new Error(`registry returned ${result.status} ${result.statusText}`); } + return result; }); + if (response.status === 404) { return undefined; } + if (!response.ok) { throw new Error(`Failed to fetch npm metadata for ${packageName}: ${response.status} ${response.statusText}`); } + return /** @type {Promise} */ (response.json()); } @@ -515,6 +543,7 @@ export async function createPackageTarball(directoryPath) { await mkdir(tarballRoot, { recursive: true }); const tarballDirectory = await mkdtemp(path.join(tarballRoot, "pack-")); const npm = resolveReleaseExecutable(repoRoot, "RELEASE_NPM_EXECUTABLE", "npm"); + const packOutput = execFileSync(npm.executable, ["pack", directoryPath, "--pack-destination", tarballDirectory, "--silent"], { cwd: repoRoot, encoding: "utf8", @@ -523,10 +552,13 @@ export async function createPackageTarball(directoryPath) { npm_config_cache: path.join(tarballDirectory, "npm-cache"), }, }).trim(); + const tarballName = packOutput.split(/\r?\n/).filter(Boolean).at(-1); + if (!tarballName) { throw new Error(`npm pack did not return a tarball name for ${directoryPath}`); } + return path.join(tarballDirectory, tarballName); } @@ -536,16 +568,20 @@ export async function createPackageTarball(directoryPath) { async function readComparablePackageSnapshot(packageDirectory) { /** @type {Map} */ const snapshot = new Map(); + for (const relativePath of await listFilesRecursively(packageDirectory)) { const fullPath = path.join(packageDirectory, relativePath); + if (relativePath === "package.json") { const packageJson = JSON.parse(await readFile(fullPath, "utf8")); delete packageJson.version; snapshot.set(relativePath, `${JSON.stringify(packageJson, undefined, 2)}\n`); continue; } + snapshot.set(relativePath, normalizeLineEndings(await readFile(fullPath, "utf8"))); } + return snapshot; } @@ -562,9 +598,11 @@ async function listFilesRecursively(directoryPath) { */ async function visit(currentDirectory, currentRelativeDirectory) { const entries = await readdir(currentDirectory, { withFileTypes: true }); + for (const entry of entries) { const fullPath = path.join(currentDirectory, entry.name); const relativePath = currentRelativeDirectory ? path.join(currentRelativeDirectory, entry.name) : entry.name; + if (entry.isDirectory()) { await visit(fullPath, relativePath); } @@ -575,7 +613,8 @@ async function listFilesRecursively(directoryPath) { } await visit(directoryPath, ""); - return relativePaths.sort(compareStrings); + + return relativePaths.sort(compareStringsCaseSensitive); } async function readCurrentSnapshot() { @@ -628,6 +667,7 @@ function renderReleaseNotes(options) { changedFiles, removedFiles, } = options; + const { packageConfig, packageVersion, snapshot } = stageSummary; const fileLines = changedFiles.length @@ -662,12 +702,14 @@ function renderReleaseNotes(options) { */ function hashComparableSnapshot(snapshot) { const hash = createHash("sha256"); - for (const relativePath of [...snapshot.keys()].sort(compareStrings)) { + + for (const relativePath of [...snapshot.keys()].sort(compareStringsCaseSensitive)) { hash.update(relativePath); hash.update("\0"); hash.update(snapshot.get(relativePath) ?? ""); hash.update("\0"); } + return `sha256-${hash.digest("hex")}`; } @@ -677,9 +719,11 @@ function hashComparableSnapshot(snapshot) { */ function renderTemplate(template, replacements) { let rendered = template; + for (const [name, value] of Object.entries(replacements)) { rendered = rendered.replaceAll(`{{${name}}}`, value); } + return rendered; } @@ -697,10 +741,13 @@ function selectPackages(packageId) { if (!packageId) { return packages; } + const selectedPackage = packages.find(packageConfig => packageConfig.id === packageId); + if (!selectedPackage) { throw new Error(`Unknown package id: ${packageId}`); } + return [selectedPackage]; } @@ -712,15 +759,8 @@ function getStageDirectory(packageConfig, stageDirectoryRoot) { if (!stageDirectoryRoot) { return packageConfig.stageDirectory; } - return path.join(stageDirectoryRoot, packageConfig.id); -} -/** - * @param {string} left - * @param {string} right - */ -function compareStrings(left, right) { - return left.localeCompare(right); + return path.join(stageDirectoryRoot, packageConfig.id); } /** diff --git a/deploy/package-registry.mjs b/deploy/package-registry.mjs index 24e47b6..abd2cc2 100644 --- a/deploy/package-registry.mjs +++ b/deploy/package-registry.mjs @@ -6,6 +6,7 @@ import { fileURLToPath } from "node:url"; const deployDirectory = path.dirname(fileURLToPath(import.meta.url)); export const repoRoot = path.resolve(deployDirectory, ".."); + export const deployGeneratedDirectory = path.join(deployDirectory, "generated"); export const baselinePackage = { diff --git a/deploy/prepareReleaseArtifact.mjs b/deploy/prepareReleaseArtifact.mjs index c403a59..1579268 100644 --- a/deploy/prepareReleaseArtifact.mjs +++ b/deploy/prepareReleaseArtifact.mjs @@ -15,10 +15,15 @@ import { } from "./release-artifact.mjs"; const deployDirectory = path.dirname(fileURLToPath(import.meta.url)); + const repoRoot = path.resolve(deployDirectory, ".."); + const args = parseArgs(process.argv.slice(2)); + assertCleanWorktree(repoRoot); + const sourceCommit = readHeadCommit(repoRoot); + if (process.env.GITHUB_SHA && process.env.GITHUB_SHA !== sourceCommit) { throw new Error(`Checked-out commit ${sourceCommit} does not match GITHUB_SHA ${process.env.GITHUB_SHA}`); } @@ -27,25 +32,32 @@ const releasePlans = await collectReleasePlans({ packageId: "baseline", versionBump: args.bump, }); + if (releasePlans.length !== 1) { throw new Error(`Expected one release plan, got ${releasePlans.length}`); } + const [releasePlan] = releasePlans; + const tarballPath = releasePlan.changed ? await createPackageTarball(releasePlan.stageDirectory) : undefined; + const plan = await writePreparedReleaseArtifact({ outputDirectory: args.outputDirectory, sourceCommit, releasePlan, tarballPath, }); + const artifactIntegrity = await hashPreparedReleaseArtifact(args.outputDirectory, plan.changed); + if (process.env.GITHUB_OUTPUT) { await appendFile(process.env.GITHUB_OUTPUT, `artifact-integrity=${artifactIntegrity}\n`); } console.log(`Prepared: ${plan.changed ? `${plan.packageName}@${plan.packageVersion}` : "no package changes"}`); + console.log(`Artifact: ${args.outputDirectory}`); /** @@ -56,22 +68,28 @@ function parseArgs(argv) { const parsed = { outputDirectory: path.join(repoRoot, "release-artifact"), }; + for (let index = 0; index < argv.length; index++) { const current = argv[index]; + if (current === "--bump") { const value = requireValue(argv[++index], current); + if (value !== "major" && value !== "minor" && value !== "patch") { throw new Error(`Unsupported release bump: ${value}`); } + parsed.bump = value; } else { throw new Error(`Unknown argument: ${current}`); } } + if (!parsed.bump) { throw new Error("Release preparation requires --bump major, minor, or patch"); } + return parsed; } @@ -83,5 +101,6 @@ function requireValue(value, flag) { if (!value) { throw new Error(`Missing value for ${flag}`); } + return value; } diff --git a/deploy/publishReleaseArtifact.mjs b/deploy/publishReleaseArtifact.mjs index 761fd94..a26828a 100644 --- a/deploy/publishReleaseArtifact.mjs +++ b/deploy/publishReleaseArtifact.mjs @@ -12,23 +12,33 @@ import { import { resolveReleaseExecutable } from "./trusted-executable.mjs"; assertReleaseWorkflowContext(process.env); + const args = parseArgs(process.argv.slice(2)); + const { plan, tarballPath, notesMarkdown } = await readPreparedReleaseArtifact(args.artifactDirectory); + const artifactIntegrity = await hashPreparedReleaseArtifact(args.artifactDirectory, plan.changed); + if (!process.env.EXPECTED_ARTIFACT_INTEGRITY || artifactIntegrity !== process.env.EXPECTED_ARTIFACT_INTEGRITY) { throw new Error("Downloaded release artifact does not match the verified build output"); } + if (!plan.changed) { throw new Error("Prepared release contains no package changes"); } + if (!process.env.GITHUB_SHA || process.env.GITHUB_SHA !== plan.sourceCommit) { throw new Error(`Release artifact commit ${plan.sourceCommit} does not match GITHUB_SHA ${process.env.GITHUB_SHA ?? ""}`); } const metadata = await readPackageMetadata(plan.packageName); + const publishedVersion = metadata?.["dist-tags"]?.latest ?? null; + const existingVersion = metadata?.versions?.[plan.packageVersion]; + let published = false; + if (existingVersion) { if (existingVersion.dist?.integrity !== plan.tarballIntegrity) { throw new Error(`${plan.packageName}@${plan.packageVersion} already exists with different integrity`); @@ -40,10 +50,13 @@ else { `npm latest changed after verification: expected ${plan.publishedVersion ?? "none"}, got ${publishedVersion ?? "none"}`, ); } + const publishArgs = ["publish", tarballPath, "--access", "public"]; + if (args.provenance) { publishArgs.push("--provenance"); } + const npm = resolveReleaseExecutable(repoRoot, "RELEASE_NPM_EXECUTABLE", "npm"); execFileSync(npm.executable, publishArgs, { stdio: "inherit", @@ -58,6 +71,7 @@ await createGitHubRelease({ sourceCommit: plan.sourceCommit, notesMarkdown, }); + console.log(`${published ? "Published" : "Verified existing"}: ${plan.packageName}@${plan.packageVersion}`); /** @@ -65,12 +79,15 @@ console.log(`${published ? "Published" : "Verified existing"}: ${plan.packageNam */ async function readPackageMetadata(packageName) { const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(packageName)}`); + if (response.status === 404) { return undefined; } + if (!response.ok) { throw new Error(`npm registry returned ${response.status} ${response.statusText}`); } + return /** @type {Promise} */ (response.json()); } @@ -80,10 +97,13 @@ async function readPackageMetadata(packageName) { async function createGitHubRelease(options) { const repository = process.env.GITHUB_REPOSITORY; const token = process.env.GITHUB_TOKEN; + if (!repository || !token) { throw new Error("GITHUB_REPOSITORY and GITHUB_TOKEN are required to create the release"); } + const tag = `${options.packageName}@${options.packageVersion}`; + const response = await fetch(`https://api.github.com/repos/${repository}/releases`, { method: "POST", headers: githubHeaders(token), @@ -94,19 +114,24 @@ async function createGitHubRelease(options) { body: options.notesMarkdown, }), }); + if (response.ok) { return; } + if (response.status === 422) { const existing = await fetch( `https://api.github.com/repos/${repository}/releases/tags/${encodeURIComponent(tag)}`, { headers: githubHeaders(token) }, ); + if (existing.ok) { assertExistingGitHubRelease(await existing.json(), tag, options.sourceCommit); + return; } } + throw new Error(`GitHub release creation failed: ${response.status} ${response.statusText}`); } @@ -131,13 +156,17 @@ function parseArgs(argv) { artifactDirectory: path.resolve("release-artifact"), provenance: false, }; + for (let index = 0; index < argv.length; index++) { const current = argv[index]; + if (current === "--artifact-dir") { const value = argv[++index]; + if (!value) { throw new Error("Missing value for --artifact-dir"); } + parsed.artifactDirectory = path.resolve(value); } else if (current === "--provenance") { @@ -147,5 +176,6 @@ function parseArgs(argv) { throw new Error(`Unknown argument: ${current}`); } } + return parsed; } diff --git a/deploy/readmes/baseline.md b/deploy/readmes/baseline.md index 740bbcf..7731154 100644 --- a/deploy/readmes/baseline.md +++ b/deploy/readmes/baseline.md @@ -57,10 +57,10 @@ The package does not expose unavailable runtime APIs to support third-party pack ## Allow a polyfilled feature -If the runtime loads an audited polyfill, add its generated `web-features` entry after the base package. For example, core-js can provide `Promise.withResolvers`: +If the runtime loads an audited polyfill, add its generated `web-features` entry after the base package. For example, core-js can provide `Promise.try`: ```ts -import "core-js/proposals/promise-with-resolvers"; +import "core-js/proposals/promise-try"; ``` ```json @@ -69,7 +69,7 @@ import "core-js/proposals/promise-with-resolvers"; "noLib": true, "types": [ "{{PACKAGE_NAME}}", - "{{PACKAGE_NAME}}/allow/promise-withresolvers" + "{{PACKAGE_NAME}}/allow/promise-try" ] } } diff --git a/deploy/release-artifact.mjs b/deploy/release-artifact.mjs index d13b7d1..c68a38e 100644 --- a/deploy/release-artifact.mjs +++ b/deploy/release-artifact.mjs @@ -20,7 +20,9 @@ import { repoRoot } from "./package-registry.mjs"; import { resolveReleaseExecutable } from "./trusted-executable.mjs"; const PLAN_FILE_NAME = "release-plan.json"; + const NOTES_FILE_NAME = "release-notes.md"; + const TARBALL_FILE_NAME = "package.tgz"; /** @@ -28,16 +30,20 @@ const TARBALL_FILE_NAME = "package.tgz"; */ export function assertCleanWorktree(repoRoot) { const worktreeStatus = getGitStatus(repoRoot, ["--untracked-files=all"]); + const ignoredGeneratedStatus = getGitStatus(repoRoot, [ "--untracked-files=all", "--ignored=matching", "--", "generated/current", ]); + const trackedFileState = runGit(repoRoot, ["ls-files", "-v"]); + const hasHiddenIndexState = trackedFileState .split("\n") .some(line => line && line[0] !== "H"); + if (worktreeStatus || ignoredGeneratedStatus || hasHiddenIndexState || hasRawTrackedChanges(repoRoot)) { throw new Error("Release artifacts require a clean worktree"); } @@ -55,6 +61,7 @@ export function readHeadCommit(repoRoot) { */ function hasRawTrackedChanges(repoRoot) { const objectFormat = runGit(repoRoot, ["rev-parse", "--show-object-format"]).trim(); + if (objectFormat !== "sha1" && objectFormat !== "sha256") { throw new Error(`Unsupported Git object format: ${objectFormat}`); } @@ -62,37 +69,48 @@ function hasRawTrackedChanges(repoRoot) { const entries = runGit(repoRoot, ["ls-tree", "-r", "-z", "HEAD"]) .split("\0") .filter(Boolean); + for (const entry of entries) { const match = /^(100644|100755|120000) blob ([0-9a-f]+)\t([\s\S]+)$/u.exec(entry); + if (!match) { throw new Error(`Unsupported tracked Git entry: ${entry}`); } + const [, mode, expectedHash, relativePath] = match; const filePath = path.join(repoRoot, relativePath); + if (!existsSync(filePath) && mode !== "120000") { return true; } + const fileStats = lstatSync(filePath, { throwIfNoEntry: false }); + if (!fileStats) { return true; } + if ( (mode === "120000" && !fileStats.isSymbolicLink()) || (mode !== "120000" && !fileStats.isFile()) ) { return true; } + const contents = mode === "120000" ? Buffer.from(readlinkSync(filePath)) : readFileSync(filePath); + const actualHash = createHash(objectFormat) .update(`blob ${contents.length}\0`) .update(contents) .digest("hex"); + if (actualHash !== expectedHash) { return true; } } + return false; } @@ -110,6 +128,7 @@ function getGitStatus(repoRoot, args) { */ function runGit(repoRoot, args) { const git = resolveReleaseExecutable(repoRoot, "RELEASE_GIT_EXECUTABLE", "git"); + const result = spawnSync(git.executable, args, { cwd: repoRoot, encoding: "utf8", @@ -118,12 +137,15 @@ function runGit(repoRoot, args) { GIT_NO_REPLACE_OBJECTS: "1", }, }); + if (result.error) { throw result.error; } + if (result.status !== 0) { throw new Error(`git ${args[0]} failed with exit code ${result.status}`); } + return result.stdout; } @@ -176,16 +198,20 @@ export function assertReleaseWorkflowContext(environment) { */ export async function writePreparedReleaseArtifact(options) { const { releasePlan } = options; + if (releasePlan.changed !== Boolean(options.tarballPath)) { throw new Error("A changed release plan must have exactly one package tarball"); } + await rm(options.outputDirectory, { recursive: true, force: true }); await mkdir(options.outputDirectory, { recursive: true }); const targetTarballPath = path.join(options.outputDirectory, TARBALL_FILE_NAME); + const tarballIntegrity = options.tarballPath ? await copyAndHashTarball(options.tarballPath, targetTarballPath) : null; + const plan = { schemaVersion: 2, sourceCommit: options.sourceCommit, @@ -195,12 +221,14 @@ export async function writePreparedReleaseArtifact(options) { publishedVersion: releasePlan.publishedVersion ?? null, tarballIntegrity, }; + validateReleasePlan(plan); await writeFile( path.join(options.outputDirectory, PLAN_FILE_NAME), `${JSON.stringify(plan, undefined, 2)}\n`, ); await writeFile(path.join(options.outputDirectory, NOTES_FILE_NAME), releasePlan.notesMarkdown); + return plan; } @@ -211,16 +239,21 @@ export async function readPreparedReleaseArtifact(artifactDirectory) { const plan = JSON.parse(await readFile(path.join(artifactDirectory, PLAN_FILE_NAME), "utf8")); validateReleasePlan(plan); const tarballPath = path.join(artifactDirectory, TARBALL_FILE_NAME); + if (plan.changed) { const integrity = await hashFile(tarballPath); + if (integrity !== plan.tarballIntegrity) { throw new Error(`Release tarball integrity mismatch: expected ${plan.tarballIntegrity}, got ${integrity}`); } + const tar = resolveReleaseExecutable(repoRoot, "RELEASE_TAR_EXECUTABLE", "tar"); + const packageJson = JSON.parse(execFileSync(tar.executable, ["-xOf", tarballPath, "package/package.json"], { encoding: "utf8", env: tar.environment, })); + if (packageJson.name !== plan.packageName || packageJson.version !== plan.packageVersion) { throw new Error( `Release tarball contains ${String(packageJson.name)}@${String(packageJson.version)}; ` @@ -228,6 +261,7 @@ export async function readPreparedReleaseArtifact(artifactDirectory) { ); } } + return { plan, tarballPath, @@ -242,13 +276,16 @@ export async function readPreparedReleaseArtifact(artifactDirectory) { export async function hashPreparedReleaseArtifact(artifactDirectory, changed) { const hash = createHash("sha512"); const fileNames = [PLAN_FILE_NAME, NOTES_FILE_NAME]; + if (changed) { fileNames.push(TARBALL_FILE_NAME); } + for (const fileName of fileNames) { const contents = await readFile(path.join(artifactDirectory, fileName)); hash.update(`${fileName}\0${contents.length}\0`).update(contents); } + return `sha512-${hash.digest("base64")}`; } @@ -258,6 +295,7 @@ export async function hashPreparedReleaseArtifact(artifactDirectory, changed) { */ async function copyAndHashTarball(sourcePath, destinationPath) { await copyFile(sourcePath, destinationPath); + return hashFile(destinationPath); } @@ -281,6 +319,7 @@ function validateReleasePlan(plan) { "sourceCommit", "tarballIntegrity", ]; + if ( !plan || typeof plan !== "object" diff --git a/deploy/trusted-executable.mjs b/deploy/trusted-executable.mjs index 963700a..4310aff 100644 --- a/deploy/trusted-executable.mjs +++ b/deploy/trusted-executable.mjs @@ -9,6 +9,7 @@ import path from "node:path"; */ export function resolveReleaseExecutable(repoRoot, environmentVariable, defaultExecutable) { const configuredExecutable = process.env[environmentVariable]; + if ( configuredExecutable && (!path.isAbsolute(configuredExecutable) || isWithinDirectory(configuredExecutable, repoRoot)) @@ -29,7 +30,9 @@ export function resolveReleaseExecutable(repoRoot, environmentVariable, defaultE && !isWithinDirectory(entry, repoRoot) && !/[\\/]node_modules[\\/]\.bin(?:$|[\\/])/u.test(entry) ); + const safePath = [...new Set(safePathEntries)].join(path.delimiter); + return { executable: configuredExecutable ?? defaultExecutable, environment: { @@ -45,6 +48,7 @@ export function resolveReleaseExecutable(repoRoot, environmentVariable, defaultE */ function isWithinDirectory(candidatePath, directoryPath) { const relativePath = path.relative(directoryPath, candidatePath); + return relativePath === "" || (!relativePath.startsWith(`..${path.sep}`) && relativePath !== ".." && !path.isAbsolute(relativePath)); } diff --git a/deploy/verifyReleaseArtifact.mjs b/deploy/verifyReleaseArtifact.mjs index dfe1e53..ffcd6a2 100644 --- a/deploy/verifyReleaseArtifact.mjs +++ b/deploy/verifyReleaseArtifact.mjs @@ -8,17 +8,23 @@ import { } from "./release-artifact.mjs"; const artifactDirectory = parseArtifactDirectory(process.argv.slice(2)); + const { plan } = await readPreparedReleaseArtifact(artifactDirectory); + const artifactIntegrity = await hashPreparedReleaseArtifact(artifactDirectory, plan.changed); + if (!process.env.EXPECTED_ARTIFACT_INTEGRITY || artifactIntegrity !== process.env.EXPECTED_ARTIFACT_INTEGRITY) { throw new Error("Release artifact does not match the integrity recorded during preparation"); } + if (process.env.GITHUB_SHA && plan.sourceCommit !== process.env.GITHUB_SHA) { throw new Error(`Release artifact commit ${plan.sourceCommit} does not match GITHUB_SHA ${process.env.GITHUB_SHA}`); } + if (process.env.GITHUB_OUTPUT) { await appendFile(process.env.GITHUB_OUTPUT, `changed=${plan.changed}\n`); } + console.log(`Verified release artifact for ${plan.packageName}@${plan.packageVersion}`); /** @param {string[]} argv */ @@ -26,5 +32,6 @@ function parseArtifactDirectory(argv) { if (argv.length !== 2 || argv[0] !== "--artifact-dir" || !argv[1]) { throw new Error("Usage: node deploy/verifyReleaseArtifact.mjs --artifact-dir "); } + return path.resolve(argv[1]); } diff --git a/derived/current/classification.json b/derived/current/classification.json index 1a4c98d..6d17e8f 100644 --- a/derived/current/classification.json +++ b/derived/current/classification.json @@ -5,44 +5,44 @@ "classificationOutputPath": "derived/current/classification.json", "compatManagementOutputPath": "derived/current/compat-management-report.json", "summary": { - "featureCount": 121, - "compatCount": 1356, - "libCompatCount": 1150, - "highCompatCount": 750, - "lowCompatCount": 77, - "falseCompatCount": 323, - "includedCompatCount": 750, - "emitCompatCount": 997, - "notModeledUpstreamCount": 51, + "featureCount": 125, + "compatCount": 1362, + "libCompatCount": 1153, + "highCompatCount": 757, + "lowCompatCount": 78, + "falseCompatCount": 318, + "includedCompatCount": 757, + "emitCompatCount": 1000, + "notModeledUpstreamCount": 52, "alreadyExcludedUpstreamCount": 1, - "managedCompatCount": 60, + "managedCompatCount": 61, "managedCategoryCounts": { "actionable_upstream_gap": 12, "handled_outside_lib": 3, "legacy_excluded": 14, - "tracked_by_canonical_lib": 27, + "tracked_by_canonical_lib": 28, "ts_settled_limitation": 4 }, "managedDeliveryCounts": { "cover-checker": 2, - "defer-upstream": 43, + "defer-upstream": 44, "exclude": 15 }, "managedUpstreamStateCounts": { "actionable": 7, "settled": 21, - "tracked": 32 + "tracked": 33 }, - "selectedUnitCount": 1444, + "selectedUnitCount": 1454, "transformedUnitCount": 2 }, "compatManagementRegistryPath": "registry/compat-management.json", "compatManagementRegistrySummary": { "kind": "typescript-baseline-lib/compat-management-registry", "schemaVersion": 1, - "sourceHash": "sha256-a2e0009acc49a898cb3154ab149fe75a1760643216e28e502d1d133a924f104a", - "groupCount": 19, - "managedCompatCount": 60 + "sourceHash": "sha256-11588342536d409110b0237bcfe2a83a7588c122398f0bc6168217cd786b4603", + "groupCount": 20, + "managedCompatCount": 61 }, "classifiedCompatRows": [ { @@ -613,14 +613,15 @@ "compatKey": "javascript.builtins.Array.fromAsync", "featureId": "array-fromasync", "featureName": "Array.fromAsync()", - "baselineStatus": "low", + "baselineStatus": "high", "baselineLowDate": "2024-01-25", + "baselineHighDate": "2026-07-25", "sourceRefs": [ "javascript.builtins.Array.fromAsync" ], "snapshot": [], "compatRoot": "Array", - "includeInTarget": false, + "includeInTarget": true, "resolutionKind": "member", "resolvedUnitIds": [ "lib.esnext.array.d.ts::ArrayConstructor.fromAsync::2", @@ -1331,7 +1332,8 @@ "resolvedUnitIds": [ "lib.es5.d.ts::ArrayBuffer::490", "lib.es5.d.ts::ArrayBuffer::502", - "lib.es5.d.ts::ArrayBufferConstructor::498" + "lib.es5.d.ts::ArrayBufferConstructor::498", + "lib.es5.d.ts::ArrayBufferTypes.ArrayBuffer::495" ], "transforms": [], "notes": "compat root ArrayBuffer selects standalone declaration units" @@ -1433,14 +1435,15 @@ "compatKey": "javascript.builtins.ArrayBuffer.detached", "featureId": "transferable-arraybuffer", "featureName": "Transferable ArrayBuffer", - "baselineStatus": "low", + "baselineStatus": "high", "baselineLowDate": "2024-03-05", + "baselineHighDate": "2026-09-05", "sourceRefs": [ "javascript.builtins.ArrayBuffer.detached" ], "snapshot": [], "compatRoot": "ArrayBuffer", - "includeInTarget": false, + "includeInTarget": true, "resolutionKind": "member", "resolvedUnitIds": [ "lib.es2024.arraybuffer.d.ts::ArrayBuffer.detached::5" @@ -1553,14 +1556,15 @@ "compatKey": "javascript.builtins.ArrayBuffer.transfer", "featureId": "transferable-arraybuffer", "featureName": "Transferable ArrayBuffer", - "baselineStatus": "low", + "baselineStatus": "high", "baselineLowDate": "2024-03-05", + "baselineHighDate": "2026-09-05", "sourceRefs": [ "javascript.builtins.ArrayBuffer.transfer" ], "snapshot": [], "compatRoot": "ArrayBuffer", - "includeInTarget": false, + "includeInTarget": true, "resolutionKind": "member", "resolvedUnitIds": [ "lib.es2024.arraybuffer.d.ts::ArrayBuffer.transfer::6" @@ -1572,14 +1576,15 @@ "compatKey": "javascript.builtins.ArrayBuffer.transferToFixedLength", "featureId": "transferable-arraybuffer", "featureName": "Transferable ArrayBuffer", - "baselineStatus": "low", + "baselineStatus": "high", "baselineLowDate": "2024-03-05", + "baselineHighDate": "2026-09-05", "sourceRefs": [ "javascript.builtins.ArrayBuffer.transferToFixedLength" ], "snapshot": [], "compatRoot": "ArrayBuffer", - "includeInTarget": false, + "includeInTarget": true, "resolutionKind": "member", "resolvedUnitIds": [ "lib.es2024.arraybuffer.d.ts::ArrayBuffer.transferToFixedLength::7" @@ -5501,7 +5506,8 @@ "compatKey": "javascript.builtins.FinalizationRegistry.unregister.symbol_as_target", "featureId": "weak-references", "featureName": "Weak references", - "baselineStatus": false, + "baselineStatus": "low", + "baselineLowDate": "2025-12-09", "sourceRefs": [ "javascript.builtins.FinalizationRegistry.unregister.symbol_as_target" ], @@ -7738,7 +7744,8 @@ "compatKey": "javascript.builtins.Intl.Locale.getCalendars", "featureId": "intl-locale-info", "featureName": "Intl.Locale info", - "baselineStatus": false, + "baselineStatus": "low", + "baselineLowDate": "2026-07-21", "sourceRefs": [ "javascript.builtins.Intl.Locale.getCalendars" ], @@ -7756,7 +7763,8 @@ "compatKey": "javascript.builtins.Intl.Locale.getCollations", "featureId": "intl-locale-info", "featureName": "Intl.Locale info", - "baselineStatus": false, + "baselineStatus": "low", + "baselineLowDate": "2026-07-21", "sourceRefs": [ "javascript.builtins.Intl.Locale.getCollations" ], @@ -7774,7 +7782,8 @@ "compatKey": "javascript.builtins.Intl.Locale.getHourCycles", "featureId": "intl-locale-info", "featureName": "Intl.Locale info", - "baselineStatus": false, + "baselineStatus": "low", + "baselineLowDate": "2026-07-21", "sourceRefs": [ "javascript.builtins.Intl.Locale.getHourCycles" ], @@ -7792,7 +7801,8 @@ "compatKey": "javascript.builtins.Intl.Locale.getNumberingSystems", "featureId": "intl-locale-info", "featureName": "Intl.Locale info", - "baselineStatus": false, + "baselineStatus": "low", + "baselineLowDate": "2026-07-21", "sourceRefs": [ "javascript.builtins.Intl.Locale.getNumberingSystems" ], @@ -7810,7 +7820,8 @@ "compatKey": "javascript.builtins.Intl.Locale.getTextInfo", "featureId": "intl-locale-info", "featureName": "Intl.Locale info", - "baselineStatus": false, + "baselineStatus": "low", + "baselineLowDate": "2026-07-21", "sourceRefs": [ "javascript.builtins.Intl.Locale.getTextInfo" ], @@ -7828,7 +7839,8 @@ "compatKey": "javascript.builtins.Intl.Locale.getTimeZones", "featureId": "intl-locale-info", "featureName": "Intl.Locale info", - "baselineStatus": false, + "baselineStatus": "low", + "baselineLowDate": "2026-07-21", "sourceRefs": [ "javascript.builtins.Intl.Locale.getTimeZones" ], @@ -7846,7 +7858,8 @@ "compatKey": "javascript.builtins.Intl.Locale.getWeekInfo", "featureId": "intl-locale-info", "featureName": "Intl.Locale info", - "baselineStatus": false, + "baselineStatus": "low", + "baselineLowDate": "2026-07-21", "sourceRefs": [ "javascript.builtins.Intl.Locale.getWeekInfo" ], @@ -9730,6 +9743,42 @@ "transforms": [], "notes": "member surface Iterator.from" }, + { + "compatKey": "javascript.builtins.Iterator.join", + "featureId": "iterator-join", + "featureName": "Iterator join()", + "baselineStatus": false, + "sourceRefs": [ + "javascript.builtins.Iterator.join" + ], + "snapshot": [], + "compatRoot": "Iterator", + "includeInTarget": false, + "resolutionKind": "not-modeled-upstream", + "resolvedUnitIds": [], + "transforms": [], + "notes": "javascript.builtins.Iterator.join is not currently modeled as a dedicated member in TypeScript's lib declarations", + "management": { + "groupId": "iterator-join-tracked", + "category": "tracked_by_canonical_lib", + "delivery": "defer-upstream", + "upstreamState": "tracked", + "reason": "Iterator.join is tracked for TypeScript 7.1 but is absent from the pinned libraries.", + "sourceUrls": [ + "https://github.com/microsoft/TypeScript/issues/63700", + "https://tc39.es/proposal-iterator-join/#sec-iterator.prototype.join" + ], + "externalAction": { + "kind": "watch-existing", + "repo": "microsoft/TypeScript", + "targetUrl": "https://github.com/microsoft/TypeScript/issues/63700", + "note": "Update the mapping when the declaration lands." + }, + "expectedResolutionKinds": [ + "not-modeled-upstream" + ] + } + }, { "compatKey": "javascript.builtins.Iterator.map", "featureId": "iterator-methods", @@ -10346,14 +10395,15 @@ "compatKey": "javascript.builtins.Map.groupBy", "featureId": "array-group", "featureName": "Array grouping", - "baselineStatus": "low", + "baselineStatus": "high", "baselineLowDate": "2024-03-05", + "baselineHighDate": "2026-09-05", "sourceRefs": [ "javascript.builtins.Map.groupBy" ], "snapshot": [], "compatRoot": "Map", - "includeInTarget": false, + "includeInTarget": true, "resolutionKind": "member", "resolvedUnitIds": [ "lib.es2024.collection.d.ts::MapConstructor.groupBy::2" @@ -12311,14 +12361,15 @@ "compatKey": "javascript.builtins.Object.groupBy", "featureId": "array-group", "featureName": "Array grouping", - "baselineStatus": "low", + "baselineStatus": "high", "baselineLowDate": "2024-03-05", + "baselineHighDate": "2026-09-05", "sourceRefs": [ "javascript.builtins.Object.groupBy" ], "snapshot": [], "compatRoot": "Object", - "includeInTarget": false, + "includeInTarget": true, "resolutionKind": "member", "resolvedUnitIds": [ "lib.es2024.object.d.ts::ObjectConstructor.groupBy::2" @@ -13082,14 +13133,15 @@ "compatKey": "javascript.builtins.Promise.withResolvers", "featureId": "promise-withresolvers", "featureName": "Promise.withResolvers()", - "baselineStatus": "low", + "baselineStatus": "high", "baselineLowDate": "2024-03-05", + "baselineHighDate": "2026-09-05", "sourceRefs": [ "javascript.builtins.Promise.withResolvers" ], "snapshot": [], "compatRoot": "Promise", - "includeInTarget": false, + "includeInTarget": true, "resolutionKind": "member", "resolvedUnitIds": [ "lib.es2024.promise.d.ts::PromiseConstructor.withResolvers::7" @@ -15275,6 +15327,7 @@ "includeInTarget": true, "resolutionKind": "root-availability", "resolvedUnitIds": [ + "lib.es2017.sharedmemory.d.ts::ArrayBufferTypes.SharedArrayBuffer::13", "lib.es2017.sharedmemory.d.ts::SharedArrayBuffer::1", "lib.es2017.sharedmemory.d.ts::SharedArrayBuffer::10", "lib.es2017.sharedmemory.d.ts::SharedArrayBufferConstructor::6" @@ -16568,6 +16621,42 @@ "transforms": [], "notes": "member surface String.trimEnd" }, + { + "compatKey": "javascript.builtins.String.trimLeft", + "featureId": "string-trim-leftright", + "featureName": "String trimLeft() and trimRight()", + "baselineStatus": false, + "sourceRefs": [ + "javascript.builtins.String.trimLeft" + ], + "snapshot": [], + "compatRoot": "String", + "includeInTarget": false, + "resolutionKind": "member", + "resolvedUnitIds": [ + "lib.es2019.string.d.ts::String.trimLeft::4" + ], + "transforms": [], + "notes": "member surface String.trimLeft" + }, + { + "compatKey": "javascript.builtins.String.trimRight", + "featureId": "string-trim-leftright", + "featureName": "String trimLeft() and trimRight()", + "baselineStatus": false, + "sourceRefs": [ + "javascript.builtins.String.trimRight" + ], + "snapshot": [], + "compatRoot": "String", + "includeInTarget": false, + "resolutionKind": "member", + "resolvedUnitIds": [ + "lib.es2019.string.d.ts::String.trimRight::5" + ], + "transforms": [], + "notes": "member surface String.trimRight" + }, { "compatKey": "javascript.builtins.String.trimStart", "featureId": "string-trim-startend", diff --git a/derived/current/compat-management-report.json b/derived/current/compat-management-report.json index d94ccee..f2749c2 100644 --- a/derived/current/compat-management-report.json +++ b/derived/current/compat-management-report.json @@ -8,33 +8,33 @@ "kind": "typescript-baseline-lib/compat-management-registry", "schemaVersion": 1, "sourcePath": "registry/compat-management.json", - "sourceHash": "sha256-a2e0009acc49a898cb3154ab149fe75a1760643216e28e502d1d133a924f104a", - "groupCount": 19, - "managedCompatCount": 60 + "sourceHash": "sha256-11588342536d409110b0237bcfe2a83a7588c122398f0bc6168217cd786b4603", + "groupCount": 20, + "managedCompatCount": 61 }, "summary": { - "managedCompatCount": 60, + "managedCompatCount": 61, "managedCategoryCounts": { "actionable_upstream_gap": 12, "handled_outside_lib": 3, "legacy_excluded": 14, - "tracked_by_canonical_lib": 27, + "tracked_by_canonical_lib": 28, "ts_settled_limitation": 4 }, "managedDeliveryCounts": { "cover-checker": 2, - "defer-upstream": 43, + "defer-upstream": 44, "exclude": 15 }, "managedUpstreamStateCounts": { "actionable": 7, "settled": 21, - "tracked": 32 + "tracked": 33 }, "managedResolutionKindCounts": { "already-excluded-upstream": 1, "member": 6, - "not-modeled-upstream": 51, + "not-modeled-upstream": 52, "root-availability": 2 } }, @@ -369,6 +369,34 @@ "not-modeled-upstream" ] }, + { + "id": "iterator-join-tracked", + "category": "tracked_by_canonical_lib", + "delivery": "defer-upstream", + "upstreamState": "tracked", + "compatRoot": "Iterator", + "expectedResolutionKinds": [ + "not-modeled-upstream" + ], + "reason": "Iterator.join is tracked for TypeScript 7.1 but is absent from the pinned libraries.", + "sourceUrls": [ + "https://github.com/microsoft/TypeScript/issues/63700", + "https://tc39.es/proposal-iterator-join/#sec-iterator.prototype.join" + ], + "externalAction": { + "kind": "watch-existing", + "repo": "microsoft/TypeScript", + "targetUrl": "https://github.com/microsoft/TypeScript/issues/63700", + "note": "Update the mapping when the declaration lands." + }, + "compatKeyCount": 1, + "compatKeys": [ + "javascript.builtins.Iterator.join" + ], + "actualResolutionKinds": [ + "not-modeled-upstream" + ] + }, { "id": "math-sum-precise-tracked", "category": "tracked_by_canonical_lib", diff --git a/derived/current/generation.json b/derived/current/generation.json index fcb7a41..fceb997 100644 --- a/derived/current/generation.json +++ b/derived/current/generation.json @@ -16,15 +16,15 @@ "inventoryOutputPath": "derived/current/inventory.json", "summary": { "sourceLibCount": 84, - "classifiedCompatCount": 1150, - "selectedUnitCount": 1909, + "classifiedCompatCount": 1153, + "selectedUnitCount": 1922, "completeContainerCount": 176, - "excludedUnitCount": 414, + "excludedUnitCount": 408, "preservedTypeOnlyUnitCount": 29, "transformedUnitCount": 2, "allowEntryCount": 15, - "allowEntryUnitCount": 126, - "allowSupportUnitCount": 77, + "allowEntryUnitCount": 118, + "allowSupportUnitCount": 73, "yearLibCount": 6 }, "sourceLibs": [ @@ -606,10 +606,6 @@ "kind": "allow-support", "outputPath": "generated/current/allow/_support/intl-segmenter.d.ts" }, - { - "kind": "allow-support", - "outputPath": "generated/current/allow/_support/promise-withresolvers.d.ts" - }, { "kind": "allow-support", "outputPath": "generated/current/allow/_support/set-methods.d.ts" @@ -676,30 +672,24 @@ ], "allowEntries": [ { - "kind": "active", + "kind": "alias", "entryName": "array-fromasync", "outputPath": "generated/current/allow/array-fromasync/index.d.ts", "compatKeys": [ "javascript.builtins.Array.fromAsync" ], - "unitIds": [ - "lib.esnext.array.d.ts::ArrayConstructor.fromAsync::2", - "lib.esnext.array.d.ts::ArrayConstructor.fromAsync::3" - ], + "unitIds": [], "supportUnitIds": [] }, { - "kind": "active", + "kind": "alias", "entryName": "array-group", "outputPath": "generated/current/allow/array-group/index.d.ts", "compatKeys": [ "javascript.builtins.Map.groupBy", "javascript.builtins.Object.groupBy" ], - "unitIds": [ - "lib.es2024.collection.d.ts::MapConstructor.groupBy::2", - "lib.es2024.object.d.ts::ObjectConstructor.groupBy::2" - ], + "unitIds": [], "supportUnitIds": [] }, { @@ -959,21 +949,14 @@ "supportUnitIds": [] }, { - "kind": "active", + "kind": "alias", "entryName": "promise-withresolvers", "outputPath": "generated/current/allow/promise-withresolvers/index.d.ts", "compatKeys": [ "javascript.builtins.Promise.withResolvers" ], - "unitIds": [ - "lib.es2024.promise.d.ts::PromiseConstructor.withResolvers::7" - ], - "supportUnitIds": [ - "lib.es2024.promise.d.ts::PromiseWithResolvers.promise::2", - "lib.es2024.promise.d.ts::PromiseWithResolvers.reject::4", - "lib.es2024.promise.d.ts::PromiseWithResolvers.resolve::3", - "lib.es2024.promise.d.ts::PromiseWithResolvers::1" - ] + "unitIds": [], + "supportUnitIds": [] }, { "kind": "active", @@ -1050,7 +1033,7 @@ ] }, { - "kind": "active", + "kind": "alias", "entryName": "transferable-arraybuffer", "outputPath": "generated/current/allow/transferable-arraybuffer/index.d.ts", "compatKeys": [ @@ -1058,11 +1041,7 @@ "javascript.builtins.ArrayBuffer.transfer", "javascript.builtins.ArrayBuffer.transferToFixedLength" ], - "unitIds": [ - "lib.es2024.arraybuffer.d.ts::ArrayBuffer.detached::5", - "lib.es2024.arraybuffer.d.ts::ArrayBuffer.transfer::6", - "lib.es2024.arraybuffer.d.ts::ArrayBuffer.transferToFixedLength::7" - ], + "unitIds": [], "supportUnitIds": [] }, { @@ -1723,7 +1702,7 @@ { "year": 2021, "outputPath": "generated/current/year/2021/index.d.ts", - "contentHash": "sha256-32eff0354d555bac166068b673c9253de514e7d66c62d9529dca525294f56a51", + "contentHash": "sha256-bef6c646f03e1822bb699a80525ad3f00b716a48f804964a5febdec4a3a73c17", "includedCompatKeys": [ "javascript.builtins.AggregateError", "javascript.builtins.AggregateError.AggregateError", @@ -2411,14 +2390,14 @@ "javascript.builtins.globalThis", "javascript.builtins.undefined" ], - "selectedUnitCount": 1791, + "selectedUnitCount": 1792, "preservedTypeOnlyUnitCount": 29, "transformedUnitCount": 2 }, { "year": 2022, "outputPath": "generated/current/year/2022/index.d.ts", - "contentHash": "sha256-3bf13c02fec1aa78b05d8181b5941a3f9a794ea0527052bad6461bb04e5685b0", + "contentHash": "sha256-b15e474041af2d6b95db413bf3e9e4953f89ead33679df763f4ad8275245000f", "includedCompatKeys": [ "javascript.builtins.AggregateError", "javascript.builtins.AggregateError.AggregateError", @@ -3116,14 +3095,14 @@ "javascript.builtins.globalThis", "javascript.builtins.undefined" ], - "selectedUnitCount": 1846, + "selectedUnitCount": 1847, "preservedTypeOnlyUnitCount": 29, "transformedUnitCount": 2 }, { "year": 2023, "outputPath": "generated/current/year/2023/index.d.ts", - "contentHash": "sha256-ab5b2463a60479471ccf98811f8ca9da76d8220a11bf92dc02b7ab06bb2044e8", + "contentHash": "sha256-7fe871afdc5ba183c2942cf3642461df4cffba6ac808c7cb5c5391a7e0d014a4", "includedCompatKeys": [ "javascript.builtins.AggregateError", "javascript.builtins.AggregateError.AggregateError", @@ -3843,14 +3822,14 @@ "javascript.builtins.globalThis", "javascript.builtins.undefined" ], - "selectedUnitCount": 1909, + "selectedUnitCount": 1910, "preservedTypeOnlyUnitCount": 29, "transformedUnitCount": 2 }, { "year": 2024, "outputPath": "generated/current/year/2024/index.d.ts", - "contentHash": "sha256-09e74810ca650a81e4dff5d5fd18b3c3669ff719b9601f11987ce85ed8d1f138", + "contentHash": "sha256-fc090f5baf6146ade06fca1d78f99cb38602c202e1f3a9820110e815bbb36732", "includedCompatKeys": [ "javascript.builtins.AggregateError", "javascript.builtins.AggregateError.AggregateError", @@ -4600,14 +4579,14 @@ "javascript.builtins.globalThis", "javascript.builtins.undefined" ], - "selectedUnitCount": 1968, + "selectedUnitCount": 1969, "preservedTypeOnlyUnitCount": 29, "transformedUnitCount": 0 }, { "year": 2025, "outputPath": "generated/current/year/2025/index.d.ts", - "contentHash": "sha256-d10524e61d0ecf8177c8c6b27c6388074c9c84c30099e9485d61a1d9cb2049b1", + "contentHash": "sha256-ca9e417cbda66904a277ae025e3cc7383a7268571ffcc5aec4a751eff7d01169", "includedCompatKeys": [ "javascript.builtins.AggregateError", "javascript.builtins.AggregateError.AggregateError", @@ -4811,6 +4790,7 @@ "javascript.builtins.FinalizationRegistry.register", "javascript.builtins.FinalizationRegistry.register.symbol_as_target", "javascript.builtins.FinalizationRegistry.unregister", + "javascript.builtins.FinalizationRegistry.unregister.symbol_as_target", "javascript.builtins.Float16Array", "javascript.builtins.Float16Array.Float16Array", "javascript.builtins.Float32Array", @@ -5397,7 +5377,7 @@ "javascript.builtins.globalThis", "javascript.builtins.undefined" ], - "selectedUnitCount": 2125, + "selectedUnitCount": 2126, "preservedTypeOnlyUnitCount": 29, "transformedUnitCount": 0 } @@ -5487,6 +5467,18 @@ "javascript.builtins.String.sup" ] }, + { + "unitId": "lib.es2019.string.d.ts::String.trimLeft::4", + "compatKeys": [ + "javascript.builtins.String.trimLeft" + ] + }, + { + "unitId": "lib.es2019.string.d.ts::String.trimRight::5", + "compatKeys": [ + "javascript.builtins.String.trimRight" + ] + }, { "unitId": "lib.es2022.intl.d.ts::Intl.Segmenter.::30", "compatKeys": [ @@ -5558,12 +5550,6 @@ "javascript.builtins.WeakSet.symbol_as_keys" ] }, - { - "unitId": "lib.es2024.arraybuffer.d.ts::ArrayBuffer.detached::5", - "compatKeys": [ - "javascript.builtins.ArrayBuffer.detached" - ] - }, { "unitId": "lib.es2024.arraybuffer.d.ts::ArrayBuffer.maxByteLength::2", "compatKeys": [ @@ -5582,36 +5568,6 @@ "javascript.builtins.ArrayBuffer.resize" ] }, - { - "unitId": "lib.es2024.arraybuffer.d.ts::ArrayBuffer.transfer::6", - "compatKeys": [ - "javascript.builtins.ArrayBuffer.transfer" - ] - }, - { - "unitId": "lib.es2024.arraybuffer.d.ts::ArrayBuffer.transferToFixedLength::7", - "compatKeys": [ - "javascript.builtins.ArrayBuffer.transferToFixedLength" - ] - }, - { - "unitId": "lib.es2024.collection.d.ts::MapConstructor.groupBy::2", - "compatKeys": [ - "javascript.builtins.Map.groupBy" - ] - }, - { - "unitId": "lib.es2024.object.d.ts::ObjectConstructor.groupBy::2", - "compatKeys": [ - "javascript.builtins.Object.groupBy" - ] - }, - { - "unitId": "lib.es2024.promise.d.ts::PromiseConstructor.withResolvers::7", - "compatKeys": [ - "javascript.builtins.Promise.withResolvers" - ] - }, { "unitId": "lib.es2024.sharedmemory.d.ts::Atomics.waitAsync::2", "compatKeys": [ @@ -6118,18 +6074,6 @@ "javascript.builtins.unescape" ] }, - { - "unitId": "lib.esnext.array.d.ts::ArrayConstructor.fromAsync::2", - "compatKeys": [ - "javascript.builtins.Array.fromAsync" - ] - }, - { - "unitId": "lib.esnext.array.d.ts::ArrayConstructor.fromAsync::3", - "compatKeys": [ - "javascript.builtins.Array.fromAsync" - ] - }, { "unitId": "lib.esnext.collection.d.ts::Map.getOrInsert::2", "compatKeys": [ diff --git a/docs/USAGE.md b/docs/USAGE.md index e3dff6f..1a47a2d 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -156,9 +156,9 @@ npm install --save-dev typescript@^7 typescript-baseline-lib Load the polyfill from the application entry point: ```ts -import "core-js/proposals/promise-with-resolvers"; +import "core-js/proposals/promise-try"; -const deferred = Promise.withResolvers(); +const result = Promise.try(() => 42); ``` Then add its audited declaration entry: @@ -170,7 +170,7 @@ Then add its audited declaration entry: "strict": true, "types": [ "typescript-baseline-lib", - "typescript-baseline-lib/allow/promise-withresolvers" + "typescript-baseline-lib/allow/promise-try" ], "noEmit": true } @@ -272,4 +272,4 @@ The output must include `typescript-baseline-lib`. It must not include standard - [Browserslist Baseline queries](https://github.com/browserslist/browserslist#queries) - [Choosing a Baseline target](https://web.dev/articles/how-to-choose-your-baseline-target) - [Baseline and polyfills](https://web.dev/articles/baseline-and-polyfills) -- [`Promise.withResolvers` in core-js](https://core-js.io/docs/features/proposals/promise-withresolvers) +- [`Promise.try` in core-js](https://github.com/zloirock/core-js/blob/master/packages/core-js/proposals/promise-try.js) diff --git a/generated/current/allow/_support/promise-withresolvers.d.ts b/generated/current/allow/_support/promise-withresolvers.d.ts deleted file mode 100644 index 0e996a2..0000000 --- a/generated/current/allow/_support/promise-withresolvers.d.ts +++ /dev/null @@ -1,30 +0,0 @@ -// -// Generated by TypeScript Baseline Lib Generator from TypeScript lib declaration files. -// Source declarations are derived from the pinned TypeScript declaration corpus. -// Do not edit this file directly. -// - -/*! ***************************************************************************** -Copyright (c) Microsoft Corporation. All rights reserved. -Licensed under the Apache License, Version 2.0 (the "License"); you may not use -this file except in compliance with the License. You may obtain a copy of the -License at http://www.apache.org/licenses/LICENSE-2.0 - -THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED -WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, -MERCHANTABILITY OR NON-INFRINGEMENT. - -See the Apache Version 2.0 License for specific language governing permissions -and limitations under the License. -***************************************************************************** */ -///////////////////////////// -// lib.es2024.promise.d.ts -///////////////////////////// -interface PromiseWithResolvers { - promise: Promise; - - resolve: (value: T | PromiseLike) => void; - - reject: (reason?: any) => void; -} diff --git a/generated/current/allow/array-fromasync/index.d.ts b/generated/current/allow/array-fromasync/index.d.ts index 2ad8326..da825f4 100644 --- a/generated/current/allow/array-fromasync/index.d.ts +++ b/generated/current/allow/array-fromasync/index.d.ts @@ -1,40 +1 @@ -// -// Generated by TypeScript Baseline Lib Generator from TypeScript lib declaration files. -// Source declarations are derived from the pinned TypeScript declaration corpus. -// Do not edit this file directly. -// - -/*! ***************************************************************************** -Copyright (c) Microsoft Corporation. All rights reserved. -Licensed under the Apache License, Version 2.0 (the "License"); you may not use -this file except in compliance with the License. You may obtain a copy of the -License at http://www.apache.org/licenses/LICENSE-2.0 - -THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED -WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, -MERCHANTABILITY OR NON-INFRINGEMENT. - -See the Apache Version 2.0 License for specific language governing permissions -and limitations under the License. -***************************************************************************** */ -///////////////////////////// -// lib.esnext.array.d.ts -///////////////////////////// -interface ArrayConstructor { - /** - * Creates an array from an async iterator or iterable object. - * @param iterableOrArrayLike An async iterator or array-like object to convert to an array. - */ - fromAsync(iterableOrArrayLike: AsyncIterable | Iterable> | ArrayLike>): Promise; - - /** - * Creates an array from an async iterator or iterable object. - * - * @param iterableOrArrayLike An async iterator or array-like object to convert to an array. - * @param mapfn A mapping function to call on every element of itarableOrArrayLike. - * Each return value is awaited before being added to result array. - * @param thisArg Value of 'this' used when executing mapfn. - */ - fromAsync(iterableOrArrayLike: AsyncIterable | Iterable | ArrayLike, mapFn: (value: Awaited, index: number) => U, thisArg?: any): Promise[]>; -} +/// diff --git a/generated/current/allow/array-group/index.d.ts b/generated/current/allow/array-group/index.d.ts index 64a93b4..da825f4 100644 --- a/generated/current/allow/array-group/index.d.ts +++ b/generated/current/allow/array-group/index.d.ts @@ -1,49 +1 @@ -// -// Generated by TypeScript Baseline Lib Generator from TypeScript lib declaration files. -// Source declarations are derived from the pinned TypeScript declaration corpus. -// Do not edit this file directly. -// - -/*! ***************************************************************************** -Copyright (c) Microsoft Corporation. All rights reserved. -Licensed under the Apache License, Version 2.0 (the "License"); you may not use -this file except in compliance with the License. You may obtain a copy of the -License at http://www.apache.org/licenses/LICENSE-2.0 - -THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED -WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, -MERCHANTABILITY OR NON-INFRINGEMENT. - -See the Apache Version 2.0 License for specific language governing permissions -and limitations under the License. -***************************************************************************** */ -///////////////////////////// -// lib.es2024.collection.d.ts -///////////////////////////// -interface MapConstructor { - /** - * Groups members of an iterable according to the return value of the passed callback. - * @param items An iterable. - * @param keySelector A callback which will be invoked for each item in items. - */ - groupBy( - items: Iterable, - keySelector: (item: T, index: number) => K, - ): Map; -} - -///////////////////////////// -// lib.es2024.object.d.ts -///////////////////////////// -interface ObjectConstructor { - /** - * Groups members of an iterable according to the return value of the passed callback. - * @param items An iterable. - * @param keySelector A callback which will be invoked for each item in items. - */ - groupBy( - items: Iterable, - keySelector: (item: T, index: number) => K, - ): Partial>; -} +/// diff --git a/generated/current/allow/promise-withresolvers/index.d.ts b/generated/current/allow/promise-withresolvers/index.d.ts index 88ab234..da825f4 100644 --- a/generated/current/allow/promise-withresolvers/index.d.ts +++ b/generated/current/allow/promise-withresolvers/index.d.ts @@ -1,36 +1 @@ -/// - -// -// Generated by TypeScript Baseline Lib Generator from TypeScript lib declaration files. -// Source declarations are derived from the pinned TypeScript declaration corpus. -// Do not edit this file directly. -// - -/*! ***************************************************************************** -Copyright (c) Microsoft Corporation. All rights reserved. -Licensed under the Apache License, Version 2.0 (the "License"); you may not use -this file except in compliance with the License. You may obtain a copy of the -License at http://www.apache.org/licenses/LICENSE-2.0 - -THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED -WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, -MERCHANTABILITY OR NON-INFRINGEMENT. - -See the Apache Version 2.0 License for specific language governing permissions -and limitations under the License. -***************************************************************************** */ -///////////////////////////// -// lib.es2024.promise.d.ts -///////////////////////////// -interface PromiseConstructor { - /** - * Creates a new Promise and returns it in an object, along with its resolve and reject functions. - * @returns An object with the properties `promise`, `resolve`, and `reject`. - * - * ```ts - * const { promise, resolve, reject } = Promise.withResolvers(); - * ``` - */ - withResolvers(): PromiseWithResolvers; -} +/// diff --git a/generated/current/allow/transferable-arraybuffer/index.d.ts b/generated/current/allow/transferable-arraybuffer/index.d.ts index a935854..da825f4 100644 --- a/generated/current/allow/transferable-arraybuffer/index.d.ts +++ b/generated/current/allow/transferable-arraybuffer/index.d.ts @@ -1,45 +1 @@ -// -// Generated by TypeScript Baseline Lib Generator from TypeScript lib declaration files. -// Source declarations are derived from the pinned TypeScript declaration corpus. -// Do not edit this file directly. -// - -/*! ***************************************************************************** -Copyright (c) Microsoft Corporation. All rights reserved. -Licensed under the Apache License, Version 2.0 (the "License"); you may not use -this file except in compliance with the License. You may obtain a copy of the -License at http://www.apache.org/licenses/LICENSE-2.0 - -THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED -WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, -MERCHANTABILITY OR NON-INFRINGEMENT. - -See the Apache Version 2.0 License for specific language governing permissions -and limitations under the License. -***************************************************************************** */ -///////////////////////////// -// lib.es2024.arraybuffer.d.ts -///////////////////////////// -interface ArrayBuffer { - /** - * Returns a boolean indicating whether or not this buffer has been detached (transferred). - * - * [MDN](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer/detached) - */ - get detached(): boolean; - - /** - * Creates a new ArrayBuffer with the same byte content as this buffer, then detaches this buffer. - * - * [MDN](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer/transfer) - */ - transfer(newByteLength?: number): ArrayBuffer; - - /** - * Creates a new non-resizable ArrayBuffer with the same byte content as this buffer, then detaches this buffer. - * - * [MDN](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer/transferToFixedLength) - */ - transferToFixedLength(newByteLength?: number): ArrayBuffer; -} +/// diff --git a/generated/current/baseline.d.ts b/generated/current/baseline.d.ts index b2a87fb..acc7fe9 100644 --- a/generated/current/baseline.d.ts +++ b/generated/current/baseline.d.ts @@ -2162,6 +2162,10 @@ interface SharedArrayBufferConstructor { declare var SharedArrayBuffer: SharedArrayBufferConstructor; +interface ArrayBufferTypes { + SharedArrayBuffer: SharedArrayBuffer; +} + interface Atomics { /** * Adds a value to the value at the given position in the array, returning the original value. @@ -5549,10 +5553,86 @@ declare namespace Intl { ///////////////////////////// // lib.es2024.arraybuffer.d.ts ///////////////////////////// +interface ArrayBuffer { + /** + * Returns a boolean indicating whether or not this buffer has been detached (transferred). + * + * [MDN](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer/detached) + */ + get detached(): boolean; + + /** + * Creates a new ArrayBuffer with the same byte content as this buffer, then detaches this buffer. + * + * [MDN](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer/transfer) + */ + transfer(newByteLength?: number): ArrayBuffer; + + /** + * Creates a new non-resizable ArrayBuffer with the same byte content as this buffer, then detaches this buffer. + * + * [MDN](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer/transferToFixedLength) + */ + transferToFixedLength(newByteLength?: number): ArrayBuffer; +} + interface ArrayBufferConstructor { new (byteLength: number): ArrayBuffer; } +///////////////////////////// +// lib.es2024.collection.d.ts +///////////////////////////// +interface MapConstructor { + /** + * Groups members of an iterable according to the return value of the passed callback. + * @param items An iterable. + * @param keySelector A callback which will be invoked for each item in items. + */ + groupBy( + items: Iterable, + keySelector: (item: T, index: number) => K, + ): Map; +} + +///////////////////////////// +// lib.es2024.object.d.ts +///////////////////////////// +interface ObjectConstructor { + /** + * Groups members of an iterable according to the return value of the passed callback. + * @param items An iterable. + * @param keySelector A callback which will be invoked for each item in items. + */ + groupBy( + items: Iterable, + keySelector: (item: T, index: number) => K, + ): Partial>; +} + +///////////////////////////// +// lib.es2024.promise.d.ts +///////////////////////////// +interface PromiseWithResolvers { + promise: Promise; + + resolve: (value: T | PromiseLike) => void; + + reject: (reason?: any) => void; +} + +interface PromiseConstructor { + /** + * Creates a new Promise and returns it in an object, along with its resolve and reject functions. + * @returns An object with the properties `promise`, `resolve`, and `reject`. + * + * ```ts + * const { promise, resolve, reject } = Promise.withResolvers(); + * ``` + */ + withResolvers(): PromiseWithResolvers; +} + ///////////////////////////// // lib.es2024.regexp.d.ts ///////////////////////////// @@ -10420,3 +10500,24 @@ interface Date { */ toLocaleTimeString(locales?: string | string[], options?: Intl.DateTimeFormatOptions): string; } + +///////////////////////////// +// lib.esnext.array.d.ts +///////////////////////////// +interface ArrayConstructor { + /** + * Creates an array from an async iterator or iterable object. + * @param iterableOrArrayLike An async iterator or array-like object to convert to an array. + */ + fromAsync(iterableOrArrayLike: AsyncIterable | Iterable> | ArrayLike>): Promise; + + /** + * Creates an array from an async iterator or iterable object. + * + * @param iterableOrArrayLike An async iterator or array-like object to convert to an array. + * @param mapfn A mapping function to call on every element of itarableOrArrayLike. + * Each return value is awaited before being added to result array. + * @param thisArg Value of 'this' used when executing mapfn. + */ + fromAsync(iterableOrArrayLike: AsyncIterable | Iterable | ArrayLike, mapFn: (value: Awaited, index: number) => U, thisArg?: any): Promise[]>; +} diff --git a/generated/current/year/2021/index.d.ts b/generated/current/year/2021/index.d.ts index dc93a4e..d8bdbbd 100644 --- a/generated/current/year/2021/index.d.ts +++ b/generated/current/year/2021/index.d.ts @@ -2162,6 +2162,10 @@ interface SharedArrayBufferConstructor { declare var SharedArrayBuffer: SharedArrayBufferConstructor; +interface ArrayBufferTypes { + SharedArrayBuffer: SharedArrayBuffer; +} + interface Atomics { /** * Adds a value to the value at the given position in the array, returning the original value. diff --git a/generated/current/year/2022/index.d.ts b/generated/current/year/2022/index.d.ts index 98a7066..2d48e99 100644 --- a/generated/current/year/2022/index.d.ts +++ b/generated/current/year/2022/index.d.ts @@ -2162,6 +2162,10 @@ interface SharedArrayBufferConstructor { declare var SharedArrayBuffer: SharedArrayBufferConstructor; +interface ArrayBufferTypes { + SharedArrayBuffer: SharedArrayBuffer; +} + interface Atomics { /** * Adds a value to the value at the given position in the array, returning the original value. diff --git a/generated/current/year/2023/index.d.ts b/generated/current/year/2023/index.d.ts index b2a87fb..617fc94 100644 --- a/generated/current/year/2023/index.d.ts +++ b/generated/current/year/2023/index.d.ts @@ -2162,6 +2162,10 @@ interface SharedArrayBufferConstructor { declare var SharedArrayBuffer: SharedArrayBufferConstructor; +interface ArrayBufferTypes { + SharedArrayBuffer: SharedArrayBuffer; +} + interface Atomics { /** * Adds a value to the value at the given position in the array, returning the original value. diff --git a/generated/current/year/2024/index.d.ts b/generated/current/year/2024/index.d.ts index 63ea590..a8e919e 100644 --- a/generated/current/year/2024/index.d.ts +++ b/generated/current/year/2024/index.d.ts @@ -2162,6 +2162,10 @@ interface SharedArrayBufferConstructor { declare var SharedArrayBuffer: SharedArrayBufferConstructor; +interface ArrayBufferTypes { + SharedArrayBuffer: SharedArrayBuffer; +} + interface Atomics { /** * Adds a value to the value at the given position in the array, returning the original value. diff --git a/generated/current/year/2025/index.d.ts b/generated/current/year/2025/index.d.ts index 053f4c3..91454a8 100644 --- a/generated/current/year/2025/index.d.ts +++ b/generated/current/year/2025/index.d.ts @@ -2165,6 +2165,10 @@ declare global { var SharedArrayBuffer: SharedArrayBufferConstructor; + interface ArrayBufferTypes { + SharedArrayBuffer: SharedArrayBuffer; + } + interface Atomics { /** * Adds a value to the value at the given position in the array, returning the original value. diff --git a/lib/allowlist-registry.mjs b/lib/allowlist-registry.mjs index c62bb9d..e6d1202 100644 --- a/lib/allowlist-registry.mjs +++ b/lib/allowlist-registry.mjs @@ -11,12 +11,14 @@ const ENTRY_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/; */ export async function loadAllowlistRegistry(registryPath) { const registry = JSON.parse(await readFile(registryPath, "utf8")); + if (registry.schemaVersion !== 1 || !Array.isArray(registry.entries)) { throw new Error(`Invalid allowlist registry ${registryPath}`); } const entryNames = new Set(); const compatKeys = new Set(); + for (const entry of registry.entries) { if ( !entry @@ -30,28 +32,36 @@ export async function loadAllowlistRegistry(registryPath) { ) { throw new Error(`Invalid allowlist entry in ${registryPath}`); } + if (entryNames.has(entry.entryName) || new Set(entry.compatKeys).size !== entry.compatKeys.length) { throw new Error(`Duplicate allowlist entry data in ${registryPath}: ${entry.entryName}`); } + for (const compatKey of entry.compatKeys) { if (compatKeys.has(compatKey)) { throw new Error(`Allowlist compat key is assigned to multiple entries: ${compatKey}`); } + compatKeys.add(compatKey); } + const sortedCompatKeys = [...entry.compatKeys].sort(compareStringsCaseSensitive); + if (JSON.stringify(sortedCompatKeys) !== JSON.stringify(entry.compatKeys)) { throw new Error(`Allowlist compat keys must be sorted for ${entry.entryName}`); } + entryNames.add(entry.entryName); } const sortedEntries = [...registry.entries].sort((left, right) => compareStringsCaseSensitive(left.entryName, right.entryName) ); + if (JSON.stringify(sortedEntries) !== JSON.stringify(registry.entries)) { throw new Error(`Allowlist entries in ${registryPath} must be sorted by entryName`); } + return registry; } diff --git a/lib/classifier.mjs b/lib/classifier.mjs index 91c4071..7d2d6e3 100644 --- a/lib/classifier.mjs +++ b/lib/classifier.mjs @@ -587,6 +587,9 @@ function classifyCompatRow(options) { if (!tailSegments.length) { const resolvedUnitIds = resolveRootAvailabilityUnitIds(inventory, rootSurface); + if (compatRoot === "ArrayBuffer" || compatRoot === "SharedArrayBuffer") { + resolvedUnitIds.push(...getMemberUnits(inventory, "ArrayBufferTypes", compatRoot).map(unit => unit.id)); + } return createClassifiedRow(compatRow, compatRoot, includeInTarget, { resolutionKind: "root-availability", resolvedUnitIds, diff --git a/lib/compat-management-registry.mjs b/lib/compat-management-registry.mjs index 302430e..b7bb6a2 100644 --- a/lib/compat-management-registry.mjs +++ b/lib/compat-management-registry.mjs @@ -4,25 +4,27 @@ import { createHash } from "node:crypto"; import { readFile } from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; -import Ajv2020Import from "ajv/dist/2020.js"; +import { Ajv2020 } from "ajv/dist/2020.js"; import { compareStringsCaseSensitive } from "./shared.mjs"; const modulePath = fileURLToPath(import.meta.url); + const moduleDirectory = path.dirname(modulePath); + const schemaPath = path.resolve(moduleDirectory, "..", "registry", "compat-management.schema.json"); -const schemaPromise = readJsonFile(schemaPath); + +/** @type {Promise>} */ +const validatorPromise = readJsonFile(schemaPath).then( + /** @param {import("ajv").JSONSchemaType} schema */ + schema => new Ajv2020({ allErrors: true, strict: true }).compile(schema), +); /** * @param {string} filePath */ export async function loadCompatManagementRegistry(filePath) { const sourceText = await readFile(filePath, "utf8"); - const data = JSON.parse(sourceText); - - await validateCompatManagementSchema({ - data, - filePath, - }); + const data = await parseCompatManagementRegistry(JSON.parse(sourceText), filePath); /** @type {CompatManagementGroupRecord[]} */ const groups = []; @@ -30,6 +32,7 @@ export async function loadCompatManagementRegistry(filePath) { const entries = []; /** @type {Map} */ const entryByCompatKey = new Map(); + /** @type {Map} */ const declarationMappingByCompatKey = new Map( Object.entries(data.declarationMappings ?? {}) @@ -39,17 +42,21 @@ export async function loadCompatManagementRegistry(filePath) { memberNames: [...mapping.memberNames], }]), ); + /** @type {string[]} */ const compilerSupportSurfaces = data.compilerSupport - .flatMap((/** @type {{ surfaces: string[]; }} */ group) => group.surfaces) + .flatMap(group => group.surfaces) .sort(compareStringsCaseSensitive); + /** @type {string[]} */ const runtimeAliasSurfaces = data.runtimeAliases - .flatMap((/** @type {{ surfaces: string[]; }} */ group) => group.surfaces) + .flatMap(group => group.surfaces) .sort(compareStringsCaseSensitive); + assertUniqueSupportSurfaces(filePath, "compiler support", compilerSupportSurfaces); assertUniqueSupportSurfaces(filePath, "runtime alias", runtimeAliasSurfaces); const conflictingSupportSurfaces = compilerSupportSurfaces.filter(surface => runtimeAliasSurfaces.includes(surface)); + if (conflictingSupportSurfaces.length) { throw new Error( `Compat management registry ${filePath} declares surfaces as both compiler support and runtime aliases:\n` @@ -62,10 +69,12 @@ export async function loadCompatManagementRegistry(filePath) { for (const rawGroup of data.groups) { const group = normalizeCompatManagementGroup(rawGroup); groups.push(group); + for (const compatKey of group.compatKeys) { if (entryByCompatKey.has(compatKey)) { throw new Error(`Compat management registry ${filePath} declares duplicate compat key ${compatKey}`); } + const entry = { compatKey, groupId: group.id, @@ -79,6 +88,7 @@ export async function loadCompatManagementRegistry(filePath) { sourceUrls: group.sourceUrls, externalAction: group.externalAction, }; + entries.push(entry); entryByCompatKey.set(compatKey, entry); } @@ -86,6 +96,7 @@ export async function loadCompatManagementRegistry(filePath) { const unmanagedMappingKeys = [...declarationMappingByCompatKey.keys()] .filter(compatKey => !entryByCompatKey.has(compatKey)); + if (unmanagedMappingKeys.length) { throw new Error( `Compat management registry ${filePath} declares mappings for unmanaged compat keys:\n` @@ -113,6 +124,7 @@ export async function loadCompatManagementRegistry(filePath) { */ function assertUniqueSupportSurfaces(filePath, groupKind, surfaces) { const duplicates = surfaces.filter((surface, index) => surfaces.indexOf(surface) !== index); + if (duplicates.length) { throw new Error( `Compat management registry ${filePath} declares duplicate ${groupKind} surfaces:\n` @@ -124,42 +136,31 @@ function assertUniqueSupportSurfaces(filePath, groupKind, surfaces) { } /** - * @param {{ - * data: any; - * filePath: string; - * }} options + * @param {unknown} data + * @param {string} filePath */ -async function validateCompatManagementSchema(options) { - const schema = await schemaPromise; - const ajv = createAjv2020({ - allErrors: true, - strict: true, - }); - const validate = ajv.compile(schema); - const valid = validate(options.data); - - if (valid) { - return; +async function parseCompatManagementRegistry(data, filePath) { + const validate = await validatorPromise; + + if (validate(data)) { + return data; } - const errors = (validate.errors ?? []) - .map( - /** @param {import("ajv").ErrorObject} error */ - error => { - const instancePath = error.instancePath || "/"; - if (error.keyword === "additionalProperties" && error.params && "additionalProperty" in error.params) { - return `${instancePath}: unexpected property ${error.params.additionalProperty}`; - } - return `${instancePath}: ${error.message ?? error.keyword}`; - }, - ) - .join("\n"); + const errors = (validate.errors ?? []).map(error => { + const instancePath = error.instancePath || "/"; + + if (error.keyword === "additionalProperties" && "additionalProperty" in error.params) { + return `${instancePath}: unexpected property ${error.params.additionalProperty}`; + } + + return `${instancePath}: ${error.message ?? error.keyword}`; + }).join("\n"); - throw new Error(`Compat management registry ${options.filePath} failed JSON schema validation:\n${errors}`); + throw new Error(`Compat management registry ${filePath} failed JSON schema validation:\n${errors}`); } /** - * @param {any} rawGroup + * @param {CompatManagementGroupRecord} rawGroup */ function normalizeCompatManagementGroup(rawGroup) { return { @@ -190,17 +191,6 @@ function hashText(value) { return `sha256-${createHash("sha256").update(value).digest("hex")}`; } -/** - * `ajv/dist/2020.js` exposes a constructor at runtime, but the ESM typing shape - * is awkward under `checkJs`. Keep the cast at this boundary and keep the rest - * of the file strongly typed. - * - * @param {ConstructorParameters[0]} options - */ -function createAjv2020(options) { - return new (/** @type {typeof import("ajv").default} */ (/** @type {unknown} */ (Ajv2020Import)))(options); -} - /** * @typedef {{ * id: string; @@ -211,7 +201,7 @@ function createAjv2020(options) { * expectedResolutionKinds?: string[]; * reason: string; * sourceUrls: string[]; - * externalAction?: any; + * externalAction: ExternalAction; * compatKeys: string[]; * }} CompatManagementGroupRecord */ @@ -228,7 +218,7 @@ function createAjv2020(options) { * declarationMapping?: DeclarationMapping; * reason: string; * sourceUrls: string[]; - * externalAction?: any; + * externalAction: ExternalAction; * }} CompatManagementEntry */ @@ -238,3 +228,21 @@ function createAjv2020(options) { * memberNames: string[]; * }} DeclarationMapping */ + +/** + * @typedef {{ + * kind: "typescript-baseline-lib/compat-management-registry"; + * schemaVersion: 1; + * declarationMappings?: Record; + * compilerSupport: SupportGroup[]; + * runtimeAliases: SupportGroup[]; + * groups: CompatManagementGroupRecord[]; + * }} RegistrySource + * + * @typedef {{ id: string; reason: string; sourceUrls: string[]; surfaces: string[]; }} SupportGroup + * + * @typedef {{ kind: "none"; note: string; } + * | { kind: "watch-existing" | "consider-focused-issue"; repo: string; targetUrl: string; note: string; } + * | { kind: "watch-proposal"; targetUrl: string; note: string; } + * | { kind: "file-issue"; repo: string; targetUrl?: string; note: string; }} ExternalAction + */ diff --git a/lib/generator.mjs b/lib/generator.mjs index 36d4f90..5958d1a 100644 --- a/lib/generator.mjs +++ b/lib/generator.mjs @@ -33,6 +33,7 @@ import { isLibCompatKey, } from "./surface-inventory.mjs"; import { loadAllowlistRegistry } from "./allowlist-registry.mjs"; +import { hasDeclarationSurface } from "./resolution-kinds.mjs"; const RUNTIME_DECLARATION_KINDS = new Set(["var", "function", "class", "enum", "namespace"]); const NON_MERGEABLE_ALLOW_CONTAINER_KINDS = new Set(["class", "type-literal-var"]); @@ -52,21 +53,7 @@ const SURFACE_DEFINING_RESOLUTION_KINDS = new Set([ "type-property", "root-availability", ]); -const DECLARATION_BACKED_RESOLUTION_KINDS = new Set([ - "constructor", - "inherited-member", - "member", - "option-property", - "root-availability", - "signature-compat", - "transform-only", - "type-property", -]); -const NON_DECLARATION_RESOLUTION_KINDS = new Set([ - "already-excluded-upstream", - "behavioral", - "not-modeled-upstream", -]); + /** * @param {{ @@ -632,7 +619,7 @@ function createAllowEntries(options) { ...row.transforms.map(transform => transform.unitId), ]; if ( - ["already-excluded-upstream", "behavioral", "not-modeled-upstream"].includes(row.resolutionKind) + !hasDeclarationSurface(row.resolutionKind) || !rowUnitIds.length || rowUnitIds.some(unitId => !options.emittedBaselineUnitIds.has(unitId)) ) { @@ -657,7 +644,7 @@ function createAllowEntries(options) { const unitIds = new Set(); for (const row of lowRows) { - if (["already-excluded-upstream", "behavioral", "not-modeled-upstream"].includes(row.resolutionKind)) { + if (!hasDeclarationSurface(row.resolutionKind)) { throw new Error( `Allow entry ${registryEntry.entryName} cannot emit ${row.compatKey} (${row.resolutionKind})`, ); @@ -888,10 +875,7 @@ function createYearCompatAudit(classifiedCompatRows) { /** @type {string[]} */ const notModeledUpstreamCompatKeys = []; for (const row of classifiedCompatRows) { - const declarationBacked = DECLARATION_BACKED_RESOLUTION_KINDS.has(row.resolutionKind); - if (!declarationBacked && !NON_DECLARATION_RESOLUTION_KINDS.has(row.resolutionKind)) { - throw new Error(`Unknown compat resolution kind ${row.resolutionKind}`); - } + const declarationBacked = hasDeclarationSurface(row.resolutionKind); if (!row.includeInTarget) { continue; } diff --git a/lib/installed-package.mjs b/lib/installed-package.mjs index a37e2c0..5ae5ea8 100644 --- a/lib/installed-package.mjs +++ b/lib/installed-package.mjs @@ -22,6 +22,7 @@ function createRepoRequire(repoRoot) { */ export function resolveInstalledPackageRoot(repoRoot, packageName) { const packageJsonFromNodeModules = path.join(repoRoot, "node_modules", ...packageName.split("/"), "package.json"); + if (existsSync(packageJsonFromNodeModules)) { return path.dirname(packageJsonFromNodeModules); } @@ -32,13 +33,17 @@ export function resolveInstalledPackageRoot(repoRoot, packageName) { for (let depth = 0; depth < 5; depth++) { const packageJsonPath = path.join(currentDirectory, "package.json"); + if (existsSync(packageJsonPath)) { return currentDirectory; } + const parentDirectory = path.dirname(currentDirectory); + if (parentDirectory === currentDirectory) { break; } + currentDirectory = parentDirectory; } @@ -73,5 +78,6 @@ export function npmViewField(repoRoot, packageSpecifier, field) { cwd: repoRoot, encoding: "utf8", })).trim(); + return output ? JSON.parse(output) : undefined; } diff --git a/lib/manifest-snapshot.mjs b/lib/manifest-snapshot.mjs index d6e7b0f..cc79b2f 100644 --- a/lib/manifest-snapshot.mjs +++ b/lib/manifest-snapshot.mjs @@ -31,9 +31,11 @@ export async function refreshManifestSnapshot(options) { const manifest = JSON.parse(await readFile(manifestPath, "utf8")); const manifestDirectory = path.dirname(manifestPath); + const datasetPath = manifest.dataset ? path.resolve(manifestDirectory, manifest.dataset) : path.join(repoRoot, "datasets", "web-features-js-compat.json"); + const derivedDirectory = path.join(repoRoot, "derived", "current"); const dataset = JSON.parse(await readFile(datasetPath, "utf8")); const repoPackageJson = JSON.parse(await readFile(path.join(repoRoot, "package.json"), "utf8")); diff --git a/lib/negative-probes.mjs b/lib/negative-probes.mjs index 488ee5f..d040e29 100644 --- a/lib/negative-probes.mjs +++ b/lib/negative-probes.mjs @@ -143,12 +143,14 @@ export function selectActiveNegativeProbes(classifiedCompatRows) { for (const probe of STABLE_NEGATIVE_PROBES) { const row = rowByCompatKey.get(probe.compatKey); + if (!row) { throw new Error( `Stable negative probe ${probe.compatKey} is missing from the classification. ` + `Update lib/negative-probes.mjs to track an API that still exists in web-features.`, ); } + if (row.includeInTarget) { throw new Error( `Stable negative probe ${probe.compatKey} is now included in the baseline target. ` @@ -159,8 +161,10 @@ export function selectActiveNegativeProbes(classifiedCompatRows) { const activeLowProbes = LOW_NEGATIVE_PROBE_CANDIDATES.filter(probe => { const row = rowByCompatKey.get(probe.compatKey); + return row !== undefined && !row.includeInTarget; }); + if (!activeLowProbes.length) { throw new Error( "Every Baseline-low negative probe candidate has been promoted to widely available. " diff --git a/lib/net-retry.mjs b/lib/net-retry.mjs index 4752d3d..8f4ccbf 100644 --- a/lib/net-retry.mjs +++ b/lib/net-retry.mjs @@ -9,6 +9,7 @@ // publishing. Failure is always safe (eventually throws). const DEFAULT_ATTEMPTS = 3; + const DEFAULT_BASE_DELAY_MS = 2000; /** @@ -41,6 +42,7 @@ export function retrySync(label, operation, options = {}) { } catch (error) { lastError = error; + if (attempt < attempts) { warnRetry(label, attempt, error); sleepSync(attempt * baseDelayMs); @@ -72,6 +74,7 @@ export async function retryAsync(label, operation, options = {}) { } catch (error) { lastError = error; + if (attempt < attempts) { warnRetry(label, attempt, error); await sleepAsync(attempt * baseDelayMs); diff --git a/lib/resolution-kinds.mjs b/lib/resolution-kinds.mjs new file mode 100644 index 0000000..1cc3b35 --- /dev/null +++ b/lib/resolution-kinds.mjs @@ -0,0 +1,31 @@ +// @ts-check + +const DECLARATION_BACKED_RESOLUTION_KINDS = new Set([ + "constructor", + "inherited-member", + "member", + "option-property", + "root-availability", + "signature-compat", + "transform-only", + "type-property", +]); + +const NON_DECLARATION_RESOLUTION_KINDS = new Set([ + "already-excluded-upstream", + "behavioral", + "not-modeled-upstream", +]); + +/** @param {string} resolutionKind */ +export function hasDeclarationSurface(resolutionKind) { + if (DECLARATION_BACKED_RESOLUTION_KINDS.has(resolutionKind)) { + return true; + } + + if (NON_DECLARATION_RESOLUTION_KINDS.has(resolutionKind)) { + return false; + } + + throw new Error(`Unknown compat resolution kind: ${resolutionKind}`); +} diff --git a/lib/shared.mjs b/lib/shared.mjs index d4869c8..e2c1f07 100644 --- a/lib/shared.mjs +++ b/lib/shared.mjs @@ -17,6 +17,7 @@ export function compareStringsCaseSensitive(left, right) { */ export function formatPathForReport(repoRoot, filePath) { const relativePath = path.relative(repoRoot, filePath); + return relativePath.startsWith("..") ? filePath : relativePath; } @@ -31,13 +32,17 @@ export function resolveManagedOutputPath(relativePath, repoRoot, manifestPath, p if (!relativePath) { throw new Error(`Manifest ${manifestPath} is missing ${propertyName}`); } + if (path.isAbsolute(relativePath) || relativePath.split(/[\\/]/u).includes("..")) { throw new Error(`Manifest ${propertyName} must be a repo-relative path without '..': ${relativePath}`); } + const outputPath = path.resolve(repoRoot, relativePath); + if (!allowedRoots.some(allowedRoot => isPathWithin(allowedRoot, outputPath))) { throw new Error(`Manifest ${propertyName} is outside its managed output root: ${relativePath}`); } + return outputPath; } @@ -50,6 +55,7 @@ export function requireRelativeManifestPath(relativePath, manifestPath, property if (!relativePath) { throw new Error(`Manifest ${manifestPath} is missing ${propertyName}`); } + return path.resolve(path.dirname(manifestPath), relativePath); } @@ -59,6 +65,7 @@ export function requireRelativeManifestPath(relativePath, manifestPath, property */ export function isPathWithin(parentPath, childPath) { const relativePath = path.relative(path.resolve(parentPath), path.resolve(childPath)); + return relativePath === "" || (!path.isAbsolute(relativePath) && relativePath !== ".." && !relativePath.startsWith(`..${path.sep}`)); } @@ -72,14 +79,19 @@ export async function removeManagedPath(targetPath, boundaryRoot, allowedRoots, if (!allowedRoots.some(allowedRoot => isPathWithin(allowedRoot, targetPath))) { throw new Error(`Refusing to remove path outside managed roots: ${targetPath}`); } + const relativePath = path.relative(path.resolve(boundaryRoot), path.resolve(targetPath)); + if (path.isAbsolute(relativePath) || relativePath === ".." || relativePath.startsWith(`..${path.sep}`)) { throw new Error(`Refusing to remove path outside boundary root: ${targetPath}`); } + let currentPath = path.resolve(boundaryRoot); + for (const segment of relativePath.split(path.sep).filter(Boolean)) { currentPath = path.join(currentPath, segment); let stats; + try { stats = await lstat(currentPath); } @@ -87,11 +99,14 @@ export async function removeManagedPath(targetPath, boundaryRoot, allowedRoots, if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") { break; } + throw error; } + if (stats.isSymbolicLink()) { throw new Error(`Refusing to remove through symbolic link: ${currentPath}`); } } + await rm(targetPath, { recursive: options.recursive ?? false, force: true }); } diff --git a/lib/text-patch.mjs b/lib/text-patch.mjs index b44ccd3..dd512a4 100644 --- a/lib/text-patch.mjs +++ b/lib/text-patch.mjs @@ -13,9 +13,11 @@ import path from "node:path"; */ export function copyFileIfChanged(sourcePath, targetPath) { const sourceText = fs.readFileSync(sourcePath, "utf8"); + const currentText = fs.existsSync(targetPath) ? fs.readFileSync(targetPath, "utf8") : undefined; + const changed = currentText !== sourceText; if (changed) { @@ -92,6 +94,7 @@ export function listRelativeFiles(rootDirectory) { const relativePaths = []; visit(rootDirectory, ""); + return relativePaths.sort(); /** @@ -103,6 +106,7 @@ export function listRelativeFiles(rootDirectory) { const entryRelativePath = currentRelativePath ? path.join(currentRelativePath, entry.name) : entry.name; + const entryPath = path.join(currentDirectory, entry.name); if (entry.isDirectory()) { diff --git a/lib/toolchain-libs.mjs b/lib/toolchain-libs.mjs index 0fd35ae..a99ae2f 100644 --- a/lib/toolchain-libs.mjs +++ b/lib/toolchain-libs.mjs @@ -8,41 +8,54 @@ import { resolveInstalledPackageRoot, } from "./installed-package.mjs"; -// From TypeScript 7 on, lib.*.d.ts ships in platform-specific packages -// (@typescript/typescript--), not in `typescript` itself. -// This module guarantees, fail-closed via the content hash recorded in the -// manifest, that generation reads identical lib content on any platform. +// TypeScript 7 ships declarations in platform packages; the hash pins their shared content. -/** - * @param {any} manifest - */ +/** @param {{ libSource?: unknown; }} manifest */ export function readLibSourceConfig(manifest) { const libSource = manifest.libSource; - if (!libSource || typeof libSource !== "object") { + + if (!libSource || typeof libSource !== "object" || Array.isArray(libSource)) { throw new Error("Manifest is missing libSource (TypeScript 7 toolchain requires an explicit lib source pin)"); } - if (!libSource.basePackage || typeof libSource.basePackage !== "string") { + + if (!("basePackage" in libSource) || typeof libSource.basePackage !== "string" || !libSource.basePackage) { throw new Error("Manifest is missing libSource.basePackage"); } - if (!libSource.platformPackagePrefix || typeof libSource.platformPackagePrefix !== "string") { + + if (!("platformPackagePrefix" in libSource) || typeof libSource.platformPackagePrefix !== "string" || !libSource.platformPackagePrefix) { throw new Error("Manifest is missing libSource.platformPackagePrefix"); } - if (!Array.isArray(libSource.referencePlatforms) || libSource.referencePlatforms.length < 2) { - throw new Error("Manifest libSource.referencePlatforms must list at least two platforms for cross-platform verification"); + + if (!("referencePlatforms" in libSource) || !Array.isArray(libSource.referencePlatforms)) { + throw new Error("Manifest libSource.referencePlatforms must list at least two distinct platforms"); + } + + const referencePlatforms = libSource.referencePlatforms.map((/** @type {unknown} */ platform) => { + if (typeof platform !== "string" || !/^[a-z0-9]+-[a-z0-9]+$/u.test(platform)) { + throw new Error("Manifest libSource.referencePlatforms contains an invalid platform"); + } + + return platform; + }); + + if (referencePlatforms.length < 2 || new Set(referencePlatforms).size !== referencePlatforms.length) { + throw new Error("Manifest libSource.referencePlatforms must list at least two distinct platforms"); } - if (!libSource.libContentHash || !/^sha256-[0-9a-f]{64}$/u.test(libSource.libContentHash)) { + + if (!("libContentHash" in libSource) || typeof libSource.libContentHash !== "string" || !/^sha256-[0-9a-f]{64}$/u.test(libSource.libContentHash)) { throw new Error("Manifest is missing a valid libSource.libContentHash (sha256-)"); } - if (!Number.isInteger(libSource.libFileCount) || libSource.libFileCount <= 0) { + + if (!("libFileCount" in libSource) || typeof libSource.libFileCount !== "number" || !Number.isInteger(libSource.libFileCount) || libSource.libFileCount <= 0) { throw new Error("Manifest is missing a valid libSource.libFileCount"); } return { - basePackage: /** @type {string} */ (libSource.basePackage), - platformPackagePrefix: /** @type {string} */ (libSource.platformPackagePrefix), - referencePlatforms: /** @type {string[]} */ (libSource.referencePlatforms), - libContentHash: /** @type {string} */ (libSource.libContentHash), - libFileCount: /** @type {number} */ (libSource.libFileCount), + basePackage: libSource.basePackage, + platformPackagePrefix: libSource.platformPackagePrefix, + referencePlatforms, + libContentHash: libSource.libContentHash, + libFileCount: libSource.libFileCount, }; } @@ -55,6 +68,7 @@ export function readLibSourceConfig(manifest) { export function resolvePlatformKey(options = {}) { const platform = options.platform ?? process.platform; const arch = options.arch ?? process.arch; + return `${platform}-${arch}`; } @@ -65,7 +79,7 @@ export function resolvePlatformKey(options = {}) { * * @param {{ * repoRoot: string; - * manifest: any; + * manifest: { libSource?: unknown; }; * platformKey?: string; * }} options */ @@ -80,6 +94,7 @@ export async function resolvePlatformLibPackageName(options) { const available = Object.keys(optionalDependencies) .filter(name => name.startsWith(libSource.platformPackagePrefix)) .sort(); + throw new Error([ `Installed ${libSource.basePackage}@${basePackageJson.version} does not declare ${expectedName}.`, "The platform lib package naming scheme may have changed upstream.", @@ -117,8 +132,10 @@ export function computeLibDirectoryContentHash(libDirectory) { const fileHash = createHash("sha256") .update(fs.readFileSync(path.join(libDirectory, fileName))) .digest("hex"); + return `${fileName}\n${fileHash}`; }); + const aggregate = createHash("sha256") .update(digestManifestLines.join("\n")) .digest("hex"); @@ -137,7 +154,7 @@ export function computeLibDirectoryContentHash(libDirectory) { * * @param {{ * repoRoot: string; - * manifest: any; + * manifest: { libSource?: unknown; }; * }} options */ export async function verifyLibSource(options) { @@ -153,6 +170,7 @@ export async function verifyLibSource(options) { "Run scripts/update-typescript-toolchain.mjs to refresh the lib source pin.", ].join("\n")); } + if (computed.hash !== libSource.libContentHash) { throw new Error([ `Installed ${platformPackageName} lib content hash ${computed.hash} does not match the manifest pin ${libSource.libContentHash}.`, diff --git a/lib/typescript-source.mjs b/lib/typescript-source.mjs index 6027dc3..893d9e6 100644 --- a/lib/typescript-source.mjs +++ b/lib/typescript-source.mjs @@ -6,6 +6,7 @@ import path from "node:path"; import { retrySync } from "./net-retry.mjs"; export const defaultTypeScriptRepository = "https://github.com/microsoft/TypeScript.git"; + export const defaultTypeScriptRef = "main"; /** @@ -36,12 +37,15 @@ export function readTypeScriptSourcePin(manifest) { if (!repository || typeof repository !== "string") { throw new Error("Manifest is missing typescriptSource.repository"); } + if (!ref || typeof ref !== "string") { throw new Error("Manifest is missing typescriptSource.ref"); } + if (!commit || typeof commit !== "string" || !/^[0-9a-f]{40}$/u.test(commit)) { throw new Error("Manifest is missing a valid typescriptSource.commit"); } + return { repository, ref, @@ -69,6 +73,7 @@ export function checkoutTypeScriptSource(options) { const reason = existingCommit === pin.commit ? "The checkout has local changes." : `The checkout is at ${existingCommit ?? "an unknown revision"}, not ${pin.commit}.`; + throw new Error( `${reason} Refusing to replace ${outDirectory}. Re-run with --force or choose an empty path.`, ); @@ -87,6 +92,7 @@ export function checkoutTypeScriptSource(options) { }); const checkedOutCommit = readGitHeadCommit(outDirectory); + if (checkedOutCommit !== pin.commit) { throw new Error( `Pinned checkout mismatch. Expected ${pin.commit}, got ${checkedOutCommit ?? ""}.`, @@ -106,11 +112,13 @@ export function checkoutTypeScriptSource(options) { */ function resolveGitRefCommit(repository, ref) { const remoteRef = ref.startsWith("refs/") ? ref : `refs/heads/${ref}`; + const output = retrySync(`resolve ${ref} from ${repository}`, () => execFileSync( "git", ["ls-remote", repository, remoteRef], { encoding: "utf8" }, )).trim(); + const lines = output.split(/\r?\n/u).filter(Boolean); if (lines.length !== 1) { @@ -118,6 +126,7 @@ function resolveGitRefCommit(repository, ref) { } const [commit, resolvedRef] = lines[0].split(/\s+/u); + if (!commit || !/^[0-9a-f]{40}$/u.test(commit) || resolvedRef !== remoteRef) { throw new Error(`Resolved invalid commit for ${remoteRef}: ${lines[0]}`); } @@ -154,5 +163,6 @@ function isGitWorkingTreeClean(directory) { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], }); + return status.length === 0; } diff --git a/lib/year-contracts.mjs b/lib/year-contracts.mjs index ef560a6..1d7d917 100644 --- a/lib/year-contracts.mjs +++ b/lib/year-contracts.mjs @@ -14,10 +14,12 @@ export function compareYearContracts(previousReportText, currentReportText) { for (const [year, previousContract] of previousContracts) { const currentContract = currentContracts.get(year); + if (!currentContract) { changes.push({ year, kind: "removed" }); continue; } + if (JSON.stringify(currentContract) === JSON.stringify(previousContract)) { continue; } @@ -25,9 +27,11 @@ export function compareYearContracts(previousReportText, currentReportText) { const removedCompatKey = previousContract.includedCompatKeys.some( compatKey => !currentContract.includedCompatKeys.includes(compatKey), ); + const addedCompatKey = currentContract.includedCompatKeys.some( compatKey => !previousContract.includedCompatKeys.includes(compatKey), ); + changes.push({ year, kind: removedCompatKey || !addedCompatKey ? "changed" : "expanded", @@ -41,6 +45,7 @@ export function compareYearContracts(previousReportText, currentReportText) { } changes.sort((left, right) => left.year - right.year || compareStringsCaseSensitive(left.kind, right.kind)); + return { changes, }; @@ -55,8 +60,10 @@ function readYearContracts(reportText, label) { const report = JSON.parse(reportText); /** @type {Map} */ const contracts = new Map(); + for (const entry of report.yearEntries ?? []) { const year = Number.isInteger(entry.year) ? Number(entry.year) : undefined; + if ( year === undefined || typeof entry.contentHash !== "string" @@ -68,11 +75,13 @@ function readYearContracts(reportText, label) { ) { throw new Error(`Invalid ${label} Baseline year contract report`); } + contracts.set(year, { contentHash: entry.contentHash, includedCompatKeys: [...entry.includedCompatKeys].sort(compareStringsCaseSensitive), notModeledUpstreamCompatKeys: [...entry.notModeledUpstreamCompatKeys].sort(compareStringsCaseSensitive), }); } + return contracts; } diff --git a/manifests/baseline-js.json b/manifests/baseline-js.json index e24b22a..6205454 100644 --- a/manifests/baseline-js.json +++ b/manifests/baseline-js.json @@ -1,11 +1,11 @@ { "snapshot": { "name": "baseline-js", - "baselineDate": "2026-07-21", + "baselineDate": "2026-10-02", "generatorVersion": "0.0.1", "typescriptVersion": "7.0.2", - "webFeaturesPackageVersion": "3.34.1", - "webFeaturesGitHead": "c8b4c88e92fe9f49168371f766358bea6a4f18ab", + "webFeaturesPackageVersion": "3.40.1", + "webFeaturesGitHead": "8606657a71535d688f5473579712717fe9442c0f", "typescriptStradaVersion": "6.0.3" }, "toolchain": { diff --git a/package-lock.json b/package-lock.json index ab030dc..f463c53 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "ajv": "8.20.0", "typescript": "7.0.2", "typescript-strada": "npm:typescript@6.0.3", - "web-features": "3.34.1" + "web-features": "3.40.1" }, "devDependencies": { "@types/node": "24.10.0", @@ -461,9 +461,9 @@ "license": "MIT" }, "node_modules/web-features": { - "version": "3.34.1", - "resolved": "https://registry.npmjs.org/web-features/-/web-features-3.34.1.tgz", - "integrity": "sha512-x+TGFo3TzB8dmXHNvveQXmaoYhzNH6blW93TO0u1Ygvr8/Bk3KIFZC9LVofHNnkcEmeodh8P3Mes5fPWyO7ATA==", + "version": "3.40.1", + "resolved": "https://registry.npmjs.org/web-features/-/web-features-3.40.1.tgz", + "integrity": "sha512-HRdUYqZRNawtIEI39iKjk6E6g1yO/bct/8bFcD/O+bNGYsbG475PV80+HTrpkYOiZnoEaVSMu5Q/LTVwnBH5ag==", "license": "Apache-2.0" }, "node_modules/yaml": { diff --git a/package.json b/package.json index 5f6936c..300b704 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "ajv": "8.20.0", "typescript": "7.0.2", "typescript-strada": "npm:typescript@6.0.3", - "web-features": "3.34.1" + "web-features": "3.40.1" }, "devDependencies": { "@types/node": "24.10.0", diff --git a/registry/compat-management.json b/registry/compat-management.json index 97daddf..8eea7b6 100644 --- a/registry/compat-management.json +++ b/registry/compat-management.json @@ -439,6 +439,30 @@ "javascript.builtins.Iterator.concat" ] }, + { + "id": "iterator-join-tracked", + "category": "tracked_by_canonical_lib", + "delivery": "defer-upstream", + "upstreamState": "tracked", + "compatRoot": "Iterator", + "expectedResolutionKinds": [ + "not-modeled-upstream" + ], + "reason": "Iterator.join is tracked for TypeScript 7.1 but is absent from the pinned libraries.", + "sourceUrls": [ + "https://github.com/microsoft/TypeScript/issues/63700", + "https://tc39.es/proposal-iterator-join/#sec-iterator.prototype.join" + ], + "externalAction": { + "kind": "watch-existing", + "repo": "microsoft/TypeScript", + "targetUrl": "https://github.com/microsoft/TypeScript/issues/63700", + "note": "Update the mapping when the declaration lands." + }, + "compatKeys": [ + "javascript.builtins.Iterator.join" + ] + }, { "id": "math-sum-precise-tracked", "category": "tracked_by_canonical_lib", diff --git a/scripts/check-typescript-peer-latest.mjs b/scripts/check-typescript-peer-latest.mjs index 74dd6c4..726780a 100644 --- a/scripts/check-typescript-peer-latest.mjs +++ b/scripts/check-typescript-peer-latest.mjs @@ -7,10 +7,13 @@ 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/checkout-typescript-source.mjs b/scripts/checkout-typescript-source.mjs index 08fab2b..4bf751a 100644 --- a/scripts/checkout-typescript-source.mjs +++ b/scripts/checkout-typescript-source.mjs @@ -9,14 +9,21 @@ import { } from "../lib/typescript-source.mjs"; const scriptPath = fileURLToPath(import.meta.url); + const scriptDirectory = path.dirname(scriptPath); + const repoRoot = path.resolve(scriptDirectory, ".."); + const defaultManifestPath = path.join(repoRoot, "manifests", "baseline-js.json"); const args = parseArgs(process.argv.slice(2)); + const manifestPath = path.resolve(args.manifest ?? defaultManifestPath); + const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); + const pin = readTypeScriptSourcePin(manifest); + const summary = checkoutTypeScriptSource({ manifest, outDirectory: args.out ?? path.join(repoRoot, ".tmp", "TypeScript"), @@ -42,6 +49,7 @@ function parseArgs(argv) { for (let index = 0; index < argv.length; index++) { const current = argv[index]; + switch (current) { case "--manifest": args.manifest = requireArgValue(argv[++index], current); @@ -72,6 +80,7 @@ function requireArgValue(value, flagName) { if (!value) { throw new Error(`Missing value for ${flagName}`); } + return value; } diff --git a/scripts/generate.mjs b/scripts/generate.mjs index 153bd14..023c6d2 100644 --- a/scripts/generate.mjs +++ b/scripts/generate.mjs @@ -5,11 +5,15 @@ import { fileURLToPath } from "node:url"; import { generateFirstClassBaselineLib } from "../lib/generator.mjs"; const scriptPath = fileURLToPath(import.meta.url); + const scriptDirectory = path.dirname(scriptPath); + const repoRoot = path.resolve(scriptDirectory, ".."); + const defaultManifestPath = path.join(repoRoot, "manifests", "baseline-js.json"); const args = parseArgs(process.argv.slice(2)); + const manifestPath = path.resolve(args.manifest ?? defaultManifestPath); await main(); @@ -37,6 +41,7 @@ function parseArgs(argv) { for (let index = 0; index < argv.length; index++) { const current = argv[index]; + switch (current) { case "--manifest": args.manifest = requireArgValue(argv[++index], current); @@ -61,6 +66,7 @@ function requireArgValue(value, flagName) { if (!value) { throw new Error(`Missing value for ${flagName}`); } + return value; } diff --git a/scripts/prepare-typescript-pr.mjs b/scripts/prepare-typescript-pr.mjs index d7d39a1..2abc287 100644 --- a/scripts/prepare-typescript-pr.mjs +++ b/scripts/prepare-typescript-pr.mjs @@ -11,14 +11,21 @@ import { generateTypeScriptProposalLib } from "../lib/typescript-proposal.mjs"; import { readTypeScriptSourcePin } from "../lib/typescript-source.mjs"; const scriptPath = fileURLToPath(import.meta.url); + const scriptDirectory = path.dirname(scriptPath); + const repoRoot = path.resolve(scriptDirectory, ".."); + const defaultSummaryPath = path.join(repoRoot, ".tmp", "typescript-pr-summary.md"); + const manifestPath = path.join(repoRoot, "manifests", "baseline-js.json"); const args = parseArgs(process.argv.slice(2)); + const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); + const expectedCommit = readTypeScriptSourcePin(manifest).commit; + const proposal = await generateTypeScriptProposalLib({ repoRoot, manifestPath, @@ -29,6 +36,7 @@ const proposal = await generateTypeScriptProposalLib({ }); let summary; + try { summary = prepareTypeScriptBaselinePatch({ repoRoot, @@ -41,6 +49,7 @@ try { finally { proposal.cleanup(); } + const summaryText = renderTypeScriptPatchSummary(summary); if (args.out) { @@ -63,6 +72,7 @@ function parseArgs(argv) { for (let index = 0; index < argv.length; index++) { const current = argv[index]; + switch (current) { case "--allow-unpinned": args.allowUnpinned = true; @@ -93,6 +103,7 @@ function requireArgValue(value, flagName) { if (!value) { throw new Error(`Missing value for ${flagName}`); } + return value; } diff --git a/scripts/test-typescript-integration.mjs b/scripts/test-typescript-integration.mjs index 9d6fcb8..058072b 100644 --- a/scripts/test-typescript-integration.mjs +++ b/scripts/test-typescript-integration.mjs @@ -21,14 +21,23 @@ import { generateTypeScriptProposalLib } from "../lib/typescript-proposal.mjs"; import { readTypeScriptSourcePin } from "../lib/typescript-source.mjs"; const scriptPath = fileURLToPath(import.meta.url); + const scriptDirectory = path.dirname(scriptPath); + const repoRoot = path.resolve(scriptDirectory, ".."); + const manifestPath = path.join(repoRoot, "manifests", "baseline-js.json"); + const fixturesRoot = path.join(repoRoot, "fixtures", "typescript"); + const defaultSummaryPath = path.join(repoRoot, ".tmp", "typescript-integration-summary.md"); + const defaultDiffPath = path.join(repoRoot, ".tmp", "typescript-baseline-changes.diff"); + const defaultFocusedBaselinesDirectory = path.join(repoRoot, ".tmp", "typescript-focused-artifact"); + const defaultLocalBaselinesDirectory = path.join(repoRoot, ".tmp", "typescript-raw-local-baselines"); + const TYPESCRIPT_PROPOSAL_PATHS = [ path.join("tsc", "internal", "bundled", "source", "baseline.d.ts"), path.join("tsc", "internal", "bundled", "source", "libs.json"), @@ -50,6 +59,7 @@ await main(); async function main() { const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); const expectedCommit = readTypeScriptSourcePin(manifest).commit; + const proposal = await generateTypeScriptProposalLib({ repoRoot, manifestPath, @@ -58,7 +68,9 @@ async function main() { expectedCommit, allowUnpinned: args.allowUnpinned, }); + let patchSummary; + try { patchSummary = prepareTypeScriptBaselinePatch({ repoRoot, @@ -72,6 +84,7 @@ async function main() { finally { proposal.cleanup(); } + const integrationDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "ts-baseline-ts-integration-")); /** @type {ReturnType | undefined} */ let smokeResults; @@ -106,23 +119,28 @@ async function main() { // the complete TypeScript suite to pass on the accepted state. if (args.mode === "gate" || args.mode === "full") { extendedResults.targetedHarness = runTargetedHarness(patchSummary.typescriptDir); + if (!extendedResults.targetedHarness.ok) { blockingFailure = new Error( extendedResults.targetedHarness.output.trim() || "TypeScript targeted libBaseline harness failed", ); } + extendedResults.focusedBaselinesPath = copyFocusedBaselinesArtifact({ typescriptDir: patchSummary.typescriptDir, outputDirectory: args.focusedBaselinesOut, }); + const unexpectedPaths = findUnexpectedTypeScriptPatchPaths( patchSummary.typescriptDir, TYPESCRIPT_PROPOSAL_PATHS, ); + if (unexpectedPaths.length) { blockingFailure ??= createUnexpectedPathsError("TypeScript generation", unexpectedPaths); } + if (args.baselineDiffOut) { fs.mkdirSync(path.dirname(args.baselineDiffOut), { recursive: true }); fs.writeFileSync(args.baselineDiffOut, renderTypeScriptPatchDiff(patchSummary.typescriptDir)); @@ -145,6 +163,7 @@ async function main() { ["hereby", "baseline-accept"], { cwd: patchSummary.typescriptDir }, ); + if (!extendedResults.baselineAccept.ok) { blockingFailure ??= new Error( extendedResults.baselineAccept.output.trim() || "TypeScript baseline-accept failed", @@ -155,6 +174,7 @@ async function main() { patchSummary.typescriptDir, TYPESCRIPT_PROPOSAL_PATHS, ); + if (unexpectedPaths.length) { blockingFailure ??= createUnexpectedPathsError("TypeScript baseline-accept", unexpectedPaths); } @@ -164,6 +184,7 @@ async function main() { ["test"], { cwd: patchSummary.typescriptDir }, ); + if (!extendedResults.fullSuiteAfterBaselineAccept.ok) { blockingFailure ??= new Error( extendedResults.fullSuiteAfterBaselineAccept.output.trim() @@ -172,8 +193,10 @@ async function main() { } } } + if (args.baselineDiffOut) { fs.mkdirSync(path.dirname(args.baselineDiffOut), { recursive: true }); + const diffText = extendedResults.baselineAccept.ok ? renderTypeScriptPatchDiff(patchSummary.typescriptDir) : renderUnavailableDiffArtifact({ @@ -182,6 +205,7 @@ async function main() { localBaselinesPath: extendedResults.localBaselinesPath, typescriptDir: patchSummary.typescriptDir, }); + fs.writeFileSync(args.baselineDiffOut, diffText); extendedResults.baselineDiffPath = args.baselineDiffOut; } @@ -206,6 +230,7 @@ async function main() { if (failure) { throw failure; } + if (blockingFailure) { throw blockingFailure; } @@ -222,6 +247,7 @@ function installTypeScriptDependencies(typescriptDir) { const installArgs = fs.existsSync(path.join(typescriptDir, "package-lock.json")) ? ["ci"] : ["install"]; + runNpm(typescriptDir, installArgs); } @@ -235,6 +261,7 @@ function runSmokeChecks(typescriptDir, integrationDirectory) { const negativeProbes = loadActiveNegativeProbes(); const negativeFlagPath = writeNegativeSmokeFixture(smokeRoot, negativeProbes); const tsconfigPath = writeSmokeTsconfig(smokeRoot); + const localTscPath = path.join( typescriptDir, "built", @@ -256,7 +283,9 @@ function runSmokeChecks(typescriptDir, integrationDirectory) { ["--strict", "--noEmit", "--lib", "baseline", negativeFlagPath], { cwd: typescriptDir }, ); + assert.equal(negativeResult.ok, false, "Expected negative baseline smoke to fail"); + for (const probe of negativeProbes) { assert.match( negativeResult.output, @@ -282,6 +311,7 @@ function loadActiveNegativeProbes() { const classification = JSON.parse( fs.readFileSync(path.join(repoRoot, "derived", "current", "classification.json"), "utf8"), ); + return selectActiveNegativeProbes(classification.classifiedCompatRows); } @@ -293,6 +323,7 @@ function writeNegativeSmokeFixture(smokeRoot, negativeProbes) { const targetPath = path.join(smokeRoot, "negative-flag.ts"); fs.mkdirSync(smokeRoot, { recursive: true }); fs.writeFileSync(targetPath, renderNegativeProbeSource(negativeProbes)); + return targetPath; } @@ -327,6 +358,7 @@ function writeSmokeTsconfig(smokeRoot) { files: ["positive-flag.ts"], }, undefined, 4)}\n`, ); + return tsconfigPath; } @@ -349,6 +381,7 @@ function writeSmokeTsconfig(smokeRoot) { */ function renderIntegrationSummary(options) { const blockingChecksPassed = !options.blockingFailureMessage; + const lines = [ "# TypeScript Integration Summary", "", @@ -369,6 +402,7 @@ function renderIntegrationSummary(options) { lines.push( `- Targeted harness test: ${formatBlockingResult(options.extendedResults.targetedHarness)}`, ); + if (options.extendedResults.focusedBaselinesPath) { lines.push(`- Focused integration artifact: \`${options.extendedResults.focusedBaselinesPath}\``); } @@ -380,10 +414,12 @@ function renderIntegrationSummary(options) { `- Baseline accept: ${formatBlockingResult(options.extendedResults.baselineAccept)}`, `- Post-accept full TypeScript suite: ${formatBlockingResult(options.extendedResults.fullSuiteAfterBaselineAccept)}`, ); + if (options.extendedResults.localBaselinesPath) { lines.push(`- Raw local baselines artifact: \`${options.extendedResults.localBaselinesPath}\``); } } + if (options.extendedResults.baselineDiffPath) { lines.push(`- Baseline diff artifact: \`${options.extendedResults.baselineDiffPath}\``); } @@ -450,6 +486,7 @@ function formatBlockingCheckStatus(ok, blockingFailureMessage) { if (ok) { return "passed (blocking)"; } + return blockingFailureMessage ? "failed (blocking)" : "not run"; } @@ -460,6 +497,7 @@ function formatBlockingResult(result) { if (!result) { return "not run"; } + return result.ok ? "passed (blocking)" : "failed (blocking)"; } @@ -470,6 +508,7 @@ function formatInitialSuiteResult(result) { if (!result) { return "not run"; } + return result.ok ? "passed (pre-accept)" : "failed (pre-accept baselines captured)"; } @@ -508,6 +547,7 @@ function parseArgs(argv) { for (let index = 0; index < argv.length; index++) { const current = argv[index]; + switch (current) { case "--allow-unpinned": args.allowUnpinned = true; @@ -529,12 +569,15 @@ function parseArgs(argv) { break; case "--mode": { const mode = requireArgValue(argv[++index], current); + if (mode !== "smoke" && mode !== "gate" && mode !== "full") { throw new Error(`Unsupported integration mode: ${mode}`); } + args.mode = mode; break; } + case "--skip-install": args.skipInstall = true; break; @@ -558,6 +601,7 @@ function requireArgValue(value, flagName) { if (!value) { throw new Error(`Missing value for ${flagName}`); } + return value; } @@ -591,17 +635,20 @@ function runNpm(cwd, args) { */ function runTargetedHarness(typescriptDir) { const testName = "TestLocal/libBaseline.ts"; + const result = runCommandAllowFailure( "go", ["test", "-json", "./internal/testrunner", "-run", "^TestLocal$/^libBaseline\\.ts$"], { cwd: path.join(typescriptDir, "tsc") }, ); + if (result.ok && !hasPassingGoTestEvent(result.output, testName)) { return { ok: false, output: `${result.output}\nExpected a passing Go test event for ${testName}.`, }; } + return result; } @@ -662,6 +709,7 @@ function runCommandAllowFailure(file, args, options) { */ function copyLocalBaselinesArtifact(options) { const sourceDirectory = path.join(options.typescriptDir, "tsc", "testdata", "baselines", "local"); + if (!fs.existsSync(sourceDirectory)) { return undefined; } @@ -669,6 +717,7 @@ function copyLocalBaselinesArtifact(options) { fs.rmSync(options.outputDirectory, { recursive: true, force: true }); fs.mkdirSync(path.dirname(options.outputDirectory), { recursive: true }); fs.cpSync(sourceDirectory, options.outputDirectory, { recursive: true }); + return options.outputDirectory; } @@ -683,16 +732,20 @@ function copyFocusedBaselinesArtifact(options) { // Keep a reviewer-sized snapshot of the proposal-specific patch surface. fs.mkdirSync(options.outputDirectory, { recursive: true }); let copiedFileCount = 0; + for (const relativePath of TYPESCRIPT_PROPOSAL_PATHS) { const sourcePath = path.join(options.typescriptDir, relativePath); + if (!fs.existsSync(sourcePath)) { continue; } + const targetPath = path.join(options.outputDirectory, relativePath); fs.mkdirSync(path.dirname(targetPath), { recursive: true }); fs.copyFileSync(sourcePath, targetPath); copiedFileCount++; } + return copiedFileCount > 0 ? options.outputDirectory : undefined; } diff --git a/scripts/update-typescript-toolchain.mjs b/scripts/update-typescript-toolchain.mjs index 2caf36c..87515ca 100644 --- a/scripts/update-typescript-toolchain.mjs +++ b/scripts/update-typescript-toolchain.mjs @@ -16,9 +16,13 @@ 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); + const repoRoot = path.resolve(scriptDirectory, ".."); + const manifestPath = path.join(repoRoot, "manifests", "baseline-js.json"); + const workRoot = path.join(repoRoot, ".tmp", "toolchain-update"); // TypeScript 7 is the primary toolchain. The lib.*.d.ts files ship in @@ -33,6 +37,7 @@ const workRoot = path.join(repoRoot, ".tmp", "toolchain-update"); // The source integration separately pins the current microsoft/TypeScript main // commit, where the compiler implementation and test harness now live together. const libSourcePlatformPackagePrefix = "@typescript/typescript-"; + const libSourceReferencePlatforms = ["linux-x64", "darwin-arm64", "win32-x64"]; const args = parseArgs(process.argv.slice(2)); @@ -45,6 +50,7 @@ async function main() { `typescript@${baselinePackage.typescriptPeerDependencyRange}`, "supported TypeScript", ); + const stradaVersion = args.stradaVersion ?? resolveLatestStradaVersion(); assertTypeScriptPeerRange(baselinePackage.typescriptPeerDependencyRange, [typescriptVersion]); @@ -109,14 +115,17 @@ function resolveLatestStradaVersion() { 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)) .sort(compareStableSemver); const latest = stableVersions.at(-1); + if (!latest) { throw new Error(`Could not resolve the latest stable ${label} version from the registry`); } + return latest; } @@ -127,12 +136,15 @@ function resolveLatestStableVersion(packageSpecifier, label) { 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; } @@ -189,11 +201,13 @@ function computeCrossPlatformLibSourcePin(typescriptVersion) { } const [first, ...rest] = results; + for (const other of rest) { if (other.hash !== first.hash || other.fileCount !== first.fileCount) { const details = results .map(result => `- ${result.platformKey}: ${result.fileCount} files, ${result.hash}`) .join("\n"); + throw new Error( `Platform lib packages for typescript@${typescriptVersion} are not content-identical:\n${details}`, ); @@ -220,6 +234,7 @@ function parseArgs(argv) { for (let index = 0; index < argv.length; index++) { const current = argv[index]; + switch (current) { case "--typescript-version": args.typescriptVersion = requireArgValue(argv[++index], current); @@ -247,6 +262,7 @@ function requireArgValue(value, flagName) { if (!value) { throw new Error(`Missing value for ${flagName}`); } + return value; } diff --git a/scripts/update-web-features.mjs b/scripts/update-web-features.mjs index dfe3a27..8f290e5 100644 --- a/scripts/update-web-features.mjs +++ b/scripts/update-web-features.mjs @@ -14,18 +14,24 @@ import { } from "../lib/web-features-dataset.mjs"; const scriptPath = fileURLToPath(import.meta.url); + const scriptDirectory = path.dirname(scriptPath); + const repoRoot = path.resolve(scriptDirectory, ".."); const args = parseArgs(process.argv.slice(2)); + const manifestPath = path.resolve(args.manifest ?? path.join(repoRoot, "manifests", "baseline-js.json")); + const snapshotDate = args.snapshotDate ?? new Date().toISOString().slice(0, 10); + const snapshotName = args.snapshotName ?? "baseline-js"; await main(); async function main() { const datasetPath = path.join(repoRoot, "datasets", "web-features-js-compat.json"); + const dataset = await buildWebFeaturesDataset({ repoRoot, snapshotDate, @@ -37,11 +43,13 @@ async function main() { const existingDataset = existsSync(datasetPath) ? JSON.parse(readFileSync(datasetPath, "utf8")) : undefined; + const effectiveDate = resolveSnapshotDate({ existingDataset, newDataset: dataset, candidateDate: snapshotDate, }); + dataset.snapshot.baselineDate = effectiveDate; dataset.snapshot.extractedDate = effectiveDate; @@ -57,6 +65,7 @@ async function main() { baselineDate: effectiveDate, updateOutputPaths: true, }); + await writeManifest({ manifestPath, manifest, @@ -78,6 +87,7 @@ function parseArgs(argv) { for (let index = 0; index < argv.length; index++) { const current = argv[index]; + switch (current) { case "--manifest": args.manifest = requireArgValue(argv[++index], current); @@ -108,6 +118,7 @@ function requireArgValue(value, flagName) { if (!value) { throw new Error(`Missing value for ${flagName}`); } + return value; } diff --git a/scripts/validate-compat-management-registry.mjs b/scripts/validate-compat-management-registry.mjs index 40eb070..7251093 100644 --- a/scripts/validate-compat-management-registry.mjs +++ b/scripts/validate-compat-management-registry.mjs @@ -5,11 +5,15 @@ import { fileURLToPath } from "node:url"; import { loadCompatManagementRegistry } from "../lib/compat-management-registry.mjs"; const scriptPath = fileURLToPath(import.meta.url); + const scriptDirectory = path.dirname(scriptPath); + const repoRoot = path.resolve(scriptDirectory, ".."); + const defaultRegistryPath = path.join(repoRoot, "registry", "compat-management.json"); const args = parseArgs(process.argv.slice(2)); + const registryPath = path.resolve(args.registry ?? defaultRegistryPath); await main(); @@ -32,6 +36,7 @@ function parseArgs(argv) { for (let index = 0; index < argv.length; index++) { const current = argv[index]; + switch (current) { case "--registry": args.registry = requireArgValue(argv[++index], current); @@ -56,6 +61,7 @@ function requireArgValue(value, flagName) { if (!value) { throw new Error(`Missing value for ${flagName}`); } + return value; } diff --git a/test/allowlist.test.mjs b/test/allowlist.test.mjs index 2ac5fa5..e12c1d9 100644 --- a/test/allowlist.test.mjs +++ b/test/allowlist.test.mjs @@ -134,6 +134,7 @@ test("every registered allow entry exposes its complete API surface under TypeSc const registry = await loadAllowlistRegistry(repoAllowlistRegistryPath); const stagedPackageJson = readJsonFile(path.join(stageDirectory, "package.json")); const stagedGeneration = readJsonFile(path.join(stageDirectory, "reports", "generation.json")); + const generationEntryByName = new Map(stagedGeneration.allowEntries.map( /** @param {{ entryName: string; kind: string; }} entry */ entry => [entry.entryName, entry], @@ -166,6 +167,7 @@ test("every registered allow entry exposes its complete API surface under TypeSc writeTextFile(path.join(consumerDirectory, `${baseName}.ts`), ALLOW_ENTRY_PROBES[entry.entryName]); writeConsumerConfig(consumerDirectory, baseName, [baselinePackageName]); const baseConfigPath = path.join(consumerDirectory, `tsconfig.${baseName}.json`); + if (generationEntryByName.get(entry.entryName)?.kind === "active") { assert.equal(runTscExpectFailure(["-p", baseConfigPath], { cwd: consumerDirectory }).ok, false); assert.equal(runTscStradaExpectFailure(["-p", baseConfigPath], { cwd: consumerDirectory }).ok, false); @@ -177,24 +179,49 @@ test("every registered allow entry exposes its complete API surface under TypeSc } }); -test("allow entries preserve isolation under TypeScript 6 and 7", async () => { +test("allow entries preserve isolation under TypeScript 6 and 7", () => { const tempDirectory = createTempDirectory(tempDirectories); const consumerDirectory = path.join(tempDirectory, "consumer"); - const { tarballPath } = await createBaselinePackageTarball({ tempDirectories }); - const promiseEntry = `${baselinePackageName}/allow/promise-withresolvers`; + const datasetPath = path.join(tempDirectory, "dataset.json"); + const dataset = readJsonFile(repoDatasetPath); - writeJsonFile(path.join(consumerDirectory, "package.json"), { - name: "baseline-allow-isolation-fixture", - private: true, - }); - runNpm(["install", "--no-package-lock", "--no-save", tarballPath], { cwd: consumerDirectory }); + for (const compatKey of [ + "javascript.builtins.Promise.withResolvers", + "javascript.builtins.Array.fromAsync", + ]) { + const row = dataset.compatRows.find( + /** @param {{ compatKey: string; }} row */ + row => row.compatKey === compatKey, + ); + + assert.ok(row); + row.baselineStatus = "low"; + delete row.baselineHighDate; + } + + writeJsonFile(datasetPath, dataset); + const fixture = createManifest(tempDirectory, { datasetPath }); + runGenerate(fixture.manifestPath); - writeTextFile(path.join(consumerDirectory, "promise-only-fail.ts"), "Array.fromAsync([1, 2, 3]);\n"); - writeConsumerConfig(consumerDirectory, "promise-only-fail", [baselinePackageName, promiseEntry]); + const declarationFiles = [ + path.join(fixture.outputRoot, "generated", "baseline.d.ts"), + path.join(fixture.outputRoot, "generated", "allow", "promise-withresolvers", "index.d.ts"), + ]; + + writeDirectConsumerConfig({ + directory: consumerDirectory, + name: "promise-only-fail", + source: "Array.fromAsync([1, 2, 3]);\n", + declarationFiles, + }); assertCompilerFailuresContain(consumerDirectory, "promise-only-fail", /fromAsync/); - writeTextFile(path.join(consumerDirectory, "limited-fail.ts"), "\"legacy\".substr(1);\n"); - writeConsumerConfig(consumerDirectory, "limited-fail", [baselinePackageName, promiseEntry]); + writeDirectConsumerConfig({ + directory: consumerDirectory, + name: "limited-fail", + source: "\"legacy\".substr(1);\n", + declarationFiles, + }); assertCompilerFailuresContain(consumerDirectory, "limited-fail", /substr/); }); @@ -228,11 +255,14 @@ test("generation is registry-bound, non-empty, safe, and deterministic", async ( ); const supportDirectory = path.join(fixture.outputRoot, "generated", "allow", "_support"); + const supportFiles = fs.existsSync(supportDirectory) ? fs.readdirSync(supportDirectory).sort() : []; + /** @type {Map>} */ const consumersByUnitId = new Map(); + for (const entry of generation.allowEntries) { for (const unitId of entry.supportUnitIds) { const consumers = consumersByUnitId.get(unitId) ?? new Set(); @@ -240,28 +270,35 @@ test("generation is registry-bound, non-empty, safe, and deterministic", async ( consumersByUnitId.set(unitId, consumers); } } + const consumerGroups = new Set( [...consumersByUnitId.values()].map(consumers => [...consumers].sort().join("\0")), ); + assert.equal(supportFiles.length, consumerGroups.size); + for (const group of consumerGroups) { const entryNames = group.split("\0"); + if (entryNames.length === 1) { assert.ok(supportFiles.includes(`${entryNames[0]}.d.ts`)); } } const referencedSupportFiles = new Set(); + for (const entry of generation.allowEntries) { const source = fs.readFileSync( path.join(fixture.outputRoot, "generated", "allow", entry.entryName, "index.d.ts"), "utf8", ); + for (const match of source.matchAll(/^\/\/\/ $/gm)) { assert.ok(supportFiles.includes(match[1])); referencedSupportFiles.add(match[1]); } } + assert.deepEqual([...referencedSupportFiles].sort(), supportFiles); runGenerate(fixture.manifestPath); @@ -335,6 +372,7 @@ test("shared support bundles preserve entry isolation and composition under Type ].join("\n"), }, ]; + for (const compilerCase of cases) { const configPath = writeDirectConsumerConfig({ directory: tempDirectory, @@ -342,6 +380,7 @@ test("shared support bundles preserve entry isolation and composition under Type source: compilerCase.source, declarationFiles: [fixture.topLevelOutputPath, ...compilerCase.entries], }); + runTsc(["-p", configPath], { cwd: tempDirectory }); runTscStrada(["-p", configPath], { cwd: tempDirectory }); } @@ -356,6 +395,7 @@ test("shared support bundles preserve entry isolation and composition under Type ].join("\n"), declarationFiles: [fixture.topLevelOutputPath, coreEntryPath], }); + assert.match(runTscExpectFailure(["-p", coreIsolationConfig], { cwd: tempDirectory }).output, /formatToParts/); assert.match(runTscStradaExpectFailure(["-p", coreIsolationConfig], { cwd: tempDirectory }).output, /formatToParts/); @@ -365,6 +405,7 @@ test("shared support bundles preserve entry isolation and composition under Type source: "new Intl.DurationFormat(\"en\");\n", declarationFiles: [fixture.topLevelOutputPath, detailsEntryPath], }); + assert.match(runTscExpectFailure(["-p", detailsIsolationConfig], { cwd: tempDirectory }).output, /DurationFormat/); assert.match(runTscStradaExpectFailure(["-p", detailsIsolationConfig], { cwd: tempDirectory }).output, /DurationFormat/); }); @@ -407,17 +448,21 @@ test("partial Baseline promotion cannot split a non-mergeable container from its const datasetPath = path.join(tempDirectory, "dataset.json"); const allowlistRegistryPath = path.join(tempDirectory, "allowlist.json"); const dataset = readJsonFile(repoDatasetPath); + const promotedCompatKeys = new Set([ "javascript.builtins.Intl.DurationFormat", "javascript.builtins.Intl.DurationFormat.DurationFormat", "javascript.builtins.Intl.DurationFormat.format", ]); + for (const row of dataset.compatRows) { if (promotedCompatKeys.has(row.compatKey)) { row.baselineStatus = "high"; } } + writeJsonFile(datasetPath, dataset); + const durationFormatCompatKeys = [ "javascript.builtins.Intl.DurationFormat", "javascript.builtins.Intl.DurationFormat.DurationFormat", @@ -434,9 +479,11 @@ test("partial Baseline promotion cannot split a non-mergeable container from its }); const fixture = createManifest(tempDirectory, { datasetPath, allowlistRegistryPath }); const failure = runGenerateExpectFailure(fixture.manifestPath); + const expectedSurfaces = entryName === "baseline" ? "baseline, baseline" : "baseline, intl-duration-format"; + assert.match( failure, new RegExp( @@ -453,6 +500,15 @@ test("a registered path becomes a permanent baseline alias after promotion", () const allowlistRegistryPath = path.join(tempDirectory, "allowlist.json"); const dataset = readJsonFile(repoDatasetPath); const compatKey = "javascript.builtins.Promise.withResolvers"; + + const compatRow = dataset.compatRows.find( + /** @param {{ compatKey: string; }} row */ + row => row.compatKey === compatKey, + ); + + assert.ok(compatRow); + compatRow.baselineStatus = "low"; + delete compatRow.baselineHighDate; writeJsonFile(datasetPath, dataset); writeJsonFile(allowlistRegistryPath, { schemaVersion: 1, @@ -465,11 +521,6 @@ test("a registered path becomes a permanent baseline alias after promotion", () assert.equal(activeGeneration.allowEntries[0].kind, "active"); assert.ok(activeGeneration.allowEntries[0].unitIds.length); - const compatRow = dataset.compatRows.find( - /** @param {{ compatKey: string; }} row */ - row => row.compatKey === compatKey, - ); - assert.ok(compatRow); compatRow.baselineStatus = "high"; writeJsonFile(datasetPath, dataset); runGenerate(fixture.manifestPath); @@ -501,12 +552,12 @@ test("a registered path becomes a permanent baseline alias after promotion", () runTsc(["-p", path.join(consumerDirectory, "tsconfig.pass.json")], { cwd: consumerDirectory }); runTscStrada(["-p", path.join(consumerDirectory, "tsconfig.pass.json")], { cwd: consumerDirectory }); - writeTextFile(path.join(consumerDirectory, "fail.ts"), "Array.fromAsync([1, 2, 3]);\n"); + writeTextFile(path.join(consumerDirectory, "fail.ts"), "\"legacy\".substr(1);\n"); writeJsonFile(path.join(consumerDirectory, "tsconfig.fail.json"), { compilerOptions: { noLib: true, strict: true }, files: ["fail.ts", aliasPath], }); - assertCompilerFailuresContain(consumerDirectory, "fail", /fromAsync/); + assertCompilerFailuresContain(consumerDirectory, "fail", /substr/); }); test("shared declaration units cannot unlock unregistered or Limited availability behavior", () => { @@ -531,10 +582,12 @@ test("every compat key in an active entry must emit declaration surface", () => const allowlistRegistryPath = path.join(tempDirectory, "allowlist.json"); const dataset = readJsonFile(repoDatasetPath); const behavioralCompatKey = "javascript.builtins.AggregateError.serializable_object"; + const behavioralRow = dataset.compatRows.find( /** @param {{ compatKey: string; }} row */ row => row.compatKey === behavioralCompatKey, ); + assert.ok(behavioralRow); behavioralRow.baselineStatus = "low"; delete behavioralRow.baselineHighDate; @@ -604,6 +657,7 @@ test("release planning preserves published paths and exact compat contracts", () compatKeys: ["javascript.builtins.Promise.withResolvers"], }], }); + const unchanged = JSON.stringify({ allowEntries: [{ kind: "alias", @@ -611,6 +665,7 @@ test("release planning preserves published paths and exact compat contracts", () compatKeys: ["javascript.builtins.Promise.withResolvers"], }], }); + assert.doesNotThrow(() => assertAllowEntryContractsPreserved(published, unchanged)); assert.throws( () => assertAllowEntryContractsPreserved(published, JSON.stringify({ @@ -653,6 +708,7 @@ function writeConsumerConfig(consumerDirectory, name, types) { */ function assertCompilerFailuresContain(consumerDirectory, name, expectedError) { const configPath = path.join(consumerDirectory, `tsconfig.${name}.json`); + for (const failure of [ runTscExpectFailure(["-p", configPath], { cwd: consumerDirectory }), runTscStradaExpectFailure(["-p", configPath], { cwd: consumerDirectory }), @@ -681,6 +737,7 @@ function writeDirectConsumerConfig(options) { compilerOptions: { noLib: true, strict: true }, files: [sourcePath, ...options.declarationFiles], }); + return configPath; } diff --git a/test/classifier.test.mjs b/test/classifier.test.mjs index d66c00d..f8f9f68 100644 --- a/test/classifier.test.mjs +++ b/test/classifier.test.mjs @@ -10,6 +10,7 @@ import { writeJsonFile, } from "./helpers.mjs"; import { classifyManifest } from "../lib/classifier.mjs"; +import { resolveExcludedUnits } from "../lib/generator.mjs"; import { createSurfaceInventory, discoverBuiltinSourceLibEntries, @@ -268,6 +269,30 @@ function findRow(classification, compatKey) { return found; } +test("SharedArrayBuffer selects its ArrayBufferLike augmentation only when the root is included", async () => { + const key = "javascript.builtins.SharedArrayBuffer"; + const libSource = [ + "interface ArrayBuffer { byteLength: number; }", + "interface ArrayBufferTypes { ArrayBuffer: ArrayBuffer; }", + "type ArrayBufferLike = ArrayBufferTypes[keyof ArrayBufferTypes];", + "interface SharedArrayBuffer { byteLength: number; }", + "interface SharedArrayBufferConstructor { new(length: number): SharedArrayBuffer; }", + "declare var SharedArrayBuffer: SharedArrayBufferConstructor;", + "interface ArrayBufferTypes { SharedArrayBuffer: SharedArrayBuffer; }", + ].join("\n"); + const rows = [row("javascript.builtins.ArrayBuffer", "high"), { ...row(key, "high"), baselineLowDate: "2021-12-01" }]; + const current = await classifyFixture({ rows, libSource }); + const currentRow = findRow(current, key); + const helperId = currentRow.resolvedUnitIds.find(id => id.includes("ArrayBufferTypes.SharedArrayBuffer")); + assert.ok(helperId); + assert.ok(findRow(current, "javascript.builtins.ArrayBuffer").resolvedUnitIds.some(id => id.includes("ArrayBufferTypes.ArrayBuffer"))); + assert.equal(resolveExcludedUnits(current.classifiedCompatRows).excludedUnitIds.has(helperId), false); + + const earlier = await classifyFixture({ rows, libSource, baselineTarget: "year:2020" }); + assert.equal(findRow(earlier, key).includeInTarget, false); + assert.equal(resolveExcludedUnits(earlier.classifiedCompatRows).excludedUnitIds.has(helperId), true); +}); + test("classifier routes synthetic compat rows to the expected resolution kinds", async () => { const classification = await classifyFixture({ rows: [ diff --git a/test/compat-management-schema.test.mjs b/test/compat-management-schema.test.mjs index 31d8643..3752e0a 100644 --- a/test/compat-management-schema.test.mjs +++ b/test/compat-management-schema.test.mjs @@ -95,6 +95,7 @@ test("compat-management registry fails schema validation on unexpected propertie if (index !== 0) { return group; } + return { ...group, category: "invalid-category", @@ -108,6 +109,7 @@ test("compat-management registry fails schema validation on unexpected propertie error => { assert.match(String(error), /failed JSON schema validation/); assert.match(String(error), /invalid-category|unexpected property stray/); + return true; }, ); diff --git a/test/consumer-smoke.test.mjs b/test/consumer-smoke.test.mjs index 3150741..0de6e7b 100644 --- a/test/consumer-smoke.test.mjs +++ b/test/consumer-smoke.test.mjs @@ -63,14 +63,17 @@ test("staged consumer smoke: stock tsc accepts supported baseline APIs and rejec "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); @@ -79,6 +82,11 @@ test("staged consumer smoke: stock tsc accepts supported baseline APIs and rejec "const reversed = [1, 2, 3].toReversed();", "Intl.supportedValuesOf(\"currency\");", "reversed.length;", + "Promise.withResolvers();", + "Array.fromAsync([1, 2, 3]);", + "Object.groupBy([1, 2, 3], value => String(value));", + "Map.groupBy([1, 2, 3], value => value % 2);", + "new ArrayBuffer(8).transfer().transferToFixedLength().detached;", REGEXP_LEGACY_STATIC_ABSENCE_ASSERTION, "", ].join("\n")); @@ -106,6 +114,7 @@ test("staged consumer smoke: stock tsc accepts supported baseline APIs and rejec const failure = runTscExpectFailure(["-p", path.join(consumerDirectory, "tsconfig.fail.json")], { cwd: consumerDirectory }); assert.equal(failure.ok, false); + for (const probe of negativeProbes) { assert.match(failure.output, probe.errorPattern, `expected excluded probe ${probe.compatKey} to fail compilation`); } @@ -114,6 +123,7 @@ test("staged consumer smoke: stock tsc accepts supported baseline APIs and rejec // caught by either checker). const stradaFailure = runTscStradaExpectFailure(["-p", path.join(consumerDirectory, "tsconfig.fail.json")], { cwd: consumerDirectory }); assert.equal(stradaFailure.ok, false); + for (const probe of negativeProbes) { assert.match(stradaFailure.output, probe.errorPattern, `expected excluded probe ${probe.compatKey} to fail compilation under strada`); } diff --git a/test/excluded-units.test.mjs b/test/excluded-units.test.mjs index fd7d410..aa2dd1a 100644 --- a/test/excluded-units.test.mjs +++ b/test/excluded-units.test.mjs @@ -41,6 +41,7 @@ async function createFixtureInventory(tempDirectory, libFiles) { version: "0.0.0-test", }, undefined, 4)}\n`, ); + for (const [fileName, sourceText] of Object.entries(libFiles)) { fs.writeFileSync(path.join(libDirectory, fileName), sourceText); } @@ -49,6 +50,7 @@ async function createFixtureInventory(tempDirectory, libFiles) { libDirectory, reportPathPrefix: "typescript/lib", }); + return createSurfaceInventory({ snapshotName: "excluded-units-test", repoRoot: tempDirectory, @@ -64,11 +66,13 @@ async function createFixtureInventory(tempDirectory, libFiles) { function requireMemberUnit(inventory, memberKey) { const units = inventory.memberUnitsByOwnerAndName.get(memberKey) ?? []; assert.equal(units.length, 1, `expected exactly one unit for ${memberKey}`); + return units[0]; } test("complete-container emission never includes excluded member units", async () => { const tempDirectory = createTempDirectory(tempDirectories); + const inventory = await createFixtureInventory(tempDirectory, { "lib.es5.d.ts": [ "interface Widget {", @@ -85,6 +89,7 @@ test("complete-container emission never includes excluded member units", async ( const widgetInterface = (inventory.declarationUnitsBySymbol.get("Widget") ?? []) .find(unit => unit.declarationKind === "interface"); + assert.ok(widgetInterface, "expected Widget interface declaration unit"); const goodMember = requireMemberUnit(inventory, "Widget::good"); const badMember = requireMemberUnit(inventory, "Widget::bad"); @@ -95,6 +100,7 @@ test("complete-container emission never includes excluded member units", async ( selectedUnitIds: [widgetInterface.id], completeContainerUnitIds: new Set([widgetInterface.id]), }); + assert.match(unrestricted, /good\(\): void;/); assert.match(unrestricted, /bad\(\): void;/); @@ -105,12 +111,14 @@ test("complete-container emission never includes excluded member units", async ( completeContainerUnitIds: new Set([widgetInterface.id]), excludedUnitIds: new Set([badMember.id]), }); + assert.match(restricted, /good\(\): void;/); assert.doesNotMatch(restricted, /bad\(\): void;/); }); test("whole-file preserved libs fail closed when they contain excluded units", async () => { const tempDirectory = createTempDirectory(tempDirectories); + const inventory = await createFixtureInventory(tempDirectory, { "lib.esnext.widget.d.ts": [ "export {};", @@ -180,6 +188,7 @@ test("resolveExcludedUnits bans surface-defining exclusions but keeps shared and test("resolveUnclaimedTypeOnlyUnitIds preserves global aliases without selecting interface surface", async () => { const tempDirectory = createTempDirectory(tempDirectories); + const inventory = await createFixtureInventory(tempDirectory, { "lib.es5.d.ts": [ "type Utility = { value: T };", @@ -199,6 +208,7 @@ test("resolveUnclaimedTypeOnlyUnitIds preserves global aliases without selecting "", ].join("\n"), }); + const claimedAlias = (inventory.declarationUnitsBySymbol.get("ClaimedAlias") ?? [])[0]; const excludedMember = requireMemberUnit(inventory, "TypeOnly::excluded"); assert.ok(claimedAlias); @@ -211,6 +221,7 @@ test("resolveUnclaimedTypeOnlyUnitIds preserves global aliases without selecting ], excludedUnitIds: new Set([excludedMember.id]), }); + const selectedUnits = selected.map(unitId => inventory.unitById.get(unitId)); assert.ok(selectedUnits.some(unit => unit?.symbolName === "Utility")); @@ -226,6 +237,7 @@ test("resolveUnclaimedTypeOnlyUnitIds preserves global aliases without selecting test("assertExclusionInvariants rejects selections that intersect excluded units", async () => { const tempDirectory = createTempDirectory(tempDirectories); + const inventory = await createFixtureInventory(tempDirectory, { "lib.es5.d.ts": [ "interface Widget {", @@ -234,6 +246,7 @@ test("assertExclusionInvariants rejects selections that intersect excluded units "", ].join("\n"), }); + const badMember = requireMemberUnit(inventory, "Widget::bad"); assert.throws( @@ -258,6 +271,7 @@ test("assertExclusionInvariants rejects selections that intersect excluded units test("runtime provenance rejects unclaimed members and accepts explicit compiler support", async () => { const tempDirectory = createTempDirectory(tempDirectories); + const inventory = await createFixtureInventory(tempDirectory, { "lib.es5.d.ts": [ "interface Widget {", @@ -271,14 +285,18 @@ test("runtime provenance rejects unclaimed members and accepts explicit compiler "", ].join("\n"), }); + const widgetDeclaration = (inventory.declarationUnitsBySymbol.get("Widget") ?? []) .find(unit => unit.declarationKind === "interface"); + const widgetValue = (inventory.declarationUnitsBySymbol.get("Widget") ?? []) .find(unit => unit.declarationKind === "var"); + const supportedMember = requireMemberUnit(inventory, "Widget::supported"); const unknownMember = requireMemberUnit(inventory, "Widget::unknown"); assert.ok(widgetDeclaration && widgetValue); const selectedUnitIds = [widgetDeclaration.id, widgetValue.id, supportedMember.id, unknownMember.id]; + const classifiedCompatRows = [ { compatKey: "javascript.builtins.Widget", @@ -321,6 +339,7 @@ test("runtime provenance rejects unclaimed members and accepts explicit compiler test("runtime provenance audits every unit emitted from a whole-file lib", async () => { const tempDirectory = createTempDirectory(tempDirectories); + const inventory = await createFixtureInventory(tempDirectory, { "lib.esnext.widget.d.ts": [ "export {};", @@ -337,10 +356,13 @@ test("runtime provenance audits every unit emitted from a whole-file lib", async "", ].join("\n"), }); + const widgetDeclaration = (inventory.declarationUnitsBySymbol.get("Widget") ?? []) .find(unit => unit.declarationKind === "interface"); + const widgetValue = (inventory.declarationUnitsBySymbol.get("Widget") ?? []) .find(unit => unit.declarationKind === "var"); + const supportedMember = requireMemberUnit(inventory, "Widget::supported"); assert.ok(widgetDeclaration && widgetValue); @@ -399,6 +421,7 @@ test("surface inventory rejects module boundaries in built-in lib inputs", async test("surface inventory parses and strips TypeScript-valid lib reference syntax", async () => { const tempDirectory = createTempDirectory(tempDirectories); + const inventory = await createFixtureInventory(tempDirectory, { "lib.esnext.widget.d.ts": [ "/// ", @@ -406,6 +429,7 @@ test("surface inventory parses and strips TypeScript-valid lib reference syntax" "", ].join("\n"), }); + const fileRecord = inventory.fileByLibFileName.get("lib.esnext.widget.d.ts"); const widget = (inventory.declarationUnitsBySymbol.get("Widget") ?? [])[0]; assert.deepEqual(fileRecord?.referenceLibs, ["es2015.iterable"]); @@ -415,6 +439,7 @@ test("surface inventory parses and strips TypeScript-valid lib reference syntax" inventory, selectedUnitIds: [widget.id], }); + assert.match(output, /interface Widget/u); assert.doesNotMatch(output, /reference\s+LIB/iu); }); diff --git a/test/generate.test.mjs b/test/generate.test.mjs index ecead61..be70f80 100644 --- a/test/generate.test.mjs +++ b/test/generate.test.mjs @@ -100,16 +100,7 @@ test("generate emits the current TypeScript-declarable Baseline JavaScript surfa ); } } - for (const probe of [ - { name: "Float16Array", pattern: /\b(?:interface|declare var) Float16Array\b/ }, - { name: "Promise.withResolvers", pattern: /\bwithResolvers\s*\(/ }, - { name: "Array.fromAsync", pattern: /\bfromAsync\s* 0); diff --git a/test/helpers.mjs b/test/helpers.mjs index 64d5258..40fa938 100644 --- a/test/helpers.mjs +++ b/test/helpers.mjs @@ -14,13 +14,21 @@ import { resolveInstalledPackageRoot } from "../lib/installed-package.mjs"; import { selectActiveNegativeProbes } from "../lib/negative-probes.mjs"; const testDirectory = path.dirname(fileURLToPath(import.meta.url)); + export const repoRoot = path.resolve(testDirectory, ".."); + export const repoManifestPath = path.join(repoRoot, "manifests", "baseline-js.json"); + export const repoManifest = JSON.parse(fs.readFileSync(repoManifestPath, "utf8")); + export const repoDatasetPath = path.join(repoRoot, "datasets", "web-features-js-compat.json"); + export const repoRegistryPath = path.join(repoRoot, "registry", "compat-management.json"); + export const repoAllowlistRegistryPath = path.join(repoRoot, "registry", "allowlist.json"); + export const repoGeneratedLibPath = path.join(repoRoot, "generated", "current", "baseline.d.ts"); + export const repoClassificationPath = path.join(repoRoot, "derived", "current", "classification.json"); /** @@ -30,6 +38,7 @@ export const repoClassificationPath = path.join(repoRoot, "derived", "current", export function loadActiveNegativeProbesFromRepo() { /** @type {{ classifiedCompatRows: Array<{ compatKey: string; includeInTarget: boolean; }>; }} */ const classification = readJsonFile(repoClassificationPath); + return selectActiveNegativeProbes(classification.classifiedCompatRows); } @@ -41,6 +50,7 @@ export function createTempDirectory(tempDirectories) { fs.mkdirSync(tempRoot, { recursive: true }); const tempDirectory = fs.mkdtempSync(path.join(tempRoot, "run-")); tempDirectories.push(tempDirectory); + return tempDirectory; } @@ -51,6 +61,7 @@ export function cleanupTempDirectories(tempDirectories) { for (const tempDirectory of tempDirectories) { fs.rmSync(tempDirectory, { recursive: true, force: true }); } + tempDirectories.length = 0; } @@ -108,6 +119,7 @@ export function createManifest(tempDirectory, options = {}) { yearDirectory: toPosixRelativePath(repoRoot, path.join(outputRoot, "generated", "year")), firstYear: repoManifest.firstClassLib.firstYear, }; + if (options.generatedOutputPath) { manifest.firstClassLib.outputFile = toPosixRelativePath(repoRoot, options.generatedOutputPath); } @@ -187,9 +199,11 @@ export function runTscExpectFailure(args, options = {}) { ...options, allowFailure: true, }); + if (typeof result === "string") { assert.fail(`Expected tsc to fail for ${args.join(" ")}`); } + return result; } @@ -202,9 +216,11 @@ export function runTscStradaExpectFailure(args, options = {}) { ...options, allowFailure: true, }); + if (typeof result === "string") { assert.fail(`Expected strada tsc to fail for ${args.join(" ")}`); } + return result; } @@ -215,6 +231,7 @@ export function runTscStradaExpectFailure(args, options = {}) { export function runNpm(args, options = {}) { const cwd = options.cwd ?? repoRoot; const npm = resolveReleaseExecutable(repoRoot, "RELEASE_NPM_EXECUTABLE", "npm"); + return execFileSync(npm.executable, args, { cwd, encoding: "utf8", @@ -231,11 +248,13 @@ export function runNpm(args, options = {}) { */ export async function stageBaselinePackage(options = {}) { const stageDirectoryRoot = createTempDirectory(options.tempDirectories ?? []); + const [summary] = await createPackageStages({ packageId: "baseline", versionOverride: options.versionOverride ?? "0.0.0-test", stageDirectoryRoot, }); + assert.ok(summary, "Expected baseline package staging summary"); return { @@ -251,6 +270,7 @@ export async function createBaselinePackageTarball(options = {}) { const summary = await stageBaselinePackage(options); const tarballPath = await createPackageTarball(summary.stageDirectory); options.tempDirectories?.push(path.dirname(tarballPath)); + return { ...summary, tarballPath, @@ -267,9 +287,11 @@ export async function createBaselinePackageTarball(options = {}) { function runPackageBinary(packageName, binName, args, options = {}) { const packageJsonPath = path.join(resolveInstalledPackageRoot(repoRoot, packageName), "package.json"); const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")); + const binRelativePath = typeof packageJson.bin === "string" ? packageJson.bin : packageJson.bin?.[binName]; + assert.ok(binRelativePath, `Package ${packageName} does not expose a ${binName} binary`); const packageRoot = path.dirname(packageJsonPath); @@ -288,11 +310,13 @@ function runPackageBinary(packageName, binName, args, options = {}) { catch (error) { if (options.allowFailure) { const failedError = /** @type {{ stdout?: string; stderr?: string; }} */ (error); + return { ok: false, output: `${failedError.stdout ?? ""}${failedError.stderr ?? ""}`, }; } + throw error; } } diff --git a/test/managed-output-path.test.mjs b/test/managed-output-path.test.mjs index 6d990cb..0a93ee8 100644 --- a/test/managed-output-path.test.mjs +++ b/test/managed-output-path.test.mjs @@ -15,8 +15,11 @@ import { } from "./helpers.mjs"; const repoRoot = path.resolve("/repo"); + const manifestPath = path.join(repoRoot, "manifests", "baseline.json"); + const generatedRoot = path.join(repoRoot, "generated", "current"); + /** @type {string[]} */ const tempDirectories = []; diff --git a/test/negative-probes.test.mjs b/test/negative-probes.test.mjs index 89d8b27..f5ec408 100644 --- a/test/negative-probes.test.mjs +++ b/test/negative-probes.test.mjs @@ -28,6 +28,7 @@ const compilerFixturePath = path.join( "compiler", "libBaseline.ts", ); + const positiveSmokeFixturePath = path.join( repoRoot, "fixtures", @@ -49,6 +50,7 @@ test("checked-in classification still excludes every stable probe and at least o `stable probe ${stableProbe.compatKey} must stay active`, ); } + assert.ok( activeProbes.length > STABLE_NEGATIVE_PROBES.length, "expected at least one active Baseline-low probe", @@ -60,18 +62,21 @@ test("TypeScript fixtures stay in sync with stable exclusion checks", () => { // uses only permanently stable probes. This catches any drift between the // fixture and the probe definitions. const fixtureSource = fs.readFileSync(compilerFixturePath, "utf8"); + for (const probe of STABLE_NEGATIVE_PROBES) { assert.ok( fixtureSource.includes(probe.sourceText), `compiler fixture must contain stable probe source: ${probe.sourceText}`, ); } + for (const candidate of LOW_NEGATIVE_PROBE_CANDIDATES) { assert.ok( !fixtureSource.includes(candidate.sourceText), `compiler fixture must not hard-code dated low probe: ${candidate.compatKey}`, ); } + assert.ok( fixtureSource.includes(REGEXP_LEGACY_STATIC_ABSENCE_ASSERTION), "compiler fixture must contain the RegExp legacy static absence assertion", @@ -82,15 +87,19 @@ test("TypeScript fixtures stay in sync with stable exclusion checks", () => { ); const registry = readJsonFile(repoRegistryPath); + const legacyGroup = registry.groups.find( /** @param {{ id: string; }} group */ group => group.id === "regexp-legacy-statics-excluded", ); + assert.ok(legacyGroup); + const mappedMemberNames = legacyGroup.compatKeys.flatMap( /** @param {string} compatKey */ compatKey => registry.declarationMappings[compatKey].memberNames, ); + assert.deepEqual( [...new Set(mappedMemberNames)].sort(), [...REGEXP_LEGACY_STATIC_MEMBER_NAMES].sort(), @@ -106,6 +115,7 @@ test("selectActiveNegativeProbes fails with actionable messages", () => { function classificationRow(compatKey, includeInTarget) { return { compatKey, includeInTarget }; } + const allExcluded = [ ...STABLE_NEGATIVE_PROBES.map(probe => classificationRow(probe.compatKey, false)), ...LOW_NEGATIVE_PROBE_CANDIDATES.map(probe => classificationRow(probe.compatKey, false)), @@ -139,15 +149,18 @@ test("selectActiveNegativeProbes fails with actionable messages", () => { // A promoted low probe silently drops out of the candidates. const [firstLow, ...remainingLow] = LOW_NEGATIVE_PROBE_CANDIDATES; + const partiallyPromoted = selectActiveNegativeProbes( allExcluded.map(row => row.compatKey === firstLow.compatKey ? classificationRow(row.compatKey, true) : row ), ); + assert.ok(!partiallyPromoted.some(probe => probe.compatKey === firstLow.compatKey)); assert.ok(remainingLow.every(candidate => partiallyPromoted.some(probe => probe.compatKey === candidate.compatKey))); const rendered = renderNegativeProbeSource(partiallyPromoted); + for (const probe of partiallyPromoted) { assert.ok(rendered.includes(probe.sourceText)); assert.ok(rendered.includes(probe.compatKey)); diff --git a/test/net-retry.test.mjs b/test/net-retry.test.mjs index 696e7d9..e409662 100644 --- a/test/net-retry.test.mjs +++ b/test/net-retry.test.mjs @@ -10,23 +10,30 @@ import { retryAsync, retrySync } from "../lib/net-retry.mjs"; test("retrySync returns immediately on first success without retrying", () => { let calls = 0; + const result = retrySync("op", () => { calls += 1; + return "ok"; }, { baseDelayMs: 0 }); + assert.equal(result, "ok"); assert.equal(calls, 1); }); test("retrySync recovers after transient failures", () => { let calls = 0; + const result = retrySync("op", () => { calls += 1; + if (calls < 3) { throw new Error("transient"); } + return "recovered"; }, { attempts: 3, baseDelayMs: 0 }); + assert.equal(result, "recovered"); assert.equal(calls, 3); }); @@ -45,13 +52,17 @@ test("retrySync gives up after exhausting attempts and surfaces the last error", test("retryAsync recovers after transient rejection", async () => { let calls = 0; + const result = await retryAsync("op", async () => { calls += 1; + if (calls < 2) { throw new Error("transient"); } + return "recovered"; }, { attempts: 3, baseDelayMs: 0 }); + assert.equal(result, "recovered"); assert.equal(calls, 2); }); diff --git a/test/package-metadata.test.mjs b/test/package-metadata.test.mjs index 4fd79ca..d993d83 100644 --- a/test/package-metadata.test.mjs +++ b/test/package-metadata.test.mjs @@ -46,6 +46,7 @@ test("package metadata stages deterministically with the TypeScript peer contrac const classification = readJsonFile(repoClassificationPath); const includedCompatCount = countIncludedCompatRows(classification.classifiedCompatRows); assert.ok(includedCompatCount < classification.summary.includedCompatCount); + const expectedSnapshot = { schemaVersion: 1, baselineDate: repoManifest.snapshot.baselineDate, @@ -55,6 +56,7 @@ test("package metadata stages deterministically with the TypeScript peer contrac includedCompatCount, generatorVersion: repoManifest.snapshot.generatorVersion, }; + assert.deepEqual( readJsonFile(path.join(firstStage.stageDirectory, "snapshot.json")), expectedSnapshot, @@ -108,12 +110,14 @@ test("TypeScript peer range contains every pinned compiler line", () => { () => 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/, @@ -158,9 +162,11 @@ function readPackageContents(packageDirectory) { 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); } @@ -171,5 +177,6 @@ function readPackageContents(packageDirectory) { } visit(packageDirectory, ""); + return contents; } diff --git a/test/packed-consumer-smoke.test.mjs b/test/packed-consumer-smoke.test.mjs index f56a113..a7e36dc 100644 --- a/test/packed-consumer-smoke.test.mjs +++ b/test/packed-consumer-smoke.test.mjs @@ -34,6 +34,7 @@ test.afterEach(() => { test("packed consumer smoke: npm-packed baseline package typechecks through compilerOptions.types", async () => { const tempDirectory = createTempDirectory(tempDirectories); const consumerDirectory = path.join(tempDirectory, "consumer"); + const tarballPath = process.env.BASELINE_PACKAGE_TARBALL ? path.resolve(process.env.BASELINE_PACKAGE_TARBALL) : (await createBaselinePackageTarball({ tempDirectories })).tarballPath; @@ -116,8 +117,10 @@ test("packed consumer smoke: npm-packed baseline package typechecks through comp runTscExpectFailure(["-p", path.join(consumerDirectory, "tsconfig.fail.json")], { cwd: consumerDirectory }), runTscStradaExpectFailure(["-p", path.join(consumerDirectory, "tsconfig.fail.json")], { cwd: consumerDirectory }), ]; + for (const failure of failures) { assert.equal(failure.ok, false); + for (const probe of negativeProbes) { assert.match(failure.output, probe.errorPattern, `expected excluded probe ${probe.compatKey} to fail compilation`); } diff --git a/test/typescript-source.test.mjs b/test/typescript-source.test.mjs index ca03937..eb68ff5 100644 --- a/test/typescript-source.test.mjs +++ b/test/typescript-source.test.mjs @@ -10,6 +10,7 @@ import { checkoutTypeScriptSource, readTypeScriptSourcePin, } from "../lib/typescript-source.mjs"; +import { readLibSourceConfig } from "../lib/toolchain-libs.mjs"; import { cleanupTempDirectories, createTempDirectory, @@ -41,10 +42,12 @@ test("source pinning and checkout remain exact after the tracked branch advances commitAll(sourceDirectory, "first"); const manifest = {}; + const pin = applyTypeScriptSourcePin({ manifest, repository: sourceDirectory, }); + assert.equal(pin.ref, "main"); fs.writeFileSync(path.join(sourceDirectory, "value.txt"), "second\n"); @@ -54,6 +57,7 @@ test("source pinning and checkout remain exact after the tracked branch advances manifest, outDirectory: checkoutDirectory, }); + assert.equal(result.commit, pin.commit); assert.equal(execFileSync("git", ["rev-parse", "HEAD"], { cwd: checkoutDirectory, encoding: "utf8" }).trim(), pin.commit); assert.equal(fs.readFileSync(path.join(checkoutDirectory, "value.txt"), "utf8"), "first\n"); @@ -69,6 +73,7 @@ test("source pinning and checkout remain exact after the tracked branch advances outDirectory: checkoutDirectory, force: true, }); + assert.equal(refreshed.reusedExistingCheckout, false); assert.ok(!fs.existsSync(path.join(checkoutDirectory, "local.txt"))); assert.equal(fs.readFileSync(path.join(checkoutDirectory, "value.txt"), "utf8"), "first\n"); @@ -84,6 +89,27 @@ test("repo manifest pins a cross-platform verified lib source", () => { assert.ok(Number.isInteger(libSource.libFileCount) && libSource.libFileCount > 0); }); +test("lib source pins reject coerced values and duplicate reference platforms", () => { + assert.deepEqual(readLibSourceConfig(repoManifest), repoManifest.libSource); + + for (const referencePlatforms of [ + ["linux-x64", "linux-x64"], + ["linux-x64", 42], + ["linux-x64", null], + ["linux-x64", "../other"], + ]) { + assert.throws( + () => readLibSourceConfig({ libSource: { ...repoManifest.libSource, referencePlatforms } }), + /referencePlatforms/, + ); + } + + assert.throws( + () => readLibSourceConfig({ libSource: { ...repoManifest.libSource, libContentHash: [repoManifest.libSource.libContentHash] } }), + /libContentHash/, + ); +}); + /** * @param {string} directory */ diff --git a/test/year-entrypoints.test.mjs b/test/year-entrypoints.test.mjs index 8b90694..726c3c0 100644 --- a/test/year-entrypoints.test.mjs +++ b/test/year-entrypoints.test.mjs @@ -42,6 +42,7 @@ test.afterEach(() => { test("year entrypoints are cumulative, date-bounded, and deterministic", () => { const tempDirectory = createTempDirectory(tempDirectories); const fixture = createManifest(tempDirectory); + const expectedYears = resolveBaselineYears( repoManifest.snapshot.baselineDate, repoManifest.firstClassLib.firstYear, @@ -51,6 +52,7 @@ test("year entrypoints are cumulative, date-bounded, and deterministic", () => { const firstSnapshot = readDirectorySnapshot(path.join(fixture.outputRoot, "generated", "year")); const report = readJsonFile(fixture.generationOutputPath); const classification = readJsonFile(fixture.classificationOutputPath); + const classifiedRowByKey = new Map(classification.classifiedCompatRows.map( /** @param {{ compatKey: string; }} row */ row => [row.compatKey, row], @@ -60,6 +62,7 @@ test("year entrypoints are cumulative, date-bounded, and deterministic", () => { /** @param {{ year: number; }} entry */ entry => entry.year, ), expectedYears); + for (let index = 1; index < report.yearEntries.length; index++) { assert.ok( report.yearEntries[index - 1].selectedUnitCount <= report.yearEntries[index].selectedUnitCount, @@ -73,14 +76,18 @@ test("year entrypoints are cumulative, date-bounded, and deterministic", () => { `Baseline ${report.yearEntries[index].year} must include the prior year's declared compat keys`, ); } + for (const entry of report.yearEntries) { assert.match(entry.contentHash, /^sha256-[0-9a-f]{64}$/); + const contents = fs.readFileSync( path.join(fixture.outputRoot, "generated", "year", String(entry.year), "index.d.ts"), ); + assert.equal(entry.contentHash, `sha256-${createHash("sha256").update(contents).digest("hex")}`); assert.deepEqual(entry.includedCompatKeys, [...entry.includedCompatKeys].sort()); assert.deepEqual(entry.notModeledUpstreamCompatKeys, [...entry.notModeledUpstreamCompatKeys].sort()); + for (const compatKey of entry.notModeledUpstreamCompatKeys) { assert.ok( classifiedRowByKey.get(compatKey)?.management, @@ -112,6 +119,7 @@ test("year entrypoints are cumulative, date-bounded, and deterministic", () => { path.join(fixture.outputRoot, "generated", "year", String(year), "index.d.ts"), "utf8", ); + assert.doesNotMatch(source, /\bsubstr\s*\(/); assert.doesNotMatch(source, /^declare function escape\b/m); } @@ -234,12 +242,14 @@ test("weekly update summary reports year contract changes for manual review", () includedCompatKeys: ["javascript.builtins.Array"], notModeledUpstreamCompatKeys: [], }]); + const currentState = createUpdateState([{ year: 2024, contentHash: `sha256-${"b".repeat(64)}`, includedCompatKeys: ["javascript.builtins.Array"], notModeledUpstreamCompatKeys: [], }]); + const manifest = { snapshot: { name: "baseline-js", @@ -249,6 +259,7 @@ test("weekly update summary reports year contract changes for manual review", () libSource: {}, typescriptSource: {}, }; + const summary = buildUpdateSummary({ currentManifest: manifest, currentState, @@ -331,6 +342,7 @@ function writeConsumerConfig(consumerDirectory, name, types) { */ function assertCompilerFailuresContain(consumerDirectory, name, expectedError) { const configPath = path.join(consumerDirectory, `tsconfig.${name}.json`); + for (const failure of [ runTscExpectFailure(["-p", configPath], { cwd: consumerDirectory }), runTscStradaExpectFailure(["-p", configPath], { cwd: consumerDirectory }),