Skip to content

Commit fe453d1

Browse files
committed
Add intents registration for admin_link
1 parent 84ee286 commit fe453d1

File tree

8 files changed

+337
-19
lines changed

8 files changed

+337
-19
lines changed

packages/app/src/cli/models/app/app.test-data.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1048,6 +1048,23 @@ const testRemoteSpecifications: RemoteSpecification[] = [
10481048
'{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","additionalProperties":false,"properties":{"pattern":{"type":"string"},"name":{"type":"string"}},"required":["pattern"]}',
10491049
},
10501050
},
1051+
{
1052+
name: 'Admin Link',
1053+
externalName: 'Admin Link',
1054+
identifier: 'admin_link',
1055+
externalIdentifier: 'admin_link_external',
1056+
gated: false,
1057+
experience: 'extension',
1058+
options: {
1059+
managementExperience: 'cli',
1060+
registrationLimit: 10,
1061+
uidIsClientProvided: true,
1062+
},
1063+
validationSchema: {
1064+
jsonSchema:
1065+
'{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","additionalProperties":true,"properties":{"name":{"type":"string"},"type":{"type":"string"},"handle":{"type":"string"},"targeting":{"type":"array","items":{"type":"object","properties":{"target":{"type":"string"},"tools":{"type":"string"},"instructions":{"type":"string"},"intents":{"type":"array"},"build_manifest":{"type":"object"}},"additionalProperties":true}}},"required":["name","targeting"]}',
1066+
},
1067+
},
10511068
]
10521069

10531070
const productSubscriptionUIExtensionTemplate: ExtensionTemplate = {

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,8 @@ export class ExtensionInstance<TConfiguration extends BaseConfigType = BaseConfi
366366
this.specification.buildConfig.filePatterns,
367367
this.specification.buildConfig.ignoredFilePatterns,
368368
)
369+
case 'copy_static_assets':
370+
return this.copyStaticAssets()
369371
case 'none':
370372
break
371373
}

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

Lines changed: 57 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/* eslint-disable @typescript-eslint/no-non-null-assertion */
22
import {ZodSchemaType, BaseConfigType, BaseSchema} from './schemas.js'
33
import {ExtensionInstance} from './extension-instance.js'
4+
import {adminLinkOverride} from './specifications/remote-overrides/admin_link.js'
45
import {blocks} from '../../constants.js'
56

67
import {Flag} from '../../utilities/developer-platform-client.js'
@@ -56,7 +57,7 @@ export interface BuildAsset {
5657
}
5758

5859
type BuildConfig =
59-
| {mode: 'ui' | 'theme' | 'function' | 'tax_calculation' | 'none'}
60+
| {mode: 'ui' | 'theme' | 'function' | 'tax_calculation' | 'none' | 'copy_static_assets'}
6061
| {mode: 'copy_files'; filePatterns: string[]; ignoredFilePatterns?: string[]}
6162
/**
6263
* Extension specification with all the needed properties and methods to load an extension.
@@ -167,6 +168,11 @@ interface CreateExtensionSpecType<TConfiguration extends BaseConfigType = BaseCo
167168
schema?: ZodSchemaType<TConfiguration>
168169
}
169170

171+
export type CreateContractOverrideExtensionSpecType<TConfiguration extends BaseConfigType = BaseConfigType> = Partial<
172+
CreateExtensionSpecType<TConfiguration>
173+
> & {
174+
transform?: (config: TConfiguration) => TConfiguration
175+
}
170176
/**
171177
* Create a new ui extension spec.
172178
*
@@ -269,21 +275,25 @@ export function createConfigExtensionSpecification<TConfiguration extends BaseCo
269275
}
270276

271277
export function createContractBasedModuleSpecification<TConfiguration extends BaseConfigType = BaseConfigType>(
272-
spec: Pick<CreateExtensionSpecType<TConfiguration>, 'identifier' | 'appModuleFeatures' | 'buildConfig'>,
278+
spec: Pick<CreateExtensionSpecType<TConfiguration>, 'identifier' | 'appModuleFeatures'> &
279+
CreateContractOverrideExtensionSpecType<TConfiguration>,
273280
) {
281+
const defaultDeployConfig = async (config: TConfiguration, directory: string) => {
282+
let parsedConfig = configWithoutFirstClassFields(config)
283+
if (spec.appModuleFeatures().includes('localization')) {
284+
const localization = await loadLocalesConfig(directory, spec.identifier)
285+
parsedConfig = {...parsedConfig, localization}
286+
}
287+
return parsedConfig
288+
}
289+
290+
const schema = spec.transform ? zod.any({}).transform(spec.transform) : zod.any({})
291+
274292
return createExtensionSpecification({
275-
identifier: spec.identifier,
276-
schema: zod.any({}) as unknown as ZodSchemaType<TConfiguration>,
277-
appModuleFeatures: spec.appModuleFeatures,
293+
...spec,
294+
schema,
278295
buildConfig: spec.buildConfig ?? {mode: 'none'},
279-
deployConfig: async (config, directory) => {
280-
let parsedConfig = configWithoutFirstClassFields(config)
281-
if (spec.appModuleFeatures().includes('localization')) {
282-
const localization = await loadLocalesConfig(directory, spec.identifier)
283-
parsedConfig = {...parsedConfig, localization}
284-
}
285-
return parsedConfig
286-
},
296+
deployConfig: spec.deployConfig ?? defaultDeployConfig,
287297
})
288298
}
289299

@@ -401,3 +411,37 @@ export function configWithoutFirstClassFields(config: JsonMapType): JsonMapType
401411
const {type, handle, uid, path, extensions, ...configWithoutFirstClassFields} = config
402412
return configWithoutFirstClassFields
403413
}
414+
415+
/**
416+
* Specification overrides for remote specifications that need custom behavior.
417+
* These overrides are applied when creating contract-based module specifications
418+
* for extensions that are defined remotely but need local customization.
419+
*
420+
* Can include any method from ExtensionSpecification plus a schema.
421+
* All properties are optional - only provide what you want to override.
422+
*/
423+
export type SpecificationOverride<TConfiguration extends BaseConfigType = BaseConfigType> = Partial<
424+
ExtensionSpecification<TConfiguration>
425+
> & {
426+
schema?: ZodSchemaType<TConfiguration>
427+
}
428+
429+
/**
430+
* Registry of specification overrides by identifier.
431+
* Add custom behavior for remote specifications here.
432+
*/
433+
export const SPECIFICATION_OVERRIDES: {[key: string]: SpecificationOverride} = {
434+
admin_link: adminLinkOverride as unknown as SpecificationOverride,
435+
}
436+
437+
/**
438+
* Get the override configuration for a specific specification identifier.
439+
*
440+
* @param identifier - The specification identifier
441+
* @returns The override configuration if it exists, undefined otherwise
442+
*/
443+
export function getSpecificationOverride<TConfiguration extends BaseConfigType = BaseConfigType>(
444+
identifier: string,
445+
): SpecificationOverride<TConfiguration> | undefined {
446+
return SPECIFICATION_OVERRIDES[identifier] as SpecificationOverride<TConfiguration> | undefined
447+
}

packages/app/src/cli/models/extensions/specifications/build-manifest-schema.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {AssetIdentifier, BuildAsset} from '../specification.js'
2-
import {fileExists, copyFile} from '@shopify/cli-kit/node/fs'
2+
import {fileExists, copyFile, mkdir} from '@shopify/cli-kit/node/fs'
33
import {joinPath, dirname, basename} from '@shopify/cli-kit/node/path'
44
import {outputContent, outputToken} from '@shopify/cli-kit/node/output'
55
import {err, ok, Result} from '@shopify/cli-kit/node/result'
@@ -14,7 +14,7 @@ export {ok, err}
1414
*/
1515
export interface TargetingWithBuildManifest {
1616
target: string
17-
build_manifest?: BuildManifest
17+
build_manifest: BuildManifest
1818
}
1919

2020
/**
@@ -142,6 +142,10 @@ async function copyAsset(
142142
if (isStatic) {
143143
const sourceFile = joinPath(directory, module)
144144
const outputFilePath = joinPath(dirname(outputPath), filepath)
145+
146+
// Ensure the directory exists before copying
147+
await mkdir(dirname(outputFilePath))
148+
145149
await copyFile(sourceFile, outputFilePath).catch((error) => {
146150
throw new Error(`Failed to copy static asset ${module} to ${outputFilePath}: ${error.message}`)
147151
})
@@ -217,9 +221,7 @@ export async function validateBuildManifestAssets(
217221
* @param extensionPoint - Extension point with build manifest
218222
* @returns Extension point with updated asset paths
219223
*/
220-
export function addDistPathToAssets<T extends TargetingWithBuildManifest & {build_manifest: BuildManifest}>(
221-
extP: T,
222-
): T {
224+
export function addDistPathToAssets<T extends TargetingWithBuildManifest>(extP: T): T {
223225
return {
224226
...extP,
225227
build_manifest: {
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import {
2+
transformStaticAssets,
3+
copyStaticBuildManifestAssets,
4+
validateBuildManifestAssets,
5+
addDistPathToAssets,
6+
TargetingWithBuildManifest,
7+
} from '../build-manifest-schema.js'
8+
import {configWithoutFirstClassFields, CreateContractOverrideExtensionSpecType} from '../../specification.js'
9+
import {loadLocalesConfig} from '../../../../utilities/extensions/locales-configuration.js'
10+
import {JsonMapType} from '@shopify/cli-kit/node/toml'
11+
12+
interface AdminLinkPartialConfig {
13+
targeting: TargetingWithBuildManifest[]
14+
handle?: string
15+
}
16+
17+
export const adminLinkOverride: CreateContractOverrideExtensionSpecType<AdminLinkPartialConfig> = {
18+
buildConfig: {mode: 'copy_static_assets'},
19+
transform: (config: AdminLinkPartialConfig) => {
20+
return {
21+
...config,
22+
targeting: config.targeting.map((targeting) => {
23+
return {
24+
...targeting,
25+
...transformStaticAssets(targeting, config.handle ?? 'admin-link'),
26+
}
27+
}),
28+
}
29+
},
30+
validate: async (config: AdminLinkPartialConfig, _path: string, directory: string) =>
31+
validateBuildManifestAssets(directory, config.targeting),
32+
copyStaticAssets: async (config: AdminLinkPartialConfig, directory: string, outputPath: string) =>
33+
copyStaticBuildManifestAssets(config.targeting, directory, outputPath),
34+
deployConfig: async (config: AdminLinkPartialConfig, directory: string) => {
35+
const parsedConfig = configWithoutFirstClassFields(config as unknown as JsonMapType)
36+
const localization = await loadLocalesConfig(directory, 'admin_link')
37+
38+
return {
39+
...parsedConfig,
40+
localization,
41+
targeting: config.targeting.map(addDistPathToAssets),
42+
}
43+
},
44+
}

packages/app/src/cli/models/extensions/specifications/ui_extension.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
addDistPathToAssets,
1313
transformStaticAssets,
1414
BuildManifest,
15+
TargetingWithBuildManifest,
1516
} from './build-manifest-schema.js'
1617
import {Asset, AssetIdentifier, ExtensionFeature, createExtensionSpecification} from '../specification.js'
1718
import {NewExtensionPointSchemaType, NewExtensionPointsSchema, BaseSchema, MetafieldSchema} from '../schemas.js'
@@ -304,7 +305,7 @@ const uiExtensionSpec = createExtensionSpecification({
304305

305306
async function validateUIExtensionPointConfig(
306307
directory: string,
307-
extensionPoints: (NewExtensionPointSchemaType & {build_manifest?: BuildManifest})[],
308+
extensionPoints: (NewExtensionPointSchemaType & TargetingWithBuildManifest)[],
308309
configPath: string,
309310
): Promise<Result<unknown, string>> {
310311
const errors: string[] = []

0 commit comments

Comments
 (0)