@nordcraft/search 1.0.65 → 1.0.67

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.
@@ -0,0 +1,319 @@
1
+ import type { CustomActionModel } from '@nordcraft/core/dist/component/component.types'
2
+ import { valueFormula } from '@nordcraft/core/dist/formula/formulaUtils'
3
+ import { describe, expect, test } from 'bun:test'
4
+ import { fixProject } from '../../../fixProject'
5
+ import { searchProject } from '../../../searchProject'
6
+ import { unknownActionArgumentRule } from './unknownActionArgumentRule'
7
+
8
+ describe('finds unknownActionArgumentRule', () => {
9
+ test('should find invalid action arguments', () => {
10
+ const problems = Array.from(
11
+ searchProject({
12
+ files: {
13
+ actions: {
14
+ 'legacy-action': {
15
+ name: 'legacy-action',
16
+ handler: '',
17
+ arguments: [
18
+ { name: 'arg1', formula: valueFormula(1) },
19
+ { name: 'arg2', formula: valueFormula(2) },
20
+ ],
21
+ variableArguments: false,
22
+ },
23
+ 'modern-action': {
24
+ name: 'modern-action',
25
+ handler: '',
26
+ version: 2,
27
+ arguments: [
28
+ { name: 'arg1', formula: valueFormula(1) },
29
+ { name: 'arg2', formula: valueFormula(2) },
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
+ { name: 'arg1', formula: valueFormula(7) },
49
+ { name: 'arg2', formula: valueFormula(9) },
50
+ { name: 'arg3', formula: valueFormula(13) },
51
+ ],
52
+ },
53
+ {
54
+ name: 'modern-action',
55
+ arguments: [
56
+ { name: 'arg1', formula: valueFormula(7) },
57
+ { name: 'arg2', formula: valueFormula(9) },
58
+ { name: 'arg3', formula: valueFormula(13) },
59
+ ],
60
+ },
61
+ ],
62
+ },
63
+ },
64
+ },
65
+ },
66
+ rules: [unknownActionArgumentRule],
67
+ }),
68
+ )
69
+
70
+ expect(problems).toHaveLength(2)
71
+ expect(problems[0].code).toBe('unknown action argument')
72
+ expect(problems[0].details).toEqual({ name: 'arg3' })
73
+ expect(problems[0].path).toEqual([
74
+ 'components',
75
+ 'test',
76
+ 'onLoad',
77
+ 'actions',
78
+ '0',
79
+ 'arguments',
80
+ 2,
81
+ ])
82
+ expect(problems[1].code).toBe('unknown action argument')
83
+ expect(problems[1].details).toEqual({ name: 'arg3' })
84
+ expect(problems[1].path).toEqual([
85
+ 'components',
86
+ 'test',
87
+ 'onLoad',
88
+ 'actions',
89
+ '1',
90
+ 'arguments',
91
+ 2,
92
+ ])
93
+ })
94
+ test('should not find valid action arguments', () => {
95
+ const problems = Array.from(
96
+ searchProject({
97
+ files: {
98
+ actions: {
99
+ 'legacy-action': {
100
+ name: 'legacy-action',
101
+ handler: '',
102
+ arguments: [
103
+ { name: 'arg1', formula: valueFormula(1) },
104
+ { name: 'arg2', formula: valueFormula(2) },
105
+ ],
106
+ variableArguments: false,
107
+ },
108
+ 'modern-action': {
109
+ name: 'modern-action',
110
+ handler: '',
111
+ version: 2,
112
+ arguments: [
113
+ { name: 'arg1', formula: valueFormula(1) },
114
+ { name: 'arg2', formula: valueFormula(2) },
115
+ ],
116
+ variableArguments: false,
117
+ },
118
+ },
119
+ components: {
120
+ test: {
121
+ name: 'test',
122
+ nodes: {},
123
+ formulas: {},
124
+ apis: {},
125
+ attributes: {},
126
+ variables: {},
127
+ onLoad: {
128
+ trigger: 'onLoad',
129
+ actions: [
130
+ {
131
+ name: 'legacy-action',
132
+ arguments: [
133
+ { name: 'arg1', formula: valueFormula(7) },
134
+ { name: 'arg2', formula: valueFormula(9) },
135
+ ],
136
+ },
137
+ {
138
+ name: 'modern-action',
139
+ arguments: [
140
+ { name: 'arg1', formula: valueFormula(7) },
141
+ { name: 'arg2', formula: valueFormula(9) },
142
+ ],
143
+ },
144
+ ],
145
+ },
146
+ },
147
+ },
148
+ },
149
+ rules: [unknownActionArgumentRule],
150
+ }),
151
+ )
152
+
153
+ expect(problems).toHaveLength(0)
154
+ })
155
+ })
156
+
157
+ describe('fix unknownActionArgumentRule', () => {
158
+ test('should fix invalid action arguments', () => {
159
+ const fixedProject = fixProject({
160
+ files: {
161
+ actions: {
162
+ 'legacy-action': {
163
+ name: 'legacy-action',
164
+ handler: '',
165
+ arguments: [
166
+ { name: 'arg1', formula: valueFormula(1) },
167
+ { name: 'arg2', formula: valueFormula(2) },
168
+ ],
169
+ variableArguments: false,
170
+ },
171
+ 'modern-action': {
172
+ name: 'modern-action',
173
+ handler: '',
174
+ version: 2,
175
+ arguments: [
176
+ { name: 'arg1', formula: valueFormula(1) },
177
+ { name: 'arg2', formula: valueFormula(2) },
178
+ ],
179
+ variableArguments: false,
180
+ },
181
+ },
182
+ components: {
183
+ test: {
184
+ name: 'test',
185
+ nodes: {},
186
+ formulas: {},
187
+ apis: {},
188
+ attributes: {},
189
+ variables: {},
190
+ onLoad: {
191
+ trigger: 'onLoad',
192
+ actions: [
193
+ {
194
+ name: 'legacy-action',
195
+ arguments: [
196
+ { name: 'arg1', formula: valueFormula(7) },
197
+ { name: 'arg2', formula: valueFormula(9) },
198
+ { name: 'arg3', formula: valueFormula(13) },
199
+ ],
200
+ },
201
+ {
202
+ name: 'modern-action',
203
+ arguments: [
204
+ { name: 'arg1', formula: valueFormula(7) },
205
+ { name: 'arg3', formula: valueFormula(13) },
206
+ { name: 'arg2', formula: valueFormula(9) },
207
+ ],
208
+ },
209
+ ],
210
+ },
211
+ },
212
+ },
213
+ },
214
+ rule: unknownActionArgumentRule,
215
+ fixType: 'delete-unknown-action-argument',
216
+ state: {},
217
+ })
218
+
219
+ expect(
220
+ (fixedProject.components['test']?.onLoad?.actions[0] as CustomActionModel)
221
+ .arguments,
222
+ ).toEqual([
223
+ {
224
+ formula: {
225
+ type: 'value',
226
+ value: 7,
227
+ },
228
+ name: 'arg1',
229
+ },
230
+ {
231
+ formula: {
232
+ type: 'value',
233
+ value: 9,
234
+ },
235
+ name: 'arg2',
236
+ },
237
+ ])
238
+ expect(
239
+ (fixedProject.components['test']?.onLoad?.actions[1] as CustomActionModel)
240
+ .arguments,
241
+ ).toEqual([
242
+ {
243
+ formula: {
244
+ type: 'value',
245
+ value: 7,
246
+ },
247
+ name: 'arg1',
248
+ },
249
+ {
250
+ formula: {
251
+ type: 'value',
252
+ value: 9,
253
+ },
254
+ name: 'arg2',
255
+ },
256
+ ])
257
+ })
258
+ test('should not remove valid action arguments', () => {
259
+ const files = {
260
+ actions: {
261
+ 'legacy-action': {
262
+ name: 'legacy-action',
263
+ handler: '',
264
+ arguments: [
265
+ { name: 'arg1', formula: valueFormula(1) },
266
+ { name: 'arg2', formula: valueFormula(2) },
267
+ ],
268
+ variableArguments: false,
269
+ },
270
+ 'modern-action': {
271
+ name: 'modern-action',
272
+ handler: '',
273
+ version: 2,
274
+ arguments: [
275
+ { name: 'arg1', formula: valueFormula(1) },
276
+ { name: 'arg2', formula: valueFormula(2) },
277
+ ],
278
+ variableArguments: false,
279
+ },
280
+ },
281
+ components: {
282
+ test: {
283
+ name: 'test',
284
+ nodes: {},
285
+ formulas: {},
286
+ apis: {},
287
+ attributes: {},
288
+ variables: {},
289
+ onLoad: {
290
+ trigger: 'onLoad',
291
+ actions: [
292
+ {
293
+ name: 'legacy-action',
294
+ arguments: [
295
+ { name: 'arg1', formula: valueFormula(7) },
296
+ { name: 'arg2', formula: valueFormula(9) },
297
+ ],
298
+ },
299
+ {
300
+ name: 'modern-action',
301
+ arguments: [
302
+ { name: 'arg1', formula: valueFormula(7) },
303
+ { name: 'arg2', formula: valueFormula(9) },
304
+ ],
305
+ },
306
+ ],
307
+ },
308
+ },
309
+ },
310
+ }
311
+ const fixedProject = fixProject({
312
+ files,
313
+ rule: unknownActionArgumentRule,
314
+ fixType: 'delete-unknown-action-argument',
315
+ state: {},
316
+ })
317
+ expect(fixedProject).toEqual(files)
318
+ })
319
+ })
@@ -0,0 +1,51 @@
1
+ import { isLegacyPluginAction } from '@nordcraft/core/dist/component/actionUtils'
2
+ import type { Rule } from '../../../types'
3
+ import { removeFromPathFix } from '../../../util/removeUnused.fix'
4
+
5
+ export const unknownActionArgumentRule: Rule<{ name: string }> = {
6
+ code: 'unknown action argument',
7
+ level: 'warning',
8
+ category: 'Unknown Reference',
9
+ visit: (report, { path, files, value, nodeType }) => {
10
+ if (nodeType !== 'action-custom-model-argument') {
11
+ return
12
+ }
13
+ const { action, argument, argumentIndex } = value
14
+ if (action.name.startsWith('@toddle')) {
15
+ return
16
+ }
17
+ const referencedAction = (
18
+ action.package ? files.packages?.[action.package]?.actions : files.actions
19
+ )?.[action.name]
20
+ if (!referencedAction) {
21
+ return
22
+ }
23
+ const referencedActionArguments = referencedAction.arguments ?? []
24
+ if (isLegacyPluginAction(referencedAction)) {
25
+ if (argumentIndex >= referencedActionArguments.length) {
26
+ report(
27
+ path,
28
+ {
29
+ name: argument.name ?? `argument at position ${argumentIndex}`,
30
+ },
31
+ ['delete-unknown-action-argument'],
32
+ )
33
+ }
34
+ } else if (
35
+ !referencedAction.arguments?.some((a) => a.name === argument.name)
36
+ ) {
37
+ report(
38
+ path,
39
+ {
40
+ name: argument.name,
41
+ },
42
+ ['delete-unknown-action-argument'],
43
+ )
44
+ }
45
+ },
46
+ fixes: {
47
+ 'delete-unknown-action-argument': removeFromPathFix,
48
+ },
49
+ }
50
+
51
+ export type UnknownActionArgumentRuleFix = 'delete-unknown-action-argument'
@@ -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
+ ]