From d0b34f635d40b395764ba2d50fc50c99ef4d7ef6 Mon Sep 17 00:00:00 2001 From: Yuxuan Zhang Date: Wed, 27 Dec 2023 22:04:29 -0500 Subject: [PATCH 01/43] feat(build): log duration for each task, use humanized format --- package.json | 2 ++ pnpm-lock.yaml | 14 ++++++++++++++ src/node/build/build.ts | 11 +++++++---- src/node/utils/task.ts | 15 ++++++++++++--- 4 files changed, 35 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index bfc8ec25..22a3b6e2 100644 --- a/package.json +++ b/package.json @@ -137,6 +137,7 @@ "@types/debug": "^4.1.12", "@types/escape-html": "^1.0.4", "@types/fs-extra": "^11.0.4", + "@types/humanize-duration": "^3.27.3", "@types/lodash.template": "^4.5.3", "@types/mark.js": "^8.11.12", "@types/markdown-it-attrs": "^4.1.3", @@ -160,6 +161,7 @@ "fs-extra": "^11.2.0", "get-port": "^7.0.0", "gray-matter": "^4.0.3", + "humanize-duration": "^3.31.0", "lint-staged": "^15.2.0", "lodash.template": "^4.5.0", "lru-cache": "^10.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ba4c0aaf..24948b52 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -111,6 +111,9 @@ importers: '@types/fs-extra': specifier: ^11.0.4 version: 11.0.4 + '@types/humanize-duration': + specifier: ^3.27.3 + version: 3.27.3 '@types/lodash.template': specifier: ^4.5.3 version: 4.5.3 @@ -180,6 +183,9 @@ importers: gray-matter: specifier: ^4.0.3 version: 4.0.3 + humanize-duration: + specifier: ^3.31.0 + version: 3.31.0 lint-staged: specifier: ^15.2.0 version: 15.2.0(supports-color@9.4.0) @@ -1146,6 +1152,10 @@ packages: resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} dev: true + /@types/humanize-duration@3.27.3: + resolution: {integrity: sha512-wiiiFYjnrYDJE/ujU7wS/NShqp12IKrejozjDtcejP0zYi+cjyjVcfZHwcFUDKVJ7tHGsmgeW2ED92ABIIjfpg==} + dev: true + /@types/jquery@3.5.29: resolution: {integrity: sha512-oXQQC9X9MOPRrMhPHHOsXqeQDnWeCDT3PelUIg/Oy8FAbzSZtFHRjc7IpbfFVmpLtJ+UOoywpRsuO5Jxjybyeg==} dependencies: @@ -2742,6 +2752,10 @@ packages: engines: {node: '>=16.17.0'} dev: true + /humanize-duration@3.31.0: + resolution: {integrity: sha512-fRrehgBG26NNZysRlTq1S+HPtDpp3u+Jzdc/d5A4cEzOD86YLAkDaJyJg8krSdCi7CJ+s7ht3fwRj8Dl+Btd0w==} + dev: true + /inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} dependencies: diff --git a/src/node/build/build.ts b/src/node/build/build.ts index 7dca478a..46e4d5c0 100644 --- a/src/node/build/build.ts +++ b/src/node/build/build.ts @@ -15,12 +15,13 @@ import { task } from '../utils/task' import { bundle } from './bundle' import { generateSitemap } from './generateSitemap' import { renderPage } from './render' +import humanizeDuration from 'humanize-duration' export async function build( root?: string, buildOptions: BuildOptions & { base?: string; mpa?: string } = {} ) { - const start = Date.now() + const timeStart = performance.now() process.env.NODE_ENV = 'production' const siteConfig = await resolveConfig(root, 'build', 'production') @@ -143,9 +144,11 @@ export async function build( await siteConfig.buildEnd?.(siteConfig) clearCache() - siteConfig.logger.info( - `build complete in ${((Date.now() - start) / 1000).toFixed(2)}s.` - ) + const timeEnd = performance.now() + const duration = humanizeDuration(timeEnd - timeStart, { + maxDecimalPoints: 2 + }) + siteConfig.logger.info(`build complete in ${duration}.`) } function linkVue() { diff --git a/src/node/utils/task.ts b/src/node/utils/task.ts index 1d8a3322..667caf67 100644 --- a/src/node/utils/task.ts +++ b/src/node/utils/task.ts @@ -1,4 +1,5 @@ import ora from 'ora' +import humanizeDuration from 'humanize-duration' export const okMark = '\x1b[32m✓\x1b[0m' export const failMark = '\x1b[31m✖\x1b[0m' @@ -7,12 +8,20 @@ export async function task(taskName: string, task: () => Promise) { const spinner = ora({ discardStdin: false }) spinner.start(taskName + '...') + let symbol = okMark + const timeStart = performance.now() + try { await task() } catch (e) { - spinner.stopAndPersist({ symbol: failMark }) + symbol = failMark throw e + } finally { + const timeEnd = performance.now() + const duration = humanizeDuration(timeEnd - timeStart, { + maxDecimalPoints: 2 + }) + const text = `${taskName} (${duration})` + spinner.stopAndPersist({ symbol, text }) } - - spinner.stopAndPersist({ symbol: okMark }) } From 0b37eff97e06bd630ea14203a520d5767319803c Mon Sep 17 00:00:00 2001 From: Yuxuan Zhang Date: Wed, 27 Dec 2023 22:20:44 -0500 Subject: [PATCH 02/43] feat(build): log duration for each task, use humanized format modify log format, use dash instead of parenthesis --- src/node/utils/task.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/utils/task.ts b/src/node/utils/task.ts index 667caf67..4a65e6e6 100644 --- a/src/node/utils/task.ts +++ b/src/node/utils/task.ts @@ -21,7 +21,7 @@ export async function task(taskName: string, task: () => Promise) { const duration = humanizeDuration(timeEnd - timeStart, { maxDecimalPoints: 2 }) - const text = `${taskName} (${duration})` + const text = `${taskName} - ${duration}` spinner.stopAndPersist({ symbol, text }) } } From e568de361f3f9de24ac7e4becb64925b88721492 Mon Sep 17 00:00:00 2001 From: Yuxuan Zhang Date: Wed, 27 Dec 2023 23:23:23 -0500 Subject: [PATCH 03/43] feat(build): add progress handler for task callback, show progress for render closes #3384 --- src/node/build/build.ts | 9 ++++++--- src/node/utils/task.ts | 19 +++++++++++++++++-- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/node/build/build.ts b/src/node/build/build.ts index 46e4d5c0..7686365b 100644 --- a/src/node/build/build.ts +++ b/src/node/build/build.ts @@ -55,7 +55,7 @@ export async function build( const entryPath = path.join(siteConfig.tempDir, 'app.js') const { render } = await import(pathToFileURL(entryPath).toString()) - await task('rendering pages', async () => { + await task('rendering pages', async (updateProgress) => { const appChunk = clientResult && (clientResult.output.find( @@ -108,9 +108,11 @@ export async function build( ]) } } - + debugger + const pages = ['404.md', ...siteConfig.pages] + let count_done = 0 await pMap( - ['404.md', ...siteConfig.pages], + pages, async (page) => { await renderPage( render, @@ -124,6 +126,7 @@ export async function build( metadataScript, additionalHeadTags ) + updateProgress(++count_done, pages.length) }, { concurrency: siteConfig.buildConcurrency } ) diff --git a/src/node/utils/task.ts b/src/node/utils/task.ts index 4a65e6e6..691624a9 100644 --- a/src/node/utils/task.ts +++ b/src/node/utils/task.ts @@ -4,7 +4,12 @@ import humanizeDuration from 'humanize-duration' export const okMark = '\x1b[32m✓\x1b[0m' export const failMark = '\x1b[31m✖\x1b[0m' -export async function task(taskName: string, task: () => Promise) { +type UpdateHandle = (done: number, total?: number) => any + +export async function task( + taskName: string, + task: (update: UpdateHandle) => Promise +): Promise { const spinner = ora({ discardStdin: false }) spinner.start(taskName + '...') @@ -12,7 +17,17 @@ export async function task(taskName: string, task: () => Promise) { const timeStart = performance.now() try { - await task() + const updateHandle: UpdateHandle = (done, total) => { + if (total === undefined) { + spinner.text = `${taskName} [ ${done} ]` + } else { + // match length to display them in same width + const _total = `${total}` + const _done = `${done}`.padStart(_total.length, ' ') + spinner.text = `${taskName} [ ${_done} / ${_total} ]` + } + } + return await task(updateHandle) } catch (e) { symbol = failMark throw e From 8849d8552317ee60761634e080381838d462d848 Mon Sep 17 00:00:00 2001 From: Yuxuan Zhang Date: Wed, 27 Dec 2023 23:45:41 -0500 Subject: [PATCH 04/43] fix: remove debugger statement for local test --- src/node/build/build.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/build/build.ts b/src/node/build/build.ts index 7686365b..8d5cf937 100644 --- a/src/node/build/build.ts +++ b/src/node/build/build.ts @@ -108,7 +108,7 @@ export async function build( ]) } } - debugger + const pages = ['404.md', ...siteConfig.pages] let count_done = 0 await pMap( From 9281648b3e7422363cefab57cdc33b67960d9d64 Mon Sep 17 00:00:00 2001 From: Yuxuan Zhang Date: Fri, 29 Dec 2023 00:19:26 -0500 Subject: [PATCH 05/43] move callback function outside try catch block, refactor logic --- src/node/utils/task.ts | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/node/utils/task.ts b/src/node/utils/task.ts index 691624a9..cd77d4fd 100644 --- a/src/node/utils/task.ts +++ b/src/node/utils/task.ts @@ -4,7 +4,7 @@ import humanizeDuration from 'humanize-duration' export const okMark = '\x1b[32m✓\x1b[0m' export const failMark = '\x1b[31m✖\x1b[0m' -type UpdateHandle = (done: number, total?: number) => any +export type UpdateHandle = (done: number, total?: number) => any export async function task( taskName: string, @@ -13,23 +13,24 @@ export async function task( const spinner = ora({ discardStdin: false }) spinner.start(taskName + '...') - let symbol = okMark + const updateHandle: UpdateHandle = (done, total) => { + if (total === undefined) { + spinner.text = `${taskName} [ ${done} ]` + } else { + // match length to display them in same width + const _total = `${total}` + const _done = `${done}`.padStart(_total.length, ' ') + spinner.text = `${taskName} [ ${_done} / ${_total} ]` + } + } + const timeStart = performance.now() + let success = true try { - const updateHandle: UpdateHandle = (done, total) => { - if (total === undefined) { - spinner.text = `${taskName} [ ${done} ]` - } else { - // match length to display them in same width - const _total = `${total}` - const _done = `${done}`.padStart(_total.length, ' ') - spinner.text = `${taskName} [ ${_done} / ${_total} ]` - } - } return await task(updateHandle) } catch (e) { - symbol = failMark + success = false throw e } finally { const timeEnd = performance.now() @@ -37,6 +38,7 @@ export async function task( maxDecimalPoints: 2 }) const text = `${taskName} - ${duration}` + const symbol = success ? okMark : failMark spinner.stopAndPersist({ symbol, text }) } } From f9f968499a610b88a1a4ee0b83c4ae2710834248 Mon Sep 17 00:00:00 2001 From: Yuxuan Zhang Date: Fri, 29 Dec 2023 02:53:23 -0500 Subject: [PATCH 06/43] config: use local machine's cpu count for default concurrency value --- src/node/config.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/node/config.ts b/src/node/config.ts index ee0e2e94..9fcf4625 100644 --- a/src/node/config.ts +++ b/src/node/config.ts @@ -19,6 +19,7 @@ import { type SiteData } from './shared' import type { RawConfigExports, SiteConfig, UserConfig } from './siteConfig' +import { cpus } from 'os' export { resolvePages } from './plugins/dynamicRoutesPlugin' export * from './siteConfig' @@ -142,7 +143,11 @@ export async function resolveConfig( rewrites, userConfig, sitemap: userConfig.sitemap, - buildConcurrency: userConfig.buildConcurrency ?? 64 + buildConcurrency: Math.max( + userConfig.buildConcurrency ?? cpus().length, + 1 // At least one thread required + ), + multithreadRender: userConfig.multithreadRender ?? false } // to be shared with content loaders From 5c06a082f396873200f5d1234f0cee54b6f9fffa Mon Sep 17 00:00:00 2001 From: Yuxuan Zhang Date: Fri, 29 Dec 2023 02:55:22 -0500 Subject: [PATCH 07/43] render: code cleanup (html composition) --- src/node/build/render.ts | 59 +++++++++++++++++++--------------------- 1 file changed, 28 insertions(+), 31 deletions(-) diff --git a/src/node/build/render.ts b/src/node/build/render.ts index 8818a5e1..cd877b08 100644 --- a/src/node/build/render.ts +++ b/src/node/build/render.ts @@ -148,37 +148,34 @@ export async function renderPage( } } - const html = ` - - - - ${ - isMetaViewportOverridden(head) - ? '' - : '' - } - ${title} - ${ - isDescriptionOverridden(head) - ? '' - : `` - } - - ${stylesheetLink} - ${metadataScript.inHead ? metadataScript.html : ''} - ${ - appChunk - ? `` - : '' - } - ${await renderHead(head)} - - ${teleports?.body || ''} -
${content}
- ${metadataScript.inHead ? '' : metadataScript.html} - ${inlinedScript} - -` + const html = [ + ``, + ``, + ``, + ``, + isMetaViewportOverridden(head) + ? '' + : '', + `${title}`, + isDescriptionOverridden(head) + ? '' + : ``, + ``, + stylesheetLink, + metadataScript.inHead ? metadataScript.html : '', + appChunk + ? `` + : '', + await renderHead(head), + ``, + ``, + teleports?.body || '', + `
${content}
`, + metadataScript.inHead ? '' : metadataScript.html, + inlinedScript, + ``, + `` + ].join('') const htmlFileName = path.join(config.outDir, page.replace(/\.md$/, '.html')) await fs.ensureDir(path.dirname(htmlFileName)) From e5ed19d67d92462b1727f78ee2e4680a9ff502db Mon Sep 17 00:00:00 2001 From: Yuxuan Zhang Date: Fri, 29 Dec 2023 02:56:11 -0500 Subject: [PATCH 08/43] feat/multithread-render: it works! --- package.json | 1 + pnpm-lock.yaml | 7 +++ rollup.config.ts | 18 +++++++- src/node/build/build.ts | 54 +++++++++++++---------- src/node/build/render-worker.ts | 77 +++++++++++++++++++++++++++++++++ src/node/build/render.ts | 34 +++++++++++---- src/node/siteConfig.ts | 8 ++++ 7 files changed, 165 insertions(+), 34 deletions(-) create mode 100644 src/node/build/render-worker.ts diff --git a/package.json b/package.json index 22a3b6e2..1883ed93 100644 --- a/package.json +++ b/package.json @@ -101,6 +101,7 @@ "mark.js": "8.11.1", "minisearch": "^6.3.0", "mrmime": "^2.0.0", + "rpc-magic-proxy": "0.0.0-beta.0", "shikiji": "^0.9.12", "shikiji-transformers": "^0.9.12", "vite": "^5.0.10", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 24948b52..cd0f668f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,6 +44,9 @@ importers: mrmime: specifier: ^2.0.0 version: 2.0.0 + rpc-magic-proxy: + specifier: 0.0.0-beta.0 + version: 0.0.0-beta.0 shikiji: specifier: ^0.9.12 version: 0.9.12 @@ -3913,6 +3916,10 @@ packages: '@rollup/rollup-win32-x64-msvc': 4.9.1 fsevents: 2.3.3 + /rpc-magic-proxy@0.0.0-beta.0: + resolution: {integrity: sha512-1UFsu4fpV/OGL8c1knrnx613zzZY9XSbqLUd5pSmOfh0ErAz1R8Dve5S65otFAtNtTO56SWFHbutHcw1lUkkcg==} + dev: false + /run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} dependencies: diff --git a/rollup.config.ts b/rollup.config.ts index 83fb93ec..877c4ecc 100644 --- a/rollup.config.ts +++ b/rollup.config.ts @@ -46,12 +46,28 @@ const plugins = [ json() ] +const node_root = r('src/node/') + const esmBuild: RollupOptions = { input: [r('src/node/index.ts'), r('src/node/cli.ts')], output: { format: 'esm', entryFileNames: `[name].js`, - chunkFileNames: 'serve-[hash].js', + chunkFileNames(chunk) { + console.log('chunkFileNames =>', chunk.name) + return `${chunk.name}-[hash].js` + }, + manualChunks(id) { + // All workers will be chunked into a single file + if (!id.startsWith(node_root)) return + id = id.slice(node_root.length).replace(/^\//, '') + const match = /^.*\-worker(?=(\.(js|ts))?$)/i.exec(id) + if (match) { + const [name] = match + console.log('manualChunks worker =>', name) + return name + } + }, dir: r('dist/node'), sourcemap: DEV }, diff --git a/src/node/build/build.ts b/src/node/build/build.ts index 8d5cf937..42d616a1 100644 --- a/src/node/build/build.ts +++ b/src/node/build/build.ts @@ -14,7 +14,7 @@ import { deserializeFunctions, serializeFunctions } from '../utils/fnSerialize' import { task } from '../utils/task' import { bundle } from './bundle' import { generateSitemap } from './generateSitemap' -import { renderPage } from './render' +import { renderPage, type RenderPageContext } from './render' import humanizeDuration from 'humanize-duration' export async function build( @@ -52,10 +52,9 @@ export async function build( return } - const entryPath = path.join(siteConfig.tempDir, 'app.js') - const { render } = await import(pathToFileURL(entryPath).toString()) - await task('rendering pages', async (updateProgress) => { + const entryPath = path.join(siteConfig.tempDir, 'app.js') + const appChunk = clientResult && (clientResult.output.find( @@ -109,27 +108,34 @@ export async function build( } } + const context: RenderPageContext = { + config: siteConfig, + result: clientResult, + appChunk, + cssChunk, + assets, + pageToHashMap, + metadataScript, + additionalHeadTags + } + const pages = ['404.md', ...siteConfig.pages] - let count_done = 0 - await pMap( - pages, - async (page) => { - await renderPage( - render, - siteConfig, - siteConfig.rewrites.map[page] || page, - clientResult, - appChunk, - cssChunk, - assets, - pageToHashMap, - metadataScript, - additionalHeadTags - ) - updateProgress(++count_done, pages.length) - }, - { concurrency: siteConfig.buildConcurrency } - ) + + if (siteConfig.multithreadRender) { + const { default: cluster } = await import('./render-worker') + await cluster(entryPath, context, pages, updateProgress) + } else { + let count_done = 0 + const { render } = await import(pathToFileURL(entryPath).toString()) + await pMap( + pages, + async (page) => { + await renderPage(render, page, context) + updateProgress(++count_done, pages.length) + }, + { concurrency: siteConfig.buildConcurrency } + ) + } }) // emit page hash map for the case where a user session is open diff --git a/src/node/build/render-worker.ts b/src/node/build/render-worker.ts new file mode 100644 index 00000000..a2c412f5 --- /dev/null +++ b/src/node/build/render-worker.ts @@ -0,0 +1,77 @@ +import { Worker, workerData, isMainThread, parentPort } from 'worker_threads' +import { type UpdateHandle } from '../utils/task' +import { type RenderPageContext } from './render' + +type TaskAllocator = () => Promise + +import RpcContext from 'rpc-magic-proxy' + +export default async function cluster( + entryPath: string, + context: RenderPageContext, + pages: string[], + update: UpdateHandle +) { + const concurrency = context.config.buildConcurrency || 1 + const num_tasks = pages.length + let progress = -concurrency + + const pageAlloc: TaskAllocator = async () => { + progress++ + if (progress >= 0) update(progress, num_tasks) + return pages.shift() + } + + const tasks = [] + + for (let _ = 0; _ < concurrency; _++) { + const ctx = new RpcContext() + const workerData = await ctx.serialize({ + entryPath, + pageAlloc, + context, + workload: 'render' + }) + const worker = new Worker(new URL(import.meta.url), { workerData }) + ctx.bind(worker) + tasks.push( + new Promise((res, rej) => + worker.once('exit', (code) => { + if (code === 0) res(code) + else rej() + }) + ) + ) + } + + await Promise.all(tasks) +} + +async function renderWorker() { + const ctx = new RpcContext(parentPort!) + try { + const { + entryPath, + pageAlloc, + context + }: { + entryPath: string + pageAlloc: TaskAllocator + context: RenderPageContext + } = ctx.deserialize(workerData) + const { pathToFileURL } = await import('url') + const { renderPage } = await import('./render') + const { render } = await import(pathToFileURL(entryPath).toString()) + while (true) { + const page = await pageAlloc() + if (!page) break + await renderPage(render, page, context) + } + } catch (e) { + console.error(e) + } finally { + ctx.reset() + } +} + +if (!isMainThread && workerData?.workload === 'render') renderWorker() diff --git a/src/node/build/render.ts b/src/node/build/render.ts index cd877b08..72485851 100644 --- a/src/node/build/render.ts +++ b/src/node/build/render.ts @@ -19,18 +19,34 @@ import { } from '../shared' import { version } from '../../../package.json' +export interface RenderPageContext { + config: SiteConfig + result: Rollup.RollupOutput | null + appChunk: Rollup.OutputChunk | null + cssChunk: Rollup.OutputAsset | null + assets: string[] + pageToHashMap: Record + metadataScript: { html: string; inHead: boolean } + additionalHeadTags: HeadConfig[] +} + export async function renderPage( render: (path: string) => Promise, - config: SiteConfig, - page: string, // foo.md - result: Rollup.RollupOutput | null, - appChunk: Rollup.OutputChunk | null, - cssChunk: Rollup.OutputAsset | null, - assets: string[], - pageToHashMap: Record, - metadataScript: { html: string; inHead: boolean }, - additionalHeadTags: HeadConfig[] + pageNameRaw: string, + renderContext: RenderPageContext ) { + const { + config, + result, + appChunk, + cssChunk, + assets, + pageToHashMap, + metadataScript, + additionalHeadTags + } = renderContext + + const page = config.rewrites.inv[pageNameRaw] || pageNameRaw const routePath = `/${page.replace(/\.md$/, '')}` const siteData = resolveSiteDataByRoute(config.site, routePath) diff --git a/src/node/siteConfig.ts b/src/node/siteConfig.ts index 74174280..21b99bce 100644 --- a/src/node/siteConfig.ts +++ b/src/node/siteConfig.ts @@ -156,6 +156,13 @@ export interface UserConfig */ buildConcurrency?: number + /** + * This option allows you to enable or disable the multithread render. + * @experimental + * @default false + */ + multithreadRender?: boolean + /** * @experimental * @@ -250,4 +257,5 @@ export interface SiteConfig logger: Logger userConfig: UserConfig buildConcurrency: number + multithreadRender: boolean } From b796f2152a19db0f5f7673cbddb0f9b1bd0770e7 Mon Sep 17 00:00:00 2001 From: Yuxuan Zhang Date: Fri, 29 Dec 2023 03:50:52 -0500 Subject: [PATCH 09/43] rollup.config: clarify comment --- rollup.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rollup.config.ts b/rollup.config.ts index 877c4ecc..49692956 100644 --- a/rollup.config.ts +++ b/rollup.config.ts @@ -58,7 +58,7 @@ const esmBuild: RollupOptions = { return `${chunk.name}-[hash].js` }, manualChunks(id) { - // All workers will be chunked into a single file + // Chunk each worker into a separate file if (!id.startsWith(node_root)) return id = id.slice(node_root.length).replace(/^\//, '') const match = /^.*\-worker(?=(\.(js|ts))?$)/i.exec(id) From 2149f6bffd84b86d2c69ed9edf5500cc1130bf87 Mon Sep 17 00:00:00 2001 From: Yuxuan Zhang Date: Fri, 29 Dec 2023 03:51:48 -0500 Subject: [PATCH 10/43] rollup.config: remove debug console logging --- rollup.config.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/rollup.config.ts b/rollup.config.ts index 49692956..40478c70 100644 --- a/rollup.config.ts +++ b/rollup.config.ts @@ -54,7 +54,6 @@ const esmBuild: RollupOptions = { format: 'esm', entryFileNames: `[name].js`, chunkFileNames(chunk) { - console.log('chunkFileNames =>', chunk.name) return `${chunk.name}-[hash].js` }, manualChunks(id) { @@ -64,7 +63,6 @@ const esmBuild: RollupOptions = { const match = /^.*\-worker(?=(\.(js|ts))?$)/i.exec(id) if (match) { const [name] = match - console.log('manualChunks worker =>', name) return name } }, From 1b47c8e3eb38f68c1fd003c5f16c01772cd1d529 Mon Sep 17 00:00:00 2001 From: Yuxuan Zhang Date: Fri, 29 Dec 2023 12:45:03 -0500 Subject: [PATCH 11/43] perf: improve rpc efficiency --- package.json | 2 +- pnpm-lock.yaml | 8 ++++---- src/node/build/render-worker.ts | 36 +++++++++++++++++++-------------- 3 files changed, 26 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index 1883ed93..0b9c8dc6 100644 --- a/package.json +++ b/package.json @@ -101,7 +101,7 @@ "mark.js": "8.11.1", "minisearch": "^6.3.0", "mrmime": "^2.0.0", - "rpc-magic-proxy": "0.0.0-beta.0", + "rpc-magic-proxy": "0.0.0-beta.1", "shikiji": "^0.9.12", "shikiji-transformers": "^0.9.12", "vite": "^5.0.10", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cd0f668f..11c9cbb3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,8 +45,8 @@ importers: specifier: ^2.0.0 version: 2.0.0 rpc-magic-proxy: - specifier: 0.0.0-beta.0 - version: 0.0.0-beta.0 + specifier: 0.0.0-beta.1 + version: 0.0.0-beta.1 shikiji: specifier: ^0.9.12 version: 0.9.12 @@ -3916,8 +3916,8 @@ packages: '@rollup/rollup-win32-x64-msvc': 4.9.1 fsevents: 2.3.3 - /rpc-magic-proxy@0.0.0-beta.0: - resolution: {integrity: sha512-1UFsu4fpV/OGL8c1knrnx613zzZY9XSbqLUd5pSmOfh0ErAz1R8Dve5S65otFAtNtTO56SWFHbutHcw1lUkkcg==} + /rpc-magic-proxy@0.0.0-beta.1: + resolution: {integrity: sha512-Vqf8sflbh/Xpw6pfFAV+sd4iY3yJYN/vhh0WdTMTVGg5Lva3gwumPfSA4X5RFjSulLcjxusOUrlXp9vJ8ycgKQ==} dev: false /run-parallel@1.2.0: diff --git a/src/node/build/render-worker.ts b/src/node/build/render-worker.ts index a2c412f5..04ce2773 100644 --- a/src/node/build/render-worker.ts +++ b/src/node/build/render-worker.ts @@ -14,24 +14,25 @@ export default async function cluster( ) { const concurrency = context.config.buildConcurrency || 1 const num_tasks = pages.length - let progress = -concurrency const pageAlloc: TaskAllocator = async () => { - progress++ - if (progress >= 0) update(progress, num_tasks) - return pages.shift() + const page = pages.shift() + if (page) update(num_tasks - pages.length, num_tasks) + return page } const tasks = [] + const ctx = new RpcContext() + const workerData = await ctx.serialize({ + concurrency, + entryPath, + pageAlloc, + context, + workload: 'render' + }) + for (let _ = 0; _ < concurrency; _++) { - const ctx = new RpcContext() - const workerData = await ctx.serialize({ - entryPath, - pageAlloc, - context, - workload: 'render' - }) const worker = new Worker(new URL(import.meta.url), { workerData }) ctx.bind(worker) tasks.push( @@ -51,10 +52,12 @@ async function renderWorker() { const ctx = new RpcContext(parentPort!) try { const { + concurrency, entryPath, pageAlloc, context }: { + concurrency: number entryPath: string pageAlloc: TaskAllocator context: RenderPageContext @@ -62,11 +65,14 @@ async function renderWorker() { const { pathToFileURL } = await import('url') const { renderPage } = await import('./render') const { render } = await import(pathToFileURL(entryPath).toString()) - while (true) { - const page = await pageAlloc() - if (!page) break - await renderPage(render, page, context) + async function executor() { + while (true) { + const page = await pageAlloc() + if (!page) break + await renderPage(render, page, context) + } } + await Promise.all(Array.from({ length: concurrency * 4 }, () => executor())) } catch (e) { console.error(e) } finally { From 541ad0a2ca7a82ebc4f80f18d5ff669872f97e63 Mon Sep 17 00:00:00 2001 From: Yuxuan Zhang Date: Sat, 30 Dec 2023 11:15:00 -0500 Subject: [PATCH 12/43] cleanup logic, improve perf --- src/node/build/render-worker.ts | 14 ++++++++++---- src/node/siteConfig.ts | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/node/build/render-worker.ts b/src/node/build/render-worker.ts index 04ce2773..8f8c890d 100644 --- a/src/node/build/render-worker.ts +++ b/src/node/build/render-worker.ts @@ -12,7 +12,14 @@ export default async function cluster( pages: string[], update: UpdateHandle ) { - const concurrency = context.config.buildConcurrency || 1 + // - Each render worker could consume up to 150% of a CPU core. + // - One extra core is allocated to the main thread. + // - Excess worker will cause too much RPC workload for main thread, + // therefore harm the overall performance. + const concurrency = Math.round( + Math.max((context.config.buildConcurrency - 1) / 1.5, 1) + ) + const num_tasks = pages.length const pageAlloc: TaskAllocator = async () => { @@ -52,12 +59,10 @@ async function renderWorker() { const ctx = new RpcContext(parentPort!) try { const { - concurrency, entryPath, pageAlloc, context }: { - concurrency: number entryPath: string pageAlloc: TaskAllocator context: RenderPageContext @@ -72,7 +77,8 @@ async function renderWorker() { await renderPage(render, page, context) } } - await Promise.all(Array.from({ length: concurrency * 4 }, () => executor())) + const concurrency = Math.max(context.config.buildConcurrency, 1) + await Promise.all(Array.from({ length: concurrency }, () => executor())) } catch (e) { console.error(e) } finally { diff --git a/src/node/siteConfig.ts b/src/node/siteConfig.ts index 21b99bce..92bae1d0 100644 --- a/src/node/siteConfig.ts +++ b/src/node/siteConfig.ts @@ -152,7 +152,7 @@ export interface UserConfig * A lower number will reduce the memory usage but will increase the build time. * * @experimental - * @default 64 + * @default "Number of CPU cores available" */ buildConcurrency?: number From 8c429b447342eb315b45871673e1253ec5eaf9f3 Mon Sep 17 00:00:00 2001 From: Yuxuan Zhang Date: Sat, 30 Dec 2023 12:39:24 -0500 Subject: [PATCH 13/43] revert rollup.config: manualChunks not helpful --- rollup.config.ts | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/rollup.config.ts b/rollup.config.ts index 40478c70..83fb93ec 100644 --- a/rollup.config.ts +++ b/rollup.config.ts @@ -46,26 +46,12 @@ const plugins = [ json() ] -const node_root = r('src/node/') - const esmBuild: RollupOptions = { input: [r('src/node/index.ts'), r('src/node/cli.ts')], output: { format: 'esm', entryFileNames: `[name].js`, - chunkFileNames(chunk) { - return `${chunk.name}-[hash].js` - }, - manualChunks(id) { - // Chunk each worker into a separate file - if (!id.startsWith(node_root)) return - id = id.slice(node_root.length).replace(/^\//, '') - const match = /^.*\-worker(?=(\.(js|ts))?$)/i.exec(id) - if (match) { - const [name] = match - return name - } - }, + chunkFileNames: 'serve-[hash].js', dir: r('dist/node'), sourcemap: DEV }, From ace515b3f9e4ea956271cad281855f3d9a168da7 Mon Sep 17 00:00:00 2001 From: Yuxuan Zhang Date: Sat, 30 Dec 2023 19:10:58 -0500 Subject: [PATCH 14/43] use picocolors instead of terminal control sequence for color marks --- src/node/utils/task.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/node/utils/task.ts b/src/node/utils/task.ts index cd77d4fd..9ceb247a 100644 --- a/src/node/utils/task.ts +++ b/src/node/utils/task.ts @@ -1,8 +1,9 @@ import ora from 'ora' import humanizeDuration from 'humanize-duration' +import c from 'picocolors' -export const okMark = '\x1b[32m✓\x1b[0m' -export const failMark = '\x1b[31m✖\x1b[0m' +export const okMark = c.green('✓') +export const failMark = c.red('✖') export type UpdateHandle = (done: number, total?: number) => any From 9195369746adc479d354e5d0362c265cc0d9894e Mon Sep 17 00:00:00 2001 From: Yuxuan Zhang Date: Sat, 30 Dec 2023 19:32:02 -0500 Subject: [PATCH 15/43] expose updateHandle for subtasks --- src/node/utils/task.ts | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/node/utils/task.ts b/src/node/utils/task.ts index 9ceb247a..1466454b 100644 --- a/src/node/utils/task.ts +++ b/src/node/utils/task.ts @@ -5,7 +5,17 @@ import c from 'picocolors' export const okMark = c.green('✓') export const failMark = c.red('✖') -export type UpdateHandle = (done: number, total?: number) => any +export type UpdateHandle = ( + done?: number, + total?: number, + subtask?: string +) => any + +let updateHandle: UpdateHandle | null = null + +export const updateCurrentTask: UpdateHandle = (...args) => { + updateHandle?.(...args) +} export async function task( taskName: string, @@ -14,14 +24,17 @@ export async function task( const spinner = ora({ discardStdin: false }) spinner.start(taskName + '...') - const updateHandle: UpdateHandle = (done, total) => { - if (total === undefined) { - spinner.text = `${taskName} [ ${done} ]` + updateHandle = (done, total, subtask) => { + const taskFullName = subtask ? `${taskName} - ${subtask}` : taskName + if (done === undefined) { + spinner.text = taskFullName + '...' + } else if (total === undefined) { + spinner.text = `${taskFullName} [ ${done} ]` } else { // match length to display them in same width const _total = `${total}` const _done = `${done}`.padStart(_total.length, ' ') - spinner.text = `${taskName} [ ${_done} / ${_total} ]` + spinner.text = `${taskFullName} [ ${_done} / ${_total} ]` } } @@ -34,6 +47,7 @@ export async function task( success = false throw e } finally { + updateHandle = null const timeEnd = performance.now() const duration = humanizeDuration(timeEnd - timeStart, { maxDecimalPoints: 2 From 007fa592dafacaf8e16a3a550c85a1c4ef3eb3de Mon Sep 17 00:00:00 2001 From: Yuxuan Zhang Date: Sat, 30 Dec 2023 20:24:54 -0500 Subject: [PATCH 16/43] build/bundle: use task return value passthrough --- src/node/build/bundle.ts | 63 ++++++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/src/node/build/bundle.ts b/src/node/build/bundle.ts index 159bb43c..8d19cee3 100644 --- a/src/node/build/bundle.ts +++ b/src/node/build/bundle.ts @@ -143,40 +143,41 @@ export async function bundle( configFile: config.vite?.configFile }) - let clientResult!: Rollup.RollupOutput | null - let serverResult!: Rollup.RollupOutput - - await task('building client + server bundles', async () => { - clientResult = config.mpa - ? null - : ((await build(await resolveViteConfig(false))) as Rollup.RollupOutput) - serverResult = (await build( - await resolveViteConfig(true) - )) as Rollup.RollupOutput - }) + const serverResult = await task( + 'building server bundle', + () => resolveViteConfig(true).then(build) as Promise + ) - if (config.mpa) { - // in MPA mode, we need to copy over the non-js asset files from the - // server build since there is no client-side build. - await Promise.all( - serverResult.output.map(async (chunk) => { - if (!chunk.fileName.endsWith('.js')) { - const tempPath = path.resolve(config.tempDir, chunk.fileName) - const outPath = path.resolve(config.outDir, chunk.fileName) - await fs.copy(tempPath, outPath) + const clientResult = !config.mpa + ? await task( + 'building client bundle', + () => + resolveViteConfig(false).then(build) as Promise + ) + : await task('building client bundle (MPA)', async () => { + // in MPA mode, we need to copy over the non-js asset files from the + // server build since there is no client-side build. + await Promise.all( + serverResult.output.map(async (chunk) => { + if (!chunk.fileName.endsWith('.js')) { + const tempPath = path.resolve(config.tempDir, chunk.fileName) + const outPath = path.resolve(config.outDir, chunk.fileName) + await fs.copy(tempPath, outPath) + } + }) + ) + // also copy over public dir + const publicDir = path.resolve(config.srcDir, 'public') + if (fs.existsSync(publicDir)) { + await fs.copy(publicDir, config.outDir) + } + // build ` - : '', - await renderHead(head), - ``, - ``, - teleports?.body || '', - `
${content}
`, - metadataScript.inHead ? '' : metadataScript.html, - inlinedScript, - ``, - `` - ].join('') + const html = ` + + + + ${ + isMetaViewportOverridden(head) + ? '' + : '' + } + ${title} + ${ + isDescriptionOverridden(head) + ? '' + : `` + } + + ${stylesheetLink} + ${metadataScript.inHead ? metadataScript.html : ''} + ${ + appChunk + ? `` + : '' + } + ${await renderHead(head)} + + ${teleports?.body || ''} +
${content}
+ ${metadataScript.inHead ? '' : metadataScript.html} + ${inlinedScript} + +` const htmlFileName = path.join(config.outDir, page.replace(/\.md$/, '.html')) await fs.ensureDir(path.dirname(htmlFileName)) diff --git a/src/node/build/viteConfig.ts b/src/node/build/viteConfig.ts deleted file mode 100644 index 80041d73..00000000 --- a/src/node/build/viteConfig.ts +++ /dev/null @@ -1,212 +0,0 @@ -import path from 'path' -import { fileURLToPath } from 'url' -import { - normalizePath, - type BuildOptions, - type Rollup, - type InlineConfig as ViteInlineConfig, - type PluginOption -} from 'vite' -import { APP_PATH } from '../alias' -import type { SiteConfig } from '../config' -import { slash } from '../shared' -import { escapeRegExp, sanitizeFileName } from '../shared' - -// https://github.com/vitejs/vite/blob/d2aa0969ee316000d3b957d7e879f001e85e369e/packages/vite/src/node/plugins/splitVendorChunk.ts#L14 -const CSS_LANGS_RE = - /\.(css|less|sass|scss|styl|stylus|pcss|postcss|sss)(?:$|\?)/ - -const clientDir = normalizePath( - path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../client') -) - -// these deps are also being used in the client code (outside of the theme) -// exclude them from the theme chunk so there is no circular dependency -const excludedModules = [ - '/@siteData', - 'node_modules/@vueuse/core/', - 'node_modules/@vueuse/shared/', - 'node_modules/vue/', - 'node_modules/vue-demi/', - clientDir -] - -const themeEntryRE = (themeDir: string) => - new RegExp( - `^${escapeRegExp( - path.resolve(themeDir, 'index.js').replace(/\\/g, '/') - ).slice(0, -2)}m?(j|t)s` - ) - -const cache = new Map() -const cacheTheme = new Map() - -/** - * Check if a module is statically imported by at least one entry. - */ -function isEagerChunk(id: string, getModuleInfo: Rollup.GetModuleInfo) { - if ( - id.includes('node_modules') && - !CSS_LANGS_RE.test(id) && - staticImportedByEntry(id, getModuleInfo, cache) - ) { - return true - } -} - -function staticImportedByEntry( - id: string, - getModuleInfo: Rollup.GetModuleInfo, - cache: Map, - entryRE: RegExp | null = null, - importStack: string[] = [] -): boolean { - if (cache.has(id)) { - return !!cache.get(id) - } - if (importStack.includes(id)) { - // circular deps! - cache.set(id, false) - return false - } - const mod = getModuleInfo(id) - if (!mod) { - cache.set(id, false) - return false - } - - if (entryRE ? entryRE.test(id) : mod.isEntry) { - cache.set(id, true) - return true - } - const someImporterIs = mod.importers.some((importer: string) => - staticImportedByEntry( - importer, - getModuleInfo, - cache, - entryRE, - importStack.concat(id) - ) - ) - cache.set(id, someImporterIs) - return someImporterIs -} - -// define custom rollup input -// this is a multi-entry build - every page is considered an entry chunk -// the loading is done via filename conversion rules so that the -// metadata doesn't need to be included in the main chunk. -const resolveInput = (config: SiteConfig) => - Object.fromEntries( - config.pages.map((file) => { - // page filename conversion - // foo/bar.md -> foo_bar.md - const alias = config.rewrites.map[file] || file - return [ - slash(alias).replace(/\//g, '_'), - path.resolve(config.srcDir, file) - ] - }) - ) as Record - -export default async function resolveViteConfig( - ssr: boolean, - { - config, - options, - plugins - }: { - config: SiteConfig - options: BuildOptions - plugins: PluginOption[] - } -): Promise { - return { - root: config.srcDir, - cacheDir: config.cacheDir, - base: config.site.base, - logLevel: config.vite?.logLevel ?? 'warn', - plugins, - ssr: { - noExternal: ['vitepress', '@docsearch/css'] - }, - build: { - ...options, - emptyOutDir: true, - ssr, - ssrEmitAssets: config.mpa, - // minify with esbuild in MPA mode (for CSS) - minify: ssr - ? config.mpa && 'esbuild' - : options.minify ?? !process.env.DEBUG, - outDir: ssr ? config.tempDir : config.outDir, - cssCodeSplit: false, - rollupOptions: { - ...options.rollupOptions, - input: { - ...resolveInput(config), - // use different entry based on ssr or not - app: path.resolve(APP_PATH, ssr ? 'ssr.js' : 'index.js') - }, - // important so that each page chunk and the index export things for each - // other - preserveEntrySignatures: 'allow-extension', - output: { - sanitizeFileName, - ...options.rollupOptions?.output, - assetFileNames: `${config.assetsDir}/[name].[hash].[ext]`, - ...(ssr - ? { - entryFileNames: '[name].js', - chunkFileNames: '[name].[hash].js' - } - : { - entryFileNames: `${config.assetsDir}/[name].[hash].js`, - chunkFileNames(chunk) { - // avoid ads chunk being intercepted by adblock - return /(?:Carbon|BuySell)Ads/.test(chunk.name) - ? `${config.assetsDir}/chunks/ui-custom.[hash].js` - : `${config.assetsDir}/chunks/[name].[hash].js` - }, - manualChunks(id, ctx) { - // move known framework code into a stable chunk so that - // custom theme changes do not invalidate hash for all pages - if (id.startsWith('\0vite')) { - return 'framework' - } - if (id.includes('plugin-vue:export-helper')) { - return 'framework' - } - if ( - id.includes(`${clientDir}/app`) && - id !== `${clientDir}/app/index.js` - ) { - return 'framework' - } - if ( - isEagerChunk(id, ctx.getModuleInfo) && - /@vue\/(runtime|shared|reactivity)/.test(id) - ) { - return 'framework' - } - - if ( - (id.startsWith(`${clientDir}/theme-default`) || - !excludedModules.some((i) => id.includes(i))) && - staticImportedByEntry( - id, - ctx.getModuleInfo, - cacheTheme, - themeEntryRE(config.themeDir) - ) - ) { - return 'theme' - } - } - }) - } - } - }, - configFile: config.vite?.configFile - } -} diff --git a/src/node/plugins/localSearchPlugin.ts b/src/node/plugins/localSearchPlugin.ts index b282aa7b..673e5eeb 100644 --- a/src/node/plugins/localSearchPlugin.ts +++ b/src/node/plugins/localSearchPlugin.ts @@ -32,9 +32,11 @@ interface IndexObject { titles: string[] } +// SSR being `true` or `false` dose not affect the config related to markdown +// renderer, so it can be reused. let md: MarkdownRenderer -let flagAllIndexed = false +let flagScanned = false const taskByLocale = new Map>() const filesByLocale = new Map>() const indexByLocale = new Map>() @@ -160,9 +162,10 @@ export async function localSearchPlugin( } } - function indexAll() { - if (flagAllIndexed) return - flagAllIndexed = true + // scan all pages, group by locale and create empty indexes accordingly. + function scanForIndex() { + if (flagScanned) return + flagScanned = true for (const page of siteConfig.pages) { const file = path.join(siteConfig.srcDir, page) @@ -209,7 +212,7 @@ export async function localSearchPlugin( async load(id) { if (id === LOCAL_SEARCH_INDEX_REQUEST_PATH) { - indexAll() + scanForIndex() let records: string[] = [] for (const [locale] of filesByLocale) { records.push( diff --git a/src/node/server.ts b/src/node/server.ts index ff079188..953d7daf 100644 --- a/src/node/server.ts +++ b/src/node/server.ts @@ -28,7 +28,10 @@ export async function createServer( configFile: config.vite?.configFile }).then((server) => Object.assign({}, server, { - close: () => server.close().then(() => stopWorkers('server.close()')) + close() { + stopWorkers('server.close()') + return server.close() + } }) ) } diff --git a/src/node/utils/queue.ts b/src/node/utils/queue.ts index 26a8123e..dff9753e 100644 --- a/src/node/utils/queue.ts +++ b/src/node/utils/queue.ts @@ -19,6 +19,7 @@ export default class Queue { enqueue(data: T) { if (this.closed) throw new Error(`Failed to enqueue ${data}, queue already closed`) + if (data === null) return this.close() if (this.pending.length) this.pending.shift()!(data) else this.queue.push(data) } diff --git a/src/node/worker.ts b/src/node/worker.ts index 69cdb50a..6e716f07 100644 --- a/src/node/worker.ts +++ b/src/node/worker.ts @@ -1,5 +1,8 @@ import { Worker, isMainThread, parentPort, workerData } from 'worker_threads' -import RPCContext, { deferPromise } from 'rpc-magic-proxy' +import RPCContext, { + deferPromise, + type RPCContextOptions +} from 'rpc-magic-proxy' import { task, updateCurrentTask } from './utils/task' import c from 'picocolors' import Queue from './utils/queue' @@ -7,8 +10,12 @@ import _debug from 'debug' import type { SiteConfig } from 'siteConfig' import humanizeDuration from 'humanize-duration' -export type SupportsParallel = 'bundle' | 'render' | 'local-search' +export type SupportsParallel = 'render' | 'local-search' +const options: RPCContextOptions = { + carryThis: false, + carrySideEffect: false +} /** * Checks if the given task should be run in parallel. * If task is omitted, checks if any task should be run in parallel. @@ -61,7 +68,7 @@ export async function launchWorkers(numWorkers: number, context: Object) { debug(`launching ${numWorkers} workers`) taskQueue = new Queue() const allInitialized: Array> = [] - const ctx = new RPCContext() + const ctx = new RPCContext(options) const getNextTask = () => taskQueue?.dequeue() ?? null for (let i = 0; i < numWorkers; i++) { const workerId = (i + 1).toString().padStart(2, '0') @@ -99,7 +106,8 @@ export function updateContext(context: Object) { return Promise.all(workers.map(({ hooks }) => hooks.updateContext(context))) } -// Wait for workers to drain the taskQueue and exit. +// Wait for workers to finish and exit. +// Will return immediately if no worker exists. export async function stopWorkers(reason: string = 'exit') { debug('stopping workers:', reason) const allClosed = workers.map((w) => @@ -111,10 +119,10 @@ export async function stopWorkers(reason: string = 'exit') { taskQueue = null const success = await Promise.any([ Promise.all(allClosed).then(() => true), - new Promise((res) => setTimeout(() => res(false), 2000)) + new Promise((res) => setTimeout(() => res(false), 1500)) ]) if (!success) { - console.warn('forcefully terminating workers') + debug('forcefully terminating workers') for (const w of workers) { try { w.terminate() @@ -152,7 +160,7 @@ export function registerWorkload( // Will keep querying next workload from main thread async function workerMainLoop() { - const ctx = new RPCContext().bind(parentPort! as any) + const ctx = new RPCContext(options).bind(parentPort! as any) const { workerMeta: _workerMeta, initWorkerHooks, From 1dd35b222658a8cf5c0c6c3106d95d54c646cab9 Mon Sep 17 00:00:00 2001 From: Yuxuan Zhang Date: Mon, 8 Jan 2024 16:50:38 -0500 Subject: [PATCH 41/43] strip changes from #3385 --- package.json | 2 - pnpm-lock.yaml | 14 ----- src/node/build/build.ts | 19 +++---- src/node/plugins/localSearchPlugin.ts | 11 ++-- src/node/utils/task.ts | 76 +++------------------------ src/node/worker.ts | 7 +-- 6 files changed, 19 insertions(+), 110 deletions(-) diff --git a/package.json b/package.json index de976c97..c6321b40 100644 --- a/package.json +++ b/package.json @@ -138,7 +138,6 @@ "@types/debug": "^4.1.12", "@types/escape-html": "^1.0.4", "@types/fs-extra": "^11.0.4", - "@types/humanize-duration": "^3.27.3", "@types/jsdom": "^21.1.6", "@types/lodash.template": "^4.5.3", "@types/mark.js": "^8.11.12", @@ -164,7 +163,6 @@ "fs-extra": "^11.2.0", "get-port": "^7.0.0", "gray-matter": "^4.0.3", - "humanize-duration": "^3.31.0", "lint-staged": "^15.2.0", "lodash.template": "^4.5.0", "lru-cache": "^10.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3477519c..5d10e226 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -114,9 +114,6 @@ importers: '@types/fs-extra': specifier: ^11.0.4 version: 11.0.4 - '@types/humanize-duration': - specifier: ^3.27.3 - version: 3.27.3 '@types/jsdom': specifier: ^21.1.6 version: 21.1.6 @@ -192,9 +189,6 @@ importers: gray-matter: specifier: ^4.0.3 version: 4.0.3 - humanize-duration: - specifier: ^3.31.0 - version: 3.31.0 lint-staged: specifier: ^15.2.0 version: 15.2.0(supports-color@9.4.0) @@ -1160,10 +1154,6 @@ packages: resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} dev: true - /@types/humanize-duration@3.27.3: - resolution: {integrity: sha512-wiiiFYjnrYDJE/ujU7wS/NShqp12IKrejozjDtcejP0zYi+cjyjVcfZHwcFUDKVJ7tHGsmgeW2ED92ABIIjfpg==} - dev: true - /@types/jquery@3.5.29: resolution: {integrity: sha512-oXQQC9X9MOPRrMhPHHOsXqeQDnWeCDT3PelUIg/Oy8FAbzSZtFHRjc7IpbfFVmpLtJ+UOoywpRsuO5Jxjybyeg==} dependencies: @@ -2803,10 +2793,6 @@ packages: engines: {node: '>=16.17.0'} dev: true - /humanize-duration@3.31.0: - resolution: {integrity: sha512-fRrehgBG26NNZysRlTq1S+HPtDpp3u+Jzdc/d5A4cEzOD86YLAkDaJyJg8krSdCi7CJ+s7ht3fwRj8Dl+Btd0w==} - dev: true - /iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} diff --git a/src/node/build/build.ts b/src/node/build/build.ts index f6467fb4..5a5ba4a5 100644 --- a/src/node/build/build.ts +++ b/src/node/build/build.ts @@ -74,16 +74,16 @@ export async function build( } try { - const { clientResult, serverResult, pageToHashMap } = await task( - 'building client + server bundles', - () => bundle(siteConfig, buildOptions) + const { clientResult, serverResult, pageToHashMap } = await bundle( + siteConfig, + buildOptions ) if (process.env.BUNDLE_ONLY) { return } - await task('rendering pages', async (updateProgress) => { + await task('rendering pages', async () => { const renderEntry = pathToFileURL(path.join(siteConfig.tempDir, 'app.js')).toString() + '?t=' + @@ -165,14 +165,9 @@ export async function build( } const pages = ['404.md', ...siteConfig.pages] - let count_done = 0 - await pMap( - pages, - (page) => task(page).then(updateProgress(++count_done, pages.length)), - { - concurrency: siteConfig.concurrency - } - ) + await pMap(pages, task, { + concurrency: siteConfig.concurrency + }) }) // emit page hash map for the case where a user session is open diff --git a/src/node/plugins/localSearchPlugin.ts b/src/node/plugins/localSearchPlugin.ts index 673e5eeb..5a62fb49 100644 --- a/src/node/plugins/localSearchPlugin.ts +++ b/src/node/plugins/localSearchPlugin.ts @@ -17,7 +17,6 @@ import { type Awaitable } from '../shared' import { processIncludes } from '../utils/processIncludes' -import { updateCurrentTask, clearLine } from '../utils/task' import type { PageSplitSection } from '../../../types/local-search' const debug = _debug('vitepress:local-search') @@ -176,12 +175,10 @@ export async function localSearchPlugin( } async function indexLocale(locale: string) { - let numIndexed = 0 - const update = () => - updateCurrentTask(++numIndexed, files.length, `🔍️ indexing ${locale}`) const files = [...filesByLocale.get(locale)!] - const task = (f: string) => indexFile(f, parallel).then(update) - await pMap(files, task, { concurrency: siteConfig.concurrency }) + await pMap(files, (f) => indexFile(f, parallel), { + concurrency: siteConfig.concurrency + }) } const parallel = shouldUseParallel(siteConfig, 'local-search') @@ -264,7 +261,7 @@ async function* splitPageIntoSections( // Skip duplicate id, content will be treated as normal text if (existingIdSet.has(id)) { console.error( - `${clearLine}⚠️ Duplicate heading id "${id}" in ${pageName}` + `\x1b[2K\r ⚠️ Duplicate heading id "${id}" in ${pageName}` ) continue } diff --git a/src/node/utils/task.ts b/src/node/utils/task.ts index 91779af4..1d8a3322 100644 --- a/src/node/utils/task.ts +++ b/src/node/utils/task.ts @@ -1,80 +1,18 @@ import ora from 'ora' -import humanizeDuration from 'humanize-duration' -import c from 'picocolors' -import { workerMeta } from '../worker' -export const okMark = c.green('✓') -export const failMark = c.red('✖') -export const clearLine = '\x1b[2K\r' - -export type UpdateHandle = ( - done?: number, - total?: number, - subtask?: string -) => any - -let updateHandle: UpdateHandle | null = null - -export const updateCurrentTask: UpdateHandle = (...args) => { - if (workerMeta) workerMeta.updateCurrentTask(...args) - else if (updateHandle) updateHandle(...args) - else if (!process.stderr.isTTY) { - return - } else if (args.length === 0) { - process.stderr.write(clearLine) - } else { - const name = args[2] || 'unknown task' - process.stderr.write( - `${clearLine}${name} [${args.slice(0, 2).join(' / ')}]` - ) - } -} - -export async function task( - taskName: string, - task: (update: UpdateHandle) => Promise -): Promise { - if (workerMeta) { - let retVal: T - await workerMeta.task(taskName, async (handle: UpdateHandle) => { - retVal = await task(handle) - }) - return retVal! - } +export const okMark = '\x1b[32m✓\x1b[0m' +export const failMark = '\x1b[31m✖\x1b[0m' +export async function task(taskName: string, task: () => Promise) { const spinner = ora({ discardStdin: false }) spinner.start(taskName + '...') - updateHandle = (done, total, subtask) => { - const taskFullName = subtask ? `${taskName} - ${subtask}` : taskName - if (done === undefined) { - spinner.text = taskFullName + '...' - } else if (total === undefined) { - spinner.text = `${taskFullName} [ ${done} ]` - } else { - // match length to display them in same width - const _total = `${total}` - const _done = `${done}`.padStart(_total.length, ' ') - spinner.text = `${taskFullName} [ ${_done} / ${_total} ]` - } - } - - const timeStart = performance.now() - let success = true - try { - return await task(updateHandle) + await task() } catch (e) { - success = false + spinner.stopAndPersist({ symbol: failMark }) throw e - } finally { - updateHandle = null - const timeEnd = performance.now() - const duration = humanizeDuration(timeEnd - timeStart, { - maxDecimalPoints: 2 - }) - const text = `${taskName} - ${duration}` - const symbol = success ? okMark : failMark - spinner.stopAndPersist({ symbol, text }) } + + spinner.stopAndPersist({ symbol: okMark }) } diff --git a/src/node/worker.ts b/src/node/worker.ts index 6e716f07..4628d017 100644 --- a/src/node/worker.ts +++ b/src/node/worker.ts @@ -3,7 +3,6 @@ import RPCContext, { deferPromise, type RPCContextOptions } from 'rpc-magic-proxy' -import { task, updateCurrentTask } from './utils/task' import c from 'picocolors' import Queue from './utils/queue' import _debug from 'debug' @@ -83,9 +82,7 @@ export async function launchWorkers(numWorkers: number, context: Object) { workerId, dispatchWork, // Save some RPC overhead when debugger is not active - debug: debug.enabled ? debug : null, - task, - updateCurrentTask + debug: debug.enabled ? debug : null }, initWorkerHooks, getNextTask, @@ -137,8 +134,6 @@ export let workerMeta: { workerId: string dispatchWork: typeof dispatchWork debug: typeof debug - task: typeof task - updateCurrentTask: typeof updateCurrentTask } | null = null const registry: Map = new Map() From e5a1c7577ba810e53a94ed9d20f2c6ef2c4f7260 Mon Sep 17 00:00:00 2001 From: Yuxuan Zhang Date: Mon, 8 Jan 2024 16:54:58 -0500 Subject: [PATCH 42/43] fix broken imports due to stripped feature --- src/node/build/build.ts | 11 ++++------- src/node/worker.ts | 7 ++----- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/src/node/build/build.ts b/src/node/build/build.ts index 5a5ba4a5..09a9ae09 100644 --- a/src/node/build/build.ts +++ b/src/node/build/build.ts @@ -15,7 +15,6 @@ import { task } from '../utils/task' import { bundle } from './bundle' import { generateSitemap } from './generateSitemap' import { renderPage, type RenderPageContext } from './render' -import humanizeDuration from 'humanize-duration' import { launchWorkers, shouldUseParallel, stopWorkers } from '../worker' import { registerWorkload, updateContext } from '../worker' @@ -45,7 +44,7 @@ export async function build( root?: string, buildOptions: BuildOptions & { base?: string; mpa?: string } = {} ) { - const timeStart = performance.now() + const start = Date.now() process.env.NODE_ENV = 'production' const siteConfig = await resolveConfig(root, 'build', 'production') @@ -185,11 +184,9 @@ export async function build( await siteConfig.buildEnd?.(siteConfig) clearCache() stopWorkers('build complete') - const timeEnd = performance.now() - const duration = humanizeDuration(timeEnd - timeStart, { - maxDecimalPoints: 2 - }) - siteConfig.logger.info(`build complete in ${duration}.`) + siteConfig.logger.info( + `build complete in ${((Date.now() - start) / 1000).toFixed(2)}s.` + ) } function linkVue() { diff --git a/src/node/worker.ts b/src/node/worker.ts index 4628d017..8ef29528 100644 --- a/src/node/worker.ts +++ b/src/node/worker.ts @@ -7,7 +7,6 @@ import c from 'picocolors' import Queue from './utils/queue' import _debug from 'debug' import type { SiteConfig } from 'siteConfig' -import humanizeDuration from 'humanize-duration' export type SupportsParallel = 'render' | 'local-search' @@ -213,10 +212,8 @@ async function workerMainLoop() { } } ctx.reset() - const duration = humanizeDuration(workTime, { - maxDecimalPoints: 2 - }) - await debug(`stopped - total workload: ${duration}`) + const duration = (workTime / 1000).toFixed(2) + await debug(`stopped - total workload: ${duration}s`) } if (!isMainThread && workerData?.[WORKER_MAGIC]) From 671087cfbdec7a8f29afa00245866d0227b63468 Mon Sep 17 00:00:00 2001 From: Yuxuan Zhang Date: Sat, 13 Jan 2024 18:51:26 -0500 Subject: [PATCH 43/43] fix pnpm lock file after merge --- pnpm-lock.yaml | 70 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 66 insertions(+), 4 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 15bdc8bf..a2d2dec1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -273,6 +273,9 @@ importers: rollup-plugin-esbuild: specifier: ^6.1.0 version: 6.1.0(esbuild@0.19.11)(rollup@4.9.5)(supports-color@9.4.0) + rpc-magic-proxy: + specifier: ^2.0.6 + version: 2.0.6 semver: specifier: ^7.5.4 version: 7.5.4 @@ -293,7 +296,7 @@ importers: version: 5.3.3 vitest: specifier: ^1.2.0 - version: 1.2.0(@types/node@20.11.0)(supports-color@9.4.0) + version: 1.2.0(@types/node@20.11.0)(jsdom@23.0.1)(supports-color@9.4.0) vue-tsc: specifier: ^1.8.27 version: 1.8.27(typescript@5.3.3) @@ -1253,7 +1256,7 @@ packages: /@types/jsdom@21.1.6: resolution: {integrity: sha512-/7kkMsC+/kMs7gAYmmBR9P0vGTnOoLhQhyhQJSlXGI5bzTHp6xdo0TtKWQAsz6pmSAeVqKSbqeyP6hytqr9FDw==} dependencies: - '@types/node': 20.10.7 + '@types/node': 20.11.0 '@types/tough-cookie': 4.0.5 parse5: 7.1.2 dev: true @@ -1405,6 +1408,10 @@ packages: resolution: {integrity: sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==} dev: true + /@types/tough-cookie@4.0.5: + resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + dev: true + /@types/trusted-types@2.0.7: resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} dev: true @@ -1690,6 +1697,14 @@ packages: resolution: {integrity: sha512-qQLMr+8o0WC4FZGQTcJiKBVC59JylcPSrTtk6usvmIDFUOCKegapy1VHQwRbFMOFyb/inzUVqHs+eMYKDM1YeQ==} dev: true + /agent-base@7.1.0(supports-color@9.4.0): + resolution: {integrity: sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==} + engines: {node: '>= 14'} + dependencies: + debug: 4.3.4(supports-color@9.4.0) + transitivePeerDependencies: + - supports-color + /algoliasearch@4.22.1: resolution: {integrity: sha512-jwydKFQJKIx9kIZ8Jm44SdpigFwRGPESaxZBaHSV0XWN2yBJAOT4mT7ppvlrpA4UGzz92pqFnVKr/kaZXrcreg==} dependencies: @@ -2275,6 +2290,13 @@ packages: engines: {node: '>= 12'} dev: true + /data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.0.0 + /de-indent@1.0.2: resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} dev: true @@ -3076,6 +3098,12 @@ packages: lru-cache: 10.1.0 dev: true + /html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + dependencies: + whatwg-encoding: 3.1.1 + /html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} dev: true @@ -3125,6 +3153,12 @@ packages: engines: {node: '>=16.17.0'} dev: true + /iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + /ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} dev: true @@ -3296,6 +3330,9 @@ packages: engines: {node: '>=12'} dev: true + /is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + /is-reference@1.2.1: resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} dependencies: @@ -4144,7 +4181,6 @@ packages: resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} dependencies: entities: 4.5.0 - dev: true /path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} @@ -4465,6 +4501,9 @@ packages: unified: 11.0.4 dev: true + /requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + /resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} dev: true @@ -4556,6 +4595,13 @@ packages: '@rollup/rollup-win32-x64-msvc': 4.9.5 fsevents: 2.3.3 + /rpc-magic-proxy@2.0.6: + resolution: {integrity: sha512-PtJfEIwXiFg64FUXUGv6cot6X3RssVeQ6Xb7t8lHaAPRDEwvuZf6zCNtHEyxgFw3qnBf+LyoLAX9EOl0ipNvig==} + dev: true + + /rrweb-cssom@0.6.0: + resolution: {integrity: sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==} + /run-applescript@7.0.0: resolution: {integrity: sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==} engines: {node: '>=18'} @@ -5088,6 +5134,12 @@ packages: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} dev: true + /tr46@5.0.0: + resolution: {integrity: sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==} + engines: {node: '>=18'} + dependencies: + punycode: 2.3.1 + /trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} dev: true @@ -5259,11 +5311,21 @@ packages: unist-util-visit-parents: 6.0.1 dev: true + /universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + /universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} dev: true + /url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} dev: true @@ -5363,7 +5425,7 @@ packages: optionalDependencies: fsevents: 2.3.3 - /vitest@1.2.0(@types/node@20.11.0)(supports-color@9.4.0): + /vitest@1.2.0(@types/node@20.11.0)(jsdom@23.0.1)(supports-color@9.4.0): resolution: {integrity: sha512-Ixs5m7BjqvLHXcibkzKRQUvD/XLw0E3rvqaCMlrm/0LMsA0309ZqYvTlPzkhh81VlEyVZXFlwWnkhb6/UMtcaQ==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true