From 1d813b77458d5ad8e142681534981cfb80feeb7e Mon Sep 17 00:00:00 2001 From: Theodor Steiner Date: Sat, 25 Jul 2026 01:53:33 +0900 Subject: [PATCH 1/2] fix(atproto-comments): ship a Svelte-external build to fix hydration crash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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, crashing with "Cannot read properties of null (reading 'f')" and wiping a correctly server-rendered thread (svebcomponents/atproto#7). Emit `dist/client-svelte`/`dist/server-svelte` (Svelte marked external) and declare a `svelte` export condition pointing at them, so bundler consumers 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. The vendored direct-props HydrationHost is now applied to every build variant (matched by outDir/entry rather than array index). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01W9oixH2t2npbf9u3ara93J --- .../atproto-comments-external-svelte.md | 17 +++++ components/atproto-comments/package.json | 2 + .../atproto-comments/svebcomponents.config.ts | 63 ++++++++++++------- 3 files changed, 59 insertions(+), 23 deletions(-) create mode 100644 .changeset/atproto-comments-external-svelte.md 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/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; From 54cb599085ad3e7918b71493d012ccc3672a4e31 Mon Sep 17 00:00:00 2001 From: Theodor Steiner Date: Sat, 25 Jul 2026 02:11:52 +0900 Subject: [PATCH 2/2] fix: improve code snippet design --- apps/host/src/routes/+page.svelte | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) 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;