From 37ea35a61efcd9ac3f27a44f475f2427f5c99f7f Mon Sep 17 00:00:00 2001 From: fi3ework Date: Tue, 29 Sep 2026 11:00:27 +0800 Subject: [PATCH 1/4] feat(vscode): sync the standalone Rslint and Rstest extensions Bring the unified extension up to rslint e5d76242 (0.9.3) and rstest 41daaa1b (0.12.2) before the standalone extensions retire. - Report ESLint-plugin host failures through the Rslint status instead of upstream's toast: a missing plugin package is the not-installed state (disabled), any other failure is crashed with a one-line detail; the dependency poll recovers both. - Let the shared missing-dependency classifier accept a Node module-not-found message after a `: ` wrapper on the first line. - Ship generated third-party notices as LICENSE.md in the VSIX. - Upgrade yuku-parser to 0.12.0. - Port the live, privacy-aware trace setting wording and the fixAll typed-diagnostic E2E waits. --- .gitignore | 4 +- packages/vscode/.vscodeignore | 2 +- packages/vscode/AGENTS.md | 6 +- .../plugin-host-failure/host-state.txt | 1 + .../plugin-host-failure/local-plugin.mjs | 23 + .../plugin-host-failure/rslint.config.mjs | 7 + .../fixtures/plugin-host-failure/src/index.ts | 2 + .../plugin-host-failure/tsconfig.json | 4 + packages/vscode/e2e/lint/runTest.ts | 5 + .../lint/suite-plugin-host-failure/index.ts | 3 + .../plugin-host-failure.test.ts | 93 +++ .../e2e/lint/suite/fixall-error.test.ts | 10 +- .../vscode/e2e/lint/suite/fixall-helpers.ts | 11 + .../e2e/lint/suite/fixall-onsave.test.ts | 25 +- packages/vscode/e2e/lint/suite/fixall.test.ts | 16 +- packages/vscode/licensePlugin.mts | 106 +++ packages/vscode/package.json | 7 +- packages/vscode/rslib.config.mts | 27 +- .../vscode/src/shared/missingDependency.ts | 7 +- .../src/stacks/lint/worker/PluginLintPool.ts | 41 +- .../vscode/src/stacks/lint/worker/index.ts | 8 +- .../tests/shared/missingDependency.test.ts | 21 + .../tests/stacks/lint/pluginLintPool.test.ts | 40 ++ pnpm-lock.yaml | 634 ++++++++++++++++-- 24 files changed, 976 insertions(+), 127 deletions(-) create mode 100644 packages/vscode/e2e/lint/fixtures/plugin-host-failure/host-state.txt create mode 100644 packages/vscode/e2e/lint/fixtures/plugin-host-failure/local-plugin.mjs create mode 100644 packages/vscode/e2e/lint/fixtures/plugin-host-failure/rslint.config.mjs create mode 100644 packages/vscode/e2e/lint/fixtures/plugin-host-failure/src/index.ts create mode 100644 packages/vscode/e2e/lint/fixtures/plugin-host-failure/tsconfig.json create mode 100644 packages/vscode/e2e/lint/suite-plugin-host-failure/index.ts create mode 100644 packages/vscode/e2e/lint/suite-plugin-host-failure/plugin-host-failure.test.ts create mode 100644 packages/vscode/licensePlugin.mts create mode 100644 packages/vscode/tests/stacks/lint/pluginLintPool.test.ts diff --git a/.gitignore b/.gitignore index b2f67e3..3a926f5 100644 --- a/.gitignore +++ b/.gitignore @@ -158,8 +158,8 @@ packages/vscode/e2e/lint/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 +# Build-time third-party notices, including the target-specific native binding +packages/vscode/LICENSE.md # Design documents are working notes, not part of the published repo /DESIGN*.md diff --git a/packages/vscode/.vscodeignore b/packages/vscode/.vscodeignore index 220723d..c0f1325 100644 --- a/packages/vscode/.vscodeignore +++ b/packages/vscode/.vscodeignore @@ -4,6 +4,6 @@ !dist/**/*.js !dist/**/*.node !icon.png -!LICENSE +!LICENSE.md !README.md !package.json diff --git a/packages/vscode/AGENTS.md b/packages/vscode/AGENTS.md index 78ab0a0..039fbe0 100644 --- a/packages/vscode/AGENTS.md +++ b/packages/vscode/AGENTS.md @@ -6,7 +6,7 @@ One extension replacing the standalone `rstack.rslint` and `rstack.rstest` exten - `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 ten ways (the "adaptations" below). When syncing upstream, preserve them. An eleventh divergence is either a bug or must be added to this list. -- **Tracked upstream state.** `stacks/lint` is synced to web-infra-dev/rslint `packages/vscode-extension` at **39536fd6** (#1617 — per-document core resolution, `CoreResolver` + `RuntimeManager`, `corePath`, PnP removed) and **892482e0** (#1630 — `configPath` on `rslint/configRefresh`). Targeted later ports are **84f9c9b5** (#1967 — languageclient-owned live LSP tracing) and **b7176723** (#1951 — remove legacy JSON config watching); the Unicode BOM E2E comes from **5fc197a5** (#1560), with its native-config fixture shape from **b7176723**. `CoreResolver.ts` / `RuntimeManager.ts` / `WorkspaceDocumentRouter.ts` / `Rslint.ts` are the files to diff when syncing further; record the new commits here when you do. `stacks/test` tracks web-infra-dev/rstest `packages/vscode` through **8f945491** (#1729 — public programmatic API), **d2812754** (#1804 — quoted exact file filters), **99be33e8** (#1805 — public test listing), **88cd5f6d** (#1806 — browser projects in watch), and **994b77e0** (#1807 — public watch API). A targeted port of web-infra-dev/rstest#1888 (not yet merged upstream at port time) switches the worker IPC from `serialization: 'advanced'` to default JSON — VS Code 1.139's Electron writes a V8 serializer format released Node cannot read — so values crossing the channel must survive a JSON round-trip, and thrown errors cross only because both `createBirpc` calls spread `rpcErrorCodec` (`stacks/test/shared/rpc.ts`). The earlier targeted lifecycle fix from **d82db4fc31a61ee74b2a74917f14a458e1bca419** is subsumed by this sync; our failed-project retry and worker-cleanup behavior remains ahead of upstream. +- **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 **41daaa1b** (0.12.2). This sync ports Rslint **84f9c9b5** trace-setting manifest wording, **72cd2b1f** fixAll waits, and plugin-host failure status (adaptations 4/7); Rstest **988f8e1d** license notices aggregate all three bundles into the sole shipped `LICENSE.md`, including the root MIT text. `@rstackjs/load-config` is not bundled; Yuku is **0.12.0**. Retained ports include per-document core resolution / pinned config paths (**39536fd6**, **892482e0**), legacy JSON watcher removal (**b7176723**), Unicode BOM coverage (**5fc197a5**), and Rstest public programmatic/list/watch APIs and exact filters (**8f945491**, **d2812754**, **99be33e8**, **88cd5f6d**, **994b77e0**). Diff `CoreResolver.ts`, `RuntimeManager.ts`, `WorkspaceDocumentRouter.ts`, and `Rslint.ts` on future syncs. The targeted 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. The earlier **d82db4fc31a61ee74b2a74917f14a458e1bca419** lifecycle fix is subsumed; 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`). ## The ten adaptations @@ -14,10 +14,10 @@ One extension replacing the standalone `rstack.rslint` and `rstack.rstest` exten 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). 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. 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`. +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. 6. **Node runtime selection** (lint, test, fmt) — the Node a project-loading child process runs on is a **User Node runtime** chosen by the extension against one uniform floor, never assumed from PATH; the recovery path is the user's own shell, and the dividing line is the **load bound** (terms in CONTEXT.md; the full rule and rationale in `docs/adr/0001-node-runtime-selection.md`). All three callers — the lint worker, the rstest worker and the `rs fmt --lsp` server — take the decision from the one shared module (`shared/nodeResolution.ts`) and share one escape hatch, the resource-scoped `rstack.nodeExecutable` (`shared/nodeExecutableSetting.ts`); each appends its own consequence to the shared preflight message. -7. **Lint worker and Rstack bridge** — the extension host is only Rslint's language client. One vscode-free, editor-shipped lint worker per **Lint runtime** (one Rslint core inside one workspace folder — CONTEXT.md) runs on the User Node runtime, owns the Go LSP plus all five reverse requests, and derives the binary/config/plugin pieces from one explicit `@rslint/core` directory. Upstream's `CoreResolver` loads that core in the extension host; ours only walks to the directory (`fs.stat` + `package.json` + semver) and hands the path to the worker, and its `CoreInstallation` therefore carries paths, not module factories; upstream's installation cache goes with the module loading it memoized (`clear()` is a no-op kept for the `RuntimeManager` contract). A bridged folder passes only rstack's published `dist/rslintConfig.js` shim; neither the extension nor the worker re-implements Rstack config semantics. Because every supported config protocol locks `configPath` per process, the shim is part of the runtime key (`folder + core identity + shim`), which upstream — having no bridge — keys on the core alone. Why: `docs/adr/0003-lint-through-editor-worker.md`. The worker also sends the editor-only `rstack/rslintConfigDependency` notification (`stacks/lint/worker/configDependencyProtocol.ts`) when config loading finds a missing package. `ConfigTransactionAdapter` rewrites only that classified `rslint/loadConfigs` candidate's error message to its first line, so Go cannot echo a require stack beside the single warning. An initialized client whose initial configRefresh rejects with that verdict stays available for retry, rather than propagating a generic startup crash through RuntimeManager. +7. **Lint worker and Rstack bridge** — the extension host is only Rslint's language client. One vscode-free, editor-shipped lint worker per **Lint runtime** (one Rslint core inside one workspace folder — CONTEXT.md) runs on the User Node runtime, owns the Go LSP plus all five reverse requests, and derives the binary/config/plugin pieces from one explicit `@rslint/core` directory. Upstream's `CoreResolver` loads that core in the extension host; ours only walks to the directory (`fs.stat` + `package.json` + semver) and hands the path to the worker, and its `CoreInstallation` therefore carries paths, not module factories; upstream's installation cache goes with the module loading it memoized (`clear()` is a no-op kept for the `RuntimeManager` contract). A bridged folder passes only rstack's published `dist/rslintConfig.js` shim; neither the extension nor the worker re-implements Rstack config semantics. Because every supported config protocol locks `configPath` per process, the shim is part of the runtime key (`folder + core identity + shim`), which upstream — having no bridge — keys on the core alone. Why: `docs/adr/0003-lint-through-editor-worker.md`. The worker also sends the editor-only `rstack/rslintConfigDependency` notification (`stacks/lint/worker/configDependencyProtocol.ts`) when config loading finds a missing package. `ConfigTransactionAdapter` rewrites only that classified `rslint/loadConfigs` candidate's error message to its first line, so Go cannot echo a require stack beside the single warning. An initialized client whose initial configRefresh rejects with that verdict stays available for retry, rather than propagating a generic startup crash through RuntimeManager. Plugin-host failures with nonempty descriptors use this same notification: bare-package failures become `missing` / `disabled` with one stack-free warning; others become `error` / `crashed` with one-line detail and deduplicated full error in Output. Status survives aborted prepares; successful prepare or empty descriptors clears it. Shell poll / lockfile retries create a new transaction generation even with an unchanged fingerprint. 8. **Self-documenting Rslint diagnostics** — client-side providers parse Inline directives into per-rule hover, DocumentLink and underline-decoration affordances (the hover renders `Rslint(rule-id)`, the shape VS Code gives the published diagnostics), and the router enriches today's `[rule-id] message` diagnostics with a derived Rule docs link. No rule metadata or network lookup is bundled (ADR 0004). The hover provider yields whenever the owning language client's resolved capabilities advertise `hoverProvider`; an optional `Rslint.onClosed` hook identity-safely prunes the controller's capability mirror; the diagnostic synthesis is removed once upstream publishes `code` / `codeDescription` natively. 9. **Color env parity with the CLI** (test) — upstream hard-codes `FORCE_COLOR: '1'` into the worker's spawn env; ours mirrors the CLI's `getForceColorEnv` (rstest `packages/core/src/utils/logger.ts`) instead (`stacks/test/shared/colorEnv.ts`): the master injects `FORCE_COLOR=1` into the composed spawn env only when neither `FORCE_COLOR` nor `NO_COLOR` is already set (marking the injection with `RSTACK_FORCE_COLOR_INJECTED`), and the worker retracts the marked injection right after config load if the config set `NO_COLOR` — the CLI's own decision point. Otherwise a project whose config sets `process.env.NO_COLOR` (rstack-cli does) hits Node's "'NO_COLOR' env is ignored" warning in every pool process. A user-set `FORCE_COLOR` beside a config-set `NO_COLOR` still warns, exactly as the bare CLI does. diff --git a/packages/vscode/e2e/lint/fixtures/plugin-host-failure/host-state.txt b/packages/vscode/e2e/lint/fixtures/plugin-host-failure/host-state.txt new file mode 100644 index 0000000..0bc3798 --- /dev/null +++ b/packages/vscode/e2e/lint/fixtures/plugin-host-failure/host-state.txt @@ -0,0 +1 @@ +crash diff --git a/packages/vscode/e2e/lint/fixtures/plugin-host-failure/local-plugin.mjs b/packages/vscode/e2e/lint/fixtures/plugin-host-failure/local-plugin.mjs new file mode 100644 index 0000000..52f3baa --- /dev/null +++ b/packages/vscode/e2e/lint/fixtures/plugin-host-failure/local-plugin.mjs @@ -0,0 +1,23 @@ +import fs from 'node:fs'; +import { isMainThread } from 'node:worker_threads'; + +// Config discovery must succeed; only the plugin-host worker import fails. +// A non-config marker lets the E2E prove polling recovery without a file-watch +// refresh or restart command masking a non-retryable failed generation. +if (!isMainThread) { + const state = fs.readFileSync(new URL('./host-state.txt', import.meta.url), 'utf8').trim(); + fs.appendFileSync(new URL('./attempts.log', import.meta.url), `${state}\n`); + if (state === 'crash') throw new Error('fixture plugin import exploded'); + if (state === 'missing') await import('rstack-e2e-absent-plugin-dependency'); +} + +export default { + rules: { + report: { + meta: { type: 'problem', schema: [], messages: { failure: 'Local plugin is running.' } }, + create(context) { + return { Program(node) { context.report({ node, messageId: 'failure' }); } }; + }, + }, + }, +}; diff --git a/packages/vscode/e2e/lint/fixtures/plugin-host-failure/rslint.config.mjs b/packages/vscode/e2e/lint/fixtures/plugin-host-failure/rslint.config.mjs new file mode 100644 index 0000000..cba1644 --- /dev/null +++ b/packages/vscode/e2e/lint/fixtures/plugin-host-failure/rslint.config.mjs @@ -0,0 +1,7 @@ +import local from './local-plugin.mjs'; + +export default [{ + files: ['**/*.ts'], + plugins: { local }, + rules: { 'local/report': 'error', 'no-console': 'error' }, +}]; diff --git a/packages/vscode/e2e/lint/fixtures/plugin-host-failure/src/index.ts b/packages/vscode/e2e/lint/fixtures/plugin-host-failure/src/index.ts new file mode 100644 index 0000000..2215db9 --- /dev/null +++ b/packages/vscode/e2e/lint/fixtures/plugin-host-failure/src/index.ts @@ -0,0 +1,2 @@ +console.log('native rule stays available'); +export {}; diff --git a/packages/vscode/e2e/lint/fixtures/plugin-host-failure/tsconfig.json b/packages/vscode/e2e/lint/fixtures/plugin-host-failure/tsconfig.json new file mode 100644 index 0000000..1bc8df6 --- /dev/null +++ b/packages/vscode/e2e/lint/fixtures/plugin-host-failure/tsconfig.json @@ -0,0 +1,4 @@ +{ + "compilerOptions": { "target": "esnext", "module": "esnext", "moduleResolution": "bundler", "noEmit": true }, + "include": ["src"] +} diff --git a/packages/vscode/e2e/lint/runTest.ts b/packages/vscode/e2e/lint/runTest.ts index 443bcd6..5866978 100644 --- a/packages/vscode/e2e/lint/runTest.ts +++ b/packages/vscode/e2e/lint/runTest.ts @@ -288,6 +288,11 @@ async function main(): Promise { workspace: fixture('eslint-plugins'), tests: suiteDir('suite-eslint-plugins'), }, + { + name: 'Plugin-host failure tests', + workspace: fixture('plugin-host-failure'), + tests: suiteDir('suite-plugin-host-failure'), + }, { name: 'unicode-bom tests', workspace: fixture('unicode-bom'), diff --git a/packages/vscode/e2e/lint/suite-plugin-host-failure/index.ts b/packages/vscode/e2e/lint/suite-plugin-host-failure/index.ts new file mode 100644 index 0000000..e8a41f6 --- /dev/null +++ b/packages/vscode/e2e/lint/suite-plugin-host-failure/index.ts @@ -0,0 +1,3 @@ +import { createRun } from '../runSuite'; + +export const run = createRun(); diff --git a/packages/vscode/e2e/lint/suite-plugin-host-failure/plugin-host-failure.test.ts b/packages/vscode/e2e/lint/suite-plugin-host-failure/plugin-host-failure.test.ts new file mode 100644 index 0000000..b4f0ff6 --- /dev/null +++ b/packages/vscode/e2e/lint/suite-plugin-host-failure/plugin-host-failure.test.ts @@ -0,0 +1,93 @@ +import * as assert from 'node:assert'; +import fs from 'node:fs'; +import path from 'node:path'; +import * as vscode from 'vscode'; +import type { StackState } from '../../../src/types'; +import { extensionExports } from '../utils/extension'; +import { + diagnosticRuleIdIncludes, + waitForRslintDiagnostics, +} from '../utils/diagnostics'; + +suite('Plugin-host status and polling recovery', function () { + this.timeout(120_000); + + test('keeps native diagnostics during plugin failure, classifies missing imports, and recovers without restart', async () => { + const root = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; + assert.ok(root); + const api = extensionExports(); + const lint = api.getStackExports('rslint') as { + getFolderStates(): ReadonlyMap; + }; + assert.ok(lint); + const waitForState = async (kind: StackState['kind']) => { + const deadline = Date.now() + 60_000; + while (Date.now() < deadline) { + const state = [...lint.getFolderStates().values()].find( + (state) => state.kind === kind, + ); + if (state) return state; + await new Promise((resolve) => setTimeout(resolve, 100)); + } + throw new Error( + `Expected ${kind}; got ${JSON.stringify([...lint.getFolderStates().values()])}`, + ); + }; + const document = await vscode.workspace.openTextDocument( + path.join(root, 'src/index.ts'), + ); + await vscode.window.showTextDocument(document); + const crashed = await waitForState('crashed'); + if (crashed.kind === 'crashed') { + assert.match( + crashed.detail, + /^ESLint plugins failed to load: .*fixture plugin import exploded/, + ); + assert.ok(!crashed.detail.includes('\n')); + } + await waitForRslintDiagnostics(document, (diagnostics) => + diagnostics.some((d) => diagnosticRuleIdIncludes(d, 'no-console')), + ); + + api.setDependencyPollIntervalForTest(250); + try { + fs.writeFileSync(path.join(root, 'host-state.txt'), 'missing'); + const disabled = await waitForState('disabled'); + if (disabled.kind === 'disabled') { + assert.ok(disabled.reason); + assert.match(disabled.reason, /install the project dependencies/); + } + const deadline = Date.now() + 60_000; + while ( + fs + .readFileSync(path.join(root, 'attempts.log'), 'utf8') + .split('\n') + .filter((line) => line === 'missing').length < 2 + ) { + assert.ok(Date.now() < deadline, 'Expected a second plugin-host retry'); + await new Promise((resolve) => setTimeout(resolve, 100)); + } + const warnings = api.getRecordedWarnings('rslint'); + assert.equal( + warnings.length, + 1, + 'Retries must not repeat the missing-package warning', + ); + assert.match(warnings[0], /rstack-e2e-absent-plugin-dependency/); + assert.ok( + !warnings[0].includes('\n'), + 'Missing-package warning must be one line', + ); + + fs.writeFileSync(path.join(root, 'host-state.txt'), 'ready'); + await waitForState('running'); + await waitForRslintDiagnostics(document, (diagnostics) => + ['local/report', 'no-console'].every((rule) => + diagnostics.some((d) => diagnosticRuleIdIncludes(d, rule)), + ), + ); + } finally { + api.setDependencyPollIntervalForTest(60_000); + } + }); +}); diff --git a/packages/vscode/e2e/lint/suite/fixall-error.test.ts b/packages/vscode/e2e/lint/suite/fixall-error.test.ts index 1261438..fa65048 100644 --- a/packages/vscode/e2e/lint/suite/fixall-error.test.ts +++ b/packages/vscode/e2e/lint/suite/fixall-error.test.ts @@ -1,10 +1,9 @@ -// Ported from web-infra-dev/rslint (deviation: setup waits go through -// waitForDiagnosticsWithRuleIds -- see fixall-helpers.ts for why). +// Ported from web-infra-dev/rslint (typed setup waits from 72cd2b1f). // `packages/vscode-extension/__tests__/suite/fixall-error.test.ts` (origin/main). import * as assert from 'assert'; import * as vscode from 'vscode'; import { - waitForDiagnosticsWithRuleIds, + waitForTypeAssertionDiagnostics, waitForContentChange, diagnosticRuleIdIncludes, findFixAllAction, @@ -59,10 +58,7 @@ suite('rslint fixAll - error flows', function () { editor, "const pVal: string = 'x';\nconst pRes = (pVal as string).trim();\n", ); - const probeDiags = await waitForDiagnosticsWithRuleIds( - doc, - 'no-unnecessary-type-assertion', - ); + const probeDiags = await waitForTypeAssertionDiagnostics(doc); assert.ok( probeDiags.some((d) => diagnosticRuleIdIncludes(d, 'no-unnecessary-type-assertion'), diff --git a/packages/vscode/e2e/lint/suite/fixall-helpers.ts b/packages/vscode/e2e/lint/suite/fixall-helpers.ts index 6bf3f3a..daa0a8d 100644 --- a/packages/vscode/e2e/lint/suite/fixall-helpers.ts +++ b/packages/vscode/e2e/lint/suite/fixall-helpers.ts @@ -33,6 +33,8 @@ export const waitForDiagnostics = waitForRslintDiagnostics; * first non-empty publish may carry only non-type-aware rules on platforms * with slow file watchers (macOS). Waiting for the expected diagnostics keeps * the terminal assertion identical without depending on publish batching. + * Our diagnostic enrichment moves rule ids to diagnostic.code; match that + * instead of upstream's message prefix. */ export function waitForDiagnosticsWithRuleIds( doc: vscode.TextDocument, @@ -49,6 +51,15 @@ export function waitForDiagnosticsWithRuleIds( export const waitForDiagnosticsCount = waitForRslintDiagnosticsCount; export const waitForDiagnosticsToChange = waitForRslintDiagnosticsToChange; +export function waitForTypeAssertionDiagnostics( + document: vscode.TextDocument, +): Promise { + return waitForDiagnosticsWithRuleIds( + document, + 'no-unnecessary-type-assertion', + ); +} + export function getFixturesDir(): string { const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; if (!workspaceFolder) diff --git a/packages/vscode/e2e/lint/suite/fixall-onsave.test.ts b/packages/vscode/e2e/lint/suite/fixall-onsave.test.ts index c36925b..88e06ab 100644 --- a/packages/vscode/e2e/lint/suite/fixall-onsave.test.ts +++ b/packages/vscode/e2e/lint/suite/fixall-onsave.test.ts @@ -1,5 +1,4 @@ -// Ported from web-infra-dev/rslint (deviation: setup waits go through -// waitForDiagnosticsWithRuleIds -- see fixall-helpers.ts for why). +// Ported from web-infra-dev/rslint (typed setup waits from 72cd2b1f). // `packages/vscode-extension/__tests__/suite/fixall-onsave.test.ts` (origin/main). import * as assert from 'assert'; import * as vscode from 'vscode'; @@ -7,7 +6,7 @@ import { getRslintDiagnostics } from '../utils/diagnostics'; import { waitForCodeActionRegistryQuiescence } from '../utils/codeActionRegistry'; import { waitForDiagnostics, - waitForDiagnosticsWithRuleIds, + waitForTypeAssertionDiagnostics, waitForDiagnosticsCount, waitForContentChange, diagnosticRuleIdIncludes, @@ -41,10 +40,7 @@ suite('rslint fixAll - on-save', function () { "const gfVal: string = 'x';\nconst gfRes = (gfVal as string).trim();\n", ); - const diags = await waitForDiagnosticsWithRuleIds( - doc, - 'no-unnecessary-type-assertion', - ); + const diags = await waitForTypeAssertionDiagnostics(doc); assertHasFixableDiagnostic(diags, 'generic source.fixAll setup'); await saveDocumentOnce( @@ -75,10 +71,7 @@ suite('rslint fixAll - on-save', function () { ].join('\n'); await replaceAll(editor, fixableContent); - const diags = await waitForDiagnosticsWithRuleIds( - doc, - 'no-unnecessary-type-assertion', - ); + const diags = await waitForTypeAssertionDiagnostics(doc); assertHasFixableDiagnostic(diags, 'fixable on-save setup'); await saveDocumentOnce(doc, 'Fixable document should save'); @@ -102,10 +95,7 @@ suite('rslint fixAll - on-save', function () { editor, "const probeVal: string = 'x';\nconst probeRes = (probeVal as string).trim();\n", ); - const probeDiags = await waitForDiagnosticsWithRuleIds( - doc, - 'no-unnecessary-type-assertion', - ); + const probeDiags = await waitForTypeAssertionDiagnostics(doc); assertHasFixableDiagnostic(probeDiags, 'clean-file probe setup'); await saveDocumentOnce(doc, 'Clean-file probe should save'); await waitForContentChange( @@ -142,10 +132,7 @@ suite('rslint fixAll - on-save', function () { editor, "const probeVal2: string = 'x';\nconst probeRes2 = (probeVal2 as string).trim();\n", ); - const probeDiags = await waitForDiagnosticsWithRuleIds( - doc, - 'no-unnecessary-type-assertion', - ); + const probeDiags = await waitForTypeAssertionDiagnostics(doc); assertHasFixableDiagnostic(probeDiags, 'non-fixable probe setup'); await saveDocumentOnce(doc, 'Non-fixable probe should save'); await waitForContentChange( diff --git a/packages/vscode/e2e/lint/suite/fixall.test.ts b/packages/vscode/e2e/lint/suite/fixall.test.ts index a37ba51..0eba05f 100644 --- a/packages/vscode/e2e/lint/suite/fixall.test.ts +++ b/packages/vscode/e2e/lint/suite/fixall.test.ts @@ -1,10 +1,10 @@ -// Ported from web-infra-dev/rslint (deviation: setup waits go through -// waitForDiagnosticsWithRuleIds -- see fixall-helpers.ts for why). +// Ported from web-infra-dev/rslint (typed setup waits from 72cd2b1f). // `packages/vscode-extension/__tests__/suite/fixall.test.ts` (origin/main). import * as assert from 'assert'; import * as vscode from 'vscode'; import { waitForDiagnostics, + waitForTypeAssertionDiagnostics, waitForDiagnosticsWithRuleIds, waitForDiagnosticsToChange, waitForDiagnosticsCount, @@ -130,10 +130,7 @@ suite('rslint fixAll - code actions', function () { const fixableContent = "const frVal: string = 'hello';\nconst frRes = (frVal as string).toUpperCase();\n"; await withTmpFile(fixableContent, async (doc) => { - const initialDiags = await waitForDiagnosticsWithRuleIds( - doc, - 'no-unnecessary-type-assertion', - ); + const initialDiags = await waitForTypeAssertionDiagnostics(doc); assert.ok(initialDiags.length > 0, 'Should have initial diagnostics'); const fixableDiags = initialDiags.filter((d) => @@ -172,6 +169,8 @@ suite('rslint fixAll - code actions', function () { '', ].join('\n'); await withTmpFile(mixedContent, async (doc) => { + // Deviation from upstream's waitForTypeAssertionDiagnostics: also await + // the non-fixable typed rule, keeping assertions independent of batching. const initialDiags = await waitForDiagnosticsWithRuleIds( doc, 'no-unnecessary-type-assertion', @@ -258,10 +257,7 @@ suite('rslint fixAll - code actions', function () { const fixableContent = "const sfVal: string = 'x';\nconst sfRes = (sfVal as string).trim();\n"; await withTmpFile(fixableContent, async (doc) => { - const initialDiags = await waitForDiagnosticsWithRuleIds( - doc, - 'no-unnecessary-type-assertion', - ); + const initialDiags = await waitForTypeAssertionDiagnostics(doc); const fixableCount = initialDiags.filter((d) => diagnosticRuleIdIncludes(d, 'no-unnecessary-type-assertion'), ).length; diff --git a/packages/vscode/licensePlugin.mts b/packages/vscode/licensePlugin.mts new file mode 100644 index 0000000..2e60be3 --- /dev/null +++ b/packages/vscode/licensePlugin.mts @@ -0,0 +1,106 @@ +import { readFileSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; +import type { RsbuildPlugin } from '@rsbuild/core'; +import WebpackLicensePlugin from 'webpack-license-plugin'; + +type PackageLicense = { + name: string; + version: string; + license?: string; + licenseText?: string; + repository?: string; +}; + +// Yuku's npm packages omit LICENSE. Copied from the upstream MIT notice: +// https://github.com/yuku-toolchain/yuku/blob/main/LICENSE +const yukuLicenseText = `MIT License + +Copyright (c) 2026 Yuku + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +`; + +export function licensePlugin( + packageRoot: string, + yukuBindingName: string, +): RsbuildPlugin { + const packages = new Map(); + return { + name: 'rstack-third-party-licenses', + setup(api) { + api.modifyRspackConfig((config, { environment }) => { + // Separate inventories prevent concurrent lib compilations from + // overwriting each other. Only the merged Markdown ships in the VSIX. + const filename = `.licenses/${environment.name}.json`; + config.plugins ??= []; + config.plugins.push( + new WebpackLicensePlugin({ + outputFilename: filename, + additionalFiles: { + [filename]: (inventory) => { + for (const pkg of inventory) + packages.set(`${pkg.name}@${pkg.version}`, pkg); + return JSON.stringify(inventory, null, 2); + }, + }, + }), + ); + }); + api.onAfterBuild(({ stats }) => { + if (stats?.hasErrors()) return; + const notices = [...packages.values()].filter( + ({ name }) => name !== 'yuku-parser', + ); + for (const name of ['yuku-parser', yukuBindingName]) { + notices.push({ + name, + version: '', + license: 'MIT', + repository: 'https://github.com/yuku-toolchain/yuku', + licenseText: yukuLicenseText, + }); + } + const body = notices + .sort( + (a, b) => + a.name.localeCompare(b.name) || + a.version.localeCompare(b.version), + ) + .map((pkg) => { + const text = pkg.licenseText; + if (!text) throw new Error(`Missing license text for ${pkg.name}`); + return `### ${pkg.name}${pkg.version ? ` (${pkg.version})` : ''}\n\nLicensed under ${pkg.license} license${pkg.repository ? ` in the repository at ${pkg.repository}` : ''}.\n\n${text + .trim() + .split('\n') + .map((line) => `> ${line}`) + .join('\n')}`; + }) + .join('\n\n'); + const header = readFileSync( + path.resolve(packageRoot, '../../LICENSE'), + 'utf8', + ).trimEnd(); + writeFileSync( + path.join(packageRoot, 'LICENSE.md'), + `${header}\n\n## Third-party licenses\n\nThe following third-party packages are bundled into the Rstack VS Code extension.\n\n${body}\n`, + ); + }); + }, + }; +} diff --git a/packages/vscode/package.json b/packages/vscode/package.json index 945c878..9d55dfc 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -160,7 +160,7 @@ ], "default": "off", "scope": "window", - "description": "Traces the communication between VS Code and the Rslint language server" + "markdownDescription": "Trace communication for every Rslint language-server connection in this VS Code window. Changes apply immediately without restarting a server. `verbose` tracing can include source text and file paths." } } }, @@ -323,7 +323,7 @@ ], "default": "off", "scope": "window", - "description": "Traces the communication between VS Code and the rs fmt language server (`messages` also shows how long each formatting request took)" + "markdownDescription": "Trace communication for every `rs fmt` language-server connection in this VS Code window. Changes apply immediately without restarting a server. `verbose` tracing can include source text and file paths. `messages` also shows how long each formatting request took." } } } @@ -455,7 +455,8 @@ "valibot": "^1.4.2", "vscode-jsonrpc": "^8.2.1", "vscode-languageclient": "^9.0.1", - "yuku-parser": "^0.8.3" + "webpack-license-plugin": "^4.5.1", + "yuku-parser": "^0.12.0" }, "engines": { "vscode": "^1.97.0" diff --git a/packages/vscode/rslib.config.mts b/packages/vscode/rslib.config.mts index 7be77a8..382cbbb 100644 --- a/packages/vscode/rslib.config.mts +++ b/packages/vscode/rslib.config.mts @@ -66,15 +66,6 @@ const libs: LibConfig[] = [ from: yukuBindingPath, to: `@yuku-parser/binding-${yukuBindingSuffix}/yuku-parser.node`, }, - { - // The VSIX must carry a LICENSE (`.vscodeignore` keeps it), and - // the workspace root LICENSE is the single source of truth. The - // copy lands next to package.json — not in dist/ — because vsce - // packages from the package directory; it is gitignored. - from: path.join(rootDir, '..', '..', 'LICENSE'), - to: path.join(rootDir, 'LICENSE'), - toType: 'file', - }, ], }), ], @@ -125,7 +116,7 @@ libs.push({ * `src/` bind in the dev host; the release build (`build`, used by CI and the * Release workflow) minifies and emits no source maps. */ -export default defineConfig(({ envMode }) => { +export default defineConfig(async ({ envMode }) => { const devBuild = envMode === 'dev'; const output: LibConfig['output'] = { target: 'node', @@ -133,5 +124,19 @@ export default defineConfig(({ envMode }) => { sourceMap: devBuild, minify: !devBuild, }; - return { lib: libs.map((lib) => ({ ...lib, output })) }; + const generateLicenses = + !devBuild && + process.argv.includes('build') && + !process.argv.includes('--watch'); + return { + lib: libs.map((lib) => ({ ...lib, output })), + plugins: generateLicenses + ? [ + (await import('./licensePlugin.mts')).licensePlugin( + rootDir, + `@yuku-parser/binding-${yukuBindingSuffix}`, + ), + ] + : [], + }; }); diff --git a/packages/vscode/src/shared/missingDependency.ts b/packages/vscode/src/shared/missingDependency.ts index 1a29828..3029e6a 100644 --- a/packages/vscode/src/shared/missingDependency.ts +++ b/packages/vscode/src/shared/missingDependency.ts @@ -21,9 +21,10 @@ export function classifyMissingDependencyMessage( message: string, ): string | undefined { const [firstLine] = message.split('\n', 1); - const specifier = /^Cannot find (?:package|module) '([^']+)'/.exec( + const match = /(?:^|: )(Cannot find (?:package|module) '([^']+)'[^\n]*)/.exec( firstLine, - )?.[1]; + ); + const specifier = match?.[2]; if ( specifier === undefined || specifier.startsWith('.') || @@ -32,7 +33,7 @@ export function classifyMissingDependencyMessage( ) { return undefined; } - return firstLine; + return match?.[1]; } /** Use for a (code, message) pair; bare messages (fmt) use the classifier directly. */ diff --git a/packages/vscode/src/stacks/lint/worker/PluginLintPool.ts b/packages/vscode/src/stacks/lint/worker/PluginLintPool.ts index ded2e48..94c87c2 100644 --- a/packages/vscode/src/stacks/lint/worker/PluginLintPool.ts +++ b/packages/vscode/src/stacks/lint/worker/PluginLintPool.ts @@ -6,6 +6,8 @@ import type { } from '@rslint/core/eslint-plugin'; import type { CancellationToken } from 'vscode-jsonrpc/node'; import type { WorkerLogger } from './logger'; +import type { ConfigDependencyStatusNotification } from './configDependencyProtocol'; +import { classifyMissingDependencyMessage } from '../../../shared/missingDependency'; type PluginHostFactory = ( configs: ConfigDescriptor[], @@ -27,6 +29,11 @@ export class PluginLintPool { private readonly shutdowns = new Set>(); private opChain: Promise = Promise.resolve(); private disposed = false; + private status: ConfigDependencyStatusNotification = { kind: 'ok' }; + + get configDependencyStatus(): ConfigDependencyStatusNotification { + return this.status; + } constructor( private readonly logger: WorkerLogger, @@ -55,6 +62,8 @@ export class PluginLintPool { return; } + const previous = this.status; + this.status = { kind: 'ok' }; if ( this.activeState?.ready && this.activeState.fingerprint === fingerprint @@ -98,6 +107,28 @@ export class PluginLintPool { this.generations.set(generation, state); ready = true; } catch (error: unknown) { + const message = error instanceof Error ? error.message : String(error); + const firstLine = message.split('\n', 1)[0]; + const descriptor = + descriptors.find((d) => firstLine.includes(d.configPath)) ?? + descriptors[0]; + const cause = classifyMissingDependencyMessage(firstLine); + const failure: Exclude< + ConfigDependencyStatusNotification, + { kind: 'ok' } + > = + cause === undefined + ? { + kind: 'error', + message: `ESLint plugins failed to load: ${firstLine}`, + } + : { + kind: 'missing', + failure: { + configPath: descriptor.configPath, + cause, + }, + }; const state: HostGeneration = { fingerprint, ready: false, @@ -106,7 +137,15 @@ export class PluginLintPool { }; this.liveStates.add(state); this.generations.set(generation, state); - this.logger.error('Failed to initialize ESLint-plugin host', error); + this.status = failure; + // The editor emits the shared one-line missing-dependency warning. + // Real errors retain their full detail here, once per failure episode. + if ( + failure.kind === 'error' && + !(previous.kind === 'error' && previous.message === failure.message) + ) { + this.logger.error('Failed to initialize ESLint-plugin host', error); + } } }); return ready; diff --git a/packages/vscode/src/stacks/lint/worker/index.ts b/packages/vscode/src/stacks/lint/worker/index.ts index 4d559da..a02b629 100644 --- a/packages/vscode/src/stacks/lint/worker/index.ts +++ b/packages/vscode/src/stacks/lint/worker/index.ts @@ -256,10 +256,14 @@ export async function runLintWorker( protocolVersion: installation.protocolVersion, configPath: options.configPath, takeConfigStatus: () => { - const failure = configDependencyFailure; - const message = configError; + let failure = configDependencyFailure; + let message = configError; configDependencyFailure = undefined; configError = undefined; + // Plugin status persists across refreshes without a new prepare. + const plugin = pluginLintPool.configDependencyStatus; + if (plugin.kind === 'error') message ??= plugin.message; + else if (plugin.kind === 'missing') failure ??= plugin.failure; if (message !== undefined) return { kind: 'error', message }; if (failure !== undefined) return { kind: 'missing', failure }; return { kind: 'ok' }; diff --git a/packages/vscode/tests/shared/missingDependency.test.ts b/packages/vscode/tests/shared/missingDependency.test.ts index 85fbf37..6fd8500 100644 --- a/packages/vscode/tests/shared/missingDependency.test.ts +++ b/packages/vscode/tests/shared/missingDependency.test.ts @@ -102,6 +102,27 @@ describe('missingDependencyCauseOf', () => { }); describe('classifyMissingDependencyMessage', () => { + it('classifies a prefixed first line without keeping wrappers or the stack', () => { + expect( + classifyMissingDependencyMessage( + "worker init failed: failed to import config file /project/config.mjs: Cannot find package 'missing' imported from /project/plugin.mjs\nRequire stack:\n- worker", + ), + ).toBe("Cannot find package 'missing' imported from /project/plugin.mjs"); + expect( + classifyMissingDependencyMessage( + "Config failed\nworker init failed: Cannot find package 'missing'", + ), + ).toBeUndefined(); + }); + + it('keeps prefixed relative imports as real errors', () => { + expect( + classifyMissingDependencyMessage( + "worker init failed: Cannot find module './missing.js'", + ), + ).toBeUndefined(); + }); + it('classifies loader messages without requiring an Error code', () => { expect( classifyMissingDependencyMessage( diff --git a/packages/vscode/tests/stacks/lint/pluginLintPool.test.ts b/packages/vscode/tests/stacks/lint/pluginLintPool.test.ts new file mode 100644 index 0000000..269d502 --- /dev/null +++ b/packages/vscode/tests/stacks/lint/pluginLintPool.test.ts @@ -0,0 +1,40 @@ +import { expect, it, rs } from '@rstest/core'; +import { PluginLintPool } from '../../../src/stacks/lint/worker/PluginLintPool'; +import { WorkerLogger } from '../../../src/stacks/lint/worker/logger'; + +it('logs the full error once per failure episode and again after recovery then re-failure', async () => { + const descriptors = [ + { configPath: '/project/rslint.config.mjs', configDirectory: '/project' }, + ]; + const logger = new WorkerLogger(); + const log = rs.spyOn(logger, 'error').mockImplementation(() => {}); + const error = new Error('plugin exploded\nfull stack detail'); + const createHost = rs.fn().mockRejectedValue(error); + const pool = new PluginLintPool(logger, createHost); + try { + await pool.prepare(descriptors, 'same', 'initial'); + await pool.commit('initial'); + await pool.prepare(descriptors, 'same', 'retry'); + expect(createHost).toHaveBeenCalledTimes(2); + expect(log).toHaveBeenCalledTimes(1); + expect(log.mock.calls[0]).toEqual([ + 'Failed to initialize ESLint-plugin host', + error, + ]); + await pool.abort('retry'); + createHost.mockResolvedValue({ + lint: async () => ({ results: [] }), + shutdown: async () => {}, + }); + await pool.prepare(descriptors, 'same', 'fixed'); + createHost.mockRejectedValue(error); + await pool.prepare(descriptors, 'changed', 'broken-again'); + expect(log).toHaveBeenCalledTimes(2); + expect(log.mock.calls[1]).toEqual([ + 'Failed to initialize ESLint-plugin host', + error, + ]); + } finally { + await pool.dispose(); + } +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a028b50..eb6dc11 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -92,9 +92,12 @@ importers: vscode-languageclient: specifier: ^9.0.1 version: 9.0.1 + webpack-license-plugin: + specifier: ^4.5.1 + version: 4.5.1(webpack@5.111.1) yuku-parser: - specifier: ^0.8.3 - version: 0.8.3 + specifier: ^0.12.0 + version: 0.12.0 packages: @@ -365,6 +368,22 @@ packages: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/source-map@0.3.11': + resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@napi-rs/keyring-darwin-arm64@1.3.0': resolution: {integrity: sha512-pl76hJvdYUBn6I24bXiOBMA9nbDapo3I5B+f3OorjDU4dUMSypXeKbOVehJe8fhgTiH24flMyTS3aAIy43xegQ==} engines: {node: '>= 10'} @@ -1129,12 +1148,18 @@ packages: '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/istanbul-lib-coverage@2.0.6': resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} '@types/istanbul-lib-report@3.0.3': resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/mocha@10.0.10': resolution: {integrity: sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==} @@ -1217,79 +1242,148 @@ packages: engines: {node: '>= 20'} hasBin: true - '@yuku-parser/binding-android-arm64@0.8.3': - resolution: {integrity: sha512-vySYRsMeul9ssvxeHdxgS9ZUIcq7gqljWNqgokjJE0uQWvVvOprihJ6hOsiifVqWsla0BMc3vAFBvNS9QqCw7g==} + '@webassemblyjs/ast@1.14.1': + resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} + + '@webassemblyjs/floating-point-hex-parser@1.13.2': + resolution: {integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==} + + '@webassemblyjs/helper-api-error@1.13.2': + resolution: {integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==} + + '@webassemblyjs/helper-buffer@1.14.1': + resolution: {integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==} + + '@webassemblyjs/helper-numbers@1.13.2': + resolution: {integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==} + + '@webassemblyjs/helper-wasm-bytecode@1.13.2': + resolution: {integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==} + + '@webassemblyjs/helper-wasm-section@1.14.1': + resolution: {integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==} + + '@webassemblyjs/ieee754@1.13.2': + resolution: {integrity: sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==} + + '@webassemblyjs/leb128@1.13.2': + resolution: {integrity: sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==} + + '@webassemblyjs/utf8@1.13.2': + resolution: {integrity: sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==} + + '@webassemblyjs/wasm-edit@1.14.1': + resolution: {integrity: sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==} + + '@webassemblyjs/wasm-gen@1.14.1': + resolution: {integrity: sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==} + + '@webassemblyjs/wasm-opt@1.14.1': + resolution: {integrity: sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==} + + '@webassemblyjs/wasm-parser@1.14.1': + resolution: {integrity: sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==} + + '@webassemblyjs/wast-printer@1.14.1': + resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} + + '@xtuc/ieee754@1.2.0': + resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} + + '@xtuc/long@4.2.2': + resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + + '@yuku-parser/binding-android-arm64@0.12.0': + resolution: {integrity: sha512-TUVnV1+c+wnxPS/mR5ReMTP6V2favLaoYCMi2Mm/nxv02WXFk3sKk2acnhDmQJEWOSr8bee1p1XiI/HoLRAySQ==} cpu: [arm64] os: [android] - '@yuku-parser/binding-darwin-arm64@0.8.3': - resolution: {integrity: sha512-+wpB/wqhiZ685Y77I+lj6v9pHSAJ3Y+QMHJmvch0Q0ahIMbNwtKk3s54MhtjCMKO1qpjPbyN/PjuHDg2hbKaVQ==} + '@yuku-parser/binding-darwin-arm64@0.12.0': + resolution: {integrity: sha512-8kHul5s57muWcymw/f3Oq99UQurJ7oHhSmrc8/aYTC1kCEA8ZGEh2N88BM4fTUnv5cUbM7/eQogwWPkqWG3QaQ==} cpu: [arm64] os: [darwin] - '@yuku-parser/binding-darwin-x64@0.8.3': - resolution: {integrity: sha512-jKqiWejj4zVy7pPtEGu4/Ty+pG1h7ooQOXIkm7shKZTSwTU9X8X+eoH11uIeKHZi2SQWV0GhNz0J56eerseysQ==} + '@yuku-parser/binding-darwin-x64@0.12.0': + resolution: {integrity: sha512-V+CzqRz1pZRsU6CpqEHzpmLgQ1gpgpvGU9KUTenEgrjO6TRF3QNAKVMDvYMmxq13aqt3CnnEk/X7/5CE8BgGQg==} cpu: [x64] os: [darwin] - '@yuku-parser/binding-freebsd-x64@0.8.3': - resolution: {integrity: sha512-FC7zSwzFzd4z9bsId07CiHLR+Iw6yW/LzIQhL5AUtPUuVXLgEyx0rilgbRUYkl1CT3GJcLpkh63WuPZUSgCDzw==} + '@yuku-parser/binding-freebsd-x64@0.12.0': + resolution: {integrity: sha512-Kdx8QK9ZbxavZ8tb2H3e55etEOYZMRiyQnIIJjGlwmJ/TW5PPNJwEu6/Pqmj0ViCdiZFyay7Hk3OvamgU9xURQ==} cpu: [x64] os: [freebsd] - '@yuku-parser/binding-linux-arm-gnu@0.8.3': - resolution: {integrity: sha512-So61j88b9/ygDnUPlWCm1EUPw4HSxAyDjrNHKgud5N3aRDQ3kw94nW7TriXbo7GBXID9oBHCMNm1r1Fof/Df5Q==} + '@yuku-parser/binding-linux-arm-gnu@0.12.0': + resolution: {integrity: sha512-DU+VMCe7dGXtRWDyltUA6qeXzf6NREIzzk/VJhdZXYDKa81SAQJFXsOYVcRzB09l9nwZluUpLPyvup1CxAlQ3g==} cpu: [arm] os: [linux] libc: [glibc] - '@yuku-parser/binding-linux-arm-musl@0.8.3': - resolution: {integrity: sha512-Nmnn20yJvSSKL8ZdtqReBRSGCDkSMqR5jEk/Sk/cdIdZmqVD49Z6M7w2GbMjdrxMI1MBPbsWFMMWxa93cd5t5g==} + '@yuku-parser/binding-linux-arm-musl@0.12.0': + resolution: {integrity: sha512-BtkS2oFmk/IRj2dkaNAJMTyB3oJDD4Xy8nNai00S0AIlvFs757y6Mokb7CZ+44gxlv1zYtQNkUz2IClzhWLUGA==} cpu: [arm] os: [linux] libc: [musl] - '@yuku-parser/binding-linux-arm64-gnu@0.8.3': - resolution: {integrity: sha512-Lfgw7AXJ0rxu6BMPGgfc8HLJWEIr8BHhCzcQp/75k+NM90uCLkHlBNqIg/K42KlSvBgAvu9euOvjdswib+4qJA==} + '@yuku-parser/binding-linux-arm64-gnu@0.12.0': + resolution: {integrity: sha512-l2w9y3199BeIcPr3vQ5VCH6G+R2Lm8cjpORUEOo+ImlfdwGODiRvRX55SsLt7dxXsFYDZSvJfUqGuPRKsz1lBQ==} cpu: [arm64] os: [linux] libc: [glibc] - '@yuku-parser/binding-linux-arm64-musl@0.8.3': - resolution: {integrity: sha512-cfRyu87xsJ0tFkHNsnMC4Rq6+xsFJ6i2dc4VAH52d2qLvykEJU/Mdi3ul1O2PyOApX/LoLT3uQZ0fWs3D5XE4w==} + '@yuku-parser/binding-linux-arm64-musl@0.12.0': + resolution: {integrity: sha512-+uq2hR8ex4Vkex9d4Ok32GJgHlEv1KPwSRJuoycLtbMa+z96QT6NSGQOS6S06o4CCou0VlEedhtFToL0+5ryRg==} cpu: [arm64] os: [linux] libc: [musl] - '@yuku-parser/binding-linux-x64-gnu@0.8.3': - resolution: {integrity: sha512-GcQQCUuYxbm6P1n+io/A50rvWKDeWHutIp6rW0ycDOZuEQjOb8hDVgS88+NDyOnd9FfS0/Z6GXopcRFDyKpzOg==} + '@yuku-parser/binding-linux-x64-gnu@0.12.0': + resolution: {integrity: sha512-pzcvJ90sJy1SUrZfJdza8s6fUHF7aEIbQTQqFF3NJsGy3EN4FIKIKaxO2hO9KGbm7KcyqFJseNuy4Js8x9PBfw==} cpu: [x64] os: [linux] libc: [glibc] - '@yuku-parser/binding-linux-x64-musl@0.8.3': - resolution: {integrity: sha512-rMkImBGZzg7GZlj8krYtdiezyjYI4igjKWMut5T65jHyNWFigMQrEpn9mDIBflloW9FKhGE3mN6yTZ/N+4HRwg==} + '@yuku-parser/binding-linux-x64-musl@0.12.0': + resolution: {integrity: sha512-C22Sze50kYeJItlJzllGOBbRFwZD/10KzfPW+VU+ZEf6sbW2zYdNG6KftxG/O5hHpyTY+vtaCxTUHDyyDNXn2A==} cpu: [x64] os: [linux] libc: [musl] - '@yuku-parser/binding-win32-arm64@0.8.3': - resolution: {integrity: sha512-/2Pl2cAzCXWxah8FqJapEj/ikpt9cEutEZFCa0hnbfrshkn5+C+aBM3ZDq62d1jsgQjBMmqr5HVhJUA4OAG/Tg==} + '@yuku-parser/binding-win32-arm64@0.12.0': + resolution: {integrity: sha512-PEfsEriglnn6Bg4FY+64kroH3J6sb3D6ERU6I1MgUkOZzZ4Of2GozN1qVRhoyEzFH1MpOgohvKI26PAihVJ4VQ==} cpu: [arm64] os: [win32] - '@yuku-parser/binding-win32-x64@0.8.3': - resolution: {integrity: sha512-Ntnvjoan9jnfLhn7Kn3h8j/bhsbVdQSVmKUqFULKtmwImLCJVHOJbLL4qbEJyrOQ7r/FBL1/c/dRvx/AQWzzXg==} + '@yuku-parser/binding-win32-x64@0.12.0': + resolution: {integrity: sha512-UKaOCLIyk/ldbuw3yJz1ZDvctfQZlGhE/tanpMWWuEzKDj5CzmVf4Kvz6BHJU92k6DNEsWdv9isGv5YeyinUKA==} cpu: [x64] os: [win32] - '@yuku-toolchain/types@0.8.3': - resolution: {integrity: sha512-9LN3HYs3A9qSPVFunsxlbfwBcUgexti3TmhOzIxB/UH8zFuaHQJXTRDcN17DW6cp1GsyZtiZA7f18uIra36Jag==} + '@yuku-toolchain/types@0.12.0': + resolution: {integrity: sha512-xhxRslz6/D43Aj2rKnp8s5Tr9RicAXsKnbTLqEvFPWSKsPUnZMvB609vYizL/Khmv899/Z7M2f7l6GJ46pfwTA==} + + acorn@8.18.0: + resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} + engines: {node: '>=0.4.0'} + hasBin: true agent-base@7.1.4: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv-keywords@5.1.0: + resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} + peerDependencies: + ajv: ^8.8.2 + ajv@8.20.0: resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} @@ -1343,6 +1437,11 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + baseline-browser-mapping@2.11.26: + resolution: {integrity: sha512-GLQdD3y6UF8iVuMJl5fHgE4jdn/ua7n+toKfLgNlg3BqQtOZjpy68T8Tup8/wGWZCDlm7KMg7tPb4MPn7oN0TQ==} + engines: {node: '>=6.0.0'} + hasBin: true + binaryextensions@6.11.0: resolution: {integrity: sha512-sXnYK/Ij80TO3lcqZVV2YgfKN5QjUWIRk/XSm2J/4bd/lPko3lvk0O4ZppH6m+6hB2/GTu+ptNwVFe1xh+QLQw==} engines: {node: '>=4'} @@ -1373,12 +1472,20 @@ packages: browser-stdout@1.3.1: resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} + browserslist@4.29.1: + resolution: {integrity: sha512-AUdjuRyCNGUYtqpqfTmWyM4fXay8yIQhmLnvYe/THMGfT9B/34X7xQd3ifKxwyNPPpowVBjLb+64BN9Rn1mizw==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} buffer-equal-constant-time@1.0.1: resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} @@ -1407,6 +1514,9 @@ packages: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} + caniuse-lite@1.0.30001812: + resolution: {integrity: sha512-qN+QNNBr93TCmFrmte0bBCjSDMuRvt78VlHT99qIGPszm4QsqCX8lnyWUFkHi8B7ZBAPq8SH+UqyXNy/odMdng==} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -1432,6 +1542,10 @@ packages: chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + chrome-trace-event@1.0.4: + resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} + engines: {node: '>=6.0'} + ci-info@2.0.0: resolution: {integrity: sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==} @@ -1473,6 +1587,9 @@ packages: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + commander@6.2.1: resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==} engines: {node: '>= 6'} @@ -1582,6 +1699,9 @@ packages: resolution: {integrity: sha512-UgGlf8IW75je7HZjNDpJdCv4cGJWIi6yumFdZ0R7A8/CIhQiWUjyGLCxdHpd8bmyD1gnkfUNK0oeOXqUS2cpfQ==} engines: {ecmascript: '>= es5', node: '>=4'} + electron-to-chromium@1.5.439: + resolution: {integrity: sha512-qu6QIPXhsb+CRcAiTMNjR4A1y/7tCYKkKjr5CZXRVih6qkDf79peZ2BpEU3qoDBNCRrcy3Mra3X9nG5oruuA7Q==} + emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} @@ -1597,6 +1717,10 @@ packages: end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + enhanced-resolve@5.25.1: + resolution: {integrity: sha512-nGXts5znJzmWPu+mIE9izCOzdg63oJca2mDzGWWTth7sr4aCToKcoyFVBQwN75Ij5Pf6p510EwkTqViTRzDV+w==} + engines: {node: '>=10.13.0'} + entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} @@ -1621,6 +1745,9 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-module-lexer@2.3.2: + resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} + es-object-atoms@1.1.2: resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} engines: {node: '>= 0.4'} @@ -1637,6 +1764,10 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + expand-template@2.0.3: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} @@ -1894,6 +2025,10 @@ packages: jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + jest-worker@27.5.1: + resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} + engines: {node: '>= 10.13.0'} + jiti@2.7.0: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true @@ -2010,6 +2145,9 @@ packages: resolution: {integrity: sha512-EDYo6VlmtnumlcBCbh1gLJ//9jvM/ndXHfVXIFrZVr6fGcwTUyCTFNTLCKuY3ffbK8L/+3Mzqnd58RojiZqHVw==} engines: {node: '>=20'} + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -2022,6 +2160,10 @@ packages: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + mime-types@2.1.35: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} @@ -2054,6 +2196,61 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + minimizer-webpack-plugin@5.12.0: + resolution: {integrity: sha512-PCN1x1OzFeFB2GlP91s46q7Gz8Jf16Z7M45bzpoqrYZlh12QeY6tYVM5h299wbwy1DxnQ69VFmmh8Hb2Ff8mbw==} + engines: {node: '>= 10.13.0'} + peerDependencies: + '@minify-html/node': '*' + '@napi-rs/image': '*' + '@swc/core': '*' + '@swc/css': '*' + '@swc/html': '*' + clean-css: '*' + cssnano: '*' + csso: '*' + esbuild: '*' + html-minifier-terser: '*' + imagemin: '*' + lightningcss: '*' + postcss: '*' + sharp: '*' + svgo: '*' + uglify-js: '*' + webpack: ^5.1.0 + peerDependenciesMeta: + '@minify-html/node': + optional: true + '@napi-rs/image': + optional: true + '@swc/core': + optional: true + '@swc/css': + optional: true + '@swc/html': + optional: true + clean-css: + optional: true + cssnano: + optional: true + csso: + optional: true + esbuild: + optional: true + html-minifier-terser: + optional: true + imagemin: + optional: true + lightningcss: + optional: true + postcss: + optional: true + sharp: + optional: true + svgo: + optional: true + uglify-js: + optional: true + minipass@7.1.3: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} @@ -2079,6 +2276,11 @@ packages: napi-build-utils@2.0.0: resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} + needle@3.5.0: + resolution: {integrity: sha512-jaQyPKKk2YokHrEg+vFDYxXIHTCBgiZwSHOoVx/8V3GIBS8/VN6NdVRmg8q1ERtPkMvmOvebsgga4sAj5hls/w==} + engines: {node: '>= 4.4.x'} + hasBin: true + node-abi@3.94.0: resolution: {integrity: sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==} engines: {node: '>=10'} @@ -2086,6 +2288,10 @@ packages: node-addon-api@4.3.0: resolution: {integrity: sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==} + node-releases@2.0.57: + resolution: {integrity: sha512-kQK9LGGFiHtrWiNhZtA7Qbw17AQz+dmsEKODRIVTXA9+e5MS/2gZEBhYJt13GrAz5/IOZKddH/0Z3TP/Zgo+yw==} + engines: {node: '>=18'} + node-sarif-builder@3.4.0: resolution: {integrity: sha512-tGnJW6OKRii9u/b2WiUViTJS+h7Apxx17qsMUjsUeNDiMMX5ZFf8F8Fcz7PAQ6omvOxHZtvDTmOYKJQwmfpjeg==} engines: {node: '>=20'} @@ -2335,6 +2541,10 @@ packages: resolution: {integrity: sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==} engines: {node: '>=11.0.0'} + schema-utils@4.5.0: + resolution: {integrity: sha512-zJlMCZ0cAR5p/Y4oVpRoqioDMJcGxaXRrQ/4rP4WyR84vc5z/DolXdbvXeDpTwbtocDFr2rhPqHPErDCUtz2kA==} + engines: {node: '>= 10.13.0'} + secretlint@10.2.2: resolution: {integrity: sha512-xVpkeHV/aoWe4vP4TansF622nBEImzCY73y/0042DuJ29iKIaqgoJ8fGxre3rVSHHbxar4FdJobmTnLp9AU0eg==} engines: {node: '>=20.0.0'} @@ -2401,6 +2611,13 @@ packages: resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} engines: {node: '>=10'} + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + spdx-correct@3.2.0: resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} @@ -2410,6 +2627,9 @@ packages: spdx-expression-parse@3.0.1: resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + spdx-expression-validate@2.0.0: + resolution: {integrity: sha512-b3wydZLM+Tc6CFvaRDBOF9d76oGIHNCLYFeHbftFXUWjnfZWganmDmvtM5sm1cRwJc/VDBMLyGGrsLFd1vOxbg==} + spdx-license-ids@3.0.23: resolution: {integrity: sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==} @@ -2474,6 +2694,10 @@ packages: resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} engines: {node: '>=10.0.0'} + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + tar-fs@2.1.5: resolution: {integrity: sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==} @@ -2485,6 +2709,11 @@ packages: resolution: {integrity: sha512-lk+vH+MccxNqgVqSnkMVKx4VLJfnLjDBGzH16JVZjKE2DoxP57s6/vt6JmXV5I3jBcfGrxNrYtC+mPtU7WJztA==} engines: {node: '>=18'} + terser@5.51.2: + resolution: {integrity: sha512-bWnjSNscmuI+GJze6ZupnHP8G/cTcsJF+bXCeQknk2SHQsgbNJnLrqiH9jZ2W4STPVXH2mDKKRX3iwPhc9Cn/Q==} + engines: {node: '>=10'} + hasBin: true + text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} @@ -2569,6 +2798,12 @@ packages: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} + update-browserslist-db@1.3.3: + resolution: {integrity: sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + url-join@4.0.1: resolution: {integrity: sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==} @@ -2608,6 +2843,30 @@ packages: vscode-languageserver-types@3.17.5: resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==} + watchpack@2.5.2: + resolution: {integrity: sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==} + engines: {node: '>=10.13.0'} + + webpack-license-plugin@4.5.1: + resolution: {integrity: sha512-WbjMSMPHzFePCSQJqP0qFskGPSwdf3JxIoXa1SK3d1hCXQee7KUrIb+riyXUId71kcHOSyE12pTyrhMl1ozszA==} + engines: {node: '>=16.0.0'} + peerDependencies: + webpack: '>=4.0.0 < 6.0.0' + + webpack-sources@3.6.0: + resolution: {integrity: sha512-EIMmPVvNI0CYXyRPp34F2Qk7W37/BZVZIofS6LAUnrNjCVahS+hO4mxwFJDjQjODTGZDtVzLYoN2+1dYkhk9qA==} + engines: {node: '>=10.13.0'} + + webpack@5.111.1: + resolution: {integrity: sha512-cNypaz0RP+S4cvQVi1M/3bRUkvNP0xZpL0TjFx+c6jg64VbxD+2weWDgY9UnjRI6dhZgtaj8DU76GGFcJ79xPQ==} + engines: {node: '>=10.13.0'} + hasBin: true + peerDependencies: + webpack-cli: '*' + peerDependenciesMeta: + webpack-cli: + optional: true + whatwg-encoding@3.1.1: resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} engines: {node: '>=18'} @@ -2695,11 +2954,8 @@ packages: resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} engines: {node: '>=18'} - yuku-ast@0.8.3: - resolution: {integrity: sha512-8x34yU5uhHUnJXzy2Qvjvec/vE9BzS0/2khVT1MsLmSLO/P8Q1Wp8IxHv+IhD+HMYETk6kherOSvP4JPWw2joQ==} - - yuku-parser@0.8.3: - resolution: {integrity: sha512-KPQcpF9aj77ywlJBIkQWCQ9DObdxnCA8AJdUOmA5CZZx042Xt4+dvbQmPJfWxF3E+KG5dVAZ2fBKuDJ8VsKWgA==} + yuku-parser@0.12.0: + resolution: {integrity: sha512-ZZBKBFdlHgTAhvV19gZ6i9CebsTBgZjFFS1GYpuowrK/BbgPeRidDXdD4wcHDrno7mxXiOwRaSLTzfUWXj5eGA==} snapshots: @@ -2988,6 +3244,25 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/source-map@0.3.11': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 + '@napi-rs/keyring-darwin-arm64@1.3.0': optional: true @@ -3584,12 +3859,16 @@ snapshots: '@types/deep-eql@4.0.2': {} + '@types/estree@1.0.9': {} + '@types/istanbul-lib-coverage@2.0.6': {} '@types/istanbul-lib-report@3.0.3': dependencies: '@types/istanbul-lib-coverage': 2.0.6 + '@types/json-schema@7.0.15': {} + '@types/mocha@10.0.10': {} '@types/node@22.20.2': @@ -3699,46 +3978,137 @@ snapshots: transitivePeerDependencies: - supports-color - '@yuku-parser/binding-android-arm64@0.8.3': + '@webassemblyjs/ast@1.14.1': + dependencies: + '@webassemblyjs/helper-numbers': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + + '@webassemblyjs/floating-point-hex-parser@1.13.2': {} + + '@webassemblyjs/helper-api-error@1.13.2': {} + + '@webassemblyjs/helper-buffer@1.14.1': {} + + '@webassemblyjs/helper-numbers@1.13.2': + dependencies: + '@webassemblyjs/floating-point-hex-parser': 1.13.2 + '@webassemblyjs/helper-api-error': 1.13.2 + '@xtuc/long': 4.2.2 + + '@webassemblyjs/helper-wasm-bytecode@1.13.2': {} + + '@webassemblyjs/helper-wasm-section@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/wasm-gen': 1.14.1 + + '@webassemblyjs/ieee754@1.13.2': + dependencies: + '@xtuc/ieee754': 1.2.0 + + '@webassemblyjs/leb128@1.13.2': + dependencies: + '@xtuc/long': 4.2.2 + + '@webassemblyjs/utf8@1.13.2': {} + + '@webassemblyjs/wasm-edit@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/helper-wasm-section': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-opt': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + '@webassemblyjs/wast-printer': 1.14.1 + + '@webassemblyjs/wasm-gen@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + + '@webassemblyjs/wasm-opt@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + + '@webassemblyjs/wasm-parser@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-api-error': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + + '@webassemblyjs/wast-printer@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@xtuc/long': 4.2.2 + + '@xtuc/ieee754@1.2.0': {} + + '@xtuc/long@4.2.2': {} + + '@yuku-parser/binding-android-arm64@0.12.0': optional: true - '@yuku-parser/binding-darwin-arm64@0.8.3': + '@yuku-parser/binding-darwin-arm64@0.12.0': optional: true - '@yuku-parser/binding-darwin-x64@0.8.3': + '@yuku-parser/binding-darwin-x64@0.12.0': optional: true - '@yuku-parser/binding-freebsd-x64@0.8.3': + '@yuku-parser/binding-freebsd-x64@0.12.0': optional: true - '@yuku-parser/binding-linux-arm-gnu@0.8.3': + '@yuku-parser/binding-linux-arm-gnu@0.12.0': optional: true - '@yuku-parser/binding-linux-arm-musl@0.8.3': + '@yuku-parser/binding-linux-arm-musl@0.12.0': optional: true - '@yuku-parser/binding-linux-arm64-gnu@0.8.3': + '@yuku-parser/binding-linux-arm64-gnu@0.12.0': optional: true - '@yuku-parser/binding-linux-arm64-musl@0.8.3': + '@yuku-parser/binding-linux-arm64-musl@0.12.0': optional: true - '@yuku-parser/binding-linux-x64-gnu@0.8.3': + '@yuku-parser/binding-linux-x64-gnu@0.12.0': optional: true - '@yuku-parser/binding-linux-x64-musl@0.8.3': + '@yuku-parser/binding-linux-x64-musl@0.12.0': optional: true - '@yuku-parser/binding-win32-arm64@0.8.3': + '@yuku-parser/binding-win32-arm64@0.12.0': optional: true - '@yuku-parser/binding-win32-x64@0.8.3': + '@yuku-parser/binding-win32-x64@0.12.0': optional: true - '@yuku-toolchain/types@0.8.3': {} + '@yuku-toolchain/types@0.12.0': {} + + acorn@8.18.0: {} agent-base@7.1.4: {} + ajv-formats@3.0.1(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + + ajv-keywords@5.1.0(ajv@8.20.0): + dependencies: + ajv: 8.20.0 + fast-deep-equal: 3.1.3 + ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 @@ -3782,6 +4152,8 @@ snapshots: base64-js@1.5.1: optional: true + baseline-browser-mapping@2.11.26: {} + binaryextensions@6.11.0: dependencies: editions: 6.22.0 @@ -3813,10 +4185,20 @@ snapshots: browser-stdout@1.3.1: {} + browserslist@4.29.1: + dependencies: + baseline-browser-mapping: 2.11.26 + caniuse-lite: 1.0.30001812 + electron-to-chromium: 1.5.439 + node-releases: 2.0.57 + update-browserslist-db: 1.3.3(browserslist@4.29.1) + buffer-crc32@0.2.13: {} buffer-equal-constant-time@1.0.1: {} + buffer-from@1.1.2: {} + buffer@5.7.1: dependencies: base64-js: 1.5.1 @@ -3853,6 +4235,8 @@ snapshots: camelcase@6.3.0: {} + caniuse-lite@1.0.30001812: {} + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -3892,6 +4276,8 @@ snapshots: chownr@1.1.4: optional: true + chrome-trace-event@1.0.4: {} + ci-info@2.0.0: {} cjs-module-lexer@2.2.1: {} @@ -3924,6 +4310,8 @@ snapshots: commander@12.1.0: {} + commander@2.20.3: {} + commander@6.2.1: {} core-js-pure@3.50.0: {} @@ -4035,6 +4423,8 @@ snapshots: dependencies: version-range: 4.15.0 + electron-to-chromium@1.5.439: {} + emoji-regex@10.6.0: {} emoji-regex@8.0.0: {} @@ -4051,6 +4441,11 @@ snapshots: once: 1.4.0 optional: true + enhanced-resolve@5.25.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + entities@4.5.0: {} entities@6.0.1: {} @@ -4063,6 +4458,8 @@ snapshots: es-errors@1.3.0: {} + es-module-lexer@2.3.2: {} + es-object-atoms@1.1.2: dependencies: es-errors: 1.3.0 @@ -4078,6 +4475,8 @@ snapshots: escape-string-regexp@4.0.0: {} + events@3.3.0: {} + expand-template@2.0.3: optional: true @@ -4327,6 +4726,12 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 + jest-worker@27.5.1: + dependencies: + '@types/node': 22.20.2 + merge-stream: 2.0.0 + supports-color: 8.1.1 + jiti@2.7.0: {} js-tokens@4.0.0: {} @@ -4449,6 +4854,8 @@ snapshots: meow@14.1.0: {} + merge-stream@2.0.0: {} + merge2@1.4.1: {} micromatch@4.0.8: @@ -4458,6 +4865,8 @@ snapshots: mime-db@1.52.0: {} + mime-db@1.54.0: {} + mime-types@2.1.35: dependencies: mime-db: 1.52.0 @@ -4484,6 +4893,14 @@ snapshots: minimist@1.2.8: optional: true + minimizer-webpack-plugin@5.12.0(webpack@5.111.1): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + jest-worker: 27.5.1 + schema-utils: 4.5.0 + terser: 5.51.2 + webpack: 5.111.1 + minipass@7.1.3: {} mkdirp-classic@0.5.3: @@ -4522,6 +4939,11 @@ snapshots: napi-build-utils@2.0.0: optional: true + needle@3.5.0: + dependencies: + iconv-lite: 0.6.3 + sax: 1.6.1 + node-abi@3.94.0: dependencies: semver: 7.8.5 @@ -4530,6 +4952,8 @@ snapshots: node-addon-api@4.3.0: optional: true + node-releases@2.0.57: {} + node-sarif-builder@3.4.0: dependencies: '@types/sarif': 2.1.7 @@ -4822,6 +5246,13 @@ snapshots: sax@1.6.1: {} + schema-utils@4.5.0: + dependencies: + '@types/json-schema': 7.0.15 + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) + ajv-keywords: 5.1.0(ajv@8.20.0) + secretlint@10.2.2(supports-color@8.1.1): dependencies: '@secretlint/config-creator': 10.2.2 @@ -4900,6 +5331,13 @@ snapshots: astral-regex: 2.0.0 is-fullwidth-code-point: 3.0.0 + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + spdx-correct@3.2.0: dependencies: spdx-expression-parse: 3.0.1 @@ -4912,6 +5350,10 @@ snapshots: spdx-exceptions: 2.5.0 spdx-license-ids: 3.0.23 + spdx-expression-validate@2.0.0: + dependencies: + spdx-expression-parse: 3.0.1 + spdx-license-ids@3.0.23: {} stacktrace-parser@0.1.11: @@ -4985,6 +5427,8 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 + tapable@2.3.3: {} + tar-fs@2.1.5: dependencies: chownr: 1.1.4 @@ -5007,6 +5451,13 @@ snapshots: ansi-escapes: 7.3.0 supports-hyperlinks: 3.2.0 + terser@5.51.2: + dependencies: + '@jridgewell/source-map': 0.3.11 + acorn: 8.18.0 + commander: 2.20.3 + source-map-support: 0.5.21 + text-table@0.2.0: {} textextensions@6.11.0: @@ -5076,6 +5527,12 @@ snapshots: universalify@2.0.1: {} + update-browserslist-db@1.3.3(browserslist@4.29.1): + dependencies: + browserslist: 4.29.1 + escalade: 3.2.0 + picocolors: 1.1.1 + url-join@4.0.1: {} util-deprecate@1.0.2: {} @@ -5108,6 +5565,58 @@ snapshots: vscode-languageserver-types@3.17.5: {} + watchpack@2.5.2: + dependencies: + graceful-fs: 4.2.11 + + webpack-license-plugin@4.5.1(webpack@5.111.1): + dependencies: + chalk: 5.6.2 + lodash: 4.18.1 + needle: 3.5.0 + spdx-expression-validate: 2.0.0 + webpack: 5.111.1 + webpack-sources: 3.6.0 + + webpack-sources@3.6.0: {} + + webpack@5.111.1: + dependencies: + '@types/estree': 1.0.9 + '@types/json-schema': 7.0.15 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + browserslist: 4.29.1 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.25.1 + es-module-lexer: 2.3.2 + events: 3.3.0 + graceful-fs: 4.2.11 + mime-db: 1.54.0 + minimizer-webpack-plugin: 5.12.0(webpack@5.111.1) + schema-utils: 4.5.0 + tapable: 2.3.3 + watchpack: 2.5.2 + webpack-sources: 3.6.0 + transitivePeerDependencies: + - '@minify-html/node' + - '@napi-rs/image' + - '@swc/core' + - '@swc/css' + - '@swc/html' + - clean-css + - cssnano + - csso + - esbuild + - html-minifier-terser + - imagemin + - lightningcss + - postcss + - sharp + - svgo + - uglify-js + whatwg-encoding@3.1.1: dependencies: iconv-lite: 0.6.3 @@ -5195,24 +5704,19 @@ snapshots: yoctocolors-cjs@2.1.3: {} - yuku-ast@0.8.3: - dependencies: - '@yuku-toolchain/types': 0.8.3 - - yuku-parser@0.8.3: + yuku-parser@0.12.0: dependencies: - '@yuku-toolchain/types': 0.8.3 - yuku-ast: 0.8.3 + '@yuku-toolchain/types': 0.12.0 optionalDependencies: - '@yuku-parser/binding-android-arm64': 0.8.3 - '@yuku-parser/binding-darwin-arm64': 0.8.3 - '@yuku-parser/binding-darwin-x64': 0.8.3 - '@yuku-parser/binding-freebsd-x64': 0.8.3 - '@yuku-parser/binding-linux-arm-gnu': 0.8.3 - '@yuku-parser/binding-linux-arm-musl': 0.8.3 - '@yuku-parser/binding-linux-arm64-gnu': 0.8.3 - '@yuku-parser/binding-linux-arm64-musl': 0.8.3 - '@yuku-parser/binding-linux-x64-gnu': 0.8.3 - '@yuku-parser/binding-linux-x64-musl': 0.8.3 - '@yuku-parser/binding-win32-arm64': 0.8.3 - '@yuku-parser/binding-win32-x64': 0.8.3 + '@yuku-parser/binding-android-arm64': 0.12.0 + '@yuku-parser/binding-darwin-arm64': 0.12.0 + '@yuku-parser/binding-darwin-x64': 0.12.0 + '@yuku-parser/binding-freebsd-x64': 0.12.0 + '@yuku-parser/binding-linux-arm-gnu': 0.12.0 + '@yuku-parser/binding-linux-arm-musl': 0.12.0 + '@yuku-parser/binding-linux-arm64-gnu': 0.12.0 + '@yuku-parser/binding-linux-arm64-musl': 0.12.0 + '@yuku-parser/binding-linux-x64-gnu': 0.12.0 + '@yuku-parser/binding-linux-x64-musl': 0.12.0 + '@yuku-parser/binding-win32-arm64': 0.12.0 + '@yuku-parser/binding-win32-x64': 0.12.0 From da2ba03c02acfba8cccd326ea97a22d599188f03 Mon Sep 17 00:00:00 2001 From: fi3ework Date: Tue, 29 Sep 2026 11:10:41 +0800 Subject: [PATCH 2/4] refactor(vscode): ship webpack-license-plugin's own notices --- .gitignore | 4 +- packages/vscode/.vscodeignore | 3 +- packages/vscode/AGENTS.md | 2 +- packages/vscode/licensePlugin.mts | 106 ------------------------------ packages/vscode/rslib.config.mts | 38 +++++++---- 5 files changed, 30 insertions(+), 123 deletions(-) delete mode 100644 packages/vscode/licensePlugin.mts diff --git a/.gitignore b/.gitignore index 3a926f5..b2f67e3 100644 --- a/.gitignore +++ b/.gitignore @@ -158,8 +158,8 @@ packages/vscode/e2e/lint/fixtures/pnpm-lock.yaml packages/vscode/e2e/lint/fixtures/*/pnpm-lock.yaml packages/vscode/e2e/rstest/fixtures/*/pnpm-lock.yaml -# Build-time third-party notices, including the target-specific native binding -packages/vscode/LICENSE.md +# Build-time copy of the workspace root LICENSE (see rslib.config.mts) +packages/vscode/LICENSE # Design documents are working notes, not part of the published repo /DESIGN*.md diff --git a/packages/vscode/.vscodeignore b/packages/vscode/.vscodeignore index c0f1325..8b657f0 100644 --- a/packages/vscode/.vscodeignore +++ b/packages/vscode/.vscodeignore @@ -3,7 +3,8 @@ ** !dist/**/*.js !dist/**/*.node +!dist/*.licenses.json !icon.png -!LICENSE.md +!LICENSE !README.md !package.json diff --git a/packages/vscode/AGENTS.md b/packages/vscode/AGENTS.md index 039fbe0..1cab9fd 100644 --- a/packages/vscode/AGENTS.md +++ b/packages/vscode/AGENTS.md @@ -6,7 +6,7 @@ One extension replacing the standalone `rstack.rslint` and `rstack.rstest` exten - `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 ten ways (the "adaptations" below). When syncing upstream, preserve them. An eleventh 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 **41daaa1b** (0.12.2). This sync ports Rslint **84f9c9b5** trace-setting manifest wording, **72cd2b1f** fixAll waits, and plugin-host failure status (adaptations 4/7); Rstest **988f8e1d** license notices aggregate all three bundles into the sole shipped `LICENSE.md`, including the root MIT text. `@rstackjs/load-config` is not bundled; Yuku is **0.12.0**. Retained ports include per-document core resolution / pinned config paths (**39536fd6**, **892482e0**), legacy JSON watcher removal (**b7176723**), Unicode BOM coverage (**5fc197a5**), and Rstest public programmatic/list/watch APIs and exact filters (**8f945491**, **d2812754**, **99be33e8**, **88cd5f6d**, **994b77e0**). Diff `CoreResolver.ts`, `RuntimeManager.ts`, `WorkspaceDocumentRouter.ts`, and `Rslint.ts` on future syncs. The targeted 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. The earlier **d82db4fc31a61ee74b2a74917f14a458e1bca419** lifecycle fix is subsumed; failed-project retry and worker cleanup remain ahead of upstream. +- **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 **41daaa1b** (0.12.2). This sync ports Rslint **84f9c9b5** trace-setting manifest wording, **72cd2b1f** fixAll waits, and plugin-host failure status (adaptations 4/7). The Rstest **988f8e1d** license-notice port uses webpack-license-plugin's per-bundle JSON output with default license-text replenishment and explicit native-binding inclusion, shipped alongside the copied root `LICENSE`. `@rstackjs/load-config` is not bundled; Yuku is **0.12.0**. Retained ports include per-document core resolution / pinned config paths (**39536fd6**, **892482e0**), legacy JSON watcher removal (**b7176723**), Unicode BOM coverage (**5fc197a5**), and Rstest public programmatic/list/watch APIs and exact filters (**8f945491**, **d2812754**, **99be33e8**, **88cd5f6d**, **994b77e0**). Diff `CoreResolver.ts`, `RuntimeManager.ts`, `WorkspaceDocumentRouter.ts`, and `Rslint.ts` on future syncs. The targeted 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. The earlier **d82db4fc31a61ee74b2a74917f14a458e1bca419** lifecycle fix is subsumed; 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`). ## The ten adaptations diff --git a/packages/vscode/licensePlugin.mts b/packages/vscode/licensePlugin.mts deleted file mode 100644 index 2e60be3..0000000 --- a/packages/vscode/licensePlugin.mts +++ /dev/null @@ -1,106 +0,0 @@ -import { readFileSync, writeFileSync } from 'node:fs'; -import path from 'node:path'; -import type { RsbuildPlugin } from '@rsbuild/core'; -import WebpackLicensePlugin from 'webpack-license-plugin'; - -type PackageLicense = { - name: string; - version: string; - license?: string; - licenseText?: string; - repository?: string; -}; - -// Yuku's npm packages omit LICENSE. Copied from the upstream MIT notice: -// https://github.com/yuku-toolchain/yuku/blob/main/LICENSE -const yukuLicenseText = `MIT License - -Copyright (c) 2026 Yuku - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -`; - -export function licensePlugin( - packageRoot: string, - yukuBindingName: string, -): RsbuildPlugin { - const packages = new Map(); - return { - name: 'rstack-third-party-licenses', - setup(api) { - api.modifyRspackConfig((config, { environment }) => { - // Separate inventories prevent concurrent lib compilations from - // overwriting each other. Only the merged Markdown ships in the VSIX. - const filename = `.licenses/${environment.name}.json`; - config.plugins ??= []; - config.plugins.push( - new WebpackLicensePlugin({ - outputFilename: filename, - additionalFiles: { - [filename]: (inventory) => { - for (const pkg of inventory) - packages.set(`${pkg.name}@${pkg.version}`, pkg); - return JSON.stringify(inventory, null, 2); - }, - }, - }), - ); - }); - api.onAfterBuild(({ stats }) => { - if (stats?.hasErrors()) return; - const notices = [...packages.values()].filter( - ({ name }) => name !== 'yuku-parser', - ); - for (const name of ['yuku-parser', yukuBindingName]) { - notices.push({ - name, - version: '', - license: 'MIT', - repository: 'https://github.com/yuku-toolchain/yuku', - licenseText: yukuLicenseText, - }); - } - const body = notices - .sort( - (a, b) => - a.name.localeCompare(b.name) || - a.version.localeCompare(b.version), - ) - .map((pkg) => { - const text = pkg.licenseText; - if (!text) throw new Error(`Missing license text for ${pkg.name}`); - return `### ${pkg.name}${pkg.version ? ` (${pkg.version})` : ''}\n\nLicensed under ${pkg.license} license${pkg.repository ? ` in the repository at ${pkg.repository}` : ''}.\n\n${text - .trim() - .split('\n') - .map((line) => `> ${line}`) - .join('\n')}`; - }) - .join('\n\n'); - const header = readFileSync( - path.resolve(packageRoot, '../../LICENSE'), - 'utf8', - ).trimEnd(); - writeFileSync( - path.join(packageRoot, 'LICENSE.md'), - `${header}\n\n## Third-party licenses\n\nThe following third-party packages are bundled into the Rstack VS Code extension.\n\n${body}\n`, - ); - }); - }, - }; -} diff --git a/packages/vscode/rslib.config.mts b/packages/vscode/rslib.config.mts index 382cbbb..71e132e 100644 --- a/packages/vscode/rslib.config.mts +++ b/packages/vscode/rslib.config.mts @@ -3,6 +3,7 @@ import { createRequire } from 'node:module'; import path from 'node:path'; 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)); @@ -66,6 +67,15 @@ const libs: LibConfig[] = [ from: yukuBindingPath, to: `@yuku-parser/binding-${yukuBindingSuffix}/yuku-parser.node`, }, + { + // The VSIX must carry a LICENSE (`.vscodeignore` keeps it), and + // the workspace root LICENSE is the single source of truth. The + // copy lands next to package.json — not in dist/ — because vsce + // packages from the package directory; it is gitignored. + from: path.join(rootDir, '..', '..', 'LICENSE'), + to: path.join(rootDir, 'LICENSE'), + toType: 'file', + }, ], }), ], @@ -116,7 +126,7 @@ libs.push({ * `src/` bind in the dev host; the release build (`build`, used by CI and the * Release workflow) minifies and emits no source maps. */ -export default defineConfig(async ({ envMode }) => { +export default defineConfig(({ envMode }) => { const devBuild = envMode === 'dev'; const output: LibConfig['output'] = { target: 'node', @@ -124,19 +134,21 @@ export default defineConfig(async ({ envMode }) => { sourceMap: devBuild, minify: !devBuild, }; - const generateLicenses = - !devBuild && - process.argv.includes('build') && - !process.argv.includes('--watch'); return { lib: libs.map((lib) => ({ ...lib, output })), - plugins: generateLicenses - ? [ - (await import('./licensePlugin.mts')).licensePlugin( - rootDir, - `@yuku-parser/binding-${yukuBindingSuffix}`, - ), - ] - : [], + tools: { + rspack: (_, { appendPlugins, environment }) => { + appendPlugins( + new WebpackLicensePlugin({ + outputFilename: `${environment.name}.licenses.json`, + replenishDefaultLicenseTexts: true, + includePackages: () => + environment.name === 'cjs0' + ? [path.dirname(yukuBindingPath)] + : [], + }), + ); + }, + }, }; }); From 331656453a45e47209c07fc113e05a274ba5f6d4 Mon Sep 17 00:00:00 2001 From: fi3ework Date: Tue, 29 Sep 2026 11:31:38 +0800 Subject: [PATCH 3/4] refactor(vscode): report plugin-host failures as disabled --- packages/vscode/AGENTS.md | 2 +- .../plugin-host-failure/local-plugin.mjs | 2 - .../plugin-host-failure.test.ts | 45 +++++-------------- .../vscode/src/shared/missingDependency.ts | 7 ++- packages/vscode/src/stacks/lint/Rslint.ts | 8 ++++ .../src/stacks/lint/worker/PluginLintPool.ts | 41 +++++------------ .../lint/worker/configDependencyProtocol.ts | 5 ++- .../vscode/src/stacks/lint/worker/index.ts | 8 ++-- .../tests/shared/missingDependency.test.ts | 21 --------- .../tests/stacks/lint/pluginLintPool.test.ts | 39 ++++++++++++++++ 10 files changed, 83 insertions(+), 95 deletions(-) diff --git a/packages/vscode/AGENTS.md b/packages/vscode/AGENTS.md index 1cab9fd..ea5492a 100644 --- a/packages/vscode/AGENTS.md +++ b/packages/vscode/AGENTS.md @@ -17,7 +17,7 @@ One extension replacing the standalone `rstack.rslint` and `rstack.rstest` exten 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. 6. **Node runtime selection** (lint, test, fmt) — the Node a project-loading child process runs on is a **User Node runtime** chosen by the extension against one uniform floor, never assumed from PATH; the recovery path is the user's own shell, and the dividing line is the **load bound** (terms in CONTEXT.md; the full rule and rationale in `docs/adr/0001-node-runtime-selection.md`). All three callers — the lint worker, the rstest worker and the `rs fmt --lsp` server — take the decision from the one shared module (`shared/nodeResolution.ts`) and share one escape hatch, the resource-scoped `rstack.nodeExecutable` (`shared/nodeExecutableSetting.ts`); each appends its own consequence to the shared preflight message. -7. **Lint worker and Rstack bridge** — the extension host is only Rslint's language client. One vscode-free, editor-shipped lint worker per **Lint runtime** (one Rslint core inside one workspace folder — CONTEXT.md) runs on the User Node runtime, owns the Go LSP plus all five reverse requests, and derives the binary/config/plugin pieces from one explicit `@rslint/core` directory. Upstream's `CoreResolver` loads that core in the extension host; ours only walks to the directory (`fs.stat` + `package.json` + semver) and hands the path to the worker, and its `CoreInstallation` therefore carries paths, not module factories; upstream's installation cache goes with the module loading it memoized (`clear()` is a no-op kept for the `RuntimeManager` contract). A bridged folder passes only rstack's published `dist/rslintConfig.js` shim; neither the extension nor the worker re-implements Rstack config semantics. Because every supported config protocol locks `configPath` per process, the shim is part of the runtime key (`folder + core identity + shim`), which upstream — having no bridge — keys on the core alone. Why: `docs/adr/0003-lint-through-editor-worker.md`. The worker also sends the editor-only `rstack/rslintConfigDependency` notification (`stacks/lint/worker/configDependencyProtocol.ts`) when config loading finds a missing package. `ConfigTransactionAdapter` rewrites only that classified `rslint/loadConfigs` candidate's error message to its first line, so Go cannot echo a require stack beside the single warning. An initialized client whose initial configRefresh rejects with that verdict stays available for retry, rather than propagating a generic startup crash through RuntimeManager. Plugin-host failures with nonempty descriptors use this same notification: bare-package failures become `missing` / `disabled` with one stack-free warning; others become `error` / `crashed` with one-line detail and deduplicated full error in Output. Status survives aborted prepares; successful prepare or empty descriptors clears it. Shell poll / lockfile retries create a new transaction generation even with an unchanged fingerprint. +7. **Lint worker and Rstack bridge** — the extension host is only Rslint's language client. One vscode-free, editor-shipped lint worker per **Lint runtime** (one Rslint core inside one workspace folder — CONTEXT.md) runs on the User Node runtime, owns the Go LSP plus all five reverse requests, and derives the binary/config/plugin pieces from one explicit `@rslint/core` directory. Upstream's `CoreResolver` loads that core in the extension host; ours only walks to the directory (`fs.stat` + `package.json` + semver) and hands the path to the worker, and its `CoreInstallation` therefore carries paths, not module factories; upstream's installation cache goes with the module loading it memoized (`clear()` is a no-op kept for the `RuntimeManager` contract). A bridged folder passes only rstack's published `dist/rslintConfig.js` shim; neither the extension nor the worker re-implements Rstack config semantics. Because every supported config protocol locks `configPath` per process, the shim is part of the runtime key (`folder + core identity + shim`), which upstream — having no bridge — keys on the core alone. Why: `docs/adr/0003-lint-through-editor-worker.md`. The worker also sends the editor-only `rstack/rslintConfigDependency` notification (`stacks/lint/worker/configDependencyProtocol.ts`) when config loading finds a missing package. `ConfigTransactionAdapter` rewrites only that classified `rslint/loadConfigs` candidate's error message to its first line, so Go cannot echo a require stack beside the single warning. An initialized client whose initial configRefresh rejects with that verdict stays available for retry, rather than propagating a generic startup crash through RuntimeManager. Plugin-host startup failures report `disabled` (a plugin could not be loaded) by decision, deliberately not classified, and recover through the dependency poll. 8. **Self-documenting Rslint diagnostics** — client-side providers parse Inline directives into per-rule hover, DocumentLink and underline-decoration affordances (the hover renders `Rslint(rule-id)`, the shape VS Code gives the published diagnostics), and the router enriches today's `[rule-id] message` diagnostics with a derived Rule docs link. No rule metadata or network lookup is bundled (ADR 0004). The hover provider yields whenever the owning language client's resolved capabilities advertise `hoverProvider`; an optional `Rslint.onClosed` hook identity-safely prunes the controller's capability mirror; the diagnostic synthesis is removed once upstream publishes `code` / `codeDescription` natively. 9. **Color env parity with the CLI** (test) — upstream hard-codes `FORCE_COLOR: '1'` into the worker's spawn env; ours mirrors the CLI's `getForceColorEnv` (rstest `packages/core/src/utils/logger.ts`) instead (`stacks/test/shared/colorEnv.ts`): the master injects `FORCE_COLOR=1` into the composed spawn env only when neither `FORCE_COLOR` nor `NO_COLOR` is already set (marking the injection with `RSTACK_FORCE_COLOR_INJECTED`), and the worker retracts the marked injection right after config load if the config set `NO_COLOR` — the CLI's own decision point. Otherwise a project whose config sets `process.env.NO_COLOR` (rstack-cli does) hits Node's "'NO_COLOR' env is ignored" warning in every pool process. A user-set `FORCE_COLOR` beside a config-set `NO_COLOR` still warns, exactly as the bare CLI does. diff --git a/packages/vscode/e2e/lint/fixtures/plugin-host-failure/local-plugin.mjs b/packages/vscode/e2e/lint/fixtures/plugin-host-failure/local-plugin.mjs index 52f3baa..7241f69 100644 --- a/packages/vscode/e2e/lint/fixtures/plugin-host-failure/local-plugin.mjs +++ b/packages/vscode/e2e/lint/fixtures/plugin-host-failure/local-plugin.mjs @@ -6,9 +6,7 @@ import { isMainThread } from 'node:worker_threads'; // refresh or restart command masking a non-retryable failed generation. if (!isMainThread) { const state = fs.readFileSync(new URL('./host-state.txt', import.meta.url), 'utf8').trim(); - fs.appendFileSync(new URL('./attempts.log', import.meta.url), `${state}\n`); if (state === 'crash') throw new Error('fixture plugin import exploded'); - if (state === 'missing') await import('rstack-e2e-absent-plugin-dependency'); } export default { diff --git a/packages/vscode/e2e/lint/suite-plugin-host-failure/plugin-host-failure.test.ts b/packages/vscode/e2e/lint/suite-plugin-host-failure/plugin-host-failure.test.ts index b4f0ff6..9f74500 100644 --- a/packages/vscode/e2e/lint/suite-plugin-host-failure/plugin-host-failure.test.ts +++ b/packages/vscode/e2e/lint/suite-plugin-host-failure/plugin-host-failure.test.ts @@ -12,7 +12,7 @@ import { suite('Plugin-host status and polling recovery', function () { this.timeout(120_000); - test('keeps native diagnostics during plugin failure, classifies missing imports, and recovers without restart', async () => { + test('disables failed plugins while native diagnostics remain and recovers without restart', async () => { const root = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; assert.ok(root); const api = extensionExports(); @@ -37,48 +37,27 @@ suite('Plugin-host status and polling recovery', function () { path.join(root, 'src/index.ts'), ); await vscode.window.showTextDocument(document); - const crashed = await waitForState('crashed'); - if (crashed.kind === 'crashed') { + const disabled = await waitForState('disabled'); + if (disabled.kind === 'disabled') { + assert.ok(disabled.reason); assert.match( - crashed.detail, + disabled.reason, /^ESLint plugins failed to load: .*fixture plugin import exploded/, ); - assert.ok(!crashed.detail.includes('\n')); + assert.ok(!disabled.reason.includes('\n')); } + const warnings = api.getRecordedWarnings('rslint'); + assert.equal(warnings.length, 1); + assert.match( + warnings[0], + /^\[[^\]]+\] ESLint plugins failed to load: .*fixture plugin import exploded$/, + ); await waitForRslintDiagnostics(document, (diagnostics) => diagnostics.some((d) => diagnosticRuleIdIncludes(d, 'no-console')), ); api.setDependencyPollIntervalForTest(250); try { - fs.writeFileSync(path.join(root, 'host-state.txt'), 'missing'); - const disabled = await waitForState('disabled'); - if (disabled.kind === 'disabled') { - assert.ok(disabled.reason); - assert.match(disabled.reason, /install the project dependencies/); - } - const deadline = Date.now() + 60_000; - while ( - fs - .readFileSync(path.join(root, 'attempts.log'), 'utf8') - .split('\n') - .filter((line) => line === 'missing').length < 2 - ) { - assert.ok(Date.now() < deadline, 'Expected a second plugin-host retry'); - await new Promise((resolve) => setTimeout(resolve, 100)); - } - const warnings = api.getRecordedWarnings('rslint'); - assert.equal( - warnings.length, - 1, - 'Retries must not repeat the missing-package warning', - ); - assert.match(warnings[0], /rstack-e2e-absent-plugin-dependency/); - assert.ok( - !warnings[0].includes('\n'), - 'Missing-package warning must be one line', - ); - fs.writeFileSync(path.join(root, 'host-state.txt'), 'ready'); await waitForState('running'); await waitForRslintDiagnostics(document, (diagnostics) => diff --git a/packages/vscode/src/shared/missingDependency.ts b/packages/vscode/src/shared/missingDependency.ts index 3029e6a..1a29828 100644 --- a/packages/vscode/src/shared/missingDependency.ts +++ b/packages/vscode/src/shared/missingDependency.ts @@ -21,10 +21,9 @@ export function classifyMissingDependencyMessage( message: string, ): string | undefined { const [firstLine] = message.split('\n', 1); - const match = /(?:^|: )(Cannot find (?:package|module) '([^']+)'[^\n]*)/.exec( + const specifier = /^Cannot find (?:package|module) '([^']+)'/.exec( firstLine, - ); - const specifier = match?.[2]; + )?.[1]; if ( specifier === undefined || specifier.startsWith('.') || @@ -33,7 +32,7 @@ export function classifyMissingDependencyMessage( ) { return undefined; } - return match?.[1]; + return firstLine; } /** Use for a (code, message) pair; bare messages (fmt) use the classifier directly. */ diff --git a/packages/vscode/src/stacks/lint/Rslint.ts b/packages/vscode/src/stacks/lint/Rslint.ts index a924fd9..d9a022a 100644 --- a/packages/vscode/src/stacks/lint/Rslint.ts +++ b/packages/vscode/src/stacks/lint/Rslint.ts @@ -361,6 +361,14 @@ export class Rslint implements Disposable { private handleConfigDependencyStatus( notification: ConfigDependencyStatusNotification, ): void { + if (notification.kind === 'missing' && notification.failure.plugin) { + const reason = `ESLint plugins failed to load: ${notification.failure.cause}`; + this.configRefreshFailed = true; + this.configDependencyEpisode.clear(); + if (this.configError.changed(reason)) this.logger.warn(reason); + this.report({ kind: 'disabled', reason }); + return; + } if (notification.kind === 'error') { this.configRefreshFailed = true; this.report({ kind: 'crashed', detail: notification.message }); diff --git a/packages/vscode/src/stacks/lint/worker/PluginLintPool.ts b/packages/vscode/src/stacks/lint/worker/PluginLintPool.ts index 94c87c2..1e0851c 100644 --- a/packages/vscode/src/stacks/lint/worker/PluginLintPool.ts +++ b/packages/vscode/src/stacks/lint/worker/PluginLintPool.ts @@ -7,7 +7,6 @@ import type { import type { CancellationToken } from 'vscode-jsonrpc/node'; import type { WorkerLogger } from './logger'; import type { ConfigDependencyStatusNotification } from './configDependencyProtocol'; -import { classifyMissingDependencyMessage } from '../../../shared/missingDependency'; type PluginHostFactory = ( configs: ConfigDescriptor[], @@ -63,7 +62,6 @@ export class PluginLintPool { } const previous = this.status; - this.status = { kind: 'ok' }; if ( this.activeState?.ready && this.activeState.fingerprint === fingerprint @@ -109,26 +107,6 @@ export class PluginLintPool { } catch (error: unknown) { const message = error instanceof Error ? error.message : String(error); const firstLine = message.split('\n', 1)[0]; - const descriptor = - descriptors.find((d) => firstLine.includes(d.configPath)) ?? - descriptors[0]; - const cause = classifyMissingDependencyMessage(firstLine); - const failure: Exclude< - ConfigDependencyStatusNotification, - { kind: 'ok' } - > = - cause === undefined - ? { - kind: 'error', - message: `ESLint plugins failed to load: ${firstLine}`, - } - : { - kind: 'missing', - failure: { - configPath: descriptor.configPath, - cause, - }, - }; const state: HostGeneration = { fingerprint, ready: false, @@ -137,13 +115,17 @@ export class PluginLintPool { }; this.liveStates.add(state); this.generations.set(generation, state); - this.status = failure; - // The editor emits the shared one-line missing-dependency warning. - // Real errors retain their full detail here, once per failure episode. - if ( - failure.kind === 'error' && - !(previous.kind === 'error' && previous.message === failure.message) - ) { + this.status = { + kind: 'missing', + failure: { + configPath: descriptors[0].configPath, + cause: firstLine, + plugin: true, + }, + }; + if (!( + previous.kind === 'missing' && previous.failure.cause === firstLine + )) { this.logger.error('Failed to initialize ESLint-plugin host', error); } } @@ -170,6 +152,7 @@ export class PluginLintPool { } this.activeGeneration = generation; this.activeState = next; + if (next.ready) this.status = { kind: 'ok' }; this.activeCommitRollback = { generation, previousGeneration, diff --git a/packages/vscode/src/stacks/lint/worker/configDependencyProtocol.ts b/packages/vscode/src/stacks/lint/worker/configDependencyProtocol.ts index 4d9dc49..bfac348 100644 --- a/packages/vscode/src/stacks/lint/worker/configDependencyProtocol.ts +++ b/packages/vscode/src/stacks/lint/worker/configDependencyProtocol.ts @@ -6,7 +6,10 @@ export const CONFIG_DEPENDENCY_STATUS_NOTIFICATION = export type ConfigDependencyStatusNotification = | { readonly kind: 'ok' } - | { readonly kind: 'missing'; readonly failure: ConfigDependencyFailure } + | { + readonly kind: 'missing'; + readonly failure: ConfigDependencyFailure & { readonly plugin?: true }; + } | { readonly kind: 'error'; readonly message: string }; /** Shared with the editor's startup retry; this module stays vscode-free. */ diff --git a/packages/vscode/src/stacks/lint/worker/index.ts b/packages/vscode/src/stacks/lint/worker/index.ts index a02b629..6d59715 100644 --- a/packages/vscode/src/stacks/lint/worker/index.ts +++ b/packages/vscode/src/stacks/lint/worker/index.ts @@ -226,7 +226,8 @@ export async function runLintWorker( logger, installation.createPluginLintHost, ); - let configDependencyFailure: ConfigDependencyFailure | undefined; + let configDependencyFailure: + (ConfigDependencyFailure & { readonly plugin?: true }) | undefined; let configError: string | undefined; const adapter = new LspConfigTransactionAdapter( installation.createConfigModuleHost(), @@ -257,13 +258,12 @@ export async function runLintWorker( configPath: options.configPath, takeConfigStatus: () => { let failure = configDependencyFailure; - let message = configError; + const message = configError; configDependencyFailure = undefined; configError = undefined; // Plugin status persists across refreshes without a new prepare. const plugin = pluginLintPool.configDependencyStatus; - if (plugin.kind === 'error') message ??= plugin.message; - else if (plugin.kind === 'missing') failure ??= plugin.failure; + if (plugin.kind === 'missing') failure ??= plugin.failure; if (message !== undefined) return { kind: 'error', message }; if (failure !== undefined) return { kind: 'missing', failure }; return { kind: 'ok' }; diff --git a/packages/vscode/tests/shared/missingDependency.test.ts b/packages/vscode/tests/shared/missingDependency.test.ts index 6fd8500..85fbf37 100644 --- a/packages/vscode/tests/shared/missingDependency.test.ts +++ b/packages/vscode/tests/shared/missingDependency.test.ts @@ -102,27 +102,6 @@ describe('missingDependencyCauseOf', () => { }); describe('classifyMissingDependencyMessage', () => { - it('classifies a prefixed first line without keeping wrappers or the stack', () => { - expect( - classifyMissingDependencyMessage( - "worker init failed: failed to import config file /project/config.mjs: Cannot find package 'missing' imported from /project/plugin.mjs\nRequire stack:\n- worker", - ), - ).toBe("Cannot find package 'missing' imported from /project/plugin.mjs"); - expect( - classifyMissingDependencyMessage( - "Config failed\nworker init failed: Cannot find package 'missing'", - ), - ).toBeUndefined(); - }); - - it('keeps prefixed relative imports as real errors', () => { - expect( - classifyMissingDependencyMessage( - "worker init failed: Cannot find module './missing.js'", - ), - ).toBeUndefined(); - }); - it('classifies loader messages without requiring an Error code', () => { expect( classifyMissingDependencyMessage( diff --git a/packages/vscode/tests/stacks/lint/pluginLintPool.test.ts b/packages/vscode/tests/stacks/lint/pluginLintPool.test.ts index 269d502..0a13a62 100644 --- a/packages/vscode/tests/stacks/lint/pluginLintPool.test.ts +++ b/packages/vscode/tests/stacks/lint/pluginLintPool.test.ts @@ -13,6 +13,14 @@ it('logs the full error once per failure episode and again after recovery then r const pool = new PluginLintPool(logger, createHost); try { await pool.prepare(descriptors, 'same', 'initial'); + expect(pool.configDependencyStatus).toEqual({ + kind: 'missing', + failure: { + configPath: '/project/rslint.config.mjs', + cause: 'plugin exploded', + plugin: true, + }, + }); await pool.commit('initial'); await pool.prepare(descriptors, 'same', 'retry'); expect(createHost).toHaveBeenCalledTimes(2); @@ -27,6 +35,8 @@ it('logs the full error once per failure episode and again after recovery then r shutdown: async () => {}, }); await pool.prepare(descriptors, 'same', 'fixed'); + await pool.commit('fixed'); + expect(pool.configDependencyStatus).toEqual({ kind: 'ok' }); createHost.mockRejectedValue(error); await pool.prepare(descriptors, 'changed', 'broken-again'); expect(log).toHaveBeenCalledTimes(2); @@ -38,3 +48,32 @@ it('logs the full error once per failure episode and again after recovery then r await pool.dispose(); } }); + +it('keeps a failure visible when a successful prepare is aborted', async () => { + const descriptors = [ + { configPath: '/project/rslint.config.mjs', configDirectory: '/project' }, + ]; + const logger = new WorkerLogger(); + rs.spyOn(logger, 'error').mockImplementation(() => {}); + const createHost = rs.fn().mockRejectedValue(new Error('broken plugin')); + const pool = new PluginLintPool(logger, createHost); + try { + await pool.prepare(descriptors, 'broken', 'initial'); + await pool.commit('initial'); + const failure = pool.configDependencyStatus; + createHost.mockResolvedValue({ + lint: async () => ({ results: [] }), + shutdown: async () => {}, + }); + expect(await pool.prepare(descriptors, 'fixed', 'aborted')).toBe(true); + expect(pool.configDependencyStatus).toEqual(failure); + await pool.abort('aborted'); + expect(pool.configDependencyStatus).toEqual(failure); + await pool.prepare([], 'empty', 'removed'); + expect(pool.configDependencyStatus).toEqual(failure); + await pool.commit('removed'); + expect(pool.configDependencyStatus).toEqual({ kind: 'ok' }); + } finally { + await pool.dispose(); + } +}); From dcc13391ad838a205d3d75daf52be476b3cceffe Mon Sep 17 00:00:00 2001 From: fi3ework Date: Tue, 29 Sep 2026 11:43:28 +0800 Subject: [PATCH 4/4] refactor(vscode): give plugin-host failures their own verdict --- packages/vscode/AGENTS.md | 4 +-- .../plugin-host-failure/local-plugin.mjs | 4 +-- .../plugin-host-failure.test.ts | 15 +++++---- .../vscode/e2e/lint/suite/fixall-helpers.ts | 13 +++----- packages/vscode/rslib.config.mts | 6 ++-- packages/vscode/src/stacks/lint/Rslint.ts | 4 +-- .../src/stacks/lint/worker/PluginLintPool.ts | 28 +++++------------ .../lint/worker/configDependencyProtocol.ts | 6 ++-- .../vscode/src/stacks/lint/worker/index.ts | 10 +++--- .../tests/stacks/lint/pluginLintPool.test.ts | 31 +++++++------------ 10 files changed, 46 insertions(+), 75 deletions(-) diff --git a/packages/vscode/AGENTS.md b/packages/vscode/AGENTS.md index ea5492a..08b444b 100644 --- a/packages/vscode/AGENTS.md +++ b/packages/vscode/AGENTS.md @@ -6,7 +6,7 @@ One extension replacing the standalone `rstack.rslint` and `rstack.rstest` exten - `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 ten ways (the "adaptations" below). When syncing upstream, preserve them. An eleventh 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 **41daaa1b** (0.12.2). This sync ports Rslint **84f9c9b5** trace-setting manifest wording, **72cd2b1f** fixAll waits, and plugin-host failure status (adaptations 4/7). The Rstest **988f8e1d** license-notice port uses webpack-license-plugin's per-bundle JSON output with default license-text replenishment and explicit native-binding inclusion, shipped alongside the copied root `LICENSE`. `@rstackjs/load-config` is not bundled; Yuku is **0.12.0**. Retained ports include per-document core resolution / pinned config paths (**39536fd6**, **892482e0**), legacy JSON watcher removal (**b7176723**), Unicode BOM coverage (**5fc197a5**), and Rstest public programmatic/list/watch APIs and exact filters (**8f945491**, **d2812754**, **99be33e8**, **88cd5f6d**, **994b77e0**). Diff `CoreResolver.ts`, `RuntimeManager.ts`, `WorkspaceDocumentRouter.ts`, and `Rslint.ts` on future syncs. The targeted 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. The earlier **d82db4fc31a61ee74b2a74917f14a458e1bca419** lifecycle fix is subsumed; failed-project retry and worker cleanup remain ahead of upstream. +- **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 **41daaa1b** (0.12.2). 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`). ## The ten adaptations @@ -17,7 +17,7 @@ One extension replacing the standalone `rstack.rslint` and `rstack.rstest` exten 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. 6. **Node runtime selection** (lint, test, fmt) — the Node a project-loading child process runs on is a **User Node runtime** chosen by the extension against one uniform floor, never assumed from PATH; the recovery path is the user's own shell, and the dividing line is the **load bound** (terms in CONTEXT.md; the full rule and rationale in `docs/adr/0001-node-runtime-selection.md`). All three callers — the lint worker, the rstest worker and the `rs fmt --lsp` server — take the decision from the one shared module (`shared/nodeResolution.ts`) and share one escape hatch, the resource-scoped `rstack.nodeExecutable` (`shared/nodeExecutableSetting.ts`); each appends its own consequence to the shared preflight message. -7. **Lint worker and Rstack bridge** — the extension host is only Rslint's language client. One vscode-free, editor-shipped lint worker per **Lint runtime** (one Rslint core inside one workspace folder — CONTEXT.md) runs on the User Node runtime, owns the Go LSP plus all five reverse requests, and derives the binary/config/plugin pieces from one explicit `@rslint/core` directory. Upstream's `CoreResolver` loads that core in the extension host; ours only walks to the directory (`fs.stat` + `package.json` + semver) and hands the path to the worker, and its `CoreInstallation` therefore carries paths, not module factories; upstream's installation cache goes with the module loading it memoized (`clear()` is a no-op kept for the `RuntimeManager` contract). A bridged folder passes only rstack's published `dist/rslintConfig.js` shim; neither the extension nor the worker re-implements Rstack config semantics. Because every supported config protocol locks `configPath` per process, the shim is part of the runtime key (`folder + core identity + shim`), which upstream — having no bridge — keys on the core alone. Why: `docs/adr/0003-lint-through-editor-worker.md`. The worker also sends the editor-only `rstack/rslintConfigDependency` notification (`stacks/lint/worker/configDependencyProtocol.ts`) when config loading finds a missing package. `ConfigTransactionAdapter` rewrites only that classified `rslint/loadConfigs` candidate's error message to its first line, so Go cannot echo a require stack beside the single warning. An initialized client whose initial configRefresh rejects with that verdict stays available for retry, rather than propagating a generic startup crash through RuntimeManager. Plugin-host startup failures report `disabled` (a plugin could not be loaded) by decision, deliberately not classified, and recover through the dependency poll. +7. **Lint worker and Rstack bridge** — the extension host is only Rslint's language client. One vscode-free, editor-shipped lint worker per **Lint runtime** (one Rslint core inside one workspace folder — CONTEXT.md) runs on the User Node runtime, owns the Go LSP plus all five reverse requests, and derives the binary/config/plugin pieces from one explicit `@rslint/core` directory. Upstream's `CoreResolver` loads that core in the extension host; ours only walks to the directory (`fs.stat` + `package.json` + semver) and hands the path to the worker, and its `CoreInstallation` therefore carries paths, not module factories; upstream's installation cache goes with the module loading it memoized (`clear()` is a no-op kept for the `RuntimeManager` contract). A bridged folder passes only rstack's published `dist/rslintConfig.js` shim; neither the extension nor the worker re-implements Rstack config semantics. Because every supported config protocol locks `configPath` per process, the shim is part of the runtime key (`folder + core identity + shim`), which upstream — having no bridge — keys on the core alone. Why: `docs/adr/0003-lint-through-editor-worker.md`. The worker also sends the editor-only `rstack/rslintConfigDependency` notification (`stacks/lint/worker/configDependencyProtocol.ts`) when config loading finds a missing package. `ConfigTransactionAdapter` rewrites only that classified `rslint/loadConfigs` candidate's error message to its first line, so Go cannot echo a require stack beside the single warning. An initialized client whose initial configRefresh rejects with that verdict stays available for retry, rather than propagating a generic startup crash through RuntimeManager. Plugin-host startup failures use a separate, unclassified `plugin` verdict on `rstack/rslintConfigDependency`, report `disabled` (a plugin could not be loaded), and recover through the dependency poll. 8. **Self-documenting Rslint diagnostics** — client-side providers parse Inline directives into per-rule hover, DocumentLink and underline-decoration affordances (the hover renders `Rslint(rule-id)`, the shape VS Code gives the published diagnostics), and the router enriches today's `[rule-id] message` diagnostics with a derived Rule docs link. No rule metadata or network lookup is bundled (ADR 0004). The hover provider yields whenever the owning language client's resolved capabilities advertise `hoverProvider`; an optional `Rslint.onClosed` hook identity-safely prunes the controller's capability mirror; the diagnostic synthesis is removed once upstream publishes `code` / `codeDescription` natively. 9. **Color env parity with the CLI** (test) — upstream hard-codes `FORCE_COLOR: '1'` into the worker's spawn env; ours mirrors the CLI's `getForceColorEnv` (rstest `packages/core/src/utils/logger.ts`) instead (`stacks/test/shared/colorEnv.ts`): the master injects `FORCE_COLOR=1` into the composed spawn env only when neither `FORCE_COLOR` nor `NO_COLOR` is already set (marking the injection with `RSTACK_FORCE_COLOR_INJECTED`), and the worker retracts the marked injection right after config load if the config set `NO_COLOR` — the CLI's own decision point. Otherwise a project whose config sets `process.env.NO_COLOR` (rstack-cli does) hits Node's "'NO_COLOR' env is ignored" warning in every pool process. A user-set `FORCE_COLOR` beside a config-set `NO_COLOR` still warns, exactly as the bare CLI does. diff --git a/packages/vscode/e2e/lint/fixtures/plugin-host-failure/local-plugin.mjs b/packages/vscode/e2e/lint/fixtures/plugin-host-failure/local-plugin.mjs index 7241f69..c83f644 100644 --- a/packages/vscode/e2e/lint/fixtures/plugin-host-failure/local-plugin.mjs +++ b/packages/vscode/e2e/lint/fixtures/plugin-host-failure/local-plugin.mjs @@ -2,8 +2,8 @@ import fs from 'node:fs'; import { isMainThread } from 'node:worker_threads'; // Config discovery must succeed; only the plugin-host worker import fails. -// A non-config marker lets the E2E prove polling recovery without a file-watch -// refresh or restart command masking a non-retryable failed generation. +// A non-config marker keeps a file-watch refresh or restart from recovering +// the state early; the dependency poll retries the generation. if (!isMainThread) { const state = fs.readFileSync(new URL('./host-state.txt', import.meta.url), 'utf8').trim(); if (state === 'crash') throw new Error('fixture plugin import exploded'); diff --git a/packages/vscode/e2e/lint/suite-plugin-host-failure/plugin-host-failure.test.ts b/packages/vscode/e2e/lint/suite-plugin-host-failure/plugin-host-failure.test.ts index 9f74500..c3a2327 100644 --- a/packages/vscode/e2e/lint/suite-plugin-host-failure/plugin-host-failure.test.ts +++ b/packages/vscode/e2e/lint/suite-plugin-host-failure/plugin-host-failure.test.ts @@ -38,14 +38,13 @@ suite('Plugin-host status and polling recovery', function () { ); await vscode.window.showTextDocument(document); const disabled = await waitForState('disabled'); - if (disabled.kind === 'disabled') { - assert.ok(disabled.reason); - assert.match( - disabled.reason, - /^ESLint plugins failed to load: .*fixture plugin import exploded/, - ); - assert.ok(!disabled.reason.includes('\n')); - } + assert.ok(disabled.kind === 'disabled'); + assert.ok(disabled.reason); + assert.match( + disabled.reason, + /^ESLint plugins failed to load: .*fixture plugin import exploded/, + ); + assert.ok(!disabled.reason.includes('\n')); const warnings = api.getRecordedWarnings('rslint'); assert.equal(warnings.length, 1); assert.match( diff --git a/packages/vscode/e2e/lint/suite/fixall-helpers.ts b/packages/vscode/e2e/lint/suite/fixall-helpers.ts index daa0a8d..33a8aee 100644 --- a/packages/vscode/e2e/lint/suite/fixall-helpers.ts +++ b/packages/vscode/e2e/lint/suite/fixall-helpers.ts @@ -26,15 +26,10 @@ export const waitForDiagnostics = waitForRslintDiagnostics; * Wait until the rslint diagnostics for `doc` include every given rule-id * fragment. * - * Deviation from the upstream suites, which assert on the first non-empty - * publish: since @rslint/core 0.8.1 (web-infra-dev/rslint#1790), a file - * created after its project was loaded is served by a type-info-less fallback - * Program until a watcher event admits it into the configured project, so the - * first non-empty publish may carry only non-type-aware rules on platforms - * with slow file watchers (macOS). Waiting for the expected diagnostics keeps - * the terminal assertion identical without depending on publish batching. - * Our diagnostic enrichment moves rule ids to diagnostic.code; match that - * instead of upstream's message prefix. + * Upstream also waits for the typed rule (72cd2b1f), since the first publish + * may contain only non-type-aware rules. Our only deviation is matching + * diagnostic.code, where our enrichment puts rule ids, instead of upstream's + * message prefix. */ export function waitForDiagnosticsWithRuleIds( doc: vscode.TextDocument, diff --git a/packages/vscode/rslib.config.mts b/packages/vscode/rslib.config.mts index 71e132e..03dcb28 100644 --- a/packages/vscode/rslib.config.mts +++ b/packages/vscode/rslib.config.mts @@ -48,6 +48,7 @@ const lintWorkerEntry = './src/stacks/lint/worker/main.ts'; const libs: LibConfig[] = [ { + id: 'extension', syntax: 'es2023', format: 'cjs', source: { @@ -86,6 +87,7 @@ const libs: LibConfig[] = [ if (hasWorkerEntry) { libs.push({ + id: 'worker', syntax: 'es2023', format: 'cjs', source: { @@ -104,6 +106,7 @@ if (hasWorkerEntry) { } libs.push({ + id: 'lint-worker', syntax: 'es2023', format: 'cjs', source: { @@ -141,9 +144,8 @@ export default defineConfig(({ envMode }) => { appendPlugins( new WebpackLicensePlugin({ outputFilename: `${environment.name}.licenses.json`, - replenishDefaultLicenseTexts: true, includePackages: () => - environment.name === 'cjs0' + environment.name === 'extension' ? [path.dirname(yukuBindingPath)] : [], }), diff --git a/packages/vscode/src/stacks/lint/Rslint.ts b/packages/vscode/src/stacks/lint/Rslint.ts index d9a022a..30b1ded 100644 --- a/packages/vscode/src/stacks/lint/Rslint.ts +++ b/packages/vscode/src/stacks/lint/Rslint.ts @@ -361,8 +361,8 @@ export class Rslint implements Disposable { private handleConfigDependencyStatus( notification: ConfigDependencyStatusNotification, ): void { - if (notification.kind === 'missing' && notification.failure.plugin) { - const reason = `ESLint plugins failed to load: ${notification.failure.cause}`; + if (notification.kind === 'plugin') { + const reason = `ESLint plugins failed to load: ${notification.cause}`; this.configRefreshFailed = true; this.configDependencyEpisode.clear(); if (this.configError.changed(reason)) this.logger.warn(reason); diff --git a/packages/vscode/src/stacks/lint/worker/PluginLintPool.ts b/packages/vscode/src/stacks/lint/worker/PluginLintPool.ts index 1e0851c..ebd131b 100644 --- a/packages/vscode/src/stacks/lint/worker/PluginLintPool.ts +++ b/packages/vscode/src/stacks/lint/worker/PluginLintPool.ts @@ -6,7 +6,6 @@ import type { } from '@rslint/core/eslint-plugin'; import type { CancellationToken } from 'vscode-jsonrpc/node'; import type { WorkerLogger } from './logger'; -import type { ConfigDependencyStatusNotification } from './configDependencyProtocol'; type PluginHostFactory = ( configs: ConfigDescriptor[], @@ -28,11 +27,7 @@ export class PluginLintPool { private readonly shutdowns = new Set>(); private opChain: Promise = Promise.resolve(); private disposed = false; - private status: ConfigDependencyStatusNotification = { kind: 'ok' }; - - get configDependencyStatus(): ConfigDependencyStatusNotification { - return this.status; - } + hostFailure: string | undefined; constructor( private readonly logger: WorkerLogger, @@ -61,7 +56,6 @@ export class PluginLintPool { return; } - const previous = this.status; if ( this.activeState?.ready && this.activeState.fingerprint === fingerprint @@ -106,7 +100,7 @@ export class PluginLintPool { ready = true; } catch (error: unknown) { const message = error instanceof Error ? error.message : String(error); - const firstLine = message.split('\n', 1)[0]; + const cause = message.split('\n', 1)[0]; const state: HostGeneration = { fingerprint, ready: false, @@ -115,19 +109,10 @@ export class PluginLintPool { }; this.liveStates.add(state); this.generations.set(generation, state); - this.status = { - kind: 'missing', - failure: { - configPath: descriptors[0].configPath, - cause: firstLine, - plugin: true, - }, - }; - if (!( - previous.kind === 'missing' && previous.failure.cause === firstLine - )) { + if (this.hostFailure !== cause) { this.logger.error('Failed to initialize ESLint-plugin host', error); } + this.hostFailure = cause; } }); return ready; @@ -152,12 +137,13 @@ export class PluginLintPool { } this.activeGeneration = generation; this.activeState = next; - if (next.ready) this.status = { kind: 'ok' }; this.activeCommitRollback = { generation, previousGeneration, previousState: previous, + previousHostFailure: this.hostFailure, }; + if (next.ready) this.hostFailure = undefined; committed = true; }); return committed; @@ -171,6 +157,7 @@ export class PluginLintPool { const aborted = this.activeState; this.activeGeneration = rollback.previousGeneration; this.activeState = rollback.previousState; + this.hostFailure = rollback.previousHostFailure; this.activeCommitRollback = undefined; if (rollback.previousGeneration) { this.cancelGenerationRetirement(rollback.previousGeneration); @@ -392,4 +379,5 @@ interface ActiveCommitRollback { generation: string; previousGeneration: string | undefined; previousState: HostGeneration | undefined; + previousHostFailure: string | undefined; } diff --git a/packages/vscode/src/stacks/lint/worker/configDependencyProtocol.ts b/packages/vscode/src/stacks/lint/worker/configDependencyProtocol.ts index bfac348..4ae4399 100644 --- a/packages/vscode/src/stacks/lint/worker/configDependencyProtocol.ts +++ b/packages/vscode/src/stacks/lint/worker/configDependencyProtocol.ts @@ -6,10 +6,8 @@ export const CONFIG_DEPENDENCY_STATUS_NOTIFICATION = export type ConfigDependencyStatusNotification = | { readonly kind: 'ok' } - | { - readonly kind: 'missing'; - readonly failure: ConfigDependencyFailure & { readonly plugin?: true }; - } + | { readonly kind: 'missing'; readonly failure: ConfigDependencyFailure } + | { readonly kind: 'plugin'; readonly cause: string } | { readonly kind: 'error'; readonly message: string }; /** Shared with the editor's startup retry; this module stays vscode-free. */ diff --git a/packages/vscode/src/stacks/lint/worker/index.ts b/packages/vscode/src/stacks/lint/worker/index.ts index 6d59715..4cbc988 100644 --- a/packages/vscode/src/stacks/lint/worker/index.ts +++ b/packages/vscode/src/stacks/lint/worker/index.ts @@ -226,8 +226,7 @@ export async function runLintWorker( logger, installation.createPluginLintHost, ); - let configDependencyFailure: - (ConfigDependencyFailure & { readonly plugin?: true }) | undefined; + let configDependencyFailure: ConfigDependencyFailure | undefined; let configError: string | undefined; const adapter = new LspConfigTransactionAdapter( installation.createConfigModuleHost(), @@ -257,15 +256,14 @@ export async function runLintWorker( protocolVersion: installation.protocolVersion, configPath: options.configPath, takeConfigStatus: () => { - let failure = configDependencyFailure; + const failure = configDependencyFailure; const message = configError; configDependencyFailure = undefined; configError = undefined; - // Plugin status persists across refreshes without a new prepare. - const plugin = pluginLintPool.configDependencyStatus; - if (plugin.kind === 'missing') failure ??= plugin.failure; if (message !== undefined) return { kind: 'error', message }; if (failure !== undefined) return { kind: 'missing', failure }; + const cause = pluginLintPool.hostFailure; + if (cause !== undefined) return { kind: 'plugin', cause }; return { kind: 'ok' }; }, observeRefresh: (reason) => fingerprinter.observeRefresh(reason), diff --git a/packages/vscode/tests/stacks/lint/pluginLintPool.test.ts b/packages/vscode/tests/stacks/lint/pluginLintPool.test.ts index 0a13a62..e37d716 100644 --- a/packages/vscode/tests/stacks/lint/pluginLintPool.test.ts +++ b/packages/vscode/tests/stacks/lint/pluginLintPool.test.ts @@ -13,14 +13,7 @@ it('logs the full error once per failure episode and again after recovery then r const pool = new PluginLintPool(logger, createHost); try { await pool.prepare(descriptors, 'same', 'initial'); - expect(pool.configDependencyStatus).toEqual({ - kind: 'missing', - failure: { - configPath: '/project/rslint.config.mjs', - cause: 'plugin exploded', - plugin: true, - }, - }); + expect(pool.hostFailure).toBe('plugin exploded'); await pool.commit('initial'); await pool.prepare(descriptors, 'same', 'retry'); expect(createHost).toHaveBeenCalledTimes(2); @@ -36,20 +29,16 @@ it('logs the full error once per failure episode and again after recovery then r }); await pool.prepare(descriptors, 'same', 'fixed'); await pool.commit('fixed'); - expect(pool.configDependencyStatus).toEqual({ kind: 'ok' }); + expect(pool.hostFailure).toBeUndefined(); createHost.mockRejectedValue(error); await pool.prepare(descriptors, 'changed', 'broken-again'); expect(log).toHaveBeenCalledTimes(2); - expect(log.mock.calls[1]).toEqual([ - 'Failed to initialize ESLint-plugin host', - error, - ]); } finally { await pool.dispose(); } }); -it('keeps a failure visible when a successful prepare is aborted', async () => { +it('preserves a failure across prepare abort and restores it after commit rollback', async () => { const descriptors = [ { configPath: '/project/rslint.config.mjs', configDirectory: '/project' }, ]; @@ -60,19 +49,21 @@ it('keeps a failure visible when a successful prepare is aborted', async () => { try { await pool.prepare(descriptors, 'broken', 'initial'); await pool.commit('initial'); - const failure = pool.configDependencyStatus; createHost.mockResolvedValue({ lint: async () => ({ results: [] }), shutdown: async () => {}, }); - expect(await pool.prepare(descriptors, 'fixed', 'aborted')).toBe(true); - expect(pool.configDependencyStatus).toEqual(failure); + await pool.prepare(descriptors, 'fixed', 'aborted'); await pool.abort('aborted'); - expect(pool.configDependencyStatus).toEqual(failure); + expect(pool.hostFailure).toBe('broken plugin'); + await pool.prepare(descriptors, 'fixed', 'rolled-back'); + await pool.commit('rolled-back'); + expect(pool.hostFailure).toBeUndefined(); + await pool.abort('rolled-back'); + expect(pool.hostFailure).toBe('broken plugin'); await pool.prepare([], 'empty', 'removed'); - expect(pool.configDependencyStatus).toEqual(failure); await pool.commit('removed'); - expect(pool.configDependencyStatus).toEqual({ kind: 'ok' }); + expect(pool.hostFailure).toBeUndefined(); } finally { await pool.dispose(); }