11import { configurationFileNames } from '../../constants.js'
2+ import { AppConfigurationAbortError } from '../app/error-parsing.js'
23import { TomlFile } from '@shopify/cli-kit/node/toml/toml-file'
34import { readAndParseDotEnv , DotEnvFile } from '@shopify/cli-kit/node/dot-env'
45import { fileExists , glob , findPathUp , readFile } from '@shopify/cli-kit/node/fs'
@@ -9,7 +10,6 @@ import {
910 usesWorkspaces as detectUsesWorkspaces ,
1011} from '@shopify/cli-kit/node/node-package-manager'
1112import { joinPath , basename } from '@shopify/cli-kit/node/path'
12- import { AbortError } from '@shopify/cli-kit/node/error'
1313import { outputDebug } from '@shopify/cli-kit/node/output'
1414import { JsonMapType } from '@shopify/cli-kit/node/toml'
1515
@@ -21,6 +21,11 @@ const DEFAULT_EXTENSION_DIR = 'extensions/*'
2121const NODE_MODULES_EXCLUDE = '**/node_modules/**'
2222const DOTENV_GLOB = '.env*'
2323
24+ interface MalformedTomlFile {
25+ path : string
26+ message : string
27+ }
28+
2429/**
2530 * A Project is the Shopify app as it exists on the filesystem.
2631 *
@@ -45,9 +50,14 @@ export class Project {
4550 const directory = await findProjectRoot ( startDirectory )
4651
4752 // Discover all app config files
48- const appConfigFiles = await discoverAppConfigFiles ( directory )
53+ const { appConfigFiles, malformedAppConfigFiles } = await discoverAppConfigFiles ( directory )
4954 if ( appConfigFiles . length === 0 ) {
50- throw new AbortError ( `Could not find a Shopify app TOML file in ${ directory } ` )
55+ const preferredMalformedConfig = getPreferredMalformedAppConfig ( malformedAppConfigFiles )
56+ if ( preferredMalformedConfig ) {
57+ throw new AppConfigurationAbortError ( preferredMalformedConfig . message , preferredMalformedConfig . path )
58+ }
59+
60+ throw new AppConfigurationAbortError ( `Could not find a Shopify app TOML file in ${ directory } ` , directory )
5161 }
5262
5363 // Discover extension files from all app configs' extension_directories (union).
@@ -92,6 +102,7 @@ export class Project {
92102 nodeDependencies,
93103 usesWorkspaces,
94104 appConfigFiles,
105+ malformedAppConfigFiles,
95106 extensionConfigFiles,
96107 webConfigFiles,
97108 dotenvFiles,
@@ -104,6 +115,7 @@ export class Project {
104115 readonly nodeDependencies : Record < string , string >
105116 readonly usesWorkspaces : boolean
106117 readonly appConfigFiles : TomlFile [ ]
118+ readonly malformedAppConfigFiles : MalformedTomlFile [ ]
107119 readonly extensionConfigFiles : TomlFile [ ]
108120 readonly webConfigFiles : TomlFile [ ]
109121
@@ -119,6 +131,7 @@ export class Project {
119131 nodeDependencies : Record < string , string >
120132 usesWorkspaces : boolean
121133 appConfigFiles : TomlFile [ ]
134+ malformedAppConfigFiles : MalformedTomlFile [ ]
122135 extensionConfigFiles : TomlFile [ ]
123136 webConfigFiles : TomlFile [ ]
124137 dotenvFiles : Map < string , DotEnvFile >
@@ -129,6 +142,7 @@ export class Project {
129142 this . nodeDependencies = options . nodeDependencies
130143 this . usesWorkspaces = options . usesWorkspaces
131144 this . appConfigFiles = options . appConfigFiles
145+ this . malformedAppConfigFiles = options . malformedAppConfigFiles
132146 this . extensionConfigFiles = options . extensionConfigFiles
133147 this . webConfigFiles = options . webConfigFiles
134148 this . dotenvFiles = options . dotenvFiles
@@ -147,6 +161,11 @@ export class Project {
147161 return this . appConfigFiles . find ( ( file ) => file . content . client_id === clientId )
148162 }
149163
164+ /** Find a malformed app config file by filename. */
165+ malformedAppConfigByName ( fileName : string ) : MalformedTomlFile | undefined {
166+ return this . malformedAppConfigFiles . find ( ( file ) => basename ( file . path ) === fileName )
167+ }
168+
150169 /** The default app config (shopify.app.toml), if it exists */
151170 get defaultAppConfig ( ) : TomlFile | undefined {
152171 return this . appConfigByName ( configurationFileNames . app )
@@ -167,18 +186,22 @@ async function findProjectRoot(startDirectory: string): Promise<string> {
167186 } ,
168187 )
169188 if ( ! found ) {
170- throw new AbortError (
189+ throw new AppConfigurationAbortError (
171190 `Could not find a Shopify app configuration file. Looked in ${ startDirectory } and parent directories.` ,
191+ startDirectory ,
172192 )
173193 }
174194 return found
175195}
176196
177- async function discoverAppConfigFiles ( directory : string ) : Promise < TomlFile [ ] > {
197+ async function discoverAppConfigFiles (
198+ directory : string ,
199+ ) : Promise < { appConfigFiles : TomlFile [ ] ; malformedAppConfigFiles : MalformedTomlFile [ ] } > {
178200 const pattern = joinPath ( directory , APP_CONFIG_GLOB )
179201 const paths = await glob ( pattern )
180202 const validPaths = paths . filter ( ( filePath ) => APP_CONFIG_REGEX . test ( basename ( filePath ) ) )
181- return readTomlFilesSafe ( validPaths )
203+ const { files, malformedFiles} = await readTomlFiles ( validPaths )
204+ return { appConfigFiles : files , malformedAppConfigFiles : malformedFiles }
182205}
183206
184207async function discoverExtensionFiles ( directory : string , extensionDirectories ?: string [ ] ) : Promise < TomlFile [ ] > {
@@ -202,19 +225,45 @@ async function discoverWebFiles(directory: string, webDirectories?: string[]): P
202225 * This prevents a malformed inactive config or extension TOML
203226 * from blocking the active config from loading.
204227 */
205- async function readTomlFilesSafe ( paths : string [ ] ) : Promise < TomlFile [ ] > {
228+ async function readTomlFiles ( paths : string [ ] ) : Promise < { files : TomlFile [ ] ; malformedFiles : MalformedTomlFile [ ] } > {
206229 const results = await Promise . all (
207230 paths . map ( async ( filePath ) => {
208231 try {
209- return await TomlFile . read ( filePath )
232+ return { file : await TomlFile . read ( filePath ) }
210233 // eslint-disable-next-line no-catch-all/no-catch-all
211- } catch {
234+ } catch ( error ) {
212235 outputDebug ( `Skipping malformed TOML file: ${ filePath } ` )
213- return undefined
236+ return {
237+ malformedFile : {
238+ path : filePath ,
239+ message : error instanceof Error ? error . message : `Failed to parse ${ filePath } ` ,
240+ } ,
241+ }
214242 }
215243 } ) ,
216244 )
217- return results . filter ( ( file ) : file is TomlFile => file !== undefined )
245+
246+ const files : TomlFile [ ] = [ ]
247+ const malformedFiles : MalformedTomlFile [ ] = [ ]
248+
249+ for ( const result of results ) {
250+ if ( 'file' in result && result . file ) {
251+ files . push ( result . file )
252+ } else {
253+ malformedFiles . push ( result . malformedFile )
254+ }
255+ }
256+
257+ return { files, malformedFiles}
258+ }
259+
260+ async function readTomlFilesSafe ( paths : string [ ] ) : Promise < TomlFile [ ] > {
261+ const { files} = await readTomlFiles ( paths )
262+ return files
263+ }
264+
265+ function getPreferredMalformedAppConfig ( malformedFiles : MalformedTomlFile [ ] ) : MalformedTomlFile | undefined {
266+ return malformedFiles . find ( ( file ) => basename ( file . path ) === configurationFileNames . app ) ?? malformedFiles [ 0 ]
218267}
219268
220269/** Discover all .env* files in the project root */
0 commit comments