Skip to content

Commit

Permalink
Merge pull request #5118 from Shopify/12-17-make_developer_console_su…
Browse files Browse the repository at this point in the history
…pport_create_delete_extensions_mid-dev

Make developer console support create/delete extensions mid-dev
  • Loading branch information
isaacroldan authored Jan 7, 2025
2 parents abebe81 + 147046b commit 49f23ca
Show file tree
Hide file tree
Showing 7 changed files with 64 additions and 25 deletions.
4 changes: 2 additions & 2 deletions packages/app/src/cli/models/extensions/extension-instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {DeveloperPlatformClient} from '../../utilities/developer-platform-client
import {AppConfigurationWithoutPath, CurrentAppConfiguration} from '../app/app.js'
import {ok} from '@shopify/cli-kit/node/result'
import {constantize, slugify} from '@shopify/cli-kit/common/string'
import {hashString, randomUUID} from '@shopify/cli-kit/node/crypto'
import {hashString, nonRandomUUID, randomUUID} from '@shopify/cli-kit/node/crypto'
import {partnersFqdn} from '@shopify/cli-kit/node/context/fqdn'
import {joinPath, basename} from '@shopify/cli-kit/node/path'
import {fileExists, touchFile, moveFile, writeFile, glob} from '@shopify/cli-kit/node/fs'
Expand Down Expand Up @@ -149,8 +149,8 @@ export class ExtensionInstance<TConfiguration extends BaseConfigType = BaseConfi
this.entrySourceFilePath = options.entryPath ?? ''
this.directory = options.directory
this.specification = options.specification
this.devUUID = `dev-${randomUUID()}`
this.handle = this.buildHandle()
this.devUUID = `dev-${nonRandomUUID(this.handle)}`
this.localIdentifier = this.handle
this.idEnvironmentVariableName = `SHOPIFY_${constantize(this.localIdentifier)}_ID`
this.outputPath = this.directory
Expand Down
3 changes: 2 additions & 1 deletion packages/app/src/cli/services/dev/extension.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ describe('devUIExtensions()', () => {

// THEN
expect(server.setupHTTPServer).toHaveBeenCalledWith({
devOptions: options,
devOptions: {...options, websocketURL: 'wss://mock.url/extensions'},
payloadStore: {mock: 'payload-store'},
})
})
Expand All @@ -82,6 +82,7 @@ describe('devUIExtensions()', () => {
...options,
httpServer: expect.objectContaining({mock: 'http-server'}),
payloadStore: {mock: 'payload-store'},
websocketURL: 'wss://mock.url/extensions',
})
})

Expand Down
57 changes: 42 additions & 15 deletions packages/app/src/cli/services/dev/extension.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
/* eslint-disable no-await-in-loop */
import {setupWebsocketConnection} from './extension/websocket.js'
import {setupHTTPServer} from './extension/server.js'
import {ExtensionsPayloadStore, getExtensionsPayloadStoreRawPayload} from './extension/payload/store.js'
import {AppEvent, AppEventWatcher} from './app-events/app-event-watcher.js'
import {
ExtensionsPayloadStore,
ExtensionsPayloadStoreOptions,
getExtensionsPayloadStoreRawPayload,
} from './extension/payload/store.js'
import {AppEvent, AppEventWatcher, EventType} from './app-events/app-event-watcher.js'
import {buildCartURLIfNeeded} from './extension/utilities.js'
import {ExtensionInstance} from '../../models/extensions/extension-instance.js'
import {AbortSignal} from '@shopify/cli-kit/node/abort'
import {outputDebug} from '@shopify/cli-kit/node/output'
Expand Down Expand Up @@ -109,32 +115,53 @@ export interface ExtensionDevOptions {
}

export async function devUIExtensions(options: ExtensionDevOptions): Promise<void> {
const payloadStoreOptions = {
const payloadOptions: ExtensionsPayloadStoreOptions = {
...options,
websocketURL: getWebSocketUrl(options.url),
}
const bundlePath = options.appWatcher.buildOutputPath
const payloadStoreRawPayload = await getExtensionsPayloadStoreRawPayload(payloadStoreOptions, bundlePath)
const payloadStore = new ExtensionsPayloadStore(payloadStoreRawPayload, payloadStoreOptions)

outputDebug(`Setting up the UI extensions HTTP server...`, options.stdout)
const httpServer = setupHTTPServer({devOptions: options, payloadStore})
// NOTE: Always use `payloadOptions`, never `options` directly. This way we can mutate `payloadOptions` without
// affecting the original `options` object and we only need to care about `payloadOptions` in this function.

outputDebug(`Setting up the UI extensions Websocket server...`, options.stdout)
const websocketConnection = setupWebsocketConnection({...options, httpServer, payloadStore})
outputDebug(`Setting up the UI extensions bundler and file watching...`, options.stdout)
const bundlePath = payloadOptions.appWatcher.buildOutputPath
const payloadStoreRawPayload = await getExtensionsPayloadStoreRawPayload(payloadOptions, bundlePath)
const payloadStore = new ExtensionsPayloadStore(payloadStoreRawPayload, payloadOptions)

outputDebug(`Setting up the UI extensions HTTP server...`, payloadOptions.stdout)
const httpServer = setupHTTPServer({devOptions: payloadOptions, payloadStore})

outputDebug(`Setting up the UI extensions Websocket server...`, payloadOptions.stdout)
const websocketConnection = setupWebsocketConnection({...payloadOptions, httpServer, payloadStore})
outputDebug(`Setting up the UI extensions bundler and file watching...`, payloadOptions.stdout)

const eventHandler = async ({extensionEvents}: AppEvent) => {
for (const event of extensionEvents) {
const status = event.buildResult?.status === 'ok' ? 'success' : 'error'
// eslint-disable-next-line no-await-in-loop
await payloadStore.updateExtension(event.extension, options, bundlePath, {status})

switch (event.type) {
case EventType.Created:
payloadOptions.extensions.push(event.extension)
if (!payloadOptions.checkoutCartUrl) {
const cartUrl = await buildCartURLIfNeeded(payloadOptions.extensions, payloadOptions.storeFqdn)
// eslint-disable-next-line require-atomic-updates
payloadOptions.checkoutCartUrl = cartUrl
}
await payloadStore.addExtension(event.extension, bundlePath)
break
case EventType.Updated:
await payloadStore.updateExtension(event.extension, payloadOptions, bundlePath, {status})
break
case EventType.Deleted:
payloadOptions.extensions = payloadOptions.extensions.filter((ext) => ext.devUUID !== event.extension.devUUID)
await payloadStore.deleteExtension(event.extension)
break
}
}
}

options.appWatcher.onEvent(eventHandler).onStart(eventHandler)
payloadOptions.appWatcher.onEvent(eventHandler).onStart(eventHandler)

options.signal.addEventListener('abort', () => {
payloadOptions.signal.addEventListener('abort', () => {
outputDebug('Closing the UI extensions dev server...')
websocketConnection.close()
httpServer.close()
Expand Down
13 changes: 13 additions & 0 deletions packages/app/src/cli/services/dev/extension/payload/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,19 @@ export class ExtensionsPayloadStore extends EventEmitter {
this.emitUpdate([extension.devUUID])
}

deleteExtension(extension: ExtensionInstance) {
const index = this.rawPayload.extensions.findIndex((ext) => ext.uuid === extension.devUUID)
if (index !== -1) {
this.rawPayload.extensions.splice(index, 1)
this.emitUpdate([extension.devUUID])
}
}

async addExtension(extension: ExtensionInstance, bundlePath: string) {
this.rawPayload.extensions.push(await getUIExtensionPayload(extension, bundlePath, this.options))
this.emitUpdate([extension.devUUID])
}

private emitUpdate(extensionIds: string[]) {
this.emit(ExtensionsPayloadStoreEvent.Update, extensionIds)
}
Expand Down
5 changes: 2 additions & 3 deletions packages/app/src/cli/services/dev/extension/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,12 @@ import {
noCacheMiddleware,
redirectToDevConsoleMiddleware,
} from './server/middlewares.js'
import {ExtensionsPayloadStore} from './payload/store.js'
import {ExtensionDevOptions} from '../extension.js'
import {ExtensionsPayloadStore, ExtensionsPayloadStoreOptions} from './payload/store.js'
import {createApp, createRouter} from 'h3'
import {createServer} from 'http'

interface SetupHTTPServerOptions {
devOptions: ExtensionDevOptions
devOptions: ExtensionsPayloadStoreOptions
payloadStore: ExtensionsPayloadStore
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ describe('setupWebsocketConnection', () => {
const payloadStore: ExtensionsPayloadStore = {on: vi.fn()} as any
const httpServer: Server = {on: vi.fn()} as any
const devOptions = {} as unknown as ExtensionDevOptions
const options = {...devOptions, httpServer, payloadStore}
const options = {...devOptions, httpServer, payloadStore, websocketURL: 'wss://mock.url/extensions'}

beforeEach(() => {
vi.useFakeTimers()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {ExtensionsPayloadStore} from '../payload/store.js'
import {ExtensionDevOptions} from '../../extension.js'
import {ExtensionsPayloadStore, ExtensionsPayloadStoreOptions} from '../payload/store.js'
import {Server} from 'http'

export enum EventType {
Expand All @@ -11,7 +10,7 @@ type DataType = 'focus' | 'unfocus'

type DataPayload = {uuid: string}[]

export type SetupWebSocketConnectionOptions = ExtensionDevOptions & {
export type SetupWebSocketConnectionOptions = ExtensionsPayloadStoreOptions & {
httpServer: Server
payloadStore: ExtensionsPayloadStore
}
Expand Down

0 comments on commit 49f23ca

Please sign in to comment.