From a5ed3aa2d2f8a0ee5394a9ad42da74ead3fdbc06 Mon Sep 17 00:00:00 2001 From: Bart Veneman <1536852+bartveneman@users.noreply.github.com> Date: Mon, 11 Nov 2024 08:47:38 +0100 Subject: [PATCH 01/14] fix tests, types and exported name --- README.md | 5 +++++ index.d.ts | 4 ++-- package.json | 2 +- src/TreeNode.js | 5 ++--- src/index.js | 35 ++++++++++++++++------------------- test/global.spec.js | 15 ++++++++++----- test/import.spec.js | 20 ++++++++++---------- test/layer.spec.js | 40 ++++++++++++++++++++++++++++++---------- 8 files changed, 76 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index c9d1818..64ab269 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,11 @@ This example would result in this `tree`: }, ], }, + { + name: '__anonymous-2__', + locations: [{ line: 10, column: 3, start: 176, end: 185 }], + children: [], + }, ] ``` diff --git a/index.d.ts b/index.d.ts index 961ebdf..a15eda7 100644 --- a/index.d.ts +++ b/index.d.ts @@ -13,5 +13,5 @@ export type TreeNode = { locations: Location[]; } -export function get_tree_from_ast(ast: CssNode): TreeNode['children']; -export function get_tree(css: string): TreeNode['children']; +export function layer_tree_from_ast(ast: CssNode): TreeNode[]; +export function layer_tree(css: string): TreeNode[]; diff --git a/package.json b/package.json index 6de5fee..5be0c19 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "Discover the composition of your CSS @layers", "repository": { "type": "git", - "url": "git+https://github.com/projectwallace/css-layers.git" + "url": "git+https://github.com/projectwallace/css-layer-tree.git" }, "homepage": "https://github.com/projectwallace/css-layer-tree", "issues": "https://github.com/projectwallace/css-layer-tree/issues", diff --git a/src/TreeNode.js b/src/TreeNode.js index 1fae15c..6f47f4b 100644 --- a/src/TreeNode.js +++ b/src/TreeNode.js @@ -7,7 +7,7 @@ export class TreeNode { /** @type {Map>} */ this.children = new Map() /** @type {T[]} */ - this.locations = [] // Store metadata for each location added + this.locations = [] } /** @@ -53,8 +53,7 @@ export class TreeNode { name: this.name, locations: this.locations, children: Array - .from(this.children.values()) - .map((child) => child.to_plain_object()), + .from(this.children.values(), (child) => child.to_plain_object()) } } } \ No newline at end of file diff --git a/src/index.js b/src/index.js index 991427a..99103b4 100644 --- a/src/index.js +++ b/src/index.js @@ -29,10 +29,22 @@ function is_layer(node) { return node.name.toLowerCase() === 'layer' } +/** + * @param {import('css-tree').AtrulePrelude} prelude + * @returns {string[]} + */ +function get_layer_names(prelude) { + return csstree + // @todo: fewer loops plz + .generate(prelude) + .split('.') + .map((s) => s.trim()) +} + /** * @param {import('css-tree').CssNode} ast */ -export function get_tree_from_ast(ast) { +export function layer_tree_from_ast(ast) { /** @type {string[]} */ let current_stack = [] let root = new TreeNode('root') @@ -44,18 +56,6 @@ export function get_tree_from_ast(ast) { return `__anonymous-${anonymous_counter}__` } - /** - * @param {import('css-tree').AtrulePrelude} prelude - * @returns {string[]} - */ - function get_layer_names(prelude) { - return csstree - // @todo: fewer loops plz - .generate(prelude) - .split('.') - .map((s) => s.trim()) - } - csstree.walk(ast, { visit: 'Atrule', enter(node) { @@ -66,10 +66,7 @@ export function get_tree_from_ast(ast) { let layer_name = get_anonymous_id() root.add_child(current_stack, layer_name, location) current_stack.push(layer_name) - return - } - - if (node.prelude.type === 'AtrulePrelude') { + } else if (node.prelude.type === 'AtrulePrelude') { if (node.block === null) { // @ts-expect-error CSSTree types are not updated yet in @types/css-tree let prelude = csstree.findAll(node.prelude, n => n.type === 'Layer').map(n => n.name) @@ -140,7 +137,7 @@ export function get_tree_from_ast(ast) { /** * @param {string} css */ -export function get_tree(css) { +export function layer_tree(css) { let ast = csstree.parse(css, { positions: true, parseAtrulePrelude: true, @@ -149,5 +146,5 @@ export function get_tree(css) { parseCustomProperty: false, }) - return get_tree_from_ast(ast) + return layer_tree_from_ast(ast) } \ No newline at end of file diff --git a/test/global.spec.js b/test/global.spec.js index 640f34a..668b9d3 100644 --- a/test/global.spec.js +++ b/test/global.spec.js @@ -1,24 +1,24 @@ import { test } from 'uvu' import * as assert from 'uvu/assert' -import { get_tree } from '../src/index.js' +import { layer_tree } from '../src/index.js' test('handles empty input', () => { - assert.equal(get_tree(''), []) + assert.equal(layer_tree(''), []) }) test('handles CSS without layers', () => { - assert.equal(get_tree('@media all { body { color: red; } }'), []) + assert.equal(layer_tree('@media all { body { color: red; } }'), []) }) test('mixed imports and layers', () => { - let actual = get_tree(` + let actual = layer_tree(` @import url("test.css") layer; @import url("test.css") LAYER(test); @layer anotherTest { @layer moreTest { @layer deepTest {} } - }; + } /* anonymous @layer */ @layer {} `) @@ -49,6 +49,11 @@ test('mixed imports and layers', () => { ] } ] + }, + { + name: '__anonymous-2__', + locations: [{ line: 10, column: 3, start: 176, end: 185 }], + children: [] } ] assert.equal(actual, expected) diff --git a/test/import.spec.js b/test/import.spec.js index 453cfee..08e31fb 100644 --- a/test/import.spec.js +++ b/test/import.spec.js @@ -1,9 +1,9 @@ import { test } from 'uvu' import * as assert from 'uvu/assert' -import { get_tree } from '../src/index.js' +import { layer_tree } from '../src/index.js' test('@import url() layer', () => { - let actual = get_tree('@import url("foo.css") layer;') + let actual = layer_tree('@import url("foo.css") layer;') let expected = [{ name: '__anonymous-1__', locations: [{ line: 1, column: 1, start: 0, end: 29 }], @@ -13,7 +13,7 @@ test('@import url() layer', () => { }) test('@import url() LAYER', () => { - let actual = get_tree('@import url("foo.css") LAYER;') + let actual = layer_tree('@import url("foo.css") LAYER;') let expected = [{ "name": "__anonymous-1__", locations: [{ line: 1, column: 1, start: 0, end: 29 }], @@ -23,7 +23,7 @@ test('@import url() LAYER', () => { }) test('@import url() layer()', () => { - let actual = get_tree('@import url("foo.css") layer();') + let actual = layer_tree('@import url("foo.css") layer();') let expected = [{ name: '__anonymous-1__', locations: [{ line: 1, column: 1, start: 0, end: 31 }], @@ -33,7 +33,7 @@ test('@import url() layer()', () => { }) test('@import url() LAYER()', () => { - let actual = get_tree('@import url("foo.css") LAYER();') + let actual = layer_tree('@import url("foo.css") LAYER();') let expected = [{ name: '__anonymous-1__', locations: [{ line: 1, column: 1, start: 0, end: 31 }], @@ -43,7 +43,7 @@ test('@import url() LAYER()', () => { }) test('@import url() layer(named)', () => { - let actual = get_tree('@import url("foo.css") layer(named);') + let actual = layer_tree('@import url("foo.css") layer(named);') let expected = [{ name: 'named', locations: [{ line: 1, column: 1, start: 0, end: 36 }], @@ -53,7 +53,7 @@ test('@import url() layer(named)', () => { }) test('@import url() LAYER(named)', () => { - let actual = get_tree('@import url("foo.css") LAYER(named);') + let actual = layer_tree('@import url("foo.css") LAYER(named);') let expected = [{ name: 'named', locations: [{ line: 1, column: 1, start: 0, end: 36 }], @@ -63,7 +63,7 @@ test('@import url() LAYER(named)', () => { }) test('@import url() layer(named.nested)', () => { - let actual = get_tree('@import url("foo.css") layer(named.nested);') + let actual = layer_tree('@import url("foo.css") layer(named.nested);') let expected = [{ name: 'named', locations: [{ line: 1, column: 1, start: 0, end: 43 }], @@ -77,7 +77,7 @@ test('@import url() layer(named.nested)', () => { }) test('@import url() layer(named.nested )', () => { - let actual = get_tree('@import url("foo.css") layer(named.nested );') + let actual = layer_tree('@import url("foo.css") layer(named.nested );') let expected = [{ name: 'named', locations: [{ line: 1, column: 1, start: 0, end: 48 }], @@ -91,7 +91,7 @@ test('@import url() layer(named.nested )', () => { }) test('@import url() layer(/* test */named.nested )', () => { - let actual = get_tree('@import url("foo.css") layer(/* test */named.nested );') + let actual = layer_tree('@import url("foo.css") layer(/* test */named.nested );') let expected = [{ name: 'named', locations: [{ line: 1, column: 1, start: 0, end: 58 }], diff --git a/test/layer.spec.js b/test/layer.spec.js index 3d72c90..2df12ad 100644 --- a/test/layer.spec.js +++ b/test/layer.spec.js @@ -1,9 +1,9 @@ import { test } from 'uvu' import * as assert from 'uvu/assert' -import { get_tree } from '../src/index.js' +import { layer_tree } from '../src/index.js' test('single anonymous layer without body', () => { - let actual = get_tree('@layer;') + let actual = layer_tree('@layer;') let expected = [ { name: '__anonymous-1__', @@ -15,7 +15,7 @@ test('single anonymous layer without body', () => { }) test('single anonymous layer with body', () => { - let actual = get_tree('@layer {}') + let actual = layer_tree('@layer {}') let expected = [ { name: '__anonymous-1__', @@ -27,7 +27,7 @@ test('single anonymous layer with body', () => { }) test('single named layer without body', () => { - let actual = get_tree('@layer first;') + let actual = layer_tree('@layer first;') let expected = [ { name: 'first', @@ -39,7 +39,7 @@ test('single named layer without body', () => { }) test('single named layer with body', () => { - let actual = get_tree('@layer first {}') + let actual = layer_tree('@layer first {}') let expected = [ { name: 'first', @@ -51,7 +51,7 @@ test('single named layer with body', () => { }) test('multiple named layers in one line', () => { - let actual = get_tree(`@layer first, second;`) + let actual = layer_tree(`@layer first, second;`) let expected = [ { name: 'first', @@ -68,7 +68,7 @@ test('multiple named layers in one line', () => { }) test('repeated use of the same layer name', () => { - let actual = get_tree(` + let actual = layer_tree(` @layer first {} @layer first {} `) @@ -86,7 +86,7 @@ test('repeated use of the same layer name', () => { }) test('nested layers', () => { - let actual = get_tree(` + let actual = layer_tree(` @layer first { @layer second { @layer third {} @@ -123,7 +123,7 @@ test('nested layers', () => { }) test('nested layers with anonymous layers', () => { - let actual = get_tree(` + let actual = layer_tree(` @layer { @layer {} } @@ -144,8 +144,28 @@ test('nested layers with anonymous layers', () => { assert.equal(actual, expected) }) +test('consecutive anonymous layers', () => { + let actual = layer_tree(` + @layer {} + @layer {} + `) + let expected = [ + { + name: '__anonymous-1__', + locations: [{ line: 2, column: 3, start: 3, end: 12 }], + children: [], + }, + { + name: '__anonymous-2__', + locations: [{ line: 3, column: 3, start: 15, end: 24 }], + children: [], + }, + ] + assert.equal(actual, expected) +}) + test('nested layers with anonymous layers and duplicate names', () => { - let actual = get_tree(` + let actual = layer_tree(` @layer { @layer first {} } From a369d9de1601b119ed0dfad7618cbd8e033d10e1 Mon Sep 17 00:00:00 2001 From: Bart Veneman <1536852+bartveneman@users.noreply.github.com> Date: Mon, 11 Nov 2024 08:47:54 +0100 Subject: [PATCH 02/14] 0.0.2 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5762170..a83422b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@projectwallace/css-layer-tree", - "version": "0.0.1", + "version": "0.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@projectwallace/css-layer-tree", - "version": "0.0.1", + "version": "0.0.2", "license": "MIT", "dependencies": { "css-tree": "^3.0.0" diff --git a/package.json b/package.json index 5be0c19..cc396a8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@projectwallace/css-layer-tree", - "version": "0.0.1", + "version": "0.0.2", "description": "Discover the composition of your CSS @layers", "repository": { "type": "git", From 7a036fdf4ee633c260e7c07df0ead1664a3d591d Mon Sep 17 00:00:00 2001 From: Bart Veneman <1536852+bartveneman@users.noreply.github.com> Date: Mon, 11 Nov 2024 08:57:47 +0100 Subject: [PATCH 03/14] 1.0.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index a83422b..d318471 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@projectwallace/css-layer-tree", - "version": "0.0.2", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@projectwallace/css-layer-tree", - "version": "0.0.2", + "version": "1.0.0", "license": "MIT", "dependencies": { "css-tree": "^3.0.0" diff --git a/package.json b/package.json index cc396a8..a1087bf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@projectwallace/css-layer-tree", - "version": "0.0.2", + "version": "1.0.0", "description": "Discover the composition of your CSS @layers", "repository": { "type": "git", From a17169e6cfa69f513013b94919d73db3f0a3b0ce Mon Sep 17 00:00:00 2001 From: Bart Veneman <1536852+bartveneman@users.noreply.github.com> Date: Tue, 12 Nov 2024 16:27:45 +0100 Subject: [PATCH 04/14] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 64ab269..ee8a47a 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ npm install @projectwallace/css-layer-tree ## Usage ```js -import { get_tree } from '@projectwallace/css-layer-tree' +import { layer_tree } from '@projectwallace/css-layer-tree' let css = ` @import url("test.css") layer; @@ -25,7 +25,7 @@ let css = ` @layer {} ` -let tree = get_tree(css) +let tree = layer_tree(css) ``` This example would result in this `tree`: From b1da44945d4522daa7f72c140c834c9c75fd584f Mon Sep 17 00:00:00 2001 From: Bart Veneman <1536852+bartveneman@users.noreply.github.com> Date: Fri, 29 Nov 2024 14:13:55 +0100 Subject: [PATCH 05/14] remove invalid `layer()` case --- src/index.js | 7 ------- test/import.spec.js | 20 -------------------- 2 files changed, 27 deletions(-) diff --git a/src/index.js b/src/index.js index 99103b4..d3b124e 100644 --- a/src/index.js +++ b/src/index.js @@ -98,13 +98,6 @@ export function layer_tree_from_ast(ast) { return this.skip } - // @import url("foo.css") layer(); - let layer_fn = csstree.find(prelude, n => n.type === 'Function' && n.name.toLowerCase() === 'layer') - if (layer_fn) { - root.add_child([], get_anonymous_id(), location) - return this.skip - } - // @import url("foo.css") layer; let layer_keyword = csstree.find(prelude, n => n.type === 'Identifier' && n.name.toLowerCase() === 'layer') if (layer_keyword) { diff --git a/test/import.spec.js b/test/import.spec.js index 08e31fb..a5785fd 100644 --- a/test/import.spec.js +++ b/test/import.spec.js @@ -22,26 +22,6 @@ test('@import url() LAYER', () => { assert.equal(actual, expected) }) -test('@import url() layer()', () => { - let actual = layer_tree('@import url("foo.css") layer();') - let expected = [{ - name: '__anonymous-1__', - locations: [{ line: 1, column: 1, start: 0, end: 31 }], - children: [] - }] - assert.equal(actual, expected) -}) - -test('@import url() LAYER()', () => { - let actual = layer_tree('@import url("foo.css") LAYER();') - let expected = [{ - name: '__anonymous-1__', - locations: [{ line: 1, column: 1, start: 0, end: 31 }], - children: [] - }] - assert.equal(actual, expected) -}) - test('@import url() layer(named)', () => { let actual = layer_tree('@import url("foo.css") layer(named);') let expected = [{ From d134631c75537e42db7af053b60aa11f4a7ed48b Mon Sep 17 00:00:00 2001 From: Bart Veneman <1536852+bartveneman@users.noreply.github.com> Date: Fri, 29 Nov 2024 14:15:52 +0100 Subject: [PATCH 06/14] 1.0.1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index d318471..e1691c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@projectwallace/css-layer-tree", - "version": "1.0.0", + "version": "1.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@projectwallace/css-layer-tree", - "version": "1.0.0", + "version": "1.0.1", "license": "MIT", "dependencies": { "css-tree": "^3.0.0" diff --git a/package.json b/package.json index a1087bf..efb7586 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@projectwallace/css-layer-tree", - "version": "1.0.0", + "version": "1.0.1", "description": "Discover the composition of your CSS @layers", "repository": { "type": "git", From cb85048b6592097dafd4b3dc3ce7ce9cdf82a938 Mon Sep 17 00:00:00 2001 From: Bart Veneman <1536852+bartveneman@users.noreply.github.com> Date: Sat, 12 Apr 2025 21:39:19 +0200 Subject: [PATCH 07/14] add `is_anonymous` flag to tree nodes (#6) --- src/TreeNode.js | 6 +++++- test/global.spec.js | 6 ++++++ test/import.spec.js | 10 ++++++++++ test/layer.spec.js | 18 ++++++++++++++++++ 4 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/TreeNode.js b/src/TreeNode.js index 6f47f4b..df57fd7 100644 --- a/src/TreeNode.js +++ b/src/TreeNode.js @@ -4,6 +4,8 @@ export class TreeNode { constructor(name) { /** @type {string} */ this.name = name + /** @type {boolean} */ + this.is_anonymous = false /** @type {Map>} */ this.children = new Map() /** @type {T[]} */ @@ -11,7 +13,6 @@ export class TreeNode { } /** - * * @param {string[]} path * @param {string} name * @param {T} location @@ -33,6 +34,7 @@ export class TreeNode { // Otherwise, create the item and add the location const new_node = new TreeNode(name) new_node.locations.push(location) + new_node.is_anonymous = name.startsWith('__anonymous') current.children.set(name, new_node) } } @@ -40,6 +42,7 @@ export class TreeNode { /** * @typedef PlainObject * @property {string} name + * @property {boolean} is_anonymous * @property {T[]} locations * @property {PlainObject[]} children */ @@ -51,6 +54,7 @@ export class TreeNode { to_plain_object() { return { name: this.name, + is_anonymous: this.is_anonymous, locations: this.locations, children: Array .from(this.children.values(), (child) => child.to_plain_object()) diff --git a/test/global.spec.js b/test/global.spec.js index 668b9d3..4df610a 100644 --- a/test/global.spec.js +++ b/test/global.spec.js @@ -25,24 +25,29 @@ test('mixed imports and layers', () => { let expected = [ { name: '__anonymous-1__', + is_anonymous: true, locations: [{ line: 2, column: 3, start: 3, end: 33 }], children: [] }, { name: 'test', + is_anonymous: false, locations: [{ line: 3, column: 3, start: 36, end: 72 }], children: [] }, { name: 'anotherTest', + is_anonymous: false, locations: [{ line: 4, column: 3, start: 75, end: 148 }], children: [ { name: 'moreTest', + is_anonymous: false, locations: [{ line: 5, column: 4, start: 99, end: 144 }], children: [ { name: 'deepTest', + is_anonymous: false, locations: [{ line: 6, column: 5, start: 121, end: 139 }], children: [] } @@ -52,6 +57,7 @@ test('mixed imports and layers', () => { }, { name: '__anonymous-2__', + is_anonymous: true, locations: [{ line: 10, column: 3, start: 176, end: 185 }], children: [] } diff --git a/test/import.spec.js b/test/import.spec.js index a5785fd..5e22b83 100644 --- a/test/import.spec.js +++ b/test/import.spec.js @@ -6,6 +6,7 @@ test('@import url() layer', () => { let actual = layer_tree('@import url("foo.css") layer;') let expected = [{ name: '__anonymous-1__', + is_anonymous: true, locations: [{ line: 1, column: 1, start: 0, end: 29 }], children: [] }] @@ -16,6 +17,7 @@ test('@import url() LAYER', () => { let actual = layer_tree('@import url("foo.css") LAYER;') let expected = [{ "name": "__anonymous-1__", + is_anonymous: true, locations: [{ line: 1, column: 1, start: 0, end: 29 }], "children": [] }] @@ -26,6 +28,7 @@ test('@import url() layer(named)', () => { let actual = layer_tree('@import url("foo.css") layer(named);') let expected = [{ name: 'named', + is_anonymous: false, locations: [{ line: 1, column: 1, start: 0, end: 36 }], children: [] }] @@ -36,6 +39,7 @@ test('@import url() LAYER(named)', () => { let actual = layer_tree('@import url("foo.css") LAYER(named);') let expected = [{ name: 'named', + is_anonymous: false, locations: [{ line: 1, column: 1, start: 0, end: 36 }], children: [] }] @@ -46,9 +50,11 @@ test('@import url() layer(named.nested)', () => { let actual = layer_tree('@import url("foo.css") layer(named.nested);') let expected = [{ name: 'named', + is_anonymous: false, locations: [{ line: 1, column: 1, start: 0, end: 43 }], children: [{ name: 'nested', + is_anonymous: false, locations: [{ line: 1, column: 1, start: 0, end: 43 }], children: [] }] @@ -60,9 +66,11 @@ test('@import url() layer(named.nested )', () => { let actual = layer_tree('@import url("foo.css") layer(named.nested );') let expected = [{ name: 'named', + is_anonymous: false, locations: [{ line: 1, column: 1, start: 0, end: 48 }], children: [{ name: 'nested', + is_anonymous: false, locations: [{ line: 1, column: 1, start: 0, end: 48 }], children: [] }] @@ -74,9 +82,11 @@ test('@import url() layer(/* test */named.nested )', () => { let actual = layer_tree('@import url("foo.css") layer(/* test */named.nested );') let expected = [{ name: 'named', + is_anonymous: false, locations: [{ line: 1, column: 1, start: 0, end: 58 }], children: [{ name: 'nested', + is_anonymous: false, locations: [{ line: 1, column: 1, start: 0, end: 58 }], children: [] }] diff --git a/test/layer.spec.js b/test/layer.spec.js index 2df12ad..1930bab 100644 --- a/test/layer.spec.js +++ b/test/layer.spec.js @@ -7,6 +7,7 @@ test('single anonymous layer without body', () => { let expected = [ { name: '__anonymous-1__', + is_anonymous: true, children: [], locations: [{ line: 1, column: 1, start: 0, end: 7 }] }, @@ -19,6 +20,7 @@ test('single anonymous layer with body', () => { let expected = [ { name: '__anonymous-1__', + is_anonymous: true, children: [], locations: [{ line: 1, column: 1, start: 0, end: 9 }] }, @@ -31,6 +33,7 @@ test('single named layer without body', () => { let expected = [ { name: 'first', + is_anonymous: false, children: [], locations: [{ line: 1, column: 1, start: 0, end: 13 }] }, @@ -43,6 +46,7 @@ test('single named layer with body', () => { let expected = [ { name: 'first', + is_anonymous: false, children: [], locations: [{ line: 1, column: 1, start: 0, end: 15 }] }, @@ -55,11 +59,13 @@ test('multiple named layers in one line', () => { let expected = [ { name: 'first', + is_anonymous: false, children: [], locations: [{ line: 1, column: 1, start: 0, end: 21 }] }, { name: 'second', + is_anonymous: false, children: [], locations: [{ line: 1, column: 1, start: 0, end: 21 }] }, @@ -75,6 +81,7 @@ test('repeated use of the same layer name', () => { let expected = [ { name: 'first', + is_anonymous: false, children: [], locations: [ { line: 2, column: 3, start: 3, end: 18 }, @@ -98,19 +105,23 @@ test('nested layers', () => { let expected = [ { name: 'first', + is_anonymous: false, locations: [{ line: 2, column: 3, start: 3, end: 104 }], children: [ { name: 'second', + is_anonymous: false, locations: [{ line: 3, column: 4, start: 21, end: 100 }], children: [ { name: 'third', + is_anonymous: false, locations: [{ line: 4, column: 5, start: 41, end: 56 }], children: [], }, { name: 'fourth', + is_anonymous: false, locations: [{ line: 6, column: 5, start: 79, end: 95 }], children: [], }, @@ -131,10 +142,12 @@ test('nested layers with anonymous layers', () => { let expected = [ { name: '__anonymous-1__', + is_anonymous: true, locations: [{ line: 2, column: 3, start: 3, end: 28 }], children: [ { name: '__anonymous-2__', + is_anonymous: true, children: [], locations: [{ line: 3, column: 4, start: 15, end: 24 }], }, @@ -152,11 +165,13 @@ test('consecutive anonymous layers', () => { let expected = [ { name: '__anonymous-1__', + is_anonymous: true, locations: [{ line: 2, column: 3, start: 3, end: 12 }], children: [], }, { name: '__anonymous-2__', + is_anonymous: true, locations: [{ line: 3, column: 3, start: 15, end: 24 }], children: [], }, @@ -175,10 +190,12 @@ test('nested layers with anonymous layers and duplicate names', () => { let expected = [ { name: '__anonymous-1__', + is_anonymous: true, locations: [{ line: 2, column: 3, start: 3, end: 34 }], children: [ { name: 'first', + is_anonymous: false, children: [], locations: [{ line: 3, column: 4, start: 15, end: 30 }], }, @@ -186,6 +203,7 @@ test('nested layers with anonymous layers and duplicate names', () => { }, { name: 'first', + is_anonymous: false, locations: [{ line: 6, column: 3, start: 38, end: 53 }], children: [], }, From a9e008813162f5e5462c73092b4059169d2e8172 Mon Sep 17 00:00:00 2001 From: Bart Veneman <1536852+bartveneman@users.noreply.github.com> Date: Sun, 13 Apr 2025 09:28:37 +0200 Subject: [PATCH 08/14] BREAKING: ship ESM only (#7) --- index.d.ts | 1 + package-lock.json | 7 +++++-- package.json | 18 ++++++++++-------- vite.config.js | 10 +--------- 4 files changed, 17 insertions(+), 19 deletions(-) diff --git a/index.d.ts b/index.d.ts index a15eda7..aa31c9c 100644 --- a/index.d.ts +++ b/index.d.ts @@ -9,6 +9,7 @@ export type Location = { export type TreeNode = { name: string; + is_anonymous: boolean; children: TreeNode[]; locations: Location[]; } diff --git a/package-lock.json b/package-lock.json index e1691c7..2ec5aa3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@projectwallace/css-layer-tree", - "version": "1.0.1", + "version": "2.0.0-alpha.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@projectwallace/css-layer-tree", - "version": "1.0.1", + "version": "2.0.0-alpha.0", "license": "MIT", "dependencies": { "css-tree": "^3.0.0" @@ -19,6 +19,9 @@ "typescript": "5.4.2", "uvu": "^0.5.6", "vite": "^5.4.10" + }, + "engines": { + "node": ">=18.0.0" } }, "node_modules/@actions/core": { diff --git a/package.json b/package.json index efb7586..dcea977 100644 --- a/package.json +++ b/package.json @@ -1,25 +1,27 @@ { "name": "@projectwallace/css-layer-tree", - "version": "1.0.1", - "description": "Discover the composition of your CSS @layers", + "version": "2.0.0-alpha.0", + "description": "Discover the composition of your CSS @layers in a tree-based format.", "repository": { "type": "git", "url": "git+https://github.com/projectwallace/css-layer-tree.git" }, + "author": { + "name": "Bart Veneman" + }, "homepage": "https://github.com/projectwallace/css-layer-tree", "issues": "https://github.com/projectwallace/css-layer-tree/issues", "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, "type": "module", - "source": "src/index.js", + "main": "./dist/css-layer-tree.js", + "types": "./index.d.ts", "exports": { "types": "./index.d.ts", - "require": "./dist/css-layer-tree.umd.cjs", "default": "./dist/css-layer-tree.js" }, - "types": "./index.d.ts", - "main": "./dist/css-layer-tree.umd.cjs", - "module": "./dist/css-layer-tree.js", - "unpkg": "./dist/css-layer-tree.umd.cjs", "scripts": { "build": "vite build", "test": "c8 --reporter=lcov uvu", diff --git a/vite.config.js b/vite.config.js index 98692b6..c1eedd2 100644 --- a/vite.config.js +++ b/vite.config.js @@ -6,20 +6,12 @@ export default defineConfig({ build: { lib: { entry: resolve(__dirname, "src/index.js"), - name: "cssLayerTree", - fileName: "css-layer-tree", + formats: ['es'] }, rollupOptions: { // make sure to externalize deps that shouldn't be bundled // into your library external: ['css-tree'], - output: { - // Provide global variables to use in the UMD build - // for externalized deps - globals: { - 'css-tree': 'csstree', - }, - }, }, }, plugins: [ From e6bb74c63d8a8da8c5eeb82253069260eacb1dc0 Mon Sep 17 00:00:00 2001 From: Bart Veneman <1536852+bartveneman@users.noreply.github.com> Date: Sun, 13 Apr 2025 09:29:51 +0200 Subject: [PATCH 09/14] 2.0.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2ec5aa3..0270100 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@projectwallace/css-layer-tree", - "version": "2.0.0-alpha.0", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@projectwallace/css-layer-tree", - "version": "2.0.0-alpha.0", + "version": "2.0.0", "license": "MIT", "dependencies": { "css-tree": "^3.0.0" diff --git a/package.json b/package.json index dcea977..7ad3585 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@projectwallace/css-layer-tree", - "version": "2.0.0-alpha.0", + "version": "2.0.0", "description": "Discover the composition of your CSS @layers in a tree-based format.", "repository": { "type": "git", From 752d9e0cf556629e682f4d1ee7dd72fc22a7aebb Mon Sep 17 00:00:00 2001 From: Bart Veneman <1536852+bartveneman@users.noreply.github.com> Date: Mon, 17 Nov 2025 21:14:58 +0100 Subject: [PATCH 10/14] fix: handle nested layers with dot notation (#10) --- package-lock.json | 2 + src/TreeNode.js | 15 ++++--- src/index.js | 13 +++++- test/global.spec.js | 99 ++++++++++++++++++++++++++++++++++++++++----- test/layer.spec.js | 18 ++++----- 5 files changed, 122 insertions(+), 25 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0270100..53e4e49 100644 --- a/package-lock.json +++ b/package-lock.json @@ -552,6 +552,7 @@ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.0.tgz", "integrity": "sha512-1LFfa/qnMQvEOAdzlQymH0ulepxbxnCYAKJZfMci/5XJyIHWgEYnDmgnKakbTh7CH2tFQ5O60oYDvns4i9RAIg==", "dev": true, + "peer": true, "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", @@ -1988,6 +1989,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.10.tgz", "integrity": "sha512-1hvaPshuPUtxeQ0hsVH3Mud0ZanOLwVTneA1EgbAM5LhaZEqyPWGRQ7BtaMvUrTDeEaC8pxtj6a6jku3x4z6SQ==", "dev": true, + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", diff --git a/src/TreeNode.js b/src/TreeNode.js index df57fd7..c9abe54 100644 --- a/src/TreeNode.js +++ b/src/TreeNode.js @@ -28,12 +28,16 @@ export class TreeNode { // If the item already exists, add the location to its metadata if (current.children.has(name)) { - // @ts-expect-error Apparently, TypeScript doesn't know that current is a TreeNode - current.children.get(name).locations.push(location) + if (location !== undefined) { + // @ts-expect-error Apparently, TypeScript doesn't know that current is a TreeNode + current.children.get(name).locations.push(location) + } } else { // Otherwise, create the item and add the location const new_node = new TreeNode(name) - new_node.locations.push(location) + if (location !== undefined) { + new_node.locations.push(location) + } new_node.is_anonymous = name.startsWith('__anonymous') current.children.set(name, new_node) } @@ -56,8 +60,7 @@ export class TreeNode { name: this.name, is_anonymous: this.is_anonymous, locations: this.locations, - children: Array - .from(this.children.values(), (child) => child.to_plain_object()) + children: Array.from(this.children.values(), (child) => child.to_plain_object()), } } -} \ No newline at end of file +} diff --git a/src/index.js b/src/index.js index d3b124e..30bd728 100644 --- a/src/index.js +++ b/src/index.js @@ -71,7 +71,18 @@ export function layer_tree_from_ast(ast) { // @ts-expect-error CSSTree types are not updated yet in @types/css-tree let prelude = csstree.findAll(node.prelude, n => n.type === 'Layer').map(n => n.name) for (let name of prelude) { - root.add_child(current_stack, name, location) + // Split the layer name by dots to handle nested layers + let parts = name.split('.').map((/** @type {string} */ s) => s.trim()) + + // Ensure all parent layers exist and add them to the tree + for (let i = 0; i < parts.length; i++) { + let path = parts.slice(0, i) + let layerName = parts[i] + // Only add location to the final layer in dotted notation + // Create a new copy to avoid sharing references + let loc = i === parts.length - 1 ? {...location} : undefined + root.add_child(path, layerName, loc) + } } } else { for (let layer_name of get_layer_names(node.prelude)) { diff --git a/test/global.spec.js b/test/global.spec.js index 4df610a..0f08623 100644 --- a/test/global.spec.js +++ b/test/global.spec.js @@ -27,13 +27,13 @@ test('mixed imports and layers', () => { name: '__anonymous-1__', is_anonymous: true, locations: [{ line: 2, column: 3, start: 3, end: 33 }], - children: [] + children: [], }, { name: 'test', is_anonymous: false, locations: [{ line: 3, column: 3, start: 36, end: 72 }], - children: [] + children: [], }, { name: 'anotherTest', @@ -49,18 +49,99 @@ test('mixed imports and layers', () => { name: 'deepTest', is_anonymous: false, locations: [{ line: 6, column: 5, start: 121, end: 139 }], - children: [] - } - ] - } - ] + children: [], + }, + ], + }, + ], }, { name: '__anonymous-2__', is_anonymous: true, locations: [{ line: 10, column: 3, start: 176, end: 185 }], - children: [] - } + children: [], + }, + ] + assert.equal(actual, expected) +}) + +test('the fokus.dev boilerplate', () => { + let actual = layer_tree(` + @layer core, third-party, components, utility; + @layer core.reset, core.tokens, core.base; + @layer third-party.imports, third-party.overrides; + @layer components.base, components.variations; + `) + let expected = [ + { + name: 'core', + is_anonymous: false, + locations: [{ line: 2, column: 3, start: 3, end: 49 }], + children: [ + { + name: 'reset', + is_anonymous: false, + locations: [{ line: 3, column: 3, start: 52, end: 94 }], + children: [], + }, + { + name: 'tokens', + is_anonymous: false, + locations: [{ line: 3, column: 3, start: 52, end: 94 }], + children: [], + }, + { + name: 'base', + is_anonymous: false, + locations: [{ line: 3, column: 3, start: 52, end: 94 }], + children: [], + }, + ], + }, + { + name: 'third-party', + is_anonymous: false, + locations: [{ line: 2, column: 3, start: 3, end: 49 }], + children: [ + { + name: 'imports', + is_anonymous: false, + locations: [{ line: 4, column: 3, start: 97, end: 147 }], + children: [], + }, + { + name: 'overrides', + is_anonymous: false, + locations: [{ line: 4, column: 3, start: 97, end: 147 }], + children: [], + }, + ], + }, + { + name: 'components', + is_anonymous: false, + locations: [{ line: 2, column: 3, start: 3, end: 49 }], + children: [ + { + name: 'base', + is_anonymous: false, + locations: [{ line: 5, column: 3, start: 150, end: 196 }], + children: [], + }, + { + name: 'variations', + is_anonymous: false, + locations: [{ line: 5, column: 3, start: 150, end: 196 }], + children: [], + }, + ], + }, + { + name: 'utility', + is_anonymous: false, + locations: [{ line: 2, column: 3, start: 3, end: 49 }], + children: [], + }, ] assert.equal(actual, expected) }) diff --git a/test/layer.spec.js b/test/layer.spec.js index 1930bab..aa30478 100644 --- a/test/layer.spec.js +++ b/test/layer.spec.js @@ -9,7 +9,7 @@ test('single anonymous layer without body', () => { name: '__anonymous-1__', is_anonymous: true, children: [], - locations: [{ line: 1, column: 1, start: 0, end: 7 }] + locations: [{ line: 1, column: 1, start: 0, end: 7 }], }, ] assert.equal(actual, expected) @@ -22,7 +22,7 @@ test('single anonymous layer with body', () => { name: '__anonymous-1__', is_anonymous: true, children: [], - locations: [{ line: 1, column: 1, start: 0, end: 9 }] + locations: [{ line: 1, column: 1, start: 0, end: 9 }], }, ] assert.equal(actual, expected) @@ -35,7 +35,7 @@ test('single named layer without body', () => { name: 'first', is_anonymous: false, children: [], - locations: [{ line: 1, column: 1, start: 0, end: 13 }] + locations: [{ line: 1, column: 1, start: 0, end: 13 }], }, ] assert.equal(actual, expected) @@ -48,7 +48,7 @@ test('single named layer with body', () => { name: 'first', is_anonymous: false, children: [], - locations: [{ line: 1, column: 1, start: 0, end: 15 }] + locations: [{ line: 1, column: 1, start: 0, end: 15 }], }, ] assert.equal(actual, expected) @@ -61,13 +61,13 @@ test('multiple named layers in one line', () => { name: 'first', is_anonymous: false, children: [], - locations: [{ line: 1, column: 1, start: 0, end: 21 }] + locations: [{ line: 1, column: 1, start: 0, end: 21 }], }, { name: 'second', is_anonymous: false, children: [], - locations: [{ line: 1, column: 1, start: 0, end: 21 }] + locations: [{ line: 1, column: 1, start: 0, end: 21 }], }, ] assert.equal(actual, expected) @@ -85,8 +85,8 @@ test('repeated use of the same layer name', () => { children: [], locations: [ { line: 2, column: 3, start: 3, end: 18 }, - { line: 3, column: 3, start: 21, end: 36 } - ] + { line: 3, column: 3, start: 21, end: 36 }, + ], }, ] assert.equal(actual, expected) @@ -199,7 +199,7 @@ test('nested layers with anonymous layers and duplicate names', () => { children: [], locations: [{ line: 3, column: 4, start: 15, end: 30 }], }, - ] + ], }, { name: 'first', From 261f2a5f5d2a15b5fb046fdf5c2058b1cfc98807 Mon Sep 17 00:00:00 2001 From: Bart Veneman Date: Mon, 17 Nov 2025 21:15:33 +0100 Subject: [PATCH 11/14] 2.0.1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 53e4e49..d644ed0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@projectwallace/css-layer-tree", - "version": "2.0.0", + "version": "2.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@projectwallace/css-layer-tree", - "version": "2.0.0", + "version": "2.0.1", "license": "MIT", "dependencies": { "css-tree": "^3.0.0" diff --git a/package.json b/package.json index 7ad3585..9bda6f1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@projectwallace/css-layer-tree", - "version": "2.0.0", + "version": "2.0.1", "description": "Discover the composition of your CSS @layers in a tree-based format.", "repository": { "type": "git", From 59491d946cfc171bd765aa003e8dadde50f4638f Mon Sep 17 00:00:00 2001 From: Bart Veneman Date: Mon, 17 Nov 2025 21:20:06 +0100 Subject: [PATCH 12/14] update release workflow --- .github/workflows/release.yml | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 91cba14..c13d839 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,17 +7,11 @@ on: release: types: [created] -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: 20 - - run: npm install --ignore-scripts --no-audit --no-fund - - run: npm test +permissions: + id-token: write # Required for OIDC + contents: read +jobs: publish-npm: needs: build runs-on: ubuntu-latest @@ -25,10 +19,10 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - cache: 'npm' + node-version: 22 registry-url: https://registry.npmjs.org/ + - run: npm install -g npm@latest - run: npm install --ignore-scripts --no-audit --no-fund + - run: npm test - run: npm run build - - run: npm publish --public - env: - NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} + - run: npm publish --access public From 818100cb8be92abb5401ecbd8384391109966d49 Mon Sep 17 00:00:00 2001 From: Bart Veneman Date: Mon, 17 Nov 2025 21:23:50 +0100 Subject: [PATCH 13/14] fix workflow --- .github/workflows/release.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c13d839..03170ea 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,7 +13,6 @@ permissions: jobs: publish-npm: - needs: build runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 From f4f8ff6ddc985f6dd8489016af013c4b078454c5 Mon Sep 17 00:00:00 2001 From: Bart Veneman Date: Mon, 17 Nov 2025 21:24:09 +0100 Subject: [PATCH 14/14] 2.0.2 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index d644ed0..095a6c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@projectwallace/css-layer-tree", - "version": "2.0.1", + "version": "2.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@projectwallace/css-layer-tree", - "version": "2.0.1", + "version": "2.0.2", "license": "MIT", "dependencies": { "css-tree": "^3.0.0" diff --git a/package.json b/package.json index 9bda6f1..539a5f2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@projectwallace/css-layer-tree", - "version": "2.0.1", + "version": "2.0.2", "description": "Discover the composition of your CSS @layers in a tree-based format.", "repository": { "type": "git",