@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
|
@@ -9,8 +9,8 @@ import type { ScopedRequestLike } from "@oj-module/runtime/contract/errors";
|
|
|
9
9
|
// 不加载 @oj-module/runtime 重依赖(antd / pro-components 在 node 环境无法 import),仅桩 request、
|
|
10
10
|
// 透传真实 zod(供生成 client 的 DEV 响应校验走真实路径)。ContractApiError 由轻量子路径提供。
|
|
11
11
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
12
|
-
import { bindRequest } from "../../../templates/
|
|
13
|
-
import { httpMarketClient } from "../../../templates/
|
|
12
|
+
import { bindRequest } from "../../../templates/src/_platform/web/__client/api";
|
|
13
|
+
import { httpMarketClient } from "../../../templates/src/oj-market/web/api/defaultClient";
|
|
14
14
|
|
|
15
15
|
// index() 直连 request(不走生成 client),故把桩 request 做成可变实现,测试内统一指向同一 mock。
|
|
16
16
|
const { requestImpl } = vi.hoisted(() => ({
|
|
@@ -63,7 +63,7 @@ function mockRequest(responder: (c: Call) => { status?: number, data?: unknown }
|
|
|
63
63
|
|
|
64
64
|
// 让 index() 直连的 request 与生成 client 的 bindRequest 指向同一 mock,行为一致。
|
|
65
65
|
// 桥接:占位方法零参签名 ↔ ScopedRequestLike 真实签名,经 any 双向打通(测试桩,无运行时差异)。
|
|
66
|
-
function
|
|
66
|
+
function bindMock(m: ReturnType<typeof mockRequest>) {
|
|
67
67
|
bindRequest(m.req);
|
|
68
68
|
const req = m.req as unknown as Record<keyof typeof requestImpl, () => never>;
|
|
69
69
|
for (const k of Object.keys(requestImpl) as Array<keyof typeof requestImpl>)
|
|
@@ -75,12 +75,12 @@ describe("httpMarketClient · 统一委托生成 client 集成回归", () => {
|
|
|
75
75
|
|
|
76
76
|
beforeEach(() => {
|
|
77
77
|
mock = mockRequest(() => ({ data: {} }));
|
|
78
|
-
|
|
78
|
+
bindMock(mock);
|
|
79
79
|
});
|
|
80
80
|
|
|
81
81
|
it("meta() → GET _platform/market/meta,返回 { apiVersion }", async () => {
|
|
82
82
|
mock = mockRequest(() => ({ data: { apiVersion: "1.2.3" } }));
|
|
83
|
-
|
|
83
|
+
bindMock(mock);
|
|
84
84
|
const r = await httpMarketClient.meta();
|
|
85
85
|
expect(mock.calls).toEqual([{ method: "get", url: "_platform/market/meta", opts: undefined }]);
|
|
86
86
|
expect(r).toEqual({ apiVersion: "1.2.3" });
|
|
@@ -89,7 +89,7 @@ describe("httpMarketClient · 统一委托生成 client 集成回归", () => {
|
|
|
89
89
|
it("index() → GET _platform/market/index,返回 IndexRow[]", async () => {
|
|
90
90
|
const rows = [{ name: "a", latest: "1.0", installed: "1.0", enabled: true, upgradable: false }];
|
|
91
91
|
mock = mockRequest(() => ({ data: rows }));
|
|
92
|
-
|
|
92
|
+
bindMock(mock);
|
|
93
93
|
const r = await httpMarketClient.index();
|
|
94
94
|
expect(mock.calls[0]).toMatchObject({ method: "get", url: "_platform/market/index" });
|
|
95
95
|
expect(r).toEqual(rows);
|
|
@@ -97,7 +97,7 @@ describe("httpMarketClient · 统一委托生成 client 集成回归", () => {
|
|
|
97
97
|
|
|
98
98
|
it("index() 遇 503(未配置市场地址)→ [] 空态,不抛(PRD §5.3)", async () => {
|
|
99
99
|
mock = mockRequest(c => (c.url === "_platform/market/index" ? { status: 503 } : { data: {} }));
|
|
100
|
-
|
|
100
|
+
bindMock(mock);
|
|
101
101
|
const r = await httpMarketClient.index();
|
|
102
102
|
expect(r).toEqual([]);
|
|
103
103
|
// 503 是配置态而非故障:silentError 抑制全局 toast + retry:0 不重试(原
|
|
@@ -108,7 +108,7 @@ describe("httpMarketClient · 统一委托生成 client 集成回归", () => {
|
|
|
108
108
|
|
|
109
109
|
it("install(input) → POST _platform/market/install,body 含 name/version/source", async () => {
|
|
110
110
|
mock = mockRequest(() => ({ data: { task_id: "t-1" } }));
|
|
111
|
-
|
|
111
|
+
bindMock(mock);
|
|
112
112
|
const r = await httpMarketClient.install({ name: "alpha", version: "2.0", source: "file" });
|
|
113
113
|
expect(mock.calls[0]).toMatchObject({ method: "post", url: "_platform/market/install" });
|
|
114
114
|
expect(mock.calls[0].opts?.json).toEqual({ name: "alpha", version: "2.0", source: "file" });
|
|
@@ -117,7 +117,7 @@ describe("httpMarketClient · 统一委托生成 client 集成回归", () => {
|
|
|
117
117
|
|
|
118
118
|
it("install(input) 缺省 version/source → null / cdn 兜底", async () => {
|
|
119
119
|
mock = mockRequest(() => ({ data: { task_id: "t-2" } }));
|
|
120
|
-
|
|
120
|
+
bindMock(mock);
|
|
121
121
|
await httpMarketClient.install({ name: "beta" });
|
|
122
122
|
expect(mock.calls[0].opts?.json).toEqual({ name: "beta", version: null, source: "cdn" });
|
|
123
123
|
});
|
|
@@ -125,7 +125,7 @@ describe("httpMarketClient · 统一委托生成 client 集成回归", () => {
|
|
|
125
125
|
it("task(id) → GET _platform/market/task? 带 id", async () => {
|
|
126
126
|
const rec = { id: "t-1", name: "a", version: "1.0", status: "done", stage: "done", progress: 100, message: "ok", stages: [], startedAt: 1 };
|
|
127
127
|
mock = mockRequest(() => ({ data: rec }));
|
|
128
|
-
|
|
128
|
+
bindMock(mock);
|
|
129
129
|
const r = await httpMarketClient.task("t-1");
|
|
130
130
|
expect(mock.calls[0]).toMatchObject({ method: "get", url: "_platform/market/task" });
|
|
131
131
|
expect(mock.calls[0].opts?.searchParams).toEqual({ id: "t-1" });
|
|
@@ -134,7 +134,7 @@ describe("httpMarketClient · 统一委托生成 client 集成回归", () => {
|
|
|
134
134
|
|
|
135
135
|
it("enable(input) → POST _platform/market/enable,body {name,enabled}", async () => {
|
|
136
136
|
mock = mockRequest(() => ({ data: { ok: true, message: "done" } }));
|
|
137
|
-
|
|
137
|
+
bindMock(mock);
|
|
138
138
|
const r = await httpMarketClient.enable({ name: "a", enabled: false });
|
|
139
139
|
expect(mock.calls[0]).toMatchObject({ method: "post", url: "_platform/market/enable" });
|
|
140
140
|
expect(mock.calls[0].opts?.json).toEqual({ name: "a", enabled: false });
|
|
@@ -144,7 +144,7 @@ describe("httpMarketClient · 统一委托生成 client 集成回归", () => {
|
|
|
144
144
|
it("history() → GET _platform/market/history,返回 MarketHistory[]", async () => {
|
|
145
145
|
const hist = [{ id: 1, solution: "a", version: "1.0", action: "install", status: "ok", operator: null, message: null, created_at: 1 }];
|
|
146
146
|
mock = mockRequest(() => ({ data: hist }));
|
|
147
|
-
|
|
147
|
+
bindMock(mock);
|
|
148
148
|
const r = await httpMarketClient.history();
|
|
149
149
|
expect(mock.calls[0]).toMatchObject({ method: "get", url: "_platform/market/history" });
|
|
150
150
|
expect(r).toEqual(hist);
|
|
@@ -152,7 +152,7 @@ describe("httpMarketClient · 统一委托生成 client 集成回归", () => {
|
|
|
152
152
|
|
|
153
153
|
it("uninstallImpact(name) → GET _platform/market/uninstall-impact? 带 name", async () => {
|
|
154
154
|
mock = mockRequest(() => ({ data: { name: "a", affectedRoles: 2, affectedUsers: 5 } }));
|
|
155
|
-
|
|
155
|
+
bindMock(mock);
|
|
156
156
|
const r = await httpMarketClient.uninstallImpact("a");
|
|
157
157
|
expect(mock.calls[0]).toMatchObject({ method: "get", url: "_platform/market/uninstall-impact" });
|
|
158
158
|
expect(mock.calls[0].opts?.searchParams).toEqual({ name: "a" });
|
|
@@ -161,7 +161,7 @@ describe("httpMarketClient · 统一委托生成 client 集成回归", () => {
|
|
|
161
161
|
|
|
162
162
|
it("uninstall(name) → POST _platform/market/uninstall,body {name}", async () => {
|
|
163
163
|
mock = mockRequest(() => ({ data: { ok: true, message: "removed" } }));
|
|
164
|
-
|
|
164
|
+
bindMock(mock);
|
|
165
165
|
const r = await httpMarketClient.uninstall("a");
|
|
166
166
|
expect(mock.calls[0]).toMatchObject({ method: "post", url: "_platform/market/uninstall" });
|
|
167
167
|
expect(mock.calls[0].opts?.json).toEqual({ name: "a" });
|
package/src/contract/check.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import type { ApiModuleRoot } from "../layout";
|
|
1
2
|
import type { HandlerRow } from "./handler-scan";
|
|
2
3
|
import type { IrEndpoint } from "./ir";
|
|
3
4
|
import type { OpenApiDoc } from "./openapi-ir";
|
|
4
5
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
5
6
|
import { dirname, join, relative, sep } from "node:path";
|
|
6
|
-
import { resolveConfigPath } from "../layout";
|
|
7
|
+
import { apiModuleRoots, apiModulesSrc, apiSrcOf, legacyApiModuleRoots, resolveConfigPath, tryResolveLayout, webSrcOf } from "../layout";
|
|
7
8
|
import { readOjAnonymousPaths } from "../oj-config";
|
|
8
9
|
import { emitClient } from "./emit-client";
|
|
9
10
|
import { planStubWrites } from "./emit-stub";
|
|
@@ -47,9 +48,12 @@ export interface ApiExemption {
|
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
51
|
+
* 加载豁免清单:按布局找位置,`opts.exempt` 为绝对/相对路径时覆盖默认。
|
|
52
|
+
* 候选顺序(第一个存在的胜出):
|
|
53
|
+
* 1. `<cwd>/.ojm-api-exempt.json` —— module-root 布局(config.yaml 在工程根)
|
|
54
|
+
* 2. `<cwd>/api/.ojm-api-exempt.json` —— 历史布局(api/ 即工程根)
|
|
55
|
+
* 3. `<cwd>/api/.ram-api-exempt.json` —— R4:存量工程旧名不因改名失效
|
|
56
|
+
* 文件缺失或 JSON 损坏 → 空豁免 `{}`(绝不抛错)。
|
|
53
57
|
* 反常识坑:清单是严格 JSON.parse——写 `//` 注释会解析失败、静默回退空豁免
|
|
54
58
|
* (豁免全失效且无报错);说明文字请放 `_comment` 字段(本加载器只读
|
|
55
59
|
* modules/paths,多余字段天然忽略)。
|
|
@@ -70,18 +74,29 @@ export function loadExemption(opts: { cwd: string, exempt?: string }): ApiExempt
|
|
|
70
74
|
}
|
|
71
75
|
}
|
|
72
76
|
|
|
73
|
-
/**
|
|
74
|
-
|
|
75
|
-
|
|
77
|
+
/**
|
|
78
|
+
* 豁免清单候选路径(按优先级;`init` 只产第一条)。
|
|
79
|
+
*
|
|
80
|
+
* 位置由布局决定而不是常量钉死:module-root 布局 config.yaml 在工程根、随行的
|
|
81
|
+
* `.ojm-api-exempt.json` 也在根;历史布局两者都在 `api/` 下。写死 `api/` 会让新
|
|
82
|
+
* 布局工程的豁免**静默失效**——`loadExemption` 缺文件即回退空豁免,`ojm api --check`
|
|
83
|
+
* 会突然对 auth/web 报一堆「有 handler 无契约」。
|
|
84
|
+
*/
|
|
85
|
+
const EXEMPT_FILES = [
|
|
86
|
+
".ojm-api-exempt.json",
|
|
87
|
+
"api/.ojm-api-exempt.json",
|
|
88
|
+
"api/.ram-api-exempt.json", // R4:旧名只读回退
|
|
89
|
+
] as const;
|
|
76
90
|
|
|
77
91
|
function resolveExemptPath(cwd: string, exempt?: string): string {
|
|
78
92
|
if (exempt)
|
|
79
93
|
return exempt;
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
94
|
+
for (const rel of EXEMPT_FILES) {
|
|
95
|
+
const candidate = join(cwd, rel);
|
|
96
|
+
if (existsSync(candidate))
|
|
97
|
+
return candidate;
|
|
98
|
+
}
|
|
99
|
+
return join(cwd, EXEMPT_FILES[0]);
|
|
85
100
|
}
|
|
86
101
|
|
|
87
102
|
/** 路由身份串(如 "order/list"、"auth/login")的模块段 = 首段 */
|
|
@@ -130,15 +145,18 @@ function walkWsFiles(dir: string, out: string[] = []): string[] {
|
|
|
130
145
|
* 只 warn 不 error:带 Bearer/cookie 的受保护 WS 是正当用法。
|
|
131
146
|
* 读不到 config 直接不判——缺配置时做不出有依据的断言。
|
|
132
147
|
*/
|
|
133
|
-
function checkWsAnonymous(
|
|
148
|
+
function checkWsAnonymous(roots: readonly ApiModuleRoot[], configPath: string): CheckViolation[] {
|
|
149
|
+
const wsFiles = roots.flatMap(root => walkWsFiles(root.dir).map(file => ({ file, root })));
|
|
134
150
|
if (!existsSync(configPath) || wsFiles.length === 0)
|
|
135
151
|
return [];
|
|
136
152
|
const anonymous = readOjAnonymousPaths(configPath);
|
|
137
153
|
if (anonymous.length === 0)
|
|
138
154
|
return [];
|
|
139
155
|
return wsFiles
|
|
140
|
-
.map((file) => {
|
|
141
|
-
const relDir = relative(
|
|
156
|
+
.map(({ file, root }) => {
|
|
157
|
+
const relDir = [root.name, relative(root.dir, dirname(file)).split(sep).filter(Boolean).join("/")]
|
|
158
|
+
.filter(Boolean)
|
|
159
|
+
.join("/");
|
|
142
160
|
return { file, path: `/${relDir}/ws` };
|
|
143
161
|
})
|
|
144
162
|
.filter(({ path }) => !anonymous.includes(path))
|
|
@@ -146,7 +164,7 @@ function checkWsAnonymous(wsFiles: string[], apiSrcDir: string, configPath: stri
|
|
|
146
164
|
level: "warn" as const,
|
|
147
165
|
kind: "ws-anonymous-missing" as const,
|
|
148
166
|
filePath: file,
|
|
149
|
-
message: `[ojm-api] WS 路由 ${path} 不在 auth.anonymous_paths——oj 0.1.30 起 WS 握手过鉴权守卫,未带凭据的客户端会握手 401(表现为「文件在却连不上」)。若该 WS 本就要求鉴权(客户端带 Bearer/Cookie)可忽略本提示;否则请在
|
|
167
|
+
message: `[ojm-api] WS 路由 ${path} 不在 auth.anonymous_paths——oj 0.1.30 起 WS 握手过鉴权守卫,未带凭据的客户端会握手 401(表现为「文件在却连不上」)。若该 WS 本就要求鉴权(客户端带 Bearer/Cookie)可忽略本提示;否则请在 config.yaml 的 auth.anonymous_paths 补上 ${path}。`,
|
|
150
168
|
}));
|
|
151
169
|
}
|
|
152
170
|
|
|
@@ -279,18 +297,29 @@ async function checkFromOpenapi(openapiPath: string, cwd: string, _exemption: Ap
|
|
|
279
297
|
const doc = JSON.parse(readFileSync(openapiPath, "utf8")) as OpenApiDoc;
|
|
280
298
|
const ir = irFromOpenapi(doc);
|
|
281
299
|
const violations: CheckViolation[] = [];
|
|
300
|
+
const layout = tryResolveLayout(cwd);
|
|
301
|
+
// 契约工具的扫描单元是「模块 api 根」而不是整棵源码树:module-root 布局里
|
|
302
|
+
// `src/<name>/api` 的 `api` 段不属于路由,且 `web/__client/api.ts` 不能被当 handler。
|
|
303
|
+
const roots = layout ? apiModuleRoots(layout) : legacyApiModuleRoots(cwd);
|
|
304
|
+
const clientDirOf = (module: string) =>
|
|
305
|
+
join(layout ? webSrcOf(layout, module) : join(cwd, "web/src", module), "__client");
|
|
282
306
|
for (const [module, eps] of groupIrByModule(ir)) {
|
|
283
307
|
const client = emitClient(eps, { target: "module", module });
|
|
284
|
-
const clientDir =
|
|
308
|
+
const clientDir = clientDirOf(module);
|
|
285
309
|
for (const [file, content] of [["api.ts", client["api.ts"]], ["api.schemas.ts", client["api.schemas.ts"]]] as const) {
|
|
286
310
|
const p = join(clientDir, file);
|
|
287
311
|
if (!existsSync(p) || readFileSync(p, "utf8") !== content)
|
|
288
312
|
violations.push({ level: "error", kind: "artifact-stale", message: `[ojm-api] 生成物过期或缺失:${relative(cwd, p)}——请重跑 ojm api 使其与 openapi.json 同步。`, filePath: p });
|
|
289
313
|
}
|
|
290
314
|
}
|
|
291
|
-
const apiSrcDir = join(cwd, "api/src");
|
|
315
|
+
const apiSrcDir = layout ? apiModulesSrc(layout) : join(cwd, "api/src");
|
|
292
316
|
if (existsSync(apiSrcDir)) {
|
|
293
|
-
const stubWrites = await planStubWrites(ir, {
|
|
317
|
+
const stubWrites = await planStubWrites(ir, {
|
|
318
|
+
apiSrcDir,
|
|
319
|
+
// stub 落模块的 api 侧:module-root 布局是 `<src>/<m>/api`,不是 `<src>/<m>`
|
|
320
|
+
apiRootOf: layout ? (m: string) => apiSrcOf(layout, m) : undefined,
|
|
321
|
+
implemented: implementedRouteKeys(roots),
|
|
322
|
+
});
|
|
294
323
|
for (const w of stubWrites) {
|
|
295
324
|
if (w.action === "create")
|
|
296
325
|
violations.push({ level: "error", kind: "artifact-stale", message: `[ojm-api] stub 缺失:${relative(cwd, w.filePath)}——请重跑 ojm api 生成。`, filePath: w.filePath });
|
|
@@ -318,19 +347,18 @@ export async function checkApi(opts: { cwd: string, exempt?: string }): Promise<
|
|
|
318
347
|
throw new Error(`[ojm-api] ${opts.cwd} 下未发现 openapi.json——请先运行 oj v0.1.73 的 \`oj openapi -c config -d api/src -b /api -o openapi.json\` 生成(OpenAPI 3.1,含全部模块)。\`ojm api --check\` 的单一事实源即 openapi.json,不再消费旧 contract.ts。`);
|
|
319
348
|
}
|
|
320
349
|
|
|
321
|
-
// ② route
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
350
|
+
// ② route 双向对账(仅后端树存在才有意义)
|
|
351
|
+
const layout = tryResolveLayout(opts.cwd);
|
|
352
|
+
const apiSrcDir = layout ? apiModulesSrc(layout) : join(opts.cwd, "api/src");
|
|
353
|
+
const roots = layout ? apiModuleRoots(layout) : legacyApiModuleRoots(opts.cwd);
|
|
354
|
+
if (existsSync(apiSrcDir)) {
|
|
355
|
+
const handlers = roots.flatMap(root =>
|
|
356
|
+
walkApiFiles(root.dir).flatMap(f => scanHandlerFile(f, root.dir, root.name)));
|
|
325
357
|
violations.push(...reconcileRoutes(uniDevIr, handlers, exemption));
|
|
326
358
|
}
|
|
327
359
|
|
|
328
360
|
// ④ WS 路由鉴权对账(oj 0.1.30 起握手过鉴权守卫)
|
|
329
|
-
violations.push(...checkWsAnonymous(
|
|
330
|
-
walkWsFiles(join(opts.cwd, "api/src")),
|
|
331
|
-
join(opts.cwd, "api/src"),
|
|
332
|
-
resolveConfigPath(opts.cwd),
|
|
333
|
-
));
|
|
361
|
+
violations.push(...checkWsAnonymous(roots, resolveConfigPath(opts.cwd)));
|
|
334
362
|
|
|
335
363
|
// ③ routes.js diff
|
|
336
364
|
const distDir = join(opts.cwd, "api/dist");
|
|
@@ -2,7 +2,7 @@ import type { OjSchemaDiag } from "./emit-oj-schema";
|
|
|
2
2
|
import type { IrEndpoint } from "./ir";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
-
import { dirname } from "node:path";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
6
|
import { emitOjSchemaLine } from "./emit-oj-schema";
|
|
7
7
|
import { splitRoute, wirePrefixOf } from "./ir";
|
|
8
8
|
import { jsonSchemaToSample } from "./json-schema-sample";
|
|
@@ -32,8 +32,14 @@ export interface StubWrite {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
export interface PlanStubOptions {
|
|
35
|
-
/** oj api 源码根(如 "api/src"
|
|
35
|
+
/** oj api 源码根(如 "api/src");仅用于未提供 `apiRootOf` 时的路径拼接 */
|
|
36
36
|
apiSrcDir: string
|
|
37
|
+
/**
|
|
38
|
+
* 模块名 → 该模块 api 侧目录。module-root 布局传 `(m) => apiSrcOf(layout, m)`
|
|
39
|
+
* (`<src>/<m>/api`)——stub 必须落进模块的 api 侧,串到 `<src>/<m>/` 会被 oj 当成
|
|
40
|
+
* 模块根下的普通文件;历史布局缺省即 `<apiSrcDir>/<m>`。
|
|
41
|
+
*/
|
|
42
|
+
apiRootOf?: (module: string) => string
|
|
37
43
|
/** 读已有文件(不存在返回 undefined);缺省真实 fs */
|
|
38
44
|
readFile?: (path: string) => string | undefined
|
|
39
45
|
/** 格式化注入缝(测试/特殊工程);缺省恒等——模板本身天然 lint-clean(F6) */
|
|
@@ -119,7 +125,9 @@ export async function planStubWrites(ir: IrEndpoint[], opts: PlanStubOptions): P
|
|
|
119
125
|
|
|
120
126
|
// 按(模块段 + 目录镜像)分组 → 一个 api.ts
|
|
121
127
|
// 已在别处(模块根嵌套组 / 其他文件)实现的端点不进 stub 计划。
|
|
122
|
-
|
|
128
|
+
// 键保留模块与目录两段:落盘路径要经 `apiRootOf` 定位模块 api 根(module-root 布局
|
|
129
|
+
// 多一层 `api`),不能只拼成一条相对串。
|
|
130
|
+
const groups = new Map<string, { module: string, dir: string, endpoints: IrEndpoint[] }>();
|
|
123
131
|
for (const ep of ir) {
|
|
124
132
|
if (opts.implemented) {
|
|
125
133
|
const ojFullAbs = (ep.apiPrefix + ep.route).replace(/\/{2,}/g, "/");
|
|
@@ -127,16 +135,20 @@ export async function planStubWrites(ir: IrEndpoint[], opts: PlanStubOptions): P
|
|
|
127
135
|
if ([ojFullAbs, contractAbs].some(a => opts.implemented!.has(`${ep.method} ${a}`)))
|
|
128
136
|
continue;
|
|
129
137
|
}
|
|
138
|
+
const module = ep.apiPrefix.replace(/^\//, "");
|
|
130
139
|
const { dir } = splitRoute(ep.route);
|
|
131
|
-
const
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
140
|
+
const key = `${module}\u0000${dir}`;
|
|
141
|
+
const group = groups.get(key);
|
|
142
|
+
if (group)
|
|
143
|
+
group.endpoints.push(ep);
|
|
144
|
+
else
|
|
145
|
+
groups.set(key, { module, dir, endpoints: [ep] });
|
|
135
146
|
}
|
|
136
147
|
|
|
137
148
|
const writes: StubWrite[] = [];
|
|
138
|
-
for (const [
|
|
139
|
-
const
|
|
149
|
+
for (const [, { module, dir, endpoints }] of [...groups.entries()].sort()) {
|
|
150
|
+
const apiRoot = opts.apiRootOf ? opts.apiRootOf(module) : join(opts.apiSrcDir, module);
|
|
151
|
+
const filePath = join(apiRoot, dir, "api.ts");
|
|
140
152
|
// 模板天然 lint-clean(F6)→ 直接哈希发射内容;注入缝仅供测试/特殊工程对齐
|
|
141
153
|
const body = await fix(emitStubBody(endpoints, opts.diag), filePath);
|
|
142
154
|
const fingerprint = `// ojm-api:stub ${endpoints.map(e => e.name).sort().join(",")} sha256:${hashContent(body)}\n`;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ApiModuleRoot } from "../layout";
|
|
1
2
|
import type { IrEndpoint } from "./ir";
|
|
2
3
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
3
4
|
import { dirname, join, relative, sep } from "node:path";
|
|
@@ -205,13 +206,19 @@ export function handlerAbs(h: HandlerRow): string {
|
|
|
205
206
|
return h.tail?.startsWith("/") ? h.tail : `/${h.full}`;
|
|
206
207
|
}
|
|
207
208
|
|
|
208
|
-
|
|
209
|
+
/**
|
|
210
|
+
* `apiDir` 是**模块的 api 侧目录**(module-root → `src/<name>/api`,历史 → `api/src/<name>`),
|
|
211
|
+
* `moduleName` 补进路由镜像的首段。两个参数而不是「扫描根 + 相对路径」,是因为 module-root
|
|
212
|
+
* 布局在扫描根与 handler 目录之间多一层 `api`,而它不属于路由(见 layout.apiModuleRoots)。
|
|
213
|
+
*/
|
|
214
|
+
export function scanHandlerFile(filePath: string, apiDir: string, moduleName = ""): HandlerRow[] {
|
|
209
215
|
const text = readFileSync(filePath, "utf8");
|
|
210
216
|
const sf = ts.createSourceFile(filePath, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
211
217
|
const routes = collectRouteDecls(sf);
|
|
212
218
|
const decls = collectObjectDecls(sf);
|
|
213
219
|
const callDecls = collectCallDecls(sf);
|
|
214
|
-
const
|
|
220
|
+
const inner = relative(apiDir, dirname(filePath)).split(sep).filter(Boolean).join("/");
|
|
221
|
+
const relDir = [moduleName, inner].filter(Boolean).join("/");
|
|
215
222
|
const rows: HandlerRow[] = [];
|
|
216
223
|
const seen = new Set<ts.Node>();
|
|
217
224
|
|
|
@@ -312,15 +319,17 @@ export function walkApiFiles(dir: string, out: string[] = []): string[] {
|
|
|
312
319
|
* emit-stub 据此跳过「已在别处(模块根嵌套组 / 其他文件)实现」的端点——否则 DDD
|
|
313
320
|
* reshape 后镜像 stub 与真实 handler 双写同路由,oj 路由表出现重复注册。
|
|
314
321
|
*/
|
|
315
|
-
export function implementedRouteKeys(
|
|
322
|
+
export function implementedRouteKeys(roots: readonly ApiModuleRoot[]): Set<string> {
|
|
316
323
|
const keys = new Set<string>();
|
|
317
|
-
for (const
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
+
for (const root of roots) {
|
|
325
|
+
for (const f of walkApiFiles(root.dir)) {
|
|
326
|
+
const text = readFileSync(f, "utf8");
|
|
327
|
+
// stub 镜像文件不算实现——它们覆盖的路由需照常报「stub 缺失/待更新」
|
|
328
|
+
if (/^\/\/ (?:ojm|ram)-api:stub /.test(text))
|
|
329
|
+
continue;
|
|
330
|
+
for (const row of scanHandlerFile(f, root.dir, root.name))
|
|
331
|
+
keys.add(pathKey(row.method, handlerAbs(row)));
|
|
332
|
+
}
|
|
324
333
|
}
|
|
325
334
|
return keys;
|
|
326
335
|
}
|
package/src/contract/run.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { dirname, join } from "node:path";
|
|
|
8
8
|
import process from "node:process";
|
|
9
9
|
import { promisify } from "node:util";
|
|
10
10
|
import { stringify as stringifyYaml } from "yaml";
|
|
11
|
+
import { apiModuleRoots, apiModulesSrc, apiSrcOf, legacyApiModuleRoots, tryResolveLayout, webSrcOf } from "../layout";
|
|
11
12
|
import { emitClient } from "./emit-client";
|
|
12
13
|
import { applyStubWrites, planStubWrites } from "./emit-stub";
|
|
13
14
|
import { implementedRouteKeys } from "./handler-scan";
|
|
@@ -81,19 +82,27 @@ async function runFromOpenapi(openapiPath: string, cwd: string, result: RunResul
|
|
|
81
82
|
const doc = JSON.parse(readFileSync(openapiPath, "utf8")) as OpenApiDoc;
|
|
82
83
|
const ir = irFromOpenapi(doc, { rawPaths });
|
|
83
84
|
const byModule = groupIrByModule(ir);
|
|
85
|
+
const layout = tryResolveLayout(cwd);
|
|
84
86
|
result.contracts += byModule.size;
|
|
85
87
|
for (const [module, eps] of byModule) {
|
|
86
88
|
const client = emitClient(eps, { target: "module", module });
|
|
87
|
-
const clientDir = join(cwd, "web/src", module, "__client");
|
|
89
|
+
const clientDir = join(layout ? webSrcOf(layout, module) : join(cwd, "web/src", module), "__client");
|
|
88
90
|
writeIfChanged(join(clientDir, "api.ts"), client["api.ts"], result);
|
|
89
91
|
writeIfChanged(join(clientDir, "api.schemas.ts"), client["api.schemas.ts"], result);
|
|
90
92
|
}
|
|
91
93
|
// stub:oj 目录镜像树(人碰过的文件永不写——plan 阶段已按指纹判好)。
|
|
92
94
|
// 已实现键全量扫一遍传入:嵌套组(模块根单文件)实现的端点不再生成镜像 stub。
|
|
93
|
-
const apiSrcDir = join(cwd, "api/src");
|
|
95
|
+
const apiSrcDir = layout ? apiModulesSrc(layout) : join(cwd, "api/src");
|
|
96
|
+
const roots = layout ? apiModuleRoots(layout) : legacyApiModuleRoots(cwd);
|
|
94
97
|
if (existsSync(apiSrcDir)) {
|
|
95
98
|
const diag: OjSchemaDiag = { warnings: [] };
|
|
96
|
-
const writes = await planStubWrites(ir, {
|
|
99
|
+
const writes = await planStubWrites(ir, {
|
|
100
|
+
apiSrcDir,
|
|
101
|
+
// stub 落模块的 api 侧:module-root 布局是 `<src>/<m>/api`,不是 `<src>/<m>`
|
|
102
|
+
apiRootOf: layout ? (m: string) => apiSrcOf(layout, m) : undefined,
|
|
103
|
+
diag,
|
|
104
|
+
implemented: implementedRouteKeys(roots),
|
|
105
|
+
});
|
|
97
106
|
const counts = applyStubWrites(writes);
|
|
98
107
|
result.stubs.created += counts.created;
|
|
99
108
|
result.stubs.updated += counts.updated;
|
|
@@ -126,7 +135,8 @@ export async function runApiDocs(cwd: string, opts: { redocBin?: string, fetchJs
|
|
|
126
135
|
}
|
|
127
136
|
const doc = JSON.parse(readFileSync(openapiPath, "utf8")) as { paths?: Record<string, Record<string, unknown>> };
|
|
128
137
|
const paths = doc.paths ?? {};
|
|
129
|
-
const
|
|
138
|
+
const layout = tryResolveLayout(cwd);
|
|
139
|
+
const isUniDev = existsSync(layout ? apiModulesSrc(layout) : join(cwd, "api/src"));
|
|
130
140
|
|
|
131
141
|
const outDir = isUniDev ? join(cwd, "api/docs") : join(cwd, "docs/api");
|
|
132
142
|
mkdirSync(outDir, { recursive: true });
|
package/src/dev.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* - 布局探测 → layout.ts
|
|
11
11
|
* - 工程 mock 约定 → dev-mock.ts
|
|
12
12
|
*
|
|
13
|
-
* 两种形态(按
|
|
13
|
+
* 两种形态(按 config.yaml 是否存在自动判定):
|
|
14
14
|
* 全栈 /api/* 反代 oj(oj 自带热更,ojm 不重启它),模块源码变更 → 重建 → SSE 刷新
|
|
15
15
|
* 纯前端 /api/* 走工程 mock(mock/*.mock.mjs 约定,未命中 404),行为与历史版本一致
|
|
16
16
|
*
|
|
@@ -28,6 +28,7 @@ import http from "node:http";
|
|
|
28
28
|
import { join, resolve } from "node:path";
|
|
29
29
|
import process from "node:process";
|
|
30
30
|
import readline from "node:readline";
|
|
31
|
+
import { syncAliasPaths } from "./alias-paths";
|
|
31
32
|
import { buildModules } from "./build";
|
|
32
33
|
import { loadContractMocks, resolveMock } from "./contract/mock";
|
|
33
34
|
import { loadProjectMocks, mockStatusCode } from "./dev-mock";
|
|
@@ -35,6 +36,7 @@ import { createReloadHub, proxyApi, proxyWsUpgrade, sseScript } from "./dev-prox
|
|
|
35
36
|
import { loadHtmlTransform } from "./html-transform";
|
|
36
37
|
import { resolveConfigPath, resolveLayout } from "./layout";
|
|
37
38
|
import { startOj } from "./oj";
|
|
39
|
+
import { requireModuleRootCapability } from "./oj-caps";
|
|
38
40
|
import { ensureDevCert } from "./oj-cert";
|
|
39
41
|
import { readOjApiPrefix } from "./oj-config";
|
|
40
42
|
import { createStaticHandler, decodeReqPath, listenOnFreePort } from "./static-handler";
|
|
@@ -48,8 +50,8 @@ export interface DevOptions {
|
|
|
48
50
|
shellDist?: string
|
|
49
51
|
/** 注入重建函数(测试);默认 buildModules(不合并全站,产物只写模块) */
|
|
50
52
|
buildModulesFn?: (projectRoot: string) => Promise<unknown>
|
|
51
|
-
/** 注入桩 oj(测试);默认 startOj
|
|
52
|
-
ojStarter?: (configPath: string, base: string,
|
|
53
|
+
/** 注入桩 oj(测试);默认 startOj;纯前端工程不调用。第三参为目录(→ --api-path)或已拼好的旗标数组(→ --module-root) */
|
|
54
|
+
ojStarter?: (configPath: string, base: string, apiSource: string | string[]) => OjProcess
|
|
53
55
|
/** 强制纯前端形态(不起 oj,/api 走 mock) */
|
|
54
56
|
frontendOnly?: boolean
|
|
55
57
|
/** 启用 Vite 风格快捷键(默认仅 TTY 启用;测试可强制开) */
|
|
@@ -70,6 +72,10 @@ export async function devServer(projectRoot: string, opts: DevOptions = {}): Pro
|
|
|
70
72
|
console.log("[ojm] 构建本地模块…");
|
|
71
73
|
await build(projectRoot);
|
|
72
74
|
|
|
75
|
+
// 别名投影跟着模块清单走:dev 是「新增模块 → 立刻写 #/<新模块>/…」的主循环,
|
|
76
|
+
// 不刷新 paths 的话编辑器要把这条 import 报成 any/TS2307,直到下次 build。内容未变不写盘。
|
|
77
|
+
syncAliasPaths(projectRoot);
|
|
78
|
+
|
|
73
79
|
// 工程 mock(可选约定 mock/*.mock.mjs):纯前端形态的 /api 边界
|
|
74
80
|
const mocks: MockRoute[] = await loadProjectMocks(projectRoot);
|
|
75
81
|
// 契约 mock(AC-D14):手写精确匹配优先,契约 pattern 兜底示例值;装载失败不崩 dev
|
|
@@ -84,17 +90,26 @@ export async function devServer(projectRoot: string, opts: DevOptions = {}): Pro
|
|
|
84
90
|
// htmlTransform(web.config.ts,可选)先于起进程校验:配置坏了不该留下 oj 孤儿
|
|
85
91
|
const transform = await loadHtmlTransform(projectRoot);
|
|
86
92
|
|
|
87
|
-
// 2) oj 后端(工程有
|
|
93
|
+
// 2) oj 后端(工程有 config.yaml 时全栈形态;桩可注入)
|
|
88
94
|
const configPath = resolveConfigPath(projectRoot);
|
|
89
95
|
let oj: OjProcess | undefined;
|
|
90
96
|
let ojTarget: string | undefined;
|
|
91
97
|
// 反代前缀跟随 config 的 mounts:(api 挂载 prefix;F2:改前缀后硬编码 /api 会静默失效)
|
|
92
98
|
let apiBase = "/api";
|
|
93
99
|
if (!opts.frontendOnly && existsSync(configPath)) {
|
|
100
|
+
// dev 读源码树(保存即热更,不经产物):module-root 布局给 `--module-root <root>/src`
|
|
101
|
+
// (oj 递归发现 manifest.yaml),历史布局给 `--api-path <root>/api/src`——两者互斥。
|
|
102
|
+
const moduleRoot = layout.kind === "module-root";
|
|
103
|
+
if (moduleRoot)
|
|
104
|
+
requireModuleRootCapability(projectRoot);
|
|
105
|
+
const apiSource: string | string[] = moduleRoot && layout.moduleRoot
|
|
106
|
+
? ["--module-root", layout.moduleRoot]
|
|
107
|
+
: resolve(projectRoot, "api/src");
|
|
94
108
|
apiBase = readOjApiPrefix(configPath);
|
|
95
109
|
await ensureDevCert(configPath);
|
|
96
|
-
const
|
|
97
|
-
|
|
110
|
+
const fallbackStarter = (cfg: string, b: string, apiSrc: string | string[]) => startOj(cfg, b, apiSrc);
|
|
111
|
+
const starter: NonNullable<DevOptions["ojStarter"]> = opts.ojStarter ?? fallbackStarter;
|
|
112
|
+
oj = starter(configPath, apiBase, apiSource);
|
|
98
113
|
try {
|
|
99
114
|
await oj.ready; // 失败人话报错(stderr 尾部),fail-fast
|
|
100
115
|
}
|
|
@@ -210,7 +225,7 @@ export async function devServer(projectRoot: string, opts: DevOptions = {}): Pro
|
|
|
210
225
|
if (ojTarget)
|
|
211
226
|
console.log(`[ojm] oj 后端已就绪:${apiBase} → ${ojTarget}(api.ts 保存即热更)`);
|
|
212
227
|
else
|
|
213
|
-
console.log("[ojm] 纯前端形态(无
|
|
228
|
+
console.log("[ojm] 纯前端形态(无 config.yaml):/api 由 mock/ 提供");
|
|
214
229
|
|
|
215
230
|
console.log("[ojm] 宿主来自 @oj-module/cli 内置 shell-dist(importmap 单例),模块来自本地 dist/");
|
|
216
231
|
|
|
@@ -279,6 +294,8 @@ export async function devServer(projectRoot: string, opts: DevOptions = {}): Pro
|
|
|
279
294
|
// 仅 TTY 默认启用(管道/CI 下不抢 stdin);测试可经 keymap + inputStream 强制。
|
|
280
295
|
const keymapEnabled = opts.keymap ?? (process.stdin.isTTY ?? false);
|
|
281
296
|
if (keymapEnabled) {
|
|
297
|
+
const footerText = "快捷键:r 热更新 · h 帮助 · c 清屏 · o 打开浏览器 · q 退出";
|
|
298
|
+
const footer = pinFooter(`[ojm] ${footerText}`);
|
|
282
299
|
setupDevKeymap({
|
|
283
300
|
server,
|
|
284
301
|
oj,
|
|
@@ -286,12 +303,15 @@ export async function devServer(projectRoot: string, opts: DevOptions = {}): Pro
|
|
|
286
303
|
port: actualPort,
|
|
287
304
|
input: opts.inputStream,
|
|
288
305
|
openBrowser: opts.openBrowser,
|
|
306
|
+
clearScreen: footer ? () => footer.clear() : undefined,
|
|
289
307
|
onReload: () => {
|
|
290
308
|
void trigger();
|
|
291
309
|
},
|
|
292
310
|
onQuit: shutdown,
|
|
293
311
|
});
|
|
294
|
-
|
|
312
|
+
// 非 TTY(pinFooter 拒绝)回退一次性打印,行为与历史一致
|
|
313
|
+
if (!footer)
|
|
314
|
+
console.log(`[ojm] ${footerText}`);
|
|
295
315
|
}
|
|
296
316
|
|
|
297
317
|
return server;
|
|
@@ -320,6 +340,52 @@ export async function shutdownDev(
|
|
|
320
340
|
setTimeout(exit, 1500, 0).unref();
|
|
321
341
|
}
|
|
322
342
|
|
|
343
|
+
/**
|
|
344
|
+
* 快捷键提示常驻底栏:DECSTBM 把滚动区设为 1..rows-1,末行留给提示。
|
|
345
|
+
* 之后本进程 console.log 与 oj 透传(均经 process.stdout.write)只在滚动区内滚动,
|
|
346
|
+
* 末行由终端保持,无需逐条重绘。非 TTY / 行数不足返回 undefined(调用方回退一次性打印)。
|
|
347
|
+
*/
|
|
348
|
+
export interface PinnedFooter {
|
|
349
|
+
/** 清屏:ED(\x1b[2J)保留滚动区(RIS \x1bc 会复位滚动区,禁用),随后重绘提示 */
|
|
350
|
+
clear: () => void
|
|
351
|
+
/** 恢复全屏滚动区并落光标到底行(进程退出/shell 接管前调用);幂等 */
|
|
352
|
+
stop: () => void
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export function pinFooter(text: string, stream: NodeJS.WriteStream = process.stdout): PinnedFooter | undefined {
|
|
356
|
+
if (!stream.isTTY || (stream.rows ?? 0) < 3)
|
|
357
|
+
return undefined;
|
|
358
|
+
const write = (s: string) => {
|
|
359
|
+
stream.write(s);
|
|
360
|
+
};
|
|
361
|
+
const render = () => {
|
|
362
|
+
const rows = stream.rows || 24;
|
|
363
|
+
write(`\x1B[1;${rows - 1}r`); // 滚动区:首行 ~ 倒数第二行(DECSTBM 会把光标送回 1;1)
|
|
364
|
+
write(`\x1B[${rows};1H\x1B[2K${text}`); // 末行清行后画常驻提示
|
|
365
|
+
write(`\x1B[${rows - 1};1H`); // 光标回滚动区底行,后续输出照常滚动
|
|
366
|
+
};
|
|
367
|
+
render();
|
|
368
|
+
const onResize = () => render();
|
|
369
|
+
let stopped = false;
|
|
370
|
+
const stop = () => {
|
|
371
|
+
if (stopped)
|
|
372
|
+
return;
|
|
373
|
+
stopped = true;
|
|
374
|
+
process.removeListener("SIGWINCH", onResize);
|
|
375
|
+
process.removeListener("exit", stop);
|
|
376
|
+
write("\x1B[r\x1B[999;1H\n"); // 恢复全屏滚动区,光标夹到底行避免 shell 提示符悬空
|
|
377
|
+
};
|
|
378
|
+
process.on("SIGWINCH", onResize);
|
|
379
|
+
process.on("exit", stop);
|
|
380
|
+
return {
|
|
381
|
+
clear: () => {
|
|
382
|
+
write("\x1B[2J");
|
|
383
|
+
render();
|
|
384
|
+
},
|
|
385
|
+
stop,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
323
389
|
/**
|
|
324
390
|
* Vite 风格 dev 快捷键(默认仅在 TTY 启用,见 devServer 内的 keymapEnabled 判定)。
|
|
325
391
|
* r 强制重建并热更新(复用 trigger)
|
|
@@ -341,6 +407,8 @@ export interface DevKeymapDeps {
|
|
|
341
407
|
*/
|
|
342
408
|
input?: NodeJS.ReadableStream & { setRawMode?: (mode: boolean) => unknown }
|
|
343
409
|
openBrowser?: (url: string) => void
|
|
410
|
+
/** 清屏实现(默认 \x1Bc);devServer 注入底栏版以保住滚动区 */
|
|
411
|
+
clearScreen?: () => void
|
|
344
412
|
onReload: () => void
|
|
345
413
|
onQuit: () => void
|
|
346
414
|
log?: (msg: string) => void
|
|
@@ -389,7 +457,10 @@ export function setupDevKeymap(deps: DevKeymapDeps): void {
|
|
|
389
457
|
help();
|
|
390
458
|
}
|
|
391
459
|
else if (k === "c") {
|
|
392
|
-
|
|
460
|
+
if (deps.clearScreen)
|
|
461
|
+
deps.clearScreen();
|
|
462
|
+
else
|
|
463
|
+
process.stdout.write("\x1Bc");
|
|
393
464
|
}
|
|
394
465
|
else if (k === "o") {
|
|
395
466
|
open(url);
|