@@ -1177,12 +1177,14 @@ Please check the configuration in ${uiExtension.configurationPath}`),
11771177 shouldRenderFileContent,
11781178 apiVersion,
11791179 target = 'admin.product-details.action.render' ,
1180+ targetDtsContent,
11801181 } : {
11811182 tmpDir : string
11821183 fileContent : string
11831184 shouldRenderFileContent ?: string
11841185 apiVersion : string
11851186 target ?: string
1187+ targetDtsContent ?: string
11861188 } ) {
11871189 // Create extension files
11881190 const srcDir = joinPath ( tmpDir , 'src' )
@@ -1197,7 +1199,11 @@ Please check the configuration in ${uiExtension.configurationPath}`),
11971199
11981200 const targetPath = joinPath ( nodeModulesPath , target )
11991201 await mkdir ( targetPath )
1200- await writeFile ( joinPath ( targetPath , 'index.js' ) , '// Mock UI extension target' )
1202+ // `require.resolve('@shopify/ui-extensions/<target>')` resolves to this file,
1203+ // and the CLI's ShopifyGlobal detector reads whatever path require.resolve
1204+ // returned. Injecting `targetDtsContent` here lets tests exercise the
1205+ // detection branch; defaults preserve the original placeholder.
1206+ await writeFile ( joinPath ( targetPath , 'index.js' ) , targetDtsContent ?? '// Mock UI extension target' )
12011207
12021208 let shouldRenderFilePath
12031209 if ( shouldRenderFileContent ) {
@@ -1367,6 +1373,134 @@ Please check the configuration in ${uiExtension.configurationPath}`),
13671373 } )
13681374 } )
13691375
1376+ test ( 'emits Api & ShopifyGlobal intersection when target re-exports ShopifyGlobal' , async ( ) => {
1377+ const typeDefinitionsByFile = new Map < string , Set < string > > ( )
1378+
1379+ await inTemporaryDirectory ( async ( tmpDir ) => {
1380+ const { extension} = await setupUIExtensionWithNodeModules ( {
1381+ tmpDir,
1382+ fileContent : '// JSX code' ,
1383+ // Remote DOM supported version
1384+ apiVersion : '2025-10' ,
1385+ // The target re-exports `ShopifyGlobal` via a named export specifier,
1386+ // which is the shape the AST helper detects. Any surface can opt in
1387+ // by emitting this shape from its target `.d.ts`.
1388+ targetDtsContent : `
1389+ interface _ShopifyGlobalInternal { addEventListener(type: string, listener: (event: unknown) => void): void }
1390+ export type {_ShopifyGlobalInternal as ShopifyGlobal}
1391+ export type Api = {placeholder: true}
1392+ ` ,
1393+ } )
1394+
1395+ // Create tsconfig.json
1396+ const tsconfigPath = joinPath ( tmpDir , 'tsconfig.json' )
1397+ await writeFile ( tsconfigPath , '// TypeScript config' )
1398+
1399+ // When
1400+ await extension . contributeToSharedTypeFile ?.( typeDefinitionsByFile )
1401+
1402+ const shopifyDtsPath = joinPath ( tmpDir , 'shopify.d.ts' )
1403+
1404+ // Then — prettier wraps the long intersection onto two lines.
1405+ expect ( typeDefinitionsByFile ) . toStrictEqual (
1406+ new Map ( [
1407+ [
1408+ shopifyDtsPath ,
1409+ new Set ( [
1410+ `//@ts-ignore\ndeclare module './src/index.jsx' {
1411+ const shopify: import('@shopify/ui-extensions/admin.product-details.action.render').Api &
1412+ import('@shopify/ui-extensions/admin.product-details.action.render').ShopifyGlobal;
1413+ const globalThis: { shopify: typeof shopify };
1414+ }\n` ,
1415+ ] ) ,
1416+ ] ,
1417+ ] ) ,
1418+ )
1419+ } )
1420+ } )
1421+
1422+ test ( 'ShopifyGlobal detection is target-agnostic — any target with the re-export opts in' , async ( ) => {
1423+ const typeDefinitionsByFile = new Map < string , Set < string > > ( )
1424+
1425+ await inTemporaryDirectory ( async ( tmpDir ) => {
1426+ // A fabricated target name belonging to no real surface. The detector
1427+ // is purely name-based on the public `ShopifyGlobal` export, so any
1428+ // surface's target can opt in by shipping this shape — there is no
1429+ // allowlist or hard-coded target in the CLI.
1430+ const genericTarget = 'fake-surface.any-target.render'
1431+
1432+ const { extension} = await setupUIExtensionWithNodeModules ( {
1433+ tmpDir,
1434+ fileContent : '// JSX code' ,
1435+ apiVersion : '2025-10' ,
1436+ target : genericTarget ,
1437+ targetDtsContent : `
1438+ interface _FakeShopifyGlobal { someHostApi(): void }
1439+ export type {_FakeShopifyGlobal as ShopifyGlobal}
1440+ export type Api = {placeholder: true}
1441+ ` ,
1442+ } )
1443+
1444+ const tsconfigPath = joinPath ( tmpDir , 'tsconfig.json' )
1445+ await writeFile ( tsconfigPath , '// TypeScript config' )
1446+
1447+ await extension . contributeToSharedTypeFile ?.( typeDefinitionsByFile )
1448+
1449+ const shopifyDtsPath = joinPath ( tmpDir , 'shopify.d.ts' )
1450+
1451+ expect ( typeDefinitionsByFile ) . toStrictEqual (
1452+ new Map ( [
1453+ [
1454+ shopifyDtsPath ,
1455+ new Set ( [
1456+ `//@ts-ignore\ndeclare module './src/index.jsx' {
1457+ const shopify: import('@shopify/ui-extensions/${ genericTarget } ').Api &
1458+ import('@shopify/ui-extensions/${ genericTarget } ').ShopifyGlobal;
1459+ const globalThis: { shopify: typeof shopify };
1460+ }\n` ,
1461+ ] ) ,
1462+ ] ,
1463+ ] ) ,
1464+ )
1465+ } )
1466+ } )
1467+
1468+ test ( 'emits plain Api when target does not re-export ShopifyGlobal' , async ( ) => {
1469+ const typeDefinitionsByFile = new Map < string , Set < string > > ( )
1470+
1471+ await inTemporaryDirectory ( async ( tmpDir ) => {
1472+ // No `targetDtsContent` — the helper writes the default placeholder,
1473+ // which contains no `ShopifyGlobal` export. This guards against the
1474+ // detection helper accidentally tripping on targets that don't opt in.
1475+ const { extension} = await setupUIExtensionWithNodeModules ( {
1476+ tmpDir,
1477+ fileContent : '// JSX code' ,
1478+ apiVersion : '2025-10' ,
1479+ } )
1480+
1481+ const tsconfigPath = joinPath ( tmpDir , 'tsconfig.json' )
1482+ await writeFile ( tsconfigPath , '// TypeScript config' )
1483+
1484+ await extension . contributeToSharedTypeFile ?.( typeDefinitionsByFile )
1485+
1486+ const shopifyDtsPath = joinPath ( tmpDir , 'shopify.d.ts' )
1487+
1488+ expect ( typeDefinitionsByFile ) . toStrictEqual (
1489+ new Map ( [
1490+ [
1491+ shopifyDtsPath ,
1492+ new Set ( [
1493+ `//@ts-ignore\ndeclare module './src/index.jsx' {
1494+ const shopify: import('@shopify/ui-extensions/admin.product-details.action.render').Api;
1495+ const globalThis: { shopify: typeof shopify };
1496+ }\n` ,
1497+ ] ) ,
1498+ ] ,
1499+ ] ) ,
1500+ )
1501+ } )
1502+ } )
1503+
13701504 test ( "throws error when when api version supports Remote DOM and there's a tsconfig.json but type reference for target could not be found" , async ( ) => {
13711505 const typeDefinitionsByFile = new Map < string , Set < string > > ( )
13721506
0 commit comments