From dc3aa28836cd0ff0998f5535e8dbf7882b4f4226 Mon Sep 17 00:00:00 2001 From: Marcos Passos Date: Thu, 16 Jul 2026 11:33:17 -0300 Subject: [PATCH] Resolve configuration file against the current working directory The croct.json path was joined with the working directory once, when the configuration manager was instantiated at CLI startup. Templates that scaffold a project and change into it (e.g. change-directory) ended up reading and writing croct.json in the folder the CLI was launched from instead of the project folder. The manager now resolves the configuration file against the live project directory on each operation, keeping absolute path overrides untouched. Co-Authored-By: Claude Fable 5 --- .../manager/jsonConfigurationFileManager.ts | 13 +- src/infrastructure/application/cli/cli.ts | 7 +- .../jsonConfigurationFileManager.test.ts | 115 ++++++++++++++++++ 3 files changed, 127 insertions(+), 8 deletions(-) create mode 100644 test/application/project/configuration/jsonConfigurationFileManager.test.ts diff --git a/src/application/project/configuration/manager/jsonConfigurationFileManager.ts b/src/application/project/configuration/manager/jsonConfigurationFileManager.ts index 7c698a2a..9196a0c9 100644 --- a/src/application/project/configuration/manager/jsonConfigurationFileManager.ts +++ b/src/application/project/configuration/manager/jsonConfigurationFileManager.ts @@ -39,6 +39,8 @@ export class JsonConfigurationFileManager implements ConfigurationManager { private readonly fileSystem: FileSystem; + private readonly projectDirectory: WorkingDirectory; + private readonly fullValidator: Validator; private readonly partialValidator: Validator; @@ -47,6 +49,7 @@ export class JsonConfigurationFileManager implements ConfigurationManager { public constructor(configuration: Configuration) { this.fileSystem = configuration.fileSystem; + this.projectDirectory = configuration.projectDirectory; this.fullValidator = configuration.fullValidator; this.partialValidator = configuration.partialValidator; this.configurationFile = configuration.configurationFile; @@ -54,7 +57,7 @@ export class JsonConfigurationFileManager implements ConfigurationManager { public async isInitialized(state: InitializationState = InitializationState.ANY): Promise { if (state === InitializationState.ANY) { - return this.fileSystem.exists(this.configurationFile); + return this.fileSystem.exists(this.getConfigurationFilePath()); } const validator = state === InitializationState.FULL @@ -159,7 +162,7 @@ export class JsonConfigurationFileManager implements ConfigurationManager { validator: Validator, ): Promise>> { const file: LoadedFile = { - path: this.configurationFile, + path: this.getConfigurationFilePath(), source: null, configuration: null, }; @@ -184,6 +187,12 @@ export class JsonConfigurationFileManager implements ConfigurationManager { return file; } + private getConfigurationFilePath(): string { + return this.fileSystem.isAbsolutePath(this.configurationFile) + ? this.configurationFile + : this.fileSystem.joinPaths(this.projectDirectory.get(), this.configurationFile); + } + private async validateConfiguration( validator: Validator, value: JsonValue, diff --git a/src/infrastructure/application/cli/cli.ts b/src/infrastructure/application/cli/cli.ts index d4725c46..2696acde 100644 --- a/src/infrastructure/application/cli/cli.ts +++ b/src/infrastructure/application/cli/cli.ts @@ -2818,12 +2818,7 @@ export class Cli { fullValidator: new FullCroctConfigurationValidator(), partialValidator: new PartialCroctConfigurationValidator(), projectDirectory: this.workingDirectory, - configurationFile: fileSystem.isAbsolutePath(this.configuration.configurationFile) - ? this.configuration.configurationFile - : fileSystem.joinPaths( - this.workingDirectory.get(), - this.configuration.configurationFile, - ), + configurationFile: this.configuration.configurationFile, }); return new IndexedConfigurationManager({ diff --git a/test/application/project/configuration/jsonConfigurationFileManager.test.ts b/test/application/project/configuration/jsonConfigurationFileManager.test.ts new file mode 100644 index 00000000..9e0fcae8 --- /dev/null +++ b/test/application/project/configuration/jsonConfigurationFileManager.test.ts @@ -0,0 +1,115 @@ +import type {FileSystem} from '@/application/fs/fileSystem'; +import type {Validator, ValidationResult} from '@/application/validation'; +import {VirtualizedWorkingDirectory} from '@/application/fs/workingDirectory/virtualizedWorkingDirectory'; +import type { + JsonProjectConfiguration, + JsonPartialProjectConfiguration, +} from '@/application/project/configuration/manager/jsonConfigurationFileManager'; +import {JsonConfigurationFileManager} from '@/application/project/configuration/manager/jsonConfigurationFileManager'; +import type {ProjectConfiguration} from '@/application/project/configuration/projectConfiguration'; + +describe('JsonConfigurationFileManager', () => { + const configuration: ProjectConfiguration = { + organization: 'org', + workspace: 'workspace', + applications: { + development: 'dev', + }, + defaultLocale: 'en', + locales: ['en'], + slots: {}, + components: {}, + }; + + function createFileSystem(files: Map): FileSystem { + return { + exists: (path: string): Promise => Promise.resolve(files.has(path)), + readTextFile: (path: string): Promise => { + const content = files.get(path); + + if (content === undefined) { + return Promise.reject(new Error(`File not found: ${path}`)); + } + + return Promise.resolve(content); + }, + writeTextFile: (path: string, content: string): Promise => { + files.set(path, content); + + return Promise.resolve(); + }, + isAbsolutePath: (path: string): boolean => path.startsWith('/'), + joinPaths: (...paths: string[]): string => paths.join('/'), + } as Partial as FileSystem; + } + + function createValidator(): Validator { + return { + validate: (data: unknown): ValidationResult => ({ + valid: true, + data: data as T, + }), + }; + } + + it('should resolve the configuration file against the current project directory', async () => { + const files = new Map(); + const projectDirectory = new VirtualizedWorkingDirectory('/initial'); + + const manager = new JsonConfigurationFileManager({ + fileSystem: createFileSystem(files), + projectDirectory: projectDirectory, + fullValidator: createValidator(), + partialValidator: createValidator(), + configurationFile: 'croct.json', + }); + + projectDirectory.setCurrentDirectory('/initial/project'); + + await manager.update(configuration); + + expect(files.has('/initial/croct.json')).toBe(false); + expect(files.has('/initial/project/croct.json')).toBe(true); + }); + + it('should load the configuration from the current project directory', async () => { + const files = new Map(); + const projectDirectory = new VirtualizedWorkingDirectory('/initial'); + + const manager = new JsonConfigurationFileManager({ + fileSystem: createFileSystem(files), + projectDirectory: projectDirectory, + fullValidator: createValidator(), + partialValidator: createValidator(), + configurationFile: 'croct.json', + }); + + files.set('/initial/project/croct.json', JSON.stringify(configuration)); + + await expect(manager.isInitialized()).resolves.toBe(false); + + projectDirectory.setCurrentDirectory('/initial/project'); + + await expect(manager.isInitialized()).resolves.toBe(true); + await expect(manager.load()).resolves.toEqual(configuration); + }); + + it('should use an absolute configuration file path as given', async () => { + const files = new Map(); + const projectDirectory = new VirtualizedWorkingDirectory('/initial'); + + const manager = new JsonConfigurationFileManager({ + fileSystem: createFileSystem(files), + projectDirectory: projectDirectory, + fullValidator: createValidator(), + partialValidator: createValidator(), + configurationFile: '/custom/croct.json', + }); + + projectDirectory.setCurrentDirectory('/initial/project'); + + await manager.update(configuration); + + expect(files.has('/custom/croct.json')).toBe(true); + }); +});