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.
- package/README.md +17 -15
- package/bin/jax.js +66 -66
- package/bin/jax.ts +9 -9
- package/build/build.ts +19 -19
- package/build/generate-config.ts +116 -116
- package/build/generate-registry.ts +603 -589
- package/build/generated-path.ts +57 -57
- package/config.ts +76 -76
- package/core/api-controller.ts +290 -294
- package/core/app.ts +199 -199
- package/core/controller.ts +86 -86
- package/core/hono.ts +64 -64
- package/core/service.ts +46 -46
- package/docs/agent.md +105 -0
- package/docs/jax.md +272 -272
- package/helpers/config.ts +23 -23
- package/helpers/crud.ts +27 -34
- package/helpers/index.ts +4 -4
- package/helpers/route.ts +7 -7
- package/index.ts +116 -116
- package/middleware/api-response.ts +44 -44
- package/middleware/jwt.ts +90 -90
- package/middleware/request-log.ts +3 -3
- package/package.json +124 -124
- package/plugins/config.ts +8 -8
- package/plugins/define.ts +5 -5
- package/plugins/mongoose/index.ts +57 -57
- package/plugins/mongoose/model.ts +322 -322
- package/plugins/mongoose/schema.ts +30 -30
- package/plugins/session/index.ts +86 -86
- package/setup.ts +11 -11
- package/types/config.ts +9 -9
- package/types/context.ts +10 -10
- package/types/index.ts +2 -2
- package/utils/array.ts +7 -7
- package/utils/async.ts +47 -47
- package/utils/crypto.ts +9 -9
- package/utils/index.ts +5 -5
- package/utils/regexp.ts +17 -17
- package/utils/transform.ts +3 -3
|
@@ -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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
161
|
-
|
|
138
|
+
if (last === parent) {
|
|
139
|
+
return parts.slice(0, -1).join('/')
|
|
140
|
+
}
|
|
162
141
|
|
|
163
|
-
|
|
164
|
-
|
|
142
|
+
if (last.startsWith(`${parent}-`) || last.startsWith(`${parent}_`)) {
|
|
143
|
+
return [...parts.slice(0, -2), last].join('/')
|
|
144
|
+
}
|
|
165
145
|
|
|
166
|
-
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
.
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
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
|
+
}
|