diff --git a/.changeset/atproto-comments-external-svelte.md b/.changeset/atproto-comments-external-svelte.md new file mode 100644 index 0000000..807ac8e --- /dev/null +++ b/.changeset/atproto-comments-external-svelte.md @@ -0,0 +1,17 @@ +--- +"@svebcomponents/atproto.comments": patch +--- + +Ship a Svelte-external build and route bundler consumers to it via the `svelte` +export condition, fixing a hydration crash (`Cannot read properties of null +(reading 'f')`) that wiped a correctly server-rendered thread on the client. + +The client build bundled its own Svelte runtime. In a SvelteKit/Vite app — +which has its own Svelte — that put **two** Svelte runtimes on the page, and +under `experimental.async` the app's async-boundary hydration would cross into +the component's separate runtime and dereference a null effect. The component +now also emits `dist/client-svelte`/`dist/server-svelte` (Svelte marked +external), and its package `exports` declare a `svelte` condition pointing at +them, so bundlers dedupe the component onto the app's single Svelte runtime. +The default `dist/client` (Svelte bundled) is unchanged and still serves the +CDN / bare-`import` drop-in, which has no app Svelte to dedupe against. diff --git a/apps/host/src/routes/+page.svelte b/apps/host/src/routes/+page.svelte index 9da0057..b592494 100644 --- a/apps/host/src/routes/+page.svelte +++ b/apps/host/src/routes/+page.svelte @@ -152,9 +152,8 @@ import "@svebcomponents/atproto.comments";`;
Open social comments

Your post.
Their voices.

- Turn any ATProto thread into a native comment section. Readers reply - with their own accounts, posts stay in their own repos, and your site - stays refreshingly database-free. + Any ATProto thread, as a native comment section — no database, no + lock-in.

Add to your site @@ -177,7 +176,8 @@ import "@svebcomponents/atproto.comments";`; <script type="module" src="https://cdn.jsdelivr.net/npm/@svebcomponents/atproto.comments""https://cdn.jsdelivr.net/npm/@svebcomponents/atproto.comments"> </script> @@ -631,9 +631,9 @@ import "@svebcomponents/atproto.comments";`; .hero { min-height: 670px; display: grid; - grid-template-columns: 1.02fr 0.98fr; + grid-template-columns: 0.72fr 1.28fr; align-items: center; - gap: clamp(3rem, 8vw, 7rem); + gap: clamp(2rem, 6vw, 4rem); padding: 5.5rem 0 6.5rem; } @@ -669,8 +669,8 @@ import "@svebcomponents/atproto.comments";`; } h1 { - max-width: 700px; - font-size: clamp(4rem, 7.2vw, 6.5rem); + max-width: 420px; + font-size: clamp(2.75rem, 4.6vw, 4.25rem); } h1 em { @@ -683,11 +683,11 @@ import "@svebcomponents/atproto.comments";`; } .lede { - max-width: 590px; - margin: 2rem 0 0; + max-width: 420px; + margin: 1.5rem 0 0; color: #514e49; - font-size: clamp(1.08rem, 1.8vw, 1.3rem); - line-height: 1.62; + font-size: clamp(1rem, 1.4vw, 1.1rem); + line-height: 1.55; } .hero-actions { @@ -776,7 +776,7 @@ import "@svebcomponents/atproto.comments";`; background: #202025; color: #f7f5f0; font: - 0.84rem/1.75 ui-monospace, + 0.95rem/1.75 ui-monospace, SFMono-Regular, Menlo, monospace; diff --git a/components/atproto-comments/package.json b/components/atproto-comments/package.json index 7f190b1..830ed61 100644 --- a/components/atproto-comments/package.json +++ b/components/atproto-comments/package.json @@ -26,10 +26,12 @@ "exports": { ".": { "types": "./dist/client/index.d.ts", + "svelte": "./dist/client-svelte/index.js", "import": "./dist/client/index.js" }, "./ssr": { "types": "./dist/server/ssr.d.ts", + "svelte": "./dist/server-svelte/ssr.js", "import": "./dist/server/ssr.js" } }, diff --git a/components/atproto-comments/svebcomponents.config.ts b/components/atproto-comments/svebcomponents.config.ts index 5b72506..46c4c6c 100644 --- a/components/atproto-comments/svebcomponents.config.ts +++ b/components/atproto-comments/svebcomponents.config.ts @@ -1,33 +1,50 @@ import { fileURLToPath } from "node:url"; import { defineConfig } from "@svebcomponents/build"; -const configs = defineConfig(); const hydrationHost = fileURLToPath( new URL("./src/HydrationHost.svelte", import.meta.url), ); -// @svebcomponents/ssr 0.3.0's shipped HydrationHost destructures $props(). -// Under Svelte 5.56's async SSR integration that produces writable prop -// signals without a parent effect and hydration fails before the user's -// component mounts. Keep the host local until that upstream package includes -// the equivalent direct-props fix. -configs[0] = { - ...configs[0], - alias: { - ...configs[0].alias, - "@svebcomponents/ssr/hydration-host": hydrationHost, - }, - noExternal: [ - ...(configs[0].noExternal ?? []), - /@svebcomponents\/ssr\/hydration/, - ], -}; +// Emit the Svelte-external build variants (dist/client-svelte, dist/server-svelte) +// alongside the default Svelte-bundled build. A bundler consumer (SvelteKit, +// Vite) resolves the package's `svelte` export condition to these, so the +// component shares the host app's single Svelte runtime instead of bundling its +// own. Two runtimes in one page is what produces the +// "Cannot read properties of null (reading 'f')" crash when the app's +// async-boundary hydration (experimental.async) crosses into the component's +// separately-bundled runtime — svebcomponents/atproto#7. The default bundled +// build (dist/client) stays for the CDN / bare `import` drop-in, which has no +// app Svelte to dedupe against. +const configs = defineConfig({ + svelteOutDir: "dist/client-svelte", + ssrSvelteOutDir: "dist/server-svelte", +}); -// Hydration requires byte-for-byte-equivalent host structure on both sides. -// Point the generated server host at the same local source as the client. -configs[2] = { - ...configs[2], - entry: { "ssr-hydration-host": hydrationHost }, -}; +// @svebcomponents/ssr's shipped HydrationHost destructures $props(). Under +// Svelte 5.56's async SSR that produces writable prop signals without a parent +// effect and hydration fails before the user's component mounts. Keep a local +// direct-props host, on *every* build variant (bundled + svelte-external, both +// halves of each side), until upstream ships the equivalent fix. Matching by +// outDir/entry rather than array index keeps this correct as the build's config +// ordering evolves. +for (const config of configs) { + const outDir = typeof config.outDir === "string" ? config.outDir : ""; + // Client builds bundle the host: redirect its import to the local source. + if (outDir === "dist/client" || outDir === "dist/client-svelte") { + config.alias = { + ...config.alias, + "@svebcomponents/ssr/hydration-host": hydrationHost, + }; + } + // Server host builds compile the host itself: point them at the local source + // so both sides stay byte-for-byte-equivalent. + if ( + config.entry && + typeof config.entry === "object" && + "ssr-hydration-host" in config.entry + ) { + config.entry = { ...config.entry, "ssr-hydration-host": hydrationHost }; + } +} export default configs;