Skip to content

fix(build): keep precompiled Svelte output valid across all of Svelte 5 - #703

Open
berkayozdin wants to merge 1 commit into
embedpdf:mainfrom
berkayozdin:fix/svelte-556-rest-props-compat
Open

fix(build): keep precompiled Svelte output valid across all of Svelte 5#703
berkayozdin wants to merge 1 commit into
embedpdf:mainfrom
berkayozdin:fix/svelte-556-rest-props-compat

Conversation

@berkayozdin

Copy link
Copy Markdown

Fixes #702.

Problem

Svelte 5.56.0 changed the exclude argument of the private rest_props runtime helper from an array to a Set (src/internal/client/reactivity/props.js): all four traps of rest_props_handler went from target.exclude.includes(key) to target.exclude.has(key), and the compiler started hoisting new Set([...]) instead of emitting an array literal at the call site.

The */svelte entry points ship precompiled component JS. 2.14.4 was built with the pinned svelte@5.50.1, so its output passes an array into a runtime that now calls .has() on it — every EmbedPDF Svelte component throws TypeError: exclude.has is not a function at mount on Svelte >= 5.56, and Svelte aborts the render.

A rebuild against a newer Svelte alone would only move the failure: the peer range is svelte: ">=5 <6", and no single precompiled bundle satisfies both sides of the 5.56 boundary.

Fix

A small post-compile step in the Svelte build mode (@embedpdf/build) rewrites

let restProps = $.rest_props($$props, ['$$slots', '$$events', '$$legacy', 'documentId', 'class']);

to call a wrapper appended to the same module:

function $$rest_props_compat(props, exclude, name) {
	const set = exclude instanceof Set ? exclude : new Set(exclude);
	if (typeof set.includes !== 'function') set.includes = Set.prototype.has.bind(set);
	return $.rest_props(props, set, name);
}

A Set with an includes alias satisfies both runtimes: in the non-legacy handler exclude is only ever read through .includes/.has — never iterated, sized or mutated — so nothing else needs emulating. legacy_rest_props is deliberately left alone; its array contract did not change.

This keeps svelte: ">=5 <6" truthful without dropping support for anyone, and it is source-compatible either way: the same rewrite works whether the compiler emits an array (< 5.56) or a hoisted Set (>= 5.56), so it stays correct when you bump the build toolchain.

Verification

Same component compiled with each compiler, then mounted (jsdom) against each runtime, asserting rest-prop passthrough and exclusion (the {...restProps} spread exercises the get, has, ownKeys and getOwnPropertyDescriptor traps):

component built with runtime 5.50.1 runtime 5.56.7
5.50.1, unpatched exclude.has is not a function
5.56.7, unpatched exclude.includes is not a function
5.50.1, patched
5.56.7, patched

Also built for real through the repo's own pipeline (@embedpdf/buildmodelscoreplugin-viewport, vite build --mode svelte, on the lockfile's svelte@5.50.1): plugin-viewport/dist/svelte/index.js contains no bare rest_props call site left, and pnpm install --frozen-lockfile is clean.

Two notes from working in the repo, unrelated to this PR: typescript: "latest" at the root now resolves to TypeScript 7, which fails @embedpdf/build's tsc (svelte-dts-resolver.ts) on a fresh install — I pinned 5.9.3 locally to run the build, but left that out of the diff. And packages/build's svelte: ^5.39.5 resolves to 5.56.x on a fresh resolution, so the next lockfile refresh would have shipped Set-form output and broken users below 5.56 — this change makes that safe either way.

The longer-term fix

The durable answer is publishing the Svelte layer as uncompiled .svelte source (svelte-package + the svelte export condition), so the consuming app's compiler generates internals matching its own Svelte version. svelte/internal/client is explicitly not a stable API, and 5.56 won't be the last change to it. That's a bigger, all-at-once migration of every */svelte subpath (a source-compiled component importing a precompiled one reproduces this exact class of bug), so I kept this PR to the immediate unblock — happy to help with the migration if it's a direction you want to take.


I'm happy to add a regression test if you'd like one — I didn't see a test runner wired up in the repo, so I left the verification out of the diff rather than introduce one unasked.

Investigated and prepared with Claude Code; every claim above was verified by running it.

Svelte 5.56 changed the `exclude` argument of the private `rest_props`
runtime helper from an array (`exclude.includes(key)`) to a Set
(`exclude.has(key)`). Because the Svelte entry points ship precompiled,
output built against one side of that change throws on the other, and
the published packages abort the render of every Svelte component on
Svelte >= 5.56 with `TypeError: exclude.has is not a function`.

Route those calls through a wrapper that hands the runtime a Set
carrying an `includes` alias, so one build satisfies both contracts
across the whole `svelte: ">=5 <6"` peer range.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 26, 2026

Copy link
Copy Markdown

@berkayozdin is attempting to deploy a commit to the OpenBook Team on Vercel.

A member of the Team first needs to authorize it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Precompiled Svelte bindings crash on Svelte >= 5.56 (TypeError: exclude.has is not a function)

1 participant