Skip to content

Commit d0c1dce

Browse files
Merge pull request #7335 from Shopify/rp/fix-theme-check-path-file-validation
fix: validate --path flag is a directory, not a file
2 parents 975bf1a + 92bb1b4 commit d0c1dce

3 files changed

Lines changed: 42 additions & 15 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@shopify/theme': patch
3+
---
4+
5+
Fix `ENOTDIR` error when a file path is passed to `--path` flag in theme commands. The flag now validates that the provided path is a directory and shows a helpful error message suggesting the parent directory instead.

packages/theme/src/cli/flags.test.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import {themeFlags} from './flags.js'
22
import {describe, expect, test} from 'vitest'
33
import Command from '@shopify/cli-kit/node/base-command'
4-
import {inTemporaryDirectory} from '@shopify/cli-kit/node/fs'
5-
import {cwd, resolvePath} from '@shopify/cli-kit/node/path'
4+
import {inTemporaryDirectory, writeFileSync} from '@shopify/cli-kit/node/fs'
5+
import {cwd, joinPath, resolvePath} from '@shopify/cli-kit/node/path'
66
import {mockAndCaptureOutput} from '@shopify/cli-kit/node/testing/output'
77

88
class MockCommand extends Command {
@@ -26,20 +26,34 @@ describe('themeFlags', () => {
2626
expect(flags.path).toEqual(cwd())
2727
})
2828

29-
test('can be expclitly provided', async () => {
29+
test('can be explicitly provided', async () => {
3030
await inTemporaryDirectory(async (tmpDir) => {
3131
const flags = await MockCommand.run(['--path', tmpDir])
3232

3333
expect(flags.path).toEqual(resolvePath(tmpDir))
3434
})
3535
})
3636

37-
test("renders an error message and exists when the path doesn't exist", async () => {
37+
test("renders an error message and exits when the path doesn't exist", async () => {
3838
const mockOutput = mockAndCaptureOutput()
3939

4040
await MockCommand.run(['--path', 'boom'])
4141

4242
expect(mockOutput.error()).toMatch("A path was explicitly provided but doesn't exist")
4343
})
44+
45+
test('renders an error message and exits when the path is a file instead of a directory', async () => {
46+
await inTemporaryDirectory(async (tmpDir) => {
47+
const filePath = joinPath(tmpDir, 'section.liquid')
48+
writeFileSync(filePath, '{% schema %}{% endschema %}')
49+
50+
const mockOutput = mockAndCaptureOutput()
51+
52+
await MockCommand.run(['--path', filePath])
53+
54+
expect(mockOutput.error()).toMatch('The path must be a directory, not a file')
55+
expect(mockOutput.error()).toMatch('section.liquid')
56+
})
57+
})
4458
})
4559
})

packages/theme/src/cli/flags.ts

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {Flags} from '@oclif/core'
22
import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn'
3-
import {resolvePath, cwd} from '@shopify/cli-kit/node/path'
4-
import {fileExistsSync} from '@shopify/cli-kit/node/fs'
3+
import {resolvePath, cwd, dirname} from '@shopify/cli-kit/node/path'
4+
import {fileExistsSync, isDirectorySync} from '@shopify/cli-kit/node/fs'
55
import {renderError} from '@shopify/cli-kit/node/ui'
66

77
/**
@@ -15,17 +15,25 @@ export const themeFlags = {
1515
parse: async (input) => {
1616
const resolvedPath = resolvePath(input)
1717

18-
if (fileExistsSync(resolvedPath)) {
19-
return resolvedPath
18+
if (!fileExistsSync(resolvedPath)) {
19+
// We can't use AbortError because oclif catches it and adds its own
20+
// messaging that breaks our UI
21+
renderError({
22+
headline: "A path was explicitly provided but doesn't exist.",
23+
body: [`Please check the path and try again: ${resolvedPath}`],
24+
})
25+
return process.exit(1)
2026
}
2127

22-
// We can't use AbortError because oclif catches it and adds its own
23-
// messaging that breaks our UI
24-
renderError({
25-
headline: "A path was explicitly provided but doesn't exist.",
26-
body: [`Please check the path and try again: ${resolvedPath}`],
27-
})
28-
process.exit(1)
28+
if (!isDirectorySync(resolvedPath)) {
29+
renderError({
30+
headline: 'The path must be a directory, not a file.',
31+
body: [`The provided path is not a directory: ${resolvedPath}`, `Did you mean: ${dirname(resolvedPath)}`],
32+
})
33+
return process.exit(1)
34+
}
35+
36+
return resolvedPath
2937
},
3038
default: async () => cwd(),
3139
noCacheDefault: true,

0 commit comments

Comments
 (0)