Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add bundleStrategy option #13173

Merged
merged 20 commits into from
Dec 19, 2024
Merged
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/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 {
paoloricciuti marked this conversation as resolved.
Show resolved Hide resolved
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
Loading