Repository: sveltejs/svelte (JavaScript) — Feature

Issue:
Minify CSS, and remove unused rules
We're already detecting unused rules — we may as well remove them. While we're at it, we could easily remove whitespace to pack things down a little bit.


Discussion:

Resolving patch:
diff --git a/src/css/Selector.ts b/src/css/Selector.ts
--- a/src/css/Selector.ts
+++ b/src/css/Selector.ts
@@ -2,12 +2,6 @@ import MagicString from 'magic-string';
 import { Validator } from '../validate/index';
 import { Node } from '../interfaces';
 
-interface Block {
-	global: boolean;
-	combinator: Node;
-	selectors: Node[]
-}
-
 export default class Selector {
 	node: Node;
 	blocks: Block[];
@@ -43,6 +37,19 @@ export default class Selector {
 		}
 	}
 
+	minify(code: MagicString) {
+		let c: number = null;
+		this.blocks.forEach((block, i) => {
+			if (i > 0) {
+				if (block.start - c > 1) {
+					code.overwrite(c, block.start, block.combinator.name || ' ');
+				}
+			}
+
+			c = block.end;
+		});
+	}
+
 	transform(code: MagicString, attr: string) {
 		function encapsulateBlock(block: Block) {
 			let i = block.selectors.length;
@@ -203,28 +210,44 @@ function unquote(str: string) {
 	}
 }
 
+class Block {
+	global: boolean;
+	combinator: Node;
+	selectors: Node[]
+	start: number;
+	end: number;
+
+	constructor(combinator: Node) {
+		this.combinator = combinator;
+		this.global = false;
+		this.selectors = [];
+
+		this.start = null;
+		this.end = null;
+	}
+
+	add(selector: Node) {
+		if (this.selectors.length === 0) {
+			this.start = selector.start;
+			this.global = selector.type === 'PseudoClassSelector' && selector.name === 'global';
+		}
+
+		this.selectors.push(selector);
+		this.end = selector.end;
+	}
+}
+
 function groupSelectors(selector: Node) {
-	let block: Block = {
-		global: selector.children[0].type === 'PseudoClassSelector' && selector.children[0].name === 'global',
-		selectors: [],
-		combinator: null
-	};
+	let block: Block = new Block(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
-			};
-
+			block = new Block(child);
 			blocks.push(block);
 		} else {
-			block.selectors.push(child);
+			block.add(child);
 		}
 	});
 
diff --git a/src/css/Stylesheet.ts b/src/css/Stylesheet.ts
--- a/src/css/Stylesheet.ts
+++ b/src/css/Stylesheet.ts
@@ -8,18 +8,62 @@ import { Node, Parsed, Warning } from '../interfaces';
 
 class Rule {
 	selectors: Selector[];
-	declarations: Node[];
+	declarations: Declaration[];
+	node: Node;
+	parent: Atrule;
 
-	constructor(node: Node) {
+	constructor(node: Node, parent: Atrule) {
+		this.node = node;
+		this.parent = parent;
 		this.selectors = node.selector.children.map((node: Node) => new Selector(node));
-		this.declarations = node.block.children;
+		this.declarations = node.block.children.map((node: Node) => new Declaration(node));
+
+		if (parent) parent.rules.push(this);
 	}
 
 	apply(node: Node, stack: Node[]) {
 		this.selectors.forEach(selector => selector.apply(node, stack)); // TODO move the logic in here?
 	}
 
+	isUsed() {
+		if (this.parent && this.parent.node.type === 'Atrule' && this.parent.node.name === 'keyframes') return true;
+		return this.selectors.some(s => s.used);
+	}
+
+	minify(code: MagicString, cascade: boolean) {
+		let c = this.node.start;
+		this.selectors.forEach((selector, i) => {
+			if (cascade || selector.used) {
+				const separator = i > 0 ? ',' : '';
+				if ((selector.node.start - c) > separator.length) {
+					code.overwrite(c, selector.node.start, separator);
+				}
+
+				selector.minify(code);
+				c = selector.node.end;
+			}
+		});
+
+		code.remove(c, this.node.block.start);
+
+		c = this.node.block.start + 1;
+		this.declarations.forEach((declaration, i) => {
+			const separator = i > 0 ? ';' : '';
+			if ((declaration.node.start - c) > separator.length) {
+				code.overwrite(c, declaration.node.start, separator);
+			}
+
+			declaration.minify(code);
+
+			c = declaration.node.end;
+		});
+
+		code.remove(c, this.node.block.end - 1);
+	}
+
 	transform(code: MagicString, id: string, keyframes: Map<string, string>, cascade: boolean) {
+		if (this.parent && this.parent.node.type === 'Atrule' && this.parent.node.name === 'keyframes') return true;
+
 		const attr = `[${id}]`;
 
 		if (cascade) {
@@ -39,9 +83,9 @@ class Rule {
 					const head = firstToken.name === '*' ? '' : css.slice(start, insert);
 					const tail = css.slice(insert, end);
 
-					transformed = `${head}${attr}${tail}, ${attr} ${selectorString}`;
+					transformed = `${head}${attr}${tail},${attr} ${selectorString}`;
 				} else {
-					transformed = `${attr}${selectorString}, ${attr} ${selectorString}`;
+					transformed = `${attr}${selectorString},${attr} ${selectorString}`;
 				}
 
 				code.overwrite(start, end, transformed);
@@ -50,27 +94,87 @@ class Rule {
 			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));
-						}
+		this.declarations.forEach(declaration => declaration.transform(code, keyframes));
+	}
+}
+
+class Declaration {
+	node: Node;
+
+	constructor(node: Node) {
+		this.node = node;
+	}
+
+	transform(code: MagicString, keyframes: Map<string, string>) {
+		const property = this.node.property.toLowerCase();
+		if (property === 'animation' || property === 'animation-name') {
+			this.node.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));
 					}
-				});
-			}
-		});
+				}
+			});
+		}
+	}
+
+	minify(code: MagicString) {
+		const c = this.node.start + this.node.property.length;
+		const first = this.node.value.children[0];
+
+		if (first.start - c > 1) {
+			code.overwrite(c, first.start, ':');
+		}
 	}
 }
 
 class Atrule {
 	node: Node;
+	rules: Rule[];
 
 	constructor(node: Node) {
 		this.node = node;
+		this.rules = [];
+	}
+
+	isUsed() {
+		return true; // TODO
+	}
+
+	minify(code: MagicString, cascade: boolean) {
+		if (this.node.name === 'media') {
+			let c = this.node.start + 6;
+			if (this.node.expression.start > c) code.remove(c, this.node.expression.start);
+
+			this.node.expression.children.forEach((query: Node) => {
+				// TODO minify queries
+				c = query.end;
+			});
+
+			code.remove(c, this.node.block.start);
+		} else if (this.node.name === 'keyframes') {
+			let c = this.node.start + 10;
+			if (this.node.expression.start - c > 1) code.overwrite(c, this.node.expression.start, ' ');
+			c = this.node.expression.end;
+			if (this.node.block.start - c > 0) code.remove(c, this.node.block.start);
+		}
+
+		// TODO other atrules
+
+		if (this.node.block) {
+			let c = this.node.block.start + 1;
+
+			this.rules.forEach(rule => {
+				if (cascade || rule.isUsed()) {
+					code.remove(c, rule.node.start);
+					rule.minify(code, cascade);
+					c = rule.node.end;
+				}
+			});
+
+			code.remove(c, this.node.block.end - 1);
+		}
 	}
 
 	transform(code: MagicString, id: string, keyframes: Map<string, string>) {
@@ -133,10 +237,17 @@ export default class Stylesheet {
 						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);
+					if (node.type === 'Rule') {
+						// TODO this is a bit confusing. Don't have a separate
+						// array of rules, just transform top-level nodes and
+						// let them worry about their children
+						const rule = new Rule(node, currentAtrule);
+
 						this.rules.push(rule);
+
+						if (!currentAtrule) {
+							this.nodes.push(rule);
+						}
 					}
 				},
 
@@ -172,8 +283,6 @@ export default class Stylesheet {
 		}
 
 		const code = new MagicString(this.source);
-		code.remove(0, this.parsed.css.start + 7);
-		code.remove(this.parsed.css.end - 8, this.source.length);
 
 		walk(this.parsed.css, {
 			enter: (node: Node) => {
@@ -182,6 +291,8 @@ export default class Stylesheet {
 			}
 		});
 
+		// TODO all transform/minify in single pass. The mutation of
+		// `keyframes` here is confusing
 		const keyframes = new Map();
 		this.atrules.forEach((atrule: Atrule) => {
 			atrule.transform(code, this.id, keyframes);
@@ -191,6 +302,17 @@ export default class Stylesheet {
 			rule.transform(code, this.id, keyframes, this.cascade);
 		});
 
+		let c = 0;
+		this.nodes.forEach(node => {
+			if (this.cascade || node.isUsed()) {
+				code.remove(c, node.node.start);
+				node.minify(code, this.cascade);
+				c = node.node.end;
+			}
+		});
+
+		code.remove(c, this.source.length);
+
 		return {
 			css: code.toString(),
 			cssMap: code.generateMap({


Test patch:
diff --git a/test/css/index.js b/test/css/index.js
--- a/test/css/index.js
+++ b/test/css/index.js
@@ -69,7 +69,7 @@ describe("css", () => {
 				css: read(`test/css/samples/${dir}/expected.css`)
 			};
 
-			assert.equal(dom.css.replace(/svelte-\d+/g, 'svelte-xyz').trim(), expected.css.trim());
+			assert.equal(dom.css.replace(/svelte-\d+/g, 'svelte-xyz'), expected.css);
 
 			// verify that the right elements have scoping selectors
 			if (expected.html !== null) {
diff --git a/test/css/samples/basic/expected.css b/test/css/samples/basic/expected.css
--- a/test/css/samples/basic/expected.css
+++ b/test/css/samples/basic/expected.css
@@ -1,4 +1 @@
-
-	div[svelte-xyz], [svelte-xyz] div {
-		color: red;
-	}
+div[svelte-xyz],[svelte-xyz] div{color:red}
\ No newline at end of file
diff --git a/test/css/samples/cascade-false-global-keyframes/expected.css b/test/css/samples/cascade-false-global-keyframes/expected.css
--- a/test/css/samples/cascade-false-global-keyframes/expected.css
+++ b/test/css/samples/cascade-false-global-keyframes/expected.css
@@ -1,13 +1 @@
-
-	@keyframes why {
-		0% { color: red; }
-		100% { color: blue; }
-	}
-
-	.animated[svelte-xyz] {
-		animation: why 2s;
-	}
-
-	.also-animated[svelte-xyz] {
-		animation: not-defined-here 2s;
-	}
+@keyframes why{0%{color:red}100%{color:blue}}.animated[svelte-xyz]{animation:why 2s}.also-animated[svelte-xyz]{animation:not-defined-here 2s}
\ No newline at end of file
diff --git a/test/css/samples/cascade-false-global/expected.css b/test/css/samples/cascade-false-global/expected.css
--- a/test/css/samples/cascade-false-global/expected.css
+++ b/test/css/samples/cascade-false-global/expected.css
@@ -1,12 +1 @@
-
-	div {
-		color: red;
-	}
-
-	div.foo {
-		color: blue;
-	}
-
-	.foo {
-		font-weight: bold;
-	}
+div{color:red}div.foo{color:blue}.foo{font-weight:bold}
\ No newline at end of file
diff --git a/test/css/samples/cascade-false-keyframes/expected.css b/test/css/samples/cascade-false-keyframes/expected.css
--- a/test/css/samples/cascade-false-keyframes/expected.css
+++ b/test/css/samples/cascade-false-keyframes/expected.css
@@ -1,13 +1 @@
-
-	@keyframes svelte-xyz-why {
-		0% { color: red; }
-		100% { color: blue; }
-	}
-
-	.animated[svelte-xyz] {
-		animation: svelte-xyz-why 2s;
-	}
-
-	.also-animated[svelte-xyz] {
-		animation: not-defined-here 2s;
-	}
+@keyframes svelte-xyz-why{0%{color:red}100%{color:blue}}.animated[svelte-xyz]{animation:svelte-xyz-why 2s}.also-animated[svelte-xyz]{animation:not-defined-here 2s}
\ No newline at end of file
diff --git a/test/css/samples/cascade-false-pseudo-element/expected.css b/test/css/samples/cascade-false-pseudo-element/expected.css
--- a/test/css/samples/cascade-false-pseudo-element/expected.css
+++ b/test/css/samples/cascade-false-pseudo-element/expected.css
@@ -1,12 +1 @@
-
-	span[svelte-xyz]::after {
-		content: 'i am a pseudo-element';
-	}
-
-	span[svelte-xyz]:first-child {
-		color: red;
-	}
-
-	span[svelte-xyz]:last-child::after {
-		color: blue;
-	}
+span[svelte-xyz]::after{content:'i am a pseudo-element'}span[svelte-xyz]:first-child{color:red}span[svelte-xyz]:last-child::after{color:blue}
\ No newline at end of file
diff --git a/test/css/samples/cascade-false-universal-selector/expected.css b/test/css/samples/cascade-false-universal-selector/expected.css
--- a/test/css/samples/cascade-false-universal-selector/expected.css
+++ b/test/css/samples/cascade-false-universal-selector/expected.css
@@ -1,4 +1 @@
-
-	[svelte-xyz] {
-		color: red;
-	}
+[svelte-xyz]{color:red}
\ No newline at end of file
diff --git a/test/css/samples/cascade-false/expected.css b/test/css/samples/cascade-false/expected.css
--- a/test/css/samples/cascade-false/expected.css
+++ b/test/css/samples/cascade-false/expected.css
@@ -1,12 +1 @@
-
-	div[svelte-xyz] {
-		color: red;
-	}
-
-	div.foo[svelte-xyz] {
-		color: blue;
-	}
-
-	.foo[svelte-xyz] {
-		font-weight: bold;
-	}
+div[svelte-xyz]{color:red}div.foo[svelte-xyz]{color:blue}.foo[svelte-xyz]{font-weight:bold}
\ No newline at end of file
diff --git a/test/css/samples/keyframes/expected.css b/test/css/samples/keyframes/expected.css
--- a/test/css/samples/keyframes/expected.css
+++ b/test/css/samples/keyframes/expected.css
@@ -1,9 +1 @@
-
-	@keyframes svelte-xyz-why {
-		0% { color: red; }
-		100% { color: blue; }
-	}
-
-	[svelte-xyz].animated, [svelte-xyz] .animated {
-		animation: svelte-xyz-why 2s;
-	}
+@keyframes svelte-xyz-why{0%{color:red}100%{color:blue}}[svelte-xyz].animated,[svelte-xyz] .animated{animation:svelte-xyz-why 2s}
\ No newline at end of file
diff --git a/test/css/samples/media-query/expected.css b/test/css/samples/media-query/expected.css
--- a/test/css/samples/media-query/expected.css
+++ b/test/css/samples/media-query/expected.css
@@ -1,6 +1 @@
-
-	@media (min-width: 400px) {
-		[svelte-xyz].large-screen, [svelte-xyz] .large-screen {
-			display: block;
-		}
-	}
+@media(min-width: 400px){[svelte-xyz].large-screen,[svelte-xyz] .large-screen{display:block}}
\ No newline at end of file
diff --git a/test/css/samples/omit-scoping-attribute-attribute-selector-contains/expected.css b/test/css/samples/omit-scoping-attribute-attribute-selector-contains/expected.css
--- a/test/css/samples/omit-scoping-attribute-attribute-selector-contains/expected.css
+++ b/test/css/samples/omit-scoping-attribute-attribute-selector-contains/expected.css
@@ -1,4 +1 @@
-
-	[data-foo*='bar'][svelte-xyz] {
-		color: red;
-	}
+[data-foo*='bar'][svelte-xyz]{color:red}
\ No newline at end of file
diff --git a/test/css/samples/omit-scoping-attribute-attribute-selector-equals-case-insensitive/expected.css b/test/css/samples/omit-scoping-attribute-attribute-selector-equals-case-insensitive/expected.css
--- a/test/css/samples/omit-scoping-attribute-attribute-selector-equals-case-insensitive/expected.css
+++ b/test/css/samples/omit-scoping-attribute-attribute-selector-equals-case-insensitive/expected.css
@@ -1,4 +1 @@
-
-	[data-foo='bar' i][svelte-xyz] {
-		color: red;
-	}
+[data-foo='bar' i][svelte-xyz]{color:red}
\ No newline at end of file
diff --git a/test/css/samples/omit-scoping-attribute-attribute-selector-equals-dynamic/expected.css b/test/css/samples/omit-scoping-attribute-attribute-selector-equals-dynamic/expected.css
--- a/test/css/samples/omit-scoping-attribute-attribute-selector-equals-dynamic/expected.css
+++ b/test/css/samples/omit-scoping-attribute-attribute-selector-equals-dynamic/expected.css
@@ -1,4 +1 @@
-
-	[data-foo='bar'][svelte-xyz] {
-		color: red;
-	}
+[data-foo='bar'][svelte-xyz]{color:red}
\ No newline at end of file
diff --git a/test/css/samples/omit-scoping-attribute-attribute-selector-equals/expected.css b/test/css/samples/omit-scoping-attribute-attribute-selector-equals/expected.css
--- a/test/css/samples/omit-scoping-attribute-attribute-selector-equals/expected.css
+++ b/test/css/samples/omit-scoping-attribute-attribute-selector-equals/expected.css
@@ -1,4 +1 @@
-
-	[data-foo='bar'][svelte-xyz] {
-		color: red;
-	}
+[data-foo='bar'][svelte-xyz]{color:red}
\ No newline at end of file
diff --git a/test/css/samples/omit-scoping-attribute-attribute-selector-pipe-equals/expected.css b/test/css/samples/omit-scoping-attribute-attribute-selector-pipe-equals/expected.css
--- a/test/css/samples/omit-scoping-attribute-attribute-selector-pipe-equals/expected.css
+++ b/test/css/samples/omit-scoping-attribute-attribute-selector-pipe-equals/expected.css
@@ -1,4 +1 @@
-
-	[data-foo|='bar'][svelte-xyz] {
-		color: red;
-	}
+[data-foo|='bar'][svelte-xyz]{color:red}
\ No newline at end of file
diff --git a/test/css/samples/omit-scoping-attribute-attribute-selector-prefix/expected.css b/test/css/samples/omit-scoping-attribute-attribute-selector-prefix/expected.css
--- a/test/css/samples/omit-scoping-attribute-attribute-selector-prefix/expected.css
+++ b/test/css/samples/omit-scoping-attribute-attribute-selector-prefix/expected.css
@@ -1,4 +1 @@
-
-	[data-foo^='bar'][svelte-xyz] {
-		color: red;
-	}
+[data-foo^='bar'][svelte-xyz]{color:red}
\ No newline at end of file
diff --git a/test/css/samples/omit-scoping-attribute-attribute-selector-suffix/expected.css b/test/css/samples/omit-scoping-attribute-attribute-selector-suffix/expected.css
--- a/test/css/samples/omit-scoping-attribute-attribute-selector-suffix/expected.css
+++ b/test/css/samples/omit-scoping-attribute-attribute-selector-suffix/expected.css
@@ -1,4 +1 @@
-
-	[data-foo$='bar'][svelte-xyz] {
-		color: red;
-	}
+[data-foo$='bar'][svelte-xyz]{color:red}
\ No newline at end of file
diff --git a/test/css/samples/omit-scoping-attribute-attribute-selector-word-equals/expected.css b/test/css/samples/omit-scoping-attribute-attribute-selector-word-equals/expected.css
--- a/test/css/samples/omit-scoping-attribute-attribute-selector-word-equals/expected.css
+++ b/test/css/samples/omit-scoping-attribute-attribute-selector-word-equals/expected.css
@@ -1,4 +1 @@
-
-	[data-foo~='bar'][svelte-xyz] {
-		color: red;
-	}
+[data-foo~='bar'][svelte-xyz]{color:red}
\ No newline at end of file
diff --git a/test/css/samples/omit-scoping-attribute-attribute-selector/expected.css b/test/css/samples/omit-scoping-attribute-attribute-selector/expected.css
--- a/test/css/samples/omit-scoping-attribute-attribute-selector/expected.css
+++ b/test/css/samples/omit-scoping-attribute-attribute-selector/expected.css
@@ -1,4 +1 @@
-
-	[autoplay][svelte-xyz] {
-		color: red;
-	}
+[autoplay][svelte-xyz]{color:red}
\ No newline at end of file
diff --git a/test/css/samples/omit-scoping-attribute-class-dynamic/expected.css b/test/css/samples/omit-scoping-attribute-class-dynamic/expected.css
--- a/test/css/samples/omit-scoping-attribute-class-dynamic/expected.css
+++ b/test/css/samples/omit-scoping-attribute-class-dynamic/expected.css
@@ -1,4 +1 @@
-
-	.foo[svelte-xyz] {
-		color: red;
-	}
+.foo[svelte-xyz]{color:red}
\ No newline at end of file
diff --git a/test/css/samples/omit-scoping-attribute-class-static/expected.css b/test/css/samples/omit-scoping-attribute-class-static/expected.css
--- a/test/css/samples/omit-scoping-attribute-class-static/expected.css
+++ b/test/css/samples/omit-scoping-attribute-class-static/expected.css
@@ -1,4 +1 @@
-
-	.foo[svelte-xyz] {
-		color: red;
-	}
+.foo[svelte-xyz]{color:red}
\ No newline at end of file
diff --git a/test/css/samples/omit-scoping-attribute-descendant-global-inner-class/expected.css b/test/css/samples/omit-scoping-attribute-descendant-global-inner-class/expected.css
--- a/test/css/samples/omit-scoping-attribute-descendant-global-inner-class/expected.css
+++ b/test/css/samples/omit-scoping-attribute-descendant-global-inner-class/expected.css
@@ -1,4 +1 @@
-
-	.foo[svelte-xyz] .bar {
-		color: red;
-	}
+.foo[svelte-xyz] .bar{color:red}
\ No newline at end of file
diff --git a/test/css/samples/omit-scoping-attribute-descendant-global-inner-multiple/expected.css b/test/css/samples/omit-scoping-attribute-descendant-global-inner-multiple/expected.css
--- a/test/css/samples/omit-scoping-attribute-descendant-global-inner-multiple/expected.css
+++ b/test/css/samples/omit-scoping-attribute-descendant-global-inner-multiple/expected.css
@@ -1,4 +1 @@
-
-	div[svelte-xyz] > p > em {
-		color: red;
-	}
+div[svelte-xyz]>p>em{color:red}
\ No newline at end of file
diff --git a/test/css/samples/omit-scoping-attribute-descendant-global-inner/expected.css b/test/css/samples/omit-scoping-attribute-descendant-global-inner/expected.css
--- a/test/css/samples/omit-scoping-attribute-descendant-global-inner/expected.css
+++ b/test/css/samples/omit-scoping-attribute-descendant-global-inner/expected.css
@@ -1,4 +1 @@
-
-	div[svelte-xyz] > p {
-		color: red;
-	}
+div[svelte-xyz]>p{color:red}
\ No newline at end of file
diff --git a/test/css/samples/omit-scoping-attribute-descendant-global-outer-multiple/expected.css b/test/css/samples/omit-scoping-attribute-descendant-global-outer-multiple/expected.css
--- a/test/css/samples/omit-scoping-attribute-descendant-global-outer-multiple/expected.css
+++ b/test/css/samples/omit-scoping-attribute-descendant-global-outer-multiple/expected.css
@@ -1,4 +1 @@
-
-	div > section > p[svelte-xyz] {
-		color: red;
-	}
+div>section>p[svelte-xyz]{color:red}
\ No newline at end of file
diff --git a/test/css/samples/omit-scoping-attribute-descendant-global-outer/expected.css b/test/css/samples/omit-scoping-attribute-descendant-global-outer/expected.css
--- a/test/css/samples/omit-scoping-attribute-descendant-global-outer/expected.css
+++ b/test/css/samples/omit-scoping-attribute-descendant-global-outer/expected.css
@@ -1,4 +1 @@
-
-	div > p[svelte-xyz] {
-		color: red;
-	}
+div>p[svelte-xyz]{color:red}
\ No newline at end of file
diff --git a/test/css/samples/omit-scoping-attribute-descendant/expected.css b/test/css/samples/omit-scoping-attribute-descendant/expected.css
--- a/test/css/samples/omit-scoping-attribute-descendant/expected.css
+++ b/test/css/samples/omit-scoping-attribute-descendant/expected.css
@@ -1,4 +0,0 @@
-
-	div[svelte-xyz] > p[svelte-xyz] {
-		color: red;
-	}
diff --git a/test/css/samples/omit-scoping-attribute-global/expected.css b/test/css/samples/omit-scoping-attribute-global/expected.css
--- a/test/css/samples/omit-scoping-attribute-global/expected.css
+++ b/test/css/samples/omit-scoping-attribute-global/expected.css
@@ -1,4 +1 @@
-
-	div {
-		color: red;
-	}
+div{color:red}
\ No newline at end of file
diff --git a/test/css/samples/omit-scoping-attribute-id/expected.css b/test/css/samples/omit-scoping-attribute-id/expected.css
--- a/test/css/samples/omit-scoping-attribute-id/expected.css
+++ b/test/css/samples/omit-scoping-attribute-id/expected.css
@@ -1,4 +1 @@
-
-	#foo[svelte-xyz] {
-		color: red;
-	}
+#foo[svelte-xyz]{color:red}
\ No newline at end of file
diff --git a/test/css/samples/omit-scoping-attribute-whitespace-multiple/expected.css b/test/css/samples/omit-scoping-attribute-whitespace-multiple/expected.css
--- a/test/css/samples/omit-scoping-attribute-whitespace-multiple/expected.css
+++ b/test/css/samples/omit-scoping-attribute-whitespace-multiple/expected.css
@@ -1,4 +1 @@
-
-	div[svelte-xyz] section p[svelte-xyz] {
-		color: red;
-	}
+div[svelte-xyz] section p[svelte-xyz]{color:red}
\ No newline at end of file
diff --git a/test/css/samples/omit-scoping-attribute-whitespace/expected.css b/test/css/samples/omit-scoping-attribute-whitespace/expected.css
--- a/test/css/samples/omit-scoping-attribute-whitespace/expected.css
+++ b/test/css/samples/omit-scoping-attribute-whitespace/expected.css
@@ -1,4 +1 @@
-
-	div[svelte-xyz] p[svelte-xyz] {
-		color: red;
-	}
+div[svelte-xyz] p[svelte-xyz]{color:red}
\ No newline at end of file
diff --git a/test/css/samples/omit-scoping-attribute/expected.css b/test/css/samples/omit-scoping-attribute/expected.css
--- a/test/css/samples/omit-scoping-attribute/expected.css
+++ b/test/css/samples/omit-scoping-attribute/expected.css
@@ -1,4 +1 @@
-
-	p[svelte-xyz] {
-		color: red;
-	}
+p[svelte-xyz]{color:red}
\ No newline at end of file
diff --git a/test/css/samples/universal-selector/expected.css b/test/css/samples/universal-selector/expected.css
--- a/test/css/samples/universal-selector/expected.css
+++ b/test/css/samples/universal-selector/expected.css
@@ -1,4 +1 @@
-
-	[svelte-xyz], [svelte-xyz] * {
-		color: red;
-	}
+[svelte-xyz],[svelte-xyz] *{color:red}
\ No newline at end of file
diff --git a/test/css/samples/unused-selector/expected.css b/test/css/samples/unused-selector/expected.css
--- a/test/css/samples/unused-selector/expected.css
+++ b/test/css/samples/unused-selector/expected.css
@@ -1,8 +1 @@
-
-	.foo[svelte-xyz] {
-		color: red;
-	}
-
-	.bar[svelte-xyz] {
-		color: blue;
-	}
+.foo[svelte-xyz]{color:red}
\ No newline at end of file
diff --git a/test/js/samples/collapses-text-around-comments/expected-bundle.js b/test/js/samples/collapses-text-around-comments/expected-bundle.js
--- a/test/js/samples/collapses-text-around-comments/expected-bundle.js
+++ b/test/js/samples/collapses-text-around-comments/expected-bundle.js
@@ -146,7 +146,7 @@ var template = (function () {
 function add_css () {
 	var style = createElement( 'style' );
 	style.id = 'svelte-3590263702-style';
-	style.textContent = "\n\tp[svelte-3590263702], [svelte-3590263702] p {\n\t\tcolor: red;\n\t}\n";
+	style.textContent = "p[svelte-3590263702],[svelte-3590263702] p{color:red}";
 	appendNode( style, document.head );
 }
 
diff --git a/test/js/samples/collapses-text-around-comments/expected.js b/test/js/samples/collapses-text-around-comments/expected.js
--- a/test/js/samples/collapses-text-around-comments/expected.js
+++ b/test/js/samples/collapses-text-around-comments/expected.js
@@ -11,7 +11,7 @@ var template = (function () {
 function add_css () {
 	var style = createElement( 'style' );
 	style.id = 'svelte-3590263702-style';
-	style.textContent = "\n\tp[svelte-3590263702], [svelte-3590263702] p {\n\t\tcolor: red;\n\t}\n";
+	style.textContent = "p[svelte-3590263702],[svelte-3590263702] p{color:red}";
 	appendNode( style, document.head );
 }
 
diff --git a/test/js/samples/css-media-query/expected-bundle.js b/test/js/samples/css-media-query/expected-bundle.js
--- a/test/js/samples/css-media-query/expected-bundle.js
+++ b/test/js/samples/css-media-query/expected-bundle.js
@@ -134,7 +134,7 @@ var proto = {
 function add_css () {
 	var style = createElement( 'style' );
 	style.id = 'svelte-2363328337-style';
-	style.textContent = "\n\t@media (min-width: 1px) {\n\t\tdiv[svelte-2363328337], [svelte-2363328337] div {\n\t\t\tcolor: red;\n\t\t}\n\t}\n";
+	style.textContent = "@media(min-width: 1px){div[svelte-2363328337],[svelte-2363328337] div{color:red}}";
 	appendNode( style, document.head );
 }
 
diff --git a/test/js/samples/css-media-query/expected.js b/test/js/samples/css-media-query/expected.js
--- a/test/js/samples/css-media-query/expected.js
+++ b/test/js/samples/css-media-query/expected.js
@@ -3,7 +3,7 @@ import { appendNode, assign, createElement, detachNode, dispatchObservers, inser
 function add_css () {
 	var style = createElement( 'style' );
 	style.id = 'svelte-2363328337-style';
-	style.textContent = "\n\t@media (min-width: 1px) {\n\t\tdiv[svelte-2363328337], [svelte-2363328337] div {\n\t\t\tcolor: red;\n\t\t}\n\t}\n";
+	style.textContent = "@media(min-width: 1px){div[svelte-2363328337],[svelte-2363328337] div{color:red}}";
 	appendNode( style, document.head );
 }
 
diff --git a/test/server-side-rendering/samples/styles-nested/_actual.css b/test/server-side-rendering/samples/styles-nested/_actual.css
--- a/test/server-side-rendering/samples/styles-nested/_actual.css
+++ b/test/server-side-rendering/samples/styles-nested/_actual.css
@@ -1,14 +1,3 @@
-
-	div[svelte-1408461649], [svelte-1408461649] div {
-		color: red;
-	}
-
-
-	div[svelte-54999591], [svelte-54999591] div {
-		color: green;
-	}
-
-
-	div[svelte-2385185803], [svelte-2385185803] div {
-		color: blue;
-	}
+div[svelte-1408461649],[svelte-1408461649] div{color:red}
+div[svelte-54999591],[svelte-54999591] div{color:green}
+div[svelte-2385185803],[svelte-2385185803] div{color:blue}
\ No newline at end of file
diff --git a/test/server-side-rendering/samples/styles-nested/_expected.css b/test/server-side-rendering/samples/styles-nested/_expected.css
--- a/test/server-side-rendering/samples/styles-nested/_expected.css
+++ b/test/server-side-rendering/samples/styles-nested/_expected.css
@@ -1,14 +1,3 @@
-
-	div[svelte-1408461649], [svelte-1408461649] div {
-		color: red;
-	}
-
-
-	div[svelte-54999591], [svelte-54999591] div {
-		color: green;
-	}
-
-
-	div[svelte-2385185803], [svelte-2385185803] div {
-		color: blue;
-	}
+div[svelte-1408461649],[svelte-1408461649] div{color:red}
+div[svelte-54999591],[svelte-54999591] div{color:green}
+div[svelte-2385185803],[svelte-2385185803] div{color:blue}
\ No newline at end of file
diff --git a/test/server-side-rendering/samples/styles/_actual.css b/test/server-side-rendering/samples/styles/_actual.css
--- a/test/server-side-rendering/samples/styles/_actual.css
+++ b/test/server-side-rendering/samples/styles/_actual.css
@@ -1,4 +1 @@
-
-	div[svelte-2278551596], [svelte-2278551596] div {
-		color: red;
-	}
+div[svelte-2278551596],[svelte-2278551596] div{color:red}
\ No newline at end of file
diff --git a/test/server-side-rendering/samples/styles/_expected.css b/test/server-side-rendering/samples/styles/_expected.css
--- a/test/server-side-rendering/samples/styles/_expected.css
+++ b/test/server-side-rendering/samples/styles/_expected.css
@@ -1,4 +1 @@
-
-	div[svelte-2278551596], [svelte-2278551596] div {
-		color: red;
-	}
+div[svelte-2278551596],[svelte-2278551596] div{color:red}
\ No newline at end of file
diff --git a/test/sourcemaps/samples/css-cascade-false/output.css b/test/sourcemaps/samples/css-cascade-false/output.css
--- a/test/sourcemaps/samples/css-cascade-false/output.css
+++ b/test/sourcemaps/samples/css-cascade-false/output.css
@@ -1,6 +1,2 @@
-
-	.foo[svelte-2772200924] {
-		color: red;
-	}
-
+.foo[svelte-2772200924]{color:red}
 /*# sourceMappingURL=output.css.map */
\ No newline at end of file
diff --git a/test/sourcemaps/samples/css-cascade-false/output.css.map b/test/sourcemaps/samples/css-cascade-false/output.css.map
--- a/test/sourcemaps/samples/css-cascade-false/output.css.map
+++ b/test/sourcemaps/samples/css-cascade-false/output.css.map
@@ -8,5 +8,5 @@
     "<p class='foo'>red</p>\n\n<style>\n\t.foo {\n\t\tcolor: red;\n\t}\n</style>"
   ],
   "names": [],
-  "mappings": "AAEO;CACN,uBAAI,CAAC;EACJ,MAAM,CAAC,GAAG;EACV;AACF"
+  "mappings": "AAGC,IAAI,mBAAC,CAAC,AACL,KAAK,CAAE,GAAG,AACX,CAAC"
 }
\ No newline at end of file
diff --git a/test/sourcemaps/samples/css/output.css b/test/sourcemaps/samples/css/output.css
--- a/test/sourcemaps/samples/css/output.css
+++ b/test/sourcemaps/samples/css/output.css
@@ -1,6 +1,2 @@
-
-	[svelte-2772200924].foo, [svelte-2772200924] .foo {
-		color: red;
-	}
-
+[svelte-2772200924].foo,[svelte-2772200924] .foo{color:red}
 /*# sourceMappingURL=output.css.map */
\ No newline at end of file
diff --git a/test/sourcemaps/samples/css/output.css.map b/test/sourcemaps/samples/css/output.css.map
--- a/test/sourcemaps/samples/css/output.css.map
+++ b/test/sourcemaps/samples/css/output.css.map
@@ -8,5 +8,5 @@
     "<p class='foo'>red</p>\n\n<style>\n\t.foo {\n\t\tcolor: red;\n\t}\n</style>"
   ],
   "names": [],
-  "mappings": "AAEO;CACN,iDAAI,CAAC;EACJ,MAAM,CAAC,GAAG;EACV;AACF"
+  "mappings": "AAGC,gDAAK,CAAC,AACL,KAAK,CAAE,GAAG,AACX,CAAC"
 }
\ No newline at end of file