From 44c83aa7135ff95a7c98fe0859fadbc91151cc24 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Wed, 30 Jul 2025 09:09:57 +0900 Subject: [PATCH 1/4] feat(rsc): add `useBuildAppHook` option to switch `buildApp` hook or config --- packages/plugin-rsc/src/plugin.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index df5b70f9b..d8737b3c8 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -123,6 +123,13 @@ export type RscPluginOptions = { * @default true */ validateImports?: boolean + + /** + * use `Plugin.buildApp` hook (introduced on Vite 7) instead of `build.buildApp` configuration + * for better composability with other plugins. + * @default false + */ + useBuildAppHook?: boolean } export default function vitePluginRsc( From 2eee84a0fe22d3584a1b4f33d83e972159d39fbd Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Wed, 30 Jul 2025 09:21:43 +0900 Subject: [PATCH 2/4] feat: implement --- .../examples/starter/vite.config.ts | 13 +++ packages/plugin-rsc/src/plugin.ts | 98 ++++++++++--------- 2 files changed, 63 insertions(+), 48 deletions(-) diff --git a/packages/plugin-rsc/examples/starter/vite.config.ts b/packages/plugin-rsc/examples/starter/vite.config.ts index 99837202c..a7ba28794 100644 --- a/packages/plugin-rsc/examples/starter/vite.config.ts +++ b/packages/plugin-rsc/examples/starter/vite.config.ts @@ -5,7 +5,14 @@ import { defineConfig } from 'vite' export default defineConfig({ plugins: [ + { + name: 'buildApp-pre', + buildApp: async () => { + console.log('🔶 [buildApp:before]') + }, + }, rsc({ + useBuildAppHook: true, // `entries` option is only a shorthand for specifying each `rollupOptions.input` below // > entries: { rsc, ssr, client }, // @@ -13,6 +20,12 @@ export default defineConfig({ // This can be disabled when setting up own server handler e.g. `@cloudflare/vite-plugin`. // > serverHandler: false }), + { + name: 'buildApp-post', + buildApp: async () => { + console.log('🔷 [buildApp:after]') + }, + }, // use any of react plugins https://github.com/vitejs/vite-plugin-react // to enable client component HMR diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index d8737b3c8..a8b946ea2 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -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, @@ -125,7 +126,7 @@ export type RscPluginOptions = { validateImports?: boolean /** - * use `Plugin.buildApp` hook (introduced on Vite 7) instead of `build.buildApp` configuration + * use `Plugin.buildApp` hook (introduced on Vite 7) instead of `builder.buildApp` configuration * for better composability with other plugins. * @default false */ @@ -135,6 +136,52 @@ export type RscPluginOptions = { export default function vitePluginRsc( rscPluginOptions: RscPluginOptions = {}, ): Plugin[] { + const buildApp: NonNullable = 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', @@ -240,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_ }, @@ -848,7 +851,6 @@ globalThis.AsyncLocalStorage = __viteRscAyncHooks.AsyncLocalStorage; detectNonOptimizedCjsPlugin(), ] } - function detectNonOptimizedCjsPlugin(): Plugin { return { name: 'rsc:detect-non-optimized-cjs', From 0bd9f480efb20a935a078394ac111c7f5f5a17b4 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Wed, 30 Jul 2025 09:50:30 +0900 Subject: [PATCH 3/4] test: add e2e --- packages/plugin-rsc/e2e/build-app.test.ts | 110 ++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 packages/plugin-rsc/e2e/build-app.test.ts diff --git a/packages/plugin-rsc/e2e/build-app.test.ts b/packages/plugin-rsc/e2e/build-app.test.ts new file mode 100644 index 000000000..d4b831cf8 --- /dev/null +++ b/packages/plugin-rsc/e2e/build-app.test.ts @@ -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) + }) + }) +}) From 7e3799bc58aed54426023c0f12c80afa57bf6bad Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Wed, 30 Jul 2025 09:50:53 +0900 Subject: [PATCH 4/4] chore: cleanup --- packages/plugin-rsc/examples/starter/vite.config.ts | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/packages/plugin-rsc/examples/starter/vite.config.ts b/packages/plugin-rsc/examples/starter/vite.config.ts index a7ba28794..99837202c 100644 --- a/packages/plugin-rsc/examples/starter/vite.config.ts +++ b/packages/plugin-rsc/examples/starter/vite.config.ts @@ -5,14 +5,7 @@ import { defineConfig } from 'vite' export default defineConfig({ plugins: [ - { - name: 'buildApp-pre', - buildApp: async () => { - console.log('🔶 [buildApp:before]') - }, - }, rsc({ - useBuildAppHook: true, // `entries` option is only a shorthand for specifying each `rollupOptions.input` below // > entries: { rsc, ssr, client }, // @@ -20,12 +13,6 @@ export default defineConfig({ // This can be disabled when setting up own server handler e.g. `@cloudflare/vite-plugin`. // > serverHandler: false }), - { - name: 'buildApp-post', - buildApp: async () => { - console.log('🔷 [buildApp:after]') - }, - }, // use any of react plugins https://github.com/vitejs/vite-plugin-react // to enable client component HMR