diff --git a/docs/guide/api-environment-frameworks.md b/docs/guide/api-environment-frameworks.md index 96eeece23a7582..74800306fb842f 100644 --- a/docs/guide/api-environment-frameworks.md +++ b/docs/guide/api-environment-frameworks.md @@ -329,6 +329,8 @@ export default { } ``` +Plugins can also define a `buildApp` hook. Order `'pre'` and `null'` are executed before the configured `builder.buildApp`, and order `'post'` hooks are executed after it. `environment.isBuilt` can be used to check if an environment has already being build. + ## Environment Agnostic Code Most of the time, the current `environment` instance will be available as part of the context of the code being run so the need to access them through `server.environments` should be rare. For example, inside plugin hooks the environment is exposed as part of the `PluginContext`, so it can be accessed using `this.environment`. See [Environment API for Plugins](./api-environment-plugins.md) to learn about how to build environment aware plugins. diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index fef2dbaa70bbf5..6c1699c1b496e3 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -1494,6 +1494,7 @@ function areSeparateFolders(a: string, b: string) { export class BuildEnvironment extends BaseEnvironment { mode = 'build' as const + isBuilt = false constructor( name: string, config: ResolvedConfig, @@ -1548,12 +1549,6 @@ export interface BuilderOptions { buildApp?: (builder: ViteBuilder) => Promise } -async function defaultBuildApp(builder: ViteBuilder): Promise { - for (const environment of Object.values(builder.environments)) { - await builder.build(environment) - } -} - export const builderOptionsDefaults = Object.freeze({ sharedConfigBuild: false, sharedPlugins: false, @@ -1565,7 +1560,7 @@ export function resolveBuilderOptions( ): ResolvedBuilderOptions | undefined { if (!options) return return mergeWithDefaults( - { ...builderOptionsDefaults, buildApp: defaultBuildApp }, + { ...builderOptionsDefaults, buildApp: async () => {} }, options, ) } @@ -1602,10 +1597,41 @@ export async function createBuilder( environments, config, async buildApp() { - return configBuilder.buildApp(builder) + // order 'pre' and 'normal' hooks are run first, then config.builder.buildApp, then 'post' hooks + let configBuilderBuildAppCalled = false + for (const p of config.getSortedPlugins('buildApp')) { + const hook = p.buildApp + if ( + !configBuilderBuildAppCalled && + typeof hook === 'object' && + hook.order === 'post' + ) { + configBuilderBuildAppCalled = true + await configBuilder.buildApp(builder) + } + const handler = getHookHandler(hook) + await handler(builder) + } + if (!configBuilderBuildAppCalled) { + await configBuilder.buildApp(builder) + } + // fallback to building all environments if no environments have been built + if ( + Object.values(builder.environments).every( + (environment) => !environment.isBuilt, + ) + ) { + for (const environment of Object.values(builder.environments)) { + await builder.build(environment) + } + } }, - async build(environment: BuildEnvironment) { - return buildEnvironment(environment) + async build( + environment: BuildEnvironment, + ): Promise { + const output = await buildEnvironment(environment) + environment.isBuilt = true + return output }, } @@ -1667,3 +1693,5 @@ export async function createBuilder( return builder } + +export type BuildAppHook = (this: void, builder: ViteBuilder) => Promise diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index d549053874bd2d..3c64cffdeb96a8 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -87,6 +87,7 @@ export type { } from './server' export type { ViteBuilder, + BuildAppHook, BuilderOptions, BuildOptions, BuildEnvironmentOptions, diff --git a/packages/vite/src/node/plugin.ts b/packages/vite/src/node/plugin.ts index b1fab8c9f598bf..8c88befd069e81 100644 --- a/packages/vite/src/node/plugin.ts +++ b/packages/vite/src/node/plugin.ts @@ -15,6 +15,7 @@ import type { UserConfig, } from './config' import type { ServerHook } from './server' +import type { BuildAppHook } from './build' import type { IndexHtmlTransform } from './plugins/html' import type { EnvironmentModuleNode } from './server/moduleGraph' import type { ModuleNode } from './server/mixedModuleGraph' @@ -291,7 +292,12 @@ export interface Plugin extends RollupPlugin { * `{ order: 'pre', handler: hook }` */ transformIndexHtml?: IndexHtmlTransform - + /** + * Build Environments + * + * @experimental + */ + buildApp?: ObjectHook /** * Perform custom handling of HMR updates. * The handler receives a context containing changed filename, timestamp, a diff --git a/playground/environment-react-ssr/vite.config.ts b/playground/environment-react-ssr/vite.config.ts index 7518f47bf06dc7..c2aa0734d85c3e 100644 --- a/playground/environment-react-ssr/vite.config.ts +++ b/playground/environment-react-ssr/vite.config.ts @@ -20,6 +20,12 @@ export default defineConfig((env) => ({ Object.assign(globalThis, { __globalServer: server }) }, }, + { + name: 'build-client', + async buildApp(builder) { + await builder.build(builder.environments.client) + }, + }, ], resolve: { noExternal: true, @@ -54,7 +60,9 @@ export default defineConfig((env) => ({ builder: { async buildApp(builder) { - await builder.build(builder.environments.client) + if (!builder.environments.client.isBuilt) { + throw new Error('Client environment should be built first') + } await builder.build(builder.environments.ssr) }, },