Skip to content

Commit

Permalink
Merge pull request #4551 from Shopify/new-patch-config-file
Browse files Browse the repository at this point in the history
Patch app toml files
  • Loading branch information
isaacroldan authored Oct 18, 2024
2 parents 7b8165f + 710cc73 commit 720bb99
Show file tree
Hide file tree
Showing 7 changed files with 352 additions and 126 deletions.
174 changes: 174 additions & 0 deletions packages/app/src/cli/services/app/patch-app-configuration-file.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import {patchAppConfigurationFile} from './patch-app-configuration-file.js'
import {getAppVersionedSchema} from '../../models/app/app.js'
import {loadLocalExtensionsSpecifications} from '../../models/extensions/load-specifications.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"
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"
`

const schema = getAppVersionedSchema(await loadLocalExtensionsSpecifications(), false)

function writeDefaulToml(tmpDir: string) {
const configPath = joinPath(tmpDir, 'shopify.app.toml')
writeFileSync(configPath, defaultToml)
return configPath
}

describe('patchAppConfigurationFile', () => {
test('updates existing configuration with new values and adds new top-levelfields', async () => {
await inTemporaryDirectory(async (tmpDir) => {
const configPath = writeDefaulToml(tmpDir)
const patch = {
name: 'Updated App Name',
application_url: 'https://example.com',
access_scopes: {
use_legacy_install_flow: false,
},
}

await patchAppConfigurationFile({path: configPath, patch, schema})

const updatedTomlFile = await readFile(configPath)
expect(updatedTomlFile)
.toEqual(`# Learn more about configuring your app at https://shopify.dev/docs/apps/tools/cli/configuration
client_id = "12345"
name = "Updated App Name"
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 = false
[auth]
redirect_urls = [
"https://example.com/redirect",
"https://example.com/redirect2"
]
[webhooks]
api_version = "2023-04"
`)
})
})

test('Adds new table to the toml file', async () => {
await inTemporaryDirectory(async (tmpDir) => {
const configPath = writeDefaulToml(tmpDir)
const patch = {
application_url: 'https://example.com',
build: {
dev_store_url: 'example.myshopify.com',
},
}

await patchAppConfigurationFile({path: configPath, patch, schema})

const updatedTomlFile = await readFile(configPath)
expect(updatedTomlFile)
.toEqual(`# 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
[build]
dev_store_url = "example.myshopify.com"
[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"
`)
})
})

test('Adds a new field to a toml table, merging with exsisting values', async () => {
await inTemporaryDirectory(async (tmpDir) => {
const configPath = writeDefaulToml(tmpDir)
const patch = {
application_url: 'https://example.com',
access_scopes: {
scopes: 'read_products',
},
}

await patchAppConfigurationFile({path: configPath, patch, schema})

const updatedTomlFile = await readFile(configPath)
expect(updatedTomlFile)
.toEqual(`# 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
scopes = "read_products"
use_legacy_install_flow = true
[auth]
redirect_urls = [
"https://example.com/redirect",
"https://example.com/redirect2"
]
[webhooks]
api_version = "2023-04"
`)
})
})

test('does not validate the toml if no schema is provided', async () => {
await inTemporaryDirectory(async (tmpDir) => {
const configPath = joinPath(tmpDir, 'shopify.app.toml')
writeFileSync(
configPath,
`
random_toml_field = "random_value"
`,
)
const patch = {name: 123}

await patchAppConfigurationFile({path: configPath, patch, schema: undefined})

const updatedTomlFile = await readFile(configPath)
expect(updatedTomlFile)
.toEqual(`# Learn more about configuring your app at https://shopify.dev/docs/apps/tools/cli/configuration
random_toml_field = "random_value"
name = 123
`)
})
})
})
35 changes: 35 additions & 0 deletions packages/app/src/cli/services/app/patch-app-configuration-file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {addDefaultCommentsToToml} from './write-app-configuration-file.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 {decodeToml, encodeToml} from '@shopify/cli-kit/node/toml'

export interface PatchTomlOptions {
path: string
patch: {[key: string]: unknown}
schema?: zod.AnyZodObject
}

/**
* Updates an app/extension configuration file with the given patch.
*
* Only updates the given fields in the patch and leaves the rest of the file unchanged.
*
* @param path - The path to the app/extension configuration file.
* @param patch - The patch to apply to the app/extension configuration file.
* @param schema - The schema to validate the patch against. If not provided, the toml will not be validated.
*/
export async function patchAppConfigurationFile({path, patch, schema}: PatchTomlOptions) {
const tomlContents = await readFile(path)
const configuration = decodeToml(tomlContents)
const updatedConfig = deepMergeObjects(configuration, patch)

// Re-parse the config with the schema to validate the patch and keep the same order in the file
// Make every field optional to not crash on invalid tomls that are missing fields.
const validSchema = schema ?? zod.object({}).passthrough()
const validatedConfig = validSchema.partial().parse(updatedConfig)
let encodedString = encodeToml(validatedConfig)

encodedString = addDefaultCommentsToToml(encodedString)
await writeFile(path, encodedString)
}
31 changes: 19 additions & 12 deletions packages/app/src/cli/services/app/write-app-configuration-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ import {outputDebug} from '@shopify/cli-kit/node/output'
// so for now, we manually add comments
export async function writeAppConfigurationFile(configuration: CurrentAppConfiguration, schema: zod.ZodTypeAny) {
outputDebug(`Writing app configuration to ${configuration.path}`)
const initialComment = `# Learn more about configuring your app at https://shopify.dev/docs/apps/tools/cli/configuration\n`
const scopesComment = `\n# Learn more at https://shopify.dev/docs/apps/tools/cli/configuration#access_scopes`

// we need to condense the compliance and non-compliance webhooks again
// so compliance topics and topics with the same uri are under
Expand All @@ -21,18 +19,10 @@ export async function writeAppConfigurationFile(configuration: CurrentAppConfigu
const sorted = rewriteConfiguration(schema, condensedWebhooksAppConfiguration) as {
[key: string]: string | boolean | object
}
const fileSplit = encodeToml(sorted as JsonMapType).split(/(\r\n|\r|\n)/)

fileSplit.unshift('\n')
fileSplit.unshift(initialComment)

fileSplit.forEach((line, index) => {
if (line === '[access_scopes]') {
fileSplit.splice(index + 1, 0, scopesComment)
}
})
const encodedString = encodeToml(sorted as JsonMapType)

const file = fileSplit.join('')
const file = addDefaultCommentsToToml(encodedString)

writeFileSync(configuration.path, file)
}
Expand Down Expand Up @@ -89,6 +79,23 @@ export const rewriteConfiguration = <T extends zod.ZodTypeAny>(schema: T, config
return config
}

export function addDefaultCommentsToToml(fileString: string) {
const appTomlInitialComment = `# Learn more about configuring your app at https://shopify.dev/docs/apps/tools/cli/configuration\n`
const appTomlScopesComment = `\n# Learn more at https://shopify.dev/docs/apps/tools/cli/configuration#access_scopes`

const fileSplit = fileString.split(/(\r\n|\r|\n)/)
fileSplit.unshift('\n')
fileSplit.unshift(appTomlInitialComment)

fileSplit.forEach((line, index) => {
if (line === '[access_scopes]') {
fileSplit.splice(index + 1, 0, appTomlScopesComment)
}
})

return fileSplit.join('')
}

/**
* When we merge webhooks, we have the privacy and non-privacy compliance subscriptions
* separated for matching remote/local config purposes,
Expand Down
Loading

0 comments on commit 720bb99

Please sign in to comment.