Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Separate SSR app render and HTML transform phases with client build in between #267

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 73 additions & 38 deletions src/node/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,18 @@ function readJson(path: string) {
return JSON.parse(fs.readFileSync(path, 'utf8'))
}

function getRouteFileName(route: string, dirStyle: ViteSSGOptions['dirStyle'], out: string): string {
const relativeRouteFile = `${(route.endsWith('/')
? `${route}index`
: route).replace(/^\//g, '')}.html`

const filename = dirStyle === 'nested'
? join(route.replace(/^\//g, ''), 'index.html')
: relativeRouteFile

return join(out, filename)
}

export async function build(cliOptions: Partial<ViteSSGOptions> = {}, viteConfig: InlineConfig = {}) {
const mode = process.env.MODE || process.env.NODE_ENV || cliOptions.mode || 'production'
const config = await resolveConfig(viteConfig, 'build', mode)
Expand Down Expand Up @@ -59,20 +71,6 @@ export async function build(cliOptions: Partial<ViteSSGOptions> = {}, viteConfig
if (fs.existsSync(ssgOut))
await fs.remove(ssgOut)

// client
buildLog('Build for client...')
await viteBuild(mergeConfig(viteConfig, {
build: {
ssrManifest: true,
rollupOptions: {
input: {
app: join(root, './index.html'),
},
},
},
mode: config.mode,
}))

// load jsdom before building the SSR and so jsdom will be available
if (mock) {
// @ts-expect-error just ignore it
Expand Down Expand Up @@ -124,38 +122,81 @@ export async function build(cliOptions: Partial<ViteSSGOptions> = {}, viteConfig
// uniq
routesPaths = Array.from(new Set(routesPaths))

buildLog('Rendering Pages...', routesPaths.length)
buildLog('Rendering SSR app for each route...', routesPaths.length)

const critters = crittersOptions !== false ? await getCritters(outDir, crittersOptions) : undefined
if (critters)
console.log(`${gray('[vite-ssg]')} ${blue('Critical CSS generation enabled via `critters`')}`)

const ssrManifest: Manifest = JSON.parse(await fs.readFile(join(out, 'ssr-manifest.json'), 'utf-8'))
let indexHTML = await fs.readFile(join(out, 'index.html'), 'utf-8')
indexHTML = rewriteScripts(indexHTML, script)

const { renderToString }: typeof import('vue/server-renderer') = await import('vue/server-renderer')

// @ts-expect-error just ignore it hasn't exports on its package
// eslint-disable-next-line new-cap
const queue = new PQueue.default({ concurrency })
const appRenderQueue = new PQueue.default({ concurrency })

// First we want to render the SSR app for all routes.
const appRenderResults: { filename: string; route: string; ctx: SSRContext; appCtx: ViteSSGContext<true> }[] = []

for (const route of routesPaths) {
queue.add(async () => {
appRenderQueue.add(async () => {
try {
const appCtx = await createApp(false, route) as ViteSSGContext<true>
const { app, router, head, initialState, triggerOnSSRAppRendered, transformState = serializeState } = appCtx
const { app, router, triggerOnSSRAppRendered } = appCtx

if (router) {
await router.push(route)
await router.isReady()
}

const transformedIndexHTML = (await onBeforePageRender?.(route, indexHTML, appCtx)) || indexHTML

const ctx: SSRContext = {}
const appHTML = await renderToString(app, ctx)
await triggerOnSSRAppRendered?.(route, appHTML, appCtx)

const filename = getRouteFileName(route, dirStyle, ssgOut)

// Write file to keep memory usage down
await fs.ensureDir(dirname(filename))
await fs.writeFile(filename, appHTML, 'utf-8')

appRenderResults.push({ filename, route, ctx, appCtx })
}
catch (err: any) {
throw new Error(`${gray('[vite-ssg]')} ${red(`Error rendering SSR app: ${cyan(route)}`)}\n${err.stack}`)
}
})
}

await appRenderQueue.start().onIdle()

// Now that the SSR apps have run (which may have created client assets), we can run the client build.
buildLog('Build for client...')
await viteBuild(mergeConfig(viteConfig, {
Copy link
Member

@userquin userquin Jul 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we reset process.env.VITE_SSG and process.env.SSR before running the client build?

EDIT: ppl can use import.meta.env.SSR and import.meta.env.VITE_SSG in their code.

Copy link
Member

@userquin userquin Jul 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about deregistering jsdom?

EDIT: check this line https://github.com/antfu/vite-ssg/blob/main/src/client/index.ts#L24

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it really necessary to reverse the build order?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are to support use cases such as the one I have provided, then yes, reversing the build order is required.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about deregistering jsdom?

EDIT: check this line https://github.com/antfu/vite-ssg/blob/main/src/client/index.ts#L24

how about deregistering jsdom?

EDIT: check this line https://github.com/antfu/vite-ssg/blob/main/src/client/index.ts#L24

I'm not sure I understand the feedback. That line looks like it's checking for the existence of the window object in order to determine whether it should do a client build. Nothing about that would be changed by my PR, as far as I can tell.

build: {
ssrManifest: true,
rollupOptions: {
input: {
app: join(root, './index.html'),
},
},
},
mode: config.mode,
}))

const ssrManifest: Manifest = JSON.parse(await fs.readFile(join(out, 'ssr-manifest.json'), 'utf-8'))
let indexHTML = await fs.readFile(join(out, 'index.html'), 'utf-8')
indexHTML = rewriteScripts(indexHTML, script)
// @ts-expect-error just ignore it hasn't exports on its package
// eslint-disable-next-line new-cap
const htmlTransformQueue = new PQueue.default({ concurrency })

buildLog('Apply HTML transformations...')

htmlTransformQueue.addAll(appRenderResults.map(({ route, filename, appCtx, ctx }) =>
async () => {
try {
const { head, initialState, transformState = serializeState } = appCtx
const appHTML = (await fs.readFile(filename)).toString('utf-8')

const transformedIndexHTML = (await onBeforePageRender?.(route, indexHTML, appCtx)) || indexHTML
// need to resolve assets so render content first
const renderedHTML = await renderHTML({
rootContainerId,
Expand All @@ -180,27 +221,21 @@ export async function build(cliOptions: Partial<ViteSSGOptions> = {}, viteConfig

const formatted = await formatHtml(transformed, formatting)

const relativeRouteFile = `${(route.endsWith('/')
? `${route}index`
: route).replace(/^\//g, '')}.html`

const filename = dirStyle === 'nested'
? join(route.replace(/^\//g, ''), 'index.html')
: relativeRouteFile
const outFilename = getRouteFileName(route, dirStyle, out)
await fs.ensureDir(dirname(outFilename))
await fs.writeFile(outFilename, formatted, 'utf-8')

await fs.ensureDir(join(out, dirname(filename)))
await fs.writeFile(join(out, filename), formatted, 'utf-8')
config.logger.info(
`${dim(`${outDir}/`)}${cyan(filename.padEnd(15, ' '))} ${dim(getSize(formatted))}`,
)
}
catch (err: any) {
throw new Error(`${gray('[vite-ssg]')} ${red(`Error on page: ${cyan(route)}`)}\n${err.stack}`)
throw new Error(`${gray('[vite-ssg]')} ${red(`Error transforming HTML: ${cyan(route)}`)}\n${err.stack}`)
}
})
}
},
))

await queue.start().onIdle()
await htmlTransformQueue.start().onIdle()

await fs.remove(ssgOut)

Expand Down