@hono-filebased-route/core 0.2.1 → 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,48 +1,51 @@
1
- {
2
- "name": "@hono-filebased-route/core",
3
- "version": "0.2.1",
4
- "type": "module",
5
- "description": "A core utility for file-based routing in Hono applications.",
6
- "author": "HM Suiji <hmsuiji@gmail.com>",
7
- "keywords": [
8
- "hono",
9
- "router",
10
- "file-based",
11
- "routing",
12
- "backend",
13
- "framework",
14
- "typescript",
15
- "file-based-route"
16
- ],
17
- "license": "MIT",
18
- "repository": {
19
- "type": "git",
20
- "url": "https://github.com/HM-Suiji/hono-filebased-route.git"
21
- },
22
- "main": "dist/index.js",
23
- "types": "dist/index.d.ts",
24
- "scripts": {
25
- "build": "bun run build.ts",
26
- "clean": "rm -rf dist"
27
- },
28
- "dependencies": {
29
- "fast-glob": "^3.3.3",
30
- "fs": "0.0.1-security",
31
- "hono": "^4.9.2",
32
- "path": "^0.12.7"
33
- },
34
- "devDependencies": {
35
- "@types/bun": "^1.2.20",
36
- "@types/node": "^24.3.0",
37
- "bun-plugin-dts": "^0.3.0",
38
- "typescript": "^5.0.0"
39
- },
40
- "peerDependenciesMeta": {
41
- "hono": {
42
- "optional": false
43
- }
44
- },
45
- "publishConfig": {
46
- "access": "public"
47
- }
1
+ {
2
+ "name": "@hono-filebased-route/core",
3
+ "version": "0.3.0",
4
+ "type": "module",
5
+ "description": "A core utility for file-based routing in Hono applications.",
6
+ "author": "HM Suiji <hmsuiji@gmail.com>",
7
+ "keywords": [
8
+ "hono",
9
+ "router",
10
+ "file-based",
11
+ "routing",
12
+ "backend",
13
+ "framework",
14
+ "typescript",
15
+ "file-based-route"
16
+ ],
17
+ "license": "MIT",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://github.com/HM-Suiji/hono-filebased-route.git"
21
+ },
22
+ "main": "dist/index.js",
23
+ "types": "dist/index.d.ts",
24
+ "scripts": {
25
+ "build": "bun run build.ts",
26
+ "clean": "rm -rf dist"
27
+ },
28
+ "dependencies": {
29
+ "fast-glob": "^3.3.3",
30
+ "fs": "0.0.1-security",
31
+ "hono": "^4.9.2",
32
+ "path": "^0.12.7"
33
+ },
34
+ "devDependencies": {
35
+ "@types/bun": "^1.2.20",
36
+ "@types/node": "^24.3.0",
37
+ "bun-plugin-dts": "^0.3.0",
38
+ "typescript": "^5.0.0"
39
+ },
40
+ "peerDependenciesMeta": {
41
+ "hono": {
42
+ "optional": false
43
+ },
44
+ "typescript": {
45
+ "optional": false
46
+ }
47
+ },
48
+ "publishConfig": {
49
+ "access": "public"
50
+ }
48
51
  }
@@ -1,65 +1,63 @@
1
1
  import { writeFile } from 'fs/promises'
2
- import { getFiles, getRoutePath } from '../utils/load-routes-utils'
2
+ import { getExportedHttpMethods, getFiles, getRoutePath } from '../utils/load-routes-utils'
3
3
  import path from 'path'
4
- import { pathToFileURL } from 'url'
4
+ import { Config, METHODS } from '../types'
5
+ import { createLogger } from '../utils/logger'
5
6
 
6
- const ROUTES_DIR = './src/routes'
7
- const OUTPUT_FILE = './src/generated-routes.ts'
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
8
14
 
9
- export async function generateRoutesFile(
10
- dir: string = ROUTES_DIR,
11
- output: string = OUTPUT_FILE
12
- ) {
13
- console.log('Generating routes file...', dir, output)
14
- const absoluteRoutesDir = path.resolve(dir)
15
- const files = await getFiles(absoluteRoutesDir)
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)
16
21
 
17
- const importStatements: string[] = []
18
- const routeDefinitions: string[] = []
19
- const methods = ['GET', 'POST']
22
+ const importStatements: string[] = []
23
+ const routeDefinitions: string[] = []
20
24
 
21
- importStatements.push(`import { Hono } from 'hono';`)
25
+ importStatements.push(`import { Hono } from 'hono';`)
22
26
 
23
- let counter = 0
24
- for (const file of files) {
25
- const routePath = getRoutePath(file, absoluteRoutesDir)
26
- .replace(/\\/g, '/')
27
- .replace(/\/index$/, '')
28
- const relativePath = path
29
- .relative(path.dirname(output), file)
30
- .replace(/\.(ts)$/, '')
31
- .replace(/\\/g, '/')
32
- const moduleName = `routeModule${counter++}`
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++}`
33
37
 
34
- importStatements.push(`import * as ${moduleName} from './${relativePath}';`)
38
+ importStatements.push(`import * as ${moduleName} from './${relativePath}';`)
35
39
 
36
- const tempHonoVar = `honoApp${moduleName}`
37
- routeDefinitions.push(` const ${tempHonoVar} = new Hono();`)
40
+ const tempHonoVar = `honoApp${moduleName}`
41
+ routeDefinitions.push(` const ${tempHonoVar} = new Hono();`)
38
42
 
39
- for await (const method of methods) {
40
- const fileUrl = pathToFileURL(file).href
41
- const module = await import(fileUrl)
42
- if (typeof module[method] === 'function') {
43
- if (routePath.endsWith('/*')) {
44
- const len = routePath.replace(/\/\*$/g, '').length + 1
45
- routeDefinitions.push(
46
- ` ${tempHonoVar}.${method.toLowerCase()}('/', async (c) => ${moduleName}.${method}(c, c.req.path.substring(${len}).split('/')));`
47
- )
48
- } else
49
- routeDefinitions.push(
50
- ` ${tempHonoVar}.${method.toLowerCase()}('/', ${moduleName}.${method});`
51
- )
52
- }
53
- }
43
+ const exportedMethods = getExportedHttpMethods(file)
54
44
 
55
- if (routePath === '/') {
56
- routeDefinitions.push(` mainApp.route('${routePath}', ${tempHonoVar});`)
57
- } else {
58
- routeDefinitions.push(` mainApp.route('${routePath}', ${tempHonoVar});`)
59
- }
60
- }
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
+ }
61
56
 
62
- const fileContent = `
57
+ routeDefinitions.push(` mainApp.route('${routePath}', ${tempHonoVar});`)
58
+ }
59
+
60
+ const fileContent = `
63
61
  // THIS FILE IS AUTO-GENERATED BY scripts/generate-routes.ts. DO NOT EDIT.
64
62
 
65
63
  ${importStatements.join('\n')}
@@ -73,7 +71,10 @@ ${routeDefinitions.join('\n')}
73
71
  }
74
72
  `
75
73
 
76
- await writeFile(output, fileContent.trimStart())
74
+ if (write) {
75
+ await writeFile(output, fileContent.trimStart())
76
+ logger.info(`Generated routes file: ${output} with ${files.length} routes.`)
77
+ }
77
78
 
78
- console.log(`Generated routes file: ${output} with ${files.length} routes.`)
79
+ return fileContent.trimStart()
79
80
  }
package/tsconfig.json CHANGED
@@ -1,18 +1,19 @@
1
- {
2
- "extends": "../../tsconfig.json",
3
- "compilerOptions": {
4
- "rootDir": ".",
5
- "outDir": "./dist",
6
- "composite": true
7
- },
8
- "include": [
9
- "utils/**/*.ts",
10
- "scripts/**/*.ts",
11
- "index.ts"
12
- ],
13
- "exclude": [
14
- "node_modules",
15
- "dist",
16
- "**/*.test.ts"
17
- ]
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": ".",
5
+ "outDir": "./dist",
6
+ "composite": true
7
+ },
8
+ "include": [
9
+ "utils/**/*.ts",
10
+ "scripts/**/*.ts",
11
+ "types/**/*.ts",
12
+ "index.ts"
13
+ ],
14
+ "exclude": [
15
+ "node_modules",
16
+ "dist",
17
+ "**/*.test.ts"
18
+ ]
18
19
  }
package/types/index.ts ADDED
@@ -0,0 +1,14 @@
1
+ export const METHODS = ['GET', 'POST'] as const
2
+ export type Method = (typeof METHODS)[number]
3
+
4
+ export type ExportedMethods = {
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
+ }
@@ -1,21 +1,32 @@
1
1
  import path from 'path'
2
2
  import fg from 'fast-glob'
3
+ import { readFileSync } from 'fs'
4
+ import { ExportedMethods, Method, METHODS } from '../types'
5
+ import {
6
+ createSourceFile,
7
+ ScriptTarget,
8
+ isVariableStatement,
9
+ isFunctionDeclaration,
10
+ SyntaxKind,
11
+ isIdentifier,
12
+ } from 'typescript'
3
13
 
4
14
  /**
5
15
  * 遍历指定目录并获取所有文件路径
6
16
  * @param dir 要遍历的目录
7
17
  * @returns 目录内所有文件绝对路径的数组
8
18
  */
9
- export async function getFiles(dir: string): Promise<string[]> {
10
- const absoluteDir = path.resolve(dir)
11
- 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, '/')
12
22
 
13
- const files = await fg(pattern, {
14
- absolute: true,
15
- onlyFiles: true
16
- })
23
+ const files = await fg(pattern, {
24
+ absolute: true,
25
+ onlyFiles: true,
26
+ ignore: externals,
27
+ })
17
28
 
18
- return files
29
+ return files
19
30
  }
20
31
 
21
32
  /**
@@ -25,17 +36,47 @@ export async function getFiles(dir: string): Promise<string[]> {
25
36
  * @returns 转换后的 Hono 路由路径
26
37
  */
27
38
  export function getRoutePath(filePath: string, baseDir: string): string {
28
- let routeName = path.relative(baseDir, filePath).replace(/\.(ts|js)$/, '')
39
+ let routeName = path.relative(baseDir, filePath).replace(/\.(ts|js)$/, '')
29
40
 
30
- routeName = routeName
31
- .replace(/\[\.\.\.(\w+)\]/g, '*') // 捕获所有:[...slug] => *
32
- .replace(/\[(\w+)\]/g, ':$1') // 动态参数:[id] => :id
41
+ routeName = routeName
42
+ .replace(/\[\.\.\.(\w+)\]/g, '*') // 捕获所有:[...slug] => *
43
+ .replace(/\[(\w+)\]/g, ':$1') // 动态参数:[id] => :id
33
44
 
34
- if (routeName === 'index') {
35
- return '/'
36
- } else if (routeName.endsWith('/index')) {
37
- return `/${routeName.slice(0, -6)}`
38
- }
45
+ if (routeName === 'index') {
46
+ return '/'
47
+ } else if (routeName.endsWith('/index')) {
48
+ return `/${routeName.slice(0, -6)}`
49
+ }
39
50
 
40
- return `/${routeName}`
51
+ return `/${routeName}`
52
+ }
53
+
54
+ /**
55
+ * 从文件中提取导出的 HTTP 方法
56
+ * @param filePath 文件的绝对路径
57
+ * @returns 导出的 HTTP 方法对象
58
+ */
59
+ export function getExportedHttpMethods(filePath: string): ExportedMethods {
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
41
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)