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
- Use a relative path with no leading slash, scheme, ?, or # — e.g. "index.js".
- Vendor any remote asset into the plugin's console/ directory and reference it locally.
- 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
- Use relative paths only — no leading slash, scheme, query, or fragment.
- Vendor remote assets locally instead of URL-referencing them.
- Configure the bundler's publicPath to '' so it emits relative paths.
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
- ESM UI provider output must contain at most one entry styles
- ESM provider manifest entry is required.
- ESM provider manifest style must be a string.
- Provider resource path must be provider-root-relative: ${res
- Provider resource path escapes its root: ${resourcePath}.
AI-assisted analysis of halo-dev/halo@d2f5165f9c (2026-08-14).
Data as JSON: /api/errors/c38cb4ddde2b94b5.
Report an issue: GitHub.