{"record":{"id":"e5101639b081ee9c","repo":"halo-dev/halo","slug":"esm-provider-manifest-style-must-be-a-string","errorCode":null,"errorMessage":"ESM provider manifest style must be a string.","messagePattern":"ESM provider manifest style must be a string\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"ui/packages/ui-plugin-bundler-kit/src/provider-manifest.ts","lineNumber":37,"sourceCode":"    !keys.includes(\"entry\") ||\n    !keys.includes(\"format\") ||\n    keys.some((key) => ![\"entry\", \"format\", \"style\"].includes(key)) ||\n    value.format !== \"esm\"\n  ) {\n    throw new Error(\n      'ESM provider manifest must contain format, entry, and optional style only with format \"esm\".'\n    );\n  }\n  if (typeof value.entry !== \"string\") {\n    throw new Error(\"ESM provider manifest entry is required.\");\n  }\n  const manifest: EsmProviderManifest = {\n    format: \"esm\",\n    entry: normalizeProviderResourcePath(value.entry),\n  };\n  if (\"style\" in value) {\n    if (typeof value.style !== \"string\") {\n      throw new Error(\"ESM provider manifest style must be a string.\");\n    }\n    manifest.style = normalizeProviderResourcePath(value.style);\n  }\n  return manifest;\n}\n\nexport function normalizeProviderResourcePath(resourcePath: string) {\n  const normalizedSlashes = resourcePath.replaceAll(\"\\\\\", \"/\");\n  if (\n    !normalizedSlashes ||\n    normalizedSlashes.startsWith(\"/\") ||\n    normalizedSlashes.startsWith(\"//\") ||\n    /^[a-zA-Z][a-zA-Z\\d+.-]*:/.test(normalizedSlashes) ||\n    normalizedSlashes.includes(\"?\") ||\n    normalizedSlashes.includes(\"#\")\n  ) {\n    throw new Error(\n      `Provider resource path must be provider-root-relative: ${resourcePath}.`","sourceCodeStart":19,"sourceCodeEnd":55,"githubUrl":"https://github.com/halo-dev/halo/blob/d2f5165f9c8f055ffcb3fa9c3f4032821a7b68c8/ui/packages/ui-plugin-bundler-kit/src/provider-manifest.ts#L19-L55","documentation":"readManifest requires the ui-plugin.json object to contain exactly format and entry, optionally style, and nothing else. This IllegalArgumentException fires when format or entry is missing, or when an unknown extra field is present. Caught upstream and surfaced as ClassifiedProvider.invalid, degrading the plugin's UI bundle.","triggerScenarios":"Shipping a console/ui-plugin.json that omits format or entry, or includes unrecognized keys (e.g. a legacy \"main\" or \"version\" field).","commonSituations":"Migrating from an older manifest schema that used different keys; copy-paste leaving a stray field; typos like \"entr\" instead of \"entry\".","solutions":["Ensure ui-plugin.json has both format and entry; add style only if needed.","Remove any field that is not format, entry, or style.","Diff against a known-good plugin's ui-plugin.json to spot stray keys."],"exampleFix":"// before\n{\n  \"format\": \"esm\",\n  \"main\": \"index.js\"\n}\n// after\n{\n  \"format\": \"esm\",\n  \"entry\": \"index.js\"\n}","handlingStrategy":"validation","validationCode":"// Enforce the allowed field set before packaging.\nSet<String> allowed = Set.of(\"format\", \"entry\", \"style\");\nSet<String> fields = new HashSet<>(manifest.propertyNames());\nif (!fields.containsAll(Set.of(\"format\", \"entry\")) || !allowed.containsAll(fields)) {\n    throw new IllegalStateException(\"ui-plugin.json must have format+entry, optional style, nothing else\");\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Keep ui-plugin.json to only format, entry, and (optional) style.","Diff new manifests against a known-good plugin.","Regenerate after schema changes instead of hand-merging."],"tags":["plugin","ui-bundle","esm","provider-manifest","validation"],"backgroundTag":null,"analyzedSha":"d2f5165f9c8f055ffcb3fa9c3f4032821a7b68c8","analyzedAt":"2026-08-14T00:18:38.915Z","schemaVersion":2},"datasetVersion":"2026-08-14T05:17:29.042Z"}