From 9a57fc64f6f502fe47f4d2c02bf39875db6920d1 Mon Sep 17 00:00:00 2001 From: Marcos Passos Date: Fri, 27 Jun 2025 17:10:08 -0300 Subject: [PATCH 1/2] Add support for pre-initialization --- src/application/cli/command/init.ts | 31 ++++--- src/application/cli/command/install.ts | 68 +++++++++++++- .../manager/cachedConfigurationManager.ts | 18 +++- .../manager/configurationManager.ts | 15 +++- .../manager/indexedConfigurationManager.ts | 22 ++++- .../manager/jsonConfigurationFileManager.ts | 89 ++++++++++++++----- .../manager/newConfigurationManager.ts | 20 +++-- .../configuration/projectConfiguration.ts | 4 + src/application/project/sdk/javasScriptSdk.ts | 2 +- src/infrastructure/application/cli/cli.ts | 31 ++++--- .../validation/croctConfigurationValidator.ts | 36 ++++++-- 11 files changed, 269 insertions(+), 67 deletions(-) diff --git a/src/application/cli/command/init.ts b/src/application/cli/command/init.ts index c3782793..205be51a 100644 --- a/src/application/cli/command/init.ts +++ b/src/application/cli/command/init.ts @@ -66,11 +66,9 @@ export class InitCommand implements Command { public async execute(input: InitInput): Promise { const {configurationManager, platformProvider, sdkProvider, io: {output}} = this.config; - if (input.override !== true && await configurationManager.isInitialized()) { - throw new HelpfulError('Configuration file already exists, specify `override` to reconfigure.', { - reason: ErrorReason.PRECONDITION, - }); - } + const currentConfiguration = input.override !== true && await configurationManager.isInitialized() + ? await configurationManager.loadPartial() + : null; const platform = await platformProvider.get(); const projectName = platform !== null @@ -90,7 +88,7 @@ export class InitCommand implements Command { const organization = await this.getOrganization( {new: input.new === 'organization'}, - input.organization, + input.organization ?? currentConfiguration?.organization, ); if (organization === null) { @@ -104,7 +102,7 @@ export class InitCommand implements Command { organization: organization, new: input.new === 'workspace', }, - input.workspace, + input.workspace ?? currentConfiguration?.workspace, ); const applicationOptions: Omit = { @@ -119,7 +117,7 @@ export class InitCommand implements Command { ...applicationOptions, environment: ApplicationEnvironment.DEVELOPMENT, }, - input.devApplication, + input.devApplication ?? currentConfiguration?.applications?.development, ); const updatedConfiguration: ProjectConfiguration = { @@ -129,9 +127,10 @@ export class InitCommand implements Command { development: devApplication.slug, }, defaultLocale: workspace.defaultLocale, - locales: workspace.locales, - slots: {}, - components: {}, + locales: [...new Set([...(currentConfiguration?.locales ?? []), ...workspace.locales])], + slots: currentConfiguration?.slots ?? {}, + components: currentConfiguration?.components ?? {}, + paths: currentConfiguration?.paths ?? {}, }; const defaultWebsite = workspace.website ?? organization.website ?? undefined; @@ -142,12 +141,20 @@ export class InitCommand implements Command { ...applicationOptions, environment: ApplicationEnvironment.PRODUCTION, }, - input.prodApplication, + input.prodApplication ?? currentConfiguration?.applications?.production, ); updatedConfiguration.applications.production = prodApplication.slug; } + if (currentConfiguration !== null) { + await configurationManager.update(updatedConfiguration); + + output.confirm('Project configuration updated'); + + return; + } + const sdk = await sdkProvider.get(); if (sdk === null) { diff --git a/src/application/cli/command/install.ts b/src/application/cli/command/install.ts index 7144fc81..cc1adae4 100644 --- a/src/application/cli/command/install.ts +++ b/src/application/cli/command/install.ts @@ -1,11 +1,20 @@ import {Command} from '@/application/cli/command/command'; import {Output} from '@/application/cli/io/output'; import {Input} from '@/application/cli/io/input'; -import {ConfigurationManager} from '@/application/project/configuration/manager/configurationManager'; +import { + ConfigurationManager, + InitializationState, +} from '@/application/project/configuration/manager/configurationManager'; import {Installation, Sdk} from '@/application/project/sdk/sdk'; +import { + ProjectConfiguration, + ProjectConfigurationError, +} from '@/application/project/configuration/projectConfiguration'; +import {ErrorReason} from '@/application/error'; export type InstallInput = { clean?: boolean, + partialConfiguration?: boolean, }; export type InstallConfig = { @@ -25,14 +34,67 @@ export class InstallCommand implements Command { } public async execute(input: InstallInput): Promise { - const {sdk, configurationManager, io} = this.configuration; + const {sdk, io} = this.configuration; const installation: Installation = { input: io.input, output: io.output, - configuration: await configurationManager.load(), + configuration: await this.getConfiguration(input.partialConfiguration ?? false), }; await sdk.update(installation, {clean: input.clean}); } + + private async getConfiguration(partial: boolean): Promise { + const {configurationManager} = this.configuration; + + if (!partial || await configurationManager.isInitialized(InitializationState.FULL)) { + return configurationManager.load(); + } + + // Partial configuration allows the install command to run when the project is only + // partially initialized. + // This is useful for template projects that have some slots or parts configured, + // but where values like organization, workspace, and applications must be defined when + // connecting to the actual workspace. + const {applications, ...partialConfiguration} = await configurationManager.loadPartial(); + + return { + paths: {}, + slots: {}, + components: {}, + get organization(): string { + return InstallCommand.reportConfigurationError('organization'); + }, + get workspace(): string { + return InstallCommand.reportConfigurationError('workspace'); + }, + applications: { + get development(): string { + return InstallCommand.reportConfigurationError('applications.development'); + }, + get production(): string { + return InstallCommand.reportConfigurationError('applications.production'); + }, + ...applications, + }, + get defaultLocale(): string { + return InstallCommand.reportConfigurationError('defaultLocale'); + }, + get locales(): string[] { + return InstallCommand.reportConfigurationError('locales'); + }, + ...partialConfiguration, + }; + } + + private static reportConfigurationError(property: string): never { + throw new ProjectConfigurationError( + `The \`${property}\` property is not defined in the project configuration.`, + { + reason: ErrorReason.INVALID_CONFIGURATION, + suggestions: ['Run `init` command to initialize the project configuration.'], + }, + ); + } } diff --git a/src/application/project/configuration/manager/cachedConfigurationManager.ts b/src/application/project/configuration/manager/cachedConfigurationManager.ts index 1592c178..e192d657 100644 --- a/src/application/project/configuration/manager/cachedConfigurationManager.ts +++ b/src/application/project/configuration/manager/cachedConfigurationManager.ts @@ -1,5 +1,11 @@ -import {ProjectConfiguration} from '@/application/project/configuration/projectConfiguration'; -import {ConfigurationManager} from '@/application/project/configuration/manager/configurationManager'; +import { + PartialProjectConfiguration, + ProjectConfiguration, +} from '@/application/project/configuration/projectConfiguration'; +import { + ConfigurationManager, + InitializationState, +} from '@/application/project/configuration/manager/configurationManager'; export class CachedConfigurationManager implements ConfigurationManager { private readonly manager: ConfigurationManager; @@ -10,8 +16,8 @@ export class CachedConfigurationManager implements ConfigurationManager { this.manager = manager; } - public isInitialized(): Promise { - return this.manager.isInitialized(); + public isInitialized(state?: InitializationState): Promise { + return this.manager.isInitialized(state); } public load(): Promise { @@ -22,6 +28,10 @@ export class CachedConfigurationManager implements ConfigurationManager { return this.configuration; } + public loadPartial(): Promise { + return this.manager.loadPartial(); + } + public update(configuration: ProjectConfiguration): Promise { this.configuration = this.manager.update(configuration); diff --git a/src/application/project/configuration/manager/configurationManager.ts b/src/application/project/configuration/manager/configurationManager.ts index 0631770c..d4023ab6 100644 --- a/src/application/project/configuration/manager/configurationManager.ts +++ b/src/application/project/configuration/manager/configurationManager.ts @@ -1,9 +1,20 @@ -import {ProjectConfiguration} from '@/application/project/configuration/projectConfiguration'; +import { + PartialProjectConfiguration, + ProjectConfiguration, +} from '@/application/project/configuration/projectConfiguration'; + +export enum InitializationState { + PARTIAL = 'partial', + FULL = 'full', + ANY = 'any', +} export interface ConfigurationManager { - isInitialized(): Promise; + isInitialized(state?: InitializationState): Promise; load(): Promise; + loadPartial(): Promise; + update(configuration: ProjectConfiguration): Promise; } diff --git a/src/application/project/configuration/manager/indexedConfigurationManager.ts b/src/application/project/configuration/manager/indexedConfigurationManager.ts index 4245d259..74efa74f 100644 --- a/src/application/project/configuration/manager/indexedConfigurationManager.ts +++ b/src/application/project/configuration/manager/indexedConfigurationManager.ts @@ -1,5 +1,11 @@ -import {ProjectConfiguration} from '@/application/project/configuration/projectConfiguration'; -import {ConfigurationManager} from '@/application/project/configuration/manager/configurationManager'; +import { + PartialProjectConfiguration, + ProjectConfiguration, +} from '@/application/project/configuration/projectConfiguration'; +import { + ConfigurationManager, + InitializationState, +} from '@/application/project/configuration/manager/configurationManager'; import {WorkingDirectory} from '@/application/fs/workingDirectory/workingDirectory'; import {CliConfigurationProvider} from '@/application/cli/configuration/provider'; @@ -22,8 +28,8 @@ export class IndexedConfigurationManager implements ConfigurationManager { this.configurationProvider = configurationProvider; } - public isInitialized(): Promise { - return this.manager.isInitialized(); + public isInitialized(state?: InitializationState): Promise { + return this.manager.isInitialized(state); } public async load(): Promise { @@ -34,6 +40,14 @@ export class IndexedConfigurationManager implements ConfigurationManager { return configuration; } + public async loadPartial(): Promise { + const configuration = this.manager.loadPartial(); + + await this.updateIndex(); + + return configuration; + } + public update(configuration: ProjectConfiguration): Promise { return Promise.all([this.manager.update(configuration), this.updateIndex()]) .then(([result]) => result); diff --git a/src/application/project/configuration/manager/jsonConfigurationFileManager.ts b/src/application/project/configuration/manager/jsonConfigurationFileManager.ts index 9f866c6e..ca75114d 100644 --- a/src/application/project/configuration/manager/jsonConfigurationFileManager.ts +++ b/src/application/project/configuration/manager/jsonConfigurationFileManager.ts @@ -1,29 +1,38 @@ import {JsonValue} from '@croct/json'; import {JsonObjectNode, JsonParser} from '@croct/json5-parser'; import { + PartialProjectConfiguration, ProjectConfiguration, ProjectConfigurationError, } from '@/application/project/configuration/projectConfiguration'; import {FileSystem} from '@/application/fs/fileSystem'; import {Validator} from '@/application/validation'; import {WorkingDirectory} from '@/application/fs/workingDirectory/workingDirectory'; -import {ConfigurationManager} from '@/application/project/configuration/manager/configurationManager'; +import { + ConfigurationManager, + InitializationState, +} from '@/application/project/configuration/manager/configurationManager'; import {ErrorReason} from '@/application/error'; -type LoadedFile = { +type LoadedFile = { path: string, source: string|null, - configuration: JsonProjectConfiguration|null, + configuration: T|null, }; export type JsonProjectConfiguration = ProjectConfiguration & { $schema?: string, }; +export type JsonPartialProjectConfiguration = PartialProjectConfiguration & { + $schema?: string, +}; + export type Configuration = { fileSystem: FileSystem, projectDirectory: WorkingDirectory, - validator: Validator, + fullValidator: Validator, + partialValidator: Validator, }; export class JsonConfigurationFileManager implements ConfigurationManager { @@ -33,22 +42,41 @@ export class JsonConfigurationFileManager implements ConfigurationManager { private readonly projectDirectory: WorkingDirectory; - private readonly validator: Validator; + private readonly fullValidator: Validator; + + private readonly partialValidator: Validator; - public constructor({fileSystem, projectDirectory, validator}: Configuration) { + public constructor({fileSystem, projectDirectory, fullValidator, partialValidator}: Configuration) { this.fileSystem = fileSystem; this.projectDirectory = projectDirectory; - this.validator = validator; + this.fullValidator = fullValidator; + this.partialValidator = partialValidator; } - public isInitialized(): Promise { - return this.fileSystem.exists(this.getConfigurationFilePath()); + public async isInitialized(state: InitializationState = InitializationState.ANY): Promise { + if (state === InitializationState.ANY) { + return this.fileSystem.exists(this.getConfigurationFilePath()); + } + + const validator = state === InitializationState.FULL + ? this.fullValidator + : this.partialValidator; + + try { + return (await this.loadConfigurationFile(validator)).configuration !== null; + } catch (error) { + if (error instanceof ProjectConfigurationError) { + return false; + } + + throw error; + } } public async load(): Promise { - const file = await this.loadConfigurationFile(); + const {configuration} = await this.loadConfigurationFile(this.fullValidator); - if (file.configuration === null) { + if (configuration === null) { throw new ProjectConfigurationError('Project configuration not found.', { reason: ErrorReason.NOT_FOUND, suggestions: [ @@ -57,15 +85,29 @@ export class JsonConfigurationFileManager implements ConfigurationManager { }); } - return file.configuration; + return configuration; + } + + public async loadPartial(): Promise { + let configuration: JsonPartialProjectConfiguration = {}; + + try { + configuration = (await this.loadConfigurationFile(this.partialValidator)).configuration ?? {}; + } catch (error) { + if (!(error instanceof ProjectConfigurationError)) { + throw error; + } + } + + return configuration; } public async update(configuration: ProjectConfiguration): Promise { - return this.updateConfigurationFile(await this.validateConfiguration(configuration)); + return this.updateConfigurationFile(await this.validateConfiguration(this.fullValidator, configuration)); } private async updateConfigurationFile(configuration: ProjectConfiguration): Promise { - const file = await this.loadConfigurationFile(); + const file = await this.loadConfigurationFile(this.partialValidator); const source = this.applyConfigurationChanges(file, configuration); const json = source.toString({ indentationCharacter: 'space', @@ -114,8 +156,10 @@ export class JsonConfigurationFileManager implements ConfigurationManager { }).cast(JsonObjectNode); } - private async loadConfigurationFile(): Promise { - const file: LoadedFile = { + private async loadConfigurationFile( + validator: Validator, + ): Promise>> { + const file: LoadedFile = { path: this.getConfigurationFilePath(), source: null, configuration: null, @@ -131,7 +175,7 @@ export class JsonConfigurationFileManager implements ConfigurationManager { } if (configuration !== null) { - file.configuration = await this.validateConfiguration(configuration, file); + file.configuration = await this.validateConfiguration(validator, configuration, file); if (file.configuration?.$schema !== undefined) { delete file.configuration?.$schema; @@ -145,18 +189,23 @@ export class JsonConfigurationFileManager implements ConfigurationManager { return this.fileSystem.joinPaths(this.projectDirectory.get(), 'croct.json'); } - private async validateConfiguration(value: JsonValue, file?: LoadedFile): Promise { - const result = await this.validator.validate(value); + private async validateConfiguration( + validator: Validator, + value: JsonValue, + file?: LoadedFile, + ): Promise { + const result = await validator.validate(value); if (!result.valid) { const violation = result.violations[0]; - throw new ProjectConfigurationError(violation.message, { + throw new ProjectConfigurationError('The project configuration is invalid.', { details: [ ...(file !== undefined ? [`File: file://${file.path.replace(/\\/g, '/')}`] : [] ), + `Cause: ${violation.message}`, `Violation path: ${violation.path}`, ], }); diff --git a/src/application/project/configuration/manager/newConfigurationManager.ts b/src/application/project/configuration/manager/newConfigurationManager.ts index 971363aa..8066b383 100644 --- a/src/application/project/configuration/manager/newConfigurationManager.ts +++ b/src/application/project/configuration/manager/newConfigurationManager.ts @@ -1,5 +1,11 @@ -import {ProjectConfiguration} from '@/application/project/configuration/projectConfiguration'; -import {ConfigurationManager} from '@/application/project/configuration/manager/configurationManager'; +import { + PartialProjectConfiguration, + ProjectConfiguration, +} from '@/application/project/configuration/projectConfiguration'; +import { + ConfigurationManager, + InitializationState, +} from '@/application/project/configuration/manager/configurationManager'; export interface ConfigurationInitializer { initialize(): Promise; @@ -20,18 +26,22 @@ export class NewConfigurationManager implements ConfigurationManager { this.initializer = initializer; } - public isInitialized(): Promise { - return this.manager.isInitialized(); + public isInitialized(state?: InitializationState): Promise { + return this.manager.isInitialized(state); } public async load(): Promise { - if (!await this.isInitialized()) { + if (!await this.isInitialized(InitializationState.FULL)) { await this.initializer.initialize(); } return this.manager.load(); } + public loadPartial(): Promise { + return this.manager.loadPartial(); + } + public update(configuration: ProjectConfiguration): Promise { return this.manager.update(configuration); } diff --git a/src/application/project/configuration/projectConfiguration.ts b/src/application/project/configuration/projectConfiguration.ts index 45ecc36c..99044018 100644 --- a/src/application/project/configuration/projectConfiguration.ts +++ b/src/application/project/configuration/projectConfiguration.ts @@ -22,6 +22,10 @@ export type ProjectConfiguration = { paths?: Partial, }; +export type PartialProjectConfiguration = Partial> & { + applications?: Partial, +}; + export class ProjectConfigurationError extends HelpfulError { public constructor(message: string, help: Help = {}) { super(message, { diff --git a/src/application/project/sdk/javasScriptSdk.ts b/src/application/project/sdk/javasScriptSdk.ts index ee4bc38f..404f2712 100644 --- a/src/application/project/sdk/javasScriptSdk.ts +++ b/src/application/project/sdk/javasScriptSdk.ts @@ -198,7 +198,7 @@ export abstract class JavaScriptSdk implements Sdk { notifier.update('Registering script'); try { - await this.packageManager.addScript('postinstall', 'croct install'); + await this.packageManager.addScript('postinstall', 'croct --no-interaction install'); notifier.confirm('Script registered'); } catch (error) { diff --git a/src/infrastructure/application/cli/cli.ts b/src/infrastructure/application/cli/cli.ts index e091311d..f09f3ce2 100644 --- a/src/infrastructure/application/cli/cli.ts +++ b/src/infrastructure/application/cli/cli.ts @@ -61,7 +61,10 @@ import {AddComponentCommand, AddComponentInput} from '@/application/cli/command/ import {ComponentForm} from '@/application/cli/form/workspace/componentForm'; import {RemoveSlotCommand, RemoveSlotInput} from '@/application/cli/command/slot/remove'; import {RemoveComponentCommand, RemoveComponentInput} from '@/application/cli/command/component/remove'; -import {ConfigurationManager} from '@/application/project/configuration/manager/configurationManager'; +import { + ConfigurationManager, + InitializationState, +} from '@/application/project/configuration/manager/configurationManager'; import {NewConfigurationManager} from '@/application/project/configuration/manager/newConfigurationManager'; import {InstallCommand, InstallInput} from '@/application/cli/command/install'; import {PageForm} from '@/application/cli/form/page'; @@ -115,7 +118,10 @@ import {HttpFileProvider} from '@/application/provider/resource/httpFileProvider import {ResourceProvider, ResourceProviderError} from '@/application/provider/resource/resourceProvider'; import {ErrorReason, HelpfulError} from '@/application/error'; import {PartialNpmPackageValidator} from '@/infrastructure/application/validation/partialNpmPackageValidator'; -import {CroctConfigurationValidator} from '@/infrastructure/application/validation/croctConfigurationValidator'; +import { + FullCroctConfigurationValidator, + PartialCroctConfigurationValidator, +} from '@/infrastructure/application/validation/croctConfigurationValidator'; import {ValidatedProvider} from '@/application/provider/resource/validatedProvider'; import {FileContentProvider} from '@/application/provider/resource/fileContentProvider'; import {Json5Provider} from '@/application/provider/resource/json5Provider'; @@ -586,6 +592,11 @@ export class Cli { } public install(input: InstallInput): Promise { + // Force partial configuration when running non-interactively or in DND mode. + // This skips prompts for missing project values and uses getters that throw errors + // if those values are accessed — useful for CI where missing info will fail fast if needed. + const partialConfiguration = !this.configuration.interactive || this.configuration.dnd; + return this.execute( new InstallCommand({ sdk: this.getSdk(), @@ -595,7 +606,10 @@ export class Cli { output: this.getOutput(), }, }), - input, + { + clean: input.clean ?? false, + partialConfiguration: input.partialConfiguration ?? partialConfiguration, + }, ); } @@ -1296,7 +1310,7 @@ export class Cli { callback: async (): Promise => { const manager = this.getConfigurationManager(); - if (!await manager.isInitialized()) { + if (!await manager.isInitialized(InitializationState.FULL)) { return this.init({}); } }, @@ -2255,10 +2269,10 @@ export class Cli { private getConfigurationManager(): ConfigurationManager { return this.share(this.getConfigurationManager, () => { - const output = this.getOutput(); const manager = new JsonConfigurationFileManager({ fileSystem: this.getFileSystem(), - validator: new CroctConfigurationValidator(), + fullValidator: new FullCroctConfigurationValidator(), + partialValidator: new PartialCroctConfigurationValidator(), projectDirectory: this.workingDirectory, }); @@ -2270,10 +2284,7 @@ export class Cli { ? new NewConfigurationManager({ manager: manager, initializer: { - initialize: async (): Promise => { - await this.init({}); - output.break(); - }, + initialize: () => this.init({}), }, }) : manager, diff --git a/src/infrastructure/application/validation/croctConfigurationValidator.ts b/src/infrastructure/application/validation/croctConfigurationValidator.ts index c15380d3..44443cee 100644 --- a/src/infrastructure/application/validation/croctConfigurationValidator.ts +++ b/src/infrastructure/application/validation/croctConfigurationValidator.ts @@ -1,7 +1,14 @@ import {z, ZodTypeDef} from 'zod'; import {ZodValidator} from '@/infrastructure/application/validation/zodValidator'; import {Version} from '@/application/model/version'; -import {ProjectConfiguration} from '@/application/project/configuration/projectConfiguration'; +import { + PartialProjectConfiguration, + ProjectConfiguration, +} from '@/application/project/configuration/projectConfiguration'; +import { + JsonPartialProjectConfiguration, + JsonProjectConfiguration, +} from '@/application/project/configuration/manager/jsonConfigurationFileManager'; const identifierSchema = z.string().regex( /^[a-z]+(-?[a-z0-9]+)*$/i, @@ -29,10 +36,10 @@ const versionSchema = z.string() 'Version range must not exceed 5 major versions.', ); -type PartialProjectConfiguration = Omit - & Partial>; +type LenientProjectConfiguration = Omit + & Partial>; -const configurationSchema: z.ZodType = z.strictObject({ +const propertySchemas = { $schema: z.string().optional(), organization: identifierSchema, workspace: identifierSchema, @@ -53,13 +60,30 @@ const configurationSchema: z.ZodType data.locales.includes(data.defaultLocale), { +}; + +const partialConfigurationSchema: z.ZodType = z.strictObject({ + ...propertySchemas, + applications: propertySchemas.applications + .partial() + .optional(), +}).partial(); + +const configurationSchema: z.ZodType = z.strictObject( + propertySchemas, +).refine(data => data.locales.includes(data.defaultLocale), { message: 'The default locale is not included in the list of locales.', path: ['defaultLocale'], }); -export class CroctConfigurationValidator extends ZodValidator { +export class FullCroctConfigurationValidator extends ZodValidator { public constructor() { super(configurationSchema); } } + +export class PartialCroctConfigurationValidator extends ZodValidator { + public constructor() { + super(partialConfigurationSchema); + } +} From bf492919039fc1f251a7ff6f946965929de5540b Mon Sep 17 00:00:00 2001 From: Marcos Passos Date: Fri, 27 Jun 2025 17:46:06 -0300 Subject: [PATCH 2/2] Add support for pre-initialization --- .../validation/croctConfigurationValidator.ts | 42 ++++++++++++------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/src/infrastructure/application/validation/croctConfigurationValidator.ts b/src/infrastructure/application/validation/croctConfigurationValidator.ts index 44443cee..d1eb29c5 100644 --- a/src/infrastructure/application/validation/croctConfigurationValidator.ts +++ b/src/infrastructure/application/validation/croctConfigurationValidator.ts @@ -1,4 +1,4 @@ -import {z, ZodTypeDef} from 'zod'; +import {z, ZodOptional, ZodTypeDef} from 'zod'; import {ZodValidator} from '@/infrastructure/application/validation/zodValidator'; import {Version} from '@/application/model/version'; import { @@ -39,7 +39,32 @@ const versionSchema = z.string() type LenientProjectConfiguration = Omit & Partial>; -const propertySchemas = { +function optional(schema: z.ZodType): ZodOptional> { + return schema.optional().catch(undefined) as unknown as ZodOptional>; +} + +const partialConfigurationSchema: z.ZodType = z.object({ + $schema: optional(z.string()), + organization: optional(identifierSchema), + workspace: optional(identifierSchema), + applications: optional(z.object({ + development: optional(identifierSchema), + production: optional(identifierSchema), + })), + locales: optional(z.array(localeSchema).min(1)), + defaultLocale: optional(localeSchema), + slots: optional(z.record(versionSchema)), + components: optional(z.record(versionSchema)), + paths: optional(z.object({ + source: optional(z.string()), + utilities: optional(z.string()), + components: optional(z.string()), + examples: optional(z.string()), + content: optional(z.string()), + })), +}); + +const configurationSchema: z.ZodType = z.strictObject({ $schema: z.string().optional(), organization: identifierSchema, workspace: identifierSchema, @@ -60,18 +85,7 @@ const propertySchemas = { examples: z.string().optional(), content: z.string().optional(), }).optional(), -}; - -const partialConfigurationSchema: z.ZodType = z.strictObject({ - ...propertySchemas, - applications: propertySchemas.applications - .partial() - .optional(), -}).partial(); - -const configurationSchema: z.ZodType = z.strictObject( - propertySchemas, -).refine(data => data.locales.includes(data.defaultLocale), { +}).refine(data => data.locales.includes(data.defaultLocale), { message: 'The default locale is not included in the list of locales.', path: ['defaultLocale'], });