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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/adr/0003-lint-through-editor-worker.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Rslint's language server is two halves: the Go process (`rslint --lsp`) lints na

- **Lint gains a Node floor it never had.** A native folder that lints on VS Code's Node today reports `version mismatch` and starts nothing when no User Node runtime clears the ADR 0001 floor. Accepted deliberately: one worker, one path, one floor (ADR 0001 already rejected per-project floors), and this is the debt that ADR named.
- **Resolution follows one chain, mirroring `rs lint`.** For a bridged folder: `rstack` from the folder root → `@rslint/core` from rstack's directory (the transitive copy `rs lint` itself imports; a pnpm project declaring only `rstack` cannot resolve `@rslint/core` from its root) → the Go binary through that core's `resolveRslintBinary()`. For a native folder the chain starts at `@rslint/core` from the **document's own directory** (the #1617 sync; before it, from the folder root). The extension walks the chain as far as the **core directory** — `fs.stat`, `package.json` reads and semver comparisons, no project code loaded, so inside the load bound and legitimately on the VS Code Node runtime, exactly as fmt resolves the `rs` bin — and gates there; the worker receives `--core <dir> [--config <shim>]` and takes the last hop itself, calling that core's `resolveRslintBinary()` on the User Node runtime, since it is a JS export of the project's package. Floors follow the evidence in `SUPPORT_MATRIX`: `@rslint/core >= 0.8.0` is the first release with explicit protocol-2 config selection, while `rstack >= 0.7.0` is the first release whose shim supplies `basePath` and itself pins `@rslint/core` 0.9.0; one answer to "which rstack does the extension support" is worth more than keeping older releases running.
- **One override, and it names a core, not a binary.** `rstack.rslint.binPath` / `customBinPath` are removed in favour of `rstack.rslint.corePath` — the setting upstream introduced in rslint #1617: a path to an `@rslint/core` package directory, resource-scoped, from which the binary, config host, protocol version and plugin host all derive. In a bridged folder it overrides the rstack → `@rslint/core` hop only; the shim stays rstack's. A binary chosen independently of its core cannot be supported: the two must speak the same protocol. The rest of #1617 — per-document core resolution, one runtime per physical installation — has since been synced (issue #13): a **Lint runtime** is now one Rslint core inside one workspace folder, resolved per open document and refcounted by it, so a folder runs as many workers as its files have distinct cores (a bridged folder always exactly one, rstack's) and none at all while nothing is open. The worker never noticed: it still takes explicit `--core` / `--config` paths, which is precisely why that change did not touch it.
- **One override, and it names a core, not a binary.** `rstack.rslint.binPath` / `customBinPath` are removed in favour of `rstack.rslint.corePath` — the setting upstream introduced in rslint #1617: a path to an `@rslint/core` package directory (or its `package.json`), resource-scoped, from which the binary, config host, protocol version and plugin host all derive. In a bridged folder it overrides the rstack → `@rslint/core` hop only; the shim stays rstack's. A binary chosen independently of its core cannot be supported: the two must speak the same protocol. The rest of #1617 — per-document core resolution, one runtime per physical installation — has since been synced (issue #13): a **Lint runtime** is now one Rslint core inside one workspace folder, resolved per open document and refcounted by it, so a folder runs as many workers as its files have distinct cores (a bridged folder always exactly one, rstack's) and none at all while nothing is open. The worker never noticed: it still takes explicit `--core` / `--config` paths, which is precisely why that change did not touch it.
- **Ownership is per folder, native wins.** One server holds one config choice for its lifetime (the supported config protocols lock `configPath` per process), and explicit and automatic modes cannot mix, so a folder is bridged only when no `rslint.config.*` exists anywhere in it and a `rstack.config.*` sits at its root; a subdirectory `rstack.config.*` lights nothing (`rs lint` in a terminal reads its cwd only — the same reason ADR 0002 rejected deepest-config-wins for fmt). Detection lights a bridged folder on the file's presence and never reads it: a `rstack.config.*` without `define.lint()` runs an empty config, as `rs lint` does.
- **Config changes refresh, mode changes restart.** Rslint has a live refresh (`rslint/configRefresh` with the same `configPath`), unlike `rs fmt --lsp`, so the extension keeps its watcher-driven refresh — extended, for a bridged folder, with the root `rstack.config.*` — and the worker re-stamps `protocolVersion` and its `configPath` on every refresh (the extension does not know either). Only a native ↔ bridged flip, or a dependency change the refresh cannot absorb, restarts the server. This is the "diverge only when the tool forces it" rule: rslint can refresh, fmt cannot.
- **Failure states mirror fmt.** No `rstack`, no `@rslint/core`, or a config importing an absent package → `disabled`; `rstack` or the chained `@rslint/core` below floor, or no Node clearing the floor → `version mismatch`; worker or Go dying → `crashed`. Config-import failures are classified where the worker still has the loader's structured error and carried to the editor as data; the live Go server remains available for a later refresh.
Expand Down
2 changes: 1 addition & 1 deletion packages/vscode/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ One extension replacing the standalone `rstack.rslint` and `rstack.rstest` exten
## The eleven adaptations

1. **Shell activation** — stacks never self-activate; `register()` returns fast and never blocks on starting a server/worker.
2. **Namespace** — everything user-visible is `rstack.*`. Legacy `rslint.*` / `rstest.*` settings and command ids are not read, aliased or migrated (breaking old settings and keybindings was an accepted cost).
2. **Namespace** — everything user-visible is `rstack.*`. Legacy `rslint.*` / `rstest.*` settings and command ids are not read, aliased or migrated (breaking old settings and keybindings was an accepted cost). Both `rstack.rslint.corePath` and `rstack.rstest.corePath` accept a package directory or its `package.json`, resolving relative paths from the workspace folder; only Rstest expands `${workspaceFolder}`. Preserve these unified override names without legacy aliases when syncing.
3. **Resolve-from-project** — no tool binaries or tool packages in the VSIX; everything resolves from the user's project so the editor runs the CLI's exact versions. Version floors surface as a status, never a crash. All cooperating lint pieces (binary, config loader, plugin host) must come from one resolution root. Enforced by lint: `@typescript-eslint/no-restricted-imports` in the root `rstack.config.ts` rejects any non-type import of `@rslint/core`, `@rstest/core`, `rstack` or `jiti` under `src/` — types only at compile time, runtime modules through explicit project paths.
4. **Status aggregation** — stacks own no UI chrome; they report to the shell's single status bar item, which always exists. Upstream's plugin-host failure toast becomes a status verdict (adaptation 7). Rslint LSP tracing shares the **Rstack: Rslint** Output channel to preserve the four-channel cap; see `stacks/lint/index.ts`. In CI the test stack's `MasterLogger` also mirrors every entry to stderr (`RSTACK_E2E_MIRROR_LOGS=1`, set by `e2e/rstest/runTest.ts`) — the output channel is unreadable there; rationale in `stacks/test/logger.ts`.
5. **Worker-cwd decoupling** (test) — a project's cwd is explicit, not derived from the config file path; for native configs behavior stays byte-identical to upstream.
Expand Down
6 changes: 3 additions & 3 deletions packages/vscode/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,13 @@ All settings live under the unified `rstack.*` namespace. There are no `rslint.*
| --- | --- | --- |
| `rstack.nodeExecutable` | — | Node binary used for the processes that load your project: the lint worker, test worker and `rs fmt` language server. Empty means the extension picks one (`PATH` first, then the `node` your interactive shell resolves). |
| `rstack.rslint.enable` | `true` | Enable/disable the Rslint integration. |
| `rstack.rslint.corePath` | — | Path to an `@rslint/core` package directory; relative paths resolve from the workspace folder. |
| `rstack.rslint.corePath` | — | Path to an `@rslint/core` package directory or its `package.json`; relative paths resolve from the workspace folder. Use only to override automatic resolution. |
| `rstack.rslint.trace.server` | `off` | LSP trace level (`off` / `messages` / `verbose`). |
| `rstack.rstest.enable` | `true` | Enable/disable the Rstest integration. |
| `rstack.rstest.configFileGlobPattern` | `["**/rstest.config.{mjs,ts,js,cjs,mts,cts}"]` | Glob patterns used to discover config files. |
| `rstack.rstest.testCaseCollectMethod` | `ast` | `ast` (fast) or `runtime` (supports dynamic test generation). |
| `rstack.rstest.applyDiagnostic` | `true` | Show diagnostics in the editor and Problems panel for failures. |
| `rstack.rstest.rstestPackagePath` | — | Explicit `@rstest/core` `package.json`, last-resort override. |
| `rstack.rstest.corePath` | — | Path to an `@rstest/core` package directory or its `package.json`; relative paths resolve from the workspace folder and `${workspaceFolder}` is supported. Use only to override automatic resolution, for both workers and Run in Terminal. |
| `rstack.rstest.nodeExecArgs` | `[]` | Extra Node args for the test worker. |
| `rstack.rstest.nodeEnv` | `null` | Extra env for the test worker. |
| `rstack.rstest.debugNodeEnv` | `null` | Extra env when debugging tests. |
Expand All @@ -101,7 +101,7 @@ Formatting runs one `rs fmt` language server per workspace folder, which loads `

## Coming from the standalone extensions

Disable or uninstall the retired `rstack.rslint` / `rstack.rstest` extensions so only one copy of each tool runs. Settings and keybindings are not carried over from them: re-enter your settings under the `rstack.*` keys listed above and re-bind any keybinding to the new `rstack.*` command ids. Legacy `rslint.binPath` / `rslint.customBinPath` have no equivalent — use `rstack.rslint.corePath` to point at an `@rslint/core` package directory if you still need an override.
Disable or uninstall the retired `rstack.rslint` / `rstack.rstest` extensions so only one copy of each tool runs. Settings and keybindings are not carried over from them: re-enter your settings under the `rstack.*` keys listed above and re-bind any keybinding to the new `rstack.*` command ids. Legacy `rslint.binPath` / `rslint.customBinPath` have no equivalent — use `rstack.rslint.corePath` to point at an `@rslint/core` package directory or its `package.json` if you still need an override.

## Community

Expand Down
25 changes: 15 additions & 10 deletions packages/vscode/e2e/lint/suite-jsconfig/core-resolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,17 +257,22 @@ suite('local core resolver', () => {
);
const document = await createSource('src/index.ts');

const resolved = await new CoreResolver().resolve(
document,
temporaryWorkspaceFolder(),
{ mode: 'native', corePath: 'vendor/rslint-core' },
);
for (const corePath of [
'vendor/rslint-core',
'vendor/rslint-core/package.json',
]) {
const resolved = await new CoreResolver().resolve(
document,
temporaryWorkspaceFolder(),
{ mode: 'native', corePath },
);

await assertSamePhysicalPath(
resolved.installation.packageDirectory,
packageDirectory,
);
assert.strictEqual(resolved.installation.version, '3.0.0');
await assertSamePhysicalPath(
resolved.installation.packageDirectory,
packageDirectory,
);
assert.strictEqual(resolved.installation.version, '3.0.0');
}
});

test('rejects a core package below the supported version floor', async () => {
Expand Down
7 changes: 3 additions & 4 deletions packages/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@
"type": "string",
"default": "",
"scope": "resource",
"markdownDescription": "Path to an `@rslint/core` package directory. Relative paths are resolved from the workspace folder. When empty, a native folder uses the nearest installation above each file and a bridged folder (root `rstack.config.*`) uses the one its `rstack` depends on."
"markdownDescription": "Path to an `@rslint/core` installation, either its package directory or its `package.json` file. Relative paths are resolved from the workspace folder. Use this override only when automatic resolution does not select the installation you need. When empty, a native folder uses the nearest installation above each file and a bridged folder (root `rstack.config.*`) uses the one its `rstack` depends on."
},
"rstack.rslint.trace.server": {
"order": 2,
Expand All @@ -175,11 +175,11 @@
"scope": "window",
"markdownDescription": "Enable the Rstest test explorer for detected workspace folders. This is a window-level kill switch; which folders run Rstest is decided by detection."
},
"rstack.rstest.rstestPackagePath": {
"rstack.rstest.corePath": {
Comment thread
fi3ework marked this conversation as resolved.
"order": 1,
"type": "string",
"scope": "resource",
"markdownDescription": "The path to a `package.json` file of an Rstest installation (usually inside `node_modules`) in case the extension cannot find it. It will be used to resolve Rstest API paths. This should be used as a last resort fix. Supports the `${workspaceFolder}` placeholder."
"markdownDescription": "Path to an `@rstest/core` installation, either its package directory or its `package.json` file. Relative paths are resolved from the workspace folder. Supports the `${workspaceFolder}` placeholder. Use this override only when automatic resolution does not select the installation you need. It applies to both worker and Run in Terminal resolution."
},
"rstack.rstest.configFileGlobPattern": {
"order": 2,
Expand Down Expand Up @@ -450,7 +450,6 @@
"vscode": "^1.97.0"
},
"icon": "icon.png",
"preview": true,
"capabilities": {
"virtualWorkspaces": {
"supported": "limited",
Expand Down
7 changes: 5 additions & 2 deletions packages/vscode/src/stacks/lint/resolution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,11 @@ function resolveConfiguredCore(
folderRoot: string,
configuredPath: string,
): PackageLocation {
const directory = path.resolve(folderRoot, configuredPath);
const packageJsonPath = path.join(directory, 'package.json');
const resolvedPath = path.resolve(folderRoot, configuredPath);
const packageJsonPath = configuredPath.endsWith('package.json')
? resolvedPath
: path.join(resolvedPath, 'package.json');
const directory = path.dirname(packageJsonPath);
try {
if (!fs.statSync(packageJsonPath).isFile()) throw new Error('not a file');
} catch (error) {
Expand Down
4 changes: 2 additions & 2 deletions packages/vscode/src/stacks/test/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ export const CONFIG_SECTION = 'rstack.rstest';
// Centralized configuration types for the extension.
// Add new keys here to extend configuration in a type-safe way.
const configSchema = object({
// The path to a package.json file of a Rstest executable.
// The path to an @rstest/core package directory or its package.json.
// Used as a last resort if the extension cannot auto-detect @rstest/core.
rstestPackagePath: fallback(optional(string()), undefined),
corePath: fallback(optional(string()), undefined),
nodeExecArgs: fallback(array(string()), []),
nodeEnv: fallback(optional(record(string(), string())), undefined),
debugNodeEnv: fallback(optional(record(string(), string())), undefined),
Expand Down
4 changes: 2 additions & 2 deletions packages/vscode/src/stacks/test/coreResolution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
* embeds a multi-line require stack and says nothing about what to do. An
* uninstalled core is the normal state of a freshly cloned repository and is
* resolved for every config file without the user asking, so it is only
* logged; a `rstestPackagePath` that does not resolve is a setting the user
* logged; a `corePath` that does not resolve is a setting the user
* has to fix, so it is notified.
*/

Expand Down Expand Up @@ -56,5 +56,5 @@ export function isModuleNotFoundError(
export function formatConfiguredCoreNotFoundMessage(
configuredPackagePath: string,
): string {
return `Cannot find "@rstest/core" at the configured "rstack.rstest.rstestPackagePath": ${configuredPackagePath}. Update the setting to point at an installed "@rstest/core" package.json.`;
return `Cannot find "@rstest/core" at the configured "rstack.rstest.corePath": ${configuredPackagePath}. Update the setting to point at an installed "@rstest/core" package directory or package.json.`;
}
Loading
Loading