@whook/whook
Version:
Build strong and efficient REST web services.
163 lines • 6.17 kB
JavaScript
import chokidar from 'chokidar';
import { dirname, join } from 'node:path';
import crypto from 'node:crypto';
import { PassThrough } from 'node:stream';
import { createWriteStream } from 'node:fs';
import initGenerateOpenAPITypes from './commands/generateOpenAPITypes.js';
import initWatchResolve from './services/watchResolve.js';
import { readFile } from 'node:fs/promises';
import ignore from 'ignore';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { Knifecycle, constant } from 'knifecycle';
import { printStackTrace } from 'yerror';
let $instance;
let log;
let delay;
let delayPromise;
let hash;
export const DEFAULT_IGNORED = ['node_modules', '*.d.ts', '.git'];
export const DEFAULT_WATCHED = ['src', 'package.json', 'package-lock.json'];
export async function watchDevProcess({ injectedNames = [], ignored, watched, afterRestartEnd, } = {
injectedNames: [],
}) {
let restartsCounter = 0;
const ignoreFilter = await (async () => {
let ignoreFileContent;
let ignoreBuilder = ignore();
ignoreBuilder = ignoreBuilder.add(ignored?.length ? ignored : DEFAULT_IGNORED);
try {
ignoreFileContent = (await readFile('.gitignore')).toString();
ignoreBuilder = ignoreBuilder.add(ignoreFileContent || []);
}
catch (err) {
// TODO: requires a deeper integration of Knifecycle to
// start with a silo containing only the environment
// and then another one for the server. It would allow
// to wrap the chokidar watch into a service too.
log?.('debug', '🤷 - Cannot find/parse .gitignore');
log?.('debug-stack', printStackTrace(err));
}
return ignoreBuilder.createFilter();
})();
await restartDevProcess({ injectedNames, afterRestartEnd, restartsCounter });
await new Promise((resolve, reject) => {
chokidar
.watch(watched?.length ? watched : DEFAULT_WATCHED, {
ignored: (str, stats) => {
if (!stats?.isFile()) {
return false;
}
return !ignoreFilter(str);
},
ignoreInitial: true,
})
.once('ready', () => {
resolve();
})
.once('error', (err) => {
reject(err);
})
.on('all', (_event, filePath) => {
// TODO: determine all the files needing a complete restart
if (filePath.match(/package.*\.json/) ||
filePath.match(/src\/[^/]*\.ts/)) {
log('warning', `☢️ - A file changed that may need a full restart (${filePath}).`);
}
if (delay) {
if (!delayPromise) {
delayPromise = delay.create(2000);
restartDevProcess({
injectedNames,
afterRestartEnd,
restartsCounter: restartsCounter++,
});
}
}
});
});
}
export async function restartDevProcess({ injectedNames = [], afterRestartEnd, restartsCounter, }) {
if ($instance) {
log('warning', `➡️ - Changes detected : Will restart the server soon (${restartsCounter})...`);
await delayPromise;
await $instance.destroy();
}
const { runProcess, prepareEnvironment, prepareProcess } = await import(pathToFileURL(join(process.cwd(), 'src', 'index.ts')).toString() +
(restartsCounter ? '?restartsCounter=' + restartsCounter : ''));
async function prepareWatchEnvironment($ = new Knifecycle()) {
$ = await prepareEnvironment($);
$.register(initWatchResolve);
$.register(constant('RESTARTS_COUNTER', restartsCounter));
return $;
}
const { ENV, OPEN_API_TYPES_CONFIG, MAIN_FILE_URL, $instance: _instance, delay: _delay, getOpenAPI, log: _log, ...additionalServices } = (await runProcess(prepareWatchEnvironment, prepareProcess, [
...new Set([
...injectedNames,
'ENV',
'OPEN_API_TYPES_CONFIG',
'MAIN_FILE_URL',
'$instance',
'delay',
'getOpenAPI',
'log',
]),
]));
$instance = _instance;
delay = _delay;
log = _log;
delayPromise = undefined;
const response = await getOpenAPI({
options: {
authenticated: true,
},
query: {
mutedMethods: ['options'],
mutedParameters: [],
},
});
const openAPIData = JSON.stringify(response.body);
const newHash = crypto.createHash('md5').update(openAPIData).digest('hex');
const apiChanged = hash !== newHash;
if (apiChanged) {
hash = newHash;
log('warning', '🦄 - API Changed : Generating API types...');
const instream = new PassThrough();
const bridge = new PassThrough();
const openAPITypesGenerationPromise = (await initGenerateOpenAPITypes({
OPEN_API_TYPES_CONFIG,
instream,
outstream: bridge,
log,
}))({
command: 'whook',
namedArguments: {},
rest: [],
});
const writeStream = createWriteStream(join(dirname(fileURLToPath(MAIN_FILE_URL)), 'openAPISchema.d.ts'));
const writeStreamCompletionPromise = new Promise((resolve, reject) => {
writeStream.once('finish', resolve);
writeStream.once('error', reject);
});
bridge.pipe(writeStream);
instream.write(openAPIData);
instream.end();
await Promise.all([
openAPITypesGenerationPromise,
writeStreamCompletionPromise,
]);
log('warning', '🦄 - API types generated!');
}
if (afterRestartEnd) {
await afterRestartEnd({
ENV,
OPEN_API_TYPES_CONFIG,
MAIN_FILE_URL,
$instance,
delay,
getOpenAPI,
log,
...additionalServices,
}, { apiChanged, openAPIData });
}
}
//# sourceMappingURL=watch.js.map