|
async getManifest(): Promise<EmDashManifest> { |
|
// Build collections from database. |
|
// Use this.db (ALS-aware getter) so playground mode picks up the |
|
// per-session DO database instead of the hardcoded singleton. |
|
const manifestCollections: Record<string, ManifestCollection> = {}; |
|
try { |
|
const registry = new SchemaRegistry(this.db); |
|
const dbCollections = await registry.listCollections(); |
|
for (const collection of dbCollections) { |
|
const collectionWithFields = await registry.getCollectionWithFields(collection.slug); |
|
const fields: Record< |
|
string, |
|
{ |
|
kind: string; |
|
label?: string; |
|
required?: boolean; |
|
widget?: string; |
|
// Two shapes: legacy enum-style `[{ value, label }]` for select widgets, |
|
// or arbitrary `Record<string, unknown>` for plugin field widgets that |
|
// need per-field config (e.g. a checkbox grid receiving its column defs). |
|
options?: Array<{ value: string; label: string }> | Record<string, unknown>; |
|
} |
|
> = {}; |
|
|
|
if (collectionWithFields?.fields) { |
|
for (const field of collectionWithFields.fields) { |
|
const entry: (typeof fields)[string] = { |
|
kind: FIELD_TYPE_TO_KIND[field.type] ?? "string", |
|
label: field.label, |
|
required: field.required, |
|
}; |
|
if (field.widget) entry.widget = field.widget; |
|
// Plugin field widgets read their per-field config from `field.options`, |
|
// which the seed schema types as `Record<string, unknown>`. Pass it |
|
// through to the manifest so plugin widgets in the admin SPA receive it. |
|
if (field.options) { |
|
entry.options = field.options; |
|
} |
|
// Legacy: select/multiSelect enum options live on `field.validation.options`. |
|
// Wins over `field.options` to preserve existing behavior for enum widgets. |
|
if (field.validation?.options) { |
|
entry.options = field.validation.options.map((v) => ({ |
|
value: v, |
|
label: v.charAt(0).toUpperCase() + v.slice(1), |
|
})); |
|
} |
|
// Include full validation for repeater fields (subFields, minItems, maxItems) |
|
if (field.type === "repeater" && field.validation) { |
|
(entry as Record<string, unknown>).validation = field.validation; |
|
} |
|
fields[field.slug] = entry; |
|
} |
|
} |
|
|
|
manifestCollections[collection.slug] = { |
|
label: collection.label, |
|
labelSingular: collection.labelSingular || collection.label, |
|
supports: collection.supports || [], |
|
hasSeo: collection.hasSeo, |
|
urlPattern: collection.urlPattern, |
|
fields, |
|
}; |
|
} |
|
} catch (error) { |
|
console.debug("EmDash: Could not load database collections:", error); |
|
} |
|
|
|
// Build plugins manifest |
|
const manifestPlugins: Record< |
|
string, |
|
{ |
|
version?: string; |
|
enabled?: boolean; |
|
sandboxed?: boolean; |
|
adminMode?: "react" | "blocks" | "none"; |
|
adminPages?: Array<{ path: string; label?: string; icon?: string }>; |
|
dashboardWidgets?: Array<{ |
|
id: string; |
|
title?: string; |
|
size?: string; |
|
}>; |
|
portableTextBlocks?: Array<{ |
|
type: string; |
|
label: string; |
|
icon?: string; |
|
description?: string; |
|
placeholder?: string; |
|
fields?: Element[]; |
|
}>; |
|
fieldWidgets?: Array<{ |
|
name: string; |
|
label: string; |
|
fieldTypes: string[]; |
|
elements?: Element[]; |
|
}>; |
|
} |
|
> = {}; |
|
|
|
for (const plugin of this.configuredPlugins) { |
|
const status = this.pluginStates.get(plugin.id); |
|
const enabled = status === undefined || status === "active"; |
|
|
|
// Determine admin mode: has admin entry → react, has pages/widgets → blocks, else none |
|
const hasAdminEntry = !!plugin.admin?.entry; |
|
const hasAdminPages = (plugin.admin?.pages?.length ?? 0) > 0; |
|
const hasWidgets = (plugin.admin?.widgets?.length ?? 0) > 0; |
|
let adminMode: "react" | "blocks" | "none" = "none"; |
|
if (hasAdminEntry) { |
|
adminMode = "react"; |
|
} else if (hasAdminPages || hasWidgets) { |
|
adminMode = "blocks"; |
|
} |
|
|
|
manifestPlugins[plugin.id] = { |
|
version: plugin.version, |
|
enabled, |
|
adminMode, |
|
adminPages: plugin.admin?.pages ?? [], |
|
dashboardWidgets: plugin.admin?.widgets ?? [], |
|
portableTextBlocks: plugin.admin?.portableTextBlocks, |
|
fieldWidgets: plugin.admin?.fieldWidgets, |
|
}; |
|
} |
|
|
|
// Add sandboxed plugins (use entries for admin config) |
|
// TODO: sandboxed plugins need fieldWidgets extracted from their manifest |
|
// to support Block Kit field widgets. Currently only trusted plugins carry |
|
// fieldWidgets through the admin.fieldWidgets path. |
|
for (const entry of this.sandboxedPluginEntries) { |
|
const status = this.pluginStates.get(entry.id); |
|
const enabled = status === undefined || status === "active"; |
|
|
|
const hasAdminPages = (entry.adminPages?.length ?? 0) > 0; |
|
const hasWidgets = (entry.adminWidgets?.length ?? 0) > 0; |
|
|
|
manifestPlugins[entry.id] = { |
|
version: entry.version, |
|
enabled, |
|
sandboxed: true, |
|
adminMode: hasAdminPages || hasWidgets ? "blocks" : "none", |
|
adminPages: entry.adminPages ?? [], |
|
dashboardWidgets: entry.adminWidgets ?? [], |
|
}; |
|
} |
|
|
|
// Add marketplace-installed plugins (dynamically loaded from R2) |
|
for (const [pluginId, meta] of marketplaceManifestCache) { |
|
// Skip if already included from build-time config |
|
if (manifestPlugins[pluginId]) continue; |
|
|
|
const status = this.pluginStates.get(pluginId); |
|
const enabled = status === "active"; |
|
|
|
const pages = meta.admin?.pages; |
|
const widgets = meta.admin?.widgets; |
|
const hasAdminPages = (pages?.length ?? 0) > 0; |
|
const hasWidgets = (widgets?.length ?? 0) > 0; |
|
|
|
manifestPlugins[pluginId] = { |
|
version: meta.version, |
|
enabled, |
|
sandboxed: true, |
|
adminMode: hasAdminPages || hasWidgets ? "blocks" : "none", |
|
adminPages: pages ?? [], |
|
dashboardWidgets: widgets ?? [], |
|
}; |
|
} |
|
|
|
// Build taxonomies from database |
|
let manifestTaxonomies: Array<{ |
|
name: string; |
|
label: string; |
|
labelSingular?: string; |
|
hierarchical: boolean; |
|
collections: string[]; |
|
}> = []; |
|
try { |
|
const rows = await this.db |
|
.selectFrom("_emdash_taxonomy_defs") |
|
.selectAll() |
|
.orderBy("name") |
|
.execute(); |
|
manifestTaxonomies = rows.map((row) => ({ |
|
name: row.name, |
|
label: row.label, |
|
labelSingular: row.label_singular ?? undefined, |
|
hierarchical: row.hierarchical === 1, |
|
collections: row.collections ? (JSON.parse(row.collections) as string[]).toSorted() : [], |
|
})); |
|
} catch (error) { |
|
console.debug("EmDash: Could not load taxonomy definitions:", error); |
|
} |
|
|
|
// Build manifest hash |
|
const manifestHash = await hashString( |
|
JSON.stringify(manifestCollections) + |
|
JSON.stringify(manifestPlugins) + |
|
JSON.stringify(manifestTaxonomies), |
|
); |
|
|
|
// Determine auth mode |
|
const authMode = getAuthMode(this.config); |
|
const authModeValue = authMode.type === "external" ? authMode.providerType : "passkey"; |
|
|
|
// Include i18n config if enabled (read from virtual module to avoid SSR module singleton mismatch) |
|
const i18nConfig = virtualConfig?.i18n; |
|
const i18n = |
|
i18nConfig && i18nConfig.locales && i18nConfig.locales.length > 1 |
|
? { defaultLocale: i18nConfig.defaultLocale, locales: i18nConfig.locales } |
|
: undefined; |
|
|
|
return { |
|
version: "0.1.0", |
|
hash: manifestHash, |
|
collections: manifestCollections, |
|
plugins: manifestPlugins, |
|
taxonomies: manifestTaxonomies, |
|
authMode: authModeValue, |
|
i18n, |
|
marketplace: !!this.config.marketplace, |
|
}; |
|
} |
Description
On
astro devwith the Cloudflare adapter andemdash()integration, EmDash performs an internalPOST /_emdash/api/typegenimmediately after the dev server starts listening.That request is not lightweight. It goes through the full EmDash middleware/runtime path, initializes the runtime, builds the admin manifest, and triggers additional SSR dependency optimization on first access.
There are two interrelated issues at play.
server.httpServer.on('listening', ...)insideastro:server:setup, but at that point Vite's SSR dependency optimization may not have completed yet. Vite lazily optimizes SSR dependencies on first access, so the typegen → middleware → runtime chain pulls in a large number of SSR dependencies all at once, causing a race between optimization and request handling. This is what produces the chunk-*.js not found errors.This makes the dev server feel very slow and sometimes temporarily unusable right after startup.
Current behavior
In astro:server:setup, EmDash fetches
/_emdash/api/typegenas soon as the server is listening:emdash/packages/core/src/astro/integration/index.ts
Lines 282 to 301 in c92e7e6
The typegen endpoint requires
locals.emdash.db, so it goes through EmDash middleware first:emdash/packages/core/src/astro/routes/api/typegen.ts
Line 73 in c92e7e6
The middleware does
getRuntimeandgetManifestonsrc/astro/middleware.tsemdash/packages/core/src/astro/middleware.ts
Lines 130 to 157 in c92e7e6
After runtime init, getManifest() reads collections and fields again from the DB:
emdash/packages/core/src/emdash-runtime.ts
Lines 1143 to 1364 in c92e7e6
Then the typegen endpoint reads collections/fields again to generate TS types:
emdash/packages/core/src/astro/routes/api/typegen.ts
Lines 37 to 55 in c92e7e6
So the same internal request does both manifest schema loading and typegen schema loading.
Also, even though EmDash configures ssr.optimizeDeps.include, in my cold-start logs Vite still optimized these later, one by one:
This seems to be the unstable part that causes the deps_ssr missing-file failures during reload.
Steps to reproduce
just start
astro devEnvironment
6.1.50.1.1@astrojs/cloudflare:13.1.8@emdash-cms/cloudflare:0.1.1Logs / error output
The file does not exist at ".../node_modules/.vite/deps_ssr/chunk-*.js" The dependency might be incompatible with the dep optimizer. Try adding it to optimizeDeps.exclude.Suggested Change
Instead of
POST /_emdash/api/typegenthrough the full middleware stack, call the typegen logic directly inastro:server:setup:This eliminates the middleware overhead and avoids triggering SSR dep optimization for middleware-related modules (
emdash/middleware/*, etc.) at startup.