halo-dev/halo · error · Error

ESM UI provider output must contain at most one entry styles

Error message

ESM UI provider output must contain at most one entry stylesheet.

What it means

Thrown in generateBundle when the single ESM entry chunk imports more than one CSS stylesheet (viteMetadata.importedCss has 2+ entries). The ESM provider manifest supports at most one entry style (the 'style' field), so multiple stylesheets cannot be represented; emitting a manifest that silently drops styles would break the plugin's UI.

Source

Thrown at ui/packages/ui-plugin-bundler-kit/src/vite-esm.ts:55

        const chunks = Object.values(bundle).filter(
          (item) => item.type === "chunk"
        );
        for (const chunk of chunks) {
          await validator.validateSource(chunk.code, chunk.fileName);
        }

        const entries = chunks.filter((chunk) => chunk.isEntry);
        if (entries.length !== 1 || !entries[0].exports.includes("default")) {
          throw new Error(
            "ESM UI provider output must contain one entry with a default PluginModule export."
          );
        }
        const entryStyles = [
          ...((entries[0] as ViteOutputChunkMetadata).viteMetadata
            ?.importedCss || []),
        ].sort();
        if (entryStyles.length > 1) {
          throw new Error(
            "ESM UI provider output must contain at most one entry stylesheet."
          );
        }
        const manifest = validateEsmProviderManifest({
          format: "esm",
          entry: `./${entries[0].fileName}`,
          ...(entryStyles[0] ? { style: `./${entryStyles[0]}` } : {}),
        });
        this.emitFile({
          type: "asset",
          fileName: ESM_PROVIDER_MANIFEST,
          source: `${JSON.stringify(manifest, null, 2)}\n`,
        });

        const report = validator.getBuildReport();
        resolvedConfig.logger.info(report.summary);
        if (report.warning) {
          resolvedConfig.logger.warn(report.warning);

View on GitHub (pinned to d2f5165f9c)

Solutions

  1. Consolidate all entry CSS into a single file (e.g. one main.css that @imports or @layers the others) and import only that from the entry.
  2. Remove the second static CSS import from the entry; load peripheral styles lazily if needed.
  3. If a dependency injects CSS, configure Vite to inline/merge CSS for the ESM build (build.cssCodeSplit: false) so only one stylesheet reaches the entry.

Example fix

// before (entry.ts)
import "./styles/base.css";
import "./styles/theme.css";

// after
import "./styles/index.css"; // index.css contains both via @import
Defensive patterns

Strategy: validation

Validate before calling

// Pre-build: ensure the entry imports at most one CSS file.
import { readFileSync } from "node:fs";
const entry = readFileSync("src/index.ts", "utf8");
const cssImports = entry.match(/^\s*import\s+["'].*\.css["']/gm) ?? [];
if (cssImports.length > 1) {
  throw new Error(`Entry imports ${cssImports.length} CSS files; consolidate into one.`);
}

Type guard

function entryHasAtMostOneStyle(importedCss: Iterable<string> | undefined): boolean {
  const count = importedCss ? [...importedCss].length : 0;
  return count <= 1;
}

Prevention

When it happens

Trigger: The entry module statically imports two or more distinct .css files (e.g. `import './a.css'` and `import './b.css'`), or imports a CSS file plus a component library that ships its own CSS as a separate chunk import.

Common situations: Importing Tailwind directives plus a third-party component CSS; an entry that imports two style files for theming; a CSS split caused by async chunking that Vite attributes to the entry's importedCss set.

Related errors


AI-assisted analysis of halo-dev/halo@d2f5165f9c (2026-08-14). Data as JSON: /api/errors/95e0c78db615a7b9. Report an issue: GitHub.