Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/typescript-update.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,6 @@ jobs:
commit-message: "chore: update TypeScript toolchain snapshot"
title: "chore: update TypeScript toolchain snapshot"
body-path: .tmp/typescript-toolchain-update-pr.md

- name: Check latest TypeScript major against the peer range
run: npm run check:typescript-peer-latest
101 changes: 99 additions & 2 deletions deploy/package-lib.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,74 @@ export async function createPackageStages(options = {}) {
return summaries;
}

const DELIVERED_COMPAT_RESOLUTION_KINDS = new Set([
"constructor",
"inherited-member",
"member",
"option-property",
"root-availability",
"signature-compat",
"transform-only",
"type-property",
]);
const NON_DECLARATION_COMPAT_RESOLUTION_KINDS = new Set([
"already-excluded-upstream",
"behavioral",
"not-modeled-upstream",
]);

/**
* @param {Array<{ includeInTarget: boolean; resolutionKind: string; }>} classifiedCompatRows
*/
export function countIncludedCompatRows(classifiedCompatRows) {
let count = 0;
for (const row of classifiedCompatRows) {
const delivered = DELIVERED_COMPAT_RESOLUTION_KINDS.has(row.resolutionKind);
const nonDeclaration = NON_DECLARATION_COMPAT_RESOLUTION_KINDS.has(row.resolutionKind);
if (!delivered && !nonDeclaration) {
throw new Error(`Unknown compat resolution kind: ${row.resolutionKind}`);
}
if (row.includeInTarget && delivered) {
count++;
}
}
return count;
}

/**
* @param {string} range
* @param {string[]} versions
*/
export function assertTypeScriptPeerRange(range, versions) {
const numericIdentifier = "(0|[1-9]\\d*)";
const rangeMatch = typeof range === "string"
? range.match(new RegExp(`^>=${numericIdentifier} <${numericIdentifier}$`))
: undefined;
if (!rangeMatch) {
throw new Error(`Unsupported TypeScript peer range: ${range}`);
}
const minimumMajor = Number(rangeMatch[1]);
const maximumMajor = Number(rangeMatch[2]);
if (minimumMajor >= maximumMajor) {
throw new Error(`Unsupported TypeScript peer range: ${range}`);
}
if (!versions.length) {
throw new Error("No TypeScript versions were provided for peer range validation");
}
for (const version of versions) {
const versionMatch = typeof version === "string"
? version.match(new RegExp(`^${numericIdentifier}\\.${numericIdentifier}\\.${numericIdentifier}$`))
: undefined;
if (!versionMatch) {
throw new Error(`Unsupported TypeScript version: ${version}`);
}
const major = Number(versionMatch[1]);
if (major < minimumMajor || major >= maximumMajor) {
throw new Error(`TypeScript ${version} is outside peer range ${range}`);
}
}
}

/**
* @param {{
* packageId?: string;
Expand Down Expand Up @@ -113,6 +181,10 @@ export async function publishReleasePlan(releasePlan, options = {}) {
* @param {string} stageDirectory
*/
async function createPackageStage(packageConfig, snapshot, versionOverride, stageDirectory) {
assertTypeScriptPeerRange(packageConfig.typescriptPeerDependencyRange, [
snapshot.manifest.snapshot.typescriptStradaVersion,
snapshot.manifest.snapshot.typescriptVersion,
]);
await rm(stageDirectory, { recursive: true, force: true });
await mkdir(stageDirectory, { recursive: true });

Expand All @@ -123,6 +195,16 @@ async function createPackageStage(packageConfig, snapshot, versionOverride, stag
}

const packageVersion = versionOverride ?? await resolveNextPackageVersion(packageConfig);
const includedCompatCount = countIncludedCompatRows(snapshot.classification.classifiedCompatRows);
const snapshotMetadata = {
schemaVersion: 1,
baselineDate: snapshot.manifest.snapshot.baselineDate,
webFeaturesPackageVersion: snapshot.manifest.snapshot.webFeaturesPackageVersion,
webFeaturesGitHead: snapshot.manifest.snapshot.webFeaturesGitHead,
typescriptVersion: snapshot.manifest.snapshot.typescriptVersion,
includedCompatCount,
generatorVersion: snapshot.manifest.snapshot.generatorVersion,
};
const packageJson = {
name: packageConfig.name,
version: packageVersion,
Expand All @@ -136,6 +218,14 @@ async function createPackageStage(packageConfig, snapshot, versionOverride, stag
bugs: {
url: packageConfig.bugsUrl,
},
peerDependencies: {
typescript: packageConfig.typescriptPeerDependencyRange,
},
peerDependenciesMeta: {
typescript: {
optional: true,
},
},
publishConfig: {
access: "public",
},
Expand All @@ -144,6 +234,7 @@ async function createPackageStage(packageConfig, snapshot, versionOverride, stag
files: [
"index.d.ts",
"baseline.d.ts",
"snapshot.json",
"reports/",
"README.md",
"LICENSE",
Expand All @@ -159,16 +250,21 @@ async function createPackageStage(packageConfig, snapshot, versionOverride, stag
path.join(stageDirectory, "package.json"),
`${JSON.stringify(packageJson, undefined, 2)}\n`,
);
await writeFile(
path.join(stageDirectory, "snapshot.json"),
`${JSON.stringify(snapshotMetadata, undefined, 2)}\n`,
);
await writeFile(
path.join(stageDirectory, "README.md"),
renderTemplate(await readFile(packageConfig.readmeTemplatePath, "utf8"), {
PACKAGE_NAME: packageConfig.name,
PACKAGE_VERSION: packageVersion,
TYPESCRIPT_PEER_DEPENDENCY_RANGE: packageConfig.typescriptPeerDependencyRange,
BASELINE_DATE: snapshot.manifest.snapshot.baselineDate,
TYPESCRIPT_VERSION: snapshot.manifest.snapshot.typescriptVersion,
WEB_FEATURES_VERSION: snapshot.manifest.snapshot.webFeaturesPackageVersion,
WEB_FEATURES_GIT_HEAD: snapshot.manifest.snapshot.webFeaturesGitHead,
INCLUDED_COMPAT_COUNT: String(snapshot.classification.summary.includedCompatCount),
INCLUDED_COMPAT_COUNT: String(includedCompatCount),
SELECTED_UNIT_COUNT: String(snapshot.generation.summary.selectedUnitCount),
TRANSFORMED_UNIT_COUNT: String(snapshot.generation.summary.transformedUnitCount),
}),
Expand Down Expand Up @@ -463,7 +559,7 @@ function renderReleaseNotes(options) {
`- TypeScript package: ${snapshot.manifest.snapshot.typescriptVersion}`,
`- web-features package: ${snapshot.manifest.snapshot.webFeaturesPackageVersion}`,
`- web-features gitHead: ${snapshot.manifest.snapshot.webFeaturesGitHead}`,
`- Included compat rows: ${snapshot.classification.summary.includedCompatCount}`,
`- Included compat rows: ${countIncludedCompatRows(snapshot.classification.classifiedCompatRows)}`,
`- Selected declaration units: ${snapshot.generation.summary.selectedUnitCount}`,
`- Transformed units: ${snapshot.generation.summary.transformedUnitCount}`,
"",
Expand Down Expand Up @@ -588,6 +684,7 @@ function compareStrings(left, right) {
* id: string;
* name: string;
* description: string;
* typescriptPeerDependencyRange: string;
* initialVersion: string;
* license: string;
* keywords: string[];
Expand Down
1 change: 1 addition & 0 deletions deploy/package-registry.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const baselinePackage = {
id: "baseline",
name: "typescript-baseline-lib",
description: "Baseline widely available JavaScript declarations for TypeScript.",
typescriptPeerDependencyRange: ">=6 <8",
initialVersion: "0.0.1",
license: "Apache-2.0",
keywords: [
Expand Down
5 changes: 5 additions & 0 deletions deploy/readmes/baseline.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ This package is produced by the [`TypeScript-Baseline-lib-generator`](https://gi

Current snapshot:

- Supported TypeScript versions: `{{TYPESCRIPT_PEER_DEPENDENCY_RANGE}}`
- Baseline date: `{{BASELINE_DATE}}`
- TypeScript package: `{{TYPESCRIPT_VERSION}}`
- web-features package: `{{WEB_FEATURES_VERSION}}`
Expand All @@ -22,6 +23,10 @@ Stock TypeScript doesn't treat `"baseline"` as a built-in `lib` yet, so install
npm install --save-dev {{PACKAGE_NAME}}
```

TypeScript is an optional peer dependency. Install a supported TypeScript 6.x or 7.x compiler separately if your project does not already provide one.

The same snapshot facts are available to tools through `{{PACKAGE_NAME}}/snapshot.json`.

```json
{
"compilerOptions": {
Expand Down
6 changes: 3 additions & 3 deletions derived/current/classification.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"lowCompatCount": 77,
"falseCompatCount": 323,
"includedCompatCount": 750,
"emitCompatCount": 998,
"emitCompatCount": 997,
"notModeledUpstreamCount": 64,
"alreadyExcludedUpstreamCount": 1,
"managedCompatCount": 68,
Expand Down Expand Up @@ -14753,10 +14753,10 @@
"snapshot": [],
"compatRoot": "RegExp",
"includeInTarget": true,
"resolutionKind": "inherited-member",
"resolutionKind": "behavioral",
"resolvedUnitIds": [],
"transforms": [],
"notes": "javascript.builtins.RegExp.toString.escaping is inherited from shared object prototype declarations"
"notes": "javascript.builtins.RegExp.toString.escaping is tracked as behavior on RegExp.toString"
},
{
"compatKey": "javascript.builtins.RegExp.unicode",
Expand Down
9 changes: 9 additions & 0 deletions lib/classifier.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -688,6 +688,15 @@ function classifyMemberRow(options) {
].sort((left, right) => compareStringsCaseSensitive(left.id, right.id));

if (!memberUnits.length) {
if (isBehaviorQualifier(qualifierSegments)) {
return {
resolutionKind: "behavioral",
resolvedUnitIds: [],
notes: `${compatRow.compatKey} is tracked as behavior on ${compatRoot}.${memberName}`,
transforms: [],
};
}

const inheritedUnits = [
...[...rootSurface.instanceContainerSymbols].flatMap(symbol =>
getDeclarationUnits(inventory, symbol)
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
},
"scripts": {
"generate": "node scripts/generate.mjs",
"check:typescript-peer-latest": "node scripts/check-typescript-peer-latest.mjs",
"update:typescript-toolchain": "node scripts/update-typescript-toolchain.mjs",
"update:web-features": "node scripts/update-web-features.mjs",
"checkout:typescript-source": "node scripts/checkout-typescript-source.mjs",
Expand Down
16 changes: 16 additions & 0 deletions scripts/check-typescript-peer-latest.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// @ts-check

import path from "node:path";
import { fileURLToPath } from "node:url";
import { assertTypeScriptPeerRange } from "../deploy/package-lib.mjs";
import { baselinePackage } from "../deploy/package-registry.mjs";
import { npmViewField } from "../lib/installed-package.mjs";

const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const latestVersion = npmViewField(repoRoot, "typescript", "dist-tags.latest");
if (typeof latestVersion !== "string") {
throw new Error("Could not resolve the latest stable TypeScript version");
}

assertTypeScriptPeerRange(baselinePackage.typescriptPeerDependencyRange, [latestVersion]);
console.log(`typescript@${latestVersion} is within peer range ${baselinePackage.typescriptPeerDependencyRange}`);
58 changes: 36 additions & 22 deletions scripts/update-typescript-toolchain.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
applyStradaSourcePin,
applyTypeScriptGoSourcePin,
} from "../lib/typescript-source.mjs";
import { assertTypeScriptPeerRange } from "../deploy/package-lib.mjs";
import { baselinePackage } from "../deploy/package-registry.mjs";

const scriptPath = fileURLToPath(import.meta.url);
const scriptDirectory = path.dirname(scriptPath);
Expand All @@ -41,16 +43,14 @@ await main();

async function main() {
const typescriptVersion = args.typescriptVersion
?? String(npmViewField(repoRoot, "typescript", "dist-tags.latest"));
?? resolveLatestStableVersion(
`typescript@${baselinePackage.typescriptPeerDependencyRange}`,
"supported TypeScript",
);
const stradaVersion = args.stradaVersion ?? resolveLatestStradaVersion();

assertStableSemver(typescriptVersion, "typescript");
assertStableSemver(stradaVersion, "typescript-strada");
if (!typescriptVersion.startsWith("7.") && !args.typescriptVersion) {
throw new Error(
`Resolved typescript dist-tags.latest is ${typescriptVersion}, expected a 7.x release. Pass --typescript-version explicitly to override.`,
);
}
assertTypeScriptPeerRange(baselinePackage.typescriptPeerDependencyRange, [typescriptVersion]);
assertTypeScriptPeerRange(">=6 <7", [stradaVersion]);

execFileSync("npm", [
"install",
Expand Down Expand Up @@ -110,18 +110,43 @@ async function main() {
* Automatically follows any security patch released on 6.0.x.
*/
function resolveLatestStradaVersion() {
const versions = npmViewField(repoRoot, "typescript@<7.0.0-0", "version");
return resolveLatestStableVersion("typescript@<7.0.0-0", "Strada (typescript <7)");
}

/**
* @param {string} packageSpecifier
* @param {string} label
*/
function resolveLatestStableVersion(packageSpecifier, label) {
const versions = npmViewField(repoRoot, packageSpecifier, "version");
const versionList = Array.isArray(versions) ? versions : [versions];
const stableVersions = versionList
.filter(version => typeof version === "string" && /^\d+\.\d+\.\d+$/u.test(version));
.filter(version => typeof version === "string" && /^\d+\.\d+\.\d+$/u.test(version))
.sort(compareStableSemver);

const latest = stableVersions.at(-1);
if (!latest) {
throw new Error("Could not resolve the latest stable Strada (typescript <7) version from the registry");
throw new Error(`Could not resolve the latest stable ${label} version from the registry`);
}
return latest;
}

/**
* @param {string} left
* @param {string} right
*/
function compareStableSemver(left, right) {
const leftParts = left.split(".").map(Number);
const rightParts = right.split(".").map(Number);
for (let index = 0; index < 3; index++) {
const difference = leftParts[index] - rightParts[index];
if (difference) {
return difference;
}
}
return 0;
}

/**
* Compute the content hash of lib/*.d.ts from every reference platform's registry
* tarball, verify they all match exactly, then return the pin.
Expand Down Expand Up @@ -197,17 +222,6 @@ function computeCrossPlatformLibSourcePin(typescriptVersion) {
};
}

/**
* @param {string} version
* @param {string} label
*/
function assertStableSemver(version, label) {
if (!/^\d+\.\d+\.\d+$/u.test(version)) {
throw new Error(`Resolved ${label} version ${version} is not a stable x.y.z release`);
}
}


/**
* @param {string[]} argv
*/
Expand Down
13 changes: 13 additions & 0 deletions test/classifier.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,9 @@ test("classifier routes synthetic compat rows to the expected resolution kinds",
row("javascript.builtins.Widget.Widget", "high"),
row("javascript.builtins.Widget.Widget.options_parameter", "low"),
row("javascript.builtins.Widget.Widget.options_size_parameter", "low"),
row("javascript.builtins.Widget.configure.options_size_parameter.extended_values", "high"),
row("javascript.builtins.Widget.toy.stable_sorting", "high"),
row("javascript.builtins.Widget.toString.escaping", "high"),
row("javascript.builtins.TypedArray.at", "high"),
row("javascript.builtins.Iterator.map", "low"),
row("javascript.functions.arguments", "high"),
Expand Down Expand Up @@ -306,10 +308,21 @@ test("classifier routes synthetic compat rows to the expected resolution kinds",
assert.equal(optionRow.resolutionKind, "option-property");
assert.ok(optionRow.resolvedUnitIds.some(unitId => unitId.includes("WidgetOptions.size")));

const qualifiedOptionRow = findRow(
classification,
"javascript.builtins.Widget.configure.options_size_parameter.extended_values",
);
assert.equal(qualifiedOptionRow.resolutionKind, "option-property");
assert.ok(qualifiedOptionRow.resolvedUnitIds.some(unitId => unitId.includes("WidgetOptions.size")));

const behaviorRow = findRow(classification, "javascript.builtins.Widget.toy.stable_sorting");
assert.equal(behaviorRow.resolutionKind, "behavioral");
assert.deepEqual(behaviorRow.resolvedUnitIds, []);

const inheritedBehaviorRow = findRow(classification, "javascript.builtins.Widget.toString.escaping");
assert.equal(inheritedBehaviorRow.resolutionKind, "behavioral");
assert.deepEqual(inheritedBehaviorRow.resolvedUnitIds, []);

// Synthetic TypedArray root: BCD's "TypedArray" expands to individual typed array types.
// But only typed arrays whose own root row is in the target get mapped.
const typedArrayRow = findRow(classification, "javascript.builtins.TypedArray.at");
Expand Down
Loading