diff --git a/packages/core/register.mjs b/packages/core/register.mjs index 84960e0f..df84b683 100644 --- a/packages/core/register.mjs +++ b/packages/core/register.mjs @@ -1,18 +1,38 @@ -import { register } 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) +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__/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__/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__/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..65681d8b --- /dev/null +++ b/packages/integrate-ava/__tests__/fixtures/stacktrace-esm.mts @@ -0,0 +1,8 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +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) + }) +})