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; }