@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
@@ -0,0 +1,114 @@
1
+ // oj 能力探针 spec:双探的第二探(设计 202610220010 §7)。
2
+ //
3
+ // 失败方式很隐蔽:能力缺失却在构建入口回落到旧布局,会得到「源码搬了、产物还是旧的」
4
+ // 半坏状态。这里钉住三件事:解析只看整词旗标、缓存按二进制指纹失效、缺失即抛且带版本。
5
+ import { mkdirSync, mkdtempSync, readFileSync, statSync, utimesSync, writeFileSync } from "node:fs";
6
+ import { tmpdir } from "node:os";
7
+ import { join } from "node:path";
8
+ import { describe, expect, it } from "vitest";
9
+
10
+ import { CAPS_FILE, probeOjCaps, readCapsCache, requireModuleRootCapability, writeCapsCache } from "../oj-caps";
11
+
12
+ function newProject(): { root: string, ojBin: string } {
13
+ const root = mkdtempSync(join(tmpdir(), "ojm-caps-"));
14
+ mkdirSync(join(root, "bin"), { recursive: true });
15
+ const ojBin = join(root, "bin", "oj");
16
+ writeFileSync(ojBin, "#!/bin/sh\necho fake\n", { mode: 0o755 });
17
+ return { root, ojBin };
18
+ }
19
+
20
+ const HELP_WITH = "Options:\n -d, --dir <DIR> 源码目录\n --module-root <GLOB> 模块根 glob\n";
21
+ const HELP_WITHOUT = "Options:\n -d, --dir <DIR> 源码目录\n";
22
+
23
+ describe("probeOjCaps", () => {
24
+ it("帮助里有 --module-root 即支持", () => {
25
+ const { ojBin } = newProject();
26
+ expect(probeOjCaps(ojBin, () => HELP_WITH).moduleRoot).toBe(true);
27
+ });
28
+
29
+ it("帮助里没有则不支持", () => {
30
+ const { ojBin } = newProject();
31
+ expect(probeOjCaps(ojBin, () => HELP_WITHOUT).moduleRoot).toBe(false);
32
+ });
33
+
34
+ it("同名前缀不算命中(--module-rooted 之类的说明文字)", () => {
35
+ const { ojBin } = newProject();
36
+ expect(probeOjCaps(ojBin, () => "--module-rooted 是别的意思\n").moduleRoot).toBe(false);
37
+ });
38
+
39
+ it("探测本身失败时报错带处置命令", () => {
40
+ const { ojBin } = newProject();
41
+ expect(() => probeOjCaps(ojBin, () => {
42
+ throw new Error("boom");
43
+ })).toThrow(/ojm vendor/);
44
+ });
45
+ });
46
+
47
+ describe("能力缓存", () => {
48
+ it("写入后能读回", () => {
49
+ const { ojBin } = newProject();
50
+ writeCapsCache(ojBin, { moduleRoot: true });
51
+ expect(readCapsCache(ojBin)).toEqual({ moduleRoot: true });
52
+ });
53
+
54
+ it("换二进制(size 变)后缓存失效", () => {
55
+ const { ojBin } = newProject();
56
+ writeCapsCache(ojBin, { moduleRoot: true });
57
+ writeFileSync(ojBin, "#!/bin/sh\necho replaced-and-longer\n", { mode: 0o755 });
58
+ expect(readCapsCache(ojBin)).toBeNull();
59
+ });
60
+
61
+ it("mtime 变(同尺寸手替)后缓存失效", () => {
62
+ const { ojBin } = newProject();
63
+ writeCapsCache(ojBin, { moduleRoot: true });
64
+ const future = new Date(Date.now() + 60_000);
65
+ utimesSync(ojBin, future, future);
66
+ expect(readCapsCache(ojBin)).toBeNull();
67
+ });
68
+
69
+ it("缓存文件损坏不影响主流程(视为未缓存)", () => {
70
+ const { root, ojBin } = newProject();
71
+ writeFileSync(join(root, "bin", CAPS_FILE), "{not json", "utf-8");
72
+ expect(readCapsCache(ojBin)).toBeNull();
73
+ });
74
+ });
75
+
76
+ describe("requireModuleRootCapability", () => {
77
+ it("支持时通过并落缓存,第二次不再探测", () => {
78
+ const { root } = newProject();
79
+ let calls = 0;
80
+ const run = () => {
81
+ calls += 1;
82
+ return HELP_WITH;
83
+ };
84
+ expect(requireModuleRootCapability(root, run).moduleRoot).toBe(true);
85
+ expect(requireModuleRootCapability(root, run).moduleRoot).toBe(true);
86
+ expect(calls).toBe(1);
87
+ expect(readFileSync(join(root, "bin", CAPS_FILE), "utf-8")).toContain("\"moduleRoot\": true");
88
+ });
89
+
90
+ it("二进制缺失时报错指向 ojm vendor", () => {
91
+ const { root } = newProject();
92
+ const run = () => HELP_WITH;
93
+ expect(() => requireModuleRootCapability(join(root, "nope"), run)).toThrow(/ojm vendor/);
94
+ });
95
+
96
+ it("能力缺失时抛错,文案含现场版本与两种处置", () => {
97
+ const { root } = newProject();
98
+ const run = (_bin: string, args: string[]) =>
99
+ args[0] === "-V" ? "oj 0.1.84\n" : HELP_WITHOUT;
100
+ expect(() => requireModuleRootCapability(root, run)).toThrow(/oj 0\.1\.84/);
101
+ expect(() => requireModuleRootCapability(root, run)).toThrow(/ojm vendor/);
102
+ });
103
+
104
+ it("能力缺失也写缓存(避免每次构建重探),换二进制后指纹变化自动重探", () => {
105
+ const { root, ojBin } = newProject();
106
+ expect(() => requireModuleRootCapability(root, (_b, args) => (args[0] === "-V" ? "oj 0.1.84" : HELP_WITHOUT))).toThrow();
107
+ expect(readCapsCache(ojBin)?.moduleRoot).toBe(false);
108
+ // 二进制更新后指纹变化 → 重探,能力补齐即放行
109
+ const before = statSync(ojBin).size;
110
+ writeFileSync(ojBin, "#!/bin/sh\necho upgraded\n", { mode: 0o755 });
111
+ expect(statSync(ojBin).size).not.toBe(before);
112
+ expect(requireModuleRootCapability(root, () => HELP_WITH).moduleRoot).toBe(true);
113
+ });
114
+ });
@@ -0,0 +1,102 @@
1
+ /**
2
+ * oj 原生别名 `#/<模块名>/…` 在 TS 侧的投影(生成 `<project>/tsconfig.paths.json`)。
3
+ *
4
+ * 为什么必须生成而不能手写一条通配:oj 的 `#/<name>/<tail>` 锚定「目标模块根」,在
5
+ * `src/<name>/{api,web}` 布局下即 `src/<name>/api`。tsconfig 的 `paths` 做不了这件事:
6
+ * - 每条规则的替换值只允许一个 `*`——想用单条 `"#/*"` 同时匹配模块名与尾路径,会被
7
+ * TS 以 TS5062 直接拒掉(两处通配只能二选一);
8
+ * - `paths` 是静态全局映射,表达不了 `#x`(本模块根,随引用方变化)。
9
+ * 所以只能**按模块逐条枚举** `"#/<name>/*": ["./src/<name>/api/*"]`。模块清单必须与
10
+ * `src/<name>/api/manifest.yaml` 同源,手写必然漂移 → 由 `ojm init` / `ojm build` / `ojm dev` 生成。
11
+ *
12
+ * 生成物只被 `src/tsconfig.json`(后端)`extends`:前端 tsconfig 不引,因为 web 侧
13
+ * 没有 `#` 解析(`build.ts` 的 esbuild/vite 不认别名),让编辑器「能过」而打包失败
14
+ * 比漏补 path 更坏。
15
+ *
16
+ * 模式串用 manifest 的 `name`(oj 的身份口径,v0.1.85 起与目录名脱钩),替换值用
17
+ * **目录名**——两者在正常工程里同值,但真分叉时只有按此口径才不会一边错位。
18
+ */
19
+ import type { ProjectLayout } from "./layout";
20
+ import fs from "node:fs";
21
+ import path from "node:path";
22
+ import { parse } from "yaml";
23
+ import { tryResolveLayout } from "./layout";
24
+
25
+ /** 生成物文件名(工程根)。`src/tsconfig.json` 以 `../tsconfig.paths.json` 引用 */
26
+ export const ALIAS_PATHS_FILE = "tsconfig.paths.json";
27
+
28
+ /** 生成物头注释:tsconfig 允许注释,且这是唯一会被 `ojm` 覆写的 tsconfig */
29
+ const HEADER = [
30
+ "// 由 ojm 生成:ojm init / ojm build / ojm dev 会按 src/*/api/manifest.yaml 重写本文件,请勿手改。",
31
+ "//",
32
+ "// oj 别名 `#/<模块名>/…` 锚在目标模块根(本布局下即 src/<模块名>/api)。tsconfig 的",
33
+ "// paths 只能静态全局映射,且每条规则的替换值只允许一个 `*`(`./src/*/api/*` 会被 TS",
34
+ "// 以 TS5062 拒掉),故按模块逐条枚举。",
35
+ ].join("\n");
36
+
37
+ /** 读模块根 manifest 的 `name`(oj 的模块身份);读不到/格式坏时回落到目录名 */
38
+ function manifestName(apiDir: string, fallback: string): string {
39
+ try {
40
+ const doc: unknown = parse(fs.readFileSync(path.join(apiDir, "manifest.yaml"), "utf-8"));
41
+ if (doc && typeof doc === "object" && "name" in doc) {
42
+ const name = (doc as { name?: unknown }).name;
43
+ if (typeof name === "string" && name.trim())
44
+ return name.trim();
45
+ }
46
+ }
47
+ catch {}
48
+ return fallback;
49
+ }
50
+
51
+ /**
52
+ * 模块名 → tsconfig `paths` 规则(仅 module-root 布局)。
53
+ *
54
+ * 键序按目录名排序(`scanModules` 已排序),保证同布局两次生成字节一致——
55
+ * 否则「生成物是否过期」的比对会因文件系统枚举顺序抖动而误报。
56
+ */
57
+ export function aliasPathEntries(layout: ProjectLayout): Record<string, string[]> {
58
+ if (layout.kind !== "module-root")
59
+ return {};
60
+ const entries: Record<string, string[]> = {};
61
+ for (const mod of fs.readdirSync(layout.modulesSrc, { withFileTypes: true })) {
62
+ if (!mod.isDirectory() || mod.name.startsWith("."))
63
+ 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", "*")}`];
69
+ }
70
+ return entries;
71
+ }
72
+
73
+ /** 渲染 `tsconfig.paths.json`(含头注释;tsconfig 解析器接受 JSONC) */
74
+ export function renderAliasPaths(layout: ProjectLayout): string {
75
+ const entries = Object.entries(aliasPathEntries(layout));
76
+ const body = entries.length === 0
77
+ ? "\t\t\"paths\": {}"
78
+ : `\t\t"paths": {\n${
79
+ entries
80
+ .map(([pattern, targets]) => `\t\t\t${JSON.stringify(pattern)}: [${targets.map(t => JSON.stringify(t)).join(", ")}]`)
81
+ .join(",\n")
82
+ }\n\t\t}`;
83
+ return `${HEADER}\n{\n\t"compilerOptions": {\n${body}\n\t}\n}\n`;
84
+ }
85
+
86
+ /**
87
+ * 按当前模块清单同步生成物,**内容未变则不写**(避免每次 build 都产生无意义 diff)。
88
+ *
89
+ * 返回 `skipped` 表示不是 module-root 布局(历史布局的别名口径不同,不在此生成)。
90
+ */
91
+ export function syncAliasPaths(projectRoot: string): "created" | "updated" | "unchanged" | "skipped" {
92
+ const layout = tryResolveLayout(projectRoot);
93
+ if (layout?.kind !== "module-root")
94
+ return "skipped";
95
+ const file = path.join(projectRoot, ALIAS_PATHS_FILE);
96
+ const next = renderAliasPaths(layout);
97
+ const prev = fs.existsSync(file) ? fs.readFileSync(file, "utf-8") : null;
98
+ if (prev === next)
99
+ return "unchanged";
100
+ fs.writeFileSync(file, next);
101
+ return prev === null ? "created" : "updated";
102
+ }
package/src/build.ts CHANGED
@@ -10,8 +10,10 @@ import { pathToFileURL } from "node:url";
10
10
  import tailwindcss from "@tailwindcss/vite";
11
11
  import { build as esbuild } from "esbuild";
12
12
  import { build } from "vite";
13
- import { loadModulesConfig, resolveModuleEntry } from "./config";
14
- import { resolveConfigPath, resolveLayout } from "./layout";
13
+ import { ALIAS_PATHS_FILE, syncAliasPaths } from "./alias-paths";
14
+ import { assertModuleList, loadSiteConfig, mergeModuleList, resolveModuleEntry } from "./config";
15
+ import { resolveConfigPath, resolveLayout, scanModules, tryResolveLayout } from "./layout";
16
+ import { requireModuleRootCapability } from "./oj-caps";
15
17
  import { isSharedDep, SHARED_DEPS } from "./shared-deps";
16
18
  import { normalizeSiteBase, rewriteIndexHtml } from "./site-base";
17
19
  import { resolveOjBin } from "./vendor";
@@ -494,18 +496,33 @@ function assertResolvableSpecifiers(
494
496
  * 无后端源码返回 false(纯前端工程跳过)。
495
497
  */
496
498
  export async function buildBackend(projectRoot: string): Promise<boolean> {
497
- const apiSrc = path.join(projectRoot, "api/src");
499
+ // 没有模块源码的工程(如 market 的隔离站点根)直接跳过——不能因为探测不到布局就抛
500
+ const layout = tryResolveLayout(projectRoot);
501
+ const moduleRoot = layout?.kind === "module-root";
502
+ const apiSrc = moduleRoot
503
+ ? layout.modulesSrc
504
+ : path.join(projectRoot, "api/src");
498
505
  if (!fs.existsSync(apiSrc))
499
506
  return false;
500
507
 
501
508
  const oj = resolveOjBin(projectRoot);
502
509
  if (!fs.existsSync(oj)) {
503
510
  throw new Error(
504
- `[ojm] 找到 api/src 但缺少 ${oj}。\n`
511
+ `[ojm] 找到 ${path.relative(projectRoot, apiSrc) || "."} 但缺少 ${oj}。\n`
505
512
  + "请重跑 ojm init 幂等补缺(不会覆盖 config 与用户代码)。",
506
513
  );
507
514
  }
508
- const apiDist = path.join(projectRoot, "api/dist");
515
+ // 能力闸门先于任何写入:失败时磁盘上不留半成品
516
+ if (moduleRoot)
517
+ requireModuleRootCapability(projectRoot);
518
+
519
+ // 别名投影:模块增删后要让 TS 的 paths 跟上(否则 `#/<模块名>/…` 在编辑器/typecheck
520
+ // 里解析不到,退化成 any)。内容未变不写盘。
521
+ const syncedPaths = syncAliasPaths(projectRoot);
522
+ if (syncedPaths === "created" || syncedPaths === "updated")
523
+ console.log(`[ojm] 已${syncedPaths === "created" ? "生成" : "更新"} ${ALIAS_PATHS_FILE}(\`#/<模块名>/…\` 的 TS paths 投影)`);
524
+
525
+ const apiDist = layout?.apiDist ?? path.join(projectRoot, "api", "dist");
509
526
  console.log("[ojm] oj build(后端)…");
510
527
  // 必须显式给 -c:oj build 的 -c 缺省是 ./config.yaml(相对 CWD),而 config 在
511
528
  // <project>/config/ 下——漏传会静默读不到配置,连带 tasks 镜像目录回落默认 "tasks"、
@@ -518,7 +535,17 @@ export async function buildBackend(projectRoot: string): Promise<boolean> {
518
535
  // 2) `if (cond) x = y` → 压成 `cond && x = y`,赋值作 && 右项缺括号 →
519
536
  // SyntaxError: Invalid left-hand side in assignment(端点 500)。
520
537
  // 0.1.32 / 0.1.50 均复现。产物体积换正确性,等上游修好再撤这个旗标。
521
- execFileSync(oj, ["build", ...configArgs, "-d", apiSrc, "-o", apiDist, "--no-minify"], { stdio: "inherit" });
538
+ // v0.1.85:`--module-root <dir>` 与 `-d` 互斥(都是「源码树根」)。新布局给 src,
539
+ // oj 在子树内递归找 manifest.yaml(命中 src/<name>/api/manifest.yaml 即停)。
540
+ const sourceArgs = layout?.moduleRoot ? ["--module-root", layout.moduleRoot] : ["-d", apiSrc];
541
+ execFileSync(oj, [
542
+ "build",
543
+ ...configArgs,
544
+ ...sourceArgs,
545
+ "-o",
546
+ apiDist,
547
+ "--no-minify",
548
+ ], { stdio: "inherit" });
522
549
  // 宿主 versions.json 双落点:站点壳从 shell-dist 拷进 web/dist(mergeShellSite),
523
550
  // 而方案安装器读的是 `<api>/dist/versions.json`(B-11:与 manifests.yaml 同层,用于
524
551
  // peerRuntime 强制比对)。两处必须同源,否则新工程 `ojm build` 完仍装不上方案——
@@ -569,8 +596,13 @@ export async function buildModules(
569
596
  projectRoot: string,
570
597
  opts: { mergeSite?: boolean } = {},
571
598
  ): Promise<BuiltModule[]> {
572
- const config = await loadModulesConfig(projectRoot);
573
599
  const layout = resolveLayout(projectRoot);
600
+ const config = await loadSiteConfig(projectRoot);
601
+ // module-root:模块清单由 src/*/web/entry.ts 扫描得出(web.config.ts 仅可选覆盖);
602
+ // 历史布局:仍以 web.config.ts 的 modules 为唯一真源。
603
+ const modules = layout.kind === "module-root"
604
+ ? mergeModuleList(scanModules(layout), config.modules, projectRoot)
605
+ : assertModuleList(projectRoot, config.modules);
574
606
  const outDir = layout.distDir;
575
607
  const baseUrl = config.baseUrl ?? "";
576
608
 
@@ -584,7 +616,7 @@ export async function buildModules(
584
616
 
585
617
  const built: BuiltModule[] = [];
586
618
 
587
- for (const item of config.modules) {
619
+ for (const item of modules) {
588
620
  if (item.enabled === false) {
589
621
  console.log(`[ojm] 跳过已禁用模块 ${item.name}`);
590
622
  continue;
@@ -595,7 +627,7 @@ export async function buildModules(
595
627
 
596
628
  if (definition.name !== item.name) {
597
629
  throw new Error(
598
- `模块名不一致:web.config.ts 中为 "${item.name}",entry.ts 中为 "${definition.name}"`,
630
+ `模块名不一致:${layout.kind === "module-root" ? `src/${item.name}` : "web.config.ts"} 中为 "${item.name}",entry.ts 中为 "${definition.name}"`,
599
631
  );
600
632
  }
601
633
 
package/src/bundle.ts CHANGED
@@ -3,16 +3,18 @@
3
3
  // 组装映射(对齐 P1-9 staging copy 源一层定版,unpack 布局即安装器 extract 段消费面):
4
4
  // <project>/manifest.yaml → <name>-<version>/manifest.yaml(Form A)
5
5
  // <project>/api/dist/<name>-<version>/ → <name>-<version>/api/<name>-<version>/(oj build 产物原样,C2-31 分层核对)
6
- // <project>/web/dist/modules/ → <name>-<version>/web/modules/(web.entry/css 存在性复核)
6
+ // <project>/web/dist/modules/<name>/ → <name>-<version>/web/modules/<name>/(只入包方案自己的模块;
7
+ // web.entry/css 存在性复核)
7
8
  // 红线:modules.json 禁入包(C2-31——该清单由安装器在目标站点 upsert,防覆盖宿主清单);
8
9
  // 文件面契约(tables↔schema.yaml、locales 双语齐备)构建期 fail-fast(契约 §4.1 构建列)。
10
+ import type { ProjectLayout } from "./layout";
9
11
  import fs from "node:fs";
10
12
  import path from "node:path";
11
13
 
12
14
  import { parse } from "yaml";
13
15
  import { readModuleDefinition } from "./build";
14
16
  import { createDeterministicTgz } from "./deterministic-tar";
15
- import { resolveLayout } from "./layout";
17
+ import { apiSrcOf, resolveLayout, webSrcOf } from "./layout";
16
18
  import { checkLocalesComplete, checkTablesAgainstSchema, parseManifest, referencedI18nKeys, validateContract } from "./manifest-contract";
17
19
  import { deriveManifestRoutes, reconcileManifestRoutes } from "./manifest-routes-derive";
18
20
 
@@ -46,7 +48,8 @@ export async function buildBundle(projectRoot: string, opts: { out?: string } =
46
48
  // 仅当 entry.ts 声明了 category 路由才覆盖;否则保留 manifest 既有 routes(未迁移方案兼容)。
47
49
  // entry 读不出来(如第三依赖未装)时软降级:沿用既有 routes,不阻断打包。
48
50
  const layout = resolveLayout(projectRoot);
49
- const entryFile = path.join(layout.modulesSrc, name, "entry.ts");
51
+ // 前端入口两侧布局都在 webSrcOf 里收口:module-root → src/<name>/web,历史 → web/src/<name>
52
+ const entryFile = path.join(webSrcOf(layout, name), "entry.ts");
50
53
  if (fs.existsSync(entryFile)) {
51
54
  try {
52
55
  const def = await readModuleDefinition(entryFile, projectRoot);
@@ -72,8 +75,8 @@ export async function buildBundle(projectRoot: string, opts: { out?: string } =
72
75
 
73
76
  // 2) 文件面契约(§4.1 构建列专属执行点;manifest 内在规则已在 validateContract 全量通过)
74
77
  const fileViolations = [
75
- ...checkTables(m, projectRoot, name),
76
- ...checkLocales(m, projectRoot, name),
78
+ ...checkTables(m, layout, name),
79
+ ...checkLocales(m, layout, name),
77
80
  ];
78
81
  if (fileViolations.length > 0) {
79
82
  throw new Error(`[ojm] 方案包构建失败:文件面契约未通过(${fileViolations.length} 项,全量列出):\n${fileViolations.map(x => `- ${x}`).join("\n")}`);
@@ -109,13 +112,22 @@ export async function buildBundle(projectRoot: string, opts: { out?: string } =
109
112
  fs.cpSync(built, path.join(root, "api", `${name}-${version}`), { recursive: true });
110
113
  }
111
114
 
112
- // web:站点构建模块产物入包(web 段缺省 = 纯后端方案,整段跳过)
115
+ // web:只入包方案自己的模块产物(web 段缺省 = 纯后端方案,整段跳过)。
116
+ // 为何不是整树:web/dist/modules 里除本方案模块外还有脚手架模板模块(home/login/
117
+ // personal-center/notification/system/oj-market…),而安装器 deployWeb 会把包内
118
+ // web/modules 整树覆盖进目标站 <dist>/modules——整树入包等于用开发机上的模板产物
119
+ // 覆盖目标站同名前端。方案包只该带自己那一份,模板模块属宿主面,不由方案分发。
113
120
  if (m.web !== undefined) {
114
121
  const webModules = path.join(projectRoot, "web/dist/modules");
115
122
  if (!fs.existsSync(webModules)) {
116
123
  throw new Error("[ojm] 方案包构建失败:缺少站点构建产物 web/dist/modules(manifest 声明了 web 段;先运行 ojm build 生成模块产物)");
117
124
  }
118
- fs.cpSync(webModules, path.join(root, "web", "modules"), { recursive: true });
125
+ const ownWeb = path.join(webModules, name);
126
+ if (!fs.existsSync(ownWeb)) {
127
+ throw new Error(`[ojm] 方案包构建失败:缺少本方案的站点产物 web/dist/modules/${name}/(方案包只入包方案自己的模块;请确认 web.config.ts 已登记 ${name} 且本次 ojm build 已产出)`);
128
+ }
129
+ fs.mkdirSync(path.join(root, "web", "modules"), { recursive: true });
130
+ fs.cpSync(ownWeb, path.join(root, "web", "modules", name), { recursive: true });
119
131
  const web = m.web as Record<string, unknown>;
120
132
  for (const rel of [web.entry, ...(Array.isArray(web.css) ? web.css : [])]) {
121
133
  if (typeof rel === "string" && !fs.existsSync(path.join(root, "web", rel))) {
@@ -173,28 +185,28 @@ function checkModuleManifest(built: string, name: string, version: string, _summ
173
185
  }
174
186
 
175
187
  /** api.tables ↔ schema.yaml 表集对照(C2-12a 构建列;schema 缺失/声明不一致均 fail-fast) */
176
- function checkTables(m: Record<string, unknown>, projectRoot: string, name: string): string[] {
188
+ function checkTables(m: Record<string, unknown>, layout: ProjectLayout, name: string): string[] {
177
189
  const api = m.api as Record<string, unknown> | undefined;
178
190
  if (api === undefined || api.tables === undefined)
179
191
  return [];
180
- const schemaPath = path.join(projectRoot, "api/src", name, "schema.yaml");
192
+ const schemaPath = path.join(apiSrcOf(layout, name), "schema.yaml");
181
193
  if (!fs.existsSync(schemaPath)) {
182
- return [`【C2-12a】api.tables 已声明但缺少 ${path.relative(projectRoot, schemaPath)},无法对照表集(oj 冗余校验同源)`];
194
+ return [`【C2-12a】api.tables 已声明但缺少 ${path.relative(layout.projectRoot, schemaPath)},无法对照表集(oj 冗余校验同源)`];
183
195
  }
184
196
  return checkTablesAgainstSchema(m, fs.readFileSync(schemaPath, "utf8"));
185
197
  }
186
198
 
187
199
  /** locales 双语齐备(C2-25 构建列缺 key fail-fast;安装期信任构建产物不重复校验) */
188
- function checkLocales(m: Record<string, unknown>, projectRoot: string, name: string): string[] {
200
+ function checkLocales(m: Record<string, unknown>, layout: ProjectLayout, name: string): string[] {
189
201
  if (referencedI18nKeys(m).length === 0)
190
202
  return [];
191
- const dir = path.join(projectRoot, "web/src", name, "locales");
203
+ const dir = path.join(webSrcOf(layout, name), "locales");
192
204
  const texts: Partial<Record<"zh-CN" | "en-US", string>> = {};
193
205
  const out: string[] = [];
194
206
  for (const lang of ["zh-CN", "en-US"] as const) {
195
207
  const p = path.join(dir, `${lang}.json`);
196
208
  if (!fs.existsSync(p)) {
197
- out.push(`【C2-25】manifest 引用了 i18n key 但缺少 ${path.relative(projectRoot, p)}(双语齐备,D4)`);
209
+ out.push(`【C2-25】manifest 引用了 i18n key 但缺少 ${path.relative(layout.projectRoot, p)}(双语齐备,D4)`);
198
210
  continue;
199
211
  }
200
212
  texts[lang] = fs.readFileSync(p, "utf8");
package/src/config.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import fs from "node:fs";
1
2
  import path from "node:path";
2
3
  import { pathToFileURL } from "node:url";
3
4
 
@@ -25,7 +26,7 @@ export interface ModulesConfig {
25
26
  * 绝对域),`siteBase` 是**站点自身**的部署位置;两者正交,可同时使用。
26
27
  */
27
28
  siteBase?: string
28
- modules: ModuleConfigEntry[]
29
+ modules?: ModuleConfigEntry[]
29
30
  /**
30
31
  * 站点 HTML 变换模块(可选):相对工程根的路径(`./html-transform.ts`)
31
32
  * 或包名。`ojm dev` / `ojm preview` 动态 import 它,用其 default 导出
@@ -43,13 +44,18 @@ const DEFAULT_CONFIG: Required<Pick<ModulesConfig, "baseUrl" | "siteBase">> = {
43
44
  };
44
45
 
45
46
  /**
46
- * 加载模块工程的 web.config.ts。
47
+ * 加载模块工程的 web.config.ts(**不校验 modules**)。
47
48
  *
48
- * 用 tsx 真实 import,而不是正则解析——这样配置文件里可以写注释、
49
- * 用变量、做条件判断,且类型错误会直接暴露(B10 的正则方案做不到)。
49
+ * 两种布局的差别只在模块清单从哪来:
50
+ * - module-root:模块由 `src/<name>/web/entry.ts` 扫描得出(一目录一模块),
51
+ * web.config.ts 退化为「站点级配置 + 可选覆盖」(设计 D3);
52
+ * - 历史布局:modules 必填,用 `loadModulesConfig`。
53
+ * 缺文件不是错误(新工程可以没有 web.config.ts),坏文件必须报错。
50
54
  */
51
- export async function loadModulesConfig(projectRoot: string): Promise<ModulesConfig & { baseUrl: string, siteBase: string }> {
55
+ export async function loadSiteConfig(projectRoot: string): Promise<ModulesConfig & { baseUrl: string, siteBase: string }> {
52
56
  const configPath = path.join(projectRoot, "web.config.ts");
57
+ if (!fs.existsSync(configPath))
58
+ return { ...DEFAULT_CONFIG };
53
59
 
54
60
  let mod: { default?: ModulesConfig };
55
61
  try {
@@ -59,12 +65,79 @@ export async function loadModulesConfig(projectRoot: string): Promise<ModulesCon
59
65
  throw new Error(`无法加载 ${configPath}:${(error as Error).message}`, { cause: error });
60
66
  }
61
67
 
62
- const config = mod.default;
63
- if (!config?.modules?.length) {
64
- throw new Error(`${configPath} 未导出有效的 modules 列表`);
68
+ return { ...DEFAULT_CONFIG, ...mod.default };
69
+ }
70
+
71
+ /**
72
+ * 加载模块工程的 web.config.ts(历史布局:modules 必填)。
73
+ *
74
+ * 用 tsx 真实 import,而不是正则解析——这样配置文件里可以写注释、
75
+ * 用变量、做条件判断,且类型错误会直接暴露(B10 的正则方案做不到)。
76
+ */
77
+ export async function loadModulesConfig(projectRoot: string): Promise<ModulesConfig & { baseUrl: string, siteBase: string, modules: ModuleConfigEntry[] }> {
78
+ const config = await loadSiteConfig(projectRoot);
79
+ return { ...config, modules: assertModuleList(projectRoot, config.modules) };
80
+ }
81
+
82
+ /** 历史布局的 modules 必填校验(module-root 布局不走这条) */
83
+ export function assertModuleList(projectRoot: string, modules: ModuleConfigEntry[] | undefined): ModuleConfigEntry[] {
84
+ if (!modules?.length)
85
+ throw new Error(`${path.join(projectRoot, "web.config.ts")} 未导出有效的 modules 列表`);
86
+ return modules;
87
+ }
88
+
89
+ /** 前端模块的最终清单项(entry 为相对工程根的路径,与 web.config.ts 历史口径一致) */
90
+ export interface ResolvedWebModule {
91
+ name: string
92
+ entry: string
93
+ enabled: boolean
94
+ }
95
+
96
+ /**
97
+ * module-root 布局的模块清单:**扫描为准,web.config.ts 只做可选覆盖**(设计 D3)。
98
+ *
99
+ * 规则(并集,不是替换——新增模块目录不会因为忘了登记而被静默漏构建):
100
+ * 1. 来源只有 `src/<name>/web/entry.ts`:纯后端模块没有前端产物,不进入本清单;
101
+ * 2. 覆盖项按声明顺序排在前,其余按模块名字典序追加;
102
+ * 3. `enabled: false` 仍生效(跳过构建);
103
+ * 4. 覆盖项指向不存在的前端入口 → 报错(改名/删目录后忘了改 config 的典型症状)。
104
+ */
105
+ export function mergeModuleList(
106
+ scanned: { name: string, webSrc?: string }[],
107
+ overrides: ModuleConfigEntry[] | undefined,
108
+ projectRoot: string,
109
+ ): ResolvedWebModule[] {
110
+ const web = scanned.filter(m => m.webSrc);
111
+ const byName = new Map(web.map(m => [m.name, m]));
112
+ const ordered: ResolvedWebModule[] = [];
113
+ const seen = new Set<string>();
114
+
115
+ for (const item of overrides ?? []) {
116
+ const hit = byName.get(item.name);
117
+ if (!hit) {
118
+ throw new Error(
119
+ `web.config.ts 登记了模块 "${item.name}",但 src/${item.name}/web/entry.ts 不存在。\n`
120
+ + "module-root 布局以目录为准:删掉这条登记,或补回该模块目录。",
121
+ );
122
+ }
123
+ seen.add(item.name);
124
+ ordered.push({
125
+ name: item.name,
126
+ entry: path.relative(projectRoot, path.join(hit.webSrc!, "entry.ts")),
127
+ enabled: item.enabled !== false,
128
+ });
65
129
  }
66
130
 
67
- return { ...DEFAULT_CONFIG, ...config };
131
+ for (const m of web) {
132
+ if (seen.has(m.name))
133
+ continue;
134
+ ordered.push({
135
+ name: m.name,
136
+ entry: path.relative(projectRoot, path.join(m.webSrc!, "entry.ts")),
137
+ enabled: true,
138
+ });
139
+ }
140
+ return ordered;
68
141
  }
69
142
 
70
143
  /** 解析模块 entry 的绝对路径 */
@@ -9,8 +9,8 @@ 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/web/src/_platform/__client/api";
13
- import { httpMarketClient } from "../../../templates/web/src/oj-market/api/defaultClient";
12
+ import { bindRequest } from "../../../templates/src/_platform/web/__client/api";
13
+ import { httpMarketClient } from "../../../templates/src/oj-market/web/api/defaultClient";
14
14
 
15
15
  // index() 直连 request(不走生成 client),故把桩 request 做成可变实现,测试内统一指向同一 mock。
16
16
  const { requestImpl } = vi.hoisted(() => ({