@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.
Files changed (62) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/LICENSE +21 -0
  3. package/package.json +29 -23
  4. package/scripts/generate-docs.ts +362 -0
  5. package/src/cli.module.ts +7 -1
  6. package/src/commands/generate-bind.command.ts +175 -0
  7. package/src/commands/generate.command.ts +13 -0
  8. package/src/services/bind-generator.service.ts +248 -0
  9. package/src/services/config-discovery.service.spec.ts +10 -10
  10. package/src/services/config-discovery.service.ts +26 -41
  11. package/src/services/config-loader.service.ts +8 -16
  12. package/src/services/schema-export.service.spec.ts +20 -22
  13. package/src/services/schema-export.service.ts +6 -11
  14. package/tsconfig.json +7 -2
  15. package/.turbo/turbo-build.log +0 -4
  16. package/.turbo/turbo-format$colon$ci.log +0 -6
  17. package/.turbo/turbo-lint$colon$ci.log +0 -4
  18. package/.turbo/turbo-test.log +0 -19
  19. package/dist/cli.module.d.ts +0 -3
  20. package/dist/cli.module.d.ts.map +0 -1
  21. package/dist/cli.module.js +0 -29
  22. package/dist/cli.module.js.map +0 -1
  23. package/dist/commands/export.command.d.ts +0 -30
  24. package/dist/commands/export.command.d.ts.map +0 -1
  25. package/dist/commands/export.command.js +0 -226
  26. package/dist/commands/export.command.js.map +0 -1
  27. package/dist/commands/list.command.d.ts +0 -13
  28. package/dist/commands/list.command.d.ts.map +0 -1
  29. package/dist/commands/list.command.js +0 -93
  30. package/dist/commands/list.command.js.map +0 -1
  31. package/dist/main.d.ts +0 -3
  32. package/dist/main.d.ts.map +0 -1
  33. package/dist/main.js +0 -17
  34. package/dist/main.js.map +0 -1
  35. package/dist/services/config-discovery.service.d.ts +0 -15
  36. package/dist/services/config-discovery.service.d.ts.map +0 -1
  37. package/dist/services/config-discovery.service.js +0 -191
  38. package/dist/services/config-discovery.service.js.map +0 -1
  39. package/dist/services/config-discovery.service.spec.d.ts +0 -2
  40. package/dist/services/config-discovery.service.spec.d.ts.map +0 -1
  41. package/dist/services/config-discovery.service.spec.js +0 -137
  42. package/dist/services/config-discovery.service.spec.js.map +0 -1
  43. package/dist/services/config-loader.service.d.ts +0 -13
  44. package/dist/services/config-loader.service.d.ts.map +0 -1
  45. package/dist/services/config-loader.service.js +0 -241
  46. package/dist/services/config-loader.service.js.map +0 -1
  47. package/dist/services/file-writer.service.d.ts +0 -6
  48. package/dist/services/file-writer.service.d.ts.map +0 -1
  49. package/dist/services/file-writer.service.js +0 -38
  50. package/dist/services/file-writer.service.js.map +0 -1
  51. package/dist/services/file-writer.service.spec.d.ts +0 -2
  52. package/dist/services/file-writer.service.spec.d.ts.map +0 -1
  53. package/dist/services/file-writer.service.spec.js +0 -98
  54. package/dist/services/file-writer.service.spec.js.map +0 -1
  55. package/dist/services/schema-export.service.d.ts +0 -14
  56. package/dist/services/schema-export.service.d.ts.map +0 -1
  57. package/dist/services/schema-export.service.js +0 -58
  58. package/dist/services/schema-export.service.js.map +0 -1
  59. package/dist/services/schema-export.service.spec.d.ts +0 -2
  60. package/dist/services/schema-export.service.spec.d.ts.map +0 -1
  61. package/dist/services/schema-export.service.spec.js +0 -69
  62. 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/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/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/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/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/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/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/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/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/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/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 * as fs from 'fs';
5
- import { ensureError } from '@config-bound/config-bound/utilities';
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
- function traverse(dir: string) {
196
- let entries;
197
- try {
198
- entries = fs.readdirSync(dir, { withFileTypes: true });
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
- for (const entry of entries) {
205
- const fullPath = path.join(dir, entry.name);
200
+ const pattern = recursive ? '**/*.ts' : '*.ts';
201
+ const files: string[] = [];
206
202
 
207
- // Skip excluded directories and hidden directories
208
- if (entry.isDirectory()) {
209
- if (excludedDirs.has(entry.name) || entry.name.startsWith('.')) {
210
- continue;
211
- }
212
- if (recursive) {
213
- traverse(fullPath);
214
- }
215
- } else if (entry.isFile() && entry.name.endsWith('.ts')) {
216
- // Skip test files and declaration files
217
- if (
218
- !entry.name.endsWith('.spec.ts') &&
219
- !entry.name.endsWith('.test.ts') &&
220
- !entry.name.endsWith('.d.ts')
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/config-bound/section';
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/config-bound';
18
+ } from '@config-bound/core';
20
19
 
21
20
  export interface LoadedConfig {
22
21
  name: string;
23
- sections: Section[];
24
- instance: ConfigBound | TypedConfigBound<ConfigSchema>;
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 | TypedConfigBound<ConfigSchema> {
360
+ ): value is ConfigBound<ConfigSchema> {
369
361
  return (
370
362
  value !== null &&
371
363
  value !== undefined &&