nx/packages/web/src/executors/file-server/file-server.impl.ts

221 lines
5.2 KiB
TypeScript

import { execFileSync, fork } from 'child_process';
import * as chalk from 'chalk';
import {
ExecutorContext,
joinPathFragments,
parseTargetString,
readTargetOptions,
workspaceLayout,
} from '@nrwl/devkit';
import ignore from 'ignore';
import { copyFileSync, readFileSync, unlinkSync } from 'fs';
import { Schema } from './schema';
import { watch } from 'chokidar';
import { platform } from 'os';
import { join, resolve } from 'path';
import { readModulePackageJson } from 'nx/src/utils/package-json';
// platform specific command name
const pmCmd = platform() === 'win32' ? `npx.cmd` : 'npx';
function getHttpServerArgs(options: Schema) {
const args = ['-c-1', '--cors'];
if (options.port) {
args.push(`-p=${options.port}`);
}
if (options.host) {
args.push(`-a=${options.host}`);
}
if (options.ssl) {
args.push(`-S`);
}
if (options.sslCert) {
args.push(`-C=${options.sslCert}`);
}
if (options.sslKey) {
args.push(`-K=${options.sslKey}`);
}
if (options.proxyUrl) {
args.push(`-P=${options.proxyUrl}`);
}
if (options.proxyOptions) {
Object.keys(options.proxyOptions).forEach((key) => {
args.push(`--proxy-options.${key}=${options.proxyOptions[key]}`);
});
}
return args;
}
function getBuildTargetCommand(options: Schema) {
const cmd = ['nx', 'run', options.buildTarget];
if (options.parallel) {
cmd.push(`--parallel`);
}
if (options.maxParallel) {
cmd.push(`--maxParallel=${options.maxParallel}`);
}
return cmd;
}
function getBuildTargetOutputPath(options: Schema, context: ExecutorContext) {
if (options.staticFilePath) {
return options.staticFilePath;
}
let buildOptions;
try {
const target = parseTargetString(options.buildTarget, context.projectGraph);
buildOptions = readTargetOptions(target, context);
} catch (e) {
throw new Error(`Invalid buildTarget: ${options.buildTarget}`);
}
// TODO: vsavkin we should also check outputs
const outputPath = buildOptions.outputPath;
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 getIgnoredGlobs(root: string) {
const ig = ignore();
try {
ig.add(readFileSync(`${root}/.gitignore`, 'utf-8'));
} catch {}
try {
ig.add(readFileSync(`${root}/.nxignore`, 'utf-8'));
} catch {}
return ig;
}
function createFileWatcher(
root: string,
changeHandler: () => void
): () => void {
const ignoredGlobs = getIgnoredGlobs(root);
const layout = workspaceLayout();
const watcher = watch(
[
joinPathFragments(layout.appsDir, '**'),
joinPathFragments(layout.libsDir, '**'),
],
{
cwd: root,
ignoreInitial: true,
}
);
watcher.on('all', (_event: string, path: string) => {
if (ignoredGlobs.ignores(path)) return;
changeHandler();
});
return () => watcher.close();
}
export default async function* fileServerExecutor(
options: Schema,
context: ExecutorContext
) {
let running = false;
const run = () => {
if (!running) {
running = true;
try {
const args = getBuildTargetCommand(options);
execFileSync(pmCmd, args, {
stdio: [0, 1, 2],
});
} catch {
throw new Error(
`Build target failed: ${chalk.bold(options.buildTarget)}`
);
} finally {
running = false;
}
}
};
let disposeWatch: () => void;
if (options.watch) {
disposeWatch = createFileWatcher(context.root, run);
}
// perform initial run
run();
const outputPath = getBuildTargetOutputPath(options, context);
if (options.spa) {
const src = join(outputPath, 'index.html');
const dst = join(outputPath, '404.html');
// See: https://github.com/http-party/http-server#magic-files
copyFileSync(src, dst);
}
const args = getHttpServerArgs(options);
const { path: pathToHttpServerPkgJson, packageJson } = readModulePackageJson(
'http-server',
module.paths
);
const pathToHttpServerBin = packageJson.bin['http-server'];
const pathToHttpServer = resolve(
pathToHttpServerPkgJson.replace('package.json', ''),
pathToHttpServerBin
);
const serve = 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) {
unlinkSync(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}:${
options.port
}`,
};
return new Promise<{ success: boolean }>((res) => {
serve.on('exit', (code) => {
if (code == 0) {
res({ success: true });
} else {
res({ success: false });
}
});
});
}