Skip to content

Conversation

@teemingc
Copy link
Member

@teemingc teemingc commented Jan 11, 2026

fixes #13878
closes #15147

This PR replaces the janky server-client css mapping (introduced by me) with simply replacing the asset URL references with the correct path when we inline the styles. Not only does this massively simplify things, it allows us to correctly prefix the paths based on the settings from paths.relative or path.assets.


Please don't delete this checklist! Before submitting the PR, please make sure you do the following:

  • It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs
  • This message body should clearly illustrate what problems it solves.
  • Ideally, include a test that fails without this PR but passes with it.

Tests

  • Run the tests with pnpm test and lint the project with pnpm lint and pnpm check

Changesets

  • If your PR makes a change that should be noted in one or more packages' changelogs, generate a changeset by running pnpm changeset and following the prompts. Changesets that add features should be minor and those that fix bugs should be patch. Please prefix changeset messages with feat:, fix:, or chore:.

Edits

  • Please ensure that 'Allow edits from maintainers' is checked. PRs without this option may be closed.

@changeset-bot
Copy link

changeset-bot bot commented Jan 11, 2026

🦋 Changeset detected

Latest commit: 3b89c6b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@sveltejs/kit Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Comment on lines +12 to +13
"test:dev": "DEV=true playwright test && DEV=true PATHS_ASSETS=https://cdn.example.com/stuff playwright test",
"test:build": "playwright test && PATHS_ASSETS=https://cdn.example.com/stuff playwright test",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seemed like the easiest way to test both CSS inlining with and without paths.assets without introducing another test matrix.

paths.assets replaces the CSS asset urls at build time but paths.relative does it at runtime

* @returns {string}
*/
export function replace_css_relative_url(contents, base) {
return contents.replaceAll(/url\((['"]?)\.\//ig, `url($1${base}/`);
Copy link
Member Author

@teemingc teemingc Jan 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fortunately, we don't have to worry about the @import 'app.css' case that omits the url(...) syntax because Vite inlines the referenced stylesheet even when referenced by more than one other stylesheet https://vite.dev/guide/features#import-inlining-and-rebasing

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I pushed some whitespace-handling changes just to guard against edge cases (I don't think we actually have to worry about them because I don't think Vite will output CSS like that... but better to be safe).

The only other edge cases I can think of:

  • Is it a problem that this doesn't work on relative paths without ./ in front of them? i.e. url(img.png) won't get transformed.
  • This will replace content inside strings. So if you had the CSS content: "Example: If you were to put url(./rickroll.gif) here...", it would get modified, in spite of being a string. I don't see a way around this other than actually parsing the CSS contents with a CSS parser...

@teemingc
Copy link
Member Author

teemingc commented Jan 13, 2026

Is it a problem that this doesn't work on relative paths without ./ in front of them? i.e. url(img.png) won't get transformed.

I think after Vite processes the CSS the urls are always prefixed with ./ but I can check how it’s handled from both the static folder or otherwise to see if they do end up this way.

This will replace content inside strings. So if you had the CSS content: "Example: If you were to put url(./rickroll.gif) here...", it would get modified, in spite of being a string. I don't see a way around this other than actually parsing the CSS contents with a CSS parser...

Yeah, I think I should try out a CSS parser. Otherwise, maybe we’d have to do a negative lookahead. Are there any CSS parsers that you’d recommend?

EDIT: I see we were previously using csstree in Svelte so we can probably try that?

@teemingc
Copy link
Member Author

teemingc commented Jan 13, 2026

  • Is it a problem that this doesn't work on relative paths without ./ in front of them? i.e. url(img.png) won't get transformed.

So I did some testing and here are the results:

  1. If the asset can be found, it's always suffixed with ./ and lives next to the CSS file.
  2. If the asset can be found and it's located in the static folder, it's suffixed with ../../../ because static assets are located at the root path and it's relative to _app/immutable/assets.
  3. If the asset can't be found, the url value remains the same.

@teemingc teemingc force-pushed the fix-missing-css-with-inlining-enabled branch from 09886d8 to df13a14 Compare January 14, 2026 08:04
@teemingc teemingc changed the title fix: use regex to replace CSS paths instead of mapping server CSS fix: fix CSS URL paths of client stylesheets Jan 14, 2026
@teemingc teemingc marked this pull request as draft January 14, 2026 10:29
@teemingc
Copy link
Member Author

teemingc commented Jan 14, 2026

  • We'll use the Svelte CSS parser to identify declaration values and avoid accidentally affecting comments, strings, etc.
  • We'll also transform the stored CSS from a string to a function so we can easily plug in the relative path at runtime
  • We'll need to handle the case where the asset path is referencing something from the root because it was placed in a static dir. I've already added a test for this.

@teemingc teemingc changed the title fix: fix CSS URL paths of client stylesheets fix: ensure CSS URLs are correct when inlining client stylesheets Jan 22, 2026
@teemingc
Copy link
Member Author

Ok, I think it's ready. Some additional edge cases accounted for are URLs with escaped quotes, fragments, query strings, etc.. Only known assets are processed too so it doesn't just match anything with ./ or ../../../.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking awesome, tysm

const url_value_match = /url\(\s*(['"]?)(.*?)\1\s*\)/i.exec(url_declaration);
if (!url_value_match) continue;

const [, , url] = url_value_match;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you could use non-capturing groups ((?:)) for the stuff you don't actually want to capture to avoid these weird commas

Copy link
Member Author

@teemingc teemingc Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The quotation's using a capture group so that it matches against the same type of closing quotation mark. This avoids matching a double quote in a single quoted string.

Unfortunately, this doesn't account for backslashed quotes in the same string

Copy link
Member Author

@teemingc teemingc Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I've fixed the escaped characters edge case but the CSS parser is duplicating the escaped characters in the declaration value it returns. I think we need to avoid adding the same characters twice here https://github.com/sveltejs/svelte/blob/9662010a135937b7a920eaf93933be7b8b0c9c9e/packages/svelte/src/compiler/phases/1-parse/read/style.js#L508-L525 Currently, a string such as \s becomes \\ss in the returned AST

EDIT: opened a PR for it here sveltejs/svelte#17554

EDIT2: We just need to wait for sveltejs/svelte#17555 to be merged and update the Svelte version in Kit to get the tests passing

* @param {string} value
* @returns {string}
*/
function tippex_comments_and_strings(value) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did have to google what tippex meant :laugh:

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fixed several tippex function edge cases and pushed a bunch of test cases to make sure it never regresses -- thank you so much for your work on this gnarly bug. Great job!

@elliott-with-the-longest-name-on-github elliott-with-the-longest-name-on-github merged commit 52965cf into main Jan 27, 2026
25 checks passed
@elliott-with-the-longest-name-on-github elliott-with-the-longest-name-on-github deleted the fix-missing-css-with-inlining-enabled branch January 27, 2026 21:38
@github-actions github-actions bot mentioned this pull request Jan 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Some CSS styles not applied with non-zero inlineStyleThreshold since v2.21.3

4 participants