@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
@@ -1,4 +1,4 @@
1
- import type { InstallContext } from "../../../templates/api/src/_platform/_lifecycle/steps/preflight";
1
+ import type { InstallContext } from "../../../templates/src/_platform/api/_lifecycle/steps/preflight";
2
2
 
3
3
  // BDD:bundle 级 manifest.yaml 契约校验(P1-11,C2 规则构建列)。
4
4
  // 规则真源:docs/prd/solution/202610171254-lifecycle-manifest-contract.md(C2-01~33 构建列,§4.1 矩阵);
@@ -13,8 +13,8 @@ import { describe, expect, it } from "vitest";
13
13
  import { parse } from "yaml";
14
14
  // 跨端引用(双端一致钉):被测实现同仓分居两运行时(计划 §1「两运行时取舍」),
15
15
  // 测试直接互调,比「两套件各读同一夹具」更强——漂移即本文件红。
16
- import { preflight } from "../../../templates/api/src/_platform/_lifecycle/steps/preflight";
17
- import { checkLocalesComplete, checkTablesAgainstSchema, groupI18nKeys, parseManifest, validateContract } from "../../manifest-contract";
16
+ import { preflight } from "../../../templates/src/_platform/api/_lifecycle/steps/preflight";
17
+ import { checkLocalesComplete, checkTablesAgainstSchema, parseManifest, validateContract } from "../../manifest-contract";
18
18
 
19
19
  const FIXTURES = join(dirname(fileURLToPath(import.meta.url)), "fixtures");
20
20
  const readText = (p: string) => readFileSync(join(FIXTURES, p), "utf8");
@@ -214,72 +214,31 @@ describe("checkLocalesComplete(C2-25:构建期缺 key fail-fast)", () => {
214
214
  });
215
215
  });
216
216
 
217
- // ── C2-43 归组父标题 i18n(solution menu grouping;纯内容入参)──
217
+ // ── 归组父标题不再进引用面(0.2.18:`menu.main` 是模块级标题的唯一真源)──
218
218
 
219
- describe("c2-43 归组父标题 i18n(solution menu grouping)", () => {
219
+ describe("main/admin 路由不再要求归组父标题", () => {
220
220
  const baseManifest = (): any => ({
221
221
  name: "demo-solution",
222
222
  version: "0.1.0",
223
223
  routes: [
224
224
  { path: "/demo-solution", category: "main", order: 200, title_key: "demo-solution:menu.main" },
225
+ { path: "/demo-solution/users", category: "admin", order: 210, title_key: "demo-solution:menu.users" },
225
226
  ],
226
227
  });
227
- const ZH_FULL = JSON.stringify({ menu: { main: "主", group: "分组" } });
228
- const EN_FULL = JSON.stringify({ menu: { main: "Main", group: "Group" } });
229
228
 
230
- it("groupI18nKeys:声明 main/admin 路由才产一条 <name>:menu.group", () => {
231
- expect(groupI18nKeys(baseManifest())).toEqual(["demo-solution:menu.group"]);
229
+ it("声明 main/admin 路由、locale 只有 menu.main → 零违规", () => {
230
+ const zh = JSON.stringify({ menu: { main: "主", users: "用户" } });
231
+ const en = JSON.stringify({ menu: { main: "Main", users: "Users" } });
232
+ expect(checkLocalesComplete(baseManifest(), zh, en)).toEqual([]);
232
233
  });
233
234
 
234
- it("groupI18nKeys:多条 main 路由去重只产一条(C5)", () => {
235
- const m = baseManifest();
236
- m.routes = [
237
- { path: "/a", category: "main", order: 200, title_key: "demo-solution:menu.a" },
238
- { path: "/b", category: "main", order: 210, title_key: "demo-solution:menu.b" },
239
- ];
240
- expect(groupI18nKeys(m)).toEqual(["demo-solution:menu.group"]);
241
- });
242
-
243
- it("groupI18nKeys:仅 hidden 路由不产 group key(C3)", () => {
244
- const m = baseManifest();
245
- m.routes = [{ path: "/x", category: "hidden", order: 210, title_key: "demo-solution:menu.x" }];
246
- expect(groupI18nKeys(m)).toEqual([]);
247
- });
248
-
249
- it("c1 正例:menu.group 双语齐备 → 零违规", () => {
250
- expect(checkLocalesComplete(baseManifest(), ZH_FULL, EN_FULL)).toEqual([]);
251
- });
252
-
253
- it("c2 反例:zh-CN 缺 menu.group → C2-43(非 C2-25,去重生效)", () => {
254
- const zh = JSON.stringify({ menu: { main: "主" } });
255
- const v = checkLocalesComplete(baseManifest(), zh, EN_FULL);
256
- const hit = v.find(x => x.includes("C2-43"));
257
- expect(hit, `期望 C2-43,实际:\n${v.join("\n")}`).toBeTruthy();
258
- expect(hit).not.toContain("C2-25");
259
- expect(hit).toContain("menu.group");
260
- });
261
-
262
- it("c4 反例:en-US 缺 menu.group → C2-43 指明 en-US", () => {
263
- const en = JSON.stringify({ menu: { main: "Main" } });
264
- const v = checkLocalesComplete(baseManifest(), ZH_FULL, en);
265
- const hit = v.find(x => x.includes("C2-43"));
266
- expect(hit).toBeTruthy();
267
- expect(hit).toContain("en-US");
268
- });
269
-
270
- it("去重:多条 main 路由只产一条 group key,缺失时按语言各报一次(非按路由重复)", () => {
271
- const m = baseManifest();
272
- m.routes = [
273
- { path: "/a", category: "main", order: 200, title_key: "demo-solution:menu.a" },
274
- { path: "/b", category: "main", order: 210, title_key: "demo-solution:menu.b" },
275
- ];
276
- const v = checkLocalesComplete(m, JSON.stringify({ menu: { main: "主" } }), JSON.stringify({ menu: { main: "Main" } }));
277
- const c243 = v.filter(x => x.includes("C2-43"));
278
- // 关键:2 条路由只产 1 个 group key → 缺失时按语言各报 1 次 = 2 条 C2-43,
279
- // 而非 2 路由 × 2 语言 = 4 条(去重生效的判据是「< 路由数 × 语言数」)
280
- expect(c243.length).toBe(2);
281
- expect(c243.some(x => x.includes("zh-CN 缺少归组父标题 key「demo-solution:menu.group」"))).toBe(true);
282
- expect(c243.some(x => x.includes("en-US 缺少归组父标题 key「demo-solution:menu.group」"))).toBe(true);
235
+ it("根标题 menu.main 仍受 C2-25 约束:缺则报 C2-25", () => {
236
+ const zh = JSON.stringify({ menu: { users: "用户" } });
237
+ const en = JSON.stringify({ menu: { main: "Main", users: "Users" } });
238
+ const v = checkLocalesComplete(baseManifest(), zh, en);
239
+ expect(v).toHaveLength(1);
240
+ expect(v[0]).toContain("C2-25");
241
+ expect(v[0]).toContain("demo-solution:menu.main");
283
242
  });
284
243
  });
285
244
 
@@ -1,5 +1,5 @@
1
1
  // bundle 级 manifest.yaml 契约校验(P1-11,`ojm build --bundle` 构建期执行点)。
2
- // 规则真源:docs/prd/solution/202610171254-lifecycle-manifest-contract.md §2/§4(C2-01~33 构建列;C2-34~42 为归档签名与信任,见 sign.ts;本文件新增 C2-43);
2
+ // 规则真源:docs/prd/solution/202610171254-lifecycle-manifest-contract.md §2/§4(C2-01~33 构建列;C2-34~42 为归档签名与信任,见 sign.ts);
3
3
  // 期望真源:src/lifecycle/__tests__/fixtures/(fixture-valid 零违规 / fixture-invalid 七码齐报)。
4
4
  // 双端一致:本实现与 isolate 侧 preflight(templates/api/.../steps/preflight.ts)共享同一组夹具,
5
5
  // 正则与规则**逐字一致**(§4.1 矩阵构建列 vs 安装列的执行点差异见各规则注释)——
@@ -581,15 +581,6 @@ export function validateContract(m: unknown): string[] {
581
581
  return v0;
582
582
  }
583
583
 
584
- /** C2-43 引用面:声明 main/admin 路由的方案须提供归组父标题 `<name>:menu.group`(单 bundle 单 name,去重只产一条) */
585
- export function groupI18nKeys(m: BundleManifest): string[] {
586
- if (!isObj(m) || typeof m.name !== "string")
587
- return [];
588
- const routes = Array.isArray(m.routes) ? m.routes : [];
589
- const hasGroupable = routes.some(r => isObj(r) && (r.category === "main" || r.category === "admin"));
590
- return hasGroupable ? [`${m.name}:menu.group`] : [];
591
- }
592
-
593
584
  /** 收集 manifest 引用的全部 i18n key(C2-25 locales 齐备检查的引用面;bundle.ts 以此判定 locales 文件是否必查) */
594
585
  export function referencedI18nKeys(m: BundleManifest): string[] {
595
586
  const keys: string[] = [];
@@ -620,10 +611,6 @@ export function referencedI18nKeys(m: BundleManifest): string[] {
620
611
  }
621
612
  }
622
613
  }
623
- // C2-43:归组父标题纳入引用面,交由 checkLocalesComplete 双语齐备(改报 C2-43)
624
- for (const k of groupI18nKeys(m)) {
625
- keys.push(k);
626
- }
627
614
  return keys;
628
615
  }
629
616
 
@@ -646,12 +633,9 @@ export function checkLocalesComplete(m: unknown, zhText: string, enText: string)
646
633
  if (!isObj(m))
647
634
  return [v("C2-25", "manifest 不是键值对象,无法核对 locales 引用")];
648
635
  const locales: [string, Record<string, unknown> | null][] = [["zh-CN", parseJson(zhText)], ["en-US", parseJson(enText)]];
649
- // group key 识别(C2-43):归组父标题缺失须归因 C2-43 而非笼统 C2-25,避免重复报错
650
- const groupKeys = new Set(groupI18nKeys(m));
651
636
  const out: string[] = [];
652
637
  for (const key of referencedI18nKeys(m)) {
653
638
  const nsKey = key.includes(":") ? key.slice(key.indexOf(":") + 1) : key;
654
- const isGroup = groupKeys.has(key);
655
639
  for (const [lang, tree] of locales) {
656
640
  if (tree === null) {
657
641
  out.push(v("C2-25", `${lang} locale 不是合法 JSON 对象,无法核对引用 key`));
@@ -659,14 +643,7 @@ export function checkLocalesComplete(m: unknown, zhText: string, enText: string)
659
643
  }
660
644
  const val = lookupKey(tree, nsKey);
661
645
  if (typeof val !== "string" || val === "") {
662
- out.push(
663
- v(
664
- isGroup ? "C2-43" : "C2-25",
665
- isGroup
666
- ? `${lang} 缺少归组父标题 key「${key}」(须为嵌套 menu.group,C2-43;声明 main/admin 路由即须提供)`
667
- : `${lang} 缺少 manifest 引用的 key「${key}」(双语齐备,D4;构建期 fail-fast)`,
668
- ),
669
- );
646
+ out.push(v("C2-25", `${lang} 缺少 manifest 引用的 key「${key}」(双语齐备,D4;构建期 fail-fast)`));
670
647
  }
671
648
  }
672
649
  }
@@ -28,8 +28,16 @@ function fakeStarter() {
28
28
  function newSourceProject(): string {
29
29
  const root = mkdtempSync(join(tmpdir(), "ojm-bootstrap-src-"));
30
30
  mkdirSync(join(root, "bin", "plugins"), { recursive: true });
31
- writeFileSync(join(root, "bin", "oj"), "#!/bin/sh\necho fake-oj\n", { mode: 0o755 });
32
- writeFileSync(join(root, "bin", ".oj-version"), "v0.1.84\n");
31
+ // 假 oj 必须过 §7 双探:`-V` 报版本、`build --help` 含 --module-root,否则 buildBackend 在闸门处 fail-fast
32
+ writeFileSync(
33
+ join(root, "bin", "oj"),
34
+ "#!/bin/sh\n"
35
+ + "if [ \"$1\" = \"-V\" ]; then echo oj 0.1.85; exit 0; fi\n"
36
+ + "if [ \"$1\" = \"build\" ]; then echo \" --module-root <MODULE_ROOT> 源码树根\"; exit 0; fi\n"
37
+ + "echo fake-oj\n",
38
+ { mode: 0o755 },
39
+ );
40
+ writeFileSync(join(root, "bin", ".oj-version"), "v0.1.85\n");
33
41
  writeFileSync(join(root, "bin", "plugins", "auth.wasm"), "plugin", "utf-8");
34
42
  mkdirSync(join(root, "api", "src", "_platform"), { recursive: true });
35
43
  writeFileSync(join(root, "api", "src", "_platform", "seed.sql"), "-- seed\n", "utf-8");
@@ -60,7 +68,7 @@ describe("given 一个装了 oj 二进制的源工程 When 自举隔离站点",
60
68
 
61
69
  const site = await bootstrapIsolatedSite({ projectRoot: src, starter });
62
70
 
63
- expect(existsSync(join(site.root, "api", "src", "_platform", "seed.sql"))).toBe(true);
71
+ expect(existsSync(join(site.root, "src", "_platform", "api", "seed.sql"))).toBe(true);
64
72
  expect(existsSync(join(site.root, "bin", "oj"))).toBe(true);
65
73
  expect(existsSync(join(site.root, "bin", "plugins", "auth.wasm"))).toBe(true);
66
74
  expect(site.root).not.toBe(src);
@@ -75,8 +83,8 @@ describe("given 一个装了 oj 二进制的源工程 When 自举隔离站点",
75
83
 
76
84
  expect(site.port).toBeGreaterThan(1024);
77
85
  expect(site.port).not.toBe(9778);
78
- expect(started[0]?.configPath).toBe(join(site.root, "api", "config.yaml"));
79
- const cfg = readFileSync(join(site.root, "api", "config.yaml"), "utf-8");
86
+ expect(started[0]?.configPath).toBe(join(site.root, "config.yaml"));
87
+ const cfg = readFileSync(join(site.root, "config.yaml"), "utf-8");
80
88
  expect(cfg).toContain(`port: ${site.port}`);
81
89
  await site.cleanup();
82
90
  });
@@ -87,7 +95,7 @@ describe("given 一个装了 oj 二进制的源工程 When 自举隔离站点",
87
95
 
88
96
  const site = await bootstrapIsolatedSite({ projectRoot: src, starter });
89
97
 
90
- const cfg = readFileSync(join(site.root, "api", "config.yaml"), "utf-8");
98
+ const cfg = readFileSync(join(site.root, "config.yaml"), "utf-8");
91
99
  expect(cfg).not.toContain("__JWT_SECRET__");
92
100
  await site.cleanup();
93
101
  });
@@ -113,7 +121,8 @@ describe("given 一个装了 oj 二进制的源工程 When 自举隔离站点",
113
121
 
114
122
  expect(site.publisher.privateKeyPem).toContain("PRIVATE KEY");
115
123
  const pemFiles = listFiles(site.root).filter(f => f.endsWith(".pem"));
116
- const outside = pemFiles.filter(f => !f.includes("api/config"));
124
+ // listFiles 返回相对隔离根的路径;站点自身证书三件套在 config/ 下,其余任何 PEM 都是泄漏
125
+ const outside = pemFiles.filter(f => !f.startsWith("config/"));
117
126
  expect(outside).toEqual([]);
118
127
  await site.cleanup();
119
128
  });
@@ -144,8 +153,7 @@ describe("given 一个装了 oj 二进制的源工程 When 自举隔离站点",
144
153
  describe("given 源工程没装 oj 二进制 When 自举", () => {
145
154
  it("then 人话报错指向 ojm vendor,且不留半成品临时根", async () => {
146
155
  const root = mkdtempSync(join(tmpdir(), "ojm-bootstrap-nooj-"));
147
- mkdirSync(join(root, "api"), { recursive: true });
148
- writeFileSync(join(root, "api", "config.yaml"), "server:\n port: 9778\n", "utf-8");
156
+ writeFileSync(join(root, "config.yaml"), "server:\n port: 9778\n", "utf-8");
149
157
  const { starter } = fakeStarter();
150
158
 
151
159
  await expect(bootstrapIsolatedSite({ projectRoot: root, starter }))
@@ -18,7 +18,7 @@
18
18
  import type { OjProcess } from "../oj";
19
19
  import { Buffer } from "node:buffer";
20
20
  import { createHash, createPrivateKey, randomBytes } from "node:crypto";
21
- import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
21
+ import { cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
22
22
  import net from "node:net";
23
23
  import { tmpdir } from "node:os";
24
24
  import path from "node:path";
@@ -26,6 +26,7 @@ import path from "node:path";
26
26
  import process from "node:process";
27
27
  import { fileURLToPath } from "node:url";
28
28
  import { buildBackend } from "../build";
29
+ import { resolveLayout } from "../layout";
29
30
  import { exportRawPublicKeyHex, fingerprintOfPublicKey } from "../lifecycle/sign";
30
31
  import { startOj } from "../oj";
31
32
  import { mintDevCert } from "../oj-cert";
@@ -49,7 +50,7 @@ export interface IsolatedPublisher {
49
50
  }
50
51
 
51
52
  export interface IsolatedSite {
52
- /** 临时根(含 api/ bin/ cdn/) */
53
+ /** 临时根(含 config.yaml / src/ / api/dist/ / bin/ / cdn/) */
53
54
  root: string
54
55
  port: number
55
56
  baseUrl: string
@@ -94,19 +95,26 @@ export async function bootstrapIsolatedSite(opts: BootstrapOptions = {}): Promis
94
95
  }
95
96
 
96
97
  const root = mkdtempSync(path.join(tmpdir(), "ojm-trial-"));
97
- const apiDir = path.join(root, "api");
98
- const configPath = path.join(apiDir, "config.yaml");
98
+ // 新布局(`src/<name>/{api,web}`):config.yaml 在隔离根,源码树根为 <root>/src,
99
+ // 构建产物仍是扁平根 <root>/api/dist(与真实工程同形)。
100
+ const srcDir = path.join(root, "src");
101
+ const configPath = path.join(root, "config.yaml");
99
102
  const cdnDir = path.join(root, "cdn");
100
- const dbPath = path.join(apiDir, "db.sqlite");
103
+ const dbPath = path.join(root, "db.sqlite");
101
104
 
102
- // 1) api 树 + config(模板由 cli 包内置,init 同源;与 cli→templates 依赖红线不冲突——那是运行期文件而非 import)
105
+ // 1) 后端源码树 + config(模板由 cli 包内置,init 同源;与 cli→templates 依赖红线不冲突——那是运行期文件而非 import)
103
106
  const cliRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
104
- const templateApi = path.join(cliRoot, "templates", "api");
105
- if (!existsSync(path.join(templateApi, "config.yaml"))) {
106
- throw new Error(`[ojm] cli 内置模板缺失:${templateApi}(@oj-module/cli 安装不完整?)`);
107
+ const templateDir = path.join(cliRoot, "templates");
108
+ if (!existsSync(path.join(templateDir, "config.yaml"))) {
109
+ throw new Error(`[ojm] cli 内置模板缺失:${templateDir}(@oj-module/cli 安装不完整?)`);
107
110
  }
108
- cpSync(path.join(templateApi, "src"), path.join(apiDir, "src"), { recursive: true });
109
- writeFileSync(configPath, readFileSync(path.join(templateApi, "config.yaml"), "utf-8"), "utf-8");
111
+ // 只拷后端侧(`src/<name>/api`):隔离站点不构建前端,web 源码拷过去只是噪音
112
+ for (const entry of readdirSync(path.join(templateDir, "src"), { withFileTypes: true })) {
113
+ const apiSide = path.join(templateDir, "src", entry.name, "api");
114
+ if (entry.isDirectory() && existsSync(apiSide))
115
+ cpSync(apiSide, path.join(srcDir, entry.name, "api"), { recursive: true });
116
+ }
117
+ writeFileSync(configPath, readFileSync(path.join(templateDir, "config.yaml"), "utf-8"), "utf-8");
110
118
 
111
119
  // 2) oj 二进制与插件(startOj 按 <root>/bin/oj 解析;plugins 让 auth 插件可用)
112
120
  mkdirSync(path.join(root, "bin"), { recursive: true });
@@ -134,10 +142,13 @@ export async function bootstrapIsolatedSite(opts: BootstrapOptions = {}): Promis
134
142
  const siteDir = path.join(root, "site");
135
143
  mkdirSync(siteDir, { recursive: true });
136
144
  writeFileSync(path.join(siteDir, "index.html"), "<!doctype html><html><body>ojm trial site</body></html>\n", "utf-8");
137
- cfg = cfg.replace("web: ../cdn", `web: ${cdnDir}\n - prefix: /\n web: ${siteDir}\n spa: true`);
145
+ cfg = cfg.replace("web: ./cdn", `web: ${cdnDir}\n - prefix: /\n web: ${siteDir}\n spa: true`);
138
146
  // 安装目标恒为 release 部署:装好的模块落在 api/dist,dev 形态(--api-path src)发现不了。
139
147
  // 故隔离站点以 dist 起站,platform.root 同步改写(oj 校验 root 必须是生效的 api 挂载目录)。
140
- cfg = cfg.replace(" root: src", " root: dist");
148
+ // root 相对 **config 所在目录** 解析——config 在隔离根,产物在 <root>/api/dist,故不能写死
149
+ // `dist`(那是 config 还在 api/ 时的旧口径),按布局取值。
150
+ const apiDistRel = path.relative(root, resolveLayout(root).apiDist).split(path.sep).join("/");
151
+ cfg = cfg.replace(" root: src", ` root: ${apiDistRel}`);
141
152
  // auth 守卫在装配期读 anonymous_paths,安装器注册的新匿名端点要重启才生效(首次安装 smoke 会 401)。
142
153
  // 隔离站点直接预置被测方案的探活路径——与 scripts/verify-solution-smoke/bootstrap-host.mjs 同手法,
143
154
  // 让 trial 验的是安装机制本身,而不是重启时序。
@@ -149,8 +160,8 @@ export async function bootstrapIsolatedSite(opts: BootstrapOptions = {}): Promis
149
160
  cfg = `${cfg.trimEnd()}\nfs:\n root: .\n readonly: false\n`;
150
161
  writeFileSync(configPath, cfg, "utf-8");
151
162
 
152
- // 4) 站点开发证书(oj serve 无绕过开关;mintDevCert 落 api/config 三件套)
153
- await mintDevCert(path.join(apiDir, "config"));
163
+ // 4) 站点开发证书(oj serve 无绕过开关;mintDevCert 落 config.yaml 同级的 config/ 三件套)
164
+ await mintDevCert(path.join(root, "config"));
154
165
 
155
166
  // 5) 一次性发布方身份:seed 派生,私钥只在内存
156
167
  const keyId = "trial.publisher";
@@ -168,7 +179,7 @@ export async function bootstrapIsolatedSite(opts: BootstrapOptions = {}): Promis
168
179
  const starter = opts.starter ?? ((cfgPath: string, base: string, apiSrc: string) => startOj(cfgPath, base, apiSrc));
169
180
  let oj: OjProcess;
170
181
  try {
171
- oj = starter(configPath, "/api", path.join(apiDir, "dist"));
182
+ oj = starter(configPath, "/api", path.join(root, "api", "dist"));
172
183
  await oj.ready;
173
184
  }
174
185
  catch (error) {
@@ -245,7 +256,9 @@ function freePort(): Promise<number> {
245
256
  function teardownReport(root: string, port: number): string[] {
246
257
  const lines = [
247
258
  `隔离根:${root}`,
248
- " api/ 站点树(src + config.yaml + config/ 证书 + db.sqlite)",
259
+ " config.yaml + config/ 证书 + db.sqlite 站点配置面",
260
+ " src/ 后端模块源码树(src/<name>/api,拷自 cli 内置模板)",
261
+ " api/dist/ 构建产物(安装目标)",
249
262
  " bin/ oj 二进制与插件(拷贝自源工程)",
250
263
  " cdn/ 隔离 CDN(fixture 包 + 两份索引 + 签名)",
251
264
  `监听端口:127.0.0.1:${port}(进程已停)`,
package/src/oj-caps.ts ADDED
@@ -0,0 +1,133 @@
1
+ /**
2
+ * oj 二进制**能力**探针(设计 202610220010 §7「双探」)。
3
+ *
4
+ * 为什么不能只看版本号:`bin/oj` 允许手改/手替(vendor.ts:143 的逃生门),
5
+ * `bin/.oj-version` 标记会漂移;且 vendor.ts:102-110 已实证「`oj -V` 正常但
6
+ * JsRuntime 能力缺失」真实存在。版本号是声明,能跑通才是事实。
7
+ *
8
+ * 为什么单独探 `--module-root`:module-root 布局(`src/<name>/{api,web}`)依赖
9
+ * oj 的模块根可配置。能力缺失时必须 fail-fast 并给处置命令——回落旧布局会让
10
+ * 「源码搬了、构建产物还是旧的」这种半坏状态长期存在,比直接失败难排查得多。
11
+ *
12
+ * 缓存:`bin/.oj-caps`,以二进制的 size+mtime 为指纹。指纹变了就重探,
13
+ * 所以手替 `bin/oj` 不会吃到旧缓存。
14
+ */
15
+
16
+ import { execFileSync } from "node:child_process";
17
+ import fs from "node:fs";
18
+ import path from "node:path";
19
+ import { resolveOjBin } from "./vendor";
20
+
21
+ export interface OjCaps {
22
+ /** `oj build --help` 是否接受 `--module-root <DIR>`(源码树根,与 `-d` 互斥) */
23
+ moduleRoot: boolean
24
+ }
25
+
26
+ /** 探针执行器(测试注入):执行 oj 子命令并返回合并输出 */
27
+ export type OjProbeRun = (ojBin: string, args: string[]) => string;
28
+
29
+ const defaultRun: OjProbeRun = (ojBin, args) =>
30
+ execFileSync(ojBin, args, { encoding: "utf-8", timeout: 10_000, stdio: ["ignore", "pipe", "pipe"] });
31
+
32
+ export const CAPS_FILE = ".oj-caps";
33
+
34
+ /** 二进制指纹:size + mtime 足以识别「换了文件」,比哈希便宜且无 IO 放大 */
35
+ function fingerprint(ojBin: string): string {
36
+ const st = fs.statSync(ojBin);
37
+ return `${st.size}:${Math.floor(st.mtimeMs)}`;
38
+ }
39
+
40
+ function capsPath(binDir: string): string {
41
+ return path.join(binDir, CAPS_FILE);
42
+ }
43
+
44
+ /** 缓存文件形状:指纹 + 能力位 */
45
+ interface CapsCache {
46
+ fingerprint?: string
47
+ caps?: Partial<OjCaps>
48
+ }
49
+
50
+ /** 读缓存;指纹不匹配或结构不对 → null(重探) */
51
+ export function readCapsCache(ojBin: string): OjCaps | null {
52
+ try {
53
+ const raw = JSON.parse(fs.readFileSync(capsPath(path.dirname(ojBin)), "utf-8")) as CapsCache | null;
54
+ if (!raw || raw.fingerprint !== fingerprint(ojBin) || typeof raw.caps?.moduleRoot !== "boolean")
55
+ return null;
56
+ return { moduleRoot: raw.caps.moduleRoot };
57
+ }
58
+ catch {
59
+ return null;
60
+ }
61
+ }
62
+
63
+ /** 写缓存(失败不影响主流程:缓存只是加速,不是正确性来源) */
64
+ export function writeCapsCache(ojBin: string, caps: OjCaps): void {
65
+ try {
66
+ fs.writeFileSync(
67
+ capsPath(path.dirname(ojBin)),
68
+ `${JSON.stringify({ fingerprint: fingerprint(ojBin), caps }, null, 2)}\n`,
69
+ "utf-8",
70
+ );
71
+ }
72
+ catch {
73
+ // 只读目录 / 权限不足:忽略
74
+ }
75
+ }
76
+
77
+ /** 现场探测:解析 `oj build --help` 的旗标列表(不缓存) */
78
+ export function probeOjCaps(ojBin: string, run: OjProbeRun = defaultRun): OjCaps {
79
+ let help = "";
80
+ try {
81
+ help = run(ojBin, ["build", "--help"]);
82
+ }
83
+ catch (error) {
84
+ throw new Error(
85
+ `[ojm] 无法执行 ${ojBin} build --help:${(error as Error).message}\n`
86
+ + "处置:`ojm vendor` 重装 oj 二进制。",
87
+ );
88
+ }
89
+ // 只认长旗标整词:`--module-root <DIR>`;避免帮助文本里的说明文字误命中
90
+ return { moduleRoot: /--module-root\b/.test(help) };
91
+ }
92
+
93
+ /**
94
+ * module-root 布局的能力闸门:缺失即抛,文案给处置命令。
95
+ * 有缓存走缓存(热路径每次 build/dev 都会经过这里)。
96
+ */
97
+ export function requireModuleRootCapability(
98
+ projectRoot: string,
99
+ run: OjProbeRun = defaultRun,
100
+ ): OjCaps {
101
+ const ojBin = resolveOjBin(projectRoot);
102
+ if (!fs.existsSync(ojBin)) {
103
+ throw new Error(
104
+ `[ojm] 缺少 ${ojBin}:module-root 布局需要 oj 提供 --module-root。\n`
105
+ + "处置:`ojm vendor` 安装 oj 二进制。",
106
+ );
107
+ }
108
+
109
+ const caps = readCapsCache(ojBin) ?? (() => {
110
+ const probed = probeOjCaps(ojBin, run);
111
+ writeCapsCache(ojBin, probed);
112
+ return probed;
113
+ })();
114
+
115
+ if (caps.moduleRoot)
116
+ return caps;
117
+
118
+ const version = (() => {
119
+ try {
120
+ return run(ojBin, ["-V"]).trim() || "(未知)";
121
+ }
122
+ catch {
123
+ return "(不可探测)";
124
+ }
125
+ })();
126
+ throw new Error(
127
+ `[ojm] 当前 oj 不支持 --module-root(版本 ${version}),无法构建 src/<name>/{api,web} 布局。\n`
128
+ + "原因:module-root 布局要求 oj build / serve 的模块根可配置;本版 oj 只认 `-d` 首层子目录为模块。\n"
129
+ + "处置:升级 oj 后重试——`ojm vendor <tag>`(需 >= 支持 --module-root 的版本),"
130
+ + "或把工程改回 web/src/<name> 布局。\n"
131
+ + `缓存:${capsPath(path.dirname(ojBin))}(换二进制后指纹变化会自动重探)。`,
132
+ );
133
+ }
package/src/oj.ts CHANGED
@@ -40,17 +40,40 @@ export interface StartOjPoll {
40
40
  timeoutMs?: number
41
41
  }
42
42
 
43
+ /**
44
+ * 工程根:从 config 所在目录向上找最近的含 `bin/` 的目录(ojm 的 vendor 布局)。
45
+ *
46
+ * 两种布局:
47
+ * <root>/api/config.yaml → <root>/bin/oj (历史布局,命中第二层)
48
+ * <root>/config.yaml → <root>/bin/oj (module-root,命中第一层)
49
+ * 找不到 `bin/` 时退回旧的「往上两级」,保持既有报错行为(缺 bin/oj 的文案不变)。
50
+ */
51
+ function resolveProjectRoot(configPath: string): string {
52
+ const configDir = path.dirname(configPath);
53
+ for (const dir of [configDir, path.dirname(configDir)]) {
54
+ if (fs.existsSync(path.join(dir, "bin")))
55
+ return dir;
56
+ }
57
+ return path.dirname(configDir);
58
+ }
59
+
43
60
  export function startOj(
44
61
  configPath: string,
45
62
  base: string,
46
- apiSrcPath: string,
63
+ apiSource: string | string[],
47
64
  poll: StartOjPoll = {},
48
65
  extraArgs: string[] = [],
49
66
  ): OjProcess {
50
67
  const port = readOjPort(configPath);
51
- // 约定:config 在 <project>/api/config.yaml,vendor 二进制在 <project>/bin/oj
52
- const projectRoot = path.dirname(path.dirname(configPath));
68
+ // 约定:vendor 二进制在 <project>/bin/oj。工程根不能按「config 往上两级」算:
69
+ // module-root 布局的 config 在仓库根(<project>/config.yaml),往上两级会越过工程,
70
+ // 落到父目录找 bin/oj。改为「向上找最近的含 bin/ 的目录」,两种布局都成立。
71
+ const projectRoot = resolveProjectRoot(configPath);
53
72
  const binPath = resolveOjBin(projectRoot);
73
+ // 后端源码参数二选一(oj v0.1.85:`--api-path` 与 `--module-root` 互斥,同时给会被
74
+ // clap 拒掉):历史布局传目录 → `--api-path <dir>`;module-root 布局传**已拼好的
75
+ // 旗标数组** → `["--module-root", "<project>/src"]`。
76
+ const apiArgs = Array.isArray(apiSource) ? apiSource : ["--api-path", apiSource];
54
77
  const healthUrl = `http://127.0.0.1:${port}${base}/health`;
55
78
 
56
79
  // 插件目录发现显式化:oj 的 resolve_plugins_dir 依赖 current_exe() 启发式,而
@@ -69,7 +92,7 @@ export function startOj(
69
92
  let stderrTail = "";
70
93
  // --console-log:oj 新版终端默认静默(console_log 缺省 false,只落 logs/),
71
94
  // ojm dev/preview 的 [oj] 透传管道必须显式打开终端输出才能看到日志(手册 §10)。
72
- const child = spawn(binPath, ["serve", "-c", configPath, "-b", base, "--api-path", apiSrcPath, "--console-log", ...extraArgs], {
95
+ const child = spawn(binPath, ["serve", "-c", configPath, "-b", base, ...apiArgs, "--console-log", ...extraArgs], {
73
96
  stdio: ["ignore", "pipe", "pipe"],
74
97
  ...(env ? { env } : {}),
75
98
  });
@@ -44,7 +44,7 @@ export interface DerivePreviewConfigOptions {
44
44
  apiRoot?: string
45
45
  /** 显式端口时覆盖 server.port;缺省沿用原 config(oj 代码默认 9778) */
46
46
  port?: number
47
- /** 原 config 文本(测试注入);缺省读 api/config.yaml */
47
+ /** 原 config 文本(测试注入);缺省读 config.yaml */
48
48
  configText?: string
49
49
  }
50
50
 
package/src/preview.ts CHANGED
@@ -59,7 +59,8 @@ export async function runPreview(projectRoot: string, opts: PreviewOptions = {})
59
59
  const siteDir = opts.siteDir ?? layout.distDir;
60
60
  const ojBin = resolveOjBin(projectRoot);
61
61
  const configPath = resolveConfigPath(projectRoot);
62
- const apiDist = path.join(projectRoot, "api", "dist");
62
+ // release 根恒为扁平 api/dist(module-root 只改源码布局,不改产物形状——设计 C-3/C-7)
63
+ const apiDist = layout.apiDist;
63
64
 
64
65
  // fail-fast 四查:缺一即提示对应补救(init 幂等补缺 / ojm build 产物)
65
66
  if (!existsSync(ojBin))
package/src/site.ts CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  import process from "node:process";
8
8
 
9
- /** 任务记录(线形状镜像;真源 templates/api/src/_platform/_lifecycle/types.ts:65,客户端不 import 模板内部) */
9
+ /** 任务记录(线形状镜像;真源 templates/src/_platform/api/_lifecycle/types.ts:65,客户端不 import 模板内部) */
10
10
  export interface TaskRecord {
11
11
  /**
12
12
  * 记录 schema 版本(P2-10 线形状镜像):缺省(undefined)=旧记录=v1 语义照常渲染;
@@ -30,7 +30,7 @@ export interface TaskRecord {
30
30
  finishedAt?: number
31
31
  }
32
32
 
33
- /** 模板 serve 缺省端口(templates/api/config.yaml server.port: 9778;host 显式钉 127.0.0.1) */
33
+ /** 模板 serve 缺省端口(templates/config.yaml server.port: 9778;host 显式钉 127.0.0.1) */
34
34
  export const DEFAULT_SITE_URL = "http://127.0.0.1:9778";
35
35
 
36
36
  /** 本 CLI 已知的任务记录 schema 版本(P2-10;与服务端 tasks.ts TASK_SCHEMA_VERSION 手工同步,更高版本触发降级提示) */
package/src/usage.ts CHANGED
@@ -27,7 +27,7 @@ export function usageText(): string {
27
27
  ojm api [dir] [--check] [--docs] [--exempt <path>]
28
28
  契约代码生成(client / routes / openapi / stub)
29
29
  --check 四重对账(生成物同步 / route 双向 / routes.js / WS 路由鉴权)
30
- --exempt <path> 指定豁免清单,覆盖默认 api/.ojm-api-exempt.json
30
+ --exempt <path> 指定豁免清单,覆盖默认 .ojm-api-exempt.json(工程根;历史布局在 api/ 下)
31
31
  --docs 出自包含文档站(单文件离线可看)
32
32
  ojm info
33
33
  输出版本矩阵与模块清单(报障用,US-7)
@@ -68,7 +68,7 @@ export function usageText(): string {
68
68
  --check 离线自检两份索引签名
69
69
  ojm market setup [--url <站点地址>] [--user <u> --pass <p>] [--new-pass <p>]
70
70
  把系统设置 framework.market_cdn_url 指向本地模拟 CDN(工程根 cdn/,由
71
- api/config.yaml 的 /cdn 挂载直出)。首跑若账号处于首次改密期(seed admin
71
+ config.yaml 的 /cdn 挂载直出)。首跑若账号处于首次改密期(seed admin
72
72
  must_change=1),会自动改密换全权 token 后再写入。
73
73
  ojm market publish [--fixture <name>@<version>] [--real <方案工程>] [--id <key>] [--key <pem>] [--rebuild-index] [--check]
74
74
  把方案包落进 cdn/:合成 fixture(或 --real 走 ojm build --bundle)→ 确定性 tgz
@@ -86,7 +86,7 @@ export function usageText(): string {
86
86
  在一次性隔离站点(随机端口 + 一次性 DB/密钥/凭据/信任库/CDN)里跑完整安装,
87
87
  跑完即毁;--keep 保留现场供排查。用于确认「这个包到底能不能装」,不污染你的工程。
88
88
  ojm test|exec|openapi|migrate|schema [args...]
89
- 透传给 oj 同名子命令(自动补 -c <project>/api/config.yaml;其余参数原样转发)
89
+ 透传给 oj 同名子命令(自动补 -c <project>/config.yaml;其余参数原样转发)
90
90
  例:ojm test --anonymous / ojm migrate --db report / ojm openapi --check
91
91
  `;
92
92
  }
@@ -104,7 +104,7 @@ const COMMAND_HELP: Record<string, string> = {
104
104
  开发模式快捷键(仅 TTY 终端,类 Vite):
105
105
  r 强制重建并热更新 h 显示帮助 c 清屏 o 浏览器打开
106
106
  q 退出(后端 oj 一并终止)
107
- port 缺省 5174(oj 后端端口以 api/config.yaml 的 server.port 为准)`,
107
+ port 缺省 5174(oj 后端端口以 config.yaml 的 server.port 为准)`,
108
108
  "build": `ojm build [--bundle] [--out <dir>]
109
109
  构建后端(oj build)与模块产物(含全站合并)
110
110
  --bundle 方案包构建:manifest.yaml 契约校验(fail-fast)→ 确定性 tgz
@@ -117,7 +117,7 @@ const COMMAND_HELP: Record<string, string> = {
117
117
  "api": `ojm api [dir] [--check] [--docs] [--exempt <path>]
118
118
  契约代码生成(client / routes / openapi / stub)
119
119
  --check 四重对账(生成物同步 / route 双向 / routes.js / WS 路由鉴权)
120
- --exempt <path> 指定豁免清单,覆盖默认 api/.ojm-api-exempt.json
120
+ --exempt <path> 指定豁免清单,覆盖默认 .ojm-api-exempt.json(工程根;历史布局在 api/ 下)
121
121
  --docs 出自包含文档站(单文件离线可看)
122
122
  dir 缺省当前目录;显式目录须先存在`,
123
123
  "info": `ojm info
@@ -168,7 +168,7 @@ const COMMAND_HELP: Record<string, string> = {
168
168
  本地模拟 CDN 与市场链路子命令:setup / publish / build-index / verify / trial
169
169
  详见各子命令帮助:ojm market setup --help 等`,
170
170
  "market setup": `ojm market setup [--url <站点地址>] [--user <u> --pass <p>] [--new-pass <p>]
171
- 把系统设置 framework.market_cdn_url 指向本地模拟 CDN(工程根 cdn/,由 api/config.yaml 的 /cdn 挂载直出)。首跑若账号处于首次改密期(seed admin must_change=1),会自动改密换全权 token 后再写入`,
171
+ 把系统设置 framework.market_cdn_url 指向本地模拟 CDN(工程根 cdn/,由 config.yaml 的 /cdn 挂载直出)。首跑若账号处于首次改密期(seed admin must_change=1),会自动改密换全权 token 后再写入`,
172
172
  "market publish": `ojm market publish [--fixture <name>@<version>] [--real <方案工程>] [--id <key>] [--key <pem>] [--rebuild-index] [--check]
173
173
  把方案包落进 cdn/:合成 fixture(或 --real 走 ojm build --bundle)→ 确定性 tgz → 包级 detached 签名 <name>-<version>.tgz.sig
174
174
  不碰索引(索引由 build-index 扫描重建,多人发布才不会互相覆盖)`,
package/src/vendor.ts CHANGED
@@ -35,8 +35,12 @@ export const NPM_PKG = "@oj-bin/oj";
35
35
  * (见 CHANGELOG 0.1.20 / docs/guides/api-codegen-openapi.md)。**0.2.13 抬到 0.1.81**:
36
36
  * 模板侧 `solution-perms.ts` 权限级联清理用 `startswith` 操作键(v0.1.81 补齐,
37
37
  * 构造器迁移收尾),oj < 0.1.81 时卸载方案报 `unknown variant startswith` fail-closed。
38
+ * **本版抬到 0.1.85**:`src/<name>/{web,api}` 布局依赖 `--module-root <DIR>`(源码树根,
39
+ * 与 `-d`/`--api-path` 互斥);旧版收到该旗标即报错。0.1.85 同时落地了
40
+ * `manifest.name` 与目录名脱钩、`#/m/x` 按 name 解析(见
41
+ * docs/prd/202610220010-layout-module-root-design.md §5)。
38
42
  */
39
- export const MIN_OJ_VERSION = "0.1.81";
43
+ export const MIN_OJ_VERSION = "0.1.85";
40
44
 
41
45
  /** 按段数值比较(字符串比较会把 0.1.9 判成大于 0.1.69);非数字段按 0 处理 */
42
46
  export function isSupportedOjVersion(version: string): boolean {