UNPKG

@factorialco/shadowdog

Version:

<img src="https://raw.githubusercontent.com/factorialco/shadowdog/refs/heads/main/logo.png" alt="drawing" width="100"/>

124 lines (123 loc) 5.42 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.generate = void 0; const path_1 = __importDefault(require("path")); const fs_1 = __importDefault(require("fs")); const plugins_1 = require("./plugins"); const task_runner_1 = require("./task-runner"); const tasks_1 = require("./tasks"); const utils_1 = require("./utils"); // Wait for artifact files to be written and readable const waitForArtifacts = async (artifacts) => { // Make max retries configurable via environment variable for faster CI tests const maxRetries = process.env.SHADOWDOG_ARTIFACT_WAIT_MAX_RETRIES ? parseInt(process.env.SHADOWDOG_ARTIFACT_WAIT_MAX_RETRIES, 10) : 50; // Default: 5 seconds max wait time const retryDelay = 100; // 100ms between retries for (const artifact of artifacts) { const artifactPath = path_1.default.join(process.cwd(), artifact.output); let retries = 0; while (retries < maxRetries) { try { // Check if file exists and is readable await fs_1.default.promises.access(artifactPath, fs_1.default.constants.F_OK | fs_1.default.constants.R_OK); // For files, also verify they have content (not empty) const stats = await fs_1.default.promises.stat(artifactPath); if (stats.isFile() && stats.size === 0) { throw new Error('File is empty'); } // File exists and is readable with content, move to next artifact break; } catch { // File doesn't exist or isn't readable yet, wait and retry await new Promise((resolve) => setTimeout(resolve, retryDelay)); retries++; } } if (retries >= maxRetries) { // Fail the build if artifact is not available after max retries // This ensures we catch cases where commands don't produce expected outputs throw new Error(`Artifact '${artifact.output}' was not created or is not readable after task completion. ` + `Waited ${(maxRetries * retryDelay) / 1000} seconds.`); } } }; const processTask = async (task, pluginsConfig, eventEmitter, options) => { switch (task.type) { case 'parallel': { return Promise.all(task.tasks.map((subTask) => processTask(subTask, pluginsConfig, eventEmitter, options))); } case 'serial': { for (const subTask of task.tasks) { await processTask(subTask, pluginsConfig, eventEmitter, options); } return; } case 'command': { eventEmitter.emit('begin', { artifacts: task.config.artifacts, }); const taskRunner = new task_runner_1.TaskRunner({ files: task.files, environment: task.environment, config: task.config, eventEmitter, }); (0, plugins_1.filterMiddlewarePlugins)(pluginsConfig).forEach(({ fn, options: pluginOptions }) => { taskRunner.use(fn.middleware, pluginOptions); }); taskRunner.use(() => { return (0, tasks_1.runTask)({ command: task.config.command, workingDirectory: path_1.default.join(process.cwd(), task.config.workingDirectory), onSpawn: () => { }, onExit: () => { }, }); }); try { await taskRunner.execute(); // Wait for all artifacts to be written and readable before proceeding // This ensures dependent tasks can read the updated artifact files await waitForArtifacts(task.config.artifacts); eventEmitter.emit('end', { artifacts: task.config.artifacts, }); } catch (error) { eventEmitter.emit('error', { artifacts: task.config.artifacts, errorMessage: error.message, }); if (!options.continueOnError) { throw error; } } break; } case 'empty': { // noop } } }; const generate = async (config, eventEmitter, options) => { const plugins = (0, plugins_1.filterCommandPlugins)(config.plugins); const task = { type: 'parallel', tasks: config.watchers.flatMap((watcherConfig) => { const files = (0, utils_1.processFiles)(watcherConfig.files, [...watcherConfig.ignored, ...config.defaultIgnoredFiles], true); // Enable preserveNonExistent for dependency tracking return watcherConfig.commands.map((commandConfig) => ({ type: 'command', config: commandConfig, files, environment: watcherConfig.environment, })); }), }; const finalTask = plugins.reduce((subTask, { fn }) => fn.command(subTask), task); return processTask(finalTask, config.plugins, eventEmitter, options); }; exports.generate = generate;