@shopify/ui-extensions-server-kit 0.0.0-experimental-20260902222805

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 (202) hide show
  1. package/CHANGELOG.md +157 -0
  2. package/README.md +75 -0
  3. package/dist/ExtensionServerClient/ExtensionServerClient.d.ts +27 -0
  4. package/dist/ExtensionServerClient/ExtensionServerClient.test.d.ts +9 -0
  5. package/dist/ExtensionServerClient/index.d.ts +2 -0
  6. package/dist/ExtensionServerClient/server-types.d.ts +66 -0
  7. package/dist/ExtensionServerClient/types.d.ts +124 -0
  8. package/dist/_virtual/jsx-runtime.cjs.js +1 -0
  9. package/dist/_virtual/jsx-runtime.cjs2.js +1 -0
  10. package/dist/_virtual/jsx-runtime.es.js +5 -0
  11. package/dist/_virtual/jsx-runtime.es2.js +4 -0
  12. package/dist/_virtual/react-jsx-runtime.development.cjs.js +1 -0
  13. package/dist/_virtual/react-jsx-runtime.development.es.js +4 -0
  14. package/dist/_virtual/react-jsx-runtime.production.min.cjs.js +1 -0
  15. package/dist/_virtual/react-jsx-runtime.production.min.es.js +4 -0
  16. package/dist/context/ExtensionServerProvider.d.ts +3 -0
  17. package/dist/context/ExtensionServerProvider.test.d.ts +1 -0
  18. package/dist/context/constants.d.ts +2 -0
  19. package/dist/context/index.d.ts +3 -0
  20. package/dist/context/types.d.ts +12 -0
  21. package/dist/hooks/index.d.ts +3 -0
  22. package/dist/hooks/useExtensionServerContext.d.ts +1 -0
  23. package/dist/hooks/useExtensionServerState.d.ts +1 -0
  24. package/dist/hooks/useIsomorphicLayoutEffect.d.ts +2 -0
  25. package/dist/i18n.d.ts +68 -0
  26. package/dist/i18n.test.d.ts +1 -0
  27. package/dist/index.cjs.js +1 -0
  28. package/dist/index.cjs2.js +1 -0
  29. package/dist/index.d.ts +7 -0
  30. package/dist/index.es.js +46 -0
  31. package/dist/index.es2.js +8 -0
  32. package/dist/node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react-jsx-runtime.development.cjs.js +22 -0
  33. package/dist/node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react-jsx-runtime.development.es.js +604 -0
  34. package/dist/node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react-jsx-runtime.production.min.cjs.js +9 -0
  35. package/dist/node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react-jsx-runtime.production.min.es.js +28 -0
  36. package/dist/node_modules/.pnpm/react@18.3.1/node_modules/react/jsx-runtime.cjs.js +1 -0
  37. package/dist/node_modules/.pnpm/react@18.3.1/node_modules/react/jsx-runtime.es.js +10 -0
  38. package/dist/packages/ui-extensions-server-kit/src/ExtensionServerClient/ExtensionServerClient.cjs.js +1 -0
  39. package/dist/packages/ui-extensions-server-kit/src/ExtensionServerClient/ExtensionServerClient.es.js +136 -0
  40. package/dist/packages/ui-extensions-server-kit/src/ExtensionServerClient/types.cjs.js +1 -0
  41. package/dist/packages/ui-extensions-server-kit/src/ExtensionServerClient/types.es.js +4 -0
  42. package/dist/packages/ui-extensions-server-kit/src/context/ExtensionServerProvider.cjs.js +1 -0
  43. package/dist/packages/ui-extensions-server-kit/src/context/ExtensionServerProvider.es.js +30 -0
  44. package/dist/packages/ui-extensions-server-kit/src/context/constants.cjs.js +1 -0
  45. package/dist/packages/ui-extensions-server-kit/src/context/constants.es.js +13 -0
  46. package/dist/packages/ui-extensions-server-kit/src/hooks/useExtensionServerContext.cjs.js +1 -0
  47. package/dist/packages/ui-extensions-server-kit/src/hooks/useExtensionServerContext.es.js +7 -0
  48. package/dist/packages/ui-extensions-server-kit/src/hooks/useExtensionServerState.cjs.js +1 -0
  49. package/dist/packages/ui-extensions-server-kit/src/hooks/useExtensionServerState.es.js +9 -0
  50. package/dist/packages/ui-extensions-server-kit/src/hooks/useIsomorphicLayoutEffect.cjs.js +1 -0
  51. package/dist/packages/ui-extensions-server-kit/src/hooks/useIsomorphicLayoutEffect.es.js +5 -0
  52. package/dist/packages/ui-extensions-server-kit/src/i18n.cjs.js +1 -0
  53. package/dist/packages/ui-extensions-server-kit/src/i18n.es.js +60 -0
  54. package/dist/packages/ui-extensions-server-kit/src/state/actions/actions.cjs.js +1 -0
  55. package/dist/packages/ui-extensions-server-kit/src/state/actions/actions.es.js +37 -0
  56. package/dist/packages/ui-extensions-server-kit/src/state/reducers/constants.cjs.js +1 -0
  57. package/dist/packages/ui-extensions-server-kit/src/state/reducers/constants.es.js +7 -0
  58. package/dist/packages/ui-extensions-server-kit/src/state/reducers/extensionServerReducer.cjs.js +1 -0
  59. package/dist/packages/ui-extensions-server-kit/src/state/reducers/extensionServerReducer.es.js +57 -0
  60. package/dist/packages/ui-extensions-server-kit/src/testing/MockExtensionServerProvider.cjs.js +1 -0
  61. package/dist/packages/ui-extensions-server-kit/src/testing/MockExtensionServerProvider.es.js +25 -0
  62. package/dist/packages/ui-extensions-server-kit/src/testing/app.cjs.js +1 -0
  63. package/dist/packages/ui-extensions-server-kit/src/testing/app.es.js +16 -0
  64. package/dist/packages/ui-extensions-server-kit/src/testing/extensions.cjs.js +1 -0
  65. package/dist/packages/ui-extensions-server-kit/src/testing/extensions.es.js +66 -0
  66. package/dist/packages/ui-extensions-server-kit/src/types.cjs.js +1 -0
  67. package/dist/packages/ui-extensions-server-kit/src/types.es.js +4 -0
  68. package/dist/packages/ui-extensions-server-kit/src/utilities/assetToString.cjs.js +1 -0
  69. package/dist/packages/ui-extensions-server-kit/src/utilities/assetToString.es.js +7 -0
  70. package/dist/packages/ui-extensions-server-kit/src/utilities/isUIExtension.cjs.js +1 -0
  71. package/dist/packages/ui-extensions-server-kit/src/utilities/isUIExtension.es.js +7 -0
  72. package/dist/packages/ui-extensions-server-kit/src/utilities/isValidSurface.cjs.js +1 -0
  73. package/dist/packages/ui-extensions-server-kit/src/utilities/isValidSurface.es.js +7 -0
  74. package/dist/packages/ui-extensions-server-kit/src/utilities/noop.cjs.js +1 -0
  75. package/dist/packages/ui-extensions-server-kit/src/utilities/noop.es.js +5 -0
  76. package/dist/packages/ui-extensions-server-kit/src/utilities/replaceUpdated.cjs.js +1 -0
  77. package/dist/packages/ui-extensions-server-kit/src/utilities/replaceUpdated.es.js +14 -0
  78. package/dist/packages/ui-extensions-server-kit/src/utilities/set.cjs.js +1 -0
  79. package/dist/packages/ui-extensions-server-kit/src/utilities/set.es.js +18 -0
  80. package/dist/state/actions/actions.d.ts +6 -0
  81. package/dist/state/actions/index.d.ts +2 -0
  82. package/dist/state/actions/types.d.ts +21 -0
  83. package/dist/state/index.d.ts +2 -0
  84. package/dist/state/reducers/constants.d.ts +2 -0
  85. package/dist/state/reducers/extensionServerReducer.d.ts +3 -0
  86. package/dist/state/reducers/extensionServerReducer.test.d.ts +1 -0
  87. package/dist/state/reducers/index.d.ts +3 -0
  88. package/dist/state/reducers/types.d.ts +6 -0
  89. package/dist/testing/MockExtensionServerProvider.d.ts +8 -0
  90. package/dist/testing/app.d.ts +2 -0
  91. package/dist/testing/extensions.d.ts +6 -0
  92. package/dist/testing/index.d.ts +3 -0
  93. package/dist/types.d.ts +196 -0
  94. package/dist/utilities/assetToString.d.ts +2 -0
  95. package/dist/utilities/assetToString.test.d.ts +1 -0
  96. package/dist/utilities/index.d.ts +6 -0
  97. package/dist/utilities/isUIExtension.d.ts +1 -0
  98. package/dist/utilities/isValidSurface.d.ts +2 -0
  99. package/dist/utilities/noop.d.ts +1 -0
  100. package/dist/utilities/replaceUpdated.d.ts +1 -0
  101. package/dist/utilities/replaceUpdated.test.d.ts +1 -0
  102. package/dist/utilities/set.d.ts +4 -0
  103. package/dist/utilities/set.test.d.ts +1 -0
  104. package/index.d.ts +1 -0
  105. package/index.js +1 -0
  106. package/index.mjs +1 -0
  107. package/node_modules/@testing-library/react/LICENSE +20 -0
  108. package/node_modules/@testing-library/react/README.md +692 -0
  109. package/node_modules/@testing-library/react/dist/@testing-library/react.cjs.js +537 -0
  110. package/node_modules/@testing-library/react/dist/@testing-library/react.esm.js +497 -0
  111. package/node_modules/@testing-library/react/dist/@testing-library/react.pure.cjs.js +501 -0
  112. package/node_modules/@testing-library/react/dist/@testing-library/react.pure.esm.js +461 -0
  113. package/node_modules/@testing-library/react/dist/@testing-library/react.pure.umd.js +502 -0
  114. package/node_modules/@testing-library/react/dist/@testing-library/react.pure.umd.js.map +1 -0
  115. package/node_modules/@testing-library/react/dist/@testing-library/react.pure.umd.min.js +2 -0
  116. package/node_modules/@testing-library/react/dist/@testing-library/react.pure.umd.min.js.map +1 -0
  117. package/node_modules/@testing-library/react/dist/@testing-library/react.umd.js +538 -0
  118. package/node_modules/@testing-library/react/dist/@testing-library/react.umd.js.map +1 -0
  119. package/node_modules/@testing-library/react/dist/@testing-library/react.umd.min.js +2 -0
  120. package/node_modules/@testing-library/react/dist/@testing-library/react.umd.min.js.map +1 -0
  121. package/node_modules/@testing-library/react/dist/act-compat.js +80 -0
  122. package/node_modules/@testing-library/react/dist/config.js +33 -0
  123. package/node_modules/@testing-library/react/dist/fire-event.js +70 -0
  124. package/node_modules/@testing-library/react/dist/index.js +52 -0
  125. package/node_modules/@testing-library/react/dist/pure.js +356 -0
  126. package/node_modules/@testing-library/react/dont-cleanup-after-each.js +1 -0
  127. package/node_modules/@testing-library/react/package.json +116 -0
  128. package/node_modules/@testing-library/react/pure.d.ts +1 -0
  129. package/node_modules/@testing-library/react/pure.js +2 -0
  130. package/node_modules/@testing-library/react/types/index.d.ts +287 -0
  131. package/node_modules/@testing-library/react/types/pure.d.ts +1 -0
  132. package/node_modules/@types/react/LICENSE +21 -0
  133. package/node_modules/@types/react/README.md +15 -0
  134. package/node_modules/@types/react/canary.d.ts +166 -0
  135. package/node_modules/@types/react/experimental.d.ts +127 -0
  136. package/node_modules/@types/react/global.d.ts +160 -0
  137. package/node_modules/@types/react/index.d.ts +4543 -0
  138. package/node_modules/@types/react/jsx-dev-runtime.d.ts +45 -0
  139. package/node_modules/@types/react/jsx-runtime.d.ts +36 -0
  140. package/node_modules/@types/react/package.json +206 -0
  141. package/node_modules/@types/react/ts5.0/canary.d.ts +166 -0
  142. package/node_modules/@types/react/ts5.0/experimental.d.ts +127 -0
  143. package/node_modules/@types/react/ts5.0/global.d.ts +160 -0
  144. package/node_modules/@types/react/ts5.0/index.d.ts +4530 -0
  145. package/node_modules/@types/react/ts5.0/jsx-dev-runtime.d.ts +44 -0
  146. package/node_modules/@types/react/ts5.0/jsx-runtime.d.ts +35 -0
  147. package/node_modules/@vitejs/plugin-react/LICENSE +21 -0
  148. package/node_modules/@vitejs/plugin-react/README.md +159 -0
  149. package/node_modules/@vitejs/plugin-react/dist/index.d.ts +75 -0
  150. package/node_modules/@vitejs/plugin-react/dist/index.js +401 -0
  151. package/node_modules/@vitejs/plugin-react/dist/refresh-runtime.js +663 -0
  152. package/node_modules/@vitejs/plugin-react/package.json +77 -0
  153. package/node_modules/@vitejs/plugin-react/types/preamble.d.ts +1 -0
  154. package/package.json +64 -0
  155. package/project.json +40 -0
  156. package/scripts/create-entry-files.ts +44 -0
  157. package/src/ExtensionServerClient/ExtensionServerClient.test.ts +1238 -0
  158. package/src/ExtensionServerClient/ExtensionServerClient.ts +320 -0
  159. package/src/ExtensionServerClient/index.ts +2 -0
  160. package/src/ExtensionServerClient/server-types.ts +65 -0
  161. package/src/ExtensionServerClient/types.ts +159 -0
  162. package/src/context/ExtensionServerProvider.test.tsx +318 -0
  163. package/src/context/ExtensionServerProvider.tsx +48 -0
  164. package/src/context/constants.ts +16 -0
  165. package/src/context/index.ts +3 -0
  166. package/src/context/types.ts +14 -0
  167. package/src/hooks/index.ts +3 -0
  168. package/src/hooks/useExtensionServerContext.ts +4 -0
  169. package/src/hooks/useExtensionServerState.ts +6 -0
  170. package/src/hooks/useIsomorphicLayoutEffect.ts +6 -0
  171. package/src/i18n.test.ts +417 -0
  172. package/src/i18n.ts +205 -0
  173. package/src/index.ts +7 -0
  174. package/src/state/actions/actions.ts +36 -0
  175. package/src/state/actions/index.ts +2 -0
  176. package/src/state/actions/types.ts +26 -0
  177. package/src/state/index.ts +2 -0
  178. package/src/state/reducers/constants.ts +6 -0
  179. package/src/state/reducers/extensionServerReducer.test.ts +165 -0
  180. package/src/state/reducers/extensionServerReducer.ts +87 -0
  181. package/src/state/reducers/index.ts +3 -0
  182. package/src/state/reducers/types.ts +7 -0
  183. package/src/testing/MockExtensionServerProvider.tsx +36 -0
  184. package/src/testing/app.ts +15 -0
  185. package/src/testing/extensions.ts +71 -0
  186. package/src/testing/index.ts +3 -0
  187. package/src/types.ts +204 -0
  188. package/src/utilities/assetToString.test.ts +16 -0
  189. package/src/utilities/assetToString.ts +7 -0
  190. package/src/utilities/index.ts +6 -0
  191. package/src/utilities/isUIExtension.ts +8 -0
  192. package/src/utilities/isValidSurface.ts +6 -0
  193. package/src/utilities/noop.ts +1 -0
  194. package/src/utilities/replaceUpdated.test.ts +26 -0
  195. package/src/utilities/replaceUpdated.ts +15 -0
  196. package/src/utilities/set.test.ts +19 -0
  197. package/src/utilities/set.ts +35 -0
  198. package/testing.d.ts +1 -0
  199. package/testing.js +1 -0
  200. package/testing.mjs +1 -0
  201. package/tests/setup.ts +3 -0
  202. package/vite.config.mts +57 -0
@@ -0,0 +1,165 @@
1
+ import {extensionServerReducer} from './extensionServerReducer'
2
+ import {INITIAL_STATE} from './constants'
3
+ import {mockExtension, mockApp} from '../../testing'
4
+ import {
5
+ createConnectedAction,
6
+ createUpdateAction,
7
+ createRefreshAction,
8
+ createFocusAction,
9
+ createUnfocusAction,
10
+ } from '../actions'
11
+ import {vi} from 'vitest'
12
+
13
+ import type {ExtensionServerState} from './types'
14
+
15
+ describe('extensionServerReducer()', () => {
16
+ test('connects to server', () => {
17
+ const app = mockApp()
18
+ const extension = mockExtension()
19
+ const action = createConnectedAction({app, extensions: [extension], store: 'test-store.com'})
20
+ const state = extensionServerReducer(INITIAL_STATE, action)
21
+
22
+ expect(state).toStrictEqual({
23
+ app,
24
+ extensions: [extension],
25
+ store: 'test-store.com',
26
+ })
27
+ })
28
+
29
+ test('replaces existing app and extensions when connecting twice', () => {
30
+ const app = mockApp()
31
+ const extension = mockExtension()
32
+ const action1 = createConnectedAction({app, extensions: [extension], store: 'test-store.com'})
33
+ const initialState = extensionServerReducer(INITIAL_STATE, action1)
34
+
35
+ expect(initialState).toStrictEqual({
36
+ app,
37
+ extensions: [extension],
38
+ store: 'test-store.com',
39
+ })
40
+
41
+ const currentExtension = initialState.extensions[0]
42
+ const newExtension = {...currentExtension, uuid: 'new-uuid'}
43
+ const newApp = {...app, id: 'new-id'}
44
+ const action2 = createConnectedAction({app: newApp, extensions: [newExtension], store: 'test-store.com'})
45
+ const secondState = extensionServerReducer(initialState, action2)
46
+
47
+ expect(secondState).toStrictEqual({
48
+ app: newApp,
49
+ extensions: [newExtension],
50
+ store: 'test-store.com',
51
+ })
52
+ })
53
+
54
+ test('receives updates from the server', () => {
55
+ const extension1 = mockExtension()
56
+ const extension2 = mockExtension()
57
+ const previousState: ExtensionServerState = {
58
+ store: 'test-store.com',
59
+ extensions: [extension1, extension2],
60
+ }
61
+
62
+ const app = mockApp()
63
+ const updated1 = {...extension1, version: 'v2'}
64
+ const action = createUpdateAction({app, extensions: [updated1]})
65
+
66
+ const state = extensionServerReducer(previousState, action)
67
+
68
+ expect(state).toStrictEqual({
69
+ app,
70
+ extensions: [updated1, extension2],
71
+ store: 'test-store.com',
72
+ })
73
+ })
74
+
75
+ test('maintains extension order after update', () => {
76
+ const extension1 = mockExtension()
77
+ const extension2 = mockExtension()
78
+ const previousState: ExtensionServerState = {
79
+ store: 'test-store.com',
80
+ extensions: [extension1, extension2],
81
+ }
82
+
83
+ const app = mockApp()
84
+ const updated2 = {...extension2, version: 'v2'}
85
+ const action = createUpdateAction({app, extensions: [updated2]})
86
+
87
+ const state = extensionServerReducer(previousState, action)
88
+
89
+ expect(state).toStrictEqual({
90
+ app,
91
+ extensions: [extension1, updated2],
92
+ store: 'test-store.com',
93
+ })
94
+ })
95
+
96
+ test('refreshes extension url', () => {
97
+ vi.useFakeTimers()
98
+ const time1 = 1000
99
+ vi.setSystemTime(time1)
100
+
101
+ const extension = mockExtension()
102
+ const previousState: ExtensionServerState = {
103
+ store: 'test-store.com',
104
+ extensions: [extension],
105
+ }
106
+
107
+ const action = createRefreshAction([{uuid: extension.uuid}])
108
+
109
+ const state1 = extensionServerReducer(previousState, action)
110
+
111
+ const url1 = new URL(state1.extensions[0].assets.main.url)
112
+ const timestamp1 = url1.searchParams.get('lastUpdated')
113
+
114
+ expect(timestamp1).toBe(time1.toString())
115
+
116
+ const time2 = 2000
117
+ vi.setSystemTime(time2)
118
+
119
+ const state2 = extensionServerReducer(state1, action)
120
+
121
+ const url2 = new URL(state2.extensions[0].assets.main.url)
122
+ const timestamp2 = url2.searchParams.get('lastUpdated')
123
+
124
+ expect(timestamp2).toBe(time2.toString())
125
+
126
+ vi.useRealTimers()
127
+ })
128
+
129
+ describe('focus', () => {
130
+ test('focuses only one extension', () => {
131
+ const extension1 = mockExtension()
132
+ const extension2 = mockExtension()
133
+
134
+ const previousState: ExtensionServerState = {
135
+ store: 'test-store.com',
136
+ extensions: [extension1, extension2],
137
+ }
138
+
139
+ const action1 = createFocusAction([{uuid: extension1.uuid}])
140
+ const state1 = extensionServerReducer(previousState, action1)
141
+
142
+ expect(state1.extensions[0].development.focused).toBe(true)
143
+
144
+ const action2 = createFocusAction([{uuid: extension2.uuid}])
145
+ const state2 = extensionServerReducer(state1, action2)
146
+
147
+ expect(state2.extensions[0].development.focused).toBe(false)
148
+ expect(state2.extensions[1].development.focused).toBe(true)
149
+ })
150
+
151
+ test('unfocuses extension', () => {
152
+ const extension = mockExtension({development: {focused: true}})
153
+
154
+ const previousState: ExtensionServerState = {
155
+ store: 'test-store.com',
156
+ extensions: [extension],
157
+ }
158
+
159
+ const action = createUnfocusAction()
160
+ const state = extensionServerReducer(previousState, action)
161
+
162
+ expect(state.extensions[0].development.focused).toBe(false)
163
+ })
164
+ })
165
+ })
@@ -0,0 +1,87 @@
1
+ import {set, replaceUpdated, assetToString} from '../../utilities'
2
+ import type {ExtensionServerActions} from '../actions'
3
+ import type {ExtensionPayload} from '../../types'
4
+
5
+ import type {ExtensionServerState} from './types'
6
+
7
+ export function extensionServerReducer(state: ExtensionServerState, action: ExtensionServerActions) {
8
+ switch (action.type) {
9
+ case 'connected': {
10
+ const extensions = (action.payload.extensions ?? []).map((extension) => {
11
+ Object.keys(extension.assets).forEach(
12
+ (asset) => (extension.assets[asset].url = assetToString(extension.assets[asset])),
13
+ )
14
+ return extension
15
+ })
16
+ return {
17
+ ...state,
18
+ store: action.payload.store,
19
+ app: action.payload.app,
20
+ extensions,
21
+ } as ExtensionServerState
22
+ }
23
+
24
+ case 'update': {
25
+ const extensions = (action.payload.extensions ?? []).map((extension) => {
26
+ Object.keys(extension.assets).forEach(
27
+ (asset) => (extension.assets[asset].url = assetToString(extension.assets[asset])),
28
+ )
29
+ return extension
30
+ })
31
+ return {
32
+ ...state,
33
+ app: {...(state.app ?? {}), ...(action.payload.app ?? {})},
34
+ extensions: replaceUpdated(state.extensions, extensions, ({uuid}) => uuid),
35
+ } as ExtensionServerState
36
+ }
37
+
38
+ case 'refresh': {
39
+ return {
40
+ ...state,
41
+ extensions: state.extensions.map((extension) => {
42
+ if (action.payload.some(({uuid}) => extension.uuid === uuid)) {
43
+ const assets: ExtensionPayload['assets'] = {}
44
+ Object.keys(extension.assets).forEach((asset) => {
45
+ const resourceURL = {...extension.assets[asset]}
46
+ resourceURL.lastUpdated = Date.now()
47
+ resourceURL.url = assetToString(resourceURL)
48
+
49
+ assets[asset] = resourceURL
50
+ })
51
+ return set(extension, (ext) => ext.assets, assets)
52
+ }
53
+ return extension
54
+ }),
55
+ }
56
+ }
57
+
58
+ case 'focus': {
59
+ return {
60
+ ...state,
61
+ extensions: state.extensions.map((extension) => {
62
+ if (action.payload.some(({uuid}) => extension.uuid === uuid)) {
63
+ return set(extension, (ext) => ext.development.focused, true)
64
+ } else if (extension.development.focused) {
65
+ return set(extension, (ext) => ext.development.focused, false)
66
+ }
67
+ return extension
68
+ }),
69
+ }
70
+ }
71
+
72
+ case 'unfocus': {
73
+ return {
74
+ ...state,
75
+ extensions: state.extensions.map((extension) => {
76
+ if (extension.development.focused) {
77
+ return set(extension, (ext) => ext.development.focused, false)
78
+ }
79
+ return extension
80
+ }),
81
+ }
82
+ }
83
+
84
+ default:
85
+ return state
86
+ }
87
+ }
@@ -0,0 +1,3 @@
1
+ export * from './constants'
2
+ export * from './extensionServerReducer'
3
+ export * from './types'
@@ -0,0 +1,7 @@
1
+ import type {App, ExtensionPayload} from '../../types'
2
+
3
+ export interface ExtensionServerState {
4
+ app?: App
5
+ extensions: ExtensionPayload[]
6
+ store: string
7
+ }
@@ -0,0 +1,36 @@
1
+ import {ExtensionServerProvider, extensionServerContext} from '../context'
2
+ import {useExtensionServerContext} from '../hooks'
3
+ import React, {useMemo} from 'react'
4
+ import type {ExtensionServerProviderProps, ExtensionServerContext} from '../context'
5
+
6
+ interface InternalProviderProps extends Partial<ExtensionServerContext> {
7
+ children?: ExtensionServerProviderProps['children']
8
+ }
9
+
10
+ function InternalProvider({children, ...mocks}: InternalProviderProps) {
11
+ const actual = useExtensionServerContext()
12
+
13
+ const context = useMemo(
14
+ () => ({
15
+ ...actual,
16
+ ...mocks,
17
+ }),
18
+ [actual, mocks],
19
+ )
20
+
21
+ return <extensionServerContext.Provider value={context}>{children}</extensionServerContext.Provider>
22
+ }
23
+
24
+ type MockExtensionServerProviderProps = Partial<ExtensionServerProviderProps> & InternalProviderProps
25
+
26
+ export function MockExtensionServerProvider({
27
+ children,
28
+ options = {connection: {}},
29
+ ...props
30
+ }: MockExtensionServerProviderProps) {
31
+ return (
32
+ <ExtensionServerProvider options={options}>
33
+ <InternalProvider {...props}>{children}</InternalProvider>
34
+ </ExtensionServerProvider>
35
+ )
36
+ }
@@ -0,0 +1,15 @@
1
+ import {App} from '../types'
2
+
3
+ export function mockApp(): App {
4
+ return {
5
+ id: 'id-1',
6
+ apiKey: '12345',
7
+ applicationUrl: 'www.applicationUrl.com',
8
+ title: 'App title',
9
+ url: 'mock url',
10
+ mobileUrl: 'mock mobile url',
11
+ icon: {
12
+ transformedSrc: 'www.transformed-src.com',
13
+ },
14
+ }
15
+ }
@@ -0,0 +1,71 @@
1
+ import {ExtensionPayload, Status} from '../types'
2
+
3
+ type DeepPartial<T> = {
4
+ [P in keyof T]?: DeepPartial<T[P]>
5
+ }
6
+
7
+ let id = 0
8
+
9
+ function pad(num: number) {
10
+ return `00000000000${num}`.slice(-12)
11
+ }
12
+
13
+ export function mockExtension(obj: DeepPartial<ExtensionPayload> = {}): ExtensionPayload {
14
+ const uuid = `00000000-0000-0000-0000-${pad(id++)}`
15
+ const lastUpdated = Date.now()
16
+ return {
17
+ handle: 'my-extension',
18
+ name: 'My extension',
19
+ description: 'My extension description',
20
+ surface: 'admin',
21
+ type: 'purchase_option',
22
+ externalType: 'external_type',
23
+ uuid,
24
+ version: 'extension version',
25
+ ...obj,
26
+ assets: {
27
+ main: {
28
+ name: 'main',
29
+ url: `https://secure-link.com/extensions/${uuid}/assets/handle.js?lastUpdated=${lastUpdated}`,
30
+ lastUpdated,
31
+ },
32
+ ...((obj.assets ?? {}) as any),
33
+ },
34
+ development: {
35
+ hidden: false,
36
+ status: Status.Success,
37
+ resource: {
38
+ url: 'resourceUrl',
39
+ },
40
+ root: {
41
+ url: `https://secure-link.com/extensions/${uuid}`,
42
+ },
43
+ renderer: {
44
+ name: 'render name',
45
+ version: '1.0.0',
46
+ },
47
+ ...((obj.development ?? {}) as any),
48
+ },
49
+ // this is due to the naive DeepPartial but also more complex ones
50
+ // [see stackoverflow](https://stackoverflow.com/a/68699273) assume that
51
+ // `DeepPartial<Array<T>> === Array<T | undefined>` while we are looking for
52
+ // `DeepPartial<Array<T>> === Array<T> | undefined`.
53
+ // This is the case for extension points and seems hard to fix
54
+ // in a generalized, non-surprising way
55
+ extensionPoints: obj.extensionPoints as any,
56
+ capabilities: obj.capabilities as any,
57
+ supportedFeatures: obj.supportedFeatures as any,
58
+ localization: obj.localization as any,
59
+ authenticatedRedirectStartUrl: obj.authenticatedRedirectStartUrl as any,
60
+ authenticatedRedirectRedirectUrls: obj.authenticatedRedirectRedirectUrls as any,
61
+ settings: {
62
+ fields: [
63
+ {
64
+ key: 'sample-key',
65
+ name: 'sample_name',
66
+ type: 'sample_type',
67
+ },
68
+ ],
69
+ },
70
+ }
71
+ }
@@ -0,0 +1,3 @@
1
+ export * from './MockExtensionServerProvider'
2
+ export * from './app'
3
+ export * from './extensions'
package/src/types.ts ADDED
@@ -0,0 +1,204 @@
1
+ import {FlattenedLocalization, Localization} from './i18n'
2
+ import './ExtensionServerClient/types'
3
+ import type {Surface} from './ExtensionServerClient/types'
4
+
5
+ declare global {
6
+ namespace ExtensionServer {
7
+ type ServerEvents =
8
+ | {
9
+ event: 'dispatch'
10
+ data: InboundEvents['dispatch']
11
+ }
12
+ | {
13
+ event: 'connected'
14
+ data: InboundEvents['connected']
15
+ }
16
+ | {
17
+ event: 'update'
18
+ data: InboundEvents['update']
19
+ }
20
+
21
+ interface InboundEvents extends DispatchEvents {
22
+ dispatch: {type: keyof DispatchEvents; payload: DispatchEvents[keyof DispatchEvents]}
23
+ connected: {extensions: ExtensionPayload[]; app?: App; store: string}
24
+ update: {extensions?: ExtensionPayload[]; app?: App}
25
+ }
26
+
27
+ interface OutboundPersistEvents {
28
+ update: {
29
+ extensions?: ({uuid: string} & DeepPartial<ExtensionPayload>)[]
30
+ app?: DeepPartial<App>
31
+ }
32
+ }
33
+
34
+ interface DispatchEvents {
35
+ refresh: {uuid: string}[]
36
+ focus: {uuid: string}[]
37
+ unfocus: void
38
+ navigate: {url: string}
39
+ }
40
+
41
+ // API responses
42
+ namespace API {
43
+ interface BaseResponse {
44
+ app: App
45
+ root: ResourceURL
46
+ socket: ResourceURL
47
+ devConsole: ResourceURL
48
+ store: string
49
+ version: string
50
+ }
51
+
52
+ interface ExtensionsResponse extends BaseResponse {
53
+ extensions: ExtensionPayload[]
54
+ }
55
+
56
+ interface ExtensionResponse extends BaseResponse {
57
+ extension: ExtensionPayload
58
+ }
59
+ }
60
+
61
+ interface UIExtension extends ExtensionPayload {
62
+ extensionPoints: ExtensionPoint[]
63
+ apiVersion: string
64
+ }
65
+ }
66
+ }
67
+
68
+ export type DeepPartial<T> = {
69
+ [P in keyof T]?: DeepPartial<T[P]>
70
+ }
71
+
72
+ interface ResourceURL {
73
+ url: string
74
+ }
75
+
76
+ export interface Asset extends ResourceURL {
77
+ name: string
78
+ lastUpdated: number
79
+ }
80
+
81
+ interface Metafield {
82
+ namespace: string
83
+ key: string
84
+ }
85
+
86
+ export interface ExtensionPointIntercept {
87
+ event: string
88
+ blocking: boolean
89
+ applies_to?: string[]
90
+ }
91
+
92
+ export interface ExtensionPointCapabilities {
93
+ allow_direct_linking?: boolean
94
+ intercepts?: ExtensionPointIntercept[]
95
+ }
96
+
97
+ export interface ExtensionPoint {
98
+ target: string
99
+ surface: Surface
100
+ metafields?: Metafield[]
101
+ resource: ResourceURL
102
+ root: ResourceURL
103
+ localization?: FlattenedLocalization | Localization | null
104
+ name: string
105
+ description?: string
106
+ assets?: Record<string, Asset>
107
+ capabilities?: ExtensionPointCapabilities
108
+ }
109
+
110
+ type ExtensionPoints = string[] | ExtensionPoint[] | null
111
+
112
+ interface CollectBuyerConsentCapabilities {
113
+ smsMarketing: boolean
114
+ customerPrivacy: boolean
115
+ }
116
+
117
+ interface IframeCapabilities {
118
+ sources: string[]
119
+ }
120
+
121
+ interface Capabilities {
122
+ apiAccess: boolean
123
+ blockProgress: boolean
124
+ networkAccess: boolean
125
+ collectBuyerConsent: CollectBuyerConsentCapabilities
126
+ iframe: IframeCapabilities
127
+ }
128
+
129
+ export interface ExtensionPayload {
130
+ type: string
131
+ externalType: string
132
+ assets: Record<string, Asset>
133
+ development: {
134
+ hidden: boolean
135
+ status: Status
136
+ focused?: boolean
137
+ resource: ResourceURL
138
+ root: ResourceURL
139
+ renderer: {
140
+ name: string
141
+ version: string
142
+ }
143
+ error?: {
144
+ message: string
145
+ file?: string
146
+ }
147
+ }
148
+ uuid: string
149
+ version: string
150
+ surface: Surface
151
+ name: string
152
+ description?: string
153
+ handle: string
154
+ extensionPoints: ExtensionPoints
155
+ capabilities?: Capabilities
156
+ supportedFeatures: ExtensionSupportedFeatures
157
+ authenticatedRedirectStartUrl?: string
158
+ authenticatedRedirectRedirectUrls?: string[]
159
+ localization?: FlattenedLocalization | Localization | null
160
+ settings?: {
161
+ fields?: {
162
+ type: string
163
+ key?: string
164
+ name?: string
165
+ description?: string
166
+ required?: boolean
167
+ validations?: unknown[]
168
+ }[]
169
+ }
170
+ }
171
+
172
+ export interface ExtensionSupportedFeatures {
173
+ runsOffline: boolean
174
+ }
175
+
176
+ export enum Status {
177
+ Success = 'success',
178
+ }
179
+
180
+ export interface App {
181
+ id: string
182
+ apiKey: string
183
+ url: string
184
+ mobileUrl: string
185
+ applicationUrl: string
186
+ handle?: string | null
187
+ title: string
188
+ developerName?: string
189
+ icon: {
190
+ transformedSrc: string
191
+ }
192
+ installation?: {
193
+ launchUrl: string
194
+ }
195
+ supportEmail?: string
196
+ supportLocales?: string[]
197
+ allowedDomains?: string[]
198
+ assets?: {
199
+ [key: string]: {
200
+ url: string
201
+ lastUpdated: number
202
+ }
203
+ }
204
+ }
@@ -0,0 +1,16 @@
1
+ import {assetToString} from './assetToString'
2
+ import type {Asset} from '../types'
3
+
4
+ describe('assetToString tests', () => {
5
+ test('creates a URL string from an asset', () => {
6
+ const asset: Asset = {
7
+ name: 'main',
8
+ url: 'http://localhost:8000/extensions/00000000/assets/handle.js',
9
+ lastUpdated: 1637004124,
10
+ }
11
+
12
+ const url = assetToString(asset)
13
+
14
+ expect(url).toBe(`${asset.url}?lastUpdated=${asset.lastUpdated}`)
15
+ })
16
+ })
@@ -0,0 +1,7 @@
1
+ import type {Asset} from '../types'
2
+
3
+ export function assetToString(asset: Asset) {
4
+ const url = new URL(asset.url)
5
+ url.searchParams.set('lastUpdated', String(asset.lastUpdated))
6
+ return url.toString()
7
+ }
@@ -0,0 +1,6 @@
1
+ export * from './noop'
2
+ export * from './replaceUpdated'
3
+ export * from './set'
4
+ export * from './assetToString'
5
+ export * from './isValidSurface'
6
+ export * from './isUIExtension'
@@ -0,0 +1,8 @@
1
+ export function isUIExtension(extension: unknown): extension is ExtensionServer.UIExtension {
2
+ const ext = extension as ExtensionServer.UIExtension
3
+ return (
4
+ ext.type === 'ui_extension' &&
5
+ Array.isArray(ext.extensionPoints) &&
6
+ ext.extensionPoints.every((extensionPoint: unknown) => typeof extensionPoint === 'object')
7
+ )
8
+ }
@@ -0,0 +1,6 @@
1
+ import {AVAILABLE_SURFACES} from '../ExtensionServerClient/types'
2
+ import type {Surface} from '../ExtensionServerClient/types'
3
+
4
+ export function isValidSurface(surface: unknown): surface is Surface {
5
+ return Boolean(surface) && AVAILABLE_SURFACES.includes(surface as Surface)
6
+ }
@@ -0,0 +1 @@
1
+ export const noop = () => {}
@@ -0,0 +1,26 @@
1
+ /* eslint-disable id-length */
2
+ import {replaceUpdated} from './replaceUpdated'
3
+
4
+ describe('replaceUpdated', () => {
5
+ test('replaces updated items in place, and appends new items', () => {
6
+ const arr = [
7
+ {id: 1, a: 'a'},
8
+ {id: 2, a: 'b'},
9
+ {id: 3, a: 'c'},
10
+ {id: 4, a: 'd'},
11
+ ]
12
+
13
+ const updates = [
14
+ {id: 2, a: 'updated'},
15
+ {id: 5, a: 'e'},
16
+ ]
17
+
18
+ expect(replaceUpdated(arr, updates, ({id}) => id)).toStrictEqual([
19
+ {id: 1, a: 'a'},
20
+ {id: 2, a: 'updated'},
21
+ {id: 3, a: 'c'},
22
+ {id: 4, a: 'd'},
23
+ {id: 5, a: 'e'},
24
+ ])
25
+ })
26
+ })
@@ -0,0 +1,15 @@
1
+ export function replaceUpdated<T>(arr: T[], updates: T[], cb: (v: T) => unknown): T[] {
2
+ const updatesMap = new Map(updates.map((updated) => [cb(updated), updated]))
3
+ const updated = arr.map((item) => {
4
+ const key = cb(item)
5
+ if (updatesMap.has(key)) {
6
+ const updated = updatesMap.get(key)!
7
+ updatesMap.delete(key)
8
+ return updated
9
+ } else {
10
+ return item
11
+ }
12
+ })
13
+
14
+ return [...updated, ...updatesMap.values()]
15
+ }