diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7dcb353..b18e402 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -46,6 +46,12 @@ jobs: - runner: ubuntu-22.04 vsce-target: linux-arm64 arch: arm64 + - runner: ubuntu-latest + vsce-target: alpine-x64 + arch: x64 + - runner: ubuntu-22.04 + vsce-target: alpine-arm64 + arch: arm64 - runner: macos-15 vsce-target: darwin-x64 arch: x64 @@ -63,7 +69,7 @@ jobs: shell: bash env: # rslib.config.mts keys the staged `@yuku-parser/binding-*` napi payload - # off this variable (Linux targets get a `-gnu` suffix appended there). + # off this variable (`linux-*` → `-gnu`, `alpine-*` → `linux-*-musl`). VSCE_TARGET: ${{ matrix.vsce-target }} steps: - name: Checkout @@ -82,12 +88,13 @@ jobs: cache-dependency-path: pnpm-lock.yaml # `supportedArchitectures` must be set BEFORE `pnpm install` so pnpm also - # fetches the cross-target optional `@yuku-parser/binding-*` package (the - # Linux runners' current libc is glibc, matching the `-gnu` bindings the - # linux-* VSIX targets need). + # fetches the cross-target optional `@yuku-parser/binding-*` package. + # musl is always listed so the alpine-* legs get Yuku'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 run: | - pnpm config set --location=project --json supportedArchitectures '{"cpu":["current","${{ matrix.arch }}"]}' + pnpm config set --location=project --json supportedArchitectures '{"cpu":["current","${{ matrix.arch }}"],"libc":["current","musl"]}' pnpm install - name: Build diff --git a/packages/vscode/AGENTS.md b/packages/vscode/AGENTS.md index 02cc2b0..f394e08 100644 --- a/packages/vscode/AGENTS.md +++ b/packages/vscode/AGENTS.md @@ -55,7 +55,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 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`. - `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. diff --git a/packages/vscode/rslib.config.mts b/packages/vscode/rslib.config.mts index 03dcb28..4686dc5 100644 --- a/packages/vscode/rslib.config.mts +++ b/packages/vscode/rslib.config.mts @@ -12,12 +12,15 @@ const rootDir = path.dirname(fileURLToPath(import.meta.url)); // collection loads the `yuku-parser` napi binding. 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). +// On musl Linux (Alpine), set `VSCE_TARGET=alpine-` explicitly. const vsceTarget = process.env.VSCE_TARGET ?? `${process.platform}-${process.arch}`; -// The Linux VSIX targets use glibc, whose Yuku bindings have a `-gnu` suffix. -const yukuBindingSuffix = vsceTarget.startsWith('linux-') - ? `${vsceTarget}-gnu` - : vsceTarget; +// 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 = { linux: 'gnu', alpine: 'musl' }; +const [targetOs, targetArch] = vsceTarget.split('-'); +const libc = LINUX_LIBC[targetOs]; +const yukuBindingSuffix = libc ? `linux-${targetArch}-${libc}` : vsceTarget; const yukuRequire = createRequire(require.resolve('yuku-parser')); // Yuku computes this package name at runtime, so Rspack cannot discover it. const yukuBindingPath = yukuRequire.resolve( diff --git a/packages/vscode/scripts/packageTargets.mjs b/packages/vscode/scripts/packageTargets.mjs index 39512a7..7f43f9b 100644 --- a/packages/vscode/scripts/packageTargets.mjs +++ b/packages/vscode/scripts/packageTargets.mjs @@ -10,11 +10,16 @@ // pnpm config set --location=project --json supportedArchitectures \ // '{"cpu":["current","arm64"]}' && pnpm install // +// The alpine-* targets need the musl bindings, so also add +// `"libc":["current","musl"]` (and, off Linux, `"os":["current","linux"]`). +// // CI does exactly that, one runner per target. import { spawnSync } from 'node:child_process'; import process from 'node:process'; const TARGETS = [ + 'alpine-arm64', + 'alpine-x64', 'darwin-arm64', 'darwin-x64', 'linux-arm64',