jax-hono 1.1.0 → 1.1.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.
@@ -39,6 +39,12 @@ type PluginFile = EnabledPlugin & {
39
39
  filePath: string
40
40
  }
41
41
 
42
+ type ControllerExportInfo = {
43
+ defaultClass: boolean
44
+ namedClasses: string[]
45
+ namedValues: string[]
46
+ }
47
+
42
48
  const controllerGroups: ControllerGroup[] = [
43
49
  { name: 'api', dirs: ['api', 'controllers/api', 'controller/api', 'modules'], suffix: '.api' },
44
50
  { name: 'backend', dirs: ['backend', 'controllers/backend', 'controller/backend', 'modules'], suffix: '.backend' },
@@ -211,6 +217,11 @@ function getFilesFromDirs(dirs: string[]) {
211
217
  return uniqueSorted(dirs.flatMap((dirName) => walkFiles(join(srcDir, dirName))))
212
218
  }
213
219
 
220
+ function isRootDirIndex(filePath: string, dirs: string[]) {
221
+ return basename(filePath) === 'index.ts'
222
+ && dirs.some((dirName) => dirname(filePath) === join(srcDir, dirName))
223
+ }
224
+
214
225
  function getEnabledPluginFiles(suffix: string) {
215
226
  return getEnabledPlugins().flatMap((plugin) => {
216
227
  return walkFiles(plugin.rootDir)
@@ -240,8 +251,47 @@ function isModuleFeatureFile(filePath: string, suffix: string) {
240
251
  return !isFileInDir(filePath, 'modules') || isSuffixedModuleFile(filePath, suffix)
241
252
  }
242
253
 
243
- function hasDefaultExport(filePath: string) {
244
- return /\bexport\s+default\b/.test(readFileSync(filePath, 'utf8'))
254
+ function getControllerExportInfo(filePath: string): ControllerExportInfo {
255
+ const source = readFileSync(filePath, 'utf8')
256
+ const classNames = new Set(
257
+ [...source.matchAll(/\b(?:export\s+)?(?:default\s+)?(?:abstract\s+)?class\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g)]
258
+ .map((match) => match[1]),
259
+ )
260
+ const defaultIdentifier = source.match(/\bexport\s+default\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\b/)?.[1]
261
+ const defaultClass = /\bexport\s+default\s+(?:abstract\s+)?class\b/.test(source)
262
+ || Boolean(defaultIdentifier && classNames.has(defaultIdentifier))
263
+ const namedClasses = [...source.matchAll(/\bexport\s+(?:abstract\s+)?class\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g)]
264
+ .map((match) => match[1])
265
+ .sort((left, right) => left.localeCompare(right))
266
+ const namedValues = [
267
+ ...source.matchAll(/\bexport\s+(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g),
268
+ ...source.matchAll(/\bexport\s+(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g),
269
+ ]
270
+ .map((match) => match[1])
271
+ .filter((name) => !namedClasses.includes(name))
272
+ .sort((left, right) => left.localeCompare(right))
273
+
274
+ return { defaultClass, namedClasses, namedValues }
275
+ }
276
+
277
+ function hasControllerExport(info: ControllerExportInfo) {
278
+ return info.defaultClass || info.namedClasses.length > 0 || info.namedValues.length > 0
279
+ }
280
+
281
+ function getControllerClassExport(info: ControllerExportInfo) {
282
+ if (info.defaultClass) {
283
+ return 'default'
284
+ }
285
+
286
+ return info.namedClasses.find((name) => name.endsWith('Controller')) ?? info.namedClasses[0]
287
+ }
288
+
289
+ function toControllerNamedExportsObject(namespaceName: string, names: string[]) {
290
+ if (names.length === 0) {
291
+ return '{}'
292
+ }
293
+
294
+ return `{\n${indentObject(names.map((name) => `${toObjectKey(name)}: ${namespaceName}.${name},`).join('\n'), 2)}\n}`
245
295
  }
246
296
 
247
297
  function indentObject(content: string, spaces = 2) {
@@ -291,32 +341,37 @@ function generateControllersRegistry() {
291
341
  const imports: string[] = []
292
342
  const controllerExports: string[] = []
293
343
  const groupLines: string[] = []
344
+ let usesCreateController = false
294
345
 
295
346
  for (const group of controllerGroups) {
296
347
  const projectEntries = getFilesFromDirs(group.dirs)
297
348
  .filter((filePath) => basename(filePath) !== 'index.ts')
298
349
  .filter((filePath) => !group.suffix || isModuleFeatureFile(filePath, group.suffix))
299
- .filter(hasDefaultExport)
300
- .map((filePath, index) => {
350
+ .map((filePath) => ({ filePath, exportInfo: getControllerExportInfo(filePath) }))
351
+ .filter(({ exportInfo }) => hasControllerExport(exportInfo))
352
+ .map(({ filePath, exportInfo }, index) => {
301
353
  const baseDir = group.dirs.find((dirName) => {
302
354
  return isFileInDir(filePath, dirName)
303
355
  }) ?? group.dirs[0]
304
356
  const modulePath = toModulePath(baseDir, filePath, group.suffix ? [group.suffix] : [])
305
357
  const importName = toIdentifier([group.name, modulePath, String(index), 'Controller'])
358
+ const namespaceName = toIdentifier([group.name, modulePath, String(index), 'ControllerExports'])
306
359
  const exportName = toLowerCamelIdentifier(group.name === 'controller' ? [modulePath] : [group.name, modulePath], 'Controller')
307
360
 
308
- return { modulePath, importName, exportName, filePath }
361
+ return { modulePath, importName, namespaceName, exportName, filePath, exportInfo }
309
362
  })
310
363
  const pluginEntries = group.name === 'controller'
311
364
  ? getEnabledPluginFiles('.controller')
312
365
  .filter(({ filePath }) => basename(filePath) !== 'index.ts')
313
- .filter(({ filePath }) => hasDefaultExport(filePath))
314
- .map(({ name, rootDir, filePath }, index) => {
366
+ .map((item) => ({ ...item, exportInfo: getControllerExportInfo(item.filePath) }))
367
+ .filter(({ exportInfo }) => hasControllerExport(exportInfo))
368
+ .map(({ name, rootDir, filePath, exportInfo }, index) => {
315
369
  const modulePath = toPluginModulePath(name, rootDir, filePath, ['.controller'])
316
370
  const importName = toIdentifier([group.name, 'plugin', modulePath, String(index), 'Controller'])
371
+ const namespaceName = toIdentifier([group.name, 'plugin', modulePath, String(index), 'ControllerExports'])
317
372
  const exportName = toLowerCamelIdentifier([modulePath], 'Controller')
318
373
 
319
- return { modulePath, importName, exportName, filePath }
374
+ return { modulePath, importName, namespaceName, exportName, filePath, exportInfo }
320
375
  })
321
376
  : []
322
377
  const entries = [...projectEntries, ...pluginEntries]
@@ -341,8 +396,33 @@ function generateControllersRegistry() {
341
396
  return true
342
397
  })
343
398
 
344
- imports.push(...entries.map((entry) => `import ${entry.importName} from '${toImportPath(entry.filePath)}'`))
345
- controllerExports.push(...entries.map((entry) => `export const ${entry.exportName} = createController(${entry.importName})`))
399
+ imports.push(...entries.flatMap((entry) => {
400
+ const importPath = toImportPath(entry.filePath)
401
+ const classExport = getControllerClassExport(entry.exportInfo)
402
+ const defaultImport = classExport === 'default'
403
+ ? [`import ${entry.importName} from '${importPath}'`]
404
+ : []
405
+
406
+ return [
407
+ ...defaultImport,
408
+ `import * as ${entry.namespaceName} from '${importPath}'`,
409
+ ]
410
+ }))
411
+ controllerExports.push(...entries.map((entry) => {
412
+ const classExport = getControllerClassExport(entry.exportInfo)
413
+ const namedExports = toControllerNamedExportsObject(entry.namespaceName, entry.exportInfo.namedValues)
414
+
415
+ if (!classExport) {
416
+ return `export const ${entry.exportName} = ${namedExports}`
417
+ }
418
+
419
+ usesCreateController = true
420
+ const controllerExport = classExport === 'default'
421
+ ? entry.importName
422
+ : `${entry.namespaceName}.${classExport}`
423
+
424
+ return `export const ${entry.exportName} = createController(${controllerExport}, ${namedExports})`
425
+ }))
346
426
 
347
427
  if (entries.length === 0) {
348
428
  continue
@@ -360,7 +440,7 @@ function generateControllersRegistry() {
360
440
  }
361
441
  }
362
442
 
363
- const helperImport = imports.length > 0
443
+ const helperImport = usesCreateController
364
444
  ? `import { createController } from 'jax-hono/core/controller'\n`
365
445
  : ''
366
446
  const content = `${generatedHeader}
@@ -442,9 +522,10 @@ ${entries.join('\n')}
442
522
  }
443
523
 
444
524
  function generateServicesRegistry() {
525
+ const serviceDirs = ['services', 'service']
445
526
  const files = [
446
- ...getFilesFromDirs(['services', 'service'])
447
- .filter((filePath) => basename(filePath) !== 'index.ts')
527
+ ...getFilesFromDirs(serviceDirs)
528
+ .filter((filePath) => !isRootDirIndex(filePath, serviceDirs))
448
529
  .map((filePath) => ({ filePath })),
449
530
  ]
450
531
  const imports: string[] = []
@@ -454,7 +535,7 @@ function generateServicesRegistry() {
454
535
  files.forEach((item) => {
455
536
  const { filePath } = item
456
537
  const source = readFileSync(filePath, 'utf8')
457
- const namedServiceObjects = [...source.matchAll(/\bexport\s+const\s+([a-zA-Z_$][a-zA-Z0-9_$]*Service)\b/g)]
538
+ const namedServiceObjects = [...source.matchAll(/\bexport\s+const\s+([a-zA-Z_$][a-zA-Z0-9_$]*(?:Service|Adapters))\b/g)]
458
539
  .map((match) => match[1])
459
540
  .sort((left, right) => left.localeCompare(right))
460
541
 
@@ -5,6 +5,9 @@ import type { GeneratedPlugins, GeneratedServices } from './generated';
5
5
  const controllerContextStorage = new AsyncLocalStorage<Context>();
6
6
  type ControllerConstructor<T extends Controller> = new (...args: any[]) => T;
7
7
  type ControllerExport<T extends Controller> = T | ControllerConstructor<T>;
8
+ type ControllerNamedExports = Record<string, unknown>;
9
+ type ControllerResult<T extends Controller, TNamedExports extends ControllerNamedExports> =
10
+ T & Omit<TNamedExports, 'default'>;
8
11
 
9
12
  const bodyCache = new WeakMap<Context, unknown>();
10
13
 
@@ -22,8 +25,33 @@ export function getJaxContext() {
22
25
  return context;
23
26
  }
24
27
 
25
- export function createController<T extends Controller>(controller: ControllerExport<T>) {
26
- return typeof controller === 'function' ? new controller() : controller;
28
+ export function createController<T extends Controller>(
29
+ controller: ControllerExport<T>,
30
+ ): T;
31
+ export function createController<T extends Controller, TNamedExports extends ControllerNamedExports>(
32
+ controller: ControllerExport<T>,
33
+ namedExports: TNamedExports,
34
+ ): ControllerResult<T, TNamedExports>;
35
+ export function createController<T extends Controller>(
36
+ controller: ControllerExport<T>,
37
+ namedExports: ControllerNamedExports = {},
38
+ ): T & ControllerNamedExports {
39
+ const instance = (typeof controller === 'function' ? new controller() : controller) as T & ControllerNamedExports;
40
+
41
+ for (const [name, value] of Object.entries(namedExports)) {
42
+ if (name === 'default') {
43
+ continue;
44
+ }
45
+
46
+ Object.defineProperty(instance, name, {
47
+ configurable: true,
48
+ enumerable: true,
49
+ writable: true,
50
+ value,
51
+ });
52
+ }
53
+
54
+ return instance;
27
55
  }
28
56
 
29
57
  export abstract class Controller {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jax-hono",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "Lightweight framework layer on top of Hono, built for Bun",
5
5
  "type": "module",
6
6
  "main": "./index.ts",