Skip to content

Commit 7063839

Browse files
authored
feat: make PluginContext available for Vite-specific hooks (#19936)
1 parent 44c6d01 commit 7063839

File tree

11 files changed

+430
-45
lines changed

11 files changed

+430
-45
lines changed
Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
import path from 'node:path'
2+
import { describe, expect, onTestFinished, test } from 'vitest'
3+
import { build } from '../../build'
4+
import type { Plugin } from '../../plugin'
5+
import { resolveConfig } from '../../config'
6+
import { createServer } from '../../server'
7+
import { preview } from '../../preview'
8+
import { promiseWithResolvers } from '../../../shared/utils'
9+
10+
const resolveConfigWithPlugin = (
11+
plugin: Plugin,
12+
command: 'serve' | 'build' = 'serve',
13+
) => {
14+
return resolveConfig(
15+
{ configFile: false, plugins: [plugin], logLevel: 'error' },
16+
command,
17+
)
18+
}
19+
20+
const createServerWithPlugin = async (plugin: Plugin) => {
21+
const server = await createServer({
22+
configFile: false,
23+
root: import.meta.dirname,
24+
plugins: [plugin],
25+
logLevel: 'error',
26+
server: {
27+
middlewareMode: true,
28+
},
29+
})
30+
onTestFinished(() => server.close())
31+
return server
32+
}
33+
34+
const createPreviewServerWithPlugin = async (plugin: Plugin) => {
35+
const server = await preview({
36+
configFile: false,
37+
root: import.meta.dirname,
38+
plugins: [
39+
{
40+
name: 'mock-preview',
41+
configurePreviewServer({ httpServer }) {
42+
// NOTE: make httpServer.listen no-op to avoid starting a server
43+
httpServer.listen = (...args: unknown[]) => {
44+
const listener = args.at(-1) as () => void
45+
listener()
46+
return httpServer as any
47+
}
48+
},
49+
},
50+
plugin,
51+
],
52+
logLevel: 'error',
53+
})
54+
onTestFinished(() => server.close())
55+
return server
56+
}
57+
58+
const buildWithPlugin = async (plugin: Plugin) => {
59+
await build({
60+
root: path.resolve(import.meta.dirname, '../packages/build-project'),
61+
logLevel: 'error',
62+
build: {
63+
write: false,
64+
},
65+
plugins: [
66+
{
67+
name: 'resolve-entry.js',
68+
resolveId(id) {
69+
if (id === 'entry.js') {
70+
return '\0' + id
71+
}
72+
},
73+
load(id) {
74+
if (id === '\0entry.js') {
75+
return 'export default {}'
76+
}
77+
},
78+
},
79+
plugin,
80+
],
81+
})
82+
}
83+
84+
describe('supports plugin context', () => {
85+
test('config hook', async () => {
86+
expect.assertions(3)
87+
88+
await resolveConfigWithPlugin({
89+
name: 'test',
90+
config() {
91+
expect(this).toMatchObject({
92+
debug: expect.any(Function),
93+
info: expect.any(Function),
94+
warn: expect.any(Function),
95+
error: expect.any(Function),
96+
meta: expect.any(Object),
97+
})
98+
expect(this.meta.rollupVersion).toBeTypeOf('string')
99+
// @ts-expect-error watchMode should not exist in types
100+
expect(this.meta.watchMode).toBeUndefined()
101+
},
102+
})
103+
})
104+
105+
test('configEnvironment hook', async () => {
106+
expect.assertions(3)
107+
108+
await resolveConfigWithPlugin({
109+
name: 'test',
110+
configEnvironment(name) {
111+
if (name !== 'client') return
112+
113+
expect(this).toMatchObject({
114+
debug: expect.any(Function),
115+
info: expect.any(Function),
116+
warn: expect.any(Function),
117+
error: expect.any(Function),
118+
meta: expect.any(Object),
119+
})
120+
expect(this.meta.rollupVersion).toBeTypeOf('string')
121+
// @ts-expect-error watchMode should not exist in types
122+
expect(this.meta.watchMode).toBeUndefined()
123+
},
124+
})
125+
})
126+
127+
test('configResolved hook', async () => {
128+
expect.assertions(3)
129+
130+
await resolveConfigWithPlugin({
131+
name: 'test',
132+
configResolved() {
133+
expect(this).toMatchObject({
134+
debug: expect.any(Function),
135+
info: expect.any(Function),
136+
warn: expect.any(Function),
137+
error: expect.any(Function),
138+
meta: expect.any(Object),
139+
})
140+
expect(this.meta.rollupVersion).toBeTypeOf('string')
141+
expect(this.meta.watchMode).toBe(true)
142+
},
143+
})
144+
})
145+
146+
test('configureServer hook', async () => {
147+
expect.assertions(3)
148+
149+
await createServerWithPlugin({
150+
name: 'test',
151+
configureServer() {
152+
expect(this).toMatchObject({
153+
debug: expect.any(Function),
154+
info: expect.any(Function),
155+
warn: expect.any(Function),
156+
error: expect.any(Function),
157+
meta: expect.any(Object),
158+
})
159+
expect(this.meta.rollupVersion).toBeTypeOf('string')
160+
expect(this.meta.watchMode).toBe(true)
161+
},
162+
})
163+
})
164+
165+
test('configurePreviewServer hook', async () => {
166+
expect.assertions(3)
167+
168+
await createPreviewServerWithPlugin({
169+
name: 'test',
170+
configurePreviewServer() {
171+
expect(this).toMatchObject({
172+
debug: expect.any(Function),
173+
info: expect.any(Function),
174+
warn: expect.any(Function),
175+
error: expect.any(Function),
176+
meta: expect.any(Object),
177+
})
178+
expect(this.meta.rollupVersion).toBeTypeOf('string')
179+
expect(this.meta.watchMode).toBe(false)
180+
},
181+
})
182+
})
183+
184+
test('transformIndexHtml hook in dev', async () => {
185+
expect.assertions(3)
186+
187+
const server = await createServerWithPlugin({
188+
name: 'test',
189+
transformIndexHtml() {
190+
expect(this).toMatchObject({
191+
debug: expect.any(Function),
192+
info: expect.any(Function),
193+
warn: expect.any(Function),
194+
error: expect.any(Function),
195+
meta: expect.any(Object),
196+
})
197+
expect(this.meta.rollupVersion).toBeTypeOf('string')
198+
expect(this.meta.watchMode).toBe(true)
199+
},
200+
})
201+
await server.transformIndexHtml('/index.html', '<html></html>')
202+
})
203+
204+
test('transformIndexHtml hook in build', async () => {
205+
expect.assertions(3)
206+
207+
await buildWithPlugin({
208+
name: 'test',
209+
transformIndexHtml() {
210+
expect(this).toMatchObject({
211+
debug: expect.any(Function),
212+
info: expect.any(Function),
213+
warn: expect.any(Function),
214+
error: expect.any(Function),
215+
meta: expect.any(Object),
216+
})
217+
expect(this.meta.rollupVersion).toBeTypeOf('string')
218+
expect(this.meta.watchMode).toBe(false)
219+
},
220+
})
221+
})
222+
223+
test('handleHotUpdate hook', async () => {
224+
expect.assertions(3)
225+
226+
const { promise, resolve } = promiseWithResolvers<void>()
227+
const server = await createServerWithPlugin({
228+
name: 'test',
229+
handleHotUpdate() {
230+
expect(this).toMatchObject({
231+
debug: expect.any(Function),
232+
info: expect.any(Function),
233+
warn: expect.any(Function),
234+
error: expect.any(Function),
235+
meta: expect.any(Object),
236+
})
237+
expect(this.meta.rollupVersion).toBeTypeOf('string')
238+
expect(this.meta.watchMode).toBe(true)
239+
resolve()
240+
},
241+
})
242+
server.watcher.emit(
243+
'change',
244+
path.resolve(import.meta.dirname, 'index.html'),
245+
)
246+
247+
await promise
248+
})
249+
250+
test('hotUpdate hook', async () => {
251+
expect.assertions(3)
252+
253+
const { promise, resolve } = promiseWithResolvers<void>()
254+
const server = await createServerWithPlugin({
255+
name: 'test',
256+
hotUpdate() {
257+
if (this.environment.name !== 'client') return
258+
259+
expect(this).toMatchObject({
260+
debug: expect.any(Function),
261+
info: expect.any(Function),
262+
warn: expect.any(Function),
263+
error: expect.any(Function),
264+
meta: expect.any(Object),
265+
environment: expect.any(Object),
266+
})
267+
expect(this.meta.rollupVersion).toBeTypeOf('string')
268+
expect(this.meta.watchMode).toBe(true)
269+
resolve()
270+
},
271+
})
272+
server.watcher.emit(
273+
'change',
274+
path.resolve(import.meta.dirname, 'index.html'),
275+
)
276+
277+
await promise
278+
})
279+
})

packages/vite/src/node/build.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import {
5555
mergeWithDefaults,
5656
normalizePath,
5757
partialEncodeURIPath,
58+
rollupVersion,
5859
} from './utils'
5960
import { perEnvironmentPlugin, resolveEnvironmentPlugins } from './plugin'
6061
import { manifestPlugin } from './plugins/manifest'
@@ -77,8 +78,9 @@ import {
7778
BaseEnvironment,
7879
getDefaultResolvedEnvironmentOptions,
7980
} from './baseEnvironment'
80-
import type { Plugin } from './plugin'
81+
import type { MinimalPluginContextWithoutEnvironment, Plugin } from './plugin'
8182
import type { RollupPluginHooks } from './typeUtils'
83+
import { BasicMinimalPluginContext } from './server/pluginContainer'
8284

8385
export interface BuildEnvironmentOptions {
8486
/**
@@ -1573,6 +1575,14 @@ export async function createBuilder(
15731575
environments,
15741576
config,
15751577
async buildApp() {
1578+
const pluginContext = new BasicMinimalPluginContext(
1579+
{
1580+
rollupVersion,
1581+
watchMode: false,
1582+
},
1583+
config.logger,
1584+
)
1585+
15761586
// order 'pre' and 'normal' hooks are run first, then config.builder.buildApp, then 'post' hooks
15771587
let configBuilderBuildAppCalled = false
15781588
for (const p of config.getSortedPlugins('buildApp')) {
@@ -1586,7 +1596,7 @@ export async function createBuilder(
15861596
await configBuilder.buildApp(builder)
15871597
}
15881598
const handler = getHookHandler(hook)
1589-
await handler(builder)
1599+
await handler.call(pluginContext, builder)
15901600
}
15911601
if (!configBuilderBuildAppCalled) {
15921602
await configBuilder.buildApp(builder)
@@ -1670,4 +1680,7 @@ export async function createBuilder(
16701680
return builder
16711681
}
16721682

1673-
export type BuildAppHook = (this: void, builder: ViteBuilder) => Promise<void>
1683+
export type BuildAppHook = (
1684+
this: MinimalPluginContextWithoutEnvironment,
1685+
builder: ViteBuilder,
1686+
) => Promise<void>

0 commit comments

Comments
 (0)