From b97060c5c5a2e101352cb592e42db9e31839c35b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 14 Feb 2024 12:19:53 -0500 Subject: [PATCH 01/29] fix type --- packages/svelte/src/compiler/css/types.d.ts | 2 +- packages/svelte/src/compiler/phases/1-parse/read/style.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/compiler/css/types.d.ts b/packages/svelte/src/compiler/css/types.d.ts index f275e114ba96..6cad42aa3985 100644 --- a/packages/svelte/src/compiler/css/types.d.ts +++ b/packages/svelte/src/compiler/css/types.d.ts @@ -24,7 +24,7 @@ export interface SelectorList extends BaseNode { } export interface ComplexSelector extends BaseNode { - type: 'Selector'; + type: 'ComplexSelector'; children: Array; } 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 8f30b5516241..960d8b73037e 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/style.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/style.js @@ -333,7 +333,7 @@ function read_selector(parser, inside_pseudo_class = false) { parser.index = index; return { - type: 'Selector', + type: 'ComplexSelector', start: list_start, end: index, children From a25753cf9351649ef39eefe5fae03b14ed20f7a9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 14 Feb 2024 13:34:59 -0500 Subject: [PATCH 02/29] parse selectors properly the first time --- packages/svelte/src/compiler/css/Selector.js | 23 ++-- packages/svelte/src/compiler/css/types.d.ts | 8 +- .../src/compiler/phases/1-parse/read/style.js | 127 +++++++++++++----- 3 files changed, 110 insertions(+), 48 deletions(-) diff --git a/packages/svelte/src/compiler/css/Selector.js b/packages/svelte/src/compiler/css/Selector.js index dc5360c893eb..2bbadf727132 100644 --- a/packages/svelte/src/compiler/css/Selector.js +++ b/packages/svelte/src/compiler/css/Selector.js @@ -188,8 +188,8 @@ export class ComplexSelector { if (selector.type === 'PseudoClassSelector' && selector.name === 'global') { const child = selector.args?.children[0].children[0]; if ( - child?.type === 'TypeSelector' && - !/[.:#]/.test(child.name[0]) && + child?.selectors[0].type === 'TypeSelector' && + !/[.:#]/.test(child.selectors[0].name[0]) && (i !== 0 || relative_selector.selectors .slice(1) @@ -861,18 +861,13 @@ class RelativeSelector { } } -/** @param {import('#compiler').Css.ComplexSelector} selector */ -function group_selectors(selector) { - let relative_selector = new RelativeSelector(null); - const relative_selectors = [relative_selector]; - - selector.children.forEach((child) => { - if (child.type === 'Combinator') { - relative_selector = new RelativeSelector(child); - relative_selectors.push(relative_selector); - } else { - relative_selector.add(child); +/** @param {import('#compiler').Css.ComplexSelector} complex_selector */ +function group_selectors(complex_selector) { + return complex_selector.children.map((node) => { + const relative_selector = new RelativeSelector(node.combinator); + for (const selector of node.selectors) { + relative_selector.add(selector); } + return relative_selector; }); - return relative_selectors; } diff --git a/packages/svelte/src/compiler/css/types.d.ts b/packages/svelte/src/compiler/css/types.d.ts index 6cad42aa3985..6b0760292e38 100644 --- a/packages/svelte/src/compiler/css/types.d.ts +++ b/packages/svelte/src/compiler/css/types.d.ts @@ -25,7 +25,13 @@ export interface SelectorList extends BaseNode { export interface ComplexSelector extends BaseNode { type: 'ComplexSelector'; - children: Array; + children: RelativeSelector[]; +} + +export interface RelativeSelector extends BaseNode { + type: 'RelativeSelector'; + combinator: null | Combinator; + selectors: SimpleSelector[]; } export interface TypeSelector extends BaseNode { 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 960d8b73037e..b5ea70d00ce0 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/style.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/style.js @@ -187,42 +187,76 @@ function read_selector_list(parser, inside_pseudo_class = false) { function read_selector(parser, inside_pseudo_class = false) { const list_start = parser.index; - /** @type {Array} */ + /** @type {import('#compiler').Css.RelativeSelector[]} */ const children = []; + /** @type {import('#compiler').Css.RelativeSelector} */ + let relative_selector = { + type: 'RelativeSelector', + combinator: null, + selectors: [], + start: parser.index, + end: -1 + }; + while (parser.index < parser.template.length) { - const start = parser.index; + let start = parser.index; + + const combinator = read_combinator(parser); + + if (combinator) { + if (relative_selector.selectors.length === 0) { + error(start, 'TODO', 'expected a selector'); + } + + // flush previous relative selector... + relative_selector.end = start; + children.push(relative_selector); + + // ...and start a new one + relative_selector = { + type: 'RelativeSelector', + combinator, + selectors: [], + start, + end: -1 + }; + + parser.allow_whitespace(); + } + + start = parser.index; if (parser.eat('*')) { let name = '*'; - if (parser.match('|')) { + + if (parser.eat('|')) { // * is the namespace (which we ignore) - parser.index++; name = read_identifier(parser); } - children.push({ + relative_selector.selectors.push({ type: 'TypeSelector', name, start, end: parser.index }); } else if (parser.eat('#')) { - children.push({ + relative_selector.selectors.push({ type: 'IdSelector', name: read_identifier(parser), start, end: parser.index }); } else if (parser.eat('.')) { - children.push({ + relative_selector.selectors.push({ type: 'ClassSelector', name: read_identifier(parser), start, end: parser.index }); } else if (parser.eat('::')) { - children.push({ + relative_selector.selectors.push({ type: 'PseudoElementSelector', name: read_identifier(parser), start, @@ -247,7 +281,7 @@ function read_selector(parser, inside_pseudo_class = false) { error(parser.index, 'invalid-css-global-selector'); } - children.push({ + relative_selector.selectors.push({ type: 'PseudoClassSelector', name, args, @@ -276,7 +310,7 @@ function read_selector(parser, inside_pseudo_class = false) { parser.allow_whitespace(); parser.eat(']', true); - children.push({ + relative_selector.selectors.push({ type: 'AttributeSelector', start, end: parser.index, @@ -288,24 +322,14 @@ function read_selector(parser, inside_pseudo_class = false) { } else if (inside_pseudo_class && parser.match_regex(REGEX_NTH_OF)) { // nth of matcher must come before combinator matcher to prevent collision else the '+' in '+2n-1' would be parsed as a combinator - children.push({ + relative_selector.selectors.push({ type: 'Nth', value: /**@type {string} */ (parser.read(REGEX_NTH_OF)), start, end: parser.index }); - } else if (parser.match_regex(REGEX_COMBINATOR_WHITESPACE)) { - parser.allow_whitespace(); - const start = parser.index; - children.push({ - type: 'Combinator', - name: /** @type {string} */ (parser.read(REGEX_COMBINATOR)), - start, - end: parser.index - }); - parser.allow_whitespace(); } else if (parser.match_regex(REGEX_PERCENTAGE)) { - children.push({ + relative_selector.selectors.push({ type: 'Percentage', value: /** @type {string} */ (parser.read(REGEX_PERCENTAGE)), start, @@ -313,12 +337,13 @@ function read_selector(parser, inside_pseudo_class = false) { }); } else { let name = read_identifier(parser); - if (parser.match('|')) { + + if (parser.eat('|')) { // we ignore the namespace when trying to find matching element classes - parser.index++; name = read_identifier(parser); } - children.push({ + + relative_selector.selectors.push({ type: 'TypeSelector', name, start, @@ -332,6 +357,14 @@ function read_selector(parser, inside_pseudo_class = false) { if (parser.match(',') || (inside_pseudo_class ? parser.match(')') : parser.match('{'))) { parser.index = index; + if (relative_selector.selectors.length === 0) { + error(index, 'TODO', 'expected a selector'); + } + + // flush + relative_selector.end = index; + children.push(relative_selector); + return { type: 'ComplexSelector', start: list_start, @@ -340,19 +373,47 @@ function read_selector(parser, inside_pseudo_class = false) { }; } - if (parser.index !== index && !parser.match_regex(REGEX_COMBINATOR)) { - children.push({ - type: 'Combinator', - name: ' ', - start: index, - end: parser.index - }); - } + parser.index = index; } error(parser.template.length, 'unexpected-eof'); } +/** + * @param {import('../index.js').Parser} parser + * @returns {import('#compiler').Css.Combinator | null} + */ +function read_combinator(parser) { + const start = parser.index; + parser.allow_whitespace(); + + const index = parser.index; + const name = parser.read(REGEX_COMBINATOR); + + if (name) { + const end = parser.index; + parser.allow_whitespace(); + + return { + type: 'Combinator', + name, + start: index, + end + }; + } + + if (parser.index !== start) { + return { + type: 'Combinator', + name: ' ', + start, + end: parser.index + }; + } + + return null; +} + /** * @param {import('../index.js').Parser} parser * @returns {import('#compiler').Css.Block} From 7c84a836509cb3336a3d6fe3c93343fb425adcff Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 14 Feb 2024 14:07:27 -0500 Subject: [PATCH 03/29] partial fix --- .../src/compiler/phases/1-parse/read/style.js | 14 ++++++++------ .../css-invalid-combinator-selector-1/errors.json | 10 ++++++++-- .../css-invalid-combinator-selector-2/errors.json | 10 ++++++++-- .../css-invalid-combinator-selector-3/errors.json | 10 ++++++++-- 4 files changed, 32 insertions(+), 12 deletions(-) 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 b5ea70d00ce0..9418ec9ecc71 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/style.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/style.js @@ -206,13 +206,15 @@ function read_selector(parser, inside_pseudo_class = false) { if (combinator) { if (relative_selector.selectors.length === 0) { - error(start, 'TODO', 'expected a selector'); + if (!inside_pseudo_class) { + error(start, 'invalid-css-selector'); + } + } else { + // flush previous relative selector... + relative_selector.end = start; + children.push(relative_selector); } - // flush previous relative selector... - relative_selector.end = start; - children.push(relative_selector); - // ...and start a new one relative_selector = { type: 'RelativeSelector', @@ -358,7 +360,7 @@ function read_selector(parser, inside_pseudo_class = false) { parser.index = index; if (relative_selector.selectors.length === 0) { - error(index, 'TODO', 'expected a selector'); + error(index, 'invalid-css-selector'); } // flush diff --git a/packages/svelte/tests/validator/samples/css-invalid-combinator-selector-1/errors.json b/packages/svelte/tests/validator/samples/css-invalid-combinator-selector-1/errors.json index fc90941b1cab..d49bf7437a31 100644 --- a/packages/svelte/tests/validator/samples/css-invalid-combinator-selector-1/errors.json +++ b/packages/svelte/tests/validator/samples/css-invalid-combinator-selector-1/errors.json @@ -2,7 +2,13 @@ { "code": "invalid-css-selector", "message": "Invalid selector", - "start": { "line": 10, "column": 1 }, - "end": { "line": 10, "column": 7 } + "start": { + "line": 10, + "column": 1 + }, + "end": { + "line": 10, + "column": 1 + } } ] diff --git a/packages/svelte/tests/validator/samples/css-invalid-combinator-selector-2/errors.json b/packages/svelte/tests/validator/samples/css-invalid-combinator-selector-2/errors.json index aae67dc75a05..2fdf3e96d1b8 100644 --- a/packages/svelte/tests/validator/samples/css-invalid-combinator-selector-2/errors.json +++ b/packages/svelte/tests/validator/samples/css-invalid-combinator-selector-2/errors.json @@ -2,7 +2,13 @@ { "code": "invalid-css-selector", "message": "Invalid selector", - "start": { "line": 8, "column": 1 }, - "end": { "line": 8, "column": 4 } + "start": { + "line": 8, + "column": 1 + }, + "end": { + "line": 8, + "column": 1 + } } ] diff --git a/packages/svelte/tests/validator/samples/css-invalid-combinator-selector-3/errors.json b/packages/svelte/tests/validator/samples/css-invalid-combinator-selector-3/errors.json index 9f3d06660c2b..b355260623ed 100644 --- a/packages/svelte/tests/validator/samples/css-invalid-combinator-selector-3/errors.json +++ b/packages/svelte/tests/validator/samples/css-invalid-combinator-selector-3/errors.json @@ -2,7 +2,13 @@ { "code": "invalid-css-selector", "message": "Invalid selector", - "start": { "line": 5, "column": 2 }, - "end": { "line": 5, "column": 8 } + "start": { + "line": 5, + "column": 2 + }, + "end": { + "line": 5, + "column": 2 + } } ] From 07df4d3d13e55179e5686753702356b6bd0cbdca Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 14 Feb 2024 14:14:24 -0500 Subject: [PATCH 04/29] fix --- .../svelte/src/compiler/phases/1-parse/read/style.js | 9 +++++---- .../css-invalid-combinator-selector-4/errors.json | 10 ++++++++-- 2 files changed, 13 insertions(+), 6 deletions(-) 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 9418ec9ecc71..d024d298d032 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/style.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/style.js @@ -225,6 +225,10 @@ function read_selector(parser, inside_pseudo_class = false) { }; parser.allow_whitespace(); + + if (parser.match(',') || (inside_pseudo_class ? parser.match(')') : parser.match('{'))) { + error(parser.index, 'invalid-css-selector'); + } } start = parser.index; @@ -357,12 +361,9 @@ function read_selector(parser, inside_pseudo_class = false) { parser.allow_whitespace(); if (parser.match(',') || (inside_pseudo_class ? parser.match(')') : parser.match('{'))) { + // rewind, so we know whether to continue building the selector list parser.index = index; - if (relative_selector.selectors.length === 0) { - error(index, 'invalid-css-selector'); - } - // flush relative_selector.end = index; children.push(relative_selector); diff --git a/packages/svelte/tests/validator/samples/css-invalid-combinator-selector-4/errors.json b/packages/svelte/tests/validator/samples/css-invalid-combinator-selector-4/errors.json index d7c9cfeb2e30..aab0a130c7ba 100644 --- a/packages/svelte/tests/validator/samples/css-invalid-combinator-selector-4/errors.json +++ b/packages/svelte/tests/validator/samples/css-invalid-combinator-selector-4/errors.json @@ -2,7 +2,13 @@ { "code": "invalid-css-selector", "message": "Invalid selector", - "start": { "line": 4, "column": 1 }, - "end": { "line": 4, "column": 5 } + "start": { + "line": 4, + "column": 5 + }, + "end": { + "line": 4, + "column": 5 + } } ] From aedc4cbd0335edbe6be1397d87b6393c13368091 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 14 Feb 2024 14:53:25 -0500 Subject: [PATCH 05/29] start moving CSS validation into analysis phase --- packages/svelte/src/compiler/css/Selector.js | 17 --------- packages/svelte/src/compiler/css/types.d.ts | 10 +++++- .../src/compiler/phases/2-analyze/index.js | 5 +++ .../phases/2-analyze/validation/css.js | 36 +++++++++++++++++++ .../svelte/src/compiler/types/template.d.ts | 5 ++- 5 files changed, 52 insertions(+), 21 deletions(-) create mode 100644 packages/svelte/src/compiler/phases/2-analyze/validation/css.js diff --git a/packages/svelte/src/compiler/css/Selector.js b/packages/svelte/src/compiler/css/Selector.js index 2bbadf727132..50c63115888a 100644 --- a/packages/svelte/src/compiler/css/Selector.js +++ b/packages/svelte/src/compiler/css/Selector.js @@ -127,28 +127,11 @@ export class ComplexSelector { /** @param {import('../phases/types.js').ComponentAnalysis} analysis */ validate(analysis) { - this.validate_global_placement(); this.validate_global_with_multiple_selectors(); this.validate_global_compound_selector(); this.validate_invalid_combinator_without_selector(analysis); } - validate_global_placement() { - let start = 0; - let end = this.relative_selectors.length; - for (; start < end; start += 1) { - if (!this.relative_selectors[start].is_global) break; - } - for (; end > start; end -= 1) { - if (!this.relative_selectors[end - 1].is_global) break; - } - for (let i = start; i < end; i += 1) { - if (this.relative_selectors[i].is_global) { - error(this.relative_selectors[i].selectors[0], 'invalid-css-global-placement'); - } - } - } - validate_global_with_multiple_selectors() { if (this.relative_selectors.length === 1 && this.relative_selectors[0].selectors.length === 1) { // standalone :global() with multiple selectors is OK diff --git a/packages/svelte/src/compiler/css/types.d.ts b/packages/svelte/src/compiler/css/types.d.ts index 6b0760292e38..2a4d510ea17a 100644 --- a/packages/svelte/src/compiler/css/types.d.ts +++ b/packages/svelte/src/compiler/css/types.d.ts @@ -105,4 +105,12 @@ export interface Declaration extends BaseNode { } // for zimmerframe -export type Node = Style | Rule | Atrule | Declaration; +export type Node = + | Style + | Rule + | Atrule + | ComplexSelector + | RelativeSelector + | Combinator + | SimpleSelector + | Declaration; diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 7ecde4b001b9..49b43bb647a8 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -21,6 +21,7 @@ import { regex_starts_with_newline } from '../patterns.js'; import { create_attribute, is_element_node } from '../nodes.js'; import { DelegatedEvents, namespace_svg } from '../../../constants.js'; import { should_proxy_or_freeze } from '../3-transform/client/utils.js'; +import { validation_css } from './validation/css.js'; /** * @param {import('#compiler').Script | null} script @@ -452,6 +453,10 @@ export function analyze_component(root, options) { } } + if (root.css) { + walk(root.css, {}, validation_css); + } + analysis.stylesheet.validate(analysis); for (const element of analysis.elements) { diff --git a/packages/svelte/src/compiler/phases/2-analyze/validation/css.js b/packages/svelte/src/compiler/phases/2-analyze/validation/css.js new file mode 100644 index 000000000000..92f70dcc1bf1 --- /dev/null +++ b/packages/svelte/src/compiler/phases/2-analyze/validation/css.js @@ -0,0 +1,36 @@ +import { error } from '../../../errors.js'; + +/** @param {import('#compiler').Css.RelativeSelector} relative_selector */ +function is_global(relative_selector) { + const first = relative_selector.selectors[0]; + + return ( + first.type === 'PseudoClassSelector' && + first.name === 'global' && + relative_selector.selectors.every( + (selector) => + selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector' + ) + ); +} + +/** + * @type {import('zimmerframe').Visitors} + */ +export const validation_css = { + ComplexSelector(node, context) { + // ensure `:global(...)` is not used in the middle of a selector + { + const a = node.children.findIndex((child) => !is_global(child)); + const b = node.children.findLastIndex((child) => !is_global(child)); + + if (a !== b) { + for (let i = a; i <= b; i += 1) { + if (is_global(node.children[i])) { + error(node.children[i].selectors[0], 'invalid-css-global-placement'); + } + } + } + } + } +}; diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index 26b6ee7ca2d9..224dc3742994 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -1,8 +1,7 @@ -import type { Binding } from '#compiler'; +import type { Binding, Css } from '#compiler'; import type { ArrayExpression, ArrowFunctionExpression, - ArrayPattern, VariableDeclaration, VariableDeclarator, Expression, @@ -460,7 +459,7 @@ export type TemplateNode = | Comment | Block; -export type SvelteNode = Node | TemplateNode | Fragment; +export type SvelteNode = Node | TemplateNode | Fragment | Css.Node; export interface Script extends BaseNode { type: 'Script'; From 0d0cc80cec14201e168ed47847801dba32cfa34c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 14 Feb 2024 15:23:25 -0500 Subject: [PATCH 06/29] finish moving validation --- packages/svelte/src/compiler/css/Selector.js | 62 ------------------- .../svelte/src/compiler/css/Stylesheet.js | 21 ------- .../src/compiler/phases/2-analyze/index.js | 2 - .../phases/2-analyze/validation/css.js | 40 ++++++++++++ 4 files changed, 40 insertions(+), 85 deletions(-) diff --git a/packages/svelte/src/compiler/css/Selector.js b/packages/svelte/src/compiler/css/Selector.js index 50c63115888a..e10b3ac816c6 100644 --- a/packages/svelte/src/compiler/css/Selector.js +++ b/packages/svelte/src/compiler/css/Selector.js @@ -124,68 +124,6 @@ export class ComplexSelector { } } } - - /** @param {import('../phases/types.js').ComponentAnalysis} analysis */ - validate(analysis) { - this.validate_global_with_multiple_selectors(); - this.validate_global_compound_selector(); - this.validate_invalid_combinator_without_selector(analysis); - } - - validate_global_with_multiple_selectors() { - if (this.relative_selectors.length === 1 && this.relative_selectors[0].selectors.length === 1) { - // standalone :global() with multiple selectors is OK - return; - } - for (const relative_selector of this.relative_selectors) { - for (const selector of relative_selector.selectors) { - if ( - selector.type === 'PseudoClassSelector' && - selector.name === 'global' && - selector.args !== null && - selector.args.children.length > 1 - ) { - error(selector, 'invalid-css-global-selector'); - } - } - } - } - - /** @param {import('../phases/types.js').ComponentAnalysis} analysis */ - validate_invalid_combinator_without_selector(analysis) { - for (let i = 0; i < this.relative_selectors.length; i++) { - const relative_selector = this.relative_selectors[i]; - if (relative_selector.selectors.length === 0) { - error(this.node, 'invalid-css-selector'); - } - } - } - - validate_global_compound_selector() { - for (const relative_selector of this.relative_selectors) { - if (relative_selector.selectors.length === 1) continue; - - for (let i = 0; i < relative_selector.selectors.length; i++) { - const selector = relative_selector.selectors[i]; - - if (selector.type === 'PseudoClassSelector' && selector.name === 'global') { - const child = selector.args?.children[0].children[0]; - if ( - child?.selectors[0].type === 'TypeSelector' && - !/[.:#]/.test(child.selectors[0].name[0]) && - (i !== 0 || - relative_selector.selectors - .slice(1) - .some( - (s) => s.type !== 'PseudoElementSelector' && s.type !== 'PseudoClassSelector' - )) - ) { - error(selector, 'invalid-css-global-selector-list'); - } - } - } - } - } } /** diff --git a/packages/svelte/src/compiler/css/Stylesheet.js b/packages/svelte/src/compiler/css/Stylesheet.js index e632a3692e7c..84c729c5f764 100644 --- a/packages/svelte/src/compiler/css/Stylesheet.js +++ b/packages/svelte/src/compiler/css/Stylesheet.js @@ -115,13 +115,6 @@ class Rule { this.declarations.forEach((declaration) => declaration.transform(code, keyframes)); } - /** @param {import('../phases/types.js').ComponentAnalysis} analysis */ - validate(analysis) { - this.selectors.forEach((selector) => { - selector.validate(analysis); - }); - } - /** @param {(selector: ComplexSelector) => void} handler */ warn_on_unused_selector(handler) { this.selectors.forEach((selector) => { @@ -309,13 +302,6 @@ class Atrule { }); } - /** @param {import('../phases/types.js').ComponentAnalysis} analysis */ - validate(analysis) { - this.children.forEach((child) => { - child.validate(analysis); - }); - } - /** @param {(selector: ComplexSelector) => void} handler */ warn_on_unused_selector(handler) { if (this.node.name !== 'media') return; @@ -510,13 +496,6 @@ export class Stylesheet { }; } - /** @param {import('../phases/types.js').ComponentAnalysis} analysis */ - validate(analysis) { - this.children.forEach((child) => { - child.validate(analysis); - }); - } - /** @param {import('../phases/types.js').ComponentAnalysis} analysis */ warn_on_unused_selectors(analysis) { // const ignores = !this.ast diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 49b43bb647a8..0b5127d2fa9b 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -457,8 +457,6 @@ export function analyze_component(root, options) { walk(root.css, {}, validation_css); } - analysis.stylesheet.validate(analysis); - for (const element of analysis.elements) { analysis.stylesheet.apply(element); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/validation/css.js b/packages/svelte/src/compiler/phases/2-analyze/validation/css.js index 92f70dcc1bf1..cda15a3576a0 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/validation/css.js +++ b/packages/svelte/src/compiler/phases/2-analyze/validation/css.js @@ -32,5 +32,45 @@ export const validation_css = { } } } + + // ensure `:global(...)`contains a single selector + // (standalone :global() with multiple selectors is OK) + if (node.children.length > 1 || node.children[0].selectors.length > 1) { + for (const relative_selector of node.children) { + for (const selector of relative_selector.selectors) { + if ( + selector.type === 'PseudoClassSelector' && + selector.name === 'global' && + selector.args !== null && + selector.args.children.length > 1 + ) { + error(selector, 'invalid-css-global-selector'); + } + } + } + } + + // ensure `:global(...)` is not part of a larger compound selector + for (const relative_selector of node.children) { + for (let i = 0; i < relative_selector.selectors.length; i++) { + const selector = relative_selector.selectors[i]; + + if (selector.type === 'PseudoClassSelector' && selector.name === 'global') { + const child = selector.args?.children[0].children[0]; + if ( + child?.selectors[0].type === 'TypeSelector' && + !/[.:#]/.test(child.selectors[0].name[0]) && + (i !== 0 || + relative_selector.selectors + .slice(1) + .some( + (s) => s.type !== 'PseudoElementSelector' && s.type !== 'PseudoClassSelector' + )) + ) { + error(selector, 'invalid-css-global-selector-list'); + } + } + } + } } }; From ab60b6a0455cf697ef6a4a39815273efa7ece9e9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 14 Feb 2024 18:35:14 -0500 Subject: [PATCH 07/29] fix tests --- packages/svelte/src/compiler/legacy.js | 27 +- .../src/compiler/types/legacy-nodes.d.ts | 10 +- .../samples/css-nth-syntax/output.json | 840 +++++++++++------- .../samples/css-pseudo-classes/output.json | 235 +++-- .../semicolon-inside-quotes/output.json | 14 +- 5 files changed, 725 insertions(+), 401 deletions(-) diff --git a/packages/svelte/src/compiler/legacy.js b/packages/svelte/src/compiler/legacy.js index 363742e7b1ac..ea463281858a 100644 --- a/packages/svelte/src/compiler/legacy.js +++ b/packages/svelte/src/compiler/legacy.js @@ -102,14 +102,7 @@ export function convert(source, ast) { }, instance, module, - css: ast.css - ? walk(ast.css, null, { - _(node) { - // @ts-ignore - delete node.parent; - } - }) - : undefined + css: ast.css ? visit(ast.css) : undefined }; }, AnimateDirective(node) { @@ -192,6 +185,24 @@ export function convert(source, ast) { ClassDirective(node) { return { ...node, type: 'Class' }; }, + ComplexSelector(node, { visit }) { + const children = []; + + for (const child of node.children) { + if (child.combinator) { + children.push(child.combinator); + } + + children.push(...child.selectors); + } + + return { + type: 'Selector', + start: node.start, + end: node.end, + children + }; + }, Component(node, { visit }) { return { type: 'InlineComponent', diff --git a/packages/svelte/src/compiler/types/legacy-nodes.d.ts b/packages/svelte/src/compiler/types/legacy-nodes.d.ts index ba89520e2092..15f1d4a6fc2f 100644 --- a/packages/svelte/src/compiler/types/legacy-nodes.d.ts +++ b/packages/svelte/src/compiler/types/legacy-nodes.d.ts @@ -1,4 +1,4 @@ -import type { StyleDirective as LegacyStyleDirective, Text } from '#compiler'; +import type { StyleDirective as LegacyStyleDirective, Text, Css } from '#compiler'; import type { ArrayExpression, AssignmentExpression, @@ -227,9 +227,17 @@ export type LegacyElementLike = | LegacyTitle | LegacyWindow; +export interface LegacySelector extends BaseNode { + type: 'Selector'; + children: Array; +} + +export type LegacyCssNode = LegacySelector; + export type LegacySvelteNode = | LegacyConstTag | LegacyElementLike | LegacyAttributeLike | LegacyAttributeShorthand + | LegacyCssNode | Text; diff --git a/packages/svelte/tests/parser-modern/samples/css-nth-syntax/output.json b/packages/svelte/tests/parser-modern/samples/css-nth-syntax/output.json index 9e13a7cb8646..c00247c3b5ce 100644 --- a/packages/svelte/tests/parser-modern/samples/css-nth-syntax/output.json +++ b/packages/svelte/tests/parser-modern/samples/css-nth-syntax/output.json @@ -13,40 +13,56 @@ "end": 80, "children": [ { - "type": "Selector", + "type": "ComplexSelector", "start": 60, "end": 80, "children": [ { - "type": "TypeSelector", - "name": "h1", - "start": 60, - "end": 62 - }, - { - "type": "PseudoClassSelector", - "name": "nth-of-type", - "args": { - "type": "SelectorList", - "start": 75, - "end": 79, - "children": [ - { - "type": "Selector", + "type": "RelativeSelector", + "combinator": null, + "selectors": [ + { + "type": "TypeSelector", + "name": "h1", + "start": 60, + "end": 62 + }, + { + "type": "PseudoClassSelector", + "name": "nth-of-type", + "args": { + "type": "SelectorList", "start": 75, "end": 79, "children": [ { - "type": "Nth", - "value": "2n+1", + "type": "ComplexSelector", "start": 75, - "end": 79 + "end": 79, + "children": [ + { + "type": "RelativeSelector", + "combinator": null, + "selectors": [ + { + "type": "Nth", + "value": "2n+1", + "start": 75, + "end": 79 + } + ], + "start": 75, + "end": 79 + } + ] } ] - } - ] - }, - "start": 62, + }, + "start": 62, + "end": 80 + } + ], + "start": 60, "end": 80 } ] @@ -78,52 +94,68 @@ "end": 153, "children": [ { - "type": "Selector", + "type": "ComplexSelector", "start": 117, "end": 153, "children": [ { - "type": "TypeSelector", - "name": "h1", - "start": 117, - "end": 119 - }, - { - "type": "PseudoClassSelector", - "name": "nth-child", - "args": { - "type": "SelectorList", - "start": 130, - "end": 152, - "children": [ - { - "type": "Selector", + "type": "RelativeSelector", + "combinator": null, + "selectors": [ + { + "type": "TypeSelector", + "name": "h1", + "start": 117, + "end": 119 + }, + { + "type": "PseudoClassSelector", + "name": "nth-child", + "args": { + "type": "SelectorList", "start": 130, "end": 152, "children": [ { - "type": "Nth", - "value": "-n + 3 of ", + "type": "ComplexSelector", "start": 130, - "end": 140 - }, - { - "type": "TypeSelector", - "name": "li", - "start": 140, - "end": 142 - }, - { - "type": "ClassSelector", - "name": "important", - "start": 142, - "end": 152 + "end": 152, + "children": [ + { + "type": "RelativeSelector", + "combinator": null, + "selectors": [ + { + "type": "Nth", + "value": "-n + 3 of ", + "start": 130, + "end": 140 + }, + { + "type": "TypeSelector", + "name": "li", + "start": 140, + "end": 142 + }, + { + "type": "ClassSelector", + "name": "important", + "start": 142, + "end": 152 + } + ], + "start": 130, + "end": 152 + } + ] } ] - } - ] - }, - "start": 119, + }, + "start": 119, + "end": 153 + } + ], + "start": 117, "end": 153 } ] @@ -155,40 +187,56 @@ "end": 206, "children": [ { - "type": "Selector", + "type": "ComplexSelector", "start": 191, "end": 206, "children": [ { - "type": "TypeSelector", - "name": "h1", - "start": 191, - "end": 193 - }, - { - "type": "PseudoClassSelector", - "name": "nth-child", - "args": { - "type": "SelectorList", - "start": 204, - "end": 205, - "children": [ - { - "type": "Selector", + "type": "RelativeSelector", + "combinator": null, + "selectors": [ + { + "type": "TypeSelector", + "name": "h1", + "start": 191, + "end": 193 + }, + { + "type": "PseudoClassSelector", + "name": "nth-child", + "args": { + "type": "SelectorList", "start": 204, "end": 205, "children": [ { - "type": "Nth", - "value": "1", + "type": "ComplexSelector", "start": 204, - "end": 205 + "end": 205, + "children": [ + { + "type": "RelativeSelector", + "combinator": null, + "selectors": [ + { + "type": "Nth", + "value": "1", + "start": 204, + "end": 205 + } + ], + "start": 204, + "end": 205 + } + ] } ] - } - ] - }, - "start": 193, + }, + "start": 193, + "end": 206 + } + ], + "start": 191, "end": 206 } ] @@ -220,40 +268,56 @@ "end": 259, "children": [ { - "type": "Selector", + "type": "ComplexSelector", "start": 244, "end": 259, "children": [ { - "type": "TypeSelector", - "name": "h1", - "start": 244, - "end": 246 - }, - { - "type": "PseudoClassSelector", - "name": "nth-child", - "args": { - "type": "SelectorList", - "start": 257, - "end": 258, - "children": [ - { - "type": "Selector", + "type": "RelativeSelector", + "combinator": null, + "selectors": [ + { + "type": "TypeSelector", + "name": "h1", + "start": 244, + "end": 246 + }, + { + "type": "PseudoClassSelector", + "name": "nth-child", + "args": { + "type": "SelectorList", "start": 257, "end": 258, "children": [ { - "type": "TypeSelector", - "name": "p", + "type": "ComplexSelector", "start": 257, - "end": 258 + "end": 258, + "children": [ + { + "type": "RelativeSelector", + "combinator": null, + "selectors": [ + { + "type": "TypeSelector", + "name": "p", + "start": 257, + "end": 258 + } + ], + "start": 257, + "end": 258 + } + ] } ] - } - ] - }, - "start": 246, + }, + "start": 246, + "end": 259 + } + ], + "start": 244, "end": 259 } ] @@ -285,40 +349,56 @@ "end": 314, "children": [ { - "type": "Selector", + "type": "ComplexSelector", "start": 297, "end": 314, "children": [ { - "type": "TypeSelector", - "name": "h1", - "start": 297, - "end": 299 - }, - { - "type": "PseudoClassSelector", - "name": "nth-child", - "args": { - "type": "SelectorList", - "start": 310, - "end": 313, - "children": [ - { - "type": "Selector", + "type": "RelativeSelector", + "combinator": null, + "selectors": [ + { + "type": "TypeSelector", + "name": "h1", + "start": 297, + "end": 299 + }, + { + "type": "PseudoClassSelector", + "name": "nth-child", + "args": { + "type": "SelectorList", "start": 310, "end": 313, "children": [ { - "type": "Nth", - "value": "n+7", + "type": "ComplexSelector", "start": 310, - "end": 313 + "end": 313, + "children": [ + { + "type": "RelativeSelector", + "combinator": null, + "selectors": [ + { + "type": "Nth", + "value": "n+7", + "start": 310, + "end": 313 + } + ], + "start": 310, + "end": 313 + } + ] } ] - } - ] - }, - "start": 299, + }, + "start": 299, + "end": 314 + } + ], + "start": 297, "end": 314 } ] @@ -350,40 +430,56 @@ "end": 370, "children": [ { - "type": "Selector", + "type": "ComplexSelector", "start": 352, "end": 370, "children": [ { - "type": "TypeSelector", - "name": "h1", - "start": 352, - "end": 354 - }, - { - "type": "PseudoClassSelector", - "name": "nth-child", - "args": { - "type": "SelectorList", - "start": 365, - "end": 369, - "children": [ - { - "type": "Selector", + "type": "RelativeSelector", + "combinator": null, + "selectors": [ + { + "type": "TypeSelector", + "name": "h1", + "start": 352, + "end": 354 + }, + { + "type": "PseudoClassSelector", + "name": "nth-child", + "args": { + "type": "SelectorList", "start": 365, "end": 369, "children": [ { - "type": "Nth", - "value": "even", + "type": "ComplexSelector", "start": 365, - "end": 369 + "end": 369, + "children": [ + { + "type": "RelativeSelector", + "combinator": null, + "selectors": [ + { + "type": "Nth", + "value": "even", + "start": 365, + "end": 369 + } + ], + "start": 365, + "end": 369 + } + ] } ] - } - ] - }, - "start": 354, + }, + "start": 354, + "end": 370 + } + ], + "start": 352, "end": 370 } ] @@ -415,40 +511,56 @@ "end": 425, "children": [ { - "type": "Selector", + "type": "ComplexSelector", "start": 408, "end": 425, "children": [ { - "type": "TypeSelector", - "name": "h1", - "start": 408, - "end": 410 - }, - { - "type": "PseudoClassSelector", - "name": "nth-child", - "args": { - "type": "SelectorList", - "start": 421, - "end": 424, - "children": [ - { - "type": "Selector", + "type": "RelativeSelector", + "combinator": null, + "selectors": [ + { + "type": "TypeSelector", + "name": "h1", + "start": 408, + "end": 410 + }, + { + "type": "PseudoClassSelector", + "name": "nth-child", + "args": { + "type": "SelectorList", "start": 421, "end": 424, "children": [ { - "type": "Nth", - "value": "odd", + "type": "ComplexSelector", "start": 421, - "end": 424 + "end": 424, + "children": [ + { + "type": "RelativeSelector", + "combinator": null, + "selectors": [ + { + "type": "Nth", + "value": "odd", + "start": 421, + "end": 424 + } + ], + "start": 421, + "end": 424 + } + ] } ] - } - ] - }, - "start": 410, + }, + "start": 410, + "end": 425 + } + ], + "start": 408, "end": 425 } ] @@ -480,40 +592,56 @@ "end": 492, "children": [ { - "type": "Selector", + "type": "ComplexSelector", "start": 463, "end": 492, "children": [ { - "type": "TypeSelector", - "name": "h1", - "start": 463, - "end": 465 - }, - { - "type": "PseudoClassSelector", - "name": "nth-child", - "args": { - "type": "SelectorList", - "start": 485, - "end": 486, - "children": [ - { - "type": "Selector", + "type": "RelativeSelector", + "combinator": null, + "selectors": [ + { + "type": "TypeSelector", + "name": "h1", + "start": 463, + "end": 465 + }, + { + "type": "PseudoClassSelector", + "name": "nth-child", + "args": { + "type": "SelectorList", "start": 485, "end": 486, "children": [ { - "type": "Nth", - "value": "n", + "type": "ComplexSelector", "start": 485, - "end": 486 + "end": 486, + "children": [ + { + "type": "RelativeSelector", + "combinator": null, + "selectors": [ + { + "type": "Nth", + "value": "n", + "start": 485, + "end": 486 + } + ], + "start": 485, + "end": 486 + } + ] } ] - } - ] - }, - "start": 465, + }, + "start": 465, + "end": 492 + } + ], + "start": 463, "end": 492 } ] @@ -545,40 +673,56 @@ "end": 544, "children": [ { - "type": "Selector", + "type": "ComplexSelector", "start": 530, "end": 544, "children": [ { - "type": "TypeSelector", - "name": "h1", - "start": 530, - "end": 532 - }, - { - "type": "PseudoClassSelector", - "name": "global", - "args": { - "type": "SelectorList", - "start": 540, - "end": 543, - "children": [ - { - "type": "Selector", + "type": "RelativeSelector", + "combinator": null, + "selectors": [ + { + "type": "TypeSelector", + "name": "h1", + "start": 530, + "end": 532 + }, + { + "type": "PseudoClassSelector", + "name": "global", + "args": { + "type": "SelectorList", "start": 540, "end": 543, "children": [ { - "type": "TypeSelector", - "name": "nav", + "type": "ComplexSelector", "start": 540, - "end": 543 + "end": 543, + "children": [ + { + "type": "RelativeSelector", + "combinator": null, + "selectors": [ + { + "type": "TypeSelector", + "name": "nav", + "start": 540, + "end": 543 + } + ], + "start": 540, + "end": 543 + } + ] } ] - } - ] - }, - "start": 532, + }, + "start": 532, + "end": 544 + } + ], + "start": 530, "end": 544 } ] @@ -610,40 +754,56 @@ "end": 601, "children": [ { - "type": "Selector", + "type": "ComplexSelector", "start": 580, "end": 601, "children": [ { - "type": "TypeSelector", - "name": "h1", - "start": 580, - "end": 582 - }, - { - "type": "PseudoClassSelector", - "name": "nth-of-type", - "args": { - "type": "SelectorList", - "start": 595, - "end": 600, - "children": [ - { - "type": "Selector", + "type": "RelativeSelector", + "combinator": null, + "selectors": [ + { + "type": "TypeSelector", + "name": "h1", + "start": 580, + "end": 582 + }, + { + "type": "PseudoClassSelector", + "name": "nth-of-type", + "args": { + "type": "SelectorList", "start": 595, "end": 600, "children": [ { - "type": "Nth", - "value": "10n+1", + "type": "ComplexSelector", "start": 595, - "end": 600 + "end": 600, + "children": [ + { + "type": "RelativeSelector", + "combinator": null, + "selectors": [ + { + "type": "Nth", + "value": "10n+1", + "start": 595, + "end": 600 + } + ], + "start": 595, + "end": 600 + } + ] } ] - } - ] - }, - "start": 582, + }, + "start": 582, + "end": 601 + } + ], + "start": 580, "end": 601 } ] @@ -675,40 +835,56 @@ "end": 657, "children": [ { - "type": "Selector", + "type": "ComplexSelector", "start": 636, "end": 657, "children": [ { - "type": "TypeSelector", - "name": "h1", - "start": 636, - "end": 638 - }, - { - "type": "PseudoClassSelector", - "name": "nth-of-type", - "args": { - "type": "SelectorList", - "start": 651, - "end": 656, - "children": [ - { - "type": "Selector", + "type": "RelativeSelector", + "combinator": null, + "selectors": [ + { + "type": "TypeSelector", + "name": "h1", + "start": 636, + "end": 638 + }, + { + "type": "PseudoClassSelector", + "name": "nth-of-type", + "args": { + "type": "SelectorList", "start": 651, "end": 656, "children": [ { - "type": "Nth", - "value": "-2n+3", + "type": "ComplexSelector", "start": 651, - "end": 656 + "end": 656, + "children": [ + { + "type": "RelativeSelector", + "combinator": null, + "selectors": [ + { + "type": "Nth", + "value": "-2n+3", + "start": 651, + "end": 656 + } + ], + "start": 651, + "end": 656 + } + ] } ] - } - ] - }, - "start": 638, + }, + "start": 638, + "end": 657 + } + ], + "start": 636, "end": 657 } ] @@ -740,40 +916,61 @@ "end": 711, "children": [ { - "type": "Selector", + "type": "ComplexSelector", "start": 692, "end": 711, "children": [ { - "type": "TypeSelector", - "name": "h1", - "start": 692, - "end": 694 - }, - { - "type": "PseudoClassSelector", - "name": "nth-of-type", - "args": { - "type": "SelectorList", - "start": 707, - "end": 710, - "children": [ - { - "type": "Selector", + "type": "RelativeSelector", + "combinator": null, + "selectors": [ + { + "type": "TypeSelector", + "name": "h1", + "start": 692, + "end": 694 + }, + { + "type": "PseudoClassSelector", + "name": "nth-of-type", + "args": { + "type": "SelectorList", "start": 707, "end": 710, "children": [ { - "type": "Nth", - "value": "+12", + "type": "ComplexSelector", "start": 707, - "end": 710 + "end": 710, + "children": [ + { + "type": "RelativeSelector", + "combinator": { + "type": "Combinator", + "name": "+", + "start": 707, + "end": 708 + }, + "selectors": [ + { + "type": "Nth", + "value": "12", + "start": 708, + "end": 710 + } + ], + "start": 707, + "end": 710 + } + ] } ] - } - ] - }, - "start": 694, + }, + "start": 694, + "end": 711 + } + ], + "start": 692, "end": 711 } ] @@ -805,40 +1002,61 @@ "end": 765, "children": [ { - "type": "Selector", + "type": "ComplexSelector", "start": 746, "end": 765, "children": [ { - "type": "TypeSelector", - "name": "h1", - "start": 746, - "end": 748 - }, - { - "type": "PseudoClassSelector", - "name": "nth-of-type", - "args": { - "type": "SelectorList", - "start": 761, - "end": 764, - "children": [ - { - "type": "Selector", + "type": "RelativeSelector", + "combinator": null, + "selectors": [ + { + "type": "TypeSelector", + "name": "h1", + "start": 746, + "end": 748 + }, + { + "type": "PseudoClassSelector", + "name": "nth-of-type", + "args": { + "type": "SelectorList", "start": 761, "end": 764, "children": [ { - "type": "Nth", - "value": "+3n", + "type": "ComplexSelector", "start": 761, - "end": 764 + "end": 764, + "children": [ + { + "type": "RelativeSelector", + "combinator": { + "type": "Combinator", + "name": "+", + "start": 761, + "end": 762 + }, + "selectors": [ + { + "type": "Nth", + "value": "3n", + "start": 762, + "end": 764 + } + ], + "start": 761, + "end": 764 + } + ] } ] - } - ] - }, - "start": 748, + }, + "start": 748, + "end": 765 + } + ], + "start": 746, "end": 765 } ] diff --git a/packages/svelte/tests/parser-modern/samples/css-pseudo-classes/output.json b/packages/svelte/tests/parser-modern/samples/css-pseudo-classes/output.json index 1431b123a4ee..25c5806aac68 100644 --- a/packages/svelte/tests/parser-modern/samples/css-pseudo-classes/output.json +++ b/packages/svelte/tests/parser-modern/samples/css-pseudo-classes/output.json @@ -13,15 +13,23 @@ "end": 86, "children": [ { - "type": "Selector", + "type": "ComplexSelector", "start": 60, "end": 86, "children": [ { - "type": "PseudoElementSelector", - "name": "view-transition-old", + "type": "RelativeSelector", + "combinator": null, + "selectors": [ + { + "type": "PseudoElementSelector", + "name": "view-transition-old", + "start": 60, + "end": 81 + } + ], "start": 60, - "end": 81 + "end": 86 } ] } @@ -52,33 +60,49 @@ "end": 146, "children": [ { - "type": "Selector", + "type": "ComplexSelector", "start": 111, "end": 146, "children": [ { - "type": "PseudoClassSelector", - "name": "global", - "args": { - "type": "SelectorList", - "start": 119, - "end": 145, - "children": [ - { - "type": "Selector", + "type": "RelativeSelector", + "combinator": null, + "selectors": [ + { + "type": "PseudoClassSelector", + "name": "global", + "args": { + "type": "SelectorList", "start": 119, "end": 145, "children": [ { - "type": "PseudoElementSelector", - "name": "view-transition-old", + "type": "ComplexSelector", "start": 119, - "end": 140 + "end": 145, + "children": [ + { + "type": "RelativeSelector", + "combinator": null, + "selectors": [ + { + "type": "PseudoElementSelector", + "name": "view-transition-old", + "start": 119, + "end": 140 + } + ], + "start": 119, + "end": 145 + } + ] } ] - } - ] - }, + }, + "start": 111, + "end": 146 + } + ], "start": 111, "end": 146 } @@ -111,15 +135,23 @@ "end": 199, "children": [ { - "type": "Selector", + "type": "ComplexSelector", "start": 171, "end": 199, "children": [ { - "type": "PseudoElementSelector", - "name": "highlight", + "type": "RelativeSelector", + "combinator": null, + "selectors": [ + { + "type": "PseudoElementSelector", + "name": "highlight", + "start": 171, + "end": 182 + } + ], "start": 171, - "end": 182 + "end": 199 } ] } @@ -150,21 +182,29 @@ "end": 245, "children": [ { - "type": "Selector", + "type": "ComplexSelector", "start": 220, "end": 245, "children": [ { - "type": "TypeSelector", - "name": "custom-element", + "type": "RelativeSelector", + "combinator": null, + "selectors": [ + { + "type": "TypeSelector", + "name": "custom-element", + "start": 220, + "end": 234 + }, + { + "type": "PseudoElementSelector", + "name": "part", + "start": 234, + "end": 240 + } + ], "start": 220, - "end": 234 - }, - { - "type": "PseudoElementSelector", - "name": "part", - "start": 234, - "end": 240 + "end": 245 } ] } @@ -195,15 +235,23 @@ "end": 285, "children": [ { - "type": "Selector", + "type": "ComplexSelector", "start": 266, "end": 285, "children": [ { - "type": "PseudoElementSelector", - "name": "slotted", + "type": "RelativeSelector", + "combinator": null, + "selectors": [ + { + "type": "PseudoElementSelector", + "name": "slotted", + "start": 266, + "end": 275 + } + ], "start": 266, - "end": 275 + "end": 285 } ] } @@ -234,58 +282,89 @@ "end": 359, "children": [ { - "type": "Selector", + "type": "ComplexSelector", "start": 306, "end": 359, "children": [ { - "type": "PseudoClassSelector", - "name": "is", - "args": { - "type": "SelectorList", - "start": 324, - "end": 355, - "children": [ - { - "type": "Selector", + "type": "RelativeSelector", + "combinator": null, + "selectors": [ + { + "type": "PseudoClassSelector", + "name": "is", + "args": { + "type": "SelectorList", "start": 324, - "end": 330, - "children": [ - { - "type": "TypeSelector", - "name": "button", - "start": 324, - "end": 330 - } - ] - }, - { - "type": "Selector", - "start": 349, "end": 355, "children": [ { - "type": "TypeSelector", - "name": "h1", - "start": 349, - "end": 351 - }, - { - "type": "Combinator", - "name": "+", - "start": 352, - "end": 353 + "type": "ComplexSelector", + "start": 324, + "end": 330, + "children": [ + { + "type": "RelativeSelector", + "combinator": null, + "selectors": [ + { + "type": "TypeSelector", + "name": "button", + "start": 324, + "end": 330 + } + ], + "start": 324, + "end": 330 + } + ] }, { - "type": "TypeSelector", - "name": "p", - "start": 354, - "end": 355 + "type": "ComplexSelector", + "start": 349, + "end": 355, + "children": [ + { + "type": "RelativeSelector", + "combinator": null, + "selectors": [ + { + "type": "TypeSelector", + "name": "h1", + "start": 349, + "end": 351 + } + ], + "start": 349, + "end": 351 + }, + { + "type": "RelativeSelector", + "combinator": { + "type": "Combinator", + "name": "+", + "start": 352, + "end": 353 + }, + "selectors": [ + { + "type": "TypeSelector", + "name": "p", + "start": 354, + "end": 355 + } + ], + "start": 351, + "end": 355 + } + ] } ] - } - ] - }, + }, + "start": 306, + "end": 359 + } + ], "start": 306, "end": 359 } diff --git a/packages/svelte/tests/parser-modern/samples/semicolon-inside-quotes/output.json b/packages/svelte/tests/parser-modern/samples/semicolon-inside-quotes/output.json index c6fbb599c7f0..72cd9907f2d8 100644 --- a/packages/svelte/tests/parser-modern/samples/semicolon-inside-quotes/output.json +++ b/packages/svelte/tests/parser-modern/samples/semicolon-inside-quotes/output.json @@ -21,13 +21,21 @@ "end": 139, "children": [ { - "type": "Selector", + "type": "ComplexSelector", "start": 137, "end": 139, "children": [ { - "type": "TypeSelector", - "name": "h1", + "type": "RelativeSelector", + "combinator": null, + "selectors": [ + { + "type": "TypeSelector", + "name": "h1", + "start": 137, + "end": 139 + } + ], "start": 137, "end": 139 } From 2353cd7566c86b8a3ab9acea24916d70fbe9d785 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 14 Feb 2024 18:40:52 -0500 Subject: [PATCH 08/29] regenerate types --- packages/svelte/types/index.d.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 314ec9dd7fed..f822b81206e0 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -964,11 +964,19 @@ declare module 'svelte/compiler' { | LegacyTitle | LegacyWindow; + interface LegacySelector extends BaseNode_1 { + type: 'Selector'; + children: Array; + } + + type LegacyCssNode = LegacySelector; + type LegacySvelteNode = | LegacyConstTag | LegacyElementLike | LegacyAttributeLike | LegacyAttributeShorthand + | LegacyCssNode | Text; /** * The preprocess function provides convenient hooks for arbitrarily transforming component source code. @@ -1489,7 +1497,7 @@ declare module 'svelte/compiler' { | Comment | Block; - type SvelteNode = Node | TemplateNode | Fragment; + type SvelteNode = Node | TemplateNode | Fragment | Css.Node; interface Script extends BaseNode { type: 'Script'; From 8d58de75c089f1887647e97d23aa667598feb83b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 14 Feb 2024 20:26:19 -0500 Subject: [PATCH 09/29] start porting scoping logic etc --- packages/svelte/src/compiler/css/types.d.ts | 9 + .../src/compiler/phases/1-parse/read/style.js | 21 +- .../compiler/phases/2-analyze/css/index.js | 662 ++++++++++++++++++ .../compiler/phases/2-analyze/css/utils.js | 48 ++ .../src/compiler/phases/2-analyze/index.js | 11 +- .../compiler/phases/3-transform/css/index.js | 126 ++++ .../src/compiler/phases/3-transform/index.js | 15 +- .../svelte/src/compiler/types/template.d.ts | 5 +- 8 files changed, 884 insertions(+), 13 deletions(-) create mode 100644 packages/svelte/src/compiler/phases/2-analyze/css/index.js create mode 100644 packages/svelte/src/compiler/phases/2-analyze/css/utils.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/css/index.js diff --git a/packages/svelte/src/compiler/css/types.d.ts b/packages/svelte/src/compiler/css/types.d.ts index 2a4d510ea17a..b1f50394aca8 100644 --- a/packages/svelte/src/compiler/css/types.d.ts +++ b/packages/svelte/src/compiler/css/types.d.ts @@ -26,12 +26,21 @@ export interface SelectorList extends BaseNode { export interface ComplexSelector extends BaseNode { type: 'ComplexSelector'; children: RelativeSelector[]; + metadata: { + used: boolean; + }; } export interface RelativeSelector extends BaseNode { type: 'RelativeSelector'; combinator: null | Combinator; selectors: SimpleSelector[]; + metadata: { + is_global: boolean; + is_host: boolean; + is_root: boolean; + should_encapsulate: boolean; + }; } export interface TypeSelector extends BaseNode { 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 d024d298d032..5a618b62350c 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/style.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/style.js @@ -196,7 +196,13 @@ function read_selector(parser, inside_pseudo_class = false) { combinator: null, selectors: [], start: parser.index, - end: -1 + end: -1, + metadata: { + is_global: false, + is_host: false, + is_root: false, + should_encapsulate: false + } }; while (parser.index < parser.template.length) { @@ -221,7 +227,13 @@ function read_selector(parser, inside_pseudo_class = false) { combinator, selectors: [], start, - end: -1 + end: -1, + metadata: { + is_global: false, + is_host: false, + is_root: false, + should_encapsulate: false + } }; parser.allow_whitespace(); @@ -372,7 +384,10 @@ function read_selector(parser, inside_pseudo_class = false) { type: 'ComplexSelector', start: list_start, end: index, - children + children, + metadata: { + used: false + } }; } diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/index.js b/packages/svelte/src/compiler/phases/2-analyze/css/index.js new file mode 100644 index 000000000000..349a34363067 --- /dev/null +++ b/packages/svelte/src/compiler/phases/2-analyze/css/index.js @@ -0,0 +1,662 @@ +import { walk } from 'zimmerframe'; +import { get_possible_values } from './utils.js'; +import { regex_ends_with_whitespace, regex_starts_with_whitespace } from '../../patterns.js'; + +/** + * @typedef {{ + * stylesheet: import('#compiler').Style; + * element: import('#compiler').RegularElement | import('#compiler').SvelteElement; + * }} State + */ +/** @typedef {typeof NodeExist[keyof typeof NodeExist]} NodeExistsValue */ + +const NO_MATCH = 'NO_MATCH'; +const POSSIBLE_MATCH = 'POSSIBLE_MATCH'; +const UNKNOWN_SELECTOR = 'UNKNOWN_SELECTOR'; + +const NodeExist = /** @type {const} */ ({ + Probably: 0, + Definitely: 1 +}); + +const whitelist_attribute_selector = new Map([ + ['details', new Set(['open'])], + ['dialog', new Set(['open'])] +]); + +/** + * + * @param {import('#compiler').Style} stylesheet + * @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} element + */ +export function prune(stylesheet, element) { + /** @type {State} */ + const state = { stylesheet, element }; + + walk(stylesheet, state, visitors); +} + +/** @type {import('zimmerframe').Visitors} */ +const visitors = { + ComplexSelector(node, context) { + context.next(); + + if (apply_selector(node.children.slice(), context.state.element, context.state.stylesheet)) { + node.metadata.used = true; + } + }, + RelativeSelector(node, context) { + node.metadata.is_global = + node.selectors.length >= 1 && + node.selectors[0].type === 'PseudoClassSelector' && + node.selectors[0].name === 'global' && + node.selectors.every( + (selector) => + selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector' + ); + + if (node.selectors.length === 1) { + const first = node.selectors[0]; + node.metadata.is_host = first.type === 'PseudoClassSelector' && first.name === 'host'; + node.metadata.is_root = first.type === 'PseudoClassSelector' && first.name === 'root'; + } + } +}; + +/** + * @param {import('#compiler').Css.RelativeSelector[]} relative_selectors + * @param {import('#compiler').RegularElement | import('#compiler').SvelteElement | null} node + * @param {import('#compiler').Style} stylesheet + * @returns {boolean} + */ +function apply_selector(relative_selectors, node, stylesheet) { + const relative_selector = relative_selectors.pop(); + if (!relative_selector) return false; + if (!node) { + return ( + (relative_selector.metadata.is_global && + relative_selectors.every((relative_selector) => relative_selector.metadata.is_global)) || + (relative_selector.metadata.is_host && relative_selectors.length === 0) + ); + } + const applies = block_might_apply_to_node(relative_selector, node); + + if (applies === NO_MATCH) { + return false; + } + + /** + * Mark both the compound selector and the node it selects as encapsulated, + * for transformation in a later step + * @param {import('#compiler').Css.RelativeSelector} relative_selector + * @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} element + */ + function mark(relative_selector, element) { + relative_selector.metadata.should_encapsulate = true; + element.metadata.scoped = true; + return true; + } + + if (applies === UNKNOWN_SELECTOR) { + return mark(relative_selector, node); + } + + if (relative_selector.combinator) { + if ( + relative_selector.combinator.type === 'Combinator' && + relative_selector.combinator.name === ' ' + ) { + for (const ancestor_block of relative_selectors) { + if (ancestor_block.metadata.is_global) { + continue; + } + if (ancestor_block.metadata.is_host) { + return mark(relative_selector, node); + } + /** @type {import('#compiler').RegularElement | import('#compiler').SvelteElement | null} */ + let parent = node; + let matched = false; + while ((parent = get_element_parent(parent))) { + if (block_might_apply_to_node(ancestor_block, parent) !== NO_MATCH) { + mark(ancestor_block, parent); + matched = true; + } + } + if (matched) { + return mark(relative_selector, node); + } + } + if (relative_selectors.every((relative_selector) => relative_selector.metadata.is_global)) { + return mark(relative_selector, node); + } + return false; + } else if (relative_selector.combinator.name === '>') { + const has_global_parent = relative_selectors.every( + (relative_selector) => relative_selector.metadata.is_global + ); + if ( + has_global_parent || + apply_selector(relative_selectors, get_element_parent(node), stylesheet) + ) { + return mark(relative_selector, node); + } + return false; + } else if ( + relative_selector.combinator.name === '+' || + relative_selector.combinator.name === '~' + ) { + const siblings = get_possible_element_siblings( + node, + relative_selector.combinator.name === '+' + ); + let has_match = false; + // NOTE: if we have :global(), we couldn't figure out what is selected within `:global` due to the + // css-tree limitation that does not parse the inner selector of :global + // so unless we are sure there will be no sibling to match, we will consider it as matched + const has_global = relative_selectors.some( + (relative_selector) => relative_selector.metadata.is_global + ); + if (has_global) { + if (siblings.size === 0 && get_element_parent(node) !== null) { + return false; + } + return mark(relative_selector, node); + } + for (const possible_sibling of siblings.keys()) { + if (apply_selector(relative_selectors.slice(), possible_sibling, stylesheet)) { + mark(relative_selector, node); + has_match = true; + } + } + return has_match; + } + + // TODO other combinators + return mark(relative_selector, node); + } + + return mark(relative_selector, node); +} + +const regex_backslash_and_following_character = /\\(.)/g; + +/** + * @param {import('#compiler').Css.RelativeSelector} relative_selector + * @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node + * @returns {NO_MATCH | POSSIBLE_MATCH | UNKNOWN_SELECTOR} + */ +function block_might_apply_to_node(relative_selector, node) { + if (relative_selector.metadata.is_host || relative_selector.metadata.is_root) return NO_MATCH; + + let i = relative_selector.selectors.length; + while (i--) { + const selector = relative_selector.selectors[i]; + + if (selector.type === 'Percentage' || selector.type === 'Nth') continue; + + const name = selector.name.replace(regex_backslash_and_following_character, '$1'); + + if (selector.type === 'PseudoClassSelector' && (name === 'host' || name === 'root')) { + return NO_MATCH; + } + if ( + relative_selector.selectors.length === 1 && + selector.type === 'PseudoClassSelector' && + name === 'global' + ) { + return NO_MATCH; + } + + if (selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector') { + continue; + } + + if (selector.type === 'AttributeSelector') { + const whitelisted = whitelist_attribute_selector.get(node.name.toLowerCase()); + if ( + !whitelisted?.has(selector.name.toLowerCase()) && + !attribute_matches( + node, + selector.name, + selector.value && unquote(selector.value), + selector.matcher, + selector.flags?.includes('i') ?? false + ) + ) { + return NO_MATCH; + } + } else { + if (selector.type === 'ClassSelector') { + if ( + !attribute_matches(node, 'class', name, '~=', false) && + !node.attributes.some( + (attribute) => attribute.type === 'ClassDirective' && attribute.name === name + ) + ) { + return NO_MATCH; + } + } else if (selector.type === 'IdSelector') { + if (!attribute_matches(node, 'id', name, '=', false)) return NO_MATCH; + } else if (selector.type === 'TypeSelector') { + if ( + node.name.toLowerCase() !== name.toLowerCase() && + name !== '*' && + node.type !== 'SvelteElement' + ) { + return NO_MATCH; + } + } else { + return UNKNOWN_SELECTOR; + } + } + } + + return POSSIBLE_MATCH; +} + +/** + * @param {any} operator + * @param {any} expected_value + * @param {any} case_insensitive + * @param {any} value + */ +function test_attribute(operator, expected_value, case_insensitive, value) { + if (case_insensitive) { + expected_value = expected_value.toLowerCase(); + value = value.toLowerCase(); + } + switch (operator) { + case '=': + return value === expected_value; + case '~=': + return value.split(/\s/).includes(expected_value); + case '|=': + return `${value}-`.startsWith(`${expected_value}-`); + case '^=': + return value.startsWith(expected_value); + case '$=': + return value.endsWith(expected_value); + case '*=': + return value.includes(expected_value); + default: + throw new Error("this shouldn't happen"); + } +} + +/** + * @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node + * @param {string} name + * @param {string | null} expected_value + * @param {string | null} operator + * @param {boolean} case_insensitive + */ +function attribute_matches(node, name, expected_value, operator, case_insensitive) { + for (const attribute of node.attributes) { + if (attribute.type === 'SpreadAttribute') return true; + if (attribute.type === 'BindDirective' && attribute.name === name) return true; + + if (attribute.type !== 'Attribute') continue; + if (attribute.name.toLowerCase() !== name.toLowerCase()) continue; + + if (attribute.value === true) return operator === null; + if (expected_value === null) return true; + + const chunks = attribute.value; + if (chunks.length === 1) { + const value = chunks[0]; + if (value.type === 'Text') { + return test_attribute(operator, expected_value, case_insensitive, value.data); + } + } + + const possible_values = new Set(); + + /** @type {string[]} */ + let prev_values = []; + for (const chunk of chunks) { + const current_possible_values = get_possible_values(chunk); + + // impossible to find out all combinations + if (!current_possible_values) return true; + + if (prev_values.length > 0) { + /** @type {string[]} */ + const start_with_space = []; + + /** @type {string[]} */ + const remaining = []; + + current_possible_values.forEach((current_possible_value) => { + if (regex_starts_with_whitespace.test(current_possible_value)) { + start_with_space.push(current_possible_value); + } else { + remaining.push(current_possible_value); + } + }); + if (remaining.length > 0) { + if (start_with_space.length > 0) { + prev_values.forEach((prev_value) => possible_values.add(prev_value)); + } + + /** @type {string[]} */ + const combined = []; + + prev_values.forEach((prev_value) => { + remaining.forEach((value) => { + combined.push(prev_value + value); + }); + }); + prev_values = combined; + start_with_space.forEach((value) => { + if (regex_ends_with_whitespace.test(value)) { + possible_values.add(value); + } else { + prev_values.push(value); + } + }); + continue; + } else { + prev_values.forEach((prev_value) => possible_values.add(prev_value)); + prev_values = []; + } + } + current_possible_values.forEach((current_possible_value) => { + if (regex_ends_with_whitespace.test(current_possible_value)) { + possible_values.add(current_possible_value); + } else { + prev_values.push(current_possible_value); + } + }); + if (prev_values.length < current_possible_values.size) { + prev_values.push(' '); + } + if (prev_values.length > 20) { + // might grow exponentially, bail out + return true; + } + } + prev_values.forEach((prev_value) => possible_values.add(prev_value)); + + for (const value of possible_values) { + if (test_attribute(operator, expected_value, case_insensitive, value)) return true; + } + } + + return false; +} + +/** @param {string} str */ +function unquote(str) { + if ((str[0] === str[str.length - 1] && str[0] === "'") || str[0] === '"') { + return str.slice(1, str.length - 1); + } + return str; +} + +/** + * @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node + * @returns {import('#compiler').RegularElement | import('#compiler').SvelteElement | null} + */ +function get_element_parent(node) { + /** @type {import('#compiler').SvelteNode | null} */ + let parent = node; + while ( + // @ts-expect-error TODO figure out a more elegant solution + (parent = parent.parent) && + parent.type !== 'RegularElement' && + parent.type !== 'SvelteElement' + ); + return parent ?? null; +} + +/** + * Finds the given node's previous sibling in the DOM + * + * The Svelte `` is just a placeholder and is not actually real. Any children nodes + * in `` are 'flattened' and considered as the same level as the ``'s siblings + * + * e.g. + * ```html + *

Heading 1

+ * + *

Heading 2

+ *
+ * ``` + * + * is considered to look like: + * ```html + *

Heading 1

+ *

Heading 2

+ * ``` + * @param {import('#compiler').SvelteNode} node + * @returns {import('#compiler').SvelteNode} + */ +function find_previous_sibling(node) { + /** @type {import('#compiler').SvelteNode} */ + let current_node = node; + do { + if (current_node.type === 'SlotElement') { + const slot_children = current_node.fragment.nodes; + if (slot_children.length > 0) { + current_node = slot_children.slice(-1)[0]; // go to its last child first + continue; + } + } + while ( + // @ts-expect-error TODO + !current_node.prev && + // @ts-expect-error TODO + current_node.parent && + // @ts-expect-error TODO + current_node.parent.type === 'SlotElement' + ) { + // @ts-expect-error TODO + current_node = current_node.parent; + } + // @ts-expect-error + current_node = current_node.prev; + } while (current_node && current_node.type === 'SlotElement'); + return current_node; +} + +/** + * @param {import('#compiler').SvelteNode} node + * @param {boolean} adjacent_only + * @returns {Map} + */ +function get_possible_element_siblings(node, adjacent_only) { + /** @type {Map} */ + const result = new Map(); + + /** @type {import('#compiler').SvelteNode} */ + let prev = node; + while ((prev = find_previous_sibling(prev))) { + if (prev.type === 'RegularElement') { + if ( + !prev.attributes.find( + (attr) => attr.type === 'Attribute' && attr.name.toLowerCase() === 'slot' + ) + ) { + result.set(prev, NodeExist.Definitely); + } + if (adjacent_only) { + break; + } + } else if (prev.type === 'EachBlock' || prev.type === 'IfBlock' || prev.type === 'AwaitBlock') { + const possible_last_child = get_possible_last_child(prev, adjacent_only); + add_to_map(possible_last_child, result); + if (adjacent_only && has_definite_elements(possible_last_child)) { + return result; + } + } + } + + if (!prev || !adjacent_only) { + /** @type {import('#compiler').SvelteNode | null} */ + let parent = node; + + while ( + // @ts-expect-error TODO + (parent = parent?.parent) && + (parent.type === 'EachBlock' || parent.type === 'IfBlock' || parent.type === 'AwaitBlock') + ) { + const possible_siblings = get_possible_element_siblings(parent, adjacent_only); + add_to_map(possible_siblings, result); + + // @ts-expect-error + if (parent.type === 'EachBlock' && !parent.fallback?.nodes.includes(node)) { + // `{#each ...}{/each}` — `` can be previous sibling of `` + add_to_map(get_possible_last_child(parent, adjacent_only), result); + } + + if (adjacent_only && has_definite_elements(possible_siblings)) { + break; + } + } + } + + return result; +} + +/** + * @param {import('#compiler').EachBlock | import('#compiler').IfBlock | import('#compiler').AwaitBlock} relative_selector + * @param {boolean} adjacent_only + * @returns {Map} + */ +function get_possible_last_child(relative_selector, adjacent_only) { + /** @typedef {Map} NodeMap */ + + /** @type {NodeMap} */ + const result = new Map(); + if (relative_selector.type === 'EachBlock') { + /** @type {NodeMap} */ + const each_result = loop_child(relative_selector.body.nodes, adjacent_only); + + /** @type {NodeMap} */ + const else_result = relative_selector.fallback + ? loop_child(relative_selector.fallback.nodes, adjacent_only) + : new Map(); + const not_exhaustive = !has_definite_elements(else_result); + if (not_exhaustive) { + mark_as_probably(each_result); + mark_as_probably(else_result); + } + add_to_map(each_result, result); + add_to_map(else_result, result); + } else if (relative_selector.type === 'IfBlock') { + /** @type {NodeMap} */ + const if_result = loop_child(relative_selector.consequent.nodes, adjacent_only); + + /** @type {NodeMap} */ + const else_result = relative_selector.alternate + ? loop_child(relative_selector.alternate.nodes, adjacent_only) + : new Map(); + const not_exhaustive = !has_definite_elements(if_result) || !has_definite_elements(else_result); + if (not_exhaustive) { + mark_as_probably(if_result); + mark_as_probably(else_result); + } + add_to_map(if_result, result); + add_to_map(else_result, result); + } else if (relative_selector.type === 'AwaitBlock') { + /** @type {NodeMap} */ + const pending_result = relative_selector.pending + ? loop_child(relative_selector.pending.nodes, adjacent_only) + : new Map(); + + /** @type {NodeMap} */ + const then_result = relative_selector.then + ? loop_child(relative_selector.then.nodes, adjacent_only) + : new Map(); + + /** @type {NodeMap} */ + const catch_result = relative_selector.catch + ? loop_child(relative_selector.catch.nodes, adjacent_only) + : new Map(); + const not_exhaustive = + !has_definite_elements(pending_result) || + !has_definite_elements(then_result) || + !has_definite_elements(catch_result); + if (not_exhaustive) { + mark_as_probably(pending_result); + mark_as_probably(then_result); + mark_as_probably(catch_result); + } + add_to_map(pending_result, result); + add_to_map(then_result, result); + add_to_map(catch_result, result); + } + return result; +} + +/** + * @param {Map} result + * @returns {boolean} + */ +function has_definite_elements(result) { + if (result.size === 0) return false; + for (const exist of result.values()) { + if (exist === NodeExist.Definitely) { + return true; + } + } + return false; +} + +/** + * @param {Map} from + * @param {Map} to + * @returns {void} + */ +function add_to_map(from, to) { + from.forEach((exist, element) => { + to.set(element, higher_existence(exist, to.get(element))); + }); +} + +/** + * @param {NodeExistsValue | undefined} exist1 + * @param {NodeExistsValue | undefined} exist2 + * @returns {NodeExistsValue} + */ +function higher_existence(exist1, exist2) { + // @ts-expect-error TODO figure out if this is a bug + if (exist1 === undefined || exist2 === undefined) return exist1 || exist2; + return exist1 > exist2 ? exist1 : exist2; +} + +/** @param {Map} result */ +function mark_as_probably(result) { + for (const key of result.keys()) { + result.set(key, NodeExist.Probably); + } +} + +/** + * @param {import('#compiler').SvelteNode[]} children + * @param {boolean} adjacent_only + */ +function loop_child(children, adjacent_only) { + /** @type {Map} */ + const result = new Map(); + for (let i = children.length - 1; i >= 0; i--) { + const child = children[i]; + if (child.type === 'RegularElement') { + result.set(child, NodeExist.Definitely); + if (adjacent_only) { + break; + } + } else if ( + child.type === 'EachBlock' || + child.type === 'IfBlock' || + child.type === 'AwaitBlock' + ) { + const child_result = get_possible_last_child(child, adjacent_only); + add_to_map(child_result, result); + if (adjacent_only && has_definite_elements(child_result)) { + break; + } + } + } + return result; +} diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/utils.js b/packages/svelte/src/compiler/phases/2-analyze/css/utils.js new file mode 100644 index 000000000000..d3554c343713 --- /dev/null +++ b/packages/svelte/src/compiler/phases/2-analyze/css/utils.js @@ -0,0 +1,48 @@ +const regex_return_characters = /\r/g; + +/** + * @param {string} str + * @returns {string} + */ +export function hash(str) { + str = str.replace(regex_return_characters, ''); + let hash = 5381; + let i = str.length; + + while (i--) hash = ((hash << 5) - hash) ^ str.charCodeAt(i); + return (hash >>> 0).toString(36); +} + +const UNKNOWN = {}; + +/** + * @param {import('estree').Node} node + * @param {Set} set + */ +function gather_possible_values(node, set) { + if (node.type === 'Literal') { + set.add(String(node.value)); + } else if (node.type === 'ConditionalExpression') { + gather_possible_values(node.consequent, set); + gather_possible_values(node.alternate, set); + } else { + set.add(UNKNOWN); + } +} + +/** + * @param {import('#compiler').Text | import('#compiler').ExpressionTag} chunk + * @returns {Set | null} + */ +export function get_possible_values(chunk) { + const values = new Set(); + + if (chunk.type === 'Text') { + values.add(chunk.data); + } else { + gather_possible_values(chunk.expression, values); + } + + if (values.has(UNKNOWN)) return null; + return values; +} diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 0b5127d2fa9b..81cbf29517b8 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -22,6 +22,7 @@ import { create_attribute, is_element_node } from '../nodes.js'; import { DelegatedEvents, namespace_svg } from '../../../constants.js'; import { should_proxy_or_freeze } from '../3-transform/client/utils.js'; import { validation_css } from './validation/css.js'; +import { prune } from './css/index.js'; /** * @param {import('#compiler').Script | null} script @@ -454,15 +455,15 @@ export function analyze_component(root, options) { } if (root.css) { + // validate walk(root.css, {}, validation_css); - } - for (const element of analysis.elements) { - analysis.stylesheet.apply(element); + // mark nodes as scoped/unused/empty etc + for (const element of analysis.elements) { + prune(root.css, element); + } } - analysis.stylesheet.reify(options.generate === 'client'); - // TODO // analysis.stylesheet.warn_on_unused_selectors(analysis); diff --git a/packages/svelte/src/compiler/phases/3-transform/css/index.js b/packages/svelte/src/compiler/phases/3-transform/css/index.js new file mode 100644 index 000000000000..9de7a40ccade --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/css/index.js @@ -0,0 +1,126 @@ +import MagicString from 'magic-string'; +import { walk } from 'zimmerframe'; + +/** @typedef {{ code: MagicString, dev: boolean }} State */ + +/** + * + * @param {string} source + * @param {import('#compiler').Style} stylesheet + * @param {string} file + * @param {boolean} dev + */ +export function render_stylesheet(source, stylesheet, file, dev) { + const code = new MagicString(source); + + /** @type {State} */ + const state = { + code, + dev + }; + + walk(/** @type {import('#compiler').Css.Node} */ (stylesheet), state, visitors); + + code.remove(0, stylesheet.content.start); + code.remove(/** @type {number} */ (stylesheet.content.end), source.length); + + return { + code: code.toString(), + map: code.generateMap({ + includeContent: true, + source: file, + file + }) + }; +} + +/** @type {import('zimmerframe').Visitors} */ +const visitors = { + _: (node, context) => { + context.state.code.addSourcemapLocation(node.start); + context.state.code.addSourcemapLocation(node.end); + context.next(); + }, + Rule(node, { state, next }) { + // keep empty rules in dev, because it's convenient to + // see them in devtools + // if (!state.dev && this.is_empty()) { + // state.code.prependRight(node.start, '/* (empty) '); + // state.code.appendLeft(node.end, '*/'); + // escape_comment_close(node, state.code); + // return; + // } + + const used = node.prelude.children.filter((s) => s.metadata.used); + + if (used.length === 0) { + state.code.prependRight(node.start, '/* (unused) '); + state.code.appendLeft(node.end, '*/'); + escape_comment_close(node, state.code); + + return; + } + + if (used.length < node.prelude.children.length) { + let pruning = false; + let last = node.prelude.children[0].start; + + for (let i = 0; i < node.prelude.children.length; i += 1) { + const selector = node.prelude.children[i]; + + if (selector.metadata.used === pruning) { + if (pruning) { + let i = selector.start; + while (state.code.original[i] !== ',') i--; + + state.code.overwrite(i, i + 1, '*/'); + } else { + if (i === 0) { + state.code.prependRight(selector.start, '/* (unused) '); + } else { + state.code.overwrite(last, selector.start, ' /* (unused) '); + } + } + + pruning = !pruning; + } + + last = selector.end; + } + + if (pruning) { + state.code.appendLeft(last, '*/'); + } + } + + next(); + } +}; + +/** + * + * @param {import('#compiler').Css.Rule} node + * @param {MagicString} code + */ +function escape_comment_close(node, code) { + let escaped = false; + let in_comment = false; + + for (let i = node.start; i < node.end; i++) { + if (escaped) { + escaped = false; + } else { + const char = code.original[i]; + if (in_comment) { + if (char === '*' && code.original[i + 1] === '/') { + code.prependRight(++i, '\\'); + in_comment = false; + } + } else if (char === '\\') { + escaped = true; + } else if (char === '/' && code.original[++i] === '*') { + in_comment = true; + } + } + } +} diff --git a/packages/svelte/src/compiler/phases/3-transform/index.js b/packages/svelte/src/compiler/phases/3-transform/index.js index af8de96d70bb..968c05bed5fd 100644 --- a/packages/svelte/src/compiler/phases/3-transform/index.js +++ b/packages/svelte/src/compiler/phases/3-transform/index.js @@ -3,6 +3,7 @@ import { VERSION } from '../../../version.js'; import { server_component, server_module } from './server/transform-server.js'; import { client_component, client_module } from './client/transform-client.js'; import { getLocator } from 'locate-character'; +import { render_stylesheet } from './css/index.js'; /** * @param {import('../types').ComponentAnalysis} analysis @@ -41,12 +42,18 @@ export function transform_component(analysis, source, options) { ]; } + const css = + analysis.stylesheet.ast && !analysis.inject_styles + ? render_stylesheet(source, analysis.stylesheet.ast, options.filename ?? 'TODO', options.dev) + : null; + return { js: print(program, { sourceMapSource: options.filename }), // TODO needs more logic to apply map from preprocess - css: - analysis.stylesheet.has_styles && !analysis.inject_styles - ? analysis.stylesheet.render(options.filename ?? 'TODO', source, options.dev) - : null, + css, + // css: + // analysis.stylesheet.has_styles && !analysis.inject_styles + // ? analysis.stylesheet.render(options.filename ?? 'TODO', source, options.dev) + // : null, warnings: transform_warnings(source, options.filename, analysis.warnings), metadata: { runes: analysis.runes diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index 224dc3742994..3fc2a0d002d1 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -15,6 +15,7 @@ import type { Program, SpreadElement } from 'estree'; +import type { Atrule, Rule } from '../css/types'; export interface BaseNode { type: string; @@ -290,6 +291,7 @@ export interface RegularElement extends BaseElement { svg: boolean; /** `true` if contains a SpreadAttribute */ has_spread: boolean; + scoped: boolean; }; } @@ -319,6 +321,7 @@ export interface SvelteElement extends BaseElement { * `null` means we can't know statically. */ svg: boolean | null; + scoped: boolean; }; } @@ -470,7 +473,7 @@ export interface Script extends BaseNode { export interface Style extends BaseNode { type: 'Style'; attributes: any[]; // TODO - children: any[]; // TODO add CSS node types + children: Array; content: { start: number; end: number; From 5fa9410ef330ce8971a68e03a89a12c6e8f2db28 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 14 Feb 2024 20:30:02 -0500 Subject: [PATCH 10/29] move Style to Css.StyleSheet --- packages/svelte/src/compiler/css/Stylesheet.js | 4 ++-- packages/svelte/src/compiler/css/types.d.ts | 15 ++++++++++++--- .../src/compiler/phases/1-parse/read/style.js | 7 +++---- .../src/compiler/phases/2-analyze/css/index.js | 6 +++--- .../src/compiler/phases/3-transform/css/index.js | 2 +- packages/svelte/src/compiler/types/template.d.ts | 13 +------------ 6 files changed, 22 insertions(+), 25 deletions(-) diff --git a/packages/svelte/src/compiler/css/Stylesheet.js b/packages/svelte/src/compiler/css/Stylesheet.js index 84c729c5f764..c0047cefe759 100644 --- a/packages/svelte/src/compiler/css/Stylesheet.js +++ b/packages/svelte/src/compiler/css/Stylesheet.js @@ -320,7 +320,7 @@ class Atrule { } export class Stylesheet { - /** @type {import('#compiler').Style | null} */ + /** @type {import('#compiler').Css.StyleSheet | null} */ ast; /** @type {string} */ @@ -343,7 +343,7 @@ export class Stylesheet { /** * @param {{ - * ast: import('#compiler').Style | null; + * ast: import('#compiler').Css.StyleSheet | null; * filename: string; * component_name: string; * get_css_hash: import('#compiler').CssHashGetter; diff --git a/packages/svelte/src/compiler/css/types.d.ts b/packages/svelte/src/compiler/css/types.d.ts index b1f50394aca8..70e5dc4006aa 100644 --- a/packages/svelte/src/compiler/css/types.d.ts +++ b/packages/svelte/src/compiler/css/types.d.ts @@ -1,10 +1,19 @@ -import type { Style } from '../types/template'; - export interface BaseNode { start: number; end: number; } +export interface StyleSheet extends BaseNode { + type: 'StyleSheet'; + attributes: any[]; // TODO + children: Array; + content: { + start: number; + end: number; + styles: string; + }; +} + export interface Atrule extends BaseNode { type: 'Atrule'; name: string; @@ -115,7 +124,7 @@ export interface Declaration extends BaseNode { // for zimmerframe export type Node = - | Style + | StyleSheet | Rule | Atrule | ComplexSelector 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 5a618b62350c..e5f880d11fed 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/style.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/style.js @@ -18,7 +18,7 @@ const REGEX_HTML_COMMENT_CLOSE = /-->/; * @param {import('../index.js').Parser} parser * @param {number} start * @param {Array} attributes - * @returns {import('#compiler').Style} + * @returns {import('#compiler').Css.StyleSheet} */ export default function read_style(parser, start, attributes) { const content_start = parser.index; @@ -28,7 +28,7 @@ export default function read_style(parser, start, attributes) { parser.read(/^<\/style\s*>/); return { - type: 'Style', + type: 'StyleSheet', start, end: parser.index, attributes, @@ -37,8 +37,7 @@ export default function read_style(parser, start, attributes) { start: content_start, end: content_end, styles: parser.template.slice(content_start, content_end) - }, - parent: null + } }; } diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/index.js b/packages/svelte/src/compiler/phases/2-analyze/css/index.js index 349a34363067..9a1520bdf006 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/index.js @@ -4,7 +4,7 @@ import { regex_ends_with_whitespace, regex_starts_with_whitespace } from '../../ /** * @typedef {{ - * stylesheet: import('#compiler').Style; + * stylesheet: import('#compiler').Css.StyleSheet; * element: import('#compiler').RegularElement | import('#compiler').SvelteElement; * }} State */ @@ -26,7 +26,7 @@ const whitelist_attribute_selector = new Map([ /** * - * @param {import('#compiler').Style} stylesheet + * @param {import('#compiler').Css.StyleSheet} stylesheet * @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} element */ export function prune(stylesheet, element) { @@ -66,7 +66,7 @@ const visitors = { /** * @param {import('#compiler').Css.RelativeSelector[]} relative_selectors * @param {import('#compiler').RegularElement | import('#compiler').SvelteElement | null} node - * @param {import('#compiler').Style} stylesheet + * @param {import('#compiler').Css.StyleSheet} stylesheet * @returns {boolean} */ function apply_selector(relative_selectors, node, stylesheet) { diff --git a/packages/svelte/src/compiler/phases/3-transform/css/index.js b/packages/svelte/src/compiler/phases/3-transform/css/index.js index 9de7a40ccade..1ba68542ec09 100644 --- a/packages/svelte/src/compiler/phases/3-transform/css/index.js +++ b/packages/svelte/src/compiler/phases/3-transform/css/index.js @@ -6,7 +6,7 @@ import { walk } from 'zimmerframe'; /** * * @param {string} source - * @param {import('#compiler').Style} stylesheet + * @param {import('#compiler').Css.StyleSheet} stylesheet * @param {string} file * @param {boolean} dev */ diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index 3fc2a0d002d1..3cf74d09d65c 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -53,7 +53,7 @@ export interface Root extends BaseNode { options: SvelteOptions | null; fragment: Fragment; /** The parsed `