From 903ff3e938cf7d7b68cd1cd3718659d136bc42b0 Mon Sep 17 00:00:00 2001 From: KCM Date: Sat, 29 Nov 2025 13:51:01 -0600 Subject: [PATCH 1/5] fix: sourcemaps for pirates hook. --- packages/core/register.mjs | 21 ++++++++- .../__tests__/fixtures/source-map-fixture.ts | 7 +++ .../integrate-ava/__tests__/register.spec.ts | 45 +++++++++++++++++++ 3 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 packages/integrate-ava/__tests__/fixtures/source-map-fixture.ts create mode 100644 packages/integrate-ava/__tests__/register.spec.ts diff --git a/packages/core/register.mjs b/packages/core/register.mjs index 84960e0f..eaa2d27a 100644 --- a/packages/core/register.mjs +++ b/packages/core/register.mjs @@ -1,4 +1,4 @@ -import { register } from 'node:module' +import { register, setSourceMapsSupport } from 'node:module' import { addHook } from 'pirates' @@ -8,11 +8,28 @@ const DEFAULT_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.mts' register('@oxc-node/core/esm', import.meta.url) +if (typeof setSourceMapsSupport === 'function') { + setSourceMapsSupport(true, { nodeModules: true, generatedCode: true }) +} else if (typeof process.setSourceMapsEnabled === 'function') { + process.setSourceMapsEnabled(true) +} + const transformer = new OxcTransformer(process.cwd()) +const SOURCEMAP_PREFIX = '\n//# sourceMappingURL=' +const SOURCEMAP_MIME = 'data:application/json;charset=utf-8;base64,' addHook( (code, filename) => { - return transformer.transform(filename, code).source() + const output = transformer.transform(filename, code) + let transformed = output.source() + const sourceMap = output.sourceMap() + + if (sourceMap) { + const inlineMap = Buffer.from(sourceMap, 'utf8').toString('base64') + transformed += SOURCEMAP_PREFIX + SOURCEMAP_MIME + inlineMap + } + + return transformed }, { ext: Array.from(DEFAULT_EXTENSIONS), diff --git a/packages/integrate-ava/__tests__/fixtures/source-map-fixture.ts b/packages/integrate-ava/__tests__/fixtures/source-map-fixture.ts new file mode 100644 index 00000000..925e3a3b --- /dev/null +++ b/packages/integrate-ava/__tests__/fixtures/source-map-fixture.ts @@ -0,0 +1,7 @@ +export function example(value: number): number { + if (value < 0) { + throw new Error('negative value') + } + + return value * 2 +} diff --git a/packages/integrate-ava/__tests__/register.spec.ts b/packages/integrate-ava/__tests__/register.spec.ts new file mode 100644 index 00000000..37a37bc2 --- /dev/null +++ b/packages/integrate-ava/__tests__/register.spec.ts @@ -0,0 +1,45 @@ +import test from 'ava' +import Module from 'node:module' +import { createRequire } from 'node:module' +import { fileURLToPath } from 'node:url' + +const require = createRequire(import.meta.url) +const SOURCE_MAP_SNIPPET = '//# sourceMappingURL=data:application/json' + +type CompileFn = (this: unknown, source: string, filename: string) => unknown +const modulePrototype = Module.prototype as unknown as { _compile: CompileFn } + +const registerPromise = import('@oxc-node/core/register') + +test('register hook adds inline source maps for TypeScript modules', async (t) => { + await registerPromise + const fixtureUrl = new URL('./fixtures/source-map-fixture.ts', import.meta.url) + const fixturePath = fileURLToPath(fixtureUrl) + const resolvedFixture = require.resolve(fixturePath) + + delete require.cache[resolvedFixture] + + const originalCompile = modulePrototype._compile + let compiledSource: string | undefined + + modulePrototype._compile = function patchedCompile(source: string, filename: string) { + if (filename === resolvedFixture) { + compiledSource = source + } + + return originalCompile.call(this, source, filename) + } + + try { + require(fixturePath) + } finally { + modulePrototype._compile = originalCompile + delete require.cache[resolvedFixture] + } + + t.truthy(compiledSource, 'fixture should compile under the register hook') + t.true( + compiledSource?.includes(SOURCE_MAP_SNIPPET), + 'inline source map data URL should be appended to transformed output', + ) +}) From 1bf4ee651e71418b6e489d02f694a9c7ca487adc Mon Sep 17 00:00:00 2001 From: KCM Date: Sat, 29 Nov 2025 15:35:43 -0600 Subject: [PATCH 2/5] test: syntax matrix for ts. --- .../__tests__/fixtures/stacktrace-cjs.cts | 8 ++ .../__tests__/fixtures/stacktrace-esm.mts | 8 ++ .../__tests__/fixtures/stacktrace-esm.ts | 8 ++ .../integrate-ava/__tests__/register.spec.ts | 96 ++++++++++++++++--- 4 files changed, 109 insertions(+), 11 deletions(-) create mode 100644 packages/integrate-ava/__tests__/fixtures/stacktrace-cjs.cts create mode 100644 packages/integrate-ava/__tests__/fixtures/stacktrace-esm.mts create mode 100644 packages/integrate-ava/__tests__/fixtures/stacktrace-esm.ts diff --git a/packages/integrate-ava/__tests__/fixtures/stacktrace-cjs.cts b/packages/integrate-ava/__tests__/fixtures/stacktrace-cjs.cts new file mode 100644 index 00000000..83c924dd --- /dev/null +++ b/packages/integrate-ava/__tests__/fixtures/stacktrace-cjs.cts @@ -0,0 +1,8 @@ +const { describe, it } = require('node:test') +const assert = require('node:assert/strict') + +describe('stacktrace cts', () => { + it('should preserve stack trace', () => { + assert.ok(false) + }) +}) diff --git a/packages/integrate-ava/__tests__/fixtures/stacktrace-esm.mts b/packages/integrate-ava/__tests__/fixtures/stacktrace-esm.mts new file mode 100644 index 00000000..c9c504dd --- /dev/null +++ b/packages/integrate-ava/__tests__/fixtures/stacktrace-esm.mts @@ -0,0 +1,8 @@ +import { describe, it } from 'node:test' +import assert from 'node:assert/strict' + +describe('stacktrace esm mts', () => { + it('should preserve stack trace', () => { + assert.ok(false) + }) +}) diff --git a/packages/integrate-ava/__tests__/fixtures/stacktrace-esm.ts b/packages/integrate-ava/__tests__/fixtures/stacktrace-esm.ts new file mode 100644 index 00000000..a7f055c4 --- /dev/null +++ b/packages/integrate-ava/__tests__/fixtures/stacktrace-esm.ts @@ -0,0 +1,8 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +describe('stacktrace esm ts', () => { + it('should preserve stack trace', () => { + assert.ok(false) + }) +}) diff --git a/packages/integrate-ava/__tests__/register.spec.ts b/packages/integrate-ava/__tests__/register.spec.ts index 37a37bc2..7a269f29 100644 --- a/packages/integrate-ava/__tests__/register.spec.ts +++ b/packages/integrate-ava/__tests__/register.spec.ts @@ -1,20 +1,35 @@ -import test from 'ava' -import Module from 'node:module' -import { createRequire } from 'node:module' -import { fileURLToPath } from 'node:url' +import test, { ExecutionContext } from 'ava' +import { spawnSync } from 'node:child_process' +import Module, { createRequire } from 'node:module' +import { fileURLToPath, pathToFileURL } from 'node:url' + +type CompileFn = (this: unknown, source: string, filename: string) => unknown const require = createRequire(import.meta.url) const SOURCE_MAP_SNIPPET = '//# sourceMappingURL=data:application/json' - -type CompileFn = (this: unknown, source: string, filename: string) => unknown +const STACKTRACE_LINE = 6 +const STACKTRACE_COLUMN = 12 const modulePrototype = Module.prototype as unknown as { _compile: CompileFn } - +const FIXTURE_PATHS = new Map( + [ + 'source-map-fixture.ts', + 'stacktrace-esm.ts', + 'stacktrace-esm.mts', + 'stacktrace-cjs.cts', + ].map((name) => [name, fileURLToPath(new URL(`./fixtures/${name}`, import.meta.url))]), +) +// @ts-expect-error Module shipped without TypeScript declarations const registerPromise = import('@oxc-node/core/register') - -test('register hook adds inline source maps for TypeScript modules', async (t) => { +const getFixturePath = (fixtureName: string) => { + const resolved = FIXTURE_PATHS.get(fixtureName) + if (!resolved) { + throw new Error(`Unknown fixture: ${fixtureName}`) + } + return resolved +} +const captureTransformedSource = async (fixtureName: string) => { await registerPromise - const fixtureUrl = new URL('./fixtures/source-map-fixture.ts', import.meta.url) - const fixturePath = fileURLToPath(fixtureUrl) + const fixturePath = getFixturePath(fixtureName) const resolvedFixture = require.resolve(fixturePath) delete require.cache[resolvedFixture] @@ -37,9 +52,68 @@ test('register hook adds inline source maps for TypeScript modules', async (t) = delete require.cache[resolvedFixture] } + return compiledSource +} + +test('register hook adds inline source maps for TypeScript modules', async (t) => { + const compiledSource = await captureTransformedSource('source-map-fixture.ts') + t.truthy(compiledSource, 'fixture should compile under the register hook') t.true( compiledSource?.includes(SOURCE_MAP_SNIPPET), 'inline source map data URL should be appended to transformed output', ) }) + +const runFixture = (loaderSpecifier: string, fixtureName: string) => { + const fixturePath = getFixturePath(fixtureName) + const result = spawnSync(process.execPath, ['--import', loaderSpecifier, '--test', fixturePath], { + encoding: 'utf8', + env: { + ...process.env, + NODE_OPTIONS: undefined, + }, + }) + + return { ...result, fixturePath } +} +// Fixture paths can contain characters (space, +, parentheses) that have special meaning in regexes, +// so escape them before embedding the path in a dynamic pattern. +const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\\]\\]/g, '\\$&') +const expectStackLocation = (t: ExecutionContext, output: string, fixturePath: string) => { + const fileUrl = pathToFileURL(fixturePath).href + const pattern = new RegExp(`(?:${escapeRegExp(fileUrl)}|${escapeRegExp(fixturePath)}):(\\d+):(\\d+)`) + const match = pattern.exec(output) + + t.truthy(match, 'stack trace should reference the failing fixture path') + + if (match) { + const [, line, column] = match + t.is(Number(line), STACKTRACE_LINE) + t.is(Number(column), STACKTRACE_COLUMN) + } +} +const stackTraceVariants: Array<{ loader: string; fixture: string }> = [ + ...['stacktrace-esm.ts', 'stacktrace-esm.mts', 'stacktrace-cjs.cts'].map((fixture) => ({ + loader: '@oxc-node/core/register', + fixture, + })), + // The ESM loader is not officially documented but ships today, so we keep a smoke test + // to track regressions for consumers using Node's --loader entry point directly. + { + loader: '@oxc-node/core/esm', + fixture: 'stacktrace-esm.ts', + }, +] + +for (const variant of stackTraceVariants) { + test(`stack trace for ${variant.fixture} via ${variant.loader}`, (t) => { + const { stdout, stderr, status, error, fixturePath } = runFixture(variant.loader, variant.fixture) + + t.falsy(error, error?.message) + t.not(status, 0, 'fixture should fail to trigger stack trace output') + + const combinedOutput = `${stdout}${stderr}` + expectStackLocation(t, combinedOutput, fixturePath) + }) +} From 21dfb475181c3a7a4708fdc08d1628cb866cb18e Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 30 Nov 2025 04:54:28 +0000 Subject: [PATCH 3/5] [autofix.ci] apply automated fixes --- packages/integrate-ava/__tests__/fixtures/stacktrace-esm.mts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/integrate-ava/__tests__/fixtures/stacktrace-esm.mts b/packages/integrate-ava/__tests__/fixtures/stacktrace-esm.mts index c9c504dd..65681d8b 100644 --- a/packages/integrate-ava/__tests__/fixtures/stacktrace-esm.mts +++ b/packages/integrate-ava/__tests__/fixtures/stacktrace-esm.mts @@ -1,5 +1,5 @@ -import { describe, it } from 'node:test' import assert from 'node:assert/strict' +import { describe, it } from 'node:test' describe('stacktrace esm mts', () => { it('should preserve stack trace', () => { From 9c9885d395053b182eec0df8178d5264a412c5f8 Mon Sep 17 00:00:00 2001 From: KCM Date: Sun, 30 Nov 2025 10:47:56 -0600 Subject: [PATCH 4/5] fix: syntax error for unsupported import. --- packages/core/register.mjs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/core/register.mjs b/packages/core/register.mjs index eaa2d27a..df84b683 100644 --- a/packages/core/register.mjs +++ b/packages/core/register.mjs @@ -1,9 +1,12 @@ -import { register, setSourceMapsSupport } from 'node:module' +import * as NodeModule from 'node:module' import { addHook } from 'pirates' import { OxcTransformer } from './index.js' +// Destructure from NodeModule namespace to support older Node.js versions +const { register, setSourceMapsSupport } = NodeModule + const DEFAULT_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.mts', '.cjs', '.cts', '.es6', '.es']) register('@oxc-node/core/esm', import.meta.url) From aa47548875d8b66eab85175bd25944dd9c2610d1 Mon Sep 17 00:00:00 2001 From: KCM Date: Sun, 30 Nov 2025 12:10:15 -0600 Subject: [PATCH 5/5] test: replace register spec with cli stack-trace test. --- packages/integrate-ava/__tests__/cli.spec.ts | 63 ++++++++++ .../integrate-ava/__tests__/register.spec.ts | 119 ------------------ 2 files changed, 63 insertions(+), 119 deletions(-) create mode 100644 packages/integrate-ava/__tests__/cli.spec.ts delete mode 100644 packages/integrate-ava/__tests__/register.spec.ts diff --git a/packages/integrate-ava/__tests__/cli.spec.ts b/packages/integrate-ava/__tests__/cli.spec.ts new file mode 100644 index 00000000..760940b5 --- /dev/null +++ b/packages/integrate-ava/__tests__/cli.spec.ts @@ -0,0 +1,63 @@ +import test, { ExecutionContext } from 'ava' +import { spawnSync } from 'node:child_process' +import { fileURLToPath, pathToFileURL } from 'node:url' + +const STACKTRACE_LINE = 6 +const STACKTRACE_COLUMN = 12 +const FIXTURE_PATHS = new Map( + ['stacktrace-esm.ts', 'stacktrace-esm.mts', 'stacktrace-cjs.cts'].map((name) => [ + name, + fileURLToPath(new URL(`./fixtures/${name}`, import.meta.url)), + ]), +) +const getFixturePath = (fixtureName: string) => { + const resolved = FIXTURE_PATHS.get(fixtureName) + if (!resolved) { + throw new Error(`Unknown fixture: ${fixtureName}`) + } + return resolved +} +const runCliFixture = (fixtureName: string) => { + const fixturePath = getFixturePath(fixtureName) + const result = spawnSync( + process.execPath, + ['--import', '@oxc-node/core/register', '--test', fixturePath], + { + encoding: 'utf8', + env: { + ...process.env, + NODE_OPTIONS: undefined, + }, + }, + ) + + return { ...result, fixturePath } +} +const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +const expectStackLocation = (t: ExecutionContext, output: string, fixturePath: string) => { + const fileUrl = pathToFileURL(fixturePath).href + const pattern = new RegExp( + `(?:${escapeRegExp(fileUrl)}|${escapeRegExp(fixturePath)}):(\\d+):(\\d+)`, + 'g', + ) + const matches = [...output.matchAll(pattern)] + + t.true(matches.length > 0, 'stack trace should reference the failing fixture path') + + const exactLocation = matches.find(([, line, column]) => { + return Number(line) === STACKTRACE_LINE && Number(column) === STACKTRACE_COLUMN + }) + + t.truthy(exactLocation, 'stack trace should include the original TypeScript location') +} + +for (const fixture of FIXTURE_PATHS.keys()) { + test(`CLI stack trace for ${fixture}`, (t) => { + const { stdout, stderr, status, error, fixturePath } = runCliFixture(fixture) + + t.falsy(error, error?.message) + t.not(status, 0, 'fixture should fail to trigger stack trace output') + + expectStackLocation(t, `${stdout}${stderr}`, fixturePath) + }) +} diff --git a/packages/integrate-ava/__tests__/register.spec.ts b/packages/integrate-ava/__tests__/register.spec.ts deleted file mode 100644 index 7a269f29..00000000 --- a/packages/integrate-ava/__tests__/register.spec.ts +++ /dev/null @@ -1,119 +0,0 @@ -import test, { ExecutionContext } from 'ava' -import { spawnSync } from 'node:child_process' -import Module, { createRequire } from 'node:module' -import { fileURLToPath, pathToFileURL } from 'node:url' - -type CompileFn = (this: unknown, source: string, filename: string) => unknown - -const require = createRequire(import.meta.url) -const SOURCE_MAP_SNIPPET = '//# sourceMappingURL=data:application/json' -const STACKTRACE_LINE = 6 -const STACKTRACE_COLUMN = 12 -const modulePrototype = Module.prototype as unknown as { _compile: CompileFn } -const FIXTURE_PATHS = new Map( - [ - 'source-map-fixture.ts', - 'stacktrace-esm.ts', - 'stacktrace-esm.mts', - 'stacktrace-cjs.cts', - ].map((name) => [name, fileURLToPath(new URL(`./fixtures/${name}`, import.meta.url))]), -) -// @ts-expect-error Module shipped without TypeScript declarations -const registerPromise = import('@oxc-node/core/register') -const getFixturePath = (fixtureName: string) => { - const resolved = FIXTURE_PATHS.get(fixtureName) - if (!resolved) { - throw new Error(`Unknown fixture: ${fixtureName}`) - } - return resolved -} -const captureTransformedSource = async (fixtureName: string) => { - await registerPromise - const fixturePath = getFixturePath(fixtureName) - const resolvedFixture = require.resolve(fixturePath) - - delete require.cache[resolvedFixture] - - const originalCompile = modulePrototype._compile - let compiledSource: string | undefined - - modulePrototype._compile = function patchedCompile(source: string, filename: string) { - if (filename === resolvedFixture) { - compiledSource = source - } - - return originalCompile.call(this, source, filename) - } - - try { - require(fixturePath) - } finally { - modulePrototype._compile = originalCompile - delete require.cache[resolvedFixture] - } - - return compiledSource -} - -test('register hook adds inline source maps for TypeScript modules', async (t) => { - const compiledSource = await captureTransformedSource('source-map-fixture.ts') - - t.truthy(compiledSource, 'fixture should compile under the register hook') - t.true( - compiledSource?.includes(SOURCE_MAP_SNIPPET), - 'inline source map data URL should be appended to transformed output', - ) -}) - -const runFixture = (loaderSpecifier: string, fixtureName: string) => { - const fixturePath = getFixturePath(fixtureName) - const result = spawnSync(process.execPath, ['--import', loaderSpecifier, '--test', fixturePath], { - encoding: 'utf8', - env: { - ...process.env, - NODE_OPTIONS: undefined, - }, - }) - - return { ...result, fixturePath } -} -// Fixture paths can contain characters (space, +, parentheses) that have special meaning in regexes, -// so escape them before embedding the path in a dynamic pattern. -const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\\]\\]/g, '\\$&') -const expectStackLocation = (t: ExecutionContext, output: string, fixturePath: string) => { - const fileUrl = pathToFileURL(fixturePath).href - const pattern = new RegExp(`(?:${escapeRegExp(fileUrl)}|${escapeRegExp(fixturePath)}):(\\d+):(\\d+)`) - const match = pattern.exec(output) - - t.truthy(match, 'stack trace should reference the failing fixture path') - - if (match) { - const [, line, column] = match - t.is(Number(line), STACKTRACE_LINE) - t.is(Number(column), STACKTRACE_COLUMN) - } -} -const stackTraceVariants: Array<{ loader: string; fixture: string }> = [ - ...['stacktrace-esm.ts', 'stacktrace-esm.mts', 'stacktrace-cjs.cts'].map((fixture) => ({ - loader: '@oxc-node/core/register', - fixture, - })), - // The ESM loader is not officially documented but ships today, so we keep a smoke test - // to track regressions for consumers using Node's --loader entry point directly. - { - loader: '@oxc-node/core/esm', - fixture: 'stacktrace-esm.ts', - }, -] - -for (const variant of stackTraceVariants) { - test(`stack trace for ${variant.fixture} via ${variant.loader}`, (t) => { - const { stdout, stderr, status, error, fixturePath } = runFixture(variant.loader, variant.fixture) - - t.falsy(error, error?.message) - t.not(status, 0, 'fixture should fail to trigger stack trace output') - - const combinedOutput = `${stdout}${stderr}` - expectStackLocation(t, combinedOutput, fixturePath) - }) -}