Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 13 commits into from
Apr 29, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .stylelintrc
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
"stylelint-config-palantir",
"stylelint-config-palantir/sass.js"
],
"plugins": [
"@blueprintjs/stylelint-plugin"
],
"rules": {
"@blueprintjs/no-prefix-literal": true,
"declaration-empty-line-before": null,
"indentation": [2, {
"ignore": ["value"]
Expand Down
9 changes: 9 additions & 0 deletions packages/stylelint-plugin/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"root": true,
"extends": ["../../.eslintrc.js"],
"rules": {
"no-duplicate-imports": "off",
"@typescript-eslint/no-duplicate-imports": ["error"]
},
"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 `ns` constant over namespaced string literals.
p-szm marked this conversation as resolved.
Show resolved Hide resolved

The `@blueprintjs` package exports a `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.
p-szm marked this conversation as resolved.
Show resolved Hide resolved

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

```diff
-.bp3-button > div {
- border: 1px solid black;
-}
+ @import "~@blueprntjs/core/lib/scss/variables";
+
+.#{$ns}-button > div {
p-szm marked this conversation as resolved.
Show resolved Hide resolved
+ 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": "git@github.com: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 2020 Palantir Technologies, Inc. All rights reserved.
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: formatting of block comment is slightly off (is the autofixer broken?)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I fixed this manually, but actually it seems like this is also a valid format and the autofixer always does it this way on my computer. Could you test what happens on your machine?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It looks like the plugin only inserts starts at the beginning and end https://github.com/Stuk/eslint-plugin-header/blob/66f5269c8a7e3282afd02b914b502f7bb95fe702/lib/rules/header.js#L46

Do you have a template for ts/js files configured in VS code by any chance?


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";

// eslint-disable-next-line import/no-default-export
p-szm marked this conversation as resolved.
Show resolved Hide resolved
export default [noPrefixLiteral];
147 changes: 147 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,147 @@
/*
* 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 = ["bp1", "bp2", "bp3", "bp4"];
p-szm marked this conversation as resolved.
Show resolved Hide resolved

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

// eslint-disable-next-line import/no-default-export
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;
styu marked this conversation as resolved.
Show resolved Hide resolved
}

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
styu marked this conversation as resolved.
Show resolved Hide resolved
function assertBpVariablesImportExists(cssSyntaxType: CssSyntax.SASS | CssSyntax.LESS) {
const importPath = options?.variablesImportPath?.[cssSyntaxType] ?? BpVariableImportMap[cssSyntaxType];
if (hasBpVariablesImport == null) {
hasBpVariablesImport = checkImportExists(root, importPath);
}
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 BpPrefixVariableMap: Record<Exclude<CssSyntax, CssSyntax.OTHER>, string> = {
[CssSyntax.SASS]: "#{$ns}",
[CssSyntax.LESS]: "@{ns}",
};

const BpVariableImportMap: Record<Exclude<CssSyntax, CssSyntax.OTHER>, string> = {
[CssSyntax.SASS]: "~@blueprntjs/core/lib/scss/variables",
p-szm marked this conversation as resolved.
Show resolved Hide resolved
[CssSyntax.LESS]: "~@blueprintjs/core/lib/less/variables",
};

/**
* Returns the flavor of the CSS we're dealing with.
*/
function getCssSyntax(fileName: string): CssSyntax {
if (fileName.endsWith(".scss")) {
return CssSyntax.SASS;
} else if (fileName.endsWith(".less")) {
return CssSyntax.LESS;
}
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"
}
}
41 changes: 41 additions & 0 deletions packages/stylelint-plugin/src/utils/checkImportExists.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/* Copyright 2020 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.
*/
export function checkImportExists(root: Root, importPath: string): boolean {
let hasBpVarsImport = false;
root.walkAtRules(/^import$/i, atRule => {
styu marked this conversation as resolved.
Show resolved Hide resolved
// `atRule.params` includes quotes around the string, so we strip them.
if (stripQuotes(atRule.params) === importPath) {
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