Skip to content

Commit b3d5736

Browse files
committed
Separate theme capabilities from the shared schema
1 parent 652c790 commit b3d5736

7 files changed

Lines changed: 43 additions & 36 deletions

File tree

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {BaseConfigType, MAX_EXTENSION_HANDLE_LENGTH, MAX_UID_LENGTH} from './schemas.js'
1+
import {BaseConfigType, BaseExtensionConfigType, MAX_EXTENSION_HANDLE_LENGTH, MAX_UID_LENGTH} from './schemas.js'
22
import {FunctionConfigType} from './specifications/function.js'
33
import {
44
DevSessionUpdateContext,
@@ -58,7 +58,7 @@ export const DEFAULT_DEV_SESSION_UPDATE_MESSAGE = 'Configuration accepted'
5858
*
5959
* This class holds the public interface to interact with extensions
6060
*/
61-
export class ExtensionInstance<TConfiguration extends BaseConfigType = BaseConfigType> {
61+
export class ExtensionInstance<TConfiguration extends BaseExtensionConfigType = BaseConfigType> {
6262
entrySourceFilePath: string
6363
devUUID: string
6464
localIdentifier: string

‎packages/app/src/cli/models/extensions/schemas.ts‎

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,8 @@ const IframeCapabilitySchema = zod.object({
2121
sources: zod.array(zod.string()).optional(),
2222
})
2323

24-
export const CapabilitiesSchema = zod.object({
25-
// Theme app extensions declare `network_access` as a table that lists the origins they contact (see the theme
26-
// specification). Every other extension declares a boolean, which is all this shared schema accepts. The wider
27-
// type lets the theme specification reuse `BaseConfigType`.
28-
network_access: zod.boolean().optional() as ZodSchemaType<boolean | {allowed_sources: string[]} | undefined>,
24+
const CapabilitiesSchema = zod.object({
25+
network_access: zod.boolean().optional(),
2926
block_progress: zod.boolean().optional(),
3027
api_access: zod.boolean().optional(),
3128
collect_buyer_consent: CollectBuyerConsentCapabilitySchema.optional(),
@@ -157,5 +154,7 @@ export const UnifiedSchema = zod.object({
157154

158155
export type NewExtensionPointSchemaType = zod.infer<typeof NewExtensionPointSchema>
159156

160-
// Base config type that all config schemas must extend.
161157
export type BaseConfigType = zod.infer<typeof BaseSchema>
158+
159+
// Base config type that all config schemas must extend.
160+
export type BaseExtensionConfigType = Omit<BaseConfigType, 'capabilities'>

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

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {ZodSchemaType, BaseConfigType, BaseSchema} from './schemas.js'
1+
import {ZodSchemaType, BaseConfigType, BaseExtensionConfigType, BaseSchema} from './schemas.js'
22
import {ExtensionInstance} from './extension-instance.js'
33
import {patchAppRelativeUrls} from './specifications/validation/app_relative_urls.js'
44
import {blocks} from '../../constants.js'
@@ -81,7 +81,7 @@ export interface DevSessionUpdateContext {
8181
/**
8282
* Extension specification with all the needed properties and methods to load an extension.
8383
*/
84-
export interface ExtensionSpecification<TConfiguration extends BaseConfigType = BaseConfigType> {
84+
export interface ExtensionSpecification<TConfiguration extends BaseExtensionConfigType = BaseConfigType> {
8585
identifier: string
8686
externalIdentifier: string
8787
externalName: string
@@ -179,7 +179,7 @@ export interface DevSessionWatchConfig {
179179
/**
180180
* Extension specification, explicitly marked as having taken remote configuration values into account.
181181
*/
182-
export type RemoteAwareExtensionSpecification<TConfiguration extends BaseConfigType = BaseConfigType> =
182+
export type RemoteAwareExtensionSpecification<TConfiguration extends BaseExtensionConfigType = BaseConfigType> =
183183
ExtensionSpecification<TConfiguration> & {
184184
loadedRemoteSpecs: true
185185
}
@@ -202,7 +202,7 @@ type ForbiddenFields =
202202
/**
203203
* Partial ExtensionSpec type used when creating a new ExtensionSpec, the only mandatory field is the identifier
204204
*/
205-
interface CreateExtensionSpecType<TConfiguration extends BaseConfigType = BaseConfigType> extends Partial<
205+
interface CreateExtensionSpecType<TConfiguration extends BaseExtensionConfigType = BaseConfigType> extends Partial<
206206
Omit<ExtensionSpecification<TConfiguration>, ForbiddenFields>
207207
> {
208208
identifier: string
@@ -229,9 +229,10 @@ interface CreateExtensionSpecType<TConfiguration extends BaseConfigType = BaseCo
229229
* hasExtensionPointTarget?: (configuration: TConfiguration, target: string) => boolean // function to determine if the extension has a given extension point target
230230
* ```
231231
*/
232-
export function createExtensionSpecification<TConfiguration extends BaseConfigType = BaseConfigType>(
232+
export function createExtensionSpecification<TConfiguration extends BaseExtensionConfigType = BaseConfigType>(
233233
spec: CreateExtensionSpecType<TConfiguration>,
234234
): ExtensionSpecification<TConfiguration> {
235+
const baseSchema: ZodSchemaType<BaseExtensionConfigType> = BaseSchema
235236
const defaults = {
236237
// these two fields are going to be overridden by the extension specification API response,
237238
// but we need them to have a default value for tests
@@ -240,7 +241,7 @@ export function createExtensionSpecification<TConfiguration extends BaseConfigTy
240241
externalName: capitalize(spec.identifier.replace(/_/g, ' ')),
241242
surface: 'test-surface',
242243
partnersWebIdentifier: spec.identifier,
243-
schema: BaseSchema as ZodSchemaType<TConfiguration>,
244+
schema: baseSchema as ZodSchemaType<TConfiguration>,
244245
registrationLimit: blocks.extensions.defaultRegistrationLimit,
245246
transform: spec.transformLocalToRemote,
246247
reverseTransform: spec.transformRemoteToLocal,
@@ -292,7 +293,9 @@ export function createExtensionSpecification<TConfiguration extends BaseConfigTy
292293
* - App config extensions have default transform and reverseTransform functions
293294
294295
*/
295-
export function createConfigExtensionSpecification<TConfiguration extends BaseConfigType = BaseConfigType>(spec: {
296+
export function createConfigExtensionSpecification<
297+
TConfiguration extends BaseExtensionConfigType = BaseConfigType,
298+
>(spec: {
296299
identifier: string
297300
schema: ZodSchemaType<TConfiguration>
298301
clientSteps?: ClientSteps
@@ -325,7 +328,7 @@ export function createConfigExtensionSpecification<TConfiguration extends BaseCo
325328
})
326329
}
327330

328-
export function createContractBasedModuleSpecification<TConfiguration extends BaseConfigType = BaseConfigType>(
331+
export function createContractBasedModuleSpecification<TConfiguration extends BaseExtensionConfigType = BaseConfigType>(
329332
spec: Pick<
330333
CreateExtensionSpecType<TConfiguration>,
331334
| 'identifier'

‎packages/app/src/cli/models/extensions/specifications/theme.test.ts‎

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,20 @@ describe('theme', () => {
5252
expect(parse({}).data?.capabilities?.network_access).toBeUndefined()
5353
})
5454

55-
test('ignores a boolean network_access, as before', () => {
56-
const result = parse({network_access: true})
55+
test.each([true, false])('rejects a boolean network_access of %s', (networkAccess) => {
56+
const result = parse({network_access: networkAccess})
57+
58+
expect(result.state).toBe('error')
59+
expect(errorMessages(result)).toEqual([
60+
'network_access must be a table with an allowed_sources list for theme app extensions',
61+
])
62+
})
63+
64+
test('ignores capabilities that do not apply to theme extensions', () => {
65+
const result = parse({network_access: {allowed_sources: []}, api_access: true, block_progress: true})
5766

5867
expect(result.state).toBe('ok')
59-
expect(result.data?.capabilities?.network_access).toBeUndefined()
68+
expect(result.data?.capabilities).toEqual({network_access: {allowed_sources: []}})
6069
})
6170

6271
test('requires allowed_sources when the network_access table is declared', () => {
@@ -77,7 +86,7 @@ describe('theme', () => {
7786
])
7887
})
7988

80-
test('rejects a network_access value that is neither a table nor a boolean', () => {
89+
test('rejects a network_access value that is not a table', () => {
8190
const result = parse({network_access: 'https://api.example.com'})
8291

8392
expect(result.state).toBe('error')
@@ -156,8 +165,8 @@ describe('theme', () => {
156165
await expect(themeSpec.deployConfig?.(config, '', '', undefined)).resolves.toEqual({theme_extension: {files: {}}})
157166
})
158167

159-
test('sends only the theme extension files when network_access is a boolean', async () => {
160-
const config = parse({network_access: true, block_progress: true}).data!
168+
test('sends only the theme extension files when other capabilities are declared', async () => {
169+
const config = parse({api_access: true, block_progress: true}).data!
161170

162171
await expect(themeSpec.deployConfig?.(config, '', '', undefined)).resolves.toEqual({theme_extension: {files: {}}})
163172
})
@@ -182,16 +191,16 @@ describe('theme', () => {
182191
})
183192

184193
describe('UI extensions', () => {
185-
test('still accept a boolean network_access', () => {
194+
test.each([true, false])('still accept a boolean network_access of %s', (networkAccess) => {
186195
const result = UIExtensionSchema.safeParse({
187196
name: 'ui extension',
188197
type: 'ui_extension',
189198
extension_points: [],
190-
capabilities: {network_access: true},
199+
capabilities: {network_access: networkAccess},
191200
})
192201

193202
expect(result.success).toBe(true)
194-
expect(result.data?.capabilities?.network_access).toBe(true)
203+
expect(result.data?.capabilities?.network_access).toBe(networkAccess)
195204
})
196205

197206
test('reject a network_access table', () => {

‎packages/app/src/cli/models/extensions/specifications/theme.ts‎

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {createExtensionSpecification} from '../specification.js'
2-
import {BaseSchema, CapabilitiesSchema} from '../schemas.js'
2+
import {BaseSchema} from '../schemas.js'
33
import {themeExtensionFiles} from '../../../utilities/extensions/theme.js'
44
import {ExtensionInstance} from '../extension-instance.js'
55
import {fileSize} from '@shopify/cli-kit/node/fs'
@@ -73,12 +73,8 @@ const ThemeNetworkAccessSchema = zod.object(
7373
{invalid_type_error: 'network_access must be a table with an allowed_sources list for theme app extensions'},
7474
)
7575

76-
const ThemeCapabilitiesSchema = CapabilitiesSchema.extend({
77-
// A boolean `network_access` (used by UI extensions) never had an effect on theme app extensions. Keep ignoring it.
78-
network_access: zod.preprocess(
79-
(value) => (typeof value === 'boolean' ? undefined : value),
80-
ThemeNetworkAccessSchema.optional(),
81-
),
76+
const ThemeCapabilitiesSchema = zod.object({
77+
network_access: ThemeNetworkAccessSchema.optional(),
8278
})
8379

8480
const ThemeSchema = BaseSchema.extend({
@@ -105,7 +101,7 @@ const themeSpec = createExtensionSpecification({
105101
deployConfig: async (config) => {
106102
const networkAccess = config.capabilities?.network_access
107103
// Only send capabilities when they're declared, so existing payloads stay the same.
108-
if (typeof networkAccess !== 'object') return {theme_extension: {files: {}}}
104+
if (!networkAccess) return {theme_extension: {files: {}}}
109105

110106
return {
111107
theme_extension: {files: {}},
@@ -157,7 +153,7 @@ const SUPPORTED_EXTS: {[dirname: string]: FilenameValidation} = {
157153
}
158154
const SUPPORTED_BUCKETS = Object.keys(SUPPORTED_EXTS)
159155

160-
async function validateThemeExtension(extension: ExtensionInstance): Promise<void> {
156+
async function validateThemeExtension(extension: Pick<ExtensionInstance, 'directory'>): Promise<void> {
161157
const themeFiles = await themeExtensionFiles(extension)
162158
const liquidBytes: number[] = []
163159
const extensionBytes: number[] = []

‎packages/app/src/cli/services/dev/extension/payload.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ export async function getUIExtensionPayload(
9898
},
9999
capabilities: {
100100
blockProgress: extension.configuration.capabilities?.block_progress ?? false,
101-
networkAccess: extension.configuration.capabilities?.network_access === true,
101+
networkAccess: extension.configuration.capabilities?.network_access ?? false,
102102
apiAccess: extension.configuration.capabilities?.api_access ?? false,
103103
collectBuyerConsent: {
104104
smsMarketing: extension.configuration.capabilities?.collect_buyer_consent?.sms_marketing ?? false,

‎packages/app/src/cli/utilities/extensions/theme.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ const ignoredFilePatterns = [
2222
'*.toml',
2323
]
2424

25-
export async function themeExtensionFiles(themeExtension: ExtensionInstance): Promise<string[]> {
25+
export async function themeExtensionFiles(themeExtension: Pick<ExtensionInstance, 'directory'>): Promise<string[]> {
2626
const filename = '.shopifyignore'
2727
const filepath = joinPath(themeExtension.directory, filename)
2828
const ignore = ignoredFilePatterns.map((pattern) => joinPath('*', pattern))

0 commit comments

Comments
 (0)