diff --git a/.changeset/inline-portable-text-hydration.md b/.changeset/inline-portable-text-hydration.md new file mode 100644 index 0000000000..59d78a9382 --- /dev/null +++ b/.changeset/inline-portable-text-hydration.md @@ -0,0 +1,5 @@ +--- +"emdash": patch +--- + +Fixes the inline Portable Text editor failing to hydrate in development when visual editing is enabled. The editor's code-block extension loads lowlight, which default-imports a CommonJS highlight.js module. The Vite client optimizer now pre-bundles `lowlight`, `highlight.js`, and `highlight.js/lib/core` so the deep CJS import is wrapped with ESM interop before it reaches the browser. diff --git a/e2e/tests/visual-editing.spec.ts b/e2e/tests/visual-editing.spec.ts index ad4cfa142c..2d6ab42da1 100644 --- a/e2e/tests/visual-editing.spec.ts +++ b/e2e/tests/visual-editing.spec.ts @@ -99,6 +99,22 @@ test.describe("Inline Editor", () => { await enableEditMode(page); }); + test("PT editor shows up in visual editing mode", async ({ page }) => { + await gotoWithRetry(page, POST_WITH_IMAGE_PATH); + + const editor = page.locator(".emdash-inline-editor"); + await expect(editor).toBeVisible({ timeout: 15000 }); + + // The island failed to hydrate with zero height when highlight.js/lib/core + // was served raw as CommonJS, so also assert some static content rendered + // inside the editor rather than disappearing. + const box = await editor.boundingBox(); + expect(box).not.toBeNull(); + expect(box!.height).toBeGreaterThan(0); + await expect(editor.locator("text=Text before image.")).toBeVisible(); + await expect(editor.locator("text=Text after image.")).toBeVisible(); + }); + test("loads without crashing on posts with image blocks", async ({ page }) => { await gotoWithRetry(page, POST_WITH_IMAGE_PATH); diff --git a/packages/core/src/astro/integration/vite-config.ts b/packages/core/src/astro/integration/vite-config.ts index 8a8cbe987c..98a0b7d90f 100644 --- a/packages/core/src/astro/integration/vite-config.ts +++ b/packages/core/src/astro/integration/vite-config.ts @@ -572,9 +572,17 @@ export function createViteConfig( optimizeDeps: { // When using source, don't pre-bundle JS — let Vite transform on the fly for HMR. // When using dist, pre-bundle to avoid re-optimization on first hydration. + // lowlight pulls in a CommonJS highlight.js entry, so the inline Portable + // Text editor requires these to be pre-bundled with ESM interop in dev. include: useSource - ? ["@astrojs/react/client.js"] - : ["@emdash-cms/admin", "@astrojs/react/client.js"], + ? ["@astrojs/react/client.js", "lowlight", "highlight.js", "highlight.js/lib/core"] + : [ + "@emdash-cms/admin", + "@astrojs/react/client.js", + "lowlight", + "highlight.js", + "highlight.js/lib/core", + ], exclude: cloudflare ? ["virtual:emdash"] : [...NODE_NATIVE_EXTERNALS, "virtual:emdash"], }, }; diff --git a/packages/core/tests/unit/astro/vite-config.test.ts b/packages/core/tests/unit/astro/vite-config.test.ts index 0b547587b6..79fde2ce15 100644 --- a/packages/core/tests/unit/astro/vite-config.test.ts +++ b/packages/core/tests/unit/astro/vite-config.test.ts @@ -171,6 +171,44 @@ describe("createViteConfig use-sync-external-store shim aliasing", () => { } }); +describe("createViteConfig inline Portable Text hydration deps", () => { + const monorepoDemoRoot = new URL("../../../../../demos/simple/", import.meta.url); + const externalProjectRoot = new URL("file:///workspace/emdash-site/"); + + function buildConfig(root: URL) { + return createViteConfig( + { + serializableConfig: {}, + resolvedConfig: {} as never, + pluginDescriptors: [], + astroConfig: { + root, + adapter: { name: "@astrojs/node" }, + } as AstroConfig, + }, + "dev", + ); + } + + it("pre-bundles lowlight and highlight.js in source-mode dev", () => { + const config = buildConfig(monorepoDemoRoot); + const include = config.optimizeDeps?.include ?? []; + + expect(include).toContain("lowlight"); + expect(include).toContain("highlight.js"); + expect(include).toContain("highlight.js/lib/core"); + }); + + it("pre-bundles lowlight and highlight.js in external dist-mode dev", () => { + const config = buildConfig(externalProjectRoot); + const include = config.optimizeDeps?.include ?? []; + + expect(include).toContain("lowlight"); + expect(include).toContain("highlight.js"); + expect(include).toContain("highlight.js/lib/core"); + }); +}); + describe("createViteConfig Astro logger optimization", () => { const astroSevenRoot = new URL("../../../../../demos/cloudflare/", import.meta.url); const astroSixRoot = new URL("../../../../../docs/", import.meta.url);