diff --git a/.changeset/strange-geese-train.md b/.changeset/strange-geese-train.md
new file mode 100644
index 000000000000..0db288421ee2
--- /dev/null
+++ b/.changeset/strange-geese-train.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': minor
+---
+
+feat: add `bundleStrategy: 'inline'` option
diff --git a/packages/kit/src/core/config/options.js b/packages/kit/src/core/config/options.js
index 5091aff533f0..472779ac0ded 100644
--- a/packages/kit/src/core/config/options.js
+++ b/packages/kit/src/core/config/options.js
@@ -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({
diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts
index 6bce6ce2aa50..b4a837768420 100644
--- a/packages/kit/src/exports/public.d.ts
+++ b/packages/kit/src/exports/public.d.ts
@@ -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?: {
/**
diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js
index d28f3d26cfb7..21bc3d49575f 100644
--- a/packages/kit/src/exports/vite/index.js
+++ b/packages/kit/src/exports/vite/index.js
@@ -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'
},
@@ -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(
diff --git a/packages/kit/src/runtime/client/bundle.js b/packages/kit/src/runtime/client/bundle.js
index 9ef13c0f2bd2..ae77db0ca4ec 100644
--- a/packages/kit/src/runtime/client/bundle.js
+++ b/packages/kit/src/runtime/client/bundle.js
@@ -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';
diff --git a/packages/kit/src/runtime/client/utils.js b/packages/kit/src/runtime/client/utils.js
index 83ff844ad95a..2a0c35f344e9 100644
--- a/packages/kit/src/runtime/client/utils.js
+++ b/packages/kit/src/runtime/client/utils.js
@@ -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;
}
diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js
index 004a5ca59a2c..550d266fc964 100644
--- a/packages/kit/src/runtime/server/page/render.js
+++ b/packages/kit/src/runtime/server/page/render.js
@@ -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)";
}
}
@@ -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));
}
}
@@ -223,6 +228,10 @@ export async function render_response({
return `${assets}/${path}`;
};
+ if (client.inline?.style) {
+ head += `\n\t`;
+ }
+
if (inline_styles.size > 0) {
const content = Array.from(inline_styles.values()).join('\n');
@@ -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`;
- } else if (state.prerendering) {
- head += `\n\t\t`;
+ 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`;
+ } else if (state.prerendering) {
+ head += `\n\t\t`;
+ }
}
}
@@ -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(', ')})
});`;
diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts
index 7aa6e2863443..ba4355a8db3f 100644
--- a/packages/kit/src/types/internal.d.ts
+++ b/packages/kit/src/types/internal.d.ts
@@ -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;
}
diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts
index bd268a10ff31..248dd814b10c 100644
--- a/packages/kit/types/index.d.ts
+++ b/packages/kit/types/index.d.ts
@@ -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?: {
/**
@@ -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;
}