/
githubmirror
/
amphtml
Обзор
Документация
Войти
/
githubmirror
/
amphtml
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
build-system/task-runner/amp-task-runner.js
287 строк
9 KB
Daniel Rozenberg
🏗 Partial updates for core devDependencies (#39765)
26 янв 2024, 20:21
Не верифицирован
26 янв 2024, 20:21
74f443b
Код
Авторство
О чём код?
/** * @fileoverview This is a lightweight task runner with a one-file implentation * based on the commander npm package. */ const argv = require('minimist')(process.argv.slice(2)); const {program} = require('commander'); const esprima = require('esprima'); const fs = require('fs-extra'); const os = require('os'); const path = require('path'); const { updatePackages, updateSubpackages, } = require('../common/update-packages'); const {cyan, green, magenta, red} = require('kleur/colors'); const {isCiBuild} = require('../common/ci'); const {log} = require('../common/logging'); const {isCircleciBuild} = require('../common/ci'); /** * @typedef {Function & { * description: string, * flags?: Object, * }} */ let TaskFuncDef; /** * Special-case constant that indicates if `amp --help` was invoked. */ const isHelpTask = argv._.length == 0 && argv.hasOwnProperty('help'); /** * Calculates and formats the duration for which an AMP task ran. * @param {DOMHighResTimeStamp} start * @return {string} */ function getTime(start) { const endTime = Date.now(); const executionTime = endTime - start; const mins = Math.floor(executionTime / 60000); const secs = Math.floor((executionTime % 60000) / 1000); const msecs = executionTime % 1000; return mins !== 0 ? `${mins}m ${secs}s` : secs != 0 ? `${secs}s` : `${msecs}ms`; } /** * Switches the current directory to the repo root if needed. This is done so * that hard-coded paths within tasks can work predictably. */ function startAtRepoRoot() { const repoRoot = path.resolve(__dirname, '..', '..'); if (repoRoot != process.cwd()) { process.chdir(repoRoot); log('Working directory changed to', magenta(path.basename(repoRoot))); } } /** * 1. Updates root-level packages during local development. (Skipped during CI * because they're guaranteed to be fresh.) * 2. Updates task-level subpackages if a package.json file exists in the task * directory. * @param {string} taskSourceFileName */ function ensureUpdatedPackages(taskSourceFileName) { if (!isCiBuild()) { updatePackages(); } const taskSourceFilePath = getTaskSourceFilePath(taskSourceFileName); const packageFile = path.join(taskSourceFilePath, 'package.json'); const hasSubpackages = fs.pathExistsSync(packageFile); if (hasSubpackages) { updateSubpackages(taskSourceFilePath); } } /** * Runs an AMP task with logging and timing after installing its subpackages. * @param {string} taskName * @param {TaskFuncDef} taskFunc * @return {Promise<void>} */ async function runTask(taskName, taskFunc) { const taskFile = path.relative(os.homedir(), 'amp.js'); log('Using task file', magenta(taskFile)); const start = Date.now(); try { log(`Starting '${cyan(taskName)}'...`); await taskFunc(); log('Finished', `'${cyan(taskName)}'`, 'after', magenta(getTime(start))); // For some reason, the `e2e` and `unit` tasks get stuck on CircleCI after // testing is finishing, despite reaching this point in the code. This is a // temporary workaround until we understand exactly why and fix the root // cause. TODO(@ampproject/wg-infra): fix this. if (isCircleciBuild() && ['e2e', 'unit'].includes(taskName)) { process.exit(); // process.exitCode is set in ../tasks/e2e/index.js:171 } } catch (err) { log(`'${cyan(taskName)}'`, red('errored after'), magenta(getTime(start))); log(err); process.exit(1); } } /** * Prints an error if the task file and / or function are invalid, and exits. * @param {string} taskSourceFileName * @param {string=} taskFuncName */ function handleInvalidTaskError(taskSourceFileName, taskFuncName) { log( red('ERROR:'), 'Could not find' + (taskFuncName ? ` ${cyan(taskFuncName + '()')} in` : ''), cyan(path.join('build-system', 'tasks', taskSourceFileName)) + '.' ); log( '⤷ Please check the arguments to', cyan('createTask()'), 'in', cyan('amp.js') + '.' ); process.exit(1); } /** * Returns a task's source file path after making sure it is either a valid JS * file or a valid dir. * @param {string} taskSourceFileName * @return {string} */ function getTaskSourceFilePath(taskSourceFileName) { const tasksDir = path.join(__dirname, '..', 'tasks'); const taskSourceFilePath = path.join(tasksDir, taskSourceFileName); const isValidSourceFilePath = fs.pathExistsSync(`${taskSourceFilePath}.js`) || // Task lives in a JS file. fs.pathExistsSync(taskSourceFilePath); // Task lives in a directory. if (!isValidSourceFilePath) { handleInvalidTaskError(taskSourceFileName); } return taskSourceFilePath; } /** * Returns a task function after making sure it is valid. * @param {string} taskSourceFileName * @param {string} taskFuncName * @return {TaskFuncDef} */ function getTaskFunc(taskSourceFileName, taskFuncName) { const taskSourceFilePath = getTaskSourceFilePath(taskSourceFileName); const taskFunc = require(taskSourceFilePath)[taskFuncName]; const isValidFunc = typeof taskFunc == 'function'; if (!isValidFunc) { handleInvalidTaskError(taskSourceFileName, taskFuncName); } return taskFunc; } /** * Uses esprima to extract the description from a task file without require-ing * it, by tokenizing the JS file and finding the sequence that matches the text * "fooTask.description = '<task description>'". * @param {string} taskSourceFileName * @param {string} taskFuncName * @return {string} */ function getTaskDescription(taskSourceFileName, taskFuncName) { const taskSourceFilePath = getTaskSourceFilePath(taskSourceFileName); const taskSourceFile = fs.existsSync(`${taskSourceFilePath}.js`) ? `${taskSourceFilePath}.js` : path.join(taskSourceFilePath, 'index.js'); const contents = fs.readFileSync(taskSourceFile, 'utf-8').trim(); const tokens = Array.from(esprima.tokenize(contents)); let description = ''; for (let i = 0; i < tokens.length; ++i) { if ( tokens[i]?.type == 'Identifier' && tokens[i]?.value == taskFuncName && tokens[i + 1]?.type == 'Punctuator' && tokens[i + 1]?.value == '.' && tokens[i + 2]?.type == 'Identifier' && tokens[i + 2]?.value == 'description' && tokens[i + 3]?.type == 'Punctuator' && tokens[i + 3]?.value == '=' && tokens[i + 4]?.type == 'String' ) { description = tokens[i + 4]?.value ?.replace(/^['"]/, '') ?.replace(/['"]$/, ''); } } return description; } /** * Helper that creates the tasks in AMP's toolchain based on the invocation: * - For `amp --help`, load all task descriptions so a list can be printed. * - For `amp <task> --help`, load and print just the task description + flags. * - When a task is actually run, update root packages, load the entry point, * validate usage, update task-specific packages, and run the task. * @param {string} taskName * @param {string=} taskFuncName * @param {string=} taskSourceFileName */ function createTask( taskName, taskFuncName = taskName, taskSourceFileName = taskName ) { const isInvokedTask = argv._.includes(taskName); // `amp <task>` const isDefaultTask = argv._.length === 0 && taskName == 'default' && !isHelpTask; // `amp` if (isHelpTask) { const task = program.command(cyan(taskName)); const description = getTaskDescription(taskSourceFileName, taskFuncName); task.description(description); } if (isInvokedTask || isDefaultTask) { startAtRepoRoot(); ensureUpdatedPackages(taskSourceFileName); const taskFunc = getTaskFunc(taskSourceFileName, taskFuncName); const task = program.command(taskName, {isDefault: isDefaultTask}); task.description(green(taskFunc.description)); task.allowUnknownOption(); // Fall through to validateUsage() task.helpOption('--help', 'Print this list of flags'); task.usage('<flags>'); for (const [flag, description] of Object.entries(taskFunc.flags ?? {})) { task.option(`--${cyan(flag)}`, description); } task.action(async () => { validateUsage(task, taskName, taskFunc); await runTask(taskName, taskFunc); }); } } /** * Validates usage by examining task and flag invocation. * @param {object} task * @param {string} taskName * @param {TaskFuncDef} taskFunc */ function validateUsage(task, taskName, taskFunc) { const tasks = argv._; const invalidTasks = tasks.filter((task) => task != taskName); const flags = Object.keys(argv).slice(1); // Everything after '_' const validFlags = taskFunc.flags ? Object.keys(taskFunc.flags) : []; const invalidFlags = flags.filter((flag) => !validFlags.includes(flag)); if (invalidTasks.length > 0 || invalidFlags.length > 0) { task.addHelpText('before', red('ERROR: ') + 'Invalid usage'); task.help({error: true}); } } /** * Finalizes the task runner by doing special-case setup for `amp --help`, * parsing the invoked command, and printing an error message if an unknown task * was called. */ function finalizeRunner() { program.addHelpCommand(false); // We already have `amp --help` and `amp <task> --help` if (isHelpTask) { program.helpOption('--help', 'Print this list of tasks'); program.usage('<task> <flags>'); } program.on('command:*', (args) => { log(red('ERROR:'), 'Unknown task', cyan(args.join(' '))); log('⤷ Run', cyan('amp --help'), 'for a full list of tasks.'); log('⤷ Run', cyan('amp <task> --help'), 'for help with a specific task.'); process.exitCode = 1; }); program.parse(); } module.exports = { createTask, finalizeRunner, };