@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,320 @@
1
+ /* eslint-disable no-console */
2
+ import {ExtensionServer} from './server-types.js'
3
+ import {Surface} from './types.js'
4
+ import {
5
+ FlattenedLocalization,
6
+ Localization,
7
+ TRANSLATED_KEYS,
8
+ getFlattenedLocalization,
9
+ isFlattenedTranslations,
10
+ } from '../i18n'
11
+ import {isUIExtension, isValidSurface} from '../utilities'
12
+ import {DeepPartial, ExtensionPayload, ExtensionPoint} from '../types'
13
+
14
+ export class ExtensionServerClient implements ExtensionServer.Client {
15
+ public id: string
16
+
17
+ public connection!: WebSocket
18
+
19
+ public options: ExtensionServer.Options
20
+
21
+ protected EVENT_THAT_WILL_MUTATE_THE_SERVER = ['update']
22
+
23
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
24
+ protected listeners: Record<string, Set<(payload: any) => void>> = {}
25
+ protected connectionListeners: {close: Set<(event: Event) => void>; open: Set<(event: Event) => void>} = {
26
+ close: new Set(),
27
+ open: new Set(),
28
+ }
29
+
30
+ protected connected = false
31
+
32
+ private uiExtensionsByUuid: Record<string, ExtensionServer.UIExtension> = {}
33
+
34
+ constructor(options: DeepPartial<ExtensionServer.Options> = {}) {
35
+ this.id = (Math.random() + 1).toString(36).substring(7)
36
+ this.options = getValidatedOptions({
37
+ ...options,
38
+ connection: {
39
+ automaticConnect: true,
40
+ protocols: [],
41
+ ...(options.connection ?? {}),
42
+ },
43
+ }) as ExtensionServer.Options
44
+
45
+ this.setupConnection(this.options.connection.automaticConnect)
46
+ }
47
+
48
+ public connect(options: ExtensionServer.Options = {connection: {}}) {
49
+ const newOptions = mergeOptions(this.options, options)
50
+ const optionsChanged = JSON.stringify(newOptions) !== JSON.stringify(this.options)
51
+
52
+ if (optionsChanged) {
53
+ this.options = newOptions as ExtensionServer.Options
54
+ this.setupConnection(true)
55
+ }
56
+
57
+ return () => {
58
+ this.closeConnection()
59
+ }
60
+ }
61
+
62
+ public on<TEvent extends keyof ExtensionServer.InboundEvents>(
63
+ event: TEvent,
64
+ listener: (payload: ExtensionServer.InboundEvents[TEvent]) => void,
65
+ ): () => void {
66
+ if (!this.listeners[event as string]) {
67
+ this.listeners[event as string] = new Set()
68
+ }
69
+
70
+ this.listeners[event as string].add(listener)
71
+ return () => this.listeners[event as string].delete(listener)
72
+ }
73
+
74
+ public persist<TEvent extends keyof ExtensionServer.OutboundPersistEvents>(
75
+ event: TEvent,
76
+ data: ExtensionServer.OutboundPersistEvents[TEvent],
77
+ ): void {
78
+ if (this.EVENT_THAT_WILL_MUTATE_THE_SERVER.includes(event as string)) {
79
+ if (!this.options.locales) {
80
+ return this.connection?.send(JSON.stringify({event, data}))
81
+ }
82
+
83
+ /**
84
+ * Since each websocket connection will have its own translated values
85
+ * we need to strip out all translated properties to prevent
86
+ * mutating the Dev Server's data
87
+ * Before:
88
+ * ```
89
+ * {
90
+ * name: 'en name'
91
+ * localization: {...},
92
+ * extensionPoints: [{
93
+ * target: 'admin.product.item.action'
94
+ * name: 'en name'
95
+ * description: 'en description'
96
+ * localization: {...}
97
+ * }],
98
+ * }
99
+ * ```
100
+ * After:
101
+ * ```
102
+ * extensionPoints: [{
103
+ * target: 'admin.product.item.action'
104
+ * }],
105
+ * }
106
+ * ```
107
+ */
108
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
109
+ ;(data as any).extensions?.forEach((extension: ExtensionPayload) => {
110
+ TRANSLATED_KEYS.forEach((key) => {
111
+ if (isUIExtension(extension)) {
112
+ extension.extensionPoints?.forEach((extensionPoint) => {
113
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
114
+ delete (extensionPoint as any)[key]
115
+ })
116
+ }
117
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
118
+ delete (extension as any)[key]
119
+ })
120
+ })
121
+ return this.connection?.send(JSON.stringify({event, data}))
122
+ }
123
+
124
+ console.warn(`You tried to use "persist" with a dispatch event. Please use the "emit" method instead.`)
125
+ }
126
+
127
+ public emit<TEvent extends keyof ExtensionServer.DispatchEvents>(...args: ExtensionServer.EmitArgs<TEvent>): void {
128
+ const event = args[0]
129
+ const data = args.length > 1 ? args[1] : undefined
130
+
131
+ if (this.EVENT_THAT_WILL_MUTATE_THE_SERVER.includes(event as string)) {
132
+ return console.warn(
133
+ `You tried to use "emit" with a the "${event}" event. Please use the "persist" method instead to persist changes to the server.`,
134
+ )
135
+ }
136
+
137
+ this.connection?.send(JSON.stringify({event: 'dispatch', data: {type: event, payload: data}}))
138
+ }
139
+
140
+ public onConnection<TEvent extends keyof typeof this.connectionListeners>(
141
+ event: TEvent,
142
+ listener: (event: Event) => void,
143
+ ): () => void {
144
+ this.connectionListeners[event].add(listener)
145
+ return () => this.connectionListeners[event].delete(listener)
146
+ }
147
+
148
+ protected initializeConnection() {
149
+ if (!this.connection) {
150
+ return
151
+ }
152
+
153
+ this.connection.addEventListener('open', (event) => {
154
+ this.connected = true
155
+ this.connectionListeners.open.forEach((listener) => listener(event))
156
+ })
157
+
158
+ this.connection.addEventListener('close', (event) => {
159
+ this.connected = false
160
+ this.connectionListeners.close.forEach((listener) => listener(event))
161
+ })
162
+
163
+ this.connection?.addEventListener('message', (message) => {
164
+ try {
165
+ const {event, data} = JSON.parse(message.data) as ExtensionServer.ServerEvents
166
+ const typedData = data
167
+
168
+ if (event === 'dispatch') {
169
+ const {type, payload} = typedData as {type: string; payload: unknown}
170
+ this.listeners[type]?.forEach((listener) => listener(payload))
171
+ return
172
+ }
173
+
174
+ const filteredExtensions = typedData.extensions
175
+ ? filterExtensionsBySurface(typedData.extensions, this.options.surface)
176
+ : typedData.extensions
177
+
178
+ this.listeners[event]?.forEach((listener) => {
179
+ listener({...typedData, extensions: this._getLocalizedExtensions(filteredExtensions)})
180
+ })
181
+ // eslint-disable-next-line no-catch-all/no-catch-all
182
+ } catch (err) {
183
+ console.error(
184
+ `[ExtensionServer] Something went wrong while parsing a server message:`,
185
+ err instanceof Error ? err.message : err,
186
+ )
187
+ }
188
+ })
189
+ }
190
+
191
+ protected setupConnection(connectWebsocket = true) {
192
+ if (!this.options.connection.url) {
193
+ return
194
+ }
195
+
196
+ if (!connectWebsocket) {
197
+ return
198
+ }
199
+
200
+ this.closeConnection()
201
+
202
+ this.connection = new WebSocket(this.options.connection.url, this.options.connection.protocols)
203
+
204
+ this.initializeConnection()
205
+ }
206
+
207
+ protected closeConnection() {
208
+ if (this.connected) {
209
+ this.connection?.close()
210
+ }
211
+ }
212
+
213
+ private _getLocalizedExtensions(extensions?: ExtensionPayload[]) {
214
+ return extensions?.map((extension) => {
215
+ if (!this.options.locales || !isUIExtension(extension)) {
216
+ return extension
217
+ }
218
+
219
+ const shouldUpdateTranslations =
220
+ this.uiExtensionsByUuid[extension.uuid]?.localization?.lastUpdated !== extension.localization?.lastUpdated
221
+
222
+ const localization = shouldUpdateTranslations
223
+ ? getFlattenedLocalization(extension.localization, this.options.locales)
224
+ : (this.uiExtensionsByUuid[extension.uuid]?.localization ?? extension.localization)
225
+
226
+ const parsedTranslation: Record<string, string> =
227
+ localization && isFlattenedTranslations(localization) ? JSON.parse(localization.translations) : localization
228
+
229
+ const localizedExtension = {
230
+ ...extension,
231
+ localization,
232
+ name:
233
+ parsedTranslation && extension.name.startsWith('t:')
234
+ ? this._getLocalizedValue(parsedTranslation, extension.name)
235
+ : extension.name,
236
+ ...(extension.description && {
237
+ description:
238
+ parsedTranslation && extension.description?.startsWith('t:')
239
+ ? this._getLocalizedValue(parsedTranslation, extension.description)
240
+ : extension.description,
241
+ }),
242
+ }
243
+
244
+ this.uiExtensionsByUuid[extension.uuid] = {
245
+ ...localizedExtension,
246
+ extensionPoints: this._getLocalizedExtensionPoints(localization, localizedExtension),
247
+ }
248
+
249
+ return this.uiExtensionsByUuid[extension.uuid]
250
+ })
251
+ }
252
+
253
+ private _getLocalizedExtensionPoints(
254
+ localization: FlattenedLocalization | Localization | null | undefined,
255
+ {extensionPoints, name, description}: ExtensionServer.UIExtension,
256
+ ): ExtensionPoint[] {
257
+ if (!localization || !isFlattenedTranslations(localization)) {
258
+ return extensionPoints
259
+ }
260
+
261
+ return extensionPoints?.map((extensionPoint) => {
262
+ return {
263
+ ...extensionPoint,
264
+ localization,
265
+ name,
266
+ ...(description && {description}),
267
+ }
268
+ })
269
+ }
270
+
271
+ private _getLocalizedValue(translations: Record<string, string>, value: string): string {
272
+ const translationKey = value.replace('t:', '')
273
+ return translations[translationKey] || value
274
+ }
275
+ }
276
+
277
+ function mergeOptions(options: ExtensionServer.Options, newOptions: ExtensionServer.Options) {
278
+ return getValidatedOptions({
279
+ ...options,
280
+ ...newOptions,
281
+ connection: {
282
+ ...options.connection,
283
+ ...newOptions.connection,
284
+ },
285
+ })
286
+ }
287
+
288
+ function getValidatedOptions<TOptions extends DeepPartial<ExtensionServer.Options>>(options: TOptions): TOptions {
289
+ if (!isValidSurface(options.surface)) {
290
+ delete options.surface
291
+ }
292
+ return options
293
+ }
294
+
295
+ function filterExtensionsBySurface(extensions: ExtensionPayload[], surface: Surface | undefined): ExtensionPayload[] {
296
+ if (!surface) {
297
+ return extensions
298
+ }
299
+
300
+ return extensions.filter((extension) => {
301
+ if (extension.surface === surface) {
302
+ return true
303
+ }
304
+
305
+ if (Array.isArray(extension.extensionPoints)) {
306
+ const extensionPoints = extension.extensionPoints as (string | {surface: Surface; [key: string]: unknown})[]
307
+ const extensionPointMatchingSurface = extensionPoints.filter((extensionPoint) => {
308
+ if (typeof extensionPoint === 'string') {
309
+ return false
310
+ }
311
+
312
+ return extensionPoint.surface === surface
313
+ })
314
+
315
+ return extensionPointMatchingSurface.length > 0
316
+ }
317
+
318
+ return false
319
+ })
320
+ }
@@ -0,0 +1,2 @@
1
+ export * from './ExtensionServerClient'
2
+ export * from './types'
@@ -0,0 +1,65 @@
1
+ import {Surface} from './types.js'
2
+ import {ExtensionPayload, ExtensionPoint, App} from '../types'
3
+ import {FlattenedLocalization, LocalesOptions, Localization} from '../i18n'
4
+
5
+ // Re-export and augment the global ExtensionServer namespace
6
+ export namespace ExtensionServer {
7
+ export interface UIExtension extends ExtensionPayload {
8
+ extensionPoints: ExtensionPoint[]
9
+ localization?: FlattenedLocalization | Localization | null
10
+ }
11
+
12
+ export interface Client {
13
+ id: string
14
+ connection: WebSocket
15
+ options: Options
16
+ connect(options?: Options): () => void
17
+ on<TEvent extends keyof InboundEvents>(
18
+ event: TEvent,
19
+ listener: (payload: InboundEvents[TEvent]) => void,
20
+ ): () => void
21
+ persist<TEvent extends keyof OutboundPersistEvents>(event: TEvent, data: OutboundPersistEvents[TEvent]): void
22
+ emit<TEvent extends keyof DispatchEvents>(...args: EmitArgs<TEvent>): void
23
+ onConnection<TEvent extends 'close' | 'open'>(event: TEvent, listener: (event: Event) => void): () => void
24
+ }
25
+
26
+ export interface Options {
27
+ connection: {
28
+ url?: string
29
+ automaticConnect?: boolean
30
+ protocols?: string | string[]
31
+ }
32
+ surface?: Surface
33
+ locales?: LocalesOptions
34
+ }
35
+
36
+ export interface ServerEvents {
37
+ event: string
38
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
39
+ data: any
40
+ }
41
+
42
+ export interface InboundEvents extends DispatchEvents {
43
+ dispatch: {type: keyof DispatchEvents; payload: DispatchEvents[keyof DispatchEvents]}
44
+ connected: {extensions: ExtensionPayload[]; app?: App; store: string}
45
+ update: {extensions?: ExtensionPayload[]; app?: App}
46
+ }
47
+
48
+ export interface OutboundPersistEvents {
49
+ update: {
50
+ extensions?: ExtensionPayload[]
51
+ app?: App
52
+ }
53
+ }
54
+
55
+ export interface DispatchEvents {
56
+ refresh: {uuid: string}[]
57
+ focus: {uuid: string}[]
58
+ unfocus: void
59
+ navigate: {url: string}
60
+ }
61
+
62
+ export type EmitArgs<TEvent extends keyof DispatchEvents> = undefined extends DispatchEvents[TEvent]
63
+ ? [event: TEvent]
64
+ : [event: TEvent, payload: DispatchEvents[TEvent]]
65
+ }
@@ -0,0 +1,159 @@
1
+ import type {LocalesOptions} from '../i18n'
2
+
3
+ declare global {
4
+ namespace ExtensionServer {
5
+ /**
6
+ * Events being received by the extension server where the keys are the event names
7
+ * and the values are the payload of the given action. In case no payload is
8
+ * required, a value of void should be used.
9
+ */
10
+ interface InboundEvents {
11
+ //
12
+ }
13
+
14
+ /**
15
+ * Events being sent to the extension server where the keys are the event names
16
+ * and the values are the payload of the given action. In case no payload is
17
+ * required, a value of void should be used.
18
+ *
19
+ * Persist events are those that will generate changes on the server, like
20
+ * update and connected events. Dispatch events are those that will
21
+ * simply be proxied to the clients connected to the server.
22
+ */
23
+ interface OutboundPersistEvents {
24
+ //
25
+ }
26
+
27
+ interface DispatchEvents {
28
+ //
29
+ }
30
+
31
+ /**
32
+ * Extension server client class options. These are used to configure
33
+ * the client class.
34
+ */
35
+ interface Options {
36
+ connection: {
37
+ /**
38
+ * The absolute URL of the WebSocket.
39
+ */
40
+ url?: string
41
+
42
+ /**
43
+ * This defines if we should automatically attempt to connect when the
44
+ * class is instantiated.
45
+ *
46
+ * @defaultValue true
47
+ */
48
+ automaticConnect?: boolean
49
+
50
+ /**
51
+ * The sub-protocol selected by the server.
52
+ *
53
+ * @defaultValue []
54
+ */
55
+ protocols?: string | string[]
56
+ }
57
+ /**
58
+ * If provided the extension server will only return extensions that matches the specified surface
59
+ */
60
+ surface?: Surface
61
+
62
+ /**
63
+ * If provided the extension server will return a requested translations object with flattened
64
+ * translations for each extension matching the requested locales
65
+ */
66
+ locales?: LocalesOptions
67
+ }
68
+
69
+ /**
70
+ * Extension server client class. This class will be used to connect and
71
+ * communicate with the extension server.
72
+ */
73
+ interface Client {
74
+ /**
75
+ * Connection options
76
+ */
77
+ options: Options
78
+
79
+ /**
80
+ * Reconnecting WebSocket Client
81
+ */
82
+ connection: WebSocket
83
+
84
+ /**
85
+ * Function to add an event listener to messages coming from
86
+ * the extension server connection.
87
+ */
88
+ on<TEvent extends keyof ExtensionServer.InboundEvents>(
89
+ event: TEvent,
90
+ cb: EventListener<TEvent>,
91
+ ): EventUnsubscriber
92
+
93
+ /**
94
+ * Function to emit an event that will persist changes to the extension server.
95
+ */
96
+ persist<TEvent extends keyof OutboundPersistEvents>(event: TEvent, payload: OutboundPersistEvents[TEvent]): void
97
+
98
+ /**
99
+ * Function to emit an event to the extension server.
100
+ */
101
+ emit<TEvent extends keyof DispatchEvents>(...args: EmitArgs<TEvent>): void
102
+
103
+ /**
104
+ * Function that opens a connection with the extensions server.
105
+ */
106
+ connect(options?: Options): () => void
107
+ }
108
+
109
+ /**
110
+ * This defines how the ExtensionServer client's static class is defined and the constructor
111
+ * arguments it requires.
112
+ *
113
+ * @example
114
+ * ```
115
+ * const client = new ExtensionServer({ url: 'wss://localhost:1234' });
116
+ * ```
117
+ */
118
+ type StaticClient = Static<ExtensionServer.Client, [option?: ExtensionServer.Options]>
119
+
120
+ // Utilities
121
+
122
+ /**
123
+ * This helper type allows us to account for nullish payloads on the emit function.
124
+ * In practice, this will allow TypeScript to type-check the event being emitted
125
+ * and, if the payload isn't required, the second argument won't be necessary.
126
+ */
127
+ type EmitArgs<TEvent extends keyof ExtensionServer.DispatchEvents> =
128
+ ExtensionServer.DispatchEvents[TEvent] extends void
129
+ ? [event: TEvent]
130
+ : [event: TEvent, payload: ExtensionServer.DispatchEvents[TEvent]]
131
+
132
+ /**
133
+ * This is a helper interface that allows us to define the static methods of a given
134
+ * class. This is useful to define static methods, static properties
135
+ * and constructor variables.
136
+ */
137
+ interface Static<T = unknown, TArgs extends unknown[] = unknown[]> {
138
+ prototype: T
139
+ new (...args: TArgs): T
140
+ }
141
+
142
+ /**
143
+ * This helper creates a partial interface with exception to the defined key values.
144
+ */
145
+ type PartialExcept<TOject, TKey extends keyof TOject> = Partial<Omit<TOject, TKey>> & Pick<TOject, TKey>
146
+
147
+ type EventListener<TEvent extends keyof ExtensionServer.InboundEvents> = (
148
+ payload: ExtensionServer.InboundEvents[TEvent],
149
+ ) => void
150
+
151
+ type EventUnsubscriber = () => void
152
+ }
153
+ }
154
+
155
+ export const AVAILABLE_SURFACES = ['admin', 'checkout', 'post_purchase', 'point_of_sale', 'customer-accounts'] as const
156
+
157
+ export type Surface = (typeof AVAILABLE_SURFACES)[number]
158
+
159
+ export {}