Update tsdown & use @tsdown/css

This update simplifies the tsdown configuration, we do not need our custom plugin to minify CSS anymore (replaced by `css.minify = true`), and same for our hooks that rename the built CSS (replaced by `css.fileName`) 😍
This commit is contained in:
Hugo Alliaume
2026-03-20 08:33:35 +01:00
parent b73abcb34a
commit 13cb147add
42 changed files with 373 additions and 483 deletions

View File

@@ -52,27 +52,19 @@ async function main() {
// We force "dependencies" and "peerDependencies" to be external to avoid bundling them.
...Object.keys(packageData.dependencies || {}),
...Object.keys(packageData.peerDependencies || {}),
// The "controllers.js" is generated on-the-fly by StimulusLoaderJavaScriptCompiler
...(isStimulusBundle ? ['./controllers.js'] : []),
// The "components.js" files are generated on-the-fly by *ControllerLoaderAssetCompiler
...(isReactOrVueOrSvelte ? ['./components.js'] : []),
]);
inputFiles.forEach((file) => {
// custom handling for StimulusBundle
if (file.includes('StimulusBundle/assets/src/loader.ts')) {
external.add('./controllers.js');
}
// React, Vue, Svelte
if (file.includes('assets/src/loader.ts')) {
external.add('./components.js');
}
});
const outDir = path.join(packageRoot, 'dist');
await build({
entry: inputFiles,
outDir,
clean: true,
external: Array.from(external),
watch: isWatch,
format: 'esm',
platform: 'browser',
target: tsConfigPackage.compilerOptions.target,
@@ -80,10 +72,23 @@ async function main() {
dts: {
entry: inputFiles.filter((inputFile) => !inputFile.endsWith('.css')),
},
watch: isWatch,
...(inputCssFile
? {
css: {
minify: true,
fileName: inputCssFile
.split('/')
.pop()
.replace(/\.css$/, '.min.css'),
},
}
: {}),
// Prevent esbuild to inline relative and "external" imports (like "./components.js" for React, Vue, Svelte).
unbundle: isStimulusBundle || isReactOrVueOrSvelte,
inlineOnly: ['idiomorph'],
deps: {
neverBundle: Array.from(external),
onlyBundle: ['idiomorph'],
},
plugins: [
{
name: 'symfony-ux:clean-output',
@@ -101,62 +106,7 @@ async function main() {
return result;
},
},
{
/**
* Rolldown plugin to minify CSS files using LightningCSS.
*
* tsdown's `minify: true` only minifies JS, not CSS assets.
* We use LightningCSS to minify CSS files.
*/
name: 'symfony-ux:css-minify',
async generateBundle(_options, bundle) {
const { transform } = await import('lightningcss');
for (const [fileName, asset] of Object.entries(bundle)) {
if (asset.type !== 'asset' || !fileName.endsWith('.css')) {
continue;
}
const result = transform({
filename: fileName,
code: Buffer.from(asset.source as string),
minify: true,
});
asset.source = result.code.toString();
console.log(`[Symfony UX] Minified CSS: ${fileName}`);
}
},
},
],
hooks: {
/**
* After the build is complete, rename CSS files to .min.css and remove empty JS wrappers.
*/
'build:done': async () => {
const files = await fs.promises.readdir(outDir);
for (const file of files) {
const filePath = path.join(outDir, file);
// Rename .css to .min.css
if (file.endsWith('.css') && !file.endsWith('.min.css')) {
const newPath = filePath.replace(/\.css$/, '.min.css');
await fs.promises.rename(filePath, newPath);
console.log(`[Symfony UX] Renamed: ${file}${path.basename(newPath)}`);
try {
const jsWrapperPath = filePath.replace(/\.css$/, '.js');
await fs.promises.access(jsWrapperPath);
await fs.promises.unlink(jsWrapperPath);
console.log(`[Symfony UX] Removed JS wrapper: ${path.basename(jsWrapperPath)}`);
} catch (err) {
// No JS wrapper found, nothing to remove
}
}
}
},
},
});
}

View File

@@ -21,13 +21,13 @@
"@puppeteer/browsers": "^2.13.0",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@tsdown/css": "^0.21.4",
"@types/node": "^22.19.11",
"lightningcss": "^1.31.1",
"oxfmt": "^0.41.0",
"oxlint": "^1.56.0",
"pkg-types": "^2.3.0",
"tinyglobby": "^0.2.15",
"tsdown": "^0.20.3",
"tsdown": "^0.21.4",
"vitest": "^4.1.0"
}
}

684
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -335,4 +335,4 @@ _Class.values = {
tomSelectOptions: Object,
preload: String
};
export { _Class as default };
export { _Class as default };

View File

@@ -60,4 +60,4 @@ var _Class = class extends Controller {
}
};
_Class.values = { view: Object };
export { _Class as default };
export { _Class as default };

View File

@@ -34,4 +34,4 @@ CropperController.values = {
publicUrl: String,
options: Object
};
export { CropperController as default };
export { CropperController as default };

View File

@@ -1 +1 @@
.cropperjs-image{max-width:100%}
.cropperjs-image{max-width:100%}

View File

@@ -78,4 +78,4 @@ _Class.targets = [
"previewFilename",
"previewImage"
];
export { _Class as default };
export { _Class as default };

View File

@@ -1 +1 @@
.dropzone-container{border:2px dashed #bbb;align-items:center;min-height:100px;padding:20px 10px;display:flex;position:relative}.dropzone-input{opacity:0;cursor:pointer;z-index:1;width:100%;height:100%;display:block;position:absolute;top:0;left:0}.dropzone-preview{align-items:center;max-width:100%;display:flex}.dropzone-preview-image{background-position:50%;background-repeat:no-repeat;background-size:contain;flex-basis:0;min-width:50px;max-width:50px;height:50px;margin-right:10px}.dropzone-preview-filename{word-wrap:anywhere}.dropzone-preview-button{z-index:1;width:auto;color:inherit;font:inherit;-webkit-font-smoothing:inherit;-moz-osx-font-smoothing:inherit;-webkit-appearance:none;background:0 0;border:none;margin:0;padding:0;line-height:normal;position:absolute;top:0;right:0;overflow:visible}.dropzone-preview-button:before{content:"×";cursor:pointer;padding:3px 7px}.dropzone-placeholder{text-align:center;color:#999;flex-grow:1}
.dropzone-container{border:2px dashed #bbb;align-items:center;min-height:100px;padding:20px 10px;display:flex;position:relative}.dropzone-input{opacity:0;cursor:pointer;z-index:1;width:100%;height:100%;display:block;position:absolute;top:0;left:0}.dropzone-preview{align-items:center;max-width:100%;display:flex}.dropzone-preview-image{background-position:50%;background-repeat:no-repeat;background-size:contain;flex-basis:0;min-width:50px;max-width:50px;height:50px;margin-right:10px}.dropzone-preview-filename{word-wrap:anywhere}.dropzone-preview-button{z-index:1;width:auto;color:inherit;font:inherit;-webkit-font-smoothing:inherit;-moz-osx-font-smoothing:inherit;-webkit-appearance:none;background:0 0;border:none;margin:0;padding:0;line-height:normal;position:absolute;top:0;right:0;overflow:visible}.dropzone-preview-button:before{content:"×";cursor:pointer;padding:3px 7px}.dropzone-placeholder{text-align:center;color:#999;flex-grow:1}

View File

@@ -30,4 +30,4 @@ _Class.values = {
src: String,
srcset: Object
};
export { _Class as default };
export { _Class as default };

View File

@@ -1 +1 @@
[data-loading=""],[data-loading=show],[data-loading*=\|show]{display:none}
[data-loading=""],[data-loading=show],[data-loading*=\|show]{display:none}

View File

@@ -2337,4 +2337,4 @@ LiveControllerDefault.values = {
}
};
LiveControllerDefault.backendFactory = (controller) => new Backend_default(controller.urlValue, controller.requestMethodValue, controller.fetchCredentialsValue);
export { Component, LiveControllerDefault as default, getComponent };
export { Component, LiveControllerDefault as default, getComponent };

View File

@@ -142,4 +142,4 @@ _Class.values = {
options: Object,
extra: Object
};
export { IconTypes, _Class as default };
export { IconTypes, _Class as default };

View File

@@ -400,4 +400,4 @@ var map_controller_default = class extends _Class {
});
}
};
export { map_controller_default as default };
export { map_controller_default as default };

View File

@@ -1,5 +1,4 @@
import { Controller } from "@hotwired/stimulus";
import "leaflet/dist/leaflet.min.css";
import * as L from "leaflet";
import { CircleOptions, ControlPosition, MapOptions, MarkerOptions, PolylineOptions, PopupOptions } from "leaflet";
type Point = {

View File

@@ -350,4 +350,4 @@ var map_controller_default = class extends _Class {
});
}
};
export { map_controller_default as default };
export { map_controller_default as default };

View File

@@ -60,4 +60,4 @@ _Class.values = {
hub: String,
topics: Array
};
export { _Class as default };
export { _Class as default };

View File

@@ -1,2 +1,2 @@
const components = {};
export { components };
export { components };

View File

@@ -1,5 +1,5 @@
import { ComponentCollection } from "./components.js";
import { ComponentClass, FunctionComponent } from "react";
import { ComponentCollection } from "./components.js";
type Component = string | FunctionComponent<object> | ComponentClass<object, any>;
declare global {
function resolveReactComponent(name: string): Component;

View File

@@ -9,4 +9,4 @@ function registerReactControllerComponents(reactComponents = components) {
return component;
};
}
export { registerReactControllerComponents };
export { registerReactControllerComponents };

View File

@@ -17,4 +17,4 @@ function registerReactControllerComponents(context) {
return component;
};
}
export { registerReactControllerComponents };
export { registerReactControllerComponents };

View File

@@ -45,4 +45,4 @@ _Class.values = {
default: false
}
};
export { _Class as default };
export { _Class as default };

View File

@@ -1,4 +1,4 @@
const eagerControllers = {};
const lazyControllers = {};
const isApplicationDebug = false;
export { eagerControllers, isApplicationDebug, lazyControllers };
export { eagerControllers, isApplicationDebug, lazyControllers };

View File

@@ -1,5 +1,5 @@
import { EagerControllersCollection, LazyControllersCollection } from "./controllers.js";
import { Application } from "@hotwired/stimulus";
import { EagerControllersCollection, LazyControllersCollection } from "./controllers.js";
declare const loadControllers: (application: Application, eagerControllers: EagerControllersCollection, lazyControllers: LazyControllersCollection) => void;
declare const startStimulusApp: () => Application;
export { loadControllers, startStimulusApp };

View File

@@ -1,5 +1,5 @@
import { eagerControllers, isApplicationDebug, lazyControllers } from "./controllers.js";
import { Application } from "@hotwired/stimulus";
import { eagerControllers, isApplicationDebug, lazyControllers } from "./controllers.js";
const controllerAttribute = "data-controller";
const loadControllers = (application, eagerControllers, lazyControllers) => {
for (const name in eagerControllers) registerController(name, eagerControllers[name], application);
@@ -67,4 +67,4 @@ function extractControllerNamesFrom(element) {
function canRegisterController(name, application) {
return !application.router.modulesByIdentifier.has(name);
}
export { loadControllers, startStimulusApp };
export { loadControllers, startStimulusApp };

View File

@@ -1,2 +1,2 @@
const components = {};
export { components };
export { components };

View File

@@ -9,4 +9,4 @@ function registerSvelteControllerComponents(svelteComponents = components) {
return component;
};
}
export { registerSvelteControllerComponents };
export { registerSvelteControllerComponents };

View File

@@ -12,4 +12,4 @@ function registerSvelteControllerComponents(context) {
return component;
};
}
export { registerSvelteControllerComponents };
export { registerSvelteControllerComponents };

View File

@@ -43,4 +43,4 @@ _Class.values = {
props: Object,
intro: Boolean
};
export { _Class as default };
export { _Class as default };

View File

@@ -45,4 +45,4 @@ _Class.values = {
debug: Boolean,
mainElement: String
};
export { _Class as default };
export { _Class as default };

View File

@@ -67,4 +67,4 @@ _Class.values = {
},
buttonClasses: Array
};
export { _Class as default };
export { _Class as default };

View File

@@ -1 +1 @@
.toggle-password-container{position:relative}.toggle-password-icon{width:1rem;height:1rem}.toggle-password-button{background-color:#0000;border:none;flex-direction:row;place-items:center;column-gap:.25rem;height:1rem;font-size:.875rem;line-height:1.25rem;display:flex;position:absolute;top:-1.25rem;right:.5rem}
.toggle-password-container{position:relative}.toggle-password-icon{width:1rem;height:1rem}.toggle-password-button{background-color:#0000;border:none;flex-direction:row;place-items:center;column-gap:.25rem;height:1rem;font-size:.875rem;line-height:1.25rem;display:flex;position:absolute;top:-1.25rem;right:.5rem}

View File

@@ -192,4 +192,4 @@ function createTranslator({ messages, locale = getDefaultLocale(), localeFallbac
trans
};
}
export { createTranslator, getDefaultLocale };
export { createTranslator, getDefaultLocale };

View File

@@ -1,4 +1,3 @@
import { Controller } from "@hotwired/stimulus";
import "@hotwired/turbo";
declare class export_default extends Controller {}
export { export_default as default };

View File

@@ -1,4 +1,4 @@
import { Controller } from "@hotwired/stimulus";
import "@hotwired/turbo";
var turbo_controller_default = class extends Controller {};
export { turbo_controller_default as default };
export { turbo_controller_default as default };

View File

@@ -32,4 +32,4 @@ _Class.values = {
hub: String,
withCredentials: Boolean
};
export { _Class as default };
export { _Class as default };

View File

@@ -86,4 +86,4 @@ _Class.values = {
default: "html"
}
};
export { _Class as default };
export { _Class as default };

View File

@@ -1,2 +1,2 @@
const components = {};
export { components };
export { components };

View File

@@ -1,5 +1,5 @@
import { ComponentCollection } from "./components.js";
import { Component } from "vue";
import { ComponentCollection } from "./components.js";
declare global {
function resolveVueComponent(name: string): Component;
interface Window {

View File

@@ -12,4 +12,4 @@ function registerVueControllerComponents(vueControllers = components) {
return loadComponent(name);
};
}
export { registerVueControllerComponents };
export { registerVueControllerComponents };

View File

@@ -27,4 +27,4 @@ function registerVueControllerComponents(context) {
return loadComponent(name);
};
}
export { registerVueControllerComponents };
export { registerVueControllerComponents };

View File

@@ -41,4 +41,4 @@ _Class.values = {
component: String,
props: Object
};
export { _Class as default };
export { _Class as default };