Skip to content

Commit a989514

Browse files
authored
feat(vscode)!: support @rstest/core 0.12 public API (#61)
* feat(vscode): support @rstest/core 0.12 public API `@rstest/core@0.12.0` removed `initCli` and `createRstest` from the main entry; the programmatic API now lives at `@rstest/core/api` (`createRstest` -> `run` / `watch` / `listTests` / `context`). Port the upstream extension's move to that API into `stacks/test`, preserving the ten documented adaptations. - Worker: `loadConfig` + `mergeRstestConfig` + `createRstest`; one-shot runs through `run()`, continuous runs through `watch()` with a graceful `closeWatcher()`; flat `listTests()` results. - Master: resolve `@rstest/core` and `@rstest/core/api` from one package directory; graceful worker close with a 30 s SIGKILL fallback; exact file/case filters via quoted paths (`quoteFilter` vendored). - Tree: build the hierarchy from the flat `ListedTest[]` shape, first project wins for a file shared by several projects. - Support floor for `@rstest/core` raised to `>=0.12.0`; fixtures pinned to `@rstest/core@0.12.0` and `rstack@0.7.6`. - Tracks web-infra-dev/rstest `packages/vscode` through #1729, #1804, #1805, #1806 and #1807 (recorded in AGENTS.md). * fix(vscode): keep empty-named declarations in runtime discovery `updateFromListedTests` skipped entries whose name was falsy, which dropped `test('', ...)` / `describe('', ...)` and attached their children one level up. File rows are already removed by `groupListedTestsByFile`, so only `undefined` names need skipping. * fix(vscode): raise the rstack floor to 0.7.6 Bridged `rstack.config.*` projects resolve `@rstest/core` from the rstack package directory, and 0.7.6 is the first rstack release pinning `@rstest/core ~0.12.0`. The floor stays uniform across lint, test and fmt. * fix(vscode): resolve @rstest/core/api through the core package itself A configured `rstestPackagePath` may point at a core outside any `node_modules/@rstest/core` directory; a bare-specifier lookup from that directory then fails while the main entry resolves fine. Anchor the api subpath on the core's own package.json via package self-reference, and advertise the 0.12.0 floor in the README support matrix.
1 parent c02ba52 commit a989514

28 files changed

Lines changed: 1613 additions & 276 deletions

File tree

‎packages/vscode/AGENTS.md‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,9 @@ One extension replacing the standalone `rstack.rslint` and `rstack.rstest` exten
66

77
- `stacks/lint` and `stacks/test` are deliberate near-verbatim copies of the upstream extensions, kept close to upstream so changes can be synced by diffing. Do NOT deduplicate or refactor across the two stacks — the duplication is the point; consolidation is a later, explicit phase.
88
- The copies diverge from upstream in exactly ten ways (the "adaptations" below). When syncing upstream, preserve them. An eleventh divergence is either a bug or must be added to this list.
9-
- **Tracked upstream state.** `stacks/lint` is synced to web-infra-dev/rslint `packages/vscode-extension` at **39536fd6** (#1617 — per-document core resolution, `CoreResolver` + `RuntimeManager`, `corePath`, PnP removed) and **892482e0** (#1630 — `configPath` on `rslint/configRefresh`). Targeted later ports are **84f9c9b5** (#1967 — languageclient-owned live LSP tracing) and **b7176723** (#1951 — remove legacy JSON config watching); the Unicode BOM E2E comes from **5fc197a5** (#1560), with its native-config fixture shape from **b7176723**. `CoreResolver.ts` / `RuntimeManager.ts` / `WorkspaceDocumentRouter.ts` / `Rslint.ts` are the files to diff when syncing further; record the new commits here when you do.
9+
- **Tracked upstream state.** `stacks/lint` is synced to web-infra-dev/rslint `packages/vscode-extension` at **39536fd6** (#1617 — per-document core resolution, `CoreResolver` + `RuntimeManager`, `corePath`, PnP removed) and **892482e0** (#1630 — `configPath` on `rslint/configRefresh`). Targeted later ports are **84f9c9b5** (#1967 — languageclient-owned live LSP tracing) and **b7176723** (#1951 — remove legacy JSON config watching); the Unicode BOM E2E comes from **5fc197a5** (#1560), with its native-config fixture shape from **b7176723**. `CoreResolver.ts` / `RuntimeManager.ts` / `WorkspaceDocumentRouter.ts` / `Rslint.ts` are the files to diff when syncing further; record the new commits here when you do. `stacks/test` tracks web-infra-dev/rstest `packages/vscode` through **8f945491** (#1729 — public programmatic API), **d2812754** (#1804 — quoted exact file filters), **99be33e8** (#1805 — public test listing), **88cd5f6d** (#1806 — browser projects in watch), and **994b77e0** (#1807 — public watch API). The earlier targeted lifecycle fix from **d82db4fc31a61ee74b2a74917f14a458e1bca419** is subsumed by this sync; our failed-project retry and worker-cleanup behavior remains ahead of upstream.
1010
- **Ahead of upstream — offer these back when syncing** (bug fixes, not adaptations): (1) `RuntimeManager.reconcile` resolves the document's core **before** sweeping pending uses (`planDocumentCore`), so a reconcile landing on the key a pending start is already producing adopts that start instead of tearing it down mid-`initialize` — the teardown made vscode-languageclient force-notify ("couldn't create connection to server") whenever the register-time pass, a detection change and `didOpen` landed inside one worker startup window (`tests/stacks/lint/runtimeManager.test.ts`). (2) `Rslint.close()` gives a still-Starting language client a bounded chance to settle before tearing down its transport, so a legitimate mid-start close (document closed during start, core key changed) stops cleanly instead of triggering the same force-notified toasts. (3) The registry-harness E2E gives its never-settling startup operation 500ms to begin and accepts only the in-flight timeout message, so a stalled runner cannot satisfy the assertion through the already-expired path (`e2e/lint/suite/registry-harness.test.ts`). (4) `Project.retryFailedConfig()` keeps a failed Rstest project and retries its config evaluation in place with one single-flight promise, so repeated dependency-change passes neither overlap workers nor repeat an unchanged not-installed warning. (5) `RuntimeManager` retires a stopped client even when its resolved key is unchanged. The existing closing barrier and pending-use adoption share one replacement across documents; running and starting clients remain untouched (`tests/stacks/lint/runtimeManager.test.ts`).
1111

12-
- **Targeted Rstest lifecycle port:** `RstestApi.getNormalizedConfig()` closes its worker in `finally`, including rejected config evaluation, matching web-infra-dev/rstest `packages/vscode/src/master.ts` at `d82db4fc31a61ee74b2a74917f14a458e1bca419`. This fixes a leak in our older copy; it is already fixed upstream. Dependency passes retry failed projects, including real config errors, while preserving single-flight loading and worker cleanup.
13-
1412
## The ten adaptations
1513

1614
1. **Shell activation** — stacks never self-activate; `register()` returns fast and never blocks on starting a server/worker.
@@ -50,11 +48,12 @@ One extension replacing the standalone `rstack.rslint` and `rstack.rstest` exten
5048
- **A Lint runtime lives as long as a document needs it, and a folder with none is `running: idle`.** Since the #1617 sync, `RuntimeManager` refcounts each runtime by open document: the first document to resolve a core starts one, the last to release it closes it, so a detected folder with nothing open holds zero workers and zero Go processes. That folder still reports `running` — with the detail `idle` — because it is live and will start a runtime on the next `didOpen`; do **not** add a `StackState` kind for it (the shell's status bar and `when` clauses read the kinds, and idle is not a kind of health). A folder's state is the **worst of** its runtimes plus any document whose core resolution currently fails (last-good: that document keeps the runtime it already had), so one failing core is never masked by a healthy sibling — the same invariant fmt pins across folders, applied inside one and across them alike (lint's rank table matches fmt's: `disabled` there means "a package is not installed" — no `rstack`, or no `@rslint/core` — not the kill switch). Dependency retries come only through the shell's detection pass: lockfile events are the low-latency path and ADR 0005's conditional poll covers unchanged lockfiles. The former lint-owned `node_modules/@rslint/core/package.json` watcher was removed because pnpm produced no event in either isolated or hoisted layout. Failures report through the status only: upstream's `window.showWarningMessage` is dropped, since stacks own no UI chrome. Consequently `whenStackActive('rslint')` means "the controller registered its folders", not "a server is up" — E2E suites open a document and await diagnostics.
5149
- The lint worker is deliberately vscode-free so it can move upstream whole. It takes explicit `--core` / `--config` native paths, writes logs only to stderr because stdout is LSP, and owns the Go child plus config/plugin lifecycles. Config edits use `rslint/configRefresh` with the same pinned path; a native ↔ bridged ownership change replaces the whole folder runtime because the supported config protocols lock that choice for the process lifetime.
5250
- The test × `rstack.config.*` bridge stays thin on purpose: it points the upstream machinery at rstack's shipped shim and lets the shim interpret the config inside the worker, same as the CLI. Bridged projects resolve `@rstest/core` from the resolved rstack package directory, mirroring lint, so rstack's dependency remains visible under isolated installs. Never re-implement rstack config semantics in the extension.
51+
- Rstest's upstream VS Code extension deep-imports `quoteFilter` from core to mark exact file filters. The published package does not export that helper, so our copy lives in `stacks/test/vendored/coreInternals.ts` beside the other core internals; keep it byte-identical when syncing filter behavior.
5352
- The fmt stack is an LSP client: one `rs fmt --lsp` server per detected workspace folder, spawned at the **folder root** even when a deeper `rstack.config.*` exists. Deepest-config-wins was removed deliberately — `rs fmt` loads one config from its cwd with no upward walk, so anchoring deeper made the editor disagree with `rs fmt` in a terminal; a subproject that needs its own fmt config becomes its own workspace folder. The stack registers **no** `DocumentFormattingEditProvider`: the client registers the provider from the server's `documentFormattingProvider` capability, and adding one by hand would double-register. A config create/change/delete **restarts** the owning folder's server (the server caches its config for its process lifetime and has no config-change message), which is also why the stack watches `RSTACK_CONFIG_GLOB` itself instead of relying on detection — a detection signature records which config files exist, not their contents. A detection pass keeps healthy servers and restarts failed ones in place (`isFailedFmtState`) — lockfile events notify even when the folder set is unchanged, precisely so a completed install or upgrade is retried without a manual restart. There is no stdin fallback below `SUPPORT_MATRIX.rstack`; that is a version gate, not an omission. **Nested workspace folders are a documented limitation, by decision**: when a folder and its subdirectory are both workspace folders and both detect fmt, the parent's per-folder selector also matches the nested folder's files, and which server VS Code hands the request to is not defined — the supported shape is subprojects as _sibling_ workspace folders (or only the subproject opened), not parent-plus-child. Routing (lint's `WorkspaceDocumentRouter` shape) was considered and deferred. Why all of it: `docs/adr/0002-fmt-lsp-on-user-node-runtime.md`.
5453
- fmt importing `stacks/lint/LanguageServerProcessOwner.ts` is not a refactor across the copies: that file has no lint imports and no lint behaviour, it only owns the native children of one language client — including the ones vscode-languageclient's automatic restart creates, which is exactly the leak an ad-hoc copy would reintroduce. Lint's `ManagedLanguageClient` is _restated_ in `stacks/fmt/index.ts` instead, because importing it from `Rslint.ts` would couple fmt to the lint stack's runtime graph. Keep that line where it is: shared process ownership yes, shared stack runtime no.
5554
- The VSIX is platform-targeted for exactly one reason: the test stack's AST collection loads a native parser binding. Do not add another native dependency — it multiplies the release matrix.
5655
- `shared/nodeResolution.ts` takes its shell, its cwd and its notify callback as options instead of importing `vscode` and a stack's `logger` singleton, unlike its neighbours. That is not stylistic: it keeps `resolveUserNode` a pure decision table over its inputs, which is what makes the case-by-case unit tests possible without a `vscode` stub. It sat in `stacks/test/` until fmt became the second stack running project code on a User Node runtime — the exact condition its old note named — and moved on that trigger, not before. Its host-scoped preflight memo is reset by the shell's restart pass only when **no** consumer stack (`USER_NODE_STACKS`) survives the pass — a single-stack `rstack.fmt.restart` beside a live Rstest controller deliberately keeps the memo, since the survivor's existing workers were built on that decision; the full `rstack.restart` always clears it. `stacks/fmt/binEntry.ts` and `stacks/fmt/status.ts` are separate pure modules for the same testability reason — `stacks/fmt/index.ts` evaluates `vscode`, and `status.ts` pins the fold's invariant (a healthy sibling folder never masks another folder's failure) in a unit test the single-folder E2E fixtures cannot.
57-
- The uniform Node floor deliberately exceeds `@rstest/core`'s own `engines` (`^20.19.0 || >=22.12.0`) and also mirrors the supported rstack line's `engines.node` (`rstack >= 0.7.0`). Do not specialise the floor per project — that was considered and rejected. Why, and what else was rejected: `docs/adr/0001-node-runtime-selection.md`.
56+
- The uniform Node floor deliberately exceeds `@rstest/core`'s own `engines` (`^20.19.0 || >=22.12.0`) and also mirrors the supported rstack line's `engines.node` (`rstack >= 0.7.6`). Do not specialise the floor per project — that was considered and rejected. Why, and what else was rejected: `docs/adr/0001-node-runtime-selection.md`.
5857
- Bun is not a supported worker runtime (it segfaults running `@rstest/core`). If that is ever revisited, gate it on an explicit setting — never on `bun.lock`, since bun-as-package-manager still runs the `rs` bin through its `#!/usr/bin/env node` shebang.
5958

6059
## Testing

‎packages/vscode/README.md‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,11 @@ A restart re-resolves every binary and package version and respawns every tool p
3636

3737
The project-resolved packages are checked against a support matrix at runtime; a mismatch shows up as the `version mismatch` status bar state.
3838

39-
| Package | Required |
40-
| -------------- | --------- |
41-
| `@rslint/core` | `>=0.8.0` |
42-
| `@rstest/core` | `>=0.6.0` |
43-
| `rstack` | `>=0.7.0` |
39+
| Package | Required |
40+
| -------------- | ---------- |
41+
| `@rslint/core` | `>=0.8.0` |
42+
| `@rstest/core` | `>=0.12.0` |
43+
| `rstack` | `>=0.7.6` |
4444

4545
## Auto-fix on save (Rslint)
4646

‎packages/vscode/e2e/fixtures/fmt-missing-config-dependency/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@
55
"type": "module",
66
"description": "E2E fixture: rs fmt config imports a package that is not installed.",
77
"dependencies": {
8-
"rstack": "0.7.4"
8+
"rstack": "0.7.6"
99
}
1010
}

‎packages/vscode/e2e/fixtures/rstack/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"type": "module",
66
"description": "E2E fixture: an rstack-cli project whose only config is `rstack.config.ts`, which lights all three stacks.",
77
"dependencies": {
8-
"rstack": "0.7.4"
8+
"rstack": "0.7.6"
99
},
1010
"devDependencies": {
1111
"jiti": "^2.0.0"

‎packages/vscode/e2e/fixtures/rstest-ownership/package.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"private": true,
55
"type": "module",
66
"dependencies": {
7-
"@rstest/core": "0.11.12",
8-
"rstack": "0.7.4"
7+
"@rstest/core": "0.12.0",
8+
"rstack": "0.7.6"
99
}
1010
}

‎packages/vscode/e2e/fixtures/rstest/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@
55
"type": "module",
66
"description": "E2E fixture: a project detected as Rstest only, installed from the npm registry.",
77
"dependencies": {
8-
"@rstest/core": "0.11.12"
8+
"@rstest/core": "0.12.0"
99
}
1010
}

‎packages/vscode/e2e/rstest/fixtures/workspace-1/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@
44
"private": true,
55
"description": "E2E fixture for the ported Rstest suites: upstream `tests/fixtures/workspace-1`, made self-contained on the published `@rstest/core`.",
66
"dependencies": {
7-
"@rstest/core": "0.11.12"
7+
"@rstest/core": "0.12.0"
88
}
99
}

‎packages/vscode/e2e/rstest/fixtures/workspace-2/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@
44
"private": true,
55
"description": "E2E fixture for the ported Rstest suites: upstream `tests/fixtures/workspace-2`. One install at the root serves both nested projects via the normal node_modules walk-up.",
66
"dependencies": {
7-
"@rstest/core": "0.11.12"
7+
"@rstest/core": "0.12.0"
88
}
99
}

‎packages/vscode/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,7 @@
434434
"@rslib/core": "^1.0.0",
435435
"@rslint/core": "^0.9.2",
436436
"@rstackjs/load-config": "^1.0.0",
437-
"@rstest/core": "^0.11.12",
437+
"@rstest/core": "^0.12.0",
438438
"@types/istanbul-lib-report": "^3.0.3",
439439
"@types/mocha": "^10.0.10",
440440
"@types/node": "^22.20.2",

‎packages/vscode/src/shared/versionCheck.ts‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ import { readPackageJson } from './packageResolve';
99
*
1010
* Launch floors (verified against npm):
1111
* - `@rslint/core >= 0.8.0` — explicit protocol-2 config selection.
12-
* - `@rstest/core >= 0.6.0` — the existing `MIN_CORE_VERSION` upstream.
13-
* - `rstack >= 0.7.0` — the first release whose lint shim sets `basePath` and
14-
* itself pins `@rslint/core` 0.9.0, preserving project-relative config
15-
* paths (#431).
12+
* - `@rstest/core >= 0.12.0` — the first release exporting the public
13+
* `@rstest/core/api` createRstest instance API; older cores lack it.
14+
* - `rstack >= 0.7.6` — the first release pinning `@rstest/core ~0.12.0`
15+
* (https://github.com/rstackjs/rstack-cli/releases/tag/v0.7.6).
16+
* Bridged `rstack.config.*` projects resolve core from the rstack package
17+
* directory, so earlier releases carry a core below the `@rstest/core` floor.
1618
*
1719
* The rstack floor is **uniform across consumers by decision**: lint, Rstest
1820
* and fmt all check the same entry, so "which rstack does the extension
@@ -21,8 +23,8 @@ import { readPackageJson } from './packageResolve';
2123
*/
2224
export const SUPPORT_MATRIX = {
2325
'@rslint/core': '>=0.8.0',
24-
'@rstest/core': '>=0.6.0',
25-
rstack: '>=0.7.0',
26+
'@rstest/core': '>=0.12.0',
27+
rstack: '>=0.7.6',
2628
} as const;
2729

2830
export type SupportedPackage = keyof typeof SUPPORT_MATRIX;

0 commit comments

Comments
 (0)