UNPKG

@graphql-codegen/cli

Version:
190 lines (189 loc) • 9.92 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.generate = generate; const tslib_1 = require("tslib"); const crypto_1 = require("crypto"); const path_1 = require("path"); 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 debugging_js_1 = require("./utils/debugging.js"); const file_system_js_1 = require("./utils/file-system.js"); const logger_js_1 = require("./utils/logger.js"); const watcher_js_1 = require("./utils/watcher.js"); const hash = (content) => (0, crypto_1.createHash)('sha1').update(content).digest('base64'); async function generate(input, saveToFile = true) { const context = (0, config_js_1.ensureContext)(input); const config = context.getConfig(); await context.profiler.run(() => (0, hooks_js_1.lifecycleHooks)(config.hooks).afterStart(), 'Lifecycle: afterStart'); // Store only the projection (`filename` + `overwrite`) rather than full results, so a // file that disappears is still judged by the `overwrite` of the entry that produced it. let previouslyGeneratedFiles = []; function removeStaleFiles(config, generationResult) { const filenames = generationResult.map(o => o.filename); // find stale files from previous build which are not present in current build const staleFiles = previouslyGeneratedFiles.filter(f => !filenames.includes(f.filename)); for (const staleFile of staleFiles) { if (normalizeOverwriteConfig(config.overwrite, staleFile.overwrite).removeStaleFiles) { (0, file_system_js_1.unlinkFile)(staleFile.filename, err => { const prettyFilename = staleFile.filename.replace(`${input.cwd || process.cwd()}/`, ''); if (err) { (0, debugging_js_1.debugLog)(`Cannot remove stale file: ${prettyFilename}\n${err}`); } else { (0, debugging_js_1.debugLog)(`Removed stale file: ${prettyFilename}`); } }); } } previouslyGeneratedFiles = generationResult.map(res => ({ filename: res.filename, overwrite: res.overwrite, })); } // Records the hash of the content codegen last wrote per file. This is always // kept up to date; `FileOutput.contentComparison` only decides whether the // skip-check trusts this record ('cache-first') or re-reads the file from disk // ('disk'), for outputs whose content depends on the file's existing content. const recentOutputHash = new Map(); async function writeOutput(generationResult) { if (!saveToFile) { return generationResult; } if (config.watch) { removeStaleFiles(config, generationResult); } await context.profiler.run(async () => { await (0, hooks_js_1.lifecycleHooks)(config.hooks).beforeAllFileWrite(generationResult.map(r => r.filename)); }, 'Lifecycle: beforeAllFileWrite'); await context.profiler.run(() => Promise.all(generationResult.map(async (result) => { // The "previous" hash the skip-check compares against: // - 'cache-first' trusts the in-memory record of what codegen last wrote // (falling back to disk when there's no entry). // - 'disk' always re-reads the file, because the output's content // depends on the file's existing content // (e.g. a preset that reads the file and rewrites part of it), so the // in-memory record could wrongly skip a write when the file was changed // on disk but the regenerated content matches a previous run. const previousHash = await (async function getPreviousHash() { const { contentComparison = 'cache-first' } = result; if (contentComparison === 'disk') { return await hashFile(result.filename); } return recentOutputHash.get(result.filename) || (await hashFile(result.filename)); })(); const exists = previousHash !== null; // Always update the cache, regardless of `cache-first` or `disk` option, // so subsequent runs have consistent entry to compare against if (previousHash) { recentOutputHash.set(result.filename, previousHash); } if (!normalizeOverwriteConfig(config.overwrite, result.overwrite).updateExistingFiles && exists) { return; } let content = result.content || ''; const currentHash = hash(content); if (previousHash && currentHash === previousHash) { (0, debugging_js_1.debugLog)(`Skipping file (${result.filename}) writing due to indentical hash...`); return; } // skip updating file in dry mode if (context.checkMode) { context.checkModeStaleFiles.push(result.filename); return; } if (content.length === 0) { return; } const absolutePath = (0, path_1.isAbsolute)(result.filename) ? result.filename : (0, path_1.join)(input.cwd || process.cwd(), result.filename); const basedir = (0, path_1.dirname)(absolutePath); await (0, file_system_js_1.mkdirp)(basedir); content = await (0, hooks_js_1.lifecycleHooks)(result.hooks).beforeOneFileWrite(absolutePath, content); content = await (0, hooks_js_1.lifecycleHooks)(config.hooks).beforeOneFileWrite(absolutePath, content); if (content !== result.content) { result.content = content; // compare the prettified content with the previous hash // to compare the content with an existing prettified file if (hash(content) === previousHash) { (0, debugging_js_1.debugLog)(`Skipping file (${result.filename}) writing due to indentical hash after prettier...`); // the modified content is NOT stored in recentOutputHash // so a diff can already be detected before executing the hook return; } } await (0, file_system_js_1.writeFile)(absolutePath, result.content); recentOutputHash.set(result.filename, currentHash); await (0, hooks_js_1.lifecycleHooks)(result.hooks).afterOneFileWrite(result.filename); await (0, hooks_js_1.lifecycleHooks)(config.hooks).afterOneFileWrite(result.filename); })), 'Write files'); await context.profiler.run(() => (0, hooks_js_1.lifecycleHooks)(config.hooks).afterAllFileWrite(generationResult.map(r => r.filename)), 'Lifecycle: afterAllFileWrite'); return generationResult; } // Flush the collected profiler events to disk, then reset the profiler so the // next run produces its own trace under a fresh filename. No-op when profiling // is disabled (the noop profiler has no `outputName`). async function writeProfilerOutput() { const { profiler } = context; if (!profiler.outputName) { return; } await (0, file_system_js_1.writeFile)((0, path_1.join)(context.cwd, profiler.outputName), JSON.stringify(profiler.collect())); profiler.clear(); } // watch mode if (config.watch) { return (0, watcher_js_1.createWatcher)(context, writeOutput, writeProfilerOutput).runningWatcher; } const { result: outputFiles, error } = await context.profiler.run(() => (0, codegen_js_1.executeCodegen)(context), 'executeCodegen'); if (error) { // If all generation failed, just throw to return non-zero code. if (outputFiles.length === 0) { throw error; } // If partial success, but partial output is not allowed, throw to return non-zero code. if (!config.allowPartialOutputs) { (0, logger_js_1.getLogger)().error(` ${log_symbols_1.default.error} One or more errors occurred, no files were generated. To allow output on errors, set config.allowPartialOutputs=true`); throw error; } // If partial success, and partial output is allowed, warn and proceed to write to files. (0, logger_js_1.getLogger)().warn(` ${log_symbols_1.default.warning} One or more errors occurred, some files were generated. To prevent any output on errors, set config.allowPartialOutputs=false`); } await context.profiler.run(() => writeOutput(outputFiles), 'writeOutput'); await context.profiler.run(() => (0, hooks_js_1.lifecycleHooks)(config.hooks).beforeDone(), 'Lifecycle: beforeDone'); await writeProfilerOutput(); return outputFiles; } function normalizeOverwriteConfig(configOverwrite, fileOverwrite) { const overwrite = fileOverwrite ?? configOverwrite ?? true; if (overwrite === true) { return { removeStaleFiles: true, updateExistingFiles: true, }; } if (overwrite === false) { return { removeStaleFiles: false, updateExistingFiles: false, }; } const { removeStaleFiles = true, updateExistingFiles = true } = overwrite; return { removeStaleFiles, updateExistingFiles }; } async function hashFile(filePath) { try { return hash(await (0, file_system_js_1.readFile)(filePath)); } catch (err) { if (err && err.code === 'ENOENT') { // return null if file does not exist return null; } // rethrow unexpected errors throw err; } }