UNPKG

@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
"use strict"; 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]; };