diff --git a/.changeset/sad-laws-kick.md b/.changeset/sad-laws-kick.md new file mode 100644 index 000000000000..33c834934e79 --- /dev/null +++ b/.changeset/sad-laws-kick.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: escape backticks and dollar signs when creating inlined css diff --git a/packages/kit/src/exports/vite/build/build_server.js b/packages/kit/src/exports/vite/build/build_server.js index 31f2af742fee..c952be9d0022 100644 --- a/packages/kit/src/exports/vite/build/build_server.js +++ b/packages/kit/src/exports/vite/build/build_server.js @@ -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'; @@ -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); }; } diff --git a/packages/kit/src/exports/vite/build/utils.js b/packages/kit/src/exports/vite/build/utils.js index 962e36c7e39a..8da2e7020c6a 100644 --- a/packages/kit/src/exports/vite/build/utils.js +++ b/packages/kit/src/exports/vite/build/utils.js @@ -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. @@ -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}\`; }`; +} diff --git a/packages/kit/src/exports/vite/build/utils.spec.js b/packages/kit/src/exports/vite/build/utils.spec.js new file mode 100644 index 000000000000..26ce1b2e9bcd --- /dev/null +++ b/packages/kit/src/exports/vite/build/utils.spec.js @@ -0,0 +1,12 @@ +import { assert, test } from 'vitest'; +import { create_function_as_string } from './utils.js'; + +test('create_dynamic_string escapes backslashes', () => { + 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); +}); diff --git a/packages/kit/src/utils/css.js b/packages/kit/src/utils/css.js index 9953de3b6de1..9553fd38e48e 100644 --- a/packages/kit/src/utils/css.js +++ b/packages/kit/src/utils/css.js @@ -1,5 +1,6 @@ import MagicString from 'magic-string'; import * as svelte from 'svelte/compiler'; +import { escape_for_interpolation } from './escape.js'; /** @typedef {ReturnType['children']} StyleSheetChildren */ @@ -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); diff --git a/packages/kit/src/utils/escape.js b/packages/kit/src/utils/escape.js index a73fd951daf8..b2d0c9a6cfa7 100644 --- a/packages/kit/src/utils/escape.js +++ b/packages/kit/src/utils/escape.js @@ -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('$', '\\$'); +} diff --git a/packages/kit/src/utils/escape.spec.js b/packages/kit/src/utils/escape.spec.js index 3c4e6a042a86..8967b487de3b 100644 --- a/packages/kit/src/utils/escape.spec.js +++ b/packages/kit/src/utils/escape.spec.js @@ -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( @@ -17,3 +17,10 @@ test('escape_html_attr escapes invalid surrogates', () => { assert.equal(escape_html('\ud800\udc00\udc00', true), '\ud800\udc00�'); assert.equal(escape_html('\ud800\ud800\udc00\udc00', true), '�\ud800\udc00�'); }); + +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}\\`"; }' + ); +});