diff --git a/package.json b/package.json index 975a2aa6..8d77feab 100644 --- a/package.json +++ b/package.json @@ -93,9 +93,9 @@ "lint": "biome check ./src && node tools/check-color-literals.mjs && node tools/check-doc-links.mjs", "lint:fix": "biome check --write ./src", "format": "biome format --write .", - "test": "node --loader=./tools/svg-loader.mjs --import=./tools/__mocks__/sprite-init.mjs --experimental-test-snapshots --require ./tools/test-setup.cjs --test --test-concurrency=4 --no-warnings src/**/*.test.{js,mjs}", - "test:watch": "node --watch --loader=./tools/svg-loader.mjs --import=./tools/__mocks__/sprite-init.mjs --experimental-test-snapshots --require ./tools/test-setup.cjs --test --test-concurrency=4 --no-warnings src/**/*.test.{js,mjs}", - "test:updateSnapshots": "node --loader=./tools/svg-loader.mjs --import=./tools/__mocks__/sprite-init.mjs --experimental-test-snapshots --test-update-snapshots --require ./tools/test-setup.cjs --test --test-concurrency=4 --no-warnings src/**/*.test.{js,mjs}", + "test": "node --import=./tools/register-test-hooks.mjs --import=./tools/__mocks__/sprite-init.mjs --experimental-test-snapshots --require ./tools/test-setup.cjs --test --test-reporter=spec --test-concurrency=4 --no-warnings \"src/**/*.test.{js,mjs}\"", + "test:watch": "node --watch --import=./tools/register-test-hooks.mjs --import=./tools/__mocks__/sprite-init.mjs --experimental-test-snapshots --require ./tools/test-setup.cjs --test --test-reporter=spec --test-concurrency=4 --no-warnings \"src/**/*.test.{js,mjs}\"", + "test:updateSnapshots": "node --import=./tools/register-test-hooks.mjs --import=./tools/__mocks__/sprite-init.mjs --experimental-test-snapshots --test-update-snapshots --require ./tools/test-setup.cjs --test --test-reporter=spec --test-concurrency=4 --no-warnings \"src/**/*.test.{js,mjs}\"", "test:ci": "npm test --coverage", "test:dist": "node --test --no-warnings tools/dist.test.mjs", "test:types": "tsc -p src/types/tsconfig.json", diff --git a/tools/register-test-hooks.mjs b/tools/register-test-hooks.mjs new file mode 100644 index 00000000..f48baae2 --- /dev/null +++ b/tools/register-test-hooks.mjs @@ -0,0 +1,16 @@ +import module from 'node:module' +import { load } from './svg-loader.mjs' + +/** + * Registers the test module hooks. Loaded with `--import` before any test code. + * + * `module.registerHooks()` (Node >= 22.15 / 23.5) runs the hooks in-thread and synchronously, + * so they also apply to `require()` of ES modules. The older `--loader` flag ran async hooks + * off-thread and crashed on Node 22 with "ERR_METHOD_NOT_IMPLEMENTED: The resolveSync() method + * is not implemented" as soon as a CommonJS module required an ES module. + */ +if (typeof module.registerHooks === 'function') { + module.registerHooks({ load }) +} else { + module.register('./svg-loader.mjs', import.meta.url) +} diff --git a/tools/svg-loader.mjs b/tools/svg-loader.mjs index 5f399718..e3e58bc4 100644 --- a/tools/svg-loader.mjs +++ b/tools/svg-loader.mjs @@ -2,37 +2,27 @@ import { readFileSync } from 'node:fs' import { fileURLToPath } from 'node:url' /** - * Custom loader to handle SVG imports with ?raw suffix in tests - * This loader is needed because Node.js doesn't natively support importing .svg files + * Module load hook that handles SVG imports (with or without the ?raw suffix) in tests, + * because Node.js doesn't natively support importing .svg files. + * + * The hook is synchronous so it can be used with `module.registerHooks()`, which also covers + * `require()` of ES modules. It works unchanged as an async `module.register()` hook. */ - -export async function resolve(specifier, context, nextResolve) { - // Let the default resolver handle the path resolution first - return nextResolve(specifier, context) -} - -export async function load(url, context, nextLoad) { - // Check if this is an SVG file (with or without ?raw query parameter) +export function load(url, context, nextLoad) { const urlWithoutQuery = url.split('?')[0] if (urlWithoutQuery.endsWith('.svg')) { + let content = '' try { - const filePath = fileURLToPath(urlWithoutQuery) - const content = readFileSync(filePath, 'utf-8') - - // Return as an ES module with a default export - return { - format: 'module', - source: `export default ${JSON.stringify(content)}`, - shortCircuit: true, - } + content = readFileSync(fileURLToPath(urlWithoutQuery), 'utf-8') } catch { - // If file doesn't exist or can't be read, return empty string - return { - format: 'module', - source: 'export default ""', - shortCircuit: true, - } + // If the file doesn't exist or can't be read, export an empty string + } + + return { + format: 'module', + source: `export default ${JSON.stringify(content)}`, + shortCircuit: true, } }