Skip to content

Commit 8bd0173

Browse files
authored
Add @layer support (#483)
* layers support * finish implementation * add more tests * add more tests
1 parent e2d2890 commit 8bd0173

File tree

8 files changed

+175
-27
lines changed

8 files changed

+175
-27
lines changed

index.js

Lines changed: 63 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const path = require("path")
44

55
// internal tooling
66
const joinMedia = require("./lib/join-media")
7+
const joinLayer = require("./lib/join-layer")
78
const resolveId = require("./lib/resolve-id")
89
const loadContent = require("./lib/load-content")
910
const processContent = require("./lib/process-content")
@@ -46,11 +47,13 @@ function AtImport(options) {
4647
throw new Error("plugins option must be an array")
4748
}
4849

49-
return parseStyles(result, styles, options, state, []).then(bundle => {
50-
applyRaws(bundle)
51-
applyMedia(bundle)
52-
applyStyles(bundle, styles)
53-
})
50+
return parseStyles(result, styles, options, state, [], []).then(
51+
bundle => {
52+
applyRaws(bundle)
53+
applyMedia(bundle)
54+
applyStyles(bundle, styles)
55+
}
56+
)
5457

5558
function applyRaws(bundle) {
5659
bundle.forEach((stmt, index) => {
@@ -68,21 +71,60 @@ function AtImport(options) {
6871

6972
function applyMedia(bundle) {
7073
bundle.forEach(stmt => {
71-
if (!stmt.media.length || stmt.type === "charset") return
74+
if (
75+
(!stmt.media.length && !stmt.layer.length) ||
76+
stmt.type === "charset"
77+
) {
78+
return
79+
}
80+
7281
if (stmt.type === "import") {
7382
stmt.node.params = `${stmt.fullUri} ${stmt.media.join(", ")}`
74-
} else if (stmt.type === "media")
83+
} else if (stmt.type === "media") {
7584
stmt.node.params = stmt.media.join(", ")
76-
else {
85+
} else {
7786
const { nodes } = stmt
7887
const { parent } = nodes[0]
79-
const mediaNode = atRule({
80-
name: "media",
81-
params: stmt.media.join(", "),
82-
source: parent.source,
83-
})
8488

85-
parent.insertBefore(nodes[0], mediaNode)
89+
let outerAtRule
90+
let innerAtRule
91+
if (stmt.media.length && stmt.layer.length) {
92+
const mediaNode = atRule({
93+
name: "media",
94+
params: stmt.media.join(", "),
95+
source: parent.source,
96+
})
97+
98+
const layerNode = atRule({
99+
name: "layer",
100+
params: stmt.layer.filter(layer => layer !== "").join("."),
101+
source: parent.source,
102+
})
103+
104+
mediaNode.append(layerNode)
105+
innerAtRule = layerNode
106+
outerAtRule = mediaNode
107+
} else if (stmt.media.length) {
108+
const mediaNode = atRule({
109+
name: "media",
110+
params: stmt.media.join(", "),
111+
source: parent.source,
112+
})
113+
114+
innerAtRule = mediaNode
115+
outerAtRule = mediaNode
116+
} else if (stmt.layer.length) {
117+
const layerNode = atRule({
118+
name: "layer",
119+
params: stmt.layer.filter(layer => layer !== "").join("."),
120+
source: parent.source,
121+
})
122+
123+
innerAtRule = layerNode
124+
outerAtRule = layerNode
125+
}
126+
127+
parent.insertBefore(nodes[0], outerAtRule)
86128

87129
// remove nodes
88130
nodes.forEach(node => {
@@ -92,11 +134,11 @@ function AtImport(options) {
92134
// better output
93135
nodes[0].raws.before = nodes[0].raws.before || "\n"
94136

95-
// wrap new rules with media query
96-
mediaNode.append(nodes)
137+
// wrap new rules with media query and/or layer at rule
138+
innerAtRule.append(nodes)
97139

98140
stmt.type = "media"
99-
stmt.node = mediaNode
141+
stmt.node = outerAtRule
100142
delete stmt.nodes
101143
}
102144
})
@@ -119,7 +161,7 @@ function AtImport(options) {
119161
})
120162
}
121163

122-
function parseStyles(result, styles, options, state, media) {
164+
function parseStyles(result, styles, options, state, media, layer) {
123165
const statements = parseStatements(result, styles)
124166

125167
return Promise.resolve(statements)
@@ -128,6 +170,7 @@ function AtImport(options) {
128170
return stmts.reduce((promise, stmt) => {
129171
return promise.then(() => {
130172
stmt.media = joinMedia(media, stmt.media || [])
173+
stmt.layer = joinLayer(layer, stmt.layer || [])
131174

132175
// skip protocol base uri (protocol://url) or protocol-relative
133176
if (
@@ -239,7 +282,7 @@ function AtImport(options) {
239282

240283
function loadImportContent(result, stmt, filename, options, state) {
241284
const atRule = stmt.node
242-
const { media } = stmt
285+
const { media, layer } = stmt
243286
if (options.skipDuplicates) {
244287
// skip files already imported at the same scope
245288
if (
@@ -287,7 +330,7 @@ function AtImport(options) {
287330
}
288331

289332
// recursion: import @import from imported file
290-
return parseStyles(result, styles, options, state, media)
333+
return parseStyles(result, styles, options, state, media, layer)
291334
})
292335
}
293336
)

lib/join-layer.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"use strict"
2+
3+
module.exports = function (parentLayer, childLayer) {
4+
if (!parentLayer.length && childLayer.length) return childLayer
5+
if (parentLayer.length && !childLayer.length) return parentLayer
6+
if (!parentLayer.length && !childLayer.length) return []
7+
8+
return parentLayer.concat(childLayer)
9+
}

lib/parse-statements.js

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ module.exports = function (result, styles) {
3838
type: "nodes",
3939
nodes,
4040
media: [],
41+
layer: [],
4142
})
4243
nodes = []
4344
}
@@ -50,6 +51,7 @@ module.exports = function (result, styles) {
5051
type: "nodes",
5152
nodes,
5253
media: [],
54+
layer: [],
5355
})
5456
}
5557

@@ -62,6 +64,7 @@ function parseMedia(result, atRule) {
6264
type: "media",
6365
node: atRule,
6466
media: split(params, 0),
67+
layer: [],
6568
}
6669
}
6770

@@ -75,6 +78,7 @@ function parseCharset(result, atRule) {
7578
type: "charset",
7679
node: atRule,
7780
media: [],
81+
layer: [],
7882
}
7983
}
8084

@@ -85,10 +89,12 @@ function parseImport(result, atRule) {
8589
if (
8690
prev.type !== "comment" &&
8791
(prev.type !== "atrule" ||
88-
(prev.name !== "import" && prev.name !== "charset"))
92+
(prev.name !== "import" &&
93+
prev.name !== "charset" &&
94+
!(prev.name === "layer" && !prev.nodes)))
8995
) {
9096
return result.warn(
91-
"@import must precede all other statements (besides @charset)",
97+
"@import must precede all other statements (besides @charset or empty @layer)",
9298
{ node: atRule }
9399
)
94100
}
@@ -109,6 +115,7 @@ function parseImport(result, atRule) {
109115
type: "import",
110116
node: atRule,
111117
media: [],
118+
layer: [],
112119
}
113120

114121
// prettier-ignore
@@ -134,11 +141,31 @@ function parseImport(result, atRule) {
134141
else stmt.uri = params[0].nodes[0].value
135142
stmt.fullUri = stringify(params[0])
136143

137-
if (params.length > 2) {
138-
if (params[1].type !== "space") {
144+
let remainder = params
145+
if (remainder.length > 2) {
146+
if (
147+
(remainder[2].type === "word" || remainder[2].type === "function") &&
148+
remainder[2].value === "layer"
149+
) {
150+
if (remainder[1].type !== "space") {
151+
return result.warn("Invalid import layer statement", { node: atRule })
152+
}
153+
154+
if (remainder[2].nodes) {
155+
stmt.layer = [stringify(remainder[2].nodes)]
156+
} else {
157+
stmt.layer = [""]
158+
}
159+
remainder = remainder.slice(2)
160+
}
161+
}
162+
163+
if (remainder.length > 2) {
164+
if (remainder[1].type !== "space") {
139165
return result.warn("Invalid import media statement", { node: atRule })
140166
}
141-
stmt.media = split(params, 2)
167+
168+
stmt.media = split(remainder, 2)
142169
}
143170

144171
return stmt
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
@layer foo {
2+
foo {}
3+
}

test/fixtures/layer.css

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
@layer layer-alpha, layer-beta.one;
2+
3+
@import "foo.css" layer;
4+
@import 'bar.css' layer(bar);
5+
@import 'bar.css' layer(bar) level-1 and level-2;
6+
@import url(baz.css) layer;
7+
@import url("foobar.css") layer(foobar);
8+
@import url("foo-layered.css") layer(foo-layered);
9+
10+
content{}

test/fixtures/layer.expected.css

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
@layer layer-alpha, layer-beta.one;
2+
@layer{
3+
foo{}
4+
}
5+
@layer bar{
6+
bar{}
7+
}
8+
@media level-1 and level-2{
9+
@layer bar{
10+
bar{}
11+
}
12+
}
13+
@layer{
14+
baz{}
15+
}
16+
@layer foobar{
17+
foobar{}
18+
}
19+
@layer foo-layered{
20+
@layer foo {
21+
foo {}
22+
}
23+
}
24+
content{}

test/layer.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
"use strict"
2+
// external tooling
3+
const test = require("ava")
4+
5+
// internal tooling
6+
const checkFixture = require("./helpers/check-fixture")
7+
8+
test("should resolve layers of import statements", checkFixture, "layer")

test/lint.js

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ test("should warn when not @charset and not @import statement before", t => {
1818
t.is(warnings.length, 1)
1919
t.is(
2020
warnings[0].text,
21-
"@import must precede all other statements (besides @charset)"
21+
"@import must precede all other statements (besides @charset or empty @layer)"
2222
)
2323
})
2424
})
@@ -39,12 +39,36 @@ test("should warn about all imports after some other CSS declaration", t => {
3939
result.warnings().forEach(warning => {
4040
t.is(
4141
warning.text,
42-
"@import must precede all other statements (besides @charset)"
42+
"@import must precede all other statements (besides @charset or empty @layer)"
4343
)
4444
})
4545
})
4646
})
4747

48+
test("should warn if non-empty @layer before @import", t => {
49+
return processor
50+
.process(`@layer { a {} } @import "a.css";`, { from: undefined })
51+
.then(result => {
52+
t.plan(1)
53+
result.warnings().forEach(warning => {
54+
t.is(
55+
warning.text,
56+
"@import must precede all other statements (besides @charset or empty @layer)"
57+
)
58+
})
59+
})
60+
})
61+
62+
test("should not warn if empty @layer before @import", t => {
63+
return processor
64+
.process(`@layer a; @import "";`, { from: undefined })
65+
.then(result => {
66+
const warnings = result.warnings()
67+
t.is(warnings.length, 1)
68+
t.is(warnings[0].text, `Unable to find uri in '@import ""'`)
69+
})
70+
})
71+
4872
test("should not warn if comments before @import", t => {
4973
return processor
5074
.process(`/* skipped comment */ @import "";`, { from: undefined })

0 commit comments

Comments
 (0)