Skip to content

Commit 39a5dc0

Browse files
vctrchuclaude
andcommitted
refactor: drive BackgroundShopifyGlobal emit off DataExtensionTargets
Replace the hardcoded `parts.join('.') === 'pos.app.ready.data'` check with an `isDataTarget` flag derived from the interface the target was declared in. Any target added to `DataExtensionTargets` now gets the wider `BackgroundShopifyGlobal` automatically — no buildTargetDts edit required. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ca6572a commit 39a5dc0

4 files changed

Lines changed: 98 additions & 24 deletions

File tree

packages/ui-extensions-tester/src/index.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import * as fs from 'fs';
22
import * as path from 'path';
33

4-
import type {AnyExtensionTarget, ApiForTarget} from './targets';
4+
import type {
5+
AnyExtensionTarget,
6+
ApiForTarget,
7+
EventMapForTarget,
8+
} from './targets';
59
import {isCheckoutTarget} from './targets';
610
import {createMockTargetApi} from './mocks/target-apis';
711
import {createMockNavigation, type Navigation} from './navigation';
@@ -99,12 +103,26 @@ interface BaseExtensionHarness<T extends AnyExtensionTarget> {
99103
* are ignored, and thrown errors are caught per-listener so one bad
100104
* listener doesn't block the others.
101105
*
106+
* The `event` argument must be a real `Event` instance, matching what
107+
* the host dispatches at runtime. Construct it with `new Event(type)`
108+
* and attach payload fields via `Object.assign`:
109+
*
102110
* ```ts
103111
* shopify.addEventListener('transactioncomplete', (event) => { ... });
104-
* extension.dispatch('transactioncomplete', { transaction: {...} });
112+
*
113+
* const event = Object.assign(new Event('transactioncomplete'), {
114+
* transactionType: 'Sale',
115+
* orderId: 1,
116+
* grandTotal: { amount: 10, currency: 'USD' },
117+
* // ...
118+
* });
119+
* extension.dispatch('transactioncomplete', event);
105120
* ```
106121
*/
107-
dispatch(type: string, event?: unknown): void;
122+
dispatch<K extends keyof EventMapForTarget<T>>(
123+
type: K,
124+
event: EventMapForTarget<T>[K],
125+
): void;
108126
}
109127

110128
/**
@@ -217,8 +235,11 @@ class Extension<T extends AnyExtensionTarget> implements ExtensionHarness<T> {
217235
this.#eventListeners.get(type)?.delete(listener);
218236
};
219237

220-
dispatch(type: string, event?: unknown): void {
221-
const listeners = this.#eventListeners.get(type);
238+
dispatch<K extends keyof EventMapForTarget<T>>(
239+
type: K,
240+
event: EventMapForTarget<T>[K],
241+
): void {
242+
const listeners = this.#eventListeners.get(type as string);
222243
if (!listeners) return;
223244
// Snapshot so listeners that register/unregister during dispatch
224245
// don't mutate the iteration.

packages/ui-extensions-tester/src/targets.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type {
99
import type {
1010
ExtensionTarget as PosExtensionTarget,
1111
ExtensionTargets as PointOfSaleExtensionTargets,
12+
ShopifyEventMap as PosEventMap,
1213
} from '@shopify/ui-extensions/point-of-sale';
1314
import type {
1415
CustomerAccountExtensionTarget,
@@ -42,6 +43,16 @@ export type ApiForTarget<T extends AnyExtensionTarget> =
4243
? D
4344
: never;
4445

46+
/**
47+
* Maps an extension target to the event map available via
48+
* `shopify.addEventListener` on that surface.
49+
*
50+
* - POS targets: the POS `ShopifyEventMap`.
51+
* - Other surfaces: no host events, so the map is empty.
52+
*/
53+
export type EventMapForTarget<T extends AnyExtensionTarget> =
54+
T extends PosExtensionTarget ? PosEventMap : {};
55+
4556
export function isCheckoutTarget(
4657
target: string,
4758
): target is CheckoutExtensionTarget {

packages/ui-extensions-tester/src/tests/shopify-events.test.ts

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,49 @@
1+
import type {
2+
CashTrackingSessionStartEvent,
3+
TransactionCompleteEvent,
4+
} from '@shopify/ui-extensions/point-of-sale';
5+
16
import {getExtension} from '../index';
27

38
import {createTestSandbox, type TestSandbox} from './helpers';
49

10+
function makeTransactionCompleteEvent(): TransactionCompleteEvent {
11+
return Object.assign(new Event('transactioncomplete'), {
12+
transactionType: 'Sale' as const,
13+
discounts: [],
14+
taxTotal: {amount: 0, currency: 'USD'},
15+
subtotal: {amount: 0, currency: 'USD'},
16+
grandTotal: {amount: 0, currency: 'USD'},
17+
paymentMethods: [],
18+
balanceDue: {amount: 0, currency: 'USD'},
19+
shippingLines: [],
20+
taxLines: [],
21+
executedAt: '2024-01-01T00:00:00Z',
22+
lineItems: [],
23+
});
24+
}
25+
26+
function makeCashTrackingSessionStartEvent(): CashTrackingSessionStartEvent {
27+
return Object.assign(new Event('cashtrackingsessionstart'), {
28+
id: 1,
29+
openingTime: '2024-01-01T00:00:00Z',
30+
});
31+
}
32+
533
describe('shopify.addEventListener / extension.dispatch', () => {
634
let sandbox: TestSandbox;
735

836
beforeEach(() => {
937
sandbox = createTestSandbox();
10-
sandbox.placeToml();
38+
sandbox.placeToml({target: 'pos.app.ready.data'});
1139
});
1240

1341
afterEach(() => {
1442
sandbox.destroy();
1543
});
1644

1745
function setUpExt() {
18-
const extension = getExtension('purchase.checkout.block.render', {
46+
const extension = getExtension('pos.app.ready.data', {
1947
configSearchDir: sandbox.tempDir,
2048
});
2149
extension.setUp();
@@ -35,11 +63,11 @@ describe('shopify.addEventListener / extension.dispatch', () => {
3563
const listener = jest.fn();
3664
shopify.addEventListener('transactioncomplete', listener);
3765

38-
const eventData = {transaction: {id: 1}};
39-
extension.dispatch('transactioncomplete', eventData);
66+
const event = makeTransactionCompleteEvent();
67+
extension.dispatch('transactioncomplete', event);
4068

4169
expect(listener).toHaveBeenCalledTimes(1);
42-
expect(listener).toHaveBeenCalledWith(eventData);
70+
expect(listener).toHaveBeenCalledWith(event);
4371
});
4472

4573
it('fires all listeners registered for the same event', () => {
@@ -50,10 +78,11 @@ describe('shopify.addEventListener / extension.dispatch', () => {
5078
shopify.addEventListener('transactioncomplete', listenerA);
5179
shopify.addEventListener('transactioncomplete', listenerB);
5280

53-
extension.dispatch('transactioncomplete', 42);
81+
const event = makeTransactionCompleteEvent();
82+
extension.dispatch('transactioncomplete', event);
5483

55-
expect(listenerA).toHaveBeenCalledWith(42);
56-
expect(listenerB).toHaveBeenCalledWith(42);
84+
expect(listenerA).toHaveBeenCalledWith(event);
85+
expect(listenerB).toHaveBeenCalledWith(event);
5786
});
5887

5988
it('does not fire other events when dispatching one', () => {
@@ -64,7 +93,7 @@ describe('shopify.addEventListener / extension.dispatch', () => {
6493
shopify.addEventListener('transactioncomplete', target);
6594
shopify.addEventListener('cashtrackingsessionstart', other);
6695

67-
extension.dispatch('transactioncomplete', undefined);
96+
extension.dispatch('transactioncomplete', makeTransactionCompleteEvent());
6897

6998
expect(target).toHaveBeenCalledTimes(1);
7099
expect(other).not.toHaveBeenCalled();
@@ -77,7 +106,7 @@ describe('shopify.addEventListener / extension.dispatch', () => {
77106
shopify.addEventListener('transactioncomplete', listener);
78107
shopify.removeEventListener('transactioncomplete', listener);
79108

80-
extension.dispatch('transactioncomplete', undefined);
109+
extension.dispatch('transactioncomplete', makeTransactionCompleteEvent());
81110

82111
expect(listener).not.toHaveBeenCalled();
83112
});
@@ -89,7 +118,7 @@ describe('shopify.addEventListener / extension.dispatch', () => {
89118
shopify.addEventListener('transactioncomplete', listener);
90119
shopify.addEventListener('transactioncomplete', listener);
91120

92-
extension.dispatch('transactioncomplete', undefined);
121+
extension.dispatch('transactioncomplete', makeTransactionCompleteEvent());
93122

94123
expect(listener).toHaveBeenCalledTimes(1);
95124
});
@@ -105,7 +134,7 @@ describe('shopify.addEventListener / extension.dispatch', () => {
105134
shopify.addEventListener('transactioncomplete', follower);
106135

107136
expect(() =>
108-
extension.dispatch('transactioncomplete', undefined),
137+
extension.dispatch('transactioncomplete', makeTransactionCompleteEvent()),
109138
).not.toThrow();
110139
expect(throwing).toHaveBeenCalledTimes(1);
111140
expect(follower).toHaveBeenCalledTimes(1);
@@ -114,7 +143,10 @@ describe('shopify.addEventListener / extension.dispatch', () => {
114143
it('is a no-op when dispatching an event with no registered listeners', () => {
115144
const extension = setUpExt();
116145
expect(() =>
117-
extension.dispatch('cashtrackingsessionstart', {}),
146+
extension.dispatch(
147+
'cashtrackingsessionstart',
148+
makeCashTrackingSessionStartEvent(),
149+
),
118150
).not.toThrow();
119151
});
120152

@@ -126,7 +158,7 @@ describe('shopify.addEventListener / extension.dispatch', () => {
126158
extension.tearDown();
127159

128160
extension.setUp();
129-
extension.dispatch('transactioncomplete', undefined);
161+
extension.dispatch('transactioncomplete', makeTransactionCompleteEvent());
130162

131163
expect(leaked).not.toHaveBeenCalled();
132164
});

packages/ui-extensions/buildTargetDts.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,12 @@ function createInitialTargetDefinition({
5858
buildPath,
5959
name,
6060
surface,
61+
isDataTarget,
6162
}: {
6263
buildPath: string;
6364
name: string;
6465
surface: string;
66+
isDataTarget: boolean;
6567
}) {
6668
const parts = name.replaceAll("'", '').split('.');
6769
const fileName = `${parts.join('.')}.d.ts`;
@@ -76,7 +78,7 @@ function createInitialTargetDefinition({
7678
}${
7779
surface === 'point-of-sale'
7880
? `export * from '../events';\n${
79-
parts.join('.') === 'pos.app.ready.data'
81+
isDataTarget
8082
? `export type {BackgroundShopifyGlobal as ShopifyGlobal} from '../globals';\n`
8183
: ''
8284
}`
@@ -141,10 +143,12 @@ function getTargets(sourceFile: SourceFile) {
141143
// Target definitions
142144
function extractTargetComponents(
143145
sourceFile: SourceFile,
144-
): {name: string; components?: string[]}[] {
146+
): {name: string; components?: string[]; isDataTarget: boolean}[] {
145147
const extensionTargetArray = getTargets(sourceFile);
146148
return extensionTargetArray
147149
.map((extensionTargets) => {
150+
const isDataTarget =
151+
extensionTargets.getName() === 'DataExtensionTargets';
148152
return extensionTargets.getProperties().map((property) => {
149153
const componentsType = property
150154
.getType()
@@ -166,6 +170,7 @@ function extractTargetComponents(
166170
return {
167171
name: property.getName(),
168172
components,
173+
isDataTarget,
169174
};
170175
});
171176
})
@@ -177,15 +182,20 @@ function createTargetDefinition({
177182
buildPath,
178183
project,
179184
surface,
180-
target: {name, components},
185+
target: {name, components, isDataTarget},
181186
}: {
182187
srcPaths: string[];
183188
buildPath: string;
184189
project: Project;
185190
surface: string;
186-
target: {name: string; components?: string[]};
191+
target: {name: string; components?: string[]; isDataTarget: boolean};
187192
}) {
188-
const targetPath = createInitialTargetDefinition({name, surface, buildPath});
193+
const targetPath = createInitialTargetDefinition({
194+
name,
195+
surface,
196+
buildPath,
197+
isDataTarget,
198+
});
189199
const targetFile = project.addSourceFileAtPath(targetPath);
190200
const names = new Set<string>();
191201

0 commit comments

Comments
 (0)