@nordcraft/search 1.0.39 → 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.
@@ -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,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
+ })
@@ -1,92 +1,106 @@
1
1
  import { ToddleComponent } from '@nordcraft/core/dist/component/ToddleComponent'
2
2
  import type { Formula } from '@nordcraft/core/dist/formula/formula'
3
3
  import { isToddleFormula } from '@nordcraft/core/dist/formula/formulaTypes'
4
+ import { omit } from '@nordcraft/core/dist/utils/collections'
4
5
  import { ToddleApiService } from '@nordcraft/ssr/dist/ToddleApiService'
5
6
  import { ToddleRoute } from '@nordcraft/ssr/dist/ToddleRoute'
6
- import type { Rule } from '../../types'
7
+ import type { NodeType, Rule } from '../../types'
7
8
 
8
9
  export const noReferenceProjectFormulaRule: Rule<void> = {
9
10
  code: 'no-reference project formula',
10
11
  level: 'warning',
11
12
  category: 'No References',
12
- visit: (report, { path, files, value, nodeType, memo }) => {
13
- if (nodeType !== 'project-formula' || value.exported === true) {
13
+ visit: (report, details) => {
14
+ if (hasReferences(details)) {
14
15
  return
15
16
  }
17
+ report(details.path, undefined, ['delete-project-formula'])
18
+ },
19
+ fixes: {
20
+ 'delete-project-formula': (data) => {
21
+ if (hasReferences(data)) {
22
+ return
23
+ }
24
+ return omit(data.files, data.path)
25
+ },
26
+ },
27
+ }
16
28
 
17
- // Check in all API services first, since that should be quick
18
- for (const apiService of Object.values(files.services ?? {})) {
19
- const service = new ToddleApiService({
20
- service: apiService,
21
- globalFormulas: { formulas: files.formulas, packages: files.packages },
22
- })
23
- const formulas = service.formulasInService()
24
- for (const { path: _formulaPath, formula } of formulas) {
25
- // Check if the formula is used in the formula
26
- if (checkFormula(formula, value.name)) {
27
- return
28
- }
29
+ export type NoReferenceProjectFormulaRuleFix = 'delete-project-formula'
30
+
31
+ const hasReferences = ({ value, files, nodeType, memo }: NodeType) => {
32
+ if (nodeType !== 'project-formula' || value.exported === true) {
33
+ return true
34
+ }
35
+
36
+ // Check in all API services first, since that should be quick
37
+ for (const apiService of Object.values(files.services ?? {})) {
38
+ const service = new ToddleApiService({
39
+ service: apiService,
40
+ globalFormulas: { formulas: files.formulas, packages: files.packages },
41
+ })
42
+ const formulas = service.formulasInService()
43
+ for (const { path: _formulaPath, formula } of formulas) {
44
+ // Check if the formula is used in the formula
45
+ if (checkFormula(formula, value.name)) {
46
+ return true
29
47
  }
30
48
  }
49
+ }
31
50
 
32
- // Check routes before components, since they should be quicker
33
- for (const projectRoute of Object.values(files.routes ?? {})) {
34
- const route = new ToddleRoute({
35
- route: projectRoute,
36
- globalFormulas: { formulas: files.formulas, packages: files.packages },
37
- })
38
- const formulas = route.formulasInRoute()
39
- for (const { path: _formulaPath, formula } of formulas) {
40
- // Check if the formula is used in the formula
41
- if (checkFormula(formula, value.name)) {
42
- return
43
- }
51
+ // Check routes before components, since they should be quicker
52
+ for (const projectRoute of Object.values(files.routes ?? {})) {
53
+ const route = new ToddleRoute({
54
+ route: projectRoute,
55
+ globalFormulas: { formulas: files.formulas, packages: files.packages },
56
+ })
57
+ const formulas = route.formulasInRoute()
58
+ for (const { path: _formulaPath, formula } of formulas) {
59
+ // Check if the formula is used in the formula
60
+ if (checkFormula(formula, value.name)) {
61
+ return true
44
62
  }
45
63
  }
64
+ }
46
65
 
47
- const componentFormulaReferences = memo(
48
- 'componentFormulaReferences',
49
- () => {
50
- const usedFormulas = new Set<string>()
51
- for (const component of Object.values(files.components)) {
52
- const c = new ToddleComponent({
53
- // Enforce that the component is not undefined since we're iterating
54
- component: component!,
55
- getComponent: (name) => files.components[name],
56
- packageName: undefined,
57
- globalFormulas: {
58
- formulas: files.formulas,
59
- packages: files.packages,
60
- },
61
- })
62
- for (const { formula } of c.formulasInComponent()) {
63
- if (formula.type === 'function') {
64
- usedFormulas.add(formula.name)
65
- }
66
- }
66
+ const componentFormulaReferences = memo('componentFormulaReferences', () => {
67
+ const usedFormulas = new Set<string>()
68
+ for (const component of Object.values(files.components)) {
69
+ const c = new ToddleComponent({
70
+ // Enforce that the component is not undefined since we're iterating
71
+ component: component!,
72
+ getComponent: (name) => files.components[name],
73
+ packageName: undefined,
74
+ globalFormulas: {
75
+ formulas: files.formulas,
76
+ packages: files.packages,
77
+ },
78
+ })
79
+ for (const { formula } of c.formulasInComponent()) {
80
+ if (formula.type === 'function') {
81
+ usedFormulas.add(formula.name)
67
82
  }
68
- return usedFormulas
69
- },
70
- )
71
-
72
- if (componentFormulaReferences.has(value.name)) {
73
- return
83
+ }
74
84
  }
85
+ return usedFormulas
86
+ })
75
87
 
76
- // TODO: Memoize similar to above. We need have a helper class `ToddleFormula` with `ToddleFormula.normalizeFormulas()`
77
- for (const f of Object.values(files.formulas ?? {})) {
78
- if (f.name === value.name) {
79
- continue
80
- }
88
+ if (componentFormulaReferences.has(value.name)) {
89
+ return true
90
+ }
81
91
 
82
- // Check if the formula is used in the formula
83
- if (isToddleFormula(f) && checkFormula(f.formula, value.name)) {
84
- return
85
- }
92
+ // TODO: Memoize similar to above. We need have a helper class `ToddleFormula` with `ToddleFormula.normalizeFormulas()`
93
+ for (const f of Object.values(files.formulas ?? {})) {
94
+ if (f.name === value.name) {
95
+ continue
86
96
  }
87
97
 
88
- report(path)
89
- },
98
+ // Check if the formula is used in the formula
99
+ if (isToddleFormula(f) && checkFormula(f.formula, value.name)) {
100
+ return true
101
+ }
102
+ }
103
+ return false
90
104
  }
91
105
 
92
106
  const checkArguments = (
package/src/types.d.ts CHANGED
@@ -21,8 +21,10 @@ import type {
21
21
  Route,
22
22
  ToddleProject,
23
23
  } from '@nordcraft/ssr/dist/ssr.types'
24
+ import type { LegacyActionRuleFix } from './rules/actions/legacyActionRule'
24
25
  import type { NoReferenceComponentRuleFix } from './rules/components/noReferenceComponentRule'
25
26
  import type { LegacyFormulaRuleFix } from './rules/formulas/legacyFormulaRule'
27
+ import type { NoReferenceProjectFormulaRuleFix } from './rules/formulas/noReferenceProjectFormulaRule'
26
28
 
27
29
  type Code =
28
30
  | 'ambiguous style variable syntax'
@@ -311,7 +313,11 @@ export type NodeType =
311
313
  | ProjectRoute
312
314
  | StyleVariantNode
313
315
 
314
- type FixType = LegacyFormulaRuleFix | NoReferenceComponentRuleFix
316
+ type FixType =
317
+ | LegacyFormulaRuleFix
318
+ | LegacyActionRuleFix
319
+ | NoReferenceComponentRuleFix
320
+ | NoReferenceProjectFormulaRuleFix
315
321
 
316
322
  export interface Rule<T = unknown, V = NodeType> {
317
323
  category: Category
@@ -1,7 +1,10 @@
1
1
  import type {
2
2
  ActionModel,
3
+ CustomActionArgument,
4
+ CustomActionModel,
3
5
  ElementNodeModel,
4
6
  } from '@nordcraft/core/dist/component/component.types'
7
+ import type { FunctionArgument } from '@nordcraft/core/dist/formula/formula'
5
8
  import { isDefined } from '@nordcraft/core/dist/utils/util'
6
9
 
7
10
  /**
@@ -44,7 +47,9 @@ export function shouldSearchExactPath({
44
47
  )
45
48
  }
46
49
 
47
- export const isLegacyAction = (model: ActionModel) => {
50
+ export const isLegacyAction = (
51
+ model: ActionModel,
52
+ ): model is CustomActionModel => {
48
53
  switch (model.type) {
49
54
  case 'Custom':
50
55
  case undefined:
@@ -58,14 +63,11 @@ const LEGACY_CUSTOM_ACTIONS = new Set([
58
63
  'If',
59
64
  'PreventDefault',
60
65
  'StopPropagation',
61
- 'Copy To Clipboard',
62
66
  'CopyToClipboard',
63
67
  'UpdateVariable',
64
- 'Update Variable',
65
68
  'Update URL parameter',
66
69
  'updateUrlParameters',
67
70
  'UpdateQueryParam',
68
- 'Update Query',
69
71
  'Fetch',
70
72
  'SetTimeout',
71
73
  'SetInterval',
@@ -74,6 +76,7 @@ const LEGACY_CUSTOM_ACTIONS = new Set([
74
76
  'GoToURL',
75
77
  'TriggerEvent',
76
78
  'Set session cookies',
79
+ '@toddle/setSessionCookies',
77
80
  ])
78
81
 
79
82
  interface BaseInteractiveContent {
@@ -130,3 +133,24 @@ export const interactiveContentElementDefinition = (
130
133
  }
131
134
  return true
132
135
  })
136
+
137
+ export const ARRAY_ARGUMENT_MAPPINGS = { List: 'Array' }
138
+ export const PREDICATE_ARGUMENT_MAPPINGS = {
139
+ ...ARRAY_ARGUMENT_MAPPINGS,
140
+ 'Predicate fx': 'Formula',
141
+ }
142
+
143
+ export const renameArguments = <
144
+ T extends FunctionArgument | CustomActionArgument,
145
+ >(
146
+ mappings: Record<string, string>,
147
+ args: T[] | undefined,
148
+ ): T[] =>
149
+ args?.map((arg) => ({
150
+ ...arg,
151
+ // Let's adjust the names
152
+ name:
153
+ typeof arg.name === 'string'
154
+ ? (mappings[arg.name] ?? arg.name)
155
+ : arg.name,
156
+ })) ?? []