Skip to content
Closed
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/rich-taxis-hear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

chore: reenable server CSS output through a compiler option
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
BLOCK_OPEN_ELSE
} from '../../../../internal/server/hydration.js';
import { filename, locator } from '../../../state.js';
import { render_stylesheet } from '../css/index.js';

/** Opens an if/each block, so that we can remove nodes in the case of a mismatch */
const block_open = b.literal(BLOCK_OPEN);
Expand Down Expand Up @@ -2158,6 +2159,18 @@ export function server_component(analysis, options) {

const body = [...state.hoisted, ...module.body];

if (options.cssRenderOnServer) {
body.push(
b.const(
'$$css',
b.object([
b.init('code', b.literal(render_stylesheet(analysis.source, analysis, options).code))
])
)
);
component_block.body.unshift(b.stmt(b.call('$$payload.css.add', b.id('$$css'))));
}

let should_inject_props =
should_inject_context ||
props.length > 0 ||
Expand Down
6 changes: 6 additions & 0 deletions packages/svelte/src/compiler/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,12 @@ export interface CompileOptions extends ModuleCompileOptions {
* @default undefined
*/
cssHash?: CssHashGetter;
/**
* Whether or not to include the CSS in the compiled server output, so that it's added to the `css` output of `render` from `svelte/server`.
*
* @default false
*/
cssRenderOnServer?: boolean;
/**
* If `true`, your HTML comments will be preserved during server-side rendering. By default, they are stripped out.
*
Expand Down
2 changes: 2 additions & 0 deletions packages/svelte/src/compiler/validate-options.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ export const validate_component_options =
return `svelte-${hash(css)}`;
}),

cssRenderOnServer: boolean(false),

// TODO this is a sourcemap option, would be good to put under a sourcemap namespace
cssOutputFilename: string(undefined),

Expand Down
5 changes: 4 additions & 1 deletion packages/svelte/src/internal/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const VoidElements = new Set([

/** @returns {import('#server').Payload} */
function create_payload() {
return { out: '', head: { title: '', out: '', anchor: 0 }, anchor: 0 };
return { out: '', css: new Set(), head: { title: '', out: '', anchor: 0 }, anchor: 0 };
}

/**
Expand Down Expand Up @@ -131,6 +131,9 @@ export function render(component, options = {}) {

return {
head: payload.head.out || payload.head.title ? payload.head.out + payload.head.title : '',
css: Array.from(payload.css)
.map(({ code }) => code)
.join('\n'),
html: payload.out,
body: payload.out
};
Expand Down
3 changes: 3 additions & 0 deletions packages/svelte/src/internal/server/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export interface Component {
export interface Payload {
out: string;
anchor: number;
css: Set<{ code: string }>;
head: {
title: string;
out: string;
Expand All @@ -28,4 +29,6 @@ export interface RenderOutput {
html: string;
/** HTML that goes somewhere into the `<body>` */
body: string;
/** The CSS from components that were compiled with `cssRenderOnServer` */
css: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { test } from '../../test';

// Test validates that by default no CSS is rendered on the server
export default test({});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div class="foo svelte-sg04hs">foo</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<div class="foo">foo</div>

<style>
.foo {
color: red;
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { test } from '../../test';

export default test({
compileOptions: {
cssRenderOnServer: true
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

.foo.svelte-sg04hs {
color: red;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div class="foo svelte-sg04hs">foo</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<div class="foo">foo</div>

<style>
.foo {
color: red;
}
</style>
22 changes: 21 additions & 1 deletion packages/svelte/tests/server-side-rendering/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// TODO: happy-dom might be faster but currently replaces quotes which fails assertions

import * as fs from 'node:fs';
import { assert } from 'vitest';
import { render } from 'svelte/server';
import { compile_directory, should_update_expected, try_read_file } from '../helpers.js';
import { assert_html_equal_with_options } from '../html_equal.js';
Expand All @@ -23,7 +24,7 @@ const { test, run } = suite<SSRTest>(async (config, test_dir) => {
const Component = (await import(`${test_dir}/_output/server/main.svelte.js`)).default;
const expected_html = try_read_file(`${test_dir}/_expected.html`);
const rendered = render(Component, { props: config.props || {} });
const { body, head } = rendered;
const { body, head, css } = rendered;

fs.writeFileSync(`${test_dir}/_actual.html`, body);

Expand Down Expand Up @@ -61,6 +62,25 @@ const { test, run } = suite<SSRTest>(async (config, test_dir) => {
}
}
}

if (fs.existsSync(`${test_dir}/_expected.css`)) {
fs.writeFileSync(`${test_dir}/_actual.css`, css);

try {
assert.equal(
css.replaceAll('\r', ''),
fs.readFileSync(`${test_dir}/_expected.css`, 'utf-8').replaceAll('\r', '')
);
} catch (error: any) {
if (should_update_expected()) {
fs.writeFileSync(`${test_dir}/_expected.css`, css);
console.log(`Updated ${test_dir}/_expected.css.`);
error.message += '\n' + `${test_dir}/main.svelte`;
} else {
throw error;
}
}
}
});

export { test };
Expand Down
14 changes: 14 additions & 0 deletions packages/svelte/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -772,6 +772,12 @@ declare module 'svelte/compiler' {
* @default undefined
*/
cssHash?: CssHashGetter;
/**
* Whether or not to include the CSS in the compiled server output, so that it's added to the `css` output of `render` from `svelte/server`.
*
* @default false
*/
cssRenderOnServer?: boolean;
/**
* If `true`, your HTML comments will be preserved during server-side rendering. By default, they are stripped out.
*
Expand Down Expand Up @@ -2183,6 +2189,8 @@ declare module 'svelte/server' {
html: string;
/** HTML that goes somewhere into the `<body>` */
body: string;
/** The CSS from components that were compiled with `cssRenderOnServer` */
css: string;
}

export {};
Expand Down Expand Up @@ -2580,6 +2588,12 @@ declare module 'svelte/types/compiler/interfaces' {
* @default undefined
*/
cssHash?: CssHashGetter;
/**
* Whether or not to include the CSS in the compiled server output, so that it's added to the `css` output of `render` from `svelte/server`.
*
* @default false
*/
cssRenderOnServer?: boolean;
/**
* If `true`, your HTML comments will be preserved during server-side rendering. By default, they are stripped out.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,8 @@ const result = render(App, {
});
```

`render` also returns a `css` string property, which is populated with the CSS from all components which were compiled with the `cssRenderOnServer` option.

## `svelte/elements`

Svelte provides built-in [DOM types](https://github.com/sveltejs/svelte/blob/master/packages/svelte/elements.d.ts). A common use case for DOM types is forwarding props to an HTML element. To properly type your props and get full intellisense, your props interface should extend the attributes type for your HTML element:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,10 @@ In the event that you need to support ancient browsers that don't implement `:wh
css = css.replace(/:where\((.+?)\)/, '$1');
```

### CSS no longer returned from server render by default

In Svelte 4, rendering a component to a string also returned the CSS of all components. In Svelte 5, this is no longer the case by default because most of the time you're using a tooling chain that takes care of it in other ways (like SvelteKit). If you need the CSS string returned from `render`, you can enable it via the `cssRenderOnServer` option.

### Error/warning codes have been renamed

Error and warning codes have been renamed. Previously they used dashes to separate the words, they now use underscores (e.g. foo-bar becomes foo_bar). Additionally, a handful of codes have been reworded slightly.
Expand Down