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.
@@ -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
+ })