@nordcraft/search 1.0.38 → 1.0.40

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 (41) hide show
  1. package/dist/fixProject.js +32 -0
  2. package/dist/fixProject.js.map +1 -0
  3. package/dist/problems.worker.js +45 -8
  4. package/dist/problems.worker.js.map +1 -1
  5. package/dist/rules/actions/legacyActionRule.js +144 -2
  6. package/dist/rules/actions/legacyActionRule.js.map +1 -1
  7. package/dist/rules/actions/legacyActionRule.test.js +235 -1
  8. package/dist/rules/actions/legacyActionRule.test.js.map +1 -1
  9. package/dist/rules/components/noReferenceComponentRule.js +31 -17
  10. package/dist/rules/components/noReferenceComponentRule.js.map +1 -1
  11. package/dist/rules/components/noReferenceComponentRule.test.js +86 -1
  12. package/dist/rules/components/noReferenceComponentRule.test.js.map +1 -1
  13. package/dist/rules/formulas/legacyFormulaRule.js +602 -6
  14. package/dist/rules/formulas/legacyFormulaRule.js.map +1 -1
  15. package/dist/rules/formulas/legacyFormulaRule.test.js +232 -1
  16. package/dist/rules/formulas/legacyFormulaRule.test.js.map +1 -1
  17. package/dist/rules/formulas/noReferenceProjectFormulaRule.js +73 -58
  18. package/dist/rules/formulas/noReferenceProjectFormulaRule.js.map +1 -1
  19. package/dist/rules/formulas/noReferenceProjectFormulaRule.test.js +34 -1
  20. package/dist/rules/formulas/noReferenceProjectFormulaRule.test.js.map +1 -1
  21. package/dist/searchProject.js +338 -217
  22. package/dist/searchProject.js.map +1 -1
  23. package/dist/util/helpers.js +31 -5
  24. package/dist/util/helpers.js.map +1 -1
  25. package/dist/util/helpers.test.js +58 -0
  26. package/dist/util/helpers.test.js.map +1 -0
  27. package/package.json +3 -2
  28. package/src/fixProject.ts +47 -0
  29. package/src/problems.worker.ts +90 -12
  30. package/src/rules/actions/legacyActionRule.test.ts +245 -1
  31. package/src/rules/actions/legacyActionRule.ts +166 -4
  32. package/src/rules/components/noReferenceComponentRule.test.ts +87 -1
  33. package/src/rules/components/noReferenceComponentRule.ts +38 -23
  34. package/src/rules/formulas/legacyFormulaRule.test.ts +242 -1
  35. package/src/rules/formulas/legacyFormulaRule.ts +697 -10
  36. package/src/rules/formulas/noReferenceProjectFormulaRule.test.ts +36 -1
  37. package/src/rules/formulas/noReferenceProjectFormulaRule.ts +78 -64
  38. package/src/searchProject.ts +217 -98
  39. package/src/types.d.ts +20 -3
  40. package/src/util/helpers.test.ts +80 -0
  41. package/src/util/helpers.ts +61 -9
@@ -1,5 +1,10 @@
1
+ import type {
2
+ ActionModel,
3
+ CustomActionArgument,
4
+ } from '@nordcraft/core/dist/component/component.types'
5
+ import { set } from '@nordcraft/core/dist/utils/collections'
1
6
  import type { Rule } from '../../types'
2
- import { isLegacyAction } from '../../util/helpers'
7
+ import { isLegacyAction, renameArguments } from '../../util/helpers'
3
8
 
4
9
  export const legacyActionRule: Rule<{
5
10
  name: string
@@ -11,7 +16,6 @@ export const legacyActionRule: Rule<{
11
16
  if (nodeType !== 'action-model') {
12
17
  return
13
18
  }
14
-
15
19
  if (isLegacyAction(value)) {
16
20
  let details: { name: string } | undefined
17
21
  if ('name' in value) {
@@ -19,8 +23,166 @@ export const legacyActionRule: Rule<{
19
23
  name: value.name,
20
24
  }
21
25
  }
22
-
23
- report(path, details)
26
+ report(
27
+ path,
28
+ details,
29
+ unfixableLegacyActions.has(value.name)
30
+ ? undefined
31
+ : !formulaNamedActions.includes(value.name) ||
32
+ // Check if the first argument is a value formula with a string value
33
+ (value.arguments?.[0].formula.type === 'value' &&
34
+ typeof value.arguments[0].formula.value === 'string')
35
+ ? ['replace-legacy-action']
36
+ : undefined,
37
+ )
24
38
  }
25
39
  },
40
+ fixes: {
41
+ 'replace-legacy-action': ({ path, value, nodeType, files }) => {
42
+ if (nodeType !== 'action-model') {
43
+ return
44
+ }
45
+ if (!isLegacyAction(value)) {
46
+ return
47
+ }
48
+
49
+ let newAction: ActionModel | undefined
50
+ switch (value.name) {
51
+ case 'If': {
52
+ const trueActions = value.events?.['true']?.actions ?? []
53
+ const falseActions = value.events?.['false']?.actions ?? []
54
+ const trueCondition: CustomActionArgument | undefined =
55
+ (value.arguments ?? [])[0]
56
+ newAction = {
57
+ type: 'Switch',
58
+ cases: [
59
+ {
60
+ condition: trueCondition?.formula ?? null,
61
+ actions: trueActions,
62
+ },
63
+ ],
64
+ default: { actions: falseActions },
65
+ }
66
+ break
67
+ }
68
+ case 'PreventDefault': {
69
+ newAction = {
70
+ name: '@toddle/preventDefault',
71
+ arguments: [],
72
+ label: 'Prevent default',
73
+ }
74
+ break
75
+ }
76
+ case 'StopPropagation': {
77
+ newAction = {
78
+ name: '@toddle/stopPropagation',
79
+ arguments: [],
80
+ label: 'Stop propagation',
81
+ }
82
+ break
83
+ }
84
+ case 'UpdateVariable': {
85
+ const variableName =
86
+ value.arguments?.[0]?.formula.type === 'value'
87
+ ? value.arguments[0].formula.value
88
+ : undefined
89
+ if (typeof variableName !== 'string') {
90
+ break
91
+ }
92
+ const variableValue = value.arguments?.[1]?.formula
93
+ if (!variableValue) {
94
+ break
95
+ }
96
+ newAction = {
97
+ type: 'SetVariable',
98
+ variable: variableName,
99
+ data: variableValue,
100
+ }
101
+ break
102
+ }
103
+ case 'SetTimeout': {
104
+ newAction = {
105
+ ...value,
106
+ name: '@toddle/sleep',
107
+ arguments: renameArguments(
108
+ { 'Delay in ms': 'Delay in milliseconds' },
109
+ value.arguments,
110
+ ),
111
+ events: value.events?.['timeout']
112
+ ? { tick: value.events.timeout }
113
+ : undefined,
114
+ label: 'Sleep',
115
+ }
116
+ break
117
+ }
118
+ case 'SetInterval': {
119
+ newAction = {
120
+ ...value,
121
+ name: '@toddle/interval',
122
+ arguments: renameArguments(
123
+ { 'Interval in ms': 'Interval in milliseconds' },
124
+ value.arguments,
125
+ ),
126
+ label: 'Interval',
127
+ }
128
+ break
129
+ }
130
+ case 'Debug': {
131
+ newAction = {
132
+ ...value,
133
+ name: '@toddle/logToConsole',
134
+ label: 'Log to console',
135
+ }
136
+ break
137
+ }
138
+ case 'GoToURL': {
139
+ newAction = {
140
+ name: '@toddle/gotToURL', // Yes, the typo is in the action name
141
+ arguments: renameArguments({ url: 'URL' }, value.arguments),
142
+ label: 'Go to URL',
143
+ }
144
+ break
145
+ }
146
+ case 'TriggerEvent': {
147
+ const eventName =
148
+ value.arguments?.[0]?.formula.type === 'value'
149
+ ? value.arguments[0].formula.value
150
+ : undefined
151
+ if (typeof eventName !== 'string') {
152
+ break
153
+ }
154
+ const eventData = value.arguments?.[1]?.formula
155
+ if (!eventData) {
156
+ break
157
+ }
158
+ newAction = {
159
+ type: 'TriggerEvent',
160
+ event: eventName,
161
+ data: eventData,
162
+ }
163
+ break
164
+ }
165
+ }
166
+ if (newAction) {
167
+ return set(files, path, newAction)
168
+ }
169
+ },
170
+ },
26
171
  }
172
+
173
+ // These actions take a first argument which is a formula as the name
174
+ // of the thing to update/trigger. We can only safely autofix these if
175
+ // the argument is a value operation and a string
176
+ const formulaNamedActions = ['UpdateVariable', 'TriggerEvent']
177
+
178
+ const unfixableLegacyActions = new Set([
179
+ 'CopyToClipboard', // Previously, this action would JSON stringify non-string inputs
180
+ 'Update URL parameter', // The user will need to pick a history mode (push/replace)
181
+ 'Fetch', // This was mainly used for APIs v1
182
+ 'FocusElement', // The new 'Focus' action takes an element as input - not a selector
183
+ 'UpdateVariable', // The variable name could be a formula in the legacy version
184
+ 'TriggerEvent', // The name of the event could be a formula in the legacy version
185
+ '@toddle/setSessionCookies', // The new 'Set cookie' action takes more arguments
186
+ ])
187
+
188
+ export type LegacyActionRuleFix = 'replace-legacy-action'
@@ -1,8 +1,9 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
+ import { fixProject } from '../../fixProject'
2
3
  import { searchProject } from '../../searchProject'
3
4
  import { noReferenceComponentRule } from './noReferenceComponentRule'
4
5
 
5
- describe('noReferenceComponentRule', () => {
6
+ describe('detect noReferenceComponentRule', () => {
6
7
  test('should detect components with no references', () => {
7
8
  const problems = Array.from(
8
9
  searchProject({
@@ -213,3 +214,88 @@ describe('noReferenceComponentRule', () => {
213
214
  expect(problems[0].path).toEqual(['components', 'exportedOrphan'])
214
215
  })
215
216
  })
217
+
218
+ describe('fix noReferenceComponentRule', () => {
219
+ test('should remove components with no references', () => {
220
+ const fixedProject = fixProject({
221
+ files: {
222
+ formulas: {},
223
+ components: {
224
+ orphan: {
225
+ name: 'test',
226
+ nodes: {},
227
+ formulas: {},
228
+ apis: {},
229
+ attributes: {},
230
+ variables: {},
231
+ },
232
+ anotherOrphan: {
233
+ name: 'test 2',
234
+ nodes: {},
235
+ formulas: {},
236
+ apis: {},
237
+ attributes: {},
238
+ variables: {},
239
+ },
240
+ exportedOrphan: {
241
+ name: 'test',
242
+ nodes: {},
243
+ formulas: {},
244
+ apis: {},
245
+ attributes: {},
246
+ variables: {},
247
+ exported: true,
248
+ },
249
+ page: {
250
+ name: 'my-page',
251
+ nodes: {},
252
+ formulas: {},
253
+ apis: {},
254
+ attributes: {},
255
+ variables: {},
256
+ route: {
257
+ info: {},
258
+ path: [],
259
+ query: {},
260
+ },
261
+ },
262
+ },
263
+ },
264
+ rule: noReferenceComponentRule,
265
+ fixType: 'delete-component',
266
+ state: {
267
+ projectDetails: {
268
+ type: 'package',
269
+ id: 'test-project-id',
270
+ name: 'test-project',
271
+ short_id: 'test-project-id',
272
+ },
273
+ },
274
+ })
275
+ // The orphan component should no longer exist
276
+ expect(fixedProject.components).toEqual({
277
+ exportedOrphan: {
278
+ name: 'test',
279
+ nodes: {},
280
+ formulas: {},
281
+ apis: {},
282
+ attributes: {},
283
+ variables: {},
284
+ exported: true,
285
+ },
286
+ page: {
287
+ name: 'my-page',
288
+ nodes: {},
289
+ formulas: {},
290
+ apis: {},
291
+ attributes: {},
292
+ variables: {},
293
+ route: {
294
+ info: {},
295
+ path: [],
296
+ query: {},
297
+ },
298
+ },
299
+ })
300
+ })
301
+ })
@@ -1,40 +1,55 @@
1
1
  import type { Component } from '@nordcraft/core/dist/component/component.types'
2
+ import { omit } from '@nordcraft/core/dist/utils/collections'
2
3
  import { isDefined } from '@nordcraft/core/dist/utils/util'
3
- import type { Rule } from '../../types'
4
+ import type { ApplicationState, NodeType, Rule } from '../../types'
4
5
 
5
6
  export const noReferenceComponentRule: Rule<void> = {
6
7
  code: 'no-reference component',
7
8
  level: 'warning',
8
9
  category: 'No References',
9
- visit: (report, { path, nodeType, files, value }, state) => {
10
- // We need a way to flag if a component is exported as a web component, as it would be a valid orphan
11
- if (
12
- nodeType !== 'component' ||
13
- isPage(value) ||
14
- (state?.projectDetails?.type === 'package' && value.exported === true)
15
- ) {
10
+ visit: (report, data, state) => {
11
+ if (hasReferences(data, state)) {
16
12
  return
17
13
  }
14
+ report(data.path, undefined, ['delete-component'])
15
+ },
16
+ fixes: {
17
+ 'delete-component': (data, state) => {
18
+ if (hasReferences(data, state)) {
19
+ return
20
+ }
21
+ return omit(data.files, data.path)
22
+ },
23
+ },
24
+ }
18
25
 
19
- for (const component of Object.values(files.components)) {
20
- // Enforce that the component is not undefined since we're iterating
21
- for (const node of Object.values(component!.nodes ?? {})) {
22
- if (
23
- node.type === 'component' &&
24
- node.name === value.name &&
25
- // Circular references from a component to itself should
26
- // not count as a reference
27
- node.name !== component!.name
28
- ) {
29
- return
30
- }
26
+ const hasReferences = (data: NodeType, state?: ApplicationState) => {
27
+ if (
28
+ data.nodeType !== 'component' ||
29
+ isPage(data.value) ||
30
+ (state?.projectDetails?.type === 'package' && data.value.exported === true)
31
+ ) {
32
+ return true
33
+ }
34
+ for (const component of Object.values(data.files.components)) {
35
+ // Enforce that the component is not undefined since we're iterating
36
+ for (const node of Object.values(component!.nodes ?? {})) {
37
+ if (
38
+ node.type === 'component' &&
39
+ node.name === data.value.name &&
40
+ // Circular references from a component to itself should
41
+ // not count as a reference
42
+ node.name !== component!.name
43
+ ) {
44
+ return true
31
45
  }
32
46
  }
33
-
34
- report(path)
35
- },
47
+ }
48
+ return false
36
49
  }
37
50
 
51
+ export type NoReferenceComponentRuleFix = 'delete-component'
52
+
38
53
  const isPage = (
39
54
  value: Component,
40
55
  ): value is Component & { route: Required<Component['route']> } =>
@@ -1,8 +1,11 @@
1
+ import type { ToddleFormula } from '@nordcraft/core/dist/formula/formulaTypes'
2
+ import type { ProjectFiles } from '@nordcraft/ssr/dist/ssr.types'
1
3
  import { describe, expect, test } from 'bun:test'
4
+ import { fixProject } from '../../fixProject'
2
5
  import { searchProject } from '../../searchProject'
3
6
  import { legacyFormulaRule } from './legacyFormulaRule'
4
7
 
5
- describe('legacyFormula', () => {
8
+ describe('detect legacyFormula', () => {
6
9
  test('should detect legacy formulas used in global formula', () => {
7
10
  const problems = Array.from(
8
11
  searchProject({
@@ -328,3 +331,241 @@ describe('legacyFormula', () => {
328
331
  expect(problems).toEqual([])
329
332
  })
330
333
  })
334
+ describe('fix legacyFormula', () => {
335
+ test('should fix the legacy AND formula', () => {
336
+ const files: ProjectFiles = {
337
+ formulas: {
338
+ AND: {
339
+ name: 'AND',
340
+ arguments: [],
341
+ handler: '',
342
+ },
343
+ 'my-formula-1': {
344
+ name: 'my-formula-1',
345
+ arguments: [],
346
+ formula: {
347
+ type: 'function',
348
+ name: '@toddle/concatenate',
349
+ arguments: [
350
+ {
351
+ name: '0',
352
+ formula: {
353
+ type: 'function',
354
+ name: 'AND',
355
+ arguments: [
356
+ {
357
+ name: 'Condition',
358
+ formula: { type: 'value', value: true },
359
+ },
360
+ {
361
+ name: 'Condition',
362
+ formula: { type: 'value', value: true },
363
+ },
364
+ ],
365
+ variableArguments: true,
366
+ },
367
+ },
368
+ ],
369
+ variableArguments: true,
370
+ display_name: 'Concatenate',
371
+ },
372
+ },
373
+ },
374
+ components: {},
375
+ }
376
+ const fixedProject = fixProject({
377
+ files,
378
+ rule: legacyFormulaRule,
379
+ fixType: 'replace-legacy-formula',
380
+ })
381
+
382
+ expect(
383
+ (fixedProject.formulas?.['my-formula-1'] as ToddleFormula).formula,
384
+ ).toEqual({
385
+ type: 'function',
386
+ name: '@toddle/concatenate',
387
+ arguments: [
388
+ {
389
+ name: '0',
390
+ formula: {
391
+ type: 'and',
392
+ arguments: [
393
+ {
394
+ formula: { type: 'value', value: true },
395
+ },
396
+ {
397
+ formula: { type: 'value', value: true },
398
+ },
399
+ ],
400
+ variableArguments: true,
401
+ },
402
+ },
403
+ ],
404
+ variableArguments: true,
405
+ display_name: 'Concatenate',
406
+ } as any)
407
+ })
408
+ test('should fix the legacy IF formula', () => {
409
+ const files: ProjectFiles = {
410
+ formulas: {
411
+ IF: {
412
+ name: 'IF',
413
+ arguments: [],
414
+ handler: '',
415
+ },
416
+ },
417
+ components: {
418
+ 'project-sidebar-item': {
419
+ apis: {},
420
+ name: 'project-sidebar-item',
421
+ nodes: {},
422
+ events: [],
423
+ onLoad: {
424
+ actions: [],
425
+ trigger: 'Load',
426
+ },
427
+ contexts: {
428
+ EditorPage: {
429
+ formulas: ['XK0T8tQWA0YhkfDUzqu-h'],
430
+ workflows: ['sNo0Ya'],
431
+ componentName: 'EditorPage',
432
+ },
433
+ },
434
+ formulas: {
435
+ p_Z1PzOcDop79KGafQ7Lm: {
436
+ name: 'Preview domain',
437
+ formula: {
438
+ name: 'IF',
439
+ type: 'function',
440
+ arguments: [
441
+ {
442
+ name: 'If',
443
+ formula: {
444
+ name: '@toddle/equals',
445
+ type: 'function',
446
+ arguments: [
447
+ {
448
+ name: 'First',
449
+ formula: {
450
+ path: ['Attributes', 'branch-name'],
451
+ type: 'path',
452
+ },
453
+ },
454
+ {
455
+ name: 'Second',
456
+ formula: {
457
+ type: 'value',
458
+ value: 'main',
459
+ },
460
+ },
461
+ ],
462
+ display_name: 'Equals',
463
+ },
464
+ },
465
+ {
466
+ name: 'Then',
467
+ formula: {
468
+ name: '@toddle/concatenate',
469
+ type: 'function',
470
+ arguments: [
471
+ {
472
+ name: 'Items',
473
+ formula: {
474
+ type: 'value',
475
+ value: 'https://',
476
+ },
477
+ },
478
+ {
479
+ formula: {
480
+ path: ['Attributes', 'project-name'],
481
+ type: 'path',
482
+ },
483
+ },
484
+ {
485
+ formula: {
486
+ type: 'value',
487
+ value: '.toddle.site',
488
+ },
489
+ },
490
+ ],
491
+ display_name: 'Concatenate',
492
+ variableArguments: true,
493
+ },
494
+ },
495
+ {
496
+ name: 'Else',
497
+ formula: {
498
+ name: '@toddle/concatenate',
499
+ type: 'function',
500
+ arguments: [
501
+ {
502
+ formula: {
503
+ type: 'value',
504
+ value: 'https://',
505
+ },
506
+ },
507
+ {
508
+ formula: {
509
+ path: ['Attributes', 'branch-name'],
510
+ type: 'path',
511
+ },
512
+ },
513
+ {
514
+ formula: {
515
+ type: 'value',
516
+ value: '-',
517
+ },
518
+ },
519
+ {
520
+ formula: {
521
+ path: ['Attributes', 'project-name'],
522
+ type: 'path',
523
+ },
524
+ },
525
+ {
526
+ formula: {
527
+ type: 'value',
528
+ value: '.toddle.site',
529
+ },
530
+ },
531
+ ],
532
+ display_name: 'Concatenate',
533
+ variableArguments: true,
534
+ },
535
+ },
536
+ ],
537
+ },
538
+ memoize: false,
539
+ arguments: [],
540
+ },
541
+ },
542
+ variables: {},
543
+ workflows: {},
544
+ attributes: {},
545
+ },
546
+ },
547
+ }
548
+ const fixedProject = fixProject({
549
+ files,
550
+ rule: legacyFormulaRule,
551
+ fixType: 'replace-legacy-formula',
552
+ pathsToVisit: [
553
+ [
554
+ 'components',
555
+ 'project-sidebar-item',
556
+ 'formulas',
557
+ 'p_Z1PzOcDop79KGafQ7Lm',
558
+ 'formula',
559
+ ],
560
+ ],
561
+ useExactPaths: true,
562
+ })
563
+
564
+ const updatedFormula = (
565
+ fixedProject.components['project-sidebar-item']?.formulas?.[
566
+ 'p_Z1PzOcDop79KGafQ7Lm'
567
+ ] as any
568
+ )?.formula
569
+ expect(updatedFormula.type).toEqual('switch')
570
+ })
571
+ })