@@ -6,9 +6,8 @@ import * as fs from 'node:fs';
66import  *  as  path  from  'node:path' ; 
77import  {  parseArgs  }  from  'node:util' ; 
88import  *  as  LightningCSS  from  'lightningcss' ; 
9- import  *  as  rollup  from  'rollup' ; 
109import  {  globSync  }  from  'tinyglobby' ; 
11- import  {  getRollupConfiguration  }  from  './rollup.ts ' ; 
10+ import  {  build  }  from  'tsdown ' ; 
1211
1312const  args  =  parseArgs ( { 
1413    allowPositionals : true , 
@@ -34,7 +33,7 @@ async function main() {
3433        process . exit ( 1 ) ; 
3534    } 
3635
37-     const  packageData  =  await  import ( path . join ( packageRoot ,  'package.json' ) ,  { with : {  type : 'json' } } ) ; 
36+     const  packageData  =  await  import ( path . join ( packageRoot ,  'package.json' ) ,  {   with : {  type : 'json'   }   } ) ; 
3837    const  packageName  =  packageData . name ; 
3938    const  srcDir  =  path . join ( packageRoot ,  'src' ) ; 
4039    const  distDir  =  path . join ( packageRoot ,  'dist' ) ; 
@@ -44,107 +43,115 @@ async function main() {
4443        process . exit ( 1 ) ; 
4544    } 
4645
47-     if  ( fs . existsSync ( distDir ) )  { 
48-         console . log ( `Cleaning up the "${ distDir }  ) ; 
49-         await  fs . promises . rm ( distDir ,  {  recursive : true  } ) ; 
50-         await  fs . promises . mkdir ( distDir ) ; 
51-     } 
52- 
53-     const  inputScriptFiles  =  [ 
46+     const  inputFiles  =  [ 
5447        ...globSync ( path . join ( srcDir ,  '*controller.ts' ) ) , 
5548        ...( [ '@symfony/ux-react' ,  '@symfony/ux-vue' ,  '@symfony/ux-svelte' ] . includes ( packageName ) 
5649            ? [ path . join ( srcDir ,  'loader.ts' ) ,  path . join ( srcDir ,  'components.ts' ) ] 
5750            : [ ] ) , 
5851        ...( packageName  ===  '@symfony/stimulus-bundle' 
5952            ? [ path . join ( srcDir ,  'loader.ts' ) ,  path . join ( srcDir ,  'controllers.ts' ) ] 
6053            : [ ] ) , 
54+         ...( packageData ?. config ?. css_source  ? [ packageData . config . css_source ]  : [ ] ) , 
55+     ] ; 
56+ 
57+     const  peerDependencies  =  [ 
58+         '@hotwired/stimulus' , 
59+         ...( packageData . peerDependencies  ? Object . keys ( packageData . peerDependencies )  : [ ] ) , 
6160    ] ; 
6261
63-     const   inputStyleFile   =   packageData . config ?. css_source ; 
64-     const   buildCss   =   async   ( )   =>   { 
65-         if  ( ! inputStyleFile )  { 
66-             return ; 
62+     inputFiles . forEach ( ( file )   =>   { 
63+          // custom handling for StimulusBundle 
64+         if  ( file . includes ( 'StimulusBundle/assets/src/loader.ts' ) )  { 
65+             peerDependencies . push ( './controllers.js' ) ; 
6766        } 
68-         const  inputStyleFileDist  =  path . resolve ( distDir ,  `${ path . basename ( inputStyleFile ,  '.css' ) }  ) ; 
69- 
70-         console . log ( 'Minifying CSS...' ) ; 
71-         const  css  =  await  fs . promises . readFile ( inputStyleFile ,  'utf-8' ) ; 
72-         const  {  code : minified  }  =  LightningCSS . transform ( { 
73-             filename : path . basename ( inputStyleFile ,  '.css' ) , 
74-             code : Buffer . from ( css ) , 
75-             minify : true , 
76-             sourceMap : false ,  // TODO: Maybe we can add source maps later? :) 
77-         } ) ; 
78-         await  fs . promises . writeFile ( inputStyleFileDist ,  minified ) ; 
79-     } ; 
80- 
81-     if  ( inputScriptFiles . length  ===  0 )  { 
82-         console . error ( 
83-             `No input files found for package "${ packageName } ${ packageRoot } ${ import . meta. filename }  
84-         ) ; 
85-         process . exit ( 1 ) ; 
86-     } 
8767
88-     const  rollupConfig  =  getRollupConfiguration ( { 
89-         packageRoot, 
90-         inputFiles : inputScriptFiles , 
91-         isWatch, 
92-         additionalPlugins : [ 
93-             ...( isWatch  &&  inputStyleFile 
94-                 ? [ 
95-                       { 
96-                           name : 'watcher' , 
97-                           buildStart ( this : rollup . PluginContext )  { 
98-                               this . addWatchFile ( inputStyleFile ) ; 
99-                           } , 
100-                       } , 
101-                   ] 
102-                 : [ ] ) , 
103-         ] , 
68+         // React, Vue 
69+         if  ( file . includes ( 'assets/src/loader.ts' ) )  { 
70+             peerDependencies . push ( './components.js' ) ; 
71+         } 
10472    } ) ; 
10573
106-     if  ( isWatch )  { 
107-         console . log ( 
108-             `Watching for JavaScript${ inputStyleFile  ? ' and CSS'  : '' } ${ srcDir }  
109-         ) ; 
110- 
111-         const  watcher  =  rollup . watch ( rollupConfig ) ; 
112-         watcher . on ( 'event' ,  ( event )  =>  { 
113-             if  ( event . code  ===  'ERROR' )  { 
114-                 console . error ( 'Error during build:' ,  event . error ) ; 
115-             } 
116- 
117-             if  ( ( event . code  ===  'BUNDLE_END'  ||  event . code  ===  'ERROR' )  &&  event . result )  { 
118-                 event . result . close ( ) ; 
119-             } 
120-         } ) ; 
121-         watcher . on ( 'change' ,  async  ( id ,  {  event } )  =>  { 
122-             if  ( event  ===  'update' )  { 
123-                 console . log ( 'Files were modified, rebuilding...' ) ; 
124-             } 
125- 
126-             if  ( inputStyleFile  &&  id  ===  inputStyleFile )  { 
127-                 await  buildCss ( ) ; 
74+     build ( { 
75+         entry : inputFiles , 
76+         outDir : distDir , 
77+         clean : true , 
78+         outputOptions : { 
79+             cssEntryFileNames : '[name].min.css' , 
80+         } , 
81+         external : peerDependencies , 
82+         format : 'esm' , 
83+         platform : 'browser' , 
84+         tsconfig : path . join ( import . meta. dirname ,  '../tsconfig.packages.json' ) , 
85+         // The target should be kept in sync with `tsconfig.packages.json` file. 
86+         // In the future, I hope the target will be read from the `tsconfig.packages.json` file, but for now we need to specify it manually. 
87+         target : 'es2021' , 
88+         watch : isWatch , 
89+         plugins : [ 
90+ 
91+             /** 
92+              * Guarantees that any files imported from a peer dependency are treated as an external. 
93+              * 
94+              * For example, if we import `chart.js/auto`, that would not normally 
95+              * match the "chart.js" we pass to the "externals" config. This plugin 
96+              * catches that case and adds it as an external. 
97+              * 
98+              * Inspired by https://github.com/oat-sa/rollup-plugin-wildcard-external 
99+              */ 
100+             { 
101+                 name : 'wildcard-externals' , 
102+                 resolveId ( source : string ,  importer : string )  { 
103+                     if  ( ! importer )  { 
104+                         return  null ;  // other ids should be handled as usually 
105+                     } 
106+ 
107+                     const  matchesExternal  =  peerDependencies . some ( ( peerDependency )  =>  { 
108+                         return  source . includes ( `/${ peerDependency }  ) 
109+                     } ) ; 
110+ 
111+                     if  ( matchesExternal )  { 
112+                         return  { 
113+                             id : source , 
114+                             external : true , 
115+                             moduleSideEffects : true , 
116+                         } ; 
117+                     } 
118+ 
119+                     return  null ;  // other ids should be handled as usually 
120+                 } , 
121+             } , 
122+             // Since minifying files is not configurable per file, we need to use a custom plugin to handle CSS minification. 
123+             { 
124+                 name : 'minimize-css' , 
125+                 transform : { 
126+                     filter : { 
127+                         id : / \. c s s $ / , 
128+                     } , 
129+                     handler  ( code ,  id )  { 
130+                         const  {  code : minifiedCode  }  =  LightningCSS . transform ( { 
131+                             filename : path . basename ( id ) , 
132+                             code : Buffer . from ( code ) , 
133+                             minify : true , 
134+                             sourceMap : false , 
135+                         } ) ; 
136+ 
137+                         return  {  code : minifiedCode . toString ( ) ,  map : null  } ; 
138+                     } 
139+                 } , 
140+             } , 
141+         ] , 
142+         hooks : { 
143+             async  'build:done' ( )  { 
144+                 // TODO: Idk why, but when we build a CSS file (e.g. `style.css`), it also generate an empty JS file (e.g. `style.js`). 
145+                 if  ( packageData ?. config ?. css_source )  { 
146+                     const  unwantedJsFile  =  path . join ( distDir ,  path . basename ( packageData . config . css_source ,  '.css' )  +  '.js' ) ; 
147+                     await  fs . promises . rm ( unwantedJsFile ,  {  force : true  } ) ; 
148+                 } 
128149            } 
129-         } ) ; 
130-     }  else  { 
131-         console . log ( `Building JavaScript files from ${ packageName }  ) ; 
132-         const  start  =  Date . now ( ) ; 
133- 
134-         if  ( typeof  rollupConfig . output  ===  'undefined'  ||  Array . isArray ( rollupConfig . output ) )  { 
135-             console . error ( 
136-                 `The rollup configuration for package "${ packageName }  
137-             ) ; 
138-             process . exit ( 1 ) ; 
139150        } 
140- 
141-         const  bundle  =  await  rollup . rollup ( rollupConfig ) ; 
142-         await  bundle . write ( rollupConfig . output ) ; 
143- 
144-         await  buildCss ( ) ; 
145- 
146-         console . log ( `Done in ${ ( ( Date . now ( )  -  start )  /  1000 ) . toFixed ( 3 ) }  ) ; 
147-     } 
151+     } ) . catch ( ( error )  =>  { 
152+         console . error ( 'Error during build:' ,  error ) ; 
153+         process . exit ( 1 ) ; 
154+     } ) ; 
148155} 
149156
150157main ( ) ; 
0 commit comments