vite-plugin-react18-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>
267 lines (250 loc) • 8.61 kB
text/typescript
import * as path from 'path'
import fs from 'fs-extra'
import type { Plugin, IndexHtmlTransformContext } from 'vite'
import type { MdxPlugin } from 'vite-plugin-mdx/dist/types'
import {
DefaultPageStrategy,
defaultFileHandler,
} from './page-strategy/DefaultPageStrategy'
import {
renderPageList,
renderPageListInSSR,
renderOnePageData,
} from './page-strategy/pageUtils'
import { PageStrategy } from './page-strategy'
import { resolveTheme } from './virtule-module-plugins/theme'
import {
DemoModuleManager,
DemoMdxPlugin,
} from './virtule-module-plugins/demo-modules'
import {
TsInfoModuleManager,
TsInfoMdxPlugin,
} from './virtule-module-plugins/ts-info-module'
import { injectHTMLTag } from './utils/injectHTMLTag'
import { VirtualModulesManager } from './utils/virtual-module'
import { ImageMdxPlugin } from './utils/mdx-plugin-image'
import { FileTextMdxPlugin } from './utils/mdx-plugin-file-text'
/**
* This is a public API that users use in their index.html.
* Changing this would introduce breaking change for users.
*/
const appEntryId = '/@pages-infra/main.js'
/**
* This is a private prefix an users should not use them
*/
const modulePrefix = '/@react-pages/'
const pagesModuleId = modulePrefix + 'pages'
const themeModuleId = modulePrefix + 'theme'
const ssrDataModuleId = modulePrefix + 'ssrData'
const tsInfoQueryReg = /\?tsInfo=(.*)$/
export default function pluginFactory(
opts: {
pagesDir?: string
pageStrategy?: PageStrategy
useHashRouter?: boolean
staticSiteGeneration?: {}
} = {}
): Plugin {
const { useHashRouter = false, staticSiteGeneration } = opts
let isBuild: boolean
let pagesDir: string
let pageStrategy: PageStrategy
/** used as data source for PageStrategy and other dynamic-modules */
const virtualModulesManager = new VirtualModulesManager()
const demoModuleManager = new DemoModuleManager()
const tsInfoModuleManager = new TsInfoModuleManager()
return {
name: 'vite-plugin-react18-pages',
enforce: 'pre',
config: () => ({
optimizeDeps: {
include: ['react', 'react-dom', 'react-router-dom', '@mdx-js/react'],
exclude: ['vite-plugin-react18-pages'],
},
define: {
__HASH_ROUTER__: !!useHashRouter,
},
build: {
rollupOptions: {
output: {
manualChunks: undefined,
},
},
},
}),
configResolved({ root, plugins, logger, command }) {
isBuild = command === 'build'
pagesDir = opts.pagesDir ?? path.resolve(root, 'pages')
if (opts.pageStrategy) {
pageStrategy = opts.pageStrategy
} else {
pageStrategy = new DefaultPageStrategy()
}
const mdxPlugin = plugins.find(
(plugin) => plugin.name === 'vite-plugin-mdx'
) as MdxPlugin | undefined
if (mdxPlugin?.mdxOptions) {
// Inject demo transformer
mdxPlugin.mdxOptions.remarkPlugins.push(...getRemarkPlugins(root))
} else {
logger.warn(
'[react-pages] Please install vite-plugin-mdx@3.1 or higher'
)
}
},
configureServer({ watcher, moduleGraph }) {
const reloadVirtualModule = (moduleId: string) => {
const module = moduleGraph.getModuleById(moduleId)
if (module) {
moduleGraph.invalidateModule(module)
watcher.emit('change', moduleId)
}
}
pageStrategy
.on('page-list', () => reloadVirtualModule(pagesModuleId))
.on('page', (pageIds: string[]) => {
pageIds.forEach((pageId) => {
reloadVirtualModule(pagesModuleId + pageId)
})
})
demoModuleManager.onUpdate(reloadVirtualModule)
tsInfoModuleManager.onUpdate(reloadVirtualModule)
},
buildStart() {
// buildStart may be called multiple times
// if the port has already been taken and vite retry with another port
// pageStrategy.start can't be put in configResolved
// because vite's resolveConfig will call configResolved without calling close hook
pageStrategy.start(pagesDir, virtualModulesManager)
},
async resolveId(id, importer) {
if (id === appEntryId) return id
if (id.startsWith(modulePrefix)) return id
// TODO
if (id.endsWith('?demo')) {
const bareImport = id.slice(0, 0 - '?demo'.length)
const resolved = await this.resolve(bareImport, importer)
if (!resolved || resolved.external)
throw new Error(`can not resolve demo: ${id}. importer: ${importer}`)
return demoModuleManager.registerProxyModule(resolved.id)
}
const matchTsInfo = id.match(tsInfoQueryReg)
if (matchTsInfo) {
const bareImport = id.replace(tsInfoQueryReg, '')
const resolved = await this.resolve(bareImport, importer)
if (!resolved || resolved.external)
throw new Error(
`can not resolve tsInfo: ${id}. importer: ${importer}`
)
const exportName = matchTsInfo[1]
return tsInfoModuleManager.registerProxyModule(resolved.id, exportName)
}
return undefined
},
async load(id) {
// vite will resolve it with v=${versionHash} query
// so that this import can be cached
if (id === appEntryId)
return `import "vite-plugin-react18-pages/dist/client/client/main.js";`
// page list
if (id === pagesModuleId) {
return renderPageList(await pageStrategy.getPages(), isBuild)
}
// one page data
if (id.startsWith(pagesModuleId + '/')) {
let pageId = id.slice(pagesModuleId.length)
if (pageId === '/index__') pageId = '/'
const page = await pageStrategy.getPage(pageId)
if (!page) {
throw Error(`Page not found: ${pageId}`)
}
return renderOnePageData(page.data)
}
if (id === themeModuleId) {
return `export { default } from "${await resolveTheme(pagesDir)}";`
}
if (id === ssrDataModuleId) {
return renderPageListInSSR(await pageStrategy.getPages())
}
if (demoModuleManager.isProxyModuleId(id)) {
return demoModuleManager.loadProxyModule(id)
}
if (tsInfoModuleManager.isProxyModuleId(id)) {
return tsInfoModuleManager.loadProxyModule(id)
}
},
// @ts-expect-error
vitePagesStaticSiteGeneration: staticSiteGeneration,
closeBundle() {
virtualModulesManager.close()
demoModuleManager.close()
tsInfoModuleManager.close()
},
transformIndexHtml(html, ctx) {
return moveScriptTagToBodyEnd(html, ctx)
},
}
}
export type {
Theme,
LoadState,
PagesLoaded,
PagesStaticData,
TsInterfaceInfo,
TsInterfacePropertyInfo,
} from '../../clientTypes'
export type { FileHandler } from './page-strategy/types.doc'
export { extractStaticData, File } from './utils/virtual-module'
export { PageStrategy }
export { DefaultPageStrategy, defaultFileHandler }
function getRemarkPlugins(root: string) {
const result: any[] = [
DemoMdxPlugin,
TsInfoMdxPlugin,
ImageMdxPlugin,
FileTextMdxPlugin,
]
const pkgJsonPath = path.join(root, 'package.json')
// TODO: user may put the whole vite-pages project
// under a sub folder (which is the root here),
// so the package.json will be located at the upper folder.
// checkout playground/custom-find-pages2.
const hasPkgJson = fs.pathExistsSync(pkgJsonPath)
// Inject frontmatter parser if missing
const { devDependencies = {}, dependencies = {} } = hasPkgJson
? require(pkgJsonPath)
: {}
// By default we add remark-frontmatter automatically.
// But if user install their own remark-frontmatter,
// they are responsible to add the plugin manually
// (they may provide some config to it)
if (
!devDependencies['remark-frontmatter'] &&
!dependencies['remark-frontmatter']
) {
result.push(require('remark-frontmatter'))
}
return result
}
/**
* vite put script before style, which cause style problem for antd
* so we move the script tag to the end of the body
* https://github.com/vitejs/vite/blob/4112c5d103673b83c50d446096086617dfaac5a3/packages/vite/src/node/plugins/html.ts#L352
*/
function moveScriptTagToBodyEnd(
html: string,
ctx: IndexHtmlTransformContext
): string | undefined {
if (ctx.chunk) {
const reg = new RegExp(
`<script\\s[^>]*?${ctx.chunk.fileName}[^<]*?<\\/script>`
)
const match = html.match(reg)
if (match) {
const script = match[0]
html = html.replace(script, '')
return injectHTMLTag(html, script)
}
}
}