diff --git a/src/index.spec.ts b/src/index.spec.ts index 1ed62bd7d..69bb28487 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -9,6 +9,7 @@ import { unlinkSync, existsSync, lstatSync } from 'fs' import * as promisify from 'util.promisify' import { sync as rimrafSync } from 'rimraf' import { createRequire, createRequireFromPath } from 'module' +import { pathToFileURL } from 'url' import Module = require('module') const execP = promisify(exec) @@ -28,7 +29,7 @@ let { register, create, VERSION }: typeof tsNodeTypes = {} as any // Pack and install ts-node locally, necessary to test package "exports" before(async function () { - this.timeout(30000) + this.timeout(5 * 60e3) rimrafSync(join(TEST_DIR, 'node_modules')) await execP(`npm install`, { cwd: TEST_DIR }) const packageLockPath = join(TEST_DIR, 'package-lock.json') @@ -724,7 +725,7 @@ describe('ts-node', function () { describe('esm', () => { this.slow(1000) - const cmd = `node --loader ts-node/esm.mjs` + const cmd = `node --loader ts-node/esm` if (semver.gte(process.version, '13.0.0')) { it('should compile and execute as ESM', (done) => { @@ -735,6 +736,19 @@ describe('ts-node', function () { return done() }) }) + it('should use source maps', function (done) { + exec(`${cmd} throw.ts`, { cwd: join(__dirname, '../tests/esm') }, function (err, stdout) { + expect(err).not.to.equal(null) + expect(err!.message).to.contain([ + `${pathToFileURL(join(__dirname, '../tests/esm/throw.ts'))}:100`, + ' bar () { throw new Error(\'this is a demo\') }', + ' ^', + 'Error: this is a demo' + ].join('\n')) + + return done() + }) + }) it('supports --experimental-specifier-resolution=node', (done) => { exec(`${cmd} --experimental-specifier-resolution=node index.ts`, { cwd: join(__dirname, '../tests/esm-node-resolver') }, function (err, stdout) { expect(err).to.equal(null) diff --git a/src/index.ts b/src/index.ts index 2b7513f2d..c1a6215f5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,7 @@ import sourceMapSupport = require('source-map-support') import * as ynModule from 'yn' import { BaseError } from 'make-error' import * as util from 'util' +import { fileURLToPath } from 'url' import * as _ts from 'typescript' /** @@ -445,8 +446,18 @@ export function create (rawOptions: CreateOptions = {}): Register { // Install source map support and read from memory cache. sourceMapSupport.install({ environment: 'node', - retrieveFile (path: string) { - return outputCache.get(normalizeSlashes(path))?.content || '' + retrieveFile (pathOrUrl: string) { + let path = pathOrUrl + // If it's a file URL, convert to local path + // Note: fileURLToPath does not exist on early node v10 + // I could not find a way to handle non-URLs except to swallow an error + if (options.experimentalEsmLoader && path.startsWith('file://')) { + try { + path = fileURLToPath(path) + } catch (e) {/* swallow error */} + } + path = normalizeSlashes(path) + return outputCache.get(path)?.content || '' } }) diff --git a/tests/esm/throw.ts b/tests/esm/throw.ts new file mode 100644 index 000000000..85ba6da48 --- /dev/null +++ b/tests/esm/throw.ts @@ -0,0 +1,103 @@ +// intentional whitespace to prove that sourcemaps are working. Throw should happen on line 100. +// 100 lines is meant to be far more space than the helper functions would take. +class Foo { + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + constructor () { this.bar() } + bar () { throw new Error('this is a demo') } +} +new Foo() +export {}