Skip to content

Commit 73363b9

Browse files
committed
notify extension dev server of app assets updates
1 parent 7efd3f7 commit 73363b9

File tree

11 files changed

+107
-3
lines changed

11 files changed

+107
-3
lines changed

packages/app/src/cli/models/app/app.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ export interface AppInterface<
229229
realExtensions: ExtensionInstance[]
230230
nonConfigExtensions: ExtensionInstance[]
231231
draftableExtensions: ExtensionInstance[]
232+
appAssetsConfigs: Record<string, string> | undefined
232233
errors: AppErrors
233234
hiddenConfig: AppHiddenConfig
234235
includeConfigOnDeploy: boolean | undefined
@@ -334,6 +335,15 @@ export class App<
334335
)
335336
}
336337

338+
get appAssetsConfigs(): Record<string, string> | undefined {
339+
if (!this.realExtensions.some((ext) => ext.specification.appAssetsConfig)) return undefined
340+
return this.realExtensions.reduce<Record<string, string>>((acc, ext) => {
341+
const config = ext.specification.appAssetsConfig?.(ext.configuration)
342+
if (config) acc[config.assetsKey] = config.assetsDir
343+
return acc
344+
}, {})
345+
}
346+
337347
setDevApplicationURLs(devApplicationURLs: ApplicationURLs) {
338348
this.patchAppConfiguration(devApplicationURLs)
339349
this.realExtensions.forEach((ext) => ext.patchWithAppDevURLs(devApplicationURLs))

packages/app/src/cli/models/extensions/specification.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,19 @@ export interface ExtensionSpecification<TConfiguration extends BaseConfigType =
143143
* or undefined to watch all files in the extension directory.
144144
*/
145145
devSessionWatchConfig?: (extension: ExtensionInstance<TConfiguration>) => DevSessionWatchConfig | undefined
146+
147+
/**
148+
* App assets configuration for this extension.
149+
* Return undefined if this extension doesn't serve app assets.
150+
*/
151+
appAssetsConfig?: (config: TConfiguration) => AppAssetsConfig | undefined
152+
}
153+
154+
export interface AppAssetsConfig {
155+
/** The config key that points to the assets directory (e.g. 'admin.static_root') */
156+
assetsKey: string
157+
/** The assets directory relative to the extension directory */
158+
assetsDir: string
146159
}
147160

148161
export interface DevSessionWatchConfig {

packages/app/src/cli/models/extensions/specifications/admin.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@ const adminSpecificationSpec = createExtensionSpecification({
6060
},
6161
],
6262
appModuleFeatures: () => [],
63+
appAssetsConfig: (config) => {
64+
const dir = config.admin?.static_root
65+
if (!dir) return undefined
66+
return {assetsKey: 'staticRoot', assetsDir: dir}
67+
},
6368
})
6469

6570
export default adminSpecificationSpec

packages/app/src/cli/services/dev/extension.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {ExtensionInstance} from '../../models/extensions/extension-instance.js'
1212
import {AbortSignal} from '@shopify/cli-kit/node/abort'
1313
import {outputDebug} from '@shopify/cli-kit/node/output'
1414
import {DotEnvFile} from '@shopify/cli-kit/node/dot-env'
15+
import {getArrayRejectingUndefined} from '@shopify/cli-kit/common/array'
1516
import {Writable} from 'stream'
1617

1718
export interface ExtensionDevOptions {
@@ -112,6 +113,11 @@ export interface ExtensionDevOptions {
112113
* The app watcher that emits events when the app is updated
113114
*/
114115
appWatcher: AppEventWatcher
116+
117+
/**
118+
* Map of asset key to absolute directory path for app-level assets (e.g., admin static_root)
119+
*/
120+
appAssets?: Record<string, string>
115121
}
116122

117123
export async function devUIExtensions(options: ExtensionDevOptions): Promise<void> {
@@ -133,7 +139,13 @@ export async function devUIExtensions(options: ExtensionDevOptions): Promise<voi
133139
}
134140

135141
outputDebug(`Setting up the UI extensions HTTP server...`, payloadOptions.stdout)
136-
const httpServer = setupHTTPServer({devOptions: payloadOptions, payloadStore, getExtensions})
142+
const getAppAssets = () => payloadOptions.appAssets
143+
const httpServer = setupHTTPServer({
144+
devOptions: payloadOptions,
145+
payloadStore,
146+
getExtensions,
147+
getAppAssets,
148+
})
137149

138150
outputDebug(`Setting up the UI extensions Websocket server...`, payloadOptions.stdout)
139151
const websocketConnection = setupWebsocketConnection({...payloadOptions, httpServer, payloadStore})
@@ -144,6 +156,14 @@ export async function devUIExtensions(options: ExtensionDevOptions): Promise<voi
144156
extensions = app.allExtensions.filter((ext) => ext.isPreviewable)
145157
}
146158

159+
// Handle App Assets updates.
160+
const appAssetsConfigs = extensionEvents.map((event) =>
161+
event.extension.specification.appAssetsConfig?.(event.extension.configuration),
162+
)
163+
getArrayRejectingUndefined(appAssetsConfigs).forEach((config) => {
164+
payloadStore.updateAppAssetTimestamp(config.assetsKey)
165+
})
166+
147167
for (const event of extensionEvents) {
148168
if (!event.extension.isPreviewable) continue
149169
const status = event.buildResult?.status === 'ok' ? 'success' : 'error'

packages/app/src/cli/services/dev/extension/payload/models.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ interface ExtensionsPayloadInterface {
88
url: string
99
mobileUrl: string
1010
title: string
11+
assets?: {
12+
[key: string]: {
13+
url: string
14+
lastUpdated: number
15+
}
16+
}
1117
}
1218
appId?: string
1319
store: string

packages/app/src/cli/services/dev/extension/payload/store.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {EventEmitter} from 'events'
99

1010
export interface ExtensionsPayloadStoreOptions extends ExtensionDevOptions {
1111
websocketURL: string
12+
appAssets?: Record<string, string>
1213
}
1314

1415
export enum ExtensionsPayloadStoreEvent {
@@ -19,7 +20,7 @@ export async function getExtensionsPayloadStoreRawPayload(
1920
options: Omit<ExtensionsPayloadStoreOptions, 'appWatcher'>,
2021
bundlePath: string,
2122
): Promise<ExtensionsEndpointPayload> {
22-
return {
23+
const payload: ExtensionsEndpointPayload = {
2324
app: {
2425
title: options.appName,
2526
apiKey: options.apiKey,
@@ -40,6 +41,18 @@ export async function getExtensionsPayloadStoreRawPayload(
4041
store: options.storeFqdn,
4142
extensions: await Promise.all(options.extensions.map((ext) => getUIExtensionPayload(ext, bundlePath, options))),
4243
}
44+
45+
if (options.appAssets) {
46+
const assets: Record<string, {url: string; lastUpdated: number}> = {}
47+
for (const assetKey of Object.keys(options.appAssets)) {
48+
assets[assetKey] = {
49+
url: new URL(`/extensions/assets/${assetKey}/`, options.url).toString(),
50+
lastUpdated: Date.now(),
51+
}
52+
}
53+
payload.app.assets = assets
54+
}
55+
return payload
4356
}
4457

4558
export class ExtensionsPayloadStore extends EventEmitter {
@@ -170,6 +183,14 @@ export class ExtensionsPayloadStore extends EventEmitter {
170183
this.emitUpdate([extension.devUUID])
171184
}
172185

186+
updateAppAssetTimestamp(assetKey: string) {
187+
const asset = this.rawPayload.app.assets?.[assetKey]
188+
if (asset) {
189+
asset.lastUpdated = Date.now()
190+
this.emitUpdate([])
191+
}
192+
}
193+
173194
private emitUpdate(extensionIds: string[]) {
174195
this.emit(ExtensionsPayloadStoreEvent.Update, extensionIds)
175196
}

packages/app/src/cli/services/dev/extension/server.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
corsMiddleware,
33
devConsoleAssetsMiddleware,
44
devConsoleIndexMiddleware,
5+
getAppAssetsMiddleware,
56
getExtensionAssetMiddleware,
67
getExtensionPayloadMiddleware,
78
getExtensionPointMiddleware,
@@ -19,6 +20,7 @@ interface SetupHTTPServerOptions {
1920
devOptions: ExtensionsPayloadStoreOptions
2021
payloadStore: ExtensionsPayloadStore
2122
getExtensions: () => ExtensionInstance[]
23+
getAppAssets?: () => Record<string, string> | undefined
2224
}
2325

2426
export function setupHTTPServer(options: SetupHTTPServerOptions) {
@@ -28,6 +30,9 @@ export function setupHTTPServer(options: SetupHTTPServerOptions) {
2830
httpApp.use(getLogMiddleware(options))
2931
httpApp.use(corsMiddleware)
3032
httpApp.use(noCacheMiddleware)
33+
if (options.getAppAssets) {
34+
httpRouter.use('/extensions/assets/:assetKey/**:filePath', getAppAssetsMiddleware(options.getAppAssets))
35+
}
3136
httpRouter.use('/extensions/dev-console', devConsoleIndexMiddleware)
3237
httpRouter.use('/extensions/dev-console/assets/**:assetPath', devConsoleAssetsMiddleware)
3338
httpRouter.use('/extensions/:extensionId', getExtensionPayloadMiddleware(options))

packages/app/src/cli/services/dev/extension/server/middlewares.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,20 @@ export const devConsoleAssetsMiddleware = defineEventHandler(async (event) => {
134134
})
135135
})
136136

137+
export function getAppAssetsMiddleware(getAppAssets: () => Record<string, string> | undefined) {
138+
return defineEventHandler(async (event) => {
139+
const {assetKey = '', filePath = ''} = getRouterParams(event)
140+
const appAssets = getAppAssets()
141+
const directory = appAssets?.[assetKey]
142+
if (!directory) {
143+
return sendError(event, {statusCode: 404, statusMessage: `No app assets configured for key: ${assetKey}`})
144+
}
145+
return fileServerMiddleware(event, {
146+
filePath: joinPath(directory, filePath),
147+
})
148+
})
149+
}
150+
137151
export function getLogMiddleware({devOptions}: GetExtensionsMiddlewareOptions) {
138152
return defineEventHandler((event) => {
139153
outputDebug(`UI extensions server received a ${event.method} request to URL ${event.path}`, devOptions.stdout)

packages/app/src/cli/services/dev/processes/previewable-extension.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ interface PreviewableExtensionOptions {
2424
grantedScopes: string[]
2525
previewableExtensions: ExtensionInstance[]
2626
appWatcher: AppEventWatcher
27+
appAssetsConfigs: Record<string, string>
2728
}
2829

2930
export interface PreviewableExtensionProcess extends BaseProcess<PreviewableExtensionOptions> {
@@ -47,6 +48,7 @@ export const launchPreviewableExtensionProcess: DevProcessFunction<PreviewableEx
4748
previewableExtensions,
4849
appDirectory,
4950
appWatcher,
51+
appAssetsConfigs,
5052
},
5153
) => {
5254
await devUIExtensions({
@@ -68,6 +70,7 @@ export const launchPreviewableExtensionProcess: DevProcessFunction<PreviewableEx
6870
subscriptionProductUrl,
6971
manifestVersion: MANIFEST_VERSION,
7072
appWatcher,
73+
appAssets: appAssetsConfigs,
7174
})
7275
}
7376

packages/app/src/cli/services/dev/processes/setup-dev-processes.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ export async function setupDevProcesses({
149149
})
150150
: undefined,
151151
await setupPreviewableExtensionsProcess({
152-
allExtensions: reloadedApp.allExtensions,
152+
allExtensions: reloadedApp.realExtensions,
153153
storeFqdn,
154154
storeId,
155155
apiKey,
@@ -162,6 +162,7 @@ export async function setupDevProcesses({
162162
appId: remoteApp.id,
163163
appDirectory: reloadedApp.directory,
164164
appWatcher,
165+
appAssetsConfigs: reloadedApp.appAssetsConfigs,
165166
}),
166167
developerPlatformClient.supportsDevSessions
167168
? await setupDevSessionProcess({

0 commit comments

Comments
 (0)