diff --git a/.changeset/client-only-named-export.md b/.changeset/client-only-named-export.md new file mode 100644 index 000000000..30b7c86a1 --- /dev/null +++ b/.changeset/client-only-named-export.md @@ -0,0 +1,11 @@ +--- +"@solidjs/start": patch +--- + +Let a `clientOnly` loader resolve to the component itself, so a named export works without wrapping it in `{ default }`: + +```tsx +const Chart = clientOnly(() => import("./chart").then(({ Chart }) => Chart)); +``` + +Destructuring the export inside the loader also lets the bundler leave the module's other exports out of the chunk. diff --git a/apps/tests/src/e2e/client-only.test.ts b/apps/tests/src/e2e/client-only.test.ts new file mode 100644 index 000000000..8c2c3af40 --- /dev/null +++ b/apps/tests/src/e2e/client-only.test.ts @@ -0,0 +1,33 @@ +import { expect, test } from "@playwright/test"; + +// These use their own routes rather than /client-only: in bundled dev mode, a +// visit to /client-only changes how its route module is loaded afterwards, +// which hydration-navigation.test.ts relies on. +test.describe("clientOnly", () => { + test("renders a default export only on the client", async ({ page, request }) => { + const html = await (await request.get("/client-only/default")).text(); + expect(html).toContain("CLIENT_ONLY_FALLBACK"); + expect(html).not.toContain("CLIENT_ONLY_DEFAULT_EXPORT_RENDERED"); + + await page.goto("/client-only/default"); + await expect(page.locator("#client-only-default")).toHaveText( + "CLIENT_ONLY_DEFAULT_EXPORT_RENDERED", + ); + await expect(page.locator("#client-only-fallback")).toHaveCount(0); + }); + + test("renders a named export when the loader resolves to the component", async ({ + page, + request, + }) => { + const html = await (await request.get("/client-only/named")).text(); + expect(html).toContain("CLIENT_ONLY_FALLBACK"); + expect(html).not.toContain("CLIENT_ONLY_NAMED_EXPORT_RENDERED"); + + await page.goto("/client-only/named"); + await expect(page.locator("#client-only-named")).toHaveText( + "CLIENT_ONLY_NAMED_EXPORT_RENDERED", + ); + await expect(page.locator("#client-only-fallback")).toHaveCount(0); + }); +}); diff --git a/apps/tests/src/routes/client-only/_default.tsx b/apps/tests/src/routes/client-only/_default.tsx new file mode 100644 index 000000000..6a808fdd7 --- /dev/null +++ b/apps/tests/src/routes/client-only/_default.tsx @@ -0,0 +1,3 @@ +export default function DefaultClientOnly() { + return
CLIENT_ONLY_DEFAULT_EXPORT_RENDERED
; +} diff --git a/apps/tests/src/routes/client-only/_named.tsx b/apps/tests/src/routes/client-only/_named.tsx new file mode 100644 index 000000000..2555f9213 --- /dev/null +++ b/apps/tests/src/routes/client-only/_named.tsx @@ -0,0 +1,8 @@ +export function NamedClientOnly() { + returnCLIENT_ONLY_NAMED_EXPORT_RENDERED
; +} + +// Never imported through clientOnly, so it should stay out of the client bundle. +export function UnusedClientOnly() { + returnCLIENT_ONLY_UNUSED_EXPORT_MARKER
; +} diff --git a/apps/tests/src/routes/client-only/default.tsx b/apps/tests/src/routes/client-only/default.tsx new file mode 100644 index 000000000..54e94ec6b --- /dev/null +++ b/apps/tests/src/routes/client-only/default.tsx @@ -0,0 +1,7 @@ +import { clientOnly } from "@solidjs/start"; + +const DefaultClientOnly = clientOnly(() => import("./_default")); + +export default function ClientOnlyDefault() { + return