diff --git a/index.mjs b/index.mjs new file mode 100644 index 0000000..1ac923d --- /dev/null +++ b/index.mjs @@ -0,0 +1,24 @@ +import { Spinner } from "./src/utils/spinner.mjs"; +import * as prompts from '@clack/prompts'; + +prompts.intro('spinner start...'); + +const spinner = new Spinner(); +spinner.total = 100; + +spinner.start(); + +new Promise((resolve) => { + let progress = 0; + const timer = setInterval(() => { + progress = Math.min(spinner.total, progress + 1); + spinner.update(1, `Loading... ${progress}/${spinner.total}`); + if (progress >= spinner.total) { + clearInterval(timer); + resolve(true); + } + }, 100); +}).then(() => { + spinner.stop('Done'); + prompts.outro('spinner stop...'); +}); diff --git a/package-lock.json b/package-lock.json index 18f2d39..198277a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,6 +6,7 @@ "": { "name": "@node-core/api-docs-tooling", "dependencies": { + "@clack/prompts": "^0.9.0", "commander": "^12.1.0", "github-slugger": "^2.0.0", "glob": "^11.0.0", @@ -44,6 +45,27 @@ "prettier": "3.3.3" } }, + "node_modules/@clack/core": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-0.4.0.tgz", + "integrity": "sha512-YJCYBsyJfNDaTbvDUVSJ3SgSuPrcujarRgkJ5NLjexDZKvaOiVVJvAQYx8lIgG0qRT8ff0fPgqyBCVivanIZ+A==", + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "sisteransi": "^1.0.5" + } + }, + "node_modules/@clack/prompts": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.9.0.tgz", + "integrity": "sha512-nGsytiExgUr4FL0pR/LeqxA28nz3E0cW7eLTSh3Iod9TGrbBt8Y7BHbV3mmkNC4G0evdYyQ3ZsbiBkk7ektArA==", + "license": "MIT", + "dependencies": { + "@clack/core": "0.4.0", + "picocolors": "^1.0.0", + "sisteransi": "^1.0.5" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -3149,6 +3171,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -3422,6 +3450,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, "node_modules/slice-ansi": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", diff --git a/package.json b/package.json index 4b15177..98c620b 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "prettier": "3.3.3" }, "dependencies": { + "@clack/prompts": "^0.9.0", "commander": "^12.1.0", "github-slugger": "^2.0.0", "glob": "^11.0.0", diff --git a/src/loader.mjs b/src/loader.mjs index b3ae300..c9aa1c9 100644 --- a/src/loader.mjs +++ b/src/loader.mjs @@ -6,6 +6,8 @@ import { extname } from 'node:path'; import { globSync } from 'glob'; import { VFile } from 'vfile'; +import { Spinner } from './utils/spinner.mjs'; + /** * This method creates a simple abstract "Loader", which technically * could be used for different things, but here we want to use it to load @@ -26,8 +28,19 @@ const createLoader = () => { filePath => extname(filePath) === '.md' ); + const spinner = new Spinner(); + spinner.total = resolvedFiles.length; + spinner.start(); + return resolvedFiles.map(async filePath => { const fileContents = await readFile(filePath, 'utf-8'); + spinner.update(1); + + // normally we stop the spinner when the loop is done + // but here we return the loop so we need to stop it when the last file is loaded + if (spinner.progress === spinner.total) { + spinner.stop(); + } return new VFile({ path: filePath, value: fileContents }); }); diff --git a/src/parser.mjs b/src/parser.mjs index 3c05b64..c12c535 100644 --- a/src/parser.mjs +++ b/src/parser.mjs @@ -11,6 +11,7 @@ import createQueries from './queries.mjs'; import { getRemark } from './utils/remark.mjs'; import { createNodeSlugger } from './utils/slugger.mjs'; +import { Spinner } from './utils/spinner.mjs'; /** * Creates an API doc parser for a given Markdown API doc file @@ -177,7 +178,20 @@ const createParser = () => { const parseApiDocs = async apiDocs => { // We do a Promise.all, to ensure that each API doc is resolved asynchronously // but all need to be resolved first before we return the result to the caller - const resolvedApiDocEntries = await Promise.all(apiDocs.map(parseApiDoc)); + + const spinner = new Spinner(); + spinner.start(); + spinner.total = apiDocs.length; + + const resolvedApiDocEntries = await Promise.all( + apiDocs.map(apiDoc => { + spinner.update(1); + + return parseApiDoc(apiDoc); + }) + ); + + spinner.stop(); return resolvedApiDocEntries.flat(); }; diff --git a/src/utils/generators.mjs b/src/utils/generators.mjs index c2df3d4..a67c336 100644 --- a/src/utils/generators.mjs +++ b/src/utils/generators.mjs @@ -2,6 +2,8 @@ import { coerce } from 'semver'; +import { Spinner } from '../utils/spinner.mjs'; + /** * Groups all the API metadata nodes by module (`api` property) so that we can process each different file * based on the module it belongs to. @@ -12,14 +14,21 @@ export const groupNodesByModule = nodes => { /** @type {Map>} */ const groupedNodes = new Map(); + const spinner = new Spinner(); + spinner.total = nodes.length; + spinner.start(); + for (const node of nodes) { if (!groupedNodes.has(node.api)) { groupedNodes.set(node.api, []); } groupedNodes.get(node.api).push(node); + spinner.update(1); } + spinner.stop(); + return groupedNodes; }; diff --git a/src/utils/spinner.mjs b/src/utils/spinner.mjs new file mode 100644 index 0000000..65c0768 --- /dev/null +++ b/src/utils/spinner.mjs @@ -0,0 +1,32 @@ +import * as prompts from '@clack/prompts'; + +export class Spinner { + constructor() { + this.spinner = prompts.spinner(); + this.progress = 0; + this.total = 0; + } + + start() { + this.spinner.start(); + } + + stop(message = 'Done') { + this.spinner.stop(message); + } + + /** + * + * @param {number} increment + * @param {string} customMessage + */ + update(increment = 1, customMessage) { + this.progress += increment; + + if (customMessage) { + this.spinner.message(customMessage); + return; + } + this.spinner.message(`Loading... ${this.progress}/${this.total}`); + } +} diff --git a/src/utils/tests/spinner.test.mjs b/src/utils/tests/spinner.test.mjs new file mode 100644 index 0000000..a678d18 --- /dev/null +++ b/src/utils/tests/spinner.test.mjs @@ -0,0 +1,11 @@ +import { describe, it } from "node:test"; +import { Spinner } from "../spinner.mjs"; + +describe("spinner", () => { + it("should instantiate a new Spinner", () => { + const spinner = new Spinner(); + spinner.start(); + spinner.stop(); + }); + +});