@nordcraft/core 1.0.0

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 (119) hide show
  1. package/README.md +5 -0
  2. package/dist/api/LegacyToddleApi.d.ts +34 -0
  3. package/dist/api/LegacyToddleApi.js +178 -0
  4. package/dist/api/LegacyToddleApi.js.map +1 -0
  5. package/dist/api/ToddleApiV2.d.ts +77 -0
  6. package/dist/api/ToddleApiV2.js +271 -0
  7. package/dist/api/ToddleApiV2.js.map +1 -0
  8. package/dist/api/api.d.ts +49 -0
  9. package/dist/api/api.js +276 -0
  10. package/dist/api/api.js.map +1 -0
  11. package/dist/api/apiTypes.d.ts +125 -0
  12. package/dist/api/apiTypes.js +11 -0
  13. package/dist/api/apiTypes.js.map +1 -0
  14. package/dist/api/headers.d.ts +10 -0
  15. package/dist/api/headers.js +36 -0
  16. package/dist/api/headers.js.map +1 -0
  17. package/dist/api/template.d.ts +5 -0
  18. package/dist/api/template.js +7 -0
  19. package/dist/api/template.js.map +1 -0
  20. package/dist/component/ToddleComponent.d.ts +45 -0
  21. package/dist/component/ToddleComponent.js +373 -0
  22. package/dist/component/ToddleComponent.js.map +1 -0
  23. package/dist/component/actionUtils.d.ts +2 -0
  24. package/dist/component/actionUtils.js +56 -0
  25. package/dist/component/actionUtils.js.map +1 -0
  26. package/dist/component/component.types.d.ts +313 -0
  27. package/dist/component/component.types.js +9 -0
  28. package/dist/component/component.types.js.map +1 -0
  29. package/dist/component/isPageComponent.d.ts +2 -0
  30. package/dist/component/isPageComponent.js +3 -0
  31. package/dist/component/isPageComponent.js.map +1 -0
  32. package/dist/formula/ToddleFormula.d.ts +15 -0
  33. package/dist/formula/ToddleFormula.js +20 -0
  34. package/dist/formula/ToddleFormula.js.map +1 -0
  35. package/dist/formula/formula.d.ts +103 -0
  36. package/dist/formula/formula.js +186 -0
  37. package/dist/formula/formula.js.map +1 -0
  38. package/dist/formula/formulaTypes.d.ts +30 -0
  39. package/dist/formula/formulaTypes.js +2 -0
  40. package/dist/formula/formulaTypes.js.map +1 -0
  41. package/dist/formula/formulaUtils.d.ts +18 -0
  42. package/dist/formula/formulaUtils.js +240 -0
  43. package/dist/formula/formulaUtils.js.map +1 -0
  44. package/dist/styling/className.d.ts +2 -0
  45. package/dist/styling/className.js +52 -0
  46. package/dist/styling/className.js.map +1 -0
  47. package/dist/styling/style.css.d.ts +5 -0
  48. package/dist/styling/style.css.js +242 -0
  49. package/dist/styling/style.css.js.map +1 -0
  50. package/dist/styling/theme.const.d.ts +3 -0
  51. package/dist/styling/theme.const.js +381 -0
  52. package/dist/styling/theme.const.js.map +1 -0
  53. package/dist/styling/theme.d.ts +72 -0
  54. package/dist/styling/theme.js +200 -0
  55. package/dist/styling/theme.js.map +1 -0
  56. package/dist/styling/variantSelector.d.ts +123 -0
  57. package/dist/styling/variantSelector.js +25 -0
  58. package/dist/styling/variantSelector.js.map +1 -0
  59. package/dist/types.d.ts +97 -0
  60. package/dist/types.js +2 -0
  61. package/dist/types.js.map +1 -0
  62. package/dist/utils/collections.d.ts +21 -0
  63. package/dist/utils/collections.js +75 -0
  64. package/dist/utils/collections.js.map +1 -0
  65. package/dist/utils/customElements.d.ts +6 -0
  66. package/dist/utils/customElements.js +15 -0
  67. package/dist/utils/customElements.js.map +1 -0
  68. package/dist/utils/hash.d.ts +1 -0
  69. package/dist/utils/hash.js +17 -0
  70. package/dist/utils/hash.js.map +1 -0
  71. package/dist/utils/json.d.ts +5 -0
  72. package/dist/utils/json.js +16 -0
  73. package/dist/utils/json.js.map +1 -0
  74. package/dist/utils/sha1.d.ts +2 -0
  75. package/dist/utils/sha1.js +13 -0
  76. package/dist/utils/sha1.js.map +1 -0
  77. package/dist/utils/url.d.ts +5 -0
  78. package/dist/utils/url.js +27 -0
  79. package/dist/utils/url.js.map +1 -0
  80. package/dist/utils/util.d.ts +3 -0
  81. package/dist/utils/util.js +4 -0
  82. package/dist/utils/util.js.map +1 -0
  83. package/package.json +18 -0
  84. package/src/api/LegacyToddleApi.ts +205 -0
  85. package/src/api/ToddleApiV2.ts +331 -0
  86. package/src/api/api.test.ts +319 -0
  87. package/src/api/api.ts +414 -0
  88. package/src/api/apiTypes.ts +145 -0
  89. package/src/api/headers.ts +41 -0
  90. package/src/api/template.ts +10 -0
  91. package/src/component/ToddleComponent.actionReferences.test.ts +75 -0
  92. package/src/component/ToddleComponent.formulasInComponent.test.ts +234 -0
  93. package/src/component/ToddleComponent.ts +470 -0
  94. package/src/component/actionUtils.ts +61 -0
  95. package/src/component/component.types.ts +362 -0
  96. package/src/component/isPageComponent.ts +6 -0
  97. package/src/formula/ToddleFormula.ts +30 -0
  98. package/src/formula/formula.ts +355 -0
  99. package/src/formula/formulaTypes.ts +45 -0
  100. package/src/formula/formulaUtils.ts +287 -0
  101. package/src/styling/className.test.ts +19 -0
  102. package/src/styling/className.ts +73 -0
  103. package/src/styling/style.css.ts +309 -0
  104. package/src/styling/theme.const.ts +390 -0
  105. package/src/styling/theme.ts +339 -0
  106. package/src/styling/variantSelector.ts +168 -0
  107. package/src/types.ts +158 -0
  108. package/src/utils/collections.test.ts +57 -0
  109. package/src/utils/collections.ts +122 -0
  110. package/src/utils/customElements.test.ts +40 -0
  111. package/src/utils/customElements.ts +16 -0
  112. package/src/utils/hash.test.ts +32 -0
  113. package/src/utils/hash.ts +18 -0
  114. package/src/utils/json.ts +18 -0
  115. package/src/utils/sha1.test.ts +50 -0
  116. package/src/utils/sha1.ts +17 -0
  117. package/src/utils/url.test.ts +17 -0
  118. package/src/utils/url.ts +33 -0
  119. package/src/utils/util.ts +8 -0
@@ -0,0 +1,122 @@
1
+ import { isDefined } from './util'
2
+
3
+ export const isObject = (input: any): input is Record<string, any> =>
4
+ typeof input === 'object' && input !== null
5
+
6
+ export const mapObject = <T, T2>(
7
+ object: Record<string, T>,
8
+ f: (kv: [string, T]) => [string, T2],
9
+ ): Record<string, T2> => Object.fromEntries(Object.entries(object).map(f))
10
+
11
+ export const mapValues = <T, T2>(
12
+ object: Record<string, T>,
13
+ f: (value: T) => T2,
14
+ ): Record<string, T2> => mapObject(object, ([key, value]) => [key, f(value)])
15
+
16
+ /**
17
+ * Deletes potentially nested keys from an object
18
+ * @param collection Array or Object
19
+ * @param path Path to the key to delete. For instance ['foo', 0, 'bar']
20
+ * @returns The updated object/array
21
+ */
22
+ export const omit = <T = unknown>(collection: T, path: string[]): T => {
23
+ const [head, ...rest] = path
24
+
25
+ const clone: any = Array.isArray(collection)
26
+ ? [...collection]
27
+ : isObject(collection)
28
+ ? { ...collection }
29
+ : {}
30
+
31
+ if (rest.length === 0) {
32
+ delete clone[head]
33
+ } else {
34
+ clone[head] = omit(clone[head], rest)
35
+ }
36
+ return clone as T
37
+ }
38
+
39
+ export const omitKeys = <T extends Record<string, any>>(
40
+ object: T,
41
+ keys: Array<keyof T>,
42
+ ): T =>
43
+ Object.fromEntries(
44
+ Object.entries(object).filter(([k]) => !keys.includes(k)),
45
+ ) as T
46
+
47
+ export const omitPaths = (object: Record<string, any>, keys: string[][]) =>
48
+ keys.reduce((acc, key) => omit(acc, key), { ...object })
49
+
50
+ export const groupBy = <T>(items: T[], f: (t: T) => string) =>
51
+ items.reduce<Record<string, T[]>>((acc, item) => {
52
+ const key = f(item)
53
+ acc[key] = acc[key] ?? []
54
+ acc[key].push(item)
55
+ return acc
56
+ }, {})
57
+
58
+ export const filterObject = <T>(
59
+ object: Record<string, T>,
60
+ f: (kv: [string, T]) => boolean,
61
+ ): Record<string, T> => Object.fromEntries(Object.entries(object).filter(f))
62
+
63
+ export function get<T = any>(collection: T, [head, ...rest]: string[]): any {
64
+ if (rest.length === 0) {
65
+ return (collection as any)?.[head]
66
+ }
67
+ return get((collection as any)?.[head], rest)
68
+ }
69
+
70
+ export const set = <T = unknown>(
71
+ collection: T,
72
+ key: string[],
73
+ value: any,
74
+ ): T => {
75
+ const [head, ...rest] = key
76
+
77
+ const clone: any = Array.isArray(collection)
78
+ ? [...collection]
79
+ : isObject(collection)
80
+ ? { ...collection }
81
+ : {}
82
+
83
+ clone[head] = rest.length === 0 ? value : set(clone[head], rest, value)
84
+ return clone as T
85
+ }
86
+
87
+ export const sortObjectEntries = <T>(
88
+ object: Record<string, T>,
89
+ f: (kv: [string, T]) => string | number | boolean,
90
+ ascending = true,
91
+ ): [string, T][] => easySort(Object.entries(object), f, ascending)
92
+
93
+ export const easySort = <T>(
94
+ collection: T[],
95
+ f: (item: T) => string | number | boolean,
96
+ ascending = true,
97
+ ) =>
98
+ [...collection].sort((a, b) => {
99
+ const keyA = f(a)
100
+ const keyB = f(b)
101
+ if (keyA === keyB) {
102
+ return 0
103
+ }
104
+ return (keyA > keyB ? 1 : -1) * (ascending ? 1 : -1)
105
+ })
106
+
107
+ export const deepSortObject = (
108
+ obj: any,
109
+ ): Record<string, any> | Array<any> | undefined | null => {
110
+ if (!isDefined(obj)) {
111
+ return obj
112
+ }
113
+ if (Array.isArray(obj)) {
114
+ return obj.map((val) => deepSortObject(val))
115
+ } else if (typeof obj === 'object' && Object.keys(obj).length > 0) {
116
+ return [...Object.keys(obj)].sort().reduce<any>((acc, key) => {
117
+ acc[key] = deepSortObject(obj[key])
118
+ return acc
119
+ }, {})
120
+ }
121
+ return obj
122
+ }
@@ -0,0 +1,40 @@
1
+ import { safeCustomElementName } from './customElements'
2
+ // cSpell:ignore mycustomelement, myelement
3
+
4
+ describe('safeCustomElementName', () => {
5
+ test('should remove leading and trailing white-spaces', () => {
6
+ const input = ' MyCustomElement '
7
+ const result = safeCustomElementName(input)
8
+ expect(result).toBe('toddle-mycustomelement')
9
+ })
10
+
11
+ test('should convert input to lowercase', () => {
12
+ const input = 'MyCustomElement'
13
+ const result = safeCustomElementName(input)
14
+ expect(result).toBe('toddle-mycustomelement')
15
+ })
16
+
17
+ test('should remove white-spaces from the middle', () => {
18
+ const input = 'My Custom Element'
19
+ const result = safeCustomElementName(input)
20
+ expect(result).toBe('toddle-mycustomelement')
21
+ })
22
+
23
+ test('should add "toddle-" prefix if no hyphen', () => {
24
+ const input = 'MyElement'
25
+ const result = safeCustomElementName(input)
26
+ expect(result).toBe('toddle-myelement')
27
+ })
28
+
29
+ test('should not add "toddle-" prefix if hyphen is present', () => {
30
+ const input = 'my-custom-element'
31
+ const result = safeCustomElementName(input)
32
+ expect(result).toBe('my-custom-element')
33
+ })
34
+
35
+ test('should remove multiple consecutive white-spaces', () => {
36
+ const input = ' My Custom Element '
37
+ const result = safeCustomElementName(input)
38
+ expect(result).toBe('toddle-mycustomelement')
39
+ })
40
+ })
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Convert a component name to a valid HTML tag name according to the custom-element specs
3
+ *
4
+ * https://html.spec.whatwg.org/multipage/custom-elements.html#custom-element
5
+ */
6
+ export const safeCustomElementName = (name: string) => {
7
+ // Remove any white spaces from the name
8
+ const tag = name.toLocaleLowerCase().replaceAll(' ', '')
9
+
10
+ // Add "toddle-" prefix if needed
11
+ if (!tag.includes('-')) {
12
+ return `toddle-${tag}`
13
+ }
14
+
15
+ return tag
16
+ }
@@ -0,0 +1,32 @@
1
+ import { hash } from './hash'
2
+
3
+ describe('hash', () => {
4
+ test('identical strings should lead to the same hash', () => {
5
+ const data1 = "{ foo: 'bar', time: now }"
6
+ const data2 = "{ foo: 'bar', time: now }"
7
+ const firstHash = hash(data1)
8
+ const secondHash = hash(data2)
9
+ expect(firstHash).toEqual(secondHash)
10
+ })
11
+ test('identical strings with same seed should lead to the same hash', () => {
12
+ const data1 = "{ foo: 'bar', time: now }"
13
+ const data2 = "{ foo: 'bar', time: now }"
14
+ const firstHash = hash(data1, 10)
15
+ const secondHash = hash(data2, 10)
16
+ expect(firstHash).toEqual(secondHash)
17
+ })
18
+ test('identical strings with different seed should lead different hashes', () => {
19
+ const data1 = "{ foo: 'bar', time: now }"
20
+ const data2 = "{ foo: 'bar', time: now }"
21
+ const firstHash = hash(data1, 10)
22
+ const secondHash = hash(data2, 5)
23
+ expect(firstHash).not.toEqual(secondHash)
24
+ })
25
+ test('different strings results in different hash values', () => {
26
+ const data1 = "{ foo: 'bar', time: now }"
27
+ const firstHash = hash(data1)
28
+ const data2 = "{ foo: 'bar', time: new Date(now.getTime() + 1000) }"
29
+ const secondHash = hash(data2)
30
+ expect(firstHash).not.toEqual(secondHash)
31
+ })
32
+ })
@@ -0,0 +1,18 @@
1
+ // Based on https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript/52171480#52171480
2
+ // This is a simple hash function for strings. It's not secure, but it's fast.
3
+ // It's not suitable for cryptographic purposes, but it's great for hash tables.
4
+ export const hash = (data: string, seed = 0) => {
5
+ let h1 = 0xdeadbeef ^ seed,
6
+ h2 = 0x41c6ce57 ^ seed
7
+ for (let i = 0, ch; i < data.length; i++) {
8
+ ch = data.charCodeAt(i)
9
+ h1 = Math.imul(h1 ^ ch, 2654435761)
10
+ h2 = Math.imul(h2 ^ ch, 1597334677)
11
+ }
12
+ h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507)
13
+ h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909)
14
+ h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507)
15
+ h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909)
16
+
17
+ return 4294967296 * (2097151 & h2) + (h1 >>> 0)
18
+ }
@@ -0,0 +1,18 @@
1
+ const iso8601Regex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:.\d{1,3})?Z$/
2
+
3
+ /**
4
+ * This function is much slower than JSON parse without reviver and is a major
5
+ * bottleneck in the runtime performance. Especially during startup.
6
+ */
7
+ export function parseJSONWithDate(input: string) {
8
+ return JSON.parse(input, (_, value) => {
9
+ if (
10
+ typeof value === 'string' &&
11
+ value.length === 24 &&
12
+ iso8601Regex.test(value)
13
+ ) {
14
+ return new Date(value)
15
+ }
16
+ return value
17
+ })
18
+ }
@@ -0,0 +1,50 @@
1
+ import { describe, expect, test } from '@jest/globals'
2
+ import crypto from 'crypto'
3
+ import { deepSortObject } from './collections'
4
+ import { sha1, stableStringify } from './sha1'
5
+ ;(global as any).crypto = crypto
6
+
7
+ describe('sha1', () => {
8
+ test('calculating sha1 of an object works', async () => {
9
+ const data = { foo: 'bar' }
10
+ const sha = await sha1(data)
11
+ expect(sha).toHaveLength(40)
12
+ })
13
+ test('identical objects should lead to the same sha', async () => {
14
+ const now = new Date()
15
+ const data1 = { foo: 'bar', time: now }
16
+ const data2 = { foo: 'bar', time: now }
17
+ const firstSha = await sha1(data1)
18
+ const secondSha = await sha1(data2)
19
+ expect(firstSha).toEqual(secondSha)
20
+ })
21
+ test('variations of an object results in different sha values', async () => {
22
+ const now = new Date()
23
+ const data1 = { foo: 'bar', time: now }
24
+ const firstSha = await sha1(data1)
25
+ const data2 = { foo: 'bar', time: new Date(now.getTime() + 1000) }
26
+ const secondSha = await sha1(data2)
27
+ expect(firstSha).not.toEqual(secondSha)
28
+ })
29
+ test('stableStringify', () => {
30
+ const now = new Date()
31
+ expect(stableStringify({ time: now, foo: 'bar' })).toEqual(
32
+ `{"foo":"bar","time":"${now.toJSON()}"}`,
33
+ )
34
+ })
35
+ test('sortObject sorts object keys as expected', () => {
36
+ expect(
37
+ deepSortObject({
38
+ c: 'test',
39
+ b: [3, 2, { b: 0, a: 1 }],
40
+ d: undefined,
41
+ a: { 2: 'val', 1: 'foo' },
42
+ }),
43
+ ).toEqual({
44
+ a: { 1: 'foo', 2: 'val' },
45
+ b: [3, 2, { a: 1, b: 0 }],
46
+ c: 'test',
47
+ d: undefined,
48
+ })
49
+ })
50
+ })
@@ -0,0 +1,17 @@
1
+ import { deepSortObject } from './collections'
2
+
3
+ export const stableStringify = (obj: any) => JSON.stringify(deepSortObject(obj))
4
+
5
+ export const sha1 = async (data: any) => {
6
+ const payload = new Uint8Array(
7
+ stableStringify(data)
8
+ .split('')
9
+ .map(function (c) {
10
+ return c.charCodeAt(0)
11
+ }),
12
+ )
13
+
14
+ const hashBuffer = await crypto.subtle.digest('SHA-1', payload)
15
+ const hashArray = Array.from(new Uint8Array(hashBuffer)) // convert buffer to byte array
16
+ return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('')
17
+ }
@@ -0,0 +1,17 @@
1
+ import { describe, expect, test } from '@jest/globals'
2
+ import { validateUrl } from './url'
3
+
4
+ describe('validateUrl()', () => {
5
+ test('it validates urls correctly', () => {
6
+ expect(validateUrl('https://toddle.dev')).toBeInstanceOf(URL)
7
+ expect(validateUrl('not-a-url')).toBe(false)
8
+ })
9
+
10
+ test('it validates urls arrays in query params correctly', async () => {
11
+ const url = validateUrl('https://toddle.dev?test=1&test=2')
12
+ expect(url).toBeInstanceOf(URL)
13
+ if (url instanceof URL) {
14
+ expect(url.searchParams.getAll('test')).toEqual(['1', '2'])
15
+ }
16
+ })
17
+ })
@@ -0,0 +1,33 @@
1
+ const LOCALHOSTS = ['http://localhost:54404', 'http://preview.localhost:54404']
2
+
3
+ export const isLocalhostUrl = (hrefOrOrigin: string) =>
4
+ LOCALHOSTS.some((host) => hrefOrOrigin.startsWith(host))
5
+
6
+ export const isLocalhostHostname = (hostname: string) =>
7
+ hostname === 'localhost' || hostname === '127.0.0.1'
8
+
9
+ export const validateUrl = (url?: string | null, base?: string) => {
10
+ if (typeof url !== 'string') {
11
+ return false
12
+ }
13
+
14
+ try {
15
+ const urlObject = new URL(url, base)
16
+ // Creating a new URL object will not correctly encode the search params
17
+ // So we need to iterate over them to make sure they are encoded as that happens when setting them explicitly
18
+ const searchCopy = new URLSearchParams(urlObject.searchParams)
19
+ searchCopy.forEach((value, key) => {
20
+ urlObject.searchParams.delete(key, value)
21
+ })
22
+ searchCopy.forEach((value, key) => {
23
+ urlObject.searchParams.append(key, value)
24
+ })
25
+ return urlObject
26
+ } catch {
27
+ return false
28
+ }
29
+ }
30
+
31
+ export const PROXY_URL_HEADER = 'x-toddle-url'
32
+
33
+ export const REWRITE_HEADER = 'x-toddle-rewrite'
@@ -0,0 +1,8 @@
1
+ export const isDefined = <T>(value: T | undefined | null): value is T =>
2
+ value !== null && value !== undefined
3
+
4
+ export const isObject = (input: any): input is Record<string, any> =>
5
+ typeof input === 'object' && input !== null
6
+
7
+ export const toBoolean = (value: any) =>
8
+ value !== false && value !== undefined && value !== null