@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/build.ts +6 -5
- package/dist/index.d.ts +9 -2
- package/dist/index.js +109 -454
- package/package.json +4 -1
- package/scripts/generate-routes.ts +62 -62
- package/types/index.ts +10 -2
- package/utils/load-routes-utils.ts +49 -42
- package/utils/logger.ts +18 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hono-filebased-route/core",
|
|
3
|
-
"version": "0.
|
|
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 {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
14
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
23
|
+
const files = await fg(pattern, {
|
|
24
|
+
absolute: true,
|
|
25
|
+
onlyFiles: true,
|
|
26
|
+
ignore: externals,
|
|
27
|
+
})
|
|
20
28
|
|
|
21
|
-
|
|
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
|
-
|
|
39
|
+
let routeName = path.relative(baseDir, filePath).replace(/\.(ts|js)$/, '')
|
|
32
40
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
41
|
+
routeName = routeName
|
|
42
|
+
.replace(/\[\.\.\.(\w+)\]/g, '*') // 捕获所有:[...slug] => *
|
|
43
|
+
.replace(/\[(\w+)\]/g, ':$1') // 动态参数:[id] => :id
|
|
36
44
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
if (routeName === 'index') {
|
|
46
|
+
return '/'
|
|
47
|
+
} else if (routeName.endsWith('/index')) {
|
|
48
|
+
return `/${routeName.slice(0, -6)}`
|
|
49
|
+
}
|
|
42
50
|
|
|
43
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
}
|
package/utils/logger.ts
ADDED
|
@@ -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)
|