@graphql-codegen/cli
Version:
<p align="center"> <img src="https://github.com/dotansimha/graphql-code-generator/blob/master/logo.png?raw=true" /> </p>
224 lines (223 loc) • 11.3 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.createWatcher = void 0;
const tslib_1 = require("tslib");
const path_1 = require("path");
const plugin_helpers_1 = require("@graphql-codegen/plugin-helpers");
const debounce_1 = tslib_1.__importDefault(require("debounce"));
const micromatch_1 = tslib_1.__importDefault(require("micromatch"));
const log_symbols_1 = tslib_1.__importDefault(require("log-symbols"));
const codegen_js_1 = require("../codegen.js");
const config_js_1 = require("../config.js");
const hooks_js_1 = require("../hooks.js");
const file_system_js_1 = require("./file-system.js");
const debugging_js_1 = require("./debugging.js");
const logger_js_1 = require("./logger.js");
const patterns_js_1 = require("./patterns.js");
const abort_controller_polyfill_js_1 = require("./abort-controller-polyfill.js");
function log(msg) {
// double spaces to inline the message with Listr
(0, logger_js_1.getLogger)().info(` ${msg}`);
}
function emitWatching(watchDir) {
log(`${log_symbols_1.default.info} Watching for changes in ${watchDir}...`);
}
const createWatcher = (initialContext, onNext) => {
(0, debugging_js_1.debugLog)(`[Watcher] Starting watcher...`);
let config = initialContext.getConfig();
const globalPatternSet = (0, patterns_js_1.makeGlobalPatternSet)(initialContext);
const localPatternSets = Object.keys(config.generates)
.map(filename => (0, plugin_helpers_1.normalizeOutputParam)(config.generates[filename]))
.map(conf => (0, patterns_js_1.makeLocalPatternSet)(conf));
const allAffirmativePatterns = (0, patterns_js_1.allAffirmativePatternsFromPatternSets)([globalPatternSet, ...localPatternSets]);
const shouldRebuild = (0, patterns_js_1.makeShouldRebuild)({ globalPatternSet, localPatternSets });
let watcherSubscription;
const runWatcher = async (abortSignal) => {
const watchDirectory = await findHighestCommonDirectory(allAffirmativePatterns);
// Try to load the parcel watcher, but don't fail if it's not available.
let parcelWatcher;
try {
parcelWatcher = await Promise.resolve().then(() => tslib_1.__importStar(require('@parcel/watcher')));
}
catch (err) {
log(`Failed to import @parcel/watcher due to the following error (to use watch mode, install https://www.npmjs.com/package/@parcel/watcher):\n${err}`);
return;
}
(0, debugging_js_1.debugLog)(`[Watcher] Parcel watcher loaded...`);
let isShutdown = false;
const debouncedExec = (0, debounce_1.default)(() => {
if (!isShutdown) {
(0, codegen_js_1.executeCodegen)(initialContext)
.then(onNext, () => Promise.resolve())
.then(() => emitWatching(watchDirectory));
}
}, 100);
emitWatching(watchDirectory);
const ignored = ['**/.git/**'];
for (const entry of Object.keys(config.generates).map(filename => ({
filename,
config: (0, plugin_helpers_1.normalizeOutputParam)(config.generates[filename]),
}))) {
// ParcelWatcher expects relative ignore patterns to be relative from watchDirectory,
// but we expect filename from config to be relative from cwd, so we need to convert
const filenameRelativeFromWatchDirectory = (0, path_1.relative)(watchDirectory, (0, path_1.resolve)(process.cwd(), entry.filename));
if (entry.config.preset) {
const extension = entry.config.presetConfig?.extension;
if (extension) {
ignored.push((0, path_1.join)(filenameRelativeFromWatchDirectory, '**', '*' + extension));
}
}
else {
ignored.push(filenameRelativeFromWatchDirectory);
}
}
watcherSubscription = await parcelWatcher.subscribe(watchDirectory, async (_, events) => {
// it doesn't matter what has changed, need to run whole process anyway
await Promise.all(
// NOTE: @parcel/watcher always provides path as an absolute path
events.map(async ({ type: eventName, path }) => {
if (!shouldRebuild({ path })) {
return;
}
(0, hooks_js_1.lifecycleHooks)(config.hooks).onWatchTriggered(eventName, path);
(0, debugging_js_1.debugLog)(`[Watcher] triggered due to a file ${eventName} event: ${path}`);
// In ESM require is not defined
try {
delete require.cache[path];
}
catch { }
if (eventName === 'update' && config.configFilePath && path === config.configFilePath) {
log(`${log_symbols_1.default.info} Config file has changed, reloading...`);
const context = await (0, config_js_1.loadContext)(config.configFilePath);
const newParsedConfig = context.getConfig();
newParsedConfig.watch = config.watch;
newParsedConfig.silent = config.silent;
newParsedConfig.overwrite = config.overwrite;
newParsedConfig.configFilePath = config.configFilePath;
config = newParsedConfig;
initialContext.updateConfig(config);
}
debouncedExec();
}));
}, { ignore: ignored });
(0, debugging_js_1.debugLog)(`[Watcher] Started`);
const shutdown = (
/** Optional callback to execute after shutdown has completed its async tasks */
afterShutdown) => {
isShutdown = true;
(0, debugging_js_1.debugLog)(`[Watcher] Shutting down`);
log(`Shutting down watch...`);
const pendingUnsubscribe = watcherSubscription.unsubscribe();
const pendingBeforeDoneHook = (0, hooks_js_1.lifecycleHooks)(config.hooks).beforeDone();
if (afterShutdown && typeof afterShutdown === 'function') {
Promise.allSettled([pendingUnsubscribe, pendingBeforeDoneHook]).then(afterShutdown);
}
};
abortSignal.addEventListener('abort', () => shutdown(abortSignal.reason));
process.once('SIGINT', () => shutdown());
process.once('SIGTERM', () => shutdown());
};
// Use an AbortController for shutdown signals
// NOTE: This will be polyfilled on Node 14 (or any environment without it defined)
const abortController = new abort_controller_polyfill_js_1.AbortController();
/**
* Send shutdown signal and return a promise that only resolves after the
* runningWatcher has resolved, which only resolved after the shutdown signal has been handled
*/
const stopWatching = async function () {
// stopWatching.afterShutdown is lazily set to resolve pendingShutdown promise
abortController.abort(stopWatching.afterShutdown);
// SUBTLE: runningWatcher waits for pendingShutdown before it resolves itself, so
// by awaiting it here, we are awaiting both the shutdown handler, and runningWatcher itself
await stopWatching.runningWatcher;
};
stopWatching.afterShutdown = () => {
(0, debugging_js_1.debugLog)('Shutdown watcher before it started');
};
stopWatching.runningWatcher = Promise.resolve();
/** Promise will resolve after the shutdown() handler completes */
const pendingShutdown = new Promise(afterShutdown => {
// afterShutdown will be passed to shutdown() handler via abortSignal.reason
stopWatching.afterShutdown = afterShutdown;
});
/**
* Promise that resolves after the watch server has shutdown, either because
* stopWatching() was called or there was an error inside it
*/
stopWatching.runningWatcher = new Promise((resolve, reject) => {
(0, codegen_js_1.executeCodegen)(initialContext)
.then(onNext, () => Promise.resolve())
.then(() => runWatcher(abortController.signal))
.catch(err => {
watcherSubscription.unsubscribe();
reject(err);
})
.then(() => pendingShutdown)
.finally(() => {
(0, debugging_js_1.debugLog)('Done watching.');
resolve();
});
});
return {
stopWatching,
runningWatcher: stopWatching.runningWatcher,
};
};
exports.createWatcher = createWatcher;
/**
* Given a list of file paths (each of which may be absolute, or relative from
* `process.cwd()`), find absolute path of the "highest" common directory,
* i.e. the directory that contains all the files in the list.
*
* @param files List of relative and/or absolute file paths (or micromatch patterns)
*/
const findHighestCommonDirectory = async (files) => {
// Map files to a list of basePaths, where "base" is the result of mm.scan(pathOrPattern)
// e.g. mm.scan("/**/foo/bar").base -> "/" ; mm.scan("/foo/bar/**/fizz/*.graphql") -> /foo/bar
const dirPaths = files
.map(filePath => ((0, path_1.isAbsolute)(filePath) ? filePath : (0, path_1.resolve)(filePath)))
// mm.scan doesn't know how to handle Windows \ path separator
.map(patterned => patterned.replace(/\\/g, '/'))
.map(patterned => micromatch_1.default.scan(patterned).base)
// revert the separators to the platform-supported ones
.map(base => base.replace(/\//g, path_1.sep));
// Return longest common prefix if it's accessible, otherwise process.cwd()
return (async (maybeValidPath) => {
(0, debugging_js_1.debugLog)(`[Watcher] Longest common prefix of all files: ${maybeValidPath}...`);
try {
await (0, file_system_js_1.access)(maybeValidPath);
return maybeValidPath;
}
catch {
log(`[Watcher] Longest common prefix (${maybeValidPath}) is not accessible`);
log(`[Watcher] Watching current working directory (${process.cwd()}) instead`);
return process.cwd();
}
})(longestCommonPrefix(dirPaths.map(path => path.split(path_1.sep))).join(path_1.sep));
};
/**
* Find the longest common prefix of an array of paths, where each item in
* the array an array of path segments which comprise an absolute path when
* joined together by a path separator
*
* Adapted from:
* https://duncan-mcardle.medium.com/leetcode-problem-14-longest-common-prefix-javascript-3bc6a2f777c4
*
* @param splitPaths An array of arrays, where each item is a path split by its separator
* @returns An array of path segments representing the longest common prefix of splitPaths
*/
const longestCommonPrefix = (splitPaths) => {
// Return early on empty input
if (!splitPaths.length) {
return [];
}
// Loop through the segments of the first path
for (let i = 0; i <= splitPaths[0].length; i++) {
// Check if this path segment is present in the same position of every path
if (!splitPaths.every(string => string[i] === splitPaths[0][i])) {
// If not, return the path segments up to and including the previous segment
return splitPaths[0].slice(0, i);
}
}
return splitPaths[0];
};
;