Skip to content

Commit

Permalink
Add @blueprintjs/stylelint-plugin package and no-prefix-literal rule (#…
Browse files Browse the repository at this point in the history
patrickszmucer authored Apr 29, 2021
1 parent fd0edb9 commit 27772ed
Showing 25 changed files with 828 additions and 15 deletions.
4 changes: 4 additions & 0 deletions .stylelintrc
Original file line number Diff line number Diff line change
@@ -3,7 +3,11 @@
"stylelint-config-palantir",
"stylelint-config-palantir/sass.js"
],
"plugins": [
"@blueprintjs/stylelint-plugin"
],
"rules": {
"@blueprintjs/no-prefix-literal": [true, { "disableFix": true }],
"declaration-empty-line-before": null,
"indentation": [2, {
"ignore": ["value"]
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -54,6 +54,7 @@ These packages define development dependencies and contain build configuration.
- [![npm](https://img.shields.io/npm/v/@blueprintjs/eslint-plugin.svg?label=@blueprintjs/eslint-plugin)](https://www.npmjs.com/package/@blueprintjs/eslint-plugin) – implementations for custom ESLint rules which enforce best practices for Blueprint usage
- [![npm](https://img.shields.io/npm/v/@blueprintjs/karma-build-scripts.svg?label=@blueprintjs/karma-build-scripts)](https://www.npmjs.com/package/@blueprintjs/karma-build-scripts)
- [![npm](https://img.shields.io/npm/v/@blueprintjs/node-build-scripts.svg?label=@blueprintjs/node-build-scripts)](https://www.npmjs.com/package/@blueprintjs/node-build-scripts) – various utility scripts for linting, working with CSS variables, and building icons
- [![npm](https://img.shields.io/npm/v/@blueprintjs/stylelint-plugin.svg?label=@blueprintjs/stylelint-plugin)](https://www.npmjs.com/package/@blueprintjs/stylelint-plugin) – implementations for custom stylelint rules which enforce best practices for Blueprint usage
- [![npm](https://img.shields.io/npm/v/@blueprintjs/test-commons.svg?label=@blueprintjs/test-commons)](https://www.npmjs.com/package/@blueprintjs/test-commons) – various utility functions used in Blueprint test suites
- [![npm](https://img.shields.io/npm/v/@blueprintjs/tslint-config.svg?label=@blueprintjs/tslint-config)](https://www.npmjs.com/package/@blueprintjs/tslint-config) – TSLint configuration used in this repo and recommended for Blueprint-related projects (should be installed by `@blueprintjs/eslint-config`, not directly)
- [![npm](https://img.shields.io/npm/v/@blueprintjs/webpack-build-scripts.svg?label=@blueprintjs/webpack-build-scripts)](https://www.npmjs.com/package/@blueprintjs/webpack-build-scripts)
2 changes: 2 additions & 0 deletions packages/core/src/common/_variables.scss
Original file line number Diff line number Diff line change
@@ -7,6 +7,8 @@
// Namespace appended to the beginning of each CSS class: `.#{$ns}-button`.
// Do not quote this value, for Less consumers.
$ns: bp3 !default;
// Alias for BP users outside this repo
$bp-ns: $ns;

// easily the most important variable, so it comes up top
// (so other variables can use it to define themselves)
10 changes: 10 additions & 0 deletions packages/stylelint-plugin/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"root": true,
"extends": ["../../.eslintrc.js"],
"rules": {
"no-duplicate-imports": "off",
"@typescript-eslint/no-duplicate-imports": ["error"],
"import/no-default-export": "off"
},
"ignorePatterns": ["node_modules", "dist", "lib", "fixtures", "coverage", "__snapshots__", "generated"]
}
69 changes: 69 additions & 0 deletions packages/stylelint-plugin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<img height="204" src="https://cloud.githubusercontent.com/assets/464822/20228152/d3f36dc2-a804-11e6-80ff-51ada2d13ea7.png">

# [Blueprint](http://blueprintjs.com/) [stylelint](https://stylelint.io/) plugin

Blueprint is a React UI toolkit for the web.

This package contains the [stylelint](https://stylelint.io/) plugin for Blueprint. It provides custom rules which are useful when developing against Blueprint libraries.

**Key features:**

- [Blueprint-specific rules](#Rules) for use with `@blueprintjs` components.

## Installation

```
yarn add --dev @blueprintjs/stylelint-plugin
```

## Usage

Simply add this plugin in your `.stylelintrc` file and then pick the rules that you need. The plugin includes Blueprint-specific rules which enforce semantics particular to usage with `@blueprintjs` packages, but does not turn them on by default.

`.stylelintrc`

```json
{
"plugins": [
"@blueprintjs/stylelint-plugin"
],
"rules": {
"@blueprintjs/no-prefix-literal": true
}
}
```

## Rules

### `@blueprintjs/no-prefix-literal` (autofixable)

Enforce usage of the `bp-ns` constant over namespaced string literals.

The `@blueprintjs` package exports a `bp-ns` CSS variable which contains the prefix for the current version of Blueprint (`bp3` for Blueprint 3, `bp4` for Blueprint 4, and etc). Using the variable instead of hardcoding the prefix means that your code will still work when new major version of Blueprint is released.

```json
{
"rules": {
"@blueprintjs/no-prefix-literal": true
}
}
```

```diff
-.bp3-button > div {
- border: 1px solid black;
-}
+ @import "~@blueprntjs/core/lib/scss/variables";
+
+.#{$bp-ns}-button > div {
+ border: 1px solid black;
+}
```

Optional secondary options:

- `disableFix: boolean` - if true, autofix will be disabled
- `variablesImportPath: { less?: string, sass?: string }` - can be used to configure a custom path for importing Blueprint variables when autofixing.


### [Full Documentation](http://blueprintjs.com/docs) | [Source Code](https://github.com/palantir/blueprint)
33 changes: 33 additions & 0 deletions packages/stylelint-plugin/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "@blueprintjs/stylelint-plugin",
"version": "0.0.0",
"description": "Stylelint rules for use with @blueprintjs packages",
"main": "lib/index.js",
"scripts": {
"compile": "tsc -p src/",
"lint": "run-p lint:es",
"lint:es": "es-lint",
"lint-fix": "es-lint --fix",
"test": "mocha test/index.js"
},
"dependencies": {
"postcss": "^7.0.35",
"postcss-selector-parser": "^6.0.5"
},
"peerDependencies": {
"stylelint": "^13.0.0"
},
"devDependencies": {
"@blueprintjs/node-build-scripts": "^1.5.0",
"@types/stylelint": "^9.10.1",
"mocha": "^8.2.1",
"typescript": "^4.1.2"
},
"repository": {
"type": "git",
"url": "[email protected]:palantir/blueprint.git",
"directory": "packages/stylelint-plugin"
},
"author": "Palantir Technologies",
"license": "Apache-2.0"
}
18 changes: 18 additions & 0 deletions packages/stylelint-plugin/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Copyright 2017 Palantir Technologies, Inc. All rights reserved.
* Licensed 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 CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import noPrefixLiteral from "./rules/no-prefix-literal";

export default [noPrefixLiteral];
152 changes: 152 additions & 0 deletions packages/stylelint-plugin/src/rules/no-prefix-literal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/*
* Copyright 2021 Palantir Technologies, Inc. All rights reserved.
* Licensed 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 CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { Root, Result } from "postcss";
import parser from "postcss-selector-parser";
import stylelint from "stylelint";
import type { Plugin, RuleTesterContext } from "stylelint";

import { checkImportExists } from "../utils/checkImportExists";
import { insertImport } from "../utils/insertImport";

const ruleName = "@blueprintjs/no-prefix-literal";

const messages = stylelint.utils.ruleMessages(ruleName, {
expected: (unfixed: string, fixed: string) => `Use the \`${fixed}\` variable instead of the \`${unfixed}\` literal`,
});

const bannedPrefixes = ["bp", "bp3", "bp4"];

interface Options {
disableFix?: boolean;
variablesImportPath?: Partial<Record<Exclude<CssSyntax, CssSyntax.OTHER>, string>>;
}

export default stylelint.createPlugin(ruleName, ((
enabled: boolean,
options: Options | undefined,
context: RuleTesterContext,
) => (root: Root, result: Result) => {
if (!enabled) {
return;
}

const validOptions = stylelint.utils.validateOptions(
result,
ruleName,
{
actual: enabled,
optional: false,
possible: [true, false],
},
{
actual: options,
optional: true,
possible: {
disableFix: [true, false],
variablesImportPath: (obj: unknown) => {
if (typeof obj !== "object" || obj == null) {
return false;
}
// Check that the keys and their values are correct
const allowedKeys = new Set<string>(Object.values(CssSyntax).filter(v => v !== CssSyntax.OTHER));
return Object.keys(obj).every(key => allowedKeys.has(key) && typeof (obj as any)[key] === "string");
},
},
},
);

if (!validOptions) {
return;
}

const disableFix = options?.disableFix ?? false;

const cssSyntax = getCssSyntax(root.source?.input.file || "");
if (cssSyntax === CssSyntax.OTHER) {
return;
}

let hasBpVariablesImport: boolean | undefined; // undefined means not checked yet
function assertBpVariablesImportExists(cssSyntaxType: CssSyntax.SASS | CssSyntax.LESS) {
const importPath = options?.variablesImportPath?.[cssSyntaxType] ?? BpVariableImportMap[cssSyntaxType];
const extension = CssExtensionMap[cssSyntaxType];
if (hasBpVariablesImport == null) {
hasBpVariablesImport = checkImportExists(root, [importPath, `${importPath}.${extension}`]);
}
if (!hasBpVariablesImport) {
insertImport(root, context, importPath);
hasBpVariablesImport = true;
}
}

root.walkRules(rule => {
parser(selectors => {
selectors.walkClasses(selector => {
for (const bannedPrefix of bannedPrefixes) {
if (!selector.value.startsWith(`${bannedPrefix}-`)) {
continue;
}
if ((context as any).fix && !disableFix) {
assertBpVariablesImportExists(cssSyntax);
rule.selector = rule.selector.replace(bannedPrefix, BpPrefixVariableMap[cssSyntax]);
} else {
stylelint.utils.report({
// HACKHACK - offset by one because otherwise the error is reported at a wrong position
index: selector.sourceIndex + 1,
message: messages.expected(bannedPrefix, BpPrefixVariableMap[cssSyntax]),
node: rule,
result,
ruleName,
});
}
}
});
}).processSync(rule.selector);
});
}) as Plugin);

enum CssSyntax {
SASS = "sass",
LESS = "less",
OTHER = "other",
}

const CssExtensionMap: Record<Exclude<CssSyntax, CssSyntax.OTHER>, string> = {
[CssSyntax.SASS]: "scss",
[CssSyntax.LESS]: "less",
};

const BpPrefixVariableMap: Record<Exclude<CssSyntax, CssSyntax.OTHER>, string> = {
[CssSyntax.SASS]: "#{$bp-ns}",
[CssSyntax.LESS]: "@{bp-ns}",
};

const BpVariableImportMap: Record<Exclude<CssSyntax, CssSyntax.OTHER>, string> = {
[CssSyntax.SASS]: "~@blueprintjs/core/lib/scss/variables",
[CssSyntax.LESS]: "~@blueprintjs/core/lib/less/variables",
};

/**
* Returns the flavor of the CSS we're dealing with.
*/
function getCssSyntax(fileName: string): CssSyntax {
for (const cssSyntax of Object.keys(CssExtensionMap)) {
if (fileName.endsWith(`.${CssExtensionMap[cssSyntax as Exclude<CssSyntax, CssSyntax.OTHER>]}`)) {
return cssSyntax as Exclude<CssSyntax, CssSyntax.OTHER>;
}
}
return CssSyntax.OTHER;
}
9 changes: 9 additions & 0 deletions packages/stylelint-plugin/src/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "../../../config/tsconfig.base",
"compilerOptions": {
"lib": ["es6", "dom"],
"module": "commonjs",
"outDir": "../lib",
"target": "ES2015"
}
}
45 changes: 45 additions & 0 deletions packages/stylelint-plugin/src/utils/checkImportExists.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright 2017 Palantir Technologies, Inc. All rights reserved.
* Licensed 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 CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import type { Root } from "postcss";

/**
* Returns true if the given import exists in the file, otherwise returns false.
* If `importPath` is an array, any of the strings has to match in order fortrue to be returned.
*/
export function checkImportExists(root: Root, importPath: string | string[]): boolean {
let hasBpVarsImport = false;
root.walkAtRules(/^import$/i, atRule => {
for (const path of typeof importPath === "string" ? [importPath] : importPath) {
// `atRule.params` includes quotes around the string, so we strip them.
if (stripQuotes(atRule.params) === path) {
hasBpVarsImport = true;
return false; // Stop the iteration
}
}
return true;
});
return hasBpVarsImport;
}

function stripQuotes(str: string): string {
if (
(str.charAt(0) === '"' && str.charAt(str.length - 1) === '"') ||
(str.charAt(0) === "'" && str.charAt(str.length - 1) === "'")
) {
return str.substr(1, str.length - 2);
}
return str;
}
Loading

1 comment on commit 27772ed

@blueprint-bot
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add @blueprintjs/stylelint-plugin package and no-prefix-literal rule (#4683)

Previews: documentation | landing | table

Please sign in to comment.