Skip to content

Commit c44f76e

Browse files
committed
merge latest master
2 parents 12022f0 + 8dfb75c commit c44f76e

File tree

14 files changed

+350
-21
lines changed

14 files changed

+350
-21
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ dist/
77
tsconfig.schema.json
88
tsconfig.schemastore-schema.json
99
.idea/
10+
/.vscode/

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,11 @@ Any error that is not a `TSError` is from node.js (e.g. `SyntaxError`), and cann
224224

225225
### Import Statements
226226

227-
Current node.js stable releases do not support ES modules. Additionally, `ts-node` does not have the required hooks into node.js to support ES modules. You will need to set `"module": "commonjs"` in your `tsconfig.json` for your code to work.
227+
There are two options when using `import` statements: compile them to CommonJS or use node's native ESM support.
228+
229+
To compile to CommonJS, you must set `"module": "CommonJS"` in your `tsconfig.json` or compiler options.
230+
231+
Node's native ESM support is currently experimental and so is `ts-node`'s ESM loader hook. For usage, limitations, and to provide feedback, see [#1007](https://github.com/TypeStrong/ts-node/issues/1007).
228232

229233
## Help! My Types Are Missing!
230234

dist-raw/node-createrequire.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Extracted from https://github.com/nodejs/node/blob/ec2ffd6b9d255e19818b6949d2f7dc7ac70faee9/lib/internal/modules/cjs/loader.js
2+
// then modified to suit our needs
3+
4+
const path = require('path');
5+
const Module = require('module');
6+
7+
exports.createRequireFromPath = createRequireFromPath;
8+
9+
function createRequireFromPath(filename) {
10+
// Allow a directory to be passed as the filename
11+
const trailingSlash =
12+
filename.endsWith('/') || (isWindows && filename.endsWith('\\'));
13+
14+
const proxyPath = trailingSlash ?
15+
path.join(filename, 'noop.js') :
16+
filename;
17+
18+
const m = new Module(proxyPath);
19+
m.filename = proxyPath;
20+
21+
m.paths = Module._nodeModulePaths(m.path);
22+
return makeRequireFunction(m, proxyPath);
23+
}
24+
25+
// This trick is much smaller than copy-pasting from https://github.com/nodejs/node/blob/ec2ffd6b9d255e19818b6949d2f7dc7ac70faee9/lib/internal/modules/cjs/helpers.js#L32-L101
26+
function makeRequireFunction(module, filename) {
27+
module._compile('module.exports = require;', filename)
28+
return mod.exports
29+
}

src/bin.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export function main (argv: string[]) {
9090
'--help': help = false,
9191
'--script-mode': scriptMode = false,
9292
'--version': version = 0,
93-
'--require': requires = [],
93+
'--require': argsRequire = [],
9494
'--eval': code = undefined,
9595
'--print': print = false,
9696
'--interactive': interactive = false,
@@ -176,6 +176,7 @@ export function main (argv: string[]) {
176176
compiler,
177177
ignoreDiagnostics,
178178
compilerOptions,
179+
require: argsRequire,
179180
readFile: code !== undefined
180181
? (path: string) => {
181182
if (path === state.path) return state.input
@@ -212,9 +213,6 @@ export function main (argv: string[]) {
212213
module.filename = state.path
213214
module.paths = (Module as any)._nodeModulePaths(cwd)
214215

215-
// Require specified modules before start-up.
216-
;(Module as any)._preloadModules(requires)
217-
218216
// Prepend `ts-node` arguments to CLI for child processes.
219217
process.execArgv.unshift(__filename, ...process.argv.slice(2, process.argv.length - args._.length))
220218
process.argv = [process.argv[1]].concat(scriptPath || []).concat(args._.slice(1))
@@ -365,7 +363,8 @@ function startRepl (service: Register, state: EvalState, code?: string) {
365363
prompt: '> ',
366364
input: process.stdin,
367365
output: process.stdout,
368-
terminal: process.stdout.isTTY,
366+
// Mimicking node's REPL implementation: https://github.com/nodejs/node/blob/168b22ba073ee1cbf8d0bcb4ded7ff3099335d04/lib/internal/repl.js#L28-L30
367+
terminal: process.stdout.isTTY && !parseInt(process.env.NODE_NO_READLINE!, 10),
369368
eval: replEval,
370369
useGlobal: true
371370
})

src/index.spec.ts

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { unlinkSync, existsSync, lstatSync } from 'fs'
99
import * as promisify from 'util.promisify'
1010
import { sync as rimrafSync } from 'rimraf'
1111
import { createRequire, createRequireFromPath } from 'module'
12+
import { pathToFileURL } from 'url'
1213
import Module = require('module')
1314

1415
const execP = promisify(exec)
@@ -28,7 +29,7 @@ let { register, create, VERSION }: typeof tsNodeTypes = {} as any
2829

2930
// Pack and install ts-node locally, necessary to test package "exports"
3031
before(async function () {
31-
this.timeout(30000)
32+
this.timeout(5 * 60e3)
3233
rimrafSync(join(TEST_DIR, 'node_modules'))
3334
await execP(`npm install`, { cwd: TEST_DIR })
3435
const packageLockPath = join(TEST_DIR, 'package-lock.json')
@@ -344,10 +345,15 @@ describe('ts-node', function () {
344345
})
345346
})
346347

347-
it.skip('should use source maps with react tsx', function (done) {
348-
exec(`${cmd} -r ./tests/emit-compiled.ts tests/jsx-react.tsx`, function (err, stdout) {
349-
expect(err).to.equal(null)
350-
expect(stdout).to.equal('todo')
348+
it('should use source maps with react tsx', function (done) {
349+
exec(`${cmd} tests/throw-react-tsx.tsx`, function (err, stdout) {
350+
expect(err).not.to.equal(null)
351+
expect(err!.message).to.contain([
352+
`${join(__dirname, '../tests/throw-react-tsx.tsx')}:100`,
353+
' bar () { throw new Error(\'this is a demo\') }',
354+
' ^',
355+
'Error: this is a demo'
356+
].join('\n'))
351357

352358
return done()
353359
})
@@ -471,7 +477,7 @@ describe('ts-node', function () {
471477
const BIN_EXEC = `"${BIN_PATH}" --project tests/tsconfig-options/tsconfig.json`
472478

473479
it('should override compiler options from env', function (done) {
474-
exec(`${BIN_EXEC} tests/tsconfig-options/log-options.js`, {
480+
exec(`${BIN_EXEC} tests/tsconfig-options/log-options1.js`, {
475481
env: {
476482
...process.env,
477483
TS_NODE_COMPILER_OPTIONS: '{"typeRoots": ["env-typeroots"]}'
@@ -485,33 +491,38 @@ describe('ts-node', function () {
485491
})
486492

487493
it('should use options from `tsconfig.json`', function (done) {
488-
exec(`${BIN_EXEC} tests/tsconfig-options/log-options.js`, function (err, stdout) {
494+
exec(`${BIN_EXEC} tests/tsconfig-options/log-options1.js`, function (err, stdout) {
489495
expect(err).to.equal(null)
490496
const { options, config } = JSON.parse(stdout)
491497
expect(config.options.typeRoots).to.deep.equal([join(__dirname, '../tests/tsconfig-options/tsconfig-typeroots').replace(/\\/g, '/')])
492498
expect(config.options.types).to.deep.equal(['tsconfig-tsnode-types'])
493499
expect(options.pretty).to.equal(undefined)
494500
expect(options.skipIgnore).to.equal(false)
495501
expect(options.transpileOnly).to.equal(true)
502+
expect(options.require).to.deep.equal([join(__dirname, '../tests/tsconfig-options/required1.js')])
496503
return done()
497504
})
498505
})
499506

500-
it('should have flags override `tsconfig.json`', function (done) {
501-
exec(`${BIN_EXEC} --skip-ignore --compiler-options "{\\"types\\":[\\"flags-types\\"]}" tests/tsconfig-options/log-options.js`, function (err, stdout) {
507+
it('should have flags override / merge with `tsconfig.json`', function (done) {
508+
exec(`${BIN_EXEC} --skip-ignore --compiler-options "{\\"types\\":[\\"flags-types\\"]}" --require ./tests/tsconfig-options/required2.js tests/tsconfig-options/log-options2.js`, function (err, stdout) {
502509
expect(err).to.equal(null)
503510
const { options, config } = JSON.parse(stdout)
504511
expect(config.options.typeRoots).to.deep.equal([join(__dirname, '../tests/tsconfig-options/tsconfig-typeroots').replace(/\\/g, '/')])
505512
expect(config.options.types).to.deep.equal(['flags-types'])
506513
expect(options.pretty).to.equal(undefined)
507514
expect(options.skipIgnore).to.equal(true)
508515
expect(options.transpileOnly).to.equal(true)
516+
expect(options.require).to.deep.equal([
517+
join(__dirname, '../tests/tsconfig-options/required1.js'),
518+
'./tests/tsconfig-options/required2.js'
519+
])
509520
return done()
510521
})
511522
})
512523

513524
it('should have `tsconfig.json` override environment', function (done) {
514-
exec(`${BIN_EXEC} tests/tsconfig-options/log-options.js`, {
525+
exec(`${BIN_EXEC} tests/tsconfig-options/log-options1.js`, {
515526
env: {
516527
...process.env,
517528
TS_NODE_PRETTY: 'true',
@@ -525,6 +536,7 @@ describe('ts-node', function () {
525536
expect(options.pretty).to.equal(true)
526537
expect(options.skipIgnore).to.equal(false)
527538
expect(options.transpileOnly).to.equal(true)
539+
expect(options.require).to.deep.equal([join(__dirname, '../tests/tsconfig-options/required1.js')])
528540
return done()
529541
})
530542
})
@@ -724,7 +736,7 @@ describe('ts-node', function () {
724736
describe('esm', () => {
725737
this.slow(1000)
726738

727-
const cmd = `node --loader ts-node/esm.mjs`
739+
const cmd = `node --loader ts-node/esm`
728740

729741
if (semver.gte(process.version, '13.0.0')) {
730742
it('should compile and execute as ESM', (done) => {
@@ -735,6 +747,27 @@ describe('ts-node', function () {
735747
return done()
736748
})
737749
})
750+
it('should use source maps', function (done) {
751+
exec(`${cmd} throw.ts`, { cwd: join(__dirname, '../tests/esm') }, function (err, stdout) {
752+
expect(err).not.to.equal(null)
753+
expect(err!.message).to.contain([
754+
`${pathToFileURL(join(__dirname, '../tests/esm/throw.ts'))}:100`,
755+
' bar () { throw new Error(\'this is a demo\') }',
756+
' ^',
757+
'Error: this is a demo'
758+
].join('\n'))
759+
760+
return done()
761+
})
762+
})
763+
it('supports --experimental-specifier-resolution=node', (done) => {
764+
exec(`${cmd} --experimental-specifier-resolution=node index.ts`, { cwd: join(__dirname, '../tests/esm-node-resolver') }, function (err, stdout) {
765+
expect(err).to.equal(null)
766+
expect(stdout).to.equal('foo bar baz biff\n')
767+
768+
return done()
769+
})
770+
})
738771

739772
describe('supports experimental-specifier-resolution=node', () => {
740773
it('via --experimental-specifier-resolution', (done) => {

src/index.ts

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import sourceMapSupport = require('source-map-support')
33
import * as ynModule from 'yn'
44
import { BaseError } from 'make-error'
55
import * as util from 'util'
6+
import { fileURLToPath } from 'url'
67
import * as _ts from 'typescript'
8+
import * as Module from 'module'
79

810
/**
911
* Does this version of node obey the package.json "type" field
@@ -195,6 +197,15 @@ export interface CreateOptions {
195197
* Ignore TypeScript warnings by diagnostic code.
196198
*/
197199
ignoreDiagnostics?: Array<number | string>
200+
/**
201+
* Modules to require, like node's `--require` flag.
202+
*
203+
* If specified in tsconfig.json, the modules will be resolved relative to the tsconfig.json file.
204+
*
205+
* If specified programmatically, each input string should be pre-resolved to an absolute path for
206+
* best results.
207+
*/
208+
require?: Array<string>
198209
readFile?: (path: string) => string | undefined
199210
fileExists?: (path: string) => boolean
200211
transformers?: _ts.CustomTransformers | ((p: _ts.Program) => _ts.CustomTransformers)
@@ -228,7 +239,7 @@ export interface TsConfigOptions extends Omit<RegisterOptions,
228239
| 'skipProject'
229240
| 'project'
230241
| 'dir'
231-
> { }
242+
> {}
232243

233244
/**
234245
* Like `Object.assign`, but ignores `undefined` properties.
@@ -382,6 +393,9 @@ export function register (opts: RegisterOptions = {}): Register {
382393
// Register the extensions.
383394
registerExtensions(service.options.preferTsExts, extensions, service, originalJsHandler)
384395

396+
// Require specified modules before start-up.
397+
;(Module as any)._preloadModules(service.options.require)
398+
385399
return service
386400
}
387401

@@ -408,7 +422,11 @@ export function create (rawOptions: CreateOptions = {}): Register {
408422

409423
// Read config file and merge new options between env and CLI options.
410424
const { config, options: tsconfigOptions } = readConfig(cwd, ts, rawOptions)
411-
const options = assign<CreateOptions>({}, DEFAULTS, tsconfigOptions || {}, rawOptions)
425+
const options = assign<RegisterOptions>({}, DEFAULTS, tsconfigOptions || {}, rawOptions)
426+
options.require = [
427+
...tsconfigOptions.require || [],
428+
...rawOptions.require || []
429+
]
412430

413431
// If `compiler` option changed based on tsconfig, re-load the compiler.
414432
if (options.compiler !== compilerName) {
@@ -445,8 +463,18 @@ export function create (rawOptions: CreateOptions = {}): Register {
445463
// Install source map support and read from memory cache.
446464
sourceMapSupport.install({
447465
environment: 'node',
448-
retrieveFile (path: string) {
449-
return outputCache.get(normalizeSlashes(path))?.content || ''
466+
retrieveFile (pathOrUrl: string) {
467+
let path = pathOrUrl
468+
// If it's a file URL, convert to local path
469+
// Note: fileURLToPath does not exist on early node v10
470+
// I could not find a way to handle non-URLs except to swallow an error
471+
if (options.experimentalEsmLoader && path.startsWith('file://')) {
472+
try {
473+
path = fileURLToPath(path)
474+
} catch (e) {/* swallow error */}
475+
}
476+
path = normalizeSlashes(path)
477+
return outputCache.get(path)?.content || ''
450478
}
451479
})
452480

@@ -991,6 +1019,14 @@ function readConfig (
9911019
useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames
9921020
}, basePath, undefined, configFileName))
9931021

1022+
if (tsconfigOptions.require) {
1023+
// Modules are found relative to the tsconfig file, not the `dir` option
1024+
const tsconfigRelativeRequire = createRequire(configFileName!)
1025+
tsconfigOptions.require = tsconfigOptions.require.map((path: string) => {
1026+
return tsconfigRelativeRequire.resolve(path)
1027+
})
1028+
}
1029+
9941030
return { config: fixedConfig, options: tsconfigOptions }
9951031
}
9961032

@@ -1051,3 +1087,12 @@ function getTokenAtPosition (ts: typeof _ts, sourceFile: _ts.SourceFile, positio
10511087
return current
10521088
}
10531089
}
1090+
1091+
let nodeCreateRequire: (path: string) => NodeRequire
1092+
function createRequire (filename: string) {
1093+
if (!nodeCreateRequire) {
1094+
// tslint:disable-next-line
1095+
nodeCreateRequire = Module.createRequire || Module.createRequireFromPath || require('../dist-raw/node-createrequire').createRequireFromPath
1096+
}
1097+
return nodeCreateRequire(filename)
1098+
}

0 commit comments

Comments
 (0)