Skip to content

Commit ac75780

Browse files
committed
feat: convert esbuild plugin to rolldown plugin
1 parent 109dd05 commit ac75780

File tree

2 files changed

+309
-1
lines changed

2 files changed

+309
-1
lines changed

packages/vite/src/node/config.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ import { resolveSSROptions, ssrConfigDefaults } from './ssr'
100100
import { PartialEnvironment } from './baseEnvironment'
101101
import { createIdResolver } from './idResolver'
102102
import { type OxcOptions, convertEsbuildConfigToOxcConfig } from './plugins/oxc'
103+
import { convertEsbuildPluginToRolldownPlugin } from './optimizer/pluginConverter'
103104

104105
const debug = createDebugger('vite:config', { depth: 10 })
105106
const promisifiedRealpath = promisify(fs.realpath)
@@ -1062,7 +1063,6 @@ function resolveDepOptimizationOptions(
10621063
// - alias (it probably does not work the same with `resolve.alias`)
10631064
// - inject
10641065
// - banner, footer
1065-
// - plugins (not sure if it's possible and need to check if it's worth it before)
10661066
// - nodePaths
10671067

10681068
// NOTE: the following options does not make sense to set / convert it
@@ -1093,6 +1093,21 @@ function resolveDepOptimizationOptions(
10931093
)
10941094
}
10951095

1096+
function applyDepOptimizationOptionCompat(resolvedConfig: ResolvedConfig) {
1097+
if (
1098+
resolvedConfig.optimizeDeps.esbuildOptions?.plugins &&
1099+
resolvedConfig.optimizeDeps.esbuildOptions.plugins.length > 0
1100+
) {
1101+
resolvedConfig.optimizeDeps.rollupOptions ??= {}
1102+
resolvedConfig.optimizeDeps.rollupOptions.plugins ||= []
1103+
;(resolvedConfig.optimizeDeps.rollupOptions.plugins as any[]).push(
1104+
...resolvedConfig.optimizeDeps.esbuildOptions.plugins.map((plugin) =>
1105+
convertEsbuildPluginToRolldownPlugin(plugin),
1106+
),
1107+
)
1108+
}
1109+
}
1110+
10961111
export async function resolveConfig(
10971112
inlineConfig: InlineConfig,
10981113
command: 'build' | 'serve',
@@ -1664,6 +1679,8 @@ export async function resolveConfig(
16641679
resolved.build.ssrEmitAssets || resolved.build.emitAssets
16651680
}
16661681

1682+
applyDepOptimizationOptionCompat(resolved)
1683+
16671684
debug?.(`using resolved config: %O`, {
16681685
...resolved,
16691686
plugins: resolved.plugins.map((p) => p.name),
Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
import { dirname } from 'node:path'
2+
import type * as esbuild from 'esbuild'
3+
import type {
4+
ImportKind,
5+
LoadResult,
6+
PluginContext,
7+
ResolveIdResult,
8+
Plugin as RolldownPlugin,
9+
RolldownPluginOption,
10+
} from 'rolldown'
11+
12+
type MaybePromise<T> = T | Promise<T>
13+
type EsbuildOnResolveCallback = (
14+
args: esbuild.OnResolveArgs,
15+
) => MaybePromise<esbuild.OnResolveResult | null | undefined>
16+
type EsbuildOnLoadCallback = (
17+
args: esbuild.OnLoadArgs,
18+
) => MaybePromise<esbuild.OnLoadResult | null | undefined>
19+
type ResolveIdHandler = (
20+
this: PluginContext,
21+
id: string,
22+
importer: string | undefined,
23+
opts: { kind: ImportKind },
24+
) => MaybePromise<ResolveIdResult>
25+
type LoadHandler = (this: PluginContext, id: string) => MaybePromise<LoadResult>
26+
27+
export function convertEsbuildPluginToRolldownPlugin(
28+
esbuildPlugin: esbuild.Plugin,
29+
): RolldownPlugin {
30+
const onStartCallbacks: Array<() => void> = []
31+
const onEndCallbacks: Array<(buildResult: esbuild.BuildResult) => void> = []
32+
const onDisposeCallbacks: Array<() => void> = []
33+
let resolveIdHandlers: ResolveIdHandler[]
34+
let loadHandlers: LoadHandler[]
35+
36+
let isSetupDone = false
37+
const setup = async (
38+
plugins: RolldownPluginOption[],
39+
platform: 'browser' | 'node' | 'neutral',
40+
) => {
41+
const onResolveCallbacks: Array<
42+
[options: esbuild.OnResolveOptions, callback: EsbuildOnResolveCallback]
43+
> = []
44+
const onLoadCallbacks: Array<
45+
[options: esbuild.OnLoadOptions, callback: EsbuildOnLoadCallback]
46+
> = []
47+
48+
const pluginBuild: esbuild.PluginBuild = {
49+
initialOptions: new Proxy(
50+
{
51+
platform,
52+
plugins:
53+
plugins?.flatMap((p) =>
54+
p && 'name' in p
55+
? [
56+
{
57+
name: p.name,
58+
// eslint-disable-next-line @typescript-eslint/no-empty-function
59+
setup() {},
60+
},
61+
]
62+
: [],
63+
) ?? [],
64+
},
65+
{
66+
get(target, p, _receiver) {
67+
if (p in target) return (target as any)[p]
68+
throw new Error('Not implemented')
69+
},
70+
},
71+
) as esbuild.BuildOptions,
72+
resolve() {
73+
throw new Error('Not implemented')
74+
},
75+
onStart(callback) {
76+
onStartCallbacks.push(callback)
77+
},
78+
onEnd(callback) {
79+
onEndCallbacks.push(callback)
80+
},
81+
onResolve(options, callback) {
82+
onResolveCallbacks.push([options, callback])
83+
},
84+
onLoad(options, callback) {
85+
onLoadCallbacks.push([options, callback])
86+
},
87+
onDispose(callback) {
88+
onDisposeCallbacks.push(callback)
89+
},
90+
get esbuild(): esbuild.PluginBuild['esbuild'] {
91+
throw new Error('Not implemented')
92+
},
93+
set esbuild(_: unknown) {
94+
throw new Error('Not implemented')
95+
},
96+
}
97+
98+
await esbuildPlugin.setup(pluginBuild)
99+
100+
resolveIdHandlers = onResolveCallbacks.map(([options, callback]) =>
101+
createResolveIdHandler(options, callback),
102+
)
103+
loadHandlers = onLoadCallbacks.map(([options, callback]) =>
104+
createLoadHandler(options, callback),
105+
)
106+
isSetupDone = true
107+
}
108+
109+
return {
110+
name: esbuildPlugin.name,
111+
async options(inputOptions) {
112+
await setup(
113+
inputOptions.plugins as RolldownPluginOption[],
114+
inputOptions.platform ?? 'node',
115+
)
116+
},
117+
async buildStart(inputOptions) {
118+
// options hook is not called for scanner
119+
if (!isSetupDone) {
120+
// inputOptions.plugins is not available for buildStart hook
121+
// put a dummy plugin to tell that this is a scan
122+
await setup(
123+
[{ name: 'vite:dep-scan' }],
124+
inputOptions.platform ?? 'node',
125+
)
126+
}
127+
128+
for (const cb of onStartCallbacks) {
129+
cb()
130+
}
131+
},
132+
generateBundle() {
133+
const buildResult = new Proxy(
134+
{},
135+
{
136+
get(_target, _prop) {
137+
throw new Error('Not implemented')
138+
},
139+
},
140+
) as esbuild.BuildResult
141+
for (const cb of onEndCallbacks) {
142+
cb(buildResult)
143+
}
144+
},
145+
async resolveId(id, importer, opts) {
146+
for (const handler of resolveIdHandlers) {
147+
const result = await handler.call(this, id, importer, opts)
148+
if (result) {
149+
return result
150+
}
151+
}
152+
},
153+
async load(id) {
154+
for (const handler of loadHandlers) {
155+
const result = await handler.call(this, id)
156+
if (result) {
157+
return result
158+
}
159+
}
160+
},
161+
closeBundle() {
162+
if (!this.meta.watchMode) {
163+
for (const cb of onDisposeCallbacks) {
164+
cb()
165+
}
166+
}
167+
},
168+
closeWatcher() {
169+
for (const cb of onDisposeCallbacks) {
170+
cb()
171+
}
172+
},
173+
}
174+
}
175+
176+
function createResolveIdHandler(
177+
options: esbuild.OnResolveOptions,
178+
callback: EsbuildOnResolveCallback,
179+
): ResolveIdHandler {
180+
return async function (id, importer, opts) {
181+
const [importerWithoutNamespace, importerNamespace] =
182+
idToPathAndNamespace(importer)
183+
if (
184+
options.namespace !== undefined &&
185+
options.namespace !== importerNamespace
186+
) {
187+
return
188+
}
189+
if (options.filter !== undefined && !options.filter.test(id)) {
190+
return
191+
}
192+
193+
const result = await callback({
194+
path: id,
195+
importer: importerWithoutNamespace ?? '',
196+
namespace: importerNamespace,
197+
resolveDir: dirname(importerWithoutNamespace ?? ''),
198+
kind:
199+
importerWithoutNamespace === undefined
200+
? 'entry-point'
201+
: opts.kind === 'import'
202+
? 'import-statement'
203+
: opts.kind,
204+
pluginData: {},
205+
with: {},
206+
})
207+
if (!result) return
208+
if (result.errors && result.errors.length > 0) {
209+
throw new AggregateError(result.errors)
210+
}
211+
if (
212+
(result.warnings && result.warnings.length > 0) ||
213+
(result.watchDirs && result.watchDirs.length > 0) ||
214+
!result.path
215+
) {
216+
throw new Error('not implemented')
217+
}
218+
for (const file of result.watchFiles ?? []) {
219+
this.addWatchFile(file)
220+
}
221+
222+
return {
223+
id: result.namespace ? `${result.namespace}:${result.path}` : result.path,
224+
external: result.external,
225+
moduleSideEffects: result.sideEffects,
226+
}
227+
}
228+
}
229+
230+
function createLoadHandler(
231+
options: esbuild.OnLoadOptions,
232+
callback: EsbuildOnLoadCallback,
233+
): LoadHandler {
234+
const textDecoder = new TextDecoder()
235+
return async function (id) {
236+
const [idWithoutNamespace, idNamespace] = idToPathAndNamespace(id)
237+
if (options.namespace !== undefined && options.namespace !== idNamespace) {
238+
return
239+
}
240+
if (options.filter !== undefined && !options.filter.test(id)) {
241+
return
242+
}
243+
244+
const result = await callback.call(this, {
245+
path: idWithoutNamespace,
246+
namespace: idNamespace,
247+
suffix: '',
248+
pluginData: {},
249+
with: {},
250+
})
251+
if (!result) return
252+
if (result.errors && result.errors.length > 0) {
253+
throw new AggregateError(result.errors)
254+
}
255+
if (
256+
(result.warnings && result.warnings.length > 0) ||
257+
(result.watchDirs && result.watchDirs.length > 0) ||
258+
!result.contents
259+
) {
260+
throw new Error('not implemented')
261+
}
262+
for (const file of result.watchFiles ?? []) {
263+
this.addWatchFile(file)
264+
}
265+
266+
return {
267+
code:
268+
typeof result.contents === 'string'
269+
? result.contents
270+
: textDecoder.decode(result.contents),
271+
moduleType: result.loader,
272+
}
273+
}
274+
}
275+
276+
function idToPathAndNamespace(id: string): [path: string, namespace: string]
277+
function idToPathAndNamespace(
278+
id: string | undefined,
279+
): [path: string | undefined, namespace: string]
280+
function idToPathAndNamespace(
281+
id: string | undefined,
282+
): [path: string | undefined, namespace: string] {
283+
if (id === undefined) return [undefined, 'file']
284+
285+
const namespaceIndex = id.indexOf(':')
286+
if (namespaceIndex >= 0) {
287+
return [id.slice(namespaceIndex + 1), id.slice(0, namespaceIndex)]
288+
} else {
289+
return [id, 'file']
290+
}
291+
}

0 commit comments

Comments
 (0)