Merge pull request #698 from sveltejs/gh-691

CSS sourcemaps
pull/716/head
Rich Harris 7 years ago committed by GitHub
commit f0fddaab82

@ -60,12 +60,12 @@
"eslint": "^3.12.2",
"eslint-plugin-html": "^3.0.0",
"eslint-plugin-import": "^2.2.0",
"estree-walker": "^0.3.0",
"estree-walker": "^0.5.0",
"fuzzyset.js": "0.0.1",
"glob": "^7.1.1",
"jsdom": "^9.9.1",
"locate-character": "^2.0.0",
"magic-string": "^0.21.1",
"magic-string": "^0.22.1",
"mocha": "^3.2.0",
"node-resolve": "^1.3.3",
"nyc": "^10.0.0",

@ -1,38 +1,37 @@
import MagicString from 'magic-string';
import { groupSelectors, isGlobalSelector, walkRules } from '../utils/css';
import { Validator } from '../validate/index';
import { Node } from '../interfaces';
interface Block {
global: boolean;
combinator: Node;
selectors: Node[]
}
export default class Selector {
node: Node;
blocks: any; // TODO
parts: Node[];
blocks: Block[];
localBlocks: Block[];
used: boolean;
constructor(node: Node) {
this.node = node;
this.blocks = groupSelectors(this.node);
this.blocks = groupSelectors(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;
}
let i = this.blocks.length;
while (i > 0) {
if (!this.blocks[i - 1].global) break;
i -= 1;
}
this.parts = node.children.slice(0, i);
this.localBlocks = this.blocks.slice(0, i);
this.used = this.blocks[0].global;
}
apply(node: Node, stack: Node[]) {
const applies = selectorAppliesTo(this.parts, node, stack.slice());
const applies = selectorAppliesTo(this.localBlocks.slice(), node, stack.slice());
if (applies) {
this.used = true;
@ -45,7 +44,7 @@ export default class Selector {
}
transform(code: MagicString, attr: string) {
function encapsulateBlock(block) {
function encapsulateBlock(block: Block) {
let i = block.selectors.length;
while (i--) {
const selector = block.selectors[i];
@ -72,77 +71,102 @@ export default class Selector {
}
});
}
validate(validator: Validator) {
this.blocks.forEach((block) => {
let i = block.selectors.length;
while (i-- > 1) {
const selector = block.selectors[i];
if (selector.type === 'PseudoClassSelector' && selector.name === 'global') {
validator.error(`:global(...) must be the first element in a compound selector`, selector.start);
}
}
});
let start = 0;
let end = this.blocks.length;
for (; start < end; start += 1) {
if (!this.blocks[start].global) break;
}
for (; end > start; end -= 1) {
if (!this.blocks[end - 1].global) break;
}
for (let i = start; i < end; i += 1) {
if (this.blocks[i].global) {
validator.error(`:global(...) can be at the start or end of a selector sequence, but not in the middle`, this.blocks[i].selectors[0].start);
}
}
}
}
function isDescendantSelector(selector: Node) {
return selector.type === 'WhiteSpace' || selector.type === 'Combinator';
}
function selectorAppliesTo(parts: Node[], node: Node, stack: Node[]): boolean {
let i = parts.length;
function selectorAppliesTo(blocks: Block[], node: Node, stack: Node[]): boolean {
const block = blocks.pop();
if (!block) return false;
if (!node) {
return blocks.every(block => block.global);
}
let i = block.selectors.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 selector = block.selectors[i];
const part = parts[i];
if (part.type === 'PseudoClassSelector' && part.name === 'global') {
if (selector.type === 'PseudoClassSelector' && selector.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') {
if (selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector') {
continue;
}
if (part.type === 'ClassSelector') {
if (!attributeMatches(node, 'class', part.name, '~=', false)) return false;
if (selector.type === 'ClassSelector') {
if (!attributeMatches(node, 'class', selector.name, '~=', false)) return false;
}
else if (part.type === 'IdSelector') {
if (!attributeMatches(node, 'id', part.name, '=', false)) return false;
else if (selector.type === 'IdSelector') {
if (!attributeMatches(node, 'id', selector.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 (selector.type === 'AttributeSelector') {
if (!attributeMatches(node, selector.name.name, selector.value && unquote(selector.value.value), selector.operator, selector.flags)) return false;
}
else if (part.type === 'TypeSelector') {
if (part.name === '*') return true;
if (node.name !== part.name) return false;
else if (selector.type === 'TypeSelector') {
if (node.name !== selector.name && selector.name !== '*') return false;
}
else if (part.type === 'WhiteSpace') {
parts = parts.slice(0, i);
else {
// bail. TODO figure out what these could be
return true;
}
}
if (block.combinator) {
if (block.combinator.type === 'WhiteSpace') {
while (stack.length) {
if (selectorAppliesTo(parts, stack.pop(), stack)) {
if (selectorAppliesTo(blocks.slice(), stack.pop(), stack)) {
return true;
}
}
return false;
} else if (block.combinator.name === '>') {
return selectorAppliesTo(blocks, stack.pop(), stack);
}
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;
}
// TODO other combinators
return true;
}
return true;
@ -177,4 +201,32 @@ function unquote(str: string) {
if (str[0] === str[str.length - 1] && str[0] === "'" || str[0] === '"') {
return str.slice(1, str.length - 1);
}
}
function groupSelectors(selector: Node) {
let block: 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;
}

@ -0,0 +1,233 @@
import MagicString from 'magic-string';
import { walk } from 'estree-walker';
import { getLocator } from 'locate-character';
import Selector from './Selector';
import getCodeFrame from '../utils/getCodeFrame';
import { Validator } from '../validate/index';
import { Node, Parsed, Warning } from '../interfaces';
class Rule {
selectors: Selector[];
declarations: Node[];
constructor(node: Node) {
this.selectors = node.selector.children.map((node: Node) => new Selector(node));
this.declarations = node.block.children;
}
apply(node: Node, stack: Node[]) {
this.selectors.forEach(selector => selector.apply(node, stack)); // TODO move the logic in here?
}
transform(code: MagicString, id: string, keyframes: Map<string, string>, cascade: boolean) {
const attr = `[${id}]`;
if (cascade) {
this.selectors.forEach(selector => {
// TODO disable cascading (without :global(...)) in v2
const { start, end, children } = selector.node;
const css = code.original;
const selectorString = css.slice(start, end);
const firstToken = children[0];
let transformed;
if (firstToken.type === 'TypeSelector') {
const insert = firstToken.end;
const head = firstToken.name === '*' ? '' : css.slice(start, insert);
const tail = css.slice(insert, end);
transformed = `${head}${attr}${tail}, ${attr} ${selectorString}`;
} else {
transformed = `${attr}${selectorString}, ${attr} ${selectorString}`;
}
code.overwrite(start, end, transformed);
});
} else {
this.selectors.forEach(selector => selector.transform(code, attr));
}
this.declarations.forEach((declaration: Node) => {
const property = declaration.property.toLowerCase();
if (property === 'animation' || property === 'animation-name') {
declaration.value.children.forEach((block: Node) => {
if (block.type === 'Identifier') {
const name = block.name;
if (keyframes.has(name)) {
code.overwrite(block.start, block.end, keyframes.get(name));
}
}
});
}
});
}
}
class Atrule {
node: Node;
constructor(node: Node) {
this.node = node;
}
transform(code: MagicString, id: string, keyframes: Map<string, string>) {
if (this.node.name !== 'keyframes') return;
this.node.expression.children.forEach((expression: Node) => {
if (expression.type === 'Identifier') {
if (expression.name.startsWith('-global-')) {
code.remove(expression.start, expression.start + 8);
} else {
const newName = `${id}-${expression.name}`;
code.overwrite(expression.start, expression.end, newName);
keyframes.set(expression.name, newName);
}
}
});
}
}
const keys = {};
export default class Stylesheet {
source: string;
parsed: Parsed;
cascade: boolean;
filename: string;
hasStyles: boolean;
id: string;
nodes: (Rule|Atrule)[];
rules: Rule[];
atrules: Atrule[];
constructor(source: string, parsed: Parsed, filename: string, cascade: boolean) {
this.source = source;
this.parsed = parsed;
this.cascade = cascade;
this.filename = filename;
this.id = `svelte-${parsed.hash}`;
this.nodes = [];
this.rules = [];
this.atrules = [];
if (parsed.css && parsed.css.children.length) {
this.hasStyles = true;
const stack: Atrule[] = [];
let currentAtrule: Atrule = null;
walk(this.parsed.css, {
enter: (node: Node) => {
if (node.type === 'Atrule') {
const atrule = currentAtrule = new Atrule(node);
stack.push(atrule);
this.nodes.push(atrule);
this.atrules.push(atrule);
}
if (node.type === 'Rule' && (!currentAtrule || /(media|supports|document)/.test(currentAtrule.node.name))) {
const rule = new Rule(node);
this.nodes.push(rule);
this.rules.push(rule);
}
},
leave: (node: Node) => {
if (node.type === 'Atrule') {
stack.pop();
currentAtrule = stack[stack.length - 1];
}
}
});
} else {
this.hasStyles = false;
}
}
apply(node: Node, stack: Node[]) {
if (!this.hasStyles) return;
if (this.cascade) {
if (stack.length === 0) node._needsCssAttribute = true;
return;
}
for (let i = 0; i < this.rules.length; i += 1) {
const rule = this.rules[i];
rule.apply(node, stack);
}
}
render(cssOutputFilename: string) {
if (!this.hasStyles) {
return { css: null, cssMap: null };
}
const code = new MagicString(this.source);
code.remove(0, this.parsed.css.start + 7);
code.remove(this.parsed.css.end - 8, this.source.length);
const keyframes = new Map();
this.atrules.forEach((atrule: Atrule) => {
atrule.transform(code, this.id, keyframes);
});
this.rules.forEach((rule: Rule) => {
rule.transform(code, this.id, keyframes, this.cascade);
});
return {
css: code.toString(),
cssMap: code.generateMap({
includeContent: true,
source: this.filename,
file: cssOutputFilename
})
};
}
validate(validator: Validator) {
this.rules.forEach(rule => {
rule.selectors.forEach(selector => {
selector.validate(validator);
});
});
}
warnOnUnusedSelectors(onwarn: (warning: Warning) => void) {
if (this.cascade) return;
let locator;
this.rules.forEach((rule: Rule) => {
rule.selectors.forEach(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`;
onwarn({
message,
frame,
loc: { line: line + 1, column },
pos,
filename: this.filename,
toString: () => `${message} (${line + 1}:${column})\n${frame}`,
});
}
});
});
}
}

@ -10,13 +10,11 @@ import namespaces from '../utils/namespaces';
import { removeNode, removeObjectKey } from '../utils/removeNode';
import getIntro from './shared/utils/getIntro';
import getOutro from './shared/utils/getOutro';
import processCss from './shared/processCss';
import annotateWithScopes from '../utils/annotateWithScopes';
import clone from '../utils/clone';
import DomBlock from './dom/Block';
import SsrBlock from './server-side-rendering/Block';
import { walkRules } from '../utils/css';
import Selector from './Selector';
import Stylesheet from '../css/Stylesheet';
import { Node, Parsed, CompileOptions } from '../interfaces';
const test = typeof global !== 'undefined' && global.__svelte_test;
@ -39,12 +37,9 @@ export default class Generator {
bindingGroups: string[];
indirectDependencies: Map<string, Set<string>>;
expectedProperties: Set<string>;
cascade: boolean;
css: string;
cssId: string;
usesRefs: boolean;
selectors: Selector[];
stylesheet: Stylesheet;
importedNames: Set<string>;
aliases: Map<string, string>;
@ -54,6 +49,7 @@ export default class Generator {
parsed: Parsed,
source: string,
name: string,
stylesheet: Stylesheet,
options: CompileOptions
) {
this.ast = clone(parsed);
@ -80,21 +76,10 @@ export default class Generator {
this.usesRefs = false;
// styles
this.cascade = options.cascade !== false; // TODO remove this option in v2
this.cssId = parsed.css ? `svelte-${parsed.hash}` : '';
this.selectors = [];
if (parsed.css) {
walkRules(parsed.css.children, node => {
node.selector.children.forEach((child: Node) => {
this.selectors.push(new Selector(child));
});
});
this.stylesheet = stylesheet;
this.css = processCss(this, this.code, this.cascade);
} else {
this.css = null;
}
// TODO this is legacy — just to get the tests to pass during the transition
this.css = this.stylesheet.render(options.cssOutputFilename).css;
// allow compiler to deconflict user's `import { get } from 'whatever'` and
// Svelte's builtin `import { get, ... } from 'svelte/shared.ts'`;
@ -231,20 +216,6 @@ 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(
contextDependencies: Map<string, string[]>,
indexes: Map<string, string>,
@ -292,7 +263,7 @@ export default class Generator {
return (expression._dependencies = dependencies);
}
generate(result, options, { name, format }) {
generate(result, options: CompileOptions, { name, format }) {
if (this.imports.length) {
const statements: string[] = [];
@ -382,6 +353,8 @@ export default class Generator {
addString(finalChunk);
addString('\n\n' + getOutro(format, name, options, this.imports));
const { css, cssMap } = this.stylesheet.render(options.cssOutputFilename);
return {
ast: this.ast,
code: compiled.toString(),
@ -389,7 +362,8 @@ export default class Generator {
includeContent: true,
file: options.outputFilename,
}),
css: this.css,
css,
cssMap
};
}
@ -624,31 +598,4 @@ export default class Generator {
this.namespace = namespace;
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}`,
});
}
});
}
}

@ -9,6 +9,7 @@ import CodeBuilder from '../../utils/CodeBuilder';
import visit from './visit';
import shared from './shared';
import Generator from '../Generator';
import Stylesheet from '../../css/Stylesheet';
import preprocess from './preprocess';
import Block from './Block';
import { Parsed, CompileOptions, Node } from '../../interfaces';
@ -28,9 +29,10 @@ export class DomGenerator extends Generator {
parsed: Parsed,
source: string,
name: string,
stylesheet: Stylesheet,
options: CompileOptions
) {
super(parsed, source, name, options);
super(parsed, source, name, stylesheet, options);
this.blocks = [];
this.readonly = new Set();
@ -45,11 +47,12 @@ export class DomGenerator extends Generator {
export default function dom(
parsed: Parsed,
source: string,
stylesheet: Stylesheet,
options: CompileOptions
) {
const format = options.format || 'es';
const generator = new DomGenerator(parsed, source, options.name || 'SvelteComponent', options);
const generator = new DomGenerator(parsed, source, options.name || 'SvelteComponent', stylesheet, options);
const {
computations,
@ -61,7 +64,7 @@ export default function dom(
const { block, state } = preprocess(generator, namespace, parsed.html);
generator.warnOnUnusedSelectors();
generator.stylesheet.warnOnUnusedSelectors(options.onwarn);
parsed.html.children.forEach((node: Node) => {
visit(generator, block, state, node, []);
@ -131,12 +134,18 @@ export default function dom(
builder.addBlock(`[✂${parsed.js.content.start}-${parsed.js.content.end}✂]`);
}
if (generator.css && options.css !== false) {
if (generator.stylesheet.hasStyles && options.css !== false) {
const { css, cssMap } = generator.stylesheet.render(options.filename);
const textContent = options.dev ?
`${css}\n/*# sourceMappingURL=${cssMap.toUrl()} */` :
css;
builder.addBlock(deindent`
function @add_css () {
var style = @createElement( 'style' );
style.id = '${generator.cssId}-style';
style.textContent = ${stringify(generator.css)};
style.id = '${generator.stylesheet.id}-style';
style.textContent = ${JSON.stringify(textContent)};
@appendNode( style, document.head );
}
`);
@ -195,9 +204,9 @@ export default function dom(
this._yield = options._yield;
this._torndown = false;
${generator.css &&
${generator.stylesheet.hasStyles &&
options.css !== false &&
`if ( !document.getElementById( '${generator.cssId}-style' ) ) @add_css();`}
`if ( !document.getElementById( '${generator.stylesheet.id}-style' ) ) @add_css();`}
${generator.hasComponents && `this._oncreate = [];`}
${generator.hasComplexBindings && `this._bindings = [];`}
${generator.hasIntroTransitions && `this._postcreate = [];`}

@ -330,7 +330,7 @@ const preprocessors = {
allUsedContexts: [],
});
generator.applyCss(node, elementStack);
generator.stylesheet.apply(node, elementStack);
}
if (node.children.length) {

@ -84,7 +84,7 @@ export default function visitElement(
// TODO add a helper for this, rather than repeating it
if (node._needsCssAttribute) {
block.builders.hydrate.addLine(
`@setAttribute( ${name}, '${generator.cssId}', '' );`
`@setAttribute( ${name}, '${generator.stylesheet.id}', '' );`
);
}

@ -1,5 +1,6 @@
import deindent from '../../utils/deindent';
import Generator from '../Generator';
import Stylesheet from '../../css/Stylesheet';
import Block from './Block';
import preprocess from './preprocess';
import visit from './visit';
@ -15,9 +16,10 @@ export class SsrGenerator extends Generator {
parsed: Parsed,
source: string,
name: string,
stylesheet: Stylesheet,
options: CompileOptions
) {
super(parsed, source, name, options);
super(parsed, source, name, stylesheet, options);
this.bindings = [];
this.renderCode = '';
this.elementDepth = 0;
@ -27,7 +29,7 @@ export class SsrGenerator extends Generator {
preprocess(this, parsed.html);
this.warnOnUnusedSelectors();
this.stylesheet.warnOnUnusedSelectors(options.onwarn);
if (templateProperties.oncreate)
removeNode(
@ -63,11 +65,12 @@ export class SsrGenerator extends Generator {
export default function ssr(
parsed: Parsed,
source: string,
stylesheet: Stylesheet,
options: CompileOptions
) {
const format = options.format || 'cjs';
const generator = new SsrGenerator(parsed, source, options.name || 'SvelteComponent', options);
const generator = new SsrGenerator(parsed, source, options.name || 'SvelteComponent', stylesheet, options);
const { computations, name, hasJs, templateProperties } = generator;
@ -83,6 +86,8 @@ export default function ssr(
visit(generator, mainBlock, node);
});
const { css, cssMap } = generator.stylesheet.render(options.filename);
const result = deindent`
${hasJs && `[✂${parsed.js.content.start}-${parsed.js.content.end}✂]`}
@ -124,12 +129,12 @@ export default function ssr(
${name}.renderCss = function () {
var components = [];
${generator.css &&
${generator.stylesheet.hasStyles &&
deindent`
components.push({
filename: ${name}.filename,
css: ${JSON.stringify(generator.css)},
map: null // TODO
css: ${JSON.stringify(css)},
map: ${JSON.stringify(cssMap)}
});
`}

@ -61,7 +61,7 @@ const preprocessors = {
generator.components.has(node.name) || node.name === ':Self';
if (!isComponent) {
generator.applyCss(node, elementStack);
generator.stylesheet.apply(node, elementStack);
}
if (node.children.length) {

@ -57,7 +57,7 @@ export default function visitElement(
});
if (node._needsCssAttribute) {
openingTag += ` ${generator.cssId}`;
openingTag += ` ${generator.stylesheet.id}`;
}
openingTag += '>';

@ -1,104 +0,0 @@
import MagicString from 'magic-string';
import { groupSelectors, isGlobalSelector, walkRules } from '../../utils/css';
import Generator from '../Generator';
import { Node } from '../../interfaces';
const commentsPattern = /\/\*[\s\S]*?\*\//g;
export default function processCss(
generator: Generator,
code: MagicString,
cascade: boolean
) {
const css = generator.parsed.css.content.styles;
const offset = generator.parsed.css.content.start;
const attr = `[svelte-${generator.parsed.hash}]`;
const keyframes = new Map();
function walkKeyframes(node: Node) {
if (node.type === 'Atrule' && node.name.toLowerCase() === 'keyframes') {
node.expression.children.forEach((expression: Node) => {
if (expression.type === 'Identifier') {
if (expression.name.startsWith('-global-')) {
code.remove(expression.start, expression.start + 8);
} else {
const newName = `svelte-${generator.parsed.hash}-${expression.name}`;
code.overwrite(expression.start, expression.end, newName);
keyframes.set(expression.name, newName);
}
}
});
} else if (node.children) {
node.children.forEach(walkKeyframes);
} else if (node.block) {
walkKeyframes(node.block);
}
}
generator.parsed.css.children.forEach(walkKeyframes);
function transform(rule: Node) {
rule.selector.children.forEach((selector: Node) => {
if (cascade) {
// TODO disable cascading (without :global(...)) in v2
const start = selector.start - offset;
const end = selector.end - offset;
const selectorString = css.slice(start, end);
const firstToken = selector.children[0];
let transformed;
if (firstToken.type === 'TypeSelector') {
const insert = firstToken.end - offset;
const head = firstToken.name === '*' ? css.slice(firstToken.end - offset, insert) : css.slice(start, insert);
const tail = css.slice(insert, end);
transformed = `${head}${attr}${tail}, ${attr} ${selectorString}`;
} else {
transformed = `${attr}${selectorString}, ${attr} ${selectorString}`;
}
code.overwrite(selector.start, selector.end, transformed);
}
});
rule.block.children.forEach((block: Node) => {
if (block.type === 'Declaration') {
const property = block.property.toLowerCase();
if (property === 'animation' || property === 'animation-name') {
block.value.children.forEach((block: Node) => {
if (block.type === 'Identifier') {
const name = block.name;
if (keyframes.has(name)) {
code.overwrite(block.start, block.end, keyframes.get(name));
}
}
});
}
}
});
}
walkRules(generator.parsed.css.children, transform);
if (!cascade) {
generator.selectors.forEach(selector => {
selector.transform(code, attr);
});
}
// remove comments. TODO would be nice if this was exposed in css-tree
let match;
while ((match = commentsPattern.exec(css))) {
const start = match.index + offset;
const end = start + match[0].length;
code.remove(start, end);
}
return code.slice(generator.parsed.css.content.start, generator.parsed.css.content.end);
}

@ -4,6 +4,7 @@ import generate from './generators/dom/index';
import generateSSR from './generators/server-side-rendering/index';
import { assign } from './shared/index.js';
import { version } from '../package.json';
import Stylesheet from './css/Stylesheet';
import { Parsed, CompileOptions, Warning } from './interfaces';
function normalizeOptions(options: CompileOptions): CompileOptions {
@ -44,11 +45,13 @@ export function compile(source: string, _options: CompileOptions) {
return;
}
validate(parsed, source, options);
const stylesheet = new Stylesheet(source, parsed, options.filename, options.cascade !== false);
validate(parsed, source, stylesheet, options);
const compiler = options.generate === 'ssr' ? generateSSR : generate;
return compiler(parsed, source, options);
return compiler(parsed, source, stylesheet, options);
}
export function create(source: string, _options: CompileOptions = {}) {

@ -40,6 +40,9 @@ export interface CompileOptions {
filename?: string;
generate?: string;
outputFilename?: string;
cssOutputFilename?: string;
dev?: boolean;
shared?: boolean | string;
cascade?: boolean;

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

@ -1,40 +0,0 @@
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,9 +1,9 @@
import validateJs from './js/index';
import validateCss from './css/index';
import validateHtml from './html/index';
import { getLocator, Location } from 'locate-character';
import getCodeFrame from '../utils/getCodeFrame';
import CompileError from '../utils/CompileError';
import Stylesheet from '../css/Stylesheet';
import { Node, Parsed, CompileOptions, Warning } from '../interfaces';
class ValidationError extends CompileError {
@ -73,6 +73,7 @@ export class Validator {
export default function validate(
parsed: Parsed,
source: string,
stylesheet: Stylesheet,
options: CompileOptions
) {
const { onwarn, onerror, name, filename } = options;
@ -103,7 +104,7 @@ export default function validate(
}
if (parsed.css) {
validateCss(validator, parsed.css);
stylesheet.validate(validator);
}
if (parsed.html) {

@ -25,12 +25,13 @@ describe("css", () => {
// add .solo to a sample directory name to only run that test
const solo = /\.solo/.test(dir);
const skip = /\.skip/.test(dir);
if (solo && process.env.CI) {
throw new Error("Forgot to remove `solo: true` from test");
}
(solo ? it.only : it)(dir, () => {
(solo ? it.only : skip ? it.skip : it)(dir, () => {
const config = tryRequire(`./samples/${dir}/_config.js`) || {};
const input = fs
.readFileSync(`test/css/samples/${dir}/input.html`, "utf-8")

@ -21,34 +21,50 @@ describe("sourcemaps", () => {
`test/sourcemaps/samples/${dir}/input.html`
);
const outputFilename = path.resolve(
`test/sourcemaps/samples/${dir}/output.js`
`test/sourcemaps/samples/${dir}/output`
);
const input = fs.readFileSync(filename, "utf-8").replace(/\s+$/, "");
const { code, map } = svelte.compile(input, {
const { code, map, css, cssMap } = svelte.compile(input, {
filename,
outputFilename
outputFilename: `${outputFilename}.js`,
cssOutputFilename: `${outputFilename}.css`
});
fs.writeFileSync(
outputFilename,
`${outputFilename}.js`,
`${code}\n//# sourceMappingURL=output.js.map`
);
fs.writeFileSync(
`${outputFilename}.map`,
`${outputFilename}.js.map`,
JSON.stringify(map, null, " ")
);
if (css) {
fs.writeFileSync(
`${outputFilename}.css`,
`${css}\n/*# sourceMappingURL=output.css.map */`
);
fs.writeFileSync(
`${outputFilename}.css.map`,
JSON.stringify(cssMap, null, " ")
);
}
assert.deepEqual(map.sources, ["input.html"]);
if (cssMap) assert.deepEqual(cssMap.sources, ["input.html"]);
const { test } = require(`./samples/${dir}/test.js`);
const smc = new SourceMapConsumer(map);
const locateInSource = getLocator(input);
const smc = new SourceMapConsumer(map);
const locateInGenerated = getLocator(code);
test({ assert, code, map, smc, locateInSource, locateInGenerated });
const smcCss = cssMap && new SourceMapConsumer(cssMap);
const locateInGeneratedCss = getLocator(css || '');
test({ assert, code, map, smc, smcCss, locateInSource, locateInGenerated, locateInGeneratedCss });
});
});
});

@ -0,0 +1,7 @@
<p class='foo'>red</p>
<style>
.foo {
color: red;
}
</style>

@ -0,0 +1,6 @@
[svelte-2772200924].foo, [svelte-2772200924] .foo {
color: red;
}
/*# sourceMappingURL=output.css.map */

@ -0,0 +1,12 @@
{
"version": 3,
"file": "output.css",
"sources": [
"input.html"
],
"sourcesContent": [
"<p class='foo'>red</p>\n\n<style>\n\t.foo {\n\t\tcolor: red;\n\t}\n</style>"
],
"names": [],
"mappings": "AAEO;CACN,iDAAI;;;AAGL"
}

@ -0,0 +1,17 @@
export function test ({ assert, smcCss, locateInSource, locateInGeneratedCss }) {
const expected = locateInSource( '.foo' );
const loc = locateInGeneratedCss( '.foo' );
const actual = smcCss.originalPositionFor({
line: loc.line + 1,
column: loc.column
});
assert.deepEqual( actual, {
source: 'input.html',
name: null,
line: expected.line + 1,
column: expected.column
});
}

@ -18,20 +18,9 @@ describe("validate", () => {
const input = fs.readFileSync(filename, "utf-8").replace(/\s+$/, "");
try {
const parsed = svelte.parse(input);
const errors = [];
const warnings = [];
svelte.validate(parsed, input, {
onerror(error) {
errors.push({
message: error.message,
pos: error.pos,
loc: error.loc
});
},
svelte.compile(input, {
onwarn(warning) {
warnings.push({
message: warning.message,
@ -41,16 +30,11 @@ describe("validate", () => {
}
});
const expectedErrors =
tryToLoadJson(`test/validator/samples/${dir}/errors.json`) || [];
const expectedWarnings =
tryToLoadJson(`test/validator/samples/${dir}/warnings.json`) || [];
assert.deepEqual(errors, expectedErrors);
assert.deepEqual(warnings, expectedWarnings);
} catch (err) {
if (err.name !== "ParseError") throw err;
try {
const expected = require(`./samples/${dir}/errors.json`)[0];

@ -1224,6 +1224,10 @@ estree-walker@^0.3.0:
version "0.3.1"
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.3.1.tgz#e6b1a51cf7292524e7237c312e5fe6660c1ce1aa"
estree-walker@^0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.5.0.tgz#aae3b57c42deb8010e349c892462f0e71c5dd1aa"
esutils@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b"
@ -1972,9 +1976,9 @@ magic-string@^0.19.0, magic-string@~0.19.0:
dependencies:
vlq "^0.2.1"
magic-string@^0.21.1:
version "0.21.3"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.21.3.tgz#87e201009ebfde6f46dc5757305a70af71e31624"
magic-string@^0.22.1:
version "0.22.1"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.22.1.tgz#a1bda64dfd4ae6c63797a45a67ee473b1f8d0e0f"
dependencies:
vlq "^0.2.1"

Loading…
Cancel
Save