import fs from 'node:fs'; import path from 'node:path'; import { createRequire } from 'node:module'; import { fileURLToPath } from 'node:url'; const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const sourceRoot = path.resolve(process.env.XIAOSHU_UNIAPP_ROOT || path.join(projectRoot, '..', 'xiaoshu-uniapp')); const sourceRequire = createRequire(path.join(sourceRoot, 'package.json')); const JSON5 = sourceRequire('json5'); const { parseComponent } = sourceRequire('vue-template-compiler'); const parser = sourceRequire('@babel/parser'); const pagesJson = JSON5.parse(fs.readFileSync(path.join(sourceRoot, 'pages.json'), 'utf8')); const pages = [ ...(pagesJson.pages || []).map((page) => page.path), ...(pagesJson.subPackages || pagesJson.subpackages || []).flatMap((group) => (group.pages || []).map((page) => `${group.root}/${page.path}`), ), ]; const rows = pages.map((pagePath) => inspectPage(pagePath)); const actions = [...new Set(rows.flatMap((page) => page.apiCalls.map((call) => call.action)).filter((action) => action !== ''))].sort(); const externalCalls = [...new Set(rows.flatMap((page) => page.externalCalls.map((call) => `${call.service}.${call.operation}`)))].sort(); const outputPath = path.join(projectRoot, 'projects/xiaoshu-mobile/src/app/core/source-parity.generated.ts'); const output = `// Generated by npm run parity:generate. Do not edit manually.\n` + `export const SOURCE_PAGE_PARITY = ${JSON.stringify(rows, null, 2)} as const;\n\n` + `export const SOURCE_API_ACTIONS = ${JSON.stringify(actions, null, 2)} as const;\n\n` + `export const SOURCE_EXTERNAL_CALLS = ${JSON.stringify(externalCalls, null, 2)} as const;\n`; if (process.argv.includes('--check')) { const current = fs.existsSync(outputPath) ? fs.readFileSync(outputPath, 'utf8') : ''; if (current !== output) { console.error('Source parity manifest is stale. Run npm run parity:generate.'); process.exitCode = 1; } } else { fs.writeFileSync(outputPath, output); console.log(`Generated ${rows.length} pages and ${actions.length} API actions.`); } function inspectPage(pagePath) { const file = path.join(sourceRoot, `${pagePath}.vue`); const source = fs.readFileSync(file, 'utf8'); const descriptor = parseComponent(source, { pad: 'line' }); const script = descriptor.script?.content || ''; const ast = parser.parse(script, { sourceType: 'module', plugins: ['optionalChaining', 'objectRestSpread'] }); const apiCalls = []; const externalCalls = []; const methods = []; const navigations = []; walk(ast, (node) => { if (node.type === 'ObjectMethod' && node.key?.type === 'Identifier') methods.push(node.key.name); if (node.type !== 'CallExpression') return; const callee = memberName(node.callee); const first = node.arguments?.[0]; const literal = first?.type === 'StringLiteral' ? first.value : ''; if (/(?:uni\.\$u\.http|this\.\$u\.http)\.(get|post)$/.test(callee)) { apiCalls.push({ method: callee.endsWith('.post') ? 'post' : 'get', action: literal, line: node.loc?.start.line || 0 }); } if (/^httpApi\.[A-Za-z0-9_]+$/.test(callee)) { externalCalls.push({ service: 'bytedesk', operation: callee.slice('httpApi.'.length), line: node.loc?.start.line || 0 }); } if (/(?:navigateTo|redirectTo|switchTab|reLaunch|\.route)$/.test(callee)) { navigations.push({ call: callee, target: literal, line: node.loc?.start.line || 0 }); } }); return { path: pagePath, sourceFile: `${pagePath}.vue`, apiCalls: unique(apiCalls, (item) => `${item.method}:${item.action}:${item.line}`), externalCalls: unique(externalCalls, (item) => `${item.service}:${item.operation}:${item.line}`), methods: [...new Set(methods)], navigations, }; } function memberName(node) { if (!node) return ''; if (node.type === 'Identifier') return node.name; if (node.type === 'ThisExpression') return 'this'; if (node.type === 'MemberExpression' || node.type === 'OptionalMemberExpression') { const property = node.computed ? node.property?.value ?? memberName(node.property) : memberName(node.property); return `${memberName(node.object)}.${property}`; } return ''; } function walk(node, visit) { if (!node || typeof node !== 'object') return; visit(node); for (const [key, value] of Object.entries(node)) { if (['loc', 'start', 'end', 'extra'].includes(key)) continue; if (Array.isArray(value)) value.forEach((entry) => walk(entry, visit)); else if (value?.type) walk(value, visit); } } function unique(items, key) { return [...new Map(items.map((item) => [key(item), item])).values()]; }