From 9d01b8bc8e80f590110bb4ce82f77c9df395dcfa Mon Sep 17 00:00:00 2001 From: Manuel Serret Date: Sat, 27 Jun 2026 16:12:35 +0200 Subject: [PATCH] fix: preserve CSS comments when printing an AST --- .changeset/curly-dingos-smile.md | 5 ++ .../src/compiler/phases/1-parse/read/style.js | 67 ++++++++++----- .../phases/2-analyze/css/css-analyze.js | 31 +++---- .../phases/2-analyze/css/css-prune.js | 30 ++++--- .../compiler/phases/2-analyze/css/utils.js | 8 +- .../compiler/phases/3-transform/css/index.js | 34 +++++--- packages/svelte/src/compiler/phases/css.js | 4 + packages/svelte/src/compiler/print/index.js | 83 +++++++++++-------- packages/svelte/src/compiler/types/css.d.ts | 14 +++- packages/svelte/tests/css-parse.test.ts | 54 +++++++++++- .../samples/css-nth-syntax/output.json | 6 ++ .../samples/css-pseudo-classes/output.json | 18 ++++ .../print/samples/style-comments/input.svelte | 16 ++++ .../samples/style-comments/output.svelte | 18 ++++ packages/svelte/types/index.d.ts | 14 +++- 15 files changed, 300 insertions(+), 102 deletions(-) create mode 100644 .changeset/curly-dingos-smile.md create mode 100644 packages/svelte/tests/print/samples/style-comments/input.svelte create mode 100644 packages/svelte/tests/print/samples/style-comments/output.svelte diff --git a/.changeset/curly-dingos-smile.md b/.changeset/curly-dingos-smile.md new file mode 100644 index 0000000000..833d1f296a --- /dev/null +++ b/.changeset/curly-dingos-smile.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: preserve CSS comments when printing an AST diff --git a/packages/svelte/src/compiler/phases/1-parse/read/style.js b/packages/svelte/src/compiler/phases/1-parse/read/style.js index 160e5da277..1fd12d474b 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/style.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/style.js @@ -48,13 +48,17 @@ export default function read_style(parser, start, attributes) { /** * @param {Parser} parser * @param {(parser: Parser) => boolean} finished - * @returns {Array} + * @returns {Array} */ function read_body(parser, finished) { - /** @type {Array} */ + /** @type {Array} */ const children = []; - while ((allow_comment_or_whitespace(parser), !finished(parser))) { + while (parser.index < parser.template.length) { + children.push(...read_comments_and_whitespace(parser)); + + if (finished(parser)) break; + if (parser.match('@')) { children.push(read_at_rule(parser)); } else { @@ -93,7 +97,8 @@ function read_at_rule(parser) { start, end: parser.index, name, - prelude, + prelude: prelude.value, + ...(prelude.raw && { raw: prelude.raw }), block }; } @@ -126,10 +131,10 @@ function read_rule(parser) { * @returns {AST.CSS.SelectorList} */ function read_selector_list(parser, inside_pseudo_class = false) { - /** @type {AST.CSS.ComplexSelector[]} */ + /** @type {Array} */ const children = []; - allow_comment_or_whitespace(parser); + children.push(...read_comments_and_whitespace(parser)); const start = parser.index; @@ -138,7 +143,7 @@ function read_selector_list(parser, inside_pseudo_class = false) { const end = parser.index; - allow_comment_or_whitespace(parser); + children.push(...read_comments_and_whitespace(parser)); if (inside_pseudo_class ? parser.match(')') : parser.match('{')) { return { @@ -149,7 +154,7 @@ function read_selector_list(parser, inside_pseudo_class = false) { }; } else { parser.eat(',', true); - allow_comment_or_whitespace(parser); + children.push(...read_comments_and_whitespace(parser)); } } @@ -323,7 +328,7 @@ function read_selector(parser, inside_pseudo_class = false) { } const index = parser.index; - allow_comment_or_whitespace(parser); + read_comments_and_whitespace(parser); if (parser.match(',') || (inside_pseudo_class ? parser.match(')') : parser.match('{'))) { // rewind, so we know whether to continue building the selector list @@ -412,11 +417,11 @@ function read_block(parser) { parser.eat('{', true); - /** @type {Array} */ + /** @type {Array} */ const children = []; while (parser.index < parser.template.length) { - allow_comment_or_whitespace(parser); + children.push(...read_comments_and_whitespace(parser)); if (parser.match('}')) { break; @@ -471,7 +476,7 @@ function read_declaration(parser) { const value = read_value(parser); - if (!value && !property.startsWith('--')) { + if (!value.value && !property.startsWith('--')) { e.css_empty_declaration({ start, end: index }); } @@ -486,16 +491,19 @@ function read_declaration(parser) { start, end, property, - value + value: value.value, + ...(value.raw && { raw: value.raw }) }; } /** * @param {Parser} parser - * @returns {string} + * @returns {{ value: string; raw: string | null }} */ function read_value(parser) { + const start = parser.index; let value = ''; + let has_comment = false; let escaped = false; let in_url = false; @@ -523,13 +531,19 @@ function read_value(parser) { } else if (char === '(' && value.slice(-3) === 'url') { in_url = true; } else if ((char === ';' || char === '{' || char === '}') && !in_url && !quote_mark) { - return value.trim(); + const normalized = value.trim(); + + return { + value: normalized, + raw: has_comment ? parser.template.slice(start, parser.index).trim() : null + }; } else if ( char === '/' && !in_url && !quote_mark && parser.template[parser.index + 1] === '*' ) { + has_comment = true; parser.index += 2; while (parser.index < parser.template.length) { if (parser.template[parser.index] === '*' && parser.template[parser.index + 1] === '/') { @@ -624,13 +638,26 @@ function read_identifier(parser) { return identifier; } -/** @param {Parser} parser */ -function allow_comment_or_whitespace(parser) { +/** + * @param {Parser} parser + * @returns {AST.CSS.CSSComment[]} + */ +function read_comments_and_whitespace(parser) { + /** @type {AST.CSS.CSSComment[]} */ + const comments = []; + parser.allow_whitespace(); while (parser.match('/*') || parser.match('