@oj-module/cli 0.2.18 → 0.2.20

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 (331) hide show
  1. package/devkit/README.md +4 -4
  2. package/devkit/SKILL.md +41 -37
  3. package/devkit/framework-dev.md +6 -5
  4. package/devkit/manual.md +259 -161
  5. package/devkit/menu-rules.md +3 -3
  6. package/devkit/module-dev-and-cdn-playground.md +31 -22
  7. package/devkit/sql-builder-missing-capabilities-pr.md +11 -6
  8. package/package.json +2 -2
  9. package/shell-dist/assets/runtime.js +2 -2
  10. package/shell-dist/index.html +2 -2
  11. package/shell-dist/versions.json +3 -3
  12. package/src/__tests__/build-backend.test.ts +54 -0
  13. package/src/__tests__/layout.test.ts +176 -0
  14. package/src/__tests__/module-list.test.ts +66 -0
  15. package/src/__tests__/oj-caps.test.ts +114 -0
  16. package/src/alias-paths.ts +102 -0
  17. package/src/build.ts +41 -9
  18. package/src/bundle.ts +25 -13
  19. package/src/config.ts +82 -9
  20. package/src/contract/__tests__/market-client.integration.test.ts +2 -2
  21. package/src/contract/check.ts +56 -28
  22. package/src/contract/emit-stub.ts +21 -9
  23. package/src/contract/handler-scan.ts +19 -10
  24. package/src/contract/run.ts +14 -4
  25. package/src/dev.ts +22 -7
  26. package/src/index.ts +1 -1
  27. package/src/info.ts +20 -3
  28. package/src/init.ts +91 -21
  29. package/src/layout.ts +282 -24
  30. package/src/lifecycle/__tests__/bundle.test.ts +16 -1
  31. package/src/lifecycle/__tests__/manifest-contract.test.ts +2 -2
  32. package/src/market/__tests__/bootstrap.test.ts +17 -9
  33. package/src/market/bootstrap.ts +30 -17
  34. package/src/oj-caps.ts +133 -0
  35. package/src/oj.ts +27 -4
  36. package/src/preview-config.ts +1 -1
  37. package/src/preview.ts +2 -1
  38. package/src/site.ts +2 -2
  39. package/src/usage.ts +6 -6
  40. package/src/vendor.ts +5 -1
  41. package/templates/{api/config.yaml → config.yaml} +13 -8
  42. package/templates/global.d.ts +7 -6
  43. package/templates/init.include.yaml +41 -36
  44. package/templates/package.json +1 -1
  45. package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/installer.ts +4 -0
  46. package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/smoke.ts +21 -2
  47. package/templates/{api/src/_platform → src/_platform/api}/market/api.ts +25 -15
  48. package/templates/{api/src/_platform → src/_platform/api}/solutions/api.ts +7 -3
  49. package/templates/{api/src/auth → src/auth/api}/_shared/login-throttle.ts +2 -2
  50. package/templates/{api/src/auth → src/auth/api}/_shared/session.ts +2 -2
  51. package/templates/{api/src/auth → src/auth/api}/api.ts +1 -1
  52. package/templates/{api/src/auth → src/auth/api}/ldap-login/api.ts +1 -1
  53. package/templates/src/auth/api/manifest.yaml +6 -0
  54. package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/InstallDialog.test.tsx +53 -1
  55. package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/marketClient.fake.ts +6 -0
  56. package/templates/{web/src/oj-market → src/oj-market/web}/api/client.ts +5 -0
  57. package/templates/{web/src/oj-market → src/oj-market/web}/api/defaultClient.ts +7 -1
  58. package/templates/{web/src/oj-market → src/oj-market/web}/components/InstallDialog.tsx +80 -5
  59. package/templates/{web/src/oj-market → src/oj-market/web}/hooks/useMarket.ts +6 -1
  60. package/templates/{web/src/oj-market → src/oj-market/web}/locales/en-US.json +10 -1
  61. package/templates/{web/src/oj-market → src/oj-market/web}/locales/zh-CN.json +10 -1
  62. package/templates/{web/src/oj-market → src/oj-market/web}/types.ts +15 -2
  63. package/templates/{api/src/system → src/system/api}/login-methods/api.ts +2 -2
  64. package/templates/src/system/api/manifest.yaml +6 -0
  65. package/templates/{api/src/system → src/system/api}/oplog/api.ts +4 -4
  66. package/templates/{api/src/system → src/system/api}/permissions/api.ts +3 -3
  67. package/templates/{api/src/system → src/system/api}/roles/api.ts +3 -3
  68. package/templates/{api/src/system → src/system/api}/roles/bindings/api.ts +3 -3
  69. package/templates/{api/src/system → src/system/api}/settings/api.ts +3 -3
  70. package/templates/{api/src/system → src/system/api}/users/api.ts +3 -3
  71. package/templates/{api/src/system → src/system/api}/users/forbidden/api.ts +3 -3
  72. package/templates/{api/src/system → src/system/api}/users/password/api.ts +3 -3
  73. package/templates/{api/src/system → src/system/api}/users/roles/api.ts +3 -3
  74. package/templates/src/tsconfig.json +16 -0
  75. package/templates/{api/tests → tests}/auth-ldap.spec.ts +2 -2
  76. package/templates/{api/tests → tests}/auth-session.spec.ts +3 -3
  77. package/templates/{api/tests → tests}/auth-throttle.spec.ts +1 -1
  78. package/templates/tests/fixture.ts +434 -0
  79. package/templates/{api/tests → tests}/guard.spec.ts +1 -1
  80. package/templates/{api/tests → tests}/lifecycle/atomic-state.spec.ts +3 -3
  81. package/templates/{api/tests → tests}/lifecycle/bootstrap.spec.ts +8 -4
  82. package/templates/{api/tests → tests}/lifecycle/deploy-smoke.spec.ts +31 -8
  83. package/templates/{api/tests → tests}/lifecycle/installer.spec.ts +15 -15
  84. package/templates/{api/tests → tests}/lifecycle/market-api.spec.ts +30 -29
  85. package/templates/{api/tests → tests}/lifecycle/migrate.spec.ts +6 -6
  86. package/templates/{api/tests → tests}/lifecycle/preflight.spec.ts +4 -4
  87. package/templates/{api/tests → tests}/lifecycle/register.spec.ts +1 -1
  88. package/templates/{api/tests → tests}/lifecycle/semver.spec.ts +1 -1
  89. package/templates/{api/tests → tests}/lifecycle/sha384.spec.ts +1 -1
  90. package/templates/{api/tests → tests}/lifecycle/sig-fixtures.spec.ts +1 -1
  91. package/templates/{api/tests → tests}/lifecycle/solutions-api.spec.ts +1 -1
  92. package/templates/{api/tests → tests}/lifecycle/steps-fs.spec.ts +12 -12
  93. package/templates/{api/tests → tests}/lifecycle/text-edit.spec.ts +1 -1
  94. package/templates/{api/tests → tests}/lifecycle/tgz-guard.spec.ts +1 -1
  95. package/templates/{api/tests → tests}/lifecycle/trust.spec.ts +5 -5
  96. package/templates/{api/tests → tests}/lifecycle/uninstall.spec.ts +7 -7
  97. package/templates/{api/tests → tests}/lifecycle/verify-checks.spec.ts +9 -6
  98. package/templates/{api/tests → tests}/lifecycle/verify-report.spec.ts +7 -5
  99. package/templates/{api/tests → tests}/query-utils.spec.ts +1 -1
  100. package/templates/{api/tests → tests}/rbac-audit.spec.ts +1 -1
  101. package/templates/{api/tests → tests}/rbac-contacts.spec.ts +2 -2
  102. package/templates/{api/tests → tests}/rbac-guard.spec.ts +1 -1
  103. package/templates/{api/tests → tests}/rbac-roles.spec.ts +2 -2
  104. package/templates/{api/tests → tests}/rbac-seed.spec.ts +1 -1
  105. package/templates/{api/tests → tests}/rbac-settings.spec.ts +2 -2
  106. package/templates/{api/tests → tests}/rbac-solution-perms.spec.ts +1 -1
  107. package/templates/{api/tests → tests}/rbac-users.spec.ts +3 -3
  108. package/templates/tsconfig.json +5 -1
  109. package/templates/tsconfig.paths.json +19 -0
  110. package/templates/types/oj-modules.d.ts +17 -0
  111. package/templates/vitest.config.ts +22 -0
  112. package/templates/web.config.ts +23 -13
  113. package/vendor/host-versions.json +3 -3
  114. package/templates/api/src/auth/manifest.yaml +0 -5
  115. package/templates/api/src/system/manifest.yaml +0 -5
  116. package/templates/api/tests/fixture.ts +0 -58
  117. package/templates/api/tsconfig.json +0 -8
  118. package/templates/api/vitest.config.ts +0 -12
  119. package/templates/logs/task-events.jsonl +0 -0
  120. /package/templates/{api/.ojm-api-exempt.json → .ojm-api-exempt.json} +0 -0
  121. /package/templates/{api/seed.mysql.sql → seed.mysql.sql} +0 -0
  122. /package/templates/{api/seed.pg.sql → seed.pg.sql} +0 -0
  123. /package/templates/{api/src/_platform → src/_platform/api}/README.md +0 -0
  124. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/atomic.ts +0 -0
  125. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/history.ts +0 -0
  126. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/lock.ts +0 -0
  127. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/semver.ts +0 -0
  128. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/sha384.ts +0 -0
  129. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/state.ts +0 -0
  130. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/deploy-web.ts +0 -0
  131. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/download.ts +0 -0
  132. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/extract.ts +0 -0
  133. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/migrate.ts +0 -0
  134. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/preflight.ts +0 -0
  135. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/register.ts +0 -0
  136. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/reload.ts +0 -0
  137. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/verify.ts +0 -0
  138. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/tasks.ts +0 -0
  139. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/text-edit.ts +0 -0
  140. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/tgz-guard.ts +0 -0
  141. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/trust.ts +0 -0
  142. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/types.ts +0 -0
  143. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/uninstall.ts +0 -0
  144. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/verify-checks.ts +0 -0
  145. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/verify-report.ts +0 -0
  146. /package/templates/{api/src/_platform → src/_platform/api}/_shared/respond.ts +0 -0
  147. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/domain/contacts.ts +0 -0
  148. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/domain/roles.ts +0 -0
  149. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/domain/settings.ts +0 -0
  150. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/domain/solution-perms.ts +0 -0
  151. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/domain/users.ts +0 -0
  152. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/infra/access.ts +0 -0
  153. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/infra/audit.ts +0 -0
  154. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/infra/guard.ts +0 -0
  155. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/infra/oplog.ts +0 -0
  156. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/infra/query.ts +0 -0
  157. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/infra/rate.ts +0 -0
  158. /package/templates/{api/src/_platform → src/_platform/api}/_shared/site-facade.ts +0 -0
  159. /package/templates/{api/src/_platform → src/_platform/api}/contacts/api.ts +0 -0
  160. /package/templates/{api/src/_platform → src/_platform/api}/contacts/detail/api.ts +0 -0
  161. /package/templates/{api/src/_platform → src/_platform/api}/fixtures/l1-test-user.sql +0 -0
  162. /package/templates/{api/src/_platform → src/_platform/api}/manifest.yaml +0 -0
  163. /package/templates/{api/src/_platform → src/_platform/api}/market/bootstrap.ts +0 -0
  164. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0001__create_users.mysql.sql +0 -0
  165. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0001__create_users.pg.sql +0 -0
  166. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0001__create_users.sql +0 -0
  167. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0002__add_users_avatar_base64.mysql.sql +0 -0
  168. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0002__add_users_avatar_base64.sql +0 -0
  169. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0003__users_source_display_name.mysql.sql +0 -0
  170. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0003__users_source_display_name.sql +0 -0
  171. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0004__sys_rbac.mysql.sql +0 -0
  172. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0004__sys_rbac.pg.sql +0 -0
  173. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0004__sys_rbac.sql +0 -0
  174. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0005__users_must_change.mysql.sql +0 -0
  175. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0005__users_must_change.sql +0 -0
  176. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0006__users_is_forbidden.mysql.sql +0 -0
  177. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0006__users_is_forbidden.sql +0 -0
  178. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0007__users_email.sql +0 -0
  179. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0008__sys_refresh_token.mysql.sql +0 -0
  180. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0008__sys_refresh_token.pg.sql +0 -0
  181. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0008__sys_refresh_token.sql +0 -0
  182. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0009__sys_throttle.mysql.sql +0 -0
  183. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0009__sys_throttle.pg.sql +0 -0
  184. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0009__sys_throttle.sql +0 -0
  185. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0010__sys_solution_route.mysql.sql +0 -0
  186. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0010__sys_solution_route.pg.sql +0 -0
  187. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0010__sys_solution_route.sql +0 -0
  188. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0011__sys_setting_group_label.mysql.sql +0 -0
  189. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0011__sys_setting_group_label.sql +0 -0
  190. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0012__market_history.mysql.sql +0 -0
  191. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0012__market_history.pg.sql +0 -0
  192. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0012__market_history.sql +0 -0
  193. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0013__users_profile.sql +0 -0
  194. /package/templates/{api/src/_platform → src/_platform/api}/schema.yaml +0 -0
  195. /package/templates/{api/src/_platform → src/_platform/api}/seed.mysql.sql +0 -0
  196. /package/templates/{api/src/_platform → src/_platform/api}/seed.pg.sql +0 -0
  197. /package/templates/{api/src/_platform → src/_platform/api}/seed.sql +0 -0
  198. /package/templates/{web/src/_platform → src/_platform/web}/__client/api.schemas.ts +0 -0
  199. /package/templates/{web/src/_platform → src/_platform/web}/__client/api.ts +0 -0
  200. /package/templates/{web/src/about → src/about/web}/entry.ts +0 -0
  201. /package/templates/{web/src/about → src/about/web}/locales/en-US.json +0 -0
  202. /package/templates/{web/src/about → src/about/web}/locales/zh-CN.json +0 -0
  203. /package/templates/{web/src/about → src/about/web}/pages/constants.ts +0 -0
  204. /package/templates/{web/src/about → src/about/web}/pages/index.tsx +0 -0
  205. /package/templates/{web/src/access → src/access/web}/entry.ts +0 -0
  206. /package/templates/{web/src/access → src/access/web}/locales/en-US.json +0 -0
  207. /package/templates/{web/src/access → src/access/web}/locales/zh-CN.json +0 -0
  208. /package/templates/{web/src/access → src/access/web}/pages/access-mode/index.tsx +0 -0
  209. /package/templates/{web/src/access → src/access/web}/pages/admin-visible/index.tsx +0 -0
  210. /package/templates/{web/src/access → src/access/web}/pages/button-control/index.tsx +0 -0
  211. /package/templates/{web/src/access → src/access/web}/pages/common-visible/index.tsx +0 -0
  212. /package/templates/{web/src/access → src/access/web}/pages/page-control/index.tsx +0 -0
  213. /package/templates/{api/src/auth → src/auth/api}/_shared/ldap-login.ts +0 -0
  214. /package/templates/{api/src/auth → src/auth/api}/_shared/ldap.ts +0 -0
  215. /package/templates/{web/src/auth → src/auth/web}/__client/api.schemas.ts +0 -0
  216. /package/templates/{web/src/auth → src/auth/web}/__client/api.ts +0 -0
  217. /package/templates/{api/src/demo → src/demo/api}/manifest.yaml +0 -0
  218. /package/templates/{api/src/demo → src/demo/api}/migrations/0001__create_todos.sql +0 -0
  219. /package/templates/{api/src/demo → src/demo/api}/schema.yaml +0 -0
  220. /package/templates/{api/src/demo → src/demo/api}/seed.sql +0 -0
  221. /package/templates/{api/src/demo → src/demo/api}/todos/api.ts +0 -0
  222. /package/templates/{web/src/demo → src/demo/web}/__client/api.schemas.ts +0 -0
  223. /package/templates/{web/src/demo → src/demo/web}/__client/api.ts +0 -0
  224. /package/templates/{web/src/demo → src/demo/web}/entry.ts +0 -0
  225. /package/templates/{web/src/demo → src/demo/web}/locales/en-US.json +0 -0
  226. /package/templates/{web/src/demo → src/demo/web}/locales/zh-CN.json +0 -0
  227. /package/templates/{web/src/demo → src/demo/web}/pages/index.tsx +0 -0
  228. /package/templates/{web/src/exception → src/exception/web}/entry.ts +0 -0
  229. /package/templates/{web/src/exception → src/exception/web}/locales/en-US.json +0 -0
  230. /package/templates/{web/src/exception → src/exception/web}/locales/zh-CN.json +0 -0
  231. /package/templates/{web/src/exception → src/exception/web}/pages/403/index.tsx +0 -0
  232. /package/templates/{web/src/exception → src/exception/web}/pages/404/index.tsx +0 -0
  233. /package/templates/{web/src/exception → src/exception/web}/pages/500/index.tsx +0 -0
  234. /package/templates/{web/src/exception → src/exception/web}/pages/unknown-component/index.tsx +0 -0
  235. /package/templates/{api/src/home → src/home/api}/line/api.ts +0 -0
  236. /package/templates/{api/src/home → src/home/api}/manifest.yaml +0 -0
  237. /package/templates/{api/src/home → src/home/api}/migrations/0001__create_home_tables.sql +0 -0
  238. /package/templates/{api/src/home → src/home/api}/pie/api.ts +0 -0
  239. /package/templates/{api/src/home → src/home/api}/schema.yaml +0 -0
  240. /package/templates/{api/src/home → src/home/api}/seed.sql +0 -0
  241. /package/templates/{web/src/home → src/home/web}/__client/api.schemas.ts +0 -0
  242. /package/templates/{web/src/home → src/home/web}/__client/api.ts +0 -0
  243. /package/templates/{web/src/home → src/home/web}/entry.ts +0 -0
  244. /package/templates/{web/src/home → src/home/web}/locales/en-US.json +0 -0
  245. /package/templates/{web/src/home → src/home/web}/locales/zh-CN.json +0 -0
  246. /package/templates/{web/src/home → src/home/web}/pages/components/bar-chart.tsx +0 -0
  247. /package/templates/{web/src/home → src/home/web}/pages/components/card-list.tsx +0 -0
  248. /package/templates/{web/src/home → src/home/web}/pages/components/line-chart.tsx +0 -0
  249. /package/templates/{web/src/home → src/home/web}/pages/components/pie-chart.tsx +0 -0
  250. /package/templates/{web/src/home → src/home/web}/pages/index.tsx +0 -0
  251. /package/templates/{web/src/login → src/login/web}/entry.ts +0 -0
  252. /package/templates/{web/src/login → src/login/web}/locales/en-US.json +0 -0
  253. /package/templates/{web/src/login → src/login/web}/locales/zh-CN.json +0 -0
  254. /package/templates/{web/src/login → src/login/web}/pages/change-password.tsx +0 -0
  255. /package/templates/{web/src/login → src/login/web}/pages/login.tsx +0 -0
  256. /package/templates/{api/src/notification → src/notification/api}/_shared/notify.ts +0 -0
  257. /package/templates/{api/src/notification → src/notification/api}/api.ts +0 -0
  258. /package/templates/{api/src/notification → src/notification/api}/del/api.ts +0 -0
  259. /package/templates/{api/src/notification → src/notification/api}/manifest.yaml +0 -0
  260. /package/templates/{api/src/notification → src/notification/api}/migrations/0001__create_notifications.sql +0 -0
  261. /package/templates/{api/src/notification → src/notification/api}/migrations/0002__notifications_user.sql +0 -0
  262. /package/templates/{api/src/notification → src/notification/api}/migrations/0003__notifications_broadcast.sql +0 -0
  263. /package/templates/{api/src/notification → src/notification/api}/migrations/0004__notifications_broadcast_hidden.sql +0 -0
  264. /package/templates/{api/src/notification → src/notification/api}/schema.yaml +0 -0
  265. /package/templates/{api/src/notification → src/notification/api}/seed.sql +0 -0
  266. /package/templates/{web/src/notification → src/notification/web}/__client/api.schemas.ts +0 -0
  267. /package/templates/{web/src/notification → src/notification/web}/__client/api.ts +0 -0
  268. /package/templates/{web/src/notification → src/notification/web}/entry.ts +0 -0
  269. /package/templates/{web/src/notification → src/notification/web}/locales/en-US.json +0 -0
  270. /package/templates/{web/src/notification → src/notification/web}/locales/zh-CN.json +0 -0
  271. /package/templates/{web/src/notification → src/notification/web}/pages/center/index.tsx +0 -0
  272. /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/CardGrid.test.tsx +0 -0
  273. /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/HistoryDrawer.test.tsx +0 -0
  274. /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/RollbackDialog.test.tsx +0 -0
  275. /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/UninstallConfirm.test.tsx +0 -0
  276. /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/poll.test.ts +0 -0
  277. /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/render.tsx +0 -0
  278. /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/runtime-stub.ts +0 -0
  279. /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/semver.test.ts +0 -0
  280. /package/templates/{web/src/oj-market → src/oj-market/web}/api/MarketClientProvider.tsx +0 -0
  281. /package/templates/{web/src/oj-market → src/oj-market/web}/api/context.ts +0 -0
  282. /package/templates/{web/src/oj-market → src/oj-market/web}/components/CardGrid.tsx +0 -0
  283. /package/templates/{web/src/oj-market → src/oj-market/web}/components/CardItem.tsx +0 -0
  284. /package/templates/{web/src/oj-market → src/oj-market/web}/components/DetailDrawer.tsx +0 -0
  285. /package/templates/{web/src/oj-market → src/oj-market/web}/components/HistoryDrawer.tsx +0 -0
  286. /package/templates/{web/src/oj-market → src/oj-market/web}/components/RollbackDialog.tsx +0 -0
  287. /package/templates/{web/src/oj-market → src/oj-market/web}/components/UninstallConfirm.tsx +0 -0
  288. /package/templates/{web/src/oj-market → src/oj-market/web}/entry.ts +0 -0
  289. /package/templates/{web/src/oj-market → src/oj-market/web}/pages/MarketApp.tsx +0 -0
  290. /package/templates/{web/src/oj-market → src/oj-market/web}/utils/poll.ts +0 -0
  291. /package/templates/{web/src/oj-market → src/oj-market/web}/utils/semver.ts +0 -0
  292. /package/templates/{web/src/outside → src/outside/web}/entry.ts +0 -0
  293. /package/templates/{web/src/outside → src/outside/web}/locales/en-US.json +0 -0
  294. /package/templates/{web/src/outside → src/outside/web}/locales/zh-CN.json +0 -0
  295. /package/templates/{api/src/personal-center → src/personal-center/api}/manifest.yaml +0 -0
  296. /package/templates/{api/src/personal-center → src/personal-center/api}/profile/api.ts +0 -0
  297. /package/templates/{api/src/personal-center → src/personal-center/api}/upload/api.ts +0 -0
  298. /package/templates/{web/src/personal-center → src/personal-center/web}/__client/api.schemas.ts +0 -0
  299. /package/templates/{web/src/personal-center → src/personal-center/web}/__client/api.ts +0 -0
  300. /package/templates/{web/src/personal-center → src/personal-center/web}/entry.ts +0 -0
  301. /package/templates/{web/src/personal-center → src/personal-center/web}/locales/en-US.json +0 -0
  302. /package/templates/{web/src/personal-center → src/personal-center/web}/locales/zh-CN.json +0 -0
  303. /package/templates/{web/src/personal-center → src/personal-center/web}/pages/my-profile/index.tsx +0 -0
  304. /package/templates/{web/src/route-nest → src/route-nest/web}/entry.ts +0 -0
  305. /package/templates/{web/src/route-nest → src/route-nest/web}/locales/en-US.json +0 -0
  306. /package/templates/{web/src/route-nest → src/route-nest/web}/locales/zh-CN.json +0 -0
  307. /package/templates/{web/src/route-nest → src/route-nest/web}/pages/menu1/menu1-1/index.tsx +0 -0
  308. /package/templates/{web/src/route-nest → src/route-nest/web}/pages/menu1/menu1-2/index.tsx +0 -0
  309. /package/templates/{web/src/route-nest → src/route-nest/web}/pages/menu2/index.tsx +0 -0
  310. /package/templates/{web/src → src}/styles.css +0 -0
  311. /package/templates/{web/src/system → src/system/web}/__client/api.schemas.ts +0 -0
  312. /package/templates/{web/src/system → src/system/web}/__client/api.ts +0 -0
  313. /package/templates/{web/src/system → src/system/web}/entry.ts +0 -0
  314. /package/templates/{web/src/system → src/system/web}/locales/en-US.json +0 -0
  315. /package/templates/{web/src/system → src/system/web}/locales/zh-CN.json +0 -0
  316. /package/templates/{web/src/system → src/system/web}/pages/errors.ts +0 -0
  317. /package/templates/{web/src/system → src/system/web}/pages/forbidden.tsx +0 -0
  318. /package/templates/{web/src/system → src/system/web}/pages/login-methods/index.tsx +0 -0
  319. /package/templates/{web/src/system → src/system/web}/pages/oplog/index.tsx +0 -0
  320. /package/templates/{web/src/system → src/system/web}/pages/roles/index.tsx +0 -0
  321. /package/templates/{web/src/system → src/system/web}/pages/settings/index.tsx +0 -0
  322. /package/templates/{web/src/system → src/system/web}/pages/users/index.tsx +0 -0
  323. /package/templates/{web/src/system → src/system/web}/pages/users/roles.tsx +0 -0
  324. /package/templates/{api/src/web → src/web/api}/get-async-routes/api.ts +0 -0
  325. /package/templates/{api/src/web → src/web/api}/hello/api.ts +0 -0
  326. /package/templates/{api/src/web → src/web/api}/manifest.yaml +0 -0
  327. /package/templates/{api/src/web → src/web/api}/user-info/api.ts +0 -0
  328. /package/templates/{web/src → src/web}/web/__client/api.schemas.ts +0 -0
  329. /package/templates/{web/src → src/web}/web/__client/api.ts +0 -0
  330. /package/templates/{api/tests → tests}/l1-builder.test.ts +0 -0
  331. /package/templates/{api/tests → tests}/rbac-tables.spec.ts +0 -0
package/src/init.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * `ojm init` —— 前后端一体化工程脚手架(设计 §3,D1/D3/D6/D10)。
3
3
  *
4
- * 产物:api/(oj 后端 + seed)、config/(config.yaml + 现场签发证书,与 api/、web/ 同级)、
5
- * web/src/(前端模块)、bin/(从 everpan/only-js 最新 release 联网下载:oj + plugins + devkit)、
6
- * 工程配置文件。
4
+ * 产物(布局 `src/<name>/{api,web}`):src/(模块源码单树)、config.yaml + config/(现场签发证书)、
5
+ * seed.*.sql / tests / vitest.config.ts(后端工程面,与 config.yaml 同级)、
6
+ * bin/(从 everpan/only-js 最新 release 联网下载:oj + plugins + devkit)、工程配置文件。
7
7
  *
8
- * 幂等补缺:对既有工程重跑只补缺失文件(bin/oj 已在则跳过下载),config.yaml 与
9
- * 用户文件永不覆盖——「重新 init 覆盖脚手架」是审阅记录二明令禁止的行为。
8
+ * 幂等补缺:对既有工程重跑只补缺失文件(bin/oj 已在则跳过下载,但先过 §7 能力闸门),
9
+ * config.yaml 与用户文件永不覆盖——「重新 init 覆盖脚手架」是审阅记录二明令禁止的行为。
10
10
  */
11
11
 
12
12
  import { execFileSync } from "node:child_process";
@@ -16,9 +16,11 @@ import path from "node:path";
16
16
  import process from "node:process";
17
17
  import { fileURLToPath } from "node:url";
18
18
  import yaml from "yaml";
19
+ import { ALIAS_PATHS_FILE, syncAliasPaths } from "./alias-paths";
19
20
  import { resolveCertDir, resolveConfigPath } from "./layout";
21
+ import { probeOjCaps } from "./oj-caps";
20
22
  import { mintDevCert } from "./oj-cert";
21
- import { resolveOjBin, vendorCommand } from "./vendor";
23
+ import { isSupportedOjVersion, MIN_OJ_VERSION, resolveOjBin, vendorCommand } from "./vendor";
22
24
  import { readHostVersions } from "./versions";
23
25
 
24
26
  export interface InitOptions {
@@ -33,6 +35,7 @@ export interface InitOptions {
33
35
  interface InitReport {
34
36
  created: string[]
35
37
  skipped: string[]
38
+ warnings: string[]
36
39
  }
37
40
 
38
41
  /**
@@ -41,6 +44,59 @@ interface InitReport {
41
44
  */
42
45
  export type Sealer = (destDir: string, plainSecret: string) => Promise<string | null>;
43
46
 
47
+ /**
48
+ * 既有 `bin/oj` 的能力预检(设计 §7「双探」;评审 BLOCKING #8)。
49
+ *
50
+ * 只查**已存在**的二进制:刚由 `vendorCommand` 装下的那份已被 `MIN_OJ_VERSION` 挡过一遍,
51
+ * 而「从别处拷来的旧二进制」正是这条闸门要拦的形态。
52
+ *
53
+ * 三档处置:
54
+ * - `-V` 读不出版本(桩/损坏二进制)→ 告警放行,不阻断幂等补缺;
55
+ * - 版本 < MIN_OJ_VERSION → 抛错(`ojm vendor --force` 可覆盖标记);
56
+ * - 版本够但 `--module-root` 缺失 → 抛错,这是「声明新、能力旧」的真实形态。
57
+ */
58
+ function assertExistingOjUsable(destDir: string, report: InitReport): void {
59
+ const ojBin = resolveOjBin(destDir);
60
+ if (!fs.existsSync(ojBin))
61
+ return;
62
+
63
+ let printed: string;
64
+ try {
65
+ printed = execFileSync(ojBin, ["-V"], { encoding: "utf-8", timeout: 10_000 }).trim();
66
+ }
67
+ catch (error) {
68
+ report.warnings.push(`无法执行 ${ojBin} -V(${(error as Error).message}),跳过版本校验`);
69
+ return;
70
+ }
71
+
72
+ const version = /(\d+\.\d+\.\d+)/.exec(printed)?.[1];
73
+ if (!version) {
74
+ report.warnings.push(`无法解析 ${ojBin} -V 的输出(${printed || "空"}),跳过版本校验`);
75
+ return;
76
+ }
77
+ if (!isSupportedOjVersion(version)) {
78
+ throw new Error(
79
+ `[ojm] ${ojBin} 是 v${version},低于 ojm 要求的最低版本 v${MIN_OJ_VERSION}。\n`
80
+ + "原因:init 不再对已存在的 bin/oj 静默跳过——旧二进制生成的新布局工程会在 `ojm build` 时才失败。\n"
81
+ + `处置:\`ojm vendor --force v${MIN_OJ_VERSION}\` 重装,或删除 ${ojBin} 后重跑 init。`,
82
+ );
83
+ }
84
+
85
+ try {
86
+ if (probeOjCaps(ojBin).moduleRoot)
87
+ return;
88
+ }
89
+ catch (error) {
90
+ report.warnings.push(`能力探针失败(${(error as Error).message}),跳过 --module-root 校验`);
91
+ return;
92
+ }
93
+ throw new Error(
94
+ `[ojm] ${ojBin}(v${version})不接受 --module-root,无法支撑 src/<name>/{api,web} 布局。\n`
95
+ + "原因:版本号是声明,能跑通才是事实——`oj build --help` 里没有该旗标。\n"
96
+ + `处置:\`ojm vendor --force v${MIN_OJ_VERSION}\` 重装。`,
97
+ );
98
+ }
99
+
44
100
  export async function initProject(destDir: string, opts: InitOptions = {}): Promise<void> {
45
101
  fs.mkdirSync(destDir, { recursive: true });
46
102
  const existing = fs.readdirSync(destDir);
@@ -51,9 +107,13 @@ export async function initProject(destDir: string, opts: InitOptions = {}): Prom
51
107
  );
52
108
  }
53
109
 
54
- const report: InitReport = { created: [], skipped: [] };
110
+ const report: InitReport = { created: [], skipped: [], warnings: [] };
55
111
  const cliRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
56
112
 
113
+ // 0) 已有 bin/oj 先过能力闸门(评审 BLOCKING #8):早前「bin/oj 存在即跳过」
114
+ // 会让从旧工程带来的 0.1.84 静默过关,等 `ojm build` 才死在 --module-root 不认。
115
+ assertExistingOjUsable(destDir, report);
116
+
57
117
  // 1) 模板(占位符替换 + 幂等跳过)
58
118
  const templatesDir = path.join(cliRoot, "templates");
59
119
  const jwtSecret = crypto.randomBytes(32).toString("hex");
@@ -65,14 +125,21 @@ export async function initProject(destDir: string, opts: InitOptions = {}): Prom
65
125
  }, report, include, templatesDir);
66
126
  pruneUnincludedApiPaths(destDir, include, report);
67
127
 
68
- // 2) 证书三件套(现场签发,D6)——落点即 resolveCertDir()(config.yaml 同级的 config/)
128
+ // 1b) tsconfig.paths.json:把 oj 别名 `#/<模块名>/…` 投影成 TS 的 paths。
129
+ // 必须**现场生成**而不是随模板拷贝——模板兼作 demo 工程(多出 demo/home 两个
130
+ // 模块),而用户工程只有白名单里的那些;照拷会让 paths 指向不存在的目录。
131
+ if (syncAliasPaths(destDir) === "created")
132
+ report.created.push(ALIAS_PATHS_FILE);
133
+
134
+ // 2) 证书三件套(现场签发,D6)——落点即 resolveCertDir()(config.yaml 同级的 config/;
135
+ // 新布局 config.yaml 在工程根,故为 <工程根>/config/)
69
136
  const certDir = resolveCertDir(destDir);
70
137
  if (fs.existsSync(path.join(certDir, "cert.jws"))) {
71
- report.skipped.push("api/config/cert.jws");
138
+ report.skipped.push("config/cert.jws");
72
139
  }
73
140
  else {
74
141
  await mintDevCert(certDir);
75
- report.created.push("api/config/{private.pem,public.pem,cert.jws}");
142
+ report.created.push("config/{private.pem,public.pem,cert.jws}");
76
143
  }
77
144
 
78
145
  // 3) oj 二进制:从最新 release 联网下载(sha256 校验 → tar 解包 → chmod),
@@ -164,6 +231,8 @@ export async function initProject(destDir: string, opts: InitOptions = {}): Prom
164
231
  console.log(`[ojm] 新增 ${report.created.length} 项,跳过(已存在)${report.skipped.length} 项`);
165
232
  for (const item of report.skipped)
166
233
  console.log(`[ojm] · 跳过 ${item}`);
234
+ for (const item of report.warnings)
235
+ console.warn(`[ojm] ! ${item}`);
167
236
  console.log("[ojm] 下一步:pnpm install && pnpm dev(登录 admin / 123456)");
168
237
  }
169
238
 
@@ -213,7 +282,7 @@ async function sealJwtSecret(destDir: string, report: InitReport, seal: Sealer =
213
282
  // secrets 段:私钥只放部署机,路径相对 config 目录(config.yaml 同级)
214
283
  const withSecrets = `${next.trimEnd()}\nsecrets:\n private_key_path: ./config/secrets-private.pem\n public_key_path: ./config/secrets-public.pem\n`;
215
284
  fs.writeFileSync(configPath, withSecrets);
216
- report.created.push("api/config/{secrets-private.pem,secrets-public.pem} + auth.jwt_secret 已密封(ENC[...])");
285
+ report.created.push("config/{secrets-private.pem,secrets-public.pem} + auth.jwt_secret 已密封(ENC[...])");
217
286
  }
218
287
 
219
288
  /**
@@ -318,9 +387,10 @@ function pruneUnincludedApiPaths(destDir: string, include: Set<string>, report:
318
387
  // 只裁本次新建的 openapi.json:init 是幂等补缺,不改动既有工程文件
319
388
  if (!report.created.includes("openapi.json"))
320
389
  return;
390
+ // 白名单里 `src/<name>/api` 这条(整模块收录)决定「脚手架有哪些后端模块」
321
391
  const mods = new Set([...include]
322
- .map(p => (p.startsWith("api/src/") ? p.slice("api/src/".length) : null))
323
- .filter((m): m is string => !!m && !m.includes("/")));
392
+ .map(p => /^src\/([^/]+)\/api$/.exec(p)?.[1])
393
+ .filter((m): m is string => !!m));
324
394
  if (mods.size === 0)
325
395
  return;
326
396
  const openapiPath = path.join(destDir, "openapi.json");
@@ -401,15 +471,15 @@ node_modules/
401
471
  bin/
402
472
  .ojm-tmp-*
403
473
  # ojm preview 的派生 config(临时产物,进程退出即删;见 preview-config.ts)
404
- api/.ojm-preview.yaml
405
- # 本地 sqlite 与运行日志
406
- api/db.sqlite
407
- api/db.sqlite-shm
408
- api/db.sqlite-wal
409
- api/logs/
474
+ .ojm-preview.yaml
475
+ # 本地 sqlite 与运行日志(config.yaml 在工程根,路径都相对工程根)
476
+ db.sqlite
477
+ db.sqlite-shm
478
+ db.sqlite-wal
479
+ logs/
410
480
  # 现场签发的开发证书与凭据私钥(ojm dev 缺证时自动签发,见 oj-cert.ts);私钥绝不入仓
411
- api/config/*.pem
412
- api/config/cert.jws
481
+ config/*.pem
482
+ config/cert.jws
413
483
  # 构建产物
414
484
  api/dist/
415
485
  web/dist/
package/src/layout.ts CHANGED
@@ -1,13 +1,24 @@
1
1
  /**
2
- * 模块工程布局探测(设计 D11,2026-09-11 硬切换)。
2
+ * 模块工程布局探测(设计 D11,2026-09-11 硬切换;2026-10 增补 module-root)。
3
3
  *
4
4
  * dev/build/preview 对「源码目录、产物目录、watch 目录」的全部路径取值
5
5
  * 一律经此模块,禁止各自手拼——布局分叉点收敛一处。
6
6
  *
7
- * 布局(uni-dev):web/src/ + web/dist/(src/dist 同级配对)。
8
- * 框架根仓自身为平铺 web/<name>/(dogfooding,产物落 build/,不经 ojm)。
9
- * 硬切换只认 web/:存量 modules/ 工程不再探测,须迁移
10
- * (modules/ → web/,modules.config.ts → web.config.ts)。
7
+ * 三种布局(互斥):
8
+ * module-root <project>/src/<name>/{api,web} 新布局:一目录一模块(设计 v2)
9
+ * new <project>/web/src/<name> 历史 uni-dev 布局
10
+ * flat <project>/web/<name> 框架根仓 dogfooding(产物落 build/)
11
+ *
12
+ * module-root 的判定只认「模块根标记文件」而不是单纯的 src/ 存在性:工程里
13
+ * 常有不属于模块的 src/(脚本、共享库),凭目录名猜会误判。
14
+ * `src/<x>/api/manifest.yaml` 与 `src/<x>/web/entry.ts` 是唯一真源(设计 C-6:
15
+ * 一侧可缺省,两侧皆无则不是模块)。
16
+ *
17
+ * 产物根在两套布局里语义相同、位置不变:
18
+ * api/dist oj 扁平 release 根(<name>-<version>/ 同层兄弟 + manifests.yaml)
19
+ * web/dist 站点产物(shell + modules/ + modules.json)
20
+ * 产物按模块聚合已被设计 §2.2 否决(实证见该节),禁止在此引入第二套形状。
21
+ *
11
22
  * watch 目标绝不含产物目录:若 watch `web/`,重建产物(web/dist)
12
23
  * 会反过来触发重建,形成自触发循环(设计 §4)。
13
24
  */
@@ -15,40 +26,270 @@
15
26
  import fs from "node:fs";
16
27
  import path from "node:path";
17
28
 
29
+ export type LayoutKind = "module-root" | "new" | "flat";
30
+
31
+ /** 模块一侧的可缺省组合:两侧皆无不是模块(设计 C-6) */
32
+ export interface ModuleSides {
33
+ name: string
34
+ /** 后端源码目录 `<project>/src/<name>/api`(无此侧为 undefined) */
35
+ apiSrc?: string
36
+ /** 前端源码目录 `<project>/src/<name>/web`(无此侧为 undefined) */
37
+ webSrc?: string
38
+ }
39
+
18
40
  export interface ProjectLayout {
19
- kind: "new" | "flat"
20
- /** 模块源码目录(watch 与 build 的 entry 来源) */
41
+ kind: LayoutKind
42
+ projectRoot: string
43
+ /** 模块源码目录:module-root → `<project>/src`;new → `<project>/web/src`;flat → `<project>/web` */
21
44
  modulesSrc: string
22
- /** 产物目录:dev 写 modules.json + modules/;ojm build 合并全站于此 */
45
+ /** 站点产物根:module-root / new → `<project>/web/dist`;flat → `<project>/build` */
23
46
  distDir: string
47
+ /** oj 扁平 release 根:恒为 `<project>/api/dist`(设计 C-3/C-7,不随布局变化) */
48
+ apiDist: string
24
49
  /** fs.watch 目标(纯源码,永不落产物) */
25
50
  watchTarget: string
51
+ /**
52
+ * oj 的 `--module-root` 取值(源码树根目录),仅 module-root 有值。
53
+ *
54
+ * v0.1.85 语义:在**该目录的子树内递归**找 `manifest.yaml`,命中即停;模块身份取
55
+ * manifest 的 `name`(目录名只作定位)。它与 `-d`/`--api-path` **互斥**——两者都是
56
+ * 「源码树根」,同时给会被 clap 拒掉,所以调用方是二选一,不是「-d + 附加旗标」。
57
+ */
58
+ moduleRoot?: string
26
59
  }
27
60
 
28
- export function resolveLayout(projectRoot: string): ProjectLayout {
29
- if (fs.existsSync(path.join(projectRoot, "web/src"))) {
61
+ function isDir(p: string): boolean {
62
+ try {
63
+ return fs.statSync(p).isDirectory();
64
+ }
65
+ catch {
66
+ return false;
67
+ }
68
+ }
69
+
70
+ function isFile(p: string): boolean {
71
+ try {
72
+ return fs.statSync(p).isFile();
73
+ }
74
+ catch {
75
+ return false;
76
+ }
77
+ }
78
+
79
+ /**
80
+ * 扫描 module-root 布局的 `src/`:返回模块清单与「像模块但缺标记文件」的坏目录。
81
+ *
82
+ * 坏目录只在 module-root 被判定为当前布局后才报错(见 resolveLayout),
83
+ * 否则一个恰好带 `src/utils/api/` 的历史工程会被误伤。
84
+ */
85
+ function scanModuleRoot(projectRoot: string): { modules: ModuleSides[], broken: string[] } | null {
86
+ const srcDir = path.join(projectRoot, "src");
87
+ if (!isDir(srcDir))
88
+ return null;
89
+
90
+ const modules: ModuleSides[] = [];
91
+ const broken: string[] = [];
92
+ // 按名字排序:readdir 的顺序取决于文件系统与创建历史(同一份源码在不同机器上可能不同),
93
+ // 而枚举顺序会进入构建顺序与产物内容——不排序则「同布局两次构建 sha256 相等」无法保证。
94
+ const entries = fs.readdirSync(srcDir, { withFileTypes: true })
95
+ .filter(ent => ent.isDirectory() && !ent.name.startsWith("."))
96
+ .sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
97
+ for (const ent of entries) {
98
+ const dir = path.join(srcDir, ent.name);
99
+ const apiDir = path.join(dir, "api");
100
+ const webDir = path.join(dir, "web");
101
+ const hasApi = isFile(path.join(apiDir, "manifest.yaml"));
102
+ const hasWeb = isFile(path.join(webDir, "entry.ts"));
103
+ if (!hasApi && !hasWeb) {
104
+ // 只把「有 api/ 或 web/ 子目录但缺标记」算坏目录;纯共享库目录(无两者)静默跳过
105
+ if (isDir(apiDir) || isDir(webDir))
106
+ broken.push(ent.name);
107
+ continue;
108
+ }
109
+ modules.push({
110
+ name: ent.name,
111
+ apiSrc: hasApi ? apiDir : undefined,
112
+ webSrc: hasWeb ? webDir : undefined,
113
+ });
114
+ }
115
+ return { modules, broken };
116
+ }
117
+
118
+ /** 历史布局:`web/src` 为 uni-dev 工程;`web/<name>/entry.ts` 为框架根仓 */
119
+ function detectLegacyKind(projectRoot: string): LayoutKind | null {
120
+ if (isDir(path.join(projectRoot, "web", "src")))
121
+ return "new";
122
+ const webDir = path.join(projectRoot, "web");
123
+ if (isDir(webDir)) {
124
+ for (const ent of fs.readdirSync(webDir, { withFileTypes: true })) {
125
+ if (ent.isDirectory() && isFile(path.join(webDir, ent.name, "entry.ts")))
126
+ return "flat";
127
+ }
128
+ }
129
+ return null;
130
+ }
131
+
132
+ /**
133
+ * 探测布局;工程里没有任何模块源码时返回 null(供 `buildBackend` 这类「没有源码就
134
+ * 跳过」的调用方使用)。布局存在但**自相矛盾**(双树并存 / src 下有坏模块目录)仍抛错。
135
+ */
136
+ export function tryResolveLayout(projectRoot: string): ProjectLayout | null {
137
+ const scanned = scanModuleRoot(projectRoot);
138
+ const isModuleRoot = !!scanned && scanned.modules.length > 0;
139
+ const legacyKind = detectLegacyKind(projectRoot);
140
+
141
+ // 互斥:两套源码树同时存在时静默选一个,会让 `ojm build` 构建 A 而 `ojm dev` 预览 B
142
+ if (isModuleRoot && legacyKind) {
143
+ throw new Error(
144
+ `[ojm] ${projectRoot} 同时存在两套模块源码树:src/<name>/{api,web} 与 `
145
+ + `${legacyKind === "new" ? "web/src/<name>" : "web/<name>"}。\n`
146
+ + "布局互斥,无法判断以哪套为准——请显式移除其中一套后重试。",
147
+ );
148
+ }
149
+
150
+ if (scanned && (isModuleRoot || legacyKind === null) && scanned.broken.length > 0) {
151
+ throw new Error(
152
+ `[ojm] ${projectRoot}/src 下这些目录既无 api/manifest.yaml 也无 web/entry.ts:\n${
153
+ scanned.broken.map(n => ` - src/${n}/`).join("\n")}\n`
154
+ + "module-root 布局里一个目录 = 一个模块,至少需要其中一侧;"
155
+ + "若这些目录不是模块(脚本、共享库),请移到 src/ 之外。",
156
+ );
157
+ }
158
+
159
+ if (isModuleRoot) {
160
+ return {
161
+ kind: "module-root",
162
+ projectRoot,
163
+ modulesSrc: path.join(projectRoot, "src"),
164
+ distDir: path.join(projectRoot, "web", "dist"),
165
+ apiDist: path.join(projectRoot, "api", "dist"),
166
+ watchTarget: path.join(projectRoot, "src"),
167
+ moduleRoot: path.join(projectRoot, "src"),
168
+ };
169
+ }
170
+
171
+ if (legacyKind === "new") {
30
172
  return {
31
173
  kind: "new",
32
- modulesSrc: path.join(projectRoot, "web/src"),
33
- distDir: path.join(projectRoot, "web/dist"),
34
- watchTarget: path.join(projectRoot, "web/src"),
174
+ projectRoot,
175
+ modulesSrc: path.join(projectRoot, "web", "src"),
176
+ distDir: path.join(projectRoot, "web", "dist"),
177
+ apiDist: path.join(projectRoot, "api", "dist"),
178
+ watchTarget: path.join(projectRoot, "web", "src"),
35
179
  };
36
180
  }
181
+
37
182
  // 框架根仓:平铺 web/<name>/(readModuleDefinition 的裸说明符扫描复用此探测,B10)
38
- if (fs.existsSync(path.join(projectRoot, "web"))) {
183
+ if (legacyKind === "flat") {
39
184
  return {
40
185
  kind: "flat",
186
+ projectRoot,
41
187
  modulesSrc: path.join(projectRoot, "web"),
42
188
  distDir: path.join(projectRoot, "build"),
189
+ apiDist: path.join(projectRoot, "api", "dist"),
43
190
  watchTarget: path.join(projectRoot, "web"),
44
191
  };
45
192
  }
193
+
194
+ return null;
195
+ }
196
+
197
+ export function resolveLayout(projectRoot: string): ProjectLayout {
198
+ const layout = tryResolveLayout(projectRoot);
199
+ if (layout)
200
+ return layout;
46
201
  throw new Error(
47
- `[ojm] ${projectRoot} 下未发现 web/——web 布局硬切换后只认 web/src + web/dist,`
202
+ `[ojm] ${projectRoot} 下未发现模块源码(硬切换后只认 web/src;2026-10 起也认 src/<name>/{api,web}):\n`
203
+ + " - 新布局:src/<name>/{api/manifest.yaml 或 web/entry.ts}\n"
204
+ + " - 历史布局:web/src/<name>(uni-dev)或 web/<name>/entry.ts(框架根仓)\n"
48
205
  + "存量 modules/ 工程请迁移:modules/ → web/,modules.config.ts → web.config.ts。",
49
206
  );
50
207
  }
51
208
 
209
+ /**
210
+ * 枚举 module-root 布局的模块(设计 C-6:真源是标记文件,不是人工登记的 web.config.ts)。
211
+ * 历史布局的模块顺序由 `web.config.ts` 的 `modules` 决定,此处不适用。
212
+ */
213
+ export function scanModules(layout: ProjectLayout): ModuleSides[] {
214
+ if (layout.kind !== "module-root")
215
+ throw new Error(`[ojm] scanModules 只适用于 module-root 布局,当前为 ${layout.kind}`);
216
+ return scanModuleRoot(layout.projectRoot)?.modules ?? [];
217
+ }
218
+
219
+ /** 契约工具的处理单元:模块名 + 该模块 api 侧目录(handler 目录镜像的根) */
220
+ export interface ApiModuleRoot {
221
+ name: string
222
+ dir: string
223
+ }
224
+
225
+ /**
226
+ * 历史布局的模块 api 根:`<project>/api/src/<name>`。
227
+ *
228
+ * 单独导出是因为**纯后端工程**可能只有 `api/src` 而没有 `web/src`——`tryResolveLayout`
229
+ * 会返回 null,契约工具仍要能对账。
230
+ */
231
+ export function legacyApiModuleRoots(projectRoot: string): ApiModuleRoot[] {
232
+ const root = path.join(projectRoot, "api", "src");
233
+ if (!isDir(root))
234
+ return [];
235
+ return fs.readdirSync(root, { withFileTypes: true })
236
+ .filter(entry => entry.isDirectory() && !entry.name.startsWith("."))
237
+ .sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))
238
+ .map(entry => ({ name: entry.name, dir: path.join(root, entry.name) }));
239
+ }
240
+
241
+ /**
242
+ * 枚举后端模块的 api 根,供 `ojm api` 生成 / `--check` 对账**按模块**扫描。
243
+ *
244
+ * 为什么不直接 walk `apiModulesSrc(layout)`:module-root 布局的树根是 `src`,模块的
245
+ * api 侧在 `<src>/<name>/api`,中间那层 `api` 是**布局分隔符、不是路由段**。拿 `src`
246
+ * 当扫描根会双重出错——路由镜像多出 `api` 段(`system/api/users` ≠ 真实 `system/users`),
247
+ * 且 `src/<name>/web/__client/api.ts` 这种前端生成物也会被当 handler 扫进来。
248
+ *
249
+ * module-root → 按 `<name>/api/manifest.yaml` 判模块(与 build 同源,name 取 manifest);
250
+ * 历史布局 → `<project>/api/src` 的一级目录即模块。
251
+ */
252
+ export function apiModuleRoots(layout: ProjectLayout): ApiModuleRoot[] {
253
+ if (layout.kind !== "module-root")
254
+ return legacyApiModuleRoots(layout.projectRoot);
255
+ return scanModules(layout)
256
+ .filter((m): m is ModuleSides & { apiSrc: string } => !!m.apiSrc)
257
+ .map(m => ({ name: m.name, dir: m.apiSrc }));
258
+ }
259
+
260
+ /** `<project>/src/<name>` */
261
+ export function moduleDir(layout: ProjectLayout, name: string): string {
262
+ return path.join(layout.modulesSrc, name);
263
+ }
264
+
265
+ /**
266
+ * 后端源码根。module-root 为 `<modulesSrc>/<name>/api`(模块的 api 侧目录);
267
+ * 历史布局为 `<project>/api/src/<name>`——不在 modulesSrc 之下,必须走 projectRoot。
268
+ */
269
+ export function apiSrcOf(layout: ProjectLayout, name: string): string {
270
+ return layout.kind === "module-root"
271
+ ? path.join(layout.modulesSrc, name, "api")
272
+ : path.join(layout.projectRoot, "api", "src", name);
273
+ }
274
+
275
+ /**
276
+ * 后端源码的**根**目录:
277
+ * module-root → `<project>/src`(oj 的 `--module-root` 取源码树根,模块在每层 api 子目录)
278
+ * 历史布局 → `<project>/api/src`(oj 的 `-d` / `--api-path` 取模块目录的父级)
279
+ */
280
+ export function apiModulesSrc(layout: ProjectLayout): string {
281
+ return layout.kind === "module-root"
282
+ ? layout.modulesSrc
283
+ : path.join(layout.projectRoot, "api", "src");
284
+ }
285
+
286
+ /** 前端源码目录(历史布局与模块目录同体) */
287
+ export function webSrcOf(layout: ProjectLayout, name: string): string {
288
+ return layout.kind === "module-root"
289
+ ? path.join(layout.modulesSrc, name, "web")
290
+ : path.join(layout.modulesSrc, name);
291
+ }
292
+
52
293
  export function resolveWatchTarget(projectRoot: string): string {
53
294
  return resolveLayout(projectRoot).watchTarget;
54
295
  }
@@ -56,21 +297,38 @@ export function resolveWatchTarget(projectRoot: string): string {
56
297
  /**
57
298
  * 定位工程 config.yaml。
58
299
  *
59
- * 唯一规范位置:`<project>/api/config.yaml`。
300
+ * 唯一规范位置由布局决定:
301
+ * module-root → `<project>/config.yaml`
302
+ * 历史布局 → `<project>/api/config.yaml`
60
303
  *
61
- * 不能挪到与 api/ 同级的 config/ 目录:oj 的模块解析把「项目根」锚定在
62
- * **config 文件所在目录**,且相对 import 被钳制在该根内(devkit 手册第 5 章)。
63
- * config 一进 `config/` 子目录,`api/src` 就整体落到项目根之外,凡是带 `../`
64
- * 相对 import 的 handler(模板的 system/auth/_platform 依赖 `_shared/`)在
65
- * `ojm dev` 下会被判 `escapes project root` 而整条路由加载失败
66
- * (实测:26 个 api 文件 → 只剩 8 个,18 条路由被跳过)。
304
+ * module-root 下没有回退到 `api/config.yaml`:oj 把「项目根」锚定在 **config 文件所在目录**,
305
+ * 相对 import 被钳制在该根内(devkit 手册第 5 章)。源码搬到 `src/` 而 config 留在 `api/`
306
+ * 时,project root 收缩成 `<project>/api`,`src/` 整体落到根之外——凡是带 `../` 相对 import
307
+ * 的 handler 在 `ojm dev` 下会被判 `escapes project root` 而整条路由加载失败
308
+ * (实测:26 个 api 文件 → 只剩 8 个,18 条路由被跳过)。回退会把这个陷阱伪装成「能跑」。
67
309
  */
68
310
  export function resolveConfigPath(projectRoot: string): string {
69
- return path.join(projectRoot, "api", "config.yaml");
311
+ const rootConfig = path.join(projectRoot, "config.yaml");
312
+ if (isFile(rootConfig))
313
+ return rootConfig;
314
+ const legacyConfig = path.join(projectRoot, "api", "config.yaml");
315
+ // 源码已是 module-root 但 config 还在 api/ ⇒ 迁移未完成。此时回退会踩上面那个陷阱,
316
+ // 且症状是「部分路由静默消失」,比直接失败更难排查。
317
+ if (isFile(legacyConfig) && (scanModuleRoot(projectRoot)?.modules.length ?? 0) > 0) {
318
+ throw new Error(
319
+ `[ojm] ${projectRoot} 已是 src/<name>/{api,web} 布局,但 config.yaml 仍在 api/ 下。\n`
320
+ + `请移到 ${rootConfig}(证书目录随之变为 ${path.join(projectRoot, "config")})。\n`
321
+ + "oj 以 config 所在目录为 project root:config 留在 api/ 会把 src/ 甩到根外,"
322
+ + "带 ../ 相对 import 的 handler 会被判 escapes project root 而整条路由不加载。",
323
+ );
324
+ }
325
+ return legacyConfig;
70
326
  }
71
327
 
72
328
  /**
73
- * 证书与密钥的唯一落点:`<project>/api/config/`(config.yaml 同级的 config/ 子目录)。
329
+ * 证书与密钥的唯一落点:config.yaml 同级的 `config/` 子目录。
330
+ *
331
+ * module-root → `<project>/config/`;历史布局 → `<project>/api/config/`。
74
332
  *
75
333
  * 三个入口都指向这里,别再各自手拼:
76
334
  * ① `server.public_key_path` → ./config/public.pem(oj 验签公钥)
@@ -2,7 +2,7 @@
2
2
  // 组装契约(对齐 tgz 内布局,契约 §2.9 + P1-9 staging copy 源定版):
3
3
  // <root>/manifest.yaml ← 必填,契约校验 fail-fast 先于打包
4
4
  // api/dist/<name>-<version>/ ← oj build 产物原样 → tgz 内 api/<name>-<version>/(C2-31 分层核对)
5
- // web/dist/modules/ ← 站点构建模块产物 → tgz 内 web/modules/(web.entry 存在性复核)
5
+ // web/dist/modules/<name>/ ← 站点构建模块产物 → tgz 内 web/modules/<name>/(只入包方案自己的模块)
6
6
  // 禁携 modules.json(C2-31 红线);产物确定性(C2-32)由 deterministic-tar 承担,此处断言端到端复现。
7
7
  import { execFileSync } from "node:child_process";
8
8
  import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
@@ -64,6 +64,16 @@ describe("buildBundle · 正路径", () => {
64
64
  expect(names, `缺成员 ${member}`).toContain(`demo-solution-0.1.0/${member}`);
65
65
  });
66
66
 
67
+ it("只入包方案自己的前端模块:模板模块(home/login…)不随包", async () => {
68
+ // 安装器 deployWeb 把包内 web/modules 整树覆盖进目标站,模板产物随包会覆盖宿主同名前端
69
+ const hostWeb = join(project, "web/dist/modules/home/0.1.0");
70
+ mkdirSync(hostWeb, { recursive: true });
71
+ writeFileSync(join(hostWeb, "entry.js"), "export default {};\n");
72
+ const names = tarList((await pack()).tgz);
73
+ expect(names.some(n => n.includes("/web/modules/home/"))).toBe(false);
74
+ expect(names).toContain("demo-solution-0.1.0/web/modules/demo-solution/0.1.0/entry.js");
75
+ });
76
+
67
77
  it("端到端确定性:同一工程两次打包 sha256 相等(C2-32)", async () => {
68
78
  const r1 = await pack();
69
79
  const r2 = await pack();
@@ -116,6 +126,11 @@ describe("buildBundle · 校验先于打包(fail-fast)", () => {
116
126
  await expect(pack()).rejects.toThrow(/web\/dist\/modules/);
117
127
  });
118
128
 
129
+ it("web/dist/modules 在但缺本方案目录 → 可读报错(不整树入包)", async () => {
130
+ rmSync(join(project, "web/dist/modules/demo-solution"), { recursive: true, force: true });
131
+ await expect(pack()).rejects.toThrow(/web\/dist\/modules\/demo-solution\//);
132
+ });
133
+
119
134
  it("c2-12a:api.tables 与 schema.yaml 表集不一致 → 构建期拒绝", async () => {
120
135
  const m = readManifest();
121
136
  m.api.tables = ["note", "ghost"];
@@ -1,4 +1,4 @@
1
- import type { InstallContext } from "../../../templates/api/src/_platform/_lifecycle/steps/preflight";
1
+ import type { InstallContext } from "../../../templates/src/_platform/api/_lifecycle/steps/preflight";
2
2
 
3
3
  // BDD:bundle 级 manifest.yaml 契约校验(P1-11,C2 规则构建列)。
4
4
  // 规则真源:docs/prd/solution/202610171254-lifecycle-manifest-contract.md(C2-01~33 构建列,§4.1 矩阵);
@@ -13,7 +13,7 @@ import { describe, expect, it } from "vitest";
13
13
  import { parse } from "yaml";
14
14
  // 跨端引用(双端一致钉):被测实现同仓分居两运行时(计划 §1「两运行时取舍」),
15
15
  // 测试直接互调,比「两套件各读同一夹具」更强——漂移即本文件红。
16
- import { preflight } from "../../../templates/api/src/_platform/_lifecycle/steps/preflight";
16
+ import { preflight } from "../../../templates/src/_platform/api/_lifecycle/steps/preflight";
17
17
  import { checkLocalesComplete, checkTablesAgainstSchema, parseManifest, validateContract } from "../../manifest-contract";
18
18
 
19
19
  const FIXTURES = join(dirname(fileURLToPath(import.meta.url)), "fixtures");