Skip to content
Merged
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/theme-check-config-directory.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/theme': patch
---

Report a directory passed to `theme check --config` as an expected user error instead of crashing with EISDIR.
4 changes: 2 additions & 2 deletions docs-shopify.dev/generated/generated_docs_data_v2.json
Original file line number Diff line number Diff line change
Expand Up @@ -8356,7 +8356,7 @@
"syntaxKind": "PropertySignature",
"name": "-C, --config <value>",
"value": "string",
"description": "Use the config provided, overriding .theme-check.yml if present Supports all theme-check: config values, e.g., theme-check:theme-app-extension,\n theme-check:recommended, theme-check:all\n For backwards compatibility, :theme_app_extension is also supported",
"description": "Path to a config file to use, overriding .theme-check.yml if present Use --path to set the theme directory\n Supports all theme-check: config values, e.g., theme-check:theme-app-extension,\n theme-check:recommended, theme-check:all\n For backwards compatibility, :theme_app_extension is also supported",
"isOptional": true,
"environmentValue": "SHOPIFY_FLAG_CONFIG"
},
Expand Down Expand Up @@ -8397,7 +8397,7 @@
"environmentValue": "SHOPIFY_FLAG_VERSION"
}
],
"value": "export interface themecheck {\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 * Automatically fix offenses\n * @environment SHOPIFY_FLAG_AUTO_CORRECT\n */\n '-a, --auto-correct'?: ''\n\n /**\n * Use the config provided, overriding .theme-check.yml if present\n Supports all theme-check: config values, e.g., theme-check:theme-app-extension,\n theme-check:recommended, theme-check:all\n For backwards compatibility, :theme_app_extension is also supported \n * @environment SHOPIFY_FLAG_CONFIG\n */\n '-C, --config <value>'?: string\n\n /**\n * The environment to apply to the current command.\n * @environment SHOPIFY_FLAG_ENVIRONMENT\n */\n '-e, --environment <value>'?: string\n\n /**\n * Minimum severity for exit with error code\n * @environment SHOPIFY_FLAG_FAIL_LEVEL\n */\n '--fail-level <value>'?: string\n\n /**\n * Generate a .theme-check.yml file\n * @environment SHOPIFY_FLAG_INIT\n */\n '--init'?: ''\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 * List enabled checks\n * @environment SHOPIFY_FLAG_LIST\n */\n '--list'?: ''\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 output format to use\n * @environment SHOPIFY_FLAG_OUTPUT\n */\n '-o, --output <value>'?: string\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 * Output active config to STDOUT\n * @environment SHOPIFY_FLAG_PRINT\n */\n '--print'?: ''\n\n /**\n * Increase the verbosity of the output. May include sensitive data.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n\n /**\n * Print Theme Check version\n * @environment SHOPIFY_FLAG_VERSION\n */\n '-v, --version'?: ''\n}"
"value": "export interface themecheck {\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 * Automatically fix offenses\n * @environment SHOPIFY_FLAG_AUTO_CORRECT\n */\n '-a, --auto-correct'?: ''\n\n /**\n * Path to a config file to use, overriding .theme-check.yml if present\n Use --path to set the theme directory\n Supports all theme-check: config values, e.g., theme-check:theme-app-extension,\n theme-check:recommended, theme-check:all\n For backwards compatibility, :theme_app_extension is also supported \n * @environment SHOPIFY_FLAG_CONFIG\n */\n '-C, --config <value>'?: string\n\n /**\n * The environment to apply to the current command.\n * @environment SHOPIFY_FLAG_ENVIRONMENT\n */\n '-e, --environment <value>'?: string\n\n /**\n * Minimum severity for exit with error code\n * @environment SHOPIFY_FLAG_FAIL_LEVEL\n */\n '--fail-level <value>'?: string\n\n /**\n * Generate a .theme-check.yml file\n * @environment SHOPIFY_FLAG_INIT\n */\n '--init'?: ''\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 * List enabled checks\n * @environment SHOPIFY_FLAG_LIST\n */\n '--list'?: ''\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 output format to use\n * @environment SHOPIFY_FLAG_OUTPUT\n */\n '-o, --output <value>'?: string\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 * Output active config to STDOUT\n * @environment SHOPIFY_FLAG_PRINT\n */\n '--print'?: ''\n\n /**\n * Increase the verbosity of the output. May include sensitive data.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n\n /**\n * Print Theme Check version\n * @environment SHOPIFY_FLAG_VERSION\n */\n '-v, --version'?: ''\n}"
}
},
"themeconsole": {
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12404,7 +12404,8 @@ USAGE

FLAGS
-C, --config=<value>
Use the config provided, overriding .theme-check.yml if present
Path to a config file to use, overriding .theme-check.yml if present
Use --path to set the theme directory
Supports all theme-check: config values, e.g., theme-check:theme-app-extension,
theme-check:recommended, theme-check:all
For backwards compatibility, :theme_app_extension is also supported
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/oclif.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -10512,7 +10512,7 @@
},
"config": {
"char": "C",
"description": "Use the config provided, overriding .theme-check.yml if present\n Supports all theme-check: config values, e.g., theme-check:theme-app-extension,\n theme-check:recommended, theme-check:all\n For backwards compatibility, :theme_app_extension is also supported ",
"description": "Path to a config file to use, overriding .theme-check.yml if present\n Use --path to set the theme directory\n Supports all theme-check: config values, e.g., theme-check:theme-app-extension,\n theme-check:recommended, theme-check:all\n For backwards compatibility, :theme_app_extension is also supported ",
"env": "SHOPIFY_FLAG_CONFIG",
"hasDynamicHelp": false,
"multiple": false,
Expand Down
28 changes: 27 additions & 1 deletion packages/theme/src/cli/commands/theme/check.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import {describe, vi, expect, test, beforeEach} from 'vitest'
import {Config} from '@oclif/core'
import {themeCheckRun, Theme, Config as ThemeConfig, Offense} from '@shopify/theme-check-node'
import {AbortError} from '@shopify/cli-kit/node/error'
import {inTemporaryDirectory} from '@shopify/cli-kit/node/fs'
import {inTemporaryDirectory, mkdir, symlink} from '@shopify/cli-kit/node/fs'
import {joinPath} from '@shopify/cli-kit/node/path'

vi.mock('@shopify/theme-check-node')
const CommandConfig = new Config({root: __dirname})
Expand Down Expand Up @@ -96,6 +97,31 @@ describe('Check', () => {
await expect(result).rejects.toThrowError(`Theme Check config file not found: ${config}`)
})

test('reports a directory passed as the config as an expected error', async () => {
await inTemporaryDirectory(async (directory) => {
const result = runThemeCheck(path, 'text', directory)

await expect(result).rejects.toBeInstanceOf(AbortError)
await expect(result).rejects.toThrowError(`Theme Check config must be a file, but ${directory} is a directory.`)
expect(themeCheckRun).not.toHaveBeenCalled()
})
})

test('reports a symlink to a directory passed as the config as an expected error', async () => {
await inTemporaryDirectory(async (directory) => {
const configDirectory = joinPath(directory, 'config')
const config = joinPath(directory, 'config-link')
await mkdir(configDirectory)
await symlink(configDirectory, config)

const result = runThemeCheck(path, 'text', config)

await expect(result).rejects.toBeInstanceOf(AbortError)
await expect(result).rejects.toThrowError(`Theme Check config must be a file, but ${config} is a directory.`)
expect(themeCheckRun).not.toHaveBeenCalled()
})
})

test('does not treat a missing extended config as a missing explicitly configured file', async () => {
const config = '/my-theme/.theme-check.yml'
const missingExtendedConfigError = Object.assign(new Error('ENOENT'), {
Expand Down
3 changes: 2 additions & 1 deletion packages/theme/src/cli/commands/theme/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ export default class Check extends ThemeCommand {
config: Flags.string({
char: 'C',
required: false,
description: `Use the config provided, overriding .theme-check.yml if present
description: `Path to a config file to use, overriding .theme-check.yml if present
Use --path to set the theme directory
Supports all theme-check: config values, e.g., theme-check:theme-app-extension,
theme-check:recommended, theme-check:all
For backwards compatibility, :theme_app_extension is also supported `,
Expand Down
48 changes: 47 additions & 1 deletion packages/theme/src/cli/services/check.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
renderOffensesText,
sortOffenses,
} from './check.js'
import {fileExists, writeFile} from '@shopify/cli-kit/node/fs'
import {fileExists, isDirectory, writeFile} from '@shopify/cli-kit/node/fs'
import {AbortError} from '@shopify/cli-kit/node/error'
import {outputInfo, outputSuccess} from '@shopify/cli-kit/node/output'
import {renderInfo} from '@shopify/cli-kit/node/ui'
Expand All @@ -25,6 +25,8 @@ import {beforeEach, describe, expect, test, vi, type Mock} from 'vitest'

vi.mock('@shopify/cli-kit/node/fs', async () => ({
fileExists: vi.fn(),
fileRealPath: vi.fn(async (path: string) => path),
isDirectory: vi.fn(),
writeFile: vi.fn(),
}))

Expand Down Expand Up @@ -449,6 +451,28 @@ describe('outputActiveConfig', () => {
await expect(result).rejects.toBeInstanceOf(AbortError)
await expect(result).rejects.toThrowError(`Theme Check config file not found: ${configPath}`)
})

test('reports a directory passed as the config as an expected error', async () => {
vi.mocked(fileExists).mockResolvedValue(true)
vi.mocked(isDirectory).mockResolvedValue(true)

const result = outputActiveConfig('/my-theme', '.')

await expect(result).rejects.toBeInstanceOf(AbortError)
await expect(result).rejects.toThrowError('Theme Check config must be a file, but . is a directory.')
expect(loadConfig).not.toHaveBeenCalled()
})

test('does not check whether modern identifiers are directories', async () => {
vi.mocked(fileExists).mockResolvedValue(true)
vi.mocked(isDirectory).mockResolvedValue(true)
vi.mocked(loadConfig).mockResolvedValue({settings: {}, ignore: []} as any)

await outputActiveConfig('/my-theme', 'theme-check:recommended')

expect(isDirectory).not.toHaveBeenCalled()
expect(loadConfig).toHaveBeenCalledWith('theme-check:recommended', '/my-theme')
})
})

describe('outputActiveChecks', () => {
Expand All @@ -465,4 +489,26 @@ describe('outputActiveChecks', () => {
await expect(result).rejects.toBeInstanceOf(AbortError)
await expect(result).rejects.toThrowError(`Theme Check config file not found: ${configPath}`)
})

test('reports a directory passed as the config as an expected error', async () => {
vi.mocked(fileExists).mockResolvedValue(true)
vi.mocked(isDirectory).mockResolvedValue(true)

const result = outputActiveChecks('/my-theme', '.')

await expect(result).rejects.toBeInstanceOf(AbortError)
await expect(result).rejects.toThrowError('Theme Check config must be a file, but . is a directory.')
expect(loadConfig).not.toHaveBeenCalled()
})

test('does not check whether modern identifiers are directories', async () => {
vi.mocked(fileExists).mockResolvedValue(true)
vi.mocked(isDirectory).mockResolvedValue(true)
vi.mocked(loadConfig).mockResolvedValue({settings: {}, ignore: [], checks: []} as any)

await outputActiveChecks('/my-theme', 'theme-check:recommended')

expect(isDirectory).not.toHaveBeenCalled()
expect(loadConfig).toHaveBeenCalledWith('theme-check:recommended', '/my-theme')
})
})
21 changes: 20 additions & 1 deletion packages/theme/src/cli/services/check.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {ThemeCheckResult} from './check/types.js'
import {withThemeCheckDiagnostics} from './check/diagnostics.js'
import {fileExists, writeFile} from '@shopify/cli-kit/node/fs'
import {fileExists, fileRealPath, isDirectory, writeFile} from '@shopify/cli-kit/node/fs'
import {outputResult, outputInfo, outputSuccess, outputDebug} from '@shopify/cli-kit/node/output'
import {joinPath, resolvePath} from '@shopify/cli-kit/node/path'
import {renderInfo} from '@shopify/cli-kit/node/ui'
Expand All @@ -12,6 +12,7 @@ import {
applyFixToString,
autofix,
loadConfig,
ModernIdentifiers,
type FixApplicator,
type Offense,
type Theme,
Expand Down Expand Up @@ -43,10 +44,28 @@ function isMissingThemeCheckConfigFile(error: unknown, configPath: string) {
)
}

/**
* Theme Check reads the config with `readFile`, which fails with an EISDIR error that often has no `path`,
* so a directory is detected before loading instead of being matched after the failure.
*/
async function assertThemeCheckConfigIsNotDirectory(configPath: string | undefined) {
if (configPath === undefined || ModernIdentifiers.some((identifier) => identifier === configPath)) return
// isDirectory throws on a missing path, so missing files are left to the not-found handling.
if (!(await fileExists(configPath))) return
// isDirectory doesn't follow symlinks, so resolve them first.
if (!(await isDirectory(await fileRealPath(configPath)))) return

throw new AbortError(
`Theme Check config must be a file, but ${configPath} is a directory.`,
'Pass the path to a .theme-check.yml file to --config, or omit --config to use the .theme-check.yml in your theme root. To choose the theme directory, use --path.',
)
}

async function withThemeCheckConfigErrorHandling<T>(
configPath: string | undefined,
operation: () => Promise<T>,
): Promise<T> {
await assertThemeCheckConfigIsNotDirectory(configPath)
try {
return await operation()
} catch (error) {
Expand Down
Loading