diff --git a/packages/rstack/rstack.config.ts b/packages/rstack/rstack.config.ts index 7967b7a9..ef5f3610 100644 --- a/packages/rstack/rstack.config.ts +++ b/packages/rstack/rstack.config.ts @@ -4,6 +4,7 @@ import { define } from 'rstack'; define.test(() => { return { + pool: 'vmForks', // Temporary projects and fixture projects may contain files that match // Rstest's test glob. exclude: [ diff --git a/packages/rstack/tests/config/extends/index.test.ts b/packages/rstack/tests/config/extends/index.test.ts index 89077fc5..34e09de2 100644 --- a/packages/rstack/tests/config/extends/index.test.ts +++ b/packages/rstack/tests/config/extends/index.test.ts @@ -1,50 +1,52 @@ +import { execFileSync } from 'node:child_process'; import path from 'node:path'; import { expect, test } from 'rstack/test'; import { loadRstackConfig } from '../../../src/config.ts'; import { resolveConfigLayers } from '../../../src/configLayers.ts'; const cwd = import.meta.dirname; -const params = { command: 'build', env: 'production' } as const; -test('loads all seven tools from shared configs and tracks their dependencies', async () => { - const { configs, filePath, dependencies } = await loadRstackConfig({ cwd }); - const layers = [configs]; +test('loads all seven tools from shared configs and tracks their dependencies', () => { + // Dependency tracking uses Node loader hooks, outside the test VM. + const output = execFileSync( + process.execPath, + [path.join(cwd, 'load-shared.ts')], + { encoding: 'utf8', timeout: 10_000 }, + ); + const result: unknown = JSON.parse(output); + const dependencies: unknown = expect.arrayContaining([ + path.join(cwd, 'shared.ts'), + ]); - expect({ - app: await resolveConfigLayers(layers, 'app', params), - lib: await resolveConfigLayers(layers, 'lib', params), - doc: await resolveConfigLayers(layers, 'doc'), - test: await resolveConfigLayers(layers, 'test'), - lint: await resolveConfigLayers(layers, 'lint'), - fmt: await resolveConfigLayers(layers, 'fmt'), - staged: await resolveConfigLayers(layers, 'staged'), - }).toEqual({ - app: [ - { - source: { - define: { ENV: 'production' }, + expect(result).toEqual({ + configs: { + app: [ + { + source: { + define: { ENV: 'production' }, + }, }, - }, - ], - lib: [ - { - lib: [{ format: 'esm' }], - }, - ], - doc: [{ root: 'docs' }], - test: [{ setupFiles: [path.join(cwd, 'setup.ts')] }], - lint: [ - [ + ], + lib: [ { - rules: { 'no-debugger': 'error' }, + lib: [{ format: 'esm' }], }, ], - ], - fmt: [{ semi: false, singleQuote: false, printWidth: 80 }], - staged: [{ '*.ts': 'rs lint' }], + doc: [{ root: 'docs' }], + test: [{ setupFiles: [path.join(cwd, 'setup.ts')] }], + lint: [ + [ + { + rules: { 'no-debugger': 'error' }, + }, + ], + ], + fmt: [{ semi: false, singleQuote: false, printWidth: 80 }], + staged: [{ '*.ts': 'rs lint' }], + }, + filePath: path.join(cwd, 'rstack.config.ts'), + dependencies, }); - expect(filePath).toBe(path.join(cwd, 'rstack.config.ts')); - expect(dependencies).toContain(path.join(cwd, 'shared.ts')); }); test('preserves project priority regardless of call order and across reloads', async () => { diff --git a/packages/rstack/tests/config/extends/load-shared.ts b/packages/rstack/tests/config/extends/load-shared.ts new file mode 100644 index 00000000..f4a0bb20 --- /dev/null +++ b/packages/rstack/tests/config/extends/load-shared.ts @@ -0,0 +1,24 @@ +import { loadRstackConfig } from '../../../src/config.ts'; +import { resolveConfigLayers } from '../../../src/configLayers.ts'; + +const { configs, filePath, dependencies } = await loadRstackConfig({ + cwd: import.meta.dirname, +}); +const layers = [configs]; +const params = { command: 'build', env: 'production' } as const; + +console.log( + JSON.stringify({ + configs: { + app: await resolveConfigLayers(layers, 'app', params), + lib: await resolveConfigLayers(layers, 'lib', params), + doc: await resolveConfigLayers(layers, 'doc'), + test: await resolveConfigLayers(layers, 'test'), + lint: await resolveConfigLayers(layers, 'lint'), + fmt: await resolveConfigLayers(layers, 'fmt'), + staged: await resolveConfigLayers(layers, 'staged'), + }, + filePath, + dependencies, + }), +); diff --git a/packages/rstack/tests/config/load-config/index.test.ts b/packages/rstack/tests/config/load-config/index.test.ts index 8d3f861d..810890d7 100644 --- a/packages/rstack/tests/config/load-config/index.test.ts +++ b/packages/rstack/tests/config/load-config/index.test.ts @@ -1,3 +1,4 @@ +import { execFileSync } from 'node:child_process'; import path from 'node:path'; import { afterEach, expect, test } from 'rstack/test'; import { define, loadRstackConfig } from '../../../src/config.ts'; @@ -6,33 +7,14 @@ import { loadCliRstackConfig, } from '../../../src/cli/config.ts'; -type Deferred = ReturnType>; - -type ConfigTestHooks = { - ready: Deferred; - release: Deferred; -}; - -declare global { - // rslint-disable-next-line no-var - var __rstackConfigTestHooks: ConfigTestHooks | undefined; -} - const state = getConfigState(); const configPath = (fileName: string): string => path.join(import.meta.dirname, fileName); const loadConfigFile = (fileName: string) => loadRstackConfig({ configFilePath: configPath(fileName) }); -const createTestHooks = (): ConfigTestHooks => - (globalThis.__rstackConfigTestHooks = { - ready: Promise.withResolvers(), - release: Promise.withResolvers(), - }); - afterEach(() => { delete state.configPath; - delete globalThis.__rstackConfigTestHooks; }); test('should discard a config session after loading fails', async () => { @@ -94,39 +76,28 @@ test('should search cwd without inheriting the CLI config path', async () => { ); }); -test('should isolate parallel config sessions across top-level await', async () => { - const hooks = createTestHooks(); - const firstLoad = loadConfigFile('parallel-first.config.ts'); - await hooks.ready.promise; - - const secondLoad = loadConfigFile('parallel-second.config.ts'); - const parallelLoads = Promise.all([firstLoad, secondLoad]); - - await secondLoad; - hooks.release.resolve(); - const [firstResult, secondResult] = await parallelLoads; +// Native loader hooks and the fixture's globals must share one Node realm. +test('should isolate parallel config sessions across top-level await', () => { + const output = execFileSync( + process.execPath, + [configPath('parallel-load.ts'), 'success'], + { encoding: 'utf8', timeout: 10_000 }, + ); - expect(firstResult.configs).toEqual({ - app: { root: 'first' }, - test: {}, - }); - expect(secondResult.configs).toEqual({ - app: { root: 'second' }, - lib: {}, - }); + expect(JSON.parse(output)).toEqual([ + { app: { root: 'first' }, test: {} }, + { app: { root: 'second' }, lib: {} }, + ]); }); -test('should keep a running session intact when another config fails', async () => { - const hooks = createTestHooks(); - const successfulLoad = loadConfigFile('parallel-first.config.ts'); - await hooks.ready.promise; - - await expect(loadConfigFile('parallel-failing.config.ts')).rejects.toThrow( - 'parallel config error', +test('should keep a running session intact when another config fails', () => { + const output = execFileSync( + process.execPath, + [configPath('parallel-load.ts'), 'failure'], + { encoding: 'utf8', timeout: 10_000 }, ); - hooks.release.resolve(); - expect((await successfulLoad).configs).toEqual({ + expect(JSON.parse(output)).toEqual({ app: { root: 'first' }, test: {}, }); diff --git a/packages/rstack/tests/config/load-config/parallel-load.ts b/packages/rstack/tests/config/load-config/parallel-load.ts new file mode 100644 index 00000000..7164a365 --- /dev/null +++ b/packages/rstack/tests/config/load-config/parallel-load.ts @@ -0,0 +1,36 @@ +import assert from 'node:assert/strict'; +import path from 'node:path'; +import { loadRstackConfig } from '../../../src/config.ts'; + +type Deferred = ReturnType>; + +declare global { + // rslint-disable-next-line no-var + var __rstackConfigTestHooks: + { ready: Deferred; release: Deferred } | undefined; +} + +const hooks = (globalThis.__rstackConfigTestHooks = { + ready: Promise.withResolvers(), + release: Promise.withResolvers(), +}); +const load = (name: string) => + loadRstackConfig({ configFilePath: path.join(import.meta.dirname, name) }); +const firstLoad = load('parallel-first.config.ts'); +await Promise.race([hooks.ready.promise, firstLoad]); + +if (process.argv[2] === 'failure') { + await assert.rejects(load('parallel-failing.config.ts'), { + message: 'parallel config error', + }); + hooks.release.resolve(); + console.log(JSON.stringify((await firstLoad).configs)); +} else { + const secondLoad = load('parallel-second.config.ts'); + const parallelLoads = Promise.all([firstLoad, secondLoad]); + await secondLoad; + hooks.release.resolve(); + console.log( + JSON.stringify((await parallelLoads).map(({ configs }) => configs)), + ); +}