@hero-design/snowflake-guard 1.4.7 → 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 CHANGED
@@ -1,16 +1,27 @@
1
1
  # @hero-design/snowflake-guard
2
2
 
3
- > A GitHub App built with [Probot](https://github.com/probot/probot) that analyzes PR changes and reports snowflake usage of Hero Design components.
3
+ ## Overview
4
4
 
5
- ## Setup
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
- 1. Copy `.env.example` to `.env` and asks Andromeda team for variable details.
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
- ## Note
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
- export type Report = {
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, ViolatingAttribute } from './reportInlineStyle';
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 } = (0, exports.getNonApprovedInlineLocs)(styleObjectLocs, violatingAttributes, commentList.styleCmts, path.value.loc.start.line);
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 } = (0, exports.getNonApprovedInlineLocs)(styleObjectLocs, violatingAttributes, commentList.styleCmts, path.value.loc.start.line);
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 } = (0, exports.getNonApprovedInlineLocs)(styleObjectLocs, violatingAttributes, commentList.styleCmts, openPath.value.loc.start.line);
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
- exports.INLINE_STYLE_PROPERTIES = [
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 (exports.INLINE_STYLE_PROPERTIES.includes(attr.name.name)) {
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;
@@ -1,2 +1 @@
1
1
  export declare const parseTypeScript: (source: string) => any;
2
- export declare const parseFlow: (source: string) => any;
@@ -33,15 +33,10 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.parseFlow = exports.parseTypeScript = void 0;
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
- exports.INLINE_STYLE_PROPERTIES = ['style', 'sx'];
44
- exports.ADDITIONAL_PROPERTIES = ['variant']; // Add any additional props you want to track
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 (exports.ADDITIONAL_PROPERTIES.includes(attr.name.name)) {
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 (exports.INLINE_STYLE_PROPERTIES.includes(attr.name.name)) {
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.7",
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,6 +20,7 @@
20
20
  "start": "yarn build && probot run ./lib/src/index.js",
21
21
  "type-check": "tsc --noEmit",
22
22
  "test": "jest --passWithNoTests",
23
+ "test:ci": "jest --passWithNoTests",
23
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"
@@ -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
  },