@oj-module/cli 0.2.17 → 0.2.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (332) 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 +260 -162
  5. package/devkit/menu-rules.md +3 -4
  6. package/devkit/module-dev-and-cdn-playground.md +31 -22
  7. package/devkit/sql-builder-missing-capabilities-pr.md +11 -6
  8. package/package.json +2 -2
  9. package/shell-dist/assets/runtime.js +2 -2
  10. package/shell-dist/index.html +2 -2
  11. package/shell-dist/versions.json +3 -3
  12. package/src/__tests__/build-backend.test.ts +54 -0
  13. package/src/__tests__/layout.test.ts +176 -0
  14. package/src/__tests__/module-list.test.ts +66 -0
  15. package/src/__tests__/oj-caps.test.ts +114 -0
  16. package/src/alias-paths.ts +102 -0
  17. package/src/build.ts +41 -9
  18. package/src/bundle.ts +25 -13
  19. package/src/config.ts +82 -9
  20. package/src/contract/__tests__/market-client.integration.test.ts +2 -2
  21. package/src/contract/check.ts +56 -28
  22. package/src/contract/emit-stub.ts +21 -9
  23. package/src/contract/handler-scan.ts +19 -10
  24. package/src/contract/run.ts +14 -4
  25. package/src/dev.ts +22 -7
  26. package/src/index.ts +1 -1
  27. package/src/info.ts +20 -3
  28. package/src/init.ts +91 -21
  29. package/src/layout.ts +282 -24
  30. package/src/lifecycle/__tests__/bundle.test.ts +16 -1
  31. package/src/lifecycle/__tests__/manifest-contract.test.ts +17 -58
  32. package/src/manifest-contract.ts +2 -25
  33. package/src/market/__tests__/bootstrap.test.ts +17 -9
  34. package/src/market/bootstrap.ts +30 -17
  35. package/src/oj-caps.ts +133 -0
  36. package/src/oj.ts +27 -4
  37. package/src/preview-config.ts +1 -1
  38. package/src/preview.ts +2 -1
  39. package/src/site.ts +2 -2
  40. package/src/usage.ts +6 -6
  41. package/src/vendor.ts +5 -1
  42. package/templates/{api/config.yaml → config.yaml} +13 -8
  43. package/templates/global.d.ts +7 -6
  44. package/templates/init.include.yaml +41 -36
  45. package/templates/package.json +1 -1
  46. package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/installer.ts +4 -0
  47. package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/smoke.ts +21 -2
  48. package/templates/{api/src/_platform → src/_platform/api}/market/api.ts +25 -15
  49. package/templates/{api/src/_platform → src/_platform/api}/solutions/api.ts +7 -3
  50. package/templates/{api/src/auth → src/auth/api}/_shared/login-throttle.ts +2 -2
  51. package/templates/{api/src/auth → src/auth/api}/_shared/session.ts +2 -2
  52. package/templates/{api/src/auth → src/auth/api}/api.ts +1 -1
  53. package/templates/{api/src/auth → src/auth/api}/ldap-login/api.ts +1 -1
  54. package/templates/src/auth/api/manifest.yaml +6 -0
  55. package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/InstallDialog.test.tsx +53 -1
  56. package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/marketClient.fake.ts +6 -0
  57. package/templates/{web/src/oj-market → src/oj-market/web}/api/client.ts +5 -0
  58. package/templates/{web/src/oj-market → src/oj-market/web}/api/defaultClient.ts +7 -1
  59. package/templates/{web/src/oj-market → src/oj-market/web}/components/InstallDialog.tsx +80 -5
  60. package/templates/{web/src/oj-market → src/oj-market/web}/hooks/useMarket.ts +6 -1
  61. package/templates/{web/src/oj-market → src/oj-market/web}/locales/en-US.json +10 -1
  62. package/templates/{web/src/oj-market → src/oj-market/web}/locales/zh-CN.json +10 -1
  63. package/templates/{web/src/oj-market → src/oj-market/web}/types.ts +15 -2
  64. package/templates/{api/src/system → src/system/api}/login-methods/api.ts +2 -2
  65. package/templates/src/system/api/manifest.yaml +6 -0
  66. package/templates/{api/src/system → src/system/api}/oplog/api.ts +4 -4
  67. package/templates/{api/src/system → src/system/api}/permissions/api.ts +3 -3
  68. package/templates/{api/src/system → src/system/api}/roles/api.ts +3 -3
  69. package/templates/{api/src/system → src/system/api}/roles/bindings/api.ts +3 -3
  70. package/templates/{api/src/system → src/system/api}/settings/api.ts +3 -3
  71. package/templates/{api/src/system → src/system/api}/users/api.ts +3 -3
  72. package/templates/{api/src/system → src/system/api}/users/forbidden/api.ts +3 -3
  73. package/templates/{api/src/system → src/system/api}/users/password/api.ts +3 -3
  74. package/templates/{api/src/system → src/system/api}/users/roles/api.ts +3 -3
  75. package/templates/src/tsconfig.json +16 -0
  76. package/templates/{api/tests → tests}/auth-ldap.spec.ts +2 -2
  77. package/templates/{api/tests → tests}/auth-session.spec.ts +3 -3
  78. package/templates/{api/tests → tests}/auth-throttle.spec.ts +1 -1
  79. package/templates/tests/fixture.ts +434 -0
  80. package/templates/{api/tests → tests}/guard.spec.ts +1 -1
  81. package/templates/{api/tests → tests}/lifecycle/atomic-state.spec.ts +3 -3
  82. package/templates/{api/tests → tests}/lifecycle/bootstrap.spec.ts +8 -4
  83. package/templates/{api/tests → tests}/lifecycle/deploy-smoke.spec.ts +31 -8
  84. package/templates/{api/tests → tests}/lifecycle/installer.spec.ts +15 -15
  85. package/templates/{api/tests → tests}/lifecycle/market-api.spec.ts +30 -29
  86. package/templates/{api/tests → tests}/lifecycle/migrate.spec.ts +6 -6
  87. package/templates/{api/tests → tests}/lifecycle/preflight.spec.ts +4 -4
  88. package/templates/{api/tests → tests}/lifecycle/register.spec.ts +1 -1
  89. package/templates/{api/tests → tests}/lifecycle/semver.spec.ts +1 -1
  90. package/templates/{api/tests → tests}/lifecycle/sha384.spec.ts +1 -1
  91. package/templates/{api/tests → tests}/lifecycle/sig-fixtures.spec.ts +1 -1
  92. package/templates/{api/tests → tests}/lifecycle/solutions-api.spec.ts +1 -1
  93. package/templates/{api/tests → tests}/lifecycle/steps-fs.spec.ts +12 -12
  94. package/templates/{api/tests → tests}/lifecycle/text-edit.spec.ts +1 -1
  95. package/templates/{api/tests → tests}/lifecycle/tgz-guard.spec.ts +1 -1
  96. package/templates/{api/tests → tests}/lifecycle/trust.spec.ts +5 -5
  97. package/templates/{api/tests → tests}/lifecycle/uninstall.spec.ts +7 -7
  98. package/templates/{api/tests → tests}/lifecycle/verify-checks.spec.ts +9 -6
  99. package/templates/{api/tests → tests}/lifecycle/verify-report.spec.ts +7 -5
  100. package/templates/{api/tests → tests}/query-utils.spec.ts +1 -1
  101. package/templates/{api/tests → tests}/rbac-audit.spec.ts +1 -1
  102. package/templates/{api/tests → tests}/rbac-contacts.spec.ts +2 -2
  103. package/templates/{api/tests → tests}/rbac-guard.spec.ts +1 -1
  104. package/templates/{api/tests → tests}/rbac-roles.spec.ts +2 -2
  105. package/templates/{api/tests → tests}/rbac-seed.spec.ts +1 -1
  106. package/templates/{api/tests → tests}/rbac-settings.spec.ts +2 -2
  107. package/templates/{api/tests → tests}/rbac-solution-perms.spec.ts +1 -1
  108. package/templates/{api/tests → tests}/rbac-users.spec.ts +3 -3
  109. package/templates/tsconfig.json +5 -1
  110. package/templates/tsconfig.paths.json +19 -0
  111. package/templates/types/oj-modules.d.ts +17 -0
  112. package/templates/vitest.config.ts +22 -0
  113. package/templates/web.config.ts +23 -13
  114. package/vendor/host-versions.json +3 -3
  115. package/templates/api/src/auth/manifest.yaml +0 -5
  116. package/templates/api/src/system/manifest.yaml +0 -5
  117. package/templates/api/tests/fixture.ts +0 -58
  118. package/templates/api/tsconfig.json +0 -8
  119. package/templates/api/vitest.config.ts +0 -12
  120. package/templates/logs/task-events.jsonl +0 -0
  121. /package/templates/{api/.ojm-api-exempt.json → .ojm-api-exempt.json} +0 -0
  122. /package/templates/{api/seed.mysql.sql → seed.mysql.sql} +0 -0
  123. /package/templates/{api/seed.pg.sql → seed.pg.sql} +0 -0
  124. /package/templates/{api/src/_platform → src/_platform/api}/README.md +0 -0
  125. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/atomic.ts +0 -0
  126. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/history.ts +0 -0
  127. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/lock.ts +0 -0
  128. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/semver.ts +0 -0
  129. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/sha384.ts +0 -0
  130. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/state.ts +0 -0
  131. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/deploy-web.ts +0 -0
  132. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/download.ts +0 -0
  133. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/extract.ts +0 -0
  134. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/migrate.ts +0 -0
  135. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/preflight.ts +0 -0
  136. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/register.ts +0 -0
  137. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/reload.ts +0 -0
  138. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/verify.ts +0 -0
  139. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/tasks.ts +0 -0
  140. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/text-edit.ts +0 -0
  141. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/tgz-guard.ts +0 -0
  142. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/trust.ts +0 -0
  143. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/types.ts +0 -0
  144. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/uninstall.ts +0 -0
  145. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/verify-checks.ts +0 -0
  146. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/verify-report.ts +0 -0
  147. /package/templates/{api/src/_platform → src/_platform/api}/_shared/respond.ts +0 -0
  148. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/domain/contacts.ts +0 -0
  149. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/domain/roles.ts +0 -0
  150. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/domain/settings.ts +0 -0
  151. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/domain/solution-perms.ts +0 -0
  152. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/domain/users.ts +0 -0
  153. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/infra/access.ts +0 -0
  154. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/infra/audit.ts +0 -0
  155. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/infra/guard.ts +0 -0
  156. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/infra/oplog.ts +0 -0
  157. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/infra/query.ts +0 -0
  158. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/infra/rate.ts +0 -0
  159. /package/templates/{api/src/_platform → src/_platform/api}/_shared/site-facade.ts +0 -0
  160. /package/templates/{api/src/_platform → src/_platform/api}/contacts/api.ts +0 -0
  161. /package/templates/{api/src/_platform → src/_platform/api}/contacts/detail/api.ts +0 -0
  162. /package/templates/{api/src/_platform → src/_platform/api}/fixtures/l1-test-user.sql +0 -0
  163. /package/templates/{api/src/_platform → src/_platform/api}/manifest.yaml +0 -0
  164. /package/templates/{api/src/_platform → src/_platform/api}/market/bootstrap.ts +0 -0
  165. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0001__create_users.mysql.sql +0 -0
  166. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0001__create_users.pg.sql +0 -0
  167. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0001__create_users.sql +0 -0
  168. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0002__add_users_avatar_base64.mysql.sql +0 -0
  169. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0002__add_users_avatar_base64.sql +0 -0
  170. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0003__users_source_display_name.mysql.sql +0 -0
  171. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0003__users_source_display_name.sql +0 -0
  172. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0004__sys_rbac.mysql.sql +0 -0
  173. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0004__sys_rbac.pg.sql +0 -0
  174. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0004__sys_rbac.sql +0 -0
  175. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0005__users_must_change.mysql.sql +0 -0
  176. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0005__users_must_change.sql +0 -0
  177. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0006__users_is_forbidden.mysql.sql +0 -0
  178. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0006__users_is_forbidden.sql +0 -0
  179. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0007__users_email.sql +0 -0
  180. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0008__sys_refresh_token.mysql.sql +0 -0
  181. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0008__sys_refresh_token.pg.sql +0 -0
  182. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0008__sys_refresh_token.sql +0 -0
  183. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0009__sys_throttle.mysql.sql +0 -0
  184. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0009__sys_throttle.pg.sql +0 -0
  185. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0009__sys_throttle.sql +0 -0
  186. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0010__sys_solution_route.mysql.sql +0 -0
  187. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0010__sys_solution_route.pg.sql +0 -0
  188. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0010__sys_solution_route.sql +0 -0
  189. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0011__sys_setting_group_label.mysql.sql +0 -0
  190. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0011__sys_setting_group_label.sql +0 -0
  191. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0012__market_history.mysql.sql +0 -0
  192. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0012__market_history.pg.sql +0 -0
  193. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0012__market_history.sql +0 -0
  194. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0013__users_profile.sql +0 -0
  195. /package/templates/{api/src/_platform → src/_platform/api}/schema.yaml +0 -0
  196. /package/templates/{api/src/_platform → src/_platform/api}/seed.mysql.sql +0 -0
  197. /package/templates/{api/src/_platform → src/_platform/api}/seed.pg.sql +0 -0
  198. /package/templates/{api/src/_platform → src/_platform/api}/seed.sql +0 -0
  199. /package/templates/{web/src/_platform → src/_platform/web}/__client/api.schemas.ts +0 -0
  200. /package/templates/{web/src/_platform → src/_platform/web}/__client/api.ts +0 -0
  201. /package/templates/{web/src/about → src/about/web}/entry.ts +0 -0
  202. /package/templates/{web/src/about → src/about/web}/locales/en-US.json +0 -0
  203. /package/templates/{web/src/about → src/about/web}/locales/zh-CN.json +0 -0
  204. /package/templates/{web/src/about → src/about/web}/pages/constants.ts +0 -0
  205. /package/templates/{web/src/about → src/about/web}/pages/index.tsx +0 -0
  206. /package/templates/{web/src/access → src/access/web}/entry.ts +0 -0
  207. /package/templates/{web/src/access → src/access/web}/locales/en-US.json +0 -0
  208. /package/templates/{web/src/access → src/access/web}/locales/zh-CN.json +0 -0
  209. /package/templates/{web/src/access → src/access/web}/pages/access-mode/index.tsx +0 -0
  210. /package/templates/{web/src/access → src/access/web}/pages/admin-visible/index.tsx +0 -0
  211. /package/templates/{web/src/access → src/access/web}/pages/button-control/index.tsx +0 -0
  212. /package/templates/{web/src/access → src/access/web}/pages/common-visible/index.tsx +0 -0
  213. /package/templates/{web/src/access → src/access/web}/pages/page-control/index.tsx +0 -0
  214. /package/templates/{api/src/auth → src/auth/api}/_shared/ldap-login.ts +0 -0
  215. /package/templates/{api/src/auth → src/auth/api}/_shared/ldap.ts +0 -0
  216. /package/templates/{web/src/auth → src/auth/web}/__client/api.schemas.ts +0 -0
  217. /package/templates/{web/src/auth → src/auth/web}/__client/api.ts +0 -0
  218. /package/templates/{api/src/demo → src/demo/api}/manifest.yaml +0 -0
  219. /package/templates/{api/src/demo → src/demo/api}/migrations/0001__create_todos.sql +0 -0
  220. /package/templates/{api/src/demo → src/demo/api}/schema.yaml +0 -0
  221. /package/templates/{api/src/demo → src/demo/api}/seed.sql +0 -0
  222. /package/templates/{api/src/demo → src/demo/api}/todos/api.ts +0 -0
  223. /package/templates/{web/src/demo → src/demo/web}/__client/api.schemas.ts +0 -0
  224. /package/templates/{web/src/demo → src/demo/web}/__client/api.ts +0 -0
  225. /package/templates/{web/src/demo → src/demo/web}/entry.ts +0 -0
  226. /package/templates/{web/src/demo → src/demo/web}/locales/en-US.json +0 -0
  227. /package/templates/{web/src/demo → src/demo/web}/locales/zh-CN.json +0 -0
  228. /package/templates/{web/src/demo → src/demo/web}/pages/index.tsx +0 -0
  229. /package/templates/{web/src/exception → src/exception/web}/entry.ts +0 -0
  230. /package/templates/{web/src/exception → src/exception/web}/locales/en-US.json +0 -0
  231. /package/templates/{web/src/exception → src/exception/web}/locales/zh-CN.json +0 -0
  232. /package/templates/{web/src/exception → src/exception/web}/pages/403/index.tsx +0 -0
  233. /package/templates/{web/src/exception → src/exception/web}/pages/404/index.tsx +0 -0
  234. /package/templates/{web/src/exception → src/exception/web}/pages/500/index.tsx +0 -0
  235. /package/templates/{web/src/exception → src/exception/web}/pages/unknown-component/index.tsx +0 -0
  236. /package/templates/{api/src/home → src/home/api}/line/api.ts +0 -0
  237. /package/templates/{api/src/home → src/home/api}/manifest.yaml +0 -0
  238. /package/templates/{api/src/home → src/home/api}/migrations/0001__create_home_tables.sql +0 -0
  239. /package/templates/{api/src/home → src/home/api}/pie/api.ts +0 -0
  240. /package/templates/{api/src/home → src/home/api}/schema.yaml +0 -0
  241. /package/templates/{api/src/home → src/home/api}/seed.sql +0 -0
  242. /package/templates/{web/src/home → src/home/web}/__client/api.schemas.ts +0 -0
  243. /package/templates/{web/src/home → src/home/web}/__client/api.ts +0 -0
  244. /package/templates/{web/src/home → src/home/web}/entry.ts +0 -0
  245. /package/templates/{web/src/home → src/home/web}/locales/en-US.json +0 -0
  246. /package/templates/{web/src/home → src/home/web}/locales/zh-CN.json +0 -0
  247. /package/templates/{web/src/home → src/home/web}/pages/components/bar-chart.tsx +0 -0
  248. /package/templates/{web/src/home → src/home/web}/pages/components/card-list.tsx +0 -0
  249. /package/templates/{web/src/home → src/home/web}/pages/components/line-chart.tsx +0 -0
  250. /package/templates/{web/src/home → src/home/web}/pages/components/pie-chart.tsx +0 -0
  251. /package/templates/{web/src/home → src/home/web}/pages/index.tsx +0 -0
  252. /package/templates/{web/src/login → src/login/web}/entry.ts +0 -0
  253. /package/templates/{web/src/login → src/login/web}/locales/en-US.json +0 -0
  254. /package/templates/{web/src/login → src/login/web}/locales/zh-CN.json +0 -0
  255. /package/templates/{web/src/login → src/login/web}/pages/change-password.tsx +0 -0
  256. /package/templates/{web/src/login → src/login/web}/pages/login.tsx +0 -0
  257. /package/templates/{api/src/notification → src/notification/api}/_shared/notify.ts +0 -0
  258. /package/templates/{api/src/notification → src/notification/api}/api.ts +0 -0
  259. /package/templates/{api/src/notification → src/notification/api}/del/api.ts +0 -0
  260. /package/templates/{api/src/notification → src/notification/api}/manifest.yaml +0 -0
  261. /package/templates/{api/src/notification → src/notification/api}/migrations/0001__create_notifications.sql +0 -0
  262. /package/templates/{api/src/notification → src/notification/api}/migrations/0002__notifications_user.sql +0 -0
  263. /package/templates/{api/src/notification → src/notification/api}/migrations/0003__notifications_broadcast.sql +0 -0
  264. /package/templates/{api/src/notification → src/notification/api}/migrations/0004__notifications_broadcast_hidden.sql +0 -0
  265. /package/templates/{api/src/notification → src/notification/api}/schema.yaml +0 -0
  266. /package/templates/{api/src/notification → src/notification/api}/seed.sql +0 -0
  267. /package/templates/{web/src/notification → src/notification/web}/__client/api.schemas.ts +0 -0
  268. /package/templates/{web/src/notification → src/notification/web}/__client/api.ts +0 -0
  269. /package/templates/{web/src/notification → src/notification/web}/entry.ts +0 -0
  270. /package/templates/{web/src/notification → src/notification/web}/locales/en-US.json +0 -0
  271. /package/templates/{web/src/notification → src/notification/web}/locales/zh-CN.json +0 -0
  272. /package/templates/{web/src/notification → src/notification/web}/pages/center/index.tsx +0 -0
  273. /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/CardGrid.test.tsx +0 -0
  274. /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/HistoryDrawer.test.tsx +0 -0
  275. /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/RollbackDialog.test.tsx +0 -0
  276. /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/UninstallConfirm.test.tsx +0 -0
  277. /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/poll.test.ts +0 -0
  278. /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/render.tsx +0 -0
  279. /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/runtime-stub.ts +0 -0
  280. /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/semver.test.ts +0 -0
  281. /package/templates/{web/src/oj-market → src/oj-market/web}/api/MarketClientProvider.tsx +0 -0
  282. /package/templates/{web/src/oj-market → src/oj-market/web}/api/context.ts +0 -0
  283. /package/templates/{web/src/oj-market → src/oj-market/web}/components/CardGrid.tsx +0 -0
  284. /package/templates/{web/src/oj-market → src/oj-market/web}/components/CardItem.tsx +0 -0
  285. /package/templates/{web/src/oj-market → src/oj-market/web}/components/DetailDrawer.tsx +0 -0
  286. /package/templates/{web/src/oj-market → src/oj-market/web}/components/HistoryDrawer.tsx +0 -0
  287. /package/templates/{web/src/oj-market → src/oj-market/web}/components/RollbackDialog.tsx +0 -0
  288. /package/templates/{web/src/oj-market → src/oj-market/web}/components/UninstallConfirm.tsx +0 -0
  289. /package/templates/{web/src/oj-market → src/oj-market/web}/entry.ts +0 -0
  290. /package/templates/{web/src/oj-market → src/oj-market/web}/pages/MarketApp.tsx +0 -0
  291. /package/templates/{web/src/oj-market → src/oj-market/web}/utils/poll.ts +0 -0
  292. /package/templates/{web/src/oj-market → src/oj-market/web}/utils/semver.ts +0 -0
  293. /package/templates/{web/src/outside → src/outside/web}/entry.ts +0 -0
  294. /package/templates/{web/src/outside → src/outside/web}/locales/en-US.json +0 -0
  295. /package/templates/{web/src/outside → src/outside/web}/locales/zh-CN.json +0 -0
  296. /package/templates/{api/src/personal-center → src/personal-center/api}/manifest.yaml +0 -0
  297. /package/templates/{api/src/personal-center → src/personal-center/api}/profile/api.ts +0 -0
  298. /package/templates/{api/src/personal-center → src/personal-center/api}/upload/api.ts +0 -0
  299. /package/templates/{web/src/personal-center → src/personal-center/web}/__client/api.schemas.ts +0 -0
  300. /package/templates/{web/src/personal-center → src/personal-center/web}/__client/api.ts +0 -0
  301. /package/templates/{web/src/personal-center → src/personal-center/web}/entry.ts +0 -0
  302. /package/templates/{web/src/personal-center → src/personal-center/web}/locales/en-US.json +0 -0
  303. /package/templates/{web/src/personal-center → src/personal-center/web}/locales/zh-CN.json +0 -0
  304. /package/templates/{web/src/personal-center → src/personal-center/web}/pages/my-profile/index.tsx +0 -0
  305. /package/templates/{web/src/route-nest → src/route-nest/web}/entry.ts +0 -0
  306. /package/templates/{web/src/route-nest → src/route-nest/web}/locales/en-US.json +0 -0
  307. /package/templates/{web/src/route-nest → src/route-nest/web}/locales/zh-CN.json +0 -0
  308. /package/templates/{web/src/route-nest → src/route-nest/web}/pages/menu1/menu1-1/index.tsx +0 -0
  309. /package/templates/{web/src/route-nest → src/route-nest/web}/pages/menu1/menu1-2/index.tsx +0 -0
  310. /package/templates/{web/src/route-nest → src/route-nest/web}/pages/menu2/index.tsx +0 -0
  311. /package/templates/{web/src → src}/styles.css +0 -0
  312. /package/templates/{web/src/system → src/system/web}/__client/api.schemas.ts +0 -0
  313. /package/templates/{web/src/system → src/system/web}/__client/api.ts +0 -0
  314. /package/templates/{web/src/system → src/system/web}/entry.ts +0 -0
  315. /package/templates/{web/src/system → src/system/web}/locales/en-US.json +0 -0
  316. /package/templates/{web/src/system → src/system/web}/locales/zh-CN.json +0 -0
  317. /package/templates/{web/src/system → src/system/web}/pages/errors.ts +0 -0
  318. /package/templates/{web/src/system → src/system/web}/pages/forbidden.tsx +0 -0
  319. /package/templates/{web/src/system → src/system/web}/pages/login-methods/index.tsx +0 -0
  320. /package/templates/{web/src/system → src/system/web}/pages/oplog/index.tsx +0 -0
  321. /package/templates/{web/src/system → src/system/web}/pages/roles/index.tsx +0 -0
  322. /package/templates/{web/src/system → src/system/web}/pages/settings/index.tsx +0 -0
  323. /package/templates/{web/src/system → src/system/web}/pages/users/index.tsx +0 -0
  324. /package/templates/{web/src/system → src/system/web}/pages/users/roles.tsx +0 -0
  325. /package/templates/{api/src/web → src/web/api}/get-async-routes/api.ts +0 -0
  326. /package/templates/{api/src/web → src/web/api}/hello/api.ts +0 -0
  327. /package/templates/{api/src/web → src/web/api}/manifest.yaml +0 -0
  328. /package/templates/{api/src/web → src/web/api}/user-info/api.ts +0 -0
  329. /package/templates/{web/src → src/web}/web/__client/api.schemas.ts +0 -0
  330. /package/templates/{web/src → src/web}/web/__client/api.ts +0 -0
  331. /package/templates/{api/tests → tests}/l1-builder.test.ts +0 -0
  332. /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
 
@@ -156,7 +156,6 @@ export default defineModule({
156
156
  - 必须**嵌套**在 `menu.main`,不能平铺成 `"<模块名>:menu.main": "..."`(i18next 点路径取值)。
157
157
  - 缺这条 key → 构建期契约校验直接 fail(构建期 fail-fast)。
158
158
  - 普通子项(space / status / collections 等)的 `title` 仍各自匹配 `<模块名>:menu.*`(如 `wiki:menu.space`)。
159
- - 旧设计的 `menu.group` key 已废弃:`menu.main` 取代它成为根标题与管理子组标题的统一真源。
160
159
 
161
160
  ---
162
161
 
@@ -191,7 +190,7 @@ handle: { currentActiveMenu: wikiPath("space") } // → "/wiki/space",随模
191
190
  推荐在模块内放一个 `paths.ts`:
192
191
 
193
192
  ```ts
194
- // web/src/<模块>/paths.ts
193
+ // src/<模块>/web/paths.ts
195
194
  export const WIKI_ROOT = "wiki"; // 模块挂载根(唯一定义处)
196
195
  export const wikiBase = `/${WIKI_ROOT}`;
197
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.17",
4
+ "version": "0.2.20",
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.17",
52
+ "@oj-module/runtime": "0.2.20",
53
53
  "@redocly/cli": "^2.51.0",
54
54
  "@tailwindcss/vite": "^4.3.0",
55
55
  "esbuild": "^0.28.2",
@@ -303,10 +303,10 @@ function getAppInfo() {
303
303
  return {
304
304
  "pkg": {
305
305
  "name": "@oj-module/runtime",
306
- "version": "0.2.17",
306
+ "version": "0.2.20",
307
307
  "license": "MIT"
308
308
  },
309
- "lastBuildTime": "2026-10-21 21:54:09"
309
+ "lastBuildTime": "2026-10-22 12:17:23"
310
310
  };
311
311
  }
312
312
  var init_get_app_info = __esmMin((() => {}));
@@ -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-NKdPzzWoLWlLgH/F6SaXrg=='; 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-oCauQheXzDS2E/yd7smCtg=='; 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="NKdPzzWoLWlLgH/F6SaXrg==">{
10
+ <script type="importmap" nonce="oCauQheXzDS2E/yd7smCtg==">{
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.17",
11
- "@oj-module/runtime/contract": "0.2.17",
12
- "@oj-module/runtime/contract/errors": "0.2.17",
10
+ "@oj-module/runtime": "0.2.20",
11
+ "@oj-module/runtime/contract": "0.2.20",
12
+ "@oj-module/runtime/contract/errors": "0.2.20",
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
+ });
@@ -0,0 +1,176 @@
1
+ // layout spec:三种布局的探测、互斥、模块枚举与 config 落点。
2
+ //
3
+ // 这些取值是 dev/build/preview/bundle 的共同底座:探测错一次,`ojm build` 会构建 A
4
+ // 而 `ojm dev` 预览 B,或产物写进空目录后表现为「health 200、模块 0 个」的假绿。
5
+ import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
6
+ import { tmpdir } from "node:os";
7
+ import { join } from "node:path";
8
+ import { describe, expect, it } from "vitest";
9
+
10
+ import {
11
+ apiSrcOf,
12
+ moduleDir,
13
+ resolveCertDir,
14
+ resolveConfigPath,
15
+ resolveLayout,
16
+ scanModules,
17
+ webSrcOf,
18
+ } from "../layout";
19
+
20
+ function newRoot(): string {
21
+ return mkdtempSync(join(tmpdir(), "ojm-layout-"));
22
+ }
23
+
24
+ function writeFile(root: string, rel: string, content = ""): void {
25
+ const abs = join(root, rel);
26
+ mkdirSync(join(abs, ".."), { recursive: true });
27
+ writeFileSync(abs, content, "utf-8");
28
+ }
29
+
30
+ /** 新布局的最小工程:src/wiki/{api/manifest.yaml, web/entry.ts} + 根 config.yaml */
31
+ function newModuleRootProject(): string {
32
+ const root = newRoot();
33
+ writeFile(root, "src/wiki/api/manifest.yaml", "name: wiki\nversion: 0.1.0\n");
34
+ writeFile(root, "src/wiki/web/entry.ts", "export default {};\n");
35
+ writeFile(root, "config.yaml", "server:\n port: 9778\n");
36
+ return root;
37
+ }
38
+
39
+ describe("resolveLayout / module-root", () => {
40
+ it("src/<name>/{api,web} 命中 module-root,产物根语义不变", () => {
41
+ const root = newModuleRootProject();
42
+ const layout = resolveLayout(root);
43
+
44
+ expect(layout.kind).toBe("module-root");
45
+ expect(layout.modulesSrc).toBe(join(root, "src"));
46
+ expect(layout.distDir).toBe(join(root, "web", "dist"));
47
+ expect(layout.apiDist).toBe(join(root, "api", "dist"));
48
+ expect(layout.watchTarget).toBe(join(root, "src"));
49
+ // --module-root 取「源码树根目录」(oj 在其子树内递归找 manifest.yaml)
50
+ expect(layout.moduleRoot).toBe(join(root, "src"));
51
+ });
52
+
53
+ it("web/dist 存在不会被误判成 flat 布局", () => {
54
+ const root = newModuleRootProject();
55
+ mkdirSync(join(root, "web", "dist", "modules", "wiki", "0.1.0"), { recursive: true });
56
+ expect(resolveLayout(root).kind).toBe("module-root");
57
+ });
58
+
59
+ it("枚举真源是标记文件:一侧可缺省,纯后端 / 纯前端各算一个模块", () => {
60
+ const root = newRoot();
61
+ writeFile(root, "src/_platform/api/manifest.yaml", "name: _platform\nversion: 0.1.0\n");
62
+ writeFile(root, "src/auth/api/manifest.yaml", "name: auth\nversion: 0.1.0\n");
63
+ writeFile(root, "src/about/web/entry.ts", "export default {};\n");
64
+
65
+ // 按名字排序(不依赖 readdir 顺序,产物才可复现)
66
+ const modules = scanModules(resolveLayout(root));
67
+ expect(modules.map(m => m.name)).toEqual(["_platform", "about", "auth"]);
68
+ expect(modules.map(m => !!m.apiSrc)).toEqual([true, false, true]);
69
+ expect(modules.map(m => !!m.webSrc)).toEqual([false, true, false]);
70
+ expect(modules[0].apiSrc).toBe(join(root, "src", "_platform", "api"));
71
+ expect(modules[1].webSrc).toBe(join(root, "src", "about", "web"));
72
+ });
73
+
74
+ it("非模块目录(既无 api/ 也无 web/,如共享库)静默跳过", () => {
75
+ const root = newRoot();
76
+ writeFile(root, "src/wiki/api/manifest.yaml", "name: wiki\nversion: 0.1.0\n");
77
+ writeFile(root, "src/shared/util.ts", "export const x = 1;\n");
78
+ expect(scanModules(resolveLayout(root)).map(m => m.name)).toEqual(["wiki"]);
79
+ });
80
+
81
+ it("有 api/ 却缺 manifest.yaml:报错并指名目录(不静默当成非模块)", () => {
82
+ const root = newRoot();
83
+ writeFile(root, "src/wiki/api/manifest.yaml", "name: wiki\nversion: 0.1.0\n");
84
+ writeFile(root, "src/broken/api/index.js", "export {};\n");
85
+ expect(() => resolveLayout(root)).toThrow(/src\/broken\//);
86
+ });
87
+
88
+ it("src/ 下没有任何模块标记时不算 module-root(避免把脚本 src/ 当模块树)", () => {
89
+ const root = newRoot();
90
+ mkdirSync(join(root, "src", "scripts"), { recursive: true });
91
+ expect(() => resolveLayout(root)).toThrow(/未发现模块源码/);
92
+ });
93
+ });
94
+
95
+ describe("resolveLayout / 历史布局", () => {
96
+ it("web/src 仍识别为 new,模块根不在 src/ 之下", () => {
97
+ const root = newRoot();
98
+ writeFile(root, "web/src/wiki/entry.ts", "export default {};\n");
99
+ writeFile(root, "api/src/wiki/manifest.yaml", "name: wiki\nversion: 0.1.0\n");
100
+
101
+ const layout = resolveLayout(root);
102
+ expect(layout.kind).toBe("new");
103
+ expect(layout.modulesSrc).toBe(join(root, "web", "src"));
104
+ expect(layout.moduleRoot).toBeUndefined();
105
+ expect(apiSrcOf(layout, "wiki")).toBe(join(root, "api", "src", "wiki"));
106
+ expect(webSrcOf(layout, "wiki")).toBe(join(root, "web", "src", "wiki"));
107
+ });
108
+
109
+ it("web/<name>/entry.ts 识别为 flat,产物落 build/", () => {
110
+ const root = newRoot();
111
+ writeFile(root, "web/home/entry.ts", "export default {};\n");
112
+ writeFile(root, "web/home/__client/api.ts", "export {};\n");
113
+
114
+ const layout = resolveLayout(root);
115
+ expect(layout.kind).toBe("flat");
116
+ expect(layout.modulesSrc).toBe(join(root, "web"));
117
+ expect(layout.distDir).toBe(join(root, "build"));
118
+ expect(webSrcOf(layout, "home")).toBe(join(root, "web", "home"));
119
+ });
120
+
121
+ it("历史布局不支持 scanModules(顺序由 web.config.ts 决定)", () => {
122
+ const root = newRoot();
123
+ writeFile(root, "web/src/wiki/entry.ts", "export default {};\n");
124
+ expect(() => scanModules(resolveLayout(root))).toThrow(/只适用于 module-root/);
125
+ });
126
+ });
127
+
128
+ describe("resolveLayout / 互斥与缺省", () => {
129
+ it("两套源码树同时存在时报错,而不是静默选一个", () => {
130
+ const root = newModuleRootProject();
131
+ writeFile(root, "web/src/legacy/entry.ts", "export default {};\n");
132
+ expect(() => resolveLayout(root)).toThrow(/同时存在两套模块源码树/);
133
+ });
134
+
135
+ it("什么布局都没有时报错并列出两种合法形状", () => {
136
+ const root = newRoot();
137
+ expect(() => resolveLayout(root)).toThrow(/未发现模块源码/);
138
+ });
139
+ });
140
+
141
+ describe("config 落点", () => {
142
+ it("module-root:config 与证书目录都在仓库根", () => {
143
+ const root = newModuleRootProject();
144
+ expect(resolveConfigPath(root)).toBe(join(root, "config.yaml"));
145
+ expect(resolveCertDir(root)).toBe(join(root, "config"));
146
+ });
147
+
148
+ it("module-root 仍把 config 留在 api/ 时报错(回退会让路由静默消失)", () => {
149
+ const root = newRoot();
150
+ writeFile(root, "src/wiki/api/manifest.yaml", "name: wiki\nversion: 0.1.0\n");
151
+ writeFile(root, "api/config.yaml", "server:\n port: 9778\n");
152
+ expect(() => resolveConfigPath(root)).toThrow(/仍在 api\/ 下/);
153
+ });
154
+
155
+ it("历史布局:config 与证书目录仍在 api/ 下", () => {
156
+ const root = newRoot();
157
+ writeFile(root, "web/src/wiki/entry.ts", "export default {};\n");
158
+ writeFile(root, "api/config.yaml", "server:\n port: 9778\n");
159
+ expect(resolveConfigPath(root)).toBe(join(root, "api", "config.yaml"));
160
+ expect(resolveCertDir(root)).toBe(join(root, "api", "config"));
161
+ });
162
+
163
+ it("空目录(init 目标)不报错:落默认位置供脚手架写入", () => {
164
+ const root = newRoot();
165
+ expect(resolveConfigPath(root)).toBe(join(root, "api", "config.yaml"));
166
+ });
167
+ });
168
+
169
+ describe("路径取值函数", () => {
170
+ it("module-root:moduleDir / apiSrcOf / webSrcOf 都在 src/<name>/ 之下", () => {
171
+ const layout = resolveLayout(newModuleRootProject());
172
+ expect(moduleDir(layout, "wiki")).toBe(join(layout.modulesSrc, "wiki"));
173
+ expect(apiSrcOf(layout, "wiki")).toBe(join(layout.modulesSrc, "wiki", "api"));
174
+ expect(webSrcOf(layout, "wiki")).toBe(join(layout.modulesSrc, "wiki", "web"));
175
+ });
176
+ });
@@ -0,0 +1,66 @@
1
+ // 前端模块清单 spec(设计 D3:扫描为准 + web.config.ts 可选覆盖)。
2
+ //
3
+ // 关键不变量是**并集而不是替换**:新增一个模块目录后忘了登记,也必须被构建;
4
+ // 反过来登记了一个已经不存在的目录,必须报错而不是静默少一个模块。
5
+ import { describe, expect, it } from "vitest";
6
+
7
+ import { mergeModuleList } from "../config";
8
+
9
+ const ROOT = "/proj";
10
+
11
+ function scanned(...names: string[]) {
12
+ return names.map(name => ({ name, webSrc: `${ROOT}/src/${name}/web` }));
13
+ }
14
+
15
+ describe("mergeModuleList", () => {
16
+ it("无覆盖时按扫描顺序(layout 已按名字排序)", () => {
17
+ expect(mergeModuleList(scanned("about", "home", "wiki"), undefined, ROOT)).toEqual([
18
+ { name: "about", entry: "src/about/web/entry.ts", enabled: true },
19
+ { name: "home", entry: "src/home/web/entry.ts", enabled: true },
20
+ { name: "wiki", entry: "src/wiki/web/entry.ts", enabled: true },
21
+ ]);
22
+ });
23
+
24
+ it("覆盖项按声明顺序排前,未登记的按扫描顺序追加(并集)", () => {
25
+ const list = mergeModuleList(scanned("about", "home", "wiki"), [
26
+ { name: "home", entry: "ignored" },
27
+ { name: "wiki", entry: "ignored" },
28
+ ], ROOT);
29
+ expect(list.map(m => m.name)).toEqual(["home", "wiki", "about"]);
30
+ });
31
+
32
+ it("enabled:false 仍生效(覆盖项与未登记项都按自己的声明走)", () => {
33
+ const list = mergeModuleList(scanned("about", "home"), [
34
+ { name: "home", entry: "x", enabled: false },
35
+ ], ROOT);
36
+ expect(list).toEqual([
37
+ { name: "home", entry: "src/home/web/entry.ts", enabled: false },
38
+ { name: "about", entry: "src/about/web/entry.ts", enabled: true },
39
+ ]);
40
+ });
41
+
42
+ it("登记了不存在的模块 → 报错并提示以目录为准", () => {
43
+ expect(() => mergeModuleList(scanned("home"), [
44
+ { name: "gone", entry: "src/gone/web/entry.ts" },
45
+ ], ROOT)).toThrow(/src\/gone\/web\/entry\.ts 不存在/);
46
+ });
47
+
48
+ it("纯后端模块(无 web/entry.ts)不进前端清单", () => {
49
+ const list = mergeModuleList([
50
+ { name: "auth" }, // 纯后端:只有 api/manifest.yaml,扫描结果没有 webSrc
51
+ { name: "home", webSrc: `${ROOT}/src/home/web` },
52
+ ], undefined, ROOT);
53
+ expect(list.map(m => m.name)).toEqual(["home"]);
54
+ });
55
+
56
+ it("登记一个纯后端模块也算错(该目录没有前端入口)", () => {
57
+ expect(() => mergeModuleList([
58
+ { name: "auth" },
59
+ ], [{ name: "auth", entry: "x" }], ROOT)).toThrow(/不存在/);
60
+ });
61
+
62
+ it("entry 用相对工程根的路径(与 web.config.ts 历史口径一致)", () => {
63
+ const [only] = mergeModuleList(scanned("wiki"), [{ name: "wiki", entry: "/abs/should-be-ignored" }], ROOT);
64
+ expect(only.entry).toBe("src/wiki/web/entry.ts");
65
+ });
66
+ });