| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899 |
- 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 !== '<dynamic>'))].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 : '<dynamic>';
- 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()];
- }
|