Skip to content

Commit 70f379b

Browse files
ryancbahanclaude
andcommitted
Extract extension_directories transforms from Zod schema
Move removeTrailingPathSeparator and fixSingleWildcards out of ExtensionDirectoriesSchema so parsed config preserves the raw TOML value. Consumers (loader, file-watcher) now call the explicit normalizeExtensionDirectories() helper at point of use. Phase 2 of the config model cleanup — extension_directories is a CLI-only field with no write-path or API impact. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 29d8b4d commit 70f379b

4 files changed

Lines changed: 40 additions & 26 deletions

File tree

packages/app/src/cli/models/app/app.test.ts

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
getUIExtensionRendererVersion,
1111
isCurrentAppSchema,
1212
isLegacyAppSchema,
13+
normalizeExtensionDirectories,
1314
validateExtensionsHandlesInCollection,
1415
validateFunctionExtensionsWithUiHandle,
1516
} from './app.js'
@@ -99,32 +100,37 @@ describe('app schema validation', () => {
99100
expect(isCurrentAppSchema(config)).toBe(false)
100101
})
101102

102-
test('extension_directories should be transformed to double asterisks', () => {
103+
test('extension_directories preserves raw values without transformation', () => {
103104
const config = {
104105
...CORRECT_CURRENT_APP_SCHEMA,
105106
extension_directories: ['extensions/*'],
106107
}
107108
const parsed = AppSchema.parse(config)
108-
expect(parsed.extension_directories).toEqual(['extensions/**'])
109+
expect(parsed.extension_directories).toEqual(['extensions/*'])
109110
})
111+
})
112+
})
110113

111-
test('extension_directories is not transformed if it ends with double asterisks', () => {
112-
const config = {
113-
...CORRECT_CURRENT_APP_SCHEMA,
114-
extension_directories: ['extensions/**'],
115-
}
116-
const parsed = AppSchema.parse(config)
117-
expect(parsed.extension_directories).toEqual(['extensions/**'])
118-
})
114+
describe('normalizeExtensionDirectories', () => {
115+
test('converts single trailing wildcard to double asterisks', () => {
116+
expect(normalizeExtensionDirectories(['extensions/*'])).toEqual(['extensions/**'])
117+
})
119118

120-
test('extension_directories is not transformed if it doesnt end with a wildcard', () => {
121-
const config = {
122-
...CORRECT_CURRENT_APP_SCHEMA,
123-
extension_directories: ['extensions'],
124-
}
125-
const parsed = AppSchema.parse(config)
126-
expect(parsed.extension_directories).toEqual(['extensions'])
127-
})
119+
test('preserves double asterisks', () => {
120+
expect(normalizeExtensionDirectories(['extensions/**'])).toEqual(['extensions/**'])
121+
})
122+
123+
test('does not transform paths without wildcards', () => {
124+
expect(normalizeExtensionDirectories(['extensions'])).toEqual(['extensions'])
125+
})
126+
127+
test('strips trailing path separators before fixing wildcards', () => {
128+
expect(normalizeExtensionDirectories(['extensions/'])).toEqual(['extensions'])
129+
expect(normalizeExtensionDirectories(['extensions\\'])).toEqual(['extensions'])
130+
})
131+
132+
test('returns undefined for undefined input', () => {
133+
expect(normalizeExtensionDirectories(undefined)).toBeUndefined()
128134
})
129135
})
130136

packages/app/src/cli/models/app/app.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,7 @@ import {deepMergeObjects} from '@shopify/cli-kit/common/object'
3434

3535
// Schemas for loading app configuration
3636

37-
const ExtensionDirectoriesSchema = zod
38-
.array(zod.string())
39-
.optional()
40-
.transform(removeTrailingPathSeparator)
41-
.transform(fixSingleWildcards)
37+
const ExtensionDirectoriesSchema = zod.array(zod.string()).optional()
4238

4339
/**
4440
* Schema for a freshly minted app template.
@@ -74,6 +70,14 @@ function fixSingleWildcards(value: string[] | undefined) {
7470
return value?.map((dir) => dir.replace(/([^\*])\*$/, '$1**'))
7571
}
7672

73+
/**
74+
* Normalize extension directories for glob/chokidar consumption:
75+
* strips trailing path separators, then converts single trailing `*` to `**`.
76+
*/
77+
export function normalizeExtensionDirectories(dirs: string[] | undefined): string[] | undefined {
78+
return fixSingleWildcards(removeTrailingPathSeparator(dirs))
79+
}
80+
7781
/**
7882
* Schema for loading template config during app init.
7983
* Uses passthrough() to allow any extra keys from full-featured templates

packages/app/src/cli/models/app/loader.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
isLegacyAppSchema,
2121
TemplateConfigSchema,
2222
getTemplateScopesArray,
23+
normalizeExtensionDirectories,
2324
} from './app.js'
2425
import {parseHumanReadableError} from './error-parsing.js'
2526
import {configurationFileNames, dotEnvFileNames} from '../../constants.js'
@@ -630,7 +631,8 @@ class AppLoader<TConfig extends AppConfiguration, TModuleSpec extends ExtensionS
630631
}
631632

632633
private async createExtensionInstances(appDirectory: string, extensionDirectories?: string[]) {
633-
const extensionConfigPaths = [...(extensionDirectories ?? [defaultExtensionDirectory])].map((extensionPath) => {
634+
const normalizedDirs = normalizeExtensionDirectories(extensionDirectories)
635+
const extensionConfigPaths = [...(normalizedDirs ?? [defaultExtensionDirectory])].map((extensionPath) => {
634636
return joinPath(appDirectory, extensionPath, '*.extension.toml')
635637
})
636638
extensionConfigPaths.push(`!${joinPath(appDirectory, '**/node_modules/**')}`)

packages/app/src/cli/services/dev/app-events/file-watcher.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* eslint-disable no-case-declarations */
2-
import {AppLinkedInterface} from '../../../models/app/app.js'
2+
import {AppLinkedInterface, normalizeExtensionDirectories} from '../../../models/app/app.js'
33
import {configurationFileNames} from '../../../constants.js'
44
import {dirname, joinPath, normalizePath, relativePath} from '@shopify/cli-kit/node/path'
55
import {FSWatcher} from 'chokidar'
@@ -85,7 +85,9 @@ export class FileWatcher {
8585
* This ensures the watcher picks up any changes in what files need to be watched.
8686
*/
8787
async start(): Promise<void> {
88-
const extensionDirectories = [...(this.app.configuration.extension_directories ?? ['extensions'])]
88+
const extensionDirectories = [
89+
...(normalizeExtensionDirectories(this.app.configuration.extension_directories) ?? ['extensions']),
90+
]
8991
const fullExtensionDirectories = extensionDirectories.map((directory) => joinPath(this.app.directory, directory))
9092
const watchPaths = [this.app.configPath, ...fullExtensionDirectories]
9193

0 commit comments

Comments
 (0)