@nordcraft/search 1.0.39 → 1.0.41

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,8 +1,14 @@
1
+ import type {
2
+ ActionModel,
3
+ ElementNodeModel,
4
+ } from '@nordcraft/core/dist/component/component.types'
5
+ import type { ProjectFiles } from '@nordcraft/ssr/dist/ssr.types'
1
6
  import { describe, expect, test } from 'bun:test'
7
+ import { fixProject } from '../../fixProject'
2
8
  import { searchProject } from '../../searchProject'
3
9
  import { legacyActionRule } from './legacyActionRule'
4
10
 
5
- describe('legacyAction', () => {
11
+ describe('find legacyActions', () => {
6
12
  test('should detect legacy actions used in components', () => {
7
13
  const problems = Array.from(
8
14
  searchProject({
@@ -90,3 +96,312 @@ describe('legacyAction', () => {
90
96
  expect(problems).toHaveLength(0)
91
97
  })
92
98
  })
99
+
100
+ describe('fix legacyActions', () => {
101
+ test('should replace the If action with a Switch action', () => {
102
+ const legacyIfAction: ActionModel = {
103
+ name: 'If',
104
+ events: {
105
+ true: {
106
+ actions: [
107
+ {
108
+ name: '@toddle/logToConsole',
109
+ arguments: [
110
+ {
111
+ name: 'Label',
112
+ description: 'A label for the message.',
113
+ formula: { type: 'value', value: '' },
114
+ },
115
+ {
116
+ name: 'Data',
117
+ type: { type: 'Any' },
118
+ description: 'The data you want to log to the console.',
119
+ formula: {
120
+ type: 'value',
121
+ value: '<Data>',
122
+ },
123
+ },
124
+ ],
125
+ label: 'Log to console',
126
+ group: 'debugging',
127
+ description: 'Log a message to the browser console.',
128
+ },
129
+ ],
130
+ },
131
+ false: {
132
+ actions: [
133
+ {
134
+ name: '@toddle/logToConsole',
135
+ arguments: [
136
+ {
137
+ name: 'Label',
138
+ description: 'A label for the message.',
139
+ formula: { type: 'value', value: '' },
140
+ },
141
+ {
142
+ name: 'Data',
143
+ type: { type: 'Any' },
144
+ description: 'The data you want to log to the console.',
145
+ formula: {
146
+ type: 'value',
147
+ value: '<Data>',
148
+ },
149
+ },
150
+ ],
151
+ label: 'Log to console',
152
+ group: 'debugging',
153
+ description: 'Log a message to the browser console.',
154
+ },
155
+ ],
156
+ },
157
+ },
158
+ arguments: [
159
+ {
160
+ name: 'Condition',
161
+ formula: { type: 'value', value: true },
162
+ },
163
+ ],
164
+ }
165
+ const projectFiles: ProjectFiles = {
166
+ formulas: {},
167
+ components: {
168
+ apiComponent: {
169
+ name: 'test',
170
+ nodes: {
171
+ root: {
172
+ tag: 'p',
173
+ type: 'element',
174
+ attrs: {},
175
+ style: {},
176
+ events: {
177
+ click: {
178
+ trigger: 'click',
179
+ actions: [legacyIfAction],
180
+ },
181
+ },
182
+ classes: {},
183
+ children: [],
184
+ },
185
+ },
186
+ formulas: {},
187
+ apis: {},
188
+ attributes: {},
189
+ variables: {},
190
+ },
191
+ },
192
+ }
193
+ const fixedProject = fixProject({
194
+ files: projectFiles,
195
+ rule: legacyActionRule,
196
+ fixType: 'replace-legacy-action',
197
+ })
198
+ const fixedAction = (
199
+ fixedProject.components['apiComponent']?.nodes['root'] as ElementNodeModel
200
+ ).events['click'].actions[0]
201
+ expect(fixedAction).toMatchInlineSnapshot(`
202
+ {
203
+ "cases": [
204
+ {
205
+ "actions": [
206
+ {
207
+ "arguments": [
208
+ {
209
+ "description": "A label for the message.",
210
+ "formula": {
211
+ "type": "value",
212
+ "value": "",
213
+ },
214
+ "name": "Label",
215
+ },
216
+ {
217
+ "description": "The data you want to log to the console.",
218
+ "formula": {
219
+ "type": "value",
220
+ "value": "<Data>",
221
+ },
222
+ "name": "Data",
223
+ "type": {
224
+ "type": "Any",
225
+ },
226
+ },
227
+ ],
228
+ "description": "Log a message to the browser console.",
229
+ "group": "debugging",
230
+ "label": "Log to console",
231
+ "name": "@toddle/logToConsole",
232
+ },
233
+ ],
234
+ "condition": {
235
+ "type": "value",
236
+ "value": true,
237
+ },
238
+ },
239
+ ],
240
+ "default": {
241
+ "actions": [
242
+ {
243
+ "arguments": [
244
+ {
245
+ "description": "A label for the message.",
246
+ "formula": {
247
+ "type": "value",
248
+ "value": "",
249
+ },
250
+ "name": "Label",
251
+ },
252
+ {
253
+ "description": "The data you want to log to the console.",
254
+ "formula": {
255
+ "type": "value",
256
+ "value": "<Data>",
257
+ },
258
+ "name": "Data",
259
+ "type": {
260
+ "type": "Any",
261
+ },
262
+ },
263
+ ],
264
+ "description": "Log a message to the browser console.",
265
+ "group": "debugging",
266
+ "label": "Log to console",
267
+ "name": "@toddle/logToConsole",
268
+ },
269
+ ],
270
+ },
271
+ "type": "Switch",
272
+ }
273
+ `)
274
+ })
275
+ test('should replace the TriggerEvent action with the builtin action', () => {
276
+ const legacyAction: ActionModel = {
277
+ name: 'TriggerEvent',
278
+ arguments: [
279
+ {
280
+ name: 'name',
281
+ formula: { type: 'value', value: 'sdfsdf' },
282
+ },
283
+ {
284
+ name: 'data',
285
+ formula: { type: 'value', value: 'test' },
286
+ },
287
+ ],
288
+ }
289
+ const projectFiles: ProjectFiles = {
290
+ formulas: {},
291
+ components: {
292
+ apiComponent: {
293
+ name: 'test',
294
+ nodes: {
295
+ root: {
296
+ tag: 'p',
297
+ type: 'element',
298
+ attrs: {},
299
+ style: {},
300
+ events: {
301
+ click: {
302
+ trigger: 'click',
303
+ actions: [legacyAction],
304
+ },
305
+ },
306
+ classes: {},
307
+ children: [],
308
+ },
309
+ },
310
+ formulas: {},
311
+ apis: {},
312
+ attributes: {},
313
+ variables: {},
314
+ },
315
+ },
316
+ }
317
+ const fixedProject = fixProject({
318
+ files: projectFiles,
319
+ rule: legacyActionRule,
320
+ fixType: 'replace-legacy-action',
321
+ })
322
+ const fixedAction = (
323
+ fixedProject.components['apiComponent']?.nodes['root'] as ElementNodeModel
324
+ ).events['click'].actions[0]
325
+ expect(fixedAction).toMatchInlineSnapshot(`
326
+ {
327
+ "data": {
328
+ "type": "value",
329
+ "value": "test",
330
+ },
331
+ "event": "sdfsdf",
332
+ "type": "TriggerEvent",
333
+ }
334
+ `)
335
+ })
336
+ test('should replace the TriggerEvent action with the builtin action', () => {
337
+ const legacyAction: ActionModel = {
338
+ name: 'FocusElement',
339
+ arguments: [
340
+ {
341
+ name: 'elementId',
342
+ formula: { type: 'value', value: 'my-id' },
343
+ },
344
+ ],
345
+ }
346
+ const projectFiles: ProjectFiles = {
347
+ formulas: {},
348
+ components: {
349
+ apiComponent: {
350
+ name: 'test',
351
+ nodes: {
352
+ root: {
353
+ tag: 'p',
354
+ type: 'element',
355
+ attrs: {},
356
+ style: {},
357
+ events: {
358
+ click: {
359
+ trigger: 'click',
360
+ actions: [legacyAction],
361
+ },
362
+ },
363
+ classes: {},
364
+ children: [],
365
+ },
366
+ },
367
+ formulas: {},
368
+ apis: {},
369
+ attributes: {},
370
+ variables: {},
371
+ },
372
+ },
373
+ }
374
+ const fixedProject = fixProject({
375
+ files: projectFiles,
376
+ rule: legacyActionRule,
377
+ fixType: 'replace-legacy-action',
378
+ })
379
+ const fixedAction = (
380
+ fixedProject.components['apiComponent']?.nodes['root'] as ElementNodeModel
381
+ ).events['click'].actions[0]
382
+ expect(fixedAction).toMatchInlineSnapshot(`
383
+ {
384
+ "arguments": [
385
+ {
386
+ "formula": {
387
+ "arguments": [
388
+ {
389
+ "formula": {
390
+ "type": "value",
391
+ "value": "my-id",
392
+ },
393
+ "name": "Id",
394
+ },
395
+ ],
396
+ "name": "@toddle/getElementById",
397
+ "type": "function",
398
+ },
399
+ "name": "Element",
400
+ },
401
+ ],
402
+ "label": "Focus",
403
+ "name": "@toddle/focus",
404
+ }
405
+ `)
406
+ })
407
+ })
@@ -1,5 +1,11 @@
1
+ import type {
2
+ ActionModel,
3
+ CustomActionArgument,
4
+ } from '@nordcraft/core/dist/component/component.types'
5
+ import { valueFormula } from '@nordcraft/core/dist/formula/formulaUtils'
6
+ import { set } from '@nordcraft/core/dist/utils/collections'
1
7
  import type { Rule } from '../../types'
2
- import { isLegacyAction } from '../../util/helpers'
8
+ import { isLegacyAction, renameArguments } from '../../util/helpers'
3
9
 
4
10
  export const legacyActionRule: Rule<{
5
11
  name: string
@@ -11,7 +17,6 @@ export const legacyActionRule: Rule<{
11
17
  if (nodeType !== 'action-model') {
12
18
  return
13
19
  }
14
-
15
20
  if (isLegacyAction(value)) {
16
21
  let details: { name: string } | undefined
17
22
  if ('name' in value) {
@@ -19,8 +24,190 @@ export const legacyActionRule: Rule<{
19
24
  name: value.name,
20
25
  }
21
26
  }
22
-
23
- report(path, details)
27
+ report(
28
+ path,
29
+ details,
30
+ unfixableLegacyActions.has(value.name)
31
+ ? undefined
32
+ : !formulaNamedActions.includes(value.name) ||
33
+ // Check if the first argument is a value formula with a string value
34
+ (value.arguments?.[0].formula.type === 'value' &&
35
+ typeof value.arguments[0].formula.value === 'string')
36
+ ? ['replace-legacy-action']
37
+ : undefined,
38
+ )
24
39
  }
25
40
  },
41
+ fixes: {
42
+ 'replace-legacy-action': ({ path, value, nodeType, files }) => {
43
+ if (nodeType !== 'action-model') {
44
+ return
45
+ }
46
+ if (!isLegacyAction(value)) {
47
+ return
48
+ }
49
+
50
+ let newAction: ActionModel | undefined
51
+ switch (value.name) {
52
+ case 'If': {
53
+ const trueActions = value.events?.['true']?.actions ?? []
54
+ const falseActions = value.events?.['false']?.actions ?? []
55
+ const trueCondition: CustomActionArgument | undefined =
56
+ (value.arguments ?? [])[0]
57
+ newAction = {
58
+ type: 'Switch',
59
+ cases: [
60
+ {
61
+ condition: trueCondition?.formula ?? null,
62
+ actions: trueActions,
63
+ },
64
+ ],
65
+ default: { actions: falseActions },
66
+ }
67
+ break
68
+ }
69
+ case 'PreventDefault': {
70
+ newAction = {
71
+ name: '@toddle/preventDefault',
72
+ arguments: [],
73
+ label: 'Prevent default',
74
+ }
75
+ break
76
+ }
77
+ case 'StopPropagation': {
78
+ newAction = {
79
+ name: '@toddle/stopPropagation',
80
+ arguments: [],
81
+ label: 'Stop propagation',
82
+ }
83
+ break
84
+ }
85
+ case 'UpdateVariable':
86
+ case 'Update Variable': {
87
+ const variableName =
88
+ value.arguments?.[0]?.formula.type === 'value'
89
+ ? value.arguments[0].formula.value
90
+ : undefined
91
+ if (typeof variableName !== 'string') {
92
+ break
93
+ }
94
+ const variableValue = value.arguments?.[1]?.formula
95
+ if (!variableValue) {
96
+ break
97
+ }
98
+ newAction = {
99
+ type: 'SetVariable',
100
+ variable: variableName,
101
+ data: variableValue,
102
+ }
103
+ break
104
+ }
105
+ case 'SetTimeout': {
106
+ newAction = {
107
+ ...value,
108
+ name: '@toddle/sleep',
109
+ arguments: renameArguments(
110
+ { 'Delay in ms': 'Delay in milliseconds' },
111
+ value.arguments,
112
+ ),
113
+ events: value.events?.['timeout']
114
+ ? { tick: value.events.timeout }
115
+ : undefined,
116
+ label: 'Sleep',
117
+ }
118
+ break
119
+ }
120
+ case 'SetInterval': {
121
+ newAction = {
122
+ ...value,
123
+ name: '@toddle/interval',
124
+ arguments: renameArguments(
125
+ { 'Interval in ms': 'Interval in milliseconds' },
126
+ value.arguments,
127
+ ),
128
+ label: 'Interval',
129
+ }
130
+ break
131
+ }
132
+ case 'Debug': {
133
+ newAction = {
134
+ ...value,
135
+ name: '@toddle/logToConsole',
136
+ label: 'Log to console',
137
+ }
138
+ break
139
+ }
140
+ case 'GoToURL': {
141
+ newAction = {
142
+ name: '@toddle/gotToURL', // Yes, the typo is in the action name
143
+ arguments: renameArguments({ url: 'URL' }, value.arguments),
144
+ label: 'Go to URL',
145
+ }
146
+ break
147
+ }
148
+ case 'TriggerEvent': {
149
+ const eventName =
150
+ value.arguments?.[0]?.formula.type === 'value'
151
+ ? value.arguments[0].formula.value
152
+ : undefined
153
+ if (typeof eventName !== 'string') {
154
+ break
155
+ }
156
+ const eventData = value.arguments?.[1]?.formula
157
+ if (!eventData) {
158
+ break
159
+ }
160
+ newAction = {
161
+ type: 'TriggerEvent',
162
+ event: eventName,
163
+ data: eventData,
164
+ }
165
+ break
166
+ }
167
+ case 'FocusElement': {
168
+ newAction = {
169
+ name: '@toddle/focus',
170
+ arguments: [
171
+ {
172
+ name: 'Element',
173
+ formula: {
174
+ type: 'function',
175
+ name: '@toddle/getElementById',
176
+ arguments: [
177
+ {
178
+ name: 'Id',
179
+ formula:
180
+ value.arguments?.[0]?.formula ?? valueFormula(null),
181
+ },
182
+ ],
183
+ },
184
+ },
185
+ ],
186
+ label: 'Focus',
187
+ }
188
+ }
189
+ }
190
+ if (newAction) {
191
+ return set(files, path, newAction)
192
+ }
193
+ },
194
+ },
26
195
  }
196
+
197
+ // These actions take a first argument which is a formula as the name
198
+ // of the thing to update/trigger. We can only safely autofix these if
199
+ // the argument is a value operation and a string
200
+ const formulaNamedActions = [
201
+ 'UpdateVariable',
202
+ 'Update Variable',
203
+ 'TriggerEvent',
204
+ ]
205
+
206
+ const unfixableLegacyActions = new Set([
207
+ 'CopyToClipboard', // Previously, this action would JSON stringify non-string inputs
208
+ 'Update URL parameter', // The user will need to pick a history mode (push/replace)
209
+ 'Fetch', // This was mainly used for APIs v1
210
+ '@toddle/setSessionCookies', // The new 'Set cookie' action takes more arguments
211
+ ])
212
+
213
+ export type LegacyActionRuleFix = 'replace-legacy-action'
@@ -1,7 +1,6 @@
1
1
  import type {
2
2
  AndOperation,
3
3
  ArrayOperation,
4
- FunctionArgument,
5
4
  FunctionOperation,
6
5
  OrOperation,
7
6
  SwitchOperation,
@@ -11,6 +10,11 @@ import { omitKeys, set } from '@nordcraft/core/dist/utils/collections'
11
10
  import { isDefined } from '@nordcraft/core/dist/utils/util'
12
11
  import type { ProjectFiles } from '@nordcraft/ssr/dist/ssr.types'
13
12
  import type { FormulaNode, NodeType, Rule } from '../../types'
13
+ import {
14
+ ARRAY_ARGUMENT_MAPPINGS,
15
+ PREDICATE_ARGUMENT_MAPPINGS,
16
+ renameArguments,
17
+ } from '../../util/helpers'
14
18
 
15
19
  export const legacyFormulaRule: Rule<{
16
20
  name: string
@@ -25,8 +29,10 @@ export const legacyFormulaRule: Rule<{
25
29
  report(
26
30
  data.path,
27
31
  { name: data.value.name },
28
- // The TYPE formula cannot be autofixed since the types have changed between the 2 implementations
29
- data.value.name !== 'TYPE' ? ['replace-legacy-formula'] : undefined,
32
+ // The TYPE and BOOLEAN formulas cannot be autofixed since the logic has changed between the 2 implementations
33
+ data.value.name !== 'TYPE' && data.value.name !== 'BOOLEAN'
34
+ ? ['replace-legacy-formula']
35
+ : undefined,
30
36
  )
31
37
  },
32
38
  fixes: {
@@ -372,14 +378,6 @@ export const legacyFormulaRule: Rule<{
372
378
  }
373
379
  return set(data.files, data.path, newAppendFormula)
374
380
  }
375
- case 'BOOLEAN': {
376
- const newBooleanFormula: FunctionOperation = {
377
- ...data.value,
378
- name: '@toddle/boolean',
379
- display_name: 'Boolean',
380
- }
381
- return set(data.files, data.path, newBooleanFormula)
382
- }
383
381
  case 'CLAMP': {
384
382
  const newClampFormula: FunctionOperation = {
385
383
  ...data.value,
@@ -845,23 +843,3 @@ const builtInFormulas = new Set([
845
843
  'uppercase',
846
844
  ])
847
845
  // cSpell: enable
848
-
849
- const ARRAY_ARGUMENT_MAPPINGS = { List: 'Array' }
850
- const PREDICATE_ARGUMENT_MAPPINGS = {
851
- ...ARRAY_ARGUMENT_MAPPINGS,
852
- 'Predicate fx': 'Formula',
853
- }
854
-
855
- const renameArguments = (
856
- mappings: Record<string, string>,
857
- args: FunctionArgument[] | undefined,
858
- ): FunctionArgument[] =>
859
- args?.map((arg) => ({
860
- ...arg,
861
- // Let's adjust the names
862
- name:
863
- typeof arg.name === 'string'
864
- ? // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
865
- (mappings[arg.name] ?? arg.name)
866
- : arg.name,
867
- })) ?? []
@@ -1,8 +1,10 @@
1
+ import type { ProjectFiles } from '@nordcraft/ssr/dist/ssr.types'
1
2
  import { describe, expect, test } from 'bun:test'
3
+ import { fixProject } from '../../fixProject'
2
4
  import { searchProject } from '../../searchProject'
3
5
  import { noReferenceProjectFormulaRule } from './noReferenceProjectFormulaRule'
4
6
 
5
- describe('noReferenceFormulaRule', () => {
7
+ describe('find noReferenceFormulaRule', () => {
6
8
  test('should detect unused global formulas', () => {
7
9
  const problems = Array.from(
8
10
  searchProject({
@@ -289,3 +291,36 @@ describe('noReferenceFormulaRule', () => {
289
291
  expect(problems).toHaveLength(0)
290
292
  })
291
293
  })
294
+
295
+ describe('fix noReferenceFormulaRule', () => {
296
+ test('should remove unused global formulas', () => {
297
+ const projectFiles: ProjectFiles = {
298
+ formulas: {
299
+ 'my-formula': {
300
+ name: 'my-formula',
301
+ arguments: [],
302
+ formula: {
303
+ type: 'value',
304
+ value: 'value',
305
+ },
306
+ },
307
+ },
308
+ components: {
309
+ test: {
310
+ name: 'test',
311
+ nodes: {},
312
+ formulas: {},
313
+ apis: {},
314
+ attributes: {},
315
+ variables: {},
316
+ },
317
+ },
318
+ }
319
+ const fixedProject = fixProject({
320
+ files: projectFiles,
321
+ rule: noReferenceProjectFormulaRule,
322
+ fixType: 'delete-project-formula',
323
+ })
324
+ expect(fixedProject.formulas).toEqual({})
325
+ })
326
+ })