halo-dev/halo · warning · Error

ESM UI provider output must expose a default PluginModule ex

Error message

ESM UI provider output must expose a default PluginModule export.

What it means

normalizeResourcePath rejects resource paths that are not provider-root-relative: a leading slash (absolute), a leading //, a '?' query, a '#' fragment, or a scheme (matched by ^[a-zA-Z][a-zA-Z0-9+.-]*:.*). The offending raw path is included in the message; the exception becomes ClassifiedProvider.invalid upstream.

Source

Thrown at ui/packages/ui-plugin-bundler-kit/src/rsbuild-esm.ts:69

          if (entryScripts.length !== 1) {
            throw new Error(
              "ESM UI provider output must contain exactly one entry JavaScript file."
            );
          }
          const entryFile = entryScripts[0];
          const entry = assets[entryFile];
          if (!entry) {
            throw new Error(
              `ESM UI provider output is missing its entry asset ${entryFile}.`
            );
          }
          const entryCode = entry.source().toString();
          await validator.validateSource(entryCode, entryFile);
          if (
            !/\bexport\s+default\b/.test(entryCode) &&
            !/\bexport\s*\{[^}]*\bdefault\b[^}]*\}/s.test(entryCode)
          ) {
            throw new Error(
              "ESM UI provider output must expose a default PluginModule export."
            );
          }
          const entryStyles =
            compilation.entrypoints
              .get("main")
              ?.getFiles()
              .filter((fileName) => fileName.endsWith(".css")) || [];
          if (entryStyles.length > 1) {
            throw new Error(
              "ESM UI provider output must contain at most one entry stylesheet."
            );
          }
          const manifest = validateEsmProviderManifest({
            format: "esm",
            entry: `./${entryFile}`,
            ...(entryStyles[0] ? { style: `./${entryStyles[0]}` } : {}),
          });

View on GitHub (pinned to d2f5165f9c)

Solutions

  1. Use a relative path with no leading slash, scheme, ?, or # — e.g. "index.js".
  2. Vendor any remote asset into the plugin's console/ directory and reference it locally.
  3. Remove leading slashes produced by the bundler's publicPath setting.

Example fix

// before
{
  "format": "esm",
  "entry": "/assets/index.js"
}
// after
{
  "format": "esm",
  "entry": "index.js"
}
Defensive patterns

Strategy: validation

Validate before calling

String slashed = entry.replace('\\', '/');
if (slashed.startsWith("/") || slashed.contains("?") || slashed.contains("#")
        || slashed.matches("^[a-zA-Z][a-zA-Z0-9+.-]*:.*")) {
    throw new IllegalStateException("entry must be provider-root-relative: " + entry);
}

Prevention

When it happens

Trigger: ui-plugin.json's entry/style is absolute ("/index.js"), protocol-prefixed ("https://cdn/x.js"), or carries query/fragment markers.

Common situations: Author copies a full URL or absolute path expecting it to load externally; bundler emits a leading slash; CDN reference pasted by mistake.

Related errors


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