Files
vercel/packages/node/src/fork-dev-server.ts
2023-04-26 12:23:43 -05:00

99 lines
2.9 KiB
TypeScript

import once from '@tootallnate/once';
import { cloneEnv } from '@vercel/build-utils';
import type { Config, Meta } from '@vercel/build-utils';
import { ChildProcess, fork, ForkOptions } from 'child_process';
import { pathToFileURL } from 'url';
import { join } from 'path';
export function forkDevServer(options: {
tsConfig: any;
config: Config;
maybeTranspile: boolean;
workPath: string | undefined;
isTypeScript: boolean;
isEsm: boolean;
require_: NodeRequire;
entrypoint: string;
meta: Meta;
/**
* A path to the dev-server path. This is used in tests.
*/
devServerPath?: string;
}) {
let nodeOptions = process.env.NODE_OPTIONS;
const tsNodePath = options.require_.resolve('ts-node');
const esmLoader = pathToFileURL(join(tsNodePath, '..', '..', 'esm.mjs'));
const cjsLoader = join(tsNodePath, '..', '..', 'register', 'index.js');
const devServerPath =
options.devServerPath || join(__dirname, 'dev-server.mjs');
if (options.maybeTranspile) {
if (options.isTypeScript) {
nodeOptions = `--loader ${esmLoader} ${nodeOptions || ''}`;
} else {
if (options.isEsm) {
// no transform needed because Node.js supports ESM natively
} else {
nodeOptions = `--require ${cjsLoader} ${nodeOptions || ''}`;
}
}
}
const forkOptions: ForkOptions = {
cwd: options.workPath,
execArgv: [],
env: cloneEnv(process.env, options.meta.env, {
VERCEL_DEV_ENTRYPOINT: options.entrypoint,
VERCEL_DEV_CONFIG: JSON.stringify(options.config),
VERCEL_DEV_BUILD_ENV: JSON.stringify(options.meta.buildEnv || {}),
TS_NODE_TRANSPILE_ONLY: '1',
TS_NODE_COMPILER_OPTIONS: options.tsConfig?.compilerOptions
? JSON.stringify(options.tsConfig.compilerOptions)
: undefined,
NODE_OPTIONS: nodeOptions,
}),
};
const child = fork(devServerPath, [], forkOptions);
checkForPid(devServerPath, child);
return child;
}
function checkForPid(
path: string,
process: ChildProcess
): asserts process is ChildProcess & { pid: number } {
if (!process.pid) {
throw new Error(`Child Process has no "pid" when forking: "${path}"`);
}
}
/**
* When launching a dev-server, we want to know its state.
* This function will be used to know whether it was exited (due to some error),
* or it is listening to new requests, and we can start proxying requests.
*/
export async function readMessage(
child: ChildProcess
): Promise<
| { state: 'message'; value: { port: number } }
| { state: 'exit'; value: [number, string | null] }
> {
const onMessage = once<{ port: number }>(child, 'message');
const onExit = once.spread<[number, string | null]>(child, 'close');
const result = await Promise.race([
onMessage.then(x => {
return { state: 'message' as const, value: x };
}),
onExit.then(v => {
return { state: 'exit' as const, value: v };
}),
]);
onExit.cancel();
onMessage.cancel();
return result;
}