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
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 @@ -8279,9 +8279,18 @@
"description": "Increase the verbosity of the output. May include sensitive data.",
"isOptional": true,
"environmentValue": "SHOPIFY_FLAG_VERBOSE"
},
{
"filePath": "docs-shopify.dev/commands/interfaces/theme-package.interface.ts",
"syntaxKind": "PropertySignature",
"name": "-j, --json",
"value": "''",
"description": "Output the result as JSON. Automatically disables color output.",
"isOptional": true,
"environmentValue": "SHOPIFY_FLAG_JSON"
}
],
"value": "export interface themepackage {\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 * 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 * The path where you want to run the command. Defaults to the current working directory.\n * @environment SHOPIFY_FLAG_PATH\n */\n '--path <value>'?: 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 themepackage {\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 * 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 * The path where you want to run the command. Defaults to the current working directory.\n * @environment SHOPIFY_FLAG_PATH\n */\n '--path <value>'?: string\n\n /**\n * Increase the verbosity of the output. May include sensitive data.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}"
}
},
"themepreview": {
Expand Down
27 changes: 26 additions & 1 deletion packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8897,9 +8897,13 @@ Package your theme into a .zip file, ready to upload to the Online Store.

```
USAGE
$ shopify theme package [--auth-alias <value>] [--json-schema] [--no-color] [--path <value>] [--verbose]
$ shopify theme package [--auth-alias <value>] [-j] [--json-schema] [--no-color] [--path <value>] [--verbose]

FLAGS
-j, --json
Output the result as JSON. Automatically disables color output.
[env: SHOPIFY_FLAG_JSON]

--auth-alias=<value>
Alias of the Shopify account to use for authentication.
[env: SHOPIFY_FLAG_AUTH_ALIAS]
Expand Down Expand Up @@ -8934,6 +8938,27 @@ DESCRIPTION

The ZIP file uses the name `theme_name-theme_version.zip`, based on parameters in your "settings_schema.json"
(https://shopify.dev/docs/storefronts/themes/architecture/config/settings-schema-json) file.

Use `--json-schema` to print the result, error, and event schemas.

Output from `--json` conforms to the `ThemePackageResult` schema.

```json
{
"type": "object",
"properties": {
"path": {
"type": "string"
}
},
"required": [
"path"
],
"additionalProperties": false,
"title": "ThemePackageResult",
"$schema": "http://json-schema.org/draft-07/schema#"
}
```
```

## `shopify theme preview`
Expand Down
11 changes: 10 additions & 1 deletion packages/cli/oclif.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -10758,7 +10758,7 @@
"args": {
},
"customPluginName": "@shopify/theme",
"description": "Packages your local theme files into a ZIP file that can be uploaded to Shopify.\n\n Only folders that match the \"default Shopify theme folder structure\" (https://shopify.dev/docs/storefronts/themes/tools/cli#directory-structure) are included in the package.\n\n The package includes the `listings` directory if present (required for multi-preset themes per \"Theme Store requirements\" (https://shopify.dev/docs/storefronts/themes/store/requirements#adding-presets-to-your-theme-zip-submission)).\n\n The ZIP file uses the name `theme_name-theme_version.zip`, based on parameters in your \"settings_schema.json\" (https://shopify.dev/docs/storefronts/themes/architecture/config/settings-schema-json) file.",
"description": "Packages your local theme files into a ZIP file that can be uploaded to Shopify.\n\n Only folders that match the \"default Shopify theme folder structure\" (https://shopify.dev/docs/storefronts/themes/tools/cli#directory-structure) are included in the package.\n\n The package includes the `listings` directory if present (required for multi-preset themes per \"Theme Store requirements\" (https://shopify.dev/docs/storefronts/themes/store/requirements#adding-presets-to-your-theme-zip-submission)).\n\n The ZIP file uses the name `theme_name-theme_version.zip`, based on parameters in your \"settings_schema.json\" (https://shopify.dev/docs/storefronts/themes/architecture/config/settings-schema-json) file.\n\nUse `--json-schema` to print the result, error, and event schemas.\n\nOutput from `--json` conforms to the `ThemePackageResult` schema.\n\n```json\n{\n \"type\": \"object\",\n \"properties\": {\n \"path\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"path\"\n ],\n \"additionalProperties\": false,\n \"title\": \"ThemePackageResult\",\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n}\n```",
"descriptionWithMarkdown": "Packages your local theme files into a ZIP file that can be uploaded to Shopify.\n\n Only folders that match the [default Shopify theme folder structure](https://shopify.dev/docs/storefronts/themes/tools/cli#directory-structure) are included in the package.\n\n The package includes the `listings` directory if present (required for multi-preset themes per [Theme Store requirements](https://shopify.dev/docs/storefronts/themes/store/requirements#adding-presets-to-your-theme-zip-submission)).\n\n The ZIP file uses the name `theme_name-theme_version.zip`, based on parameters in your [settings_schema.json](https://shopify.dev/docs/storefronts/themes/architecture/config/settings-schema-json) file.",
"enableJsonFlag": false,
"flags": {
Expand All @@ -10770,6 +10770,15 @@
"name": "auth-alias",
"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.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ const commandExceptions = [
'packages/plugin-did-you-mean/src/commands/config/autocorrect/status.ts',
'packages/theme/src/cli/commands/theme/init.ts',
'packages/theme/src/cli/commands/theme/metafields/pull.ts',
'packages/theme/src/cli/commands/theme/package.ts',

// Streaming commands without a single finite result.
'packages/app/src/cli/commands/app/dev.ts',
Expand Down
66 changes: 66 additions & 0 deletions packages/theme/src/cli/commands/theme/package.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import Package from './package.js'
import {themePackageJsonOutputSchema} from '../../services/package/types.js'
import {renderThemePackageResult} from '../../services/package/result.js'
import {Config} from '@oclif/core'
import {runWithCommandEventsForCommand} from '@shopify/cli-kit/node/command-events'
import {inTemporaryDirectory, mkdir, writeFile, fileExists} from '@shopify/cli-kit/node/fs'
import {joinPath, relativizePath, cwd} from '@shopify/cli-kit/node/path'
import {withCapturedStandardStreams} from '@shopify/cli-kit/node/testing/output'
import {renderSuccess} from '@shopify/cli-kit/node/ui'
import {expect, test, vi} from 'vitest'

vi.mock('@shopify/cli-kit/node/ui')
vi.mock('@shopify/cli-kit/node/analytics')
vi.mock('@shopify/cli-kit/node/metadata')
vi.mock('@shopify/cli-kit/node/environments')

async function run(argv: string[]) {
const config = new Config({root: __dirname})
await config.load()
await new Package(argv, config).run()
}

test.each([true, false])('writes the package receipt to stdout with version=%s', async (includeVersion) => {
await inTemporaryDirectory(async (directory) => {
await mkdir(joinPath(directory, 'config'))
await writeFile(
joinPath(directory, 'config/settings_schema.json'),
JSON.stringify([{name: 'theme_info', theme_name: 'Dawn', ...(includeVersion ? {theme_version: '1.0'} : {})}]),
)
const path = joinPath(directory, includeVersion ? 'Dawn-1.0.zip' : 'Dawn.zip')

await withCapturedStandardStreams(async ({stdout, stderr}) => {
await runWithCommandEventsForCommand(['--json'], () => run(['--path', directory, '--json']))
expect(stdout()).toBe(`${themePackageJsonOutputSchema.encode({path})}\n`)
expect(stderr()).toBe('')
})
await expect(fileExists(path)).resolves.toBe(true)
expect(renderSuccess).not.toHaveBeenCalled()
})
})

test('preserves the text success banner', () => {
const path = joinPath(cwd(), 'Dawn.zip')
renderThemePackageResult({path}, 'text')
expect(renderSuccess).toHaveBeenCalledWith({
body: ['Your local theme was packaged in', {filePath: relativizePath(path)}],
})
})

test('does not emit a result when packaging fails', async () => {
await inTemporaryDirectory(async (directory) => {
await withCapturedStandardStreams(async ({stdout}) => {
await expect(run(['--path', directory, '--json'])).rejects.toThrow(
'Provide a config/settings_schema.json to package your theme.',
)
expect(stdout()).toBe('')
})
})
})

test('exposes the schema and rejects invalid package paths', () => {
expect(Package.jsonOutputSchema).toBe(themePackageJsonOutputSchema)
expect(Package.flags.json).toBeDefined()
expect(Package.description).toContain('--json-schema')
expect(() => themePackageJsonOutputSchema.validate({path: 42})).toThrow()
})
12 changes: 10 additions & 2 deletions packages/theme/src/cli/commands/theme/package.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import {themeFlags} from '../../flags.js'
import ThemeCommand, {RequiredFlags} from '../../utilities/theme-command.js'
import {packageTheme} from '../../services/package.js'
import {globalFlags} from '@shopify/cli-kit/node/cli'
import {themePackageJsonOutputSchema} from '../../services/package/types.js'
import {renderThemePackageResult} from '../../services/package/result.js'
import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli'
import {InferredFlags} from '@oclif/core/interfaces'

type PackageFlags = InferredFlags<typeof Package.flags>
export default class Package extends ThemeCommand {
static get jsonOutputSchema() {
return themePackageJsonOutputSchema
}

static summary = 'Package your theme into a .zip file, ready to upload to the Online Store.'

static descriptionWithMarkdown = `Packages your local theme files into a ZIP file that can be uploaded to Shopify.
Expand All @@ -20,12 +26,14 @@ export default class Package extends ThemeCommand {

static flags = {
...globalFlags,
...jsonFlag,
path: themeFlags.path,
}

static multiEnvironmentsFlags: RequiredFlags = null

async command(flags: PackageFlags) {
await packageTheme(flags.path)
const result = await packageTheme(flags.path)
renderThemePackageResult(result, flags.json ? 'json' : 'text')
}
}
21 changes: 9 additions & 12 deletions packages/theme/src/cli/services/package.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ describe('packageTheme', () => {
)

// When
await packageTheme(inputDirectory)
const result = await packageTheme(inputDirectory)

// Then
const expectedOutputZipPath = joinPath(inputDirectory, 'Dawn-7.0.2.zip')
Expand All @@ -42,9 +42,8 @@ describe('packageTheme', () => {
const archiveEntries = await readArchiveFiles(expectedOutputZipPath)
expect(themeRelativePaths.sort()).toEqual(archiveEntries.sort())

expect(renderSuccess).toBeCalledWith({
body: ['Your local theme was packaged in', {filePath: expectedOutputZipPath}],
})
expect(result).toEqual({path: expectedOutputZipPath})
expect(renderSuccess).not.toHaveBeenCalled()
})
})

Expand Down Expand Up @@ -73,7 +72,7 @@ describe('packageTheme', () => {
)

// When
await packageTheme(inputDirectory)
const result = await packageTheme(inputDirectory)

// Then
const expectedOutputZipPath = joinPath(inputDirectory, 'Dawn-7.0.2.zip')
Expand All @@ -82,9 +81,8 @@ describe('packageTheme', () => {
const archiveEntries = await readArchiveFiles(expectedOutputZipPath)
expect(expectedThemeRelativePaths.sort()).toEqual(archiveEntries.sort())

expect(renderSuccess).toBeCalledWith({
body: ['Your local theme was packaged in', {filePath: expectedOutputZipPath}],
})
expect(result).toEqual({path: expectedOutputZipPath})
expect(renderSuccess).not.toHaveBeenCalled()
})
})

Expand All @@ -105,7 +103,7 @@ describe('packageTheme', () => {
await createSettingsSchema('[{"name": "theme_info", "theme_name": "Dawn"}]', inputDirectory)

// When
await packageTheme(inputDirectory)
const result = await packageTheme(inputDirectory)

// Then
const expectedOutputZipPath = joinPath(inputDirectory, 'Dawn.zip')
Expand All @@ -114,9 +112,8 @@ describe('packageTheme', () => {
const archiveEntries = await readArchiveFiles(expectedOutputZipPath)
expect(themeRelativePaths.sort()).toEqual(archiveEntries.sort())

expect(renderSuccess).toBeCalledWith({
body: ['Your local theme was packaged in', {filePath: expectedOutputZipPath}],
})
expect(result).toEqual({path: expectedOutputZipPath})
expect(renderSuccess).not.toHaveBeenCalled()
})
})

Expand Down
10 changes: 4 additions & 6 deletions packages/theme/src/cli/services/package.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {type ThemePackageResult} from './package/types.js'
import {zip} from '@shopify/cli-kit/node/archiver'
import {fileExists, readFile} from '@shopify/cli-kit/node/fs'
import {AbortError} from '@shopify/cli-kit/node/error'
import {renderSuccess} from '@shopify/cli-kit/node/ui'
import {resolvePath, relativizePath} from '@shopify/cli-kit/node/path'
import {resolvePath} from '@shopify/cli-kit/node/path'
import {parseJSON} from '@shopify/theme-check-node'

const themeFilesPattern = [
Expand All @@ -23,7 +23,7 @@ const themeFilesPattern = [
].join('|')

// package is a reserved word so the function needs to be named packageTheme
export async function packageTheme(inputDirectory: string) {
export async function packageTheme(inputDirectory: string): Promise<ThemePackageResult> {
const packageName = await getThemePackageName(inputDirectory)

const outputZipPath = `${inputDirectory}/${packageName}`
Expand All @@ -35,9 +35,7 @@ export async function packageTheme(inputDirectory: string) {
matchFilePattern,
})

renderSuccess({
body: ['Your local theme was packaged in', {filePath: relativizePath(outputZipPath)}],
})
return {path: outputZipPath}
}

async function getThemePackageName(inputDirectory: string) {
Expand Down
13 changes: 13 additions & 0 deletions packages/theme/src/cli/services/package/result.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {themePackageJsonOutputSchema, type ThemePackageResult} from './types.js'
import {outputResult} from '@shopify/cli-kit/node/output'
import {relativizePath} from '@shopify/cli-kit/node/path'
import {renderSuccess} from '@shopify/cli-kit/node/ui'

export function renderThemePackageResult(result: ThemePackageResult, format: 'text' | 'json'): void {
if (format === 'json') {
outputResult(themePackageJsonOutputSchema.encode(result))
return
}

renderSuccess({body: ['Your local theme was packaged in', {filePath: relativizePath(result.path)}]})
}
9 changes: 9 additions & 0 deletions packages/theme/src/cli/services/package/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import {defineJsonOutputSchema, type InferJsonOutputSchema} from '@shopify/cli-kit/node/json-output-schema'
import {zod} from '@shopify/cli-kit/node/schema'

export const themePackageJsonOutputSchema = defineJsonOutputSchema({
name: 'ThemePackageResult',
schema: zod.object({path: zod.string()}),
})

export type ThemePackageResult = InferJsonOutputSchema<typeof themePackageJsonOutputSchema>
Loading