Skip to content

Commit dad6100

Browse files
committed
feat: convert esbuild plugin to rolldown plugin
1 parent 1d20b9a commit dad6100

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
@@ -102,6 +102,7 @@ import { PartialEnvironment } from './baseEnvironment'
102102
import { createIdResolver } from './idResolver'
103103
import { getAdditionalAllowedHosts } from './server/middlewares/hostCheck'
104104
import { type OxcOptions, convertEsbuildConfigToOxcConfig } from './plugins/oxc'
105+
import { convertEsbuildPluginToRolldownPlugin } from './optimizer/pluginConverter'
105106

106107
const debug = createDebugger('vite:config', { depth: 10 })
107108
const promisifiedRealpath = promisify(fs.realpath)
@@ -1095,7 +1096,6 @@ function resolveDepOptimizationOptions(
10951096
// - alias (it probably does not work the same with `resolve.alias`)
10961097
// - inject
10971098
// - banner, footer
1098-
// - plugins (not sure if it's possible and need to check if it's worth it before)
10991099
// - nodePaths
11001100

11011101
// NOTE: the following options does not make sense to set / convert it
@@ -1126,6 +1126,21 @@ function resolveDepOptimizationOptions(
11261126
)
11271127
}
11281128

1129+
function applyDepOptimizationOptionCompat(resolvedConfig: ResolvedConfig) {
1130+
if (
1131+
resolvedConfig.optimizeDeps.esbuildOptions?.plugins &&
1132+
resolvedConfig.optimizeDeps.esbuildOptions.plugins.length > 0
1133+
) {
1134+
resolvedConfig.optimizeDeps.rollupOptions ??= {}
1135+
resolvedConfig.optimizeDeps.rollupOptions.plugins ||= []
1136+
;(resolvedConfig.optimizeDeps.rollupOptions.plugins as any[]).push(
1137+
...resolvedConfig.optimizeDeps.esbuildOptions.plugins.map((plugin) =>
1138+
convertEsbuildPluginToRolldownPlugin(plugin),
1139+
),
1140+
)
1141+
}
1142+
}
1143+
11291144
export async function resolveConfig(
11301145
inlineConfig: InlineConfig,
11311146
command: 'build' | 'serve',
@@ -1706,6 +1721,8 @@ export async function resolveConfig(
17061721
resolved.build.ssrEmitAssets || resolved.build.emitAssets
17071722
}
17081723

1724+
applyDepOptimizationOptionCompat(resolved)
1725+
17091726
debug?.(`using resolved config: %O`, {
17101727
...resolved,
17111728
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)