Skip to content

Commit e28e357

Browse files
authored
Merge pull request #5150 from Shopify/01-03-use_uid_instead_of_handle_to_identify_extensions_in_app-watcher
Use UID instead of handle to identify extensions in app-watcher
2 parents 6b8c002 + e433cf6 commit e28e357

File tree

10 files changed

+80
-66
lines changed

10 files changed

+80
-66
lines changed

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ export interface AppInterface<
273273
*/
274274
creationDefaultOptions(): AppCreationDefaultOptions
275275
manifest: () => Promise<JsonMapType>
276-
removeExtension: (extensionHandle: string) => void
276+
removeExtension: (extensionUid: string) => void
277277
}
278278

279279
type AppConstructor<
@@ -441,8 +441,8 @@ export class App<
441441
}
442442
}
443443

444-
removeExtension(extensionHandle: string) {
445-
this.realExtensions = this.realExtensions.filter((ext) => ext.handle !== extensionHandle)
444+
removeExtension(extensionUid: string) {
445+
this.realExtensions = this.realExtensions.filter((ext) => ext.uid !== extensionUid)
446446
}
447447

448448
get includeConfigOnDeploy() {

packages/app/src/cli/models/extensions/extension-instance.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import {DeveloperPlatformClient} from '../../utilities/developer-platform-client
2525
import {AppConfigurationWithoutPath, CurrentAppConfiguration} from '../app/app.js'
2626
import {ok} from '@shopify/cli-kit/node/result'
2727
import {constantize, slugify} from '@shopify/cli-kit/common/string'
28-
import {hashString, nonRandomUUID, randomUUID} from '@shopify/cli-kit/node/crypto'
28+
import {hashString, nonRandomUUID} from '@shopify/cli-kit/node/crypto'
2929
import {partnersFqdn} from '@shopify/cli-kit/node/context/fqdn'
3030
import {joinPath, basename} from '@shopify/cli-kit/node/path'
3131
import {fileExists, touchFile, moveFile, writeFile, glob} from '@shopify/cli-kit/node/fs'
@@ -150,11 +150,12 @@ export class ExtensionInstance<TConfiguration extends BaseConfigType = BaseConfi
150150
this.directory = options.directory
151151
this.specification = options.specification
152152
this.handle = this.buildHandle()
153-
this.devUUID = `dev-${nonRandomUUID(this.handle)}`
153+
const uuidFromHandle = nonRandomUUID(this.handle)
154+
this.devUUID = `dev-${uuidFromHandle}`
154155
this.localIdentifier = this.handle
155156
this.idEnvironmentVariableName = `SHOPIFY_${constantize(this.localIdentifier)}_ID`
156157
this.outputPath = this.directory
157-
this.uid = this.configuration.uid ?? randomUUID()
158+
this.uid = this.configuration.uid ?? uuidFromHandle
158159

159160
if (this.features.includes('esbuild') || this.type === 'tax_calculation') {
160161
this.outputPath = joinPath(this.directory, 'dist', this.outputFileName)
@@ -369,7 +370,7 @@ export class ExtensionInstance<TConfiguration extends BaseConfigType = BaseConfi
369370
}
370371

371372
getOutputFolderId(extensionId?: string) {
372-
return extensionId ?? this.configuration.uid ?? this.handle
373+
return extensionId ?? this.uid ?? this.handle
373374
}
374375

375376
getOutputPathForDirectory(directory: string, extensionId?: string) {

packages/app/src/cli/services/dev/app-events/app-diffing.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import {appDiff} from './app-diffing.js'
22
import {testApp, testAppConfigExtensions, testUIExtension} from '../../../models/app/app.test-data.js'
33
import {describe, expect, test} from 'vitest'
44

5-
const extension1 = await testUIExtension({type: 'ui_extension', handle: 'h1', directory: '/extensions/ui_extension_1'})
6-
const extension2 = await testUIExtension({type: 'ui_extension', directory: '/extensions/ui_extension_2'})
5+
const extension1 = await testUIExtension({type: 'ui_extension', directory: '/extensions/ui_extension_1', uid: 'uid1'})
6+
const extension2 = await testUIExtension({type: 'ui_extension', directory: '/extensions/ui_extension_2', uid: 'uid2'})
77
const posExtension = await testAppConfigExtensions()
88
const posExtensionWithDifferentConfig = await testAppConfigExtensions(true)
99

packages/app/src/cli/services/dev/app-events/app-diffing.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,21 @@ interface AppExtensionsDiff {
2020
*/
2121
export function appDiff(app: AppInterface, newApp: AppInterface, includeUpdated = true): AppExtensionsDiff {
2222
const oldExtensions = app.realExtensions
23-
const oldExtensionsHandles = oldExtensions.map((ext) => ext.handle)
23+
const oldExtensionsUids = oldExtensions.map((ext) => ext.uid)
2424
const newExtensions = newApp.realExtensions
25-
const newExtensionsHandles = newExtensions.map((ext) => ext.handle)
25+
const newExtensionsUids = newExtensions.map((ext) => ext.uid)
2626

27-
const createdExtensions = newExtensions.filter((ext) => !oldExtensionsHandles.includes(ext.handle))
28-
const deletedExtensions = oldExtensions.filter((ext) => !newExtensionsHandles.includes(ext.handle))
27+
const createdExtensions = newExtensions.filter((ext) => !oldExtensionsUids.includes(ext.uid))
28+
const deletedExtensions = oldExtensions.filter((ext) => !newExtensionsUids.includes(ext.uid))
2929

3030
let updatedExtensions
3131
if (includeUpdated) {
3232
updatedExtensions = newExtensions.filter((ext) => {
33-
const oldConfig = oldExtensions.find((oldExt) => oldExt.handle === ext.handle)?.configuration
34-
const newConfig = ext.configuration
35-
if (oldConfig === undefined) return false
36-
return JSON.stringify(oldConfig) !== JSON.stringify(newConfig)
33+
const oldExtension = oldExtensions.find((oldExt) => oldExt.uid === ext.uid)
34+
if (!oldExtension) return false
35+
const configChanged = JSON.stringify(oldExtension.configuration) !== JSON.stringify(ext.configuration)
36+
const extensionPathChanged = oldExtension.configurationPath !== ext.configurationPath
37+
return configChanged || extensionPathChanged
3738
})
3839
}
3940

packages/app/src/cli/services/dev/app-events/app-event-watcher-handler.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@ export async function handleWatcherEvents(
2424
const appReloadNeeded = events.some((event) => eventsThatRequireReload.includes(event.type))
2525
const otherEvents = events.filter((event) => !eventsThatRequireReload.includes(event.type))
2626

27-
let appEvent: AppEvent = {app, extensionEvents: [], path: events[0].path, startTime: events[0].startTime}
28-
if (appReloadNeeded) appEvent = await ReloadAppHandler({event: events[0], app, options, extensions: []})
27+
if (appReloadNeeded) return ReloadAppHandler({event: events[0], app, options, extensions: []})
28+
29+
const appEvent: AppEvent = {app, extensionEvents: [], path: events[0].path, startTime: events[0].startTime}
2930

3031
for (const event of otherEvents) {
3132
const affectedExtensions = app.realExtensions.filter((ext) => ext.directory === event.extensionPath)
@@ -73,7 +74,7 @@ const handlers: {[key in WatcherEvent['type']]: Handler} = {
7374
*/
7475
function ExtensionFolderDeletedHandler({event, app, extensions}: HandlerInput): AppEvent {
7576
const events = extensions.map((ext) => {
76-
app.removeExtension(ext.handle)
77+
app.removeExtension(ext.uid)
7778
return {type: EventType.Deleted, extension: ext}
7879
})
7980
return {app, extensionEvents: events, startTime: event.startTime, path: event.path}

packages/app/src/cli/services/dev/app-events/app-event-watcher.test.ts

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,16 @@ import {AbortSignal, AbortController} from '@shopify/cli-kit/node/abort'
1717
import {flushPromises} from '@shopify/cli-kit/node/promises'
1818
import {inTemporaryDirectory} from '@shopify/cli-kit/node/fs'
1919
import {joinPath} from '@shopify/cli-kit/node/path'
20+
import {nonRandomUUID} from '@shopify/cli-kit/node/crypto'
2021
import {Writable} from 'stream'
2122

2223
vi.mock('../../../models/app/loader.js')
2324
vi.mock('./app-watcher-esbuild.js')
2425

2526
// Extensions 1 and 1B simulate extensions defined in the same directory (same toml)
26-
const extension1 = await testUIExtension({type: 'ui_extension', handle: 'h1', directory: '/extensions/ui_extension_1'})
27-
const extension1B = await testUIExtension({type: 'ui_extension', handle: 'h2', directory: '/extensions/ui_extension_1'})
28-
const extension2 = await testUIExtension({type: 'ui_extension', directory: '/extensions/ui_extension_2'})
27+
const extension1 = await testUIExtension({type: 'ui_extension', directory: '/extensions/ui_extension_1', uid: 'uid1'})
28+
const extension1B = await testUIExtension({type: 'ui_extension', directory: '/extensions/ui_extension_1', uid: 'uid1B'})
29+
const extension2 = await testUIExtension({type: 'ui_extension', directory: '/extensions/ui_extension_2', uid: 'uid2'})
2930
const flowExtension = await testFlowActionExtension('/extensions/flow_action')
3031
const posExtension = await testAppConfigExtensions()
3132
const appAccessExtension = await testAppAccessConfigExtension()
@@ -35,14 +36,14 @@ const webhookExtension = await testSingleWebhookSubscriptionExtension()
3536
const extension1Updated = await testUIExtension({
3637
type: 'ui_extension',
3738
name: 'updated_name1',
38-
handle: 'h1',
3939
directory: '/extensions/ui_extension_1',
40+
uid: 'uid1',
4041
})
4142
const extension1BUpdated = await testUIExtension({
4243
type: 'ui_extension',
4344
name: 'updated_name1B',
44-
handle: 'h2',
4545
directory: '/extensions/ui_extension_1',
46+
uid: 'uid1B',
4647
})
4748
const posExtensionUpdated = await testAppConfigExtensions(true)
4849

@@ -105,9 +106,7 @@ const testCases: TestCase[] = [
105106
},
106107
initialExtensions: [extension1, posExtension],
107108
finalExtensions: [extension1, extension2, posExtension],
108-
extensionEvents: [
109-
{type: EventType.Created, extension: extension2, buildResult: {status: 'ok', handle: 'test-ui-extension'}},
110-
],
109+
extensionEvents: [{type: EventType.Created, extension: extension2, buildResult: {status: 'ok', uid: 'uid2'}}],
111110
needsAppReload: true,
112111
},
113112
{
@@ -120,7 +119,7 @@ const testCases: TestCase[] = [
120119
},
121120
initialExtensions: [extension1, extension2, posExtension],
122121
finalExtensions: [extension1, extension2, posExtension],
123-
extensionEvents: [{type: EventType.Updated, extension: extension1, buildResult: {status: 'ok', handle: 'h1'}}],
122+
extensionEvents: [{type: EventType.Updated, extension: extension1, buildResult: {status: 'ok', uid: 'uid1'}}],
124123
},
125124
{
126125
name: 'file_updated affecting a single extension',
@@ -132,7 +131,7 @@ const testCases: TestCase[] = [
132131
},
133132
initialExtensions: [extension1, extension2, posExtension],
134133
finalExtensions: [extension1, extension2, posExtension],
135-
extensionEvents: [{type: EventType.Updated, extension: extension1, buildResult: {status: 'ok', handle: 'h1'}}],
134+
extensionEvents: [{type: EventType.Updated, extension: extension1, buildResult: {status: 'ok', uid: 'uid1'}}],
136135
},
137136
{
138137
name: 'file_deleted affecting a single extension',
@@ -144,7 +143,7 @@ const testCases: TestCase[] = [
144143
},
145144
initialExtensions: [extension1, extension2, posExtension],
146145
finalExtensions: [extension1, extension2, posExtension],
147-
extensionEvents: [{type: EventType.Updated, extension: extension1, buildResult: {status: 'ok', handle: 'h1'}}],
146+
extensionEvents: [{type: EventType.Updated, extension: extension1, buildResult: {status: 'ok', uid: 'uid1'}}],
148147
},
149148
{
150149
name: 'file_created affecting a multiple extensions',
@@ -157,8 +156,8 @@ const testCases: TestCase[] = [
157156
initialExtensions: [extension1, extension1B, extension2, posExtension],
158157
finalExtensions: [extension1, extension1B, extension2, posExtension],
159158
extensionEvents: [
160-
{type: EventType.Updated, extension: extension1, buildResult: {status: 'ok', handle: 'h1'}},
161-
{type: EventType.Updated, extension: extension1B, buildResult: {status: 'ok', handle: 'h2'}},
159+
{type: EventType.Updated, extension: extension1, buildResult: {status: 'ok', uid: 'uid1'}},
160+
{type: EventType.Updated, extension: extension1B, buildResult: {status: 'ok', uid: 'uid1B'}},
162161
],
163162
},
164163
{
@@ -172,8 +171,8 @@ const testCases: TestCase[] = [
172171
initialExtensions: [extension1, extension1B, extension2, posExtension],
173172
finalExtensions: [extension1, extension1B, extension2, posExtension],
174173
extensionEvents: [
175-
{type: EventType.Updated, extension: extension1, buildResult: {status: 'ok', handle: 'h1'}},
176-
{type: EventType.Updated, extension: extension1B, buildResult: {status: 'ok', handle: 'h2'}},
174+
{type: EventType.Updated, extension: extension1, buildResult: {status: 'ok', uid: 'uid1'}},
175+
{type: EventType.Updated, extension: extension1B, buildResult: {status: 'ok', uid: 'uid1B'}},
177176
],
178177
},
179178
{
@@ -187,8 +186,8 @@ const testCases: TestCase[] = [
187186
initialExtensions: [extension1, extension1B, extension2, posExtension],
188187
finalExtensions: [extension1, extension1B, extension2, posExtension],
189188
extensionEvents: [
190-
{type: EventType.Updated, extension: extension1, buildResult: {status: 'ok', handle: 'h1'}},
191-
{type: EventType.Updated, extension: extension1B, buildResult: {status: 'ok', handle: 'h2'}},
189+
{type: EventType.Updated, extension: extension1, buildResult: {status: 'ok', uid: 'uid1'}},
190+
{type: EventType.Updated, extension: extension1B, buildResult: {status: 'ok', uid: 'uid1B'}},
192191
],
193192
},
194193
{
@@ -202,9 +201,17 @@ const testCases: TestCase[] = [
202201
initialExtensions: [extension1, extension2, posExtension, webhookExtension],
203202
finalExtensions: [extension1, extension2, posExtensionUpdated, appAccessExtension],
204203
extensionEvents: [
205-
{type: EventType.Updated, extension: posExtensionUpdated, buildResult: {status: 'ok', handle: 'point-of-sale'}},
204+
{
205+
type: EventType.Updated,
206+
extension: posExtensionUpdated,
207+
buildResult: {status: 'ok', uid: nonRandomUUID('point-of-sale')},
208+
},
206209
{type: EventType.Deleted, extension: webhookExtension},
207-
{type: EventType.Created, extension: appAccessExtension, buildResult: {status: 'ok', handle: 'app-access'}},
210+
{
211+
type: EventType.Created,
212+
extension: appAccessExtension,
213+
buildResult: {status: 'ok', uid: nonRandomUUID('app-access')},
214+
},
208215
],
209216
needsAppReload: true,
210217
},
@@ -219,8 +226,8 @@ const testCases: TestCase[] = [
219226
initialExtensions: [extension1, extension1B, extension2],
220227
finalExtensions: [extension1Updated, extension1BUpdated, extension2],
221228
extensionEvents: [
222-
{type: EventType.Updated, extension: extension1Updated, buildResult: {status: 'ok', handle: 'h1'}},
223-
{type: EventType.Updated, extension: extension1BUpdated, buildResult: {status: 'ok', handle: 'h2'}},
229+
{type: EventType.Updated, extension: extension1Updated, buildResult: {status: 'ok', uid: 'uid1'}},
230+
{type: EventType.Updated, extension: extension1BUpdated, buildResult: {status: 'ok', uid: 'uid1B'}},
224231
],
225232
needsAppReload: true,
226233
},
@@ -295,7 +302,7 @@ describe('app-event-watcher', () => {
295302
const initialEvents = app.realExtensions.map((eve) => ({
296303
type: EventType.Updated,
297304
extension: eve,
298-
buildResult: {status: 'ok', handle: eve.handle},
305+
buildResult: {status: 'ok', uid: eve.uid},
299306
}))
300307
expect(emitSpy).toHaveBeenCalledWith('ready', {
301308
app,
@@ -406,8 +413,9 @@ describe('app-event-watcher', () => {
406413
class MockESBuildContextManager extends ESBuildContextManager {
407414
contexts = {
408415
// The keys are the extension handles, the values are the ESBuild contexts mocked
409-
h1: [{rebuild: vi.fn(), watch: vi.fn(), serve: vi.fn(), cancel: vi.fn(), dispose: vi.fn()}],
410-
h2: [{rebuild: vi.fn(), watch: vi.fn(), serve: vi.fn(), cancel: vi.fn(), dispose: vi.fn()}],
416+
uid1: [{rebuild: vi.fn(), watch: vi.fn(), serve: vi.fn(), cancel: vi.fn(), dispose: vi.fn()}],
417+
uid1B: [{rebuild: vi.fn(), watch: vi.fn(), serve: vi.fn(), cancel: vi.fn(), dispose: vi.fn()}],
418+
uid2: [{rebuild: vi.fn(), watch: vi.fn(), serve: vi.fn(), cancel: vi.fn(), dispose: vi.fn()}],
411419
'test-ui-extension': [{rebuild: vi.fn(), watch: vi.fn(), serve: vi.fn(), cancel: vi.fn(), dispose: vi.fn()}],
412420
}
413421

packages/app/src/cli/services/dev/app-events/app-event-watcher.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ export interface AppEvent {
8383
appWasReloaded?: boolean
8484
}
8585

86-
type ExtensionBuildResult = {status: 'ok'; handle: string} | {status: 'error'; error: string; handle: string}
86+
type ExtensionBuildResult = {status: 'ok'; uid: string} | {status: 'error'; error: string; uid: string}
8787

8888
/**
8989
* App event watcher will emit events when changes are detected in the file system.
@@ -230,12 +230,12 @@ export class AppEventWatcher extends EventEmitter {
230230
const ext = extEvent.extension
231231
return useConcurrentOutputContext({outputPrefix: ext.handle, stripAnsi: false}, async () => {
232232
try {
233-
if (this.esbuildManager.contexts?.[ext.handle]?.length) {
233+
if (this.esbuildManager.contexts?.[ext.uid]?.length) {
234234
await this.esbuildManager.rebuildContext(ext)
235235
} else {
236236
await this.buildExtension(ext)
237237
}
238-
extEvent.buildResult = {status: 'ok', handle: ext.handle}
238+
extEvent.buildResult = {status: 'ok', uid: ext.uid}
239239
// eslint-disable-next-line no-catch-all/no-catch-all, @typescript-eslint/no-explicit-any
240240
} catch (error: any) {
241241
// If there is an `errors` array, it's an esbuild error, format it and log it
@@ -249,7 +249,7 @@ export class AppEventWatcher extends EventEmitter {
249249
} else {
250250
this.options.stderr.write(error.message)
251251
}
252-
extEvent.buildResult = {status: 'error', error: error.message, handle: ext.handle}
252+
extEvent.buildResult = {status: 'error', error: error.message, uid: ext.uid}
253253
}
254254
})
255255
})

packages/app/src/cli/services/dev/app-events/app-watcher-esbuild.test.ts

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ vi.mock('@luckycatfactory/esbuild-graphql-loader', () => ({
1212
},
1313
}))
1414

15-
const extension1 = await testUIExtension({type: 'ui_extension', handle: 'h1', directory: '/extensions/ui_extension_1'})
16-
const extension2 = await testUIExtension({type: 'ui_extension', directory: '/extensions/ui_extension_2'})
15+
const extension1 = await testUIExtension({type: 'ui_extension', directory: '/extensions/ui_extension_1', uid: 'uid1'})
16+
const extension2 = await testUIExtension({type: 'ui_extension', directory: '/extensions/ui_extension_2', uid: 'uid2'})
1717

1818
describe('app-watcher-esbuild', () => {
1919
const options: DevAppWatcherOptions = {
@@ -31,8 +31,8 @@ describe('app-watcher-esbuild', () => {
3131
await manager.createContexts(extensions)
3232

3333
// Then
34-
expect(manager.contexts).toHaveProperty('h1')
35-
expect(manager.contexts).toHaveProperty('test-ui-extension')
34+
expect(manager.contexts).toHaveProperty('uid1')
35+
expect(manager.contexts).toHaveProperty('uid2')
3636
})
3737

3838
test('creating multiple contexts for the same extension', async () => {
@@ -44,6 +44,7 @@ describe('app-watcher-esbuild', () => {
4444
}
4545
const manager = new ESBuildContextManager(options)
4646
const extension = await testUIExtension({
47+
uid: 'conditional-extension-uid',
4748
configuration: {
4849
...extension2.configuration,
4950
handle: 'conditional-extension',
@@ -75,8 +76,8 @@ describe('app-watcher-esbuild', () => {
7576
await manager.createContexts([extension])
7677

7778
// Then
78-
expect(manager.contexts).toHaveProperty('conditional-extension')
79-
expect(manager.contexts['conditional-extension']).toHaveLength(2)
79+
expect(manager.contexts).toHaveProperty('conditional-extension-uid')
80+
expect(manager.contexts['conditional-extension-uid']).toHaveLength(2)
8081
})
8182

8283
test('deleting contexts', async () => {
@@ -89,8 +90,8 @@ describe('app-watcher-esbuild', () => {
8990
await manager.deleteContexts([extension1])
9091

9192
// Then
92-
expect(manager.contexts).not.toHaveProperty('h1')
93-
expect(manager.contexts).toHaveProperty('test-ui-extension')
93+
expect(manager.contexts).not.toHaveProperty('uid1')
94+
expect(manager.contexts).toHaveProperty('uid2')
9495
})
9596

9697
test('updating contexts with an app event', async () => {
@@ -112,22 +113,22 @@ describe('app-watcher-esbuild', () => {
112113
await manager.updateContexts(appEvent)
113114

114115
// Then
115-
expect(manager.contexts).toHaveProperty('h1')
116-
expect(manager.contexts).not.toHaveProperty('test-ui-extension')
116+
expect(manager.contexts).toHaveProperty('uid1')
117+
expect(manager.contexts).not.toHaveProperty('uid2')
117118
})
118119

119120
test('rebuilding contexts', async () => {
120121
// Given
121122
const manager = new ESBuildContextManager(options)
122123
await manager.createContexts([extension1])
123-
const spyContext = vi.spyOn(manager.contexts.h1![0]!, 'rebuild').mockResolvedValue({} as any)
124+
const spyContext = vi.spyOn(manager.contexts.uid1![0]!, 'rebuild').mockResolvedValue({} as any)
124125
const spyCopy = vi.spyOn(fs, 'copyFile').mockResolvedValue()
125126

126127
// When
127128
await manager.rebuildContext(extension1)
128129

129130
// Then
130131
expect(spyContext).toHaveBeenCalled()
131-
expect(spyCopy).toHaveBeenCalledWith('/path/to/output/h1/dist', '/extensions/ui_extension_1/dist')
132+
expect(spyCopy).toHaveBeenCalledWith('/path/to/output/uid1/dist', '/extensions/ui_extension_1/dist')
132133
})
133134
})

0 commit comments

Comments
 (0)