diff --git a/bun.lock b/bun.lock index 64887ff4..1c03c238 100644 --- a/bun.lock +++ b/bun.lock @@ -357,7 +357,7 @@ }, "packages/dev-server": { "name": "@wrnexus/dev-server", - "version": "0.8.57", + "version": "0.8.58", "dependencies": { "@wrnexus/authz": "workspace:*", "@wrnexus/cache": "workspace:*", diff --git a/packages/dev-server/package.json b/packages/dev-server/package.json index 6e5ce518..95541951 100644 --- a/packages/dev-server/package.json +++ b/packages/dev-server/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/dev-server", - "version": "0.8.57", + "version": "0.8.58", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/dev-server/src/runtime.ts b/packages/dev-server/src/runtime.ts index 537fa319..b8eed1eb 100644 --- a/packages/dev-server/src/runtime.ts +++ b/packages/dev-server/src/runtime.ts @@ -2622,8 +2622,9 @@ function extractSlots(inner: string): { named: Record; def: stri const named: Record = {}; if (!inner.includes("data-slot=")) return { named, def: inner }; - const SLOT_MOUNT_RE = - /<([A-Za-z][A-Za-z0-9-]*)\b([^>]*?\bdata-slot="([A-Za-z0-9_-]+)"[^>]*?)(\/?)>/; + // Only direct children belong to this mount. Descendant named slots belong + // to their own component (for example a splitter inside a shell's slot). + const SLOT_MOUNT_RE = /<([A-Za-z][A-Za-z0-9-]*)\b([^>]*?)(\/?)>/; let def = ""; let i = 0; for (;;) { @@ -2634,11 +2635,16 @@ function extractSlots(inner: string): { named: Record; def: stri } const tagStart = i + m.index; def += inner.slice(i, tagStart); - const [open, tag, , name, selfClose] = m; + const [open, tag, attributes, selfClose] = m; + const name = /\bdata-slot="([A-Za-z0-9_-]+)"/.exec(attributes!)?.[1]; const openEnd = tagStart + open.length; const { inner: content, end } = - selfClose === "/" ? { inner: "", end: openEnd } : readElementBody(inner, tag!, openEnd); - named[name!] = content; + selfClose === "/" || + /^(area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)$/i.test(tag!) + ? { inner: "", end: openEnd } + : readElementBody(inner, tag!, openEnd); + if (name) named[name] = content; + else def += inner.slice(tagStart, end); i = end; } return { named, def }; diff --git a/packages/dev-server/test/nested-named-slots.test.ts b/packages/dev-server/test/nested-named-slots.test.ts new file mode 100644 index 00000000..fc6fabdc --- /dev/null +++ b/packages/dev-server/test/nested-named-slots.test.ts @@ -0,0 +1,24 @@ +import { expect, test } from "bun:test"; +import { fillSlots } from "../src/runtime.ts"; + +test("an outer default slot preserves a nested component's named slots", () => { + const child = + '
Inbox
Message
'; + const shell = fillSlots("
", child, true); + expect(shell).toContain(child); + const panes = fillSlots( + '
', + '
Inbox
Message
', + true, + ); + expect(panes).toContain("Inbox"); + expect(panes).toContain("Message"); +}); + +test("void children do not swallow following direct named slots", () => { + const html = fillSlots( + '', + 'Text
Heading

Body

', + ); + expect(html).toBe("HeadingText

Body

"); +});