@@ -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 } " directory...` ) ;
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' ) } .min.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 } " (directory "${ packageRoot } ").\nEnsure you have at least a file matching the pattern "src/*_controller.ts", or manually specify input files in "${ import . meta. filename } " file.`
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' : '' } files modifications in "${ srcDir } " directory...`
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 } package...` ) ;
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 } " does not contain a valid output configuration.`
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 ) } seconds.` ) ;
147- }
151+ } ) . catch ( ( error ) => {
152+ console . error ( 'Error during build:' , error ) ;
153+ process . exit ( 1 ) ;
154+ } ) ;
148155}
149156
150157main ( ) ;
0 commit comments