Skip to content

create_function_as_string doesn't escape backticks/template expressions, breaking SSR with CSS containing literal backticks #15588

@scosman

Description

@scosman

Describe the bug

SvelteKit's create_function_as_string in src/exports/vite/build/utils.js wraps CSS content into a JavaScript template literal, but uses JSON.stringify to escape the string contents. JSON.stringify does not escape backticks or ${ sequences, since those are not special in JSON strings — but they are special in template literals.

This causes a SyntaxError: Invalid or unexpected token during prerendering when any CSS content contains a literal backtick character.

Suggested fix

Escape backticks and ${ sequences after JSON.stringify:

export function create_function_as_string(name, placeholder_names, str) {
    str = s(str).slice(1, -1).replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
    const args = placeholder_names ? placeholder_names.join(', ') : '';
    return `function ${name}(${args}) { return \`${str}\`; }`;
}

Severity

This is a build-breaking bug triggered by a common CSS library (@tailwindcss/typography) combined with inlineStyleThreshold. Standard Vite/PostCSS plugin hooks cannot work around it because build_server_nodes runs inside SvelteKit's own writeBundle hook, after all user plugin hooks have executed, and immediately before prerender.

Workaround

Using patch-package to apply the above one-line fix to src/exports/vite/build/utils.js.

Reproduction

  1. Create a SvelteKit project with @tailwindcss/typography and an inlineStyleThreshold large enough to inline the full stylesheet:
// svelte.config.js
export default {
  kit: {
    adapter: adapter(),
    inlineStyleThreshold: 150000,
  },
};
  1. Include @tailwindcss/typography in your CSS:
@import "tailwindcss";
@plugin '@tailwindcss/typography';
  1. Run vite build

The build fails during SSR prerendering with:

SyntaxError: Invalid or unexpected token

Root cause

@tailwindcss/typography generates a CSS rule with a literal backtick:

.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):before,
.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):after {
  content: "`";
}

In create_function_as_string, the CSS is embedded into a template literal:

export function create_function_as_string(name, placeholder_names, str) {
    str = s(str).slice(1, -1);  // s = JSON.stringify
    const args = placeholder_names ? placeholder_names.join(', ') : '';
    return `function ${name}(${args}) { return \`${str}\`; }`;
}

JSON.stringify converts the CSS to a JSON string and strips the outer quotes, but backticks pass through unescaped. The resulting generated code in .svelte-kit/output/server/stylesheets/*.css.js looks like:

export default function css(assets, base) { return `...content:"\`"...`; }
//                                                        ^ breaks the template literal

Logs

System Info

- `@sveltejs/kit` 2.55.0
- `@tailwindcss/typography` (via `@tailwindcss/postcss`)
- Tailwind CSS v4.0.9
- Node.js v24
- Vite 6

Severity

serious, but I can work around it

Additional Information

AI assisted issue, but real issue impacting pretty popular SvelteKit template: https://github.com/scosman/CMSaasStarter

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions