Skip to content
Open
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
30 changes: 16 additions & 14 deletions packages/app/src/cli/models/app/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,20 +157,22 @@ export enum WebType {

const WebConfigurationAuthCallbackPathSchema = zod.preprocess(ensurePathStartsWithSlash, zod.string())

const baseWebConfigurationSchema = zod.object({
auth_callback_path: zod
.union([WebConfigurationAuthCallbackPathSchema, WebConfigurationAuthCallbackPathSchema.array()])
.optional(),
webhooks_path: zod.preprocess(ensurePathStartsWithSlash, zod.string()).optional(),
port: zod.number().max(65536).min(0).optional(),
commands: zod.object({
build: zod.string().optional(),
predev: zod.string().optional(),
dev: zod.string(),
}),
name: zod.string().optional(),
hmr_server: zod.object({http_paths: zod.string().array()}).optional(),
})
const baseWebConfigurationSchema = zod
.object({
auth_callback_path: zod
.union([WebConfigurationAuthCallbackPathSchema, WebConfigurationAuthCallbackPathSchema.array()])
.optional(),
webhooks_path: zod.preprocess(ensurePathStartsWithSlash, zod.string()).optional(),
port: zod.number().max(65536).min(0).optional(),
commands: zod.object({
build: zod.string().optional(),
predev: zod.string().optional(),
dev: zod.string(),
}),
name: zod.string().optional(),
hmr_server: zod.object({http_paths: zod.string().array()}).optional(),
})
.strict()
const webTypes = zod.enum([WebType.Frontend, WebType.Backend, WebType.Background]).default(WebType.Frontend)
export const WebConfigurationSchema = zod.union([
baseWebConfigurationSchema.extend({roles: zod.array(webTypes)}),
Expand Down
94 changes: 65 additions & 29 deletions packages/app/src/cli/models/app/loader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {platformAndArch} from '@shopify/cli-kit/node/os'
import {zod} from '@shopify/cli-kit/node/schema'
import colors from '@shopify/cli-kit/node/colors'
import {showMultipleCLIWarningIfNeeded} from '@shopify/cli-kit/node/multiple-installation-warning'
import {stringifyMessage} from '@shopify/cli-kit/node/output'

vi.mock('../../services/local-storage.js')
vi.mock('@shopify/cli-kit/node/system')
Expand Down Expand Up @@ -699,6 +700,28 @@ describe('load', () => {
expect(app.webs.length).toBe(0)
})

test('collects an error for unrecognized keys in web TOML', async () => {
// Given
const {webDirectory} = await writeConfig(appConfiguration)
const webConfiguration = `
type = "backend"
unknown_key = "should-not-be-here"

[commands]
build = "build"
dev = "dev"
`
await writeFile(joinPath(webDirectory, blocks.web.configurationName), webConfiguration)

// When
const app = await loadTestingApp()

// Then
expect(app.errors.isEmpty()).toBe(false)
const errors = app.errors.toJSON().map(stringifyMessage).join('\n')
expect(errors).toMatch(/Unrecognized key.*unknown_key/)
})

test('loads the app when it has a extension with a valid configuration', async () => {
// Given
await writeConfig(appConfiguration)
Expand Down Expand Up @@ -999,40 +1022,63 @@ describe('load', () => {
await expect(() => loadTestingApp()).rejects.toThrowError()
})

test('loads the app when it has a function with a valid configuration', async () => {
test('collects an error for unrecognized keys in unified extension TOML', async () => {
// Given
await writeConfig(appConfiguration)

const blockConfiguration = `
name = "my-function"
type = "order_discounts"
api_version = "2022-07"
api_version = "2024-01"

[build]
command = "make build"
path = "dist/index.wasm"
[metaobjects]
something = "misplaced"

# extra fields not included in the schema should be ignored
[[invalid_field]]
namespace = "my-namespace"
key = "my-key"
[[extensions]]
type = "flow_action"
handle = "my-flow-action"
name = "My Flow Action"
description = "A flow action"
runtime_url = "https://example.com"
`
await writeBlockConfig({
blockConfiguration,
name: 'my-function',
name: 'my-extension',
})
await mkdir(joinPath(blockPath('my-function'), 'src'))
await writeFile(joinPath(blockPath('my-function'), 'src', 'index.js'), '')
await writeFile(joinPath(blockPath('my-extension'), 'index.js'), '')

// When
const app = await loadTestingApp()
const myFunction = app.allExtensions[0]!

// Then
expect(myFunction.configuration.name).toBe('my-function')
expect(myFunction.idEnvironmentVariableName).toBe('SHOPIFY_MY_FUNCTION_ID')
expect(myFunction.localIdentifier).toBe('my-function')
expect(myFunction.entrySourceFilePath).toContain(joinPath(blockPath('my-function'), 'src', 'index.js'))
expect(app.errors.isEmpty()).toBe(false)
const errors = app.errors.toJSON().map(stringifyMessage).join('\n')
expect(errors).toMatch(/Unrecognized key.*metaobjects/)
})

test('does not collect errors when unified extension TOML has only recognized keys', async () => {
// Given
await writeConfig(appConfiguration)

const blockConfiguration = `
api_version = "2024-01"

[[extensions]]
type = "flow_action"
handle = "my-flow-action"
name = "My Flow Action"
description = "A flow action"
runtime_url = "https://example.com"
`
await writeBlockConfig({
blockConfiguration,
name: 'my-extension',
})
await writeFile(joinPath(blockPath('my-extension'), 'index.js'), '')

// When
const app = await loadTestingApp()

// Then
expect(app.errors.isEmpty()).toBe(true)
})

test('loads the app with a Flow trigger extension that has a full valid configuration', async () => {
Expand All @@ -1054,11 +1100,6 @@ describe('load', () => {
[[settings.fields]]
type = "single_line_text_field"
key = "your field key"

# extra fields not included in the schema should be ignored
[[invalid_field]]
namespace = "my-namespace"
key = "my-key"
`
await writeBlockConfig({
blockConfiguration,
Expand Down Expand Up @@ -1123,11 +1164,6 @@ describe('load', () => {
name = "Display name"
description = "A description of my field"
required = true

# extra fields not included in the schema should be ignored
[[invalid_field]]
namespace = "my-namespace"
key = "my-key"
`
await writeBlockConfig({
blockConfiguration,
Expand Down
43 changes: 42 additions & 1 deletion packages/app/src/cli/models/extensions/schemas.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {BaseSchema, MAX_UID_LENGTH} from './schemas.js'
import {BaseSchema, MAX_UID_LENGTH, UnifiedSchema} from './schemas.js'
import {describe, expect, test} from 'vitest'

const validUIDTestCases = [
Expand Down Expand Up @@ -29,6 +29,47 @@ const invalidUIDTestCases = [
['-----', "UID can't start or end with a hyphen"],
]

describe('UnifiedSchema', () => {
test('rejects unrecognized top-level keys', () => {
// Given
const config = {
api_version: '2024-01',
extensions: [{type: 'ui_extension', handle: 'my-ext'}],
metaobjects: {something: 'misplaced'},
}

// When
const result = UnifiedSchema.safeParse(config)

// Then
expect(result.success).toBe(false)
if (!result.success) {
expect(result.error.issues[0]!.code).toBe('unrecognized_keys')
expect(result.error.issues[0]!.message).toMatch(/metaobjects/)
}
})

test('rejects multiple unrecognized keys', () => {
// Given
const config = {
extensions: [],
metaobjects: {},
unknown_field: 'value',
}

// When
const result = UnifiedSchema.safeParse(config)

// Then
expect(result.success).toBe(false)
if (!result.success) {
expect(result.error.issues[0]!.code).toBe('unrecognized_keys')
expect(result.error.issues[0]!.message).toMatch(/metaobjects/)
expect(result.error.issues[0]!.message).toMatch(/unknown_field/)
}
})
})

describe('UIDSchema', () => {
describe('valid UIDs', () => {
test.each(validUIDTestCases)('accepts %s (%s)', (uid) => {
Expand Down
14 changes: 8 additions & 6 deletions packages/app/src/cli/models/extensions/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,12 +124,14 @@ export const BaseSchemaWithoutHandle = BaseSchema.omit({
handle: true,
})

export const UnifiedSchema = zod.object({
api_version: ApiVersionSchema.optional(),
description: zod.string().optional(),
extensions: zod.array(zod.any()),
settings: SettingsSchema.optional(),
})
export const UnifiedSchema = zod
.object({
api_version: ApiVersionSchema.optional(),
description: zod.string().optional(),
extensions: zod.array(zod.any()),
settings: SettingsSchema.optional(),
})
.strict()

export type NewExtensionPointSchemaType = zod.infer<typeof NewExtensionPointSchema>

Expand Down
Loading