From a54d61d1e8240a7762f0ef188189720873db7458 Mon Sep 17 00:00:00 2001 From: csr632 <632882184@qq.com> Date: Tue, 13 Dec 2022 12:35:06 +0800 Subject: [PATCH] implement a SSR plugin system to decouple vite-pages ssr from antd ssr steps --- packages/react-pages/client.ts | 19 +++++--- packages/react-pages/clientTypes.d.ts | 8 ++++ packages/react-pages/src/client/SSRPlugin.tsx | 34 ++++++++++++++ .../src/client/entries/ssg-server.tsx | 45 ++++++++++++++++--- packages/react-pages/src/client/utils.ts | 5 ++- packages/react-pages/src/node/cli.ts | 7 ++- .../src/node/static-site-generation/index.ts | 9 +++- packages/theme-doc/rollup.config.mjs | 15 +------ packages/theme-doc/src/index.tsx | 2 + packages/theme-doc/src/prepareSSR.tsx | 12 ----- packages/theme-doc/src/useAntdSSRPlugin.tsx | 20 +++++++++ packages/theme-doc/src/utils.ts | 2 +- packages/theme-doc/tsconfig.json | 2 +- 13 files changed, 135 insertions(+), 45 deletions(-) create mode 100644 packages/react-pages/src/client/SSRPlugin.tsx delete mode 100644 packages/theme-doc/src/prepareSSR.tsx create mode 100644 packages/theme-doc/src/useAntdSSRPlugin.tsx diff --git a/packages/react-pages/client.ts b/packages/react-pages/client.ts index 03632e9f..d0f98b45 100644 --- a/packages/react-pages/client.ts +++ b/packages/react-pages/client.ts @@ -1,12 +1,15 @@ -// this module exists so that users or themes can: +// this module exists so that users or themes can import utils from it: // import { useStaticData } from "vite-plugin-react-pages/client" -// This module can be imported by theme, which may be optimized by vite -// so this module must be optimizable too. -// So this module can't import vite-pages core. -// Otherwise vite will try to optimize vite-pages core during dev. +// This module can be imported by theme. So: +// - This module can't import vite-pages core. Otherwise vite will try to optimize vite-pages core during dev (when theme been optimized or built). +// - This module can be duplicated (due to dep optimization, or built step of themes) and been executed multiple times -import type { UseStaticData, UseAllPagesOutlines } from './clientTypes' +import type { + UseStaticData, + UseAllPagesOutlines, + SSRPlugin, +} from './clientTypes' // access globalThis['__vite_pages_use_static_data'] lazily export const useStaticData: UseStaticData = (...params: any[]) => { @@ -22,3 +25,7 @@ export const useAllPagesOutlines: UseAllPagesOutlines = (...params) => { } export type { Theme } from './clientTypes' + +export function useSSRPlugin(ssrPlugin: SSRPlugin) { + ;(globalThis as any)['__vite_pages_useSSRPlugin'](ssrPlugin) +} diff --git a/packages/react-pages/clientTypes.d.ts b/packages/react-pages/clientTypes.d.ts index a928b35e..3f6a542b 100644 --- a/packages/react-pages/clientTypes.d.ts +++ b/packages/react-pages/clientTypes.d.ts @@ -30,3 +30,11 @@ export interface TsInterfacePropertyInfo { } export type UseAllPagesOutlines = (timeout: number) => any + +export type SSRPlugin = { + id: string + prepare: (app: React.ReactNode) => { + app?: React.ReactNode + extractStyle?: () => string + } +} diff --git a/packages/react-pages/src/client/SSRPlugin.tsx b/packages/react-pages/src/client/SSRPlugin.tsx new file mode 100644 index 00000000..9d69dc62 --- /dev/null +++ b/packages/react-pages/src/client/SSRPlugin.tsx @@ -0,0 +1,34 @@ +import React, { createContext, useContext } from 'react' +import ReactDOM from 'react-dom/server' + +import { isSSR } from './utils' +import type { SSRPlugin } from '../../clientTypes' +export type { SSRPlugin } from '../../clientTypes' + +export const SSRPluginContext = createContext(new Map()) + +export type SSRPluginMap = Map + +/** + * render React tree on the server to get the server plugins that are declared by themes or users + */ +export function collectSSRPlugins(app: React.ReactNode): SSRPlugin[] { + const map: SSRPluginMap = new Map() + ReactDOM.renderToString( + {app} + ) + return [...map.values()] +} + +/** + * Users or themes use this function to declare SSR plugin. + * This function is exported from vite-plugin-react-pages/client + */ +export function useSSRPlugin(plugin: SSRPlugin) { + if (isSSR) { + const ctxVal = useContext(SSRPluginContext) + ctxVal?.set(plugin.id, plugin) + } +} + +;(globalThis as any)['__vite_pages_useSSRPlugin'] = useSSRPlugin diff --git a/packages/react-pages/src/client/entries/ssg-server.tsx b/packages/react-pages/src/client/entries/ssg-server.tsx index f96c4f42..e328b411 100644 --- a/packages/react-pages/src/client/entries/ssg-server.tsx +++ b/packages/react-pages/src/client/entries/ssg-server.tsx @@ -12,12 +12,48 @@ import ssrData from '/@react-pages/ssrData' import App from '../App' import { dataCacheCtx } from '../ctx' import type { PagesLoaded } from '../../../clientTypes' +import type { SSRPlugin } from '../SSRPlugin' +import { collectSSRPlugins as collect } from '../SSRPlugin' + +export { ssrData } // put all page data in cache, so that we don't need to load it in ssr const dataCache: PagesLoaded = ssrData -export function renderToString(url: string) { - let ssrApp = ( +export function renderToString( + url: string, + { applySSRPlugins }: { applySSRPlugins?: SSRPlugin[] } = {} +) { + let ssrApp: React.ReactNode = + + const extractStyleArr: (() => string)[] = [] + applySSRPlugins?.reverse().forEach((ssrPlugin) => { + const { app, extractStyle } = ssrPlugin.prepare(ssrApp) + if (extractStyle) extractStyleArr.push(extractStyle) + if (app) ssrApp = app + }) + + const contentText = ReactDOM.renderToString(ssrApp) + + const styles = extractStyleArr + .map((extractStyle) => { + return extractStyle() + }) + .filter(Boolean) + const styleText = styles.join('\n') + + return { + contentText, + styleText, + } +} + +export function collectSSRPlugins(url: string): SSRPlugin[] { + return collect() +} + +function SSRApp({ url }: { url: string }) { + return ( ) - const contentText = ReactDOM.renderToString(ssrApp) - const styleText = '' - return { contentText, styleText } } - -export { ssrData } diff --git a/packages/react-pages/src/client/utils.ts b/packages/react-pages/src/client/utils.ts index 329221c7..c7ff6f59 100644 --- a/packages/react-pages/src/client/utils.ts +++ b/packages/react-pages/src/client/utils.ts @@ -1,6 +1,7 @@ import { useLayoutEffect, useEffect } from 'react' +export const isSSR = import.meta.env.SSR + // fix warning of useLayoutEffect during ssr // https://gist.github.com/gaearon/e7d97cdf38a2907924ea12e4ebdf3c85 -export const useIsomorphicLayoutEffect = - typeof window !== 'undefined' ? useLayoutEffect : useEffect +export const useIsomorphicLayoutEffect = isSSR ? useEffect : useLayoutEffect diff --git a/packages/react-pages/src/node/cli.ts b/packages/react-pages/src/node/cli.ts index beeea8e8..a64aeb28 100644 --- a/packages/react-pages/src/node/cli.ts +++ b/packages/react-pages/src/node/cli.ts @@ -26,7 +26,12 @@ if (root) { ;(async () => { if (!command || command === 'ssr') { // user can pass in vite config like --outDir or --configFile - const viteConfig = await resolveConfig(argv, 'build') + const viteConfig = await resolveConfig( + argv, + 'build', + 'production', + 'production' + ) const thisPlugin = viteConfig.plugins.find((plugin) => { return plugin.name === 'vite-plugin-react-pages' }) diff --git a/packages/react-pages/src/node/static-site-generation/index.ts b/packages/react-pages/src/node/static-site-generation/index.ts index 9a530d1d..5dfba82c 100644 --- a/packages/react-pages/src/node/static-site-generation/index.ts +++ b/packages/react-pages/src/node/static-site-generation/index.ts @@ -50,7 +50,7 @@ export async function ssrBuild( console.log('\n\nrendering html...') - const { renderToString, ssrData } = await import( + const { renderToString, collectSSRPlugins, ssrData } = await import( pathToFileURL(path.join(ssrOutDir, 'ssg-server.mjs')).toString() ) @@ -120,6 +120,9 @@ export async function ssrBuild( ) } + // render any page, and collect SSRPlugin from it + const ssrPlugins = collectSSRPlugins('/internal-404-page') + await Promise.all( pagePaths.map(async (pagePath) => { // currently not support pages with path params @@ -165,7 +168,9 @@ export async function ssrBuild( return function renderHTML(pagePath: string) { - const { contentText, styleText } = renderToString(pagePath) + const { contentText, styleText } = renderToString(pagePath, { + applySSRPlugins: ssrPlugins, + }) const ssrInfo = { routePath: pagePath, } diff --git a/packages/theme-doc/rollup.config.mjs b/packages/theme-doc/rollup.config.mjs index bbed80f1..bd326278 100644 --- a/packages/theme-doc/rollup.config.mjs +++ b/packages/theme-doc/rollup.config.mjs @@ -14,11 +14,6 @@ export default { format: 'esm', sourcemap: true, }, - { - dir: 'dist-cjs', - format: 'cjs', - sourcemap: true, - }, ], external: [ 'react', @@ -31,6 +26,7 @@ export default { resolve({ // prevent bundling unexpected deps // resolveOnly: ['antd', /^antd\/.*$/, '@babel/runtime'], + // resolveOnly: ['none!'], extensions, }), commonjs(), @@ -73,14 +69,7 @@ export default { chunk.imports.push('./index.css') chunk.importedBindings['./index.css'] = [] const s = new MagicString(code) - if (options.format === 'cjs') { - if (code.startsWith(`'use strict';`)) { - s.remove(0, `'use strict';`.length) - } - s.prepend(`'use strict';\nrequire('./index.css');\n`) - } else { - s.prepend(`import './index.css';\n`) - } + s.prepend(`import './index.css';\n`) const map = s.generateMap({ hires: true }) return { code: s.toString(), diff --git a/packages/theme-doc/src/index.tsx b/packages/theme-doc/src/index.tsx index fc21d83b..0db1ba04 100644 --- a/packages/theme-doc/src/index.tsx +++ b/packages/theme-doc/src/index.tsx @@ -12,6 +12,7 @@ import AnchorLink from './components/AnchorLink' import type { ThemeConfig, ThemeContextValue } from './ThemeConfig.doc' import { normalizeI18nConfig, useIsomorphicLayoutEffect } from './utils' import { getPageGroups, matchPagePathLocalePrefix } from './analyzeStaticData' +import { useAntdSSRPlugin } from './useAntdSSRPlugin' export function createTheme( originalThemeConfig: ThemeConfig @@ -24,6 +25,7 @@ export function createTheme( } const ThemeComp = (props: ThemeProps) => { + useAntdSSRPlugin() const { loadState, loadedData } = props const staticData = useStaticData() diff --git a/packages/theme-doc/src/prepareSSR.tsx b/packages/theme-doc/src/prepareSSR.tsx deleted file mode 100644 index e4148d18..00000000 --- a/packages/theme-doc/src/prepareSSR.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react' -// https://ant.design/docs/react/customize-theme#server-side-render-ssr -import { createCache, extractStyle, StyleProvider } from '@ant-design/cssinjs' - -// todo: not useful currently -export function prepareSSR(app: React.ReactNode) { - const cache = createCache() - return { - app: {app}, - extractStyle: () => extractStyle(cache) - } -} diff --git a/packages/theme-doc/src/useAntdSSRPlugin.tsx b/packages/theme-doc/src/useAntdSSRPlugin.tsx new file mode 100644 index 00000000..8e88c59e --- /dev/null +++ b/packages/theme-doc/src/useAntdSSRPlugin.tsx @@ -0,0 +1,20 @@ +import React from 'react' +import { useSSRPlugin } from 'vite-plugin-react-pages/client' +// https://ant.design/docs/react/customize-theme#server-side-render-ssr +import { createCache, extractStyle, StyleProvider } from '@ant-design/cssinjs' +import { isSSR } from './utils' + +export function useAntdSSRPlugin() { + if (isSSR) { + useSSRPlugin({ + id: 'vite-pages-theme-doc-antd-ssr', + prepare(app) { + const cache = createCache() + return { + app: {app}, + extractStyle: () => extractStyle(cache), + } + }, + }) + } +} diff --git a/packages/theme-doc/src/utils.ts b/packages/theme-doc/src/utils.ts index 7836e554..58597794 100644 --- a/packages/theme-doc/src/utils.ts +++ b/packages/theme-doc/src/utils.ts @@ -57,7 +57,7 @@ export function commonjsExportsInterop(commonjsExports: T) { export const Anchor_Scroll_Offset = 72 -export const isSSR = typeof window === 'undefined' +export const isSSR = import.meta.env.SSR // fix warning of useLayoutEffect during ssr // https://gist.github.com/gaearon/e7d97cdf38a2907924ea12e4ebdf3c85 diff --git a/packages/theme-doc/tsconfig.json b/packages/theme-doc/tsconfig.json index 02078688..201af24c 100644 --- a/packages/theme-doc/tsconfig.json +++ b/packages/theme-doc/tsconfig.json @@ -2,7 +2,7 @@ "exclude": ["./lib/**/*"], "extends": "../../tsconfig.json", "compilerOptions": { - "module": "CommonJS", + "module": "ESNext", "rootDir": "./src", "outDir": "./lib" }