Merge pull request #680 from sveltejs/gh-679

Only apply svelte-123xyz attributes where necessary
pull/690/head
Rich Harris 8 years ago committed by GitHub
commit ef33466c12

@ -1,5 +1,7 @@
import MagicString, { Bundle } from 'magic-string'; import MagicString, { Bundle } from 'magic-string';
import { walk } from 'estree-walker'; import { walk } from 'estree-walker';
import { getLocator } from 'locate-character';
import getCodeFrame from '../utils/getCodeFrame';
import isReference from '../utils/isReference'; import isReference from '../utils/isReference';
import flattenReference from '../utils/flattenReference'; import flattenReference from '../utils/flattenReference';
import globalWhitelist from '../utils/globalWhitelist'; import globalWhitelist from '../utils/globalWhitelist';
@ -13,6 +15,8 @@ import annotateWithScopes from '../utils/annotateWithScopes';
import clone from '../utils/clone'; import clone from '../utils/clone';
import DomBlock from './dom/Block'; import DomBlock from './dom/Block';
import SsrBlock from './server-side-rendering/Block'; import SsrBlock from './server-side-rendering/Block';
import { walkRules } from '../utils/css';
import Selector from './Selector';
import { Node, Parsed, CompileOptions } from '../interfaces'; import { Node, Parsed, CompileOptions } from '../interfaces';
const test = typeof global !== 'undefined' && global.__svelte_test; const test = typeof global !== 'undefined' && global.__svelte_test;
@ -40,6 +44,8 @@ export default class Generator {
cssId: string; cssId: string;
usesRefs: boolean; usesRefs: boolean;
selectors: Selector[];
importedNames: Set<string>; importedNames: Set<string>;
aliases: Map<string, string>; aliases: Map<string, string>;
usedNames: Set<string>; usedNames: Set<string>;
@ -71,10 +77,24 @@ export default class Generator {
this.expectedProperties = new Set(); this.expectedProperties = new Set();
this.code = new MagicString(source); this.code = new MagicString(source);
this.usesRefs = false;
// styles
this.cascade = options.cascade !== false; // TODO remove this option in v2 this.cascade = options.cascade !== false; // TODO remove this option in v2
this.css = parsed.css ? processCss(parsed, this.code, this.cascade) : null;
this.cssId = parsed.css ? `svelte-${parsed.hash}` : ''; this.cssId = parsed.css ? `svelte-${parsed.hash}` : '';
this.usesRefs = false; this.selectors = [];
if (parsed.css) {
walkRules(parsed.css.children, node => {
node.selector.children.forEach((child: Node) => {
this.selectors.push(new Selector(child));
});
});
this.css = processCss(this, this.code, this.cascade);
} else {
this.css = null;
}
// allow compiler to deconflict user's `import { get } from 'whatever'` and // allow compiler to deconflict user's `import { get } from 'whatever'` and
// Svelte's builtin `import { get, ... } from 'svelte/shared.ts'`; // Svelte's builtin `import { get, ... } from 'svelte/shared.ts'`;
@ -211,6 +231,20 @@ export default class Generator {
}; };
} }
applyCss(node: Node, stack: Node[]) {
if (!this.cssId) return;
if (this.cascade) {
if (stack.length === 0) node._needsCssAttribute = true;
return;
}
for (let i = 0; i < this.selectors.length; i += 1) {
const selector = this.selectors[i];
selector.apply(node, stack);
}
}
findDependencies( findDependencies(
contextDependencies: Map<string, string[]>, contextDependencies: Map<string, string[]>,
indexes: Map<string, string>, indexes: Map<string, string>,
@ -590,4 +624,31 @@ export default class Generator {
this.namespace = namespace; this.namespace = namespace;
this.templateProperties = templateProperties; this.templateProperties = templateProperties;
} }
warnOnUnusedSelectors() {
if (this.cascade) return;
let locator;
this.selectors.forEach((selector: Selector) => {
if (!selector.used) {
const pos = selector.node.start;
if (!locator) locator = getLocator(this.source);
const { line, column } = locator(pos);
const frame = getCodeFrame(this.source, line, column);
const message = `Unused CSS selector`;
this.options.onwarn({
message,
frame,
loc: { line: line + 1, column },
pos,
filename: this.options.filename,
toString: () => `${message} (${line + 1}:${column})\n${frame}`,
});
}
});
}
} }

@ -0,0 +1,180 @@
import MagicString from 'magic-string';
import { groupSelectors, isGlobalSelector, walkRules } from '../utils/css';
import { Node } from '../interfaces';
export default class Selector {
node: Node;
blocks: any; // TODO
parts: Node[];
used: boolean;
constructor(node: Node) {
this.node = node;
this.blocks = groupSelectors(this.node);
// take trailing :global(...) selectors out of consideration
let i = node.children.length;
while (i > 2) {
const last = node.children[i-1];
const penultimate = node.children[i-2];
if (last.type === 'PseudoClassSelector' && last.name === 'global') {
i -= 2;
} else {
break;
}
}
this.parts = node.children.slice(0, i);
this.used = this.blocks[0].global;
}
apply(node: Node, stack: Node[]) {
const applies = selectorAppliesTo(this.parts, node, stack.slice());
if (applies) {
this.used = true;
// add svelte-123xyz attribute to outermost and innermost
// elements — no need to add it to intermediate elements
node._needsCssAttribute = true;
if (stack[0] && this.node.children.find(isDescendantSelector)) stack[0]._needsCssAttribute = true;
}
}
transform(code: MagicString, attr: string) {
function encapsulateBlock(block) {
let i = block.selectors.length;
while (i--) {
const selector = block.selectors[i];
if (selector.type === 'PseudoElementSelector' || selector.type === 'PseudoClassSelector') continue;
if (selector.type === 'TypeSelector' && selector.name === '*') {
code.overwrite(selector.start, selector.end, attr);
} else {
code.appendLeft(selector.end, attr);
}
return;
}
}
this.blocks.forEach((block, i) => {
if (block.global) {
const selector = block.selectors[0];
const first = selector.children[0];
const last = selector.children[selector.children.length - 1];
code.remove(selector.start, first.start).remove(last.end, selector.end);
} else if (i === 0 || i === this.blocks.length - 1) {
encapsulateBlock(block);
}
});
}
}
function isDescendantSelector(selector: Node) {
return selector.type === 'WhiteSpace' || selector.type === 'Combinator';
}
function selectorAppliesTo(parts: Node[], node: Node, stack: Node[]): boolean {
let i = parts.length;
let j = stack.length;
while (i--) {
if (!node) {
return parts.every((part: Node) => {
return part.type === 'Combinator' || (part.type === 'PseudoClassSelector' && part.name === 'global');
});
}
const part = parts[i];
if (part.type === 'PseudoClassSelector' && part.name === 'global') {
// TODO shouldn't see this here... maybe we should enforce that :global(...)
// cannot be sandwiched between non-global selectors?
return false;
}
if (part.type === 'PseudoClassSelector' || part.type === 'PseudoElementSelector') {
continue;
}
if (part.type === 'ClassSelector') {
if (!attributeMatches(node, 'class', part.name, '~=', false)) return false;
}
else if (part.type === 'IdSelector') {
if (!attributeMatches(node, 'id', part.name, '=', false)) return false;
}
else if (part.type === 'AttributeSelector') {
if (!attributeMatches(node, part.name.name, part.value && unquote(part.value.value), part.operator, part.flags)) return false;
}
else if (part.type === 'TypeSelector') {
if (part.name === '*') return true;
if (node.name !== part.name) return false;
}
else if (part.type === 'WhiteSpace') {
parts = parts.slice(0, i);
while (stack.length) {
if (selectorAppliesTo(parts, stack.pop(), stack)) {
return true;
}
}
return false;
}
else if (part.type === 'Combinator') {
if (part.name === '>') {
return selectorAppliesTo(parts.slice(0, i), stack.pop(), stack);
}
// TODO other combinators
return true;
}
else {
// bail. TODO figure out what these could be
return true;
}
}
return true;
}
const operators = {
'=' : (value: string, flags: string) => new RegExp(`^${value}$`, flags),
'~=': (value: string, flags: string) => new RegExp(`\\b${value}\\b`, flags),
'|=': (value: string, flags: string) => new RegExp(`^${value}(-.+)?$`, flags),
'^=': (value: string, flags: string) => new RegExp(`^${value}`, flags),
'$=': (value: string, flags: string) => new RegExp(`${value}$`, flags),
'*=': (value: string, flags: string) => new RegExp(value, flags)
};
function attributeMatches(node: Node, name: string, expectedValue: string, operator: string, caseInsensitive: boolean) {
const attr = node.attributes.find((attr: Node) => attr.name === name);
if (!attr) return false;
if (attr.value === true) return operator === null;
if (isDynamic(attr.value)) return true;
const actualValue = attr.value[0].data;
const pattern = operators[operator](expectedValue, caseInsensitive ? 'i' : '');
return pattern.test(actualValue);
}
function isDynamic(value: Node) {
return value.length > 1 || value[0].type !== 'Text';
}
function unquote(str: string) {
if (str[0] === str[str.length - 1] && str[0] === "'" || str[0] === '"') {
return str.slice(1, str.length - 1);
}
}

@ -61,8 +61,10 @@ export default function dom(
const { block, state } = preprocess(generator, namespace, parsed.html); const { block, state } = preprocess(generator, namespace, parsed.html);
generator.warnOnUnusedSelectors();
parsed.html.children.forEach((node: Node) => { parsed.html.children.forEach((node: Node) => {
visit(generator, block, state, node); visit(generator, block, state, node, []);
}); });
const builder = new CodeBuilder(); const builder = new CodeBuilder();

@ -40,6 +40,7 @@ const preprocessors = {
block: Block, block: Block,
state: State, state: State,
node: Node, node: Node,
elementStack: Node[],
stripWhitespace: boolean stripWhitespace: boolean
) => { ) => {
const dependencies = block.findDependencies(node.expression); const dependencies = block.findDependencies(node.expression);
@ -55,6 +56,7 @@ const preprocessors = {
block: Block, block: Block,
state: State, state: State,
node: Node, node: Node,
elementStack: Node[],
stripWhitespace: boolean stripWhitespace: boolean
) => { ) => {
const dependencies = block.findDependencies(node.expression); const dependencies = block.findDependencies(node.expression);
@ -66,7 +68,14 @@ const preprocessors = {
node._state = getChildState(state, { basename, name }); node._state = getChildState(state, { basename, name });
}, },
Text: (generator: DomGenerator, block: Block, state: State, node: Node, stripWhitespace: boolean) => { Text: (
generator: DomGenerator,
block: Block,
state: State,
node: Node,
elementStack: Node[],
stripWhitespace: boolean
) => {
node._state = getChildState(state); node._state = getChildState(state);
if (!/\S/.test(node.data)) { if (!/\S/.test(node.data)) {
@ -83,6 +92,7 @@ const preprocessors = {
block: Block, block: Block,
state: State, state: State,
node: Node, node: Node,
elementStack: Node[],
stripWhitespace: boolean, stripWhitespace: boolean,
nextSibling: Node nextSibling: Node
) => { ) => {
@ -102,7 +112,7 @@ const preprocessors = {
node._state = getChildState(state); node._state = getChildState(state);
blocks.push(node._block); blocks.push(node._block);
preprocessChildren(generator, node._block, node._state, node, stripWhitespace, node); preprocessChildren(generator, node._block, node._state, node, elementStack, stripWhitespace, nextSibling);
if (node._block.dependencies.size > 0) { if (node._block.dependencies.size > 0) {
dynamic = true; dynamic = true;
@ -127,6 +137,7 @@ const preprocessors = {
node.else._block, node.else._block,
node.else._state, node.else._state,
node.else, node.else,
elementStack,
stripWhitespace, stripWhitespace,
nextSibling nextSibling
); );
@ -154,6 +165,7 @@ const preprocessors = {
block: Block, block: Block,
state: State, state: State,
node: Node, node: Node,
elementStack: Node[],
stripWhitespace: boolean, stripWhitespace: boolean,
nextSibling: Node nextSibling: Node
) => { ) => {
@ -202,7 +214,7 @@ const preprocessors = {
}); });
generator.blocks.push(node._block); generator.blocks.push(node._block);
preprocessChildren(generator, node._block, node._state, node, stripWhitespace, nextSibling); preprocessChildren(generator, node._block, node._state, node, elementStack, stripWhitespace, nextSibling);
block.addDependencies(node._block.dependencies); block.addDependencies(node._block.dependencies);
node._block.hasUpdateMethod = node._block.dependencies.size > 0; node._block.hasUpdateMethod = node._block.dependencies.size > 0;
@ -219,6 +231,7 @@ const preprocessors = {
node.else._block, node.else._block,
node.else._state, node.else._state,
node.else, node.else,
elementStack,
stripWhitespace, stripWhitespace,
nextSibling nextSibling
); );
@ -231,6 +244,7 @@ const preprocessors = {
block: Block, block: Block,
state: State, state: State,
node: Node, node: Node,
elementStack: Node[],
stripWhitespace: boolean, stripWhitespace: boolean,
nextSibling: Node nextSibling: Node
) => { ) => {
@ -315,6 +329,8 @@ const preprocessors = {
: state.namespace, : state.namespace,
allUsedContexts: [], allUsedContexts: [],
}); });
generator.applyCss(node, elementStack);
} }
if (node.children.length) { if (node.children.length) {
@ -328,12 +344,12 @@ const preprocessors = {
}); });
generator.blocks.push(node._block); generator.blocks.push(node._block);
preprocessChildren(generator, node._block, node._state, node, stripWhitespace, nextSibling); preprocessChildren(generator, node._block, node._state, node, elementStack, stripWhitespace, nextSibling);
block.addDependencies(node._block.dependencies); block.addDependencies(node._block.dependencies);
node._block.hasUpdateMethod = node._block.dependencies.size > 0; node._block.hasUpdateMethod = node._block.dependencies.size > 0;
} else { } else {
if (node.name === 'pre' || node.name === 'textarea') stripWhitespace = false; if (node.name === 'pre' || node.name === 'textarea') stripWhitespace = false;
preprocessChildren(generator, block, node._state, node, stripWhitespace, nextSibling); preprocessChildren(generator, block, node._state, node, elementStack.concat(node), stripWhitespace, nextSibling);
} }
} }
}, },
@ -344,6 +360,7 @@ function preprocessChildren(
block: Block, block: Block,
state: State, state: State,
node: Node, node: Node,
elementStack: Node[],
stripWhitespace: boolean, stripWhitespace: boolean,
nextSibling: Node nextSibling: Node
) { ) {
@ -373,7 +390,7 @@ function preprocessChildren(
cleaned.forEach((child: Node, i: number) => { cleaned.forEach((child: Node, i: number) => {
const preprocessor = preprocessors[child.type]; const preprocessor = preprocessors[child.type];
if (preprocessor) preprocessor(generator, block, state, child, stripWhitespace, cleaned[i + 1] || nextSibling); if (preprocessor) preprocessor(generator, block, state, child, elementStack, stripWhitespace, cleaned[i + 1] || nextSibling);
if (lastChild) { if (lastChild) {
lastChild.next = child; lastChild.next = child;
@ -432,7 +449,7 @@ export default function preprocess(
}; };
generator.blocks.push(block); generator.blocks.push(block);
preprocessChildren(generator, block, state, node, true, null); preprocessChildren(generator, block, state, node, [], true, null);
block.hasUpdateMethod = block.dependencies.size > 0; block.hasUpdateMethod = block.dependencies.size > 0;
return { block, state }; return { block, state };

@ -2,13 +2,15 @@ import visitors from './visitors/index';
import { DomGenerator } from './index'; import { DomGenerator } from './index';
import Block from './Block'; import Block from './Block';
import { Node } from '../../interfaces'; import { Node } from '../../interfaces';
import { State } from './interfaces';
export default function visit( export default function visit(
generator: DomGenerator, generator: DomGenerator,
block: Block, block: Block,
state, state: State,
node: Node node: Node,
elementStack: Node[]
) { ) {
const visitor = visitors[node.type]; const visitor = visitors[node.type];
visitor(generator, block, state, node); visitor(generator, block, state, node, elementStack);
} }

@ -40,7 +40,8 @@ export default function visitComponent(
generator: DomGenerator, generator: DomGenerator,
block: Block, block: Block,
state: State, state: State,
node: Node node: Node,
elementStack: Node[]
) { ) {
const hasChildren = node.children.length > 0; const hasChildren = node.children.length > 0;
const name = block.getUniqueName( const name = block.getUniqueName(
@ -121,7 +122,7 @@ export default function visitComponent(
const childBlock = node._block; const childBlock = node._block;
node.children.forEach((child: Node) => { node.children.forEach((child: Node) => {
visit(generator, childBlock, childState, child); visit(generator, childBlock, childState, child, elementStack);
}); });
const yieldFragment = block.getUniqueName(`${name}_yield_fragment`); const yieldFragment = block.getUniqueName(`${name}_yield_fragment`);

@ -9,7 +9,8 @@ export default function visitEachBlock(
generator: DomGenerator, generator: DomGenerator,
block: Block, block: Block,
state: State, state: State,
node: Node node: Node,
elementStack: Node[]
) { ) {
const each_block = generator.getUniqueName(`each_block`); const each_block = generator.getUniqueName(`each_block`);
const create_each_block = node._block.name; const create_each_block = node._block.name;
@ -125,12 +126,12 @@ export default function visitEachBlock(
} }
node.children.forEach((child: Node) => { node.children.forEach((child: Node) => {
visit(generator, node._block, node._state, child); visit(generator, node._block, node._state, child, elementStack);
}); });
if (node.else) { if (node.else) {
node.else.children.forEach((child: Node) => { node.else.children.forEach((child: Node) => {
visit(generator, node.else._block, node.else._state, child); visit(generator, node.else._block, node.else._state, child, elementStack);
}); });
} }
} }

@ -35,14 +35,15 @@ export default function visitElement(
generator: DomGenerator, generator: DomGenerator,
block: Block, block: Block,
state: State, state: State,
node: Node node: Node,
elementStack: Node[]
) { ) {
if (node.name in meta) { if (node.name in meta) {
return meta[node.name](generator, block, node); return meta[node.name](generator, block, node);
} }
if (generator.components.has(node.name) || node.name === ':Self') { if (generator.components.has(node.name) || node.name === ':Self') {
return visitComponent(generator, block, state, node); return visitComponent(generator, block, state, node, elementStack);
} }
const childState = node._state; const childState = node._state;
@ -80,7 +81,8 @@ export default function visitElement(
} }
// add CSS encapsulation attribute // add CSS encapsulation attribute
if (generator.cssId && (!generator.cascade || state.isTopLevel)) { // TODO add a helper for this, rather than repeating it
if (node._needsCssAttribute) {
block.builders.hydrate.addLine( block.builders.hydrate.addLine(
`@setAttribute( ${name}, '${generator.cssId}', '' );` `@setAttribute( ${name}, '${generator.cssId}', '' );`
); );
@ -181,7 +183,7 @@ export default function visitElement(
} }
node.children.forEach((child: Node) => { node.children.forEach((child: Node) => {
visit(generator, block, childState, child); visit(generator, block, childState, child, elementStack.concat(node));
}); });
if (node.lateUpdate) { if (node.lateUpdate) {

@ -19,7 +19,8 @@ function getBranches(
generator: DomGenerator, generator: DomGenerator,
block: Block, block: Block,
state: State, state: State,
node: Node node: Node,
elementStack: Node[]
) { ) {
const branches = [ const branches = [
{ {
@ -31,11 +32,11 @@ function getBranches(
}, },
]; ];
visitChildren(generator, block, state, node); visitChildren(generator, block, state, node, elementStack);
if (isElseIf(node.else)) { if (isElseIf(node.else)) {
branches.push( branches.push(
...getBranches(generator, block, state, node.else.children[0]) ...getBranches(generator, block, state, node.else.children[0], elementStack)
); );
} else { } else {
branches.push({ branches.push({
@ -47,7 +48,7 @@ function getBranches(
}); });
if (node.else) { if (node.else) {
visitChildren(generator, block, state, node.else); visitChildren(generator, block, state, node.else, elementStack);
} }
} }
@ -58,10 +59,11 @@ function visitChildren(
generator: DomGenerator, generator: DomGenerator,
block: Block, block: Block,
state: State, state: State,
node: Node node: Node,
elementStack: Node[]
) { ) {
node.children.forEach((child: Node) => { node.children.forEach((child: Node) => {
visit(generator, node._block, node._state, child); visit(generator, node._block, node._state, child, elementStack);
}); });
} }
@ -69,7 +71,8 @@ export default function visitIfBlock(
generator: DomGenerator, generator: DomGenerator,
block: Block, block: Block,
state: State, state: State,
node: Node node: Node,
elementStack: Node[]
) { ) {
const name = generator.getUniqueName(`if_block`); const name = generator.getUniqueName(`if_block`);
const anchor = node.needsAnchor const anchor = node.needsAnchor
@ -77,7 +80,7 @@ export default function visitIfBlock(
: (node.next && node.next._state.name) || 'null'; : (node.next && node.next._state.name) || 'null';
const params = block.params.join(', '); const params = block.params.join(', ');
const branches = getBranches(generator, block, state, node); const branches = getBranches(generator, block, state, node, elementStack);
const hasElse = isElseBranch(branches[branches.length - 1]); const hasElse = isElseBranch(branches[branches.length - 1]);
const if_name = hasElse ? '' : `if ( ${name} ) `; const if_name = hasElse ? '' : `if ( ${name} ) `;

@ -1,6 +1,7 @@
import deindent from '../../utils/deindent'; import deindent from '../../utils/deindent';
import Generator from '../Generator'; import Generator from '../Generator';
import Block from './Block'; import Block from './Block';
import preprocess from './preprocess';
import visit from './visit'; import visit from './visit';
import { removeNode, removeObjectKey } from '../../utils/removeNode'; import { removeNode, removeObjectKey } from '../../utils/removeNode';
import { Parsed, Node, CompileOptions } from '../../interfaces'; import { Parsed, Node, CompileOptions } from '../../interfaces';
@ -24,6 +25,10 @@ export class SsrGenerator extends Generator {
// in an SSR context, we don't need to include events, methods, oncreate or ondestroy // in an SSR context, we don't need to include events, methods, oncreate or ondestroy
const { templateProperties, defaultExport } = this; const { templateProperties, defaultExport } = this;
preprocess(this, parsed.html);
this.warnOnUnusedSelectors();
if (templateProperties.oncreate) if (templateProperties.oncreate)
removeNode( removeNode(
this.code, this.code,

@ -0,0 +1,90 @@
import { SsrGenerator } from './index';
import { Node } from '../../interfaces';
function noop () {}
function isElseIf(node: Node) {
return (
node && node.children.length === 1 && node.children[0].type === 'IfBlock'
);
}
const preprocessors = {
MustacheTag: noop,
RawMustacheTag: noop,
Text: noop,
IfBlock: (
generator: SsrGenerator,
node: Node,
elementStack: Node[]
) => {
function attachBlocks(node: Node) {
preprocessChildren(generator, node, elementStack);
if (isElseIf(node.else)) {
attachBlocks(node.else.children[0]);
} else if (node.else) {
preprocessChildren(
generator,
node.else,
elementStack
);
}
}
attachBlocks(node);
},
EachBlock: (
generator: SsrGenerator,
node: Node,
elementStack: Node[]
) => {
preprocessChildren(generator, node, elementStack);
if (node.else) {
preprocessChildren(
generator,
node.else,
elementStack
);
}
},
Element: (
generator: SsrGenerator,
node: Node,
elementStack: Node[]
) => {
const isComponent =
generator.components.has(node.name) || node.name === ':Self';
if (!isComponent) {
generator.applyCss(node, elementStack);
}
if (node.children.length) {
if (isComponent) {
preprocessChildren(generator, node, elementStack);
} else {
preprocessChildren(generator, node, elementStack.concat(node));
}
}
},
};
function preprocessChildren(
generator: SsrGenerator,
node: Node,
elementStack: Node[]
) {
node.children.forEach((child: Node, i: number) => {
const preprocessor = preprocessors[child.type];
if (preprocessor) preprocessor(generator, child, elementStack);
});
}
export default function preprocess(generator: SsrGenerator, html: Node) {
preprocessChildren(generator, html, []);
}

@ -56,7 +56,7 @@ export default function visitElement(
} }
}); });
if (generator.cssId && (!generator.cascade || generator.elementDepth === 0)) { if (node._needsCssAttribute) {
openingTag += ` ${generator.cssId}`; openingTag += ` ${generator.cssId}`;
} }

@ -1,17 +1,19 @@
import MagicString from 'magic-string'; import MagicString from 'magic-string';
import { Parsed, Node } from '../../interfaces'; import { groupSelectors, isGlobalSelector, walkRules } from '../../utils/css';
import Generator from '../Generator';
import { Node } from '../../interfaces';
const commentsPattern = /\/\*[\s\S]*?\*\//g; const commentsPattern = /\/\*[\s\S]*?\*\//g;
export default function processCss( export default function processCss(
parsed: Parsed, generator: Generator,
code: MagicString, code: MagicString,
cascade: boolean cascade: boolean
) { ) {
const css = parsed.css.content.styles; const css = generator.parsed.css.content.styles;
const offset = parsed.css.content.start; const offset = generator.parsed.css.content.start;
const attr = `[svelte-${parsed.hash}]`; const attr = `[svelte-${generator.parsed.hash}]`;
const keyframes = new Map(); const keyframes = new Map();
@ -22,7 +24,7 @@ export default function processCss(
if (expression.name.startsWith('-global-')) { if (expression.name.startsWith('-global-')) {
code.remove(expression.start, expression.start + 8); code.remove(expression.start, expression.start + 8);
} else { } else {
const newName = `svelte-${parsed.hash}-${expression.name}`; const newName = `svelte-${generator.parsed.hash}-${expression.name}`;
code.overwrite(expression.start, expression.end, newName); code.overwrite(expression.start, expression.end, newName);
keyframes.set(expression.name, newName); keyframes.set(expression.name, newName);
} }
@ -35,7 +37,7 @@ export default function processCss(
} }
} }
parsed.css.children.forEach(walkKeyframes); generator.parsed.css.children.forEach(walkKeyframes);
function transform(rule: Node) { function transform(rule: Node) {
rule.selector.children.forEach((selector: Node) => { rule.selector.children.forEach((selector: Node) => {
@ -52,7 +54,7 @@ export default function processCss(
if (firstToken.type === 'TypeSelector') { if (firstToken.type === 'TypeSelector') {
const insert = firstToken.end - offset; const insert = firstToken.end - offset;
const head = css.slice(start, insert); const head = firstToken.name === '*' ? css.slice(firstToken.end - offset, insert) : css.slice(start, insert);
const tail = css.slice(insert, end); const tail = css.slice(insert, end);
transformed = `${head}${attr}${tail}, ${attr} ${selectorString}`; transformed = `${head}${attr}${tail}, ${attr} ${selectorString}`;
@ -61,41 +63,6 @@ export default function processCss(
} }
code.overwrite(selector.start, selector.end, transformed); code.overwrite(selector.start, selector.end, transformed);
} else {
let shouldTransform = true;
let c = selector.start;
selector.children.forEach((child: Node) => {
if (child.type === 'WhiteSpace' || child.type === 'Combinator') {
code.appendLeft(c, attr);
shouldTransform = true;
return;
}
if (!shouldTransform) return;
if (child.type === 'PseudoClassSelector') {
// `:global(xyz)` > xyz
if (child.name === 'global') {
const first = child.children[0];
const last = child.children[child.children.length - 1];
code.remove(child.start, first.start).remove(last.end, child.end);
} else {
code.prependRight(c, attr);
}
shouldTransform = false;
} else if (child.type === 'PseudoElementSelector') {
code.prependRight(c, attr);
shouldTransform = false;
}
c = child.end;
});
if (shouldTransform) {
code.appendLeft(c, attr);
}
} }
}); });
@ -116,22 +83,13 @@ export default function processCss(
}); });
} }
function walk(node: Node) { walkRules(generator.parsed.css.children, transform);
if (node.type === 'Rule') {
transform(node);
} else if (
node.type === 'Atrule' &&
node.name.toLowerCase() === 'keyframes'
) {
// these have already been processed
} else if (node.children) {
node.children.forEach(walk);
} else if (node.block) {
walk(node.block);
}
}
parsed.css.children.forEach(walk); if (!cascade) {
generator.selectors.forEach(selector => {
selector.transform(code, attr);
});
}
// remove comments. TODO would be nice if this was exposed in css-tree // remove comments. TODO would be nice if this was exposed in css-tree
let match; let match;
@ -142,5 +100,5 @@ export default function processCss(
code.remove(start, end); code.remove(start, end);
} }
return code.slice(parsed.css.content.start, parsed.css.content.end); return code.slice(generator.parsed.css.content.start, generator.parsed.css.content.end);
} }

@ -26,9 +26,11 @@ export interface Parsed {
} }
export interface Warning { export interface Warning {
loc?: { line: number; column: number; pos: number }; loc?: { line: number; column: number; pos?: number };
pos?: number;
message: string; message: string;
filename?: string; filename?: string;
frame?: string;
toString: () => string; toString: () => string;
} }

@ -0,0 +1,45 @@
import { Node } from '../interfaces';
export function isGlobalSelector(block: Node[]) {
return block[0].type === 'PseudoClassSelector' && block[0].name === 'global';
}
export function groupSelectors(selector: Node) {
let block = {
global: selector.children[0].type === 'PseudoClassSelector' && selector.children[0].name === 'global',
selectors: [],
combinator: null
};
const blocks = [block];
selector.children.forEach((child: Node, i: number) => {
if (child.type === 'WhiteSpace' || child.type === 'Combinator') {
const next = selector.children[i + 1];
block = {
global: next.type === 'PseudoClassSelector' && next.name === 'global',
selectors: [],
combinator: child
};
blocks.push(block);
} else {
block.selectors.push(child);
}
});
return blocks;
}
export function walkRules(nodes: Node[], callback: (node: Node) => void) {
nodes.forEach((node: Node) => {
if (node.type === 'Rule') {
callback(node);
} else if (node.type === 'Atrule') {
if (node.name === 'media' || node.name === 'supports' || node.name === 'document') {
walkRules(node.block.children, callback);
}
}
});
}

@ -0,0 +1,40 @@
import { groupSelectors, isGlobalSelector, walkRules } from '../../utils/css';
import { Validator } from '../index';
import { Node } from '../../interfaces';
export default function validateCss(validator: Validator, css: Node) {
walkRules(css.children, rule => {
rule.selector.children.forEach(validateSelector);
});
function validateSelector(selector: Node) {
const blocks = groupSelectors(selector);
blocks.forEach((block) => {
let i = block.selectors.length;
while (i-- > 1) {
const part = block.selectors[i];
if (part.type === 'PseudoClassSelector' && part.name === 'global') {
validator.error(`:global(...) must be the first element in a compound selector`, part.start);
}
}
});
let start = 0;
let end = blocks.length;
for (; start < end; start += 1) {
if (!blocks[start].global) break;
}
for (; end > start; end -= 1) {
if (!blocks[end - 1].global) break;
}
for (let i = start; i < end; i += 1) {
if (blocks[i].global) {
validator.error(`:global(...) can be at the start or end of a selector sequence, but not in the middle`, blocks[i].selectors[0].start);
}
}
}
}

@ -1,4 +1,5 @@
import validateJs from './js/index'; import validateJs from './js/index';
import validateCss from './css/index';
import validateHtml from './html/index'; import validateHtml from './html/index';
import { getLocator, Location } from 'locate-character'; import { getLocator, Location } from 'locate-character';
import getCodeFrame from '../utils/getCodeFrame'; import getCodeFrame from '../utils/getCodeFrame';
@ -34,9 +35,9 @@ export class Validator {
constructor(parsed: Parsed, source: string, options: CompileOptions) { constructor(parsed: Parsed, source: string, options: CompileOptions) {
this.source = source; this.source = source;
this.filename = options !== undefined ? options.filename : undefined; this.filename = options.filename;
this.onwarn = options !== undefined ? options.onwarn : undefined; this.onwarn = options.onwarn;
this.namespace = null; this.namespace = null;
this.defaultExport = null; this.defaultExport = null;
@ -101,6 +102,10 @@ export default function validate(
validateJs(validator, parsed.js); validateJs(validator, parsed.js);
} }
if (parsed.css) {
validateCss(validator, parsed.css);
}
if (parsed.html) { if (parsed.html) {
validateHtml(validator, parsed.html); validateHtml(validator, parsed.html);
} }

@ -1,16 +1,24 @@
import assert from "assert"; import assert from "assert";
import * as fs from "fs"; import * as fs from "fs";
import { svelte } from "../helpers.js"; import { env, normalizeHtml, svelte } from "../helpers.js";
function tryRequire(file) { function tryRequire(file) {
try { try {
return require(file).default; const mod = require(file);
return mod.default || mod;
} catch (err) { } catch (err) {
if (err.code !== "MODULE_NOT_FOUND") throw err; if (err.code !== "MODULE_NOT_FOUND") throw err;
return null; return null;
} }
} }
function normalizeWarning(warning) {
warning.frame = warning.frame.replace(/^\n/, '').replace(/^\t+/gm, '');
delete warning.filename;
delete warning.toString;
return warning;
}
describe("css", () => { describe("css", () => {
fs.readdirSync("test/css/samples").forEach(dir => { fs.readdirSync("test/css/samples").forEach(dir => {
if (dir[0] === ".") return; if (dir[0] === ".") return;
@ -28,14 +36,74 @@ describe("css", () => {
.readFileSync(`test/css/samples/${dir}/input.html`, "utf-8") .readFileSync(`test/css/samples/${dir}/input.html`, "utf-8")
.replace(/\s+$/, ""); .replace(/\s+$/, "");
const actual = svelte.compile(input, config).css; const expectedWarnings = (config.warnings || []).map(normalizeWarning);
fs.writeFileSync(`test/css/samples/${dir}/_actual.css`, actual); const domWarnings = [];
const expected = fs.readFileSync( const ssrWarnings = [];
`test/css/samples/${dir}/expected.css`,
"utf-8" const dom = svelte.compile(input, Object.assign(config, {
); format: 'iife',
name: 'SvelteComponent',
onwarn: warning => {
domWarnings.push(warning);
}
}));
const ssr = svelte.compile(input, Object.assign(config, {
format: 'iife',
generate: 'ssr',
name: 'SvelteComponent',
onwarn: warning => {
ssrWarnings.push(warning);
}
}));
assert.equal(dom.css, ssr.css);
assert.deepEqual(domWarnings.map(normalizeWarning), ssrWarnings.map(normalizeWarning));
assert.deepEqual(domWarnings.map(normalizeWarning), expectedWarnings);
fs.writeFileSync(`test/css/samples/${dir}/_actual.css`, dom.css);
const expected = {
html: read(`test/css/samples/${dir}/expected.html`),
css: read(`test/css/samples/${dir}/expected.css`)
};
assert.equal(dom.css.replace(/svelte-\d+/g, 'svelte-xyz').trim(), expected.css.trim());
// verify that the right elements have scoping selectors
if (expected.html !== null) {
return env().then(window => {
const Component = eval(`(function () { ${dom.code}; return SvelteComponent; }())`);
const target = window.document.querySelector("main");
assert.equal(actual.trim(), expected.trim()); new Component({ target, data: config.data });
const html = target.innerHTML;
fs.writeFileSync(`test/css/samples/${dir}/_actual.html`, html);
// dom
assert.equal(
normalizeHtml(window, html).replace(/svelte-\d+/g, 'svelte-xyz'),
normalizeHtml(window, expected.html)
);
// ssr
const component = eval(`(function () { ${ssr.code}; return SvelteComponent; }())`);
assert.equal(
normalizeHtml(window, component.render(config.data)).replace(/svelte-\d+/g, 'svelte-xyz'),
normalizeHtml(window, expected.html)
);
});
}
}); });
}); });
}); });
function read(file) {
try {
return fs.readFileSync(file, 'utf-8');
} catch(err) {
return null;
}
}

@ -1,4 +1,4 @@
div[svelte-2278551596], [svelte-2278551596] div { div[svelte-xyz], [svelte-xyz] div {
color: red; color: red;
} }

@ -4,10 +4,10 @@
100% { color: blue; } 100% { color: blue; }
} }
.animated[svelte-90785995] { .animated[svelte-xyz] {
animation: why 2s; animation: why 2s;
} }
.also-animated[svelte-90785995] { .also-animated[svelte-xyz] {
animation: not-defined-here 2s; animation: not-defined-here 2s;
} }

@ -6,7 +6,7 @@
color: red; color: red;
} }
:global(div.foo) { :global(div).foo {
color: blue; color: blue;
} }

@ -1,13 +1,13 @@
@keyframes svelte-1647166666-why { @keyframes svelte-xyz-why {
0% { color: red; } 0% { color: red; }
100% { color: blue; } 100% { color: blue; }
} }
.animated[svelte-1647166666] { .animated[svelte-xyz] {
animation: svelte-1647166666-why 2s; animation: svelte-xyz-why 2s;
} }
.also-animated[svelte-1647166666] { .also-animated[svelte-xyz] {
animation: not-defined-here 2s; animation: not-defined-here 2s;
} }

@ -1,12 +1,12 @@
span[svelte-2146001331]::after { span[svelte-xyz]::after {
content: 'i am a pseudo-element'; content: 'i am a pseudo-element';
} }
span[svelte-2146001331]:first-child { span[svelte-xyz]:first-child {
color: red; color: red;
} }
span[svelte-2146001331]:last-child::after { span[svelte-xyz]:last-child::after {
color: blue; color: blue;
} }

@ -0,0 +1,3 @@
export default {
cascade: false
};

@ -0,0 +1,7 @@
<div></div>
<style>
* {
color: red;
}
</style>

@ -1,12 +1,12 @@
div[svelte-781920915] { div[svelte-xyz] {
color: red; color: red;
} }
div.foo[svelte-781920915] { div.foo[svelte-xyz] {
color: blue; color: blue;
} }
.foo[svelte-781920915] { .foo[svelte-xyz] {
font-weight: bold; font-weight: bold;
} }

@ -1,9 +1,9 @@
@keyframes svelte-2931302006-why { @keyframes svelte-xyz-why {
0% { color: red; } 0% { color: red; }
100% { color: blue; } 100% { color: blue; }
} }
[svelte-2931302006].animated, [svelte-2931302006] .animated { [svelte-xyz].animated, [svelte-xyz] .animated {
animation: svelte-2931302006-why 2s; animation: svelte-xyz-why 2s;
} }

@ -1,6 +1,6 @@
@media (min-width: 400px) { @media (min-width: 400px) {
[svelte-411199634].large-screen, [svelte-411199634] .large-screen { [svelte-xyz].large-screen, [svelte-xyz] .large-screen {
display: block; display: block;
} }
} }

@ -0,0 +1,2 @@
<div><p svelte-xyz="" data-foo="foobarbaz">this is styled</p>
<p data-foo="fooBARbaz">this is unstyled</p></div>

@ -0,0 +1,10 @@
<div>
<p data-foo='foobarbaz'>this is styled</p>
<p data-foo='fooBARbaz'>this is unstyled</p>
</div>
<style>
[data-foo*='bar'] {
color: red;
}
</style>

@ -0,0 +1,2 @@
<div><p svelte-xyz="" data-foo="BAR">this is styled</p>
<p data-foo="BAZ">this is unstyled</p></div>

@ -0,0 +1,10 @@
<div>
<p data-foo='BAR'>this is styled</p>
<p data-foo='BAZ'>this is unstyled</p>
</div>
<style>
[data-foo='bar' i] {
color: red;
}
</style>

@ -0,0 +1,6 @@
export default {
cascade: false,
data: {
dynamic: 'whatever'
}
};

@ -0,0 +1,2 @@
<div><p svelte-xyz="" data-foo="whatever">this is styled</p>
<p data-foo="baz">this is unstyled</p></div>

@ -0,0 +1,10 @@
<div>
<p data-foo='{{dynamic}}'>this is styled</p>
<p data-foo='baz'>this is unstyled</p>
</div>
<style>
[data-foo='bar'] {
color: red;
}
</style>

@ -0,0 +1,2 @@
<div><p svelte-xyz="" data-foo="bar">this is styled</p>
<p data-foo="baz">this is unstyled</p></div>

@ -0,0 +1,10 @@
<div>
<p data-foo='bar'>this is styled</p>
<p data-foo='baz'>this is unstyled</p>
</div>
<style>
[data-foo='bar'] {
color: red;
}
</style>

@ -0,0 +1,3 @@
<div><p svelte-xyz="" data-foo="bar">this is styled</p>
<p svelte-xyz="" data-foo="bar-baz">this is styled</p>
<p data-foo="baz-bar">this is unstyled</p></div>

@ -0,0 +1,11 @@
<div>
<p data-foo='bar'>this is styled</p>
<p data-foo='bar-baz'>this is styled</p>
<p data-foo='baz-bar'>this is unstyled</p>
</div>
<style>
[data-foo|='bar'] {
color: red;
}
</style>

@ -0,0 +1,2 @@
<div><p svelte-xyz="" data-foo="barbaz">this is styled</p>
<p data-foo="bazbar">this is unstyled</p></div>

@ -0,0 +1,10 @@
<div>
<p data-foo='barbaz'>this is styled</p>
<p data-foo='bazbar'>this is unstyled</p>
</div>
<style>
[data-foo^='bar'] {
color: red;
}
</style>

@ -0,0 +1,2 @@
<div><p data-foo="barbaz">this is unstyled</p>
<p svelte-xyz="" data-foo="bazbar">this is styled</p></div>

@ -0,0 +1,10 @@
<div>
<p data-foo='barbaz'>this is unstyled</p>
<p data-foo='bazbar'>this is styled</p>
</div>
<style>
[data-foo$='bar'] {
color: red;
}
</style>

@ -0,0 +1,2 @@
<div><p svelte-xyz="" data-foo="qux bar">this is styled</p>
<p data-foo="qux baz">this is unstyled</p></div>

@ -0,0 +1,10 @@
<div>
<p data-foo='qux bar'>this is styled</p>
<p data-foo='qux baz'>this is unstyled</p>
</div>
<style>
[data-foo~='bar'] {
color: red;
}
</style>

@ -0,0 +1,2 @@
<div><video svelte-xyz autoplay></video>
<video></video></div>

@ -0,0 +1,10 @@
<div>
<video autoplay></video>
<video></video>
</div>
<style>
[autoplay] {
color: red;
}
</style>

@ -0,0 +1,2 @@
<p svelte-xyz="" class="whatever">this is styled</p>
<p class="bar">this is unstyled</p>

@ -0,0 +1,18 @@
<p class='{{unknown}}'>this is styled</p>
<p class='bar'>this is unstyled</p>
<style>
.foo {
color: red;
}
</style>
<script>
export default {
data () {
return {
unknown: 'whatever'
};
}
};
</script>

@ -0,0 +1,2 @@
<p svelte-xyz="" class="foo">this is styled</p>
<p class="bar">this is unstyled</p>

@ -0,0 +1,8 @@
<p class='foo'>this is styled</p>
<p class='bar'>this is unstyled</p>
<style>
.foo {
color: red;
}
</style>

@ -0,0 +1,7 @@
export default {
cascade: false,
data: {
raw: '<p>raw</p>'
}
};

@ -0,0 +1,9 @@
<div class='foo'>
<!-- html injected somehow -->
</div>
<style>
.foo :global(.bar) {
color: red;
}
</style>

@ -0,0 +1,7 @@
export default {
cascade: false,
data: {
raw: '<p>raw</p>'
}
};

@ -0,0 +1,9 @@
<div>
<!-- html injected somehow -->
</div>
<style>
div > :global(p) > :global(em) {
color: red;
}
</style>

@ -0,0 +1,7 @@
export default {
cascade: false,
data: {
raw: '<p>raw</p>'
}
};

@ -0,0 +1,9 @@
<div>
<!-- html injected somehow -->
</div>
<style>
div > :global(p) {
color: red;
}
</style>

@ -0,0 +1,7 @@
export default {
cascade: false,
data: {
raw: '<p>raw</p>'
}
};

@ -0,0 +1,7 @@
<p>this may or may not be styled</p>
<style>
:global(div) > :global(section) > p {
color: red;
}
</style>

@ -0,0 +1,7 @@
export default {
cascade: false,
data: {
raw: '<p>raw</p>'
}
};

@ -0,0 +1 @@
<p svelte-xyz="">this may or may not be styled</p>

@ -0,0 +1,7 @@
<p>this may or may not be styled</p>
<style>
:global(div) > p {
color: red;
}
</style>

@ -0,0 +1,19 @@
export default {
cascade: false,
warnings: [{
message: 'Unused CSS selector',
loc: {
line: 8,
column: 1
},
pos: 74,
frame: `
6:
7: <style>
8: div > p {
^
9: color: red;
10: }`
}]
};

@ -0,0 +1,4 @@
div[svelte-xyz] > p[svelte-xyz] {
color: red;
}

@ -0,0 +1 @@
<div><section><p>this is not styled</p></section></div>

@ -0,0 +1,11 @@
<div>
<section>
<p>this is not styled</p>
</section>
</div>
<style>
div > p {
color: red;
}
</style>

@ -0,0 +1,7 @@
export default {
cascade: false,
data: {
raw: '<p>raw</p>'
}
};

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save