Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
6 changes: 3 additions & 3 deletions .github/renovate.json5
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
'schedule:weekly',
'helpers:pinGitHubActionDigests',
],
// E2E fixtures deliberately pin the published toolchain versions the suites
// run against; bumping them is a manual, E2E-verified decision.
ignorePaths: ['**/node_modules/**', 'packages/vscode/e2e/**'],
// E2E fixtures pin published packages with committed lockfiles; their Rstack
// dependencies are updated by the `rstack toolchain` group below.
ignorePaths: ['**/node_modules/**'],
packageRules: [
// Use chore as semantic commit type for commit messages
{
Expand Down
7 changes: 2 additions & 5 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -150,12 +150,9 @@ tests-dist/
# F5 playground's materialized Node (see .vscode/tasks.json "playground node")
packages/vscode/.playground/

# E2E fixtures install published npm versions on demand; only
# their manifests and configs are tracked.
# E2E fixtures install published npm versions from committed lockfiles;
# their node_modules directories stay disposable.
packages/vscode/e2e/fixtures/*/node_modules/
packages/vscode/e2e/fixtures/*/pnpm-lock.yaml
packages/vscode/e2e/lint/fixtures/pnpm-lock.yaml
packages/vscode/e2e/rstest/fixtures/*/pnpm-lock.yaml

# Build-time copy of the workspace root LICENSE (see rslib.config.mts)
packages/vscode/LICENSE
Expand Down
6 changes: 3 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Thanks for your interest in contributing to Rstack Editor!

## Setup

- Node.js: the version is pinned in [`.nvmrc`](./.nvmrc) (`nvm use` / `fnm use`). Node >= 22.12 is required.
- Node.js: the version is pinned in [`.nvmrc`](./.nvmrc) (`nvm use` / `fnm use`). Node `^22.18.0 || >=24.3.0` is required.
- pnpm: pinned via the `packageManager` field — run `corepack enable pnpm` once and the right version is used automatically.

```bash
Expand All @@ -19,9 +19,9 @@ pnpm install # also installs the git hooks (rs hooks)
| `pnpm lint` | Lint + type check (`rs lint --type-check`) |
| `pnpm fmt` | Format the repo (`rs fmt`) |
| `pnpm test:unit` | Unit tests |
| `pnpm test:e2e` | Full E2E chain (installs fixtures, launches a real VS Code) |
| `pnpm test:e2e` | Full E2E chain (installs fixtures from committed lockfiles, launches a real VS Code) |

To try the extension: press F5 in VS Code at the repo root — the playground launch config starts a watch build, lets you pick a fixture project, and opens an Extension Development Host on it. Run `pnpm --filter rstack test:e2e:fixtures` once beforehand to install the fixture dependencies.
To try the extension: press F5 in VS Code at the repo root — the playground launch config starts a watch build, lets you pick a fixture project, and opens an Extension Development Host on it. Run `pnpm --filter rstack test:e2e:fixtures` once beforehand to install the fixture dependencies from their committed lockfiles.

## Submitting changes

Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ Or search for `rstack.rstack` in the editor's Extensions view.

The extension resolves the tools from your project's `node_modules` and checks them against a support matrix at runtime:

| Package | Required |
| -------------- | --------- |
| `@rslint/core` | `>=0.8.0` |
| `@rstest/core` | `>=0.6.0` |
| `rstack` | `>=0.6.1` |
| Package | Required |
| -------------- | ----------- |
| `@rslint/core` | `>=0.9.0` |
| `@rstest/core` | `>=0.11.11` |
| `rstack` | `>=0.7.2` |

## Documentation

Expand Down
6 changes: 3 additions & 3 deletions docs/adr/0003-lint-through-editor-worker.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ status: accepted

Rslint's language server is two halves: the Go process (`rslint --lsp`) lints natively, but it hands config evaluation (`rslint/loadConfigs`, `activateConfigs`, `commit`/`abort`) and JS plugin rules (`rslint/pluginLint`) back to its client over reverse requests. Upstream's VS Code extension is that client, and so — as a near-verbatim copy — was ours: the project's `rslint.config.*` was evaluated inside the extension host, on the VS Code Node runtime, with a cwd that means nothing (ADR 0001's lint entry). Linting from a Rstack config (`define.lint()`) forces the issue: `rs lint`'s answer is a shim rstack ships (`<rstack>/dist/rslintConfig.js`, treated as a stable path by agreement with rstack-cli) that calls `loadRstackConfig()` and finds `rstack.config.*` from **the evaluating process's cwd**. Evaluated in the extension host, it finds nothing. The first attempt (PR #10) worked around that by writing a **generated shim** into the project with the absolute config path baked in; it was rejected — the editor must not manufacture bridging artifacts.

**Decision.** The extension ships a **lint worker**: a vscode-free Node script, run once per lint server on a **User Node runtime** (ADR 0001's floor and candidate order, `rstack.nodeExecutable` as the shared escape hatch) with its cwd at the workspace folder root. The worker spawns the Go `rslint --lsp` of the resolved `@rslint/core`, proxies LSP over stdio to the extension, and answers Go's reverse requests itself with that core's `ConfigModuleHost` and `createPluginLintHost`. Its entry is `--lsp [--config <absolute module>]`: with `--config` it pins the server to that module through the protocol-2 `configPath` of `rslint/configRefresh` (`@rslint/core >= 0.8.0`, rslint #1630); without it the server does its ordinary automatic discovery. A **bridged folder** runs the worker with `--config <rstack>/dist/rslintConfig.js`; a **native folder** runs it without. From the extension's side, lint is now the shape fmt already has: a thin language client per server, and no project code loaded in the extension host — which retires the lint entry on ADR 0001's debt list.
**Decision.** The extension ships a **lint worker**: a vscode-free Node script, run once per lint server on a **User Node runtime** (ADR 0001's floor and candidate order, `rstack.nodeExecutable` as the shared escape hatch) with its cwd at the workspace folder root. The worker spawns the Go `rslint --lsp` of the resolved `@rslint/core`, proxies LSP over stdio to the extension, and answers Go's reverse requests itself with that core's `ConfigModuleHost` and `createPluginLintHost`. Its entry is `--lsp [--config <absolute module>]`: with `--config` it pins the server to that module through the protocol-3 `configPath` of `rslint/configRefresh` (`@rslint/core >= 0.9.0`; the explicit path was introduced in rslint #1630); without it the server does its ordinary automatic discovery. A **bridged folder** runs the worker with `--config <rstack>/dist/rslintConfig.js`; a **native folder** runs it without. From the extension's side, lint is now the shape fmt already has: a thin language client per server, and no project code loaded in the extension host — which retires the lint entry on ADR 0001's debt list.

## Considered options

Expand All @@ -23,9 +23,9 @@ Rslint's language server is two halves: the Go process (`rslint --lsp`) lints na
## Consequences

- **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 `^22.18.0 || >=23.6.0`. 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 "latest release only" rule: `@rslint/core >= 0.8.0` (protocol 1 support removed) and `rstack >= 0.6.1` toolchain-wide — `rstack` 0.5.2 still depends on `@rslint/core ~0.7.3`, and one answer to "which rstack does the extension support" is worth more than keeping 0.5.x users' tests running.
- **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 "latest release only" rule: `@rslint/core >= 0.9.0` and `rstack >= 0.7.2` toolchain-wide; 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.
- **Ownership is per folder, native wins.** One server holds one config choice for its lifetime (protocol 2 locks `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.
- **Ownership is per folder, native wins.** One server holds one config choice for its lifetime (protocol 3 locks `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.** Bridged folder: no `rstack` → `disabled`; `rstack` or the chained `@rslint/core` below floor, or no Node clearing the floor → `version mismatch`; worker or Go dying → `crashed`. Native folder missing `@rslint/core` stays `crashed` — the user asked for Rslint by name.
- The lint copy diverges further from upstream: the reverse-request adapter and plugin pool move into the worker unchanged in logic, and the extension-side `Rslint.ts` keeps only the language-client half. Recorded as an adaptation in `packages/vscode/AGENTS.md`.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@
},
"devDependencies": {
"bumpp": "^11.1.0",
"rstack": "^0.6.4"
"rstack": "^0.7.2"
},
"packageManager": "pnpm@11.20.0",
"engines": {
"node": ">=22.12.0",
"node": "^22.18.0 || >=24.3.0",
"pnpm": ">=11.0.0"
}
}
Loading
Loading