vite-plugin-react-pages
Version:
<p> <a href="https://www.npmjs.com/package/vite-plugin-react-pages" target="_blank" rel="noopener"><img src="https://img.shields.io/npm/v/vite-plugin-react-pages.svg" alt="npm package" /></a> </p>
1 lines • 163 kB
Source Map (JSON)
{"version":3,"file":"index.mjs","sources":["../../src/node/utils/virtual-module/utils/extractStaticData.ts","../../src/node/utils/virtual-module/utils/PendingTaskCounter.ts","../../src/node/utils/virtual-module/utils/File.ts","../../src/node/utils/virtual-module/VirtualModules.ts","../../src/node/utils/virtual-module/VirtualModulesManager.ts","../../src/node/utils/virtual-module/ProxyModulesManager.ts","../../src/node/page-strategy/UpdateBuffer.ts","../../src/node/page-strategy/PagesDataKeeper.ts","../../src/node/page-strategy/index.ts","../../src/node/page-strategy/DefaultPageStrategy/index.ts","../../src/node/page-strategy/pageUtils.ts","../../src/node/virtual-module-plugins/theme.ts","../../src/node/utils/mdastUtils.ts","../../src/node/virtual-module-plugins/demo-modules/mdx-plugin.ts","../../src/node/virtual-module-plugins/demo-modules/index.tsx","../../src/node/virtual-module-plugins/ts-info-module/extract.ts","../../src/node/virtual-module-plugins/ts-info-module/mdx-plugin.ts","../../src/node/virtual-module-plugins/ts-info-module/index.ts","../../src/node/utils/injectHTMLTag.ts","../../src/node/utils/mdx-plugin-file-text.ts","../../src/node/virtual-module-plugins/outline-info-module/extractOutlineInfo.ts","../../src/node/virtual-module-plugins/outline-info-module/index.tsx","../../src/node/index.ts"],"sourcesContent":["import { extract, parse } from 'jest-docblock'\nimport grayMatter from 'gray-matter'\nimport { File } from './File'\n\nexport async function extractStaticData(\n file: File\n): Promise<{ sourceType: string; [key: string]: any }> {\n const code = await file.read()\n switch (file.extname) {\n case 'md':\n case 'mdx':\n const { data: frontmatter } = grayMatter(code)\n const staticData: any = {\n ...frontmatter,\n sourceType: 'md',\n __sourceFilePath: file.path,\n }\n if (staticData.title === undefined) {\n staticData.title = extractMarkdownTitle(code)\n }\n return staticData\n case 'js':\n case 'jsx':\n case 'ts':\n case 'tsx':\n return { ...parse(extract(code)), sourceType: 'js' }\n default:\n throw new Error(`unexpected extension name \"${file.extname}\"`)\n }\n}\nfunction extractMarkdownTitle(code: string) {\n const match = code.match(/^# (.*)$/m)\n return match?.[1]\n}\n","export class PendingTaskCounter {\n private count = 0\n private callbacks: (() => void)[] = []\n\n public countTask() {\n this.count++\n let ended = false\n return () => {\n if (ended) return\n ended = true\n this.count--\n if (this.count === 0) {\n this.callbacks.forEach((cb) => cb())\n this.callbacks.length = 0\n }\n }\n }\n\n /**\n * the callback style is preferred over the promise style\n * because cb will be called **synchronously** when count turn 0\n * while promise-then-cb would be called in next microtask (at that time the state may have changed)\n */\n public callOnceWhenIdle(cb: () => void) {\n if (this.count === 0) {\n cb()\n } else {\n this.callbacks.push(cb)\n }\n }\n\n /** track a changeable pending state */\n public countPendingState(pendingState: PendingState) {\n let stopCounting: undefined | (() => void)\n pendingState.onStateChange((isPending) => {\n if (isPending) {\n // if this task has already been counted, don't count again\n if (stopCounting) return\n stopCounting = this.countTask()\n } else {\n stopCounting?.()\n stopCounting = undefined\n }\n })\n }\n}\n\nexport class PendingState {\n private _isPending = false\n get isPending() {\n return this._isPending\n }\n set isPending(value: boolean) {\n if (this._isPending === value) return\n this._isPending = value\n this.cbs.forEach((cb) => cb(value))\n }\n\n private cbs: Array<(isPending: boolean) => void> = []\n onStateChange(cb: (isPending: boolean) => void) {\n this.cbs.push(cb)\n return () => {\n this.cbs = this.cbs.filter((v) => v !== cb)\n }\n }\n}\n","import fs from 'fs-extra'\nimport * as path from 'path'\n\nexport class File {\n content: Promise<string> | null = null\n\n constructor(readonly path: string, readonly base: string) {}\n\n get relative() {\n return path.posix.relative(this.base, this.path)\n }\n\n get extname() {\n return path.posix.extname(this.path).slice(1)\n }\n\n read() {\n return this.content || (this.content = fs.readFile(this.path, 'utf-8'))\n }\n}\n","import { PendingState } from './utils'\n\nexport class VirtualModuleGraph {\n /**\n * the module inside this graph may be virtual module or real fs module\n */\n private readonly modules: Map<string, Module> = new Map()\n\n /**\n * Serialize the update works (instead of doing them concurrently)\n * to make the result more predictable.\n *\n * If there is already a queuing update with same updaterId,\n * it won't schedule a new one.\n *\n * Before executing an updater, it will automatically cleanup the effects of\n * previous update with same updaterId.\n * Example:\n * When find module1 for the first time:\n * the updater set data for module2 and module3 (upstreamModule is module1)\n * Then, when observe that module1 is updated:\n * the updater set data for module2 (upstreamModule is module1)\n * At this time, the data in module3 should be automatically cleanup!\n * So the updater(users) don't need to manually delete the old data in module3.\n */\n private updateQueue = new UpdateQueue()\n /** track updateQueue empty state (isPending means not empty) */\n public updateQueueEmptyState = new PendingState()\n\n public getModuleIds(filter?: (moduleId: string) => boolean): string[] {\n const ids = Array.from(this.modules.keys())\n if (filter) return ids.filter(filter)\n return ids\n }\n\n public getModuleData(moduleId: string): any[] {\n const module = this.modules.get(moduleId)\n if (!module) return []\n return module.getData()\n }\n\n public getModules(filter?: (moduleId: string) => boolean) {\n let entries = Array.from(this.modules.entries())\n // filter is a performance optimization:\n // don't call module.getData() for filtered-out modules\n if (filter) entries = entries.filter(([moduleId]) => filter(moduleId))\n const modules: { [id: string]: any[] } = {}\n entries.forEach(([moduleId, module]) => {\n modules[moduleId] = module.getData()\n })\n return modules\n }\n\n /**\n * This is the only way to update virtual modules\n */\n public scheduleUpdate(updaterId: string, updater: Update['updater']) {\n this.updateQueue.push(updaterId, updater)\n this.updateQueueEmptyState.isPending = true\n // don't schedule setTimeout if there is already one\n if (this.updateQueue.size === 1) {\n setTimeout(() => {\n this.executeUpdates()\n }, 0)\n }\n }\n\n public addModuleListener(\n handler: ModuleListener,\n filter?: (moduleId: string) => boolean\n ) {\n return this._addModuleListener((moduleId, data, prevData) => {\n if (filter && !filter(moduleId)) return\n handler(moduleId, data, prevData)\n })\n }\n /**\n * listen to virtual module updates.\n * users can scheduleUpdate in these listeners, creating dependency chain of\n * virtual modules.\n * (.i.e when a virtual module changes, it will update another virtual module)\n *\n * users will retrieve new module data and previous module data,\n * so users can diff them to decide whether the module has \"really\" changed.\n * if users think they are the same, they can skip updating other virtual modules.\n * VirtualModuleGraph works on a very low level. It don't know what module data means. So it send updates event to users very often and let users to interpret module data (and whether the data has \"really\" changed).\n *\n * @return unsubscribe function\n */\n private _addModuleListener(cb: ModuleListener) {\n this.moduleUpdateListeners.push(cb)\n return () => {\n const index = this.moduleUpdateListeners.indexOf(cb)\n if (index === -1) return\n this.moduleUpdateListeners.splice(index, 1)\n }\n }\n private moduleUpdateListeners: ModuleListener[] = []\n private callModuleUpdateListeners(\n updatedModules: Map<Module, { prevData: any[] }>\n ) {\n updatedModules.forEach(({ prevData }, module) => {\n const data = module.getData()\n this.moduleUpdateListeners.forEach((moduleUpdateListener) => {\n moduleUpdateListener(module.id, data, prevData)\n })\n })\n }\n\n // executeUpdates_Inner is not reentrant\n // use a state(lock) to prevent concurrent execution\n public updateExecutingState = new PendingState()\n private async executeUpdates() {\n if (this.updateExecutingState.isPending) return\n this.updateExecutingState.isPending = true\n try {\n await this.executeUpdates_Inner()\n } finally {\n this.updateExecutingState.isPending = false\n if (this.updateQueue.size === 0)\n this.updateQueueEmptyState.isPending = false\n }\n }\n private async executeUpdates_Inner(depth = 1) {\n if (this.updateQueue.size === 0) return\n if (depth > MAX_CASCADE_UPDATE_DEPTH)\n throw new Error(\n `Cascaded updates exceed max depth ${MAX_CASCADE_UPDATE_DEPTH}. Probably because the depth of the virtual module tree is too high, or there is a cycle in the virtual module graph.`\n )\n\n // record the updatedModules so that we can notify listeners in the end\n // also store prevData so users can diff it with new data\n const updatedModules = new Map<Module, { prevData: any[] }>()\n /** it must be called before updating data so that it can record prevData */\n const recordAffectedModule = (module: Module) => {\n if (updatedModules.has(module)) return\n updatedModules.set(module, { prevData: module.getData() })\n }\n\n while (true) {\n const update = this.updateQueue.pop()\n if (!update) break\n // cleanup the effects of previous update with same updaterId\n cleanupEdgesWithUpdaterId(update.updaterId, recordAffectedModule)\n const { disableAPIs, ...apis } = this.createUpdateAPIs(\n update.updaterId,\n recordAffectedModule\n )\n await update.updater(apis)\n disableAPIs()\n }\n this.callModuleUpdateListeners(updatedModules)\n // if the listeners schedule more updates,\n // execute them synchronously and recursively\n await this.executeUpdates_Inner(depth + 1)\n }\n\n private createUpdateAPIs(\n updaterId: string,\n recordAffectedModule: (module: Module) => void\n ): VirtualModuleAPIs & { disableAPIs(): void } {\n let outdated = false\n const _this = this\n const OUTDATED_ERROR_MSG = `You should not call update APIs after the updater async function.`\n return {\n addModuleData(moduleId: string, data: any, upstreamModuleId: string) {\n if (outdated) throw new Error(OUTDATED_ERROR_MSG)\n if (moduleId === upstreamModuleId)\n throw new Error(\n `addModuleData param error: source and target modules are the same`\n )\n // upstreamModuleId may be real file in fs\n const fromModule = _this.ensureModule(upstreamModuleId)\n const toModule = _this.ensureModule(moduleId)\n recordAffectedModule(toModule)\n Edge.addEdge(fromModule, toModule, data, updaterId)\n },\n getModuleData(moduleId: string) {\n if (outdated) throw new Error(OUTDATED_ERROR_MSG)\n return _this.getModuleData(moduleId)\n },\n deleteModule(moduleId: string) {\n if (outdated) throw new Error(OUTDATED_ERROR_MSG)\n const module = _this.modules.get(moduleId)\n if (!module) return\n module.delete(recordAffectedModule)\n _this.modules.delete(moduleId)\n },\n disableAPIs() {\n outdated = true\n },\n }\n }\n\n private ensureModule(moduleId: string): Module {\n let result = this.modules.get(moduleId)\n if (!result) {\n result = new Module(moduleId)\n this.modules.set(moduleId, result)\n }\n return result\n }\n}\n\nexport type ModuleListener = (\n moduleId: string,\n data: any[],\n prevData: any[]\n) => void\n\n/**\n * Modules are nodes in the graph\n */\nclass Module {\n constructor(public id: string) {}\n\n public getData() {\n return Array.from(this.data).map(({ data }) => data)\n }\n\n /** unlink this module */\n public delete(recordAffectedModule: (module: Module) => void) {\n if (this.data.size > 0) {\n // there are upstream modules that are \"piping\" data to this module\n throw new Error(\n `This module has upstream modules. You should delete modules in topological order. moduleID: ${this.id}`\n )\n }\n recordAffectedModule(this)\n this.downstream.forEach((edge) => {\n recordAffectedModule(edge.to)\n edge.unlink()\n })\n }\n\n /**\n * incoming edges of the node\n * indicating the data of this virtual module\n *\n * real fs module won't need this\n */\n private data: Set<Edge> = new Set()\n\n /**\n * outcoming edges of the node\n * indicating which modules depend on this module\n *\n * it is needed because we need to update downstream modules\n * when a fs module is deleted\n */\n private downstream: Set<Edge> = new Set()\n}\ninterface ModuleInternal {\n data: Set<Edge>\n downstream: Set<Edge>\n}\n\nclass Edge {\n static addEdge(from: Module, to: Module, data: any, updaterId: string) {\n const edge = new Edge(from, to, data, updaterId)\n // set private fields of modules\n ;(from as unknown as ModuleInternal).downstream.add(edge)\n ;(to as unknown as ModuleInternal).data.add(edge)\n bindEdgeWithUpdaterId(edge)\n }\n\n private constructor(\n public from: Module,\n public to: Module,\n public data: any,\n public updaterId: string\n ) {}\n\n private hasUnlinked = false\n public unlink() {\n if (this.hasUnlinked) {\n return\n }\n // set private fields of modules\n ;(this.from as unknown as ModuleInternal).downstream.delete(this)\n ;(this.to as unknown as ModuleInternal).data.delete(this)\n unbindEdgeWithUpdaterId(this)\n this.hasUnlinked = true\n }\n}\n\nconst mapUpdaterIdToEdges = new Map<string, Set<Edge>>()\nfunction bindEdgeWithUpdaterId(edge: Edge) {\n const { updaterId } = edge\n let edges = mapUpdaterIdToEdges.get(updaterId)\n if (!edges) {\n edges = new Set()\n mapUpdaterIdToEdges.set(updaterId, edges)\n }\n edges.add(edge)\n}\nfunction unbindEdgeWithUpdaterId(edge: Edge) {\n const { updaterId } = edge\n const edges = mapUpdaterIdToEdges.get(updaterId)\n if (!edges || !edges.has(edge))\n throw new Error(`assertion fail: unlinkEdgeWithUpdaterId`)\n edges.delete(edge)\n}\nfunction cleanupEdgesWithUpdaterId(\n updaterId: string,\n recordAffectedModule: (module: Module) => void\n) {\n const edges = mapUpdaterIdToEdges.get(updaterId)\n if (!edges) return\n edges.forEach((edge) => {\n recordAffectedModule(edge.to)\n edge.unlink()\n })\n if (edges.size > 0)\n throw new Error(\n `assertion fail: all edges with updaterId should already be unlinked`\n )\n edges.clear()\n}\n\nexport interface VirtualModuleAPIs {\n addModuleData(moduleId: string, data: any, upstreamModuleId: string): void\n getModuleData(moduleId: string): any[]\n deleteModule(moduleId: string): void\n}\n\nclass Update {\n constructor(\n public updaterId: string,\n public updater: (apis: VirtualModuleAPIs) => void | Promise<void>\n ) {}\n}\n\nclass UpdateQueue {\n private queue: Update[] = []\n private map = new Map<string, Update>()\n get size() {\n return this.queue.length\n }\n push(updaterId: string, updater: Update['updater']) {\n // ignore it if the updaterId already exists in the queue\n if (this.map.has(updaterId)) return\n const update = new Update(updaterId, updater)\n this.queue.push(update)\n this.map.set(updaterId, update)\n }\n pop() {\n const update = this.queue.shift()\n if (!update) return null\n const { updaterId } = update\n this.map.delete(updaterId)\n return update\n }\n}\n\n// it indicates the depth of virtual modules\nconst MAX_CASCADE_UPDATE_DEPTH = 10\n","import * as path from 'path'\nimport chokidar, { FSWatcher } from 'chokidar'\nimport slash from 'slash'\n\nimport {\n ModuleListener,\n VirtualModuleAPIs,\n VirtualModuleGraph,\n} from './VirtualModules'\nimport { PendingTaskCounter } from './utils'\nimport { File } from './utils'\n\nlet nextWatcherId = 0\n\n/**\n * watch fs and update corresponding virtual module when a file changed\n */\nexport class VirtualModulesManager {\n private watchers = new Set<FSWatcher>()\n private virtualModules = new VirtualModuleGraph()\n private fileCache: { [path: string]: File } = {}\n /**\n * don't return half-finished data when there are pending tasks\n */\n private pendingTaskCounter = new PendingTaskCounter()\n\n constructor() {\n this.pendingTaskCounter.countPendingState(\n this.virtualModules.updateExecutingState\n )\n this.pendingTaskCounter.countPendingState(\n this.virtualModules.updateQueueEmptyState\n )\n }\n\n public addFSWatcher(\n baseDir: string,\n globs: string[],\n fileHandler: FileHandler\n ) {\n const watcherId = String(nextWatcherId++)\n\n // should wait for a complete fs scan\n // before returning the page data\n const fsScanFinish = this.pendingTaskCounter.countTask()\n\n this.watchers.add(\n chokidar\n .watch(globs, {\n cwd: baseDir,\n ignored: ['**/node_modules/**/*', '**/.git/**'],\n })\n .on('add', this.handleFileChange(baseDir, fileHandler, watcherId))\n .on('change', this.handleFileChange(baseDir, fileHandler, watcherId))\n .on('unlink', this.handleFileUnLink(baseDir, watcherId))\n .on('ready', () => {\n setTimeout(() => {\n // ready event may be fired too early,\n // before initial scan callbacks are called\n // https://github.com/paulmillr/chokidar/issues/1011\n fsScanFinish()\n }, 10)\n })\n )\n }\n\n public getModules(\n cb: (modules: { [id: string]: any[] }) => void,\n filter?: (moduleId: string) => boolean\n ) {\n this.callOnceWhenIdle(() => {\n cb(this.virtualModules.getModules(filter))\n })\n }\n\n public getModule(moduleId: string, cb: (moduleData: any[]) => void) {\n this.callOnceWhenIdle(() => {\n cb(this.virtualModules.getModuleData(moduleId))\n })\n }\n\n /**\n * Idle means:\n * fs watcher is ready\n * no update is executing\n * update queue is empty\n */\n public callOnceWhenIdle(cb: () => void) {\n this.pendingTaskCounter.callOnceWhenIdle(cb)\n }\n\n /**\n * return the current state of modules.\n * it doesn't wait for update task to finish\n * so it may see intermediate state.\n * use it carefully.\n */\n public _getModulesNow(filter?: (moduleId: string) => boolean) {\n return this.virtualModules.getModules(filter)\n }\n /**\n * return the current state of module.\n * it doesn't wait for update task to finish\n * so it may see intermediate state.\n * use it carefully.\n */\n public _getModuleDataNow(moduleId: string) {\n return this.virtualModules.getModuleData(moduleId)\n }\n\n public addModuleListener(\n handler: ModuleListener,\n filter?: (moduleId: string) => boolean\n ) {\n return this.virtualModules.addModuleListener(handler, filter)\n }\n\n public close() {\n this.watchers.forEach((w) => w.close())\n }\n\n public scheduleUpdate(\n updaterId: string,\n updater: (apis: VirtualModuleAPIs) => void | Promise<void>\n ): void {\n return this.virtualModules.scheduleUpdate(updaterId, updater)\n }\n\n private handleFileChange(\n baseDir: string,\n fileHandler: FileHandler,\n watcherId: string\n ) {\n return (filePath: string) => {\n filePath = slash(path.join(baseDir, filePath))\n\n const file =\n this.fileCache[filePath] ||\n (this.fileCache[filePath] = new File(filePath, baseDir))\n // update content cache\n file.content = null\n file.read()\n\n this.virtualModules.scheduleUpdate(\n `${watcherId}-${filePath}`,\n async (apis) => {\n const handlerAPI: FileHandlerAPIs = {\n addModuleData(moduleId: string, data: any) {\n apis.addModuleData(moduleId, data, filePath)\n },\n getModuleData: apis.getModuleData,\n }\n await fileHandler(file, handlerAPI)\n }\n )\n }\n }\n\n private handleFileUnLink(baseDir: string, watcherId: string) {\n return (filePath: string) => {\n filePath = slash(path.join(baseDir, filePath))\n\n this.virtualModules.scheduleUpdate(\n `${watcherId}-${filePath}-unlink`,\n async (apis) => {\n // delete the node that represent this fs file in the virtual modules graph\n // also delete all outcome edges\n apis.deleteModule(filePath)\n }\n )\n }\n }\n}\n\ntype FileHandler = (file: File, api: FileHandlerAPIs) => void | Promise<void>\n\nexport interface FileHandlerAPIs {\n addModuleData(moduleId: string, data: any): void\n getModuleData(moduleId: string): any[]\n}\n","import { File, VirtualModulesManager } from '.'\n\n/**\n * built upon VirtualModulesManager.\n * map each sourceFile into a proxyModule, which is a virtual module.\n * when the sourceFile is updated, it will emit update event for the proxyModule.\n */\nexport class ProxyModulesManager {\n private vmm = new VirtualModulesManager()\n private register: Map<string, { loaded: boolean; sourceFilePath: string }> =\n new Map()\n\n constructor(public readonly proxyModulePrefix: string) {\n if (!proxyModulePrefix)\n throw new Error(`invalid proxyModulePrefix: ${proxyModulePrefix}`)\n }\n\n /**\n * register a source file to watch,\n * map its data into a proxy module,\n * return the proxyModuleId\n *\n * to create multiple proxy modules for one sourceFilePath,\n * you can pass in keys to differentiate between them.\n */\n registerProxyModule(\n sourceFilePath: string,\n getProxyModuleData: (sourceFile: File) => any,\n key?: string\n ) {\n const proxyModuleId = this.getProxyModuleId(sourceFilePath, key)\n if (this.register.has(proxyModuleId)) return proxyModuleId\n this.vmm.addFSWatcher('', [sourceFilePath], async (file, api) => {\n const proxyModuleData = await getProxyModuleData(file)\n api.addModuleData(proxyModuleId, proxyModuleData)\n })\n this.register.set(proxyModuleId, { loaded: false, sourceFilePath })\n return proxyModuleId\n }\n\n /**\n * get proxy module data by proxyModuleId\n */\n async getProxyModuleData(proxyModuleId: string) {\n return new Promise<any>((res, rej) => {\n this.vmm.getModule(proxyModuleId, (moduleData) => {\n if (!Array.isArray(moduleData) || moduleData.length === 0)\n return rej(\n new Error(`assertion fail: proxy module is empty: ${proxyModuleId}`)\n )\n if (moduleData.length !== 1)\n return rej(\n new Error(\n `assertion fail: proxy module has multiple data: ${proxyModuleId}`\n )\n )\n res(moduleData[0])\n // set loaded flag after a timeout to avoid some race condition\n // (onProxyModuleUpdate cb is triggered before this load event)\n setTimeout(() => {\n const registerItem = this.register.get(proxyModuleId)\n if (registerItem && !registerItem.loaded) {\n this.register.set(proxyModuleId, { ...registerItem, loaded: true })\n }\n }, 50)\n })\n })\n }\n\n /**\n * emit event when a proxyModule is updated since loaded\n */\n onProxyModuleUpdate(\n cb: (proxyModuleId: string, data: any[], prevData: any[]) => void\n ) {\n this.vmm.addModuleListener((proxyModuleId, data, prevData) => {\n const registerItem = this.register.get(proxyModuleId)\n const notLoaded = registerItem && !registerItem.loaded\n // bail out if this is the first-load event\n if (notLoaded && prevData.length === 0) return\n cb(proxyModuleId, data, prevData)\n })\n }\n\n close() {\n this.vmm.close()\n }\n\n private getProxyModuleId(sourceFilePath: string, key?: string) {\n let prefix = this.proxyModulePrefix\n if (key) prefix += `--${key}--`\n return prefix + sourceFilePath\n }\n\n isProxyModuleId(id: string) {\n return id.startsWith(this.proxyModulePrefix) && this.register.has(id)\n }\n\n getSourceFilePath(id: string) {\n return this.register.get(id)?.sourceFilePath\n }\n}\n","import { EventEmitter } from 'events'\nimport { debounce } from 'mini-debounce'\n\n/**\n * Types of page data updates.\n *\n * add:\n * A new page is added.\n * The page list module will be updated.\n * update:\n * A page is updated.\n * The page list module will be updated if it is static data change\n * The page data module will be updated if it is runtime data change\n * delete:\n * A page is deleted.\n * The page list module will be updated.\n * Buffered update of the deleted page will be canceled.\n */\ntype Update =\n | {\n type: 'add' | 'delete'\n pageId: string\n }\n | {\n type: 'update'\n pageId: string\n dataType: 'runtime' | 'static'\n }\n\nexport type ScheduleUpdate = (update: Update) => void\n\n/**\n * Buffer page data updates.\n * Can flush a batch of updates together\n * and cancel unnecessary updates\n */\nexport class PageUpdateBuffer extends EventEmitter {\n /**\n * which pages should be updated\n */\n private pageUpdateBuffer = new Set<string>()\n\n /**\n * whether the page list should be updated\n */\n private pageListUpdateBuffer = false\n\n private scheduleFlush: () => void\n\n constructor() {\n super()\n this.scheduleFlush = debounce(() => {\n let havePageUpdate = false\n if (this.pageUpdateBuffer.size > 0) {\n havePageUpdate = true\n const updates = [...this.pageUpdateBuffer.values()]\n this.emit('page', updates)\n this.pageUpdateBuffer.clear()\n }\n\n if (this.pageListUpdateBuffer) {\n // if we have just sent a page update,\n // we don't need to trigger page list update.\n // because during the page update hmr, the page list will automatically get updated\n // (because the whole import chain will get re-imported)\n if (!havePageUpdate) this.emit('page-list')\n this.pageListUpdateBuffer = false\n }\n }, 100)\n }\n\n scheduleUpdate(update: Update) {\n switch (update.type) {\n case 'add':\n this.pageListUpdateBuffer = true\n break\n case 'update':\n if (update.dataType === 'static') this.pageListUpdateBuffer = true\n else this.pageUpdateBuffer.add(update.pageId)\n break\n case 'delete':\n this.pageListUpdateBuffer = true\n this.pageUpdateBuffer.delete(update.pageId)\n break\n default:\n throw new Error(`invalid update type ${JSON.stringify(update)}`)\n }\n this.scheduleFlush()\n }\n\n async batchUpdate(exec: (scheduleUpdate: ScheduleUpdate) => Promise<void>) {\n let updates: Update[] | null = []\n const _this = this\n\n try {\n await exec(scheduleUpdate)\n } finally {\n updates.forEach((update) => {\n _this.scheduleUpdate(update)\n })\n updates = null\n this.scheduleFlush()\n }\n\n function scheduleUpdate(update: Update) {\n if (!updates) {\n // the batch lifetime has already expired\n // add it to buffer directly\n _this.scheduleUpdate(update)\n return\n }\n // store it, will flush these updates together later\n updates.push(update)\n }\n }\n}\n","import { dequal } from 'dequal'\n\nimport { PageUpdateBuffer } from './UpdateBuffer'\nimport {\n VirtualModuleAPIs,\n FileHandlerAPIs,\n VirtualModulesManager,\n} from '../utils/virtual-module'\nimport type { FileHandler, PageAPIs, DataPiece } from './types.doc'\n\nconst PAGE_MODULE_PREFIX = '/@vp-page'\nconst ensurePageId = (moduleId: string) =>\n moduleId.startsWith(PAGE_MODULE_PREFIX)\n ? moduleId.slice(PAGE_MODULE_PREFIX.length)\n : moduleId\nconst ensureModuleId = (pageId: string) =>\n pageId.startsWith(PAGE_MODULE_PREFIX) ? pageId : PAGE_MODULE_PREFIX + pageId\n\nconst isPageRelatedModule = (moduleId: string) =>\n moduleId.startsWith(PAGE_MODULE_PREFIX)\n\n/**\n * building upon VirtualModulesManager,\n * PagesDataKeeper recognize and handle page modules.\n */\nexport class PagesDataKeeper extends PageUpdateBuffer {\n /**\n * this.pages is a cache of this.virtualModulesManager.getModules\n * which is updated in batch (may be outdated for a short time)\n */\n private readonly pages: PagesDataInternal = {}\n\n constructor(private readonly virtualModulesManager: VirtualModulesManager) {\n super()\n virtualModulesManager.getModules((modules) => {\n Object.entries(modules).forEach(([moduleId, data]) => {\n this.setPageData(moduleId, data)\n })\n virtualModulesManager.addModuleListener((moduleId, data) => {\n this.setPageData(moduleId, data)\n }, isPageRelatedModule)\n }, isPageRelatedModule)\n }\n\n /** turn PagesDataInternal to PagesData */\n public getPages(): PagesData {\n return Object.fromEntries(\n Object.entries(this.pages).map(([pageId, page]) => [\n pageId,\n transformOnePageDataInternal(page),\n ])\n )\n }\n\n public getPage(pageId: string): OnePageData | null {\n const page = this.pages[pageId]\n if (!page) return null\n return transformOnePageDataInternal(page)\n }\n\n /**\n * when low-level page virtual modules has changed, update this.pages data\n * and notify listeners\n */\n private setPageData(moduleId: string, rawData: any[]) {\n const pageId = ensurePageId(moduleId)\n const oldPageData: OnePageDataInternal | undefined = this.pages[pageId]\n const pageData = this.createPageDataFromRaw(rawData)\n // Page is deleted\n if (!pageData) {\n if (oldPageData) {\n delete this.pages[pageId]\n this.scheduleUpdate({\n type: 'delete',\n pageId,\n })\n }\n return\n }\n // Page is added\n if (!oldPageData) {\n this.pages[pageId] = pageData\n this.scheduleUpdate({\n type: 'add',\n pageId,\n })\n return\n }\n // Page is updated\n this.pages[pageId] = pageData\n if (!dequal(pageData.runtimeData, oldPageData.runtimeData)) {\n this.scheduleUpdate({\n type: 'update',\n dataType: 'runtime',\n pageId,\n })\n }\n if (!dequal(pageData.staticData, oldPageData.staticData)) {\n this.scheduleUpdate({\n type: 'update',\n dataType: 'static',\n pageId,\n })\n }\n }\n\n private createPageDataFromRaw(rawData: any[]): OnePageDataInternal | null {\n const pageData: OnePageDataInternal = {\n runtimeData: {},\n staticData: {},\n }\n const { runtimeData: dataMap, staticData: staticDataMap } = pageData\n rawData.forEach((data: DataPiece) => {\n if (!data) return\n const { dataPath, staticData } = data\n if (!dataPath && !staticData) return\n const key = data.key ?? 'main'\n const priority = data.priority ?? 1\n if (dataPath) {\n if (!dataMap[key] || priority > dataMap[key].priority)\n dataMap[key] = { dataPath, priority }\n }\n if (staticData) {\n if (!staticDataMap[key] || priority > staticDataMap[key].priority)\n staticDataMap[key] = { staticData, priority }\n }\n })\n if (isEmptyPage(pageData)) return null\n return pageData\n\n function isEmptyPage(pageData: OnePageDataInternal) {\n const { runtimeData, staticData } = pageData\n return (\n Object.keys(runtimeData).length === 0 &&\n Object.keys(staticData).length === 0\n )\n }\n }\n\n /**\n * update page virtual modules according to fs files\n */\n public addFSWatcher(\n baseDir: string,\n globs: string[],\n fileHandler: FileHandler\n ) {\n this.virtualModulesManager.addFSWatcher(\n baseDir,\n globs,\n async (file, lowerAPI) => {\n const pageAPIs = this.createPageAPIs(lowerAPI)\n const res = await fileHandler(file, pageAPIs)\n if (res) {\n pageAPIs.addPageData(res)\n }\n }\n )\n }\n\n public createOneTimePageAPIs(updaterAPIs: VirtualModuleAPIs): PageAPIs {\n const handlerAPI: FileHandlerAPIs = {\n addModuleData(moduleId: string, data: any) {\n // if the update has no upstream, use a constant name\n updaterAPIs.addModuleData(moduleId, data, 'VP_ANONYMOUS_MODULE')\n },\n getModuleData: updaterAPIs.getModuleData,\n }\n return this.createPageAPIs(handlerAPI)\n }\n\n /**\n * TODO:\n * getRuntimeData and getStaticData are very inefficient to implement,\n * redesign them in the next version\n */\n private createPageAPIs(lowerAPI: FileHandlerAPIs): PageAPIs {\n const getRuntimeData = (pageId: string) => {\n const moduleId = ensureModuleId(pageId)\n // don't use pages as data source because this is a cache updated in batch.\n // instead, get data by virtualModulesManager._getModuleDataNow\n // which is updated immediately after updating virtual modules\n const getDataObject = () => {\n // reconstruct the data object, which is inefficient\n const rawData = this.virtualModulesManager._getModuleDataNow(moduleId)\n const pageData = this.createPageDataFromRaw(rawData)\n if (!pageData) return {}\n return pageData.runtimeData\n }\n const setData = (key: string, value: any) => {\n lowerAPI.addModuleData(moduleId, {\n key,\n dataPath: value,\n } as DataPiece)\n }\n const getData = (key: string) => {\n const existValue = getDataObject()[key]\n return existValue?.dataPath\n }\n return createProxy({ getDataObject, setData, getData })\n }\n\n const getStaticData = (pageId: string) => {\n const moduleId = ensureModuleId(pageId)\n const getDataObject = () => {\n const rawData = this.virtualModulesManager._getModuleDataNow(moduleId)\n const pageData = this.createPageDataFromRaw(rawData)\n if (!pageData) return {}\n return pageData.staticData\n }\n const setData = (key: string, value: any) => {\n lowerAPI.addModuleData(moduleId, {\n key,\n staticData: value,\n } as DataPiece)\n }\n const getData = (key: string) => {\n const existValue = getDataObject()[key]\n return existValue?.staticData\n }\n return createProxy({ getDataObject, setData, getData })\n }\n\n const addPageData = (dataPiece: DataPiece) => {\n const moduleId = ensureModuleId(dataPiece.pageId)\n lowerAPI.addModuleData(moduleId, dataPiece)\n }\n\n return {\n getRuntimeData,\n getStaticData,\n addPageData,\n }\n\n function createProxy({\n getDataObject,\n setData,\n getData,\n }: {\n getDataObject: () => object\n setData: (key: string, value: any) => void\n getData: (key: string) => any\n }) {\n return new Proxy(\n {} as {\n [key: string]: string\n },\n {\n ...defaultProxyTraps,\n set(target, key: string, value) {\n setData(key, value)\n return true\n },\n get(target, key: string) {\n return getData(key)\n },\n has(target, key) {\n return Reflect.has(getDataObject(), key)\n },\n ownKeys: function (target) {\n return Reflect.ownKeys(getDataObject())\n },\n }\n )\n }\n }\n}\n\nexport interface PagesData {\n /**\n * pageId: The page route path.\n * User can register multiple page data with same pageId,\n * as long as they have different keys.\n * Page data with same pageId will be merged.\n *\n * @example '/posts/hello-world'\n */\n [pageId: string]: OnePageData\n}\n\nexport interface OnePageData {\n data: {\n [key: string]: string\n }\n staticData: {\n [key: string]: any\n }\n}\n\ninterface PagesDataInternal {\n [pageId: string]: OnePageDataInternal\n}\n\ninterface OnePageDataInternal {\n runtimeData: {\n /**\n * The value of runtimeData should be a path to module\n * (to be evaluated at runtime)\n */\n [key: string]: { dataPath: string; priority: number }\n }\n staticData: {\n /**\n * The value of staticData can be any json value\n */\n [key: string]: { staticData: any; priority: number }\n }\n}\n\nconst defaultProxyTraps = Object.fromEntries(\n Object.getOwnPropertyNames(Reflect).map((fnName) => [\n fnName,\n () => {\n throw new Error(`unsupported operation on page data object proxy`)\n },\n ])\n)\n\nfunction transformOnePageDataInternal(page: OnePageDataInternal): OnePageData {\n const runtimeData = Object.fromEntries(\n Object.entries(page.runtimeData).map(([key, { dataPath }]) => [\n key,\n dataPath,\n ])\n )\n const staticData = Object.fromEntries(\n Object.entries(page.staticData).map(([key, { staticData }]) => [\n key,\n staticData,\n ])\n )\n return { data: runtimeData, staticData }\n}\n","import * as path from 'path'\nimport { EventEmitter } from 'events'\nimport slash from 'slash'\n\nimport {\n extractStaticData,\n VirtualModulesManager,\n} from '../utils/virtual-module'\nimport { PagesDataKeeper, PagesData, OnePageData } from './PagesDataKeeper'\nimport type { FindPages, PageHelpers, FileHandler, PageAPIs } from './types.doc'\n\nexport class PageStrategy extends EventEmitter {\n protected pagesDir: string = '/pagesDir_not_initialized'\n private virtualModulesManager: VirtualModulesManager = null as any\n private pagesDataKeeper: PagesDataKeeper = null as any\n private started = false\n\n constructor(private findPages: FindPages) {\n super()\n }\n\n /**\n * start() will be called by the vite buildStart hook,\n * which may be called multiple times.\n * we only execute it once\n */\n public start(pagesDir: string, virtualModulesManager: VirtualModulesManager) {\n if (this.started) return\n this.started = true\n this.pagesDir = pagesDir\n\n this.virtualModulesManager = virtualModulesManager\n this.pagesDataKeeper = new PagesDataKeeper(virtualModulesManager)\n this.pagesDataKeeper.on('page', (updates: string[]) => {\n this.emit('page', updates)\n })\n this.pagesDataKeeper.on('page-list', () => {\n this.emit('page-list')\n })\n\n this.virtualModulesManager.scheduleUpdate(\n 'pages-init',\n async (virtualModuleAPIs) => {\n this.oneTimePageAPIs =\n this.pagesDataKeeper.createOneTimePageAPIs(virtualModuleAPIs)\n const helpers = this.createHelpers(() => {\n throw new Error(\n `No defaultFileHandler found. You should pass fileHandler argument when calling watchFiles`\n )\n })\n await this.findPages(pagesDir, helpers)\n }\n )\n }\n\n // these are one-time api that are only used in \"pages-init\"\n private oneTimePageAPIs: PageAPIs = null as any\n\n public getPages(): Promise<PagesData> {\n if (!this.started) throw new Error(`PageStrategy not started yet`)\n return new Promise((resolve) => {\n this.virtualModulesManager.callOnceWhenIdle(() => {\n resolve(this.pagesDataKeeper.getPages())\n })\n })\n }\n\n public getPage(pageId: string): Promise<OnePageData | null> {\n if (!this.started) throw new Error(`PageStrategy not started yet`)\n return new Promise((resolve) => {\n this.virtualModulesManager.callOnceWhenIdle(() => {\n resolve(this.pagesDataKeeper.getPage(pageId))\n })\n })\n }\n\n /**\n * Custom PageStrategy can use it to create helpers with custom defaultFileHandler\n */\n protected createHelpers(defaultFileHandler: FileHandler): PageHelpers {\n const helpers: PageHelpers = {\n extractStaticData,\n watchFiles,\n ...this.oneTimePageAPIs,\n }\n const _this = this\n return helpers\n\n function watchFiles(\n baseDir: string,\n arg2?: string | string[] | FileHandler,\n arg3?: FileHandler\n ) {\n const { pagesDir, pagesDataKeeper } = _this\n // Strip trailing slash and make absolute\n baseDir = slash(path.resolve(pagesDir, baseDir))\n let globs: string[]\n let fileHandler: FileHandler\n if (typeof arg2 === 'function') {\n globs = ['**/*']\n fileHandler = arg2\n } else {\n globs = Array.isArray(arg2) ? arg2 : [arg2 || '**/*']\n fileHandler = arg3 || defaultFileHandler\n }\n\n pagesDataKeeper.addFSWatcher(baseDir, globs, fileHandler)\n }\n }\n}\n\nexport * from './types.doc'\n","import { PageStrategy } from '..'\nimport { extractStaticData, File } from '../../utils/virtual-module'\nimport type { FileHandler, FindPages } from '../types.doc'\n\nexport class DefaultPageStrategy extends PageStrategy {\n constructor(\n opts: { extraFindPages?: FindPages; fileHandler?: FileHandler } = {}\n ) {\n const { extraFindPages, fileHandler = defaultFileHandler } = opts\n // pass a wrapped findPages function to super class\n super((pagesDir, helpersFromParent) => {\n // we can create our own helpers, providing a default fileHandler\n // and not using helpersFromParent\n const helpers = this.createHelpers(fileHandler)\n helpers.watchFiles(pagesDir, '**/*$.{md,mdx,js,jsx,ts,tsx}')\n if (typeof extraFindPages === 'function') {\n extraFindPages(pagesDir, helpers)\n }\n })\n }\n}\n\n/**\n * The defaultFileHandler return the result to caller,\n * instead of directly setting the pageData object.\n * so that it is more useful to users.\n */\nexport const defaultFileHandler: FileHandler = async (file: File, api) => {\n const pagePublicPath = getPagePublicPath(file.relative)\n\n const staticData = await extractStaticData(file)\n\n if (staticData.sourceType === 'md') {\n api.addPageData({\n pageId: pagePublicPath,\n key: 'outlineInfo',\n dataPath: `${file.path}?outlineInfo`,\n })\n }\n\n return {\n pageId: pagePublicPath,\n dataPath: file.path,\n staticData,\n }\n}\n\n/**\n * turn `sub-path/page2/index.tsx` into `/sub-path/page2`\n */\nexport function getPagePublicPath(relativePageFilePath: string) {\n let pagePublicPath = relativePageFilePath.replace(\n /\\$\\.(md|mdx|js|jsx|ts|tsx)$/,\n ''\n )\n pagePublicPath = pagePublicPath.replace(/index$/, '')\n // remove trailing slash\n pagePublicPath = pagePublicPath.replace(/\\/$/, '')\n // ensure starting slash\n pagePublicPath = pagePublicPath.replace(/^\\//, '')\n pagePublicPath = `/${pagePublicPath}`\n\n // turn [id] into :id\n // so that react-router can recognize it as url params\n pagePublicPath = pagePublicPath.replace(\n /\\[(.*?)\\]/g,\n (_, paramName) => `:${paramName}`\n )\n\n return pagePublicPath\n}\n","import slash from 'slash'\nimport type { PagesData } from './PagesDataKeeper'\n\nexport async function renderPageList(pagesData: PagesData, isBuild: boolean) {\n const addPagesData = Object.entries(pagesData).map(\n ([pageId, { staticData }]) => {\n let subPath = pageId\n if (subPath === '/') {\n // import(\"/@react-pages/pages/\") would make vite confused\n // so we change the sub path\n subPath = '/index__'\n }\n const dataModulePath = `/@react-pages/pages${subPath}`\n let code = `\npages[\"${pageId}\"] = {};\npages[\"${pageId}\"].data = () => import(\"${dataModulePath}\");\npages[\"${pageId}\"].staticData = ${JSON.stringify(cleanStaticData(staticData))};`\n return code\n }\n )\n return `\nconst pages = {};\n${addPagesData.join('\\n')}\nexport default pages;\n`\n}\n\nexport async function renderPageListInSSR(pagesData: PagesData) {\n const addPagesData = Object.entries(pagesData).map(\n ([pageId, { staticData }], index) => {\n let subPath = pageId\n if (subPath === '/') {\n // import(\"/@react-pages/pages/\") would make vite confused\n // so we change the sub path\n subPath = '/index__'\n }\n const code = `\npages[\"${pageId}\"] = {};\nimport page${index} from \"/@react-pages/pages${subPath}\";\npages[\"${pageId}\"] = page${index};`\n return code\n }\n )\n return `\nconst pages = {};\n${addPagesData.join('\\n')}\nexport default pages;\n`\n}\n\nexport function renderOnePageData(onePageData: { [dataKey: string]: string }) {\n const importModule = Object.entries(onePageData).map(\n ([dataKey, path], idx) => `\nimport * as m${idx} from \"${slash(path)}\";\nmodules[\"${dataKey}\"] = m${idx};`\n )\n return `\n const modules = {};\n ${importModule.join('\\n')}\n export default modules;`\n}\n\nexport function renderAllPagesOutlines(pagesData: PagesData) {\n const res = [] as string[]\n Object.entries(pagesData).map(([pageId, { staticData }], index1) => {\n const outlinesForThisPage = [] as any[]\n // check all data pieces (identified by key within a page) of all pages\n Object.entries(staticData).forEach(([key, dataPiece], index2) => {\n if (dataPiece?.sourceType === 'md' && dataPiece.__sourceFilePath) {\n // collect outline info of markdown pages\n const varName = `pageOutline_${index1}_${index2}`\n outlinesForThisPage.push({\n key,\n varName,\n importOutlineInfo: `import * as ${varName} from ${JSON.stringify(\n dataPiece.__sourceFilePath + '?outlineInfo'\n )}`,\n })\n }\n })\n if (outlinesForThisPage.length === 0) return\n res.push(`allPagesOutlines[\"${pageId}\"] = {};`)\n outlinesForThisPage.forEach(({ key, varName, importOutlineInfo }) => {\n res.push(importOutlineInfo)\n res.push(`allPagesOutlines[\"${pageId}\"][\"${key}\"] = ${varName};`)\n })\n })\n return `\nexport const allPagesOutlines = {};\n${res.join('\\n')}\n`\n}\n\n// filter out internal data field in staticData\n// don't leak them into build output assets\nfunction cleanStaticData(staticData: any) {\n if (!staticData || typeof staticData !== 'object') return staticData\n return Object.fromEntries(\n Object.entries(staticData).map(([key, value]: [string, any]) => {\n if (value?.__sourceFilePath)\n return [\n key,\n {\n ...value,\n __sourceFilePath: undefined,\n },\n ]\n return [key, value]\n })\n )\n}\n","import fs from 'fs-extra'\nimport * as path from 'path'\nimport slash from 'slash'\n\nexport async function resolveTheme(pagesDirPath: string) {\n for (let filename of ['_theme.js', '_theme.ts', '_theme.jsx', '_theme.tsx']) {\n filename = path.join(pagesDirPath, filename)\n if (await fs.pathExists(filename)) {\n return slash(filename)\n }\n }\n throw new Error(\"can't find theme inside pagesDir: \" + pagesDirPath)\n}\n","import type { MdxjsEsm, MdxJsxFlowElement } from 'mdast-util-mdx'\n\n/**\n * create mdast node for expression:\n * import * as name from 'from'\n */\nexport function createNameSpaceImportNode({\n name,\n from,\n}: {\n name: string\n from: string\n}): MdxjsEsm {\n return {\n type: 'mdxjsEsm',\n value: '',\n data: {\n estree: {\n type: 'Program',\n sourceType: 'module',\n body: [\n {\n type: 'ImportDeclaration',\n specifiers: [\n {\n type: 'ImportNamespaceSpecifier',\n local: {\n type: 'Identifier',\n name,\n },\n },\n ],\n source: {\n type: 'Literal',\n value: from,\n raw: JSON.stringify(from),\n },\n },\n ],\n },\n },\n }\n}\n\n/**\n * create mdast node for expression:\n * import name from 'from'\n */\nexport function createDefaultImportNode({\n name,\n from,\n}: {\n name: string\n from: string\n}): MdxjsEsm {\n return {\n type: 'mdxjsEsm',\n value: '',\n data: {\n estree: {\n type: 'Program',\n sourceType: 'module',\n body: [\n {\n type: 'ImportDeclaration',\n specifiers: [\n {\n type: 'ImportDefaultSpecifier',\n local: {\n type: 'Identifier',\n name,\n },\n },\n ],\n source: {\n type: 'Literal',\n value: from,\n raw: JSON.stringify(from),\n },\n },\n ],\n },\n },\n }\n}\n\n/**\n * create mdast node for expression:\n * <Component {...props} />\n * checkout the parsed node structure in https://mdxjs.com/playground/\n */\nexport function createJSXWithSpreadPropsNode({\n Component,\n props,\n}: {\n Component: string\n props: string\n}): MdxJsxFlowElement {\n return {\n type: 'mdxJsxFlowElement',\n name: Component,\n data: {\n _mdxExplicitJsx: true,\n } as any,\n children: [],\n attributes: [\n {\n type: 'mdxJsxExpressionAttribute',\n value: '',\n data: {\n estree: {\n type: 'Prog