jax-hono 1.0.13 → 1.0.15

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