diff --git a/.changeset/app-dev-clean-json.md b/.changeset/app-dev-clean-json.md new file mode 100644 index 00000000000..d07284f25f5 --- /dev/null +++ b/.changeset/app-dev-clean-json.md @@ -0,0 +1,5 @@ +--- +'@shopify/cli': minor +--- + +Add typed JSON output to `app dev clean`. diff --git a/bin/prettify-manifests.js b/bin/prettify-manifests.js index 2363b227819..5ce134edb21 100755 --- a/bin/prettify-manifests.js +++ b/bin/prettify-manifests.js @@ -13,6 +13,11 @@ const manifestFiles = glob.sync(`packages/*/oclif.manifest.json`) for (const file of manifestFiles) { console.log(`Prettifying ${file}...`) const content = fs.readFileSync(file) - const prettyContent = stringify(JSON.parse(content), {space: ' '}).replaceAll(root, '.') + const manifest = JSON.parse(content) + for (const command of Object.values(manifest.commands)) { + // Runtime schemas belong to command classes, not cached metadata. + delete command.jsonOutputSchema + } + const prettyContent = stringify(manifest, {space: ' '}).replaceAll(root, '.') fs.writeFileSync(file, prettyContent) } diff --git a/docs-shopify.dev/generated/generated_docs_data_v2.json b/docs-shopify.dev/generated/generated_docs_data_v2.json index af6f199146d..c7249ce0e1a 100644 --- a/docs-shopify.dev/generated/generated_docs_data_v2.json +++ b/docs-shopify.dev/generated/generated_docs_data_v2.json @@ -1112,6 +1112,15 @@ "isOptional": true, "environmentValue": "SHOPIFY_FLAG_APP_CONFIG" }, + { + "filePath": "docs-shopify.dev/commands/interfaces/app-dev-clean.interface.ts", + "syntaxKind": "PropertySignature", + "name": "-j, --json", + "value": "''", + "description": "Output the result as JSON. Automatically disables color output.", + "isOptional": true, + "environmentValue": "SHOPIFY_FLAG_JSON" + }, { "filePath": "docs-shopify.dev/commands/interfaces/app-dev-clean.interface.ts", "syntaxKind": "PropertySignature", @@ -1122,7 +1131,7 @@ "environmentValue": "SHOPIFY_FLAG_STORE" } ], - "value": "export interface appdevclean {\n /**\n * Alias of the Shopify account to use for authentication.\n * @environment SHOPIFY_FLAG_AUTH_ALIAS\n */\n '--auth-alias '?: string\n\n /**\n * The Client ID of your app. Use with --config to clean up the dev preview for a different app than the one it is linked to.\n * @environment SHOPIFY_FLAG_CLIENT_ID\n */\n '--client-id '?: string\n\n /**\n * The name of the app configuration.\n * @environment SHOPIFY_FLAG_APP_CONFIG\n */\n '-c, --config '?: string\n\n /**\n * Print the command's JSON schemas.\n * @environment SHOPIFY_FLAG_JSON_SCHEMA\n */\n '--json-schema'?: ''\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * Disable interactive prompts and browser authentication.\n * @environment SHOPIFY_FLAG_NO_INPUT\n */\n '--no-input'?: ''\n\n /**\n * The path to your app directory.\n * @environment SHOPIFY_FLAG_PATH\n */\n '--path '?: string\n\n /**\n * Reset all your settings.\n * @environment SHOPIFY_FLAG_RESET\n */\n '--reset'?: ''\n\n /**\n * Store URL. Must be an existing development store.\n * @environment SHOPIFY_FLAG_STORE\n */\n '-s, --store '?: string\n\n /**\n * Increase the verbosity of the output. May include sensitive data.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}" + "value": "export interface appdevclean {\n /**\n * Alias of the Shopify account to use for authentication.\n * @environment SHOPIFY_FLAG_AUTH_ALIAS\n */\n '--auth-alias '?: string\n\n /**\n * The Client ID of your app. Use with --config to clean up the dev preview for a different app than the one it is linked to.\n * @environment SHOPIFY_FLAG_CLIENT_ID\n */\n '--client-id '?: string\n\n /**\n * The name of the app configuration.\n * @environment SHOPIFY_FLAG_APP_CONFIG\n */\n '-c, --config '?: string\n\n /**\n * Output the result as JSON. Automatically disables color output.\n * @environment SHOPIFY_FLAG_JSON\n */\n '-j, --json'?: ''\n\n /**\n * Print the command's JSON schemas.\n * @environment SHOPIFY_FLAG_JSON_SCHEMA\n */\n '--json-schema'?: ''\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * Disable interactive prompts and browser authentication.\n * @environment SHOPIFY_FLAG_NO_INPUT\n */\n '--no-input'?: ''\n\n /**\n * The path to your app directory.\n * @environment SHOPIFY_FLAG_PATH\n */\n '--path '?: string\n\n /**\n * Reset all your settings.\n * @environment SHOPIFY_FLAG_RESET\n */\n '--reset'?: ''\n\n /**\n * Store URL. Must be an existing development store.\n * @environment SHOPIFY_FLAG_STORE\n */\n '-s, --store '?: string\n\n /**\n * Increase the verbosity of the output. May include sensitive data.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}" } }, "appdev": { diff --git a/packages/app/src/cli/commands/app/dev.test.ts b/packages/app/src/cli/commands/app/dev.test.ts index f14870d69fa..098e329f753 100644 --- a/packages/app/src/cli/commands/app/dev.test.ts +++ b/packages/app/src/cli/commands/app/dev.test.ts @@ -50,6 +50,11 @@ describe('app dev command', () => { vi.mocked(linkedAppContext).mockResolvedValue(appContextResult) vi.mocked(storeContext).mockResolvedValue(store) + vi.mocked(devClean).mockResolvedValue({ + status: 'success', + app: {name: appContextResult.remoteApp.title, clientId: appContextResult.remoteApp.apiKey}, + storeHostname: store.shopDomain, + }) return {store} } diff --git a/packages/app/src/cli/commands/app/dev/clean.test.ts b/packages/app/src/cli/commands/app/dev/clean.test.ts new file mode 100644 index 00000000000..ccab2fa1723 --- /dev/null +++ b/packages/app/src/cli/commands/app/dev/clean.test.ts @@ -0,0 +1,69 @@ +import DevClean from './clean.js' +import {linkedAppContext} from '../../../services/app-context.js' +import {storeContext} from '../../../services/store-context.js' +import { + testAppLinked, + testDeveloperPlatformClient, + testOrganization, + testOrganizationApp, + testOrganizationStore, + testProject, +} from '../../../models/app/app.test-data.js' +import {Config} from '@oclif/core' +import {afterEach, expect, test, vi} from 'vitest' +import {runWithCommandEventsForCommand} from '@shopify/cli-kit/node/command-events' +import {handler} from '@shopify/cli-kit/node/error' +import {outputInfo} from '@shopify/cli-kit/node/output' +import {withCapturedStandardStreams} from '@shopify/cli-kit/node/testing/output' + +vi.mock('../../../services/app-context.js') +vi.mock('../../../services/store-context.js') + +afterEach(() => vi.unstubAllEnvs()) + +async function runCommand(response: unknown = {devSessionDelete: {userErrors: []}}, diagnostic = false) { + const appContext = { + app: testAppLinked(), + remoteApp: testOrganizationApp({title: 'Test App', apiKey: 'public-client-id'}), + developerPlatformClient: testDeveloperPlatformClient({devSessionDelete: vi.fn().mockResolvedValue(response)}), + organization: testOrganization(), + specifications: [], + project: testProject(), + activeConfig: {} as never, + } + vi.mocked(linkedAppContext).mockImplementation(async () => { + if (diagnostic) outputInfo('Using the selected app.') + return appContext + }) + vi.mocked(storeContext).mockResolvedValue(testOrganizationStore({shopDomain: 'test-store.myshopify.com'})) + const command = new DevClean(['--json'], await Config.load()) + return runWithCommandEventsForCommand(['--json'], () => command.run()) +} + +test('writes one public JSON result with diagnostics on stderr', async () => { + await withCapturedStandardStreams(async ({stdout, stderr}) => { + await runCommand(undefined, true) + expect(JSON.parse(stdout())).toEqual({ + status: 'success', + app: {name: 'Test App', clientId: 'public-client-id'}, + storeDomain: 'test-store.myshopify.com', + }) + expect(JSON.parse(stderr())).toMatchObject({type: 'diagnostic', message: 'Using the selected app.'}) + }) +}) + +test('writes one fatal JSON document when deletion returns user errors', async () => { + vi.stubEnv('SHOPIFY_FLAG_JSON', '1') + const userErrors = [{message: 'Preview cannot be stopped', code: 'UPSTREAM_ERROR'}] + await withCapturedStandardStreams(async ({stdout, stderr}) => { + await runCommand({devSessionDelete: {userErrors}}).catch(handler) + expect(JSON.parse(stdout())).toEqual({ + error: { + type: 'abort', + message: 'Failed to stop the dev preview: Preview cannot be stopped', + details: {userErrors}, + }, + }) + expect(stderr()).toBe('') + }) +}) diff --git a/packages/app/src/cli/commands/app/dev/clean.ts b/packages/app/src/cli/commands/app/dev/clean.ts index c4c78c56772..e5054492159 100644 --- a/packages/app/src/cli/commands/app/dev/clean.ts +++ b/packages/app/src/cli/commands/app/dev/clean.ts @@ -3,7 +3,9 @@ import AppLinkedCommand, {AppLinkedCommandOutput} from '../../../utilities/app-l import {appFlags} from '../../../flags.js' import {storeContext} from '../../../services/store-context.js' import {devClean} from '../../../services/dev-clean.js' -import {globalFlags} from '@shopify/cli-kit/node/cli' +import {appDevCleanJsonOutputSchema} from '../../../services/dev-clean/types.js' +import {renderDevCleanResult} from '../../../services/dev-clean/result.js' +import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli' import {Flags} from '@oclif/core' import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn' @@ -15,11 +17,14 @@ export default class DevClean extends AppLinkedCommand { It restores the app's active version to the selected development store. ` + static jsonOutputSchema = appDevCleanJsonOutputSchema + static description = this.descriptionForHelp() static flags = { ...globalFlags, ...appFlags, + ...jsonFlag, 'client-id': Flags.string({ hidden: false, description: @@ -51,7 +56,8 @@ export default class DevClean extends AppLinkedCommand { forceReselectStore: flags.reset, }) - await devClean({appContextResult, store}) + const result = await devClean({appContextResult, store}) + renderDevCleanResult(result, flags.json ? 'json' : 'text') return {app: appContextResult.app} } diff --git a/packages/app/src/cli/services/dev-clean.test.ts b/packages/app/src/cli/services/dev-clean.test.ts index 3d8fe124c37..dcb319540e9 100644 --- a/packages/app/src/cli/services/dev-clean.test.ts +++ b/packages/app/src/cli/services/dev-clean.test.ts @@ -1,57 +1,42 @@ import {devClean} from './dev-clean.js' import {LoadedAppContextOutput} from './app-context.js' -import {testDeveloperPlatformClient, testOrganizationStore} from '../models/app/app.test-data.js' -import {renderSuccess} from '@shopify/cli-kit/node/ui' -import {describe, expect, test, vi} from 'vitest' - -vi.mock('@shopify/cli-kit/node/ui') - -const shopDomain = 'test-store.myshopify.com' -const mockStore = testOrganizationStore({shopDomain}) - -const mockOptions = { - appContextResult: { - developerPlatformClient: testDeveloperPlatformClient(), - remoteApp: {id: 'app-id-1', title: 'Test App', apiKey: 'api-key-1'}, - } as unknown as LoadedAppContextOutput, - store: mockStore, +import {testDeveloperPlatformClient, testOrganizationApp, testOrganizationStore} from '../models/app/app.test-data.js' +import {expect, test, vi} from 'vitest' + +function options(response: unknown = {devSessionDelete: {userErrors: []}}) { + const devSessionDelete = vi.fn().mockResolvedValue(response) + return { + appContextResult: { + developerPlatformClient: testDeveloperPlatformClient({devSessionDelete}), + remoteApp: testOrganizationApp({id: 'app-id-1', title: 'Test App', apiKey: 'public-client-id'}), + } as unknown as LoadedAppContextOutput, + store: testOrganizationStore({shopDomain: 'test-store.myshopify.com'}), + } } -describe('devClean', () => { - test('successfully stops dev preview and renders success message', async () => { - // Given - mockOptions.appContextResult.developerPlatformClient = customDevPlatformClient() - - // When - await devClean(mockOptions) - - // Then - expect(renderSuccess).toHaveBeenCalledWith({ - headline: 'Dev preview stopped.', - body: [ - `The dev preview has been stopped on ${mockStore.shopDomain} and the app's active version has been restored.`, - 'You can start it again with', - {command: 'shopify app dev'}, - ], - }) +test('returns public app and store data after stopping the dev preview', async () => { + const input = options() + await expect(devClean(input)).resolves.toEqual({ + status: 'success', + app: {name: 'Test App', clientId: 'public-client-id'}, + storeHostname: 'test-store.myshopify.com', }) +}) - test('throws AbortError when devSessionDelete returns user errors', async () => { - // Given - const errorMessage = 'Failed to stop dev preview' - mockOptions.appContextResult.developerPlatformClient = customDevPlatformClient(errorMessage) - - // When/Then - await expect(devClean(mockOptions)).rejects.toThrow(`Failed to stop the dev preview: ${errorMessage}`) +test('retains user error text and native details', async () => { + const userErrors = [{message: 'First error'}, {message: 'Second error', code: 'UPSTREAM_ERROR'}] + await expect(devClean(options({devSessionDelete: {userErrors}}))).rejects.toMatchObject({ + message: 'Failed to stop the dev preview: First error\nSecond error', + details: {userErrors}, }) }) -function customDevPlatformClient(devSessionDeleteError?: string) { - return testDeveloperPlatformClient({ - devSessionDelete: vi.fn().mockResolvedValue({ - devSessionDelete: { - userErrors: devSessionDeleteError ? [{message: devSessionDeleteError}] : [], - }, - }), - }) -} +test.each([null, {devSessionDelete: {userErrors: [{message: null}]}}])( + 'rejects a missing or malformed deletion response: %j', + async (response) => { + await expect(devClean(options(response))).rejects.toMatchObject({ + message: 'Failed to stop the dev preview: the server returned an invalid response.', + details: {data: response}, + }) + }, +) diff --git a/packages/app/src/cli/services/dev-clean.ts b/packages/app/src/cli/services/dev-clean.ts index ee3118b365e..018980ffc6a 100644 --- a/packages/app/src/cli/services/dev-clean.ts +++ b/packages/app/src/cli/services/dev-clean.ts @@ -1,6 +1,6 @@ import {LoadedAppContextOutput} from './app-context.js' +import {DevCleanResult} from './dev-clean/types.js' import {OrganizationStore} from '../models/organization.js' -import {renderSuccess} from '@shopify/cli-kit/node/ui' import {AbortError} from '@shopify/cli-kit/node/error' interface DevCleanOptions { @@ -8,23 +8,29 @@ interface DevCleanOptions { store: OrganizationStore } -export async function devClean(options: DevCleanOptions) { +export async function devClean(options: DevCleanOptions): Promise { const client = options.appContextResult.developerPlatformClient const remoteApp = options.appContextResult.remoteApp const result = await client.devSessionDelete({shopFqdn: options.store.shopDomain, appId: remoteApp.id}) - if (result.devSessionDelete?.userErrors.length) { - const errors = result.devSessionDelete.userErrors.map((error) => error.message).join('\n') - throw new AbortError(`Failed to stop the dev preview: ${errors}`) + const userErrors = result?.devSessionDelete?.userErrors + if (!Array.isArray(userErrors) || userErrors.some((error) => typeof error?.message !== 'string')) { + const error = new AbortError('Failed to stop the dev preview: the server returned an invalid response.') + error.details = {data: result} + throw error } - renderSuccess({ - headline: 'Dev preview stopped.', - body: [ - `The dev preview has been stopped on ${options.store.shopDomain} and the app's active version has been restored.`, - 'You can start it again with', - {command: 'shopify app dev'}, - ], - }) + if (userErrors.length) { + const errors = userErrors.map((error) => error.message).join('\n') + const error = new AbortError(`Failed to stop the dev preview: ${errors}`) + error.details = {userErrors} + throw error + } + + return { + status: 'success', + app: {name: remoteApp.title, clientId: remoteApp.apiKey}, + storeHostname: options.store.shopDomain, + } } diff --git a/packages/app/src/cli/services/dev-clean/result.test.ts b/packages/app/src/cli/services/dev-clean/result.test.ts new file mode 100644 index 00000000000..928696fd353 --- /dev/null +++ b/packages/app/src/cli/services/dev-clean/result.test.ts @@ -0,0 +1,44 @@ +import {renderDevCleanResult} from './result.js' +import {DevCleanResult} from './types.js' +import {renderSuccess} from '@shopify/cli-kit/node/ui' +import {withCapturedStandardStreams} from '@shopify/cli-kit/node/testing/output' +import {expect, test, vi} from 'vitest' + +vi.mock('@shopify/cli-kit/node/ui') + +test('keeps the dev preview success message', () => { + const result: DevCleanResult = { + status: 'success', + app: {name: 'Test App', clientId: 'public-client-id'}, + storeHostname: 'test-store.myshopify.com', + } + renderDevCleanResult(result, 'text') + expect(renderSuccess).toHaveBeenCalledExactlyOnceWith({ + headline: 'Dev preview stopped.', + body: [ + "The dev preview has been stopped on test-store.myshopify.com and the app's active version has been restored.", + 'You can start it again with', + {command: 'shopify app dev'}, + ], + }) +}) + +test('an unknown canonical store domain is null in JSON and retains its hostname in text', async () => { + const result: DevCleanResult = { + status: 'success', + app: {name: 'Test App', clientId: 'public-client-id'}, + storeHostname: 'test-store.my.shop.dev', + } + await withCapturedStandardStreams(async ({stdout}) => { + renderDevCleanResult(result, 'json') + expect(JSON.parse(stdout())).toMatchObject({storeDomain: null}) + }) + renderDevCleanResult(result, 'text') + expect(renderSuccess).toHaveBeenCalledWith( + expect.objectContaining({ + body: expect.arrayContaining([ + "The dev preview has been stopped on test-store.my.shop.dev and the app's active version has been restored.", + ]), + }), + ) +}) diff --git a/packages/app/src/cli/services/dev-clean/result.ts b/packages/app/src/cli/services/dev-clean/result.ts new file mode 100644 index 00000000000..bdbe8ca08df --- /dev/null +++ b/packages/app/src/cli/services/dev-clean/result.ts @@ -0,0 +1,25 @@ +import {appDevCleanJsonOutputSchema, AppDevCleanResult, DevCleanResult} from './types.js' +import {outputResult} from '@shopify/cli-kit/node/output' +import {renderSuccess} from '@shopify/cli-kit/node/ui' +import {extractMyshopifyHandle} from '@shopify/cli-kit/common/url' + +export function renderDevCleanResult(result: DevCleanResult, format: 'json' | 'text'): void { + if (format === 'json') { + const publicResult: AppDevCleanResult = { + status: result.status, + app: result.app, + storeDomain: extractMyshopifyHandle(result.storeHostname) ? result.storeHostname : null, + } + outputResult(appDevCleanJsonOutputSchema.encode(publicResult)) + return + } + + renderSuccess({ + headline: 'Dev preview stopped.', + body: [ + `The dev preview has been stopped on ${result.storeHostname} and the app's active version has been restored.`, + 'You can start it again with', + {command: 'shopify app dev'}, + ], + }) +} diff --git a/packages/app/src/cli/services/dev-clean/types.ts b/packages/app/src/cli/services/dev-clean/types.ts new file mode 100644 index 00000000000..12c81ed0945 --- /dev/null +++ b/packages/app/src/cli/services/dev-clean/types.ts @@ -0,0 +1,20 @@ +import {defineJsonOutputSchema, type InferJsonOutputSchema} from '@shopify/cli-kit/node/json-output-schema' +import {zod} from '@shopify/cli-kit/node/schema' + +const appSchema = zod.object({name: zod.string(), clientId: zod.string().min(1)}).strict() + +export const appDevCleanJsonOutputSchema = defineJsonOutputSchema({ + name: 'AppDevCleanResult', + schema: zod + .object({ + status: zod.literal('success'), + app: appSchema, + storeDomain: zod.string().nullable().describe('The canonical *.myshopify.com hostname, or null when unknown.'), + }) + .strict(), + definitions: {AppDevCleanApp: appSchema}, +}) + +export type AppDevCleanResult = InferJsonOutputSchema + +export type DevCleanResult = Omit & {storeHostname: string} diff --git a/packages/cli/README.md b/packages/cli/README.md index 997b0a3b74e..b35895e3e57 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -854,14 +854,18 @@ Cleans up the dev preview from the selected store. ``` USAGE - $ shopify app dev clean [--auth-alias ] [--client-id ] [--json-schema] [--no-color] [--no-input] - [--path ] [--reset | -c ] [-s ] [--verbose] + $ shopify app dev clean [--auth-alias ] [--client-id ] [-j] [--json-schema] [--no-color] + [--no-input] [--path ] [--reset | -c ] [-s ] [--verbose] FLAGS -c, --config= The name of the app configuration. [env: SHOPIFY_FLAG_APP_CONFIG] + -j, --json + Output the result as JSON. Automatically disables color output. + [env: SHOPIFY_FLAG_JSON] + -s, --store= Store URL. Must be an existing development store. [env: SHOPIFY_FLAG_STORE] @@ -905,6 +909,60 @@ DESCRIPTION Stop the dev preview that was started with `shopify app dev`. It restores the app's active version to the selected development store. + + + Use `--json-schema` to print the result, error, and event schemas. + + Output from `--json` conforms to the `AppDevCleanResult` schema. + + ```json + { + "type": "object", + "properties": { + "status": { + "type": "string", + "const": "success" + }, + "app": { + "$ref": "#/definitions/AppDevCleanApp" + }, + "storeDomain": { + "type": [ + "string", + "null" + ], + "description": "The canonical *.myshopify.com hostname, or null when unknown." + } + }, + "required": [ + "status", + "app", + "storeDomain" + ], + "additionalProperties": false, + "title": "AppDevCleanResult", + "definitions": { + "AppDevCleanApp": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "clientId": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "name", + "clientId" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + } + ``` ``` ## `shopify app env pull` diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index ea9b7cfb824..549730004f0 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -1351,7 +1351,7 @@ "args": { }, "customPluginName": "@shopify/app", - "description": "Stop the dev preview that was started with `shopify app dev`.\n\n It restores the app's active version to the selected development store.\n ", + "description": "Stop the dev preview that was started with `shopify app dev`.\n\n It restores the app's active version to the selected development store.\n \n\nUse `--json-schema` to print the result, error, and event schemas.\n\nOutput from `--json` conforms to the `AppDevCleanResult` schema.\n\n```json\n{\n \"type\": \"object\",\n \"properties\": {\n \"status\": {\n \"type\": \"string\",\n \"const\": \"success\"\n },\n \"app\": {\n \"$ref\": \"#/definitions/AppDevCleanApp\"\n },\n \"storeDomain\": {\n \"type\": [\n \"string\",\n \"null\"\n ],\n \"description\": \"The canonical *.myshopify.com hostname, or null when unknown.\"\n }\n },\n \"required\": [\n \"status\",\n \"app\",\n \"storeDomain\"\n ],\n \"additionalProperties\": false,\n \"title\": \"AppDevCleanResult\",\n \"definitions\": {\n \"AppDevCleanApp\": {\n \"type\": \"object\",\n \"properties\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"clientId\": {\n \"type\": \"string\",\n \"minLength\": 1\n }\n },\n \"required\": [\n \"name\",\n \"clientId\"\n ],\n \"additionalProperties\": false\n }\n },\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n}\n```", "descriptionWithMarkdown": "Stop the dev preview that was started with `shopify app dev`.\n\n It restores the app's active version to the selected development store.\n ", "flags": { "auth-alias": { @@ -1381,6 +1381,15 @@ "name": "config", "type": "option" }, + "json": { + "allowNo": false, + "char": "j", + "description": "Output the result as JSON. Automatically disables color output.", + "env": "SHOPIFY_FLAG_JSON", + "hidden": false, + "name": "json", + "type": "boolean" + }, "json-schema": { "allowNo": false, "description": "Print the command's JSON schemas.", diff --git a/packages/eslint-plugin-cli/rules/command-json-output.js b/packages/eslint-plugin-cli/rules/command-json-output.js index a6493344283..4d41adab57a 100644 --- a/packages/eslint-plugin-cli/rules/command-json-output.js +++ b/packages/eslint-plugin-cli/rules/command-json-output.js @@ -62,7 +62,9 @@ function repositoryPath(filename) { function hasJsonOutputSchema(classMembers) { return classMembers.some( (member) => - member.type === 'MethodDefinition' && member.kind === 'get' && isStaticMemberNamed(member, 'jsonOutputSchema'), + isStaticMemberNamed(member, 'jsonOutputSchema') && + ((member.type === 'MethodDefinition' && member.kind === 'get') || + (member.type === 'PropertyDefinition' && Boolean(member.value))), ) } diff --git a/packages/eslint-plugin-cli/rules/command-json-output.test.js b/packages/eslint-plugin-cli/rules/command-json-output.test.js index 5eaa3d46144..b095246b579 100644 --- a/packages/eslint-plugin-cli/rules/command-json-output.test.js +++ b/packages/eslint-plugin-cli/rules/command-json-output.test.js @@ -37,9 +37,7 @@ ruleTester.run('command-json-output', rule, { code: ` export default class WidgetDelete extends Command { static flags = {...globalFlags, ...jsonFlag} - static get jsonOutputSchema() { - return widgetDeleteJsonOutputSchema - } + static jsonOutputSchema = widgetDeleteJsonOutputSchema } `, }, diff --git a/packages/eslint-plugin-cli/rules/json-output-command-exceptions.js b/packages/eslint-plugin-cli/rules/json-output-command-exceptions.js index adf82f9988f..137908bf8ac 100644 --- a/packages/eslint-plugin-cli/rules/json-output-command-exceptions.js +++ b/packages/eslint-plugin-cli/rules/json-output-command-exceptions.js @@ -11,7 +11,6 @@ const commandExceptions = [ 'packages/app/src/cli/commands/app/config/use.ts', 'packages/app/src/cli/commands/app/config/validate.ts', 'packages/app/src/cli/commands/app/deploy.ts', - 'packages/app/src/cli/commands/app/dev/clean.ts', 'packages/app/src/cli/commands/app/env/pull.ts', 'packages/app/src/cli/commands/app/env/show.ts', 'packages/app/src/cli/commands/app/execute.ts',