Skip to content
Merged
Show file tree
Hide file tree
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
110 changes: 110 additions & 0 deletions packages/plugin-rsc/e2e/build-app.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { test, expect } from '@playwright/test'
import { setupInlineFixture, useFixture } from './fixture'
import { x } from 'tinyexec'
import { waitForHydration } from './helper'

test.describe('buildApp hook', () => {
const root = 'examples/e2e/temp/buildApp'
test.beforeAll(async () => {
await setupInlineFixture({
src: 'examples/starter',
dest: root,
files: {
'vite.config.base.ts': { cp: 'vite.config.ts' },
'vite.config.ts': /* js */ `
import rsc from '@vitejs/plugin-rsc'
import react from '@vitejs/plugin-react'
import { defineConfig, mergeConfig } from 'vite'
import baseConfig from './vite.config.base.ts'

delete baseConfig.plugins

const overrideConfig = defineConfig({
plugins: [
{
name: 'buildApp-prafter',
buildApp: async () => {
console.log('++++ buildApp:before ++++')
},
},
rsc({
useBuildAppHook: process.env.TEST_USE_BUILD_APP_HOOK === 'true',
}),
{
name: 'buildApp-after',
buildApp: async () => {
console.log('++++ buildApp:after ++++')
},
},
react(),
],
})

export default mergeConfig(baseConfig, overrideConfig)
`,
},
})
})

function verifyMatchOrder(s: string, matches: string[]) {
const found = matches
.map((match) => ({ match, index: s.indexOf(match) }))
.filter((item) => item.index !== -1)
.sort((a, b) => a.index - b.index)
.map((item) => item.match)
expect(found).toEqual(matches)
}

test('useBuildAppHook: true', async () => {
const result = await x('pnpm', ['build'], {
nodeOptions: {
cwd: root,
env: {
TEST_USE_BUILD_APP_HOOK: 'true',
},
},
throwOnError: true,
})
verifyMatchOrder(result.stdout, [
'++++ buildApp:before ++++',
'building for production...',
'++++ buildApp:after ++++',
])
expect(result.exitCode).toBe(0)
})

test('useBuildAppHook: false', async () => {
const result = await x('pnpm', ['build'], {
nodeOptions: {
cwd: root,
env: {
TEST_USE_BUILD_APP_HOOK: 'false',
},
},
throwOnError: true,
})
verifyMatchOrder(result.stdout, [
'++++ buildApp:before ++++',
'++++ buildApp:after ++++',
'building for production...',
])
expect(result.exitCode).toBe(0)
})

test.describe('build', () => {
const f = useFixture({
root,
mode: 'build',
cliOptions: {
env: {
TEST_USE_BUILD_APP_HOOK: 'true',
},
},
})

test('basic', async ({ page }) => {
await page.goto(f.url())
await waitForHydration(page)
})
})
})
103 changes: 56 additions & 47 deletions packages/plugin-rsc/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { createRequestListener } from '@mjackson/node-fetch-server'
import * as esModuleLexer from 'es-module-lexer'
import MagicString from 'magic-string'
import {
type BuilderOptions,
type DevEnvironment,
type EnvironmentModuleNode,
type Plugin,
Expand Down Expand Up @@ -123,11 +124,64 @@ export type RscPluginOptions = {
* @default true
*/
validateImports?: boolean

/**
* use `Plugin.buildApp` hook (introduced on Vite 7) instead of `builder.buildApp` configuration
* for better composability with other plugins.
* @default false
*/
useBuildAppHook?: boolean
}

export default function vitePluginRsc(
rscPluginOptions: RscPluginOptions = {},
): Plugin[] {
const buildApp: NonNullable<BuilderOptions['buildApp']> = async (builder) => {
// no-ssr case
// rsc -> client -> rsc -> client
if (!builder.environments.ssr?.config.build.rollupOptions.input) {
isScanBuild = true
builder.environments.rsc!.config.build.write = false
builder.environments.client!.config.build.write = false
await builder.build(builder.environments.rsc!)
await builder.build(builder.environments.client!)
isScanBuild = false
builder.environments.rsc!.config.build.write = true
builder.environments.client!.config.build.write = true
await builder.build(builder.environments.rsc!)
// sort for stable build
clientReferenceMetaMap = sortObject(clientReferenceMetaMap)
serverResourcesMetaMap = sortObject(serverResourcesMetaMap)
await builder.build(builder.environments.client!)

const assetsManifestCode = `export default ${serializeValueWithRuntime(
buildAssetsManifest,
)}`
const manifestPath = path.join(
builder.environments!.rsc!.config.build!.outDir!,
BUILD_ASSETS_MANIFEST_NAME,
)
fs.writeFileSync(manifestPath, assetsManifestCode)
return
}

// rsc -> ssr -> rsc -> client -> ssr
isScanBuild = true
builder.environments.rsc!.config.build.write = false
builder.environments.ssr!.config.build.write = false
await builder.build(builder.environments.rsc!)
await builder.build(builder.environments.ssr!)
isScanBuild = false
builder.environments.rsc!.config.build.write = true
builder.environments.ssr!.config.build.write = true
await builder.build(builder.environments.rsc!)
// sort for stable build
clientReferenceMetaMap = sortObject(clientReferenceMetaMap)
serverResourcesMetaMap = sortObject(serverResourcesMetaMap)
await builder.build(builder.environments.client!)
await builder.build(builder.environments.ssr!)
}

return [
{
name: 'rsc',
Expand Down Expand Up @@ -233,58 +287,14 @@ export default function vitePluginRsc(
},
},
},
// TODO: use buildApp hook on v7?
builder: {
sharedPlugins: true,
sharedConfigBuild: true,
async buildApp(builder) {
// no-ssr case
// rsc -> client -> rsc -> client
if (!builder.environments.ssr?.config.build.rollupOptions.input) {
isScanBuild = true
builder.environments.rsc!.config.build.write = false
builder.environments.client!.config.build.write = false
await builder.build(builder.environments.rsc!)
await builder.build(builder.environments.client!)
isScanBuild = false
builder.environments.rsc!.config.build.write = true
builder.environments.client!.config.build.write = true
await builder.build(builder.environments.rsc!)
// sort for stable build
clientReferenceMetaMap = sortObject(clientReferenceMetaMap)
serverResourcesMetaMap = sortObject(serverResourcesMetaMap)
await builder.build(builder.environments.client!)

const assetsManifestCode = `export default ${serializeValueWithRuntime(
buildAssetsManifest,
)}`
const manifestPath = path.join(
builder.environments!.rsc!.config.build!.outDir!,
BUILD_ASSETS_MANIFEST_NAME,
)
fs.writeFileSync(manifestPath, assetsManifestCode)
return
}

// rsc -> ssr -> rsc -> client -> ssr
isScanBuild = true
builder.environments.rsc!.config.build.write = false
builder.environments.ssr!.config.build.write = false
await builder.build(builder.environments.rsc!)
await builder.build(builder.environments.ssr!)
isScanBuild = false
builder.environments.rsc!.config.build.write = true
builder.environments.ssr!.config.build.write = true
await builder.build(builder.environments.rsc!)
// sort for stable build
clientReferenceMetaMap = sortObject(clientReferenceMetaMap)
serverResourcesMetaMap = sortObject(serverResourcesMetaMap)
await builder.build(builder.environments.client!)
await builder.build(builder.environments.ssr!)
},
buildApp: rscPluginOptions.useBuildAppHook ? undefined : buildApp,
},
}
},
buildApp: rscPluginOptions.useBuildAppHook ? buildApp : undefined,
configResolved(config_) {
config = config_
},
Expand Down Expand Up @@ -841,7 +851,6 @@ globalThis.AsyncLocalStorage = __viteRscAyncHooks.AsyncLocalStorage;
detectNonOptimizedCjsPlugin(),
]
}

function detectNonOptimizedCjsPlugin(): Plugin {
return {
name: 'rsc:detect-non-optimized-cjs',
Expand Down
Loading