diff --git a/package.json b/package.json index ab80fe5..30fdfe6 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "dox:serve": "flecks dox docusaurus && cd website && DOCUSAURUS_GENERATED_FILES_DIR_NAME=node_modules/.cache/docusaurus node_modules/.bin/docusaurus build --no-minify --out-dir ../dox-tmp && node_modules/.bin/docusaurus serve --dir ../dox-tmp", "dox": "flecks dox docusaurus && cd website && DOCUSAURUS_GENERATED_FILES_DIR_NAME=node_modules/.cache/docusaurus node_modules/.bin/docusaurus", "lint": "node build/tasks lint", - "test": "node build/tasks -- test -t 60000" + "test": "node build/tasks -- test -t 300000" }, "devDependencies": { "husky": "^9.0.7", diff --git a/packages/build/build/commands.js b/packages/build/build/commands.js index 3ee2e09..1b44e41 100644 --- a/packages/build/build/commands.js +++ b/packages/build/build/commands.js @@ -18,6 +18,7 @@ const { const D = require('@flecks/core/build/debug'); const { add, + binaryPath, lockFile, spawnWith, } = require('@flecks/core/src/server'); @@ -178,7 +179,7 @@ exports.commands = (program, flecks) => { debug('Building...', opts); const webpackConfig = await flecks.resolveBuildConfig('fleckspack.config.js'); const cmd = [ - 'npx', 'webpack', + await binaryPath('webpack'), ...((watch || hot) ? ['watch'] : []), '--config', webpackConfig, '--mode', (production && !hot) ? 'production' : 'development', @@ -210,7 +211,7 @@ exports.commands = (program, flecks) => { .map((pkg) => join(process.cwd(), pkg)) .map(async (cwd) => { const cmd = [ - 'npx', 'eslint', + await binaryPath('eslint'), '--config', await flecks.resolveBuildConfig('eslint.config.js'), '.', ]; diff --git a/packages/core/src/server/process.js b/packages/core/src/server/process.js index 9af9bef..a624ee9 100644 --- a/packages/core/src/server/process.js +++ b/packages/core/src/server/process.js @@ -1,10 +1,22 @@ -const {spawn} = require('child_process'); +const {exec, spawn} = require('child_process'); const D = require('../../build/debug'); const debug = D('@flecks/core/server'); const debugSilly = debug.extend('silly'); +exports.binaryPath = (binary) => ( + new Promise((resolve, reject) => { + exec(`npx which ${binary}`, (error, stdout) => { + if (error) { + reject(error); + return; + } + resolve(stdout.trim()); + }); + }) +); + exports.processCode = (child) => new Promise((resolve, reject) => { child.on('error', reject); child.on('exit', (code) => { @@ -13,6 +25,8 @@ exports.processCode = (child) => new Promise((resolve, reject) => { }); }); +const children = []; + exports.spawnWith = (cmd, opts = {}) => { debug("spawning: '%s'", cmd.join(' ')); debugSilly('with options: %O', opts); @@ -24,5 +38,31 @@ exports.spawnWith = (cmd, opts = {}) => { ...opts.env, }, }); + children.push(child); + child.on('exit', () => { + children.splice(children.indexOf(child), 1); + }); return child; }; + +let killed = false; + +function handleTerminationEvent(signal) { + // Clean up on exit. + process.on(signal, () => { + if (killed) { + return; + } + killed = true; + children.forEach((child) => { + child.kill(); + }); + if ('exit' !== signal) { + process.exit(1); + } + }); +} + +handleTerminationEvent('exit'); +handleTerminationEvent('SIGINT'); +handleTerminationEvent('SIGTERM'); diff --git a/packages/server/build/server.webpack.config.js b/packages/server/build/server.webpack.config.js index 80405a7..83b595f 100644 --- a/packages/server/build/server.webpack.config.js +++ b/packages/server/build/server.webpack.config.js @@ -58,7 +58,7 @@ module.exports = async (env, argv, flecks) => { NODE_PRESERVE_SYMLINKS: flecks.roots.some(([path, request]) => path !== request) ? 1 : 0, }, exec: 'index.js', - killOnExit: !!hot, + killOnExit: !hot, // Bail hard on unhandled rejections and report. nodeArgs: [...nodeArgs, '--unhandled-rejections=strict', '--trace-uncaught'], // HMR. diff --git a/packages/server/build/start.js b/packages/server/build/start.js index a2b1081..067fe79 100644 --- a/packages/server/build/start.js +++ b/packages/server/build/start.js @@ -16,6 +16,12 @@ class StartServerPlugin { signal: false, ...('string' === typeof options ? {name: options} : options), }; + ['exit', 'SIGINT', 'SIGTERM'] + .forEach((event) => { + process.on(event, () => { + this.worker.kill('exit' === event ? 'SIGKILL' : event); + }); + }); } apply(compiler) { @@ -85,23 +91,21 @@ class StartServerPlugin { args, ...(inspectPort && {inspectPort}), }); - const setupListeners = (worker) => { - if (killOnExit) { - worker.on('exit', () => { - process.exit(); - }); - } - worker.on('message', (message) => { - if ('hmr-restart' === message) { + this.worker = cluster.fork(env); + if (killOnExit) { + this.worker.on('exit', (code) => { + process.exit(code); + }); + } + else { + this.worker.on('disconnect', () => { + if (this.worker.exitedAfterDisconnect) { // eslint-disable-next-line no-console console.error('[HMR] Restarting application...'); this.worker = cluster.fork(env); - setupListeners(this.worker); } }); - }; - this.worker = cluster.fork(env); - setupListeners(this.worker); + } return new Promise((resolve, reject) => { this.worker.on('error', reject); this.worker.on('online', resolve); diff --git a/packages/server/src/entry.js b/packages/server/src/entry.js index 4107ff6..3b62ce4 100644 --- a/packages/server/src/entry.js +++ b/packages/server/src/entry.js @@ -35,7 +35,7 @@ import {D, Flecks} from '@flecks/core'; if (module.hot) { module.hot.accept('./runtime', () => { if (cluster.isWorker) { - cluster.worker.send('hmr-restart'); + cluster.worker.disconnect(); const error = new Error('Restart requested!'); error.stack = ''; throw error; diff --git a/packages/server/test/server/build/build.js b/packages/server/test/server/build/build.js index a20f519..5e9bd74 100644 --- a/packages/server/test/server/build/build.js +++ b/packages/server/test/server/build/build.js @@ -2,7 +2,7 @@ import {cp, mkdir} from 'fs/promises'; import {join} from 'path'; import {rimraf} from '@flecks/build/server'; -import {processCode, spawnWith} from '@flecks/core/server'; +import {binaryPath, processCode, spawnWith} from '@flecks/core/server'; import {listen} from './listen'; @@ -21,9 +21,9 @@ export async function createApplicationAt(path) { return qualified; } -export function build(path, {args = [], opts = {}} = {}) { - return processCode(spawnWith( - ['npx', 'flecks', 'build', ...args], +export async function buildChild(path, {args = [], opts = {}} = {}) { + return spawnWith( + [await binaryPath('flecks'), 'build', ...args], { ...opts, env: { @@ -33,7 +33,11 @@ export function build(path, {args = [], opts = {}} = {}) { ...opts.env, }, }, - )); + ); +} + +export async function build(path, {args = [], opts = {}} = {}) { + return processCode(await buildChild(path, {args, opts})); } export async function serverActions(path, actions) { diff --git a/packages/server/test/server/template/comm/src/server.js b/packages/server/test/server/template/comm/src/server.js index 5106427..0a9fd22 100644 --- a/packages/server/test/server/template/comm/src/server.js +++ b/packages/server/test/server/template/comm/src/server.js @@ -1,3 +1,4 @@ +import cluster from 'cluster'; import {createConnection} from 'net'; const { @@ -18,7 +19,12 @@ export const hooks = { if (!FLECKS_SERVER_TEST_SOCKET) { return; } - const socket = createConnection({path: FLECKS_SERVER_TEST_SOCKET}); + const socket = createConnection(FLECKS_SERVER_TEST_SOCKET); + if (cluster.isWorker) { + cluster.worker.on('disconnect', () => { + socket.end(); + }); + } flecks.socket = socket; socket.on('connect', () => { socket.on('data', (data) => { diff --git a/packages/web/build/flecks.bootstrap.js b/packages/web/build/flecks.bootstrap.js index caa0b90..59a8e1e 100644 --- a/packages/web/build/flecks.bootstrap.js +++ b/packages/web/build/flecks.bootstrap.js @@ -174,7 +174,7 @@ exports.hooks = { '--hot', '--config', await flecks.resolveBuildConfig('fleckspack.config.js'), ]; - const child = spawnWith( + spawnWith( cmd, { env: { @@ -182,10 +182,6 @@ exports.hooks = { }, }, ); - // Clean up on exit. - process.on('exit', () => { - child.kill(); - }); // Remove the build config since we're handing off to WDS. delete configs.web; },