Skip to content

Detect ShopifyGlobal re-export, emit intersection in shopify.d.ts#7387

Merged
vctrchu merged 2 commits intomainfrom
vchu/shopify-global-type-detection
Apr 24, 2026
Merged

Detect ShopifyGlobal re-export, emit intersection in shopify.d.ts#7387
vctrchu merged 2 commits intomainfrom
vchu/shopify-global-type-detection

Conversation

@vctrchu
Copy link
Copy Markdown
Contributor

@vctrchu vctrchu commented Apr 23, 2026

Closes https://github.com/shop/issues-retail/issues/28334

Companion ui-extension PR

Summary

When a @shopify/ui-extensions target .d.ts re-exports a ShopifyGlobal type, the CLI-generated shopify.d.ts now types the shopify binding as Api & ShopifyGlobal instead of just Api. Targets that do not re-export ShopifyGlobal are unchanged.

This unblocks POS background extensions, where the runtime shopify object exposes both the target's data surface (shopify.cart, shopify.products, etc.) and host-level event APIs (shopify.addEventListener). Existing consumers are unaffected — the .Api half of the intersection preserves their current typing.

Screen Recording 2026-04-23 at 3.47.17 PM.mov (uploaded via Graphite)

Detection

  • AST-based, via the typescript compiler API (already a dependency of this file).
  • Scans for a named export specifier whose public name is ShopifyGlobal (matches export type {ShopifyGlobal} from '../globals', export {ShopifyGlobal}, and export {Foo as ShopifyGlobal}).
  • No surface name is hard-coded anywhere in the CLI.

Scope — who's affected

Surface Defines ShopifyGlobal in globals.ts today? Re-exports from target .d.ts after companion PR? Output change from this CLI PR?
admin no no none
checkout yes no none
customer-account yes no none
point-of-sale yes yes (opt-in on certain target) ApiApi & ShopifyGlobal

Other surfaces: nothing breaks; no action required. The regression test added in this PR asserts that targets without a ShopifyGlobal re-export emit the exact same output as before.

Why POS is the first adopter: POS background extensions are the first pattern that writes code accessing both the target Api (via e.g. shopify.cart.current) and host-level APIs (via shopify.addEventListener) through the same shopify identifier in an entry file that also imports from @shopify/ui-extensions/<target>. That combination exposes a module-shadowing issue in the CLI-generated shopify.d.ts that other surfaces haven't hit yet. Any surface that later needs the same pattern can opt in with a one-line change to their branch of buildTargetDts.ts in ui-extensions — no CLI release required.

Backwards compatibility

  • Targets without a ShopifyGlobal re-export: output byte-identical to main.
  • Older @shopify/ui-extensions versions (no ShopifyGlobal export on any target): graceful fallback to plain .Api for every target.
  • Existing consumers with in-flight shopify.<target-api-field> usage: unchanged — intersection, not replacement

How to test

Before / after in generated shopify.d.ts — this is the only behavior change:

 declare module './src/BackgroundExtension.ts' {
-  const shopify: import('@shopify/ui-extensions/pos.app.ready.data').Api;
+  const shopify: import('@shopify/ui-extensions/pos.app.ready.data').Api &
+    import('@shopify/ui-extensions/pos.app.ready.data').ShopifyGlobal;
   const globalThis: { shopify: typeof shopify };
 }

For a live regeneration: link a POS extension to the local ui-extensions companion branch (pnpm overrides.@shopify/ui-extensions: link:...), run pnpm shopify app build --path <app> from inside the CLI repo, and inspect shopify.d.ts. Full CLI setup per vault BKMy.

Test plan

  • Unit test — intersection emitted when target re-exports ShopifyGlobal.
  • Unit test — plain Api emitted when target does not re-export ShopifyGlobal (regression guard).
  • Full ui_extension.test.ts suite passes (45/45).
  • Typecheck + lint clean.
  • End-to-end validated against a POS background extension — shopify.addEventListener and shopify.cart.current.value both type-check against the same generated shopify.d.ts.

Rollback

Revert this PR. No migration required — targets without the companion ui-extensions change are byte-identical to main, and targets with it fall back to the pre-PR output shape on the prior CLI version.t use a certa

…Global

The CLI-generated shopify.d.ts now types the `shopify` binding as
`Api & ShopifyGlobal` for UI extension targets whose .d.ts re-exports
a type named `ShopifyGlobal`. Detection is AST-based via the typescript
compiler API (already a dependency), matching on the public export name
`ShopifyGlobal` so the CLI does not need to know about specific surfaces
or targets.

Targets that do not re-export `ShopifyGlobal` emit byte-identical output
to main. Existing consumers who access the target API via `shopify.*`
are unaffected. Net effect: host-level APIs like `shopify.addEventListener`
now type-check automatically for opt-in targets (e.g. POS background
extensions) without any CLI release coordination when new targets opt in.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vctrchu vctrchu self-assigned this Apr 23, 2026
@vctrchu vctrchu marked this pull request as ready for review April 23, 2026 23:19
@vctrchu vctrchu requested review from a team as code owners April 23, 2026 23:19
Copilot AI review requested due to automatic review settings April 23, 2026 23:19
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates CLI-generated UI extension type declarations so the shopify binding can include host-level APIs when a target opts in by re-exporting ShopifyGlobal. This aligns the generated shopify.d.ts with POS background extension runtime behavior while keeping existing targets byte-identical.

Changes:

  • Detects ShopifyGlobal re-exports in a resolved target file using the TypeScript compiler API.
  • Emits Api & ShopifyGlobal for opt-in targets; keeps Api only for all others.
  • Adds unit tests covering both the opt-in intersection case and the regression “no export → unchanged output” case.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
packages/app/src/cli/models/extensions/specifications/type-generation.ts Adds ShopifyGlobal re-export detection and conditionally emits an intersection type for shopify.
packages/app/src/cli/models/extensions/specifications/ui_extension.test.ts Extends the node_modules mock helper and adds tests for both intersection and non-intersection outputs.
.changeset/shopify-global-type-detection.md Documents the patch change in generated shopify.d.ts behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@vctrchu vctrchu requested a review from isaacroldan April 24, 2026 00:03
Adds a third test case that uses a fabricated target name belonging to
no real surface. Documents in the test body that the detector has no
allowlist and no hard-coded target — any surface can opt in by shipping
the same export shape from its target `.d.ts`.

Softens the POS-specific phrasing in the first test's comment; POS
is the first adopter, not a special case.
@vctrchu vctrchu changed the title Emit Api & ShopifyGlobal intersection in shopify.d.ts when target re-exports ShopifyGlobal Detect ShopifyGlobal re-export, emit intersection in shopify.d.ts Apr 24, 2026
@vctrchu vctrchu added this pull request to the merge queue Apr 24, 2026
Merged via the queue into main with commit 3598271 Apr 24, 2026
27 checks passed
@vctrchu vctrchu deleted the vchu/shopify-global-type-detection branch April 24, 2026 18:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants