55import * as fs from 'node:fs' ;
66import * as path from 'node:path' ;
77import { parseArgs } from 'node:util' ;
8- import * as LightningCSS from 'lightningcss' ;
9- import * as rollup from 'rollup' ;
108import { globSync } from 'tinyglobby' ;
11- import { getRollupConfiguration } from './rollup.ts' ;
9+ import { build } from 'tsup' ;
10+ import { readPackageJSON } from "pkg-types" ;
1211
1312const args = parseArgs ( {
1413 allowPositionals : true ,
@@ -34,117 +33,107 @@ async function main() {
3433 process . exit ( 1 ) ;
3534 }
3635
37- const packageData = await import ( path . join ( packageRoot , 'package.json' ) , { with : { type : 'json' } } ) ;
38- const packageName = packageData . name ;
39- const srcDir = path . join ( packageRoot , 'src' ) ;
40- const distDir = path . join ( packageRoot , 'dist' ) ;
36+ const packageData = await readPackageJSON ( path . join ( packageRoot , 'package.json' ) ) ;
4137
42- if ( ! fs . existsSync ( srcDir ) ) {
43- console . error ( `The package directory "${ packageRoot } " does not contain a "src" directory.` ) ;
44- process . exit ( 1 ) ;
45- }
46-
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 = [
54- ...globSync ( path . join ( srcDir , '*controller.ts' ) ) ,
55- ...( [ '@symfony/ux-react' , '@symfony/ux-vue' , '@symfony/ux-svelte' ] . includes ( packageName )
56- ? [ path . join ( srcDir , 'loader.ts' ) , path . join ( srcDir , 'components.ts' ) ]
38+ const inputCssFile = packageData ?. config ?. css_source ;
39+ const inputFiles = [
40+ ...globSync ( 'src/*controller.ts' ) ,
41+ ...( [ '@symfony/ux-react' , '@symfony/ux-vue' , '@symfony/ux-svelte' ] . includes ( packageData . name )
42+ ? [ 'src/loader.ts' , 'src/components.ts' ]
5743 : [ ] ) ,
58- ...( packageName === '@symfony/stimulus-bundle'
59- ? [ path . join ( srcDir , ' loader.ts') , path . join ( srcDir , ' controllers.ts') ]
44+ ...( packageData . name === '@symfony/stimulus-bundle'
45+ ? [ 'src/ loader.ts', 'src/ controllers.ts']
6046 : [ ] ) ,
47+ ...( inputCssFile ? [ inputCssFile ] : [ ] ) ,
6148 ] ;
6249
63- const inputStyleFile = packageData . config ?. css_source ;
64- const buildCss = async ( ) => {
65- if ( ! inputStyleFile ) {
66- return ;
50+ const external = new Set ( [
51+ // We force "dependencies" and "peerDependencies" to be external to avoid bundling them.
52+ ...Object . keys ( packageData . dependencies || { } ) ,
53+ ...Object . keys ( packageData . peerDependencies || { } ) ,
54+ ] ) ;
55+
56+ inputFiles . forEach ( ( file ) => {
57+ // custom handling for StimulusBundle
58+ if ( file . includes ( 'StimulusBundle/assets/src/loader.ts' ) ) {
59+ external . add ( './controllers.js' ) ;
6760 }
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- }
8761
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- ] ,
62+ // React, Vue, Svelte
63+ if ( file . includes ( 'assets/src/loader.ts' ) ) {
64+ external . add ( './components.js' ) ;
65+ }
10466 } ) ;
10567
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 ( ) ;
68+ await build ( {
69+ entry : inputFiles ,
70+ outDir : path . join ( packageRoot , 'dist' ) ,
71+ clean : true ,
72+ external : Array . from ( external ) ,
73+ format : 'esm' ,
74+ platform : 'browser' ,
75+ tsconfig : path . join ( import . meta. dirname , '../tsconfig.packages.json' ) ,
76+ dts : {
77+ entry : inputFiles . filter ( inputFile => ! inputFile . endsWith ( '.css' ) ) ,
78+ } ,
79+ watch : isWatch ,
80+ splitting : false ,
81+ esbuildOptions ( options ) {
82+ // Disabling `bundle` option prevent esbuild to inline relative (but external) imports (like "./components.js" for React, Vue, Svelte).
83+ options . bundle = false ;
84+ } ,
85+ plugins : [
86+ {
87+ /**
88+ * This plugin is used to minify CSS files using LightningCSS.
89+ *
90+ * Even if tsup supports CSS minification through ESBuild by setting the `minify: true` option,
91+ * it also minifies JS files but we don't want that.
92+ */
93+ name : 'symfony-ux:minify-css' ,
94+ async renderChunk ( code , chunkInfo ) {
95+ if ( ! / \. c s s $ / . test ( chunkInfo . path ) ) {
96+ return null ;
97+ }
98+
99+ const { transform } = await import ( 'lightningcss' ) ;
100+ const result = transform ( {
101+ filename : chunkInfo . path ,
102+ code : Buffer . from ( code ) ,
103+ minify : true ,
104+ } ) ;
105+
106+ console . log ( `[Symfony UX] Minified CSS file: ${ chunkInfo . path } ` ) ;
107+
108+ return {
109+ code : result . code . toString ( ) ,
110+ map : result . map ? result . map . toString ( ) : null ,
111+ }
112+ } ,
113+ } ,
114+
115+ /**
116+ * Unlike tsdown/rolldown and the option "cssEntryFileNames", tsup does not support
117+ * customizing the output file names for CSS files.
118+ * A plugin is needed to rename the written CSS files to add the ".min" suffix.
119+ */
120+ {
121+ name : 'symfony-ux:append-min-to-css' ,
122+ async buildEnd ( { writtenFiles } ) {
123+ for ( const writtenFile of writtenFiles ) {
124+ if ( ! writtenFile . name . endsWith ( '.css' ) ) {
125+ continue ;
126+ }
127+
128+ const newName = writtenFile . name . replace ( / \. c s s $ / , '.min.css' ) ;
129+ await fs . promises . rename ( writtenFile . name , newName ) ;
130+
131+ console . info ( `[Symfony UX] Renamed ${ writtenFile . name } to ${ newName } ` ) ;
132+ }
133+ }
128134 }
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 ) ;
139- }
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- }
135+ ] ,
136+ } ) ;
148137}
149138
150139main ( ) ;
0 commit comments