@shopify/ui-extensions-server-kit 0.0.0-nightly-20250605112924

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 (260) hide show
  1. package/CHANGELOG.md +133 -0
  2. package/README.md +74 -0
  3. package/dist/ExtensionServerClient/ExtensionServerClient.cjs.js +1 -0
  4. package/dist/ExtensionServerClient/ExtensionServerClient.d.ts +28 -0
  5. package/dist/ExtensionServerClient/ExtensionServerClient.es.js +133 -0
  6. package/dist/ExtensionServerClient/ExtensionServerClient.test.d.ts +1 -0
  7. package/dist/ExtensionServerClient/index.d.ts +2 -0
  8. package/dist/ExtensionServerClient/types.cjs.js +1 -0
  9. package/dist/ExtensionServerClient/types.d.ts +124 -0
  10. package/dist/ExtensionServerClient/types.es.js +4 -0
  11. package/dist/context/ExtensionServerProvider.cjs.js +1 -0
  12. package/dist/context/ExtensionServerProvider.d.ts +2 -0
  13. package/dist/context/ExtensionServerProvider.es.js +30 -0
  14. package/dist/context/ExtensionServerProvider.test.d.ts +1 -0
  15. package/dist/context/constants.cjs.js +1 -0
  16. package/dist/context/constants.d.ts +3 -0
  17. package/dist/context/constants.es.js +14 -0
  18. package/dist/context/index.d.ts +3 -0
  19. package/dist/context/types.d.ts +11 -0
  20. package/dist/hooks/index.d.ts +5 -0
  21. package/dist/hooks/useExtensionClient.cjs.js +1 -0
  22. package/dist/hooks/useExtensionClient.d.ts +1 -0
  23. package/dist/hooks/useExtensionClient.es.js +8 -0
  24. package/dist/hooks/useExtensionServerContext.cjs.js +1 -0
  25. package/dist/hooks/useExtensionServerContext.d.ts +1 -0
  26. package/dist/hooks/useExtensionServerContext.es.js +6 -0
  27. package/dist/hooks/useExtensionServerEvent.cjs.js +1 -0
  28. package/dist/hooks/useExtensionServerEvent.d.ts +1 -0
  29. package/dist/hooks/useExtensionServerEvent.es.js +9 -0
  30. package/dist/hooks/useExtensionServerState.cjs.js +1 -0
  31. package/dist/hooks/useExtensionServerState.d.ts +1 -0
  32. package/dist/hooks/useExtensionServerState.es.js +9 -0
  33. package/dist/hooks/useIsomorphicLayoutEffect.cjs.js +1 -0
  34. package/dist/hooks/useIsomorphicLayoutEffect.d.ts +2 -0
  35. package/dist/hooks/useIsomorphicLayoutEffect.es.js +5 -0
  36. package/dist/i18n.cjs.js +1 -0
  37. package/dist/i18n.d.ts +93 -0
  38. package/dist/i18n.es.js +61 -0
  39. package/dist/i18n.test.d.ts +1 -0
  40. package/dist/index.cjs.js +1 -0
  41. package/dist/index.cjs2.js +1 -0
  42. package/dist/index.d.ts +7 -0
  43. package/dist/index.es.js +55 -0
  44. package/dist/index.es2.js +8 -0
  45. package/dist/state/actions/actions.cjs.js +1 -0
  46. package/dist/state/actions/actions.d.ts +7 -0
  47. package/dist/state/actions/actions.es.js +44 -0
  48. package/dist/state/actions/index.d.ts +2 -0
  49. package/dist/state/actions/types.d.ts +25 -0
  50. package/dist/state/index.d.ts +2 -0
  51. package/dist/state/reducers/constants.cjs.js +1 -0
  52. package/dist/state/reducers/constants.d.ts +2 -0
  53. package/dist/state/reducers/constants.es.js +7 -0
  54. package/dist/state/reducers/extensionServerReducer.cjs.js +1 -0
  55. package/dist/state/reducers/extensionServerReducer.d.ts +3 -0
  56. package/dist/state/reducers/extensionServerReducer.es.js +57 -0
  57. package/dist/state/reducers/extensionServerReducer.test.d.ts +1 -0
  58. package/dist/state/reducers/index.d.ts +3 -0
  59. package/dist/state/reducers/types.d.ts +6 -0
  60. package/dist/testing/MockExtensionServerProvider.cjs.js +1 -0
  61. package/dist/testing/MockExtensionServerProvider.d.ts +7 -0
  62. package/dist/testing/MockExtensionServerProvider.es.js +24 -0
  63. package/dist/testing/app.cjs.js +1 -0
  64. package/dist/testing/app.d.ts +2 -0
  65. package/dist/testing/app.es.js +16 -0
  66. package/dist/testing/extensions.cjs.js +1 -0
  67. package/dist/testing/extensions.d.ts +6 -0
  68. package/dist/testing/extensions.es.js +65 -0
  69. package/dist/testing/index.d.ts +3 -0
  70. package/dist/types.cjs.js +1 -0
  71. package/dist/types.d.ts +181 -0
  72. package/dist/types.es.js +4 -0
  73. package/dist/utilities/assetToString.cjs.js +1 -0
  74. package/dist/utilities/assetToString.d.ts +2 -0
  75. package/dist/utilities/assetToString.es.js +7 -0
  76. package/dist/utilities/assetToString.test.d.ts +1 -0
  77. package/dist/utilities/groupByKey.cjs.js +1 -0
  78. package/dist/utilities/groupByKey.d.ts +3 -0
  79. package/dist/utilities/groupByKey.es.js +6 -0
  80. package/dist/utilities/index.d.ts +7 -0
  81. package/dist/utilities/isUIExtension.cjs.js +1 -0
  82. package/dist/utilities/isUIExtension.d.ts +1 -0
  83. package/dist/utilities/isUIExtension.es.js +6 -0
  84. package/dist/utilities/isValidSurface.cjs.js +1 -0
  85. package/dist/utilities/isValidSurface.d.ts +2 -0
  86. package/dist/utilities/isValidSurface.es.js +7 -0
  87. package/dist/utilities/noop.cjs.js +1 -0
  88. package/dist/utilities/noop.d.ts +1 -0
  89. package/dist/utilities/noop.es.js +5 -0
  90. package/dist/utilities/replaceUpdated.cjs.js +1 -0
  91. package/dist/utilities/replaceUpdated.d.ts +1 -0
  92. package/dist/utilities/replaceUpdated.es.js +14 -0
  93. package/dist/utilities/replaceUpdated.test.d.ts +1 -0
  94. package/dist/utilities/set.cjs.js +1 -0
  95. package/dist/utilities/set.d.ts +4 -0
  96. package/dist/utilities/set.es.js +18 -0
  97. package/dist/utilities/set.test.d.ts +1 -0
  98. package/index.d.ts +1 -0
  99. package/index.js +1 -0
  100. package/index.mjs +1 -0
  101. package/node_modules/@shopify/react-testing/LICENSE.md +21 -0
  102. package/node_modules/@shopify/react-testing/README.md +711 -0
  103. package/node_modules/@shopify/react-testing/build/cjs/TestWrapper.js +52 -0
  104. package/node_modules/@shopify/react-testing/build/cjs/_virtual/_rollupPluginBabelHelpers.js +47 -0
  105. package/node_modules/@shopify/react-testing/build/cjs/compat.js +14 -0
  106. package/node_modules/@shopify/react-testing/build/cjs/destroy.js +13 -0
  107. package/node_modules/@shopify/react-testing/build/cjs/element.js +225 -0
  108. package/node_modules/@shopify/react-testing/build/cjs/index.js +21 -0
  109. package/node_modules/@shopify/react-testing/build/cjs/matchers/components.js +46 -0
  110. package/node_modules/@shopify/react-testing/build/cjs/matchers/context.js +25 -0
  111. package/node_modules/@shopify/react-testing/build/cjs/matchers/index.js +16 -0
  112. package/node_modules/@shopify/react-testing/build/cjs/matchers/props.js +38 -0
  113. package/node_modules/@shopify/react-testing/build/cjs/matchers/strings.js +42 -0
  114. package/node_modules/@shopify/react-testing/build/cjs/matchers/utilities.js +110 -0
  115. package/node_modules/@shopify/react-testing/build/cjs/mount.js +76 -0
  116. package/node_modules/@shopify/react-testing/build/cjs/root.js +284 -0
  117. package/node_modules/@shopify/react-testing/build/cjs/toReactString.js +86 -0
  118. package/node_modules/@shopify/react-testing/build/cjs/types.js +28 -0
  119. package/node_modules/@shopify/react-testing/build/esm/TestWrapper.mjs +44 -0
  120. package/node_modules/@shopify/react-testing/build/esm/_virtual/_rollupPluginBabelHelpers.mjs +42 -0
  121. package/node_modules/@shopify/react-testing/build/esm/compat.mjs +10 -0
  122. package/node_modules/@shopify/react-testing/build/esm/destroy.mjs +9 -0
  123. package/node_modules/@shopify/react-testing/build/esm/element.mjs +221 -0
  124. package/node_modules/@shopify/react-testing/build/esm/index.mjs +5 -0
  125. package/node_modules/@shopify/react-testing/build/esm/matchers/components.mjs +41 -0
  126. package/node_modules/@shopify/react-testing/build/esm/matchers/context.mjs +21 -0
  127. package/node_modules/@shopify/react-testing/build/esm/matchers/index.mjs +14 -0
  128. package/node_modules/@shopify/react-testing/build/esm/matchers/props.mjs +33 -0
  129. package/node_modules/@shopify/react-testing/build/esm/matchers/strings.mjs +37 -0
  130. package/node_modules/@shopify/react-testing/build/esm/matchers/utilities.mjs +101 -0
  131. package/node_modules/@shopify/react-testing/build/esm/mount.mjs +70 -0
  132. package/node_modules/@shopify/react-testing/build/esm/root.mjs +275 -0
  133. package/node_modules/@shopify/react-testing/build/esm/toReactString.mjs +80 -0
  134. package/node_modules/@shopify/react-testing/build/esm/types.mjs +26 -0
  135. package/node_modules/@shopify/react-testing/build/esnext/TestWrapper.esnext +44 -0
  136. package/node_modules/@shopify/react-testing/build/esnext/compat.esnext +10 -0
  137. package/node_modules/@shopify/react-testing/build/esnext/destroy.esnext +9 -0
  138. package/node_modules/@shopify/react-testing/build/esnext/element.esnext +221 -0
  139. package/node_modules/@shopify/react-testing/build/esnext/index.esnext +5 -0
  140. package/node_modules/@shopify/react-testing/build/esnext/matchers/components.esnext +41 -0
  141. package/node_modules/@shopify/react-testing/build/esnext/matchers/context.esnext +21 -0
  142. package/node_modules/@shopify/react-testing/build/esnext/matchers/index.esnext +14 -0
  143. package/node_modules/@shopify/react-testing/build/esnext/matchers/props.esnext +33 -0
  144. package/node_modules/@shopify/react-testing/build/esnext/matchers/strings.esnext +37 -0
  145. package/node_modules/@shopify/react-testing/build/esnext/matchers/utilities.esnext +99 -0
  146. package/node_modules/@shopify/react-testing/build/esnext/mount.esnext +71 -0
  147. package/node_modules/@shopify/react-testing/build/esnext/root.esnext +275 -0
  148. package/node_modules/@shopify/react-testing/build/esnext/toReactString.esnext +80 -0
  149. package/node_modules/@shopify/react-testing/build/esnext/types.esnext +26 -0
  150. package/node_modules/@shopify/react-testing/build/ts/TestWrapper.d.ts +17 -0
  151. package/node_modules/@shopify/react-testing/build/ts/TestWrapper.d.ts.map +1 -0
  152. package/node_modules/@shopify/react-testing/build/ts/compat.d.ts +3 -0
  153. package/node_modules/@shopify/react-testing/build/ts/compat.d.ts.map +1 -0
  154. package/node_modules/@shopify/react-testing/build/ts/destroy.d.ts +2 -0
  155. package/node_modules/@shopify/react-testing/build/ts/destroy.d.ts.map +1 -0
  156. package/node_modules/@shopify/react-testing/build/ts/element.d.ts +42 -0
  157. package/node_modules/@shopify/react-testing/build/ts/element.d.ts.map +1 -0
  158. package/node_modules/@shopify/react-testing/build/ts/index.d.ts +7 -0
  159. package/node_modules/@shopify/react-testing/build/ts/index.d.ts.map +1 -0
  160. package/node_modules/@shopify/react-testing/build/ts/matchers/components.d.ts +12 -0
  161. package/node_modules/@shopify/react-testing/build/ts/matchers/components.d.ts.map +1 -0
  162. package/node_modules/@shopify/react-testing/build/ts/matchers/context.d.ts +8 -0
  163. package/node_modules/@shopify/react-testing/build/ts/matchers/context.d.ts.map +1 -0
  164. package/node_modules/@shopify/react-testing/build/ts/matchers/index.d.ts +20 -0
  165. package/node_modules/@shopify/react-testing/build/ts/matchers/index.d.ts.map +1 -0
  166. package/node_modules/@shopify/react-testing/build/ts/matchers/props.d.ts +10 -0
  167. package/node_modules/@shopify/react-testing/build/ts/matchers/props.d.ts.map +1 -0
  168. package/node_modules/@shopify/react-testing/build/ts/matchers/strings.d.ts +11 -0
  169. package/node_modules/@shopify/react-testing/build/ts/matchers/strings.d.ts.map +1 -0
  170. package/node_modules/@shopify/react-testing/build/ts/matchers/utilities.d.ts +17 -0
  171. package/node_modules/@shopify/react-testing/build/ts/matchers/utilities.d.ts.map +1 -0
  172. package/node_modules/@shopify/react-testing/build/ts/mount.d.ts +39 -0
  173. package/node_modules/@shopify/react-testing/build/ts/mount.d.ts.map +1 -0
  174. package/node_modules/@shopify/react-testing/build/ts/root.d.ts +55 -0
  175. package/node_modules/@shopify/react-testing/build/ts/root.d.ts.map +1 -0
  176. package/node_modules/@shopify/react-testing/build/ts/toReactString.d.ts +5 -0
  177. package/node_modules/@shopify/react-testing/build/ts/toReactString.d.ts.map +1 -0
  178. package/node_modules/@shopify/react-testing/build/ts/types.d.ts +89 -0
  179. package/node_modules/@shopify/react-testing/build/ts/types.d.ts.map +1 -0
  180. package/node_modules/@shopify/react-testing/index.esnext +1 -0
  181. package/node_modules/@shopify/react-testing/index.js +1 -0
  182. package/node_modules/@shopify/react-testing/index.mjs +1 -0
  183. package/node_modules/@shopify/react-testing/matchers.esnext +1 -0
  184. package/node_modules/@shopify/react-testing/matchers.js +1 -0
  185. package/node_modules/@shopify/react-testing/matchers.mjs +1 -0
  186. package/node_modules/@shopify/react-testing/package.json +69 -0
  187. package/node_modules/@shopify/ui-extensions-test-utils/CHANGELOG.md +66 -0
  188. package/node_modules/@shopify/ui-extensions-test-utils/dist/index.d.ts +3 -0
  189. package/node_modules/@shopify/ui-extensions-test-utils/dist/index.js +3 -0
  190. package/node_modules/@shopify/ui-extensions-test-utils/dist/render.d.ts +2 -0
  191. package/node_modules/@shopify/ui-extensions-test-utils/dist/render.js +5 -0
  192. package/node_modules/@shopify/ui-extensions-test-utils/dist/renderHook.d.ts +17 -0
  193. package/node_modules/@shopify/ui-extensions-test-utils/dist/renderHook.js +20 -0
  194. package/node_modules/@shopify/ui-extensions-test-utils/dist/withProviders.d.ts +9 -0
  195. package/node_modules/@shopify/ui-extensions-test-utils/dist/withProviders.js +7 -0
  196. package/node_modules/@shopify/ui-extensions-test-utils/package.json +40 -0
  197. package/node_modules/@shopify/ui-extensions-test-utils/project.json +39 -0
  198. package/node_modules/@types/react/LICENSE +21 -0
  199. package/node_modules/@types/react/README.md +16 -0
  200. package/node_modules/@types/react/experimental.d.ts +192 -0
  201. package/node_modules/@types/react/global.d.ts +151 -0
  202. package/node_modules/@types/react/index.d.ts +3175 -0
  203. package/node_modules/@types/react/jsx-dev-runtime.d.ts +2 -0
  204. package/node_modules/@types/react/jsx-runtime.d.ts +2 -0
  205. package/node_modules/@types/react/package.json +149 -0
  206. package/node_modules/@vitejs/plugin-react-refresh/LICENSE +21 -0
  207. package/node_modules/@vitejs/plugin-react-refresh/README.md +73 -0
  208. package/node_modules/@vitejs/plugin-react-refresh/index.d.ts +14 -0
  209. package/node_modules/@vitejs/plugin-react-refresh/index.js +239 -0
  210. package/node_modules/@vitejs/plugin-react-refresh/package.json +35 -0
  211. package/package.json +67 -0
  212. package/project.json +72 -0
  213. package/scripts/create-entry-files.ts +44 -0
  214. package/src/ExtensionServerClient/ExtensionServerClient.test.ts +730 -0
  215. package/src/ExtensionServerClient/ExtensionServerClient.ts +311 -0
  216. package/src/ExtensionServerClient/index.ts +2 -0
  217. package/src/ExtensionServerClient/types.ts +161 -0
  218. package/src/context/ExtensionServerProvider.test.tsx +173 -0
  219. package/src/context/ExtensionServerProvider.tsx +48 -0
  220. package/src/context/constants.ts +15 -0
  221. package/src/context/index.ts +3 -0
  222. package/src/context/types.ts +13 -0
  223. package/src/hooks/index.ts +5 -0
  224. package/src/hooks/useExtensionClient.ts +6 -0
  225. package/src/hooks/useExtensionServerContext.ts +4 -0
  226. package/src/hooks/useExtensionServerEvent.ts +11 -0
  227. package/src/hooks/useExtensionServerState.ts +6 -0
  228. package/src/hooks/useIsomorphicLayoutEffect.ts +6 -0
  229. package/src/i18n.test.ts +417 -0
  230. package/src/i18n.ts +208 -0
  231. package/src/index.ts +7 -0
  232. package/src/state/actions/actions.ts +43 -0
  233. package/src/state/actions/index.ts +2 -0
  234. package/src/state/actions/types.ts +37 -0
  235. package/src/state/index.ts +2 -0
  236. package/src/state/reducers/constants.ts +6 -0
  237. package/src/state/reducers/extensionServerReducer.test.ts +174 -0
  238. package/src/state/reducers/extensionServerReducer.ts +87 -0
  239. package/src/state/reducers/index.ts +3 -0
  240. package/src/state/reducers/types.ts +7 -0
  241. package/src/testing/MockExtensionServerProvider.tsx +36 -0
  242. package/src/testing/app.ts +15 -0
  243. package/src/testing/extensions.ts +70 -0
  244. package/src/testing/index.ts +3 -0
  245. package/src/types.ts +180 -0
  246. package/src/utilities/assetToString.test.ts +16 -0
  247. package/src/utilities/assetToString.ts +8 -0
  248. package/src/utilities/groupByKey.ts +3 -0
  249. package/src/utilities/index.ts +7 -0
  250. package/src/utilities/isUIExtension.ts +7 -0
  251. package/src/utilities/isValidSurface.ts +7 -0
  252. package/src/utilities/noop.ts +1 -0
  253. package/src/utilities/replaceUpdated.test.ts +26 -0
  254. package/src/utilities/replaceUpdated.ts +17 -0
  255. package/src/utilities/set.test.ts +19 -0
  256. package/src/utilities/set.ts +30 -0
  257. package/testing.d.ts +1 -0
  258. package/testing.js +1 -0
  259. package/testing.mjs +1 -0
  260. package/tests/setup.ts +6 -0
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ export * from './context'
2
+ export * from './ExtensionServerClient'
3
+ export * from './hooks'
4
+ export * from './state'
5
+ export * from './types'
6
+ export * from './utilities'
7
+ export * from './i18n'
@@ -0,0 +1,43 @@
1
+ import type {ConnectedAction, UpdateAction, RefreshAction, FocusAction, UnfocusAction, LogAction} from './types'
2
+
3
+ export function createConnectedAction(payload: ConnectedAction['payload']): ConnectedAction {
4
+ return {
5
+ type: 'connected',
6
+ payload,
7
+ }
8
+ }
9
+
10
+ export function createUpdateAction(payload: UpdateAction['payload']): UpdateAction {
11
+ return {
12
+ type: 'update',
13
+ payload,
14
+ }
15
+ }
16
+
17
+ export function createRefreshAction(payload: RefreshAction['payload']): RefreshAction {
18
+ return {
19
+ type: 'refresh',
20
+ payload,
21
+ }
22
+ }
23
+
24
+ export function createFocusAction(payload: FocusAction['payload']): FocusAction {
25
+ return {
26
+ type: 'focus',
27
+ payload,
28
+ }
29
+ }
30
+
31
+ export function createUnfocusAction(payload: UnfocusAction['payload']): UnfocusAction {
32
+ return {
33
+ type: 'unfocus',
34
+ payload,
35
+ }
36
+ }
37
+
38
+ export function createLogAction(payload: LogAction['payload']): LogAction {
39
+ return {
40
+ type: 'log',
41
+ payload,
42
+ }
43
+ }
@@ -0,0 +1,2 @@
1
+ export * from './actions'
2
+ export * from './types'
@@ -0,0 +1,37 @@
1
+ export interface ConnectedAction {
2
+ type: 'connected'
3
+ payload: ExtensionServer.InboundEvents['connected']
4
+ }
5
+
6
+ export interface UpdateAction {
7
+ type: 'update'
8
+ payload: ExtensionServer.InboundEvents['update']
9
+ }
10
+
11
+ export interface RefreshAction {
12
+ type: 'refresh'
13
+ payload: ExtensionServer.InboundEvents['refresh']
14
+ }
15
+
16
+ export interface FocusAction {
17
+ type: 'focus'
18
+ payload: ExtensionServer.InboundEvents['focus']
19
+ }
20
+
21
+ export interface UnfocusAction {
22
+ type: 'unfocus'
23
+ payload: ExtensionServer.InboundEvents['unfocus']
24
+ }
25
+
26
+ export interface LogAction {
27
+ type: 'log'
28
+ payload: ExtensionServer.InboundEvents['log']
29
+ }
30
+
31
+ export type ExtensionServerActions =
32
+ | ConnectedAction
33
+ | UpdateAction
34
+ | RefreshAction
35
+ | FocusAction
36
+ | UnfocusAction
37
+ | LogAction
@@ -0,0 +1,2 @@
1
+ export * from './actions'
2
+ export * from './reducers'
@@ -0,0 +1,6 @@
1
+ import type {ExtensionServerState} from './types'
2
+
3
+ export const INITIAL_STATE: ExtensionServerState = {
4
+ extensions: [],
5
+ store: '',
6
+ }
@@ -0,0 +1,174 @@
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
+ createLogAction,
11
+ } from '../actions'
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
+ // eslint-disable-next-line vitest/no-disabled-tests
97
+ test.skip('refreshes extension url', async () => {
98
+ const extension = mockExtension()
99
+ const previousState: ExtensionServerState = {
100
+ store: 'test-store.com',
101
+ extensions: [extension],
102
+ }
103
+
104
+ const action = createRefreshAction([{uuid: extension.uuid}])
105
+
106
+ const state1 = extensionServerReducer(previousState, action)
107
+ // eslint-disable-next-line node/no-unsupported-features/node-builtins
108
+ const url1 = new URL(state1.extensions[0].assets.main.url)
109
+ const timestamp1 = url1.searchParams.get('lastUpdated') ?? ''
110
+
111
+ expect(timestamp1.length).toBeGreaterThan(0)
112
+
113
+ // sleep 1ms to guarantee new timestamp
114
+ await new Promise((resolve) => setTimeout(resolve, 1))
115
+
116
+ const state2 = extensionServerReducer(state1, action)
117
+ // eslint-disable-next-line node/no-unsupported-features/node-builtins
118
+ const url2 = new URL(state2.extensions[0].assets.main.url)
119
+ const timestamp2 = url2.searchParams.get('lastUpdated') ?? ''
120
+
121
+ expect(timestamp2.length).toBeGreaterThan(0)
122
+ expect(timestamp1).not.toStrictEqual(timestamp2)
123
+ })
124
+
125
+ describe('focus', () => {
126
+ test('focuses only one extension', () => {
127
+ const extension1 = mockExtension()
128
+ const extension2 = mockExtension()
129
+
130
+ const previousState: ExtensionServerState = {
131
+ store: 'test-store.com',
132
+ extensions: [extension1, extension2],
133
+ }
134
+
135
+ const action1 = createFocusAction([{uuid: extension1.uuid}])
136
+ const state1 = extensionServerReducer(previousState, action1)
137
+
138
+ expect(state1.extensions[0].development.focused).toBe(true)
139
+
140
+ const action2 = createFocusAction([{uuid: extension2.uuid}])
141
+ const state2 = extensionServerReducer(state1, action2)
142
+
143
+ expect(state2.extensions[0].development.focused).toBe(false)
144
+ expect(state2.extensions[1].development.focused).toBe(true)
145
+ })
146
+
147
+ test('unfocuses extension', () => {
148
+ const extension = mockExtension({development: {focused: true}})
149
+
150
+ const previousState: ExtensionServerState = {
151
+ store: 'test-store.com',
152
+ extensions: [extension],
153
+ }
154
+
155
+ const action = createUnfocusAction()
156
+ const state = extensionServerReducer(previousState, action)
157
+
158
+ expect(state.extensions[0].development.focused).toBe(false)
159
+ })
160
+ })
161
+
162
+ test('does not mutate the state when receiving log events', () => {
163
+ const extension = mockExtension()
164
+ const previousState: ExtensionServerState = {
165
+ store: 'test-store.com',
166
+ extensions: [extension],
167
+ }
168
+
169
+ const action = createLogAction({level: 'info', args: ['test'], extensionName: extension.name})
170
+ const state = extensionServerReducer(previousState, action)
171
+
172
+ expect(state).toStrictEqual(previousState)
173
+ })
174
+ })
@@ -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,70 @@
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
+ localization: obj.localization as any,
58
+ authenticatedRedirectStartUrl: obj.authenticatedRedirectStartUrl as any,
59
+ authenticatedRedirectRedirectUrls: obj.authenticatedRedirectRedirectUrls as any,
60
+ settings: {
61
+ fields: [
62
+ {
63
+ key: 'sample-key',
64
+ name: 'sample_name',
65
+ type: 'sample_type',
66
+ },
67
+ ],
68
+ },
69
+ }
70
+ }
@@ -0,0 +1,3 @@
1
+ export * from './MockExtensionServerProvider'
2
+ export * from './app'
3
+ export * from './extensions'
package/src/types.ts ADDED
@@ -0,0 +1,180 @@
1
+ /* eslint-disable @typescript-eslint/no-invalid-void-type */
2
+ /* eslint-disable @shopify/strict-component-boundaries */
3
+ import {FlattenedLocalization, Localization} from './i18n'
4
+ import './ExtensionServerClient/types'
5
+ import type {Surface} from './ExtensionServerClient/types'
6
+
7
+ declare global {
8
+ namespace ExtensionServer {
9
+ type ServerEvents =
10
+ | {
11
+ event: 'dispatch'
12
+ data: InboundEvents['dispatch']
13
+ }
14
+ | {
15
+ event: 'connected'
16
+ data: InboundEvents['connected']
17
+ }
18
+ | {
19
+ event: 'update'
20
+ data: InboundEvents['update']
21
+ }
22
+
23
+ interface InboundEvents extends DispatchEvents {
24
+ dispatch: {type: keyof DispatchEvents; payload: DispatchEvents[keyof DispatchEvents]}
25
+ connected: {extensions: ExtensionPayload[]; app?: App; store: string}
26
+ update: {extensions?: ExtensionPayload[]; app?: App}
27
+ }
28
+
29
+ interface OutboundPersistEvents {
30
+ update: {
31
+ extensions?: ({uuid: string} & DeepPartial<ExtensionPayload>)[]
32
+ app?: DeepPartial<App>
33
+ }
34
+ }
35
+
36
+ interface DispatchEvents {
37
+ refresh: {uuid: string}[]
38
+ focus: {uuid: string}[]
39
+ unfocus: void
40
+ navigate: {url: string}
41
+ log: {level: string; args: unknown[]; extensionName: string}
42
+ }
43
+
44
+ // API responses
45
+ namespace API {
46
+ interface BaseResponse {
47
+ app: App
48
+ root: ResourceURL
49
+ socket: ResourceURL
50
+ devConsole: ResourceURL
51
+ store: string
52
+ version: string
53
+ }
54
+
55
+ interface ExtensionsResponse extends BaseResponse {
56
+ extensions: ExtensionPayload[]
57
+ }
58
+
59
+ interface ExtensionResponse extends BaseResponse {
60
+ extension: ExtensionPayload
61
+ }
62
+ }
63
+
64
+ interface UIExtension extends ExtensionPayload {
65
+ extensionPoints: ExtensionPoint[]
66
+ apiVersion: string
67
+ }
68
+ }
69
+ }
70
+
71
+ export type DeepPartial<T> = {
72
+ [P in keyof T]?: DeepPartial<T[P]>
73
+ }
74
+
75
+ export interface ResourceURL {
76
+ url: string
77
+ }
78
+
79
+ export interface Asset extends ResourceURL {
80
+ name: string
81
+ lastUpdated: number
82
+ }
83
+
84
+ export interface Metafield {
85
+ namespace: string
86
+ key: string
87
+ }
88
+
89
+ export interface ExtensionPoint {
90
+ target: string
91
+ surface: Surface
92
+ metafields?: Metafield[]
93
+ resource: ResourceURL
94
+ root: ResourceURL
95
+ localization?: FlattenedLocalization | Localization | null
96
+ name: string
97
+ description?: string
98
+ assets?: {[name: string]: Asset}
99
+ }
100
+
101
+ export type ExtensionPoints = string[] | ExtensionPoint[] | null
102
+
103
+ interface CollectBuyerConsentCapabilities {
104
+ smsMarketing: boolean
105
+ customerPrivacy: boolean
106
+ }
107
+
108
+ interface IframeCapabilities {
109
+ sources: string[]
110
+ }
111
+
112
+ interface Capabilities {
113
+ apiAccess: boolean
114
+ blockProgress: boolean
115
+ networkAccess: boolean
116
+ collectBuyerConsent: CollectBuyerConsentCapabilities
117
+ iframe: IframeCapabilities
118
+ }
119
+
120
+ export interface ExtensionPayload {
121
+ type: string
122
+ externalType: string
123
+ assets: {[name: string]: Asset}
124
+ development: {
125
+ hidden: boolean
126
+ status: Status
127
+ focused?: boolean
128
+ resource: ResourceURL
129
+ root: ResourceURL
130
+ renderer: {
131
+ name: string
132
+ version: string
133
+ }
134
+ }
135
+ uuid: string
136
+ version: string
137
+ surface: Surface
138
+ name: string
139
+ description?: string
140
+ handle: string
141
+ extensionPoints: ExtensionPoints
142
+ capabilities?: Capabilities
143
+ authenticatedRedirectStartUrl?: string
144
+ authenticatedRedirectRedirectUrls?: string[]
145
+ localization?: FlattenedLocalization | Localization | null
146
+ settings?: {
147
+ fields?: {
148
+ type: string
149
+ key?: string
150
+ name?: string
151
+ description?: string
152
+ required?: boolean
153
+ validations?: any[]
154
+ }[]
155
+ }
156
+ }
157
+
158
+ export enum Status {
159
+ Error = 'error',
160
+ Success = 'success',
161
+ }
162
+
163
+ export interface App {
164
+ id: string
165
+ apiKey: string
166
+ url: string
167
+ mobileUrl: string
168
+ applicationUrl: string
169
+ handle?: string | null
170
+ title: string
171
+ developerName?: string
172
+ icon: {
173
+ transformedSrc: string
174
+ }
175
+ installation?: {
176
+ launchUrl: string
177
+ }
178
+ supportEmail?: string
179
+ supportLocales?: string[]
180
+ }