Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .changeset/atproto-comments-external-svelte.md
Original file line number Diff line number Diff line change
@@ -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.
26 changes: 13 additions & 13 deletions apps/host/src/routes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -152,9 +152,8 @@ import "@svebcomponents/atproto.comments";`;
<div class="eyebrow"><span></span> Open social comments</div>
<h1>Your post.<br /><em>Their voices.</em></h1>
<p class="lede">
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.
</p>
<div class="hero-actions">
<a class="button primary" href="#start">Add to your site</a>
Expand All @@ -177,7 +176,8 @@ import "@svebcomponents/atproto.comments";`;
&lt;<span class="code-pink">script</span> <span class="code-blue">type</span
>=<span class="code-green">"module"</span>
<span class="code-blue">src</span>=<span class="code-green"
>"https://cdn.jsdelivr.net/npm/@svebcomponents/atproto.comments"</span
>"https://cdn.jsdelivr.net/npm/<wbr />@svebcomponents/<wbr
/>atproto.comments"</span
>&gt;
&lt;/<span class="code-pink">script</span>&gt;

Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions components/atproto-comments/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
},
Expand Down
63 changes: 40 additions & 23 deletions components/atproto-comments/svebcomponents.config.ts
Original file line number Diff line number Diff line change
@@ -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;