diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8f4cde1..80c4ae2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -124,9 +124,9 @@ jobs: # jobs (volar-test, witnesses, sim-nav-shard) restore it with # fail-on-cache-miss, and their needs: build guarantees this job's # post-job save has landed. The tree is BUILT (install + pnpm run build, - # same as nightly) — the sim-nav shards need the compiled plugin; v4 - # splits off stale install-only v2 entries so a shard never restores an - # unbuilt tree. + # same as nightly) — the sim-nav shards need the compiled plugin; v5 + # splits off the link:-override v4 trees (install-volar.sh's frozen + # install + repointed store entries replaced them). - name: Clone volar (cache) id: volar-cache uses: actions/cache@v4 @@ -134,7 +134,7 @@ jobs: path: | ${{ runner.temp }}/volar ${{ runner.temp }}/volar-stock - key: volar-${{ env.VOLAR_SHA }}-v4 + key: volar-${{ env.VOLAR_SHA }}-v5 - name: Clone volar if: steps.volar-cache.outputs.cache-hit != 'true' @@ -144,19 +144,12 @@ jobs: git -C "$RUNNER_TEMP/volar" fetch --deepen 50 origin "$VOLAR_BRANCH" git -C "$RUNNER_TEMP/volar" checkout "$VOLAR_SHA" } - # Point volar's typescript override at the CI-built TNB — volar - # master's override is `npm:typescript-native-bridge@`, which - # would fetch the released package instead of the commit under test. - cd "$RUNNER_TEMP/volar" - node -e "const fs=require('fs');const p='pnpm-workspace.yaml';let s=fs.readFileSync(p,'utf8');s=s.replace(/typescript: npm:typescript-native-bridge@.*/,'typescript: link:'+process.env.GITHUB_WORKSPACE);fs.writeFileSync(p,s);console.log(s.match(/overrides:[\s\S]*/)[0])" - name: Install + build volar if: steps.volar-cache.outputs.cache-hit != 'true' - run: | - cd "$RUNNER_TEMP/volar" - corepack enable - pnpm install --no-frozen-lockfile - pnpm run build + # volar pins a released TNB; install-volar.sh repoints it at the + # commit under test. + run: tools/install-volar.sh "$RUNNER_TEMP/volar" - name: Pack stock typescript if: steps.volar-cache.outputs.cache-hit != 'true' @@ -277,7 +270,7 @@ jobs: path: | ${{ runner.temp }}/volar ${{ runner.temp }}/volar-stock - key: volar-${{ env.VOLAR_SHA }}-v4 + key: volar-${{ env.VOLAR_SHA }}-v5 fail-on-cache-miss: true # Runs on every CI run: the cached node_modules links @@ -286,7 +279,8 @@ jobs: run: | # Volar's tsserver harness resolves typescript/lib/tsserver via a # fresh module lookup from dirname(realpath(typescript/package.json)). - # With the link: override that realpath is this repo, so `typescript` + # install-volar.sh repoints the store entries here, so that realpath + # is this repo and `typescript` # must resolve from here too — self-link it (TNB has no typescript # dependency of its own; node_modules is CI-local state). # node_modules may not exist at all: the root package has no @@ -332,7 +326,7 @@ jobs: path: | ${{ runner.temp }}/volar ${{ runner.temp }}/volar-stock - key: volar-${{ env.VOLAR_SHA }}-v4 + key: volar-${{ env.VOLAR_SHA }}-v5 fail-on-cache-miss: true - name: sweep-ls-throws @@ -449,7 +443,7 @@ jobs: path: | ${{ runner.temp }}/volar ${{ runner.temp }}/volar-stock - key: volar-${{ env.VOLAR_SHA }}-v4 + key: volar-${{ env.VOLAR_SHA }}-v5 fail-on-cache-miss: true - name: Run sim-nav shard diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index eefc4b5..1298c3b 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -97,33 +97,26 @@ jobs: id: volar-cache # Both workflows need a BUILT volar tree (install + pnpm run build — # the sim-nav shards load the compiled plugin). Keys stay per-workflow - # (nightly-v2 vs ci.yml's v4): first-writer-wins on a shared key would + # (nightly-v3 vs ci.yml's v5): first-writer-wins on a shared key would # let one workflow's write timing define the other's content. uses: actions/cache@v4 with: path: | ${{ runner.temp }}/volar ${{ runner.temp }}/volar-stock - key: volar-${{ env.VOLAR_SHA }}-nightly-v2 + key: volar-${{ env.VOLAR_SHA }}-nightly-v3 - name: Clone volar if: steps.volar-cache.outputs.cache-hit != 'true' run: | git clone --depth 50 --branch "$VOLAR_BRANCH" https://github.com/vuejs/language-tools.git "$RUNNER_TEMP/volar" git -C "$RUNNER_TEMP/volar" checkout "$VOLAR_SHA" - # Point volar's typescript override at the CI-built TNB — volar - # master's override is `npm:typescript-native-bridge@`, which - # would fetch the released package instead of the commit under test. - cd "$RUNNER_TEMP/volar" - node -e "const fs=require('fs');const p='pnpm-workspace.yaml';let s=fs.readFileSync(p,'utf8');s=s.replace(/typescript: npm:typescript-native-bridge@.*/,'typescript: link:'+process.env.GITHUB_WORKSPACE);fs.writeFileSync(p,s)" - name: Install + build volar if: steps.volar-cache.outputs.cache-hit != 'true' - run: | - cd "$RUNNER_TEMP/volar" - corepack enable - pnpm install --no-frozen-lockfile - pnpm run build + # volar pins a released TNB; install-volar.sh repoints it at the + # commit under test. + run: tools/install-volar.sh "$RUNNER_TEMP/volar" - name: Pack stock typescript if: steps.volar-cache.outputs.cache-hit != 'true' @@ -238,7 +231,7 @@ jobs: path: | ${{ runner.temp }}/volar ${{ runner.temp }}/volar-stock - key: volar-${{ env.VOLAR_SHA }}-nightly-v2 + key: volar-${{ env.VOLAR_SHA }}-nightly-v3 fail-on-cache-miss: true - name: Run sim-nav shard diff --git a/AGENTS.md b/AGENTS.md index be6580a..76935bb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -39,9 +39,8 @@ TNB is a tsgo-backed TypeScript fork: upstream `microsoft/TypeScript` and `micro export VOLAR_SHA=<.github/workflows/ci.yml value> git clone --depth 50 --branch master https://github.com/vuejs/language-tools.git /tmp/volar git -C /tmp/volar checkout "$VOLAR_SHA" || { git -C /tmp/volar fetch --deepen 50 origin master; git -C /tmp/volar checkout "$VOLAR_SHA"; } - # point /tmp/volar/pnpm-workspace.yaml's typescript override at this repo (link:) - cd /tmp/volar && corepack enable && pnpm install --no-frozen-lockfile && pnpm run build - cd && VOLAR_ROOT=/tmp/volar STOCK_TSSERVER_PATH=/tmp/stock-ts-p3/package/lib/tsserver.js npm run check:sim-nav + cd && tools/install-volar.sh /tmp/volar # frozen install, released TNB repointed at + VOLAR_ROOT=/tmp/volar STOCK_TSSERVER_PATH=/tmp/stock-ts-p3/package/lib/tsserver.js npm run check:sim-nav ``` `check:sourcefile-guard` needs only `VOLAR_ROOT`. Both were run in full for #72/#73. `check-pristine-attribution` clones pristine tsgo into `/tmp/tnb-pristine-tsgo` and refuses to run only if one of its own repro files is already present there — a stray file of another name goes unnoticed, so check `git -C /tmp/tnb-pristine-tsgo status --porcelain` yourself. - `node tools/ci-witness-groups.mjs all` (the orphan / local-only / baseline sweep) runs only locally and in nightly — ci.yml's prepare job calls `matrix` mode, so a witness added without a group entry is not caught per-commit. @@ -95,7 +94,7 @@ Mechanism-level compromises the convergence audit decided to keep as-is. Each en - **Arena oversize escapes to the JSON path** — an arena encode that would overflow (record/string regions meet) fails and the call's JSON document returns out-of-band as the napi value instead; 4 MiB is ~80× the largest expected hot-class response, so capacity is by design. Accepted because the escape is the same transport every method already has, and it turns an overflow into a loud failed encode rather than memory corruption. Policed by `triage-arena-parity.mjs` (every arena-capable method runs once per transport — JSON and arena — and must byte-equal). - **Stale type handles die loudly, by construction** — Go TypeIDs are per-checker counters (`t.id = TypeId(c.TypeCount)`) with no generation marker, so the JS side pins handles in a snapshot-scoped registry (`projectRegistries`, keyed by TypeID = `t.Id()`): a handle from an old snapshot either still names the same type or fails with "type handle N not found in project registry" — it can never silently resolve to a rebuilt snapshot's same-id type. Accepted because stable ids are what let unchanged types reuse across generations; the cost is the loud-error contract. Policed by `triage-crossgen-reuse.mjs` (negative test: a handle captured pre-edit must die, not re-resolve). -- **Stable-SF cross-generation reuse rides external-change event completeness** — a reused stable host SourceFile is only as fresh as the events that evict it: every external rewrite must reach `_pendingExternalChangePaths`, drained by the overlay collect — the single transport choke (host≠disk rides the overlay push, host==disk becomes updateSnapshot `fileChanges.changed`). Accepted because one choke keeps the invariant checkable. Policed by `triage-external-edits.mjs` (local-only), incl. the stablecache phase. +- **Stable-SF cross-generation reuse rides external-change event completeness** — a reused stable host SourceFile is only as fresh as the events that evict it: every external rewrite must reach `_pendingExternalChangePaths`, drained by `drainExternalFileChanges` — the single transport choke, called from both updateSnapshot sites (the createTsgoProgram collect and the reused-program query sync `pushHostOverlayToTsgo`, issue #74) (host≠disk rides the overlay push, host==disk becomes updateSnapshot `fileChanges.changed`). Accepted because one choke keeps the invariant checkable. Policed by `triage-external-edits.mjs` (local-only), incl. the stablecache phase. - **Patch monoliths are accepted debt** — 0001-bridge-inplace (napi-shim overlay files + checker accessors + native-preview client — 48 files) and 0004-api-surface (the whole RPC surface: `internal/api/proto.go` + `session.go` + the native-preview api files) stay as single patches; splitting is deferred. Accepted because the carve-outs that do rebase independently (osvfs, noembed) already have their own patches. Policed by `npm run save-patches` (regenerates both from the submodule diff). - **tsgoChecker.ts is one monolith** — the overlay adapter is a single ~13.8k-line file. Accepted because it mirrors one bridge contract; splitting it would invent an interface for ceremony's sake. Policed by review, not a gate. - **SubstitutionType.constraint alias is code-path parity only** — the stock-name `constraint` resolving alias fires the same fetch the NESTED wire loop uses for `substConstraint`. Accepted because it is a one-line alias over an already-gated fetch. Policed by `triage-checker-fullwalk`'s FW_SUBST_CONSTRAINT keys (the census corpus's `NoInfer` is the reliable source-level trigger); the alias-vs-wire field-name divergence stays pinned there. diff --git a/patches/typescript/0001-tsgo-hooks.patch b/patches/typescript/0001-tsgo-hooks.patch index d6e9133..dd3949b 100644 --- a/patches/typescript/0001-tsgo-hooks.patch +++ b/patches/typescript/0001-tsgo-hooks.patch @@ -515,7 +515,7 @@ index aa578c7ed0..0e49d57945 100644 const result = this.tryFindDefaultConfiguredProjectAndLoadAncestorsForOpenScriptInfo( info, diff --git a/src/server/scriptInfo.ts b/src/server/scriptInfo.ts -index 8635d543cd..bcc848c1a6 100644 +index 8635d543cd..99cad4cab8 100644 --- a/src/server/scriptInfo.ts +++ b/src/server/scriptInfo.ts @@ -34,6 +34,8 @@ import { @@ -563,7 +563,17 @@ index 8635d543cd..bcc848c1a6 100644 /** * Reads the contents from tempFile(if supplied) or own file and sets it as contents * returns true if text changed -@@ -440,7 +471,7 @@ export class ScriptInfo { +@@ -179,6 +210,9 @@ export class TextStorage { + this.getFileTextAndSize(tempFileName) : + { text: "", fileSize: undefined }; + const reloaded = this.reload(newText); ++ // #74: tsserver `reload` of an open file (the client re-read it from ++ // disk after an external rewrite) — same signal as reloadForOpen. ++ if (reloaded) tnbNoteExternalFileChange(this.info.fileName); + this.fileSize = fileSize; // NB: after reload since reload clears it + this.ownFileText = !tempFileName || tempFileName === this.info.fileName; + // In case we update this text before mTime gets updated to present file modified time +@@ -440,7 +474,7 @@ export class ScriptInfo { this.textStorage.isOpen = true; if ( newText !== undefined && @@ -572,7 +582,7 @@ index 8635d543cd..bcc848c1a6 100644 ) { // reload new contents only if the existing contents changed this.markContainingProjectsAsDirty(); -@@ -675,6 +706,13 @@ export class ScriptInfo { +@@ -675,6 +709,13 @@ export class ScriptInfo { editContent(start: number, end: number, newText: string): void { this.textStorage.edit(start, end, newText); diff --git a/patches/typescript/overlay/src/compiler/tsgoChecker.ts b/patches/typescript/overlay/src/compiler/tsgoChecker.ts index b22cc0f..196815a 100644 --- a/patches/typescript/overlay/src/compiler/tsgoChecker.ts +++ b/patches/typescript/overlay/src/compiler/tsgoChecker.ts @@ -2366,14 +2366,14 @@ const _pendingOverlayEditsByFile: Map = tnbBridgeProcessState().pendingExternalChangePaths ??= new Set(); @@ -2386,6 +2386,33 @@ export function tnbNoteExternalFileChange(fileName: string): void { _pendingExternalChangePaths.add(resolveHostFileName(fileName)); } +/** + * Drain the external-change signals. Both updateSnapshot sites call it: the + * createTsgoProgram collect, and pushHostOverlayToTsgo for a reused thin + * program — tsserver's steady state (issue #74: __tnbIsProgramUptoDate keeps + * the program across updateOpen/reload, so the collect never reruns and a + * host==disk rewrite would otherwise never reach Go). `changed` is the + * host==disk wire list; files with a synced overlay are excluded (the overlay + * already supersedes disk in Go's reads), as are gone-from-disk files (their + * rootNames/params churn drives a real snapshot on its own). + */ +function drainExternalFileChanges(): { drained: Set; changed: string[] | undefined; } | undefined { + if (_pendingExternalChangePaths.size === 0) return undefined; + const drained = new Set(_pendingExternalChangePaths); + _pendingExternalChangePaths.clear(); + let changed: string[] | undefined; + for (const f of drained) { + // An externally-rewritten stable-path file (node_modules/lib .d.ts) + // must not keep serving the process-global bound AST: a + // constant-version host (typescript-estree getScriptVersion "1") + // matches the stable version check forever, so evict the entry and + // the next materialization re-reads host/disk like Go does. + if (isStableHostSfPath(f)) _hostSfStableGlobal.delete(f); + if (fileExistsOnDisk(f) && !_syncedOverlayContentByFile.has(f)) (changed ??= []).push(f); + } + return { drained, changed }; +} + /** * Record one host edit delta (stock tsserver `change` → ScriptInfo.editContent) * for the next overlay sync. The wire then carries `{edits, baseVersion}` @@ -7053,32 +7080,12 @@ export function createTsgoProgram( } return !!(lsHost?.getScriptSnapshot?.(fn) ?? lsHost?.getScriptSnapshot?.(resolvedFn)); }; - // Drain the external-change signals (issue #49) — every source only - // feeds the set; this collect is the single choke deciding transport. - // The wire list is host==disk files Go must re-read (fileChanges.changed): - // Go's SnapshotFS froze each disk file at FIRST read, so a host text - // equal to CURRENT disk can still differ from Go's frozen view. Files - // with a synced overlay are excluded — the overlay already supersedes - // disk in Go's reads. Gone-from-disk files are excluded — their - // rootNames/params churn drives a real snapshot on its own. - if (_pendingExternalChangePaths.size > 0) { - pendingExternalSet = new Set(_pendingExternalChangePaths); - _pendingExternalChangePaths.clear(); - for (const f of pendingExternalSet) { - // Stable host-SF cache eviction: an externally-rewritten - // stable-path file (node_modules/lib .d.ts) must not keep - // serving the process-global bound AST. Go re-reads disk via - // fileChanges.changed, but a constant-version host - // (typescript-estree getScriptVersion "1") makes the stable - // version check below match forever — evict the entry so the - // next materialization re-reads host/disk like Go does. The - // per-program sfCache is empty at this choke (fresh per - // createTsgoProgram, populated only by later materialization), - // so the process-global entry is the only stale cache. - if (isStableHostSfPath(f)) _hostSfStableGlobal.delete(f); - if (fileExistsOnDisk(f) && !_syncedOverlayContentByFile.has(f)) (externalChanged ??= []).push(f); - } - } + // The per-program sfCache is empty here (fresh per createTsgoProgram, + // populated only by later materialization), so the drain's stable + // eviction is the only host-SF invalidation this site needs. + const external = drainExternalFileChanges(); + pendingExternalSet = external?.drained; + externalChanged = external?.changed; // Skip the whole collect when host content provably cannot diverge // anywhere: no snapshot/LS host, no projectService, no extra-extension // or disk-absent file, no still-synced overlay, no external-change @@ -10130,6 +10137,9 @@ export function createTsgoChecker(program: any): any { const syncHost = hostForOverlaySyncLocal(); if (!syncHost) return; + // Drained before the walk so the synced-overlay exclusion sees the + // mirror the walk is about to reconcile — the same order as the collect. + const externalChanged = drainExternalFileChanges()?.changed; const openFiles = collectTsgoOpenFileNames(syncHost, requestedFileName ? [requestedFileName] : undefined); const openFilesWithContent: { fileName: string; content: string; scriptKind: number }[] = []; for (const hostFileName of openFiles) { @@ -10157,7 +10167,7 @@ export function createTsgoChecker(program: any): any { } openFilesWithContent.push({ fileName: hostFileName, content: content.text, scriptKind: content.scriptKind }); } - if (!openFiles.length && !openFilesWithContent.length) return; + if (!openFiles.length && !openFilesWithContent.length && !externalChanged) return; // No-change fast path: bumping the snapshot here would dispose the // handle registry the reused thin program's caches still reference @@ -10171,13 +10181,14 @@ export function createTsgoChecker(program: any): any { // empty snapshot. ensureProject records the same key after its own // snapshot so the first query sync after a rebuild is a no-op too. const pushKey = openFiles.join("\n"); - if (openFilesWithContent.length === 0 && _lastOverlayPushKeyByConfig.get(ctx.configFilePath) === pushKey) return; + if (openFilesWithContent.length === 0 && !externalChanged && _lastOverlayPushKeyByConfig.get(ctx.configFilePath) === pushKey) return; traceOverlaySync(openFilesWithContent, /*deduped*/ false); const snapshot: any = _api.updateSnapshot({ openProject: openProjectParam(ctx.configFilePath, ctx.options), ...(openFiles.length > 0 ? { openFiles } : {}), openFilesWithContent, + ...(externalChanged ? { fileChanges: { changed: externalChanged } } : {}), ...(_lastExtraFileExtensions ? { extraFileExtensions: _lastExtraFileExtensions } : {}), // Host-injected extra roots (svelte2tsx/glint shims) — without them // every hook-driven rebuild drops the ambient shim files from the @@ -10202,16 +10213,16 @@ export function createTsgoChecker(program: any): any { _projectCache.set(ctx.configFilePath, refreshed); _currentProjectRef.project = refreshed; installTsgoBackedSourceFileLoader(() => project); - for (const f of openFilesWithContent) { - commitSyncedOverlay(f); - tsgoSfCache.delete(f.fileName); - nodeIndexCache.delete(f.fileName); - nodeAtPosCache.delete(f.fileName); + for (const f of openFilesWithContent) commitSyncedOverlay(f); + for (const fileName of [...openFilesWithContent.map(f => f.fileName), ...(externalChanged ?? [])]) { + tsgoSfCache.delete(fileName); + nodeIndexCache.delete(fileName); + nodeAtPosCache.delete(fileName); // The Go snapshot advanced; the JS-side host SourceFile caches for // this file are now stale (sfCache keys on a constant host version), // so drop them too or semantic highlights/diagnostics walk the old // AST against the new snapshot. - programCtx?.thinProgram?.__tnbInvalidateHostSourceFile?.(f.fileName); + programCtx?.thinProgram?.__tnbInvalidateHostSourceFile?.(fileName); } // Every updateSnapshot REPLACES the Go snapshot and disposes the // previous generation's handle registry — the caches below all hold diff --git a/tools/install-volar.sh b/tools/install-volar.sh new file mode 100755 index 0000000..a803655 --- /dev/null +++ b/tools/install-volar.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# Install + build a volar checkout against this repo — the one recipe behind +# ci.yml, nightly.yml and the AGENTS.md local setup. +# tools/install-volar.sh +# +# Frozen: volar's lockfile is the resolution pnpm 11's supply-chain policies +# already vetted. Rewriting its `typescript` override to link: this repo made +# pnpm re-resolve every `latest` specifier from scratch (vitest 5.0.2 pulled +# why-is-node-running@3.2.2, which trustPolicy rejects), so the lockfile's +# released TNB is installed and its store entries are repointed here instead. +set -euo pipefail +repo=$(cd "$(dirname "$0")/.." && pwd) +cd "$1" +export COREPACK_ENABLE_DOWNLOAD_PROMPT=0 +corepack enable +pnpm install --frozen-lockfile +shopt -s nullglob +entries=(node_modules/.pnpm/typescript-native-bridge@*/node_modules/typescript-native-bridge) +((${#entries[@]})) || { echo "install-volar: volar's lockfile has no typescript-native-bridge entry" >&2; exit 1; } +for d in "${entries[@]}"; do + rm -rf "$d" + ln -s "$repo" "$d" +done +# The released package's platform bridges resolve ahead of /native +# (tsgoChecker's bridge lookup order) from any non-realpath load of the store +# entry — pairing this commit's JS with the pinned release's bridge.node. +rm -rf node_modules/.pnpm/@typescript-native-bridge+* node_modules/.pnpm/typescript-native-bridge@*/node_modules/@typescript-native-bridge +pnpm run build diff --git a/tools/triage-external-edits.mjs b/tools/triage-external-edits.mjs index 3749b3f..f28a362 100644 --- a/tools/triage-external-edits.mjs +++ b/tools/triage-external-edits.mjs @@ -8,8 +8,10 @@ * - estree: typescript-eslint classic `project:` path — watch program * lints a.ts; external rewrite + unsaved-buffer code must both * change the linted file's diagnostics like stock. - * - tsserver: external rewrite of a.ts + updateOpen re-send must clear the - * dependent b.ts diagnostic. + * - tsserver: external rewrite of a.ts + the client's buffer re-sync + * (re-open, updateOpen changedFiles, reload — issue #74) must + * clear the dependent b.ts diagnostic and classify a.ts at its + * new offsets. * - tscwatch: plain `tsc -w` — content edit must clear the error, file add * must surface the new file's error, output text stock-equal. * - stablecache: external rewrite of a node_modules .d.ts under a @@ -168,37 +170,57 @@ const send = (command, args) => new Promise((res) => { const openBoth = (aText) => send('updateOpen', { openFiles: [ { file: a, fileContent: aText, scriptKindName: 'TS' }, { file: b, fileContent: fs.readFileSync(b, 'utf8'), scriptKindName: 'TS' }, -] }); // fire-and-forget; the awaited diagnosticsSync below orders after it +] }); // fire-and-forget; the awaited requests below order after it const bDiags = async () => (await send('semanticDiagnosticsSync', { file: b })).body?.length; +const aSpans = async () => (await send('encodedSemanticClassifications-full', + { file: a, start: 0, length: fs.readFileSync(a, 'utf8').length, format: '2020' })).body?.spans; -openBoth(fs.readFileSync(a, 'utf8')); +const v1 = fs.readFileSync(a, 'utf8'); +openBoth(v1); console.log('before external rewrite: diags(b.ts) =', await bDiags()); -fs.writeFileSync(a, 'export const alpha = "str";\\n'); // external tool rewrites a.ts -openBoth(fs.readFileSync(a, 'utf8')); // IDE watcher re-sends the new disk text +// Shifted offsets: a stale Go text answers classifications at v1 positions. +fs.writeFileSync(a, '// shifted\\nfunction useThing() {}\\nexport const alpha = "str";\\nuseThing();\\n'); +const v2 = fs.readFileSync(a, 'utf8'); +// How the client re-syncs the open buffer after the rewrite (argv[3]). +const resync = { + reopen: () => openBoth(v2), + changedFiles: () => send('updateOpen', { changedFiles: [{ fileName: a, textChanges: [ + { start: { line: 1, offset: 1 }, end: { line: v1.split('\\n').length, offset: 1 }, newText: v2 }, + ] }] }), + reload: () => send('reload', { file: a, tmpfile: a }), +}[process.argv[3]]; +await resync(); console.log('after external rewrite: diags(b.ts) =', await bDiags()); +console.log('after external rewrite: spans(a.ts) =', JSON.stringify(await aSpans())); srv.kill(); process.exit(0); `; +// reopen: #49 (openFiles re-send). changedFiles / reload: #74 — the reused +// tsserver program only syncs through the query-path overlay push. +const TSSERVER_RESYNC_MODES = ['reopen', 'changedFiles', 'reload']; + function runTsserver() { const driver = path.join(scratchRoot, 'tsserver-driver.mjs'); fs.mkdirSync(scratchRoot, { recursive: true }); fs.writeFileSync(driver, TSSERVER_DRIVER); - const outTnb = runNode(driver, [path.join(repoRoot, 'lib', 'tsserver.js')], scratchRoot); - const outStock = runNode(driver, [path.join(stockPkg, 'lib', 'tsserver.js')], scratchRoot); - const grab = (out, label) => { - const m = out.match(new RegExp(label + 'diags\\(b\\.ts\\) = (\\d+)')); - return m ? +m[1] : undefined; - }; - const tnb = { before: grab(outTnb, 'before external rewrite: '), after: grab(outTnb, 'after external rewrite: ') }; - const stock = { before: grab(outStock, 'before external rewrite: '), after: grab(outStock, 'after external rewrite: ') }; - if (stock.before !== 1 || stock.after !== 0) { - return fail('tsserver', `stock control diverged from issue table: ${JSON.stringify(stock)}\n${outStock}`); - } - if (tnb.before !== stock.before || tnb.after !== stock.after) { - return fail('tsserver', `phase mismatch vs stock:\n tnb: ${JSON.stringify(tnb)}\n stock: ${JSON.stringify(stock)}\n--- tnb output ---\n${outTnb}`); + const parse = (out) => ({ + before: +out.match(/before external rewrite: diags\(b\.ts\) = (\d+)/)?.[1], + after: +out.match(/after external rewrite: diags\(b\.ts\) = (\d+)/)?.[1], + spans: out.match(/after external rewrite: spans\(a\.ts\) = (.*)/)?.[1], + }); + for (const mode of TSSERVER_RESYNC_MODES) { + const outTnb = runNode(driver, [path.join(repoRoot, 'lib', 'tsserver.js'), mode], scratchRoot); + const outStock = runNode(driver, [path.join(stockPkg, 'lib', 'tsserver.js'), mode], scratchRoot); + const tnb = parse(outTnb), stock = parse(outStock); + if (stock.before !== 1 || stock.after !== 0 || !stock.spans || stock.spans === '[]') { + return fail('tsserver', `${mode}: stock control diverged from issue table: ${JSON.stringify(stock)}\n${outStock}`); + } + if (JSON.stringify(tnb) !== JSON.stringify(stock)) { + return fail('tsserver', `${mode}: phase mismatch vs stock:\n tnb: ${JSON.stringify(tnb)}\n stock: ${JSON.stringify(stock)}\n--- tnb output ---\n${outTnb}`); + } } - console.log('[tsserver] ok (before=1, after=0, stock-identical)'); + console.log(`[tsserver] ok (${TSSERVER_RESYNC_MODES.join('/')}: before=1, after=0, rewritten-file classifications stock-identical)`); return true; }