Skip to content
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
11 changes: 10 additions & 1 deletion docs-shopify.dev/generated/generated_docs_data_v2.json
Original file line number Diff line number Diff line change
Expand Up @@ -1114,6 +1114,15 @@
"isOptional": true,
"environmentValue": "SHOPIFY_FLAG_APP_CONFIG"
},
{
"filePath": "docs-shopify.dev/commands/interfaces/app-dev.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.interface.ts",
"syntaxKind": "PropertySignature",
Expand All @@ -1133,7 +1142,7 @@
"environmentValue": "SHOPIFY_FLAG_THEME"
}
],
"value": "export interface appdev {\n /**\n * Alias of the Shopify account to use for authentication.\n * @environment SHOPIFY_FLAG_AUTH_ALIAS\n */\n '--auth-alias <value>'?: string\n\n /**\n * Resource URL for checkout UI extension. Format: \"/cart/{productVariantID}:{productQuantity}\"\n * @environment SHOPIFY_FLAG_CHECKOUT_CART_URL\n */\n '--checkout-cart-url <value>'?: string\n\n /**\n * The Client ID of your app.\n * @environment SHOPIFY_FLAG_CLIENT_ID\n */\n '--client-id <value>'?: string\n\n /**\n * The name of the app configuration.\n * @environment SHOPIFY_FLAG_APP_CONFIG\n */\n '-c, --config <value>'?: string\n\n /**\n * Install and use mkcert to generate localhost certificates when --use-localhost is enabled without prompting.\n * @environment SHOPIFY_FLAG_INSTALL_MKCERT\n */\n '--install-mkcert'?: ''\n\n /**\n * Port to use for localhost. Must be between 1 and 65535.\n * @environment SHOPIFY_FLAG_LOCALHOST_PORT\n */\n '--localhost-port <value>'?: string\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * Uses the app URL from the toml file instead an autogenerated URL for dev.\n * @environment SHOPIFY_FLAG_NO_UPDATE\n */\n '--no-update'?: ''\n\n /**\n * The file path or URL. The file path is to a file that you want updated on idle. The URL path is where you want a webhook posted to report on file changes.\n * @environment SHOPIFY_FLAG_NOTIFY\n */\n '--notify <value>'?: string\n\n /**\n * The path to your app directory.\n * @environment SHOPIFY_FLAG_PATH\n */\n '--path <value>'?: string\n\n /**\n * Reset all your settings.\n * @environment SHOPIFY_FLAG_RESET\n */\n '--reset'?: ''\n\n /**\n * Skips the installation of dependencies. Deprecated, use workspaces instead.\n * @environment SHOPIFY_FLAG_SKIP_DEPENDENCIES_INSTALLATION\n */\n '--skip-dependencies-installation'?: ''\n\n /**\n * Store URL. Must be an existing development or Shopify Plus sandbox store.\n * @environment SHOPIFY_FLAG_STORE\n */\n '-s, --store <value>'?: string\n\n /**\n * The password for storefronts with password protection.\n * @environment SHOPIFY_FLAG_STORE_PASSWORD\n */\n '--store-password <value>'?: string\n\n /**\n * Resource URL for subscription UI extension. Format: \"/products/{productId}\"\n * @environment SHOPIFY_FLAG_SUBSCRIPTION_PRODUCT_URL\n */\n '--subscription-product-url <value>'?: string\n\n /**\n * Theme ID or name of the theme app extension host theme.\n * @environment SHOPIFY_FLAG_THEME\n */\n '-t, --theme <value>'?: string\n\n /**\n * Local port of the theme app extension development server. Must be between 1 and 65535.\n * @environment SHOPIFY_FLAG_THEME_APP_EXTENSION_PORT\n */\n '--theme-app-extension-port <value>'?: string\n\n /**\n * Use a custom tunnel, it must be running before executing dev. Format: \"https://my-tunnel-url:port\".\n * @environment SHOPIFY_FLAG_TUNNEL_URL\n */\n '--tunnel-url <value>'?: string\n\n /**\n * Service entry point will listen to localhost. A tunnel won't be used. Will work for testing many app features, but not those that directly invoke your app (E.g: Webhooks)\n * @environment SHOPIFY_FLAG_USE_LOCALHOST\n */\n '--use-localhost'?: ''\n\n /**\n * Increase the verbosity of the output.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}"
"value": "export interface appdev {\n /**\n * Alias of the Shopify account to use for authentication.\n * @environment SHOPIFY_FLAG_AUTH_ALIAS\n */\n '--auth-alias <value>'?: string\n\n /**\n * Resource URL for checkout UI extension. Format: \"/cart/{productVariantID}:{productQuantity}\"\n * @environment SHOPIFY_FLAG_CHECKOUT_CART_URL\n */\n '--checkout-cart-url <value>'?: string\n\n /**\n * The Client ID of your app.\n * @environment SHOPIFY_FLAG_CLIENT_ID\n */\n '--client-id <value>'?: string\n\n /**\n * The name of the app configuration.\n * @environment SHOPIFY_FLAG_APP_CONFIG\n */\n '-c, --config <value>'?: string\n\n /**\n * Install and use mkcert to generate localhost certificates when --use-localhost is enabled without prompting.\n * @environment SHOPIFY_FLAG_INSTALL_MKCERT\n */\n '--install-mkcert'?: ''\n\n /**\n * Output the result as JSON. Automatically disables color output.\n * @environment SHOPIFY_FLAG_JSON\n */\n '-j, --json'?: ''\n\n /**\n * Port to use for localhost. Must be between 1 and 65535.\n * @environment SHOPIFY_FLAG_LOCALHOST_PORT\n */\n '--localhost-port <value>'?: string\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * Uses the app URL from the toml file instead an autogenerated URL for dev.\n * @environment SHOPIFY_FLAG_NO_UPDATE\n */\n '--no-update'?: ''\n\n /**\n * The file path or URL. The file path is to a file that you want updated on idle. The URL path is where you want a webhook posted to report on file changes.\n * @environment SHOPIFY_FLAG_NOTIFY\n */\n '--notify <value>'?: string\n\n /**\n * The path to your app directory.\n * @environment SHOPIFY_FLAG_PATH\n */\n '--path <value>'?: string\n\n /**\n * Reset all your settings.\n * @environment SHOPIFY_FLAG_RESET\n */\n '--reset'?: ''\n\n /**\n * Skips the installation of dependencies. Deprecated, use workspaces instead.\n * @environment SHOPIFY_FLAG_SKIP_DEPENDENCIES_INSTALLATION\n */\n '--skip-dependencies-installation'?: ''\n\n /**\n * Store URL. Must be an existing development or Shopify Plus sandbox store.\n * @environment SHOPIFY_FLAG_STORE\n */\n '-s, --store <value>'?: string\n\n /**\n * The password for storefronts with password protection.\n * @environment SHOPIFY_FLAG_STORE_PASSWORD\n */\n '--store-password <value>'?: string\n\n /**\n * Resource URL for subscription UI extension. Format: \"/products/{productId}\"\n * @environment SHOPIFY_FLAG_SUBSCRIPTION_PRODUCT_URL\n */\n '--subscription-product-url <value>'?: string\n\n /**\n * Theme ID or name of the theme app extension host theme.\n * @environment SHOPIFY_FLAG_THEME\n */\n '-t, --theme <value>'?: string\n\n /**\n * Local port of the theme app extension development server. Must be between 1 and 65535.\n * @environment SHOPIFY_FLAG_THEME_APP_EXTENSION_PORT\n */\n '--theme-app-extension-port <value>'?: string\n\n /**\n * Use a custom tunnel, it must be running before executing dev. Format: \"https://my-tunnel-url:port\".\n * @environment SHOPIFY_FLAG_TUNNEL_URL\n */\n '--tunnel-url <value>'?: string\n\n /**\n * Service entry point will listen to localhost. A tunnel won't be used. Will work for testing many app features, but not those that directly invoke your app (E.g: Webhooks)\n * @environment SHOPIFY_FLAG_USE_LOCALHOST\n */\n '--use-localhost'?: ''\n\n /**\n * Increase the verbosity of the output.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}"
}
},
"appenvpull": {
Expand Down
1 change: 1 addition & 0 deletions packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"@luckycatfactory/esbuild-graphql-loader": "3.8.1",
"@oclif/core": "4.8.3",
"@shopify/cli-kit": "4.5.0",
"@shopify/diagnostics": "4.5.0",
"@shopify/organizations": "4.5.0",
"@shopify/plugin-cloudflare": "4.5.0",
"@shopify/theme": "4.5.0",
Expand Down
25 changes: 25 additions & 0 deletions packages/app/src/cli/commands/app/dev.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ describe('app dev command', () => {
localhostPort: undefined,
})
expect(dev).toHaveBeenCalledWith(expect.objectContaining({installMkcert: false, tunnel: {mode: 'auto'}}))
expect(dev).toHaveBeenCalledWith(expect.objectContaining({format: 'text'}))
})
})

Expand All @@ -69,4 +70,28 @@ describe('app dev command', () => {
expect(dev).not.toHaveBeenCalled()
})
})

test('uses JSON output when --json is passed', async () => {
await inTemporaryDirectory(async (tmp) => {
const app = testAppLinked({directory: tmp})
const appContextResult = {
app,
remoteApp: testOrganizationApp(),
organization: testOrganization(),
project: testProject(),
activeConfig: {} as never,
specifications: [],
developerPlatformClient: testDeveloperPlatformClient(),
} as Awaited<ReturnType<typeof linkedAppContext>>
const store = testOrganizationStore({shopDomain: 'dev-store.myshopify.com'})

vi.mocked(linkedAppContext).mockResolvedValue(appContextResult)
vi.mocked(storeContext).mockResolvedValue(store)
vi.mocked(getTunnelMode).mockResolvedValue({mode: 'auto'})

await Dev.run(['--path', tmp, '--store', store.shopDomain, '--json'], import.meta.url)

expect(dev).toHaveBeenCalledWith(expect.objectContaining({format: 'json'}))
})
})
})
7 changes: 6 additions & 1 deletion packages/app/src/cli/commands/app/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import AppLinkedCommand, {AppLinkedCommandOutput} from '../../utilities/app-link
import {linkedAppContext} from '../../services/app-context.js'
import {storeContext} from '../../services/store-context.js'
import {getTunnelMode} from '../../services/dev/tunnel-mode.js'
import {DevEvent, devJsonEventSink} from '../../services/dev/json.js'
import {Flags} from '@oclif/core'
import {createSyncDiagnosticChannel} from '@shopify/diagnostics'
import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn'
import {globalFlags, portFlag} from '@shopify/cli-kit/node/cli'
import {globalFlags, jsonFlag, portFlag} from '@shopify/cli-kit/node/cli'
import {addPublicMetadata} from '@shopify/cli-kit/node/metadata'

export default class Dev extends AppLinkedCommand {
Expand All @@ -20,6 +22,7 @@ export default class Dev extends AppLinkedCommand {
static flags = {
...globalFlags,
...appFlags,
...jsonFlag,
store: Flags.string({
char: 's',
description: 'Store URL. Must be an existing development or Shopify Plus sandbox store.',
Expand Down Expand Up @@ -149,6 +152,8 @@ export default class Dev extends AppLinkedCommand {
graphiqlKey: flags['graphiql-key'],
installMkcert: flags['install-mkcert'] ?? false,
tunnel: tunnelMode,
format: flags.json ? 'json' : 'text',
events: createSyncDiagnosticChannel<DevEvent>(...(flags.json ? [devJsonEventSink] : [])),
}

await dev(devOptions)
Expand Down
3 changes: 3 additions & 0 deletions packages/app/src/cli/services/dev.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {describe, expect, test, vi} from 'vitest'
import {hashString} from '@shopify/cli-kit/node/crypto'
import {reportAnalyticsEvent} from '@shopify/cli-kit/node/analytics'
import {checkPortAvailability, getAvailableTCPPort} from '@shopify/cli-kit/node/tcp'
import {createSyncDiagnosticChannel} from '@shopify/diagnostics'

vi.mock('./dev/fetch.js')
vi.mock('./dev/processes/setup-dev-processes.js')
Expand Down Expand Up @@ -62,6 +63,8 @@ describe('dev', () => {
commandConfig: {} as any,
skipDependenciesInstallation: true,
tunnel: {mode: 'custom', url: 'https://localhost:3456'},
format: 'text',
events: createSyncDiagnosticChannel(),
})

expect(publicMetadata).toEqual(
Expand Down
6 changes: 6 additions & 0 deletions packages/app/src/cli/services/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {DevProcessFunction} from './dev/processes/types.js'
import {getCachedAppInfo, setCachedAppInfo} from './local-storage.js'
import {fetchAppRemoteConfiguration} from './app/select-app.js'
import {DevSessionStatusManager} from './dev/processes/dev-session/dev-session-status-manager.js'
import {DevEvent, DevFormat} from './dev/json.js'
import {TunnelMode} from './dev/tunnel-mode.js'
import {PortDetail, renderPortWarnings} from './dev/port-warnings.js'
import {DeveloperPlatformClient} from '../utilities/developer-platform-client.js'
Expand All @@ -39,6 +40,7 @@ import {reportAnalyticsEvent} from '@shopify/cli-kit/node/analytics'
import {OutputProcess} from '@shopify/cli-kit/node/output'
import {hashString} from '@shopify/cli-kit/node/crypto'
import {AbortError} from '@shopify/cli-kit/node/error'
import {SyncDiagnosticChannel} from '@shopify/diagnostics'

export interface DevOptions {
app: AppLinkedInterface
Expand All @@ -62,6 +64,8 @@ export interface DevOptions {
graphiqlPort?: number
graphiqlKey?: string
installMkcert?: boolean
format: DevFormat
events: SyncDiagnosticChannel<DevEvent>
}

export async function dev(commandOptions: DevOptions) {
Expand Down Expand Up @@ -357,6 +361,8 @@ async function launchDevProcesses({
organizationName: config.commandOptions.organization.businessName,
configPath: config.localApp.configPath,
localURL: config.network.proxyUrl,
format: config.commandOptions.format,
events: config.commandOptions.events,
})
}

Expand Down
74 changes: 74 additions & 0 deletions packages/app/src/cli/services/dev/json.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import {DevEvent, devJsonEventSink, renderDevJson} from './json.js'
import {DevSessionStatusManager} from './processes/dev-session/dev-session-status-manager.js'
import {afterEach, describe, expect, test} from 'vitest'
import {createSyncDiagnosticChannel} from '@shopify/diagnostics'
import {mockAndCaptureOutput} from '@shopify/cli-kit/node/testing/output'
import {AbortController} from '@shopify/cli-kit/node/abort'

afterEach(() => {
mockAndCaptureOutput().clear()
})

describe('renderDevJson', () => {
test('renders startup, process output, and status updates as NDJSON events', async () => {
const output = mockAndCaptureOutput()
const events = createSyncDiagnosticChannel<DevEvent>(devJsonEventSink)
const devSessionStatusManager = new DevSessionStatusManager()

await renderDevJson({
processes: [
{
prefix: 'web',
action: async (stdout, stderr) => {
stdout.write('\u001B[32mServer ready\u001B[39m\n')
stderr.write('Dependency warning\n')
devSessionStatusManager.updateStatus({
isReady: true,
statusMessage: {message: 'Ready, watching for changes', type: 'success'},
})
},
},
],
previewUrl: 'https://example.myshopify.com',
graphiqlUrl: 'http://localhost:3457/graphiql',
abortController: new AbortController(),
devSessionStatusManager,
events,
})

const renderedEvents = output
.output()
.split('\n')
.map((line) => JSON.parse(line))
expect(renderedEvents).toEqual([
{
type: 'started',
level: 'info',
message: 'Dev session started',
preview_url: 'https://example.myshopify.com',
graphiql_url: 'http://localhost:3457/graphiql',
},
{
type: 'log',
level: 'info',
message: 'Server ready\n',
source: 'web',
stream: 'stdout',
},
{
type: 'log',
level: 'info',
message: 'Dependency warning\n',
source: 'web',
stream: 'stderr',
},
{
type: 'status',
level: 'info',
message: 'Ready, watching for changes',
status: 'success',
is_ready: true,
},
])
})
})
109 changes: 109 additions & 0 deletions packages/app/src/cli/services/dev/json.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import {DevSessionStatus, DevSessionStatusManager} from './processes/dev-session/dev-session-status-manager.js'
import {OutputProcess, outputResult, unstyled} from '@shopify/cli-kit/node/output'
import {AbortController} from '@shopify/cli-kit/node/abort'
import {DiagnosticEvent, SyncDiagnosticChannel} from '@shopify/diagnostics'
import {Writable} from 'stream'

export type DevFormat = 'json' | 'text'

export type DevEvent =
| (DiagnosticEvent & {
type: 'started'
level: 'info'
preview_url: string
graphiql_url?: string
})
| (DiagnosticEvent & {
type: 'status'
level: 'info' | 'warning'
status: DevSessionStatusMessage
is_ready: boolean
preview_url?: string
graphiql_url?: string
})
| (DiagnosticEvent & {
type: 'log'
level: 'info'
source: string
stream: 'stdout' | 'stderr'
})

type DevSessionStatusMessage = NonNullable<DevSessionStatus['statusMessage']>['type'] | 'unknown'

interface RenderDevJsonOptions {
processes: OutputProcess[]
previewUrl: string
graphiqlUrl?: string
abortController: AbortController
devSessionStatusManager: DevSessionStatusManager
events: SyncDiagnosticChannel<DevEvent>
}

export function devJsonEventSink(event: DevEvent): void {
outputResult(JSON.stringify(event))
}

export async function renderDevJson({
processes,
previewUrl,
graphiqlUrl,
abortController,
devSessionStatusManager,
events,
}: RenderDevJsonOptions): Promise<void> {
events.emit({
type: 'started',
level: 'info',
message: 'Dev session started',
preview_url: previewUrl,
...(graphiqlUrl ? {graphiql_url: graphiqlUrl} : {}),
})

const emitStatus = (status: DevSessionStatus) => {
const statusMessage = status.statusMessage
events.emit({
type: 'status',
level: statusMessage?.type === 'error' ? 'warning' : 'info',
message: statusMessage?.message ?? 'Dev session updated',
status: statusMessage?.type ?? 'unknown',
is_ready: status.isReady,
...(status.previewURL ? {preview_url: status.previewURL} : {}),
...(status.graphiqlURL ? {graphiql_url: status.graphiqlURL} : {}),
})
}

devSessionStatusManager.on('dev-session-update', emitStatus)

try {
await Promise.all(
processes.map(async (devProcess) => {
await devProcess.action(
createLogStream(devProcess.prefix, 'stdout', events),
createLogStream(devProcess.prefix, 'stderr', events),
abortController.signal,
)
}),
)
} finally {
devSessionStatusManager.off('dev-session-update', emitStatus)
}
}

function createLogStream(
source: string,
stream: 'stdout' | 'stderr',
events: SyncDiagnosticChannel<DevEvent>,
): Writable {
return new Writable({
write(chunk, _encoding, callback) {
events.emit({
type: 'log',
level: 'info',
message: unstyled(chunk.toString('utf8')),
source,
stream,
})
callback()
},
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {getEnvironmentVariables} from '@shopify/cli-kit/node/environment'
import {isStorefrontPasswordProtected} from '@shopify/theme'
import {fetchTheme} from '@shopify/cli-kit/node/themes/api'
import {adminFqdn} from '@shopify/cli-kit/node/context/fqdn'
import {createSyncDiagnosticChannel} from '@shopify/diagnostics'

vi.mock('@shopify/cli-kit/node/session.js')
vi.mock('../fetch.js')
Expand Down Expand Up @@ -73,6 +74,8 @@ const appContextResult = {
organization: testOrganization(),
store: testOrganizationStore({}),
specifications: [],
format: 'text' as const,
events: createSyncDiagnosticChannel(),
}

describe('setup-dev-processes', () => {
Expand Down
Loading
Loading