@hero-design/snowflake-guard 1.4.6 → 1.4.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -5
- package/lib/src/graphql/types.d.ts +2 -1
- package/lib/src/reports/mobile/reportCustomStyleProperties.d.ts +1 -18
- package/lib/src/reports/mobile/reportCustomStyleProperties.js +3 -5
- package/lib/src/reports/mobile/reportInlineStyle.d.ts +0 -1
- package/lib/src/reports/mobile/reportInlineStyle.js +2 -3
- package/lib/src/reports/mobile/testUtils.d.ts +0 -1
- package/lib/src/reports/mobile/testUtils.js +1 -6
- package/lib/src/reports/reportInlineStyle.d.ts +0 -2
- package/lib/src/reports/reportInlineStyle.js +4 -5
- package/package.json +4 -14
package/README.md
CHANGED
|
@@ -1,16 +1,27 @@
|
|
|
1
1
|
# @hero-design/snowflake-guard
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## Overview
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
A GitHub App built with [Probot](https://github.com/probot/probot) that analyzes PR changes and reports snowflake usage of Hero Design components. The bot detects when components are customized using inline styles, styled-components, sx props, or className attributes that may violate design system guidelines.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
- Node.js >= 10.13.0
|
|
10
|
+
- Yarn package manager
|
|
11
|
+
- Access to [internal-tool-integrations service](https://github.com/Thinkei/internal-tool-integrations) for storing reports
|
|
12
|
+
- GitHub App installation permissions for the repository
|
|
13
|
+
|
|
14
|
+
## Development
|
|
15
|
+
|
|
16
|
+
### Setup
|
|
17
|
+
|
|
18
|
+
1. Copy `.env.example` to `.env` and ask Andromeda team for variable details.
|
|
8
19
|
|
|
9
20
|
```sh
|
|
10
21
|
cp .env.example .env
|
|
11
22
|
```
|
|
12
23
|
|
|
13
|
-
For React Native projects, include your repo name in the `MOBILE_REPO_NAMES` env.
|
|
24
|
+
For React Native projects, include your repo name in the `MOBILE_REPO_NAMES` env variable.
|
|
14
25
|
|
|
15
26
|
2. Set up [internal-tool-integrations service](https://github.com/Thinkei/internal-tool-integrations) for the bot to store report.
|
|
16
27
|
|
|
@@ -32,8 +43,60 @@ yarn install
|
|
|
32
43
|
# Run the bot
|
|
33
44
|
yarn start
|
|
34
45
|
```
|
|
46
|
+
|
|
35
47
|
6. Open or update a pull request to trigger webhook events.
|
|
36
48
|
|
|
37
|
-
|
|
49
|
+
### Scripts
|
|
50
|
+
|
|
51
|
+
- `yarn build` - Compile TypeScript to JavaScript
|
|
52
|
+
- `yarn start` - Build and run the bot with Probot
|
|
53
|
+
- `yarn test` - Run tests with Jest
|
|
54
|
+
- `yarn lint` - Run ESLint
|
|
55
|
+
- `yarn type-check` - Type check without emitting files
|
|
56
|
+
- `yarn deploy` - Deploy to Netlify
|
|
57
|
+
- `yarn publish:npm` - Publish package to npm
|
|
58
|
+
|
|
59
|
+
## Project Structure
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
apps/snowflake-guard/
|
|
63
|
+
├── src/
|
|
64
|
+
│ ├── index.ts # Main entry point, Probot app setup
|
|
65
|
+
│ ├── parseSource.ts # Web source code parser
|
|
66
|
+
│ ├── parseMobileSource.ts # Mobile source code parser
|
|
67
|
+
│ ├── parsers/ # TypeScript and Flow parsers
|
|
68
|
+
│ ├── reports/ # Snowflake detection logic
|
|
69
|
+
│ │ ├── constants.ts # Report constants and configurations
|
|
70
|
+
│ │ ├── mobile/ # Mobile-specific report generators
|
|
71
|
+
│ │ └── ... # Web report generators
|
|
72
|
+
│ ├── graphql/ # GraphQL queries for report storage
|
|
73
|
+
│ └── utils/ # Utility functions
|
|
74
|
+
├── netlify/ # Netlify serverless functions
|
|
75
|
+
├── lib/ # Compiled JavaScript output
|
|
76
|
+
└── package.json
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Deployment
|
|
80
|
+
|
|
81
|
+
The app is deployed to Netlify as a serverless function. Deployment is automated via GitHub Actions when a new version is published.
|
|
82
|
+
|
|
83
|
+
To deploy manually:
|
|
84
|
+
|
|
85
|
+
```sh
|
|
86
|
+
yarn deploy
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
This requires the `NETLIFY_AUTH_TOKEN` environment variable to be set.
|
|
90
|
+
|
|
91
|
+
## Contributing
|
|
38
92
|
|
|
39
93
|
This bot does not support hot-reload yet, please restart the app once you make any changes to the code.
|
|
94
|
+
|
|
95
|
+
To contribute:
|
|
96
|
+
|
|
97
|
+
1. Make changes to the source code in `src/`
|
|
98
|
+
2. Test locally with `yarn start`
|
|
99
|
+
3. Ensure tests pass with `yarn test`
|
|
100
|
+
4. Submit a pull request
|
|
101
|
+
|
|
102
|
+
Note: The bot analyzes PR diffs and comments on potential snowflake usage violations. Approved patterns can be marked with special comments like `@snowflake-guard/approved-inline-style` or `@snowflake-guard/approved-classname`.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
type Report = {
|
|
2
2
|
id: string;
|
|
3
3
|
repoName: string;
|
|
4
4
|
prNumber: number;
|
|
@@ -15,3 +15,4 @@ export type FetchReportResponse = {
|
|
|
15
15
|
fetchHdSnowflakeGuardReport: Pick<Report, 'id' | 'originalCount' | 'latestCount' | 'approvedCount'>;
|
|
16
16
|
};
|
|
17
17
|
};
|
|
18
|
+
export {};
|
|
@@ -1,23 +1,6 @@
|
|
|
1
1
|
import * as recast from 'recast';
|
|
2
|
-
import type { InlineStyleProps
|
|
2
|
+
import type { InlineStyleProps } from './reportInlineStyle';
|
|
3
3
|
import type { CompoundMobileComponentName, MobileComponentName } from './types';
|
|
4
|
-
export declare const getNonApprovedInlineLocs: (reportedLocs: {
|
|
5
|
-
style?: number;
|
|
6
|
-
barStyle?: number;
|
|
7
|
-
containerStyle?: number;
|
|
8
|
-
textStyle?: number;
|
|
9
|
-
}, violatingAttributes: ViolatingAttribute[], approvedCmts: {
|
|
10
|
-
loc: number;
|
|
11
|
-
comment: string;
|
|
12
|
-
}[], elementLoc: number) => {
|
|
13
|
-
reportedLocs: {
|
|
14
|
-
style?: number;
|
|
15
|
-
barStyle?: number;
|
|
16
|
-
containerStyle?: number;
|
|
17
|
-
textStyle?: number;
|
|
18
|
-
};
|
|
19
|
-
noneApprovedAttributes: ViolatingAttribute[];
|
|
20
|
-
};
|
|
21
4
|
declare const reportCustomProperties: (ast: recast.types.ASTNode, componentList: {
|
|
22
5
|
[k: string]: MobileComponentName;
|
|
23
6
|
}, commentList: {
|
|
@@ -36,7 +36,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.getNonApprovedInlineLocs = void 0;
|
|
40
39
|
const recast = __importStar(require("recast"));
|
|
41
40
|
const reportInlineStyle_1 = __importDefault(require("./reportInlineStyle"));
|
|
42
41
|
const mapViolatingAttributesAndAdditionalProps = (violatingAttributes) => {
|
|
@@ -73,7 +72,6 @@ const getNonApprovedInlineLocs = (reportedLocs, violatingAttributes, approvedCmt
|
|
|
73
72
|
}
|
|
74
73
|
return { reportedLocs, noneApprovedAttributes: violatingAttributes };
|
|
75
74
|
};
|
|
76
|
-
exports.getNonApprovedInlineLocs = getNonApprovedInlineLocs;
|
|
77
75
|
const reportCustomProperties = (ast, componentList, commentList) => {
|
|
78
76
|
const report = {
|
|
79
77
|
style: [],
|
|
@@ -92,7 +90,7 @@ const reportCustomProperties = (ast, componentList, commentList) => {
|
|
|
92
90
|
const attributes = path.value
|
|
93
91
|
.attributes;
|
|
94
92
|
const { locs: styleObjectLocs, violatingAttributes } = (0, reportInlineStyle_1.default)(ast, attributes, componentList[path.value.name.name]);
|
|
95
|
-
const { reportedLocs, noneApprovedAttributes } =
|
|
93
|
+
const { reportedLocs, noneApprovedAttributes } = getNonApprovedInlineLocs(styleObjectLocs, violatingAttributes, commentList.styleCmts, path.value.loc.start.line);
|
|
96
94
|
if (reportedLocs.style) {
|
|
97
95
|
report.style.push(reportedLocs.style);
|
|
98
96
|
}
|
|
@@ -120,7 +118,7 @@ const reportCustomProperties = (ast, componentList, commentList) => {
|
|
|
120
118
|
const attributes = path.value
|
|
121
119
|
.attributes;
|
|
122
120
|
const { locs: styleObjectLocs, violatingAttributes } = (0, reportInlineStyle_1.default)(ast, attributes, compoundComponentName);
|
|
123
|
-
const { reportedLocs, noneApprovedAttributes } =
|
|
121
|
+
const { reportedLocs, noneApprovedAttributes } = getNonApprovedInlineLocs(styleObjectLocs, violatingAttributes, commentList.styleCmts, path.value.loc.start.line);
|
|
124
122
|
if (reportedLocs.style) {
|
|
125
123
|
report.style.push(reportedLocs.style);
|
|
126
124
|
}
|
|
@@ -166,7 +164,7 @@ const reportCustomProperties = (ast, componentList, commentList) => {
|
|
|
166
164
|
].join('.');
|
|
167
165
|
const { attributes } = openPath.value;
|
|
168
166
|
const { locs: styleObjectLocs, violatingAttributes } = (0, reportInlineStyle_1.default)(ast, attributes, compoundComponentName);
|
|
169
|
-
const { reportedLocs, noneApprovedAttributes } =
|
|
167
|
+
const { reportedLocs, noneApprovedAttributes } = getNonApprovedInlineLocs(styleObjectLocs, violatingAttributes, commentList.styleCmts, openPath.value.loc.start.line);
|
|
170
168
|
if (reportedLocs.style) {
|
|
171
169
|
report.style.push(reportedLocs.style);
|
|
172
170
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import * as recast from 'recast';
|
|
2
2
|
import type { CompoundMobileComponentName } from './types';
|
|
3
3
|
export type InlineStyleProps = 'style' | 'barStyle' | 'containerStyle' | 'textStyle';
|
|
4
|
-
export declare const INLINE_STYLE_PROPERTIES: string[];
|
|
5
4
|
export type ViolatingAttribute = {
|
|
6
5
|
attributeName: string;
|
|
7
6
|
attributeValue: string | null;
|
|
@@ -33,7 +33,6 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.INLINE_STYLE_PROPERTIES = void 0;
|
|
37
36
|
const recast = __importStar(require("recast"));
|
|
38
37
|
const constants_1 = require("./constants");
|
|
39
38
|
const BLACKLIST_PROPERTIES = {
|
|
@@ -42,7 +41,7 @@ const BLACKLIST_PROPERTIES = {
|
|
|
42
41
|
containerStyle: constants_1.CONTAINER_STYLE_RULESET_MAP,
|
|
43
42
|
textStyle: constants_1.TEXT_STYLE_RULESET_MAP,
|
|
44
43
|
};
|
|
45
|
-
|
|
44
|
+
const INLINE_STYLE_PROPERTIES = [
|
|
46
45
|
'style',
|
|
47
46
|
'barStyle',
|
|
48
47
|
'containerStyle',
|
|
@@ -76,7 +75,7 @@ const reportInlineStyle = (ast, attributes, componentName) => {
|
|
|
76
75
|
if (((_a = attr.value) === null || _a === void 0 ? void 0 : _a.type) !== 'JSXExpressionContainer') {
|
|
77
76
|
return;
|
|
78
77
|
}
|
|
79
|
-
if (
|
|
78
|
+
if (INLINE_STYLE_PROPERTIES.includes(attr.name.name)) {
|
|
80
79
|
styleObjName = attr.name.name;
|
|
81
80
|
const PROHIBITED_PROPERTIES = BLACKLIST_PROPERTIES[styleObjName][componentName] || [];
|
|
82
81
|
const { expression } = attr.value;
|
|
@@ -33,15 +33,10 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.
|
|
36
|
+
exports.parseTypeScript = void 0;
|
|
37
37
|
const recast = __importStar(require("recast"));
|
|
38
38
|
const tsParser = __importStar(require("../../parsers/typescript"));
|
|
39
|
-
const flowParser = __importStar(require("../../parsers/flow"));
|
|
40
39
|
const parseTypeScript = (source) => {
|
|
41
40
|
return recast.parse(source, { parser: tsParser });
|
|
42
41
|
};
|
|
43
42
|
exports.parseTypeScript = parseTypeScript;
|
|
44
|
-
const parseFlow = (source) => {
|
|
45
|
-
return recast.parse(source, { parser: flowParser });
|
|
46
|
-
};
|
|
47
|
-
exports.parseFlow = parseFlow;
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import * as recast from 'recast';
|
|
2
2
|
import type { CompoundComponentName } from './types';
|
|
3
3
|
export type InlineStyleProps = 'style' | 'sx';
|
|
4
|
-
export declare const INLINE_STYLE_PROPERTIES: string[];
|
|
5
|
-
export declare const ADDITIONAL_PROPERTIES: string[];
|
|
6
4
|
export type ViolatingAttribute = {
|
|
7
5
|
attributeName: string;
|
|
8
6
|
attributeValue: string | null;
|
|
@@ -33,15 +33,14 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.ADDITIONAL_PROPERTIES = exports.INLINE_STYLE_PROPERTIES = void 0;
|
|
37
36
|
const recast = __importStar(require("recast"));
|
|
38
37
|
const constants_1 = require("./constants");
|
|
39
38
|
const BLACKLIST_PROPERTIES = {
|
|
40
39
|
style: constants_1.RULESET_MAP,
|
|
41
40
|
sx: constants_1.SX_RULESET_MAP,
|
|
42
41
|
};
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
const INLINE_STYLE_PROPERTIES = ['style', 'sx'];
|
|
43
|
+
const ADDITIONAL_PROPERTIES = ['variant']; // Add any additional props you want to track
|
|
45
44
|
const addViolatingAttribute = (prop, styleObjName, componentName, loc, violatingAttributes) => {
|
|
46
45
|
if (prop.key.type !== 'Identifier') {
|
|
47
46
|
return;
|
|
@@ -82,7 +81,7 @@ const reportInlineStyle = (ast, attributes, componentName) => {
|
|
|
82
81
|
if (typeof attr.name.name !== 'string') {
|
|
83
82
|
return;
|
|
84
83
|
}
|
|
85
|
-
if (
|
|
84
|
+
if (ADDITIONAL_PROPERTIES.includes(attr.name.name)) {
|
|
86
85
|
// Handle expression container like `style={{ ... }}`
|
|
87
86
|
additionalProps.push({
|
|
88
87
|
propName: attr.name.name,
|
|
@@ -92,7 +91,7 @@ const reportInlineStyle = (ast, attributes, componentName) => {
|
|
|
92
91
|
if (((_a = attr.value) === null || _a === void 0 ? void 0 : _a.type) !== 'JSXExpressionContainer') {
|
|
93
92
|
return;
|
|
94
93
|
}
|
|
95
|
-
if (
|
|
94
|
+
if (INLINE_STYLE_PROPERTIES.includes(attr.name.name)) {
|
|
96
95
|
styleObjName = attr.name.name;
|
|
97
96
|
const { expression } = attr.value;
|
|
98
97
|
if (expression.type === 'ObjectExpression') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hero-design/snowflake-guard",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.8",
|
|
4
4
|
"description": "A hero-design bot detecting snowflake usage",
|
|
5
5
|
"author": "Hau Dao",
|
|
6
6
|
"license": "ISC",
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"start": "yarn build && probot run ./lib/src/index.js",
|
|
21
21
|
"type-check": "tsc --noEmit",
|
|
22
22
|
"test": "jest --passWithNoTests",
|
|
23
|
-
"
|
|
23
|
+
"test:ci": "jest --passWithNoTests",
|
|
24
|
+
"lint": "eslint src --quiet",
|
|
24
25
|
"deploy": "netlify deploy --site snowflake-guard.netlify.app --prod --auth $NETLIFY_AUTH_TOKEN",
|
|
25
26
|
"publish:npm": "yarn publish --access public"
|
|
26
27
|
},
|
|
@@ -35,24 +36,13 @@
|
|
|
35
36
|
"@eslint/eslintrc": "^3.1.0",
|
|
36
37
|
"@types/jest": "^29.0.0",
|
|
37
38
|
"@types/node": "^20.14.8",
|
|
38
|
-
"@typescript-eslint/eslint-plugin": "^5.12.1",
|
|
39
|
-
"@typescript-eslint/parser": "^5.12.1",
|
|
40
|
-
"config-tsconfig": "8.42.5",
|
|
41
39
|
"eslint": "^8.56.0",
|
|
42
|
-
"eslint-config-airbnb": "^19.0.4",
|
|
43
40
|
"eslint-config-hd": "8.42.5",
|
|
44
|
-
"eslint-config-prettier": "^8.5.0",
|
|
45
|
-
"eslint-import-resolver-typescript": "^3.5.2",
|
|
46
|
-
"eslint-plugin-import": "^2.32.0",
|
|
47
|
-
"eslint-plugin-jsx-a11y": "^6.5.1",
|
|
48
|
-
"eslint-plugin-prettier": "^4.0.0",
|
|
49
|
-
"eslint-plugin-react": "^7.37.5",
|
|
50
|
-
"eslint-plugin-react-hooks": "^4.3.0",
|
|
51
41
|
"jest": "^29.0.0",
|
|
52
42
|
"nock": "^13.0.5",
|
|
53
|
-
"prettier": "^2.5.1",
|
|
54
43
|
"prettier-config-hd": "8.42.4",
|
|
55
44
|
"smee-client": "^1.2.2",
|
|
45
|
+
"ts-config-hd": "8.42.5",
|
|
56
46
|
"ts-jest": "^29.0.0",
|
|
57
47
|
"typescript": "^5.7.3"
|
|
58
48
|
},
|