Repository: postcss/postcss-import
Pull request #486 (2022-03-07 16:59:15)

Update actions/setup-node action to v3
[![WhiteSource Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [actions/setup-node](https://togithub.com/actions/setup-node) | action | major | `v2` -> `v3` |

---

### Release Notes

<details>
<summary>actions/setup-node</summary>

### [`v3`](https://togithub.com/actions/setup-node/compare/v2...v3)

[Compare Source](https://togithub.com/actions/setup-node/compare/v2...v3)

</details>

---

### Configuration

📅 **Schedule**: At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, click this checkbox.

---

This PR has been generated by [WhiteSource Renovate](https://renovate.whitesourcesoftware.com). View repository job log [here](https://app.renovatebot.com/dashboard#github/postcss/postcss-import).


Patch:
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 9ba7fbd..e49ad26 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -12,9 +12,9 @@ jobs:
       matrix:
         node-version: [10.x, 12.x, 14.x, 16.x, 17.x]
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
     - name: Use Node.js ${{ matrix.node-version }}
-      uses: actions/setup-node@v2
+      uses: actions/setup-node@v3
       with:
         node-version: ${{ matrix.node-version }}
     - run: npm install
diff --git a/index.js b/index.js
index 889cdaa..e6a3dc3 100755
--- a/index.js
+++ b/index.js
@@ -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")
@@ -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) => {
@@ -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 => {
@@ -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
           }
         })
@@ -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)
@@ -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 (
@@ -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 (
@@ -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)
             })
           }
         )
diff --git a/lib/join-layer.js b/lib/join-layer.js
new file mode 100644
index 0000000..9d91519
--- /dev/null
+++ b/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)
+}
diff --git a/lib/parse-statements.js b/lib/parse-statements.js
index 59bb427..0c94e5a 100644
--- a/lib/parse-statements.js
+++ b/lib/parse-statements.js
@@ -38,6 +38,7 @@ module.exports = function (result, styles) {
           type: "nodes",
           nodes,
           media: [],
+          layer: [],
         })
         nodes = []
       }
@@ -50,6 +51,7 @@ module.exports = function (result, styles) {
       type: "nodes",
       nodes,
       media: [],
+      layer: [],
     })
   }
 
@@ -62,6 +64,7 @@ function parseMedia(result, atRule) {
     type: "media",
     node: atRule,
     media: split(params, 0),
+    layer: [],
   }
 }
 
@@ -75,6 +78,7 @@ function parseCharset(result, atRule) {
     type: "charset",
     node: atRule,
     media: [],
+    layer: [],
   }
 }
 
@@ -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 }
         )
       }
@@ -109,6 +115,7 @@ function parseImport(result, atRule) {
     type: "import",
     node: atRule,
     media: [],
+    layer: [],
   }
 
   // prettier-ignore
@@ -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


Test patch:
diff --git a/test/fixtures/imports/foo-layered.css b/test/fixtures/imports/foo-layered.css
new file mode 100755
index 0000000..30f6252
--- /dev/null
+++ b/test/fixtures/imports/foo-layered.css
@@ -0,0 +1,3 @@
+@layer foo {
+  foo {}
+}
diff --git a/test/fixtures/layer.css b/test/fixtures/layer.css
new file mode 100644
index 0000000..1d1c59a
--- /dev/null
+++ b/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{}
diff --git a/test/fixtures/layer.expected.css b/test/fixtures/layer.expected.css
new file mode 100644
index 0000000..0a18986
--- /dev/null
+++ b/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{}
diff --git a/test/layer.js b/test/layer.js
new file mode 100644
index 0000000..1b50f4c
--- /dev/null
+++ b/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")
diff --git a/test/lint.js b/test/lint.js
index 7e65c09..71a7c3b 100644
--- a/test/lint.js
+++ b/test/lint.js
@@ -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)"
       )
     })
   })
@@ -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 })