From fd4e07ef9adbc5737c3c6bd36e7b849c4249368e Mon Sep 17 00:00:00 2001 From: Ryuya Date: Sun, 23 Aug 2026 17:04:53 -0700 Subject: [PATCH] refactor: unify TypeScript upstream integration --- .github/workflows/release.yml | 9 + .github/workflows/test-typescript-go.yml | 70 ---- .github/workflows/test-typescript.yml | 9 + .github/workflows/typescript-update.yml | 15 + derived/current/generation.json | 12 +- docs/OPERATIONS.md | 11 +- .../baselines/reference/libBaseline.types | 81 ----- .../allow/_support/intl-duration-format.d.ts | 2 +- .../allow/_support/intl-segmenter.d.ts | 2 +- .../allow/_support/promise-withresolvers.d.ts | 2 +- .../current/allow/_support/set-methods.d.ts | 2 +- .../current/allow/array-fromasync/index.d.ts | 2 +- .../current/allow/array-group/index.d.ts | 2 +- .../current/allow/atomics-pause/index.d.ts | 2 +- .../allow/atomics-wait-async/index.d.ts | 2 +- .../current/allow/float16array/index.d.ts | 2 +- .../current/allow/getorinsert/index.d.ts | 2 +- .../allow/intl-duration-format/index.d.ts | 2 +- .../current/allow/intl-segmenter/index.d.ts | 2 +- .../current/allow/promise-try/index.d.ts | 2 +- .../allow/promise-withresolvers/index.d.ts | 2 +- .../current/allow/regexp-escape/index.d.ts | 2 +- .../allow/resizable-buffers/index.d.ts | 2 +- .../current/allow/set-methods/index.d.ts | 2 +- .../allow/transferable-arraybuffer/index.d.ts | 2 +- .../allow/uint8array-base64-hex/index.d.ts | 2 +- generated/current/baseline.d.ts | 2 +- generated/current/year/2020/index.d.ts | 2 +- generated/current/year/2021/index.d.ts | 2 +- generated/current/year/2022/index.d.ts | 2 +- generated/current/year/2023/index.d.ts | 2 +- generated/current/year/2024/index.d.ts | 2 +- generated/current/year/2025/index.d.ts | 2 +- lib/generator.mjs | 24 +- lib/manifest-snapshot.mjs | 4 - lib/surface-inventory.mjs | 6 +- lib/text-patch.mjs | 8 +- lib/typescript-go-upstream.mjs | 141 -------- lib/typescript-proposal.mjs | 141 ++++++++ lib/typescript-source.mjs | 284 +++------------ lib/typescript-upstream.mjs | 255 +++++++------ manifests/baseline-js.json | 10 +- package.json | 2 - scripts/checkout-typescript-source.mjs | 65 +--- scripts/prepare-typescript-go-pr.mjs | 95 ----- scripts/prepare-typescript-pr.mjs | 25 +- scripts/test-typescript-go-integration.mjs | 198 ---------- scripts/test-typescript-integration.mjs | 132 +++++-- scripts/update-typescript-toolchain.mjs | 27 +- scripts/write-update-pr-body.mjs | 8 +- test/consumer-smoke.test.mjs | 2 +- test/helpers.mjs | 4 +- test/packed-consumer-smoke.test.mjs | 3 +- test/toolchain-smoke.test.mjs | 4 +- test/typescript-go-upstream.test.mjs | 150 -------- test/typescript-proposal.test.mjs | 140 ++++++++ test/typescript-source.test.mjs | 95 ++++- test/typescript-upstream.test.mjs | 340 ++++++++++-------- test/year-entrypoints.test.mjs | 1 - 59 files changed, 994 insertions(+), 1429 deletions(-) delete mode 100644 .github/workflows/test-typescript-go.yml delete mode 100644 lib/typescript-go-upstream.mjs create mode 100644 lib/typescript-proposal.mjs delete mode 100644 scripts/prepare-typescript-go-pr.mjs delete mode 100644 scripts/test-typescript-go-integration.mjs delete mode 100644 test/typescript-go-upstream.test.mjs create mode 100644 test/typescript-proposal.test.mjs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 13c0821..19d5b03 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -59,6 +59,15 @@ jobs: - name: Checkout pinned TypeScript source run: npm run checkout:typescript-source -- --out .tmp/TypeScript --force + - name: Read required Go version + id: go-version + run: echo "version=$(awk '/^go / { print $2; exit }' .tmp/TypeScript/tsc/go.mod)" >> "$GITHUB_OUTPUT" + + - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 + with: + go-version: "${{ steps.go-version.outputs.version }}" + cache-dependency-path: .tmp/TypeScript/tsc/go.sum + - name: Run blocking TypeScript integration run: npm run test:typescript:full -- --typescript-dir ./.tmp/TypeScript --summary-out typescript-integration-artifacts/summary.md --baseline-diff-out typescript-integration-artifacts/baseline-changes.diff --focused-baselines-out typescript-integration-artifacts/focused --local-baselines-out typescript-integration-artifacts/raw-local-baselines diff --git a/.github/workflows/test-typescript-go.yml b/.github/workflows/test-typescript-go.yml deleted file mode 100644 index 41fb1fb..0000000 --- a/.github/workflows/test-typescript-go.yml +++ /dev/null @@ -1,70 +0,0 @@ -name: TypeScript Go Integration - -# Forward-looking gate that embeds the baseline lib into TypeScript 7 -# (microsoft/typescript-go) and verifies tsgo --lib baseline runs self-consistently. -# It's heavy (involves a Go build), so run it only on push to main / weekly / manually, not per PR. -on: - push: - branches: - - main - schedule: - # Every Friday 05:00 UTC (after web-features on Tue / TS toolchain on Thu) - - cron: "0 5 * * 5" - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: typescript-go-integration-${{ github.ref }} - cancel-in-progress: false - -defaults: - run: - shell: bash - -jobs: - integration: - runs-on: ubuntu-latest - timeout-minutes: 45 - - steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version: "22" - cache: "npm" - - run: npm ci - - - name: Verify pinned web-features dataset - run: npm run verify:dataset - - - name: Regenerate baseline - run: npm run generate - - - name: Checkout pinned typescript-go source - run: npm run checkout:typescript-source -- --source go --out .tmp/typescript-go --force - - # generate.go and the build need src/lib from the submodule (_submodules/TypeScript). - - name: Initialize typescript-go submodule at pinned pointer - run: git -C .tmp/typescript-go submodule update --init --depth 1 _submodules/TypeScript - - - name: Read required Go version - id: go-version - run: echo "version=$(grep -m1 '^go ' .tmp/typescript-go/go.mod | awk '{print $2}')" >> "$GITHUB_OUTPUT" - - - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 - with: - go-version: "${{ steps.go-version.outputs.version }}" - cache-dependency-path: .tmp/typescript-go/go.sum - - - name: Run tsgo --lib baseline integration - run: npm run test:typescript-go -- --typescript-go-dir .tmp/typescript-go --out typescript-integration-artifacts/typescript-go-summary.md - - - name: Upload integration summary - if: always() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: typescript-go-integration - path: typescript-integration-artifacts/typescript-go-summary.md - if-no-files-found: error diff --git a/.github/workflows/test-typescript.yml b/.github/workflows/test-typescript.yml index f2dc0a9..d03230e 100644 --- a/.github/workflows/test-typescript.yml +++ b/.github/workflows/test-typescript.yml @@ -72,6 +72,15 @@ jobs: - name: Checkout pinned TypeScript source run: npm run checkout:typescript-source -- --out .tmp/TypeScript --force + - name: Read required Go version + id: go-version + run: echo "version=$(awk '/^go / { print $2; exit }' .tmp/TypeScript/tsc/go.mod)" >> "$GITHUB_OUTPUT" + + - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 + with: + go-version: "${{ steps.go-version.outputs.version }}" + cache-dependency-path: .tmp/TypeScript/tsc/go.sum + - name: Run TypeScript integration checks run: npm run test:typescript:${{ steps.mode.outputs.mode }} -- --typescript-dir ./.tmp/TypeScript --summary-out typescript-integration-artifacts/summary.md --baseline-diff-out typescript-integration-artifacts/baseline-changes.diff --focused-baselines-out typescript-integration-artifacts/focused --local-baselines-out typescript-integration-artifacts/raw-local-baselines diff --git a/.github/workflows/typescript-update.yml b/.github/workflows/typescript-update.yml index 6a2e4f8..090219e 100644 --- a/.github/workflows/typescript-update.yml +++ b/.github/workflows/typescript-update.yml @@ -46,6 +46,21 @@ jobs: - name: Run standalone validation run: npm test + - name: Checkout updated TypeScript source + run: npm run checkout:typescript-source -- --out .tmp/TypeScript --force + + - name: Read required Go version + id: go-version + run: echo "version=$(awk '/^go / { print $2; exit }' .tmp/TypeScript/tsc/go.mod)" >> "$GITHUB_OUTPUT" + + - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 + with: + go-version: "${{ steps.go-version.outputs.version }}" + cache-dependency-path: .tmp/TypeScript/tsc/go.sum + + - name: Test updated TypeScript source + run: npm run test:typescript:gate -- --typescript-dir ./.tmp/TypeScript + - name: Prepare pull request body run: npm run write:update-pr-body -- --out .tmp/typescript-toolchain-update-pr.md --summary-out .tmp/typescript-toolchain-update-summary.json diff --git a/derived/current/generation.json b/derived/current/generation.json index 064cfa8..fcb7a41 100644 --- a/derived/current/generation.json +++ b/derived/current/generation.json @@ -1092,7 +1092,7 @@ { "year": 2020, "outputPath": "generated/current/year/2020/index.d.ts", - "contentHash": "sha256-7f66eaec52ccad3d4870c33071c0e9e227becdadd2f2f61725ee5cd9fcee70df", + "contentHash": "sha256-37070d861cfe386653cb7f2174009f9040e1e38e7dd8c42a271c90d03221c8cc", "includedCompatKeys": [ "javascript.builtins.AggregateError", "javascript.builtins.AggregateError.AggregateError", @@ -1723,7 +1723,7 @@ { "year": 2021, "outputPath": "generated/current/year/2021/index.d.ts", - "contentHash": "sha256-9d910337717de9eb085908983fb7b95b14daa2a019805947aaff9d6f2b6bceed", + "contentHash": "sha256-32eff0354d555bac166068b673c9253de514e7d66c62d9529dca525294f56a51", "includedCompatKeys": [ "javascript.builtins.AggregateError", "javascript.builtins.AggregateError.AggregateError", @@ -2418,7 +2418,7 @@ { "year": 2022, "outputPath": "generated/current/year/2022/index.d.ts", - "contentHash": "sha256-ebf80adec1d38790ab32677574c22971e2fdfab97bd0bb3c2706822254aa8cfb", + "contentHash": "sha256-3bf13c02fec1aa78b05d8181b5941a3f9a794ea0527052bad6461bb04e5685b0", "includedCompatKeys": [ "javascript.builtins.AggregateError", "javascript.builtins.AggregateError.AggregateError", @@ -3123,7 +3123,7 @@ { "year": 2023, "outputPath": "generated/current/year/2023/index.d.ts", - "contentHash": "sha256-7713c5b7bb9ec55b967f04ad05931baf43001c1064a2e818e21dfbf24851add9", + "contentHash": "sha256-ab5b2463a60479471ccf98811f8ca9da76d8220a11bf92dc02b7ab06bb2044e8", "includedCompatKeys": [ "javascript.builtins.AggregateError", "javascript.builtins.AggregateError.AggregateError", @@ -3850,7 +3850,7 @@ { "year": 2024, "outputPath": "generated/current/year/2024/index.d.ts", - "contentHash": "sha256-32d89c1cf963d5b3d3dc83024a3fa741d9cba39006812e8197ab2cdf1f11b0a0", + "contentHash": "sha256-09e74810ca650a81e4dff5d5fd18b3c3669ff719b9601f11987ce85ed8d1f138", "includedCompatKeys": [ "javascript.builtins.AggregateError", "javascript.builtins.AggregateError.AggregateError", @@ -4607,7 +4607,7 @@ { "year": 2025, "outputPath": "generated/current/year/2025/index.d.ts", - "contentHash": "sha256-85a9499d1c70e8b1e1b88265b92731b1877c324ab79bad7c895ab8bbfc97d520", + "contentHash": "sha256-d10524e61d0ecf8177c8c6b27c6388074c9c84c30099e9485d61a1d9cb2049b1", "includedCompatKeys": [ "javascript.builtins.AggregateError", "javascript.builtins.AggregateError.AggregateError", diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md index 3b1d745..7afe6ed 100644 --- a/docs/OPERATIONS.md +++ b/docs/OPERATIONS.md @@ -17,9 +17,7 @@ The generator reads only these four inputs: Before the generator reads the lib files, it compares their file count and content hash with `libSource` in the manifest. -`typescript-strada` parses declarations and performs compiler self-tests. It is an npm alias for the frozen TypeScript 6 Strada release. - -The generator uses this package because the TypeScript 7 JavaScript API differs from the Strada compiler API. +`typescript-strada` parses declarations and performs compiler self-tests. It is an npm alias for the frozen TypeScript 6 release because the native TypeScript package does not expose the same JavaScript compiler API. ## Determinism and fail-closed layers @@ -29,12 +27,11 @@ Identical inputs produce byte-identical artifacts on each supported operating sy - The lib-source layer requires the pinned content hash and file count. - The classification layer rejects unmanaged keys, stale registry entries, and resolution-kind changes. A strict JSON Schema validates the registry. - The generation layer rejects excluded declarations in the output. It also compiles the complete output as a self-test. -- The packaging layer installs staged and packed packages. It compiles consumer fixtures with TypeScript 7 and Strada. +- The packaging layer installs staged and packed packages. It compiles consumer fixtures with TypeScript 7 and TypeScript 6. ## Toolchain pins -`npm run update:typescript-toolchain` refreshes these three pins in `manifests/baseline-js.json`: +`npm run update:typescript-toolchain` refreshes these two pins in `manifests/baseline-js.json`: - `libSource` records the platform package and one content hash. The update requires identical files on Linux, macOS, and Windows. -- `typescriptSource` records the frozen Strada tag and commit for the Strada integration gate. -- `typescriptGoSource` records the `typescript/vX.Y.Z` tag and the `_submodules/TypeScript` commit. +- `typescriptSource` records the exact `microsoft/TypeScript` `main` commit used by proposal preparation and integration tests. Proposal artifacts are generated from that checkout's declaration corpus. diff --git a/fixtures/typescript/tests/baselines/reference/libBaseline.types b/fixtures/typescript/tests/baselines/reference/libBaseline.types index 3213098..6e789ee 100644 --- a/fixtures/typescript/tests/baselines/reference/libBaseline.types +++ b/fixtures/typescript/tests/baselines/reference/libBaseline.types @@ -3,129 +3,75 @@ === libBaseline.ts === Intl.supportedValuesOf("currency"); >Intl.supportedValuesOf("currency") : string[] -> : ^^^^^^^^ >Intl.supportedValuesOf : (key: "calendar" | "collation" | "currency" | "numberingSystem" | "timeZone" | "unit") => string[] -> : ^ ^^ ^^^^^ >Intl : typeof Intl -> : ^^^^^^^^^^^ >supportedValuesOf : (key: "calendar" | "collation" | "currency" | "numberingSystem" | "timeZone" | "unit") => string[] -> : ^ ^^ ^^^^^ >"currency" : "currency" -> : ^^^^^^^^^^ new Intl.NumberFormat().formatRange(1, 2); >new Intl.NumberFormat().formatRange(1, 2) : string -> : ^^^^^^ >new Intl.NumberFormat().formatRange : (start: number | bigint | Intl.StringNumericLiteral, end: number | bigint | Intl.StringNumericLiteral) => string -> : ^ ^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ >new Intl.NumberFormat() : Intl.NumberFormat -> : ^^^^^^^^^^^^^^^^^ >Intl.NumberFormat : Intl.NumberFormatConstructor -> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ >Intl : typeof Intl -> : ^^^^^^^^^^^ >NumberFormat : Intl.NumberFormatConstructor -> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ >formatRange : (start: number | bigint | Intl.StringNumericLiteral, end: number | bigint | Intl.StringNumericLiteral) => string -> : ^ ^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ >1 : 1 -> : ^ >2 : 2 -> : ^ [1, 2, 3].at(0); >[1, 2, 3].at(0) : number | undefined -> : ^^^^^^^^^^^^^^^^^^ >[1, 2, 3].at : (index: number) => number | undefined -> : ^ ^^ ^^^^^^^^^^^^^^^^^^^^^^^ >[1, 2, 3] : number[] -> : ^^^^^^^^ >1 : 1 -> : ^ >2 : 2 -> : ^ >3 : 3 -> : ^ >at : (index: number) => number | undefined -> : ^ ^^ ^^^^^^^^^^^^^^^^^^^^^^^ >0 : 0 -> : ^ [1, 2, 3].toReversed(); >[1, 2, 3].toReversed() : number[] -> : ^^^^^^^^ >[1, 2, 3].toReversed : () => number[] -> : ^^^^^^^^^^^^^^ >[1, 2, 3] : number[] -> : ^^^^^^^^ >1 : 1 -> : ^ >2 : 2 -> : ^ >3 : 3 -> : ^ >toReversed : () => number[] -> : ^^^^^^^^^^^^^^ "baseline".at(0); >"baseline".at(0) : string | undefined -> : ^^^^^^^^^^^^^^^^^^ >"baseline".at : (index: number) => string | undefined -> : ^ ^^ ^^^^^ >"baseline" : "baseline" -> : ^^^^^^^^^^ >at : (index: number) => string | undefined -> : ^ ^^ ^^^^^ >0 : 0 -> : ^ Object.hasOwn({ baseline: true }, "baseline"); >Object.hasOwn({ baseline: true }, "baseline") : boolean -> : ^^^^^^^ >Object.hasOwn : (o: object, v: PropertyKey) => boolean -> : ^ ^^ ^^ ^^ ^^^^^ >Object : ObjectConstructor -> : ^^^^^^^^^^^^^^^^^ >hasOwn : (o: object, v: PropertyKey) => boolean -> : ^ ^^ ^^ ^^ ^^^^^ >{ baseline: true } : { baseline: boolean; } -> : ^^^^^^^^^^^^^^^^^^^^^^ >baseline : boolean -> : ^^^^^^^ >true : true -> : ^^^^ >"baseline" : "baseline" -> : ^^^^^^^^^^ new Error("problem", { cause: new Error("root") }); >new Error("problem", { cause: new Error("root") }) : Error -> : ^^^^^ >Error : ErrorConstructor -> : ^^^^^^^^^^^^^^^^ >"problem" : "problem" -> : ^^^^^^^^^ >{ cause: new Error("root") } : { cause: Error; } -> : ^^^^^^^^^^^^^^^^^ >cause : Error -> : ^^^^^ >new Error("root") : Error -> : ^^^^^ >Error : ErrorConstructor -> : ^^^^^^^^^^^^^^^^ >"root" : "root" -> : ^^^^^^ /baseline/d.hasIndices; >/baseline/d.hasIndices : boolean -> : ^^^^^^^ >/baseline/d : RegExp -> : ^^^^^^ >hasIndices : boolean -> : ^^^^^^^ type __BaselineLegacyRegExpStatic = >__BaselineLegacyRegExpStatic : __BaselineLegacyRegExpStatic -> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | "$1" | "$2" @@ -148,70 +94,43 @@ type __BaselineLegacyRegExpStatic = | "$'"; type __BaselineAssertNever = T; >__BaselineAssertNever : T -> : ^ type __BaselineHasNoLegacyRegExpStatics = __BaselineAssertNever< >__BaselineHasNoLegacyRegExpStatics : never -> : ^^^^^ Extract<__BaselineLegacyRegExpStatic, keyof typeof RegExp> >RegExp : RegExpConstructor -> : ^^^^^^^^^^^^^^^^^ >; (function probeCaller() {}).caller; // Error >(function probeCaller() {}).caller : any -> : ^^^ >(function probeCaller() {}) : () => void -> : ^^^^^^^^^^ >function probeCaller() {} : () => void -> : ^^^^^^^^^^ >probeCaller : () => void -> : ^^^^^^^^^^ >caller : any -> : ^^^ "baseline".substr(1); // Error >"baseline".substr(1) : any -> : ^^^ >"baseline".substr : any -> : ^^^ >"baseline" : "baseline" -> : ^^^^^^^^^^ >substr : any -> : ^^^ >1 : 1 -> : ^ new RegExp("baseline").compile("baseline"); // Error >new RegExp("baseline").compile("baseline") : any -> : ^^^ >new RegExp("baseline").compile : any -> : ^^^ >new RegExp("baseline") : RegExp -> : ^^^^^^ >RegExp : RegExpConstructor -> : ^^^^^^^^^^^^^^^^^ >"baseline" : "baseline" -> : ^^^^^^^^^^ >compile : any -> : ^^^ >"baseline" : "baseline" -> : ^^^^^^^^^^ (function probeArguments() { return arguments.callee; })(); // Error >(function probeArguments() { return arguments.callee; })() : any -> : ^^^ >(function probeArguments() { return arguments.callee; }) : () => any -> : ^^^^^^^^^ >function probeArguments() { return arguments.callee; } : () => any -> : ^^^^^^^^^ >probeArguments : () => any -> : ^^^^^^^^^ >arguments.callee : any -> : ^^^ >arguments : IArguments -> : ^^^^^^^^^^ >callee : any -> : ^^^ diff --git a/generated/current/allow/_support/intl-duration-format.d.ts b/generated/current/allow/_support/intl-duration-format.d.ts index 7b6201b..792dcb6 100644 --- a/generated/current/allow/_support/intl-duration-format.d.ts +++ b/generated/current/allow/_support/intl-duration-format.d.ts @@ -1,6 +1,6 @@ // // Generated by TypeScript Baseline Lib Generator from TypeScript lib declaration files. -// Source declarations are derived from the npm `typescript` package. +// Source declarations are derived from the pinned TypeScript declaration corpus. // Do not edit this file directly. // diff --git a/generated/current/allow/_support/intl-segmenter.d.ts b/generated/current/allow/_support/intl-segmenter.d.ts index 2eb7ec3..66f7f7c 100644 --- a/generated/current/allow/_support/intl-segmenter.d.ts +++ b/generated/current/allow/_support/intl-segmenter.d.ts @@ -1,6 +1,6 @@ // // Generated by TypeScript Baseline Lib Generator from TypeScript lib declaration files. -// Source declarations are derived from the npm `typescript` package. +// Source declarations are derived from the pinned TypeScript declaration corpus. // Do not edit this file directly. // diff --git a/generated/current/allow/_support/promise-withresolvers.d.ts b/generated/current/allow/_support/promise-withresolvers.d.ts index 1a2fd6d..0e996a2 100644 --- a/generated/current/allow/_support/promise-withresolvers.d.ts +++ b/generated/current/allow/_support/promise-withresolvers.d.ts @@ -1,6 +1,6 @@ // // Generated by TypeScript Baseline Lib Generator from TypeScript lib declaration files. -// Source declarations are derived from the npm `typescript` package. +// Source declarations are derived from the pinned TypeScript declaration corpus. // Do not edit this file directly. // diff --git a/generated/current/allow/_support/set-methods.d.ts b/generated/current/allow/_support/set-methods.d.ts index 0e6fce1..c892446 100644 --- a/generated/current/allow/_support/set-methods.d.ts +++ b/generated/current/allow/_support/set-methods.d.ts @@ -1,6 +1,6 @@ // // Generated by TypeScript Baseline Lib Generator from TypeScript lib declaration files. -// Source declarations are derived from the npm `typescript` package. +// Source declarations are derived from the pinned TypeScript declaration corpus. // Do not edit this file directly. // diff --git a/generated/current/allow/array-fromasync/index.d.ts b/generated/current/allow/array-fromasync/index.d.ts index 868c45b..2ad8326 100644 --- a/generated/current/allow/array-fromasync/index.d.ts +++ b/generated/current/allow/array-fromasync/index.d.ts @@ -1,6 +1,6 @@ // // Generated by TypeScript Baseline Lib Generator from TypeScript lib declaration files. -// Source declarations are derived from the npm `typescript` package. +// Source declarations are derived from the pinned TypeScript declaration corpus. // Do not edit this file directly. // diff --git a/generated/current/allow/array-group/index.d.ts b/generated/current/allow/array-group/index.d.ts index 47666e7..64a93b4 100644 --- a/generated/current/allow/array-group/index.d.ts +++ b/generated/current/allow/array-group/index.d.ts @@ -1,6 +1,6 @@ // // Generated by TypeScript Baseline Lib Generator from TypeScript lib declaration files. -// Source declarations are derived from the npm `typescript` package. +// Source declarations are derived from the pinned TypeScript declaration corpus. // Do not edit this file directly. // diff --git a/generated/current/allow/atomics-pause/index.d.ts b/generated/current/allow/atomics-pause/index.d.ts index e062ed7..35f1aba 100644 --- a/generated/current/allow/atomics-pause/index.d.ts +++ b/generated/current/allow/atomics-pause/index.d.ts @@ -1,6 +1,6 @@ // // Generated by TypeScript Baseline Lib Generator from TypeScript lib declaration files. -// Source declarations are derived from the npm `typescript` package. +// Source declarations are derived from the pinned TypeScript declaration corpus. // Do not edit this file directly. // diff --git a/generated/current/allow/atomics-wait-async/index.d.ts b/generated/current/allow/atomics-wait-async/index.d.ts index bd76d4d..e9c44f0 100644 --- a/generated/current/allow/atomics-wait-async/index.d.ts +++ b/generated/current/allow/atomics-wait-async/index.d.ts @@ -1,6 +1,6 @@ // // Generated by TypeScript Baseline Lib Generator from TypeScript lib declaration files. -// Source declarations are derived from the npm `typescript` package. +// Source declarations are derived from the pinned TypeScript declaration corpus. // Do not edit this file directly. // diff --git a/generated/current/allow/float16array/index.d.ts b/generated/current/allow/float16array/index.d.ts index 585eea2..99bcd7b 100644 --- a/generated/current/allow/float16array/index.d.ts +++ b/generated/current/allow/float16array/index.d.ts @@ -1,6 +1,6 @@ // // Generated by TypeScript Baseline Lib Generator from TypeScript lib declaration files. -// Source declarations are derived from the npm `typescript` package. +// Source declarations are derived from the pinned TypeScript declaration corpus. // Do not edit this file directly. // diff --git a/generated/current/allow/getorinsert/index.d.ts b/generated/current/allow/getorinsert/index.d.ts index 21e716f..075d298 100644 --- a/generated/current/allow/getorinsert/index.d.ts +++ b/generated/current/allow/getorinsert/index.d.ts @@ -1,6 +1,6 @@ // // Generated by TypeScript Baseline Lib Generator from TypeScript lib declaration files. -// Source declarations are derived from the npm `typescript` package. +// Source declarations are derived from the pinned TypeScript declaration corpus. // Do not edit this file directly. // diff --git a/generated/current/allow/intl-duration-format/index.d.ts b/generated/current/allow/intl-duration-format/index.d.ts index b5de055..831e837 100644 --- a/generated/current/allow/intl-duration-format/index.d.ts +++ b/generated/current/allow/intl-duration-format/index.d.ts @@ -2,7 +2,7 @@ // // Generated by TypeScript Baseline Lib Generator from TypeScript lib declaration files. -// Source declarations are derived from the npm `typescript` package. +// Source declarations are derived from the pinned TypeScript declaration corpus. // Do not edit this file directly. // diff --git a/generated/current/allow/intl-segmenter/index.d.ts b/generated/current/allow/intl-segmenter/index.d.ts index a16fd34..1f0091d 100644 --- a/generated/current/allow/intl-segmenter/index.d.ts +++ b/generated/current/allow/intl-segmenter/index.d.ts @@ -2,7 +2,7 @@ // // Generated by TypeScript Baseline Lib Generator from TypeScript lib declaration files. -// Source declarations are derived from the npm `typescript` package. +// Source declarations are derived from the pinned TypeScript declaration corpus. // Do not edit this file directly. // diff --git a/generated/current/allow/promise-try/index.d.ts b/generated/current/allow/promise-try/index.d.ts index c554244..023d042 100644 --- a/generated/current/allow/promise-try/index.d.ts +++ b/generated/current/allow/promise-try/index.d.ts @@ -1,6 +1,6 @@ // // Generated by TypeScript Baseline Lib Generator from TypeScript lib declaration files. -// Source declarations are derived from the npm `typescript` package. +// Source declarations are derived from the pinned TypeScript declaration corpus. // Do not edit this file directly. // diff --git a/generated/current/allow/promise-withresolvers/index.d.ts b/generated/current/allow/promise-withresolvers/index.d.ts index f85fc7d..88ab234 100644 --- a/generated/current/allow/promise-withresolvers/index.d.ts +++ b/generated/current/allow/promise-withresolvers/index.d.ts @@ -2,7 +2,7 @@ // // Generated by TypeScript Baseline Lib Generator from TypeScript lib declaration files. -// Source declarations are derived from the npm `typescript` package. +// Source declarations are derived from the pinned TypeScript declaration corpus. // Do not edit this file directly. // diff --git a/generated/current/allow/regexp-escape/index.d.ts b/generated/current/allow/regexp-escape/index.d.ts index 5262174..9869659 100644 --- a/generated/current/allow/regexp-escape/index.d.ts +++ b/generated/current/allow/regexp-escape/index.d.ts @@ -1,6 +1,6 @@ // // Generated by TypeScript Baseline Lib Generator from TypeScript lib declaration files. -// Source declarations are derived from the npm `typescript` package. +// Source declarations are derived from the pinned TypeScript declaration corpus. // Do not edit this file directly. // diff --git a/generated/current/allow/resizable-buffers/index.d.ts b/generated/current/allow/resizable-buffers/index.d.ts index f021ccb..243e681 100644 --- a/generated/current/allow/resizable-buffers/index.d.ts +++ b/generated/current/allow/resizable-buffers/index.d.ts @@ -1,6 +1,6 @@ // // Generated by TypeScript Baseline Lib Generator from TypeScript lib declaration files. -// Source declarations are derived from the npm `typescript` package. +// Source declarations are derived from the pinned TypeScript declaration corpus. // Do not edit this file directly. // diff --git a/generated/current/allow/set-methods/index.d.ts b/generated/current/allow/set-methods/index.d.ts index da31934..b377beb 100644 --- a/generated/current/allow/set-methods/index.d.ts +++ b/generated/current/allow/set-methods/index.d.ts @@ -2,7 +2,7 @@ // // Generated by TypeScript Baseline Lib Generator from TypeScript lib declaration files. -// Source declarations are derived from the npm `typescript` package. +// Source declarations are derived from the pinned TypeScript declaration corpus. // Do not edit this file directly. // diff --git a/generated/current/allow/transferable-arraybuffer/index.d.ts b/generated/current/allow/transferable-arraybuffer/index.d.ts index ab9914b..a935854 100644 --- a/generated/current/allow/transferable-arraybuffer/index.d.ts +++ b/generated/current/allow/transferable-arraybuffer/index.d.ts @@ -1,6 +1,6 @@ // // Generated by TypeScript Baseline Lib Generator from TypeScript lib declaration files. -// Source declarations are derived from the npm `typescript` package. +// Source declarations are derived from the pinned TypeScript declaration corpus. // Do not edit this file directly. // diff --git a/generated/current/allow/uint8array-base64-hex/index.d.ts b/generated/current/allow/uint8array-base64-hex/index.d.ts index 11dda1c..e6063b5 100644 --- a/generated/current/allow/uint8array-base64-hex/index.d.ts +++ b/generated/current/allow/uint8array-base64-hex/index.d.ts @@ -1,6 +1,6 @@ // // Generated by TypeScript Baseline Lib Generator from TypeScript lib declaration files. -// Source declarations are derived from the npm `typescript` package. +// Source declarations are derived from the pinned TypeScript declaration corpus. // Do not edit this file directly. // diff --git a/generated/current/baseline.d.ts b/generated/current/baseline.d.ts index bd052b4..b2a87fb 100644 --- a/generated/current/baseline.d.ts +++ b/generated/current/baseline.d.ts @@ -1,6 +1,6 @@ // // Generated by TypeScript Baseline Lib Generator from TypeScript lib declaration files. -// Source declarations are derived from the npm `typescript` package. +// Source declarations are derived from the pinned TypeScript declaration corpus. // Do not edit this file directly. // diff --git a/generated/current/year/2020/index.d.ts b/generated/current/year/2020/index.d.ts index e59bab9..3f55665 100644 --- a/generated/current/year/2020/index.d.ts +++ b/generated/current/year/2020/index.d.ts @@ -1,6 +1,6 @@ // // Generated by TypeScript Baseline Lib Generator from TypeScript lib declaration files. -// Source declarations are derived from the npm `typescript` package. +// Source declarations are derived from the pinned TypeScript declaration corpus. // Do not edit this file directly. // diff --git a/generated/current/year/2021/index.d.ts b/generated/current/year/2021/index.d.ts index d2cb466..dc93a4e 100644 --- a/generated/current/year/2021/index.d.ts +++ b/generated/current/year/2021/index.d.ts @@ -1,6 +1,6 @@ // // Generated by TypeScript Baseline Lib Generator from TypeScript lib declaration files. -// Source declarations are derived from the npm `typescript` package. +// Source declarations are derived from the pinned TypeScript declaration corpus. // Do not edit this file directly. // diff --git a/generated/current/year/2022/index.d.ts b/generated/current/year/2022/index.d.ts index e19f842..98a7066 100644 --- a/generated/current/year/2022/index.d.ts +++ b/generated/current/year/2022/index.d.ts @@ -1,6 +1,6 @@ // // Generated by TypeScript Baseline Lib Generator from TypeScript lib declaration files. -// Source declarations are derived from the npm `typescript` package. +// Source declarations are derived from the pinned TypeScript declaration corpus. // Do not edit this file directly. // diff --git a/generated/current/year/2023/index.d.ts b/generated/current/year/2023/index.d.ts index bd052b4..b2a87fb 100644 --- a/generated/current/year/2023/index.d.ts +++ b/generated/current/year/2023/index.d.ts @@ -1,6 +1,6 @@ // // Generated by TypeScript Baseline Lib Generator from TypeScript lib declaration files. -// Source declarations are derived from the npm `typescript` package. +// Source declarations are derived from the pinned TypeScript declaration corpus. // Do not edit this file directly. // diff --git a/generated/current/year/2024/index.d.ts b/generated/current/year/2024/index.d.ts index b27d176..63ea590 100644 --- a/generated/current/year/2024/index.d.ts +++ b/generated/current/year/2024/index.d.ts @@ -1,6 +1,6 @@ // // Generated by TypeScript Baseline Lib Generator from TypeScript lib declaration files. -// Source declarations are derived from the npm `typescript` package. +// Source declarations are derived from the pinned TypeScript declaration corpus. // Do not edit this file directly. // diff --git a/generated/current/year/2025/index.d.ts b/generated/current/year/2025/index.d.ts index 996b88e..053f4c3 100644 --- a/generated/current/year/2025/index.d.ts +++ b/generated/current/year/2025/index.d.ts @@ -1,6 +1,6 @@ // // Generated by TypeScript Baseline Lib Generator from TypeScript lib declaration files. -// Source declarations are derived from the npm `typescript` package. +// Source declarations are derived from the pinned TypeScript declaration corpus. // Do not edit this file directly. // diff --git a/lib/generator.mjs b/lib/generator.mjs index b25494e..36d4f90 100644 --- a/lib/generator.mjs +++ b/lib/generator.mjs @@ -7,8 +7,8 @@ import { } from "node:fs/promises"; import { createHash } from "node:crypto"; import path from "node:path"; -// TypeScript 7 (tsgo) has a different JS API than Strada, so parse .d.ts and -// self-check with the frozen final Strada line (npm alias: typescript-strada). +// The native TypeScript package does not expose the compiler API used here, so +// parse .d.ts and self-check with the TypeScript 6 npm alias. import ts from "typescript-strada"; import { classifyCompatRowsForTarget, @@ -72,6 +72,9 @@ const NON_DECLARATION_RESOLUTION_KINDS = new Set([ * @param {{ * manifestPath: string; * repoRoot: string; + * manifest?: any; + * libDirectory?: string; + * reportPathPrefix?: string; * }} options */ export async function generateFirstClassBaselineLib(options) { @@ -373,10 +376,14 @@ export function assertRuntimeDeclarationProvenance(options) { * @param {{ * manifestPath: string; * repoRoot: string; + * manifest?: any; + * libDirectory?: string; + * reportPathPrefix?: string; * }} options */ -async function createGenerationPlan({ manifestPath, repoRoot }) { - const manifest = await readManifest(manifestPath); +async function createGenerationPlan(options) { + const { manifestPath, repoRoot } = options; + const manifest = options.manifest ?? await readManifest(manifestPath); if (!manifest.firstClassLib?.libName) { throw new Error(`Manifest ${manifestPath} is missing firstClassLib.libName`); } @@ -435,13 +442,12 @@ async function createGenerationPlan({ manifestPath, repoRoot }) { // In TS7, lib.*.d.ts ships in platform-specific packages, so verify it // matches the manifest libSource pin (content hash) before reading. - const libSource = await verifyLibSource({ - repoRoot, - manifest, - }); + const libSource = options.libDirectory + ? { libDirectory: path.resolve(options.libDirectory) } + : await verifyLibSource({ repoRoot, manifest }); const sourceLibEntries = await discoverBuiltinSourceLibEntries({ libDirectory: libSource.libDirectory, - reportPathPrefix: `${manifest.libSource.basePackage}/lib`, + reportPathPrefix: options.reportPathPrefix ?? `${manifest.libSource.basePackage}/lib`, }); const inventory = await createSurfaceInventory({ snapshotName: manifest.snapshot.name, diff --git a/lib/manifest-snapshot.mjs b/lib/manifest-snapshot.mjs index c60a574..d6e7b0f 100644 --- a/lib/manifest-snapshot.mjs +++ b/lib/manifest-snapshot.mjs @@ -55,10 +55,6 @@ export async function refreshManifestSnapshot(options) { webFeaturesPackageVersion: dataset.snapshot.webFeaturesPackageVersion, webFeaturesGitHead, }; - // In TypeScript 7, native-preview merged into the typescript package (next tag). - // Explicitly drop the legacy fields carried over from old manifests. - delete manifest.snapshot.tsgoVersion; - delete manifest.toolchain?.tsgoPackage; manifest.dataset = toPosixRelativePath(manifestDirectory, datasetPath); if (updateOutputPaths) { diff --git a/lib/surface-inventory.mjs b/lib/surface-inventory.mjs index ce810d2..c2215ab 100644 --- a/lib/surface-inventory.mjs +++ b/lib/surface-inventory.mjs @@ -8,8 +8,8 @@ import { writeFile, } from "node:fs/promises"; import path from "node:path"; -// TypeScript 7 (tsgo) has a different JS API than Strada, so parse .d.ts and -// self-check with the frozen final Strada line (npm alias: typescript-strada). +// The native TypeScript package does not expose the compiler API used here, so +// parse .d.ts and self-check with the TypeScript 6 npm alias. import ts from "typescript-strada"; import { compareStringsCaseSensitive, @@ -26,7 +26,7 @@ const READONLY_COMPANION_BY_RUNTIME_ROOT = new Map([ const GENERATED_LIB_HEADER = [ "// ", "// Generated by TypeScript Baseline Lib Generator from TypeScript lib declaration files.", - "// Source declarations are derived from the npm `typescript` package.", + "// Source declarations are derived from the pinned TypeScript declaration corpus.", "// Do not edit this file directly.", "// ", "", diff --git a/lib/text-patch.mjs b/lib/text-patch.mjs index eb972f5..b44ccd3 100644 --- a/lib/text-patch.mjs +++ b/lib/text-patch.mjs @@ -3,9 +3,7 @@ import fs from "node:fs"; import path from "node:path"; -// Shared text operations for idempotent patches to upstream -// (microsoft/TypeScript = Strada, microsoft/typescript-go = tsgo). -// Centralized here so both patchers run under the same invariants. +// Shared text operations for idempotent patches to upstream TypeScript. // Policy: throw if the anchor is missing (fail-closed, so we don't silently miss an // upstream reformat), and no-op if the marker is already present (idempotent re-runs). @@ -57,7 +55,9 @@ export function ensurePatchedTextFile(filePath, options) { throw new Error(`Could not find ${options.description} anchor in ${filePath}`); } - const nextText = originalText.replace(options.anchor, options.insertion); + const lineEnding = originalText.includes("\r\n") ? "\r\n" : "\n"; + const insertion = options.insertion.replace(/\r?\n/gu, lineEnding); + const nextText = originalText.replace(options.anchor, insertion); fs.writeFileSync(filePath, nextText); return { diff --git a/lib/typescript-go-upstream.mjs b/lib/typescript-go-upstream.mjs deleted file mode 100644 index 32274b9..0000000 --- a/lib/typescript-go-upstream.mjs +++ /dev/null @@ -1,141 +0,0 @@ -// @ts-check - -import { execFileSync } from "node:child_process"; -import fs from "node:fs"; -import path from "node:path"; -import { - assertFileExists, - copyFileIfChanged, - ensurePatchedTextFile, -} from "./text-patch.mjs"; - -// Baseline lib patch for microsoft/typescript-go (TypeScript 7). -// -// typescript-go generates lib.*.d.ts from the submodule's (_submodules/TypeScript) -// src/lib via `go generate ./internal/bundled/` (generate.go). -// The generated libs_generated.go / embed_generated.go are gofmt'd and DO NOT EDIT. -// So we don't touch the generated files by hand; we ride the same "add input and -// regenerate" path as upstream: -// 1. Put baseline.d.ts at the submodule's src/lib/baseline.d.ts -// (generate.go treats libName "baseline" as sourced from "baseline.d.ts") -// 2. Add "baseline" to the libs array in the submodule's src/lib/libs.json -// (this is generate.go's only lib enumeration input) -// 3. Add {Key: "baseline", Value: "lib.baseline.d.ts"} to the hand-maintained -// LibMap (internal/tsoptions/enummaps.go) -// Running `go generate` and the build is the caller's job (integration script / CI). -// This module handles only the deterministic text patch and doesn't depend on the Go toolchain. - -export const typescriptGoSubmoduleLibDir = path.join("_submodules", "TypeScript", "src", "lib"); -export const enumMapsRelativePath = path.join("internal", "tsoptions", "enummaps.go"); - -const libMapEntry = '\t{Key: "baseline", Value: "lib.baseline.d.ts"},'; -const libMapAnchor = '\t{Key: "esnext", Value: "lib.esnext.d.ts"},'; -const libsJsonEntry = ' "baseline",'; -const libsJsonAnchor = ' "esnext",'; - -/** - * When expectedCommit is passed, verify the target clone's HEAD matches the pin - * before writing. Pass allowUnpinned to explicitly skip the check. - * - * @param {{ - * repoRoot: string; - * typescriptGoDir: string; - * generatedLibPath?: string; - * expectedCommit?: string; - * allowUnpinned?: boolean; - * }} options - */ -export function prepareTypeScriptGoBaselinePatch(options) { - const repoRoot = path.resolve(options.repoRoot); - const typescriptGoDir = path.resolve(options.typescriptGoDir); - - if (options.expectedCommit && !options.allowUnpinned) { - assertCloneMatchesPin(typescriptGoDir, options.expectedCommit); - } - - const generatedLibPath = path.resolve( - options.generatedLibPath ?? path.join(repoRoot, "generated", "current", "baseline.d.ts"), - ); - const submoduleLibDir = path.join(typescriptGoDir, typescriptGoSubmoduleLibDir); - const targetLibSourcePath = path.join(submoduleLibDir, "baseline.d.ts"); - const libsJsonPath = path.join(submoduleLibDir, "libs.json"); - const enumMapsPath = path.join(typescriptGoDir, enumMapsRelativePath); - - assertFileExists(generatedLibPath, "generated baseline lib"); - assertFileExists(libsJsonPath, "typescript-go submodule libs.json (did you check out submodules?)"); - assertFileExists(enumMapsPath, "typescript-go enummaps.go"); - - const copiedGeneratedLib = copyFileIfChanged(generatedLibPath, targetLibSourcePath); - const patchedLibsJson = ensurePatchedTextFile(libsJsonPath, { - alreadyPresentMarker: '"baseline"', - anchor: libsJsonAnchor, - insertion: `${libsJsonAnchor}\n${libsJsonEntry}`, - description: "submodule libs.json lib entry", - }); - const patchedEnumMaps = ensurePatchedTextFile(enumMapsPath, { - alreadyPresentMarker: libMapEntry.trim(), - anchor: libMapAnchor, - insertion: `${libMapAnchor}\n${libMapEntry}`, - description: "enummaps.go LibMap entry", - }); - - return { - typescriptGoDir, - generatedLibPath, - targetLibSourcePath, - libsJsonPath, - enumMapsPath, - copiedGeneratedLib, - patchedLibsJson, - patchedEnumMaps, - }; -} - -/** - * @param {ReturnType} summary - */ -export function renderTypeScriptGoPatchSummary(summary) { - const lines = [ - "# TypeScript Go Patch Summary", - "", - `- typescript-go clone: \`${summary.typescriptGoDir}\``, - `- Generated source: \`${summary.generatedLibPath}\``, - `- Installed lib source: \`${summary.targetLibSourcePath}\``, - `- submodule libs.json patched: ${summary.patchedLibsJson.changed ? "yes" : "no"}`, - `- enummaps.go LibMap patched: ${summary.patchedEnumMaps.changed ? "yes" : "no"}`, - "", - "## Next step", - "", - "Run `go generate ./internal/bundled/` in the clone to regenerate", - "`libs_generated.go` / `embed_generated.go` with the baseline lib, then build.", - ]; - - return `${lines.join("\n")}\n`; -} - -/** - * @param {string} typescriptGoDir - * @param {string} expectedCommit - */ -function assertCloneMatchesPin(typescriptGoDir, expectedCommit) { - /** @type {string | undefined} */ - let headCommit; - try { - headCommit = execFileSync("git", ["rev-parse", "HEAD"], { - cwd: typescriptGoDir, - encoding: "utf8", - stdio: ["ignore", "pipe", "ignore"], - }).trim(); - } - catch { - headCommit = undefined; - } - - if (headCommit !== expectedCommit) { - throw new Error([ - `typescript-go clone at ${typescriptGoDir} is at ${headCommit ?? ""}, but the manifest pins ${expectedCommit}.`, - "Refusing to patch an unpinned clone.", - "Use scripts/checkout-typescript-source.mjs --source go to get a pinned checkout, or pass --allow-unpinned.", - ].join("\n")); - } -} diff --git a/lib/typescript-proposal.mjs b/lib/typescript-proposal.mjs new file mode 100644 index 0000000..ce784eb --- /dev/null +++ b/lib/typescript-proposal.mjs @@ -0,0 +1,141 @@ +// @ts-check + +import { execFileSync } from "node:child_process"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { generateFirstClassBaselineLib } from "./generator.mjs"; +import { computeLibDirectoryContentHash } from "./toolchain-libs.mjs"; +import { + assertTypeScriptSourcePin, + typescriptGeneratedLibDirectory, +} from "./typescript-upstream.mjs"; + +/** + * Generate the proposal artifact from the declaration corpus in the pinned + * TypeScript checkout rather than from the npm distribution corpus. + * + * @param {{ + * repoRoot: string; + * manifestPath: string; + * manifest: any; + * typescriptDir?: string; + * expectedCommit?: string; + * allowUnpinned?: boolean; + * }} options + */ +export async function generateTypeScriptProposalLib(options) { + const repoRoot = path.resolve(options.repoRoot); + const typescriptDir = path.resolve(options.typescriptDir ?? path.join(repoRoot, ".tmp", "TypeScript")); + if (options.expectedCommit && !options.allowUnpinned) { + assertTypeScriptSourcePin(typescriptDir, options.expectedCommit); + } + + const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), "typescript-baseline-proposal-")); + try { + const libDirectory = path.join(workspaceRoot, "libs"); + if (options.expectedCommit && !options.allowUnpinned) { + snapshotDeclarationCorpusFromCommit(typescriptDir, options.expectedCommit, libDirectory); + } + else { + snapshotDeclarationCorpus( + path.join(typescriptDir, typescriptGeneratedLibDirectory), + libDirectory, + ); + } + + const manifest = structuredClone(options.manifest); + const fingerprint = computeLibDirectoryContentHash(libDirectory); + manifest.libSource = { + ...manifest.libSource, + basePackage: "microsoft/TypeScript", + libContentHash: fingerprint.hash, + libFileCount: fingerprint.fileCount, + }; + manifest.classificationOutput = ".tmp/derived/classification.json"; + manifest.compatManagementOutput = ".tmp/derived/compat-management-report.json"; + manifest.inventoryOutput = ".tmp/derived/inventory.json"; + manifest.generationOutput = ".tmp/derived/generation.json"; + manifest.firstClassLib = { + ...manifest.firstClassLib, + outputFile: ".tmp/generated/baseline.d.ts", + allowDirectory: ".tmp/generated/allow", + yearDirectory: ".tmp/generated/year", + }; + + const plan = await generateFirstClassBaselineLib({ + repoRoot: workspaceRoot, + manifestPath: options.manifestPath, + manifest, + libDirectory, + reportPathPrefix: "microsoft/TypeScript/tsc/internal/bundled/libs", + }); + const verifiedFingerprint = computeLibDirectoryContentHash(libDirectory); + if ( + verifiedFingerprint.hash !== fingerprint.hash + || verifiedFingerprint.fileCount !== fingerprint.fileCount + ) { + throw new Error("TypeScript declaration corpus changed while generating the proposal"); + } + return { + outputPath: plan.topLevelOutputPath, + cleanup: () => fs.rmSync(workspaceRoot, { recursive: true, force: true }), + }; + } + catch (error) { + fs.rmSync(workspaceRoot, { recursive: true, force: true }); + throw error; + } +} + +/** + * @param {string} typescriptDir + * @param {string} commit + * @param {string} destinationDirectory + */ +function snapshotDeclarationCorpusFromCommit(typescriptDir, commit, destinationDirectory) { + const relativeDirectory = typescriptGeneratedLibDirectory.split(path.sep).join("/"); + const filePaths = execFileSync( + "git", + ["--no-replace-objects", "ls-tree", "-r", "--name-only", "-z", commit, "--", relativeDirectory], + { cwd: typescriptDir, encoding: "utf8" }, + ).split("\0").filter(relativePath => ( + path.posix.dirname(relativePath) === relativeDirectory + && relativePath.endsWith(".d.ts") + && path.posix.basename(relativePath) !== "lib.baseline.d.ts" + )).sort(); + if (!filePaths.length) { + throw new Error(`Pinned TypeScript commit contains no declaration files under ${relativeDirectory}`); + } + + fs.mkdirSync(destinationDirectory, { recursive: true }); + for (const relativePath of filePaths) { + const source = execFileSync("git", ["--no-replace-objects", "show", `${commit}:${relativePath}`], { + cwd: typescriptDir, + maxBuffer: 16 * 1024 * 1024, + }); + fs.writeFileSync(path.join(destinationDirectory, path.posix.basename(relativePath)), source); + } +} + +/** + * @param {string} sourceDirectory + * @param {string} destinationDirectory + */ +function snapshotDeclarationCorpus(sourceDirectory, destinationDirectory) { + if (!fs.existsSync(sourceDirectory) || !fs.statSync(sourceDirectory).isDirectory()) { + throw new Error(`TypeScript generated lib directory does not exist: ${sourceDirectory}`); + } + + fs.mkdirSync(destinationDirectory, { recursive: true }); + const fileNames = fs.readdirSync(sourceDirectory, { withFileTypes: true }) + .filter(entry => entry.isFile() && entry.name.endsWith(".d.ts") && entry.name !== "lib.baseline.d.ts") + .map(entry => entry.name) + .sort(); + if (!fileNames.length) { + throw new Error(`TypeScript generated lib directory contains no declaration files: ${sourceDirectory}`); + } + for (const fileName of fileNames) { + fs.copyFileSync(path.join(sourceDirectory, fileName), path.join(destinationDirectory, fileName)); + } +} diff --git a/lib/typescript-source.mjs b/lib/typescript-source.mjs index 02545f7..6027dc3 100644 --- a/lib/typescript-source.mjs +++ b/lib/typescript-source.mjs @@ -5,219 +5,59 @@ import fs from "node:fs"; import path from "node:path"; import { retrySync } from "./net-retry.mjs"; -export const defaultStradaRepository = "https://github.com/microsoft/TypeScript.git"; -export const defaultTypeScriptGoRepository = "https://github.com/microsoft/typescript-go.git"; -export const typescriptGoSubmodulePath = "_submodules/TypeScript"; - -// TypeScript 6 (Strada / microsoft/TypeScript) is feature-frozen on the 6.0 line. -// - typescriptSource: frozen pin of the final Strada series. Used by the existing -// heavyweight integration gate (hereby build + libBaseline fixture) and to verify upstream PR shape. -// - typescriptGoSource: pin of TypeScript 7 (microsoft/typescript-go). -// Tags are named `typescript/vX.Y.Z`. The canonical source for lib.d.ts is -// that repo's _submodules/TypeScript (= a microsoft/TypeScript commit), so we -// pin that submodule commit alongside it. - -/** - * @param {string} stradaVersion - */ -export function resolveStradaSourceTag(stradaVersion) { - return `v${stradaVersion}`; -} - -/** - * @param {string} typescriptVersion - */ -export function resolveTypeScriptGoSourceTag(typescriptVersion) { - return `typescript/v${typescriptVersion}`; -} +export const defaultTypeScriptRepository = "https://github.com/microsoft/TypeScript.git"; +export const defaultTypeScriptRef = "main"; /** - * @param {{ manifest: any; stradaVersion: string; repository?: string; }} options + * @param {{ manifest: any; repository?: string; ref?: string; }} options */ -export function applyStradaSourcePin(options) { - const repository = options.repository ?? defaultStradaRepository; - const tag = resolveStradaSourceTag(options.stradaVersion); - const commit = resolveGitTagCommit(repository, tag); +export function applyTypeScriptSourcePin(options) { + const repository = options.repository ?? defaultTypeScriptRepository; + const ref = options.ref ?? defaultTypeScriptRef; + const commit = resolveGitRefCommit(repository, ref); options.manifest.typescriptSource = { repository, - tag, + ref, commit, }; return options.manifest.typescriptSource; } -/** - * Pin the typescript-go release tag and the Strada submodule commit at that tag. - * ls-remote can't return the submodule commit, so read it with `git ls-tree` - * from a temporary shallow clone. - * - * @param {{ - * manifest: any; - * typescriptVersion: string; - * repository?: string; - * workDirectory: string; - * }} options - */ -export function applyTypeScriptGoSourcePin(options) { - const repository = options.repository ?? defaultTypeScriptGoRepository; - const tag = resolveTypeScriptGoSourceTag(options.typescriptVersion); - const commit = resolveGitTagCommit(repository, tag); - - const cloneDirectory = path.join(options.workDirectory, "typescript-go-pin"); - fs.rmSync(cloneDirectory, { recursive: true, force: true }); - fs.mkdirSync(path.dirname(cloneDirectory), { recursive: true }); - retrySync(`clone ${repository}@${tag}`, () => { - fs.rmSync(cloneDirectory, { recursive: true, force: true }); - execFileSync("git", ["clone", "--depth", "1", "--branch", tag, repository, cloneDirectory], { - stdio: "inherit", - }); - }); - - const checkedOutCommit = readGitHeadCommit(cloneDirectory); - if (checkedOutCommit !== commit) { - throw new Error(`Pinned typescript-go checkout mismatch. Expected ${commit} from ${tag}, got ${checkedOutCommit ?? ""}.`); - } - const stradaSubmoduleCommit = readSubmoduleCommit(cloneDirectory, typescriptGoSubmodulePath); - fs.rmSync(cloneDirectory, { recursive: true, force: true }); - - options.manifest.typescriptGoSource = { - repository, - tag, - commit, - stradaSubmoduleCommit, - }; - - return options.manifest.typescriptGoSource; -} - /** * @param {any} manifest */ -export function readStradaSourcePin(manifest) { +export function readTypeScriptSourcePin(manifest) { const repository = manifest.typescriptSource?.repository; - const tag = manifest.typescriptSource?.tag; + const ref = manifest.typescriptSource?.ref; const commit = manifest.typescriptSource?.commit; if (!repository || typeof repository !== "string") { throw new Error("Manifest is missing typescriptSource.repository"); } - if (!tag || typeof tag !== "string") { - throw new Error("Manifest is missing typescriptSource.tag"); + 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"); } - - const expectedTag = manifest.snapshot?.typescriptStradaVersion - ? resolveStradaSourceTag(manifest.snapshot.typescriptStradaVersion) - : undefined; - if (expectedTag && tag !== expectedTag) { - throw new Error( - `Manifest typescriptSource.tag ${tag} does not match snapshot.typescriptStradaVersion ${manifest.snapshot.typescriptStradaVersion}`, - ); - } - return { repository, - tag, + ref, commit, }; } /** - * @param {any} manifest - */ -export function readTypeScriptGoSourcePin(manifest) { - const repository = manifest.typescriptGoSource?.repository; - const tag = manifest.typescriptGoSource?.tag; - const commit = manifest.typescriptGoSource?.commit; - const stradaSubmoduleCommit = manifest.typescriptGoSource?.stradaSubmoduleCommit; - - if (!repository || typeof repository !== "string") { - throw new Error("Manifest is missing typescriptGoSource.repository"); - } - if (!tag || typeof tag !== "string") { - throw new Error("Manifest is missing typescriptGoSource.tag"); - } - if (!commit || typeof commit !== "string" || !/^[0-9a-f]{40}$/u.test(commit)) { - throw new Error("Manifest is missing a valid typescriptGoSource.commit"); - } - if (!stradaSubmoduleCommit || typeof stradaSubmoduleCommit !== "string" || !/^[0-9a-f]{40}$/u.test(stradaSubmoduleCommit)) { - throw new Error("Manifest is missing a valid typescriptGoSource.stradaSubmoduleCommit"); - } - - const expectedTag = manifest.snapshot?.typescriptVersion - ? resolveTypeScriptGoSourceTag(manifest.snapshot.typescriptVersion) - : undefined; - if (expectedTag && tag !== expectedTag) { - throw new Error( - `Manifest typescriptGoSource.tag ${tag} does not match snapshot.typescriptVersion ${manifest.snapshot.typescriptVersion}`, - ); - } - - return { - repository, - tag, - commit, - stradaSubmoduleCommit, - }; -} - -/** - * @param {{ manifest: any; outDirectory: string; force?: boolean; }} options - */ -export function checkoutStradaSource(options) { - const pin = readStradaSourcePin(options.manifest); - return checkoutPinnedRepository({ - pin, - outDirectory: options.outDirectory, - force: options.force, - }); -} - -/** - * Check out typescript-go at the pinned tag and verify the submodule pointer - * matches the manifest's stradaSubmoduleCommit. - * Doesn't initialize the submodule contents (not needed for the patch gate). - * * @param {{ manifest: any; outDirectory: string; force?: boolean; }} options */ -export function checkoutTypeScriptGoSource(options) { - const pin = readTypeScriptGoSourcePin(options.manifest); - const result = checkoutPinnedRepository({ - pin, - outDirectory: options.outDirectory, - force: options.force, - }); - - const submoduleCommit = readSubmoduleCommit(result.outDirectory, typescriptGoSubmodulePath); - if (submoduleCommit !== pin.stradaSubmoduleCommit) { - throw new Error( - `typescript-go submodule pointer ${submoduleCommit} does not match pinned stradaSubmoduleCommit ${pin.stradaSubmoduleCommit}`, - ); - } - - return { - ...result, - stradaSubmoduleCommit: submoduleCommit, - }; -} - -/** - * @param {{ - * pin: { repository: string; tag: string; commit: string; }; - * outDirectory: string; - * force?: boolean; - * }} options - */ -function checkoutPinnedRepository(options) { - const pin = options.pin; +export function checkoutTypeScriptSource(options) { + const pin = readTypeScriptSourcePin(options.manifest); const outDirectory = path.resolve(options.outDirectory); const existingCommit = readGitHeadCommit(outDirectory); - if (existingCommit === pin.commit) { + if (existingCommit === pin.commit && isGitWorkingTreeClean(outDirectory)) { return { ...pin, outDirectory, @@ -225,26 +65,31 @@ function checkoutPinnedRepository(options) { }; } - if (fs.existsSync(outDirectory)) { - if (!options.force) { - throw new Error( - `Refusing to replace existing directory ${outDirectory}. Re-run with --force or choose an empty path.`, - ); - } - fs.rmSync(outDirectory, { recursive: true, force: true }); + if (fs.existsSync(outDirectory) && !options.force) { + 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.`, + ); } - retrySync(`clone ${pin.repository}@${pin.tag}`, () => { + retrySync(`checkout ${pin.repository}@${pin.commit}`, () => { fs.rmSync(outDirectory, { recursive: true, force: true }); - execFileSync("git", ["clone", "--depth", "1", "--branch", pin.tag, pin.repository, outDirectory], { + fs.mkdirSync(outDirectory, { recursive: true }); + execFileSync("git", ["init", "--quiet"], { cwd: outDirectory }); + execFileSync("git", ["remote", "add", "origin", pin.repository], { cwd: outDirectory }); + execFileSync("git", ["fetch", "--depth", "1", "origin", pin.commit], { + cwd: outDirectory, stdio: "inherit", }); + execFileSync("git", ["checkout", "--quiet", "--detach", "FETCH_HEAD"], { cwd: outDirectory }); }); const checkedOutCommit = readGitHeadCommit(outDirectory); if (checkedOutCommit !== pin.commit) { throw new Error( - `Pinned checkout mismatch. Expected ${pin.commit} from ${pin.tag}, got ${checkedOutCommit ?? ""}.`, + `Pinned checkout mismatch. Expected ${pin.commit}, got ${checkedOutCommit ?? ""}.`, ); } @@ -257,56 +102,29 @@ function checkoutPinnedRepository(options) { /** * @param {string} repository - * @param {string} tag + * @param {string} ref */ -function resolveGitTagCommit(repository, tag) { - const output = retrySync(`resolve ${tag} from ${repository}`, () => execFileSync("git", ["ls-remote", "--tags", repository, `${tag}*`], { - encoding: "utf8", - })).trim(); +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 (!output) { - throw new Error(`Could not resolve ${tag} from ${repository}`); + if (lines.length !== 1) { + throw new Error(`Expected one exact match for ${remoteRef} from ${repository}, got ${lines.length}`); } - const lines = output - .split(/\r?\n/u) - .map(line => line.trim()) - .filter(Boolean); - const preferredRef = `refs/tags/${tag}^{}`; - const directRef = `refs/tags/${tag}`; - const preferredLine = lines.find(line => line.endsWith(preferredRef)); - const directLine = lines.find(line => line.endsWith(directRef)); - const selectedLine = preferredLine ?? directLine; - - if (!selectedLine) { - throw new Error(`Could not resolve exact tag ${tag} from ${repository}`); - } - - const [commit] = selectedLine.split(/\s+/u); - if (!commit || !/^[0-9a-f]{40}$/u.test(commit)) { - throw new Error(`Resolved invalid commit for ${tag}: ${selectedLine}`); + 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]}`); } return commit; } -/** - * @param {string} repositoryDirectory - * @param {string} submodulePath - */ -function readSubmoduleCommit(repositoryDirectory, submodulePath) { - const output = execFileSync("git", ["ls-tree", "HEAD", submodulePath], { - cwd: repositoryDirectory, - encoding: "utf8", - }).trim(); - - const match = output.match(/^160000\s+commit\s+([0-9a-f]{40})\s/u); - if (!match) { - throw new Error(`Could not read submodule commit for ${submodulePath} in ${repositoryDirectory}: ${output || ""}`); - } - return match[1]; -} - /** * @param {string} directory */ @@ -314,6 +132,7 @@ function readGitHeadCommit(directory) { if (!fs.existsSync(directory)) { return undefined; } + try { return execFileSync("git", ["rev-parse", "HEAD"], { cwd: directory, @@ -326,3 +145,14 @@ function readGitHeadCommit(directory) { } } +/** + * @param {string} directory + */ +function isGitWorkingTreeClean(directory) { + const status = execFileSync("git", ["status", "--porcelain", "--untracked-files=all"], { + cwd: directory, + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + }); + return status.length === 0; +} diff --git a/lib/typescript-upstream.mjs b/lib/typescript-upstream.mjs index 690057f..bcd89ab 100644 --- a/lib/typescript-upstream.mjs +++ b/lib/typescript-upstream.mjs @@ -1,7 +1,8 @@ // @ts-check -import { execFileSync } from "node:child_process"; +import { execFileSync, spawnSync } from "node:child_process"; import fs from "node:fs"; +import os from "node:os"; import path from "node:path"; import { assertDirectoryExists, @@ -11,19 +12,16 @@ import { ensurePatchedTextFile, } from "./text-patch.mjs"; -const commandLineParserEntry = ' ["baseline", "lib.baseline.d.ts"],'; -const commandLineParserAnchor = ' ["esnext", "lib.esnext.d.ts"],'; +export const typescriptLibSourceDirectory = path.join("tsc", "internal", "bundled", "source"); +export const typescriptGeneratedLibDirectory = path.join("tsc", "internal", "bundled", "libs"); +export const enumMapsRelativePath = path.join("tsc", "internal", "tsoptions", "enummaps.go"); + +const libMapEntry = '\t{Key: "baseline", Value: "lib.baseline.d.ts"},'; +const libMapAnchor = '\t{Key: "esnext", Value: "lib.esnext.d.ts"},'; const libsJsonEntry = ' "baseline",'; const libsJsonAnchor = ' "esnext",'; -const eslintArrayTypeEntry = ' files: ["src/lib/es2019.array.d.ts", "src/lib/baseline.d.ts"],'; -const eslintArrayTypeAnchor = ' files: ["src/lib/es2019.array.d.ts"],'; /** - * When expectedCommit is passed, verify the target clone's HEAD matches the pin - * before writing. This prevents accidentally patching an unpinned clone found by - * auto-discovery (e.g. ../TypeScript) while it's in a state unrelated to the pin. - * Only pass allowUnpinned to deliberately patch a different commit. - * * @param {{ * repoRoot: string; * typescriptDir?: string; @@ -38,57 +36,60 @@ export function prepareTypeScriptBaselinePatch(options) { const typescriptDir = resolveTypeScriptWorkingDirectory(repoRoot, options.typescriptDir); if (options.expectedCommit && !options.allowUnpinned) { - assertCloneMatchesPin(typescriptDir, options.expectedCommit); + assertTypeScriptSourcePin(typescriptDir, options.expectedCommit); } - const generatedLibPath = path.resolve(options.generatedLibPath ?? path.join(repoRoot, "generated", "current", "baseline.d.ts")); + const generatedLibPath = path.resolve( + options.generatedLibPath ?? path.join(repoRoot, "generated", "current", "baseline.d.ts"), + ); const fixturesRoot = path.resolve(options.fixturesRoot ?? path.join(repoRoot, "fixtures", "typescript")); - const sourceTestsRoot = path.join(fixturesRoot, "tests"); - const targetLibPath = path.join(typescriptDir, "src", "lib", "baseline.d.ts"); - const commandLineParserPath = path.join(typescriptDir, "src", "compiler", "commandLineParser.ts"); - const libsJsonPath = path.join(typescriptDir, "src", "lib", "libs.json"); - const eslintConfigPath = path.join(typescriptDir, "eslint.config.mjs"); + const fixtureCasesRoot = path.join(fixturesRoot, "tests", "cases"); + const fixtureBaselinesRoot = path.join(fixturesRoot, "tests", "baselines", "reference"); + const libSourceDirectory = path.join(typescriptDir, typescriptLibSourceDirectory); + const targetLibSourcePath = path.join(libSourceDirectory, "baseline.d.ts"); + const libsJsonPath = path.join(libSourceDirectory, "libs.json"); + const enumMapsPath = path.join(typescriptDir, enumMapsRelativePath); assertFileExists(generatedLibPath, "generated baseline lib"); - assertFileExists(commandLineParserPath, "TypeScript commandLineParser.ts"); - assertFileExists(libsJsonPath, "TypeScript libs.json"); - assertFileExists(eslintConfigPath, "TypeScript eslint.config.mjs"); - assertDirectoryExists(sourceTestsRoot, "TypeScript fixture tests root"); + assertFileExists(libsJsonPath, "TypeScript bundled source libs.json"); + assertFileExists(enumMapsPath, "TypeScript LibMap"); + assertDirectoryExists(fixtureCasesRoot, "TypeScript fixture cases root"); + assertDirectoryExists(fixtureBaselinesRoot, "TypeScript fixture baselines root"); - const copiedGeneratedLib = copyFileIfChanged(generatedLibPath, targetLibPath); - const patchedCommandLineParser = ensurePatchedTextFile(commandLineParserPath, { - alreadyPresentMarker: commandLineParserEntry, - anchor: commandLineParserAnchor, - insertion: `${commandLineParserAnchor}\n${commandLineParserEntry}`, - description: "commandLineParser lib entry", - }); + const copiedGeneratedLib = copyFileIfChanged(generatedLibPath, targetLibSourcePath); const patchedLibsJson = ensurePatchedTextFile(libsJsonPath, { alreadyPresentMarker: '"baseline"', anchor: libsJsonAnchor, insertion: `${libsJsonAnchor}\n${libsJsonEntry}`, - description: "libs.json lib entry", + description: "bundled source libs.json entry", }); - const patchedEslintConfig = ensurePatchedTextFile(eslintConfigPath, { - alreadyPresentMarker: eslintArrayTypeEntry, - anchor: eslintArrayTypeAnchor, - insertion: eslintArrayTypeEntry, - description: "baseline lib array-type exemption", + const patchedEnumMaps = ensurePatchedTextFile(enumMapsPath, { + alreadyPresentMarker: libMapEntry.trim(), + anchor: libMapAnchor, + insertion: `${libMapAnchor}\n${libMapEntry}`, + description: "enummaps.go LibMap entry", }); - // Track the full fixture list and the files actually rewritten this run separately. - // On an idempotent re-run, reporting unchanged files as "copied" would be misleading. - const fixtureFiles = copyDirectoryContents(sourceTestsRoot, path.join(typescriptDir, "tests")); + const fixtureFiles = [ + ...copyDirectoryContents( + fixtureCasesRoot, + path.join(typescriptDir, "tsc", "testdata", "tests", "cases"), + ), + ...copyDirectoryContents( + fixtureBaselinesRoot, + path.join(typescriptDir, "tsc", "testdata", "baselines", "reference", "compiler"), + ), + ]; const fixtureFilePaths = fixtureFiles.map(entry => entry.targetPath); const changedFixtureFiles = fixtureFiles.filter(entry => entry.changed).map(entry => entry.targetPath); return { typescriptDir, generatedLibPath, - targetGeneratedLibPath: targetLibPath, - commandLineParserPath, + targetLibSourcePath, libsJsonPath, + enumMapsPath, copiedGeneratedLib, - patchedCommandLineParser, patchedLibsJson, - patchedEslintConfig, + patchedEnumMaps, fixtureFiles: fixtureFilePaths, changedFixtureFiles, }; @@ -118,13 +119,52 @@ export function findUnexpectedTypeScriptPatchPaths(typescriptDir, allowedRelativ .sort(); } +/** + * @param {string} typescriptDir + */ +export function renderTypeScriptPatchDiff(typescriptDir) { + const trackedDiff = execFileSync("git", ["diff", "--binary", "HEAD", "--"], { + cwd: typescriptDir, + encoding: "utf8", + }); + const untrackedPaths = execFileSync("git", ["ls-files", "--others", "--exclude-standard"], { + cwd: typescriptDir, + encoding: "utf8", + }).split(/\r?\n/u).filter(Boolean).sort(); + const untrackedDiffs = untrackedPaths.map(relativePath => { + const result = spawnSync("git", ["diff", "--no-index", "--binary", "--", os.devNull, relativePath], { + cwd: typescriptDir, + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }); + if (result.error || result.status !== 1) { + throw result.error ?? new Error( + `Could not render diff for ${relativePath}: ${result.stderr || `git exited with ${result.status}`}`, + ); + } + return result.stdout; + }); + return [trackedDiff, ...untrackedDiffs].filter(Boolean).join(""); +} + +/** + * @param {string} output + * @param {string} testName + */ +export function hasPassingGoTestEvent(output, testName) { + return output.split(/\r?\n/u) + .filter(line => line.startsWith("{")) + .map(line => JSON.parse(line)) + .some(event => event.Action === "pass" && event.Test === testName); +} + /** * @param {string} typescriptDir * @param {string} relativePath */ function isExpectedTypeScriptBaselineUpdate(typescriptDir, relativePath) { if ( - !relativePath.startsWith("tests/baselines/reference/") + !relativePath.startsWith("tsc/testdata/baselines/reference/") || !relativePath.endsWith(".js") || !fs.existsSync(path.join(typescriptDir, relativePath)) ) { @@ -135,33 +175,67 @@ function isExpectedTypeScriptBaselineUpdate(typescriptDir, relativePath) { encoding: "utf8", }); const nextText = fs.readFileSync(path.join(typescriptDir, relativePath), "utf8"); - return normalizeTypeScriptBaselineUpdate(previousText) === normalizeTypeScriptBaselineUpdate(nextText); + const previousNormalized = normalizeTypeScriptBaselineUpdate(previousText); + const nextNormalized = normalizeTypeScriptBaselineUpdate(nextText); + return previousNormalized !== undefined + && nextNormalized !== undefined + && previousNormalized === nextNormalized; } /** * @param {string} text */ function normalizeTypeScriptBaselineUpdate(text) { - return text.split(/\r?\n/u) - .map(line => { - if (line.includes("Argument for '--lib' option must be:")) { - return line.replace(", 'baseline'", ""); - } - if (line.includes("one or more: es5,")) { - return line.replace(", baseline", ""); - } - return line; - }) - .join("\n") - .replace(/Inode:: \d+/gu, "Inode:: ") - .replace(/"inode":\d+/gu, '"inode":'); + let matched = false; + const diagnosticNormalized = text.replace( + /^.*Argument for '--lib' option must be:.*$/gmu, + line => { + matched = true; + return line.replace(/,\s*'baseline'(?=,)/gu, ""); + }, + ); + const helpNormalized = diagnosticNormalized.replace( + /^[^\r\n]*one or more:\s+es5[\s\S]*?(?=\r?\n(?:[+-]?[ \t]*default:|[ \t]*\r?\n))/gimu, + block => { + matched = true; + return block.replace(/,\s*baseline(?=,)/gu, "").replace(/\s+/gu, ""); + }, + ); + return matched ? helpNormalized : undefined; +} + +/** + * @param {ReturnType} summary + */ +export function renderTypeScriptPatchSummary(summary) { + const lines = [ + "# TypeScript Patch Summary", + "", + `- TypeScript clone: \`${summary.typescriptDir}\``, + `- Generated source: \`${summary.generatedLibPath}\``, + `- Installed lib source: \`${summary.targetLibSourcePath}\``, + `- bundled source libs.json patched: ${formatBoolean(summary.patchedLibsJson.changed)}`, + `- enummaps.go LibMap patched: ${formatBoolean(summary.patchedEnumMaps.changed)}`, + `- Compiler fixture files: ${summary.fixtureFiles.length} total, ${summary.changedFixtureFiles.length} written this run`, + "", + "## Installed Files", + "", + `- \`${summary.targetLibSourcePath}\``, + ...summary.fixtureFiles.map(filePath => `- \`${filePath}\`${summary.changedFixtureFiles.includes(filePath) ? " (updated)" : " (unchanged)"}`), + "", + "## Next Step", + "", + `Run \`npm ci && npm run generate\` in \`${summary.typescriptDir}\` before reviewing or proposing the complete upstream diff.`, + ]; + + return `${lines.join("\n")}\n`; } /** * @param {string} typescriptDir * @param {string} expectedCommit */ -function assertCloneMatchesPin(typescriptDir, expectedCommit) { +export function assertTypeScriptSourcePin(typescriptDir, expectedCommit) { /** @type {string | undefined} */ let headCommit; try { @@ -179,77 +253,24 @@ function assertCloneMatchesPin(typescriptDir, expectedCommit) { throw new Error([ `TypeScript clone at ${typescriptDir} is at ${headCommit ?? ""}, but the manifest pins ${expectedCommit}.`, "Refusing to patch an unpinned clone.", - "Use scripts/checkout-typescript-source.mjs to get a pinned checkout, or pass --allow-unpinned to patch deliberately.", + "Use scripts/checkout-typescript-source.mjs to get the pinned checkout, or pass --allow-unpinned deliberately.", ].join("\n")); } } -/** - * @param {{ - * repoRoot: string; - * explicitDirectory?: string; - * }} options - */ -function findTypeScriptWorkingDirectory(options) { - const repoRoot = path.resolve(options.repoRoot); - const candidates = [ - options.explicitDirectory, - path.join(repoRoot, "..", "TypeScript"), - path.join(repoRoot, "TypeScript"), - path.join(process.cwd(), "TypeScript"), - ].filter(Boolean); - - for (const candidate of candidates) { - const candidatePath = path.resolve(String(candidate)); - if (fs.existsSync(path.join(candidatePath, "src", "compiler", "commandLineParser.ts"))) { - return candidatePath; - } - } - - return undefined; -} - -/** - * @param {ReturnType} summary - */ -export function renderTypeScriptPatchSummary(summary) { - const lines = [ - "# TypeScript Patch Summary", - "", - `- TypeScript clone: \`${summary.typescriptDir}\``, - `- Generated source: \`${summary.generatedLibPath}\``, - `- Installed lib: \`${summary.targetGeneratedLibPath}\``, - `- commandLineParser patched: ${formatBoolean(summary.patchedCommandLineParser.changed)}`, - `- libs.json patched: ${formatBoolean(summary.patchedLibsJson.changed)}`, - `- eslint.config.mjs patched: ${formatBoolean(summary.patchedEslintConfig.changed)}`, - `- Compiler fixture files: ${summary.fixtureFiles.length} total, ${summary.changedFixtureFiles.length} written this run`, - "", - "## Installed Files", - "", - `- \`${summary.targetGeneratedLibPath}\``, - ...summary.fixtureFiles.map(filePath => `- \`${filePath}\`${summary.changedFixtureFiles.includes(filePath) ? " (updated)" : " (unchanged)"}`), - ]; - - return `${lines.join("\n")}\n`; -} - /** * @param {string} repoRoot * @param {string | undefined} explicitDirectory */ function resolveTypeScriptWorkingDirectory(repoRoot, explicitDirectory) { - const foundDirectory = findTypeScriptWorkingDirectory({ - repoRoot, - explicitDirectory, - }); - - if (!foundDirectory) { - throw new Error( - "Could not find a TypeScript clone. Pass --typescript-dir or place a clone at ../TypeScript.", - ); + const candidatePath = path.resolve(explicitDirectory ?? path.join(repoRoot, ".tmp", "TypeScript")); + if (fs.existsSync(path.join(candidatePath, typescriptLibSourceDirectory, "libs.json"))) { + return candidatePath; } - return foundDirectory; + throw new Error( + `Could not find a current TypeScript clone at ${candidatePath}. Run npm run checkout:typescript-source or pass --typescript-dir.`, + ); } /** diff --git a/manifests/baseline-js.json b/manifests/baseline-js.json index 162dbf5..e24b22a 100644 --- a/manifests/baseline-js.json +++ b/manifests/baseline-js.json @@ -15,8 +15,8 @@ }, "typescriptSource": { "repository": "https://github.com/microsoft/TypeScript.git", - "tag": "v6.0.3", - "commit": "050880ce59e30b356b686bd3144efe24f875ebc8" + "ref": "main", + "commit": "d6c4afddb2c55f4a9dea7b59293a99a8fdea1799" }, "baselineTarget": "high", "dataset": "../datasets/web-features-js-compat.json", @@ -43,11 +43,5 @@ ], "libContentHash": "sha256-bc4de3c01fd5e0499aa13f277bc88818adb4cb549935e8b15b5ecb86b9c73f0a", "libFileCount": 108 - }, - "typescriptGoSource": { - "repository": "https://github.com/microsoft/typescript-go.git", - "tag": "typescript/v7.0.2", - "commit": "2bd066d87f5bafd315be9f40889d0a60b9e58e0b", - "stradaSubmoduleCommit": "4d4f005c8541e0255a9d8791205fdce326e462bc" } } diff --git a/package.json b/package.json index a3e2a5a..5f6936c 100644 --- a/package.json +++ b/package.json @@ -38,11 +38,9 @@ "lint": "node node_modules/typescript-strada/bin/tsc -p tsconfig.typecheck.json --pretty false", "validate:registry": "node scripts/validate-compat-management-registry.mjs", "prepare:typescript-pr": "node scripts/prepare-typescript-pr.mjs", - "prepare:typescript-go-pr": "node scripts/prepare-typescript-go-pr.mjs", "test:typescript:smoke": "node scripts/test-typescript-integration.mjs --mode smoke", "test:typescript:gate": "node scripts/test-typescript-integration.mjs --mode gate", "test:typescript:full": "node scripts/test-typescript-integration.mjs --mode full", - "test:typescript-go": "node scripts/test-typescript-go-integration.mjs", "pack:baseline": "node deploy/createPackage.mjs", "pack:baseline:tarball": "node deploy/createPackageTarball.mjs", "release:prepare": "node deploy/prepareReleaseArtifact.mjs", diff --git a/scripts/checkout-typescript-source.mjs b/scripts/checkout-typescript-source.mjs index 1e32cca..08fab2b 100644 --- a/scripts/checkout-typescript-source.mjs +++ b/scripts/checkout-typescript-source.mjs @@ -4,10 +4,8 @@ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { - checkoutStradaSource, - checkoutTypeScriptGoSource, - readStradaSourcePin, - readTypeScriptGoSourcePin, + checkoutTypeScriptSource, + readTypeScriptSourcePin, } from "../lib/typescript-source.mjs"; const scriptPath = fileURLToPath(import.meta.url); @@ -16,56 +14,30 @@ 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"), + force: args.force, +}); -main(); - -function main() { - const manifestPath = path.resolve(args.manifest ?? defaultManifestPath); - const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); - - if (args.source === "go") { - const pin = readTypeScriptGoSourcePin(manifest); - const summary = checkoutTypeScriptGoSource({ - manifest, - outDirectory: args.out ?? path.join(repoRoot, ".tmp", "typescript-go"), - force: args.force, - }); - - console.log(`# TypeScript Go Source Checkout +console.log(`# TypeScript Source Checkout - Repository: \`${pin.repository}\` -- Tag: \`${pin.tag}\` +- Ref: \`${pin.ref}\` - Commit: \`${pin.commit}\` -- Strada submodule commit: \`${summary.stradaSubmoduleCommit}\` - Directory: \`${summary.outDirectory}\` - Reused existing checkout: ${summary.reusedExistingCheckout ? "yes" : "no"}`); - return; - } - - const pin = readStradaSourcePin(manifest); - const summary = checkoutStradaSource({ - manifest, - outDirectory: args.out ?? path.join(repoRoot, ".tmp", "TypeScript"), - force: args.force, - }); - - console.log(`# TypeScript Source Checkout - -- Repository: \`${pin.repository}\` -- Tag: \`${pin.tag}\` -- Commit: \`${pin.commit}\` -- Directory: \`${summary.outDirectory}\` -- Reused existing checkout: ${summary.reusedExistingCheckout ? "yes" : "no"}`); -} /** * @param {string[]} argv */ function parseArgs(argv) { - /** @type {{ manifest?: string; out?: string; force: boolean; source: "strada" | "go"; }} */ + /** @type {{ manifest?: string; out?: string; force: boolean; }} */ const args = { force: false, - source: "strada", }; for (let index = 0; index < argv.length; index++) { @@ -77,14 +49,6 @@ function parseArgs(argv) { case "--out": args.out = path.resolve(requireArgValue(argv[++index], current)); break; - case "--source": { - const value = requireArgValue(argv[++index], current); - if (value !== "strada" && value !== "go") { - throw new Error(`--source must be "strada" or "go", got ${value}`); - } - args.source = value; - break; - } case "--force": args.force = true; break; @@ -113,12 +77,11 @@ function requireArgValue(value, flagName) { function printUsageAndExit() { console.log(`Usage: - node scripts/checkout-typescript-source.mjs [--manifest ] [--out ] [--source strada|go] [--force] + node scripts/checkout-typescript-source.mjs [--manifest ] [--out ] [--force] Examples: node scripts/checkout-typescript-source.mjs node scripts/checkout-typescript-source.mjs --out .tmp/TypeScript --force - node scripts/checkout-typescript-source.mjs --source go --out .tmp/typescript-go --force `); process.exit(0); } diff --git a/scripts/prepare-typescript-go-pr.mjs b/scripts/prepare-typescript-go-pr.mjs deleted file mode 100644 index 28f1a42..0000000 --- a/scripts/prepare-typescript-go-pr.mjs +++ /dev/null @@ -1,95 +0,0 @@ -// @ts-check - -import fs from "node:fs"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import { - prepareTypeScriptGoBaselinePatch, - renderTypeScriptGoPatchSummary, -} from "../lib/typescript-go-upstream.mjs"; -import { readTypeScriptGoSourcePin } 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 defaultSummaryPath = path.join(repoRoot, ".tmp", "typescript-go-pr-summary.md"); - -const args = parseArgs(process.argv.slice(2)); - -main(); - -function main() { - if (!args.typescriptGoDir) { - throw new Error("Missing --typescript-go-dir"); - } - const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); - - const summary = prepareTypeScriptGoBaselinePatch({ - repoRoot, - typescriptGoDir: args.typescriptGoDir, - expectedCommit: readTypeScriptGoSourcePin(manifest).commit, - allowUnpinned: args.allowUnpinned, - }); - const summaryText = renderTypeScriptGoPatchSummary(summary); - - const outPath = args.out ?? defaultSummaryPath; - fs.mkdirSync(path.dirname(outPath), { recursive: true }); - fs.writeFileSync(outPath, summaryText); - - console.log(summaryText.trimEnd()); -} - -/** - * @param {string[]} argv - */ -function parseArgs(argv) { - /** @type {{ typescriptGoDir?: string; out?: string; allowUnpinned?: boolean; }} */ - const args = {}; - - for (let index = 0; index < argv.length; index++) { - const current = argv[index]; - switch (current) { - case "--typescript-go-dir": - args.typescriptGoDir = path.resolve(requireArgValue(argv[++index], current)); - break; - case "--out": - args.out = path.resolve(requireArgValue(argv[++index], current)); - break; - case "--allow-unpinned": - args.allowUnpinned = true; - break; - case "--help": - case "-h": - printUsageAndExit(); - break; - default: - throw new Error(`Unknown argument: ${current}`); - } - } - - return args; -} - -/** - * @param {string | undefined} value - * @param {string} flagName - */ -function requireArgValue(value, flagName) { - if (!value) { - throw new Error(`Missing value for ${flagName}`); - } - return value; -} - -function printUsageAndExit() { - console.log(`Usage: - node scripts/prepare-typescript-go-pr.mjs --typescript-go-dir [--out ] [--allow-unpinned] - -Example: - node scripts/checkout-typescript-source.mjs --source go --out .tmp/typescript-go --force - git -C .tmp/typescript-go submodule update --init --depth 1 _submodules/TypeScript - node scripts/prepare-typescript-go-pr.mjs --typescript-go-dir .tmp/typescript-go -`); - process.exit(0); -} diff --git a/scripts/prepare-typescript-pr.mjs b/scripts/prepare-typescript-pr.mjs index 9f6486c..d7d39a1 100644 --- a/scripts/prepare-typescript-pr.mjs +++ b/scripts/prepare-typescript-pr.mjs @@ -7,7 +7,8 @@ import { prepareTypeScriptBaselinePatch, renderTypeScriptPatchSummary, } from "../lib/typescript-upstream.mjs"; -import { readStradaSourcePin } from "../lib/typescript-source.mjs"; +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); @@ -17,13 +18,29 @@ 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 summary = prepareTypeScriptBaselinePatch({ +const expectedCommit = readTypeScriptSourcePin(manifest).commit; +const proposal = await generateTypeScriptProposalLib({ repoRoot, + manifestPath, + manifest, typescriptDir: args.typescriptDir, - expectedCommit: readStradaSourcePin(manifest).commit, + expectedCommit, allowUnpinned: args.allowUnpinned, }); + +let summary; +try { + summary = prepareTypeScriptBaselinePatch({ + repoRoot, + typescriptDir: args.typescriptDir, + generatedLibPath: proposal.outputPath, + expectedCommit, + allowUnpinned: args.allowUnpinned, + }); +} +finally { + proposal.cleanup(); +} const summaryText = renderTypeScriptPatchSummary(summary); if (args.out) { diff --git a/scripts/test-typescript-go-integration.mjs b/scripts/test-typescript-go-integration.mjs deleted file mode 100644 index 65d03b6..0000000 --- a/scripts/test-typescript-go-integration.mjs +++ /dev/null @@ -1,198 +0,0 @@ -// @ts-check - -import assert from "node:assert/strict"; -import { execFileSync, spawnSync } from "node:child_process"; -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import { - renderNegativeProbeSource, - selectActiveNegativeProbes, -} from "../lib/negative-probes.mjs"; -import { - prepareTypeScriptGoBaselinePatch, - renderTypeScriptGoPatchSummary, -} from "../lib/typescript-go-upstream.mjs"; -import { readTypeScriptGoSourcePin } from "../lib/typescript-source.mjs"; - -// Authoritative gate that verifies `tsgo --lib baseline` runs self-consistently -// with the baseline lib built into microsoft/typescript-go (TypeScript 7). -// It needs the Go toolchain, so it stays out of the normal `npm test` and runs in -// a dedicated workflow or a local Go environment. Assumes a pinned go clone + submodule. - -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 generatedLibPath = path.join(repoRoot, "generated", "current", "baseline.d.ts"); -const positiveFixturePath = path.join(repoRoot, "fixtures", "typescript", "smoke", "positive-flag.ts"); - -const args = parseArgs(process.argv.slice(2)); - -main(); - -function main() { - if (!args.typescriptGoDir) { - throw new Error("Missing --typescript-go-dir"); - } - assertGoAvailable(); - - const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); - const typescriptGoDir = path.resolve(args.typescriptGoDir); - - const patchSummary = prepareTypeScriptGoBaselinePatch({ - repoRoot, - typescriptGoDir, - generatedLibPath, - expectedCommit: readTypeScriptGoSourcePin(manifest).commit, - allowUnpinned: args.allowUnpinned, - }); - console.log(renderTypeScriptGoPatchSummary(patchSummary).trimEnd()); - - // Regenerate the generated files (libs_generated.go / embed_generated.go) via the upstream mechanism. - runGo(typescriptGoDir, ["generate", "./internal/bundled/"]); - - const tsgoBinary = path.join(typescriptGoDir, process.platform === "win32" ? "tsgo.exe" : "tsgo"); - runGo(typescriptGoDir, ["build", "-o", tsgoBinary, "./cmd/tsgo"]); - assert.ok(fs.existsSync(tsgoBinary), `Expected built tsgo at ${tsgoBinary}`); - - const smokeRoot = fs.mkdtempSync(path.join(os.tmpdir(), "ts-baseline-go-smoke-")); - try { - runSmokeChecks(tsgoBinary, smokeRoot); - } - finally { - fs.rmSync(smokeRoot, { recursive: true, force: true }); - } - - const summaryText = [ - "# TypeScript Go Integration Summary", - "", - `- typescript-go clone: \`${typescriptGoDir}\``, - `- tsgo binary: \`${tsgoBinary}\``, - "- go generate: ok", - "- go build: ok", - "- Positive `--lib baseline` smoke: passed", - "- Negative `--lib baseline` smoke: passed", - ].join("\n"); - if (args.out) { - fs.mkdirSync(path.dirname(args.out), { recursive: true }); - fs.writeFileSync(args.out, `${summaryText}\n`); - } - console.log(`\n${summaryText}`); -} - -/** - * @param {string} tsgoBinary - * @param {string} smokeRoot - */ -function runSmokeChecks(tsgoBinary, smokeRoot) { - const positiveTargetPath = path.join(smokeRoot, "positive-flag.ts"); - fs.copyFileSync(positiveFixturePath, positiveTargetPath); - - const negativeProbes = loadActiveNegativeProbes(); - const negativeTargetPath = path.join(smokeRoot, "negative-flag.ts"); - fs.writeFileSync(negativeTargetPath, renderNegativeProbeSource(negativeProbes)); - - // Positive: Baseline high APIs pass under --lib baseline. - const positive = runTsgoAllowFailure(tsgoBinary, ["--strict", "--noEmit", "--lib", "baseline", positiveTargetPath]); - assert.ok(positive.ok, `Expected positive baseline smoke to pass:\n${positive.output}`); - - // Negative: currently excluded APIs become type errors. - const negative = runTsgoAllowFailure(tsgoBinary, ["--strict", "--noEmit", "--lib", "baseline", negativeTargetPath]); - assert.equal(negative.ok, false, "Expected negative baseline smoke to fail under tsgo --lib baseline"); - for (const probe of negativeProbes) { - assert.match( - negative.output, - probe.errorPattern, - `Expected excluded probe ${probe.compatKey} to fail under tsgo --lib baseline`, - ); - } -} - -function loadActiveNegativeProbes() { - /** @type {{ classifiedCompatRows: Array<{ compatKey: string; includeInTarget: boolean; }>; }} */ - const classification = JSON.parse( - fs.readFileSync(path.join(repoRoot, "derived", "current", "classification.json"), "utf8"), - ); - return selectActiveNegativeProbes(classification.classifiedCompatRows); -} - -function assertGoAvailable() { - const result = spawnSync("go", ["version"], { encoding: "utf8" }); - if (result.status !== 0) { - throw new Error("`go` toolchain is required for the typescript-go integration gate but was not found on PATH"); - } - console.log(result.stdout.trim()); -} - -/** - * @param {string} cwd - * @param {string[]} goArgs - */ -function runGo(cwd, goArgs) { - execFileSync("go", goArgs, { cwd, stdio: "inherit" }); -} - -/** - * @param {string} tsgoBinary - * @param {string[]} tsgoArgs - */ -function runTsgoAllowFailure(tsgoBinary, tsgoArgs) { - const result = spawnSync(tsgoBinary, tsgoArgs, { encoding: "utf8" }); - return { - ok: result.status === 0, - output: `${result.stdout ?? ""}${result.stderr ?? ""}`, - }; -} - -/** - * @param {string[]} argv - */ -function parseArgs(argv) { - /** @type {{ typescriptGoDir?: string; out?: string; allowUnpinned?: boolean; }} */ - const args = {}; - - for (let index = 0; index < argv.length; index++) { - const current = argv[index]; - switch (current) { - case "--typescript-go-dir": - args.typescriptGoDir = path.resolve(requireArgValue(argv[++index], current)); - break; - case "--out": - args.out = path.resolve(requireArgValue(argv[++index], current)); - break; - case "--allow-unpinned": - args.allowUnpinned = true; - break; - case "--help": - case "-h": - printUsageAndExit(); - break; - default: - throw new Error(`Unknown argument: ${current}`); - } - } - - return args; -} - -/** - * @param {string | undefined} value - * @param {string} flagName - */ -function requireArgValue(value, flagName) { - if (!value) { - throw new Error(`Missing value for ${flagName}`); - } - return value; -} - -function printUsageAndExit() { - console.log(`Usage: - node scripts/test-typescript-go-integration.mjs --typescript-go-dir [--out ] [--allow-unpinned] - -Requires the Go toolchain and a typescript-go checkout with its _submodules/TypeScript submodule initialized. -`); - process.exit(0); -} diff --git a/scripts/test-typescript-integration.mjs b/scripts/test-typescript-integration.mjs index 6af3fe8..9d6fcb8 100644 --- a/scripts/test-typescript-integration.mjs +++ b/scripts/test-typescript-integration.mjs @@ -12,10 +12,13 @@ import { } from "../lib/negative-probes.mjs"; import { findUnexpectedTypeScriptPatchPaths, + hasPassingGoTestEvent, prepareTypeScriptBaselinePatch, + renderTypeScriptPatchDiff, renderTypeScriptPatchSummary, } from "../lib/typescript-upstream.mjs"; -import { readStradaSourcePin } from "../lib/typescript-source.mjs"; +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); @@ -27,15 +30,17 @@ const defaultDiffPath = path.join(repoRoot, ".tmp", "typescript-baseline-changes const defaultFocusedBaselinesDirectory = path.join(repoRoot, ".tmp", "typescript-focused-artifact"); const defaultLocalBaselinesDirectory = path.join(repoRoot, ".tmp", "typescript-raw-local-baselines"); const TYPESCRIPT_PROPOSAL_PATHS = [ - "eslint.config.mjs", - path.join("src", "compiler", "commandLineParser.ts"), - path.join("src", "lib", "baseline.d.ts"), - path.join("src", "lib", "libs.json"), - path.join("tests", "cases", "compiler", "libBaseline.ts"), - path.join("tests", "baselines", "reference", "libBaseline.errors.txt"), - path.join("tests", "baselines", "reference", "libBaseline.js"), - path.join("tests", "baselines", "reference", "libBaseline.symbols"), - path.join("tests", "baselines", "reference", "libBaseline.types"), + path.join("tsc", "internal", "bundled", "source", "baseline.d.ts"), + path.join("tsc", "internal", "bundled", "source", "libs.json"), + path.join("tsc", "internal", "bundled", "libs", "lib.baseline.d.ts"), + path.join("tsc", "internal", "bundled", "libs_generated.go"), + path.join("tsc", "internal", "bundled", "embed_generated.go"), + path.join("tsc", "internal", "tsoptions", "enummaps.go"), + path.join("tsc", "testdata", "tests", "cases", "compiler", "libBaseline.ts"), + path.join("tsc", "testdata", "baselines", "reference", "compiler", "libBaseline.errors.txt"), + path.join("tsc", "testdata", "baselines", "reference", "compiler", "libBaseline.js"), + path.join("tsc", "testdata", "baselines", "reference", "compiler", "libBaseline.symbols"), + path.join("tsc", "testdata", "baselines", "reference", "compiler", "libBaseline.types"), ]; const args = parseArgs(process.argv.slice(2)); @@ -44,13 +49,29 @@ await main(); async function main() { const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); - const patchSummary = prepareTypeScriptBaselinePatch({ + const expectedCommit = readTypeScriptSourcePin(manifest).commit; + const proposal = await generateTypeScriptProposalLib({ repoRoot, + manifestPath, + manifest, typescriptDir: args.typescriptDir, - fixturesRoot, - expectedCommit: readStradaSourcePin(manifest).commit, + expectedCommit, allowUnpinned: args.allowUnpinned, }); + let patchSummary; + try { + patchSummary = prepareTypeScriptBaselinePatch({ + repoRoot, + typescriptDir: args.typescriptDir, + fixturesRoot, + generatedLibPath: proposal.outputPath, + expectedCommit, + allowUnpinned: args.allowUnpinned, + }); + } + finally { + proposal.cleanup(); + } const integrationDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "ts-baseline-ts-integration-")); /** @type {ReturnType | undefined} */ let smokeResults; @@ -75,7 +96,8 @@ async function main() { installTypeScriptDependencies(patchSummary.typescriptDir); } - runNpm(patchSummary.typescriptDir, ["run", "build:compiler"]); + runNpm(patchSummary.typescriptDir, ["run", "generate"]); + runNpm(patchSummary.typescriptDir, ["run", "build"]); smokeResults = runSmokeChecks(patchSummary.typescriptDir, integrationDirectory); // gate: blocking checks only (smoke + targeted harness). Meant to run @@ -83,12 +105,7 @@ async function main() { // full: on top of gate, accepts the generated baselines and requires // the complete TypeScript suite to pass on the accepted state. if (args.mode === "gate" || args.mode === "full") { - runNpm(patchSummary.typescriptDir, ["run", "build:tests"]); - extendedResults.targetedHarness = runCommandAllowFailure( - "npx", - ["hereby", "runtests", "--tests=libBaseline", "--light=false"], - { cwd: patchSummary.typescriptDir }, - ); + extendedResults.targetedHarness = runTargetedHarness(patchSummary.typescriptDir); if (!extendedResults.targetedHarness.ok) { blockingFailure = new Error( extendedResults.targetedHarness.output.trim() || @@ -99,6 +116,18 @@ async function main() { 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)); + extendedResults.baselineDiffPath = args.baselineDiffOut; + } } if (args.mode === "full") { @@ -127,10 +156,7 @@ async function main() { TYPESCRIPT_PROPOSAL_PATHS, ); if (unexpectedPaths.length) { - blockingFailure ??= new Error( - `TypeScript baseline-accept changed files outside the proposal surface:\n` - + unexpectedPaths.map(relativePath => `- ${relativePath}`).join("\n"), - ); + blockingFailure ??= createUnexpectedPathsError("TypeScript baseline-accept", unexpectedPaths); } else { extendedResults.fullSuiteAfterBaselineAccept = runCommandAllowFailure( @@ -149,10 +175,7 @@ async function main() { if (args.baselineDiffOut) { fs.mkdirSync(path.dirname(args.baselineDiffOut), { recursive: true }); const diffText = extendedResults.baselineAccept.ok - ? execFileSync("git", ["diff"], { - cwd: patchSummary.typescriptDir, - encoding: "utf8", - }) + ? renderTypeScriptPatchDiff(patchSummary.typescriptDir) : renderUnavailableDiffArtifact({ baselineAccept: extendedResults.baselineAccept, focusedBaselinesPath: extendedResults.focusedBaselinesPath, @@ -212,20 +235,25 @@ function runSmokeChecks(typescriptDir, integrationDirectory) { const negativeProbes = loadActiveNegativeProbes(); const negativeFlagPath = writeNegativeSmokeFixture(smokeRoot, negativeProbes); const tsconfigPath = writeSmokeTsconfig(smokeRoot); - const localTscPath = path.join(typescriptDir, "built", "local", "tsc.js"); + const localTscPath = path.join( + typescriptDir, + "built", + "local", + process.platform === "win32" ? "tsc.exe" : "tsc", + ); assert.ok(fs.existsSync(localTscPath), `Expected built local tsc at ${localTscPath}`); - runCommand(process.execPath, [localTscPath, "--strict", "--noEmit", "--lib", "baseline", positiveFlagPath], { + runCommand(localTscPath, ["--strict", "--noEmit", "--lib", "baseline", positiveFlagPath], { cwd: typescriptDir, }); - runCommand(process.execPath, [localTscPath, "-p", tsconfigPath], { + runCommand(localTscPath, ["-p", tsconfigPath], { cwd: typescriptDir, }); const negativeResult = runCommandAllowFailure( - process.execPath, - [localTscPath, "--strict", "--noEmit", "--lib", "baseline", negativeFlagPath], + localTscPath, + ["--strict", "--noEmit", "--lib", "baseline", negativeFlagPath], { cwd: typescriptDir }, ); assert.equal(negativeResult.ok, false, "Expected negative baseline smoke to fail"); @@ -355,9 +383,9 @@ function renderIntegrationSummary(options) { 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}\``); - } + } + if (options.extendedResults.baselineDiffPath) { + lines.push(`- Baseline diff artifact: \`${options.extendedResults.baselineDiffPath}\``); } lines.push( @@ -558,6 +586,36 @@ function runNpm(cwd, args) { runCommand("npm", args, { cwd }); } +/** + * @param {string} typescriptDir + */ +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; +} + +/** + * @param {string} phase + * @param {string[]} paths + */ +function createUnexpectedPathsError(phase, paths) { + return new Error( + `${phase} changed files outside the proposal surface:\n` + + paths.map(relativePath => `- ${relativePath}`).join("\n"), + ); +} + /** * @param {string} file * @param {string[]} args @@ -603,7 +661,7 @@ function runCommandAllowFailure(file, args, options) { * }} options */ function copyLocalBaselinesArtifact(options) { - const sourceDirectory = path.join(options.typescriptDir, "tests", "baselines", "local"); + const sourceDirectory = path.join(options.typescriptDir, "tsc", "testdata", "baselines", "local"); if (!fs.existsSync(sourceDirectory)) { return undefined; } diff --git a/scripts/update-typescript-toolchain.mjs b/scripts/update-typescript-toolchain.mjs index b3c27bc..2caf36c 100644 --- a/scripts/update-typescript-toolchain.mjs +++ b/scripts/update-typescript-toolchain.mjs @@ -11,10 +11,7 @@ import { import { npmViewField } from "../lib/installed-package.mjs"; import { retrySync } from "../lib/net-retry.mjs"; import { computeLibDirectoryContentHash } from "../lib/toolchain-libs.mjs"; -import { - applyStradaSourcePin, - applyTypeScriptGoSourcePin, -} from "../lib/typescript-source.mjs"; +import { applyTypeScriptSourcePin } from "../lib/typescript-source.mjs"; import { assertTypeScriptPeerRange } from "../deploy/package-lib.mjs"; import { baselinePackage } from "../deploy/package-registry.mjs"; @@ -24,16 +21,17 @@ const repoRoot = path.resolve(scriptDirectory, ".."); const manifestPath = path.join(repoRoot, "manifests", "baseline-js.json"); const workRoot = path.join(repoRoot, ".tmp", "toolchain-update"); -// TypeScript 7 (tsgo) is the primary toolchain. The lib.*.d.ts files ship in +// TypeScript 7 is the primary toolchain. The lib.*.d.ts files ship in // per-platform packages (@typescript/typescript--), so at pin time we // cross-check tarballs from multiple platforms, record a content hash, and use it // as the basis for the fail-closed comparison at generate time (lib/toolchain-libs.mjs). // // Strada (TypeScript 6) is feature-frozen on the 6.0 line. We keep -// `typescript-strada` (npm alias: typescript@6.x) for three roles: +// `typescript-strada` (npm alias: typescript@6.x) for two roles: // 1. The generator's .d.ts parser / self-check compiler API (TS7's JS API is different) // 2. Compat smoke for TS6-line consumers -// 3. The version anchor for the existing Strada integration gate (hereby + libBaseline) +// 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"]; @@ -80,15 +78,7 @@ async function main() { }; manifest.libSource = libSource; - const stradaSource = applyStradaSourcePin({ - manifest, - stradaVersion, - }); - const goSource = applyTypeScriptGoSourcePin({ - manifest, - typescriptVersion, - workDirectory: workRoot, - }); + const typescriptSource = applyTypeScriptSourcePin({ manifest }); await writeManifest({ manifestPath, @@ -97,11 +87,10 @@ async function main() { console.log([ `Pinned TypeScript toolchain:`, - `- typescript@${typescriptVersion} (tsgo)`, + `- typescript@${typescriptVersion}`, `- typescript-strada@npm:typescript@${stradaVersion}`, `- lib source: ${libSource.libFileCount} files, ${libSource.libContentHash} (verified across ${libSource.referencePlatforms.join(", ")})`, - `- Strada source: ${stradaSource.tag} (${stradaSource.commit})`, - `- typescript-go source: ${goSource.tag} (${goSource.commit}, strada submodule ${goSource.stradaSubmoduleCommit})`, + `- TypeScript source: ${typescriptSource.ref} (${typescriptSource.commit})`, ].join("\n")); } diff --git a/scripts/write-update-pr-body.mjs b/scripts/write-update-pr-body.mjs index 7a6c727..f6c4a5e 100644 --- a/scripts/write-update-pr-body.mjs +++ b/scripts/write-update-pr-body.mjs @@ -239,13 +239,11 @@ export function renderUpdateMarkdown(summary) { `- Baseline date: ${formatTransition(summary.snapshot.previous?.baselineDate, summary.snapshot.current.baselineDate)}`, `- web-features package: ${formatTransition(summary.snapshot.previous?.webFeaturesPackageVersion, summary.snapshot.current.webFeaturesPackageVersion)}`, `- web-features gitHead: ${formatTransition(summary.snapshot.previous?.webFeaturesGitHead, summary.snapshot.current.webFeaturesGitHead)}`, - `- TypeScript package (tsgo): ${formatTransition(summary.snapshot.previous?.typescriptVersion, summary.snapshot.current.typescriptVersion)}`, + `- TypeScript package: ${formatTransition(summary.snapshot.previous?.typescriptVersion, summary.snapshot.current.typescriptVersion)}`, `- TypeScript Strada compat package: ${formatTransition(summary.snapshot.previous?.typescriptStradaVersion, summary.snapshot.current.typescriptStradaVersion)}`, `- Lib source content hash: ${formatTransition(summary.previousManifest?.libSource?.libContentHash, summary.currentManifest.libSource?.libContentHash)}`, - `- Strada source tag: ${formatTransition(summary.previousManifest?.typescriptSource?.tag, summary.currentManifest.typescriptSource?.tag)}`, - `- Strada source commit: ${formatTransition(summary.previousManifest?.typescriptSource?.commit, summary.currentManifest.typescriptSource?.commit)}`, - `- typescript-go source tag: ${formatTransition(summary.previousManifest?.typescriptGoSource?.tag, summary.currentManifest.typescriptGoSource?.tag)}`, - `- typescript-go source commit: ${formatTransition(summary.previousManifest?.typescriptGoSource?.commit, summary.currentManifest.typescriptGoSource?.commit)}`, + `- TypeScript source ref: ${formatTransition(summary.previousManifest?.typescriptSource?.ref, summary.currentManifest.typescriptSource?.ref)}`, + `- TypeScript source commit: ${formatTransition(summary.previousManifest?.typescriptSource?.commit, summary.currentManifest.typescriptSource?.commit)}`, `- Generator version: ${summary.snapshot.current.generatorVersion}`, "", "## Generator Output", diff --git a/test/consumer-smoke.test.mjs b/test/consumer-smoke.test.mjs index aad6407..3150741 100644 --- a/test/consumer-smoke.test.mjs +++ b/test/consumer-smoke.test.mjs @@ -83,7 +83,7 @@ test("staged consumer smoke: stock tsc accepts supported baseline APIs and rejec "", ].join("\n")); - // Consumers will run a mix of TypeScript 6 (Strada) and 7 (tsgo) for now, + // Consumers will run a mix of TypeScript 6 and 7 for now, // so pin that the staged package passes under both toolchains. runTsc(["-p", path.join(consumerDirectory, "tsconfig.json")], { cwd: consumerDirectory }); runTscStrada(["-p", path.join(consumerDirectory, "tsconfig.json")], { cwd: consumerDirectory }); diff --git a/test/helpers.mjs b/test/helpers.mjs index 52e1c89..64d5258 100644 --- a/test/helpers.mjs +++ b/test/helpers.mjs @@ -154,11 +154,11 @@ export function runGenerateExpectFailure(manifestPath, label = manifestPath) { } // Compiler runs resolve the in-package bin explicitly instead of node_modules/.bin. -// typescript (7.x, tsgo) and typescript-strada (6.x alias) both expose a tsc bin +// TypeScript 7 and the TypeScript 6 compatibility alias both expose a tsc bin. // of the same name, and which one the .bin symlink wins is non-deterministic. /** - * Run the tsc from TypeScript 7 (tsgo). The primary toolchain. + * Run the tsc from TypeScript 7. The primary toolchain. * * @param {string[]} args * @param {{ allowFailure?: boolean; cwd?: string; }} [options] diff --git a/test/packed-consumer-smoke.test.mjs b/test/packed-consumer-smoke.test.mjs index 4e2407f..f56a113 100644 --- a/test/packed-consumer-smoke.test.mjs +++ b/test/packed-consumer-smoke.test.mjs @@ -79,8 +79,7 @@ test("packed consumer smoke: npm-packed baseline package typechecks through comp "", ].join("\n")); - // Pin that the npm-packed artifact reads under both TypeScript 7 (tsgo) - // and Strada (the 6.x series). + // Pin that the npm-packed artifact reads under both TypeScript 7 and 6. runTsc(["-p", path.join(consumerDirectory, "tsconfig.json")], { cwd: consumerDirectory }); runTscStrada(["-p", path.join(consumerDirectory, "tsconfig.json")], { cwd: consumerDirectory }); diff --git a/test/toolchain-smoke.test.mjs b/test/toolchain-smoke.test.mjs index 6125e25..5cad172 100644 --- a/test/toolchain-smoke.test.mjs +++ b/test/toolchain-smoke.test.mjs @@ -19,11 +19,11 @@ test.afterEach(() => { cleanupTempDirectories(tempDirectories); }); -// Pin that the generated lib alone reads under both the primary toolchain (TypeScript 7 / tsgo) +// Pin that the generated lib alone reads under both the primary toolchain (TypeScript 7) // and the compat toolchain (the final Strada series). Consumers will run a mix of 6.x and 7.x // for now, so we must not ship a generated artifact that only one of them can read. -test("toolchain smoke: baseline declarations typecheck under TypeScript 7 (tsgo)", () => { +test("toolchain smoke: baseline declarations typecheck under TypeScript 7", () => { const tempDirectory = createTempDirectory(tempDirectories); const fixture = createManifest(tempDirectory); runGenerate(fixture.manifestPath); diff --git a/test/typescript-go-upstream.test.mjs b/test/typescript-go-upstream.test.mjs deleted file mode 100644 index 76b9126..0000000 --- a/test/typescript-go-upstream.test.mjs +++ /dev/null @@ -1,150 +0,0 @@ -// @ts-check - -import assert from "node:assert/strict"; -import { execFileSync } from "node:child_process"; -import fs from "node:fs"; -import path from "node:path"; -import test from "node:test"; -import { - cleanupTempDirectories, - createTempDirectory, -} from "./helpers.mjs"; -import { - enumMapsRelativePath, - prepareTypeScriptGoBaselinePatch, - typescriptGoSubmoduleLibDir, -} from "../lib/typescript-go-upstream.mjs"; - -/** @type {string[]} */ -const tempDirectories = []; - -test.afterEach(() => { - cleanupTempDirectories(tempDirectories); -}); - -// generate.go regenerates lib from two inputs: the submodule's src/lib/libs.json and -// the LibMap in enummaps.go. The patcher deterministically touches only those two inputs -// plus the baseline.d.ts install (never the generated files). Pin at the text level without a Go build. - -test("prepareTypeScriptGoBaselinePatch installs baseline source and patches libs.json + enummaps once", () => { - const tempDirectory = createTempDirectory(tempDirectories); - const typescriptGoDir = createFakeTypeScriptGoTree(tempDirectory); - const generatedLibPath = path.join(tempDirectory, "generated", "baseline.d.ts"); - fs.mkdirSync(path.dirname(generatedLibPath), { recursive: true }); - fs.writeFileSync(generatedLibPath, "// generated baseline\n"); - - const first = prepareTypeScriptGoBaselinePatch({ - repoRoot: tempDirectory, - typescriptGoDir, - generatedLibPath, - }); - - const libSourcePath = path.join(typescriptGoDir, typescriptGoSubmoduleLibDir, "baseline.d.ts"); - const libsJsonPath = path.join(typescriptGoDir, typescriptGoSubmoduleLibDir, "libs.json"); - const enumMapsPath = path.join(typescriptGoDir, enumMapsRelativePath); - - assert.equal(fs.readFileSync(libSourcePath, "utf8"), "// generated baseline\n"); - - const libsJson = fs.readFileSync(libsJsonPath, "utf8"); - assert.match(libsJson, /"baseline"/); - assert.equal([...libsJson.matchAll(/"baseline"/g)].length, 1); - // baseline lands right after esnext (order is pinned too). - assert.ok(libsJson.indexOf('"esnext"') < libsJson.indexOf('"baseline"')); - - const enumMaps = fs.readFileSync(enumMapsPath, "utf8"); - assert.match(enumMaps, /\{Key: "baseline", Value: "lib\.baseline\.d\.ts"\},/); - assert.equal([...enumMaps.matchAll(/Value: "lib\.baseline\.d\.ts"/g)].length, 1); - - assert.equal(first.copiedGeneratedLib.changed, true); - assert.equal(first.patchedLibsJson.changed, true); - assert.equal(first.patchedEnumMaps.changed, true); - - // Idempotent: the second run has zero diff. - const second = prepareTypeScriptGoBaselinePatch({ - repoRoot: tempDirectory, - typescriptGoDir, - generatedLibPath, - }); - assert.equal(second.copiedGeneratedLib.changed, false); - assert.equal(second.patchedLibsJson.changed, false); - assert.equal(second.patchedEnumMaps.changed, false); - assert.equal([...fs.readFileSync(enumMapsPath, "utf8").matchAll(/Value: "lib\.baseline\.d\.ts"/g)].length, 1); -}); - -test("prepareTypeScriptGoBaselinePatch fails closed when the LibMap anchor is missing", () => { - const tempDirectory = createTempDirectory(tempDirectories); - const typescriptGoDir = createFakeTypeScriptGoTree(tempDirectory); - const generatedLibPath = path.join(tempDirectory, "generated", "baseline.d.ts"); - fs.mkdirSync(path.dirname(generatedLibPath), { recursive: true }); - fs.writeFileSync(generatedLibPath, "// generated baseline\n"); - - // Reproduce upstream reformatting the esnext line. - const enumMapsPath = path.join(typescriptGoDir, enumMapsRelativePath); - fs.writeFileSync(enumMapsPath, "var LibMap = ...\n\t{Key: \"es2025\", Value: \"lib.es2025.d.ts\"},\n"); - - assert.throws( - () => prepareTypeScriptGoBaselinePatch({ repoRoot: tempDirectory, typescriptGoDir, generatedLibPath }), - /enummaps\.go LibMap entry anchor/u, - ); -}); - -test("prepareTypeScriptGoBaselinePatch refuses to patch a clone that drifted from the pin", () => { - const tempDirectory = createTempDirectory(tempDirectories); - const typescriptGoDir = createFakeTypeScriptGoTree(tempDirectory); - const generatedLibPath = path.join(tempDirectory, "generated", "baseline.d.ts"); - fs.mkdirSync(path.dirname(generatedLibPath), { recursive: true }); - fs.writeFileSync(generatedLibPath, "// generated baseline\n"); - - execFileSync("git", ["init", "--quiet"], { cwd: typescriptGoDir }); - execFileSync("git", ["-c", "user.email=t@example.com", "-c", "user.name=t", "add", "."], { cwd: typescriptGoDir }); - execFileSync("git", ["-c", "user.email=t@example.com", "-c", "user.name=t", "commit", "--quiet", "-m", "fixture"], { cwd: typescriptGoDir }); - - assert.throws( - () => prepareTypeScriptGoBaselinePatch({ - repoRoot: tempDirectory, - typescriptGoDir, - generatedLibPath, - expectedCommit: "0000000000000000000000000000000000000000", - }), - /Refusing to patch an unpinned clone/u, - ); - assert.ok(!fs.existsSync(path.join(typescriptGoDir, typescriptGoSubmoduleLibDir, "baseline.d.ts"))); -}); - -/** - * @param {string} tempDirectory - */ -function createFakeTypeScriptGoTree(tempDirectory) { - const typescriptGoDir = path.join(tempDirectory, "typescript-go"); - const submoduleLibDir = path.join(typescriptGoDir, typescriptGoSubmoduleLibDir); - const enumMapsDir = path.dirname(path.join(typescriptGoDir, enumMapsRelativePath)); - - fs.mkdirSync(submoduleLibDir, { recursive: true }); - fs.mkdirSync(enumMapsDir, { recursive: true }); - - fs.writeFileSync( - path.join(submoduleLibDir, "libs.json"), - `{ - "libs": [ - "es2025", - "esnext", - "dom" - ] -} -`, - ); - fs.writeFileSync( - path.join(typescriptGoDir, enumMapsRelativePath), - `package tsoptions - -var LibMap = collections.NewOrderedMapFromList([]collections.MapEntry[string, any]{ - {Key: "es2025", Value: "lib.es2025.d.ts"}, - {Key: "esnext", Value: "lib.esnext.d.ts"}, - // Host only - {Key: "dom", Value: "lib.dom.d.ts"}, -}) -`, - ); - - return typescriptGoDir; -} diff --git a/test/typescript-proposal.test.mjs b/test/typescript-proposal.test.mjs new file mode 100644 index 0000000..6bd228a --- /dev/null +++ b/test/typescript-proposal.test.mjs @@ -0,0 +1,140 @@ +// @ts-check + +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import fs from "node:fs"; +import path from "node:path"; +import test from "node:test"; +import { generateTypeScriptProposalLib } from "../lib/typescript-proposal.mjs"; +import { typescriptGeneratedLibDirectory } from "../lib/typescript-upstream.mjs"; +import { verifyLibSource } from "../lib/toolchain-libs.mjs"; +import { + cleanupTempDirectories, + createTempDirectory, + repoGeneratedLibPath, + repoManifest, + repoManifestPath, + repoRoot, +} from "./helpers.mjs"; + +/** @type {string[]} */ +const tempDirectories = []; + +test.afterEach(() => { + cleanupTempDirectories(tempDirectories); +}); + +test("proposal generation follows the pinned TypeScript checkout declaration corpus", async () => { + const tempDirectory = createTempDirectory(tempDirectories); + const typescriptDir = path.join(tempDirectory, "TypeScript"); + const targetLibDirectory = path.join(typescriptDir, typescriptGeneratedLibDirectory); + const proposalRepoRoot = path.join(tempDirectory, "repo"); + const externalDirectory = path.join(tempDirectory, "external"); + const sentinelPath = path.join(externalDirectory, "libs", "sentinel.txt"); + const packageLibSource = await verifyLibSource({ repoRoot, manifest: repoManifest }); + + fs.cpSync(packageLibSource.libDirectory, targetLibDirectory, { recursive: true }); + removePluralRulesCallSignature(path.join(targetLibDirectory, "lib.es2018.intl.d.ts")); + removePluralRulesCallSignature(path.join(targetLibDirectory, "lib.es2020.intl.d.ts")); + fs.mkdirSync(path.dirname(sentinelPath), { recursive: true }); + fs.writeFileSync(sentinelPath, "keep\n"); + fs.mkdirSync(path.join(proposalRepoRoot, ".tmp"), { recursive: true }); + fs.symlinkSync(externalDirectory, path.join(proposalRepoRoot, ".tmp", "typescript-proposal")); + + const proposals = await Promise.all([ + generateTypeScriptProposalLib({ repoRoot: proposalRepoRoot, manifestPath: repoManifestPath, manifest: repoManifest, typescriptDir }), + generateTypeScriptProposalLib({ repoRoot: proposalRepoRoot, manifestPath: repoManifestPath, manifest: repoManifest, typescriptDir }), + ]); + try { + const packageOutput = fs.readFileSync(repoGeneratedLibPath, "utf8"); + const proposalOutputs = proposals.map(proposal => fs.readFileSync(proposal.outputPath, "utf8")); + + assert.match(packageOutput, /\(locales\?: LocalesArgument, options\?: PluralRulesOptions\): PluralRules;/u); + assert.equal(proposalOutputs[0], proposalOutputs[1]); + assert.doesNotMatch(proposalOutputs[0], /\n\s*\(locales\?: [^\n]+PluralRulesOptions\): PluralRules;/u); + assert.match(proposalOutputs[0], /new \(locales\?: LocalesArgument, options\?: PluralRulesOptions\): PluralRules;/u); + assert.equal(fs.readFileSync(sentinelPath, "utf8"), "keep\n"); + } + finally { + for (const proposal of proposals) { + proposal.cleanup(); + } + } +}); + +test("pinned proposal generation reads commit blobs instead of hidden working-tree changes", async () => { + const tempDirectory = createTempDirectory(tempDirectories); + const typescriptDir = path.join(tempDirectory, "TypeScript"); + const targetLibDirectory = path.join(typescriptDir, typescriptGeneratedLibDirectory); + const packageLibSource = await verifyLibSource({ repoRoot, manifest: repoManifest }); + + fs.cpSync(packageLibSource.libDirectory, targetLibDirectory, { recursive: true }); + execFileSync("git", ["init", "--quiet", "--initial-branch=main"], { cwd: typescriptDir }); + execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: typescriptDir }); + execFileSync("git", ["config", "user.name", "Test"], { cwd: typescriptDir }); + execFileSync("git", ["add", "."], { cwd: typescriptDir }); + execFileSync("git", ["commit", "--quiet", "-m", "fixture"], { cwd: typescriptDir }); + const expectedCommit = execFileSync("git", ["rev-parse", "HEAD"], { + cwd: typescriptDir, + encoding: "utf8", + }).trim(); + const trackedRelativePath = "tsc/internal/bundled/libs/lib.es2018.intl.d.ts"; + const trackedPath = path.join(typescriptDir, trackedRelativePath); + fs.writeFileSync( + trackedPath, + fs.readFileSync(trackedPath, "utf8").replace( + "select(n: number): LDMLPluralRule;", + 'select(n: "REPLACEMENT"): LDMLPluralRule;', + ), + ); + execFileSync("git", ["add", trackedRelativePath], { cwd: typescriptDir }); + execFileSync("git", ["commit", "--quiet", "-m", "replacement"], { cwd: typescriptDir }); + const replacementCommit = execFileSync("git", ["rev-parse", "HEAD"], { + cwd: typescriptDir, + encoding: "utf8", + }).trim(); + execFileSync("git", ["reset", "--hard", expectedCommit], { cwd: typescriptDir }); + execFileSync("git", ["replace", expectedCommit, replacementCommit], { cwd: typescriptDir }); + fs.writeFileSync( + trackedPath, + fs.readFileSync(trackedPath, "utf8").replace( + "select(n: number): LDMLPluralRule;", + 'select(n: "LOCAL"): LDMLPluralRule;', + ), + ); + execFileSync("git", ["update-index", "--assume-unchanged", trackedRelativePath], { cwd: typescriptDir }); + fs.writeFileSync( + path.join(typescriptDir, ".git", "info", "exclude"), + "tsc/internal/bundled/libs/lib.injected.d.ts\n", + ); + fs.writeFileSync( + path.join(targetLibDirectory, "lib.injected.d.ts"), + "interface Array { injected(): void; }\n", + ); + + const proposal = await generateTypeScriptProposalLib({ + repoRoot, + manifestPath: repoManifestPath, + manifest: repoManifest, + typescriptDir, + expectedCommit, + }); + try { + const output = fs.readFileSync(proposal.outputPath, "utf8"); + assert.doesNotMatch(output, /"LOCAL"|"REPLACEMENT"|injected\(\): void/u); + assert.match(output, /select\(n: number\): LDMLPluralRule;/u); + } + finally { + proposal.cleanup(); + } +}); + +/** + * @param {string} filePath + */ +function removePluralRulesCallSignature(filePath) { + const source = fs.readFileSync(filePath, "utf8"); + const updated = source.replace(/^\s*\(locales\?: .*PluralRulesOptions\): PluralRules;\r?\n/gmu, ""); + assert.notEqual(updated, source, `expected a PluralRules call signature in ${filePath}`); + fs.writeFileSync(filePath, updated); +} diff --git a/test/typescript-source.test.mjs b/test/typescript-source.test.mjs index 494af4b..ca03937 100644 --- a/test/typescript-source.test.mjs +++ b/test/typescript-source.test.mjs @@ -1,33 +1,77 @@ // @ts-check import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import fs from "node:fs"; +import path from "node:path"; import test from "node:test"; -import { repoManifest } from "./helpers.mjs"; import { - readStradaSourcePin, - readTypeScriptGoSourcePin, - resolveStradaSourceTag, - resolveTypeScriptGoSourceTag, + applyTypeScriptSourcePin, + checkoutTypeScriptSource, + readTypeScriptSourcePin, } from "../lib/typescript-source.mjs"; +import { + cleanupTempDirectories, + createTempDirectory, + repoManifest, +} from "./helpers.mjs"; + +/** @type {string[]} */ +const tempDirectories = []; -// Pin the exhaustive consistency of the pins (tag <-> snapshot version, commit -// format) offline. That the commit matches the tag's actual object is verified at checkout. +test.afterEach(() => { + cleanupTempDirectories(tempDirectories); +}); -test("repo manifest pins a frozen Strada source tag that matches the strada package version", () => { - const pin = readStradaSourcePin(repoManifest); +test("repo manifest pins the unified TypeScript source", () => { + const pin = readTypeScriptSourcePin(repoManifest); assert.equal(pin.repository, "https://github.com/microsoft/TypeScript.git"); - assert.equal(pin.tag, resolveStradaSourceTag(repoManifest.snapshot.typescriptStradaVersion)); + assert.equal(pin.ref, "main"); assert.match(pin.commit, /^[0-9a-f]{40}$/u); }); -test("repo manifest pins a typescript-go source tag that matches the typescript package version", () => { - const pin = readTypeScriptGoSourcePin(repoManifest); +test("source pinning and checkout remain exact after the tracked branch advances", () => { + const tempDirectory = createTempDirectory(tempDirectories); + const sourceDirectory = path.join(tempDirectory, "source"); + const checkoutDirectory = path.join(tempDirectory, "checkout"); - assert.equal(pin.repository, "https://github.com/microsoft/typescript-go.git"); - assert.equal(pin.tag, resolveTypeScriptGoSourceTag(repoManifest.snapshot.typescriptVersion)); - assert.match(pin.commit, /^[0-9a-f]{40}$/u); - assert.match(pin.stradaSubmoduleCommit, /^[0-9a-f]{40}$/u); + initializeRepository(sourceDirectory); + fs.writeFileSync(path.join(sourceDirectory, "value.txt"), "first\n"); + 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"); + commitAll(sourceDirectory, "second"); + + const result = checkoutTypeScriptSource({ + 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"); + + fs.writeFileSync(path.join(checkoutDirectory, "local.txt"), "dirty\n"); + assert.throws( + () => checkoutTypeScriptSource({ manifest, outDirectory: checkoutDirectory }), + /checkout has local changes/u, + ); + + const refreshed = checkoutTypeScriptSource({ + manifest, + 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"); }); test("repo manifest pins a cross-platform verified lib source", () => { @@ -39,3 +83,22 @@ test("repo manifest pins a cross-platform verified lib source", () => { assert.match(libSource.libContentHash, /^sha256-[0-9a-f]{64}$/u); assert.ok(Number.isInteger(libSource.libFileCount) && libSource.libFileCount > 0); }); + +/** + * @param {string} directory + */ +function initializeRepository(directory) { + fs.mkdirSync(directory, { recursive: true }); + execFileSync("git", ["init", "--quiet", "--initial-branch=main"], { cwd: directory }); + execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: directory }); + execFileSync("git", ["config", "user.name", "Test"], { cwd: directory }); +} + +/** + * @param {string} directory + * @param {string} message + */ +function commitAll(directory, message) { + execFileSync("git", ["add", "."], { cwd: directory }); + execFileSync("git", ["commit", "--quiet", "-m", message], { cwd: directory }); +} diff --git a/test/typescript-upstream.test.mjs b/test/typescript-upstream.test.mjs index 03e87f2..a28d5ca 100644 --- a/test/typescript-upstream.test.mjs +++ b/test/typescript-upstream.test.mjs @@ -6,13 +6,18 @@ import fs from "node:fs"; import path from "node:path"; import test from "node:test"; import { - cleanupTempDirectories, - createTempDirectory, -} from "./helpers.mjs"; -import { + enumMapsRelativePath, findUnexpectedTypeScriptPatchPaths, + hasPassingGoTestEvent, prepareTypeScriptBaselinePatch, + renderTypeScriptPatchDiff, + renderTypeScriptPatchSummary, + typescriptLibSourceDirectory, } from "../lib/typescript-upstream.mjs"; +import { + cleanupTempDirectories, + createTempDirectory, +} from "./helpers.mjs"; /** @type {string[]} */ const tempDirectories = []; @@ -21,202 +26,255 @@ test.afterEach(() => { cleanupTempDirectories(tempDirectories); }); -test("prepareTypeScriptBaselinePatch installs baseline.d.ts source and patches TypeScript once", () => { +test("prepareTypeScriptBaselinePatch patches the unified TypeScript tree once", () => { const tempDirectory = createTempDirectory(tempDirectories); const typescriptDir = createFakeTypeScriptTree(tempDirectory); const generatedLibPath = path.join(tempDirectory, "generated", "baseline.d.ts"); - const fixturesRoot = path.join(tempDirectory, "fixtures"); + const fixturesRoot = createFixtureTree(tempDirectory); fs.mkdirSync(path.dirname(generatedLibPath), { recursive: true }); fs.writeFileSync(generatedLibPath, "// generated baseline\n"); - fs.mkdirSync(path.join(fixturesRoot, "tests", "cases", "compiler"), { recursive: true }); - fs.mkdirSync(path.join(fixturesRoot, "tests", "baselines", "reference"), { recursive: true }); - fs.writeFileSync( - path.join(fixturesRoot, "tests", "cases", "compiler", "libBaseline.ts"), - "// @lib: baseline\nObject.hasOwn({}, 'x');\n", - ); - fs.writeFileSync( - path.join(fixturesRoot, "tests", "baselines", "reference", "libBaseline.errors.txt"), - "fixture baseline\n", - ); - - const firstSummary = prepareTypeScriptBaselinePatch({ + const first = prepareTypeScriptBaselinePatch({ repoRoot: tempDirectory, typescriptDir, generatedLibPath, fixturesRoot, }); - const targetLibPath = path.join(typescriptDir, "src", "lib", "baseline.d.ts"); - const commandLineParserPath = path.join(typescriptDir, "src", "compiler", "commandLineParser.ts"); - const libsJsonPath = path.join(typescriptDir, "src", "lib", "libs.json"); - const eslintConfigPath = path.join(typescriptDir, "eslint.config.mjs"); + const libSourcePath = path.join(typescriptDir, typescriptLibSourceDirectory, "baseline.d.ts"); + const libsJsonPath = path.join(typescriptDir, typescriptLibSourceDirectory, "libs.json"); + const enumMapsPath = path.join(typescriptDir, enumMapsRelativePath); + const testCasePath = path.join(typescriptDir, "tsc", "testdata", "tests", "cases", "compiler", "libBaseline.ts"); + const baselinePath = path.join(typescriptDir, "tsc", "testdata", "baselines", "reference", "compiler", "libBaseline.errors.txt"); - assert.equal(fs.readFileSync(targetLibPath, "utf8"), "// generated baseline\n"); - assert.match(fs.readFileSync(commandLineParserPath, "utf8"), /\["baseline", "lib\.baseline\.d\.ts"\],/); - assert.equal( - [...fs.readFileSync(commandLineParserPath, "utf8").matchAll(/\["baseline", "lib\.baseline\.d\.ts"\],/g)].length, - 1, - ); - assert.match(fs.readFileSync(libsJsonPath, "utf8"), /"baseline"/); - assert.equal( - [...fs.readFileSync(libsJsonPath, "utf8").matchAll(/"baseline"/g)].length, - 1, - ); - assert.match( - fs.readFileSync(eslintConfigPath, "utf8"), - /files: \["src\/lib\/es2019\.array\.d\.ts", "src\/lib\/baseline\.d\.ts"\]/, - ); - assert.ok(fs.existsSync(path.join(typescriptDir, "tests", "cases", "compiler", "libBaseline.ts"))); - assert.ok(fs.existsSync(path.join(typescriptDir, "tests", "baselines", "reference", "libBaseline.errors.txt"))); - assert.equal(firstSummary.fixtureFiles.length, 2); - // First run actually writes both files. - assert.equal(firstSummary.changedFixtureFiles.length, 2); + assert.equal(fs.readFileSync(libSourcePath, "utf8"), "// generated baseline\n"); + assert.equal([...fs.readFileSync(libsJsonPath, "utf8").matchAll(/"baseline"/g)].length, 1); + assert.equal([...fs.readFileSync(enumMapsPath, "utf8").matchAll(/Value: "lib\.baseline\.d\.ts"/g)].length, 1); + assert.ok(fs.existsSync(testCasePath)); + assert.ok(fs.existsSync(baselinePath)); + assert.equal(first.copiedGeneratedLib.changed, true); + assert.equal(first.patchedLibsJson.changed, true); + assert.equal(first.patchedEnumMaps.changed, true); + assert.equal(first.fixtureFiles.length, 2); + assert.equal(first.changedFixtureFiles.length, 2); + assert.match(renderTypeScriptPatchSummary(first), /npm ci && npm run generate/u); - const secondSummary = prepareTypeScriptBaselinePatch({ + const second = prepareTypeScriptBaselinePatch({ repoRoot: tempDirectory, typescriptDir, generatedLibPath, fixturesRoot, }); + assert.equal(second.copiedGeneratedLib.changed, false); + assert.equal(second.patchedLibsJson.changed, false); + assert.equal(second.patchedEnumMaps.changed, false); + assert.equal(second.fixtureFiles.length, 2); + assert.equal(second.changedFixtureFiles.length, 0); +}); + +test("prepareTypeScriptBaselinePatch uses the checkout command's default directory", () => { + const tempDirectory = createTempDirectory(tempDirectories); + const sourceDirectory = createFakeTypeScriptTree(tempDirectory); + const typescriptDir = path.join(tempDirectory, ".tmp", "TypeScript"); + const generatedLibPath = path.join(tempDirectory, "generated", "baseline.d.ts"); + const fixturesRoot = createFixtureTree(tempDirectory); - assert.equal(secondSummary.copiedGeneratedLib.changed, false); - assert.equal(secondSummary.patchedCommandLineParser.changed, false); - assert.equal(secondSummary.patchedLibsJson.changed, false); - assert.equal(secondSummary.patchedEslintConfig.changed, false); - // Second run reports the same fixture list, but zero files changed - // (summary must not misreport unchanged files as "copied"). - assert.equal(secondSummary.fixtureFiles.length, 2); - assert.equal(secondSummary.changedFixtureFiles.length, 0); + fs.mkdirSync(path.dirname(typescriptDir), { recursive: true }); + fs.renameSync(sourceDirectory, typescriptDir); + fs.mkdirSync(path.dirname(generatedLibPath), { recursive: true }); + fs.writeFileSync(generatedLibPath, "// generated baseline\n"); + + const summary = prepareTypeScriptBaselinePatch({ + repoRoot: tempDirectory, + generatedLibPath, + fixturesRoot, + }); + assert.equal(summary.typescriptDir, typescriptDir); }); -/** - * @param {string} tempDirectory - */ -function createFakeTypeScriptTree(tempDirectory) { - const typescriptDir = path.join(tempDirectory, "TypeScript"); - const compilerDirectory = path.join(typescriptDir, "src", "compiler"); - const libDirectory = path.join(typescriptDir, "src", "lib"); +test("prepareTypeScriptBaselinePatch fails closed when the LibMap anchor drifts", () => { + const tempDirectory = createTempDirectory(tempDirectories); + const typescriptDir = createFakeTypeScriptTree(tempDirectory); + const generatedLibPath = path.join(tempDirectory, "generated", "baseline.d.ts"); + const fixturesRoot = createFixtureTree(tempDirectory); + const enumMapsPath = path.join(typescriptDir, enumMapsRelativePath); - fs.mkdirSync(compilerDirectory, { recursive: true }); - fs.mkdirSync(libDirectory, { recursive: true }); + fs.mkdirSync(path.dirname(generatedLibPath), { recursive: true }); + fs.writeFileSync(generatedLibPath, "// generated baseline\n"); + fs.writeFileSync(enumMapsPath, "var LibMap = ...\n\t{Key: \"es2025\", Value: \"lib.es2025.d.ts\"},\n"); - fs.writeFileSync( - path.join(compilerDirectory, "commandLineParser.ts"), - `const libEntries: [string, string][] = [ - ["es2025", "lib.es2025.d.ts"], - ["esnext", "lib.esnext.d.ts"], - // Host only - ["dom", "lib.dom.d.ts"], -]; -`, - ); - fs.writeFileSync( - path.join(libDirectory, "libs.json"), - `{ - "libs": [ - "es2025", - "esnext", - // Host only - "dom" - ] -} -`, - ); - fs.writeFileSync( - path.join(typescriptDir, "eslint.config.mjs"), - `export default [{ - files: ["src/lib/es2019.array.d.ts"], - rules: { "@typescript-eslint/array-type": "off" }, -}]; -`, + assert.throws( + () => prepareTypeScriptBaselinePatch({ repoRoot: tempDirectory, typescriptDir, generatedLibPath, fixturesRoot }), + /enummaps\.go LibMap entry anchor/u, ); +}); - return typescriptDir; -} - -test("prepareTypeScriptBaselinePatch refuses to patch a clone that drifted from the pin", () => { +test("prepareTypeScriptBaselinePatch preserves upstream line endings", () => { const tempDirectory = createTempDirectory(tempDirectories); const typescriptDir = createFakeTypeScriptTree(tempDirectory); const generatedLibPath = path.join(tempDirectory, "generated", "baseline.d.ts"); - const fixturesRoot = path.join(tempDirectory, "fixtures"); + const fixturesRoot = createFixtureTree(tempDirectory); + const libsJsonPath = path.join(typescriptDir, typescriptLibSourceDirectory, "libs.json"); fs.mkdirSync(path.dirname(generatedLibPath), { recursive: true }); fs.writeFileSync(generatedLibPath, "// generated baseline\n"); - fs.mkdirSync(path.join(fixturesRoot, "tests", "cases", "compiler"), { recursive: true }); + fs.writeFileSync(libsJsonPath, fs.readFileSync(libsJsonPath, "utf8").replace(/\n/gu, "\r\n")); + + prepareTypeScriptBaselinePatch({ repoRoot: tempDirectory, typescriptDir, generatedLibPath, fixturesRoot }); + assert.doesNotMatch(fs.readFileSync(libsJsonPath, "utf8"), /(? { + const tempDirectory = createTempDirectory(tempDirectories); + const typescriptDir = createFakeTypeScriptTree(tempDirectory); + const generatedLibPath = path.join(tempDirectory, "generated", "baseline.d.ts"); + const fixturesRoot = createFixtureTree(tempDirectory); + + fs.mkdirSync(path.dirname(generatedLibPath), { recursive: true }); + fs.writeFileSync(generatedLibPath, "// generated baseline\n"); execFileSync("git", ["init", "--quiet"], { cwd: typescriptDir }); execFileSync("git", ["-c", "user.email=t@example.com", "-c", "user.name=t", "add", "."], { cwd: typescriptDir }); execFileSync("git", ["-c", "user.email=t@example.com", "-c", "user.name=t", "commit", "--quiet", "-m", "fixture"], { cwd: typescriptDir }); - const pinnedCommit = "0000000000000000000000000000000000000000"; assert.throws( () => prepareTypeScriptBaselinePatch({ repoRoot: tempDirectory, typescriptDir, generatedLibPath, fixturesRoot, - expectedCommit: pinnedCommit, + expectedCommit: "0000000000000000000000000000000000000000", }), /Refusing to patch an unpinned clone/u, ); - - // Nothing was written (validation runs before patching). - assert.ok(!fs.existsSync(path.join(typescriptDir, "src", "lib", "baseline.d.ts"))); - - // allowUnpinned is an explicit opt-out that lets the patch through. - const summary = prepareTypeScriptBaselinePatch({ - repoRoot: tempDirectory, - typescriptDir, - generatedLibPath, - fixturesRoot, - expectedCommit: pinnedCommit, - allowUnpinned: true, - }); - assert.equal(summary.copiedGeneratedLib.changed, true); + assert.ok(!fs.existsSync(path.join(typescriptDir, typescriptLibSourceDirectory, "baseline.d.ts"))); }); test("TypeScript patch auditing rejects changes outside the proposal surface", () => { const tempDirectory = createTempDirectory(tempDirectories); - execFileSync("git", ["init", "--quiet"], { cwd: tempDirectory }); - execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: tempDirectory }); - execFileSync("git", ["config", "user.name", "Test"], { cwd: tempDirectory }); - fs.writeFileSync(path.join(tempDirectory, "allowed.txt"), "before\n"); - execFileSync("git", ["add", "allowed.txt"], { cwd: tempDirectory }); - execFileSync("git", ["commit", "--quiet", "-m", "fixture"], { cwd: tempDirectory }); + initializeGitFixture(tempDirectory, { "allowed.txt": "before\n" }); fs.writeFileSync(path.join(tempDirectory, "allowed.txt"), "after\n"); fs.writeFileSync(path.join(tempDirectory, "unexpected.txt"), "unexpected\n"); - assert.deepEqual( - findUnexpectedTypeScriptPatchPaths(tempDirectory, ["allowed.txt"]), - ["unexpected.txt"], - ); + assert.deepEqual(findUnexpectedTypeScriptPatchPaths(tempDirectory, ["allowed.txt"]), ["unexpected.txt"]); +}); + +test("TypeScript patch diff includes tracked and untracked proposal files without staging them", () => { + const tempDirectory = createTempDirectory(tempDirectories); + initializeGitFixture(tempDirectory, { "tracked.txt": "before\n" }); + + fs.writeFileSync(path.join(tempDirectory, "tracked.txt"), "after\n"); + fs.writeFileSync(path.join(tempDirectory, "new.txt"), "new\n"); + const diff = renderTypeScriptPatchDiff(tempDirectory); + + assert.match(diff, /-before/u); + assert.match(diff, /\+after/u); + assert.match(diff, /diff --git .*new\.txt/u); + assert.match(diff, /\+new/u); + assert.equal(execFileSync("git", ["diff", "--cached", "--name-only"], { + cwd: tempDirectory, + encoding: "utf8", + }), ""); +}); + +test("Go harness output must contain the exact passing subtest", () => { + const packagePass = '{"Action":"pass","Package":"example.test/internal/testrunner"}'; + const targetPass = '{"Action":"pass","Test":"TestLocal/libBaseline.ts"}'; + const otherPass = '{"Action":"pass","Test":"TestLocal/other.ts"}'; + + assert.equal(hasPassingGoTestEvent(`${packagePass}\n${targetPass}\n`, "TestLocal/libBaseline.ts"), true); + assert.equal(hasPassingGoTestEvent(`${packagePass}\n${otherPass}\n`, "TestLocal/libBaseline.ts"), false); }); test("TypeScript patch auditing accepts only mechanical lib-list baseline updates", () => { const tempDirectory = createTempDirectory(tempDirectories); - const baselinePath = "tests/baselines/reference/config/lib-list.js"; - const fullBaselinePath = path.join(tempDirectory, baselinePath); - const semanticPath = "tests/baselines/reference/config/semantic.js"; - const fullSemanticPath = path.join(tempDirectory, semanticPath); - execFileSync("git", ["init", "--quiet"], { cwd: tempDirectory }); - execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: tempDirectory }); - execFileSync("git", ["config", "user.name", "Test"], { cwd: tempDirectory }); - fs.mkdirSync(path.dirname(fullBaselinePath), { recursive: true }); - fs.writeFileSync(fullBaselinePath, "one or more: es5, esnext, dom\n//// [/file] Inode:: 10\n {\"inode\":10}\n"); - fs.writeFileSync(fullSemanticPath, "semantic result: alpha, omega\n"); - execFileSync("git", ["add", baselinePath, semanticPath], { cwd: tempDirectory }); - execFileSync("git", ["commit", "--quiet", "-m", "fixture"], { cwd: tempDirectory }); - - fs.writeFileSync(fullBaselinePath, "one or more: es5, esnext, baseline, dom\n//// [/file] Inode:: 11\n {\"inode\":11}\n"); + const baselinePath = "tsc/testdata/baselines/reference/tsoptions/lib-list.js"; + const wrappedHelpPath = "tsc/testdata/baselines/reference/tsc/commandLine/help.js"; + const diffHelpPath = "tsc/testdata/baselines/reference/tscWatch/commandLineWatch/help-diff.js"; + const mixedContentPath = "tsc/testdata/baselines/reference/tsc/commandLine/mixed.js"; + const semanticPath = "tsc/testdata/baselines/reference/compiler/semantic.js"; + initializeGitFixture(tempDirectory, { + [baselinePath]: "Argument for '--lib' option must be: 'es5', 'esnext', 'dom'.\n", + [wrappedHelpPath]: "header\none or more: es5, esnext, do\n m, webworker\n\nfooter\n", + [diffHelpPath]: "Diff::\n-one or more: es5, esnext, dom\n-default: undefined\n", + [mixedContentPath]: "one or more: es5, esnext, dom\n\nresult: alpha, omega\n", + [semanticPath]: "semantic result: alpha, omega\n", + }); + + fs.writeFileSync( + path.join(tempDirectory, baselinePath), + "Argument for '--lib' option must be: 'es5', 'esnext', 'baseline', 'dom'.\n", + ); + fs.writeFileSync( + path.join(tempDirectory, wrappedHelpPath), + "header\none or more: es5, esnext, baseline, dom,\n webworker\n\nfooter\n", + ); + fs.writeFileSync( + path.join(tempDirectory, diffHelpPath), + "Diff::\n-one or more: es5, esnext, baseline, dom\n-default: undefined\n", + ); assert.deepEqual(findUnexpectedTypeScriptPatchPaths(tempDirectory, []), []); - fs.writeFileSync(fullSemanticPath, "semantic result: alpha, baseline, omega\n"); - assert.deepEqual(findUnexpectedTypeScriptPatchPaths(tempDirectory, []), [semanticPath]); - fs.writeFileSync(fullSemanticPath, "semantic result: alpha, omega\n"); + fs.writeFileSync( + path.join(tempDirectory, mixedContentPath), + "one or more: es5, esnext, baseline, dom\n\nresult: alpha, baseline, omega\n", + ); + assert.deepEqual(findUnexpectedTypeScriptPatchPaths(tempDirectory, []), [mixedContentPath]); - fs.appendFileSync(fullBaselinePath, "semantic change\n"); - assert.deepEqual(findUnexpectedTypeScriptPatchPaths(tempDirectory, []), [baselinePath]); + fs.writeFileSync(path.join(tempDirectory, semanticPath), "semantic result: alpha, baseline, omega\n"); + assert.deepEqual(findUnexpectedTypeScriptPatchPaths(tempDirectory, []), [semanticPath, mixedContentPath]); }); + +/** + * @param {string} tempDirectory + */ +function createFakeTypeScriptTree(tempDirectory) { + const typescriptDir = path.join(tempDirectory, "TypeScript"); + const libSourceDirectory = path.join(typescriptDir, typescriptLibSourceDirectory); + const enumMapsPath = path.join(typescriptDir, enumMapsRelativePath); + + fs.mkdirSync(libSourceDirectory, { recursive: true }); + fs.mkdirSync(path.dirname(enumMapsPath), { recursive: true }); + fs.writeFileSync( + path.join(libSourceDirectory, "libs.json"), + "{\n \"libs\": [\n \"es2025\",\n \"esnext\",\n \"dom.generated\"\n ]\n}\n", + ); + fs.writeFileSync( + enumMapsPath, + "var LibMap = collections.NewOrderedMapFromList([]collections.MapEntry[string, any]{\n\t{Key: \"es2025\", Value: \"lib.es2025.d.ts\"},\n\t{Key: \"esnext\", Value: \"lib.esnext.d.ts\"},\n\t{Key: \"dom\", Value: \"lib.dom.d.ts\"},\n})\n", + ); + + return typescriptDir; +} + +/** + * @param {string} tempDirectory + */ +function createFixtureTree(tempDirectory) { + const fixturesRoot = path.join(tempDirectory, "fixtures"); + const testCasePath = path.join(fixturesRoot, "tests", "cases", "compiler", "libBaseline.ts"); + const baselinePath = path.join(fixturesRoot, "tests", "baselines", "reference", "libBaseline.errors.txt"); + + fs.mkdirSync(path.dirname(testCasePath), { recursive: true }); + fs.mkdirSync(path.dirname(baselinePath), { recursive: true }); + fs.writeFileSync(testCasePath, "// @lib: baseline\nObject.hasOwn({}, 'x');\n"); + fs.writeFileSync(baselinePath, "fixture baseline\n"); + return fixturesRoot; +} + +/** + * @param {string} directory + * @param {Record} files + */ +function initializeGitFixture(directory, files) { + execFileSync("git", ["init", "--quiet"], { cwd: directory }); + execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: directory }); + execFileSync("git", ["config", "user.name", "Test"], { cwd: directory }); + for (const [relativePath, text] of Object.entries(files)) { + const filePath = path.join(directory, relativePath); + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, text); + } + execFileSync("git", ["add", "."], { cwd: directory }); + execFileSync("git", ["commit", "--quiet", "-m", "fixture"], { cwd: directory }); +} diff --git a/test/year-entrypoints.test.mjs b/test/year-entrypoints.test.mjs index 581b00c..8b90694 100644 --- a/test/year-entrypoints.test.mjs +++ b/test/year-entrypoints.test.mjs @@ -248,7 +248,6 @@ test("weekly update summary reports year contract changes for manual review", () }, libSource: {}, typescriptSource: {}, - typescriptGoSource: {}, }; const summary = buildUpdateSummary({ currentManifest: manifest,