Skip to content

Commit e91ae5e

Browse files
authored
Add support for partially defined project configurations (#107)
1 parent 5f48c4e commit e91ae5e

11 files changed

Lines changed: 283 additions & 67 deletions

File tree

src/application/cli/command/init.ts

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,9 @@ export class InitCommand implements Command<InitInput> {
6666
public async execute(input: InitInput): Promise<void> {
6767
const {configurationManager, platformProvider, sdkProvider, io: {output}} = this.config;
6868

69-
if (input.override !== true && await configurationManager.isInitialized()) {
70-
throw new HelpfulError('Configuration file already exists, specify `override` to reconfigure.', {
71-
reason: ErrorReason.PRECONDITION,
72-
});
73-
}
69+
const currentConfiguration = input.override !== true && await configurationManager.isInitialized()
70+
? await configurationManager.loadPartial()
71+
: null;
7472

7573
const platform = await platformProvider.get();
7674
const projectName = platform !== null
@@ -90,7 +88,7 @@ export class InitCommand implements Command<InitInput> {
9088

9189
const organization = await this.getOrganization(
9290
{new: input.new === 'organization'},
93-
input.organization,
91+
input.organization ?? currentConfiguration?.organization,
9492
);
9593

9694
if (organization === null) {
@@ -104,7 +102,7 @@ export class InitCommand implements Command<InitInput> {
104102
organization: organization,
105103
new: input.new === 'workspace',
106104
},
107-
input.workspace,
105+
input.workspace ?? currentConfiguration?.workspace,
108106
);
109107

110108
const applicationOptions: Omit<ApplicationOptions, 'environment'> = {
@@ -119,7 +117,7 @@ export class InitCommand implements Command<InitInput> {
119117
...applicationOptions,
120118
environment: ApplicationEnvironment.DEVELOPMENT,
121119
},
122-
input.devApplication,
120+
input.devApplication ?? currentConfiguration?.applications?.development,
123121
);
124122

125123
const updatedConfiguration: ProjectConfiguration = {
@@ -129,9 +127,10 @@ export class InitCommand implements Command<InitInput> {
129127
development: devApplication.slug,
130128
},
131129
defaultLocale: workspace.defaultLocale,
132-
locales: workspace.locales,
133-
slots: {},
134-
components: {},
130+
locales: [...new Set([...(currentConfiguration?.locales ?? []), ...workspace.locales])],
131+
slots: currentConfiguration?.slots ?? {},
132+
components: currentConfiguration?.components ?? {},
133+
paths: currentConfiguration?.paths ?? {},
135134
};
136135

137136
const defaultWebsite = workspace.website ?? organization.website ?? undefined;
@@ -142,12 +141,20 @@ export class InitCommand implements Command<InitInput> {
142141
...applicationOptions,
143142
environment: ApplicationEnvironment.PRODUCTION,
144143
},
145-
input.prodApplication,
144+
input.prodApplication ?? currentConfiguration?.applications?.production,
146145
);
147146

148147
updatedConfiguration.applications.production = prodApplication.slug;
149148
}
150149

150+
if (currentConfiguration !== null) {
151+
await configurationManager.update(updatedConfiguration);
152+
153+
output.confirm('Project configuration updated');
154+
155+
return;
156+
}
157+
151158
const sdk = await sdkProvider.get();
152159

153160
if (sdk === null) {

src/application/cli/command/install.ts

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
import {Command} from '@/application/cli/command/command';
22
import {Output} from '@/application/cli/io/output';
33
import {Input} from '@/application/cli/io/input';
4-
import {ConfigurationManager} from '@/application/project/configuration/manager/configurationManager';
4+
import {
5+
ConfigurationManager,
6+
InitializationState,
7+
} from '@/application/project/configuration/manager/configurationManager';
58
import {Installation, Sdk} from '@/application/project/sdk/sdk';
9+
import {
10+
ProjectConfiguration,
11+
ProjectConfigurationError,
12+
} from '@/application/project/configuration/projectConfiguration';
13+
import {ErrorReason} from '@/application/error';
614

715
export type InstallInput = {
816
clean?: boolean,
17+
partialConfiguration?: boolean,
918
};
1019

1120
export type InstallConfig = {
@@ -25,14 +34,67 @@ export class InstallCommand implements Command<InstallInput> {
2534
}
2635

2736
public async execute(input: InstallInput): Promise<void> {
28-
const {sdk, configurationManager, io} = this.configuration;
37+
const {sdk, io} = this.configuration;
2938

3039
const installation: Installation = {
3140
input: io.input,
3241
output: io.output,
33-
configuration: await configurationManager.load(),
42+
configuration: await this.getConfiguration(input.partialConfiguration ?? false),
3443
};
3544

3645
await sdk.update(installation, {clean: input.clean});
3746
}
47+
48+
private async getConfiguration(partial: boolean): Promise<ProjectConfiguration> {
49+
const {configurationManager} = this.configuration;
50+
51+
if (!partial || await configurationManager.isInitialized(InitializationState.FULL)) {
52+
return configurationManager.load();
53+
}
54+
55+
// Partial configuration allows the install command to run when the project is only
56+
// partially initialized.
57+
// This is useful for template projects that have some slots or parts configured,
58+
// but where values like organization, workspace, and applications must be defined when
59+
// connecting to the actual workspace.
60+
const {applications, ...partialConfiguration} = await configurationManager.loadPartial();
61+
62+
return {
63+
paths: {},
64+
slots: {},
65+
components: {},
66+
get organization(): string {
67+
return InstallCommand.reportConfigurationError('organization');
68+
},
69+
get workspace(): string {
70+
return InstallCommand.reportConfigurationError('workspace');
71+
},
72+
applications: {
73+
get development(): string {
74+
return InstallCommand.reportConfigurationError('applications.development');
75+
},
76+
get production(): string {
77+
return InstallCommand.reportConfigurationError('applications.production');
78+
},
79+
...applications,
80+
},
81+
get defaultLocale(): string {
82+
return InstallCommand.reportConfigurationError('defaultLocale');
83+
},
84+
get locales(): string[] {
85+
return InstallCommand.reportConfigurationError('locales');
86+
},
87+
...partialConfiguration,
88+
};
89+
}
90+
91+
private static reportConfigurationError(property: string): never {
92+
throw new ProjectConfigurationError(
93+
`The \`${property}\` property is not defined in the project configuration.`,
94+
{
95+
reason: ErrorReason.INVALID_CONFIGURATION,
96+
suggestions: ['Run `init` command to initialize the project configuration.'],
97+
},
98+
);
99+
}
38100
}

src/application/project/configuration/manager/cachedConfigurationManager.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1-
import {ProjectConfiguration} from '@/application/project/configuration/projectConfiguration';
2-
import {ConfigurationManager} from '@/application/project/configuration/manager/configurationManager';
1+
import {
2+
PartialProjectConfiguration,
3+
ProjectConfiguration,
4+
} from '@/application/project/configuration/projectConfiguration';
5+
import {
6+
ConfigurationManager,
7+
InitializationState,
8+
} from '@/application/project/configuration/manager/configurationManager';
39

410
export class CachedConfigurationManager implements ConfigurationManager {
511
private readonly manager: ConfigurationManager;
@@ -10,8 +16,8 @@ export class CachedConfigurationManager implements ConfigurationManager {
1016
this.manager = manager;
1117
}
1218

13-
public isInitialized(): Promise<boolean> {
14-
return this.manager.isInitialized();
19+
public isInitialized(state?: InitializationState): Promise<boolean> {
20+
return this.manager.isInitialized(state);
1521
}
1622

1723
public load(): Promise<ProjectConfiguration> {
@@ -22,6 +28,10 @@ export class CachedConfigurationManager implements ConfigurationManager {
2228
return this.configuration;
2329
}
2430

31+
public loadPartial(): Promise<PartialProjectConfiguration> {
32+
return this.manager.loadPartial();
33+
}
34+
2535
public update(configuration: ProjectConfiguration): Promise<ProjectConfiguration> {
2636
this.configuration = this.manager.update(configuration);
2737

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,20 @@
1-
import {ProjectConfiguration} from '@/application/project/configuration/projectConfiguration';
1+
import {
2+
PartialProjectConfiguration,
3+
ProjectConfiguration,
4+
} from '@/application/project/configuration/projectConfiguration';
5+
6+
export enum InitializationState {
7+
PARTIAL = 'partial',
8+
FULL = 'full',
9+
ANY = 'any',
10+
}
211

312
export interface ConfigurationManager {
4-
isInitialized(): Promise<boolean>;
13+
isInitialized(state?: InitializationState): Promise<boolean>;
514

615
load(): Promise<ProjectConfiguration>;
716

17+
loadPartial(): Promise<PartialProjectConfiguration>;
18+
819
update(configuration: ProjectConfiguration): Promise<ProjectConfiguration>;
920
}

src/application/project/configuration/manager/indexedConfigurationManager.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1-
import {ProjectConfiguration} from '@/application/project/configuration/projectConfiguration';
2-
import {ConfigurationManager} from '@/application/project/configuration/manager/configurationManager';
1+
import {
2+
PartialProjectConfiguration,
3+
ProjectConfiguration,
4+
} from '@/application/project/configuration/projectConfiguration';
5+
import {
6+
ConfigurationManager,
7+
InitializationState,
8+
} from '@/application/project/configuration/manager/configurationManager';
39
import {WorkingDirectory} from '@/application/fs/workingDirectory/workingDirectory';
410
import {CliConfigurationProvider} from '@/application/cli/configuration/provider';
511

@@ -22,8 +28,8 @@ export class IndexedConfigurationManager implements ConfigurationManager {
2228
this.configurationProvider = configurationProvider;
2329
}
2430

25-
public isInitialized(): Promise<boolean> {
26-
return this.manager.isInitialized();
31+
public isInitialized(state?: InitializationState): Promise<boolean> {
32+
return this.manager.isInitialized(state);
2733
}
2834

2935
public async load(): Promise<ProjectConfiguration> {
@@ -34,6 +40,14 @@ export class IndexedConfigurationManager implements ConfigurationManager {
3440
return configuration;
3541
}
3642

43+
public async loadPartial(): Promise<PartialProjectConfiguration> {
44+
const configuration = this.manager.loadPartial();
45+
46+
await this.updateIndex();
47+
48+
return configuration;
49+
}
50+
3751
public update(configuration: ProjectConfiguration): Promise<ProjectConfiguration> {
3852
return Promise.all([this.manager.update(configuration), this.updateIndex()])
3953
.then(([result]) => result);

0 commit comments

Comments
 (0)