Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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/sad-laws-kick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

fix: escape backticks and dollar signs when creating inlined css
5 changes: 3 additions & 2 deletions packages/kit/src/exports/vite/build/build_server.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import fs from 'node:fs';
import { mkdirp } from '../../../utils/filesystem.js';
import { filter_fonts, find_deps, resolve_symlinks } from './utils.js';
import { create_function_as_string, filter_fonts, find_deps, resolve_symlinks } from './utils.js';
import { s } from '../../../utils/misc.js';
import { normalizePath } from 'vite';
import { basename } from 'node:path';
Expand Down Expand Up @@ -78,8 +78,9 @@ export function build_server_nodes(

// 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 create_function_as_string('css', ['assets', 'base'], transformed_css);
}

return s(css);
};
}
Expand Down
16 changes: 16 additions & 0 deletions packages/kit/src/exports/vite/build/utils.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import fs from 'node:fs';
import path from 'node:path';
import { normalizePath } from 'vite';
import { s } from '../../../utils/misc.js';

/**
* Adds transitive JS and CSS dependencies to the js and css inputs.
Expand Down Expand Up @@ -129,3 +130,18 @@ export function filter_fonts(assets) {
export function assets_base(config) {
return (config.paths.assets || config.paths.base || '.') + '/';
}

/**
* Writes a function with arguments used by a template literal.
* This helps us store strings in a module and inject values at runtime.
* @param {string} name The name of the function
* @param {string[]} placeholder_names The names of the placeholders in the string
* @param {string} str A string with placeholders such as "Hello ${arg0}".
* It must have backticks and dollar signs escaped.
* @returns {string} The function written as a string
*/
export function create_function_as_string(name, placeholder_names, str) {
str = s(str).slice(1, -1);
const args = placeholder_names ? placeholder_names.join(', ') : '';
return `function ${name}(${args}) { return \`${str}\`; }`;
}
12 changes: 12 additions & 0 deletions packages/kit/src/exports/vite/build/utils.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { assert, test } from 'vitest';
import { create_function_as_string } from './utils.js';

test('create_dynamic_string escapes backslashes', () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should test for dollar signs too.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The escaping is done in a separate function and is tested in https://github.com/sveltejs/kit/pull/15320/changes#diff-ee61d627d800b208617d7a722797dc2636f588c2909ea779b52d3db38fd75d20R21

This makes the code kind of roundabout since the escaping and function creation isn't in the same place. But we needed to escape the CSS before we add the placeholders ${example}. Otherwise, the needed dollar signs would have been escaped too. https://github.com/sveltejs/kit/pull/15320/changes#diff-6198c333fe881f783129119271d28c82da0710a57556a80a844eaf27ee0681b3R63

We could insert our own placeholders instead such as __SVELTEKIT_ASSET__, then do the escaping and placeholder replacements in the util function. But that also seems a bit roundabout. Is there a better way to go about this?

const input = "div:after { content: '\\s'; }";
const code = create_function_as_string('css', [], input);
assert.equal(code, "function css() { return `div:after { content: '\\\\s'; }`; }");

const css = eval(`(${code})()`);

assert.equal(css, input);
});
3 changes: 3 additions & 0 deletions packages/kit/src/utils/css.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import MagicString from 'magic-string';
import * as svelte from 'svelte/compiler';
import { escape_for_interpolation } from './escape.js';

/** @typedef {ReturnType<typeof import('svelte/compiler').parseCss>['children']} StyleSheetChildren */

Expand Down Expand Up @@ -59,6 +60,8 @@ export function fix_css_urls({
return css;
}

css = escape_for_interpolation(css);

// safe guard in case of trailing slashes (but this should never happen)
if (paths_assets.endsWith('/')) {
paths_assets = paths_assets.slice(0, -1);
Expand Down
9 changes: 9 additions & 0 deletions packages/kit/src/utils/escape.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,12 @@ export function escape_html(str, is_attr) {

return escaped_str;
}

/**
* Escapes backticks and dollar signs so that they can be safely used in template literals.
* @param {string} str
* @returns {string} escaped string
*/
export function escape_for_interpolation(str) {
return str.replaceAll('`', '\\`').replaceAll('$', '\\$');
}
9 changes: 8 additions & 1 deletion packages/kit/src/utils/escape.spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { assert, test } from 'vitest';
import { escape_html } from './escape.js';
import { escape_for_interpolation, escape_html } from './escape.js';

test('escape_html_attr escapes special attribute characters', () => {
assert.equal(
Expand All @@ -17,3 +17,10 @@ test('escape_html_attr escapes invalid surrogates', () => {
assert.equal(escape_html('\ud800\udc00\udc00', true), '\ud800\udc00&#56320;');
assert.equal(escape_html('\ud800\ud800\udc00\udc00', true), '&#55296;\ud800\udc00&#56320;');
});

test('escape_for_interpolation escapes both backticks and dollar signs', () => {
assert.equal(
escape_for_interpolation('div:after { content: "` and ${example}`"; }'),
'div:after { content: "\\` and \\${example}\\`"; }'
);
});
Loading