chore: use zimmerframe for CSS analysis/transformation (#10482)

* fix type

* parse selectors properly the first time

* partial fix

* fix

* start moving CSS validation into analysis phase

* finish moving validation

* fix tests

* regenerate types

* start porting scoping logic etc

* move Style to Css.StyleSheet

* some encouraging progress

* more progress

* more progress

* fix a bunch of cases

* more fixes

* tweak

* almost there

* keyframes

* fix

* all CSS tests passing

* legacy stuff

* all tests passing

* delete old code

* regenerate types

* defer analysis, address TODO

* unused

* tidy up

* rename stuff

* read selectors before combinators

---------

Co-authored-by: Rich Harris <rich.harris@vercel.com>
pull/10479/head
Rich Harris 6 months ago committed by GitHub
parent f8ff2b6ea3
commit cec3540ac2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1,549 +0,0 @@
import MagicString from 'magic-string';
import { walk } from 'zimmerframe';
import { ComplexSelector } from './Selector.js';
import { hash } from './utils.js';
import { create_attribute } from '../phases/nodes.js'; // TODO move this
import { merge_with_preprocessor_map } from '../utils/mapped_code.js';
const regex_css_browser_prefix = /^-((webkit)|(moz)|(o)|(ms))-/;
const regex_name_boundary = /^[\s,;}]$/;
/**
* @param {string} name
* @returns {string}
*/
function remove_css_prefix(name) {
return name.replace(regex_css_browser_prefix, '');
}
/** @param {import('#compiler').Css.Atrule} node */
const is_keyframes_node = (node) => remove_css_prefix(node.name) === 'keyframes';
/**
*
* @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;
}
}
}
}
class Rule {
/** @type {ComplexSelector[]} */
selectors;
/** @type {import('#compiler').Css.Rule} */
node;
/** @type {Stylesheet | Atrule} */
parent;
/** @type {Declaration[]} */
declarations;
/**
* @param {import('#compiler').Css.Rule} node
* @param {any} stylesheet
* @param {Stylesheet | Atrule} parent
*/
constructor(node, stylesheet, parent) {
this.node = node;
this.parent = parent;
this.selectors = node.prelude.children.map((node) => new ComplexSelector(node, stylesheet));
this.declarations = /** @type {import('#compiler').Css.Declaration[]} */ (
node.block.children
).map((node) => new Declaration(node));
}
/** @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node */
apply(node) {
this.selectors.forEach((selector) => selector.apply(node)); // TODO move the logic in here?
}
/** @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;
}
for (const selector of this.selectors) {
if (selector.used) return true;
}
return false;
}
/**
* @param {import('magic-string').default} code
* @param {string} id
* @param {Map<string, string>} keyframes
*/
transform(code, id, keyframes) {
if (this.parent instanceof Atrule && is_keyframes_node(this.parent.node)) {
return;
}
const modifier = `.${id}`;
this.selectors.forEach((selector) => selector.transform(code, modifier));
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) => {
if (!selector.used) handler(selector);
});
}
/**
* @param {MagicString} code
* @param {boolean} dev
*/
prune(code, dev) {
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 (!dev && this.is_empty()) {
code.prependRight(this.node.start, '/* (empty) ');
code.appendLeft(this.node.end, '*/');
escape_comment_close(this.node, code);
return;
}
const used = this.selectors.filter((s) => s.used);
if (used.length === 0) {
code.prependRight(this.node.start, '/* (unused) ');
code.appendLeft(this.node.end, '*/');
escape_comment_close(this.node, code);
return;
}
if (used.length < this.selectors.length) {
let pruning = false;
let last = this.selectors[0].node.start;
for (let i = 0; i < this.selectors.length; i += 1) {
const selector = this.selectors[i];
if (selector.used === pruning) {
if (pruning) {
let i = selector.node.start;
while (code.original[i] !== ',') i--;
code.overwrite(i, i + 1, '*/');
} else {
if (i === 0) {
code.prependRight(selector.node.start, '/* (unused) ');
} else {
code.overwrite(last, selector.node.start, ' /* (unused) ');
}
}
pruning = !pruning;
}
last = selector.node.end;
}
if (pruning) {
code.appendLeft(last, '*/');
}
}
}
}
class Declaration {
/** @type {import('#compiler').Css.Declaration} */
node;
/** @param {import('#compiler').Css.Declaration} node */
constructor(node) {
this.node = node;
}
/**
* @param {import('magic-string').default} code
* @param {Map<string, string>} keyframes
*/
transform(code, keyframes) {
const property = this.node.property && remove_css_prefix(this.node.property.toLowerCase());
if (property === 'animation' || property === 'animation-name') {
let index = this.node.start + this.node.property.length + 1;
let name = '';
while (index < code.original.length) {
const character = code.original[index];
if (regex_name_boundary.test(character)) {
const keyframe = keyframes.get(name);
if (keyframe) {
code.update(index - name.length, index, keyframe);
}
if (character === ';' || character === '}') {
break;
}
name = '';
} else {
name += character;
}
index++;
}
}
}
}
class Atrule {
/** @type {import('#compiler').Css.Atrule} */
node;
/** @type {Array<Atrule | Rule>} */
children;
/** @type {Declaration[]} */
declarations;
/** @param {import('#compiler').Css.Atrule} node */
constructor(node) {
this.node = node;
this.children = [];
this.declarations = [];
}
/** @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node */
apply(node) {
if (
this.node.name === 'container' ||
this.node.name === 'media' ||
this.node.name === 'supports' ||
this.node.name === 'layer'
) {
this.children.forEach((child) => {
child.apply(node);
});
} else if (is_keyframes_node(this.node)) {
/** @type {Rule[]} */ (this.children).forEach((rule) => {
rule.selectors.forEach((selector) => {
selector.used = true;
});
});
}
}
is_empty() {
return false; // TODO
}
is_used() {
return true; // TODO
}
/**
* @param {import('magic-string').default} code
* @param {string} id
* @param {Map<string, string>} keyframes
*/
transform(code, id, keyframes) {
if (is_keyframes_node(this.node)) {
let start = this.node.start + this.node.name.length + 1;
while (code.original[start] === ' ') start += 1;
let end = start;
while (code.original[end] !== '{' && code.original[end] !== ' ') end += 1;
if (this.node.prelude.startsWith('-global-')) {
code.remove(start, start + 8);
/** @type {Rule[]} */ (this.children).forEach((rule) => {
rule.selectors.forEach((selector) => {
selector.used = true;
});
});
} else {
const keyframe = /** @type {string} */ (keyframes.get(this.node.prelude));
code.update(start, end, keyframe);
}
}
this.children.forEach((child) => {
child.transform(code, id, keyframes);
});
}
/** @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;
this.children.forEach((child) => {
child.warn_on_unused_selector(handler);
});
}
/**
* @param {MagicString} code
* @param {boolean} dev
*/
prune(code, dev) {
// TODO prune children
}
}
export class Stylesheet {
/** @type {import('#compiler').Style | null} */
ast;
/** @type {string} Path of Svelte file the CSS is in */
filename;
/** @type {boolean} */
has_styles;
/** @type {string} */
id;
/** @type {Array<Rule | Atrule>} */
children = [];
/** @type {Map<string, string>} */
keyframes = new Map();
/** @type {Set<import('#compiler').RegularElement | import('#compiler').SvelteElement>} */
nodes_with_css_class = new Set();
/**
* @param {{
* ast: import('#compiler').Style | null;
* filename: string;
* component_name: string;
* get_css_hash: import('#compiler').CssHashGetter;
* }} params
*/
constructor({ ast, component_name, filename, get_css_hash }) {
this.ast = ast;
this.filename = filename;
if (!ast || ast.children.length === 0) {
this.has_styles = false;
this.id = '';
return;
}
this.id = get_css_hash({
filename,
name: component_name,
css: ast.content.styles,
hash
});
this.has_styles = true;
const state = {
/** @type {Stylesheet | Atrule | Rule} */
current: this
};
walk(/** @type {import('#compiler').Css.Node} */ (ast), state, {
Atrule: (node, context) => {
const atrule = new Atrule(node);
if (is_keyframes_node(node)) {
if (!node.prelude.startsWith('-global-')) {
this.keyframes.set(node.prelude, `${this.id}-${node.prelude}`);
}
}
// @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) => {
// @ts-expect-error temporary, until nesting is implemented
const rule = new Rule(node, this, context.state.current);
// @ts-expect-error temporary, until nesting is implemented
context.state.current.children.push(rule);
context.next({ current: rule });
}
});
}
/** @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node */
apply(node) {
if (!this.has_styles) return;
for (let i = 0; i < this.children.length; i += 1) {
const child = this.children[i];
child.apply(node);
}
}
/** @param {boolean} is_dom_mode */
reify(is_dom_mode) {
nodes: for (const node of this.nodes_with_css_class) {
// Dynamic elements in dom mode always use spread for attributes and therefore shouldn't have a class attribute added to them
// TODO this happens during the analysis phase, which shouldn't know anything about client vs server
if (node.type === 'SvelteElement' && is_dom_mode) continue;
/** @type {import('#compiler').Attribute | undefined} */
let class_attribute = undefined;
for (const attribute of node.attributes) {
if (attribute.type === 'SpreadAttribute') {
// The spread method appends the hash to the end of the class attribute on its own
continue nodes;
}
if (attribute.type !== 'Attribute') continue;
if (attribute.name.toLowerCase() !== 'class') continue;
class_attribute = attribute;
}
if (class_attribute && class_attribute.value !== true) {
const chunks = class_attribute.value;
if (chunks.length === 1 && chunks[0].type === 'Text') {
chunks[0].data += ` ${this.id}`;
} else {
chunks.push({
type: 'Text',
data: ` ${this.id}`,
raw: ` ${this.id}`,
start: -1,
end: -1,
parent: null
});
}
} else {
node.attributes.push(
create_attribute('class', -1, -1, [
{ type: 'Text', data: this.id, raw: this.id, parent: null, start: -1, end: -1 }
])
);
}
}
}
/**
* @param {string} source
* @param {import('#compiler').ValidatedCompileOptions} options
*/
render(source, options) {
// TODO neaten this up
if (!this.ast) throw new Error('Unexpected error');
const code = new MagicString(source);
// Generate source mappings for the style sheet nodes we have.
// Note that resolution is a bit more coarse than in Svelte 4 because
// our own CSS AST is not as detailed with regards to the node values.
walk(/** @type {import('#compiler').Css.Node} */ (this.ast), null, {
_: (node, { next }) => {
code.addSourcemapLocation(node.start);
code.addSourcemapLocation(node.end);
next();
}
});
for (const child of this.children) {
child.transform(code, this.id, this.keyframes);
}
code.remove(0, this.ast.content.start);
for (const child of this.children) {
child.prune(code, options.dev);
}
code.remove(/** @type {number} */ (this.ast.content.end), source.length);
const css = {
code: code.toString(),
map: code.generateMap({
// include source content; makes it easier/more robust looking up the source map code
includeContent: true,
// generateMap takes care of calculating source relative to file
source: this.filename,
file: options.cssOutputFilename || this.filename
})
};
merge_with_preprocessor_map(css, options, css.map.sources[0]);
if (options.dev && options.css === 'injected' && css.code) {
css.code += `\n/*# sourceMappingURL=${css.map.toUrl()} */`;
}
return css;
}
/** @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
// ? []
// : extract_ignores_above_position(this.ast.css.start, this.ast.html.children);
// analysis.push_ignores(ignores);
// this.children.forEach((child) => {
// child.warn_on_unused_selector((selector) => {
// analysis.warn(selector.node, {
// code: 'css-unused-selector',
// message: `Unused CSS selector "${this.source.slice(
// selector.node.start,
// selector.node.end
// )}"`
// });
// });
// });
// analysis.pop_ignores();
}
}

@ -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',
@ -389,6 +400,13 @@ export function convert(source, ast) {
SpreadAttribute(node) {
return { ...node, type: 'Spread' };
},
StyleSheet(node, context) {
return {
...node,
...context.next(),
type: 'Style'
};
},
SvelteBody(node, { visit }) {
return {
type: 'Body',

@ -18,7 +18,7 @@ const REGEX_HTML_COMMENT_CLOSE = /-->/;
* @param {import('../index.js').Parser} parser
* @param {number} start
* @param {Array<import('#compiler').Attribute | import('#compiler').SpreadAttribute | import('#compiler').Directive>} 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
}
};
}
@ -187,42 +186,66 @@ function read_selector_list(parser, inside_pseudo_class = false) {
function read_selector(parser, inside_pseudo_class = false) {
const list_start = parser.index;
/** @type {Array<import('#compiler').Css.SimpleSelector | import('#compiler').Css.Combinator>} */
/** @type {import('#compiler').Css.RelativeSelector[]} */
const children = [];
/**
* @param {import('#compiler').Css.Combinator | null} combinator
* @param {number} start
* @returns {import('#compiler').Css.RelativeSelector}
*/
function create_selector(combinator, start) {
return {
type: 'RelativeSelector',
combinator,
selectors: [],
start,
end: -1,
metadata: {
is_global: false,
is_host: false,
is_root: false,
scoped: false
}
};
}
/** @type {import('#compiler').Css.RelativeSelector} */
let relative_selector = create_selector(null, parser.index);
while (parser.index < parser.template.length) {
const start = parser.index;
let 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 +270,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 +299,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,37 +311,28 @@ 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,
end: parser.index
});
} else {
} else if (!parser.match_regex(REGEX_COMBINATOR)) {
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,
@ -330,29 +344,85 @@ 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;
relative_selector.end = index;
children.push(relative_selector);
return {
type: 'Selector',
type: 'ComplexSelector',
start: list_start,
end: index,
children
children,
metadata: {
used: false
}
};
}
if (parser.index !== index && !parser.match_regex(REGEX_COMBINATOR)) {
children.push({
type: 'Combinator',
name: ' ',
start: index,
end: parser.index
});
parser.index = index;
const combinator = read_combinator(parser);
if (combinator) {
if (relative_selector.selectors.length === 0) {
if (!inside_pseudo_class) {
error(start, 'invalid-css-selector');
}
} else {
relative_selector.end = index;
children.push(relative_selector);
}
// ...and start a new one
relative_selector = create_selector(combinator, combinator.start);
parser.allow_whitespace();
if (parser.match(',') || (inside_pseudo_class ? parser.match(')') : parser.match('{'))) {
error(parser.index, 'invalid-css-selector');
}
}
}
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}

@ -0,0 +1,124 @@
import { error } from '../../../errors.js';
import { is_keyframes_node } from '../../css.js';
import { merge } from '../../visitors.js';
/**
* @typedef {import('zimmerframe').Visitors<
* import('#compiler').Css.Node,
* NonNullable<import('../../types.js').ComponentAnalysis['css']>
* >} Visitors
*/
/** @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 {Visitors} */
const analysis = {
Atrule(node, context) {
if (is_keyframes_node(node)) {
if (!node.prelude.startsWith('-global-')) {
context.state.keyframes.push(node.prelude);
}
}
},
ComplexSelector(node, context) {
context.next(); // analyse relevant selectors first
node.metadata.used = node.children.every(
({ metadata }) => metadata.is_global || metadata.is_host || metadata.is_root
);
},
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 = !!node.selectors.find(
(child) => child.type === 'PseudoClassSelector' && child.name === 'root'
);
context.next();
}
};
/** @type {Visitors} */
const validation = {
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');
}
}
}
}
// 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');
}
}
}
}
}
};
export const css_visitors = merge(analysis, validation);

@ -1,7 +1,14 @@
import { walk } from 'zimmerframe';
import { get_possible_values } from './utils.js';
import { regex_starts_with_whitespace, regex_ends_with_whitespace } from '../phases/patterns.js';
import { error } from '../errors.js';
import { Stylesheet } from './Stylesheet.js';
import { regex_ends_with_whitespace, regex_starts_with_whitespace } from '../../patterns.js';
/**
* @typedef {{
* stylesheet: import('#compiler').Css.StyleSheet;
* 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';
@ -12,216 +19,59 @@ const NodeExist = /** @type {const} */ ({
Definitely: 1
});
/** @typedef {typeof NodeExist[keyof typeof NodeExist]} NodeExistsValue */
const whitelist_attribute_selector = new Map([
['details', new Set(['open'])],
['dialog', new Set(['open'])]
['details', ['open']],
['dialog', ['open']]
]);
export class ComplexSelector {
/** @type {import('#compiler').Css.ComplexSelector} */
node;
/** @type {import('./Stylesheet.js').Stylesheet} */
stylesheet;
/** @type {RelativeSelector[]} */
relative_selectors;
/**
* The `relative_selectors`, minus any trailing global selectors
* (which includes `:root` and `:host`) since we ignore these
* when determining if a selector is used.
* @type {RelativeSelector[]}
*/
local_relative_selectors;
used = false;
/**
* @param {import('#compiler').Css.ComplexSelector} node
* @param {import('./Stylesheet.js').Stylesheet} stylesheet
*/
constructor(node, stylesheet) {
this.node = node;
this.stylesheet = stylesheet;
this.relative_selectors = group_selectors(node);
// take trailing :global(...) selectors out of consideration
const i = this.relative_selectors.findLastIndex((s) => !s.can_ignore());
this.local_relative_selectors = this.relative_selectors.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_relative_selectors.length === 0;
}
/** @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node */
apply(node) {
if (apply_selector(this.local_relative_selectors.slice(), node, this.stylesheet)) {
this.used = true;
}
}
/**
* @param {import('magic-string').default} code
* @param {string} modifier
*/
transform(code, modifier) {
/** @param {import('#compiler').Css.SimpleSelector} selector */
function remove_global_pseudo_class(selector) {
code
.remove(selector.start, selector.start + ':global('.length)
.remove(selector.end - 1, selector.end);
}
/**
* @param {RelativeSelector} relative_selector
* @param {string} modifier
*/
function encapsulate_block(relative_selector, modifier) {
for (const selector of relative_selector.selectors) {
if (selector.type === 'PseudoClassSelector' && selector.name === 'global') {
remove_global_pseudo_class(selector);
}
}
let i = relative_selector.selectors.length;
while (i--) {
const selector = relative_selector.selectors[i];
if (selector.type === 'PseudoElementSelector' || selector.type === 'PseudoClassSelector') {
if (selector.name !== 'root' && selector.name !== 'host') {
if (i === 0) code.prependRight(selector.start, modifier);
}
continue;
}
if (selector.type === 'TypeSelector' && selector.name === '*') {
code.update(selector.start, selector.end, modifier);
} else {
code.appendLeft(selector.end, modifier);
}
/**
*
* @param {import('#compiler').Css.StyleSheet} stylesheet
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} element
*/
export function prune(stylesheet, element) {
/** @type {State} */
const state = { stylesheet, element };
break;
}
}
walk(stylesheet, state, visitors);
}
let first = true;
for (const relative_selector of this.relative_selectors) {
if (relative_selector.is_global) {
remove_global_pseudo_class(relative_selector.selectors[0]);
}
/** @type {import('zimmerframe').Visitors<import('#compiler').Css.Node, State>} */
const visitors = {
ComplexSelector(node, context) {
context.next();
if (relative_selector.should_encapsulate) {
// for the first occurrence, we use a classname selector, so that every
// encapsulated selector gets a +0-1-0 specificity bump. thereafter,
// we use a `:where` selector, which does not affect specificity
encapsulate_block(relative_selector, first ? modifier : `:where(${modifier})`);
first = false;
}
}
}
const i = node.children.findLastIndex((child) => {
return !child.metadata.is_global && !child.metadata.is_host && !child.metadata.is_root;
});
/** @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);
}
const relative_selectors = node.children.slice(0, i + 1);
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');
}
if (apply_selector(relative_selectors, context.state.element, context.state.stylesheet)) {
node.metadata.used = true;
}
},
RelativeSelector(node, context) {
// for now, don't visit children (i.e. inside `:foo(...)`)
// this will likely change when we implement `:is(...)` etc
}
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?.type === 'TypeSelector' &&
!/[.:#]/.test(child.name[0]) &&
(i !== 0 ||
relative_selector.selectors
.slice(1)
.some(
(s) => s.type !== 'PseudoElementSelector' && s.type !== 'PseudoClassSelector'
))
) {
error(selector, 'invalid-css-global-selector-list');
}
}
}
}
}
}
};
/**
* @param {RelativeSelector[]} relative_selectors
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement | null} node
* @param {Stylesheet} stylesheet
* @param {import('#compiler').Css.RelativeSelector[]} relative_selectors
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement | null} element
* @param {import('#compiler').Css.StyleSheet} stylesheet
* @returns {boolean}
*/
function apply_selector(relative_selectors, node, stylesheet) {
function apply_selector(relative_selectors, element, stylesheet) {
if (!element) {
return relative_selectors.every(({ metadata }) => metadata.is_global || metadata.is_host);
}
const relative_selector = relative_selectors.pop();
if (!relative_selector) return false;
if (!node) {
return (
(relative_selector.is_global &&
relative_selectors.every((relative_selector) => relative_selector.is_global)) ||
(relative_selector.is_host && relative_selectors.length === 0)
);
}
const applies = block_might_apply_to_node(relative_selector, node);
const applies = relative_selector_might_apply_to_node(relative_selector, element);
if (applies === NO_MATCH) {
return false;
@ -230,17 +80,17 @@ function apply_selector(relative_selectors, node, stylesheet) {
/**
* Mark both the compound selector and the node it selects as encapsulated,
* for transformation in a later step
* @param {RelativeSelector} relative_selector
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node
* @param {import('#compiler').Css.RelativeSelector} relative_selector
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} element
*/
function mark(relative_selector, node) {
relative_selector.should_encapsulate = true;
stylesheet.nodes_with_css_class.add(node);
function mark(relative_selector, element) {
relative_selector.metadata.scoped = true;
element.metadata.scoped = true;
return true;
}
if (applies === UNKNOWN_SELECTOR) {
return mark(relative_selector, node);
return mark(relative_selector, element);
}
if (relative_selector.combinator) {
@ -248,87 +98,99 @@ function apply_selector(relative_selectors, node, stylesheet) {
relative_selector.combinator.type === 'Combinator' &&
relative_selector.combinator.name === ' '
) {
for (const ancestor_block of relative_selectors) {
if (ancestor_block.is_global) {
for (const ancestor_selector of relative_selectors) {
if (ancestor_selector.metadata.is_global) {
continue;
}
if (ancestor_block.is_host) {
return mark(relative_selector, node);
if (ancestor_selector.metadata.is_host) {
return mark(relative_selector, element);
}
/** @type {import('#compiler').RegularElement | import('#compiler').SvelteElement | null} */
let parent = node;
let parent = element;
let matched = false;
while ((parent = get_element_parent(parent))) {
if (block_might_apply_to_node(ancestor_block, parent) !== NO_MATCH) {
mark(ancestor_block, parent);
if (relative_selector_might_apply_to_node(ancestor_selector, parent) !== NO_MATCH) {
mark(ancestor_selector, parent);
matched = true;
}
}
if (matched) {
return mark(relative_selector, node);
return mark(relative_selector, element);
}
}
if (relative_selectors.every((relative_selector) => relative_selector.is_global)) {
return mark(relative_selector, node);
if (relative_selectors.every((relative_selector) => relative_selector.metadata.is_global)) {
return mark(relative_selector, element);
}
return false;
} else if (relative_selector.combinator.name === '>') {
}
if (relative_selector.combinator.name === '>') {
const has_global_parent = relative_selectors.every(
(relative_selector) => relative_selector.is_global
(relative_selector) => relative_selector.metadata.is_global
);
if (
has_global_parent ||
apply_selector(relative_selectors, get_element_parent(node), stylesheet)
apply_selector(relative_selectors, get_element_parent(element), stylesheet)
) {
return mark(relative_selector, node);
return mark(relative_selector, element);
}
return false;
} else if (
relative_selector.combinator.name === '+' ||
relative_selector.combinator.name === '~'
) {
}
if (relative_selector.combinator.name === '+' || relative_selector.combinator.name === '~') {
const siblings = get_possible_element_siblings(
node,
element,
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.is_global
(relative_selector) => relative_selector.metadata.is_global
);
if (has_global) {
if (siblings.size === 0 && get_element_parent(node) !== null) {
if (siblings.size === 0 && get_element_parent(element) !== null) {
return false;
}
return mark(relative_selector, node);
return mark(relative_selector, element);
}
for (const possible_sibling of siblings.keys()) {
if (apply_selector(relative_selectors.slice(), possible_sibling, stylesheet)) {
mark(relative_selector, node);
mark(relative_selector, element);
has_match = true;
}
}
return has_match;
}
// TODO other combinators
return mark(relative_selector, node);
return mark(relative_selector, element);
}
return mark(relative_selector, node);
return mark(relative_selector, element);
}
const regex_backslash_and_following_character = /\\(.)/g;
/**
* @param {RelativeSelector} relative_selector
* @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.is_host || relative_selector.is_root) return NO_MATCH;
function relative_selector_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--) {
@ -356,7 +218,7 @@ function block_might_apply_to_node(relative_selector, node) {
if (selector.type === 'AttributeSelector') {
const whitelisted = whitelist_attribute_selector.get(node.name.toLowerCase());
if (
!whitelisted?.has(selector.name.toLowerCase()) &&
!whitelisted?.includes(selector.name.toLowerCase()) &&
!attribute_matches(
node,
selector.name,
@ -802,77 +664,3 @@ function loop_child(children, adjacent_only) {
}
return result;
}
/**
* Represents a compound selector (aka an array of simple selectors) plus
* a preceding combinator (if not the first in the list). Given this...
*
* ```css
* .a + .b.c {...}
* ```
*
* ...both `.a` and `+ .b.c` are relative selectors.
* Combined, they are a complex selector.
*/
class RelativeSelector {
/** @type {import('#compiler').Css.Combinator | null} */
combinator;
/** @type {import('#compiler').Css.SimpleSelector[]} */
selectors = [];
is_host = false;
is_root = false;
should_encapsulate = false;
start = -1;
end = -1;
/** @param {import('#compiler').Css.Combinator | null} combinator */
constructor(combinator) {
this.combinator = combinator;
}
/** @param {import('#compiler').Css.SimpleSelector} selector */
add(selector) {
if (this.selectors.length === 0) {
this.start = selector.start;
this.is_host = selector.type === 'PseudoClassSelector' && selector.name === 'host';
}
this.is_root =
this.is_root || (selector.type === 'PseudoClassSelector' && selector.name === 'root');
this.selectors.push(selector);
this.end = selector.end;
}
can_ignore() {
return this.is_global || this.is_host || this.is_root;
}
get is_global() {
return (
this.selectors.length >= 1 &&
this.selectors[0].type === 'PseudoClassSelector' &&
this.selectors[0].name === 'global' &&
this.selectors.every(
(selector) =>
selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector'
)
);
}
}
/** @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);
}
});
return relative_selectors;
}

@ -1,18 +1,3 @@
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 = {};
/**

@ -13,7 +13,6 @@ 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 { 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';
@ -21,6 +20,9 @@ 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 { css_visitors } from './css/css-analyze.js';
import { prune } from './css/css-prune.js';
import { hash } from './utils.js';
/**
* @param {import('#compiler').Script | null} script
@ -340,13 +342,6 @@ export function analyze_component(root, options) {
instance,
template,
elements: [],
stylesheet: new Stylesheet({
ast: root.css,
// TODO are any of these necessary or can we just pass in the whole `analysis` object later?
filename: options.filename || 'input.svelte',
component_name,
get_css_hash: options.cssHash
}),
runes,
immutable: runes || options.immutable,
exports: [],
@ -360,7 +355,19 @@ export function analyze_component(root, options) {
reactive_statements: new Map(),
binding_groups: new Map(),
slot_names: new Set(),
warnings
warnings,
css: {
ast: root.css,
hash: root.css
? options.cssHash({
css: root.css.content.styles,
filename: options.filename ?? '<unknown>',
name: component_name,
hash
})
: '',
keyframes: []
}
};
if (analysis.runes) {
@ -452,13 +459,68 @@ export function analyze_component(root, options) {
}
}
analysis.stylesheet.validate(analysis);
if (analysis.css.ast) {
// validate
walk(analysis.css.ast, analysis.css, css_visitors);
for (const element of analysis.elements) {
analysis.stylesheet.apply(element);
}
// mark nodes as scoped/unused/empty etc
for (const element of analysis.elements) {
prune(analysis.css.ast, element);
}
outer: for (const element of analysis.elements) {
if (element.metadata.scoped) {
// Dynamic elements in dom mode always use spread for attributes and therefore shouldn't have a class attribute added to them
// TODO this happens during the analysis phase, which shouldn't know anything about client vs server
if (element.type === 'SvelteElement' && options.generate === 'client') continue;
/** @type {import('#compiler').Attribute | undefined} */
let class_attribute = undefined;
for (const attribute of element.attributes) {
if (attribute.type === 'SpreadAttribute') {
// The spread method appends the hash to the end of the class attribute on its own
continue outer;
}
analysis.stylesheet.reify(options.generate === 'client');
if (attribute.type !== 'Attribute') continue;
if (attribute.name.toLowerCase() !== 'class') continue;
class_attribute = attribute;
}
if (class_attribute && class_attribute.value !== true) {
const chunks = class_attribute.value;
if (chunks.length === 1 && chunks[0].type === 'Text') {
chunks[0].data += ` ${analysis.css.hash}`;
} else {
chunks.push({
type: 'Text',
data: ` ${analysis.css.hash}`,
raw: ` ${analysis.css.hash}`,
start: -1,
end: -1,
parent: null
});
}
} else {
element.attributes.push(
create_attribute('class', -1, -1, [
{
type: 'Text',
data: analysis.css.hash,
raw: analysis.css.hash,
parent: null,
start: -1,
end: -1
}
])
);
}
}
}
}
// TODO
// analysis.stylesheet.warn_on_unused_selectors(analysis);

@ -0,0 +1,14 @@
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);
}

@ -8,6 +8,7 @@ import { javascript_visitors } from './visitors/javascript.js';
import { javascript_visitors_runes } from './visitors/javascript-runes.js';
import { javascript_visitors_legacy } from './visitors/javascript-legacy.js';
import { is_state_source, serialize_get_binding } from './utils.js';
import { render_stylesheet } from '../css/index.js';
/**
* This function ensures visitor sets don't accidentally clobber each other
@ -271,15 +272,15 @@ export function client_component(source, analysis, options) {
]);
const append_styles =
analysis.inject_styles && analysis.stylesheet.has_styles
analysis.inject_styles && analysis.css.ast
? () =>
component_block.body.push(
b.stmt(
b.call(
'$.append_styles',
b.id('$$anchor'),
b.literal(analysis.stylesheet.id),
b.literal(analysis.stylesheet.render(source, options).code)
b.literal(analysis.css.hash),
b.literal(render_stylesheet(source, analysis, options).code)
)
)
)

@ -322,7 +322,7 @@ function serialize_element_spread_attributes(attributes, context, element, eleme
element_id,
b.thunk(b.array(values)),
lowercase_attributes,
b.literal(context.state.analysis.stylesheet.id)
b.literal(context.state.analysis.css.hash)
)
);
@ -345,7 +345,7 @@ function serialize_element_spread_attributes(attributes, context, element, eleme
b.id(id),
b.array(values),
lowercase_attributes,
b.literal(context.state.analysis.stylesheet.id)
b.literal(context.state.analysis.css.hash)
)
)
)
@ -364,9 +364,9 @@ function serialize_element_spread_attributes(attributes, context, element, eleme
*/
function serialize_dynamic_element_attributes(attributes, context, element_id) {
if (attributes.length === 0) {
if (context.state.analysis.stylesheet.id) {
if (context.state.analysis.css.hash) {
context.state.init.push(
b.stmt(b.call('$.class_name', element_id, b.literal(context.state.analysis.stylesheet.id)))
b.stmt(b.call('$.class_name', element_id, b.literal(context.state.analysis.css.hash)))
);
}
return false;
@ -399,7 +399,7 @@ function serialize_dynamic_element_attributes(attributes, context, element_id) {
'$.spread_dynamic_element_attributes_effect',
element_id,
b.thunk(b.array(values)),
b.literal(context.state.analysis.stylesheet.id)
b.literal(context.state.analysis.css.hash)
)
);
@ -420,7 +420,7 @@ function serialize_dynamic_element_attributes(attributes, context, element_id) {
element_id,
b.id(id),
b.array(values),
b.literal(context.state.analysis.stylesheet.id)
b.literal(context.state.analysis.css.hash)
)
)
)
@ -434,7 +434,7 @@ function serialize_dynamic_element_attributes(attributes, context, element_id) {
element_id,
b.literal(null),
b.array(values),
b.literal(context.state.analysis.stylesheet.id)
b.literal(context.state.analysis.css.hash)
)
)
);

@ -0,0 +1,252 @@
import MagicString from 'magic-string';
import { walk } from 'zimmerframe';
import { is_keyframes_node, regex_css_name_boundary, remove_css_prefix } from '../../css.js';
import { merge_with_preprocessor_map } from '../../../utils/mapped_code.js';
/** @typedef {{ code: MagicString, dev: boolean, hash: string, selector: string, keyframes: string[] }} State */
/**
*
* @param {string} source
* @param {import('../../types.js').ComponentAnalysis} analysis
* @param {import('#compiler').ValidatedCompileOptions} options
*/
export function render_stylesheet(source, analysis, options) {
const code = new MagicString(source);
/** @type {State} */
const state = {
code,
dev: options.dev,
hash: analysis.css.hash,
selector: `.${analysis.css.hash}`,
keyframes: analysis.css.keyframes
};
const ast = /** @type {import('#compiler').Css.StyleSheet} */ (analysis.css.ast);
walk(/** @type {import('#compiler').Css.Node} */ (ast), state, visitors);
code.remove(0, ast.content.start);
code.remove(/** @type {number} */ (ast.content.end), source.length);
const css = {
code: code.toString(),
map: code.generateMap({
// include source content; makes it easier/more robust looking up the source map code
includeContent: true,
// generateMap takes care of calculating source relative to file
source: options.filename,
file: options.cssOutputFilename || options.filename
})
};
merge_with_preprocessor_map(css, options, css.map.sources[0]);
if (options.dev && options.css === 'injected' && css.code) {
css.code += `\n/*# sourceMappingURL=${css.map.toUrl()} */`;
}
return css;
}
/** @type {import('zimmerframe').Visitors<import('#compiler').Css.Node, State>} */
const visitors = {
_: (node, context) => {
context.state.code.addSourcemapLocation(node.start);
context.state.code.addSourcemapLocation(node.end);
context.next();
},
Atrule(node, { state, next }) {
if (is_keyframes_node(node)) {
let start = node.start + node.name.length + 1;
while (state.code.original[start] === ' ') start += 1;
let end = start;
while (state.code.original[end] !== '{' && state.code.original[end] !== ' ') end += 1;
if (node.prelude.startsWith('-global-')) {
state.code.remove(start, start + 8);
} else {
state.code.prependRight(start, `${state.hash}-`);
}
return; // don't transform anything within
}
next();
},
Declaration(node, { state, next }) {
const property = node.property && remove_css_prefix(node.property.toLowerCase());
if (property === 'animation' || property === 'animation-name') {
let index = node.start + node.property.length + 1;
let name = '';
while (index < state.code.original.length) {
const character = state.code.original[index];
if (regex_css_name_boundary.test(character)) {
if (state.keyframes.includes(name)) {
state.code.prependRight(index - name.length, `${state.hash}-`);
}
if (character === ';' || character === '}') {
break;
}
name = '';
} else {
name += character;
}
index++;
}
}
},
Rule(node, { state, next }) {
// keep empty rules in dev, because it's convenient to
// see them in devtools
if (!state.dev && is_empty(node)) {
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();
},
ComplexSelector(node, context) {
/** @param {import('#compiler').Css.SimpleSelector} selector */
function remove_global_pseudo_class(selector) {
context.state.code
.remove(selector.start, selector.start + ':global('.length)
.remove(selector.end - 1, selector.end);
}
let first = true;
for (const relative_selector of node.children) {
if (relative_selector.metadata.is_global) {
remove_global_pseudo_class(relative_selector.selectors[0]);
}
if (relative_selector.metadata.scoped) {
// for the first occurrence, we use a classname selector, so that every
// encapsulated selector gets a +0-1-0 specificity bump. thereafter,
// we use a `:where` selector, which does not affect specificity
let modifier = context.state.selector;
if (!first) modifier = `:where(${modifier})`;
first = false;
// TODO err... can this happen?
for (const selector of relative_selector.selectors) {
if (selector.type === 'PseudoClassSelector' && selector.name === 'global') {
remove_global_pseudo_class(selector);
}
}
let i = relative_selector.selectors.length;
while (i--) {
const selector = relative_selector.selectors[i];
if (
selector.type === 'PseudoElementSelector' ||
selector.type === 'PseudoClassSelector'
) {
if (selector.name !== 'root' && selector.name !== 'host') {
if (i === 0) context.state.code.prependRight(selector.start, modifier);
}
continue;
}
if (selector.type === 'TypeSelector' && selector.name === '*') {
context.state.code.update(selector.start, selector.end, modifier);
} else {
context.state.code.appendLeft(selector.end, modifier);
}
break;
}
first = false;
}
}
context.next();
}
};
/** @param {import('#compiler').Css.Rule} rule */
function is_empty(rule) {
if (rule.block.children.length > 0) return false;
return true;
}
/**
*
* @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;
}
}
}
}

@ -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';
import { merge_with_preprocessor_map, get_source_name } from '../../utils/mapped_code.js';
/**
@ -51,8 +52,8 @@ export function transform_component(analysis, source, options) {
merge_with_preprocessor_map(js, options, js_source_name);
const css =
analysis.stylesheet.has_styles && !analysis.inject_styles
? analysis.stylesheet.render(source, options)
analysis.css.ast && !analysis.inject_styles
? render_stylesheet(source, analysis, options)
: null;
return {

@ -760,7 +760,7 @@ function serialize_element_spread_attributes(
b.array(values),
lowercase_attributes,
is_svg,
b.literal(context.state.analysis.stylesheet.id)
b.literal(context.state.analysis.css.hash)
];
if (style_directives.length > 0 || class_directives.length > 0) {

@ -0,0 +1,13 @@
const regex_css_browser_prefix = /^-((webkit)|(moz)|(o)|(ms))-/;
export const regex_css_name_boundary = /^[\s,;}]$/;
/**
* @param {string} name
* @returns {string}
*/
export function remove_css_prefix(name) {
return name.replace(regex_css_browser_prefix, '');
}
/** @param {import('#compiler').Css.Atrule} node */
export const is_keyframes_node = (node) => remove_css_prefix(node.name) === 'keyframes';

@ -1,5 +1,6 @@
import type {
Binding,
Css,
Fragment,
RegularElement,
SvelteElement,
@ -7,7 +8,6 @@ import type {
SvelteOptions
} from '#compiler';
import type { Identifier, LabeledStatement, Program } from 'estree';
import { Stylesheet } from '../css/Stylesheet.js';
import type { Scope, ScopeRoot } from './scope.js';
export interface Js {
@ -51,7 +51,6 @@ export interface ComponentAnalysis extends Analysis {
root: ScopeRoot;
instance: Js;
template: Template;
stylesheet: Stylesheet;
elements: Array<RegularElement | SvelteElement>;
runes: boolean;
exports: Array<{ name: string; alias: string | null }>;
@ -69,6 +68,11 @@ export interface ComponentAnalysis extends Analysis {
/** Identifiers that make up the `bind:group` expression -> internal group binding name */
binding_groups: Map<[key: string, bindings: Array<Binding | null>], Identifier>;
slot_names: Set<string>;
css: {
ast: Css.StyleSheet | null;
hash: string;
keyframes: string[];
};
}
declare module 'estree' {

@ -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<Atrule | Rule>;
content: {
start: number;
end: number;
styles: string;
};
}
export interface Atrule extends BaseNode {
type: 'Atrule';
name: string;
@ -24,8 +33,23 @@ export interface SelectorList extends BaseNode {
}
export interface ComplexSelector extends BaseNode {
type: 'Selector';
children: Array<SimpleSelector | Combinator>;
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;
scoped: boolean;
};
}
export interface TypeSelector extends BaseNode {
@ -99,4 +123,12 @@ export interface Declaration extends BaseNode {
}
// for zimmerframe
export type Node = Style | Rule | Atrule | Declaration;
export type Node =
| StyleSheet
| Rule
| Atrule
| ComplexSelector
| RelativeSelector
| Combinator
| SimpleSelector
| Declaration;

@ -10,7 +10,7 @@ import type { Location } from 'locate-character';
import type { SourceMap } from 'magic-string';
import type { Context } from 'zimmerframe';
import type { Scope } from '../phases/scope.js';
import * as Css from '../css/types.js';
import * as Css from './css.js';
import type { EachBlock, Namespace, SvelteNode } from './template.js';
/** The return value of `compile` from `svelte/compiler` */

@ -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,28 @@ export type LegacyElementLike =
| LegacyTitle
| LegacyWindow;
export interface LegacyStyle extends BaseNode {
type: 'Style';
attributes: any[];
content: {
start: number;
end: number;
styles: string;
};
children: any[];
}
export interface LegacySelector extends BaseNode {
type: 'Selector';
children: Array<Css.Combinator | Css.SimpleSelector>;
}
export type LegacyCssNode = LegacyStyle | LegacySelector;
export type LegacySvelteNode =
| LegacyConstTag
| LegacyElementLike
| LegacyAttributeLike
| LegacyAttributeShorthand
| LegacyCssNode
| Text;

@ -1,8 +1,7 @@
import type { Binding } from '#compiler';
import type { Binding, Css } from '#compiler';
import type {
ArrayExpression,
ArrowFunctionExpression,
ArrayPattern,
VariableDeclaration,
VariableDeclarator,
Expression,
@ -16,6 +15,7 @@ import type {
Program,
SpreadElement
} from 'estree';
import type { Atrule, Rule } from './css';
export interface BaseNode {
type: string;
@ -53,7 +53,7 @@ export interface Root extends BaseNode {
options: SvelteOptions | null;
fragment: Fragment;
/** The parsed `<style>` element, if exists */
css: Style | null;
css: Css.StyleSheet | null;
/** The parsed `<script>` element, if exists */
instance: Script | null;
/** The parsed `<script context="module">` element, if exists */
@ -291,6 +291,7 @@ export interface RegularElement extends BaseElement {
svg: boolean;
/** `true` if contains a SpreadAttribute */
has_spread: boolean;
scoped: boolean;
};
}
@ -320,6 +321,7 @@ export interface SvelteElement extends BaseElement {
* `null` means we can't know statically.
*/
svg: boolean | null;
scoped: boolean;
};
}
@ -460,7 +462,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';
@ -468,17 +470,6 @@ export interface Script extends BaseNode {
content: Program;
}
export interface Style extends BaseNode {
type: 'Style';
attributes: any[]; // TODO
children: any[]; // TODO add CSS node types
content: {
start: number;
end: number;
styles: string;
};
}
declare module 'estree' {
export interface BaseNode {
/** Added by the Svelte parser */

@ -1,3 +1,3 @@
/* (empty) .foo.svelte-xyz {
/* (empty) .foo {
/* empty *\/
}*/

@ -1,3 +1,3 @@
/* (unused) .foo .bar {
/* (unused) :global(.foo) .bar {
color: red;
}*/

@ -5,5 +5,5 @@
input + p.svelte-xyz { color: red; }
input ~ p.svelte-xyz { color: red; }
/* (unused) input + span { color: red; }*/
/* (unused) input ~ span { color: red; }*/
/* (unused) :global(input) + span { color: red; }*/
/* (unused) :global(input) ~ span { color: red; }*/

@ -3,6 +3,6 @@
color: red;
}
/* (empty) div.svelte-xyz {
/* (empty) div {
}*/

@ -1,6 +1,6 @@
{
"css": {
"type": "Style",
"type": "StyleSheet",
"start": 0,
"end": 386,
"attributes": [],
@ -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": 352,
"end": 355
}
]
}
]
}
]
},
},
"start": 306,
"end": 359
}
],
"start": 306,
"end": 359
}

@ -1,6 +1,6 @@
{
"css": {
"type": "Style",
"type": "StyleSheet",
"start": 36,
"end": 205,
"attributes": [],
@ -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
}

@ -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
}
}
]

@ -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
}
}
]

@ -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
}
}
]

@ -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
}
}
]

@ -963,11 +963,30 @@ declare module 'svelte/compiler' {
| LegacyTitle
| LegacyWindow;
interface LegacyStyle extends BaseNode_1 {
type: 'Style';
attributes: any[];
content: {
start: number;
end: number;
styles: string;
};
children: any[];
}
interface LegacySelector extends BaseNode_1 {
type: 'Selector';
children: Array<Css.Combinator | Css.SimpleSelector>;
}
type LegacyCssNode = LegacyStyle | LegacySelector;
type LegacySvelteNode =
| LegacyConstTag
| LegacyElementLike
| LegacyAttributeLike
| LegacyAttributeShorthand
| LegacyCssNode
| Text;
/**
* The preprocess function provides convenient hooks for arbitrarily transforming component source code.
@ -1081,7 +1100,7 @@ declare module 'svelte/compiler' {
options: SvelteOptions | null;
fragment: Fragment;
/** The parsed `<style>` element, if exists */
css: Style | null;
css: Css.StyleSheet | null;
/** The parsed `<script>` element, if exists */
instance: Script | null;
/** The parsed `<script context="module">` element, if exists */
@ -1319,6 +1338,7 @@ declare module 'svelte/compiler' {
svg: boolean;
/** `true` if contains a SpreadAttribute */
has_spread: boolean;
scoped: boolean;
};
}
@ -1348,6 +1368,7 @@ declare module 'svelte/compiler' {
* `null` means we can't know statically.
*/
svg: boolean | null;
scoped: boolean;
};
}
@ -1488,24 +1509,13 @@ declare module 'svelte/compiler' {
| Comment
| Block;
type SvelteNode = Node | TemplateNode | Fragment;
type SvelteNode = Node | TemplateNode | Fragment | Css.Node;
interface Script extends BaseNode {
type: 'Script';
context: string;
content: Program;
}
interface Style extends BaseNode {
type: 'Style';
attributes: any[]; // TODO
children: any[]; // TODO add CSS node types
content: {
start: number;
end: number;
styles: string;
};
}
/**
* The result of a preprocessor run. If the preprocessor does not return a result, it is assumed that the code is unchanged.
*/

Loading…
Cancel
Save