You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
// src/components/ContactForm/Form.client.tsxexportfunctionForm(){ ... }// named export// default.server.tsximport{Form}from"./Form.client.jsx";<Islandcomponent={Form}/>
Builds without warning, renders fine server-side. In the browser the island requests /modules/<module>/undefined.js and never hydrates — for a form, the submit falls back to a native GET and the page reloads. Console: <Island> failed to load ... Failed to fetch dynamically imported module: .../undefined.js.
Cause:vite-plugin/src/insert-filename.ts only attaches the hydration metadata (__filename) to ExportDefaultDeclaration nodes; Island.tsx then builds the URL from Component.__filename → "undefined.js". Switching to export default function Form() fixes it.
Expected (either):
Support named exports — tag all exported function/const declarations in .client.{jsx,tsx} files; or
Fail fast — build error (or at minimum a loud warning) when a .client.* file has no default export, and a server-side render error when <Island> receives a component without __filename, instead of the runtime undefined.js fetch.
Found during a hands-on DX evaluation; cost ~20 minutes to diagnose with codebase knowledge. Astro supports named exports for islands (component-export attribute).
Technical plan (agent-executable)
Written for execution by a Claude (Opus/Sonnet) agent inside the Jahia Cortex harness.
Harness setup
Open Cortex; cortex-sources → javascript-modules @ main.
Unit/snapshot work needs no Jahia. For the e2e step, jahia-docker up an instance, build the engine (mvn install -pl javascript-modules-engine -am -DskipTests), deploy with jahia-deploy.
Design decision
Do BOTH halves — they protect different failure points:
Support named exports (feature parity, removes the trap entirely).
Fail fast when metadata is missing (server-side guard so any future gap is a loud render error, never a runtime undefined.js).
Code map — the three coordinated pieces (all in this monorepo, released together)
File
Today
Change
vite-plugin/src/insert-filename.ts
Tags only ExportDefaultDeclaration with __filename
insert-filename.ts: in the AST walk, additionally handle ExportNamedDeclaration nodes whose declaration is a FunctionDeclaration or a VariableDeclaration (each declarator with an Identifier id). For each exported binding, append after the statement: Object.defineProperties(<name>, { __filename: {...}, __exportName: { value: "<name>", enumerable: false } });
(appending statements avoids the wrap-expression trick used for default exports, which cannot wrap declarations). Keep the default-export wrapping as-is but extend it to also set __exportName: "default". Skip non-identifier patterns (destructuring exports) — out of scope, they fall into the fail-fast path. ⚠️ Guard the defineProperties call with the existing typeof check (only functions/objects).
Island.tsx:
const file = Component.__filename; if (file === undefined) throw new Error(\ received a component without hydration metadata. Client components must be exported from a *.client.{jsx,tsx} file (named or default export). Got: ${Component.displayName ?? Component.name ?? "anonymous"}`);`
Emit "data-export": Component.__exportName on the jsm-island element (omit when "default" to keep markup stable).
hydrate.tsx: const exportName = element.dataset.export ?? "default"; const Component = (await import(entry))[exportName]; if (!Component) throw new Error(...) — include entry + exportName in the message.
Type surface: the @ts-expect-error __filename is added by the vite plugin comment in Island.tsx — extend the internal type to { __filename?: string; __exportName?: string } and drop the expect-error if it becomes representable.
Verification
Snapshot tests (vite-plugin/fixtures, run yarn workspace @jahia/vite-plugin test — it builds fixtures then diffs dist/ against expected/):
Add fixtures/src/named.client.tsx with a named export used via <Island> from a server fixture, and one file with BOTH a named and a default export.
Rebuild and update expected/ snapshots; review the diff by hand — assert __exportName strings appear for both export kinds.
Unit-ish: assert in the fixture test that the built server bundle contains __filename for the named export (string match is fine, matching the snapshot style).
e2e (tests/ cypress, jahia-cypress tool): extend the island/hydration spec — a named-export island hydrates (button click mutates DOM). Assert no undefined.js request (cy.intercept on **/undefined.js failing the test).
Fail-fast path: a server view passing a plain (non-client) component to <Island> → render error mentioning "hydration metadata" (with Dev error surfacing: visible errors, source-mapped stacks #700's dev box, visible on page; otherwise assert on the server log / module error comment).
Regression: full fixtures snapshot + existing island e2e stay green; samples/hydrogen builds unchanged (its Celebrate.client.tsx uses a default export — snapshot should show __exportName: "default" added, an expected diff).
A component without metadata fails at server render with an actionable message — undefined.js can no longer reach the browser.
Mixed named+default export files work; each export hydrates independently.
Snapshots + e2e cover both export kinds and the failure path.
Risks / notes
The three pieces are version-coupled (plugin injects, library emits, engine loader consumes). They ship from one monorepo — note in the PR that modules must rebuild with the matching plugin version to use named exports; OLD modules keep working (loader falls back to default when data-export is absent).
magic-string + this.parse (rolldown AST) are already in place — no new dependencies.
Part of EPIC #698.
Repro:
Builds without warning, renders fine server-side. In the browser the island requests
/modules/<module>/undefined.jsand never hydrates — for a form, the submit falls back to a native GET and the page reloads. Console:<Island> failed to load ... Failed to fetch dynamically imported module: .../undefined.js.Cause:
vite-plugin/src/insert-filename.tsonly attaches the hydration metadata (__filename) toExportDefaultDeclarationnodes;Island.tsxthen builds the URL fromComponent.__filename→"undefined.js". Switching toexport default function Form()fixes it.Expected (either):
.client.{jsx,tsx}files; or.client.*file has no default export, and a server-side render error when<Island>receives a component without__filename, instead of the runtimeundefined.jsfetch.Found during a hands-on DX evaluation; cost ~20 minutes to diagnose with codebase knowledge. Astro supports named exports for islands (
component-exportattribute).Technical plan (agent-executable)
Harness setup
cortex-sources→javascript-modules@main.jahia-dockerup an instance, build the engine (mvn install -pl javascript-modules-engine -am -DskipTests), deploy withjahia-deploy.Design decision
Do BOTH halves — they protect different failure points:
undefined.js).Code map — the three coordinated pieces (all in this monorepo, released together)
vite-plugin/src/insert-filename.tsExportDefaultDeclarationwith__filename__exportNamejavascript-modules-library/src/components/render/Island.tsxbuildModuleFileUrl(${Component.__filename}.js)→"undefined.js"when untagged; emitsdata-src__filenameis missing; emitdata-exportjavascript-modules-engine/src/client/hydrate.tsx(line ~53)const { default: Component } = await import(entry);— hardcodes the default exportmodule[element.dataset.export ?? "default"]Implementation steps
insert-filename.ts: in the AST walk, additionally handleExportNamedDeclarationnodes whosedeclarationis aFunctionDeclarationor aVariableDeclaration(each declarator with anIdentifierid). For each exported binding, append after the statement:Object.defineProperties(<name>, { __filename: {...}, __exportName: { value: "<name>", enumerable: false } });(appending statements avoids the wrap-expression trick used for default exports, which cannot wrap declarations). Keep the default-export wrapping as-is but extend it to also set
__exportName: "default". Skip non-identifier patterns (destructuring exports) — out of scope, they fall into the fail-fast path.definePropertiescall with the existingtypeofcheck (only functions/objects).Island.tsx:const file = Component.__filename; if (file === undefined) throw new Error(\received a component without hydration metadata. Client components must be exported from a *.client.{jsx,tsx} file (named or default export). Got: ${Component.displayName ?? Component.name ?? "anonymous"}`);`"data-export": Component.__exportNameon thejsm-islandelement (omit when"default"to keep markup stable).hydrate.tsx:const exportName = element.dataset.export ?? "default"; const Component = (await import(entry))[exportName]; if (!Component) throw new Error(...)— include entry + exportName in the message.@ts-expect-error __filename is added by the vite plugincomment inIsland.tsx— extend the internal type to{ __filename?: string; __exportName?: string }and drop the expect-error if it becomes representable.Verification
vite-plugin/fixtures, runyarn workspace @jahia/vite-plugin test— it builds fixtures then diffsdist/againstexpected/):fixtures/src/named.client.tsxwith a named export used via<Island>from a server fixture, and one file with BOTH a named and a default export.expected/snapshots; review the diff by hand — assert__exportNamestrings appear for both export kinds.__filenamefor the named export (string match is fine, matching the snapshot style).tests/cypress,jahia-cypresstool): extend the island/hydration spec — a named-export island hydrates (button click mutates DOM). Assert noundefined.jsrequest (cy.intercept on**/undefined.jsfailing the test).<Island>→ render error mentioning "hydration metadata" (with Dev error surfacing: visible errors, source-mapped stacks #700's dev box, visible on page; otherwise assert on the server log / module error comment).samples/hydrogenbuilds unchanged (itsCelebrate.client.tsxuses a default export — snapshot should show__exportName: "default"added, an expected diff).Acceptance criteria
.client.tsxcomponents hydrate correctly end-to-end.undefined.jscan no longer reach the browser.Risks / notes
defaultwhendata-exportis absent).magic-string+this.parse(rolldown AST) are already in place — no new dependencies.