Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"permissions": {
"allow": [
"Bash(rg:*)",
"mcp__claudeception__search_agents",
"mcp__claudeception__generate_conversation",
"mcp__claudeception__append_conversation",
"mcp__claudeception__launch_claude_agent",
"mcp__claudeception__read_conversation",
"Bash(pnpm test:*)",
"Bash(pnpm vitest run:*)",
"Bash(pnpm lint:fix:*)",
"Bash(pnpm eslint:*)",
"Bash(pnpm run lint:*)",
"Bash(grep:*)",
"Bash(find:*)",
"Bash(npm run type-check:*)",
"Bash(npx tsc:*)"
],
"deny": []
}
}
46 changes: 33 additions & 13 deletions packages/app/src/cli/models/app/loader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import {
import {inTemporaryDirectory, moveFile, mkdir, mkTmpDir, rmdir, writeFile, readFile} from '@shopify/cli-kit/node/fs'
import {joinPath, dirname, cwd, normalizePath} from '@shopify/cli-kit/node/path'
import {platformAndArch} from '@shopify/cli-kit/node/os'
import {outputContent, outputToken} from '@shopify/cli-kit/node/output'
import {stringifyMessage} from '@shopify/cli-kit/node/output'
import {zod} from '@shopify/cli-kit/node/schema'
import colors from '@shopify/cli-kit/node/colors'
import {showMultipleCLIWarningIfNeeded} from '@shopify/cli-kit/node/multiple-installation-warning'
Expand Down Expand Up @@ -2752,9 +2752,14 @@ describe('parseConfigurationObject', () => {
message: 'Boolean is required',
},
]
const expectedFormatted = outputContent`\n${outputToken.errorText(
'Validation errors',
)} in tmp:\n\n${parseHumanReadableError(errorObject)}`
const expectedFormatted = stringifyMessage([
'\n',
{error: 'Validation errors'},
' in ',
{filePath: 'tmp'},
':\n\n',
parseHumanReadableError(errorObject),
])

const abortOrReport = vi.fn()

Expand All @@ -2779,9 +2784,14 @@ describe('parseConfigurationObject', () => {
message: 'Expected string, received array',
},
]
const expectedFormatted = outputContent`\n${outputToken.errorText(
'Validation errors',
)} in tmp:\n\n${parseHumanReadableError(errorObject)}`
const expectedFormatted = stringifyMessage([
'\n',
{error: 'Validation errors'},
' in ',
{filePath: 'tmp'},
':\n\n',
parseHumanReadableError(errorObject),
])
const abortOrReport = vi.fn()
await parseConfigurationObject(LegacyAppSchema, 'tmp', configurationObject, abortOrReport)

Expand Down Expand Up @@ -2828,9 +2838,14 @@ describe('parseConfigurationObject', () => {
message: 'Invalid input',
},
]
const expectedFormatted = outputContent`\n${outputToken.errorText(
'Validation errors',
)} in tmp:\n\n${parseHumanReadableError(errorObject)}`
const expectedFormatted = stringifyMessage([
'\n',
{error: 'Validation errors'},
' in ',
{filePath: 'tmp'},
':\n\n',
parseHumanReadableError(errorObject),
])
const abortOrReport = vi.fn()
await parseConfigurationObject(WebConfigurationSchema, 'tmp', configurationObject, abortOrReport)

Expand Down Expand Up @@ -3308,9 +3323,14 @@ describe('WebhooksSchema', () => {

async function setupParsing(errorObj: zod.ZodIssue | {}, webhookConfigOverrides: WebhooksConfig) {
const err = Array.isArray(errorObj) ? errorObj : [errorObj]
const expectedFormatted = outputContent`\n${outputToken.errorText(
'Validation errors',
)} in tmp:\n\n${parseHumanReadableError(err)}`
const expectedFormatted = stringifyMessage([
'\n',
{error: 'Validation errors'},
' in ',
{filePath: 'tmp'},
':\n\n',
parseHumanReadableError(err),
])
const abortOrReport = vi.fn()

const {path, ...toParse} = getWebhookConfig(webhookConfigOverrides)
Expand Down
86 changes: 53 additions & 33 deletions packages/app/src/cli/models/app/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ import {hashString} from '@shopify/cli-kit/node/crypto'
import {JsonMapType, decodeToml} 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'
import {outputDebug, OutputMessage, stringifyMessage} from '@shopify/cli-kit/node/output'
import {joinWithAnd, slugify} from '@shopify/cli-kit/common/string'
import {getArrayRejectingUndefined} from '@shopify/cli-kit/common/array'
import {showNotificationsIfNeeded} from '@shopify/cli-kit/node/notifications-system'
Expand Down Expand Up @@ -79,7 +79,7 @@ export async function loadConfigurationFileContent(
decode: (input: string) => JsonMapType = decodeToml,
): Promise<JsonMapType> {
if (!(await fileExists(filepath))) {
return abortOrReport(outputContent`Couldn't find an app toml file at ${outputToken.path(filepath)}`, {}, filepath)
return abortOrReport(stringifyMessage(["Couldn't find an app toml file at ", {filePath: filepath}]), {}, filepath)
}

try {
Expand All @@ -90,7 +90,7 @@ export async function loadConfigurationFileContent(
// 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}`,
stringifyMessage(['Fix the following error in ', {filePath: filepath}, ':\n', err.message]),
{},
filepath,
)
Expand Down Expand Up @@ -145,9 +145,14 @@ export function parseConfigurationObject<TSchema extends zod.ZodType>(
const parseResult = schema.safeParse(configurationObject)
if (!parseResult.success) {
return abortOrReport(
outputContent`\n${outputToken.errorText('Validation errors')} in ${outputToken.path(
filepath,
)}:\n\n${parseHumanReadableError(parseResult.error.issues)}`,
stringifyMessage([
'\n',
{error: 'Validation errors'},
' in ',
{filePath: filepath},
':\n\n',
parseHumanReadableError(parseResult.error.issues),
]),
fallbackOutput,
filepath,
)
Expand All @@ -172,9 +177,12 @@ export function parseConfigurationObjectAgainstSpecification<TSchema extends zod
case 'error': {
const fallbackOutput = {} as zod.TypeOf<TSchema>
return abortOrReport(
outputContent`App configuration is not valid\nValidation errors in ${outputToken.path(
filepath,
)}:\n\n${parseHumanReadableError(parsed.errors)}`,
stringifyMessage([
'App configuration is not valid\nValidation errors in ',
{filePath: filepath},
':\n\n',
parseHumanReadableError(parsed.errors),
]),
fallbackOutput,
filepath,
)
Expand Down Expand Up @@ -234,7 +242,7 @@ export async function checkFolderIsValidApp(directory: string) {
const thereAreConfigFiles = (await findConfigFiles(directory)).length > 0
if (thereAreConfigFiles) return
throw new AbortError(
outputContent`Couldn't find an app toml file at ${outputToken.path(directory)}, is this an app directory?`,
stringifyMessage(["Couldn't find an app toml file at ", {filePath: directory}, ', is this an app directory?']),
)
}

Expand Down Expand Up @@ -454,7 +462,11 @@ class AppLoader<TConfig extends AppConfiguration, TModuleSpec extends ExtensionS
const websOfType = webs.filter((web) => web.configuration.roles.includes(webType))
if (websOfType[1]) {
this.abortOrReport(
outputContent`You can only have one web with the ${outputToken.yellow(webType)} role in your app`,
stringifyMessage([
'You can only have one web with the ',
{color: {text: webType, color: 'yellow'}},
' role in your app',
]),
undefined,

joinPath(websOfType[1].directory, configurationFileNames.web),
Expand Down Expand Up @@ -489,7 +501,7 @@ class AppLoader<TConfig extends AppConfiguration, TModuleSpec extends ExtensionS
usedKnownSpecification = true
} else {
return this.abortOrReport(
outputContent`Invalid extension type "${type}" in "${relativizePath(configurationPath)}"`,
`Invalid extension type "${type}" in "${relativizePath(configurationPath)}"`,
undefined,
configurationPath,
)
Expand Down Expand Up @@ -526,7 +538,7 @@ class AppLoader<TConfig extends AppConfiguration, TModuleSpec extends ExtensionS
if (usedKnownSpecification) {
const validateResult = await extensionInstance.validate()
if (validateResult.isErr()) {
this.abortOrReport(outputContent`\n${validateResult.error}`, undefined, configurationPath)
this.abortOrReport(stringifyMessage(['\n', validateResult.error]), undefined, configurationPath)
}
}
return extensionInstance
Expand Down Expand Up @@ -554,10 +566,14 @@ class AppLoader<TConfig extends AppConfiguration, TModuleSpec extends ExtensionS
if (extension.handle && handles.has(extension.handle)) {
const matchingExtensions = allExtensions.filter((ext) => ext.handle === extension.handle)
const result = joinWithAnd(matchingExtensions.map((ext) => ext.name))
const handle = outputToken.cyan(extension.handle)

this.abortOrReport(
outputContent`Duplicated handle "${handle}" in extensions ${result}. Handle needs to be unique per extension.`,
stringifyMessage([
'Duplicated handle "',
{color: {text: extension.handle, color: 'cyan'}},
'" in extensions ',
result,
'. Handle needs to be unique per extension.',
]),
undefined,
extension.configurationPath,
)
Expand Down Expand Up @@ -586,7 +602,7 @@ class AppLoader<TConfig extends AppConfiguration, TModuleSpec extends ExtensionS
const parseResult = ExtensionsArraySchema.safeParse(obj)
if (!parseResult.success) {
this.abortOrReport(
outputContent`Invalid extension configuration at ${relativePath(appDirectory, configurationPath)}`,
stringifyMessage(['Invalid extension configuration at ', relativePath(appDirectory, configurationPath)]),
undefined,
configurationPath,
)
Expand All @@ -605,10 +621,7 @@ class AppLoader<TConfig extends AppConfiguration, TModuleSpec extends ExtensionS
if (!mergedConfig.handle) {
// Handle is required for unified config extensions.
this.abortOrReport(
outputContent`Missing handle for extension "${mergedConfig.name}" at ${relativePath(
appDirectory,
configurationPath,
)}`,
`Missing handle for extension "${mergedConfig.name}" at ${relativePath(appDirectory, configurationPath)}`,
undefined,
configurationPath,
)
Expand All @@ -622,9 +635,11 @@ class AppLoader<TConfig extends AppConfiguration, TModuleSpec extends ExtensionS
return this.createExtensionInstance(type, obj, configurationPath, directory)
} else {
return this.abortOrReport(
outputContent`Invalid extension type at "${outputToken.path(
relativePath(appDirectory, configurationPath),
)}". Please specify a type.`,
stringifyMessage([
'Invalid extension type at "',
{filePath: relativePath(appDirectory, configurationPath)},
'". Please specify a type.',
]),
undefined,
configurationPath,
)
Expand Down Expand Up @@ -706,12 +721,12 @@ class AppLoader<TConfig extends AppConfiguration, TModuleSpec extends ExtensionS
if (unusedKeys.length > 0) {
if (failIfUnsupportedConfigProperty) {
this.abortOrReport(
outputContent`Unsupported section(s) in app configuration: ${unusedKeys.sort().join(', ')}`,
stringifyMessage(['Unsupported section(s) in app configuration: ', unusedKeys.sort().join(', ')]),
undefined,
appConfiguration.path,
)
} else {
outputDebug(outputContent`Unused keys in app configuration: ${outputToken.json(unusedKeys)}`)
outputDebug(`Unused keys in app configuration: ${JSON.stringify(unusedKeys, null, 2)}`)
}
}
return extensionInstancesWithKeys
Expand Down Expand Up @@ -744,9 +759,12 @@ class AppLoader<TConfig extends AppConfiguration, TModuleSpec extends ExtensionS
).find((sourcePath) => sourcePath !== undefined)
if (!entryPath) {
this.abortOrReport(
outputContent`Couldn't find an index.{js,jsx,ts,tsx} file in the directories ${outputToken.path(
directory,
)} or ${outputToken.path(joinPath(directory, 'src'))}`,
stringifyMessage([
"Couldn't find an index.{js,jsx,ts,tsx} file in the directories ",
{filePath: directory},
' or ',
{filePath: joinPath(directory, 'src')},
]),
undefined,
directory,
)
Expand Down Expand Up @@ -788,7 +806,7 @@ class AppLoader<TConfig extends AppConfiguration, TModuleSpec extends ExtensionS
if (targetExtensions.length > 1) {
const extensionHandles = ['', ...targetExtensions.map((ext) => ext.configuration.handle)].join('\n · ')
this.abortOrReport(
outputContent`A single target can't support two print action extensions from the same app. Point your extensions at different targets, or remove an extension.\n\nThe following extensions both target ${target}:${extensionHandles}`,
`A single target can't support two print action extensions from the same app. Point your extensions at different targets, or remove an extension.\n\nThe following extensions both target ${target}:${extensionHandles}`,
undefined,
extension.configurationPath,
)
Expand Down Expand Up @@ -1043,7 +1061,9 @@ async function getConfigurationPath(appDirectory: string, configName: string | u
if (await fileExists(configurationPath)) {
return {configurationPath, configurationFileName}
} else {
throw new AbortError(outputContent`Couldn't find ${configurationFileName} in ${outputToken.path(appDirectory)}.`)
throw new AbortError(
stringifyMessage(["Couldn't find ", configurationFileName, ' in ', {filePath: appDirectory}, '.']),
)
}
}

Expand All @@ -1055,7 +1075,7 @@ async function getConfigurationPath(appDirectory: string, configName: string | u
*/
async function getAppDirectory(directory: string) {
if (!(await fileExists(directory))) {
throw new AbortError(outputContent`Couldn't find directory ${outputToken.path(directory)}`)
throw new AbortError(stringifyMessage(["Couldn't find directory ", {filePath: directory}]))
}

// In order to find the chosen config for the app, we need to find the directory of the app.
Expand All @@ -1078,7 +1098,7 @@ async function getAppDirectory(directory: string) {
return appDirectory
} else {
throw new AbortError(
outputContent`Couldn't find an app toml file at ${outputToken.path(directory)}, is this an app directory?`,
stringifyMessage(["Couldn't find an app toml file at ", {filePath: directory}, ', is this an app directory?']),
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {zod} from '@shopify/cli-kit/node/schema'
import {joinPath} from '@shopify/cli-kit/node/path'
import {fileExists, readFile} from '@shopify/cli-kit/node/fs'
import {AbortError} from '@shopify/cli-kit/node/error'
import {outputContent} from '@shopify/cli-kit/node/output'
import {randomUUID} from '@shopify/cli-kit/node/crypto'

interface UI {
Expand Down Expand Up @@ -144,7 +143,12 @@ const functionSpec = createExtensionSpecification({
const wasmExists = await fileExists(extension.outputPath)
if (!wasmExists) {
throw new AbortError(
outputContent`The function extension "${extension.handle}" hasn't compiled the wasm in the expected path: ${extension.outputPath}`,
[
'The function extension "',
{color: {text: extension.handle, color: 'cyan'}},
'" hasn\'t compiled the wasm in the expected path: ',
{color: {text: extension.outputPath, color: 'cyan'}},
],
`Make sure the build command outputs the wasm in the expected directory.`,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {useThemebundling} from '@shopify/cli-kit/node/context/local'
import {fileSize} from '@shopify/cli-kit/node/fs'
import {dirname, relativePath} from '@shopify/cli-kit/node/path'
import {AbortError} from '@shopify/cli-kit/node/error'
import {outputContent, outputToken} from '@shopify/cli-kit/node/output'

const themeSpec = createExtensionSpecification({
identifier: 'theme',
Expand Down Expand Up @@ -107,7 +106,10 @@ function validateLiquidBytes(liquidBytesTotal: number): void {
function validateFile(filepath: string, dirname: string): void {
if (!SUPPORTED_BUCKETS.includes(dirname)) {
throw new AbortError(
outputContent`Your theme app extension includes files in an unsupported directory, ${outputToken.path(dirname)}`,
[
'Your theme app extension includes files in an unsupported directory, ',
{color: {text: dirname, color: 'cyan'}},
],
`Make sure all theme app extension files are in the supported directories: ${SUPPORTED_BUCKETS.join(', ')}`,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {ExtensionInstance} from '../extension-instance.js'
import {err, ok, Result} from '@shopify/cli-kit/node/result'
import {fileExists, findPathUp} from '@shopify/cli-kit/node/fs'
import {dirname, joinPath, relativizePath} from '@shopify/cli-kit/node/path'
import {outputContent, outputToken} from '@shopify/cli-kit/node/output'
import {zod} from '@shopify/cli-kit/node/schema'
import {AbortError} from '@shopify/cli-kit/node/error'
import {createRequire} from 'module'
Expand Down Expand Up @@ -238,11 +237,9 @@ async function validateUIExtensionPointConfig(
const exists = await fileExists(fullPath)

if (!exists) {
const notFoundPath = outputToken.path(joinPath(directory, module))

errors.push(
outputContent`Couldn't find ${notFoundPath}
Please check the module path for ${target}`.value,
`Couldn't find ${joinPath(directory, module)}
Please check the module path for ${target}`,
)
}

Expand Down
Loading