Skip to content

Commit fe0b46f

Browse files
ryancbahanclaude
andcommitted
Integrate Project model into loader and app-context
Wires the Project domain model into the existing loading pipeline: - getAppConfigurationState uses Project.load() for filesystem discovery - getAppConfigurationContext returns project + activeConfig + state as independent values (project is never nested inside state) - AppLoader reads from Project's pre-loaded data: extension files, web files, dotenv, hidden config, deps, package manager, workspaces - No duplicate filesystem scanning — Project discovers once, loader reads from it - AppConfigurationState no longer carries project as a field - LoadedAppContextOutput exposes project and activeConfig as top-level fields for commands - All extension/web file discovery filtered to active config's directories via config-selection functions Zero behavioral changes. All 3801 existing tests pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent bf48bb0 commit fe0b46f

27 files changed

+1252
-819
lines changed

packages/app/src/cli/commands/app/config/link.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export default class ConfigLink extends AppLinkedCommand {
3434
directory: flags.path,
3535
clientId: undefined,
3636
forceRelink: false,
37-
userProvidedConfigName: result.state.configurationFileName,
37+
userProvidedConfigName: result.configFileName,
3838
})
3939

4040
return {app}

packages/app/src/cli/commands/app/deploy.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ describe('app deploy --force deprecation warning', () => {
2929
source: OrganizationSource.Partners,
3030
},
3131
specifications: [],
32+
project: {} as any,
33+
activeConfig: {} as any,
3234
})
3335
vi.mocked(deploy).mockResolvedValue({app: testAppLinked()})
3436
})

packages/app/src/cli/commands/app/release.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ describe('app release --force deprecation warning', () => {
2929
source: OrganizationSource.Partners,
3030
},
3131
specifications: [],
32+
project: {} as any,
33+
activeConfig: {} as any,
3234
})
3335
vi.mocked(release).mockResolvedValue(undefined)
3436
})
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import {
2+
getAppConfigurationFileName,
3+
getAppConfigurationShorthand,
4+
isValidFormatAppConfigurationFileName,
5+
} from './config-file-naming.js'
6+
import {describe, expect, test} from 'vitest'
7+
8+
describe('getAppConfigurationFileName', () => {
9+
test('returns default filename when no config name is provided', () => {
10+
expect(getAppConfigurationFileName()).toBe('shopify.app.toml')
11+
expect(getAppConfigurationFileName(undefined)).toBe('shopify.app.toml')
12+
})
13+
14+
test('returns the config name as-is when it matches the valid format', () => {
15+
expect(getAppConfigurationFileName('shopify.app.production.toml')).toBe('shopify.app.production.toml')
16+
expect(getAppConfigurationFileName('shopify.app.staging.toml')).toBe('shopify.app.staging.toml')
17+
expect(getAppConfigurationFileName('shopify.app.toml')).toBe('shopify.app.toml')
18+
})
19+
20+
test('slugifies arbitrary strings into the filename pattern', () => {
21+
expect(getAppConfigurationFileName('production')).toBe('shopify.app.production.toml')
22+
expect(getAppConfigurationFileName('My Store')).toBe('shopify.app.my-store.toml')
23+
})
24+
})
25+
26+
describe('getAppConfigurationShorthand', () => {
27+
test('returns undefined for the default config filename', () => {
28+
expect(getAppConfigurationShorthand('shopify.app.toml')).toBeUndefined()
29+
expect(getAppConfigurationShorthand('/some/path/shopify.app.toml')).toBeUndefined()
30+
})
31+
32+
test('extracts the shorthand from a named config', () => {
33+
expect(getAppConfigurationShorthand('shopify.app.production.toml')).toBe('production')
34+
expect(getAppConfigurationShorthand('/path/to/shopify.app.staging.toml')).toBe('staging')
35+
expect(getAppConfigurationShorthand('shopify.app.my-store.toml')).toBe('my-store')
36+
})
37+
38+
test('returns undefined for non-matching filenames', () => {
39+
expect(getAppConfigurationShorthand('random.toml')).toBeUndefined()
40+
expect(getAppConfigurationShorthand('shopify.web.toml')).toBeUndefined()
41+
})
42+
})
43+
44+
describe('isValidFormatAppConfigurationFileName', () => {
45+
test('returns true for valid app configuration filenames', () => {
46+
expect(isValidFormatAppConfigurationFileName('shopify.app.toml')).toBe(true)
47+
expect(isValidFormatAppConfigurationFileName('shopify.app.production.toml')).toBe(true)
48+
expect(isValidFormatAppConfigurationFileName('shopify.app.my-store.toml')).toBe(true)
49+
expect(isValidFormatAppConfigurationFileName('shopify.app.test_env.toml')).toBe(true)
50+
})
51+
52+
test('returns false for invalid filenames', () => {
53+
expect(isValidFormatAppConfigurationFileName('production')).toBe(false)
54+
expect(isValidFormatAppConfigurationFileName('shopify.web.toml')).toBe(false)
55+
expect(isValidFormatAppConfigurationFileName('shopify.app..toml')).toBe(false)
56+
expect(isValidFormatAppConfigurationFileName('')).toBe(false)
57+
})
58+
})
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import {configurationFileNames} from '../../constants.js'
2+
import {slugify} from '@shopify/cli-kit/common/string'
3+
import {basename} from '@shopify/cli-kit/node/path'
4+
5+
const appConfigurationFileNameRegex = /^shopify\.app(\.[-\w]+)?\.toml$/
6+
export type AppConfigurationFileName = 'shopify.app.toml' | `shopify.app.${string}.toml`
7+
8+
/**
9+
* Gets the name of the app configuration file (e.g. `shopify.app.production.toml`) based on a provided config name.
10+
*
11+
* @param configName - Optional config name to base the file name upon
12+
* @returns Either the default app configuration file name (`shopify.app.toml`), the given config name (if it matched the valid format), or `shopify.app.<config name>.toml` if it was an arbitrary string
13+
*/
14+
export function getAppConfigurationFileName(configName?: string): AppConfigurationFileName {
15+
if (!configName) {
16+
return configurationFileNames.app
17+
}
18+
19+
if (isValidFormatAppConfigurationFileName(configName)) {
20+
return configName
21+
} else {
22+
return `shopify.app.${slugify(configName)}.toml`
23+
}
24+
}
25+
26+
/**
27+
* Given a path to an app configuration file, extract the shorthand section from the file name.
28+
*
29+
* This is undefined for `shopify.app.toml` files, or returns e.g. `production` for `shopify.app.production.toml`.
30+
*/
31+
export function getAppConfigurationShorthand(path: string) {
32+
const match = basename(path).match(appConfigurationFileNameRegex)
33+
return match?.[1]?.slice(1)
34+
}
35+
36+
/** Checks if configName is a valid one (`shopify.app.toml`, or `shopify.app.<something>.toml`) */
37+
export function isValidFormatAppConfigurationFileName(configName: string): configName is AppConfigurationFileName {
38+
if (appConfigurationFileNameRegex.test(configName)) {
39+
return true
40+
}
41+
return false
42+
}

0 commit comments

Comments
 (0)