diff --git a/packages/core/src/core/rsbuild.ts b/packages/core/src/core/rsbuild.ts index 218adef..44ca371 100644 --- a/packages/core/src/core/rsbuild.ts +++ b/packages/core/src/core/rsbuild.ts @@ -19,6 +19,47 @@ const isMultiCompiler = < return 'compilers' in compiler && Array.isArray(compiler.compilers); }; +const autoExternalNodeModules: ( + data: Rspack.ExternalItemFunctionData, + callback: ( + err?: Error, + result?: Rspack.ExternalItemValue, + type?: Rspack.ExternalsType, + ) => void, +) => void = ({ context, request, dependencyType, getResolve }, callback) => { + if (!request || request.startsWith('node:')) { + return callback(); + } + + const doExternal = () => { + callback( + undefined, + `${dependencyType === 'commonjs' ? 'commonjs' : 'module-import'} ${request}`, + ); + }; + if (/node_modules/.test(request)) { + return doExternal(); + } + + const resolver = getResolve?.(); + + if (!resolver) { + return callback(); + } + + resolver(context!, request!, (err, resolvePath) => { + if (err) { + // ignore resolve error + return callback(); + } + + if (resolvePath && /node_modules/.test(resolvePath!)) { + return doExternal(); + } + return callback(); + }); +}; + class TestFileWatchPlugin { private contextToWatch: string | null = null; @@ -49,6 +90,8 @@ export const prepareRsbuild = async ( setupFiles: Record, ): Promise => { RsbuildLogger.level = isDebug() ? 'verbose' : 'error'; + // TODO: find a better way to test outputs + const writeToDisk = process.env.DEBUG_RSTEST_OUTPUTS === 'true'; const rsbuildInstance = await createRsbuild({ rsbuildConfig: { @@ -60,20 +103,25 @@ export const prepareRsbuild = async ( environments: { [name]: { dev: { - writeToDisk: false, + writeToDisk, }, output: { sourceMap: { js: 'source-map', }, - externals: { - '@rstest/core': 'global @rstest/core', - }, + externals: [ + { + '@rstest/core': 'global @rstest/core', + }, + autoExternalNodeModules, + ], target: 'node', }, tools: { rspack: (config) => { config.output ??= {}; + config.output.iife = false; + config.externalsPresets = { node: true }; config.output.devtoolModuleFilenameTemplate = '[absolute-resource-path]'; diff --git a/packages/core/src/pool/index.ts b/packages/core/src/pool/index.ts index 2ab8168..fad1d0f 100644 --- a/packages/core/src/pool/index.ts +++ b/packages/core/src/pool/index.ts @@ -79,7 +79,12 @@ export const runInPool = async ({ isolate, maxWorkers, minWorkers, - execArgv: [...(poolOptions?.execArgv ?? []), ...execArgv], + execArgv: [ + ...(poolOptions?.execArgv ?? []), + ...execArgv, + '--experimental-vm-modules', + '--experimental-import-meta-resolve', + ], env: { NODE_ENV: 'test', // enable diff color by default diff --git a/packages/core/src/runtime/worker/index.ts b/packages/core/src/runtime/worker/index.ts index 174d059..d1a1cfb 100644 --- a/packages/core/src/runtime/worker/index.ts +++ b/packages/core/src/runtime/worker/index.ts @@ -64,7 +64,7 @@ const runInPool = async ({ for (const { filePath, originPath } of setupEntries) { const setupCodeContent = assetFiles[filePath]!; - loadModule({ + await loadModule({ codeContent: setupCodeContent, distPath: filePath, originPath: originPath, @@ -73,7 +73,7 @@ const runInPool = async ({ }); } - loadModule({ + await loadModule({ codeContent, distPath: filePath, originPath, diff --git a/packages/core/src/runtime/worker/loadModule.ts b/packages/core/src/runtime/worker/loadModule.ts index 705a13b..fba6ff4 100644 --- a/packages/core/src/runtime/worker/loadModule.ts +++ b/packages/core/src/runtime/worker/loadModule.ts @@ -1,4 +1,5 @@ import { createRequire as createNativeRequire } from 'node:module'; +import { pathToFileURL } from 'node:url'; import vm from 'node:vm'; import path from 'pathe'; import { logger } from '../../utils/logger'; @@ -87,6 +88,20 @@ export const loadModule = ({ filename: distPath, lineOffset: 0, columnOffset: -codeDefinition.length, + importModuleDynamically: async ( + specifier, + _referencer, + importAttributes, + ) => { + const dependencyAsset = import.meta.resolve( + specifier, + pathToFileURL(originPath), + ); + + // @ts-expect-error + const res = await import(dependencyAsset, importAttributes); + return res; + }, }); fn(...Object.values(context)); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1a688d1..859616d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -156,6 +156,18 @@ importers: specifier: workspace:* version: link:../../packages/core + tests/externals: + devDependencies: + '@rstest/core': + specifier: workspace:* + version: link:../../packages/core + picocolors: + specifier: ^1.1.1 + version: 1.1.1 + strip-ansi: + specifier: ^7.1.0 + version: 7.1.0 + tests/lifecycle: devDependencies: '@rstest/core': diff --git a/tests/externals/fixtures/index.test.ts b/tests/externals/fixtures/index.test.ts new file mode 100644 index 0000000..cd17f46 --- /dev/null +++ b/tests/externals/fixtures/index.test.ts @@ -0,0 +1,16 @@ +import { expect, it } from '@rstest/core'; +import stripAnsi from 'strip-ansi'; + +it('should load esm correctly', () => { + expect(stripAnsi('\u001B[4mUnicorn\u001B[0m')).toBe('Unicorn'); +}); + +it('should load esm dynamic correctly', async () => { + const { default: stripAnsi } = await import('strip-ansi'); + expect(stripAnsi('\u001B[4mUnicorn\u001B[0m')).toBe('Unicorn'); +}); + +it('should load cjs with require correctly', () => { + const picocolors = require('picocolors'); + expect(picocolors.green).toBeDefined(); +}); diff --git a/tests/externals/index.test.ts b/tests/externals/index.test.ts new file mode 100644 index 0000000..09e55e1 --- /dev/null +++ b/tests/externals/index.test.ts @@ -0,0 +1,34 @@ +import fs from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from '@rstest/core'; +import { runRstestCli } from '../scripts/'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +describe('test externals', () => { + it('should external node_modules by default', async () => { + process.env.DEBUG_RSTEST_OUTPUTS = 'true'; + const { cli } = await runRstestCli({ + command: 'rstest', + args: ['run', './fixtures/index.test.ts'], + options: { + nodeOptions: { + cwd: __dirname, + }, + }, + }); + + await cli.exec; + expect(cli.exec.process?.exitCode).toBe(0); + + const outputPath = join(__dirname, 'dist/fixtures/index.test.ts.js'); + + expect(fs.existsSync(outputPath)).toBeTruthy(); + const content = fs.readFileSync(outputPath, 'utf-8'); + + expect(content).toContain('require("picocolors")'); + expect(content).toContain('import("strip-ansi")'); + }); +}); diff --git a/tests/externals/package.json b/tests/externals/package.json new file mode 100644 index 0000000..9d3be4a --- /dev/null +++ b/tests/externals/package.json @@ -0,0 +1,10 @@ +{ + "private": true, + "name": "@rstest/tests-externals", + "version": "1.0.0", + "devDependencies": { + "@rstest/core": "workspace:*", + "picocolors": "^1.1.1", + "strip-ansi": "^7.1.0" + } +}