@hero-design/snowflake-guard 1.0.7-alpha0

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.
Files changed (49) hide show
  1. package/.dockerignore +12 -0
  2. package/.env.example +9 -0
  3. package/.eslintrc.js +8 -0
  4. package/CHANGELOG.md +47 -0
  5. package/Dockerfile +8 -0
  6. package/LICENSE +15 -0
  7. package/README.md +33 -0
  8. package/app.yml +137 -0
  9. package/jest.config.js +9 -0
  10. package/lib/netlify/functions/snowflake.d.ts +2 -0
  11. package/lib/netlify/functions/snowflake.js +10 -0
  12. package/lib/src/__mocks__/sourceSample.d.ts +2 -0
  13. package/lib/src/__mocks__/sourceSample.js +27 -0
  14. package/lib/src/__tests__/parseSource.spec.d.ts +1 -0
  15. package/lib/src/__tests__/parseSource.spec.js +41 -0
  16. package/lib/src/index.d.ts +3 -0
  17. package/lib/src/index.js +142 -0
  18. package/lib/src/parseSource.d.ts +7 -0
  19. package/lib/src/parseSource.js +102 -0
  20. package/lib/src/parsers/typescript.d.ts +3 -0
  21. package/lib/src/parsers/typescript.js +37 -0
  22. package/lib/src/reports/constants.d.ts +215 -0
  23. package/lib/src/reports/constants.js +848 -0
  24. package/lib/src/reports/reportClassName.d.ts +3 -0
  25. package/lib/src/reports/reportClassName.js +15 -0
  26. package/lib/src/reports/reportCustomStyleProperties.d.ts +10 -0
  27. package/lib/src/reports/reportCustomStyleProperties.js +109 -0
  28. package/lib/src/reports/reportInlineStyle.d.ts +7 -0
  29. package/lib/src/reports/reportInlineStyle.js +179 -0
  30. package/lib/src/reports/reportStyledComponents.d.ts +6 -0
  31. package/lib/src/reports/reportStyledComponents.js +95 -0
  32. package/lib/src/reports/types.d.ts +3 -0
  33. package/lib/src/reports/types.js +2 -0
  34. package/lib/src/test.tsx +123 -0
  35. package/netlify/functions/snowflake.ts +9 -0
  36. package/netlify.toml +21 -0
  37. package/package.json +44 -0
  38. package/src/__mocks__/sourceSample.tsx +67 -0
  39. package/src/__tests__/parseSource.spec.ts +15 -0
  40. package/src/index.ts +201 -0
  41. package/src/parseSource.ts +97 -0
  42. package/src/parsers/typescript.ts +8 -0
  43. package/src/reports/constants.ts +965 -0
  44. package/src/reports/reportClassName.ts +20 -0
  45. package/src/reports/reportCustomStyleProperties.ts +125 -0
  46. package/src/reports/reportInlineStyle.ts +221 -0
  47. package/src/reports/reportStyledComponents.ts +109 -0
  48. package/src/reports/types.ts +5 -0
  49. package/tsconfig.json +15 -0
@@ -0,0 +1,20 @@
1
+ import { types } from 'recast';
2
+
3
+ const reportClassName = (attributes: types.namedTypes.JSXAttribute[]) => {
4
+ const locs: number[] = [];
5
+
6
+ attributes.forEach((attr) => {
7
+ if (
8
+ attr.type === 'JSXAttribute' &&
9
+ attr.name.type === 'JSXIdentifier' &&
10
+ attr.name.name === 'className' &&
11
+ attr.loc
12
+ ) {
13
+ locs.push(attr.loc.start.line);
14
+ }
15
+ });
16
+
17
+ return locs;
18
+ };
19
+
20
+ export default reportClassName;
@@ -0,0 +1,125 @@
1
+ import * as recast from 'recast';
2
+ import reportClassName from './reportClassName';
3
+ import reportInlineStyle from './reportInlineStyle';
4
+ import type { ComponentName, CompoundComponentName } from './types';
5
+
6
+ const reportCustomProperties = (
7
+ ast: recast.types.ASTNode,
8
+ componentList: { [k: string]: ComponentName }
9
+ ) => {
10
+ const locs: {
11
+ className: number[];
12
+ style: number[];
13
+ sx: number[];
14
+ } = {
15
+ className: [],
16
+ style: [],
17
+ sx: [],
18
+ };
19
+ const localComponentList = Object.keys(componentList);
20
+
21
+ recast.visit(ast, {
22
+ visitJSXOpeningElement(path) {
23
+ this.traverse(path);
24
+
25
+ // Case 1: Custom default component, e.g. <Card />
26
+ if (
27
+ path.value.name.type === 'JSXIdentifier' &&
28
+ localComponentList.includes(path.value.name.name)
29
+ ) {
30
+ const attributes = path.value
31
+ .attributes as recast.types.namedTypes.JSXAttribute[];
32
+ const styleObjectLocs = reportInlineStyle(
33
+ ast,
34
+ attributes,
35
+ componentList[path.value.name.name] as CompoundComponentName
36
+ );
37
+
38
+ locs.className = [...locs.className, ...reportClassName(attributes)];
39
+ locs.style = [...locs.style, ...styleObjectLocs.style];
40
+ locs.sx = [...locs.sx, ...styleObjectLocs.sx];
41
+ }
42
+
43
+ // Case 2: Custom compound component, e.g. <Card.Header />
44
+ if (
45
+ path.value.name.type === 'JSXMemberExpression' &&
46
+ localComponentList.includes(path.value.name.object.name)
47
+ ) {
48
+ const compoundComponentName = [
49
+ componentList[path.value.name.object.name],
50
+ path.value.name.property.name,
51
+ ].join('.') as CompoundComponentName;
52
+ const attributes = path.value
53
+ .attributes as recast.types.namedTypes.JSXAttribute[];
54
+ const styleObjectLocs = reportInlineStyle(
55
+ ast,
56
+ attributes,
57
+ compoundComponentName
58
+ );
59
+
60
+ locs.className = [...locs.className, ...reportClassName(attributes)];
61
+ locs.style = [...locs.style, ...styleObjectLocs.style];
62
+ locs.sx = [...locs.sx, ...styleObjectLocs.sx];
63
+ }
64
+ },
65
+
66
+ // Case 3: Custom compound component with spead operator. e.g. const { Header } = Card, then <Header />
67
+ visitVariableDeclaration(path) {
68
+ this.traverse(path);
69
+
70
+ const declaration = path.value
71
+ .declarations[0] as recast.types.namedTypes.VariableDeclarator;
72
+ if (
73
+ declaration.init &&
74
+ declaration.init.type === 'Identifier' &&
75
+ localComponentList.includes(declaration.init.name)
76
+ ) {
77
+ const componentName = declaration.init.name;
78
+ const { id } = declaration;
79
+
80
+ if (id.type === 'ObjectPattern') {
81
+ const compoundComponentNames = id.properties
82
+ .map((prop) =>
83
+ prop.type === 'ObjectProperty' && prop.key.type === 'Identifier'
84
+ ? prop.key.name
85
+ : null
86
+ )
87
+ .filter((name): name is string => name !== null);
88
+
89
+ recast.visit(ast, {
90
+ visitJSXOpeningElement(openPath) {
91
+ this.traverse(openPath);
92
+
93
+ if (
94
+ openPath.value.name.type === 'JSXIdentifier' &&
95
+ compoundComponentNames.includes(openPath.value.name.name)
96
+ ) {
97
+ const compoundComponentName = [
98
+ componentList[componentName],
99
+ openPath.value.name.name,
100
+ ].join('.') as CompoundComponentName;
101
+ const { attributes } = openPath.value;
102
+ const styleObjectLocs = reportInlineStyle(
103
+ ast,
104
+ attributes,
105
+ compoundComponentName
106
+ );
107
+
108
+ locs.className = [
109
+ ...locs.className,
110
+ ...reportClassName(attributes),
111
+ ];
112
+ locs.style = [...locs.style, ...styleObjectLocs.style];
113
+ locs.sx = [...locs.sx, ...styleObjectLocs.sx];
114
+ }
115
+ },
116
+ });
117
+ }
118
+ }
119
+ },
120
+ });
121
+
122
+ return locs;
123
+ };
124
+
125
+ export default reportCustomProperties;
@@ -0,0 +1,221 @@
1
+ import * as recast from 'recast';
2
+ import { RULESET_MAP, SX_RULESET_MAP } from './constants';
3
+ import type { CompoundComponentName } from './types';
4
+
5
+ type InlineStyleProps = 'style' | 'sx';
6
+
7
+ const BLACKLIST_PROPERTIES = {
8
+ style: RULESET_MAP,
9
+ sx: SX_RULESET_MAP,
10
+ };
11
+
12
+ const INLINE_STYLE_PROPERTIES = ['style', 'sx'];
13
+
14
+ const reportInlineStyle = (
15
+ ast: recast.types.ASTNode,
16
+ attributes: recast.types.namedTypes.JSXAttribute[],
17
+ componentName: CompoundComponentName
18
+ ) => {
19
+ const locs: { style: number[]; sx: number[] } = {
20
+ style: [],
21
+ sx: [],
22
+ };
23
+
24
+ let hasCustomStyle = false;
25
+ let styleObjName: InlineStyleProps;
26
+
27
+ attributes.forEach((attr) => {
28
+ if (
29
+ attr.type === 'JSXAttribute' &&
30
+ typeof attr.name.name === 'string' &&
31
+ INLINE_STYLE_PROPERTIES.includes(attr.name.name) &&
32
+ attr.value?.type === 'JSXExpressionContainer'
33
+ ) {
34
+ styleObjName = attr.name.name as InlineStyleProps;
35
+
36
+ const { expression } = attr.value;
37
+
38
+ if (expression.type === 'ObjectExpression') {
39
+ expression.properties.forEach((prop) => {
40
+ // Case 1: Use direct object, e.g. <Card style={{ color: 'red' }} />
41
+ if (
42
+ prop.type === 'ObjectProperty' &&
43
+ prop.key.type === 'Identifier' &&
44
+ BLACKLIST_PROPERTIES[styleObjName][componentName].includes(
45
+ prop.key.name
46
+ )
47
+ ) {
48
+ hasCustomStyle = true;
49
+ }
50
+
51
+ // Case 2: Use spread operator, e.g. <Card style={{ ...customStyle }} />
52
+ if (
53
+ prop.type === 'SpreadElement' &&
54
+ prop.argument.type === 'Identifier'
55
+ ) {
56
+ const variableName = prop.argument.name;
57
+
58
+ recast.visit(ast, {
59
+ visitVariableDeclaration(path) {
60
+ this.traverse(path);
61
+
62
+ const declaration = path.value
63
+ .declarations[0] as recast.types.namedTypes.VariableDeclarator;
64
+
65
+ if (
66
+ declaration.id.type === 'Identifier' &&
67
+ declaration.id.name === variableName &&
68
+ declaration.init?.type === 'ObjectExpression'
69
+ ) {
70
+ declaration.init.properties.forEach((deProp) => {
71
+ if (
72
+ deProp.type === 'ObjectProperty' &&
73
+ deProp.key.type === 'Identifier' &&
74
+ BLACKLIST_PROPERTIES[styleObjName][
75
+ componentName
76
+ ].includes(deProp.key.name)
77
+ ) {
78
+ hasCustomStyle = true;
79
+ }
80
+ });
81
+ }
82
+ },
83
+ });
84
+ }
85
+
86
+ // Case 3: Use spread operator with variable's property, e.g. <Card style={{ ...customStyle.tileCard }} />
87
+ if (
88
+ prop.type === 'SpreadElement' &&
89
+ prop.argument.type === 'MemberExpression' &&
90
+ prop.argument.object.type === 'Identifier' &&
91
+ prop.argument.property.type === 'Identifier'
92
+ ) {
93
+ const variableName = prop.argument.object.name;
94
+ const propName = prop.argument.property.name;
95
+
96
+ recast.visit(ast, {
97
+ visitVariableDeclaration(path) {
98
+ this.traverse(path);
99
+
100
+ const declaration = path.value
101
+ .declarations[0] as recast.types.namedTypes.VariableDeclarator;
102
+
103
+ if (
104
+ declaration.id.type === 'Identifier' &&
105
+ declaration.id.name === variableName &&
106
+ declaration.init?.type === 'ObjectExpression'
107
+ ) {
108
+ declaration.init.properties.forEach((deProp) => {
109
+ if (
110
+ deProp.type === 'ObjectProperty' &&
111
+ deProp.key.type === 'Identifier' &&
112
+ deProp.key.name === propName &&
113
+ deProp.value.type === 'ObjectExpression'
114
+ ) {
115
+ deProp.value.properties.forEach((p) => {
116
+ if (
117
+ p.type === 'ObjectProperty' &&
118
+ p.key.type === 'Identifier' &&
119
+ BLACKLIST_PROPERTIES[styleObjName][
120
+ componentName
121
+ ].includes(p.key.name)
122
+ ) {
123
+ hasCustomStyle = true;
124
+ }
125
+ });
126
+ }
127
+ });
128
+ }
129
+ },
130
+ });
131
+ }
132
+ });
133
+ }
134
+
135
+ // Case 4: Use variable, e.g. <Card style={customStyle} />
136
+ if (expression.type === 'Identifier') {
137
+ const variableName = expression.name;
138
+ recast.visit(ast, {
139
+ visitVariableDeclaration(path) {
140
+ this.traverse(path);
141
+
142
+ const declaration = path.value
143
+ .declarations[0] as recast.types.namedTypes.VariableDeclarator;
144
+
145
+ if (
146
+ declaration.id.type === 'Identifier' &&
147
+ declaration.id.name === variableName &&
148
+ declaration.init?.type === 'ObjectExpression'
149
+ ) {
150
+ declaration.init.properties.forEach((prop) => {
151
+ if (
152
+ prop.type === 'ObjectProperty' &&
153
+ prop.key.type === 'Identifier' &&
154
+ BLACKLIST_PROPERTIES[styleObjName][componentName].includes(
155
+ prop.key.name
156
+ )
157
+ ) {
158
+ hasCustomStyle = true;
159
+ }
160
+ });
161
+ }
162
+ },
163
+ });
164
+ }
165
+
166
+ // Case 5: Use variable's property, e.g. <Card style={customStyle.tileCard} />
167
+ if (
168
+ expression.type === 'MemberExpression' &&
169
+ expression.object.type === 'Identifier' &&
170
+ expression.property.type === 'Identifier'
171
+ ) {
172
+ const objectName = expression.object.name;
173
+ const propName = expression.property.name;
174
+
175
+ recast.visit(ast, {
176
+ visitVariableDeclaration(path) {
177
+ this.traverse(path);
178
+
179
+ const declaration = path.value
180
+ .declarations[0] as recast.types.namedTypes.VariableDeclarator;
181
+
182
+ if (
183
+ declaration.id.type === 'Identifier' &&
184
+ declaration.id.name === objectName &&
185
+ declaration.init?.type === 'ObjectExpression'
186
+ ) {
187
+ declaration.init.properties.forEach((prop) => {
188
+ if (
189
+ prop.type === 'ObjectProperty' &&
190
+ prop.key.type === 'Identifier' &&
191
+ prop.key.name === propName &&
192
+ prop.value.type === 'ObjectExpression'
193
+ ) {
194
+ prop.value.properties.forEach((p) => {
195
+ if (
196
+ p.type === 'ObjectProperty' &&
197
+ p.key.type === 'Identifier' &&
198
+ BLACKLIST_PROPERTIES[styleObjName][
199
+ componentName
200
+ ].includes(p.key.name)
201
+ ) {
202
+ hasCustomStyle = true;
203
+ }
204
+ });
205
+ }
206
+ });
207
+ }
208
+ },
209
+ });
210
+ }
211
+
212
+ if (hasCustomStyle && attr.loc) {
213
+ locs[styleObjName].push(attr.loc.start.line);
214
+ }
215
+ }
216
+ });
217
+
218
+ return locs;
219
+ };
220
+
221
+ export default reportInlineStyle;
@@ -0,0 +1,109 @@
1
+ import * as recast from 'recast';
2
+ import type { ComponentName } from './types';
3
+
4
+ const reportStyledComponents = (
5
+ ast: recast.types.ASTNode,
6
+ componentList: { [k: string]: ComponentName },
7
+ styledAliasName: string
8
+ ) => {
9
+ const locs: number[] = [];
10
+ const localComponentList = Object.keys(componentList);
11
+
12
+ recast.visit(ast, {
13
+ visitVariableDeclaration(path) {
14
+ this.traverse(path);
15
+
16
+ const declaration = path.value
17
+ .declarations[0] as recast.types.namedTypes.VariableDeclarator;
18
+
19
+ if (
20
+ declaration.init &&
21
+ declaration.init.type === 'TaggedTemplateExpression'
22
+ ) {
23
+ const { tag } = declaration.init;
24
+
25
+ if (
26
+ tag.type === 'CallExpression' &&
27
+ tag.callee.type === 'Identifier' &&
28
+ tag.callee.name === styledAliasName
29
+ ) {
30
+ const arg = tag.arguments[0];
31
+
32
+ // Case 1: Custom default component, e.g. styled(Card)
33
+ if (
34
+ arg.type === 'Identifier' &&
35
+ localComponentList.includes(arg.name) &&
36
+ tag.loc
37
+ ) {
38
+ locs.push(tag.loc.start.line);
39
+ }
40
+
41
+ // Case 2: Custom compound component, e.g. styled(Card.Header)
42
+ if (
43
+ arg.type === 'MemberExpression' &&
44
+ arg.object.type === 'Identifier' &&
45
+ localComponentList.includes(arg.object.name) &&
46
+ tag.loc
47
+ ) {
48
+ locs.push(tag.loc.start.line);
49
+ }
50
+ }
51
+ }
52
+
53
+ // Case 3: Custom compound component with spead operator. e.g. const { Header } = Card, then styled(Header)
54
+ if (
55
+ declaration.init &&
56
+ declaration.init.type === 'Identifier' &&
57
+ localComponentList.includes(declaration.init.name)
58
+ ) {
59
+ const { id } = declaration;
60
+
61
+ if (id.type === 'ObjectPattern') {
62
+ const compoundComponentNames = id.properties
63
+ .map((prop) =>
64
+ prop.type === 'ObjectProperty' && prop.key.type === 'Identifier'
65
+ ? prop.key.name
66
+ : null
67
+ )
68
+ .filter((name): name is string => name !== null);
69
+
70
+ recast.visit(ast, {
71
+ visitVariableDeclaration(spreadPath) {
72
+ this.traverse(spreadPath);
73
+
74
+ const spreadDeclaration = spreadPath.value
75
+ .declarations[0] as recast.types.namedTypes.VariableDeclarator;
76
+
77
+ if (
78
+ spreadDeclaration.init &&
79
+ spreadDeclaration.init.type === 'TaggedTemplateExpression'
80
+ ) {
81
+ const { tag } = spreadDeclaration.init;
82
+
83
+ if (
84
+ tag.type === 'CallExpression' &&
85
+ tag.callee.type === 'Identifier' &&
86
+ tag.callee.name === styledAliasName
87
+ ) {
88
+ const arg = tag.arguments[0];
89
+
90
+ if (
91
+ arg.type === 'Identifier' &&
92
+ compoundComponentNames.includes(arg.name) &&
93
+ tag.loc
94
+ ) {
95
+ locs.push(tag.loc.start.line);
96
+ }
97
+ }
98
+ }
99
+ },
100
+ });
101
+ }
102
+ }
103
+ },
104
+ });
105
+
106
+ return locs;
107
+ };
108
+
109
+ export default reportStyledComponents;
@@ -0,0 +1,5 @@
1
+ import { HD_COMPONENTS, RULESET_MAP } from './constants';
2
+
3
+ export type ComponentName = typeof HD_COMPONENTS[number];
4
+
5
+ export type CompoundComponentName = keyof typeof RULESET_MAP;
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "extends": "config-tsconfig/base.json",
3
+ "compilerOptions": {
4
+ "module": "commonjs",
5
+ "outDir": "./lib",
6
+ "noImplicitReturns": true,
7
+ "pretty": false,
8
+ "skipLibCheck": true,
9
+ "typeRoots": ["./node_modules/@types/*"],
10
+ "types": ["jest"]
11
+ },
12
+ "include": ["src", "netlify"],
13
+ "exclude": ["node_modules", "src/__mocks__/**/*"],
14
+ "compileOnSave": false
15
+ }