From 436e3a809585dd445deea40d09d65a59e5aa5644 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Thu, 30 Jul 2020 12:47:49 -0400 Subject: [PATCH] Allow specifying "require" option via tsconfig (#925) --- dist-raw/node-createrequire.js | 29 ++++++++++++++ src/bin.ts | 6 +-- src/index.spec.ts | 16 +++++--- src/index.ts | 38 ++++++++++++++++++- .../{log-options.js => log-options1.js} | 1 + tests/tsconfig-options/log-options2.js | 3 ++ tests/tsconfig-options/required1.js | 1 + tests/tsconfig-options/required2.js | 2 + tests/tsconfig-options/tsconfig.json | 1 + 9 files changed, 86 insertions(+), 11 deletions(-) create mode 100644 dist-raw/node-createrequire.js rename tests/tsconfig-options/{log-options.js => log-options1.js} (87%) create mode 100644 tests/tsconfig-options/log-options2.js create mode 100644 tests/tsconfig-options/required1.js create mode 100644 tests/tsconfig-options/required2.js diff --git a/dist-raw/node-createrequire.js b/dist-raw/node-createrequire.js new file mode 100644 index 000000000..649deb10d --- /dev/null +++ b/dist-raw/node-createrequire.js @@ -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 +} diff --git a/src/bin.ts b/src/bin.ts index 001e1f483..ee3380ffc 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -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, @@ -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 @@ -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)) diff --git a/src/index.spec.ts b/src/index.spec.ts index 1b7d12324..3e64030ae 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -477,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"]}' @@ -491,7 +491,7 @@ 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, '/')]) @@ -499,12 +499,13 @@ describe('ts-node', function () { 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, '/')]) @@ -512,12 +513,16 @@ describe('ts-node', function () { 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', @@ -531,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() }) }) diff --git a/src/index.ts b/src/index.ts index c1a6215f5..5aa2ea23b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,7 @@ 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 @@ -196,6 +197,15 @@ export interface CreateOptions { * Ignore TypeScript warnings by diagnostic code. */ ignoreDiagnostics?: Array + /** + * 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 readFile?: (path: string) => string | undefined fileExists?: (path: string) => boolean transformers?: _ts.CustomTransformers | ((p: _ts.Program) => _ts.CustomTransformers) @@ -229,7 +239,7 @@ export interface TsConfigOptions extends Omit { } + > {} /** * Like `Object.assign`, but ignores `undefined` properties. @@ -383,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 } @@ -409,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({}, DEFAULTS, tsconfigOptions || {}, rawOptions) + const options = assign({}, DEFAULTS, tsconfigOptions || {}, rawOptions) + options.require = [ + ...tsconfigOptions.require || [], + ...rawOptions.require || [] + ] // If `compiler` option changed based on tsconfig, re-load the compiler. if (options.compiler !== compilerName) { @@ -1002,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 } } @@ -1062,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) +} diff --git a/tests/tsconfig-options/log-options.js b/tests/tsconfig-options/log-options1.js similarity index 87% rename from tests/tsconfig-options/log-options.js rename to tests/tsconfig-options/log-options1.js index 8a5a76de8..59bc33323 100644 --- a/tests/tsconfig-options/log-options.js +++ b/tests/tsconfig-options/log-options1.js @@ -1,4 +1,5 @@ const assert = require('assert') +assert(process.required1) const register = process[Symbol.for('ts-node.register.instance')] console.log(JSON.stringify({ options: register.options, diff --git a/tests/tsconfig-options/log-options2.js b/tests/tsconfig-options/log-options2.js new file mode 100644 index 000000000..30f402d16 --- /dev/null +++ b/tests/tsconfig-options/log-options2.js @@ -0,0 +1,3 @@ +const assert = require('assert') +require('./log-options1') +assert(process.required2) diff --git a/tests/tsconfig-options/required1.js b/tests/tsconfig-options/required1.js new file mode 100644 index 000000000..8f3ac408f --- /dev/null +++ b/tests/tsconfig-options/required1.js @@ -0,0 +1 @@ +process.required1 = true diff --git a/tests/tsconfig-options/required2.js b/tests/tsconfig-options/required2.js new file mode 100644 index 000000000..69a3ba618 --- /dev/null +++ b/tests/tsconfig-options/required2.js @@ -0,0 +1,2 @@ +require('assert')(process.required1) +process.required2 = true diff --git a/tests/tsconfig-options/tsconfig.json b/tests/tsconfig-options/tsconfig.json index 6efea1e6f..3f248317e 100644 --- a/tests/tsconfig-options/tsconfig.json +++ b/tests/tsconfig-options/tsconfig.json @@ -5,6 +5,7 @@ "types": ["tsconfig-tsnode-types"] }, "transpileOnly": true, + "require": ["./required1"], "skipIgnore": false }, "compilerOptions": {