@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
@@ -0,0 +1,29 @@
1
+ // BDD:脚手架 package.json scripts 与模板的双写守卫(T3,发布工程师评审建议)。
2
+ // 生成器(init.ts generatePackageJson)与模板(templates/package.json)各写一份 scripts,
3
+ // 人肉同步必漂移(实证:typecheck:api 曾漏发生成器)——钉成测试:生成 scripts 的每个键
4
+ // 都必须存在于模板 scripts(值一致),新增 script 先改模板再改生成器。
5
+ import { readFileSync } from "node:fs";
6
+ import { dirname, join } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ import { describe, expect, it } from "vitest";
9
+ import { generatePackageJson } from "../init";
10
+
11
+ const cliRoot = join(dirname(fileURLToPath(import.meta.url)), "../..");
12
+
13
+ describe("脚手架 package.json scripts 双写守卫(T3)", () => {
14
+ it("生成 scripts 的每个键都存在于模板且值一致", () => {
15
+ const template = JSON.parse(readFileSync(join(cliRoot, "templates/package.json"), "utf-8"));
16
+ const generated = generatePackageJson(cliRoot, "guard-check");
17
+ for (const [key, value] of Object.entries(generated.scripts)) {
18
+ expect(template.scripts[key], `生成器有而模板无的 script:${key}(先改 templates/package.json)`)
19
+ .toBe(value);
20
+ }
21
+ });
22
+
23
+ it("多方案 scripts 齐备:new / bundle / typecheck:api", () => {
24
+ const generated = generatePackageJson(cliRoot, "guard-check");
25
+ expect(generated.scripts.new).toBe("ojm new");
26
+ expect(generated.scripts.bundle).toBe("ojm build --bundle");
27
+ expect(generated.scripts["typecheck:api"]).toBe("tsc --noEmit -p src/tsconfig.json");
28
+ });
29
+ });
@@ -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
+ });
@@ -0,0 +1,114 @@
1
+ // oj 能力探针 spec:双探的第二探(设计 202610220010 §7)。
2
+ //
3
+ // 失败方式很隐蔽:能力缺失却在构建入口回落到旧布局,会得到「源码搬了、产物还是旧的」
4
+ // 半坏状态。这里钉住三件事:解析只看整词旗标、缓存按二进制指纹失效、缺失即抛且带版本。
5
+ import { mkdirSync, mkdtempSync, readFileSync, statSync, utimesSync, writeFileSync } from "node:fs";
6
+ import { tmpdir } from "node:os";
7
+ import { join } from "node:path";
8
+ import { describe, expect, it } from "vitest";
9
+
10
+ import { CAPS_FILE, probeOjCaps, readCapsCache, requireModuleRootCapability, writeCapsCache } from "../oj-caps";
11
+
12
+ function newProject(): { root: string, ojBin: string } {
13
+ const root = mkdtempSync(join(tmpdir(), "ojm-caps-"));
14
+ mkdirSync(join(root, "bin"), { recursive: true });
15
+ const ojBin = join(root, "bin", "oj");
16
+ writeFileSync(ojBin, "#!/bin/sh\necho fake\n", { mode: 0o755 });
17
+ return { root, ojBin };
18
+ }
19
+
20
+ const HELP_WITH = "Options:\n -d, --dir <DIR> 源码目录\n --module-root <GLOB> 模块根 glob\n";
21
+ const HELP_WITHOUT = "Options:\n -d, --dir <DIR> 源码目录\n";
22
+
23
+ describe("probeOjCaps", () => {
24
+ it("帮助里有 --module-root 即支持", () => {
25
+ const { ojBin } = newProject();
26
+ expect(probeOjCaps(ojBin, () => HELP_WITH).moduleRoot).toBe(true);
27
+ });
28
+
29
+ it("帮助里没有则不支持", () => {
30
+ const { ojBin } = newProject();
31
+ expect(probeOjCaps(ojBin, () => HELP_WITHOUT).moduleRoot).toBe(false);
32
+ });
33
+
34
+ it("同名前缀不算命中(--module-rooted 之类的说明文字)", () => {
35
+ const { ojBin } = newProject();
36
+ expect(probeOjCaps(ojBin, () => "--module-rooted 是别的意思\n").moduleRoot).toBe(false);
37
+ });
38
+
39
+ it("探测本身失败时报错带处置命令", () => {
40
+ const { ojBin } = newProject();
41
+ expect(() => probeOjCaps(ojBin, () => {
42
+ throw new Error("boom");
43
+ })).toThrow(/ojm vendor/);
44
+ });
45
+ });
46
+
47
+ describe("能力缓存", () => {
48
+ it("写入后能读回", () => {
49
+ const { ojBin } = newProject();
50
+ writeCapsCache(ojBin, { moduleRoot: true });
51
+ expect(readCapsCache(ojBin)).toEqual({ moduleRoot: true });
52
+ });
53
+
54
+ it("换二进制(size 变)后缓存失效", () => {
55
+ const { ojBin } = newProject();
56
+ writeCapsCache(ojBin, { moduleRoot: true });
57
+ writeFileSync(ojBin, "#!/bin/sh\necho replaced-and-longer\n", { mode: 0o755 });
58
+ expect(readCapsCache(ojBin)).toBeNull();
59
+ });
60
+
61
+ it("mtime 变(同尺寸手替)后缓存失效", () => {
62
+ const { ojBin } = newProject();
63
+ writeCapsCache(ojBin, { moduleRoot: true });
64
+ const future = new Date(Date.now() + 60_000);
65
+ utimesSync(ojBin, future, future);
66
+ expect(readCapsCache(ojBin)).toBeNull();
67
+ });
68
+
69
+ it("缓存文件损坏不影响主流程(视为未缓存)", () => {
70
+ const { root, ojBin } = newProject();
71
+ writeFileSync(join(root, "bin", CAPS_FILE), "{not json", "utf-8");
72
+ expect(readCapsCache(ojBin)).toBeNull();
73
+ });
74
+ });
75
+
76
+ describe("requireModuleRootCapability", () => {
77
+ it("支持时通过并落缓存,第二次不再探测", () => {
78
+ const { root } = newProject();
79
+ let calls = 0;
80
+ const run = () => {
81
+ calls += 1;
82
+ return HELP_WITH;
83
+ };
84
+ expect(requireModuleRootCapability(root, run).moduleRoot).toBe(true);
85
+ expect(requireModuleRootCapability(root, run).moduleRoot).toBe(true);
86
+ expect(calls).toBe(1);
87
+ expect(readFileSync(join(root, "bin", CAPS_FILE), "utf-8")).toContain("\"moduleRoot\": true");
88
+ });
89
+
90
+ it("二进制缺失时报错指向 ojm vendor", () => {
91
+ const { root } = newProject();
92
+ const run = () => HELP_WITH;
93
+ expect(() => requireModuleRootCapability(join(root, "nope"), run)).toThrow(/ojm vendor/);
94
+ });
95
+
96
+ it("能力缺失时抛错,文案含现场版本与两种处置", () => {
97
+ const { root } = newProject();
98
+ const run = (_bin: string, args: string[]) =>
99
+ args[0] === "-V" ? "oj 0.1.84\n" : HELP_WITHOUT;
100
+ expect(() => requireModuleRootCapability(root, run)).toThrow(/oj 0\.1\.84/);
101
+ expect(() => requireModuleRootCapability(root, run)).toThrow(/ojm vendor/);
102
+ });
103
+
104
+ it("能力缺失也写缓存(避免每次构建重探),换二进制后指纹变化自动重探", () => {
105
+ const { root, ojBin } = newProject();
106
+ expect(() => requireModuleRootCapability(root, (_b, args) => (args[0] === "-V" ? "oj 0.1.84" : HELP_WITHOUT))).toThrow();
107
+ expect(readCapsCache(ojBin)?.moduleRoot).toBe(false);
108
+ // 二进制更新后指纹变化 → 重探,能力补齐即放行
109
+ const before = statSync(ojBin).size;
110
+ writeFileSync(ojBin, "#!/bin/sh\necho upgraded\n", { mode: 0o755 });
111
+ expect(statSync(ojBin).size).not.toBe(before);
112
+ expect(requireModuleRootCapability(root, () => HELP_WITH).moduleRoot).toBe(true);
113
+ });
114
+ });
@@ -0,0 +1,102 @@
1
+ /**
2
+ * oj 原生别名 `#/<模块名>/…` 在 TS 侧的投影(生成 `<project>/tsconfig.paths.json`)。
3
+ *
4
+ * 为什么必须生成而不能手写一条通配:oj 的 `#/<name>/<tail>` 锚定「目标模块根」,在
5
+ * `src/<name>/{api,web}` 布局下即 `src/<name>/api`。tsconfig 的 `paths` 做不了这件事:
6
+ * - 每条规则的替换值只允许一个 `*`——想用单条 `"#/*"` 同时匹配模块名与尾路径,会被
7
+ * TS 以 TS5062 直接拒掉(两处通配只能二选一);
8
+ * - `paths` 是静态全局映射,表达不了 `#x`(本模块根,随引用方变化)。
9
+ * 所以只能**按模块逐条枚举** `"#/<name>/*": ["./src/<name>/api/*"]`。模块清单必须与
10
+ * `src/<name>/api/manifest.yaml` 同源,手写必然漂移 → 由 `ojm init` / `ojm build` / `ojm dev` 生成。
11
+ *
12
+ * 生成物只被 `src/tsconfig.json`(后端)`extends`:前端 tsconfig 不引,因为 web 侧
13
+ * 没有 `#` 解析(`build.ts` 的 esbuild/vite 不认别名),让编辑器「能过」而打包失败
14
+ * 比漏补 path 更坏。
15
+ *
16
+ * 模式串用 manifest 的 `name`(oj 的身份口径,v0.1.85 起与目录名脱钩),替换值用
17
+ * **目录名**——两者在正常工程里同值,但真分叉时只有按此口径才不会一边错位。
18
+ */
19
+ import type { ProjectLayout } from "./layout";
20
+ import fs from "node:fs";
21
+ import path from "node:path";
22
+ import { parse } from "yaml";
23
+ import { tryResolveLayout } from "./layout";
24
+
25
+ /** 生成物文件名(工程根)。`src/tsconfig.json` 以 `../tsconfig.paths.json` 引用 */
26
+ export const ALIAS_PATHS_FILE = "tsconfig.paths.json";
27
+
28
+ /** 生成物头注释:tsconfig 允许注释,且这是唯一会被 `ojm` 覆写的 tsconfig */
29
+ const HEADER = [
30
+ "// 由 ojm 生成:ojm init / ojm build / ojm dev 会按 src/*/api/manifest.yaml 重写本文件,请勿手改。",
31
+ "//",
32
+ "// oj 别名 `#/<模块名>/…` 锚在目标模块根(本布局下即 src/<模块名>/api)。tsconfig 的",
33
+ "// paths 只能静态全局映射,且每条规则的替换值只允许一个 `*`(`./src/*/api/*` 会被 TS",
34
+ "// 以 TS5062 拒掉),故按模块逐条枚举。",
35
+ ].join("\n");
36
+
37
+ /** 读模块根 manifest 的 `name`(oj 的模块身份);读不到/格式坏时回落到目录名 */
38
+ function manifestName(apiDir: string, fallback: string): string {
39
+ try {
40
+ const doc: unknown = parse(fs.readFileSync(path.join(apiDir, "manifest.yaml"), "utf-8"));
41
+ if (doc && typeof doc === "object" && "name" in doc) {
42
+ const name = (doc as { name?: unknown }).name;
43
+ if (typeof name === "string" && name.trim())
44
+ return name.trim();
45
+ }
46
+ }
47
+ catch {}
48
+ return fallback;
49
+ }
50
+
51
+ /**
52
+ * 模块名 → tsconfig `paths` 规则(仅 module-root 布局)。
53
+ *
54
+ * 键序按目录名排序(`scanModules` 已排序),保证同布局两次生成字节一致——
55
+ * 否则「生成物是否过期」的比对会因文件系统枚举顺序抖动而误报。
56
+ */
57
+ export function aliasPathEntries(layout: ProjectLayout): Record<string, string[]> {
58
+ if (layout.kind !== "module-root")
59
+ return {};
60
+ const entries: Record<string, string[]> = {};
61
+ for (const mod of fs.readdirSync(layout.modulesSrc, { withFileTypes: true })) {
62
+ if (!mod.isDirectory() || mod.name.startsWith("."))
63
+ continue;
64
+ const apiDir = path.join(layout.modulesSrc, mod.name, "api");
65
+ if (!fs.existsSync(path.join(apiDir, "manifest.yaml")))
66
+ continue;
67
+ const name = manifestName(apiDir, mod.name);
68
+ entries[`#/${name}/*`] = [`./${path.posix.join("src", mod.name, "api", "*")}`];
69
+ }
70
+ return entries;
71
+ }
72
+
73
+ /** 渲染 `tsconfig.paths.json`(含头注释;tsconfig 解析器接受 JSONC) */
74
+ export function renderAliasPaths(layout: ProjectLayout): string {
75
+ const entries = Object.entries(aliasPathEntries(layout));
76
+ const body = entries.length === 0
77
+ ? "\t\t\"paths\": {}"
78
+ : `\t\t"paths": {\n${
79
+ entries
80
+ .map(([pattern, targets]) => `\t\t\t${JSON.stringify(pattern)}: [${targets.map(t => JSON.stringify(t)).join(", ")}]`)
81
+ .join(",\n")
82
+ }\n\t\t}`;
83
+ return `${HEADER}\n{\n\t"compilerOptions": {\n${body}\n\t}\n}\n`;
84
+ }
85
+
86
+ /**
87
+ * 按当前模块清单同步生成物,**内容未变则不写**(避免每次 build 都产生无意义 diff)。
88
+ *
89
+ * 返回 `skipped` 表示不是 module-root 布局(历史布局的别名口径不同,不在此生成)。
90
+ */
91
+ export function syncAliasPaths(projectRoot: string): "created" | "updated" | "unchanged" | "skipped" {
92
+ const layout = tryResolveLayout(projectRoot);
93
+ if (layout?.kind !== "module-root")
94
+ return "skipped";
95
+ const file = path.join(projectRoot, ALIAS_PATHS_FILE);
96
+ const next = renderAliasPaths(layout);
97
+ const prev = fs.existsSync(file) ? fs.readFileSync(file, "utf-8") : null;
98
+ if (prev === next)
99
+ return "unchanged";
100
+ fs.writeFileSync(file, next);
101
+ return prev === null ? "created" : "updated";
102
+ }
package/src/args.ts CHANGED
@@ -40,13 +40,16 @@ export function parseApiArgs(argv: string[]): { dir: string, check: boolean, doc
40
40
 
41
41
  /**
42
42
  * `ojm build [--bundle] [--out <dir>]`。
43
- * `--bundle`:方案包构建(manifest.yaml 契约校验 → 确定性 tgz + sha256),
44
- * 与站点级 `ojm build`(后端 oj build + 模块产物合并)互斥分工(devkit 手册「打包与分发」)。
43
+ * `--bundle`:方案包构建(契约校验 → 确定性 tgz + sha256)。bundle 级契约位置:优先工程根
44
+ * `solution.yaml`、或 `src/<模块>/solution.yaml`(模块级方案,契约与所打包模块同位);旧方案回退
45
+ * `manifest.yaml`(同两处位置)。与站点级 `ojm build`(后端 oj build + 模块产物合并)互斥分工
46
+ * (devkit 手册「打包与分发」)。
45
47
  * `--out` 的值不得以 `--` 开头(同 parseApiArgs 的消费式扫描,P6 同款坑)。
46
48
  */
47
- export function parseBuildArgs(argv: string[]): { bundle: boolean, out?: string } {
49
+ export function parseBuildArgs(argv: string[]): { bundle: boolean, bundleName?: string, out?: string } {
48
50
  const flags = new Set<string>();
49
51
  let out: string | undefined;
52
+ let bundleName: string | undefined;
50
53
  for (let i = 0; i < argv.length; i++) {
51
54
  const a = argv[i];
52
55
  if (a === "--out") {
@@ -54,11 +57,58 @@ export function parseBuildArgs(argv: string[]): { bundle: boolean, out?: string
54
57
  i++; // 消费紧随的值,不被当 flag
55
58
  continue;
56
59
  }
60
+ // --bundle <name> / --bundle=<name>:只打这一份方案(无值 = 全部用户方案)
61
+ if (a === "--bundle") {
62
+ flags.add(a);
63
+ const next = argv[i + 1];
64
+ if (next !== undefined && !next.startsWith("--")) {
65
+ bundleName = next;
66
+ i++;
67
+ }
68
+ continue;
69
+ }
70
+ if (a.startsWith("--bundle=")) {
71
+ flags.add("--bundle");
72
+ bundleName = a.slice("--bundle=".length);
73
+ continue;
74
+ }
57
75
  if (a.startsWith("--")) {
58
76
  flags.add(a);
59
77
  }
60
78
  }
61
- return { bundle: flags.has("--bundle"), out };
79
+ return { bundle: flags.has("--bundle"), bundleName, out };
80
+ }
81
+
82
+ /**
83
+ * `ojm new <name> [--layout container|none] [--api]`:新方案骨架生成参数。
84
+ * name 为首个非 flag 位置参数(必填,由调用方判空——非交互/CI 友好)。
85
+ */
86
+ export function parseNewArgs(argv: string[]): { name?: string, layout?: "container" | "none", api?: boolean } {
87
+ let name: string | undefined;
88
+ let layout: "container" | "none" | undefined;
89
+ let api = false;
90
+ for (let i = 0; i < argv.length; i++) {
91
+ const a = argv[i];
92
+ if (a === "--layout") {
93
+ const v = argv[i + 1];
94
+ if (v !== "container" && v !== "none")
95
+ throw new Error(`[ojm] --layout 只接受 container | none(实际:${v})`);
96
+ layout = v;
97
+ i++;
98
+ continue;
99
+ }
100
+ if (a === "--api") {
101
+ api = true;
102
+ continue;
103
+ }
104
+ if (a !== undefined && !a.startsWith("--")) {
105
+ if (name !== undefined)
106
+ throw new Error(`[ojm] ojm new 只接受一个方案名(多余参数:${a})`);
107
+ name = a;
108
+ continue;
109
+ }
110
+ }
111
+ return { name, layout, api };
62
112
  }
63
113
 
64
114
  /**
package/src/build.ts CHANGED
@@ -10,8 +10,10 @@ import { pathToFileURL } from "node:url";
10
10
  import tailwindcss from "@tailwindcss/vite";
11
11
  import { build as esbuild } from "esbuild";
12
12
  import { build } from "vite";
13
- import { loadModulesConfig, resolveModuleEntry } from "./config";
14
- import { resolveConfigPath, resolveLayout } from "./layout";
13
+ import { ALIAS_PATHS_FILE, syncAliasPaths } from "./alias-paths";
14
+ import { assertModuleList, loadSiteConfig, mergeModuleList, resolveModuleEntry } from "./config";
15
+ import { resolveConfigPath, resolveLayout, scanModules, tryResolveLayout } from "./layout";
16
+ import { requireModuleRootCapability } from "./oj-caps";
15
17
  import { isSharedDep, SHARED_DEPS } from "./shared-deps";
16
18
  import { normalizeSiteBase, rewriteIndexHtml } from "./site-base";
17
19
  import { resolveOjBin } from "./vendor";
@@ -494,18 +496,33 @@ function assertResolvableSpecifiers(
494
496
  * 无后端源码返回 false(纯前端工程跳过)。
495
497
  */
496
498
  export async function buildBackend(projectRoot: string): Promise<boolean> {
497
- const apiSrc = path.join(projectRoot, "api/src");
499
+ // 没有模块源码的工程(如 market 的隔离站点根)直接跳过——不能因为探测不到布局就抛
500
+ const layout = tryResolveLayout(projectRoot);
501
+ const moduleRoot = layout?.kind === "module-root";
502
+ const apiSrc = moduleRoot
503
+ ? layout.modulesSrc
504
+ : path.join(projectRoot, "api/src");
498
505
  if (!fs.existsSync(apiSrc))
499
506
  return false;
500
507
 
501
508
  const oj = resolveOjBin(projectRoot);
502
509
  if (!fs.existsSync(oj)) {
503
510
  throw new Error(
504
- `[ojm] 找到 api/src 但缺少 ${oj}。\n`
511
+ `[ojm] 找到 ${path.relative(projectRoot, apiSrc) || "."} 但缺少 ${oj}。\n`
505
512
  + "请重跑 ojm init 幂等补缺(不会覆盖 config 与用户代码)。",
506
513
  );
507
514
  }
508
- const apiDist = path.join(projectRoot, "api/dist");
515
+ // 能力闸门先于任何写入:失败时磁盘上不留半成品
516
+ if (moduleRoot)
517
+ requireModuleRootCapability(projectRoot);
518
+
519
+ // 别名投影:模块增删后要让 TS 的 paths 跟上(否则 `#/<模块名>/…` 在编辑器/typecheck
520
+ // 里解析不到,退化成 any)。内容未变不写盘。
521
+ const syncedPaths = syncAliasPaths(projectRoot);
522
+ if (syncedPaths === "created" || syncedPaths === "updated")
523
+ console.log(`[ojm] 已${syncedPaths === "created" ? "生成" : "更新"} ${ALIAS_PATHS_FILE}(\`#/<模块名>/…\` 的 TS paths 投影)`);
524
+
525
+ const apiDist = layout?.apiDist ?? path.join(projectRoot, "api", "dist");
509
526
  console.log("[ojm] oj build(后端)…");
510
527
  // 必须显式给 -c:oj build 的 -c 缺省是 ./config.yaml(相对 CWD),而 config 在
511
528
  // <project>/config/ 下——漏传会静默读不到配置,连带 tasks 镜像目录回落默认 "tasks"、
@@ -518,7 +535,17 @@ export async function buildBackend(projectRoot: string): Promise<boolean> {
518
535
  // 2) `if (cond) x = y` → 压成 `cond && x = y`,赋值作 && 右项缺括号 →
519
536
  // SyntaxError: Invalid left-hand side in assignment(端点 500)。
520
537
  // 0.1.32 / 0.1.50 均复现。产物体积换正确性,等上游修好再撤这个旗标。
521
- execFileSync(oj, ["build", ...configArgs, "-d", apiSrc, "-o", apiDist, "--no-minify"], { stdio: "inherit" });
538
+ // v0.1.85:`--module-root <dir>` 与 `-d` 互斥(都是「源码树根」)。新布局给 src,
539
+ // oj 在子树内递归找 manifest.yaml(命中 src/<name>/api/manifest.yaml 即停)。
540
+ const sourceArgs = layout?.moduleRoot ? ["--module-root", layout.moduleRoot] : ["-d", apiSrc];
541
+ execFileSync(oj, [
542
+ "build",
543
+ ...configArgs,
544
+ ...sourceArgs,
545
+ "-o",
546
+ apiDist,
547
+ "--no-minify",
548
+ ], { stdio: "inherit" });
522
549
  // 宿主 versions.json 双落点:站点壳从 shell-dist 拷进 web/dist(mergeShellSite),
523
550
  // 而方案安装器读的是 `<api>/dist/versions.json`(B-11:与 manifests.yaml 同层,用于
524
551
  // peerRuntime 强制比对)。两处必须同源,否则新工程 `ojm build` 完仍装不上方案——
@@ -569,8 +596,13 @@ export async function buildModules(
569
596
  projectRoot: string,
570
597
  opts: { mergeSite?: boolean } = {},
571
598
  ): Promise<BuiltModule[]> {
572
- const config = await loadModulesConfig(projectRoot);
573
599
  const layout = resolveLayout(projectRoot);
600
+ const config = await loadSiteConfig(projectRoot);
601
+ // module-root:模块清单由 src/*/web/entry.ts 扫描得出(web.config.ts 仅可选覆盖);
602
+ // 历史布局:仍以 web.config.ts 的 modules 为唯一真源。
603
+ const modules = layout.kind === "module-root"
604
+ ? mergeModuleList(scanModules(layout), config.modules, projectRoot)
605
+ : assertModuleList(projectRoot, config.modules);
574
606
  const outDir = layout.distDir;
575
607
  const baseUrl = config.baseUrl ?? "";
576
608
 
@@ -584,7 +616,7 @@ export async function buildModules(
584
616
 
585
617
  const built: BuiltModule[] = [];
586
618
 
587
- for (const item of config.modules) {
619
+ for (const item of modules) {
588
620
  if (item.enabled === false) {
589
621
  console.log(`[ojm] 跳过已禁用模块 ${item.name}`);
590
622
  continue;
@@ -595,7 +627,7 @@ export async function buildModules(
595
627
 
596
628
  if (definition.name !== item.name) {
597
629
  throw new Error(
598
- `模块名不一致:web.config.ts 中为 "${item.name}",entry.ts 中为 "${definition.name}"`,
630
+ `模块名不一致:${layout.kind === "module-root" ? `src/${item.name}` : "web.config.ts"} 中为 "${item.name}",entry.ts 中为 "${definition.name}"`,
599
631
  );
600
632
  }
601
633