@ohos-cpf/3rdloop 0.0.2 → 0.0.3

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.
Files changed (393) hide show
  1. package/README.md +244 -244
  2. package/bin/3rdloop.mjs +46 -46
  3. package/lib/cli.js +781 -781
  4. package/lib/config-cmd.js +451 -451
  5. package/lib/config.js +318 -318
  6. package/lib/doctor.js +113 -113
  7. package/lib/exit-codes.js +30 -30
  8. package/lib/opencode.js +155 -155
  9. package/lib/orch.js +426 -426
  10. package/lib/queries.js +263 -263
  11. package/lib/step.js +325 -325
  12. package/lib/ui.js +222 -222
  13. package/lib/update.js +279 -279
  14. package/lib/workflow.js +308 -308
  15. package/lib/workflows.js +131 -131
  16. package/package.json +50 -50
  17. package/vendor/Server/Agent/SkillSelector/README.md +343 -343
  18. package/vendor/Server/Agent/SkillSelector/core/Agent.js +207 -207
  19. package/vendor/Server/Agent/SkillSelector/index.js +14 -14
  20. package/vendor/Server/Agent/SkillSelector/llm/llmClient.js +313 -313
  21. package/vendor/Server/Agent/SkillSelector/llm/prompts.js +161 -161
  22. package/vendor/Server/Agent/SkillSelector/llm/reactSteps.js +144 -144
  23. package/vendor/Server/Agent/SkillSelector/matcher/fuzzyMatcher.js +433 -433
  24. package/vendor/Server/Agent/SkillSelector/scanner/skillScanner.js +382 -382
  25. package/vendor/Server/Agent/SkillSelector/utils/actionWords.js +59 -59
  26. package/vendor/Server/Brain/AnalysisStrategies.js +422 -422
  27. package/vendor/Server/Brain/Brain.js +686 -686
  28. package/vendor/Server/Brain/DecompositionStrategies.js +621 -621
  29. package/vendor/Server/Brain/PlanPersistence.js +196 -196
  30. package/vendor/Server/Brain/SkillExecutor.js +251 -251
  31. package/vendor/Server/Brain/index.js +11 -11
  32. package/vendor/Server/CLI/cli.js +275 -275
  33. package/vendor/Server/CLI/deveco-code/index.js +71 -71
  34. package/vendor/Server/CLI/deveco-code.js +5 -5
  35. package/vendor/Server/CLI/opencode/index.js +817 -818
  36. package/vendor/Server/CLI/opencode.js +5 -5
  37. package/vendor/Server/DbUse/MessageArchive.js +61 -61
  38. package/vendor/Server/DbUse/StorageManager.js +107 -107
  39. package/vendor/Server/DbUse/adapters/BaseAdapter.js +94 -94
  40. package/vendor/Server/DbUse/adapters/FileAdapter.js +138 -138
  41. package/vendor/Server/DbUse/adapters/JsonAdapter.js +138 -138
  42. package/vendor/Server/DbUse/adapters/JsonlAdapter.js +114 -114
  43. package/vendor/Server/DbUse/adapters/SqliteAdapter.js +157 -157
  44. package/vendor/Server/DbUse/index.js +160 -160
  45. package/vendor/Server/DbUse/stores/SessionStore.js +279 -279
  46. package/vendor/Server/DbUse/stores/TaskExecutionStore.js +133 -133
  47. package/vendor/Server/DbUse/stores/TaskRegistryStore.js +262 -262
  48. package/vendor/Server/DbUse/stores/TaskStepStore.js +279 -279
  49. package/vendor/Server/FlexRunner/FlexRunner.js +1759 -1759
  50. package/vendor/Server/FlexRunner/index.js +12 -12
  51. package/vendor/Server/Knowledge/KnowledgeImporter.js +523 -523
  52. package/vendor/Server/Knowledge/KnowledgeManager.js +341 -341
  53. package/vendor/Server/Knowledge/index.js +33 -33
  54. package/vendor/Server/LoopEngine/LoopEngine.js +1133 -1133
  55. package/vendor/Server/LoopEngine/index.js +11 -11
  56. package/vendor/Server/Orchestrator/Orchestrator.js +0 -3
  57. package/vendor/Server/Orchestrator/StepNode.js +312 -312
  58. package/vendor/Server/Orchestrator/index.js +13 -13
  59. package/vendor/Server/Routes/controllers/BrainController.js +241 -241
  60. package/vendor/Server/Routes/controllers/FlexRunnerController.js +1125 -1125
  61. package/vendor/Server/Routes/controllers/HealthController.js +44 -44
  62. package/vendor/Server/Routes/controllers/KnowledgeImportController.js +355 -355
  63. package/vendor/Server/Routes/controllers/LoopEngineController.js +788 -788
  64. package/vendor/Server/Routes/controllers/StatsController.js +57 -57
  65. package/vendor/Server/Routes/controllers/StreamController.js +67 -67
  66. package/vendor/Server/Routes/library/logger.js +179 -179
  67. package/vendor/Server/Routes/library/zipArchive.js +172 -172
  68. package/vendor/Server/Routes/router/Router.js +204 -204
  69. package/vendor/Server/Routes/routes/brain.js +47 -47
  70. package/vendor/Server/Routes/routes/flexrunner.js +72 -72
  71. package/vendor/Server/Routes/routes/health.js +28 -28
  72. package/vendor/Server/Routes/routes/index.js +57 -57
  73. package/vendor/Server/Routes/routes/knowledge.js +55 -55
  74. package/vendor/Server/Routes/routes/loopengine.js +58 -58
  75. package/vendor/Server/Routes/routes/orchestrator.js +61 -61
  76. package/vendor/Server/Routes/routes/registry.js +97 -97
  77. package/vendor/Server/Routes/routes/stats.js +23 -23
  78. package/vendor/Server/Routes/routes/stream.js +30 -30
  79. package/vendor/Server/Routes/server.js +203 -203
  80. package/vendor/Server/Routes/sse/SseHub.js +308 -308
  81. package/vendor/Server/Skills/acceptance-check/SKILL.md +191 -191
  82. package/vendor/Server/Skills/acceptance-check/references/PHASE1_ARTIFACT_COLLECTION.md +52 -52
  83. package/vendor/Server/Skills/acceptance-check/references/PHASE2_CRITERIA_EVALUATION.md +77 -77
  84. package/vendor/Server/Skills/acceptance-check/references/PHASE2_EVIDENCE_VERIFICATION.md +70 -70
  85. package/vendor/Server/Skills/arkts-code-check/SKILL.md +320 -320
  86. package/vendor/Server/Skills/arkts-code-check/references/CODE_CHECK_GUIDE.md +210 -210
  87. package/vendor/Server/Skills/arkts-code-check/references/FILE_TYPE_GUIDE.md +28 -28
  88. package/vendor/Server/Skills/arkts-code-check/references/PHASE1_INFO_COLLECTION.md +233 -233
  89. package/vendor/Server/Skills/arkts-code-check/references/REPORT_TEMPLATE.md +219 -219
  90. package/vendor/Server/Skills/arkts-library-compile-validate/SKILL.md +328 -328
  91. package/vendor/Server/Skills/arkts-library-compile-validate/scripts/build.ps1 +93 -93
  92. package/vendor/Server/Skills/arkts-library-compile-validate/scripts/code-linter.cjs +432 -432
  93. package/vendor/Server/Skills/arkts-library-compile-validate/scripts/migrate.cjs +851 -851
  94. package/vendor/Server/Skills/arkts-library-compile-validate/scripts/ohpm-switch.cjs +788 -788
  95. package/vendor/Server/Skills/arkts-library-demo-code/SKILL.md +291 -291
  96. package/vendor/Server/Skills/arkts-library-demo-code/references/API_SIGNATURE_CHECKS.md +68 -68
  97. package/vendor/Server/Skills/arkts-library-demo-code/references/BUILD_VALIDATION.md +184 -184
  98. package/vendor/Server/Skills/arkts-library-demo-code/references/CODE_STANDARDS.md +337 -337
  99. package/vendor/Server/Skills/arkts-library-demo-code/references/EXAMPLES.md +400 -400
  100. package/vendor/Server/Skills/arkts-library-demo-code/references/SYSTEM_API_LOOKUP.md +98 -98
  101. package/vendor/Server/Skills/arkts-library-demo-code/references/TEMPLATE.md +544 -544
  102. package/vendor/Server/Skills/arkts-library-demo-code/scripts/build.ps1 +93 -93
  103. package/vendor/Server/Skills/arkts-library-demo-code/scripts/code-linter.cjs +432 -432
  104. package/vendor/Server/Skills/arkts-library-demo-code/scripts/migrate.cjs +851 -851
  105. package/vendor/Server/Skills/arkts-library-demo-code/scripts/ohpm-switch.cjs +788 -788
  106. package/vendor/Server/Skills/arkts-library-demo-coverage/SKILL.md +174 -174
  107. package/vendor/Server/Skills/arkts-library-demo-scenario/SKILL.md +321 -321
  108. package/vendor/Server/Skills/arkts-library-demo-scenario/references/EXAMPLES.md +100 -100
  109. package/vendor/Server/Skills/arkts-library-demo-scenario/references/TEMPLATE.md +185 -185
  110. package/vendor/Server/Skills/arkts-library-documentation-quality-check/SKILL.md +233 -233
  111. package/vendor/Server/Skills/arkts-library-documentation-quality-check/assets/REPORT_TEMPLATE.md +102 -102
  112. package/vendor/Server/Skills/arkts-library-documentation-quality-check/references/CHECK_DIMENSIONS.md +89 -89
  113. package/vendor/Server/Skills/arkts-library-documentation-quality-check/references/COMPLIANCE_CHECKLIST.md +62 -62
  114. package/vendor/Server/Skills/arkts-library-documentation-quality-check/references/KB_QUERY_GUIDE.md +107 -107
  115. package/vendor/Server/Skills/arkts-library-documentation-quality-check/references/MCP_TOOL_DISCOVERY.md +124 -124
  116. package/vendor/Server/Skills/arkts-library-documentation-quality-check/references/REPORT_TEMPLATE.md +90 -90
  117. package/vendor/Server/Skills/arkts-library-documentation-quality-check/references/SCORING_FORMULA.md +57 -57
  118. package/vendor/Server/Skills/arkts-library-documentation-quality-check/scripts/validate-skill.cjs +259 -259
  119. package/vendor/Server/Skills/arkts-library-interface-spec/SKILL.md +97 -97
  120. package/vendor/Server/Skills/arkts-library-memory-leak-check/SKILL.md +391 -391
  121. package/vendor/Server/Skills/arkts-library-source-clone/SKILL.md +113 -113
  122. package/vendor/Server/Skills/arkts-library-stability-check/SKILL.md +312 -312
  123. package/vendor/Server/Skills/arkts-library-stability-check/assets/REPORT_TEMPLATE.md +154 -154
  124. package/vendor/Server/Skills/arkts-library-stability-check/references/CHECKLIST.md +165 -165
  125. package/vendor/Server/Skills/arkts-library-stability-check/references/CODE_EXAMPLES.md +308 -308
  126. package/vendor/Server/Skills/arkts-library-stability-check/references/SCORING_RULES.md +90 -90
  127. package/vendor/Server/Skills/arkts-library-stability-check/scripts/validate-skill.cjs +282 -282
  128. package/vendor/Server/Skills/arkts-library-test-coverage-check/SKILL.md +360 -360
  129. package/vendor/Server/Skills/arkts-library-test-coverage-check/assets/REPORT_TEMPLATE.md +145 -145
  130. package/vendor/Server/Skills/arkts-library-test-coverage-check/references/CHECKLIST.md +252 -252
  131. package/vendor/Server/Skills/arkts-library-test-coverage-check/references/MCP_TOOL_USAGE.md +114 -114
  132. package/vendor/Server/Skills/arkts-library-test-coverage-check/scripts/validate-skill.cjs +262 -262
  133. package/vendor/Server/Skills/arkts-library-xts-code/SKILL.md +455 -455
  134. package/vendor/Server/Skills/arkts-library-xts-code/assets/report-template.md +60 -60
  135. package/vendor/Server/Skills/arkts-library-xts-code/references/ACCESS_MODIFIER_PRECHECK.md +76 -76
  136. package/vendor/Server/Skills/arkts-library-xts-code/references/BUILD_VERIFICATION.md +200 -200
  137. package/vendor/Server/Skills/arkts-library-xts-code/references/CODE_STANDARDS.md +518 -518
  138. package/vendor/Server/Skills/arkts-library-xts-code/references/SUBAGENT_PROMPT_TEMPLATE.md +276 -276
  139. package/vendor/Server/Skills/arkts-library-xts-code/references/SYSTEM_API_LOOKUP.md +92 -92
  140. package/vendor/Server/Skills/arkts-library-xts-code/references/XTS_TEMPLATE.md +767 -767
  141. package/vendor/Server/Skills/arkts-library-xts-code/scripts/build.ps1 +93 -93
  142. package/vendor/Server/Skills/arkts-library-xts-code/scripts/code-linter.cjs +432 -432
  143. package/vendor/Server/Skills/arkts-library-xts-code/scripts/migrate.cjs +851 -851
  144. package/vendor/Server/Skills/arkts-library-xts-code/scripts/ohpm-switch.cjs +788 -788
  145. package/vendor/Server/Skills/arkts-library-xts-code/scripts/precheck-access-modifiers.cjs +221 -221
  146. package/vendor/Server/Skills/arkts-library-xts-code-fix/SKILL.md +228 -228
  147. package/vendor/Server/Skills/arkts-library-xts-coverage/SKILL.md +97 -97
  148. package/vendor/Server/Skills/arkts-library-xts-doc/SKILL.md +194 -194
  149. package/vendor/Server/Skills/arkts-library-xts-doc/references/EXAMPLES.md +575 -575
  150. package/vendor/Server/Skills/arkts-library-xts-doc/references/TEMPLATE.md +574 -574
  151. package/vendor/Server/Skills/arkts-library-xts-doc/references/TESTABILITY_GUIDE.md +182 -182
  152. package/vendor/Server/Skills/arkts-library-xts-execution/SKILL.md +490 -490
  153. package/vendor/Server/Skills/arkts-library-xts-execution/references/error-fixing-examples.md +217 -217
  154. package/vendor/Server/Skills/arkts-library-xts-execution/scripts/build.ps1 +93 -93
  155. package/vendor/Server/Skills/arkts-library-xts-execution/scripts/code-linter.cjs +432 -432
  156. package/vendor/Server/Skills/arkts-library-xts-execution/scripts/migrate.cjs +851 -851
  157. package/vendor/Server/Skills/arkts-library-xts-execution/scripts/ohpm-switch.cjs +788 -788
  158. package/vendor/Server/Skills/flutter-code-check/SKILL.md +299 -299
  159. package/vendor/Server/Skills/flutter-code-check/assets/OUTPUT_TEMPLATE.md +311 -311
  160. package/vendor/Server/Skills/flutter-code-check/references/CHECKLIST.md +190 -190
  161. package/vendor/Server/Skills/flutter-code-check/references/PLATFORMS.md +133 -133
  162. package/vendor/Server/Skills/flutter-code-check/scripts/validate-skill.cjs +259 -259
  163. package/vendor/Server/Skills/flutter-interface/README.md +75 -75
  164. package/vendor/Server/Skills/flutter-interface/SKILL.md +291 -291
  165. package/vendor/Server/Skills/flutter-interface/references/EXAMPLES.md +336 -336
  166. package/vendor/Server/Skills/flutter-interface/references/GIT_CLONE_GUIDE.md +190 -190
  167. package/vendor/Server/Skills/flutter-interface/references/SCRIPT_REFERENCE.md +258 -258
  168. package/vendor/Server/Skills/flutter-interface/references/TEMPLATE.md +254 -254
  169. package/vendor/Server/Skills/flutter-library-consistency-check/SKILL.md +517 -517
  170. package/vendor/Server/Skills/flutter-library-consistency-check/references/CHECKLIST.md +381 -381
  171. package/vendor/Server/Skills/flutter-library-consistency-check/references/FUNCTION_COMPARISON_GUIDE.md +341 -341
  172. package/vendor/Server/Skills/flutter-library-consistency-check/references/PLATFORM_LIMITATION_GUIDE.md +71 -71
  173. package/vendor/Server/Skills/flutter-library-documentation-quality-check/SKILL.md +518 -518
  174. package/vendor/Server/Skills/flutter-library-documentation-quality-check/references/CHECKLIST.md +406 -406
  175. package/vendor/Server/Skills/flutter-library-memory-leak-check/SKILL.md +652 -652
  176. package/vendor/Server/Skills/flutter-library-memory-leak-check/references/CHECKLIST.md +396 -396
  177. package/vendor/Server/Skills/flutter-library-stability-check/SKILL.md +744 -744
  178. package/vendor/Server/Skills/flutter-library-stability-check/references/CHECKLIST.md +720 -720
  179. package/vendor/Server/Skills/flutter-library-test-coverage-check/SKILL.md +414 -414
  180. package/vendor/Server/Skills/flutter-library-test-coverage-check/assets/REPORT_TEMPLATE.md +150 -150
  181. package/vendor/Server/Skills/flutter-library-test-coverage-check/references/CHECKLIST.md +304 -304
  182. package/vendor/Server/Skills/flutter-library-test-coverage-check/references/SCORING_RULES.md +265 -265
  183. package/vendor/Server/Skills/flutter-library-test-coverage-check/references/SCRIPT_USAGE.md +286 -286
  184. package/vendor/Server/Skills/knowledge-import/SKILL.md +383 -383
  185. package/vendor/Server/Skills/knowledge-import/references/MCP_TOOL_USAGE.md +165 -165
  186. package/vendor/Server/Skills/knowledge-import/references/SKILL_LOCATOR_GUIDE.md +91 -91
  187. package/vendor/Server/Skills/knowledge-import/references/SKILL_OPTIMIZATION_GUIDE.md +127 -127
  188. package/vendor/Server/Skills/ohos-lib-pr-push/SKILL.md +453 -301
  189. package/vendor/Server/Skills/ohos-lib-pr-push/assets/PR_CHECKLIST.md +151 -0
  190. package/vendor/Server/Skills/ohos-lib-pr-push/assets/PR_ISSUE_TEMPLATE.md +257 -105
  191. package/vendor/Server/Skills/ohos-lib-pr-push/references/GITCODE_API.md +191 -127
  192. package/vendor/Server/Skills/ohos-lib-pr-push/scripts/gitcode-api.cjs +317 -208
  193. package/vendor/Server/Skills/rn-code-check/SKILL.md +320 -320
  194. package/vendor/Server/Skills/rn-code-check/references/API-GUIDE.md +234 -234
  195. package/vendor/Server/Skills/rn-code-check/references/CHECKLIST.md +188 -188
  196. package/vendor/Server/Skills/rn-code-check/references/REPORT-TEMPLATE.md +246 -246
  197. package/vendor/Server/Skills/rn-lib-add-example/SKILL.md +198 -198
  198. package/vendor/Server/Skills/rn-lib-add-example/references/APP_RENAME_GUIDE.md +40 -40
  199. package/vendor/Server/Skills/rn-lib-add-example/references/SHELL_ADAPTATION_CHECKLIST.md +78 -78
  200. package/vendor/Server/Skills/rn-lib-add-example/references/TEMPLATE_REPO_GUIDE.md +70 -70
  201. package/vendor/Server/Skills/rn-lib-add-example/references/VERIFICATION_GUIDE.md +47 -47
  202. package/vendor/Server/Skills/rn-lib-add-example/scripts/create-example.cjs +252 -252
  203. package/vendor/Server/Skills/rn-lib-update-doc/SKILL.md +176 -176
  204. package/vendor/Server/Skills/rn-lib-update-doc/config.json +5 -5
  205. package/vendor/Server/Skills/rn-lib-update-doc/references/changelog-template.md +35 -35
  206. package/vendor/Server/Skills/rn-lib-update-doc/references/format-diff.md +137 -137
  207. package/vendor/Server/Skills/rn-lib-update-doc/references/link-registry.json +33 -33
  208. package/vendor/Server/Skills/rn-lib-update-doc/references/model-0.4.2-overview.md +40 -40
  209. package/vendor/Server/Skills/rn-lib-update-doc/references/model-0.4.2.md +429 -429
  210. package/vendor/Server/Skills/rn-lib-update-doc/scripts/detect-readme-format.cjs +105 -105
  211. package/vendor/Server/Skills/rn-lib-update-doc/scripts/find-usage-doc.cjs +90 -90
  212. package/vendor/Server/Skills/rn-lib-update-doc/scripts/link-verify.cjs +213 -213
  213. package/vendor/Server/Skills/rn-lib-update-doc/scripts/normalize-changelog.cjs +73 -73
  214. package/vendor/Server/Skills/rn-lib-update-doc/scripts/normalize-package-meta.cjs +258 -258
  215. package/vendor/Server/Skills/rn-lib-update-doc/scripts/normalize-readme-links.cjs +341 -341
  216. package/vendor/Server/Skills/rn-lib-update-doc/scripts/resolve-usage-docs.cjs +67 -67
  217. package/vendor/Server/Skills/rn-library-consistency-check/SKILL.md +299 -299
  218. package/vendor/Server/Skills/rn-library-consistency-check/assets/REPORT_TEMPLATE.md +293 -293
  219. package/vendor/Server/Skills/rn-library-consistency-check/references/ARKTS_COMPONENT_CHECK_GUIDE.md +857 -857
  220. package/vendor/Server/Skills/rn-library-consistency-check/references/ARKTS_TURBOMODULE_CHECK_GUIDE.md +856 -856
  221. package/vendor/Server/Skills/rn-library-consistency-check/references/CHECKLIST.md +157 -157
  222. package/vendor/Server/Skills/rn-library-consistency-check/references/CPP_COMPONENT_CHECK_GUIDE.md +801 -801
  223. package/vendor/Server/Skills/rn-library-consistency-check/references/CPP_TURBOMODULE_CHECK_GUIDE.md +683 -683
  224. package/vendor/Server/Skills/rn-library-consistency-check/references/FUNCTION_COMPARISON_GUIDE.md +233 -233
  225. package/vendor/Server/Skills/rn-library-consistency-check/references/NATIVE_COMPONENT_IDENTIFICATION.md +1373 -1373
  226. package/vendor/Server/Skills/rn-library-consistency-check/references/SCORING_STANDARD.md +220 -220
  227. package/vendor/Server/Skills/rn-library-consistency-check/scripts/validate-skill.py +245 -245
  228. package/vendor/Server/Skills/rn-library-documentation-quality-check/SKILL.md +263 -263
  229. package/vendor/Server/Skills/rn-library-documentation-quality-check/assets/MODEL.md +414 -414
  230. package/vendor/Server/Skills/rn-library-documentation-quality-check/assets/MODEL_en.md +413 -413
  231. package/vendor/Server/Skills/rn-library-documentation-quality-check/assets/REPORT_TEMPLATE.md +311 -311
  232. package/vendor/Server/Skills/rn-library-documentation-quality-check/references/CHECK_CHECKLIST.md +355 -355
  233. package/vendor/Server/Skills/rn-library-documentation-quality-check/references/CROSS_VALIDATION_GUIDE.md +115 -115
  234. package/vendor/Server/Skills/rn-library-documentation-quality-check/references/DOC_COLLECTION_GUIDE.md +139 -139
  235. package/vendor/Server/Skills/rn-library-documentation-quality-check/references/EXAMPLES.md +586 -586
  236. package/vendor/Server/Skills/rn-library-documentation-quality-check/references/GOTCHAS.md +57 -57
  237. package/vendor/Server/Skills/rn-library-documentation-quality-check/scripts/validate-skill.py +259 -259
  238. package/vendor/Server/Skills/rn-library-interface-analyzer/SKILL.md +301 -301
  239. package/vendor/Server/Skills/rn-library-interface-analyzer/assets/REPORT_TEMPLATE.md +148 -148
  240. package/vendor/Server/Skills/rn-library-interface-analyzer/references/DOC_ANNOTATION_GUIDE.md +118 -118
  241. package/vendor/Server/Skills/rn-library-interface-analyzer/references/SCRIPT_USAGE_GUIDE.md +168 -168
  242. package/vendor/Server/Skills/rn-library-interface-analyzer/references/URL_PARSING_RULES.md +176 -176
  243. package/vendor/Server/Skills/rn-library-issue-generalizer/SKILL.md +274 -274
  244. package/vendor/Server/Skills/rn-library-issue-generalizer/assets/case-template.md +45 -45
  245. package/vendor/Server/Skills/rn-library-issue-generalizer/assets/rule-template.md +25 -25
  246. package/vendor/Server/Skills/rn-library-issue-generalizer/references/PR_FETCH_GUIDE.md +61 -61
  247. package/vendor/Server/Skills/rn-library-issue-generalizer/references/RULE_TEMPLATE_GUIDE.md +165 -165
  248. package/vendor/Server/Skills/rn-library-issue-generalizer/scripts/fetch-pr-info.py +239 -239
  249. package/vendor/Server/Skills/rn-library-known-issue-check/SKILL.md +263 -263
  250. package/vendor/Server/Skills/rn-library-known-issue-check/assets/REPORT_TEMPLATE.md +96 -96
  251. package/vendor/Server/Skills/rn-library-known-issue-check/references/COMPLIANCE_CHECKLIST.md +57 -57
  252. package/vendor/Server/Skills/rn-library-known-issue-check/references/INSPECTION_GUIDE.md +99 -99
  253. package/vendor/Server/Skills/rn-library-known-issue-check/references/PRESCREEN_GUIDE.md +64 -64
  254. package/vendor/Server/Skills/rn-library-known-issue-check/references/SOURCE_PREP_GUIDE.md +74 -74
  255. package/vendor/Server/Skills/rn-library-known-issue-check/scripts/validate-known-issue-report.py +164 -164
  256. package/vendor/Server/Skills/rn-library-memory-leak-check/SKILL.md +356 -339
  257. package/vendor/Server/Skills/rn-library-memory-leak-check/assets/REPORT_TEMPLATE.md +85 -83
  258. package/vendor/Server/Skills/rn-library-memory-leak-check/references/CHECKLIST.md +905 -859
  259. package/vendor/Server/Skills/rn-library-memory-leak-check/references/HARMONY_CODE_PATTERNS.md +124 -122
  260. package/vendor/Server/Skills/rn-library-memory-leak-check/scripts/scan_harmony_code.py +287 -279
  261. package/vendor/Server/Skills/rn-library-stability-check/SKILL.md +273 -273
  262. package/vendor/Server/Skills/rn-library-stability-check/assets/REPORT_TEMPLATE.md +129 -129
  263. package/vendor/Server/Skills/rn-library-stability-check/references/CHECKLIST.md +581 -581
  264. package/vendor/Server/Skills/rn-library-stability-check/references/SCORING_RULES.md +138 -138
  265. package/vendor/Server/Skills/rn-library-stability-check/scripts/scan-harmony-code.py +168 -168
  266. package/vendor/Server/Skills/rn-library-test-coverage-check/SKILL.md +526 -506
  267. package/vendor/Server/Skills/rn-library-test-coverage-check/references/CHECKLIST.md +321 -280
  268. package/vendor/Server/Skills/rn-library-test-coverage-check/references/DEMO_LOCATION.md +221 -221
  269. package/vendor/Server/Skills/rn-library-test-coverage-check/references/REPORT_TEMPLATE.md +220 -209
  270. package/vendor/Server/Skills/rn-library-test-coverage-check/references/SCRIPT_USAGE.md +325 -325
  271. package/vendor/Server/Skills/rn-ohos-xts-generator/SKILL.md +279 -279
  272. package/vendor/Server/Skills/rn-ohos-xts-generator/assets/TEST_FILE_TEMPLATES.md +404 -404
  273. package/vendor/Server/Skills/rn-ohos-xts-generator/references/BUILD_TROUBLESHOOTING.md +185 -185
  274. package/vendor/Server/Skills/rn-ohos-xts-generator/references/BUILD_WORKFLOW.md +265 -265
  275. package/vendor/Server/Skills/rn-ohos-xts-generator/references/COMMON_CONFIG.md +189 -189
  276. package/vendor/Server/Skills/rn-ohos-xts-generator/references/CONFIG_GUIDE.md +203 -203
  277. package/vendor/Server/Skills/rn-ohos-xts-generator/references/COVERAGE_ANALYZER.md +147 -147
  278. package/vendor/Server/Skills/rn-ohos-xts-generator/references/DESIGN_DOC_GUIDE.md +148 -148
  279. package/vendor/Server/Skills/rn-ohos-xts-generator/references/GENERATION_FLOW.md +165 -165
  280. package/vendor/Server/Skills/rn-ohos-xts-generator/references/GENERATION_STRATEGY.md +195 -195
  281. package/vendor/Server/Skills/rn-ohos-xts-generator/references/HARMONY_SCANNER.md +249 -249
  282. package/vendor/Server/Skills/rn-ohos-xts-generator/references/JEST_FRAMEWORK.md +235 -235
  283. package/vendor/Server/Skills/rn-ohos-xts-generator/references/PLATFORM_COMPAT.md +161 -161
  284. package/vendor/Server/Skills/rn-ohos-xts-generator/references/RN_TEST_PATTERNS.md +413 -413
  285. package/vendor/Server/Skills/rn-ohos-xts-generator/references/SOURCE_PARSER.md +235 -235
  286. package/vendor/Server/Skills/rn-ohos-xts-generator/references/TEST_CONVENTIONS.md +245 -245
  287. package/vendor/Server/Skills/rn-ohos-xts-generator/references/TROUBLESHOOTING.md +282 -282
  288. package/vendor/Server/Skills/rn-ohos-xts-generator/references/USAGE_GUIDE.md +251 -251
  289. package/vendor/Server/Skills/rnoh-cptf-api-test/SKILL.md +469 -469
  290. package/vendor/Server/Skills/rnoh-cptf-api-test/assets/CPTF_TEST_TEMPLATE.md +310 -310
  291. package/vendor/Server/Skills/rnoh-cptf-api-test/assets/DEMO_PAGE_TEMPLATE.md +303 -303
  292. package/vendor/Server/Skills/rnoh-cptf-api-test/references/CPTF_TEST_GENERATION_GUIDE.md +421 -421
  293. package/vendor/Server/Skills/rnoh-cptf-api-test/references/DEMO_GENERATION_GUIDE.md +358 -358
  294. package/vendor/Server/Skills/rnoh-cptf-api-test/references/QUALITY_CHECKLIST.md +114 -114
  295. package/vendor/Server/Skills/rnoh-cptf-api-test/references/SPEC_PARSING_GUIDE.md +274 -274
  296. package/vendor/Server/Skills/rnoh-cptf-api-test/scripts/validate-rnoh-cptf-api-test.sh +185 -185
  297. package/vendor/Server/Skills/rnoh-cptf-demo-test/SKILL.md +191 -191
  298. package/vendor/Server/Skills/rnoh-cptf-demo-test/assets/TEST_TEMPLATE.md +216 -216
  299. package/vendor/Server/Skills/rnoh-cptf-demo-test/references/CPTF_SETUP.md +107 -107
  300. package/vendor/Server/Skills/rnoh-cptf-demo-test/references/DEMO_ANALYSIS.md +120 -120
  301. package/vendor/Server/Skills/rnoh-cptf-demo-test/scripts/validate-rnoh-cptf-test.sh +108 -108
  302. package/vendor/Server/Skills/rnoh-cptf-test-run/SKILL.md +237 -237
  303. package/vendor/Server/Skills/rnoh-cptf-test-run/references/DEBUG_PLAYBOOK.md +261 -261
  304. package/vendor/Server/Skills/rnoh-cptf-test-run/scripts/validate-run-env.sh +112 -112
  305. package/vendor/Server/Skills/rnoh-lib-code-adapt/SKILL.md +497 -497
  306. package/vendor/Server/Skills/rnoh-lib-code-adapt/references/adapt-checklist.md +270 -270
  307. package/vendor/Server/Skills/rnoh-lib-code-adapt/references/api-migration-checklist.md +164 -164
  308. package/vendor/Server/Skills/rnoh-lib-code-adapt/references/common-compile-errors.md +556 -556
  309. package/vendor/Server/Skills/rnoh-lib-code-adapt/references/deveco-cli-tools.md +459 -459
  310. package/vendor/Server/Skills/rnoh-lib-code-adapt/references/edge-cases.md +65 -65
  311. package/vendor/Server/Skills/rnoh-lib-code-adapt/references/har-tgz-build.md +512 -512
  312. package/vendor/Server/Skills/rnoh-lib-code-adapt/references/version-compat-matrix.md +132 -132
  313. package/vendor/Server/Skills/rnoh-lib-code-adapt/scripts/auto-integrate.sh +424 -424
  314. package/vendor/Server/Skills/rnoh-lib-demo-coverage/SKILL.md +137 -137
  315. package/vendor/Server/Skills/rnoh-lib-demo-create/SKILL.md +306 -306
  316. package/vendor/Server/Skills/rnoh-lib-demo-create/assets/DEMO_PAGE_TEMPLATE.md +485 -485
  317. package/vendor/Server/Skills/rnoh-lib-demo-create/assets/INDEX_TEMPLATE.md +172 -172
  318. package/vendor/Server/Skills/rnoh-lib-demo-create/references/DEMO_CODE_RULES.md +422 -422
  319. package/vendor/Server/Skills/rnoh-lib-demo-create/references/QUALITY_CHECKLIST.md +95 -95
  320. package/vendor/Server/Skills/rnoh-lib-demo-create/references/SOURCE_EXPLORATION.md +169 -169
  321. package/vendor/Server/Skills/rnoh-lib-demo-doc/SKILL.md +554 -554
  322. package/vendor/Server/Skills/rnoh-lib-demo-doc/assets/DEMO_DOC_TEMPLATE.md +106 -106
  323. package/vendor/Server/Skills/rnoh-lib-demo-doc/references/COVERAGE_INCREMENTAL.md +216 -216
  324. package/vendor/Server/Skills/rnoh-lib-demo-doc/references/DEMO_DESIGN_RULES.md +413 -413
  325. package/vendor/Server/Skills/rnoh-lib-demo-doc/references/OUTPUT_FORMAT.md +269 -269
  326. package/vendor/Server/Skills/rnoh-lib-demo-doc/references/QUALITY_CHECKLIST.md +98 -98
  327. package/vendor/Server/Skills/rnoh-lib-func-test/SKILL.md +337 -337
  328. package/vendor/Server/Skills/rnoh-lib-rnt-search/SKILL.md +722 -722
  329. package/vendor/Server/Skills/rnoh-lib-rnt-search/assets/PORT_REPORT_TEMPLATE.md +219 -219
  330. package/vendor/Server/Skills/rnoh-lib-rnt-search/references/DEMO_PORTING_GUIDE.md +345 -345
  331. package/vendor/Server/Skills/rnoh-lib-rnt-search/references/PORT_VERIFICATION.md +269 -269
  332. package/vendor/Server/Skills/rnoh-lib-rnt-search/references/RNT_PROJECT_STRUCTURE.md +173 -173
  333. package/vendor/Server/Skills/rnoh-lib-rnt-search/scripts/analyze-demo-deps.cjs +267 -267
  334. package/vendor/Server/Skills/rnoh-lib-rnt-search/scripts/detect-rnoh-version.cjs +174 -174
  335. package/vendor/Server/Skills/rnoh-lib-rnt-search/scripts/find-rnt-demo.sh +111 -111
  336. package/vendor/Server/Skills/rnoh-lib-rnt-search/scripts/port-demo.sh +186 -186
  337. package/vendor/Server/Skills/rnoh-lib-rnt-search/scripts/read-lib-meta.cjs +150 -150
  338. package/vendor/Server/Skills/rnoh-lib-test-align/SKILL.md +296 -296
  339. package/vendor/Server/Skills/rnoh-lib-test-align/references/jest-troubleshooting.md +371 -371
  340. package/vendor/Server/Skills/rnoh-lib-test-align/references/port-test-demo-guide.md +212 -212
  341. package/vendor/Server/Skills/rnoh-lib-test-align/references/runtime-troubleshooting.md +345 -345
  342. package/vendor/Server/Skills/rnoh-lib-test-align/references/unit-test-template.md +223 -223
  343. package/vendor/Server/Skills/rnoh-lib-test-align/scripts/port-test-demo.sh +1053 -1053
  344. package/vendor/Server/Skills/rnoh-lib-version-diff/SKILL.md +295 -255
  345. package/vendor/Server/Skills/rnoh-lib-version-diff/references/OHOS_ADAPTATION.md +82 -77
  346. package/vendor/Server/Skills/rnoh-lib-version-diff/references/OHOS_API_LOOKUP.md +224 -224
  347. package/vendor/Server/Skills/rnoh-lib-version-diff/references/script-reference.md +175 -174
  348. package/vendor/Server/Skills/rnoh-lib-version-diff/scripts/detect-upstream-version.js +967 -967
  349. package/vendor/Server/Skills/rnoh-lib-version-diff/scripts/generate-diff.js +1798 -1798
  350. package/vendor/Server/Skills/rnoh-lib-version-diff/scripts/package.json +5 -5
  351. package/vendor/Server/Skills/rnoh-lib-version-diff/templates/report-template.md +180 -137
  352. package/vendor/Server/Skills/rnoh-lib-xts-coverage/SKILL.md +145 -145
  353. package/vendor/Server/Skills/skill-generator/SKILL.md +365 -365
  354. package/vendor/Server/Skills/skill-generator/assets/SKILL_TEMPLATE.md +183 -183
  355. package/vendor/Server/Skills/skill-generator/references/COMPLIANCE_CHECKLIST.md +82 -82
  356. package/vendor/Server/Skills/skill-generator/references/KB_QUERY_GUIDE.md +110 -110
  357. package/vendor/Server/Skills/skill-generator/references/MCP_TOOL_DISCOVERY.md +122 -122
  358. package/vendor/Server/Skills/skill-generator/references/SKILL_SPEC.md +121 -121
  359. package/vendor/Server/Skills/skill-generator/scripts/validate-skill.cjs +259 -259
  360. package/vendor/Server/Skills/task-analysis/SKILL.md +234 -234
  361. package/vendor/Server/Skills/task-analysis/assets/OUTPUT_EXAMPLES.md +160 -160
  362. package/vendor/Server/Skills/task-analysis/references/ACCEPTANCE_CRITERIA_GUIDE.md +190 -190
  363. package/vendor/Server/Skills/task-analysis/references/ANALYSIS_SCHEMA.md +224 -224
  364. package/vendor/Server/Skills/task-analysis/references/INTEGRATION_CONTEXT.md +162 -162
  365. package/vendor/Server/Skills/task-analysis/references/LESSONS_LEARNED.md +55 -55
  366. package/vendor/Server/Skills/task-analysis/references/REPLAN_CONTEXT_GUIDE.md +126 -126
  367. package/vendor/Server/Skills/task-analysis/scripts/validate-analysis-output.sh +256 -256
  368. package/vendor/Server/Skills/task-decomposition/SKILL.md +528 -528
  369. package/vendor/Server/Skills/task-decomposition/assets/OUTPUT_EXAMPLES.md +451 -451
  370. package/vendor/Server/Skills/task-decomposition/references/ACCEPTANCE_CRITERIA_GUIDE.md +179 -179
  371. package/vendor/Server/Skills/task-decomposition/references/DEPENDENCY_GRAPH_GUIDE.md +152 -152
  372. package/vendor/Server/Skills/task-decomposition/references/INTEGRATION_CONTEXT.md +175 -175
  373. package/vendor/Server/Skills/task-decomposition/references/REPLAN_CONTEXT_GUIDE.md +195 -195
  374. package/vendor/Server/Skills/task-decomposition/references/TASK_DECOMPOSITION_TEMPLATES.md +226 -226
  375. package/vendor/Server/Skills/task-decomposition/scripts/list-available-skills.cjs +263 -263
  376. package/vendor/Server/Skills/task-decomposition/scripts/skill-blacklist.json +29 -29
  377. package/vendor/Server/Skills/task-decomposition/scripts/validate-decomposition-output.cjs +376 -376
  378. package/vendor/Server/Skills/task-decomposition/scripts/validate-param-consistency.cjs +259 -259
  379. package/vendor/Server/Skills/task-decomposition/scripts/validate-specified-skills.cjs +216 -216
  380. package/vendor/Server/Skills/task-decomposition/scripts/validate-step-graph.cjs +424 -424
  381. package/vendor/Server/Skills/tpc-cpp-check/SKILL.md +427 -427
  382. package/vendor/Server/Skills/tpc-cpp-check/references/CHECKLIST.md +214 -214
  383. package/vendor/Server/Skills/tpc-cpp-check/references/HPKBUILD_GUIDE.md +235 -235
  384. package/vendor/Server/Skills/tpc-cpp-check/references/REPORT_TEMPLATE.md +250 -250
  385. package/vendor/Server/TestCheck/TestCheck.js +775 -775
  386. package/vendor/Server/TestCheck/index.js +11 -11
  387. package/vendor/Server/library/McpToolGuide.js +280 -280
  388. package/vendor/Server/library/StatsService.js +126 -126
  389. package/vendor/Server/library/cstTime.js +16 -16
  390. package/vendor/VERSION +3 -3
  391. package/workflows/pr-check.mjs +105 -105
  392. package/workflows/pr-push.mjs +192 -192
  393. package/vendor/Server/Agent/SkillSelector/types.js +0 -63
@@ -1,1798 +1,1798 @@
1
- #!/usr/bin/env node
2
- /**
3
- * generate-diff.js
4
- *
5
- * 通过 npm tgz 发行版对比 RN 三方库的两个版本, 生成 diff 文件 (.patch)。
6
- *
7
- * 工作流:
8
- * 1. 通过 npm pack 下载指定版本的 tgz (或使用本地 .tgz 文件)
9
- * 2. 解压到输出目录 (strip package/ 一级)
10
- * 3. 用文件系统 diff 对比两个目录, 生成 unified diff
11
- * 4. 生成变更摘要、适配分析报告、接口差异检查清单
12
- *
13
- * 默认聚焦三方库核心代码变更, 自动排除 lock 文件、example/、CI 配置、文档、
14
- * 编译产物 (lib/, dist/, build/) 等与鸿蒙化升级排查无关的噪音文件。
15
- * 使用 --full 可生成包含所有变更的完整 diff。
16
- *
17
- * 输出文件各有侧重:
18
- * diff_*.patch — diff 补丁 (应用排除规则, 聚焦可合并的代码变更)
19
- * summary_*.md — 变更摘要 (同 diff 的排除范围, 按目录统计增删行数)
20
- * structure_*.md — 适配分析报告 (以接口差异为锚点:
21
- * 先用 rn-scan-exports.cjs 判定是否需要原生适配,
22
- * 有接口变更时按变更项展开 + 附其他平台实现参考,
23
- * 无接口变更时只列 JS 合并清单 + 文件计数)
24
- * api-diff_*.md — 接口差异适配检查清单 (每个变更项含 checkbox, 直接指导适配)
25
- *
26
- * 输出结构:
27
- * diff/
28
- * ├── <version1>/ (版本1 源代码)
29
- * ├── <version2>/ (版本2 源代码)
30
- * ├── diff_<v1>_<v2>.patch (对比 diff 文件)
31
- * ├── summary_<v1>_<v2>.md (变更摘要, 按文件统计增删行数)
32
- * ├── structure_<v1>_<v2>.md (适配分析报告, 接口驱动)
33
- * └── api-diff_<v1>_<v2>.md (接口差异适配检查清单)
34
- *
35
- * 默认排除的路径模式 (聚焦核心代码):
36
- * yarn.lock, package-lock.json, *.lock
37
- * .yarn/, .github/, example/, docs/
38
- * CHANGELOG.md, CONTRIBUTING.md, MIGRATION*.md, README.md
39
- * .gitignore, .yarnrc.yml, .npmrc, .idea/
40
- * android/, ios/, apple/, windows/ (其他平台原生代码, glob 匹配任意层级)
41
- *
42
- * tgz 额外排除 (npm 包含编译产物):
43
- * lib/, dist/, build/, *.map, *.tsbuildinfo
44
- * apple-frameworks/, xcframework/, framework/
45
- *
46
- * 用法:
47
- * node generate-diff.js --npm-package <name> --tgz1 <ver> --tgz2 <ver> [选项]
48
- *
49
- * 示例:
50
- * # 对比 async-storage 的 2.2.0 和 3.1.1 (默认排除噪音)
51
- * node generate-diff.js \
52
- * --npm-package @react-native-async-storage/async-storage \
53
- * --tgz1 2.2.0 \
54
- * --tgz2 3.1.1 \
55
- * --output-dir ./diff-output
56
- *
57
- * # 使用本地已下载的 tgz 文件
58
- * node generate-diff.js \
59
- * --npm-package @react-native-async-storage/async-storage \
60
- * --tgz1 ./pkg-2.2.0.tgz \
61
- * --tgz2 ./pkg-3.1.1.tgz \
62
- * --output-dir ./diff-output
63
- *
64
- * # 生成完整 diff (不排除任何文件)
65
- * node generate-diff.js \
66
- * --npm-package @react-native-async-storage/async-storage \
67
- * --tgz1 2.2.0 \
68
- * --tgz2 3.1.1 \
69
- * --full
70
- *
71
- * # 两阶段流程: 第二轮只对 AI 筛选的文件生成精简 patch
72
- * node generate-diff.js \
73
- * --npm-package @react-native-async-storage/async-storage \
74
- * --tgz1 2.2.0 \
75
- * --tgz2 3.1.1 \
76
- * --files relevant-files.txt \
77
- * --output-dir ./diff-focused
78
- */
79
-
80
- 'use strict';
81
-
82
- const { execFileSync, execSync } = require('child_process');
83
- const os = require('os');
84
- const path = require('path');
85
- const fs = require('fs');
86
-
87
- // 引入 rn-scan-exports.cjs 模块 (接口扫描 + 对比)
88
- // 位于 3rdLibraryLoop/MCP/scripts/,通过相对路径引用
89
- const scanExports = require('../../../../MCP/scripts/rn-scan-exports.cjs');
90
-
91
- // ── 颜色输出 ─────────────────────────────────────────────
92
- const COLORS = {
93
- RED: '\x1b[0;31m',
94
- GREEN: '\x1b[0;32m',
95
- YELLOW: '\x1b[1;33m',
96
- BLUE: '\x1b[0;34m',
97
- CYAN: '\x1b[0;36m',
98
- NC: '\x1b[0m',
99
- };
100
-
101
- function info(msg) { console.log(`${COLORS.BLUE}[INFO]${COLORS.NC} ${msg}`); }
102
- function ok(msg) { console.log(`${COLORS.GREEN}[OK]${COLORS.NC} ${msg}`); }
103
- function warn(msg) { console.log(`${COLORS.YELLOW}[WARN]${COLORS.NC} ${msg}`); }
104
- function error(msg) {
105
- console.error(`${COLORS.RED}[ERROR]${COLORS.NC} ${msg}`);
106
- process.exit(1);
107
- }
108
- function debug(msg) {
109
- if (VERBOSE) {
110
- console.log(`${COLORS.CYAN}[DEBUG]${COLORS.NC} ${msg}`);
111
- }
112
- }
113
-
114
- // ── 默认值 ───────────────────────────────────────────────
115
- let OUTPUT_DIR = '';
116
- let VERBOSE = false;
117
- let FULL_DIFF = false;
118
- let FILES_LIST = '';
119
- let TGZ1 = '';
120
- let TGZ2 = '';
121
- let NPM_PACKAGE = '';
122
- const EXCLUDES = [];
123
-
124
- // ── 默认排除规则 (聚焦三方库核心代码, 过滤与鸿蒙化升级排查无关的噪音) ──
125
- // 这些路径在 --full 模式下不会被排除
126
- const DEFAULT_EXCLUDES = [
127
- // lock 文件
128
- 'yarn.lock',
129
- 'package-lock.json',
130
- '*.lock',
131
- // 工具链 / CI
132
- '.yarn/',
133
- '.github/',
134
- '.yarnrc.yml',
135
- '.npmrc',
136
- '.idea/',
137
- '.gitignore',
138
- // 示例工程 (example/ 单数 + examples/ 复数, 兼容 monorepo 命名)
139
- 'example/',
140
- 'examples/',
141
- // 其他平台原生代码 (鸿蒙适配只关心 JS/TS 源码 + TurboModule Spec)
142
- // 含 ** 的模式使用 glob 匹配任意层级目录内文件
143
- '**/android/**',
144
- '**/ios/**',
145
- '**/apple/**',
146
- '**/windows/**',
147
- // 文档
148
- 'docs/',
149
- 'CHANGELOG.md',
150
- 'CONTRIBUTING.md',
151
- 'MIGRATION*.md',
152
- 'README.md',
153
- ];
154
-
155
- // ── 参数解析 ─────────────────────────────────────────────
156
- function usage() {
157
- console.log(`
158
- RNOH 三方库版本对比脚本 (tgz 模式)
159
-
160
- 通过 npm tgz 发行版对比两个版本, 无需 git clone。
161
- --tgz1/--tgz2 可以是本地 .tgz 路径或 npm 版本号 (自动 npm pack 下载)。
162
- 默认聚焦核心代码, 自动排除 lock 文件、example/、CI 配置、文档、编译产物等噪音。
163
- 使用 --full 可生成包含所有变更的完整 diff。
164
-
165
- 输出文件各有侧重:
166
- diff_*.patch — diff 补丁 (应用排除规则, 聚焦可合并的代码变更)
167
- summary_*.md — 变更摘要 (同 diff 的排除范围, 按目录统计增删行数)
168
- structure_*.md — 适配分析报告 (以接口差异为锚点:
169
- 先用 rn-scan-exports.cjs 判定是否需要原生适配,
170
- 有接口变更时按变更项展开 + 附其他平台实现参考,
171
- 无接口变更时只列 JS 合并清单 + 文件计数)
172
- api-diff_*.md — 接口差异适配检查清单 (每个变更项含 checkbox, 直接指导适配)
173
-
174
- 输出结构:
175
- diff/
176
- ├── <version1>/ (版本1 源代码)
177
- ├── <version2>/ (版本2 源代码)
178
- ├── diff_<v1>_<v2>.patch (对比 diff 文件)
179
- ├── summary_<v1>_<v2>.md (变更摘要, 按文件统计增删行数)
180
- ├── structure_<v1>_<v2>.md (适配分析报告, 接口驱动)
181
- └── api-diff_<v1>_<v2>.md (接口差异适配检查清单)
182
-
183
- 用法:
184
- node generate-diff.js [选项]
185
-
186
- 必选参数:
187
- --npm-package <name> npm 包名 (如 @react-native-async-storage/async-storage)
188
- --tgz1 <spec> 第一个版本: 版本号 (如 2.2.0) 或本地 .tgz 文件路径
189
- --tgz2 <spec> 第二个版本: 版本号 (如 3.1.1) 或本地 .tgz 文件路径
190
-
191
- 可选参数:
192
- --output-dir <path> 输出目录 (默认: 当前目录下的 diff/ 文件夹)
193
- --exclude <pattern> 排除文件路径模式, 在默认排除基础上追加 (可多次指定)
194
- --full 生成完整 diff, 不使用默认排除规则 (仍可配合 --exclude)
195
- --files <path> 只对指定文件列表生成精简 diff patch (每行一个文件路径)
196
- 用于两阶段流程: 第一轮生成全量清单 → AI 筛选 → 第二轮精简 patch
197
- 与 --full 互斥; 指定后跳过排除规则, 只生成指定文件的 diff
198
- --verbose 显示详细调试信息
199
- --help, -h 显示帮助
200
-
201
- 环境变量:
202
- DIFF_BIN <path> 指定 diff 二进制绝对路径 (默认自动探测: /usr/bin/diff 优先,
203
- 所有候选须通过行为验证 — PATH 中 DevEco toolchains 等不兼容
204
- 实现会被跳过)
205
-
206
- 默认排除的路径 (除非使用 --full):
207
- yarn.lock, package-lock.json, *.lock
208
- .yarn/, .github/, example/, examples/, docs/
209
- android/, ios/, apple/, windows/ (其他平台原生代码, glob 匹配任意层级)
210
- CHANGELOG.md, CONTRIBUTING.md, MIGRATION*.md, README.md
211
- .gitignore, .yarnrc.yml, .npmrc, .idea/
212
- lib/, dist/, build/, *.map, *.tsbuildinfo (tgz 编译产物)
213
-
214
- 示例:
215
- # 对比 async-storage 的 2.2.0 和 3.1.1 (默认排除噪音)
216
- node generate-diff.js \\
217
- --npm-package @react-native-async-storage/async-storage \\
218
- --tgz1 2.2.0 \\
219
- --tgz2 3.1.1 \\
220
- --output-dir ./diff-output
221
-
222
- # 使用本地已下载的 tgz 文件
223
- node generate-diff.js \\
224
- --npm-package @react-native-async-storage/async-storage \\
225
- --tgz1 ./pkg-2.2.0.tgz \\
226
- --tgz2 ./pkg-3.1.1.tgz \\
227
- --output-dir ./diff-output
228
-
229
- # 生成完整 diff (不排除任何文件)
230
- node generate-diff.js \\
231
- --npm-package @react-native-async-storage/async-storage \\
232
- --tgz1 2.2.0 \\
233
- --tgz2 3.1.1 \\
234
- --full
235
-
236
- # 在默认排除基础上追加额外排除
237
- node generate-diff.js \\
238
- --npm-package @react-native-async-storage/async-storage \\
239
- --tgz1 2.2.0 \\
240
- --tgz2 3.1.1 \\
241
- --exclude packages/core/windows/
242
-
243
- # 两阶段流程: 第二轮只对 AI 筛选的文件生成精简 patch
244
- node generate-diff.js \\
245
- --npm-package @react-native-async-storage/async-storage \\
246
- --tgz1 2.2.0 \\
247
- --tgz2 3.1.1 \\
248
- --files relevant-files.txt \\
249
- --output-dir ./diff-focused
250
- `);
251
- process.exit(0);
252
- }
253
-
254
- function parseArgs() {
255
- const args = process.argv.slice(2);
256
- let i = 0;
257
- while (i < args.length) {
258
- switch (args[i]) {
259
- case '--output-dir': OUTPUT_DIR = args[i + 1]; i += 2; break;
260
- case '--exclude': EXCLUDES.push(args[i + 1]); i += 2; break;
261
- case '--full': FULL_DIFF = true; i += 1; break;
262
- case '--files': FILES_LIST = args[i + 1]; i += 2; break;
263
- case '--tgz1': TGZ1 = args[i + 1]; i += 2; break;
264
- case '--tgz2': TGZ2 = args[i + 1]; i += 2; break;
265
- case '--npm-package': NPM_PACKAGE = args[i + 1]; i += 2; break;
266
- case '--verbose': VERBOSE = true; i += 1; break;
267
- case '--help':
268
- case '-h': usage(); break;
269
- default: error(`未知参数: ${args[i]} (使用 --help 查看用法)`);
270
- }
271
- }
272
- }
273
-
274
- // ── 工具函数 ─────────────────────────────────────────────
275
-
276
- /**
277
- * 清理分支名用于文件名 (去除特殊字符)。
278
- * @param {string} name 分支/tag 名称
279
- * @returns {string} 清理后的名称
280
- */
281
- function cleanBranchName(name) {
282
- return name
283
- .replace(/^origin\//, '') // 去除 origin/ 前缀
284
- .replace(/\//g, '-'); // 替换 / 为 -
285
- }
286
-
287
- /**
288
- * 格式化文件大小。
289
- * @param {number} bytes 字节数
290
- * @returns {string} 人类可读的大小
291
- */
292
- function formatSize(bytes) {
293
- if (bytes < 1024) return `${bytes}B`;
294
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
295
- return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
296
- }
297
-
298
- // ── diff 二进制解析 (规避 PATH 中不兼容的 diff 实现) ──────
299
- // 部分工具链会向 PATH 注入行为不兼容的 diff (如 DevEco Studio 的
300
- // openharmony toolchains diff: 对不同文件输出为空且退出码为 0,
301
- // 且不支持 -u 选项仍返回 0), 会静默吞掉所有文件差异,
302
- // 导致版本对比报告误判为"零变更"。
303
- // 这里对候选 diff 做行为验证 (smoke test), 只使用验证通过的实现。
304
-
305
- let DIFF_BIN = null;
306
-
307
- /**
308
- * 验证 diff 二进制行为是否可靠。
309
- * 必须满足: 对内容不同的两个文件, 以 unified 格式产出非空输出。
310
- * @param {string} bin diff 二进制路径
311
- * @returns {boolean} 是否通过验证
312
- */
313
- function isReliableDiff(bin) {
314
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'diff-smoke-'));
315
- try {
316
- const f1 = path.join(tmpDir, 'a.txt');
317
- const f2 = path.join(tmpDir, 'b.txt');
318
- fs.writeFileSync(f1, 'line1\n');
319
- fs.writeFileSync(f2, 'line1\nline2\n');
320
- let out = '';
321
- try {
322
- out = execFileSync(bin, ['-u', '-L', 'a/x', '-L', 'b/x', f1, f2], {
323
- encoding: 'utf-8',
324
- stdio: ['pipe', 'pipe', 'pipe'],
325
- });
326
- } catch (err) {
327
- // BSD/GNU diff 对不同文件返回退出码 1, 输出在 err.stdout
328
- out = (err.stdout || '').toString('utf-8');
329
- }
330
- return out.includes('line2');
331
- } catch {
332
- return false;
333
- } finally {
334
- fs.rmSync(tmpDir, { recursive: true, force: true });
335
- }
336
- }
337
-
338
- /**
339
- * 解析可用的 diff 二进制 (带缓存)。
340
- * 优先级: 环境变量 DIFF_BIN > /usr/bin/diff > PATH 中的 diff。
341
- * 每个候选都必须通过行为验证, 全部失败则报错退出。
342
- * @returns {string} 验证过的 diff 二进制路径
343
- */
344
- function resolveDiffBinary() {
345
- if (DIFF_BIN) return DIFF_BIN;
346
- const candidates = [];
347
- if (process.env.DIFF_BIN) candidates.push(process.env.DIFF_BIN);
348
- candidates.push('/usr/bin/diff');
349
- candidates.push('diff');
350
- for (const candidate of candidates) {
351
- try {
352
- if (isReliableDiff(candidate)) {
353
- DIFF_BIN = candidate;
354
- debug(`diff 二进制 (已通过行为验证): ${candidate}`);
355
- return DIFF_BIN;
356
- }
357
- warn(`diff 候选未通过行为验证 (对不同文件无输出或缺少 -u/-L 支持), 跳过: ${candidate}`);
358
- } catch (err) {
359
- debug(`diff 候选不可用: ${candidate} (${err.message})`);
360
- }
361
- }
362
- error(
363
- '未找到可靠的 diff 二进制。PATH 中的 diff 行为异常 ' +
364
- '(可能被 DevEco Studio toolchains 等劫持, 对不同文件静默返回无差异), ' +
365
- '可通过环境变量 DIFF_BIN 指定绝对路径 (如 /usr/bin/diff)'
366
- );
367
- }
368
-
369
- /**
370
- * 用已验证的 diff 二进制执行 diff。
371
- * 退出码约定: 0 = 相同, 1 = 有差异 (输出在 stdout), 其他 = 错误。
372
- * @param {string[]} args diff 参数 (不含二进制名)
373
- * @returns {string} diff 输出 (无差异返回空串)
374
- */
375
- function runDiff(args) {
376
- const bin = resolveDiffBinary();
377
- try {
378
- return execFileSync(bin, args, {
379
- encoding: 'utf-8',
380
- stdio: ['pipe', 'pipe', 'pipe'],
381
- maxBuffer: 50 * 1024 * 1024,
382
- });
383
- } catch (err) {
384
- const stdout = (err.stdout || '').toString('utf-8');
385
- if (stdout) return stdout; // 退出码 1: 有差异
386
- if (err.status === 1) return ''; // 有差异但无输出 (异常实现, 视为无差异)
387
- throw new Error(
388
- `diff 执行失败 (退出码 ${err.status}): ${(err.stderr || '').toString('utf-8').trim()}`
389
- );
390
- }
391
- }
392
-
393
- /**
394
- * tgz 模式额外排除规则。
395
- *
396
- * npm tgz 包含编译产物 (lib/, dist/, build/) 和 source maps (*.map),
397
- * 这些在 git 仓库中通常被 .gitignore 排除, 不会出现在 diff 中。
398
- * tgz 模式需要额外排除以聚焦源码变更。
399
- */
400
- const TGZ_EXTRA_EXCLUDES = [
401
- 'lib/',
402
- 'dist/',
403
- 'build/',
404
- '*.map',
405
- '*.tsbuildinfo',
406
- // 预编译二进制框架 (npm 包中的 xcframework 等)
407
- 'apple-frameworks/',
408
- '**/*.xcframework/**',
409
- '**/*.framework/**',
410
- ];
411
-
412
- /**
413
- * 构建最终的排除列表。
414
- * --full 模式下只用用户 --exclude 指定的; 否则合并默认排除 + 用户追加。
415
- * tgz 模式始终合并编译产物排除规则。
416
- * @returns {string[]} 排除路径模式数组
417
- */
418
- function buildExcludeList() {
419
- if (FULL_DIFF) {
420
- return [...TGZ_EXTRA_EXCLUDES, ...EXCLUDES];
421
- }
422
- return [...DEFAULT_EXCLUDES, ...TGZ_EXTRA_EXCLUDES, ...EXCLUDES];
423
- }
424
-
425
- // ── tgz 模式: npm 包下载 + 解压 ──────────────────────────
426
-
427
- /**
428
- * 判断字符串是否为有效的 npm 语义版本号。
429
- * 匹配 x.y.z 格式 (可含 prerelease 标签如 -next.0)。
430
- * @param {string} str 待判断的字符串
431
- * @returns {boolean}
432
- */
433
- function isVersionString(str) {
434
- return /^\d+\.\d+\.\d+/.test(str);
435
- }
436
-
437
- /**
438
- * 获取 tgz 文件: 如果是本地路径直接返回, 如果是版本号则通过 npm pack 下载。
439
- * @param {string} tgzSpec 本地 .tgz 路径或版本号 (如 "2.2.0")
440
- * @param {string} npmPackage npm 包名 (如 @react-native-async-storage/async-storage)
441
- * @param {string} workDir 临时工作目录
442
- * @returns {string} tgz 文件的绝对路径
443
- */
444
- function resolveTgz(tgzSpec, npmPackage, workDir) {
445
- // 如果是本地文件路径
446
- if (fs.existsSync(tgzSpec) && tgzSpec.endsWith('.tgz')) {
447
- const absPath = path.resolve(tgzSpec);
448
- ok(`使用本地 tgz 文件: ${absPath}`);
449
- return absPath;
450
- }
451
-
452
- // 如果是版本号, 通过 npm pack 下载
453
- if (isVersionString(tgzSpec)) {
454
- if (!npmPackage) {
455
- error(`--tgz1/--tgz2 指定了版本号 "${tgzSpec}" 但未指定 --npm-package`);
456
- }
457
- info(`通过 npm pack 下载: ${npmPackage}@${tgzSpec}`);
458
- const tgzFile = execSync(`npm pack ${npmPackage}@${tgzSpec}`, {
459
- cwd: workDir,
460
- encoding: 'utf-8',
461
- stdio: ['pipe', 'pipe', 'pipe'],
462
- timeout: 60000,
463
- }).trim();
464
- const absPath = path.join(workDir, tgzFile);
465
- if (!fs.existsSync(absPath)) {
466
- error(`npm pack 下载失败: ${npmPackage}@${tgzSpec} (预期文件: ${absPath})`);
467
- }
468
- ok(`已下载: ${tgzFile}`);
469
- return absPath;
470
- }
471
-
472
- error(`无效的 tgz 参数: "${tgzSpec}" — 应为本地 .tgz 文件路径或版本号 (如 2.2.0)`);
473
- }
474
-
475
- /**
476
- * 解压 tgz 文件到目标目录 (npm tgz 内部为 package/ 目录, 去除一级)。
477
- * @param {string} tgzFile tgz 文件路径
478
- * @param {string} destDir 目标目录
479
- */
480
- function extractTgz(tgzFile, destDir) {
481
- fs.mkdirSync(destDir, { recursive: true });
482
- // npm pack 生成的 tgz 内部结构为 package/..., 用 --strip-components=1 去除
483
- execSync(`tar xzf "${tgzFile}" -C "${destDir}" --strip-components=1`, {
484
- stdio: 'pipe',
485
- timeout: 60000,
486
- });
487
- }
488
-
489
- // ── 文件系统遍历 + diff ────────────────────────────────
490
-
491
- /**
492
- * 将排除模式转换为正则表达式 (用于文件系统遍历)。
493
- * 支持两种模式:
494
- * - 含 glob 双星号的模式 (如 android 子树): 匹配任意层级同名目录下的文件
495
- * - 普通路径: example/ → 匹配根目录 example/ 下的文件
496
- * @param {string} pattern 排除模式
497
- * @returns {RegExp} 匹配正则
498
- */
499
- function excludePatternToRegex(pattern) {
500
- // 去除尾部 /
501
- let p = pattern.replace(/\/$/, '');
502
- if (p.includes('**')) {
503
- // glob 双星号模式: 匹配任意路径中包含对应目录名的文件
504
- const escaped = p
505
- .replace(/\*\*/g, '<<<GLOB>>>')
506
- .replace(/[.+^${}()|[\]\\]/g, '\\$&')
507
- .replace(/<<<GLOB>>>/g, '.*')
508
- .replace(/\*/g, '[^/]*');
509
- return new RegExp(`(^|/)${escaped}(/|$)`);
510
- }
511
- // 普通模式: 匹配根目录或任意层级同名目录
512
- const escaped = p.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^/]*');
513
- return new RegExp(`(^|/)${escaped}(/|$)`);
514
- }
515
-
516
- /**
517
- * 判断文件相对路径是否应被排除。
518
- * @param {string} relPath 相对于源码根目录的路径
519
- * @param {RegExp[]} excludeRegexes 排除正则数组
520
- * @returns {boolean} 是否应排除
521
- */
522
- function shouldExclude(relPath, excludeRegexes) {
523
- return excludeRegexes.some(re => re.test(relPath));
524
- }
525
-
526
- /**
527
- * 递归遍历目录, 返回所有文件的相对路径列表。
528
- * @param {string} dir 根目录
529
- * @param {RegExp[]} excludeRegexes 排除正则数组
530
- * @returns {string[]} 相对路径列表 (POSIX 风格 / 分隔)
531
- */
532
- function walkDir(dir, excludeRegexes = []) {
533
- const results = [];
534
- function walk(currentDir, relPrefix) {
535
- let entries;
536
- try {
537
- entries = fs.readdirSync(currentDir, { withFileTypes: true });
538
- } catch {
539
- return;
540
- }
541
- for (const entry of entries) {
542
- const relPath = relPrefix ? `${relPrefix}/${entry.name}` : entry.name;
543
- // 检查是否被排除
544
- if (shouldExclude(relPath, excludeRegexes)) continue;
545
- if (entry.isDirectory()) {
546
- walk(path.join(currentDir, entry.name), relPath);
547
- } else if (entry.isFile()) {
548
- results.push(relPath);
549
- }
550
- }
551
- }
552
- walk(dir, '');
553
- return results.sort();
554
- }
555
-
556
- /**
557
- * 用 GNU diff 对比两个目录, 生成 unified diff 输出。
558
- *
559
- * @param {string} dir1 目录1
560
- * @param {string} dir2 目录2
561
- * @param {string[]} excludePatterns 排除模式列表
562
- * @returns {string} diff 输出
563
- */
564
- function diffDirsWithFs(dir1, dir2, excludePatterns) {
565
- const excludeRegexes = excludePatterns.map(excludePatternToRegex);
566
-
567
- // 收集两个目录的文件列表 (应用排除)
568
- const files1 = walkDir(dir1, excludeRegexes);
569
- const files2 = walkDir(dir2, excludeRegexes);
570
-
571
- const set1 = new Set(files1);
572
- const set2 = new Set(files2);
573
-
574
- // 合并所有文件 (排序)
575
- const allFiles = [...new Set([...files1, ...files2])].sort();
576
-
577
- const lines = [];
578
-
579
- for (const file of allFiles) {
580
- const in1 = set1.has(file);
581
- const in2 = set2.has(file);
582
- const path1 = path.join(dir1, file);
583
- const path2 = path.join(dir2, file);
584
-
585
- if (in1 && in2) {
586
- // 两边都有: 用 diff 对比内容
587
- // 注意: 必须使用 runDiff (行为验证过的 diff 二进制),
588
- // 不能裸调 PATH 中的 diff — 部分 toolchains 的 diff 对不同文件静默无输出
589
- try {
590
- const output = runDiff(['-u', '-L', `a/${file}`, '-L', `b/${file}`, path1, path2]);
591
- if (output) {
592
- lines.push(`diff --git a/${file} b/${file}`);
593
- lines.push(output);
594
- }
595
- } catch (err) {
596
- warn(`diff 失败 (${file}): ${err.message}`);
597
- }
598
- } else if (in1 && !in2) {
599
- // 仅在 dir1: 删除
600
- lines.push(`diff --git a/${file} b/${file}`);
601
- lines.push(`deleted file mode 100644`);
602
- lines.push(`--- a/${file}`);
603
- lines.push(`+++ /dev/null`);
604
- try {
605
- const content = fs.readFileSync(path1, 'utf-8');
606
- for (const line of content.split('\n')) {
607
- lines.push(`-${line}`);
608
- }
609
- } catch { /* binary */ }
610
- lines.push('');
611
- } else if (!in1 && in2) {
612
- // 仅在 dir2: 新增
613
- lines.push(`diff --git a/${file} b/${file}`);
614
- lines.push(`new file mode 100644`);
615
- lines.push(`--- /dev/null`);
616
- lines.push(`+++ b/${file}`);
617
- try {
618
- const content = fs.readFileSync(path2, 'utf-8');
619
- for (const line of content.split('\n')) {
620
- lines.push(`+${line}`);
621
- }
622
- } catch { /* binary */ }
623
- lines.push('');
624
- }
625
- // rename 检测: tgz 模式不做跨文件相似度匹配
626
- // (npm 包之间的文件移动较少见, 且 diff -u 已能展示内容差异)
627
- }
628
-
629
- return lines.join('\n');
630
- }
631
-
632
- /**
633
- * 统计文件增删行数 (文件系统遍历, 替代 git diff --numstat)。
634
- * 对每个有差异的文件, 用 diff 统计 +N / -N。
635
- *
636
- * @param {string} dir1 目录1
637
- * @param {string} dir2 目录2
638
- * @param {string[]} excludePatterns 排除模式列表
639
- * @returns {Map<string, {added: number, deleted: number}>} filepath → numstat
640
- */
641
- function fetchNumstatWithFs(dir1, dir2, excludePatterns) {
642
- const excludeRegexes = excludePatterns.map(excludePatternToRegex);
643
- const files1 = walkDir(dir1, excludeRegexes);
644
- const files2 = walkDir(dir2, excludeRegexes);
645
- const set1 = new Set(files1);
646
- const set2 = new Set(files2);
647
- const allFiles = [...new Set([...files1, ...files2])].sort();
648
-
649
- const result = new Map();
650
-
651
- for (const file of allFiles) {
652
- const in1 = set1.has(file);
653
- const in2 = set2.has(file);
654
- const path1 = path.join(dir1, file);
655
- const path2 = path.join(dir2, file);
656
-
657
- let added = 0;
658
- let deleted = 0;
659
-
660
- if (in1 && in2) {
661
- try {
662
- const output = runDiff([path1, path2]);
663
- if (output) {
664
- for (const line of output.split('\n')) {
665
- if (line.startsWith('> ')) added++;
666
- if (line.startsWith('< ')) deleted++;
667
- }
668
- }
669
- } catch (err) {
670
- warn(`diff 统计失败 (${file}): ${err.message}`);
671
- }
672
- } else if (!in1 && in2) {
673
- // 新文件: 全部为新增行
674
- try {
675
- const content = fs.readFileSync(path2, 'utf-8');
676
- added = content.split('\n').filter(Boolean).length;
677
- } catch { /* binary */ }
678
- } else if (in1 && !in2) {
679
- // 删除文件: 全部为删除行
680
- try {
681
- const content = fs.readFileSync(path1, 'utf-8');
682
- deleted = content.split('\n').filter(Boolean).length;
683
- } catch { /* binary */ }
684
- }
685
-
686
- if (added > 0 || deleted > 0) {
687
- result.set(file, { added, deleted });
688
- }
689
- }
690
-
691
- return result;
692
- }
693
-
694
- // ── 结构变更分析: 鸿蒙相关性分类 ─────────────────────────
695
-
696
- /**
697
- * 结构分析使用的排除集。
698
- *
699
- * 与 DEFAULT_EXCLUDES 一致, 排除所有非 JS/TS 文件 (原生平台代码、文档、CI 等)。
700
- * 只保留 JS/TS 源码、TurboModule Spec、包配置和构建规格。
701
- */
702
- const MINIMAL_EXCLUDES = [
703
- // lock 文件
704
- 'yarn.lock',
705
- 'package-lock.json',
706
- '*.lock',
707
- // 工具链 / CI
708
- '.yarn/',
709
- '.github/',
710
- '.yarnrc.yml',
711
- '.npmrc',
712
- '.idea/',
713
- '.gitignore',
714
- // 示例工程
715
- 'example/',
716
- 'examples/',
717
- // 其他平台原生代码 (只关心 JS/TS 层)
718
- '**/android/**',
719
- '**/ios/**',
720
- '**/apple/**',
721
- '**/windows/**',
722
- // Kotlin Multiplatform (shared-storage 等)
723
- '**/commonMain/**',
724
- '**/androidMain/**',
725
- '**/appleMain/**',
726
- // 文档
727
- 'docs/',
728
- 'CHANGELOG.md',
729
- 'CONTRIBUTING.md',
730
- 'MIGRATION*.md',
731
- 'README.md',
732
- // 二进制文件
733
- '**/*.jar',
734
- '**/*.aar',
735
- '**/*.dylib',
736
- '**/*.so',
737
- '**/gradle/wrapper/**',
738
- ];
739
-
740
- /**
741
- * 鸿蒙相关性分类。
742
- *
743
- * 将变更文件按对鸿蒙 RN 三方库适配的相关性分为以下类别:
744
- *
745
- * @typedef {string} RelevanceCategory
746
- * @property {'js-ts-source'} JS/TS 源码 — 需合并到 harmony/src/ 的文件
747
- * @property {'turbo-module-spec'} TurboModule Spec — 定义原生接口, 需 codegen
748
- * @property {'package-config'} 包配置 — package.json, tsconfig
749
- * @property {'build-spec'} 构建规格 — podspec, CMakeLists
750
- * @property {'not-relevant'} 不相关 — 文档/图片/CI/lock/原生平台代码等
751
- */
752
-
753
- /**
754
- * 对变更文件进行鸿蒙相关性分类。
755
- *
756
- * 判断依据:
757
- * 1. 文件扩展名 (源码类型)
758
- * 2. 文件路径 (是否在 src/, android/, ios/ 等关键目录)
759
- * 3. 文件名模式 (Native*.ts → TurboModule Spec)
760
- *
761
- * @param {ChangeEntry} entry 变更条目
762
- * @returns {RelevanceCategory} 相关性类别
763
- */
764
- function classifyFileRelevance(entry) {
765
- // 优先用 newPath 判断 (新增/修改的文件), D 类型用 oldPath
766
- const checkPath = entry.status === 'D' ? entry.oldPath : entry.newPath;
767
- const lower = checkPath.toLowerCase();
768
- const ext = checkPath.substring(checkPath.lastIndexOf('.') + 1).toLowerCase();
769
- const basename = checkPath.substring(checkPath.lastIndexOf('/') + 1);
770
-
771
- // ── TurboModule Spec: Native*.ts — 最高优先级 ──
772
- // 匹配 NativeXxx.ts / NativeXxx.tsx, 通常在 src/ 下
773
- if (/^Native[A-Z]/.test(basename) && ['ts', 'tsx'].includes(ext)) {
774
- return 'turbo-module-spec';
775
- }
776
-
777
- // ── JS/TS 源码: src/ 下的 .ts/.tsx/.js/.jsx ──
778
- // 也匹配根目录或 packages/*/src/ 下的源码
779
- if (['ts', 'tsx', 'js', 'jsx'].includes(ext)) {
780
- if (lower.includes('/src/') || lower.startsWith('src/')) {
781
- return 'js-ts-source';
782
- }
783
- // 根目录的 index.ts/index.js 也算源码入口
784
- if (!checkPath.includes('/') && ['index.ts', 'index.js', 'index.tsx'].includes(basename.toLowerCase())) {
785
- return 'js-ts-source';
786
- }
787
- // jest/ 下的 mock 也算 (与鸿蒙测试相关)
788
- if (lower.includes('/jest/') || lower.startsWith('jest/')) {
789
- return 'js-ts-source';
790
- }
791
- }
792
-
793
- // ── 包配置: package.json, tsconfig*.json ──
794
- if (basename === 'package.json' || basename.toLowerCase().startsWith('tsconfig')) {
795
- return 'package-config';
796
- }
797
-
798
- // ── 构建规格: podspec, CMakeLists.txt ──
799
- if (ext === 'podspec' || basename.toLowerCase() === 'cmakelists.txt') {
800
- return 'build-spec';
801
- }
802
-
803
- // ── 其他: 不相关 (包括原生平台代码、文档、图片、CI 等) ──
804
- return 'not-relevant';
805
- }
806
-
807
- /**
808
- * 相关性类别的元信息 (展示顺序、标题、说明)。
809
- */
810
- const RELEVANCE_META = {
811
- 'js-ts-source': {
812
- title: 'JS/TS 源码',
813
- icon: '📄',
814
- desc: '需合并到鸿蒙化仓库 src/ 的源代码文件',
815
- showDetail: true,
816
- },
817
- 'turbo-module-spec': {
818
- title: 'TurboModule Spec',
819
- icon: '🔌',
820
- desc: '定义原生模块接口, 需更新 codegen 并同步 ETS/C++ 层',
821
- showDetail: true,
822
- },
823
- 'package-config': {
824
- title: '包配置',
825
- icon: '📦',
826
- desc: 'package.json / tsconfig 变更, 影响 codegen 和依赖',
827
- showDetail: true,
828
- },
829
- 'build-spec': {
830
- title: '构建规格',
831
- icon: '🔧',
832
- desc: 'podspec / CMakeLists 等构建配置',
833
- showDetail: true,
834
- },
835
- 'not-relevant': {
836
- title: '不相关变更 (已过滤)',
837
- icon: '🔇',
838
- desc: '文档/图片/CI/lock/原生平台代码等, 与鸿蒙适配无关',
839
- showDetail: false,
840
- },
841
- };
842
-
843
- /**
844
- * 相关性类别的展示顺序。
845
- */
846
- const RELEVANCE_ORDER = [
847
- 'turbo-module-spec',
848
- 'js-ts-source',
849
- 'package-config',
850
- 'build-spec',
851
- 'not-relevant',
852
- ];
853
-
854
- // ── 结构变更分析 ─────────────────────────────────────────
855
-
856
- /**
857
- * 变更文件的状态类型。
858
- * @typedef {'R'|'M'|'A'|'D'|'C'|'T'} FileStatus
859
- * R = Renamed (重命名/移动, 可能含内容修改)
860
- * M = Modified (原地修改)
861
- * A = Added (纯新增)
862
- * D = Deleted (纯删除)
863
- * C = Copied (复制)
864
- * T = Type changed (文件类型变更, 如 文件→符号链接)
865
- */
866
-
867
- /**
868
- * 单个变更文件的描述。
869
- * @typedef {Object} ChangeEntry
870
- * @property {FileStatus} status - 变更状态
871
- * @property {string} oldPath - 旧路径 (M/A 为空或同 newPath)
872
- * @property {string} newPath - 新路径 (D 为空或同 oldPath)
873
- * @property {number} similarity - 相似度百分比 (仅 R/C 有意义, 0-100)
874
- * @property {number} added - 新增行数 (来自 numstat)
875
- * @property {number} deleted - 删除行数 (来自 numstat)
876
- */
877
-
878
- /**
879
- * 分析两个目录之间的结构变更。
880
- *
881
- * 通过文件存在性判断状态:
882
- * - 两边都有 → Modified
883
- * - 仅 dir2 有 → Added
884
- * - 仅 dir1 有 → Deleted
885
- * (rename 检测在 tgz 模式下降级为 modified + added + deleted)
886
- *
887
- * @param {string} dir1 目录1源码
888
- * @param {string} dir2 目录2源码
889
- * @param {string[]} extraExcludes 额外排除模式
890
- * @returns {{entries: ChangeEntry[], stats: Object}} 变更条目列表和统计信息
891
- */
892
- function analyzeStructuralChangesWithFs(dir1, dir2, extraExcludes = []) {
893
- const structureExcludes = [...MINIMAL_EXCLUDES, ...TGZ_EXTRA_EXCLUDES, ...extraExcludes];
894
- const excludeRegexes = structureExcludes.map(excludePatternToRegex);
895
-
896
- const files1 = walkDir(dir1, excludeRegexes);
897
- const files2 = walkDir(dir2, excludeRegexes);
898
- const set1 = new Set(files1);
899
- const set2 = new Set(files2);
900
- const allFiles = [...new Set([...files1, ...files2])].sort();
901
-
902
- const numstatMap = fetchNumstatWithFs(dir1, dir2, structureExcludes);
903
- const entries = [];
904
-
905
- for (const file of allFiles) {
906
- const in1 = set1.has(file);
907
- const in2 = set2.has(file);
908
-
909
- let status, oldPath, newPath;
910
- if (in1 && in2) {
911
- // 检查是否有实际差异
912
- const ns = numstatMap.get(file);
913
- if (!ns) continue; // 无差异, 跳过
914
- status = 'M';
915
- oldPath = file;
916
- newPath = file;
917
- } else if (!in1 && in2) {
918
- status = 'A';
919
- oldPath = '';
920
- newPath = file;
921
- } else if (in1 && !in2) {
922
- status = 'D';
923
- oldPath = file;
924
- newPath = '';
925
- } else {
926
- continue;
927
- }
928
-
929
- const ns = numstatMap.get(file) || { added: 0, deleted: 0 };
930
- entries.push({
931
- status,
932
- oldPath,
933
- newPath,
934
- similarity: 100,
935
- added: ns.added,
936
- deleted: ns.deleted,
937
- });
938
- }
939
-
940
- // 鸿蒙相关性分类
941
- for (const entry of entries) {
942
- entry.relevance = classifyFileRelevance(entry);
943
- }
944
-
945
- // 统计
946
- const relevantEntries = entries.filter(e => e.relevance !== 'not-relevant');
947
- const irrelevantEntries = entries.filter(e => e.relevance === 'not-relevant');
948
- const relevanceCounts = {};
949
- for (const cat of RELEVANCE_ORDER) {
950
- relevanceCounts[cat] = entries.filter(e => e.relevance === cat).length;
951
- }
952
-
953
- const stats = {
954
- total: entries.length,
955
- relevant: relevantEntries.length,
956
- irrelevant: irrelevantEntries.length,
957
- renamed: 0,
958
- modified: entries.filter(e => e.status === 'M').length,
959
- added: entries.filter(e => e.status === 'A').length,
960
- deleted: entries.filter(e => e.status === 'D').length,
961
- copied: 0,
962
- typeChanged: 0,
963
- totalAdded: entries.reduce((sum, e) => sum + e.added, 0),
964
- totalDeleted: entries.reduce((sum, e) => sum + e.deleted, 0),
965
- relevanceCounts,
966
- };
967
-
968
- return { entries, stats };
969
- }
970
-
971
- /**
972
- * 生成变更摘要 Markdown。
973
- *
974
- * @param {string} dir1 目录1源码
975
- * @param {string} dir2 目录2源码
976
- * @param {string[]} excludePatterns 排除模式列表
977
- * @param {object} meta 元信息
978
- * @returns {string} 摘要 Markdown 内容
979
- */
980
- function generateSummaryWithFs(dir1, dir2, excludePatterns, meta) {
981
- const numstatMap = fetchNumstatWithFs(dir1, dir2, excludePatterns);
982
-
983
- const files = [];
984
- let totalAdded = 0;
985
- let totalDeleted = 0;
986
-
987
- for (const [filepath, { added, deleted }] of numstatMap) {
988
- files.push({ filepath, added, deleted });
989
- totalAdded += added;
990
- totalDeleted += deleted;
991
- }
992
-
993
- // 按目录分组
994
- const groups = {};
995
- for (const f of files) {
996
- const sep = f.filepath.lastIndexOf('/');
997
- const dir = sep >= 0 ? f.filepath.substring(0, sep) : '(根目录)';
998
- if (!groups[dir]) groups[dir] = [];
999
- groups[dir].push(f);
1000
- }
1001
-
1002
- const lines = [];
1003
- lines.push(`# 变更摘要: ${meta.branch1} → ${meta.branch2}`);
1004
- lines.push('');
1005
- lines.push(`| 项目 | 值 |`);
1006
- lines.push(`|------|------|`);
1007
- lines.push(`| 仓库 | ${meta.repoUrl} |`);
1008
- lines.push(`| 版本 1 | ${meta.branch1} (${meta.commit1Hash}) |`);
1009
- lines.push(`| 版本 2 | ${meta.branch2} (${meta.commit2Hash}) |`);
1010
- lines.push(`| 变更文件数 | ${files.length} |`);
1011
- lines.push(`| 新增行数 | +${totalAdded} |`);
1012
- lines.push(`| 删除行数 | -${totalDeleted} |`);
1013
- lines.push(`| 对比方式 | tgz (npm 发行版) |`);
1014
- if (excludePatterns.length > 0) {
1015
- lines.push(`| 排除模式 | ${excludePatterns.join(', ')} |`);
1016
- } else {
1017
- lines.push(`| 排除模式 | 无 (--full 模式) |`);
1018
- }
1019
- lines.push('');
1020
- lines.push('---');
1021
- lines.push('');
1022
-
1023
- for (const dir of Object.keys(groups).sort()) {
1024
- lines.push(`## ${dir}/`);
1025
- lines.push('');
1026
- lines.push('| 文件 | 新增 | 删除 |');
1027
- lines.push('|------|------|------|');
1028
- for (const f of groups[dir].sort((a, b) => (b.added + b.deleted) - (a.added + a.deleted))) {
1029
- lines.push(`| ${f.filepath} | +${f.added} | -${f.deleted} |`);
1030
- }
1031
- lines.push('');
1032
- }
1033
-
1034
- return lines.join('\n');
1035
- }
1036
-
1037
- /**
1038
- * 扫描目录的导出接口 (支持 monorepo)。
1039
- *
1040
- * 如果根目录有 package.json 且包含 codegenConfig 或 source 字段, 直接扫描。
1041
- * 否则递归查找 packages 子目录中有 codegenConfig 的包。
1042
- *
1043
- * @param {string} dir 源码目录
1044
- * @returns {Object|null} 扫描结果, 未找到包返回 null
1045
- */
1046
- function scanDirectoryForExports(dir) {
1047
- // 先尝试根目录
1048
- try {
1049
- const result = scanExports.scanDirectory(dir);
1050
- if (result.entryFile || (result.turboModuleSpecs && result.turboModuleSpecs.length > 0)) {
1051
- return result;
1052
- }
1053
- } catch { /* continue */ }
1054
-
1055
- // monorepo: 查找 packages/*/ 子目录
1056
- const packagesDir = path.join(dir, 'packages');
1057
- if (fs.existsSync(packagesDir)) {
1058
- let entries;
1059
- try {
1060
- entries = fs.readdirSync(packagesDir, { withFileTypes: true });
1061
- } catch {
1062
- return null;
1063
- }
1064
-
1065
- for (const entry of entries) {
1066
- if (!entry.isDirectory()) continue;
1067
- const subDir = path.join(packagesDir, entry.name);
1068
- const pkgPath = path.join(subDir, 'package.json');
1069
- if (!fs.existsSync(pkgPath)) continue;
1070
-
1071
- try {
1072
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
1073
- // 优先选择有 codegenConfig 的包 (那是 TurboModule 库)
1074
- // 其次选择有 source 字段的包
1075
- if (pkg.codegenConfig || pkg.source || (pkg.name && !pkg.name.includes('eslint-config') && !pkg.name.includes('website'))) {
1076
- const result = scanExports.scanDirectory(subDir);
1077
- if (result.entryFile || (result.turboModuleSpecs && result.turboModuleSpecs.length > 0)) {
1078
- debug(`接口扫描: 使用 monorepo 子包 packages/${entry.name}`);
1079
- return result;
1080
- }
1081
- }
1082
- } catch { /* continue */ }
1083
- }
1084
- }
1085
-
1086
- // 回退: 再试根目录 (即使没有 entryFile, 也可能有 TurboModule Spec)
1087
- try {
1088
- return scanExports.scanDirectory(dir);
1089
- } catch {
1090
- return null;
1091
- }
1092
- }
1093
-
1094
- /**
1095
- * 生成接口驱动的结构变更摘要。
1096
- *
1097
- * 核心逻辑:
1098
- * 1. 调用 rn-scan-exports.cjs 对比两个版本的 JS 层接口 (导出 + TurboModule Spec)
1099
- * 2. 如果接口未变更 → 报告"仅 JS 合并 + codegen, 无需原生适配", 文件变更只计数
1100
- * 3. 如果接口有变更 → 以接口变更项为锚点组织内容:
1101
- * - 每个新增/变更的接口方法下, 附上其他平台 (Android/iOS) 的实现文件作为参考
1102
- * - 纯 bug 修复 / 文档 / CI 等 → 只计数, 不展开
1103
- *
1104
- * @param {string} dir1 目录1源码
1105
- * @param {string} dir2 目录2源码
1106
- * @param {string[]} extraExcludes 用户额外排除模式
1107
- * @param {object} meta 元信息
1108
- * @param {string} branch1Dir 分支1 导出目录 (用于 rn-scan-exports.cjs)
1109
- * @param {string} branch2Dir 分支2 导出目录 (用于 rn-scan-exports.cjs)
1110
- * @returns {{content: string, stats: Object, apiDiff: Object, scan1: Object, scan2: Object}}
1111
- */
1112
- function generateStructureSummaryTgz(dir1, dir2, extraExcludes, meta, branch1Dir, branch2Dir) {
1113
- // ── Step 1: 扫描接口差异 ──
1114
- let scan1 = null;
1115
- let scan2 = null;
1116
- let apiDiff = null;
1117
- try {
1118
- scan1 = scanDirectoryForExports(branch1Dir);
1119
- scan2 = scanDirectoryForExports(branch2Dir);
1120
- if (scan1 && scan2) {
1121
- apiDiff = scanExports.compareScans(scan1, scan2);
1122
- } else {
1123
- warn('接口扫描: 未找到包入口 (monorepo 子包可能未包含 codegenConfig)');
1124
- }
1125
- } catch (err) {
1126
- warn(`接口扫描失败: ${err.message}`);
1127
- }
1128
-
1129
- // ── Step 2: 分析结构变更 (文件级, 用文件系统遍历) ──
1130
- const { entries, stats } = analyzeStructuralChangesWithFs(dir1, dir2, extraExcludes);
1131
-
1132
- const lines = [];
1133
-
1134
- // ── 头部 ──
1135
- lines.push(`# 适配分析报告: ${meta.branch1} → ${meta.branch2}`);
1136
- lines.push('');
1137
- lines.push(`| 项目 | 值 |`);
1138
- lines.push(`|------|------|`);
1139
- lines.push(`| 仓库 | ${meta.repoUrl} |`);
1140
- lines.push(`| 版本 1 | ${meta.branch1} (${meta.commit1Hash}) |`);
1141
- lines.push(`| 版本 2 | ${meta.branch2} (${meta.commit2Hash}) |`);
1142
- lines.push(`| 文件变更总数 | ${stats.total} |`);
1143
- lines.push(`| 对比方式 | tgz (npm 发行版, 无 rename 检测) |`);
1144
- lines.push('');
1145
-
1146
- // ── 判定结果 ──
1147
- lines.push('## 判定结果');
1148
- lines.push('');
1149
- if (apiDiff) {
1150
- if (apiDiff.summary.needsNativeAdaptation) {
1151
- lines.push('> ⚠️ **需要原生适配** — JS 层接口或 TurboModule Spec 发生变更。');
1152
- lines.push('> 下方按接口变更项展开, 每项附其他平台实现作为参考。');
1153
- } else {
1154
- lines.push('> ✅ **无需原生适配** — JS 层接口未变更, 仅 bug 修复/内部重构。');
1155
- lines.push('> 鸿蒙侧只需合并 JS 代码 + 重新 codegen。');
1156
- }
1157
- lines.push('');
1158
- lines.push('| 检查项 | 结果 |');
1159
- lines.push('|--------|------|');
1160
- lines.push(`| JS 导出变更 | ${apiDiff.summary.hasExportChanges ? '⚠️ 有' : '✅ 无'} |`);
1161
- lines.push(`| 接口定义变更 | ${apiDiff.summary.hasInterfaceChanges ? '⚠️ 有' : '✅ 无'} |`);
1162
- lines.push(`| TurboModule Spec 变更 | ${apiDiff.summary.hasTurboModuleChanges ? '⚠️ 有' : '✅ 无'} |`);
1163
- lines.push(`| **需要原生适配** | ${apiDiff.summary.needsNativeAdaptation ? '⚠️ **是**' : '✅ **否**'} |`);
1164
- } else {
1165
- lines.push('> ⚠️ 接口扫描失败, 无法判定是否需要原生适配, 请手动检查。');
1166
- }
1167
- lines.push('');
1168
- lines.push('---');
1169
- lines.push('');
1170
-
1171
- // ── 文件变更计数摘要 ──
1172
- lines.push('## 文件变更统计');
1173
- lines.push('');
1174
- lines.push('| 类别 | 数量 | 说明 |');
1175
- lines.push('|------|------|------|');
1176
- for (const cat of RELEVANCE_ORDER) {
1177
- const count = stats.relevanceCounts[cat] || 0;
1178
- if (count === 0 && cat === 'not-relevant') continue;
1179
- const m = RELEVANCE_META[cat];
1180
- lines.push(`| ${m.icon} ${m.title} | ${count} | ${m.desc} |`);
1181
- }
1182
- lines.push('');
1183
-
1184
- // ── 辅助函数 ──
1185
- const getBase = (p) => {
1186
- const sep = p.lastIndexOf('/');
1187
- return sep >= 0 ? p.substring(sep + 1) : p;
1188
- };
1189
- const statusIcon = (s) => ({ R: '🔄', M: '✏️', A: '➕', D: '➖', C: '📋', T: '🔀' }[s] || '❓');
1190
-
1191
- // ── 核心内容: 接口变更驱动的适配分析 ──
1192
- if (apiDiff && apiDiff.summary.needsNativeAdaptation) {
1193
- lines.push('## 适配分析 (按接口变更项)');
1194
- lines.push('');
1195
- lines.push('> 以下每个检查项对应一个接口变更, 需在鸿蒙侧对等实现。');
1196
- lines.push('> 接口不变的文件变更 (bug 修复/内部重构) 不在此展开, 见下方计数。');
1197
- lines.push('');
1198
-
1199
- let itemNum = 0;
1200
-
1201
- // ── TurboModule Spec 变更 ──
1202
- if (apiDiff.turboModuleDiff.length > 0) {
1203
- lines.push('### TurboModule Spec 变更');
1204
- lines.push('');
1205
- lines.push('> 原生模块接口变更, 需同步 codegen + ETS TurboModule + C++ Package。');
1206
- lines.push('');
1207
-
1208
- for (const d of apiDiff.turboModuleDiff) {
1209
- if (d.change === 'added') {
1210
- itemNum++;
1211
- lines.push(`#### [ ] #${itemNum} 新增 TurboModule: \`${d.moduleName}\``);
1212
- lines.push('');
1213
- lines.push('**新增方法:**');
1214
- lines.push('');
1215
- lines.push('| 方法 | 签名 |');
1216
- lines.push('|------|------|');
1217
- for (const m of d.methods) {
1218
- lines.push(`| \`${m.name}\` | \`${m.signature}\` |`);
1219
- }
1220
- lines.push('');
1221
- } else if (d.change === 'removed') {
1222
- itemNum++;
1223
- lines.push(`#### [ ] #${itemNum} 移除 TurboModule: \`${d.moduleName}\``);
1224
- lines.push('');
1225
- lines.push('**移除方法:**');
1226
- lines.push('');
1227
- lines.push('| 方法 | 签名 |');
1228
- lines.push('|------|------|');
1229
- for (const m of d.methods) {
1230
- lines.push(`| \`${m.name}\` | \`${m.signature}\` |`);
1231
- }
1232
- lines.push('');
1233
- } else if (d.change === 'modified') {
1234
- if (d.note) {
1235
- itemNum++;
1236
- lines.push(`#### [ ] #${itemNum} TurboModule 结构变更: \`${d.moduleName}\``);
1237
- lines.push(`> ⚠️ ${d.note}`);
1238
- if (d.specFiles1) lines.push(`> 旧 Spec: ${d.specFiles1.join(', ')}`);
1239
- if (d.specFiles2) lines.push(`> 新 Spec: ${d.specFiles2.join(', ')}`);
1240
- lines.push('');
1241
- } else {
1242
- const added = d.methods.filter(m => m.change === 'added');
1243
- const removed = d.methods.filter(m => m.change === 'removed');
1244
- const modified = d.methods.filter(m => m.change === 'modified');
1245
-
1246
- if (added.length > 0) {
1247
- itemNum++;
1248
- lines.push(`#### [ ] #${itemNum} TurboModule \`${d.moduleName}\` 新增方法`);
1249
- lines.push('');
1250
- lines.push('| 方法 | 签名 |');
1251
- lines.push('|------|------|');
1252
- for (const m of added) {
1253
- lines.push(`| \`${m.name}\` | \`${m.signature}\` |`);
1254
- }
1255
- lines.push('');
1256
- }
1257
- if (removed.length > 0) {
1258
- itemNum++;
1259
- lines.push(`#### [ ] #${itemNum} TurboModule \`${d.moduleName}\` 移除方法`);
1260
- lines.push('');
1261
- lines.push('| 方法 | 签名 |');
1262
- lines.push('|------|------|');
1263
- for (const m of removed) {
1264
- lines.push(`| \`${m.name}\` | \`${m.signature}\` |`);
1265
- }
1266
- lines.push('');
1267
- }
1268
- if (modified.length > 0) {
1269
- itemNum++;
1270
- lines.push(`#### [ ] #${itemNum} TurboModule \`${d.moduleName}\` 修改方法签名`);
1271
- lines.push('');
1272
- lines.push('| 方法 | 旧签名 | 新签名 |');
1273
- lines.push('|------|--------|--------|');
1274
- for (const m of modified) {
1275
- lines.push(`| \`${m.name}\` | \`${m.oldSignature}\` | \`${m.newSignature}\` |`);
1276
- }
1277
- lines.push('');
1278
- }
1279
- }
1280
- }
1281
- }
1282
- }
1283
-
1284
- // ── 公共接口/类型定义变更 ──
1285
- if (apiDiff.interfaceDefsDiff.length > 0) {
1286
- lines.push('### 公共接口/类型定义变更');
1287
- lines.push('');
1288
- lines.push('> JS 层公共 API 接口变更, 需同步鸿蒙化仓库 src/。');
1289
- lines.push('');
1290
-
1291
- for (const d of apiDiff.interfaceDefsDiff) {
1292
- if (d.change === 'added') {
1293
- itemNum++;
1294
- lines.push(`#### [ ] #${itemNum} 新增接口: \`${d.name}\``);
1295
- lines.push('');
1296
- lines.push('| 方法 |');
1297
- lines.push('|------|');
1298
- for (const m of d.methods) lines.push(`| \`${m}\` |`);
1299
- lines.push('');
1300
- } else if (d.change === 'removed') {
1301
- itemNum++;
1302
- lines.push(`#### [ ] #${itemNum} 移除接口: \`${d.name}\``);
1303
- lines.push('');
1304
- lines.push('| 方法 |');
1305
- lines.push('|------|');
1306
- for (const m of d.methods) lines.push(`| \`${m}\` |`);
1307
- lines.push('');
1308
- } else if (d.change === 'modified') {
1309
- if (d.added.length > 0) {
1310
- itemNum++;
1311
- lines.push(`#### [ ] #${itemNum} 接口 \`${d.name}\` 新增方法`);
1312
- lines.push('');
1313
- lines.push('| 方法 | 签名 |');
1314
- lines.push('|------|------|');
1315
- for (const m of d.added) lines.push(`| \`${m.name}\` | \`${m.signature}\` |`);
1316
- lines.push('');
1317
- }
1318
- if (d.removed.length > 0) {
1319
- itemNum++;
1320
- lines.push(`#### [ ] #${itemNum} 接口 \`${d.name}\` 移除方法`);
1321
- lines.push('');
1322
- lines.push('| 方法 | 签名 |');
1323
- lines.push('|------|------|');
1324
- for (const m of d.removed) lines.push(`| \`${m.name}\` | \`${m.signature}\` |`);
1325
- lines.push('');
1326
- }
1327
- if (d.modified.length > 0) {
1328
- itemNum++;
1329
- lines.push(`#### [ ] #${itemNum} 接口 \`${d.name}\` 修改方法签名`);
1330
- lines.push('');
1331
- lines.push('| 方法 | 旧签名 | 新签名 |');
1332
- lines.push('|------|--------|--------|');
1333
- for (const m of d.modified) {
1334
- lines.push(`| \`${m.name}\` | \`${m.oldSignature}\` | \`${m.newSignature}\` |`);
1335
- }
1336
- lines.push('');
1337
- }
1338
- }
1339
- }
1340
- }
1341
-
1342
- // ── JS 导出变更 ──
1343
- const e = apiDiff.exportsDiff;
1344
- if (e.defaultChanged || e.added.length > 0 || e.removed.length > 0 || e.typesAdded.length > 0 || e.typesRemoved.length > 0) {
1345
- lines.push('### JS 导出变更');
1346
- lines.push('');
1347
- if (e.defaultChanged) {
1348
- itemNum++;
1349
- lines.push(`- [ ] **#${itemNum} default export 变更:** \`${e.defaultFrom || '(无)'}\` → \`${e.defaultTo || '(无)'}\``);
1350
- }
1351
- if (e.added.length > 0) {
1352
- itemNum++;
1353
- lines.push(`- [ ] **#${itemNum} 新增导出:** ${e.added.map(n => `\`${n}\``).join(', ')}`);
1354
- }
1355
- if (e.removed.length > 0) {
1356
- itemNum++;
1357
- lines.push(`- [ ] **#${itemNum} 移除导出:** ${e.removed.map(n => `\`${n}\``).join(', ')}`);
1358
- }
1359
- if (e.typesAdded.length > 0) {
1360
- itemNum++;
1361
- lines.push(`- [ ] **#${itemNum} 新增类型导出:** ${e.typesAdded.map(n => `\`${n}\``).join(', ')}`);
1362
- }
1363
- if (e.typesRemoved.length > 0) {
1364
- itemNum++;
1365
- lines.push(`- [ ] **#${itemNum} 移除类型导出:** ${e.typesRemoved.map(n => `\`${n}\``).join(', ')}`);
1366
- }
1367
- lines.push('');
1368
- }
1369
-
1370
- if (itemNum === 0) {
1371
- lines.push('*接口变更项为空。*');
1372
- lines.push('');
1373
- }
1374
- } else if (apiDiff && !apiDiff.summary.needsNativeAdaptation) {
1375
- // ── 接口未变更: 只需合并 JS ──
1376
- lines.push('## JS 代码合并清单');
1377
- lines.push('');
1378
- lines.push('> 接口未变更, 以下 JS/TS 源码变更只需合并到鸿蒙化仓库 src/。');
1379
- lines.push('> 包配置变更需同步到 package.json。');
1380
- lines.push('');
1381
-
1382
- const jsEntries = entries.filter(e => e.relevance === 'js-ts-source' || e.relevance === 'turbo-module-spec');
1383
- if (jsEntries.length > 0) {
1384
- const groups = {};
1385
- for (const e of jsEntries) {
1386
- const checkPath = e.status === 'D' ? e.oldPath : e.newPath;
1387
- const sep = checkPath.lastIndexOf('/');
1388
- const dir = sep >= 0 ? checkPath.substring(0, sep) : '(根目录)';
1389
- if (!groups[dir]) groups[dir] = [];
1390
- groups[dir].push(e);
1391
- }
1392
- for (const dir of Object.keys(groups).sort()) {
1393
- lines.push(`**${dir}/**`);
1394
- lines.push('');
1395
- lines.push('| 状态 | 文件 | 新增 | 删除 |');
1396
- lines.push('|------|------|------|------|');
1397
- for (const e of groups[dir].sort((a, b) => (b.added + b.deleted) - (a.added + a.deleted))) {
1398
- const name = e.status === 'D' ? getBase(e.oldPath) : getBase(e.newPath);
1399
- lines.push(`| ${statusIcon(e.status)} ${e.status} | ${name} | +${e.added} | -${e.deleted} |`);
1400
- }
1401
- lines.push('');
1402
- }
1403
- } else {
1404
- lines.push('*无 JS/TS 源码变更。*');
1405
- lines.push('');
1406
- }
1407
- }
1408
-
1409
- // ── 无需展开的变更 (只计数) ──
1410
- lines.push('## 其他变更 (仅计数)');
1411
- lines.push('');
1412
- lines.push('> 以下变更与接口适配无关, 不展开详情。');
1413
- lines.push('');
1414
-
1415
- const configEntries = entries.filter(e => e.relevance === 'package-config');
1416
- if (configEntries.length > 0) {
1417
- lines.push(`**包配置 (${configEntries.length}):**`);
1418
- lines.push('');
1419
- lines.push('| 状态 | 文件 | 新增 | 删除 |');
1420
- lines.push('|------|------|------|------|');
1421
- for (const e of configEntries.sort((a, b) => (b.added + b.deleted) - (a.added + a.deleted))) {
1422
- const name = e.status === 'R' ? `${getBase(e.oldPath)} → ${getBase(e.newPath)}` : (e.status === 'D' ? getBase(e.oldPath) : getBase(e.newPath));
1423
- lines.push(`| ${statusIcon(e.status)} ${e.status} | ${name} | +${e.added} | -${e.deleted} |`);
1424
- }
1425
- lines.push('');
1426
- }
1427
-
1428
- const buildEntries = entries.filter(e => e.relevance === 'build-spec');
1429
- if (buildEntries.length > 0) {
1430
- lines.push(`**构建规格 (${buildEntries.length}):**`);
1431
- lines.push('');
1432
- lines.push('| 状态 | 文件 | 新增 | 删除 |');
1433
- lines.push('|------|------|------|------|');
1434
- for (const e of buildEntries.sort((a, b) => (b.added + b.deleted) - (a.added + a.deleted))) {
1435
- const name = e.status === 'D' ? getBase(e.oldPath) : getBase(e.newPath);
1436
- lines.push(`| ${statusIcon(e.status)} ${e.status} | ${name} | +${e.added} | -${e.deleted} |`);
1437
- }
1438
- lines.push('');
1439
- }
1440
-
1441
- if (stats.irrelevant > 0) {
1442
- lines.push(`**不相关变更 (${stats.irrelevant}):** 文档/图片/CI/lock/原生平台代码等, 已过滤`);
1443
- lines.push('');
1444
- }
1445
-
1446
- if (entries.length === 0) {
1447
- lines.push('*两个版本之间没有结构差异。*');
1448
- }
1449
-
1450
- return { content: lines.join('\n'), stats, apiDiff, scan1, scan2 };
1451
- }
1452
-
1453
- // ── 主流程 ───────────────────────────────────────────────
1454
-
1455
- /**
1456
- * 主流程: 通过 npm 包发行版对比两个版本, 无需 git。
1457
- *
1458
- * 工作流:
1459
- * 1. 创建临时工作目录
1460
- * 2. 下载/定位两个 tgz 文件 (本地路径或 npm pack)
1461
- * 3. 解压到输出目录
1462
- * 4. 用文件系统 diff 生成 patch + summary + structure + api-diff
1463
- * 5. 清理临时文件
1464
- */
1465
- function main() {
1466
- parseArgs();
1467
-
1468
- // 参数校验
1469
- if (!NPM_PACKAGE) error('必须指定 --npm-package <name>');
1470
- if (!TGZ1) error('必须指定 --tgz1 <版本号或.tgz路径>');
1471
- if (!TGZ2) error('必须指定 --tgz2 <版本号或.tgz路径>');
1472
- if (TGZ1 === TGZ2) error(`两个版本不能相同: ${TGZ1}`);
1473
-
1474
- // 检查依赖 (需要 tar, diff, npm)
1475
- try {
1476
- execFileSync('tar', ['--version'], { stdio: 'pipe' });
1477
- } catch {
1478
- error('未找到 tar, 请先安装');
1479
- }
1480
- // diff: 必须通过行为验证 — PATH 中可能存在不兼容实现
1481
- // (如 DevEco Studio toolchains 的 diff 对不同文件静默返回无差异, 会吞掉全部变更)
1482
- resolveDiffBinary();
1483
- try {
1484
- execFileSync('npm', ['--version'], { stdio: 'pipe' });
1485
- } catch {
1486
- error('未找到 npm, 请先安装');
1487
- }
1488
-
1489
- // 默认输出目录
1490
- if (!OUTPUT_DIR) {
1491
- OUTPUT_DIR = path.join(process.cwd(), 'diff');
1492
- }
1493
- fs.mkdirSync(OUTPUT_DIR, { recursive: true });
1494
-
1495
- // 生成文件名/目录名
1496
- const fileBranch1 = isVersionString(TGZ1) ? `v${TGZ1}` : cleanBranchName(path.basename(TGZ1, '.tgz'));
1497
- const fileBranch2 = isVersionString(TGZ2) ? `v${TGZ2}` : cleanBranchName(path.basename(TGZ2, '.tgz'));
1498
- const outputFile = path.join(OUTPUT_DIR, `diff_${fileBranch1}_${fileBranch2}.patch`);
1499
- const branch1Dir = path.join(OUTPUT_DIR, fileBranch1);
1500
- const branch2Dir = path.join(OUTPUT_DIR, fileBranch2);
1501
-
1502
- console.log('');
1503
- console.log('==========================================');
1504
- console.log(' RNOH 三方库版本对比 (tgz 模式)');
1505
- console.log('==========================================');
1506
- console.log('');
1507
-
1508
- const effectiveExcludes = buildExcludeList();
1509
-
1510
- info(`npm 包名: ${NPM_PACKAGE}`);
1511
- info(`版本 1: ${TGZ1}`);
1512
- info(`版本 2: ${TGZ2}`);
1513
- info(`输出目录: ${OUTPUT_DIR}`);
1514
- info(`输出文件: ${outputFile}`);
1515
- info(`对比方式: tgz (npm 发行版, 无 rename 检测)`);
1516
- if (FULL_DIFF) {
1517
- info(`模式: 完整 diff (--full)`);
1518
- } else {
1519
- info(`模式: 聚焦核心代码 (默认排除噪音)`);
1520
- }
1521
- if (effectiveExcludes.length > 0) {
1522
- info(`排除模式: ${effectiveExcludes.join(' ')}`);
1523
- }
1524
- console.log('');
1525
-
1526
- // ── 1. 创建临时工作目录 ────────────────────────────────
1527
- const workDir = fs.mkdtempSync(path.join(os.tmpdir(), 'generate-diff-tgz-'));
1528
- debug(`临时目录: ${workDir}`);
1529
-
1530
- // ── 2. 下载/定位 tgz 文件 ──────────────────────────────
1531
- info('获取 tgz 文件...');
1532
- const tgzFile1 = resolveTgz(TGZ1, NPM_PACKAGE, workDir);
1533
- const tgzFile2 = resolveTgz(TGZ2, NPM_PACKAGE, workDir);
1534
- console.log('');
1535
-
1536
- // ── 3. 解压到输出目录 ──────────────────────────────────
1537
- info('解压 tgz 文件...');
1538
-
1539
- // 清理旧目录
1540
- for (const dir of [branch1Dir, branch2Dir]) {
1541
- if (fs.existsSync(dir)) {
1542
- fs.rmSync(dir, { recursive: true, force: true });
1543
- }
1544
- }
1545
-
1546
- try {
1547
- extractTgz(tgzFile1, branch1Dir);
1548
- ok(` ✓ ${TGZ1} 已解压到: ${branch1Dir}`);
1549
- } catch (err) {
1550
- error(`解压 ${TGZ1} 失败: ${err.message}`);
1551
- }
1552
-
1553
- try {
1554
- extractTgz(tgzFile2, branch2Dir);
1555
- ok(` ✓ ${TGZ2} 已解压到: ${branch2Dir}`);
1556
- } catch (err) {
1557
- error(`解压 ${TGZ2} 失败: ${err.message}`);
1558
- }
1559
- console.log('');
1560
-
1561
- // 版本标签用于报告
1562
- const version1Label = isVersionString(TGZ1) ? TGZ1 : fileBranch1;
1563
- const version2Label = isVersionString(TGZ2) ? TGZ2 : fileBranch2;
1564
- const commit1Hash = 'npm-tgz';
1565
- const commit2Hash = 'npm-tgz';
1566
-
1567
- const meta = {
1568
- repoUrl: `npm:${NPM_PACKAGE}`,
1569
- branch1: version1Label,
1570
- branch2: version2Label,
1571
- commit1Hash,
1572
- commit2Hash,
1573
- };
1574
-
1575
- // ── 4. --files 模式: 精简 patch ────────────────────────
1576
- if (FILES_LIST) {
1577
- info(`--files 模式: 从 ${FILES_LIST} 读取文件列表`);
1578
-
1579
- let fileEntries;
1580
- try {
1581
- const content = fs.readFileSync(FILES_LIST, 'utf-8');
1582
- fileEntries = content.split('\n').map(s => s.trim()).filter(Boolean);
1583
- } catch (err) {
1584
- error(`无法读取文件列表: ${FILES_LIST} (${err.message})`);
1585
- }
1586
- if (fileEntries.length === 0) {
1587
- error('文件列表为空');
1588
- }
1589
- info(` 共 ${fileEntries.length} 个文件`);
1590
-
1591
- // 逐文件生成 diff
1592
- const lines = [];
1593
- for (const file of fileEntries) {
1594
- const path1 = path.join(branch1Dir, file);
1595
- const path2 = path.join(branch2Dir, file);
1596
- const exists1 = fs.existsSync(path1);
1597
- const exists2 = fs.existsSync(path2);
1598
-
1599
- if (exists1 && exists2) {
1600
- try {
1601
- const output = runDiff(['-u', '-L', `a/${file}`, '-L', `b/${file}`, path1, path2]);
1602
- if (output) {
1603
- lines.push(`diff --git a/${file} b/${file}`);
1604
- lines.push(output);
1605
- }
1606
- } catch (err) {
1607
- warn(`diff 失败 (${file}): ${err.message}`);
1608
- }
1609
- } else if (!exists1 && exists2) {
1610
- lines.push(`diff --git a/${file} b/${file}`);
1611
- lines.push(`new file mode 100644`);
1612
- lines.push(`--- /dev/null`);
1613
- lines.push(`+++ b/${file}`);
1614
- try {
1615
- const content = fs.readFileSync(path2, 'utf-8');
1616
- for (const line of content.split('\n')) lines.push(`+${line}`);
1617
- } catch { /* binary */ }
1618
- lines.push('');
1619
- } else if (exists1 && !exists2) {
1620
- lines.push(`diff --git a/${file} b/${file}`);
1621
- lines.push(`deleted file mode 100644`);
1622
- lines.push(`--- a/${file}`);
1623
- lines.push(`+++ /dev/null`);
1624
- try {
1625
- const content = fs.readFileSync(path1, 'utf-8');
1626
- for (const line of content.split('\n')) lines.push(`-${line}`);
1627
- } catch { /* binary */ }
1628
- lines.push('');
1629
- }
1630
- }
1631
-
1632
- const focusedHeader = [
1633
- `From npm:${NPM_PACKAGE}`,
1634
- `Date: ${new Date().toISOString()}`,
1635
- `Subject: [PATCH] Focused diff between ${version1Label} and ${version2Label} (tgz)`,
1636
- '',
1637
- `npm Package: ${NPM_PACKAGE}`,
1638
- `Version 1: ${version1Label}`,
1639
- `Version 2: ${version2Label}`,
1640
- `Files: ${fileEntries.length} (from ${FILES_LIST})`,
1641
- `Mode: tgz (npm release)`,
1642
- '',
1643
- ].join('\n');
1644
-
1645
- fs.writeFileSync(outputFile, focusedHeader + lines.join('\n'), 'utf-8');
1646
-
1647
- const focusedSize = fs.statSync(outputFile).size;
1648
- ok(`精简 diff 已生成: ${outputFile} (${formatSize(focusedSize)}, ${fileEntries.length} 文件)`);
1649
-
1650
- console.log('');
1651
- console.log('==========================================');
1652
- console.log(' 精简 diff 生成完成 (tgz)');
1653
- console.log('==========================================');
1654
- console.log(` 包名: ${NPM_PACKAGE}`);
1655
- console.log(` 版本 1: ${version1Label}`);
1656
- console.log(` 版本 2: ${version2Label}`);
1657
- console.log(` 文件数: ${fileEntries.length}`);
1658
- console.log(` patch: ${formatSize(focusedSize)}`);
1659
- console.log(` 输出: ${outputFile}`);
1660
- console.log('==========================================');
1661
- console.log('');
1662
-
1663
- // 清理
1664
- fs.rmSync(workDir, { recursive: true, force: true });
1665
- debug(`已清理临时目录: ${workDir}`);
1666
- return;
1667
- }
1668
-
1669
- // ── 5. 生成完整 diff patch ─────────────────────────────
1670
- info('生成 diff (文件系统对比)...');
1671
-
1672
- const diffContent = diffDirsWithFs(branch1Dir, branch2Dir, effectiveExcludes);
1673
-
1674
- // 统计变更文件数
1675
- const numstatMap = fetchNumstatWithFs(branch1Dir, branch2Dir, effectiveExcludes);
1676
- const changedFiles = numstatMap.size;
1677
-
1678
- info(` 变更文件数: ${changedFiles}`);
1679
- if (changedFiles === 0) {
1680
- warn('两个版本之间没有差异');
1681
- }
1682
-
1683
- const header = [
1684
- `From npm:${NPM_PACKAGE}`,
1685
- `Date: ${new Date().toISOString()}`,
1686
- `Subject: [PATCH] Diff between ${version1Label} and ${version2Label} (tgz)`,
1687
- '',
1688
- `npm Package: ${NPM_PACKAGE}`,
1689
- `Version 1: ${version1Label}`,
1690
- `Version 2: ${version2Label}`,
1691
- `Changed files: ${changedFiles}`,
1692
- `Mode: tgz (npm release, no rename detection)`,
1693
- FULL_DIFF ? 'Mode: full (no default excludes)' : 'Mode: filtered (default excludes applied)',
1694
- effectiveExcludes.length > 0 ? `Excludes: ${effectiveExcludes.join(', ')}` : 'Excludes: none',
1695
- '',
1696
- ].join('\n');
1697
-
1698
- fs.writeFileSync(outputFile, header + diffContent, 'utf-8');
1699
- ok(`diff 文件已生成: ${outputFile}`);
1700
-
1701
- // ── 5b. 生成变更摘要 summary.md ───────────────────────
1702
- const summaryFile = path.join(OUTPUT_DIR, `summary_${fileBranch1}_${fileBranch2}.md`);
1703
- info('生成变更摘要...');
1704
- try {
1705
- const summaryContent = generateSummaryWithFs(branch1Dir, branch2Dir, effectiveExcludes, meta);
1706
- fs.writeFileSync(summaryFile, summaryContent, 'utf-8');
1707
- ok(`变更摘要已生成: ${summaryFile}`);
1708
- } catch (err) {
1709
- warn(`生成变更摘要失败: ${err.message}`);
1710
- }
1711
-
1712
- // ── 5c. 生成适配分析报告 structure.md ─────────────────
1713
- const structureFile = path.join(OUTPUT_DIR, `structure_${fileBranch1}_${fileBranch2}.md`);
1714
- info('生成适配分析报告 (接口驱动)...');
1715
- let structureStats = null;
1716
- let apiDiffResult = null;
1717
- let scanResult = null;
1718
- try {
1719
- scanResult = generateStructureSummaryTgz(branch1Dir, branch2Dir, EXCLUDES, meta, branch1Dir, branch2Dir);
1720
- fs.writeFileSync(structureFile, scanResult.content, 'utf-8');
1721
- structureStats = scanResult.stats;
1722
- apiDiffResult = scanResult.apiDiff;
1723
- ok(`适配分析报告已生成: ${structureFile}`);
1724
-
1725
- if (apiDiffResult) {
1726
- console.log('');
1727
- if (apiDiffResult.summary.needsNativeAdaptation) {
1728
- info('⚠️ 需要原生适配 — 接口或 TurboModule Spec 发生变更');
1729
- } else {
1730
- info('✅ 无需原生适配 — 接口未变更, 仅需合并 JS + codegen');
1731
- }
1732
- }
1733
-
1734
- if (structureStats) {
1735
- console.log('');
1736
- info(`文件变更统计 (总数 ${structureStats.total}):`);
1737
- for (const cat of RELEVANCE_ORDER) {
1738
- const count = structureStats.relevanceCounts[cat] || 0;
1739
- if (count > 0) {
1740
- const m = RELEVANCE_META[cat];
1741
- info(` ${m.icon} ${m.title}: ${count}`);
1742
- }
1743
- }
1744
- }
1745
- } catch (err) {
1746
- warn(`生成适配分析报告失败: ${err.message}`);
1747
- }
1748
-
1749
- // ── 5d. 生成接口差异适配检查清单 api-diff.md ──────────
1750
- const apiDiffFile = path.join(OUTPUT_DIR, `api-diff_${fileBranch1}_${fileBranch2}.md`);
1751
- if (apiDiffResult && scanResult && scanResult.scan1 && scanResult.scan2) {
1752
- info('生成接口差异适配检查清单...');
1753
- try {
1754
- const apiReport = scanExports.generateMarkdownReport(scanResult.scan1, scanResult.scan2, apiDiffResult, {
1755
- version1: version1Label,
1756
- version2: version2Label,
1757
- repoUrl: `npm:${NPM_PACKAGE}`,
1758
- });
1759
- fs.writeFileSync(apiDiffFile, apiReport, 'utf-8');
1760
- ok(`接口差异检查清单已生成: ${apiDiffFile}`);
1761
- } catch (err) {
1762
- warn(`生成接口差异检查清单失败: ${err.message}`);
1763
- }
1764
- }
1765
- console.log('');
1766
-
1767
- // ── 6. 显示摘要 ────────────────────────────────────────
1768
- const fileSize = fs.statSync(outputFile).size;
1769
-
1770
- console.log('==========================================');
1771
- console.log(' 对比完成 (tgz 模式)');
1772
- console.log('==========================================');
1773
- console.log(` 包名: ${NPM_PACKAGE}`);
1774
- console.log(` 版本 1: ${version1Label}`);
1775
- console.log(` 版本 2: ${version2Label}`);
1776
- console.log(` 变更文件: ${changedFiles}`);
1777
- console.log(` 模式: ${FULL_DIFF ? '完整 diff (--full)' : '聚焦核心代码 (默认排除噪音)'}`);
1778
- console.log('');
1779
- console.log(` 输出目录: ${OUTPUT_DIR}/`);
1780
- console.log(` ├── ${fileBranch1}/ (版本1 源代码)`);
1781
- console.log(` ├── ${fileBranch2}/ (版本2 源代码)`);
1782
- console.log(` ├── diff_${fileBranch1}_${fileBranch2}.patch (${formatSize(fileSize)})`);
1783
- console.log(` ├── summary_${fileBranch1}_${fileBranch2}.md (变更摘要)`);
1784
- console.log(` ├── structure_${fileBranch1}_${fileBranch2}.md (适配分析报告)`);
1785
- if (apiDiffResult && scanResult && scanResult.scan1 && scanResult.scan2) {
1786
- console.log(` └── api-diff_${fileBranch1}_${fileBranch2}.md (接口差异检查清单)`);
1787
- } else {
1788
- console.log(` └── (接口差异检查清单生成失败)`);
1789
- }
1790
- console.log('==========================================');
1791
- console.log('');
1792
-
1793
- // ── 7. 清理临时目录 ────────────────────────────────────
1794
- fs.rmSync(workDir, { recursive: true, force: true });
1795
- debug(`已清理临时目录: ${workDir}`);
1796
- }
1797
-
1798
- main();
1
+ #!/usr/bin/env node
2
+ /**
3
+ * generate-diff.js
4
+ *
5
+ * 通过 npm tgz 发行版对比 RN 三方库的两个版本, 生成 diff 文件 (.patch)。
6
+ *
7
+ * 工作流:
8
+ * 1. 通过 npm pack 下载指定版本的 tgz (或使用本地 .tgz 文件)
9
+ * 2. 解压到输出目录 (strip package/ 一级)
10
+ * 3. 用文件系统 diff 对比两个目录, 生成 unified diff
11
+ * 4. 生成变更摘要、适配分析报告、接口差异检查清单
12
+ *
13
+ * 默认聚焦三方库核心代码变更, 自动排除 lock 文件、example/、CI 配置、文档、
14
+ * 编译产物 (lib/, dist/, build/) 等与鸿蒙化升级排查无关的噪音文件。
15
+ * 使用 --full 可生成包含所有变更的完整 diff。
16
+ *
17
+ * 输出文件各有侧重:
18
+ * diff_*.patch — diff 补丁 (应用排除规则, 聚焦可合并的代码变更)
19
+ * summary_*.md — 变更摘要 (同 diff 的排除范围, 按目录统计增删行数)
20
+ * structure_*.md — 适配分析报告 (以接口差异为锚点:
21
+ * 先用 rn-scan-exports.cjs 判定是否需要原生适配,
22
+ * 有接口变更时按变更项展开 + 附其他平台实现参考,
23
+ * 无接口变更时只列 JS 合并清单 + 文件计数)
24
+ * api-diff_*.md — 接口差异适配检查清单 (每个变更项含 checkbox, 直接指导适配)
25
+ *
26
+ * 输出结构:
27
+ * diff/
28
+ * ├── <version1>/ (版本1 源代码)
29
+ * ├── <version2>/ (版本2 源代码)
30
+ * ├── diff_<v1>_<v2>.patch (对比 diff 文件)
31
+ * ├── summary_<v1>_<v2>.md (变更摘要, 按文件统计增删行数)
32
+ * ├── structure_<v1>_<v2>.md (适配分析报告, 接口驱动)
33
+ * └── api-diff_<v1>_<v2>.md (接口差异适配检查清单)
34
+ *
35
+ * 默认排除的路径模式 (聚焦核心代码):
36
+ * yarn.lock, package-lock.json, *.lock
37
+ * .yarn/, .github/, example/, docs/
38
+ * CHANGELOG.md, CONTRIBUTING.md, MIGRATION*.md, README.md
39
+ * .gitignore, .yarnrc.yml, .npmrc, .idea/
40
+ * android/, ios/, apple/, windows/ (其他平台原生代码, glob 匹配任意层级)
41
+ *
42
+ * tgz 额外排除 (npm 包含编译产物):
43
+ * lib/, dist/, build/, *.map, *.tsbuildinfo
44
+ * apple-frameworks/, xcframework/, framework/
45
+ *
46
+ * 用法:
47
+ * node generate-diff.js --npm-package <name> --tgz1 <ver> --tgz2 <ver> [选项]
48
+ *
49
+ * 示例:
50
+ * # 对比 async-storage 的 2.2.0 和 3.1.1 (默认排除噪音)
51
+ * node generate-diff.js \
52
+ * --npm-package @react-native-async-storage/async-storage \
53
+ * --tgz1 2.2.0 \
54
+ * --tgz2 3.1.1 \
55
+ * --output-dir ./diff-output
56
+ *
57
+ * # 使用本地已下载的 tgz 文件
58
+ * node generate-diff.js \
59
+ * --npm-package @react-native-async-storage/async-storage \
60
+ * --tgz1 ./pkg-2.2.0.tgz \
61
+ * --tgz2 ./pkg-3.1.1.tgz \
62
+ * --output-dir ./diff-output
63
+ *
64
+ * # 生成完整 diff (不排除任何文件)
65
+ * node generate-diff.js \
66
+ * --npm-package @react-native-async-storage/async-storage \
67
+ * --tgz1 2.2.0 \
68
+ * --tgz2 3.1.1 \
69
+ * --full
70
+ *
71
+ * # 两阶段流程: 第二轮只对 AI 筛选的文件生成精简 patch
72
+ * node generate-diff.js \
73
+ * --npm-package @react-native-async-storage/async-storage \
74
+ * --tgz1 2.2.0 \
75
+ * --tgz2 3.1.1 \
76
+ * --files relevant-files.txt \
77
+ * --output-dir ./diff-focused
78
+ */
79
+
80
+ 'use strict';
81
+
82
+ const { execFileSync, execSync } = require('child_process');
83
+ const os = require('os');
84
+ const path = require('path');
85
+ const fs = require('fs');
86
+
87
+ // 引入 rn-scan-exports.cjs 模块 (接口扫描 + 对比)
88
+ // 位于 3rdLibraryLoop/MCP/scripts/,通过相对路径引用
89
+ const scanExports = require('../../../../MCP/scripts/rn-scan-exports.cjs');
90
+
91
+ // ── 颜色输出 ─────────────────────────────────────────────
92
+ const COLORS = {
93
+ RED: '\x1b[0;31m',
94
+ GREEN: '\x1b[0;32m',
95
+ YELLOW: '\x1b[1;33m',
96
+ BLUE: '\x1b[0;34m',
97
+ CYAN: '\x1b[0;36m',
98
+ NC: '\x1b[0m',
99
+ };
100
+
101
+ function info(msg) { console.log(`${COLORS.BLUE}[INFO]${COLORS.NC} ${msg}`); }
102
+ function ok(msg) { console.log(`${COLORS.GREEN}[OK]${COLORS.NC} ${msg}`); }
103
+ function warn(msg) { console.log(`${COLORS.YELLOW}[WARN]${COLORS.NC} ${msg}`); }
104
+ function error(msg) {
105
+ console.error(`${COLORS.RED}[ERROR]${COLORS.NC} ${msg}`);
106
+ process.exit(1);
107
+ }
108
+ function debug(msg) {
109
+ if (VERBOSE) {
110
+ console.log(`${COLORS.CYAN}[DEBUG]${COLORS.NC} ${msg}`);
111
+ }
112
+ }
113
+
114
+ // ── 默认值 ───────────────────────────────────────────────
115
+ let OUTPUT_DIR = '';
116
+ let VERBOSE = false;
117
+ let FULL_DIFF = false;
118
+ let FILES_LIST = '';
119
+ let TGZ1 = '';
120
+ let TGZ2 = '';
121
+ let NPM_PACKAGE = '';
122
+ const EXCLUDES = [];
123
+
124
+ // ── 默认排除规则 (聚焦三方库核心代码, 过滤与鸿蒙化升级排查无关的噪音) ──
125
+ // 这些路径在 --full 模式下不会被排除
126
+ const DEFAULT_EXCLUDES = [
127
+ // lock 文件
128
+ 'yarn.lock',
129
+ 'package-lock.json',
130
+ '*.lock',
131
+ // 工具链 / CI
132
+ '.yarn/',
133
+ '.github/',
134
+ '.yarnrc.yml',
135
+ '.npmrc',
136
+ '.idea/',
137
+ '.gitignore',
138
+ // 示例工程 (example/ 单数 + examples/ 复数, 兼容 monorepo 命名)
139
+ 'example/',
140
+ 'examples/',
141
+ // 其他平台原生代码 (鸿蒙适配只关心 JS/TS 源码 + TurboModule Spec)
142
+ // 含 ** 的模式使用 glob 匹配任意层级目录内文件
143
+ '**/android/**',
144
+ '**/ios/**',
145
+ '**/apple/**',
146
+ '**/windows/**',
147
+ // 文档
148
+ 'docs/',
149
+ 'CHANGELOG.md',
150
+ 'CONTRIBUTING.md',
151
+ 'MIGRATION*.md',
152
+ 'README.md',
153
+ ];
154
+
155
+ // ── 参数解析 ─────────────────────────────────────────────
156
+ function usage() {
157
+ console.log(`
158
+ RNOH 三方库版本对比脚本 (tgz 模式)
159
+
160
+ 通过 npm tgz 发行版对比两个版本, 无需 git clone。
161
+ --tgz1/--tgz2 可以是本地 .tgz 路径或 npm 版本号 (自动 npm pack 下载)。
162
+ 默认聚焦核心代码, 自动排除 lock 文件、example/、CI 配置、文档、编译产物等噪音。
163
+ 使用 --full 可生成包含所有变更的完整 diff。
164
+
165
+ 输出文件各有侧重:
166
+ diff_*.patch — diff 补丁 (应用排除规则, 聚焦可合并的代码变更)
167
+ summary_*.md — 变更摘要 (同 diff 的排除范围, 按目录统计增删行数)
168
+ structure_*.md — 适配分析报告 (以接口差异为锚点:
169
+ 先用 rn-scan-exports.cjs 判定是否需要原生适配,
170
+ 有接口变更时按变更项展开 + 附其他平台实现参考,
171
+ 无接口变更时只列 JS 合并清单 + 文件计数)
172
+ api-diff_*.md — 接口差异适配检查清单 (每个变更项含 checkbox, 直接指导适配)
173
+
174
+ 输出结构:
175
+ diff/
176
+ ├── <version1>/ (版本1 源代码)
177
+ ├── <version2>/ (版本2 源代码)
178
+ ├── diff_<v1>_<v2>.patch (对比 diff 文件)
179
+ ├── summary_<v1>_<v2>.md (变更摘要, 按文件统计增删行数)
180
+ ├── structure_<v1>_<v2>.md (适配分析报告, 接口驱动)
181
+ └── api-diff_<v1>_<v2>.md (接口差异适配检查清单)
182
+
183
+ 用法:
184
+ node generate-diff.js [选项]
185
+
186
+ 必选参数:
187
+ --npm-package <name> npm 包名 (如 @react-native-async-storage/async-storage)
188
+ --tgz1 <spec> 第一个版本: 版本号 (如 2.2.0) 或本地 .tgz 文件路径
189
+ --tgz2 <spec> 第二个版本: 版本号 (如 3.1.1) 或本地 .tgz 文件路径
190
+
191
+ 可选参数:
192
+ --output-dir <path> 输出目录 (默认: 当前目录下的 diff/ 文件夹)
193
+ --exclude <pattern> 排除文件路径模式, 在默认排除基础上追加 (可多次指定)
194
+ --full 生成完整 diff, 不使用默认排除规则 (仍可配合 --exclude)
195
+ --files <path> 只对指定文件列表生成精简 diff patch (每行一个文件路径)
196
+ 用于两阶段流程: 第一轮生成全量清单 → AI 筛选 → 第二轮精简 patch
197
+ 与 --full 互斥; 指定后跳过排除规则, 只生成指定文件的 diff
198
+ --verbose 显示详细调试信息
199
+ --help, -h 显示帮助
200
+
201
+ 环境变量:
202
+ DIFF_BIN <path> 指定 diff 二进制绝对路径 (默认自动探测: /usr/bin/diff 优先,
203
+ 所有候选须通过行为验证 — PATH 中 DevEco toolchains 等不兼容
204
+ 实现会被跳过)
205
+
206
+ 默认排除的路径 (除非使用 --full):
207
+ yarn.lock, package-lock.json, *.lock
208
+ .yarn/, .github/, example/, examples/, docs/
209
+ android/, ios/, apple/, windows/ (其他平台原生代码, glob 匹配任意层级)
210
+ CHANGELOG.md, CONTRIBUTING.md, MIGRATION*.md, README.md
211
+ .gitignore, .yarnrc.yml, .npmrc, .idea/
212
+ lib/, dist/, build/, *.map, *.tsbuildinfo (tgz 编译产物)
213
+
214
+ 示例:
215
+ # 对比 async-storage 的 2.2.0 和 3.1.1 (默认排除噪音)
216
+ node generate-diff.js \\
217
+ --npm-package @react-native-async-storage/async-storage \\
218
+ --tgz1 2.2.0 \\
219
+ --tgz2 3.1.1 \\
220
+ --output-dir ./diff-output
221
+
222
+ # 使用本地已下载的 tgz 文件
223
+ node generate-diff.js \\
224
+ --npm-package @react-native-async-storage/async-storage \\
225
+ --tgz1 ./pkg-2.2.0.tgz \\
226
+ --tgz2 ./pkg-3.1.1.tgz \\
227
+ --output-dir ./diff-output
228
+
229
+ # 生成完整 diff (不排除任何文件)
230
+ node generate-diff.js \\
231
+ --npm-package @react-native-async-storage/async-storage \\
232
+ --tgz1 2.2.0 \\
233
+ --tgz2 3.1.1 \\
234
+ --full
235
+
236
+ # 在默认排除基础上追加额外排除
237
+ node generate-diff.js \\
238
+ --npm-package @react-native-async-storage/async-storage \\
239
+ --tgz1 2.2.0 \\
240
+ --tgz2 3.1.1 \\
241
+ --exclude packages/core/windows/
242
+
243
+ # 两阶段流程: 第二轮只对 AI 筛选的文件生成精简 patch
244
+ node generate-diff.js \\
245
+ --npm-package @react-native-async-storage/async-storage \\
246
+ --tgz1 2.2.0 \\
247
+ --tgz2 3.1.1 \\
248
+ --files relevant-files.txt \\
249
+ --output-dir ./diff-focused
250
+ `);
251
+ process.exit(0);
252
+ }
253
+
254
+ function parseArgs() {
255
+ const args = process.argv.slice(2);
256
+ let i = 0;
257
+ while (i < args.length) {
258
+ switch (args[i]) {
259
+ case '--output-dir': OUTPUT_DIR = args[i + 1]; i += 2; break;
260
+ case '--exclude': EXCLUDES.push(args[i + 1]); i += 2; break;
261
+ case '--full': FULL_DIFF = true; i += 1; break;
262
+ case '--files': FILES_LIST = args[i + 1]; i += 2; break;
263
+ case '--tgz1': TGZ1 = args[i + 1]; i += 2; break;
264
+ case '--tgz2': TGZ2 = args[i + 1]; i += 2; break;
265
+ case '--npm-package': NPM_PACKAGE = args[i + 1]; i += 2; break;
266
+ case '--verbose': VERBOSE = true; i += 1; break;
267
+ case '--help':
268
+ case '-h': usage(); break;
269
+ default: error(`未知参数: ${args[i]} (使用 --help 查看用法)`);
270
+ }
271
+ }
272
+ }
273
+
274
+ // ── 工具函数 ─────────────────────────────────────────────
275
+
276
+ /**
277
+ * 清理分支名用于文件名 (去除特殊字符)。
278
+ * @param {string} name 分支/tag 名称
279
+ * @returns {string} 清理后的名称
280
+ */
281
+ function cleanBranchName(name) {
282
+ return name
283
+ .replace(/^origin\//, '') // 去除 origin/ 前缀
284
+ .replace(/\//g, '-'); // 替换 / 为 -
285
+ }
286
+
287
+ /**
288
+ * 格式化文件大小。
289
+ * @param {number} bytes 字节数
290
+ * @returns {string} 人类可读的大小
291
+ */
292
+ function formatSize(bytes) {
293
+ if (bytes < 1024) return `${bytes}B`;
294
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
295
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
296
+ }
297
+
298
+ // ── diff 二进制解析 (规避 PATH 中不兼容的 diff 实现) ──────
299
+ // 部分工具链会向 PATH 注入行为不兼容的 diff (如 DevEco Studio 的
300
+ // openharmony toolchains diff: 对不同文件输出为空且退出码为 0,
301
+ // 且不支持 -u 选项仍返回 0), 会静默吞掉所有文件差异,
302
+ // 导致版本对比报告误判为"零变更"。
303
+ // 这里对候选 diff 做行为验证 (smoke test), 只使用验证通过的实现。
304
+
305
+ let DIFF_BIN = null;
306
+
307
+ /**
308
+ * 验证 diff 二进制行为是否可靠。
309
+ * 必须满足: 对内容不同的两个文件, 以 unified 格式产出非空输出。
310
+ * @param {string} bin diff 二进制路径
311
+ * @returns {boolean} 是否通过验证
312
+ */
313
+ function isReliableDiff(bin) {
314
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'diff-smoke-'));
315
+ try {
316
+ const f1 = path.join(tmpDir, 'a.txt');
317
+ const f2 = path.join(tmpDir, 'b.txt');
318
+ fs.writeFileSync(f1, 'line1\n');
319
+ fs.writeFileSync(f2, 'line1\nline2\n');
320
+ let out = '';
321
+ try {
322
+ out = execFileSync(bin, ['-u', '-L', 'a/x', '-L', 'b/x', f1, f2], {
323
+ encoding: 'utf-8',
324
+ stdio: ['pipe', 'pipe', 'pipe'],
325
+ });
326
+ } catch (err) {
327
+ // BSD/GNU diff 对不同文件返回退出码 1, 输出在 err.stdout
328
+ out = (err.stdout || '').toString('utf-8');
329
+ }
330
+ return out.includes('line2');
331
+ } catch {
332
+ return false;
333
+ } finally {
334
+ fs.rmSync(tmpDir, { recursive: true, force: true });
335
+ }
336
+ }
337
+
338
+ /**
339
+ * 解析可用的 diff 二进制 (带缓存)。
340
+ * 优先级: 环境变量 DIFF_BIN > /usr/bin/diff > PATH 中的 diff。
341
+ * 每个候选都必须通过行为验证, 全部失败则报错退出。
342
+ * @returns {string} 验证过的 diff 二进制路径
343
+ */
344
+ function resolveDiffBinary() {
345
+ if (DIFF_BIN) return DIFF_BIN;
346
+ const candidates = [];
347
+ if (process.env.DIFF_BIN) candidates.push(process.env.DIFF_BIN);
348
+ candidates.push('/usr/bin/diff');
349
+ candidates.push('diff');
350
+ for (const candidate of candidates) {
351
+ try {
352
+ if (isReliableDiff(candidate)) {
353
+ DIFF_BIN = candidate;
354
+ debug(`diff 二进制 (已通过行为验证): ${candidate}`);
355
+ return DIFF_BIN;
356
+ }
357
+ warn(`diff 候选未通过行为验证 (对不同文件无输出或缺少 -u/-L 支持), 跳过: ${candidate}`);
358
+ } catch (err) {
359
+ debug(`diff 候选不可用: ${candidate} (${err.message})`);
360
+ }
361
+ }
362
+ error(
363
+ '未找到可靠的 diff 二进制。PATH 中的 diff 行为异常 ' +
364
+ '(可能被 DevEco Studio toolchains 等劫持, 对不同文件静默返回无差异), ' +
365
+ '可通过环境变量 DIFF_BIN 指定绝对路径 (如 /usr/bin/diff)'
366
+ );
367
+ }
368
+
369
+ /**
370
+ * 用已验证的 diff 二进制执行 diff。
371
+ * 退出码约定: 0 = 相同, 1 = 有差异 (输出在 stdout), 其他 = 错误。
372
+ * @param {string[]} args diff 参数 (不含二进制名)
373
+ * @returns {string} diff 输出 (无差异返回空串)
374
+ */
375
+ function runDiff(args) {
376
+ const bin = resolveDiffBinary();
377
+ try {
378
+ return execFileSync(bin, args, {
379
+ encoding: 'utf-8',
380
+ stdio: ['pipe', 'pipe', 'pipe'],
381
+ maxBuffer: 50 * 1024 * 1024,
382
+ });
383
+ } catch (err) {
384
+ const stdout = (err.stdout || '').toString('utf-8');
385
+ if (stdout) return stdout; // 退出码 1: 有差异
386
+ if (err.status === 1) return ''; // 有差异但无输出 (异常实现, 视为无差异)
387
+ throw new Error(
388
+ `diff 执行失败 (退出码 ${err.status}): ${(err.stderr || '').toString('utf-8').trim()}`
389
+ );
390
+ }
391
+ }
392
+
393
+ /**
394
+ * tgz 模式额外排除规则。
395
+ *
396
+ * npm tgz 包含编译产物 (lib/, dist/, build/) 和 source maps (*.map),
397
+ * 这些在 git 仓库中通常被 .gitignore 排除, 不会出现在 diff 中。
398
+ * tgz 模式需要额外排除以聚焦源码变更。
399
+ */
400
+ const TGZ_EXTRA_EXCLUDES = [
401
+ 'lib/',
402
+ 'dist/',
403
+ 'build/',
404
+ '*.map',
405
+ '*.tsbuildinfo',
406
+ // 预编译二进制框架 (npm 包中的 xcframework 等)
407
+ 'apple-frameworks/',
408
+ '**/*.xcframework/**',
409
+ '**/*.framework/**',
410
+ ];
411
+
412
+ /**
413
+ * 构建最终的排除列表。
414
+ * --full 模式下只用用户 --exclude 指定的; 否则合并默认排除 + 用户追加。
415
+ * tgz 模式始终合并编译产物排除规则。
416
+ * @returns {string[]} 排除路径模式数组
417
+ */
418
+ function buildExcludeList() {
419
+ if (FULL_DIFF) {
420
+ return [...TGZ_EXTRA_EXCLUDES, ...EXCLUDES];
421
+ }
422
+ return [...DEFAULT_EXCLUDES, ...TGZ_EXTRA_EXCLUDES, ...EXCLUDES];
423
+ }
424
+
425
+ // ── tgz 模式: npm 包下载 + 解压 ──────────────────────────
426
+
427
+ /**
428
+ * 判断字符串是否为有效的 npm 语义版本号。
429
+ * 匹配 x.y.z 格式 (可含 prerelease 标签如 -next.0)。
430
+ * @param {string} str 待判断的字符串
431
+ * @returns {boolean}
432
+ */
433
+ function isVersionString(str) {
434
+ return /^\d+\.\d+\.\d+/.test(str);
435
+ }
436
+
437
+ /**
438
+ * 获取 tgz 文件: 如果是本地路径直接返回, 如果是版本号则通过 npm pack 下载。
439
+ * @param {string} tgzSpec 本地 .tgz 路径或版本号 (如 "2.2.0")
440
+ * @param {string} npmPackage npm 包名 (如 @react-native-async-storage/async-storage)
441
+ * @param {string} workDir 临时工作目录
442
+ * @returns {string} tgz 文件的绝对路径
443
+ */
444
+ function resolveTgz(tgzSpec, npmPackage, workDir) {
445
+ // 如果是本地文件路径
446
+ if (fs.existsSync(tgzSpec) && tgzSpec.endsWith('.tgz')) {
447
+ const absPath = path.resolve(tgzSpec);
448
+ ok(`使用本地 tgz 文件: ${absPath}`);
449
+ return absPath;
450
+ }
451
+
452
+ // 如果是版本号, 通过 npm pack 下载
453
+ if (isVersionString(tgzSpec)) {
454
+ if (!npmPackage) {
455
+ error(`--tgz1/--tgz2 指定了版本号 "${tgzSpec}" 但未指定 --npm-package`);
456
+ }
457
+ info(`通过 npm pack 下载: ${npmPackage}@${tgzSpec}`);
458
+ const tgzFile = execSync(`npm pack ${npmPackage}@${tgzSpec}`, {
459
+ cwd: workDir,
460
+ encoding: 'utf-8',
461
+ stdio: ['pipe', 'pipe', 'pipe'],
462
+ timeout: 60000,
463
+ }).trim();
464
+ const absPath = path.join(workDir, tgzFile);
465
+ if (!fs.existsSync(absPath)) {
466
+ error(`npm pack 下载失败: ${npmPackage}@${tgzSpec} (预期文件: ${absPath})`);
467
+ }
468
+ ok(`已下载: ${tgzFile}`);
469
+ return absPath;
470
+ }
471
+
472
+ error(`无效的 tgz 参数: "${tgzSpec}" — 应为本地 .tgz 文件路径或版本号 (如 2.2.0)`);
473
+ }
474
+
475
+ /**
476
+ * 解压 tgz 文件到目标目录 (npm tgz 内部为 package/ 目录, 去除一级)。
477
+ * @param {string} tgzFile tgz 文件路径
478
+ * @param {string} destDir 目标目录
479
+ */
480
+ function extractTgz(tgzFile, destDir) {
481
+ fs.mkdirSync(destDir, { recursive: true });
482
+ // npm pack 生成的 tgz 内部结构为 package/..., 用 --strip-components=1 去除
483
+ execSync(`tar xzf "${tgzFile}" -C "${destDir}" --strip-components=1`, {
484
+ stdio: 'pipe',
485
+ timeout: 60000,
486
+ });
487
+ }
488
+
489
+ // ── 文件系统遍历 + diff ────────────────────────────────
490
+
491
+ /**
492
+ * 将排除模式转换为正则表达式 (用于文件系统遍历)。
493
+ * 支持两种模式:
494
+ * - 含 glob 双星号的模式 (如 android 子树): 匹配任意层级同名目录下的文件
495
+ * - 普通路径: example/ → 匹配根目录 example/ 下的文件
496
+ * @param {string} pattern 排除模式
497
+ * @returns {RegExp} 匹配正则
498
+ */
499
+ function excludePatternToRegex(pattern) {
500
+ // 去除尾部 /
501
+ let p = pattern.replace(/\/$/, '');
502
+ if (p.includes('**')) {
503
+ // glob 双星号模式: 匹配任意路径中包含对应目录名的文件
504
+ const escaped = p
505
+ .replace(/\*\*/g, '<<<GLOB>>>')
506
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
507
+ .replace(/<<<GLOB>>>/g, '.*')
508
+ .replace(/\*/g, '[^/]*');
509
+ return new RegExp(`(^|/)${escaped}(/|$)`);
510
+ }
511
+ // 普通模式: 匹配根目录或任意层级同名目录
512
+ const escaped = p.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^/]*');
513
+ return new RegExp(`(^|/)${escaped}(/|$)`);
514
+ }
515
+
516
+ /**
517
+ * 判断文件相对路径是否应被排除。
518
+ * @param {string} relPath 相对于源码根目录的路径
519
+ * @param {RegExp[]} excludeRegexes 排除正则数组
520
+ * @returns {boolean} 是否应排除
521
+ */
522
+ function shouldExclude(relPath, excludeRegexes) {
523
+ return excludeRegexes.some(re => re.test(relPath));
524
+ }
525
+
526
+ /**
527
+ * 递归遍历目录, 返回所有文件的相对路径列表。
528
+ * @param {string} dir 根目录
529
+ * @param {RegExp[]} excludeRegexes 排除正则数组
530
+ * @returns {string[]} 相对路径列表 (POSIX 风格 / 分隔)
531
+ */
532
+ function walkDir(dir, excludeRegexes = []) {
533
+ const results = [];
534
+ function walk(currentDir, relPrefix) {
535
+ let entries;
536
+ try {
537
+ entries = fs.readdirSync(currentDir, { withFileTypes: true });
538
+ } catch {
539
+ return;
540
+ }
541
+ for (const entry of entries) {
542
+ const relPath = relPrefix ? `${relPrefix}/${entry.name}` : entry.name;
543
+ // 检查是否被排除
544
+ if (shouldExclude(relPath, excludeRegexes)) continue;
545
+ if (entry.isDirectory()) {
546
+ walk(path.join(currentDir, entry.name), relPath);
547
+ } else if (entry.isFile()) {
548
+ results.push(relPath);
549
+ }
550
+ }
551
+ }
552
+ walk(dir, '');
553
+ return results.sort();
554
+ }
555
+
556
+ /**
557
+ * 用 GNU diff 对比两个目录, 生成 unified diff 输出。
558
+ *
559
+ * @param {string} dir1 目录1
560
+ * @param {string} dir2 目录2
561
+ * @param {string[]} excludePatterns 排除模式列表
562
+ * @returns {string} diff 输出
563
+ */
564
+ function diffDirsWithFs(dir1, dir2, excludePatterns) {
565
+ const excludeRegexes = excludePatterns.map(excludePatternToRegex);
566
+
567
+ // 收集两个目录的文件列表 (应用排除)
568
+ const files1 = walkDir(dir1, excludeRegexes);
569
+ const files2 = walkDir(dir2, excludeRegexes);
570
+
571
+ const set1 = new Set(files1);
572
+ const set2 = new Set(files2);
573
+
574
+ // 合并所有文件 (排序)
575
+ const allFiles = [...new Set([...files1, ...files2])].sort();
576
+
577
+ const lines = [];
578
+
579
+ for (const file of allFiles) {
580
+ const in1 = set1.has(file);
581
+ const in2 = set2.has(file);
582
+ const path1 = path.join(dir1, file);
583
+ const path2 = path.join(dir2, file);
584
+
585
+ if (in1 && in2) {
586
+ // 两边都有: 用 diff 对比内容
587
+ // 注意: 必须使用 runDiff (行为验证过的 diff 二进制),
588
+ // 不能裸调 PATH 中的 diff — 部分 toolchains 的 diff 对不同文件静默无输出
589
+ try {
590
+ const output = runDiff(['-u', '-L', `a/${file}`, '-L', `b/${file}`, path1, path2]);
591
+ if (output) {
592
+ lines.push(`diff --git a/${file} b/${file}`);
593
+ lines.push(output);
594
+ }
595
+ } catch (err) {
596
+ warn(`diff 失败 (${file}): ${err.message}`);
597
+ }
598
+ } else if (in1 && !in2) {
599
+ // 仅在 dir1: 删除
600
+ lines.push(`diff --git a/${file} b/${file}`);
601
+ lines.push(`deleted file mode 100644`);
602
+ lines.push(`--- a/${file}`);
603
+ lines.push(`+++ /dev/null`);
604
+ try {
605
+ const content = fs.readFileSync(path1, 'utf-8');
606
+ for (const line of content.split('\n')) {
607
+ lines.push(`-${line}`);
608
+ }
609
+ } catch { /* binary */ }
610
+ lines.push('');
611
+ } else if (!in1 && in2) {
612
+ // 仅在 dir2: 新增
613
+ lines.push(`diff --git a/${file} b/${file}`);
614
+ lines.push(`new file mode 100644`);
615
+ lines.push(`--- /dev/null`);
616
+ lines.push(`+++ b/${file}`);
617
+ try {
618
+ const content = fs.readFileSync(path2, 'utf-8');
619
+ for (const line of content.split('\n')) {
620
+ lines.push(`+${line}`);
621
+ }
622
+ } catch { /* binary */ }
623
+ lines.push('');
624
+ }
625
+ // rename 检测: tgz 模式不做跨文件相似度匹配
626
+ // (npm 包之间的文件移动较少见, 且 diff -u 已能展示内容差异)
627
+ }
628
+
629
+ return lines.join('\n');
630
+ }
631
+
632
+ /**
633
+ * 统计文件增删行数 (文件系统遍历, 替代 git diff --numstat)。
634
+ * 对每个有差异的文件, 用 diff 统计 +N / -N。
635
+ *
636
+ * @param {string} dir1 目录1
637
+ * @param {string} dir2 目录2
638
+ * @param {string[]} excludePatterns 排除模式列表
639
+ * @returns {Map<string, {added: number, deleted: number}>} filepath → numstat
640
+ */
641
+ function fetchNumstatWithFs(dir1, dir2, excludePatterns) {
642
+ const excludeRegexes = excludePatterns.map(excludePatternToRegex);
643
+ const files1 = walkDir(dir1, excludeRegexes);
644
+ const files2 = walkDir(dir2, excludeRegexes);
645
+ const set1 = new Set(files1);
646
+ const set2 = new Set(files2);
647
+ const allFiles = [...new Set([...files1, ...files2])].sort();
648
+
649
+ const result = new Map();
650
+
651
+ for (const file of allFiles) {
652
+ const in1 = set1.has(file);
653
+ const in2 = set2.has(file);
654
+ const path1 = path.join(dir1, file);
655
+ const path2 = path.join(dir2, file);
656
+
657
+ let added = 0;
658
+ let deleted = 0;
659
+
660
+ if (in1 && in2) {
661
+ try {
662
+ const output = runDiff([path1, path2]);
663
+ if (output) {
664
+ for (const line of output.split('\n')) {
665
+ if (line.startsWith('> ')) added++;
666
+ if (line.startsWith('< ')) deleted++;
667
+ }
668
+ }
669
+ } catch (err) {
670
+ warn(`diff 统计失败 (${file}): ${err.message}`);
671
+ }
672
+ } else if (!in1 && in2) {
673
+ // 新文件: 全部为新增行
674
+ try {
675
+ const content = fs.readFileSync(path2, 'utf-8');
676
+ added = content.split('\n').filter(Boolean).length;
677
+ } catch { /* binary */ }
678
+ } else if (in1 && !in2) {
679
+ // 删除文件: 全部为删除行
680
+ try {
681
+ const content = fs.readFileSync(path1, 'utf-8');
682
+ deleted = content.split('\n').filter(Boolean).length;
683
+ } catch { /* binary */ }
684
+ }
685
+
686
+ if (added > 0 || deleted > 0) {
687
+ result.set(file, { added, deleted });
688
+ }
689
+ }
690
+
691
+ return result;
692
+ }
693
+
694
+ // ── 结构变更分析: 鸿蒙相关性分类 ─────────────────────────
695
+
696
+ /**
697
+ * 结构分析使用的排除集。
698
+ *
699
+ * 与 DEFAULT_EXCLUDES 一致, 排除所有非 JS/TS 文件 (原生平台代码、文档、CI 等)。
700
+ * 只保留 JS/TS 源码、TurboModule Spec、包配置和构建规格。
701
+ */
702
+ const MINIMAL_EXCLUDES = [
703
+ // lock 文件
704
+ 'yarn.lock',
705
+ 'package-lock.json',
706
+ '*.lock',
707
+ // 工具链 / CI
708
+ '.yarn/',
709
+ '.github/',
710
+ '.yarnrc.yml',
711
+ '.npmrc',
712
+ '.idea/',
713
+ '.gitignore',
714
+ // 示例工程
715
+ 'example/',
716
+ 'examples/',
717
+ // 其他平台原生代码 (只关心 JS/TS 层)
718
+ '**/android/**',
719
+ '**/ios/**',
720
+ '**/apple/**',
721
+ '**/windows/**',
722
+ // Kotlin Multiplatform (shared-storage 等)
723
+ '**/commonMain/**',
724
+ '**/androidMain/**',
725
+ '**/appleMain/**',
726
+ // 文档
727
+ 'docs/',
728
+ 'CHANGELOG.md',
729
+ 'CONTRIBUTING.md',
730
+ 'MIGRATION*.md',
731
+ 'README.md',
732
+ // 二进制文件
733
+ '**/*.jar',
734
+ '**/*.aar',
735
+ '**/*.dylib',
736
+ '**/*.so',
737
+ '**/gradle/wrapper/**',
738
+ ];
739
+
740
+ /**
741
+ * 鸿蒙相关性分类。
742
+ *
743
+ * 将变更文件按对鸿蒙 RN 三方库适配的相关性分为以下类别:
744
+ *
745
+ * @typedef {string} RelevanceCategory
746
+ * @property {'js-ts-source'} JS/TS 源码 — 需合并到 harmony/src/ 的文件
747
+ * @property {'turbo-module-spec'} TurboModule Spec — 定义原生接口, 需 codegen
748
+ * @property {'package-config'} 包配置 — package.json, tsconfig
749
+ * @property {'build-spec'} 构建规格 — podspec, CMakeLists
750
+ * @property {'not-relevant'} 不相关 — 文档/图片/CI/lock/原生平台代码等
751
+ */
752
+
753
+ /**
754
+ * 对变更文件进行鸿蒙相关性分类。
755
+ *
756
+ * 判断依据:
757
+ * 1. 文件扩展名 (源码类型)
758
+ * 2. 文件路径 (是否在 src/, android/, ios/ 等关键目录)
759
+ * 3. 文件名模式 (Native*.ts → TurboModule Spec)
760
+ *
761
+ * @param {ChangeEntry} entry 变更条目
762
+ * @returns {RelevanceCategory} 相关性类别
763
+ */
764
+ function classifyFileRelevance(entry) {
765
+ // 优先用 newPath 判断 (新增/修改的文件), D 类型用 oldPath
766
+ const checkPath = entry.status === 'D' ? entry.oldPath : entry.newPath;
767
+ const lower = checkPath.toLowerCase();
768
+ const ext = checkPath.substring(checkPath.lastIndexOf('.') + 1).toLowerCase();
769
+ const basename = checkPath.substring(checkPath.lastIndexOf('/') + 1);
770
+
771
+ // ── TurboModule Spec: Native*.ts — 最高优先级 ──
772
+ // 匹配 NativeXxx.ts / NativeXxx.tsx, 通常在 src/ 下
773
+ if (/^Native[A-Z]/.test(basename) && ['ts', 'tsx'].includes(ext)) {
774
+ return 'turbo-module-spec';
775
+ }
776
+
777
+ // ── JS/TS 源码: src/ 下的 .ts/.tsx/.js/.jsx ──
778
+ // 也匹配根目录或 packages/*/src/ 下的源码
779
+ if (['ts', 'tsx', 'js', 'jsx'].includes(ext)) {
780
+ if (lower.includes('/src/') || lower.startsWith('src/')) {
781
+ return 'js-ts-source';
782
+ }
783
+ // 根目录的 index.ts/index.js 也算源码入口
784
+ if (!checkPath.includes('/') && ['index.ts', 'index.js', 'index.tsx'].includes(basename.toLowerCase())) {
785
+ return 'js-ts-source';
786
+ }
787
+ // jest/ 下的 mock 也算 (与鸿蒙测试相关)
788
+ if (lower.includes('/jest/') || lower.startsWith('jest/')) {
789
+ return 'js-ts-source';
790
+ }
791
+ }
792
+
793
+ // ── 包配置: package.json, tsconfig*.json ──
794
+ if (basename === 'package.json' || basename.toLowerCase().startsWith('tsconfig')) {
795
+ return 'package-config';
796
+ }
797
+
798
+ // ── 构建规格: podspec, CMakeLists.txt ──
799
+ if (ext === 'podspec' || basename.toLowerCase() === 'cmakelists.txt') {
800
+ return 'build-spec';
801
+ }
802
+
803
+ // ── 其他: 不相关 (包括原生平台代码、文档、图片、CI 等) ──
804
+ return 'not-relevant';
805
+ }
806
+
807
+ /**
808
+ * 相关性类别的元信息 (展示顺序、标题、说明)。
809
+ */
810
+ const RELEVANCE_META = {
811
+ 'js-ts-source': {
812
+ title: 'JS/TS 源码',
813
+ icon: '📄',
814
+ desc: '需合并到鸿蒙化仓库 src/ 的源代码文件',
815
+ showDetail: true,
816
+ },
817
+ 'turbo-module-spec': {
818
+ title: 'TurboModule Spec',
819
+ icon: '🔌',
820
+ desc: '定义原生模块接口, 需更新 codegen 并同步 ETS/C++ 层',
821
+ showDetail: true,
822
+ },
823
+ 'package-config': {
824
+ title: '包配置',
825
+ icon: '📦',
826
+ desc: 'package.json / tsconfig 变更, 影响 codegen 和依赖',
827
+ showDetail: true,
828
+ },
829
+ 'build-spec': {
830
+ title: '构建规格',
831
+ icon: '🔧',
832
+ desc: 'podspec / CMakeLists 等构建配置',
833
+ showDetail: true,
834
+ },
835
+ 'not-relevant': {
836
+ title: '不相关变更 (已过滤)',
837
+ icon: '🔇',
838
+ desc: '文档/图片/CI/lock/原生平台代码等, 与鸿蒙适配无关',
839
+ showDetail: false,
840
+ },
841
+ };
842
+
843
+ /**
844
+ * 相关性类别的展示顺序。
845
+ */
846
+ const RELEVANCE_ORDER = [
847
+ 'turbo-module-spec',
848
+ 'js-ts-source',
849
+ 'package-config',
850
+ 'build-spec',
851
+ 'not-relevant',
852
+ ];
853
+
854
+ // ── 结构变更分析 ─────────────────────────────────────────
855
+
856
+ /**
857
+ * 变更文件的状态类型。
858
+ * @typedef {'R'|'M'|'A'|'D'|'C'|'T'} FileStatus
859
+ * R = Renamed (重命名/移动, 可能含内容修改)
860
+ * M = Modified (原地修改)
861
+ * A = Added (纯新增)
862
+ * D = Deleted (纯删除)
863
+ * C = Copied (复制)
864
+ * T = Type changed (文件类型变更, 如 文件→符号链接)
865
+ */
866
+
867
+ /**
868
+ * 单个变更文件的描述。
869
+ * @typedef {Object} ChangeEntry
870
+ * @property {FileStatus} status - 变更状态
871
+ * @property {string} oldPath - 旧路径 (M/A 为空或同 newPath)
872
+ * @property {string} newPath - 新路径 (D 为空或同 oldPath)
873
+ * @property {number} similarity - 相似度百分比 (仅 R/C 有意义, 0-100)
874
+ * @property {number} added - 新增行数 (来自 numstat)
875
+ * @property {number} deleted - 删除行数 (来自 numstat)
876
+ */
877
+
878
+ /**
879
+ * 分析两个目录之间的结构变更。
880
+ *
881
+ * 通过文件存在性判断状态:
882
+ * - 两边都有 → Modified
883
+ * - 仅 dir2 有 → Added
884
+ * - 仅 dir1 有 → Deleted
885
+ * (rename 检测在 tgz 模式下降级为 modified + added + deleted)
886
+ *
887
+ * @param {string} dir1 目录1源码
888
+ * @param {string} dir2 目录2源码
889
+ * @param {string[]} extraExcludes 额外排除模式
890
+ * @returns {{entries: ChangeEntry[], stats: Object}} 变更条目列表和统计信息
891
+ */
892
+ function analyzeStructuralChangesWithFs(dir1, dir2, extraExcludes = []) {
893
+ const structureExcludes = [...MINIMAL_EXCLUDES, ...TGZ_EXTRA_EXCLUDES, ...extraExcludes];
894
+ const excludeRegexes = structureExcludes.map(excludePatternToRegex);
895
+
896
+ const files1 = walkDir(dir1, excludeRegexes);
897
+ const files2 = walkDir(dir2, excludeRegexes);
898
+ const set1 = new Set(files1);
899
+ const set2 = new Set(files2);
900
+ const allFiles = [...new Set([...files1, ...files2])].sort();
901
+
902
+ const numstatMap = fetchNumstatWithFs(dir1, dir2, structureExcludes);
903
+ const entries = [];
904
+
905
+ for (const file of allFiles) {
906
+ const in1 = set1.has(file);
907
+ const in2 = set2.has(file);
908
+
909
+ let status, oldPath, newPath;
910
+ if (in1 && in2) {
911
+ // 检查是否有实际差异
912
+ const ns = numstatMap.get(file);
913
+ if (!ns) continue; // 无差异, 跳过
914
+ status = 'M';
915
+ oldPath = file;
916
+ newPath = file;
917
+ } else if (!in1 && in2) {
918
+ status = 'A';
919
+ oldPath = '';
920
+ newPath = file;
921
+ } else if (in1 && !in2) {
922
+ status = 'D';
923
+ oldPath = file;
924
+ newPath = '';
925
+ } else {
926
+ continue;
927
+ }
928
+
929
+ const ns = numstatMap.get(file) || { added: 0, deleted: 0 };
930
+ entries.push({
931
+ status,
932
+ oldPath,
933
+ newPath,
934
+ similarity: 100,
935
+ added: ns.added,
936
+ deleted: ns.deleted,
937
+ });
938
+ }
939
+
940
+ // 鸿蒙相关性分类
941
+ for (const entry of entries) {
942
+ entry.relevance = classifyFileRelevance(entry);
943
+ }
944
+
945
+ // 统计
946
+ const relevantEntries = entries.filter(e => e.relevance !== 'not-relevant');
947
+ const irrelevantEntries = entries.filter(e => e.relevance === 'not-relevant');
948
+ const relevanceCounts = {};
949
+ for (const cat of RELEVANCE_ORDER) {
950
+ relevanceCounts[cat] = entries.filter(e => e.relevance === cat).length;
951
+ }
952
+
953
+ const stats = {
954
+ total: entries.length,
955
+ relevant: relevantEntries.length,
956
+ irrelevant: irrelevantEntries.length,
957
+ renamed: 0,
958
+ modified: entries.filter(e => e.status === 'M').length,
959
+ added: entries.filter(e => e.status === 'A').length,
960
+ deleted: entries.filter(e => e.status === 'D').length,
961
+ copied: 0,
962
+ typeChanged: 0,
963
+ totalAdded: entries.reduce((sum, e) => sum + e.added, 0),
964
+ totalDeleted: entries.reduce((sum, e) => sum + e.deleted, 0),
965
+ relevanceCounts,
966
+ };
967
+
968
+ return { entries, stats };
969
+ }
970
+
971
+ /**
972
+ * 生成变更摘要 Markdown。
973
+ *
974
+ * @param {string} dir1 目录1源码
975
+ * @param {string} dir2 目录2源码
976
+ * @param {string[]} excludePatterns 排除模式列表
977
+ * @param {object} meta 元信息
978
+ * @returns {string} 摘要 Markdown 内容
979
+ */
980
+ function generateSummaryWithFs(dir1, dir2, excludePatterns, meta) {
981
+ const numstatMap = fetchNumstatWithFs(dir1, dir2, excludePatterns);
982
+
983
+ const files = [];
984
+ let totalAdded = 0;
985
+ let totalDeleted = 0;
986
+
987
+ for (const [filepath, { added, deleted }] of numstatMap) {
988
+ files.push({ filepath, added, deleted });
989
+ totalAdded += added;
990
+ totalDeleted += deleted;
991
+ }
992
+
993
+ // 按目录分组
994
+ const groups = {};
995
+ for (const f of files) {
996
+ const sep = f.filepath.lastIndexOf('/');
997
+ const dir = sep >= 0 ? f.filepath.substring(0, sep) : '(根目录)';
998
+ if (!groups[dir]) groups[dir] = [];
999
+ groups[dir].push(f);
1000
+ }
1001
+
1002
+ const lines = [];
1003
+ lines.push(`# 变更摘要: ${meta.branch1} → ${meta.branch2}`);
1004
+ lines.push('');
1005
+ lines.push(`| 项目 | 值 |`);
1006
+ lines.push(`|------|------|`);
1007
+ lines.push(`| 仓库 | ${meta.repoUrl} |`);
1008
+ lines.push(`| 版本 1 | ${meta.branch1} (${meta.commit1Hash}) |`);
1009
+ lines.push(`| 版本 2 | ${meta.branch2} (${meta.commit2Hash}) |`);
1010
+ lines.push(`| 变更文件数 | ${files.length} |`);
1011
+ lines.push(`| 新增行数 | +${totalAdded} |`);
1012
+ lines.push(`| 删除行数 | -${totalDeleted} |`);
1013
+ lines.push(`| 对比方式 | tgz (npm 发行版) |`);
1014
+ if (excludePatterns.length > 0) {
1015
+ lines.push(`| 排除模式 | ${excludePatterns.join(', ')} |`);
1016
+ } else {
1017
+ lines.push(`| 排除模式 | 无 (--full 模式) |`);
1018
+ }
1019
+ lines.push('');
1020
+ lines.push('---');
1021
+ lines.push('');
1022
+
1023
+ for (const dir of Object.keys(groups).sort()) {
1024
+ lines.push(`## ${dir}/`);
1025
+ lines.push('');
1026
+ lines.push('| 文件 | 新增 | 删除 |');
1027
+ lines.push('|------|------|------|');
1028
+ for (const f of groups[dir].sort((a, b) => (b.added + b.deleted) - (a.added + a.deleted))) {
1029
+ lines.push(`| ${f.filepath} | +${f.added} | -${f.deleted} |`);
1030
+ }
1031
+ lines.push('');
1032
+ }
1033
+
1034
+ return lines.join('\n');
1035
+ }
1036
+
1037
+ /**
1038
+ * 扫描目录的导出接口 (支持 monorepo)。
1039
+ *
1040
+ * 如果根目录有 package.json 且包含 codegenConfig 或 source 字段, 直接扫描。
1041
+ * 否则递归查找 packages 子目录中有 codegenConfig 的包。
1042
+ *
1043
+ * @param {string} dir 源码目录
1044
+ * @returns {Object|null} 扫描结果, 未找到包返回 null
1045
+ */
1046
+ function scanDirectoryForExports(dir) {
1047
+ // 先尝试根目录
1048
+ try {
1049
+ const result = scanExports.scanDirectory(dir);
1050
+ if (result.entryFile || (result.turboModuleSpecs && result.turboModuleSpecs.length > 0)) {
1051
+ return result;
1052
+ }
1053
+ } catch { /* continue */ }
1054
+
1055
+ // monorepo: 查找 packages/*/ 子目录
1056
+ const packagesDir = path.join(dir, 'packages');
1057
+ if (fs.existsSync(packagesDir)) {
1058
+ let entries;
1059
+ try {
1060
+ entries = fs.readdirSync(packagesDir, { withFileTypes: true });
1061
+ } catch {
1062
+ return null;
1063
+ }
1064
+
1065
+ for (const entry of entries) {
1066
+ if (!entry.isDirectory()) continue;
1067
+ const subDir = path.join(packagesDir, entry.name);
1068
+ const pkgPath = path.join(subDir, 'package.json');
1069
+ if (!fs.existsSync(pkgPath)) continue;
1070
+
1071
+ try {
1072
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
1073
+ // 优先选择有 codegenConfig 的包 (那是 TurboModule 库)
1074
+ // 其次选择有 source 字段的包
1075
+ if (pkg.codegenConfig || pkg.source || (pkg.name && !pkg.name.includes('eslint-config') && !pkg.name.includes('website'))) {
1076
+ const result = scanExports.scanDirectory(subDir);
1077
+ if (result.entryFile || (result.turboModuleSpecs && result.turboModuleSpecs.length > 0)) {
1078
+ debug(`接口扫描: 使用 monorepo 子包 packages/${entry.name}`);
1079
+ return result;
1080
+ }
1081
+ }
1082
+ } catch { /* continue */ }
1083
+ }
1084
+ }
1085
+
1086
+ // 回退: 再试根目录 (即使没有 entryFile, 也可能有 TurboModule Spec)
1087
+ try {
1088
+ return scanExports.scanDirectory(dir);
1089
+ } catch {
1090
+ return null;
1091
+ }
1092
+ }
1093
+
1094
+ /**
1095
+ * 生成接口驱动的结构变更摘要。
1096
+ *
1097
+ * 核心逻辑:
1098
+ * 1. 调用 rn-scan-exports.cjs 对比两个版本的 JS 层接口 (导出 + TurboModule Spec)
1099
+ * 2. 如果接口未变更 → 报告"仅 JS 合并 + codegen, 无需原生适配", 文件变更只计数
1100
+ * 3. 如果接口有变更 → 以接口变更项为锚点组织内容:
1101
+ * - 每个新增/变更的接口方法下, 附上其他平台 (Android/iOS) 的实现文件作为参考
1102
+ * - 纯 bug 修复 / 文档 / CI 等 → 只计数, 不展开
1103
+ *
1104
+ * @param {string} dir1 目录1源码
1105
+ * @param {string} dir2 目录2源码
1106
+ * @param {string[]} extraExcludes 用户额外排除模式
1107
+ * @param {object} meta 元信息
1108
+ * @param {string} branch1Dir 分支1 导出目录 (用于 rn-scan-exports.cjs)
1109
+ * @param {string} branch2Dir 分支2 导出目录 (用于 rn-scan-exports.cjs)
1110
+ * @returns {{content: string, stats: Object, apiDiff: Object, scan1: Object, scan2: Object}}
1111
+ */
1112
+ function generateStructureSummaryTgz(dir1, dir2, extraExcludes, meta, branch1Dir, branch2Dir) {
1113
+ // ── Step 1: 扫描接口差异 ──
1114
+ let scan1 = null;
1115
+ let scan2 = null;
1116
+ let apiDiff = null;
1117
+ try {
1118
+ scan1 = scanDirectoryForExports(branch1Dir);
1119
+ scan2 = scanDirectoryForExports(branch2Dir);
1120
+ if (scan1 && scan2) {
1121
+ apiDiff = scanExports.compareScans(scan1, scan2);
1122
+ } else {
1123
+ warn('接口扫描: 未找到包入口 (monorepo 子包可能未包含 codegenConfig)');
1124
+ }
1125
+ } catch (err) {
1126
+ warn(`接口扫描失败: ${err.message}`);
1127
+ }
1128
+
1129
+ // ── Step 2: 分析结构变更 (文件级, 用文件系统遍历) ──
1130
+ const { entries, stats } = analyzeStructuralChangesWithFs(dir1, dir2, extraExcludes);
1131
+
1132
+ const lines = [];
1133
+
1134
+ // ── 头部 ──
1135
+ lines.push(`# 适配分析报告: ${meta.branch1} → ${meta.branch2}`);
1136
+ lines.push('');
1137
+ lines.push(`| 项目 | 值 |`);
1138
+ lines.push(`|------|------|`);
1139
+ lines.push(`| 仓库 | ${meta.repoUrl} |`);
1140
+ lines.push(`| 版本 1 | ${meta.branch1} (${meta.commit1Hash}) |`);
1141
+ lines.push(`| 版本 2 | ${meta.branch2} (${meta.commit2Hash}) |`);
1142
+ lines.push(`| 文件变更总数 | ${stats.total} |`);
1143
+ lines.push(`| 对比方式 | tgz (npm 发行版, 无 rename 检测) |`);
1144
+ lines.push('');
1145
+
1146
+ // ── 判定结果 ──
1147
+ lines.push('## 判定结果');
1148
+ lines.push('');
1149
+ if (apiDiff) {
1150
+ if (apiDiff.summary.needsNativeAdaptation) {
1151
+ lines.push('> ⚠️ **需要原生适配** — JS 层接口或 TurboModule Spec 发生变更。');
1152
+ lines.push('> 下方按接口变更项展开, 每项附其他平台实现作为参考。');
1153
+ } else {
1154
+ lines.push('> ✅ **无需原生适配** — JS 层接口未变更, 仅 bug 修复/内部重构。');
1155
+ lines.push('> 鸿蒙侧只需合并 JS 代码 + 重新 codegen。');
1156
+ }
1157
+ lines.push('');
1158
+ lines.push('| 检查项 | 结果 |');
1159
+ lines.push('|--------|------|');
1160
+ lines.push(`| JS 导出变更 | ${apiDiff.summary.hasExportChanges ? '⚠️ 有' : '✅ 无'} |`);
1161
+ lines.push(`| 接口定义变更 | ${apiDiff.summary.hasInterfaceChanges ? '⚠️ 有' : '✅ 无'} |`);
1162
+ lines.push(`| TurboModule Spec 变更 | ${apiDiff.summary.hasTurboModuleChanges ? '⚠️ 有' : '✅ 无'} |`);
1163
+ lines.push(`| **需要原生适配** | ${apiDiff.summary.needsNativeAdaptation ? '⚠️ **是**' : '✅ **否**'} |`);
1164
+ } else {
1165
+ lines.push('> ⚠️ 接口扫描失败, 无法判定是否需要原生适配, 请手动检查。');
1166
+ }
1167
+ lines.push('');
1168
+ lines.push('---');
1169
+ lines.push('');
1170
+
1171
+ // ── 文件变更计数摘要 ──
1172
+ lines.push('## 文件变更统计');
1173
+ lines.push('');
1174
+ lines.push('| 类别 | 数量 | 说明 |');
1175
+ lines.push('|------|------|------|');
1176
+ for (const cat of RELEVANCE_ORDER) {
1177
+ const count = stats.relevanceCounts[cat] || 0;
1178
+ if (count === 0 && cat === 'not-relevant') continue;
1179
+ const m = RELEVANCE_META[cat];
1180
+ lines.push(`| ${m.icon} ${m.title} | ${count} | ${m.desc} |`);
1181
+ }
1182
+ lines.push('');
1183
+
1184
+ // ── 辅助函数 ──
1185
+ const getBase = (p) => {
1186
+ const sep = p.lastIndexOf('/');
1187
+ return sep >= 0 ? p.substring(sep + 1) : p;
1188
+ };
1189
+ const statusIcon = (s) => ({ R: '🔄', M: '✏️', A: '➕', D: '➖', C: '📋', T: '🔀' }[s] || '❓');
1190
+
1191
+ // ── 核心内容: 接口变更驱动的适配分析 ──
1192
+ if (apiDiff && apiDiff.summary.needsNativeAdaptation) {
1193
+ lines.push('## 适配分析 (按接口变更项)');
1194
+ lines.push('');
1195
+ lines.push('> 以下每个检查项对应一个接口变更, 需在鸿蒙侧对等实现。');
1196
+ lines.push('> 接口不变的文件变更 (bug 修复/内部重构) 不在此展开, 见下方计数。');
1197
+ lines.push('');
1198
+
1199
+ let itemNum = 0;
1200
+
1201
+ // ── TurboModule Spec 变更 ──
1202
+ if (apiDiff.turboModuleDiff.length > 0) {
1203
+ lines.push('### TurboModule Spec 变更');
1204
+ lines.push('');
1205
+ lines.push('> 原生模块接口变更, 需同步 codegen + ETS TurboModule + C++ Package。');
1206
+ lines.push('');
1207
+
1208
+ for (const d of apiDiff.turboModuleDiff) {
1209
+ if (d.change === 'added') {
1210
+ itemNum++;
1211
+ lines.push(`#### [ ] #${itemNum} 新增 TurboModule: \`${d.moduleName}\``);
1212
+ lines.push('');
1213
+ lines.push('**新增方法:**');
1214
+ lines.push('');
1215
+ lines.push('| 方法 | 签名 |');
1216
+ lines.push('|------|------|');
1217
+ for (const m of d.methods) {
1218
+ lines.push(`| \`${m.name}\` | \`${m.signature}\` |`);
1219
+ }
1220
+ lines.push('');
1221
+ } else if (d.change === 'removed') {
1222
+ itemNum++;
1223
+ lines.push(`#### [ ] #${itemNum} 移除 TurboModule: \`${d.moduleName}\``);
1224
+ lines.push('');
1225
+ lines.push('**移除方法:**');
1226
+ lines.push('');
1227
+ lines.push('| 方法 | 签名 |');
1228
+ lines.push('|------|------|');
1229
+ for (const m of d.methods) {
1230
+ lines.push(`| \`${m.name}\` | \`${m.signature}\` |`);
1231
+ }
1232
+ lines.push('');
1233
+ } else if (d.change === 'modified') {
1234
+ if (d.note) {
1235
+ itemNum++;
1236
+ lines.push(`#### [ ] #${itemNum} TurboModule 结构变更: \`${d.moduleName}\``);
1237
+ lines.push(`> ⚠️ ${d.note}`);
1238
+ if (d.specFiles1) lines.push(`> 旧 Spec: ${d.specFiles1.join(', ')}`);
1239
+ if (d.specFiles2) lines.push(`> 新 Spec: ${d.specFiles2.join(', ')}`);
1240
+ lines.push('');
1241
+ } else {
1242
+ const added = d.methods.filter(m => m.change === 'added');
1243
+ const removed = d.methods.filter(m => m.change === 'removed');
1244
+ const modified = d.methods.filter(m => m.change === 'modified');
1245
+
1246
+ if (added.length > 0) {
1247
+ itemNum++;
1248
+ lines.push(`#### [ ] #${itemNum} TurboModule \`${d.moduleName}\` 新增方法`);
1249
+ lines.push('');
1250
+ lines.push('| 方法 | 签名 |');
1251
+ lines.push('|------|------|');
1252
+ for (const m of added) {
1253
+ lines.push(`| \`${m.name}\` | \`${m.signature}\` |`);
1254
+ }
1255
+ lines.push('');
1256
+ }
1257
+ if (removed.length > 0) {
1258
+ itemNum++;
1259
+ lines.push(`#### [ ] #${itemNum} TurboModule \`${d.moduleName}\` 移除方法`);
1260
+ lines.push('');
1261
+ lines.push('| 方法 | 签名 |');
1262
+ lines.push('|------|------|');
1263
+ for (const m of removed) {
1264
+ lines.push(`| \`${m.name}\` | \`${m.signature}\` |`);
1265
+ }
1266
+ lines.push('');
1267
+ }
1268
+ if (modified.length > 0) {
1269
+ itemNum++;
1270
+ lines.push(`#### [ ] #${itemNum} TurboModule \`${d.moduleName}\` 修改方法签名`);
1271
+ lines.push('');
1272
+ lines.push('| 方法 | 旧签名 | 新签名 |');
1273
+ lines.push('|------|--------|--------|');
1274
+ for (const m of modified) {
1275
+ lines.push(`| \`${m.name}\` | \`${m.oldSignature}\` | \`${m.newSignature}\` |`);
1276
+ }
1277
+ lines.push('');
1278
+ }
1279
+ }
1280
+ }
1281
+ }
1282
+ }
1283
+
1284
+ // ── 公共接口/类型定义变更 ──
1285
+ if (apiDiff.interfaceDefsDiff.length > 0) {
1286
+ lines.push('### 公共接口/类型定义变更');
1287
+ lines.push('');
1288
+ lines.push('> JS 层公共 API 接口变更, 需同步鸿蒙化仓库 src/。');
1289
+ lines.push('');
1290
+
1291
+ for (const d of apiDiff.interfaceDefsDiff) {
1292
+ if (d.change === 'added') {
1293
+ itemNum++;
1294
+ lines.push(`#### [ ] #${itemNum} 新增接口: \`${d.name}\``);
1295
+ lines.push('');
1296
+ lines.push('| 方法 |');
1297
+ lines.push('|------|');
1298
+ for (const m of d.methods) lines.push(`| \`${m}\` |`);
1299
+ lines.push('');
1300
+ } else if (d.change === 'removed') {
1301
+ itemNum++;
1302
+ lines.push(`#### [ ] #${itemNum} 移除接口: \`${d.name}\``);
1303
+ lines.push('');
1304
+ lines.push('| 方法 |');
1305
+ lines.push('|------|');
1306
+ for (const m of d.methods) lines.push(`| \`${m}\` |`);
1307
+ lines.push('');
1308
+ } else if (d.change === 'modified') {
1309
+ if (d.added.length > 0) {
1310
+ itemNum++;
1311
+ lines.push(`#### [ ] #${itemNum} 接口 \`${d.name}\` 新增方法`);
1312
+ lines.push('');
1313
+ lines.push('| 方法 | 签名 |');
1314
+ lines.push('|------|------|');
1315
+ for (const m of d.added) lines.push(`| \`${m.name}\` | \`${m.signature}\` |`);
1316
+ lines.push('');
1317
+ }
1318
+ if (d.removed.length > 0) {
1319
+ itemNum++;
1320
+ lines.push(`#### [ ] #${itemNum} 接口 \`${d.name}\` 移除方法`);
1321
+ lines.push('');
1322
+ lines.push('| 方法 | 签名 |');
1323
+ lines.push('|------|------|');
1324
+ for (const m of d.removed) lines.push(`| \`${m.name}\` | \`${m.signature}\` |`);
1325
+ lines.push('');
1326
+ }
1327
+ if (d.modified.length > 0) {
1328
+ itemNum++;
1329
+ lines.push(`#### [ ] #${itemNum} 接口 \`${d.name}\` 修改方法签名`);
1330
+ lines.push('');
1331
+ lines.push('| 方法 | 旧签名 | 新签名 |');
1332
+ lines.push('|------|--------|--------|');
1333
+ for (const m of d.modified) {
1334
+ lines.push(`| \`${m.name}\` | \`${m.oldSignature}\` | \`${m.newSignature}\` |`);
1335
+ }
1336
+ lines.push('');
1337
+ }
1338
+ }
1339
+ }
1340
+ }
1341
+
1342
+ // ── JS 导出变更 ──
1343
+ const e = apiDiff.exportsDiff;
1344
+ if (e.defaultChanged || e.added.length > 0 || e.removed.length > 0 || e.typesAdded.length > 0 || e.typesRemoved.length > 0) {
1345
+ lines.push('### JS 导出变更');
1346
+ lines.push('');
1347
+ if (e.defaultChanged) {
1348
+ itemNum++;
1349
+ lines.push(`- [ ] **#${itemNum} default export 变更:** \`${e.defaultFrom || '(无)'}\` → \`${e.defaultTo || '(无)'}\``);
1350
+ }
1351
+ if (e.added.length > 0) {
1352
+ itemNum++;
1353
+ lines.push(`- [ ] **#${itemNum} 新增导出:** ${e.added.map(n => `\`${n}\``).join(', ')}`);
1354
+ }
1355
+ if (e.removed.length > 0) {
1356
+ itemNum++;
1357
+ lines.push(`- [ ] **#${itemNum} 移除导出:** ${e.removed.map(n => `\`${n}\``).join(', ')}`);
1358
+ }
1359
+ if (e.typesAdded.length > 0) {
1360
+ itemNum++;
1361
+ lines.push(`- [ ] **#${itemNum} 新增类型导出:** ${e.typesAdded.map(n => `\`${n}\``).join(', ')}`);
1362
+ }
1363
+ if (e.typesRemoved.length > 0) {
1364
+ itemNum++;
1365
+ lines.push(`- [ ] **#${itemNum} 移除类型导出:** ${e.typesRemoved.map(n => `\`${n}\``).join(', ')}`);
1366
+ }
1367
+ lines.push('');
1368
+ }
1369
+
1370
+ if (itemNum === 0) {
1371
+ lines.push('*接口变更项为空。*');
1372
+ lines.push('');
1373
+ }
1374
+ } else if (apiDiff && !apiDiff.summary.needsNativeAdaptation) {
1375
+ // ── 接口未变更: 只需合并 JS ──
1376
+ lines.push('## JS 代码合并清单');
1377
+ lines.push('');
1378
+ lines.push('> 接口未变更, 以下 JS/TS 源码变更只需合并到鸿蒙化仓库 src/。');
1379
+ lines.push('> 包配置变更需同步到 package.json。');
1380
+ lines.push('');
1381
+
1382
+ const jsEntries = entries.filter(e => e.relevance === 'js-ts-source' || e.relevance === 'turbo-module-spec');
1383
+ if (jsEntries.length > 0) {
1384
+ const groups = {};
1385
+ for (const e of jsEntries) {
1386
+ const checkPath = e.status === 'D' ? e.oldPath : e.newPath;
1387
+ const sep = checkPath.lastIndexOf('/');
1388
+ const dir = sep >= 0 ? checkPath.substring(0, sep) : '(根目录)';
1389
+ if (!groups[dir]) groups[dir] = [];
1390
+ groups[dir].push(e);
1391
+ }
1392
+ for (const dir of Object.keys(groups).sort()) {
1393
+ lines.push(`**${dir}/**`);
1394
+ lines.push('');
1395
+ lines.push('| 状态 | 文件 | 新增 | 删除 |');
1396
+ lines.push('|------|------|------|------|');
1397
+ for (const e of groups[dir].sort((a, b) => (b.added + b.deleted) - (a.added + a.deleted))) {
1398
+ const name = e.status === 'D' ? getBase(e.oldPath) : getBase(e.newPath);
1399
+ lines.push(`| ${statusIcon(e.status)} ${e.status} | ${name} | +${e.added} | -${e.deleted} |`);
1400
+ }
1401
+ lines.push('');
1402
+ }
1403
+ } else {
1404
+ lines.push('*无 JS/TS 源码变更。*');
1405
+ lines.push('');
1406
+ }
1407
+ }
1408
+
1409
+ // ── 无需展开的变更 (只计数) ──
1410
+ lines.push('## 其他变更 (仅计数)');
1411
+ lines.push('');
1412
+ lines.push('> 以下变更与接口适配无关, 不展开详情。');
1413
+ lines.push('');
1414
+
1415
+ const configEntries = entries.filter(e => e.relevance === 'package-config');
1416
+ if (configEntries.length > 0) {
1417
+ lines.push(`**包配置 (${configEntries.length}):**`);
1418
+ lines.push('');
1419
+ lines.push('| 状态 | 文件 | 新增 | 删除 |');
1420
+ lines.push('|------|------|------|------|');
1421
+ for (const e of configEntries.sort((a, b) => (b.added + b.deleted) - (a.added + a.deleted))) {
1422
+ const name = e.status === 'R' ? `${getBase(e.oldPath)} → ${getBase(e.newPath)}` : (e.status === 'D' ? getBase(e.oldPath) : getBase(e.newPath));
1423
+ lines.push(`| ${statusIcon(e.status)} ${e.status} | ${name} | +${e.added} | -${e.deleted} |`);
1424
+ }
1425
+ lines.push('');
1426
+ }
1427
+
1428
+ const buildEntries = entries.filter(e => e.relevance === 'build-spec');
1429
+ if (buildEntries.length > 0) {
1430
+ lines.push(`**构建规格 (${buildEntries.length}):**`);
1431
+ lines.push('');
1432
+ lines.push('| 状态 | 文件 | 新增 | 删除 |');
1433
+ lines.push('|------|------|------|------|');
1434
+ for (const e of buildEntries.sort((a, b) => (b.added + b.deleted) - (a.added + a.deleted))) {
1435
+ const name = e.status === 'D' ? getBase(e.oldPath) : getBase(e.newPath);
1436
+ lines.push(`| ${statusIcon(e.status)} ${e.status} | ${name} | +${e.added} | -${e.deleted} |`);
1437
+ }
1438
+ lines.push('');
1439
+ }
1440
+
1441
+ if (stats.irrelevant > 0) {
1442
+ lines.push(`**不相关变更 (${stats.irrelevant}):** 文档/图片/CI/lock/原生平台代码等, 已过滤`);
1443
+ lines.push('');
1444
+ }
1445
+
1446
+ if (entries.length === 0) {
1447
+ lines.push('*两个版本之间没有结构差异。*');
1448
+ }
1449
+
1450
+ return { content: lines.join('\n'), stats, apiDiff, scan1, scan2 };
1451
+ }
1452
+
1453
+ // ── 主流程 ───────────────────────────────────────────────
1454
+
1455
+ /**
1456
+ * 主流程: 通过 npm 包发行版对比两个版本, 无需 git。
1457
+ *
1458
+ * 工作流:
1459
+ * 1. 创建临时工作目录
1460
+ * 2. 下载/定位两个 tgz 文件 (本地路径或 npm pack)
1461
+ * 3. 解压到输出目录
1462
+ * 4. 用文件系统 diff 生成 patch + summary + structure + api-diff
1463
+ * 5. 清理临时文件
1464
+ */
1465
+ function main() {
1466
+ parseArgs();
1467
+
1468
+ // 参数校验
1469
+ if (!NPM_PACKAGE) error('必须指定 --npm-package <name>');
1470
+ if (!TGZ1) error('必须指定 --tgz1 <版本号或.tgz路径>');
1471
+ if (!TGZ2) error('必须指定 --tgz2 <版本号或.tgz路径>');
1472
+ if (TGZ1 === TGZ2) error(`两个版本不能相同: ${TGZ1}`);
1473
+
1474
+ // 检查依赖 (需要 tar, diff, npm)
1475
+ try {
1476
+ execFileSync('tar', ['--version'], { stdio: 'pipe' });
1477
+ } catch {
1478
+ error('未找到 tar, 请先安装');
1479
+ }
1480
+ // diff: 必须通过行为验证 — PATH 中可能存在不兼容实现
1481
+ // (如 DevEco Studio toolchains 的 diff 对不同文件静默返回无差异, 会吞掉全部变更)
1482
+ resolveDiffBinary();
1483
+ try {
1484
+ execFileSync('npm', ['--version'], { stdio: 'pipe' });
1485
+ } catch {
1486
+ error('未找到 npm, 请先安装');
1487
+ }
1488
+
1489
+ // 默认输出目录
1490
+ if (!OUTPUT_DIR) {
1491
+ OUTPUT_DIR = path.join(process.cwd(), 'diff');
1492
+ }
1493
+ fs.mkdirSync(OUTPUT_DIR, { recursive: true });
1494
+
1495
+ // 生成文件名/目录名
1496
+ const fileBranch1 = isVersionString(TGZ1) ? `v${TGZ1}` : cleanBranchName(path.basename(TGZ1, '.tgz'));
1497
+ const fileBranch2 = isVersionString(TGZ2) ? `v${TGZ2}` : cleanBranchName(path.basename(TGZ2, '.tgz'));
1498
+ const outputFile = path.join(OUTPUT_DIR, `diff_${fileBranch1}_${fileBranch2}.patch`);
1499
+ const branch1Dir = path.join(OUTPUT_DIR, fileBranch1);
1500
+ const branch2Dir = path.join(OUTPUT_DIR, fileBranch2);
1501
+
1502
+ console.log('');
1503
+ console.log('==========================================');
1504
+ console.log(' RNOH 三方库版本对比 (tgz 模式)');
1505
+ console.log('==========================================');
1506
+ console.log('');
1507
+
1508
+ const effectiveExcludes = buildExcludeList();
1509
+
1510
+ info(`npm 包名: ${NPM_PACKAGE}`);
1511
+ info(`版本 1: ${TGZ1}`);
1512
+ info(`版本 2: ${TGZ2}`);
1513
+ info(`输出目录: ${OUTPUT_DIR}`);
1514
+ info(`输出文件: ${outputFile}`);
1515
+ info(`对比方式: tgz (npm 发行版, 无 rename 检测)`);
1516
+ if (FULL_DIFF) {
1517
+ info(`模式: 完整 diff (--full)`);
1518
+ } else {
1519
+ info(`模式: 聚焦核心代码 (默认排除噪音)`);
1520
+ }
1521
+ if (effectiveExcludes.length > 0) {
1522
+ info(`排除模式: ${effectiveExcludes.join(' ')}`);
1523
+ }
1524
+ console.log('');
1525
+
1526
+ // ── 1. 创建临时工作目录 ────────────────────────────────
1527
+ const workDir = fs.mkdtempSync(path.join(os.tmpdir(), 'generate-diff-tgz-'));
1528
+ debug(`临时目录: ${workDir}`);
1529
+
1530
+ // ── 2. 下载/定位 tgz 文件 ──────────────────────────────
1531
+ info('获取 tgz 文件...');
1532
+ const tgzFile1 = resolveTgz(TGZ1, NPM_PACKAGE, workDir);
1533
+ const tgzFile2 = resolveTgz(TGZ2, NPM_PACKAGE, workDir);
1534
+ console.log('');
1535
+
1536
+ // ── 3. 解压到输出目录 ──────────────────────────────────
1537
+ info('解压 tgz 文件...');
1538
+
1539
+ // 清理旧目录
1540
+ for (const dir of [branch1Dir, branch2Dir]) {
1541
+ if (fs.existsSync(dir)) {
1542
+ fs.rmSync(dir, { recursive: true, force: true });
1543
+ }
1544
+ }
1545
+
1546
+ try {
1547
+ extractTgz(tgzFile1, branch1Dir);
1548
+ ok(` ✓ ${TGZ1} 已解压到: ${branch1Dir}`);
1549
+ } catch (err) {
1550
+ error(`解压 ${TGZ1} 失败: ${err.message}`);
1551
+ }
1552
+
1553
+ try {
1554
+ extractTgz(tgzFile2, branch2Dir);
1555
+ ok(` ✓ ${TGZ2} 已解压到: ${branch2Dir}`);
1556
+ } catch (err) {
1557
+ error(`解压 ${TGZ2} 失败: ${err.message}`);
1558
+ }
1559
+ console.log('');
1560
+
1561
+ // 版本标签用于报告
1562
+ const version1Label = isVersionString(TGZ1) ? TGZ1 : fileBranch1;
1563
+ const version2Label = isVersionString(TGZ2) ? TGZ2 : fileBranch2;
1564
+ const commit1Hash = 'npm-tgz';
1565
+ const commit2Hash = 'npm-tgz';
1566
+
1567
+ const meta = {
1568
+ repoUrl: `npm:${NPM_PACKAGE}`,
1569
+ branch1: version1Label,
1570
+ branch2: version2Label,
1571
+ commit1Hash,
1572
+ commit2Hash,
1573
+ };
1574
+
1575
+ // ── 4. --files 模式: 精简 patch ────────────────────────
1576
+ if (FILES_LIST) {
1577
+ info(`--files 模式: 从 ${FILES_LIST} 读取文件列表`);
1578
+
1579
+ let fileEntries;
1580
+ try {
1581
+ const content = fs.readFileSync(FILES_LIST, 'utf-8');
1582
+ fileEntries = content.split('\n').map(s => s.trim()).filter(Boolean);
1583
+ } catch (err) {
1584
+ error(`无法读取文件列表: ${FILES_LIST} (${err.message})`);
1585
+ }
1586
+ if (fileEntries.length === 0) {
1587
+ error('文件列表为空');
1588
+ }
1589
+ info(` 共 ${fileEntries.length} 个文件`);
1590
+
1591
+ // 逐文件生成 diff
1592
+ const lines = [];
1593
+ for (const file of fileEntries) {
1594
+ const path1 = path.join(branch1Dir, file);
1595
+ const path2 = path.join(branch2Dir, file);
1596
+ const exists1 = fs.existsSync(path1);
1597
+ const exists2 = fs.existsSync(path2);
1598
+
1599
+ if (exists1 && exists2) {
1600
+ try {
1601
+ const output = runDiff(['-u', '-L', `a/${file}`, '-L', `b/${file}`, path1, path2]);
1602
+ if (output) {
1603
+ lines.push(`diff --git a/${file} b/${file}`);
1604
+ lines.push(output);
1605
+ }
1606
+ } catch (err) {
1607
+ warn(`diff 失败 (${file}): ${err.message}`);
1608
+ }
1609
+ } else if (!exists1 && exists2) {
1610
+ lines.push(`diff --git a/${file} b/${file}`);
1611
+ lines.push(`new file mode 100644`);
1612
+ lines.push(`--- /dev/null`);
1613
+ lines.push(`+++ b/${file}`);
1614
+ try {
1615
+ const content = fs.readFileSync(path2, 'utf-8');
1616
+ for (const line of content.split('\n')) lines.push(`+${line}`);
1617
+ } catch { /* binary */ }
1618
+ lines.push('');
1619
+ } else if (exists1 && !exists2) {
1620
+ lines.push(`diff --git a/${file} b/${file}`);
1621
+ lines.push(`deleted file mode 100644`);
1622
+ lines.push(`--- a/${file}`);
1623
+ lines.push(`+++ /dev/null`);
1624
+ try {
1625
+ const content = fs.readFileSync(path1, 'utf-8');
1626
+ for (const line of content.split('\n')) lines.push(`-${line}`);
1627
+ } catch { /* binary */ }
1628
+ lines.push('');
1629
+ }
1630
+ }
1631
+
1632
+ const focusedHeader = [
1633
+ `From npm:${NPM_PACKAGE}`,
1634
+ `Date: ${new Date().toISOString()}`,
1635
+ `Subject: [PATCH] Focused diff between ${version1Label} and ${version2Label} (tgz)`,
1636
+ '',
1637
+ `npm Package: ${NPM_PACKAGE}`,
1638
+ `Version 1: ${version1Label}`,
1639
+ `Version 2: ${version2Label}`,
1640
+ `Files: ${fileEntries.length} (from ${FILES_LIST})`,
1641
+ `Mode: tgz (npm release)`,
1642
+ '',
1643
+ ].join('\n');
1644
+
1645
+ fs.writeFileSync(outputFile, focusedHeader + lines.join('\n'), 'utf-8');
1646
+
1647
+ const focusedSize = fs.statSync(outputFile).size;
1648
+ ok(`精简 diff 已生成: ${outputFile} (${formatSize(focusedSize)}, ${fileEntries.length} 文件)`);
1649
+
1650
+ console.log('');
1651
+ console.log('==========================================');
1652
+ console.log(' 精简 diff 生成完成 (tgz)');
1653
+ console.log('==========================================');
1654
+ console.log(` 包名: ${NPM_PACKAGE}`);
1655
+ console.log(` 版本 1: ${version1Label}`);
1656
+ console.log(` 版本 2: ${version2Label}`);
1657
+ console.log(` 文件数: ${fileEntries.length}`);
1658
+ console.log(` patch: ${formatSize(focusedSize)}`);
1659
+ console.log(` 输出: ${outputFile}`);
1660
+ console.log('==========================================');
1661
+ console.log('');
1662
+
1663
+ // 清理
1664
+ fs.rmSync(workDir, { recursive: true, force: true });
1665
+ debug(`已清理临时目录: ${workDir}`);
1666
+ return;
1667
+ }
1668
+
1669
+ // ── 5. 生成完整 diff patch ─────────────────────────────
1670
+ info('生成 diff (文件系统对比)...');
1671
+
1672
+ const diffContent = diffDirsWithFs(branch1Dir, branch2Dir, effectiveExcludes);
1673
+
1674
+ // 统计变更文件数
1675
+ const numstatMap = fetchNumstatWithFs(branch1Dir, branch2Dir, effectiveExcludes);
1676
+ const changedFiles = numstatMap.size;
1677
+
1678
+ info(` 变更文件数: ${changedFiles}`);
1679
+ if (changedFiles === 0) {
1680
+ warn('两个版本之间没有差异');
1681
+ }
1682
+
1683
+ const header = [
1684
+ `From npm:${NPM_PACKAGE}`,
1685
+ `Date: ${new Date().toISOString()}`,
1686
+ `Subject: [PATCH] Diff between ${version1Label} and ${version2Label} (tgz)`,
1687
+ '',
1688
+ `npm Package: ${NPM_PACKAGE}`,
1689
+ `Version 1: ${version1Label}`,
1690
+ `Version 2: ${version2Label}`,
1691
+ `Changed files: ${changedFiles}`,
1692
+ `Mode: tgz (npm release, no rename detection)`,
1693
+ FULL_DIFF ? 'Mode: full (no default excludes)' : 'Mode: filtered (default excludes applied)',
1694
+ effectiveExcludes.length > 0 ? `Excludes: ${effectiveExcludes.join(', ')}` : 'Excludes: none',
1695
+ '',
1696
+ ].join('\n');
1697
+
1698
+ fs.writeFileSync(outputFile, header + diffContent, 'utf-8');
1699
+ ok(`diff 文件已生成: ${outputFile}`);
1700
+
1701
+ // ── 5b. 生成变更摘要 summary.md ───────────────────────
1702
+ const summaryFile = path.join(OUTPUT_DIR, `summary_${fileBranch1}_${fileBranch2}.md`);
1703
+ info('生成变更摘要...');
1704
+ try {
1705
+ const summaryContent = generateSummaryWithFs(branch1Dir, branch2Dir, effectiveExcludes, meta);
1706
+ fs.writeFileSync(summaryFile, summaryContent, 'utf-8');
1707
+ ok(`变更摘要已生成: ${summaryFile}`);
1708
+ } catch (err) {
1709
+ warn(`生成变更摘要失败: ${err.message}`);
1710
+ }
1711
+
1712
+ // ── 5c. 生成适配分析报告 structure.md ─────────────────
1713
+ const structureFile = path.join(OUTPUT_DIR, `structure_${fileBranch1}_${fileBranch2}.md`);
1714
+ info('生成适配分析报告 (接口驱动)...');
1715
+ let structureStats = null;
1716
+ let apiDiffResult = null;
1717
+ let scanResult = null;
1718
+ try {
1719
+ scanResult = generateStructureSummaryTgz(branch1Dir, branch2Dir, EXCLUDES, meta, branch1Dir, branch2Dir);
1720
+ fs.writeFileSync(structureFile, scanResult.content, 'utf-8');
1721
+ structureStats = scanResult.stats;
1722
+ apiDiffResult = scanResult.apiDiff;
1723
+ ok(`适配分析报告已生成: ${structureFile}`);
1724
+
1725
+ if (apiDiffResult) {
1726
+ console.log('');
1727
+ if (apiDiffResult.summary.needsNativeAdaptation) {
1728
+ info('⚠️ 需要原生适配 — 接口或 TurboModule Spec 发生变更');
1729
+ } else {
1730
+ info('✅ 无需原生适配 — 接口未变更, 仅需合并 JS + codegen');
1731
+ }
1732
+ }
1733
+
1734
+ if (structureStats) {
1735
+ console.log('');
1736
+ info(`文件变更统计 (总数 ${structureStats.total}):`);
1737
+ for (const cat of RELEVANCE_ORDER) {
1738
+ const count = structureStats.relevanceCounts[cat] || 0;
1739
+ if (count > 0) {
1740
+ const m = RELEVANCE_META[cat];
1741
+ info(` ${m.icon} ${m.title}: ${count}`);
1742
+ }
1743
+ }
1744
+ }
1745
+ } catch (err) {
1746
+ warn(`生成适配分析报告失败: ${err.message}`);
1747
+ }
1748
+
1749
+ // ── 5d. 生成接口差异适配检查清单 api-diff.md ──────────
1750
+ const apiDiffFile = path.join(OUTPUT_DIR, `api-diff_${fileBranch1}_${fileBranch2}.md`);
1751
+ if (apiDiffResult && scanResult && scanResult.scan1 && scanResult.scan2) {
1752
+ info('生成接口差异适配检查清单...');
1753
+ try {
1754
+ const apiReport = scanExports.generateMarkdownReport(scanResult.scan1, scanResult.scan2, apiDiffResult, {
1755
+ version1: version1Label,
1756
+ version2: version2Label,
1757
+ repoUrl: `npm:${NPM_PACKAGE}`,
1758
+ });
1759
+ fs.writeFileSync(apiDiffFile, apiReport, 'utf-8');
1760
+ ok(`接口差异检查清单已生成: ${apiDiffFile}`);
1761
+ } catch (err) {
1762
+ warn(`生成接口差异检查清单失败: ${err.message}`);
1763
+ }
1764
+ }
1765
+ console.log('');
1766
+
1767
+ // ── 6. 显示摘要 ────────────────────────────────────────
1768
+ const fileSize = fs.statSync(outputFile).size;
1769
+
1770
+ console.log('==========================================');
1771
+ console.log(' 对比完成 (tgz 模式)');
1772
+ console.log('==========================================');
1773
+ console.log(` 包名: ${NPM_PACKAGE}`);
1774
+ console.log(` 版本 1: ${version1Label}`);
1775
+ console.log(` 版本 2: ${version2Label}`);
1776
+ console.log(` 变更文件: ${changedFiles}`);
1777
+ console.log(` 模式: ${FULL_DIFF ? '完整 diff (--full)' : '聚焦核心代码 (默认排除噪音)'}`);
1778
+ console.log('');
1779
+ console.log(` 输出目录: ${OUTPUT_DIR}/`);
1780
+ console.log(` ├── ${fileBranch1}/ (版本1 源代码)`);
1781
+ console.log(` ├── ${fileBranch2}/ (版本2 源代码)`);
1782
+ console.log(` ├── diff_${fileBranch1}_${fileBranch2}.patch (${formatSize(fileSize)})`);
1783
+ console.log(` ├── summary_${fileBranch1}_${fileBranch2}.md (变更摘要)`);
1784
+ console.log(` ├── structure_${fileBranch1}_${fileBranch2}.md (适配分析报告)`);
1785
+ if (apiDiffResult && scanResult && scanResult.scan1 && scanResult.scan2) {
1786
+ console.log(` └── api-diff_${fileBranch1}_${fileBranch2}.md (接口差异检查清单)`);
1787
+ } else {
1788
+ console.log(` └── (接口差异检查清单生成失败)`);
1789
+ }
1790
+ console.log('==========================================');
1791
+ console.log('');
1792
+
1793
+ // ── 7. 清理临时目录 ────────────────────────────────────
1794
+ fs.rmSync(workDir, { recursive: true, force: true });
1795
+ debug(`已清理临时目录: ${workDir}`);
1796
+ }
1797
+
1798
+ main();