diff --git a/packages/rstack/src/cli/commands.ts b/packages/rstack/src/cli/commands.ts index 5fc8cd68..0f935fbf 100644 --- a/packages/rstack/src/cli/commands.ts +++ b/packages/rstack/src/cli/commands.ts @@ -1,6 +1,6 @@ import { join, resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; -import { getConfigState } from '../config.ts'; +import { CLI_CONFIG_PATH_ENV, getConfigState } from '../config.ts'; import { insertConfigArg, parseArgs, parseCliArgs } from './args.ts'; import { hasHelpFlag, printCommandHelp } from './help.ts'; @@ -153,6 +153,15 @@ async function runRslintCLI(args: string[]): Promise { return printCommandHelp('lint'); } + // Rslint's JS plugin workers inherit `process.env`. Clear an inherited path so + // that a run without `--config` still searches cwd. + const { configPath } = getConfigState(); + if (configPath === undefined) { + delete process.env[CLI_CONFIG_PATH_ENV]; + } else { + process.env[CLI_CONFIG_PATH_ENV] = configPath; + } + const argv = [ process.execPath, 'rslint', diff --git a/packages/rstack/src/config.ts b/packages/rstack/src/config.ts index e6c2ceb0..904f5aad 100644 --- a/packages/rstack/src/config.ts +++ b/packages/rstack/src/config.ts @@ -140,6 +140,13 @@ export const getConfigState = (): ConfigState => { return globalThis.__rstackCliState; }; +/** + * Passes the `--config` path to Rslint's JS plugin workers, which import the + * internal lint config in a fresh realm without the CLI state above. Only that + * config reads it. + */ +export const CLI_CONFIG_PATH_ENV = 'RSTACK_CLI_CONFIG_PATH'; + type Define = { /** * Inherits shared configs from left to right, followed by the project's own diff --git a/packages/rstack/src/rslintConfig.ts b/packages/rstack/src/rslintConfig.ts index 44062819..38dce415 100644 --- a/packages/rstack/src/rslintConfig.ts +++ b/packages/rstack/src/rslintConfig.ts @@ -1,13 +1,20 @@ import { existsSync } from 'node:fs'; import { join } from 'node:path'; -import { loadRstackConfig, type LoadedRstackConfig } from './config.ts'; +import { + CLI_CONFIG_PATH_ENV, + loadRstackConfig, + type LoadedRstackConfig, +} from './config.ts'; import type { RslintConfig } from '@rslint/core'; import { color, logger } from 'rslog'; import { resolveRslintConfig } from './configLayers.ts'; // Expose the loaded config so `rs check` can pass it to fmt instead of loading -// and executing the Rstack config a second time. -export const loadedConfig: LoadedRstackConfig = await loadRstackConfig(); +// and executing the Rstack config a second time. Rslint also imports this +// module in its JS plugin workers, which get `--config` from the environment. +export const loadedConfig: LoadedRstackConfig = await loadRstackConfig({ + configFilePath: process.env[CLI_CONFIG_PATH_ENV], +}); const { configs } = loadedConfig; const lintConfig = await resolveRslintConfig([configs]); diff --git a/packages/rstack/tests/cli/specify-config-lint/index.test.ts b/packages/rstack/tests/cli/specify-config-lint/index.test.ts new file mode 100644 index 00000000..6927fc1d --- /dev/null +++ b/packages/rstack/tests/cli/specify-config-lint/index.test.ts @@ -0,0 +1,47 @@ +import path from 'node:path'; +import { test } from '#test-helpers'; +import { expect } from 'rstack/test'; + +// Rslint runs JS plugin rules in worker threads, which must also load the +// config selected by `--config` instead of searching cwd. +test('should run JS plugin rules from --config outside cwd', async ({ + execCli, + logHelper, +}) => { + expect(() => + execCli('lint -c workspace/app/rstack.config.ts workspace/app/src'), + ).toThrow(); + await logHelper.expectLog('foo is not allowed'); +}); + +test('should prefer --config JS plugin rules over the cwd config', async ({ + cwd, + execCli, + logHelper, +}) => { + expect(() => + execCli('lint -c app/rstack.config.ts app/src', { + cwd: path.join(cwd, 'workspace'), + }), + ).toThrow(); + await logHelper.expectLog('foo is not allowed'); + logHelper.expectNoLog('rule from the cwd config'); +}); + +test('should ignore an inherited config path without --config', async ({ + cwd, + execCli, + logHelper, +}) => { + const workspace = path.join(cwd, 'workspace'); + expect(() => + execCli('lint app/src', { + cwd: workspace, + env: { + RSTACK_CLI_CONFIG_PATH: path.join(workspace, 'app/rstack.config.ts'), + }, + }), + ).toThrow(); + await logHelper.expectLog('rule from the cwd config'); + logHelper.expectNoLog('foo is not allowed'); +}); diff --git a/packages/rstack/tests/cli/specify-config-lint/workspace/app/rstack.config.ts b/packages/rstack/tests/cli/specify-config-lint/workspace/app/rstack.config.ts new file mode 100644 index 00000000..589600fa --- /dev/null +++ b/packages/rstack/tests/cli/specify-config-lint/workspace/app/rstack.config.ts @@ -0,0 +1,26 @@ +import { define } from 'rstack'; + +type Context = { report: (descriptor: object) => void }; + +define.lint([ + { + files: ['**/*.js'], + plugins: { + local: { + rules: { + 'no-foo': { + meta: { messages: { foo: 'foo is not allowed' } }, + create: (context: Context) => ({ + Identifier(node: { name: string }) { + if (node.name === 'foo') { + context.report({ node, messageId: 'foo' }); + } + }, + }), + }, + }, + }, + }, + rules: { 'local/no-foo': 'error' }, + }, +]); diff --git a/packages/rstack/tests/cli/specify-config-lint/workspace/app/src/index.js b/packages/rstack/tests/cli/specify-config-lint/workspace/app/src/index.js new file mode 100644 index 00000000..bb1843d1 --- /dev/null +++ b/packages/rstack/tests/cli/specify-config-lint/workspace/app/src/index.js @@ -0,0 +1 @@ +export const foo = 1; diff --git a/packages/rstack/tests/cli/specify-config-lint/workspace/rstack.config.ts b/packages/rstack/tests/cli/specify-config-lint/workspace/rstack.config.ts new file mode 100644 index 00000000..eb252f6d --- /dev/null +++ b/packages/rstack/tests/cli/specify-config-lint/workspace/rstack.config.ts @@ -0,0 +1,24 @@ +import { define } from 'rstack'; + +type Context = { report: (descriptor: object) => void }; + +// Registers the same plugin name as `app/rstack.config.ts` with another rule. +define.lint([ + { + plugins: { + local: { + rules: { + 'no-foo': { + meta: { messages: { cwd: 'rule from the cwd config' } }, + create: (context: Context) => ({ + Program(node: object) { + context.report({ node, messageId: 'cwd' }); + }, + }), + }, + }, + }, + }, + rules: { 'local/no-foo': 'error' }, + }, +]);