@oj-module/cli 0.2.53 → 0.2.56

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 (330) hide show
  1. package/devkit/SKILL.md +4 -4
  2. package/devkit/manual.md +79 -65
  3. package/package.json +2 -2
  4. package/shell-dist/assets/{index-e_Mp2mb5.js → index-CImwskAz.js} +24 -11
  5. package/shell-dist/assets/runtime.js +7 -4
  6. package/shell-dist/index.html +3 -3
  7. package/shell-dist/versions.json +3 -3
  8. package/src/__tests__/layout.test.ts +30 -1
  9. package/src/__tests__/templates-schema-columns.test.ts +99 -0
  10. package/src/__tests__/templates-tenant-scope.test.ts +3 -2
  11. package/src/alias-paths.ts +11 -12
  12. package/src/builtin.ts +10 -3
  13. package/src/config.ts +9 -0
  14. package/src/contract/__tests__/client-path.test.ts +30 -0
  15. package/src/contract/__tests__/emit-client.test.ts +20 -2
  16. package/src/contract/__tests__/handler-scan.test.ts +23 -0
  17. package/src/contract/__tests__/market-client.integration.test.ts +2 -2
  18. package/src/contract/__tests__/run-api.test.ts +2 -2
  19. package/src/contract/check.ts +11 -7
  20. package/src/contract/client-path.ts +30 -0
  21. package/src/contract/emit-client.ts +27 -6
  22. package/src/contract/handler-scan.ts +40 -3
  23. package/src/contract/run.ts +15 -5
  24. package/src/init.ts +29 -3
  25. package/src/layout.ts +72 -16
  26. package/src/lifecycle/__tests__/manifest-contract.test.ts +2 -2
  27. package/src/market/__tests__/bootstrap.test.ts +1 -1
  28. package/src/site.ts +1 -1
  29. package/templates/README.md +49 -43
  30. package/templates/init.include.yaml +31 -19
  31. package/templates/openapi.json +1488 -120
  32. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/domain/users.ts +27 -0
  33. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/infra/oplog.ts +4 -2
  34. package/templates/src/{_platform/api → app/api/_platform}/contacts/api.ts +29 -1
  35. package/templates/src/{_platform/api → app/api/_platform}/contacts/detail/api.ts +25 -1
  36. package/templates/src/app/api/_platform/migrations/0017__users_employee_no.mysql.sql +7 -0
  37. package/templates/src/app/api/_platform/migrations/0017__users_employee_no.pg.sql +4 -0
  38. package/templates/src/app/api/_platform/migrations/0017__users_employee_no.sql +8 -0
  39. package/templates/src/{_platform/api → app/api/_platform}/schema.yaml +5 -0
  40. package/templates/src/{_platform/api → app/api/_platform}/solutions/api.ts +60 -1
  41. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/replay.ts +9 -3
  42. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/tenant-admin.ts +18 -27
  43. package/templates/src/{auth/api → app/api/auth}/api.ts +59 -0
  44. package/templates/src/{auth/api → app/api/auth}/ldap-login/api.ts +37 -1
  45. package/templates/src/{auth/api → app/api/auth}/wecom/api.ts +23 -0
  46. package/templates/src/{notification/api → app/api/notification}/api.ts +17 -0
  47. package/templates/src/{notification/api → app/api/notification}/del/api.ts +2 -0
  48. package/templates/src/{system/api → app/api/system}/oplog/api.ts +5 -3
  49. package/templates/src/{system/api → app/api/system}/roles/api.ts +3 -0
  50. package/templates/src/{system/api → app/api/system}/roles/bindings/api.ts +3 -0
  51. package/templates/src/{system/api → app/api/system}/settings/api.ts +3 -0
  52. package/templates/src/{system/api → app/api/system}/users/forbidden/api.ts +3 -0
  53. package/templates/src/{system/api → app/api/system}/users/password/api.ts +3 -0
  54. package/templates/src/{system/api → app/api/system}/users/roles/api.ts +3 -0
  55. package/templates/src/app/api/web/get-async-routes/api.ts +23 -0
  56. package/templates/src/app/api/web/hello/api.ts +22 -0
  57. package/templates/src/{web/api → app/api/web}/user-info/api.ts +21 -1
  58. package/templates/src/{_platform/web → app/web/_platform}/__client/api.schemas.ts +99 -35
  59. package/templates/src/{_platform/web/__client/api.ts → app/web/_platform/__client/index.ts} +108 -61
  60. package/templates/src/{auth/web → app/web/auth}/__client/api.schemas.ts +133 -13
  61. package/templates/src/{auth/web/__client/api.ts → app/web/auth/__client/index.ts} +134 -33
  62. package/templates/src/app/web/entry.ts +77 -0
  63. package/templates/src/{home/web/__client/api.ts → app/web/home/__client/index.ts} +5 -2
  64. package/templates/src/app/web/home/routes.ts +41 -0
  65. package/templates/src/app/web/login/routes.ts +71 -0
  66. package/templates/src/{notification/web → app/web/notification}/__client/api.schemas.ts +14 -0
  67. package/templates/src/{notification/web/__client/api.ts → app/web/notification/__client/index.ts} +67 -20
  68. package/templates/src/{notification/web → app/web/notification}/pages/center/index.tsx +1 -1
  69. package/templates/src/app/web/notification/routes.ts +70 -0
  70. package/templates/src/{personal-center/web/__client/api.ts → app/web/personal-center/__client/index.ts} +5 -2
  71. package/templates/src/{personal-center/web → app/web/personal-center}/pages/my-profile/index.tsx +1 -1
  72. package/templates/src/app/web/personal-center/routes.ts +49 -0
  73. package/templates/src/{system/web → app/web/system}/__client/api.schemas.ts +29 -22
  74. package/templates/src/{system/web/__client/api.ts → app/web/system/__client/index.ts} +103 -58
  75. package/templates/src/{system/web → app/web/system}/locales/en-US.json +2 -3
  76. package/templates/src/{system/web → app/web/system}/locales/zh-CN.json +2 -3
  77. package/templates/src/{system/web → app/web/system}/pages/departments/index.tsx +2 -2
  78. package/templates/src/{system/web → app/web/system}/pages/login-methods/index.tsx +1 -1
  79. package/templates/src/{system/web → app/web/system}/pages/oplog/index.tsx +44 -19
  80. package/templates/src/{system/web → app/web/system}/pages/roles/index.tsx +5 -3
  81. package/templates/src/app/web/system/pages/roles/visible-roles.ts +14 -0
  82. package/templates/src/{system/web → app/web/system}/pages/settings/index.tsx +2 -2
  83. package/templates/src/app/web/system/pages/users/can-manage.ts +21 -0
  84. package/templates/src/{system/web → app/web/system}/pages/users/index.tsx +23 -15
  85. package/templates/src/{system/web → app/web/system}/pages/users/roles.tsx +1 -1
  86. package/templates/src/{system/web → app/web/system}/pages/wecom/index.tsx +12 -3
  87. package/templates/src/{system/web/entry.ts → app/web/system/routes.ts} +25 -36
  88. package/templates/src/app/web/web/__client/api.schemas.ts +36 -0
  89. package/templates/src/{web/web/__client/api.ts → app/web/web/__client/index.ts} +38 -11
  90. package/templates/src/oj-market/web/api/defaultClient.ts +1 -1
  91. package/templates/tests/auth-ldap.spec.ts +2 -2
  92. package/templates/tests/auth-session.spec.ts +3 -3
  93. package/templates/tests/auth-throttle.spec.ts +1 -1
  94. package/templates/tests/fixture.ts +6 -4
  95. package/templates/tests/guard.spec.ts +1 -1
  96. package/templates/tests/lifecycle/atomic-state.spec.ts +3 -3
  97. package/templates/tests/lifecycle/bootstrap.spec.ts +3 -3
  98. package/templates/tests/lifecycle/deploy-smoke.spec.ts +8 -8
  99. package/templates/tests/lifecycle/installer.spec.ts +10 -10
  100. package/templates/tests/lifecycle/market-api.spec.ts +14 -14
  101. package/templates/tests/lifecycle/migrate.spec.ts +6 -6
  102. package/templates/tests/lifecycle/preflight.spec.ts +3 -3
  103. package/templates/tests/lifecycle/register.spec.ts +1 -1
  104. package/templates/tests/lifecycle/semver.spec.ts +1 -1
  105. package/templates/tests/lifecycle/sha384.spec.ts +1 -1
  106. package/templates/tests/lifecycle/solutions-api.spec.ts +1 -1
  107. package/templates/tests/lifecycle/source-seq.spec.ts +1 -1
  108. package/templates/tests/lifecycle/steps-fs.spec.ts +9 -9
  109. package/templates/tests/lifecycle/text-edit.spec.ts +1 -1
  110. package/templates/tests/lifecycle/tgz-guard.spec.ts +2 -2
  111. package/templates/tests/lifecycle/trust.spec.ts +5 -5
  112. package/templates/tests/lifecycle/uninstall.spec.ts +7 -7
  113. package/templates/tests/lifecycle/verify-checks.spec.ts +5 -5
  114. package/templates/tests/lifecycle/verify-report.spec.ts +4 -4
  115. package/templates/tests/org-departments.spec.ts +10 -6
  116. package/templates/tests/org-read.spec.ts +7 -7
  117. package/templates/tests/query-utils.spec.ts +1 -1
  118. package/templates/tests/rbac-audit.spec.ts +1 -1
  119. package/templates/tests/rbac-contacts.spec.ts +2 -2
  120. package/templates/tests/rbac-guard.spec.ts +1 -1
  121. package/templates/tests/rbac-roles.spec.ts +10 -2
  122. package/templates/tests/rbac-seed.spec.ts +1 -1
  123. package/templates/tests/rbac-settings.spec.ts +77 -7
  124. package/templates/tests/rbac-solution-perms.spec.ts +1 -1
  125. package/templates/tests/rbac-tables.spec.ts +29 -0
  126. package/templates/tests/rbac-users.spec.ts +55 -3
  127. package/templates/tests/roles-page.spec.ts +40 -0
  128. package/templates/tests/seed-role-accounts.spec.ts +1 -1
  129. package/templates/tests/tenant-access-cache.spec.ts +1 -1
  130. package/templates/tests/tenant-forgery.spec.ts +5 -5
  131. package/templates/tests/tenant-isolation.spec.ts +5 -4
  132. package/templates/tests/username-namespace.spec.ts +3 -3
  133. package/templates/tests/users-page.spec.ts +39 -0
  134. package/templates/tests/wecom-admin.spec.ts +6 -6
  135. package/templates/tests/wecom-client-timeout.spec.ts +1 -1
  136. package/templates/tests/wecom-connector.spec.ts +9 -9
  137. package/templates/tests/wecom-corp.spec.ts +9 -9
  138. package/templates/tests/wecom-crypt.spec.ts +1 -1
  139. package/templates/tests/wecom-endpoints.spec.ts +2 -2
  140. package/templates/tests/wecom-landing.spec.ts +1 -1
  141. package/templates/tests/wecom-org-sync.spec.ts +7 -7
  142. package/templates/tests/wecom-sync.spec.ts +3 -3
  143. package/templates/tsconfig.paths.json +11 -11
  144. package/templates/vitest.config.ts +2 -2
  145. package/templates/web.config.ts +24 -15
  146. package/vendor/host-versions.json +3 -3
  147. package/templates/src/home/web/entry.ts +0 -49
  148. package/templates/src/login/web/entry.ts +0 -79
  149. package/templates/src/notification/web/entry.ts +0 -84
  150. package/templates/src/personal-center/web/entry.ts +0 -60
  151. package/templates/src/web/api/get-async-routes/api.ts +0 -8
  152. package/templates/src/web/api/hello/api.ts +0 -10
  153. package/templates/src/web/web/__client/api.schemas.ts +0 -8
  154. package/templates/src/{_platform/api → app/api/_platform}/README.md +0 -0
  155. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/atomic.ts +0 -0
  156. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/history.ts +0 -0
  157. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/installer.ts +0 -0
  158. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/limits.ts +0 -0
  159. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/lock.ts +0 -0
  160. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/semver.ts +0 -0
  161. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/sha384.ts +0 -0
  162. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/source-seq.ts +0 -0
  163. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/state.ts +0 -0
  164. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/deploy-web.ts +0 -0
  165. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/download.ts +0 -0
  166. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/extract.ts +0 -0
  167. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/migrate.ts +0 -0
  168. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/preflight.ts +0 -0
  169. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/register.ts +0 -0
  170. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/reload.ts +0 -0
  171. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/smoke.ts +0 -0
  172. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/verify.ts +0 -0
  173. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/tasks.ts +0 -0
  174. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/text-edit.ts +0 -0
  175. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/tgz-guard.ts +0 -0
  176. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/trust.ts +0 -0
  177. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/types.ts +0 -0
  178. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/uninstall.ts +0 -0
  179. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/verify-checks.ts +0 -0
  180. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/verify-report.ts +0 -0
  181. package/templates/src/{_platform/api → app/api/_platform}/_shared/respond.ts +0 -0
  182. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/domain/contacts.ts +0 -0
  183. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/domain/org-read.ts +0 -0
  184. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/domain/org.ts +0 -0
  185. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/domain/roles.ts +0 -0
  186. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/domain/settings.ts +0 -0
  187. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/domain/solution-perms.ts +0 -0
  188. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/infra/access.ts +0 -0
  189. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/infra/audit.ts +0 -0
  190. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/infra/guard.ts +0 -0
  191. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/infra/query.ts +0 -0
  192. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/infra/rate.ts +0 -0
  193. package/templates/src/{_platform/api → app/api/_platform}/_shared/site-facade.ts +0 -0
  194. package/templates/src/{_platform/api → app/api/_platform}/fixtures/l1-test-user.sql +0 -0
  195. package/templates/src/{_platform/api → app/api/_platform}/manifest.yaml +0 -0
  196. package/templates/src/{_platform/api → app/api/_platform}/market/api.ts +0 -0
  197. package/templates/src/{_platform/api → app/api/_platform}/market/bootstrap.ts +0 -0
  198. package/templates/src/{_platform/api → app/api/_platform}/market/index-source.ts +0 -0
  199. package/templates/src/{_platform/api → app/api/_platform}/migrations/0001__create_users.mysql.sql +0 -0
  200. package/templates/src/{_platform/api → app/api/_platform}/migrations/0001__create_users.pg.sql +0 -0
  201. package/templates/src/{_platform/api → app/api/_platform}/migrations/0001__create_users.sql +0 -0
  202. package/templates/src/{_platform/api → app/api/_platform}/migrations/0002__add_users_avatar_base64.mysql.sql +0 -0
  203. package/templates/src/{_platform/api → app/api/_platform}/migrations/0002__add_users_avatar_base64.sql +0 -0
  204. package/templates/src/{_platform/api → app/api/_platform}/migrations/0003__users_source_display_name.mysql.sql +0 -0
  205. package/templates/src/{_platform/api → app/api/_platform}/migrations/0003__users_source_display_name.sql +0 -0
  206. package/templates/src/{_platform/api → app/api/_platform}/migrations/0004__sys_rbac.mysql.sql +0 -0
  207. package/templates/src/{_platform/api → app/api/_platform}/migrations/0004__sys_rbac.pg.sql +0 -0
  208. package/templates/src/{_platform/api → app/api/_platform}/migrations/0004__sys_rbac.sql +0 -0
  209. package/templates/src/{_platform/api → app/api/_platform}/migrations/0005__users_must_change.mysql.sql +0 -0
  210. package/templates/src/{_platform/api → app/api/_platform}/migrations/0005__users_must_change.sql +0 -0
  211. package/templates/src/{_platform/api → app/api/_platform}/migrations/0006__users_is_forbidden.mysql.sql +0 -0
  212. package/templates/src/{_platform/api → app/api/_platform}/migrations/0006__users_is_forbidden.sql +0 -0
  213. package/templates/src/{_platform/api → app/api/_platform}/migrations/0007__users_email.sql +0 -0
  214. package/templates/src/{_platform/api → app/api/_platform}/migrations/0008__sys_refresh_token.mysql.sql +0 -0
  215. package/templates/src/{_platform/api → app/api/_platform}/migrations/0008__sys_refresh_token.pg.sql +0 -0
  216. package/templates/src/{_platform/api → app/api/_platform}/migrations/0008__sys_refresh_token.sql +0 -0
  217. package/templates/src/{_platform/api → app/api/_platform}/migrations/0009__sys_throttle.mysql.sql +0 -0
  218. package/templates/src/{_platform/api → app/api/_platform}/migrations/0009__sys_throttle.pg.sql +0 -0
  219. package/templates/src/{_platform/api → app/api/_platform}/migrations/0009__sys_throttle.sql +0 -0
  220. package/templates/src/{_platform/api → app/api/_platform}/migrations/0010__sys_solution_route.mysql.sql +0 -0
  221. package/templates/src/{_platform/api → app/api/_platform}/migrations/0010__sys_solution_route.pg.sql +0 -0
  222. package/templates/src/{_platform/api → app/api/_platform}/migrations/0010__sys_solution_route.sql +0 -0
  223. package/templates/src/{_platform/api → app/api/_platform}/migrations/0011__sys_setting_group_label.mysql.sql +0 -0
  224. package/templates/src/{_platform/api → app/api/_platform}/migrations/0011__sys_setting_group_label.sql +0 -0
  225. package/templates/src/{_platform/api → app/api/_platform}/migrations/0012__market_history.mysql.sql +0 -0
  226. package/templates/src/{_platform/api → app/api/_platform}/migrations/0012__market_history.pg.sql +0 -0
  227. package/templates/src/{_platform/api → app/api/_platform}/migrations/0012__market_history.sql +0 -0
  228. package/templates/src/{_platform/api → app/api/_platform}/migrations/0013__users_profile.sql +0 -0
  229. package/templates/src/{_platform/api → app/api/_platform}/migrations/0014__reg_source_seq.mysql.sql +0 -0
  230. package/templates/src/{_platform/api → app/api/_platform}/migrations/0014__reg_source_seq.pg.sql +0 -0
  231. package/templates/src/{_platform/api → app/api/_platform}/migrations/0014__reg_source_seq.sql +0 -0
  232. package/templates/src/{_platform/api → app/api/_platform}/migrations/0015__tenant.mysql.sql +0 -0
  233. package/templates/src/{_platform/api → app/api/_platform}/migrations/0015__tenant.pg.sql +0 -0
  234. package/templates/src/{_platform/api → app/api/_platform}/migrations/0015__tenant.sql +0 -0
  235. package/templates/src/{_platform/api → app/api/_platform}/migrations/0016__org.mysql.sql +0 -0
  236. package/templates/src/{_platform/api → app/api/_platform}/migrations/0016__org.pg.sql +0 -0
  237. package/templates/src/{_platform/api → app/api/_platform}/migrations/0016__org.sql +0 -0
  238. package/templates/src/{_platform/api → app/api/_platform}/org/departments/api.ts +0 -0
  239. package/templates/src/{_platform/api → app/api/_platform}/org/is-member/api.ts +0 -0
  240. package/templates/src/{_platform/api → app/api/_platform}/org/members/api.ts +0 -0
  241. package/templates/src/{_platform/api → app/api/_platform}/org/picker/api.ts +0 -0
  242. package/templates/src/{_platform/api → app/api/_platform}/org/resolve/api.ts +0 -0
  243. package/templates/src/{_platform/api → app/api/_platform}/org/tree/api.ts +0 -0
  244. package/templates/src/{_platform/api → app/api/_platform}/org/user/api.ts +0 -0
  245. package/templates/src/{_platform/api → app/api/_platform}/seed.mysql.sql +0 -0
  246. package/templates/src/{_platform/api → app/api/_platform}/seed.pg.sql +0 -0
  247. package/templates/src/{_platform/api → app/api/_platform}/seed.sql +0 -0
  248. package/templates/src/{auth/api → app/api/auth}/_shared/ldap-login.ts +0 -0
  249. package/templates/src/{auth/api → app/api/auth}/_shared/ldap.ts +0 -0
  250. package/templates/src/{auth/api → app/api/auth}/_shared/login-throttle.ts +0 -0
  251. package/templates/src/{auth/api → app/api/auth}/_shared/session.ts +0 -0
  252. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/admin.ts +1 -1
  253. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/client.ts +0 -0
  254. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/config.ts +0 -0
  255. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/contacts.ts +0 -0
  256. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/crypt.ts +0 -0
  257. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/events.ts +1 -1
  258. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/login.ts +0 -0
  259. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/payload.ts +0 -0
  260. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/store.ts +0 -0
  261. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/sync.ts +0 -0
  262. package/templates/src/{auth/api → app/api/auth}/manifest.yaml +0 -0
  263. package/templates/src/{auth/api → app/api/auth}/migrations/0001__wecom.mysql.sql +0 -0
  264. package/templates/src/{auth/api → app/api/auth}/migrations/0001__wecom.pg.sql +0 -0
  265. package/templates/src/{auth/api → app/api/auth}/migrations/0001__wecom.sql +0 -0
  266. package/templates/src/{auth/api → app/api/auth}/migrations/0002__org_sync.mysql.sql +0 -0
  267. package/templates/src/{auth/api → app/api/auth}/migrations/0002__org_sync.pg.sql +0 -0
  268. package/templates/src/{auth/api → app/api/auth}/migrations/0002__org_sync.sql +0 -0
  269. package/templates/src/{auth/api → app/api/auth}/migrations/0003__credential_kind.mysql.sql +0 -0
  270. package/templates/src/{auth/api → app/api/auth}/migrations/0003__credential_kind.pg.sql +0 -0
  271. package/templates/src/{auth/api → app/api/auth}/migrations/0003__credential_kind.sql +0 -0
  272. package/templates/src/{auth/api → app/api/auth}/schema.yaml +0 -0
  273. package/templates/src/{auth/api → app/api/auth}/wecom/admin/api.ts +0 -0
  274. package/templates/src/{auth/api → app/api/auth}/wecom/admin/tenant-admin/api.ts +1 -1
  275. package/templates/src/{auth/api → app/api/auth}/wecom/admin/tenant-admin/password/api.ts +1 -1
  276. /package/templates/src/{auth/api → app/api/auth}/wecom/sync/api.ts +0 -0
  277. /package/templates/src/{home/api → app/api/home}/line/api.ts +0 -0
  278. /package/templates/src/{home/api → app/api/home}/manifest.yaml +0 -0
  279. /package/templates/src/{home/api → app/api/home}/migrations/0001__create_home_tables.sql +0 -0
  280. /package/templates/src/{home/api → app/api/home}/migrations/0002__tenant.mysql.sql +0 -0
  281. /package/templates/src/{home/api → app/api/home}/migrations/0002__tenant.sql +0 -0
  282. /package/templates/src/{home/api → app/api/home}/pie/api.ts +0 -0
  283. /package/templates/src/{home/api → app/api/home}/schema.yaml +0 -0
  284. /package/templates/src/{home/api → app/api/home}/seed.sql +0 -0
  285. /package/templates/src/{notification/api → app/api/notification}/_shared/notify.ts +0 -0
  286. /package/templates/src/{notification/api → app/api/notification}/manifest.yaml +0 -0
  287. /package/templates/src/{notification/api → app/api/notification}/migrations/0001__create_notifications.sql +0 -0
  288. /package/templates/src/{notification/api → app/api/notification}/migrations/0002__notifications_user.sql +0 -0
  289. /package/templates/src/{notification/api → app/api/notification}/migrations/0003__notifications_broadcast.sql +0 -0
  290. /package/templates/src/{notification/api → app/api/notification}/migrations/0004__notifications_broadcast_hidden.sql +0 -0
  291. /package/templates/src/{notification/api → app/api/notification}/migrations/0005__tenant.mysql.sql +0 -0
  292. /package/templates/src/{notification/api → app/api/notification}/migrations/0005__tenant.sql +0 -0
  293. /package/templates/src/{notification/api → app/api/notification}/schema.yaml +0 -0
  294. /package/templates/src/{notification/api → app/api/notification}/seed.sql +0 -0
  295. /package/templates/src/{personal-center/api → app/api/personal-center}/manifest.yaml +0 -0
  296. /package/templates/src/{personal-center/api → app/api/personal-center}/profile/api.ts +0 -0
  297. /package/templates/src/{personal-center/api → app/api/personal-center}/upload/api.ts +0 -0
  298. /package/templates/src/{system/api → app/api/system}/departments/api.ts +0 -0
  299. /package/templates/src/{system/api → app/api/system}/departments/detach/api.ts +0 -0
  300. /package/templates/src/{system/api → app/api/system}/departments/members/api.ts +0 -0
  301. /package/templates/src/{system/api → app/api/system}/departments/move/api.ts +0 -0
  302. /package/templates/src/{system/api → app/api/system}/login-methods/api.ts +0 -0
  303. /package/templates/src/{system/api → app/api/system}/manifest.yaml +0 -0
  304. /package/templates/src/{system/api → app/api/system}/permissions/api.ts +0 -0
  305. /package/templates/src/{system/api → app/api/system}/users/api.ts +0 -0
  306. /package/templates/src/{system/api → app/api/system}/users/profile/api.ts +0 -0
  307. /package/templates/src/{web/api → app/api/web}/manifest.yaml +0 -0
  308. /package/templates/src/{home/web → app/web/home}/__client/api.schemas.ts +0 -0
  309. /package/templates/src/{home/web → app/web/home}/locales/en-US.json +0 -0
  310. /package/templates/src/{home/web → app/web/home}/locales/zh-CN.json +0 -0
  311. /package/templates/src/{home/web → app/web/home}/pages/components/bar-chart.tsx +0 -0
  312. /package/templates/src/{home/web → app/web/home}/pages/components/card-list.tsx +0 -0
  313. /package/templates/src/{home/web → app/web/home}/pages/components/line-chart.tsx +0 -0
  314. /package/templates/src/{home/web → app/web/home}/pages/components/pie-chart.tsx +0 -0
  315. /package/templates/src/{home/web → app/web/home}/pages/index.tsx +0 -0
  316. /package/templates/src/{login/web → app/web/login}/auth-provider.ts +0 -0
  317. /package/templates/src/{login/web → app/web/login}/locales/en-US.json +0 -0
  318. /package/templates/src/{login/web → app/web/login}/locales/zh-CN.json +0 -0
  319. /package/templates/src/{login/web → app/web/login}/pages/change-password.tsx +0 -0
  320. /package/templates/src/{login/web → app/web/login}/pages/login.tsx +0 -0
  321. /package/templates/src/{login/web → app/web/login}/wecom.ts +0 -0
  322. /package/templates/src/{notification/web → app/web/notification}/locales/en-US.json +0 -0
  323. /package/templates/src/{notification/web → app/web/notification}/locales/zh-CN.json +0 -0
  324. /package/templates/src/{personal-center/web → app/web/personal-center}/__client/api.schemas.ts +0 -0
  325. /package/templates/src/{personal-center/web → app/web/personal-center}/locales/en-US.json +0 -0
  326. /package/templates/src/{personal-center/web → app/web/personal-center}/locales/zh-CN.json +0 -0
  327. /package/templates/src/{system/web → app/web/system}/pages/departments/sync-bar.tsx +0 -0
  328. /package/templates/src/{system/web → app/web/system}/pages/departments/tree.ts +0 -0
  329. /package/templates/src/{system/web → app/web/system}/pages/errors.ts +0 -0
  330. /package/templates/src/{system/web → app/web/system}/pages/forbidden.tsx +0 -0
@@ -0,0 +1,99 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
+ import { dirname, join, relative, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { describe, expect, it } from "vitest";
5
+ import { parse } from "yaml";
6
+
7
+ /**
8
+ * BDD:模板后端「迁移落库的列」必须在所属模块 schema.yaml 的列白名单里声明。
9
+ *
10
+ * 为什么要这条:oj 的 SQL 构造器是二进制内的 Rust 实现,select/insert/update 的列会**先**按
11
+ * schema.yaml 校验,列在库里但不在白名单时,SQL 还没发就被拒成
12
+ * `unknown column 'x' in <site>`(site 随语句位置变:join on / insert values / update sets …)。
13
+ * 线上表现是端点 500、日志里没有 SQL,看起来像「列不存在」,实际是声明漏了。
14
+ *
15
+ * 真实事故(2026-11):_platform 0017 加了 users.employee_no,schema.yaml 漏声明,
16
+ * /system/departments(选人列表依赖 /system/users)整页 500。
17
+ *
18
+ * 校验面取 `ALTER TABLE … ADD COLUMN`:已有表新增列的唯一入口,也是最容易「只写迁移、
19
+ * 忘了补声明」的一类。整表声明由 t-2(租户定性)与表数快照兜底。
20
+ */
21
+
22
+ const templatesRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../../templates");
23
+ const srcRoot = join(templatesRoot, "src");
24
+
25
+ interface ColumnDecl {
26
+ type?: string
27
+ [key: string]: unknown
28
+ }
29
+
30
+ interface SchemaDoc {
31
+ tables?: Record<string, { columns?: Record<string, ColumnDecl> }>
32
+ }
33
+
34
+ /** `ALTER TABLE <表> ADD [COLUMN] <列>`(忽略方言专属文件,sqlite 基础件是唯一真相) */
35
+ const ADD_COLUMN_RE = /ALTER\s+TABLE\s+[`"']?(\w+)[`"']?\s+ADD\s+(?:COLUMN\s+)?[`"']?(\w+)[`"']?/gi;
36
+
37
+ function walk(dir: string, out: string[] = []): string[] {
38
+ for (const entry of readdirSync(dir)) {
39
+ const full = join(dir, entry);
40
+ if (statSync(full).isDirectory())
41
+ walk(full, out);
42
+ else
43
+ out.push(full);
44
+ }
45
+ return out;
46
+ }
47
+
48
+ /** 表名 → 声明的列集合(跨模块同名表由 templates-tenant-scope 的 S002 断言拦,这里取并集) */
49
+ function loadDeclaredColumns(files: string[]): Map<string, Set<string>> {
50
+ const declared = new Map<string, Set<string>>();
51
+ for (const file of files.filter(f => f.endsWith("schema.yaml"))) {
52
+ const doc = parse(readFileSync(file, "utf-8")) as SchemaDoc | null;
53
+ for (const [table, decl] of Object.entries(doc?.tables ?? {}))
54
+ declared.set(table, new Set(Object.keys(decl.columns ?? {})));
55
+ }
56
+ return declared;
57
+ }
58
+
59
+ /** 基础迁移文件:`migrations/*.sql`,排除 `<名>.mysql.sql` / `<名>.pg.sql` */
60
+ function isBaseMigration(file: string): boolean {
61
+ return /[/\\]migrations[/\\][^/\\]+\.sql$/.test(file) && !/\.(?:mysql|pg)\.sql$/.test(file);
62
+ }
63
+
64
+ describe("templates 后端 schema.yaml 列白名单", () => {
65
+ it("s-1 迁移里 ADD COLUMN 的列都在 schema.yaml 有声明(漏声明 = 构造器拒查)", () => {
66
+ const files = walk(srcRoot);
67
+ const declared = loadDeclaredColumns(files);
68
+ const violations: string[] = [];
69
+
70
+ for (const file of files.filter(isBaseMigration)) {
71
+ for (const m of readFileSync(file, "utf-8").matchAll(ADD_COLUMN_RE)) {
72
+ const [, table, column] = m;
73
+ const where = relative(templatesRoot, file);
74
+ const cols = declared.get(table);
75
+ if (!cols) {
76
+ violations.push(`${where}: 表 ${table} 未在任何 schema.yaml 声明`);
77
+ continue;
78
+ }
79
+ if (!cols.has(column)) {
80
+ violations.push(
81
+ `${where}: ${table}.${column} 未在 schema.yaml 声明`
82
+ + `(构造器会拒为 unknown column '${column}' in on '${table}';补到 owning 模块的 tables.${table}.columns)`,
83
+ );
84
+ }
85
+ }
86
+ }
87
+
88
+ expect(violations).toEqual([]);
89
+ });
90
+
91
+ it("s-2 回归锚点:0017 三方言迁移齐备,users.employee_no 已声明为 text", () => {
92
+ const migDir = join(srcRoot, "app", "api", "_platform", "migrations");
93
+ for (const file of ["0017__users_employee_no.sql", "0017__users_employee_no.mysql.sql", "0017__users_employee_no.pg.sql"])
94
+ expect(existsSync(join(migDir, file)), `缺迁移 ${file}`).toBe(true);
95
+
96
+ const schema = parse(readFileSync(join(srcRoot, "app", "api", "_platform", "schema.yaml"), "utf-8")) as SchemaDoc;
97
+ expect(schema.tables?.users?.columns?.employee_no).toMatchObject({ type: "text" });
98
+ });
99
+ });
@@ -115,7 +115,7 @@ describe("templates 租户定性(A 租户表 / B 共享表)", () => {
115
115
  expect(unknown).toEqual([]);
116
116
  });
117
117
 
118
- it("t-4 28 张表的 A/B 分组与方案 v3 §5.2 一致(新增表须先定性)", () => {
118
+ it("t-4 27 张表的 A/B 分组与方案 v3 §5.2 一致(新增表须先定性)", () => {
119
119
  const { tables } = loadSchemaTables();
120
120
  const shared = [...tables].filter(([, d]) => d.tenant === false).map(([n]) => n).sort();
121
121
  // Phase 0 落地 21 张(11 A + 10 B);Phase 1 加企微三表,全部 B 组(D9:查它们时必然还没有租户上下文)
@@ -124,7 +124,8 @@ describe("templates 租户定性(A 租户表 / B 共享表)", () => {
124
124
  // auth/0002 加 wecom_sync_job / wecom_sync_lock 两张,同属 A 组:同步只在调用方自己的租户内跑
125
125
  // (D7 不接受 corpId 参数),故不进 shared_allow——跨租户读元数据那条另走显式 tenant_id 的裸 SQL。
126
126
  // 改这张表清单 = 改租户隔离边界,必须同步 config.yaml 的 tenant.shared_allow(t-2/t-3 会把关)。
127
- expect(tables.size).toBe(28);
127
+ // 2026-11-02 demo 模块下线(ecc7b35e)删掉 demo/api/schema.yaml 的 todos 表,28 → 27。
128
+ expect(tables.size).toBe(27);
128
129
  expect(shared).toEqual([
129
130
  "_oj_migrations",
130
131
  "market_history",
@@ -20,18 +20,18 @@ import type { ProjectLayout } from "./layout";
20
20
  import fs from "node:fs";
21
21
  import path from "node:path";
22
22
  import { parse } from "yaml";
23
- import { tryResolveLayout } from "./layout";
23
+ import { scanModules, tryResolveLayout } from "./layout";
24
24
 
25
25
  /** 生成物文件名(工程根)。`src/tsconfig.json` 以 `../tsconfig.paths.json` 引用 */
26
26
  export const ALIAS_PATHS_FILE = "tsconfig.paths.json";
27
27
 
28
28
  /** 生成物头注释:tsconfig 允许注释,且这是唯一会被 `ojm` 覆写的 tsconfig */
29
29
  const HEADER = [
30
- "// 由 ojm 生成:ojm init / ojm build / ojm dev 会按 src/*/api/manifest.yaml 重写本文件,请勿手改。",
30
+ "// 由 ojm 生成:ojm init / ojm build / ojm dev 会按模块清单重写本文件,请勿手改。",
31
31
  "//",
32
- "// oj 别名 `#/<模块名>/…` 锚在目标模块根(本布局下即 src/<模块名>/api)。tsconfig 的",
33
- "// paths 只能静态全局映射,且每条规则的替换值只允许一个 `*`(`./src/*/api/*` 会被 TS",
34
- "// 以 TS5062 拒掉),故按模块逐条枚举。",
32
+ "// oj 别名 `#/<模块名>/…` 锚在目标模块根(src/<模块名>/api 或 src/app/api/<模块名>)。",
33
+ "// tsconfig 的 paths 只能静态全局映射,且每条规则的替换值只允许一个 `*`(`./src/*/api/*`",
34
+ "// 会被 TS 以 TS5062 拒掉),故按模块逐条枚举。",
35
35
  ].join("\n");
36
36
 
37
37
  /** 读模块根 manifest 的 `name`(oj 的模块身份);读不到/格式坏时回落到目录名 */
@@ -58,14 +58,13 @@ export function aliasPathEntries(layout: ProjectLayout): Record<string, string[]
58
58
  if (layout.kind !== "module-root")
59
59
  return {};
60
60
  const entries: Record<string, string[]> = {};
61
- for (const mod of fs.readdirSync(layout.modulesSrc, { withFileTypes: true })) {
62
- if (!mod.isDirectory() || mod.name.startsWith("."))
61
+ // 以扫描结果为准:既覆盖平铺 src/<name>/api,也覆盖聚合容器 src/app/api/<name>
62
+ for (const mod of scanModules(layout)) {
63
+ if (!mod.apiSrc)
63
64
  continue;
64
- const apiDir = path.join(layout.modulesSrc, mod.name, "api");
65
- if (!fs.existsSync(path.join(apiDir, "manifest.yaml")))
66
- continue;
67
- const name = manifestName(apiDir, mod.name);
68
- entries[`#/${name}/*`] = [`./${path.posix.join("src", mod.name, "api", "*")}`];
65
+ const name = manifestName(mod.apiSrc, mod.name);
66
+ const rel = path.relative(layout.projectRoot, mod.apiSrc).split(path.sep).join("/");
67
+ entries[`#/${name}/*`] = [`./${rel}/*`];
69
68
  }
70
69
  return entries;
71
70
  }
package/src/builtin.ts CHANGED
@@ -64,9 +64,16 @@ export function readBuiltinModules(projectRoot: string): Set<string> {
64
64
  export function deriveBuiltinModules(include: Iterable<string>): Set<string> {
65
65
  const mods = new Set<string>();
66
66
  for (const p of include) {
67
- const m = /^src\/([^/]+)\/(?:api|web)(?:\/|$)/.exec(p)?.[1];
68
- if (m)
69
- mods.add(m);
67
+ // 聚合容器下的后端模块:src/app/api/<name>(模块名取 api 的下一段,不是容器名)
68
+ const nested = /^src\/[^/]+\/api\/([^/]+)(?:\/|$)/.exec(p)?.[1];
69
+ if (nested) {
70
+ mods.add(nested);
71
+ continue;
72
+ }
73
+ // 平铺后端 src/<name>/api 与前端 src/<name>/web/...
74
+ const flat = /^src\/([^/]+)\/(?:api|web)(?:\/|$)/.exec(p)?.[1];
75
+ if (flat)
76
+ mods.add(flat);
70
77
  }
71
78
  return mods;
72
79
  }
package/src/config.ts CHANGED
@@ -48,6 +48,15 @@ export interface ModulesConfig {
48
48
  * `(path, html) => string | { html, cacheControl? } | Promise<…>`)。
49
49
  */
50
50
  htmlTransform?: string
51
+ /**
52
+ * 生成 client 的归属模块:`api 前缀 → 承载其 __client 的 web 模块`。
53
+ *
54
+ * 缺省(不配)= 同名模块,即 `src/<api 前缀>/web/__client`(历史行为,零漂移)。
55
+ * 多根聚合模块把多个业务域的页面/client 收进同一个 web 模块时配它:生成物落
56
+ * `src/<归属模块>/web/<api 前缀>/__client`,页面相对导入 `../__client` 不变。
57
+ * 只影响 `ojm api` 的落盘位置与生成物里的认领护栏,不影响路由与后端。
58
+ */
59
+ clientOwners?: Record<string, string>
51
60
  }
52
61
 
53
62
  const DEFAULT_CONFIG: Required<Pick<ModulesConfig, "baseUrl" | "siteBase">> = {
@@ -0,0 +1,30 @@
1
+ import type { ProjectLayout } from "../../layout";
2
+ import { join } from "node:path";
3
+ import { describe, expect, it } from "vitest";
4
+ import { clientDirOf } from "../client-path";
5
+
6
+ const layout: ProjectLayout = {
7
+ kind: "module-root",
8
+ projectRoot: "/proj",
9
+ modulesSrc: "/proj/src",
10
+ distDir: "/proj/dist/web",
11
+ apiDist: "/proj/dist/api",
12
+ watchTarget: "/proj/src",
13
+ moduleRoot: "/proj/src",
14
+ };
15
+
16
+ describe("clientDirOf(client 落盘目录)", () => {
17
+ it("缺省 = 同名模块 src/<模块>/web/__client(历史口径零漂移)", () => {
18
+ expect(clientDirOf(layout, "/proj", "system")).toBe(join("/proj/src/system/web", "__client"));
19
+ });
20
+
21
+ it("clientOwners 指定归属 → src/<归属>/web/<api 前缀>/__client", () => {
22
+ expect(clientDirOf(layout, "/proj", "system", { system: "app" }))
23
+ .toBe(join("/proj/src/app/web/system", "__client"));
24
+ });
25
+
26
+ it("历史布局(layout=null)同样支持归属", () => {
27
+ expect(clientDirOf(null, "/proj", "system", { system: "app" }))
28
+ .toBe(join("/proj/web/src/app/system", "__client"));
29
+ });
30
+ });
@@ -72,7 +72,7 @@ function emit() {
72
72
  }
73
73
 
74
74
  describe("emitClient (module target)", () => {
75
- const { "api.ts": apiTs, "api.schemas.ts": schemasTs } = emit();
75
+ const { "index.ts": apiTs, "api.schemas.ts": schemasTs } = emit();
76
76
 
77
77
  it("aPI_PREFIX / MODULE_PREFIX 用 JSON 字符串字面量(注入安全)", () => {
78
78
  expect(apiTs).toContain("export const API_PREFIX = \"/wiki\";");
@@ -114,7 +114,7 @@ describe("emitClient (module target)", () => {
114
114
  describe("emitClient 注入安全:模块名含特殊字符不破坏源码", () => {
115
115
  it("module 名含 -/$ 时 MODULE_PREFIX/工厂名仍走字符串字面量(API_PREFIX 取 IR wirePrefix)", () => {
116
116
  const ir = irFromOpenapi(DOC, { base: "/v1/api" });
117
- const { "api.ts": apiTs } = emitClient(ir, { target: "module", module: "weird-name" });
117
+ const { "index.ts": apiTs } = emitClient(ir, { target: "module", module: "weird-name" });
118
118
  // module 选项只影响 MODULE_PREFIX 与 create<Module>Client 名
119
119
  expect(apiTs).toContain("export const MODULE_PREFIX = \"/weird-name\";");
120
120
  expect(apiTs).toContain("export function createWeirdNameClient(");
@@ -125,6 +125,24 @@ describe("emitClient 注入安全:模块名含特殊字符不破坏源码", ()
125
125
  });
126
126
  });
127
127
 
128
+ describe("emitClient 多根聚合归属(owner)", () => {
129
+ it("owner 与 module 不同 → 落 MODULE_OWNER,护栏对比归属模块;MODULE_PREFIX 仍是 api 前缀", () => {
130
+ const ir = irFromOpenapi(DOC, { base: "/v1/api" });
131
+ const { "index.ts": apiTs } = emitClient(ir, { target: "module", module: "system", owner: "app" });
132
+ expect(apiTs).toContain("export const MODULE_PREFIX = \"/system\";");
133
+ expect(apiTs).toContain("export const MODULE_OWNER = \"/app\";");
134
+ expect(apiTs).toContain("ctx.module.name !== MODULE_OWNER.slice(1)");
135
+ expect(apiTs).not.toContain("ctx.module.name !== MODULE_PREFIX.slice(1)");
136
+ });
137
+
138
+ it("owner 缺省 = module → 生成物与历史逐字一致(不出现 MODULE_OWNER)", () => {
139
+ const ir = irFromOpenapi(DOC, { base: "/v1/api" });
140
+ const { "index.ts": apiTs } = emitClient(ir, { target: "module", module: "system", owner: "system" });
141
+ expect(apiTs).not.toContain("MODULE_OWNER");
142
+ expect(apiTs).toContain("ctx.module.name !== MODULE_PREFIX.slice(1)");
143
+ });
144
+ });
145
+
128
146
  describe("emitClient 空 IR 报错", () => {
129
147
  it("iR 为空抛错(无端点无需生成)", () => {
130
148
  expect(() => emitClient([], { target: "module", module: "x" })).toThrow();
@@ -181,3 +181,26 @@ describe("handler-scan · 对账键与契约侧同形(参数路由精确命中
181
181
  expect(pathKey(row.method, row.full)).toBe(pathKey("DELETE", "wiki/{id}"));
182
182
  });
183
183
  });
184
+
185
+ describe("handler-scan · 工厂返回本地对象(const api = { … }; return api)", () => {
186
+ it("回归:聚合工厂 return 标识符时仍展开动词与嵌套组(solutions/api.ts 形态)", () => {
187
+ const rows = scan("api.ts", `
188
+ import type { Env } from "./types";
189
+ export function createApi(env: Env) {
190
+ \tconst routes = { async get() {} };
191
+ \tconst verify = {
192
+ \t\tasync post() {},
193
+ \t\tlast: { async get() {} },
194
+ \t};
195
+ \tconst api = { routes, verify };
196
+ \treturn api;
197
+ }
198
+ export default createApi({} as Env);
199
+ `);
200
+ expect(rows.map(r => pathKey(r.method, handlerAbs(r))).sort()).toEqual([
201
+ "GET /_platform/market/routes",
202
+ "GET /_platform/market/verify/last",
203
+ "POST /_platform/market/verify",
204
+ ]);
205
+ });
206
+ });
@@ -1,4 +1,4 @@
1
- // ScopedRequestLike 真源在 runtime contract(生成物 __client/api.ts 也从这里 import,
1
+ // ScopedRequestLike 真源在 runtime contract(生成物 __client/index.ts 也从这里 import,
2
2
  // 自身不再 re-export——旧写法 type import 自 __client 会在再生后 TS2305)。
3
3
  import type { ScopedRequestLike } from "@oj-module/runtime/contract/errors";
4
4
 
@@ -9,7 +9,7 @@ import type { ScopedRequestLike } from "@oj-module/runtime/contract/errors";
9
9
  // 不加载 @oj-module/runtime 重依赖(antd / pro-components 在 node 环境无法 import),仅桩 request、
10
10
  // 透传真实 zod(供生成 client 的 DEV 响应校验走真实路径)。ContractApiError 由轻量子路径提供。
11
11
  import { beforeEach, describe, expect, it, vi } from "vitest";
12
- import { bindRequest } from "../../../templates/src/_platform/web/__client/api";
12
+ import { bindRequest } from "../../../templates/src/app/web/_platform/__client";
13
13
  import { httpMarketClient } from "../../../templates/src/oj-market/web/api/defaultClient";
14
14
 
15
15
  // index() 直连 request(不走生成 client),故把桩 request 做成可变实现,测试内统一指向同一 mock。
@@ -46,9 +46,9 @@ describe("runApi 消费 openapi.json(新口径 Task #4)", () => {
46
46
  const result = await runApi({ cwd: dir, openapi: "openapi.json" });
47
47
  expect(result.contracts).toBe(2);
48
48
 
49
- const wikiApi = join(dir, "web/src/wiki/__client/api.ts");
49
+ const wikiApi = join(dir, "web/src/wiki/__client/index.ts");
50
50
  const wikiSchemas = join(dir, "web/src/wiki/__client/api.schemas.ts");
51
- const userApi = join(dir, "web/src/user/__client/api.ts");
51
+ const userApi = join(dir, "web/src/user/__client/index.ts");
52
52
  expect(existsSync(wikiApi)).toBe(true);
53
53
  expect(existsSync(wikiSchemas)).toBe(true);
54
54
  expect(existsSync(userApi)).toBe(true);
@@ -4,8 +4,9 @@ import type { IrEndpoint } from "./ir";
4
4
  import type { OpenApiDoc } from "./openapi-ir";
5
5
  import { existsSync, readdirSync, readFileSync } from "node:fs";
6
6
  import { dirname, join, relative, sep } from "node:path";
7
- import { apiModuleRoots, apiModulesSrc, apiSrcOf, legacyApiModuleRoots, resolveConfigPath, tryResolveLayout, webSrcOf } from "../layout";
7
+ import { apiModuleRoots, apiModulesSrc, apiSrcOf, legacyApiModuleRoots, resolveConfigPath, tryResolveLayout } from "../layout";
8
8
  import { readOjAnonymousPaths } from "../oj-config";
9
+ import { clientDirOf, loadClientOwners } from "./client-path";
9
10
  import { emitClient } from "./emit-client";
10
11
  import { planStubWrites } from "./emit-stub";
11
12
  import { handlerAbs, implementedRouteKeys, pathKey, scanHandlerFile, VERB_OF, walkApiFiles } from "./handler-scan";
@@ -306,18 +307,21 @@ async function checkFromOpenapi(openapiPath: string, cwd: string, _exemption: Ap
306
307
  const violations: CheckViolation[] = [];
307
308
  const layout = tryResolveLayout(cwd);
308
309
  // 契约工具的扫描单元是「模块 api 根」而不是整棵源码树:module-root 布局里
309
- // `src/<name>/api` 的 `api` 段不属于路由,且 `web/__client/api.ts` 不能被当 handler。
310
+ // `src/<name>/api` 的 `api` 段不属于路由,且 `web/__client/index.ts` 不能被当 handler。
310
311
  const roots = layout ? apiModuleRoots(layout) : legacyApiModuleRoots(cwd);
311
- const clientDirOf = (module: string) =>
312
- join(layout ? webSrcOf(layout, module) : join(cwd, "web/src", module), "__client");
312
+ const owners = await loadClientOwners(layout?.projectRoot ?? cwd);
313
313
  for (const [module, eps] of groupIrByModule(ir)) {
314
- const client = emitClient(eps, { target: "module", module });
315
- const clientDir = clientDirOf(module);
316
- for (const [file, content] of [["api.ts", client["api.ts"]], ["api.schemas.ts", client["api.schemas.ts"]]] as const) {
314
+ const client = emitClient(eps, { target: "module", module, owner: owners[module] });
315
+ const clientDir = clientDirOf(layout, cwd, module, owners);
316
+ for (const [file, content] of [["index.ts", client["index.ts"]], ["api.schemas.ts", client["api.schemas.ts"]]] as const) {
317
317
  const p = join(clientDir, file);
318
318
  if (!existsSync(p) || readFileSync(p, "utf8") !== content)
319
319
  violations.push({ level: "error", kind: "artifact-stale", message: `[ojm-api] 生成物过期或缺失:${relative(cwd, p)}——请重跑 ojm api 使其与 openapi.json 同步。`, filePath: p });
320
320
  }
321
+ // 旧生成物(0.2.54 前叫 api.ts):不删会被 oj dev 当路由扫描并因 import 逃逸报错
322
+ const legacy = join(clientDir, "api.ts");
323
+ if (existsSync(legacy))
324
+ violations.push({ level: "error", kind: "artifact-stale", message: `[ojm-api] 旧生成物仍在:${relative(cwd, legacy)}——入口已改为 index.ts,请重跑 ojm api(会删除旧文件)。`, filePath: legacy });
321
325
  }
322
326
  const apiSrcDir = layout ? apiModulesSrc(layout) : join(cwd, "api/src");
323
327
  if (existsSync(apiSrcDir)) {
@@ -0,0 +1,30 @@
1
+ import type { ProjectLayout } from "../layout";
2
+
3
+ import path from "node:path";
4
+ import { loadSiteConfig } from "../config";
5
+ import { webSrcOf } from "../layout";
6
+
7
+ /**
8
+ * 生成 client 的落盘目录(`ojm api` 写、`ojm api --check` 核对,同一真源)。
9
+ *
10
+ * 缺省口径不变:`<project>/src/<api 前缀>/web/__client`(历史布局 `web/src/<模块>/__client`)。
11
+ * 多根聚合模块(web.config `clientOwners`)把多个业务域的 client 收进同一个 web 模块:
12
+ * 落 `<project>/src/<归属模块>/web/<api 前缀>/__client`——页面与该目录一起迁移,
13
+ * 页面里的相对导入 `../../__client` 不用改。
14
+ */
15
+ export function clientDirOf(
16
+ layout: ProjectLayout | null,
17
+ cwd: string,
18
+ apiModule: string,
19
+ owners: Record<string, string> = {},
20
+ ): string {
21
+ const owner = owners[apiModule] ?? apiModule;
22
+ const base = layout ? webSrcOf(layout, owner) : path.join(cwd, "web/src", owner);
23
+ return owner === apiModule ? path.join(base, "__client") : path.join(base, apiModule, "__client");
24
+ }
25
+
26
+ /** 读 web.config.ts 的 `clientOwners`(缺文件 / 未配置 = 空表,口径回落同名模块) */
27
+ export async function loadClientOwners(projectRoot: string): Promise<Record<string, string>> {
28
+ const config = await loadSiteConfig(projectRoot);
29
+ return config.clientOwners ?? {};
30
+ }
@@ -229,8 +229,8 @@ function emitEndpoint(ep: IrEndpoint): string {
229
229
  }`;
230
230
  }
231
231
 
232
- function emitPrelude(target: "module" | "internal", module?: string, endpointNames: string[] = [], wirePrefix?: string): string {
233
- const binding = target === "module"
232
+ function emitPrelude(target: "module" | "internal", module?: string, endpointNames: string[] = [], wirePrefix?: string, owner?: string): string {
233
+ let binding = target === "module"
234
234
  ? `let req: ScopedRequestLike | undefined;
235
235
 
236
236
  /** 线上 API 前缀(ojm api 从契约抽取,唯一真源,勿手改):请求基址 + 前缀登记用 */
@@ -275,6 +275,20 @@ function ensureReq(): ScopedRequestLike {
275
275
  return request;
276
276
  }`;
277
277
 
278
+ // 多根聚合模块:client 落在归属模块目录下,认领护栏的对比基准随之改为归属模块名。
279
+ // 缺省(未配 clientOwners)owner === module,生成物与历史逐字一致(快照不变)。
280
+ if (target === "module" && owner && owner !== module) {
281
+ binding = binding
282
+ .replace(
283
+ `export const MODULE_PREFIX = ${JSON.stringify(`/${module}`)};`,
284
+ `export const MODULE_PREFIX = ${JSON.stringify(`/${module}`)};
285
+
286
+ /** 认领方模块(web.config clientOwners 指定):跨模块认领护栏对比基准 */
287
+ export const MODULE_OWNER = ${JSON.stringify(`/${owner}`)};`,
288
+ )
289
+ .replaceAll("MODULE_PREFIX.slice(1)", "MODULE_OWNER.slice(1)");
290
+ }
291
+
278
292
  const ctxImport = target === "module"
279
293
  ? "import type { ModuleContext } from \"@oj-module/runtime\";\n"
280
294
  : "";
@@ -343,8 +357,15 @@ ${entries.join("\n")}
343
357
  `;
344
358
  }
345
359
 
346
- /** 发射双产物:api.ts(类型 + 请求函数 + 工厂)与 api.schemas.ts(DEV 校验 schema) */
347
- export function emitClient(ir: IrEndpoint[], opts: { target: "module" | "internal", module?: string }): { "api.ts": string, "api.schemas.ts": string } {
360
+ /**
361
+ * 发射双产物:index.ts(类型 + 请求函数 + 工厂)与 api.schemas.ts(DEV 校验 schema)。
362
+ *
363
+ * 入口文件名是 `index.ts` 而非 `api.ts`:oj dev 的 `--module-root` 扫描会把模块目录下
364
+ * **任何 `api.ts`** 当路由文件导入,聚合模块(src/app 同时有 api/ 与 web/)里的
365
+ * `web/<root>/__client/index.ts` 会被误扫(import 逃逸 project root 直接报错)。
366
+ * `index.ts` 不在扫描名单内,且 `from "../__client"` 的导入更短。
367
+ */
368
+ export function emitClient(ir: IrEndpoint[], opts: { target: "module" | "internal", module?: string, /** client 归属模块(多根聚合);缺省 = module */ owner?: string }): { "index.ts": string, "api.schemas.ts": string } {
348
369
  if (ir.length === 0)
349
370
  throw new Error("[ojm-api] client 发射失败:IR 为空——openapi.json 里没有任何端点(operationId + tags),无需生成;请检查 \`oj openapi\` 产物。");
350
371
  if (opts.target === "module" && !opts.module) {
@@ -362,7 +383,7 @@ export function emitClient(ir: IrEndpoint[], opts: { target: "module" | "interna
362
383
  + "——register.apiPrefix 一模块一值,urlPrefix 映射须整模块一致(不同前缀请拆模块)。",
363
384
  );
364
385
  }
365
- const sections: string[] = [emitPrelude(opts.target, opts.module, ir.map(ep => ep.name), wirePrefixes.size === 1 ? wirePrefixes.values().next().value : undefined)];
386
+ const sections: string[] = [emitPrelude(opts.target, opts.module, ir.map(ep => ep.name), wirePrefixes.size === 1 ? wirePrefixes.values().next().value : undefined, opts.owner)];
366
387
  for (const ep of ir) {
367
388
  const types = emitTypes(ep);
368
389
  if (types.length)
@@ -372,7 +393,7 @@ export function emitClient(ir: IrEndpoint[], opts: { target: "module" | "interna
372
393
  sections.push(emitEndpoint(ep));
373
394
  }
374
395
  return {
375
- "api.ts": `${sections.join("\n\n")}\n`,
396
+ "index.ts": `${sections.join("\n\n")}\n`,
376
397
  "api.schemas.ts": emitSchemas(ir, opts.target),
377
398
  };
378
399
  }
@@ -161,7 +161,23 @@ function defaultExportObject(sf: ts.SourceFile, decls: Map<string, ts.ObjectLite
161
161
  * 旧逻辑直接 dottedNameOf(call.expression) 对 CallExpression 返回 undefined → 扫成 0 路由,
162
162
  * 致使 ojm api --check 把 market 全族端点判为「未实现 / stub 缺失」。此处穿透一层取被调函数名。
163
163
  */
164
- function factoryReturnObjects(sf: ts.SourceFile, call: ts.CallExpression): ts.ObjectLiteralExpression[] {
164
+ /**
165
+ * 取函数体内 `const <name> = { … }` 的对象字面量初始化器(工厂 return 标识符时用)。
166
+ * 只认直接声明,不追闭包/别名——够用即可,追深了会把无关对象当路由组。
167
+ */
168
+ function objectLiteralOf(body: ts.Block, name: string): ts.ObjectLiteralExpression | undefined {
169
+ for (const st of body.statements) {
170
+ if (!ts.isVariableStatement(st))
171
+ continue;
172
+ for (const d of st.declarationList.declarations) {
173
+ if (ts.isIdentifier(d.name) && d.name.text === name && d.initializer && ts.isObjectLiteralExpression(d.initializer))
174
+ return d.initializer;
175
+ }
176
+ }
177
+ return undefined;
178
+ }
179
+
180
+ function factoryReturnObjects(sf: ts.SourceFile, call: ts.CallExpression, decls: Map<string, ts.ObjectLiteralExpression>): ts.ObjectLiteralExpression[] {
165
181
  // `export default createXxxApi(marketEnv())`:`call.expression` 为带实参的调用表达式,
166
182
  // 函数名在其 `.expression`;`export default obj.createXxxApi(marketEnv())` 同样穿透。
167
183
  const target = ts.isCallExpression(call.expression) ? call.expression.expression : call.expression;
@@ -171,10 +187,31 @@ function factoryReturnObjects(sf: ts.SourceFile, call: ts.CallExpression): ts.Ob
171
187
  for (const stmt of sf.statements) {
172
188
  if (!ts.isFunctionDeclaration(stmt) || stmt.name?.text !== callee || stmt.body === undefined)
173
189
  continue;
190
+ // 工厂体内的局部对象声明(`const routes = {…}`)合并进声明表:
191
+ // return 的对象里用简写属性引用它们时,walk 才能解析到组。
192
+ for (const st of stmt.body.statements) {
193
+ if (!ts.isVariableStatement(st))
194
+ continue;
195
+ for (const d of st.declarationList.declarations) {
196
+ if (ts.isIdentifier(d.name) && d.initializer && ts.isObjectLiteralExpression(d.initializer))
197
+ decls.set(d.name.text, d.initializer);
198
+ }
199
+ }
174
200
  const out: ts.ObjectLiteralExpression[] = [];
175
201
  for (const st of stmt.body.statements) {
176
- if (ts.isReturnStatement(st) && st.expression !== undefined && ts.isObjectLiteralExpression(st.expression))
202
+ if (!ts.isReturnStatement(st) || st.expression === undefined)
203
+ continue;
204
+ if (ts.isObjectLiteralExpression(st.expression)) {
177
205
  out.push(st.expression);
206
+ continue;
207
+ }
208
+ // `const api = { routes, verify }; … return api;` 形态(聚合工厂):
209
+ // 返回标识符时回溯同函数体内的 object literal 初始化器。
210
+ if (ts.isIdentifier(st.expression)) {
211
+ const local = objectLiteralOf(stmt.body, st.expression.text);
212
+ if (local)
213
+ out.push(local);
214
+ }
178
215
  }
179
216
  return out;
180
217
  }
@@ -317,7 +354,7 @@ export function scanHandlerFile(filePath: string, apiDir: string, moduleName = "
317
354
  ? expr
318
355
  : (ts.isIdentifier(expr) ? callDecls.get(expr.text) : undefined);
319
356
  if (callExpr) {
320
- for (const obj of factoryReturnObjects(sf, callExpr))
357
+ for (const obj of factoryReturnObjects(sf, callExpr, decls))
321
358
  walk(obj, "", "", 1);
322
359
  }
323
360
  }
@@ -2,13 +2,14 @@ import type { OjSchemaDiag } from "./emit-oj-schema";
2
2
  import type { IrEndpoint } from "./ir";
3
3
  import type { OpenApiDoc } from "./openapi-ir";
4
4
  import { execFile as execFileCb } from "node:child_process";
5
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
6
6
  import { createRequire } from "node:module";
7
7
  import { basename, dirname, join } from "node:path";
8
8
  import process from "node:process";
9
9
  import { promisify } from "node:util";
10
10
  import { stringify as stringifyYaml } from "yaml";
11
- import { apiModuleRoots, apiModulesSrc, apiSrcOf, legacyApiModuleRoots, scanModules, tryResolveLayout, webSrcOf } from "../layout";
11
+ import { apiModuleRoots, apiModulesSrc, apiSrcOf, legacyApiModuleRoots, scanModules, tryResolveLayout } from "../layout";
12
+ import { clientDirOf, loadClientOwners } from "./client-path";
12
13
  import { emitClient } from "./emit-client";
13
14
  import { applyStubWrites, planStubWrites } from "./emit-stub";
14
15
  import { implementedRouteKeys } from "./handler-scan";
@@ -83,12 +84,21 @@ async function runFromOpenapi(openapiPath: string, cwd: string, result: RunResul
83
84
  const ir = irFromOpenapi(doc, { rawPaths });
84
85
  const byModule = groupIrByModule(ir);
85
86
  const layout = tryResolveLayout(cwd);
87
+ const owners = await loadClientOwners(layout?.projectRoot ?? cwd);
86
88
  result.contracts += byModule.size;
87
89
  for (const [module, eps] of byModule) {
88
- const client = emitClient(eps, { target: "module", module });
89
- const clientDir = join(layout ? webSrcOf(layout, module) : join(cwd, "web/src", module), "__client");
90
- writeIfChanged(join(clientDir, "api.ts"), client["api.ts"], result);
90
+ const client = emitClient(eps, { target: "module", module, owner: owners[module] });
91
+ const clientDir = clientDirOf(layout, cwd, module, owners);
92
+ writeIfChanged(join(clientDir, "index.ts"), client["index.ts"], result);
91
93
  writeIfChanged(join(clientDir, "api.schemas.ts"), client["api.schemas.ts"], result);
94
+ // 迁移:0.2.54 起入口文件从 api.ts 改为 index.ts。旧文件必须删——oj dev 的
95
+ // --module-root 扫描会把模块目录下任何 api.ts 当路由导入(聚合模块的
96
+ // web/<root>/__client/index.ts 会直接报 import 逃逸)。
97
+ const legacy = join(clientDir, "api.ts");
98
+ if (existsSync(legacy)) {
99
+ rmSync(legacy);
100
+ result.written.push(legacy);
101
+ }
92
102
  }
93
103
  // stub:oj 目录镜像树(人碰过的文件永不写——plan 阶段已按指纹判好)。
94
104
  // 已实现键全量扫一遍传入:嵌套组(模块根单文件)实现的端点不再生成镜像 stub。