diff --git a/packages/svelte/src/compiler/phases/1-parse/index.js b/packages/svelte/src/compiler/phases/1-parse/index.js index acf96678ff..4e94d944c1 100644 --- a/packages/svelte/src/compiler/phases/1-parse/index.js +++ b/packages/svelte/src/compiler/phases/1-parse/index.js @@ -47,7 +47,7 @@ export class Parser { throw new TypeError('Template must be a string'); } - this.template = template.trimRight(); + this.template = template.trimEnd(); let match_lang; @@ -147,9 +147,9 @@ export class Parser { /** * @param {string} str - * @param {boolean} [required] + * @param {boolean} required */ - eat(str, required) { + eat(str, required = false) { if (this.match(str)) { this.index += str.length; return true; diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js b/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js index 2d9368332c..098b632e84 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js @@ -1,6 +1,7 @@ import { get_possible_values } from './gather_possible_values.js'; import { regex_starts_with_whitespace, regex_ends_with_whitespace } from '../../patterns.js'; import { error } from '../../../errors.js'; +import { Stylesheet } from './Stylesheet.js'; const NO_MATCH = 'NO_MATCH'; const POSSIBLE_MATCH = 'POSSIBLE_MATCH'; @@ -22,7 +23,7 @@ export default class Selector { /** @type {import('#compiler').Css.Selector} */ node; - /** @type {import('./Stylesheet.js').default} */ + /** @type {import('./Stylesheet.js').Stylesheet} */ stylesheet; /** @type {Block[]} */ @@ -31,39 +32,28 @@ export default class Selector { /** @type {Block[]} */ local_blocks; - /** @type {boolean} */ - used; + used = false; /** * @param {import('#compiler').Css.Selector} node - * @param {import('./Stylesheet.js').default} stylesheet + * @param {import('./Stylesheet.js').Stylesheet} stylesheet */ constructor(node, stylesheet) { this.node = node; this.stylesheet = stylesheet; this.blocks = group_selectors(node); // take trailing :global(...) selectors out of consideration - let i = this.blocks.length; - while (i > 0) { - if (!this.blocks[i - 1].global) break; - i -= 1; - } - this.local_blocks = this.blocks.slice(0, i); - const host_only = this.blocks.length === 1 && this.blocks[0].host; - const root_only = this.blocks.length === 1 && this.blocks[0].root; - this.used = this.local_blocks.length === 0 || host_only || root_only; + const i = this.blocks.findLastIndex((block) => !block.can_ignore()); + this.local_blocks = this.blocks.slice(0, i + 1); + + // if we have a `:root {...}` or `:global(...) {...}` selector, we need to mark + // this selector as `used` even if the component doesn't contain any nodes + this.used = this.local_blocks.length === 0; } /** @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node */ apply(node) { - /** @type {Array<{ node: import('#compiler').RegularElement | import('#compiler').SvelteElement; block: Block }>} */ - const to_encapsulate = []; - apply_selector(this.local_blocks.slice(), node, to_encapsulate); - if (to_encapsulate.length > 0) { - to_encapsulate.forEach(({ node, block }) => { - this.stylesheet.nodes_with_css_class.add(node); - block.should_encapsulate = true; - }); + if (apply_selector(this.local_blocks.slice(), node, this.stylesheet)) { this.used = true; } } @@ -130,6 +120,13 @@ export default class Selector { /** @param {import('../../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.blocks.length; for (; start < end; start += 1) { @@ -143,9 +140,6 @@ export default class Selector { error(this.blocks[i].selectors[0], 'invalid-css-global-placement'); } } - this.validate_global_with_multiple_selectors(); - this.validate_global_compound_selector(); - this.validate_invalid_combinator_without_selector(analysis); } validate_global_with_multiple_selectors() { @@ -207,10 +201,10 @@ export default class Selector { /** * @param {Block[]} blocks * @param {import('#compiler').RegularElement | import('#compiler').SvelteElement | null} node - * @param {Array<{ node: import('#compiler').RegularElement | import('#compiler').SvelteElement; block: Block }>} to_encapsulate + * @param {Stylesheet} stylesheet * @returns {boolean} */ -function apply_selector(blocks, node, to_encapsulate) { +function apply_selector(blocks, node, stylesheet) { const block = blocks.pop(); if (!block) return false; if (!node) { @@ -224,11 +218,22 @@ function apply_selector(blocks, node, to_encapsulate) { return false; } - if (applies === UNKNOWN_SELECTOR) { - to_encapsulate.push({ node, block }); + /** + * Mark both the compound selector and the node it selects as encapsulated, + * for transformation in a later step + * @param {Block} block + * @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node + */ + function mark(block, node) { + block.should_encapsulate = true; + stylesheet.nodes_with_css_class.add(node); return true; } + if (applies === UNKNOWN_SELECTOR) { + return mark(block, node); + } + if (block.combinator) { if (block.combinator.type === 'Combinator' && block.combinator.name === ' ') { for (const ancestor_block of blocks) { @@ -236,31 +241,29 @@ function apply_selector(blocks, node, to_encapsulate) { continue; } if (ancestor_block.host) { - to_encapsulate.push({ node, block }); - return true; + return mark(block, 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) { - to_encapsulate.push({ node: parent, block: ancestor_block }); + mark(ancestor_block, parent); + matched = true; } } - if (to_encapsulate.length) { - to_encapsulate.push({ node, block }); - return true; + if (matched) { + return mark(block, node); } } if (blocks.every((block) => block.global)) { - to_encapsulate.push({ node, block }); - return true; + return mark(block, node); } return false; } else if (block.combinator.name === '>') { const has_global_parent = blocks.every((block) => block.global); - if (has_global_parent || apply_selector(blocks, get_element_parent(node), to_encapsulate)) { - to_encapsulate.push({ node, block }); - return true; + if (has_global_parent || apply_selector(blocks, get_element_parent(node), stylesheet)) { + return mark(block, node); } return false; } else if (block.combinator.name === '+' || block.combinator.name === '~') { @@ -274,23 +277,22 @@ function apply_selector(blocks, node, to_encapsulate) { if (siblings.size === 0 && get_element_parent(node) !== null) { return false; } - to_encapsulate.push({ node, block }); - return true; + return mark(block, node); } for (const possible_sibling of siblings.keys()) { - if (apply_selector(blocks.slice(), possible_sibling, to_encapsulate)) { - to_encapsulate.push({ node, block }); + if (apply_selector(blocks.slice(), possible_sibling, stylesheet)) { + mark(block, node); has_match = true; } } return has_match; } + // TODO other combinators - to_encapsulate.push({ node, block }); - return true; + return mark(block, node); } - to_encapsulate.push({ node, block }); - return true; + + return mark(block, node); } const regex_backslash_and_following_character = /\\(.)/g; @@ -815,6 +817,11 @@ class Block { this.selectors.push(selector); this.end = selector.end; } + + can_ignore() { + return this.global || this.host || this.root; + } + get global() { return ( this.selectors.length >= 1 && diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/Stylesheet.js b/packages/svelte/src/compiler/phases/2-analyze/css/Stylesheet.js index 7522de5ad0..84914485a0 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Stylesheet.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Stylesheet.js @@ -52,19 +52,19 @@ class Rule { /** @type {import('./Selector.js').default[]} */ selectors; - /** @type {Declaration[]} */ - declarations; - /** @type {import('#compiler').Css.Rule} */ node; - /** @type {Atrule | undefined} */ + /** @type {Stylesheet | Atrule} */ parent; + /** @type {Declaration[]} */ + declarations; + /** * @param {import('#compiler').Css.Rule} node * @param {any} stylesheet - * @param {Atrule | undefined} parent + * @param {Stylesheet | Atrule} parent */ constructor(node, stylesheet, parent) { this.node = node; @@ -81,16 +81,24 @@ class Rule { this.selectors.forEach((selector) => selector.apply(node)); // TODO move the logic in here? } - /** @param {boolean} dev */ - is_used(dev) { - if (this.parent && this.parent.node.type === 'Atrule' && is_keyframes_node(this.parent.node)) + /** @returns {boolean} */ + is_empty() { + if (this.declarations.length > 0) return false; + + return true; + } + + /** @returns {boolean} */ + is_used() { + if (this.parent instanceof Atrule && is_keyframes_node(this.parent.node)) { return true; + } - // keep empty rules in dev, because it's convenient to - // see them in devtools - if (this.declarations.length === 0) return dev; + for (const selector of this.selectors) { + if (selector.used) return true; + } - return this.selectors.some((s) => s.used); + return false; } /** @@ -99,7 +107,7 @@ class Rule { * @param {Map} keyframes */ transform(code, id, keyframes) { - if (this.parent && this.parent.node.type === 'Atrule' && is_keyframes_node(this.parent.node)) { + if (this.parent instanceof Atrule && is_keyframes_node(this.parent.node)) { return; } @@ -127,19 +135,16 @@ class Rule { * @param {boolean} dev */ prune(code, dev) { - if (this.parent && this.parent.node.type === 'Atrule' && is_keyframes_node(this.parent.node)) { + if (this.parent instanceof Atrule && is_keyframes_node(this.parent.node)) { return; } // keep empty rules in dev, because it's convenient to // see them in devtools - if (this.declarations.length === 0) { - if (!dev) { - code.prependRight(this.node.start, '/* (empty) '); - code.appendLeft(this.node.end, '*/'); - escape_comment_close(this.node, code); - } - + if (!dev && this.is_empty()) { + code.prependRight(this.node.start, '/* (empty) '); + code.appendLeft(this.node.end, '*/'); + escape_comment_close(this.node, code); return; } @@ -268,8 +273,11 @@ class Atrule { } } - /** @param {boolean} _dev */ - is_used(_dev) { + is_empty() { + return false; // TODO + } + + is_used() { return true; // TODO } @@ -326,7 +334,7 @@ class Atrule { } } -export default class Stylesheet { +export class Stylesheet { /** @type {import('#compiler').Style | null} */ ast; @@ -375,53 +383,35 @@ export default class Stylesheet { this.has_styles = true; const state = { - /** @type {Atrule | undefined} */ - atrule: undefined + /** @type {Stylesheet | Atrule | Rule} */ + current: this }; walk(/** @type {import('#compiler').Css.Node} */ (ast), state, { Atrule: (node, context) => { const atrule = new Atrule(node); - if (context.state.atrule) { - context.state.atrule.children.push(atrule); - } else { - this.children.push(atrule); - } - if (is_keyframes_node(node)) { if (!node.prelude.startsWith('-global-')) { this.keyframes.set(node.prelude, `${this.id}-${node.prelude}`); } - } else if (node.block) { - /** @type {Declaration[]} */ - const declarations = []; - - for (const child of node.block.children) { - if (child.type === 'Declaration') { - declarations.push(new Declaration(child)); - } - } - - if (declarations.length > 0) { - push_array(atrule.declarations, declarations); - } } - context.next({ - ...context.state, - atrule - }); + // @ts-expect-error temporary, until nesting is implemented + context.state.current.children.push(atrule); + context.next({ current: atrule }); + }, + Declaration: (node, context) => { + const declaration = new Declaration(node); + /** @type {Atrule | Rule} */ (context.state.current).declarations.push(declaration); }, Rule: (node, context) => { - const rule = new Rule(node, this, context.state.atrule); - if (context.state.atrule) { - context.state.atrule.children.push(rule); - } else { - this.children.push(rule); - } + // @ts-expect-error temporary, until nesting is implemented + const rule = new Rule(node, this, context.state.current); - context.next(); + // @ts-expect-error temporary, until nesting is implemented + context.state.current.children.push(rule); + context.next({ current: rule }); } }); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index ec0bcd30ae..e8384931f7 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -13,7 +13,7 @@ import * as b from '../../utils/builders.js'; import { ReservedKeywords, Runes, SVGElements } from '../constants.js'; import { Scope, ScopeRoot, create_scopes, get_rune, set_scope } from '../scope.js'; import { merge } from '../visitors.js'; -import Stylesheet from './css/Stylesheet.js'; +import { Stylesheet } from './css/Stylesheet.js'; import { validation_legacy, validation_runes, validation_runes_js } from './validation.js'; import { warn } from '../../warnings.js'; import check_graph_for_cycles from './utils/check_graph_for_cycles.js'; diff --git a/packages/svelte/src/compiler/phases/types.d.ts b/packages/svelte/src/compiler/phases/types.d.ts index 44d9ce0d60..0a92486c85 100644 --- a/packages/svelte/src/compiler/phases/types.d.ts +++ b/packages/svelte/src/compiler/phases/types.d.ts @@ -7,7 +7,7 @@ import type { SvelteOptions } from '#compiler'; import type { Identifier, LabeledStatement, Program } from 'estree'; -import type Stylesheet from './2-analyze/css/Stylesheet.js'; +import { Stylesheet } from './2-analyze/css/Stylesheet.js'; import type { Scope, ScopeRoot } from './scope.js'; export interface Js { diff --git a/packages/svelte/src/compiler/types/css.d.ts b/packages/svelte/src/compiler/types/css.d.ts index 49a3fc5143..7258e7748c 100644 --- a/packages/svelte/src/compiler/types/css.d.ts +++ b/packages/svelte/src/compiler/types/css.d.ts @@ -99,4 +99,4 @@ export interface Declaration extends BaseNode { } // for zimmerframe -export type Node = Style | Rule | Atrule; +export type Node = Style | Rule | Atrule | Declaration; diff --git a/packages/svelte/tests/css/samples/unknown-at-rule-with-following-rules/expected.css b/packages/svelte/tests/css/samples/unknown-at-rule-with-following-rules/expected.css index f75d9d89f2..a7bc48137b 100644 --- a/packages/svelte/tests/css/samples/unknown-at-rule-with-following-rules/expected.css +++ b/packages/svelte/tests/css/samples/unknown-at-rule-with-following-rules/expected.css @@ -1,5 +1,6 @@ div.svelte-xyz { @apply --funky-div; + color: red; } /* (empty) div.svelte-xyz { diff --git a/packages/svelte/tests/css/samples/unknown-at-rule-with-following-rules/input.svelte b/packages/svelte/tests/css/samples/unknown-at-rule-with-following-rules/input.svelte index 6b75d37e34..06a8726707 100644 --- a/packages/svelte/tests/css/samples/unknown-at-rule-with-following-rules/input.svelte +++ b/packages/svelte/tests/css/samples/unknown-at-rule-with-following-rules/input.svelte @@ -3,6 +3,7 @@ \ No newline at end of file +