| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586 |
- /** Production documentation-site build with project-owned output preparation. */
- import { lstatSync, realpathSync, rmSync, unlinkSync } from 'node:fs'
- import { dirname, isAbsolute, relative, resolve, sep } from 'node:path'
- import { pathToFileURL } from 'node:url'
- import { build } from 'vitepress'
- const websiteRoot = resolve(import.meta.dirname)
- type DocSiteBuildOptions = NonNullable<Parameters<typeof build>[1]>
- function escapesRoot(root: string, candidate: string): boolean {
- const child = relative(root, candidate)
- return child === '..' || child.startsWith(`..${sep}`) || isAbsolute(child)
- }
- function nearestExistingAncestor(path: string): string {
- let ancestor = path
- for (;;) {
- if (lstatSync(ancestor, { throwIfNoEntry: false }) !== undefined) return ancestor
- const parent = dirname(ancestor)
- if (parent === ancestor) {
- throw new Error(`website/build: no existing ancestor found for ${JSON.stringify(path)}.`)
- }
- ancestor = parent
- }
- }
- /**
- * Remove one documentation build output without traversing a link-shaped output or an outside parent.
- * @param siteRoot - VitePress site root that owns the output.
- * @param outDir - Resolved VitePress output directory.
- * @throws When `outDir` is not a proper child of `siteRoot` or its existing parent resolves outside it.
- */
- export function cleanDocSiteOutput(siteRoot: string, outDir: string): void {
- const root = resolve(siteRoot)
- const output = resolve(outDir)
- const child = relative(root, output)
- if (child === '' || escapesRoot(root, output)) {
- throw new Error(`website/build: output directory ${JSON.stringify(output)} must be a child of site root ${JSON.stringify(root)}.`)
- }
- const realRoot = realpathSync(root)
- const realParent = realpathSync(nearestExistingAncestor(dirname(output)))
- if (escapesRoot(realRoot, realParent)) {
- throw new Error(`website/build: output directory ${JSON.stringify(output)} must resolve inside site root ${JSON.stringify(realRoot)}.`)
- }
- const outputStats = lstatSync(output, { throwIfNoEntry: false })
- if (outputStats?.isSymbolicLink()) {
- unlinkSync(output)
- return
- }
- rmSync(output, { recursive: true, force: true })
- }
- /**
- * Create VitePress build options that remove the resolved output directory before bundling.
- * @param siteRoot - VitePress site root to build.
- * @param mpa - Whether to use VitePress's multi-page application build.
- * @returns VitePress options with project-owned output preparation.
- */
- export function docSiteBuildOptions(siteRoot: string, mpa: boolean): DocSiteBuildOptions {
- const root = resolve(siteRoot)
- return {
- ...mpa ? { mpa: 'true' } : {},
- onAfterConfigResolve(siteConfig) {
- cleanDocSiteOutput(root, siteConfig.outDir)
- },
- }
- }
- async function buildDocSite(siteRoot: string, mpa: boolean): Promise<void> {
- const root = resolve(siteRoot)
- await build(root, docSiteBuildOptions(root, mpa))
- }
- function parseMpa(args: string[]): boolean {
- if (args.length === 0) return false
- if (args.length === 1 && args[0] === '--mpa') return true
- throw new Error(`website/build: expected no arguments or --mpa, got ${JSON.stringify(args)}.`)
- }
- const invokedPath = process.argv[1]
- if (invokedPath !== undefined && import.meta.url === pathToFileURL(resolve(invokedPath)).href) {
- await buildDocSite(websiteRoot, parseMpa(process.argv.slice(2)))
- }
|