@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.
- package/CHANGELOG.md +157 -0
- package/README.md +75 -0
- package/dist/ExtensionServerClient/ExtensionServerClient.d.ts +27 -0
- package/dist/ExtensionServerClient/ExtensionServerClient.test.d.ts +9 -0
- package/dist/ExtensionServerClient/index.d.ts +2 -0
- package/dist/ExtensionServerClient/server-types.d.ts +66 -0
- package/dist/ExtensionServerClient/types.d.ts +124 -0
- package/dist/_virtual/jsx-runtime.cjs.js +1 -0
- package/dist/_virtual/jsx-runtime.cjs2.js +1 -0
- package/dist/_virtual/jsx-runtime.es.js +5 -0
- package/dist/_virtual/jsx-runtime.es2.js +4 -0
- package/dist/_virtual/react-jsx-runtime.development.cjs.js +1 -0
- package/dist/_virtual/react-jsx-runtime.development.es.js +4 -0
- package/dist/_virtual/react-jsx-runtime.production.min.cjs.js +1 -0
- package/dist/_virtual/react-jsx-runtime.production.min.es.js +4 -0
- package/dist/context/ExtensionServerProvider.d.ts +3 -0
- package/dist/context/ExtensionServerProvider.test.d.ts +1 -0
- package/dist/context/constants.d.ts +2 -0
- package/dist/context/index.d.ts +3 -0
- package/dist/context/types.d.ts +12 -0
- package/dist/hooks/index.d.ts +3 -0
- package/dist/hooks/useExtensionServerContext.d.ts +1 -0
- package/dist/hooks/useExtensionServerState.d.ts +1 -0
- package/dist/hooks/useIsomorphicLayoutEffect.d.ts +2 -0
- package/dist/i18n.d.ts +68 -0
- package/dist/i18n.test.d.ts +1 -0
- package/dist/index.cjs.js +1 -0
- package/dist/index.cjs2.js +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.es.js +46 -0
- package/dist/index.es2.js +8 -0
- package/dist/node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react-jsx-runtime.development.cjs.js +22 -0
- package/dist/node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react-jsx-runtime.development.es.js +604 -0
- package/dist/node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react-jsx-runtime.production.min.cjs.js +9 -0
- package/dist/node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react-jsx-runtime.production.min.es.js +28 -0
- package/dist/node_modules/.pnpm/react@18.3.1/node_modules/react/jsx-runtime.cjs.js +1 -0
- package/dist/node_modules/.pnpm/react@18.3.1/node_modules/react/jsx-runtime.es.js +10 -0
- package/dist/packages/ui-extensions-server-kit/src/ExtensionServerClient/ExtensionServerClient.cjs.js +1 -0
- package/dist/packages/ui-extensions-server-kit/src/ExtensionServerClient/ExtensionServerClient.es.js +136 -0
- package/dist/packages/ui-extensions-server-kit/src/ExtensionServerClient/types.cjs.js +1 -0
- package/dist/packages/ui-extensions-server-kit/src/ExtensionServerClient/types.es.js +4 -0
- package/dist/packages/ui-extensions-server-kit/src/context/ExtensionServerProvider.cjs.js +1 -0
- package/dist/packages/ui-extensions-server-kit/src/context/ExtensionServerProvider.es.js +30 -0
- package/dist/packages/ui-extensions-server-kit/src/context/constants.cjs.js +1 -0
- package/dist/packages/ui-extensions-server-kit/src/context/constants.es.js +13 -0
- package/dist/packages/ui-extensions-server-kit/src/hooks/useExtensionServerContext.cjs.js +1 -0
- package/dist/packages/ui-extensions-server-kit/src/hooks/useExtensionServerContext.es.js +7 -0
- package/dist/packages/ui-extensions-server-kit/src/hooks/useExtensionServerState.cjs.js +1 -0
- package/dist/packages/ui-extensions-server-kit/src/hooks/useExtensionServerState.es.js +9 -0
- package/dist/packages/ui-extensions-server-kit/src/hooks/useIsomorphicLayoutEffect.cjs.js +1 -0
- package/dist/packages/ui-extensions-server-kit/src/hooks/useIsomorphicLayoutEffect.es.js +5 -0
- package/dist/packages/ui-extensions-server-kit/src/i18n.cjs.js +1 -0
- package/dist/packages/ui-extensions-server-kit/src/i18n.es.js +60 -0
- package/dist/packages/ui-extensions-server-kit/src/state/actions/actions.cjs.js +1 -0
- package/dist/packages/ui-extensions-server-kit/src/state/actions/actions.es.js +37 -0
- package/dist/packages/ui-extensions-server-kit/src/state/reducers/constants.cjs.js +1 -0
- package/dist/packages/ui-extensions-server-kit/src/state/reducers/constants.es.js +7 -0
- package/dist/packages/ui-extensions-server-kit/src/state/reducers/extensionServerReducer.cjs.js +1 -0
- package/dist/packages/ui-extensions-server-kit/src/state/reducers/extensionServerReducer.es.js +57 -0
- package/dist/packages/ui-extensions-server-kit/src/testing/MockExtensionServerProvider.cjs.js +1 -0
- package/dist/packages/ui-extensions-server-kit/src/testing/MockExtensionServerProvider.es.js +25 -0
- package/dist/packages/ui-extensions-server-kit/src/testing/app.cjs.js +1 -0
- package/dist/packages/ui-extensions-server-kit/src/testing/app.es.js +16 -0
- package/dist/packages/ui-extensions-server-kit/src/testing/extensions.cjs.js +1 -0
- package/dist/packages/ui-extensions-server-kit/src/testing/extensions.es.js +66 -0
- package/dist/packages/ui-extensions-server-kit/src/types.cjs.js +1 -0
- package/dist/packages/ui-extensions-server-kit/src/types.es.js +4 -0
- package/dist/packages/ui-extensions-server-kit/src/utilities/assetToString.cjs.js +1 -0
- package/dist/packages/ui-extensions-server-kit/src/utilities/assetToString.es.js +7 -0
- package/dist/packages/ui-extensions-server-kit/src/utilities/isUIExtension.cjs.js +1 -0
- package/dist/packages/ui-extensions-server-kit/src/utilities/isUIExtension.es.js +7 -0
- package/dist/packages/ui-extensions-server-kit/src/utilities/isValidSurface.cjs.js +1 -0
- package/dist/packages/ui-extensions-server-kit/src/utilities/isValidSurface.es.js +7 -0
- package/dist/packages/ui-extensions-server-kit/src/utilities/noop.cjs.js +1 -0
- package/dist/packages/ui-extensions-server-kit/src/utilities/noop.es.js +5 -0
- package/dist/packages/ui-extensions-server-kit/src/utilities/replaceUpdated.cjs.js +1 -0
- package/dist/packages/ui-extensions-server-kit/src/utilities/replaceUpdated.es.js +14 -0
- package/dist/packages/ui-extensions-server-kit/src/utilities/set.cjs.js +1 -0
- package/dist/packages/ui-extensions-server-kit/src/utilities/set.es.js +18 -0
- package/dist/state/actions/actions.d.ts +6 -0
- package/dist/state/actions/index.d.ts +2 -0
- package/dist/state/actions/types.d.ts +21 -0
- package/dist/state/index.d.ts +2 -0
- package/dist/state/reducers/constants.d.ts +2 -0
- package/dist/state/reducers/extensionServerReducer.d.ts +3 -0
- package/dist/state/reducers/extensionServerReducer.test.d.ts +1 -0
- package/dist/state/reducers/index.d.ts +3 -0
- package/dist/state/reducers/types.d.ts +6 -0
- package/dist/testing/MockExtensionServerProvider.d.ts +8 -0
- package/dist/testing/app.d.ts +2 -0
- package/dist/testing/extensions.d.ts +6 -0
- package/dist/testing/index.d.ts +3 -0
- package/dist/types.d.ts +196 -0
- package/dist/utilities/assetToString.d.ts +2 -0
- package/dist/utilities/assetToString.test.d.ts +1 -0
- package/dist/utilities/index.d.ts +6 -0
- package/dist/utilities/isUIExtension.d.ts +1 -0
- package/dist/utilities/isValidSurface.d.ts +2 -0
- package/dist/utilities/noop.d.ts +1 -0
- package/dist/utilities/replaceUpdated.d.ts +1 -0
- package/dist/utilities/replaceUpdated.test.d.ts +1 -0
- package/dist/utilities/set.d.ts +4 -0
- package/dist/utilities/set.test.d.ts +1 -0
- package/index.d.ts +1 -0
- package/index.js +1 -0
- package/index.mjs +1 -0
- package/node_modules/@testing-library/react/LICENSE +20 -0
- package/node_modules/@testing-library/react/README.md +692 -0
- package/node_modules/@testing-library/react/dist/@testing-library/react.cjs.js +537 -0
- package/node_modules/@testing-library/react/dist/@testing-library/react.esm.js +497 -0
- package/node_modules/@testing-library/react/dist/@testing-library/react.pure.cjs.js +501 -0
- package/node_modules/@testing-library/react/dist/@testing-library/react.pure.esm.js +461 -0
- package/node_modules/@testing-library/react/dist/@testing-library/react.pure.umd.js +502 -0
- package/node_modules/@testing-library/react/dist/@testing-library/react.pure.umd.js.map +1 -0
- package/node_modules/@testing-library/react/dist/@testing-library/react.pure.umd.min.js +2 -0
- package/node_modules/@testing-library/react/dist/@testing-library/react.pure.umd.min.js.map +1 -0
- package/node_modules/@testing-library/react/dist/@testing-library/react.umd.js +538 -0
- package/node_modules/@testing-library/react/dist/@testing-library/react.umd.js.map +1 -0
- package/node_modules/@testing-library/react/dist/@testing-library/react.umd.min.js +2 -0
- package/node_modules/@testing-library/react/dist/@testing-library/react.umd.min.js.map +1 -0
- package/node_modules/@testing-library/react/dist/act-compat.js +80 -0
- package/node_modules/@testing-library/react/dist/config.js +33 -0
- package/node_modules/@testing-library/react/dist/fire-event.js +70 -0
- package/node_modules/@testing-library/react/dist/index.js +52 -0
- package/node_modules/@testing-library/react/dist/pure.js +356 -0
- package/node_modules/@testing-library/react/dont-cleanup-after-each.js +1 -0
- package/node_modules/@testing-library/react/package.json +116 -0
- package/node_modules/@testing-library/react/pure.d.ts +1 -0
- package/node_modules/@testing-library/react/pure.js +2 -0
- package/node_modules/@testing-library/react/types/index.d.ts +287 -0
- package/node_modules/@testing-library/react/types/pure.d.ts +1 -0
- package/node_modules/@types/react/LICENSE +21 -0
- package/node_modules/@types/react/README.md +15 -0
- package/node_modules/@types/react/canary.d.ts +166 -0
- package/node_modules/@types/react/experimental.d.ts +127 -0
- package/node_modules/@types/react/global.d.ts +160 -0
- package/node_modules/@types/react/index.d.ts +4543 -0
- package/node_modules/@types/react/jsx-dev-runtime.d.ts +45 -0
- package/node_modules/@types/react/jsx-runtime.d.ts +36 -0
- package/node_modules/@types/react/package.json +206 -0
- package/node_modules/@types/react/ts5.0/canary.d.ts +166 -0
- package/node_modules/@types/react/ts5.0/experimental.d.ts +127 -0
- package/node_modules/@types/react/ts5.0/global.d.ts +160 -0
- package/node_modules/@types/react/ts5.0/index.d.ts +4530 -0
- package/node_modules/@types/react/ts5.0/jsx-dev-runtime.d.ts +44 -0
- package/node_modules/@types/react/ts5.0/jsx-runtime.d.ts +35 -0
- package/node_modules/@vitejs/plugin-react/LICENSE +21 -0
- package/node_modules/@vitejs/plugin-react/README.md +159 -0
- package/node_modules/@vitejs/plugin-react/dist/index.d.ts +75 -0
- package/node_modules/@vitejs/plugin-react/dist/index.js +401 -0
- package/node_modules/@vitejs/plugin-react/dist/refresh-runtime.js +663 -0
- package/node_modules/@vitejs/plugin-react/package.json +77 -0
- package/node_modules/@vitejs/plugin-react/types/preamble.d.ts +1 -0
- package/package.json +64 -0
- package/project.json +40 -0
- package/scripts/create-entry-files.ts +44 -0
- package/src/ExtensionServerClient/ExtensionServerClient.test.ts +1238 -0
- package/src/ExtensionServerClient/ExtensionServerClient.ts +320 -0
- package/src/ExtensionServerClient/index.ts +2 -0
- package/src/ExtensionServerClient/server-types.ts +65 -0
- package/src/ExtensionServerClient/types.ts +159 -0
- package/src/context/ExtensionServerProvider.test.tsx +318 -0
- package/src/context/ExtensionServerProvider.tsx +48 -0
- package/src/context/constants.ts +16 -0
- package/src/context/index.ts +3 -0
- package/src/context/types.ts +14 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/useExtensionServerContext.ts +4 -0
- package/src/hooks/useExtensionServerState.ts +6 -0
- package/src/hooks/useIsomorphicLayoutEffect.ts +6 -0
- package/src/i18n.test.ts +417 -0
- package/src/i18n.ts +205 -0
- package/src/index.ts +7 -0
- package/src/state/actions/actions.ts +36 -0
- package/src/state/actions/index.ts +2 -0
- package/src/state/actions/types.ts +26 -0
- package/src/state/index.ts +2 -0
- package/src/state/reducers/constants.ts +6 -0
- package/src/state/reducers/extensionServerReducer.test.ts +165 -0
- package/src/state/reducers/extensionServerReducer.ts +87 -0
- package/src/state/reducers/index.ts +3 -0
- package/src/state/reducers/types.ts +7 -0
- package/src/testing/MockExtensionServerProvider.tsx +36 -0
- package/src/testing/app.ts +15 -0
- package/src/testing/extensions.ts +71 -0
- package/src/testing/index.ts +3 -0
- package/src/types.ts +204 -0
- package/src/utilities/assetToString.test.ts +16 -0
- package/src/utilities/assetToString.ts +7 -0
- package/src/utilities/index.ts +6 -0
- package/src/utilities/isUIExtension.ts +8 -0
- package/src/utilities/isValidSurface.ts +6 -0
- package/src/utilities/noop.ts +1 -0
- package/src/utilities/replaceUpdated.test.ts +26 -0
- package/src/utilities/replaceUpdated.ts +15 -0
- package/src/utilities/set.test.ts +19 -0
- package/src/utilities/set.ts +35 -0
- package/testing.d.ts +1 -0
- package/testing.js +1 -0
- package/testing.mjs +1 -0
- package/tests/setup.ts +3 -0
- 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,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
|
+
}
|
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,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
|
+
}
|