|
| 1 | + |
| 2 | +import { dedent } from 'ts-dedent'; |
| 3 | +import { parseArgs } from 'node:util'; |
| 4 | +import { fileURLToPath } from 'node:url'; |
| 5 | +import * as fs from 'node:fs/promises'; |
| 6 | +import { exec } from 'node:child_process'; |
| 7 | +import { AsyncLocalStorage } from 'node:async_hooks'; |
| 8 | + |
| 9 | + |
| 10 | +// |
| 11 | +// Setup |
| 12 | +// |
| 13 | + |
| 14 | +type Logger = Pick<Console, 'info' | 'error' | 'log'>; |
| 15 | +type Services = { logger: Logger }; |
| 16 | +const servicesStorage = new AsyncLocalStorage<Services>(); |
| 17 | +const getServices = () => { |
| 18 | + const services = servicesStorage.getStore(); |
| 19 | + if (typeof services === 'undefined') { throw new Error(`Missing services`); } |
| 20 | + return services; |
| 21 | +}; |
| 22 | + |
| 23 | +type ScriptArgs = { |
| 24 | + values: { |
| 25 | + help?: undefined | boolean, |
| 26 | + silent?: undefined | boolean, |
| 27 | + }, |
| 28 | + positionals: Array<string>, |
| 29 | +}; |
| 30 | + |
| 31 | + |
| 32 | +// |
| 33 | +// Common |
| 34 | +// |
| 35 | + |
| 36 | +const getCurrentGitBranch = () => new Promise<string>((resolve, reject) => { |
| 37 | + return exec('git rev-parse --abbrev-ref HEAD', (err, stdout, stderr) => { |
| 38 | + if (err) { |
| 39 | + reject(`Failed to determine current git branch: ${err}`); |
| 40 | + } else if (typeof stdout === 'string') { |
| 41 | + resolve(stdout.trim()); |
| 42 | + } |
| 43 | + }); |
| 44 | +}); |
| 45 | + |
| 46 | +const readDistCss = async () => { |
| 47 | + const path = fileURLToPath(new URL('../dist/lib.css', import.meta.url)); |
| 48 | + return (await fs.readFile(path)).toString(); // May throw |
| 49 | +}; |
| 50 | + |
| 51 | + |
| 52 | +// |
| 53 | +// Commands |
| 54 | +// |
| 55 | + |
| 56 | +export const runVerifyBuild = async (args: ScriptArgs) => { |
| 57 | + const { logger } = getServices(); |
| 58 | + |
| 59 | + const cssContent = await readDistCss(); |
| 60 | + const cssContentStripped = cssContent.replaceAll(`@charset "UTF-8";`, '').trim(); |
| 61 | + |
| 62 | + // We need to make sure that an `@layer` ordering is at the beginning of the CSS build file. |
| 63 | + if (!cssContentStripped.match(/^@layer [^{]+?;/)) { |
| 64 | + throw new Error(`Missing @layer ordering at the start of the CSS build file`); |
| 65 | + } |
| 66 | + |
| 67 | + logger.log('verify:build – No issues found'); |
| 68 | +}; |
| 69 | + |
| 70 | + |
| 71 | +// |
| 72 | +// Run |
| 73 | +// |
| 74 | + |
| 75 | +const printUsage = () => { |
| 76 | + const { logger } = getServices(); |
| 77 | + |
| 78 | + logger.info(dedent` |
| 79 | + Usage: verify.ts <cmd> <...args> |
| 80 | + |
| 81 | + Commands: |
| 82 | + - verify:build |
| 83 | + `); |
| 84 | +}; |
| 85 | + |
| 86 | +// Run the script with the given CLI arguments |
| 87 | +export const run = async (argsRaw: Array<string>): Promise<void> => { |
| 88 | + // Ref: https://exploringjs.com/nodejs-shell-scripting/ch_node-util-parseargs.html |
| 89 | + const args = parseArgs({ |
| 90 | + args: argsRaw, |
| 91 | + allowPositionals: true, |
| 92 | + options: { |
| 93 | + help: { type: 'boolean', short: 'h' }, |
| 94 | + silent: { type: 'boolean' }, |
| 95 | + }, |
| 96 | + }); |
| 97 | + |
| 98 | + // Services |
| 99 | + const logger: Logger = { |
| 100 | + info: args.values.silent ? () => {} : console.info, |
| 101 | + error: console.error, |
| 102 | + log: console.log, |
| 103 | + }; |
| 104 | + |
| 105 | + await servicesStorage.run({ logger }, async () => { |
| 106 | + const command: null | string = args.positionals[0] ?? null; |
| 107 | + if (command === null || args.values.help) { |
| 108 | + printUsage(); |
| 109 | + return; |
| 110 | + } |
| 111 | + |
| 112 | + const argsForCommand: ScriptArgs = { ...args, positionals: args.positionals.slice(1) }; |
| 113 | + switch (command) { |
| 114 | + case 'verify:build': await runVerifyBuild(argsForCommand); break; |
| 115 | + default: |
| 116 | + logger.error(`Unknown command '${command}'\n`); |
| 117 | + printUsage(); |
| 118 | + break; |
| 119 | + } |
| 120 | + }); |
| 121 | +}; |
| 122 | + |
| 123 | +// Detect if this module is being run directly from the command line |
| 124 | +const [_argExec, argScript, ...args] = process.argv; // First two arguments should be the executable + script |
| 125 | +if (argScript && await fs.realpath(argScript) === fileURLToPath(import.meta.url)) { |
| 126 | + try { |
| 127 | + await run(args); |
| 128 | + process.exit(0); |
| 129 | + } catch (error: unknown) { |
| 130 | + console.error(error); |
| 131 | + process.exit(1); |
| 132 | + } |
| 133 | +} |
0 commit comments