@nordcraft/core 2.0.7 → 2.0.8

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 (36) hide show
  1. package/dist/api/LegacyToddleApi.d.ts +1 -0
  2. package/dist/api/LegacyToddleApi.js +3 -0
  3. package/dist/api/LegacyToddleApi.js.map +1 -1
  4. package/dist/api/ToddleApiV2.d.ts +2 -1
  5. package/dist/api/ToddleApiV2.js +3 -0
  6. package/dist/api/ToddleApiV2.js.map +1 -1
  7. package/dist/api/api.d.ts +2 -4
  8. package/dist/api/api.js +1 -42
  9. package/dist/api/api.js.map +1 -1
  10. package/dist/api/apiTypes.d.ts +2 -0
  11. package/dist/api/apiTypes.js.map +1 -1
  12. package/dist/component/ToddleComponent.d.ts +0 -2
  13. package/dist/component/ToddleComponent.js +7 -21
  14. package/dist/component/ToddleComponent.js.map +1 -1
  15. package/dist/component/component.types.d.ts +2 -0
  16. package/dist/component/component.types.js.map +1 -1
  17. package/dist/component/schemas/api-schema.js +5 -0
  18. package/dist/component/schemas/api-schema.js.map +1 -1
  19. package/dist/styling/className.d.ts +4 -1
  20. package/dist/styling/className.js +4 -1
  21. package/dist/styling/className.js.map +1 -1
  22. package/dist/styling/style.css.js.map +1 -1
  23. package/package.json +1 -1
  24. package/src/api/LegacyToddleApi.ts +3 -0
  25. package/src/api/ToddleApiV2.ts +5 -1
  26. package/src/api/api.test.ts +47 -0
  27. package/src/api/api.ts +3 -63
  28. package/src/api/apiTypes.ts +2 -0
  29. package/src/component/ToddleComponent.subComponents.test.ts +219 -0
  30. package/src/component/ToddleComponent.ts +10 -54
  31. package/src/component/component.types.ts +5 -0
  32. package/src/component/schemas/api-schema.ts +5 -0
  33. package/src/styling/className.test.ts +16 -1
  34. package/src/styling/className.ts +15 -2
  35. package/src/styling/style.css.ts +2 -3
  36. package/src/component/ToddleComponent.actionReferences.test.ts +0 -112
@@ -7,10 +7,13 @@ import {
7
7
  getRequestPath,
8
8
  getRequestQueryParams,
9
9
  getUrl,
10
+ isLegacyApi,
10
11
  toFormData,
11
12
  } from './api'
12
13
  import type { ApiRequest } from './apiTypes'
13
14
  import { ApiMethod } from './apiTypes'
15
+ import { LegacyToddleApi } from './LegacyToddleApi'
16
+ import { ToddleApiV2 } from './ToddleApiV2'
14
17
 
15
18
  describe('getApiPath()', () => {
16
19
  test('it returns a valid url path string', () => {
@@ -486,3 +489,47 @@ describe('toFormData()', () => {
486
489
  expect(formData.get('valid')).toBe('working')
487
490
  })
488
491
  })
492
+ describe('isLegacyApi()', () => {
493
+ test('it checks if an API is legacy', () => {
494
+ expect(
495
+ isLegacyApi({
496
+ name: 'Legacy API',
497
+ type: 'REST',
498
+ }),
499
+ ).toBe(true)
500
+ expect(
501
+ isLegacyApi({
502
+ name: 'New API',
503
+ type: 'http',
504
+ version: 2,
505
+ inputs: {},
506
+ }),
507
+ ).toBe(false)
508
+ expect(
509
+ isLegacyApi(
510
+ new LegacyToddleApi(
511
+ {
512
+ name: 'Legacy API',
513
+ type: 'REST',
514
+ },
515
+ 'myLegacyApi',
516
+ {},
517
+ ),
518
+ ),
519
+ ).toBe(true)
520
+ expect(
521
+ isLegacyApi(
522
+ new ToddleApiV2(
523
+ {
524
+ name: 'New API',
525
+ type: 'http',
526
+ version: 2,
527
+ inputs: {},
528
+ },
529
+ 'myNewApi',
530
+ {},
531
+ ),
532
+ ),
533
+ ).toBe(false)
534
+ })
535
+ })
package/src/api/api.ts CHANGED
@@ -14,15 +14,14 @@ import type {
14
14
  } from './apiTypes'
15
15
  import { ApiMethod } from './apiTypes'
16
16
  import { isJsonHeader } from './headers'
17
- import { LegacyToddleApi } from './LegacyToddleApi'
18
- import { ToddleApiV2 } from './ToddleApiV2'
17
+ import type { LegacyToddleApi } from './LegacyToddleApi'
18
+ import type { ToddleApiV2 } from './ToddleApiV2'
19
19
 
20
20
  export const NON_BODY_RESPONSE_CODES = [101, 204, 205, 304]
21
21
 
22
22
  export const isLegacyApi = <Handler>(
23
23
  api: ComponentAPI | LegacyToddleApi<Handler> | ToddleApiV2<Handler>,
24
- ): api is LegacyComponentAPI | LegacyToddleApi<Handler> =>
25
- api instanceof LegacyToddleApi ? true : !('version' in api)
24
+ ): api is LegacyComponentAPI | LegacyToddleApi<Handler> => !('version' in api)
26
25
 
27
26
  export const createApiRequest = <Handler>({
28
27
  api,
@@ -420,62 +419,3 @@ export const createApiEvent = (
420
419
  detail,
421
420
  })
422
421
  }
423
-
424
- const compareApiDependencies = <Handler>(
425
- a: LegacyToddleApi<Handler> | ToddleApiV2<Handler>,
426
- b: LegacyToddleApi<Handler> | ToddleApiV2<Handler>,
427
- ) => {
428
- const isADependentOnB = a.apiReferences.has(b.name)
429
- const isBDependentOnA = b.apiReferences.has(a.name)
430
- if (isADependentOnB === isBDependentOnA) {
431
- return 0
432
- }
433
- // 1 means A goes last - hence B is evaluated before A
434
- return isADependentOnB ? 1 : -1
435
- }
436
-
437
- export const sortApiObjects = <Handler>(
438
- apis: Array<[string, ComponentAPI]>,
439
- ) => {
440
- const apiMap = new Map<
441
- string,
442
- LegacyToddleApi<Handler> | ToddleApiV2<Handler>
443
- >()
444
- const getApi = (apiObj: ComponentAPI, key: string) => {
445
- let api = apiMap.get(key)
446
- if (!api) {
447
- api =
448
- apiObj.version === 2
449
- ? new ToddleApiV2<Handler>(
450
- apiObj,
451
- key,
452
- // global formulas are not required for sorting
453
- {
454
- formulas: {},
455
- packages: {},
456
- },
457
- )
458
- : new LegacyToddleApi<Handler>(
459
- apiObj,
460
- key,
461
- // global formulas are not required for sorting
462
- {
463
- formulas: {},
464
- packages: {},
465
- },
466
- )
467
- apiMap.set(key, api)
468
- }
469
- return api
470
- }
471
-
472
- return [...apis].sort(([aKey, aObj], [bKey, bObj]) => {
473
- const a = getApi(aObj, aKey)
474
- const b = getApi(bObj, bKey)
475
- return compareApiDependencies(a, b)
476
- })
477
- }
478
-
479
- export const sortApiEntries = <Handler>(
480
- apis: Array<[string, LegacyToddleApi<Handler> | ToddleApiV2<Handler>]>,
481
- ) => [...apis].sort(([_, a], [__, b]) => compareApiDependencies(a, b))
@@ -23,6 +23,7 @@ export interface LegacyComponentAPI {
23
23
  onCompleted?: Nullable<EventModel>
24
24
  onFailed?: Nullable<EventModel>
25
25
  version?: never
26
+ dependsOn?: string[]
26
27
  }
27
28
 
28
29
  export interface LegacyApiStatus {
@@ -137,6 +138,7 @@ export interface ApiRequest extends ApiBase {
137
138
  isError?: Nullable<{ formula: Formula }>
138
139
  // Formula for determining when the request should time out
139
140
  timeout?: Nullable<{ formula: Formula }>
141
+ dependsOn?: string[]
140
142
  }
141
143
 
142
144
  export interface ApiStatus {
@@ -0,0 +1,219 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { ToddleComponent } from './ToddleComponent'
3
+ import type { Component } from './component.types'
4
+
5
+ describe('ToddleComponent.uniqueSubComponents', () => {
6
+ test('it should pass down the package name to sub-components', () => {
7
+ const packagedComp: Component = {
8
+ name: 'PackagedComp',
9
+ nodes: {
10
+ node1: {
11
+ type: 'component',
12
+ name: 'InternalComp',
13
+ },
14
+ },
15
+ apis: {},
16
+ attributes: {},
17
+ variables: {},
18
+ workflows: {},
19
+ }
20
+
21
+ const internalComp: Component = {
22
+ name: 'InternalComp',
23
+ nodes: {},
24
+ apis: {},
25
+ attributes: {},
26
+ variables: {},
27
+ workflows: {},
28
+ }
29
+
30
+ const root: Component = {
31
+ name: 'root',
32
+ nodes: {
33
+ node1: {
34
+ type: 'component',
35
+ name: 'PackagedComp',
36
+ package: 'my-package',
37
+ },
38
+ },
39
+ apis: {},
40
+ attributes: {},
41
+ variables: {},
42
+ workflows: {},
43
+ }
44
+
45
+ const componentsMap: Record<string, Component> = {
46
+ 'my-package/PackagedComp': packagedComp,
47
+ 'my-package/InternalComp': internalComp,
48
+ }
49
+
50
+ const getComponent = (name: string, packageName?: string) => {
51
+ const key = packageName ? `${packageName}/${name}` : name
52
+ return componentsMap[key]
53
+ }
54
+
55
+ const demo = new ToddleComponent({
56
+ component: root,
57
+ getComponent,
58
+ packageName: undefined,
59
+ globalFormulas: { formulas: {}, packages: {} },
60
+ })
61
+
62
+ const subComponents = demo.uniqueSubComponents
63
+ expect(subComponents).toHaveLength(2)
64
+ expect(subComponents[0].name).toBe('PackagedComp')
65
+ expect(subComponents[0].packageName).toBe('my-package')
66
+ expect(subComponents[1].name).toBe('InternalComp')
67
+ expect(subComponents[1].packageName).toBe('my-package')
68
+ })
69
+
70
+ test('it should handle nested components and avoid duplicates', () => {
71
+ const compA: Component = {
72
+ name: 'CompA',
73
+ nodes: {
74
+ node1: {
75
+ type: 'component',
76
+ name: 'CompB',
77
+ },
78
+ },
79
+ apis: {},
80
+ attributes: {},
81
+ variables: {},
82
+ workflows: {},
83
+ }
84
+
85
+ const compB: Component = {
86
+ name: 'CompB',
87
+ nodes: {},
88
+ apis: {},
89
+ attributes: {},
90
+ variables: {},
91
+ workflows: {},
92
+ }
93
+
94
+ const root: Component = {
95
+ name: 'root',
96
+ nodes: {
97
+ node1: {
98
+ type: 'component',
99
+ name: 'CompA',
100
+ },
101
+ node2: {
102
+ type: 'component',
103
+ name: 'CompB',
104
+ },
105
+ },
106
+ apis: {},
107
+ attributes: {},
108
+ variables: {},
109
+ workflows: {},
110
+ }
111
+
112
+ const componentsMap: Record<string, Component> = {
113
+ CompA: compA,
114
+ CompB: compB,
115
+ }
116
+
117
+ const getComponent = (name: string, packageName?: string) => {
118
+ const key = packageName ? `${packageName}/${name}` : name
119
+ return componentsMap[key]
120
+ }
121
+
122
+ const demo = new ToddleComponent({
123
+ component: root,
124
+ getComponent,
125
+ packageName: undefined,
126
+ globalFormulas: { formulas: {}, packages: {} },
127
+ })
128
+
129
+ const subComponents = demo.uniqueSubComponents
130
+ expect(subComponents).toHaveLength(2)
131
+ const names = subComponents.map((c) => c.name).sort()
132
+ expect(names).toEqual(['CompA', 'CompB'])
133
+ })
134
+
135
+ test('it should allow sub-components to override the package name', () => {
136
+ const pkgAComp: Component = {
137
+ name: 'PkgAComp',
138
+ nodes: {
139
+ node1: {
140
+ type: 'component',
141
+ name: 'PkgBComp',
142
+ package: 'package-b',
143
+ },
144
+ },
145
+ apis: {},
146
+ attributes: {},
147
+ variables: {},
148
+ workflows: {},
149
+ }
150
+
151
+ const pkgBComp: Component = {
152
+ name: 'PkgBComp',
153
+ nodes: {
154
+ node1: {
155
+ type: 'component',
156
+ name: 'InternalComp',
157
+ },
158
+ },
159
+ apis: {},
160
+ attributes: {},
161
+ variables: {},
162
+ workflows: {},
163
+ }
164
+
165
+ const internalComp: Component = {
166
+ name: 'InternalComp',
167
+ nodes: {},
168
+ apis: {},
169
+ attributes: {},
170
+ variables: {},
171
+ workflows: {},
172
+ }
173
+
174
+ const root: Component = {
175
+ name: 'root',
176
+ nodes: {
177
+ node1: {
178
+ type: 'component',
179
+ name: 'PkgAComp',
180
+ package: 'package-a',
181
+ },
182
+ },
183
+ apis: {},
184
+ attributes: {},
185
+ variables: {},
186
+ workflows: {},
187
+ }
188
+
189
+ const componentsMap: Record<string, Component> = {
190
+ 'package-a/PkgAComp': pkgAComp,
191
+ 'package-b/PkgBComp': pkgBComp,
192
+ 'package-b/InternalComp': internalComp,
193
+ }
194
+
195
+ const getComponent = (name: string, packageName?: string) => {
196
+ const key = packageName ? `${packageName}/${name}` : name
197
+ return componentsMap[key]
198
+ }
199
+
200
+ const demo = new ToddleComponent({
201
+ component: root,
202
+ getComponent,
203
+ packageName: undefined,
204
+ globalFormulas: { formulas: {}, packages: {} },
205
+ })
206
+
207
+ const subComponents = demo.uniqueSubComponents
208
+ expect(subComponents).toHaveLength(3)
209
+
210
+ const pA = subComponents.find((c) => c.name === 'PkgAComp')
211
+ expect(pA?.packageName).toBe('package-a')
212
+
213
+ const pB = subComponents.find((c) => c.name === 'PkgBComp')
214
+ expect(pB?.packageName).toBe('package-b')
215
+
216
+ const internal = subComponents.find((c) => c.name === 'InternalComp')
217
+ expect(internal?.packageName).toBe('package-b')
218
+ })
219
+ })
@@ -2,7 +2,7 @@ import { isLegacyApi } from '../api/api'
2
2
  import type { ComponentAPI } from '../api/apiTypes'
3
3
  import { LegacyToddleApi } from '../api/LegacyToddleApi'
4
4
  import { ToddleApiV2 } from '../api/ToddleApiV2'
5
- import type { Formula, FunctionOperation } from '../formula/formula'
5
+ import type { Formula } from '../formula/formula'
6
6
  import type { GlobalFormulas } from '../formula/formulaTypes'
7
7
  import {
8
8
  getFormulasInAction,
@@ -10,12 +10,7 @@ import {
10
10
  } from '../formula/formulaUtils'
11
11
  import { isDefined } from '../utils/util'
12
12
  import { getActionsInAction } from './actionUtils'
13
- import type {
14
- ActionModel,
15
- Component,
16
- CustomActionModel,
17
- NodeModel,
18
- } from './component.types'
13
+ import type { ActionModel, Component, NodeModel } from './component.types'
19
14
  import { isPageComponent } from './isPageComponent'
20
15
 
21
16
  export class ToddleComponent<Handler> {
@@ -54,10 +49,8 @@ export class ToddleComponent<Handler> {
54
49
  if (components.has(node.name)) {
55
50
  return
56
51
  }
57
- const component = this.getComponent(
58
- node.name,
59
- node.package ?? packageName,
60
- )
52
+ const componentPackageName = node.package ?? packageName
53
+ const component = this.getComponent(node.name, componentPackageName)
61
54
  if (!component) {
62
55
  return
63
56
  }
@@ -66,13 +59,17 @@ export class ToddleComponent<Handler> {
66
59
  new ToddleComponent({
67
60
  component,
68
61
  getComponent: this.getComponent,
69
- packageName: node.package ?? packageName,
62
+ packageName: componentPackageName,
70
63
  globalFormulas: this.globalFormulas,
71
64
  }),
72
65
  )
73
66
  Object.values(component.nodes ?? {}).forEach((node) => {
74
67
  if (isDefined(node)) {
75
- visitNode((node as any).package ?? packageName)(node)
68
+ const nodePackageName =
69
+ (node.type === 'component' ? node.package : undefined) ??
70
+ componentPackageName ??
71
+ packageName
72
+ visitNode(nodePackageName)(node)
76
73
  }
77
74
  })
78
75
  }
@@ -84,47 +81,6 @@ export class ToddleComponent<Handler> {
84
81
  return [...components.values()]
85
82
  }
86
83
 
87
- get formulaReferences() {
88
- return new Set(
89
- Array.from(this.formulasInComponent())
90
- .filter(
91
- (
92
- entry,
93
- ): entry is {
94
- path: (string | number)[]
95
- formula: FunctionOperation
96
- packageName?: string
97
- } => entry.formula.type === 'function',
98
- )
99
- .flatMap((entry) => {
100
- const refs = [entry.formula.name]
101
- const packageName =
102
- entry.formula.package ?? entry.packageName ?? this.packageName
103
- if (
104
- packageName &&
105
- this.globalFormulas.packages?.[packageName]?.formulas?.[
106
- entry.formula.name
107
- ]
108
- ) {
109
- refs.push([packageName, entry.formula.name].join('/'))
110
- }
111
-
112
- return refs
113
- }),
114
- )
115
- }
116
-
117
- get actionReferences(): Set<string> {
118
- return new Set(
119
- Array.from(this.actionModelsInComponent())
120
- .filter(
121
- (entry): entry is [(string | number)[], CustomActionModel] =>
122
- entry[1].type === 'Custom' || entry[1].type === undefined,
123
- )
124
- .map(([, a]) => [a.package, a.name].filter(isDefined).join('/')),
125
- )
126
- }
127
-
128
84
  /**
129
85
  * Traverse all formulas in the component.
130
86
  * @returns An iterable that yields the path and formula.
@@ -206,6 +206,8 @@ export interface Component {
206
206
  // Later, we will add information about allowed origins here
207
207
  enabled?: Nullable<Formula>
208
208
  }>
209
+ // Used to indicate if a page or any of its child components use custom actions/formulas
210
+ customCode?: Nullable<boolean>
209
211
  }
210
212
 
211
213
  export interface ComponentFormula extends NordcraftMetadata {
@@ -418,6 +420,9 @@ export interface ComponentEvent extends NordcraftMetadata {
418
420
 
419
421
  export interface ComponentVariable extends NordcraftMetadata {
420
422
  initialValue: Formula
423
+ // @deprecated - variables should be referenced by their key in component.variables
424
+ // name is only here to better reflect how variables are stored in legacy components
425
+ name?: never
421
426
  }
422
427
 
423
428
  export interface ComponentAttribute extends NordcraftMetadata {
@@ -210,6 +210,10 @@ const ApiRequestSchema: z.ZodType<ApiRequest> = z
210
210
  .describe(
211
211
  'Rules for redirecting based on response data. The key is a unique identifier for the rule.',
212
212
  ),
213
+ dependsOn: z
214
+ .array(z.string())
215
+ .optional()
216
+ .describe('List of APIs that this API depends on.'),
213
217
  })
214
218
  .describe('Schema defining an API request from a component or a page.')
215
219
 
@@ -244,6 +248,7 @@ const LegacyComponentAPISchema: z.ZodType<LegacyComponentAPI> = z
244
248
  type: z.enum(['Bearer id_token', 'Bearer access_token']),
245
249
  })
246
250
  .nullish(),
251
+ dependsOn: z.array(z.string()).optional(),
247
252
  })
248
253
  .describe(
249
254
  'Legacy API schema for backward compatibility. Never use this for new APIs.',
@@ -1,4 +1,5 @@
1
- import { toValidClassName } from './className'
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { getClassName, toValidClassName } from './className'
2
3
 
3
4
  describe('toValidClassName()', () => {
4
5
  test('it trims leading and trailing whitespace and replace the remaining whitespace with hyphens', () => {
@@ -17,3 +18,17 @@ describe('toValidClassName()', () => {
17
18
  expect(toValidClassName('1class')).toBe('_1class')
18
19
  })
19
20
  })
21
+
22
+ describe('getClassName()', () => {
23
+ test('it produces the same classname whether a nullish entry is provided or not', () => {
24
+ expect(getClassName([null, undefined, { color: 'red' }])).toBe(
25
+ getClassName([{ color: 'red' }]),
26
+ )
27
+ })
28
+
29
+ test('it produces the same classname whether an empty object is provided or not', () => {
30
+ expect(getClassName([{}, { color: 'red' }])).toBe(
31
+ getClassName([{ color: 'red' }]),
32
+ )
33
+ })
34
+ })
@@ -1,9 +1,22 @@
1
+ import type { NodeStyleModel } from '../component/component.types'
2
+ import type { Nullable } from '../types'
1
3
  import { generateAlphabeticName, hash } from './hash'
4
+ import type { StyleVariant } from './variantSelector'
2
5
 
3
6
  // Classnames are reused a lot, and JS hashing is expensive, so there is benefit in caching them in a native hashmap.
4
7
  const CLASSNAME_LOOKUP = new Map<string, string>()
5
- export const getClassName = (object: any) => {
6
- const stringified = JSON.stringify(object)
8
+ export const getClassName = (
9
+ object: [Nullable<NodeStyleModel>, Nullable<StyleVariant[]>],
10
+ ) => {
11
+ const stringified = JSON.stringify(
12
+ object.filter(
13
+ (item) =>
14
+ item !== null && // Skip nullish values
15
+ item !== undefined &&
16
+ (Array.isArray(item) ? item.length > 0 : true) && // Skip empty arrays/objects
17
+ (typeof item === 'object' ? Object.keys(item).length > 0 : true),
18
+ ),
19
+ )
7
20
  if (CLASSNAME_LOOKUP.has(stringified)) {
8
21
  return CLASSNAME_LOOKUP.get(stringified)!
9
22
  }
@@ -284,11 +284,10 @@ export const createStylesheet = (
284
284
  if (childComponent) {
285
285
  insertComponentStyles(childComponent, node.package ?? package_name)
286
286
  stylesheet += getNodeStyles(
287
- node as any,
287
+ node,
288
288
  toValidClassName(`${component.name}:${id}`, true),
289
289
  animationHashes,
290
290
  )
291
-
292
291
  return
293
292
  }
294
293
  }
@@ -300,7 +299,7 @@ export const createStylesheet = (
300
299
  return ''
301
300
  }
302
301
  hashes.add(classHash)
303
- stylesheet += getNodeStyles(node as any, classHash, animationHashes)
302
+ stylesheet += getNodeStyles(node, classHash, animationHashes)
304
303
  })
305
304
  }
306
305
  insertComponentStyles(root)