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>

456 lines (432 loc) 15.7 kB
import * as path from 'path' import type { PluggableList, Pluggable } from 'unified' import type { Plugin, IndexHtmlTransformContext, PluginOption, Rollup, } from 'vite' import type { staticSiteGenerationConfig } from './types' import { DefaultPageStrategy, defaultFileHandler, } from './page-strategy/DefaultPageStrategy' import { renderPageList, renderPageListInSSR, renderOnePageData, renderAllPagesOutlines, } from './page-strategy/pageUtils' import { PageStrategy } from './page-strategy' import { resolveTheme } from './virtual-module-plugins/theme' import { DemoModuleManager, DemoMdxPlugin, } from './virtual-module-plugins/demo-modules' import { TsInfoModuleManager, TsInfoMdxPlugin, } from './virtual-module-plugins/ts-info-module' import { injectHTMLTag } from './utils/injectHTMLTag' import { VirtualModulesManager } from './utils/virtual-module' import { FileTextMdxPlugin } from './utils/mdx-plugin-file-text' import { OutlineInfoModuleManager, OUTLINE_INFO_MODULE_ID_PREFIX, } from './virtual-module-plugins/outline-info-module' /** * 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 and users should not use them directly */ const modulePrefix = '/@react-pages/' const pagesModuleId = modulePrefix + 'pages' const themeModuleId = modulePrefix + 'theme' const ssrDataModuleId = modulePrefix + 'ssrData' const allOutlineDataModuleId = modulePrefix + 'allPagesOutlines' const tsInfoQueryReg = /\?tsInfo=(.*)$/ export interface PluginConfig { pagesDir?: string pageStrategy?: PageStrategy useHashRouter?: boolean staticSiteGeneration?: staticSiteGenerationConfig /** user can add/remove remark plugins passed to mdx */ modifyRemarkPlugins?: ModifyNamedUnifiedPlugins /** user can add/remove rehype plugins passed to mdx */ modifyRehypePlugins?: ModifyNamedUnifiedPlugins } export type NamedUnifiedPlugin = { /** use name so that modifier can recognize a plugin */ name: string createPlugin: () => Pluggable | Promise<Pluggable> } export type ModifyNamedUnifiedPlugins = ( original: NamedUnifiedPlugin[] ) => NamedUnifiedPlugin[] function pluginFactory(opts: PluginConfig = {}): 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() const outlineInfoModuleManager = new OutlineInfoModuleManager() return { name: 'vite-plugin-react-pages', enforce: 'pre', config: (config, env) => ({ optimizeDeps: { include: [ 'react', // fix https://github.com/vitejs/vite-plugin-react-pages/issues/132#issuecomment-1536515395 'react/jsx-runtime', 'react-dom', 'react-dom/client', 'react-router-dom', '@mdx-js/react', ], exclude: ['vite-plugin-react-pages'], }, define: { __HASH_ROUTER__: !!useHashRouter, 'process.env.VITE_PAGES_IS_SSR': config.build?.ssr ? JSON.stringify('true') : JSON.stringify('false'), }, build: { rollupOptions: { output: { manualChunks: undefined, // local rollup's types may not be compatible with vite's rollup types plugins: [outputPluginDisableJekyll() as any], }, }, }, }), async 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' ) if (mdxPlugin) { throw new Error( 'You should not use vite-plugin-mdx with vite-plugin-react-pages. vite-pages v5 has buildin plugin for mdx.' ) } }, 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) outlineInfoModuleManager.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 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) } if (id.endsWith('?outlineInfo')) { const bareImport = id.slice(0, 0 - '?outlineInfo'.length) const resolved = await this.resolve(bareImport, importer) if (!resolved || resolved.external) throw new Error( `can not resolve outlineInfo: ${id}. importer: ${importer}` ) return outlineInfoModuleManager.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-react-pages/dist/client-bundles/entries/csr.mjs";` // 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 (outlineInfoModuleManager.isProxyModuleId(id)) { return outlineInfoModuleManager.loadProxyModule(id) } if (id === allOutlineDataModuleId) { return renderAllPagesOutlines(await pageStrategy.getPages()) } if (tsInfoModuleManager.isProxyModuleId(id)) { return tsInfoModuleManager.loadProxyModule(id) } }, closeBundle() { virtualModulesManager.close() demoModuleManager.close() tsInfoModuleManager.close() outlineInfoModuleManager.close() }, transformIndexHtml(html, ctx) { return moveScriptTagToBodyEnd(html, ctx) }, // Read by the cli script to get staticSiteGeneration config // @ts-expect-error vitePagesStaticSiteGeneration: staticSiteGeneration, } } export type { Theme, LoadState, PagesLoaded, PagesStaticData, TsInfo, TsPropertyOrMethodInfo, } from '../../clientTypes' export type { FileHandler } from './page-strategy/types.doc' export { extractStaticData, File } from './utils/virtual-module' export { PageStrategy } export { DefaultPageStrategy, defaultFileHandler } /** * 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) } } } export default async function setupPlugins( vpConfig: PluginConfig = {} ): Promise<PluginOption[]> { // use dynamic import so that it supports node commonjs const mdx = await import('@mdx-js/rollup') const mdxPlugin = mdx.default({ remarkPlugins: await getRemarkPlugins(vpConfig.modifyRemarkPlugins), rehypePlugins: await getRehypePlugins(vpConfig.modifyRehypePlugins), // treat .md as mdx mdExtensions: [], mdxExtensions: ['.md', '.mdx'], providerImportSource: '@mdx-js/react', }) return [ { ...mdxPlugin, enforce: 'pre', }, createMdxTransformPlugin(), pluginFactory(vpConfig), ] } function getRemarkPlugins( modifyPlugins?: ModifyNamedUnifiedPlugins ): Promise<PluggableList> { const originalPlugins: NamedUnifiedPlugin[] = [ { name: 'remark-frontmatter', // use dynamic import so that it works in node commonjs // use lazy-eval function so that we don't import/create a plugin until we actually need it // (it may be removed by modifyPlugins so we can avoid calling it) createPlugin: () => import('remark-frontmatter').then((m) => m.default), }, { name: 'remark-gfm', createPlugin: () => import('remark-gfm').then((m) => m.default), }, { name: 'remark-mdx-images', createPlugin: () => import('remark-mdx-images').then((m) => m.default), }, { name: 'DemoMdxPlugin', createPlugin: () => DemoMdxPlugin, }, { name: 'TsInfoMdxPlugin', createPlugin: () => TsInfoMdxPlugin, }, { name: 'FileTextMdxPlugin', createPlugin: () => FileTextMdxPlugin, }, ] return createFinalPlugins(originalPlugins, modifyPlugins) } function getRehypePlugins( modifyPlugins?: ModifyNamedUnifiedPlugins ): Promise<PluggableList> { const originalPlugins: NamedUnifiedPlugin[] = [ { name: 'rehype-slug', // use dynamic import so that it works in node commonjs // use lazy-eval function so that we don't import/create a plugin until we actually need it // (it may be removed by modifyPlugins so we can avoid calling it) createPlugin: () => import('rehype-slug').then((m) => m.default), }, ] return createFinalPlugins(originalPlugins, modifyPlugins) } function createFinalPlugins( originalPlugins: NamedUnifiedPlugin[], modifyPlugins: ModifyNamedUnifiedPlugins | undefined ) { const finalPlugins = (() => { if (typeof modifyPlugins === 'function') { const res = modifyPlugins(originalPlugins) if (Array.isArray(res)) return res } return originalPlugins })() return Promise.all(finalPlugins.map(({ createPlugin }) => createPlugin())) } /** * use @vitejs/plugin-react to transform the output of @mdx-js/rollup, * adding react-refresh hmr ability to .md and .mdx files * workaround this issue: https://github.com/vitejs/vite-plugin-react/issues/38 */ function createMdxTransformPlugin(): Plugin { let vitePluginReactTrasnform: Plugin['transform'] | undefined const PLUGIN_NAME = 'vite-pages:mdx-fast-refresh' return { name: PLUGIN_NAME, apply: 'serve', configResolved: ({ plugins }) => { // find this plugin to call it's transform function: // https://github.com/vitejs/vite-plugin-react/blob/b647e74c38565696bd6fb931b8bd9ac7f3bebe88/packages/plugin-react/src/index.ts#L206 // or https://github.com/vitejs/vite-plugin-react-swc/blob/95e991914322e7b011d1c8d18d501b9eee21adaa/src/index.ts#L111 vitePluginReactTrasnform = plugins.find( (p) => (p.name === 'vite:react-babel' && typeof p.transform === 'function') || (p.name === 'vite:react-swc' && typeof p.transform === 'function') )?.transform if (!vitePluginReactTrasnform) { throw new Error( `Can't find an instance of @vitejs/plugin-react or @vitejs/plugin-react-swc. You should apply either of these plugins to make mdx work.` ) } const reactSwcPluginIndex = plugins.findIndex( (p) => p.name === 'vite:react-swc' && typeof p.transform === 'function' ) const thisPluginIndex = plugins.findIndex((p) => p.name === PLUGIN_NAME) if ( reactSwcPluginIndex !== -1 && thisPluginIndex !== -1 && reactSwcPluginIndex > thisPluginIndex ) { throw new Error( '[vite-plugin-react-pages]: @vitejs/plugin-react-swc should be placed before this plugin' ) } }, transform: (code, id, options) => { const [filepath, ...qs] = id.split('?') if ( filepath.match(/\.mdx?$/) && !id.startsWith(OUTLINE_INFO_MODULE_ID_PREFIX) ) { const refreshContentRE = /\$Refresh(?:Reg|Sig)\$\(/ if (code.includes('/@react-refresh') && refreshContentRE.test(code)) { // the mdx output has already been transformed by @vitejs/plugin-react-swc // https://github.com/vitejs/vite-plugin-react-swc/blob/95e991914322e7b011d1c8d18d501b9eee21adaa/src/index.ts#L199C3-L199C3 // don't transform it again return } // turn file path like `/path/to/md-file$.md` into `/path/to/md-file$.jsx` // make vite-plugin-react transform "the output of @mdx-js/rollup" like a jsx file // https://github.com/vitejs/vite-plugin-react/blob/caa9b5330092c70288fcb94ceb96ca42438df2a2/packages/plugin-react/src/index.ts#L170 const newFilePath = filepath.replace(/\.mdx?$/, '.jsx') const newId = [newFilePath, ...qs].join('?') return (vitePluginReactTrasnform as any)(code, newId, options) } }, } } /** * Some chunk filenames may start with `_`, which will be treated as special resource by github pages. So we need to disable jekyll of github pages. * https://github.blog/2009-12-29-bypassing-jekyll-on-github-pages/ */ function outputPluginDisableJekyll(): Rollup.OutputPlugin { return { name: 'vite-pages-disable-jekyll', generateBundle() { this.emitFile({ type: 'asset', fileName: '.nojekyll', source: '', }) }, } }