From be93f014cbdf2d8ae5a87229ff8888bcea0245a4 Mon Sep 17 00:00:00 2001 From: Tee Ming Chew Date: Mon, 1 Jun 2026 02:08:39 +0800 Subject: [PATCH 1/6] fix and test --- packages/kit/src/exports/vite/index.js | 34 ++++++++++++++-------- packages/kit/test/apps/basics/test/test.js | 30 ++++++++++++++----- 2 files changed, 45 insertions(+), 19 deletions(-) diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index e12a9a9bb2c0..f36891ecc455 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -1225,22 +1225,32 @@ async function kit({ svelte_config }) { ) }; + const nodes = manifest_data.nodes.map((node, i) => { + if (node.component || node.universal) { + const entry = `${out_dir}/generated/client-optimized/nodes/${i}.js`; + const deps = deps_of(entry, true); + + /** @type {string | undefined} */ + let file; + + const key = path.relative(vite_config.root, entry); + if (node.page_options?.csr === false && key in client_manifest) { + fs.rmSync(`${out}/client/${client_manifest[key].file}`); + } else { + file = resolve_symlinks(client_manifest, entry).chunk.file; + } + + return { + file, + css: deps.stylesheets + }; + } + }); + // In case of server-side route resolution, we create a purpose-built route manifest that is // similar to that on the client, with as much information computed upfront so that we // don't need to include any code of the actual routes in the server bundle. if (svelte_config.kit.router.resolution === 'server') { - const nodes = manifest_data.nodes.map((node, i) => { - if (node.component || node.universal) { - const entry = `${out_dir}/generated/client-optimized/nodes/${i}.js`; - const deps = deps_of(entry, true); - const file = resolve_symlinks( - client_manifest, - `${out_dir}/generated/client-optimized/nodes/${i}.js` - ).chunk.file; - - return { file, css: deps.stylesheets }; - } - }); build_data.client.nodes = nodes.map((node) => node?.file); build_data.client.css = nodes.map((node) => node?.css); diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js index 768a69f8039d..c0541ca2b956 100644 --- a/packages/kit/test/apps/basics/test/test.js +++ b/packages/kit/test/apps/basics/test/test.js @@ -1,4 +1,5 @@ import process from 'node:process'; +import fs from 'node:fs'; import { expect } from '@playwright/test'; import { test } from '../../../utils.js'; @@ -679,14 +680,29 @@ test.describe('Page options', () => { page, javaScriptEnabled }) => { - if (!javaScriptEnabled) { - await page.goto('/no-csr'); - expect(await page.textContent('h1')).toBe('look ma no javascript'); - expect(await page.$$('link[rel="modulepreload"]')).toHaveLength(0); + test.skip(javaScriptEnabled); - // ensure data wasn't inlined - expect(await page.$$('script[sveltekit\\:data-type="data"]')).toHaveLength(0); - } + await page.goto('/no-csr'); + expect(await page.textContent('h1')).toBe('look ma no javascript'); + expect(await page.$$('link[rel="modulepreload"]')).toHaveLength(0); + + // ensure data wasn't inlined + expect(await page.$$('script[sveltekit\\:data-type="data"]')).toHaveLength(0); + }); + + test('does not include client node in build output with csr=false', async ({ + javaScriptEnabled + }) => { + test.skip(!!(javaScriptEnabled || process.env.DEV)); + + const app = fs.readFileSync('.svelte-kit/generated/client-optimized/app.js', 'utf-8'); + const i = app.match(/"\/no-csr": \[(\d+)\],/)[1]; + + const client_manifest = JSON.parse( + fs.readFileSync('.svelte-kit/output/client/.vite/manifest.json', 'utf-8') + ); + const { file } = client_manifest[`.svelte-kit/generated/client-optimized/nodes/${i}.js`]; + expect(fs.existsSync(`.svelte-kit/output/client/${file}`)).toBe(false); }); test('does not SSR page with ssr=false', async ({ page, javaScriptEnabled }) => { From 47f417f802cd65e0aa5e97041501a4dde6dad1c1 Mon Sep 17 00:00:00 2001 From: Tee Ming Chew Date: Mon, 1 Jun 2026 02:25:45 +0800 Subject: [PATCH 2/6] fix typescript --- packages/kit/test/apps/basics/test/test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js index c0541ca2b956..7c95295bc758 100644 --- a/packages/kit/test/apps/basics/test/test.js +++ b/packages/kit/test/apps/basics/test/test.js @@ -696,7 +696,7 @@ test.describe('Page options', () => { test.skip(!!(javaScriptEnabled || process.env.DEV)); const app = fs.readFileSync('.svelte-kit/generated/client-optimized/app.js', 'utf-8'); - const i = app.match(/"\/no-csr": \[(\d+)\],/)[1]; + const i = app.match(/"\/no-csr": \[(\d+)\],/)?.[1]; const client_manifest = JSON.parse( fs.readFileSync('.svelte-kit/output/client/.vite/manifest.json', 'utf-8') From fee52a61651cbe899b3500a6356585789d50bef6 Mon Sep 17 00:00:00 2001 From: Tee Ming Chew Date: Mon, 1 Jun 2026 02:41:21 +0800 Subject: [PATCH 3/6] fix test --- packages/kit/test/apps/basics/test/test.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js index 7c95295bc758..f4f9de02bf91 100644 --- a/packages/kit/test/apps/basics/test/test.js +++ b/packages/kit/test/apps/basics/test/test.js @@ -695,8 +695,11 @@ test.describe('Page options', () => { }) => { test.skip(!!(javaScriptEnabled || process.env.DEV)); - const app = fs.readFileSync('.svelte-kit/generated/client-optimized/app.js', 'utf-8'); - const i = app.match(/"\/no-csr": \[(\d+)\],/)?.[1]; + const route = (await import('../.svelte-kit/output/server/manifest.js')).manifest._.routes.find( + (route) => route.id === '/no-csr' + ); + const i = route?.page?.leaf; + if (!i) throw new Error('could not find route for /no-csr from server manifest'); const client_manifest = JSON.parse( fs.readFileSync('.svelte-kit/output/client/.vite/manifest.json', 'utf-8') From 8c7ad21fa3551c23421e68fd7abd50ee89e59be4 Mon Sep 17 00:00:00 2001 From: Tee Ming Chew Date: Mon, 1 Jun 2026 03:07:53 +0800 Subject: [PATCH 4/6] disable test for server side router resolution --- packages/kit/test/apps/basics/test/test.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js index f4f9de02bf91..4dab1944a3c4 100644 --- a/packages/kit/test/apps/basics/test/test.js +++ b/packages/kit/test/apps/basics/test/test.js @@ -693,13 +693,14 @@ test.describe('Page options', () => { test('does not include client node in build output with csr=false', async ({ javaScriptEnabled }) => { - test.skip(!!(javaScriptEnabled || process.env.DEV)); - - const route = (await import('../.svelte-kit/output/server/manifest.js')).manifest._.routes.find( - (route) => route.id === '/no-csr' + // we can't easily find the client node when building for server router resolution + // so there's no point in trying to run this test for that test environment + test.skip( + !!(javaScriptEnabled || process.env.DEV || process.env.ROUTER_RESOLUTION === 'server') ); - const i = route?.page?.leaf; - if (!i) throw new Error('could not find route for /no-csr from server manifest'); + + const app = fs.readFileSync('.svelte-kit/generated/client-optimized/app.js', 'utf-8'); + const i = app.match(/"\/no-csr": \[(\d+)\],/)?.[1]; const client_manifest = JSON.parse( fs.readFileSync('.svelte-kit/output/client/.vite/manifest.json', 'utf-8') From b40014f75bf216dc63975c49aa88646e0ee281cd Mon Sep 17 00:00:00 2001 From: Tee Ming Chew Date: Mon, 1 Jun 2026 22:27:19 +0800 Subject: [PATCH 5/6] changeset --- .changeset/yellow-waves-care.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/yellow-waves-care.md diff --git a/.changeset/yellow-waves-care.md b/.changeset/yellow-waves-care.md new file mode 100644 index 000000000000..e06d902bac32 --- /dev/null +++ b/.changeset/yellow-waves-care.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: remove client nodes from the build output for routes with CSR disabled From a8e7715a53b242a6dd35f808db0d4f3eb2643847 Mon Sep 17 00:00:00 2001 From: Tee Ming Chew Date: Sun, 7 Jun 2026 23:41:57 +1000 Subject: [PATCH 6/6] try --- packages/kit/src/exports/vite/build/utils.js | 8 --- packages/kit/src/exports/vite/index.js | 55 +++++++++++++------- 2 files changed, 37 insertions(+), 26 deletions(-) diff --git a/packages/kit/src/exports/vite/build/utils.js b/packages/kit/src/exports/vite/build/utils.js index 303841266c4d..fc4079b80d90 100644 --- a/packages/kit/src/exports/vite/build/utils.js +++ b/packages/kit/src/exports/vite/build/utils.js @@ -122,14 +122,6 @@ export function filter_fonts(assets) { return assets.filter((asset) => /\.(woff2?|ttf|otf)$/.test(asset)); } -/** - * @param {import('types').ValidatedKitConfig} config - * @returns {string} - */ -export function assets_base(config) { - return (config.paths.assets || config.paths.base || '.') + '/'; -} - /** * Writes a function with arguments used by a template literal. * This helps us store strings in a module and inject values at runtime. diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index dc9160ad2aa1..4cb2476e055a 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -26,7 +26,7 @@ import { load_svelte_config, process_config } from '../../core/config/index.js'; import { generate_manifest } from '../../core/generate_manifest/index.js'; import { build_server_nodes } from './build/build_server.js'; import { build_service_worker } from './build/build_service_worker.js'; -import { assets_base, find_deps, resolve_symlinks } from './build/utils.js'; +import { find_deps, resolve_symlinks } from './build/utils.js'; import { dev } from './dev/index.js'; import { preview } from './preview/index.js'; import { error_for_missing_config, get_config_aliases, get_env, normalize_id } from './utils.js'; @@ -1034,18 +1034,20 @@ async function kit({ svelte_config }) { // see the kit.output.preloadStrategy option for details on why we have multiple options here const ext = kit.output.preloadStrategy === 'preload-mjs' ? 'mjs' : 'js'; - // We could always use a relative asset base path here, but it's better for performance not to. - // E.g. Vite generates `new URL('/asset.png', import.meta).href` for a relative path vs just '/asset.png'. - // That's larger and takes longer to run and also causes an HTML diff between SSR and client - // causing us to do a more expensive hydration check. - const client_base = - kit.paths.relative !== false || kit.paths.assets ? './' : kit_paths_base; - const inline = !ssr && svelte_config.kit.output.bundleStrategy === 'inline'; const split = ssr || svelte_config.kit.output.bundleStrategy === 'split'; + /** @type {string} */ + const base = (kit.paths.assets || kit.paths.base) + '/'; + const root_to_assets = prefix + '/assets/'; + const assets_to_root = + prefix + .split('/') + .map(() => '..') + .join('/') + '/../'; + new_config = { - base: ssr ? assets_base(kit) : client_base, + base: './', build: { copyPublicDir: !ssr, cssCodeSplit: svelte_config.kit.output.bundleStrategy !== 'inline', @@ -1094,6 +1096,32 @@ async function kit({ svelte_config }) { hoistTransitiveImports: false } } + }, + experimental: { + // Allows us to use relative paths in as many places as we can + renderBuiltUrl(filename, { ssr, hostType }) { + if (hostType === 'js') { + // SSR builds should use an absolute path in JS modules to + // match the default Vite behaviour + if (ssr) return base + filename; + + // We could always use a relative asset base path here, but it's better for performance not to. + // E.g. Vite generates `new URL('/asset.png', import.meta).href` for a relative path vs just '/asset.png'. + // That's larger and takes longer to run and also causes an HTML diff between SSR and client + // causing us to do a more expensive hydration check. + return { + relative: kit.paths.relative !== false || !!kit.paths.assets + }; + } + + // _app/immutable/assets files + if (filename.startsWith(root_to_assets)) { + return `./${filename.slice(root_to_assets.length)}`; + } + + // static dir files + return assets_to_root + filename; + } } }; @@ -1301,15 +1329,6 @@ async function kit({ svelte_config }) { continue; } - if (file.endsWith('.css')) { - // make absolute paths in CSS relative, for portability - const content = fs - .readFileSync(src, 'utf-8') - .replaceAll(`${kit.paths.base}/${assets_path}`, '.'); - - fs.writeFileSync(src, content); - } - copy(src, dest); } }