Skip to content

Commit 66600db

Browse files
committed
refactor: reorganize the code and export a express server for fasting usage
1 parent 89fed5f commit 66600db

File tree

12 files changed

+357
-160
lines changed

12 files changed

+357
-160
lines changed

bin/vite-ssr.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#!/usr/bin/env node
22
'use strict'
3-
if (typeof __dirname !== 'undefined') require('../dist/node/cli.cjs')
3+
if (typeof __dirname !== 'undefined')
4+
require('../dist/node/cli.cjs')
45
else import('../dist/node/cli.mjs')

build.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export default defineBuildConfig({
44
entries: [
55
{ input: 'src/index', name: 'index' },
66
{ input: 'src/node/cli', name: 'node/cli' },
7+
{ input: 'src/server/express', name: 'express' },
78
],
89
clean: true,
910
declaration: true,
@@ -12,6 +13,7 @@ export default defineBuildConfig({
1213
'vite',
1314
'vue/server-renderer',
1415
'vue/compiler-sfc',
16+
'express-serve-static-core',
1517
],
1618
rollup: {
1719
emitCJS: true,

src/config.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { join as _join, resolve as _resolve } from 'path'
1+
import path, { join as _join, resolve as _resolve } from 'path'
22
import { resolveConfig } from 'vite'
33
import type { ResolvedConfig } from 'vite'
44
import fs from 'fs-extra'
5+
56
import type { ViteSSROptions } from './types'
67

78
let __CONFIG__: ResolvedConfig
@@ -19,6 +20,7 @@ export async function getConfig(mode: string | undefined = process.env.NODE_ENV)
1920
}, __CONFIG__?.ssrOptions || {})
2021

2122
const join = (dir: string) => _join(__CONFIG__.root, dir)
23+
2224
const resolve = (dir: string) => _resolve(__CONFIG__.root, dir)
2325

2426
return {
@@ -47,3 +49,24 @@ export async function getIndexTemplate(mode?: string | undefined) {
4749
const { join, ssrOptions } = await getConfig(mode)
4850
return fs.readFileSync(join(ssrOptions.input as string), 'utf-8')
4951
}
52+
53+
// Get main entry file, `src/main`
54+
export async function getEntryPoint(
55+
ssrOption: ViteSSROptions,
56+
config?: ResolvedConfig,
57+
indexHtml?: string,
58+
) {
59+
if (!config)
60+
config = await resolveViteConfig()
61+
if (!indexHtml)
62+
indexHtml = fs.readFileSync(ssrOption.input || path.resolve(config.root, 'index.html'), 'utf-8')
63+
// <script type="module" src="/src/main.ts"></script>
64+
const matches = indexHtml.match(/<script type="module" src="(.*)"/i)
65+
const entryFile = matches?.[1] || 'src/main'
66+
return path.join(config.root, entryFile)
67+
}
68+
69+
// Get all vite configurations
70+
export function resolveViteConfig(mode?: string) {
71+
return resolveConfig({}, 'build', mode || process.env.MODE || process.env.NODE_ENV)
72+
}

src/entry.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
import type { Component } from 'vue'
22
import { createApp as createClientApp, createSSRApp } from 'vue'
33
import { createMemoryHistory, createRouter, createWebHistory } from 'vue-router'
4-
import { provideContext } from './components'
54
import type { RouterOptions, ViteSSRClientOptions, ViteSSRContext } from './types'
5+
import { provideContext } from './components'
66
import { documentReady } from './utils/document-ready'
77
import { deserializeState } from './utils/state'
8-
import { generateRenderFn } from './node/render'
9-
108
export { ClientOnly, useContext, useFetch } from './components'
119
export * from './types'
1210

@@ -15,11 +13,11 @@ export function ViteSSR(
1513
routerOptions: RouterOptions = { base: '/', routes: [] },
1614
fn?: (context: ViteSSRContext<true>) => Promise<void> | void,
1715
options: ViteSSRClientOptions = {},
18-
) {
16+
): (client: boolean, routePath?: string) => Promise<ViteSSRContext> {
1917
const { transformState, rootContainer = '#app' } = options
2018
const isClient = typeof window !== 'undefined'
2119

22-
// client - true is client side, false is server side
20+
// client - `true` is client side, `false` is server side
2321
async function createApp(client = false, routePath?: string) {
2422
const app = client ? createClientApp(App) : createSSRApp(App)
2523

@@ -74,7 +72,7 @@ export function ViteSSR(
7472

7573
await router.isReady()
7674
context.initialState = router.currentRoute.value.meta.state as Record<string, any> || {}
77-
context.render = generateRenderFn(app, router) as typeof context.render
75+
// context.render = createRender(app, context) as any
7876
}
7977

8078
const initialState = context.initialState

src/node/build.ts

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
/* eslint-disable no-console */
22
import { isAbsolute, join } from 'path'
3+
import type { ResolvedConfig, UserConfig } from 'vite'
34
import { build as viteBuild } from 'vite'
4-
import { getConfig, getEntry } from '../config'
5+
import { getEntryPoint, resolveViteConfig } from '../config'
56
import { buildLog } from './utils'
67

78
export interface CliOptions {
@@ -11,49 +12,41 @@ export interface CliOptions {
1112

1213
export async function build(cliOptions: CliOptions = {}) {
1314
const mode = process.env.MODE || process.env.NODE_ENV || cliOptions.mode || 'production'
14-
const { config, ssrOptions } = await getConfig(mode)
15+
const config: ResolvedConfig = await resolveViteConfig(mode)
1516

1617
const cwd = process.cwd()
1718
const root = config.root || cwd
1819
const outDir = config.build.outDir || 'dist'
1920
const out = isAbsolute(outDir) ? outDir : join(root, outDir)
2021

21-
const { format = 'cjs' } = Object.assign({}, ssrOptions || {}, cliOptions)
22-
23-
// client
2422
buildLog('Build for client...')
2523
await viteBuild({
2624
build: {
2725
ssrManifest: true,
2826
outDir: join(out, 'client'),
2927
rollupOptions: {
3028
input: {
31-
app: join(root, ssrOptions.input as string),
29+
app: join(root, input || 'index.html'),
3230
},
3331
},
3432
},
3533
mode: config.mode,
36-
})
34+
} as UserConfig)
3735

3836
buildLog('Build for server...')
3937
await viteBuild({
4038
build: {
41-
ssr: await getEntry(),
39+
ssr: await getEntryPoint(config.ssrOptions || {}, config),
4240
outDir: join(out, 'server'),
4341
minify: false,
4442
cssCodeSplit: false,
4543
rollupOptions: {
46-
output: format === 'esm'
47-
? {
48-
entryFileNames: '[name].mjs',
49-
format: 'esm',
50-
}
51-
: {
52-
entryFileNames: '[name].cjs',
53-
format: 'cjs',
54-
},
44+
output: {
45+
entryFileNames: '[name].cjs',
46+
format: 'cjs',
47+
},
5548
},
5649
},
5750
mode: config.mode,
58-
})
51+
} as UserConfig)
5952
}

src/node/cli.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { bold, gray, red, reset, underline } from 'kolorist'
33
import yargs from 'yargs'
44
import { hideBin } from 'yargs/helpers'
55
import { bugs } from '../../package.json'
6-
import { start } from './server'
6+
import { startServer } from './server'
77
import { build } from './build'
88

99
yargs(hideBin(process.argv))
@@ -13,17 +13,13 @@ yargs(hideBin(process.argv))
1313
'build',
1414
'Build SSR',
1515
args => args,
16-
async(args) => {
17-
await build(args as any)
18-
},
16+
args => build(args as any),
1917
)
2018
.command(
2119
'*',
2220
'development and production environment',
2321
args => args,
24-
async(args) => {
25-
await start(args)
26-
},
22+
async args => (await startServer)(args),
2723
)
2824
.fail((msg, err, yargs) => {
2925
console.error(`\n${gray('[vite-ssr]')} ${bold(red('An internal error occurred.'))}`)

src/node/render.ts

Lines changed: 118 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,109 @@
1-
import type { App } from 'vue'
2-
import type { Router } from 'vue-router'
1+
import path from 'path'
2+
import fs from 'fs-extra'
33
import { renderToString } from 'vue/server-renderer'
4+
import { serializeState } from '../utils/state'
5+
import type { CreateRenderOptions, RenderOptions, ViteSSROptions } from '../types'
6+
7+
/**
8+
* Create a render function environment to avoid regenerate variables in request.
9+
* it will returns a render function for every request.
10+
*
11+
* @returns a render function
12+
*/
13+
export async function createRender({
14+
isProd = false,
15+
root = process.cwd(),
16+
outDir = 'dist',
17+
context: _context,
18+
viteServer,
19+
ssrOptions = {},
20+
}: CreateRenderOptions) {
21+
const out = path.isAbsolute(outDir) ? outDir : path.join(root, outDir)
22+
const resolve = (dir: string) => path.resolve((isProd ? out : root) as string, dir)
23+
const templatePath = resolve(isProd ? './client/index.html' : 'index.html')
24+
25+
const getTemplate = () => {
26+
return fs.existsSync(templatePath)
27+
? fs.readFileSync(templatePath, 'utf-8')
28+
: getDefaultTemplate(ssrOptions as ViteSSROptions)
29+
}
30+
const _template = getTemplate()
31+
const manifest = isProd
32+
? (await fs.readJSON(resolve('./client/ssr-manifest.json')), 'utf-8')
33+
: {}
34+
35+
// Polyfill window.fetch
36+
if (!globalThis.fetch) {
37+
const fetch = await import('node-fetch')
38+
// @ts-expect-error global variable
39+
globalThis.fetch = fetch.default || fetch
40+
}
41+
42+
const render = async(url: string, opts?: RenderOptions) => {
43+
const context = _context || opts?.context
44+
if (!context)
45+
throw new Error('context is required')
46+
47+
const {
48+
router,
49+
transformState = serializeState,
50+
initialState,
51+
onBeforePageRender,
52+
onPageRender,
53+
app,
54+
} = context
455

5-
export function generateRenderFn(app: App, router: Router) {
6-
return async(url: string, manifest: any) => {
756
await router.push(url)
857
await router.isReady()
958

59+
// Before page render hook
60+
await onBeforePageRender?.(context)
61+
1062
const ctx: any = {}
11-
const html = await renderToString(app, ctx)
12-
const preloadLinks = await renderPreloadLinks(ctx.modules, manifest)
63+
let appHtml = await renderToString(app, ctx)
1364

14-
return { appHtml: html, preloadLinks }
65+
let preloadLinks = await renderPreloadLinks(ctx.modules, manifest)
66+
67+
let template = _template
68+
if (!isProd) {
69+
template = getTemplate()
70+
template = (await viteServer?.transformIndexHtml(url, template)) as string
71+
}
72+
73+
// After page render hook
74+
const pageRenderResult = await onPageRender?.({
75+
route: url,
76+
appCtx: context,
77+
appHtml,
78+
preloadLinks,
79+
})
80+
appHtml = pageRenderResult?.appHtml || appHtml
81+
preloadLinks = pageRenderResult?.preloadLinks || preloadLinks
82+
83+
const state = transformState(initialState)
84+
const stateScript = state
85+
? `\n\t<script>window.__INITIAL_STATE__=${state}</script>`
86+
: ''
87+
88+
const html = template
89+
.replace('</title>', `</title>\n${preloadLinks}`)
90+
.replace(
91+
'<div id="app"></div>',
92+
`<div id="app">${appHtml}</div>${stateScript}`,
93+
)
94+
95+
return html
1596
}
97+
98+
return render
1699
}
17100

18101
export async function renderPreloadLinks(modules: any, manifest: any) {
19102
let links = ''
20103
const seen = new Set()
21104
const { basename } = await import('path')
22-
modules.forEach((id: any) => {
105+
106+
modules && modules.forEach((id: any) => {
23107
const files = manifest[id]
24108
if (files) {
25109
files.forEach((file: any) => {
@@ -67,3 +151,29 @@ function renderPreloadLink(file: string) {
67151
return ''
68152
}
69153
}
154+
155+
function getDefaultTemplate(ssrOptions: ViteSSROptions) {
156+
const { rootContainerId = 'app', entry = 'src/main.ts' } = ssrOptions
157+
const template = `
158+
<!DOCTYPE html>
159+
<html lang="en">
160+
<head>
161+
<meta charset="UTF-8">
162+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
163+
<meta http-equiv="X-UA-Compatible" content="ie=edge">
164+
<title>Vite SSR</title>
165+
<style>
166+
body {
167+
margin: 0;
168+
padding: 0;
169+
}
170+
</style>
171+
</head>
172+
<body>
173+
<div id="${rootContainerId}"></div>
174+
<script type="module" src="${entry}"></script>
175+
</body>
176+
</html>
177+
`
178+
return template
179+
}

0 commit comments

Comments
 (0)