From b485b0ac6857dc570862089a9058c8198acfe2ab Mon Sep 17 00:00:00 2001 From: Ryan Bahan Date: Fri, 6 Mar 2026 09:18:03 -0700 Subject: [PATCH 1/3] Introduce TomlFile and migrate all TOML I/O callsites MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a general-purpose TomlFile class in cli-kit that provides a unified interface for reading, patching, removing keys from, and replacing TOML files on disk. Migrates all callsites to use it, replacing the scattered setAppConfigValue/setManyAppConfigValues/unsetAppConfigValue functions and direct encodeToml/decodeToml usage. Key changes: - TomlFile class with read/patch/remove/replace/transformRaw methods - Extension builders return objects instead of TOML strings - writeAppConfigurationFile goes through TomlFile (replace + transformRaw for comment injection) - breakdown-extensions uses Object.keys() instead of encode→regex round-trip - encodeToml/decodeToml moved to internal codec module (not publicly exported) - TomlParseError wraps parse errors with file path context - Removed decode parameter from loadConfigurationFileContent/parseConfigurationFile Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/app/src/cli/models/app/loader.ts | 36 +- .../app/src/cli/prompts/import-extensions.ts | 2 +- .../admin-link/extension-to-toml.test.ts | 104 ++-- .../services/admin-link/extension-to-toml.ts | 6 +- .../app/add-uid-to-extension-toml.test.ts | 1 - .../services/app/add-uid-to-extension-toml.ts | 35 +- .../app/patch-app-configuration-file.test.ts | 188 +------ .../app/patch-app-configuration-file.ts | 61 -- .../app/write-app-configuration-file.ts | 18 +- packages/app/src/cli/services/context.test.ts | 53 +- packages/app/src/cli/services/context.ts | 8 +- .../context/breakdown-extensions.test.ts | 3 +- .../services/context/breakdown-extensions.ts | 82 +-- packages/app/src/cli/services/dev.ts | 5 +- .../src/cli/services/dev/update-extension.ts | 10 +- .../app/src/cli/services/dev/urls.test.ts | 49 +- packages/app/src/cli/services/dev/urls.ts | 27 +- .../services/flow/extension-to-toml.test.ts | 152 +++-- .../cli/services/flow/extension-to-toml.ts | 6 +- .../app/src/cli/services/import-extensions.ts | 13 +- .../extension-to-toml.test.ts | 66 +-- .../marketing_activity/extension-to-toml.ts | 6 +- .../payments/extension-to-toml.test.ts | 521 +++++++++--------- .../services/payments/extension-to-toml.ts | 6 +- .../extension-to-toml.test.ts | 20 +- .../subscription_link/extension-to-toml.ts | 6 +- .../src/cli/utilities/app/config/getTomls.ts | 10 +- packages/cli-kit/package.json | 5 + .../src/public/node/base-command.test.ts | 2 +- .../src/public/node/environments.test.ts | 2 +- .../cli-kit/src/public/node/environments.ts | 7 +- .../src/public/node/json-schema.test.ts | 2 +- .../node/{toml.test.ts => toml/codec.test.ts} | 2 +- .../public/node/{toml.ts => toml/codec.ts} | 2 +- .../cli-kit/src/public/node/toml/index.ts | 1 + .../src/public/node/toml/toml-file.test.ts | 284 ++++++++++ .../cli-kit/src/public/node/toml/toml-file.ts | 157 ++++++ pnpm-lock.yaml | 3 + 38 files changed, 1042 insertions(+), 919 deletions(-) rename packages/cli-kit/src/public/node/{toml.test.ts => toml/codec.test.ts} (95%) rename packages/cli-kit/src/public/node/{toml.ts => toml/codec.ts} (92%) create mode 100644 packages/cli-kit/src/public/node/toml/index.ts create mode 100644 packages/cli-kit/src/public/node/toml/toml-file.test.ts create mode 100644 packages/cli-kit/src/public/node/toml/toml-file.ts diff --git a/packages/app/src/cli/models/app/loader.ts b/packages/app/src/cli/models/app/loader.ts index 9e7172feb65..4d176a3aadc 100644 --- a/packages/app/src/cli/models/app/loader.ts +++ b/packages/app/src/cli/models/app/loader.ts @@ -39,6 +39,7 @@ import {getOrCreateAppConfigHiddenPath} from '../../utilities/app/config/hidden- import {ApplicationURLs, generateApplicationURLs} from '../../services/dev/urls.js' import {showMultipleCLIWarningIfNeeded} from '@shopify/cli-kit/node/multiple-installation-warning' import {fileExists, readFile, glob, findPathUp, fileExistsSync} from '@shopify/cli-kit/node/fs' +import {TomlFile, TomlParseError} from '@shopify/cli-kit/node/toml/toml-file' import {zod} from '@shopify/cli-kit/node/schema' import {readAndParseDotEnv, DotEnvFile} from '@shopify/cli-kit/node/dot-env' import { @@ -49,7 +50,7 @@ import { } from '@shopify/cli-kit/node/node-package-manager' import {resolveFramework} from '@shopify/cli-kit/node/framework' import {hashString} from '@shopify/cli-kit/node/crypto' -import {JsonMapType, decodeToml} from '@shopify/cli-kit/node/toml' +import {JsonMapType} from '@shopify/cli-kit/node/toml' import {joinPath, dirname, basename, relativePath, relativizePath} from '@shopify/cli-kit/node/path' import {AbortError} from '@shopify/cli-kit/node/error' import {outputContent, outputDebug, OutputMessage, outputToken} from '@shopify/cli-kit/node/output' @@ -82,27 +83,19 @@ const noopAbortOrReport: AbortOrReport = (_errorMessage, fallback, _configuratio export async function loadConfigurationFileContent( filepath: string, abortOrReport: AbortOrReport = abort, - decode: (input: string) => JsonMapType = decodeToml, ): Promise { if (!(await fileExists(filepath))) { return abortOrReport(outputContent`Couldn't find an app toml file at ${outputToken.path(filepath)}`, {}, filepath) } try { - const configurationContent = await readFile(filepath) - return decode(configurationContent) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (err: any) { - // TOML errors have line, pos and col properties - if (err.line !== undefined && err.pos !== undefined && err.col !== undefined) { - return abortOrReport( - outputContent`Fix the following error in ${outputToken.path(filepath)}:\n${err.message}`, - {}, - filepath, - ) - } else { - throw err + const file = await TomlFile.read(filepath) + return file.content + } catch (err) { + if (err instanceof TomlParseError) { + return abortOrReport(outputContent`${err.message}`, {}, filepath) } + throw err } } @@ -115,12 +108,11 @@ export async function parseConfigurationFile( schema: TSchema, filepath: string, abortOrReport: AbortOrReport = abort, - decode: (input: string) => JsonMapType = decodeToml, preloadedContent?: JsonMapType, ): Promise & {path: string}> { const fallbackOutput = {} as zod.TypeOf - const configurationObject = preloadedContent ?? (await loadConfigurationFileContent(filepath, abortOrReport, decode)) + const configurationObject = preloadedContent ?? (await loadConfigurationFileContent(filepath, abortOrReport)) if (!configurationObject) return fallbackOutput @@ -517,13 +509,8 @@ class AppLoader( - schema: TSchema, - filepath: string, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - decode: (input: any) => any = decodeToml, - ) { - return parseConfigurationFile(schema, filepath, this.abortOrReport.bind(this), decode) + private parseConfigurationFile(schema: TSchema, filepath: string) { + return parseConfigurationFile(schema, filepath, this.abortOrReport.bind(this)) } private validateWebs(webs: Web[]): void { @@ -1027,7 +1014,6 @@ async function loadAppConfigurationFromState< schemaForConfigurationFile, configState.configurationPath, abort, - decodeToml, file, )) as LoadedAppConfigFromConfigState const allClientIdsByConfigName = await getAllLinkedConfigClientIds(configState.appDirectory, { diff --git a/packages/app/src/cli/prompts/import-extensions.ts b/packages/app/src/cli/prompts/import-extensions.ts index 051f4be1f1d..347b5a8be03 100644 --- a/packages/app/src/cli/prompts/import-extensions.ts +++ b/packages/app/src/cli/prompts/import-extensions.ts @@ -16,7 +16,7 @@ export interface MigrationChoice { ext: ExtensionRegistration, allExtensions: ExtensionRegistration[], appConfiguration: CurrentAppConfiguration, - ) => string + ) => object } export const allMigrationChoices: MigrationChoice[] = [ diff --git a/packages/app/src/cli/services/admin-link/extension-to-toml.test.ts b/packages/app/src/cli/services/admin-link/extension-to-toml.test.ts index e9dde53e4ec..82066d9ea44 100644 --- a/packages/app/src/cli/services/admin-link/extension-to-toml.test.ts +++ b/packages/app/src/cli/services/admin-link/extension-to-toml.test.ts @@ -3,7 +3,7 @@ import {ExtensionRegistration} from '../../api/graphql/all_app_extension_registr import {describe, expect, test} from 'vitest' describe('extension-to-toml', () => { - test('correctly builds a toml string for a app_link extension on a non embedded app', () => { + test('correctly builds a toml object for a app_link extension on a non embedded app', () => { // Given const appConfig = { path: '', @@ -28,18 +28,24 @@ describe('extension-to-toml', () => { const got = buildTomlObject(extension1, [], appConfig) // Then - expect(got).toEqual(`[[extensions]] -type = "admin_link" -name = "Admin link label" -handle = "admin-link-title" - - [[extensions.targeting]] - url = "https://google.es" - target = "admin.collection-details.action.link" -`) + expect(got).toEqual({ + extensions: [ + { + type: 'admin_link', + name: 'Admin link label', + handle: 'admin-link-title', + targeting: [ + { + url: 'https://google.es', + target: 'admin.collection-details.action.link', + }, + ], + }, + ], + }) }) - test('correctly builds a toml string for bulk_action extension with path in an embedded app', () => { + test('correctly builds a toml object for bulk_action extension with path in an embedded app', () => { // Given const appConfig = { path: '', @@ -63,17 +69,23 @@ handle = "admin-link-title" const got = buildTomlObject(extension1, [], appConfig) // Then - expect(got).toEqual(`[[extensions]] -type = "admin_link" -name = "Bulk action label" -handle = "bulk-action-title" - - [[extensions.targeting]] - url = "app://action/product?product_id=123#hash" - target = "admin.product-index.selection-action.link" -`) + expect(got).toEqual({ + extensions: [ + { + type: 'admin_link', + name: 'Bulk action label', + handle: 'bulk-action-title', + targeting: [ + { + url: 'app://action/product?product_id=123#hash', + target: 'admin.product-index.selection-action.link', + }, + ], + }, + ], + }) }) - test('correctly builds a toml string for bulk_action extension with no path in an embedded app', () => { + test('correctly builds a toml object for bulk_action extension with no path in an embedded app', () => { // Given const appConfig = { path: '', @@ -97,17 +109,23 @@ handle = "bulk-action-title" const got = buildTomlObject(extension1, [], appConfig) // Then - expect(got).toEqual(`[[extensions]] -type = "admin_link" -name = "Bulk action label" -handle = "bulk-action-title" - - [[extensions.targeting]] - url = "app://" - target = "admin.product-index.selection-action.link" -`) + expect(got).toEqual({ + extensions: [ + { + type: 'admin_link', + name: 'Bulk action label', + handle: 'bulk-action-title', + targeting: [ + { + url: 'app://', + target: 'admin.product-index.selection-action.link', + }, + ], + }, + ], + }) }) - test('correctly builds a toml string for bulk_action extension with no path but search query in an embedded app', () => { + test('correctly builds a toml object for bulk_action extension with no path but search query in an embedded app', () => { // Given const appConfig = { path: '', @@ -131,14 +149,20 @@ handle = "bulk-action-title" const got = buildTomlObject(extension1, [], appConfig) // Then - expect(got).toEqual(`[[extensions]] -type = "admin_link" -name = "Bulk action label" -handle = "bulk-action-title" - - [[extensions.targeting]] - url = "app://?foo=bar" - target = "admin.product-index.selection-action.link" -`) + expect(got).toEqual({ + extensions: [ + { + type: 'admin_link', + name: 'Bulk action label', + handle: 'bulk-action-title', + targeting: [ + { + url: 'app://?foo=bar', + target: 'admin.product-index.selection-action.link', + }, + ], + }, + ], + }) }) }) diff --git a/packages/app/src/cli/services/admin-link/extension-to-toml.ts b/packages/app/src/cli/services/admin-link/extension-to-toml.ts index 7c0d5faa7ac..71e426fd361 100644 --- a/packages/app/src/cli/services/admin-link/extension-to-toml.ts +++ b/packages/app/src/cli/services/admin-link/extension-to-toml.ts @@ -2,7 +2,6 @@ import {contextToTarget} from './utils.js' import {ExtensionRegistration} from '../../api/graphql/all_app_extension_registrations.js' import {MAX_EXTENSION_HANDLE_LENGTH} from '../../models/extensions/schemas.js' import {CurrentAppConfiguration} from '../../models/app/app.js' -import {encodeToml} from '@shopify/cli-kit/node/toml' import {slugify} from '@shopify/cli-kit/common/string' interface AdminLinkConfig { @@ -17,7 +16,7 @@ export function buildTomlObject( extension: ExtensionRegistration, _: ExtensionRegistration[], appConfiguration: CurrentAppConfiguration, -): string { +): object { const versionConfig = extension.activeVersion?.config ?? extension.draftVersion?.config if (!versionConfig) throw new Error('No config found for extension') @@ -55,6 +54,5 @@ export function buildTomlObject( }, ], } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return encodeToml(localExtensionRepresentation as any) + return localExtensionRepresentation } diff --git a/packages/app/src/cli/services/app/add-uid-to-extension-toml.test.ts b/packages/app/src/cli/services/app/add-uid-to-extension-toml.test.ts index ded45e32c73..1406a8ce833 100644 --- a/packages/app/src/cli/services/app/add-uid-to-extension-toml.test.ts +++ b/packages/app/src/cli/services/app/add-uid-to-extension-toml.test.ts @@ -64,7 +64,6 @@ describe('addUidToTomlsIfNecessary', () => { // Then const updatedContent = await readFile(tomlPath) expect(updatedContent).toContain('uid = "123"') - expect(updatedContent).toMatch(/uid.*type/s) }) }) diff --git a/packages/app/src/cli/services/app/add-uid-to-extension-toml.ts b/packages/app/src/cli/services/app/add-uid-to-extension-toml.ts index 102727a1a98..da816fecc0e 100644 --- a/packages/app/src/cli/services/app/add-uid-to-extension-toml.ts +++ b/packages/app/src/cli/services/app/add-uid-to-extension-toml.ts @@ -1,7 +1,6 @@ import {ExtensionInstance} from '../../models/extensions/extension-instance.js' import {DeveloperPlatformClient} from '../../utilities/developer-platform-client.js' -import {decodeToml} from '@shopify/cli-kit/node/toml' -import {readFile, writeFile} from '@shopify/cli-kit/node/fs' +import {TomlFile} from '@shopify/cli-kit/node/toml/toml-file' import {getPathValue} from '@shopify/cli-kit/common/object' export async function addUidToTomlsIfNecessary( @@ -20,26 +19,26 @@ export async function addUidToTomlsIfNecessary( async function addUidToToml(extension: ExtensionInstance) { if (!extension.isUUIDStrategyExtension || extension.configuration.uid) return - const tomlContents = await readFile(extension.configurationPath) - const extensionConfig = decodeToml(tomlContents) - const extensions = getPathValue(extensionConfig, 'extensions') as ExtensionInstance[] + const file = await TomlFile.read(extension.configurationPath) + const extensionsArray = getPathValue(file.content, 'extensions') as ExtensionInstance[] - if ('uid' in extensionConfig) return - if (extensions) { - const currentExtension = extensions.find((ext) => ext.handle === extension.handle) + if ('uid' in file.content) return + if (extensionsArray) { + const currentExtension = extensionsArray.find((ext) => ext.handle === extension.handle) if (currentExtension && 'uid' in currentExtension) return } - let updatedTomlContents = tomlContents - if (extensions?.length > 1) { - // If the TOML has multiple extensions, we look for the correct handle to add the uid below - const regex = new RegExp(`(\\n?(\\s*)handle\\s*=\\s*"${extension.handle}")`) - updatedTomlContents = tomlContents.replace(regex, `$1\n$2uid = "${extension.uid}"`) + if (extensionsArray && extensionsArray.length > 1) { + // Multi-extension TOML: use regex to insert uid after the correct handle. + // updateTomlValues (WASM) doesn't support patching individual array-of-tables entries, + // so transformRaw with positional insertion is the pragmatic choice here. + const handle = extension.handle + await file.transformRaw((raw) => { + const regex = new RegExp(`(\\n?(\\s*)handle\\s*=\\s*"${handle}")`) + return raw.replace(regex, `$1\n$2uid = "${extension.uid}"`) + }) } else { - // If the TOML has only one extension, we add the uid before the type, which is always present - if ('uid' in extensionConfig) return - const regex = /\n?((\s*)type\s*=\s*"\S*")/ - updatedTomlContents = tomlContents.replace(regex, `$2\nuid = "${extension.uid}"\n$1`) + // Single extension (or no extensions array): add uid at the top level via WASM patch + await file.patch({uid: extension.uid}) } - await writeFile(extension.configurationPath, updatedTomlContents) } diff --git a/packages/app/src/cli/services/app/patch-app-configuration-file.test.ts b/packages/app/src/cli/services/app/patch-app-configuration-file.test.ts index c2f9af0a6e7..e9784411bb6 100644 --- a/packages/app/src/cli/services/app/patch-app-configuration-file.test.ts +++ b/packages/app/src/cli/services/app/patch-app-configuration-file.test.ts @@ -1,39 +1,8 @@ -import { - patchAppHiddenConfigFile, - setAppConfigValue, - unsetAppConfigValue, - setManyAppConfigValues, -} from './patch-app-configuration-file.js' +import {patchAppHiddenConfigFile} from './patch-app-configuration-file.js' import {readFile, writeFileSync, inTemporaryDirectory} from '@shopify/cli-kit/node/fs' import {joinPath} from '@shopify/cli-kit/node/path' import {describe, expect, test} from 'vitest' -const defaultToml = `# Learn more about configuring your app at https://shopify.dev/docs/apps/tools/cli/configuration -client_id = "12345" -name = "app1" -application_url = "https://example.com" -embedded = true - -[access_scopes] -# Learn more at https://shopify.dev/docs/apps/tools/cli/configuration#access_scopes -use_legacy_install_flow = true - -[auth] -redirect_urls = [ - "https://example.com/redirect", - "https://example.com/redirect2" -] - -[webhooks] -api_version = "2023-04" -` - -function writeDefaulToml(tmpDir: string) { - const configPath = joinPath(tmpDir, 'shopify.app.toml') - writeFileSync(configPath, defaultToml) - return configPath -} - describe('patchAppHiddenConfigFile', () => { test('creates a new hidden config file when it does not exist', async () => { await inTemporaryDirectory(async (tmpDir) => { @@ -114,158 +83,3 @@ describe('patchAppHiddenConfigFile', () => { }) }) }) - -describe('setAppConfigValue', () => { - test('sets a top-level value in the configuration', async () => { - await inTemporaryDirectory(async (tmpDir) => { - const configPath = writeDefaulToml(tmpDir) - - await setAppConfigValue(configPath, 'name', 'Updated App Name') - - const updatedTomlFile = await readFile(configPath) - expect(updatedTomlFile).toContain('name = "Updated App Name"') - }) - }) - - test('sets a nested value in the configuration', async () => { - await inTemporaryDirectory(async (tmpDir) => { - const configPath = writeDefaulToml(tmpDir) - - await setAppConfigValue(configPath, 'build.dev_store_url', 'example.myshopify.com') - - const updatedTomlFile = await readFile(configPath) - expect(updatedTomlFile).toContain('[build]') - expect(updatedTomlFile).toContain('dev_store_url = "example.myshopify.com"') - }) - }) - - test('sets a deeply nested value in the configuration', async () => { - await inTemporaryDirectory(async (tmpDir) => { - const configPath = writeDefaulToml(tmpDir) - - await setAppConfigValue(configPath, 'build.auth.settings', true) - - const updatedTomlFile = await readFile(configPath) - expect(updatedTomlFile).toContain('[build.auth]') - expect(updatedTomlFile).toContain('settings = true') - }) - }) -}) - -describe('unsetAppConfigValue', () => { - test('unsets a top-level value in the configuration', async () => { - await inTemporaryDirectory(async (tmpDir) => { - const configPath = writeDefaulToml(tmpDir) - - await unsetAppConfigValue(configPath, 'name') - - const updatedTomlFile = await readFile(configPath) - expect(updatedTomlFile).not.toContain('name = "app1"') - }) - }) - - test('unsets a nested value in existing table in the configuration', async () => { - await inTemporaryDirectory(async (tmpDir) => { - const configPath = writeDefaulToml(tmpDir) - - // Add a value first - await setAppConfigValue(configPath, 'build.dev_store_url', 'example.myshopify.com') - - // Now unset it - await unsetAppConfigValue(configPath, 'build.dev_store_url') - - const updatedTomlFile = await readFile(configPath) - expect(updatedTomlFile).toContain('[build]') - expect(updatedTomlFile).not.toContain('dev_store_url = "example.myshopify.com"') - }) - }) -}) - -describe('setManyAppConfigValues', () => { - test('sets multiple top-level values in the configuration', async () => { - await inTemporaryDirectory(async (tmpDir) => { - const configPath = writeDefaulToml(tmpDir) - - await setManyAppConfigValues(configPath, [ - {keyPath: 'name', value: 'Updated App Name'}, - {keyPath: 'client_id', value: '67890'}, - ]) - - const updatedTomlFile = await readFile(configPath) - expect(updatedTomlFile).toContain('name = "Updated App Name"') - expect(updatedTomlFile).toContain('client_id = "67890"') - }) - }) - - test('sets a mix of top-level and nested values in the configuration', async () => { - await inTemporaryDirectory(async (tmpDir) => { - const configPath = writeDefaulToml(tmpDir) - - await setManyAppConfigValues(configPath, [ - {keyPath: 'name', value: 'Updated App Name'}, - {keyPath: 'build.dev_store_url', value: 'example.myshopify.com'}, - ]) - - const updatedTomlFile = await readFile(configPath) - expect(updatedTomlFile).toContain('name = "Updated App Name"') - expect(updatedTomlFile).toContain('[build]') - expect(updatedTomlFile).toContain('dev_store_url = "example.myshopify.com"') - }) - }) - - test('properly handles array values in the configuration', async () => { - await inTemporaryDirectory(async (tmpDir) => { - const configPath = writeDefaulToml(tmpDir) - - await setManyAppConfigValues(configPath, [ - {keyPath: 'auth.redirect_urls', value: ['https://example.com/redirect3', 'https://example.com/redirect4']}, - ]) - - const updatedTomlFile = await readFile(configPath) - expect(updatedTomlFile).toContain('[auth]') - expect(updatedTomlFile).toContain('redirect_urls = [') - expect(updatedTomlFile).toContain('"https://example.com/redirect3"') - expect(updatedTomlFile).toContain('"https://example.com/redirect4"') - expect(updatedTomlFile).not.toContain('"https://example.com/redirect"') - expect(updatedTomlFile).not.toContain('"https://example.com/redirect2"') - }) - }) - - test('combines multiple nested keys into a single object structure', async () => { - await inTemporaryDirectory(async (tmpDir) => { - const configPath = writeDefaulToml(tmpDir) - - await setManyAppConfigValues(configPath, [ - {keyPath: 'build.dev_store_url', value: 'example.myshopify.com'}, - {keyPath: 'build.automatically_update_urls_on_dev', value: true}, - ]) - - const updatedTomlFile = await readFile(configPath) - expect(updatedTomlFile).toContain('[build]') - expect(updatedTomlFile).toContain('dev_store_url = "example.myshopify.com"') - expect(updatedTomlFile).toContain('automatically_update_urls_on_dev = true') - }) - }) - - test('updates existing configuration with new values and replaces arrays', async () => { - await inTemporaryDirectory(async (tmpDir) => { - const configPath = writeDefaulToml(tmpDir) - - await setManyAppConfigValues(configPath, [ - {keyPath: 'name', value: 'Updated App Name'}, - {keyPath: 'application_url', value: 'https://example.com'}, - {keyPath: 'access_scopes.use_legacy_install_flow', value: false}, - {keyPath: 'auth.redirect_urls', value: ['https://example.com/redirect3', 'https://example.com/redirect4']}, - ]) - - const updatedTomlFile = await readFile(configPath) - expect(updatedTomlFile).toContain('name = "Updated App Name"') - expect(updatedTomlFile).toContain('application_url = "https://example.com"') - expect(updatedTomlFile).toContain('use_legacy_install_flow = false') - expect(updatedTomlFile).toContain('redirect_urls = [') - expect(updatedTomlFile).toContain('"https://example.com/redirect3"') - expect(updatedTomlFile).toContain('"https://example.com/redirect4"') - expect(updatedTomlFile).not.toContain('"https://example.com/redirect"') - }) - }) -}) diff --git a/packages/app/src/cli/services/app/patch-app-configuration-file.ts b/packages/app/src/cli/services/app/patch-app-configuration-file.ts index e62ddecb3d0..cfce44be796 100644 --- a/packages/app/src/cli/services/app/patch-app-configuration-file.ts +++ b/packages/app/src/cli/services/app/patch-app-configuration-file.ts @@ -1,67 +1,6 @@ import {AppHiddenConfig} from '../../models/app/app.js' import {deepMergeObjects} from '@shopify/cli-kit/common/object' import {readFile, writeFile} from '@shopify/cli-kit/node/fs' -import {zod} from '@shopify/cli-kit/node/schema' -import {updateTomlValues} from '@shopify/toml-patch' - -export interface PatchTomlOptions { - path: string - patch: {[key: string]: unknown} - schema?: zod.AnyZodObject -} - -type TomlPatchValue = string | number | boolean | undefined | string[] - -async function patchAppConfigurationFileWithWasm( - path: string, - configValues: {keyPath: string; value: TomlPatchValue}[], -) { - const tomlContents = await readFile(path) - const updatedConfig = await updateTomlValues( - tomlContents, - configValues.map(({keyPath, value}) => [keyPath.split('.'), value]), - ) - await writeFile(path, updatedConfig) -} - -/** - * Sets a single value in the app configuration file based on a dotted key path. - * - * @param path - The path to the app configuration file. - * @param keyPath - The dotted key path to set the value at (e.g. 'build.dev_store_url') - * @param value - The value to set - */ -export async function setAppConfigValue(path: string, keyPath: string, value: TomlPatchValue) { - return patchAppConfigurationFileWithWasm(path, [{keyPath, value}]) -} - -/** - * Sets multiple values in the app configuration file. - * - * @param path - The path to the app configuration file - * @param configValues - Array of keyPath and value pairs to set - * - * @example - * ```ts - * await setManyAppConfigValues('shopify.app.toml', [ - * { keyPath: 'application_url', value: 'https://example.com' }, - * { keyPath: 'auth.redirect_urls', value: ['https://example.com/callback'] } - * ]) - * ``` - */ -export async function setManyAppConfigValues(path: string, configValues: {keyPath: string; value: TomlPatchValue}[]) { - return patchAppConfigurationFileWithWasm(path, configValues) -} - -/** - * Unsets a value in the app configuration file based on a dotted key path. - * - * @param path - The path to the app configuration file. - * @param keyPath - The dotted key path to unset (e.g. 'build.include_config_on_deploy') - */ -export async function unsetAppConfigValue(path: string, keyPath: string) { - return patchAppConfigurationFileWithWasm(path, [{keyPath, value: undefined}]) -} function replaceArrayStrategy(_: unknown[], newArray: unknown[]): unknown[] { return newArray diff --git a/packages/app/src/cli/services/app/write-app-configuration-file.ts b/packages/app/src/cli/services/app/write-app-configuration-file.ts index d4b0f3b3d41..4bc3f3ac6b8 100644 --- a/packages/app/src/cli/services/app/write-app-configuration-file.ts +++ b/packages/app/src/cli/services/app/write-app-configuration-file.ts @@ -1,13 +1,11 @@ import {CurrentAppConfiguration} from '../../models/app/app.js' import {reduceWebhooks} from '../../models/extensions/specifications/transform/app_config_webhook.js' import {removeTrailingSlash} from '../../models/extensions/specifications/validation/common.js' -import {writeFileSync} from '@shopify/cli-kit/node/fs' -import {JsonMapType, encodeToml} from '@shopify/cli-kit/node/toml' +import {TomlFile} from '@shopify/cli-kit/node/toml/toml-file' +import {JsonMapType} from '@shopify/cli-kit/node/toml' import {zod} from '@shopify/cli-kit/node/schema' import {outputDebug} from '@shopify/cli-kit/node/output' -// toml does not support comments and there aren't currently any good/maintained libs for this, -// so for now, we manually add comments export async function writeAppConfigurationFile(configuration: CurrentAppConfiguration, schema: zod.ZodTypeAny) { outputDebug(`Writing app configuration to ${configuration.path}`) @@ -16,15 +14,11 @@ export async function writeAppConfigurationFile(configuration: CurrentAppConfigu // the same [[webhooks.subscriptions]] in the TOML const condensedWebhooksAppConfiguration = condenseComplianceAndNonComplianceWebhooks(configuration) - const sorted = rewriteConfiguration(schema, condensedWebhooksAppConfiguration) as { - [key: string]: string | boolean | object - } - - const encodedString = encodeToml(sorted as JsonMapType) - - const file = addDefaultCommentsToToml(encodedString) + const sorted = rewriteConfiguration(schema, condensedWebhooksAppConfiguration) as JsonMapType - writeFileSync(configuration.path, file) + const file = new TomlFile(configuration.path, {}) + await file.replace(sorted) + await file.transformRaw(addDefaultCommentsToToml) } export const rewriteConfiguration = (schema: T, config: unknown): unknown => { diff --git a/packages/app/src/cli/services/context.test.ts b/packages/app/src/cli/services/context.test.ts index 2a0a98d0c09..37666bc6281 100644 --- a/packages/app/src/cli/services/context.test.ts +++ b/packages/app/src/cli/services/context.test.ts @@ -7,7 +7,6 @@ import {createExtension} from './dev/create-extension.js' import {CachedAppInfo} from './local-storage.js' import link from './app/config/link.js' import {fetchSpecifications} from './generate/fetch-extension-specifications.js' -import * as patchAppConfigurationFileModule from './app/patch-app-configuration-file.js' import {DeployOptions} from './deploy.js' import { MinimalAppIdentifiers, @@ -35,6 +34,7 @@ import { selectDeveloperPlatformClient, } from '../utilities/developer-platform-client.js' import {RemoteAwareExtensionSpecification} from '../models/extensions/specification.js' +import {TomlFile} from '@shopify/cli-kit/node/toml/toml-file' import {isServiceAccount, isUserAccount} from '@shopify/cli-kit/node/session' import {afterEach, beforeAll, beforeEach, describe, expect, test, vi} from 'vitest' import {AbortError} from '@shopify/cli-kit/node/error' @@ -142,13 +142,8 @@ vi.mock('@shopify/cli-kit/node/session') vi.mock('./generate/fetch-extension-specifications.js') vi.mock('./app/select-app.js') vi.mock('../utilities/developer-platform-client.js') -vi.mock('./app/patch-app-configuration-file.js', () => { - return { - patchAppConfigurationFile: vi.fn(), - setAppConfigValue: vi.fn(), - unsetAppConfigValue: vi.fn(), - } -}) +const mockTomlFilePatch = vi.fn() +const mockTomlFileRemove = vi.fn() beforeAll(async () => { const localSpecs = await loadSpecifications.loadLocalExtensionsSpecifications() @@ -169,6 +164,12 @@ beforeEach(async () => { vi.mocked(getPackageManager).mockResolvedValue('npm') vi.mocked(isWebType).mockReturnValue(true) vi.mocked(renderConfirmationPrompt).mockResolvedValue(false) + vi.spyOn(TomlFile, 'read').mockResolvedValue({ + patch: mockTomlFilePatch, + remove: mockTomlFileRemove, + content: {}, + path: '', + } as unknown as TomlFile) vi.mocked(link).mockResolvedValue({ configuration: testAppWithConfig({config: {path: 'shopify.app.toml', client_id: APP2.apiKey}}).configuration, remoteApp: APP2, @@ -205,7 +206,7 @@ describe('ensureDeployContext', () => { vi.mocked(loadApp).mockResolvedValue(app) vi.mocked(link).mockResolvedValue((app as any).configuration) vi.mocked(getAppConfigurationFileName).mockReturnValue('shopify.app.toml') - const setAppConfigValueSpy = vi.spyOn(patchAppConfigurationFileModule, 'setAppConfigValue').mockResolvedValue() + mockTomlFilePatch.mockResolvedValue(undefined) const metadataSpyOn = vi.spyOn(metadata, 'addPublicMetadata').mockImplementation(async () => {}) const options = deployOptions(app) @@ -218,7 +219,7 @@ describe('ensureDeployContext', () => { expect(metadataSpyOn).toHaveBeenCalled() expect(renderConfirmationPrompt).toHaveBeenCalled() - expect(setAppConfigValueSpy).toHaveBeenCalled() + expect(mockTomlFilePatch).toHaveBeenCalled() expect(renderInfo).toHaveBeenCalledWith({ body: [ { @@ -235,7 +236,7 @@ describe('ensureDeployContext', () => { ], headline: 'Using shopify.app.toml for default values:', }) - setAppConfigValueSpy.mockRestore() + mockTomlFilePatch.mockClear() }) test('doesnt prompt the user to include the configuration and display the current value if the config is true', async () => { @@ -253,7 +254,7 @@ describe('ensureDeployContext', () => { vi.mocked(link).mockResolvedValue((app as any).configuration) // vi.mocked(selectDeveloperPlatformClient).mockReturnValue(testDeveloperPlatformClient) vi.mocked(getAppConfigurationFileName).mockReturnValue('shopify.app.toml') - const setAppConfigValueSpy = vi.spyOn(patchAppConfigurationFileModule, 'setAppConfigValue').mockResolvedValue() + mockTomlFilePatch.mockResolvedValue(undefined) const metadataSpyOn = vi.spyOn(metadata, 'addPublicMetadata').mockImplementation(async () => {}) const options = deployOptions(app) @@ -266,7 +267,7 @@ describe('ensureDeployContext', () => { expect(metadataSpyOn).not.toHaveBeenCalled() expect(renderConfirmationPrompt).not.toHaveBeenCalled() - expect(setAppConfigValueSpy).not.toHaveBeenCalled() + expect(mockTomlFilePatch).not.toHaveBeenCalled() expect(renderInfo).toHaveBeenCalledWith({ body: [ { @@ -283,7 +284,7 @@ describe('ensureDeployContext', () => { ], headline: 'Using shopify.app.toml for default values:', }) - setAppConfigValueSpy.mockRestore() + mockTomlFilePatch.mockClear() }) test('prompts the user to include the configuration when reset is used and the flag is present', async () => { @@ -298,7 +299,7 @@ describe('ensureDeployContext', () => { vi.mocked(ensureDeploymentIdsPresence).mockResolvedValue(identifiers) vi.mocked(renderConfirmationPrompt).mockResolvedValue(false) vi.mocked(getAppConfigurationFileName).mockReturnValue('shopify.app.toml') - const setAppConfigValueSpy = vi.spyOn(patchAppConfigurationFileModule, 'setAppConfigValue').mockResolvedValue() + mockTomlFilePatch.mockResolvedValue(undefined) const metadataSpyOn = vi.spyOn(metadata, 'addPublicMetadata').mockImplementation(async () => {}) const options = deployOptions(app, true) @@ -311,7 +312,7 @@ describe('ensureDeployContext', () => { expect(metadataSpyOn.mock.calls[0]![0]()).toEqual({cmd_deploy_confirm_include_config_used: false}) expect(renderConfirmationPrompt).toHaveBeenCalled() - expect(setAppConfigValueSpy).toHaveBeenCalledWith(app.configuration.path, 'build.include_config_on_deploy', false) + expect(mockTomlFilePatch).toHaveBeenCalledWith({build: {include_config_on_deploy: false}}) expect(renderInfo).toHaveBeenCalledWith({ body: [ @@ -329,7 +330,7 @@ describe('ensureDeployContext', () => { ], headline: 'Using shopify.app.toml for default values:', }) - setAppConfigValueSpy.mockRestore() + mockTomlFilePatch.mockClear() }) test('aborts when force is true and include_config_on_deploy is not set on Partners', async () => { @@ -386,14 +387,14 @@ describe('ensureDeployContext', () => { vi.mocked(ensureDeploymentIdsPresence).mockResolvedValue(identifiers) vi.mocked(renderConfirmationPrompt).mockResolvedValue(false) vi.mocked(getAppConfigurationFileName).mockReturnValue('shopify.app.toml') - const setAppConfigValueSpy = vi.spyOn(patchAppConfigurationFileModule, 'setAppConfigValue').mockResolvedValue() + mockTomlFilePatch.mockResolvedValue(undefined) // When await ensureDeployContext(deployOptions(app, false, true)) // Then expect(renderConfirmationPrompt).not.toHaveBeenCalled() - expect(setAppConfigValueSpy).not.toHaveBeenCalled() + expect(mockTomlFilePatch).not.toHaveBeenCalled() expect(renderInfo).toHaveBeenCalledWith({ body: [ { @@ -410,7 +411,7 @@ describe('ensureDeployContext', () => { ], headline: 'Using shopify.app.toml for default values:', }) - setAppConfigValueSpy.mockRestore() + mockTomlFilePatch.mockClear() }) test('removes the include_config_on_deploy field when using app management API and the value is true', async () => { @@ -424,7 +425,7 @@ describe('ensureDeployContext', () => { } vi.mocked(ensureDeploymentIdsPresence).mockResolvedValue(identifiers) vi.mocked(getAppConfigurationFileName).mockReturnValue('shopify.app.toml') - const unsetAppConfigValueSpy = vi.spyOn(patchAppConfigurationFileModule, 'unsetAppConfigValue').mockResolvedValue() + mockTomlFileRemove.mockResolvedValue(undefined) // When const options = { @@ -441,7 +442,7 @@ describe('ensureDeployContext', () => { // Then expect(renderConfirmationPrompt).not.toHaveBeenCalled() - expect(unsetAppConfigValueSpy).toHaveBeenCalledWith(app.configuration.path, 'build.include_config_on_deploy') + expect(mockTomlFileRemove).toHaveBeenCalledWith('build.include_config_on_deploy') expect(renderInfo).toHaveBeenCalledWith({ body: [ 'The `include_config_on_deploy` field is no longer supported, since all apps must now include configuration on deploy. It has been removed from your configuration file.', @@ -452,7 +453,7 @@ describe('ensureDeployContext', () => { url: 'https://shopify.dev/docs/apps/build/cli-for-apps/app-configuration#build', }, }) - unsetAppConfigValueSpy.mockRestore() + mockTomlFileRemove.mockClear() }) test('removes the include_config_on_deploy field when using app management API and the value is false', async () => { @@ -466,7 +467,7 @@ describe('ensureDeployContext', () => { } vi.mocked(ensureDeploymentIdsPresence).mockResolvedValue(identifiers) vi.mocked(getAppConfigurationFileName).mockReturnValue('shopify.app.toml') - const unsetAppConfigValueSpy = vi.spyOn(patchAppConfigurationFileModule, 'unsetAppConfigValue').mockResolvedValue() + mockTomlFileRemove.mockResolvedValue(undefined) // When const options = { @@ -483,7 +484,7 @@ describe('ensureDeployContext', () => { // Then expect(renderConfirmationPrompt).not.toHaveBeenCalled() - expect(unsetAppConfigValueSpy).toHaveBeenCalledWith(app.configuration.path, 'build.include_config_on_deploy') + expect(mockTomlFileRemove).toHaveBeenCalledWith('build.include_config_on_deploy') expect(renderWarning).toHaveBeenCalledWith({ body: [ "The `include_config_on_deploy` field is no longer supported and has been removed from your configuration file. Review this file to ensure it's up to date with the correct configuration.", @@ -494,7 +495,7 @@ describe('ensureDeployContext', () => { url: 'https://shopify.dev/docs/apps/build/cli-for-apps/app-configuration#build', }, }) - unsetAppConfigValueSpy.mockRestore() + mockTomlFileRemove.mockClear() }) test('sets didMigrateExtensionsToDevDash to true when app modules are missing registration IDs', async () => { diff --git a/packages/app/src/cli/services/context.ts b/packages/app/src/cli/services/context.ts index a89a30efbca..326f472046f 100644 --- a/packages/app/src/cli/services/context.ts +++ b/packages/app/src/cli/services/context.ts @@ -3,7 +3,6 @@ import {fetchOrganizations} from './dev/fetch.js' import {ensureDeploymentIdsPresence} from './context/identifiers.js' import {createExtension} from './dev/create-extension.js' import {CachedAppInfo} from './local-storage.js' -import {setAppConfigValue, unsetAppConfigValue} from './app/patch-app-configuration-file.js' import {DeployOptions} from './deploy.js' import {formatConfigInfoBody} from './format-config-info-body.js' import {selectOrganizationPrompt} from '../prompts/dev.js' @@ -25,6 +24,7 @@ import { DeveloperPlatformClient, selectDeveloperPlatformClient, } from '../utilities/developer-platform-client.js' +import {TomlFile} from '@shopify/cli-kit/node/toml/toml-file' import {isServiceAccount, isUserAccount} from '@shopify/cli-kit/node/session' import {tryParseInt} from '@shopify/cli-kit/common/string' import {Token, renderConfirmationPrompt, renderInfo, renderWarning} from '@shopify/cli-kit/node/ui' @@ -237,7 +237,8 @@ async function removeIncludeConfigOnDeployField(localApp: AppInterface) { const includeConfigOnDeploy = configuration.build?.include_config_on_deploy if (includeConfigOnDeploy === undefined) return - await unsetAppConfigValue(localApp.configuration.path, 'build.include_config_on_deploy') + const configFile = await TomlFile.read(localApp.configuration.path) + await configFile.remove('build.include_config_on_deploy') includeConfigOnDeploy ? renderInfoAboutIncludeConfigOnDeploy() : renderWarningAboutIncludeConfigOnDeploy() } @@ -275,7 +276,8 @@ async function promptAndSaveIncludeConfigOnDeploy(options: ShouldOrPromptInclude ...localConfiguration.build, include_config_on_deploy: shouldIncludeConfigDeploy, } - await setAppConfigValue(localConfiguration.path, 'build.include_config_on_deploy', shouldIncludeConfigDeploy) + const configFile = await TomlFile.read(localConfiguration.path) + await configFile.patch({build: {include_config_on_deploy: shouldIncludeConfigDeploy}}) await metadata.addPublicMetadata(() => ({cmd_deploy_confirm_include_config_used: shouldIncludeConfigDeploy})) return shouldIncludeConfigDeploy } diff --git a/packages/app/src/cli/services/context/breakdown-extensions.test.ts b/packages/app/src/cli/services/context/breakdown-extensions.test.ts index e74ab310183..69e9746556b 100644 --- a/packages/app/src/cli/services/context/breakdown-extensions.test.ts +++ b/packages/app/src/cli/services/context/breakdown-extensions.test.ts @@ -1847,9 +1847,10 @@ describe('configExtensionsIdentifiersBreakdown', () => { expect(result).toEqual({ existingFieldNames: [], existingUpdatedFieldNames: [], - newFieldNames: ['name', 'application_url', 'embedded', 'webhooks'], + newFieldNames: expect.arrayContaining(['name', 'application_url', 'embedded', 'webhooks']), deletedFieldNames: [], }) + expect(result!.newFieldNames).toHaveLength(4) }) }) describe('deploy not including the configuration app modules', () => { diff --git a/packages/app/src/cli/services/context/breakdown-extensions.ts b/packages/app/src/cli/services/context/breakdown-extensions.ts index 283281b46e0..84413719791 100644 --- a/packages/app/src/cli/services/context/breakdown-extensions.ts +++ b/packages/app/src/cli/services/context/breakdown-extensions.ts @@ -21,7 +21,6 @@ import {AppConfigurationUsedByCli} from '../../models/extensions/specifications/ import {removeTrailingSlash} from '../../models/extensions/specifications/validation/common.js' import {throwUidMappingError} from '../../prompts/uid-mapping-error.js' import {deepCompare, deepDifference} from '@shopify/cli-kit/common/object' -import {encodeToml} from '@shopify/cli-kit/node/toml' import {zod} from '@shopify/cli-kit/node/schema' export interface ConfigExtensionIdentifiersBreakdown { @@ -220,19 +219,15 @@ async function resolveRemoteConfigExtensionIdentifiersBreakdown( }) } - const diffConfigContent = buildDiffConfigContent(baselineConfig, remoteConfig, app.configSchema) + const diffFieldNames = buildDiffFieldNames(baselineConfig, remoteConfig, app.configSchema) // List of field included in the config except the ones that only affect the CLI and are not pushed to the server // (versioned fields) const versionedLocalFieldNames = filterNonVersionedAppFields(baselineConfig) // List of remote fields that have different values to the local ones or are not present in the local config - const remoteDiffModifications = diffConfigContent - ? getFieldsFromDiffConfigContent(diffConfigContent.baselineContent) - : [] - // List of local fields that have different values to the remote ones or are not present in the remote config - const localDiffModifications = diffConfigContent - ? getFieldsFromDiffConfigContent(diffConfigContent.updatedContent) - : [] + const remoteDiffModifications = diffFieldNames?.baselineFieldNames ?? [] + // List of local fields that have different values to the remote ones or are not present in the remote config + const localDiffModifications = diffFieldNames?.updatedFieldNames ?? [] // List of versioned field that exists locally and remotely and have the same value const notModifiedVersionedLocalFieldNames = versionedLocalFieldNames.filter( (field) => !remoteDiffModifications.includes(field) && !localDiffModifications.includes(field), @@ -259,7 +254,11 @@ async function resolveRemoteConfigExtensionIdentifiersBreakdown( } } -function buildDiffConfigContent( +/** + * Computes the diff between local and remote config (after schema rewriting) and returns + * the top-level field names that differ on each side. + */ +function buildDiffFieldNames( localConfig: CurrentAppConfiguration, remoteConfig: Partial, schema: zod.ZodTypeAny, @@ -273,66 +272,17 @@ function buildDiffConfigContent( return undefined } + const definedKeys = (obj: object) => + Object.entries(obj) + .filter(([_, value]) => value !== undefined) + .map(([key]) => key) + return { - baselineContent: encodeToml(baseline), - updatedContent: encodeToml(updated), + updatedFieldNames: definedKeys(updated), + baselineFieldNames: definedKeys(baseline), } } -/** - * This method extracts the list of global fields or global sections from the string that represents a toml section like - * this: - * embedded = true - * - * [access_scopes] - * scopes = "read_products,write_products,write_discounts" - * - * [webhooks.privacy_compliance] - * customer_deletion_url = "https://myhooks.dev/apps/customer_deletion_url_edited" - * - * Each block is separated by a breaking line. The method will the extract - * the `field` following these patterns: - * - = (in this case all the fields inside the block that matches the pattern will be returned) - * - [] - * - [\] - * - * @param diffConfigContent - The toml string to parse - * @returns The list of fields - */ -function getFieldsFromDiffConfigContent(diffConfigContent: string): string[] { - const fields = diffConfigContent - // Split the input string into sections by one or more blank lines - .split(/\n\s*\n/) - .flatMap((section) => { - // Split each section into lines - const lines = section.split('\n') - if (lines.length === 0) return [] - // Match the first line of the section against a regular expression to extract the first field name based on the - // described patterns - const firstLineMatch = lines[0]!.match(/^(?:\[(\w+)|(\w+)\s*=)/) - if (!firstLineMatch) return [] - // Extract the first field name from the appropriate capture group - const firstFieldName = firstLineMatch[1] || firstLineMatch[2] - if (!firstFieldName) return [] - // Return field if matches either the pattern [\] or [] - if (firstFieldName.includes('.')) return [firstFieldName.split('.')[0]] - // If the first line of the section matches the pattern = extract the following - // = that match that condition until the section is finished - const otherFieldNames = firstLineMatch[2] - ? lines - .slice(1) - .map((line) => line.match(/^(\w+)\s*=/)) - .filter(Boolean) - .map((match) => match![1]) - : [] - return [firstFieldName, ...otherFieldNames] - }) - .filter((match): match is string => match !== undefined) - - // Return the list of fields without duplicates - return Array.from(new Set(fields)) -} - function loadLocalExtensionsIdentifiersBreakdown({ validMatches: localRegistration, extensionsToCreate: localSourceToCreate, diff --git a/packages/app/src/cli/services/dev.ts b/packages/app/src/cli/services/dev.ts index b4de346b615..c115b12fccb 100644 --- a/packages/app/src/cli/services/dev.ts +++ b/packages/app/src/cli/services/dev.ts @@ -24,7 +24,6 @@ import {DevProcessFunction} from './dev/processes/types.js' import {getCachedAppInfo, setCachedAppInfo} from './local-storage.js' import {canEnablePreviewMode} from './extensions/common.js' import {fetchAppRemoteConfiguration} from './app/select-app.js' -import {setAppConfigValue} from './app/patch-app-configuration-file.js' import {DevSessionStatusManager} from './dev/processes/dev-session/dev-session-status-manager.js' import {TunnelMode} from './dev/tunnel-mode.js' import {PortDetail, renderPortWarnings} from './dev/port-warnings.js' @@ -38,6 +37,7 @@ import {RemoteAwareExtensionSpecification} from '../models/extensions/specificat import {ports} from '../constants.js' import {generateCertificate} from '../utilities/mkcert.js' import {throwUidMappingError} from '../prompts/uid-mapping-error.js' +import {TomlFile} from '@shopify/cli-kit/node/toml/toml-file' import {Config} from '@oclif/core' import {AbortController} from '@shopify/cli-kit/node/abort' import {checkPortAvailability, getAvailableTCPPort} from '@shopify/cli-kit/node/tcp' @@ -114,7 +114,8 @@ async function prepareForDev(commandOptions: DevOptions): Promise { ...app.configuration.build, dev_store_url: store.shopDomain, } - await setAppConfigValue(app.configuration.path, 'build.dev_store_url', store.shopDomain) + const configFile = await TomlFile.read(app.configuration.path) + await configFile.patch({build: {dev_store_url: store.shopDomain}}) } if (!commandOptions.skipDependenciesInstallation && !app.usesWorkspaces) { diff --git a/packages/app/src/cli/services/dev/update-extension.ts b/packages/app/src/cli/services/dev/update-extension.ts index db193d0e33b..4340d0e9dcc 100644 --- a/packages/app/src/cli/services/dev/update-extension.ts +++ b/packages/app/src/cli/services/dev/update-extension.ts @@ -3,16 +3,13 @@ import { ExtensionUpdateDraftMutationVariables, } from '../../api/graphql/partners/generated/update-draft.js' import {AppConfigurationWithoutPath} from '../../models/app/app.js' -import { - loadConfigurationFileContent, - parseConfigurationFile, - parseConfigurationObjectAgainstSpecification, -} from '../../models/app/loader.js' +import {parseConfigurationFile, parseConfigurationObjectAgainstSpecification} from '../../models/app/loader.js' import {ExtensionInstance} from '../../models/extensions/extension-instance.js' import {ExtensionsArraySchema, UnifiedSchema} from '../../models/extensions/schemas.js' import {DeveloperPlatformClient} from '../../utilities/developer-platform-client.js' import {themeExtensionConfig} from '../deploy/theme-extension-config.js' import {AbortError} from '@shopify/cli-kit/node/error' +import {TomlFile} from '@shopify/cli-kit/node/toml/toml-file' import {readFile} from '@shopify/cli-kit/node/fs' import {OutputMessage, outputInfo} from '@shopify/cli-kit/node/output' import {relativizePath} from '@shopify/cli-kit/node/path' @@ -123,7 +120,8 @@ export async function reloadExtensionConfig({extension}: UpdateExtensionConfigOp throw new AbortError(message) } - let configObject = await loadConfigurationFileContent(extension.configurationPath) + const tomlFile = await TomlFile.read(extension.configurationPath) + let configObject = tomlFile.content const {extensions} = ExtensionsArraySchema.parse(configObject) if (extensions) { diff --git a/packages/app/src/cli/services/dev/urls.test.ts b/packages/app/src/cli/services/dev/urls.test.ts index af2869706bc..34dca162e59 100644 --- a/packages/app/src/cli/services/dev/urls.test.ts +++ b/packages/app/src/cli/services/dev/urls.test.ts @@ -15,8 +15,8 @@ import { } from '../../models/app/app.test-data.js' import {UpdateURLsVariables} from '../../api/graphql/update_urls.js' import {setCachedAppInfo} from '../local-storage.js' -import {setManyAppConfigValues} from '../app/patch-app-configuration-file.js' import {AppLinkedInterface} from '../../models/app/app.js' +import {TomlFile} from '@shopify/cli-kit/node/toml/toml-file' import {beforeEach, describe, expect, vi, test} from 'vitest' import {AbortError} from '@shopify/cli-kit/node/error' import {getAvailableTCPPort} from '@shopify/cli-kit/node/tcp' @@ -25,11 +25,7 @@ import {renderConfirmationPrompt, renderSelectPrompt} from '@shopify/cli-kit/nod import {terminalSupportsPrompting} from '@shopify/cli-kit/node/system' vi.mock('../local-storage.js') -vi.mock('../app/patch-app-configuration-file.js', () => { - return { - setManyAppConfigValues: vi.fn(), - } -}) +const mockTomlFilePatch = vi.fn() vi.mock('@shopify/cli-kit/node/tcp') vi.mock('@shopify/cli-kit/node/context/local') vi.mock('@shopify/cli-kit/node/plugins') @@ -40,6 +36,11 @@ beforeEach(() => { vi.mocked(getAvailableTCPPort).mockResolvedValue(3042) vi.mocked(isUnitTest).mockReturnValue(true) vi.mocked(terminalSupportsPrompting).mockReturnValue(true) + vi.spyOn(TomlFile, 'read').mockResolvedValue({ + patch: mockTomlFilePatch, + content: {}, + path: '', + } as unknown as TomlFile) }) const defaultOptions: FrontendURLOptions = { @@ -94,17 +95,16 @@ describe('updateURLs', () => { await updateURLs(urls, apiKey, testDeveloperPlatformClient(), appWithConfig) // Then - expect(setManyAppConfigValues).toHaveBeenCalledWith(appWithConfig.configuration.path, [ - {keyPath: 'application_url', value: 'https://example.com'}, - { - keyPath: 'auth.redirect_urls', - value: [ + expect(mockTomlFilePatch).toHaveBeenCalledWith({ + application_url: 'https://example.com', + auth: { + redirect_urls: [ 'https://example.com/auth/callback', 'https://example.com/auth/shopify/callback', 'https://example.com/api/auth/callback', ], }, - ]) + }) }) test('throws an error if requests has a user error', async () => { @@ -177,20 +177,21 @@ describe('updateURLs', () => { await updateURLs(urls, apiKey, testDeveloperPlatformClient(), appWithConfig) // Then - expect(setManyAppConfigValues).toHaveBeenCalledWith(appWithConfig.configuration.path, [ - {keyPath: 'application_url', value: 'https://example.com'}, - { - keyPath: 'auth.redirect_urls', - value: [ + expect(mockTomlFilePatch).toHaveBeenCalledWith({ + application_url: 'https://example.com', + auth: { + redirect_urls: [ 'https://example.com/auth/callback', 'https://example.com/auth/shopify/callback', 'https://example.com/api/auth/callback', ], }, - {keyPath: 'app_proxy.url', value: 'https://example.com'}, - {keyPath: 'app_proxy.subpath', value: 'subpath'}, - {keyPath: 'app_proxy.prefix', value: 'prefix'}, - ]) + app_proxy: { + url: 'https://example.com', + subpath: 'subpath', + prefix: 'prefix', + }, + }) }) }) @@ -350,7 +351,7 @@ describe('shouldOrPromptUpdateURLs', () => { // Then expect(result).toBe(true) expect(setCachedAppInfo).not.toHaveBeenCalled() - expect(setManyAppConfigValues).not.toHaveBeenCalled() + expect(mockTomlFilePatch).not.toHaveBeenCalled() }) test('updates the config file if current config client matches remote', async () => { @@ -375,9 +376,7 @@ describe('shouldOrPromptUpdateURLs', () => { // Then expect(result).toBe(true) expect(setCachedAppInfo).not.toHaveBeenCalled() - expect(setManyAppConfigValues).toHaveBeenCalledWith(localApp.configuration.path, [ - {keyPath: 'build.automatically_update_urls_on_dev', value: true}, - ]) + expect(mockTomlFilePatch).toHaveBeenCalledWith({build: {automatically_update_urls_on_dev: true}}) }) }) diff --git a/packages/app/src/cli/services/dev/urls.ts b/packages/app/src/cli/services/dev/urls.ts index 859a2dc855d..e17509c93e0 100644 --- a/packages/app/src/cli/services/dev/urls.ts +++ b/packages/app/src/cli/services/dev/urls.ts @@ -5,7 +5,7 @@ import {setCachedAppInfo} from '../local-storage.js' import {AppConfigurationUsedByCli} from '../../models/extensions/specifications/types/app_config.js' import {prependApplicationUrl} from '../../models/extensions/specifications/validation/url_prepender.js' import {DeveloperPlatformClient} from '../../utilities/developer-platform-client.js' -import {setManyAppConfigValues} from '../app/patch-app-configuration-file.js' +import {TomlFile} from '@shopify/cli-kit/node/toml/toml-file' import {AbortError, BugError} from '@shopify/cli-kit/node/error' import {Config} from '@oclif/core' import {isValidURL} from '@shopify/cli-kit/common/url' @@ -187,20 +187,21 @@ export async function updateURLs( } if (localApp && localApp.configuration.client_id === apiKey) { - const configValues = [ - {keyPath: 'application_url', value: urls.applicationUrl}, - {keyPath: 'auth.redirect_urls', value: urls.redirectUrlWhitelist}, - ] + const configFile = await TomlFile.read(localApp.configuration.path) + const patch: {[key: string]: unknown} = { + application_url: urls.applicationUrl, + auth: {redirect_urls: urls.redirectUrlWhitelist}, + } if (urls.appProxy) { - configValues.push( - {keyPath: 'app_proxy.url', value: urls.appProxy.proxyUrl}, - {keyPath: 'app_proxy.subpath', value: urls.appProxy.proxySubPath}, - {keyPath: 'app_proxy.prefix', value: urls.appProxy.proxySubPathPrefix}, - ) + patch.app_proxy = { + url: urls.appProxy.proxyUrl, + subpath: urls.appProxy.proxySubPath, + prefix: urls.appProxy.proxySubPathPrefix, + } } - await setManyAppConfigValues(localApp.configuration.path, configValues) + await configFile.patch(patch) } } @@ -249,8 +250,8 @@ export async function shouldOrPromptUpdateURLs(options: ShouldOrPromptUpdateURLs ...localConfiguration.build, automatically_update_urls_on_dev: shouldUpdateURLs, } - const path = options.localApp.configuration.path - await setManyAppConfigValues(path, [{keyPath: 'build.automatically_update_urls_on_dev', value: shouldUpdateURLs}]) + const configFile = await TomlFile.read(options.localApp.configuration.path) + await configFile.patch({build: {automatically_update_urls_on_dev: shouldUpdateURLs}}) } else { setCachedAppInfo({directory: options.appDirectory, updateURLs: shouldUpdateURLs}) } diff --git a/packages/app/src/cli/services/flow/extension-to-toml.test.ts b/packages/app/src/cli/services/flow/extension-to-toml.test.ts index 89d1ab1ace6..a7d4e058196 100644 --- a/packages/app/src/cli/services/flow/extension-to-toml.test.ts +++ b/packages/app/src/cli/services/flow/extension-to-toml.test.ts @@ -3,7 +3,7 @@ import {ExtensionRegistration} from '../../api/graphql/all_app_extension_registr import {describe, expect, test} from 'vitest' describe('extension-to-toml', () => { - test('correctly builds a toml string for a flow_action', () => { + test('correctly builds a toml object for a flow_action', () => { // Given const extension1: ExtensionRegistration = { id: '26237698049', @@ -20,38 +20,28 @@ describe('extension-to-toml', () => { const got = buildTomlObject(extension1) // Then - expect(got).toEqual(`[[extensions]] -type = "flow_action" -name = "action title" -handle = "flow-action-char" -description = "action description" -runtime_url = "https://google.es" -config_page_url = "https://destinationsurl.test.dev" -config_page_preview_url = "https://previewurl.test.dev" -validation_url = "https://validation.test.dev" - -[[settings.fields]] -type = "customer_reference" -required = true - -[[settings.fields]] -type = "product_reference" -required = true - -[[settings.fields]] -key = "email field" -description = "email help" -type = "email" -name = "email label" -required = false - -[[settings.fields]] -key = "number name" -description = "number help" -type = "number_decimal" -name = "number label" -required = true -`) + expect(got).toEqual({ + extensions: [ + { + type: 'flow_action', + name: 'action title', + handle: 'flow-action-char', + description: 'action description', + runtime_url: 'https://google.es', + config_page_url: 'https://destinationsurl.test.dev', + config_page_preview_url: 'https://previewurl.test.dev', + validation_url: 'https://validation.test.dev', + }, + ], + settings: { + fields: [ + {type: 'customer_reference', required: true}, + {type: 'product_reference', required: true}, + {key: 'email field', description: 'email help', type: 'email', name: 'email label', required: false}, + {key: 'number name', description: 'number help', type: 'number_decimal', name: 'number label', required: true}, + ], + }, + }) }) test('truncates the handle if the title has >50 characters', () => { @@ -71,41 +61,31 @@ required = true const got = buildTomlObject(extension1) // Then - expect(got).toEqual(`[[extensions]] -type = "flow_action" -name = "action title" -handle = "flow-action-char-flow-action-char-flow-actio" -description = "action description" -runtime_url = "https://google.es" -config_page_url = "https://destinationsurl.test.dev" -config_page_preview_url = "https://previewurl.test.dev" -validation_url = "https://validation.test.dev" - -[[settings.fields]] -type = "customer_reference" -required = true - -[[settings.fields]] -type = "product_reference" -required = true - -[[settings.fields]] -key = "email field" -description = "email help" -type = "email" -name = "email label" -required = false - -[[settings.fields]] -key = "number name" -description = "number help" -type = "number_decimal" -name = "number label" -required = true -`) + expect(got).toEqual({ + extensions: [ + { + type: 'flow_action', + name: 'action title', + handle: 'flow-action-char-flow-action-char-flow-actio', + description: 'action description', + runtime_url: 'https://google.es', + config_page_url: 'https://destinationsurl.test.dev', + config_page_preview_url: 'https://previewurl.test.dev', + validation_url: 'https://validation.test.dev', + }, + ], + settings: { + fields: [ + {type: 'customer_reference', required: true}, + {type: 'product_reference', required: true}, + {key: 'email field', description: 'email help', type: 'email', name: 'email label', required: false}, + {key: 'number name', description: 'number help', type: 'number_decimal', name: 'number label', required: true}, + ], + }, + }) }) - test('correctly builds a toml string for a flow_trigger', () => { + test('correctly builds a toml object for a flow_trigger', () => { // Given const extension2 = { id: '26237861889', @@ -126,24 +106,26 @@ required = true const got = buildTomlObject(extension2) // Then - expect(got).toEqual(`[[extensions]] -type = "flow_trigger" -name = "trigger title" -handle = "trigger-ext" -description = "trigger description" - -[[settings.fields]] -type = "customer_reference" - -[[settings.fields]] -key = "number property" -description = "number description" -type = "number_decimal" - -[[settings.fields]] -key = "email name" -description = "email description" -type = "email" -`) + expect(got).toEqual({ + extensions: [ + { + type: 'flow_trigger', + name: 'trigger title', + handle: 'trigger-ext', + description: 'trigger description', + runtime_url: undefined, + config_page_url: undefined, + config_page_preview_url: undefined, + validation_url: undefined, + }, + ], + settings: { + fields: [ + {type: 'customer_reference'}, + {key: 'number property', description: 'number description', type: 'number_decimal'}, + {key: 'email name', description: 'email description', type: 'email'}, + ], + }, + }) }) }) diff --git a/packages/app/src/cli/services/flow/extension-to-toml.ts b/packages/app/src/cli/services/flow/extension-to-toml.ts index 8c612637e3e..6c5f1775531 100644 --- a/packages/app/src/cli/services/flow/extension-to-toml.ts +++ b/packages/app/src/cli/services/flow/extension-to-toml.ts @@ -2,7 +2,6 @@ import {configFromSerializedFields} from './serialize-partners-fields.js' import {FlowPartnersExtensionTypes} from './types.js' import {ExtensionRegistration} from '../../api/graphql/all_app_extension_registrations.js' import {MAX_EXTENSION_HANDLE_LENGTH} from '../../models/extensions/schemas.js' -import {encodeToml} from '@shopify/cli-kit/node/toml' import {slugify} from '@shopify/cli-kit/common/string' // Used for importing flow_action_definition and flow_trigger_definition migrating them to flow_action and flow_trigger @@ -32,7 +31,7 @@ interface FlowWebhookConfig { * Given a flow extension config file, convert it to toml * Works for both trigger and action because trigger config is a subset of action config */ -export function buildTomlObject(extension: ExtensionRegistration): string { +export function buildTomlObject(extension: ExtensionRegistration): object { const versionConfig = extension.activeVersion?.config ?? extension.draftVersion?.config if (!versionConfig) throw new Error('No config found for extension') @@ -71,6 +70,5 @@ export function buildTomlObject(extension: ExtensionRegistration): string { settings: (fields?.length ?? 0) > 0 ? {fields} : undefined, } } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return encodeToml(localExtensionRepresentation as any) + return localExtensionRepresentation } diff --git a/packages/app/src/cli/services/import-extensions.ts b/packages/app/src/cli/services/import-extensions.ts index 9234404fdc7..2bbd726b6ab 100644 --- a/packages/app/src/cli/services/import-extensions.ts +++ b/packages/app/src/cli/services/import-extensions.ts @@ -8,7 +8,9 @@ import {allMigrationChoices, getMigrationChoices} from '../prompts/import-extens import {configurationFileNames, blocks} from '../constants.js' import {renderSelectPrompt, renderSuccess} from '@shopify/cli-kit/node/ui' import {basename, joinPath} from '@shopify/cli-kit/node/path' -import {removeFile, writeFile, fileExists, mkdir, touchFile} from '@shopify/cli-kit/node/fs' +import {removeFile, fileExists, mkdir, touchFile} from '@shopify/cli-kit/node/fs' +import {TomlFile} from '@shopify/cli-kit/node/toml/toml-file' +import {JsonMapType} from '@shopify/cli-kit/node/toml' import {outputContent} from '@shopify/cli-kit/node/output' import {slugify, hyphenate} from '@shopify/cli-kit/common/string' import {AbortError, AbortSilentError} from '@shopify/cli-kit/node/error' @@ -28,7 +30,7 @@ interface ImportOptions extends ImportAllOptions { ext: ExtensionRegistration, allExtensions: ExtensionRegistration[], appConfig: CurrentAppConfiguration, - ) => string + ) => object all?: boolean } @@ -109,9 +111,10 @@ export async function importExtensions(options: ImportOptions) { extensionUuids[handle] = ext.uuid if (action === DirectoryAction.Write) { - const tomlObject = buildTomlObject(ext, extensions, app.configuration) - const path = joinPath(directory, 'shopify.extension.toml') - await writeFile(path, tomlObject) + const tomlContent = buildTomlObject(ext, extensions, app.configuration) + const tomlPath = joinPath(directory, 'shopify.extension.toml') + const file = new TomlFile(tomlPath, tomlContent as JsonMapType) + await file.replace(tomlContent as JsonMapType) const lockFilePath = joinPath(directory, configurationFileNames.lockFile) await removeFile(lockFilePath) } diff --git a/packages/app/src/cli/services/marketing_activity/extension-to-toml.test.ts b/packages/app/src/cli/services/marketing_activity/extension-to-toml.test.ts index 136178b0ca7..b3e44d8db52 100644 --- a/packages/app/src/cli/services/marketing_activity/extension-to-toml.test.ts +++ b/packages/app/src/cli/services/marketing_activity/extension-to-toml.test.ts @@ -41,32 +41,36 @@ describe('extension-to-toml', () => { const got = buildTomlObject(extension) // Then - expect(got).toEqual(`[[extensions]] -type = "marketing_activity" -name = "mae @ test! 123" -handle = "mae-test-123" -title = "test mae" -description = "test mae description" -api_path = "/api/v1" -tactic = "ad" -marketing_channel = "social" -referring_domain = "facebook.com" -is_automation = false - - [[extensions.preview_data]] - label = "test label" - value = "test value" - - [[extensions.fields]] - ui_type = "text-single-line" - name = "test_field" - label = "test field" - help_text = "help text" - required = false - min_length = 1 - max_length = 50 - placeholder = "placeholder" -`) + expect(got).toEqual({ + extensions: [ + { + type: 'marketing_activity', + name: 'mae @ test! 123', + handle: 'mae-test-123', + title: 'test mae', + description: 'test mae description', + api_path: '/api/v1', + tactic: 'ad', + marketing_channel: 'social', + referring_domain: 'facebook.com', + is_automation: false, + use_external_editor: undefined, + preview_data: [{label: 'test label', value: 'test value'}], + fields: [ + { + ui_type: 'text-single-line', + name: 'test_field', + label: 'test field', + help_text: 'help text', + required: false, + min_length: 1, + max_length: 50, + placeholder: 'placeholder', + }, + ], + }, + ], + }) }) test('truncates the handle if the title has >50 characters', () => { @@ -82,10 +86,10 @@ is_automation = false } // When - const got = buildTomlObject(extension) + const got = buildTomlObject(extension) as {extensions: {handle: string}[]} // Then - expect(got).toContain('handle = "mae-test-12345555555554444447777778888888123455"') + expect(got.extensions[0]!.handle).toBe('mae-test-12345555555554444447777778888888123455') }) test('sets the channel and referring domain to empty string if no platform mapping is found', () => { @@ -101,10 +105,10 @@ is_automation = false } // When - const got = buildTomlObject(extension) + const got = buildTomlObject(extension) as {extensions: {marketing_channel: string; referring_domain: string}[]} // Then - expect(got).toContain('marketing_channel = ""') - expect(got).toContain('referring_domain = ""') + expect(got.extensions[0]!.marketing_channel).toBe('') + expect(got.extensions[0]!.referring_domain).toBe('') }) }) diff --git a/packages/app/src/cli/services/marketing_activity/extension-to-toml.ts b/packages/app/src/cli/services/marketing_activity/extension-to-toml.ts index 40287ddd4fe..268a87ba9cc 100644 --- a/packages/app/src/cli/services/marketing_activity/extension-to-toml.ts +++ b/packages/app/src/cli/services/marketing_activity/extension-to-toml.ts @@ -1,6 +1,5 @@ import {MAX_EXTENSION_HANDLE_LENGTH} from '../../models/extensions/schemas.js' import {ExtensionRegistration} from '../../api/graphql/all_app_extension_registrations.js' -import {encodeToml} from '@shopify/cli-kit/node/toml' import {slugify} from '@shopify/cli-kit/common/string' interface BaseField { @@ -163,7 +162,7 @@ function getUrlPath(url: string) { /** * Given a dashboard-built marketing activity extension config file, convert it to toml for the CLI extension */ -export function buildTomlObject(extension: ExtensionRegistration): string { +export function buildTomlObject(extension: ExtensionRegistration): object { const versionConfig = extension.activeVersion?.config ?? extension.draftVersion?.config if (!versionConfig) throw new Error('No config found for extension') const config: MarketingActivityDashboardConfig = JSON.parse(versionConfig) @@ -187,6 +186,5 @@ export function buildTomlObject(extension: ExtensionRegistration): string { }, ], } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return encodeToml(localExtensionRepresentation as any) + return localExtensionRepresentation } diff --git a/packages/app/src/cli/services/payments/extension-to-toml.test.ts b/packages/app/src/cli/services/payments/extension-to-toml.test.ts index 054fc25b8bd..d3bdd64a555 100644 --- a/packages/app/src/cli/services/payments/extension-to-toml.test.ts +++ b/packages/app/src/cli/services/payments/extension-to-toml.test.ts @@ -27,25 +27,34 @@ const translateDeployConfigKeyToCLI = (deployConfigKey: string): string => { return 'void_session_url' case 'default_buyer_label': return 'buyer_label' + case 'buyer_label_to_locale': + return 'buyer_label_translations' + case 'encryption_certificate': + return 'encryption_certificate_fingerprint' default: return deployConfigKey } } -const expectIncludesKeys = (got: string, config: string) => { +const expectObjectIncludesKeys = (got: object, config: string) => { const configObj = JSON.parse(config) + const gotObj = got as {extensions: {[key: string]: unknown}[]} + const ext = gotObj.extensions[0]! + const topLevel = got as {[key: string]: unknown} const keys = Object.keys(configObj) for (const key of keys) { if (configObj[key] === null) continue if (key === 'ui_extension_registration_uuid') continue if (Array.isArray(configObj[key]) && configObj[key].length === 0) continue const translatedKey = translateDeployConfigKeyToCLI(key) - expect(got).toContain(translatedKey) + const foundInExt = translatedKey in ext + const foundInTop = translatedKey in topLevel + expect(foundInExt || foundInTop).toBe(true) } } describe('extension-to-toml', () => { - test('correctly builds a toml string for a CLI payments extension', async () => { + test('correctly builds a toml object for a CLI payments extension', async () => { // Given const extension1: ExtensionRegistration = { id: '30366498817', @@ -62,42 +71,35 @@ describe('extension-to-toml', () => { const got = buildTomlObject(extension1, [extension1]) // Then - expectIncludesKeys(got, SAMPLE_OFFSITE_CONFIG) - expect(got).toEqual(`api_version = "2023-10" - -[[extensions]] -name = "Bogus Pay" -type = "payments_extension" -handle = "bogus-pay" -payment_session_url = "https://bogus-app/payment-sessions/start" -refund_session_url = "https://bogus-app/payment-sessions/refund" -capture_session_url = "https://bogus-app/payment-sessions/capture" -void_session_url = "https://bogus-app/payment-sessions/void" -confirmation_callback_url = "https://bogus-app/payment-sessions/confirm" -multiple_capture = false -merchant_label = "Offsite Payments App Extension" -supported_countries = [ "GG", "AF", "AZ", "BH" ] -supported_payment_methods = [ - "visa", - "master", - "american_express", - "discover", - "diners_club", - "jcb" -] -test_mode_available = true -supports_oversell_protection = false -supports_3ds = true -supports_deferred_payments = true -supports_installments = true - - [[extensions.supported_buyer_contexts]] - currency = "USD" - countries = [ "US" ] - - [[extensions.targeting]] - target = "payments.offsite.render" -`) + expectObjectIncludesKeys(got, SAMPLE_OFFSITE_CONFIG) + expect(got).toEqual({ + api_version: '2023-10', + extensions: [ + { + name: 'Bogus Pay', + type: 'payments_extension', + handle: 'bogus-pay', + payment_session_url: 'https://bogus-app/payment-sessions/start', + refund_session_url: 'https://bogus-app/payment-sessions/refund', + capture_session_url: 'https://bogus-app/payment-sessions/capture', + void_session_url: 'https://bogus-app/payment-sessions/void', + confirmation_callback_url: 'https://bogus-app/payment-sessions/confirm', + multiple_capture: false, + merchant_label: 'Offsite Payments App Extension', + supported_countries: ['GG', 'AF', 'AZ', 'BH'], + supported_payment_methods: ['visa', 'master', 'american_express', 'discover', 'diners_club', 'jcb'], + supported_buyer_contexts: [{currency: 'USD', countries: ['US']}], + test_mode_available: true, + buyer_label: null, + buyer_label_translations: null, + supports_oversell_protection: false, + supports_3ds: true, + supports_deferred_payments: true, + supports_installments: true, + targeting: [{target: 'payments.offsite.render'}], + }, + ], + }) }) test('truncates the handle if the title has >50 characters', () => { @@ -117,45 +119,38 @@ supports_installments = true const got = buildTomlObject(extension1, [extension1]) // Then - expectIncludesKeys(got, SAMPLE_OFFSITE_CONFIG) - expect(got).toEqual(`api_version = "2023-10" - -[[extensions]] -name = "Bogus Pay Bogus Pay Bogus Pay Bogus Pay Bogus Pay Bogus Pay Bogus" -type = "payments_extension" -handle = "bogus-pay-bogus-pay-bogus-pay-bogus-pay-bogus-pay" -payment_session_url = "https://bogus-app/payment-sessions/start" -refund_session_url = "https://bogus-app/payment-sessions/refund" -capture_session_url = "https://bogus-app/payment-sessions/capture" -void_session_url = "https://bogus-app/payment-sessions/void" -confirmation_callback_url = "https://bogus-app/payment-sessions/confirm" -multiple_capture = false -merchant_label = "Offsite Payments App Extension" -supported_countries = [ "GG", "AF", "AZ", "BH" ] -supported_payment_methods = [ - "visa", - "master", - "american_express", - "discover", - "diners_club", - "jcb" -] -test_mode_available = true -supports_oversell_protection = false -supports_3ds = true -supports_deferred_payments = true -supports_installments = true - - [[extensions.supported_buyer_contexts]] - currency = "USD" - countries = [ "US" ] - - [[extensions.targeting]] - target = "payments.offsite.render" -`) + expectObjectIncludesKeys(got, SAMPLE_OFFSITE_CONFIG) + expect(got).toEqual({ + api_version: '2023-10', + extensions: [ + { + name: 'Bogus Pay Bogus Pay Bogus Pay Bogus Pay Bogus Pay Bogus Pay Bogus', + type: 'payments_extension', + handle: 'bogus-pay-bogus-pay-bogus-pay-bogus-pay-bogus-pay', + payment_session_url: 'https://bogus-app/payment-sessions/start', + refund_session_url: 'https://bogus-app/payment-sessions/refund', + capture_session_url: 'https://bogus-app/payment-sessions/capture', + void_session_url: 'https://bogus-app/payment-sessions/void', + confirmation_callback_url: 'https://bogus-app/payment-sessions/confirm', + multiple_capture: false, + merchant_label: 'Offsite Payments App Extension', + supported_countries: ['GG', 'AF', 'AZ', 'BH'], + supported_payment_methods: ['visa', 'master', 'american_express', 'discover', 'diners_club', 'jcb'], + supported_buyer_contexts: [{currency: 'USD', countries: ['US']}], + test_mode_available: true, + buyer_label: null, + buyer_label_translations: null, + supports_oversell_protection: false, + supports_3ds: true, + supports_deferred_payments: true, + supports_installments: true, + targeting: [{target: 'payments.offsite.render'}], + }, + ], + }) }) - test('correctly builds a toml string for an offsite app', async () => { + test('correctly builds a toml object for an offsite app', async () => { // Given const extension1: ExtensionRegistration = { id: '30366498817', @@ -171,45 +166,38 @@ supports_installments = true const got = buildTomlObject(extension1, [extension1]) // Then - expectIncludesKeys(got, SAMPLE_OFFSITE_CONFIG) - expect(got).toEqual(`api_version = "2023-10" - -[[extensions]] -name = "Bogus Pay" -type = "payments_extension" -handle = "bogus-pay" -payment_session_url = "https://bogus-app/payment-sessions/start" -refund_session_url = "https://bogus-app/payment-sessions/refund" -capture_session_url = "https://bogus-app/payment-sessions/capture" -void_session_url = "https://bogus-app/payment-sessions/void" -confirmation_callback_url = "https://bogus-app/payment-sessions/confirm" -multiple_capture = false -merchant_label = "Offsite Payments App Extension" -supported_countries = [ "GG", "AF", "AZ", "BH" ] -supported_payment_methods = [ - "visa", - "master", - "american_express", - "discover", - "diners_club", - "jcb" -] -test_mode_available = true -supports_oversell_protection = false -supports_3ds = true -supports_deferred_payments = true -supports_installments = true - - [[extensions.supported_buyer_contexts]] - currency = "USD" - countries = [ "US" ] - - [[extensions.targeting]] - target = "payments.offsite.render" -`) + expectObjectIncludesKeys(got, SAMPLE_OFFSITE_CONFIG) + expect(got).toEqual({ + api_version: '2023-10', + extensions: [ + { + name: 'Bogus Pay', + type: 'payments_extension', + handle: 'bogus-pay', + payment_session_url: 'https://bogus-app/payment-sessions/start', + refund_session_url: 'https://bogus-app/payment-sessions/refund', + capture_session_url: 'https://bogus-app/payment-sessions/capture', + void_session_url: 'https://bogus-app/payment-sessions/void', + confirmation_callback_url: 'https://bogus-app/payment-sessions/confirm', + multiple_capture: false, + merchant_label: 'Offsite Payments App Extension', + supported_countries: ['GG', 'AF', 'AZ', 'BH'], + supported_payment_methods: ['visa', 'master', 'american_express', 'discover', 'diners_club', 'jcb'], + supported_buyer_contexts: [{currency: 'USD', countries: ['US']}], + test_mode_available: true, + buyer_label: null, + buyer_label_translations: null, + supports_oversell_protection: false, + supports_3ds: true, + supports_deferred_payments: true, + supports_installments: true, + targeting: [{target: 'payments.offsite.render'}], + }, + ], + }) }) - test('correctly builds a toml string for a credit card app', async () => { + test('correctly builds a toml object for a credit card app', async () => { // Given const extension1: ExtensionRegistration = { id: '30366498817', @@ -231,39 +219,40 @@ supports_installments = true const got = buildTomlObject(extension1, [extension1, extension2]) // Then - expectIncludesKeys(got, SAMPLE_CREDIT_CARD_CONFIG) - expect(got).toEqual(`api_version = "2023-04" - -[[extensions]] -name = "Bogus Pay" -type = "payments_extension" -handle = "bogus-pay" -payment_session_url = "https://test-domain.com/authorize" -refund_session_url = "https://test-domain.com/refund" -capture_session_url = "https://test-domain.com/capture" -void_session_url = "https://test-domain.com/void" -confirmation_callback_url = "https://test-domain.com/confirm" -multiple_capture = false -merchant_label = "test-label" -supported_countries = [ "JP" ] -supported_payment_methods = [ "master", "visa", "jcb", "american_express", "diners_club" ] -test_mode_available = true -supports_3ds = true -supports_deferred_payments = false -supports_installments = false -encryption_certificate_fingerprint = "fingerprint" -ui_extension_handle = "checkout-ui-extension" - - [[extensions.supported_buyer_contexts]] - currency = "USD" - countries = [ "US", "CA" ] - - [[extensions.targeting]] - target = "payments.credit-card.render" -`) + expectObjectIncludesKeys(got, SAMPLE_CREDIT_CARD_CONFIG) + expect(got).toEqual({ + api_version: '2023-04', + extensions: [ + { + name: 'Bogus Pay', + type: 'payments_extension', + handle: 'bogus-pay', + payment_session_url: 'https://test-domain.com/authorize', + refund_session_url: 'https://test-domain.com/refund', + capture_session_url: 'https://test-domain.com/capture', + void_session_url: 'https://test-domain.com/void', + confirmation_callback_url: 'https://test-domain.com/confirm', + multiple_capture: false, + merchant_label: 'test-label', + supported_countries: ['JP'], + supported_payment_methods: ['master', 'visa', 'jcb', 'american_express', 'diners_club'], + supported_buyer_contexts: [{currency: 'USD', countries: ['US', 'CA']}], + test_mode_available: true, + supports_3ds: true, + supports_moto: undefined, + supports_deferred_payments: false, + supports_installments: false, + verification_session_url: undefined, + encryption_certificate_fingerprint: 'fingerprint', + checkout_payment_method_fields: undefined, + ui_extension_handle: 'checkout-ui-extension', + targeting: [{target: 'payments.credit-card.render'}], + }, + ], + }) }) - test('correctly builds a toml string for a custom credit card app', async () => { + test('correctly builds a toml object for a custom credit card app', async () => { // Given const extension1: ExtensionRegistration = { id: '30366498817', @@ -285,42 +274,39 @@ ui_extension_handle = "checkout-ui-extension" const got = buildTomlObject(extension1, [extension1, extension2]) // Then - expectIncludesKeys(got, SAMPLE_CUSTOM_CREDIT_CARD_CONFIG) - expect(got).toEqual(`api_version = "unstable" - -[[extensions]] -name = "Bogus Pay" -type = "payments_extension" -handle = "bogus-pay" -payment_session_url = "https://bogus-payment-sessions.shopifycloud.com/bogus/custom_card/payment_sessions" -refund_session_url = "https://bogus-payment-sessions.shopifycloud.com/bogus/custom_card/refund_sessions" -capture_session_url = "https://bogus-payment-sessions.shopifycloud.com/bogus/custom_card/capture_sessions" -void_session_url = "https://bogus-payment-sessions.shopifycloud.com/bogus/custom_card/void_sessions" -confirmation_callback_url = "https://bogus-payment-sessions.shopifycloud.com/bogus/custom_card/confirm" -merchant_label = "Bogus Private Card App" -supports_3ds = false -supported_countries = [ "CA", "MX", "US" ] -supported_payment_methods = [ "visa" ] -encryption_certificate_fingerprint = "Test Certificate" -test_mode_available = true -multiple_capture = false -checkout_hosted_fields = [ "name", "expiry", "verification_value" ] -ui_extension_handle = "checkout-ui-extension" - - [[extensions.supported_buyer_contexts]] - currency = "EUR" - - [[extensions.checkout_payment_method_fields]] - key = "payment_plan" - type = "string" - required = true - - [[extensions.targeting]] - target = "payments.custom-credit-card.render" -`) + expectObjectIncludesKeys(got, SAMPLE_CUSTOM_CREDIT_CARD_CONFIG) + expect(got).toEqual({ + api_version: 'unstable', + extensions: [ + { + name: 'Bogus Pay', + type: 'payments_extension', + handle: 'bogus-pay', + payment_session_url: 'https://bogus-payment-sessions.shopifycloud.com/bogus/custom_card/payment_sessions', + refund_session_url: 'https://bogus-payment-sessions.shopifycloud.com/bogus/custom_card/refund_sessions', + capture_session_url: 'https://bogus-payment-sessions.shopifycloud.com/bogus/custom_card/capture_sessions', + void_session_url: 'https://bogus-payment-sessions.shopifycloud.com/bogus/custom_card/void_sessions', + confirmation_callback_url: 'https://bogus-payment-sessions.shopifycloud.com/bogus/custom_card/confirm', + merchant_label: 'Bogus Private Card App', + supports_3ds: false, + supported_countries: ['CA', 'MX', 'US'], + supported_payment_methods: ['visa'], + supported_buyer_contexts: [{currency: 'EUR'}], + buyer_label: null, + buyer_label_translations: null, + encryption_certificate_fingerprint: 'Test Certificate', + test_mode_available: true, + multiple_capture: false, + checkout_payment_method_fields: [{key: 'payment_plan', type: 'string', required: true}], + checkout_hosted_fields: ['name', 'expiry', 'verification_value'], + ui_extension_handle: 'checkout-ui-extension', + targeting: [{target: 'payments.custom-credit-card.render'}], + }, + ], + }) }) - test('correctly builds a toml string for a custom onsite app', async () => { + test('correctly builds a toml object for a custom onsite app', async () => { // Given const extension1: ExtensionRegistration = { id: '30366498817', @@ -342,41 +328,43 @@ ui_extension_handle = "checkout-ui-extension" const got = buildTomlObject(extension1, [extension1, extension2]) // Then - expectIncludesKeys(got, SAMPLE_CUSTOM_ONSITE_CONFIG) - expect(got).toEqual(`api_version = "unstable" - -[[extensions]] -name = "Bogus Pay" -type = "payments_extension" -handle = "bogus-pay" -payment_session_url = "https://test-domain.com/startsession/bogus-pay" -refund_session_url = "https://test-domain.com/refund" -capture_session_url = "https://test-domain.com/capture" -void_session_url = "https://test-domain.com/void" -merchant_label = "Test Label" -supports_oversell_protection = false -supports_3ds = false -supports_installments = false -supports_deferred_payments = false -supported_countries = [ "BR" ] -supported_payment_methods = [ "bogus-pay" ] -test_mode_available = true -multiple_capture = false -buyer_label = "Bogus Pay Buyer Label" -buyer_label_translations = [ ] -ui_extension_handle = "checkout-ui-extension" - - [[extensions.checkout_payment_method_fields]] - key = "bogus_customer_document" - type = "string" - required = true - - [[extensions.targeting]] - target = "payments.custom-onsite.render" -`) + expectObjectIncludesKeys(got, SAMPLE_CUSTOM_ONSITE_CONFIG) + expect(got).toEqual({ + api_version: 'unstable', + extensions: [ + { + name: 'Bogus Pay', + type: 'payments_extension', + handle: 'bogus-pay', + payment_session_url: 'https://test-domain.com/startsession/bogus-pay', + refund_session_url: 'https://test-domain.com/refund', + capture_session_url: 'https://test-domain.com/capture', + void_session_url: 'https://test-domain.com/void', + confirmation_callback_url: null, + update_payment_session_url: undefined, + start_verification_session_url: undefined, + merchant_label: 'Test Label', + supports_oversell_protection: false, + supports_3ds: false, + supports_installments: false, + supports_deferred_payments: false, + supported_countries: ['BR'], + supported_payment_methods: ['bogus-pay'], + supported_buyer_contexts: undefined, + test_mode_available: true, + multiple_capture: false, + buyer_label: 'Bogus Pay Buyer Label', + buyer_label_translations: [], + checkout_payment_method_fields: [{key: 'bogus_customer_document', type: 'string', required: true}], + modal_payment_method_fields: undefined, + ui_extension_handle: 'checkout-ui-extension', + targeting: [{target: 'payments.custom-onsite.render'}], + }, + ], + }) }) - test('correctly builds a toml string for a redeemable app', async () => { + test('correctly builds a toml object for a redeemable app', async () => { // Given const extension1: ExtensionRegistration = { id: '30366498817', @@ -398,47 +386,38 @@ ui_extension_handle = "checkout-ui-extension" const got = buildTomlObject(extension1, [extension1, extension2]) // Then - expectIncludesKeys(got, SAMPLE_REDEEMABLE_CONFIG) - expect(got).toEqual(`api_version = "unstable" - -[[extensions]] -name = "Bogus Pay" -type = "payments_extension" -handle = "bogus-pay" -payment_session_url = "https://bogus-payment-sessions.shopifycloud.com/bogus/redeemable/payment_sessions" -refund_session_url = "https://bogus-payment-sessions.shopifycloud.com/bogus/redeemable/refund_sessions" -capture_session_url = "https://bogus-payment-sessions.shopifycloud.com/bogus/redeemable/capture_sessions" -void_session_url = "https://bogus-payment-sessions.shopifycloud.com/bogus/redeemable/void_sessions" -merchant_label = "Bogus Redeemable Payments App" -supported_countries = [ "CA", "MX", "US" ] -supported_payment_methods = [ "gift-card" ] -test_mode_available = true -balance_url = "https://bogus-payment-sessions.shopifycloud.com/bogus/redeemable/retrieve_balance" -ui_extension_handle = "checkout-ui-extension" - - [[extensions.supported_buyer_contexts]] - currency = "USD" - countries = [ "US" ] - - [[extensions.supported_buyer_contexts]] - currency = "GBP" - - [[extensions.checkout_payment_method_fields]] - key = "card_number" - type = "string" - required = true - - [[extensions.checkout_payment_method_fields]] - key = "pin" - type = "string" - required = true - - [[extensions.targeting]] - target = "payments.redeemable.render" -`) + expectObjectIncludesKeys(got, SAMPLE_REDEEMABLE_CONFIG) + expect(got).toEqual({ + api_version: 'unstable', + extensions: [ + { + name: 'Bogus Pay', + type: 'payments_extension', + handle: 'bogus-pay', + payment_session_url: 'https://bogus-payment-sessions.shopifycloud.com/bogus/redeemable/payment_sessions', + refund_session_url: 'https://bogus-payment-sessions.shopifycloud.com/bogus/redeemable/refund_sessions', + capture_session_url: 'https://bogus-payment-sessions.shopifycloud.com/bogus/redeemable/capture_sessions', + void_session_url: 'https://bogus-payment-sessions.shopifycloud.com/bogus/redeemable/void_sessions', + merchant_label: 'Bogus Redeemable Payments App', + supported_countries: ['CA', 'MX', 'US'], + supported_payment_methods: ['gift-card'], + supported_buyer_contexts: [{currency: 'USD', countries: ['US']}, {currency: 'GBP'}], + test_mode_available: true, + buyer_label: null, + buyer_label_translations: null, + balance_url: 'https://bogus-payment-sessions.shopifycloud.com/bogus/redeemable/retrieve_balance', + checkout_payment_method_fields: [ + {key: 'card_number', type: 'string', required: true}, + {key: 'pin', type: 'string', required: true}, + ], + ui_extension_handle: 'checkout-ui-extension', + targeting: [{target: 'payments.redeemable.render'}], + }, + ], + }) }) - test('correctly builds a toml string for a card present app', async () => { + test('correctly builds a toml object for a card present app', async () => { // Given const extension1: ExtensionRegistration = { id: '30366498817', @@ -454,25 +433,27 @@ ui_extension_handle = "checkout-ui-extension" const got = buildTomlObject(extension1, [extension1]) // Then - expectIncludesKeys(got, SAMPLE_CARD_PRESENT_CONFIG) - expect(got).toEqual(`api_version = "2025-04" - -[[extensions]] -name = "Bogus Pay" -type = "payments_extension" -handle = "bogus-pay" -payment_session_url = "https://bogus-payment-sessions.shopifycloud.com/bogus/card_present/payment_sessions" -refund_session_url = "https://bogus-payment-sessions.shopifycloud.com/bogus/card_present/refund_sessions" -capture_session_url = "https://bogus-payment-sessions.shopifycloud.com/bogus/card_present/capture_sessions" -void_session_url = "https://bogus-payment-sessions.shopifycloud.com/bogus/card_present/void_sessions" -sync_terminal_transaction_result_url = "https://bogus-payment-sessions.shopifycloud.com/bogus/card_present/terminal_transaction_result" -merchant_label = "Card Present Payments App Extension" -supported_countries = [ "AU", "CA", "GB", "US" ] -supported_payment_methods = [ "visa", "master", "american_express" ] -test_mode_available = true - - [[extensions.targeting]] - target = "payments.card-present.render" -`) + expectObjectIncludesKeys(got, SAMPLE_CARD_PRESENT_CONFIG) + expect(got).toEqual({ + api_version: '2025-04', + extensions: [ + { + name: 'Bogus Pay', + type: 'payments_extension', + handle: 'bogus-pay', + payment_session_url: 'https://bogus-payment-sessions.shopifycloud.com/bogus/card_present/payment_sessions', + refund_session_url: 'https://bogus-payment-sessions.shopifycloud.com/bogus/card_present/refund_sessions', + capture_session_url: 'https://bogus-payment-sessions.shopifycloud.com/bogus/card_present/capture_sessions', + void_session_url: 'https://bogus-payment-sessions.shopifycloud.com/bogus/card_present/void_sessions', + sync_terminal_transaction_result_url: + 'https://bogus-payment-sessions.shopifycloud.com/bogus/card_present/terminal_transaction_result', + merchant_label: 'Card Present Payments App Extension', + supported_countries: ['AU', 'CA', 'GB', 'US'], + supported_payment_methods: ['visa', 'master', 'american_express'], + test_mode_available: true, + targeting: [{target: 'payments.card-present.render'}], + }, + ], + }) }) }) diff --git a/packages/app/src/cli/services/payments/extension-to-toml.ts b/packages/app/src/cli/services/payments/extension-to-toml.ts index ed5e8208a35..522d8de2630 100644 --- a/packages/app/src/cli/services/payments/extension-to-toml.ts +++ b/packages/app/src/cli/services/payments/extension-to-toml.ts @@ -31,7 +31,6 @@ import { CARD_PRESENT_TARGET, } from '../../models/extensions/specifications/payments_app_extension_schemas/card_present_payments_app_extension_schema.js' import {MAX_EXTENSION_HANDLE_LENGTH} from '../../models/extensions/schemas.js' -import {encodeToml} from '@shopify/cli-kit/node/toml' import {slugify} from '@shopify/cli-kit/common/string' function typeToContext(type: string) { @@ -59,7 +58,7 @@ export enum DashboardPaymentExtensionType { CardPresent = 'payments_app_card_present', } -export function buildTomlObject(extension: ExtensionRegistration, allExtensions: ExtensionRegistration[]): string { +export function buildTomlObject(extension: ExtensionRegistration, allExtensions: ExtensionRegistration[]): object { const context = extension.activeVersion?.context || extension.draftVersion?.context || typeToContext(extension.type) switch (context) { case OFFSITE_TARGET: @@ -136,6 +135,5 @@ function buildPaymentsToml( }, ], } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return encodeToml(localExtensionRepresentation as any) + return localExtensionRepresentation } diff --git a/packages/app/src/cli/services/subscription_link/extension-to-toml.test.ts b/packages/app/src/cli/services/subscription_link/extension-to-toml.test.ts index c2e1a5c3e4f..9a7fae4c5e7 100644 --- a/packages/app/src/cli/services/subscription_link/extension-to-toml.test.ts +++ b/packages/app/src/cli/services/subscription_link/extension-to-toml.test.ts @@ -22,12 +22,16 @@ describe('extension-to-toml', () => { const got = buildTomlObject(extension) // Then - expect(got).toEqual(`[[extensions]] -type = "subscription_link_extension" -name = "custom subscription link" -handle = "custom-subscription-link" -pattern = "/subscriptions{?customer_id,shop}&id={contract_id}" -`) + expect(got).toEqual({ + extensions: [ + { + type: 'subscription_link_extension', + name: 'custom subscription link', + handle: 'custom-subscription-link', + pattern: '/subscriptions{?customer_id,shop}&id={contract_id}', + }, + ], + }) }) test('truncates the handle if the title has >50 characters', () => { @@ -46,6 +50,8 @@ pattern = "/subscriptions{?customer_id,shop}&id={contract_id}" const got = buildTomlObject(extension) // Then - expect(got).toContain('handle = "subscription-link-test-123455555555544444477777"') + expect((got as {extensions: [{handle: string}]}).extensions[0].handle).toBe( + 'subscription-link-test-123455555555544444477777', + ) }) }) diff --git a/packages/app/src/cli/services/subscription_link/extension-to-toml.ts b/packages/app/src/cli/services/subscription_link/extension-to-toml.ts index 43b732400ba..9ba6df67bc7 100644 --- a/packages/app/src/cli/services/subscription_link/extension-to-toml.ts +++ b/packages/app/src/cli/services/subscription_link/extension-to-toml.ts @@ -1,6 +1,5 @@ import {MAX_EXTENSION_HANDLE_LENGTH} from '../../models/extensions/schemas.js' import {ExtensionRegistration} from '../../api/graphql/all_app_extension_registrations.js' -import {encodeToml} from '@shopify/cli-kit/node/toml' import {slugify} from '@shopify/cli-kit/common/string' export interface SubscriptionLinkDashboardConfig { @@ -10,7 +9,7 @@ export interface SubscriptionLinkDashboardConfig { /** * Given a dashboard-built subscription link extension config file, convert it to toml for the CLI extension */ -export function buildTomlObject(extension: ExtensionRegistration): string { +export function buildTomlObject(extension: ExtensionRegistration): object { const versionConfig = extension.activeVersion?.config ?? extension.draftVersion?.config if (!versionConfig) throw new Error('No config found for extension') const config: SubscriptionLinkDashboardConfig = JSON.parse(versionConfig) @@ -25,6 +24,5 @@ export function buildTomlObject(extension: ExtensionRegistration): string { }, ], } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return encodeToml(localExtensionRepresentation as any) + return localExtensionRepresentation } diff --git a/packages/app/src/cli/utilities/app/config/getTomls.ts b/packages/app/src/cli/utilities/app/config/getTomls.ts index 8d39a0ef3b6..f6662db42fa 100644 --- a/packages/app/src/cli/utilities/app/config/getTomls.ts +++ b/packages/app/src/cli/utilities/app/config/getTomls.ts @@ -1,10 +1,7 @@ -import { - AppConfigurationFileName, - isValidFormatAppConfigurationFileName, - loadConfigurationFileContent, -} from '../../../models/app/loader.js' +import {AppConfigurationFileName, isValidFormatAppConfigurationFileName} from '../../../models/app/loader.js' import {isDirectory} from '@shopify/cli-kit/node/fs' import {joinPath} from '@shopify/cli-kit/node/path' +import {TomlFile} from '@shopify/cli-kit/node/toml/toml-file' import {readdirSync} from 'fs' export async function getTomls(appDirectory?: string): Promise<{[clientId: string]: AppConfigurationFileName}> { @@ -19,8 +16,9 @@ export async function getTomls(appDirectory?: string): Promise<{[clientId: strin files.map(async (file) => { if (isValidFormatAppConfigurationFileName(file)) { const filePath = joinPath(appDirectory, file) + const tomlFile = await TomlFile.read(filePath) // eslint-disable-next-line @typescript-eslint/no-explicit-any - const parsedToml = (await loadConfigurationFileContent(filePath)) as {[key: string]: any} + const parsedToml = tomlFile.content as {[key: string]: any} if (parsedToml.client_id) { clientIds[parsedToml.client_id] = file diff --git a/packages/cli-kit/package.json b/packages/cli-kit/package.json index edb89f4f820..61056a93ad6 100644 --- a/packages/cli-kit/package.json +++ b/packages/cli-kit/package.json @@ -24,6 +24,10 @@ "import": "./dist/index.js", "types": "./dist/index.d.ts" }, + "./node/toml": { + "node": "./dist/public/node/toml/index.js", + "types": "./dist/public/node/toml/index.d.ts" + }, "./*": { "node": "./dist/public/*.js", "types": "./dist/public/*.d.ts" @@ -104,6 +108,7 @@ "@graphql-typed-document-node/core": "3.2.0", "@iarna/toml": "2.2.5", "@oclif/core": "4.5.3", + "@shopify/toml-patch": "0.3.0", "@opentelemetry/api": "1.9.0", "@opentelemetry/core": "1.30.0", "@opentelemetry/exporter-metrics-otlp-http": "0.57.0", diff --git a/packages/cli-kit/src/public/node/base-command.test.ts b/packages/cli-kit/src/public/node/base-command.test.ts index 63c0a0fd19a..30dfe245fed 100644 --- a/packages/cli-kit/src/public/node/base-command.test.ts +++ b/packages/cli-kit/src/public/node/base-command.test.ts @@ -1,6 +1,6 @@ import Command from './base-command.js' import {Environments} from './environments.js' -import {encodeToml as encodeTOML} from './toml.js' +import {encodeToml as encodeTOML} from './toml/codec.js' import {globalFlags} from './cli.js' import {inTemporaryDirectory, mkdir, writeFile} from './fs.js' import {joinPath, resolvePath, cwd} from './path.js' diff --git a/packages/cli-kit/src/public/node/environments.test.ts b/packages/cli-kit/src/public/node/environments.test.ts index 888ae04d58a..e646a8a80ca 100644 --- a/packages/cli-kit/src/public/node/environments.test.ts +++ b/packages/cli-kit/src/public/node/environments.test.ts @@ -1,5 +1,5 @@ import * as environments from './environments.js' -import {encodeToml as tomlEncode} from './toml.js' +import {encodeToml as tomlEncode} from './toml/codec.js' import {inTemporaryDirectory, writeFile} from './fs.js' import {joinPath} from './path.js' import {mockAndCaptureOutput} from './testing/output.js' diff --git a/packages/cli-kit/src/public/node/environments.ts b/packages/cli-kit/src/public/node/environments.ts index cdbf3c8075d..72d1d69be4b 100644 --- a/packages/cli-kit/src/public/node/environments.ts +++ b/packages/cli-kit/src/public/node/environments.ts @@ -1,5 +1,5 @@ -import {decodeToml} from './toml.js' -import {findPathUp, readFile} from './fs.js' +import {TomlFile} from './toml/toml-file.js' +import {findPathUp} from './fs.js' import {cwd} from './path.js' import * as metadata from './metadata.js' import {renderWarning} from './ui.js' @@ -38,7 +38,8 @@ export async function loadEnvironment( renderWarningIfNeeded({body: 'Environment file not found.'}, options?.silent) return undefined } - const environmentsJson = decodeToml(await readFile(filePath)) as Environments + const file = await TomlFile.read(filePath) + const environmentsJson = file.content as Environments const environments = environmentsJson.environments if (!environments) { renderWarningIfNeeded( diff --git a/packages/cli-kit/src/public/node/json-schema.test.ts b/packages/cli-kit/src/public/node/json-schema.test.ts index a7202625675..46583d30050 100644 --- a/packages/cli-kit/src/public/node/json-schema.test.ts +++ b/packages/cli-kit/src/public/node/json-schema.test.ts @@ -1,5 +1,5 @@ import {jsonSchemaValidate, normaliseJsonSchema} from './json-schema.js' -import {decodeToml} from './toml.js' +import {decodeToml} from './toml/codec.js' import {zod} from './schema.js' import {describe, expect, test} from 'vitest' diff --git a/packages/cli-kit/src/public/node/toml.test.ts b/packages/cli-kit/src/public/node/toml/codec.test.ts similarity index 95% rename from packages/cli-kit/src/public/node/toml.test.ts rename to packages/cli-kit/src/public/node/toml/codec.test.ts index 545fd28fe06..bbadfd7b30b 100644 --- a/packages/cli-kit/src/public/node/toml.test.ts +++ b/packages/cli-kit/src/public/node/toml/codec.test.ts @@ -1,4 +1,4 @@ -import {decodeToml} from './toml.js' +import {decodeToml} from './codec.js' import {describe, expect, test} from 'vitest' describe('decodeToml', () => { diff --git a/packages/cli-kit/src/public/node/toml.ts b/packages/cli-kit/src/public/node/toml/codec.ts similarity index 92% rename from packages/cli-kit/src/public/node/toml.ts rename to packages/cli-kit/src/public/node/toml/codec.ts index ecc00e9204f..5fd5460807e 100644 --- a/packages/cli-kit/src/public/node/toml.ts +++ b/packages/cli-kit/src/public/node/toml/codec.ts @@ -1,4 +1,4 @@ -import {JsonMap} from '../../private/common/json.js' +import {JsonMap} from '../../../private/common/json.js' import * as toml from '@iarna/toml' export type JsonMapType = JsonMap diff --git a/packages/cli-kit/src/public/node/toml/index.ts b/packages/cli-kit/src/public/node/toml/index.ts new file mode 100644 index 00000000000..997348f9a4d --- /dev/null +++ b/packages/cli-kit/src/public/node/toml/index.ts @@ -0,0 +1 @@ +export type {JsonMapType} from './codec.js' diff --git a/packages/cli-kit/src/public/node/toml/toml-file.test.ts b/packages/cli-kit/src/public/node/toml/toml-file.test.ts new file mode 100644 index 00000000000..1df1f796913 --- /dev/null +++ b/packages/cli-kit/src/public/node/toml/toml-file.test.ts @@ -0,0 +1,284 @@ +import {TomlFile, TomlParseError} from './toml-file.js' +import {writeFile, readFile, inTemporaryDirectory} from '../fs.js' +import {joinPath} from '../path.js' +import {describe, expect, test} from 'vitest' + +describe('TomlFile', () => { + describe('read', () => { + test('reads and parses a TOML file', async () => { + await inTemporaryDirectory(async (dir) => { + const path = joinPath(dir, 'test.toml') + await writeFile(path, 'name = "my-app"\nclient_id = "123"\n') + + const file = await TomlFile.read(path) + + expect(file.path).toBe(path) + expect(file.content).toStrictEqual({name: 'my-app', client_id: '123'}) + }) + }) + + test('reads nested tables', async () => { + await inTemporaryDirectory(async (dir) => { + const path = joinPath(dir, 'test.toml') + await writeFile(path, '[build]\ndev_store_url = "my-store.myshopify.com"\n') + + const file = await TomlFile.read(path) + + expect(file.content).toStrictEqual({build: {dev_store_url: 'my-store.myshopify.com'}}) + }) + }) + + test('throws TomlParseError with file path on invalid TOML', async () => { + await inTemporaryDirectory(async (dir) => { + const path = joinPath(dir, 'bad.toml') + await writeFile(path, 'name = [invalid') + + await expect(TomlFile.read(path)).rejects.toThrow(TomlParseError) + await expect(TomlFile.read(path)).rejects.toThrow(/bad\.toml/) + }) + }) + + test('throws if file does not exist', async () => { + await expect(TomlFile.read('/nonexistent/path/test.toml')).rejects.toThrow() + }) + }) + + describe('patch', () => { + test('sets a top-level value', async () => { + await inTemporaryDirectory(async (dir) => { + const path = joinPath(dir, 'test.toml') + await writeFile(path, 'name = "old"\n') + + const file = await TomlFile.read(path) + await file.patch({name: 'new'}) + + expect(file.content.name).toBe('new') + const raw = await readFile(path) + expect(raw).toContain('name = "new"') + }) + }) + + test('sets a nested value', async () => { + await inTemporaryDirectory(async (dir) => { + const path = joinPath(dir, 'test.toml') + await writeFile(path, '[build]\ndev_store_url = "old.myshopify.com"\n') + + const file = await TomlFile.read(path) + await file.patch({build: {dev_store_url: 'new.myshopify.com'}}) + + expect(file.content).toStrictEqual({build: {dev_store_url: 'new.myshopify.com'}}) + }) + }) + + test('creates intermediate tables', async () => { + await inTemporaryDirectory(async (dir) => { + const path = joinPath(dir, 'test.toml') + await writeFile(path, 'name = "app"\n') + + const file = await TomlFile.read(path) + await file.patch({build: {dev_store_url: 'store.myshopify.com'}}) + + expect(file.content).toStrictEqual({ + name: 'app', + build: {dev_store_url: 'store.myshopify.com'}, + }) + }) + }) + + test('sets multiple values at once', async () => { + await inTemporaryDirectory(async (dir) => { + const path = joinPath(dir, 'test.toml') + await writeFile(path, 'name = "app"\nclient_id = "123"\n') + + const file = await TomlFile.read(path) + await file.patch({name: 'updated', client_id: '456'}) + + expect(file.content.name).toBe('updated') + expect(file.content.client_id).toBe('456') + }) + }) + + test('preserves comments', async () => { + await inTemporaryDirectory(async (dir) => { + const path = joinPath(dir, 'test.toml') + await writeFile(path, '# This is a comment\nname = "app"\n') + + const file = await TomlFile.read(path) + await file.patch({name: 'updated'}) + + const raw = await readFile(path) + expect(raw).toContain('# This is a comment') + expect(raw).toContain('name = "updated"') + }) + }) + + test('handles array values', async () => { + await inTemporaryDirectory(async (dir) => { + const path = joinPath(dir, 'test.toml') + await writeFile(path, '[auth]\nredirect_urls = ["https://old.com"]\n') + + const file = await TomlFile.read(path) + await file.patch({auth: {redirect_urls: ['https://new.com', 'https://other.com']}}) + + const content = file.content as {auth: {redirect_urls: string[]}} + expect(content.auth.redirect_urls).toStrictEqual(['https://new.com', 'https://other.com']) + }) + }) + }) + + describe('remove', () => { + test('removes a top-level key', async () => { + await inTemporaryDirectory(async (dir) => { + const path = joinPath(dir, 'test.toml') + await writeFile(path, 'name = "app"\nclient_id = "123"\n') + + const file = await TomlFile.read(path) + await file.remove('name') + + expect(file.content.name).toBeUndefined() + expect(file.content.client_id).toBe('123') + }) + }) + + test('removes a nested key', async () => { + await inTemporaryDirectory(async (dir) => { + const path = joinPath(dir, 'test.toml') + await writeFile( + path, + '[build]\ndev_store_url = "store.myshopify.com"\nautomatically_update_urls_on_dev = true\n', + ) + + const file = await TomlFile.read(path) + await file.remove('build.dev_store_url') + + const build = file.content.build as {[key: string]: unknown} + expect(build.dev_store_url).toBeUndefined() + expect(build.automatically_update_urls_on_dev).toBe(true) + }) + }) + + test('preserves unrelated content', async () => { + await inTemporaryDirectory(async (dir) => { + const path = joinPath(dir, 'test.toml') + await writeFile(path, 'name = "app"\nclient_id = "123"\n') + + const file = await TomlFile.read(path) + await file.remove('name') + + const raw = await readFile(path) + expect(raw).toContain('client_id = "123"') + expect(raw).not.toContain('name') + }) + }) + }) + + describe('replace', () => { + test('replaces the entire file content', async () => { + await inTemporaryDirectory(async (dir) => { + const path = joinPath(dir, 'test.toml') + await writeFile(path, 'name = "old"\n') + + const file = await TomlFile.read(path) + await file.replace({name: 'new', client_id: '789'}) + + expect(file.content).toStrictEqual({name: 'new', client_id: '789'}) + const raw = await readFile(path) + expect(raw).toContain('name = "new"') + expect(raw).toContain('client_id = "789"') + }) + }) + + test('does not preserve comments', async () => { + await inTemporaryDirectory(async (dir) => { + const path = joinPath(dir, 'test.toml') + await writeFile(path, '# Comment\nname = "old"\n') + + const file = await TomlFile.read(path) + await file.replace({name: 'new'}) + + const raw = await readFile(path) + expect(raw).not.toContain('# Comment') + }) + }) + + test('round-trips read → replace → read', async () => { + await inTemporaryDirectory(async (dir) => { + const path = joinPath(dir, 'test.toml') + const original = { + name: 'my-app', + client_id: 'abc123', + build: {dev_store_url: 'store.myshopify.com'}, + } + await writeFile(path, 'name = "placeholder"\n') + + const file = await TomlFile.read(path) + await file.replace(original) + + const reread = await TomlFile.read(path) + expect(reread.content).toStrictEqual(original) + }) + }) + }) + + describe('transformRaw', () => { + test('transforms the raw TOML string and updates content', async () => { + await inTemporaryDirectory(async (dir) => { + const path = joinPath(dir, 'test.toml') + await writeFile(path, 'name = "app"\n') + + const file = await TomlFile.read(path) + await file.transformRaw((raw) => `# Header comment\n${raw}`) + + const raw = await readFile(path) + expect(raw).toContain('# Header comment') + expect(raw).toContain('name = "app"') + expect(file.content.name).toBe('app') + }) + }) + + test('injected comments survive subsequent patch calls', async () => { + await inTemporaryDirectory(async (dir) => { + const path = joinPath(dir, 'test.toml') + await writeFile(path, 'name = "app"\nclient_id = "123"\n') + + const file = await TomlFile.read(path) + await file.transformRaw((raw) => `# Keep this comment\n${raw}`) + await file.patch({name: 'updated'}) + + const raw = await readFile(path) + expect(raw).toContain('# Keep this comment') + expect(raw).toContain('name = "updated"') + }) + }) + + test('works after replace to add comments', async () => { + await inTemporaryDirectory(async (dir) => { + const path = joinPath(dir, 'test.toml') + await writeFile(path, '') + + const file = new TomlFile(path, {}) + await file.replace({name: 'app', client_id: '123'}) + await file.transformRaw((raw) => `# Doc link\n${raw}`) + + const raw = await readFile(path) + expect(raw).toContain('# Doc link') + expect(raw).toContain('name = "app"') + expect(file.content).toStrictEqual({name: 'app', client_id: '123'}) + }) + }) + }) + + describe('constructor', () => { + test('creates a TomlFile instance for new files', async () => { + await inTemporaryDirectory(async (dir) => { + const path = joinPath(dir, 'new.toml') + const file = new TomlFile(path, {}) + await file.replace({type: 'ui_extension', name: 'My Extension'}) + + const raw = await readFile(path) + expect(raw).toContain('type = "ui_extension"') + expect(raw).toContain('name = "My Extension"') + }) + }) + }) +}) diff --git a/packages/cli-kit/src/public/node/toml/toml-file.ts b/packages/cli-kit/src/public/node/toml/toml-file.ts new file mode 100644 index 00000000000..fb1ffffc4c1 --- /dev/null +++ b/packages/cli-kit/src/public/node/toml/toml-file.ts @@ -0,0 +1,157 @@ +import {JsonMapType, decodeToml, encodeToml} from './codec.js' +import {readFile, writeFile} from '../fs.js' +import {updateTomlValues} from '@shopify/toml-patch' + +type TomlPatchValue = string | number | boolean | undefined | (string | number | boolean)[] + +/** + * Thrown when a TOML file cannot be parsed. Includes the file path for context. + */ +export class TomlParseError extends Error { + readonly path: string + + constructor(path: string, cause: Error) { + super(`Fix the following error in ${path}:\n${cause.message}`) + this.name = 'TomlParseError' + this.path = path + } +} + +/** + * General-purpose TOML file abstraction. + * + * Provides a unified interface for reading, patching, removing keys from, and replacing + * the content of TOML files on disk. + * + * - `read` populates content from disk + * - `patch` does surgical WASM-based edits (preserves comments and formatting) + * - `remove` deletes a key by dotted path (preserves comments and formatting) + * - `replace` does a full re-serialization (comments and formatting are NOT preserved). + * - `transformRaw` applies a function to the raw TOML string on disk. + */ +export class TomlFile { + /** + * Read and parse a TOML file from disk. Throws if the file doesn't exist or contains invalid TOML. + * Parse errors are wrapped in {@link TomlParseError} with the file path for context. + * + * @param path - Absolute path to the TOML file. + * @returns A TomlFile instance with parsed content. + */ + static async read(path: string): Promise { + const raw = await readFile(path) + try { + const content = decodeToml(raw) + return new TomlFile(path, content) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (err: any) { + if (err.line !== undefined && err.col !== undefined) { + throw new TomlParseError(path, err) + } + throw err + } + } + + readonly path: string + content: JsonMapType + + constructor(path: string, content: JsonMapType) { + this.path = path + this.content = content + } + + /** + * Surgically patch values in the TOML file, preserving comments and formatting. + * + * Accepts a nested object whose leaf values are set in the TOML. Intermediate tables are + * created automatically. Setting a leaf to `undefined` removes it (use `remove()` for a + * clearer API when deleting keys). + * + * @example + * ```ts + * await file.patch({build: {dev_store_url: 'my-store.myshopify.com'}}) + * await file.patch({application_url: 'https://example.com', auth: {redirect_urls: ['...']}}) + * ``` + */ + async patch(changes: {[key: string]: unknown}): Promise { + const patches = flattenToPatchEntries(changes) + const raw = await readFile(this.path) + const updated = updateTomlValues(raw, patches) + await writeFile(this.path, updated) + this.content = decodeToml(updated) + } + + /** + * Remove a key from the TOML file by dotted path, preserving comments and formatting. + * + * @param keyPath - Dotted key path to remove (e.g. 'build.include_config_on_deploy'). + * @example + * ```ts + * await file.remove('build.include_config_on_deploy') + * ``` + */ + async remove(keyPath: string): Promise { + const keys = keyPath.split('.') + const raw = await readFile(this.path) + const updated = updateTomlValues(raw, [[keys, undefined]]) + await writeFile(this.path, updated) + this.content = decodeToml(updated) + } + + /** + * Replace the entire file content. The file is fully re-serialized — comments and formatting + * are NOT preserved. + * + * @param content - The new content to write. + * @example + * ```ts + * await file.replace({client_id: 'abc', name: 'My App'}) + * ``` + */ + async replace(content: JsonMapType): Promise { + const encoded = encodeToml(content) + await writeFile(this.path, encoded) + this.content = content + } + + /** + * Transform the raw TOML string on disk. Reads the file, applies the transform function + * to the raw text, writes back, and re-parses to keep `content` in sync. + * + * Use this for text-level operations that can't be expressed as structured edits — + * e.g. Injecting comments or positional insertion of keys in arrays-of-tables. + * Subsequent `patch()` calls will preserve any comments added this way. + * + * @param transform - A function that receives the raw TOML string and returns the modified string. + * @example + * ```ts + * await file.transformRaw((raw) => `# Header comment\n${raw}`) + * ``` + */ + async transformRaw(transform: (raw: string) => string): Promise { + const raw = await readFile(this.path) + const transformed = transform(raw) + await writeFile(this.path, transformed) + this.content = decodeToml(transformed) + } +} + +/** + * Flatten a nested object into an array of `[keyPath, value]` patch entries + * suitable for `updateTomlValues`. + * + * @param obj - The nested object to flatten. + * @param prefix - Key path prefix for recursion. + * @returns Flattened patch entries. + */ +function flattenToPatchEntries(obj: {[key: string]: unknown}, prefix: string[] = []): [string[], TomlPatchValue][] { + const entries: [string[], TomlPatchValue][] = [] + for (const [key, value] of Object.entries(obj)) { + const path = [...prefix, key] + if (value !== null && typeof value === 'object' && !Array.isArray(value)) { + entries.push(...flattenToPatchEntries(value as {[key: string]: unknown}, path)) + } else { + entries.push([path, value as TomlPatchValue]) + } + } + return entries +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7a9a193f3ff..9c8fab35382 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -351,6 +351,9 @@ importers: '@opentelemetry/semantic-conventions': specifier: 1.28.0 version: 1.28.0 + '@shopify/toml-patch': + specifier: 0.3.0 + version: 0.3.0 '@types/archiver': specifier: 5.3.2 version: 5.3.2 From 4e36d26b7491890a86bede3cc3096aa528aea8e8 Mon Sep 17 00:00:00 2001 From: Ryan Bahan Date: Fri, 6 Mar 2026 09:23:38 -0700 Subject: [PATCH 2/3] update extension-to-toml tests --- .../cli/services/flow/extension-to-toml.test.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/app/src/cli/services/flow/extension-to-toml.test.ts b/packages/app/src/cli/services/flow/extension-to-toml.test.ts index a7d4e058196..0764c0f50d6 100644 --- a/packages/app/src/cli/services/flow/extension-to-toml.test.ts +++ b/packages/app/src/cli/services/flow/extension-to-toml.test.ts @@ -38,7 +38,13 @@ describe('extension-to-toml', () => { {type: 'customer_reference', required: true}, {type: 'product_reference', required: true}, {key: 'email field', description: 'email help', type: 'email', name: 'email label', required: false}, - {key: 'number name', description: 'number help', type: 'number_decimal', name: 'number label', required: true}, + { + key: 'number name', + description: 'number help', + type: 'number_decimal', + name: 'number label', + required: true, + }, ], }, }) @@ -79,7 +85,13 @@ describe('extension-to-toml', () => { {type: 'customer_reference', required: true}, {type: 'product_reference', required: true}, {key: 'email field', description: 'email help', type: 'email', name: 'email label', required: false}, - {key: 'number name', description: 'number help', type: 'number_decimal', name: 'number label', required: true}, + { + key: 'number name', + description: 'number help', + type: 'number_decimal', + name: 'number label', + required: true, + }, ], }, }) From b3b43569ece4cdf9cbb9b06955a0029d2a901fb9 Mon Sep 17 00:00:00 2001 From: Ryan Bahan Date: Fri, 6 Mar 2026 09:43:45 -0700 Subject: [PATCH 3/3] =?UTF-8?q?Rename=20extension-to-toml=20=E2=86=92=20ex?= =?UTF-8?q?tension-config-builder,=20buildTomlObject=20=E2=86=92=20buildEx?= =?UTF-8?q?tensionConfig?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These functions now return config objects instead of TOML strings, so the names should reflect that. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/cli/commands/app/import-extensions.ts | 2 +- .../src/cli/prompts/import-extensions.test.ts | 20 ++++++++--------- .../app/src/cli/prompts/import-extensions.ts | 22 +++++++++---------- ...st.ts => extension-config-builder.test.ts} | 12 +++++----- ...to-toml.ts => extension-config-builder.ts} | 2 +- ...st.ts => extension-config-builder.test.ts} | 10 ++++----- ...to-toml.ts => extension-config-builder.ts} | 2 +- .../cli/services/import-extensions.test.ts | 14 ++++++------ .../app/src/cli/services/import-extensions.ts | 8 +++---- ...st.ts => extension-config-builder.test.ts} | 10 ++++----- ...to-toml.ts => extension-config-builder.ts} | 2 +- ...st.ts => extension-config-builder.test.ts} | 20 ++++++++--------- ...to-toml.ts => extension-config-builder.ts} | 2 +- ...st.ts => extension-config-builder.test.ts} | 8 +++---- ...to-toml.ts => extension-config-builder.ts} | 2 +- 15 files changed, 68 insertions(+), 68 deletions(-) rename packages/app/src/cli/services/admin-link/{extension-to-toml.test.ts => extension-config-builder.test.ts} (91%) rename packages/app/src/cli/services/admin-link/{extension-to-toml.ts => extension-config-builder.ts} (98%) rename packages/app/src/cli/services/flow/{extension-to-toml.test.ts => extension-config-builder.test.ts} (96%) rename packages/app/src/cli/services/flow/{extension-to-toml.ts => extension-config-builder.ts} (97%) rename packages/app/src/cli/services/marketing_activity/{extension-to-toml.test.ts => extension-config-builder.test.ts} (88%) rename packages/app/src/cli/services/marketing_activity/{extension-to-toml.ts => extension-config-builder.ts} (98%) rename packages/app/src/cli/services/payments/{extension-to-toml.test.ts => extension-config-builder.test.ts} (97%) rename packages/app/src/cli/services/payments/{extension-to-toml.ts => extension-config-builder.ts} (97%) rename packages/app/src/cli/services/subscription_link/{extension-to-toml.test.ts => extension-config-builder.test.ts} (86%) rename packages/app/src/cli/services/subscription_link/{extension-to-toml.ts => extension-config-builder.ts} (92%) diff --git a/packages/app/src/cli/commands/app/import-extensions.ts b/packages/app/src/cli/commands/app/import-extensions.ts index bd7d77bf4b6..6eff533f4c3 100644 --- a/packages/app/src/cli/commands/app/import-extensions.ts +++ b/packages/app/src/cli/commands/app/import-extensions.ts @@ -48,7 +48,7 @@ export default class ImportExtensions extends AppLinkedCommand { ...appContext, extensions, extensionTypes: migrationChoice.extensionTypes, - buildTomlObject: migrationChoice.buildTomlObject, + buildExtensionConfig: migrationChoice.buildExtensionConfig, }) } diff --git a/packages/app/src/cli/prompts/import-extensions.test.ts b/packages/app/src/cli/prompts/import-extensions.test.ts index f4e24840faa..f44bc323946 100644 --- a/packages/app/src/cli/prompts/import-extensions.test.ts +++ b/packages/app/src/cli/prompts/import-extensions.test.ts @@ -23,10 +23,10 @@ describe('allMigrationChoices', () => { expect(choice).toHaveProperty('label') expect(choice).toHaveProperty('value') expect(choice).toHaveProperty('extensionTypes') - expect(choice).toHaveProperty('buildTomlObject') + expect(choice).toHaveProperty('buildExtensionConfig') expect(Array.isArray(choice.extensionTypes)).toBe(true) expect(choice.extensionTypes.length).toBeGreaterThan(0) - expect(typeof choice.buildTomlObject).toBe('function') + expect(typeof choice.buildExtensionConfig).toBe('function') }) }) @@ -134,7 +134,7 @@ describe('selectMigrationChoice', () => { label: 'Test Extension', value: 'test', extensionTypes: ['test_type'], - buildTomlObject: vi.fn(), + buildExtensionConfig: vi.fn(), } const result = await selectMigrationChoice([singleChoice]) expect(result).toBe(singleChoice) @@ -147,13 +147,13 @@ describe('selectMigrationChoice', () => { label: 'Choice 1', value: 'choice1', extensionTypes: ['type1'], - buildTomlObject: vi.fn(), + buildExtensionConfig: vi.fn(), }, { label: 'Choice 2', value: 'choice2', extensionTypes: ['type2'], - buildTomlObject: vi.fn(), + buildExtensionConfig: vi.fn(), }, ] @@ -177,13 +177,13 @@ describe('selectMigrationChoice', () => { label: 'Choice 1', value: 'choice1', extensionTypes: ['type1'], - buildTomlObject: vi.fn(), + buildExtensionConfig: vi.fn(), }, { label: 'Choice 2', value: 'choice2', extensionTypes: ['type2'], - buildTomlObject: vi.fn(), + buildExtensionConfig: vi.fn(), }, ] @@ -204,19 +204,19 @@ describe('selectMigrationChoice', () => { label: 'Payments Extensions', value: 'payments', extensionTypes: ['payments_app'], - buildTomlObject: vi.fn(), + buildExtensionConfig: vi.fn(), }, { label: 'Flow Extensions', value: 'flow', extensionTypes: ['flow_action_definition'], - buildTomlObject: vi.fn(), + buildExtensionConfig: vi.fn(), }, { label: 'Marketing Activity Extensions', value: 'marketing activity', extensionTypes: ['marketing_activity_extension'], - buildTomlObject: vi.fn(), + buildExtensionConfig: vi.fn(), }, ] diff --git a/packages/app/src/cli/prompts/import-extensions.ts b/packages/app/src/cli/prompts/import-extensions.ts index 347b5a8be03..bb0928a99ec 100644 --- a/packages/app/src/cli/prompts/import-extensions.ts +++ b/packages/app/src/cli/prompts/import-extensions.ts @@ -1,8 +1,8 @@ -import {buildTomlObject as buildPaymentsTomlObject} from '../services/payments/extension-to-toml.js' -import {buildTomlObject as buildFlowTomlObject} from '../services/flow/extension-to-toml.js' -import {buildTomlObject as buildAdminLinkTomlObject} from '../services/admin-link/extension-to-toml.js' -import {buildTomlObject as buildMarketingActivityTomlObject} from '../services/marketing_activity/extension-to-toml.js' -import {buildTomlObject as buildSubscriptionLinkTomlObject} from '../services/subscription_link/extension-to-toml.js' +import {buildExtensionConfig as buildPaymentsConfig} from '../services/payments/extension-config-builder.js' +import {buildExtensionConfig as buildFlowConfig} from '../services/flow/extension-config-builder.js' +import {buildExtensionConfig as buildAdminLinkConfig} from '../services/admin-link/extension-config-builder.js' +import {buildExtensionConfig as buildMarketingActivityConfig} from '../services/marketing_activity/extension-config-builder.js' +import {buildExtensionConfig as buildSubscriptionLinkConfig} from '../services/subscription_link/extension-config-builder.js' import {ExtensionRegistration} from '../api/graphql/all_app_extension_registrations.js' import {CurrentAppConfiguration} from '../models/app/app.js' import {AbortError} from '@shopify/cli-kit/node/error' @@ -12,7 +12,7 @@ export interface MigrationChoice { label: string value: string extensionTypes: string[] - buildTomlObject: ( + buildExtensionConfig: ( ext: ExtensionRegistration, allExtensions: ExtensionRegistration[], appConfiguration: CurrentAppConfiguration, @@ -31,31 +31,31 @@ export const allMigrationChoices: MigrationChoice[] = [ 'payments_app_redeemable', 'payments_extension', ], - buildTomlObject: buildPaymentsTomlObject, + buildExtensionConfig: buildPaymentsConfig, }, { label: 'Flow Extensions', value: 'flow', extensionTypes: ['flow_action_definition', 'flow_trigger_definition', 'flow_trigger_discovery_webhook'], - buildTomlObject: buildFlowTomlObject, + buildExtensionConfig: buildFlowConfig, }, { label: 'Marketing Activity Extensions', value: 'marketing activity', extensionTypes: ['marketing_activity_extension'], - buildTomlObject: buildMarketingActivityTomlObject, + buildExtensionConfig: buildMarketingActivityConfig, }, { label: 'Subscription Link Extensions', value: 'subscription link', extensionTypes: ['subscription_link', 'subscription_link_extension'], - buildTomlObject: buildSubscriptionLinkTomlObject, + buildExtensionConfig: buildSubscriptionLinkConfig, }, { label: 'Admin Link extensions', value: 'link extension', extensionTypes: ['app_link', 'bulk_action'], - buildTomlObject: buildAdminLinkTomlObject, + buildExtensionConfig: buildAdminLinkConfig, }, ] diff --git a/packages/app/src/cli/services/admin-link/extension-to-toml.test.ts b/packages/app/src/cli/services/admin-link/extension-config-builder.test.ts similarity index 91% rename from packages/app/src/cli/services/admin-link/extension-to-toml.test.ts rename to packages/app/src/cli/services/admin-link/extension-config-builder.test.ts index 82066d9ea44..89abf903ac9 100644 --- a/packages/app/src/cli/services/admin-link/extension-to-toml.test.ts +++ b/packages/app/src/cli/services/admin-link/extension-config-builder.test.ts @@ -1,8 +1,8 @@ -import {buildTomlObject} from './extension-to-toml.js' +import {buildExtensionConfig} from './extension-config-builder.js' import {ExtensionRegistration} from '../../api/graphql/all_app_extension_registrations.js' import {describe, expect, test} from 'vitest' -describe('extension-to-toml', () => { +describe('extension-config-builder', () => { test('correctly builds a toml object for a app_link extension on a non embedded app', () => { // Given const appConfig = { @@ -25,7 +25,7 @@ describe('extension-to-toml', () => { } // When - const got = buildTomlObject(extension1, [], appConfig) + const got = buildExtensionConfig(extension1, [], appConfig) // Then expect(got).toEqual({ @@ -66,7 +66,7 @@ describe('extension-to-toml', () => { } // When - const got = buildTomlObject(extension1, [], appConfig) + const got = buildExtensionConfig(extension1, [], appConfig) // Then expect(got).toEqual({ @@ -106,7 +106,7 @@ describe('extension-to-toml', () => { } // When - const got = buildTomlObject(extension1, [], appConfig) + const got = buildExtensionConfig(extension1, [], appConfig) // Then expect(got).toEqual({ @@ -146,7 +146,7 @@ describe('extension-to-toml', () => { } // When - const got = buildTomlObject(extension1, [], appConfig) + const got = buildExtensionConfig(extension1, [], appConfig) // Then expect(got).toEqual({ diff --git a/packages/app/src/cli/services/admin-link/extension-to-toml.ts b/packages/app/src/cli/services/admin-link/extension-config-builder.ts similarity index 98% rename from packages/app/src/cli/services/admin-link/extension-to-toml.ts rename to packages/app/src/cli/services/admin-link/extension-config-builder.ts index 71e426fd361..a77295ca087 100644 --- a/packages/app/src/cli/services/admin-link/extension-to-toml.ts +++ b/packages/app/src/cli/services/admin-link/extension-config-builder.ts @@ -12,7 +12,7 @@ interface AdminLinkConfig { /** * Given an app_link or bulk_action extension config file, convert it to toml */ -export function buildTomlObject( +export function buildExtensionConfig( extension: ExtensionRegistration, _: ExtensionRegistration[], appConfiguration: CurrentAppConfiguration, diff --git a/packages/app/src/cli/services/flow/extension-to-toml.test.ts b/packages/app/src/cli/services/flow/extension-config-builder.test.ts similarity index 96% rename from packages/app/src/cli/services/flow/extension-to-toml.test.ts rename to packages/app/src/cli/services/flow/extension-config-builder.test.ts index 0764c0f50d6..b58eac99422 100644 --- a/packages/app/src/cli/services/flow/extension-to-toml.test.ts +++ b/packages/app/src/cli/services/flow/extension-config-builder.test.ts @@ -1,8 +1,8 @@ -import {buildTomlObject} from './extension-to-toml.js' +import {buildExtensionConfig} from './extension-config-builder.js' import {ExtensionRegistration} from '../../api/graphql/all_app_extension_registrations.js' import {describe, expect, test} from 'vitest' -describe('extension-to-toml', () => { +describe('extension-config-builder', () => { test('correctly builds a toml object for a flow_action', () => { // Given const extension1: ExtensionRegistration = { @@ -17,7 +17,7 @@ describe('extension-to-toml', () => { } // When - const got = buildTomlObject(extension1) + const got = buildExtensionConfig(extension1) // Then expect(got).toEqual({ @@ -64,7 +64,7 @@ describe('extension-to-toml', () => { } // When - const got = buildTomlObject(extension1) + const got = buildExtensionConfig(extension1) // Then expect(got).toEqual({ @@ -115,7 +115,7 @@ describe('extension-to-toml', () => { } // When - const got = buildTomlObject(extension2) + const got = buildExtensionConfig(extension2) // Then expect(got).toEqual({ diff --git a/packages/app/src/cli/services/flow/extension-to-toml.ts b/packages/app/src/cli/services/flow/extension-config-builder.ts similarity index 97% rename from packages/app/src/cli/services/flow/extension-to-toml.ts rename to packages/app/src/cli/services/flow/extension-config-builder.ts index 6c5f1775531..3d800cd5e60 100644 --- a/packages/app/src/cli/services/flow/extension-to-toml.ts +++ b/packages/app/src/cli/services/flow/extension-config-builder.ts @@ -31,7 +31,7 @@ interface FlowWebhookConfig { * Given a flow extension config file, convert it to toml * Works for both trigger and action because trigger config is a subset of action config */ -export function buildTomlObject(extension: ExtensionRegistration): object { +export function buildExtensionConfig(extension: ExtensionRegistration): object { const versionConfig = extension.activeVersion?.config ?? extension.draftVersion?.config if (!versionConfig) throw new Error('No config found for extension') diff --git a/packages/app/src/cli/services/import-extensions.test.ts b/packages/app/src/cli/services/import-extensions.test.ts index 060d886297b..47e60a58c9a 100644 --- a/packages/app/src/cli/services/import-extensions.test.ts +++ b/packages/app/src/cli/services/import-extensions.test.ts @@ -1,5 +1,5 @@ import {importExtensions, filterOutImportedExtensions} from './import-extensions.js' -import {buildTomlObject} from './flow/extension-to-toml.js' +import {buildExtensionConfig} from './flow/extension-config-builder.js' import {testAppLinked, testDeveloperPlatformClient, testUIExtension} from '../models/app/app.test-data.js' import {OrganizationApp} from '../models/organization.js' import {ExtensionRegistration} from '../api/graphql/all_app_extension_registrations.js' @@ -110,7 +110,7 @@ describe('import-extensions', () => { 'subscription_link_extension', ], extensions, - buildTomlObject, + buildExtensionConfig, }) expect(renderSuccess).toHaveBeenCalledWith({ @@ -167,7 +167,7 @@ describe('import-extensions', () => { developerPlatformClient: testDeveloperPlatformClient(), extensionTypes: ['flow_action_definition'], extensions, - buildTomlObject, + buildExtensionConfig, }) // Then - expect the success message to be shown (even for skipped extensions) @@ -213,7 +213,7 @@ describe('import-extensions', () => { developerPlatformClient: testDeveloperPlatformClient(), extensionTypes: ['flow_action_definition'], extensions, - buildTomlObject, + buildExtensionConfig, }) // Then - expect the success message to be shown @@ -261,7 +261,7 @@ describe('import-extensions', () => { developerPlatformClient: testDeveloperPlatformClient(), extensionTypes: ['flow_action_definition'], extensions, - buildTomlObject, + buildExtensionConfig, }), ).rejects.toThrow(AbortSilentError) @@ -300,7 +300,7 @@ describe('import-extensions', () => { 'subscription_link_extension', ], extensions, - buildTomlObject, + buildExtensionConfig, }) expect(renderSuccess).toHaveBeenCalledWith({ @@ -349,7 +349,7 @@ describe('import-extensions', () => { 'subscription_link_extension', ], extensions, - buildTomlObject, + buildExtensionConfig, }), ).rejects.toThrow('No extensions to migrate') diff --git a/packages/app/src/cli/services/import-extensions.ts b/packages/app/src/cli/services/import-extensions.ts index 2bbd726b6ab..9c72206eaf1 100644 --- a/packages/app/src/cli/services/import-extensions.ts +++ b/packages/app/src/cli/services/import-extensions.ts @@ -26,7 +26,7 @@ interface ImportAllOptions { interface ImportOptions extends ImportAllOptions { extensionTypes: string[] - buildTomlObject: ( + buildExtensionConfig: ( ext: ExtensionRegistration, allExtensions: ExtensionRegistration[], appConfig: CurrentAppConfiguration, @@ -79,7 +79,7 @@ async function handleExtensionDirectory({ } export async function importExtensions(options: ImportOptions) { - const {app, remoteApp, developerPlatformClient, extensionTypes, extensions, buildTomlObject, all} = options + const {app, remoteApp, developerPlatformClient, extensionTypes, extensions, buildExtensionConfig, all} = options let extensionsToMigrate = extensions.filter((ext) => extensionTypes.includes(ext.type.toLowerCase())) extensionsToMigrate = filterOutImportedExtensions(app, extensionsToMigrate) @@ -111,7 +111,7 @@ export async function importExtensions(options: ImportOptions) { extensionUuids[handle] = ext.uuid if (action === DirectoryAction.Write) { - const tomlContent = buildTomlObject(ext, extensions, app.configuration) + const tomlContent = buildExtensionConfig(ext, extensions, app.configuration) const tomlPath = joinPath(directory, 'shopify.extension.toml') const file = new TomlFile(tomlPath, tomlContent as JsonMapType) await file.replace(tomlContent as JsonMapType) @@ -150,7 +150,7 @@ export async function importAllExtensions(options: ImportAllOptions) { return importExtensions({ ...options, extensionTypes: choice.extensionTypes, - buildTomlObject: choice.buildTomlObject, + buildExtensionConfig: choice.buildExtensionConfig, all: true, }) }), diff --git a/packages/app/src/cli/services/marketing_activity/extension-to-toml.test.ts b/packages/app/src/cli/services/marketing_activity/extension-config-builder.test.ts similarity index 88% rename from packages/app/src/cli/services/marketing_activity/extension-to-toml.test.ts rename to packages/app/src/cli/services/marketing_activity/extension-config-builder.test.ts index b3e44d8db52..99d4368cbdc 100644 --- a/packages/app/src/cli/services/marketing_activity/extension-to-toml.test.ts +++ b/packages/app/src/cli/services/marketing_activity/extension-config-builder.test.ts @@ -1,4 +1,4 @@ -import {buildTomlObject, MarketingActivityDashboardConfig} from './extension-to-toml.js' +import {buildExtensionConfig, MarketingActivityDashboardConfig} from './extension-config-builder.js' import {ExtensionRegistration} from '../../api/graphql/all_app_extension_registrations.js' import {describe, expect, test} from 'vitest' @@ -24,7 +24,7 @@ const defaultDashboardConfig: MarketingActivityDashboardConfig = { }, ], } -describe('extension-to-toml', () => { +describe('extension-config-builder', () => { test('converts the dashboard config to the new cli config', () => { // Given const extension: ExtensionRegistration = { @@ -38,7 +38,7 @@ describe('extension-to-toml', () => { } // When - const got = buildTomlObject(extension) + const got = buildExtensionConfig(extension) // Then expect(got).toEqual({ @@ -86,7 +86,7 @@ describe('extension-to-toml', () => { } // When - const got = buildTomlObject(extension) as {extensions: {handle: string}[]} + const got = buildExtensionConfig(extension) as {extensions: {handle: string}[]} // Then expect(got.extensions[0]!.handle).toBe('mae-test-12345555555554444447777778888888123455') @@ -105,7 +105,7 @@ describe('extension-to-toml', () => { } // When - const got = buildTomlObject(extension) as {extensions: {marketing_channel: string; referring_domain: string}[]} + const got = buildExtensionConfig(extension) as {extensions: {marketing_channel: string; referring_domain: string}[]} // Then expect(got.extensions[0]!.marketing_channel).toBe('') diff --git a/packages/app/src/cli/services/marketing_activity/extension-to-toml.ts b/packages/app/src/cli/services/marketing_activity/extension-config-builder.ts similarity index 98% rename from packages/app/src/cli/services/marketing_activity/extension-to-toml.ts rename to packages/app/src/cli/services/marketing_activity/extension-config-builder.ts index 268a87ba9cc..92eba4cf761 100644 --- a/packages/app/src/cli/services/marketing_activity/extension-to-toml.ts +++ b/packages/app/src/cli/services/marketing_activity/extension-config-builder.ts @@ -162,7 +162,7 @@ function getUrlPath(url: string) { /** * Given a dashboard-built marketing activity extension config file, convert it to toml for the CLI extension */ -export function buildTomlObject(extension: ExtensionRegistration): object { +export function buildExtensionConfig(extension: ExtensionRegistration): object { const versionConfig = extension.activeVersion?.config ?? extension.draftVersion?.config if (!versionConfig) throw new Error('No config found for extension') const config: MarketingActivityDashboardConfig = JSON.parse(versionConfig) diff --git a/packages/app/src/cli/services/payments/extension-to-toml.test.ts b/packages/app/src/cli/services/payments/extension-config-builder.test.ts similarity index 97% rename from packages/app/src/cli/services/payments/extension-to-toml.test.ts rename to packages/app/src/cli/services/payments/extension-config-builder.test.ts index d3bdd64a555..a55d4d13329 100644 --- a/packages/app/src/cli/services/payments/extension-to-toml.test.ts +++ b/packages/app/src/cli/services/payments/extension-config-builder.test.ts @@ -1,4 +1,4 @@ -import {buildTomlObject, DashboardPaymentExtensionType} from './extension-to-toml.js' +import {buildExtensionConfig, DashboardPaymentExtensionType} from './extension-config-builder.js' import {ExtensionRegistration} from '../../api/graphql/all_app_extension_registrations.js' import {describe, expect, test} from 'vitest' @@ -53,7 +53,7 @@ const expectObjectIncludesKeys = (got: object, config: string) => { } } -describe('extension-to-toml', () => { +describe('extension-config-builder', () => { test('correctly builds a toml object for a CLI payments extension', async () => { // Given const extension1: ExtensionRegistration = { @@ -68,7 +68,7 @@ describe('extension-to-toml', () => { } // When - const got = buildTomlObject(extension1, [extension1]) + const got = buildExtensionConfig(extension1, [extension1]) // Then expectObjectIncludesKeys(got, SAMPLE_OFFSITE_CONFIG) @@ -116,7 +116,7 @@ describe('extension-to-toml', () => { } // When - const got = buildTomlObject(extension1, [extension1]) + const got = buildExtensionConfig(extension1, [extension1]) // Then expectObjectIncludesKeys(got, SAMPLE_OFFSITE_CONFIG) @@ -163,7 +163,7 @@ describe('extension-to-toml', () => { } // When - const got = buildTomlObject(extension1, [extension1]) + const got = buildExtensionConfig(extension1, [extension1]) // Then expectObjectIncludesKeys(got, SAMPLE_OFFSITE_CONFIG) @@ -216,7 +216,7 @@ describe('extension-to-toml', () => { } // When - const got = buildTomlObject(extension1, [extension1, extension2]) + const got = buildExtensionConfig(extension1, [extension1, extension2]) // Then expectObjectIncludesKeys(got, SAMPLE_CREDIT_CARD_CONFIG) @@ -271,7 +271,7 @@ describe('extension-to-toml', () => { } // When - const got = buildTomlObject(extension1, [extension1, extension2]) + const got = buildExtensionConfig(extension1, [extension1, extension2]) // Then expectObjectIncludesKeys(got, SAMPLE_CUSTOM_CREDIT_CARD_CONFIG) @@ -325,7 +325,7 @@ describe('extension-to-toml', () => { } // When - const got = buildTomlObject(extension1, [extension1, extension2]) + const got = buildExtensionConfig(extension1, [extension1, extension2]) // Then expectObjectIncludesKeys(got, SAMPLE_CUSTOM_ONSITE_CONFIG) @@ -383,7 +383,7 @@ describe('extension-to-toml', () => { } // When - const got = buildTomlObject(extension1, [extension1, extension2]) + const got = buildExtensionConfig(extension1, [extension1, extension2]) // Then expectObjectIncludesKeys(got, SAMPLE_REDEEMABLE_CONFIG) @@ -430,7 +430,7 @@ describe('extension-to-toml', () => { } // When - const got = buildTomlObject(extension1, [extension1]) + const got = buildExtensionConfig(extension1, [extension1]) // Then expectObjectIncludesKeys(got, SAMPLE_CARD_PRESENT_CONFIG) diff --git a/packages/app/src/cli/services/payments/extension-to-toml.ts b/packages/app/src/cli/services/payments/extension-config-builder.ts similarity index 97% rename from packages/app/src/cli/services/payments/extension-to-toml.ts rename to packages/app/src/cli/services/payments/extension-config-builder.ts index 522d8de2630..8be80b6bc36 100644 --- a/packages/app/src/cli/services/payments/extension-to-toml.ts +++ b/packages/app/src/cli/services/payments/extension-config-builder.ts @@ -58,7 +58,7 @@ export enum DashboardPaymentExtensionType { CardPresent = 'payments_app_card_present', } -export function buildTomlObject(extension: ExtensionRegistration, allExtensions: ExtensionRegistration[]): object { +export function buildExtensionConfig(extension: ExtensionRegistration, allExtensions: ExtensionRegistration[]): object { const context = extension.activeVersion?.context || extension.draftVersion?.context || typeToContext(extension.type) switch (context) { case OFFSITE_TARGET: diff --git a/packages/app/src/cli/services/subscription_link/extension-to-toml.test.ts b/packages/app/src/cli/services/subscription_link/extension-config-builder.test.ts similarity index 86% rename from packages/app/src/cli/services/subscription_link/extension-to-toml.test.ts rename to packages/app/src/cli/services/subscription_link/extension-config-builder.test.ts index 9a7fae4c5e7..19fe08d97ac 100644 --- a/packages/app/src/cli/services/subscription_link/extension-to-toml.test.ts +++ b/packages/app/src/cli/services/subscription_link/extension-config-builder.test.ts @@ -1,11 +1,11 @@ -import {buildTomlObject, SubscriptionLinkDashboardConfig} from './extension-to-toml.js' +import {buildExtensionConfig, SubscriptionLinkDashboardConfig} from './extension-config-builder.js' import {ExtensionRegistration} from '../../api/graphql/all_app_extension_registrations.js' import {describe, expect, test} from 'vitest' const defaultDashboardConfig: SubscriptionLinkDashboardConfig = { pattern: '/subscriptions{?customer_id,shop}&id={contract_id}', } -describe('extension-to-toml', () => { +describe('extension-config-builder', () => { test('converts the dashboard config to the new cli config', () => { // Given const extension: ExtensionRegistration = { @@ -19,7 +19,7 @@ describe('extension-to-toml', () => { } // When - const got = buildTomlObject(extension) + const got = buildExtensionConfig(extension) // Then expect(got).toEqual({ @@ -47,7 +47,7 @@ describe('extension-to-toml', () => { } // When - const got = buildTomlObject(extension) + const got = buildExtensionConfig(extension) // Then expect((got as {extensions: [{handle: string}]}).extensions[0].handle).toBe( diff --git a/packages/app/src/cli/services/subscription_link/extension-to-toml.ts b/packages/app/src/cli/services/subscription_link/extension-config-builder.ts similarity index 92% rename from packages/app/src/cli/services/subscription_link/extension-to-toml.ts rename to packages/app/src/cli/services/subscription_link/extension-config-builder.ts index 9ba6df67bc7..3e816c13da4 100644 --- a/packages/app/src/cli/services/subscription_link/extension-to-toml.ts +++ b/packages/app/src/cli/services/subscription_link/extension-config-builder.ts @@ -9,7 +9,7 @@ export interface SubscriptionLinkDashboardConfig { /** * Given a dashboard-built subscription link extension config file, convert it to toml for the CLI extension */ -export function buildTomlObject(extension: ExtensionRegistration): object { +export function buildExtensionConfig(extension: ExtensionRegistration): object { const versionConfig = extension.activeVersion?.config ?? extension.draftVersion?.config if (!versionConfig) throw new Error('No config found for extension') const config: SubscriptionLinkDashboardConfig = JSON.parse(versionConfig)