Skip to content

Commit 9a6f461

Browse files
Rich-Harrispaoloricciutidummdidumm
authored
feat: add `bundleStrategy: 'inline' (#13193)
* 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 * inline strategy * omit modulepreloads when inlining script * regenerate * fix * changeset * regenerate * fix * jsdoc * allow trailing /index.html when serving from filesystem * treat current directory as base when using hash-based routing * make self-contained apps possible * lint * oops --------- Co-authored-by: paoloricciuti <[email protected]> Co-authored-by: Simon Holthausen <[email protected]>
1 parent a58a82e commit 9a6f461

File tree

9 files changed

+106
-43
lines changed

9 files changed

+106
-43
lines changed

.changeset/strange-geese-train.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: 'inline'` option

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ const options = object(
143143

144144
output: object({
145145
preloadStrategy: list(['modulepreload', 'preload-js', 'preload-mjs']),
146-
bundleStrategy: list(['split', 'single'])
146+
bundleStrategy: list(['split', 'single', 'inline'])
147147
}),
148148

149149
paths: object({

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -499,14 +499,15 @@ export interface KitConfig {
499499
*/
500500
preloadStrategy?: 'modulepreload' | 'preload-js' | 'preload-mjs';
501501
/**
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.
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+
* - If `'inline'`, inlines all JavaScript and CSS of the entire app into the HTML. The result is usable without a server (i.e. you can just open the file in your browser).
504505
*
505506
* 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).
506507
* @default 'split'
507508
* @since 2.13.0
508509
*/
509-
bundleStrategy?: 'split' | 'single';
510+
bundleStrategy?: 'split' | 'single' | 'inline';
510511
};
511512
paths?: {
512513
/**

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

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -631,26 +631,30 @@ async function kit({ svelte_config }) {
631631
const client_base =
632632
kit.paths.relative !== false || kit.paths.assets ? './' : kit.paths.base || '/';
633633

634+
const inline = !ssr && svelte_config.kit.output.bundleStrategy === 'inline';
635+
const split = ssr || svelte_config.kit.output.bundleStrategy === 'split';
636+
634637
new_config = {
635638
base: ssr ? assets_base(kit) : client_base,
636639
build: {
637640
copyPublicDir: !ssr,
638-
cssCodeSplit: true,
641+
cssCodeSplit: svelte_config.kit.output.bundleStrategy !== 'inline',
639642
cssMinify: initial_config.build?.minify == null ? true : !!initial_config.build.minify,
640643
// don't use the default name to avoid collisions with 'static/manifest.json'
641644
manifest: '.vite/manifest.json', // TODO: remove this after bumping peer dep to vite 5
642645
outDir: `${out}/${ssr ? 'server' : 'client'}`,
643646
rollupOptions: {
644-
input,
647+
input: inline ? input['bundle'] : input,
645648
output: {
646-
format: 'esm',
649+
format: inline ? 'iife' : 'esm',
650+
name: `__sveltekit_${version_hash}.app`,
647651
entryFileNames: ssr ? '[name].js' : `${prefix}/[name].[hash].${ext}`,
648652
chunkFileNames: ssr ? 'chunks/[name].js' : `${prefix}/chunks/[name].[hash].${ext}`,
649653
assetFileNames: `${prefix}/assets/[name].[hash][extname]`,
650654
hoistTransitiveImports: false,
651655
sourcemapIgnoreList,
652-
manualChunks:
653-
svelte_config.kit.output.bundleStrategy === 'single' ? () => 'bundle' : undefined
656+
manualChunks: split ? undefined : () => 'bundle',
657+
inlineDynamicImports: false
654658
},
655659
preserveEntrySignatures: 'strict'
656660
},
@@ -868,6 +872,22 @@ async function kit({ svelte_config }) {
868872
(chunk) => chunk.type === 'chunk' && chunk.modules[env_dynamic_public]
869873
)
870874
};
875+
876+
if (svelte_config.kit.output.bundleStrategy === 'inline') {
877+
const style = /** @type {import('rollup').OutputAsset} */ (
878+
output.find(
879+
(chunk) =>
880+
chunk.type === 'asset' &&
881+
chunk.names.length === 1 &&
882+
chunk.names[0] === 'style.css'
883+
)
884+
);
885+
886+
build_data.client.inline = {
887+
script: read(`${out}/client/${start.file}`),
888+
style: /** @type {string | undefined} */ (style?.source)
889+
};
890+
}
871891
}
872892

873893
const css = output.filter(

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/* if `bundleStrategy === 'single'`, this file is used as the entry point */
1+
/* if `bundleStrategy` is 'single' or 'inline', this file is used as the entry point */
22

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

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

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -303,12 +303,25 @@ export function create_updated_store() {
303303
* - uses hash router and pathname is more than base
304304
* @param {URL} url
305305
* @param {string} base
306-
* @param {boolean} has_pathname_in_hash
306+
* @param {boolean} hash_routing
307307
*/
308-
export function is_external_url(url, base, has_pathname_in_hash) {
309-
return (
310-
url.origin !== origin ||
311-
!url.pathname.startsWith(base) ||
312-
(has_pathname_in_hash && url.pathname !== (base || '/'))
313-
);
308+
export function is_external_url(url, base, hash_routing) {
309+
if (url.origin !== origin || !url.pathname.startsWith(base)) {
310+
return true;
311+
}
312+
313+
if (hash_routing) {
314+
if (url.pathname === base + '/') {
315+
return false;
316+
}
317+
318+
// be lenient if serving from filesystem
319+
if (url.protocol === 'file:' && url.pathname.replace(/\/[^/]+\.html?$/, '') === base) {
320+
return false;
321+
}
322+
323+
return true;
324+
}
325+
326+
return false;
314327
}

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

Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -95,16 +95,21 @@ export async function render_response({
9595
let base_expression = s(paths.base);
9696

9797
// if appropriate, use relative paths for greater portability
98-
if (paths.relative && !state.prerendering?.fallback) {
99-
const segments = event.url.pathname.slice(paths.base.length).split('/').slice(2);
98+
if (paths.relative) {
99+
if (!state.prerendering?.fallback) {
100+
const segments = event.url.pathname.slice(paths.base.length).split('/').slice(2);
100101

101-
base = segments.map(() => '..').join('/') || '.';
102+
base = segments.map(() => '..').join('/') || '.';
102103

103-
// resolve e.g. '../..' against current location, then remove trailing slash
104-
base_expression = `new URL(${s(base)}, location).pathname.slice(0, -1)`;
104+
// resolve e.g. '../..' against current location, then remove trailing slash
105+
base_expression = `new URL(${s(base)}, location).pathname.slice(0, -1)`;
105106

106-
if (!paths.assets || (paths.assets[0] === '/' && paths.assets !== SVELTE_KIT_ASSETS)) {
107-
assets = base;
107+
if (!paths.assets || (paths.assets[0] === '/' && paths.assets !== SVELTE_KIT_ASSETS)) {
108+
assets = base;
109+
}
110+
} else if (options.hash_routing) {
111+
// we have to assume that we're in the right place
112+
base_expression = "new URL('.', location).pathname.slice(0, -1)";
108113
}
109114
}
110115

@@ -197,7 +202,7 @@ export async function render_response({
197202
for (const url of node.stylesheets) stylesheets.add(url);
198203
for (const url of node.fonts) fonts.add(url);
199204

200-
if (node.inline_styles) {
205+
if (node.inline_styles && !client.inline) {
201206
Object.entries(await node.inline_styles()).forEach(([k, v]) => inline_styles.set(k, v));
202207
}
203208
}
@@ -223,6 +228,10 @@ export async function render_response({
223228
return `${assets}/${path}`;
224229
};
225230

231+
if (client.inline?.style) {
232+
head += `\n\t<style>${client.inline.style}</style>`;
233+
}
234+
226235
if (inline_styles.size > 0) {
227236
const content = Array.from(inline_styles.values()).join('\n');
228237

@@ -293,17 +302,19 @@ export async function render_response({
293302
modulepreloads.add(`${options.app_dir}/env.js`);
294303
}
295304

296-
const included_modulepreloads = Array.from(modulepreloads, (dep) => prefixed(dep)).filter(
297-
(path) => resolve_opts.preload({ type: 'js', path })
298-
);
299-
300-
for (const path of included_modulepreloads) {
301-
// see the kit.output.preloadStrategy option for details on why we have multiple options here
302-
link_header_preloads.add(`<${encodeURI(path)}>; rel="modulepreload"; nopush`);
303-
if (options.preload_strategy !== 'modulepreload') {
304-
head += `\n\t\t<link rel="preload" as="script" crossorigin="anonymous" href="${path}">`;
305-
} else if (state.prerendering) {
306-
head += `\n\t\t<link rel="modulepreload" href="${path}">`;
305+
if (!client.inline) {
306+
const included_modulepreloads = Array.from(modulepreloads, (dep) => prefixed(dep)).filter(
307+
(path) => resolve_opts.preload({ type: 'js', path })
308+
);
309+
310+
for (const path of included_modulepreloads) {
311+
// see the kit.output.preloadStrategy option for details on why we have multiple options here
312+
link_header_preloads.add(`<${encodeURI(path)}>; rel="modulepreload"; nopush`);
313+
if (options.preload_strategy !== 'modulepreload') {
314+
head += `\n\t\t<link rel="preload" as="script" crossorigin="anonymous" href="${path}">`;
315+
} else if (state.prerendering) {
316+
head += `\n\t\t<link rel="modulepreload" href="${path}">`;
317+
}
307318
}
308319
}
309320

@@ -392,15 +403,19 @@ export async function render_response({
392403
args.push(`{\n${indent}\t${hydrate.join(`,\n${indent}\t`)}\n${indent}}`);
393404
}
394405

395-
// `client.app` is a proxy for `bundleStrategy !== 'single'`
396-
const boot = client.app
397-
? `Promise.all([
406+
// `client.app` is a proxy for `bundleStrategy === 'split'`
407+
const boot = client.inline
408+
? `${client.inline.script}
409+
410+
__sveltekit_${options.version_hash}.app.start(${args.join(', ')});`
411+
: client.app
412+
? `Promise.all([
398413
import(${s(prefixed(client.start))}),
399414
import(${s(prefixed(client.app))})
400415
]).then(([kit, app]) => {
401416
kit.start(app, ${args.join(', ')});
402417
});`
403-
: `import(${s(prefixed(client.start))}).then((app) => {
418+
: `import(${s(prefixed(client.start))}).then((app) => {
404419
app.start(${args.join(', ')})
405420
});`;
406421

packages/kit/src/types/internal.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ export interface BuildData {
7474
stylesheets: string[];
7575
fonts: string[];
7676
uses_env_dynamic_public: boolean;
77+
inline?: {
78+
script: string;
79+
style: string | undefined;
80+
};
7781
} | null;
7882
server_manifest: import('vite').Manifest;
7983
}

packages/kit/types/index.d.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -481,14 +481,15 @@ declare module '@sveltejs/kit' {
481481
*/
482482
preloadStrategy?: 'modulepreload' | 'preload-js' | 'preload-mjs';
483483
/**
484-
* 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.
485-
* If `'single'`, creates just one .js bundle and one .css file containing code for the entire app.
484+
* - 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.
485+
* - If `'single'`, creates just one .js bundle and one .css file containing code for the entire app.
486+
* - If `'inline'`, inlines all JavaScript and CSS of the entire app into the HTML. The result is usable without a server (i.e. you can just open the file in your browser).
486487
*
487488
* 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).
488489
* @default 'split'
489490
* @since 2.13.0
490491
*/
491-
bundleStrategy?: 'split' | 'single';
492+
bundleStrategy?: 'split' | 'single' | 'inline';
492493
};
493494
paths?: {
494495
/**
@@ -1667,6 +1668,10 @@ declare module '@sveltejs/kit' {
16671668
stylesheets: string[];
16681669
fonts: string[];
16691670
uses_env_dynamic_public: boolean;
1671+
inline?: {
1672+
script: string;
1673+
style: string | undefined;
1674+
};
16701675
} | null;
16711676
server_manifest: import('vite').Manifest;
16721677
}

0 commit comments

Comments
 (0)