Skip to content

Commit 13c35f8

Browse files
authored
feat: unified VS Code extension for the Rstack toolchain (#1)
* feat: unified VS Code extension for the Rstack toolchain One extension (rstack.rstack) replacing the standalone rstack.rslint and rstack.rstest extensions: a thin shell (activation, per-folder detection, status bar, settings migration) hosting the Rslint and Rstest stacks as near-verbatim upstream copies, plus a detection-only stub for rstack-cli formatting. - pnpm monorepo with a single package (packages/vscode) - rstack-cli powers the repo harness: rs lint --type-check, rs fmt, rs setup git hooks and rs staged on pre-commit - E2E suites ported from both upstream extensions, running a real VS Code (8 shell/detection + 8 rstest + 133 lint tests), plus 107 unit tests - CI (Linux + Windows) and a 6-target VSIX release workflow - Contributor scaffolding: AGENTS.md/CLAUDE.md, CONTRIBUTING.md, issue/PR templates * fix: spawn pnpm through a shell on Windows in the fixture installer Node rejects spawning .cmd shims without a shell (CVE-2024-27980 hardening), so the E2E fixture installs failed with EINVAL on the Windows CI runner. * fix: Windows and CI-checkout E2E failures - The detection E2E asserted the extensionless POSIX bin name; on Windows the probe correctly finds the rs.cmd shim, so compare without the extension. - basic/src/gitignored.ts is a test asset deliberately listed in the lint fixture's own .gitignore, which also hid it from this repo's checkout — CI never had the file. Force-track it. * fix: retry sandbox directory removal in the JS-config E2E suite On the Windows runner the language server / VS Code watcher can still hold handles inside a nested-config directory, so an immediate recursive rmSync fails with EPERM. Use Node's built-in maxRetries/retryDelay, as the suite harness already does for the profile root. * fix: deflake the JS-config E2E suite on CI runners - Broken-nested-config cleanup: wait for the restored root config to take effect before deleting the nested directory — on Windows the server holds handles inside it until the refresh lands, so the delete hit EPERM even with short retries. Also widen the retry window. - Parent-ignore catalog test: VS Code's watcher can miss events for files created inside a just-created directory, leaving the nested config undiscovered forever (observed twice on the Linux runner). Nudge the config file and retry discovery, and compare the evaluation marker against a baseline instead of a literal count. * fix: address review findings in the port-owned adaptations Four fixes to code this port owns (upstream-inherited findings are left for upstream): - migration: rstack.rslint.enable is window-scoped in the manifest, so map the legacy rslint.enable with targetScope 'window' — folder-level legacy values are now skipped as not-folder-scoped instead of being migrated into a layer where they have no effect. - rstack bridge: a native rstest.config.* now suppresses a bridged rstack.config.* project only at its own directory, per the documented 'native wins at the same root' contract — a native config in a sibling monorepo package no longer disables every bridge in the folder. - status: crash / version-mismatch reports are latched so a detection refresh cannot paint 'running' over a live failure; the latch clears when a worker actually spawns or the version check passes again. - rstack bridge: watch rstack.config.* content changes and rebuild the bridged project, so define.test() edits reach the Test Explorer without a window reload (path-only detection signatures skip these). * fix(vscode): lockfile-driven retries and per-root status latches Two fixes from the second Codex review round: - A lockfile-only change (e.g. pnpm install) used to produce a detection pass whose signature was unchanged, so no event fired and stacks never retried failed resolutions until a window reload. Lockfile-triggered passes now always notify, and the lint stack explicitly re-attempts roots whose last start failed (a failed slot is generation-pinned and reconcile alone never retries it). - The rstest status failure latches were single-slot: in a multi-root workspace one root's successful version check or worker spawn cleared another root's live mismatch/crash. Latches are now keyed by the resolution root that reported them and only that root's recovery clears its entry. * fix(vscode): re-resolve live bridges and drop latches for removed roots Third Codex review round, both on the previous round's fixes: - syncBridgeProjects kept a live bridge without re-resolving the shim, so a dependency upgrade could leave the project pinned to a pruned pnpm store path. The shim is now re-resolved on every sync; a bridge is kept (worker stays warm) only while the resolution lands on the same file, and is rebuilt when it moved or stopped resolving. - A latched crash/mismatch now dies with its root: Project.dispose forgets its cwd's entries, and dangling mismatches from failed shim resolutions (which never create a project) are cleared when the directory stops being a bridge candidate or the folder is disposed. * fix(vscode): retry failed native projects, key latches per project Fourth Codex review round: - A native project whose one-shot config evaluation rejected (deps not installed yet) stayed an empty tree forever. Projects now record the failure and are recreated on the next detection pass (lockfile-driven installs included). Only detection events trigger the retry, so a persistently broken config cannot loop. - Master status reports now latch under the project source URI instead of cwd: two configs in one directory no longer share a latch key, so disposing or recovering one cannot clear its sibling's live failure. This also unshadows bridge resolution latches (keyed by config dir) from bridge project disposal -- previously a sync that latched a fresh mismatch and then dropped the dead bridge wiped its own report. * fix(vscode): gate restart on a live controller, one bridge per directory Fifth Codex review round: - The status bar offered Restart for a detected-but-disabled stack (enable-setting off, Restricted Mode), whose controller never registered the command; selecting it failed with command-not-found. Restart now shows only for states a registered controller produces. - A directory shipping several rstack config names (e.g. .ts next to .js mid-migration) produced one bridge project per file, but the shim probes the default names in its cwd itself, so every sibling loaded the same winning config and duplicated its tests. One bridge per directory now, keyed to the file rstack's own probe order picks. * fix(vscode): uncached version reads, restart tracks controller liveness Sixth Codex review round: - Version checks read package.json through nodeRequire, whose module cache pinned the first-seen version for the extension host lifetime; an in-place upgrade (npm/yarn reuse the path) kept the old verdict. readPackageVersion() now does a plain filesystem read; used by both the @rstest/core check (master) and the rstack shim check (bridge). - Restart availability now tracks controller registration explicitly (StatusBar.setActive, reported by the shell) instead of being inferred from the state kind: a crashed state can mean either a live controller whose worker died (restart valid) or a failed registration whose command was disposed with the controller (restart dead). * fix(vscode): bypass require.resolve's path cache for re-resolutions Seventh Codex review round, one root cause in three places: Node caches every successful require.resolve() for the process lifetime (request + paths in Module._pathCache, symlink targets in the CJS realpath cache), so once a package resolved, re-resolving after pnpm retargets the node_modules symlink replayed the pre-upgrade store path. Failed lookups are never cached, which is why resolve-after-install always worked; this closes the upgrade/downgrade half. findPackageJsonUncached() walks node_modules up with plain fs calls and an uncached realpathSync. It now anchors: - the Rslint core location (locateCore), whose cooperating pieces already resolve from the returned path (one-resolution-root rule); - the @rstest/core worker/bin resolution, with the package entry resolved from the realpath'd package dir so the cache key is version-pinned on pnpm and the exports map stays honored (a configured rstestPackagePath keeps its explicit-pin behavior); - the rstack shim resolution, replacing the previous resolve plus node_modules containment check (the walk-up only yields node_modules candidates). * fix(vscode): reload PnP on retry, report unexpected worker exits Eighth Codex review round: - The Yarn PnP fallback required .pnp.cjs through the module cache, so a dependency-driven retry kept resolving against the map seen by the first load. The cache entry is evicted before each load -- the PnP flavor of the resolution staleness findPackageJsonUncached avoids. - A worker that spawned successfully but exited before being closed (e.g. an invalid nodeExecArgs option) cleared the crash latch on spawn and then vanished silently: birpc was unblocked but no state was reported, leaving the status on running over an empty explorer. Unexpected exits now report crashed; deliberate teardowns are recognized either by $close having run first or by an explicit expected-exit mark set by dispose and the failed-debug-attach path, so project rebuilds and disposals do not misreport. * fix(vscode): close birpc for expected worker exits too Ninth Codex review round, on the previous round's fix: the expected-exit early return also skipped worker.$close(), so a disposed or rebuilt project's pending RPC calls stayed alive until timeout and the worker lingered in the tracking set. Expected exits now skip only the crash report; birpc is always closed when the process exits before $close. * fix(vscode): unbounded rstack config discovery Tenth Codex review round: the 100-file findFiles cap is fine for rows that are mere detection signals (the stacks rescan their own configs), but the rstack config list is consumed as-is by the test stack's bridge sync, so a monorepo with more than 100 rstack configs silently lost the overflow's bridged projects. The rstack row is now unbounded; the cap stays for the signal-only rows.
1 parent 6fbb53e commit 13c35f8

235 files changed

Lines changed: 29141 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
name: '🐞 Bug Report'
2+
description: Report a bug in the Rstack VS Code extension
3+
title: '[Bug]: '
4+
labels: ['🐞 bug']
5+
body:
6+
- type: markdown
7+
attributes:
8+
value: |
9+
Thanks for taking the time to report this issue! Before submitting, please note:
10+
11+
- Make sure you searched in the [Issues](https://github.com/rstackjs/rstack-editor/issues) and didn't find the same issue.
12+
- If the problem reproduces with the CLI too (`rs lint` / `rs test` / `rs fmt`), it likely belongs to the tool's own repo — file it against [rslint](https://github.com/web-infra-dev/rslint), [rstest](https://github.com/web-infra-dev/rstest) or [rstack-cli](https://github.com/rstackjs/rstack-cli) instead.
13+
14+
- type: dropdown
15+
id: area
16+
attributes:
17+
label: Affected area
18+
options:
19+
- Linting (Rslint)
20+
- Testing (Rstest)
21+
- Formatting
22+
- Detection / status bar / settings
23+
- Other
24+
validations:
25+
required: true
26+
27+
- type: textarea
28+
id: versions
29+
attributes:
30+
label: Versions
31+
description: |
32+
Extension version, VS Code version + platform (Help → About), and the tool versions resolved from your project (`npm ls @rslint/core @rstest/core rstack`).
33+
render: sh
34+
validations:
35+
required: true
36+
37+
- type: textarea
38+
id: details
39+
attributes:
40+
label: Details
41+
description: Describe the bug, including screenshots and relevant output-channel logs (Output panel → Rstack channels).
42+
validations:
43+
required: true
44+
45+
- type: textarea
46+
id: reproduce-steps
47+
attributes:
48+
label: Reproduce steps
49+
description: The simplest steps (ideally with a minimal repo link) so we can quickly reproduce the problem.
50+
validations:
51+
required: true
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
name: '💡 Feature Request'
2+
description: Suggest a feature for the Rstack VS Code extension
3+
title: '[Feature]: '
4+
labels: ['💡 feature']
5+
body:
6+
- type: textarea
7+
id: problem
8+
attributes:
9+
label: What problem does this feature solve?
10+
validations:
11+
required: true
12+
13+
- type: textarea
14+
id: solution
15+
attributes:
16+
label: What does the proposed API/UX look like?
17+
validations:
18+
required: true

.github/ISSUE_TEMPLATE/config.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
blank_issues_enabled: false
2+
contact_links:
3+
- name: Ask a question
4+
url: https://github.com/rstackjs/rstack-editor/discussions
5+
about: Ask a question about the Rstack editor extensions

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
## Summary
2+
3+
## Related Links
4+
5+
<!-- Provide links of related issues or pages -->
6+
7+
## Checklist
8+
9+
<!-- Check and mark with an "x" -->
10+
11+
- [ ] Tests updated (or not required).
12+
- [ ] Documentation updated (or not required).

.github/workflows/ci.yml

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
7+
pull_request:
8+
9+
concurrency:
10+
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
11+
cancel-in-progress: ${{ github.ref_name != 'main' }}
12+
13+
permissions:
14+
contents: read
15+
16+
jobs:
17+
# ======== linux ========
18+
test-linux:
19+
name: Test (ubuntu-latest)
20+
runs-on: ubuntu-latest
21+
timeout-minutes: 30
22+
23+
steps:
24+
- name: Checkout
25+
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
26+
with:
27+
fetch-depth: 1
28+
29+
- name: Setup pnpm
30+
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
31+
32+
- name: Setup Node.js
33+
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
34+
with:
35+
node-version: 22
36+
cache: 'pnpm'
37+
cache-dependency-path: pnpm-lock.yaml
38+
39+
- name: Install Dependencies
40+
run: pnpm install --frozen-lockfile --prefer-offline
41+
42+
- name: Build
43+
run: pnpm run build
44+
45+
- name: Lint
46+
run: pnpm run lint
47+
48+
- name: Format Check
49+
run: pnpm run fmt:check
50+
51+
- name: Unit Test
52+
run: pnpm run test:unit
53+
54+
# `@vscode/test-electron` downloads a full VS Code into
55+
# `packages/vscode/.vscode-test`. Cache only the immutable distribution
56+
# directories, keyed on the extension manifest (which carries
57+
# `engines.vscode`); the restore-keys fallback keeps older downloads
58+
# available while `version: 'stable'` moves forward.
59+
- name: Cache VS Code Download
60+
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
61+
with:
62+
path: packages/vscode/.vscode-test/vscode-*
63+
key: vscode-test-${{ runner.os }}-${{ hashFiles('packages/vscode/package.json') }}
64+
restore-keys: |
65+
vscode-test-${{ runner.os }}-
66+
67+
# The E2E fixtures install published npm packages at test time
68+
# (tests/e2e/setupFixtures.mjs, invoked by test:e2e), so this step needs
69+
# network access. The VS Code Extension Host needs a display on Linux,
70+
# hence xvfb (preinstalled on GitHub-hosted Ubuntu runners).
71+
- name: E2E Test
72+
run: xvfb-run -a pnpm run test:e2e
73+
74+
# ======== windows ========
75+
# A dedicated GitHub-hosted Windows job: upstream rslint notes the VS Code
76+
# extension E2E suite is unreliable on self-hosted Windows runners.
77+
test-windows:
78+
name: Test (windows-latest)
79+
runs-on: windows-latest
80+
timeout-minutes: 40
81+
82+
steps:
83+
- name: Checkout
84+
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
85+
with:
86+
fetch-depth: 1
87+
88+
- name: Setup pnpm
89+
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
90+
91+
- name: Setup Node.js
92+
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
93+
with:
94+
node-version: 22
95+
cache: 'pnpm'
96+
cache-dependency-path: pnpm-lock.yaml
97+
98+
- name: Install Dependencies
99+
run: pnpm install --frozen-lockfile --prefer-offline
100+
101+
- name: Build
102+
run: pnpm run build
103+
104+
- name: Lint
105+
run: pnpm run lint
106+
107+
- name: Unit Test
108+
run: pnpm run test:unit
109+
110+
- name: Cache VS Code Download
111+
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
112+
with:
113+
path: packages/vscode/.vscode-test/vscode-*
114+
key: vscode-test-${{ runner.os }}-${{ hashFiles('packages/vscode/package.json') }}
115+
restore-keys: |
116+
vscode-test-${{ runner.os }}-
117+
118+
- name: E2E Test
119+
run: pnpm run test:e2e

.github/workflows/release.yml

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
name: Release
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
dry_run:
7+
description: 'Dry run: package and upload the VSIX artifacts only, skip publishing'
8+
required: false
9+
type: boolean
10+
default: false
11+
12+
permissions:
13+
contents: read
14+
15+
jobs:
16+
release_vscode_extension:
17+
name: Release VS Code Extension (${{ matrix.vsce-target }})
18+
if: github.repository == 'rstackjs/rstack-editor' && github.event_name == 'workflow_dispatch'
19+
runs-on: ${{ matrix.runner }}
20+
environment: vscode-marketplace
21+
timeout-minutes: 30
22+
strategy:
23+
fail-fast: false
24+
matrix:
25+
include:
26+
- runner: ubuntu-latest
27+
vsce-target: linux-x64
28+
arch: x64
29+
- runner: ubuntu-22.04
30+
vsce-target: linux-arm64
31+
arch: arm64
32+
- runner: macos-15
33+
vsce-target: darwin-x64
34+
arch: x64
35+
- runner: macos-15
36+
vsce-target: darwin-arm64
37+
arch: arm64
38+
- runner: windows-2022
39+
vsce-target: win32-x64
40+
arch: x64
41+
- runner: windows-latest
42+
vsce-target: win32-arm64
43+
arch: arm64
44+
defaults:
45+
run:
46+
shell: bash
47+
env:
48+
# rslib.config.mts keys the staged `@yuku-parser/binding-*` napi payload
49+
# off this variable (Linux targets get a `-gnu` suffix appended there).
50+
VSCE_TARGET: ${{ matrix.vsce-target }}
51+
steps:
52+
- name: Checkout
53+
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
54+
with:
55+
fetch-depth: 1
56+
57+
- name: Setup pnpm
58+
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
59+
60+
- name: Setup Node.js
61+
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
62+
with:
63+
node-version: 22
64+
cache: 'pnpm'
65+
cache-dependency-path: pnpm-lock.yaml
66+
67+
# `supportedArchitectures` must be set BEFORE `pnpm install` so pnpm also
68+
# fetches the cross-target optional `@yuku-parser/binding-*` package (the
69+
# Linux runners' current libc is glibc, matching the `-gnu` bindings the
70+
# linux-* VSIX targets need).
71+
- name: Install Dependencies
72+
run: |
73+
pnpm config set --location=project --json supportedArchitectures '{"cpu":["current","${{ matrix.arch }}"]}'
74+
pnpm install
75+
76+
- name: Build
77+
run: pnpm run build
78+
79+
- name: Package VS Code Extension
80+
working-directory: packages/vscode
81+
run: pnpm exec vsce package --target ${{ matrix.vsce-target }} -o rstack-${{ matrix.vsce-target }}.vsix
82+
83+
- name: Upload VSIX Artifact
84+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
85+
with:
86+
name: rstack-${{ matrix.vsce-target }}
87+
path: packages/vscode/rstack-${{ matrix.vsce-target }}.vsix
88+
if-no-files-found: error
89+
90+
- name: Publish VS Code Extension
91+
if: ${{ !inputs.dry_run }}
92+
working-directory: packages/vscode
93+
env:
94+
VSCE_PAT: ${{ secrets.VSCE_PAT }}
95+
run: pnpm exec vsce publish --target ${{ matrix.vsce-target }} --skip-duplicate
96+
97+
- name: Publish OVSX Extension
98+
if: ${{ !inputs.dry_run }}
99+
working-directory: packages/vscode
100+
env:
101+
OVSX_PAT: ${{ secrets.OVSX_PAT }}
102+
run: pnpm exec ovsx publish --target ${{ matrix.vsce-target }} --skip-duplicate

.gitignore

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,3 +141,24 @@ dist
141141
vite.config.js.timestamp-*
142142
vite.config.ts.timestamp-*
143143
.vite/
144+
145+
# rstack-editor
146+
*.vsix
147+
tests-dist/
148+
.rsdoctor/
149+
150+
# E2E fixtures install published npm versions on demand; only
151+
# their manifests and configs are tracked.
152+
packages/vscode/tests/e2e/fixtures/*/node_modules/
153+
packages/vscode/tests/e2e/fixtures/*/pnpm-lock.yaml
154+
packages/vscode/tests/e2e/lint/fixtures/pnpm-lock.yaml
155+
packages/vscode/tests/e2e/rstest/fixtures/*/pnpm-lock.yaml
156+
157+
# Build-time copy of the workspace root LICENSE (see rslib.config.mts)
158+
packages/vscode/LICENSE
159+
160+
# Design documents are working notes, not part of the published repo
161+
/DESIGN*.md
162+
163+
# Claude Code local worktrees
164+
.claude/worktrees/

.nvmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
26

.rstack/hooks/pre-commit

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
rs staged

.vscode/extensions.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
// Rslint powers `pnpm lint` in this repo; the extension surfaces the same
3+
// diagnostics in the editor. To be replaced by `rstack.rstack` (this very
4+
// extension) once it is published.
5+
"recommendations": ["rstack.rslint"]
6+
}

0 commit comments

Comments
 (0)