build(editor): Ship compiled theme CSS and an SCSS passthrough in the design-system dist (no-changelog) (#35452)

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
Alex Grozav
2026-08-04 14:29:49 +00:00
committed by GitHub
co-authored by Claude multica-agent
parent 6d0c0e8572
commit a8bc37804e
2 changed files with 79 additions and 2 deletions
@@ -6,7 +6,9 @@ storybook-static
.storybook
.browserslistrc
vite.config.ts
# Stale as `vite.config.ts` since the rename to .mts, so both build configs were
# shipping to consumers. Glob covers vite.config.mts and vite.config.styles.mts.
vite.config.*
*.md
*.stories.js
@@ -1,6 +1,7 @@
import vue from '@vitejs/plugin-vue';
import { cpSync } from 'node:fs';
import { resolve } from 'path';
import { defineConfig, mergeConfig } from 'vite';
import { build, defineConfig, mergeConfig, type InlineConfig, type Plugin } from 'vite';
import icons from 'unplugin-icons/vite';
import dts from 'vite-plugin-dts';
import { vitestConfig } from '@n8n/vitest-config/frontend';
@@ -8,6 +9,72 @@ import svgLoader from 'vite-svg-loader';
import { lucideIconsPlugin } from './src/icons/lucide/vite';
const packagesDir = resolve(__dirname, '..', '..', '..');
const srcDir = resolve(__dirname, 'src');
const distDir = resolve(__dirname, 'dist');
/** Emit stylesheets at the dist root, everything else under `assets/`. */
const assetFileNames = (name: string) => (asset: { names?: string[] }) =>
asset.names?.[0]?.endsWith('.css') ? name : 'assets/[name][extname]';
/**
* Ship the SCSS sources alongside the compiled CSS so consumers with a sass
* toolchain can `@use` our mixins and token maps from their own stylesheets.
*
* The copy keeps the `css/` folder name on purpose: internal code already writes
* `@n8n/design-system/css/mixins/motion` (49 call sites, resolved through an
* alias to `src/css/`), so `dist/css` is the target that lets those specifiers
* read identically inside and outside the monorepo.
*
* `dist/css/` sits two levels below the package root, exactly like `src/css/`,
* so the `url('../../assets/fonts/…')` in `fonts.scss` and `icon.scss` keeps
* resolving without rewriting a single path.
*/
function copyScssSources(): Plugin {
return {
name: 'n8n:copy-scss-sources',
closeBundle() {
cpSync(resolve(srcDir, 'css'), resolve(distDir, 'css'), {
recursive: true,
// The stylesheets ship; the test that asserts on their selector order does not.
filter: (source) => !source.endsWith('.test.ts'),
});
cpSync(resolve(srcDir, 'utils.scss'), resolve(distDir, 'css', 'utils.scss'));
},
};
}
/**
* Second pass over `dist`, run from `builder.buildApp` below.
*
* `src/css/index.scss` is the theme every consumer needs — design tokens,
* `@font-face` rules, the reset and the element-plus overrides — and no Vue SFC
* imports it, so the library pass never emits it. Without it the shipped
* component CSS references `var(--…)` tokens that are never defined.
*
* It cannot ride the library pass: `build.lib.entry` takes JS modules, and a
* second entry would force code splitting, which the UMD output cannot do. So it
* gets its own pass — in this file, off the same `vite build`.
*/
const themeBuild: InlineConfig = {
// This file is the config; loading it again here would recurse.
configFile: false,
root: __dirname,
plugins: [copyScssSources()],
// Font `url()`s emit relative to the stylesheet rather than Vite's default
// absolute `/assets/…`, which 404s wherever the app is not served from the web
// root. Scoped to this pass — the library pass emits no assets at all.
base: './',
build: {
// The library pass owns dist; this one only adds to it.
emptyOutDir: false,
outDir: 'dist',
cssCodeSplit: true,
rollupOptions: {
input: { theme: resolve(srcDir, 'css', 'index.scss') },
output: { assetFileNames: assetFileNames('[name][extname]') },
},
},
};
export default mergeConfig(
defineConfig({
@@ -83,9 +150,17 @@ export default mergeConfig(
globals: {
vue: 'Vue',
},
assetFileNames: assetFileNames('style.css'),
},
},
},
builder: {
// Library first — it owns dist and empties it. Theme second, additive.
buildApp: async (builder) => {
await builder.build(builder.environments.client);
await build(themeBuild);
},
},
}),
vitestConfig,
);