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
5 changes: 5 additions & 0 deletions .changeset/env-help-after-description.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/cli': patch
---

Move help output env labels after flag descriptions.
1,612 changes: 808 additions & 804 deletions packages/cli/README.md

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions packages/cli/bin/bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ esBuild({
{in: './src/index.ts', out: 'index'},
{in: './src/bootstrap.ts', out: 'bootstrap'},
{in: './src/command-registry.ts', out: 'command-registry'},
// Custom oclif help class, loaded at runtime via `oclif.helpClass` in
// package.json, so it needs to be emitted as its own entry point.
{in: './src/cli/help.ts', out: 'cli/help'},
...hookEntryPoints.map(toEntry),
...commandEntryPoints.map(toEntry),
...externalCommandEntryPoints,
Expand Down
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
"engine-strict": true,
"oclif": {
"bin": "shopify",
"helpClass": "./dist/cli/help.js",
"commands": {
"strategy": "explicit",
"target": "./dist/index.js",
Expand Down
101 changes: 101 additions & 0 deletions packages/cli/src/cli/help.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import ShopifyHelp, {ShopifyCommandHelp} from './help.js'
import {CommandHelp, Help} from '@oclif/core'
import {describe, expect, test} from 'vitest'
import type {Command, Interfaces} from '@oclif/core'

const stripAnsi = (value: string) => value.replace(new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, 'g'), '')

function renderFlags(flags: Command.Flag.Any[]): [string, string | undefined][] {
const help = new ShopifyCommandHelp({} as Command.Loadable, {} as Interfaces.Config, {} as Interfaces.HelpOptions)
// `flags()` is protected; reach in to exercise the unit directly.
const rows = (
help as unknown as {flags: (f: Command.Flag.Any[]) => [string, string | undefined][] | undefined}
).flags(flags)
return (rows ?? []).map(([left, right]) => [stripAnsi(left), right === undefined ? undefined : stripAnsi(right)])
}

describe('ShopifyCommandHelp', () => {
test('moves the env metadata to the end of a boolean flag description', () => {
// Given
const flags = [
{
name: 'json',
char: 'j',
type: 'boolean',
env: 'SHOPIFY_FLAG_JSON',
summary: 'Output the result as JSON.',
} as Command.Flag.Any,
]

// When
const right = renderFlags(flags)[0]?.[1]

// Then
expect(right).toBe('Output the result as JSON. [env: SHOPIFY_FLAG_JSON]')
})

test('keeps default at the front and moves env to the end for option flags', () => {
// Given
const flags = [
{
name: 'name',
type: 'option',
env: 'SHOPIFY_FLAG_PREVIEW_STORE_NAME',
default: 'my-store',
summary: 'The name of the store.',
} as Command.Flag.Any,
]

// When
const right = renderFlags(flags)[0]?.[1]

// Then
expect(right).toBe('[default: my-store] The name of the store. [env: SHOPIFY_FLAG_PREVIEW_STORE_NAME]')
})

test('leaves flags without an env untouched', () => {
// Given
const flags = [
{
name: 'verbose',
type: 'boolean',
summary: 'Increase the verbosity of the output.',
} as Command.Flag.Any,
]

// When
const right = renderFlags(flags)[0]?.[1]

// Then
expect(right).toBe('Increase the verbosity of the output.')
})

test('uses the env label as the description when a flag has no summary', () => {
// Given
const flags = [
{
name: 'store',
type: 'option',
env: 'SHOPIFY_FLAG_STORE',
} as Command.Flag.Any,
]

// When
const right = renderFlags(flags)[0]?.[1]

// Then
expect(right).toBe('[env: SHOPIFY_FLAG_STORE]')
})
})

describe('ShopifyHelp', () => {
test('is an oclif Help that renders command help with ShopifyCommandHelp', () => {
// When
const help = new ShopifyHelp({} as Interfaces.Config)

// Then
expect(help).toBeInstanceOf(Help)
expect((help as unknown as {CommandHelpClass: unknown}).CommandHelpClass).toBe(ShopifyCommandHelp)
expect(ShopifyCommandHelp.prototype).toBeInstanceOf(CommandHelp)
})
})
50 changes: 50 additions & 0 deletions packages/cli/src/cli/help.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {CommandHelp, Help} from '@oclif/core'
import type {Command} from '@oclif/core'

/**
* Custom command-help renderer that relocates the `[env: ...]` metadata to the
* end of a flag's description instead of the front.
*
* Since oclif 4.8, flag help renders the backing environment variable inline
* *before* the description, e.g.:
*
* -j, --json [env: SHOPIFY_FLAG_JSON] Output the result as JSON.
*
* Because Shopify CLI's `SHOPIFY_FLAG_*` names are long, that pushes the actual
* description far to the right and hurts readability. We keep the information
* (it's useful for scripting and agents) but move it to the end:
*
* -j, --json Output the result as JSON. [env: SHOPIFY_FLAG_JSON]
*
* We do this by cloning each flag, clearing its `env`, and appending the env
* label to the summary before delegating to oclif's own `flags()` renderer.
* That way all the other formatting oclif applies — alignment, wrapping,
* theming, `[default: ...]`, `(required)`, `<options: ...>` — is reused
* unchanged, and the only coupling to oclif internals is that `flag.env` is
* what drives the env label. Other help sections receive the original flags.
*/
export class ShopifyCommandHelp extends CommandHelp {
protected flags(flags: Command.Flag.Any[]): [string, string | undefined][] | undefined {
const relocated = flags.map((flag) => {
if (!flag.env) return flag
const description = flag.summary ?? flag.description ?? ''
const envLabel = `[env: ${flag.env}]`
return {
...flag,
env: undefined,
summary: description === '' ? envLabel : `${description} ${envLabel}`,
} as Command.Flag.Any
})

return super.flags(relocated)
}
}

/**
* Custom help class, wired up via `oclif.helpClass` in this package's
* `package.json`. It only swaps in {@link ShopifyCommandHelp}; everything else
* uses oclif's default help behaviour.
*/
export default class ShopifyHelp extends Help {
protected CommandHelpClass = ShopifyCommandHelp
}
Loading