From a32c4f88fc3a268e3a67c3169f4a8202a0ede97f Mon Sep 17 00:00:00 2001 From: James Meng Date: Wed, 11 Dec 2024 17:26:45 -0800 Subject: [PATCH] Validate that generate path points to a valid theme directories --- packages/cli/oclif.manifest.json | 27 +++++++++++++ .../cli/commands/theme/generate/block.test.ts | 39 +++++++++++++++++++ .../src/cli/commands/theme/generate/block.ts | 18 ++++++++- .../commands/theme/generate/section.test.ts | 39 +++++++++++++++++++ .../cli/commands/theme/generate/section.ts | 18 ++++++++- .../commands/theme/generate/template.test.ts | 39 +++++++++++++++++++ .../cli/commands/theme/generate/template.ts | 18 ++++++++- 7 files changed, 195 insertions(+), 3 deletions(-) create mode 100644 packages/theme/src/cli/commands/theme/generate/block.test.ts create mode 100644 packages/theme/src/cli/commands/theme/generate/section.test.ts create mode 100644 packages/theme/src/cli/commands/theme/generate/template.test.ts diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index b060ac80dc..4727bafe9e 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -5326,6 +5326,15 @@ "description": "Creates a new \"theme block\" (https://shopify.dev/docs/themes/architecture/blocks) in your local theme directory.\n\n The block is created in the `blocks` directory with the basic structure needed, including schema and settings.\n\n You can specify the type of block to generate using the `--type` flag. The block will be created with appropriate default settings based on the type.", "descriptionWithMarkdown": "Creates a new [theme block](https://shopify.dev/docs/themes/architecture/blocks) in your local theme directory.\n\n The block is created in the `blocks` directory with the basic structure needed, including schema and settings.\n\n You can specify the type of block to generate using the `--type` flag. The block will be created with appropriate default settings based on the type.", "flags": { + "force": { + "allowNo": false, + "char": "f", + "description": "Proceed without confirmation, if current directory does not seem to be theme directory.", + "env": "SHOPIFY_FLAG_FORCE", + "hidden": true, + "name": "force", + "type": "boolean" + }, "name": { "char": "n", "description": "Name of the block", @@ -5396,6 +5405,15 @@ "description": "Creates a new \"theme section\" (https://shopify.dev/docs/themes/architecture/sections) in your local theme directory.\n\n The section is created in the `sections` directory with the basic structure needed, including schema, settings, and blocks.\n\n You can specify the type of section to generate using the `--type` flag. The section will be created with appropriate default settings and blocks based on the type.", "descriptionWithMarkdown": "Creates a new [theme section](https://shopify.dev/docs/themes/architecture/sections) in your local theme directory.\n\n The section is created in the `sections` directory with the basic structure needed, including schema, settings, and blocks.\n\n You can specify the type of section to generate using the `--type` flag. The section will be created with appropriate default settings and blocks based on the type.", "flags": { + "force": { + "allowNo": false, + "char": "f", + "description": "Proceed without confirmation, if current directory does not seem to be theme directory.", + "env": "SHOPIFY_FLAG_FORCE", + "hidden": true, + "name": "force", + "type": "boolean" + }, "name": { "char": "n", "description": "Name of the section", @@ -5465,6 +5483,15 @@ "description": "Creates a new \"theme template\" (https://shopify.dev/docs/themes/architecture/templates) in your local theme directory.\n\n The template is created in the `templates` directory with the basic structure needed, including layout and content sections.\n\n You can specify the type of template to generate using the `--type` flag. The template will be created with appropriate default sections based on the type.", "descriptionWithMarkdown": "Creates a new [theme template](https://shopify.dev/docs/themes/architecture/templates) in your local theme directory.\n\n The template is created in the `templates` directory with the basic structure needed, including layout and content sections.\n\n You can specify the type of template to generate using the `--type` flag. The template will be created with appropriate default sections based on the type.", "flags": { + "force": { + "allowNo": false, + "char": "f", + "description": "Proceed without confirmation, if current directory does not seem to be theme directory.", + "env": "SHOPIFY_FLAG_FORCE", + "hidden": true, + "name": "force", + "type": "boolean" + }, "name": { "char": "n", "description": "Name of the template", diff --git a/packages/theme/src/cli/commands/theme/generate/block.test.ts b/packages/theme/src/cli/commands/theme/generate/block.test.ts new file mode 100644 index 0000000000..000b841d07 --- /dev/null +++ b/packages/theme/src/cli/commands/theme/generate/block.test.ts @@ -0,0 +1,39 @@ +import GenerateBlock from './block.js' +import {hasRequiredThemeDirectories} from '../../../utilities/theme-fs.js' +import {describe, expect, test, vi} from 'vitest' +import {renderWarning} from '@shopify/cli-kit/node/ui' +import {cwd} from '@shopify/cli-kit/node/path' + +vi.mock('../../../utilities/theme-fs.js') +vi.mock('@shopify/cli-kit/node/ui') + +describe('GenerateBlock', () => { + const path = cwd() + test('validates theme directory structure by default', async () => { + // Given + vi.mocked(hasRequiredThemeDirectories).mockResolvedValue(false) + + // When + await GenerateBlock.run(['--path', path]) + + // Then + expect(hasRequiredThemeDirectories).toHaveBeenCalledWith(path) + expect(renderWarning).toHaveBeenCalledWith({ + body: [ + 'The current directory does not contain the required theme directories (config, layout, sections, templates).', + ], + }) + }) + + test('skips directory validation when force flag is used', async () => { + // Given + vi.mocked(hasRequiredThemeDirectories).mockResolvedValue(false) + + // When + await GenerateBlock.run(['--path', path, '--force']) + + // Then + expect(hasRequiredThemeDirectories).not.toHaveBeenCalled() + expect(renderWarning).not.toHaveBeenCalled() + }) +}) diff --git a/packages/theme/src/cli/commands/theme/generate/block.ts b/packages/theme/src/cli/commands/theme/generate/block.ts index 1da9847320..5c42fbf70d 100644 --- a/packages/theme/src/cli/commands/theme/generate/block.ts +++ b/packages/theme/src/cli/commands/theme/generate/block.ts @@ -1,8 +1,9 @@ import {themeFlags} from '../../../flags.js' import ThemeCommand from '../../../utilities/theme-command.js' +import {hasRequiredThemeDirectories} from '../../../utilities/theme-fs.js' import {Flags} from '@oclif/core' import {globalFlags} from '@shopify/cli-kit/node/cli' -import {renderSelectPrompt, renderSuccess, renderTextPrompt} from '@shopify/cli-kit/node/ui' +import {renderSelectPrompt, renderSuccess, renderTextPrompt, renderWarning} from '@shopify/cli-kit/node/ui' const BLOCK_TYPES = ['text', 'image', 'video', 'product', 'collection'] @@ -31,11 +32,26 @@ export default class GenerateBlock extends ThemeCommand { options: [...BLOCK_TYPES], env: 'SHOPIFY_FLAG_BLOCK_TYPE', }), + force: Flags.boolean({ + hidden: true, + char: 'f', + description: 'Proceed without confirmation, if current directory does not seem to be theme directory.', + env: 'SHOPIFY_FLAG_FORCE', + }), } async run(): Promise { const {flags} = await this.parse(GenerateBlock) + if (!flags.force && !(await hasRequiredThemeDirectories(flags.path))) { + renderWarning({ + body: [ + 'The current directory does not contain the required theme directories (config, layout, sections, templates).', + ], + }) + return + } + const name = flags.name ?? (await renderTextPrompt({ diff --git a/packages/theme/src/cli/commands/theme/generate/section.test.ts b/packages/theme/src/cli/commands/theme/generate/section.test.ts new file mode 100644 index 0000000000..52c93ba77b --- /dev/null +++ b/packages/theme/src/cli/commands/theme/generate/section.test.ts @@ -0,0 +1,39 @@ +import GenerateSection from './section.js' +import {hasRequiredThemeDirectories} from '../../../utilities/theme-fs.js' +import {describe, expect, test, vi} from 'vitest' +import {renderWarning} from '@shopify/cli-kit/node/ui' +import {cwd} from '@shopify/cli-kit/node/path' + +vi.mock('../../../utilities/theme-fs.js') +vi.mock('@shopify/cli-kit/node/ui') + +describe('GenerateSection', () => { + const path = cwd() + test('validates theme directory structure by default', async () => { + // Given + vi.mocked(hasRequiredThemeDirectories).mockResolvedValue(false) + + // When + await GenerateSection.run(['--path', path]) + + // Then + expect(hasRequiredThemeDirectories).toHaveBeenCalledWith(path) + expect(renderWarning).toHaveBeenCalledWith({ + body: [ + 'The current directory does not contain the required theme directories (config, layout, sections, templates).', + ], + }) + }) + + test('skips directory validation when force flag is used', async () => { + // Given + vi.mocked(hasRequiredThemeDirectories).mockResolvedValue(false) + + // When + await GenerateSection.run(['--path', path, '--force']) + + // Then + expect(hasRequiredThemeDirectories).not.toHaveBeenCalled() + expect(renderWarning).not.toHaveBeenCalled() + }) +}) diff --git a/packages/theme/src/cli/commands/theme/generate/section.ts b/packages/theme/src/cli/commands/theme/generate/section.ts index 0e0f3021f8..0e09fa2578 100644 --- a/packages/theme/src/cli/commands/theme/generate/section.ts +++ b/packages/theme/src/cli/commands/theme/generate/section.ts @@ -1,8 +1,9 @@ import {themeFlags} from '../../../flags.js' import ThemeCommand from '../../../utilities/theme-command.js' +import {hasRequiredThemeDirectories} from '../../../utilities/theme-fs.js' import {Flags} from '@oclif/core' import {globalFlags} from '@shopify/cli-kit/node/cli' -import {renderSelectPrompt, renderSuccess, renderTextPrompt} from '@shopify/cli-kit/node/ui' +import {renderSelectPrompt, renderSuccess, renderTextPrompt, renderWarning} from '@shopify/cli-kit/node/ui' const SECTION_TYPES = ['featured-collection', 'image-with-text', 'rich-text', 'custom'] @@ -31,11 +32,26 @@ export default class GenerateSection extends ThemeCommand { options: [...SECTION_TYPES], env: 'SHOPIFY_FLAG_SECTION_TYPE', }), + force: Flags.boolean({ + hidden: true, + char: 'f', + description: 'Proceed without confirmation, if current directory does not seem to be theme directory.', + env: 'SHOPIFY_FLAG_FORCE', + }), } async run(): Promise { const {flags} = await this.parse(GenerateSection) + if (!flags.force && !(await hasRequiredThemeDirectories(flags.path))) { + renderWarning({ + body: [ + 'The current directory does not contain the required theme directories (config, layout, sections, templates).', + ], + }) + return + } + const name = flags.name ?? (await renderTextPrompt({ diff --git a/packages/theme/src/cli/commands/theme/generate/template.test.ts b/packages/theme/src/cli/commands/theme/generate/template.test.ts new file mode 100644 index 0000000000..f21540b733 --- /dev/null +++ b/packages/theme/src/cli/commands/theme/generate/template.test.ts @@ -0,0 +1,39 @@ +import GenerateTemplate from './template.js' +import {hasRequiredThemeDirectories} from '../../../utilities/theme-fs.js' +import {describe, expect, test, vi} from 'vitest' +import {renderWarning} from '@shopify/cli-kit/node/ui' +import {cwd} from '@shopify/cli-kit/node/path' + +vi.mock('../../../utilities/theme-fs.js') +vi.mock('@shopify/cli-kit/node/ui') + +describe('GenerateTemplate', () => { + const path = cwd() + test('validates theme directory structure by default', async () => { + // Given + vi.mocked(hasRequiredThemeDirectories).mockResolvedValue(false) + + // When + await GenerateTemplate.run(['--path', path]) + + // Then + expect(hasRequiredThemeDirectories).toHaveBeenCalledWith(path) + expect(renderWarning).toHaveBeenCalledWith({ + body: [ + 'The current directory does not contain the required theme directories (config, layout, sections, templates).', + ], + }) + }) + + test('skips directory validation when force flag is used', async () => { + // Given + vi.mocked(hasRequiredThemeDirectories).mockResolvedValue(false) + + // When + await GenerateTemplate.run(['--path', path, '--force']) + + // Then + expect(hasRequiredThemeDirectories).not.toHaveBeenCalled() + expect(renderWarning).not.toHaveBeenCalled() + }) +}) diff --git a/packages/theme/src/cli/commands/theme/generate/template.ts b/packages/theme/src/cli/commands/theme/generate/template.ts index 4eb1b3b5d3..d23e6ad9d5 100644 --- a/packages/theme/src/cli/commands/theme/generate/template.ts +++ b/packages/theme/src/cli/commands/theme/generate/template.ts @@ -1,9 +1,10 @@ import {TEMPLATE_TYPES, promptForType} from '../../../utilities/generator.js' import {themeFlags} from '../../../flags.js' import ThemeCommand from '../../../utilities/theme-command.js' +import {hasRequiredThemeDirectories} from '../../../utilities/theme-fs.js' import {Flags} from '@oclif/core' import {globalFlags} from '@shopify/cli-kit/node/cli' -import {renderSuccess, renderTextPrompt} from '@shopify/cli-kit/node/ui' +import {renderSuccess, renderTextPrompt, renderWarning} from '@shopify/cli-kit/node/ui' export default class GenerateTemplate extends ThemeCommand { static summary = 'Creates and adds a new template file to your local theme directory' @@ -30,11 +31,26 @@ export default class GenerateTemplate extends ThemeCommand { options: [...TEMPLATE_TYPES], env: 'SHOPIFY_FLAG_TEMPLATE_TYPE', }), + force: Flags.boolean({ + hidden: true, + char: 'f', + description: 'Proceed without confirmation, if current directory does not seem to be theme directory.', + env: 'SHOPIFY_FLAG_FORCE', + }), } async run(): Promise { const {flags} = await this.parse(GenerateTemplate) + if (!flags.force && !(await hasRequiredThemeDirectories(flags.path))) { + renderWarning({ + body: [ + 'The current directory does not contain the required theme directories (config, layout, sections, templates).', + ], + }) + return + } + const name = flags.name ?? (await renderTextPrompt({