jax-hono 1.0.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.
@@ -0,0 +1,615 @@
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
2
+ import { basename, join, parse, relative } from 'node:path'
3
+
4
+ const srcDir = join(import.meta.dir, '../..')
5
+ const generatedDir = join(srcDir, 'jax/generated')
6
+ const moduleExtensions = new Set(['.ts', '.js', '.mjs', '.cjs'])
7
+ const generatedHeader = `// This file is generated by src/jax/build/generate-registry.ts.
8
+ // Do not edit it directly.
9
+ `
10
+
11
+ type ControllerGroup = {
12
+ name: string
13
+ dirs: string[]
14
+ suffix?: string
15
+ }
16
+
17
+ type PluginConfigItem =
18
+ | false
19
+ | {
20
+ enable?: boolean
21
+ package?: string
22
+ options?: unknown
23
+ }
24
+
25
+ type PluginConfig = Record<string, PluginConfigItem>
26
+
27
+ const controllerGroups: ControllerGroup[] = [
28
+ { name: 'api', dirs: ['api', 'controllers/api', 'controller/api', 'modules'], suffix: '.api' },
29
+ { name: 'backend', dirs: ['backend', 'controllers/backend', 'controller/backend', 'modules'], suffix: '.backend' },
30
+ { name: 'open', dirs: ['open', 'controllers/open', 'controller/open', 'modules'], suffix: '.open' },
31
+ { name: 'client', dirs: ['client', 'controllers/client', 'controller/client', 'modules'], suffix: '.client' },
32
+ { name: 'controller', dirs: ['controllers', 'controller'] },
33
+ ]
34
+
35
+ const scopedControllerGroups = new Set(
36
+ controllerGroups
37
+ .filter((group) => group.name !== 'controller')
38
+ .map((group) => group.name),
39
+ )
40
+
41
+ function walkFiles(dir: string): string[] {
42
+ if (!existsSync(dir)) {
43
+ return []
44
+ }
45
+
46
+ const files: string[] = []
47
+
48
+ for (const item of readdirSync(dir, { withFileTypes: true })) {
49
+ const itemPath = join(dir, item.name)
50
+
51
+ if (item.isDirectory()) {
52
+ files.push(...walkFiles(itemPath))
53
+ continue
54
+ }
55
+
56
+ if (item.isFile() && moduleExtensions.has(parse(item.name).ext)) {
57
+ files.push(itemPath)
58
+ }
59
+ }
60
+
61
+ return files
62
+ }
63
+
64
+ function writeGeneratedFile(fileName: string, content: string) {
65
+ const outputFile = join(generatedDir, fileName)
66
+
67
+ mkdirSync(generatedDir, { recursive: true })
68
+
69
+ if (existsSync(outputFile) && readFileSync(outputFile, 'utf8') === content) {
70
+ return
71
+ }
72
+
73
+ writeFileSync(outputFile, content)
74
+ console.log(`Generated ${relative(srcDir, outputFile)}.`)
75
+ }
76
+
77
+ function toImportPath(filePath: string) {
78
+ const path = relative(srcDir, filePath)
79
+ .replace(parse(filePath).ext, '')
80
+ .replaceAll('\\', '/')
81
+
82
+ return `@/${path}`
83
+ }
84
+
85
+ function toIdentifier(parts: string[]) {
86
+ const value = parts
87
+ .join('_')
88
+ .replace(/[^a-zA-Z0-9_$]/g, '_')
89
+ .replace(/^([^a-zA-Z_$])/, '_$1')
90
+
91
+ return value || 'module'
92
+ }
93
+
94
+ function toCamelCase(value: string) {
95
+ return value.replace(/[-_]+([a-zA-Z0-9])/g, (_match, char: string) => char.toUpperCase())
96
+ }
97
+
98
+ function toPascalCase(value: string) {
99
+ const camel = toCamelCase(value)
100
+
101
+ return camel.charAt(0).toUpperCase() + camel.slice(1)
102
+ }
103
+
104
+ function toServiceKey(name: string) {
105
+ const baseName = name.endsWith('Service')
106
+ ? name.slice(0, -'Service'.length)
107
+ : name
108
+
109
+ return baseName.replace(/^./, (char) => char.toLowerCase())
110
+ }
111
+
112
+ function toMiddlewareKey(exportName: string) {
113
+ return exportName.endsWith('Middleware')
114
+ ? exportName.slice(0, -'Middleware'.length)
115
+ : exportName
116
+ }
117
+
118
+ function stripKnownSuffix(modulePath: string, suffixes: string[]) {
119
+ for (const suffix of suffixes) {
120
+ if (modulePath.endsWith(suffix)) {
121
+ return modulePath.slice(0, -suffix.length)
122
+ }
123
+ }
124
+
125
+ return modulePath
126
+ }
127
+
128
+ function normalizeModulePath(modulePath: string) {
129
+ const parts = modulePath.split('/')
130
+ const last = parts.at(-1)
131
+ const parent = parts.at(-2)
132
+
133
+ return last && parent && last === parent
134
+ ? parts.slice(0, -1).join('/')
135
+ : modulePath
136
+ }
137
+
138
+ function toModulePath(baseDir: string, filePath: string, suffixes: string[] = []) {
139
+ const modulePath = relative(join(srcDir, baseDir), filePath)
140
+ .replace(parse(filePath).ext, '')
141
+ .replaceAll('\\', '/')
142
+ const withoutIndex = modulePath.endsWith('/index')
143
+ ? modulePath.slice(0, -'/index'.length)
144
+ : modulePath
145
+
146
+ const stripped = stripKnownSuffix(withoutIndex, suffixes)
147
+
148
+ return baseDir === 'modules' ? normalizeModulePath(stripped) : stripped
149
+ }
150
+
151
+ function uniqueSorted(values: string[]) {
152
+ return [...new Set(values)].sort((left, right) => left.localeCompare(right))
153
+ }
154
+
155
+ function getFilesFromDirs(dirs: string[]) {
156
+ return uniqueSorted(dirs.flatMap((dirName) => walkFiles(join(srcDir, dirName))))
157
+ }
158
+
159
+ function isFileInDir(filePath: string, dirName: string) {
160
+ const rel = relative(join(srcDir, dirName), filePath)
161
+
162
+ return rel && !rel.startsWith('..')
163
+ }
164
+
165
+ function isSuffixedModuleFile(filePath: string, suffix: string) {
166
+ return relative(srcDir, filePath)
167
+ .replace(parse(filePath).ext, '')
168
+ .replaceAll('\\', '/')
169
+ .endsWith(suffix)
170
+ }
171
+
172
+ function isModuleFeatureFile(filePath: string, suffix: string) {
173
+ return !isFileInDir(filePath, 'modules') || isSuffixedModuleFile(filePath, suffix)
174
+ }
175
+
176
+ function hasDefaultExport(filePath: string) {
177
+ return /\bexport\s+default\b/.test(readFileSync(filePath, 'utf8'))
178
+ }
179
+
180
+ function indentObject(content: string, spaces = 2) {
181
+ if (!content) {
182
+ return ''
183
+ }
184
+
185
+ const indent = ' '.repeat(spaces)
186
+
187
+ return content
188
+ .split('\n')
189
+ .map((line) => `${indent}${line}`)
190
+ .join('\n')
191
+ }
192
+
193
+ function buildNestedObject(entries: { path: string[]; value: string }[]): string {
194
+ const groups = new Map<string, { path: string[]; value: string }[]>()
195
+ const leaves: { key: string; value: string }[] = []
196
+
197
+ for (const entry of entries) {
198
+ const [head, ...tail] = entry.path
199
+
200
+ if (!head) {
201
+ continue
202
+ }
203
+
204
+ if (tail.length === 0) {
205
+ leaves.push({ key: head, value: entry.value })
206
+ continue
207
+ }
208
+
209
+ groups.set(head, [...(groups.get(head) ?? []), { path: tail, value: entry.value }])
210
+ }
211
+
212
+ const lines = [
213
+ ...leaves.map((leaf) => `${toObjectKey(leaf.key)}: ${leaf.value},`),
214
+ ...[...groups.entries()].map(([key, groupEntries]) => {
215
+ return `${toObjectKey(key)}: {\n${indentObject(buildNestedObject(groupEntries), 2)}\n},`
216
+ }),
217
+ ]
218
+
219
+ return lines.join('\n')
220
+ }
221
+
222
+ function generateControllersRegistry() {
223
+ const used = new Set<string>()
224
+ const imports: string[] = []
225
+ const groupLines: string[] = []
226
+
227
+ for (const group of controllerGroups) {
228
+ const entries = getFilesFromDirs(group.dirs)
229
+ .filter((filePath) => basename(filePath) !== 'index.ts')
230
+ .filter((filePath) => !group.suffix || isModuleFeatureFile(filePath, group.suffix))
231
+ .filter(hasDefaultExport)
232
+ .map((filePath, index) => {
233
+ const baseDir = group.dirs.find((dirName) => {
234
+ return isFileInDir(filePath, dirName)
235
+ }) ?? group.dirs[0]
236
+ const modulePath = toModulePath(baseDir, filePath, group.suffix ? [group.suffix] : [])
237
+ const importName = toIdentifier([group.name, modulePath, String(index), 'Controller'])
238
+
239
+ return { modulePath, importName, filePath }
240
+ })
241
+ .filter((entry) => {
242
+ if (group.name !== 'controller') {
243
+ return true
244
+ }
245
+
246
+ const [scope] = entry.modulePath.split('/')
247
+
248
+ return !scopedControllerGroups.has(scope)
249
+ })
250
+ .filter((entry) => {
251
+ const key = `${group.name}:${entry.modulePath}`
252
+
253
+ if (used.has(key)) {
254
+ return false
255
+ }
256
+
257
+ used.add(key)
258
+
259
+ return true
260
+ })
261
+
262
+ imports.push(...entries.map((entry) => `import ${entry.importName} from '${toImportPath(entry.filePath)}'`))
263
+
264
+ if (entries.length === 0) {
265
+ continue
266
+ }
267
+
268
+ const objectBody = indentObject(
269
+ buildNestedObject(entries.map((entry) => ({
270
+ path: entry.modulePath.split('/').map(toCamelCase),
271
+ value: `createController(${entry.importName})`,
272
+ }))),
273
+ 4,
274
+ )
275
+
276
+ groupLines.push(` ${group.name}: {\n${objectBody}\n },`)
277
+ }
278
+
279
+ const helperImport = imports.length > 0
280
+ ? `import { createController } from '@/jax/core/controller'\n`
281
+ : ''
282
+ const content = `${generatedHeader}
283
+ ${helperImport}${imports.join('\n')}
284
+
285
+ export const controllers = {
286
+ ${groupLines.join('\n')}
287
+ }
288
+ `
289
+
290
+ writeGeneratedFile('controllers.generated.ts', content)
291
+ }
292
+
293
+ function toObjectKey(value: string) {
294
+ return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(value) ? value : JSON.stringify(value)
295
+ }
296
+
297
+ function generateMiddlewaresRegistry() {
298
+ const files = getFilesFromDirs(['middlewares', 'middleware', 'modules'])
299
+ .filter((filePath) => isModuleFeatureFile(filePath, '.middleware'))
300
+ const imports: string[] = []
301
+ const entries: string[] = []
302
+
303
+ files.forEach((filePath, index) => {
304
+ const source = readFileSync(filePath, 'utf8')
305
+ const exportNames = [...source.matchAll(/\bexport\s+(?:const|function)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g)]
306
+ .map((match) => match[1])
307
+ .sort((left, right) => left.localeCompare(right))
308
+
309
+ if (exportNames.length === 0) {
310
+ return
311
+ }
312
+
313
+ imports.push(`import { ${exportNames.join(', ')} } from '${toImportPath(filePath)}'`)
314
+ entries.push(...exportNames.map((name) => ` ${toMiddlewareKey(name)}: ${name},`))
315
+ })
316
+
317
+ const content = `${generatedHeader}
318
+ ${imports.join('\n')}
319
+
320
+ export const middlewares = {
321
+ ${entries.join('\n')}
322
+ }
323
+ `
324
+
325
+ writeGeneratedFile('middlewares.generated.ts', content)
326
+ }
327
+
328
+ function generateModelsRegistry() {
329
+ const files = getFilesFromDirs(['models', 'model', 'modules'])
330
+ .filter((filePath) => isModuleFeatureFile(filePath, '.model'))
331
+ const imports: string[] = []
332
+ const entries: string[] = []
333
+
334
+ files.forEach((filePath) => {
335
+ const source = readFileSync(filePath, 'utf8')
336
+ const exportedModels = [...source.matchAll(/\bexport\s+const\s+([a-zA-Z_$][a-zA-Z0-9_$]*Model)\b/g)]
337
+ .map((match) => match[1])
338
+ .sort((left, right) => left.localeCompare(right))
339
+
340
+ if (exportedModels.length === 0) {
341
+ return
342
+ }
343
+
344
+ const modelKey = source.match(/\bexport\s+const\s+modelKey\s*=\s*['"]([a-zA-Z_$][a-zA-Z0-9_$]*)['"]/)?.[1]
345
+
346
+ imports.push(`import { ${exportedModels.join(', ')} } from '${toImportPath(filePath)}'`)
347
+ entries.push(...exportedModels.map((name) => {
348
+ const key = exportedModels.length === 1 && modelKey
349
+ ? modelKey
350
+ : name.slice(0, -'Model'.length)
351
+
352
+ return ` ${key}: ${name},`
353
+ }))
354
+ })
355
+
356
+ const content = `${generatedHeader}
357
+ ${imports.join('\n')}
358
+
359
+ export const models = {
360
+ ${entries.join('\n')}
361
+ }
362
+
363
+ export type Models = typeof models
364
+ `
365
+
366
+ writeGeneratedFile('models.generated.ts', content)
367
+ }
368
+
369
+ function generateServicesRegistry() {
370
+ const files = getFilesFromDirs(['services', 'service', 'modules'])
371
+ .filter((filePath) => isModuleFeatureFile(filePath, '.service'))
372
+ const imports: string[] = []
373
+ const entries: string[] = []
374
+
375
+ files.forEach((filePath) => {
376
+ const source = readFileSync(filePath, 'utf8')
377
+ const namedServiceClasses = [...source.matchAll(/\bexport\s+class\s+([a-zA-Z_$][a-zA-Z0-9_$]*Service)\b/g)]
378
+ .map((match) => match[1])
379
+ .sort((left, right) => left.localeCompare(right))
380
+ const defaultServiceClass = source.match(/\bexport\s+default\s+class(?:\s+([a-zA-Z_$][a-zA-Z0-9_$]*))?/)
381
+ const defaultServiceName = defaultServiceClass
382
+ ? defaultServiceClass[1] ?? `${toPascalCase(toModulePath('services', filePath))}Service`
383
+ : ''
384
+ const defaultImportName = defaultServiceName
385
+ ? toIdentifier([defaultServiceName, 'Default'])
386
+ : ''
387
+
388
+ if (namedServiceClasses.length === 0 && !defaultServiceName) {
389
+ return
390
+ }
391
+
392
+ if (defaultServiceName) {
393
+ imports.push(`import ${defaultImportName} from '${toImportPath(filePath)}'`)
394
+ entries.push(` ${toServiceKey(defaultServiceName)}: new ${defaultImportName}(deps),`)
395
+ }
396
+
397
+ if (namedServiceClasses.length > 0) {
398
+ imports.push(`import { ${namedServiceClasses.join(', ')} } from '${toImportPath(filePath)}'`)
399
+ entries.push(...namedServiceClasses.map((name) => ` ${toServiceKey(name)}: new ${name}(deps),`))
400
+ }
401
+ })
402
+
403
+ const content = `${generatedHeader}
404
+ ${imports.join('\n')}
405
+
406
+ export function createServices(deps: any) {
407
+ return {
408
+ ${entries.join('\n')}
409
+ }
410
+ }
411
+
412
+ export type Services = ReturnType<typeof createServices>
413
+ `
414
+
415
+ writeGeneratedFile('services.generated.ts', content)
416
+ }
417
+
418
+ function findBalancedObject(source: string, startIndex: number) {
419
+ let depth = 0
420
+ let quote: string | undefined
421
+ let escaped = false
422
+ let lineComment = false
423
+ let blockComment = false
424
+
425
+ for (let index = startIndex; index < source.length; index += 1) {
426
+ const char = source[index]
427
+ const next = source[index + 1]
428
+
429
+ if (lineComment) {
430
+ if (char === '\n') {
431
+ lineComment = false
432
+ }
433
+ continue
434
+ }
435
+
436
+ if (blockComment) {
437
+ if (char === '*' && next === '/') {
438
+ blockComment = false
439
+ index += 1
440
+ }
441
+ continue
442
+ }
443
+
444
+ if (quote) {
445
+ if (escaped) {
446
+ escaped = false
447
+ continue
448
+ }
449
+
450
+ if (char === '\\') {
451
+ escaped = true
452
+ continue
453
+ }
454
+
455
+ if (char === quote) {
456
+ quote = undefined
457
+ }
458
+ continue
459
+ }
460
+
461
+ if (char === '/' && next === '/') {
462
+ lineComment = true
463
+ index += 1
464
+ continue
465
+ }
466
+
467
+ if (char === '/' && next === '*') {
468
+ blockComment = true
469
+ index += 1
470
+ continue
471
+ }
472
+
473
+ if (char === '"' || char === '\'' || char === '`') {
474
+ quote = char
475
+ continue
476
+ }
477
+
478
+ if (char === '{') {
479
+ depth += 1
480
+ continue
481
+ }
482
+
483
+ if (char === '}') {
484
+ depth -= 1
485
+
486
+ if (depth === 0) {
487
+ return source.slice(startIndex, index + 1)
488
+ }
489
+ }
490
+ }
491
+
492
+ return undefined
493
+ }
494
+
495
+ function extractPluginConfigObject(source: string) {
496
+ const configCallIndex = source.indexOf('definePluginConfig')
497
+ const searchStart = configCallIndex >= 0 ? configCallIndex : source.indexOf('export default')
498
+ const objectStart = searchStart >= 0 ? source.indexOf('{', searchStart) : -1
499
+
500
+ if (objectStart < 0) {
501
+ return undefined
502
+ }
503
+
504
+ return findBalancedObject(source, objectStart)
505
+ }
506
+
507
+ function readPluginConfig(filePath: string): PluginConfig {
508
+ if (!existsSync(filePath)) {
509
+ return {}
510
+ }
511
+
512
+ const objectSource = extractPluginConfigObject(readFileSync(filePath, 'utf8'))
513
+
514
+ if (!objectSource) {
515
+ return {}
516
+ }
517
+
518
+ try {
519
+ const value = new Function(`return (${objectSource})`)() as unknown
520
+
521
+ return value && typeof value === 'object' && !Array.isArray(value)
522
+ ? (value as PluginConfig)
523
+ : {}
524
+ } catch (error) {
525
+ throw new Error(`Invalid plugin config ${relative(srcDir, filePath)}: ${(error as Error).message}`)
526
+ }
527
+ }
528
+
529
+ function normalizePluginPackage(packageName: string, configFile: string) {
530
+ if (!packageName.startsWith('./') && !packageName.startsWith('../')) {
531
+ return packageName
532
+ }
533
+
534
+ const normalized = relative(srcDir, join(configFile, '..', packageName))
535
+ .replaceAll('\\', '/')
536
+ .replace(/\/index$/, '')
537
+
538
+ if (normalized.startsWith('jax/')) {
539
+ return normalized
540
+ }
541
+
542
+ return `@/${normalized}`
543
+ }
544
+
545
+ function mergePluginConfig(configs: { filePath: string; config: PluginConfig }[]) {
546
+ const merged = new Map<string, Exclude<PluginConfigItem, false>>()
547
+
548
+ for (const { filePath, config } of configs) {
549
+ for (const [name, item] of Object.entries(config)) {
550
+ if (item === false) {
551
+ merged.delete(name)
552
+ continue
553
+ }
554
+
555
+ const current = merged.get(name) ?? {}
556
+ const next = { ...current, ...item }
557
+
558
+ if (next.package) {
559
+ next.package = normalizePluginPackage(next.package, filePath)
560
+ }
561
+
562
+ merged.set(name, next)
563
+ }
564
+ }
565
+
566
+ return [...merged.entries()]
567
+ .filter((entry): entry is [string, Exclude<PluginConfigItem, false> & { package: string }] => {
568
+ const [, item] = entry
569
+
570
+ return item.enable === true && typeof item.package === 'string' && item.package.length > 0
571
+ })
572
+ .sort(([left], [right]) => left.localeCompare(right))
573
+ }
574
+
575
+ function serializePluginOptions(options: unknown) {
576
+ return options === undefined ? '' : `, options: ${JSON.stringify(options)}`
577
+ }
578
+
579
+ function generatePluginContextRegistry() {
580
+ const enabledPlugins = mergePluginConfig([
581
+ { filePath: join(srcDir, 'jax/plugins/config.ts'), config: readPluginConfig(join(srcDir, 'jax/plugins/config.ts')) },
582
+ { filePath: join(srcDir, 'config/plugin.ts'), config: readPluginConfig(join(srcDir, 'config/plugin.ts')) },
583
+ ])
584
+ const imports = enabledPlugins.map(([name, item]) => {
585
+ return `import ${toIdentifier(['plugin', name])} from ${JSON.stringify(item.package)}`
586
+ })
587
+ const moduleEntries = enabledPlugins.map(
588
+ ([name, item]) =>
589
+ ` { name: ${JSON.stringify(name)}, module: ${toIdentifier(['plugin', name])}${serializePluginOptions(item.options)} },`,
590
+ )
591
+
592
+ const content = `${generatedHeader}${imports.length > 0 ? `${imports.join('\n')}\n\n` : ''}
593
+ export const plugins = {} as Record<string, unknown>
594
+
595
+ export type Plugins = typeof plugins
596
+
597
+ export const pluginModules = [
598
+ ${moduleEntries.join('\n')}
599
+ ] as const
600
+ `
601
+
602
+ writeGeneratedFile('plugins.generated.ts', content)
603
+ }
604
+
605
+ export function generateRegistry() {
606
+ generateControllersRegistry()
607
+ generateMiddlewaresRegistry()
608
+ generateModelsRegistry()
609
+ generateServicesRegistry()
610
+ generatePluginContextRegistry()
611
+ }
612
+
613
+ if (import.meta.main) {
614
+ generateRegistry()
615
+ }
package/config.ts ADDED
@@ -0,0 +1,76 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { cwd } from 'node:process';
3
+ import merge from 'deepmerge';
4
+ import { config as loadDotenv } from 'dotenv';
5
+ import minimist from 'minimist';
6
+ import { configModules } from './generated/config';
7
+ import type { ProjectConfig } from './generated/config';
8
+ import type { JaxConfig } from './types/config';
9
+
10
+ type PlainConfig = Record<string, unknown>;
11
+ export type { JaxConfig } from './types/config';
12
+ export type Config = JaxConfig & ProjectConfig;
13
+
14
+ loadDotenvFile('.env');
15
+
16
+ const argv = minimist(Bun.argv.slice(2), {
17
+ string: ['env'],
18
+ alias: {
19
+ env: 'e'
20
+ }
21
+ });
22
+
23
+ const env = argv.env || Bun.env.APP_ENV || Bun.env.NODE_ENV || 'prod';
24
+
25
+ console.log('env', env);
26
+
27
+ loadDotenvFile(`.env.${env}`);
28
+
29
+ function loadDotenvFile(file: string) {
30
+ if (existsSync(file)) {
31
+ loadDotenv({ path: file, override: true });
32
+ }
33
+ }
34
+
35
+ function isPlainConfig(value: unknown): value is PlainConfig {
36
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
37
+ }
38
+
39
+ function shouldUseConfigStage(stage: string) {
40
+ return stage === 'base' || stage === 'default' || stage === env;
41
+ }
42
+
43
+ function parsePort(value: string | undefined) {
44
+ const port = Number(value);
45
+
46
+ return Number.isInteger(port) && port > 0 ? port : 3000;
47
+ }
48
+
49
+ export function loadConfig(): Config {
50
+ const jaxConfig: JaxConfig = {
51
+ rootDir: cwd(),
52
+ port: parsePort(Bun.env.PORT),
53
+ env
54
+ };
55
+
56
+ const configs = configModules
57
+ .filter((item) => shouldUseConfigStage(item.stage))
58
+ .map((item) => {
59
+ if (!isPlainConfig(item.config)) {
60
+ throw new TypeError(`Config file ${item.name} must export a plain object.`);
61
+ }
62
+
63
+ return item.config;
64
+ });
65
+
66
+ return merge.all<PlainConfig>([jaxConfig, ...configs], {
67
+ arrayMerge: (_destination, source) => source
68
+ }) as Config;
69
+ }
70
+
71
+ const config = loadConfig();
72
+
73
+ console.log('port', config.port);
74
+
75
+ export { env };
76
+ export default config;