1
1
import { readFileSync } from 'node:fs'
2
2
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'
8
13
import { walk } from 'estree-walker'
9
14
import MagicString from 'magic-string'
15
+ import type {
16
+ Directive ,
17
+ ModuleExportName ,
18
+ Program ,
19
+ Statement ,
20
+ } from '@oxc-project/types'
10
21
11
22
const depTypesDir = new URL ( './src/types/' , import . meta. url )
12
23
const pkg = JSON . parse (
@@ -32,7 +43,7 @@ export default defineConfig({
32
43
format : 'esm' ,
33
44
} ,
34
45
external,
35
- plugins : [ patchTypes ( ) , dts ( { respectExternal : true } ) ] ,
46
+ plugins : [ patchTypes ( ) , dts ( { dtsInput : true } ) ] ,
36
47
} )
37
48
38
49
// Taken from https://stackoverflow.com/a/36328890
@@ -47,22 +58,48 @@ const identifierWithTrailingDollarRE = /\b(\w+)\$\d+\b/g
47
58
*/
48
59
const identifierReplacements : Record < string , Record < string , string > > = {
49
60
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' ,
54
63
} ,
55
64
esbuild : {
56
65
TransformResult$2 : 'esbuild_TransformResult' ,
57
66
TransformOptions$1 : 'esbuild_TransformOptions' ,
58
67
BuildOptions$1 : 'esbuild_BuildOptions' ,
59
68
} ,
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
+ } ,
60
77
'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' ,
63
93
} ,
64
94
}
65
95
96
+ // type names that are declared
97
+ const ignoreConfusingTypeNames = [
98
+ 'Plugin$1' ,
99
+ 'MinimalPluginContext$1' ,
100
+ 'ServerOptions$1' ,
101
+ ]
102
+
66
103
/**
67
104
* Patch the types files before passing to dts plugin
68
105
* 1. Resolve `dep-types/*` and `types/*` imports
@@ -74,47 +111,102 @@ const identifierReplacements: Record<string, Record<string, string>> = {
74
111
function patchTypes ( ) : Plugin {
75
112
return {
76
113
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 )
91
124
}
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
+ } ,
93
133
} ,
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
+ }
107
153
}
108
- return code
109
154
} ,
110
155
}
111
156
}
112
157
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
+
113
201
/**
114
202
* Runner chunk should only import local dependencies to stay lightweight
115
203
*/
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 ) {
118
210
if (
119
211
! id . startsWith ( './' ) &&
120
212
! id . startsWith ( '../' ) &&
@@ -133,9 +225,13 @@ function validateRunnerChunk(this: PluginContext, chunk: RenderedChunk) {
133
225
/**
134
226
* Validate that chunk imports do not import dev deps
135
227
*/
136
- function validateChunkImports ( this : PluginContext , chunk : RenderedChunk ) {
228
+ function validateChunkImports (
229
+ this : PluginContext ,
230
+ chunk : RenderedChunk ,
231
+ importBindings : ImportBindings [ ] ,
232
+ ) {
137
233
const deps = Object . keys ( pkg . dependencies )
138
- for ( const [ id , bindings ] of Object . entries ( chunk . importedBindings ) ) {
234
+ for ( const { id, bindings } of importBindings ) {
139
235
if (
140
236
! id . startsWith ( './' ) &&
141
237
! id . startsWith ( '../' ) &&
@@ -163,17 +259,13 @@ function validateChunkImports(this: PluginContext, chunk: RenderedChunk) {
163
259
*/
164
260
function replaceConfusingTypeNames (
165
261
this : PluginContext ,
166
- code : string ,
167
- chunk : RenderedChunk ,
262
+ chunk : OutputChunk ,
263
+ importBindings : ImportBindings [ ] ,
168
264
) {
169
- const imports = findStaticImports ( code )
170
-
171
265
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 )
175
267
// Validate that `identifierReplacements` is not outdated if there's no match
176
- if ( ! imp ) {
268
+ if ( imp . length === 0 ) {
177
269
this . warn (
178
270
`${ chunk . fileName } does not import "${ modName } " for replacement` ,
179
271
)
@@ -184,7 +276,7 @@ function replaceConfusingTypeNames(
184
276
const replacements = identifierReplacements [ modName ]
185
277
for ( const id in replacements ) {
186
278
// 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 ) ) ) {
188
280
this . warn (
189
281
`${ chunk . fileName } does not import "${ id } " from "${ modName } " for replacement` ,
190
282
)
@@ -198,17 +290,26 @@ function replaceConfusingTypeNames(
198
290
// named import cannot be replaced with `Foo as Namespace.Foo`, so we
199
291
// pre-emptively remove the whole named import
200
292
if ( betterId . includes ( '.' ) ) {
201
- code = code . replace (
293
+ chunk . code = chunk . code . replace (
202
294
new RegExp ( `\\b\\w+\\b as ${ regexEscapedId } ,?\\s?` ) ,
203
295
'' ,
204
296
)
205
297
}
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
+ )
207
302
}
208
303
}
209
304
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 ) ,
212
313
)
213
314
if ( unreplacedIds . length ) {
214
315
const unreplacedStr = unreplacedIds . map ( ( id ) => `\n- ${ id } ` ) . join ( '' )
@@ -217,23 +318,29 @@ function replaceConfusingTypeNames(
217
318
)
218
319
process . exitCode = 1
219
320
}
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
+ }
222
332
}
223
333
224
334
/**
225
335
* While we already enable `compilerOptions.stripInternal`, some internal comments
226
336
* like internal parameters are still not stripped by TypeScript, so we run another
227
337
* pass here.
228
338
*/
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 , {
237
344
plugins : [ 'typescript' ] ,
238
345
sourceType : 'module' ,
239
346
} )
@@ -246,15 +353,13 @@ function stripInternalTypes(
246
353
} ,
247
354
} )
248
355
249
- code = s . toString ( )
356
+ chunk . code = s . toString ( )
250
357
251
- if ( code . includes ( '@internal' ) ) {
358
+ if ( chunk . code . includes ( '@internal' ) ) {
252
359
this . warn ( `${ chunk . fileName } has unhandled @internal declarations` )
253
360
process . exitCode = 1
254
361
}
255
362
}
256
-
257
- return code
258
363
}
259
364
260
365
/**
@@ -283,8 +388,8 @@ function removeInternal(s: MagicString, node: any): boolean {
283
388
return false
284
389
}
285
390
286
- function cleanUnnecessaryComments ( code : string ) {
287
- return code
391
+ function cleanUnnecessaryComments ( chunk : OutputChunk ) {
392
+ chunk . code = chunk . code
288
393
. replace ( multilineCommentsRE , ( m ) => {
289
394
return licenseCommentsRE . test ( m ) ? '' : m
290
395
} )
0 commit comments