@nordcraft/search 1.0.66 → 1.0.68

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 (31) hide show
  1. package/dist/rules/issues/actions/actionRules.index.js +4 -0
  2. package/dist/rules/issues/actions/actionRules.index.js.map +1 -1
  3. package/dist/rules/issues/actions/unknownActionArgumentRule.js +37 -0
  4. package/dist/rules/issues/actions/unknownActionArgumentRule.js.map +1 -0
  5. package/dist/rules/issues/actions/unknownActionArgumentRule.test.js +306 -0
  6. package/dist/rules/issues/actions/unknownActionArgumentRule.test.js.map +1 -0
  7. package/dist/rules/issues/actions/unknownActionEventRule.js +26 -0
  8. package/dist/rules/issues/actions/unknownActionEventRule.js.map +1 -0
  9. package/dist/rules/issues/actions/unknownActionEventRule.test.js +281 -0
  10. package/dist/rules/issues/actions/unknownActionEventRule.test.js.map +1 -0
  11. package/dist/rules/issues/style/noReferenceGlobalCSSVariable.js +57 -0
  12. package/dist/rules/issues/style/noReferenceGlobalCSSVariable.js.map +1 -0
  13. package/dist/rules/issues/style/noReferenceGlobalCSSVariable.test.js +230 -0
  14. package/dist/rules/issues/style/noReferenceGlobalCSSVariable.test.js.map +1 -0
  15. package/dist/rules/issues/style/styleRules.index.js +6 -1
  16. package/dist/rules/issues/style/styleRules.index.js.map +1 -1
  17. package/dist/searchProject.js +65 -0
  18. package/dist/searchProject.js.map +1 -1
  19. package/dist/util/helpers.js.map +1 -1
  20. package/package.json +3 -3
  21. package/src/rules/issues/actions/actionRules.index.ts +4 -0
  22. package/src/rules/issues/actions/unknownActionArgumentRule.test.ts +319 -0
  23. package/src/rules/issues/actions/unknownActionArgumentRule.ts +51 -0
  24. package/src/rules/issues/actions/unknownActionEventRule.test.ts +295 -0
  25. package/src/rules/issues/actions/unknownActionEventRule.ts +35 -0
  26. package/src/rules/issues/style/noReferenceGlobalCSSVariable.test.ts +246 -0
  27. package/src/rules/issues/style/noReferenceGlobalCSSVariable.ts +78 -0
  28. package/src/rules/issues/style/styleRules.index.ts +6 -1
  29. package/src/searchProject.ts +68 -0
  30. package/src/types.d.ts +41 -0
  31. package/src/util/helpers.ts +2 -4
@@ -0,0 +1,295 @@
1
+ /* eslint-disable inclusive-language/use-inclusive-words */
2
+ import type { CustomActionModel } from '@nordcraft/core/dist/component/component.types'
3
+ import { describe, expect, test } from 'bun:test'
4
+ import { fixProject } from '../../../fixProject'
5
+ import { searchProject } from '../../../searchProject'
6
+ import { unknownActionEventRule } from './unknownActionEventRule'
7
+
8
+ describe('finds unknownActionEventRule', () => {
9
+ test('should find invalid action events', () => {
10
+ const problems = Array.from(
11
+ searchProject({
12
+ files: {
13
+ actions: {
14
+ 'legacy-action': {
15
+ name: 'legacy-action',
16
+ handler: '',
17
+ arguments: [],
18
+ events: {
19
+ success: { dummyEvent: 'hello' },
20
+ },
21
+ variableArguments: false,
22
+ },
23
+ 'modern-action': {
24
+ name: 'modern-action',
25
+ handler: '',
26
+ version: 2,
27
+ arguments: [],
28
+ events: {
29
+ success: { dummyEvent: 'hello' },
30
+ },
31
+ variableArguments: false,
32
+ },
33
+ },
34
+ components: {
35
+ test: {
36
+ name: 'test',
37
+ nodes: {},
38
+ formulas: {},
39
+ apis: {},
40
+ attributes: {},
41
+ variables: {},
42
+ onLoad: {
43
+ trigger: 'onLoad',
44
+ actions: [
45
+ {
46
+ name: 'legacy-action',
47
+ arguments: [],
48
+ events: {
49
+ nonExistingEvent: { actions: [] },
50
+ },
51
+ },
52
+ {
53
+ name: 'modern-action',
54
+ arguments: [],
55
+ events: {
56
+ nonExistingEvent: { actions: [] },
57
+ },
58
+ },
59
+ ],
60
+ },
61
+ },
62
+ },
63
+ },
64
+ rules: [unknownActionEventRule],
65
+ }),
66
+ )
67
+
68
+ expect(problems).toHaveLength(2)
69
+ expect(problems[0].code).toBe('unknown action event')
70
+ expect(problems[0].details).toEqual({ name: 'nonExistingEvent' })
71
+ expect(problems[0].path).toEqual([
72
+ 'components',
73
+ 'test',
74
+ 'onLoad',
75
+ 'actions',
76
+ '0',
77
+ 'events',
78
+ 'nonExistingEvent',
79
+ ])
80
+ expect(problems[1].code).toBe('unknown action event')
81
+ expect(problems[1].details).toEqual({ name: 'nonExistingEvent' })
82
+ expect(problems[1].path).toEqual([
83
+ 'components',
84
+ 'test',
85
+ 'onLoad',
86
+ 'actions',
87
+ '1',
88
+ 'events',
89
+ 'nonExistingEvent',
90
+ ])
91
+ })
92
+ test('should not find valid action events', () => {
93
+ const problems = Array.from(
94
+ searchProject({
95
+ files: {
96
+ actions: {
97
+ 'legacy-action': {
98
+ name: 'legacy-action',
99
+ handler: '',
100
+ arguments: [],
101
+ events: {
102
+ success: { dummyEvent: 'hello' },
103
+ },
104
+ variableArguments: false,
105
+ },
106
+ 'modern-action': {
107
+ name: 'modern-action',
108
+ handler: '',
109
+ version: 2,
110
+ arguments: [],
111
+ events: {
112
+ success: { dummyEvent: 'hello' },
113
+ },
114
+ variableArguments: false,
115
+ },
116
+ },
117
+ components: {
118
+ test: {
119
+ name: 'test',
120
+ nodes: {},
121
+ formulas: {},
122
+ apis: {},
123
+ attributes: {},
124
+ variables: {},
125
+ onLoad: {
126
+ trigger: 'onLoad',
127
+ actions: [
128
+ {
129
+ name: 'legacy-action',
130
+ arguments: [],
131
+ events: {
132
+ success: { actions: [] },
133
+ },
134
+ },
135
+ {
136
+ name: 'modern-action',
137
+ arguments: [],
138
+ events: {
139
+ success: { actions: [] },
140
+ },
141
+ },
142
+ ],
143
+ },
144
+ },
145
+ },
146
+ },
147
+ rules: [unknownActionEventRule],
148
+ }),
149
+ )
150
+
151
+ expect(problems).toHaveLength(0)
152
+ })
153
+ })
154
+
155
+ describe('fix unknownActionEventRule', () => {
156
+ test('should fix invalid action events', () => {
157
+ const fixedProject = fixProject({
158
+ files: {
159
+ actions: {
160
+ 'legacy-action': {
161
+ name: 'legacy-action',
162
+ handler: '',
163
+ arguments: [],
164
+ events: {
165
+ success: { dummyEvent: 'hello' },
166
+ },
167
+ variableArguments: false,
168
+ },
169
+ 'modern-action': {
170
+ name: 'modern-action',
171
+ handler: '',
172
+ version: 2,
173
+ arguments: [],
174
+ events: {
175
+ success: { dummyEvent: 'hello' },
176
+ },
177
+ variableArguments: false,
178
+ },
179
+ },
180
+ components: {
181
+ test: {
182
+ name: 'test',
183
+ nodes: {},
184
+ formulas: {},
185
+ apis: {},
186
+ attributes: {},
187
+ variables: {},
188
+ onLoad: {
189
+ trigger: 'onLoad',
190
+ actions: [
191
+ {
192
+ name: 'legacy-action',
193
+ arguments: [],
194
+ events: {
195
+ success: { actions: [] },
196
+ nonExistingEvent: { actions: [] },
197
+ },
198
+ },
199
+ {
200
+ name: 'modern-action',
201
+ arguments: [],
202
+ events: {
203
+ success: { actions: [] },
204
+ nonExistingEvent: { actions: [] },
205
+ },
206
+ },
207
+ ],
208
+ },
209
+ },
210
+ },
211
+ },
212
+ rule: unknownActionEventRule,
213
+ fixType: 'delete-unknown-action-event',
214
+ state: {},
215
+ })
216
+
217
+ expect(
218
+ (fixedProject.components['test']?.onLoad?.actions[0] as CustomActionModel)
219
+ .events,
220
+ ).toEqual({
221
+ success: {
222
+ actions: [],
223
+ },
224
+ })
225
+ expect(
226
+ (fixedProject.components['test']?.onLoad?.actions[1] as CustomActionModel)
227
+ .events,
228
+ ).toEqual({
229
+ success: {
230
+ actions: [],
231
+ },
232
+ })
233
+ })
234
+ test('should not remove valid action arguments', () => {
235
+ const files = {
236
+ actions: {
237
+ 'legacy-action': {
238
+ name: 'legacy-action',
239
+ handler: '',
240
+ arguments: [],
241
+ events: {
242
+ success: { dummyEvent: 'hello' },
243
+ },
244
+ variableArguments: false,
245
+ },
246
+ 'modern-action': {
247
+ name: 'modern-action',
248
+ handler: '',
249
+ version: 2,
250
+ arguments: [],
251
+ events: {
252
+ success: { dummyEvent: 'hello' },
253
+ },
254
+ variableArguments: false,
255
+ },
256
+ },
257
+ components: {
258
+ test: {
259
+ name: 'test',
260
+ nodes: {},
261
+ formulas: {},
262
+ apis: {},
263
+ attributes: {},
264
+ variables: {},
265
+ onLoad: {
266
+ trigger: 'onLoad',
267
+ actions: [
268
+ {
269
+ name: 'legacy-action',
270
+ arguments: [],
271
+ events: {
272
+ success: { actions: [] },
273
+ },
274
+ },
275
+ {
276
+ name: 'modern-action',
277
+ arguments: [],
278
+ events: {
279
+ success: { actions: [] },
280
+ },
281
+ },
282
+ ],
283
+ },
284
+ },
285
+ },
286
+ }
287
+ const fixedProject = fixProject({
288
+ files,
289
+ rule: unknownActionEventRule,
290
+ fixType: 'delete-unknown-action-argument',
291
+ state: {},
292
+ })
293
+ expect(fixedProject).toEqual(files)
294
+ })
295
+ })
@@ -0,0 +1,35 @@
1
+ import { isDefined } from '@nordcraft/core/dist/utils/util'
2
+ import type { Rule } from '../../../types'
3
+ import { removeFromPathFix } from '../../../util/removeUnused.fix'
4
+
5
+ export const unknownActionEventRule: Rule<{ name: string }> = {
6
+ code: 'unknown action event',
7
+ level: 'warning',
8
+ category: 'Unknown Reference',
9
+ visit: (report, { path, files, value, nodeType }) => {
10
+ if (nodeType !== 'action-custom-model-event') {
11
+ return
12
+ }
13
+ const { action, eventName } = value
14
+ const referencedAction = (
15
+ action.package ? files.packages?.[action.package]?.actions : files.actions
16
+ )?.[action.name]
17
+ if (!referencedAction) {
18
+ return
19
+ }
20
+ if (!isDefined(referencedAction.events?.[eventName])) {
21
+ report(
22
+ path,
23
+ {
24
+ name: eventName,
25
+ },
26
+ ['delete-unknown-action-event'],
27
+ )
28
+ }
29
+ },
30
+ fixes: {
31
+ 'delete-unknown-action-event': removeFromPathFix,
32
+ },
33
+ }
34
+
35
+ export type UnknownActionEventRuleFix = 'delete-unknown-action-event'
@@ -0,0 +1,246 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { searchProject } from '../../../searchProject'
3
+ import { noReferenceGlobalCSSVariableRule } from './noReferenceGlobalCSSVariable'
4
+
5
+ describe('noReferenceGlobalCSSVariableRule', () => {
6
+ test('should not report global CSS variables that are used', () => {
7
+ const problems = Array.from(
8
+ searchProject({
9
+ files: {
10
+ themes: {
11
+ Default: {
12
+ fonts: [],
13
+ propertyDefinitions: {
14
+ '--global-color': {
15
+ syntax: { type: 'primitive', name: 'color' },
16
+ description: 'A global color',
17
+ inherits: true,
18
+ initialValue: 'blue',
19
+ values: {},
20
+ },
21
+ '--local-margin': {
22
+ syntax: { type: 'primitive', name: 'length-percentage' },
23
+ description: 'A local margin',
24
+ inherits: true,
25
+ initialValue: '10px',
26
+ values: {},
27
+ },
28
+ },
29
+ },
30
+ },
31
+ formulas: {},
32
+ components: {
33
+ test: {
34
+ name: 'test',
35
+ nodes: {
36
+ root: {
37
+ tag: 'div',
38
+ type: 'element',
39
+ attrs: {},
40
+ style: {
41
+ color: 'var(--global-color)',
42
+ margin: 'var(--local-margin)',
43
+ },
44
+ events: {},
45
+ classes: {},
46
+ children: [],
47
+ },
48
+ },
49
+ formulas: {},
50
+ apis: {},
51
+ attributes: {},
52
+ variables: {},
53
+ },
54
+ },
55
+ },
56
+ rules: [noReferenceGlobalCSSVariableRule],
57
+ }),
58
+ )
59
+
60
+ expect(problems).toHaveLength(0)
61
+ })
62
+
63
+ test('should report global CSS variables that are never used', () => {
64
+ const problems = Array.from(
65
+ searchProject({
66
+ files: {
67
+ themes: {
68
+ Default: {
69
+ fonts: [],
70
+ propertyDefinitions: {
71
+ '--global-color': {
72
+ syntax: { type: 'primitive', name: 'color' },
73
+ description: 'A global color',
74
+ inherits: true,
75
+ initialValue: 'blue',
76
+ values: {},
77
+ },
78
+ },
79
+ },
80
+ },
81
+ formulas: {},
82
+ components: {
83
+ test: {
84
+ name: 'test',
85
+ nodes: {
86
+ root: {
87
+ tag: 'div',
88
+ type: 'element',
89
+ attrs: {},
90
+ style: {
91
+ color: 'red',
92
+ },
93
+ events: {},
94
+ classes: {},
95
+ children: [],
96
+ },
97
+ },
98
+ formulas: {},
99
+ apis: {},
100
+ attributes: {},
101
+ variables: {},
102
+ },
103
+ },
104
+ },
105
+ rules: [noReferenceGlobalCSSVariableRule],
106
+ }),
107
+ )
108
+
109
+ expect(problems).toHaveLength(1)
110
+ expect(problems[0].level).toBe('warning')
111
+ expect(problems[0].code).toBe('no-reference global css variable')
112
+ expect(problems[0].path).toEqual([
113
+ 'themes',
114
+ 'Default',
115
+ 'propertyDefinitions',
116
+ '--global-color',
117
+ ])
118
+ expect(problems[0].details).toEqual({ name: '--global-color' })
119
+ })
120
+
121
+ test('should handle complex use syntax: calc, with fallback value etc.', () => {
122
+ const problems = Array.from(
123
+ searchProject({
124
+ files: {
125
+ themes: {
126
+ Default: {
127
+ fonts: [],
128
+ propertyDefinitions: {
129
+ '--global-color': {
130
+ syntax: { type: 'primitive', name: 'color' },
131
+ description: 'A global color',
132
+ inherits: true,
133
+ initialValue: 'blue',
134
+ values: {},
135
+ },
136
+ '--another-global-color': {
137
+ syntax: { type: 'primitive', name: 'color' },
138
+ description: 'Another global color',
139
+ inherits: true,
140
+ initialValue: 'green',
141
+ values: {},
142
+ },
143
+ '--a-third-global-color': {
144
+ syntax: { type: 'primitive', name: 'color' },
145
+ description: 'A third global color',
146
+ inherits: true,
147
+ initialValue: 'yellow',
148
+ values: {},
149
+ },
150
+ },
151
+ },
152
+ },
153
+ formulas: {},
154
+ components: {
155
+ test: {
156
+ name: 'test',
157
+ nodes: {
158
+ root: {
159
+ tag: 'div',
160
+ type: 'element',
161
+ attrs: {},
162
+ style: {
163
+ color: 'calc(var(--global-color, red) + 10%)',
164
+ },
165
+ events: {},
166
+ classes: {},
167
+ children: [],
168
+ variants: [
169
+ {
170
+ breakpoint: 'medium',
171
+ style: {
172
+ backgroundColor:
173
+ 'color-mix(in srgb, plum, var(--a-third-global-color))',
174
+ },
175
+ },
176
+ ],
177
+ },
178
+ },
179
+ formulas: {},
180
+ apis: {},
181
+ attributes: {},
182
+ variables: {},
183
+ },
184
+ },
185
+ },
186
+ rules: [noReferenceGlobalCSSVariableRule],
187
+ }),
188
+ )
189
+
190
+ expect(problems).toHaveLength(1)
191
+ expect(problems[0].details).toEqual({ name: '--another-global-color' })
192
+ })
193
+
194
+ test('should not report when variable is used in other theme property definitions', () => {
195
+ const problems = Array.from(
196
+ searchProject({
197
+ files: {
198
+ themes: {
199
+ Default: {
200
+ fonts: [],
201
+ propertyDefinitions: {
202
+ '--global-color': {
203
+ syntax: { type: 'primitive', name: 'color' },
204
+ description: 'A global color',
205
+ inherits: true,
206
+ initialValue: 'blue',
207
+ values: {},
208
+ },
209
+ '--local-color': {
210
+ syntax: { type: 'primitive', name: 'color' },
211
+ description: 'A local color',
212
+ inherits: true,
213
+ initialValue: 'var(--global-color)',
214
+ values: {},
215
+ },
216
+ '--another-local-color': {
217
+ syntax: { type: 'primitive', name: 'color' },
218
+ description: 'Another local color',
219
+ inherits: true,
220
+ initialValue: 'green',
221
+ values: {},
222
+ },
223
+ '--a-third-local-color': {
224
+ syntax: { type: 'primitive', name: 'color' },
225
+ description: 'A third local color',
226
+ inherits: true,
227
+ initialValue: 'yellow',
228
+ values: {
229
+ Default: 'calc(var(--another-local-color, red) + 10%)',
230
+ },
231
+ },
232
+ },
233
+ },
234
+ },
235
+ formulas: {},
236
+ components: {},
237
+ },
238
+ rules: [noReferenceGlobalCSSVariableRule],
239
+ }),
240
+ )
241
+
242
+ expect(problems).toHaveLength(2)
243
+ expect(problems[0].details).toEqual({ name: '--local-color' })
244
+ expect(problems[1].details).toEqual({ name: '--a-third-local-color' })
245
+ })
246
+ })
@@ -0,0 +1,78 @@
1
+ import type { Rule } from '../../../types'
2
+
3
+ const REGEX = /var\(\s*(--[\w-]+)/g
4
+
5
+ export const noReferenceGlobalCSSVariableRule: Rule<{
6
+ name: string
7
+ }> = {
8
+ code: 'no-reference global css variable',
9
+ level: 'warning',
10
+ category: 'No References',
11
+ visit: (report, { path, value, nodeType, files, memo }) => {
12
+ if (nodeType !== 'project-theme-property') {
13
+ return
14
+ }
15
+
16
+ const theme = files.themes?.Default
17
+ if (!theme) {
18
+ return
19
+ }
20
+
21
+ const usedCSSVariablesInComponents = memo(
22
+ 'css-variables-used-in-components',
23
+ () => {
24
+ const vars = new Set<string>()
25
+ Object.entries(files.components).forEach(([_, component]) => {
26
+ Object.values(component?.nodes ?? {}).forEach((node) => {
27
+ if (node.type === 'element' || node.type === 'component') {
28
+ ;[{ style: node.style }, ...(node.variants ?? [])].forEach(
29
+ ({ style }) => {
30
+ Object.values(style ?? {}).forEach((styleValue) => {
31
+ if (typeof styleValue !== 'string') {
32
+ return
33
+ }
34
+ styleValue.matchAll(REGEX).forEach(([_, varName]) => {
35
+ vars.add(varName)
36
+ })
37
+ })
38
+ },
39
+ )
40
+ }
41
+ })
42
+ })
43
+
44
+ return vars
45
+ },
46
+ )
47
+
48
+ const usedCSSVariablesInCSSVariables = memo(
49
+ 'css-variables-used-in-css-variables',
50
+ () => {
51
+ const vars = new Set<string>()
52
+ Object.values(theme.propertyDefinitions ?? {}).forEach((propDef) => {
53
+ ;[...Object.values(propDef.values), propDef.initialValue].forEach(
54
+ (val) => {
55
+ if (typeof val !== 'string') {
56
+ return
57
+ }
58
+
59
+ val.matchAll(REGEX).forEach(([_, varName]) => {
60
+ vars.add(varName)
61
+ })
62
+ },
63
+ )
64
+ })
65
+
66
+ return vars
67
+ },
68
+ )
69
+
70
+ if (
71
+ usedCSSVariablesInCSSVariables.has(value.key) ||
72
+ usedCSSVariablesInComponents.has(value.key)
73
+ ) {
74
+ return
75
+ }
76
+ report(path, { name: value.key })
77
+ },
78
+ }
@@ -1,4 +1,9 @@
1
1
  import { invalidStyleSyntaxRule } from './invalidStyleSyntaxRule'
2
+ import { noReferenceGlobalCSSVariableRule } from './noReferenceGlobalCSSVariable'
2
3
  import { unknownClassnameRule } from './unknownClassnameRule'
3
4
 
4
- export default [invalidStyleSyntaxRule, unknownClassnameRule]
5
+ export default [
6
+ invalidStyleSyntaxRule,
7
+ unknownClassnameRule,
8
+ noReferenceGlobalCSSVariableRule,
9
+ ]