Skip to content

Commit

Permalink
feat: add bundleStrategy option (#13173)
Browse files Browse the repository at this point in the history
* 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 <rich.harris@vercel.com>
Co-authored-by: Rich Harris <hello@rich-harris.dev>
  • Loading branch information
3 people authored Dec 19, 2024
1 parent 8d6c469 commit 20f2720
Show file tree
Hide file tree
Showing 17 changed files with 177 additions and 47 deletions.
5 changes: 5 additions & 0 deletions .changeset/beige-carpets-wave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': minor
---

feat: add `bundleStrategy: 'split' | 'single'` option
2 changes: 1 addition & 1 deletion packages/kit/src/core/config/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ const get_defaults = (prefix = '') => ({
},
inlineStyleThreshold: 0,
moduleExtensions: ['.js', '.ts'],
output: { preloadStrategy: 'modulepreload' },
output: { preloadStrategy: 'modulepreload', bundleStrategy: 'split' },
outDir: join(prefix, '.svelte-kit'),
serviceWorker: {
register: true
Expand Down
3 changes: 2 additions & 1 deletion packages/kit/src/core/config/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,8 @@ const options = object(
outDir: string('.svelte-kit'),

output: object({
preloadStrategy: list(['modulepreload', 'preload-js', 'preload-mjs'], 'modulepreload')
preloadStrategy: list(['modulepreload', 'preload-js', 'preload-mjs']),
bundleStrategy: list(['split', 'single'])
}),

paths: object({
Expand Down
9 changes: 9 additions & 0 deletions packages/kit/src/exports/public.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,15 @@ export interface KitConfig {
* @since 1.8.4
*/
preloadStrategy?: 'modulepreload' | 'preload-js' | 'preload-mjs';
/**
* 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.
* If `'single'`, creates just one .js bundle and one .css file containing code for the entire app.
*
* 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).
* @default 'split'
* @since 2.13.0
*/
bundleStrategy?: 'split' | 'single';
};
paths?: {
/**
Expand Down
5 changes: 3 additions & 2 deletions packages/kit/src/exports/vite/build/build_server.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ import { normalizePath } from 'vite';
* @param {import('vite').Manifest} server_manifest
* @param {import('vite').Manifest | null} client_manifest
* @param {import('vite').Rollup.OutputAsset[] | null} css
* @param {import('types').RecursiveRequired<import('types').ValidatedConfig['kit']['output']>} output_config
*/
export function build_server_nodes(out, kit, manifest_data, server_manifest, client_manifest, css) {
export function build_server_nodes(out, kit, manifest_data, server_manifest, client_manifest, css, output_config) {
mkdirp(`${out}/server/nodes`);
mkdirp(`${out}/server/stylesheets`);

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

if (client_manifest && (node.universal || node.component)) {
if (client_manifest && (node.universal || node.component) && output_config.bundleStrategy === 'split') {
const entry = find_deps(
client_manifest,
`${normalizePath(kit.outDir)}/generated/client-optimized/nodes/${i}.js`,
Expand Down
72 changes: 55 additions & 17 deletions packages/kit/src/exports/vite/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,10 @@ async function kit({ svelte_config }) {
name: 'vite-plugin-sveltekit-virtual-modules',

resolveId(id, importer) {
if (id === '__sveltekit/manifest') {
return `${kit.outDir}/generated/client-optimized/app.js`;
}

// If importing from a service-worker, only allow $service-worker & $env/static/public, but none of the other virtual modules.
// This check won't catch transitive imports, but it will warn when the import comes from a service-worker directly.
// Transitive imports will be caught during the build.
Expand Down Expand Up @@ -605,10 +609,11 @@ async function kit({ svelte_config }) {
const name = posixify(path.join('entries/matchers', key));
input[name] = path.resolve(file);
});
} else if (svelte_config.kit.output.bundleStrategy !== 'split') {
input['bundle'] = `${runtime_directory}/client/bundle.js`;
} else {
input['entry/start'] = `${runtime_directory}/client/entry.js`;
input['entry/app'] = `${kit.outDir}/generated/client-optimized/app.js`;

manifest_data.nodes.forEach((node, i) => {
if (node.component || node.universal) {
input[`nodes/${i}`] = `${kit.outDir}/generated/client-optimized/nodes/${i}.js`;
Expand Down Expand Up @@ -643,7 +648,9 @@ async function kit({ svelte_config }) {
chunkFileNames: ssr ? 'chunks/[name].js' : `${prefix}/chunks/[name].[hash].${ext}`,
assetFileNames: `${prefix}/assets/[name].[hash][extname]`,
hoistTransitiveImports: false,
sourcemapIgnoreList
sourcemapIgnoreList,
manualChunks:
svelte_config.kit.output.bundleStrategy === 'single' ? () => 'bundle' : undefined
},
preserveEntrySignatures: 'strict'
},
Expand Down Expand Up @@ -775,7 +782,15 @@ async function kit({ svelte_config }) {
// first, build server nodes without the client manifest so we can analyse it
log.info('Analysing routes');

build_server_nodes(out, kit, manifest_data, server_manifest, null, null);
build_server_nodes(
out,
kit,
manifest_data,
server_manifest,
null,
null,
svelte_config.output
);

const metadata = await analyse({
manifest_path,
Expand Down Expand Up @@ -825,19 +840,34 @@ async function kit({ svelte_config }) {

const deps_of = /** @param {string} f */ (f) =>
find_deps(client_manifest, posixify(path.relative('.', f)), false);
const start = deps_of(`${runtime_directory}/client/entry.js`);
const app = deps_of(`${kit.outDir}/generated/client-optimized/app.js`);

build_data.client = {
start: start.file,
app: app.file,
imports: [...start.imports, ...app.imports],
stylesheets: [...start.stylesheets, ...app.stylesheets],
fonts: [...start.fonts, ...app.fonts],
uses_env_dynamic_public: output.some(
(chunk) => chunk.type === 'chunk' && chunk.modules[env_dynamic_public]
)
};

if (svelte_config.kit.output.bundleStrategy === 'split') {
const start = deps_of(`${runtime_directory}/client/entry.js`);
const app = deps_of(`${kit.outDir}/generated/client-optimized/app.js`);

build_data.client = {
start: start.file,
app: app.file,
imports: [...start.imports, ...app.imports],
stylesheets: [...start.stylesheets, ...app.stylesheets],
fonts: [...start.fonts, ...app.fonts],
uses_env_dynamic_public: output.some(
(chunk) => chunk.type === 'chunk' && chunk.modules[env_dynamic_public]
)
};
} else {
const start = deps_of(`${runtime_directory}/client/bundle.js`);

build_data.client = {
start: start.file,
imports: start.imports,
stylesheets: start.stylesheets,
fonts: start.fonts,
uses_env_dynamic_public: output.some(
(chunk) => chunk.type === 'chunk' && chunk.modules[env_dynamic_public]
)
};
}

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

// regenerate nodes with the client manifest...
build_server_nodes(out, kit, manifest_data, server_manifest, client_manifest, css);
build_server_nodes(
out,
kit,
manifest_data,
server_manifest,
client_manifest,
css,
svelte_config.kit.output
);

// ...and prerender
const { prerendered, prerender_map } = await prerender({
Expand Down
15 changes: 15 additions & 0 deletions packages/kit/src/runtime/client/bundle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/* if `bundleStrategy === 'single'`, this file is used as the entry point */

import * as kit from './entry.js';

// @ts-expect-error
import * as app from '__sveltekit/manifest';

/**
*
* @param {HTMLElement} element
* @param {import('./types.js').HydrateOptions} options
*/
export function start(element, options) {
kit.start(app, element, options);
}
10 changes: 1 addition & 9 deletions packages/kit/src/runtime/client/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -2354,15 +2354,7 @@ function _start_router() {

/**
* @param {HTMLElement} target
* @param {{
* status: number;
* error: App.Error | null;
* node_ids: number[];
* params: Record<string, string>;
* route: { id: string | null };
* data: Array<import('types').ServerDataNode | null>;
* form: Record<string, any> | null;
* }} opts
* @param {import('./types.js').HydrateOptions} opts
*/
async function _hydrate(
target,
Expand Down
20 changes: 19 additions & 1 deletion packages/kit/src/runtime/client/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { SvelteComponent } from 'svelte';
import { ClientHooks, CSRPageNode, CSRPageNodeLoader, CSRRoute, TrailingSlash, Uses } from 'types';
import {
ClientHooks,
CSRPageNode,
CSRPageNodeLoader,
CSRRoute,
ServerDataNode,
TrailingSlash,
Uses
} from 'types';
import { Page, ParamMatcher } from '@sveltejs/kit';

export interface SvelteKitApp {
Expand Down Expand Up @@ -88,3 +96,13 @@ export interface NavigationState {
route: CSRRoute | null;
url: URL;
}

export interface HydrateOptions {
status: number;
error: App.Error | null;
node_ids: number[];
params: Record<string, string>;
route: { id: string | null };
data: Array<ServerDataNode | null>;
form: Record<string, any> | null;
}
28 changes: 15 additions & 13 deletions packages/kit/src/runtime/server/page/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ export async function render_response({
${properties.join(',\n\t\t\t\t\t\t')}
};`);

const args = ['app', 'element'];
const args = ['element'];

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

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

// `client.app` is a proxy for `bundleStrategy !== 'single'`
const boot = client.app
? `Promise.all([
import(${s(prefixed(client.start))}),
import(${s(prefixed(client.app))})
]).then(([kit, app]) => {
kit.start(app, ${args.join(', ')});
});`
: `import(${s(prefixed(client.start))}).then((app) => {
app.start(${args.join(', ')})
});`;

if (load_env_eagerly) {
blocks.push(`import(${s(`${base}/${options.app_dir}/env.js`)}).then(({ env }) => {
${global}.env = env;
Promise.all([
import(${s(prefixed(client.start))}),
import(${s(prefixed(client.app))})
]).then(([kit, app]) => {
kit.start(${args.join(', ')});
});
${boot.replace(/\n/g, '\n\t')}
});`);
} else {
blocks.push(`Promise.all([
import(${s(prefixed(client.start))}),
import(${s(prefixed(client.app))})
]).then(([kit, app]) => {
kit.start(${args.join(', ')});
});`);
blocks.push(boot);
}

if (options.service_worker) {
Expand Down
2 changes: 1 addition & 1 deletion packages/kit/src/types/internal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export interface BuildData {
service_worker: string | null;
client: {
start: string;
app: string;
app?: string;
imports: string[];
stylesheets: string[];
fonts: string[];
Expand Down
6 changes: 6 additions & 0 deletions packages/kit/test/apps/options-2/src/routes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,9 @@
<p data-testid="assets">assets: {assets}</p>

<a href="{base}/hello" data-testid="link">Go to /hello</a>

<style>
a {
text-decoration: none;
}
</style>
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,9 @@

<p data-testid="base">base: {base}</p>
<p data-testid="assets">assets: {assets}</p>

<style>
p {
color: red;
}
</style>
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
<h1>Prerendered</h1>

<style>
h1 {
background-color: green;
}
</style>
3 changes: 3 additions & 0 deletions packages/kit/test/apps/options-2/svelte.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ const config = {
},
env: {
dir: '../../env'
},
output: {
bundleStrategy: 'single'
}
}
};
Expand Down
21 changes: 20 additions & 1 deletion packages/kit/test/apps/options-2/test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ test.describe('Service worker', () => {
});

expect(self.base).toBe('/basepath');
expect(self.build[0]).toMatch(/\/basepath\/_app\/immutable\/entry\/start\.[\w-]+\.js/);
expect(self.build[0]).toMatch(/\/basepath\/_app\/immutable\/bundle\.[\w-]+\.js/);
expect(self.image_src).toMatch(/\/basepath\/_app\/immutable\/assets\/image\.[\w-]+\.jpg/);
});

Expand All @@ -87,3 +87,22 @@ test.describe('Service worker', () => {
expect(await page.content()).not.toMatch(/navigator\.serviceWorker/);
});
});

test.describe("bundleStrategy: 'single'", () => {
test.skip(({ javaScriptEnabled }) => !javaScriptEnabled || !!process.env.DEV);
test('loads a single js file and a single css file', async ({ page }) => {
/** @type {string[]} */
const requests = [];
page.on('request', (r) => requests.push(new URL(r.url()).pathname));

await page.goto('/basepath');

await Promise.all([
page.waitForTimeout(100), // wait for preloading to start
page.waitForLoadState('networkidle') // wait for preloading to finish
]);

expect(requests.filter((req) => req.endsWith('.js')).length).toBe(1);
expect(requests.filter((req) => req.endsWith('.css')).length).toBe(1);
});
});
11 changes: 10 additions & 1 deletion packages/kit/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,15 @@ declare module '@sveltejs/kit' {
* @since 1.8.4
*/
preloadStrategy?: 'modulepreload' | 'preload-js' | 'preload-mjs';
/**
* 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.
* If `'single'`, creates just one .js bundle and one .css file containing code for the entire app.
*
* 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).
* @default 'split'
* @since 2.13.0
*/
bundleStrategy?: 'split' | 'single';
};
paths?: {
/**
Expand Down Expand Up @@ -1641,7 +1650,7 @@ declare module '@sveltejs/kit' {
service_worker: string | null;
client: {
start: string;
app: string;
app?: string;
imports: string[];
stylesheets: string[];
fonts: string[];
Expand Down

0 comments on commit 20f2720

Please sign in to comment.