@valkyrjaio/sindri 26.4.0 → 26.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@valkyrjaio/sindri",
3
- "version": "26.4.0",
3
+ "version": "26.5.0",
4
4
  "description": "The Valkyrja Sindri build tool.",
5
5
  "homepage": "https://www.valkyrja.io/sindri",
6
6
  "keywords": [
@@ -38,7 +38,7 @@
38
38
  "node": ">=22"
39
39
  },
40
40
  "dependencies": {
41
- "@valkyrjaio/valkyrja": "^26.4.0",
41
+ "@valkyrjaio/valkyrja": "^26.4.1",
42
42
  "ts-morph": "^28.0.0"
43
43
  },
44
44
  "scripts": {
@@ -0,0 +1,356 @@
1
+ /*
2
+ * This file is part of the Sindri package.
3
+ *
4
+ * (c) Melech Mizrachi <melechmizrachi@gmail.com>
5
+ *
6
+ * For the full copyright and license information, please view the LICENSE
7
+ * file that was distributed with this source code.
8
+ */
9
+
10
+ import { Node } from 'ts-morph';
11
+
12
+ import { AstReader } from './Abstract/AstReader.ts';
13
+ import { ConfigImport } from './Data/ConfigImport.ts';
14
+ import { ConfigSourceResult } from './Data/Result/ConfigSourceResult.ts';
15
+
16
+ import type { ClassDeclaration, SourceFile } from 'ts-morph';
17
+
18
+ import type { ConfigSourceReaderContract } from './Contract/ConfigSourceReaderContract.ts';
19
+
20
+ /**
21
+ * Reads an application config class and returns every field as source text.
22
+ *
23
+ * Two shapes are supported, because an application config may either extend a
24
+ * framework config and pass positional `super()` arguments, or implement the
25
+ * contract directly and declare its own properties:
26
+ *
27
+ * ```ts
28
+ * class Config extends HttpConfig { constructor() { super('App', process.cwd(), ...); } }
29
+ * class Config implements HttpConfigContract { readonly namespace = 'App'; }
30
+ * ```
31
+ *
32
+ * Values are never evaluated. `process.env['APP_KEY']` is carried through as the
33
+ * text `process.env['APP_KEY']`, so a generated config keeps whatever the author
34
+ * wrote — including expressions no static reader could resolve.
35
+ */
36
+ export class ConfigSourceReader extends AstReader implements ConfigSourceReaderContract {
37
+ /**
38
+ * The constructor parameter order of each framework config base.
39
+ *
40
+ * Positional `super()` arguments carry no names, so the base class a config
41
+ * extends is what gives each argument its meaning. `CliConfig` inserts two
42
+ * fields ahead of `providers`, so the orders are not interchangeable.
43
+ */
44
+ protected static readonly BASE_FIELDS: Readonly<Record<string, readonly string[]>> = {
45
+ Config: [
46
+ 'namespace',
47
+ 'dir',
48
+ 'version',
49
+ 'environment',
50
+ 'debugMode',
51
+ 'timezone',
52
+ 'key',
53
+ 'dataPath',
54
+ 'dataNamespace',
55
+ 'providers',
56
+ 'callbacks',
57
+ ],
58
+ HttpConfig: [
59
+ 'namespace',
60
+ 'dir',
61
+ 'version',
62
+ 'environment',
63
+ 'debugMode',
64
+ 'timezone',
65
+ 'key',
66
+ 'dataPath',
67
+ 'dataNamespace',
68
+ 'providers',
69
+ 'callbacks',
70
+ 'requestReceivedMiddleware',
71
+ 'routeMatchedMiddleware',
72
+ 'routeNotMatchedMiddleware',
73
+ 'routeDispatchedMiddleware',
74
+ 'throwableCaughtMiddleware',
75
+ 'sendingResponseMiddleware',
76
+ 'responseSentMiddleware',
77
+ ],
78
+ CliConfig: [
79
+ 'namespace',
80
+ 'dir',
81
+ 'version',
82
+ 'environment',
83
+ 'debugMode',
84
+ 'timezone',
85
+ 'key',
86
+ 'dataPath',
87
+ 'dataNamespace',
88
+ 'applicationName',
89
+ 'defaultCommandName',
90
+ 'providers',
91
+ 'callbacks',
92
+ 'inputReceivedMiddleware',
93
+ 'routeMatchedMiddleware',
94
+ 'routeNotMatchedMiddleware',
95
+ 'routeDispatchedMiddleware',
96
+ 'throwableCaughtMiddleware',
97
+ 'processExitingMiddleware',
98
+ ],
99
+ };
100
+
101
+ /** The contract each framework config base satisfies. */
102
+ protected static readonly BASE_CONTRACTS: Readonly<Record<string, string>> = {
103
+ Config: 'ConfigContract',
104
+ HttpConfig: 'HttpConfigContract',
105
+ CliConfig: 'CliConfigContract',
106
+ };
107
+
108
+ /** Where each contract is imported from. */
109
+ protected static readonly CONTRACT_SPECIFIERS: Readonly<Record<string, string>> = {
110
+ ConfigContract: '@valkyrjaio/valkyrja/Application/Data/Contract/ConfigContract.ts',
111
+ HttpConfigContract: '@valkyrjaio/valkyrja/Application/Data/Contract/HttpConfigContract.ts',
112
+ CliConfigContract: '@valkyrjaio/valkyrja/Application/Data/Contract/CliConfigContract.ts',
113
+ };
114
+
115
+ readFile(filePath: string): ConfigSourceResult {
116
+ const context = this.parseClassFile(filePath);
117
+
118
+ if (context === undefined) {
119
+ return new ConfigSourceResult();
120
+ }
121
+
122
+ const { sourceFile, classDecl } = context;
123
+ const className = classDecl.getName() ?? '';
124
+
125
+ if (className === '') {
126
+ return new ConfigSourceResult();
127
+ }
128
+
129
+ const contract = this.resolveContract(classDecl);
130
+
131
+ if (contract === undefined) {
132
+ return new ConfigSourceResult();
133
+ }
134
+
135
+ const base = this.readBase(classDecl, context.useMap, filePath);
136
+ const fields = { ...base.fields, ...this.readFields(classDecl) };
137
+ const types = { ...base.types, ...this.readPropertyTypes(classDecl) };
138
+
139
+ if (Object.keys(fields).length === 0) {
140
+ return new ConfigSourceResult();
141
+ }
142
+
143
+ return new ConfigSourceResult(className, contract.name, contract.specifier, fields, types, [
144
+ ...this.readImports(sourceFile),
145
+ ...base.imports,
146
+ ]);
147
+ }
148
+
149
+ /**
150
+ * Read the defaults of the framework config a class extends.
151
+ *
152
+ * A config passes only the arguments it wants to change, so the rest come
153
+ * from the base's constructor parameter defaults. A generated config
154
+ * implements the contract directly and inherits nothing, so those defaults
155
+ * must be copied in or they are silently lost — `CliConfig`, for one,
156
+ * defaults several middleware lists to non-empty values.
157
+ */
158
+ protected readBase(
159
+ classDecl: ClassDeclaration,
160
+ useMap: Record<string, string>,
161
+ filePath: string,
162
+ ): { fields: Record<string, string>; types: Record<string, string>; imports: ConfigImport[] } {
163
+ const baseName = classDecl.getExtends()?.getExpression().getText() ?? '';
164
+
165
+ if (ConfigSourceReader.BASE_FIELDS[baseName] === undefined) {
166
+ return { fields: {}, types: {}, imports: [] };
167
+ }
168
+
169
+ const baseFile = this.parseDeclaringSourceFile(baseName, useMap, filePath);
170
+ const baseClass = baseFile?.getClass(baseName);
171
+
172
+ if (baseFile === undefined || baseClass === undefined) {
173
+ return { fields: {}, types: {}, imports: [] };
174
+ }
175
+
176
+ const fields: Record<string, string> = {};
177
+ const types: Record<string, string> = {};
178
+
179
+ for (const parameter of baseClass.getConstructors()[0]?.getParameters() ?? []) {
180
+ const initializer = parameter.getInitializer();
181
+
182
+ if (initializer === undefined) {
183
+ continue;
184
+ }
185
+
186
+ fields[parameter.getName()] = initializer.getText();
187
+
188
+ const typeNode = parameter.getTypeNode();
189
+
190
+ if (typeNode !== undefined) {
191
+ types[parameter.getName()] = typeNode.getText();
192
+ }
193
+ }
194
+
195
+ return { fields, types, imports: this.readImports(baseFile) };
196
+ }
197
+
198
+ /**
199
+ * Determine which config contract the class satisfies, and where that
200
+ * contract is imported from.
201
+ *
202
+ * A class that extends a framework config takes that base's contract; a
203
+ * standalone class names its contract in the `implements` clause. Returns
204
+ * undefined when neither names a contract this reader knows.
205
+ */
206
+ protected resolveContract(classDecl: ClassDeclaration): { name: string; specifier: string } | undefined {
207
+ const baseName = classDecl.getExtends()?.getExpression().getText() ?? '';
208
+ const names = [ConfigSourceReader.BASE_CONTRACTS[baseName]];
209
+
210
+ for (const implemented of classDecl.getImplements()) {
211
+ names.push(implemented.getExpression().getText());
212
+ }
213
+
214
+ for (const name of names) {
215
+ const specifier = name === undefined ? undefined : ConfigSourceReader.CONTRACT_SPECIFIERS[name];
216
+
217
+ if (name !== undefined && specifier !== undefined) {
218
+ return { name, specifier };
219
+ }
220
+ }
221
+
222
+ return undefined;
223
+ }
224
+
225
+ /**
226
+ * Read every field initializer as source text, from whichever shape the
227
+ * class uses.
228
+ */
229
+ protected readFields(classDecl: ClassDeclaration): Record<string, string> {
230
+ const baseName = classDecl.getExtends()?.getExpression().getText() ?? '';
231
+ const order = ConfigSourceReader.BASE_FIELDS[baseName];
232
+
233
+ if (order !== undefined) {
234
+ return this.readFieldsFromSuperCall(classDecl, order);
235
+ }
236
+
237
+ return this.readFieldsFromProperties(classDecl);
238
+ }
239
+
240
+ /**
241
+ * Map positional `super()` arguments onto the base's field order.
242
+ *
243
+ * Arguments the author omitted keep the base class default, so they are left
244
+ * out of the result rather than recorded as empty.
245
+ */
246
+ protected readFieldsFromSuperCall(classDecl: ClassDeclaration, order: readonly string[]): Record<string, string> {
247
+ const fields: Record<string, string> = {};
248
+ const args = this.findSuperCallArguments(classDecl);
249
+
250
+ for (const [index, arg] of args.entries()) {
251
+ const name = order[index];
252
+
253
+ if (name === undefined) {
254
+ continue;
255
+ }
256
+
257
+ fields[name] = arg;
258
+ }
259
+
260
+ return fields;
261
+ }
262
+
263
+ /**
264
+ * Collect the argument source texts of the constructor's `super()` call.
265
+ */
266
+ protected findSuperCallArguments(classDecl: ClassDeclaration): string[] {
267
+ const constructor = classDecl.getConstructors()[0];
268
+
269
+ if (constructor === undefined) {
270
+ return [];
271
+ }
272
+
273
+ for (const statement of constructor.getStatements()) {
274
+ if (!Node.isExpressionStatement(statement)) {
275
+ continue;
276
+ }
277
+
278
+ const expression = statement.getExpression();
279
+
280
+ if (!Node.isCallExpression(expression) || expression.getExpression().getKindName() !== 'SuperKeyword') {
281
+ continue;
282
+ }
283
+
284
+ return expression.getArguments().map((arg) => arg.getText());
285
+ }
286
+
287
+ return [];
288
+ }
289
+
290
+ /**
291
+ * Read initializers from a standalone class's property declarations.
292
+ */
293
+ protected readFieldsFromProperties(classDecl: ClassDeclaration): Record<string, string> {
294
+ const fields: Record<string, string> = {};
295
+
296
+ for (const property of classDecl.getProperties()) {
297
+ if (property.isStatic()) {
298
+ continue;
299
+ }
300
+
301
+ const initializer = property.getInitializer();
302
+
303
+ if (initializer === undefined) {
304
+ continue;
305
+ }
306
+
307
+ fields[property.getName()] = initializer.getText();
308
+ }
309
+
310
+ return fields;
311
+ }
312
+
313
+ /**
314
+ * Read the declared types of a standalone class's own properties.
315
+ */
316
+ protected readPropertyTypes(classDecl: ClassDeclaration): Record<string, string> {
317
+ const types: Record<string, string> = {};
318
+
319
+ for (const property of classDecl.getProperties()) {
320
+ const typeNode = property.getTypeNode();
321
+
322
+ if (!property.isStatic() && typeNode !== undefined) {
323
+ types[property.getName()] = typeNode.getText();
324
+ }
325
+ }
326
+
327
+ return types;
328
+ }
329
+
330
+ /**
331
+ * Collect every named import in the config source file.
332
+ *
333
+ * Default and namespace imports are skipped: a config field can only
334
+ * reference a named binding, so nothing else can appear in the copied text.
335
+ */
336
+ protected readImports(sourceFile: SourceFile): ConfigImport[] {
337
+ const imports: ConfigImport[] = [];
338
+ const ownerPath = sourceFile.getFilePath();
339
+
340
+ for (const declaration of sourceFile.getImportDeclarations()) {
341
+ const specifier = declaration.getModuleSpecifierValue();
342
+ const isTypeOnly = declaration.isTypeOnly();
343
+ const resolvedPath = specifier.startsWith('.')
344
+ ? this.resolveModuleSpecifierToFilePath(specifier, ownerPath)
345
+ : '';
346
+
347
+ for (const named of declaration.getNamedImports()) {
348
+ imports.push(
349
+ new ConfigImport(named.getName(), specifier, isTypeOnly || named.isTypeOnly(), resolvedPath),
350
+ );
351
+ }
352
+ }
353
+
354
+ return imports;
355
+ }
356
+ }
@@ -0,0 +1,14 @@
1
+ /*
2
+ * This file is part of the Sindri package.
3
+ *
4
+ * (c) Melech Mizrachi <melechmizrachi@gmail.com>
5
+ *
6
+ * For the full copyright and license information, please view the LICENSE
7
+ * file that was distributed with this source code.
8
+ */
9
+
10
+ import type { ConfigSourceResult } from '../Data/Result/ConfigSourceResult.ts';
11
+
12
+ export interface ConfigSourceReaderContract {
13
+ readFile(filePath: string): ConfigSourceResult;
14
+ }
@@ -0,0 +1,26 @@
1
+ /*
2
+ * This file is part of the Sindri package.
3
+ *
4
+ * (c) Melech Mizrachi <melechmizrachi@gmail.com>
5
+ *
6
+ * For the full copyright and license information, please view the LICENSE
7
+ * file that was distributed with this source code.
8
+ */
9
+
10
+ /**
11
+ * A single named import read from a config source file.
12
+ *
13
+ * A relative specifier is only meaningful next to the file that declares it.
14
+ * Imports copied out of a framework config base therefore carry `resolvedPath`,
15
+ * the absolute path the specifier points at, so the generate layer can rewrite
16
+ * the specifier for wherever the generated file goes. An import that already
17
+ * uses a bare package specifier needs no rewrite and leaves `resolvedPath` empty.
18
+ */
19
+ export class ConfigImport {
20
+ constructor(
21
+ readonly name: string,
22
+ readonly specifier: string,
23
+ readonly isType: boolean = false,
24
+ readonly resolvedPath: string = '',
25
+ ) {}
26
+ }
@@ -0,0 +1,28 @@
1
+ /*
2
+ * This file is part of the Sindri package.
3
+ *
4
+ * (c) Melech Mizrachi <melechmizrachi@gmail.com>
5
+ *
6
+ * For the full copyright and license information, please view the LICENSE
7
+ * file that was distributed with this source code.
8
+ */
9
+
10
+ import type { ConfigImport } from '../ConfigImport.ts';
11
+
12
+ /**
13
+ * The source-level shape of an application config class.
14
+ *
15
+ * Field initializers are kept as source text rather than evaluated values, so a
16
+ * generated config reproduces expressions such as `process.env['APP_KEY']` or
17
+ * `process.cwd()` exactly as the author wrote them.
18
+ */
19
+ export class ConfigSourceResult {
20
+ constructor(
21
+ readonly className: string = '',
22
+ readonly contractName: string = '',
23
+ readonly contractSpecifier: string = '',
24
+ readonly fields: Readonly<Record<string, string>> = {},
25
+ readonly types: Readonly<Record<string, string>> = {},
26
+ readonly imports: readonly ConfigImport[] = [],
27
+ ) {}
28
+ }
@@ -0,0 +1,47 @@
1
+ /*
2
+ * This file is part of the Sindri package.
3
+ *
4
+ * (c) Melech Mizrachi <melechmizrachi@gmail.com>
5
+ *
6
+ * For the full copyright and license information, please view the LICENSE
7
+ * file that was distributed with this source code.
8
+ */
9
+
10
+ import { ConfigReader } from '../../Ast/ConfigReader.ts';
11
+ import { ConfigSourceReader } from '../../Ast/ConfigSourceReader.ts';
12
+ import { GenerateCachedConfigFromAst } from '../../Generate/Abstract/GenerateCachedConfigFromAst.ts';
13
+ import { AstCachedConfigFileGenerator } from '../../Generator/Ast/Config/AstCachedConfigFileGenerator.ts';
14
+
15
+ import type { ConfigReaderContract } from '../../Ast/Contract/ConfigReaderContract.ts';
16
+ import type { ConfigSourceReaderContract } from '../../Ast/Contract/ConfigSourceReaderContract.ts';
17
+ import type { CachedConfigFileGeneratorContract } from '../../Generator/Config/Contract/CachedConfigFileGeneratorContract.ts';
18
+ import type { OutputContract } from '@valkyrjaio/valkyrja/Cli/Interaction/Output/Contract/OutputContract.ts';
19
+ import type { OutputFactoryContract } from '@valkyrjaio/valkyrja/Cli/Interaction/Output/Factory/Contract/OutputFactoryContract.ts';
20
+ import type { RouteContract } from '@valkyrjaio/valkyrja/Cli/Routing/Data/Contract/RouteContract.ts';
21
+
22
+ export class GenerateCachedConfigCommand extends GenerateCachedConfigFromAst {
23
+ public constructor(
24
+ protected override readonly route: RouteContract,
25
+ outputFactory: OutputFactoryContract,
26
+ configReader: ConfigReaderContract = new ConfigReader(),
27
+ configSourceReader: ConfigSourceReaderContract = new ConfigSourceReader(),
28
+ cachedConfigGenerator: CachedConfigFileGeneratorContract = new AstCachedConfigFileGenerator(),
29
+ ) {
30
+ super(
31
+ outputFactory,
32
+ route,
33
+ 'Generating Cached Config From Config',
34
+ configReader,
35
+ configSourceReader,
36
+ cachedConfigGenerator,
37
+ );
38
+ }
39
+
40
+ public run(): OutputContract {
41
+ return this.generateCachedConfig();
42
+ }
43
+
44
+ protected override getConfigFilePath(): string {
45
+ return this.route.getArgument('config').getArguments()[0]?.getValue() ?? '';
46
+ }
47
+ }
@@ -9,4 +9,5 @@
9
9
 
10
10
  export class CommandName {
11
11
  static readonly DATA_GENERATE = 'data:generate' as const;
12
+ static readonly CONFIG_GENERATE = 'config:generate' as const;
12
13
  }
@@ -8,8 +8,8 @@
8
8
  */
9
9
 
10
10
  export class SindriInfo {
11
- static readonly VERSION = '26.4.0' as const;
12
- static readonly VERSION_BUILD_DATE_TIME = 'July 27 2026 22:16:03 MST' as const;
11
+ static readonly VERSION = '26.5.0' as const;
12
+ static readonly VERSION_BUILD_DATE_TIME = 'July 31 2026 08:59:24 MST' as const;
13
13
  static readonly ICON = `\
14
14
  ██████████
15
15
  ████ ████
@@ -0,0 +1,159 @@
1
+ /*
2
+ * This file is part of the Sindri package.
3
+ *
4
+ * (c) Melech Mizrachi <melechmizrachi@gmail.com>
5
+ *
6
+ * For the full copyright and license information, please view the LICENSE
7
+ * file that was distributed with this source code.
8
+ */
9
+
10
+ import * as path from 'path';
11
+
12
+ import { ErrorFormatter } from '@valkyrjaio/valkyrja/Cli/Interaction/Formatter/ErrorFormatter.ts';
13
+ import { HighlightedTextFormatter } from '@valkyrjaio/valkyrja/Cli/Interaction/Formatter/HighlightedTextFormatter.ts';
14
+ import { SuccessFormatter } from '@valkyrjaio/valkyrja/Cli/Interaction/Formatter/SuccessFormatter.ts';
15
+ import { WarningFormatter } from '@valkyrjaio/valkyrja/Cli/Interaction/Formatter/WarningFormatter.ts';
16
+ import { Header } from '@valkyrjaio/valkyrja/Cli/Interaction/Message/Header.ts';
17
+ import { Message } from '@valkyrjaio/valkyrja/Cli/Interaction/Message/Message.ts';
18
+ import { NewLine } from '@valkyrjaio/valkyrja/Cli/Interaction/Message/NewLine.ts';
19
+
20
+ import { ConfigReader } from '../../Ast/ConfigReader.ts';
21
+ import { ConfigSourceReader } from '../../Ast/ConfigSourceReader.ts';
22
+ import { ConfigImport } from '../../Ast/Data/ConfigImport.ts';
23
+ import { ConfigSourceResult } from '../../Ast/Data/Result/ConfigSourceResult.ts';
24
+ import { SindriInfo } from '../../Constant/SindriInfo.ts';
25
+ import { AstCachedConfigFileGenerator } from '../../Generator/Ast/Config/AstCachedConfigFileGenerator.ts';
26
+ import { GenerateStatus } from '../../Generator/Enum/GenerateStatus.ts';
27
+ import { GenerateFromAst } from './GenerateFromAst.ts';
28
+
29
+ import type { ConfigReaderContract } from '../../Ast/Contract/ConfigReaderContract.ts';
30
+ import type { ConfigSourceReaderContract } from '../../Ast/Contract/ConfigSourceReaderContract.ts';
31
+ import type { CachedConfigFileGeneratorContract } from '../../Generator/Config/Contract/CachedConfigFileGeneratorContract.ts';
32
+ import type { OutputContract } from '@valkyrjaio/valkyrja/Cli/Interaction/Output/Contract/OutputContract.ts';
33
+ import type { OutputFactoryContract } from '@valkyrjaio/valkyrja/Cli/Interaction/Output/Factory/Contract/OutputFactoryContract.ts';
34
+ import type { RouteContract } from '@valkyrjaio/valkyrja/Cli/Routing/Data/Contract/RouteContract.ts';
35
+
36
+ /**
37
+ * Writes a cached config beside an application config.
38
+ *
39
+ * This is an opt-in command, separate from data generation. An application only
40
+ * needs a cached config when it starts often enough for module loading to
41
+ * matter — a CGI or lambda deployment, a CLI command, or a cron job. A worker
42
+ * process pays that cost one time and gains nothing.
43
+ */
44
+ export abstract class GenerateCachedConfigFromAst extends GenerateFromAst {
45
+ /** The container data class the generated config publishes. */
46
+ protected static readonly CONTAINER_DATA_CLASS = 'AppContainerData';
47
+
48
+ /** The name the generated config file takes. */
49
+ protected static readonly GENERATED_CLASS = 'CachedConfig';
50
+
51
+ public constructor(
52
+ protected readonly outputFactory: OutputFactoryContract,
53
+ protected readonly route: RouteContract,
54
+ protected readonly title: string = 'Generating Cached Config From Config',
55
+ protected readonly configReader: ConfigReaderContract = new ConfigReader(),
56
+ protected readonly configSourceReader: ConfigSourceReaderContract = new ConfigSourceReader(),
57
+ protected readonly cachedConfigGenerator: CachedConfigFileGeneratorContract = new AstCachedConfigFileGenerator(),
58
+ ) {
59
+ super();
60
+ }
61
+
62
+ protected generateCachedConfig(): OutputContract {
63
+ let output = this.getOutput();
64
+
65
+ const configFilePath = this.getConfigFilePath();
66
+ const config = this.configReader.readFile(configFilePath);
67
+ const source = this.configSourceReader.readFile(configFilePath);
68
+
69
+ output = output
70
+ .withAddedMessages(new Message('Generating Cached Config.......................'))
71
+ .writeMessages();
72
+
73
+ if (source.className === '' || config.dataPath === '') {
74
+ return this.addMessagesForGenerateStatus(output, GenerateStatus.FAILURE)
75
+ .withAddedMessages(new NewLine())
76
+ .writeMessages();
77
+ }
78
+
79
+ const outputDirectory = path.dirname(configFilePath);
80
+ const status = this.cachedConfigGenerator.generateFile(
81
+ outputDirectory,
82
+ GenerateCachedConfigFromAst.GENERATED_CLASS,
83
+ this.withResolvedImports(source, outputDirectory),
84
+ GenerateCachedConfigFromAst.CONTAINER_DATA_CLASS,
85
+ this.importSpecifier(
86
+ outputDirectory,
87
+ path.join(config.dataPath, `${GenerateCachedConfigFromAst.CONTAINER_DATA_CLASS}.ts`),
88
+ ),
89
+ );
90
+
91
+ return this.addMessagesForGenerateStatus(output, status)
92
+ .withAddedMessages(new NewLine())
93
+ .writeMessages()
94
+ .withAddedMessages(new NewLine());
95
+ }
96
+
97
+ /**
98
+ * Rewrite each copied import for the generated file's location.
99
+ *
100
+ * A relative specifier only means something next to the file that declared
101
+ * it. An import copied out of a framework config base points at the
102
+ * framework directory, so it must be written again as the package specifier
103
+ * or as a path relative to the output directory.
104
+ */
105
+ protected withResolvedImports(source: ConfigSourceResult, outputDirectory: string): ConfigSourceResult {
106
+ const imports = source.imports.map((entry) =>
107
+ entry.resolvedPath === ''
108
+ ? entry
109
+ : new ConfigImport(
110
+ entry.name,
111
+ this.importSpecifier(outputDirectory, entry.resolvedPath),
112
+ entry.isType,
113
+ entry.resolvedPath,
114
+ ),
115
+ );
116
+
117
+ return new ConfigSourceResult(
118
+ source.className,
119
+ source.contractName,
120
+ source.contractSpecifier,
121
+ source.fields,
122
+ source.types,
123
+ imports,
124
+ );
125
+ }
126
+
127
+ protected getOutput(): OutputContract {
128
+ return this.outputFactory
129
+ .createOutput()
130
+ .withAddedMessages(
131
+ new Header('Sindri', SindriInfo.VERSION, this.route, SindriInfo.ICON),
132
+ new NewLine(),
133
+ new NewLine(),
134
+ new Message(`${this.title}:`, new HighlightedTextFormatter()),
135
+ new NewLine(),
136
+ new NewLine(),
137
+ )
138
+ .writeMessages();
139
+ }
140
+
141
+ protected addMessagesForGenerateStatus(output: OutputContract, status: GenerateStatus): OutputContract {
142
+ let text = 'Failed';
143
+ let formatter = new ErrorFormatter();
144
+
145
+ if (status === GenerateStatus.SUCCESS) {
146
+ text = 'Success';
147
+ formatter = new SuccessFormatter();
148
+ }
149
+
150
+ if (status === GenerateStatus.SKIPPED) {
151
+ text = 'Skipped';
152
+ formatter = new WarningFormatter();
153
+ }
154
+
155
+ return output.withAddedMessages(new Message(text, formatter), new NewLine());
156
+ }
157
+
158
+ protected abstract getConfigFilePath(): string;
159
+ }