@oj-module/cli 0.2.18 → 0.2.23

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 (345) 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 +352 -168
  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 +59 -34
  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__/init-packagejson.test.ts +29 -0
  14. package/src/__tests__/layout.test.ts +176 -0
  15. package/src/__tests__/module-list.test.ts +66 -0
  16. package/src/__tests__/oj-caps.test.ts +114 -0
  17. package/src/alias-paths.ts +102 -0
  18. package/src/args.ts +54 -4
  19. package/src/build.ts +41 -9
  20. package/src/builtin.ts +93 -0
  21. package/src/bundle.ts +254 -38
  22. package/src/config.ts +82 -9
  23. package/src/contract/__tests__/market-client.integration.test.ts +14 -14
  24. package/src/contract/check.ts +56 -28
  25. package/src/contract/emit-stub.ts +21 -9
  26. package/src/contract/handler-scan.ts +19 -10
  27. package/src/contract/run.ts +14 -4
  28. package/src/dev.ts +80 -9
  29. package/src/index.ts +27 -7
  30. package/src/info.ts +69 -10
  31. package/src/init.ts +114 -28
  32. package/src/layout.ts +282 -24
  33. package/src/lifecycle/__tests__/args.test.ts +20 -0
  34. package/src/lifecycle/__tests__/builtin.test.ts +97 -0
  35. package/src/lifecycle/__tests__/bundle.test.ts +184 -6
  36. package/src/lifecycle/__tests__/manifest-contract.test.ts +2 -2
  37. package/src/lifecycle/__tests__/new.test.ts +129 -0
  38. package/src/lifecycle/__tests__/smoke.test.ts +2 -1
  39. package/src/manifest-contract.ts +16 -3
  40. package/src/market/__tests__/bootstrap.test.ts +17 -9
  41. package/src/market/bootstrap.ts +30 -17
  42. package/src/market/publish.ts +10 -2
  43. package/src/new.ts +208 -0
  44. package/src/oj-caps.ts +133 -0
  45. package/src/oj.ts +27 -4
  46. package/src/preview-config.ts +1 -1
  47. package/src/preview.ts +2 -1
  48. package/src/site.ts +2 -2
  49. package/src/usage.ts +24 -12
  50. package/src/vendor.ts +5 -1
  51. package/templates/{api/config.yaml → config.yaml} +13 -8
  52. package/templates/global.d.ts +7 -6
  53. package/templates/init.include.yaml +41 -36
  54. package/templates/package.json +3 -1
  55. package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/installer.ts +4 -0
  56. package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/smoke.ts +21 -2
  57. package/templates/{api/src/_platform → src/_platform/api}/fixtures/l1-test-user.sql +4 -5
  58. package/templates/{api/src/_platform → src/_platform/api}/market/api.ts +25 -15
  59. package/templates/src/_platform/api/seed.mysql.sql +16 -0
  60. package/templates/src/_platform/api/seed.pg.sql +16 -0
  61. package/templates/{api/src/_platform → src/_platform/api}/seed.sql +16 -5
  62. package/templates/{api/src/_platform → src/_platform/api}/solutions/api.ts +7 -3
  63. package/templates/{api/src/auth → src/auth/api}/_shared/login-throttle.ts +2 -2
  64. package/templates/{api/src/auth → src/auth/api}/_shared/session.ts +2 -2
  65. package/templates/{api/src/auth → src/auth/api}/api.ts +1 -1
  66. package/templates/{api/src/auth → src/auth/api}/ldap-login/api.ts +1 -1
  67. package/templates/src/auth/api/manifest.yaml +6 -0
  68. package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/InstallDialog.test.tsx +53 -1
  69. package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/marketClient.fake.ts +6 -0
  70. package/templates/{web/src/oj-market → src/oj-market/web}/api/client.ts +5 -0
  71. package/templates/{web/src/oj-market → src/oj-market/web}/api/context.ts +2 -2
  72. package/templates/{web/src/oj-market → src/oj-market/web}/api/defaultClient.ts +7 -1
  73. package/templates/src/oj-market/web/components/InstallDialog.tsx +194 -0
  74. package/templates/{web/src/oj-market → src/oj-market/web}/components/RollbackDialog.tsx +7 -3
  75. package/templates/{web/src/oj-market → src/oj-market/web}/components/UninstallConfirm.tsx +18 -7
  76. package/templates/{web/src/oj-market → src/oj-market/web}/hooks/useMarket.ts +29 -12
  77. package/templates/{web/src/oj-market → src/oj-market/web}/locales/en-US.json +10 -1
  78. package/templates/{web/src/oj-market → src/oj-market/web}/locales/zh-CN.json +10 -1
  79. package/templates/{web/src/oj-market → src/oj-market/web}/types.ts +15 -2
  80. package/templates/{api/src/system → src/system/api}/login-methods/api.ts +2 -2
  81. package/templates/src/system/api/manifest.yaml +6 -0
  82. package/templates/{api/src/system → src/system/api}/oplog/api.ts +4 -4
  83. package/templates/{api/src/system → src/system/api}/permissions/api.ts +3 -3
  84. package/templates/{api/src/system → src/system/api}/roles/api.ts +3 -3
  85. package/templates/{api/src/system → src/system/api}/roles/bindings/api.ts +3 -3
  86. package/templates/{api/src/system → src/system/api}/settings/api.ts +3 -3
  87. package/templates/{api/src/system → src/system/api}/users/api.ts +3 -3
  88. package/templates/{api/src/system → src/system/api}/users/forbidden/api.ts +3 -3
  89. package/templates/{api/src/system → src/system/api}/users/password/api.ts +3 -3
  90. package/templates/{api/src/system → src/system/api}/users/roles/api.ts +3 -3
  91. package/templates/{web/src/system → src/system/web}/locales/en-US.json +1 -1
  92. package/templates/{web/src/system → src/system/web}/locales/zh-CN.json +1 -1
  93. package/templates/src/tsconfig.json +16 -0
  94. package/templates/{api/tests → tests}/auth-ldap.spec.ts +2 -2
  95. package/templates/{api/tests → tests}/auth-session.spec.ts +3 -3
  96. package/templates/{api/tests → tests}/auth-throttle.spec.ts +1 -1
  97. package/templates/tests/fixture.ts +434 -0
  98. package/templates/{api/tests → tests}/guard.spec.ts +1 -1
  99. package/templates/{api/tests → tests}/l1-builder.test.ts +2 -2
  100. package/templates/{api/tests → tests}/lifecycle/atomic-state.spec.ts +3 -3
  101. package/templates/{api/tests → tests}/lifecycle/bootstrap.spec.ts +8 -4
  102. package/templates/{api/tests → tests}/lifecycle/deploy-smoke.spec.ts +31 -8
  103. package/templates/{api/tests → tests}/lifecycle/installer.spec.ts +15 -15
  104. package/templates/{api/tests → tests}/lifecycle/market-api.spec.ts +30 -29
  105. package/templates/{api/tests → tests}/lifecycle/migrate.spec.ts +6 -6
  106. package/templates/{api/tests → tests}/lifecycle/preflight.spec.ts +4 -4
  107. package/templates/{api/tests → tests}/lifecycle/register.spec.ts +1 -1
  108. package/templates/{api/tests → tests}/lifecycle/semver.spec.ts +1 -1
  109. package/templates/{api/tests → tests}/lifecycle/sha384.spec.ts +1 -1
  110. package/templates/{api/tests → tests}/lifecycle/sig-fixtures.spec.ts +1 -1
  111. package/templates/{api/tests → tests}/lifecycle/solutions-api.spec.ts +1 -1
  112. package/templates/{api/tests → tests}/lifecycle/steps-fs.spec.ts +12 -12
  113. package/templates/{api/tests → tests}/lifecycle/text-edit.spec.ts +1 -1
  114. package/templates/{api/tests → tests}/lifecycle/tgz-guard.spec.ts +1 -1
  115. package/templates/{api/tests → tests}/lifecycle/trust.spec.ts +5 -5
  116. package/templates/{api/tests → tests}/lifecycle/uninstall.spec.ts +7 -7
  117. package/templates/{api/tests → tests}/lifecycle/verify-checks.spec.ts +9 -6
  118. package/templates/{api/tests → tests}/lifecycle/verify-report.spec.ts +7 -5
  119. package/templates/{api/tests → tests}/query-utils.spec.ts +1 -1
  120. package/templates/{api/tests → tests}/rbac-audit.spec.ts +1 -1
  121. package/templates/{api/tests → tests}/rbac-contacts.spec.ts +2 -2
  122. package/templates/{api/tests → tests}/rbac-guard.spec.ts +1 -1
  123. package/templates/{api/tests → tests}/rbac-roles.spec.ts +2 -2
  124. package/templates/{api/tests → tests}/rbac-seed.spec.ts +13 -11
  125. package/templates/{api/tests → tests}/rbac-settings.spec.ts +2 -2
  126. package/templates/{api/tests → tests}/rbac-solution-perms.spec.ts +1 -1
  127. package/templates/{api/tests → tests}/rbac-users.spec.ts +3 -3
  128. package/templates/tests/seed-role-accounts.spec.ts +87 -0
  129. package/templates/tsconfig.json +5 -1
  130. package/templates/tsconfig.paths.json +19 -0
  131. package/templates/types/oj-modules.d.ts +17 -0
  132. package/templates/vitest.config.ts +22 -0
  133. package/templates/web.config.ts +23 -13
  134. package/vendor/host-versions.json +3 -3
  135. package/templates/api/src/_platform/seed.mysql.sql +0 -8
  136. package/templates/api/src/_platform/seed.pg.sql +0 -8
  137. package/templates/api/src/auth/manifest.yaml +0 -5
  138. package/templates/api/src/system/manifest.yaml +0 -5
  139. package/templates/api/tests/fixture.ts +0 -58
  140. package/templates/api/tsconfig.json +0 -8
  141. package/templates/api/vitest.config.ts +0 -12
  142. package/templates/logs/task-events.jsonl +0 -0
  143. package/templates/web/src/oj-market/components/InstallDialog.tsx +0 -112
  144. /package/templates/{api/.ojm-api-exempt.json → .ojm-api-exempt.json} +0 -0
  145. /package/templates/{api/seed.mysql.sql → seed.mysql.sql} +0 -0
  146. /package/templates/{api/seed.pg.sql → seed.pg.sql} +0 -0
  147. /package/templates/{api/src/_platform → src/_platform/api}/README.md +0 -0
  148. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/atomic.ts +0 -0
  149. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/history.ts +0 -0
  150. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/lock.ts +0 -0
  151. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/semver.ts +0 -0
  152. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/sha384.ts +0 -0
  153. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/state.ts +0 -0
  154. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/deploy-web.ts +0 -0
  155. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/download.ts +0 -0
  156. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/extract.ts +0 -0
  157. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/migrate.ts +0 -0
  158. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/preflight.ts +0 -0
  159. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/register.ts +0 -0
  160. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/reload.ts +0 -0
  161. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/verify.ts +0 -0
  162. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/tasks.ts +0 -0
  163. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/text-edit.ts +0 -0
  164. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/tgz-guard.ts +0 -0
  165. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/trust.ts +0 -0
  166. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/types.ts +0 -0
  167. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/uninstall.ts +0 -0
  168. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/verify-checks.ts +0 -0
  169. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/verify-report.ts +0 -0
  170. /package/templates/{api/src/_platform → src/_platform/api}/_shared/respond.ts +0 -0
  171. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/domain/contacts.ts +0 -0
  172. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/domain/roles.ts +0 -0
  173. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/domain/settings.ts +0 -0
  174. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/domain/solution-perms.ts +0 -0
  175. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/domain/users.ts +0 -0
  176. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/infra/access.ts +0 -0
  177. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/infra/audit.ts +0 -0
  178. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/infra/guard.ts +0 -0
  179. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/infra/oplog.ts +0 -0
  180. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/infra/query.ts +0 -0
  181. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/infra/rate.ts +0 -0
  182. /package/templates/{api/src/_platform → src/_platform/api}/_shared/site-facade.ts +0 -0
  183. /package/templates/{api/src/_platform → src/_platform/api}/contacts/api.ts +0 -0
  184. /package/templates/{api/src/_platform → src/_platform/api}/contacts/detail/api.ts +0 -0
  185. /package/templates/{api/src/_platform → src/_platform/api}/manifest.yaml +0 -0
  186. /package/templates/{api/src/_platform → src/_platform/api}/market/bootstrap.ts +0 -0
  187. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0001__create_users.mysql.sql +0 -0
  188. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0001__create_users.pg.sql +0 -0
  189. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0001__create_users.sql +0 -0
  190. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0002__add_users_avatar_base64.mysql.sql +0 -0
  191. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0002__add_users_avatar_base64.sql +0 -0
  192. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0003__users_source_display_name.mysql.sql +0 -0
  193. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0003__users_source_display_name.sql +0 -0
  194. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0004__sys_rbac.mysql.sql +0 -0
  195. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0004__sys_rbac.pg.sql +0 -0
  196. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0004__sys_rbac.sql +0 -0
  197. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0005__users_must_change.mysql.sql +0 -0
  198. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0005__users_must_change.sql +0 -0
  199. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0006__users_is_forbidden.mysql.sql +0 -0
  200. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0006__users_is_forbidden.sql +0 -0
  201. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0007__users_email.sql +0 -0
  202. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0008__sys_refresh_token.mysql.sql +0 -0
  203. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0008__sys_refresh_token.pg.sql +0 -0
  204. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0008__sys_refresh_token.sql +0 -0
  205. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0009__sys_throttle.mysql.sql +0 -0
  206. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0009__sys_throttle.pg.sql +0 -0
  207. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0009__sys_throttle.sql +0 -0
  208. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0010__sys_solution_route.mysql.sql +0 -0
  209. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0010__sys_solution_route.pg.sql +0 -0
  210. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0010__sys_solution_route.sql +0 -0
  211. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0011__sys_setting_group_label.mysql.sql +0 -0
  212. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0011__sys_setting_group_label.sql +0 -0
  213. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0012__market_history.mysql.sql +0 -0
  214. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0012__market_history.pg.sql +0 -0
  215. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0012__market_history.sql +0 -0
  216. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0013__users_profile.sql +0 -0
  217. /package/templates/{api/src/_platform → src/_platform/api}/schema.yaml +0 -0
  218. /package/templates/{web/src/_platform → src/_platform/web}/__client/api.schemas.ts +0 -0
  219. /package/templates/{web/src/_platform → src/_platform/web}/__client/api.ts +0 -0
  220. /package/templates/{web/src/about → src/about/web}/entry.ts +0 -0
  221. /package/templates/{web/src/about → src/about/web}/locales/en-US.json +0 -0
  222. /package/templates/{web/src/about → src/about/web}/locales/zh-CN.json +0 -0
  223. /package/templates/{web/src/about → src/about/web}/pages/constants.ts +0 -0
  224. /package/templates/{web/src/about → src/about/web}/pages/index.tsx +0 -0
  225. /package/templates/{web/src/access → src/access/web}/entry.ts +0 -0
  226. /package/templates/{web/src/access → src/access/web}/locales/en-US.json +0 -0
  227. /package/templates/{web/src/access → src/access/web}/locales/zh-CN.json +0 -0
  228. /package/templates/{web/src/access → src/access/web}/pages/access-mode/index.tsx +0 -0
  229. /package/templates/{web/src/access → src/access/web}/pages/admin-visible/index.tsx +0 -0
  230. /package/templates/{web/src/access → src/access/web}/pages/button-control/index.tsx +0 -0
  231. /package/templates/{web/src/access → src/access/web}/pages/common-visible/index.tsx +0 -0
  232. /package/templates/{web/src/access → src/access/web}/pages/page-control/index.tsx +0 -0
  233. /package/templates/{api/src/auth → src/auth/api}/_shared/ldap-login.ts +0 -0
  234. /package/templates/{api/src/auth → src/auth/api}/_shared/ldap.ts +0 -0
  235. /package/templates/{web/src/auth → src/auth/web}/__client/api.schemas.ts +0 -0
  236. /package/templates/{web/src/auth → src/auth/web}/__client/api.ts +0 -0
  237. /package/templates/{api/src/demo → src/demo/api}/manifest.yaml +0 -0
  238. /package/templates/{api/src/demo → src/demo/api}/migrations/0001__create_todos.sql +0 -0
  239. /package/templates/{api/src/demo → src/demo/api}/schema.yaml +0 -0
  240. /package/templates/{api/src/demo → src/demo/api}/seed.sql +0 -0
  241. /package/templates/{api/src/demo → src/demo/api}/todos/api.ts +0 -0
  242. /package/templates/{web/src/demo → src/demo/web}/__client/api.schemas.ts +0 -0
  243. /package/templates/{web/src/demo → src/demo/web}/__client/api.ts +0 -0
  244. /package/templates/{web/src/demo → src/demo/web}/entry.ts +0 -0
  245. /package/templates/{web/src/demo → src/demo/web}/locales/en-US.json +0 -0
  246. /package/templates/{web/src/demo → src/demo/web}/locales/zh-CN.json +0 -0
  247. /package/templates/{web/src/demo → src/demo/web}/pages/index.tsx +0 -0
  248. /package/templates/{web/src/exception → src/exception/web}/entry.ts +0 -0
  249. /package/templates/{web/src/exception → src/exception/web}/locales/en-US.json +0 -0
  250. /package/templates/{web/src/exception → src/exception/web}/locales/zh-CN.json +0 -0
  251. /package/templates/{web/src/exception → src/exception/web}/pages/403/index.tsx +0 -0
  252. /package/templates/{web/src/exception → src/exception/web}/pages/404/index.tsx +0 -0
  253. /package/templates/{web/src/exception → src/exception/web}/pages/500/index.tsx +0 -0
  254. /package/templates/{web/src/exception → src/exception/web}/pages/unknown-component/index.tsx +0 -0
  255. /package/templates/{api/src/home → src/home/api}/line/api.ts +0 -0
  256. /package/templates/{api/src/home → src/home/api}/manifest.yaml +0 -0
  257. /package/templates/{api/src/home → src/home/api}/migrations/0001__create_home_tables.sql +0 -0
  258. /package/templates/{api/src/home → src/home/api}/pie/api.ts +0 -0
  259. /package/templates/{api/src/home → src/home/api}/schema.yaml +0 -0
  260. /package/templates/{api/src/home → src/home/api}/seed.sql +0 -0
  261. /package/templates/{web/src/home → src/home/web}/__client/api.schemas.ts +0 -0
  262. /package/templates/{web/src/home → src/home/web}/__client/api.ts +0 -0
  263. /package/templates/{web/src/home → src/home/web}/entry.ts +0 -0
  264. /package/templates/{web/src/home → src/home/web}/locales/en-US.json +0 -0
  265. /package/templates/{web/src/home → src/home/web}/locales/zh-CN.json +0 -0
  266. /package/templates/{web/src/home → src/home/web}/pages/components/bar-chart.tsx +0 -0
  267. /package/templates/{web/src/home → src/home/web}/pages/components/card-list.tsx +0 -0
  268. /package/templates/{web/src/home → src/home/web}/pages/components/line-chart.tsx +0 -0
  269. /package/templates/{web/src/home → src/home/web}/pages/components/pie-chart.tsx +0 -0
  270. /package/templates/{web/src/home → src/home/web}/pages/index.tsx +0 -0
  271. /package/templates/{web/src/login → src/login/web}/entry.ts +0 -0
  272. /package/templates/{web/src/login → src/login/web}/locales/en-US.json +0 -0
  273. /package/templates/{web/src/login → src/login/web}/locales/zh-CN.json +0 -0
  274. /package/templates/{web/src/login → src/login/web}/pages/change-password.tsx +0 -0
  275. /package/templates/{web/src/login → src/login/web}/pages/login.tsx +0 -0
  276. /package/templates/{api/src/notification → src/notification/api}/_shared/notify.ts +0 -0
  277. /package/templates/{api/src/notification → src/notification/api}/api.ts +0 -0
  278. /package/templates/{api/src/notification → src/notification/api}/del/api.ts +0 -0
  279. /package/templates/{api/src/notification → src/notification/api}/manifest.yaml +0 -0
  280. /package/templates/{api/src/notification → src/notification/api}/migrations/0001__create_notifications.sql +0 -0
  281. /package/templates/{api/src/notification → src/notification/api}/migrations/0002__notifications_user.sql +0 -0
  282. /package/templates/{api/src/notification → src/notification/api}/migrations/0003__notifications_broadcast.sql +0 -0
  283. /package/templates/{api/src/notification → src/notification/api}/migrations/0004__notifications_broadcast_hidden.sql +0 -0
  284. /package/templates/{api/src/notification → src/notification/api}/schema.yaml +0 -0
  285. /package/templates/{api/src/notification → src/notification/api}/seed.sql +0 -0
  286. /package/templates/{web/src/notification → src/notification/web}/__client/api.schemas.ts +0 -0
  287. /package/templates/{web/src/notification → src/notification/web}/__client/api.ts +0 -0
  288. /package/templates/{web/src/notification → src/notification/web}/entry.ts +0 -0
  289. /package/templates/{web/src/notification → src/notification/web}/locales/en-US.json +0 -0
  290. /package/templates/{web/src/notification → src/notification/web}/locales/zh-CN.json +0 -0
  291. /package/templates/{web/src/notification → src/notification/web}/pages/center/index.tsx +0 -0
  292. /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/CardGrid.test.tsx +0 -0
  293. /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/HistoryDrawer.test.tsx +0 -0
  294. /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/RollbackDialog.test.tsx +0 -0
  295. /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/UninstallConfirm.test.tsx +0 -0
  296. /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/poll.test.ts +0 -0
  297. /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/render.tsx +0 -0
  298. /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/runtime-stub.ts +0 -0
  299. /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/semver.test.ts +0 -0
  300. /package/templates/{web/src/oj-market → src/oj-market/web}/api/MarketClientProvider.tsx +0 -0
  301. /package/templates/{web/src/oj-market → src/oj-market/web}/components/CardGrid.tsx +0 -0
  302. /package/templates/{web/src/oj-market → src/oj-market/web}/components/CardItem.tsx +0 -0
  303. /package/templates/{web/src/oj-market → src/oj-market/web}/components/DetailDrawer.tsx +0 -0
  304. /package/templates/{web/src/oj-market → src/oj-market/web}/components/HistoryDrawer.tsx +0 -0
  305. /package/templates/{web/src/oj-market → src/oj-market/web}/entry.ts +0 -0
  306. /package/templates/{web/src/oj-market → src/oj-market/web}/pages/MarketApp.tsx +0 -0
  307. /package/templates/{web/src/oj-market → src/oj-market/web}/utils/poll.ts +0 -0
  308. /package/templates/{web/src/oj-market → src/oj-market/web}/utils/semver.ts +0 -0
  309. /package/templates/{web/src/outside → src/outside/web}/entry.ts +0 -0
  310. /package/templates/{web/src/outside → src/outside/web}/locales/en-US.json +0 -0
  311. /package/templates/{web/src/outside → src/outside/web}/locales/zh-CN.json +0 -0
  312. /package/templates/{api/src/personal-center → src/personal-center/api}/manifest.yaml +0 -0
  313. /package/templates/{api/src/personal-center → src/personal-center/api}/profile/api.ts +0 -0
  314. /package/templates/{api/src/personal-center → src/personal-center/api}/upload/api.ts +0 -0
  315. /package/templates/{web/src/personal-center → src/personal-center/web}/__client/api.schemas.ts +0 -0
  316. /package/templates/{web/src/personal-center → src/personal-center/web}/__client/api.ts +0 -0
  317. /package/templates/{web/src/personal-center → src/personal-center/web}/entry.ts +0 -0
  318. /package/templates/{web/src/personal-center → src/personal-center/web}/locales/en-US.json +0 -0
  319. /package/templates/{web/src/personal-center → src/personal-center/web}/locales/zh-CN.json +0 -0
  320. /package/templates/{web/src/personal-center → src/personal-center/web}/pages/my-profile/index.tsx +0 -0
  321. /package/templates/{web/src/route-nest → src/route-nest/web}/entry.ts +0 -0
  322. /package/templates/{web/src/route-nest → src/route-nest/web}/locales/en-US.json +0 -0
  323. /package/templates/{web/src/route-nest → src/route-nest/web}/locales/zh-CN.json +0 -0
  324. /package/templates/{web/src/route-nest → src/route-nest/web}/pages/menu1/menu1-1/index.tsx +0 -0
  325. /package/templates/{web/src/route-nest → src/route-nest/web}/pages/menu1/menu1-2/index.tsx +0 -0
  326. /package/templates/{web/src/route-nest → src/route-nest/web}/pages/menu2/index.tsx +0 -0
  327. /package/templates/{web/src → src}/styles.css +0 -0
  328. /package/templates/{web/src/system → src/system/web}/__client/api.schemas.ts +0 -0
  329. /package/templates/{web/src/system → src/system/web}/__client/api.ts +0 -0
  330. /package/templates/{web/src/system → src/system/web}/entry.ts +0 -0
  331. /package/templates/{web/src/system → src/system/web}/pages/errors.ts +0 -0
  332. /package/templates/{web/src/system → src/system/web}/pages/forbidden.tsx +0 -0
  333. /package/templates/{web/src/system → src/system/web}/pages/login-methods/index.tsx +0 -0
  334. /package/templates/{web/src/system → src/system/web}/pages/oplog/index.tsx +0 -0
  335. /package/templates/{web/src/system → src/system/web}/pages/roles/index.tsx +0 -0
  336. /package/templates/{web/src/system → src/system/web}/pages/settings/index.tsx +0 -0
  337. /package/templates/{web/src/system → src/system/web}/pages/users/index.tsx +0 -0
  338. /package/templates/{web/src/system → src/system/web}/pages/users/roles.tsx +0 -0
  339. /package/templates/{api/src/web → src/web/api}/get-async-routes/api.ts +0 -0
  340. /package/templates/{api/src/web → src/web/api}/hello/api.ts +0 -0
  341. /package/templates/{api/src/web → src/web/api}/manifest.yaml +0 -0
  342. /package/templates/{api/src/web → src/web/api}/user-info/api.ts +0 -0
  343. /package/templates/{web/src → src/web}/web/__client/api.schemas.ts +0 -0
  344. /package/templates/{web/src → src/web}/web/__client/api.ts +0 -0
  345. /package/templates/{api/tests → tests}/rbac-tables.spec.ts +0 -0
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 验签公钥)
@@ -0,0 +1,20 @@
1
+ // BDD:ojm new 参数解析(统一审查 F8——多余位置参数须显式拒绝,不能静默丢弃)。
2
+ // 静默丢弃的恶果:ojm new crm erp(手滑/粘贴)只建 crm,用户以为两份都建了。
3
+ import { describe, expect, it } from "vitest";
4
+ import { parseNewArgs } from "../../args";
5
+
6
+ describe("parseNewArgs", () => {
7
+ it("name + 旗标组合", () => {
8
+ expect(parseNewArgs(["crm"])).toEqual({ name: "crm", layout: undefined, api: false });
9
+ expect(parseNewArgs(["crm", "--layout", "none", "--api"])).toEqual({ name: "crm", layout: "none", api: true });
10
+ expect(parseNewArgs([])).toEqual({ name: undefined, layout: undefined, api: false });
11
+ });
12
+
13
+ it("多余位置参数 → 报错而非静默丢弃", () => {
14
+ expect(() => parseNewArgs(["crm", "erp"])).toThrow(/只接受一个方案名/);
15
+ });
16
+
17
+ it("非法 --layout 值 → 报错", () => {
18
+ expect(() => parseNewArgs(["crm", "--layout", "sidebar"])).toThrow(/container \| none/);
19
+ });
20
+ });
@@ -0,0 +1,97 @@
1
+ // BDD:脚手架自带模块身份(`src/.ojm-builtin.json`)。
2
+ // 消费契约:bundle 排除打包 / ojm info 折叠 / ojm new 拒名;文件缺失 → 空名单(旧工程兼容)。
3
+ // 用例对应设计 docs/plans/2026-10-22-1640-multi-solution-bundle-design.md:
4
+ // B12(坏文件 fail-fast)/ T1(名单 == 白名单派生集)/ T2(copyIfAbsent,不覆盖用户手改)
5
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
6
+ import { tmpdir } from "node:os";
7
+ import { join } from "node:path";
8
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
9
+ import { BUILTIN_LIST_FILE, deriveBuiltinModules, readBuiltinModules, writeBuiltinList } from "../../builtin";
10
+
11
+ let project: string;
12
+
13
+ beforeEach(() => {
14
+ project = mkdtempSync(join(tmpdir(), "ojm-builtin-"));
15
+ });
16
+
17
+ afterEach(() => {
18
+ rmSync(project, { recursive: true, force: true });
19
+ });
20
+
21
+ function writeList(content: string): void {
22
+ mkdirSync(join(project, "src"), { recursive: true });
23
+ writeFileSync(join(project, "src", BUILTIN_LIST_FILE), content);
24
+ }
25
+
26
+ describe("readBuiltinModules", () => {
27
+ it("文件缺失 → 空名单(旧工程/框架仓无内置概念,行为退化兼容)", () => {
28
+ expect(readBuiltinModules(project).size).toBe(0);
29
+ });
30
+
31
+ it("合法文件 → 返回 modules 集合", () => {
32
+ writeList(JSON.stringify({ modules: ["auth", "home"] }));
33
+ expect([...readBuiltinModules(project)].sort()).toEqual(["auth", "home"]);
34
+ });
35
+
36
+ it("b12 坏 JSON → fail-fast,报错含文件路径与错因(不静默降级)", () => {
37
+ writeList("{ not json");
38
+ expect(() => readBuiltinModules(project)).toThrow(new RegExp(`${BUILTIN_LIST_FILE.replace(".", "\\.")}`));
39
+ });
40
+
41
+ it("b12 modules 非数组 → fail-fast", () => {
42
+ writeList(JSON.stringify({ modules: "auth" }));
43
+ expect(() => readBuiltinModules(project)).toThrow(/modules/);
44
+ });
45
+
46
+ it("b12 modules 含非字符串项 → fail-fast", () => {
47
+ writeList(JSON.stringify({ modules: ["auth", 42] }));
48
+ expect(() => readBuiltinModules(project)).toThrow(/modules/);
49
+ });
50
+ });
51
+
52
+ describe("deriveBuiltinModules(名单派生自 init.include.yaml,单一真源)", () => {
53
+ it("t1 取 src/<name>/(api|web) 条目的模块名(含 web-only 模块),忽略无关路径", () => {
54
+ const include = new Set([
55
+ "src/auth/api",
56
+ "src/notification/api",
57
+ "src/login/web/entry.ts",
58
+ "src/oj-market/web/pages",
59
+ "src/styles.css",
60
+ "src/tsconfig.json",
61
+ "config.yaml",
62
+ "web.config.ts",
63
+ ]);
64
+ expect([...deriveBuiltinModules(include)].sort())
65
+ .toEqual(["auth", "login", "notification", "oj-market"]);
66
+ });
67
+
68
+ it("白名单无模块条目 → 空集(不凭空造身份)", () => {
69
+ expect(deriveBuiltinModules(new Set(["config.yaml", "README.md"])).size).toBe(0);
70
+ });
71
+ });
72
+
73
+ describe("writeBuiltinList(init 接线:copyIfAbsent)", () => {
74
+ it("t1 写入派生名单(排序)+ 自注释头,返回 created", () => {
75
+ const include = new Set(["src/home/web/entry.ts", "src/auth/api", "src/login/web"]);
76
+ expect(writeBuiltinList(project, include)).toBe("created");
77
+ const p = join(project, "src", BUILTIN_LIST_FILE);
78
+ expect(existsSync(p)).toBe(true);
79
+ const doc = JSON.parse(readFileSync(p, "utf-8"));
80
+ expect(doc.modules).toEqual(["auth", "home", "login"]);
81
+ expect(typeof doc["//"]).toBe("string");
82
+ });
83
+
84
+ it("t2 已存在 → 返回 skipped 且内容原样不动(用户手改权)", () => {
85
+ const include = new Set(["src/auth/api"]);
86
+ expect(writeBuiltinList(project, include)).toBe("created");
87
+ const p = join(project, "src", BUILTIN_LIST_FILE);
88
+ writeFileSync(p, JSON.stringify({ modules: ["my-edit"] }));
89
+ expect(writeBuiltinList(project, include)).toBe("skipped");
90
+ expect(JSON.parse(readFileSync(p, "utf-8")).modules).toEqual(["my-edit"]);
91
+ });
92
+
93
+ it("派生集为空 → 不写文件,返回 none", () => {
94
+ expect(writeBuiltinList(project, new Set(["config.yaml"]))).toBe("none");
95
+ expect(existsSync(join(project, "src", BUILTIN_LIST_FILE))).toBe(false);
96
+ });
97
+ });