Skip to content

Commit 490c047

Browse files
ryancbahanclaude
andcommitted
Decompose loader into composable stages with narrow interfaces
Replace the monolithic loadApp pipeline with composable stages: - loadApp is now a thin wrapper: getAppConfigurationContext → loadAppFromContext - loadAppFromContext takes narrow Project + ActiveConfig directly - getAppConfigurationContext is discovery-only (no parsing/state construction) - ReloadState replaces passing entire AppLinkedInterface through reloads - AppLoader takes reloadState? instead of previousApp? - link() returns {remoteApp, configFileName, configuration} (no state) - linkedAppContext uses activeConfig directly, no AppConfigurationState Remove dead code: AppConfigurationState, toAppConfigurationState, loadAppConfigurationFromState, loadAppUsingConfigurationState, loadAppConfiguration, getAppConfigurationState, getAppDirectory, loadDotEnv, loadHiddenConfig, findWebConfigPaths, loadWebsForAppCreation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 46ef701 commit 490c047

11 files changed

Lines changed: 239 additions & 639 deletions

File tree

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/models/app/loader.test.ts

Lines changed: 15 additions & 163 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,12 @@ import {
33
getAppConfigurationFileName,
44
loadApp,
55
loadOpaqueApp,
6-
loadDotEnv,
76
parseConfigurationObject,
87
checkFolderIsValidApp,
98
AppLoaderMode,
10-
getAppConfigurationState,
9+
getAppConfigurationContext,
1110
loadConfigForAppCreation,
1211
reloadApp,
13-
loadHiddenConfig,
1412
} from './loader.js'
1513
import {parseHumanReadableError} from './error-parsing.js'
1614
import {App, AppConfiguration, AppInterface, AppLinkedInterface, AppSchema, WebConfigurationSchema} from './app.js'
@@ -33,7 +31,7 @@ import {
3331
PackageJson,
3432
pnpmWorkspaceFile,
3533
} from '@shopify/cli-kit/node/node-package-manager'
36-
import {inTemporaryDirectory, moveFile, mkdir, mkTmpDir, rmdir, writeFile, readFile} from '@shopify/cli-kit/node/fs'
34+
import {inTemporaryDirectory, moveFile, mkdir, mkTmpDir, rmdir, writeFile} from '@shopify/cli-kit/node/fs'
3735
import {joinPath, dirname, cwd, normalizePath} from '@shopify/cli-kit/node/path'
3836
import {platformAndArch} from '@shopify/cli-kit/node/os'
3937
import {outputContent, outputToken} from '@shopify/cli-kit/node/output'
@@ -2813,46 +2811,6 @@ describe('getAppConfigurationShorthand', () => {
28132811
})
28142812
})
28152813

2816-
describe('loadDotEnv', () => {
2817-
test('it returns undefined if the env is missing', async () => {
2818-
await inTemporaryDirectory(async (tmp) => {
2819-
// When
2820-
const got = await loadDotEnv(tmp, joinPath(tmp, 'shopify.app.toml'))
2821-
2822-
// Then
2823-
expect(got).toBeUndefined()
2824-
})
2825-
})
2826-
2827-
test('it loads from the default env file', async () => {
2828-
await inTemporaryDirectory(async (tmp) => {
2829-
// Given
2830-
await writeFile(joinPath(tmp, '.env'), 'FOO="bar"')
2831-
2832-
// When
2833-
const got = await loadDotEnv(tmp, joinPath(tmp, 'shopify.app.toml'))
2834-
2835-
// Then
2836-
expect(got).toBeDefined()
2837-
expect(got!.variables.FOO).toEqual('bar')
2838-
})
2839-
})
2840-
2841-
test('it loads from the config specific env file', async () => {
2842-
await inTemporaryDirectory(async (tmp) => {
2843-
// Given
2844-
await writeFile(joinPath(tmp, '.env.staging'), 'FOO="bar"')
2845-
2846-
// When
2847-
const got = await loadDotEnv(tmp, joinPath(tmp, 'shopify.app.staging.toml'))
2848-
2849-
// Then
2850-
expect(got).toBeDefined()
2851-
expect(got!.variables.FOO).toEqual('bar')
2852-
})
2853-
})
2854-
})
2855-
28562814
describe('checkFolderIsValidApp', () => {
28572815
test('throws an error if the folder does not contain a shopify.app.toml file', async () => {
28582816
await inTemporaryDirectory(async (tmp) => {
@@ -3484,46 +3442,26 @@ describe('WebhooksSchema', () => {
34843442
}
34853443
})
34863444

3487-
describe('getAppConfigurationState', () => {
3445+
describe('getAppConfigurationContext', () => {
34883446
test.each([
3489-
[
3490-
`client_id="abcdef"`,
3491-
{
3492-
basicConfiguration: {
3493-
client_id: 'abcdef',
3494-
},
3495-
isLinked: true,
3496-
},
3497-
],
3447+
[`client_id="abcdef"`, {client_id: 'abcdef'}, true],
34983448
[
34993449
`client_id="abcdef"
35003450
something_extra="keep"`,
3501-
{
3502-
basicConfiguration: {
3503-
client_id: 'abcdef',
3504-
something_extra: 'keep',
3505-
},
3506-
isLinked: true,
3507-
},
3451+
{client_id: 'abcdef', something_extra: 'keep'},
3452+
true,
35083453
],
3509-
[
3510-
`client_id=""`,
3511-
{
3512-
basicConfiguration: {
3513-
client_id: '',
3514-
},
3515-
isLinked: false,
3516-
},
3517-
],
3518-
])('loads from %s', async (content, resultShouldContain) => {
3454+
[`client_id=""`, {client_id: ''}, false],
3455+
])('loads from %s', async (content, expectedContent, expectedIsLinked) => {
35193456
await inTemporaryDirectory(async (tmpDir) => {
35203457
const appConfigPath = joinPath(tmpDir, 'shopify.app.toml')
35213458
const packageJsonPath = joinPath(tmpDir, 'package.json')
35223459
await writeFile(appConfigPath, content)
35233460
await writeFile(packageJsonPath, '{}')
35243461

3525-
const state = await getAppConfigurationState(tmpDir, undefined)
3526-
expect(state).toMatchObject(resultShouldContain)
3462+
const {activeConfig} = await getAppConfigurationContext(tmpDir, undefined)
3463+
expect(activeConfig.file.content).toMatchObject(expectedContent)
3464+
expect(activeConfig.isLinked).toBe(expectedIsLinked)
35273465
})
35283466
})
35293467

@@ -3535,10 +3473,10 @@ describe('getAppConfigurationState', () => {
35353473
await writeFile(appConfigPath, content)
35363474
await writeFile(packageJsonPath, '{}')
35373475

3538-
const result = await getAppConfigurationState(tmpDir, undefined)
3476+
const {activeConfig} = await getAppConfigurationContext(tmpDir, undefined)
35393477

3540-
expect(result.basicConfiguration.client_id).toBe('')
3541-
expect(result.isLinked).toBe(false)
3478+
expect(activeConfig.file.content.client_id).toBe('')
3479+
expect(activeConfig.isLinked).toBe(false)
35423480
})
35433481
})
35443482
})
@@ -3683,6 +3621,7 @@ value = true
36833621
})
36843622
})
36853623

3624+
<<<<<<< HEAD
36863625
describe('loadHiddenConfig', () => {
36873626
test('returns empty object if hidden config file does not exist', async () => {
36883627
await inTemporaryDirectory(async (tmpDir) => {
@@ -3705,94 +3644,7 @@ describe('loadHiddenConfig', () => {
37053644
})
37063645
})
37073646

3708-
test('returns config for client_id if hidden config file exists', async () => {
3709-
await inTemporaryDirectory(async (tmpDir) => {
3710-
// Given
3711-
const configuration = {
3712-
client_id: '12345',
3713-
} as AppConfiguration
3714-
const hiddenConfigPath = joinPath(tmpDir, '.shopify', 'project.json')
3715-
await mkdir(dirname(hiddenConfigPath))
3716-
await writeFile(
3717-
hiddenConfigPath,
3718-
JSON.stringify({
3719-
'12345': {someKey: 'someValue'},
3720-
'other-id': {otherKey: 'otherValue'},
3721-
}),
3722-
)
3723-
3724-
// When
3725-
const got = await loadHiddenConfig(tmpDir, configuration)
3726-
3727-
// Then
3728-
expect(got).toEqual({someKey: 'someValue'})
3729-
})
3730-
})
3731-
3732-
test('returns empty object if client_id not found in existing hidden config', async () => {
3733-
await inTemporaryDirectory(async (tmpDir) => {
3734-
// Given
3735-
const configuration = {
3736-
client_id: 'not-found',
3737-
} as AppConfiguration
3738-
const hiddenConfigPath = joinPath(tmpDir, '.shopify', 'project.json')
3739-
await mkdir(dirname(hiddenConfigPath))
3740-
await writeFile(
3741-
hiddenConfigPath,
3742-
JSON.stringify({
3743-
'other-id': {someKey: 'someValue'},
3744-
}),
3745-
)
3746-
3747-
// When
3748-
const got = await loadHiddenConfig(tmpDir, configuration)
3749-
3750-
// Then
3751-
expect(got).toEqual({})
3752-
})
3753-
})
3754-
3755-
test('returns config if hidden config has an old format with just a dev_store_url', async () => {
3756-
await inTemporaryDirectory(async (tmpDir) => {
3757-
// Given
3758-
const configuration = {
3759-
client_id: 'not-found',
3760-
} as AppConfiguration
3761-
const hiddenConfigPath = joinPath(tmpDir, '.shopify', 'project.json')
3762-
await mkdir(dirname(hiddenConfigPath))
3763-
await writeFile(
3764-
hiddenConfigPath,
3765-
JSON.stringify({
3766-
dev_store_url: 'https://dev-store.myshopify.com',
3767-
}),
3768-
)
3769-
3770-
// When
3771-
const got = await loadHiddenConfig(tmpDir, configuration)
3772-
3773-
// Then
3774-
expect(got).toEqual({dev_store_url: 'https://dev-store.myshopify.com'})
3775-
})
3776-
})
3777-
3778-
test('returns empty object if hidden config file is invalid JSON', async () => {
3779-
await inTemporaryDirectory(async (tmpDir) => {
3780-
// Given
3781-
const configuration = {
3782-
client_id: '12345',
3783-
} as AppConfiguration
3784-
const hiddenConfigPath = joinPath(tmpDir, '.shopify', 'project.json')
3785-
await mkdir(dirname(hiddenConfigPath))
3786-
await writeFile(hiddenConfigPath, 'invalid json')
3787-
3788-
// When
3789-
const got = await loadHiddenConfig(tmpDir, configuration)
37903647

3791-
// Then
3792-
expect(got).toEqual({})
3793-
})
3794-
})
3795-
})
37963648

37973649
describe('loadOpaqueApp', () => {
37983650
let specifications: ExtensionSpecification[]

0 commit comments

Comments
 (0)