Skip to content

Commit

Permalink
feat: add `bundleStrategy: 'inline' (#13193)
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

* 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 <ricciutipaolo@gmail.com>
Co-authored-by: Simon Holthausen <simon.holthausen@vercel.com>
  • Loading branch information
3 people authored Dec 22, 2024
1 parent a58a82e commit 9a6f461
Show file tree
Hide file tree
Showing 9 changed files with 106 additions and 43 deletions.
5 changes: 5 additions & 0 deletions .changeset/strange-geese-train.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': minor
---

feat: add `bundleStrategy: 'inline'` option
2 changes: 1 addition & 1 deletion packages/kit/src/core/config/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ const options = object(

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

paths: object({
Expand Down
7 changes: 4 additions & 3 deletions packages/kit/src/exports/public.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -499,14 +499,15 @@ export interface KitConfig {
*/
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.
* - 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.
* - 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).
*
* 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';
bundleStrategy?: 'split' | 'single' | 'inline';
};
paths?: {
/**
Expand Down
30 changes: 25 additions & 5 deletions packages/kit/src/exports/vite/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -631,26 +631,30 @@ async function kit({ svelte_config }) {
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';

new_config = {
base: ssr ? assets_base(kit) : client_base,
build: {
copyPublicDir: !ssr,
cssCodeSplit: true,
cssCodeSplit: svelte_config.kit.output.bundleStrategy !== 'inline',
cssMinify: initial_config.build?.minify == null ? true : !!initial_config.build.minify,
// don't use the default name to avoid collisions with 'static/manifest.json'
manifest: '.vite/manifest.json', // TODO: remove this after bumping peer dep to vite 5
outDir: `${out}/${ssr ? 'server' : 'client'}`,
rollupOptions: {
input,
input: inline ? input['bundle'] : input,
output: {
format: 'esm',
format: inline ? 'iife' : 'esm',
name: `__sveltekit_${version_hash}.app`,
entryFileNames: ssr ? '[name].js' : `${prefix}/[name].[hash].${ext}`,
chunkFileNames: ssr ? 'chunks/[name].js' : `${prefix}/chunks/[name].[hash].${ext}`,
assetFileNames: `${prefix}/assets/[name].[hash][extname]`,
hoistTransitiveImports: false,
sourcemapIgnoreList,
manualChunks:
svelte_config.kit.output.bundleStrategy === 'single' ? () => 'bundle' : undefined
manualChunks: split ? undefined : () => 'bundle',
inlineDynamicImports: false
},
preserveEntrySignatures: 'strict'
},
Expand Down Expand Up @@ -868,6 +872,22 @@ async function kit({ svelte_config }) {
(chunk) => chunk.type === 'chunk' && chunk.modules[env_dynamic_public]
)
};

if (svelte_config.kit.output.bundleStrategy === 'inline') {
const style = /** @type {import('rollup').OutputAsset} */ (
output.find(
(chunk) =>
chunk.type === 'asset' &&
chunk.names.length === 1 &&
chunk.names[0] === 'style.css'
)
);

build_data.client.inline = {
script: read(`${out}/client/${start.file}`),
style: /** @type {string | undefined} */ (style?.source)
};
}
}

const css = output.filter(
Expand Down
2 changes: 1 addition & 1 deletion packages/kit/src/runtime/client/bundle.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* if `bundleStrategy === 'single'`, this file is used as the entry point */
/* if `bundleStrategy` is 'single' or 'inline', this file is used as the entry point */

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

Expand Down
27 changes: 20 additions & 7 deletions packages/kit/src/runtime/client/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -303,12 +303,25 @@ export function create_updated_store() {
* - uses hash router and pathname is more than base
* @param {URL} url
* @param {string} base
* @param {boolean} has_pathname_in_hash
* @param {boolean} hash_routing
*/
export function is_external_url(url, base, has_pathname_in_hash) {
return (
url.origin !== origin ||
!url.pathname.startsWith(base) ||
(has_pathname_in_hash && url.pathname !== (base || '/'))
);
export function is_external_url(url, base, hash_routing) {
if (url.origin !== origin || !url.pathname.startsWith(base)) {
return true;
}

if (hash_routing) {
if (url.pathname === base + '/') {
return false;
}

// be lenient if serving from filesystem
if (url.protocol === 'file:' && url.pathname.replace(/\/[^/]+\.html?$/, '') === base) {
return false;
}

return true;
}

return false;
}
61 changes: 38 additions & 23 deletions packages/kit/src/runtime/server/page/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,16 +95,21 @@ export async function render_response({
let base_expression = s(paths.base);

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

base = segments.map(() => '..').join('/') || '.';
base = segments.map(() => '..').join('/') || '.';

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

if (!paths.assets || (paths.assets[0] === '/' && paths.assets !== SVELTE_KIT_ASSETS)) {
assets = base;
if (!paths.assets || (paths.assets[0] === '/' && paths.assets !== SVELTE_KIT_ASSETS)) {
assets = base;
}
} else if (options.hash_routing) {
// we have to assume that we're in the right place
base_expression = "new URL('.', location).pathname.slice(0, -1)";
}
}

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

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

if (client.inline?.style) {
head += `\n\t<style>${client.inline.style}</style>`;
}

if (inline_styles.size > 0) {
const content = Array.from(inline_styles.values()).join('\n');

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

const included_modulepreloads = Array.from(modulepreloads, (dep) => prefixed(dep)).filter(
(path) => resolve_opts.preload({ type: 'js', path })
);

for (const path of included_modulepreloads) {
// see the kit.output.preloadStrategy option for details on why we have multiple options here
link_header_preloads.add(`<${encodeURI(path)}>; rel="modulepreload"; nopush`);
if (options.preload_strategy !== 'modulepreload') {
head += `\n\t\t<link rel="preload" as="script" crossorigin="anonymous" href="${path}">`;
} else if (state.prerendering) {
head += `\n\t\t<link rel="modulepreload" href="${path}">`;
if (!client.inline) {
const included_modulepreloads = Array.from(modulepreloads, (dep) => prefixed(dep)).filter(
(path) => resolve_opts.preload({ type: 'js', path })
);

for (const path of included_modulepreloads) {
// see the kit.output.preloadStrategy option for details on why we have multiple options here
link_header_preloads.add(`<${encodeURI(path)}>; rel="modulepreload"; nopush`);
if (options.preload_strategy !== 'modulepreload') {
head += `\n\t\t<link rel="preload" as="script" crossorigin="anonymous" href="${path}">`;
} else if (state.prerendering) {
head += `\n\t\t<link rel="modulepreload" href="${path}">`;
}
}
}

Expand Down Expand Up @@ -392,15 +403,19 @@ 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([
// `client.app` is a proxy for `bundleStrategy === 'split'`
const boot = client.inline
? `${client.inline.script}
__sveltekit_${options.version_hash}.app.start(${args.join(', ')});`
: 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) => {
: `import(${s(prefixed(client.start))}).then((app) => {
app.start(${args.join(', ')})
});`;

Expand Down
4 changes: 4 additions & 0 deletions packages/kit/src/types/internal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ export interface BuildData {
stylesheets: string[];
fonts: string[];
uses_env_dynamic_public: boolean;
inline?: {
script: string;
style: string | undefined;
};
} | null;
server_manifest: import('vite').Manifest;
}
Expand Down
11 changes: 8 additions & 3 deletions packages/kit/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -481,14 +481,15 @@ declare module '@sveltejs/kit' {
*/
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.
* - 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.
* - 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).
*
* 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';
bundleStrategy?: 'split' | 'single' | 'inline';
};
paths?: {
/**
Expand Down Expand Up @@ -1667,6 +1668,10 @@ declare module '@sveltejs/kit' {
stylesheets: string[];
fonts: string[];
uses_env_dynamic_public: boolean;
inline?: {
script: string;
style: string | undefined;
};
} | null;
server_manifest: import('vite').Manifest;
}
Expand Down

0 comments on commit 9a6f461

Please sign in to comment.