Skip to content

Commit

Permalink
Add @layer support (#483)
Browse files Browse the repository at this point in the history
* layers support

* finish implementation

* add more tests

* add more tests
  • Loading branch information
romainmenke committed Mar 22, 2022
1 parent e2d2890 commit 8bd0173
Show file tree
Hide file tree
Showing 8 changed files with 175 additions and 27 deletions.
83 changes: 63 additions & 20 deletions index.js
Expand Up @@ -4,6 +4,7 @@ const path = require("path")

// internal tooling
const joinMedia = require("./lib/join-media")
const joinLayer = require("./lib/join-layer")
const resolveId = require("./lib/resolve-id")
const loadContent = require("./lib/load-content")
const processContent = require("./lib/process-content")
Expand Down Expand Up @@ -46,11 +47,13 @@ function AtImport(options) {
throw new Error("plugins option must be an array")
}

return parseStyles(result, styles, options, state, []).then(bundle => {
applyRaws(bundle)
applyMedia(bundle)
applyStyles(bundle, styles)
})
return parseStyles(result, styles, options, state, [], []).then(
bundle => {
applyRaws(bundle)
applyMedia(bundle)
applyStyles(bundle, styles)
}
)

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

function applyMedia(bundle) {
bundle.forEach(stmt => {
if (!stmt.media.length || stmt.type === "charset") return
if (
(!stmt.media.length && !stmt.layer.length) ||
stmt.type === "charset"
) {
return
}

if (stmt.type === "import") {
stmt.node.params = `${stmt.fullUri} ${stmt.media.join(", ")}`
} else if (stmt.type === "media")
} else if (stmt.type === "media") {
stmt.node.params = stmt.media.join(", ")
else {
} else {
const { nodes } = stmt
const { parent } = nodes[0]
const mediaNode = atRule({
name: "media",
params: stmt.media.join(", "),
source: parent.source,
})

parent.insertBefore(nodes[0], mediaNode)
let outerAtRule
let innerAtRule
if (stmt.media.length && stmt.layer.length) {
const mediaNode = atRule({
name: "media",
params: stmt.media.join(", "),
source: parent.source,
})

const layerNode = atRule({
name: "layer",
params: stmt.layer.filter(layer => layer !== "").join("."),
source: parent.source,
})

mediaNode.append(layerNode)
innerAtRule = layerNode
outerAtRule = mediaNode
} else if (stmt.media.length) {
const mediaNode = atRule({
name: "media",
params: stmt.media.join(", "),
source: parent.source,
})

innerAtRule = mediaNode
outerAtRule = mediaNode
} else if (stmt.layer.length) {
const layerNode = atRule({
name: "layer",
params: stmt.layer.filter(layer => layer !== "").join("."),
source: parent.source,
})

innerAtRule = layerNode
outerAtRule = layerNode
}

parent.insertBefore(nodes[0], outerAtRule)

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

// wrap new rules with media query
mediaNode.append(nodes)
// wrap new rules with media query and/or layer at rule
innerAtRule.append(nodes)

stmt.type = "media"
stmt.node = mediaNode
stmt.node = outerAtRule
delete stmt.nodes
}
})
Expand All @@ -119,7 +161,7 @@ function AtImport(options) {
})
}

function parseStyles(result, styles, options, state, media) {
function parseStyles(result, styles, options, state, media, layer) {
const statements = parseStatements(result, styles)

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

// skip protocol base uri (protocol://url) or protocol-relative
if (
Expand Down Expand Up @@ -239,7 +282,7 @@ function AtImport(options) {

function loadImportContent(result, stmt, filename, options, state) {
const atRule = stmt.node
const { media } = stmt
const { media, layer } = stmt
if (options.skipDuplicates) {
// skip files already imported at the same scope
if (
Expand Down Expand Up @@ -287,7 +330,7 @@ function AtImport(options) {
}

// recursion: import @import from imported file
return parseStyles(result, styles, options, state, media)
return parseStyles(result, styles, options, state, media, layer)
})
}
)
Expand Down
9 changes: 9 additions & 0 deletions lib/join-layer.js
@@ -0,0 +1,9 @@
"use strict"

module.exports = function (parentLayer, childLayer) {
if (!parentLayer.length && childLayer.length) return childLayer
if (parentLayer.length && !childLayer.length) return parentLayer
if (!parentLayer.length && !childLayer.length) return []

return parentLayer.concat(childLayer)
}
37 changes: 32 additions & 5 deletions lib/parse-statements.js
Expand Up @@ -38,6 +38,7 @@ module.exports = function (result, styles) {
type: "nodes",
nodes,
media: [],
layer: [],
})
nodes = []
}
Expand All @@ -50,6 +51,7 @@ module.exports = function (result, styles) {
type: "nodes",
nodes,
media: [],
layer: [],
})
}

Expand All @@ -62,6 +64,7 @@ function parseMedia(result, atRule) {
type: "media",
node: atRule,
media: split(params, 0),
layer: [],
}
}

Expand All @@ -75,6 +78,7 @@ function parseCharset(result, atRule) {
type: "charset",
node: atRule,
media: [],
layer: [],
}
}

Expand All @@ -85,10 +89,12 @@ function parseImport(result, atRule) {
if (
prev.type !== "comment" &&
(prev.type !== "atrule" ||
(prev.name !== "import" && prev.name !== "charset"))
(prev.name !== "import" &&
prev.name !== "charset" &&
!(prev.name === "layer" && !prev.nodes)))
) {
return result.warn(
"@import must precede all other statements (besides @charset)",
"@import must precede all other statements (besides @charset or empty @layer)",
{ node: atRule }
)
}
Expand All @@ -109,6 +115,7 @@ function parseImport(result, atRule) {
type: "import",
node: atRule,
media: [],
layer: [],
}

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

if (params.length > 2) {
if (params[1].type !== "space") {
let remainder = params
if (remainder.length > 2) {
if (
(remainder[2].type === "word" || remainder[2].type === "function") &&
remainder[2].value === "layer"
) {
if (remainder[1].type !== "space") {
return result.warn("Invalid import layer statement", { node: atRule })
}

if (remainder[2].nodes) {
stmt.layer = [stringify(remainder[2].nodes)]
} else {
stmt.layer = [""]
}
remainder = remainder.slice(2)
}
}

if (remainder.length > 2) {
if (remainder[1].type !== "space") {
return result.warn("Invalid import media statement", { node: atRule })
}
stmt.media = split(params, 2)

stmt.media = split(remainder, 2)
}

return stmt
Expand Down
3 changes: 3 additions & 0 deletions test/fixtures/imports/foo-layered.css
@@ -0,0 +1,3 @@
@layer foo {
foo {}
}
10 changes: 10 additions & 0 deletions test/fixtures/layer.css
@@ -0,0 +1,10 @@
@layer layer-alpha, layer-beta.one;

@import "foo.css" layer;
@import 'bar.css' layer(bar);
@import 'bar.css' layer(bar) level-1 and level-2;
@import url(baz.css) layer;
@import url("foobar.css") layer(foobar);
@import url("foo-layered.css") layer(foo-layered);

content{}
24 changes: 24 additions & 0 deletions test/fixtures/layer.expected.css
@@ -0,0 +1,24 @@
@layer layer-alpha, layer-beta.one;
@layer{
foo{}
}
@layer bar{
bar{}
}
@media level-1 and level-2{
@layer bar{
bar{}
}
}
@layer{
baz{}
}
@layer foobar{
foobar{}
}
@layer foo-layered{
@layer foo {
foo {}
}
}
content{}
8 changes: 8 additions & 0 deletions test/layer.js
@@ -0,0 +1,8 @@
"use strict"
// external tooling
const test = require("ava")

// internal tooling
const checkFixture = require("./helpers/check-fixture")

test("should resolve layers of import statements", checkFixture, "layer")
28 changes: 26 additions & 2 deletions test/lint.js
Expand Up @@ -18,7 +18,7 @@ test("should warn when not @charset and not @import statement before", t => {
t.is(warnings.length, 1)
t.is(
warnings[0].text,
"@import must precede all other statements (besides @charset)"
"@import must precede all other statements (besides @charset or empty @layer)"
)
})
})
Expand All @@ -39,12 +39,36 @@ test("should warn about all imports after some other CSS declaration", t => {
result.warnings().forEach(warning => {
t.is(
warning.text,
"@import must precede all other statements (besides @charset)"
"@import must precede all other statements (besides @charset or empty @layer)"
)
})
})
})

test("should warn if non-empty @layer before @import", t => {
return processor
.process(`@layer { a {} } @import "a.css";`, { from: undefined })
.then(result => {
t.plan(1)
result.warnings().forEach(warning => {
t.is(
warning.text,
"@import must precede all other statements (besides @charset or empty @layer)"
)
})
})
})

test("should not warn if empty @layer before @import", t => {
return processor
.process(`@layer a; @import "";`, { from: undefined })
.then(result => {
const warnings = result.warnings()
t.is(warnings.length, 1)
t.is(warnings[0].text, `Unable to find uri in '@import ""'`)
})
})

test("should not warn if comments before @import", t => {
return processor
.process(`/* skipped comment */ @import "";`, { from: undefined })
Expand Down

0 comments on commit 8bd0173

Please sign in to comment.