Skip to content

Commit 20f2720

Browse files
feat: add bundleStrategy option (#13173)
* feat: add `codeSplitJs` option * fix: only use `manualChunks` when `!codeSplitJS` * Create beige-carpets-wave.md * chore: regenerate types * chore: add option to `options` app for tests * chore: move tests from `options` to `options-2` and add test for bundle * chore: rename chunk, add css test * chore: rename option to `codeSplit` * chore: apply suggestions from code review * this looks unintentional, reverting * tweak docs * DRY out * get rid of Promise.all when codeSplit: false * types * rename to bundleStrategy * more detail * pretty sure this is unused * tweak * small tweaks * Update .changeset/beige-carpets-wave.md --------- Co-authored-by: Rich Harris <[email protected]> Co-authored-by: Rich Harris <[email protected]>
1 parent 8d6c469 commit 20f2720

File tree

17 files changed

+177
-47
lines changed

17 files changed

+177
-47
lines changed

.changeset/beige-carpets-wave.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': minor
3+
---
4+
5+
feat: add `bundleStrategy: 'split' | 'single'` option

packages/kit/src/core/config/index.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ const get_defaults = (prefix = '') => ({
9292
},
9393
inlineStyleThreshold: 0,
9494
moduleExtensions: ['.js', '.ts'],
95-
output: { preloadStrategy: 'modulepreload' },
95+
output: { preloadStrategy: 'modulepreload', bundleStrategy: 'split' },
9696
outDir: join(prefix, '.svelte-kit'),
9797
serviceWorker: {
9898
register: true

packages/kit/src/core/config/options.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,8 @@ const options = object(
142142
outDir: string('.svelte-kit'),
143143

144144
output: object({
145-
preloadStrategy: list(['modulepreload', 'preload-js', 'preload-mjs'], 'modulepreload')
145+
preloadStrategy: list(['modulepreload', 'preload-js', 'preload-mjs']),
146+
bundleStrategy: list(['split', 'single'])
146147
}),
147148

148149
paths: object({

packages/kit/src/exports/public.d.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,15 @@ export interface KitConfig {
498498
* @since 1.8.4
499499
*/
500500
preloadStrategy?: 'modulepreload' | 'preload-js' | 'preload-mjs';
501+
/**
502+
* If `'split'`, splits the app up into multiple .js/.css files so that they are loaded lazily as the user navigates around the app. This is the default, and is recommended for most scenarios.
503+
* If `'single'`, creates just one .js bundle and one .css file containing code for the entire app.
504+
*
505+
* When using `'split'`, you can also adjust the bundling behaviour by setting [`output.experimentalMinChunkSize`](https://rollupjs.org/configuration-options/#output-experimentalminchunksize) and [`output.manualChunks`](https://rollupjs.org/configuration-options/#output-manualchunks)inside your Vite config's [`build.rollupOptions`](https://vite.dev/config/build-options.html#build-rollupoptions).
506+
* @default 'split'
507+
* @since 2.13.0
508+
*/
509+
bundleStrategy?: 'split' | 'single';
501510
};
502511
paths?: {
503512
/**

packages/kit/src/exports/vite/build/build_server.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ import { normalizePath } from 'vite';
1111
* @param {import('vite').Manifest} server_manifest
1212
* @param {import('vite').Manifest | null} client_manifest
1313
* @param {import('vite').Rollup.OutputAsset[] | null} css
14+
* @param {import('types').RecursiveRequired<import('types').ValidatedConfig['kit']['output']>} output_config
1415
*/
15-
export function build_server_nodes(out, kit, manifest_data, server_manifest, client_manifest, css) {
16+
export function build_server_nodes(out, kit, manifest_data, server_manifest, client_manifest, css, output_config) {
1617
mkdirp(`${out}/server/nodes`);
1718
mkdirp(`${out}/server/stylesheets`);
1819

@@ -69,7 +70,7 @@ export function build_server_nodes(out, kit, manifest_data, server_manifest, cli
6970
exports.push(`export const server_id = ${s(node.server)};`);
7071
}
7172

72-
if (client_manifest && (node.universal || node.component)) {
73+
if (client_manifest && (node.universal || node.component) && output_config.bundleStrategy === 'split') {
7374
const entry = find_deps(
7475
client_manifest,
7576
`${normalizePath(kit.outDir)}/generated/client-optimized/nodes/${i}.js`,

packages/kit/src/exports/vite/index.js

Lines changed: 55 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,10 @@ async function kit({ svelte_config }) {
361361
name: 'vite-plugin-sveltekit-virtual-modules',
362362

363363
resolveId(id, importer) {
364+
if (id === '__sveltekit/manifest') {
365+
return `${kit.outDir}/generated/client-optimized/app.js`;
366+
}
367+
364368
// If importing from a service-worker, only allow $service-worker & $env/static/public, but none of the other virtual modules.
365369
// This check won't catch transitive imports, but it will warn when the import comes from a service-worker directly.
366370
// Transitive imports will be caught during the build.
@@ -605,10 +609,11 @@ async function kit({ svelte_config }) {
605609
const name = posixify(path.join('entries/matchers', key));
606610
input[name] = path.resolve(file);
607611
});
612+
} else if (svelte_config.kit.output.bundleStrategy !== 'split') {
613+
input['bundle'] = `${runtime_directory}/client/bundle.js`;
608614
} else {
609615
input['entry/start'] = `${runtime_directory}/client/entry.js`;
610616
input['entry/app'] = `${kit.outDir}/generated/client-optimized/app.js`;
611-
612617
manifest_data.nodes.forEach((node, i) => {
613618
if (node.component || node.universal) {
614619
input[`nodes/${i}`] = `${kit.outDir}/generated/client-optimized/nodes/${i}.js`;
@@ -643,7 +648,9 @@ async function kit({ svelte_config }) {
643648
chunkFileNames: ssr ? 'chunks/[name].js' : `${prefix}/chunks/[name].[hash].${ext}`,
644649
assetFileNames: `${prefix}/assets/[name].[hash][extname]`,
645650
hoistTransitiveImports: false,
646-
sourcemapIgnoreList
651+
sourcemapIgnoreList,
652+
manualChunks:
653+
svelte_config.kit.output.bundleStrategy === 'single' ? () => 'bundle' : undefined
647654
},
648655
preserveEntrySignatures: 'strict'
649656
},
@@ -775,7 +782,15 @@ async function kit({ svelte_config }) {
775782
// first, build server nodes without the client manifest so we can analyse it
776783
log.info('Analysing routes');
777784

778-
build_server_nodes(out, kit, manifest_data, server_manifest, null, null);
785+
build_server_nodes(
786+
out,
787+
kit,
788+
manifest_data,
789+
server_manifest,
790+
null,
791+
null,
792+
svelte_config.output
793+
);
779794

780795
const metadata = await analyse({
781796
manifest_path,
@@ -825,19 +840,34 @@ async function kit({ svelte_config }) {
825840

826841
const deps_of = /** @param {string} f */ (f) =>
827842
find_deps(client_manifest, posixify(path.relative('.', f)), false);
828-
const start = deps_of(`${runtime_directory}/client/entry.js`);
829-
const app = deps_of(`${kit.outDir}/generated/client-optimized/app.js`);
830-
831-
build_data.client = {
832-
start: start.file,
833-
app: app.file,
834-
imports: [...start.imports, ...app.imports],
835-
stylesheets: [...start.stylesheets, ...app.stylesheets],
836-
fonts: [...start.fonts, ...app.fonts],
837-
uses_env_dynamic_public: output.some(
838-
(chunk) => chunk.type === 'chunk' && chunk.modules[env_dynamic_public]
839-
)
840-
};
843+
844+
if (svelte_config.kit.output.bundleStrategy === 'split') {
845+
const start = deps_of(`${runtime_directory}/client/entry.js`);
846+
const app = deps_of(`${kit.outDir}/generated/client-optimized/app.js`);
847+
848+
build_data.client = {
849+
start: start.file,
850+
app: app.file,
851+
imports: [...start.imports, ...app.imports],
852+
stylesheets: [...start.stylesheets, ...app.stylesheets],
853+
fonts: [...start.fonts, ...app.fonts],
854+
uses_env_dynamic_public: output.some(
855+
(chunk) => chunk.type === 'chunk' && chunk.modules[env_dynamic_public]
856+
)
857+
};
858+
} else {
859+
const start = deps_of(`${runtime_directory}/client/bundle.js`);
860+
861+
build_data.client = {
862+
start: start.file,
863+
imports: start.imports,
864+
stylesheets: start.stylesheets,
865+
fonts: start.fonts,
866+
uses_env_dynamic_public: output.some(
867+
(chunk) => chunk.type === 'chunk' && chunk.modules[env_dynamic_public]
868+
)
869+
};
870+
}
841871

842872
const css = output.filter(
843873
/** @type {(value: any) => value is import('vite').Rollup.OutputAsset} */
@@ -855,7 +885,15 @@ async function kit({ svelte_config }) {
855885
);
856886

857887
// regenerate nodes with the client manifest...
858-
build_server_nodes(out, kit, manifest_data, server_manifest, client_manifest, css);
888+
build_server_nodes(
889+
out,
890+
kit,
891+
manifest_data,
892+
server_manifest,
893+
client_manifest,
894+
css,
895+
svelte_config.kit.output
896+
);
859897

860898
// ...and prerender
861899
const { prerendered, prerender_map } = await prerender({
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/* if `bundleStrategy === 'single'`, this file is used as the entry point */
2+
3+
import * as kit from './entry.js';
4+
5+
// @ts-expect-error
6+
import * as app from '__sveltekit/manifest';
7+
8+
/**
9+
*
10+
* @param {HTMLElement} element
11+
* @param {import('./types.js').HydrateOptions} options
12+
*/
13+
export function start(element, options) {
14+
kit.start(app, element, options);
15+
}

packages/kit/src/runtime/client/client.js

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2354,15 +2354,7 @@ function _start_router() {
23542354

23552355
/**
23562356
* @param {HTMLElement} target
2357-
* @param {{
2358-
* status: number;
2359-
* error: App.Error | null;
2360-
* node_ids: number[];
2361-
* params: Record<string, string>;
2362-
* route: { id: string | null };
2363-
* data: Array<import('types').ServerDataNode | null>;
2364-
* form: Record<string, any> | null;
2365-
* }} opts
2357+
* @param {import('./types.js').HydrateOptions} opts
23662358
*/
23672359
async function _hydrate(
23682360
target,

packages/kit/src/runtime/client/types.d.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
import { SvelteComponent } from 'svelte';
2-
import { ClientHooks, CSRPageNode, CSRPageNodeLoader, CSRRoute, TrailingSlash, Uses } from 'types';
2+
import {
3+
ClientHooks,
4+
CSRPageNode,
5+
CSRPageNodeLoader,
6+
CSRRoute,
7+
ServerDataNode,
8+
TrailingSlash,
9+
Uses
10+
} from 'types';
311
import { Page, ParamMatcher } from '@sveltejs/kit';
412

513
export interface SvelteKitApp {
@@ -88,3 +96,13 @@ export interface NavigationState {
8896
route: CSRRoute | null;
8997
url: URL;
9098
}
99+
100+
export interface HydrateOptions {
101+
status: number;
102+
error: App.Error | null;
103+
node_ids: number[];
104+
params: Record<string, string>;
105+
route: { id: string | null };
106+
data: Array<ServerDataNode | null>;
107+
form: Record<string, any> | null;
108+
}

packages/kit/src/runtime/server/page/render.js

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,7 @@ export async function render_response({
354354
${properties.join(',\n\t\t\t\t\t\t')}
355355
};`);
356356

357-
const args = ['app', 'element'];
357+
const args = ['element'];
358358

359359
blocks.push('const element = document.currentScript.parentElement;');
360360

@@ -392,24 +392,26 @@ export async function render_response({
392392
args.push(`{\n${indent}\t${hydrate.join(`,\n${indent}\t`)}\n${indent}}`);
393393
}
394394

395+
// `client.app` is a proxy for `bundleStrategy !== 'single'`
396+
const boot = client.app
397+
? `Promise.all([
398+
import(${s(prefixed(client.start))}),
399+
import(${s(prefixed(client.app))})
400+
]).then(([kit, app]) => {
401+
kit.start(app, ${args.join(', ')});
402+
});`
403+
: `import(${s(prefixed(client.start))}).then((app) => {
404+
app.start(${args.join(', ')})
405+
});`;
406+
395407
if (load_env_eagerly) {
396408
blocks.push(`import(${s(`${base}/${options.app_dir}/env.js`)}).then(({ env }) => {
397409
${global}.env = env;
398410
399-
Promise.all([
400-
import(${s(prefixed(client.start))}),
401-
import(${s(prefixed(client.app))})
402-
]).then(([kit, app]) => {
403-
kit.start(${args.join(', ')});
404-
});
411+
${boot.replace(/\n/g, '\n\t')}
405412
});`);
406413
} else {
407-
blocks.push(`Promise.all([
408-
import(${s(prefixed(client.start))}),
409-
import(${s(prefixed(client.app))})
410-
]).then(([kit, app]) => {
411-
kit.start(${args.join(', ')});
412-
});`);
414+
blocks.push(boot);
413415
}
414416

415417
if (options.service_worker) {

0 commit comments

Comments
 (0)