@config-bound/cli 0.1.0 → 0.2.1
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/CHANGELOG.md +24 -0
- package/LICENSE +21 -0
- package/package.json +29 -23
- package/scripts/generate-docs.ts +362 -0
- package/src/cli.module.ts +7 -1
- package/src/commands/generate-bind.command.ts +175 -0
- package/src/commands/generate.command.ts +13 -0
- package/src/services/bind-generator.service.ts +248 -0
- package/src/services/config-discovery.service.spec.ts +10 -10
- package/src/services/config-discovery.service.ts +26 -41
- package/src/services/config-loader.service.ts +8 -16
- package/src/services/schema-export.service.spec.ts +20 -22
- package/src/services/schema-export.service.ts +6 -11
- package/tsconfig.json +7 -2
- package/.turbo/turbo-build.log +0 -4
- package/.turbo/turbo-format$colon$ci.log +0 -6
- package/.turbo/turbo-lint$colon$ci.log +0 -4
- package/.turbo/turbo-test.log +0 -19
- package/dist/cli.module.d.ts +0 -3
- package/dist/cli.module.d.ts.map +0 -1
- package/dist/cli.module.js +0 -29
- package/dist/cli.module.js.map +0 -1
- package/dist/commands/export.command.d.ts +0 -30
- package/dist/commands/export.command.d.ts.map +0 -1
- package/dist/commands/export.command.js +0 -226
- package/dist/commands/export.command.js.map +0 -1
- package/dist/commands/list.command.d.ts +0 -13
- package/dist/commands/list.command.d.ts.map +0 -1
- package/dist/commands/list.command.js +0 -93
- package/dist/commands/list.command.js.map +0 -1
- package/dist/main.d.ts +0 -3
- package/dist/main.d.ts.map +0 -1
- package/dist/main.js +0 -17
- package/dist/main.js.map +0 -1
- package/dist/services/config-discovery.service.d.ts +0 -15
- package/dist/services/config-discovery.service.d.ts.map +0 -1
- package/dist/services/config-discovery.service.js +0 -191
- package/dist/services/config-discovery.service.js.map +0 -1
- package/dist/services/config-discovery.service.spec.d.ts +0 -2
- package/dist/services/config-discovery.service.spec.d.ts.map +0 -1
- package/dist/services/config-discovery.service.spec.js +0 -137
- package/dist/services/config-discovery.service.spec.js.map +0 -1
- package/dist/services/config-loader.service.d.ts +0 -13
- package/dist/services/config-loader.service.d.ts.map +0 -1
- package/dist/services/config-loader.service.js +0 -241
- package/dist/services/config-loader.service.js.map +0 -1
- package/dist/services/file-writer.service.d.ts +0 -6
- package/dist/services/file-writer.service.d.ts.map +0 -1
- package/dist/services/file-writer.service.js +0 -38
- package/dist/services/file-writer.service.js.map +0 -1
- package/dist/services/file-writer.service.spec.d.ts +0 -2
- package/dist/services/file-writer.service.spec.d.ts.map +0 -1
- package/dist/services/file-writer.service.spec.js +0 -98
- package/dist/services/file-writer.service.spec.js.map +0 -1
- package/dist/services/schema-export.service.d.ts +0 -14
- package/dist/services/schema-export.service.d.ts.map +0 -1
- package/dist/services/schema-export.service.js +0 -58
- package/dist/services/schema-export.service.js.map +0 -1
- package/dist/services/schema-export.service.spec.d.ts +0 -2
- package/dist/services/schema-export.service.spec.d.ts.map +0 -1
- package/dist/services/schema-export.service.spec.js +0 -69
- package/dist/services/schema-export.service.spec.js.map +0 -1
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { Command, CommandRunner, Option } from 'nest-commander';
|
|
2
|
+
import {
|
|
3
|
+
BindGeneratorService,
|
|
4
|
+
type BindGeneratorMode,
|
|
5
|
+
type BindNames
|
|
6
|
+
} from '../services/bind-generator.service.js';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import * as readline from 'readline';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
|
|
11
|
+
interface GenerateBindCommandOptions {
|
|
12
|
+
output?: string;
|
|
13
|
+
type?: BindGeneratorMode;
|
|
14
|
+
dryRun?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@Command({
|
|
18
|
+
name: 'bind',
|
|
19
|
+
description: 'Scaffold a new bind',
|
|
20
|
+
arguments: '<name>',
|
|
21
|
+
argsDescription: {
|
|
22
|
+
name: 'The bind name in kebab-case (e.g. 1password, vault, aws-ssm)'
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
export class GenerateBindCommand extends CommandRunner {
|
|
26
|
+
constructor(private readonly generatorService: BindGeneratorService) {
|
|
27
|
+
super();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async run(
|
|
31
|
+
[name]: string[],
|
|
32
|
+
options?: GenerateBindCommandOptions
|
|
33
|
+
): Promise<void> {
|
|
34
|
+
if (!name) {
|
|
35
|
+
this.command.error(
|
|
36
|
+
'bind name is required.\nUsage: configbound generate bind <name>'
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const names = this.generatorService.deriveNames(name);
|
|
41
|
+
const outputDir = options?.output
|
|
42
|
+
? path.resolve(options.output)
|
|
43
|
+
: process.cwd();
|
|
44
|
+
|
|
45
|
+
const mode: BindGeneratorMode = options?.type ?? (await this.promptMode());
|
|
46
|
+
|
|
47
|
+
const files = this.generatorService.renderFiles(names, mode);
|
|
48
|
+
|
|
49
|
+
if (options?.dryRun) {
|
|
50
|
+
console.log(chalk.blue('\nFiles that would be generated:\n'));
|
|
51
|
+
for (const file of files) {
|
|
52
|
+
const fullPath = path.join(outputDir, file.relativePath);
|
|
53
|
+
console.log(chalk.cyan(` ${fullPath}`));
|
|
54
|
+
console.log(chalk.gray(' ' + '─'.repeat(60)));
|
|
55
|
+
console.log(
|
|
56
|
+
file.content
|
|
57
|
+
.split('\n')
|
|
58
|
+
.map((line) => chalk.gray(` ${line}`))
|
|
59
|
+
.join('\n')
|
|
60
|
+
);
|
|
61
|
+
console.log();
|
|
62
|
+
}
|
|
63
|
+
console.log(chalk.yellow('Dry run — no files written.'));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.log(
|
|
68
|
+
chalk.blue(
|
|
69
|
+
`\nGenerating ${mode === 'package' ? 'community bind package' : 'embedded bind class'} for ${chalk.bold(names.pascal)}...\n`
|
|
70
|
+
)
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
this.generatorService.writeFiles(files, outputDir);
|
|
74
|
+
|
|
75
|
+
console.log(chalk.green('Done! Generated files:'));
|
|
76
|
+
for (const file of files) {
|
|
77
|
+
console.log(chalk.cyan(` ${path.join(outputDir, file.relativePath)}`));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
this.printNextSteps(names, mode, outputDir);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private async promptMode(): Promise<BindGeneratorMode> {
|
|
84
|
+
console.log(chalk.blue('\nWhat would you like to generate?\n'));
|
|
85
|
+
console.log(
|
|
86
|
+
` ${chalk.cyan('1.')} Community bind package ${chalk.gray('(package.json, tsconfig, src/) — standalone, publishable to npm')}`
|
|
87
|
+
);
|
|
88
|
+
console.log(
|
|
89
|
+
` ${chalk.cyan('2.')} Embedded bind class file ${chalk.gray('— lives in your project, not published')}`
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
while (true) {
|
|
93
|
+
const answer = await new Promise<string>((resolve) => {
|
|
94
|
+
const rl = readline.createInterface({
|
|
95
|
+
input: process.stdin,
|
|
96
|
+
output: process.stdout
|
|
97
|
+
});
|
|
98
|
+
rl.question(chalk.yellow('\nSelect (1 or 2): '), (ans) => {
|
|
99
|
+
rl.close();
|
|
100
|
+
resolve(ans.trim());
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
if (answer === '1') return 'package';
|
|
105
|
+
if (answer === '2') return 'embedded';
|
|
106
|
+
|
|
107
|
+
console.log(chalk.red(' Invalid selection. Please enter 1 or 2.'));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private printNextSteps(
|
|
112
|
+
names: BindNames,
|
|
113
|
+
mode: BindGeneratorMode,
|
|
114
|
+
outputDir: string
|
|
115
|
+
): void {
|
|
116
|
+
console.log(chalk.blue('\nNext steps:\n'));
|
|
117
|
+
|
|
118
|
+
if (mode === 'package') {
|
|
119
|
+
const packageDir = path.join(outputDir, `bind-${names.kebab}`);
|
|
120
|
+
console.log(
|
|
121
|
+
` ${chalk.cyan('1.')} Add your SDK dependency to ${chalk.bold(path.join(packageDir, 'package.json'))}`
|
|
122
|
+
);
|
|
123
|
+
console.log(
|
|
124
|
+
` ${chalk.cyan('2.')} Fill in the ${chalk.bold(`create()`)} method in ${chalk.bold(path.join(packageDir, 'src', `${names.pascal}Bind.ts`))}`
|
|
125
|
+
);
|
|
126
|
+
console.log(
|
|
127
|
+
` ${chalk.cyan('3.')} Run ${chalk.bold('npm install')} in the package directory`
|
|
128
|
+
);
|
|
129
|
+
console.log(
|
|
130
|
+
` ${chalk.cyan('4.')} When ready to publish, set ${chalk.bold('"private": false')} in package.json`
|
|
131
|
+
);
|
|
132
|
+
} else {
|
|
133
|
+
const classFile = path.join(outputDir, `${names.pascal}Bind.ts`);
|
|
134
|
+
console.log(
|
|
135
|
+
` ${chalk.cyan('1.')} Add your SDK to your project's dependencies`
|
|
136
|
+
);
|
|
137
|
+
console.log(
|
|
138
|
+
` ${chalk.cyan('2.')} Fill in the ${chalk.bold(`create()`)} method in ${chalk.bold(classFile)}`
|
|
139
|
+
);
|
|
140
|
+
console.log(
|
|
141
|
+
` ${chalk.cyan('3.')} Pass an instance to ${chalk.bold('ConfigBound.createConfig(..., { binds: [bind] })')}`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
console.log();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@Option({
|
|
149
|
+
flags: '-o, --output <dir>',
|
|
150
|
+
description: 'Output directory (default: current directory)'
|
|
151
|
+
})
|
|
152
|
+
parseOutput(val: string): string {
|
|
153
|
+
return val;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
@Option({
|
|
157
|
+
flags: '--type <type>',
|
|
158
|
+
description:
|
|
159
|
+
'Generation mode: package or embedded (skips interactive prompt)'
|
|
160
|
+
})
|
|
161
|
+
parseType(val: string): BindGeneratorMode {
|
|
162
|
+
if (val !== 'package' && val !== 'embedded') {
|
|
163
|
+
throw new Error(`Invalid type: ${val}. Must be "package" or "embedded".`);
|
|
164
|
+
}
|
|
165
|
+
return val;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
@Option({
|
|
169
|
+
flags: '--dry-run',
|
|
170
|
+
description: 'Preview generated files without writing'
|
|
171
|
+
})
|
|
172
|
+
parseDryRun(): boolean {
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Command, CommandRunner } from 'nest-commander';
|
|
2
|
+
import { GenerateBindCommand } from './generate-bind.command.js';
|
|
3
|
+
|
|
4
|
+
@Command({
|
|
5
|
+
name: 'generate',
|
|
6
|
+
description: 'Generate new ConfigBound components',
|
|
7
|
+
subCommands: [GenerateBindCommand]
|
|
8
|
+
})
|
|
9
|
+
export class GenerateCommand extends CommandRunner {
|
|
10
|
+
async run(): Promise<void> {
|
|
11
|
+
this.command.help();
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { FileWriterService } from './file-writer.service.js';
|
|
4
|
+
|
|
5
|
+
const DIGIT_WORDS = new Map([
|
|
6
|
+
['1', 'One'],
|
|
7
|
+
['2', 'Two'],
|
|
8
|
+
['3', 'Three'],
|
|
9
|
+
['4', 'Four'],
|
|
10
|
+
['5', 'Five'],
|
|
11
|
+
['6', 'Six'],
|
|
12
|
+
['7', 'Seven'],
|
|
13
|
+
['8', 'Eight'],
|
|
14
|
+
['9', 'Nine'],
|
|
15
|
+
['10', 'Ten']
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
function digitWordsPrefix(digits: string): string {
|
|
19
|
+
return DIGIT_WORDS.get(digits) ?? `N${digits}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type BindGeneratorMode = 'package' | 'embedded';
|
|
23
|
+
|
|
24
|
+
export interface BindNames {
|
|
25
|
+
kebab: string;
|
|
26
|
+
pascal: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface GeneratedFile {
|
|
30
|
+
relativePath: string;
|
|
31
|
+
content: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@Injectable()
|
|
35
|
+
export class BindGeneratorService {
|
|
36
|
+
constructor(private readonly fileWriter: FileWriterService) {}
|
|
37
|
+
|
|
38
|
+
deriveNames(input: string): BindNames {
|
|
39
|
+
const kebab = input
|
|
40
|
+
.toLowerCase()
|
|
41
|
+
.replace(/[^a-z0-9-]/g, '-')
|
|
42
|
+
.replace(/-+/g, '-')
|
|
43
|
+
.replace(/^-|-$/g, '');
|
|
44
|
+
|
|
45
|
+
const pascal = kebab
|
|
46
|
+
.split('-')
|
|
47
|
+
.map((part) => {
|
|
48
|
+
// Handle numeric-leading segments (e.g. "1password" → "OnePassword")
|
|
49
|
+
const withoutLeadingDigits = part.replace(/^\d+/, digitWordsPrefix);
|
|
50
|
+
return (
|
|
51
|
+
withoutLeadingDigits.charAt(0).toUpperCase() +
|
|
52
|
+
withoutLeadingDigits.slice(1)
|
|
53
|
+
);
|
|
54
|
+
})
|
|
55
|
+
.join('');
|
|
56
|
+
|
|
57
|
+
return { kebab, pascal };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
renderFiles(names: BindNames, mode: BindGeneratorMode): GeneratedFile[] {
|
|
61
|
+
if (mode === 'embedded') {
|
|
62
|
+
return [
|
|
63
|
+
{
|
|
64
|
+
relativePath: `${names.pascal}Bind.ts`,
|
|
65
|
+
content: this.renderBindClass(names)
|
|
66
|
+
}
|
|
67
|
+
];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const packageDir = `bind-${names.kebab}`;
|
|
71
|
+
return [
|
|
72
|
+
{
|
|
73
|
+
relativePath: path.join(packageDir, 'src', `${names.pascal}Bind.ts`),
|
|
74
|
+
content: this.renderBindClass(names)
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
relativePath: path.join(packageDir, 'src', 'index.ts'),
|
|
78
|
+
content: this.renderIndex(names)
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
relativePath: path.join(packageDir, 'package.json'),
|
|
82
|
+
content: this.renderPackageJson(names)
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
relativePath: path.join(packageDir, 'tsconfig.json'),
|
|
86
|
+
content: this.renderTsConfig()
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
relativePath: path.join(packageDir, 'eslint.config.mjs'),
|
|
90
|
+
content: this.renderEslintConfig()
|
|
91
|
+
}
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
writeFiles(files: GeneratedFile[], outputDir: string): void {
|
|
96
|
+
for (const file of files) {
|
|
97
|
+
const fullPath = path.join(outputDir, file.relativePath);
|
|
98
|
+
this.fileWriter.writeToFile(fullPath, file.content);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private renderBindClass(names: BindNames): string {
|
|
103
|
+
return `import { Bind } from '@config-bound/core/bind';
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Options for constructing a ${names.pascal}Bind.
|
|
107
|
+
* Add the SDK client / credential options for ${names.pascal} here.
|
|
108
|
+
*/
|
|
109
|
+
export interface ${names.pascal}BindOptions {
|
|
110
|
+
// e.g. token?: string; endpoint?: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* A {@link Bind} that retrieves values from ${names.pascal}.
|
|
115
|
+
*
|
|
116
|
+
* Uses a static factory method ({@link ${names.pascal}Bind.create}) to pre-load all values
|
|
117
|
+
* into an in-memory map at startup, so that {@link ${names.pascal}Bind.retrieve} reads
|
|
118
|
+
* from memory without making additional network calls.
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* \`\`\`typescript
|
|
122
|
+
* const bind = await ${names.pascal}Bind.create({ token: process.env.${names.kebab.toUpperCase().replace(/-/g, '_')}_TOKEN });
|
|
123
|
+
* const config = ConfigBound.createConfig(schema, { binds: [bind] });
|
|
124
|
+
* \`\`\`
|
|
125
|
+
*/
|
|
126
|
+
export class ${names.pascal}Bind extends Bind {
|
|
127
|
+
private readonly values: Map<string, unknown>;
|
|
128
|
+
|
|
129
|
+
private constructor(values: Map<string, unknown>) {
|
|
130
|
+
super('${names.pascal}');
|
|
131
|
+
this.values = values;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Creates a ${names.pascal}Bind by pre-loading values from the source.
|
|
136
|
+
*
|
|
137
|
+
* Call this once at application startup and pass the returned instance to
|
|
138
|
+
* \`ConfigBound.createConfig\`.
|
|
139
|
+
*/
|
|
140
|
+
static async create(_options: ${names.pascal}BindOptions): Promise<${names.pascal}Bind> {
|
|
141
|
+
const values = new Map<string, unknown>();
|
|
142
|
+
|
|
143
|
+
// TODO: Initialize the SDK client using \`options\`, fetch values, and
|
|
144
|
+
// populate \`values\` with entries keyed by the full element path
|
|
145
|
+
// (format: "sectionName.elementName"). For example:
|
|
146
|
+
//
|
|
147
|
+
// const client = new SdkClient(options);
|
|
148
|
+
// const secrets = await client.listValues();
|
|
149
|
+
// for (const secret of secrets) {
|
|
150
|
+
// values.set(secret.path, secret.data);
|
|
151
|
+
// }
|
|
152
|
+
return new ${names.pascal}Bind(values);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async retrieve<T>(elementName: string): Promise<T | undefined> {
|
|
156
|
+
return this.values.get(elementName) as T | undefined;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private renderIndex(names: BindNames): string {
|
|
163
|
+
return `export { ${names.pascal}Bind, type ${names.pascal}BindOptions } from './${names.pascal}Bind.js';
|
|
164
|
+
`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private renderPackageJson(names: BindNames): string {
|
|
168
|
+
return `{
|
|
169
|
+
"name": "configbound-bind-${names.kebab}",
|
|
170
|
+
"version": "0.1.0",
|
|
171
|
+
"description": "${names.pascal} bind for ConfigBound",
|
|
172
|
+
"private": true,
|
|
173
|
+
"main": "./dist/index.js",
|
|
174
|
+
"types": "./dist/index.d.ts",
|
|
175
|
+
"exports": {
|
|
176
|
+
".": {
|
|
177
|
+
"types": "./dist/index.d.ts",
|
|
178
|
+
"require": "./dist/index.js",
|
|
179
|
+
"import": "./dist/index.js"
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
"scripts": {
|
|
183
|
+
"build": "tsc",
|
|
184
|
+
"clean": "rimraf dist",
|
|
185
|
+
"test": "jest",
|
|
186
|
+
"lint": "eslint src/**/*.ts --fix",
|
|
187
|
+
"lint:ci": "eslint src/**/*.ts",
|
|
188
|
+
"format": "prettier --write src/**/*.ts",
|
|
189
|
+
"format:ci": "prettier --check src/**/*.ts"
|
|
190
|
+
},
|
|
191
|
+
"keywords": ["configbound", "config", "${names.kebab}"],
|
|
192
|
+
"license": "MIT",
|
|
193
|
+
"dependencies": {
|
|
194
|
+
"@config-bound/core": "^0.1.0"
|
|
195
|
+
},
|
|
196
|
+
"devDependencies": {
|
|
197
|
+
"@eslint/js": "^9.0.0",
|
|
198
|
+
"@types/jest": "^30.0.0",
|
|
199
|
+
"@types/node": "^24.0.0",
|
|
200
|
+
"eslint": "^9.0.0",
|
|
201
|
+
"jest": "^30.0.0",
|
|
202
|
+
"rimraf": "^6.0.0",
|
|
203
|
+
"ts-jest": "^29.0.0",
|
|
204
|
+
"typescript": "^6.0.0",
|
|
205
|
+
"typescript-eslint": "^8.0.0"
|
|
206
|
+
},
|
|
207
|
+
"publishConfig": {
|
|
208
|
+
"access": "public",
|
|
209
|
+
"registry": "https://registry.npmjs.org"
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private renderTsConfig(): string {
|
|
216
|
+
return `{
|
|
217
|
+
"compilerOptions": {
|
|
218
|
+
"target": "ES2022",
|
|
219
|
+
"module": "NodeNext",
|
|
220
|
+
"moduleResolution": "NodeNext",
|
|
221
|
+
"lib": ["ES2022"],
|
|
222
|
+
"outDir": "./dist",
|
|
223
|
+
"rootDir": "./src",
|
|
224
|
+
"strict": true,
|
|
225
|
+
"esModuleInterop": true,
|
|
226
|
+
"skipLibCheck": true,
|
|
227
|
+
"declaration": true,
|
|
228
|
+
"declarationMap": true,
|
|
229
|
+
"sourceMap": true
|
|
230
|
+
},
|
|
231
|
+
"include": ["src/**/*"],
|
|
232
|
+
"exclude": ["node_modules", "dist", "**/*.spec.ts", "**/*.test.ts"]
|
|
233
|
+
}
|
|
234
|
+
`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private renderEslintConfig(): string {
|
|
238
|
+
return `import js from '@eslint/js';
|
|
239
|
+
import tseslint from 'typescript-eslint';
|
|
240
|
+
|
|
241
|
+
export default [
|
|
242
|
+
{ ignores: ['**/dist/**', '**/node_modules/**'] },
|
|
243
|
+
js.configs.recommended,
|
|
244
|
+
...tseslint.configs.recommended,
|
|
245
|
+
];
|
|
246
|
+
`;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
@@ -45,7 +45,7 @@ describe('ConfigDiscoveryService', () => {
|
|
|
45
45
|
createTempFile(
|
|
46
46
|
'config.ts',
|
|
47
47
|
`
|
|
48
|
-
import { ConfigBound } from '@config-bound/
|
|
48
|
+
import { ConfigBound } from '@config-bound/core';
|
|
49
49
|
|
|
50
50
|
export const appConfig = ConfigBound.createConfig({
|
|
51
51
|
name: 'MyApp',
|
|
@@ -66,7 +66,7 @@ describe('ConfigDiscoveryService', () => {
|
|
|
66
66
|
createTempFile(
|
|
67
67
|
'config.ts',
|
|
68
68
|
`
|
|
69
|
-
import { createConfig } from '@config-bound/
|
|
69
|
+
import { createConfig } from '@config-bound/core';
|
|
70
70
|
|
|
71
71
|
export const dbConfig = createConfig({
|
|
72
72
|
name: 'Database',
|
|
@@ -86,7 +86,7 @@ describe('ConfigDiscoveryService', () => {
|
|
|
86
86
|
createTempFile(
|
|
87
87
|
'config.ts',
|
|
88
88
|
`
|
|
89
|
-
import { ConfigBound } from '@config-bound/
|
|
89
|
+
import { ConfigBound } from '@config-bound/core';
|
|
90
90
|
|
|
91
91
|
export default ConfigBound.createConfig({
|
|
92
92
|
name: 'DefaultConfig',
|
|
@@ -107,7 +107,7 @@ describe('ConfigDiscoveryService', () => {
|
|
|
107
107
|
createTempFile(
|
|
108
108
|
'configs.ts',
|
|
109
109
|
`
|
|
110
|
-
import { createConfig } from '@config-bound/
|
|
110
|
+
import { createConfig } from '@config-bound/core';
|
|
111
111
|
|
|
112
112
|
export const config1 = createConfig({ name: 'Config1', sections: {} });
|
|
113
113
|
export const config2 = createConfig({ name: 'Config2', sections: {} });
|
|
@@ -126,7 +126,7 @@ describe('ConfigDiscoveryService', () => {
|
|
|
126
126
|
createTempFile(
|
|
127
127
|
'config1.ts',
|
|
128
128
|
`
|
|
129
|
-
import { createConfig } from '@config-bound/
|
|
129
|
+
import { createConfig } from '@config-bound/core';
|
|
130
130
|
export const config1 = createConfig({ name: 'Config1', sections: {} });
|
|
131
131
|
`
|
|
132
132
|
);
|
|
@@ -134,7 +134,7 @@ describe('ConfigDiscoveryService', () => {
|
|
|
134
134
|
createTempFile(
|
|
135
135
|
'subdir/config2.ts',
|
|
136
136
|
`
|
|
137
|
-
import { createConfig } from '@config-bound/
|
|
137
|
+
import { createConfig } from '@config-bound/core';
|
|
138
138
|
export const config2 = createConfig({ name: 'Config2', sections: {} });
|
|
139
139
|
`
|
|
140
140
|
);
|
|
@@ -148,7 +148,7 @@ describe('ConfigDiscoveryService', () => {
|
|
|
148
148
|
createTempFile(
|
|
149
149
|
'config1.ts',
|
|
150
150
|
`
|
|
151
|
-
import { createConfig } from '@config-bound/
|
|
151
|
+
import { createConfig } from '@config-bound/core';
|
|
152
152
|
export const config1 = createConfig({ name: 'Config1', sections: {} });
|
|
153
153
|
`
|
|
154
154
|
);
|
|
@@ -156,7 +156,7 @@ describe('ConfigDiscoveryService', () => {
|
|
|
156
156
|
createTempFile(
|
|
157
157
|
'subdir/config2.ts',
|
|
158
158
|
`
|
|
159
|
-
import { createConfig } from '@config-bound/
|
|
159
|
+
import { createConfig } from '@config-bound/core';
|
|
160
160
|
export const config2 = createConfig({ name: 'Config2', sections: {} });
|
|
161
161
|
`
|
|
162
162
|
);
|
|
@@ -170,7 +170,7 @@ describe('ConfigDiscoveryService', () => {
|
|
|
170
170
|
createTempFile(
|
|
171
171
|
'config.ts',
|
|
172
172
|
`
|
|
173
|
-
import { createConfig } from '@config-bound/
|
|
173
|
+
import { createConfig } from '@config-bound/core';
|
|
174
174
|
const privateConfig = createConfig({ name: 'Private', sections: {} });
|
|
175
175
|
`
|
|
176
176
|
);
|
|
@@ -184,7 +184,7 @@ describe('ConfigDiscoveryService', () => {
|
|
|
184
184
|
createTempFile(
|
|
185
185
|
'config.spec.ts',
|
|
186
186
|
`
|
|
187
|
-
import { createConfig } from '@config-bound/
|
|
187
|
+
import { createConfig } from '@config-bound/core';
|
|
188
188
|
export const testConfig = createConfig({ name: 'Test', sections: {} });
|
|
189
189
|
`
|
|
190
190
|
);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Injectable } from '@nestjs/common';
|
|
2
2
|
import { Project, Node, SyntaxKind, SourceFile } from 'ts-morph';
|
|
3
3
|
import * as path from 'path';
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
4
|
+
import { promises as fs } from 'fs';
|
|
5
|
+
import { glob } from 'node:fs/promises';
|
|
6
|
+
import { ensureError } from '@config-bound/core/utilities';
|
|
6
7
|
|
|
7
8
|
export interface DiscoveredConfig {
|
|
8
9
|
filePath: string;
|
|
@@ -18,7 +19,7 @@ export class ConfigDiscoveryService {
|
|
|
18
19
|
searchPath: string,
|
|
19
20
|
recursive: boolean = true
|
|
20
21
|
): Promise<DiscoveredConfig[]> {
|
|
21
|
-
const tsFiles = this.findTypeScriptFiles(searchPath, recursive);
|
|
22
|
+
const tsFiles = await this.findTypeScriptFiles(searchPath, recursive);
|
|
22
23
|
const discovered: DiscoveredConfig[] = [];
|
|
23
24
|
|
|
24
25
|
// Create a single project instance for all files
|
|
@@ -171,11 +172,10 @@ export class ConfigDiscoveryService {
|
|
|
171
172
|
return undefined;
|
|
172
173
|
}
|
|
173
174
|
|
|
174
|
-
private findTypeScriptFiles(
|
|
175
|
+
private async findTypeScriptFiles(
|
|
175
176
|
searchPath: string,
|
|
176
177
|
recursive: boolean
|
|
177
|
-
): string[] {
|
|
178
|
-
const files: string[] = [];
|
|
178
|
+
): Promise<string[]> {
|
|
179
179
|
const excludedDirs = new Set([
|
|
180
180
|
'node_modules',
|
|
181
181
|
'dist',
|
|
@@ -192,46 +192,31 @@ export class ConfigDiscoveryService {
|
|
|
192
192
|
'env'
|
|
193
193
|
]);
|
|
194
194
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
} catch {
|
|
200
|
-
// Skip directories we can't read
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
195
|
+
const stats = await fs.stat(searchPath);
|
|
196
|
+
if (stats.isFile()) {
|
|
197
|
+
return searchPath.endsWith('.ts') ? [searchPath] : [];
|
|
198
|
+
}
|
|
203
199
|
|
|
204
|
-
|
|
205
|
-
|
|
200
|
+
const pattern = recursive ? '**/*.ts' : '*.ts';
|
|
201
|
+
const files: string[] = [];
|
|
206
202
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
) {
|
|
222
|
-
files.push(fullPath);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
203
|
+
for await (const file of glob(pattern, {
|
|
204
|
+
cwd: searchPath,
|
|
205
|
+
exclude: (p) =>
|
|
206
|
+
p
|
|
207
|
+
.split(path.sep)
|
|
208
|
+
.some((part) => excludedDirs.has(part) || part.startsWith('.'))
|
|
209
|
+
})) {
|
|
210
|
+
const name = path.basename(file);
|
|
211
|
+
if (
|
|
212
|
+
!name.endsWith('.spec.ts') &&
|
|
213
|
+
!name.endsWith('.test.ts') &&
|
|
214
|
+
!name.endsWith('.d.ts')
|
|
215
|
+
) {
|
|
216
|
+
files.push(path.join(searchPath, file));
|
|
225
217
|
}
|
|
226
218
|
}
|
|
227
219
|
|
|
228
|
-
const stats = fs.statSync(searchPath);
|
|
229
|
-
if (stats.isDirectory()) {
|
|
230
|
-
traverse(searchPath);
|
|
231
|
-
} else if (stats.isFile() && searchPath.endsWith('.ts')) {
|
|
232
|
-
files.push(searchPath);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
220
|
return files;
|
|
236
221
|
}
|
|
237
222
|
}
|
|
@@ -2,10 +2,9 @@ import { Injectable } from '@nestjs/common';
|
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import * as fs from 'fs';
|
|
4
4
|
import { pathToFileURL } from 'url';
|
|
5
|
-
import { Section } from '@config-bound/
|
|
5
|
+
import { Section } from '@config-bound/core/section';
|
|
6
6
|
import {
|
|
7
7
|
ConfigBound,
|
|
8
|
-
TypedConfigBound,
|
|
9
8
|
ConfigSchema,
|
|
10
9
|
ConfigLoaderException,
|
|
11
10
|
ConfigFileNotFoundException,
|
|
@@ -16,12 +15,12 @@ import {
|
|
|
16
15
|
InvalidConfigBoundInstanceException,
|
|
17
16
|
ConfigFileParseException,
|
|
18
17
|
MissingDependencyException
|
|
19
|
-
} from '@config-bound/
|
|
18
|
+
} from '@config-bound/core';
|
|
20
19
|
|
|
21
20
|
export interface LoadedConfig {
|
|
22
21
|
name: string;
|
|
23
|
-
sections: Section
|
|
24
|
-
instance: ConfigBound
|
|
22
|
+
sections: ReadonlyArray<Section>;
|
|
23
|
+
instance: ConfigBound<ConfigSchema>;
|
|
25
24
|
}
|
|
26
25
|
|
|
27
26
|
@Injectable()
|
|
@@ -94,10 +93,7 @@ export class ConfigLoaderService {
|
|
|
94
93
|
const fileURL = pathToFileURL(fileToLoad).href;
|
|
95
94
|
const module = await import(fileURL);
|
|
96
95
|
|
|
97
|
-
let configInstance:
|
|
98
|
-
| ConfigBound
|
|
99
|
-
| TypedConfigBound<ConfigSchema>
|
|
100
|
-
| undefined;
|
|
96
|
+
let configInstance: ConfigBound<ConfigSchema> | undefined;
|
|
101
97
|
|
|
102
98
|
if (exportName && exportName !== 'default') {
|
|
103
99
|
configInstance = module[exportName];
|
|
@@ -156,9 +152,7 @@ export class ConfigLoaderService {
|
|
|
156
152
|
);
|
|
157
153
|
}
|
|
158
154
|
|
|
159
|
-
configInstance = configExports[0][1] as
|
|
160
|
-
| ConfigBound
|
|
161
|
-
| TypedConfigBound<ConfigSchema>;
|
|
155
|
+
configInstance = configExports[0][1] as ConfigBound<ConfigSchema>;
|
|
162
156
|
} else {
|
|
163
157
|
// exportName === 'default' was explicitly requested, so use it even if invalid
|
|
164
158
|
configInstance = defaultExport;
|
|
@@ -320,9 +314,7 @@ export class ConfigLoaderService {
|
|
|
320
314
|
exportName !== 'default' &&
|
|
321
315
|
this.isConfigBoundInstance(exportValue)
|
|
322
316
|
) {
|
|
323
|
-
const typedValue = exportValue as
|
|
324
|
-
| ConfigBound
|
|
325
|
-
| TypedConfigBound<ConfigSchema>;
|
|
317
|
+
const typedValue = exportValue as ConfigBound<ConfigSchema>;
|
|
326
318
|
configs.set(exportName, {
|
|
327
319
|
name: typedValue.name,
|
|
328
320
|
sections: typedValue.sections,
|
|
@@ -365,7 +357,7 @@ export class ConfigLoaderService {
|
|
|
365
357
|
|
|
366
358
|
private isConfigBoundInstance(
|
|
367
359
|
value: unknown
|
|
368
|
-
): value is ConfigBound
|
|
360
|
+
): value is ConfigBound<ConfigSchema> {
|
|
369
361
|
return (
|
|
370
362
|
value !== null &&
|
|
371
363
|
value !== undefined &&
|