@hono-filebased-route/core 0.2.3 → 0.3.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hono-filebased-route/core",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "description": "A core utility for file-based routing in Hono applications.",
6
6
  "author": "HM Suiji <hmsuiji@gmail.com>",
@@ -40,6 +40,9 @@
40
40
  "peerDependenciesMeta": {
41
41
  "hono": {
42
42
  "optional": false
43
+ },
44
+ "typescript": {
45
+ "optional": false
43
46
  }
44
47
  },
45
48
  "publishConfig": {
@@ -1,62 +1,63 @@
1
1
  import { writeFile } from 'fs/promises'
2
2
  import { getExportedHttpMethods, getFiles, getRoutePath } from '../utils/load-routes-utils'
3
3
  import path from 'path'
4
- import { pathToFileURL } from 'url'
5
- import { METHODS } from '../types'
6
-
7
- const ROUTES_DIR = './src/routes'
8
- const OUTPUT_FILE = './src/generated-routes.ts'
9
-
10
- export async function generateRoutesFile(
11
- dir: string = ROUTES_DIR,
12
- output: string = OUTPUT_FILE,
13
- write: boolean = true
14
- ) {
15
- console.log('Generating routes file...', dir, output)
16
- const absoluteRoutesDir = path.resolve(dir)
17
- const files = await getFiles(absoluteRoutesDir)
18
-
19
- const importStatements: string[] = []
20
- const routeDefinitions: string[] = []
21
-
22
- importStatements.push(`import { Hono } from 'hono';`)
23
-
24
- let counter = 0
25
- for (const file of files) {
26
- const routePath = getRoutePath(file, absoluteRoutesDir)
27
- .replace(/\\/g, '/')
28
- .replace(/\/index$/, '')
29
- const relativePath = path
30
- .relative(path.dirname(output), file)
31
- .replace(/\.(ts)$/, '')
32
- .replace(/\\/g, '/')
33
- const moduleName = `routeModule${counter++}`
34
-
35
- importStatements.push(`import * as ${moduleName} from './${relativePath}';`)
36
-
37
- const tempHonoVar = `honoApp${moduleName}`
38
- routeDefinitions.push(` const ${tempHonoVar} = new Hono();`)
39
-
40
- const exportedMethods = getExportedHttpMethods(file);
41
-
42
- for (const method of METHODS) {
43
- if (exportedMethods[method]) {
44
- if (routePath.endsWith('/*')) {
45
- const len = routePath.replace(/\/\*$/g, '').length + 1
46
- routeDefinitions.push(
47
- ` ${tempHonoVar}.${method.toLowerCase()}('/', async (c) => ${moduleName}.${method}(c, c.req.path.substring(${len}).split('/')));`
48
- )
49
- } else
50
- routeDefinitions.push(
51
- ` ${tempHonoVar}.${method.toLowerCase()}('/', ${moduleName}.${method});`
52
- )
53
- }
54
- }
55
-
56
- routeDefinitions.push(` mainApp.route('${routePath}', ${tempHonoVar});`)
57
- }
58
-
59
- const fileContent = `
4
+ import { Config, METHODS } from '../types'
5
+ import { createLogger } from '../utils/logger'
6
+
7
+ const defaultConfig: Config = {
8
+ dir: './src/routes',
9
+ output: './src/generated-routes.ts',
10
+ write: true,
11
+ verbose: false,
12
+ externals: [],
13
+ } as const
14
+
15
+ export async function generateRoutesFile(config?: Partial<Config>) {
16
+ const { dir, output, write, verbose, externals } = { ...defaultConfig, ...config }
17
+ const logger = createLogger(verbose)
18
+ logger.info(`Generating routes file..., ${dir}, ${output}`)
19
+ const absoluteRoutesDir = path.resolve(dir)
20
+ const files = await getFiles(absoluteRoutesDir, externals)
21
+
22
+ const importStatements: string[] = []
23
+ const routeDefinitions: string[] = []
24
+
25
+ importStatements.push(`import { Hono } from 'hono';`)
26
+
27
+ let counter = 0
28
+ for (const file of files) {
29
+ const routePath = getRoutePath(file, absoluteRoutesDir)
30
+ .replace(/\\/g, '/')
31
+ .replace(/\/index$/, '')
32
+ const relativePath = path
33
+ .relative(path.dirname(output), file)
34
+ .replace(/\.(ts)$/, '')
35
+ .replace(/\\/g, '/')
36
+ const moduleName = `routeModule${counter++}`
37
+
38
+ importStatements.push(`import * as ${moduleName} from './${relativePath}';`)
39
+
40
+ const tempHonoVar = `honoApp${moduleName}`
41
+ routeDefinitions.push(` const ${tempHonoVar} = new Hono();`)
42
+
43
+ const exportedMethods = getExportedHttpMethods(file)
44
+
45
+ for (const method of METHODS) {
46
+ if (exportedMethods[method]) {
47
+ if (routePath.endsWith('/*')) {
48
+ const len = routePath.replace(/\/\*$/g, '').length + 1
49
+ routeDefinitions.push(
50
+ ` ${tempHonoVar}.${method.toLowerCase()}('/', async (c) => ${moduleName}.${method}(c, c.req.path.substring(${len}).split('/')));`
51
+ )
52
+ } else routeDefinitions.push(` ${tempHonoVar}.${method.toLowerCase()}('/', ${moduleName}.${method});`)
53
+ logger.info(`Generated route: ${method} ${routePath}`)
54
+ }
55
+ }
56
+
57
+ routeDefinitions.push(` mainApp.route('${routePath}', ${tempHonoVar});`)
58
+ }
59
+
60
+ const fileContent = `
60
61
  // THIS FILE IS AUTO-GENERATED BY scripts/generate-routes.ts. DO NOT EDIT.
61
62
 
62
63
  ${importStatements.join('\n')}
@@ -70,11 +71,10 @@ ${routeDefinitions.join('\n')}
70
71
  }
71
72
  `
72
73
 
73
- if (write) {
74
- await writeFile(output, fileContent.trimStart())
75
- }
76
-
77
- console.log(`Generated routes file: ${output} with ${files.length} routes.`)
74
+ if (write) {
75
+ await writeFile(output, fileContent.trimStart())
76
+ logger.info(`Generated routes file: ${output} with ${files.length} routes.`)
77
+ }
78
78
 
79
- return fileContent.trimStart()
79
+ return fileContent.trimStart()
80
80
  }
package/types/index.ts CHANGED
@@ -2,5 +2,13 @@ export const METHODS = ['GET', 'POST'] as const
2
2
  export type Method = (typeof METHODS)[number]
3
3
 
4
4
  export type ExportedMethods = {
5
- [key in Method]: boolean;
6
- }
5
+ [key in Method]: boolean
6
+ }
7
+
8
+ export type Config = {
9
+ dir: string
10
+ output: string
11
+ write: boolean
12
+ verbose: boolean
13
+ externals: string[]
14
+ }
@@ -2,23 +2,31 @@ import path from 'path'
2
2
  import fg from 'fast-glob'
3
3
  import { readFileSync } from 'fs'
4
4
  import { ExportedMethods, Method, METHODS } from '../types'
5
- import { createSourceFile, ScriptTarget, isVariableStatement, isFunctionDeclaration, SyntaxKind, isIdentifier } from 'typescript'
5
+ import {
6
+ createSourceFile,
7
+ ScriptTarget,
8
+ isVariableStatement,
9
+ isFunctionDeclaration,
10
+ SyntaxKind,
11
+ isIdentifier,
12
+ } from 'typescript'
6
13
 
7
14
  /**
8
15
  * 遍历指定目录并获取所有文件路径
9
16
  * @param dir 要遍历的目录
10
17
  * @returns 目录内所有文件绝对路径的数组
11
18
  */
12
- export async function getFiles(dir: string): Promise<string[]> {
13
- const absoluteDir = path.resolve(dir)
14
- const pattern = path.join(absoluteDir, '**', '*.{ts,js}').replace(/\\/g, '/')
19
+ export async function getFiles(dir: string, externals?: string[]): Promise<string[]> {
20
+ const absoluteDir = path.resolve(dir)
21
+ const pattern = path.join(absoluteDir, '**', '*.{ts,js}').replace(/\\/g, '/')
15
22
 
16
- const files = await fg(pattern, {
17
- absolute: true,
18
- onlyFiles: true
19
- })
23
+ const files = await fg(pattern, {
24
+ absolute: true,
25
+ onlyFiles: true,
26
+ ignore: externals,
27
+ })
20
28
 
21
- return files
29
+ return files
22
30
  }
23
31
 
24
32
  /**
@@ -28,19 +36,19 @@ export async function getFiles(dir: string): Promise<string[]> {
28
36
  * @returns 转换后的 Hono 路由路径
29
37
  */
30
38
  export function getRoutePath(filePath: string, baseDir: string): string {
31
- let routeName = path.relative(baseDir, filePath).replace(/\.(ts|js)$/, '')
39
+ let routeName = path.relative(baseDir, filePath).replace(/\.(ts|js)$/, '')
32
40
 
33
- routeName = routeName
34
- .replace(/\[\.\.\.(\w+)\]/g, '*') // 捕获所有:[...slug] => *
35
- .replace(/\[(\w+)\]/g, ':$1') // 动态参数:[id] => :id
41
+ routeName = routeName
42
+ .replace(/\[\.\.\.(\w+)\]/g, '*') // 捕获所有:[...slug] => *
43
+ .replace(/\[(\w+)\]/g, ':$1') // 动态参数:[id] => :id
36
44
 
37
- if (routeName === 'index') {
38
- return '/'
39
- } else if (routeName.endsWith('/index')) {
40
- return `/${routeName.slice(0, -6)}`
41
- }
45
+ if (routeName === 'index') {
46
+ return '/'
47
+ } else if (routeName.endsWith('/index')) {
48
+ return `/${routeName.slice(0, -6)}`
49
+ }
42
50
 
43
- return `/${routeName}`
51
+ return `/${routeName}`
44
52
  }
45
53
 
46
54
  /**
@@ -49,27 +57,26 @@ export function getRoutePath(filePath: string, baseDir: string): string {
49
57
  * @returns 导出的 HTTP 方法对象
50
58
  */
51
59
  export function getExportedHttpMethods(filePath: string): ExportedMethods {
52
- const fileContent = readFileSync(filePath, 'utf8');
53
- const sourceFile = createSourceFile(
54
- filePath,
55
- fileContent,
56
- ScriptTarget.ESNext,
57
- true
58
- );
59
- const methods: ExportedMethods = {} as ExportedMethods
60
- sourceFile.forEachChild(node => {
61
- // 寻找 export const GET = ... 或 export function POST() { ... } 形式
62
- if (isVariableStatement(node) && node.modifiers && node.modifiers.some(m => m.kind === SyntaxKind.ExportKeyword)) {
63
- for (const declaration of node.declarationList.declarations) {
64
- if (isIdentifier(declaration.name) && METHODS.includes(declaration.name.text as Method)) {
65
- methods[declaration.name.text as Method] = true;
66
- }
67
- }
68
- } else if (isFunctionDeclaration(node) && node.modifiers && node.modifiers.some(m => m.kind === SyntaxKind.ExportKeyword)) {
69
- if (node.name && METHODS.includes(node.name.text as Method)) {
70
- methods[node.name.text as Method] = true;
71
- }
72
- }
73
- });
74
- return methods;
60
+ const fileContent = readFileSync(filePath, 'utf8')
61
+ const sourceFile = createSourceFile(filePath, fileContent, ScriptTarget.ESNext, true)
62
+ const methods: ExportedMethods = {} as ExportedMethods
63
+ sourceFile.forEachChild(node => {
64
+ // 寻找 export const GET = ... 或 export function POST() { ... } 形式
65
+ if (isVariableStatement(node) && node.modifiers && node.modifiers.some(m => m.kind === SyntaxKind.ExportKeyword)) {
66
+ for (const declaration of node.declarationList.declarations) {
67
+ if (isIdentifier(declaration.name) && METHODS.includes(declaration.name.text as Method)) {
68
+ methods[declaration.name.text as Method] = true
69
+ }
70
+ }
71
+ } else if (
72
+ isFunctionDeclaration(node) &&
73
+ node.modifiers &&
74
+ node.modifiers.some(m => m.kind === SyntaxKind.ExportKeyword)
75
+ ) {
76
+ if (node.name && METHODS.includes(node.name.text as Method)) {
77
+ methods[node.name.text as Method] = true
78
+ }
79
+ }
80
+ })
81
+ return methods
75
82
  }
@@ -0,0 +1,18 @@
1
+ import pino, { Logger } from 'pino'
2
+
3
+ export const createLogger = (verbose: boolean = false) =>
4
+ verbose
5
+ ? pino({
6
+ transport: {
7
+ target: 'pino-pretty',
8
+ options: {
9
+ colorize: true,
10
+ },
11
+ },
12
+ })
13
+ : ({
14
+ info: () => { },
15
+ debug: () => { },
16
+ error: () => { },
17
+ warn: () => { },
18
+ } as unknown as Logger)