diff --git a/.changeset/quick-functions-info.md b/.changeset/quick-functions-info.md new file mode 100644 index 00000000000..736088afcc1 --- /dev/null +++ b/.changeset/quick-functions-info.md @@ -0,0 +1,5 @@ +--- +'@shopify/app': minor +--- + +Document and validate the `app function info --json` result contract. diff --git a/packages/app/src/cli/commands/app/function/info.test.ts b/packages/app/src/cli/commands/app/function/info.test.ts new file mode 100644 index 00000000000..a5df507f919 --- /dev/null +++ b/packages/app/src/cli/commands/app/function/info.test.ts @@ -0,0 +1,11 @@ +import FunctionInfo from './info.js' +import {describe, expect, test} from 'vitest' + +describe('FunctionInfo', () => { + test('includes the JSON result type in its help description', () => { + expect(FunctionInfo.descriptionWithMarkdown).toContain( + 'With `--json`, the command returns `FunctionInfoResult`, described by these TypeScript types:', + ) + expect(FunctionInfo.descriptionWithMarkdown).toContain('targeting: Record') + }) +}) diff --git a/packages/app/src/cli/commands/app/function/info.ts b/packages/app/src/cli/commands/app/function/info.ts index 345c0790f26..82e5070b77c 100644 --- a/packages/app/src/cli/commands/app/function/info.ts +++ b/packages/app/src/cli/commands/app/function/info.ts @@ -1,16 +1,20 @@ import {chooseFunction, functionFlags, getOrGenerateSchemaPath} from '../../../services/function/common.js' import {functionRunnerBinary, downloadBinary} from '../../../services/function/binaries.js' import {functionInfo} from '../../../services/function/info.js' +import {presentFunctionInfoResult} from '../../../services/function/info-result.js' +import {functionInfoJsonOutputSchema} from '../../../services/function/info-types.js' import {localAppContext} from '../../../services/app-context.js' import {appFlags} from '../../../flags.js' import AppUnlinkedCommand, {AppUnlinkedCommandOutput} from '../../../utilities/app-unlinked-command.js' import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli' -import {outputResult} from '@shopify/cli-kit/node/output' -import {AlertCustomSection, renderInfo} from '@shopify/cli-kit/node/ui' export default class FunctionInfo extends AppUnlinkedCommand { static summary = 'Print basic information about your function.' + static get jsonOutputSchema() { + return functionInfoJsonOutputSchema + } + static descriptionWithMarkdown = `The information returned includes the following: - The function handle @@ -51,18 +55,11 @@ export default class FunctionInfo extends AppUnlinkedCommand { ) const result = functionInfo(ourFunction, { - format: flags.json ? 'json' : 'text', functionRunnerPath: functionRunner.path, schemaPath, }) - if (flags.json) { - outputResult(result as string) - } else { - renderInfo({ - customSections: result as AlertCustomSection[], - }) - } + presentFunctionInfoResult(result, flags.json ? 'json' : 'text') return {app} } diff --git a/packages/app/src/cli/services/function/info-result.test.ts b/packages/app/src/cli/services/function/info-result.test.ts new file mode 100644 index 00000000000..cd1ade9969e --- /dev/null +++ b/packages/app/src/cli/services/function/info-result.test.ts @@ -0,0 +1,81 @@ +import { + buildBuildSection, + buildConfigurationSection, + buildTargetingSection, + buildTextFormatSections, + encodeFunctionInfoJson, +} from './info-result.js' +import {type FunctionInfoResult} from './info-types.js' +import {describe, expect, test} from 'vitest' + +const result: FunctionInfoResult = { + handle: 'my-function', + name: 'My Function', + apiVersion: '2024-01', + targeting: { + 'purchase.payment-customization.run': { + inputQueryPath: '/path/to/function/query.graphql', + export: 'run', + }, + }, + schemaPath: '/path/to/schema.graphql', + wasmPath: '/path/to/function.wasm', + functionRunnerPath: '/path/to/runner', +} + +describe('function info result presentation', () => { + test('encodes a JSON document that matches the declared schema', () => { + expect(JSON.parse(encodeFunctionInfoJson(result))).toEqual(result) + }) + + test('omits absent optional fields from the JSON document', () => { + const requiredResult: FunctionInfoResult = { + name: result.name, + targeting: result.targeting, + wasmPath: result.wasmPath, + functionRunnerPath: result.functionRunnerPath, + } + + expect(JSON.parse(encodeFunctionInfoJson(requiredResult))).toEqual(requiredResult) + }) + + test('builds configuration rows from the result', () => { + expect(buildConfigurationSection(result).body).toMatchObject({ + tabularData: [ + ['Handle', 'my-function'], + ['Name', 'My Function'], + ['API Version', '2024-01'], + ], + firstColumnSubdued: true, + }) + }) + + test('builds targeting rows from the result', () => { + const section = buildTargetingSection(result.targeting) + + expect(section?.title).toBe('\nTARGETING\n') + expect((section?.body as {tabularData: unknown[][]}).tabularData).toHaveLength(3) + }) + + test('omits the targeting section when there are no targets', () => { + expect(buildTargetingSection({})).toBeUndefined() + }) + + test('builds path rows from the result', () => { + expect(buildBuildSection(result).body).toMatchObject({ + tabularData: [ + ['Schema Path', {filePath: '/path/to/schema.graphql'}], + ['Wasm Path', {filePath: '/path/to/function.wasm'}], + ], + }) + }) + + test('builds every text section', () => { + expect(buildTextFormatSections(result).map((section) => section.title)).toEqual([ + 'CONFIGURATION\n', + '\nTARGETING\n', + '\nBUILD\n', + '\nFUNCTION RUNNER\n', + ]) + }) +}) diff --git a/packages/app/src/cli/services/function/info-result.ts b/packages/app/src/cli/services/function/info-result.ts new file mode 100644 index 00000000000..7e337dda61d --- /dev/null +++ b/packages/app/src/cli/services/function/info-result.ts @@ -0,0 +1,85 @@ +import {functionInfoJsonOutputSchema, type FunctionInfoResult, type FunctionTargeting} from './info-types.js' +import {outputContent, outputResult, outputToken} from '@shopify/cli-kit/node/output' +import {renderInfo, type AlertCustomSection, type InlineToken} from '@shopify/cli-kit/node/ui' + +type FunctionInfoOutputFormat = 'json' | 'text' + +export function presentFunctionInfoResult(result: FunctionInfoResult, format: FunctionInfoOutputFormat): void { + if (format === 'json') { + outputResult(encodeFunctionInfoJson(result)) + return + } + + renderInfo({customSections: buildTextFormatSections(result)}) +} + +export function encodeFunctionInfoJson(result: FunctionInfoResult): string { + return JSON.stringify(functionInfoJsonOutputSchema.schema.parse(result), null, 2) +} + +export function buildConfigurationSection(result: FunctionInfoResult): AlertCustomSection { + return { + title: 'CONFIGURATION\n', + body: { + tabularData: [ + ['Handle', result.handle ?? 'N/A'], + ['Name', result.name], + ['API Version', result.apiVersion ?? 'N/A'], + ], + firstColumnSubdued: true, + }, + } +} + +export function buildTargetingSection(targeting: Record): AlertCustomSection | undefined { + if (Object.keys(targeting).length === 0) return undefined + + const targetingData: InlineToken[][] = [] + Object.entries(targeting).forEach(([target, config]) => { + targetingData.push([outputContent`${outputToken.cyan(target)}`.value, '']) + if (config.inputQueryPath) { + targetingData.push([{subdued: ' Input Query Path'}, {filePath: config.inputQueryPath}]) + } + if (config.export) { + targetingData.push([{subdued: ' Export'}, config.export]) + } + }) + + return { + title: '\nTARGETING\n', + body: {tabularData: targetingData}, + } +} + +export function buildBuildSection(result: FunctionInfoResult): AlertCustomSection { + return { + title: '\nBUILD\n', + body: { + tabularData: [ + ['Schema Path', {filePath: result.schemaPath ?? 'N/A'}], + ['Wasm Path', {filePath: result.wasmPath}], + ], + firstColumnSubdued: true, + }, + } +} + +function buildFunctionRunnerSection(functionRunnerPath: string): AlertCustomSection { + return { + title: '\nFUNCTION RUNNER\n', + body: { + tabularData: [['Path', {filePath: functionRunnerPath}]], + firstColumnSubdued: true, + }, + } +} + +export function buildTextFormatSections(result: FunctionInfoResult): AlertCustomSection[] { + const sections = [buildConfigurationSection(result)] + const targetingSection = buildTargetingSection(result.targeting) + + if (targetingSection) sections.push(targetingSection) + + sections.push(buildBuildSection(result), buildFunctionRunnerSection(result.functionRunnerPath)) + return sections +} diff --git a/packages/app/src/cli/services/function/info-types.test.ts b/packages/app/src/cli/services/function/info-types.test.ts new file mode 100644 index 00000000000..96ae609a4de --- /dev/null +++ b/packages/app/src/cli/services/function/info-types.test.ts @@ -0,0 +1,38 @@ +import {functionInfoJsonOutputSchema} from './info-types.js' +import {renderJsonOutputSchema} from '@shopify/cli-kit/node/json-output-schema' +import {describe, expect, test} from 'vitest' + +describe('functionInfoJsonOutputSchema', () => { + test('accepts the function info JSON result', () => { + expect( + functionInfoJsonOutputSchema.schema.safeParse({ + name: 'My Function', + targeting: { + 'purchase.payment-customization.run': { + inputQueryPath: '/path/to/query.graphql', + export: 'run', + }, + }, + wasmPath: '/path/to/function.wasm', + functionRunnerPath: '/path/to/runner', + }).success, + ).toBe(true) + }) + + test('renders the named result and targeting types', () => { + expect(renderJsonOutputSchema(functionInfoJsonOutputSchema)).toBe(`interface FunctionInfoResult { + handle?: string + name: string + apiVersion?: string + targeting: Record + schemaPath?: string + wasmPath: string + functionRunnerPath: string +} + +interface FunctionTargeting { + inputQueryPath?: string + export?: string +}`) + }) +}) diff --git a/packages/app/src/cli/services/function/info-types.ts b/packages/app/src/cli/services/function/info-types.ts new file mode 100644 index 00000000000..361b3628761 --- /dev/null +++ b/packages/app/src/cli/services/function/info-types.ts @@ -0,0 +1,28 @@ +import {defineJsonOutputSchema, type InferJsonOutputSchema} from '@shopify/cli-kit/node/json-output-schema' +import {zod} from '@shopify/cli-kit/node/schema' + +export const FunctionTargetingSchema = zod.object({ + inputQueryPath: zod.string().optional(), + export: zod.string().optional(), +}) + +const FunctionInfoResultSchema = zod.object({ + handle: zod.string().optional(), + name: zod.string(), + apiVersion: zod.string().optional(), + targeting: zod.record(FunctionTargetingSchema), + schemaPath: zod.string().optional(), + wasmPath: zod.string(), + functionRunnerPath: zod.string(), +}) + +export const functionInfoJsonOutputSchema = defineJsonOutputSchema({ + name: 'FunctionInfoResult', + schema: FunctionInfoResultSchema, + definitions: { + FunctionTargeting: FunctionTargetingSchema, + }, +}) + +export type FunctionTargeting = zod.infer +export type FunctionInfoResult = InferJsonOutputSchema diff --git a/packages/app/src/cli/services/function/info.test.ts b/packages/app/src/cli/services/function/info.test.ts index 1bfd70a2392..5b8270d682d 100644 --- a/packages/app/src/cli/services/function/info.test.ts +++ b/packages/app/src/cli/services/function/info.test.ts @@ -1,23 +1,10 @@ -import { - functionInfo, - buildTargetingData, - formatAsJson, - buildConfigurationSection, - buildTargetingSection, - buildBuildSection, - buildFunctionRunnerSection, - buildTextFormatSections, -} from './info.js' +import {buildTargetingData, functionInfo} from './info.js' import {testFunctionExtension} from '../../models/app/app.test-data.js' -import {ExtensionInstance} from '../../models/extensions/extension-instance.js' -import {describe, expect, test, beforeEach} from 'vitest' -import {AlertCustomSection} from '@shopify/cli-kit/node/ui' +import {describe, expect, test} from 'vitest' describe('functionInfo', () => { - let ourFunction: ExtensionInstance - - beforeEach(async () => { - ourFunction = await testFunctionExtension({ + test('returns the function information as a typed result', async () => { + const extension = await testFunctionExtension({ dir: '/path/to/function', config: { name: 'My Function', @@ -27,411 +14,77 @@ describe('functionInfo', () => { configuration_ui: false, }, }) - }) - - describe('functionInfo integration', () => { - test('returns JSON string when format is json', async () => { - // Given - const options = { - format: 'json' as const, - functionRunnerPath: '/path/to/runner', - schemaPath: '/path/to/schema.graphql', - } - - // When - const result = functionInfo(ourFunction, options) - // Then - expect(typeof result).toBe('string') - const parsed = JSON.parse(result as string) - expect(parsed).toHaveProperty('handle') - expect(parsed).toHaveProperty('name') - expect(parsed).toHaveProperty('apiVersion') + const result = functionInfo(extension, { + functionRunnerPath: '/path/to/runner', + schemaPath: '/path/to/schema.graphql', }) - test('uses build.path from config for wasmPath when present', async () => { - // Given - const funcWithBuildPath = await testFunctionExtension({ - dir: '/path/to/function', - config: { - name: 'My Function', - type: 'function', - handle: 'my-function', - api_version: '2024-01', - configuration_ui: false, - build: { - path: 'custom/output.wasm', - wasm_opt: false, - }, - }, - }) - const options = { - format: 'json' as const, - functionRunnerPath: '/path/to/runner', - schemaPath: '/path/to/schema.graphql', - } - - // When - const result = functionInfo(funcWithBuildPath, options) - - // Then - const parsed = JSON.parse(result as string) - expect(parsed.wasmPath).toBe('/path/to/function/custom/output.wasm') - }) - - test('falls back to outputRelativePath when build.path is not set', async () => { - // Given - const options = { - format: 'json' as const, - functionRunnerPath: '/path/to/runner', - schemaPath: '/path/to/schema.graphql', - } - - // When - const result = functionInfo(ourFunction, options) - - // Then - const parsed = JSON.parse(result as string) - expect(parsed.wasmPath).toBe(ourFunction.outputPath) - }) - - test('returns AlertCustomSection array when format is text', async () => { - // Given - const options = { - format: 'text' as const, - functionRunnerPath: '/path/to/runner', - schemaPath: '/path/to/schema.graphql', - } - - // When - const result = functionInfo(ourFunction, options) as AlertCustomSection[] - - // Then - expect(Array.isArray(result)).toBe(true) - expect(result.length).toBeGreaterThan(0) + expect(result).toEqual({ + handle: 'my-function', + name: 'My Function', + apiVersion: '2024-01', + targeting: {}, + schemaPath: '/path/to/schema.graphql', + wasmPath: extension.outputPath, + functionRunnerPath: '/path/to/runner', }) }) - describe('buildTargetingData', () => { - test('transforms targeting configuration with multiple targets', () => { - // Given - const config = { - handle: 'test', - targeting: [ - { - target: 'purchase.payment-customization.run', - input_query: 'query1.graphql', - export: 'run', - }, - { - target: 'purchase.checkout.delivery-customization.run', - input_query: 'query2.graphql', - export: 'customize', - }, - ], - } - - // When - const result = buildTargetingData(config, '/path/to/function') - - // Then - expect(result).toEqual({ - 'purchase.payment-customization.run': { - inputQueryPath: '/path/to/function/query1.graphql', - export: 'run', - }, - 'purchase.checkout.delivery-customization.run': { - inputQueryPath: '/path/to/function/query2.graphql', - export: 'customize', - }, - }) + test('uses build.path for the WASM path when present', async () => { + const extension = await testFunctionExtension({ + dir: '/path/to/function', + config: { + name: 'My Function', + type: 'function', + handle: 'my-function', + api_version: '2024-01', + configuration_ui: false, + build: {path: 'custom/output.wasm', wasm_opt: false}, + }, }) - test('handles targets without input_query', () => { - // Given - const config = { - handle: 'test', - targeting: [ - { - target: 'purchase.payment-customization.run', - export: 'run', - }, - ], - } - - // When - const result = buildTargetingData(config, '/path/to/function') + const result = functionInfo(extension, {functionRunnerPath: '/path/to/runner'}) - // Then - expect(result).toEqual({ - 'purchase.payment-customization.run': { - export: 'run', - }, - }) - }) + expect(result.wasmPath).toBe('/path/to/function/custom/output.wasm') + }) +}) - test('handles targets without export', () => { - // Given - const config = { - handle: 'test', +describe('buildTargetingData', () => { + test('maps targeting paths relative to the function directory', () => { + const result = buildTargetingData( + { targeting: [ { target: 'purchase.payment-customization.run', input_query: 'query.graphql', - }, - ], - } - - // When - const result = buildTargetingData(config, '/path/to/function') - - // Then - expect(result).toEqual({ - 'purchase.payment-customization.run': { - inputQueryPath: '/path/to/function/query.graphql', - }, - }) - }) - }) - - describe('formatAsJson', () => { - test('returns correctly formatted JSON string', async () => { - // Given - const testFunc = await testFunctionExtension({ - dir: '/path/to/function', - config: { - name: 'My Function', - type: 'function', - handle: 'my-function', - api_version: '2024-01', - configuration_ui: false, - }, - }) - const config = { - handle: 'my-function', - name: 'My Function', - api_version: '2024-01', - } - const targeting = { - 'purchase.payment-customization.run': { - inputQueryPath: '/path/to/function/query.graphql', - export: 'run', - }, - } - - // When - const functionOutputPath = '/path/to/function/output.wasm' - const result = formatAsJson( - testFunc, - config, - targeting, - '/path/to/runner', - functionOutputPath, - '/path/to/schema.graphql', - ) - - // Then - const parsed = JSON.parse(result) - expect(parsed).toEqual({ - handle: 'my-function', - name: 'My Function', - apiVersion: '2024-01', - targeting: { - 'purchase.payment-customization.run': { - inputQueryPath: '/path/to/function/query.graphql', export: 'run', }, - }, - schemaPath: '/path/to/schema.graphql', - wasmPath: functionOutputPath, - functionRunnerPath: '/path/to/runner', - }) - }) - - test('handles missing optional fields', async () => { - // Given - const testFunc = await testFunctionExtension({ - dir: '/path/to/function', - config: { - name: 'My Function', - type: 'function', - api_version: '2024-01', - configuration_ui: false, - }, - }) - const config = {} - const targeting = {} - - // When - const result = formatAsJson(testFunc, config, targeting, '/path/to/runner', 'path/to/function.wasm', undefined) - - // Then - const parsed = JSON.parse(result) - expect(parsed.handle).toBeUndefined() - expect(parsed.schemaPath).toBeUndefined() - expect(parsed.targeting).toEqual({}) - }) - }) - - describe('buildConfigurationSection', () => { - test('builds configuration section with all fields', () => { - // Given - const config = { - handle: 'my-function', - name: 'My Function', - api_version: '2024-01', - } - - // When - const result = buildConfigurationSection(config, 'My Function') - - // Then - expect(result.title).toBe('CONFIGURATION\n') - expect(result.body).toHaveProperty('tabularData') - expect(result.body).toHaveProperty('firstColumnSubdued', true) - expect((result.body as {tabularData: unknown[][]}).tabularData).toEqual([ - ['Handle', 'my-function'], - ['Name', 'My Function'], - ['API Version', '2024-01'], - ]) - }) - - test('uses N/A for missing fields', () => { - // Given - const config = {} - - // When - const result = buildConfigurationSection(config, undefined as unknown as string) - - // Then - expect((result.body as {tabularData: unknown[][]}).tabularData).toEqual([ - ['Handle', 'N/A'], - ['Name', 'N/A'], - ['API Version', 'N/A'], - ]) - }) - }) - - describe('buildTargetingSection', () => { - test('builds targeting section with multiple targets', () => { - // Given - const targeting = { - 'purchase.payment-customization.run': { - inputQueryPath: '/path/to/function/query1.graphql', - export: 'run', - }, - 'purchase.checkout.delivery-customization.run': { - inputQueryPath: '/path/to/function/query2.graphql', - export: 'customize', - }, - } - - // When - const result = buildTargetingSection(targeting) - - // Then - expect(result).not.toBeNull() - expect(result?.title).toBe('\nTARGETING\n') - const tabularData = (result?.body as {tabularData: unknown[][]})?.tabularData - // 2 targets × 3 rows each - expect(tabularData?.length).toBe(6) - }) - }) - - describe('buildBuildSection', () => { - test('builds build section with schema and wasm paths', () => { - // Given - const wasmPath = '/path/to/function.wasm' - const schemaPath = '/path/to/schema.graphql' - - // When - const result = buildBuildSection(wasmPath, schemaPath) - - // Then - expect(result.title).toBe('\nBUILD\n') - expect(result.body).toHaveProperty('tabularData') - expect(result.body).toHaveProperty('firstColumnSubdued', true) - expect((result.body as {tabularData: unknown[][]}).tabularData).toEqual([ - ['Schema Path', {filePath: schemaPath}], - ['Wasm Path', {filePath: wasmPath}], - ]) - }) - - test('uses N/A for missing schema path', () => { - // Given - const wasmPath = '/path/to/function.wasm' - - // When - const result = buildBuildSection(wasmPath) - - // Then - expect((result.body as {tabularData: unknown[][]}).tabularData).toEqual([ - ['Schema Path', {filePath: 'N/A'}], - ['Wasm Path', {filePath: wasmPath}], - ]) - }) - }) - - describe('buildFunctionRunnerSection', () => { - test('builds function runner section', () => { - // Given - const functionRunnerPath = '/path/to/runner' - - // When - const result = buildFunctionRunnerSection(functionRunnerPath) + ], + }, + '/path/to/function', + ) - // Then - expect(result.title).toBe('\nFUNCTION RUNNER\n') - expect(result.body).toHaveProperty('tabularData') - expect(result.body).toHaveProperty('firstColumnSubdued', true) - expect((result.body as {tabularData: unknown[][]}).tabularData).toEqual([ - ['Path', {filePath: functionRunnerPath}], - ]) + expect(result).toEqual({ + 'purchase.payment-customization.run': { + inputQueryPath: '/path/to/function/query.graphql', + export: 'run', + }, }) }) - describe('buildTextFormatSections', () => { - test('includes all sections when targeting is present', async () => { - // Given - const testFunc = await testFunctionExtension({ - dir: '/path/to/function', - config: { - name: 'My Function', - type: 'function', - handle: 'my-function', - api_version: '2024-01', - configuration_ui: false, - }, - }) - const config = { - handle: 'my-function', - name: 'My Function', - api_version: '2024-01', - } - const targeting = { - 'purchase.payment-customization.run': { - inputQueryPath: '/path/to/function/query.graphql', - export: 'run', - }, - } - - // When - const result = buildTextFormatSections( - testFunc, - config, - targeting, - '/path/to/runner', - '/path/to/function/output.wasm', - '/path/to/schema.graphql', - ) - - // Then - // configuration, targeting, build, function runner - expect(result.length).toBe(4) - expect(result[0]?.title).toContain('CONFIGURATION') - expect(result[1]?.title).toContain('TARGETING') - expect(result[2]?.title).toContain('BUILD') - expect(result[3]?.title).toContain('FUNCTION RUNNER') + test.each([ + { + target: {target: 'purchase.payment-customization.run', export: 'run'}, + expected: {export: 'run'}, + }, + { + target: {target: 'purchase.payment-customization.run', input_query: 'query.graphql'}, + expected: {inputQueryPath: '/path/to/function/query.graphql'}, + }, + ])('omits targeting fields that are not configured', ({target, expected}) => { + expect(buildTargetingData({targeting: [target]}, '/path/to/function')).toEqual({ + 'purchase.payment-customization.run': expected, }) }) }) diff --git a/packages/app/src/cli/services/function/info.ts b/packages/app/src/cli/services/function/info.ts index 5c1261adefd..d146c57669c 100644 --- a/packages/app/src/cli/services/function/info.ts +++ b/packages/app/src/cli/services/function/info.ts @@ -1,12 +1,8 @@ +import {type FunctionInfoResult, type FunctionTargeting} from './info-types.js' import {ExtensionInstance} from '../../models/extensions/extension-instance.js' -import {outputContent, outputToken} from '@shopify/cli-kit/node/output' import {joinPath} from '@shopify/cli-kit/node/path' -import {InlineToken, AlertCustomSection} from '@shopify/cli-kit/node/ui' - -type Format = 'json' | 'text' interface FunctionInfoOptions { - format: Format functionRunnerPath: string schemaPath?: string } @@ -28,8 +24,8 @@ interface FunctionConfiguration { export function buildTargetingData( config: FunctionConfiguration, functionDirectory: string, -): {[key: string]: {inputQueryPath?: string; export?: string}} { - const targeting: {[key: string]: {inputQueryPath?: string; export?: string}} = {} +): Record { + const targeting: Record = {} config.targeting?.forEach((target) => { if (target.target) { targeting[target.target] = { @@ -41,126 +37,19 @@ export function buildTargetingData( return targeting } -export function formatAsJson( - ourFunction: ExtensionInstance, - config: FunctionConfiguration, - targeting: {[key: string]: {inputQueryPath?: string; export?: string}}, - functionRunnerPath: string, - functionOutputPath: string, - schemaPath?: string, -): string { - return JSON.stringify( - { - handle: config.handle, - name: ourFunction.name, - apiVersion: config.api_version, - targeting, - schemaPath, - wasmPath: functionOutputPath, - functionRunnerPath, - }, - null, - 2, - ) -} - -export function buildConfigurationSection(config: FunctionConfiguration, functionName: string): AlertCustomSection { - return { - title: 'CONFIGURATION\n', - body: { - tabularData: [ - ['Handle', config.handle ?? 'N/A'], - ['Name', functionName ?? 'N/A'], - ['API Version', config.api_version ?? 'N/A'], - ], - firstColumnSubdued: true, - }, - } -} - -export function buildTargetingSection(targeting: { - [key: string]: {inputQueryPath?: string; export?: string} -}): AlertCustomSection | null { - if (Object.keys(targeting).length === 0) { - return null - } - - const targetingData: InlineToken[][] = [] - Object.entries(targeting).forEach(([target, config]) => { - targetingData.push([outputContent`${outputToken.cyan(target)}`.value, '']) - if (config.inputQueryPath) { - targetingData.push([{subdued: ' Input Query Path'}, {filePath: config.inputQueryPath}]) - } - if (config.export) { - targetingData.push([{subdued: ' Export'}, config.export]) - } - }) - - return { - title: '\nTARGETING\n', - body: { - tabularData: targetingData, - }, - } -} - -export function buildBuildSection(wasmPath: string, schemaPath?: string): AlertCustomSection { - return { - title: '\nBUILD\n', - body: { - tabularData: [ - ['Schema Path', {filePath: schemaPath ?? 'N/A'}], - ['Wasm Path', {filePath: wasmPath}], - ], - firstColumnSubdued: true, - }, - } -} - -export function buildFunctionRunnerSection(functionRunnerPath: string): AlertCustomSection { - return { - title: '\nFUNCTION RUNNER\n', - body: { - tabularData: [['Path', {filePath: functionRunnerPath}]], - firstColumnSubdued: true, - }, - } -} - -export function buildTextFormatSections( - ourFunction: ExtensionInstance, - config: FunctionConfiguration, - targeting: {[key: string]: {inputQueryPath?: string; export?: string}}, - functionRunnerPath: string, - functionOutputPath: string, - schemaPath?: string, -): AlertCustomSection[] { - const sections: AlertCustomSection[] = [buildConfigurationSection(config, ourFunction.name)] - - const targetingSection = buildTargetingSection(targeting) - if (targetingSection) { - sections.push(targetingSection) - } - - sections.push(buildBuildSection(functionOutputPath, schemaPath), buildFunctionRunnerSection(functionRunnerPath)) - - return sections -} - -export function functionInfo( - ourFunction: ExtensionInstance, - options: FunctionInfoOptions, -): string | AlertCustomSection[] { - const {format, functionRunnerPath, schemaPath} = options +export function functionInfo(ourFunction: ExtensionInstance, options: FunctionInfoOptions): FunctionInfoResult { + const {functionRunnerPath, schemaPath} = options const config = ourFunction.configuration as FunctionConfiguration - const targeting = buildTargetingData(config, ourFunction.directory) + const wasmPath = joinPath(ourFunction.directory, config.build?.path ?? ourFunction.outputRelativePath) - const functionOutputPath = joinPath(ourFunction.directory, config.build?.path ?? ourFunction.outputRelativePath) - - if (format === 'json') { - return formatAsJson(ourFunction, config, targeting, functionRunnerPath, functionOutputPath, schemaPath) + return { + handle: config.handle, + name: ourFunction.name, + apiVersion: config.api_version, + targeting, + schemaPath, + wasmPath, + functionRunnerPath, } - - return buildTextFormatSections(ourFunction, config, targeting, functionRunnerPath, functionOutputPath, schemaPath) } diff --git a/packages/cli/README.md b/packages/cli/README.md index 471d20727ab..e845ad62a85 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -1069,6 +1069,25 @@ DESCRIPTION - The schema path - The WASM path - The function runner path + + With `--json`, the command returns `FunctionInfoResult`, described by these TypeScript types: + + ```ts + interface FunctionInfoResult { + handle?: string + name: string + apiVersion?: string + targeting: Record + schemaPath?: string + wasmPath: string + functionRunnerPath: string + } + + interface FunctionTargeting { + inputQueryPath?: string + export?: string + } + ``` ``` ## `shopify app function replay` diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index 4a95d50b31f..59fdb6c7ffe 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -1729,8 +1729,8 @@ "args": { }, "customPluginName": "@shopify/app", - "description": "The information returned includes the following:\n\n - The function handle\n - The function name\n - The function API version\n - The targeting configuration\n - The schema path\n - The WASM path\n - The function runner path", - "descriptionWithMarkdown": "The information returned includes the following:\n\n - The function handle\n - The function name\n - The function API version\n - The targeting configuration\n - The schema path\n - The WASM path\n - The function runner path", + "description": "The information returned includes the following:\n\n - The function handle\n - The function name\n - The function API version\n - The targeting configuration\n - The schema path\n - The WASM path\n - The function runner path\n\nWith `--json`, the command returns `FunctionInfoResult`, described by these TypeScript types:\n\n```ts\ninterface FunctionInfoResult {\n handle?: string\n name: string\n apiVersion?: string\n targeting: Record\n schemaPath?: string\n wasmPath: string\n functionRunnerPath: string\n}\n\ninterface FunctionTargeting {\n inputQueryPath?: string\n export?: string\n}\n```", + "descriptionWithMarkdown": "The information returned includes the following:\n\n - The function handle\n - The function name\n - The function API version\n - The targeting configuration\n - The schema path\n - The WASM path\n - The function runner path\n\nWith `--json`, the command returns `FunctionInfoResult`, described by these TypeScript types:\n\n```ts\ninterface FunctionInfoResult {\n handle?: string\n name: string\n apiVersion?: string\n targeting: Record\n schemaPath?: string\n wasmPath: string\n functionRunnerPath: string\n}\n\ninterface FunctionTargeting {\n inputQueryPath?: string\n export?: string\n}\n```", "flags": { "auth-alias": { "description": "Alias of the Shopify account to use for authentication.",