Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
ac63953
add fix and test
teemingc Jan 11, 2026
2f60ab4
add unit test
teemingc Jan 11, 2026
251e4c8
changeset
teemingc Jan 11, 2026
885a5c8
add test for inlining conditionally rendered component css
teemingc Jan 11, 2026
3ff57e2
format
teemingc Jan 11, 2026
6bc6d4b
Apply suggestion from @teemingc
teemingc Jan 12, 2026
6dc758a
Apply suggestion from @teemingc
teemingc Jan 12, 2026
a4163c2
Update css.js
teemingc Jan 12, 2026
b331999
handle whitespace, add some additional test cases
elliott-with-the-longest-name-on-github Jan 12, 2026
12fa7df
Merge branch 'main' into fix-missing-css-with-inlining-enabled
teemingc Jan 12, 2026
df13a14
add failing test for assets in static dir
teemingc Jan 14, 2026
43fbe0f
Merge branch 'main' into fix-missing-css-with-inlining-enabled
teemingc Jan 14, 2026
ff2f6d0
Merge branch 'main' into fix-missing-css-with-inlining-enabled
teemingc Jan 22, 2026
b457b97
bump svelte
teemingc Jan 22, 2026
88aa343
this should just work
teemingc Jan 22, 2026
f9a3aa7
fix lockfile
teemingc Jan 22, 2026
9217194
ok its working now
teemingc Jan 22, 2026
390b892
last fix
teemingc Jan 22, 2026
40d1e4f
format
teemingc Jan 22, 2026
db54bdd
Merge branch 'main' into fix-missing-css-with-inlining-enabled
teemingc Jan 22, 2026
fe704f8
push wip
teemingc Jan 22, 2026
c3f54ac
Merge `origin/main` into `fix-missing-css-with-inlining-enabled`
teemingc Jan 23, 2026
16c66b9
tests are passing
teemingc Jan 23, 2026
e77a985
Merge branch 'main' into fix-missing-css-with-inlining-enabled
teemingc Jan 26, 2026
4f2f3a8
Merge branch 'main' into fix-missing-css-with-inlining-enabled
teemingc Jan 26, 2026
4e665a2
split tests
teemingc Jan 27, 2026
8cdc8c6
rename parser to parse
teemingc Jan 27, 2026
41c3280
hoist regexes
teemingc Jan 27, 2026
d26f914
use test.each
teemingc Jan 27, 2026
fe352d5
add test for content and comments
teemingc Jan 27, 2026
6807f52
rename assets to paths_assets
teemingc Jan 27, 2026
f34dbad
add tests for escaped characters
teemingc Jan 27, 2026
27e522e
add test for encoded characters
teemingc Jan 27, 2026
abc4c64
safeguard against trailing slashes
teemingc Jan 27, 2026
4902991
decode vite asset filenames
teemingc Jan 27, 2026
5a6e24e
a bit of clean up
teemingc Jan 27, 2026
0a019aa
oops
teemingc Jan 27, 2026
b9e9fa3
tippex comments
teemingc Jan 27, 2026
7f260ba
tippex strings
teemingc Jan 27, 2026
cacb600
tippex wip
teemingc Jan 27, 2026
3f4bea3
harden comment and escaped character tests
teemingc Jan 27, 2026
6894599
account for nested app dir
teemingc Jan 27, 2026
5f3fbf8
bump svelte
Rich-Harris Jan 27, 2026
3b89c6b
chore: fix tippex and add test cases
elliott-with-the-longest-name-on-github Jan 27, 2026
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/brave-guests-hope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

fix: ensure inlined CSS follows `paths.assets` and `paths.relative` settings
5 changes: 5 additions & 0 deletions .changeset/shy-tables-shout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

fix: ensure CSS inlining includes components that are conditionally rendered
2 changes: 1 addition & 1 deletion packages/enhanced-img/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
},
"types": "types/index.d.ts",
"dependencies": {
"magic-string": "^0.30.5",
"magic-string": "catalog:",
"sharp": "^0.34.1",
"svelte-parse-markup": "^0.1.5",
"vite-imagetools": "^9.0.2",
Expand Down
2 changes: 1 addition & 1 deletion packages/kit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"devalue": "^5.6.2",
"esm-env": "^1.2.2",
"kleur": "^4.1.5",
"magic-string": "^0.30.5",
"magic-string": "catalog:",
"mrmime": "^2.0.0",
"sade": "^1.8.1",
"set-cookie-parser": "^2.6.0",
Expand Down
196 changes: 116 additions & 80 deletions packages/kit/src/exports/vite/build/build_server.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,43 @@ import { s } from '../../../utils/misc.js';
import { normalizePath } from 'vite';
import { basename, join } from 'node:path';
import { create_node_analyser } from '../static_analysis/index.js';
import { fix_css_urls } from '../../../utils/css.js';

/**
* Regenerate server nodes after acquiring client manifest
* @overload
* @param {string} out
* @param {import('types').ValidatedKitConfig} kit
* @param {import('types').ManifestData} manifest_data
* @param {import('vite').Manifest} server_manifest
* @param {import('vite').Manifest} client_manifest
* @param {string} assets_path
* @param {import('vite').Rollup.RollupOutput['output']} client_chunks
* @param {import('types').RecursiveRequired<import('types').ValidatedConfig['kit']['output']>} output_config
* @param {Map<string, { page_options: Record<string, any> | null, children: string[] }>} static_exports
* @returns {Promise<void>}
*/
/**
* Build server nodes without client manifest for analysis phase
* @overload
* @param {string} out
* @param {import('types').ValidatedKitConfig} kit
* @param {import('types').ManifestData} manifest_data
* @param {import('vite').Manifest} server_manifest
* @param {null} client_manifest
* @param {null} assets_path
* @param {null} client_chunks
* @param {import('types').RecursiveRequired<import('types').ValidatedConfig['kit']['output']>} output_config
* @param {Map<string, { page_options: Record<string, any> | null, children: string[] }>} static_exports
* @returns {Promise<void>}
*/
/**
* @param {string} out
* @param {import('types').ValidatedKitConfig} kit
* @param {import('types').ManifestData} manifest_data
* @param {import('vite').Manifest} server_manifest
* @param {import('vite').Manifest | null} client_manifest
* @param {import('vite').Rollup.OutputBundle | null} server_bundle
* @param {string | null} assets_path
* @param {import('vite').Rollup.RollupOutput['output'] | null} client_chunks
* @param {import('types').RecursiveRequired<import('types').ValidatedConfig['kit']['output']>} output_config
* @param {Map<string, { page_options: Record<string, any> | null, children: string[] }>} static_exports
Expand All @@ -23,46 +52,67 @@ export async function build_server_nodes(
manifest_data,
server_manifest,
client_manifest,
server_bundle,
assets_path,
client_chunks,
output_config,
static_exports
) {
mkdirp(`${out}/server/nodes`);
mkdirp(`${out}/server/stylesheets`);

/** @type {Map<string, string>} */
/**
* Stylesheet names and their contents which are below the inline threshold
* @type {Map<string, string>}
*/
const stylesheets_to_inline = new Map();

if (server_bundle && client_chunks && kit.inlineStyleThreshold > 0) {
const client = get_stylesheets(client_chunks);
const server = get_stylesheets(Object.values(server_bundle));
/**
* For CSS inlining, we either store a string or a function that returns the
* styles with the correct relative URLs
* @type {(css: string, eager_assets: Set<string>) => string}
*/
let prepare_css_for_inlining = (css) => s(css);

// map server stylesheet name to the client stylesheet name
for (const [id, client_stylesheet] of client.stylesheets_used) {
const server_stylesheet = server.stylesheets_used.get(id);
if (!server_stylesheet) {
if (client_chunks && kit.inlineStyleThreshold > 0 && output_config.bundleStrategy === 'split') {
for (const chunk of client_chunks) {
if (chunk.type !== 'asset' || !chunk.fileName.endsWith('.css')) {
continue;
}
client_stylesheet.forEach((file, i) => {
stylesheets_to_inline.set(file, server_stylesheet[i]);
});
}

// filter out stylesheets that should not be inlined
for (const [fileName, content] of client.stylesheet_content) {
if (content.length >= kit.inlineStyleThreshold) {
stylesheets_to_inline.delete(fileName);
const source = chunk.source.toString();
if (source.length < kit.inlineStyleThreshold) {
stylesheets_to_inline.set(chunk.fileName, source);
}
}

// map server stylesheet source to the client stylesheet name
for (const [client_file, server_file] of stylesheets_to_inline) {
const source = server.stylesheet_content.get(server_file);
if (!source) {
throw new Error(`Server stylesheet source not found for client stylesheet ${client_file}`);
}
stylesheets_to_inline.set(client_file, source);
// If the client CSS has URL references to assets, we need to adjust the
// relative path so that they are correct when inlined into the document.
// Although `paths.assets` is static, we need to pass in a fake path
// `/_svelte_kit_assets` at runtime when running `vite preview`
if (kit.paths.assets || kit.paths.relative) {
const static_assets = new Set(
manifest_data.assets.map((asset) => decodeURIComponent(asset.file))
);

const segments = /** @type {string} */ (assets_path).split('/');
const static_asset_prefix = segments.map(() => '..').join('/') + '/';

prepare_css_for_inlining = (css, eager_assets) => {
const transformed_css = fix_css_urls({
css,
vite_assets: eager_assets,
static_assets,
paths_assets: '${assets}',
base: '${base}',
static_asset_prefix
});

// only convert to a function if we have adjusted any URLs
if (css !== transformed_css) {
return `function css(assets, base) { return \`${s(transformed_css).slice(1, -1)}\`; }`;
}
return s(css);
};
}
}

Expand Down Expand Up @@ -96,6 +146,9 @@ export async function build_server_nodes(
/** @type {string[]} */
let fonts = [];

/** @type {Set<string>} */
let eager_assets = new Set();

if (node.component && client_manifest) {
exports.push(
'let component_cache;',
Expand Down Expand Up @@ -135,7 +188,7 @@ export async function build_server_nodes(
const entry_path = `${normalizePath(kit.outDir)}/generated/client-optimized/nodes/${i}.js`;
const entry = find_deps(client_manifest, entry_path, true);

// eagerly load client stylesheets and fonts imported by the SSR-ed page to avoid FOUC.
// Eagerly load client stylesheets and fonts imported by the SSR-ed page to avoid FOUC.
// However, if it is not used during SSR (not present in the server manifest),
// then it can be lazily loaded in the browser.

Expand All @@ -153,8 +206,6 @@ export async function build_server_nodes(

/** @type {Set<string>} */
const eager_css = new Set();
/** @type {Set<string>} */
const eager_assets = new Set();

entry.stylesheet_map.forEach((value, filepath) => {
// pages and layouts are renamed to node indexes when optimised for the client
Expand All @@ -180,27 +231,46 @@ export async function build_server_nodes(
`export const fonts = ${s(fonts)};`
);

/** @type {string[]} */
const inline_styles = [];

stylesheets.forEach((file, i) => {
if (stylesheets_to_inline.has(file)) {
const filename = basename(file);
const dest = `${out}/server/stylesheets/${filename}.js`;
const source = stylesheets_to_inline.get(file);
if (!source) {
throw new Error(`Server stylesheet source not found for client stylesheet ${file}`);
/**
* Assets that have been processed by Vite (decoded and with the asset path stripped)
* @type {Set<string>}
*/
let vite_assets = new Set();

// Keep track of Vite asset filenames so that we avoid touching unrelated ones
// when adjusting the inlined CSS
if (stylesheets_to_inline.size && assets_path && eager_assets.size) {
vite_assets = new Set(
Array.from(eager_assets).map((asset) => {
return decodeURIComponent(asset.replace(`${assets_path}/`, ''));
})
);
}

if (stylesheets_to_inline.size) {
/** @type {string[]} */
const inline_styles = [];

stylesheets.forEach((file, i) => {
if (stylesheets_to_inline.has(file)) {
const filename = basename(file);
const dest = `${out}/server/stylesheets/${filename}.js`;

let css = /** @type {string} */ (stylesheets_to_inline.get(file));

fs.writeFileSync(
dest,
`// ${filename}\nexport default ${prepare_css_for_inlining(css, vite_assets)};`
);
const name = `stylesheet_${i}`;
imports.push(`import ${name} from '../stylesheets/${filename}.js';`);
inline_styles.push(`\t${s(file)}: ${name}`);
}
fs.writeFileSync(dest, `// ${filename}\nexport default ${s(source)};`);
});

const name = `stylesheet_${i}`;
imports.push(`import ${name} from '../stylesheets/${filename}.js';`);
inline_styles.push(`\t${s(file)}: ${name}`);
if (inline_styles.length > 0) {
exports.push(`export const inline_styles = () => ({\n${inline_styles.join(',\n')}\n});`);
}
});

if (inline_styles.length > 0) {
exports.push(`export const inline_styles = () => ({\n${inline_styles.join(',\n')}\n});`);
}

fs.writeFileSync(
Expand All @@ -209,37 +279,3 @@ export async function build_server_nodes(
);
}
}

/**
* @param {(import('vite').Rollup.OutputAsset | import('vite').Rollup.OutputChunk)[]} chunks
*/
function get_stylesheets(chunks) {
/**
* A map of module IDs and the stylesheets they use.
* @type {Map<string, string[]>}
*/
const stylesheets_used = new Map();

/**
* A map of stylesheet names and their content.
* @type {Map<string, string>}
*/
const stylesheet_content = new Map();

for (const chunk of chunks) {
if (chunk.type === 'asset') {
if (chunk.fileName.endsWith('.css')) {
stylesheet_content.set(chunk.fileName, chunk.source.toString());
}
continue;
}

if (chunk.viteMetadata?.importedCss.size) {
const css = Array.from(chunk.viteMetadata.importedCss);
for (const id of chunk.moduleIds) {
stylesheets_used.set(id, css);
}
}
}
return { stylesheets_used, stylesheet_content };
}
4 changes: 2 additions & 2 deletions packages/kit/src/exports/vite/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1031,7 +1031,7 @@ async function kit({ svelte_config }) {
*/
writeBundle: {
sequential: true,
async handler(_options, bundle) {
async handler(_options) {
if (secondary_build_started) return; // only run this once

const verbose = vite_config.logLevel === 'info';
Expand Down Expand Up @@ -1271,7 +1271,7 @@ async function kit({ svelte_config }) {
manifest_data,
server_manifest,
client_manifest,
bundle,
assets_path,
client_chunks,
svelte_config.kit.output,
static_exports
Expand Down
9 changes: 8 additions & 1 deletion packages/kit/src/runtime/server/page/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,14 @@ export async function render_response({
for (const url of node.fonts) fonts.add(url);

if (node.inline_styles && !client.inline) {
Object.entries(await node.inline_styles()).forEach(([k, v]) => inline_styles.set(k, v));
Object.entries(await node.inline_styles()).forEach(([filename, css]) => {
if (typeof css === 'string') {
inline_styles.set(filename, css);
return;
}

inline_styles.set(filename, css(`${assets}/${paths.app_dir}/immutable/assets`, assets));
});
}
}
} else {
Expand Down
4 changes: 3 additions & 1 deletion packages/kit/src/types/internal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,9 @@ export interface SSRNode {
server_id?: string;

/** inlined styles */
inline_styles?(): MaybePromise<Record<string, string>>;
inline_styles?(): MaybePromise<
Record<string, string | ((assets: string, base: string) => string)>
>;
/** Svelte component */
component?: SSRComponentLoader;
/** +page.js or +layout.js */
Expand Down
Loading
Loading