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
text/typescript
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 }
}