diff --git a/frontend-source/scripts/extract-router.js b/frontend-source/scripts/extract-router.js new file mode 100644 index 00000000..e125bd5d --- /dev/null +++ b/frontend-source/scripts/extract-router.js @@ -0,0 +1,116 @@ +#!/usr/bin/env node +/** + * 路由表提取器 + * 从格式化后的 JS 中提取 vue-router 的 routes 定义 + * + * 用法: node extract-router.js + */ + +const fs = require('fs'); +const path = require('path'); +const parser = require('@babel/parser'); +const traverse = require('@babel/traverse').default; + +function extractRouter(inputDir, outputFile) { + const files = fs.readdirSync(inputDir).filter(f => f.endsWith('.formatted.js')); + const routes = []; + + for (const file of files) { + const code = fs.readFileSync(path.join(inputDir, file), 'utf-8'); + + try { + const ast = parser.parse(code, { + sourceType: 'script', + plugins: ['jsx', 'typescript', 'classProperties', 'dynamicImport'], + errorRecovery: true, + }); + + traverse(ast, { + // Match routes: [...] arrays containing path/component objects + ArrayExpression(nodePath) { + const node = nodePath.node; + + const hasRoutes = node.elements.some(el => + el.type === 'ObjectExpression' && + el.properties.some(prop => prop.key && prop.key.name === 'path') + ); + + if (hasRoutes) { + node.elements.forEach(el => { + if (el.type === 'ObjectExpression') { + const routeDef = {}; + el.properties.forEach(prop => { + if (prop.key && prop.key.type === 'Identifier') { + if (prop.key.name === 'path' && prop.value.type === 'StringLiteral') { + routeDef.path = prop.value.value; + } + if (prop.key.name === 'name' && prop.value.type === 'StringLiteral') { + routeDef.name = prop.value.value; + } + if (prop.key.name === 'meta' && prop.value.type === 'ObjectExpression') { + routeDef.meta = {}; + prop.value.properties.forEach(mp => { + if (mp.key && mp.key.type === 'Identifier') { + routeDef.meta[mp.key.name] = mp.value.type === 'StringLiteral' ? mp.value.value : true; + } + }); + } + if (prop.key.name === 'component') { + routeDef.component = prop.value.type === 'ArrowFunctionExpression' + ? 'lazyImport' + : (prop.value.type === 'Identifier' ? prop.value.name : 'inline'); + } + } + }); + + if (routeDef.path && !routes.find(r => r.path === routeDef.path)) { + routes.push(routeDef); + } + } + }); + } + }, + + // Match path-like strings: /xxx/xxx + StringLiteral(nodePath) { + const value = nodePath.node.value; + if (typeof value === 'string' && /^\/[a-z][a-z0-9_/-]*$/i.test(value) && value.length > 2) { + const parent = nodePath.parent; + if (parent.type === 'ObjectProperty' && parent.key && parent.key.name === 'path') { + return; // Already handled above + } + if (!routes.find(r => r.path === value)) { + routes.push({ path: value, source: 'stringLiteral', file: file }); + } + } + }, + }); + } catch (e) { + // Skip unparseable files + } + } + + const result = routes.sort((a, b) => (a.path || '').localeCompare(b.path || '')); + + fs.writeFileSync(outputFile, JSON.stringify(result, null, 2)); + console.log(`提取完成: ${result.length} 条路由 → ${outputFile}`); + + // Generate Markdown route table + const mdPath = outputFile.replace('.json', '.md'); + let md = '# 路由表\n\n'; + md += `| Path | Name | Component | Meta |\n`; + md += `|------|------|-----------|------|\n`; + result.forEach(r => { + md += `| \`${r.path || '-'}\` | ${r.name || '-'} | ${r.component || '-'} | ${r.meta ? JSON.stringify(r.meta) : '-'} |\n`; + }); + fs.writeFileSync(mdPath, md); + console.log(`路由表 Markdown: ${mdPath}`); +} + +const args = process.argv.slice(2); +if (args.length < 2) { + console.log('用法: node extract-router.js '); + process.exit(1); +} + +extractRouter(args[0], args[1]);