@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.
- package/devkit/README.md +4 -4
- package/devkit/SKILL.md +41 -37
- package/devkit/framework-dev.md +6 -5
- package/devkit/manual.md +352 -168
- package/devkit/menu-rules.md +3 -3
- package/devkit/module-dev-and-cdn-playground.md +31 -22
- package/devkit/sql-builder-missing-capabilities-pr.md +11 -6
- package/package.json +2 -2
- package/shell-dist/assets/runtime.js +59 -34
- package/shell-dist/index.html +2 -2
- package/shell-dist/versions.json +3 -3
- package/src/__tests__/build-backend.test.ts +54 -0
- package/src/__tests__/init-packagejson.test.ts +29 -0
- package/src/__tests__/layout.test.ts +176 -0
- package/src/__tests__/module-list.test.ts +66 -0
- package/src/__tests__/oj-caps.test.ts +114 -0
- package/src/alias-paths.ts +102 -0
- package/src/args.ts +54 -4
- package/src/build.ts +41 -9
- package/src/builtin.ts +93 -0
- package/src/bundle.ts +254 -38
- package/src/config.ts +82 -9
- package/src/contract/__tests__/market-client.integration.test.ts +14 -14
- package/src/contract/check.ts +56 -28
- package/src/contract/emit-stub.ts +21 -9
- package/src/contract/handler-scan.ts +19 -10
- package/src/contract/run.ts +14 -4
- package/src/dev.ts +80 -9
- package/src/index.ts +27 -7
- package/src/info.ts +69 -10
- package/src/init.ts +114 -28
- package/src/layout.ts +282 -24
- package/src/lifecycle/__tests__/args.test.ts +20 -0
- package/src/lifecycle/__tests__/builtin.test.ts +97 -0
- package/src/lifecycle/__tests__/bundle.test.ts +184 -6
- package/src/lifecycle/__tests__/manifest-contract.test.ts +2 -2
- package/src/lifecycle/__tests__/new.test.ts +129 -0
- package/src/lifecycle/__tests__/smoke.test.ts +2 -1
- package/src/manifest-contract.ts +16 -3
- package/src/market/__tests__/bootstrap.test.ts +17 -9
- package/src/market/bootstrap.ts +30 -17
- package/src/market/publish.ts +10 -2
- package/src/new.ts +208 -0
- package/src/oj-caps.ts +133 -0
- package/src/oj.ts +27 -4
- package/src/preview-config.ts +1 -1
- package/src/preview.ts +2 -1
- package/src/site.ts +2 -2
- package/src/usage.ts +24 -12
- package/src/vendor.ts +5 -1
- package/templates/{api/config.yaml → config.yaml} +13 -8
- package/templates/global.d.ts +7 -6
- package/templates/init.include.yaml +41 -36
- package/templates/package.json +3 -1
- package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/installer.ts +4 -0
- package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/smoke.ts +21 -2
- package/templates/{api/src/_platform → src/_platform/api}/fixtures/l1-test-user.sql +4 -5
- package/templates/{api/src/_platform → src/_platform/api}/market/api.ts +25 -15
- package/templates/src/_platform/api/seed.mysql.sql +16 -0
- package/templates/src/_platform/api/seed.pg.sql +16 -0
- package/templates/{api/src/_platform → src/_platform/api}/seed.sql +16 -5
- package/templates/{api/src/_platform → src/_platform/api}/solutions/api.ts +7 -3
- package/templates/{api/src/auth → src/auth/api}/_shared/login-throttle.ts +2 -2
- package/templates/{api/src/auth → src/auth/api}/_shared/session.ts +2 -2
- package/templates/{api/src/auth → src/auth/api}/api.ts +1 -1
- package/templates/{api/src/auth → src/auth/api}/ldap-login/api.ts +1 -1
- package/templates/src/auth/api/manifest.yaml +6 -0
- package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/InstallDialog.test.tsx +53 -1
- package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/marketClient.fake.ts +6 -0
- package/templates/{web/src/oj-market → src/oj-market/web}/api/client.ts +5 -0
- package/templates/{web/src/oj-market → src/oj-market/web}/api/context.ts +2 -2
- package/templates/{web/src/oj-market → src/oj-market/web}/api/defaultClient.ts +7 -1
- package/templates/src/oj-market/web/components/InstallDialog.tsx +194 -0
- package/templates/{web/src/oj-market → src/oj-market/web}/components/RollbackDialog.tsx +7 -3
- package/templates/{web/src/oj-market → src/oj-market/web}/components/UninstallConfirm.tsx +18 -7
- package/templates/{web/src/oj-market → src/oj-market/web}/hooks/useMarket.ts +29 -12
- package/templates/{web/src/oj-market → src/oj-market/web}/locales/en-US.json +10 -1
- package/templates/{web/src/oj-market → src/oj-market/web}/locales/zh-CN.json +10 -1
- package/templates/{web/src/oj-market → src/oj-market/web}/types.ts +15 -2
- package/templates/{api/src/system → src/system/api}/login-methods/api.ts +2 -2
- package/templates/src/system/api/manifest.yaml +6 -0
- package/templates/{api/src/system → src/system/api}/oplog/api.ts +4 -4
- package/templates/{api/src/system → src/system/api}/permissions/api.ts +3 -3
- package/templates/{api/src/system → src/system/api}/roles/api.ts +3 -3
- package/templates/{api/src/system → src/system/api}/roles/bindings/api.ts +3 -3
- package/templates/{api/src/system → src/system/api}/settings/api.ts +3 -3
- package/templates/{api/src/system → src/system/api}/users/api.ts +3 -3
- package/templates/{api/src/system → src/system/api}/users/forbidden/api.ts +3 -3
- package/templates/{api/src/system → src/system/api}/users/password/api.ts +3 -3
- package/templates/{api/src/system → src/system/api}/users/roles/api.ts +3 -3
- package/templates/{web/src/system → src/system/web}/locales/en-US.json +1 -1
- package/templates/{web/src/system → src/system/web}/locales/zh-CN.json +1 -1
- package/templates/src/tsconfig.json +16 -0
- package/templates/{api/tests → tests}/auth-ldap.spec.ts +2 -2
- package/templates/{api/tests → tests}/auth-session.spec.ts +3 -3
- package/templates/{api/tests → tests}/auth-throttle.spec.ts +1 -1
- package/templates/tests/fixture.ts +434 -0
- package/templates/{api/tests → tests}/guard.spec.ts +1 -1
- package/templates/{api/tests → tests}/l1-builder.test.ts +2 -2
- package/templates/{api/tests → tests}/lifecycle/atomic-state.spec.ts +3 -3
- package/templates/{api/tests → tests}/lifecycle/bootstrap.spec.ts +8 -4
- package/templates/{api/tests → tests}/lifecycle/deploy-smoke.spec.ts +31 -8
- package/templates/{api/tests → tests}/lifecycle/installer.spec.ts +15 -15
- package/templates/{api/tests → tests}/lifecycle/market-api.spec.ts +30 -29
- package/templates/{api/tests → tests}/lifecycle/migrate.spec.ts +6 -6
- package/templates/{api/tests → tests}/lifecycle/preflight.spec.ts +4 -4
- package/templates/{api/tests → tests}/lifecycle/register.spec.ts +1 -1
- package/templates/{api/tests → tests}/lifecycle/semver.spec.ts +1 -1
- package/templates/{api/tests → tests}/lifecycle/sha384.spec.ts +1 -1
- package/templates/{api/tests → tests}/lifecycle/sig-fixtures.spec.ts +1 -1
- package/templates/{api/tests → tests}/lifecycle/solutions-api.spec.ts +1 -1
- package/templates/{api/tests → tests}/lifecycle/steps-fs.spec.ts +12 -12
- package/templates/{api/tests → tests}/lifecycle/text-edit.spec.ts +1 -1
- package/templates/{api/tests → tests}/lifecycle/tgz-guard.spec.ts +1 -1
- package/templates/{api/tests → tests}/lifecycle/trust.spec.ts +5 -5
- package/templates/{api/tests → tests}/lifecycle/uninstall.spec.ts +7 -7
- package/templates/{api/tests → tests}/lifecycle/verify-checks.spec.ts +9 -6
- package/templates/{api/tests → tests}/lifecycle/verify-report.spec.ts +7 -5
- package/templates/{api/tests → tests}/query-utils.spec.ts +1 -1
- package/templates/{api/tests → tests}/rbac-audit.spec.ts +1 -1
- package/templates/{api/tests → tests}/rbac-contacts.spec.ts +2 -2
- package/templates/{api/tests → tests}/rbac-guard.spec.ts +1 -1
- package/templates/{api/tests → tests}/rbac-roles.spec.ts +2 -2
- package/templates/{api/tests → tests}/rbac-seed.spec.ts +13 -11
- package/templates/{api/tests → tests}/rbac-settings.spec.ts +2 -2
- package/templates/{api/tests → tests}/rbac-solution-perms.spec.ts +1 -1
- package/templates/{api/tests → tests}/rbac-users.spec.ts +3 -3
- package/templates/tests/seed-role-accounts.spec.ts +87 -0
- package/templates/tsconfig.json +5 -1
- package/templates/tsconfig.paths.json +19 -0
- package/templates/types/oj-modules.d.ts +17 -0
- package/templates/vitest.config.ts +22 -0
- package/templates/web.config.ts +23 -13
- package/vendor/host-versions.json +3 -3
- package/templates/api/src/_platform/seed.mysql.sql +0 -8
- package/templates/api/src/_platform/seed.pg.sql +0 -8
- package/templates/api/src/auth/manifest.yaml +0 -5
- package/templates/api/src/system/manifest.yaml +0 -5
- package/templates/api/tests/fixture.ts +0 -58
- package/templates/api/tsconfig.json +0 -8
- package/templates/api/vitest.config.ts +0 -12
- package/templates/logs/task-events.jsonl +0 -0
- package/templates/web/src/oj-market/components/InstallDialog.tsx +0 -112
- /package/templates/{api/.ojm-api-exempt.json → .ojm-api-exempt.json} +0 -0
- /package/templates/{api/seed.mysql.sql → seed.mysql.sql} +0 -0
- /package/templates/{api/seed.pg.sql → seed.pg.sql} +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/README.md +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/atomic.ts +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/history.ts +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/lock.ts +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/semver.ts +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/sha384.ts +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/state.ts +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/deploy-web.ts +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/download.ts +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/extract.ts +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/migrate.ts +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/preflight.ts +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/register.ts +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/reload.ts +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/verify.ts +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/tasks.ts +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/text-edit.ts +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/tgz-guard.ts +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/trust.ts +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/types.ts +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/uninstall.ts +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/verify-checks.ts +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/verify-report.ts +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/_shared/respond.ts +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/domain/contacts.ts +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/domain/roles.ts +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/domain/settings.ts +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/domain/solution-perms.ts +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/domain/users.ts +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/infra/access.ts +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/infra/audit.ts +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/infra/guard.ts +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/infra/oplog.ts +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/infra/query.ts +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/infra/rate.ts +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/_shared/site-facade.ts +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/contacts/api.ts +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/contacts/detail/api.ts +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/manifest.yaml +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/market/bootstrap.ts +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/migrations/0001__create_users.mysql.sql +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/migrations/0001__create_users.pg.sql +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/migrations/0001__create_users.sql +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/migrations/0002__add_users_avatar_base64.mysql.sql +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/migrations/0002__add_users_avatar_base64.sql +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/migrations/0003__users_source_display_name.mysql.sql +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/migrations/0003__users_source_display_name.sql +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/migrations/0004__sys_rbac.mysql.sql +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/migrations/0004__sys_rbac.pg.sql +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/migrations/0004__sys_rbac.sql +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/migrations/0005__users_must_change.mysql.sql +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/migrations/0005__users_must_change.sql +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/migrations/0006__users_is_forbidden.mysql.sql +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/migrations/0006__users_is_forbidden.sql +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/migrations/0007__users_email.sql +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/migrations/0008__sys_refresh_token.mysql.sql +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/migrations/0008__sys_refresh_token.pg.sql +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/migrations/0008__sys_refresh_token.sql +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/migrations/0009__sys_throttle.mysql.sql +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/migrations/0009__sys_throttle.pg.sql +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/migrations/0009__sys_throttle.sql +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/migrations/0010__sys_solution_route.mysql.sql +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/migrations/0010__sys_solution_route.pg.sql +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/migrations/0010__sys_solution_route.sql +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/migrations/0011__sys_setting_group_label.mysql.sql +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/migrations/0011__sys_setting_group_label.sql +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/migrations/0012__market_history.mysql.sql +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/migrations/0012__market_history.pg.sql +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/migrations/0012__market_history.sql +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/migrations/0013__users_profile.sql +0 -0
- /package/templates/{api/src/_platform → src/_platform/api}/schema.yaml +0 -0
- /package/templates/{web/src/_platform → src/_platform/web}/__client/api.schemas.ts +0 -0
- /package/templates/{web/src/_platform → src/_platform/web}/__client/api.ts +0 -0
- /package/templates/{web/src/about → src/about/web}/entry.ts +0 -0
- /package/templates/{web/src/about → src/about/web}/locales/en-US.json +0 -0
- /package/templates/{web/src/about → src/about/web}/locales/zh-CN.json +0 -0
- /package/templates/{web/src/about → src/about/web}/pages/constants.ts +0 -0
- /package/templates/{web/src/about → src/about/web}/pages/index.tsx +0 -0
- /package/templates/{web/src/access → src/access/web}/entry.ts +0 -0
- /package/templates/{web/src/access → src/access/web}/locales/en-US.json +0 -0
- /package/templates/{web/src/access → src/access/web}/locales/zh-CN.json +0 -0
- /package/templates/{web/src/access → src/access/web}/pages/access-mode/index.tsx +0 -0
- /package/templates/{web/src/access → src/access/web}/pages/admin-visible/index.tsx +0 -0
- /package/templates/{web/src/access → src/access/web}/pages/button-control/index.tsx +0 -0
- /package/templates/{web/src/access → src/access/web}/pages/common-visible/index.tsx +0 -0
- /package/templates/{web/src/access → src/access/web}/pages/page-control/index.tsx +0 -0
- /package/templates/{api/src/auth → src/auth/api}/_shared/ldap-login.ts +0 -0
- /package/templates/{api/src/auth → src/auth/api}/_shared/ldap.ts +0 -0
- /package/templates/{web/src/auth → src/auth/web}/__client/api.schemas.ts +0 -0
- /package/templates/{web/src/auth → src/auth/web}/__client/api.ts +0 -0
- /package/templates/{api/src/demo → src/demo/api}/manifest.yaml +0 -0
- /package/templates/{api/src/demo → src/demo/api}/migrations/0001__create_todos.sql +0 -0
- /package/templates/{api/src/demo → src/demo/api}/schema.yaml +0 -0
- /package/templates/{api/src/demo → src/demo/api}/seed.sql +0 -0
- /package/templates/{api/src/demo → src/demo/api}/todos/api.ts +0 -0
- /package/templates/{web/src/demo → src/demo/web}/__client/api.schemas.ts +0 -0
- /package/templates/{web/src/demo → src/demo/web}/__client/api.ts +0 -0
- /package/templates/{web/src/demo → src/demo/web}/entry.ts +0 -0
- /package/templates/{web/src/demo → src/demo/web}/locales/en-US.json +0 -0
- /package/templates/{web/src/demo → src/demo/web}/locales/zh-CN.json +0 -0
- /package/templates/{web/src/demo → src/demo/web}/pages/index.tsx +0 -0
- /package/templates/{web/src/exception → src/exception/web}/entry.ts +0 -0
- /package/templates/{web/src/exception → src/exception/web}/locales/en-US.json +0 -0
- /package/templates/{web/src/exception → src/exception/web}/locales/zh-CN.json +0 -0
- /package/templates/{web/src/exception → src/exception/web}/pages/403/index.tsx +0 -0
- /package/templates/{web/src/exception → src/exception/web}/pages/404/index.tsx +0 -0
- /package/templates/{web/src/exception → src/exception/web}/pages/500/index.tsx +0 -0
- /package/templates/{web/src/exception → src/exception/web}/pages/unknown-component/index.tsx +0 -0
- /package/templates/{api/src/home → src/home/api}/line/api.ts +0 -0
- /package/templates/{api/src/home → src/home/api}/manifest.yaml +0 -0
- /package/templates/{api/src/home → src/home/api}/migrations/0001__create_home_tables.sql +0 -0
- /package/templates/{api/src/home → src/home/api}/pie/api.ts +0 -0
- /package/templates/{api/src/home → src/home/api}/schema.yaml +0 -0
- /package/templates/{api/src/home → src/home/api}/seed.sql +0 -0
- /package/templates/{web/src/home → src/home/web}/__client/api.schemas.ts +0 -0
- /package/templates/{web/src/home → src/home/web}/__client/api.ts +0 -0
- /package/templates/{web/src/home → src/home/web}/entry.ts +0 -0
- /package/templates/{web/src/home → src/home/web}/locales/en-US.json +0 -0
- /package/templates/{web/src/home → src/home/web}/locales/zh-CN.json +0 -0
- /package/templates/{web/src/home → src/home/web}/pages/components/bar-chart.tsx +0 -0
- /package/templates/{web/src/home → src/home/web}/pages/components/card-list.tsx +0 -0
- /package/templates/{web/src/home → src/home/web}/pages/components/line-chart.tsx +0 -0
- /package/templates/{web/src/home → src/home/web}/pages/components/pie-chart.tsx +0 -0
- /package/templates/{web/src/home → src/home/web}/pages/index.tsx +0 -0
- /package/templates/{web/src/login → src/login/web}/entry.ts +0 -0
- /package/templates/{web/src/login → src/login/web}/locales/en-US.json +0 -0
- /package/templates/{web/src/login → src/login/web}/locales/zh-CN.json +0 -0
- /package/templates/{web/src/login → src/login/web}/pages/change-password.tsx +0 -0
- /package/templates/{web/src/login → src/login/web}/pages/login.tsx +0 -0
- /package/templates/{api/src/notification → src/notification/api}/_shared/notify.ts +0 -0
- /package/templates/{api/src/notification → src/notification/api}/api.ts +0 -0
- /package/templates/{api/src/notification → src/notification/api}/del/api.ts +0 -0
- /package/templates/{api/src/notification → src/notification/api}/manifest.yaml +0 -0
- /package/templates/{api/src/notification → src/notification/api}/migrations/0001__create_notifications.sql +0 -0
- /package/templates/{api/src/notification → src/notification/api}/migrations/0002__notifications_user.sql +0 -0
- /package/templates/{api/src/notification → src/notification/api}/migrations/0003__notifications_broadcast.sql +0 -0
- /package/templates/{api/src/notification → src/notification/api}/migrations/0004__notifications_broadcast_hidden.sql +0 -0
- /package/templates/{api/src/notification → src/notification/api}/schema.yaml +0 -0
- /package/templates/{api/src/notification → src/notification/api}/seed.sql +0 -0
- /package/templates/{web/src/notification → src/notification/web}/__client/api.schemas.ts +0 -0
- /package/templates/{web/src/notification → src/notification/web}/__client/api.ts +0 -0
- /package/templates/{web/src/notification → src/notification/web}/entry.ts +0 -0
- /package/templates/{web/src/notification → src/notification/web}/locales/en-US.json +0 -0
- /package/templates/{web/src/notification → src/notification/web}/locales/zh-CN.json +0 -0
- /package/templates/{web/src/notification → src/notification/web}/pages/center/index.tsx +0 -0
- /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/CardGrid.test.tsx +0 -0
- /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/HistoryDrawer.test.tsx +0 -0
- /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/RollbackDialog.test.tsx +0 -0
- /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/UninstallConfirm.test.tsx +0 -0
- /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/poll.test.ts +0 -0
- /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/render.tsx +0 -0
- /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/runtime-stub.ts +0 -0
- /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/semver.test.ts +0 -0
- /package/templates/{web/src/oj-market → src/oj-market/web}/api/MarketClientProvider.tsx +0 -0
- /package/templates/{web/src/oj-market → src/oj-market/web}/components/CardGrid.tsx +0 -0
- /package/templates/{web/src/oj-market → src/oj-market/web}/components/CardItem.tsx +0 -0
- /package/templates/{web/src/oj-market → src/oj-market/web}/components/DetailDrawer.tsx +0 -0
- /package/templates/{web/src/oj-market → src/oj-market/web}/components/HistoryDrawer.tsx +0 -0
- /package/templates/{web/src/oj-market → src/oj-market/web}/entry.ts +0 -0
- /package/templates/{web/src/oj-market → src/oj-market/web}/pages/MarketApp.tsx +0 -0
- /package/templates/{web/src/oj-market → src/oj-market/web}/utils/poll.ts +0 -0
- /package/templates/{web/src/oj-market → src/oj-market/web}/utils/semver.ts +0 -0
- /package/templates/{web/src/outside → src/outside/web}/entry.ts +0 -0
- /package/templates/{web/src/outside → src/outside/web}/locales/en-US.json +0 -0
- /package/templates/{web/src/outside → src/outside/web}/locales/zh-CN.json +0 -0
- /package/templates/{api/src/personal-center → src/personal-center/api}/manifest.yaml +0 -0
- /package/templates/{api/src/personal-center → src/personal-center/api}/profile/api.ts +0 -0
- /package/templates/{api/src/personal-center → src/personal-center/api}/upload/api.ts +0 -0
- /package/templates/{web/src/personal-center → src/personal-center/web}/__client/api.schemas.ts +0 -0
- /package/templates/{web/src/personal-center → src/personal-center/web}/__client/api.ts +0 -0
- /package/templates/{web/src/personal-center → src/personal-center/web}/entry.ts +0 -0
- /package/templates/{web/src/personal-center → src/personal-center/web}/locales/en-US.json +0 -0
- /package/templates/{web/src/personal-center → src/personal-center/web}/locales/zh-CN.json +0 -0
- /package/templates/{web/src/personal-center → src/personal-center/web}/pages/my-profile/index.tsx +0 -0
- /package/templates/{web/src/route-nest → src/route-nest/web}/entry.ts +0 -0
- /package/templates/{web/src/route-nest → src/route-nest/web}/locales/en-US.json +0 -0
- /package/templates/{web/src/route-nest → src/route-nest/web}/locales/zh-CN.json +0 -0
- /package/templates/{web/src/route-nest → src/route-nest/web}/pages/menu1/menu1-1/index.tsx +0 -0
- /package/templates/{web/src/route-nest → src/route-nest/web}/pages/menu1/menu1-2/index.tsx +0 -0
- /package/templates/{web/src/route-nest → src/route-nest/web}/pages/menu2/index.tsx +0 -0
- /package/templates/{web/src → src}/styles.css +0 -0
- /package/templates/{web/src/system → src/system/web}/__client/api.schemas.ts +0 -0
- /package/templates/{web/src/system → src/system/web}/__client/api.ts +0 -0
- /package/templates/{web/src/system → src/system/web}/entry.ts +0 -0
- /package/templates/{web/src/system → src/system/web}/pages/errors.ts +0 -0
- /package/templates/{web/src/system → src/system/web}/pages/forbidden.tsx +0 -0
- /package/templates/{web/src/system → src/system/web}/pages/login-methods/index.tsx +0 -0
- /package/templates/{web/src/system → src/system/web}/pages/oplog/index.tsx +0 -0
- /package/templates/{web/src/system → src/system/web}/pages/roles/index.tsx +0 -0
- /package/templates/{web/src/system → src/system/web}/pages/settings/index.tsx +0 -0
- /package/templates/{web/src/system → src/system/web}/pages/users/index.tsx +0 -0
- /package/templates/{web/src/system → src/system/web}/pages/users/roles.tsx +0 -0
- /package/templates/{api/src/web → src/web/api}/get-async-routes/api.ts +0 -0
- /package/templates/{api/src/web → src/web/api}/hello/api.ts +0 -0
- /package/templates/{api/src/web → src/web/api}/manifest.yaml +0 -0
- /package/templates/{api/src/web → src/web/api}/user-info/api.ts +0 -0
- /package/templates/{web/src → src/web}/web/__client/api.schemas.ts +0 -0
- /package/templates/{web/src → src/web}/web/__client/api.ts +0 -0
- /package/templates/{api/tests → tests}/rbac-tables.spec.ts +0 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// BDD:`ojm build --bundle` 方案包组装(P1-11)。
|
|
2
2
|
// 组装契约(对齐 tgz 内布局,契约 §2.9 + P1-9 staging copy 源定版):
|
|
3
|
-
// <root>/manifest.yaml
|
|
3
|
+
// <root>/solution.yaml | manifest.yaml ← 必填(solution.yaml 优先,避开 oj 的 api 枚举冲突),契约校验 fail-fast 先于打包
|
|
4
|
+
// <root>/src/<模块>/solution.yaml | manifest.yaml ← 模块级方案:契约与所打包模块同位(同优先级回退)
|
|
4
5
|
// api/dist/<name>-<version>/ ← oj build 产物原样 → tgz 内 api/<name>-<version>/(C2-31 分层核对)
|
|
5
|
-
// web/dist/modules
|
|
6
|
+
// web/dist/modules/<name>/ ← 站点构建模块产物 → tgz 内 web/modules/<name>/(只入包方案自己的模块)
|
|
6
7
|
// 禁携 modules.json(C2-31 红线);产物确定性(C2-32)由 deterministic-tar 承担,此处断言端到端复现。
|
|
7
8
|
import { execFileSync } from "node:child_process";
|
|
8
9
|
import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
@@ -48,7 +49,25 @@ function makePrebuiltArtifacts(): void {
|
|
|
48
49
|
writeFileSync(join(webDist, "demo-solution.css"), ".demo{}\n");
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
const pack = () => buildBundle(project, { out });
|
|
52
|
+
const pack = () => buildBundle(project, { out }).then(r => r[0]!);
|
|
53
|
+
|
|
54
|
+
/** 造一个 web-only 用户方案(模块级契约 + 站点产物,无 api 段校验面最小) */
|
|
55
|
+
function addWebSolution(name: string, version = "0.1.0"): void {
|
|
56
|
+
const dir = join(project, "src", name);
|
|
57
|
+
mkdirSync(dir, { recursive: true });
|
|
58
|
+
writeFileSync(join(dir, "solution.yaml"), [
|
|
59
|
+
`name: ${name}`,
|
|
60
|
+
`version: ${version}`,
|
|
61
|
+
`summary: ${name} 用户方案`,
|
|
62
|
+
`changelog: ${name} 首个版本`,
|
|
63
|
+
"web:",
|
|
64
|
+
` entry: modules/${name}/${version}/entry.js`,
|
|
65
|
+
"",
|
|
66
|
+
].join("\n"));
|
|
67
|
+
const webDist = join(project, "web/dist/modules", name, version);
|
|
68
|
+
mkdirSync(webDist, { recursive: true });
|
|
69
|
+
writeFileSync(join(webDist, "entry.js"), "export default {};\n");
|
|
70
|
+
}
|
|
52
71
|
|
|
53
72
|
describe("buildBundle · 正路径", () => {
|
|
54
73
|
it("产出 out/<name>-<version>.tgz + 64 位 sha256,布局单根三分(manifest/api/web)", async () => {
|
|
@@ -64,6 +83,16 @@ describe("buildBundle · 正路径", () => {
|
|
|
64
83
|
expect(names, `缺成员 ${member}`).toContain(`demo-solution-0.1.0/${member}`);
|
|
65
84
|
});
|
|
66
85
|
|
|
86
|
+
it("只入包方案自己的前端模块:模板模块(home/login…)不随包", async () => {
|
|
87
|
+
// 安装器 deployWeb 把包内 web/modules 整树覆盖进目标站,模板产物随包会覆盖宿主同名前端
|
|
88
|
+
const hostWeb = join(project, "web/dist/modules/home/0.1.0");
|
|
89
|
+
mkdirSync(hostWeb, { recursive: true });
|
|
90
|
+
writeFileSync(join(hostWeb, "entry.js"), "export default {};\n");
|
|
91
|
+
const names = tarList((await pack()).tgz);
|
|
92
|
+
expect(names.some(n => n.includes("/web/modules/home/"))).toBe(false);
|
|
93
|
+
expect(names).toContain("demo-solution-0.1.0/web/modules/demo-solution/0.1.0/entry.js");
|
|
94
|
+
});
|
|
95
|
+
|
|
67
96
|
it("端到端确定性:同一工程两次打包 sha256 相等(C2-32)", async () => {
|
|
68
97
|
const r1 = await pack();
|
|
69
98
|
const r2 = await pack();
|
|
@@ -75,17 +104,161 @@ describe("buildBundle · 正路径", () => {
|
|
|
75
104
|
expect(tarList(r.tgz).some(n => n.endsWith("/modules.json"))).toBe(false);
|
|
76
105
|
});
|
|
77
106
|
|
|
107
|
+
it("solution.yaml 作为 bundle 契约(优先于 manifest.yaml,避开 oj 按 manifest.yaml 枚举 api 后端的冲突)", async () => {
|
|
108
|
+
// 根目录若用 manifest.yaml 当 bundle 契约,会被 oj 的 api 模块搜索枚举误判为 api 模块;
|
|
109
|
+
// 故解决方案改用 solution.yaml,bundle 工具优先认它(manifest.yaml 仅旧方案回退)。
|
|
110
|
+
const mf = join(project, "manifest.yaml");
|
|
111
|
+
const sf = join(project, "solution.yaml");
|
|
112
|
+
cpSync(mf, sf);
|
|
113
|
+
rmSync(mf);
|
|
114
|
+
const r = await pack();
|
|
115
|
+
expect(r.name).toBe("demo-solution");
|
|
116
|
+
expect(existsSync(r.tgz)).toBe(true);
|
|
117
|
+
// tgz 内固定为 manifest.yaml(安装侧只认这个名字),与源文件名无关
|
|
118
|
+
const names = tarList(r.tgz);
|
|
119
|
+
expect(names).toContain("demo-solution-0.1.0/manifest.yaml");
|
|
120
|
+
expect(names.some(n => n.endsWith("/solution.yaml"))).toBe(false);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("src/<模块>/solution.yaml 作为 bundle 契约(模块级方案:契约与所打包模块同位)", async () => {
|
|
124
|
+
// 单模块方案:契约紧贴它打包的模块 src/<name>/,比工程根更直观;
|
|
125
|
+
// 仍不在 api/ 下、也不叫 manifest.yaml,等价避开 oj 按 manifest.yaml 枚举 api 后端的冲突。
|
|
126
|
+
const mf = join(project, "manifest.yaml");
|
|
127
|
+
const sf = join(project, "src/demo-solution/solution.yaml");
|
|
128
|
+
mkdirSync(dirname(sf), { recursive: true });
|
|
129
|
+
cpSync(mf, sf);
|
|
130
|
+
rmSync(mf);
|
|
131
|
+
const r = await pack();
|
|
132
|
+
expect(r.name).toBe("demo-solution");
|
|
133
|
+
expect(existsSync(r.tgz)).toBe(true);
|
|
134
|
+
// tgz 内固定为 manifest.yaml(安装侧只认这个名字),与源文件名/源位置无关
|
|
135
|
+
const names = tarList(r.tgz);
|
|
136
|
+
expect(names).toContain("demo-solution-0.1.0/manifest.yaml");
|
|
137
|
+
expect(names.some(n => n.endsWith("/solution.yaml"))).toBe(false);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("src/ 下多份模块级契约 → 全部打包(多方案;B2)", async () => {
|
|
141
|
+
// 多方案正是本特性要支持的场景:每份契约独立校验、独立出包
|
|
142
|
+
rmSync(join(project, "manifest.yaml"));
|
|
143
|
+
addWebSolution("mod-a");
|
|
144
|
+
addWebSolution("mod-b");
|
|
145
|
+
const results = await buildBundle(project, { out });
|
|
146
|
+
expect(results.map(r => r.name).sort()).toEqual(["mod-a", "mod-b"]);
|
|
147
|
+
for (const r of results)
|
|
148
|
+
expect(existsSync(r.tgz)).toBe(true);
|
|
149
|
+
});
|
|
150
|
+
|
|
78
151
|
it("产物树混入 modules.json → 打包自检拒绝(C2-31 红线)", async () => {
|
|
79
152
|
writeFileSync(join(project, "web/dist/modules/demo-solution/0.1.0/modules.json"), "[]");
|
|
80
153
|
await expect(pack()).rejects.toThrow(/modules\.json/);
|
|
81
154
|
});
|
|
82
155
|
|
|
83
|
-
it("私钥/凭证混入包树 → 打包自检拒绝(P2-4 入包红线:*.pem/*.key/.env/id_ed25519/id_rsa)", async () => {
|
|
156
|
+
it("私钥/凭证混入包树 → 打包自检拒绝(P2-4 入包红线:*.pem/*.key/.env(.local…)/id_ed25519/id_rsa/*.p12/*.pfx)", async () => {
|
|
84
157
|
writeFileSync(join(project, "api/dist/demo-solution-0.1.0/signing.pem"), "-----BEGIN PRIVATE KEY-----\n");
|
|
85
158
|
await expect(pack()).rejects.toThrow(/私钥\/凭证/);
|
|
86
159
|
rmSync(join(project, "api/dist/demo-solution-0.1.0/signing.pem"));
|
|
87
160
|
writeFileSync(join(project, "web/dist/modules/demo-solution/0.1.0/.env"), "SECRET=1\n");
|
|
88
161
|
await expect(pack()).rejects.toThrow(/私钥\/凭证/);
|
|
162
|
+
rmSync(join(project, "web/dist/modules/demo-solution/0.1.0/.env"));
|
|
163
|
+
writeFileSync(join(project, "web/dist/modules/demo-solution/0.1.0/.env.production"), "SECRET=1\n");
|
|
164
|
+
await expect(pack()).rejects.toThrow(/私钥\/凭证/);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe("buildBundle · 多方案与内置排除(B3/B7/B8/B9/B10/B12)", () => {
|
|
169
|
+
it("b3 内置方案跳过:名单内的模块契约不出包,用户契约照打", async () => {
|
|
170
|
+
rmSync(join(project, "manifest.yaml"));
|
|
171
|
+
addWebSolution("crm");
|
|
172
|
+
addWebSolution("oj-market"); // 自带方案也带契约(第一方可分发形态)
|
|
173
|
+
mkdirSync(join(project, "src"), { recursive: true });
|
|
174
|
+
writeFileSync(join(project, "src/.ojm-builtin.json"), JSON.stringify({ modules: ["oj-market"] }));
|
|
175
|
+
const results = await buildBundle(project, { out });
|
|
176
|
+
expect(results.map(r => r.name)).toEqual(["crm"]);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("b6/B3 无名单文件 → 无内置概念,多份契约全部打包", async () => {
|
|
180
|
+
rmSync(join(project, "manifest.yaml"));
|
|
181
|
+
addWebSolution("crm");
|
|
182
|
+
addWebSolution("oj-market");
|
|
183
|
+
const results = await buildBundle(project, { out });
|
|
184
|
+
expect(results.map(r => r.name).sort()).toEqual(["crm", "oj-market"]);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("b7+B8:目录不同契约 name 撞车 → 被 name===目录名 校验拦下(B8 使 B7 向量不可达,决策见计划文档)", async () => {
|
|
188
|
+
rmSync(join(project, "manifest.yaml"));
|
|
189
|
+
addWebSolution("crm");
|
|
190
|
+
// 拷贝改目录忘改 name 的典型症状——B8 在收集期即拒,dist/ 不会出现同名产物
|
|
191
|
+
const dir = join(project, "src", "crm-copy");
|
|
192
|
+
mkdirSync(dir, { recursive: true });
|
|
193
|
+
writeFileSync(join(dir, "solution.yaml"), "name: crm\nversion: 0.1.0\nsummary: dup\nchangelog: x\nweb:\n entry: modules/crm/0.1.0/entry.js\n");
|
|
194
|
+
await expect(buildBundle(project, { out })).rejects.toThrow(/与目录名「crm-copy」不一致/);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("b8 模块级契约 name ≠ 目录名 → fail-fast", async () => {
|
|
198
|
+
rmSync(join(project, "manifest.yaml"));
|
|
199
|
+
const dir = join(project, "src", "crm");
|
|
200
|
+
mkdirSync(dir, { recursive: true });
|
|
201
|
+
writeFileSync(join(dir, "solution.yaml"), "name: erp\nversion: 0.1.0\nsummary: x\n");
|
|
202
|
+
await expect(pack()).rejects.toThrow(/与目录名「crm」不一致/);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("b9 两段式:多契约其一校验失败 → dist/ 零增量", async () => {
|
|
206
|
+
rmSync(join(project, "manifest.yaml"));
|
|
207
|
+
addWebSolution("crm");
|
|
208
|
+
// erp 契约违规(name 大写触发契约校验)
|
|
209
|
+
const dir = join(project, "src", "erp");
|
|
210
|
+
mkdirSync(dir, { recursive: true });
|
|
211
|
+
writeFileSync(join(dir, "solution.yaml"), "name: erp\nversion: 0.1.0\nsummary: x\n");
|
|
212
|
+
await expect(buildBundle(project, { out })).rejects.toThrow(/C2-08/);
|
|
213
|
+
expect(existsSync(join(out, "crm-0.1.0.tgz"))).toBe(false);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("b10 旧契约回退:src/<name>/manifest.yaml(非内置)参与收集", async () => {
|
|
217
|
+
rmSync(join(project, "manifest.yaml"));
|
|
218
|
+
const dir = join(project, "src", "crm");
|
|
219
|
+
mkdirSync(dir, { recursive: true });
|
|
220
|
+
writeFileSync(join(dir, "manifest.yaml"), "name: crm\nversion: 0.1.0\nsummary: x\nchangelog: x\nweb:\n entry: modules/crm/0.1.0/entry.js\n");
|
|
221
|
+
const webDist = join(project, "web/dist/modules/crm/0.1.0");
|
|
222
|
+
mkdirSync(webDist, { recursive: true });
|
|
223
|
+
writeFileSync(join(webDist, "entry.js"), "export default {};\n");
|
|
224
|
+
const results = await buildBundle(project, { out });
|
|
225
|
+
expect(results.map(r => r.name)).toEqual(["crm"]);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("b12 坏 .ojm-builtin.json → fail-fast 报路径与错因", async () => {
|
|
229
|
+
mkdirSync(join(project, "src"), { recursive: true });
|
|
230
|
+
writeFileSync(join(project, "src/.ojm-builtin.json"), "{ not json");
|
|
231
|
+
await expect(pack()).rejects.toThrow(/\.ojm-builtin\.json/);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("b4 --bundle <name>:按名打包;内置名拒绝;非法名拒绝", async () => {
|
|
235
|
+
rmSync(join(project, "manifest.yaml"));
|
|
236
|
+
addWebSolution("crm");
|
|
237
|
+
addWebSolution("erp");
|
|
238
|
+
const one = await buildBundle(project, { out, name: "crm" });
|
|
239
|
+
expect(one.map(r => r.name)).toEqual(["crm"]);
|
|
240
|
+
await expect(buildBundle(project, { out, name: "erp2" })).rejects.toThrow(/未找到「erp2」/);
|
|
241
|
+
writeFileSync(join(project, "src/.ojm-builtin.json"), JSON.stringify({ modules: ["erp"] }));
|
|
242
|
+
await expect(buildBundle(project, { out, name: "erp" })).rejects.toThrow(/框架自带方案/);
|
|
243
|
+
await expect(buildBundle(project, { out, name: "../evil" })).rejects.toThrow(/非法/);
|
|
244
|
+
await expect(buildBundle(project, { out, name: "Bad Name" })).rejects.toThrow(/非法/);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("b1 根契约优先于 src 契约(旧行为)+ B11 hint", async () => {
|
|
248
|
+
addWebSolution("crm"); // 根 manifest.yaml(demo-solution)仍在
|
|
249
|
+
const spy: string[] = [];
|
|
250
|
+
const orig = console.log;
|
|
251
|
+
console.log = (m?: unknown) => {
|
|
252
|
+
spy.push(String(m));
|
|
253
|
+
};
|
|
254
|
+
try {
|
|
255
|
+
const results = await buildBundle(project, { out });
|
|
256
|
+
expect(results.map(r => r.name)).toEqual(["demo-solution"]);
|
|
257
|
+
}
|
|
258
|
+
finally {
|
|
259
|
+
console.log = orig;
|
|
260
|
+
}
|
|
261
|
+
expect(spy.join("\n")).toMatch(/工程根契约优先/);
|
|
89
262
|
});
|
|
90
263
|
});
|
|
91
264
|
|
|
@@ -96,9 +269,9 @@ describe("buildBundle · 校验先于打包(fail-fast)", () => {
|
|
|
96
269
|
expect(existsSync(join(out, "Demo-1.0.0+build.5.tgz"))).toBe(false);
|
|
97
270
|
});
|
|
98
271
|
|
|
99
|
-
it("manifest.yaml
|
|
272
|
+
it("manifest.yaml 缺失且无用户方案:可读报错指向 ojm new(B5)", async () => {
|
|
100
273
|
rmSync(join(project, "manifest.yaml"));
|
|
101
|
-
await expect(pack()).rejects.toThrow(
|
|
274
|
+
await expect(pack()).rejects.toThrow(/没有可打包的用户方案/);
|
|
102
275
|
});
|
|
103
276
|
|
|
104
277
|
it("c2-31 分层核对:模块级 desc ≠ bundle summary → 放行(等值要求已移除)", async () => {
|
|
@@ -116,6 +289,11 @@ describe("buildBundle · 校验先于打包(fail-fast)", () => {
|
|
|
116
289
|
await expect(pack()).rejects.toThrow(/web\/dist\/modules/);
|
|
117
290
|
});
|
|
118
291
|
|
|
292
|
+
it("web/dist/modules 在但缺本方案目录 → 可读报错(不整树入包)", async () => {
|
|
293
|
+
rmSync(join(project, "web/dist/modules/demo-solution"), { recursive: true, force: true });
|
|
294
|
+
await expect(pack()).rejects.toThrow(/web\/dist\/modules\/demo-solution\//);
|
|
295
|
+
});
|
|
296
|
+
|
|
119
297
|
it("c2-12a:api.tables 与 schema.yaml 表集不一致 → 构建期拒绝", async () => {
|
|
120
298
|
const m = readManifest();
|
|
121
299
|
m.api.tables = ["note", "ghost"];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { InstallContext } from "../../../templates/
|
|
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,7 +13,7 @@ import { describe, expect, it } from "vitest";
|
|
|
13
13
|
import { parse } from "yaml";
|
|
14
14
|
// 跨端引用(双端一致钉):被测实现同仓分居两运行时(计划 §1「两运行时取舍」),
|
|
15
15
|
// 测试直接互调,比「两套件各读同一夹具」更强——漂移即本文件红。
|
|
16
|
-
import { preflight } from "../../../templates/
|
|
16
|
+
import { preflight } from "../../../templates/src/_platform/api/_lifecycle/steps/preflight";
|
|
17
17
|
import { checkLocalesComplete, checkTablesAgainstSchema, parseManifest, validateContract } from "../../manifest-contract";
|
|
18
18
|
|
|
19
19
|
const FIXTURES = join(dirname(fileURLToPath(import.meta.url)), "fixtures");
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// BDD:`ojm new <name>` 方案骨架生成(docs/plans/2026-10-22-1640-multi-solution-bundle-design.md §C)。
|
|
2
|
+
// N0 非工程拒绝 / N1 骨架 round-trip(生成→补产物→buildBundle 直接过)/ N2 --api
|
|
3
|
+
// N3 非法·保留·内置·重名拒绝 / N5 空名拒绝 / N6 历史布局拒绝 / N7 失败清理
|
|
4
|
+
import fs, { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync, writeFileSync as writeFileSyncOrig } from "node:fs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
8
|
+
import { buildBundle } from "../../bundle";
|
|
9
|
+
import { createSolution } from "../../new";
|
|
10
|
+
|
|
11
|
+
let project: string;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
project = mkdtempSync(join(tmpdir(), "ojm-new-"));
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
rmSync(project, { recursive: true, force: true });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
/** 最小 module-root 工程面:web.config.ts + src/(含一个自带模块 home 与内置名单) */
|
|
22
|
+
function makeModuleRootProject(builtin = true): void {
|
|
23
|
+
mkdirSync(join(project, "src", "home", "web"), { recursive: true });
|
|
24
|
+
writeFileSync(join(project, "src", "home", "web", "entry.ts"), "export default { name: \"home\" };\n");
|
|
25
|
+
writeFileSync(join(project, "web.config.ts"), "export default { baseUrl: \"\", modules: [] };\n");
|
|
26
|
+
if (builtin)
|
|
27
|
+
writeFileSync(join(project, "src", ".ojm-builtin.json"), JSON.stringify({ modules: ["home"] }));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 给已生成的方案补站点产物,模拟「ojm build 之后」的打包前置 */
|
|
31
|
+
function addWebArtifacts(name: string, version = "0.1.0"): void {
|
|
32
|
+
const webDist = join(project, "web/dist/modules", name, version);
|
|
33
|
+
mkdirSync(webDist, { recursive: true });
|
|
34
|
+
writeFileSync(join(webDist, "entry.js"), "export default {};\n");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe("createSolution · 守卫", () => {
|
|
38
|
+
it("n0 非 ojm 工程(无 web.config.ts / src/)→ 拒绝且零写入", () => {
|
|
39
|
+
expect(() => createSolution(project, "crm")).toThrow(/不是 ojm 工程/);
|
|
40
|
+
expect(existsSync(join(project, "src"))).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("n6 历史布局工程(web/src 树)→ 拒绝 + 迁移指引", () => {
|
|
44
|
+
mkdirSync(join(project, "web", "src", "old"), { recursive: true });
|
|
45
|
+
writeFileSync(join(project, "web.config.ts"), "export default { modules: [] };\n");
|
|
46
|
+
expect(() => createSolution(project, "crm")).toThrow(/module-root/);
|
|
47
|
+
expect(existsSync(join(project, "src", "crm"))).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("n3 非法名(大写/数字开头/路径穿越)→ 拒绝", () => {
|
|
51
|
+
makeModuleRootProject();
|
|
52
|
+
for (const bad of ["Crm", "9crm", "../crm", "bad name"])
|
|
53
|
+
expect(() => createSolution(project, bad)).toThrow(/非法/);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("n3 保留名(system)与内置名(home)→ 拒绝 + 指引", () => {
|
|
57
|
+
makeModuleRootProject();
|
|
58
|
+
expect(() => createSolution(project, "system")).toThrow(/保留名/);
|
|
59
|
+
expect(() => createSolution(project, "home")).toThrow(/框架自带/);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("n3 目录已存在(非内置共享目录撞名)→ 拒绝", () => {
|
|
63
|
+
makeModuleRootProject();
|
|
64
|
+
mkdirSync(join(project, "src", "crm"), { recursive: true });
|
|
65
|
+
expect(() => createSolution(project, "crm")).toThrow(/已存在/);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("n5 空名 → 拒绝", () => {
|
|
69
|
+
makeModuleRootProject();
|
|
70
|
+
expect(() => createSolution(project, "")).toThrow(/非法/);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("createSolution · 生成", () => {
|
|
75
|
+
it("n1 生成 web 骨架:solution.yaml/entry.ts/页面/双语 locales 齐备,layout 写进 handle", () => {
|
|
76
|
+
makeModuleRootProject();
|
|
77
|
+
const r = createSolution(project, "crm", { layout: "container" });
|
|
78
|
+
expect(r.files).toHaveLength(5);
|
|
79
|
+
const entry = readFileSync(join(project, "src/crm/web/entry.ts"), "utf-8");
|
|
80
|
+
expect(entry).toContain("layout: \"container\"");
|
|
81
|
+
expect(entry).toContain("crm:menu.main");
|
|
82
|
+
expect(readFileSync(join(project, "src/crm/solution.yaml"), "utf-8")).toContain("name: crm");
|
|
83
|
+
expect(JSON.parse(readFileSync(join(project, "src/crm/web/locales/zh-CN.json"), "utf-8")).menu.main).toBeTruthy();
|
|
84
|
+
expect(JSON.parse(readFileSync(join(project, "src/crm/web/locales/en-US.json"), "utf-8")).menu.main).toBeTruthy();
|
|
85
|
+
// --layout none
|
|
86
|
+
createSolution(project, "erp", { layout: "none" });
|
|
87
|
+
expect(readFileSync(join(project, "src/erp/web/entry.ts"), "utf-8")).toContain("layout: \"none\"");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("n1 round-trip:生成的骨架补产物后 buildBundle({ name }) 直接过", async () => {
|
|
91
|
+
makeModuleRootProject();
|
|
92
|
+
createSolution(project, "crm");
|
|
93
|
+
addWebArtifacts("crm");
|
|
94
|
+
const results = await buildBundle(project, { out: join(project, "dist"), name: "crm" });
|
|
95
|
+
expect(results.map(r => r.name)).toEqual(["crm"]);
|
|
96
|
+
expect(existsSync(results[0]!.tgz)).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("n2 --api:额外生成 api 骨架且 solution.yaml 含 api 段", () => {
|
|
100
|
+
makeModuleRootProject();
|
|
101
|
+
createSolution(project, "crm", { api: true });
|
|
102
|
+
expect(existsSync(join(project, "src/crm/api/manifest.yaml"))).toBe(true);
|
|
103
|
+
expect(readFileSync(join(project, "src/crm/api/manifest.yaml"), "utf-8")).toContain("name: crm");
|
|
104
|
+
expect(readFileSync(join(project, "src/crm/api/api.ts"), "utf-8")).toContain("/api/crm/ping");
|
|
105
|
+
const sol = readFileSync(join(project, "src/crm/solution.yaml"), "utf-8");
|
|
106
|
+
expect(sol).toContain("api:");
|
|
107
|
+
expect(sol).toContain("prefix: /api/crm");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("n7 生成中途失败 → 不残留半截 src/<name>/", () => {
|
|
111
|
+
makeModuleRootProject();
|
|
112
|
+
// 让第二次写盘(entry.ts)必失败:首写(solution.yaml)成功后触发清理路径——
|
|
113
|
+
// 预置冲突文件会先在「目录已存在」守卫被拒,测不到 rmSync 回滚(审查发现 n7 旧用例假绿)
|
|
114
|
+
let calls = 0;
|
|
115
|
+
const spy = vi.spyOn(fs, "writeFileSync").mockImplementation(((...args: unknown[]) => {
|
|
116
|
+
calls++;
|
|
117
|
+
if (calls === 2)
|
|
118
|
+
throw new Error("ENOSPC: disk full");
|
|
119
|
+
return (writeFileSyncOrig as (...a: unknown[]) => unknown)(...args);
|
|
120
|
+
}) as typeof fs.writeFileSync);
|
|
121
|
+
try {
|
|
122
|
+
expect(() => createSolution(project, "crm")).toThrow(/disk full/);
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
spy.mockRestore();
|
|
126
|
+
}
|
|
127
|
+
expect(existsSync(join(project, "src", "crm"))).toBe(false);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -49,7 +49,8 @@ describe.skipIf(!smokeReady)("真机冒烟(需 OJM_SMOKE_URL/OJM_SMOKE_USER/OJ
|
|
|
49
49
|
mkdirSync(webDist, { recursive: true });
|
|
50
50
|
writeFileSync(join(webDist, "entry.js"), "export default {};\n");
|
|
51
51
|
writeFileSync(join(webDist, "demo-solution.css"), ".demo{}\n");
|
|
52
|
-
const
|
|
52
|
+
const [built] = await buildBundle(project, { out: join(project, "dist") });
|
|
53
|
+
const { tgz } = built!;
|
|
53
54
|
|
|
54
55
|
const record = await installCommand(tgz, { url: SMOKE_URL ?? DEFAULT_SITE_URL, user: SMOKE_USER, pass: SMOKE_PASS });
|
|
55
56
|
expect(record.status).toBe("done");
|
package/src/manifest-contract.ts
CHANGED
|
@@ -16,11 +16,24 @@ export type ParseResult = { ok: true, manifest: BundleManifest } | { ok: false,
|
|
|
16
16
|
|
|
17
17
|
// ── 契约 §2/§4 正则与清单(与 preflight.ts 逐字对应;改动须先改契约再同步双端与夹具)──
|
|
18
18
|
|
|
19
|
-
/** C2-01:name
|
|
20
|
-
const NAME_RE = /^[a-z][a-z0-9-]{0,63}$/;
|
|
19
|
+
/** C2-01:name 白名单(导出供 ojm new/--bundle <name> 入口校验共用,DRY) */
|
|
20
|
+
export const NAME_RE = /^[a-z][a-z0-9-]{0,63}$/;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 方案名入口校验(`--bundle <name>` / `ojm new <name>` 共用,先校验再拼路径):
|
|
24
|
+
* 与契约 C2-01 同一条正则;入口先给可读错误,契约校验再做正式判罚。
|
|
25
|
+
* 放在这里而非 bundle.ts——new 入口不应拖入打包模块图(esbuild/deterministic-tar 等)。
|
|
26
|
+
*/
|
|
27
|
+
export function assertValidSolutionName(name: string, where: string): void {
|
|
28
|
+
if (!NAME_RE.test(name)) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`[ojm] ${where}:「${name}」非法——须为 kebab-case(小写字母/数字/连字符,首字符为小写字母)。`,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
21
34
|
|
|
22
35
|
/** §4.2 保留名清单(C2-01/13/14 共用;market 经 R9 恢复保留) */
|
|
23
|
-
const RESERVED_NAMES = new Set(["_platform", "system", "auth", "market", "framework", "health", "plugins", "_system"]);
|
|
36
|
+
export const RESERVED_NAMES = new Set(["_platform", "system", "auth", "market", "framework", "health", "plugins", "_system"]);
|
|
24
37
|
|
|
25
38
|
/** C2-02 字符集:[0-9A-Za-z.-](禁 +——oj validate_version 拒装,附录 B-1;禁 _——契约收紧) */
|
|
26
39
|
const VERSION_CHARS_RE = /^[0-9A-Z.-]+$/i;
|
|
@@ -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
|
-
|
|
32
|
-
writeFileSync(
|
|
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, "
|
|
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, "
|
|
79
|
-
const cfg = readFileSync(join(site.root, "
|
|
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, "
|
|
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
|
-
|
|
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
|
-
|
|
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 }))
|
package/src/market/bootstrap.ts
CHANGED
|
@@ -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
|
-
|
|
98
|
-
|
|
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(
|
|
103
|
+
const dbPath = path.join(root, "db.sqlite");
|
|
101
104
|
|
|
102
|
-
// 1)
|
|
105
|
+
// 1) 后端源码树 + config(模板由 cli 包内置,init 同源;与 cli→templates 依赖红线不冲突——那是运行期文件而非 import)
|
|
103
106
|
const cliRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
104
|
-
const
|
|
105
|
-
if (!existsSync(path.join(
|
|
106
|
-
throw new Error(`[ojm] 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
|
-
|
|
109
|
-
|
|
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:
|
|
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
|
-
|
|
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 落
|
|
153
|
-
await mintDevCert(path.join(
|
|
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(
|
|
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
|
-
"
|
|
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/market/publish.ts
CHANGED
|
@@ -118,9 +118,17 @@ async function buildFixture(args: { fixture?: string, staging: string, cdnDir: s
|
|
|
118
118
|
return { name: spec.name, version: spec.version, tgzPath, sha256, preExisted: before.exists };
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
/** `--real`:走既有 ojm build --bundle,产物搬到 CDN
|
|
121
|
+
/** `--real`:走既有 ojm build --bundle,产物搬到 CDN 目录(一次只发一份:多方案工程请显式指定) */
|
|
122
122
|
async function buildRealPackage(solutionRoot: string, staging: string, cdnDir: string): Promise<{ name: string, version: string, tgzPath: string, sha256: string, preExisted: boolean }> {
|
|
123
|
-
const
|
|
123
|
+
const builtList = await buildBundle(solutionRoot, { out: staging });
|
|
124
|
+
if (builtList.length !== 1) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
`[ojm] market publish --real 一次只发布一份方案,但该工程打出了 ${builtList.length} 份用户方案。`
|
|
127
|
+
+ "请改用显式链路:在方案工程里 `ojm build --bundle <name>`,再 `ojm sign dist/<name>-<version>.tgz --id <publisher>.<key>`,"
|
|
128
|
+
+ "把 tgz + .sig 放进 cdn/ 后 `ojm market build-index`。",
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
const built = builtList[0]!;
|
|
124
132
|
const tgzPath = path.join(cdnDir, `${built.name}-${built.version}.tgz`);
|
|
125
133
|
const before = captureExisting(tgzPath);
|
|
126
134
|
|