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
6 changes: 3 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ jobs:
run:
shell: bash
env:
# rslib.config.mts keys the staged `@yuku-core/binding-*` napi payload
# rslib.config.mts keys the staged `@swc-next/parser-binding-*` napi payload
# off this variable (`linux-*` → `-gnu`, `alpine-*` → `linux-*-musl`).
VSCE_TARGET: ${{ matrix.vsce-target }}
steps:
Expand All @@ -88,8 +88,8 @@ jobs:
cache-dependency-path: pnpm-lock.yaml

# `supportedArchitectures` must be set BEFORE `pnpm install` so pnpm also
# fetches the cross-target optional `@yuku-core/binding-*` package.
# musl is always listed so the alpine-* legs get Yuku's `-musl` binding;
# fetches the cross-target optional `@swc-next/parser-binding-*` package.
# musl is always listed so the alpine-* legs get SWC Next's `-musl` binding;
# on the glibc Linux legs that is a few extra optional downloads, and the
# macOS/Windows legs are unaffected because pnpm still filters by os.
- name: Install Dependencies
Expand Down
1 change: 1 addition & 0 deletions packages/vscode/.vscodeignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
!dist/**/*.js
!dist/rstack-resolve-hook.mjs
!dist/**/*.node
!dist/node_modules/@swc-next/parser-binding-*/package.json
!dist/*.licenses.json
!icon.png
!LICENSE
Expand Down
8 changes: 5 additions & 3 deletions packages/vscode/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ One extension replacing the standalone `rstack.rslint` and `rstack.rstest` exten
## The copies are intentional

- `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.
- The copies diverge from upstream in exactly twelve ways (the "adaptations" below). When syncing upstream, preserve them. A thirteenth divergence is either a bug or must be added to this list.
- The copies diverge from upstream in exactly thirteen ways (the "adaptations" below). When syncing upstream, preserve them. A fourteenth divergence is either a bug or must be added to this list.
- **Tracked upstream state.** `stacks/lint` tracks web-infra-dev/rslint `packages/vscode-extension` at **e5d76242** (0.9.3); `stacks/test` tracks web-infra-dev/rstest `packages/vscode` at **e1607861** (post-0.12.2 main). Additional ports: Rslint **84f9c9b5** trace wording and **72cd2b1f** fixAll waits; Rstest **988f8e1d** per-bundle license notices, with explicit native-binding inclusion and no network license-text replenishment. We replace upstream's plugin-host failure toast with status (adaptations 4/7). Diff `CoreResolver.ts`, `RuntimeManager.ts`, `WorkspaceDocumentRouter.ts`, and `Rslint.ts` on future syncs. The rstest#1888 IPC port uses default JSON instead of advanced serialization: values must survive a JSON round-trip, and both `createBirpc` calls spread `rpcErrorCodec` (`stacks/test/shared/rpc.ts`) to preserve thrown errors. Failed-project retry and worker cleanup remain ahead of upstream.
- **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`). (6) The lint E2E runner (`e2e/lint/runSuite.ts`) gives Mocha a 120s default timeout instead of 2000ms, so hooks doing real workspace I/O (the router suite's `suiteSetup`) do not time out on Windows CI runners.

## The twelve adaptations
## The thirteen 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). 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. Every consumer of the `rstack` package (fmt, bridged lint, bridged test) also reads the shared `rstack.rstackCli.packagePath` (`shared/rstackPackage.ts`), which replaces the walk-up for `rstack` itself; the tool core is still resolved from the directory it selects unless `corePath` is set. Like `corePath`, it changes resolution only, never detection. While it is set, every process that loads `rstack.config.*` (fmt server, bridged lint and Rstest workers, Rstest's Run in Terminal) also gets a resolve hook as a `--import` node arg so the config's own `import 'rstack'` reaches the same package (`rstackResolveHookArgs` in `shared/rstackPackage.ts`). The hook bridges to Node's resolver; never replace it with `NODE_PATH`, `JITI_ALIAS` or anything else that imitates a package-manager shim or names files inside `rstack`.
Expand All @@ -27,6 +27,8 @@ One extension replacing the standalone `rstack.rslint` and `rstack.rstest` exten

12. **Function-title discovery** (test) — `stacks/test/parserTest.ts` replaces upstream's scope pre-pass with walk-order bindings and assignment tracking for runtime-name parity. Unbound identifiers (including default imports) and non-computed members use best-effort identifier/property names rather than giving up with `unnamed test`; known unknown bindings still stay unnamed. Re-apply this adaptation when syncing `parserTest.ts`, preserving the upstream tests and local runtime-parity cases.

13. **SWC Next parser** (test) — static AST collection uses `@swc-next/parser` instead of upstream's `yuku-parser`. Preserve the parser migration and target-specific native binding packaging when syncing the test stack.

## Rules

- **Pre-1.0.0 the extension breaks freely.** No compatibility is owed with earlier unpublished states of this extension — settings, command ids and behavior may change without deprecation paths, and dead compat code for them is removed, not kept. No settings migration exists either — not for earlier states of this extension, and not for the two retired standalone extensions (removed in #15; users re-enter their settings under `rstack.*`). Testing and fixtures track only the latest published releases, pinned exactly and bumped by Renovate; a green E2E run speaks only for those releases. `SUPPORT_MATRIX` floors are the minimum versions the extension accepts: each entry is the lowest release evidence shows works with the current code, and its comment records that evidence. Move a floor only when a change makes older releases stop working, never because a devDependency or fixture moved. Raising a floor needs no transition story; the status names the required version.
Expand Down Expand Up @@ -58,7 +60,7 @@ One extension replacing the standalone `rstack.rslint` and `rstack.rstest` exten
- 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.
- 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`.
- 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.
- 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. The `alpine-*` legs exist because VS Code installs a platform-specific extension only on an exact target match, so without them VS Code Server on Alpine (musl) cannot install the extension at all; they need Yuku's `-musl` binding, which pnpm installs only when `supportedArchitectures` lists `libc: musl`.
- 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. The `alpine-*` legs exist because VS Code installs a platform-specific extension only on an exact target match, so without them VS Code Server on Alpine (musl) cannot install the extension at all; they need SWC Next's `-musl` binding, which pnpm installs only when `supportedArchitectures` lists `libc: musl`.
- `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.
- 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`.
- 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.
Expand Down
4 changes: 2 additions & 2 deletions packages/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,7 @@
"@rslint/core": "^0.9.5",
"@rstackjs/load-config": "^1.0.2",
"@rstest/core": "^0.12.4",
"@swc-next/parser": "^0.4.1",
"@types/istanbul-lib-report": "^3.0.3",
"@types/mocha": "^10.0.10",
"@types/node": "^22.20.5",
Expand All @@ -456,8 +457,7 @@
"valibot": "^1.5.0",
"vscode-jsonrpc": "^8.2.1",
"vscode-languageclient": "^9.0.1",
"webpack-license-plugin": "^4.5.1",
"yuku-parser": "^0.19.0"
"webpack-license-plugin": "^4.5.1"
},
"engines": {
"vscode": "^1.97.0"
Expand Down
31 changes: 14 additions & 17 deletions packages/vscode/rslib.config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,23 @@ import { fileURLToPath } from 'node:url';
import { defineConfig, type LibConfig, rspack } from '@rslib/core';
import WebpackLicensePlugin from 'webpack-license-plugin';

const require = createRequire(import.meta.url);
const rootDir = path.dirname(fileURLToPath(import.meta.url));

// The VSIX is platform-targeted for exactly one reason: Rstest's AST test-case
// collection loads the `yuku-core` napi binding (via `yuku-parser`). Everything
// else in this extension is platform neutral (the Rslint Go binary and its napi
// parser are resolved from the project at runtime and never ship in the VSIX).
// The test stack's AST collection is the only native dependency in the VSIX.
// On musl Linux (Alpine), set `VSCE_TARGET=alpine-<arch>` explicitly.
const vsceTarget =
process.env.VSCE_TARGET ?? `${process.platform}-${process.arch}`;
// Yuku's loader resolves `binding-${platform}-${arch}${libc}`; the Linux VSIX
// targets differ only by libc: `linux-*` is glibc, `alpine-*` is musl.
const LINUX_LIBC: Record<string, string> = { linux: 'gnu', alpine: 'musl' };
const [targetOs, targetArch] = vsceTarget.split('-');
const libc = LINUX_LIBC[targetOs];
const yukuBindingSuffix = libc ? `linux-${targetArch}-${libc}` : vsceTarget;
const yukuCoreRequire = createRequire(
createRequire(require.resolve('yuku-parser')).resolve('yuku-core'),
);
// Yuku computes this package name at runtime, so Rspack cannot discover it.
const yukuBindingPackage = `@yuku-core/binding-${yukuBindingSuffix}`;
const yukuBindingPath = yukuCoreRequire.resolve(yukuBindingPackage);
const parserBindingSuffix = libc
? `linux-${targetArch}-${libc}`
: targetOs === 'win32'
? `${vsceTarget}-msvc`
: vsceTarget;
const parserRequire = createRequire(import.meta.resolve('@swc-next/parser'));
const parserBindingPackage = `@swc-next/parser-binding-${parserBindingSuffix}`;
const parserBindingPath = parserRequire.resolve(parserBindingPackage);

/**
* Packages that are always resolved from the user's project at runtime, never
Expand All @@ -36,6 +31,8 @@ const RUNTIME_RESOLVED_PACKAGES = /^(@rslint\/core|@rstest\/core|jiti)(\/|$)/;

const externals: NonNullable<NonNullable<LibConfig['output']>['externals']> = [
{ vscode: 'commonjs vscode' },
// Keep native package loading in Node; ship only the selected VSIX target.
/^@swc-next\/parser-binding-/,
({ request }, callback) => {
if (request && RUNTIME_RESOLVED_PACKAGES.test(request)) {
return callback(undefined, `commonjs ${request}`);
Expand Down Expand Up @@ -69,8 +66,8 @@ const libs: LibConfig[] = [
new rspack.CopyRspackPlugin({
patterns: [
{
from: yukuBindingPath,
to: `${yukuBindingPackage}/${path.basename(yukuBindingPath)}`,
from: path.dirname(parserBindingPath),
to: `node_modules/${parserBindingPackage}`,
},
{
// Preloaded via a `--import` node arg, so a file of its own.
Expand Down Expand Up @@ -158,7 +155,7 @@ export default defineConfig(({ envMode }) => {
outputFilename: `${environment.name}.licenses.json`,
includePackages: () =>
environment.name === 'extension'
? [path.dirname(yukuBindingPath)]
? [path.dirname(parserBindingPath)]
: [],
}),
);
Expand Down
4 changes: 2 additions & 2 deletions packages/vscode/scripts/packageTargets.mjs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
// Builds and packages one platform-targeted VSIX per supported target.
//
// The extension is platform targeted for a single reason: Rstest's AST test
// collection loads the `yuku-core` napi binding (via `yuku-parser`), which
// collection loads the `@swc-next/parser` napi binding, which
// `rslib.config.mts` stages into `dist/` based on `VSCE_TARGET`.
//
// Packaging a target other than the host platform requires the matching
// optional `@yuku-core/binding-*` package to be installed locally, e.g.:
// optional `@swc-next/parser-binding-*` package to be installed locally, e.g.:
//
// pnpm config set --location=project --json supportedArchitectures \
// '{"cpu":["current","arm64"]}' && pnpm install
Expand Down
4 changes: 2 additions & 2 deletions packages/vscode/src/stacks/test/parserTest.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type Node, parse } from 'yuku-parser';
import { type Node, parseSync } from '@swc-next/parser';

export class Range {
constructor(
Expand All @@ -25,7 +25,7 @@ export const parseTestFile = (
): (() => void) | void;
},
) => {
const result = parse(code, {
const result = parseSync(code, {
lang: 'tsx',
preserveParens: false,
sourceType: 'module',
Expand Down
Loading
Loading