Repository: sveltejs/svelte (JavaScript)
Date: 2017-09-27 18:23:25+00:00
Task type: Bug Fix

Issue:
CSS @apply Rule support
The [`@apply`](https://tabatkins.github.io/specs/css-apply-rule/) rule [isn't currently supported by any browser](http://caniuse.com/#feat=css-apply-rule) but in my opinion it is a very useful feature in order to keep styles reusable by sharing global custom sets down to scoped styles. 

To overcome the lack of support of custom sets, I tried to use cascading mechanism between components but I encountered several troubles to avoid unwanted style overrides with root level shared classes.

At the end I found a way to process styles with PostCSS/nextcss but when I try to use the `@apply` rule in a component I get the following error: [Cannot read property 'toLowerCase' of undefined](https://svelte.technology/repl?version=1.40.1&gist=3be642fa5e37204571256533a713c5c2).

I was wondering if it is a known limitation of the parser and if there is a way to fix it.


Issue discussion:
My understanding is that `@apply` is effectively dead in the water, which is a shame as it would have been very useful. That notwithstanding, it'd probably be nice if we just disregarded constructs that Svelte doesn't know what to do with (as long as they're parseable by css-tree — beyond those cases, it's probably better to process the styles *before* Svelte gets to them).

Looking at the stack trace, it should be straightforward to fix it so that it just ignores `@apply`.

Solution patch:
diff --git a/src/css/Stylesheet.ts b/src/css/Stylesheet.ts
--- a/src/css/Stylesheet.ts
+++ b/src/css/Stylesheet.ts
@@ -120,7 +120,7 @@ class Declaration {
 	}
 
 	transform(code: MagicString, keyframes: Map<string, string>) {
-		const property = this.node.property.toLowerCase();
+		const property = this.node.property && this.node.property.toLowerCase();
 		if (property === 'animation' || property === 'animation-name') {
 			this.node.value.children.forEach((block: Node) => {
 				if (block.type === 'Identifier') {
@@ -134,6 +134,8 @@ class Declaration {
 	}
 
 	minify(code: MagicString) {
+		if (!this.node.property) return; // @apply, and possibly other weird cases?
+
 		const c = this.node.start + this.node.property.length;
 		const first = this.node.value.children  ?
 			this.node.value.children[0] :
@@ -274,15 +276,21 @@ export default class Stylesheet {
 		if (parsed.css && parsed.css.children.length) {
 			this.hasStyles = true;
 
-			const stack: Atrule[] = [];
+			const stack: (Rule | Atrule)[] = [];
 			let currentAtrule: Atrule = null;
 
 			walk(this.parsed.css, {
 				enter: (node: Node) => {
 					if (node.type === 'Atrule') {
+						const last = stack[stack.length - 1];
+
 						const atrule = new Atrule(node);
 						stack.push(atrule);
 
+						// this is an awkward special case — @apply (and
+						// possibly other future constructs)
+						if (last && !(last instanceof Atrule)) return;
+
 						if (currentAtrule) {
 							currentAtrule.children.push(atrule);
 						} else {
@@ -302,6 +310,7 @@ export default class Stylesheet {
 
 					if (node.type === 'Rule') {
 						const rule = new Rule(node, currentAtrule);
+						stack.push(rule);
 
 						if (currentAtrule) {
 							currentAtrule.children.push(rule);
@@ -312,10 +321,8 @@ export default class Stylesheet {
 				},
 
 				leave: (node: Node) => {
-					if (node.type === 'Atrule') {
-						stack.pop();
-						currentAtrule = stack[stack.length - 1];
-					}
+					if (node.type === 'Rule' || node.type === 'Atrule') stack.pop();
+					if (node.type === 'Atrule') currentAtrule = stack[stack.length - 1];
 				}
 			});
 		} else {


Test patch:
diff --git a/test/css/samples/unknown-at-rule/expected.css b/test/css/samples/unknown-at-rule/expected.css
new file mode 100644
--- /dev/null
+++ b/test/css/samples/unknown-at-rule/expected.css
@@ -0,0 +1 @@
+div[svelte-xyz],[svelte-xyz] div{@apply --funky-div;}
\ No newline at end of file
diff --git a/test/css/samples/unknown-at-rule/input.html b/test/css/samples/unknown-at-rule/input.html
new file mode 100644
--- /dev/null
+++ b/test/css/samples/unknown-at-rule/input.html
@@ -0,0 +1,7 @@
+<div></div>
+
+<style>
+	div {
+		@apply --funky-div;
+	}
+</style>
\ No newline at end of file