Skip to content

Commit 7749fae

Browse files
ryancbahanclaude
andcommitted
Migrate all TOML I/O callsites to TomlFile
Replaces scattered setAppConfigValue/setManyAppConfigValues/unsetAppConfigValue with TomlFile.patch/remove. Extension builders return objects instead of TOML strings. writeAppConfigurationFile uses TomlFile.replace + transformRaw for comment injection. breakdown-extensions uses Object.keys() instead of encode-then-regex-parse. Removes decode parameter from loadConfigurationFileContent. encodeToml/decodeToml no longer imported outside cli-kit. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e13bc32 commit 7749fae

29 files changed

Lines changed: 599 additions & 915 deletions

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

Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import {getOrCreateAppConfigHiddenPath} from '../../utilities/app/config/hidden-
3939
import {ApplicationURLs, generateApplicationURLs} from '../../services/dev/urls.js'
4040
import {showMultipleCLIWarningIfNeeded} from '@shopify/cli-kit/node/multiple-installation-warning'
4141
import {fileExists, readFile, glob, findPathUp, fileExistsSync} from '@shopify/cli-kit/node/fs'
42+
import {TomlFile, TomlParseError} from '@shopify/cli-kit/node/toml/toml-file'
4243
import {zod} from '@shopify/cli-kit/node/schema'
4344
import {readAndParseDotEnv, DotEnvFile} from '@shopify/cli-kit/node/dot-env'
4445
import {
@@ -49,7 +50,7 @@ import {
4950
} from '@shopify/cli-kit/node/node-package-manager'
5051
import {resolveFramework} from '@shopify/cli-kit/node/framework'
5152
import {hashString} from '@shopify/cli-kit/node/crypto'
52-
import {JsonMapType, decodeToml} from '@shopify/cli-kit/node/toml'
53+
import {JsonMapType} from '@shopify/cli-kit/node/toml'
5354
import {joinPath, dirname, basename, relativePath, relativizePath} from '@shopify/cli-kit/node/path'
5455
import {AbortError} from '@shopify/cli-kit/node/error'
5556
import {outputContent, outputDebug, OutputMessage, outputToken} from '@shopify/cli-kit/node/output'
@@ -82,27 +83,19 @@ const noopAbortOrReport: AbortOrReport = (_errorMessage, fallback, _configuratio
8283
export async function loadConfigurationFileContent(
8384
filepath: string,
8485
abortOrReport: AbortOrReport = abort,
85-
decode: (input: string) => JsonMapType = decodeToml,
8686
): Promise<JsonMapType> {
8787
if (!(await fileExists(filepath))) {
8888
return abortOrReport(outputContent`Couldn't find an app toml file at ${outputToken.path(filepath)}`, {}, filepath)
8989
}
9090

9191
try {
92-
const configurationContent = await readFile(filepath)
93-
return decode(configurationContent)
94-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
95-
} catch (err: any) {
96-
// TOML errors have line, pos and col properties
97-
if (err.line !== undefined && err.pos !== undefined && err.col !== undefined) {
98-
return abortOrReport(
99-
outputContent`Fix the following error in ${outputToken.path(filepath)}:\n${err.message}`,
100-
{},
101-
filepath,
102-
)
103-
} else {
104-
throw err
92+
const file = await TomlFile.read(filepath)
93+
return file.content
94+
} catch (err) {
95+
if (err instanceof TomlParseError) {
96+
return abortOrReport(outputContent`${err.message}`, {}, filepath)
10597
}
98+
throw err
10699
}
107100
}
108101

@@ -115,12 +108,11 @@ export async function parseConfigurationFile<TSchema extends zod.ZodType>(
115108
schema: TSchema,
116109
filepath: string,
117110
abortOrReport: AbortOrReport = abort,
118-
decode: (input: string) => JsonMapType = decodeToml,
119111
preloadedContent?: JsonMapType,
120112
): Promise<zod.TypeOf<TSchema> & {path: string}> {
121113
const fallbackOutput = {} as zod.TypeOf<TSchema>
122114

123-
const configurationObject = preloadedContent ?? (await loadConfigurationFileContent(filepath, abortOrReport, decode))
115+
const configurationObject = preloadedContent ?? (await loadConfigurationFileContent(filepath, abortOrReport))
124116

125117
if (!configurationObject) return fallbackOutput
126118

@@ -517,13 +509,8 @@ class AppLoader<TConfig extends AppConfiguration, TModuleSpec extends ExtensionS
517509
)
518510
}
519511

520-
private parseConfigurationFile<TSchema extends zod.ZodType>(
521-
schema: TSchema,
522-
filepath: string,
523-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
524-
decode: (input: any) => any = decodeToml,
525-
) {
526-
return parseConfigurationFile(schema, filepath, this.abortOrReport.bind(this), decode)
512+
private parseConfigurationFile<TSchema extends zod.ZodType>(schema: TSchema, filepath: string) {
513+
return parseConfigurationFile(schema, filepath, this.abortOrReport.bind(this))
527514
}
528515

529516
private validateWebs(webs: Web[]): void {
@@ -1027,7 +1014,6 @@ async function loadAppConfigurationFromState<
10271014
schemaForConfigurationFile,
10281015
configState.configurationPath,
10291016
abort,
1030-
decodeToml,
10311017
file,
10321018
)) as LoadedAppConfigFromConfigState<TConfig>
10331019
const allClientIdsByConfigName = await getAllLinkedConfigClientIds(configState.appDirectory, {

packages/app/src/cli/prompts/import-extensions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export interface MigrationChoice {
1616
ext: ExtensionRegistration,
1717
allExtensions: ExtensionRegistration[],
1818
appConfiguration: CurrentAppConfiguration,
19-
) => string
19+
) => object
2020
}
2121

2222
export const allMigrationChoices: MigrationChoice[] = [

packages/app/src/cli/services/admin-link/extension-to-toml.test.ts

Lines changed: 64 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {ExtensionRegistration} from '../../api/graphql/all_app_extension_registr
33
import {describe, expect, test} from 'vitest'
44

55
describe('extension-to-toml', () => {
6-
test('correctly builds a toml string for a app_link extension on a non embedded app', () => {
6+
test('correctly builds a toml object for a app_link extension on a non embedded app', () => {
77
// Given
88
const appConfig = {
99
path: '',
@@ -28,18 +28,24 @@ describe('extension-to-toml', () => {
2828
const got = buildTomlObject(extension1, [], appConfig)
2929

3030
// Then
31-
expect(got).toEqual(`[[extensions]]
32-
type = "admin_link"
33-
name = "Admin link label"
34-
handle = "admin-link-title"
35-
36-
[[extensions.targeting]]
37-
url = "https://google.es"
38-
target = "admin.collection-details.action.link"
39-
`)
31+
expect(got).toEqual({
32+
extensions: [
33+
{
34+
type: 'admin_link',
35+
name: 'Admin link label',
36+
handle: 'admin-link-title',
37+
targeting: [
38+
{
39+
url: 'https://google.es',
40+
target: 'admin.collection-details.action.link',
41+
},
42+
],
43+
},
44+
],
45+
})
4046
})
4147

42-
test('correctly builds a toml string for bulk_action extension with path in an embedded app', () => {
48+
test('correctly builds a toml object for bulk_action extension with path in an embedded app', () => {
4349
// Given
4450
const appConfig = {
4551
path: '',
@@ -63,17 +69,23 @@ handle = "admin-link-title"
6369
const got = buildTomlObject(extension1, [], appConfig)
6470

6571
// Then
66-
expect(got).toEqual(`[[extensions]]
67-
type = "admin_link"
68-
name = "Bulk action label"
69-
handle = "bulk-action-title"
70-
71-
[[extensions.targeting]]
72-
url = "app://action/product?product_id=123#hash"
73-
target = "admin.product-index.selection-action.link"
74-
`)
72+
expect(got).toEqual({
73+
extensions: [
74+
{
75+
type: 'admin_link',
76+
name: 'Bulk action label',
77+
handle: 'bulk-action-title',
78+
targeting: [
79+
{
80+
url: 'app://action/product?product_id=123#hash',
81+
target: 'admin.product-index.selection-action.link',
82+
},
83+
],
84+
},
85+
],
86+
})
7587
})
76-
test('correctly builds a toml string for bulk_action extension with no path in an embedded app', () => {
88+
test('correctly builds a toml object for bulk_action extension with no path in an embedded app', () => {
7789
// Given
7890
const appConfig = {
7991
path: '',
@@ -97,17 +109,23 @@ handle = "bulk-action-title"
97109
const got = buildTomlObject(extension1, [], appConfig)
98110

99111
// Then
100-
expect(got).toEqual(`[[extensions]]
101-
type = "admin_link"
102-
name = "Bulk action label"
103-
handle = "bulk-action-title"
104-
105-
[[extensions.targeting]]
106-
url = "app://"
107-
target = "admin.product-index.selection-action.link"
108-
`)
112+
expect(got).toEqual({
113+
extensions: [
114+
{
115+
type: 'admin_link',
116+
name: 'Bulk action label',
117+
handle: 'bulk-action-title',
118+
targeting: [
119+
{
120+
url: 'app://',
121+
target: 'admin.product-index.selection-action.link',
122+
},
123+
],
124+
},
125+
],
126+
})
109127
})
110-
test('correctly builds a toml string for bulk_action extension with no path but search query in an embedded app', () => {
128+
test('correctly builds a toml object for bulk_action extension with no path but search query in an embedded app', () => {
111129
// Given
112130
const appConfig = {
113131
path: '',
@@ -131,14 +149,20 @@ handle = "bulk-action-title"
131149
const got = buildTomlObject(extension1, [], appConfig)
132150

133151
// Then
134-
expect(got).toEqual(`[[extensions]]
135-
type = "admin_link"
136-
name = "Bulk action label"
137-
handle = "bulk-action-title"
138-
139-
[[extensions.targeting]]
140-
url = "app://?foo=bar"
141-
target = "admin.product-index.selection-action.link"
142-
`)
152+
expect(got).toEqual({
153+
extensions: [
154+
{
155+
type: 'admin_link',
156+
name: 'Bulk action label',
157+
handle: 'bulk-action-title',
158+
targeting: [
159+
{
160+
url: 'app://?foo=bar',
161+
target: 'admin.product-index.selection-action.link',
162+
},
163+
],
164+
},
165+
],
166+
})
143167
})
144168
})

packages/app/src/cli/services/admin-link/extension-to-toml.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import {contextToTarget} from './utils.js'
22
import {ExtensionRegistration} from '../../api/graphql/all_app_extension_registrations.js'
33
import {MAX_EXTENSION_HANDLE_LENGTH} from '../../models/extensions/schemas.js'
44
import {CurrentAppConfiguration} from '../../models/app/app.js'
5-
import {encodeToml} from '@shopify/cli-kit/node/toml'
65
import {slugify} from '@shopify/cli-kit/common/string'
76

87
interface AdminLinkConfig {
@@ -17,7 +16,7 @@ export function buildTomlObject(
1716
extension: ExtensionRegistration,
1817
_: ExtensionRegistration[],
1918
appConfiguration: CurrentAppConfiguration,
20-
): string {
19+
): object {
2120
const versionConfig = extension.activeVersion?.config ?? extension.draftVersion?.config
2221
if (!versionConfig) throw new Error('No config found for extension')
2322

@@ -55,6 +54,5 @@ export function buildTomlObject(
5554
},
5655
],
5756
}
58-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
59-
return encodeToml(localExtensionRepresentation as any)
57+
return localExtensionRepresentation
6058
}

packages/app/src/cli/services/app/add-uid-to-extension-toml.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@ describe('addUidToTomlsIfNecessary', () => {
6464
// Then
6565
const updatedContent = await readFile(tomlPath)
6666
expect(updatedContent).toContain('uid = "123"')
67-
expect(updatedContent).toMatch(/uid.*type/s)
6867
})
6968
})
7069

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import {ExtensionInstance} from '../../models/extensions/extension-instance.js'
22
import {DeveloperPlatformClient} from '../../utilities/developer-platform-client.js'
3-
import {decodeToml} from '@shopify/cli-kit/node/toml'
4-
import {readFile, writeFile} from '@shopify/cli-kit/node/fs'
3+
import {TomlFile} from '@shopify/cli-kit/node/toml/toml-file'
54
import {getPathValue} from '@shopify/cli-kit/common/object'
65

76
export async function addUidToTomlsIfNecessary(
@@ -20,26 +19,26 @@ export async function addUidToTomlsIfNecessary(
2019
async function addUidToToml(extension: ExtensionInstance) {
2120
if (!extension.isUUIDStrategyExtension || extension.configuration.uid) return
2221

23-
const tomlContents = await readFile(extension.configurationPath)
24-
const extensionConfig = decodeToml(tomlContents)
25-
const extensions = getPathValue(extensionConfig, 'extensions') as ExtensionInstance[]
22+
const file = await TomlFile.read(extension.configurationPath)
23+
const extensionsArray = getPathValue(file.content, 'extensions') as ExtensionInstance[]
2624

27-
if ('uid' in extensionConfig) return
28-
if (extensions) {
29-
const currentExtension = extensions.find((ext) => ext.handle === extension.handle)
25+
if ('uid' in file.content) return
26+
if (extensionsArray) {
27+
const currentExtension = extensionsArray.find((ext) => ext.handle === extension.handle)
3028
if (currentExtension && 'uid' in currentExtension) return
3129
}
3230

33-
let updatedTomlContents = tomlContents
34-
if (extensions?.length > 1) {
35-
// If the TOML has multiple extensions, we look for the correct handle to add the uid below
36-
const regex = new RegExp(`(\\n?(\\s*)handle\\s*=\\s*"${extension.handle}")`)
37-
updatedTomlContents = tomlContents.replace(regex, `$1\n$2uid = "${extension.uid}"`)
31+
if (extensionsArray && extensionsArray.length > 1) {
32+
// Multi-extension TOML: use regex to insert uid after the correct handle.
33+
// updateTomlValues (WASM) doesn't support patching individual array-of-tables entries,
34+
// so transformRaw with positional insertion is the pragmatic choice here.
35+
const handle = extension.handle
36+
await file.transformRaw((raw) => {
37+
const regex = new RegExp(`(\\n?(\\s*)handle\\s*=\\s*"${handle}")`)
38+
return raw.replace(regex, `$1\n$2uid = "${extension.uid}"`)
39+
})
3840
} else {
39-
// If the TOML has only one extension, we add the uid before the type, which is always present
40-
if ('uid' in extensionConfig) return
41-
const regex = /\n?((\s*)type\s*=\s*"\S*")/
42-
updatedTomlContents = tomlContents.replace(regex, `$2\nuid = "${extension.uid}"\n$1`)
41+
// Single extension (or no extensions array): add uid at the top level via WASM patch
42+
await file.patch({uid: extension.uid})
4343
}
44-
await writeFile(extension.configurationPath, updatedTomlContents)
4544
}

0 commit comments

Comments
 (0)