From 061e3a871d3c1d3fb7cd4a6ccb3458784f8b95c4 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Thu, 4 Jul 2024 09:46:54 +0200 Subject: [PATCH] chore: reenable server CSS output through a compiler option There are various use cases where this continues to be necessary/nice to have: - rendering OG cards - rendering emails - basically anything where you use `render` manually and want to quickly stitch together the CSS without setting up an elaborate tooling chain --- .changeset/rich-taxis-hear.md | 5 +++++ .../3-transform/server/transform-server.js | 13 +++++++++++ packages/svelte/src/compiler/types/index.d.ts | 6 +++++ .../svelte/src/compiler/validate-options.js | 2 ++ packages/svelte/src/internal/server/index.js | 5 ++++- .../svelte/src/internal/server/types.d.ts | 3 +++ .../samples/css-empty/_config.js | 4 ++++ .../samples/css-empty/_expected.css | 0 .../samples/css-empty/_expected.html | 1 + .../samples/css-empty/main.svelte | 7 ++++++ .../samples/css/_config.js | 7 ++++++ .../samples/css/_expected.css | 4 ++++ .../samples/css/_expected.html | 1 + .../samples/css/main.svelte | 7 ++++++ .../tests/server-side-rendering/test.ts | 22 ++++++++++++++++++- packages/svelte/types/index.d.ts | 14 ++++++++++++ .../routes/docs/content/01-api/05-imports.md | 2 ++ .../03-appendix/02-breaking-changes.md | 4 ++++ 18 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 .changeset/rich-taxis-hear.md create mode 100644 packages/svelte/tests/server-side-rendering/samples/css-empty/_config.js create mode 100644 packages/svelte/tests/server-side-rendering/samples/css-empty/_expected.css create mode 100644 packages/svelte/tests/server-side-rendering/samples/css-empty/_expected.html create mode 100644 packages/svelte/tests/server-side-rendering/samples/css-empty/main.svelte create mode 100644 packages/svelte/tests/server-side-rendering/samples/css/_config.js create mode 100644 packages/svelte/tests/server-side-rendering/samples/css/_expected.css create mode 100644 packages/svelte/tests/server-side-rendering/samples/css/_expected.html create mode 100644 packages/svelte/tests/server-side-rendering/samples/css/main.svelte 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 b9d6a5bc4da3..945d98a01fa1 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 } from '../../../../internal/server/hydration.js'; import { filename, locator } from '../../../state.js'; +import { render_stylesheet } from '../css/index.js'; export const block_open = b.literal(BLOCK_OPEN); export const block_close = b.literal(BLOCK_CLOSE); @@ -2134,6 +2135,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 129b5da175cc..c95f071edb0f 100644 --- a/packages/svelte/src/compiler/types/index.d.ts +++ b/packages/svelte/src/compiler/types/index.d.ts @@ -119,6 +119,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 97ddc6c3aa07..30f547f52144 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 }; } /** @@ -125,6 +125,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 305e1240db8b..3ff18f9a4c98 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -778,6 +778,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. * @@ -2194,6 +2200,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 {}; @@ -2601,6 +2609,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 5b08103d8050..248f50c73521 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.