create-librex 1.0.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.
- package/index.mjs +110 -0
- package/package.json +22 -0
- package/template/.env.development +1 -0
- package/template/_gitignore +23 -0
- package/template/env.d.ts +1 -0
- package/template/index.html +12 -0
- package/template/package.json +26 -0
- package/template/scripts/create-page.mjs +453 -0
- package/template/src/App.vue +19 -0
- package/template/src/main.ts +60 -0
- package/template/src/pages/auth/LoginPage.vue +92 -0
- package/template/src/pages/notFound/NotFound.vue +29 -0
- package/template/src/router/index.ts +64 -0
- package/template/src/stores/asyncRoute.ts +24 -0
- package/template/src/stores/user.ts +100 -0
- package/template/src/vite-env.d.ts +12 -0
- package/template/tsconfig.app.json +4 -0
- package/template/tsconfig.json +24 -0
- package/template/tsconfig.node.json +4 -0
- package/template/vite-plugin-librex.ts +293 -0
- package/template/vite.config.ts +35 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vite-plugin-librex — 编译期路由提取插件
|
|
3
|
+
*
|
|
4
|
+
* 编译期递归扫描 pages 目录,提取 definePageConfig({...}) 生成
|
|
5
|
+
* virtual:librex-routes 模块,零 import.meta.glob 运行时开销。
|
|
6
|
+
*/
|
|
7
|
+
import type { Plugin, ViteDevServer } from 'vite'
|
|
8
|
+
import path from 'node:path'
|
|
9
|
+
import fs from 'node:fs'
|
|
10
|
+
|
|
11
|
+
const ROUTES_MODULE_ID = 'virtual:librex-routes'
|
|
12
|
+
const RESOLVED_ROUTES_MODULE_ID = '\0' + ROUTES_MODULE_ID
|
|
13
|
+
|
|
14
|
+
interface BrickRouteRecord {
|
|
15
|
+
path: string
|
|
16
|
+
name: string
|
|
17
|
+
title: string
|
|
18
|
+
icon: string
|
|
19
|
+
navGroup?: string
|
|
20
|
+
navOrder?: number
|
|
21
|
+
permissions: string[]
|
|
22
|
+
pageDir: string
|
|
23
|
+
/** pageConfig.ts 文件的绝对路径 */
|
|
24
|
+
configPath: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function vitePluginLibrex(): Plugin {
|
|
28
|
+
let pagesDir: string = ''
|
|
29
|
+
let brickRoutes: BrickRouteRecord[] = []
|
|
30
|
+
|
|
31
|
+
function scanPages(root: string): { brickRoutes: BrickRouteRecord[] } {
|
|
32
|
+
const dir = path.resolve(root, 'src/pages')
|
|
33
|
+
pagesDir = dir
|
|
34
|
+
|
|
35
|
+
if (!fs.existsSync(dir)) return { brickRoutes: [] }
|
|
36
|
+
|
|
37
|
+
const brickList: BrickRouteRecord[] = []
|
|
38
|
+
|
|
39
|
+
/** 递归扫描目录 */
|
|
40
|
+
function walk(currentDir: string, prefix: string) {
|
|
41
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true })
|
|
42
|
+
|
|
43
|
+
const relativeDir = path.relative(dir, currentDir)
|
|
44
|
+
const pageConfigPath = path.join(currentDir, 'pageConfig.ts')
|
|
45
|
+
|
|
46
|
+
// ── pageConfig.ts → Brick 路由 ──
|
|
47
|
+
if (fs.existsSync(pageConfigPath)) {
|
|
48
|
+
const relDir = relativeDir.replace(/\\/g, '/')
|
|
49
|
+
if (!relDir.startsWith('_') && relDir !== 'notFound') {
|
|
50
|
+
try {
|
|
51
|
+
const raw = fs.readFileSync(pageConfigPath, 'utf-8')
|
|
52
|
+
const meta = extractPageConfigMeta(raw)
|
|
53
|
+
if (meta) {
|
|
54
|
+
const rawPath = meta.path || (prefix ? `/${prefix.replace(/\\/g, '/')}` : '/')
|
|
55
|
+
const routePath = rawPath.startsWith('/') ? rawPath : `/${rawPath}`
|
|
56
|
+
brickList.push({
|
|
57
|
+
path: routePath,
|
|
58
|
+
name: routePath.replace(/^\//, '').replace(/\/:?/g, '-'),
|
|
59
|
+
title: meta.title || path.basename(currentDir),
|
|
60
|
+
icon: meta.icon || 'file',
|
|
61
|
+
navGroup: meta.navGroup,
|
|
62
|
+
navOrder: meta.navOrder,
|
|
63
|
+
permissions: meta.permissions || [],
|
|
64
|
+
pageDir: relDir,
|
|
65
|
+
configPath: pageConfigPath,
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
} catch (e) {
|
|
69
|
+
console.error(`[vite-plugin-librex] 解析 pageConfig 失败: ${pageConfigPath}`, e)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 递归子目录
|
|
75
|
+
for (const entry of entries) {
|
|
76
|
+
if (!entry.isDirectory()) continue
|
|
77
|
+
if (entry.name.startsWith('_')) continue
|
|
78
|
+
if (entry.name === 'notFound') continue
|
|
79
|
+
walk(path.join(currentDir, entry.name), prefix ? `${prefix}/${entry.name}` : entry.name)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
walk(dir, '')
|
|
84
|
+
|
|
85
|
+
// 排序
|
|
86
|
+
brickList.sort((a, b) => (a.navOrder ?? 999) - (b.navOrder ?? 999))
|
|
87
|
+
|
|
88
|
+
// ── 自动分组 ──
|
|
89
|
+
autoGroupBrickRoutes(brickList)
|
|
90
|
+
|
|
91
|
+
return { brickRoutes: brickList }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Brick 路由自动分组 */
|
|
95
|
+
function autoGroupBrickRoutes(r: BrickRouteRecord[]) {
|
|
96
|
+
for (const route of r) {
|
|
97
|
+
const lastSlash = route.path.lastIndexOf('/')
|
|
98
|
+
if (lastSlash <= 0) continue
|
|
99
|
+
const parentPath = route.path.slice(0, lastSlash)
|
|
100
|
+
const parent = r.find(rr => rr.path === parentPath)
|
|
101
|
+
if (!parent) continue
|
|
102
|
+
const groupName = parent.navGroup || `${parent.icon} ${parent.title}`
|
|
103
|
+
if (!route.navGroup) route.navGroup = groupName
|
|
104
|
+
if (!parent.navGroup) parent.navGroup = groupName
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function generateRoutesModule(brick: BrickRouteRecord[]): string {
|
|
109
|
+
const parts: string[] = [
|
|
110
|
+
'// Auto-generated by vite-plugin-librex — DO NOT EDIT',
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
// ── Brick 路由 ──
|
|
114
|
+
parts.push('')
|
|
115
|
+
parts.push("import { PageContainer } from 'librex'")
|
|
116
|
+
|
|
117
|
+
const brickDefs: string[] = []
|
|
118
|
+
brick.forEach((route, i) => {
|
|
119
|
+
parts.push(`import __brick_cfg_${i} from '${toFsPath(route.configPath)}'`)
|
|
120
|
+
|
|
121
|
+
const metaFields: string[] = [
|
|
122
|
+
`title: '${escapeStr(route.title)}'`,
|
|
123
|
+
`icon: '${escapeStr(route.icon)}'`,
|
|
124
|
+
`pageConfig: __brick_cfg_${i}`,
|
|
125
|
+
`pageDir: '${escapeStr(route.pageDir)}'`,
|
|
126
|
+
]
|
|
127
|
+
if (route.navGroup) metaFields.push(`navGroup: '${escapeStr(route.navGroup)}'`)
|
|
128
|
+
if (route.navOrder !== undefined) metaFields.push(`navOrder: ${route.navOrder}`)
|
|
129
|
+
metaFields.push(`permissions: ${JSON.stringify(route.permissions)}`)
|
|
130
|
+
|
|
131
|
+
brickDefs.push(` { path: '${route.path}', name: '${route.name}', component: PageContainer, meta: { ${metaFields.join(', ')} } }`)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
parts.push('')
|
|
135
|
+
parts.push('export const brickRoutes = [')
|
|
136
|
+
parts.push(brickDefs.join(',\n'))
|
|
137
|
+
parts.push(']')
|
|
138
|
+
|
|
139
|
+
return parts.join('\n')
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
name: 'vite-plugin-librex',
|
|
144
|
+
enforce: 'pre',
|
|
145
|
+
|
|
146
|
+
configResolved(config) {
|
|
147
|
+
const result = scanPages(config.root)
|
|
148
|
+
brickRoutes = result.brickRoutes
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
resolveId(id) {
|
|
152
|
+
if (id === ROUTES_MODULE_ID) return RESOLVED_ROUTES_MODULE_ID
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
load(id) {
|
|
156
|
+
if (id === RESOLVED_ROUTES_MODULE_ID) {
|
|
157
|
+
return generateRoutesModule(brickRoutes)
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
configureServer(server: ViteDevServer) {
|
|
162
|
+
const watchPattern = pagesDir
|
|
163
|
+
? path.join(pagesDir, '**/*.{vue,ts}')
|
|
164
|
+
: 'src/pages/**/*.{vue,ts}'
|
|
165
|
+
|
|
166
|
+
const onPageChange = (filePath: string) => {
|
|
167
|
+
if (!isPageFile(filePath)) return
|
|
168
|
+
const result = scanPages(server.config.root)
|
|
169
|
+
brickRoutes = result.brickRoutes
|
|
170
|
+
reloadVirtualModule(server)
|
|
171
|
+
server.ws.send({ type: 'full-reload' })
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
server.watcher.on('add', onPageChange)
|
|
175
|
+
server.watcher.on('change', onPageChange)
|
|
176
|
+
server.watcher.on('unlink', onPageChange)
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
buildStart() {
|
|
180
|
+
// routes 已在 configResolved 中初始化
|
|
181
|
+
},
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function isPageFile(filePath: string): boolean {
|
|
186
|
+
const normalized = filePath.replace(/\\/g, '/')
|
|
187
|
+
return /\/pages\/(?!.*(?:\/|^)_)[^/]+(?:\/[^/]+)*\/(?:index\.vue|pageConfig\.ts)$/.test(normalized)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** 从 .ts 文件中提取 definePageConfig({...}) 元数据 */
|
|
191
|
+
function extractPageConfigMeta(raw: string): Record<string, any> | null {
|
|
192
|
+
const braced = extractBracedCall(raw, 'definePageConfig')
|
|
193
|
+
if (!braced) return null
|
|
194
|
+
const fields = ['title', 'path', 'icon', 'navGroup', 'navOrder', 'permissions']
|
|
195
|
+
const result = extractMetaFields(braced, fields)
|
|
196
|
+
return Object.keys(result).length > 0 ? result : null
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* 从对象字面量文本中提取顶层字段值(不支持嵌套 JSON 解析,仅元数据提取)
|
|
201
|
+
* 可处理包含 function 体 / 嵌套对象的 definePageConfig({...})。
|
|
202
|
+
*/
|
|
203
|
+
function extractMetaFields(raw: string, fields: string[]): Record<string, any> {
|
|
204
|
+
const result: Record<string, any> = {}
|
|
205
|
+
for (const field of fields) {
|
|
206
|
+
// 匹配 fieldName: value — value 可以是数组、字符串、数字、布尔
|
|
207
|
+
const patterns = [
|
|
208
|
+
new RegExp(`["']?${field}["']?\\s*:\\s*(\\[[^\\]]*\\])`, 'm'), // 数组(先匹配)
|
|
209
|
+
new RegExp(`["']?${field}["']?\\s*:\\s*["']([^"']*)["']`, 'm'), // 字符串
|
|
210
|
+
new RegExp(`["']?${field}["']?\\s*:\\s*(\\d+)`, 'm'), // 数字
|
|
211
|
+
new RegExp(`["']?${field}["']?\\s*:\\s*(true|false)`, 'm'), // 布尔
|
|
212
|
+
]
|
|
213
|
+
for (const re of patterns) {
|
|
214
|
+
const m = raw.match(re)
|
|
215
|
+
if (m) {
|
|
216
|
+
const v = m[1]
|
|
217
|
+
if (v === 'true') result[field] = true
|
|
218
|
+
else if (v === 'false') result[field] = false
|
|
219
|
+
else if (/^\d+$/.test(v)) result[field] = Number(v)
|
|
220
|
+
else if (v.startsWith('[')) {
|
|
221
|
+
try { result[field] = JSON.parse(v.replace(/'/g, '"')) } catch (e) { result[field] = []; console.error(`[vite-plugin-librex] JSON.parse 数组字段 ${field} 失败:`, v, e) }
|
|
222
|
+
} else result[field] = v
|
|
223
|
+
break
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return result
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* 从文本中匹配 defineXxx({...}) 调用并返回花括号内的内容。
|
|
232
|
+
* 用贪婪匹配 `[\s\S]*` 加括号计数,正确处理 `setPageHooks({...})` 等嵌套。
|
|
233
|
+
*/
|
|
234
|
+
function extractBracedCall(raw: string, fnName: string): string | null {
|
|
235
|
+
const re = new RegExp(fnName + '\\s*\\(\\s*(\\{)', 'm')
|
|
236
|
+
const match = raw.match(re)
|
|
237
|
+
if (!match || match.index === undefined) return null
|
|
238
|
+
|
|
239
|
+
let depth = 0
|
|
240
|
+
let start = match.index + match[0].length - 1 // position of the opening '{'
|
|
241
|
+
let end = -1
|
|
242
|
+
let inString: string | null = null
|
|
243
|
+
let inComment: '//' | '/*' | null = null
|
|
244
|
+
|
|
245
|
+
for (let i = start; i < raw.length; i++) {
|
|
246
|
+
const ch = raw[i]
|
|
247
|
+
const next = raw[i + 1] || ''
|
|
248
|
+
|
|
249
|
+
if (inComment === '//') {
|
|
250
|
+
if (ch === '\n') inComment = null
|
|
251
|
+
continue
|
|
252
|
+
}
|
|
253
|
+
if (inComment === '/*') {
|
|
254
|
+
if (ch === '*' && next === '/') inComment = null
|
|
255
|
+
continue
|
|
256
|
+
}
|
|
257
|
+
if (ch === '/' && next === '/') { inComment = '//'; continue }
|
|
258
|
+
if (ch === '/' && next === '*') { inComment = '/*'; continue }
|
|
259
|
+
|
|
260
|
+
if (inString) {
|
|
261
|
+
if (ch === '\\') { i++; continue } // escape
|
|
262
|
+
if (ch === inString) inString = null
|
|
263
|
+
continue
|
|
264
|
+
}
|
|
265
|
+
if (ch === "'" || ch === '"' || ch === '`') { inString = ch; continue }
|
|
266
|
+
|
|
267
|
+
if (ch === '{') depth++
|
|
268
|
+
else if (ch === '}') {
|
|
269
|
+
depth--
|
|
270
|
+
if (depth === 0) { end = i; break }
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (end === -1) return null
|
|
275
|
+
return raw.slice(start, end + 1)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function escapeStr(s: string): string {
|
|
279
|
+
return s.replace(/\\/g, '\\\\').replace(/'/g, "\\'")
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function toFsPath(p: string): string {
|
|
283
|
+
const n = p.replace(/\\/g, '/')
|
|
284
|
+
return `/@fs${n.startsWith('/') ? '' : '/'}${n}`
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function reloadVirtualModule(server: ViteDevServer) {
|
|
288
|
+
const mod = server.moduleGraph.getModuleById(RESOLVED_ROUTES_MODULE_ID)
|
|
289
|
+
if (mod) {
|
|
290
|
+
server.moduleGraph.invalidateModule(mod)
|
|
291
|
+
server.reloadModule(mod)
|
|
292
|
+
}
|
|
293
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { defineConfig, loadEnv } from 'vite'
|
|
2
|
+
import vue from '@vitejs/plugin-vue'
|
|
3
|
+
import { fileURLToPath, URL } from 'node:url'
|
|
4
|
+
import { vitePluginLibrex } from './vite-plugin-librex'
|
|
5
|
+
|
|
6
|
+
export default defineConfig(({ mode }) => {
|
|
7
|
+
const env = loadEnv(mode, process.cwd(), '')
|
|
8
|
+
return {
|
|
9
|
+
server: {
|
|
10
|
+
proxy: {
|
|
11
|
+
'/api': {
|
|
12
|
+
target: env.VITE_PROXY_TARGET || 'http://localhost:3000',
|
|
13
|
+
changeOrigin: true,
|
|
14
|
+
secure: false,
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
plugins: [
|
|
19
|
+
vitePluginLibrex(),
|
|
20
|
+
vue(),
|
|
21
|
+
],
|
|
22
|
+
resolve: {
|
|
23
|
+
alias: {
|
|
24
|
+
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
css: {
|
|
28
|
+
preprocessorOptions: {
|
|
29
|
+
less: {
|
|
30
|
+
javascriptEnabled: true,
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
})
|