From 796b5932186869cb9db7c2ab0b3994030061aa33 Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Fri, 24 Jun 2022 13:51:25 +0000 Subject: [PATCH 01/11] Fix ESM node processes being unable to fork into other scripts Currently, Node processes instantiated through the `--esm` flag result in a child process being created so that the ESM loader can be registered. This works fine and is reasonable. The child process approach to register ESM hooks currently prevents the NodeJS `fork` method from being used because the `execArgv` propagated into forked processes causes `ts-node` (which is also propagated as child exec script -- this is good because it allows nested type resolution to work) to always execute the original entry-point, causing potential infinite loops because the designated fork module script is not executed as expected. This commit fixes this by not encoding the entry-point information into the state that is captured as part of the `execArgv`. Instead the entry-point information is always retrieved from the parsed rest command line arguments in the final stage (`phase4`). Fixes #1812. --- src/bin.ts | 125 +++++++++++++----- src/child/child-entrypoint.ts | 3 +- src/test/esm-loader.spec.ts | 40 ++++++ .../index.mts | 23 ++++ .../tsconfig.json | 5 + .../worker.mts | 3 + .../process-forking-nested-esm/index.ts | 20 +++ .../process-forking-nested-esm/package.json | 3 + .../process-forking-nested-esm/tsconfig.json | 5 + .../process-forking-nested-esm/worker.ts | 3 + .../process-forking/index.ts | 20 +++ .../process-forking/package.json | 3 + .../process-forking/tsconfig.json | 5 + .../process-forking/worker.js | 1 + 14 files changed, 224 insertions(+), 35 deletions(-) create mode 100644 tests/esm-child-process/process-forking-nested-esm-node-next/index.mts create mode 100644 tests/esm-child-process/process-forking-nested-esm-node-next/tsconfig.json create mode 100644 tests/esm-child-process/process-forking-nested-esm-node-next/worker.mts create mode 100644 tests/esm-child-process/process-forking-nested-esm/index.ts create mode 100644 tests/esm-child-process/process-forking-nested-esm/package.json create mode 100644 tests/esm-child-process/process-forking-nested-esm/tsconfig.json create mode 100644 tests/esm-child-process/process-forking-nested-esm/worker.ts create mode 100644 tests/esm-child-process/process-forking/index.ts create mode 100644 tests/esm-child-process/process-forking/package.json create mode 100644 tests/esm-child-process/process-forking/tsconfig.json create mode 100644 tests/esm-child-process/process-forking/worker.js diff --git a/src/bin.ts b/src/bin.ts index 8b5f91767..93f078658 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -48,7 +48,7 @@ export function main( const state: BootstrapState = { shouldUseChildProcess: false, isInChildProcess: false, - entrypoint: __filename, + tsNodeScript: __filename, parseArgvResult: args, }; return bootstrap(state); @@ -62,7 +62,7 @@ export function main( export interface BootstrapState { isInChildProcess: boolean; shouldUseChildProcess: boolean; - entrypoint: string; + tsNodeScript: string; parseArgvResult: ReturnType; phase2Result?: ReturnType; phase3Result?: ReturnType; @@ -264,8 +264,7 @@ function parseArgv(argv: string[], entrypointArgs: Record) { } function phase2(payload: BootstrapState) { - const { help, version, code, interactive, cwdArg, restArgs, esm } = - payload.parseArgvResult; + const { help, version, cwdArg, esm } = payload.parseArgvResult; if (help) { console.log(` @@ -319,28 +318,16 @@ Options: process.exit(0); } - // 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 && restArgs.length); - const executeEntrypoint = !executeEval && restArgs.length > 0; - const executeRepl = - !executeEntrypoint && - (interactive || (process.stdin.isTTY && !executeEval)); - const executeStdin = !executeEval && !executeRepl && !executeEntrypoint; - const cwd = cwdArg || process.cwd(); - /** 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; + // If ESM is explicitly enabled through the flag, stage3 should be run in a child process + // with the ESM loaders configured. + if (esm) { + payload.shouldUseChildProcess = true; + } + return { - executeEval, - executeEntrypoint, - executeRepl, - executeStdin, cwd, - scriptPath, }; } @@ -372,7 +359,18 @@ function phase3(payload: BootstrapState) { esm, experimentalSpecifierResolution, } = payload.parseArgvResult; - const { cwd, scriptPath } = payload.phase2Result!; + const { cwd } = payload.phase2Result!; + + // NOTE: When we transition to a child process for ESM, the entry-point script determined + // here might not be the one used later in `phase4`. This can happen when we execute the + // original entry-point but then the process forks itself using e.g. `child_process.fork`. + // We will always use the original TS project in forked processes anyway, so it is + // expected and acceptable to retrieve the entry-point information here in `phase2`. + // See: https://github.com/TypeStrong/ts-node/issues/1812. + const { entryPointPath } = getEntryPointInfo( + payload.parseArgvResult!, + payload.phase2Result! + ); const preloadedConfig = findAndReadConfig({ cwd, @@ -387,7 +385,12 @@ function phase3(payload: BootstrapState) { compilerHost, ignore, logError, - projectSearchDir: getProjectSearchDir(cwd, scriptMode, cwdMode, scriptPath), + projectSearchDir: getProjectSearchDir( + cwd, + scriptMode, + cwdMode, + entryPointPath + ), project, skipProject, skipIgnore, @@ -403,23 +406,76 @@ function phase3(payload: BootstrapState) { experimentalSpecifierResolution as ExperimentalSpecifierResolution, }); - if (preloadedConfig.options.esm) payload.shouldUseChildProcess = true; + // If ESM is enabled through the parsed tsconfig, stage4 should be run in a child + // process with the ESM loaders configured. + if (preloadedConfig.options.esm) { + payload.shouldUseChildProcess = true; + } + return { preloadedConfig }; } +/** + * Determines the entry-point information from the argv and phase2 result. This + * method will be invoked in two places: + * + * 1. In phase 3 to be able to find a project from the potential entry-point script. + * 2. In phase 4 to determine the actual entry-point script. + * + * Note that we need to explicitly re-resolve the entry-point information in the final + * stage because the previous stage information could be modified when the bootstrap + * invocation transitioned into a child process for ESM. + * + * Stages before (phase 4) can and will be cached by the child process through the Brotli + * configuration and entry-point information is only reliable in the final phase. More + * details can be found in here: https://github.com/TypeStrong/ts-node/issues/1812. + */ +function getEntryPointInfo( + argvResult: NonNullable, + phase2Result: NonNullable +) { + const { code, interactive, restArgs } = argvResult; + const { cwd } = phase2Result; + + // 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 && restArgs.length); + const executeEntrypoint = !executeEval && restArgs.length > 0; + const executeRepl = + !executeEntrypoint && + (interactive || (process.stdin.isTTY && !executeEval)); + const executeStdin = !executeEval && !executeRepl && !executeEntrypoint; + + /** Unresolved. May point to a symlink, not realpath. May be missing file extension */ + const entryPointPath = executeEntrypoint + ? resolve(cwd, restArgs[0]) + : undefined; + + return { + executeEval, + executeEntrypoint, + executeRepl, + executeStdin, + entryPointPath, + }; +} + function phase4(payload: BootstrapState) { - const { isInChildProcess, entrypoint } = payload; + const { isInChildProcess, tsNodeScript } = payload; const { version, showConfig, restArgs, code, print, argv } = payload.parseArgvResult; + const { cwd } = payload.phase2Result!; + const { preloadedConfig } = payload.phase3Result!; + const { + entryPointPath, + executeEntrypoint, executeEval, - cwd, - executeStdin, executeRepl, - executeEntrypoint, - scriptPath, - } = payload.phase2Result!; - const { preloadedConfig } = payload.phase3Result!; + executeStdin, + } = getEntryPointInfo(payload.parseArgvResult!, payload.phase2Result!); + /** * , [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. @@ -566,12 +622,13 @@ function phase4(payload: BootstrapState) { // Prepend `ts-node` arguments to CLI for child processes. process.execArgv.push( - entrypoint, + tsNodeScript, ...argv.slice(2, argv.length - restArgs.length) ); + // TODO this comes from BoostrapState process.argv = [process.argv[1]] - .concat(executeEntrypoint ? ([scriptPath] as string[]) : []) + .concat(executeEntrypoint ? ([entryPointPath] as string[]) : []) .concat(restArgs.slice(executeEntrypoint ? 1 : 0)); // Execute the main contents (either eval, script or piped). diff --git a/src/child/child-entrypoint.ts b/src/child/child-entrypoint.ts index 03a02d2e9..55d77ad9d 100644 --- a/src/child/child-entrypoint.ts +++ b/src/child/child-entrypoint.ts @@ -8,8 +8,9 @@ const base64Payload = base64ConfigArg.slice(argPrefix.length); const payload = JSON.parse( brotliDecompressSync(Buffer.from(base64Payload, 'base64')).toString() ) as BootstrapState; + payload.isInChildProcess = true; -payload.entrypoint = __filename; +payload.tsNodeScript = __filename; payload.parseArgvResult.argv = process.argv; payload.parseArgvResult.restArgs = process.argv.slice(3); diff --git a/src/test/esm-loader.spec.ts b/src/test/esm-loader.spec.ts index 41c421fd6..f12e04821 100644 --- a/src/test/esm-loader.spec.ts +++ b/src/test/esm-loader.spec.ts @@ -22,6 +22,7 @@ import { TEST_DIR, tsSupportsImportAssertions, tsSupportsResolveJsonModule, + tsSupportsStableNodeNextNode16, } from './helpers'; import { createExec, createSpawn, ExecReturn } from './exec-helpers'; import { join, resolve } from 'path'; @@ -358,6 +359,45 @@ test.suite('esm', (test) => { }); } + test.suite('esm child process and forking', (test) => { + test('should be able to fork vanilla NodeJS script', async () => { + const { err, stdout, stderr } = await exec( + `${BIN_PATH} --esm --cwd ./esm-child-process/process-forking/ index.ts` + ); + + expect(err).toBe(null); + expect(stdout.trim()).toBe('Passing: from main'); + expect(stderr).toBe(''); + }); + + test('should be able to fork into a nested TypeScript ESM script', async () => { + const { err, stdout, stderr } = await exec( + `${BIN_PATH} --esm --cwd ./esm-child-process/process-forking-nested-esm/ index.ts` + ); + + expect(err).toBe(null); + expect(stdout.trim()).toBe('Passing: from main'); + expect(stderr).toBe(''); + }); + + test.suite( + 'with NodeNext TypeScript resolution and `.mts` extension', + (test) => { + test.runIf(tsSupportsStableNodeNextNode16); + + test('should be able to fork into a nested TypeScript ESM script', async () => { + const { err, stdout, stderr } = await exec( + `${BIN_PATH} --esm ./esm-child-process/process-forking-nested-esm-node-next/index.mts` + ); + + expect(err).toBe(null); + expect(stdout.trim()).toBe('Passing: from main'); + expect(stderr).toBe(''); + }); + } + ); + }); + test.suite('parent passes signals to child', (test) => { test.runSerially(); diff --git a/tests/esm-child-process/process-forking-nested-esm-node-next/index.mts b/tests/esm-child-process/process-forking-nested-esm-node-next/index.mts new file mode 100644 index 000000000..e76286d8e --- /dev/null +++ b/tests/esm-child-process/process-forking-nested-esm-node-next/index.mts @@ -0,0 +1,23 @@ +import { fork } from 'child_process'; +import { dirname, join } from 'path'; +import { fileURLToPath } from 'url'; + +// Initially set the exit code to non-zero. We only set it to `0` when the +// worker process finishes properly with the expected stdout message. +process.exitCode = 1; + +const projectDir = dirname(fileURLToPath(import.meta.url)); +const workerProcess = fork(join(projectDir, 'worker.mts'), [], { + stdio: 'pipe', +}); + +let stdout = ''; + +workerProcess.stdout.on('data', (chunk) => (stdout += chunk.toString('utf8'))); +workerProcess.on('error', () => (process.exitCode = 1)); +workerProcess.on('close', (status, signal) => { + if (status === 0 && signal === null && stdout.trim() === 'Works') { + console.log('Passing: from main'); + process.exitCode = 0; + } +}); diff --git a/tests/esm-child-process/process-forking-nested-esm-node-next/tsconfig.json b/tests/esm-child-process/process-forking-nested-esm-node-next/tsconfig.json new file mode 100644 index 000000000..3998b5074 --- /dev/null +++ b/tests/esm-child-process/process-forking-nested-esm-node-next/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "module": "NodeNext" + } +} diff --git a/tests/esm-child-process/process-forking-nested-esm-node-next/worker.mts b/tests/esm-child-process/process-forking-nested-esm-node-next/worker.mts new file mode 100644 index 000000000..4114d5ab0 --- /dev/null +++ b/tests/esm-child-process/process-forking-nested-esm-node-next/worker.mts @@ -0,0 +1,3 @@ +const message: string = 'Works'; + +console.log(message); diff --git a/tests/esm-child-process/process-forking-nested-esm/index.ts b/tests/esm-child-process/process-forking-nested-esm/index.ts new file mode 100644 index 000000000..ff0f2e61b --- /dev/null +++ b/tests/esm-child-process/process-forking-nested-esm/index.ts @@ -0,0 +1,20 @@ +import { fork } from 'child_process'; + +// Initially set the exit code to non-zero. We only set it to `0` when the +// worker process finishes properly with the expected stdout message. +process.exitCode = 1; + +const workerProcess = fork('./worker.ts', [], { + stdio: 'pipe', +}); + +let stdout = ''; + +workerProcess.stdout.on('data', (chunk) => (stdout += chunk.toString('utf8'))); +workerProcess.on('error', () => (process.exitCode = 1)); +workerProcess.on('close', (status, signal) => { + if (status === 0 && signal === null && stdout.trim() === 'Works') { + console.log('Passing: from main'); + process.exitCode = 0; + } +}); diff --git a/tests/esm-child-process/process-forking-nested-esm/package.json b/tests/esm-child-process/process-forking-nested-esm/package.json new file mode 100644 index 000000000..3dbc1ca59 --- /dev/null +++ b/tests/esm-child-process/process-forking-nested-esm/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/tests/esm-child-process/process-forking-nested-esm/tsconfig.json b/tests/esm-child-process/process-forking-nested-esm/tsconfig.json new file mode 100644 index 000000000..1ac61592b --- /dev/null +++ b/tests/esm-child-process/process-forking-nested-esm/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "module": "ESNext" + } +} diff --git a/tests/esm-child-process/process-forking-nested-esm/worker.ts b/tests/esm-child-process/process-forking-nested-esm/worker.ts new file mode 100644 index 000000000..4114d5ab0 --- /dev/null +++ b/tests/esm-child-process/process-forking-nested-esm/worker.ts @@ -0,0 +1,3 @@ +const message: string = 'Works'; + +console.log(message); diff --git a/tests/esm-child-process/process-forking/index.ts b/tests/esm-child-process/process-forking/index.ts new file mode 100644 index 000000000..2e3e05ec8 --- /dev/null +++ b/tests/esm-child-process/process-forking/index.ts @@ -0,0 +1,20 @@ +import { fork } from 'child_process'; + +// Initially set the exit code to non-zero. We only set it to `0` when the +// worker process finishes properly with the expected stdout message. +process.exitCode = 1; + +const workerProcess = fork('./worker.js', [], { + stdio: 'pipe', +}); + +let stdout = ''; + +workerProcess.stdout.on('data', (chunk) => (stdout += chunk.toString('utf8'))); +workerProcess.on('error', () => (process.exitCode = 1)); +workerProcess.on('close', (status, signal) => { + if (status === 0 && signal === null && stdout.trim() === 'Works') { + console.log('Passing: from main'); + process.exitCode = 0; + } +}); diff --git a/tests/esm-child-process/process-forking/package.json b/tests/esm-child-process/process-forking/package.json new file mode 100644 index 000000000..3dbc1ca59 --- /dev/null +++ b/tests/esm-child-process/process-forking/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/tests/esm-child-process/process-forking/tsconfig.json b/tests/esm-child-process/process-forking/tsconfig.json new file mode 100644 index 000000000..1ac61592b --- /dev/null +++ b/tests/esm-child-process/process-forking/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "module": "ESNext" + } +} diff --git a/tests/esm-child-process/process-forking/worker.js b/tests/esm-child-process/process-forking/worker.js new file mode 100644 index 000000000..820d10b2e --- /dev/null +++ b/tests/esm-child-process/process-forking/worker.js @@ -0,0 +1 @@ +console.log('Works'); From d78e437f21dfcaf5fe6399360e7a9fd61167c505 Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Sun, 3 Jul 2022 10:37:06 +0000 Subject: [PATCH 02/11] Fix `--cwd` to actually set the working directory and work with ESM child process Currently the `--esm` option does not necessarily do what the documentation suggests. i.e. the script does not run as if the working directory is the specified directory. This commit fixes this, so that the option is useful for TSConfig resolution, as well as for controlling the script working directory. Also fixes that the CWD encoded in the bootstrap brotli state for the ESM child process messes with the entry-point resolution, if e.g. the entry-point in `child_process.fork` is relative to a specified `cwd`. --- src/bin.ts | 89 +++++++++++++------ src/child/spawn-child.ts | 10 ++- src/test/esm-loader.spec.ts | 43 +++++++++ src/test/index.spec.ts | 24 +++++ .../process-forking-nested-relative/index.ts | 22 +++++ .../package.json | 3 + .../subfolder/worker.ts | 3 + .../tsconfig.json | 5 ++ tests/working-dir/cjs/index.ts | 7 ++ tests/working-dir/esm-node-next/index.ts | 11 +++ tests/working-dir/esm-node-next/package.json | 3 + tests/working-dir/esm-node-next/tsconfig.json | 5 ++ tests/working-dir/esm/index.ts | 8 ++ tests/working-dir/esm/package.json | 3 + tests/working-dir/esm/tsconfig.json | 5 ++ tests/working-dir/forking/index.ts | 22 +++++ tests/working-dir/forking/subfolder/worker.ts | 3 + 17 files changed, 235 insertions(+), 31 deletions(-) create mode 100644 tests/esm-child-process/process-forking-nested-relative/index.ts create mode 100644 tests/esm-child-process/process-forking-nested-relative/package.json create mode 100644 tests/esm-child-process/process-forking-nested-relative/subfolder/worker.ts create mode 100644 tests/esm-child-process/process-forking-nested-relative/tsconfig.json create mode 100644 tests/working-dir/cjs/index.ts create mode 100644 tests/working-dir/esm-node-next/index.ts create mode 100644 tests/working-dir/esm-node-next/package.json create mode 100644 tests/working-dir/esm-node-next/tsconfig.json create mode 100644 tests/working-dir/esm/index.ts create mode 100644 tests/working-dir/esm/package.json create mode 100644 tests/working-dir/esm/tsconfig.json create mode 100644 tests/working-dir/forking/index.ts create mode 100644 tests/working-dir/forking/subfolder/worker.ts diff --git a/src/bin.ts b/src/bin.ts index 93f078658..e740bf79c 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -73,13 +73,17 @@ export function bootstrap(state: BootstrapState) { if (!state.phase2Result) { state.phase2Result = phase2(state); if (state.shouldUseChildProcess && !state.isInChildProcess) { - return callInChild(state); + // Note: When transitioning into the child-process after `phase2`, + // the updated working directory needs to be preserved. + return callInChild(state, state.phase2Result.targetCwd); } } if (!state.phase3Result) { state.phase3Result = phase3(state); if (state.shouldUseChildProcess && !state.isInChildProcess) { - return callInChild(state); + // Note: When transitioning into the child-process after `phase2`, + // the updated working directory needs to be preserved. + return callInChild(state, state.phase2Result.targetCwd); } } return phase4(state); @@ -293,7 +297,8 @@ Options: -D, --ignoreDiagnostics [code] Ignore TypeScript warnings by diagnostic code -O, --compilerOptions [opts] JSON object to merge with compiler options - --cwd Behave as if invoked within this working directory. + --cwd Sets the working directory of the spawned script. Also useful for the + automatic discovering of the \`tsconfig.json\` project. --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 @@ -318,7 +323,15 @@ Options: process.exit(0); } - const cwd = cwdArg || process.cwd(); + let targetCwd: string; + + // Switch to the target `--cwd` if specified. + if (cwdArg !== undefined) { + targetCwd = resolve(cwdArg); + process.chdir(targetCwd); + } else { + targetCwd = process.cwd(); + } // If ESM is explicitly enabled through the flag, stage3 should be run in a child process // with the ESM loaders configured. @@ -327,7 +340,7 @@ Options: } return { - cwd, + targetCwd, }; } @@ -359,7 +372,7 @@ function phase3(payload: BootstrapState) { esm, experimentalSpecifierResolution, } = payload.parseArgvResult; - const { cwd } = payload.phase2Result!; + const { targetCwd } = payload.phase2Result!; // NOTE: When we transition to a child process for ESM, the entry-point script determined // here might not be the one used later in `phase4`. This can happen when we execute the @@ -367,13 +380,10 @@ function phase3(payload: BootstrapState) { // We will always use the original TS project in forked processes anyway, so it is // expected and acceptable to retrieve the entry-point information here in `phase2`. // See: https://github.com/TypeStrong/ts-node/issues/1812. - const { entryPointPath } = getEntryPointInfo( - payload.parseArgvResult!, - payload.phase2Result! - ); + const { entryPointPath } = getEntryPointInfo(payload.parseArgvResult!); const preloadedConfig = findAndReadConfig({ - cwd, + cwd: targetCwd, emit, files, pretty, @@ -386,7 +396,7 @@ function phase3(payload: BootstrapState) { ignore, logError, projectSearchDir: getProjectSearchDir( - cwd, + targetCwd, scriptMode, cwdMode, entryPointPath @@ -431,11 +441,9 @@ function phase3(payload: BootstrapState) { * details can be found in here: https://github.com/TypeStrong/ts-node/issues/1812. */ function getEntryPointInfo( - argvResult: NonNullable, - phase2Result: NonNullable + argvResult: NonNullable ) { const { code, interactive, restArgs } = argvResult; - const { cwd } = phase2Result; // Figure out which we are executing: piped stdin, --eval, REPL, and/or entrypoint // This is complicated because node's behavior is complicated @@ -447,10 +455,8 @@ function getEntryPointInfo( (interactive || (process.stdin.isTTY && !executeEval)); const executeStdin = !executeEval && !executeRepl && !executeEntrypoint; - /** Unresolved. May point to a symlink, not realpath. May be missing file extension */ - const entryPointPath = executeEntrypoint - ? resolve(cwd, restArgs[0]) - : undefined; + /** Unresolved. May point to a symlink, not realpath. May be missing file extension */ + const entryPointPath = executeEntrypoint ? resolve(restArgs[0]) : undefined; return { executeEval, @@ -465,7 +471,7 @@ function phase4(payload: BootstrapState) { const { isInChildProcess, tsNodeScript } = payload; const { version, showConfig, restArgs, code, print, argv } = payload.parseArgvResult; - const { cwd } = payload.phase2Result!; + const { targetCwd } = payload.phase2Result!; const { preloadedConfig } = payload.phase3Result!; const { @@ -474,7 +480,7 @@ function phase4(payload: BootstrapState) { executeEval, executeRepl, executeStdin, - } = getEntryPointInfo(payload.parseArgvResult!, payload.phase2Result!); + } = getEntryPointInfo(payload.parseArgvResult!); /** * , [stdin], and [eval] are all essentially virtual files that do not exist on disc and are backed by a REPL @@ -490,7 +496,7 @@ function phase4(payload: BootstrapState) { let stdinStuff: VirtualFileState | undefined; let evalAwarePartialHost: EvalAwarePartialHost | undefined = undefined; if (executeEval) { - const state = new EvalState(join(cwd, EVAL_FILENAME)); + const state = new EvalState(join(targetCwd, EVAL_FILENAME)); evalStuff = { state, repl: createRepl({ @@ -503,10 +509,10 @@ function phase4(payload: BootstrapState) { // Create a local module instance based on `cwd`. const module = (evalStuff.module = new Module(EVAL_NAME)); module.filename = evalStuff.state.path; - module.paths = (Module as any)._nodeModulePaths(cwd); + module.paths = (Module as any)._nodeModulePaths(targetCwd); } if (executeStdin) { - const state = new EvalState(join(cwd, STDIN_FILENAME)); + const state = new EvalState(join(targetCwd, STDIN_FILENAME)); stdinStuff = { state, repl: createRepl({ @@ -519,10 +525,10 @@ function phase4(payload: BootstrapState) { // Create a local module instance based on `cwd`. const module = (stdinStuff.module = new Module(STDIN_NAME)); module.filename = stdinStuff.state.path; - module.paths = (Module as any)._nodeModulePaths(cwd); + module.paths = (Module as any)._nodeModulePaths(targetCwd); } if (executeRepl) { - const state = new EvalState(join(cwd, REPL_FILENAME)); + const state = new EvalState(join(targetCwd, REPL_FILENAME)); replStuff = { state, repl: createRepl({ @@ -607,7 +613,8 @@ function phase4(payload: BootstrapState) { }, ...ts.convertToTSConfig( service.config, - service.configFilePath ?? join(cwd, 'ts-node-implicit-tsconfig.json'), + service.configFilePath ?? + join(targetCwd, 'ts-node-implicit-tsconfig.json'), service.ts.sys ), }; @@ -623,10 +630,10 @@ function phase4(payload: BootstrapState) { // Prepend `ts-node` arguments to CLI for child processes. process.execArgv.push( tsNodeScript, - ...argv.slice(2, argv.length - restArgs.length) + ...sanitizeArgvForChildForking(argv.slice(2, argv.length - restArgs.length)) ); - // TODO this comes from BoostrapState + // TODO this comes from BootstrapState process.argv = [process.argv[1]] .concat(executeEntrypoint ? ([entryPointPath] as string[]) : []) .concat(restArgs.slice(executeEntrypoint ? 1 : 0)); @@ -749,6 +756,30 @@ function requireResolveNonCached(absoluteModuleSpecifier: string) { }); } +/** + * Sanitizes the specified argv string array to be useful for child processes + * which may be created using `child_process.fork`. Some initial `ts-node` options + * should not be preserved and forwarded to child process forks. + * + * * `--cwd` should not override the working directory in forked processes. + */ +function sanitizeArgvForChildForking(argv: string[]): string[] { + let result: string[] = []; + let omitNext = false; + + for (const value of argv) { + if (value === '--cwd' || value === '--dir') { + omitNext = true; + } else if (!omitNext) { + result.push(value); + } else { + omitNext = false; + } + } + + return result; +} + /** * Evaluate an [eval] or [stdin] script */ diff --git a/src/child/spawn-child.ts b/src/child/spawn-child.ts index 12368fcef..198c96f58 100644 --- a/src/child/spawn-child.ts +++ b/src/child/spawn-child.ts @@ -6,8 +6,13 @@ import { versionGteLt } from '../util'; const argPrefix = '--brotli-base64-config='; -/** @internal */ -export function callInChild(state: BootstrapState) { +/** + * @internal + * @param state Bootstrap state to be transferred into the child process. + * @param targetCwd Working directory to be preserved when transitioning to + * the child process. + */ +export function callInChild(state: BootstrapState, targetCwd: string) { 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.' @@ -29,6 +34,7 @@ export function callInChild(state: BootstrapState) { ], { stdio: 'inherit', + cwd: targetCwd, argv0: process.argv0, } ); diff --git a/src/test/esm-loader.spec.ts b/src/test/esm-loader.spec.ts index f12e04821..765c5948e 100644 --- a/src/test/esm-loader.spec.ts +++ b/src/test/esm-loader.spec.ts @@ -359,6 +359,35 @@ test.suite('esm', (test) => { }); } + test.suite('esm child process working directory', (test) => { + test('should have the correct working directory in the user entry-point', async () => { + const { err, stdout, stderr } = await exec( + `${BIN_PATH} --esm --cwd ./working-dir/esm/ index.ts` + ); + + expect(err).toBe(null); + expect(stdout.trim()).toBe('Passing'); + expect(stderr).toBe(''); + }); + + test.suite( + 'with NodeNext TypeScript resolution and `.mts` extension', + (test) => { + test.runIf(tsSupportsStableNodeNextNode16); + + test('should have the correct working directory in the user entry-point', async () => { + const { err, stdout, stderr } = await exec( + `${BIN_PATH} --esm --cwd ./working-dir/esm-node-next/ index.ts` + ); + + expect(err).toBe(null); + expect(stdout.trim()).toBe('Passing'); + expect(stderr).toBe(''); + }); + } + ); + }); + test.suite('esm child process and forking', (test) => { test('should be able to fork vanilla NodeJS script', async () => { const { err, stdout, stderr } = await exec( @@ -380,6 +409,20 @@ test.suite('esm', (test) => { expect(stderr).toBe(''); }); + test( + 'should be possible to fork into a nested TypeScript script with respect to ' + + 'the working directory', + async () => { + const { err, stdout, stderr } = await exec( + `${BIN_PATH} --esm --cwd ./esm-child-process/process-forking-nested-relative/ index.ts` + ); + + expect(err).toBe(null); + expect(stdout.trim()).toBe('Passing: from main'); + expect(stderr).toBe(''); + } + ); + test.suite( 'with NodeNext TypeScript resolution and `.mts` extension', (test) => { diff --git a/src/test/index.spec.ts b/src/test/index.spec.ts index ca4c2cf85..3f8555d16 100644 --- a/src/test/index.spec.ts +++ b/src/test/index.spec.ts @@ -617,6 +617,30 @@ test.suite('ts-node', (test) => { } }); + test('should have the correct working directory in the user entry-point', async () => { + const { err, stdout, stderr } = await exec( + `${BIN_PATH} --cwd ./working-dir/cjs/ index.ts` + ); + + expect(err).toBe(null); + expect(stdout.trim()).toBe('Passing'); + expect(stderr).toBe(''); + }); + + test( + 'should be able to fork into a nested TypeScript script with a modified ' + + 'working directory', + async () => { + const { err, stdout, stderr } = await exec( + `${BIN_PATH} --cwd ./working-dir/forking/ index.ts` + ); + + expect(err).toBe(null); + expect(stdout.trim()).toBe('Passing: from main'); + expect(stderr).toBe(''); + } + ); + test.suite('should read ts-node options from tsconfig.json', (test) => { const BIN_EXEC = `"${BIN_PATH}" --project tsconfig-options/tsconfig.json`; diff --git a/tests/esm-child-process/process-forking-nested-relative/index.ts b/tests/esm-child-process/process-forking-nested-relative/index.ts new file mode 100644 index 000000000..e0b27f3cf --- /dev/null +++ b/tests/esm-child-process/process-forking-nested-relative/index.ts @@ -0,0 +1,22 @@ +import { fork } from 'child_process'; +import { join } from 'path'; + +// Initially set the exit code to non-zero. We only set it to `0` when the +// worker process finishes properly with the expected stdout message. +process.exitCode = 1; + +const workerProcess = fork('./worker.ts', [], { + stdio: 'pipe', + cwd: join(process.cwd(), 'subfolder/'), +}); + +let stdout = ''; + +workerProcess.stdout.on('data', (chunk) => (stdout += chunk.toString('utf8'))); +workerProcess.on('error', () => (process.exitCode = 1)); +workerProcess.on('close', (status, signal) => { + if (status === 0 && signal === null && stdout.trim() === 'Works') { + console.log('Passing: from main'); + process.exitCode = 0; + } +}); diff --git a/tests/esm-child-process/process-forking-nested-relative/package.json b/tests/esm-child-process/process-forking-nested-relative/package.json new file mode 100644 index 000000000..3dbc1ca59 --- /dev/null +++ b/tests/esm-child-process/process-forking-nested-relative/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/tests/esm-child-process/process-forking-nested-relative/subfolder/worker.ts b/tests/esm-child-process/process-forking-nested-relative/subfolder/worker.ts new file mode 100644 index 000000000..4114d5ab0 --- /dev/null +++ b/tests/esm-child-process/process-forking-nested-relative/subfolder/worker.ts @@ -0,0 +1,3 @@ +const message: string = 'Works'; + +console.log(message); diff --git a/tests/esm-child-process/process-forking-nested-relative/tsconfig.json b/tests/esm-child-process/process-forking-nested-relative/tsconfig.json new file mode 100644 index 000000000..1ac61592b --- /dev/null +++ b/tests/esm-child-process/process-forking-nested-relative/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "module": "ESNext" + } +} diff --git a/tests/working-dir/cjs/index.ts b/tests/working-dir/cjs/index.ts new file mode 100644 index 000000000..982e5195d --- /dev/null +++ b/tests/working-dir/cjs/index.ts @@ -0,0 +1,7 @@ +import { strictEqual } from 'assert'; +import { normalize } from 'path'; + +// Expect the working directory to be the current directory. +strictEqual(normalize(process.cwd()), normalize(__dirname)); + +console.log('Passing'); diff --git a/tests/working-dir/esm-node-next/index.ts b/tests/working-dir/esm-node-next/index.ts new file mode 100644 index 000000000..f290171a9 --- /dev/null +++ b/tests/working-dir/esm-node-next/index.ts @@ -0,0 +1,11 @@ +import { strictEqual } from 'assert'; +import { normalize, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +// Expect the working directory to be the current directory. +strictEqual( + normalize(process.cwd()), + normalize(dirname(fileURLToPath(import.meta.url))) +); + +console.log('Passing'); diff --git a/tests/working-dir/esm-node-next/package.json b/tests/working-dir/esm-node-next/package.json new file mode 100644 index 000000000..3dbc1ca59 --- /dev/null +++ b/tests/working-dir/esm-node-next/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/tests/working-dir/esm-node-next/tsconfig.json b/tests/working-dir/esm-node-next/tsconfig.json new file mode 100644 index 000000000..3998b5074 --- /dev/null +++ b/tests/working-dir/esm-node-next/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "module": "NodeNext" + } +} diff --git a/tests/working-dir/esm/index.ts b/tests/working-dir/esm/index.ts new file mode 100644 index 000000000..0f6220303 --- /dev/null +++ b/tests/working-dir/esm/index.ts @@ -0,0 +1,8 @@ +import { ok } from 'assert'; + +// Expect the working directory to be the current directory. +// Note: Cannot use `import.meta.url` in this variant of the test +// because older TypeScript versions do not know about this syntax. +ok(/working-dir[\/\\]esm[\/\\]?/.test(process.cwd())); + +console.log('Passing'); diff --git a/tests/working-dir/esm/package.json b/tests/working-dir/esm/package.json new file mode 100644 index 000000000..3dbc1ca59 --- /dev/null +++ b/tests/working-dir/esm/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/tests/working-dir/esm/tsconfig.json b/tests/working-dir/esm/tsconfig.json new file mode 100644 index 000000000..1ac61592b --- /dev/null +++ b/tests/working-dir/esm/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "module": "ESNext" + } +} diff --git a/tests/working-dir/forking/index.ts b/tests/working-dir/forking/index.ts new file mode 100644 index 000000000..45ff8afd7 --- /dev/null +++ b/tests/working-dir/forking/index.ts @@ -0,0 +1,22 @@ +import { fork } from 'child_process'; +import { join } from 'path'; + +// Initially set the exit code to non-zero. We only set it to `0` when the +// worker process finishes properly with the expected stdout message. +process.exitCode = 1; + +const workerProcess = fork('./worker.ts', [], { + stdio: 'pipe', + cwd: join(__dirname, 'subfolder'), +}); + +let stdout = ''; + +workerProcess.stdout!.on('data', (chunk) => (stdout += chunk.toString('utf8'))); +workerProcess.on('error', () => (process.exitCode = 1)); +workerProcess.on('close', (status, signal) => { + if (status === 0 && signal === null && stdout.trim() === 'Works') { + console.log('Passing: from main'); + process.exitCode = 0; + } +}); diff --git a/tests/working-dir/forking/subfolder/worker.ts b/tests/working-dir/forking/subfolder/worker.ts new file mode 100644 index 000000000..4114d5ab0 --- /dev/null +++ b/tests/working-dir/forking/subfolder/worker.ts @@ -0,0 +1,3 @@ +const message: string = 'Works'; + +console.log(message); From 38befcce8be37f9e8a4d11139b3e55d672c3a3ac Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Tue, 12 Jul 2022 14:21:49 -0400 Subject: [PATCH 03/11] changes based on review --- src/bin.ts | 96 +++++++++++++---------------------- src/child/argv-payload.ts | 19 +++++++ src/child/child-entrypoint.ts | 29 +++++++---- src/child/spawn-child.ts | 11 ++-- 4 files changed, 76 insertions(+), 79 deletions(-) create mode 100644 src/child/argv-payload.ts diff --git a/src/bin.ts b/src/bin.ts index e740bf79c..2ac4c2882 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -48,6 +48,7 @@ export function main( const state: BootstrapState = { shouldUseChildProcess: false, isInChildProcess: false, + isCli: true, tsNodeScript: __filename, parseArgvResult: args, }; @@ -62,6 +63,11 @@ export function main( export interface BootstrapState { isInChildProcess: boolean; shouldUseChildProcess: boolean; + /** + * True if bootstrapping the ts-node CLI process or the direct child necessitated by `--esm`. + * false if bootstrapping a subsequently `fork()`ed child. + */ + isCli: boolean; tsNodeScript: string; parseArgvResult: ReturnType; phase2Result?: ReturnType; @@ -75,7 +81,7 @@ export function bootstrap(state: BootstrapState) { if (state.shouldUseChildProcess && !state.isInChildProcess) { // Note: When transitioning into the child-process after `phase2`, // the updated working directory needs to be preserved. - return callInChild(state, state.phase2Result.targetCwd); + return callInChild(state); } } if (!state.phase3Result) { @@ -83,7 +89,7 @@ export function bootstrap(state: BootstrapState) { if (state.shouldUseChildProcess && !state.isInChildProcess) { // Note: When transitioning into the child-process after `phase2`, // the updated working directory needs to be preserved. - return callInChild(state, state.phase2Result.targetCwd); + return callInChild(state); } } return phase4(state); @@ -297,8 +303,7 @@ Options: -D, --ignoreDiagnostics [code] Ignore TypeScript warnings by diagnostic code -O, --compilerOptions [opts] JSON object to merge with compiler options - --cwd Sets the working directory of the spawned script. Also useful for the - automatic discovering of the \`tsconfig.json\` project. + --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 @@ -323,24 +328,14 @@ Options: process.exit(0); } - let targetCwd: string; - - // Switch to the target `--cwd` if specified. - if (cwdArg !== undefined) { - targetCwd = resolve(cwdArg); - process.chdir(targetCwd); - } else { - targetCwd = process.cwd(); - } + const cwd = cwdArg ? resolve(cwdArg) : process.cwd(); // If ESM is explicitly enabled through the flag, stage3 should be run in a child process // with the ESM loaders configured. - if (esm) { - payload.shouldUseChildProcess = true; - } + if (esm) payload.shouldUseChildProcess = true; return { - targetCwd, + cwd, }; } @@ -372,7 +367,7 @@ function phase3(payload: BootstrapState) { esm, experimentalSpecifierResolution, } = payload.parseArgvResult; - const { targetCwd } = payload.phase2Result!; + const { cwd } = payload.phase2Result!; // NOTE: When we transition to a child process for ESM, the entry-point script determined // here might not be the one used later in `phase4`. This can happen when we execute the @@ -380,10 +375,10 @@ function phase3(payload: BootstrapState) { // We will always use the original TS project in forked processes anyway, so it is // expected and acceptable to retrieve the entry-point information here in `phase2`. // See: https://github.com/TypeStrong/ts-node/issues/1812. - const { entryPointPath } = getEntryPointInfo(payload.parseArgvResult!); + const { entryPointPath } = getEntryPointInfo(payload); const preloadedConfig = findAndReadConfig({ - cwd: targetCwd, + cwd, emit, files, pretty, @@ -396,7 +391,7 @@ function phase3(payload: BootstrapState) { ignore, logError, projectSearchDir: getProjectSearchDir( - targetCwd, + cwd, scriptMode, cwdMode, entryPointPath @@ -418,9 +413,7 @@ function phase3(payload: BootstrapState) { // If ESM is enabled through the parsed tsconfig, stage4 should be run in a child // process with the ESM loaders configured. - if (preloadedConfig.options.esm) { - payload.shouldUseChildProcess = true; - } + if (preloadedConfig.options.esm) payload.shouldUseChildProcess = true; return { preloadedConfig }; } @@ -441,9 +434,11 @@ function phase3(payload: BootstrapState) { * details can be found in here: https://github.com/TypeStrong/ts-node/issues/1812. */ function getEntryPointInfo( - argvResult: NonNullable + state: BootstrapState ) { - const { code, interactive, restArgs } = argvResult; + const { code, interactive, restArgs } = state.parseArgvResult!; + const { cwd } = state.phase2Result!; + const { isCli } = state; // Figure out which we are executing: piped stdin, --eval, REPL, and/or entrypoint // This is complicated because node's behavior is complicated @@ -455,8 +450,11 @@ function getEntryPointInfo( (interactive || (process.stdin.isTTY && !executeEval)); const executeStdin = !executeEval && !executeRepl && !executeEntrypoint; - /** Unresolved. May point to a symlink, not realpath. May be missing file extension */ - const entryPointPath = executeEntrypoint ? resolve(restArgs[0]) : undefined; + /** + * Unresolved. May point to a symlink, not realpath. May be missing file extension + * NOTE: resolution relative to cwd option (not `process.cwd()`) is legacy backwards-compat; should be changed in next major: https://github.com/TypeStrong/ts-node/issues/1834 + */ + const entryPointPath = executeEntrypoint ? (isCli ? resolve(cwd, restArgs[0]) : resolve(restArgs[0])) : undefined; return { executeEval, @@ -471,7 +469,7 @@ function phase4(payload: BootstrapState) { const { isInChildProcess, tsNodeScript } = payload; const { version, showConfig, restArgs, code, print, argv } = payload.parseArgvResult; - const { targetCwd } = payload.phase2Result!; + const { cwd } = payload.phase2Result!; const { preloadedConfig } = payload.phase3Result!; const { @@ -480,7 +478,7 @@ function phase4(payload: BootstrapState) { executeEval, executeRepl, executeStdin, - } = getEntryPointInfo(payload.parseArgvResult!); + } = getEntryPointInfo(payload); /** * , [stdin], and [eval] are all essentially virtual files that do not exist on disc and are backed by a REPL @@ -496,7 +494,7 @@ function phase4(payload: BootstrapState) { let stdinStuff: VirtualFileState | undefined; let evalAwarePartialHost: EvalAwarePartialHost | undefined = undefined; if (executeEval) { - const state = new EvalState(join(targetCwd, EVAL_FILENAME)); + const state = new EvalState(join(cwd, EVAL_FILENAME)); evalStuff = { state, repl: createRepl({ @@ -509,10 +507,10 @@ function phase4(payload: BootstrapState) { // Create a local module instance based on `cwd`. const module = (evalStuff.module = new Module(EVAL_NAME)); module.filename = evalStuff.state.path; - module.paths = (Module as any)._nodeModulePaths(targetCwd); + module.paths = (Module as any)._nodeModulePaths(cwd); } if (executeStdin) { - const state = new EvalState(join(targetCwd, STDIN_FILENAME)); + const state = new EvalState(join(cwd, STDIN_FILENAME)); stdinStuff = { state, repl: createRepl({ @@ -525,10 +523,10 @@ function phase4(payload: BootstrapState) { // Create a local module instance based on `cwd`. const module = (stdinStuff.module = new Module(STDIN_NAME)); module.filename = stdinStuff.state.path; - module.paths = (Module as any)._nodeModulePaths(targetCwd); + module.paths = (Module as any)._nodeModulePaths(cwd); } if (executeRepl) { - const state = new EvalState(join(targetCwd, REPL_FILENAME)); + const state = new EvalState(join(cwd, REPL_FILENAME)); replStuff = { state, repl: createRepl({ @@ -614,7 +612,7 @@ function phase4(payload: BootstrapState) { ...ts.convertToTSConfig( service.config, service.configFilePath ?? - join(targetCwd, 'ts-node-implicit-tsconfig.json'), + join(cwd, 'ts-node-implicit-tsconfig.json'), service.ts.sys ), }; @@ -630,7 +628,7 @@ function phase4(payload: BootstrapState) { // Prepend `ts-node` arguments to CLI for child processes. process.execArgv.push( tsNodeScript, - ...sanitizeArgvForChildForking(argv.slice(2, argv.length - restArgs.length)) + ...argv.slice(2, argv.length - restArgs.length) ); // TODO this comes from BootstrapState @@ -756,30 +754,6 @@ function requireResolveNonCached(absoluteModuleSpecifier: string) { }); } -/** - * Sanitizes the specified argv string array to be useful for child processes - * which may be created using `child_process.fork`. Some initial `ts-node` options - * should not be preserved and forwarded to child process forks. - * - * * `--cwd` should not override the working directory in forked processes. - */ -function sanitizeArgvForChildForking(argv: string[]): string[] { - let result: string[] = []; - let omitNext = false; - - for (const value of argv) { - if (value === '--cwd' || value === '--dir') { - omitNext = true; - } else if (!omitNext) { - result.push(value); - } else { - omitNext = false; - } - } - - return result; -} - /** * Evaluate an [eval] or [stdin] script */ diff --git a/src/child/argv-payload.ts b/src/child/argv-payload.ts new file mode 100644 index 000000000..b4514aad9 --- /dev/null +++ b/src/child/argv-payload.ts @@ -0,0 +1,19 @@ +import {brotliCompressSync, brotliDecompressSync, constants} from 'zlib'; + +/** @internal */ +export const argPrefix = '--brotli-base64-config='; + +/** @internal */ +export function compress(object: any) { + return brotliCompressSync( + Buffer.from(JSON.stringify(object), 'utf8'), + { + [constants.BROTLI_PARAM_QUALITY]: constants.BROTLI_MIN_QUALITY + } + ).toString('base64'); +} + +/** @internal */ +export function decompress(str: string) { + return JSON.parse(brotliDecompressSync(Buffer.from(str, 'base64')).toString()); +} diff --git a/src/child/child-entrypoint.ts b/src/child/child-entrypoint.ts index 55d77ad9d..eba9797a3 100644 --- a/src/child/child-entrypoint.ts +++ b/src/child/child-entrypoint.ts @@ -1,17 +1,26 @@ import { BootstrapState, bootstrap } from '../bin'; -import { brotliDecompressSync } from 'zlib'; +import { argPrefix, compress, decompress } from './argv-payload'; const base64ConfigArg = process.argv[2]; -const argPrefix = '--brotli-base64-config='; if (!base64ConfigArg.startsWith(argPrefix)) throw new Error('unexpected argv'); const base64Payload = base64ConfigArg.slice(argPrefix.length); -const payload = JSON.parse( - brotliDecompressSync(Buffer.from(base64Payload, 'base64')).toString() -) as BootstrapState; +const state = decompress(base64Payload) as BootstrapState; -payload.isInChildProcess = true; -payload.tsNodeScript = __filename; -payload.parseArgvResult.argv = process.argv; -payload.parseArgvResult.restArgs = process.argv.slice(3); +state.isInChildProcess = true; +state.tsNodeScript = __filename; +state.parseArgvResult.argv = process.argv; +state.parseArgvResult.restArgs = process.argv.slice(3); -bootstrap(payload); +// Modify and re-compress the payload delivered to subsequent child processes. +// This logic may be refactored into bin.ts by https://github.com/TypeStrong/ts-node/issues/1831 +if(state.isCli) { + const stateForChildren: BootstrapState = { + ...state, + isCli: false + }; + state.parseArgvResult.argv[2] = `${argPrefix}${ + compress(stateForChildren) + }`; +} + +bootstrap(state); diff --git a/src/child/spawn-child.ts b/src/child/spawn-child.ts index 198c96f58..618b8190a 100644 --- a/src/child/spawn-child.ts +++ b/src/child/spawn-child.ts @@ -1,10 +1,8 @@ import type { BootstrapState } from '../bin'; import { spawn } from 'child_process'; -import { brotliCompressSync } from 'zlib'; import { pathToFileURL } from 'url'; import { versionGteLt } from '../util'; - -const argPrefix = '--brotli-base64-config='; +import { argPrefix, compress } from './argv-payload'; /** * @internal @@ -12,7 +10,7 @@ const argPrefix = '--brotli-base64-config='; * @param targetCwd Working directory to be preserved when transitioning to * the child process. */ -export function callInChild(state: BootstrapState, targetCwd: string) { +export function callInChild(state: BootstrapState) { 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.' @@ -27,14 +25,11 @@ export function callInChild(state: BootstrapState, targetCwd: string) { // 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') - ).toString('base64')}`, + `${argPrefix}${compress(state)}`, ...state.parseArgvResult.restArgs, ], { stdio: 'inherit', - cwd: targetCwd, argv0: process.argv0, } ); From ad19d9fd93cbdca3d2aba3e609f3665e763e7dfe Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Tue, 12 Jul 2022 14:24:26 -0400 Subject: [PATCH 04/11] lint-fix --- src/bin.ts | 13 +++++++------ src/child/argv-payload.ts | 15 +++++++-------- src/child/child-entrypoint.ts | 8 +++----- 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/src/bin.ts b/src/bin.ts index 2ac4c2882..fb3208c48 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -433,9 +433,7 @@ function phase3(payload: BootstrapState) { * configuration and entry-point information is only reliable in the final phase. More * details can be found in here: https://github.com/TypeStrong/ts-node/issues/1812. */ -function getEntryPointInfo( - state: BootstrapState -) { +function getEntryPointInfo(state: BootstrapState) { const { code, interactive, restArgs } = state.parseArgvResult!; const { cwd } = state.phase2Result!; const { isCli } = state; @@ -454,7 +452,11 @@ function getEntryPointInfo( * Unresolved. May point to a symlink, not realpath. May be missing file extension * NOTE: resolution relative to cwd option (not `process.cwd()`) is legacy backwards-compat; should be changed in next major: https://github.com/TypeStrong/ts-node/issues/1834 */ - const entryPointPath = executeEntrypoint ? (isCli ? resolve(cwd, restArgs[0]) : resolve(restArgs[0])) : undefined; + const entryPointPath = executeEntrypoint + ? isCli + ? resolve(cwd, restArgs[0]) + : resolve(restArgs[0]) + : undefined; return { executeEval, @@ -611,8 +613,7 @@ function phase4(payload: BootstrapState) { }, ...ts.convertToTSConfig( service.config, - service.configFilePath ?? - join(cwd, 'ts-node-implicit-tsconfig.json'), + service.configFilePath ?? join(cwd, 'ts-node-implicit-tsconfig.json'), service.ts.sys ), }; diff --git a/src/child/argv-payload.ts b/src/child/argv-payload.ts index b4514aad9..abe6da9db 100644 --- a/src/child/argv-payload.ts +++ b/src/child/argv-payload.ts @@ -1,19 +1,18 @@ -import {brotliCompressSync, brotliDecompressSync, constants} from 'zlib'; +import { brotliCompressSync, brotliDecompressSync, constants } from 'zlib'; /** @internal */ export const argPrefix = '--brotli-base64-config='; /** @internal */ export function compress(object: any) { - return brotliCompressSync( - Buffer.from(JSON.stringify(object), 'utf8'), - { - [constants.BROTLI_PARAM_QUALITY]: constants.BROTLI_MIN_QUALITY - } - ).toString('base64'); + return brotliCompressSync(Buffer.from(JSON.stringify(object), 'utf8'), { + [constants.BROTLI_PARAM_QUALITY]: constants.BROTLI_MIN_QUALITY, + }).toString('base64'); } /** @internal */ export function decompress(str: string) { - return JSON.parse(brotliDecompressSync(Buffer.from(str, 'base64')).toString()); + return JSON.parse( + brotliDecompressSync(Buffer.from(str, 'base64')).toString() + ); } diff --git a/src/child/child-entrypoint.ts b/src/child/child-entrypoint.ts index eba9797a3..0550170bf 100644 --- a/src/child/child-entrypoint.ts +++ b/src/child/child-entrypoint.ts @@ -13,14 +13,12 @@ state.parseArgvResult.restArgs = process.argv.slice(3); // Modify and re-compress the payload delivered to subsequent child processes. // This logic may be refactored into bin.ts by https://github.com/TypeStrong/ts-node/issues/1831 -if(state.isCli) { +if (state.isCli) { const stateForChildren: BootstrapState = { ...state, - isCli: false + isCli: false, }; - state.parseArgvResult.argv[2] = `${argPrefix}${ - compress(stateForChildren) - }`; + state.parseArgvResult.argv[2] = `${argPrefix}${compress(stateForChildren)}`; } bootstrap(state); From e112f7c089205f8f542f6836f0c4b4173e65c499 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Tue, 12 Jul 2022 19:30:29 -0400 Subject: [PATCH 05/11] enable transpileOnly in new tests for performance --- .../process-forking-nested-esm-node-next/tsconfig.json | 3 +++ .../process-forking-nested-esm/tsconfig.json | 3 +++ .../process-forking-nested-relative/tsconfig.json | 3 +++ tests/esm-child-process/process-forking/tsconfig.json | 3 +++ tests/working-dir/esm-node-next/tsconfig.json | 3 +++ tests/working-dir/esm/tsconfig.json | 3 +++ tests/working-dir/tsconfig.json | 6 ++++++ 7 files changed, 24 insertions(+) create mode 100644 tests/working-dir/tsconfig.json diff --git a/tests/esm-child-process/process-forking-nested-esm-node-next/tsconfig.json b/tests/esm-child-process/process-forking-nested-esm-node-next/tsconfig.json index 3998b5074..0b0d8971d 100644 --- a/tests/esm-child-process/process-forking-nested-esm-node-next/tsconfig.json +++ b/tests/esm-child-process/process-forking-nested-esm-node-next/tsconfig.json @@ -1,5 +1,8 @@ { "compilerOptions": { "module": "NodeNext" + }, + "ts-node": { + "transpileOnly": true } } diff --git a/tests/esm-child-process/process-forking-nested-esm/tsconfig.json b/tests/esm-child-process/process-forking-nested-esm/tsconfig.json index 1ac61592b..f16e691d2 100644 --- a/tests/esm-child-process/process-forking-nested-esm/tsconfig.json +++ b/tests/esm-child-process/process-forking-nested-esm/tsconfig.json @@ -1,5 +1,8 @@ { "compilerOptions": { "module": "ESNext" + }, + "ts-node": { + "transpileOnly": true } } diff --git a/tests/esm-child-process/process-forking-nested-relative/tsconfig.json b/tests/esm-child-process/process-forking-nested-relative/tsconfig.json index 1ac61592b..f16e691d2 100644 --- a/tests/esm-child-process/process-forking-nested-relative/tsconfig.json +++ b/tests/esm-child-process/process-forking-nested-relative/tsconfig.json @@ -1,5 +1,8 @@ { "compilerOptions": { "module": "ESNext" + }, + "ts-node": { + "transpileOnly": true } } diff --git a/tests/esm-child-process/process-forking/tsconfig.json b/tests/esm-child-process/process-forking/tsconfig.json index 1ac61592b..f16e691d2 100644 --- a/tests/esm-child-process/process-forking/tsconfig.json +++ b/tests/esm-child-process/process-forking/tsconfig.json @@ -1,5 +1,8 @@ { "compilerOptions": { "module": "ESNext" + }, + "ts-node": { + "transpileOnly": true } } diff --git a/tests/working-dir/esm-node-next/tsconfig.json b/tests/working-dir/esm-node-next/tsconfig.json index 3998b5074..0b0d8971d 100644 --- a/tests/working-dir/esm-node-next/tsconfig.json +++ b/tests/working-dir/esm-node-next/tsconfig.json @@ -1,5 +1,8 @@ { "compilerOptions": { "module": "NodeNext" + }, + "ts-node": { + "transpileOnly": true } } diff --git a/tests/working-dir/esm/tsconfig.json b/tests/working-dir/esm/tsconfig.json index 1ac61592b..f16e691d2 100644 --- a/tests/working-dir/esm/tsconfig.json +++ b/tests/working-dir/esm/tsconfig.json @@ -1,5 +1,8 @@ { "compilerOptions": { "module": "ESNext" + }, + "ts-node": { + "transpileOnly": true } } diff --git a/tests/working-dir/tsconfig.json b/tests/working-dir/tsconfig.json new file mode 100644 index 000000000..484405d0e --- /dev/null +++ b/tests/working-dir/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../tsconfig.json", + "ts-node": { + "transpileOnly": true + } +} From 112a47c6acd096fc782a5be8c12c33448afb4ea8 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Tue, 12 Jul 2022 19:33:03 -0400 Subject: [PATCH 06/11] Tweak basic working dir tests to verify that --cwd affects entrypoint resolution but not process.cwd() --- src/test/esm-loader.spec.ts | 10 ++++++++-- src/test/index.spec.ts | 5 ++++- tests/working-dir/cjs/index.ts | 6 +++--- tests/working-dir/esm-node-next/index.ts | 4 ++-- tests/working-dir/esm/index.ts | 13 ++++++++----- 5 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/test/esm-loader.spec.ts b/src/test/esm-loader.spec.ts index 765c5948e..67a97137c 100644 --- a/src/test/esm-loader.spec.ts +++ b/src/test/esm-loader.spec.ts @@ -362,7 +362,10 @@ test.suite('esm', (test) => { test.suite('esm child process working directory', (test) => { test('should have the correct working directory in the user entry-point', async () => { const { err, stdout, stderr } = await exec( - `${BIN_PATH} --esm --cwd ./working-dir/esm/ index.ts` + `${BIN_PATH} --esm --cwd ./esm/ index.ts`, + { + cwd: resolve(TEST_DIR, 'working-dir'), + } ); expect(err).toBe(null); @@ -377,7 +380,10 @@ test.suite('esm', (test) => { test('should have the correct working directory in the user entry-point', async () => { const { err, stdout, stderr } = await exec( - `${BIN_PATH} --esm --cwd ./working-dir/esm-node-next/ index.ts` + `${BIN_PATH} --esm --cwd ./esm-node-next/ index.ts`, + { + cwd: resolve(TEST_DIR, 'working-dir'), + } ); expect(err).toBe(null); diff --git a/src/test/index.spec.ts b/src/test/index.spec.ts index 3f8555d16..62a449a52 100644 --- a/src/test/index.spec.ts +++ b/src/test/index.spec.ts @@ -619,7 +619,10 @@ test.suite('ts-node', (test) => { test('should have the correct working directory in the user entry-point', async () => { const { err, stdout, stderr } = await exec( - `${BIN_PATH} --cwd ./working-dir/cjs/ index.ts` + `${BIN_PATH} --cwd ./cjs index.ts`, + { + cwd: resolve(TEST_DIR, 'working-dir'), + } ); expect(err).toBe(null); diff --git a/tests/working-dir/cjs/index.ts b/tests/working-dir/cjs/index.ts index 982e5195d..f3ba1b30a 100644 --- a/tests/working-dir/cjs/index.ts +++ b/tests/working-dir/cjs/index.ts @@ -1,7 +1,7 @@ import { strictEqual } from 'assert'; -import { normalize } from 'path'; +import { normalize, dirname } from 'path'; -// Expect the working directory to be the current directory. -strictEqual(normalize(process.cwd()), normalize(__dirname)); +// Expect the working directory to be the parent directory. +strictEqual(normalize(process.cwd()), normalize(dirname(__dirname))); console.log('Passing'); diff --git a/tests/working-dir/esm-node-next/index.ts b/tests/working-dir/esm-node-next/index.ts index f290171a9..21230f9d8 100644 --- a/tests/working-dir/esm-node-next/index.ts +++ b/tests/working-dir/esm-node-next/index.ts @@ -2,10 +2,10 @@ import { strictEqual } from 'assert'; import { normalize, dirname } from 'path'; import { fileURLToPath } from 'url'; -// Expect the working directory to be the current directory. +// Expect the working directory to be the parent directory. strictEqual( normalize(process.cwd()), - normalize(dirname(fileURLToPath(import.meta.url))) + normalize(dirname(dirname(fileURLToPath(import.meta.url)))) ); console.log('Passing'); diff --git a/tests/working-dir/esm/index.ts b/tests/working-dir/esm/index.ts index 0f6220303..21230f9d8 100644 --- a/tests/working-dir/esm/index.ts +++ b/tests/working-dir/esm/index.ts @@ -1,8 +1,11 @@ -import { ok } from 'assert'; +import { strictEqual } from 'assert'; +import { normalize, dirname } from 'path'; +import { fileURLToPath } from 'url'; -// Expect the working directory to be the current directory. -// Note: Cannot use `import.meta.url` in this variant of the test -// because older TypeScript versions do not know about this syntax. -ok(/working-dir[\/\\]esm[\/\\]?/.test(process.cwd())); +// Expect the working directory to be the parent directory. +strictEqual( + normalize(process.cwd()), + normalize(dirname(dirname(fileURLToPath(import.meta.url)))) +); console.log('Passing'); From d52c0b5655aa5b8a4b5571b398399128de3408e3 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Tue, 12 Jul 2022 19:46:22 -0400 Subject: [PATCH 07/11] update forking tests: disable non --esm test with comment about known bug and link to tickets make tests set cwd for fork() call, to be sure it is respected and not overridden by --cwd --- src/test/esm-loader.spec.ts | 29 +++++++++---------- src/test/index.spec.ts | 24 +++++++-------- .../process-forking-nested-esm/index.ts | 4 +++ .../process-forking-nested-relative/index.ts | 5 ++-- .../process-forking/index.ts | 4 +++ 5 files changed, 37 insertions(+), 29 deletions(-) diff --git a/src/test/esm-loader.spec.ts b/src/test/esm-loader.spec.ts index 67a97137c..902f658b6 100644 --- a/src/test/esm-loader.spec.ts +++ b/src/test/esm-loader.spec.ts @@ -397,7 +397,7 @@ test.suite('esm', (test) => { test.suite('esm child process and forking', (test) => { test('should be able to fork vanilla NodeJS script', async () => { const { err, stdout, stderr } = await exec( - `${BIN_PATH} --esm --cwd ./esm-child-process/process-forking/ index.ts` + `${BIN_PATH} --esm --cwd ./esm-child-process/ ./process-forking/index.ts` ); expect(err).toBe(null); @@ -407,7 +407,7 @@ test.suite('esm', (test) => { test('should be able to fork into a nested TypeScript ESM script', async () => { const { err, stdout, stderr } = await exec( - `${BIN_PATH} --esm --cwd ./esm-child-process/process-forking-nested-esm/ index.ts` + `${BIN_PATH} --esm --cwd ./esm-child-process/ ./process-forking-nested-esm/index.ts` ); expect(err).toBe(null); @@ -415,19 +415,15 @@ test.suite('esm', (test) => { expect(stderr).toBe(''); }); - test( - 'should be possible to fork into a nested TypeScript script with respect to ' + - 'the working directory', - async () => { - const { err, stdout, stderr } = await exec( - `${BIN_PATH} --esm --cwd ./esm-child-process/process-forking-nested-relative/ index.ts` - ); + test('should be possible to fork into a nested TypeScript script with respect to the working directory', async () => { + const { err, stdout, stderr } = await exec( + `${BIN_PATH} --esm --cwd ./esm-child-process/ ./process-forking-nested-relative/index.ts` + ); - expect(err).toBe(null); - expect(stdout.trim()).toBe('Passing: from main'); - expect(stderr).toBe(''); - } - ); + expect(err).toBe(null); + expect(stdout.trim()).toBe('Passing: from main'); + expect(stderr).toBe(''); + }); test.suite( 'with NodeNext TypeScript resolution and `.mts` extension', @@ -436,7 +432,10 @@ test.suite('esm', (test) => { test('should be able to fork into a nested TypeScript ESM script', async () => { const { err, stdout, stderr } = await exec( - `${BIN_PATH} --esm ./esm-child-process/process-forking-nested-esm-node-next/index.mts` + `${BIN_PATH} --esm ./process-forking-nested-esm-node-next/index.mts`, + { + cwd: resolve(TEST_DIR, 'esm-child-process'), + } ); expect(err).toBe(null); diff --git a/src/test/index.spec.ts b/src/test/index.spec.ts index 62a449a52..f085a3639 100644 --- a/src/test/index.spec.ts +++ b/src/test/index.spec.ts @@ -630,19 +630,19 @@ test.suite('ts-node', (test) => { expect(stderr).toBe(''); }); - test( - 'should be able to fork into a nested TypeScript script with a modified ' + - 'working directory', - async () => { - const { err, stdout, stderr } = await exec( - `${BIN_PATH} --cwd ./working-dir/forking/ index.ts` - ); + // Disabled due to bug: + // --cwd is passed to forked children when not using --esm, erroneously affects their entrypoint resolution. + // tracked/fixed by either https://github.com/TypeStrong/ts-node/issues/1834 + // or https://github.com/TypeStrong/ts-node/issues/1831 + test.skip('should be able to fork into a nested TypeScript script with a modified working directory', async () => { + const { err, stdout, stderr } = await exec( + `${BIN_PATH} --cwd ./working-dir/forking/ index.ts` + ); - expect(err).toBe(null); - expect(stdout.trim()).toBe('Passing: from main'); - expect(stderr).toBe(''); - } - ); + expect(err).toBe(null); + expect(stdout.trim()).toBe('Passing: from main'); + expect(stderr).toBe(''); + }); test.suite('should read ts-node options from tsconfig.json', (test) => { const BIN_EXEC = `"${BIN_PATH}" --project tsconfig-options/tsconfig.json`; diff --git a/tests/esm-child-process/process-forking-nested-esm/index.ts b/tests/esm-child-process/process-forking-nested-esm/index.ts index ff0f2e61b..1a24be2bb 100644 --- a/tests/esm-child-process/process-forking-nested-esm/index.ts +++ b/tests/esm-child-process/process-forking-nested-esm/index.ts @@ -1,9 +1,13 @@ import { fork } from 'child_process'; +import { dirname } from 'path'; +import { fileURLToPath } from 'url'; // Initially set the exit code to non-zero. We only set it to `0` when the // worker process finishes properly with the expected stdout message. process.exitCode = 1; +process.chdir(dirname(fileURLToPath(import.meta.url))); + const workerProcess = fork('./worker.ts', [], { stdio: 'pipe', }); diff --git a/tests/esm-child-process/process-forking-nested-relative/index.ts b/tests/esm-child-process/process-forking-nested-relative/index.ts index e0b27f3cf..ec3fde010 100644 --- a/tests/esm-child-process/process-forking-nested-relative/index.ts +++ b/tests/esm-child-process/process-forking-nested-relative/index.ts @@ -1,5 +1,6 @@ import { fork } from 'child_process'; -import { join } from 'path'; +import { dirname, join } from 'path'; +import { fileURLToPath } from 'url'; // Initially set the exit code to non-zero. We only set it to `0` when the // worker process finishes properly with the expected stdout message. @@ -7,7 +8,7 @@ process.exitCode = 1; const workerProcess = fork('./worker.ts', [], { stdio: 'pipe', - cwd: join(process.cwd(), 'subfolder/'), + cwd: join(dirname(fileURLToPath(import.meta.url)), 'subfolder'), }); let stdout = ''; diff --git a/tests/esm-child-process/process-forking/index.ts b/tests/esm-child-process/process-forking/index.ts index 2e3e05ec8..88a3bd61a 100644 --- a/tests/esm-child-process/process-forking/index.ts +++ b/tests/esm-child-process/process-forking/index.ts @@ -1,9 +1,13 @@ import { fork } from 'child_process'; +import { dirname } from 'path'; +import { fileURLToPath } from 'url'; // Initially set the exit code to non-zero. We only set it to `0` when the // worker process finishes properly with the expected stdout message. process.exitCode = 1; +process.chdir(dirname(fileURLToPath(import.meta.url))); + const workerProcess = fork('./worker.js', [], { stdio: 'pipe', }); From b7bcdcfbce3628a988579e0d013c498b455c5139 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Tue, 12 Jul 2022 20:01:23 -0400 Subject: [PATCH 08/11] use swc compiler to avoid issue with ancient TS versions not understanding import.meta.url syntax --- tests/working-dir/esm/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/working-dir/esm/tsconfig.json b/tests/working-dir/esm/tsconfig.json index f16e691d2..04e93e5c7 100644 --- a/tests/working-dir/esm/tsconfig.json +++ b/tests/working-dir/esm/tsconfig.json @@ -3,6 +3,6 @@ "module": "ESNext" }, "ts-node": { - "transpileOnly": true + "swc": true } } From 3d9e0408dcf5385287f75daddd2986e989b67f96 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Tue, 12 Jul 2022 20:05:01 -0400 Subject: [PATCH 09/11] Remove tests that I think are redundant (but I've asked for confirmation in code review) --- src/test/esm-loader.spec.ts | 40 ------------------- .../index.mts | 23 ----------- .../tsconfig.json | 8 ---- .../worker.mts | 3 -- tests/working-dir/esm-node-next/index.ts | 11 ----- tests/working-dir/esm-node-next/package.json | 3 -- tests/working-dir/esm-node-next/tsconfig.json | 8 ---- 7 files changed, 96 deletions(-) delete mode 100644 tests/esm-child-process/process-forking-nested-esm-node-next/index.mts delete mode 100644 tests/esm-child-process/process-forking-nested-esm-node-next/tsconfig.json delete mode 100644 tests/esm-child-process/process-forking-nested-esm-node-next/worker.mts delete mode 100644 tests/working-dir/esm-node-next/index.ts delete mode 100644 tests/working-dir/esm-node-next/package.json delete mode 100644 tests/working-dir/esm-node-next/tsconfig.json diff --git a/src/test/esm-loader.spec.ts b/src/test/esm-loader.spec.ts index 902f658b6..3a344e31f 100644 --- a/src/test/esm-loader.spec.ts +++ b/src/test/esm-loader.spec.ts @@ -372,26 +372,6 @@ test.suite('esm', (test) => { expect(stdout.trim()).toBe('Passing'); expect(stderr).toBe(''); }); - - test.suite( - 'with NodeNext TypeScript resolution and `.mts` extension', - (test) => { - test.runIf(tsSupportsStableNodeNextNode16); - - test('should have the correct working directory in the user entry-point', async () => { - const { err, stdout, stderr } = await exec( - `${BIN_PATH} --esm --cwd ./esm-node-next/ index.ts`, - { - cwd: resolve(TEST_DIR, 'working-dir'), - } - ); - - expect(err).toBe(null); - expect(stdout.trim()).toBe('Passing'); - expect(stderr).toBe(''); - }); - } - ); }); test.suite('esm child process and forking', (test) => { @@ -424,26 +404,6 @@ test.suite('esm', (test) => { expect(stdout.trim()).toBe('Passing: from main'); expect(stderr).toBe(''); }); - - test.suite( - 'with NodeNext TypeScript resolution and `.mts` extension', - (test) => { - test.runIf(tsSupportsStableNodeNextNode16); - - test('should be able to fork into a nested TypeScript ESM script', async () => { - const { err, stdout, stderr } = await exec( - `${BIN_PATH} --esm ./process-forking-nested-esm-node-next/index.mts`, - { - cwd: resolve(TEST_DIR, 'esm-child-process'), - } - ); - - expect(err).toBe(null); - expect(stdout.trim()).toBe('Passing: from main'); - expect(stderr).toBe(''); - }); - } - ); }); test.suite('parent passes signals to child', (test) => { diff --git a/tests/esm-child-process/process-forking-nested-esm-node-next/index.mts b/tests/esm-child-process/process-forking-nested-esm-node-next/index.mts deleted file mode 100644 index e76286d8e..000000000 --- a/tests/esm-child-process/process-forking-nested-esm-node-next/index.mts +++ /dev/null @@ -1,23 +0,0 @@ -import { fork } from 'child_process'; -import { dirname, join } from 'path'; -import { fileURLToPath } from 'url'; - -// Initially set the exit code to non-zero. We only set it to `0` when the -// worker process finishes properly with the expected stdout message. -process.exitCode = 1; - -const projectDir = dirname(fileURLToPath(import.meta.url)); -const workerProcess = fork(join(projectDir, 'worker.mts'), [], { - stdio: 'pipe', -}); - -let stdout = ''; - -workerProcess.stdout.on('data', (chunk) => (stdout += chunk.toString('utf8'))); -workerProcess.on('error', () => (process.exitCode = 1)); -workerProcess.on('close', (status, signal) => { - if (status === 0 && signal === null && stdout.trim() === 'Works') { - console.log('Passing: from main'); - process.exitCode = 0; - } -}); diff --git a/tests/esm-child-process/process-forking-nested-esm-node-next/tsconfig.json b/tests/esm-child-process/process-forking-nested-esm-node-next/tsconfig.json deleted file mode 100644 index 0b0d8971d..000000000 --- a/tests/esm-child-process/process-forking-nested-esm-node-next/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "compilerOptions": { - "module": "NodeNext" - }, - "ts-node": { - "transpileOnly": true - } -} diff --git a/tests/esm-child-process/process-forking-nested-esm-node-next/worker.mts b/tests/esm-child-process/process-forking-nested-esm-node-next/worker.mts deleted file mode 100644 index 4114d5ab0..000000000 --- a/tests/esm-child-process/process-forking-nested-esm-node-next/worker.mts +++ /dev/null @@ -1,3 +0,0 @@ -const message: string = 'Works'; - -console.log(message); diff --git a/tests/working-dir/esm-node-next/index.ts b/tests/working-dir/esm-node-next/index.ts deleted file mode 100644 index 21230f9d8..000000000 --- a/tests/working-dir/esm-node-next/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { strictEqual } from 'assert'; -import { normalize, dirname } from 'path'; -import { fileURLToPath } from 'url'; - -// Expect the working directory to be the parent directory. -strictEqual( - normalize(process.cwd()), - normalize(dirname(dirname(fileURLToPath(import.meta.url)))) -); - -console.log('Passing'); diff --git a/tests/working-dir/esm-node-next/package.json b/tests/working-dir/esm-node-next/package.json deleted file mode 100644 index 3dbc1ca59..000000000 --- a/tests/working-dir/esm-node-next/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "type": "module" -} diff --git a/tests/working-dir/esm-node-next/tsconfig.json b/tests/working-dir/esm-node-next/tsconfig.json deleted file mode 100644 index 0b0d8971d..000000000 --- a/tests/working-dir/esm-node-next/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "compilerOptions": { - "module": "NodeNext" - }, - "ts-node": { - "transpileOnly": true - } -} From 5164e2f0f9db01722de0bd7bdb2bbfa4f7d9394c Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Tue, 12 Jul 2022 20:19:21 -0400 Subject: [PATCH 10/11] fix another issue with old TS --- .../esm-child-process/process-forking-nested-esm/tsconfig.json | 2 +- .../process-forking-nested-relative/tsconfig.json | 2 +- tests/esm-child-process/process-forking/tsconfig.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/esm-child-process/process-forking-nested-esm/tsconfig.json b/tests/esm-child-process/process-forking-nested-esm/tsconfig.json index f16e691d2..04e93e5c7 100644 --- a/tests/esm-child-process/process-forking-nested-esm/tsconfig.json +++ b/tests/esm-child-process/process-forking-nested-esm/tsconfig.json @@ -3,6 +3,6 @@ "module": "ESNext" }, "ts-node": { - "transpileOnly": true + "swc": true } } diff --git a/tests/esm-child-process/process-forking-nested-relative/tsconfig.json b/tests/esm-child-process/process-forking-nested-relative/tsconfig.json index f16e691d2..04e93e5c7 100644 --- a/tests/esm-child-process/process-forking-nested-relative/tsconfig.json +++ b/tests/esm-child-process/process-forking-nested-relative/tsconfig.json @@ -3,6 +3,6 @@ "module": "ESNext" }, "ts-node": { - "transpileOnly": true + "swc": true } } diff --git a/tests/esm-child-process/process-forking/tsconfig.json b/tests/esm-child-process/process-forking/tsconfig.json index f16e691d2..04e93e5c7 100644 --- a/tests/esm-child-process/process-forking/tsconfig.json +++ b/tests/esm-child-process/process-forking/tsconfig.json @@ -3,6 +3,6 @@ "module": "ESNext" }, "ts-node": { - "transpileOnly": true + "swc": true } } From 58ddc33433f5039b63c63aa7ff024e8a5bdd11ed Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Wed, 13 Jul 2022 15:06:37 -0400 Subject: [PATCH 11/11] final review updates --- src/test/esm-loader.spec.ts | 10 +++++----- .../index.ts | 0 .../package.json | 0 .../tsconfig.json | 0 .../worker.js | 0 .../index.ts | 14 ++++++++------ .../package.json | 0 .../subfolder}/worker.ts | 0 .../tsconfig.json | 0 .../index.ts | 3 ++- .../package.json | 0 .../subfolder/worker.ts | 0 .../tsconfig.json | 0 13 files changed, 15 insertions(+), 12 deletions(-) rename tests/esm-child-process/{process-forking => process-forking-js}/index.ts (100%) rename tests/esm-child-process/{process-forking-nested-esm => process-forking-js}/package.json (100%) rename tests/esm-child-process/{process-forking-nested-esm => process-forking-js}/tsconfig.json (100%) rename tests/esm-child-process/{process-forking => process-forking-js}/worker.js (100%) rename tests/esm-child-process/{process-forking-nested-esm => process-forking-ts-abs}/index.ts (77%) rename tests/esm-child-process/{process-forking-nested-relative => process-forking-ts-abs}/package.json (100%) rename tests/esm-child-process/{process-forking-nested-esm => process-forking-ts-abs/subfolder}/worker.ts (100%) rename tests/esm-child-process/{process-forking-nested-relative => process-forking-ts-abs}/tsconfig.json (100%) rename tests/esm-child-process/{process-forking-nested-relative => process-forking-ts}/index.ts (90%) rename tests/esm-child-process/{process-forking => process-forking-ts}/package.json (100%) rename tests/esm-child-process/{process-forking-nested-relative => process-forking-ts}/subfolder/worker.ts (100%) rename tests/esm-child-process/{process-forking => process-forking-ts}/tsconfig.json (100%) diff --git a/src/test/esm-loader.spec.ts b/src/test/esm-loader.spec.ts index 3a344e31f..375012a76 100644 --- a/src/test/esm-loader.spec.ts +++ b/src/test/esm-loader.spec.ts @@ -377,7 +377,7 @@ test.suite('esm', (test) => { test.suite('esm child process and forking', (test) => { test('should be able to fork vanilla NodeJS script', async () => { const { err, stdout, stderr } = await exec( - `${BIN_PATH} --esm --cwd ./esm-child-process/ ./process-forking/index.ts` + `${BIN_PATH} --esm --cwd ./esm-child-process/ ./process-forking-js/index.ts` ); expect(err).toBe(null); @@ -385,9 +385,9 @@ test.suite('esm', (test) => { expect(stderr).toBe(''); }); - test('should be able to fork into a nested TypeScript ESM script', async () => { + test('should be able to fork TypeScript script', async () => { const { err, stdout, stderr } = await exec( - `${BIN_PATH} --esm --cwd ./esm-child-process/ ./process-forking-nested-esm/index.ts` + `${BIN_PATH} --esm --cwd ./esm-child-process/ ./process-forking-ts/index.ts` ); expect(err).toBe(null); @@ -395,9 +395,9 @@ test.suite('esm', (test) => { expect(stderr).toBe(''); }); - test('should be possible to fork into a nested TypeScript script with respect to the working directory', async () => { + test('should be able to fork TypeScript script by absolute path', async () => { const { err, stdout, stderr } = await exec( - `${BIN_PATH} --esm --cwd ./esm-child-process/ ./process-forking-nested-relative/index.ts` + `${BIN_PATH} --esm --cwd ./esm-child-process/ ./process-forking-ts-abs/index.ts` ); expect(err).toBe(null); diff --git a/tests/esm-child-process/process-forking/index.ts b/tests/esm-child-process/process-forking-js/index.ts similarity index 100% rename from tests/esm-child-process/process-forking/index.ts rename to tests/esm-child-process/process-forking-js/index.ts diff --git a/tests/esm-child-process/process-forking-nested-esm/package.json b/tests/esm-child-process/process-forking-js/package.json similarity index 100% rename from tests/esm-child-process/process-forking-nested-esm/package.json rename to tests/esm-child-process/process-forking-js/package.json diff --git a/tests/esm-child-process/process-forking-nested-esm/tsconfig.json b/tests/esm-child-process/process-forking-js/tsconfig.json similarity index 100% rename from tests/esm-child-process/process-forking-nested-esm/tsconfig.json rename to tests/esm-child-process/process-forking-js/tsconfig.json diff --git a/tests/esm-child-process/process-forking/worker.js b/tests/esm-child-process/process-forking-js/worker.js similarity index 100% rename from tests/esm-child-process/process-forking/worker.js rename to tests/esm-child-process/process-forking-js/worker.js diff --git a/tests/esm-child-process/process-forking-nested-esm/index.ts b/tests/esm-child-process/process-forking-ts-abs/index.ts similarity index 77% rename from tests/esm-child-process/process-forking-nested-esm/index.ts rename to tests/esm-child-process/process-forking-ts-abs/index.ts index 1a24be2bb..ec94e846d 100644 --- a/tests/esm-child-process/process-forking-nested-esm/index.ts +++ b/tests/esm-child-process/process-forking-ts-abs/index.ts @@ -1,16 +1,18 @@ import { fork } from 'child_process'; -import { dirname } from 'path'; +import { dirname, join } from 'path'; import { fileURLToPath } from 'url'; // Initially set the exit code to non-zero. We only set it to `0` when the // worker process finishes properly with the expected stdout message. process.exitCode = 1; -process.chdir(dirname(fileURLToPath(import.meta.url))); - -const workerProcess = fork('./worker.ts', [], { - stdio: 'pipe', -}); +const workerProcess = fork( + join(dirname(fileURLToPath(import.meta.url)), 'subfolder/worker.ts'), + [], + { + stdio: 'pipe', + } +); let stdout = ''; diff --git a/tests/esm-child-process/process-forking-nested-relative/package.json b/tests/esm-child-process/process-forking-ts-abs/package.json similarity index 100% rename from tests/esm-child-process/process-forking-nested-relative/package.json rename to tests/esm-child-process/process-forking-ts-abs/package.json diff --git a/tests/esm-child-process/process-forking-nested-esm/worker.ts b/tests/esm-child-process/process-forking-ts-abs/subfolder/worker.ts similarity index 100% rename from tests/esm-child-process/process-forking-nested-esm/worker.ts rename to tests/esm-child-process/process-forking-ts-abs/subfolder/worker.ts diff --git a/tests/esm-child-process/process-forking-nested-relative/tsconfig.json b/tests/esm-child-process/process-forking-ts-abs/tsconfig.json similarity index 100% rename from tests/esm-child-process/process-forking-nested-relative/tsconfig.json rename to tests/esm-child-process/process-forking-ts-abs/tsconfig.json diff --git a/tests/esm-child-process/process-forking-nested-relative/index.ts b/tests/esm-child-process/process-forking-ts/index.ts similarity index 90% rename from tests/esm-child-process/process-forking-nested-relative/index.ts rename to tests/esm-child-process/process-forking-ts/index.ts index ec3fde010..2d59e0aab 100644 --- a/tests/esm-child-process/process-forking-nested-relative/index.ts +++ b/tests/esm-child-process/process-forking-ts/index.ts @@ -6,9 +6,10 @@ import { fileURLToPath } from 'url'; // worker process finishes properly with the expected stdout message. process.exitCode = 1; +process.chdir(join(dirname(fileURLToPath(import.meta.url)), 'subfolder')); + const workerProcess = fork('./worker.ts', [], { stdio: 'pipe', - cwd: join(dirname(fileURLToPath(import.meta.url)), 'subfolder'), }); let stdout = ''; diff --git a/tests/esm-child-process/process-forking/package.json b/tests/esm-child-process/process-forking-ts/package.json similarity index 100% rename from tests/esm-child-process/process-forking/package.json rename to tests/esm-child-process/process-forking-ts/package.json diff --git a/tests/esm-child-process/process-forking-nested-relative/subfolder/worker.ts b/tests/esm-child-process/process-forking-ts/subfolder/worker.ts similarity index 100% rename from tests/esm-child-process/process-forking-nested-relative/subfolder/worker.ts rename to tests/esm-child-process/process-forking-ts/subfolder/worker.ts diff --git a/tests/esm-child-process/process-forking/tsconfig.json b/tests/esm-child-process/process-forking-ts/tsconfig.json similarity index 100% rename from tests/esm-child-process/process-forking/tsconfig.json rename to tests/esm-child-process/process-forking-ts/tsconfig.json