@hero-design/snowflake-guard 1.0.11 → 1.0.12

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.
@@ -1,97 +0,0 @@
1
- import {
2
- generateFetchReportQuery,
3
- generateCreateReportQuery,
4
- generateUpdateReportQuery,
5
- } from '../queryGenerators';
6
-
7
- const replaceWhiteSpace = (str: string) => str.replace(/\s/g, '');
8
-
9
- describe('generateFetchReportQuery', () => {
10
- it('returns correct query', () => {
11
- const query = generateFetchReportQuery({
12
- repoName: 'repoName',
13
- prNumber: 123,
14
- });
15
-
16
- expect(replaceWhiteSpace(query)).toBe(
17
- replaceWhiteSpace(`
18
- query {
19
- fetchHdSnowflakeGuardReport(repoName: "repoName", prNumber: 123) {
20
- id
21
- originalCount
22
- latestCount
23
- approvedCount
24
- }
25
- }
26
- `)
27
- );
28
- });
29
- });
30
-
31
- describe('generateCreateReportQuery', () => {
32
- it('returns correct query', () => {
33
- const query = generateCreateReportQuery({
34
- repoName: 'repoName',
35
- prNumber: 123,
36
- owner: 'owner',
37
- originalCount: 3,
38
- latestCount: 1,
39
- approvedCount: 2,
40
- });
41
-
42
- expect(replaceWhiteSpace(query)).toBe(
43
- replaceWhiteSpace(`
44
- mutation {
45
- createHdSnowflakeGuardReport(
46
- input: {
47
- params: {
48
- repoName: "repoName"
49
- prNumber: 123
50
- owner: "owner"
51
- originalCount: 3
52
- latestCount: 1
53
- approvedCount: 2
54
- }
55
- }
56
- ) {
57
- hdSnowflakeGuardReport {
58
- id
59
- repoName
60
- prNumber
61
- originalCount
62
- latestCount
63
- approvedCount
64
- }
65
- }
66
- }
67
- `)
68
- );
69
- });
70
- });
71
-
72
- describe('generateUpdateReportQuery', () => {
73
- it('returns correct query', () => {
74
- const query = generateUpdateReportQuery({
75
- id: 'id',
76
- latestCount: 1,
77
- approvedCount: 2,
78
- });
79
-
80
- expect(replaceWhiteSpace(query)).toBe(
81
- replaceWhiteSpace(`
82
- mutation {
83
- updateHdSnowflakeGuardReport(
84
- input: { id: "id", latestCount: 1, approvedCount: 2 }
85
- ) {
86
- hdSnowflakeGuardReport {
87
- id
88
- latestCount
89
- originalCount
90
- approvedCount
91
- }
92
- }
93
- }
94
- `)
95
- );
96
- });
97
- });
@@ -1,13 +0,0 @@
1
- const fetchGraphql = async (query: string) => {
2
- const host = process.env.DB_HOST || 'http://localhost:3000';
3
- return fetch(`${host}/graphql`, {
4
- method: 'POST',
5
- headers: {
6
- 'Content-Type': 'application/json',
7
- 'Snowflake-Guard-Auth': process.env.SNOWFLAKE_GUARD_SECRET || '',
8
- },
9
- body: JSON.stringify({ query }),
10
- }).then((response) => response.json());
11
- };
12
-
13
- export default fetchGraphql;
@@ -1,91 +0,0 @@
1
- const generateFetchReportQuery = ({
2
- repoName,
3
- prNumber,
4
- }: {
5
- repoName: string;
6
- prNumber: number;
7
- }) => {
8
- return `
9
- query {
10
- fetchHdSnowflakeGuardReport(repoName: "${repoName}", prNumber: ${prNumber}) {
11
- id
12
- originalCount
13
- latestCount
14
- approvedCount
15
- }
16
- }
17
- `;
18
- };
19
-
20
- const generateCreateReportQuery = ({
21
- repoName,
22
- prNumber,
23
- owner,
24
- originalCount,
25
- latestCount,
26
- approvedCount,
27
- }: {
28
- repoName: string;
29
- prNumber: number;
30
- owner: string;
31
- originalCount: number;
32
- latestCount: number;
33
- approvedCount: number;
34
- }) => {
35
- return `
36
- mutation {
37
- createHdSnowflakeGuardReport(
38
- input: {
39
- params: {
40
- repoName: "${repoName}"
41
- prNumber: ${prNumber}
42
- owner: "${owner}"
43
- originalCount: ${originalCount}
44
- latestCount: ${latestCount}
45
- approvedCount: ${approvedCount}
46
- }
47
- }
48
- ) {
49
- hdSnowflakeGuardReport {
50
- id
51
- repoName
52
- prNumber
53
- originalCount
54
- latestCount
55
- approvedCount
56
- }
57
- }
58
- }
59
- `;
60
- };
61
-
62
- const generateUpdateReportQuery = ({
63
- id,
64
- latestCount,
65
- approvedCount,
66
- }: {
67
- id: string;
68
- latestCount: number;
69
- approvedCount: number;
70
- }) => {
71
- return `
72
- mutation {
73
- updateHdSnowflakeGuardReport(
74
- input: { id: "${id}", latestCount: ${latestCount}, approvedCount: ${approvedCount} }
75
- ) {
76
- hdSnowflakeGuardReport {
77
- id
78
- latestCount
79
- originalCount
80
- approvedCount
81
- }
82
- }
83
- }
84
- `;
85
- };
86
-
87
- export {
88
- generateFetchReportQuery,
89
- generateCreateReportQuery,
90
- generateUpdateReportQuery,
91
- };
@@ -1,19 +0,0 @@
1
- export type Report = {
2
- id: string;
3
- repoName: string;
4
- prNumber: number;
5
- owner: string;
6
- originalCount: number;
7
- latestCount: number;
8
- approvedCount: number;
9
- };
10
-
11
- export type FetchReportResponse = {
12
- errors: { message: string }[];
13
- data: null | {
14
- fetchHdSnowflakeGuardReport: Pick<
15
- Report,
16
- 'id' | 'originalCount' | 'latestCount' | 'approvedCount'
17
- >;
18
- };
19
- };
package/src/index.ts DELETED
@@ -1,208 +0,0 @@
1
- import { Probot } from 'probot';
2
- import parseSource from './parseSource';
3
- import { APPROVED_CLASSNAME_COMMENT } from './reports/constants';
4
- import fetchGraphql from './graphql/fetchGraphql';
5
- import {
6
- generateFetchReportQuery,
7
- generateUpdateReportQuery,
8
- generateCreateReportQuery,
9
- } from './graphql/queryGenerators';
10
- import type { FetchReportResponse } from './graphql/types';
11
- import { getDiffLocs } from './utils/getDiffLocs';
12
- import type { DiffLocs } from './utils/getDiffLocs';
13
-
14
- type Comment = {
15
- path: string;
16
- body: string;
17
- line: number;
18
- };
19
-
20
- const TSX_REGEX = /\.tsx$/;
21
- const TEST_REGEX = /__tests__/;
22
-
23
- const SNOWFLAKE_COMMENTS = {
24
- style:
25
- 'Snowflake detected! A component is customized using inline styles. Make sure to not use [prohibited CSS properties](https://docs.google.com/spreadsheets/d/1Dj8vqLdFaf-CSaSVoYqyYZIkGqF6OoyP7K4G1_9L62U/edit?usp=sharing).',
26
- sx: 'Snowflake detected! A component is customized via sx prop. Make sure to not use [prohibited CSS properties](https://docs.google.com/spreadsheets/d/1Dj8vqLdFaf-CSaSVoYqyYZIkGqF6OoyP7K4G1_9L62U/edit?usp=sharing).',
27
- 'styled-component':
28
- 'Please do not use styled-component to customize this component, use sx prop or inline style instead.',
29
- className: `Please make sure that this className is not used as a CSS classname for component customization purposes, use sx prop or inline style instead. In case this is none-css classname, please flag it with this comment \`${APPROVED_CLASSNAME_COMMENT}\`.`,
30
- };
31
-
32
- const checkIfDetectedSnowflakesInDiff = (
33
- diffLocs: DiffLocs,
34
- locToComment: number
35
- ) => {
36
- const locIdx = diffLocs.findIndex(([start, end]) => {
37
- return locToComment >= start && locToComment <= end;
38
- });
39
-
40
- return locIdx !== -1;
41
- };
42
-
43
- export = (app: Probot) => {
44
- app.on(
45
- ['pull_request.opened', 'pull_request.synchronize'],
46
- async (context) => {
47
- // Get PR info
48
- const prNumber = context.payload.number;
49
- const repoInfo = {
50
- repo: context.payload.repository.name,
51
- owner: context.payload.repository.owner.login,
52
- };
53
- const prBranch = context.payload.pull_request.head.ref;
54
-
55
- // List all changed files
56
- const prFiles = await context.octokit.pulls.listFiles({
57
- ...repoInfo,
58
- pull_number: prNumber,
59
- });
60
- const tsxFiles = prFiles.data.filter(
61
- (file) =>
62
- TSX_REGEX.test(file.filename) &&
63
- !TEST_REGEX.test(file.filename) &&
64
- file.status !== 'removed'
65
- );
66
-
67
- // Saving file patches to get diff locations
68
- const prFilePatches = tsxFiles.reduce((acc, file) => {
69
- acc[file.filename] = file.patch || '';
70
- return acc;
71
- }, {} as { [key: string]: string });
72
-
73
- // Get file contents
74
- const prFileContentPromises = tsxFiles.map((file) =>
75
- context.octokit.repos.getContent({
76
- ...repoInfo,
77
- path: file.filename,
78
- ref: prBranch,
79
- })
80
- );
81
- const prFileContents = await Promise.all(prFileContentPromises);
82
-
83
- const snowflakeComments: Comment[] = [];
84
- const approvedSnowflakeLocs: number[] = [];
85
-
86
- prFileContents.forEach(async (file) => {
87
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
88
- // @ts-ignore
89
- const filePath = file.data.path;
90
- const diffLocs = getDiffLocs(prFilePatches[filePath]);
91
-
92
- const stringContent = Buffer.from(
93
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
94
- // @ts-ignore
95
- file.data.content,
96
- 'base64'
97
- ).toString();
98
-
99
- // Parse file content to check for snowflakes
100
- const snowflakeReport = parseSource(stringContent);
101
-
102
- snowflakeReport.styleLocs.forEach((loc) => {
103
- if (checkIfDetectedSnowflakesInDiff(diffLocs, loc)) {
104
- snowflakeComments.push({
105
- path: filePath,
106
- body: SNOWFLAKE_COMMENTS['style'],
107
- line: loc,
108
- });
109
- }
110
- });
111
-
112
- snowflakeReport.sxLocs.forEach((loc) => {
113
- if (checkIfDetectedSnowflakesInDiff(diffLocs, loc)) {
114
- snowflakeComments.push({
115
- path: filePath,
116
- body: SNOWFLAKE_COMMENTS['sx'],
117
- line: loc,
118
- });
119
- }
120
- });
121
-
122
- snowflakeReport.styledComponentLocs.forEach((loc) => {
123
- if (checkIfDetectedSnowflakesInDiff(diffLocs, loc)) {
124
- snowflakeComments.push({
125
- path: filePath,
126
- body: SNOWFLAKE_COMMENTS['styled-component'],
127
- line: loc,
128
- });
129
- }
130
- });
131
-
132
- snowflakeReport.classNameLocs.forEach((loc) => {
133
- if (checkIfDetectedSnowflakesInDiff(diffLocs, loc)) {
134
- snowflakeComments.push({
135
- path: filePath,
136
- body: SNOWFLAKE_COMMENTS['className'],
137
- line: loc,
138
- });
139
- }
140
- });
141
-
142
- snowflakeReport.approvedLocs.forEach((loc) => {
143
- if (checkIfDetectedSnowflakesInDiff(diffLocs, loc)) {
144
- approvedSnowflakeLocs.push(loc);
145
- }
146
- });
147
- });
148
-
149
- // Saving report
150
- const snowflakeCount = snowflakeComments.length;
151
- const report = (await fetchGraphql(
152
- generateFetchReportQuery({ repoName: repoInfo.repo, prNumber })
153
- )) as FetchReportResponse;
154
- const reportData = report.data?.fetchHdSnowflakeGuardReport;
155
- if (reportData) {
156
- await fetchGraphql(
157
- generateUpdateReportQuery({
158
- id: reportData.id,
159
- latestCount: snowflakeCount,
160
- approvedCount: approvedSnowflakeLocs.length,
161
- })
162
- );
163
- } else {
164
- await fetchGraphql(
165
- generateCreateReportQuery({
166
- repoName: repoInfo.repo,
167
- prNumber,
168
- owner: repoInfo.owner,
169
- originalCount: snowflakeCount,
170
- latestCount: snowflakeCount,
171
- approvedCount: approvedSnowflakeLocs.length,
172
- })
173
- );
174
- }
175
-
176
- // No snowflakes detected
177
- // Create success check-run
178
- if (snowflakeCount === 0) {
179
- return context.octokit.checks.create({
180
- ...repoInfo,
181
- name: 'SnowflakeGuard/Check',
182
- head_sha: context.payload.pull_request.head.sha,
183
- status: 'completed',
184
- conclusion: 'success',
185
- });
186
- }
187
-
188
- // Snowflakes detected
189
- // Create failed check-run & comment
190
- await context.octokit.checks.create({
191
- ...repoInfo,
192
- name: 'SnowflakeGuard/Check',
193
- head_sha: context.payload.pull_request.head.sha,
194
- status: 'completed',
195
- conclusion: 'failure',
196
- });
197
-
198
- return context.octokit.pulls.createReview({
199
- ...repoInfo,
200
- pull_number: prNumber,
201
- commit_id: context.payload.pull_request.head.sha,
202
- event: 'COMMENT',
203
- body: 'Snowflake Guard Bot has detected some snowflakes in this PR. Please review the following comments.',
204
- comments: snowflakeComments,
205
- });
206
- }
207
- );
208
- };
@@ -1,115 +0,0 @@
1
- import * as recast from 'recast';
2
- import * as tsParser from './parsers/typescript';
3
- import reportCustomProperties from './reports/reportCustomStyleProperties';
4
- import reportStyledComponents from './reports/reportStyledComponents';
5
- import {
6
- HD_COMPONENTS,
7
- APPROVED_COMMENT,
8
- APPROVED_CLASSNAME_COMMENT,
9
- } from './reports/constants';
10
- import type { ComponentName } from './reports/types';
11
-
12
- const parseSource = (source: string) => {
13
- let hasHeroDesignImport = false;
14
- let hasStyledComponentsImport = false;
15
-
16
- const componentList: { [k: string]: ComponentName } = {};
17
- let styledAliasName = 'styled';
18
-
19
- let styledComponentLocs: number[] = [];
20
- let classNameLocs: number[] = [];
21
- let styleLocs: number[] = [];
22
- let sxLocs: number[] = [];
23
- const approvedCmtLocs: number[] = [];
24
- const approvedClassnameLocs: number[] = [];
25
-
26
- const ast = recast.parse(source, { parser: tsParser });
27
-
28
- recast.visit(ast, {
29
- visitImportDeclaration(path) {
30
- this.traverse(path);
31
-
32
- const importedFrom = path.value.source.value as string;
33
-
34
- // Check if file imports components from '@hero-design/react'
35
- if (importedFrom === '@hero-design/react') {
36
- recast.visit(path.node, {
37
- visitImportSpecifier(importPath) {
38
- this.traverse(importPath);
39
-
40
- if (HD_COMPONENTS.includes(importPath.value.imported.name)) {
41
- componentList[importPath.value.local.name] =
42
- importPath.value.imported.name;
43
- hasHeroDesignImport = true;
44
- }
45
- },
46
- });
47
- }
48
-
49
- // Check if file imports from 'styled-components'
50
- if (importedFrom === 'styled-components') {
51
- recast.visit(path.node, {
52
- visitImportDefaultSpecifier(importPath) {
53
- this.traverse(importPath);
54
-
55
- styledAliasName = importPath.value.local.name;
56
- hasStyledComponentsImport = true;
57
- },
58
- });
59
- }
60
- },
61
-
62
- visitComment(path) {
63
- this.traverse(path);
64
-
65
- const comment = path.value.value as string;
66
- if (comment.toLowerCase().includes(APPROVED_COMMENT.toLowerCase())) {
67
- approvedCmtLocs.push(path.value.loc.start.line);
68
- }
69
-
70
- if (
71
- comment.toLowerCase().includes(APPROVED_CLASSNAME_COMMENT.toLowerCase())
72
- ) {
73
- approvedClassnameLocs.push(path.value.loc.start.line);
74
- }
75
- },
76
- });
77
-
78
- const isNotApprovedSnowflakes = (loc: number) =>
79
- !approvedCmtLocs.includes(loc);
80
-
81
- const isNotApprovedClassnameSnowflakes = (loc: number) =>
82
- !approvedClassnameLocs.includes(loc);
83
-
84
- if (hasHeroDesignImport) {
85
- // Case 1: Using className to customise components
86
- // Case 2: Using style object to customise components
87
- // Case 3: Using sx object to customise components
88
- const customPropLocs = reportCustomProperties(ast, componentList);
89
- classNameLocs = customPropLocs.className.filter(
90
- (loc) =>
91
- isNotApprovedSnowflakes(loc) && isNotApprovedClassnameSnowflakes(loc)
92
- );
93
- styleLocs = customPropLocs.style.filter(isNotApprovedSnowflakes);
94
- sxLocs = customPropLocs.sx.filter(isNotApprovedSnowflakes);
95
-
96
- // Case 4: Using styled-components to customise components
97
- if (hasStyledComponentsImport) {
98
- styledComponentLocs = reportStyledComponents(
99
- ast,
100
- componentList,
101
- styledAliasName
102
- ).filter(isNotApprovedSnowflakes);
103
- }
104
- }
105
-
106
- return {
107
- classNameLocs,
108
- styleLocs,
109
- sxLocs,
110
- styledComponentLocs,
111
- approvedLocs: [...approvedCmtLocs, ...approvedClassnameLocs],
112
- };
113
- };
114
-
115
- export default parseSource;
@@ -1,8 +0,0 @@
1
- import * as babelParser from '@babel/parser';
2
- import getBabelOptions, { Overrides } from 'recast/parsers/_babel_options';
3
-
4
- export const parse = (source: string, options?: Overrides) => {
5
- const babelOptions = getBabelOptions(options);
6
- babelOptions.plugins.push('jsx', 'typescript');
7
- return babelParser.parse(source, babelOptions);
8
- };