diff --git a/bin/commands/generate.mjs b/bin/commands/generate.mjs index d4586663..9baa11d5 100644 --- a/bin/commands/generate.mjs +++ b/bin/commands/generate.mjs @@ -21,6 +21,7 @@ const { runGenerators } = createGenerator(); * @property {string} index * @property {boolean} minify * @property {string} typeMap + * @property {boolean} progress */ export default new Command('generate') @@ -58,6 +59,7 @@ export default new Command('generate') .addOption(new Option('--index ', 'index.md URL or path')) .addOption(new Option('--minify', 'Minify?')) .addOption(new Option('--type-map ', 'Type map URL or path')) + .addOption(new Option('--no-progress', 'Disable the progress bar')) .action( errorWrap(async opts => { diff --git a/package-lock.json b/package-lock.json index bd4633ad..85434567 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@rollup/plugin-virtual": "^3.0.2", "@swc/html-wasm": "^1.15.18", "acorn": "^8.16.0", + "cli-progress": "^3.12.0", "commander": "^14.0.3", "dedent": "^1.7.1", "estree-util-to-js": "^2.0.0", @@ -4076,6 +4077,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cli-progress": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz", + "integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==", + "license": "MIT", + "dependencies": { + "string-width": "^4.2.3" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/cli-truncate": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.1.tgz", @@ -4133,35 +4146,6 @@ "node": ">=8" } }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/cliui/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/cliui/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -4411,6 +4395,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, "node_modules/enhanced-resolve": { "version": "5.18.4", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", @@ -8300,6 +8290,50 @@ "dev": true, "license": "MIT" }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/stringify-entities": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", @@ -9116,56 +9150,6 @@ "node": ">=12" } }, - "node_modules/yargs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/yargs/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 16c3aacb..b68ccad0 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "@rollup/plugin-virtual": "^3.0.2", "@swc/html-wasm": "^1.15.18", "acorn": "^8.16.0", + "cli-progress": "^3.12.0", "commander": "^14.0.3", "dedent": "^1.7.1", "estree-util-to-js": "^2.0.0", diff --git a/src/generators.mjs b/src/generators.mjs index 451c39b5..e9b1f30b 100644 --- a/src/generators.mjs +++ b/src/generators.mjs @@ -5,6 +5,7 @@ import logger from './logger/index.mjs'; import { isAsyncGenerator, createStreamingCache } from './streaming.mjs'; import createWorkerPool from './threading/index.mjs'; import createParallelWorker from './threading/parallel.mjs'; +import createProgressBar from './utils/progressBar.mjs'; const generatorsLogger = logger.child('generators'); @@ -92,7 +93,7 @@ const createGenerator = () => { /** * Runs all requested generators with their dependencies. * - * @param {import('./utils/configuration/types').Configuration} options - Runtime options + * @param {import('./utils/configuration/types').Configuration} configuration - Runtime options * @returns {Promise} Results of all requested generators */ const runGenerators = async configuration => { @@ -111,6 +112,9 @@ const createGenerator = () => { await scheduleGenerator(name, configuration); } + const progress = createProgressBar({ enabled: configuration.progress }); + progress.start(generators.length, 0, { phase: 'Starting...' }); + // Start all collections in parallel (don't await sequentially) const resultPromises = generators.map(async name => { let result = await cachedGenerators[name]; @@ -119,12 +123,14 @@ const createGenerator = () => { result = await streamingCache.getOrCollect(name, result); } + progress.increment({ phase: name }); return result; }); const results = await Promise.all(resultPromises); await pool.destroy(); + progress.stop(); return results; }; diff --git a/src/utils/configuration/__tests__/index.test.mjs b/src/utils/configuration/__tests__/index.test.mjs index 293a468f..dc74d5a5 100644 --- a/src/utils/configuration/__tests__/index.test.mjs +++ b/src/utils/configuration/__tests__/index.test.mjs @@ -93,6 +93,7 @@ describe('config.mjs', () => { target: 'json', threads: 4, chunkSize: 5, + progress: true, }; const config = createConfigFromCLIOptions(options); @@ -112,6 +113,7 @@ describe('config.mjs', () => { target: 'json', threads: 4, chunkSize: 5, + progress: true, }); }); diff --git a/src/utils/configuration/index.mjs b/src/utils/configuration/index.mjs index fd3d210d..9be006bc 100644 --- a/src/utils/configuration/index.mjs +++ b/src/utils/configuration/index.mjs @@ -35,6 +35,7 @@ export const getDefaultConfig = lazy(() => threads: cpus().length, chunkSize: 10, + progress: true, }) ) ); @@ -96,6 +97,7 @@ export const createConfigFromCLIOptions = options => ({ target: options.target, threads: options.threads, chunkSize: options.chunkSize, + progress: options.progress, }); /** diff --git a/src/utils/configuration/types.d.ts b/src/utils/configuration/types.d.ts index 3536ba05..96b14677 100644 --- a/src/utils/configuration/types.d.ts +++ b/src/utils/configuration/types.d.ts @@ -15,6 +15,9 @@ export type Configuration = { // Number of items to process per worker thread chunkSize: number; + + // Whether or not the progress bar is enabled + progress: boolean; } & { [K in keyof AllGenerators]: GlobalConfiguration & AllGenerators[K]['defaultConfiguration']; diff --git a/src/utils/progressBar.mjs b/src/utils/progressBar.mjs new file mode 100644 index 00000000..a6fef029 --- /dev/null +++ b/src/utils/progressBar.mjs @@ -0,0 +1,31 @@ +'use strict'; + +import { PassThrough } from 'node:stream'; + +import { SingleBar, Presets } from 'cli-progress'; + +/** + * Creates a progress bar for the generation pipeline. + * Writes to stderr to avoid conflicts with the logger (stdout). + * When disabled or stderr is not a TTY (e.g. CI), output is sent + * to a PassThrough stream (silently discarded). + * + * @param {object} [options] + * @param {boolean} [options.enabled=true] Whether to render the progress bar + * @returns {SingleBar} + */ +const createProgressBar = ({ enabled = true } = {}) => { + const shouldEnable = enabled && process.stderr.isTTY; + + return new SingleBar( + { + stream: shouldEnable ? process.stderr : new PassThrough(), + format: ' {phase} [{bar}] {percentage}% | {value}/{total}', + hideCursor: true, + clearOnComplete: false, + }, + Presets.shades_grey + ); +}; + +export default createProgressBar;