Skip to content

[DNM] Generate and store a combined extension schema #5609

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: shauns/04-04-output-debug-friendly-schema
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions packages/app/src/cli/models/extensions/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@
key: zod.string(),
})

export const MetafieldSchemaAsJson = {

Check failure on line 13 in packages/app/src/cli/models/extensions/schemas.ts

View workflow job for this annotation

GitHub Actions / knip-reporter-annotations-check

packages/app/src/cli/models/extensions/schemas.ts#L13

'MetafieldSchemaAsJson' is an unused export
type: 'object',
properties: {
key: {type: 'string'},
namespace: {type: 'string'},
},
required: ['key', 'namespace'],
}

const CollectBuyerConsentCapabilitySchema = zod.object({
sms_marketing: zod.boolean().optional(),
customer_privacy: zod.boolean().optional(),
Expand Down Expand Up @@ -59,6 +68,45 @@
.optional(),
})

export const NewExtensionPointsSchemaAsJson = {
type: 'object',
properties: {
target: {type: 'string'},
module: {type: 'string'},
should_render: {
type: 'object',
properties: {
module: {type: 'string'},
},
required: ['module'],
},
metafields: {
type: 'array',
items: MetafieldSchemaAsJson,
},
default_placement: {type: 'string'},
urls: {
type: 'object',
properties: {
edit: {type: 'string'},
},
},
capabilities: {
type: 'object',
properties: {
allow_direct_linking: {type: 'boolean'},
},
},
preloads: {
type: 'object',
properties: {
chat: {type: 'string'},
},
},
},
required: ['target', 'module'],
}

export const NewExtensionPointsSchema = zod.array(NewExtensionPointSchema)
const ApiVersionSchema = zod.string()

Expand All @@ -80,6 +128,30 @@
fields: zod.array(FieldSchema).optional(),
})

export const SettingsSchemaAsJson = {
type: 'object',
properties: {
fields: {
type: 'array',
items: {
type: 'object',
properties: {
key: {type: 'string'},
name: {type: 'string'},
description: {type: 'string'},
required: {type: 'boolean'},
type: {type: 'string'},
validations: {type: 'array', items: {type: 'object'}},
marketingActivityCreateUrl: {type: 'string'},
marketingActivityDeleteUrl: {type: 'string'},
},
required: ['type'],
additionalProperties: true,
},
},
},
}

const HandleSchema = zod
.string()
.trim()
Expand All @@ -106,6 +178,46 @@
handle: HandleSchema,
})

export const BaseSchemaWithHandleAsJson = {
type: 'object',
properties: {
name: {type: 'string'},
extension_points: {type: 'array', items: NewExtensionPointsSchemaAsJson},
targeting: {
type: 'array',
items: NewExtensionPointsSchemaAsJson,
},
handle: {type: 'string'},
uid: {type: 'string'},
description: {type: 'string'},
capabilities: {
type: 'object',
properties: {
network_access: {type: 'boolean'},
block_progress: {type: 'boolean'},
api_access: {type: 'boolean'},
collect_buyer_consent: {
type: 'object',
properties: {
sms_marketing: {type: 'boolean'},
customer_privacy: {type: 'boolean'},
},
},
iframe: {
type: 'object',
properties: {
sources: {type: 'array', items: {type: 'string'}},
},
},
},
},
metafields: {type: 'array', items: MetafieldSchemaAsJson},
settings: SettingsSchemaAsJson,
},
required: ['name', 'type', 'handle'],
additionalProperties: false,
}

export const UnifiedSchema = zod.object({
api_version: ApiVersionSchema.optional(),
description: zod.string().optional(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {createExtensionSpecification} from '../specification.js'
import {BaseSchema} from '../schemas.js'
import {BaseSchema, BaseSchemaWithHandleAsJson} from '../schemas.js'
import {loadLocalesConfig} from '../../../utilities/extensions/locales-configuration.js'
import {zod} from '@shopify/cli-kit/node/schema'
import {joinPath} from '@shopify/cli-kit/node/path'
Expand Down Expand Up @@ -80,6 +80,66 @@ const functionSpec = createExtensionSpecification({
'pickup_point_delivery_option_generator',
],
schema: FunctionExtensionSchema,
hardcodedInputJsonSchema: JSON.stringify({
...BaseSchemaWithHandleAsJson,
properties: {
...BaseSchemaWithHandleAsJson.properties,
type: {const: 'function'},
build: {
type: 'object',
properties: {
command: {type: 'string'},
path: {type: 'string'},
watch: {
type: 'string',
},
wasm_opt: {type: 'boolean'},
},
},
configuration_ui: {type: 'boolean'},
ui: {
type: 'object',
properties: {
enable_create: {type: 'boolean'},
paths: {
type: 'object',
properties: {
create: {type: 'string'},
details: {type: 'string'},
},
required: ['create', 'details'],
},
handle: {type: 'string'},
},
},
input: {
type: 'object',
properties: {
variables: {
type: 'object',
properties: {
namespace: {type: 'string'},
key: {type: 'string'},
},
required: ['namespace', 'key'],
},
},
},
targeting: {
type: 'array',
items: {
type: 'object',
properties: {
target: {type: 'string'},
input_query: {type: 'string'},
export: {type: 'string'},
},
required: ['target'],
},
},
},
required: [...BaseSchemaWithHandleAsJson.required, 'build'],
}),
appModuleFeatures: (_) => ['function', 'bundling'],
deployConfig: async (config, directory, apiKey) => {
let inputQuery: string | undefined
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import {Asset, AssetIdentifier, ExtensionFeature, createExtensionSpecification} from '../specification.js'
import {NewExtensionPointSchemaType, NewExtensionPointsSchema, BaseSchema} from '../schemas.js'
import {
NewExtensionPointSchemaType,
NewExtensionPointsSchema,
BaseSchema,
NewExtensionPointsSchemaAsJson,
BaseSchemaWithHandleAsJson,
} from '../schemas.js'
import {loadLocalesConfig} from '../../../utilities/extensions/locales-configuration.js'
import {getExtensionPointTargetSurface} from '../../../services/dev/extension/utilities.js'
import {err, ok, Result} from '@shopify/cli-kit/node/result'
Expand Down Expand Up @@ -77,6 +83,17 @@ const uiExtensionSpec = createExtensionSpecification({
identifier: 'ui_extension',
dependency,
schema: UIExtensionSchema,
hardcodedInputJsonSchema: JSON.stringify({
...BaseSchemaWithHandleAsJson,
properties: {
...BaseSchemaWithHandleAsJson.properties,
type: {const: 'ui_extension'},
targeting: {
type: 'array',
items: NewExtensionPointsSchemaAsJson,
},
},
}),
appModuleFeatures: (config) => {
const basic: ExtensionFeature[] = ['ui_preview', 'bundling', 'esbuild', 'generates_source_maps']
const needsCart =
Expand Down
55 changes: 53 additions & 2 deletions packages/app/src/cli/services/app-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import {AppLinkedInterface} from '../models/app/app.js'
import metadata from '../metadata.js'
import {FlattenedRemoteSpecification} from '../api/graphql/extension_specifications.js'
import {SettingsSchemaAsJson} from '../models/extensions/schemas.js'
import {tryParseInt} from '@shopify/cli-kit/common/string'
import {mkdir, fileExists, writeFile, removeFile, readdir} from '@shopify/cli-kit/node/fs'
import {joinPath} from '@shopify/cli-kit/node/path'
Expand Down Expand Up @@ -62,7 +63,7 @@
// Get list of expected schema file names based on specifications
const expectedSchemaFiles = new Set(specifications.map((spec) => `${spec.identifier}.schema.json`))
expectedSchemaFiles.add('app.schema.json')

expectedSchemaFiles.add('extension.schema.json')
// Prepare all the schema writing promises
const writePromises = specifications.map((spec) => {
// Cast to include FlattenedRemoteSpecification to access validationSchema
Expand All @@ -79,11 +80,14 @@
})

const combinedConfigSchema = generateCombinedConfigSchema(specifications)

const combinedExtensionSchema = generateCombinedExtensionSchema(specifications)
// combined config schema should be written to the .shopify/schemas directory
const combinedConfigSchemaPath = joinPath(directory, '.shopify', 'schemas', 'app.schema.json')
await writeFile(combinedConfigSchemaPath, JSON.stringify(combinedConfigSchema, null, 2))

const combinedExtensionSchemaPath = joinPath(directory, '.shopify', 'schemas', 'extension.schema.json')
await writeFile(combinedExtensionSchemaPath, JSON.stringify(combinedExtensionSchema, null, 2))

// Execute all write operations in parallel
await Promise.all(writePromises)

Expand All @@ -106,8 +110,18 @@
`app-${timestamp}.schema.json`,
)
await writeFile(combinedConfigSchemaPathWithTimestamp, JSON.stringify(combinedConfigSchema, null, 2))
// TODO drop this in real use

Check failure on line 113 in packages/app/src/cli/services/app-context.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/app/src/cli/services/app-context.ts#L113

[no-warning-comments] Unexpected 'todo' comment: 'TODO drop this in real use'.
outputInfo(`Combined config schema written to: ${combinedConfigSchemaPathWithTimestamp}`)

const combinedExtensionSchemaPathWithTimestamp = joinPath(
directory,
'.shopify',
'schemas',
`extension-${timestamp}.schema.json`,
)
await writeFile(combinedExtensionSchemaPathWithTimestamp, JSON.stringify(combinedExtensionSchema, null, 2))
// TODO drop this in real use

Check failure on line 123 in packages/app/src/cli/services/app-context.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/app/src/cli/services/app-context.ts#L123

[no-warning-comments] Unexpected 'todo' comment: 'TODO drop this in real use'.
outputInfo(`Combined extension schema written to: ${combinedExtensionSchemaPathWithTimestamp}`)
}

function generateCombinedConfigSchema(specifications: RemoteAwareExtensionSpecification[]) {
Expand Down Expand Up @@ -138,6 +152,43 @@
return combinedConfigSchema
}

function generateCombinedExtensionSchema(specifications: RemoteAwareExtensionSpecification[]) {
const extensionModules = specifications.filter(
(spec) => spec.uidStrategy === 'uuid',
) as (RemoteAwareExtensionSpecification & FlattenedRemoteSpecification)[]

const combinedExtensionSchema = {
type: 'object',
properties: {
api_version: {type: 'string'},
description: {type: 'string'},
settings: SettingsSchemaAsJson,
extensions: {
type: 'array',
items: {
type: 'object',
discriminator: {propertyName: 'type'},
required: ['type'],
oneOf: [] as object[],
},
},
},
required: ['api_version', 'extensions'],
additionalProperties: false,
}

for (const spec of extensionModules) {
const schema = spec.hardcodedInputJsonSchema ?? spec.validationSchema?.jsonSchema
if (!schema) continue
const jsonSchemaContent = JSON.parse(schema)

// each schema is added to the oneOf array
combinedExtensionSchema.properties.extensions.items.oneOf.push(jsonSchemaContent)
}

return combinedExtensionSchema
}

/**
* This function always returns an app that has been correctly linked and was loaded using the remote specifications.
*
Expand Down
Loading