diff --git a/.changeset/rich-taxis-hear.md b/.changeset/rich-taxis-hear.md
new file mode 100644
index 000000000000..0c13363b4996
--- /dev/null
+++ b/.changeset/rich-taxis-hear.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+chore: reenable server CSS output through a compiler option
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js
index 9359f967dfd2..50957339d756 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js
@@ -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);
@@ -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 ||
diff --git a/packages/svelte/src/compiler/types/index.d.ts b/packages/svelte/src/compiler/types/index.d.ts
index 9574491756fa..acd09e28b193 100644
--- a/packages/svelte/src/compiler/types/index.d.ts
+++ b/packages/svelte/src/compiler/types/index.d.ts
@@ -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.
*
diff --git a/packages/svelte/src/compiler/validate-options.js b/packages/svelte/src/compiler/validate-options.js
index bd997a36ab8c..e5bf6d189ca4 100644
--- a/packages/svelte/src/compiler/validate-options.js
+++ b/packages/svelte/src/compiler/validate-options.js
@@ -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),
diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js
index 3fcee27a9915..dab8ff91601d 100644
--- a/packages/svelte/src/internal/server/index.js
+++ b/packages/svelte/src/internal/server/index.js
@@ -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 };
}
/**
@@ -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
};
diff --git a/packages/svelte/src/internal/server/types.d.ts b/packages/svelte/src/internal/server/types.d.ts
index d0c4a63f878c..891c54e6d007 100644
--- a/packages/svelte/src/internal/server/types.d.ts
+++ b/packages/svelte/src/internal/server/types.d.ts
@@ -14,6 +14,7 @@ export interface Component {
export interface Payload {
out: string;
anchor: number;
+ css: Set<{ code: string }>;
head: {
title: string;
out: string;
@@ -28,4 +29,6 @@ export interface RenderOutput {
html: string;
/** HTML that goes somewhere into the `
` */
body: string;
+ /** The CSS from components that were compiled with `cssRenderOnServer` */
+ css: string;
}
diff --git a/packages/svelte/tests/server-side-rendering/samples/css-empty/_config.js b/packages/svelte/tests/server-side-rendering/samples/css-empty/_config.js
new file mode 100644
index 000000000000..388d51616229
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/css-empty/_config.js
@@ -0,0 +1,4 @@
+import { test } from '../../test';
+
+// Test validates that by default no CSS is rendered on the server
+export default test({});
diff --git a/packages/svelte/tests/server-side-rendering/samples/css-empty/_expected.css b/packages/svelte/tests/server-side-rendering/samples/css-empty/_expected.css
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/packages/svelte/tests/server-side-rendering/samples/css-empty/_expected.html b/packages/svelte/tests/server-side-rendering/samples/css-empty/_expected.html
new file mode 100644
index 000000000000..8ebe1ad73e11
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/css-empty/_expected.html
@@ -0,0 +1 @@
+foo
\ No newline at end of file
diff --git a/packages/svelte/tests/server-side-rendering/samples/css-empty/main.svelte b/packages/svelte/tests/server-side-rendering/samples/css-empty/main.svelte
new file mode 100644
index 000000000000..4b8552429547
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/css-empty/main.svelte
@@ -0,0 +1,7 @@
+foo
+
+
diff --git a/packages/svelte/tests/server-side-rendering/samples/css/_config.js b/packages/svelte/tests/server-side-rendering/samples/css/_config.js
new file mode 100644
index 000000000000..619f8298a871
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/css/_config.js
@@ -0,0 +1,7 @@
+import { test } from '../../test';
+
+export default test({
+ compileOptions: {
+ cssRenderOnServer: true
+ }
+});
diff --git a/packages/svelte/tests/server-side-rendering/samples/css/_expected.css b/packages/svelte/tests/server-side-rendering/samples/css/_expected.css
new file mode 100644
index 000000000000..8882c6ec7e31
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/css/_expected.css
@@ -0,0 +1,4 @@
+
+ .foo.svelte-sg04hs {
+ color: red;
+ }
diff --git a/packages/svelte/tests/server-side-rendering/samples/css/_expected.html b/packages/svelte/tests/server-side-rendering/samples/css/_expected.html
new file mode 100644
index 000000000000..8ebe1ad73e11
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/css/_expected.html
@@ -0,0 +1 @@
+foo
\ No newline at end of file
diff --git a/packages/svelte/tests/server-side-rendering/samples/css/main.svelte b/packages/svelte/tests/server-side-rendering/samples/css/main.svelte
new file mode 100644
index 000000000000..4b8552429547
--- /dev/null
+++ b/packages/svelte/tests/server-side-rendering/samples/css/main.svelte
@@ -0,0 +1,7 @@
+foo
+
+
diff --git a/packages/svelte/tests/server-side-rendering/test.ts b/packages/svelte/tests/server-side-rendering/test.ts
index 2597f1d2eb52..7f8e56564bbb 100644
--- a/packages/svelte/tests/server-side-rendering/test.ts
+++ b/packages/svelte/tests/server-side-rendering/test.ts
@@ -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';
@@ -23,7 +24,7 @@ const { test, run } = suite(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);
@@ -61,6 +62,25 @@ const { test, run } = suite(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 };
diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts
index 5055799a9d7e..3ff9df9ee5c3 100644
--- a/packages/svelte/types/index.d.ts
+++ b/packages/svelte/types/index.d.ts
@@ -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.
*
@@ -2183,6 +2189,8 @@ declare module 'svelte/server' {
html: string;
/** HTML that goes somewhere into the `` */
body: string;
+ /** The CSS from components that were compiled with `cssRenderOnServer` */
+ css: string;
}
export {};
@@ -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.
*
diff --git a/sites/svelte-5-preview/src/routes/docs/content/01-api/05-imports.md b/sites/svelte-5-preview/src/routes/docs/content/01-api/05-imports.md
index f9befc4c6682..a3c26a5cc3a7 100644
--- a/sites/svelte-5-preview/src/routes/docs/content/01-api/05-imports.md
+++ b/sites/svelte-5-preview/src/routes/docs/content/01-api/05-imports.md
@@ -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:
diff --git a/sites/svelte-5-preview/src/routes/docs/content/03-appendix/02-breaking-changes.md b/sites/svelte-5-preview/src/routes/docs/content/03-appendix/02-breaking-changes.md
index 8de2bfbfe422..77d915747347 100644
--- a/sites/svelte-5-preview/src/routes/docs/content/03-appendix/02-breaking-changes.md
+++ b/sites/svelte-5-preview/src/routes/docs/content/03-appendix/02-breaking-changes.md
@@ -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.