Skip to content

Commit 6fb49df

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 ba98bae commit 6fb49df

File tree

11 files changed

+234
-667
lines changed

11 files changed

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

Lines changed: 14 additions & 195 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, 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) => {
@@ -3485,49 +3443,26 @@ describe('WebhooksSchema', () => {
34853443
}
34863444
})
34873445

3488-
describe('getAppConfigurationState', () => {
3446+
describe('getAppConfigurationContext', () => {
34893447
test.each([
3490-
[
3491-
`client_id="abcdef"`,
3492-
{
3493-
basicConfiguration: {
3494-
path: expect.stringMatching(/shopify.app.toml$/),
3495-
client_id: 'abcdef',
3496-
},
3497-
isLinked: true,
3498-
},
3499-
],
3448+
[`client_id="abcdef"`, {client_id: 'abcdef'}, true],
35003449
[
35013450
`client_id="abcdef"
35023451
something_extra="keep"`,
3503-
{
3504-
basicConfiguration: {
3505-
path: expect.stringMatching(/shopify.app.toml$/),
3506-
client_id: 'abcdef',
3507-
something_extra: 'keep',
3508-
},
3509-
isLinked: true,
3510-
},
3511-
],
3512-
[
3513-
`client_id=""`,
3514-
{
3515-
basicConfiguration: {
3516-
path: expect.stringMatching(/shopify.app.toml$/),
3517-
client_id: '',
3518-
},
3519-
isLinked: false,
3520-
},
3452+
{client_id: 'abcdef', something_extra: 'keep'},
3453+
true,
35213454
],
3522-
])('loads from %s', async (content, resultShouldContain) => {
3455+
[`client_id=""`, {client_id: ''}, false],
3456+
])('loads from %s', async (content, expectedContent, expectedIsLinked) => {
35233457
await inTemporaryDirectory(async (tmpDir) => {
35243458
const appConfigPath = joinPath(tmpDir, 'shopify.app.toml')
35253459
const packageJsonPath = joinPath(tmpDir, 'package.json')
35263460
await writeFile(appConfigPath, content)
35273461
await writeFile(packageJsonPath, '{}')
35283462

3529-
const state = await getAppConfigurationState(tmpDir, undefined)
3530-
expect(state).toMatchObject(resultShouldContain)
3463+
const {activeConfig} = await getAppConfigurationContext(tmpDir, undefined)
3464+
expect(activeConfig.file.content).toMatchObject(expectedContent)
3465+
expect(activeConfig.isLinked).toBe(expectedIsLinked)
35313466
})
35323467
})
35333468

@@ -3539,10 +3474,10 @@ describe('getAppConfigurationState', () => {
35393474
await writeFile(appConfigPath, content)
35403475
await writeFile(packageJsonPath, '{}')
35413476

3542-
const result = await getAppConfigurationState(tmpDir, undefined)
3477+
const {activeConfig} = await getAppConfigurationContext(tmpDir, undefined)
35433478

3544-
expect(result.basicConfiguration.client_id).toBe('')
3545-
expect(result.isLinked).toBe(false)
3479+
expect(activeConfig.file.content.client_id).toBe('')
3480+
expect(activeConfig.isLinked).toBe(false)
35463481
})
35473482
})
35483483
})
@@ -3687,122 +3622,6 @@ value = true
36873622
})
36883623
})
36893624

3690-
describe('loadHiddenConfig', () => {
3691-
test('returns empty object if hidden config file does not exist', async () => {
3692-
await inTemporaryDirectory(async (tmpDir) => {
3693-
// Given
3694-
const configuration = {
3695-
path: joinPath(tmpDir, 'shopify.app.toml'),
3696-
client_id: '12345',
3697-
}
3698-
await writeFile(joinPath(tmpDir, '.gitignore'), '')
3699-
3700-
// When
3701-
const got = await loadHiddenConfig(tmpDir, configuration)
3702-
3703-
// Then
3704-
expect(got).toEqual({})
3705-
3706-
// Verify empty config file was created
3707-
const hiddenConfigPath = joinPath(tmpDir, '.shopify', 'project.json')
3708-
const fileContent = await readFile(hiddenConfigPath)
3709-
expect(JSON.parse(fileContent)).toEqual({})
3710-
})
3711-
})
3712-
3713-
test('returns config for client_id if hidden config file exists', async () => {
3714-
await inTemporaryDirectory(async (tmpDir) => {
3715-
// Given
3716-
const configuration = {
3717-
path: joinPath(tmpDir, 'shopify.app.toml'),
3718-
client_id: '12345',
3719-
}
3720-
const hiddenConfigPath = joinPath(tmpDir, '.shopify', 'project.json')
3721-
await mkdir(dirname(hiddenConfigPath))
3722-
await writeFile(
3723-
hiddenConfigPath,
3724-
JSON.stringify({
3725-
'12345': {someKey: 'someValue'},
3726-
'other-id': {otherKey: 'otherValue'},
3727-
}),
3728-
)
3729-
3730-
// When
3731-
const got = await loadHiddenConfig(tmpDir, configuration)
3732-
3733-
// Then
3734-
expect(got).toEqual({someKey: 'someValue'})
3735-
})
3736-
})
3737-
3738-
test('returns empty object if client_id not found in existing hidden config', async () => {
3739-
await inTemporaryDirectory(async (tmpDir) => {
3740-
// Given
3741-
const configuration = {
3742-
path: joinPath(tmpDir, 'shopify.app.toml'),
3743-
client_id: 'not-found',
3744-
}
3745-
const hiddenConfigPath = joinPath(tmpDir, '.shopify', 'project.json')
3746-
await mkdir(dirname(hiddenConfigPath))
3747-
await writeFile(
3748-
hiddenConfigPath,
3749-
JSON.stringify({
3750-
'other-id': {someKey: 'someValue'},
3751-
}),
3752-
)
3753-
3754-
// When
3755-
const got = await loadHiddenConfig(tmpDir, configuration)
3756-
3757-
// Then
3758-
expect(got).toEqual({})
3759-
})
3760-
})
3761-
3762-
test('returns config if hidden config has an old format with just a dev_store_url', async () => {
3763-
await inTemporaryDirectory(async (tmpDir) => {
3764-
// Given
3765-
const configuration = {
3766-
path: joinPath(tmpDir, 'shopify.app.toml'),
3767-
client_id: 'not-found',
3768-
}
3769-
const hiddenConfigPath = joinPath(tmpDir, '.shopify', 'project.json')
3770-
await mkdir(dirname(hiddenConfigPath))
3771-
await writeFile(
3772-
hiddenConfigPath,
3773-
JSON.stringify({
3774-
dev_store_url: 'https://dev-store.myshopify.com',
3775-
}),
3776-
)
3777-
3778-
// When
3779-
const got = await loadHiddenConfig(tmpDir, configuration)
3780-
3781-
// Then
3782-
expect(got).toEqual({dev_store_url: 'https://dev-store.myshopify.com'})
3783-
})
3784-
})
3785-
3786-
test('returns empty object if hidden config file is invalid JSON', async () => {
3787-
await inTemporaryDirectory(async (tmpDir) => {
3788-
// Given
3789-
const configuration = {
3790-
path: joinPath(tmpDir, 'shopify.app.toml'),
3791-
client_id: '12345',
3792-
}
3793-
const hiddenConfigPath = joinPath(tmpDir, '.shopify', 'project.json')
3794-
await mkdir(dirname(hiddenConfigPath))
3795-
await writeFile(hiddenConfigPath, 'invalid json')
3796-
3797-
// When
3798-
const got = await loadHiddenConfig(tmpDir, configuration)
3799-
3800-
// Then
3801-
expect(got).toEqual({})
3802-
})
3803-
})
3804-
})
3805-
38063625
describe('loadOpaqueApp', () => {
38073626
let specifications: ExtensionSpecification[]
38083627

0 commit comments

Comments
 (0)