From b09bfd51cece5e88fe8314668a591ab11de36b4d Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Wed, 1 Apr 2026 12:27:00 +0100 Subject: [PATCH 1/5] fix: exclude virtual:emdash from optimizeDeps to fix npm installs on Cloudflare --- .changeset/fix-virtual-module-optimizedeps.md | 5 +++++ packages/core/src/astro/integration/vite-config.ts | 9 ++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 .changeset/fix-virtual-module-optimizedeps.md diff --git a/.changeset/fix-virtual-module-optimizedeps.md b/.changeset/fix-virtual-module-optimizedeps.md new file mode 100644 index 0000000..d25e7a6 --- /dev/null +++ b/.changeset/fix-virtual-module-optimizedeps.md @@ -0,0 +1,5 @@ +--- +"emdash": patch +--- + +Fix virtual module resolution errors when emdash is installed from npm on Cloudflare. The esbuild dependency pre-bundler was encountering `virtual:emdash/*` imports while crawling dist files and failing to resolve them. These are now excluded from the optimizeDeps scan. diff --git a/packages/core/src/astro/integration/vite-config.ts b/packages/core/src/astro/integration/vite-config.ts index e5e19ff..07ac986 100644 --- a/packages/core/src/astro/integration/vite-config.ts +++ b/packages/core/src/astro/integration/vite-config.ts @@ -269,6 +269,13 @@ export function createViteConfig( // Vite discovers them one-by-one on first request, causing workerd // to enter "worker cancelled" state on cold cache. optimizeDeps: { + // Exclude EmDash virtual modules from esbuild's dependency + // scan. These are resolved by the Vite plugin at transform time, + // but esbuild encounters them when crawling emdash's dist files + // during pre-bundling and can't resolve them. Vite's exclude + // uses prefix matching (id.startsWith(m + "/")), so + // "virtual:emdash" matches all "virtual:emdash/*" imports. + exclude: ["virtual:emdash"], include: [ // EmDash direct deps "emdash > @portabletext/toolkit", @@ -322,7 +329,7 @@ export function createViteConfig( include: useSource ? ["@astrojs/react/client.js"] : ["@emdash-cms/admin", "@astrojs/react/client.js"], - exclude: cloudflare ? [] : NODE_NATIVE_EXTERNALS, + exclude: cloudflare ? ["virtual:emdash"] : [...NODE_NATIVE_EXTERNALS, "virtual:emdash"], }, }; } From 478570ff344043bbfb6302f0cea8559494bf05ad Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Wed, 1 Apr 2026 12:39:06 +0100 Subject: [PATCH 2/5] Fix ci --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 44d309c..d832aad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: - run: pnpm install --frozen-lockfile - run: pnpm build - run: pnpm typecheck - - run: pnpm run --filteremdash-demo --filter @emdash-cms/demo-cloudflare typecheck + - run: pnpm run --filter emdash-demo --filter @emdash-cms/demo-cloudflare typecheck - run: pnpm typecheck:templates lint: @@ -138,7 +138,7 @@ jobs: cache: pnpm - run: pnpm install --frozen-lockfile - run: pnpm build - - run: pnpm --filteremdash exec vitest run --config vitest.smoke.config.ts + - run: pnpm --filter emdash exec vitest run --config vitest.smoke.config.ts timeout-minutes: 5 env: DATABASE_URL: postgres://postgres:test@localhost:5432/emdash_smoke From cb7a816675a4463ee4f5e79b935e884a0336be9f Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Wed, 1 Apr 2026 12:39:19 +0100 Subject: [PATCH 3/5] Fix migration --- .../src/database/migrations/001_initial.ts | 44 ++++++++++++++++--- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/packages/core/src/database/migrations/001_initial.ts b/packages/core/src/database/migrations/001_initial.ts index 8165712..aba2d82 100644 --- a/packages/core/src/database/migrations/001_initial.ts +++ b/packages/core/src/database/migrations/001_initial.ts @@ -14,6 +14,7 @@ export async function up(db: Kysely): Promise { // References entries in ec_* tables by collection + entry_id await db.schema .createTable("revisions") + .ifNotExists() .addColumn("id", "text", (col) => col.primaryKey()) .addColumn("collection", "text", (col) => col.notNull()) // e.g., 'posts' .addColumn("entry_id", "text", (col) => col.notNull()) // ID in the ec_* table @@ -24,6 +25,7 @@ export async function up(db: Kysely): Promise { await db.schema .createIndex("idx_revisions_entry") + .ifNotExists() .on("revisions") .columns(["collection", "entry_id"]) .execute(); @@ -31,6 +33,7 @@ export async function up(db: Kysely): Promise { // Taxonomies await db.schema .createTable("taxonomies") + .ifNotExists() .addColumn("id", "text", (col) => col.primaryKey()) .addColumn("name", "text", (col) => col.notNull()) .addColumn("slug", "text", (col) => col.notNull()) @@ -43,11 +46,17 @@ export async function up(db: Kysely): Promise { ) .execute(); - await db.schema.createIndex("idx_taxonomies_name").on("taxonomies").column("name").execute(); + await db.schema + .createIndex("idx_taxonomies_name") + .ifNotExists() + .on("taxonomies") + .column("name") + .execute(); // Content-Taxonomy junction - references entries in ec_* tables await db.schema .createTable("content_taxonomies") + .ifNotExists() .addColumn("collection", "text", (col) => col.notNull()) // e.g., 'posts' .addColumn("entry_id", "text", (col) => col.notNull()) // ID in the ec_* table .addColumn("taxonomy_id", "text", (col) => col.notNull()) @@ -64,6 +73,7 @@ export async function up(db: Kysely): Promise { // Media await db.schema .createTable("media") + .ifNotExists() .addColumn("id", "text", (col) => col.primaryKey()) .addColumn("filename", "text", (col) => col.notNull()) .addColumn("mime_type", "text", (col) => col.notNull()) @@ -80,6 +90,7 @@ export async function up(db: Kysely): Promise { await db.schema .createIndex("idx_media_content_hash") + .ifNotExists() .on("media") .column("content_hash") .execute(); @@ -87,6 +98,7 @@ export async function up(db: Kysely): Promise { // Users await db.schema .createTable("users") + .ifNotExists() .addColumn("id", "text", (col) => col.primaryKey()) .addColumn("email", "text", (col) => col.notNull().unique()) .addColumn("password_hash", "text", (col) => col.notNull()) @@ -97,11 +109,17 @@ export async function up(db: Kysely): Promise { .addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))) .execute(); - await db.schema.createIndex("idx_users_email").on("users").column("email").execute(); + await db.schema + .createIndex("idx_users_email") + .ifNotExists() + .on("users") + .column("email") + .execute(); // Options (key-value store) await db.schema .createTable("options") + .ifNotExists() .addColumn("name", "text", (col) => col.primaryKey()) .addColumn("value", "text", (col) => col.notNull()) .execute(); @@ -109,6 +127,7 @@ export async function up(db: Kysely): Promise { // Audit logs (security events) await db.schema .createTable("audit_logs") + .ifNotExists() .addColumn("id", "text", (col) => col.primaryKey()) .addColumn("timestamp", "text", (col) => col.defaultTo(currentTimestamp(db))) .addColumn("actor_id", "text") @@ -120,9 +139,24 @@ export async function up(db: Kysely): Promise { .addColumn("status", "text") .execute(); - await db.schema.createIndex("idx_audit_actor").on("audit_logs").column("actor_id").execute(); - await db.schema.createIndex("idx_audit_action").on("audit_logs").column("action").execute(); - await db.schema.createIndex("idx_audit_timestamp").on("audit_logs").column("timestamp").execute(); + await db.schema + .createIndex("idx_audit_actor") + .ifNotExists() + .on("audit_logs") + .column("actor_id") + .execute(); + await db.schema + .createIndex("idx_audit_action") + .ifNotExists() + .on("audit_logs") + .column("action") + .execute(); + await db.schema + .createIndex("idx_audit_timestamp") + .ifNotExists() + .on("audit_logs") + .column("timestamp") + .execute(); } export async function down(db: Kysely): Promise { From 086647bff4d183ac2ef3a2e53aa7fc5e26b07bd5 Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Wed, 1 Apr 2026 12:39:34 +0100 Subject: [PATCH 4/5] fix: use placeholder database_id in templates for local dev Miniflare asserts database_id is truthy. An empty string crashes on startup. Use 'local' as a placeholder -- miniflare doesn't validate it for local dev, and the comment tells users to replace it with a real ID for deploy. --- templates/blog-cloudflare/wrangler.jsonc | 4 ++-- templates/marketing-cloudflare/wrangler.jsonc | 4 ++-- templates/portfolio-cloudflare/wrangler.jsonc | 4 ++-- templates/starter-cloudflare/wrangler.jsonc | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/templates/blog-cloudflare/wrangler.jsonc b/templates/blog-cloudflare/wrangler.jsonc index 9eb7d0c..fac093f 100644 --- a/templates/blog-cloudflare/wrangler.jsonc +++ b/templates/blog-cloudflare/wrangler.jsonc @@ -10,8 +10,8 @@ { "binding": "DB", "database_name": "my-emdash-site", - // Run `wrangler d1 create my-emdash-site` and paste the ID here - "database_id": "", + // Run `wrangler d1 create my-emdash-site` and paste the real ID here for deploy + "database_id": "local", }, ], "r2_buckets": [ diff --git a/templates/marketing-cloudflare/wrangler.jsonc b/templates/marketing-cloudflare/wrangler.jsonc index 861112d..a0f1fc3 100644 --- a/templates/marketing-cloudflare/wrangler.jsonc +++ b/templates/marketing-cloudflare/wrangler.jsonc @@ -10,8 +10,8 @@ { "binding": "DB", "database_name": "my-marketing-site", - // Run `wrangler d1 create my-marketing-site` and paste the ID here - "database_id": "", + // Run `wrangler d1 create my-marketing-site` and paste the real ID here for deploy + "database_id": "local", }, ], "r2_buckets": [ diff --git a/templates/portfolio-cloudflare/wrangler.jsonc b/templates/portfolio-cloudflare/wrangler.jsonc index 0c5fb58..c3cb752 100644 --- a/templates/portfolio-cloudflare/wrangler.jsonc +++ b/templates/portfolio-cloudflare/wrangler.jsonc @@ -10,8 +10,8 @@ { "binding": "DB", "database_name": "my-portfolio-site", - // Run `wrangler d1 create my-portfolio-site` and paste the ID here - "database_id": "", + // Run `wrangler d1 create my-portfolio-site` and paste the real ID here for deploy + "database_id": "local", }, ], "r2_buckets": [ diff --git a/templates/starter-cloudflare/wrangler.jsonc b/templates/starter-cloudflare/wrangler.jsonc index 9eb7d0c..fac093f 100644 --- a/templates/starter-cloudflare/wrangler.jsonc +++ b/templates/starter-cloudflare/wrangler.jsonc @@ -10,8 +10,8 @@ { "binding": "DB", "database_name": "my-emdash-site", - // Run `wrangler d1 create my-emdash-site` and paste the ID here - "database_id": "", + // Run `wrangler d1 create my-emdash-site` and paste the real ID here for deploy + "database_id": "local", }, ], "r2_buckets": [ From 9f14710d886dc257c980eb976f6a9c78eb6ed5ae Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Wed, 1 Apr 2026 12:39:52 +0100 Subject: [PATCH 5/5] Update env --- templates/starter-cloudflare/emdash-env.d.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/templates/starter-cloudflare/emdash-env.d.ts b/templates/starter-cloudflare/emdash-env.d.ts index 9dc6de3..abb2626 100644 --- a/templates/starter-cloudflare/emdash-env.d.ts +++ b/templates/starter-cloudflare/emdash-env.d.ts @@ -3,7 +3,7 @@ /// -import type { PortableTextBlock } from "emdash"; +import type { ContentBylineCredit, PortableTextBlock } from "emdash"; export interface Page { id: string; @@ -14,6 +14,7 @@ export interface Page { createdAt: Date; updatedAt: Date; publishedAt: Date | null; + bylines?: ContentBylineCredit[]; } export interface Post { @@ -27,6 +28,7 @@ export interface Post { createdAt: Date; updatedAt: Date; publishedAt: Date | null; + bylines?: ContentBylineCredit[]; } declare module "emdash" { @@ -34,4 +36,4 @@ declare module "emdash" { pages: Page; posts: Post; } -} +} \ No newline at end of file