Skip to content

Commit fbc7754

Browse files
committed
fixup! Consistently perform bootstrap and encode Brotli config for improved caching/reduced complexity
Re-add fast-path for ESM when we detect it before phase3. And simplify code.
1 parent e43b665 commit fbc7754

File tree

4 files changed

+84
-67
lines changed

4 files changed

+84
-67
lines changed

src/bin.ts

Lines changed: 67 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ import { callInChildWithEsm } from './child/spawn-child-with-esm';
3232
import { findAndReadConfig } from './configuration';
3333
import { getChildProcessArguments } from './child/child-exec-args';
3434

35+
type MarkPropAsRequired<T, K extends keyof T> = Omit<T, K> &
36+
Required<Pick<T, K>>;
37+
3538
/**
3639
* Main `bin` functionality.
3740
*
@@ -73,66 +76,74 @@ export interface BootstrapState {
7376
* only capture information that should be persisted to e.g. forked child processes.
7477
*/
7578
export interface BootstrapStateForForkedProcesses {
76-
// For the final bootstrap we are only interested in the user arguments
77-
// that should be passed to the entry-point script (or eval script).
78-
// We don't want to encode any options that would break child forking. e.g.
79-
// persisting the `--eval` option would break `child_process.fork` in scripts.
80-
parseArgvResult: Pick<ReturnType<typeof parseArgv>, 'restArgs'>;
79+
// For the final bootstrap we are only interested in the user arguments that should
80+
// be passed to the entry-point script (or eval script). We don't want to encode any
81+
// other options from `parseArgvResult` that would break child forking.
82+
// e.g. persisting the `--eval` option would break `child_process.fork` in scripts.
83+
restArgs: string[];
8184
phase3Result: Pick<
8285
ReturnType<typeof phase3>,
8386
'enableEsmLoader' | 'preloadedConfig'
8487
>;
8588
}
8689

87-
export interface BootstrapStateInitialProcessChild
88-
extends Omit<BootstrapStateForForkedProcesses, 'initialProcessOptions'> {
89-
initialProcessOptions: { resolutionCwd: string } & Pick<
90-
ReturnType<typeof parseArgv>,
91-
// These are options which should not persist into forked child processes,
92-
// but can be passed-through in the initial child process creation -- but should
93-
// not be encoded in the Brotli state for child process forks (through `execArgv`.)
94-
'version' | 'showConfig' | 'code' | 'print' | 'interactive'
95-
>;
90+
export interface BootstrapStateInitialProcess
91+
extends Omit<BootstrapStateForForkedProcesses, 'phase3Result'> {
92+
initialChildArgv: ReturnType<typeof parseArgv>;
93+
initialChildResolutionCwd: string;
94+
phase3Result?: ReturnType<typeof phase3>;
9695
}
9796

98-
export type BootstrapStateForChild = Omit<
99-
BootstrapStateForForkedProcesses,
100-
'initialProcessOptions'
101-
> &
102-
Partial<BootstrapStateInitialProcessChild>;
97+
export type BootstrapStateForChild = BootstrapStateForForkedProcesses &
98+
Partial<BootstrapStateInitialProcess>;
10399

104100
/** @internal */
105101
export function bootstrap(state: BootstrapState) {
106102
state.phase2Result = phase2(state);
107-
state.phase3Result = phase3(state);
108-
109-
const initialChildState: BootstrapStateInitialProcessChild = {
110-
...createBootstrapStateForChildProcess(state as Required<BootstrapState>),
111-
// Aside with the default child process state, we attach the initial process
112-
// options since this `callInChild` invocation is from the initial process.
113-
// Later when forking, the initial process options are omitted / not persisted.
114-
initialProcessOptions: {
115-
code: state.parseArgvResult.code,
116-
interactive: state.parseArgvResult.interactive,
117-
print: state.parseArgvResult.print,
118-
showConfig: state.parseArgvResult.showConfig,
119-
version: state.parseArgvResult.version,
120-
resolutionCwd: state.phase2Result.resolutionCwd,
121-
},
103+
104+
const initialProcessState: BootstrapStateInitialProcess = {
105+
restArgs: state.parseArgvResult.restArgs,
106+
initialChildArgv: state.parseArgvResult,
107+
initialChildResolutionCwd: state.phase2Result.resolutionCwd,
122108
};
123109

110+
// Perf optimization for ESM until ESM hooks can be registered without needing
111+
// a child process. We skip phase3 and defer it to the child process where we
112+
// would load the TS compiler anyway, avoiding loading it twice in different processes.
113+
if (initialProcessState.initialChildArgv.esm) {
114+
callInChildWithEsm(initialProcessState, process.cwd());
115+
return;
116+
}
117+
118+
const phase3Result = phase3(initialProcessState);
119+
124120
// For ESM, we need to spawn a new Node process to be able to register our hooks.
125-
if (state.phase3Result.enableEsmLoader) {
121+
if (phase3Result.enableEsmLoader) {
126122
// Note: When transitioning into the child process for the final phase,
127123
// we want to preserve the initial user working directory.
128-
callInChildWithEsm(initialChildState, process.cwd());
124+
callInChildWithEsm(initialProcessState, process.cwd());
129125
} else {
130-
completeBootstrap(initialChildState);
126+
completeBootstrap({ ...initialProcessState, phase3Result });
131127
}
132128
}
133129
/** Final phase of the bootstrap. */
134-
export function completeBootstrap(state: BootstrapStateForChild) {
135-
return phase4(state);
130+
export function completeBootstrap(
131+
state: BootstrapStateForForkedProcesses | BootstrapStateInitialProcess
132+
) {
133+
// IMPORTANT: This is an optimization when we detected `--esm` early in the CLI.
134+
// In such cases we skip phase3 and let phase3 to be processed in the child process here.
135+
// This avoids loading the TS compiler twice as loading TS is rather slow.
136+
// TODO: Remove this when we don't need to spawn a child process for ESM. See:
137+
if (state.phase3Result === undefined) {
138+
state.phase3Result = phase3(state as BootstrapStateInitialProcess);
139+
}
140+
141+
return phase4(
142+
state as MarkPropAsRequired<
143+
BootstrapStateForForkedProcesses | BootstrapStateInitialProcess,
144+
'phase3Result'
145+
>
146+
);
136147
}
137148

138149
function parseArgv(argv: string[], entrypointArgs: Record<string, any>) {
@@ -394,8 +405,8 @@ function phase3(payload: BootstrapState) {
394405
scopeDir,
395406
esm,
396407
experimentalSpecifierResolution,
397-
} = payload.parseArgvResult;
398-
const { resolutionCwd } = payload.phase2Result!;
408+
} = payload.initialChildArgv;
409+
const resolutionCwd = payload.initialChildResolutionCwd;
399410

400411
// NOTE: When we transition to a child process for ESM, the entry-point script determined
401412
// here might not be the one used later in `phase4`. This can happen when we execute the
@@ -405,7 +416,7 @@ function phase3(payload: BootstrapState) {
405416
// See: https://github.com/TypeStrong/ts-node/issues/1812.
406417
const { entryPointPath } = getEntryPointInfo(
407418
resolutionCwd,
408-
payload.parseArgvResult!
419+
payload.initialChildArgv
409420
);
410421

411422
const preloadedConfig = findAndReadConfig({
@@ -498,10 +509,9 @@ function getEntryPointInfo(
498509
}
499510

500511
function phase4(payload: BootstrapStateForChild) {
501-
const { restArgs } = payload.parseArgvResult;
512+
const restArgs = payload.restArgs;
502513
const { preloadedConfig } = payload.phase3Result;
503-
const resolutionCwd =
504-
payload.initialProcessOptions?.resolutionCwd ?? process.cwd();
514+
const resolutionCwd = payload.initialChildResolutionCwd ?? process.cwd();
505515

506516
const {
507517
entryPointPath,
@@ -510,9 +520,9 @@ function phase4(payload: BootstrapStateForChild) {
510520
executeRepl,
511521
executeStdin,
512522
} = getEntryPointInfo(resolutionCwd, {
513-
code: payload.initialProcessOptions?.code,
514-
interactive: payload.initialProcessOptions?.interactive,
515-
restArgs: payload.parseArgvResult.restArgs,
523+
code: payload.initialChildArgv?.code,
524+
interactive: payload.initialChildArgv?.interactive,
525+
restArgs: payload.restArgs,
516526
});
517527

518528
/**
@@ -597,13 +607,13 @@ function phase4(payload: BootstrapStateForChild) {
597607
stdinStuff?.repl.setService(service);
598608

599609
// Output project information.
600-
if (payload.initialProcessOptions?.version === 2) {
610+
if (payload.initialChildArgv?.version === 2) {
601611
console.log(`ts-node v${VERSION}`);
602612
console.log(`node ${process.version}`);
603613
console.log(`compiler v${service.ts.version}`);
604614
process.exit(0);
605615
}
606-
if ((payload.initialProcessOptions?.version ?? 0) >= 3) {
616+
if ((payload.initialChildArgv?.version ?? 0) >= 3) {
607617
console.log(`ts-node v${VERSION} ${dirname(__dirname)}`);
608618
console.log(`node ${process.version}`);
609619
console.log(
@@ -612,7 +622,7 @@ function phase4(payload: BootstrapStateForChild) {
612622
process.exit(0);
613623
}
614624

615-
if (payload.initialProcessOptions?.showConfig) {
625+
if (payload.initialChildArgv?.showConfig) {
616626
const ts = service.ts as any as TSInternal;
617627
if (typeof ts.convertToTSConfig !== 'function') {
618628
console.error(
@@ -700,8 +710,8 @@ function phase4(payload: BootstrapStateForChild) {
700710
evalAndExitOnTsError(
701711
evalStuff!.repl,
702712
evalStuff!.module!,
703-
payload.initialProcessOptions!.code!,
704-
payload.initialProcessOptions!.print,
713+
payload.initialChildArgv!.code!,
714+
payload.initialChildArgv!.print,
705715
'eval'
706716
);
707717
}
@@ -711,15 +721,15 @@ function phase4(payload: BootstrapStateForChild) {
711721
}
712722

713723
if (executeStdin) {
714-
let buffer = payload.initialProcessOptions?.code ?? '';
724+
let buffer = payload.initialChildArgv?.code ?? '';
715725
process.stdin.on('data', (chunk: Buffer) => (buffer += chunk));
716726
process.stdin.on('end', () => {
717727
evalAndExitOnTsError(
718728
stdinStuff!.repl,
719729
stdinStuff!.module!,
720730
buffer,
721731
// `echo 123 | node -p` still prints 123
722-
payload.initialProcessOptions?.print ?? false,
732+
payload.initialChildArgv?.print ?? false,
723733
'stdin'
724734
);
725735
});
@@ -728,14 +738,12 @@ function phase4(payload: BootstrapStateForChild) {
728738
}
729739

730740
function createBootstrapStateForChildProcess(
731-
state: BootstrapStateInitialProcessChild | BootstrapStateForForkedProcesses
741+
state: BootstrapStateInitialProcess | BootstrapStateForForkedProcesses
732742
): BootstrapStateForForkedProcesses {
733743
// NOTE: Build up the child process fork bootstrap state manually so that we do
734744
// not encode unnecessary properties into the bootstrap state that is persisted
735745
return {
736-
parseArgvResult: {
737-
restArgs: state.parseArgvResult.restArgs,
738-
},
746+
restArgs: state.restArgs,
739747
phase3Result: {
740748
enableEsmLoader: state.phase3Result!.enableEsmLoader,
741749
preloadedConfig: state.phase3Result!.preloadedConfig,

src/child/child-entrypoint.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
1-
import { completeBootstrap, BootstrapStateForChild } from '../bin';
1+
import {
2+
completeBootstrap,
3+
BootstrapStateInitialProcess,
4+
BootstrapStateForForkedProcesses,
5+
} from '../bin';
26
import { argPrefix, decompress } from './argv-payload';
37

48
const base64ConfigArg = process.argv[2];
59
if (!base64ConfigArg.startsWith(argPrefix)) throw new Error('unexpected argv');
610
const base64Payload = base64ConfigArg.slice(argPrefix.length);
7-
const state = decompress(base64Payload) as BootstrapStateForChild;
11+
const state = decompress(base64Payload) as
12+
| BootstrapStateForForkedProcesses
13+
| BootstrapStateInitialProcess;
814

9-
state.parseArgvResult.restArgs = process.argv.slice(3);
15+
state.restArgs = process.argv.slice(3);
1016

1117
completeBootstrap(state);

src/child/child-exec-args.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import { pathToFileURL } from 'url';
22
import { brotliCompressSync } from 'zlib';
3-
import type { BootstrapStateForChild } from '../bin';
3+
import type {
4+
BootstrapStateForForkedProcesses,
5+
BootstrapStateInitialProcess,
6+
} from '../bin';
47
import { argPrefix } from './argv-payload';
58

69
export function getChildProcessArguments(
710
enableEsmLoader: boolean,
8-
state: BootstrapStateForChild
11+
state: BootstrapStateForForkedProcesses | BootstrapStateInitialProcess
912
) {
1013
const nodeExecArgs = [];
1114

src/child/spawn-child-with-esm.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { fork } from 'child_process';
2-
import type { BootstrapStateForChild } from '../bin';
2+
import type { BootstrapStateInitialProcess } from '../bin';
33
import { getChildProcessArguments } from './child-exec-args';
44

55
/**
@@ -11,13 +11,13 @@ import { getChildProcessArguments } from './child-exec-args';
1111
* the child process.
1212
*/
1313
export function callInChildWithEsm(
14-
state: BootstrapStateForChild,
14+
state: BootstrapStateInitialProcess,
1515
targetCwd: string
1616
) {
1717
const { childScriptArgs, childScriptPath, nodeExecArgs } =
1818
getChildProcessArguments(/* enableEsmLoader */ true, state);
1919

20-
childScriptArgs.push(...state.parseArgvResult.restArgs);
20+
childScriptArgs.push(...state.restArgs);
2121

2222
const child = fork(childScriptPath, childScriptArgs, {
2323
stdio: 'inherit',

0 commit comments

Comments
 (0)