From 4f690c7a3049fb6ba65c4e91c7ed10a5657eea16 Mon Sep 17 00:00:00 2001 From: Tee Ming Date: Sat, 28 Feb 2026 01:15:20 +0800 Subject: [PATCH 1/9] reimplement treeshaking --- .vscode/settings.json | 15 +++ packages/kit/src/exports/vite/build/remote.js | 110 ++++++++++++++++++ packages/kit/src/exports/vite/index.js | 5 +- .../kit/test/apps/async/test/server.test.js | 16 ++- 4 files changed, 141 insertions(+), 5 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 packages/kit/src/exports/vite/build/remote.js diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000000..abe447de8bf5 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,15 @@ +{ + "search.exclude": { + "**/.custom-out-dir": true, + "**/.netlify": true, + "**/.svelte-kit": true, + "**/.wrangler": true, + "**/CHANGELOG*.md": true, + "**/dist": true, + "**/test/**/build": true, + "**/test-results": true, + "packages/adapter-cloudflare/files": true, + "packages/adapter-netlify/files": true, + "packages/adapter-node/files": true + } +} diff --git a/packages/kit/src/exports/vite/build/remote.js b/packages/kit/src/exports/vite/build/remote.js new file mode 100644 index 000000000000..4bde95cd5771 --- /dev/null +++ b/packages/kit/src/exports/vite/build/remote.js @@ -0,0 +1,110 @@ +/** @import { ServerMetadata } from 'types' */ +/** @import { OutputBundle } from 'rollup' */ + +import fs from 'node:fs'; +import path from 'node:path'; +import { Parser } from 'acorn'; +import MagicString from 'magic-string'; +import { posixify } from '../../../utils/filesystem.js'; +import { import_peer } from '../../../utils/import.js'; +import { s } from '../../../utils/misc.js'; + +/** + * @param {string} out + * @param {Array<{ hash: string, file: string }>} remotes + * @param {ServerMetadata} metadata + * @param {string} cwd + * @param {OutputBundle} server_bundle + */ +export async function treeshake_prerendered_remotes(out, remotes, metadata, cwd, server_bundle) { + if (remotes.length === 0) return; + + const vite = /** @type {typeof import('vite')} */ (await import_peer('vite')); + + for (const remote of remotes) { + const exports_map = metadata.remotes.get(remote.hash); + if (!exports_map) continue; + + /** @type {string[]} */ + const dynamic = []; + /** @type {string[]} */ + const prerendered = []; + + for (const [name, value] of exports_map) { + (value.dynamic ? dynamic : prerendered).push(name); + } + + if (prerendered.length === 0) continue; // nothing to treeshake + + // remove file extension + const remote_filename = path.basename(remote.file).split('.').slice(0, -1).join('.'); + + const remote_chunk = Object.values(server_bundle).find((chunk) => { + return chunk.name === remote_filename; + }); + + if (!remote_chunk) return; + + const chunk_path = posixify(path.relative(cwd, `${out}/server/${remote_chunk.fileName}`)); + + const code = fs.readFileSync(chunk_path, 'utf-8'); + const parsed = Parser.parse(code, { sourceType: 'module', ecmaVersion: 'latest' }); + const modified_code = new MagicString(code); + + for (const fn of prerendered) { + for (const node of parsed.body) { + const declaration = + node.type === 'ExportNamedDeclaration' + ? node.declaration + : node.type === 'VariableDeclaration' + ? node + : null; + + if (!declaration || declaration.type !== 'VariableDeclaration') continue; + + for (const declarator of declaration.declarations) { + if (declarator.id.type === 'Identifier' && declarator.id.name === fn) { + modified_code.overwrite( + node.start, + node.end, + `const ${fn} = { __: { type: 'prerender', id: ${s(`${remote.hash}/${fn}`)}, name: ${s(fn)} } }` + ); + } + } + } + } + + for (const node of parsed.body) { + if (node.type === 'ExportDefaultDeclaration') { + modified_code.remove(node.start, node.end); + } + } + + const stubbed = modified_code.toString(); + fs.writeFileSync(chunk_path, stubbed); + + const bundle = /** @type {import('vite').Rollup.RollupOutput} */ ( + await vite.build({ + configFile: false, + build: { + write: false, + ssr: true, + target: 'esnext', + rollupOptions: { + // treat everything as external + external: (id) => !id.endsWith(chunk_path), + input: { + treeshaken: chunk_path + } + } + } + }) + ); + + const chunk = bundle.output.find((c) => c.type === 'chunk' && c.name === 'treeshaken'); + + if (chunk && chunk.type === 'chunk') { + fs.writeFileSync(chunk_path, chunk.code); + } + } +} diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index 72db04111e56..a2d9b93b482f 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -41,6 +41,7 @@ import { import { import_peer } from '../../utils/import.js'; import { compact } from '../../utils/array.js'; import { should_ignore, has_children } from './static_analysis/utils.js'; +import { treeshake_prerendered_remotes } from './build/remote.js'; const cwd = posixify(process.cwd()); @@ -1048,7 +1049,7 @@ async function kit({ svelte_config }) { */ writeBundle: { sequential: true, - async handler(_options) { + async handler(_options, server_bundle) { if (secondary_build_started) return; // only run this once const verbose = vite_config.logLevel === 'info'; @@ -1303,6 +1304,8 @@ async function kit({ svelte_config }) { env: { ...env.private, ...env.public } }); + await treeshake_prerendered_remotes(out, remotes, metadata, cwd, server_bundle); + // generate a new manifest that doesn't include prerendered pages fs.writeFileSync( `${out}/server/manifest.js`, diff --git a/packages/kit/test/apps/async/test/server.test.js b/packages/kit/test/apps/async/test/server.test.js index 69412eadbcbe..ebb000518681 100644 --- a/packages/kit/test/apps/async/test/server.test.js +++ b/packages/kit/test/apps/async/test/server.test.js @@ -1,9 +1,9 @@ -import process from 'node:process'; -import { expect } from '@playwright/test'; -import { test } from '../../../utils.js'; import fs from 'node:fs'; +import process from 'node:process'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; +import { expect } from '@playwright/test'; +import { test } from '../../../utils.js'; test.skip(({ javaScriptEnabled }) => javaScriptEnabled); @@ -11,7 +11,15 @@ const root = path.resolve(fileURLToPath(import.meta.url), '..', '..'); test.describe('remote functions', () => { test("doesn't write bundle to disk when treeshaking prerendered remote functions", () => { - test.skip(!!process.env.DEV, 'skip when in dev mode'); + test.skip(!!process.env.DEV, 'only applicable after build'); expect(fs.existsSync(path.join(root, 'dist'))).toBe(false); }); + + test('non-dynamic prerendered remote functions are treeshaken', () => { + test.skip(!!process.env.DEV, 'only applicable after build'); + const code = fs.readFileSync( + path.join(root, '.svelte-kit', 'output', 'server', 'chunks', 'prerender.remote.js') + ); + expect(code.includes('with_read')).toBe(false); + }); }); From 854a82b28cb049dbbb0127ff03a5af4154193d48 Mon Sep 17 00:00:00 2001 From: Tee Ming Date: Sat, 28 Feb 2026 01:16:08 +0800 Subject: [PATCH 2/9] changeset --- .changeset/thin-turkeys-give.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/thin-turkeys-give.md diff --git a/.changeset/thin-turkeys-give.md b/.changeset/thin-turkeys-give.md new file mode 100644 index 000000000000..111dec8b6bc2 --- /dev/null +++ b/.changeset/thin-turkeys-give.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: reimplement treeshaking non-dynamic prerendered remote functions From 959280579dd3b70edb4f714027f0de008d62a103 Mon Sep 17 00:00:00 2001 From: Tee Ming Date: Sat, 28 Feb 2026 01:20:08 +0800 Subject: [PATCH 3/9] whoops --- .vscode/settings.json | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index abe447de8bf5..000000000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "search.exclude": { - "**/.custom-out-dir": true, - "**/.netlify": true, - "**/.svelte-kit": true, - "**/.wrangler": true, - "**/CHANGELOG*.md": true, - "**/dist": true, - "**/test/**/build": true, - "**/test-results": true, - "packages/adapter-cloudflare/files": true, - "packages/adapter-netlify/files": true, - "packages/adapter-node/files": true - } -} From 3415251fd94ec7d698854312e43ad34f0b5f73da Mon Sep 17 00:00:00 2001 From: Tee Ming Date: Sat, 28 Feb 2026 01:25:18 +0800 Subject: [PATCH 4/9] Update packages/kit/src/exports/vite/build/remote.js Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com> --- packages/kit/src/exports/vite/build/remote.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kit/src/exports/vite/build/remote.js b/packages/kit/src/exports/vite/build/remote.js index 4bde95cd5771..0853361a9893 100644 --- a/packages/kit/src/exports/vite/build/remote.js +++ b/packages/kit/src/exports/vite/build/remote.js @@ -43,7 +43,7 @@ export async function treeshake_prerendered_remotes(out, remotes, metadata, cwd, return chunk.name === remote_filename; }); - if (!remote_chunk) return; + if (!remote_chunk) continue; const chunk_path = posixify(path.relative(cwd, `${out}/server/${remote_chunk.fileName}`)); From 065e6f5088a763d13be70849f7718be311b11053 Mon Sep 17 00:00:00 2001 From: Tee Ming Date: Sat, 28 Feb 2026 01:36:51 +0800 Subject: [PATCH 5/9] fix test --- packages/kit/test/apps/async/test/server.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kit/test/apps/async/test/server.test.js b/packages/kit/test/apps/async/test/server.test.js index ebb000518681..424524ebd262 100644 --- a/packages/kit/test/apps/async/test/server.test.js +++ b/packages/kit/test/apps/async/test/server.test.js @@ -20,6 +20,6 @@ test.describe('remote functions', () => { const code = fs.readFileSync( path.join(root, '.svelte-kit', 'output', 'server', 'chunks', 'prerender.remote.js') ); - expect(code.includes('with_read')).toBe(false); + expect(code.includes('const with_read = prerender(')).toBe(false); }); }); From ff8a7cb2378b626be2a30595509e67698d55d689 Mon Sep 17 00:00:00 2001 From: Tee Ming Date: Sat, 28 Feb 2026 02:54:48 +0800 Subject: [PATCH 6/9] llm hallucination --- packages/kit/src/exports/vite/build/remote.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kit/src/exports/vite/build/remote.js b/packages/kit/src/exports/vite/build/remote.js index 0853361a9893..926de5c8b7c2 100644 --- a/packages/kit/src/exports/vite/build/remote.js +++ b/packages/kit/src/exports/vite/build/remote.js @@ -67,7 +67,7 @@ export async function treeshake_prerendered_remotes(out, remotes, metadata, cwd, modified_code.overwrite( node.start, node.end, - `const ${fn} = { __: { type: 'prerender', id: ${s(`${remote.hash}/${fn}`)}, name: ${s(fn)} } }` + `const ${fn} = prerender('unchecked', () => { throw new Error('Unexpectedly called prerender function. Did you forget to set { dynamic: true } ?') });` ); } } From 733fd0f88629e488167945d14416884e11ce559e Mon Sep 17 00:00:00 2001 From: Tee Ming Date: Sat, 28 Feb 2026 03:08:18 +0800 Subject: [PATCH 7/9] oops --- packages/kit/src/exports/vite/build/remote.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/kit/src/exports/vite/build/remote.js b/packages/kit/src/exports/vite/build/remote.js index 926de5c8b7c2..e13a951497ea 100644 --- a/packages/kit/src/exports/vite/build/remote.js +++ b/packages/kit/src/exports/vite/build/remote.js @@ -7,7 +7,6 @@ import { Parser } from 'acorn'; import MagicString from 'magic-string'; import { posixify } from '../../../utils/filesystem.js'; import { import_peer } from '../../../utils/import.js'; -import { s } from '../../../utils/misc.js'; /** * @param {string} out From 58289aab01514a9bfe7c3259bac1008a9cd8d7b2 Mon Sep 17 00:00:00 2001 From: Tee Ming Date: Tue, 3 Mar 2026 22:54:47 +0800 Subject: [PATCH 8/9] respect sourcemap option --- packages/kit/src/exports/vite/build/remote.js | 28 ++++++++++++++----- packages/kit/src/exports/vite/index.js | 9 +++++- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/packages/kit/src/exports/vite/build/remote.js b/packages/kit/src/exports/vite/build/remote.js index e13a951497ea..79bb6ce1c548 100644 --- a/packages/kit/src/exports/vite/build/remote.js +++ b/packages/kit/src/exports/vite/build/remote.js @@ -14,8 +14,16 @@ import { import_peer } from '../../../utils/import.js'; * @param {ServerMetadata} metadata * @param {string} cwd * @param {OutputBundle} server_bundle + * @param {NonNullable['sourcemap']} sourcemap */ -export async function treeshake_prerendered_remotes(out, remotes, metadata, cwd, server_bundle) { +export async function treeshake_prerendered_remotes( + out, + remotes, + metadata, + cwd, + server_bundle, + sourcemap +) { if (remotes.length === 0) return; const vite = /** @type {typeof import('vite')} */ (await import_peer('vite')); @@ -79,9 +87,6 @@ export async function treeshake_prerendered_remotes(out, remotes, metadata, cwd, } } - const stubbed = modified_code.toString(); - fs.writeFileSync(chunk_path, stubbed); - const bundle = /** @type {import('vite').Rollup.RollupOutput} */ ( await vite.build({ configFile: false, @@ -89,8 +94,9 @@ export async function treeshake_prerendered_remotes(out, remotes, metadata, cwd, write: false, ssr: true, target: 'esnext', + sourcemap, rollupOptions: { - // treat everything as external + // avoid resolving imports external: (id) => !id.endsWith(chunk_path), input: { treeshaken: chunk_path @@ -100,10 +106,18 @@ export async function treeshake_prerendered_remotes(out, remotes, metadata, cwd, }) ); - const chunk = bundle.output.find((c) => c.type === 'chunk' && c.name === 'treeshaken'); - + const chunk = bundle.output.find( + (output) => output.type === 'chunk' && output.name === 'treeshaken' + ); if (chunk && chunk.type === 'chunk') { fs.writeFileSync(chunk_path, chunk.code); + + const chunk_sourcemap = bundle.output.find( + (output) => output.type === 'asset' && output.fileName === chunk.fileName + '.map' + ); + if (chunk_sourcemap && chunk_sourcemap.type === 'asset') { + fs.writeFileSync(chunk_path + '.map', chunk_sourcemap.source); + } } } } diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index a2d9b93b482f..49374826ee18 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -1304,7 +1304,14 @@ async function kit({ svelte_config }) { env: { ...env.private, ...env.public } }); - await treeshake_prerendered_remotes(out, remotes, metadata, cwd, server_bundle); + await treeshake_prerendered_remotes( + out, + remotes, + metadata, + cwd, + server_bundle, + vite_config.build.sourcemap + ); // generate a new manifest that doesn't include prerendered pages fs.writeFileSync( From 7e63740aeeff231a7018d44f6125821451944f9e Mon Sep 17 00:00:00 2001 From: Tee Ming Date: Tue, 3 Mar 2026 23:11:06 +0800 Subject: [PATCH 9/9] accidental removal --- packages/kit/src/exports/vite/build/remote.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/kit/src/exports/vite/build/remote.js b/packages/kit/src/exports/vite/build/remote.js index 79bb6ce1c548..689882593e92 100644 --- a/packages/kit/src/exports/vite/build/remote.js +++ b/packages/kit/src/exports/vite/build/remote.js @@ -87,6 +87,9 @@ export async function treeshake_prerendered_remotes( } } + const stubbed = modified_code.toString(); + fs.writeFileSync(chunk_path, stubbed); + const bundle = /** @type {import('vite').Rollup.RollupOutput} */ ( await vite.build({ configFile: false,