diff --git a/.changeset/rich-taxis-hear.md b/.changeset/rich-taxis-hear.md new file mode 100644 index 0000000000..af6aa1e41f --- /dev/null +++ b/.changeset/rich-taxis-hear.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +feat: include CSS in `` when `css: 'injected'` 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 d8ceb45372..6685f75cf3 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,16 @@ 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 && analysis.inject_styles) { + const hash = b.literal(analysis.css.hash); + const code = b.literal(render_stylesheet(analysis.source, analysis, options).code); - append_styles(); + state.hoisted.push(b.const('$$css', b.object([b.init('hash', hash), b.init('code', code)]))); + + component_block.body.unshift( + b.stmt(b.call('$.append_styles', b.id('$$anchor'), b.id('$$css'))) + ); + } const should_inject_context = analysis.needs_context || @@ -423,10 +417,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 ``; + } + return { - head: payload.head.out || payload.head.title ? payload.head.out + payload.head.title : '', + head, 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 2f472c5c78..e6c235147b 100644 --- a/packages/svelte/src/internal/server/types.d.ts +++ b/packages/svelte/src/internal/server/types.d.ts @@ -13,6 +13,7 @@ export interface Component { export interface Payload { out: string; + css: Set<{ hash: string; code: string }>; head: { title: string; out: 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 0000000000..388d516162 --- /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 0000000000..e69de29bb2 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 0000000000..8ebe1ad73e --- /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 0000000000..4b85524295 --- /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 0000000000..194b2443f9 --- /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: { + css: 'injected' + } +}); 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 0000000000..8882c6ec7e --- /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 0000000000..8ebe1ad73e --- /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/_expected_head.html b/packages/svelte/tests/server-side-rendering/samples/css/_expected_head.html new file mode 100644 index 0000000000..7281935646 --- /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/css/main.svelte b/packages/svelte/tests/server-side-rendering/samples/css/main.svelte new file mode 100644 index 0000000000..4b85524295 --- /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/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 2597f1d2eb..27a48efa83 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'; @@ -25,7 +26,11 @@ const { test, run } = suite(async (config, test_dir) => { const rendered = render(Component, { props: config.props || {} }); 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 || '', { @@ -42,19 +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.`); + 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; diff --git a/packages/svelte/tests/snapshot/samples/hmr/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/hmr/_expected/client/index.svelte.js index 6cb6845c1c..5446bb3355 100644 --- a/packages/svelte/tests/snapshot/samples/hmr/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/hmr/_expected/client/index.svelte.js @@ -13,17 +13,17 @@ if (import.meta.hot) { const s = $.source(Hmr); const filename = Hmr.filename; + const accept = (module) => { + $.set(s, module.default); + }; + Hmr = $.hmr(s); Hmr.filename = filename; if (import.meta.hot.acceptExports) { - import.meta.hot.acceptExports(["default"], (module) => { - $.set(s, module.default); - }); + import.meta.hot.acceptExports(["default"], accept); } else { - import.meta.hot.accept((module) => { - $.set(s, module.default); - }); + import.meta.hot.accept(accept); } } diff --git a/packages/svelte/tests/sourcemaps/samples/css-injected-map/_config.js b/packages/svelte/tests/sourcemaps/samples/css-injected-map/_config.js index 845d677bc6..74a2185076 100644 --- a/packages/svelte/tests/sourcemaps/samples/css-injected-map/_config.js +++ b/packages/svelte/tests/sourcemaps/samples/css-injected-map/_config.js @@ -30,10 +30,10 @@ export default test({ async test({ assert, code_client }) { // Check that the css source map embedded in the js is accurate const match = code_client.match( - /append_styles\(\$\$anchor, "svelte-.{6}", "(.*?)(?:\\n\/\*# sourceMappingURL=data:(.*?);charset=(.*?);base64,(.*?) \*\/)?"\);/ + /code: "(.*?)(?:\\n\/\*# sourceMappingURL=data:(.*?);charset=(.*?);base64,(.*?) \*\/)?"/ ); - assert.notEqual(match, null); + assert.ok(match); const [css, mime_type, encoding, css_map_base64] = /** @type {RegExpMatchArray} */ ( match diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 85349af2c4..c5d5db2f1f 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'; @@ -2568,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'; 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 f9befc4c66..af4118eaab 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, { }); ``` +If the `css` compiler option was set to `'injected'`, `