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) + }) + }) +}) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index df5b70f9b..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, @@ -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 = 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', @@ -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_ }, @@ -841,7 +851,6 @@ globalThis.AsyncLocalStorage = __viteRscAyncHooks.AsyncLocalStorage; detectNonOptimizedCjsPlugin(), ] } - function detectNonOptimizedCjsPlugin(): Plugin { return { name: 'rsc:detect-non-optimized-cjs',