From 1e5e6660f8ca7fb649b51611fac8ead1df706755 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Tue, 22 Feb 2022 02:47:28 -0500 Subject: [PATCH 01/27] WIP --- child-loader.mjs | 7 ++ package.json | 6 +- src/bin-esm.ts | 5 ++ src/bin.ts | 159 ++++++++++++++++++++++++++-------- src/child/child-entrypoint.ts | 10 +++ src/child/child-loader.ts | 31 +++++++ src/child/child-require.ts | 23 +++++ src/child/spawn-child.ts | 56 ++++++++++++ src/configuration.ts | 2 + src/esm.ts | 32 ++++--- src/index.ts | 4 + 11 files changed, 287 insertions(+), 48 deletions(-) create mode 100644 child-loader.mjs create mode 100644 src/bin-esm.ts create mode 100644 src/child/child-entrypoint.ts create mode 100644 src/child/child-loader.ts create mode 100644 src/child/child-require.ts create mode 100644 src/child/spawn-child.ts diff --git a/child-loader.mjs b/child-loader.mjs new file mode 100644 index 000000000..3a96eeea4 --- /dev/null +++ b/child-loader.mjs @@ -0,0 +1,7 @@ +import { fileURLToPath } from 'url'; +import { createRequire } from 'module'; +const require = createRequire(fileURLToPath(import.meta.url)); + +/** @type {import('./dist/child-loader')} */ +const childLoader = require('./dist/child/child-loader'); +export const { resolve, load, getFormat, transformSource } = childLoader; diff --git a/package.json b/package.json index 320ee79fc..717834db0 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,8 @@ "./dist/bin-script.js": "./dist/bin-script.js", "./dist/bin-cwd": "./dist/bin-cwd.js", "./dist/bin-cwd.js": "./dist/bin-cwd.js", + "./dist/bin-esm": "./dist/bin-esm.js", + "./dist/bin-esm.js": "./dist/bin-esm.js", "./register": "./register/index.js", "./register/files": "./register/files.js", "./register/transpile-only": "./register/transpile-only.js", @@ -23,6 +25,7 @@ "./esm.mjs": "./esm.mjs", "./esm/transpile-only": "./esm/transpile-only.mjs", "./esm/transpile-only.mjs": "./esm/transpile-only.mjs", + "./child-loader.mjs": "./child-loader.mjs", "./transpilers/swc": "./transpilers/swc.js", "./transpilers/swc-experimental": "./transpilers/swc-experimental.js", "./node10/tsconfig.json": "./node10/tsconfig.json", @@ -36,7 +39,8 @@ "ts-script": "dist/bin-script-deprecated.js", "ts-node-script": "dist/bin-script.js", "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-transpile-only": "dist/bin-transpile.js" + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-node-esm": "dist/bin-esm.js" }, "files": [ "/transpilers/", diff --git a/src/bin-esm.ts b/src/bin-esm.ts new file mode 100644 index 000000000..3bc6bbbd2 --- /dev/null +++ b/src/bin-esm.ts @@ -0,0 +1,5 @@ +#!/usr/bin/env node + +import { main } from './bin'; + +main(undefined, { '--esm': true }); diff --git a/src/bin.ts b/src/bin.ts index 564ddaba3..5934fc798 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -1,9 +1,10 @@ #!/usr/bin/env node import { join, resolve, dirname, parse as parsePath } from 'path'; +import { spawnSync } from 'child_process'; import { inspect } from 'util'; import Module = require('module'); -import arg = require('arg'); +let arg: typeof import('arg'); import { parse, createRequire, hasOwnProperty } from './util'; import { EVAL_FILENAME, @@ -17,17 +18,65 @@ import { STDIN_NAME, REPL_FILENAME, } from './repl'; -import { VERSION, TSError, register, versionGteLt } from './index'; +import { VERSION, TSError, register, versionGteLt, create } from './index'; import type { TSInternal } from './ts-compiler-types'; import { addBuiltinLibsToObject } from '../dist-raw/node-cjs-helpers'; +import { callInChild } from './child/spawn-child'; /** * Main `bin` functionality. + * + * This file is split into a chain of functions (phases), each one adding to a shared state object. + * This is done so that the next function can either be invoked in-process or, if necessary, invoked in a child process. + * + * The functions are intentionally given uncreative names and left in the same order as the original code, to make a + * smaller git diff. */ export function main( argv: string[] = process.argv.slice(2), entrypointArgs: Record = {} ) { + const args = parseArgv(argv, entrypointArgs); + const state: BootstrapState = { + shouldUseChildProcess: false, + isInChildProcess: false, + parseArgvResult: args, + }; + return bootstrap(state); +} + +/** + * @internal + * Describes state of CLI bootstrapping. + * Can be marshalled when necessary to resume bootstrapping in a child process. + */ +export interface BootstrapState { + isInChildProcess: boolean; + shouldUseChildProcess: boolean; + parseArgvResult: ReturnType; + phase2Result?: ReturnType; + phase3Result?: ReturnType; +} + +/** @internal */ +export function bootstrap(state: BootstrapState) { + if(!state.phase2Result) { + state.phase2Result = phase2(state); + if(state.shouldUseChildProcess && !state.isInChildProcess) { + return callInChild(state); + } + } + if(!state.phase3Result) { + state.phase3Result = phase3(state); + if(state.shouldUseChildProcess && !state.isInChildProcess) { + return callInChild(state); + } + } + return phase4(state); +} + +function parseArgv(argv: string[], entrypointArgs: Record) { + arg ??= require('arg'); // HACK: technically, this function is not marked @internal so it's possible // that libraries in the wild are doing `require('ts-node/dist/bin').main({'--transpile-only': true})` // We can mark this function @internal in next major release. @@ -58,6 +107,7 @@ export function main( '--scriptMode': Boolean, '--version': arg.COUNT, '--showConfig': Boolean, + '--esm': Boolean, // Project options. '--cwd': String, @@ -156,7 +206,20 @@ export function main( '--scope': scope = undefined, '--scopeDir': scopeDir = undefined, '--noExperimentalReplAwait': noExperimentalReplAwait, + '--esm': esm, + _: restArgs } = args; + return { + restArgs, + cwdArg, help, scriptMode, cwdMode, version, showConfig, argsRequire, code, print, interactive, files, compiler, + compilerOptions, project, ignoreDiagnostics, ignore, transpileOnly, typeCheck, transpiler, swc, compilerHost, + pretty, skipProject, skipIgnore, preferTsExts, logError, emit, scope, scopeDir, noExperimentalReplAwait, + esm + }; +} + +function phase2(payload: BootstrapState) { + const {help, version, code, interactive, cwdArg, restArgs, esm} = payload.parseArgvResult; if (help) { console.log(` @@ -209,8 +272,8 @@ Options: // Figure out which we are executing: piped stdin, --eval, REPL, and/or entrypoint // This is complicated because node's behavior is complicated // `node -e code -i ./script.js` ignores -e - const executeEval = code != null && !(interactive && args._.length); - const executeEntrypoint = !executeEval && args._.length > 0; + const executeEval = code != null && !(interactive && restArgs.length); + const executeEntrypoint = !executeEval && restArgs.length > 0; const executeRepl = !executeEntrypoint && (interactive || (process.stdin.isTTY && !executeEval)); @@ -218,8 +281,60 @@ Options: const cwd = cwdArg || process.cwd(); /** Unresolved. May point to a symlink, not realpath. May be missing file extension */ - const scriptPath = executeEntrypoint ? resolve(cwd, args._[0]) : undefined; + const scriptPath = executeEntrypoint ? resolve(cwd, restArgs[0]) : undefined; + if(esm) payload.shouldUseChildProcess = true; + return {executeEval, executeEntrypoint, executeRepl, executeStdin, cwd, scriptPath}; +} + +function phase3(payload: BootstrapState) { + const { + emit, files, pretty, transpileOnly, transpiler, noExperimentalReplAwait, typeCheck, swc, compilerHost, ignore, + preferTsExts, logError, scriptMode, cwdMode, project, skipProject, skipIgnore, compiler, ignoreDiagnostics, + compilerOptions, argsRequire, scope, scopeDir + } = payload.parseArgvResult; + const {cwd, scriptPath} = payload.phase2Result!; + + // const configWeAlreadyParsed = getConfig({ + const configWeAlreadyParsed = create({ + cwd, + emit, + files, + pretty, + transpileOnly: transpileOnly ?? transpiler != null ? true : undefined, + experimentalReplAwait: noExperimentalReplAwait ? false : undefined, + typeCheck, + transpiler, + swc, + compilerHost, + ignore, + logError, + projectSearchDir: getProjectSearchDir(cwd, scriptMode, cwdMode, scriptPath), + project, + skipProject, + skipIgnore, + compiler, + ignoreDiagnostics, + compilerOptions, + require: argsRequire, + // readFile: evalAwarePartialHost?.readFile ?? undefined, + // fileExists: evalAwarePartialHost?.fileExists ?? undefined, + readFile: undefined, + fileExists: undefined, + scope, + scopeDir, + // }); + }).options; + + // attach new locals to the payload + if(configWeAlreadyParsed.esm) payload.shouldUseChildProcess = true; + return {configWeAlreadyParsed}; +} + +function phase4(payload: BootstrapState) { + const {version, showConfig, restArgs, code, print} = payload.parseArgvResult; + const {executeEval, cwd, executeStdin, executeRepl, executeEntrypoint, scriptPath} = payload.phase2Result!; + const {configWeAlreadyParsed} = payload.phase3Result!; /** * , [stdin], and [eval] are all essentially virtual files that do not exist on disc and are backed by a REPL * service to handle eval-ing of code. @@ -278,33 +393,9 @@ Options: } // Register the TypeScript compiler instance. - const service = register({ - cwd, - emit, - files, - pretty, - transpileOnly: transpileOnly ?? transpiler != null ? true : undefined, - experimentalReplAwait: noExperimentalReplAwait ? false : undefined, - typeCheck, - transpiler, - swc, - compilerHost, - ignore, - preferTsExts, - logError, - projectSearchDir: getProjectSearchDir(cwd, scriptMode, cwdMode, scriptPath), - project, - skipProject, - skipIgnore, - compiler, - ignoreDiagnostics, - compilerOptions, - require: argsRequire, - readFile: evalAwarePartialHost?.readFile ?? undefined, - fileExists: evalAwarePartialHost?.fileExists ?? undefined, - scope, - scopeDir, - }); + // TODO replace this with a call to `getConfig()` + // const service = register(createFromConfig(configWeAlreadyParsed)); + const service = register(configWeAlreadyParsed); // Bind REPL service to ts-node compiler service (chicken-and-egg problem) replStuff?.repl.setService(service); @@ -352,11 +443,11 @@ Options: // Prepend `ts-node` arguments to CLI for child processes. process.execArgv.unshift( __filename, - ...process.argv.slice(2, process.argv.length - args._.length) + ...process.argv.slice(2, process.argv.length - restArgs.length) ); process.argv = [process.argv[1]] .concat(executeEntrypoint ? ([scriptPath] as string[]) : []) - .concat(args._.slice(executeEntrypoint ? 1 : 0)); + .concat(restArgs.slice(executeEntrypoint ? 1 : 0)); // Execute the main contents (either eval, script or piped). if (executeEntrypoint) { diff --git a/src/child/child-entrypoint.ts b/src/child/child-entrypoint.ts new file mode 100644 index 000000000..3a7ffe5c6 --- /dev/null +++ b/src/child/child-entrypoint.ts @@ -0,0 +1,10 @@ +import {BootstrapState, bootstrap} from '../bin'; + +const environmentVariableName = process.argv[2]; +const base64Payload = process.env[environmentVariableName]!; +delete process.env[environmentVariableName]; +const payload = JSON.parse(Buffer.from(base64Payload, 'base64').toString()) as BootstrapState; +console.dir({payloadSize: base64Payload.length, payload: JSON.stringify(payload)}); +payload.isInChildProcess = true; + +bootstrap(payload); diff --git a/src/child/child-loader.ts b/src/child/child-loader.ts new file mode 100644 index 000000000..10caaecda --- /dev/null +++ b/src/child/child-loader.ts @@ -0,0 +1,31 @@ +// TODO same version check as ESM loader, but export stubs +// Also export a binder function that allows re-binding where the stubs +// delegate. + +import type { NodeLoaderHooksAPI1, NodeLoaderHooksAPI2 } from ".."; +import { filterHooksByAPIVersion } from "../esm"; + +let hooks: NodeLoaderHooksAPI1 & NodeLoaderHooksAPI2; + +/** @internal */ +export function lateBindHooks(_hooks: NodeLoaderHooksAPI1 | NodeLoaderHooksAPI2) { + hooks = _hooks as NodeLoaderHooksAPI1 & NodeLoaderHooksAPI2; +} + +const proxy: NodeLoaderHooksAPI1 & NodeLoaderHooksAPI2 = { + resolve(...args: Parameters) { + return hooks.resolve(...args); + }, + load(...args: Parameters) { + return hooks.load(...args); + }, + getFormat(...args: Parameters) { + return hooks.getFormat(...args); + }, + transformSource(...args: Parameters) { + return hooks.transformSource(...args); + } +} + +/** @internal */ +export const {resolve, load, getFormat, transformSource} = filterHooksByAPIVersion(proxy) as NodeLoaderHooksAPI1 & NodeLoaderHooksAPI2; diff --git a/src/child/child-require.ts b/src/child/child-require.ts new file mode 100644 index 000000000..3fdb0c46f --- /dev/null +++ b/src/child/child-require.ts @@ -0,0 +1,23 @@ +interface EventEmitterInternals { + _events: Record>; +} +const _process = process as any as EventEmitterInternals; + +// Not shown here: Additional logic to correctly interact with process's events, either using this direct manipulation, or via the API + +let originalOnWarning: Function | undefined; +if(Array.isArray(_process._events.warning)) { + originalOnWarning = _process._events.warning[0]; + _process._events.warning[0] = onWarning; +} else { + originalOnWarning = _process._events.warning; + _process._events.warning = onWarning; +} + +const messageMatch = /--(?:experimental-)?loader\b/; +function onWarning(this: any, warning: Error, ...rest: any[]) { + // Suppress warning about how `--loader` is experimental + if(warning?.name === 'ExperimentalWarning' && messageMatch.test(warning?.message)) return; + // Will be undefined if `--no-warnings` + return originalOnWarning?.call(this, warning, ...rest); +} diff --git a/src/child/spawn-child.ts b/src/child/spawn-child.ts new file mode 100644 index 000000000..f616a32cc --- /dev/null +++ b/src/child/spawn-child.ts @@ -0,0 +1,56 @@ +import type { BootstrapState } from "../bin"; +import {spawn} from 'child_process'; +import * as fs from "fs"; + +const passFirstXFds = 100; +const stdio: number[] = []; +for(let i = 0; i < passFirstXFds; i++) { + stdio[i] = i; +} + +/** @internal */ +export function callInChild(state: BootstrapState) { + let envVarName: string = 'TS_NODE_BOOTSTRAP'; + for(let i = 0; i < Number.MAX_SAFE_INTEGER; i++) { + envVarName = `TS_NODE_BOOTSTRAP_${i}`; + if(process.env[envVarName] === undefined) break; + } + const child = spawn(process.execPath, [ + '--require', + require.resolve('./child-require.js'), + '--loader', + require.resolve('../../child-loader.mjs'), + require.resolve('./child-entrypoint.js'), + envVarName + ], { + env: { + ...process.env, + [envVarName!]: Buffer.from(JSON.stringify(state), 'utf8').toString('base64') + }, + stdio, + argv0: process.argv0 + }); + child.on('error', (error) => { + console.error(error); + process.exit(1); + }); + child.on('exit', (code) => { + child.removeAllListeners(); + process.off('SIGINT', onSigInt); + process.off('SIGTERM', onSigTerm); + process.exitCode = code === null ? 1 : code; + }); + // Ignore sigint and sigterm in parent; pass them to child + process.on('SIGINT', onSigInt); + function onSigInt() { + process.kill(child.pid, 'SIGINT'); + } + process.on('SIGTERM', onSigTerm); + function onSigTerm() { + process.kill(child.pid, 'SIGTERM'); + } + // Close all (well, a lot of) FDs in parent to avoid keeping them open. + for(let fd = 0; fd < 100; fd++) { + fs.close(fd, () => {}); + } +} diff --git a/src/configuration.ts b/src/configuration.ts index b536926dd..1626b282a 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -278,6 +278,7 @@ function filterRecognizedTsConfigTsNodeOptions(jsonObject: any): { experimentalReplAwait, swc, experimentalResolverFeatures, + esm, ...unrecognized } = jsonObject as TsConfigOptions; const filteredTsConfigOptions = { @@ -302,6 +303,7 @@ function filterRecognizedTsConfigTsNodeOptions(jsonObject: any): { moduleTypes, swc, experimentalResolverFeatures, + esm, }; // Use the typechecker to make sure this implementation has the correct set of properties const catchExtraneousProps: keyof TsConfigOptions = diff --git a/src/esm.ts b/src/esm.ts index 48dd13f1d..b5bb56488 100644 --- a/src/esm.ts +++ b/src/esm.ts @@ -95,6 +95,24 @@ export interface NodeImportAssertions { type?: 'json'; } +// The hooks API changed in node version X so we need to check for backwards compatibility. +// TODO: When the new API is backported to v12, v14, update these version checks accordingly. +const newHooksAPI = + versionGteLt(process.versions.node, '17.0.0') || + versionGteLt(process.versions.node, '16.12.0', '17.0.0') || + versionGteLt(process.versions.node, '14.999.999', '15.0.0') || + versionGteLt(process.versions.node, '12.999.999', '13.0.0'); + +/** @internal */ +export function filterHooksByAPIVersion(hooks: NodeLoaderHooksAPI1 & NodeLoaderHooksAPI2): NodeLoaderHooksAPI1 | NodeLoaderHooksAPI2 { + const {getFormat, load, resolve, transformSource} = hooks; + // Explicit return type to avoid TS's non-ideal inferred type + const hooksAPI: NodeLoaderHooksAPI1 | NodeLoaderHooksAPI2 = newHooksAPI + ? { resolve, load, getFormat: undefined, transformSource: undefined } + : { resolve, getFormat, transformSource, load: undefined }; + return hooksAPI; +} + /** @internal */ export function registerAndCreateEsmHooks(opts?: RegisterOptions) { // Automatically performs registration just like `-r ts-node/register` @@ -112,19 +130,7 @@ export function createEsmHooks(tsNodeService: Service) { preferTsExts: tsNodeService.options.preferTsExts, }); - // The hooks API changed in node version X so we need to check for backwards compatibility. - // TODO: When the new API is backported to v12, v14, update these version checks accordingly. - const newHooksAPI = - versionGteLt(process.versions.node, '17.0.0') || - versionGteLt(process.versions.node, '16.12.0', '17.0.0') || - versionGteLt(process.versions.node, '14.999.999', '15.0.0') || - versionGteLt(process.versions.node, '12.999.999', '13.0.0'); - - // Explicit return type to avoid TS's non-ideal inferred type - const hooksAPI: NodeLoaderHooksAPI1 | NodeLoaderHooksAPI2 = newHooksAPI - ? { resolve, load, getFormat: undefined, transformSource: undefined } - : { resolve, getFormat, transformSource, load: undefined }; - return hooksAPI; + return filterHooksByAPIVersion({resolve, load, getFormat, transformSource}); function isFileUrlOrNodeStyleSpecifier(parsed: UrlWithStringQuery) { // We only understand file:// URLs, but in node, the specifier can be a node-style `./foo` or `foo` diff --git a/src/index.ts b/src/index.ts index 17aed8998..952e6ce22 100644 --- a/src/index.ts +++ b/src/index.ts @@ -364,6 +364,10 @@ export interface CreateOptions { * @default console.log */ tsTrace?: (str: string) => void; + /** + * TODO DOCS YAY + */ + esm?: boolean; } type ModuleTypes = Record; From a35462b5ca2c509f38e9ea681f9a2523f42ec0c4 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Tue, 22 Feb 2022 19:52:59 +0000 Subject: [PATCH 02/27] lint-fix --- src/bin.ts | 107 +++++++++++++++++++++++++++------- src/child/child-entrypoint.ts | 11 +++- src/child/child-loader.ts | 15 +++-- src/child/child-require.ts | 8 ++- src/child/spawn-child.ts | 50 +++++++++------- src/esm.ts | 19 ++++-- 6 files changed, 149 insertions(+), 61 deletions(-) diff --git a/src/bin.ts b/src/bin.ts index 087885802..b9e6b690c 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -59,15 +59,15 @@ export interface BootstrapState { /** @internal */ export function bootstrap(state: BootstrapState) { - if(!state.phase2Result) { + if (!state.phase2Result) { state.phase2Result = phase2(state); - if(state.shouldUseChildProcess && !state.isInChildProcess) { + if (state.shouldUseChildProcess && !state.isInChildProcess) { return callInChild(state); } } - if(!state.phase3Result) { + if (!state.phase3Result) { state.phase3Result = phase3(state); - if(state.shouldUseChildProcess && !state.isInChildProcess) { + if (state.shouldUseChildProcess && !state.isInChildProcess) { return callInChild(state); } } @@ -206,19 +206,47 @@ function parseArgv(argv: string[], entrypointArgs: Record) { '--scopeDir': scopeDir = undefined, '--noExperimentalReplAwait': noExperimentalReplAwait, '--esm': esm, - _: restArgs + _: restArgs, } = args; return { restArgs, - cwdArg, help, scriptMode, cwdMode, version, showConfig, argsRequire, code, print, interactive, files, compiler, - compilerOptions, project, ignoreDiagnostics, ignore, transpileOnly, typeCheck, transpiler, swc, compilerHost, - pretty, skipProject, skipIgnore, preferTsExts, logError, emit, scope, scopeDir, noExperimentalReplAwait, - esm + cwdArg, + help, + scriptMode, + cwdMode, + version, + showConfig, + argsRequire, + code, + print, + interactive, + files, + compiler, + compilerOptions, + project, + ignoreDiagnostics, + ignore, + transpileOnly, + typeCheck, + transpiler, + swc, + compilerHost, + pretty, + skipProject, + skipIgnore, + preferTsExts, + logError, + emit, + scope, + scopeDir, + noExperimentalReplAwait, + esm, }; } function phase2(payload: BootstrapState) { - const {help, version, code, interactive, cwdArg, restArgs, esm} = payload.parseArgvResult; + const { help, version, code, interactive, cwdArg, restArgs, esm } = + payload.parseArgvResult; if (help) { console.log(` @@ -282,17 +310,44 @@ Options: /** Unresolved. May point to a symlink, not realpath. May be missing file extension */ const scriptPath = executeEntrypoint ? resolve(cwd, restArgs[0]) : undefined; - if(esm) payload.shouldUseChildProcess = true; - return {executeEval, executeEntrypoint, executeRepl, executeStdin, cwd, scriptPath}; + if (esm) payload.shouldUseChildProcess = true; + return { + executeEval, + executeEntrypoint, + executeRepl, + executeStdin, + cwd, + scriptPath, + }; } function phase3(payload: BootstrapState) { const { - emit, files, pretty, transpileOnly, transpiler, noExperimentalReplAwait, typeCheck, swc, compilerHost, ignore, - preferTsExts, logError, scriptMode, cwdMode, project, skipProject, skipIgnore, compiler, ignoreDiagnostics, - compilerOptions, argsRequire, scope, scopeDir + emit, + files, + pretty, + transpileOnly, + transpiler, + noExperimentalReplAwait, + typeCheck, + swc, + compilerHost, + ignore, + preferTsExts, + logError, + scriptMode, + cwdMode, + project, + skipProject, + skipIgnore, + compiler, + ignoreDiagnostics, + compilerOptions, + argsRequire, + scope, + scopeDir, } = payload.parseArgvResult; - const {cwd, scriptPath} = payload.phase2Result!; + const { cwd, scriptPath } = payload.phase2Result!; // const configWeAlreadyParsed = getConfig({ const configWeAlreadyParsed = create({ @@ -322,18 +377,26 @@ function phase3(payload: BootstrapState) { fileExists: undefined, scope, scopeDir, - // }); + // }); }).options; // attach new locals to the payload - if(configWeAlreadyParsed.esm) payload.shouldUseChildProcess = true; - return {configWeAlreadyParsed}; + if (configWeAlreadyParsed.esm) payload.shouldUseChildProcess = true; + return { configWeAlreadyParsed }; } function phase4(payload: BootstrapState) { - const {version, showConfig, restArgs, code, print} = payload.parseArgvResult; - const {executeEval, cwd, executeStdin, executeRepl, executeEntrypoint, scriptPath} = payload.phase2Result!; - const {configWeAlreadyParsed} = payload.phase3Result!; + const { version, showConfig, restArgs, code, print } = + payload.parseArgvResult; + const { + executeEval, + cwd, + executeStdin, + executeRepl, + executeEntrypoint, + scriptPath, + } = payload.phase2Result!; + const { configWeAlreadyParsed } = payload.phase3Result!; /** * , [stdin], and [eval] are all essentially virtual files that do not exist on disc and are backed by a REPL * service to handle eval-ing of code. diff --git a/src/child/child-entrypoint.ts b/src/child/child-entrypoint.ts index 3a7ffe5c6..c342197d4 100644 --- a/src/child/child-entrypoint.ts +++ b/src/child/child-entrypoint.ts @@ -1,10 +1,15 @@ -import {BootstrapState, bootstrap} from '../bin'; +import { BootstrapState, bootstrap } from '../bin'; const environmentVariableName = process.argv[2]; const base64Payload = process.env[environmentVariableName]!; delete process.env[environmentVariableName]; -const payload = JSON.parse(Buffer.from(base64Payload, 'base64').toString()) as BootstrapState; -console.dir({payloadSize: base64Payload.length, payload: JSON.stringify(payload)}); +const payload = JSON.parse( + Buffer.from(base64Payload, 'base64').toString() +) as BootstrapState; +console.dir({ + payloadSize: base64Payload.length, + payload: JSON.stringify(payload), +}); payload.isInChildProcess = true; bootstrap(payload); diff --git a/src/child/child-loader.ts b/src/child/child-loader.ts index 10caaecda..3a177e6ff 100644 --- a/src/child/child-loader.ts +++ b/src/child/child-loader.ts @@ -2,13 +2,15 @@ // Also export a binder function that allows re-binding where the stubs // delegate. -import type { NodeLoaderHooksAPI1, NodeLoaderHooksAPI2 } from ".."; -import { filterHooksByAPIVersion } from "../esm"; +import type { NodeLoaderHooksAPI1, NodeLoaderHooksAPI2 } from '..'; +import { filterHooksByAPIVersion } from '../esm'; let hooks: NodeLoaderHooksAPI1 & NodeLoaderHooksAPI2; /** @internal */ -export function lateBindHooks(_hooks: NodeLoaderHooksAPI1 | NodeLoaderHooksAPI2) { +export function lateBindHooks( + _hooks: NodeLoaderHooksAPI1 | NodeLoaderHooksAPI2 +) { hooks = _hooks as NodeLoaderHooksAPI1 & NodeLoaderHooksAPI2; } @@ -24,8 +26,9 @@ const proxy: NodeLoaderHooksAPI1 & NodeLoaderHooksAPI2 = { }, transformSource(...args: Parameters) { return hooks.transformSource(...args); - } -} + }, +}; /** @internal */ -export const {resolve, load, getFormat, transformSource} = filterHooksByAPIVersion(proxy) as NodeLoaderHooksAPI1 & NodeLoaderHooksAPI2; +export const { resolve, load, getFormat, transformSource } = + filterHooksByAPIVersion(proxy) as NodeLoaderHooksAPI1 & NodeLoaderHooksAPI2; diff --git a/src/child/child-require.ts b/src/child/child-require.ts index 3fdb0c46f..2ee155221 100644 --- a/src/child/child-require.ts +++ b/src/child/child-require.ts @@ -6,7 +6,7 @@ const _process = process as any as EventEmitterInternals; // Not shown here: Additional logic to correctly interact with process's events, either using this direct manipulation, or via the API let originalOnWarning: Function | undefined; -if(Array.isArray(_process._events.warning)) { +if (Array.isArray(_process._events.warning)) { originalOnWarning = _process._events.warning[0]; _process._events.warning[0] = onWarning; } else { @@ -17,7 +17,11 @@ if(Array.isArray(_process._events.warning)) { const messageMatch = /--(?:experimental-)?loader\b/; function onWarning(this: any, warning: Error, ...rest: any[]) { // Suppress warning about how `--loader` is experimental - if(warning?.name === 'ExperimentalWarning' && messageMatch.test(warning?.message)) return; + if ( + warning?.name === 'ExperimentalWarning' && + messageMatch.test(warning?.message) + ) + return; // Will be undefined if `--no-warnings` return originalOnWarning?.call(this, warning, ...rest); } diff --git a/src/child/spawn-child.ts b/src/child/spawn-child.ts index f616a32cc..18e69c858 100644 --- a/src/child/spawn-child.ts +++ b/src/child/spawn-child.ts @@ -1,35 +1,41 @@ -import type { BootstrapState } from "../bin"; -import {spawn} from 'child_process'; -import * as fs from "fs"; +import type { BootstrapState } from '../bin'; +import { spawn } from 'child_process'; +import * as fs from 'fs'; const passFirstXFds = 100; const stdio: number[] = []; -for(let i = 0; i < passFirstXFds; i++) { +for (let i = 0; i < passFirstXFds; i++) { stdio[i] = i; } /** @internal */ export function callInChild(state: BootstrapState) { let envVarName: string = 'TS_NODE_BOOTSTRAP'; - for(let i = 0; i < Number.MAX_SAFE_INTEGER; i++) { + for (let i = 0; i < Number.MAX_SAFE_INTEGER; i++) { envVarName = `TS_NODE_BOOTSTRAP_${i}`; - if(process.env[envVarName] === undefined) break; + if (process.env[envVarName] === undefined) break; } - const child = spawn(process.execPath, [ - '--require', - require.resolve('./child-require.js'), - '--loader', - require.resolve('../../child-loader.mjs'), - require.resolve('./child-entrypoint.js'), - envVarName - ], { - env: { - ...process.env, - [envVarName!]: Buffer.from(JSON.stringify(state), 'utf8').toString('base64') - }, - stdio, - argv0: process.argv0 - }); + const child = spawn( + process.execPath, + [ + '--require', + require.resolve('./child-require.js'), + '--loader', + require.resolve('../../child-loader.mjs'), + require.resolve('./child-entrypoint.js'), + envVarName, + ], + { + env: { + ...process.env, + [envVarName!]: Buffer.from(JSON.stringify(state), 'utf8').toString( + 'base64' + ), + }, + stdio, + argv0: process.argv0, + } + ); child.on('error', (error) => { console.error(error); process.exit(1); @@ -50,7 +56,7 @@ export function callInChild(state: BootstrapState) { process.kill(child.pid, 'SIGTERM'); } // Close all (well, a lot of) FDs in parent to avoid keeping them open. - for(let fd = 0; fd < 100; fd++) { + for (let fd = 0; fd < 100; fd++) { fs.close(fd, () => {}); } } diff --git a/src/esm.ts b/src/esm.ts index 54052851e..38f4f0d58 100644 --- a/src/esm.ts +++ b/src/esm.ts @@ -104,13 +104,15 @@ export interface NodeImportAssertions { // TODO: When the new API is backported to v12, v14, update these version checks accordingly. const newHooksAPI = versionGteLt(process.versions.node, '17.0.0') || - versionGteLt(process.versions.node, '16.12.0', '17.0.0') || - versionGteLt(process.versions.node, '14.999.999', '15.0.0') || - versionGteLt(process.versions.node, '12.999.999', '13.0.0'); + versionGteLt(process.versions.node, '16.12.0', '17.0.0') || + versionGteLt(process.versions.node, '14.999.999', '15.0.0') || + versionGteLt(process.versions.node, '12.999.999', '13.0.0'); /** @internal */ -export function filterHooksByAPIVersion(hooks: NodeLoaderHooksAPI1 & NodeLoaderHooksAPI2): NodeLoaderHooksAPI1 | NodeLoaderHooksAPI2 { - const {getFormat, load, resolve, transformSource} = hooks; +export function filterHooksByAPIVersion( + hooks: NodeLoaderHooksAPI1 & NodeLoaderHooksAPI2 +): NodeLoaderHooksAPI1 | NodeLoaderHooksAPI2 { + const { getFormat, load, resolve, transformSource } = hooks; // Explicit return type to avoid TS's non-ideal inferred type const hooksAPI: NodeLoaderHooksAPI1 | NodeLoaderHooksAPI2 = newHooksAPI ? { resolve, load, getFormat: undefined, transformSource: undefined } @@ -135,7 +137,12 @@ export function createEsmHooks(tsNodeService: Service) { preferTsExts: tsNodeService.options.preferTsExts, }); - const hooksAPI = filterHooksByAPIVersion({resolve, load, getFormat, transformSource}); + const hooksAPI = filterHooksByAPIVersion({ + resolve, + load, + getFormat, + transformSource, + }); function isFileUrlOrNodeStyleSpecifier(parsed: UrlWithStringQuery) { // We only understand file:// URLs, but in node, the specifier can be a node-style `./foo` or `foo` From 48f7e3f0438d55aa81ce1daf6da038ca8d42b13e Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Wed, 23 Feb 2022 01:20:44 -0500 Subject: [PATCH 03/27] WIP --- package.json | 1 + src/bin.ts | 17 ++++++- src/child/child-entrypoint.ts | 11 ++--- src/child/child-loader.ts | 8 +-- src/child/spawn-child.ts | 43 +++++----------- src/test/esm-loader.spec.ts | 49 ++++++++++++++++++- src/test/helpers.ts | 3 ++ tests/esm-child-process/via-flag/index.ts | 17 +++++++ tests/esm-child-process/via-flag/package.json | 3 ++ .../esm-child-process/via-flag/tsconfig.json | 6 +++ tests/esm-child-process/via-tsconfig/index.ts | 3 ++ .../via-tsconfig/package.json | 3 ++ .../via-tsconfig/tsconfig.json | 9 ++++ 13 files changed, 128 insertions(+), 45 deletions(-) create mode 100644 tests/esm-child-process/via-flag/index.ts create mode 100644 tests/esm-child-process/via-flag/package.json create mode 100644 tests/esm-child-process/via-flag/tsconfig.json create mode 100644 tests/esm-child-process/via-tsconfig/index.ts create mode 100644 tests/esm-child-process/via-tsconfig/package.json create mode 100644 tests/esm-child-process/via-tsconfig/tsconfig.json diff --git a/package.json b/package.json index 729d03a73..8b4a12b08 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "/register/", "/esm/", "/esm.mjs", + "/child-loader.mjs", "/LICENSE", "/tsconfig.schema.json", "/tsconfig.schemastore-schema.json", diff --git a/src/bin.ts b/src/bin.ts index 07a7f6395..cd36f929f 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -17,7 +17,14 @@ import { STDIN_NAME, REPL_FILENAME, } from './repl'; -import { VERSION, TSError, register, versionGteLt, create } from './index'; +import { + VERSION, + TSError, + register, + versionGteLt, + create, + createEsmHooks, +} from './index'; import type { TSInternal } from './ts-compiler-types'; import { addBuiltinLibsToObject } from '../dist-raw/node-cjs-helpers'; import { callInChild } from './child/spawn-child'; @@ -386,6 +393,7 @@ function phase3(payload: BootstrapState) { } function phase4(payload: BootstrapState) { + const { isInChildProcess } = payload; const { version, showConfig, restArgs, code, print } = payload.parseArgvResult; const { @@ -458,6 +466,10 @@ function phase4(payload: BootstrapState) { // TODO replace this with a call to `getConfig()` // const service = register(createFromConfig(configWeAlreadyParsed)); const service = register(configWeAlreadyParsed); + if (isInChildProcess) + ( + require('./child/child-loader') as typeof import('./child/child-loader') + ).lateBindHooks(createEsmHooks(service)); // Bind REPL service to ts-node compiler service (chicken-and-egg problem) replStuff?.repl.setService(service); @@ -530,9 +542,12 @@ function phase4(payload: BootstrapState) { // Prepend `ts-node` arguments to CLI for child processes. process.execArgv.push( + // TODO this comes from BoostrapState __filename, + // TODO this comes from BoostrapState ...process.argv.slice(2, process.argv.length - restArgs.length) ); + // TODO this comes from BoostrapState process.argv = [process.argv[1]] .concat(executeEntrypoint ? ([scriptPath] as string[]) : []) .concat(restArgs.slice(executeEntrypoint ? 1 : 0)); diff --git a/src/child/child-entrypoint.ts b/src/child/child-entrypoint.ts index c342197d4..3b0d4ebf7 100644 --- a/src/child/child-entrypoint.ts +++ b/src/child/child-entrypoint.ts @@ -1,15 +1,12 @@ import { BootstrapState, bootstrap } from '../bin'; -const environmentVariableName = process.argv[2]; -const base64Payload = process.env[environmentVariableName]!; -delete process.env[environmentVariableName]; +const base64ConfigArg = process.argv[2]; +const argPrefix = '--base64-config='; +if (!base64ConfigArg.startsWith(argPrefix)) throw new Error('unexpected argv'); +const base64Payload = base64ConfigArg.slice(argPrefix.length); const payload = JSON.parse( Buffer.from(base64Payload, 'base64').toString() ) as BootstrapState; -console.dir({ - payloadSize: base64Payload.length, - payload: JSON.stringify(payload), -}); payload.isInChildProcess = true; bootstrap(payload); diff --git a/src/child/child-loader.ts b/src/child/child-loader.ts index 3a177e6ff..0ac018132 100644 --- a/src/child/child-loader.ts +++ b/src/child/child-loader.ts @@ -16,16 +16,16 @@ export function lateBindHooks( const proxy: NodeLoaderHooksAPI1 & NodeLoaderHooksAPI2 = { resolve(...args: Parameters) { - return hooks.resolve(...args); + return (hooks?.resolve ?? args[2])(...args); }, load(...args: Parameters) { - return hooks.load(...args); + return (hooks?.load ?? args[2])(...args); }, getFormat(...args: Parameters) { - return hooks.getFormat(...args); + return (hooks?.getFormat ?? args[2])(...args); }, transformSource(...args: Parameters) { - return hooks.transformSource(...args); + return (hooks?.transformSource ?? args[2])(...args); }, }; diff --git a/src/child/spawn-child.ts b/src/child/spawn-child.ts index 18e69c858..94d4dcae5 100644 --- a/src/child/spawn-child.ts +++ b/src/child/spawn-child.ts @@ -1,20 +1,10 @@ import type { BootstrapState } from '../bin'; import { spawn } from 'child_process'; -import * as fs from 'fs'; -const passFirstXFds = 100; -const stdio: number[] = []; -for (let i = 0; i < passFirstXFds; i++) { - stdio[i] = i; -} +const argPrefix = '--base64-config='; /** @internal */ export function callInChild(state: BootstrapState) { - let envVarName: string = 'TS_NODE_BOOTSTRAP'; - for (let i = 0; i < Number.MAX_SAFE_INTEGER; i++) { - envVarName = `TS_NODE_BOOTSTRAP_${i}`; - if (process.env[envVarName] === undefined) break; - } const child = spawn( process.execPath, [ @@ -23,16 +13,12 @@ export function callInChild(state: BootstrapState) { '--loader', require.resolve('../../child-loader.mjs'), require.resolve('./child-entrypoint.js'), - envVarName, + `${argPrefix}${Buffer.from(JSON.stringify(state), 'utf8').toString( + 'base64' + )}`, ], { - env: { - ...process.env, - [envVarName!]: Buffer.from(JSON.stringify(state), 'utf8').toString( - 'base64' - ), - }, - stdio, + stdio: 'inherit', argv0: process.argv0, } ); @@ -42,21 +28,14 @@ export function callInChild(state: BootstrapState) { }); child.on('exit', (code) => { child.removeAllListeners(); - process.off('SIGINT', onSigInt); - process.off('SIGTERM', onSigTerm); + process.off('SIGINT', sendSignalToChild); + process.off('SIGTERM', sendSignalToChild); process.exitCode = code === null ? 1 : code; }); // Ignore sigint and sigterm in parent; pass them to child - process.on('SIGINT', onSigInt); - function onSigInt() { - process.kill(child.pid, 'SIGINT'); - } - process.on('SIGTERM', onSigTerm); - function onSigTerm() { - process.kill(child.pid, 'SIGTERM'); - } - // Close all (well, a lot of) FDs in parent to avoid keeping them open. - for (let fd = 0; fd < 100; fd++) { - fs.close(fd, () => {}); + process.on('SIGINT', sendSignalToChild); + process.on('SIGTERM', sendSignalToChild); + function sendSignalToChild(signal: string) { + process.kill(child.pid, signal); } } diff --git a/src/test/esm-loader.spec.ts b/src/test/esm-loader.spec.ts index d4a943798..1099cbef5 100644 --- a/src/test/esm-loader.spec.ts +++ b/src/test/esm-loader.spec.ts @@ -5,10 +5,12 @@ import { context } from './testlib'; import semver = require('semver'); import { + BIN_ESM_PATH, BIN_PATH, CMD_ESM_LOADER_WITHOUT_PROJECT, CMD_TS_NODE_WITHOUT_PROJECT_FLAG, contextTsNodeUnderTest, + delay, EXPERIMENTAL_MODULES_FLAG, nodeSupportsEsmHooks, nodeSupportsImportAssertions, @@ -16,7 +18,7 @@ import { resetNodeEnvironment, TEST_DIR, } from './helpers'; -import { createExec } from './exec-helpers'; +import { createExec, ExecReturn } from './exec-helpers'; import { join, resolve } from 'path'; import * as expect from 'expect'; import type { NodeLoaderHooksAPI2 } from '../'; @@ -301,6 +303,51 @@ test.suite('esm', (test) => { }); } ); + + test.suite('spawns child process', async (test) => { + basic('ts-node-esm executable', () => + exec(`${BIN_ESM_PATH} ./esm-child-process/via-flag/index.ts`) + ); + basic('ts-node --esm flag', () => + exec(`${BIN_PATH} --esm ./esm-child-process/via-flag/index.ts`) + ); + basic('ts-node w/tsconfig esm:true', () => + exec(`${BIN_PATH} --esm ./esm-child-process/via-tsconfig/index.ts`) + ); + + function basic(title: string, cb: () => ExecReturn) { + test(title, async (t) => { + const { err, stdout, stderr } = await cb(); + expect(err).toBe(null); + expect(stdout.trim()).toBe('Hello world!'); + expect(stderr).toBe(''); + }); + } + + test.suite('parent passes signals to child', (test) => { + signalTest('SIGINT'); + signalTest('SIGTERM'); + function signalTest(signal: string) { + test(signal, async (t) => { + const childP = exec( + `${BIN_PATH} ./esm-child-process/via-tsconfig/index.ts sleep` + ); + let code: number | null | undefined = undefined; + childP.child.on('exit', (_code) => (code = _code)); + await delay(2e3); + expect(code).toBeUndefined(); + process.kill(childP.child.pid, 'SIGINT'); + await delay(2e3); + expect(code).toBeUndefined(); + const { stdout, stderr, err } = await childP; + expect(err).toBe(null); + expect(stdout.trim()).toBe('Hello World!'); + expect(stderr).toBe(''); + expect(code).toBe(123); + }); + } + }); + }); }); test.suite('node >= 12.x.x', (test) => { diff --git a/src/test/helpers.ts b/src/test/helpers.ts index 5327459be..6ecadcc64 100644 --- a/src/test/helpers.ts +++ b/src/test/helpers.ts @@ -35,6 +35,7 @@ export const BIN_SCRIPT_PATH = join( 'node_modules/.bin/ts-node-script' ); export const BIN_CWD_PATH = join(TEST_DIR, 'node_modules/.bin/ts-node-cwd'); +export const BIN_ESM_PATH = join(TEST_DIR, 'node_modules/.bin/ts-node-esm'); /** Default `ts-node --project` invocation */ export const CMD_TS_NODE_WITH_PROJECT_FLAG = `"${BIN_PATH}" --project "${PROJECT}"`; /** Default `ts-node` invocation without `--project` */ @@ -224,3 +225,5 @@ function resetObject( // Reset descriptors Object.defineProperties(object, state.descriptors); } + +export const delay = promisify(setTimeout); diff --git a/tests/esm-child-process/via-flag/index.ts b/tests/esm-child-process/via-flag/index.ts new file mode 100644 index 000000000..4f154afea --- /dev/null +++ b/tests/esm-child-process/via-flag/index.ts @@ -0,0 +1,17 @@ +import assert from 'assert'; +assert(import.meta.url.includes('index.ts')); +console.log('Hello world!'); +console.dir(process.argv); +if (process.argv[2] === 'sleep') { + setTimeout(function () { + console.log('Slept 30 seconds'); + }, 30e3); + process.on('SIGTERM', onSignal); + process.on('SIGINT', onSignal); +} +function onSignal(signal: string) { + console.log(`child received signal: ${signal}`); + setTimeout(() => { + process.exit(123); + }, 5e3); +} diff --git a/tests/esm-child-process/via-flag/package.json b/tests/esm-child-process/via-flag/package.json new file mode 100644 index 000000000..3dbc1ca59 --- /dev/null +++ b/tests/esm-child-process/via-flag/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/tests/esm-child-process/via-flag/tsconfig.json b/tests/esm-child-process/via-flag/tsconfig.json new file mode 100644 index 000000000..217b77096 --- /dev/null +++ b/tests/esm-child-process/via-flag/tsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "module": "ESNext", + "esModuleInterop": true + } +} diff --git a/tests/esm-child-process/via-tsconfig/index.ts b/tests/esm-child-process/via-tsconfig/index.ts new file mode 100644 index 000000000..3eb5a24e7 --- /dev/null +++ b/tests/esm-child-process/via-tsconfig/index.ts @@ -0,0 +1,3 @@ +import assert from 'assert'; +assert(import.meta.url.includes('index.ts')); +console.log('Hello world!'); diff --git a/tests/esm-child-process/via-tsconfig/package.json b/tests/esm-child-process/via-tsconfig/package.json new file mode 100644 index 000000000..3dbc1ca59 --- /dev/null +++ b/tests/esm-child-process/via-tsconfig/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/tests/esm-child-process/via-tsconfig/tsconfig.json b/tests/esm-child-process/via-tsconfig/tsconfig.json new file mode 100644 index 000000000..3757044e9 --- /dev/null +++ b/tests/esm-child-process/via-tsconfig/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "module": "ESNext", + "esModuleInterop": true + }, + "ts-node": { + "esm": true + } +} From fbb496cf2adbee055e79bab2c25f2d602954adc3 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Thu, 24 Feb 2022 02:40:11 -0500 Subject: [PATCH 04/27] it works! --- src/bin.ts | 47 ++++++++++------- src/child/child-entrypoint.ts | 8 ++- src/child/spawn-child.ts | 9 ++-- src/configuration.ts | 96 ++++++++++++++++++++++++++++++++++- src/index.ts | 88 +++++++++++--------------------- src/transpilers/swc.ts | 3 +- src/transpilers/types.ts | 6 +++ 7 files changed, 172 insertions(+), 85 deletions(-) diff --git a/src/bin.ts b/src/bin.ts index cd36f929f..65393e5ed 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -22,12 +22,14 @@ import { TSError, register, versionGteLt, - create, createEsmHooks, + createFromPreloadedConfig, + DEFAULTS, } from './index'; import type { TSInternal } from './ts-compiler-types'; import { addBuiltinLibsToObject } from '../dist-raw/node-cjs-helpers'; import { callInChild } from './child/spawn-child'; +import { findAndReadConfig } from './configuration'; /** * Main `bin` functionality. @@ -46,6 +48,7 @@ export function main( const state: BootstrapState = { shouldUseChildProcess: false, isInChildProcess: false, + entrypoint: __filename, parseArgvResult: args, }; return bootstrap(state); @@ -59,6 +62,7 @@ export function main( export interface BootstrapState { isInChildProcess: boolean; shouldUseChildProcess: boolean; + entrypoint: string; parseArgvResult: ReturnType; phase2Result?: ReturnType; phase3Result?: ReturnType; @@ -216,7 +220,10 @@ function parseArgv(argv: string[], entrypointArgs: Record) { _: restArgs, } = args; return { + // Note: argv and restArgs may be overwritten by child process + argv: process.argv, restArgs, + cwdArg, help, scriptMode, @@ -356,8 +363,7 @@ function phase3(payload: BootstrapState) { } = payload.parseArgvResult; const { cwd, scriptPath } = payload.phase2Result!; - // const configWeAlreadyParsed = getConfig({ - const configWeAlreadyParsed = create({ + const preloadedConfig = findAndReadConfig({ cwd, emit, files, @@ -384,17 +390,15 @@ function phase3(payload: BootstrapState) { fileExists: undefined, scope, scopeDir, - // }); - }).options; + }); - // attach new locals to the payload - if (configWeAlreadyParsed.esm) payload.shouldUseChildProcess = true; - return { configWeAlreadyParsed }; + if (preloadedConfig.options.esm) payload.shouldUseChildProcess = true; + return { preloadedConfig }; } function phase4(payload: BootstrapState) { - const { isInChildProcess } = payload; - const { version, showConfig, restArgs, code, print } = + const { isInChildProcess, entrypoint } = payload; + const { version, showConfig, restArgs, code, print, argv } = payload.parseArgvResult; const { executeEval, @@ -404,7 +408,7 @@ function phase4(payload: BootstrapState) { executeEntrypoint, scriptPath, } = payload.phase2Result!; - const { configWeAlreadyParsed } = payload.phase3Result!; + const { preloadedConfig } = payload.phase3Result!; /** * , [stdin], and [eval] are all essentially virtual files that do not exist on disc and are backed by a REPL * service to handle eval-ing of code. @@ -463,9 +467,18 @@ function phase4(payload: BootstrapState) { } // Register the TypeScript compiler instance. - // TODO replace this with a call to `getConfig()` - // const service = register(createFromConfig(configWeAlreadyParsed)); - const service = register(configWeAlreadyParsed); + const service = createFromPreloadedConfig({ + // Since this struct may have been marshalled across thread or process boundaries, we must restore + // un-marshall-able values. + ...preloadedConfig, + options: { + ...preloadedConfig.options, + readFile: evalAwarePartialHost?.readFile ?? undefined, + fileExists: evalAwarePartialHost?.fileExists ?? undefined, + tsTrace: DEFAULTS.tsTrace, + }, + }); + register(service); if (isInChildProcess) ( require('./child/child-loader') as typeof import('./child/child-loader') @@ -542,10 +555,8 @@ function phase4(payload: BootstrapState) { // Prepend `ts-node` arguments to CLI for child processes. process.execArgv.push( - // TODO this comes from BoostrapState - __filename, - // TODO this comes from BoostrapState - ...process.argv.slice(2, process.argv.length - restArgs.length) + entrypoint, + ...argv.slice(2, argv.length - restArgs.length) ); // TODO this comes from BoostrapState process.argv = [process.argv[1]] diff --git a/src/child/child-entrypoint.ts b/src/child/child-entrypoint.ts index 3b0d4ebf7..03a02d2e9 100644 --- a/src/child/child-entrypoint.ts +++ b/src/child/child-entrypoint.ts @@ -1,12 +1,16 @@ import { BootstrapState, bootstrap } from '../bin'; +import { brotliDecompressSync } from 'zlib'; const base64ConfigArg = process.argv[2]; -const argPrefix = '--base64-config='; +const argPrefix = '--brotli-base64-config='; if (!base64ConfigArg.startsWith(argPrefix)) throw new Error('unexpected argv'); const base64Payload = base64ConfigArg.slice(argPrefix.length); const payload = JSON.parse( - Buffer.from(base64Payload, 'base64').toString() + brotliDecompressSync(Buffer.from(base64Payload, 'base64')).toString() ) as BootstrapState; payload.isInChildProcess = true; +payload.entrypoint = __filename; +payload.parseArgvResult.argv = process.argv; +payload.parseArgvResult.restArgs = process.argv.slice(3); bootstrap(payload); diff --git a/src/child/spawn-child.ts b/src/child/spawn-child.ts index 94d4dcae5..45c5f0a71 100644 --- a/src/child/spawn-child.ts +++ b/src/child/spawn-child.ts @@ -1,7 +1,8 @@ import type { BootstrapState } from '../bin'; import { spawn } from 'child_process'; +import { brotliCompressSync } from 'zlib'; -const argPrefix = '--base64-config='; +const argPrefix = '--brotli-base64-config='; /** @internal */ export function callInChild(state: BootstrapState) { @@ -13,9 +14,9 @@ export function callInChild(state: BootstrapState) { '--loader', require.resolve('../../child-loader.mjs'), require.resolve('./child-entrypoint.js'), - `${argPrefix}${Buffer.from(JSON.stringify(state), 'utf8').toString( - 'base64' - )}`, + `${argPrefix}${brotliCompressSync( + Buffer.from(JSON.stringify(state), 'utf8') + ).toString('base64')}`, ], { stdio: 'inherit', diff --git a/src/configuration.ts b/src/configuration.ts index ef1d22e13..5d23b0a26 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -4,13 +4,19 @@ import { CreateOptions, DEFAULTS, OptionBasePaths, + RegisterOptions, TSCommon, TsConfigOptions, } from './index'; import type { TSInternal } from './ts-compiler-types'; import { createTsInternals } from './ts-internals'; import { getDefaultTsconfigJsonForNodeVersion } from './tsconfigs'; -import { assign, createProjectLocalResolveHelper } from './util'; +import { + assign, + attemptRequireWithV8CompileCache, + createProjectLocalResolveHelper, + getBasePathForProjectLocalDependencyResolution, +} from './util'; /** * TypeScript compiler option values required by `ts-node` which cannot be overridden. @@ -49,6 +55,65 @@ function fixConfig(ts: TSCommon, config: _ts.ParsedCommandLine) { return config; } +/** @internal */ +export function findAndReadConfig(rawOptions: CreateOptions) { + const cwd = resolve( + rawOptions.cwd ?? rawOptions.dir ?? DEFAULTS.cwd ?? process.cwd() + ); + const compilerName = rawOptions.compiler ?? DEFAULTS.compiler; + + // Compute minimum options to read the config file. + let projectLocalResolveDir = getBasePathForProjectLocalDependencyResolution( + undefined, + rawOptions.projectSearchDir, + rawOptions.project, + cwd + ); + let { compiler, ts } = resolveAndLoadCompiler( + compilerName, + projectLocalResolveDir + ); + + // Read config file and merge new options between env and CLI options. + const { configFilePath, config, tsNodeOptionsFromTsconfig, optionBasePaths } = + readConfig(cwd, ts, rawOptions); + + const options = assign( + {}, + DEFAULTS, + tsNodeOptionsFromTsconfig || {}, + { optionBasePaths }, + rawOptions + ); + options.require = [ + ...(tsNodeOptionsFromTsconfig.require || []), + ...(rawOptions.require || []), + ]; + + // Re-resolve the compiler in case it has changed. + // Compiler is loaded relative to tsconfig.json, so tsconfig discovery may cause us to load a + // different compiler than we did above, even if the name has not changed. + if (configFilePath) { + projectLocalResolveDir = getBasePathForProjectLocalDependencyResolution( + configFilePath, + rawOptions.projectSearchDir, + rawOptions.project, + cwd + ); + ({ compiler } = resolveCompiler(options.compiler, projectLocalResolveDir)); + } + + return { + options, + config, + projectLocalResolveDir, + optionBasePaths, + configFilePath, + cwd, + compiler, + }; +} + /** * Load TypeScript configuration. Returns the parsed TypeScript config and * any `ts-node` options specified in the config file. @@ -255,6 +320,35 @@ export function readConfig( }; } +/** + * Load the typescript compiler. It is required to load the tsconfig but might + * be changed by the tsconfig, so we have to do this twice. + * @internal + */ +export function resolveAndLoadCompiler( + name: string | undefined, + relativeToPath: string +) { + const { compiler, projectLocalResolveHelper } = resolveCompiler( + name, + relativeToPath + ); + const ts = loadCompiler(compiler); + return { compiler, ts, projectLocalResolveHelper }; +} + +function resolveCompiler(name: string | undefined, relativeToPath: string) { + const projectLocalResolveHelper = + createProjectLocalResolveHelper(relativeToPath); + const compiler = projectLocalResolveHelper(name || 'typescript', true); + return { projectLocalResolveHelper, compiler }; +} + +/** @internal */ +export function loadCompiler(compiler: string): TSCommon { + return attemptRequireWithV8CompileCache(require, compiler); +} + /** * Given the raw "ts-node" sub-object from a tsconfig, return an object with only the properties * recognized by "ts-node" diff --git a/src/index.ts b/src/index.ts index 60592f3e1..245442562 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import { relative, basename, extname, resolve, dirname, join } from 'path'; +import { relative, basename, extname, dirname, join } from 'path'; import { Module } from 'module'; import * as util from 'util'; import { fileURLToPath } from 'url'; @@ -9,18 +9,15 @@ import type * as _ts from 'typescript'; import type { Transpiler, TranspilerFactory } from './transpilers/types'; import { - assign, - attemptRequireWithV8CompileCache, cachedLookup, createProjectLocalResolveHelper, - getBasePathForProjectLocalDependencyResolution, normalizeSlashes, parse, ProjectLocalResolveHelper, split, yn, } from './util'; -import { readConfig } from './configuration'; +import { findAndReadConfig, loadCompiler } from './configuration'; import type { TSCommon, TSInternal } from './ts-compiler-types'; import { createModuleTypeClassifier, @@ -596,63 +593,29 @@ export function register( * Create TypeScript compiler instance. */ export function create(rawOptions: CreateOptions = {}): Service { - const cwd = resolve( - rawOptions.cwd ?? rawOptions.dir ?? DEFAULTS.cwd ?? process.cwd() - ); - const compilerName = rawOptions.compiler ?? DEFAULTS.compiler; - - /** - * Load the typescript compiler. It is required to load the tsconfig but might - * be changed by the tsconfig, so we have to do this twice. - */ - function loadCompiler(name: string | undefined, relativeToPath: string) { - const projectLocalResolveHelper = - createProjectLocalResolveHelper(relativeToPath); - const compiler = projectLocalResolveHelper(name || 'typescript', true); - const ts: TSCommon = attemptRequireWithV8CompileCache(require, compiler); - return { compiler, ts, projectLocalResolveHelper }; - } + const foundConfigResult = findAndReadConfig(rawOptions); + return createFromPreloadedConfig(foundConfigResult); +} - // Compute minimum options to read the config file. - let { compiler, ts, projectLocalResolveHelper } = loadCompiler( - compilerName, - getBasePathForProjectLocalDependencyResolution( - undefined, - rawOptions.projectSearchDir, - rawOptions.project, - cwd - ) - ); +/** @internal */ +export function createFromPreloadedConfig( + foundConfigResult: ReturnType +): Service { + const { + configFilePath, + cwd, + options, + config, + compiler, + projectLocalResolveDir, + optionBasePaths, + } = foundConfigResult; - // Read config file and merge new options between env and CLI options. - const { configFilePath, config, tsNodeOptionsFromTsconfig, optionBasePaths } = - readConfig(cwd, ts, rawOptions); - const options = assign( - {}, - DEFAULTS, - tsNodeOptionsFromTsconfig || {}, - { optionBasePaths }, - rawOptions + const projectLocalResolveHelper = createProjectLocalResolveHelper( + projectLocalResolveDir ); - options.require = [ - ...(tsNodeOptionsFromTsconfig.require || []), - ...(rawOptions.require || []), - ]; - // Re-load the compiler in case it has changed. - // Compiler is loaded relative to tsconfig.json, so tsconfig discovery may cause us to load a - // different compiler than we did above, even if the name has not changed. - if (configFilePath) { - ({ compiler, ts, projectLocalResolveHelper } = loadCompiler( - options.compiler, - getBasePathForProjectLocalDependencyResolution( - configFilePath, - rawOptions.projectSearchDir, - rawOptions.project, - cwd - ) - )); - } + const ts = loadCompiler(compiler); // Experimental REPL await is not compatible targets lower than ES2018 const targetSupportsTla = config.options.target! >= ts.ScriptTarget.ES2018; @@ -767,7 +730,13 @@ export function create(rawOptions: CreateOptions = {}): Service { typeof transpiler === 'string' ? transpiler : transpiler[0]; const transpilerOptions = typeof transpiler === 'string' ? {} : transpiler[1] ?? {}; - const transpilerPath = projectLocalResolveHelper(transpilerName, true); + const transpilerConfigLocalResolveHelper = optionBasePaths.transpiler + ? createProjectLocalResolveHelper(optionBasePaths.transpiler!) + : projectLocalResolveHelper; + const transpilerPath = transpilerConfigLocalResolveHelper( + transpilerName, + true + ); const transpilerFactory = require(transpilerPath) .create as TranspilerFactory; createTranspiler = function (compilerOptions) { @@ -780,6 +749,7 @@ export function create(rawOptions: CreateOptions = {}): Service { }, projectLocalResolveHelper, }, + transpilerConfigLocalResolveHelper, ...transpilerOptions, }); }; diff --git a/src/transpilers/swc.ts b/src/transpilers/swc.ts index 23949595d..07cf49801 100644 --- a/src/transpilers/swc.ts +++ b/src/transpilers/swc.ts @@ -16,12 +16,13 @@ export function create(createOptions: SwcTranspilerOptions): Transpiler { const { swc, service: { config, projectLocalResolveHelper }, + transpilerConfigLocalResolveHelper, } = createOptions; // Load swc compiler let swcInstance: typeof swcWasm; if (typeof swc === 'string') { - swcInstance = require(projectLocalResolveHelper( + swcInstance = require(transpilerConfigLocalResolveHelper( swc, true )) as typeof swcWasm; diff --git a/src/transpilers/types.ts b/src/transpilers/types.ts index ab524cbdc..f5eeff5bd 100644 --- a/src/transpilers/types.ts +++ b/src/transpilers/types.ts @@ -1,5 +1,6 @@ import type * as ts from 'typescript'; import type { Service } from '../index'; +import type { ProjectLocalResolveHelper } from '../util'; /** * Third-party transpilers are implemented as a CommonJS module with a @@ -21,6 +22,11 @@ export interface CreateTranspilerOptions { Service, Extract<'config' | 'options' | 'projectLocalResolveHelper', keyof Service> >; + /** + * If `"transpiler"` option is declared in an "extends" tsconfig, this path might be different than + * the `projectLocalResolveHelper` + */ + transpilerConfigLocalResolveHelper: ProjectLocalResolveHelper; } export interface Transpiler { // TODOs From 2ab1af32057a9e35fed6ca84c6845e8ed7b6871b Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Thu, 24 Feb 2022 02:57:46 -0500 Subject: [PATCH 05/27] Update index.ts --- tests/esm-child-process/via-flag/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/esm-child-process/via-flag/index.ts b/tests/esm-child-process/via-flag/index.ts index 4f154afea..4db9b6f38 100644 --- a/tests/esm-child-process/via-flag/index.ts +++ b/tests/esm-child-process/via-flag/index.ts @@ -1,7 +1,6 @@ import assert from 'assert'; assert(import.meta.url.includes('index.ts')); console.log('Hello world!'); -console.dir(process.argv); if (process.argv[2] === 'sleep') { setTimeout(function () { console.log('Slept 30 seconds'); From 3ea54b605c1d13be15d6891af111cfae74da7e6e Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Thu, 24 Feb 2022 13:07:37 -0500 Subject: [PATCH 06/27] Move `preferTsExts` from `RegisterOptions` to `CreateOptions` --- src/bin.ts | 1 + src/index.ts | 17 ++++++++--------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/bin.ts b/src/bin.ts index 65393e5ed..6ec01fc31 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -390,6 +390,7 @@ function phase3(payload: BootstrapState) { fileExists: undefined, scope, scopeDir, + preferTsExts, }); if (preloadedConfig.options.esm) payload.shouldUseChildProcess = true; diff --git a/src/index.ts b/src/index.ts index 245442562..e8ca57b8d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -370,6 +370,14 @@ export interface CreateOptions { * TODO DOCS YAY */ esm?: boolean; + /** + * Re-order file extensions so that TypeScript imports are preferred. + * + * For example, when both `index.js` and `index.ts` exist, enabling this option causes `require('./index')` to resolve to `index.ts` instead of `index.js` + * + * @default false + */ + preferTsExts?: boolean; } export type ModuleTypes = Record; @@ -385,15 +393,6 @@ export interface OptionBasePaths { * Options for registering a TypeScript compiler instance globally. */ export interface RegisterOptions extends CreateOptions { - /** - * Re-order file extensions so that TypeScript imports are preferred. - * - * For example, when both `index.js` and `index.ts` exist, enabling this option causes `require('./index')` to resolve to `index.ts` instead of `index.js` - * - * @default false - */ - preferTsExts?: boolean; - /** * Enable experimental features that re-map imports and require calls to support: * `baseUrl`, `paths`, `rootDirs`, `.js` to `.ts` file extension mappings, From 490078ac4a42441e6e166e1b57b059135cae28eb Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Thu, 24 Feb 2022 14:06:53 -0500 Subject: [PATCH 07/27] fix --- src/test/esm-loader.spec.ts | 23 +++++--- src/test/exec-helpers.ts | 59 ++++++++++++++++++- tests/esm-child-process/via-flag/index.ts | 13 ---- tests/esm-child-process/via-tsconfig/sleep.ts | 11 ++++ 4 files changed, 82 insertions(+), 24 deletions(-) create mode 100644 tests/esm-child-process/via-tsconfig/sleep.ts diff --git a/src/test/esm-loader.spec.ts b/src/test/esm-loader.spec.ts index 1099cbef5..5f30353fc 100644 --- a/src/test/esm-loader.spec.ts +++ b/src/test/esm-loader.spec.ts @@ -18,7 +18,7 @@ import { resetNodeEnvironment, TEST_DIR, } from './helpers'; -import { createExec, ExecReturn } from './exec-helpers'; +import { createExec, createSpawn, ExecReturn } from './exec-helpers'; import { join, resolve } from 'path'; import * as expect from 'expect'; import type { NodeLoaderHooksAPI2 } from '../'; @@ -29,6 +29,9 @@ const test = context(contextTsNodeUnderTest); const exec = createExec({ cwd: TEST_DIR, }); +const spawn = createSpawn({ + cwd: TEST_DIR, +}); test.suite('esm', (test) => { test.suite('when node supports loader hooks', (test) => { @@ -329,20 +332,22 @@ test.suite('esm', (test) => { signalTest('SIGTERM'); function signalTest(signal: string) { test(signal, async (t) => { - const childP = exec( - `${BIN_PATH} ./esm-child-process/via-tsconfig/index.ts sleep` - ); + const childP = spawn([ + BIN_PATH, + `./esm-child-process/via-tsconfig/sleep.ts`, + ]); let code: number | null | undefined = undefined; childP.child.on('exit', (_code) => (code = _code)); await delay(2e3); expect(code).toBeUndefined(); - process.kill(childP.child.pid, 'SIGINT'); + process.kill(childP.child.pid, signal); await delay(2e3); expect(code).toBeUndefined(); - const { stdout, stderr, err } = await childP; - expect(err).toBe(null); - expect(stdout.trim()).toBe('Hello World!'); - expect(stderr).toBe(''); + const { stdoutP, stderrP } = await childP; + expect((await stdoutP).trim()).toBe( + `child received signal: ${signal}` + ); + expect(await stderrP).toBe(''); expect(code).toBe(123); }); } diff --git a/src/test/exec-helpers.ts b/src/test/exec-helpers.ts index fc70f0e3f..b95488c32 100644 --- a/src/test/exec-helpers.ts +++ b/src/test/exec-helpers.ts @@ -1,5 +1,14 @@ -import type { ChildProcess, ExecException, ExecOptions } from 'child_process'; -import { exec as childProcessExec } from 'child_process'; +import type { + ChildProcess, + ExecException, + ExecOptions, + SpawnOptions, +} from 'child_process'; +import { + exec as childProcessExec, + spawn as childProcessSpawn, +} from 'child_process'; +import { getStream } from './helpers'; import { expect } from './testlib'; export type ExecReturn = Promise & { child: ChildProcess }; @@ -44,6 +53,52 @@ export function createExec>( }; } +export type SpawnReturn = Promise & { child: ChildProcess }; +export interface SpawnResult { + stdoutP: Promise; + stderrP: Promise; + code: number | null; + child: ChildProcess; +} + +export function createSpawn>( + preBoundOptions?: T +) { + /** + * Helper to spawn a child process. + * Returns a Promise and a reference to the child process to suite multiple situations. + * + * Should almost always avoid this helper, and instead use `createExec` / `exec`. `spawn` + * may be necessary if you need to avoid `exec`'s intermediate shell. + */ + return function spawn( + cmd: string[], + opts?: Pick> & + Partial> + ) { + let child!: ChildProcess; + return Object.assign( + new Promise((resolve, reject) => { + child = childProcessSpawn(cmd[0], cmd.slice(1), { + ...preBoundOptions, + ...opts, + }); + const stdoutP = getStream(child.stdout!); + const stderrP = getStream(child.stderr!); + child.on('exit', (code) => { + resolve({ stdoutP, stderrP, code, child }); + }); + child.on('error', (error) => { + reject(error); + }); + }), + { + child, + } + ); + }; +} + const defaultExec = createExec(); export interface ExecTesterOptions { diff --git a/tests/esm-child-process/via-flag/index.ts b/tests/esm-child-process/via-flag/index.ts index 4db9b6f38..3eb5a24e7 100644 --- a/tests/esm-child-process/via-flag/index.ts +++ b/tests/esm-child-process/via-flag/index.ts @@ -1,16 +1,3 @@ import assert from 'assert'; assert(import.meta.url.includes('index.ts')); console.log('Hello world!'); -if (process.argv[2] === 'sleep') { - setTimeout(function () { - console.log('Slept 30 seconds'); - }, 30e3); - process.on('SIGTERM', onSignal); - process.on('SIGINT', onSignal); -} -function onSignal(signal: string) { - console.log(`child received signal: ${signal}`); - setTimeout(() => { - process.exit(123); - }, 5e3); -} diff --git a/tests/esm-child-process/via-tsconfig/sleep.ts b/tests/esm-child-process/via-tsconfig/sleep.ts new file mode 100644 index 000000000..dd2a786e1 --- /dev/null +++ b/tests/esm-child-process/via-tsconfig/sleep.ts @@ -0,0 +1,11 @@ +setTimeout(function () { + console.log('Slept 30 seconds'); +}, 30e3); +process.on('SIGTERM', onSignal); +process.on('SIGINT', onSignal); +function onSignal(signal: string) { + console.log(`child received signal: ${signal}`); + setTimeout(() => { + process.exit(123); + }, 5e3); +} From 58a9e24da6e942657c754d1514860f0528282ec6 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Thu, 24 Feb 2022 14:16:30 -0500 Subject: [PATCH 08/27] fix --- src/child/spawn-child.ts | 1 + src/test/esm-loader.spec.ts | 10 ++++++---- tests/esm-child-process/via-flag/index.ts | 2 +- tests/esm-child-process/via-tsconfig/index.ts | 2 +- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/child/spawn-child.ts b/src/child/spawn-child.ts index 45c5f0a71..651037826 100644 --- a/src/child/spawn-child.ts +++ b/src/child/spawn-child.ts @@ -17,6 +17,7 @@ export function callInChild(state: BootstrapState) { `${argPrefix}${brotliCompressSync( Buffer.from(JSON.stringify(state), 'utf8') ).toString('base64')}`, + ...state.parseArgvResult.restArgs, ], { stdio: 'inherit', diff --git a/src/test/esm-loader.spec.ts b/src/test/esm-loader.spec.ts index 5f30353fc..7f354f9bb 100644 --- a/src/test/esm-loader.spec.ts +++ b/src/test/esm-loader.spec.ts @@ -309,20 +309,22 @@ test.suite('esm', (test) => { test.suite('spawns child process', async (test) => { basic('ts-node-esm executable', () => - exec(`${BIN_ESM_PATH} ./esm-child-process/via-flag/index.ts`) + exec(`${BIN_ESM_PATH} ./esm-child-process/via-flag/index.ts foo bar`) ); basic('ts-node --esm flag', () => - exec(`${BIN_PATH} --esm ./esm-child-process/via-flag/index.ts`) + exec(`${BIN_PATH} --esm ./esm-child-process/via-flag/index.ts foo bar`) ); basic('ts-node w/tsconfig esm:true', () => - exec(`${BIN_PATH} --esm ./esm-child-process/via-tsconfig/index.ts`) + exec( + `${BIN_PATH} --esm ./esm-child-process/via-tsconfig/index.ts foo bar` + ) ); function basic(title: string, cb: () => ExecReturn) { test(title, async (t) => { const { err, stdout, stderr } = await cb(); expect(err).toBe(null); - expect(stdout.trim()).toBe('Hello world!'); + expect(stdout.trim()).toBe('CLI args: foo bar'); expect(stderr).toBe(''); }); } diff --git a/tests/esm-child-process/via-flag/index.ts b/tests/esm-child-process/via-flag/index.ts index 3eb5a24e7..8f1906c2c 100644 --- a/tests/esm-child-process/via-flag/index.ts +++ b/tests/esm-child-process/via-flag/index.ts @@ -1,3 +1,3 @@ import assert from 'assert'; assert(import.meta.url.includes('index.ts')); -console.log('Hello world!'); +console.log(`CLI args: ${process.argv.slice(2).join(' ')}`); diff --git a/tests/esm-child-process/via-tsconfig/index.ts b/tests/esm-child-process/via-tsconfig/index.ts index 3eb5a24e7..8f1906c2c 100644 --- a/tests/esm-child-process/via-tsconfig/index.ts +++ b/tests/esm-child-process/via-tsconfig/index.ts @@ -1,3 +1,3 @@ import assert from 'assert'; assert(import.meta.url.includes('index.ts')); -console.log('Hello world!'); +console.log(`CLI args: ${process.argv.slice(2).join(' ')}`); From b1520d8ddad1259780ba7d02d36e53158d5b2786 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Thu, 24 Feb 2022 23:23:19 -0500 Subject: [PATCH 09/27] fix tests --- src/test/esm-loader.spec.ts | 22 ++++++++++++++----- tests/esm-child-process/via-flag/index.ts | 4 ++-- .../esm-child-process/via-flag/tsconfig.json | 3 +++ tests/esm-child-process/via-tsconfig/index.ts | 4 ++-- .../via-tsconfig/tsconfig.json | 3 ++- 5 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/test/esm-loader.spec.ts b/src/test/esm-loader.spec.ts index 7f354f9bb..e84c26c1f 100644 --- a/src/test/esm-loader.spec.ts +++ b/src/test/esm-loader.spec.ts @@ -330,6 +330,7 @@ test.suite('esm', (test) => { } test.suite('parent passes signals to child', (test) => { + test.runSerially(); signalTest('SIGINT'); signalTest('SIGTERM'); function signalTest(signal: string) { @@ -341,15 +342,24 @@ test.suite('esm', (test) => { let code: number | null | undefined = undefined; childP.child.on('exit', (_code) => (code = _code)); await delay(2e3); - expect(code).toBeUndefined(); + const codeAfter2Seconds = code; process.kill(childP.child.pid, signal); await delay(2e3); - expect(code).toBeUndefined(); + const codeAfter4Seconds = code; const { stdoutP, stderrP } = await childP; - expect((await stdoutP).trim()).toBe( - `child received signal: ${signal}` - ); - expect(await stderrP).toBe(''); + const stdout = await stdoutP; + const stderr = await stderrP; + t.log({ + stdout, + stderr, + codeAfter2Seconds, + codeAfter4Seconds, + code, + }); + expect(codeAfter2Seconds).toBeUndefined(); + expect(codeAfter4Seconds).toBeUndefined(); + expect(stdout.trim()).toBe(`child received signal: ${signal}`); + expect(stderr).toBe(''); expect(code).toBe(123); }); } diff --git a/tests/esm-child-process/via-flag/index.ts b/tests/esm-child-process/via-flag/index.ts index 8f1906c2c..939272be8 100644 --- a/tests/esm-child-process/via-flag/index.ts +++ b/tests/esm-child-process/via-flag/index.ts @@ -1,3 +1,3 @@ -import assert from 'assert'; -assert(import.meta.url.includes('index.ts')); +import { strictEqual } from 'assert'; +strictEqual(import.meta.url.includes('index.ts'), true); console.log(`CLI args: ${process.argv.slice(2).join(' ')}`); diff --git a/tests/esm-child-process/via-flag/tsconfig.json b/tests/esm-child-process/via-flag/tsconfig.json index 217b77096..25a7642af 100644 --- a/tests/esm-child-process/via-flag/tsconfig.json +++ b/tests/esm-child-process/via-flag/tsconfig.json @@ -2,5 +2,8 @@ "compilerOptions": { "module": "ESNext", "esModuleInterop": true + }, + "ts-node": { + "swc": true } } diff --git a/tests/esm-child-process/via-tsconfig/index.ts b/tests/esm-child-process/via-tsconfig/index.ts index 8f1906c2c..939272be8 100644 --- a/tests/esm-child-process/via-tsconfig/index.ts +++ b/tests/esm-child-process/via-tsconfig/index.ts @@ -1,3 +1,3 @@ -import assert from 'assert'; -assert(import.meta.url.includes('index.ts')); +import { strictEqual } from 'assert'; +strictEqual(import.meta.url.includes('index.ts'), true); console.log(`CLI args: ${process.argv.slice(2).join(' ')}`); diff --git a/tests/esm-child-process/via-tsconfig/tsconfig.json b/tests/esm-child-process/via-tsconfig/tsconfig.json index 3757044e9..31f702b87 100644 --- a/tests/esm-child-process/via-tsconfig/tsconfig.json +++ b/tests/esm-child-process/via-tsconfig/tsconfig.json @@ -4,6 +4,7 @@ "esModuleInterop": true }, "ts-node": { - "esm": true + "esm": true, + "swc": true } } From f654960aa84f3ea7d8d4f18bc596b4be8cf5251a Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Fri, 25 Feb 2022 00:14:50 -0500 Subject: [PATCH 10/27] fix? --- src/test/esm-loader.spec.ts | 10 ++++++---- src/test/exec-helpers.ts | 4 ++-- src/test/testlib.ts | 31 +++++++++++++++++++++++-------- 3 files changed, 31 insertions(+), 14 deletions(-) diff --git a/src/test/esm-loader.spec.ts b/src/test/esm-loader.spec.ts index e84c26c1f..983b0a412 100644 --- a/src/test/esm-loader.spec.ts +++ b/src/test/esm-loader.spec.ts @@ -335,10 +335,12 @@ test.suite('esm', (test) => { signalTest('SIGTERM'); function signalTest(signal: string) { test(signal, async (t) => { - const childP = spawn([ - BIN_PATH, - `./esm-child-process/via-tsconfig/sleep.ts`, - ]); + const childP = spawn( + [BIN_PATH, `./esm-child-process/via-tsconfig/sleep.ts`], + { + stdio: ['ignore', 'pipe', 'pipe'], + } + ); let code: number | null | undefined = undefined; childP.child.on('exit', (_code) => (code = _code)); await delay(2e3); diff --git a/src/test/exec-helpers.ts b/src/test/exec-helpers.ts index b95488c32..bf0766475 100644 --- a/src/test/exec-helpers.ts +++ b/src/test/exec-helpers.ts @@ -73,8 +73,8 @@ export function createSpawn>( */ return function spawn( cmd: string[], - opts?: Pick> & - Partial> + opts?: Pick> & + Partial> ) { let child!: ChildProcess; return Object.assign( diff --git a/src/test/testlib.ts b/src/test/testlib.ts index 377d93ef3..6796a9a7f 100644 --- a/src/test/testlib.ts +++ b/src/test/testlib.ts @@ -19,6 +19,19 @@ export { ExecutionContext, expect }; // each .spec file in its own process, so actual concurrency is higher. const concurrencyLimiter = throat(16); +function errorPostprocessor(fn: T): T { + return async function (this: any) { + try { + return await fn.call(this, arguments); + } catch (error: any) { + delete error?.matcherResult; + // delete error?.matcherResult?.message; + if (error?.message) error.message = `\n${error.message}\n`; + throw error; + } + } as any; +} + function once(func: T): T { let run = false; let ret: any = undefined; @@ -167,14 +180,16 @@ function createTestInterface(opts: { ) { const wrappedMacros = macros.map((macro) => { return async function (t: ExecutionContext, ...args: any[]) { - return concurrencyLimiter(async () => { - let i = 0; - for (const func of beforeEachFunctions) { - await func(t); - i++; - } - return macro(t, ...args); - }); + return concurrencyLimiter( + errorPostprocessor(async () => { + let i = 0; + for (const func of beforeEachFunctions) { + await func(t); + i++; + } + return macro(t, ...args); + }) + ); }; }); const computedTitle = computeTitle(title); From fd7fb550fa8c911242d2645ed610a9a355ade9de Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Fri, 25 Feb 2022 00:57:12 -0500 Subject: [PATCH 11/27] fix --- ava.config.js | 1 + src/test/esm-loader.spec.ts | 14 +++++++------- tests/esm-child-process/via-tsconfig/sleep.ts | 1 + 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/ava.config.js b/ava.config.js index 6181565d3..915de176d 100644 --- a/ava.config.js +++ b/ava.config.js @@ -11,4 +11,5 @@ export default { }, require: ['./src/test/remove-env-var-force-color.js'], timeout: '300s', + concurrency: 1, }; diff --git a/src/test/esm-loader.spec.ts b/src/test/esm-loader.spec.ts index 983b0a412..b0482aa9b 100644 --- a/src/test/esm-loader.spec.ts +++ b/src/test/esm-loader.spec.ts @@ -335,12 +335,10 @@ test.suite('esm', (test) => { signalTest('SIGTERM'); function signalTest(signal: string) { test(signal, async (t) => { - const childP = spawn( - [BIN_PATH, `./esm-child-process/via-tsconfig/sleep.ts`], - { - stdio: ['ignore', 'pipe', 'pipe'], - } - ); + const childP = spawn([ + BIN_PATH, + `./esm-child-process/via-tsconfig/sleep.ts`, + ]); let code: number | null | undefined = undefined; childP.child.on('exit', (_code) => (code = _code)); await delay(2e3); @@ -360,7 +358,9 @@ test.suite('esm', (test) => { }); expect(codeAfter2Seconds).toBeUndefined(); expect(codeAfter4Seconds).toBeUndefined(); - expect(stdout.trim()).toBe(`child received signal: ${signal}`); + expect(stdout.trim()).toBe( + `child received signal: ${signal}\nchild exiting` + ); expect(stderr).toBe(''); expect(code).toBe(123); }); diff --git a/tests/esm-child-process/via-tsconfig/sleep.ts b/tests/esm-child-process/via-tsconfig/sleep.ts index dd2a786e1..309a0e881 100644 --- a/tests/esm-child-process/via-tsconfig/sleep.ts +++ b/tests/esm-child-process/via-tsconfig/sleep.ts @@ -6,6 +6,7 @@ process.on('SIGINT', onSignal); function onSignal(signal: string) { console.log(`child received signal: ${signal}`); setTimeout(() => { + console.log(`child exiting`); process.exit(123); }, 5e3); } From 7074b1c75325007bf4a57ffc2475c64684941d86 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Fri, 25 Feb 2022 01:17:56 -0500 Subject: [PATCH 12/27] fix? --- src/test/esm-loader.spec.ts | 2 +- tests/esm-child-process/via-tsconfig/sleep.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/esm-loader.spec.ts b/src/test/esm-loader.spec.ts index b0482aa9b..48d28ab21 100644 --- a/src/test/esm-loader.spec.ts +++ b/src/test/esm-loader.spec.ts @@ -359,7 +359,7 @@ test.suite('esm', (test) => { expect(codeAfter2Seconds).toBeUndefined(); expect(codeAfter4Seconds).toBeUndefined(); expect(stdout.trim()).toBe( - `child received signal: ${signal}\nchild exiting` + `child registered signal handlers\nchild received signal: ${signal}\nchild exiting` ); expect(stderr).toBe(''); expect(code).toBe(123); diff --git a/tests/esm-child-process/via-tsconfig/sleep.ts b/tests/esm-child-process/via-tsconfig/sleep.ts index 309a0e881..f45b9dadc 100644 --- a/tests/esm-child-process/via-tsconfig/sleep.ts +++ b/tests/esm-child-process/via-tsconfig/sleep.ts @@ -3,6 +3,7 @@ setTimeout(function () { }, 30e3); process.on('SIGTERM', onSignal); process.on('SIGINT', onSignal); +console.log('child registered signal handlers'); function onSignal(signal: string) { console.log(`child received signal: ${signal}`); setTimeout(() => { From e0a1e90d76451598dea4d53c79320e6d3302e297 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Fri, 25 Feb 2022 01:27:03 -0500 Subject: [PATCH 13/27] fix? --- src/test/esm-loader.spec.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/test/esm-loader.spec.ts b/src/test/esm-loader.spec.ts index 48d28ab21..1a0f80e0b 100644 --- a/src/test/esm-loader.spec.ts +++ b/src/test/esm-loader.spec.ts @@ -341,23 +341,23 @@ test.suite('esm', (test) => { ]); let code: number | null | undefined = undefined; childP.child.on('exit', (_code) => (code = _code)); - await delay(2e3); - const codeAfter2Seconds = code; + await delay(6e3); + const codeAfter6Seconds = code; process.kill(childP.child.pid, signal); await delay(2e3); - const codeAfter4Seconds = code; + const codeAfter8Seconds = code; const { stdoutP, stderrP } = await childP; const stdout = await stdoutP; const stderr = await stderrP; t.log({ stdout, stderr, - codeAfter2Seconds, - codeAfter4Seconds, + codeAfter2Seconds: codeAfter6Seconds, + codeAfter4Seconds: codeAfter8Seconds, code, }); - expect(codeAfter2Seconds).toBeUndefined(); - expect(codeAfter4Seconds).toBeUndefined(); + expect(codeAfter6Seconds).toBeUndefined(); + expect(codeAfter8Seconds).toBeUndefined(); expect(stdout.trim()).toBe( `child registered signal handlers\nchild received signal: ${signal}\nchild exiting` ); From 6db4facf27119033677f87bb2f904487943efe67 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Fri, 25 Feb 2022 01:52:23 -0500 Subject: [PATCH 14/27] fix! --- src/child/spawn-child.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/child/spawn-child.ts b/src/child/spawn-child.ts index 651037826..ce0bc46ac 100644 --- a/src/child/spawn-child.ts +++ b/src/child/spawn-child.ts @@ -1,8 +1,12 @@ import type { BootstrapState } from '../bin'; import { spawn } from 'child_process'; import { brotliCompressSync } from 'zlib'; +import { pathToFileURL } from 'url'; +import { versionGteLt } from '..'; const argPrefix = '--brotli-base64-config='; +const extraNodeFlags: string[] = []; +if(!versionGteLt(process.version, '12.17.0')) extraNodeFlags.push('--experimental-modules'); /** @internal */ export function callInChild(state: BootstrapState) { @@ -11,8 +15,10 @@ export function callInChild(state: BootstrapState) { [ '--require', require.resolve('./child-require.js'), + ...extraNodeFlags, '--loader', - require.resolve('../../child-loader.mjs'), + // Node on Windows doesn't like `c:\` absolute paths here; must be `file:///c:/` + pathToFileURL(require.resolve('../../child-loader.mjs')).toString(), require.resolve('./child-entrypoint.js'), `${argPrefix}${brotliCompressSync( Buffer.from(JSON.stringify(state), 'utf8') From d02927f8ab6ccf94c3d4c0bcdc884cd69f81da1e Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Fri, 25 Feb 2022 01:54:06 -0500 Subject: [PATCH 15/27] fix --- src/child/spawn-child.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/child/spawn-child.ts b/src/child/spawn-child.ts index ce0bc46ac..869808ffb 100644 --- a/src/child/spawn-child.ts +++ b/src/child/spawn-child.ts @@ -6,7 +6,8 @@ import { versionGteLt } from '..'; const argPrefix = '--brotli-base64-config='; const extraNodeFlags: string[] = []; -if(!versionGteLt(process.version, '12.17.0')) extraNodeFlags.push('--experimental-modules'); +if (!versionGteLt(process.version, '12.17.0')) + extraNodeFlags.push('--experimental-modules'); /** @internal */ export function callInChild(state: BootstrapState) { From ece1952a4ea91b0f52f63d114dedebe87ab8c2de Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Fri, 25 Feb 2022 02:06:50 -0500 Subject: [PATCH 16/27] fix!! --- src/child/spawn-child.ts | 7 +++---- src/test/esm-loader.spec.ts | 3 +++ src/test/helpers.ts | 4 ++++ 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/child/spawn-child.ts b/src/child/spawn-child.ts index 869808ffb..6e74e561f 100644 --- a/src/child/spawn-child.ts +++ b/src/child/spawn-child.ts @@ -5,18 +5,17 @@ import { pathToFileURL } from 'url'; import { versionGteLt } from '..'; const argPrefix = '--brotli-base64-config='; -const extraNodeFlags: string[] = []; -if (!versionGteLt(process.version, '12.17.0')) - extraNodeFlags.push('--experimental-modules'); /** @internal */ export function callInChild(state: BootstrapState) { + if (!versionGteLt(process.version, '12.17.0')) { + throw new Error('`ts-node-esm` and `ts-node --esm` require node version 12.17.0 or newer.'); + } const child = spawn( process.execPath, [ '--require', require.resolve('./child-require.js'), - ...extraNodeFlags, '--loader', // Node on Windows doesn't like `c:\` absolute paths here; must be `file:///c:/` pathToFileURL(require.resolve('../../child-loader.mjs')).toString(), diff --git a/src/test/esm-loader.spec.ts b/src/test/esm-loader.spec.ts index 1a0f80e0b..e3871f18a 100644 --- a/src/test/esm-loader.spec.ts +++ b/src/test/esm-loader.spec.ts @@ -14,6 +14,7 @@ import { EXPERIMENTAL_MODULES_FLAG, nodeSupportsEsmHooks, nodeSupportsImportAssertions, + nodeSupportsSpawningChildProcess, nodeUsesNewHooksApi, resetNodeEnvironment, TEST_DIR, @@ -308,6 +309,8 @@ test.suite('esm', (test) => { ); test.suite('spawns child process', async (test) => { + test.runIf(nodeSupportsSpawningChildProcess); + basic('ts-node-esm executable', () => exec(`${BIN_ESM_PATH} ./esm-child-process/via-flag/index.ts foo bar`) ); diff --git a/src/test/helpers.ts b/src/test/helpers.ts index 6ecadcc64..fd1643614 100644 --- a/src/test/helpers.ts +++ b/src/test/helpers.ts @@ -18,6 +18,10 @@ const createRequire: typeof _createRequire = require('create-require'); export { tsNodeTypes }; export const nodeSupportsEsmHooks = semver.gte(process.version, '12.16.0'); +export const nodeSupportsSpawningChildProcess = semver.gte( + process.version, + '12.17.0' +); export const nodeUsesNewHooksApi = semver.gte(process.version, '16.12.0'); export const nodeSupportsImportAssertions = semver.gte( process.version, From 5cc5f420e5dadddc8843b8b388a79e562d5b6e0f Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Fri, 25 Feb 2022 02:07:12 -0500 Subject: [PATCH 17/27] fix --- src/child/spawn-child.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/child/spawn-child.ts b/src/child/spawn-child.ts index 6e74e561f..f5222155b 100644 --- a/src/child/spawn-child.ts +++ b/src/child/spawn-child.ts @@ -9,7 +9,9 @@ const argPrefix = '--brotli-base64-config='; /** @internal */ export function callInChild(state: BootstrapState) { if (!versionGteLt(process.version, '12.17.0')) { - throw new Error('`ts-node-esm` and `ts-node --esm` require node version 12.17.0 or newer.'); + throw new Error( + '`ts-node-esm` and `ts-node --esm` require node version 12.17.0 or newer.' + ); } const child = spawn( process.execPath, From 2446c0823595e8ba126d32dc236bc671509e9fe1 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Fri, 25 Feb 2022 02:32:50 -0500 Subject: [PATCH 18/27] fix... --- src/child/spawn-child.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/child/spawn-child.ts b/src/child/spawn-child.ts index f5222155b..74bf4017c 100644 --- a/src/child/spawn-child.ts +++ b/src/child/spawn-child.ts @@ -8,7 +8,7 @@ const argPrefix = '--brotli-base64-config='; /** @internal */ export function callInChild(state: BootstrapState) { - if (!versionGteLt(process.version, '12.17.0')) { + if (!versionGteLt(process.versions.node, '12.17.0')) { throw new Error( '`ts-node-esm` and `ts-node --esm` require node version 12.17.0 or newer.' ); From 5bbffd6fb1af01cdaf143f5cee55e2f51f2570a8 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Fri, 25 Feb 2022 02:42:49 -0500 Subject: [PATCH 19/27] tweak test lib's suite delimiter to match ava's --- src/test/testlib.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/testlib.ts b/src/test/testlib.ts index 6796a9a7f..c99f3878c 100644 --- a/src/test/testlib.ts +++ b/src/test/testlib.ts @@ -48,7 +48,8 @@ export const test = createTestInterface({ mustDoSerial: false, automaticallyDoSerial: false, automaticallySkip: false, - separator: ' > ', + // The little right chevron used by ava + separator: ' \u203a ', titlePrefix: undefined, }); // In case someone wants to `const test = _test.context()` From b652088dc47156d4cfa13f7d9d4b376d93a61b27 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Fri, 25 Feb 2022 02:54:24 -0500 Subject: [PATCH 20/27] fix --- src/test/esm-loader.spec.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/test/esm-loader.spec.ts b/src/test/esm-loader.spec.ts index e3871f18a..dbcf38799 100644 --- a/src/test/esm-loader.spec.ts +++ b/src/test/esm-loader.spec.ts @@ -7,6 +7,7 @@ import semver = require('semver'); import { BIN_ESM_PATH, BIN_PATH, + BIN_PATH_JS, CMD_ESM_LOADER_WITHOUT_PROJECT, CMD_TS_NODE_WITHOUT_PROJECT_FLAG, contextTsNodeUnderTest, @@ -339,7 +340,9 @@ test.suite('esm', (test) => { function signalTest(signal: string) { test(signal, async (t) => { const childP = spawn([ - BIN_PATH, + // exec lets us run the shims on windows; spawn does not + process.execPath, + BIN_PATH_JS, `./esm-child-process/via-tsconfig/sleep.ts`, ]); let code: number | null | undefined = undefined; From 7fe01669e618332e3dea43d32c6ed0c5fe8d11a2 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Fri, 25 Feb 2022 03:21:51 -0500 Subject: [PATCH 21/27] fix --- src/test/esm-loader.spec.ts | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/test/esm-loader.spec.ts b/src/test/esm-loader.spec.ts index dbcf38799..f9197af5c 100644 --- a/src/test/esm-loader.spec.ts +++ b/src/test/esm-loader.spec.ts @@ -335,8 +335,10 @@ test.suite('esm', (test) => { test.suite('parent passes signals to child', (test) => { test.runSerially(); + signalTest('SIGINT'); signalTest('SIGTERM'); + function signalTest(signal: string) { test(signal, async (t) => { const childP = spawn([ @@ -358,17 +360,25 @@ test.suite('esm', (test) => { t.log({ stdout, stderr, - codeAfter2Seconds: codeAfter6Seconds, - codeAfter4Seconds: codeAfter8Seconds, + codeAfter6Seconds, + codeAfter8Seconds, code, }); expect(codeAfter6Seconds).toBeUndefined(); - expect(codeAfter8Seconds).toBeUndefined(); - expect(stdout.trim()).toBe( - `child registered signal handlers\nchild received signal: ${signal}\nchild exiting` - ); + if (process.platform === 'win32') { + // Windows doesn't have signals, and node attempts an imperfect facsimile. + // In Windows, SIGINT and SIGTERM kill the process immediately with exit + // code 1, and the process can't catch or prevent this. + expect(codeAfter8Seconds).toBe(1); + expect(code).toBe(1); + } else { + expect(codeAfter8Seconds).toBe(undefined); + expect(code).toBe(123); + expect(stdout.trim()).toBe( + `child registered signal handlers\nchild received signal: ${signal}\nchild exiting` + ); + } expect(stderr).toBe(''); - expect(code).toBe(123); }); } }); From 2ebc55c1677bbeda1ad1e5ada77c2504d53e7117 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Fri, 4 Mar 2022 02:45:27 -0500 Subject: [PATCH 22/27] docs, fixes --- dist-raw/node-primordials.js | 1 - package.json | 6 +++--- src/bin.ts | 12 +++++------- website/docs/imports.md | 28 +++++++++++++++++++++++++++- website/docs/options.md | 1 + website/docs/usage.md | 3 +++ 6 files changed, 39 insertions(+), 12 deletions(-) diff --git a/dist-raw/node-primordials.js b/dist-raw/node-primordials.js index ae3b8b911..21d8cfd19 100644 --- a/dist-raw/node-primordials.js +++ b/dist-raw/node-primordials.js @@ -1,7 +1,6 @@ module.exports = { ArrayFrom: Array.from, ArrayIsArray: Array.isArray, - ArrayPrototypeJoin: (obj, separator) => Array.prototype.join.call(obj, separator), ArrayPrototypeShift: (obj) => Array.prototype.shift.call(obj), ArrayPrototypeForEach: (arr, ...rest) => Array.prototype.forEach.apply(arr, rest), ArrayPrototypeIncludes: (arr, ...rest) => Array.prototype.includes.apply(arr, rest), diff --git a/package.json b/package.json index 225937857..84a5ad16d 100644 --- a/package.json +++ b/package.json @@ -36,11 +36,11 @@ "types": "dist/index.d.ts", "bin": { "ts-node": "dist/bin.js", - "ts-script": "dist/bin-script-deprecated.js", - "ts-node-script": "dist/bin-script.js", "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-node-esm": "dist/bin-esm.js" + "ts-script": "dist/bin-script-deprecated.js" }, "files": [ "/transpilers/", diff --git a/src/bin.ts b/src/bin.ts index 6ec01fc31..3e972dd97 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -273,13 +273,14 @@ Options: -r, --require [path] Require a node module before execution -i, --interactive Opens the REPL even if stdin does not appear to be a terminal + --esm Bootstrap with the ESM loader, enabling full ESM support + --swc Use the faster swc transpiler + -h, --help Print CLI usage - -v, --version Print module version information - --cwdMode Use current directory instead of for config resolution + -v, --version Print module version information. -vvv to print additional information --showConfig Print resolved configuration and exit -T, --transpileOnly Use TypeScript's faster \`transpileModule\` or a third-party transpiler - --swc Use the swc transpiler -H, --compilerHost Use TypeScript's compiler host API -I, --ignore [pattern] Override the path patterns to skip compilation -P, --project [path] Path to TypeScript JSON project file @@ -291,6 +292,7 @@ Options: --cwd Behave as if invoked within this working directory. --files Load \`files\`, \`include\` and \`exclude\` from \`tsconfig.json\` on startup --pretty Use pretty diagnostic formatter (usually enabled by default) + --cwdMode Use current directory instead of for config resolution --skipProject Skip reading \`tsconfig.json\` --skipIgnore Skip \`--ignore\` checks --emit Emit output files into \`.ts-node\` directory @@ -384,10 +386,6 @@ function phase3(payload: BootstrapState) { ignoreDiagnostics, compilerOptions, require: argsRequire, - // readFile: evalAwarePartialHost?.readFile ?? undefined, - // fileExists: evalAwarePartialHost?.fileExists ?? undefined, - readFile: undefined, - fileExists: undefined, scope, scopeDir, preferTsExts, diff --git a/website/docs/imports.md b/website/docs/imports.md index 6b04f776f..4a0ea5c7b 100644 --- a/website/docs/imports.md +++ b/website/docs/imports.md @@ -11,7 +11,7 @@ Here is a brief comparison of the two. | Write native `import` syntax | Write native `import` syntax | | Transforms `import` into `require()` | Does not transform `import` | | Node executes scripts using the classic [CommonJS loader](https://nodejs.org/dist/latest-v16.x/docs/api/modules.html) | Node executes scripts using the new [ESM loader](https://nodejs.org/dist/latest-v16.x/docs/api/esm.html) | -| Use any of:
ts-node CLI
`node -r ts-node/register`
`NODE_OPTIONS="ts-node/register" node`
`require('ts-node').register({/* options */})` | Must use the ESM loader via:
`node --loader ts-node/esm`
`NODE_OPTIONS="--loader ts-node/esm" node` | +| Use any of:
`ts-node`
`node -r ts-node/register`
`NODE_OPTIONS="ts-node/register" node`
`require('ts-node').register({/* options */})` | Use any of:
`ts-node --esm`
`ts-node-esm`
Set `"esm": true` in `tsconfig.json`
`node --loader ts-node/esm`
`NODE_OPTIONS="--loader ts-node/esm" node` | ## CommonJS @@ -65,6 +65,32 @@ You must set [`"type": "module"`](https://nodejs.org/api/packages.html#packages_ { "compilerOptions": { "module": "ESNext" // or ES2015, ES2020 + }, + "ts-node": { + // Tell ts-node CLI to install the --loader automatically, explained below + "esm": true } } ``` + +You must also ensure node is passed `--loader`. The ts-node CLI will do this automatically with our `esm` option. + +> Note: `--esm` must spawn a child process to pass it `--loader`. This may change if node adds the ability to install loader hooks +into the current process. + +```shell +# pass the flag +ts-node --esm +# Use the convenience binary +ts-node-esm +# or add `"esm": true` to your tsconfig.json to make it automatic +ts-node +``` + +If you are not using our CLI, pass the loader flag to node. + +```shell +node --loader ts-node/esm ./index.ts +# Or via environment variable +NODE_OPTIONS="--loader ts-node/esm" node ./index.ts +``` diff --git a/website/docs/options.md b/website/docs/options.md index aadf36da9..96e49c1eb 100644 --- a/website/docs/options.md +++ b/website/docs/options.md @@ -15,6 +15,7 @@ _Environment variables, where available, are in `ALL_CAPS`_ - `-e, --eval` Evaluate code - `-p, --print` Print result of `--eval` - `-i, --interactive` Opens the REPL even if stdin does not appear to be a terminal +- `--esm` Bootstrap with the ESM loader, enabling full ESM support ## TSConfig diff --git a/website/docs/usage.md b/website/docs/usage.md index d988e606b..1e6e563e8 100644 --- a/website/docs/usage.md +++ b/website/docs/usage.md @@ -25,6 +25,9 @@ ts-node-transpile-only script.ts # Equivalent to ts-node --cwdMode ts-node-cwd script.ts + +# Equivalent to ts-node --esm +ts-node-esm script.ts ``` ## Shebang From 4cdf1a50a09fc4ae7d950b378ad176cf3b289d98 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Sat, 5 Mar 2022 00:24:47 -0500 Subject: [PATCH 23/27] fix #1662 and add tests --- src/configuration.ts | 17 +++--- src/index.ts | 19 +++--- src/test/pluggable-dep-resolution.spec.ts | 59 +++++++++++++++++++ src/transpilers/swc.ts | 4 +- .../node_modules/@swc/core/index.js | 5 ++ .../node_modules/@swc/wasm/index.js | 5 ++ .../node_modules/custom-compiler/index.js | 9 +++ .../node_modules/custom-swc/index.js | 5 ++ .../node_modules/custom-transpiler/index.js | 10 ++++ .../node_modules/@swc/core/index.js | 5 ++ .../node_modules/@swc/wasm/index.js | 5 ++ .../node_modules/custom-compiler/index.js | 9 +++ .../node_modules/custom-swc/index.js | 5 ++ .../node_modules/custom-transpiler/index.js | 10 ++++ .../tsconfig-custom-compiler.json | 1 + .../tsconfig-custom-transpiler.json | 1 + .../shared-config/tsconfig-swc-core.json | 1 + .../tsconfig-swc-custom-backend.json | 1 + .../shared-config/tsconfig-swc-wasm.json | 1 + .../shared-config/tsconfig-swc.json | 1 + .../tsconfig-custom-compiler.json | 1 + .../tsconfig-custom-transpiler.json | 1 + .../tsconfig-extend-custom-compiler.json | 1 + .../tsconfig-extend-custom-transpiler.json | 1 + .../tsconfig-extend-swc-core.json | 1 + .../tsconfig-extend-swc-custom-backend.json | 1 + .../tsconfig-extend-swc-wasm.json | 1 + .../tsconfig-extend-swc.json | 1 + .../tsconfig-swc-core.json | 6 ++ .../tsconfig-swc-custom-backend.json | 6 ++ .../tsconfig-swc-wasm.json | 6 ++ .../tsconfig-swc.json | 1 + 32 files changed, 184 insertions(+), 16 deletions(-) create mode 100644 src/test/pluggable-dep-resolution.spec.ts create mode 100644 tests/pluggable-dep-resolution/node_modules/@swc/core/index.js create mode 100644 tests/pluggable-dep-resolution/node_modules/@swc/wasm/index.js create mode 100644 tests/pluggable-dep-resolution/node_modules/custom-compiler/index.js create mode 100644 tests/pluggable-dep-resolution/node_modules/custom-swc/index.js create mode 100644 tests/pluggable-dep-resolution/node_modules/custom-transpiler/index.js create mode 100644 tests/pluggable-dep-resolution/node_modules/shared-config/node_modules/@swc/core/index.js create mode 100644 tests/pluggable-dep-resolution/node_modules/shared-config/node_modules/@swc/wasm/index.js create mode 100644 tests/pluggable-dep-resolution/node_modules/shared-config/node_modules/custom-compiler/index.js create mode 100644 tests/pluggable-dep-resolution/node_modules/shared-config/node_modules/custom-swc/index.js create mode 100644 tests/pluggable-dep-resolution/node_modules/shared-config/node_modules/custom-transpiler/index.js create mode 100644 tests/pluggable-dep-resolution/node_modules/shared-config/tsconfig-custom-compiler.json create mode 100644 tests/pluggable-dep-resolution/node_modules/shared-config/tsconfig-custom-transpiler.json create mode 100644 tests/pluggable-dep-resolution/node_modules/shared-config/tsconfig-swc-core.json create mode 100644 tests/pluggable-dep-resolution/node_modules/shared-config/tsconfig-swc-custom-backend.json create mode 100644 tests/pluggable-dep-resolution/node_modules/shared-config/tsconfig-swc-wasm.json create mode 100644 tests/pluggable-dep-resolution/node_modules/shared-config/tsconfig-swc.json create mode 100644 tests/pluggable-dep-resolution/tsconfig-custom-compiler.json create mode 100644 tests/pluggable-dep-resolution/tsconfig-custom-transpiler.json create mode 100644 tests/pluggable-dep-resolution/tsconfig-extend-custom-compiler.json create mode 100644 tests/pluggable-dep-resolution/tsconfig-extend-custom-transpiler.json create mode 100644 tests/pluggable-dep-resolution/tsconfig-extend-swc-core.json create mode 100644 tests/pluggable-dep-resolution/tsconfig-extend-swc-custom-backend.json create mode 100644 tests/pluggable-dep-resolution/tsconfig-extend-swc-wasm.json create mode 100644 tests/pluggable-dep-resolution/tsconfig-extend-swc.json create mode 100644 tests/pluggable-dep-resolution/tsconfig-swc-core.json create mode 100644 tests/pluggable-dep-resolution/tsconfig-swc-custom-backend.json create mode 100644 tests/pluggable-dep-resolution/tsconfig-swc-wasm.json create mode 100644 tests/pluggable-dep-resolution/tsconfig-swc.json diff --git a/src/configuration.ts b/src/configuration.ts index 5d23b0a26..13b7ad28f 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -100,7 +100,10 @@ export function findAndReadConfig(rawOptions: CreateOptions) { rawOptions.project, cwd ); - ({ compiler } = resolveCompiler(options.compiler, projectLocalResolveDir)); + ({ compiler } = resolveCompiler( + options.compiler, + optionBasePaths.compiler ?? projectLocalResolveDir + )); } return { @@ -258,6 +261,9 @@ export function readConfig( if (options.compiler != null) { optionBasePaths.compiler = basePath; } + if (options.swc != null) { + optionBasePaths.swc = basePath; + } assign(tsNodeOptionsFromTsconfig, options); } @@ -329,19 +335,16 @@ export function resolveAndLoadCompiler( name: string | undefined, relativeToPath: string ) { - const { compiler, projectLocalResolveHelper } = resolveCompiler( - name, - relativeToPath - ); + const { compiler } = resolveCompiler(name, relativeToPath); const ts = loadCompiler(compiler); - return { compiler, ts, projectLocalResolveHelper }; + return { compiler, ts }; } function resolveCompiler(name: string | undefined, relativeToPath: string) { const projectLocalResolveHelper = createProjectLocalResolveHelper(relativeToPath); const compiler = projectLocalResolveHelper(name || 'typescript', true); - return { projectLocalResolveHelper, compiler }; + return { compiler }; } /** @internal */ diff --git a/src/index.ts b/src/index.ts index e8ca57b8d..8ad86b1bc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -387,6 +387,7 @@ export interface OptionBasePaths { moduleTypes?: string; transpiler?: string; compiler?: string; + swc?: string; } /** @@ -658,11 +659,15 @@ export function createFromPreloadedConfig( const transpileOnly = (options.transpileOnly === true || options.swc === true) && options.typeCheck !== true; - const transpiler = options.transpiler - ? options.transpiler - : options.swc - ? require.resolve('./transpilers/swc.js') - : undefined; + let transpiler: RegisterOptions['transpiler'] | undefined = undefined; + let transpilerBasePath: string | undefined = undefined; + if (options.transpiler) { + transpiler = options.transpiler; + transpilerBasePath = optionBasePaths.transpiler; + } else if (options.swc) { + transpiler = require.resolve('./transpilers/swc.js'); + transpilerBasePath = optionBasePaths.swc; + } const transformers = options.transformers || undefined; const diagnosticFilters: Array = [ { @@ -729,8 +734,8 @@ export function createFromPreloadedConfig( typeof transpiler === 'string' ? transpiler : transpiler[0]; const transpilerOptions = typeof transpiler === 'string' ? {} : transpiler[1] ?? {}; - const transpilerConfigLocalResolveHelper = optionBasePaths.transpiler - ? createProjectLocalResolveHelper(optionBasePaths.transpiler!) + const transpilerConfigLocalResolveHelper = transpilerBasePath + ? createProjectLocalResolveHelper(transpilerBasePath) : projectLocalResolveHelper; const transpilerPath = transpilerConfigLocalResolveHelper( transpilerName, diff --git a/src/test/pluggable-dep-resolution.spec.ts b/src/test/pluggable-dep-resolution.spec.ts new file mode 100644 index 000000000..cdb351d33 --- /dev/null +++ b/src/test/pluggable-dep-resolution.spec.ts @@ -0,0 +1,59 @@ +import { context } from './testlib'; +import { contextTsNodeUnderTest, resetNodeEnvironment } from './helpers'; +import * as expect from 'expect'; +import { resolve } from 'path'; + +const test = context(contextTsNodeUnderTest); + +test.suite( + 'Pluggable dependency (compiler, transpiler, swc backend) is require()d relative to the tsconfig file that declares it', + (test) => { + test.runSerially(); + + // The use-case we want to support: + // + // User shares their tsconfig across multiple projects as an npm module named "shared-config", similar to @tsconfig/bases + // In their npm module + // They have tsconfig.json with `swc: true` or `compiler: "ts-patch"` or something like that + // The module declares a dependency on a known working version of @swc/core, or ts-patch, or something like that. + // They use this reusable config via `npm install shared-config` and `"extends": "shared-config/tsconfig.json"` + // + // ts-node should resolve ts-patch or @swc/core relative to the extended tsconfig + // to ensure we use the known working versions. + + macro('tsconfig-custom-compiler.json', 'root custom compiler'); + macro('tsconfig-custom-transpiler.json', 'root custom transpiler'); + macro('tsconfig-swc-custom-backend.json', 'root custom swc backend'); + macro('tsconfig-swc-core.json', 'root @swc/core'); + macro('tsconfig-swc-wasm.json', 'root @swc/wasm'); + macro('tsconfig-swc.json', 'root @swc/core'); + + macro('node_modules/shared-config/tsconfig-custom-compiler.json', 'shared-config custom compiler'); + macro('node_modules/shared-config/tsconfig-custom-transpiler.json', 'shared-config custom transpiler'); + macro('node_modules/shared-config/tsconfig-swc-custom-backend.json', 'shared-config custom swc backend'); + macro('node_modules/shared-config/tsconfig-swc-core.json', 'shared-config @swc/core'); + macro('node_modules/shared-config/tsconfig-swc-wasm.json', 'shared-config @swc/wasm'); + macro('node_modules/shared-config/tsconfig-swc.json', 'shared-config @swc/core'); + + macro('tsconfig-extend-custom-compiler.json', 'shared-config custom compiler'); + macro('tsconfig-extend-custom-transpiler.json', 'shared-config custom transpiler'); + macro('tsconfig-extend-swc-custom-backend.json', 'shared-config custom swc backend'); + macro('tsconfig-extend-swc-core.json', 'shared-config @swc/core'); + macro('tsconfig-extend-swc-wasm.json', 'shared-config @swc/wasm'); + macro('tsconfig-extend-swc.json', 'shared-config @swc/core'); + + function macro(config: string, expected: string) { + test(`${config} uses ${expected}`, async (t) => { + t.teardown(resetNodeEnvironment); + + const output = t.context.tsNodeUnderTest + .create({ + project: resolve('tests/pluggable-dep-resolution', config), + }) + .compile('', 'index.ts'); + + expect(output).toContain(`emit from ${expected}\n`); + }); + } + } +); diff --git a/src/transpilers/swc.ts b/src/transpilers/swc.ts index 07cf49801..1d8d1c441 100644 --- a/src/transpilers/swc.ts +++ b/src/transpilers/swc.ts @@ -29,10 +29,10 @@ export function create(createOptions: SwcTranspilerOptions): Transpiler { } else if (swc == null) { let swcResolved; try { - swcResolved = projectLocalResolveHelper('@swc/core', true); + swcResolved = transpilerConfigLocalResolveHelper('@swc/core', true); } catch (e) { try { - swcResolved = projectLocalResolveHelper('@swc/wasm', true); + swcResolved = transpilerConfigLocalResolveHelper('@swc/wasm', true); } catch (e) { throw new Error( 'swc compiler requires either @swc/core or @swc/wasm to be installed as a dependency. See https://typestrong.org/ts-node/docs/transpilers' diff --git a/tests/pluggable-dep-resolution/node_modules/@swc/core/index.js b/tests/pluggable-dep-resolution/node_modules/@swc/core/index.js new file mode 100644 index 000000000..b9924d5f4 --- /dev/null +++ b/tests/pluggable-dep-resolution/node_modules/@swc/core/index.js @@ -0,0 +1,5 @@ +module.exports = { + transformSync() { + return { code: 'emit from root @swc/core', map: '{}' }; + }, + }; diff --git a/tests/pluggable-dep-resolution/node_modules/@swc/wasm/index.js b/tests/pluggable-dep-resolution/node_modules/@swc/wasm/index.js new file mode 100644 index 000000000..f149018fb --- /dev/null +++ b/tests/pluggable-dep-resolution/node_modules/@swc/wasm/index.js @@ -0,0 +1,5 @@ +module.exports = { + transformSync() { + return { code: 'emit from root @swc/wasm', map: '{}' }; + }, + }; diff --git a/tests/pluggable-dep-resolution/node_modules/custom-compiler/index.js b/tests/pluggable-dep-resolution/node_modules/custom-compiler/index.js new file mode 100644 index 000000000..806376ab1 --- /dev/null +++ b/tests/pluggable-dep-resolution/node_modules/custom-compiler/index.js @@ -0,0 +1,9 @@ +module.exports = { + ...require('typescript'), + transpileModule() { + return { + outputText: 'emit from root custom compiler', + sourceMapText: '{}', + }; + }, + }; diff --git a/tests/pluggable-dep-resolution/node_modules/custom-swc/index.js b/tests/pluggable-dep-resolution/node_modules/custom-swc/index.js new file mode 100644 index 000000000..e23907430 --- /dev/null +++ b/tests/pluggable-dep-resolution/node_modules/custom-swc/index.js @@ -0,0 +1,5 @@ +module.exports = { + transformSync() { + return { code: 'emit from root custom swc backend', map: '{}' }; + }, + }; diff --git a/tests/pluggable-dep-resolution/node_modules/custom-transpiler/index.js b/tests/pluggable-dep-resolution/node_modules/custom-transpiler/index.js new file mode 100644 index 000000000..ed3d1cb28 --- /dev/null +++ b/tests/pluggable-dep-resolution/node_modules/custom-transpiler/index.js @@ -0,0 +1,10 @@ +module.exports.create = function () { + return { + transpile() { + return { + outputText: 'emit from root custom transpiler', + sourceMapText: '{}', + }; + }, + }; + }; diff --git a/tests/pluggable-dep-resolution/node_modules/shared-config/node_modules/@swc/core/index.js b/tests/pluggable-dep-resolution/node_modules/shared-config/node_modules/@swc/core/index.js new file mode 100644 index 000000000..ee65ccdd9 --- /dev/null +++ b/tests/pluggable-dep-resolution/node_modules/shared-config/node_modules/@swc/core/index.js @@ -0,0 +1,5 @@ +module.exports = { + transformSync() { + return { code: 'emit from shared-config @swc/core', map: '{}' }; + }, + }; diff --git a/tests/pluggable-dep-resolution/node_modules/shared-config/node_modules/@swc/wasm/index.js b/tests/pluggable-dep-resolution/node_modules/shared-config/node_modules/@swc/wasm/index.js new file mode 100644 index 000000000..7b4a479ea --- /dev/null +++ b/tests/pluggable-dep-resolution/node_modules/shared-config/node_modules/@swc/wasm/index.js @@ -0,0 +1,5 @@ +module.exports = { + transformSync() { + return { code: 'emit from shared-config @swc/wasm', map: '{}' }; + }, + }; diff --git a/tests/pluggable-dep-resolution/node_modules/shared-config/node_modules/custom-compiler/index.js b/tests/pluggable-dep-resolution/node_modules/shared-config/node_modules/custom-compiler/index.js new file mode 100644 index 000000000..b1a45e628 --- /dev/null +++ b/tests/pluggable-dep-resolution/node_modules/shared-config/node_modules/custom-compiler/index.js @@ -0,0 +1,9 @@ +module.exports = { + ...require('typescript'), + transpileModule() { + return { + outputText: 'emit from shared-config custom compiler', + sourceMapText: '{}', + }; + }, + }; diff --git a/tests/pluggable-dep-resolution/node_modules/shared-config/node_modules/custom-swc/index.js b/tests/pluggable-dep-resolution/node_modules/shared-config/node_modules/custom-swc/index.js new file mode 100644 index 000000000..9d69e702a --- /dev/null +++ b/tests/pluggable-dep-resolution/node_modules/shared-config/node_modules/custom-swc/index.js @@ -0,0 +1,5 @@ +module.exports = { + transformSync() { + return { code: 'emit from shared-config custom swc backend', map: '{}' }; + }, + }; diff --git a/tests/pluggable-dep-resolution/node_modules/shared-config/node_modules/custom-transpiler/index.js b/tests/pluggable-dep-resolution/node_modules/shared-config/node_modules/custom-transpiler/index.js new file mode 100644 index 000000000..d8ca0d3f6 --- /dev/null +++ b/tests/pluggable-dep-resolution/node_modules/shared-config/node_modules/custom-transpiler/index.js @@ -0,0 +1,10 @@ +module.exports.create = function () { + return { + transpile() { + return { + outputText: 'emit from shared-config custom transpiler', + sourceMapText: '{}', + }; + }, + }; + }; diff --git a/tests/pluggable-dep-resolution/node_modules/shared-config/tsconfig-custom-compiler.json b/tests/pluggable-dep-resolution/node_modules/shared-config/tsconfig-custom-compiler.json new file mode 100644 index 000000000..926d54985 --- /dev/null +++ b/tests/pluggable-dep-resolution/node_modules/shared-config/tsconfig-custom-compiler.json @@ -0,0 +1 @@ +{"ts-node":{"transpileOnly":true,"compiler":"custom-compiler"}} diff --git a/tests/pluggable-dep-resolution/node_modules/shared-config/tsconfig-custom-transpiler.json b/tests/pluggable-dep-resolution/node_modules/shared-config/tsconfig-custom-transpiler.json new file mode 100644 index 000000000..bb64bd1f2 --- /dev/null +++ b/tests/pluggable-dep-resolution/node_modules/shared-config/tsconfig-custom-transpiler.json @@ -0,0 +1 @@ +{"ts-node":{"transpileOnly":true,"transpiler":"custom-transpiler"}} diff --git a/tests/pluggable-dep-resolution/node_modules/shared-config/tsconfig-swc-core.json b/tests/pluggable-dep-resolution/node_modules/shared-config/tsconfig-swc-core.json new file mode 100644 index 000000000..c4191aec0 --- /dev/null +++ b/tests/pluggable-dep-resolution/node_modules/shared-config/tsconfig-swc-core.json @@ -0,0 +1 @@ +{"ts-node":{"transpileOnly":true,"transpiler":["ts-node/transpilers/swc",{"swc":"@swc/core"}]}} diff --git a/tests/pluggable-dep-resolution/node_modules/shared-config/tsconfig-swc-custom-backend.json b/tests/pluggable-dep-resolution/node_modules/shared-config/tsconfig-swc-custom-backend.json new file mode 100644 index 000000000..c23cd162e --- /dev/null +++ b/tests/pluggable-dep-resolution/node_modules/shared-config/tsconfig-swc-custom-backend.json @@ -0,0 +1 @@ +{"ts-node":{"transpileOnly":true,"transpiler":["ts-node/transpilers/swc",{"swc":"custom-swc"}]}} diff --git a/tests/pluggable-dep-resolution/node_modules/shared-config/tsconfig-swc-wasm.json b/tests/pluggable-dep-resolution/node_modules/shared-config/tsconfig-swc-wasm.json new file mode 100644 index 000000000..94d91973a --- /dev/null +++ b/tests/pluggable-dep-resolution/node_modules/shared-config/tsconfig-swc-wasm.json @@ -0,0 +1 @@ +{"ts-node":{"transpileOnly":true,"transpiler":["ts-node/transpilers/swc",{"swc":"@swc/wasm"}]}} diff --git a/tests/pluggable-dep-resolution/node_modules/shared-config/tsconfig-swc.json b/tests/pluggable-dep-resolution/node_modules/shared-config/tsconfig-swc.json new file mode 100644 index 000000000..430482e84 --- /dev/null +++ b/tests/pluggable-dep-resolution/node_modules/shared-config/tsconfig-swc.json @@ -0,0 +1 @@ +{"ts-node":{"swc":true}} diff --git a/tests/pluggable-dep-resolution/tsconfig-custom-compiler.json b/tests/pluggable-dep-resolution/tsconfig-custom-compiler.json new file mode 100644 index 000000000..12f1bfe6d --- /dev/null +++ b/tests/pluggable-dep-resolution/tsconfig-custom-compiler.json @@ -0,0 +1 @@ +{ "ts-node": { "transpileOnly": true, "compiler": "custom-compiler" } } diff --git a/tests/pluggable-dep-resolution/tsconfig-custom-transpiler.json b/tests/pluggable-dep-resolution/tsconfig-custom-transpiler.json new file mode 100644 index 000000000..c2339a1ea --- /dev/null +++ b/tests/pluggable-dep-resolution/tsconfig-custom-transpiler.json @@ -0,0 +1 @@ +{ "ts-node": { "transpileOnly": true, "transpiler": "custom-transpiler" } } diff --git a/tests/pluggable-dep-resolution/tsconfig-extend-custom-compiler.json b/tests/pluggable-dep-resolution/tsconfig-extend-custom-compiler.json new file mode 100644 index 000000000..674b908e2 --- /dev/null +++ b/tests/pluggable-dep-resolution/tsconfig-extend-custom-compiler.json @@ -0,0 +1 @@ +{ "extends": "shared-config/tsconfig-custom-compiler.json" } diff --git a/tests/pluggable-dep-resolution/tsconfig-extend-custom-transpiler.json b/tests/pluggable-dep-resolution/tsconfig-extend-custom-transpiler.json new file mode 100644 index 000000000..afe9b5d7e --- /dev/null +++ b/tests/pluggable-dep-resolution/tsconfig-extend-custom-transpiler.json @@ -0,0 +1 @@ +{ "extends": "shared-config/tsconfig-custom-transpiler.json" } diff --git a/tests/pluggable-dep-resolution/tsconfig-extend-swc-core.json b/tests/pluggable-dep-resolution/tsconfig-extend-swc-core.json new file mode 100644 index 000000000..4ad6e1a89 --- /dev/null +++ b/tests/pluggable-dep-resolution/tsconfig-extend-swc-core.json @@ -0,0 +1 @@ +{ "extends": "shared-config/tsconfig-swc-core.json" } diff --git a/tests/pluggable-dep-resolution/tsconfig-extend-swc-custom-backend.json b/tests/pluggable-dep-resolution/tsconfig-extend-swc-custom-backend.json new file mode 100644 index 000000000..c28b49a1a --- /dev/null +++ b/tests/pluggable-dep-resolution/tsconfig-extend-swc-custom-backend.json @@ -0,0 +1 @@ +{ "extends": "shared-config/tsconfig-swc-custom-backend.json" } diff --git a/tests/pluggable-dep-resolution/tsconfig-extend-swc-wasm.json b/tests/pluggable-dep-resolution/tsconfig-extend-swc-wasm.json new file mode 100644 index 000000000..8acee2395 --- /dev/null +++ b/tests/pluggable-dep-resolution/tsconfig-extend-swc-wasm.json @@ -0,0 +1 @@ +{ "extends": "shared-config/tsconfig-swc-wasm.json" } diff --git a/tests/pluggable-dep-resolution/tsconfig-extend-swc.json b/tests/pluggable-dep-resolution/tsconfig-extend-swc.json new file mode 100644 index 000000000..29827a78a --- /dev/null +++ b/tests/pluggable-dep-resolution/tsconfig-extend-swc.json @@ -0,0 +1 @@ +{ "extends": "shared-config/tsconfig-swc.json" } diff --git a/tests/pluggable-dep-resolution/tsconfig-swc-core.json b/tests/pluggable-dep-resolution/tsconfig-swc-core.json new file mode 100644 index 000000000..8e33432ef --- /dev/null +++ b/tests/pluggable-dep-resolution/tsconfig-swc-core.json @@ -0,0 +1,6 @@ +{ + "ts-node": { + "transpileOnly": true, + "transpiler": ["ts-node/transpilers/swc", { "swc": "@swc/core" }] + } +} diff --git a/tests/pluggable-dep-resolution/tsconfig-swc-custom-backend.json b/tests/pluggable-dep-resolution/tsconfig-swc-custom-backend.json new file mode 100644 index 000000000..7a3d24429 --- /dev/null +++ b/tests/pluggable-dep-resolution/tsconfig-swc-custom-backend.json @@ -0,0 +1,6 @@ +{ + "ts-node": { + "transpileOnly": true, + "transpiler": ["ts-node/transpilers/swc", { "swc": "custom-swc" }] + } +} diff --git a/tests/pluggable-dep-resolution/tsconfig-swc-wasm.json b/tests/pluggable-dep-resolution/tsconfig-swc-wasm.json new file mode 100644 index 000000000..bfa5a0ebe --- /dev/null +++ b/tests/pluggable-dep-resolution/tsconfig-swc-wasm.json @@ -0,0 +1,6 @@ +{ + "ts-node": { + "transpileOnly": true, + "transpiler": ["ts-node/transpilers/swc", { "swc": "@swc/wasm" }] + } +} diff --git a/tests/pluggable-dep-resolution/tsconfig-swc.json b/tests/pluggable-dep-resolution/tsconfig-swc.json new file mode 100644 index 000000000..9f1295318 --- /dev/null +++ b/tests/pluggable-dep-resolution/tsconfig-swc.json @@ -0,0 +1 @@ +{ "ts-node": { "swc": true } } From 0b5a47baa6d413803175e1f494f0cea536519ba1 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Sat, 5 Mar 2022 00:38:30 -0500 Subject: [PATCH 24/27] lint-fix --- src/test/pluggable-dep-resolution.spec.ts | 47 ++++++++++++++++++----- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/src/test/pluggable-dep-resolution.spec.ts b/src/test/pluggable-dep-resolution.spec.ts index cdb351d33..6ee461b16 100644 --- a/src/test/pluggable-dep-resolution.spec.ts +++ b/src/test/pluggable-dep-resolution.spec.ts @@ -28,16 +28,43 @@ test.suite( macro('tsconfig-swc-wasm.json', 'root @swc/wasm'); macro('tsconfig-swc.json', 'root @swc/core'); - macro('node_modules/shared-config/tsconfig-custom-compiler.json', 'shared-config custom compiler'); - macro('node_modules/shared-config/tsconfig-custom-transpiler.json', 'shared-config custom transpiler'); - macro('node_modules/shared-config/tsconfig-swc-custom-backend.json', 'shared-config custom swc backend'); - macro('node_modules/shared-config/tsconfig-swc-core.json', 'shared-config @swc/core'); - macro('node_modules/shared-config/tsconfig-swc-wasm.json', 'shared-config @swc/wasm'); - macro('node_modules/shared-config/tsconfig-swc.json', 'shared-config @swc/core'); - - macro('tsconfig-extend-custom-compiler.json', 'shared-config custom compiler'); - macro('tsconfig-extend-custom-transpiler.json', 'shared-config custom transpiler'); - macro('tsconfig-extend-swc-custom-backend.json', 'shared-config custom swc backend'); + macro( + 'node_modules/shared-config/tsconfig-custom-compiler.json', + 'shared-config custom compiler' + ); + macro( + 'node_modules/shared-config/tsconfig-custom-transpiler.json', + 'shared-config custom transpiler' + ); + macro( + 'node_modules/shared-config/tsconfig-swc-custom-backend.json', + 'shared-config custom swc backend' + ); + macro( + 'node_modules/shared-config/tsconfig-swc-core.json', + 'shared-config @swc/core' + ); + macro( + 'node_modules/shared-config/tsconfig-swc-wasm.json', + 'shared-config @swc/wasm' + ); + macro( + 'node_modules/shared-config/tsconfig-swc.json', + 'shared-config @swc/core' + ); + + macro( + 'tsconfig-extend-custom-compiler.json', + 'shared-config custom compiler' + ); + macro( + 'tsconfig-extend-custom-transpiler.json', + 'shared-config custom transpiler' + ); + macro( + 'tsconfig-extend-swc-custom-backend.json', + 'shared-config custom swc backend' + ); macro('tsconfig-extend-swc-core.json', 'shared-config @swc/core'); macro('tsconfig-extend-swc-wasm.json', 'shared-config @swc/wasm'); macro('tsconfig-extend-swc.json', 'shared-config @swc/core'); From 3f21c317038fcd82bf03ee7894e22ef4a70b190b Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Sat, 5 Mar 2022 02:05:51 -0500 Subject: [PATCH 25/27] test cleanup, remove or cleanup version checks, skip failing tsconfig "extends" tests on TS 2.7 --- src/test/helpers.ts | 42 +++++-- src/test/index.spec.ts | 133 +++++++++++----------- src/test/pluggable-dep-resolution.spec.ts | 48 +++++--- src/test/testlib.ts | 5 + 4 files changed, 132 insertions(+), 96 deletions(-) diff --git a/src/test/helpers.ts b/src/test/helpers.ts index fd1643614..cddf7575d 100644 --- a/src/test/helpers.ts +++ b/src/test/helpers.ts @@ -17,17 +17,7 @@ import semver = require('semver'); const createRequire: typeof _createRequire = require('create-require'); export { tsNodeTypes }; -export const nodeSupportsEsmHooks = semver.gte(process.version, '12.16.0'); -export const nodeSupportsSpawningChildProcess = semver.gte( - process.version, - '12.17.0' -); -export const nodeUsesNewHooksApi = semver.gte(process.version, '16.12.0'); -export const nodeSupportsImportAssertions = semver.gte( - process.version, - '17.1.0' -); - +//#region Paths export const ROOT_DIR = resolve(__dirname, '../..'); export const DIST_DIR = resolve(__dirname, '..'); export const TEST_DIR = join(__dirname, '../../tests'); @@ -40,6 +30,9 @@ export const BIN_SCRIPT_PATH = join( ); export const BIN_CWD_PATH = join(TEST_DIR, 'node_modules/.bin/ts-node-cwd'); export const BIN_ESM_PATH = join(TEST_DIR, 'node_modules/.bin/ts-node-esm'); +//#endregion + +//#region command lines /** Default `ts-node --project` invocation */ export const CMD_TS_NODE_WITH_PROJECT_FLAG = `"${BIN_PATH}" --project "${PROJECT}"`; /** Default `ts-node` invocation without `--project` */ @@ -48,12 +41,33 @@ export const EXPERIMENTAL_MODULES_FLAG = semver.gte(process.version, '12.17.0') ? '' : '--experimental-modules'; export const CMD_ESM_LOADER_WITHOUT_PROJECT = `node ${EXPERIMENTAL_MODULES_FLAG} --loader ts-node/esm`; +//#endregion // `createRequire` does not exist on older node versions export const testsDirRequire = createRequire(join(TEST_DIR, 'index.js')); export const ts = testsDirRequire('typescript'); +//#region version checks +export const nodeSupportsEsmHooks = semver.gte(process.version, '12.16.0'); +export const nodeSupportsSpawningChildProcess = semver.gte( + process.version, + '12.17.0' +); +export const nodeUsesNewHooksApi = semver.gte(process.version, '16.12.0'); +export const nodeSupportsImportAssertions = semver.gte( + process.version, + '17.1.0' +); +/** Supports tsconfig "extends" >= v3.2.0 */ +export const tsSupportsTsconfigInheritanceViaNodePackages = semver.gte( + ts.version, + '3.2.0' +); +/** Supports --showConfig: >= v3.2.0 */ +export const tsSupportsShowConfig = semver.gte(ts.version, '3.2.0'); +//#endregion + export const xfs = new NodeFS(fs); /** Pass to `test.context()` to get access to the ts-node API under test */ @@ -65,6 +79,7 @@ export const contextTsNodeUnderTest = once(async () => { }; }); +//#region install ts-node tarball const ts_node_install_lock = process.env.ts_node_install_lock as string; const lockPath = join(__dirname, ts_node_install_lock); @@ -133,6 +148,7 @@ async function lockedMemoizedOperation( releaseLock(); } } +//#endregion /** * Get a stream into a string. @@ -170,6 +186,8 @@ export function getStream(stream: Readable, waitForPattern?: string | RegExp) { } } +//#region Reset node environment + const defaultRequireExtensions = captureObjectState(require.extensions); const defaultProcess = captureObjectState(process); const defaultModule = captureObjectState(require('module')); @@ -230,4 +248,6 @@ function resetObject( Object.defineProperties(object, state.descriptors); } +//#endregion + export const delay = promisify(setTimeout); diff --git a/src/test/index.spec.ts b/src/test/index.spec.ts index 5487a7b64..72faf86ea 100644 --- a/src/test/index.spec.ts +++ b/src/test/index.spec.ts @@ -3,7 +3,13 @@ import * as expect from 'expect'; import { join, resolve, sep as pathSep } from 'path'; import { tmpdir } from 'os'; import semver = require('semver'); -import { BIN_PATH_JS, nodeSupportsEsmHooks, ts } from './helpers'; +import { + BIN_PATH_JS, + nodeSupportsEsmHooks, + ts, + tsSupportsShowConfig, + tsSupportsTsconfigInheritanceViaNodePackages, +} from './helpers'; import { lstatSync, mkdtempSync } from 'fs'; import { npath } from '@yarnpkg/fslib'; import type _createRequire from 'create-require'; @@ -167,31 +173,29 @@ test.suite('ts-node', (test) => { expect(stdout).toBe('object\n'); }); - if (semver.gte(ts.version, '1.8.0')) { - test('should allow js', async () => { - const { err, stdout } = await exec( - [ - CMD_TS_NODE_WITH_PROJECT_FLAG, - '-O "{\\"allowJs\\":true}"', - '-pe "import { main } from \'./allow-js/run\';main()"', - ].join(' ') - ); - expect(err).toBe(null); - expect(stdout).toBe('hello world\n'); - }); + test('should allow js', async () => { + const { err, stdout } = await exec( + [ + CMD_TS_NODE_WITH_PROJECT_FLAG, + '-O "{\\"allowJs\\":true}"', + '-pe "import { main } from \'./allow-js/run\';main()"', + ].join(' ') + ); + expect(err).toBe(null); + expect(stdout).toBe('hello world\n'); + }); - test('should include jsx when `allow-js` true', async () => { - const { err, stdout } = await exec( - [ - CMD_TS_NODE_WITH_PROJECT_FLAG, - '-O "{\\"allowJs\\":true}"', - '-pe "import { Foo2 } from \'./allow-js/with-jsx\'; Foo2.sayHi()"', - ].join(' ') - ); - expect(err).toBe(null); - expect(stdout).toBe('hello world\n'); - }); - } + test('should include jsx when `allow-js` true', async () => { + const { err, stdout } = await exec( + [ + CMD_TS_NODE_WITH_PROJECT_FLAG, + '-O "{\\"allowJs\\":true}"', + '-pe "import { Foo2 } from \'./allow-js/with-jsx\'; Foo2.sayHi()"', + ].join(' ') + ); + expect(err).toBe(null); + expect(stdout).toBe('hello world\n'); + }); test('should eval code', async () => { const { err, stdout } = await exec( @@ -501,21 +505,16 @@ test.suite('ts-node', (test) => { }); test.suite('issue #884', (test) => { + // TODO disabled because it consistently fails on Windows on TS 2.7 + test.skipIf( + process.platform === 'win32' && semver.satisfies(ts.version, '2.7') + ); test('should compile', async (t) => { - // TODO disabled because it consistently fails on Windows on TS 2.7 - if ( - process.platform === 'win32' && - semver.satisfies(ts.version, '2.7') - ) { - t.log('Skipping'); - return; - } else { - const { err, stdout } = await exec( - `"${BIN_PATH}" --project issue-884/tsconfig.json issue-884` - ); - expect(err).toBe(null); - expect(stdout).toBe(''); - } + const { err, stdout } = await exec( + `"${BIN_PATH}" --project issue-884/tsconfig.json issue-884` + ); + expect(err).toBe(null); + expect(stdout).toBe(''); }); }); @@ -706,7 +705,7 @@ test.suite('ts-node', (test) => { ]); }); - if (semver.gte(ts.version, '3.2.0')) { + if (tsSupportsTsconfigInheritanceViaNodePackages) { test('should pull ts-node options from extended `tsconfig.json`', async () => { const { err, stdout } = await exec( `${BIN_PATH} --show-config --project ./tsconfig-extends/tsconfig.json` @@ -810,33 +809,33 @@ test.suite('ts-node', (test) => { } ); - if (semver.gte(ts.version, '3.2.0')) { - test.suite( - 'should bundle @tsconfig/bases to be used in your own tsconfigs', - (test) => { - const macro = test.macro((nodeVersion: string) => async (t) => { - const config = require(`@tsconfig/${nodeVersion}/tsconfig.json`); - const { err, stdout, stderr } = await exec( - `${BIN_PATH} --showConfig -e 10n`, - { - cwd: join(TEST_DIR, 'tsconfig-bases', nodeVersion), - } - ); - expect(err).toBe(null); - t.like(JSON.parse(stdout), { - compilerOptions: { - target: config.compilerOptions.target, - lib: config.compilerOptions.lib, - }, - }); + test.suite( + 'should bundle @tsconfig/bases to be used in your own tsconfigs', + (test) => { + test.runIf(tsSupportsTsconfigInheritanceViaNodePackages); + + const macro = test.macro((nodeVersion: string) => async (t) => { + const config = require(`@tsconfig/${nodeVersion}/tsconfig.json`); + const { err, stdout, stderr } = await exec( + `${BIN_PATH} --showConfig -e 10n`, + { + cwd: join(TEST_DIR, 'tsconfig-bases', nodeVersion), + } + ); + expect(err).toBe(null); + t.like(JSON.parse(stdout), { + compilerOptions: { + target: config.compilerOptions.target, + lib: config.compilerOptions.lib, + }, }); - test(`ts-node/node10/tsconfig.json`, macro, 'node10'); - test(`ts-node/node12/tsconfig.json`, macro, 'node12'); - test(`ts-node/node14/tsconfig.json`, macro, 'node14'); - test(`ts-node/node16/tsconfig.json`, macro, 'node16'); - } - ); - } + }); + test(`ts-node/node10/tsconfig.json`, macro, 'node10'); + test(`ts-node/node12/tsconfig.json`, macro, 'node12'); + test(`ts-node/node14/tsconfig.json`, macro, 'node14'); + test(`ts-node/node16/tsconfig.json`, macro, 'node16'); + } + ); test.suite('compiler host', (test) => { test('should execute cli', async () => { @@ -896,7 +895,7 @@ test.suite('ts-node', (test) => { }); }); - if (semver.gte(ts.version, '3.2.0')) { + if (tsSupportsShowConfig) { test('--showConfig should log resolved configuration', async (t) => { function native(path: string) { return path.replace(/\/|\\/g, pathSep); diff --git a/src/test/pluggable-dep-resolution.spec.ts b/src/test/pluggable-dep-resolution.spec.ts index 6ee461b16..95504351b 100644 --- a/src/test/pluggable-dep-resolution.spec.ts +++ b/src/test/pluggable-dep-resolution.spec.ts @@ -1,5 +1,9 @@ import { context } from './testlib'; -import { contextTsNodeUnderTest, resetNodeEnvironment } from './helpers'; +import { + contextTsNodeUnderTest, + resetNodeEnvironment, + tsSupportsTsconfigInheritanceViaNodePackages, +} from './helpers'; import * as expect from 'expect'; import { resolve } from 'path'; @@ -21,6 +25,8 @@ test.suite( // ts-node should resolve ts-patch or @swc/core relative to the extended tsconfig // to ensure we use the known working versions. + const macro = _macro.bind(null, test); + macro('tsconfig-custom-compiler.json', 'root custom compiler'); macro('tsconfig-custom-transpiler.json', 'root custom transpiler'); macro('tsconfig-swc-custom-backend.json', 'root custom swc backend'); @@ -53,24 +59,30 @@ test.suite( 'shared-config @swc/core' ); - macro( - 'tsconfig-extend-custom-compiler.json', - 'shared-config custom compiler' - ); - macro( - 'tsconfig-extend-custom-transpiler.json', - 'shared-config custom transpiler' - ); - macro( - 'tsconfig-extend-swc-custom-backend.json', - 'shared-config custom swc backend' - ); - macro('tsconfig-extend-swc-core.json', 'shared-config @swc/core'); - macro('tsconfig-extend-swc-wasm.json', 'shared-config @swc/wasm'); - macro('tsconfig-extend-swc.json', 'shared-config @swc/core'); + test.suite('"extends"', (test) => { + test.runIf(tsSupportsTsconfigInheritanceViaNodePackages); + + const macro = _macro.bind(null, test); + + macro( + 'tsconfig-extend-custom-compiler.json', + 'shared-config custom compiler' + ); + macro( + 'tsconfig-extend-custom-transpiler.json', + 'shared-config custom transpiler' + ); + macro( + 'tsconfig-extend-swc-custom-backend.json', + 'shared-config custom swc backend' + ); + macro('tsconfig-extend-swc-core.json', 'shared-config @swc/core'); + macro('tsconfig-extend-swc-wasm.json', 'shared-config @swc/wasm'); + macro('tsconfig-extend-swc.json', 'shared-config @swc/core'); + }); - function macro(config: string, expected: string) { - test(`${config} uses ${expected}`, async (t) => { + function _macro(_test: typeof test, config: string, expected: string) { + _test(`${config} uses ${expected}`, async (t) => { t.teardown(resetNodeEnvironment); const output = t.context.tsNodeUnderTest diff --git a/src/test/testlib.ts b/src/test/testlib.ts index c99f3878c..6304164bb 100644 --- a/src/test/testlib.ts +++ b/src/test/testlib.ts @@ -115,6 +115,8 @@ export interface TestInterface< skipUnless(conditional: boolean): void; /** If conditional is true, run tests, otherwise skip them */ runIf(conditional: boolean): void; + /** If conditional is false, skip tests */ + skipIf(conditional: boolean): void; // TODO add teardownEach } @@ -286,5 +288,8 @@ function createTestInterface(opts: { assertOrderingForDeclaringSkipUnless(); automaticallySkip = automaticallySkip || !runIfTrue; }; + test.skipIf = function (skipIfTrue: boolean) { + test.runIf(!skipIfTrue); + }; return test as any; } From 611adc23a66cc6920e13b6ff5baad451debbc69f Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Sat, 5 Mar 2022 12:28:48 -0500 Subject: [PATCH 26/27] ensure tests are forced to install and use most recent ts-node tarball and cannot accidentally use project-root nor outdated tarball --- ava.config.cjs | 39 +++++++++++++++++++++++++++++++++++++++ ava.config.js | 15 --------------- package-lock.json | 27 +++++++++++++++++++++++++++ package.json | 1 + 4 files changed, 67 insertions(+), 15 deletions(-) create mode 100644 ava.config.cjs delete mode 100644 ava.config.js diff --git a/ava.config.cjs b/ava.config.cjs new file mode 100644 index 000000000..1bed11b9e --- /dev/null +++ b/ava.config.cjs @@ -0,0 +1,39 @@ +const expect = require('expect'); +const { createRequire } = require('module'); + +module.exports = { + files: ['dist/test/**/*.spec.js'], + failWithoutAssertions: false, + environmentVariables: { + ts_node_install_lock: `id-${Math.floor(Math.random() * 10e9)}`, + // Force jest expect() errors to generate colorized strings, makes output more readable. + // Delete the env var within ava processes via `require` option below. + // This avoids passing it to spawned processes under test, which would negatively affect + // their behavior. + FORCE_COLOR: '3', + }, + require: ['./src/test/remove-env-var-force-color.js'], + timeout: '300s', + concurrency: 1, +}; + +{ + /* + * Tests *must* install and use our most recent ts-node tarball. + * We must prevent them from accidentally require-ing a different version of + * ts-node, from either node_modules or tests/node_modules + */ + + const { rmSync, existsSync } = require('fs-extra'); + const { resolve } = require('path'); + + remove(resolve(__dirname, 'node_modules/ts-node')); + remove(resolve(__dirname, 'tests/node_modules/ts-node')); + + // Prove that we did it correctly + expect(() => {createRequire(resolve(__dirname, 'tests/foo.js')).resolve('ts-node')}).toThrow(); + + function remove(p) { + if(existsSync(p)) rmSync(p, {recursive: true}) + } +} diff --git a/ava.config.js b/ava.config.js deleted file mode 100644 index 915de176d..000000000 --- a/ava.config.js +++ /dev/null @@ -1,15 +0,0 @@ -export default { - files: ['dist/test/**/*.spec.js'], - failWithoutAssertions: false, - environmentVariables: { - ts_node_install_lock: `id-${Math.floor(Math.random() * 10e9)}`, - // Force jest expect() errors to generate colorized strings, makes output more readable. - // Delete the env var within ava processes via `require` option below. - // This avoids passing it to spawned processes under test, which would negatively affect - // their behavior. - FORCE_COLOR: '3', - }, - require: ['./src/test/remove-env-var-force-color.js'], - timeout: '300s', - concurrency: 1, -}; diff --git a/package-lock.json b/package-lock.json index 3ad590ffe..df5013119 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2376,6 +2376,17 @@ "integrity": "sha512-33X7H/wdfO99GdRLLgkjUrD4geAFdq/Uv0kl3HD4da6HDixd2GUg8Mw7dahLCV9r/EARkmtYBB6Tch4EEokFTQ==", "dev": true }, + "fs-extra": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.1.tgz", + "integrity": "sha512-NbdoVMZso2Lsrn/QwLXOy6rm0ufY2zEOKCDzJR/0kBsb0E6qed0P3iYK+Ath3BfvXEeu4JhEtXLgILx5psUfag==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3031,6 +3042,16 @@ "integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==", "dev": true }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, "keyv": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", @@ -4767,6 +4788,12 @@ "crypto-random-string": "^2.0.0" } }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true + }, "update-notifier": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-5.1.0.tgz", diff --git a/package.json b/package.json index 84a5ad16d..d9c32a5ce 100644 --- a/package.json +++ b/package.json @@ -127,6 +127,7 @@ "axios": "^0.21.1", "chai": "^4.0.1", "expect": "^27.0.2", + "fs-extra": "^10.0.1", "get-stream": "^6.0.0", "lodash": "^4.17.15", "ntypescript": "^1.201507091536.1", From f1326e21ed3235b5f25e1910674a397f7e24f31c Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Sat, 5 Mar 2022 13:44:21 -0500 Subject: [PATCH 27/27] fix absence of fs method on old node --- ava.config.cjs | 5 +++-- package-lock.json | 27 --------------------------- package.json | 1 - 3 files changed, 3 insertions(+), 30 deletions(-) diff --git a/ava.config.cjs b/ava.config.cjs index 1bed11b9e..aa04b33bf 100644 --- a/ava.config.cjs +++ b/ava.config.cjs @@ -24,7 +24,8 @@ module.exports = { * ts-node, from either node_modules or tests/node_modules */ - const { rmSync, existsSync } = require('fs-extra'); + const { existsSync } = require('fs'); + const rimraf = require('rimraf'); const { resolve } = require('path'); remove(resolve(__dirname, 'node_modules/ts-node')); @@ -34,6 +35,6 @@ module.exports = { expect(() => {createRequire(resolve(__dirname, 'tests/foo.js')).resolve('ts-node')}).toThrow(); function remove(p) { - if(existsSync(p)) rmSync(p, {recursive: true}) + if(existsSync(p)) rimraf.sync(p, {recursive: true}) } } diff --git a/package-lock.json b/package-lock.json index df5013119..3ad590ffe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2376,17 +2376,6 @@ "integrity": "sha512-33X7H/wdfO99GdRLLgkjUrD4geAFdq/Uv0kl3HD4da6HDixd2GUg8Mw7dahLCV9r/EARkmtYBB6Tch4EEokFTQ==", "dev": true }, - "fs-extra": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.1.tgz", - "integrity": "sha512-NbdoVMZso2Lsrn/QwLXOy6rm0ufY2zEOKCDzJR/0kBsb0E6qed0P3iYK+Ath3BfvXEeu4JhEtXLgILx5psUfag==", - "dev": true, - "requires": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - } - }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3042,16 +3031,6 @@ "integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==", "dev": true }, - "jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6", - "universalify": "^2.0.0" - } - }, "keyv": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", @@ -4788,12 +4767,6 @@ "crypto-random-string": "^2.0.0" } }, - "universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - "dev": true - }, "update-notifier": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-5.1.0.tgz", diff --git a/package.json b/package.json index d9c32a5ce..84a5ad16d 100644 --- a/package.json +++ b/package.json @@ -127,7 +127,6 @@ "axios": "^0.21.1", "chai": "^4.0.1", "expect": "^27.0.2", - "fs-extra": "^10.0.1", "get-stream": "^6.0.0", "lodash": "^4.17.15", "ntypescript": "^1.201507091536.1",