Skip to content
Draft
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
5 changes: 5 additions & 0 deletions .changeset/yellow-waves-care.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

fix: remove client nodes from the build output for routes with CSR disabled
8 changes: 0 additions & 8 deletions packages/kit/src/exports/vite/build/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
89 changes: 59 additions & 30 deletions packages/kit/src/exports/vite/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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;
}
}
};

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

Expand Down
34 changes: 27 additions & 7 deletions packages/kit/test/apps/basics/test/test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import process from 'node:process';
import fs from 'node:fs';
import { expect } from '@playwright/test';
import { test } from '../../../utils.js';

Expand Down Expand Up @@ -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 }) => {
Expand Down
Loading