Skip to content

Commit 449d7f3

Browse files
authored
chore: use rolldown-plugin-dts for dts bundling (vitejs#19990)
1 parent 4a8aa82 commit 449d7f3

File tree

3 files changed

+188
-79
lines changed

3 files changed

+188
-79
lines changed

packages/vite/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@
7272
"build-bundle": "rolldown --config rollup.config.ts",
7373
"build-types": "pnpm build-types-temp && pnpm build-types-roll && pnpm build-types-check",
7474
"build-types-temp": "tsc --emitDeclarationOnly --outDir temp -p src/node/tsconfig.build.json",
75-
"build-types-roll": "NODE_OPTIONS='--import tsx' rollup --config rollup.dts.config.ts && premove temp",
75+
"build-types-roll": "rolldown --config rollup.dts.config.ts && premove temp",
7676
"build-types-check": "tsc --project tsconfig.check.json",
7777
"typecheck": "tsc --noEmit && tsc --noEmit -p src/node",
7878
"lint": "eslint --cache --ext .ts src/**",
@@ -97,6 +97,7 @@
9797
"@babel/parser": "^7.27.2",
9898
"@jridgewell/trace-mapping": "^0.3.25",
9999
"@oxc-project/runtime": "^0.70.0",
100+
"@oxc-project/types": "^0.70.0",
100101
"@polka/compression": "^1.0.0-next.25",
101102
"@rollup/plugin-alias": "^5.1.1",
102103
"@rollup/plugin-commonjs": "^28.0.3",
@@ -139,7 +140,7 @@
139140
"postcss-modules": "^6.0.1",
140141
"resolve.exports": "^2.0.3",
141142
"rolldown": "^1.0.0-beta.9",
142-
"rollup-plugin-dts": "^6.2.1",
143+
"rolldown-plugin-dts": "^0.13.3",
143144
"rollup-plugin-license": "^3.6.0",
144145
"sass": "^1.88.0",
145146
"sass-embedded": "^1.88.0",

packages/vite/rollup.dts.config.ts

Lines changed: 179 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,23 @@
11
import { readFileSync } from 'node:fs'
22
import { fileURLToPath } from 'node:url'
3-
import { findStaticImports } from 'mlly'
4-
import { defineConfig } from 'rollup'
5-
import type { Plugin, PluginContext, RenderedChunk } from 'rollup'
6-
import dts from 'rollup-plugin-dts'
7-
import { parse } from '@babel/parser'
3+
import { defineConfig } from 'rolldown'
4+
import type {
5+
OutputChunk,
6+
Plugin,
7+
PluginContext,
8+
RenderedChunk,
9+
} from 'rolldown'
10+
import { parseAst } from 'rolldown/parseAst'
11+
import { dts } from 'rolldown-plugin-dts'
12+
import { parse as parseWithBabel } from '@babel/parser'
813
import { walk } from 'estree-walker'
914
import MagicString from 'magic-string'
15+
import type {
16+
Directive,
17+
ModuleExportName,
18+
Program,
19+
Statement,
20+
} from '@oxc-project/types'
1021

1122
const depTypesDir = new URL('./src/types/', import.meta.url)
1223
const pkg = JSON.parse(
@@ -32,7 +43,7 @@ export default defineConfig({
3243
format: 'esm',
3344
},
3445
external,
35-
plugins: [patchTypes(), dts({ respectExternal: true })],
46+
plugins: [patchTypes(), dts({ dtsInput: true })],
3647
})
3748

3849
// Taken from https://stackoverflow.com/a/36328890
@@ -47,22 +58,48 @@ const identifierWithTrailingDollarRE = /\b(\w+)\$\d+\b/g
4758
*/
4859
const identifierReplacements: Record<string, Record<string, string>> = {
4960
rollup: {
50-
Plugin$1: 'rollup.Plugin',
51-
PluginContext$1: 'rollup.PluginContext',
52-
MinimalPluginContext$1: 'rollup.MinimalPluginContext',
53-
TransformResult$1: 'rollup.TransformResult',
61+
Plugin$2: 'Rollup.Plugin',
62+
TransformResult$1: 'Rollup.TransformResult',
5463
},
5564
esbuild: {
5665
TransformResult$2: 'esbuild_TransformResult',
5766
TransformOptions$1: 'esbuild_TransformOptions',
5867
BuildOptions$1: 'esbuild_BuildOptions',
5968
},
69+
'node:http': {
70+
// https://github.com/rolldown/rolldown/issues/4324
71+
http$1: 'http_1',
72+
http$2: 'http_2',
73+
http$3: 'http_3',
74+
Server$1: 'http.Server',
75+
IncomingMessage$1: 'http.IncomingMessage',
76+
},
6077
'node:https': {
61-
Server$1: 'HttpsServer',
62-
ServerOptions$1: 'HttpsServerOptions',
78+
Server$2: 'HttpsServer',
79+
ServerOptions$2: 'HttpsServerOptions',
80+
},
81+
'vite/module-runner': {
82+
FetchResult$1: 'moduleRunner_FetchResult',
83+
},
84+
'../../types/hmrPayload.js': {
85+
CustomPayload$1: 'hmrPayload_CustomPayload',
86+
HotPayload$1: 'hmrPayload_HotPayload',
87+
},
88+
'../../types/customEvent.js': {
89+
InferCustomEventPayload$1: 'hmrPayload_InferCustomEventPayload',
90+
},
91+
'../../types/internal/lightningcssOptions.js': {
92+
LightningCSSOptions$1: 'lightningcssOptions_LightningCSSOptions',
6393
},
6494
}
6595

96+
// type names that are declared
97+
const ignoreConfusingTypeNames = [
98+
'Plugin$1',
99+
'MinimalPluginContext$1',
100+
'ServerOptions$1',
101+
]
102+
66103
/**
67104
* Patch the types files before passing to dts plugin
68105
* 1. Resolve `dep-types/*` and `types/*` imports
@@ -74,47 +111,102 @@ const identifierReplacements: Record<string, Record<string, string>> = {
74111
function patchTypes(): Plugin {
75112
return {
76113
name: 'patch-types',
77-
resolveId(id) {
78-
// Dep types should be bundled
79-
if (id.startsWith('dep-types/')) {
80-
const fileUrl = new URL(
81-
`./${id.slice('dep-types/'.length)}.d.ts`,
82-
depTypesDir,
83-
)
84-
return fileURLToPath(fileUrl)
85-
}
86-
// Ambient types are unbundled and externalized
87-
if (id.startsWith('types/')) {
88-
return {
89-
id: '../../' + (id.endsWith('.js') ? id : id + '.js'),
90-
external: true,
114+
resolveId: {
115+
order: 'pre',
116+
handler(id) {
117+
// Dep types should be bundled
118+
if (id.startsWith('dep-types/')) {
119+
const fileUrl = new URL(
120+
`./${id.slice('dep-types/'.length)}.d.ts`,
121+
depTypesDir,
122+
)
123+
return fileURLToPath(fileUrl)
91124
}
92-
}
125+
// Ambient types are unbundled and externalized
126+
if (id.startsWith('types/')) {
127+
return {
128+
id: '../../' + (id.endsWith('.js') ? id : id + '.js'),
129+
external: true,
130+
}
131+
}
132+
},
93133
},
94-
renderChunk(code, chunk) {
95-
if (
96-
chunk.fileName.startsWith('module-runner') ||
97-
// index and moduleRunner have a common chunk "moduleRunnerTransport"
98-
chunk.fileName.startsWith('moduleRunnerTransport') ||
99-
chunk.fileName.startsWith('types.d-')
100-
) {
101-
validateRunnerChunk.call(this, chunk)
102-
} else {
103-
validateChunkImports.call(this, chunk)
104-
code = replaceConfusingTypeNames.call(this, code, chunk)
105-
code = stripInternalTypes.call(this, code, chunk)
106-
code = cleanUnnecessaryComments(code)
134+
generateBundle(_opts, bundle) {
135+
for (const chunk of Object.values(bundle)) {
136+
if (chunk.type !== 'chunk') continue
137+
138+
const ast = parseAst(chunk.code, { lang: 'ts', sourceType: 'module' })
139+
const importBindings = getAllImportBindings(ast)
140+
if (
141+
chunk.fileName.startsWith('module-runner') ||
142+
// index and moduleRunner have a common chunk "moduleRunnerTransport"
143+
chunk.fileName.startsWith('moduleRunnerTransport') ||
144+
chunk.fileName.startsWith('types.d-')
145+
) {
146+
validateRunnerChunk.call(this, chunk, importBindings)
147+
} else {
148+
validateChunkImports.call(this, chunk, importBindings)
149+
replaceConfusingTypeNames.call(this, chunk, importBindings)
150+
stripInternalTypes.call(this, chunk)
151+
cleanUnnecessaryComments(chunk)
152+
}
107153
}
108-
return code
109154
},
110155
}
111156
}
112157

158+
function stringifyModuleExportName(node: ModuleExportName): string {
159+
if (node.type === 'Identifier') {
160+
return node.name
161+
}
162+
return node.value
163+
}
164+
165+
type ImportBindings = { id: string; bindings: string[]; locals: string[] }
166+
167+
function getImportBindings(
168+
node: Directive | Statement,
169+
): ImportBindings | undefined {
170+
if (node.type === 'ImportDeclaration') {
171+
return {
172+
id: node.source.value,
173+
bindings: node.specifiers.map((s) =>
174+
s.type === 'ImportDefaultSpecifier'
175+
? 'default'
176+
: s.type === 'ImportNamespaceSpecifier'
177+
? '*'
178+
: stringifyModuleExportName(s.imported),
179+
),
180+
locals: node.specifiers.map((s) => s.local.name),
181+
}
182+
}
183+
if (node.type === 'ExportNamedDeclaration') {
184+
if (!node.source) return undefined
185+
return {
186+
id: node.source.value,
187+
bindings: node.specifiers.map((s) => stringifyModuleExportName(s.local)),
188+
locals: [],
189+
}
190+
}
191+
if (node.type === 'ExportAllDeclaration') {
192+
if (!node.source) return undefined
193+
return { id: node.source.value, bindings: ['*'], locals: [] }
194+
}
195+
}
196+
197+
function getAllImportBindings(ast: Program): ImportBindings[] {
198+
return ast.body.flatMap((node) => getImportBindings(node) ?? [])
199+
}
200+
113201
/**
114202
* Runner chunk should only import local dependencies to stay lightweight
115203
*/
116-
function validateRunnerChunk(this: PluginContext, chunk: RenderedChunk) {
117-
for (const [id, bindings] of Object.entries(chunk.importedBindings)) {
204+
function validateRunnerChunk(
205+
this: PluginContext,
206+
chunk: RenderedChunk,
207+
importBindings: ImportBindings[],
208+
) {
209+
for (const { id, bindings } of importBindings) {
118210
if (
119211
!id.startsWith('./') &&
120212
!id.startsWith('../') &&
@@ -133,9 +225,13 @@ function validateRunnerChunk(this: PluginContext, chunk: RenderedChunk) {
133225
/**
134226
* Validate that chunk imports do not import dev deps
135227
*/
136-
function validateChunkImports(this: PluginContext, chunk: RenderedChunk) {
228+
function validateChunkImports(
229+
this: PluginContext,
230+
chunk: RenderedChunk,
231+
importBindings: ImportBindings[],
232+
) {
137233
const deps = Object.keys(pkg.dependencies)
138-
for (const [id, bindings] of Object.entries(chunk.importedBindings)) {
234+
for (const { id, bindings } of importBindings) {
139235
if (
140236
!id.startsWith('./') &&
141237
!id.startsWith('../') &&
@@ -163,17 +259,13 @@ function validateChunkImports(this: PluginContext, chunk: RenderedChunk) {
163259
*/
164260
function replaceConfusingTypeNames(
165261
this: PluginContext,
166-
code: string,
167-
chunk: RenderedChunk,
262+
chunk: OutputChunk,
263+
importBindings: ImportBindings[],
168264
) {
169-
const imports = findStaticImports(code)
170-
171265
for (const modName in identifierReplacements) {
172-
const imp = imports.find(
173-
(imp) => imp.specifier === modName && imp.imports.includes('{'),
174-
)
266+
const imp = importBindings.filter((imp) => imp.id === modName)
175267
// Validate that `identifierReplacements` is not outdated if there's no match
176-
if (!imp) {
268+
if (imp.length === 0) {
177269
this.warn(
178270
`${chunk.fileName} does not import "${modName}" for replacement`,
179271
)
@@ -184,7 +276,7 @@ function replaceConfusingTypeNames(
184276
const replacements = identifierReplacements[modName]
185277
for (const id in replacements) {
186278
// Validate that `identifierReplacements` is not outdated if there's no match
187-
if (!imp.imports.includes(id)) {
279+
if (!imp.some((i) => i.locals.includes(id))) {
188280
this.warn(
189281
`${chunk.fileName} does not import "${id}" from "${modName}" for replacement`,
190282
)
@@ -198,17 +290,26 @@ function replaceConfusingTypeNames(
198290
// named import cannot be replaced with `Foo as Namespace.Foo`, so we
199291
// pre-emptively remove the whole named import
200292
if (betterId.includes('.')) {
201-
code = code.replace(
293+
chunk.code = chunk.code.replace(
202294
new RegExp(`\\b\\w+\\b as ${regexEscapedId},?\\s?`),
203295
'',
204296
)
205297
}
206-
code = code.replace(new RegExp(`\\b${regexEscapedId}\\b`, 'g'), betterId)
298+
chunk.code = chunk.code.replace(
299+
new RegExp(`\\b${regexEscapedId}\\b`, 'g'),
300+
betterId,
301+
)
207302
}
208303
}
209304

210-
const unreplacedIds = unique(
211-
Array.from(code.matchAll(identifierWithTrailingDollarRE), (m) => m[0]),
305+
const identifiers = unique(
306+
Array.from(
307+
chunk.code.matchAll(identifierWithTrailingDollarRE),
308+
(m) => m[0],
309+
),
310+
)
311+
const unreplacedIds = identifiers.filter(
312+
(id) => !ignoreConfusingTypeNames.includes(id),
212313
)
213314
if (unreplacedIds.length) {
214315
const unreplacedStr = unreplacedIds.map((id) => `\n- ${id}`).join('')
@@ -217,23 +318,29 @@ function replaceConfusingTypeNames(
217318
)
218319
process.exitCode = 1
219320
}
220-
221-
return code
321+
const notUsedConfusingTypeNames = ignoreConfusingTypeNames.filter(
322+
(id) => !identifiers.includes(id),
323+
)
324+
// Validate that `identifierReplacements` is not outdated if there's no match
325+
if (notUsedConfusingTypeNames.length) {
326+
const notUsedStr = notUsedConfusingTypeNames
327+
.map((id) => `\n- ${id}`)
328+
.join('')
329+
this.warn(`${chunk.fileName} contains unused identifier names${notUsedStr}`)
330+
process.exitCode = 1
331+
}
222332
}
223333

224334
/**
225335
* While we already enable `compilerOptions.stripInternal`, some internal comments
226336
* like internal parameters are still not stripped by TypeScript, so we run another
227337
* pass here.
228338
*/
229-
function stripInternalTypes(
230-
this: PluginContext,
231-
code: string,
232-
chunk: RenderedChunk,
233-
) {
234-
if (code.includes('@internal')) {
235-
const s = new MagicString(code)
236-
const ast = parse(code, {
339+
function stripInternalTypes(this: PluginContext, chunk: OutputChunk) {
340+
if (chunk.code.includes('@internal')) {
341+
const s = new MagicString(chunk.code)
342+
// need to parse with babel to get the comments
343+
const ast = parseWithBabel(chunk.code, {
237344
plugins: ['typescript'],
238345
sourceType: 'module',
239346
})
@@ -246,15 +353,13 @@ function stripInternalTypes(
246353
},
247354
})
248355

249-
code = s.toString()
356+
chunk.code = s.toString()
250357

251-
if (code.includes('@internal')) {
358+
if (chunk.code.includes('@internal')) {
252359
this.warn(`${chunk.fileName} has unhandled @internal declarations`)
253360
process.exitCode = 1
254361
}
255362
}
256-
257-
return code
258363
}
259364

260365
/**
@@ -283,8 +388,8 @@ function removeInternal(s: MagicString, node: any): boolean {
283388
return false
284389
}
285390

286-
function cleanUnnecessaryComments(code: string) {
287-
return code
391+
function cleanUnnecessaryComments(chunk: OutputChunk) {
392+
chunk.code = chunk.code
288393
.replace(multilineCommentsRE, (m) => {
289394
return licenseCommentsRE.test(m) ? '' : m
290395
})

0 commit comments

Comments
 (0)