diff --git a/docs/adr/0003-lint-through-editor-worker.md b/docs/adr/0003-lint-through-editor-worker.md index 27a6033..6698d27 100644 --- a/docs/adr/0003-lint-through-editor-worker.md +++ b/docs/adr/0003-lint-through-editor-worker.md @@ -24,7 +24,7 @@ Rslint's language server is two halves: the Go process (`rslint --lsp`) lints na - **Lint gains a Node floor it never had.** A native folder that lints on VS Code's Node today reports `version mismatch` and starts nothing when no User Node runtime clears the ADR 0001 floor. Accepted deliberately: one worker, one path, one floor (ADR 0001 already rejected per-project floors), and this is the debt that ADR named. - **Resolution follows one chain, mirroring `rs lint`.** For a bridged folder: `rstack` from the folder root → `@rslint/core` from rstack's directory (the transitive copy `rs lint` itself imports; a pnpm project declaring only `rstack` cannot resolve `@rslint/core` from its root) → the Go binary through that core's `resolveRslintBinary()`. For a native folder the chain starts at `@rslint/core` from the **document's own directory** (the #1617 sync; before it, from the folder root). The extension walks the chain as far as the **core directory** — `fs.stat`, `package.json` reads and semver comparisons, no project code loaded, so inside the load bound and legitimately on the VS Code Node runtime, exactly as fmt resolves the `rs` bin — and gates there; the worker receives `--core [--config ]` and takes the last hop itself, calling that core's `resolveRslintBinary()` on the User Node runtime, since it is a JS export of the project's package. Floors follow the evidence in `SUPPORT_MATRIX`: `@rslint/core >= 0.8.0` is the first release with explicit protocol-2 config selection, while `rstack >= 0.7.0` is the first release whose shim supplies `basePath` and itself pins `@rslint/core` 0.9.0; one answer to "which rstack does the extension support" is worth more than keeping older releases running. -- **One override, and it names a core, not a binary.** `rstack.rslint.binPath` / `customBinPath` are removed in favour of `rstack.rslint.corePath` — the setting upstream introduced in rslint #1617: a path to an `@rslint/core` package directory, resource-scoped, from which the binary, config host, protocol version and plugin host all derive. In a bridged folder it overrides the rstack → `@rslint/core` hop only; the shim stays rstack's. A binary chosen independently of its core cannot be supported: the two must speak the same protocol. The rest of #1617 — per-document core resolution, one runtime per physical installation — has since been synced (issue #13): a **Lint runtime** is now one Rslint core inside one workspace folder, resolved per open document and refcounted by it, so a folder runs as many workers as its files have distinct cores (a bridged folder always exactly one, rstack's) and none at all while nothing is open. The worker never noticed: it still takes explicit `--core` / `--config` paths, which is precisely why that change did not touch it. +- **One override, and it names a core, not a binary.** `rstack.rslint.binPath` / `customBinPath` are removed in favour of `rstack.rslint.corePath` — the setting upstream introduced in rslint #1617: a path to an `@rslint/core` package directory (or its `package.json`), resource-scoped, from which the binary, config host, protocol version and plugin host all derive. In a bridged folder it overrides the rstack → `@rslint/core` hop only; the shim stays rstack's. A binary chosen independently of its core cannot be supported: the two must speak the same protocol. The rest of #1617 — per-document core resolution, one runtime per physical installation — has since been synced (issue #13): a **Lint runtime** is now one Rslint core inside one workspace folder, resolved per open document and refcounted by it, so a folder runs as many workers as its files have distinct cores (a bridged folder always exactly one, rstack's) and none at all while nothing is open. The worker never noticed: it still takes explicit `--core` / `--config` paths, which is precisely why that change did not touch it. - **Ownership is per folder, native wins.** One server holds one config choice for its lifetime (the supported config protocols lock `configPath` per process), and explicit and automatic modes cannot mix, so a folder is bridged only when no `rslint.config.*` exists anywhere in it and a `rstack.config.*` sits at its root; a subdirectory `rstack.config.*` lights nothing (`rs lint` in a terminal reads its cwd only — the same reason ADR 0002 rejected deepest-config-wins for fmt). Detection lights a bridged folder on the file's presence and never reads it: a `rstack.config.*` without `define.lint()` runs an empty config, as `rs lint` does. - **Config changes refresh, mode changes restart.** Rslint has a live refresh (`rslint/configRefresh` with the same `configPath`), unlike `rs fmt --lsp`, so the extension keeps its watcher-driven refresh — extended, for a bridged folder, with the root `rstack.config.*` — and the worker re-stamps `protocolVersion` and its `configPath` on every refresh (the extension does not know either). Only a native ↔ bridged flip, or a dependency change the refresh cannot absorb, restarts the server. This is the "diverge only when the tool forces it" rule: rslint can refresh, fmt cannot. - **Failure states mirror fmt.** No `rstack`, no `@rslint/core`, or a config importing an absent package → `disabled`; `rstack` or the chained `@rslint/core` below floor, or no Node clearing the floor → `version mismatch`; worker or Go dying → `crashed`. Config-import failures are classified where the worker still has the loader's structured error and carried to the editor as data; the live Go server remains available for a later refresh. diff --git a/packages/vscode/AGENTS.md b/packages/vscode/AGENTS.md index 3ac38af..405a3b4 100644 --- a/packages/vscode/AGENTS.md +++ b/packages/vscode/AGENTS.md @@ -12,7 +12,7 @@ One extension replacing the standalone `rstack.rslint` and `rstack.rstest` exten ## The eleven adaptations 1. **Shell activation** — stacks never self-activate; `register()` returns fast and never blocks on starting a server/worker. -2. **Namespace** — everything user-visible is `rstack.*`. Legacy `rslint.*` / `rstest.*` settings and command ids are not read, aliased or migrated (breaking old settings and keybindings was an accepted cost). +2. **Namespace** — everything user-visible is `rstack.*`. Legacy `rslint.*` / `rstest.*` settings and command ids are not read, aliased or migrated (breaking old settings and keybindings was an accepted cost). Both `rstack.rslint.corePath` and `rstack.rstest.corePath` accept a package directory or its `package.json`, resolving relative paths from the workspace folder; only Rstest expands `${workspaceFolder}`. Preserve these unified override names without legacy aliases when syncing. 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. 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. diff --git a/packages/vscode/README.md b/packages/vscode/README.md index 181c3e7..c7357da 100644 --- a/packages/vscode/README.md +++ b/packages/vscode/README.md @@ -78,13 +78,13 @@ All settings live under the unified `rstack.*` namespace. There are no `rslint.* | --- | --- | --- | | `rstack.nodeExecutable` | — | Node binary used for the processes that load your project: the lint worker, test worker and `rs fmt` language server. Empty means the extension picks one (`PATH` first, then the `node` your interactive shell resolves). | | `rstack.rslint.enable` | `true` | Enable/disable the Rslint integration. | -| `rstack.rslint.corePath` | — | Path to an `@rslint/core` package directory; relative paths resolve from the workspace folder. | +| `rstack.rslint.corePath` | — | Path to an `@rslint/core` package directory or its `package.json`; relative paths resolve from the workspace folder. Use only to override automatic resolution. | | `rstack.rslint.trace.server` | `off` | LSP trace level (`off` / `messages` / `verbose`). | | `rstack.rstest.enable` | `true` | Enable/disable the Rstest integration. | | `rstack.rstest.configFileGlobPattern` | `["**/rstest.config.{mjs,ts,js,cjs,mts,cts}"]` | Glob patterns used to discover config files. | | `rstack.rstest.testCaseCollectMethod` | `ast` | `ast` (fast) or `runtime` (supports dynamic test generation). | | `rstack.rstest.applyDiagnostic` | `true` | Show diagnostics in the editor and Problems panel for failures. | -| `rstack.rstest.rstestPackagePath` | — | Explicit `@rstest/core` `package.json`, last-resort override. | +| `rstack.rstest.corePath` | — | Path to an `@rstest/core` package directory or its `package.json`; relative paths resolve from the workspace folder and `${workspaceFolder}` is supported. Use only to override automatic resolution, for both workers and Run in Terminal. | | `rstack.rstest.nodeExecArgs` | `[]` | Extra Node args for the test worker. | | `rstack.rstest.nodeEnv` | `null` | Extra env for the test worker. | | `rstack.rstest.debugNodeEnv` | `null` | Extra env when debugging tests. | @@ -101,7 +101,7 @@ Formatting runs one `rs fmt` language server per workspace folder, which loads ` ## Coming from the standalone extensions -Disable or uninstall the retired `rstack.rslint` / `rstack.rstest` extensions so only one copy of each tool runs. Settings and keybindings are not carried over from them: re-enter your settings under the `rstack.*` keys listed above and re-bind any keybinding to the new `rstack.*` command ids. Legacy `rslint.binPath` / `rslint.customBinPath` have no equivalent — use `rstack.rslint.corePath` to point at an `@rslint/core` package directory if you still need an override. +Disable or uninstall the retired `rstack.rslint` / `rstack.rstest` extensions so only one copy of each tool runs. Settings and keybindings are not carried over from them: re-enter your settings under the `rstack.*` keys listed above and re-bind any keybinding to the new `rstack.*` command ids. Legacy `rslint.binPath` / `rslint.customBinPath` have no equivalent — use `rstack.rslint.corePath` to point at an `@rslint/core` package directory or its `package.json` if you still need an override. ## Community diff --git a/packages/vscode/e2e/lint/suite-jsconfig/core-resolver.test.ts b/packages/vscode/e2e/lint/suite-jsconfig/core-resolver.test.ts index 21cb30c..44c7649 100644 --- a/packages/vscode/e2e/lint/suite-jsconfig/core-resolver.test.ts +++ b/packages/vscode/e2e/lint/suite-jsconfig/core-resolver.test.ts @@ -257,17 +257,22 @@ suite('local core resolver', () => { ); const document = await createSource('src/index.ts'); - const resolved = await new CoreResolver().resolve( - document, - temporaryWorkspaceFolder(), - { mode: 'native', corePath: 'vendor/rslint-core' }, - ); + for (const corePath of [ + 'vendor/rslint-core', + 'vendor/rslint-core/package.json', + ]) { + const resolved = await new CoreResolver().resolve( + document, + temporaryWorkspaceFolder(), + { mode: 'native', corePath }, + ); - await assertSamePhysicalPath( - resolved.installation.packageDirectory, - packageDirectory, - ); - assert.strictEqual(resolved.installation.version, '3.0.0'); + await assertSamePhysicalPath( + resolved.installation.packageDirectory, + packageDirectory, + ); + assert.strictEqual(resolved.installation.version, '3.0.0'); + } }); test('rejects a core package below the supported version floor', async () => { diff --git a/packages/vscode/package.json b/packages/vscode/package.json index 12661a3..a5a995c 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -148,7 +148,7 @@ "type": "string", "default": "", "scope": "resource", - "markdownDescription": "Path to an `@rslint/core` package directory. Relative paths are resolved from the workspace folder. When empty, a native folder uses the nearest installation above each file and a bridged folder (root `rstack.config.*`) uses the one its `rstack` depends on." + "markdownDescription": "Path to an `@rslint/core` installation, either its package directory or its `package.json` file. Relative paths are resolved from the workspace folder. Use this override only when automatic resolution does not select the installation you need. When empty, a native folder uses the nearest installation above each file and a bridged folder (root `rstack.config.*`) uses the one its `rstack` depends on." }, "rstack.rslint.trace.server": { "order": 2, @@ -175,11 +175,11 @@ "scope": "window", "markdownDescription": "Enable the Rstest test explorer for detected workspace folders. This is a window-level kill switch; which folders run Rstest is decided by detection." }, - "rstack.rstest.rstestPackagePath": { + "rstack.rstest.corePath": { "order": 1, "type": "string", "scope": "resource", - "markdownDescription": "The path to a `package.json` file of an Rstest installation (usually inside `node_modules`) in case the extension cannot find it. It will be used to resolve Rstest API paths. This should be used as a last resort fix. Supports the `${workspaceFolder}` placeholder." + "markdownDescription": "Path to an `@rstest/core` installation, either its package directory or its `package.json` file. Relative paths are resolved from the workspace folder. Supports the `${workspaceFolder}` placeholder. Use this override only when automatic resolution does not select the installation you need. It applies to both worker and Run in Terminal resolution." }, "rstack.rstest.configFileGlobPattern": { "order": 2, @@ -450,7 +450,6 @@ "vscode": "^1.97.0" }, "icon": "icon.png", - "preview": true, "capabilities": { "virtualWorkspaces": { "supported": "limited", diff --git a/packages/vscode/src/stacks/lint/resolution.ts b/packages/vscode/src/stacks/lint/resolution.ts index e5860a1..f774e63 100644 --- a/packages/vscode/src/stacks/lint/resolution.ts +++ b/packages/vscode/src/stacks/lint/resolution.ts @@ -84,8 +84,11 @@ function resolveConfiguredCore( folderRoot: string, configuredPath: string, ): PackageLocation { - const directory = path.resolve(folderRoot, configuredPath); - const packageJsonPath = path.join(directory, 'package.json'); + const resolvedPath = path.resolve(folderRoot, configuredPath); + const packageJsonPath = configuredPath.endsWith('package.json') + ? resolvedPath + : path.join(resolvedPath, 'package.json'); + const directory = path.dirname(packageJsonPath); try { if (!fs.statSync(packageJsonPath).isFile()) throw new Error('not a file'); } catch (error) { diff --git a/packages/vscode/src/stacks/test/config.ts b/packages/vscode/src/stacks/test/config.ts index d1b0428..979ff42 100644 --- a/packages/vscode/src/stacks/test/config.ts +++ b/packages/vscode/src/stacks/test/config.ts @@ -23,9 +23,9 @@ export const CONFIG_SECTION = 'rstack.rstest'; // Centralized configuration types for the extension. // Add new keys here to extend configuration in a type-safe way. const configSchema = object({ - // The path to a package.json file of a Rstest executable. + // The path to an @rstest/core package directory or its package.json. // Used as a last resort if the extension cannot auto-detect @rstest/core. - rstestPackagePath: fallback(optional(string()), undefined), + corePath: fallback(optional(string()), undefined), nodeExecArgs: fallback(array(string()), []), nodeEnv: fallback(optional(record(string(), string())), undefined), debugNodeEnv: fallback(optional(record(string(), string())), undefined), diff --git a/packages/vscode/src/stacks/test/coreResolution.ts b/packages/vscode/src/stacks/test/coreResolution.ts index 2530981..974674a 100644 --- a/packages/vscode/src/stacks/test/coreResolution.ts +++ b/packages/vscode/src/stacks/test/coreResolution.ts @@ -9,7 +9,7 @@ * embeds a multi-line require stack and says nothing about what to do. An * uninstalled core is the normal state of a freshly cloned repository and is * resolved for every config file without the user asking, so it is only - * logged; a `rstestPackagePath` that does not resolve is a setting the user + * logged; a `corePath` that does not resolve is a setting the user * has to fix, so it is notified. */ @@ -56,5 +56,5 @@ export function isModuleNotFoundError( export function formatConfiguredCoreNotFoundMessage( configuredPackagePath: string, ): string { - return `Cannot find "@rstest/core" at the configured "rstack.rstest.rstestPackagePath": ${configuredPackagePath}. Update the setting to point at an installed "@rstest/core" package.json.`; + return `Cannot find "@rstest/core" at the configured "rstack.rstest.corePath": ${configuredPackagePath}. Update the setting to point at an installed "@rstest/core" package directory or package.json.`; } diff --git a/packages/vscode/src/stacks/test/master.ts b/packages/vscode/src/stacks/test/master.ts index 388253e..c9b60e2 100644 --- a/packages/vscode/src/stacks/test/master.ts +++ b/packages/vscode/src/stacks/test/master.ts @@ -57,7 +57,7 @@ const CORE_NOT_INSTALLED_STATUS = formatNotInstalledStatus( 'rstest', '@rstest/core', ); -const CORE_NOT_INSTALLED_CONSEQUENCE = `install the project dependencies, or set "${CONFIG_SECTION}.rstestPackagePath" to an installed @rstest/core package.json`; +const CORE_NOT_INSTALLED_CONSEQUENCE = `install the project dependencies, or set "${CONFIG_SECTION}.corePath" to an installed @rstest/core package directory or package.json`; type WorkerRpc = BirpcReturn; type RstestPaths = Pick; @@ -155,7 +155,7 @@ export const warmWorkerNodePreflight = ( } }; -// The specifier used when `rstestPackagePath` is unset. +// The specifier used when `corePath` is unset. const CORE_PACKAGE_JSON = '@rstest/core/package.json'; export class RstestApi { @@ -189,7 +189,7 @@ export class RstestApi { /** * Where the default `@rstest/core` (and CLI bin) walk-up starts. Chosen * by `Project` — see `ProjectSource.rstestResolutionDir`; an explicit - * `rstestPackagePath` bypasses it. + * `corePath` bypasses it. */ private rstestResolutionDir: string, ) {} @@ -314,24 +314,19 @@ export class RstestApi { } } - // The validated absolute path to the package.json a `rstestPackagePath` - // setting points at, or `undefined` when the setting is unset and the bare + // The absolute path to the package.json selected by a `corePath` + // setting, or `undefined` when the setting is unset and the bare // `CORE_PACKAGE_JSON` specifier applies. Shared by the worker resolution and // the terminal CLI resolution, which both also report the configured path. private resolveConfiguredPackageJson(): string | undefined { // TODO: support Yarn PnP - let configuredPackagePath = getConfigValue( - 'rstestPackagePath', - this.workspace, - ); + let configuredPackagePath = getConfigValue('corePath', this.workspace); if (!configuredPackagePath) { return undefined; } configuredPackagePath = this.expandWorkspaceFolder(configuredPackagePath); if (!configuredPackagePath.endsWith('package.json')) { - throw new Error( - `"${CONFIG_SECTION}.rstestPackagePath" must point to a package.json file, instead got: ${configuredPackagePath}`, - ); + configuredPackagePath = path.join(configuredPackagePath, 'package.json'); } return path.isAbsolute(configuredPackagePath) ? configuredPackagePath @@ -343,7 +338,7 @@ export class RstestApi { // whose dependencies are not installed yet, so it is written to the output // channel and never raised as a notification. This is the only place that // policy lives, and it deliberately does not cover `configuredPackagePath`: - // a `rstestPackagePath` that does not resolve is a setting the user got + // a `corePath` that does not resolve is a setting the user got // wrong, so it is rethrown for the caller to report like any other failure. private resolveFromCwd( specifier: string, @@ -400,7 +395,7 @@ export class RstestApi { let corePackageJsonPath: string; let nodeExport: string | undefined; if (configured) { - logger.debug('Using configured rstestPackagePath:', configured); + logger.debug('Using configured corePath:', configured); // An explicit pin, resolved exactly as before — the setting names one // fixed path, so cache staleness is moot. // `dirname` turns the package.json specifier into its package entry. @@ -493,7 +488,7 @@ export class RstestApi { } // Resolve the rstest CLI executable (its package `bin`) for the terminal run - // mode, honoring a configured `rstestPackagePath` the same way as the worker + // mode, honoring a configured `corePath` the same way as the worker // resolution above. private resolveRstestBin(): string | undefined { const configured = this.resolveConfiguredPackageJson(); @@ -747,7 +742,7 @@ export class RstestApi { // runtime the user must fix: detection watches the config file and will // drop or re-add the project. Refusing here — before package resolution, // which would misread the deleted directory as "@rstest/core is not - // installed" (or notify for a `rstestPackagePath` inside it) — spares + // installed" (or notify for a `corePath` inside it) — spares // the caller a worker whose every call rejects with an opaque // "[birpc] rpc is closed", and spares the user Node's misreading of a // missing cwd as "spawn node ENOENT". diff --git a/packages/vscode/tests/extension.test.ts b/packages/vscode/tests/extension.test.ts index e5f56be..2342c32 100644 --- a/packages/vscode/tests/extension.test.ts +++ b/packages/vscode/tests/extension.test.ts @@ -529,7 +529,7 @@ describe('dependency recovery polling', () => { }); describe('the extension manifest', () => { - it('scopes language-client tracing to the window', () => { + it('scopes core overrides to resources and language-client tracing to the window', () => { const manifest = require('../package.json') as { contributes: { configuration: Array<{ @@ -547,6 +547,8 @@ describe('the extension manifest', () => { // the running client never reads. expect(settings['rstack.rslint.trace.server']?.scope).toBe('window'); expect(settings['rstack.fmt.trace.server']?.scope).toBe('window'); + expect(settings['rstack.rslint.corePath']?.scope).toBe('resource'); + expect(settings['rstack.rstest.corePath']?.scope).toBe('resource'); }); }); diff --git a/packages/vscode/tests/stacks/lint/resolution.test.ts b/packages/vscode/tests/stacks/lint/resolution.test.ts index 216e293..8a651ca 100644 --- a/packages/vscode/tests/stacks/lint/resolution.test.ts +++ b/packages/vscode/tests/stacks/lint/resolution.test.ts @@ -42,24 +42,42 @@ describe('resolveRslint', () => { }); }); - it('uses corePath for the core hop in both modes', () => { + it.each(['custom-core', 'custom-core/package.json'])( + 'uses a relative corePath for the core hop in both modes (%s)', + (corePath) => { + const root = temporaryDirectory(); + const rstackDir = installPackage(root, 'rstack', '0.6.1'); + installShim(rstackDir); + const customCore = writePackage( + path.join(root, 'custom-core'), + '@rslint/core', + '0.8.1', + ); + for (const mode of ['native', 'bridged'] as const) { + const resolution = resolveRslint({ + folderRoot: root, + mode, + corePath, + }); + expect(resolution.coreDir).toBe(customCore); + expect(resolution.coreVersion).toBe('0.8.1'); + } + }, + ); + + it('reports an invalid configured path', () => { + const corePath = 'missing-core'; const root = temporaryDirectory(); - const rstackDir = installPackage(root, 'rstack', '0.6.1'); - installShim(rstackDir); - const customCore = writePackage( - path.join(root, 'custom-core'), - '@rslint/core', - '0.8.1', + expect(() => + resolveRslint({ folderRoot: root, mode: 'native', corePath }), + ).toThrow( + `Could not access @rslint/core at ${path.join(root, 'missing-core')}`, ); - - for (const mode of ['native', 'bridged'] as const) { - const resolution = resolveRslint({ - folderRoot: root, - mode, - corePath: './custom-core', - }); - expect(resolution.coreDir).toBe(customCore); - expect(resolution.coreVersion).toBe('0.8.1'); + try { + resolveRslint({ folderRoot: root, mode: 'native', corePath }); + } catch (error) { + expect(error).toBeInstanceOf(RslintResolutionError); + expect(error).toMatchObject({ code: 'invalid-package' }); } }); diff --git a/packages/vscode/tests/stacks/test/master.test.ts b/packages/vscode/tests/stacks/test/master.test.ts index 4dd0a36..12fd199 100644 --- a/packages/vscode/tests/stacks/test/master.test.ts +++ b/packages/vscode/tests/stacks/test/master.test.ts @@ -283,29 +283,34 @@ describe('RstestApi package-resolution anchor', () => { }); }); - it('lets rstestPackagePath override the bridge anchor', () => { - writeCoreInstall(storeEntry); - const configured = writeCoreInstall(path.join(root, 'configured')); - settings.rstestPackagePath = path.join( - configured.packageDir, - 'package.json', - ); - - expect(resolveRstestPaths(createApi(cwd, rstackDir))).toEqual({ - paths: { - apiPath: path.join(configured.packageDir, 'api.js'), - rstestPath: configured.entry, - }, - bin: configured.bin, - }); - }); + it.each(['directory', 'relative', 'workspaceFolder'])( + 'lets corePath override the bridge anchor (%s)', + (form) => { + writeCoreInstall(storeEntry); + const configured = writeCoreInstall(path.join(root, 'configured')); + settings.corePath = + form === 'relative' + ? path.relative(cwd, configured.packageDir) + : form === 'workspaceFolder' + ? '${workspaceFolder}/' + path.relative(cwd, configured.packageDir) + : configured.packageDir; + + expect(resolveRstestPaths(createApi(cwd, rstackDir))).toEqual({ + paths: { + apiPath: path.join(configured.packageDir, 'api.js'), + rstestPath: configured.entry, + }, + bin: configured.bin, + }); + }, + ); it('resolves the configured core API outside node_modules by self-reference', () => { const installed = writeCoreInstall(root); const packageDir = path.join(root, 'vendor', 'rstest-core'); fs.mkdirSync(path.dirname(packageDir), { recursive: true }); fs.renameSync(installed.packageDir, packageDir); - settings.rstestPackagePath = path.join(packageDir, 'package.json'); + settings.corePath = path.join(packageDir, 'package.json'); expect(resolveRstestPaths(createApi(cwd))).toEqual({ paths: { @@ -391,7 +396,7 @@ describe('RstestApi with a missing @rstest/core', () => { const logged = loggedWarnings.join('\n'); expect(logged).toContain('@rstest/core is not installed'); expect(logged).toContain(`searched from ${noCoreDir}`); - expect(logged).toContain('rstestPackagePath'); + expect(logged).toContain('corePath'); expect(logged).not.toContain('Require stack'); expect(reported).toEqual([ { @@ -455,21 +460,22 @@ describe('RstestApi with an unusable @rstest/core', () => { }); }); -// A configured `rstestPackagePath` that does not resolve is not the +// A configured `corePath` that does not resolve is not the // "dependencies are not installed yet" state — the user picked that path and // has to fix it, so silence would strand them. -describe('RstestApi with an unresolvable rstestPackagePath', () => { +describe('RstestApi with an unresolvable corePath', () => { const configured = `${noCoreDir}/vendor/core/package.json`; beforeEach(() => { shownMessages.length = 0; - settings.rstestPackagePath = configured; + settings.corePath = configured; }); - it('should notify while discovering projects', async () => { + it('should notify while discovering projects with an invalid directory', async () => { + settings.corePath = path.dirname(configured); await expect(createApi().getNormalizedConfig()).rejects.toThrow(); expect(shownMessages).toHaveLength(1); - expect(shownMessages[0]).toContain('rstack.rstest.rstestPackagePath'); + expect(shownMessages[0]).toContain('rstack.rstest.corePath'); expect(shownMessages[0]).toContain(configured); }); @@ -486,13 +492,10 @@ describe('RstestApi with an unresolvable rstestPackagePath', () => { expect(resolve).toThrow(); expect(shownMessages).toHaveLength(1); - settings.rstestPackagePath = path.join( - installed.packageDir, - 'package.json', - ); + settings.corePath = path.join(installed.packageDir, 'package.json'); expect(resolve().rstestPath).toBe(installed.entry); - settings.rstestPackagePath = configured; + settings.corePath = configured; expect(resolve).toThrow(); expect(shownMessages).toHaveLength(2); } finally { @@ -506,7 +509,7 @@ describe('RstestApi with an unresolvable rstestPackagePath', () => { ); const installed = writeCoreInstall(root); const metadata = path.join(installed.packageDir, 'package.json'); - settings.rstestPackagePath = metadata; + settings.corePath = metadata; loggedErrors.length = 0; const original = nodeRequire.resolve; let broken = true; @@ -539,7 +542,7 @@ describe('RstestApi with an unresolvable rstestPackagePath', () => { it('should notify for a terminal run', () => { createApi().runInTerminal({}); expect(shownMessages).toHaveLength(1); - expect(shownMessages[0]).toContain('rstack.rstest.rstestPackagePath'); + expect(shownMessages[0]).toContain('rstack.rstest.corePath'); expect(createdTerminals).toEqual([]); }); });