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
2 changes: 2 additions & 0 deletions docs/guide/api-environment-frameworks.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
48 changes: 38 additions & 10 deletions packages/vite/src/node/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -1548,12 +1549,6 @@ export interface BuilderOptions {
buildApp?: (builder: ViteBuilder) => Promise<void>
}

async function defaultBuildApp(builder: ViteBuilder): Promise<void> {
for (const environment of Object.values(builder.environments)) {
await builder.build(environment)
}
}
Comment on lines -1551 to -1555
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Instead of dcea0b1, can the default behavior adjusted by checking environment.isBuilt for each environment here?

if (!environment.isBuilt) {
  await builder.build(environment)
}

I think the difference is whether plugin.buildApp partially building environments should cause Vite to build the rest of environments. As a default behavior, this seemed natural to me. plugin.buildApp can still technically avoid that by doing delete builder.environments["noNeedThisEnv"] 🤔

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

We can't do that except if we move the order post hooks before config.builder.buildApp, and we also may deprecate and remove config.builder.buildApp in the future


export const builderOptionsDefaults = Object.freeze({
sharedConfigBuild: false,
sharedPlugins: false,
Expand All @@ -1565,7 +1560,7 @@ export function resolveBuilderOptions(
): ResolvedBuilderOptions | undefined {
if (!options) return
return mergeWithDefaults(
{ ...builderOptionsDefaults, buildApp: defaultBuildApp },
{ ...builderOptionsDefaults, buildApp: async () => {} },
options,
)
}
Expand Down Expand Up @@ -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)
}
}
Comment on lines +1618 to +1627
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Putting config.builder.buildApp aside #19971 (comment), I think my question is whether there's any rational in choosing the default behavior here "fallback to building all environments if no environments have been built" or the below, assuming config.builder.buildApp is removed:

// always build rest of unbuilt environments
for (const environment of Object.values(builder.environments)) {
  if (!environment.isBuilt) {
    await builder.build(environment)
  }
}

I don't see anything significant right now, so either way seems fine to start with, but I just wanted to check if I'm missing something.

Copy link
Copy Markdown
Contributor

@hi-ogawa hi-ogawa May 12, 2025

Choose a reason for hiding this comment

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

Well, actually this seems like it's also about enforce: "post" hook (sorry, I should've just commented above). What's the expected behavior when there's post plugin.buildApp hook but without user's config.builder.buildApp now (and also hypothetically builder.buildApp is removed)?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The idea of the post hook was to give a way to plugins to build environments only if no other plugin has built them. See the problem described in the linked discussion. And they also want to make sure to do build related tasks after all environments have been built.

Once builder.buildApp is removed, the expected behavior with post is just to ensure it is run after all pre and normal order buildApp hooks. It is like having an afterBuildApp hook, but where you can still build environments. We could still have a beforeBuildApp and afterBuildApp hook.

But to the point in your other thread, I see now that it isn't only about order and the builder.buildApp hook. We can still move it as the last thing, after all post hooks (anyways, it is going to be removed). And to define the default as you propose building all environments that haven't been defined. In that case, the buildApp hooks will act more like a way to order the environment builds, more than deciding what gets built.

We could have an environment.build.manual: true config or something similar to define that this environment shouldn't be built if a hook hasn't build it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The idea of the post hook was to give a way to plugins to build environments only if no other plugin has built them

Oh right, I remembered that's the whole point of providing isBuilt to user land. What I had in mind was "hook after all writeBundle" #16358 (comment).

},
async build(environment: BuildEnvironment) {
return buildEnvironment(environment)
async build(
environment: BuildEnvironment,
): Promise<RollupOutput | RollupOutput[] | RollupWatcher> {
const output = await buildEnvironment(environment)
environment.isBuilt = true
return output
},
}

Expand Down Expand Up @@ -1667,3 +1693,5 @@ export async function createBuilder(

return builder
}

export type BuildAppHook = (this: void, builder: ViteBuilder) => Promise<void>
1 change: 1 addition & 0 deletions packages/vite/src/node/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export type {
} from './server'
export type {
ViteBuilder,
BuildAppHook,
BuilderOptions,
BuildOptions,
BuildEnvironmentOptions,
Expand Down
8 changes: 7 additions & 1 deletion packages/vite/src/node/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -291,7 +292,12 @@ export interface Plugin<A = any> extends RollupPlugin<A> {
* `{ order: 'pre', handler: hook }`
*/
transformIndexHtml?: IndexHtmlTransform

/**
* Build Environments
Comment thread
patak-cat marked this conversation as resolved.
*
* @experimental
*/
buildApp?: ObjectHook<BuildAppHook>
/**
* Perform custom handling of HMR updates.
* The handler receives a context containing changed filename, timestamp, a
Expand Down
10 changes: 9 additions & 1 deletion playground/environment-react-ssr/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
},
},
Expand Down