cli: Integrate CSS bundling as a Rollup plugin

Instead of having a separate post-build step for CSS entry points,
the CSS bundling is now integrated directly into the Rollup build
as a plugin. This is cleaner and will enable better watch mode
support in the future.

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Patrik Oldsberg
2026-02-09 17:30:35 +01:00
parent a1c11eef03
commit cad0b68d9c
4 changed files with 47 additions and 75 deletions
@@ -37,7 +37,7 @@ import {
OutputPlugin,
} from 'rollup';
import { forwardFileImports } from './plugins';
import { forwardFileImports, cssEntryPoints } from './plugins';
import { BuildOptions, Output } from './types';
import { paths } from '../../../../lib/paths';
import { BackstagePackageJson } from '@backstage/cli-node';
@@ -275,6 +275,7 @@ export async function makeRollupConfigs(
target: 'ES2023',
minify: options.minify,
}),
cssEntryPoints({ entryPoints, targetDir }),
],
});
}
@@ -1,66 +0,0 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import fs from 'fs-extra';
import postcss from 'postcss';
// @ts-expect-error - no types available
import postcssImport from 'postcss-import';
import { resolve as resolvePath, dirname } from 'node:path';
import { EntryPoint } from '../../../../lib/entryPoints';
/**
* Bundles a CSS entry point, resolving @import statements.
*/
export async function buildCSSEntryPoint(
entryPoint: EntryPoint,
targetDir: string,
): Promise<void> {
const sourcePath = resolvePath(targetDir, entryPoint.path);
// Convert src/ path to dist/ path
const outputPath = resolvePath(
targetDir,
entryPoint.path.replace(/^(\.\/)?src\//, 'dist/'),
);
// Read source CSS
const source = await fs.readFile(sourcePath, 'utf8');
// Bundle @import statements using postcss-import
const result = await postcss([postcssImport()]).process(source, {
from: sourcePath,
to: outputPath,
});
// Ensure output directory exists
await fs.mkdir(dirname(outputPath), { recursive: true });
// Write the bundled CSS
await fs.writeFile(outputPath, result.css);
}
/**
* Builds all CSS entry points for a package.
*/
export async function buildCSSEntryPoints(
entryPoints: EntryPoint[],
targetDir: string,
): Promise<void> {
const cssEntryPoints = entryPoints.filter(ep => ep.ext === '.css');
for (const entryPoint of cssEntryPoints) {
await buildCSSEntryPoint(entryPoint, targetDir);
}
}
@@ -23,8 +23,6 @@ import { makeRollupConfigs } from './config';
import { BuildOptions, Output } from './types';
import { PackageRoles } from '@backstage/cli-node';
import { runParallelWorkers } from '../../../../lib/parallel';
import { buildCSSEntryPoints } from './css';
import { readEntryPoints } from '../../../../lib/entryPoints';
export function formatErrorMessage(error: any) {
let msg = '';
@@ -115,12 +113,6 @@ export const buildPackage = async (options: BuildOptions) => {
const buildTasks = rollupConfigs.map(rollupBuild);
await Promise.all(buildTasks);
// Build CSS entry points after the main build
if (options.packageJson) {
const entryPoints = readEntryPoints(options.packageJson);
await buildCSSEntryPoints(entryPoints, targetDir);
}
};
export const buildPackages = async (options: BuildOptions[]) => {
@@ -15,6 +15,9 @@
*/
import fs from 'fs-extra';
import postcss from 'postcss';
// @ts-expect-error - no types available
import postcssImport from 'postcss-import';
import {
dirname,
resolve as resolvePath,
@@ -27,6 +30,7 @@ import {
OutputChunk,
HasModuleSideEffects,
} from 'rollup';
import { EntryPoint } from '../../../../lib/entryPoints';
type ForwardFileImportsOptions = {
include: Array<string | RegExp> | string | RegExp | null;
@@ -169,3 +173,44 @@ export function forwardFileImports(options: ForwardFileImportsOptions) {
},
} satisfies Plugin;
}
interface CssEntryPointsOptions {
entryPoints: EntryPoint[];
targetDir: string;
}
/**
* Rollup plugin that bundles CSS entry points using postcss-import.
* CSS files declared in package.json exports are processed and emitted
* as part of the Rollup bundle.
*/
export function cssEntryPoints(options: CssEntryPointsOptions): Plugin {
const cssEntries = options.entryPoints.filter(ep => ep.ext === '.css');
return {
name: 'backstage-css-entry-points',
async generateBundle() {
for (const entryPoint of cssEntries) {
const sourcePath = resolvePath(options.targetDir, entryPoint.path);
// Convert src/ path to dist/ path for output filename
const outputPath = entryPoint.path.replace(/^(\.\/)?src\//, '');
// Read source CSS
const source = await fs.readFile(sourcePath, 'utf8');
// Bundle @import statements using postcss-import
const result = await postcss([postcssImport()]).process(source, {
from: sourcePath,
});
// Emit the bundled CSS as an asset
this.emitFile({
type: 'asset',
fileName: outputPath,
source: result.css,
});
}
},
};
}