11import { readFileSync } from 'node:fs'
22import { 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'
813import { walk } from 'estree-walker'
914import MagicString from 'magic-string'
15+ import type {
16+ Directive ,
17+ ModuleExportName ,
18+ Program ,
19+ Statement ,
20+ } from '@oxc-project/types'
1021
1122const depTypesDir = new URL ( './src/types/' , import . meta. url )
1223const 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 */
4859const 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>> = {
74111function 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 */
164260function 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