Skip to content

Commit 0cc3119

Browse files
feat!: support static loader
Squashed commit of the following: commit 6986a88 Author: Riri <Daydreamerriri@outlook.com> Date: Thu Jul 25 03:00:00 2024 +0800 chore: update demo commit 7e8fc2a Author: Riri <Daydreamerriri@outlook.com> Date: Thu Jul 25 01:07:38 2024 +0800 chore: release v0.7.0-beta.1 commit b6ff813 Author: Riri <Daydreamerriri@outlook.com> Date: Wed Jul 24 21:04:35 2024 +0800 feat: server loader on dev commit 94ad60a Author: Riri <Daydreamerriri@outlook.com> Date: Tue Jul 23 17:06:08 2024 +0800 feat: static loader data on prod commit cbe7157 Author: Riri <Daydreamerriri@outlook.com> Date: Tue Jul 23 09:24:45 2024 +0800 feat: manifest
1 parent faab397 commit 0cc3119

File tree

16 files changed

+496
-141
lines changed

16 files changed

+496
-141
lines changed

examples/with-loader/src/main.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const routes: RouteRecord[] = [
1010
entry: 'src/pages/index.tsx',
1111
},
1212
{
13+
id: 'doc?s',
1314
path: '/docs/:docs',
1415
lazy: () => import('./pages/[docs]'),
1516
},
@@ -25,4 +26,4 @@ const routesWithLayout = [{
2526
children: routes,
2627
}]
2728

28-
export const createRoot = ViteReactSSG({ routes: routesWithLayout })
29+
export const createRoot = ViteReactSSG({ routes: routesWithLayout, basename: import.meta.env.BASE_URL })

examples/with-loader/src/pages/[docs].tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
1+
/* eslint-disable no-console */
12
import type { Params } from 'react-router-dom'
2-
import { useLoaderData } from 'react-router-dom'
3+
import { Link, useLoaderData, useParams } from 'react-router-dom'
34

45
export default function Docs() {
56
const doc = useLoaderData() as string
7+
const { docs } = useParams()
8+
const anotherPage = docs === 'a' ? 'b' : 'a'
69

710
return (
8-
// eslint-disable-next-line react-dom/no-dangerously-set-innerhtml
9-
<div dangerouslySetInnerHTML={{ __html: doc }} />
11+
<>
12+
{/* eslint-disable-next-line react-dom/no-dangerously-set-innerhtml */}
13+
<div dangerouslySetInnerHTML={{ __html: doc }} />
14+
<Link to={`/docs/${anotherPage}`}>{anotherPage}</Link>
15+
</>
1016
)
1117
}
1218

@@ -15,6 +21,7 @@ export const Component = Docs
1521
export const entry = 'src/pages/[docs].tsx'
1622

1723
export async function loader({ params }: { params: Params<string> }) {
24+
console.log('🚀 ~ loader ~ params:', params)
1825
const doc = await import(`../docs/${params.docs}.md`)
1926
const { renderToString } = await import('react-dom/server')
2027
const html = renderToString(<doc.default />)

examples/with-loader/src/pages/index.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ export default function Index() {
1111
</div>
1212
<h1>Vite + React</h1>
1313
<div className="card">
14-
<Link to="/docs/a">TO A</Link>
15-
{' '}
16-
<Link to="/docs/b">TO B</Link>
14+
<Link style={{ display: 'block' }} to="/docs/a">TO A</Link>
15+
<Link style={{ display: 'block' }} to="/docs/b">TO B</Link>
16+
<Link style={{ display: 'block' }} to="/json">TO JSON</Link>
1717
</div>
1818
</>
1919
)
Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import { useLoaderData } from 'react-router-dom'
22

33
export default function Docs() {
4-
const json = useLoaderData() as typeof import('../docs/test.json')
4+
const data = useLoaderData() as Awaited<ReturnType<typeof loader>>
55

66
return (
7-
<div>{json.key}</div>
7+
<>
8+
<div>{data.key}</div>
9+
<pre>{data.packageJson}</pre>
10+
</>
811
)
912
}
1013

@@ -14,7 +17,13 @@ export const entry = 'src/pages/json.tsx'
1417

1518
export async function loader() {
1619
// If you use `import('../docs/test.json?raw')`, it will return a JSON string.
20+
const fs = (await import('node:fs'))
21+
const cwd = process.cwd()
1722
const json = (await import('../docs/test.json')).default
23+
const packageJson = await fs.promises.readFile(`${cwd}/package.json`, 'utf-8')
1824

19-
return json
25+
return {
26+
...json,
27+
packageJson,
28+
}
2029
}

examples/with-loader/tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
"resolveJsonModule": true,
1212

1313
"types": [
14-
"vite/client"
14+
"vite/client",
15+
"@types/node",
1516
],
1617
"allowImportingTsExtensions": true,
1718
/* Linting */

examples/with-loader/vite.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import mdx from '@mdx-js/rollup'
44

55
// https://vitejs.dev/config/
66
export default defineConfig({
7+
base: '/with-loader/',
78
plugins: [
89
mdx(),
910
react(),

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "vite-react-ssg",
33
"type": "module",
4-
"version": "0.6.3",
4+
"version": "0.7.0-beta.1",
55
"packageManager": "pnpm@9.4.0",
66
"description": "",
77
"author": "Riri <Daydreamerriri@outlook.com>",

src/client/index.tsx

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ import React from 'react'
22
import { createRoot as ReactDOMCreateRoot, hydrateRoot } from 'react-dom/client'
33
import { HelmetProvider } from 'react-helmet-async'
44
import { RouterProvider, createBrowserRouter, matchRoutes } from 'react-router-dom'
5-
import type { RouterOptions, ViteReactSSGClientOptions, ViteReactSSGContext } from '../types'
5+
import type { RouteRecord, RouterOptions, ViteReactSSGClientOptions, ViteReactSSGContext } from '../types'
66
import { documentReady } from '../utils/document-ready'
77
import { deserializeState } from '../utils/state'
8+
import { joinUrlSegments, stripBase, withLeadingSlash } from '../utils/path'
9+
import { convertRoutesToDataRoutes } from '../utils/remix-router'
810

911
export * from '../types'
1012

@@ -28,7 +30,7 @@ export function ViteReactSSG(
2830
const BASE_URL = routerOptions.basename ?? '/'
2931

3032
async function createRoot(client = false, routePath?: string) {
31-
const browserRouter = client ? createBrowserRouter(routerOptions.routes, { basename: BASE_URL }) : undefined
33+
const browserRouter = client ? createBrowserRouter(convertRoutesToDataRoutes(routerOptions.routes, transformStaticLoaderRoute), { basename: BASE_URL }) : undefined
3234

3335
const appRenderCallbacks: Function[] = []
3436
const onSSRAppRendered = client
@@ -122,6 +124,45 @@ export function ViteReactSSG(
122124
}
123125

124126
return createRoot
127+
128+
function transformStaticLoaderRoute(route: RouteRecord) {
129+
const loader: RouteRecord['loader'] = async ({ request }) => {
130+
if (import.meta.env.DEV) {
131+
const routeId = encodeURIComponent(route.id!)
132+
const dataQuery = `_data=${routeId}`
133+
const url = request.url.includes('?') ? `${request.url}&${dataQuery}` : `${request.url}?${dataQuery}`
134+
return fetch(url)
135+
}
136+
else {
137+
let staticLoadData: any
138+
if (window.__VITE_REACT_SSG_STATIC_LOADER_DATA__) {
139+
staticLoadData = window.__VITE_REACT_SSG_STATIC_LOADER_DATA__
140+
}
141+
else {
142+
const manifestUrl = joinUrlSegments(BASE_URL, `static-loader-data-manifest-${window.__VITE_REACT_SSG_HASH__}.json`)
143+
staticLoadData = await (await fetch(withLeadingSlash(manifestUrl))).json()
144+
window.__VITE_REACT_SSG_STATIC_LOADER_DATA__ = staticLoadData
145+
}
146+
147+
const { url } = request
148+
let { pathname } = new URL(url)
149+
if (BASE_URL !== '/') {
150+
pathname = stripBase(pathname, BASE_URL)
151+
}
152+
const routeData = staticLoadData?.[pathname]?.[route.id!]
153+
return routeData ?? null
154+
}
155+
}
156+
route.loader = loader
157+
return route
158+
}
159+
}
160+
161+
declare global {
162+
interface Window {
163+
__VITE_REACT_SSG_STATIC_LOADER_DATA__: any
164+
__VITE_REACT_SSG_HASH__: string
165+
}
125166
}
126167

127168
export { default as Head } from './components/Head'

src/node/build.ts

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,16 @@ import { blue, cyan, dim, gray, green, red, yellow } from 'kolorist'
55
import PQueue from 'p-queue'
66
import fs from 'fs-extra'
77
import type { InlineConfig } from 'vite'
8-
import { mergeConfig, resolveConfig, build as viteBuild, version as viteVersion } from 'vite'
8+
import { createLogger, mergeConfig, resolveConfig, build as viteBuild, version as viteVersion } from 'vite'
99
import type { VitePluginPWAAPI } from 'vite-plugin-pwa'
1010
import { JSDOM } from 'jsdom'
1111
import type { RouteRecord, ViteReactSSGContext, ViteReactSSGOptions } from '../types'
1212
import { serializeState } from '../utils/state'
13-
import { buildLog, createRequest, getSize, removeLeadingSlash, resolveAlias, routesToPaths, withTrailingSlash } from './utils'
13+
import { removeLeadingSlash, withTrailingSlash } from '../utils/path'
14+
import { buildLog, createRequest, getSize, resolveAlias, routesToPaths } from './utils'
1415
import { getCritters } from './critial'
1516
import { render } from './server'
16-
import { detectEntry, renderHTML } from './html'
17+
import { SCRIPT_COMMENT_PLACEHOLDER, detectEntry, renderHTML } from './html'
1718
import { renderPreloadLinks } from './preload-links'
1819

1920
export type SSRManifest = Record<string, string[]>
@@ -27,6 +28,8 @@ export interface ManifestItem {
2728

2829
export type Manifest = Record<string, ManifestItem>
2930

31+
export type StaticLoaderDataManifest = Record<string, Record<string, unknown> | undefined>
32+
3033
export type CreateRootFactory = (client: boolean, routePath?: string) => Promise<ViteReactSSGContext<true> | ViteReactSSGContext<false>>
3134

3235
function DefaultIncludedRoutes(paths: string[], _routes: Readonly<RouteRecord[]>) {
@@ -39,7 +42,8 @@ export async function build(ssgOptions: Partial<ViteReactSSGOptions> = {}, viteC
3942
const config = await resolveConfig(viteConfig, 'build', mode, mode)
4043
const cwd = process.cwd()
4144
const root = config.root || cwd
42-
const ssgOut = join(root, '.vite-react-ssg-temp', Math.random().toString(36).substring(2, 12))
45+
const hash = Math.random().toString(36).substring(2, 12)
46+
const ssgOut = join(root, '.vite-react-ssg-temp', hash)
4347
const outDir = config.build.outDir || 'dist'
4448
const out = isAbsolute(outDir) ? outDir : join(root, outDir)
4549
const configBase = config.base
@@ -64,6 +68,13 @@ export async function build(ssgOptions: Partial<ViteReactSSGOptions> = {}, viteC
6468
if (fs.existsSync(ssgOut))
6569
await fs.remove(ssgOut)
6670

71+
const clientLogger = createLogger()
72+
const loggerWarn = clientLogger.warn
73+
clientLogger.warn = (msg: string, options) => {
74+
if (msg.includes('vite:resolve') && msg.includes('externalized for browser compatibility'))
75+
return
76+
loggerWarn(msg, options)
77+
}
6778
// client
6879
buildLog('Build for client...')
6980
await viteBuild(mergeConfig(viteConfig, {
@@ -82,7 +93,9 @@ export async function build(ssgOptions: Partial<ViteReactSSGOptions> = {}, viteC
8293
},
8394
},
8495
},
96+
customLogger: clientLogger,
8597
mode: config.mode,
98+
ssr: { noExternal: ['vite-react-ssg'] },
8699
}))
87100

88101
if (mock) {
@@ -114,6 +127,7 @@ export async function build(ssgOptions: Partial<ViteReactSSGOptions> = {}, viteC
114127
},
115128
},
116129
mode: config.mode,
130+
ssr: { noExternal: ['vite-react-ssg'] },
117131
}))
118132

119133
const prefix = (format === 'esm' && process.platform === 'win32') ? 'file://' : ''
@@ -153,6 +167,8 @@ export async function build(ssgOptions: Partial<ViteReactSSGOptions> = {}, viteC
153167
const queue = new PQueue({ concurrency })
154168
const crittersQueue = new PQueue({ concurrency: 1 })
155169

170+
const staticLoaderDataManifest: StaticLoaderDataManifest = {}
171+
156172
for (const path of routesPaths) {
157173
queue.add(async () => {
158174
try {
@@ -166,7 +182,8 @@ export async function build(ssgOptions: Partial<ViteReactSSGOptions> = {}, viteC
166182
const fetchUrl = `${withTrailingSlash(base)}${removeLeadingSlash(path)}`
167183
const request = createRequest(fetchUrl)
168184

169-
const { appHTML, bodyAttributes, htmlAttributes, metaAttributes, styleTag } = await render(app ?? [...routes], request, styleCollector, base)
185+
const { appHTML, bodyAttributes, htmlAttributes, metaAttributes, styleTag, routerContext } = await render(app ?? [...routes], request, styleCollector, base)
186+
staticLoaderDataManifest[path] = routerContext?.loaderData
170187

171188
await triggerOnSSRAppRendered?.(path, appHTML, appCtx)
172189

@@ -188,6 +205,7 @@ export async function build(ssgOptions: Partial<ViteReactSSGOptions> = {}, viteC
188205

189206
const html = jsdom.serialize()
190207
let transformed = (await onPageRendered?.(path, html, appCtx)) || html
208+
transformed = transformed.replace(SCRIPT_COMMENT_PLACEHOLDER, `window.__VITE_REACT_SSG_HASH__ = '${hash}'`)
191209
if (critters) {
192210
transformed = (await crittersQueue.add(() => critters.process(transformed)))!
193211
transformed = transformed.replace(/<link\srel="stylesheet"/g, '<link rel="stylesheet" crossorigin')
@@ -220,6 +238,13 @@ export async function build(ssgOptions: Partial<ViteReactSSGOptions> = {}, viteC
220238

221239
await queue.start().onIdle()
222240

241+
buildLog('Generating static loader data manifest...')
242+
const staticLoaderDataManifestString = JSON.stringify(staticLoaderDataManifest, null, 0)
243+
await fs.writeFile(join(out, `static-loader-data-manifest-${hash}.json`), staticLoaderDataManifestString)
244+
config.logger.info(
245+
`${dim(`${outDir}/`)}${cyan(`static-loader-data-manifest-${hash}.json`.padEnd(15, ' '))} ${dim(getSize(staticLoaderDataManifestString))}`,
246+
)
247+
223248
await fs.remove(join(root, '.vite-react-ssg-temp'))
224249

225250
const pwaPlugin: VitePluginPWAAPI = config.plugins.find(i => i.name === 'vite-plugin-pwa')?.api

0 commit comments

Comments
 (0)