@nx/web
Version:
223 lines (222 loc) • 8.52 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = fileServerExecutor;
const internal_1 = require("@nx/devkit/internal");
const child_process_1 = require("child_process");
const pc = require("picocolors");
const devkit_1 = require("@nx/devkit");
const fs_1 = require("fs");
const os_1 = require("os");
const path_1 = require("path");
const package_json_1 = require("nx/src/utils/package-json");
const client_1 = require("nx/src/daemon/client/client");
const utils_1 = require("nx/src/tasks-runner/utils");
const detectPort = require('detect-port');
// platform specific command name
const pmCmd = (0, os_1.platform)() === 'win32' ? `npx.cmd` : 'npx';
function getHttpServerArgs(options) {
const { buildTarget, parallel, host, proxyUrl, ssl, sslCert, sslKey, proxyOptions, watch, spa, cacheSeconds, ...rest } = options;
const args = [`-c${options.cacheSeconds}`];
for (const [key, value] of Object.entries(rest)) {
if (typeof value === 'boolean' && value) {
args.push(`--${key}`);
}
else if (typeof value === 'string') {
args.push(`--${key}=${value}`);
}
}
if (host) {
args.push(`-a=${host}`);
}
if (ssl) {
args.push(`-S`);
}
if (sslCert) {
args.push(`-C=${sslCert}`);
}
if (sslKey) {
args.push(`-K=${sslKey}`);
}
if (proxyUrl) {
args.push(`-P=${proxyUrl}`);
}
if (proxyOptions) {
Object.keys(options.proxyOptions).forEach((key) => {
args.push(`--proxy-options.${key}=${options.proxyOptions[key]}`);
});
}
return args;
}
function getBuildTargetCommand(options, context) {
const target = (0, devkit_1.parseTargetString)(options.buildTarget, context);
const cmd = ['nx', 'run'];
if (target.configuration) {
cmd.push(`${target.project}:${target.target}:${target.configuration}`);
}
else {
cmd.push(`${target.project}:${target.target}`);
}
if (options.parallel) {
cmd.push(`--parallel`);
}
if (options.maxParallel) {
cmd.push(`--maxParallel=${options.maxParallel}`);
}
return cmd;
}
function getBuildTargetOutputPath(options, context) {
if (options.staticFilePath) {
return options.staticFilePath;
}
let outputPath;
try {
const target = (0, devkit_1.parseTargetString)(options.buildTarget, context);
const buildOptions = (0, devkit_1.readTargetOptions)(target, context);
if (buildOptions?.outputPath) {
outputPath = buildOptions.outputPath;
}
else {
const project = context.projectGraph.nodes[context.projectName];
const buildTarget = project.data.targets[target.target];
outputPath = buildTarget.outputs?.[0];
if (outputPath)
outputPath = (0, utils_1.interpolate)(outputPath, {
projectName: project.data.name,
projectRoot: project.data.root,
});
}
}
catch (e) {
throw new Error(`Invalid buildTarget: ${options.buildTarget}`);
}
if (!outputPath) {
throw new Error(`Unable to get the outputPath from buildTarget ${options.buildTarget}. Make sure ${options.buildTarget} has an outputPath property or manually provide an staticFilePath property`);
}
return outputPath;
}
function createFileWatcher(project, changeHandler) {
return client_1.daemonClient.registerFileWatcher({
watchProjects: project ? [project] : 'all',
includeGlobalWorkspaceFiles: true,
includeDependentProjects: true,
}, async (error, val) => {
if (error === 'closed') {
throw new Error('Watch error: Daemon closed the connection');
}
else if (error) {
throw new Error(`Watch error: ${error?.message ?? 'Unknown'}`);
}
else if (val?.changedFiles.length > 0) {
changeHandler();
}
});
}
async function* fileServerExecutor(options, context) {
if (!options.buildTarget && !options.staticFilePath) {
throw new Error("You must set either 'buildTarget' or 'staticFilePath'.");
}
if (options.watch && !options.buildTarget) {
throw new Error("Watch error: You can only specify 'watch' when 'buildTarget' is set.");
}
let running = false;
let disposeWatch;
if (options.buildTarget) {
const run = () => {
if (!running) {
running = true;
/**
* Expose a variable to the build target to know if it's being run by the serve-static executor
* This is useful because a config might need to change if it's being run by serve-static without the user's input
* or if being ran by another executor (eg. E2E tests)
* */
process.env.NX_SERVE_STATIC_BUILD_RUNNING = 'true';
try {
const args = getBuildTargetCommand(options, context);
(0, child_process_1.execFileSync)(pmCmd, args, {
stdio: [0, 1, 2],
shell: true,
windowsHide: false,
});
}
catch {
throw new Error(`Build target failed: ${pc.bold(options.buildTarget)}`);
}
finally {
process.env.NX_SERVE_STATIC_BUILD_RUNNING = undefined;
running = false;
}
}
};
if (!client_1.daemonClient.enabled() && options.watch) {
devkit_1.output.warn({
title: 'Nx Daemon is not enabled. Static server is not watching for changes.',
});
}
if (client_1.daemonClient.enabled() && options.watch) {
disposeWatch = await createFileWatcher(context.projectName, run);
}
// perform initial run
run();
}
const port = await detectPort(options.port || 8080);
const outputPath = getBuildTargetOutputPath(options, context);
if (options.spa) {
const src = (0, path_1.join)(outputPath, 'index.html');
const dst = (0, path_1.join)(outputPath, '404.html');
// See: https://github.com/http-party/http-server#magic-files
(0, fs_1.copyFileSync)(src, dst);
// We also need to ensure the proxyUrl is set, otherwise the browser will continue to throw a 404 error
// This can cause unexpected behaviors and failures especially in automated test suites
options.proxyUrl ??= `http${options.ssl ? 's' : ''}://localhost:${port}?`;
}
const args = getHttpServerArgs(options);
const { path: pathToHttpServerPkgJson, packageJson } = (0, package_json_1.readModulePackageJson)('http-server', module.paths);
const pathToHttpServerBin = packageJson.bin['http-server'];
const pathToHttpServer = (0, path_1.resolve)(pathToHttpServerPkgJson.replace('package.json', ''), pathToHttpServerBin);
// detect port as close to when used to prevent port being used by another process
// when running in parallel
args.push(`-p=${port}`);
const serve = (0, child_process_1.fork)(pathToHttpServer, [outputPath, ...args], {
stdio: 'pipe',
cwd: context.root,
env: {
FORCE_COLOR: 'true',
...process.env,
},
});
const processExitListener = () => {
serve.kill();
if (disposeWatch) {
disposeWatch();
}
if (options.spa) {
(0, fs_1.unlinkSync)((0, path_1.join)(outputPath, '404.html'));
}
};
process.on('exit', processExitListener);
process.on('SIGTERM', processExitListener);
serve.stdout.on('data', (chunk) => {
if (chunk.toString().indexOf('GET') === -1) {
process.stdout.write(chunk);
}
});
serve.stderr.on('data', (chunk) => {
process.stderr.write(chunk);
});
yield {
success: true,
baseUrl: `${options.ssl ? 'https' : 'http'}://${options.host}:${port}`,
};
return new Promise((res) => {
serve.on('exit', (code, signal) => {
if (code === null)
code = (0, internal_1.signalToCode)(signal);
if (code == 0) {
res({ success: true });
}
else {
res({ success: false });
}
});
});
}