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 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 c19db65a1b1e..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); } } @@ -1339,22 +1358,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..4dab1944a3c4 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,33 @@ 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 + }) => { + // 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 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 }) => {