Skip to content

Commit

Permalink
merge latest master
Browse files Browse the repository at this point in the history
  • Loading branch information
cspotcode committed Aug 4, 2020
2 parents 12022f0 + 8dfb75c commit c44f76e
Show file tree
Hide file tree
Showing 14 changed files with 350 additions and 21 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ dist/
tsconfig.schema.json
tsconfig.schemastore-schema.json
.idea/
/.vscode/
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,11 @@ Any error that is not a `TSError` is from node.js (e.g. `SyntaxError`), and cann

### Import Statements

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.
There are two options when using `import` statements: compile them to CommonJS or use node's native ESM support.

To compile to CommonJS, you must set `"module": "CommonJS"` in your `tsconfig.json` or compiler options.

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).

## Help! My Types Are Missing!

Expand Down
29 changes: 29 additions & 0 deletions dist-raw/node-createrequire.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Extracted from https://github.com/nodejs/node/blob/ec2ffd6b9d255e19818b6949d2f7dc7ac70faee9/lib/internal/modules/cjs/loader.js
// then modified to suit our needs

const path = require('path');
const Module = require('module');

exports.createRequireFromPath = createRequireFromPath;

function createRequireFromPath(filename) {
// Allow a directory to be passed as the filename
const trailingSlash =
filename.endsWith('/') || (isWindows && filename.endsWith('\\'));

const proxyPath = trailingSlash ?
path.join(filename, 'noop.js') :
filename;

const m = new Module(proxyPath);
m.filename = proxyPath;

m.paths = Module._nodeModulePaths(m.path);
return makeRequireFunction(m, proxyPath);
}

// This trick is much smaller than copy-pasting from https://github.com/nodejs/node/blob/ec2ffd6b9d255e19818b6949d2f7dc7ac70faee9/lib/internal/modules/cjs/helpers.js#L32-L101
function makeRequireFunction(module, filename) {
module._compile('module.exports = require;', filename)
return mod.exports
}
9 changes: 4 additions & 5 deletions src/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export function main (argv: string[]) {
'--help': help = false,
'--script-mode': scriptMode = false,
'--version': version = 0,
'--require': requires = [],
'--require': argsRequire = [],
'--eval': code = undefined,
'--print': print = false,
'--interactive': interactive = false,
Expand Down Expand Up @@ -176,6 +176,7 @@ export function main (argv: string[]) {
compiler,
ignoreDiagnostics,
compilerOptions,
require: argsRequire,
readFile: code !== undefined
? (path: string) => {
if (path === state.path) return state.input
Expand Down Expand Up @@ -212,9 +213,6 @@ export function main (argv: string[]) {
module.filename = state.path
module.paths = (Module as any)._nodeModulePaths(cwd)

// Require specified modules before start-up.
;(Module as any)._preloadModules(requires)

// Prepend `ts-node` arguments to CLI for child processes.
process.execArgv.unshift(__filename, ...process.argv.slice(2, process.argv.length - args._.length))
process.argv = [process.argv[1]].concat(scriptPath || []).concat(args._.slice(1))
Expand Down Expand Up @@ -365,7 +363,8 @@ function startRepl (service: Register, state: EvalState, code?: string) {
prompt: '> ',
input: process.stdin,
output: process.stdout,
terminal: process.stdout.isTTY,
// Mimicking node's REPL implementation: https://github.com/nodejs/node/blob/168b22ba073ee1cbf8d0bcb4ded7ff3099335d04/lib/internal/repl.js#L28-L30
terminal: process.stdout.isTTY && !parseInt(process.env.NODE_NO_READLINE!, 10),
eval: replEval,
useGlobal: true
})
Expand Down
55 changes: 44 additions & 11 deletions src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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')
Expand Down Expand Up @@ -344,10 +345,15 @@ describe('ts-node', function () {
})
})

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

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

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

it('should use options from `tsconfig.json`', function (done) {
exec(`${BIN_EXEC} tests/tsconfig-options/log-options.js`, function (err, stdout) {
exec(`${BIN_EXEC} tests/tsconfig-options/log-options1.js`, function (err, stdout) {
expect(err).to.equal(null)
const { options, config } = JSON.parse(stdout)
expect(config.options.typeRoots).to.deep.equal([join(__dirname, '../tests/tsconfig-options/tsconfig-typeroots').replace(/\\/g, '/')])
expect(config.options.types).to.deep.equal(['tsconfig-tsnode-types'])
expect(options.pretty).to.equal(undefined)
expect(options.skipIgnore).to.equal(false)
expect(options.transpileOnly).to.equal(true)
expect(options.require).to.deep.equal([join(__dirname, '../tests/tsconfig-options/required1.js')])
return done()
})
})

it('should have flags override `tsconfig.json`', function (done) {
exec(`${BIN_EXEC} --skip-ignore --compiler-options "{\\"types\\":[\\"flags-types\\"]}" tests/tsconfig-options/log-options.js`, function (err, stdout) {
it('should have flags override / merge with `tsconfig.json`', function (done) {
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) {
expect(err).to.equal(null)
const { options, config } = JSON.parse(stdout)
expect(config.options.typeRoots).to.deep.equal([join(__dirname, '../tests/tsconfig-options/tsconfig-typeroots').replace(/\\/g, '/')])
expect(config.options.types).to.deep.equal(['flags-types'])
expect(options.pretty).to.equal(undefined)
expect(options.skipIgnore).to.equal(true)
expect(options.transpileOnly).to.equal(true)
expect(options.require).to.deep.equal([
join(__dirname, '../tests/tsconfig-options/required1.js'),
'./tests/tsconfig-options/required2.js'
])
return done()
})
})

it('should have `tsconfig.json` override environment', function (done) {
exec(`${BIN_EXEC} tests/tsconfig-options/log-options.js`, {
exec(`${BIN_EXEC} tests/tsconfig-options/log-options1.js`, {
env: {
...process.env,
TS_NODE_PRETTY: 'true',
Expand All @@ -525,6 +536,7 @@ describe('ts-node', function () {
expect(options.pretty).to.equal(true)
expect(options.skipIgnore).to.equal(false)
expect(options.transpileOnly).to.equal(true)
expect(options.require).to.deep.equal([join(__dirname, '../tests/tsconfig-options/required1.js')])
return done()
})
})
Expand Down Expand Up @@ -724,7 +736,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) => {
Expand All @@ -735,6 +747,27 @@ 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)
expect(stdout).to.equal('foo bar baz biff\n')

return done()
})
})

describe('supports experimental-specifier-resolution=node', () => {
it('via --experimental-specifier-resolution', (done) => {
Expand Down
53 changes: 49 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ 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'
import * as Module from 'module'

/**
* Does this version of node obey the package.json "type" field
Expand Down Expand Up @@ -195,6 +197,15 @@ export interface CreateOptions {
* Ignore TypeScript warnings by diagnostic code.
*/
ignoreDiagnostics?: Array<number | string>
/**
* Modules to require, like node's `--require` flag.
*
* If specified in tsconfig.json, the modules will be resolved relative to the tsconfig.json file.
*
* If specified programmatically, each input string should be pre-resolved to an absolute path for
* best results.
*/
require?: Array<string>
readFile?: (path: string) => string | undefined
fileExists?: (path: string) => boolean
transformers?: _ts.CustomTransformers | ((p: _ts.Program) => _ts.CustomTransformers)
Expand Down Expand Up @@ -228,7 +239,7 @@ export interface TsConfigOptions extends Omit<RegisterOptions,
| 'skipProject'
| 'project'
| 'dir'
> { }
> {}

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

// Require specified modules before start-up.
;(Module as any)._preloadModules(service.options.require)

return service
}

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

// Read config file and merge new options between env and CLI options.
const { config, options: tsconfigOptions } = readConfig(cwd, ts, rawOptions)
const options = assign<CreateOptions>({}, DEFAULTS, tsconfigOptions || {}, rawOptions)
const options = assign<RegisterOptions>({}, DEFAULTS, tsconfigOptions || {}, rawOptions)
options.require = [
...tsconfigOptions.require || [],
...rawOptions.require || []
]

// If `compiler` option changed based on tsconfig, re-load the compiler.
if (options.compiler !== compilerName) {
Expand Down Expand Up @@ -445,8 +463,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 || ''
}
})

Expand Down Expand Up @@ -991,6 +1019,14 @@ function readConfig (
useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames
}, basePath, undefined, configFileName))

if (tsconfigOptions.require) {
// Modules are found relative to the tsconfig file, not the `dir` option
const tsconfigRelativeRequire = createRequire(configFileName!)
tsconfigOptions.require = tsconfigOptions.require.map((path: string) => {
return tsconfigRelativeRequire.resolve(path)
})
}

return { config: fixedConfig, options: tsconfigOptions }
}

Expand Down Expand Up @@ -1051,3 +1087,12 @@ function getTokenAtPosition (ts: typeof _ts, sourceFile: _ts.SourceFile, positio
return current
}
}

let nodeCreateRequire: (path: string) => NodeRequire
function createRequire (filename: string) {
if (!nodeCreateRequire) {
// tslint:disable-next-line
nodeCreateRequire = Module.createRequire || Module.createRequireFromPath || require('../dist-raw/node-createrequire').createRequireFromPath
}
return nodeCreateRequire(filename)
}
Loading

0 comments on commit c44f76e

Please sign in to comment.