diff --git a/tests/index.ts b/tests/index.ts index ac959be87..780a182f0 100644 --- a/tests/index.ts +++ b/tests/index.ts @@ -12,6 +12,7 @@ import { nodeVersions } from './utils/node-versions'; await describe(`Node ${node.version}`, async ({ runTestSuite }) => { await runTestSuite(import('./specs/cli'), node); await runTestSuite(import('./specs/watch'), node); + await runTestSuite(import('./specs/loaders'), node); await runTestSuite( import('./specs/smoke'), node, diff --git a/tests/specs/cli.ts b/tests/specs/cli.ts index 37e5a115c..351b30bcf 100644 --- a/tests/specs/cli.ts +++ b/tests/specs/cli.ts @@ -7,18 +7,11 @@ import { tsxPath } from '../utils/tsx.js'; import { ptyShell, isWindows } from '../utils/pty-shell/index'; import { expectMatchInOrder } from '../utils/expect-match-in-order.js'; import type { NodeApis } from '../utils/tsx.js'; -import { compareNodeVersion, type Version } from '../../src/utils/node-features.js'; - -const isProcessAlive = (pid: number) => { - try { - process.kill(pid, 0); - return true; - } catch {} - return false; -}; +import { isProcessAlive } from '../utils/is-process-alive.js'; export default testSuite(({ describe }, node: NodeApis) => { const { tsx } = node; + describe('CLI', ({ describe, test }) => { describe('argv', async ({ describe, onFinish }) => { const fixture = await createFixture({ @@ -109,12 +102,7 @@ export default testSuite(({ describe }, node: NodeApis) => { }); }); - const nodeVersion = node.version.split('.').map(Number) as Version; - - // https://nodejs.org/docs/latest-v18.x/api/cli.html#--test - const cliTestFlag = compareNodeVersion([18, 1, 0], nodeVersion) >= 0; - const testRunnerGlob = compareNodeVersion([21, 0, 0], nodeVersion) >= 0; - if (cliTestFlag) { + if (node.supports.cliTestFlag) { test('Node.js test runner', async ({ onTestFinish }) => { const fixture = await createFixture({ 'test.ts': ` @@ -132,7 +120,7 @@ export default testSuite(({ describe }, node: NodeApis) => { [ '--test', ...( - testRunnerGlob + node.supports.testRunnerGlob ? [] : ['test.ts'] ), @@ -140,7 +128,7 @@ export default testSuite(({ describe }, node: NodeApis) => { fixture.path, ); - if (testRunnerGlob) { + if (node.supports.testRunnerGlob) { expect(tsxProcess.stdout).toMatch('some passing test\n'); } else { expect(tsxProcess.stdout).toMatch('# pass 1\n'); diff --git a/tests/specs/loaders.ts b/tests/specs/loaders.ts new file mode 100644 index 000000000..e26ff3f01 --- /dev/null +++ b/tests/specs/loaders.ts @@ -0,0 +1,90 @@ +import { testSuite, expect } from 'manten'; +import { createFixture } from 'fs-fixture'; +import type { NodeApis } from '../utils/tsx.js'; + +export default testSuite(({ describe }, node: NodeApis) => { + describe('Loaders', ({ describe }) => { + describe('Hooks', async ({ test }) => { + const fixture = await createFixture({ + 'package.json': JSON.stringify({ type: 'module' }), + + 'ts.ts': ` + import fs from 'fs'; + + console.log(Boolean(fs) as unknown as string); + `, + 'mts.mts': ` + import fs from 'fs'; + + console.log(JSON.stringify([Boolean(fs) as unknown as string, import.meta.url])); + `, + }); + + test('.ts', async () => { + const tsxResult = await node.hook(['./ts.ts'], fixture.path); + + expect(tsxResult.stdout).toBe('true'); + if (node.supports.moduleRegister) { + expect(tsxResult.stderr).toBe(''); + } else { + expect(tsxResult.stderr).toMatch('ExperimentalWarning: Custom ESM Loaders is an experimental feature'); + } + expect(tsxResult.exitCode).toBe(0); + }); + + test('.mts', async () => { + const tsxResult = await node.hook(['./mts.mts'], fixture.path); + + const [imported, importMetaUrl] = JSON.parse(tsxResult.stdout); + expect(imported).toBe(true); + expect(importMetaUrl.endsWith('/mts.mts')).toBeTruthy(); + + if (node.supports.moduleRegister) { + expect(tsxResult.stderr).toBe(''); + } else { + expect(tsxResult.stderr).toMatch('ExperimentalWarning: Custom ESM Loaders is an experimental feature'); + } + expect(tsxResult.exitCode).toBe(0); + }); + }); + + describe('CJS patching', async ({ test }) => { + const fixture = await createFixture({ + 'package.json': JSON.stringify({ type: 'commonjs' }), + + 'ts.ts': ` + import fs from 'fs'; + + console.log(Boolean(fs) as unknown as string); + `, + 'cts.cts': ` + import fs from 'fs'; + + console.log(Boolean(fs) as unknown as string); + `, + 'mts.mts': ` + import fs from 'fs'; + + console.log(Boolean(fs) as unknown as string, import.meta.url); + `, + }); + + test('.ts', async () => { + const tsxResult = await node.cjsPatched(['./ts.ts'], fixture.path); + + expect(tsxResult.stdout).toBe('true'); + expect(tsxResult.stderr).toBe(''); + expect(tsxResult.exitCode).toBe(0); + }); + + // TODO: Investigate why this works -- it shouldnt + // test('should not be able to load .mjs', async () => { + // const tsxResult = await node.cjsPatched(['./mts.mts'], fixture.path); + + // expect(tsxResult.stdout).toBe('true'); + // expect(tsxResult.stderr).toBe(''); + // expect(tsxResult.exitCode).toBe(0); + // }); + }); + }); +}); diff --git a/tests/utils/is-process-alive.ts b/tests/utils/is-process-alive.ts new file mode 100644 index 000000000..ef55e3df9 --- /dev/null +++ b/tests/utils/is-process-alive.ts @@ -0,0 +1,7 @@ +export const isProcessAlive = (pid: number) => { + try { + process.kill(pid, 0); + return true; + } catch {} + return false; +}; diff --git a/tests/utils/node-features.ts b/tests/utils/node-features.ts new file mode 100644 index 000000000..a175bddb8 --- /dev/null +++ b/tests/utils/node-features.ts @@ -0,0 +1,10 @@ +export type Version = [number, number, number]; + +export const compareNodeVersion = ( + version: Version, + nodeVersion: Version, +) => ( + nodeVersion[0] - version[0] + || nodeVersion[1] - version[1] + || nodeVersion[2] - version[2] +); diff --git a/tests/utils/tsx.ts b/tests/utils/tsx.ts index 4e20a8db3..7722b69b2 100644 --- a/tests/utils/tsx.ts +++ b/tests/utils/tsx.ts @@ -1,7 +1,7 @@ -import path from 'path'; import { fileURLToPath } from 'url'; import { execaNode } from 'execa'; import getNode from 'get-node'; +import { compareNodeVersion, type Version } from './node-features.js'; type Options = { args: string[]; @@ -9,8 +9,10 @@ type Options = { cwd?: string; }; -const __dirname = fileURLToPath(import.meta.url); -export const tsxPath = path.join(__dirname, '../../../dist/cli.mjs'); +export const tsxPath = fileURLToPath(new URL('../../dist/cli.mjs', import.meta.url).toString()); + +const cjsPatchPath = fileURLToPath(new URL('../../dist/cjs/index.cjs', import.meta.url).toString()); +const hookPath = new URL('../../dist/esm/index.cjs', import.meta.url).toString(); export const tsx = ( options: Options, @@ -38,11 +40,26 @@ export const createNode = async ( const node = await getNode(nodeVersion, { progress: true, }); - console.log('Got node', Date.now() - startTime, node); + console.log(`Got node in ${Date.now() - startTime}ms`, node); + + const versionParsed = node.version.split('.').map(Number) as Version; + const supports = { + moduleRegister: compareNodeVersion([20, 6, 0], versionParsed) >= 0, + + // https://nodejs.org/docs/latest-v18.x/api/cli.html#--test + cliTestFlag: compareNodeVersion([18, 1, 0], versionParsed) >= 0, + + testRunnerGlob: compareNodeVersion([21, 0, 0], versionParsed) >= 0, + }; + const hookFlag = supports.moduleRegister ? '--import' : '--loader'; return { version: node.version, + path: node.path, + + supports, + tsx: ( args: string[], cwd?: string, @@ -61,6 +78,24 @@ export const createNode = async ( all: true, }, ), + + cjsPatched: ( + args: string[], + cwd?: string, + ) => execaNode(args[0], args.slice(1), { + cwd, + nodePath: node.path, + nodeOptions: ['--require', cjsPatchPath], + }), + + hook: ( + args: string[], + cwd?: string, + ) => execaNode(args[0], args.slice(1), { + cwd, + nodePath: node.path, + nodeOptions: [hookFlag, hookPath], + }), }; };