@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
@@ -144,10 +144,10 @@ export default defineModule({
144
144
  - 因此你只需在双语 locale 里声明**一条** `<模块名>:menu.main`:
145
145
 
146
146
  ```jsonc
147
- // web/src/<模块>/locales/zh-CN.json
147
+ // src/<模块>/web/locales/zh-CN.json
148
148
  { "menu": { "main": "Wiki" } } // 命中 <模块名>:menu.main(根菜单 + 系统设置里的管理子组都读它)
149
149
 
150
- // web/src/<模块>/locales/en-US.json
150
+ // src/<模块>/web/locales/en-US.json
151
151
  { "menu": { "main": "Wiki" } }
152
152
  ```
153
153
 
@@ -190,7 +190,7 @@ handle: { currentActiveMenu: wikiPath("space") } // → "/wiki/space",随模
190
190
  推荐在模块内放一个 `paths.ts`:
191
191
 
192
192
  ```ts
193
- // web/src/<模块>/paths.ts
193
+ // src/<模块>/web/paths.ts
194
194
  export const WIKI_ROOT = "wiki"; // 模块挂载根(唯一定义处)
195
195
  export const wikiBase = `/${WIKI_ROOT}`;
196
196
  export const wikiPath = (...segments: Array<string | number>): string =>
@@ -3,6 +3,8 @@
3
3
  > 一篇写给「第一次在本工程开发模块」的人的教学文档。读完之后你应该能回答三个问题:
4
4
  > **① 开发中的模块要不要「安装」?② 要的话装到哪、会不会重复?③ 怎么在本地把市场链路完整跑一遍?**
5
5
  > 适用版本:`@oj-module/cli` 0.2.12+(`ojm market` 子命令),运行期 `oj` ≥ 0.1.73。
6
+ > 本文按**当前模板的 module-root 布局**书写(`src/<模块>/{api,web}` + 工程根 `config.yaml`),
7
+ > 建新工程另需 `oj` ≥ 0.1.85(`--module-root`)。历史布局(`api/src` + `web/src`)对照写在括注里。
6
8
 
7
9
  ## 1. 先建立心智模型:开发态 ≠ 分发态
8
10
 
@@ -10,12 +12,12 @@
10
12
 
11
13
  | | 开发态 | 分发态 |
12
14
  |---|---|---|
13
- | 模块在哪 | 工程内源码:`web/src/A` + `api/src/A` | 一个 `.tgz` 包(`ojm build --bundle` 产出) |
14
- | 谁登记它 | `web.config.ts` 的 `modules` 列表 → `ojm build` 生成 `modules.json` | 安装器往目标站点 upsert |
15
+ | 模块在哪 | 工程内源码:`src/A/api` + `src/A/web`(历史布局:`api/src/A` + `web/src/A`) | 一个 `.tgz` 包(`ojm build --bundle` 产出) |
16
+ | 谁登记它 | `ojm build` 扫描 `src/*/web/entry.ts`(`web.config.ts` 可选覆盖)→ 生成 `web/dist/modules.json` | 安装器往目标站点 upsert |
15
17
  | 跑起来靠 | `ojm dev` 直跑源码,改一行刷新即见 | 安装器十阶段(下载→验签→解压→迁移→注册→热更→冒烟) |
16
18
  | 典型场景 | 你在写它 | 别人(或另一个站点)装它 |
17
19
 
18
- 一句话:**开发态没有「安装」这个动作**。`ojm dev` 起的站点直接扫描 `api/src` 产生路由、读 `modules.json` 装配菜单——你写的模块 A 此刻「已经在跑」,只是还没打过包。
20
+ 一句话:**开发态没有「安装」这个动作**。`ojm dev` 起的站点直接扫描 `src/` 产生路由(`oj build --module-root src`)、读 `modules.json` 装配菜单——你写的模块 A 此刻「已经在跑」,只是还没打过包。
19
21
 
20
22
  ## 2. 常见问题:我开发了模块 A,怎么「安装」我自己?
21
23
 
@@ -26,7 +28,7 @@
26
28
  真正想验证「A 这个包能不能装、安装流程对不对」时,有三个去处,按推荐顺序:
27
29
 
28
30
  1. **一次性隔离站点**:`ojm market trial --fixture <name>@<version>`(随机端口 + 一次性 DB + 一次性密钥 + 隔离信任库/CDN),跑完即毁,不碰你的工程、DB、信任库。这是唯一「装了也不心疼」的地方,也是最快的「这包能不能装」判据(实测 11 段全 ok 约 12 秒)。
29
- 2. **另一个工程**:`ojm install dist/A-0.1.0.tgz`(file 源,multipart 直传)。
31
+ 2. **另一个工程**:`ojm install dist/A-0.1.0.tgz`(file 源,multipart 直传;`dist/` 是 `--bundle` 的默认输出目录,与后端产物 `api/dist` 无关)。
30
32
  3. **市场链路**:`ojm market publish` 落 CDN → 市场页点安装(§4 全套)。
31
33
 
32
34
  ### 2.2 偏要在本工程试怎么办
@@ -56,7 +58,7 @@
56
58
 
57
59
  注意 §3.1 的 upsert 是**按 name/path** 而不是按来源。于是存在这种状态:
58
60
 
59
- > 你源码里的 A(`api/src/A`,路由由 oj 扫描源码产生)和某次安装产物 `dist/A-0.1.0/` 同时存在。
61
+ > 你源码里的 A(`src/A/api`,路由由 oj 扫描源码产生)和某次安装产物 `api/dist/A-0.1.0/` 同时存在。
60
62
 
61
63
  表现是:路由与模块条目各自只有一份(不重复),但**哪一份生效取决于挂载命中顺序**——症状为「我改了源码却不生效」或「行为怎么都不对」。这是本主题下最难查的一类问题:没有报错、没有重复,只是「被另一个自己顶替了」。
62
64
 
@@ -64,7 +66,7 @@
64
66
 
65
67
  ## 4. 本地 CDN 演练场:把市场链路完整跑一遍
66
68
 
67
- 模板自带一个**本地模拟 CDN**(工程根 `cdn/` + `api/config.yaml` 的 `/cdn` 挂载),配上 `ojm market` 四条命令,就能在本地复刻「发包 → 上架 → 索引 → 安装」全链路。不需要外部服务。
69
+ 模板自带一个**本地模拟 CDN**(工程根 `cdn/` + 工程根 `config.yaml` 里的 `/cdn` 挂载),配上 `ojm market` 四条命令,就能在本地复刻「发包 → 上架 → 索引 → 安装」全链路。不需要外部服务。
68
70
 
69
71
  ### 4.1 全流程(五步)
70
72
 
@@ -91,7 +93,7 @@ ojm market verify --rebuild-index
91
93
 
92
94
  | 命令 | 做什么 | **不**做什么 |
93
95
  |---|---|---|
94
- | `market setup` | 写系统设置 `framework.market_cdn_url` | 不改写 `api/config.yaml`(挂载由模板自带) |
96
+ | `market setup` | 写系统设置 `framework.market_cdn_url` | 不改写工程根 `config.yaml`(`/cdn` 挂载由模板自带) |
95
97
  | `market publish` | 落 `<name>-<version>.tgz` + 包级 `.tgz.sig` | **不碰索引**(索引是共享单文件,多人发布会互相覆盖) |
96
98
  | `market build-index` | 扫 `cdn/*.tgz` 提取包内 manifest,全量重建两份索引并签名 | 不落包 |
97
99
  | `market verify` | 完整性 / 包级签 / 索引签 / manifest 结构四类校验 + 漂移检出 | 不起站点、不装、不访问任何远端 |
@@ -129,19 +131,26 @@ ojm market verify --rebuild-index
129
131
 
130
132
  ```text
131
133
  solution-demo/
132
- ├── manifest.yaml # 方案契约清单(Form A)
133
- ├── api/src/<name>/
134
- │ ├── manifest.yaml # oj 原生清单(name/desc/version/tables)
135
- │ ├── schema.yaml # 表结构真源(必须与 manifest.tables 对齐)
136
- │ ├── routes.js
137
- │ ├── migrations/0001__init.sql
138
- │ └── ping/api.ts # handler 必须是 .ts
139
- └── web/src/<name>/locales/{zh-CN,en-US}.json # 引用 i18n key 时必需
134
+ ├── manifest.yaml # 方案契约清单(Form A,恒在工程根)
135
+ └── src/<name>/
136
+ ├── api/
137
+ │ ├── manifest.yaml # oj 原生清单(name/desc/version/tables)
138
+ │ ├── schema.yaml # 表结构真源(必须与 manifest.tables 对齐)
139
+ │ ├── routes.js
140
+ │ ├── migrations/0001__init.sql
141
+ │ └── ping/api.ts # handler 必须是 .ts
142
+ └── web/
143
+ └── locales/{zh-CN,en-US}.json # 引用 i18n key 时必需
140
144
  ```
141
145
 
146
+ > 历史布局把这棵树换成 `api/src/<name>/…` + `web/src/<name>/locales/…`,构建命令里的
147
+ > `--module-root src` 换成 `-d api/src`(两者互斥,都是「源码树根」)。布局判定看标记文件
148
+ > 而不是目录名:`src/<name>/api/manifest.yaml` 或 `src/<name>/web/entry.ts` 存在即成模块。
149
+ > 方案工程若由 `ojm init` 起(会带工程根 `config.yaml`),构建时加 `-c config.yaml`。
150
+
142
151
  ```bash
143
- <host>/bin/oj build -d api/src -o api/dist # 后端构建(只用宿主已装的 oj)
144
- node "$CLI_PKG/bin/ojm.mjs" build --bundle # 打包(构建即校验)
152
+ <host>/bin/oj build --module-root src -o api/dist # 后端构建(只用宿主已装的 oj)
153
+ node "$CLI_PKG/bin/ojm.mjs" build --bundle # 打包(构建即校验)
145
154
  node "$CLI_PKG/bin/ojm.mjs" keygen --id demo.publisher --out ./keys --yes
146
155
  node "$CLI_PKG/bin/ojm.mjs" sign dist/<name>-<version>.tgz --id demo.publisher --key ./keys/private.pem --check
147
156
  ```
@@ -154,8 +163,8 @@ node "$CLI_PKG/bin/ojm.mjs" market publish --real ../solution-demo --id demo.pub
154
163
  node "$CLI_PKG/bin/ojm.mjs" market verify --rebuild-index
155
164
 
156
165
  node "$CLI_PKG/bin/ojm.mjs" build # 生成 api/dist + web/dist(含 api/dist/versions.json)
157
- # config:platform.root 改成 dist(安装目标恒为 release 部署)
158
- (cd api && ../bin/oj serve -c config.yaml -b /api --api-path dist --console-log &)
166
+ # config:platform.root 改成 api/dist(安装目标恒为 release 部署,root 必须与 --api-path 同源)
167
+ (bin/oj serve -c config.yaml -b /api --api-path api/dist --console-log &)
159
168
  node "$CLI_PKG/bin/ojm.mjs" market setup --url http://127.0.0.1:9778 --user admin --pass 123456 --new-pass <新口令>
160
169
  # 市场页点安装 → 首次停在「待信任」→ 核对指纹 → ojm trust confirm --task <id> → 重试 → done
161
170
  ```
@@ -166,11 +175,11 @@ node "$CLI_PKG/bin/ojm.mjs" market setup --url http://127.0.0.1:9778 --user admi
166
175
  |---|---|---|
167
176
  | ① | `oj build` 报 `tables 声明 "x" 不在 schema.yaml` | oj 用 `schema.yaml` 做表结构真源,`manifest.tables` 只是引用面;补 `schema.yaml`(`tables: {<t>: {pk, columns}}`),与迁移语句对齐 |
168
177
  | ② | `oj build` 成功但 `routes.js` 是空表、dist 里没有 handler | **只编译 `.ts` handler**;写成 `ping/api.js` 会被静默丢弃 → 改 `api.ts` |
169
- | ③ | `build --bundle` 报 `缺少 web/src/<name>/locales/…` | manifest 一旦引用 `title_key`/`label_key` 就必须双语 locale(后端方案也要);且 key 必须**嵌套** `{"menu":{"main":"…"}}`,平铺 `"menu.main"` 查不到 |
178
+ | ③ | `build --bundle` 报 `缺少 src/<name>/web/locales/…`(历史布局是 `web/src/<name>/locales/…`) | manifest 一旦引用 `title_key`/`label_key` 就必须双语 locale(后端方案也要);且 key 必须**嵌套** `{"menu":{"main":"…"}}`,平铺 `"menu.main"` 查不到 |
170
179
  | ④ | 安装 POST 返回 200,但 `market/task` 恒 404 | 宿主 config 缺 `fs:` 段 → 安装器落盘被 jail 拒、失败发生在任务文件写入前;0.2.13 起模板自带,旧工程手补 |
171
180
  | ⑤ | 安装卡 `preflight`:`无法读取宿主 runtime 版本` | 安装器读 `api/dist/versions.json`;0.2.13 起 `ojm build` 双落点(旧版手抄 `web/dist/versions.json`) |
172
181
  | ⑥ | 安装卡 `reloading`:`NotCapable: module '_platform' is not platform-privileged` | 宿主 config 的 `platform.modules` 未列 `_platform`(0.2.13 起模板自带) |
173
- | ⑦ | 安装卡 `reloading`:`reloadConfig 返回 unchanged` / 卡 `smoke`:探活 404 | 起站形态不对:`ojm preview` 服务派生 config(安装器热更写的是 `api/config.yaml`,被监视文件没变);dev(`--api-path src`)装到 dist 的模块不被发现。**安装必须对真实 config 以 `--api-path dist` 起站** |
182
+ | ⑦ | 安装卡 `reloading`:`reloadConfig 返回 unchanged` / 卡 `smoke`:探活 404 | 起站形态不对:`ojm preview` 服务派生 config(安装器热更写的是真实 config,派生文件没变);dev 用 `--module-root src`(历史布局 `--api-path src`)读源码,装到 `api/dist` 的模块不被发现。**安装必须对真实 config 以 `--api-path api/dist`(历史布局 `--api-path dist`)起站** |
174
183
 
175
184
  ### 6.4 装成之后怎么确认
176
185
 
@@ -186,7 +195,7 @@ ls api/dist/<name>-<version>/ # 安装产物目录
186
195
  写模块 → 源码直跑(ojm dev),不要装
187
196
  验证能装 → ojm market trial(隔离站点,12s),或 ojm install <tgz> 到另一个工程
188
197
  重复? → 不会。注册面全 upsert;同名再装被前置校验直接拒
189
- 市场链路 → setup → publish → build-index → verify → 市场页安装(release 形态:--api-path dist)
198
+ 市场链路 → setup → publish → build-index → verify → 市场页安装(release 形态:--api-path api/dist)
190
199
  绝对 URL → 市场地址端口取自 config(9778),不是 dev server 的 5174
191
200
  信任 → 包级签=锚,索引签=完整性,TOFU 管陌生发布方,verify 不背书内容安全
192
201
  ```
@@ -3,14 +3,15 @@
3
3
  > **闭环状态(2026-10-20):7 项诉求全部闭环**——6 项随 oj 0.1.81/0.1.82 发布落地,第 4 项
4
4
  > (orIgnore + fromSelect)能力本身已发布但**组合在 sqlite 上被上游明文拒绝**,模板按上游
5
5
  > 文档指引保留一处 sanctioned 裸 SQL 绕行(`notify.ts:216`,api-manual §"orIgnore + fromSelect
6
- > 组合在 sqlite 不支持"原文即指向该绕行)。逐项映射见 §6;模板 `api/src` 内 `保留 db.query`
7
- > 注释已归零(仅剩上述 1 处 `保留 db.exec`)。
6
+ > 组合在 sqlite 不支持"原文即指向该绕行)。逐项映射见 §6;模板 `src/*/api`(布局 v2 前为
7
+ > `api/src`)内 `保留 db.query` 注释已归零(仅剩 `notify.ts` 里 `markAllRead` / `clearNotices`
8
+ > 两处 `保留 db.exec`)。
8
9
  >
9
10
  > 目的:把模板 `db.query` 全面迁移到 `db.table(...)` 构造器以获得多库(sqlite / mysql / postgres)可移植性时,
10
11
  > 识别出构造器**当时无法表达**、只能回退 `db.query` 的 7 类能力。本文档作为向上游 `oj` 运行时 SQL 构造器提 PR 的依据。
11
12
  >
12
13
  > 证据来源:`~/.local/share/oj/devkit/api-manual.md`(查询构造器章节,含 `like` 操作清单 §1142、子查询限制 §1343、
13
- > `union` §1346、CTE §1378 等);框架 `README.md` v0.1.66(ESCAPE 纪律);模板代码 `packages/cli/templates/api/src/**`。
14
+ > `union` §1346、CTE §1378 等);框架 `README.md` v0.1.66(ESCAPE 纪律);模板代码 `packages/cli/templates/src/*/api/**`。
14
15
 
15
16
  ---
16
17
 
@@ -243,12 +244,15 @@ db.table("sys_refresh_token").select("*").where({ field: "token_hash", op: "eq",
243
244
  6. `demo/todos/api.ts:12` / `uninstall.ts:166` → 动态 WHERE 安全出口。
244
245
  7. `session.ts:114-115` → `select("*")`。
245
246
 
246
- 全部关闭后,模板 `api/src` 内 `保留 db.query` / `保留 d.exec` 注释应归零,达成"零原生 SQL 可移植"目标。
247
+ 全部关闭后,模板 `src/*/api` 内 `保留 db.query` / `保留 db.exec` 注释应归零,达成"零原生 SQL 可移植"目标。
247
248
 
248
249
  ---
249
250
 
250
251
  ## 6. 闭环状态(2026-10-20 实测)
251
252
 
253
+ > 表中 `notify.ts:NN` 等行号是 **2026-10-20 的快照**,代码此后有增删(文件也已迁到
254
+ > `src/notification/api/_shared/notify.ts`)——按函数名定位,别按行号跳。
255
+
252
256
  | # | 诉求 | 上游落地(版本) | 模板现状 |
253
257
  |---|------|------------------|----------|
254
258
  | 1 | `like` 的 `ESCAPE` 转义子句 | `contains` / `startswith` / `endswith` 语义化模糊匹配(**v0.1.81**),引擎自动转义 `%`/`_`/`!` | 已闭环:`contacts.ts` 等模糊搜索全部走 `contains`。**订正**:原判「`like` 无 escape 出口」系误报——escape 逃生舱本就存在(白名单 `! / | ~ # @`);真正缺的是业务零转义语义 op,上游按此落地(L1 实测 `%` 元字符不泛化) |
@@ -259,9 +263,10 @@ db.table("sys_refresh_token").select("*").where({ field: "token_hash", op: "eq",
259
263
  | 6 | 动态 WHERE 组合 | `whereIf(test, cond)`(**v0.1.81**):`undefined/null/false/[]` 整条跳过(注意 `""`/`0` 是合法值会保留——可选过滤的缺失值必须归一化为 `undefined`,模板 oplog 曾踩此坑并修复) | 已闭环:`demo/todos`、`uninstall.ts` 等全部改 `whereIf`;未采纳裸片段出口(whereIf 已覆盖诉求场景) |
260
264
  | 7 | `select *` | `select(["*"])`(**v0.1.81**,必须是唯一元素) | 已闭环:`session.ts` refresh 查询改 `select(["*"])` |
261
265
 
262
- **结论**:模板 `api/src` 内 `保留 db.query` 注释归零;`保留 d.exec` 仅剩 `notify.ts:216` 一处,
266
+ **结论**:模板 `src/*/api` 内 `保留 db.query` 注释归零;`保留 db.exec` 仅剩 `notify.ts` 的
267
+ `markAllRead` / `clearNotices` 两处(`src/notification/api/_shared/notify.ts`),
263
268
  且属上游文档明文记载的 sqlite 组合限制绕行(非能力缺口)。"零原生 SQL 可移植"目标以
264
- 「一处上游 sanctioned 绕行」的形态达成。
269
+ 「上游 sanctioned 绕行」的形态达成。
265
270
 
266
271
  > 上游侧复核记录(oj 0.1.81 release notes,devkit `oj-api-dev/README.md`):7 条诉求落地时
267
272
  > 判定「1 条误报、1 条部分误报」——误报即 §3.1 的 escape 出口(本就存在),部分误报即 §3.3
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@oj-module/cli",
3
3
  "type": "module",
4
- "version": "0.2.18",
4
+ "version": "0.2.23",
5
5
  "description": "React Antd Admin 模块工程命令行工具(含预构建宿主 shell-dist)",
6
6
  "license": "MIT",
7
7
  "exports": {
@@ -49,7 +49,7 @@
49
49
  ],
50
50
  "dependencies": {
51
51
  "@faker-js/faker": "^10.4.0",
52
- "@oj-module/runtime": "0.2.18",
52
+ "@oj-module/runtime": "0.2.23",
53
53
  "@redocly/cli": "^2.51.0",
54
54
  "@tailwindcss/vite": "^4.3.0",
55
55
  "esbuild": "^0.28.2",
@@ -204,6 +204,7 @@ var init_site_base = __esmMin((() => {
204
204
  * 子路径部署(/oj/wiki/...)下直接与 loginPath 比较恒为 false,401 时会重复 goLogin。
205
205
  * React 组件内请用 `useLocation()`(react-router 已剥离 basename),无需本函数。
206
206
  *
207
+ * @param pathname 原始路径(含部署 base,如 `location.pathname`)
207
208
  * @param base 部署 base,恒以 `/` 结尾;缺省取 `resolveSiteBase()`(部署期注入优先,
208
209
  * 见 utils/site-base.ts —— runtime 产物里的 `import.meta.env.BASE_URL` 是构建期
209
210
  * 内联的 `/`,子路径部署下直接用它恒错)
@@ -303,10 +304,10 @@ function getAppInfo() {
303
304
  return {
304
305
  "pkg": {
305
306
  "name": "@oj-module/runtime",
306
- "version": "0.2.18",
307
+ "version": "0.2.23",
307
308
  "license": "MIT"
308
309
  },
309
- "lastBuildTime": "2026-10-21 22:40:24"
310
+ "lastBuildTime": "2026-10-22 18:50:31"
310
311
  };
311
312
  }
312
313
  var init_get_app_info = __esmMin((() => {}));
@@ -429,7 +430,7 @@ function PageError({ error, resetErrorBoundary }) {
429
430
  };
430
431
  useEffect(() => {
431
432
  if (enableDynamicTitle) document.title = t("exception.pageErrorTitle");
432
- }, [enableDynamicTitle]);
433
+ }, [enableDynamicTitle, t]);
433
434
  return /* @__PURE__ */ jsx(Result, {
434
435
  status: "error",
435
436
  icon: /* @__PURE__ */ jsx("div", {
@@ -3495,7 +3496,7 @@ function useCurrentRoute() {
3495
3496
  const matches = useMatches();
3496
3497
  return useMemo(() => {
3497
3498
  return matches[matches.length - 1];
3498
- }, [matches, location]);
3499
+ }, [matches]);
3499
3500
  }
3500
3501
  var init_use_current_route = __esmMin((() => {}));
3501
3502
  //#endregion
@@ -3595,7 +3596,7 @@ function useCssVar(name, root = defaultRoot, options = {}) {
3595
3596
  }), [name, root]);
3596
3597
  useEffect(() => {
3597
3598
  if (options?.initialValue) controls.set(options.initialValue);
3598
- }, [options?.initialValue]);
3599
+ }, [options?.initialValue, controls]);
3599
3600
  useEffect(() => {
3600
3601
  return () => controls.remove();
3601
3602
  }, [controls]);
@@ -3656,7 +3657,7 @@ var init_dom = __esmMin((() => {}));
3656
3657
  * @en Get the style of the layout content area
3657
3658
  */
3658
3659
  function useLayoutContentStyle() {
3659
- const contentElement = useRef(null);
3660
+ const contentElementRef = useRef(null);
3660
3661
  const [visibleDomRect, setVisibleDomRect] = useState(null);
3661
3662
  const resizeObserverRef = useRef(null);
3662
3663
  const contentHeightControls = useCssVar(CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT);
@@ -3673,7 +3674,7 @@ function useLayoutContentStyle() {
3673
3674
  };
3674
3675
  }, [visibleDomRect]);
3675
3676
  const debouncedCalcHeight = useDebounceFn((_entries) => {
3676
- const rect = getElementVisibleRect(contentElement.current);
3677
+ const rect = getElementVisibleRect(contentElementRef.current);
3677
3678
  setVisibleDomRect(rect);
3678
3679
  if (rect) {
3679
3680
  contentHeightControls.set(`${rect.height}px`);
@@ -3681,10 +3682,10 @@ function useLayoutContentStyle() {
3681
3682
  }
3682
3683
  }, { wait: 16 });
3683
3684
  useEffect(() => {
3684
- if (contentElement.current && !resizeObserverRef.current) {
3685
+ if (contentElementRef.current && !resizeObserverRef.current) {
3685
3686
  const resizeObserver = new ResizeObserver(debouncedCalcHeight.run);
3686
3687
  resizeObserverRef.current = resizeObserver;
3687
- resizeObserver.observe(contentElement.current);
3688
+ resizeObserver.observe(contentElementRef.current);
3688
3689
  }
3689
3690
  return () => {
3690
3691
  resizeObserverRef.current?.disconnect();
@@ -3692,7 +3693,7 @@ function useLayoutContentStyle() {
3692
3693
  };
3693
3694
  }, [debouncedCalcHeight]);
3694
3695
  return {
3695
- contentElement,
3696
+ contentElementRef,
3696
3697
  overlayStyle,
3697
3698
  visibleDomRect
3698
3699
  };
@@ -4173,7 +4174,7 @@ function KeepAliveLayer() {
4173
4174
  (aliveRef.current?.getCacheNodes?.())?.forEach((node) => {
4174
4175
  if (!openTabs.has(node.cacheKey)) aliveRef.current?.destroy(node.cacheKey);
4175
4176
  });
4176
- }, [openTabs]);
4177
+ }, [openTabs, aliveRef]);
4177
4178
  /**
4178
4179
  * 关闭多 tab 功能时,清空所有非当前页面缓存
4179
4180
  */
@@ -4181,10 +4182,18 @@ function KeepAliveLayer() {
4181
4182
  if (!tabbarEnable) (aliveRef.current?.getCacheNodes?.())?.forEach((node) => {
4182
4183
  if (node.cacheKey !== cacheKey) aliveRef.current?.destroy(node.cacheKey);
4183
4184
  });
4184
- }, [tabbarEnable]);
4185
+ }, [
4186
+ tabbarEnable,
4187
+ aliveRef,
4188
+ cacheKey
4189
+ ]);
4185
4190
  useEffect(() => {
4186
4191
  if (tabbarEnable && isRefresh) aliveRef.current?.refresh();
4187
- }, [isRefresh]);
4192
+ }, [
4193
+ isRefresh,
4194
+ aliveRef,
4195
+ tabbarEnable
4196
+ ]);
4188
4197
  const keepAliveExclude = useMemo(() => {
4189
4198
  /**
4190
4199
  * 不开启多 tab 时不需要 KeepAlive,把所有路由放进 exclude 仅保留切换动画
@@ -4212,7 +4221,7 @@ var init_keep_alive_layer = __esmMin((() => {
4212
4221
  //#region src/layout/layout-content/index.tsx
4213
4222
  function LayoutContent() {
4214
4223
  const { token: { colorBgLayout } } = theme.useToken();
4215
- const { contentElement } = useLayoutContentStyle();
4224
+ const { contentElementRef } = useLayoutContentStyle();
4216
4225
  const enableFooter = usePreferencesStore((state) => state.enableFooter);
4217
4226
  const fixedFooter = usePreferencesStore((state) => state.fixedFooter);
4218
4227
  /**
@@ -4221,7 +4230,7 @@ function LayoutContent() {
4221
4230
  */
4222
4231
  return /* @__PURE__ */ jsx("main", {
4223
4232
  id: ELEMENT_ID_MAIN_CONTENT,
4224
- ref: contentElement,
4233
+ ref: contentElementRef,
4225
4234
  className: "relative overflow-y-auto overflow-x-hidden grow",
4226
4235
  style: { backgroundColor: colorBgLayout },
4227
4236
  children: /* @__PURE__ */ jsx(Scrollbar, { children: /* @__PURE__ */ jsx(GlobalSpin, { children: /* @__PURE__ */ jsxs("div", {
@@ -4447,7 +4456,11 @@ function GlobalSearch() {
4447
4456
  useKeyPress("downarrow", handleDown);
4448
4457
  useEffect(() => {
4449
4458
  if (!keyword.length && Array.isArray(searchHistory)) setResultOptions(searchMenuList.filter((item) => searchHistory?.includes(item.key)));
4450
- }, [keyword, searchHistory]);
4459
+ }, [
4460
+ keyword,
4461
+ searchHistory,
4462
+ searchMenuList
4463
+ ]);
4451
4464
  return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
4452
4465
  onClick: () => setOpen((open) => !open),
4453
4466
  className: "group flex justify-center items-center gap-2 md:bg-colorBgLayout px-3 py-1.5 rounded-full cursor-pointer text-colorTextSecondary hover:text-colorText md:mr-2.5",
@@ -5113,18 +5126,18 @@ function Sidebar() {
5113
5126
  name: item.name,
5114
5127
  checked: item.value,
5115
5128
  onChange: handleChange,
5116
- children: item.label,
5117
- disabled: item.disabled
5129
+ disabled: item.disabled,
5130
+ children: item.label
5118
5131
  }, item.name);
5119
5132
  }),
5120
5133
  /* @__PURE__ */ jsx(SwitchItem, {
5121
5134
  name: "sidebarTheme",
5122
5135
  checked: sidebarTheme === "light",
5123
5136
  onChange: (name, value) => setPreferences(name, value ? "light" : "dark"),
5124
- children: t("preferences.sidebar.sidebarTheme"),
5125
5137
  disabled: !sidebarEnable,
5126
5138
  unCheckedChildren: /* @__PURE__ */ jsx(RiContrastFill, {}),
5127
- checkedChildren: /* @__PURE__ */ jsx(RiMoonIcon, {})
5139
+ checkedChildren: /* @__PURE__ */ jsx(RiMoonIcon, {}),
5140
+ children: t("preferences.sidebar.sidebarTheme")
5128
5141
  }),
5129
5142
  /* @__PURE__ */ jsx(NumberInputSpinner, {
5130
5143
  min: 40,
@@ -5702,7 +5715,7 @@ function UserMenu({ ...restProps }) {
5702
5715
  }
5703
5716
  if (key === "personal-center") navigate("/personal-center/my-profile");
5704
5717
  };
5705
- const altView = useMemo(() => isWindowsOs() ? "Alt" : "⌥", [isWindowsOs]);
5718
+ const altView = useMemo(() => isWindowsOs() ? "Alt" : "⌥", []);
5706
5719
  const items = [{
5707
5720
  label: t("personal-center:menu.personalCenter"),
5708
5721
  key: "personal-center",
@@ -5973,7 +5986,7 @@ function LayoutMenu({ mode = "inline", autoExpandCurrentMenu, handleMenuSelect,
5973
5986
  isMobile,
5974
5987
  sidebarCollapsed
5975
5988
  ]);
5976
- const handleOpenChange = (keys) => {
5989
+ const handleOpenChange = useCallback((keys) => {
5977
5990
  /**
5978
5991
  * 1. 手风琴模式,点击菜单项,自动关闭其他菜单
5979
5992
  * 2. 非手风琴模式且菜单是收起的,鼠标悬浮菜单自动关闭其他菜单
@@ -5992,7 +6005,12 @@ function LayoutMenu({ mode = "inline", autoExpandCurrentMenu, handleMenuSelect,
5992
6005
  if (currentCloseKey) setOpenKeys(menuParentKeys[currentCloseKey]);
5993
6006
  }
5994
6007
  } else setOpenKeys(keys);
5995
- };
6008
+ }, [
6009
+ accordion,
6010
+ sidebarCollapsed,
6011
+ openKeys,
6012
+ menuParentKeys
6013
+ ]);
5996
6014
  const menuOpenProps = useMemo(() => {
5997
6015
  if (autoExpandCurrentMenu) return {
5998
6016
  openKeys,
@@ -6019,7 +6037,8 @@ function LayoutMenu({ mode = "inline", autoExpandCurrentMenu, handleMenuSelect,
6019
6037
  }, [
6020
6038
  matches,
6021
6039
  sidebarCollapsed,
6022
- getSelectedKeys
6040
+ getSelectedKeys,
6041
+ accordion
6023
6042
  ]);
6024
6043
  return /* @__PURE__ */ jsx(Menu, {
6025
6044
  /**
@@ -6552,7 +6571,6 @@ function useDropdownMenu() {
6552
6571
  }, [
6553
6572
  t,
6554
6573
  activeKey,
6555
- homePath,
6556
6574
  openTabs
6557
6575
  ]);
6558
6576
  /**
@@ -6569,6 +6587,8 @@ function useDropdownMenu() {
6569
6587
  [TabActionKeys.CLOSE_OTHERS]: closeOtherTabs,
6570
6588
  [TabActionKeys.CLOSE_ALL]: closeAllTabs
6571
6589
  }), [
6590
+ refresh,
6591
+ setIsRefresh,
6572
6592
  removeTab,
6573
6593
  closeRightTabs,
6574
6594
  closeLeftTabs,
@@ -6814,7 +6834,7 @@ function LayoutTabbar() {
6814
6834
  hash: ""
6815
6835
  };
6816
6836
  navigate(key + historyState.search + historyState.hash);
6817
- }, [openTabs]);
6837
+ }, [openTabs, navigate]);
6818
6838
  /**
6819
6839
  * 处理标签页编辑(关闭)
6820
6840
  * @param {React.MouseEvent | React.KeyboardEvent | string} key - 被编辑的标签页的key
@@ -6941,7 +6961,8 @@ function LayoutTabbar() {
6941
6961
  location,
6942
6962
  currentRoute,
6943
6963
  setActiveKey,
6944
- addTab
6964
+ addTab,
6965
+ activeKey
6945
6966
  ]);
6946
6967
  return /* @__PURE__ */ jsx("div", {
6947
6968
  className: classes.tabsContainer,
@@ -7023,7 +7044,7 @@ function ContainerLayout() {
7023
7044
  if (screens.lg && !screens.xl) setPreferences("sidebarCollapsed", true);
7024
7045
  else if (screens.xl) setPreferences("sidebarCollapsed", false);
7025
7046
  else if (screens.xs || screens.sm && !screens.md) setPreferences("sidebarCollapsed", false);
7026
- }, [screens]);
7047
+ }, [screens, setPreferences]);
7027
7048
  const sidebarEnableState = useMemo(() => !isTopNav && sidebarEnable, [isTopNav, sidebarEnable]);
7028
7049
  const computedSidebarWidth = useMemo(() => {
7029
7050
  if (isMaximize || isMobile) return 0;
@@ -7049,7 +7070,7 @@ function ContainerLayout() {
7049
7070
  let height = 48;
7050
7071
  if (tabbarEnable) height += 35;
7051
7072
  return height;
7052
- }, [tabbarEnable, 35]);
7073
+ }, [tabbarEnable]);
7053
7074
  useEffect(() => {
7054
7075
  setLayoutHeaderHeight(isMaximize ? 35 : headerWrapperHeight);
7055
7076
  }, [headerWrapperHeight, isMaximize]);
@@ -7934,7 +7955,7 @@ async function generateRoutesFromBackend(backendRoutes) {
7934
7955
  /**
7935
7956
  * 转换路由配置
7936
7957
  * @param route 原始路由配置
7937
- * @param parentPath 父级路径(用于嵌套路由)
7958
+ * @param parentComponentPath 父级组件路径(用于嵌套路由)
7938
7959
  * @returns 转换后的路由配置
7939
7960
  */
7940
7961
  const transformRoute = async (route, parentComponentPath) => {
@@ -8918,8 +8939,6 @@ var init_remember_route = __esmMin((() => {
8918
8939
  * 必须带上部署 base(`resolveSiteBase()`):子路径部署下 `location.href = "/login"`
8919
8940
  * 会跳到宿主机根,而不是 `/oj/login`。不能用 `import.meta.env.BASE_URL`——预构建
8920
8941
  * 产物里它是构建期内联的 `/`。
8921
- *
8922
- * @returns 无返回值
8923
8942
  */
8924
8943
  function goLogin() {
8925
8944
  useAuthStore.getState().reset();
@@ -9022,6 +9041,8 @@ function onRefreshFailed(error) {
9022
9041
  * 订阅者对象应包含 resolve 和 reject 方法。
9023
9042
  *
9024
9043
  * @param subscriber 订阅者对象,包含 resolve 和 reject 方法
9044
+ * @param subscriber.resolve 当 token 刷新成功时调用的函数,传入新的 token
9045
+ * @param subscriber.reject 当 token 刷新失败时调用的函数,传入错误信息
9025
9046
  */
9026
9047
  function addRefreshSubscriber(subscriber) {
9027
9048
  refreshSubscribers.push(subscriber);
@@ -9447,6 +9468,10 @@ function useAccess() {
9447
9468
  * 权限验证组件
9448
9469
  *
9449
9470
  * @param AccessControlProps 权限验证组件的属性
9471
+ * @param AccessControlProps.type 权限类型,默认为 code
9472
+ * @param AccessControlProps.codes 权限值,可以是字符串或字符串数组
9473
+ * @param AccessControlProps.children 有权限时渲染的子组件
9474
+ * @param AccessControlProps.fallback 无权限时显示的内容
9450
9475
  * @returns 若子组件存在,并且传入的权限值有效,则返回子组件;否则返回 null
9451
9476
  */
9452
9477
  function AccessControl({ type = "code", codes, children, fallback }) {
@@ -9800,7 +9825,7 @@ function BasicTable(props) {
9800
9825
  if (enableFooter && fixedFooter) return 40;
9801
9826
  return 0;
9802
9827
  }, [enableFooter, fixedFooter]);
9803
- const getPaginationProps = () => {
9828
+ const getPaginationProps = useCallback(() => {
9804
9829
  if (props.pagination === false) return false;
9805
9830
  return {
9806
9831
  placement: ["bottomStart"],
@@ -9810,7 +9835,7 @@ function BasicTable(props) {
9810
9835
  showTotal: (total) => t("common.pagination", { total }),
9811
9836
  ...props.pagination
9812
9837
  };
9813
- };
9838
+ }, [props.pagination, t]);
9814
9839
  /**
9815
9840
  * @description 计算分页器的高度
9816
9841
  * 如果分页器被禁用,则返回 0,否则根据分页器的大小返回相应的高度
@@ -5,9 +5,9 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <link rel="icon" href="/favicon.ico" />
7
7
  <title>React Antd Admin · Shell</title>
8
- <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' https://modules.cdn.example.com 'wasm-unsafe-eval' 'nonce-NpuwH1G8uW9SNQdyFsPFYQ=='; script-src-attr 'none'; connect-src 'self' data: https://modules.cdn.example.com; style-src 'self' https://modules.cdn.example.com 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; frame-src https://ant.design https://react.dev https://condorheroblog.github.io; form-action 'none'; base-uri 'none'; object-src 'none'; upgrade-insecure-requests" />
8
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' https://modules.cdn.example.com 'wasm-unsafe-eval' 'nonce-KUwgkL5p/qZWtW8VUNiLzg=='; script-src-attr 'none'; connect-src 'self' data: https://modules.cdn.example.com; style-src 'self' https://modules.cdn.example.com 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; frame-src https://ant.design https://react.dev https://condorheroblog.github.io; form-action 'none'; base-uri 'none'; object-src 'none'; upgrade-insecure-requests" />
9
9
 
10
- <script type="importmap" nonce="NpuwH1G8uW9SNQdyFsPFYQ==">{
10
+ <script type="importmap" nonce="KUwgkL5p/qZWtW8VUNiLzg==">{
11
11
  "imports": {
12
12
  "react": "/assets/react.js",
13
13
  "react/jsx-runtime": "/assets/jsx-runtime.js",
@@ -7,9 +7,9 @@
7
7
  "react-router": "7.18.2",
8
8
  "react-router/dom": "7.18.2",
9
9
  "@tanstack/react-query": "5.102.8",
10
- "@oj-module/runtime": "0.2.18",
11
- "@oj-module/runtime/contract": "0.2.18",
12
- "@oj-module/runtime/contract/errors": "0.2.18",
10
+ "@oj-module/runtime": "0.2.23",
11
+ "@oj-module/runtime/contract": "0.2.23",
12
+ "@oj-module/runtime/contract/errors": "0.2.23",
13
13
  "@plane/kit": "0.1.20",
14
14
  "@plane/editor": "0.1.20",
15
15
  "antd": "6.6.1",
@@ -62,3 +62,57 @@ describe("given 纯前端工程(无 api/src)When ojm build 后端", () => {
62
62
  expect(existsSync(join(root, "api", "dist"))).toBe(false);
63
63
  });
64
64
  });
65
+
66
+ /**
67
+ * 假 oj:把 argv 落盘供断言;`build --help` 的输出可切(模拟是否支持 --module-root)。
68
+ * 能力探针会先跑 `build --help`,所以脚本必须区分这两次调用。
69
+ */
70
+ function fakeOjRecording(projectRoot: string, supportsModuleRoot: boolean): void {
71
+ mkdirSync(join(projectRoot, "bin"), { recursive: true });
72
+ const bin = join(projectRoot, "bin", "oj");
73
+ writeFileSync(bin, [
74
+ "#!/bin/sh",
75
+ "if [ \"$1\" = \"build\" ] && [ \"$2\" = \"--help\" ]; then",
76
+ ` ${supportsModuleRoot ? "echo ' --module-root <GLOB> 模块根 glob'" : "echo ' -d, --dir <DIR>'"}`,
77
+ " exit 0",
78
+ "fi",
79
+ "if [ \"$1\" = \"-V\" ]; then echo 'oj 0.1.84'; exit 0; fi",
80
+ "printf '%s\\n' \"$*\" > \"$(dirname \"$0\")/invoked\"",
81
+ "exit 0",
82
+ "",
83
+ ].join("\n"), { mode: 0o755 });
84
+ }
85
+
86
+ /** 新布局工程:src/<name>/{api,web} + 仓库根 config.yaml */
87
+ function newModuleRootProject(): string {
88
+ const root = mkdtempSync(join(tmpdir(), "ojm-build-backend-mr-"));
89
+ mkdirSync(join(root, "src", "wiki", "api"), { recursive: true });
90
+ writeFileSync(join(root, "src", "wiki", "api", "manifest.yaml"), "name: wiki\nversion: 0.1.0\n", "utf-8");
91
+ writeFileSync(join(root, "config.yaml"), "server:\n port: 9778\n", "utf-8");
92
+ return root;
93
+ }
94
+
95
+ describe("given module-root 布局 When ojm build 后端", () => {
96
+ it("then 一次调用、单一输出根:--module-root src + -o api/dist(与 -d 互斥)", async () => {
97
+ const root = newModuleRootProject();
98
+ fakeOjRecording(root, true);
99
+
100
+ expect(await buildBackend(root)).toBe(true);
101
+
102
+ // 逐模块 `-o <tmp>` 会让每个 tmp 各写一份 manifests.yaml,锁与兄弟目录关系全丢
103
+ const invoked = readFileSync(join(root, "bin", "invoked"), "utf-8").trim();
104
+ expect(invoked).toBe(
105
+ `build -c ${join(root, "config.yaml")} --module-root ${join(root, "src")} -o ${join(root, "api", "dist")} --no-minify`,
106
+ );
107
+ expect(existsSync(join(root, "api", "dist", "versions.json"))).toBe(true);
108
+ });
109
+
110
+ it("then oj 缺 --module-root 能力时 fail-fast,不落任何产物", async () => {
111
+ const root = newModuleRootProject();
112
+ fakeOjRecording(root, false);
113
+
114
+ await expect(buildBackend(root)).rejects.toThrow(/不支持 --module-root/);
115
+ expect(existsSync(join(root, "bin", "invoked"))).toBe(false); // 没跑 oj build
116
+ expect(existsSync(join(root, "api", "dist"))).toBe(false);
117
+ });
118
+ });