UNPKG

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>

334 lines (310 loc) 9.42 kB
import { dequal } from 'dequal' import { PageUpdateBuffer } from './UpdateBuffer' import { VirtualModuleAPIs, FileHandlerAPIs, VirtualModulesManager, } from '../utils/virtual-module' import type { FileHandler, PageAPIs, DataPiece } from './types.doc' const PAGE_MODULE_PREFIX = '/@vp-page' const ensurePageId = (moduleId: string) => moduleId.startsWith(PAGE_MODULE_PREFIX) ? moduleId.slice(PAGE_MODULE_PREFIX.length) : moduleId const ensureModuleId = (pageId: string) => pageId.startsWith(PAGE_MODULE_PREFIX) ? pageId : PAGE_MODULE_PREFIX + pageId const isPageRelatedModule = (moduleId: string) => moduleId.startsWith(PAGE_MODULE_PREFIX) /** * building upon VirtualModulesManager, * PagesDataKeeper recognize and handle page modules. */ export class PagesDataKeeper extends PageUpdateBuffer { /** * this.pages is a cache of this.virtualModulesManager.getModules * which is updated in batch (may be outdated for a short time) */ private readonly pages: PagesDataInternal = {} constructor(private readonly virtualModulesManager: VirtualModulesManager) { super() virtualModulesManager.getModules((modules) => { Object.entries(modules).forEach(([moduleId, data]) => { this.setPageData(moduleId, data) }) virtualModulesManager.addModuleListener((moduleId, data) => { this.setPageData(moduleId, data) }, isPageRelatedModule) }, isPageRelatedModule) } /** turn PagesDataInternal to PagesData */ public getPages(): PagesData { return Object.fromEntries( Object.entries(this.pages).map(([pageId, page]) => [ pageId, transformOnePageDataInternal(page), ]) ) } public getPage(pageId: string): OnePageData | null { const page = this.pages[pageId] if (!page) return null return transformOnePageDataInternal(page) } /** * when low-level page virtual modules has changed, update this.pages data * and notify listeners */ private setPageData(moduleId: string, rawData: any[]) { const pageId = ensurePageId(moduleId) const oldPageData: OnePageDataInternal | undefined = this.pages[pageId] const pageData = this.createPageDataFromRaw(rawData) // Page is deleted if (!pageData) { if (oldPageData) { delete this.pages[pageId] this.scheduleUpdate({ type: 'delete', pageId, }) } return } // Page is added if (!oldPageData) { this.pages[pageId] = pageData this.scheduleUpdate({ type: 'add', pageId, }) return } // Page is updated this.pages[pageId] = pageData if (!dequal(pageData.runtimeData, oldPageData.runtimeData)) { this.scheduleUpdate({ type: 'update', dataType: 'runtime', pageId, }) } if (!dequal(pageData.staticData, oldPageData.staticData)) { this.scheduleUpdate({ type: 'update', dataType: 'static', pageId, }) } } private createPageDataFromRaw(rawData: any[]): OnePageDataInternal | null { const pageData: OnePageDataInternal = { runtimeData: {}, staticData: {}, } const { runtimeData: dataMap, staticData: staticDataMap } = pageData rawData.forEach((data: DataPiece) => { if (!data) return const { dataPath, staticData } = data if (!dataPath && !staticData) return const key = data.key ?? 'main' const priority = data.priority ?? 1 if (dataPath) { if (!dataMap[key] || priority > dataMap[key].priority) dataMap[key] = { dataPath, priority } } if (staticData) { if (!staticDataMap[key] || priority > staticDataMap[key].priority) staticDataMap[key] = { staticData, priority } } }) if (isEmptyPage(pageData)) return null return pageData function isEmptyPage(pageData: OnePageDataInternal) { const { runtimeData, staticData } = pageData return ( Object.keys(runtimeData).length === 0 && Object.keys(staticData).length === 0 ) } } /** * update page virtual modules according to fs files */ public addFSWatcher( baseDir: string, globs: string[], fileHandler: FileHandler ) { this.virtualModulesManager.addFSWatcher( baseDir, globs, async (file, lowerAPI) => { const pageAPIs = this.createPageAPIs(lowerAPI) const res = await fileHandler(file, pageAPIs) if (res) { pageAPIs.addPageData(res) } } ) } public createOneTimePageAPIs(updaterAPIs: VirtualModuleAPIs): PageAPIs { const handlerAPI: FileHandlerAPIs = { addModuleData(moduleId: string, data: any) { // if the update has no upstream, use a constant name updaterAPIs.addModuleData(moduleId, data, 'VP_ANONYMOUS_MODULE') }, getModuleData: updaterAPIs.getModuleData, } return this.createPageAPIs(handlerAPI) } /** * TODO: * getRuntimeData and getStaticData are very inefficient to implement, * redesign them in the next version */ private createPageAPIs(lowerAPI: FileHandlerAPIs): PageAPIs { const getRuntimeData = (pageId: string) => { const moduleId = ensureModuleId(pageId) // don't use pages as data source because this is a cache updated in batch. // instead, get data by virtualModulesManager._getModuleDataNow // which is updated immediately after updating virtual modules const getDataObject = () => { // reconstruct the data object, which is inefficient const rawData = this.virtualModulesManager._getModuleDataNow(moduleId) const pageData = this.createPageDataFromRaw(rawData) if (!pageData) return {} return pageData.runtimeData } const setData = (key: string, value: any) => { lowerAPI.addModuleData(moduleId, { key, dataPath: value, } as DataPiece) } const getData = (key: string) => { const existValue = getDataObject()[key] return existValue?.dataPath } return createProxy({ getDataObject, setData, getData }) } const getStaticData = (pageId: string) => { const moduleId = ensureModuleId(pageId) const getDataObject = () => { const rawData = this.virtualModulesManager._getModuleDataNow(moduleId) const pageData = this.createPageDataFromRaw(rawData) if (!pageData) return {} return pageData.staticData } const setData = (key: string, value: any) => { lowerAPI.addModuleData(moduleId, { key, staticData: value, } as DataPiece) } const getData = (key: string) => { const existValue = getDataObject()[key] return existValue?.staticData } return createProxy({ getDataObject, setData, getData }) } const addPageData = (dataPiece: DataPiece) => { const moduleId = ensureModuleId(dataPiece.pageId) lowerAPI.addModuleData(moduleId, dataPiece) } return { getRuntimeData, getStaticData, addPageData, } function createProxy({ getDataObject, setData, getData, }: { getDataObject: () => object setData: (key: string, value: any) => void getData: (key: string) => any }) { return new Proxy( {} as { [key: string]: string }, { ...defaultProxyTraps, set(target, key: string, value) { setData(key, value) return true }, get(target, key: string) { return getData(key) }, has(target, key) { return Reflect.has(getDataObject(), key) }, ownKeys: function (target) { return Reflect.ownKeys(getDataObject()) }, } ) } } } export interface PagesData { /** * pageId: The page route path. * User can register multiple page data with same pageId, * as long as they have different keys. * Page data with same pageId will be merged. * * @example '/posts/hello-world' */ [pageId: string]: OnePageData } export interface OnePageData { data: { [key: string]: string } staticData: { [key: string]: any } } interface PagesDataInternal { [pageId: string]: OnePageDataInternal } interface OnePageDataInternal { runtimeData: { /** * The value of runtimeData should be a path to module * (to be evaluated at runtime) */ [key: string]: { dataPath: string; priority: number } } staticData: { /** * The value of staticData can be any json value */ [key: string]: { staticData: any; priority: number } } } const defaultProxyTraps = Object.fromEntries( Object.getOwnPropertyNames(Reflect).map((fnName) => [ fnName, () => { throw new Error(`unsupported operation on page data object proxy`) }, ]) ) function transformOnePageDataInternal(page: OnePageDataInternal): OnePageData { const runtimeData = Object.fromEntries( Object.entries(page.runtimeData).map(([key, { dataPath }]) => [ key, dataPath, ]) ) const staticData = Object.fromEntries( Object.entries(page.staticData).map(([key, { staticData }]) => [ key, staticData, ]) ) return { data: runtimeData, staticData } }