From 061e3a871d3c1d3fb7cd4a6ccb3458784f8b95c4 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Thu, 4 Jul 2024 09:46:54 +0200 Subject: [PATCH 01/16] 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. From 236e6a27c8518224d0a9359a8f399204d50de2c6 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 9 Jul 2024 15:10:22 -0400 Subject: [PATCH 02/16] cssRenderOnServer -> css: 'injected' --- .../3-transform/server/transform-server.js | 2 +- packages/svelte/src/compiler/types/index.d.ts | 10 ++------- .../svelte/src/compiler/validate-options.js | 2 -- .../svelte/src/internal/server/types.d.ts | 2 -- .../samples/css/_config.js | 2 +- packages/svelte/types/index.d.ts | 22 ++++--------------- .../routes/docs/content/01-api/05-imports.md | 2 +- .../03-appendix/02-breaking-changes.md | 2 +- 8 files changed, 10 insertions(+), 34 deletions(-) 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 50957339d756..0106223a7f74 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 @@ -2159,7 +2159,7 @@ export function server_component(analysis, options) { const body = [...state.hoisted, ...module.body]; - if (options.cssRenderOnServer) { + if (options.css === 'injected') { body.push( b.const( '$$css', diff --git a/packages/svelte/src/compiler/types/index.d.ts b/packages/svelte/src/compiler/types/index.d.ts index acd09e28b193..1b27146982a9 100644 --- a/packages/svelte/src/compiler/types/index.d.ts +++ b/packages/svelte/src/compiler/types/index.d.ts @@ -99,8 +99,8 @@ export interface CompileOptions extends ModuleCompileOptions { */ immutable?: boolean; /** - * - `'injected'`: styles will be included in the JavaScript class and injected at runtime for the components actually rendered. - * - `'external'`: the CSS will be returned in the `css` field of the compilation result. Most Svelte bundler plugins will set this to `'external'` and use the CSS that is statically generated for better performance, as it will result in smaller JavaScript bundles and the output can be served as cacheable `.css` files. + * - `'injected'`: styles will be included in the `head` when using `render(...)`, and injected into the document (if not already present) when the component mounts. For components compiled as custom elements, styles are injected to the shadow root. + * - `'external'`: the CSS will only be returned in the `css` field of the compilation result. Most Svelte bundler plugins will set this to `'external'` and use the CSS that is statically generated for better performance, as it will result in smaller JavaScript bundles and the output can be served as cacheable `.css` files. * This is always `'injected'` when compiling with `customElement` mode. */ css?: 'injected' | 'external'; @@ -111,12 +111,6 @@ 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 e5bf6d189ca4..bd997a36ab8c 100644 --- a/packages/svelte/src/compiler/validate-options.js +++ b/packages/svelte/src/compiler/validate-options.js @@ -69,8 +69,6 @@ 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/types.d.ts b/packages/svelte/src/internal/server/types.d.ts index 891c54e6d007..af7d8e4a5bbd 100644 --- a/packages/svelte/src/internal/server/types.d.ts +++ b/packages/svelte/src/internal/server/types.d.ts @@ -29,6 +29,4 @@ 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/_config.js b/packages/svelte/tests/server-side-rendering/samples/css/_config.js index 619f8298a871..194b2443f9c9 100644 --- a/packages/svelte/tests/server-side-rendering/samples/css/_config.js +++ b/packages/svelte/tests/server-side-rendering/samples/css/_config.js @@ -2,6 +2,6 @@ import { test } from '../../test'; export default test({ compileOptions: { - cssRenderOnServer: true + css: 'injected' } }); diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 3ff9df9ee5c3..969d4f824470 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -760,8 +760,8 @@ declare module 'svelte/compiler' { */ immutable?: boolean; /** - * - `'injected'`: styles will be included in the JavaScript class and injected at runtime for the components actually rendered. - * - `'external'`: the CSS will be returned in the `css` field of the compilation result. Most Svelte bundler plugins will set this to `'external'` and use the CSS that is statically generated for better performance, as it will result in smaller JavaScript bundles and the output can be served as cacheable `.css` files. + * - `'injected'`: styles will be included in the `head` when using `render(...)`, and injected into the document (if not already present) when the component mounts. For components compiled as custom elements, styles are injected to the shadow root. + * - `'external'`: the CSS will only be returned in the `css` field of the compilation result. Most Svelte bundler plugins will set this to `'external'` and use the CSS that is statically generated for better performance, as it will result in smaller JavaScript bundles and the output can be served as cacheable `.css` files. * This is always `'injected'` when compiling with `customElement` mode. */ css?: 'injected' | 'external'; @@ -772,12 +772,6 @@ 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. * @@ -2189,8 +2183,6 @@ 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 {}; @@ -2576,8 +2568,8 @@ declare module 'svelte/types/compiler/interfaces' { */ immutable?: boolean; /** - * - `'injected'`: styles will be included in the JavaScript class and injected at runtime for the components actually rendered. - * - `'external'`: the CSS will be returned in the `css` field of the compilation result. Most Svelte bundler plugins will set this to `'external'` and use the CSS that is statically generated for better performance, as it will result in smaller JavaScript bundles and the output can be served as cacheable `.css` files. + * - `'injected'`: styles will be included in the `head` when using `render(...)`, and injected into the document (if not already present) when the component mounts. For components compiled as custom elements, styles are injected to the shadow root. + * - `'external'`: the CSS will only be returned in the `css` field of the compilation result. Most Svelte bundler plugins will set this to `'external'` and use the CSS that is statically generated for better performance, as it will result in smaller JavaScript bundles and the output can be served as cacheable `.css` files. * This is always `'injected'` when compiling with `customElement` mode. */ css?: 'injected' | 'external'; @@ -2588,12 +2580,6 @@ 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 a3c26a5cc3a7..af4118eaabae 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,7 +155,7 @@ 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. +If the `css` compiler option was set to `'injected'`, ``; + head += ``; } return { diff --git a/packages/svelte/tests/server-side-rendering/samples/css/_expected_head.html b/packages/svelte/tests/server-side-rendering/samples/css/_expected_head.html new file mode 100644 index 000000000000..72819356469a --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/css/_expected_head.html @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/head-html-and-component/_expected-head.html b/packages/svelte/tests/server-side-rendering/samples/head-html-and-component/_expected_head.html similarity index 100% rename from packages/svelte/tests/server-side-rendering/samples/head-html-and-component/_expected-head.html rename to packages/svelte/tests/server-side-rendering/samples/head-html-and-component/_expected_head.html diff --git a/packages/svelte/tests/server-side-rendering/samples/head-meta-hydrate-duplicate/_expected-head.html b/packages/svelte/tests/server-side-rendering/samples/head-meta-hydrate-duplicate/_expected_head.html similarity index 100% rename from packages/svelte/tests/server-side-rendering/samples/head-meta-hydrate-duplicate/_expected-head.html rename to packages/svelte/tests/server-side-rendering/samples/head-meta-hydrate-duplicate/_expected_head.html diff --git a/packages/svelte/tests/server-side-rendering/samples/head-multiple-title/_expected-head.html b/packages/svelte/tests/server-side-rendering/samples/head-multiple-title/_expected_head.html similarity index 100% rename from packages/svelte/tests/server-side-rendering/samples/head-multiple-title/_expected-head.html rename to packages/svelte/tests/server-side-rendering/samples/head-multiple-title/_expected_head.html diff --git a/packages/svelte/tests/server-side-rendering/samples/head-no-duplicates-with-binding/_expected-head.html b/packages/svelte/tests/server-side-rendering/samples/head-no-duplicates-with-binding/_expected_head.html similarity index 100% rename from packages/svelte/tests/server-side-rendering/samples/head-no-duplicates-with-binding/_expected-head.html rename to packages/svelte/tests/server-side-rendering/samples/head-no-duplicates-with-binding/_expected_head.html diff --git a/packages/svelte/tests/server-side-rendering/samples/head-title/_expected-head.html b/packages/svelte/tests/server-side-rendering/samples/head-title/_expected_head.html similarity index 100% rename from packages/svelte/tests/server-side-rendering/samples/head-title/_expected-head.html rename to packages/svelte/tests/server-side-rendering/samples/head-title/_expected_head.html diff --git a/packages/svelte/tests/server-side-rendering/test.ts b/packages/svelte/tests/server-side-rendering/test.ts index 7f8e56564bbb..27a48efa8382 100644 --- a/packages/svelte/tests/server-side-rendering/test.ts +++ b/packages/svelte/tests/server-side-rendering/test.ts @@ -24,9 +24,13 @@ 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, css } = rendered; + const { body, head } = rendered; - fs.writeFileSync(`${test_dir}/_actual.html`, body); + fs.writeFileSync(`${test_dir}/_output/rendered.html`, body); + + if (head) { + fs.writeFileSync(`${test_dir}/_output/rendered_head.html`, head); + } try { assert_html_equal_with_options(body, expected_html || '', { @@ -43,38 +47,17 @@ const { test, run } = suite(async (config, test_dir) => { } } - if (fs.existsSync(`${test_dir}/_expected-head.html`)) { - fs.writeFileSync(`${test_dir}/_actual-head.html`, head); - + if (fs.existsSync(`${test_dir}/_expected_head.html`)) { try { assert_html_equal_with_options( head, - fs.readFileSync(`${test_dir}/_expected-head.html`, 'utf-8'), + fs.readFileSync(`${test_dir}/_expected_head.html`, 'utf-8'), {} ); } catch (error: any) { if (should_update_expected()) { - fs.writeFileSync(`${test_dir}/_expected-head.html`, head); - console.log(`Updated ${test_dir}/_expected-head.html.`); - error.message += '\n' + `${test_dir}/main.svelte`; - } else { - throw error; - } - } - } - - 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.`); + fs.writeFileSync(`${test_dir}/_expected_head.html`, head); + console.log(`Updated ${test_dir}/_expected_head.html.`); error.message += '\n' + `${test_dir}/main.svelte`; } else { throw error; From 43c0f3353ac13f33acb54624862a35fe1d13ac80 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 9 Jul 2024 16:14:56 -0400 Subject: [PATCH 04/16] move append_styles into new module, update implementation --- .../3-transform/client/transform-client.js | 29 ++++++++-------- .../svelte/src/internal/client/dom/css.js | 29 ++++++++++++++++ packages/svelte/src/internal/client/index.js | 3 +- packages/svelte/src/internal/client/render.js | 33 +------------------ 4 files changed, 46 insertions(+), 48 deletions(-) create mode 100644 packages/svelte/src/internal/client/dom/css.js diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index a8438c0aca3c..61772317a760 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -332,22 +332,21 @@ export function client_component(source, analysis, options) { } } - const append_styles = - analysis.inject_styles && analysis.css.ast - ? () => - component_block.body.push( - b.stmt( - b.call( - '$.append_styles', - b.id('$$anchor'), - b.literal(analysis.css.hash), - b.literal(render_stylesheet(source, analysis, options).code) - ) - ) - ) - : () => {}; + if (analysis.css.ast !== null && options.css === 'injected') { + state.hoisted.push( + b.const( + '$$css', + b.object([ + b.init('hash', b.literal(analysis.css.hash)), + b.init('code', b.literal(render_stylesheet(analysis.source, analysis, options).code)) + ]) + ) + ); - append_styles(); + component_block.body.unshift( + b.stmt(b.call('$.append_styles', b.id('$$anchor'), b.id('$$css'))) + ); + } const should_inject_context = analysis.needs_context || diff --git a/packages/svelte/src/internal/client/dom/css.js b/packages/svelte/src/internal/client/dom/css.js new file mode 100644 index 000000000000..f5d453743487 --- /dev/null +++ b/packages/svelte/src/internal/client/dom/css.js @@ -0,0 +1,29 @@ +import { queue_micro_task } from './task.js'; + +var seen = new Set(); + +/** + * @param {Node} anchor + * @param {{ hash: string, code: string }} css + */ +export function append_styles(anchor, css) { + if (seen.has(css.hash)) return; + seen.add(css.hash); + + // Use `queue_micro_task` to ensure `anchor` is in the DOM, otherwise getRootNode() will yield wrong results + queue_micro_task(() => { + var root = anchor.getRootNode(); + + var target = /** @type {ShadowRoot} */ (root).host + ? /** @type {ShadowRoot} */ (root) + : /** @type {Document} */ (root).head; + + if (!target.querySelector('#' + css.hash)) { + const style = document.createElement('style'); + style.id = css.hash; + style.textContent = css.code; + + target.appendChild(style); + } + }); +} diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index ce9d9a999ddd..855631b0f345 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -20,6 +20,7 @@ export { snippet, wrap_snippet } from './dom/blocks/snippet.js'; export { component } from './dom/blocks/svelte-component.js'; export { element } from './dom/blocks/svelte-element.js'; export { head } from './dom/blocks/svelte-head.js'; +export { append_styles } from './dom/css.js'; export { action } from './dom/elements/actions.js'; export { remove_input_defaults, @@ -120,7 +121,7 @@ export { update_pre_store, update_store } from './reactivity/store.js'; -export { append_styles, set_text } from './render.js'; +export { set_text } from './render.js'; export { get, invalidate_inner_signals, diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index a00fd23d5433..97718b6f5366 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -22,6 +22,7 @@ import * as w from './warnings.js'; import * as e from './errors.js'; import { validate_component } from '../shared/validate.js'; import { assign_nodes } from './dom/template.js'; +import { queue_micro_task } from './dom/task.js'; /** @type {Set} */ export const all_registered_events = new Set(); @@ -294,35 +295,3 @@ export function unmount(component) { } fn?.(); } - -/** - * @param {Node} target - * @param {string} style_sheet_id - * @param {string} styles - */ -export async function append_styles(target, style_sheet_id, styles) { - // Wait a tick so that the template is added to the dom, else getRootNode() will yield wrong results - // If it turns out that this results in noticeable flickering, we need to do something like doing the - // append outside and adding code in mount that appends all stylesheets (similar to how we do it with event delegation) - await Promise.resolve(); - const append_styles_to = get_root_for_style(target); - if (!append_styles_to.getElementById(style_sheet_id)) { - const style = document.createElement('style'); - style.id = style_sheet_id; - style.textContent = styles; - const target = /** @type {Document} */ (append_styles_to).head || append_styles_to; - target.appendChild(style); - } -} - -/** - * @param {Node} node - */ -function get_root_for_style(node) { - if (!node) return document; - const root = node.getRootNode ? node.getRootNode() : node.ownerDocument; - if (root && /** @type {ShadowRoot} */ (root).host) { - return /** @type {ShadowRoot} */ (root); - } - return /** @type {Document} */ (node.ownerDocument); -} From 32eb838e82660c658766f4fcd1ace5098780d78c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 9 Jul 2024 16:27:08 -0400 Subject: [PATCH 05/16] get HMR working --- .../3-transform/client/transform-client.js | 33 +++++++++++++++---- .../svelte/src/internal/client/dom/css.js | 8 +++-- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index 61772317a760..ac0bbc3c2d23 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -422,10 +422,26 @@ export function client_component(source, analysis, options) { ); if (options.hmr) { - const accept_fn = b.arrow( - [b.id('module')], - b.block([b.stmt(b.call('$.set', b.id('s'), b.member(b.id('module'), b.id('default'))))]) - ); + const accept_fn_body = [ + b.stmt(b.call('$.set', b.id('s'), b.member(b.id('module'), b.id('default')))) + ]; + + if (analysis.css.hash) { + // remove existing `