Skip to content

Commit 27e432e

Browse files
authored
fix(core): surface registry configuration errors (#3077)
* fix: surface registry configuration errors * fix: validate registry exclusion shape
1 parent dd5ef1a commit 27e432e

12 files changed

Lines changed: 403 additions & 36 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"emdash": patch
3+
"@emdash-cms/admin": patch
4+
---
5+
6+
Fixes invalid plugin registry settings causing the admin manifest to fail with a generic server error. EmDash reports malformed `experimental.registry` fields while Astro loads the site configuration. If invalid registry settings reach the runtime, the admin remains available and shows which field to correct in `astro.config.mjs`.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { Banner } from "@cloudflare/kumo";
2+
import { useLingui } from "@lingui/react/macro";
3+
4+
import type { AdminManifest } from "../lib/api/client.js";
5+
6+
type RegistryConfigurationError = NonNullable<AdminManifest["registryConfigurationError"]>;
7+
8+
export function RegistryConfigurationBanner({ error }: { error: RegistryConfigurationError }) {
9+
const { t } = useLingui();
10+
let description: string;
11+
12+
switch (error.field) {
13+
case "experimental.registry.aggregatorUrl":
14+
description = t`Check experimental.registry.aggregatorUrl in astro.config.mjs, then restart EmDash.`;
15+
break;
16+
case "experimental.registry.policy.minimumReleaseAge":
17+
description = t`Check experimental.registry.policy.minimumReleaseAge in astro.config.mjs, then restart EmDash.`;
18+
break;
19+
case "experimental.registry.policy.minimumReleaseAgeExclude":
20+
description = t`Check experimental.registry.policy.minimumReleaseAgeExclude in astro.config.mjs, then restart EmDash.`;
21+
break;
22+
default:
23+
description = t`Check experimental.registry in astro.config.mjs, then restart EmDash.`;
24+
}
25+
26+
return (
27+
<Banner
28+
variant="error"
29+
role="alert"
30+
aria-label={t`Plugin registry configuration error`}
31+
title={t`Plugin registry configuration error`}
32+
description={description}
33+
/>
34+
);
35+
}

packages/admin/src/components/Shell.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { useMatches } from "@tanstack/react-router";
22
import * as React from "react";
33

4+
import type { AdminManifest } from "../lib/api/client.js";
45
import { useCurrentUser } from "../lib/api/current-user";
56
import { getLocaleDir } from "../locales/config.js";
67
import { useLocale } from "../locales/useLocale.js";
78
import { AdminCommandPalette } from "./AdminCommandPalette";
89
import { Header } from "./Header";
10+
import { RegistryConfigurationBanner } from "./RegistryConfigurationBanner.js";
911
import { Sidebar, SidebarNav } from "./Sidebar";
1012
import { WelcomeModal } from "./WelcomeModal";
1113

@@ -39,6 +41,7 @@ export interface ShellProps {
3941
}>;
4042
i18n?: { defaultLocale: string; locales: string[] };
4143
version?: string;
44+
registryConfigurationError?: AdminManifest["registryConfigurationError"];
4245
};
4346
}
4447

@@ -104,6 +107,11 @@ export function Shell({ children, manifest }: ShellProps) {
104107
{/* Main content area — scrolls independently so sidebar stays full height */}
105108
<div className="flex flex-1 flex-col overflow-hidden">
106109
<Header />
110+
{manifest.registryConfigurationError && (
111+
<div className="px-6 pt-6">
112+
<RegistryConfigurationBanner error={manifest.registryConfigurationError} />
113+
</div>
114+
)}
107115
<main
108116
className={
109117
fullBleed

packages/admin/src/lib/api/client.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,19 @@ export interface AdminManifest {
270270
minimumReleaseAgeExclude?: string[];
271271
};
272272
};
273+
/** Field-level diagnostic returned when registry configuration is invalid. */
274+
registryConfigurationError?: {
275+
code:
276+
| "REGISTRY_AGGREGATOR_URL_REQUIRED"
277+
| "REGISTRY_AGGREGATOR_URL_INVALID"
278+
| "REGISTRY_AGGREGATOR_URL_FORBIDDEN"
279+
| "REGISTRY_MINIMUM_RELEASE_AGE_INVALID"
280+
| "REGISTRY_MINIMUM_RELEASE_AGE_EXCLUDE_INVALID";
281+
field:
282+
| "experimental.registry.aggregatorUrl"
283+
| "experimental.registry.policy.minimumReleaseAge"
284+
| "experimental.registry.policy.minimumReleaseAgeExclude";
285+
};
273286
/**
274287
* Admin branding overrides for white-labeling.
275288
* Set via the `admin` config in `astro.config.mjs`.
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { describe, expect, it } from "vitest";
2+
3+
import { RegistryConfigurationBanner } from "../../src/components/RegistryConfigurationBanner";
4+
import { render } from "../utils/render.tsx";
5+
6+
describe("RegistryConfigurationBanner", () => {
7+
it("directs administrators to the invalid aggregator URL setting", async () => {
8+
const screen = await render(
9+
<RegistryConfigurationBanner
10+
error={{
11+
code: "REGISTRY_AGGREGATOR_URL_INVALID",
12+
field: "experimental.registry.aggregatorUrl",
13+
}}
14+
/>,
15+
);
16+
17+
await expect
18+
.element(screen.getByRole("alert", { name: "Plugin registry configuration error" }))
19+
.toBeInTheDocument();
20+
await expect
21+
.element(
22+
screen.getByText(
23+
"Check experimental.registry.aggregatorUrl in astro.config.mjs, then restart EmDash.",
24+
),
25+
)
26+
.toBeInTheDocument();
27+
});
28+
29+
it("directs administrators to the invalid release policy setting", async () => {
30+
const screen = await render(
31+
<RegistryConfigurationBanner
32+
error={{
33+
code: "REGISTRY_MINIMUM_RELEASE_AGE_INVALID",
34+
field: "experimental.registry.policy.minimumReleaseAge",
35+
}}
36+
/>,
37+
);
38+
39+
await expect
40+
.element(
41+
screen.getByText(
42+
"Check experimental.registry.policy.minimumReleaseAge in astro.config.mjs, then restart EmDash.",
43+
),
44+
)
45+
.toBeInTheDocument();
46+
});
47+
48+
it("does not name the wrong setting for a diagnostic from a newer server", async () => {
49+
const screen = await render(
50+
<RegistryConfigurationBanner
51+
error={
52+
{
53+
code: "REGISTRY_FUTURE_SETTING_INVALID",
54+
field: "experimental.registry.futureSetting",
55+
} as never
56+
}
57+
/>,
58+
);
59+
60+
await expect
61+
.element(
62+
screen.getByText("Check experimental.registry in astro.config.mjs, then restart EmDash."),
63+
)
64+
.toBeInTheDocument();
65+
expect(screen.container.textContent).not.toContain("aggregatorUrl");
66+
});
67+
});

packages/core/src/astro/integration/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
import { buildMigrationManifest } from "../../migrations/manifest-builder.js";
2828
import { writeMigrationManifest } from "../../migrations/manifest-writer.js";
2929
import type { ResolvedPlugin } from "../../plugins/types.js";
30+
import { normalizeRegistryConfig } from "../../registry/config.js";
3031
import { VERSION } from "../../version.js";
3132
import { setDevTypegenRefresh } from "../dev-typegen.js";
3233
import { local } from "../storage/adapters.js";
@@ -328,6 +329,11 @@ export function emdash(config: EmDashConfig = {}): AstroIntegration {
328329
migrations: normalizeMigrationConfig(config.migrations),
329330
};
330331

332+
// Validate environment-independent registry settings while Astro is still
333+
// evaluating its config. The command-aware check in astro:config:setup
334+
// applies the stricter production localhost policy.
335+
normalizeRegistryConfig(resolvedConfig.experimental?.registry, { allowLocalhost: true });
336+
331337
// Validate marketplace URL
332338
if (resolvedConfig.marketplace) {
333339
const url = resolvedConfig.marketplace;
@@ -463,6 +469,9 @@ export function emdash(config: EmDashConfig = {}): AstroIntegration {
463469
command,
464470
}) => {
465471
astroCommand = command;
472+
normalizeRegistryConfig(resolvedConfig.experimental?.registry, {
473+
allowLocalhost: command === "dev" || command === "sync",
474+
});
466475
printBanner(logger);
467476
// Capture the host's Astro version so the runtime can expose it
468477
// to the admin and the registry install gate for `env:astro`

packages/core/src/astro/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type { Kysely } from "kysely";
1010

1111
import type { ContentFieldFilters } from "../content-list-query.js";
1212
import type { RouteCallerInput, RouteMeta } from "../plugins/routes.js";
13+
import type { ManifestRegistryConfigurationError } from "../registry/config.js";
1314

1415
// Re-export core types
1516
export type {
@@ -214,6 +215,8 @@ export interface EmDashManifest {
214215
minimumReleaseAgeExclude?: string[];
215216
};
216217
};
218+
/** Safe field-level diagnostic when the registry configuration cannot be normalized. */
219+
registryConfigurationError?: ManifestRegistryConfigurationError;
217220
/**
218221
* Admin branding overrides for white-labeling.
219222
* Set via the `admin` config in `astro.config.mjs`.

packages/core/src/emdash-runtime.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ import { isContentSaveRejection } from "./plugins/save-rejection.js";
227227
import type { CronScheduler } from "./plugins/scheduler/types.js";
228228
import { PluginStateRepository } from "./plugins/state.js";
229229
import { syncDeclaredStorageIndexes } from "./plugins/storage-indexes.js";
230-
import { normalizeRegistryConfig } from "./registry/config.js";
230+
import { resolveManifestRegistryConfig } from "./registry/config.js";
231231
import { requestCached } from "./request-cache.js";
232232
import { getRequestContext } from "./request-context.js";
233233
import { publishDueContent, type PublishedRef } from "./scheduled-publish.js";
@@ -2606,11 +2606,14 @@ export class EmDashRuntime {
26062606
}
26072607
: undefined;
26082608

2609-
// Normalize the experimental registry config for browser consumption.
2610-
// Validation errors here surface as 500s from the manifest endpoint
2611-
// rather than being silently dropped -- a misconfigured registry
2612-
// should be loud, not invisible.
2613-
const registry = normalizeRegistryConfig(this.config.experimental?.registry) ?? undefined;
2609+
const { registry, error: registryConfigurationError } = resolveManifestRegistryConfig(
2610+
this.config.experimental?.registry,
2611+
);
2612+
if (registryConfigurationError) {
2613+
console.error(
2614+
`EmDash registry configuration error in ${registryConfigurationError.field} (${registryConfigurationError.code})`,
2615+
);
2616+
}
26142617

26152618
return {
26162619
version: VERSION,
@@ -2628,6 +2631,7 @@ export class EmDashRuntime {
26282631
},
26292632
marketplace: !!this.config.marketplace,
26302633
registry,
2634+
registryConfigurationError,
26312635
};
26322636
}
26332637

0 commit comments

Comments
 (0)