Skip to content

Commit

Permalink
chore: add stylelint theme alignment tool; harden settings for s2 (#3026
Browse files Browse the repository at this point in the history
)
  • Loading branch information
castastrophe authored Sep 19, 2024
1 parent 1830942 commit 544a803
Show file tree
Hide file tree
Showing 15 changed files with 399 additions and 287 deletions.
5 changes: 5 additions & 0 deletions .changeset/giant-scissors-protect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@spectrum-tools/theme-alignment": major
---

Initial release of the stylelint theme alignment tool. This package uses the base file (themes/spectrum.css) for a Spectrum CSS component as a "source of truth" and validates the sub-themes (i.e., themes/express.css) use only selectors and custom properties defined in the base file.
7 changes: 7 additions & 0 deletions .changeset/twelve-melons-battle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@spectrum-tools/stylelint-no-unknown-custom-properties": patch
"@spectrum-tools/stylelint-no-unused-custom-properties": patch
"@spectrum-tools/stylelint-no-missing-var": patch
---

Dependency updates to align with versions used in the parent repository.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@
"stylelint-header": "^2.0.1",
"stylelint-high-performance-animation": "^1.10.0",
"stylelint-order": "^6.0.4",
"stylelint-selector-bem-pattern": "^4.0.0",
"stylelint-selector-bem-pattern": "^4.0.1",
"stylelint-use-logical": "^2.1.2",
"tar": "^7.4.3",
"yargs": "^17.7.2"
Expand Down
4 changes: 2 additions & 2 deletions plugins/stylelint-no-missing-var/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
},
"devDependencies": {
"ava": "^6.1.3",
"c8": "^9.1.0",
"stylelint": "^16.5.0"
"c8": "^10.1.2",
"stylelint": "^16.9.0"
},
"keywords": [
"css",
Expand Down
6 changes: 3 additions & 3 deletions plugins/stylelint-no-unknown-custom-properties/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const messages = ruleMessages(ruleName, {

import fg from "fast-glob";
import { parse } from "postcss";
import valueParser from "postcss-value-parser";
import valuesParser from "postcss-values-parser";

/** @type {import('stylelint').Plugin} */
const ruleFunction = (enabled, options = {}) => {
Expand Down Expand Up @@ -133,9 +133,9 @@ const ruleFunction = (enabled, options = {}) => {
/* Collect variable use information */
root.walkDecls((decl) => {
// Parse value and get a list of variables used
const parsed = valueParser(decl.value);
const parsed = valuesParser.parse(decl.value);
parsed.walk((node) => {
if (node.type !== "function" || node.value !== "var") {
if (node.type !== "func" || node.name !== "var") {
return;
}

Expand Down
16 changes: 12 additions & 4 deletions plugins/stylelint-no-unknown-custom-properties/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,32 @@
"description": "Report on any unknown custom property definitions",
"license": "Apache-2.0",
"author": "Adobe",
"contributors": [
"Cassondra Roberts <[email protected]>"
],
"type": "module",
"main": "index.js",
"files": [
"package.json",
"index.js",
"*.md"
],
"scripts": {
"test": "ava"
},
"dependencies": {
"colors": "^1.4.0",
"fast-glob": "^3.3.2",
"postcss": "^8.4.45",
"postcss-value-parser": "^4.2.0"
"postcss": "^8.4.47",
"postcss-values-parser": "^6.0.2"
},
"peerDependencies": {
"stylelint": ">=16.0.0"
},
"devDependencies": {
"ava": "^6.1.3",
"c8": "^9.1.0",
"stylelint": "^16.5.0"
"c8": "^10.1.2",
"stylelint": "^16.9.0"
},
"keywords": [
"css",
Expand Down
7 changes: 3 additions & 4 deletions plugins/stylelint-no-unused-custom-properties/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const messages = ruleMessages(ruleName, {
referenced: (prop) => `Custom property ${prop.magenta}'s references have been removed`,
});

import valueParser from "postcss-value-parser";
import valuesParser from "postcss-values-parser";

/** @type {import('stylelint').Plugin} */
const ruleFunction = (enabled, { ignoreList = [] } = {}, context = {}) => {
Expand Down Expand Up @@ -76,9 +76,9 @@ const ruleFunction = (enabled, { ignoreList = [] } = {}, context = {}) => {
const usedInDecl = new Set();

// Parse value and get a list of variables used
const parsed = valueParser(decl.value);
const parsed = valuesParser.parse(decl.value);
parsed.walk((node) => {
if (node.type !== "function" || node.value !== "var" || !node.nodes.length) {
if (node.type !== "func" || node.name !== "var" || !node.nodes.length) {
return;
}

Expand Down Expand Up @@ -189,7 +189,6 @@ const ruleFunction = (enabled, { ignoreList = [] } = {}, context = {}) => {
};
};


ruleFunction.ruleName = ruleName;
ruleFunction.messages = messages;

Expand Down
14 changes: 11 additions & 3 deletions plugins/stylelint-no-unused-custom-properties/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,30 @@
"description": "Report on any unused custom property definitions",
"license": "Apache-2.0",
"author": "Adobe",
"contributors": [
"Cassondra Roberts <[email protected]>"
],
"type": "module",
"main": "index.js",
"files": [
"package.json",
"index.js",
"*.md"
],
"scripts": {
"test": "ava"
},
"dependencies": {
"colors": "^1.4.0",
"postcss-value-parser": "^4.2.0"
"postcss-values-parser": "^6.0.2"
},
"peerDependencies": {
"stylelint": ">=16.0.0"
},
"devDependencies": {
"ava": "^6.1.3",
"c8": "^9.1.0",
"stylelint": "^16.5.0"
"c8": "^10.1.2",
"stylelint": "^16.9.0"
},
"keywords": [
"css",
Expand Down
1 change: 1 addition & 0 deletions plugins/stylelint-theme-alignment/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Change Log
78 changes: 78 additions & 0 deletions plugins/stylelint-theme-alignment/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# stylelint-no-unused-custom-properties

> Remove or report on unused variable definitions
## Installation

```sh
yarn add -D @spectrum-tools/stylelint-no-unused-custom-properties
```

## Usage

Assuming you have some variables defined and rule(s) that use them:

```css
:root {
--prefix-component-background-color: blue;
--prefix-component-width: 10px;
--prefix-component-height: 10px;
--prefix-component-size: 10px;
}

.component {
background-color: var(--prefix-component-background-color);

width: var(--prefix-component-width);
height: var(--prefix-component-height);
}
```

The variables that are not used in any rule will be removed from the output or reported to the console:

```css
:root {
--prefix-component-background-color: blue;
--prefix-component-width: 10px;
--prefix-component-height: 10px;
}

.component {
background-color: var(--prefix-component-background-color);

width: var(--prefix-component-width);
height: var(--prefix-component-height);
}
```

To allow variables to be defined without being used, such as when you want to pass custom properties down to a child component, you can add a `/* @passthrough */` comment to the variable definition:

```css
:root {
/* @passthrough */
--nested-component-background-color: blue;
--prefix-component-width: 10px;
--prefix-component-height: 10px;
--prefix-component-size: 10px;
}
```

To allow a group of properties to be passed down, you can prefix the set with `/* @passthrough start */` and suffix it with `/* @passthrough end */`:

```css
:root {
/* @passthrough start */
--nested-component-background-color: blue;
--nested-component-width: 10px;
/* @passthrough end */

--prefix-component-height: 10px;
--prefix-component-size: 10px;
}
```

## Options

### `ignoreList` (default: `[]`)

An array of strings or regular expressions that will be matched against the variable name. If a match is found, the variable will be ignored.
143 changes: 143 additions & 0 deletions plugins/stylelint-theme-alignment/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/*!
* Copyright 2024 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

import fs from "node:fs";
import { relative, sep } from "node:path";

import postcss from "postcss";
import valuesParser from "postcss-values-parser";
import stylelint from "stylelint";

const {
createPlugin,
utils: { report, ruleMessages, validateOptions }
} = stylelint;

import "colors";

const ruleName = "spectrum-tools/theme-alignment";
const messages = ruleMessages(ruleName, {
missing: (baseFile, sourceFile, rootPath) => `A base file (${relative(rootPath, baseFile)}) is required to validate ${relative(rootPath, sourceFile)}.`,
// Report if a selector is in this file but not in the base file
expected: (selector, baseFile, rootPath) => `Selector "${selector}" is not used or defined in the base file (${relative(rootPath, baseFile)}).`,
// Report if a custom property is used in this file but not in the base file
referenced: (property, baseFile, rootPath) => `Custom property "${property}" is not used or defined by the base file (${relative(rootPath, baseFile)}).`,
});

/** @type {import('stylelint').Plugin} */
const ruleFunction = (enabled) => {
return (root, result) => {
const validOptions = validateOptions(
result,
ruleName,
{
actual: enabled,
possible: [true],
},
);

if (!validOptions) return;

const sourceFile = root.source.input.file;
const parts = sourceFile ? sourceFile.split(sep) : [];
const isTheme = parts[parts.length - 2] === "themes";
const filename = parts[parts.length - 1];

if (!isTheme || filename === "spectrum.css") return;

// All the parts of the source file but replace the filename with spectrum-two.css
const baseFile = [...parts.slice(0, -1), "spectrum.css"].join(sep);
const rootPath = parts.slice(0, -2).join(sep);

// If the base file doesn't exist, throw an error
if (!fs.existsSync(baseFile)) {
report({
message: messages.missing,
messageArgs: [baseFile, sourceFile, rootPath],
node: root,
result,
ruleName,
});
return;
}

// Read in the base file and parse it
const baseContent = fs.readFileSync(baseFile, "utf8");
const baseRoot = postcss.parse(baseContent);

/* A list of all selectors in the base file */
const baseSelectors = new Set();
/* A list of all properties in the base file */
const baseProperties = new Set();

/* Iterate over selectors in the base root */
baseRoot.walkRules((rule) => {
// Add this selector to the selectors set
baseSelectors.add(rule.selector);

rule.walkDecls((decl) => {
// If this is a custom property, add it to the properties set
if (decl.prop.startsWith("--")) {
baseProperties.add(decl.prop);
}

// If the value of this declaration includes a custom property, add it to the properties set
const parsed = valuesParser.parse(decl.value);
parsed.walk((node) => {
if (node.type === "func" && node.value === "var") {
baseProperties.add(node.nodes[0].value);
}
});
});
});

/* Iterate over selectors in the source root and validate that they align with the base */
root.walkRules((rule) => {
// Check if this selector exists in the base
if (!baseSelectors.has(rule.selector)) {
// Report any selectors that don't exist in the base
report({
message: messages.expected,
messageArgs: [rule.selector, baseFile, rootPath],
node: rule,
result,
ruleName,
});
return;
}

rule.walkDecls((decl) => {
const isProperty = decl.prop.startsWith("--");
// @todo should report that this is setting something other than a custom property in the theme file
if (!isProperty) {
return;
}

// If this is a custom property, check if it's used in the base
if (!baseProperties.has(decl.prop)) {
report({
message: messages.referenced,
messageArgs: [decl.prop, baseFile, rootPath],
node: decl,
result,
ruleName,
});
}
});
});
};
};

ruleFunction.ruleName = ruleName;
ruleFunction.messages = messages;

export default createPlugin(ruleName, ruleFunction);
Loading

0 comments on commit 544a803

Please sign in to comment.