@bleedingdev/modern-js-create 3.5.0-ultramodern.9 → 3.5.0-ultramodern.90
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/README.md +25 -25
- package/bin/run.js +0 -0
- package/dist/cjs/cli/flags.cjs +357 -0
- package/dist/cjs/cli/help.cjs +108 -0
- package/dist/cjs/cli/package-source.cjs +188 -0
- package/dist/cjs/cli/project-setup.cjs +162 -0
- package/dist/cjs/cli/prompts.cjs +100 -0
- package/dist/cjs/index.cjs +35 -585
- package/dist/cjs/locale/en.cjs +5 -6
- package/dist/cjs/locale/zh.cjs +5 -6
- package/dist/cjs/ultramodern-package-source.cjs +0 -32
- package/dist/cjs/ultramodern-release-cohort.cjs +206 -0
- package/dist/cjs/ultramodern-tooling/commands/cloudflare-output-verify.cjs +136 -0
- package/dist/cjs/ultramodern-tooling/commands/context.cjs +131 -0
- package/dist/cjs/ultramodern-tooling/commands/mf-types.cjs +55 -0
- package/dist/cjs/ultramodern-tooling/commands/migrate-strict-effect/api-metadata.cjs +212 -0
- package/dist/cjs/ultramodern-tooling/commands/migrate-strict-effect/declaration-patches.cjs +50 -0
- package/dist/cjs/ultramodern-tooling/commands/migrate-strict-effect/dependency-usage.cjs +87 -0
- package/dist/cjs/ultramodern-tooling/commands/migrate-strict-effect/generated-artifacts-backend-federation.cjs +58 -0
- package/dist/cjs/ultramodern-tooling/commands/migrate-strict-effect/generated-artifacts-build-identity.cjs +100 -0
- package/dist/cjs/ultramodern-tooling/commands/migrate-strict-effect/generated-artifacts-modern-configs.cjs +100 -0
- package/dist/cjs/ultramodern-tooling/commands/migrate-strict-effect/generated-artifacts-oxfmt.cjs +132 -0
- package/dist/cjs/ultramodern-tooling/commands/migrate-strict-effect/generated-artifacts-removal.cjs +67 -0
- package/dist/cjs/ultramodern-tooling/commands/migrate-strict-effect/generated-artifacts-typescript.cjs +206 -0
- package/dist/cjs/ultramodern-tooling/commands/migrate-strict-effect/generated-artifacts-zerops.cjs +60 -0
- package/dist/cjs/ultramodern-tooling/commands/migrate-strict-effect/generated-artifacts.cjs +67 -0
- package/dist/cjs/ultramodern-tooling/commands/migrate-strict-effect/generated-patches.cjs +67 -0
- package/dist/cjs/ultramodern-tooling/commands/migrate-strict-effect/generated-ui-source.cjs +148 -0
- package/dist/cjs/ultramodern-tooling/commands/migrate-strict-effect/help.cjs +54 -0
- package/dist/cjs/ultramodern-tooling/commands/migrate-strict-effect/install.cjs +52 -0
- package/dist/cjs/ultramodern-tooling/commands/migrate-strict-effect/io.cjs +350 -0
- package/dist/cjs/ultramodern-tooling/commands/migrate-strict-effect/package-cohort.cjs +200 -0
- package/dist/cjs/ultramodern-tooling/commands/migrate-strict-effect/package-source.cjs +89 -0
- package/dist/cjs/ultramodern-tooling/commands/migrate-strict-effect/pnpm-policy.cjs +476 -0
- package/dist/cjs/ultramodern-tooling/commands/migrate-strict-effect/pnpm-yaml.cjs +376 -0
- package/dist/cjs/ultramodern-tooling/commands/migrate-strict-effect/policy-constants.cjs +99 -0
- package/dist/cjs/ultramodern-tooling/commands/migrate-strict-effect/toolchain-pins.cjs +135 -0
- package/dist/cjs/ultramodern-tooling/commands/migrate-strict-effect.cjs +425 -0
- package/dist/cjs/ultramodern-tooling/commands/options.cjs +59 -0
- package/dist/cjs/ultramodern-tooling/commands/routes-generate.cjs +122 -0
- package/dist/cjs/ultramodern-tooling/commands/skills.cjs +49 -0
- package/dist/cjs/ultramodern-tooling/commands/sync-delivery-unit.cjs +40 -0
- package/dist/cjs/ultramodern-tooling/commands/validate.cjs +53 -0
- package/dist/cjs/ultramodern-tooling/commands.cjs +27 -491
- package/dist/cjs/ultramodern-tooling/config/constants.cjs +48 -0
- package/dist/cjs/ultramodern-tooling/config/index.cjs +104 -0
- package/dist/cjs/ultramodern-tooling/config/json.cjs +61 -0
- package/dist/cjs/ultramodern-tooling/config/load.cjs +62 -0
- package/dist/cjs/ultramodern-tooling/config/metadata.cjs +92 -0
- package/dist/cjs/ultramodern-tooling/config/normalize.cjs +236 -0
- package/dist/cjs/ultramodern-tooling/config/synthesize.cjs +157 -0
- package/dist/cjs/ultramodern-tooling/config/types.cjs +18 -0
- package/dist/cjs/ultramodern-tooling/config.cjs +23 -133
- package/dist/cjs/ultramodern-workspace/add-shell.cjs +274 -0
- package/dist/cjs/ultramodern-workspace/add-vertical/constants.cjs +52 -0
- package/dist/cjs/ultramodern-workspace/add-vertical/execute.cjs +183 -0
- package/dist/cjs/ultramodern-workspace/add-vertical/plan.cjs +255 -0
- package/dist/cjs/ultramodern-workspace/add-vertical/preflight.cjs +256 -0
- package/dist/cjs/ultramodern-workspace/add-vertical/shell-files.cjs +122 -0
- package/dist/cjs/ultramodern-workspace/add-vertical/topology.cjs +152 -0
- package/dist/cjs/ultramodern-workspace/add-vertical/transaction.cjs +327 -0
- package/dist/cjs/ultramodern-workspace/add-vertical/workspace-state.cjs +137 -0
- package/dist/cjs/ultramodern-workspace/add-vertical.cjs +22 -571
- package/dist/cjs/ultramodern-workspace/api/checkout-cart.cjs +145 -0
- package/dist/cjs/ultramodern-workspace/api/client.cjs +224 -0
- package/dist/cjs/ultramodern-workspace/api/contracts.cjs +251 -0
- package/dist/cjs/ultramodern-workspace/api/names.cjs +98 -0
- package/dist/cjs/ultramodern-workspace/api/rpc.cjs +264 -0
- package/dist/cjs/ultramodern-workspace/api/service.cjs +252 -0
- package/dist/cjs/ultramodern-workspace/api/shared.cjs +270 -0
- package/dist/cjs/ultramodern-workspace/api.cjs +49 -1005
- package/dist/cjs/ultramodern-workspace/app-files.cjs +51 -69
- package/dist/cjs/ultramodern-workspace/backend-federation.cjs +346 -0
- package/dist/cjs/ultramodern-workspace/bridge-config/cli.cjs +71 -0
- package/dist/cjs/ultramodern-workspace/bridge-config/defaults.cjs +43 -0
- package/dist/cjs/ultramodern-workspace/bridge-config/gates.cjs +93 -0
- package/dist/cjs/ultramodern-workspace/bridge-config/index.cjs +60 -0
- package/dist/cjs/ultramodern-workspace/bridge-config/normalize.cjs +68 -0
- package/dist/cjs/ultramodern-workspace/bridge-config/schema.cjs +78 -0
- package/dist/cjs/ultramodern-workspace/bridge-config/shared.cjs +110 -0
- package/dist/cjs/ultramodern-workspace/bridge-config/validation.cjs +68 -0
- package/dist/cjs/ultramodern-workspace/bridge-config/workspace-packages.cjs +108 -0
- package/dist/cjs/ultramodern-workspace/bridge-config.cjs +32 -297
- package/dist/cjs/ultramodern-workspace/codesmith.cjs +10 -1
- package/dist/cjs/ultramodern-workspace/contracts.cjs +80 -26
- package/dist/cjs/ultramodern-workspace/delivery-unit-schema/types.cjs +291 -0
- package/dist/cjs/ultramodern-workspace/delivery-unit-schema/up-projection.cjs +188 -0
- package/dist/cjs/ultramodern-workspace/delivery-unit-stamp.cjs +56 -0
- package/dist/cjs/ultramodern-workspace/delivery-unit-sync.cjs +153 -0
- package/dist/cjs/ultramodern-workspace/delivery-unit.cjs +75 -0
- package/dist/cjs/ultramodern-workspace/demo-components.cjs +474 -205
- package/dist/cjs/ultramodern-workspace/descriptors.cjs +101 -16
- package/dist/cjs/ultramodern-workspace/effect-diagnostics.cjs +114 -0
- package/dist/cjs/ultramodern-workspace/fs-io.cjs +26 -36
- package/dist/cjs/ultramodern-workspace/generation-result.cjs +36 -3
- package/dist/cjs/ultramodern-workspace/index.cjs +28 -0
- package/dist/cjs/ultramodern-workspace/locales.cjs +3 -17
- package/dist/cjs/ultramodern-workspace/mf-validation/constants.cjs +69 -0
- package/dist/cjs/ultramodern-workspace/mf-validation/discovery.cjs +77 -0
- package/dist/cjs/ultramodern-workspace/mf-validation/inspect.cjs +84 -0
- package/dist/cjs/ultramodern-workspace/mf-validation/object-literal.cjs +188 -0
- package/dist/cjs/ultramodern-workspace/mf-validation/path-utils.cjs +165 -0
- package/dist/cjs/ultramodern-workspace/mf-validation/syntax.cjs +201 -0
- package/dist/cjs/ultramodern-workspace/mf-validation/types.cjs +18 -0
- package/dist/cjs/ultramodern-workspace/mf-validation/validate.cjs +112 -0
- package/dist/cjs/ultramodern-workspace/mf-validation.cjs +6 -529
- package/dist/cjs/ultramodern-workspace/module-federation/config.cjs +336 -0
- package/dist/cjs/ultramodern-workspace/module-federation/index.cjs +73 -0
- package/dist/cjs/ultramodern-workspace/module-federation/reexport-module.cjs +111 -0
- package/dist/cjs/ultramodern-workspace/module-federation/remote-refs.cjs +106 -0
- package/dist/cjs/ultramodern-workspace/module-federation/shared-config.cjs +53 -0
- package/dist/cjs/ultramodern-workspace/module-federation.cjs +18 -477
- package/dist/cjs/ultramodern-workspace/naming.cjs +1 -7
- package/dist/cjs/ultramodern-workspace/overlay-baseline-guard.cjs +408 -0
- package/dist/cjs/ultramodern-workspace/overlays.cjs +39 -5
- package/dist/cjs/ultramodern-workspace/package-json.cjs +54 -301
- package/dist/cjs/ultramodern-workspace/package-source.cjs +11 -2
- package/dist/cjs/ultramodern-workspace/policy.cjs +801 -24
- package/dist/cjs/ultramodern-workspace/public-surface.cjs +1 -22
- package/dist/cjs/ultramodern-workspace/routes.cjs +8 -157
- package/dist/cjs/ultramodern-workspace/shared-patches.cjs +41 -0
- package/dist/cjs/ultramodern-workspace/shells.cjs +151 -0
- package/dist/cjs/ultramodern-workspace/tooling-command-catalog.cjs +147 -0
- package/dist/cjs/ultramodern-workspace/tsconfigs.cjs +200 -0
- package/dist/cjs/ultramodern-workspace/types.cjs +27 -0
- package/dist/cjs/ultramodern-workspace/versions.cjs +52 -37
- package/dist/cjs/ultramodern-workspace/workspace-script-plan.cjs +174 -0
- package/dist/cjs/ultramodern-workspace/workspace-scripts.cjs +118 -88
- package/dist/cjs/ultramodern-workspace/workspace-validation-contract.cjs +642 -0
- package/dist/cjs/ultramodern-workspace/write-app.cjs +148 -0
- package/dist/cjs/ultramodern-workspace/write-workspace.cjs +45 -67
- package/dist/cjs/ultramodern-workspace/zerops.cjs +108 -0
- package/dist/esm/cli/flags.js +262 -0
- package/dist/esm/cli/help.js +67 -0
- package/dist/esm/cli/package-source.js +124 -0
- package/dist/esm/cli/project-setup.js +105 -0
- package/dist/esm/cli/prompts.js +51 -0
- package/dist/esm/index.js +19 -565
- package/dist/esm/locale/en.js +5 -6
- package/dist/esm/locale/zh.js +5 -6
- package/dist/esm/ultramodern-package-source.js +1 -24
- package/dist/esm/ultramodern-release-cohort.js +133 -0
- package/dist/esm/ultramodern-tooling/commands/cloudflare-output-verify.js +88 -0
- package/dist/esm/ultramodern-tooling/commands/context.js +66 -0
- package/dist/esm/ultramodern-tooling/commands/mf-types.js +17 -0
- package/dist/esm/ultramodern-tooling/commands/migrate-strict-effect/api-metadata.js +154 -0
- package/dist/esm/ultramodern-tooling/commands/migrate-strict-effect/declaration-patches.js +12 -0
- package/dist/esm/ultramodern-tooling/commands/migrate-strict-effect/dependency-usage.js +38 -0
- package/dist/esm/ultramodern-tooling/commands/migrate-strict-effect/generated-artifacts-backend-federation.js +10 -0
- package/dist/esm/ultramodern-tooling/commands/migrate-strict-effect/generated-artifacts-build-identity.js +45 -0
- package/dist/esm/ultramodern-tooling/commands/migrate-strict-effect/generated-artifacts-modern-configs.js +51 -0
- package/dist/esm/ultramodern-tooling/commands/migrate-strict-effect/generated-artifacts-oxfmt.js +83 -0
- package/dist/esm/ultramodern-tooling/commands/migrate-strict-effect/generated-artifacts-removal.js +16 -0
- package/dist/esm/ultramodern-tooling/commands/migrate-strict-effect/generated-artifacts-typescript.js +157 -0
- package/dist/esm/ultramodern-tooling/commands/migrate-strict-effect/generated-artifacts-zerops.js +12 -0
- package/dist/esm/ultramodern-tooling/commands/migrate-strict-effect/generated-artifacts.js +7 -0
- package/dist/esm/ultramodern-tooling/commands/migrate-strict-effect/generated-patches.js +15 -0
- package/dist/esm/ultramodern-tooling/commands/migrate-strict-effect/generated-ui-source.js +97 -0
- package/dist/esm/ultramodern-tooling/commands/migrate-strict-effect/help.js +16 -0
- package/dist/esm/ultramodern-tooling/commands/migrate-strict-effect/install.js +14 -0
- package/dist/esm/ultramodern-tooling/commands/migrate-strict-effect/io.js +285 -0
- package/dist/esm/ultramodern-tooling/commands/migrate-strict-effect/package-cohort.js +156 -0
- package/dist/esm/ultramodern-tooling/commands/migrate-strict-effect/package-source.js +51 -0
- package/dist/esm/ultramodern-tooling/commands/migrate-strict-effect/pnpm-policy.js +417 -0
- package/dist/esm/ultramodern-tooling/commands/migrate-strict-effect/pnpm-yaml.js +329 -0
- package/dist/esm/ultramodern-tooling/commands/migrate-strict-effect/policy-constants.js +24 -0
- package/dist/esm/ultramodern-tooling/commands/migrate-strict-effect/toolchain-pins.js +80 -0
- package/dist/esm/ultramodern-tooling/commands/migrate-strict-effect.js +376 -0
- package/dist/esm/ultramodern-tooling/commands/options.js +18 -0
- package/dist/esm/ultramodern-tooling/commands/routes-generate.js +74 -0
- package/dist/esm/ultramodern-tooling/commands/skills.js +11 -0
- package/dist/esm/ultramodern-tooling/commands/sync-delivery-unit.js +1 -0
- package/dist/esm/ultramodern-tooling/commands/validate.js +15 -0
- package/dist/esm/ultramodern-tooling/commands.js +23 -482
- package/dist/esm/ultramodern-tooling/config/constants.js +4 -0
- package/dist/esm/ultramodern-tooling/config/index.js +7 -0
- package/dist/esm/ultramodern-tooling/config/json.js +10 -0
- package/dist/esm/ultramodern-tooling/config/load.js +13 -0
- package/dist/esm/ultramodern-tooling/config/metadata.js +37 -0
- package/dist/esm/ultramodern-tooling/config/normalize.js +173 -0
- package/dist/esm/ultramodern-tooling/config/synthesize.js +108 -0
- package/dist/esm/ultramodern-tooling/config/types.js +0 -0
- package/dist/esm/ultramodern-tooling/config.js +1 -120
- package/dist/esm/ultramodern-workspace/add-shell.js +221 -0
- package/dist/esm/ultramodern-workspace/add-vertical/constants.js +5 -0
- package/dist/esm/ultramodern-workspace/add-vertical/execute.js +135 -0
- package/dist/esm/ultramodern-workspace/add-vertical/plan.js +205 -0
- package/dist/esm/ultramodern-workspace/add-vertical/preflight.js +198 -0
- package/dist/esm/ultramodern-workspace/add-vertical/shell-files.js +65 -0
- package/dist/esm/ultramodern-workspace/add-vertical/topology.js +108 -0
- package/dist/esm/ultramodern-workspace/add-vertical/transaction.js +254 -0
- package/dist/esm/ultramodern-workspace/add-vertical/workspace-state.js +67 -0
- package/dist/esm/ultramodern-workspace/add-vertical.js +6 -544
- package/dist/esm/ultramodern-workspace/api/checkout-cart.js +86 -0
- package/dist/esm/ultramodern-workspace/api/client.js +183 -0
- package/dist/esm/ultramodern-workspace/api/contracts.js +201 -0
- package/dist/esm/ultramodern-workspace/api/names.js +33 -0
- package/dist/esm/ultramodern-workspace/api/rpc.js +205 -0
- package/dist/esm/ultramodern-workspace/api/service.js +211 -0
- package/dist/esm/ultramodern-workspace/api/shared.js +226 -0
- package/dist/esm/ultramodern-workspace/api.js +6 -984
- package/dist/esm/ultramodern-workspace/app-files.js +51 -51
- package/dist/esm/ultramodern-workspace/backend-federation.js +281 -0
- package/dist/esm/ultramodern-workspace/bridge-config/cli.js +30 -0
- package/dist/esm/ultramodern-workspace/bridge-config/defaults.js +5 -0
- package/dist/esm/ultramodern-workspace/bridge-config/gates.js +52 -0
- package/dist/esm/ultramodern-workspace/bridge-config/index.js +3 -0
- package/dist/esm/ultramodern-workspace/bridge-config/normalize.js +30 -0
- package/dist/esm/ultramodern-workspace/bridge-config/schema.js +31 -0
- package/dist/esm/ultramodern-workspace/bridge-config/shared.js +51 -0
- package/dist/esm/ultramodern-workspace/bridge-config/validation.js +27 -0
- package/dist/esm/ultramodern-workspace/bridge-config/workspace-packages.js +67 -0
- package/dist/esm/ultramodern-workspace/bridge-config.js +1 -270
- package/dist/esm/ultramodern-workspace/codesmith.js +10 -1
- package/dist/esm/ultramodern-workspace/contracts.js +81 -27
- package/dist/esm/ultramodern-workspace/delivery-unit-schema/types.js +235 -0
- package/dist/esm/ultramodern-workspace/delivery-unit-schema/up-projection.js +129 -0
- package/dist/esm/ultramodern-workspace/delivery-unit-stamp.js +18 -0
- package/dist/esm/ultramodern-workspace/delivery-unit-sync.js +104 -0
- package/dist/esm/ultramodern-workspace/delivery-unit.js +21 -0
- package/dist/esm/ultramodern-workspace/demo-components.js +461 -204
- package/dist/esm/ultramodern-workspace/descriptors.js +75 -17
- package/dist/esm/ultramodern-workspace/effect-diagnostics.js +76 -0
- package/dist/esm/ultramodern-workspace/fs-io.js +27 -18
- package/dist/esm/ultramodern-workspace/generation-result.js +33 -3
- package/dist/esm/ultramodern-workspace/index.js +4 -0
- package/dist/esm/ultramodern-workspace/locales.js +3 -4
- package/dist/esm/ultramodern-workspace/mf-validation/constants.js +19 -0
- package/dist/esm/ultramodern-workspace/mf-validation/discovery.js +28 -0
- package/dist/esm/ultramodern-workspace/mf-validation/inspect.js +46 -0
- package/dist/esm/ultramodern-workspace/mf-validation/object-literal.js +144 -0
- package/dist/esm/ultramodern-workspace/mf-validation/path-utils.js +89 -0
- package/dist/esm/ultramodern-workspace/mf-validation/syntax.js +154 -0
- package/dist/esm/ultramodern-workspace/mf-validation/types.js +0 -0
- package/dist/esm/ultramodern-workspace/mf-validation/validate.js +57 -0
- package/dist/esm/ultramodern-workspace/mf-validation.js +3 -516
- package/dist/esm/ultramodern-workspace/module-federation/config.js +289 -0
- package/dist/esm/ultramodern-workspace/module-federation/index.js +4 -0
- package/dist/esm/ultramodern-workspace/module-federation/reexport-module.js +67 -0
- package/dist/esm/ultramodern-workspace/module-federation/remote-refs.js +65 -0
- package/dist/esm/ultramodern-workspace/module-federation/shared-config.js +12 -0
- package/dist/esm/ultramodern-workspace/module-federation.js +2 -458
- package/dist/esm/ultramodern-workspace/naming.js +2 -2
- package/dist/esm/ultramodern-workspace/overlay-baseline-guard.js +350 -0
- package/dist/esm/ultramodern-workspace/overlays.js +28 -5
- package/dist/esm/ultramodern-workspace/package-json.js +48 -283
- package/dist/esm/ultramodern-workspace/package-source.js +7 -1
- package/dist/esm/ultramodern-workspace/policy.js +784 -10
- package/dist/esm/ultramodern-workspace/public-surface.js +2 -2
- package/dist/esm/ultramodern-workspace/routes.js +9 -143
- package/dist/esm/ultramodern-workspace/shared-patches.js +3 -0
- package/dist/esm/ultramodern-workspace/shells.js +92 -0
- package/dist/esm/ultramodern-workspace/tooling-command-catalog.js +100 -0
- package/dist/esm/ultramodern-workspace/tsconfigs.js +137 -0
- package/dist/esm/ultramodern-workspace/types.js +16 -1
- package/dist/esm/ultramodern-workspace/versions.js +35 -26
- package/dist/esm/ultramodern-workspace/workspace-script-plan.js +124 -0
- package/dist/esm/ultramodern-workspace/workspace-scripts.js +115 -85
- package/dist/esm/ultramodern-workspace/workspace-validation-contract.js +604 -0
- package/dist/esm/ultramodern-workspace/write-app.js +105 -0
- package/dist/esm/ultramodern-workspace/write-workspace.js +47 -66
- package/dist/esm/ultramodern-workspace/zerops.js +70 -0
- package/dist/esm-node/cli/flags.js +263 -0
- package/dist/esm-node/cli/help.js +68 -0
- package/dist/esm-node/cli/package-source.js +125 -0
- package/dist/esm-node/cli/project-setup.js +106 -0
- package/dist/esm-node/cli/prompts.js +52 -0
- package/dist/esm-node/index.js +19 -565
- package/dist/esm-node/locale/en.js +5 -6
- package/dist/esm-node/locale/zh.js +5 -6
- package/dist/esm-node/ultramodern-package-source.js +1 -24
- package/dist/esm-node/ultramodern-release-cohort.js +134 -0
- package/dist/esm-node/ultramodern-tooling/commands/cloudflare-output-verify.js +89 -0
- package/dist/esm-node/ultramodern-tooling/commands/context.js +67 -0
- package/dist/esm-node/ultramodern-tooling/commands/mf-types.js +18 -0
- package/dist/esm-node/ultramodern-tooling/commands/migrate-strict-effect/api-metadata.js +155 -0
- package/dist/esm-node/ultramodern-tooling/commands/migrate-strict-effect/declaration-patches.js +13 -0
- package/dist/esm-node/ultramodern-tooling/commands/migrate-strict-effect/dependency-usage.js +39 -0
- package/dist/esm-node/ultramodern-tooling/commands/migrate-strict-effect/generated-artifacts-backend-federation.js +11 -0
- package/dist/esm-node/ultramodern-tooling/commands/migrate-strict-effect/generated-artifacts-build-identity.js +46 -0
- package/dist/esm-node/ultramodern-tooling/commands/migrate-strict-effect/generated-artifacts-modern-configs.js +52 -0
- package/dist/esm-node/ultramodern-tooling/commands/migrate-strict-effect/generated-artifacts-oxfmt.js +84 -0
- package/dist/esm-node/ultramodern-tooling/commands/migrate-strict-effect/generated-artifacts-removal.js +17 -0
- package/dist/esm-node/ultramodern-tooling/commands/migrate-strict-effect/generated-artifacts-typescript.js +158 -0
- package/dist/esm-node/ultramodern-tooling/commands/migrate-strict-effect/generated-artifacts-zerops.js +13 -0
- package/dist/esm-node/ultramodern-tooling/commands/migrate-strict-effect/generated-artifacts.js +8 -0
- package/dist/esm-node/ultramodern-tooling/commands/migrate-strict-effect/generated-patches.js +16 -0
- package/dist/esm-node/ultramodern-tooling/commands/migrate-strict-effect/generated-ui-source.js +98 -0
- package/dist/esm-node/ultramodern-tooling/commands/migrate-strict-effect/help.js +17 -0
- package/dist/esm-node/ultramodern-tooling/commands/migrate-strict-effect/install.js +15 -0
- package/dist/esm-node/ultramodern-tooling/commands/migrate-strict-effect/io.js +286 -0
- package/dist/esm-node/ultramodern-tooling/commands/migrate-strict-effect/package-cohort.js +157 -0
- package/dist/esm-node/ultramodern-tooling/commands/migrate-strict-effect/package-source.js +52 -0
- package/dist/esm-node/ultramodern-tooling/commands/migrate-strict-effect/pnpm-policy.js +418 -0
- package/dist/esm-node/ultramodern-tooling/commands/migrate-strict-effect/pnpm-yaml.js +330 -0
- package/dist/esm-node/ultramodern-tooling/commands/migrate-strict-effect/policy-constants.js +25 -0
- package/dist/esm-node/ultramodern-tooling/commands/migrate-strict-effect/toolchain-pins.js +81 -0
- package/dist/esm-node/ultramodern-tooling/commands/migrate-strict-effect.js +377 -0
- package/dist/esm-node/ultramodern-tooling/commands/options.js +19 -0
- package/dist/esm-node/ultramodern-tooling/commands/routes-generate.js +75 -0
- package/dist/esm-node/ultramodern-tooling/commands/skills.js +12 -0
- package/dist/esm-node/ultramodern-tooling/commands/sync-delivery-unit.js +2 -0
- package/dist/esm-node/ultramodern-tooling/commands/validate.js +16 -0
- package/dist/esm-node/ultramodern-tooling/commands.js +23 -482
- package/dist/esm-node/ultramodern-tooling/config/constants.js +5 -0
- package/dist/esm-node/ultramodern-tooling/config/index.js +8 -0
- package/dist/esm-node/ultramodern-tooling/config/json.js +11 -0
- package/dist/esm-node/ultramodern-tooling/config/load.js +14 -0
- package/dist/esm-node/ultramodern-tooling/config/metadata.js +38 -0
- package/dist/esm-node/ultramodern-tooling/config/normalize.js +174 -0
- package/dist/esm-node/ultramodern-tooling/config/synthesize.js +109 -0
- package/dist/esm-node/ultramodern-tooling/config/types.js +1 -0
- package/dist/esm-node/ultramodern-tooling/config.js +1 -120
- package/dist/esm-node/ultramodern-workspace/add-shell.js +222 -0
- package/dist/esm-node/ultramodern-workspace/add-vertical/constants.js +6 -0
- package/dist/esm-node/ultramodern-workspace/add-vertical/execute.js +136 -0
- package/dist/esm-node/ultramodern-workspace/add-vertical/plan.js +206 -0
- package/dist/esm-node/ultramodern-workspace/add-vertical/preflight.js +199 -0
- package/dist/esm-node/ultramodern-workspace/add-vertical/shell-files.js +66 -0
- package/dist/esm-node/ultramodern-workspace/add-vertical/topology.js +109 -0
- package/dist/esm-node/ultramodern-workspace/add-vertical/transaction.js +255 -0
- package/dist/esm-node/ultramodern-workspace/add-vertical/workspace-state.js +68 -0
- package/dist/esm-node/ultramodern-workspace/add-vertical.js +6 -544
- package/dist/esm-node/ultramodern-workspace/api/checkout-cart.js +87 -0
- package/dist/esm-node/ultramodern-workspace/api/client.js +184 -0
- package/dist/esm-node/ultramodern-workspace/api/contracts.js +202 -0
- package/dist/esm-node/ultramodern-workspace/api/names.js +34 -0
- package/dist/esm-node/ultramodern-workspace/api/rpc.js +206 -0
- package/dist/esm-node/ultramodern-workspace/api/service.js +212 -0
- package/dist/esm-node/ultramodern-workspace/api/shared.js +227 -0
- package/dist/esm-node/ultramodern-workspace/api.js +6 -984
- package/dist/esm-node/ultramodern-workspace/app-files.js +51 -51
- package/dist/esm-node/ultramodern-workspace/backend-federation.js +282 -0
- package/dist/esm-node/ultramodern-workspace/bridge-config/cli.js +31 -0
- package/dist/esm-node/ultramodern-workspace/bridge-config/defaults.js +6 -0
- package/dist/esm-node/ultramodern-workspace/bridge-config/gates.js +53 -0
- package/dist/esm-node/ultramodern-workspace/bridge-config/index.js +4 -0
- package/dist/esm-node/ultramodern-workspace/bridge-config/normalize.js +31 -0
- package/dist/esm-node/ultramodern-workspace/bridge-config/schema.js +32 -0
- package/dist/esm-node/ultramodern-workspace/bridge-config/shared.js +52 -0
- package/dist/esm-node/ultramodern-workspace/bridge-config/validation.js +28 -0
- package/dist/esm-node/ultramodern-workspace/bridge-config/workspace-packages.js +68 -0
- package/dist/esm-node/ultramodern-workspace/bridge-config.js +1 -270
- package/dist/esm-node/ultramodern-workspace/codesmith.js +10 -1
- package/dist/esm-node/ultramodern-workspace/contracts.js +81 -27
- package/dist/esm-node/ultramodern-workspace/delivery-unit-schema/types.js +236 -0
- package/dist/esm-node/ultramodern-workspace/delivery-unit-schema/up-projection.js +130 -0
- package/dist/esm-node/ultramodern-workspace/delivery-unit-stamp.js +19 -0
- package/dist/esm-node/ultramodern-workspace/delivery-unit-sync.js +105 -0
- package/dist/esm-node/ultramodern-workspace/delivery-unit.js +22 -0
- package/dist/esm-node/ultramodern-workspace/demo-components.js +461 -204
- package/dist/esm-node/ultramodern-workspace/descriptors.js +75 -17
- package/dist/esm-node/ultramodern-workspace/effect-diagnostics.js +77 -0
- package/dist/esm-node/ultramodern-workspace/fs-io.js +27 -18
- package/dist/esm-node/ultramodern-workspace/generation-result.js +33 -3
- package/dist/esm-node/ultramodern-workspace/index.js +4 -0
- package/dist/esm-node/ultramodern-workspace/locales.js +3 -4
- package/dist/esm-node/ultramodern-workspace/mf-validation/constants.js +20 -0
- package/dist/esm-node/ultramodern-workspace/mf-validation/discovery.js +29 -0
- package/dist/esm-node/ultramodern-workspace/mf-validation/inspect.js +47 -0
- package/dist/esm-node/ultramodern-workspace/mf-validation/object-literal.js +145 -0
- package/dist/esm-node/ultramodern-workspace/mf-validation/path-utils.js +90 -0
- package/dist/esm-node/ultramodern-workspace/mf-validation/syntax.js +155 -0
- package/dist/esm-node/ultramodern-workspace/mf-validation/types.js +1 -0
- package/dist/esm-node/ultramodern-workspace/mf-validation/validate.js +58 -0
- package/dist/esm-node/ultramodern-workspace/mf-validation.js +3 -516
- package/dist/esm-node/ultramodern-workspace/module-federation/config.js +290 -0
- package/dist/esm-node/ultramodern-workspace/module-federation/index.js +5 -0
- package/dist/esm-node/ultramodern-workspace/module-federation/reexport-module.js +68 -0
- package/dist/esm-node/ultramodern-workspace/module-federation/remote-refs.js +66 -0
- package/dist/esm-node/ultramodern-workspace/module-federation/shared-config.js +13 -0
- package/dist/esm-node/ultramodern-workspace/module-federation.js +2 -458
- package/dist/esm-node/ultramodern-workspace/naming.js +2 -2
- package/dist/esm-node/ultramodern-workspace/overlay-baseline-guard.js +351 -0
- package/dist/esm-node/ultramodern-workspace/overlays.js +28 -5
- package/dist/esm-node/ultramodern-workspace/package-json.js +48 -283
- package/dist/esm-node/ultramodern-workspace/package-source.js +7 -1
- package/dist/esm-node/ultramodern-workspace/policy.js +784 -10
- package/dist/esm-node/ultramodern-workspace/public-surface.js +2 -2
- package/dist/esm-node/ultramodern-workspace/routes.js +9 -143
- package/dist/esm-node/ultramodern-workspace/shared-patches.js +4 -0
- package/dist/esm-node/ultramodern-workspace/shells.js +93 -0
- package/dist/esm-node/ultramodern-workspace/tooling-command-catalog.js +101 -0
- package/dist/esm-node/ultramodern-workspace/tsconfigs.js +138 -0
- package/dist/esm-node/ultramodern-workspace/types.js +16 -1
- package/dist/esm-node/ultramodern-workspace/versions.js +35 -26
- package/dist/esm-node/ultramodern-workspace/workspace-script-plan.js +125 -0
- package/dist/esm-node/ultramodern-workspace/workspace-scripts.js +115 -85
- package/dist/esm-node/ultramodern-workspace/workspace-validation-contract.js +605 -0
- package/dist/esm-node/ultramodern-workspace/write-app.js +106 -0
- package/dist/esm-node/ultramodern-workspace/write-workspace.js +47 -66
- package/dist/esm-node/ultramodern-workspace/zerops.js +71 -0
- package/dist/types/cli/flags.d.ts +35 -0
- package/dist/types/cli/help.d.ts +2 -0
- package/dist/types/cli/package-source.d.ts +15 -0
- package/dist/types/cli/project-setup.d.ts +8 -0
- package/dist/types/cli/prompts.d.ts +4 -0
- package/dist/types/locale/en.d.ts +3 -4
- package/dist/types/locale/index.d.ts +6 -8
- package/dist/types/locale/zh.d.ts +3 -4
- package/dist/types/ultramodern-package-source.d.ts +0 -11
- package/dist/types/ultramodern-release-cohort.d.ts +30 -0
- package/dist/types/ultramodern-tooling/commands/cloudflare-output-verify.d.ts +2 -0
- package/dist/types/ultramodern-tooling/commands/context.d.ts +11 -0
- package/dist/types/ultramodern-tooling/commands/mf-types.d.ts +2 -0
- package/dist/types/ultramodern-tooling/commands/migrate-strict-effect/api-metadata.d.ts +12 -0
- package/dist/types/ultramodern-tooling/commands/migrate-strict-effect/declaration-patches.d.ts +4 -0
- package/dist/types/ultramodern-tooling/commands/migrate-strict-effect/dependency-usage.d.ts +1 -0
- package/dist/types/ultramodern-tooling/commands/migrate-strict-effect/generated-artifacts-backend-federation.d.ts +3 -0
- package/dist/types/ultramodern-tooling/commands/migrate-strict-effect/generated-artifacts-build-identity.d.ts +5 -0
- package/dist/types/ultramodern-tooling/commands/migrate-strict-effect/generated-artifacts-modern-configs.d.ts +3 -0
- package/dist/types/ultramodern-tooling/commands/migrate-strict-effect/generated-artifacts-oxfmt.d.ts +2 -0
- package/dist/types/ultramodern-tooling/commands/migrate-strict-effect/generated-artifacts-removal.d.ts +4 -0
- package/dist/types/ultramodern-tooling/commands/migrate-strict-effect/generated-artifacts-typescript.d.ts +3 -0
- package/dist/types/ultramodern-tooling/commands/migrate-strict-effect/generated-artifacts-zerops.d.ts +3 -0
- package/dist/types/ultramodern-tooling/commands/migrate-strict-effect/generated-artifacts.d.ts +7 -0
- package/dist/types/ultramodern-tooling/commands/migrate-strict-effect/generated-patches.d.ts +3 -0
- package/dist/types/ultramodern-tooling/commands/migrate-strict-effect/generated-ui-source.d.ts +3 -0
- package/dist/types/ultramodern-tooling/commands/migrate-strict-effect/help.d.ts +1 -0
- package/dist/types/ultramodern-tooling/commands/migrate-strict-effect/install.d.ts +2 -0
- package/dist/types/ultramodern-tooling/commands/migrate-strict-effect/io.d.ts +20 -0
- package/dist/types/ultramodern-tooling/commands/migrate-strict-effect/package-cohort.d.ts +9 -0
- package/dist/types/ultramodern-tooling/commands/migrate-strict-effect/package-source.d.ts +3 -0
- package/dist/types/ultramodern-tooling/commands/migrate-strict-effect/pnpm-policy.d.ts +27 -0
- package/dist/types/ultramodern-tooling/commands/migrate-strict-effect/pnpm-yaml.d.ts +26 -0
- package/dist/types/ultramodern-tooling/commands/migrate-strict-effect/policy-constants.d.ts +10 -0
- package/dist/types/ultramodern-tooling/commands/migrate-strict-effect/toolchain-pins.d.ts +4 -0
- package/dist/types/ultramodern-tooling/commands/migrate-strict-effect.d.ts +2 -0
- package/dist/types/ultramodern-tooling/commands/options.d.ts +2 -0
- package/dist/types/ultramodern-tooling/commands/routes-generate.d.ts +2 -0
- package/dist/types/ultramodern-tooling/commands/skills.d.ts +2 -0
- package/dist/types/ultramodern-tooling/commands/sync-delivery-unit.d.ts +1 -0
- package/dist/types/ultramodern-tooling/commands/validate.d.ts +2 -0
- package/dist/types/ultramodern-tooling/commands.d.ts +1 -1
- package/dist/types/ultramodern-tooling/config/constants.d.ts +3 -0
- package/dist/types/ultramodern-tooling/config/index.d.ts +7 -0
- package/dist/types/ultramodern-tooling/config/json.d.ts +2 -0
- package/dist/types/ultramodern-tooling/config/load.d.ts +2 -0
- package/dist/types/ultramodern-tooling/config/metadata.d.ts +4 -0
- package/dist/types/ultramodern-tooling/config/normalize.d.ts +22 -0
- package/dist/types/ultramodern-tooling/config/synthesize.d.ts +5 -0
- package/dist/types/ultramodern-tooling/config/types.d.ts +45 -0
- package/dist/types/ultramodern-tooling/config.d.ts +1 -43
- package/dist/types/ultramodern-workspace/add-shell.d.ts +14 -0
- package/dist/types/ultramodern-workspace/add-vertical/constants.d.ts +4 -0
- package/dist/types/ultramodern-workspace/add-vertical/execute.d.ts +9 -0
- package/dist/types/ultramodern-workspace/add-vertical/plan.d.ts +2 -0
- package/dist/types/ultramodern-workspace/add-vertical/preflight.d.ts +42 -0
- package/dist/types/ultramodern-workspace/add-vertical/shell-files.d.ts +6 -0
- package/dist/types/ultramodern-workspace/add-vertical/topology.d.ts +9 -0
- package/dist/types/ultramodern-workspace/add-vertical/transaction.d.ts +80 -0
- package/dist/types/ultramodern-workspace/add-vertical/workspace-state.d.ts +15 -0
- package/dist/types/ultramodern-workspace/add-vertical.d.ts +7 -39
- package/dist/types/ultramodern-workspace/api/checkout-cart.d.ts +33 -0
- package/dist/types/ultramodern-workspace/api/client.d.ts +6 -0
- package/dist/types/ultramodern-workspace/api/contracts.d.ts +15 -0
- package/dist/types/ultramodern-workspace/api/names.d.ts +41 -0
- package/dist/types/ultramodern-workspace/api/rpc.d.ts +41 -0
- package/dist/types/ultramodern-workspace/api/service.d.ts +6 -0
- package/dist/types/ultramodern-workspace/api/shared.d.ts +10 -0
- package/dist/types/ultramodern-workspace/api.d.ts +6 -73
- package/dist/types/ultramodern-workspace/app-files.d.ts +4 -10
- package/dist/types/ultramodern-workspace/backend-federation.d.ts +11 -0
- package/dist/types/ultramodern-workspace/bridge-config/cli.d.ts +3 -0
- package/dist/types/ultramodern-workspace/bridge-config/defaults.d.ts +1 -0
- package/dist/types/ultramodern-workspace/bridge-config/gates.d.ts +3 -0
- package/dist/types/ultramodern-workspace/bridge-config/index.d.ts +4 -0
- package/dist/types/ultramodern-workspace/bridge-config/normalize.d.ts +2 -0
- package/dist/types/ultramodern-workspace/bridge-config/schema.d.ts +61 -0
- package/dist/types/ultramodern-workspace/bridge-config/shared.d.ts +12 -0
- package/dist/types/ultramodern-workspace/bridge-config/validation.d.ts +3 -0
- package/dist/types/ultramodern-workspace/bridge-config/workspace-packages.d.ts +3 -0
- package/dist/types/ultramodern-workspace/bridge-config.d.ts +1 -64
- package/dist/types/ultramodern-workspace/codesmith.d.ts +12 -6
- package/dist/types/ultramodern-workspace/contracts.d.ts +3 -3
- package/dist/types/ultramodern-workspace/delivery-unit-schema/types.d.ts +291 -0
- package/dist/types/ultramodern-workspace/delivery-unit-schema/up-projection.d.ts +189 -0
- package/dist/types/ultramodern-workspace/delivery-unit-stamp.d.ts +3 -0
- package/dist/types/ultramodern-workspace/delivery-unit-sync.d.ts +6 -0
- package/dist/types/ultramodern-workspace/delivery-unit.d.ts +8 -0
- package/dist/types/ultramodern-workspace/demo-components.d.ts +14 -3
- package/dist/types/ultramodern-workspace/descriptors.d.ts +27 -3
- package/dist/types/ultramodern-workspace/effect-diagnostics.d.ts +1 -0
- package/dist/types/ultramodern-workspace/fs-io.d.ts +2 -8
- package/dist/types/ultramodern-workspace/generation-result.d.ts +3 -1
- package/dist/types/ultramodern-workspace/index.d.ts +6 -1
- package/dist/types/ultramodern-workspace/locales.d.ts +1 -133
- package/dist/types/ultramodern-workspace/mf-validation/constants.d.ts +5 -0
- package/dist/types/ultramodern-workspace/mf-validation/discovery.d.ts +2 -0
- package/dist/types/ultramodern-workspace/mf-validation/inspect.d.ts +2 -0
- package/dist/types/ultramodern-workspace/mf-validation/object-literal.d.ts +4 -0
- package/dist/types/ultramodern-workspace/mf-validation/path-utils.d.ts +11 -0
- package/dist/types/ultramodern-workspace/mf-validation/syntax.d.ts +5 -0
- package/dist/types/ultramodern-workspace/mf-validation/types.d.ts +33 -0
- package/dist/types/ultramodern-workspace/mf-validation/validate.d.ts +4 -0
- package/dist/types/ultramodern-workspace/mf-validation.d.ts +4 -27
- package/dist/types/ultramodern-workspace/module-federation/config.d.ts +5 -0
- package/dist/types/ultramodern-workspace/module-federation/index.d.ts +4 -0
- package/dist/types/ultramodern-workspace/module-federation/reexport-module.d.ts +4 -0
- package/dist/types/ultramodern-workspace/module-federation/remote-refs.d.ts +3 -0
- package/dist/types/ultramodern-workspace/module-federation/shared-config.d.ts +2 -0
- package/dist/types/ultramodern-workspace/module-federation.d.ts +2 -17
- package/dist/types/ultramodern-workspace/naming.d.ts +0 -2
- package/dist/types/ultramodern-workspace/overlay-baseline-guard.d.ts +57 -0
- package/dist/types/ultramodern-workspace/package-json.d.ts +2 -17
- package/dist/types/ultramodern-workspace/package-source.d.ts +1 -0
- package/dist/types/ultramodern-workspace/policy.d.ts +254 -47
- package/dist/types/ultramodern-workspace/public-api.d.ts +1 -1
- package/dist/types/ultramodern-workspace/public-surface.d.ts +5 -15
- package/dist/types/ultramodern-workspace/routes.d.ts +0 -9
- package/dist/types/ultramodern-workspace/shared-patches.d.ts +1 -0
- package/dist/types/ultramodern-workspace/shells.d.ts +51 -0
- package/dist/types/ultramodern-workspace/tooling-command-catalog.d.ts +18 -0
- package/dist/types/ultramodern-workspace/tsconfigs.d.ts +13 -0
- package/dist/types/ultramodern-workspace/types.d.ts +134 -3
- package/dist/types/ultramodern-workspace/versions.d.ts +35 -26
- package/dist/types/ultramodern-workspace/workspace-script-plan.d.ts +63 -0
- package/dist/types/ultramodern-workspace/workspace-scripts.d.ts +15 -5
- package/dist/types/ultramodern-workspace/workspace-validation-contract.d.ts +243 -0
- package/dist/types/ultramodern-workspace/write-app.d.ts +4 -0
- package/dist/types/ultramodern-workspace/write-workspace.d.ts +3 -3
- package/dist/types/ultramodern-workspace/zerops.d.ts +2 -0
- package/package.json +15 -13
- package/template-workspace/.gitignore.handlebars +8 -0
- package/template-workspace/.modernjs/release-cohort.json +208 -0
- package/template-workspace/AGENTS.md.handlebars +16 -89
- package/template-workspace/README.md.handlebars +17 -16
- package/template-workspace/patches/@module-federation__bridge-react@2.8.0.patch +185 -0
- package/template-workspace/patches/@module-federation__modern-js-v3@2.8.0.patch +259 -0
- package/template-workspace/patches/@tanstack__router-core@1.171.14.patch +123 -0
- package/template-workspace/patches/drizzle-orm-ts7-strict-declarations.patch +452 -0
- package/template-workspace/patches/effect-schema-error-type-id.patch +18 -0
- package/template-workspace/pnpm-workspace.yaml.handlebars +8 -25
- package/template-workspace/scripts/bootstrap-agent-skills.mjs +11 -3
- package/templates/app/shell-frame.tsx +5 -4
- package/templates/workspace/apps/modern.config.shared-module-federation.ts.handlebars +37 -0
- package/templates/workspace/apps/modern.config.ts.handlebars +191 -0
- package/templates/workspace/apps/shared/src/routes/ultramodern-jsonld.ts.handlebars +119 -0
- package/templates/workspace/apps/shared/src/routes/ultramodern-route-metadata.ts.handlebars +21 -0
- package/templates/workspace/apps/shell-super-app/src/routes/[lang]/page.tsx.handlebars +49 -0
- package/templates/workspace/apps/shell-super-app/src/routes/vertical-components.helpers.tsx.handlebars +27 -0
- package/templates/workspace/apps/shell-super-app/src/routes/vertical-components.imports.tsx.handlebars +4 -0
- package/templates/workspace/apps/shell-super-app/src/routes/vertical-components.tsx.handlebars +44 -0
- package/templates/workspace/apps/shell-super-app/src/routes/vertical-components.worker.helpers.tsx.handlebars +17 -0
- package/templates/workspace/apps/shell-super-app/src/routes/vertical-components.worker.imports.tsx.handlebars +1 -0
- package/templates/workspace/packages/shared-design-tokens/tokens.css.handlebars +7 -0
- package/templates/workspace/tailwind.config.ts.handlebars +3 -0
- package/templates/workspace/verticals/server/checkout-cart-handlers.ts.handlebars +49 -0
- package/templates/workspace/verticals/server/checkout-cart-state.ts.handlebars +23 -0
- package/templates/workspace/verticals/shared/api.checkout-cart-contract-fields.ts.handlebars +1 -0
- package/templates/workspace/verticals/shared/api.checkout-cart-endpoints.ts.handlebars +23 -0
- package/templates/workspace/verticals/shared/api.checkout-cart-operation-contexts.ts.handlebars +29 -0
- package/templates/workspace/verticals/shared/api.checkout-cart-schemas.ts.handlebars +48 -0
- package/templates/workspace/verticals/src/api/checkout-cart-client-exports.ts.handlebars +70 -0
- package/templates/workspace-scripts/assert-mf-types.mjs.handlebars +4 -4
- package/templates/workspace-scripts/check-ultramodern-api-boundaries.mts +40 -2
- package/templates/workspace-scripts/generate-node-backend-federation.mjs +379 -0
- package/templates/workspace-scripts/generate-public-surface-assets.mjs +5 -5
- package/templates/workspace-scripts/materialize-zerops-runtime.mjs +447 -0
- package/templates/workspace-scripts/proof-cloudflare-version.mjs +237 -9
- package/templates/workspace-scripts/proof-node-backend-federation.mjs +1223 -0
- package/templates/workspace-scripts/proof-workerd-ssr.mts +815 -0
- package/templates/workspace-scripts/ultramodern-cloudflare-proof.mjs +188 -2
- package/templates/workspace-scripts/ultramodern-typecheck.mjs +6 -46
- package/templates/workspace-scripts/validate-ultramodern-workspace.mjs.handlebars +2436 -258
- package/template-workspace/patches/@tanstack__router-core@1.171.13.patch +0 -51
|
@@ -3,33 +3,55 @@ import crypto from 'node:crypto';
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
|
|
6
|
+
// Generated by modern-js-create with an immutable expected proof contract.
|
|
6
7
|
const root = process.cwd();
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
8
|
+
const workspaceValidationContract = {{workspaceValidationContractJson}};
|
|
9
|
+
const packageScope = workspaceValidationContract.packageScope;
|
|
10
|
+
const expectedNodeVersion = workspaceValidationContract.versions.node;
|
|
11
|
+
const expectedEffectVersion = workspaceValidationContract.versions.effect;
|
|
12
|
+
const expectedModuleFederationVersion =
|
|
13
|
+
workspaceValidationContract.versions.moduleFederation;
|
|
14
|
+
const expectedCloudflareCompatibilityDate =
|
|
15
|
+
workspaceValidationContract.versions.cloudflareCompatibilityDate;
|
|
16
|
+
const tailwindEnabled = workspaceValidationContract.tailwindEnabled;
|
|
17
|
+
const fullStackVerticals = workspaceValidationContract.fullStackVerticals;
|
|
18
|
+
// Backend-federation and Zerops runtime surfaces only exist when the workspace
|
|
19
|
+
// exposes API-bearing verticals. Shell-only workspaces skip their
|
|
20
|
+
// materialization during migrate, so the contract must not require them.
|
|
21
|
+
const hasBackendSurfaces = fullStackVerticals.some(
|
|
22
|
+
vertical => vertical.emitsApi,
|
|
23
|
+
);
|
|
24
|
+
// Every vertical (ui-only and horizontal-remote included) is a delivery unit
|
|
25
|
+
// and deploys via Zerops; only the BACKEND proof/generation surfaces depend on
|
|
26
|
+
// an API-bearing unit existing (split gating).
|
|
27
|
+
const hasDeliveryUnits = fullStackVerticals.length > 0;
|
|
28
|
+
const shellNamespace = workspaceValidationContract.shellNamespace;
|
|
29
|
+
const oldRemotePaths = workspaceValidationContract.oldRemotePaths;
|
|
30
|
+
const expectedBuildScript = workspaceValidationContract.scripts.build;
|
|
31
|
+
const expectedCloudflareBuildScript =
|
|
32
|
+
workspaceValidationContract.scripts.cloudflareBuild;
|
|
33
|
+
const expectedCloudflareDeployScript =
|
|
34
|
+
workspaceValidationContract.scripts.cloudflareDeploy;
|
|
35
|
+
const expectedCloudflareSecurity =
|
|
36
|
+
workspaceValidationContract.cloudflareSecurity;
|
|
37
|
+
const publicSurfaceManagedSourceAssetPaths =
|
|
38
|
+
workspaceValidationContract.publicSurfaceManagedSourceAssetPaths;
|
|
39
|
+
const shellRouteMetaPaths = workspaceValidationContract.shellRouteMetaPaths;
|
|
40
|
+
const compactConfigPath =
|
|
41
|
+
workspaceValidationContract.metadata.compactConfig.path;
|
|
42
|
+
const retiredMetadataPaths =
|
|
43
|
+
workspaceValidationContract.legacy.retiredMetadataPaths;
|
|
44
|
+
const modernPackageCohort =
|
|
45
|
+
workspaceValidationContract.cohort.modernPackages;
|
|
46
|
+
const expectedAdditionalShellIds =
|
|
47
|
+
workspaceValidationContract.cohort.additionalShellIds ?? [];
|
|
48
|
+
const expectedAdditionalShells =
|
|
49
|
+
workspaceValidationContract.additionalShells ?? [];
|
|
50
|
+
const expectedPrimaryShellVerticalIds =
|
|
51
|
+
workspaceValidationContract.topology?.referenceTopology?.shell
|
|
52
|
+
?.verticalRefs ?? workspaceValidationContract.cohort.verticalIds;
|
|
53
|
+
const expectedReleaseCohort =
|
|
54
|
+
workspaceValidationContract.cohort.releaseCohort;
|
|
33
55
|
const expectedModernPackageSpecifier = packageName => {
|
|
34
56
|
if (packageSource.strategy === 'workspace') {
|
|
35
57
|
return 'workspace:*';
|
|
@@ -53,8 +75,64 @@ const assertExists = relativePath => {
|
|
|
53
75
|
const assertNotExists = relativePath => {
|
|
54
76
|
assert(!fs.existsSync(path.join(root, relativePath)), `Unexpected ${relativePath}`);
|
|
55
77
|
};
|
|
78
|
+
const assertAnyOf = relativePaths => {
|
|
79
|
+
assert(
|
|
80
|
+
relativePaths.some(relativePath =>
|
|
81
|
+
fs.existsSync(path.join(root, relativePath)),
|
|
82
|
+
),
|
|
83
|
+
`Missing one of: ${relativePaths.join(', ')}`,
|
|
84
|
+
);
|
|
85
|
+
};
|
|
86
|
+
const requiredShellWorkerCompositionPath = shellPath => {
|
|
87
|
+
const workerCompositionPath = `${shellPath}/src/routes/vertical-components.worker.tsx`;
|
|
88
|
+
if (fs.existsSync(path.join(root, workerCompositionPath))) {
|
|
89
|
+
return workerCompositionPath;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const browserComposition = readText(
|
|
93
|
+
`${shellPath}/src/routes/vertical-components.tsx`,
|
|
94
|
+
);
|
|
95
|
+
const frameworkGeneratedComposition =
|
|
96
|
+
browserComposition.includes('const createRemoteComponent =') &&
|
|
97
|
+
browserComposition.includes('export const VerticalShowcase =');
|
|
98
|
+
return frameworkGeneratedComposition
|
|
99
|
+
? workerCompositionPath
|
|
100
|
+
: `${shellPath}/src/federated-components.worker.tsx`;
|
|
101
|
+
};
|
|
102
|
+
const canonicalizeJson = value => {
|
|
103
|
+
if (Array.isArray(value)) {
|
|
104
|
+
const entries = value.map(canonicalizeJson);
|
|
105
|
+
const keyedEntries = entries.every(
|
|
106
|
+
entry =>
|
|
107
|
+
entry !== null &&
|
|
108
|
+
typeof entry === 'object' &&
|
|
109
|
+
!Array.isArray(entry) &&
|
|
110
|
+
typeof entry.id === 'string',
|
|
111
|
+
);
|
|
112
|
+
if (keyedEntries) {
|
|
113
|
+
const ids = entries.map(entry => entry.id);
|
|
114
|
+
if (new Set(ids).size === ids.length) {
|
|
115
|
+
return entries.toSorted((left, right) => left.id.localeCompare(right.id));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return entries;
|
|
119
|
+
}
|
|
120
|
+
if (value !== null && typeof value === 'object') {
|
|
121
|
+
return Object.fromEntries(
|
|
122
|
+
Object.keys(value)
|
|
123
|
+
.toSorted()
|
|
124
|
+
.map(key => [key, canonicalizeJson(value[key])]),
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
return value;
|
|
128
|
+
};
|
|
129
|
+
const sameJson = (actual, expected) =>
|
|
130
|
+
JSON.stringify(canonicalizeJson(actual)) ===
|
|
131
|
+
JSON.stringify(canonicalizeJson(expected));
|
|
56
132
|
const formatJson = value =>
|
|
57
|
-
value === undefined ? 'undefined' : JSON.stringify(value);
|
|
133
|
+
value === undefined ? 'undefined' : JSON.stringify(canonicalizeJson(value));
|
|
134
|
+
const quoteYamlString = value => `'${String(value).replace(/'/gu, "''")}'`;
|
|
135
|
+
const quoteShellValue = value => `'${String(value).replace(/'/gu, "'\\''")}'`;
|
|
58
136
|
const selfCheckFailure = (contract, message, fixArea) =>
|
|
59
137
|
`MicroVertical contract self-check failed: ${contract}. ${message}. Fix area: ${fixArea}.`;
|
|
60
138
|
const assertSelfCheck = (condition, contract, message, fixArea) => {
|
|
@@ -62,7 +140,7 @@ const assertSelfCheck = (condition, contract, message, fixArea) => {
|
|
|
62
140
|
};
|
|
63
141
|
const assertSameJson = (actual, expected, contract, fixArea) => {
|
|
64
142
|
assertSelfCheck(
|
|
65
|
-
|
|
143
|
+
sameJson(actual, expected),
|
|
66
144
|
contract,
|
|
67
145
|
`Expected ${formatJson(expected)}, found ${formatJson(actual)}`,
|
|
68
146
|
fixArea,
|
|
@@ -84,6 +162,590 @@ const assertArray = (value, contract, fixArea) => {
|
|
|
84
162
|
fixArea,
|
|
85
163
|
);
|
|
86
164
|
};
|
|
165
|
+
const assertUniqueStrings = (values, contract) => {
|
|
166
|
+
assert(Array.isArray(values), `${contract} must be an array`);
|
|
167
|
+
const seen = new Set();
|
|
168
|
+
for (const value of values) {
|
|
169
|
+
assert(
|
|
170
|
+
typeof value === 'string' && value.length > 0,
|
|
171
|
+
`${contract} must contain non-empty strings`,
|
|
172
|
+
);
|
|
173
|
+
assert(!seen.has(value), `Duplicate value "${value}" in ${contract}`);
|
|
174
|
+
seen.add(value);
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
const assertUniqueIdEntries = (entries, contract) => {
|
|
178
|
+
assert(Array.isArray(entries), `${contract} must be an array`);
|
|
179
|
+
const seen = new Set();
|
|
180
|
+
for (const entry of entries) {
|
|
181
|
+
const id = entry?.id;
|
|
182
|
+
assert(
|
|
183
|
+
typeof id === 'string' && id.length > 0,
|
|
184
|
+
`${contract} entries must have non-empty string ids`,
|
|
185
|
+
);
|
|
186
|
+
assert(!seen.has(id), `Duplicate id "${id}" in ${contract}`);
|
|
187
|
+
seen.add(id);
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
const assertSameIdCohort = (entries, expectedIds, contract, fixArea) => {
|
|
191
|
+
assertUniqueIdEntries(entries, contract);
|
|
192
|
+
assertSameJson(
|
|
193
|
+
entries.map(entry => entry.id).toSorted(),
|
|
194
|
+
[...expectedIds].toSorted(),
|
|
195
|
+
`${contract} cohort`,
|
|
196
|
+
fixArea,
|
|
197
|
+
);
|
|
198
|
+
};
|
|
199
|
+
const assertWorkspaceValidationContract = contract => {
|
|
200
|
+
assert(
|
|
201
|
+
contract !== null &&
|
|
202
|
+
typeof contract === 'object' &&
|
|
203
|
+
!Array.isArray(contract),
|
|
204
|
+
'Workspace validation contract must be a JSON object',
|
|
205
|
+
);
|
|
206
|
+
assert(
|
|
207
|
+
contract.schemaVersion === 1,
|
|
208
|
+
`Unsupported workspace validation contract schemaVersion ${formatJson(contract.schemaVersion)}; expected 1`,
|
|
209
|
+
);
|
|
210
|
+
assert(
|
|
211
|
+
contract.kind === 'modernjs.ultramodern-workspace-validation-contract',
|
|
212
|
+
`Unsupported workspace validation contract kind ${formatJson(contract.kind)}`,
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const metadataEntries = Object.entries(contract.metadata ?? {});
|
|
216
|
+
assert(
|
|
217
|
+
metadataEntries.length === (contract.cohort?.releaseCohort ? 5 : 4),
|
|
218
|
+
'Workspace validation contract must declare every structured metadata input',
|
|
219
|
+
);
|
|
220
|
+
for (const [name, metadata] of metadataEntries) {
|
|
221
|
+
assert(
|
|
222
|
+
typeof metadata?.path === 'string' && metadata.path.length > 0,
|
|
223
|
+
`Workspace validation contract metadata.${name}.path is required`,
|
|
224
|
+
);
|
|
225
|
+
assert(
|
|
226
|
+
metadata.schemaVersion === 1,
|
|
227
|
+
`Unsupported expected metadata schemaVersion ${formatJson(metadata.schemaVersion)} for ${name}`,
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
assertUniqueStrings(
|
|
232
|
+
contract.cohort?.modernPackages,
|
|
233
|
+
'workspace validation contract Modern package cohort',
|
|
234
|
+
);
|
|
235
|
+
assertUniqueStrings(
|
|
236
|
+
contract.cohort?.appIds,
|
|
237
|
+
'workspace validation contract app cohort',
|
|
238
|
+
);
|
|
239
|
+
assertUniqueStrings(
|
|
240
|
+
contract.cohort?.additionalShellIds ?? [],
|
|
241
|
+
'workspace validation contract additional-shell cohort',
|
|
242
|
+
);
|
|
243
|
+
for (const [field, label] of [
|
|
244
|
+
[
|
|
245
|
+
'additionalShellOwnerIds',
|
|
246
|
+
'workspace validation contract additional-shell owner cohort',
|
|
247
|
+
],
|
|
248
|
+
[
|
|
249
|
+
'additionalShellDeliveryUnitIds',
|
|
250
|
+
'workspace validation contract additional-shell delivery-unit cohort',
|
|
251
|
+
],
|
|
252
|
+
[
|
|
253
|
+
'additionalShellDegradedStateIds',
|
|
254
|
+
'workspace validation contract additional-shell degraded-state cohort',
|
|
255
|
+
],
|
|
256
|
+
[
|
|
257
|
+
'additionalShellBuildMarkerIds',
|
|
258
|
+
'workspace validation contract additional-shell build-marker cohort',
|
|
259
|
+
],
|
|
260
|
+
]) {
|
|
261
|
+
assertUniqueStrings(contract.cohort?.[field] ?? [], label);
|
|
262
|
+
}
|
|
263
|
+
assertUniqueStrings(
|
|
264
|
+
contract.cohort?.backendAppIds,
|
|
265
|
+
'workspace validation contract backend app cohort',
|
|
266
|
+
);
|
|
267
|
+
assertUniqueStrings(
|
|
268
|
+
contract.cohort?.verticalIds,
|
|
269
|
+
'workspace validation contract vertical cohort',
|
|
270
|
+
);
|
|
271
|
+
assertUniqueStrings(
|
|
272
|
+
contract.cohort?.sharedPackageIds,
|
|
273
|
+
'workspace validation contract shared package cohort',
|
|
274
|
+
);
|
|
275
|
+
assertUniqueStrings(
|
|
276
|
+
contract.cohort?.ownerIds,
|
|
277
|
+
'workspace validation contract owner cohort',
|
|
278
|
+
);
|
|
279
|
+
assertUniqueIdEntries(
|
|
280
|
+
contract.cohort?.packageManifests,
|
|
281
|
+
'workspace validation contract package manifests',
|
|
282
|
+
);
|
|
283
|
+
assertUniqueStrings(
|
|
284
|
+
(contract.cohort?.packageManifests ?? []).map(manifest => manifest.path),
|
|
285
|
+
'workspace validation contract package manifest paths',
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
const generatedSurfacePolicy = contract.generatedSurfacePolicy;
|
|
289
|
+
assert(
|
|
290
|
+
generatedSurfacePolicy?.schemaVersion === 1,
|
|
291
|
+
`Unsupported generated surface policy schemaVersion ${formatJson(generatedSurfacePolicy?.schemaVersion)}; expected 1`,
|
|
292
|
+
);
|
|
293
|
+
assertUniqueIdEntries(
|
|
294
|
+
generatedSurfacePolicy?.rules,
|
|
295
|
+
'workspace validation contract generated surface policy rules',
|
|
296
|
+
);
|
|
297
|
+
for (const rule of generatedSurfacePolicy.rules) {
|
|
298
|
+
assertUniqueStrings(
|
|
299
|
+
(rule.paths ?? []).map(entry => entry.path),
|
|
300
|
+
`generated surface policy ${rule.id} paths`,
|
|
301
|
+
);
|
|
302
|
+
assert(
|
|
303
|
+
Array.isArray(rule.paths) && rule.paths.length > 0,
|
|
304
|
+
`generated surface policy ${rule.id} must target generated paths`,
|
|
305
|
+
);
|
|
306
|
+
for (const target of rule.paths) {
|
|
307
|
+
assert(
|
|
308
|
+
target?.kind === 'file' || target?.kind === 'directory',
|
|
309
|
+
`generated surface policy ${rule.id} has an invalid target kind`,
|
|
310
|
+
);
|
|
311
|
+
assert(
|
|
312
|
+
typeof target.path === 'string' && target.path.length > 0,
|
|
313
|
+
`generated surface policy ${rule.id} target path is required`,
|
|
314
|
+
);
|
|
315
|
+
if (target.kind === 'directory') {
|
|
316
|
+
assertUniqueStrings(
|
|
317
|
+
target.extensions,
|
|
318
|
+
`generated surface policy ${rule.id} directory extensions`,
|
|
319
|
+
);
|
|
320
|
+
assertUniqueStrings(
|
|
321
|
+
target.excludePaths ?? [],
|
|
322
|
+
`generated surface policy ${rule.id} directory exclusions`,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
assertUniqueIdEntries(
|
|
327
|
+
rule.patterns,
|
|
328
|
+
`generated surface policy ${rule.id} patterns`,
|
|
329
|
+
);
|
|
330
|
+
for (const pattern of rule.patterns) {
|
|
331
|
+
if (pattern.structuralMatcher !== undefined) {
|
|
332
|
+
assert(
|
|
333
|
+
pattern.expression === undefined && pattern.flags === undefined,
|
|
334
|
+
`generated surface policy ${rule.id}.${pattern.id} must use exactly one matcher`,
|
|
335
|
+
);
|
|
336
|
+
assert(
|
|
337
|
+
pattern.structuralMatcher?.kind === 'jsx-attribute',
|
|
338
|
+
`generated surface policy ${rule.id}.${pattern.id} has an unsupported structural matcher`,
|
|
339
|
+
);
|
|
340
|
+
assert(
|
|
341
|
+
typeof pattern.structuralMatcher.elementName === 'string' &&
|
|
342
|
+
pattern.structuralMatcher.elementName.length > 0,
|
|
343
|
+
`generated surface policy ${rule.id}.${pattern.id} structural elementName is required`,
|
|
344
|
+
);
|
|
345
|
+
assert(
|
|
346
|
+
typeof pattern.structuralMatcher.attributeName === 'string' &&
|
|
347
|
+
pattern.structuralMatcher.attributeName.length > 0,
|
|
348
|
+
`generated surface policy ${rule.id}.${pattern.id} structural attributeName is required`,
|
|
349
|
+
);
|
|
350
|
+
} else {
|
|
351
|
+
assert(
|
|
352
|
+
typeof pattern.expression === 'string' && pattern.expression.length > 0,
|
|
353
|
+
`generated surface policy ${rule.id}.${pattern.id} expression is required`,
|
|
354
|
+
);
|
|
355
|
+
assert(
|
|
356
|
+
pattern.flags === 'u',
|
|
357
|
+
`generated surface policy ${rule.id}.${pattern.id} must use deterministic Unicode matching`,
|
|
358
|
+
);
|
|
359
|
+
new RegExp(pattern.expression, pattern.flags);
|
|
360
|
+
}
|
|
361
|
+
assert(
|
|
362
|
+
typeof pattern.diagnostic === 'string' && pattern.diagnostic.length > 0,
|
|
363
|
+
`generated surface policy ${rule.id}.${pattern.id} diagnostic is required`,
|
|
364
|
+
);
|
|
365
|
+
assert(
|
|
366
|
+
typeof pattern.fixArea === 'string' && pattern.fixArea.length > 0,
|
|
367
|
+
`generated surface policy ${rule.id}.${pattern.id} fixArea is required`,
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
const generatedSurfacePolicyFiles = target => {
|
|
373
|
+
const absolutePath = path.join(root, target.path);
|
|
374
|
+
if (target.kind === 'file') {
|
|
375
|
+
assert(fs.existsSync(absolutePath), `Missing generated surface policy file ${target.path}`);
|
|
376
|
+
return [target.path];
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
assert(fs.existsSync(absolutePath), `Missing generated surface policy directory ${target.path}`);
|
|
380
|
+
const files = [];
|
|
381
|
+
const queue = [absolutePath];
|
|
382
|
+
while (queue.length > 0) {
|
|
383
|
+
const current = queue.shift();
|
|
384
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
385
|
+
const absoluteEntryPath = path.join(current, entry.name);
|
|
386
|
+
if (entry.isDirectory()) {
|
|
387
|
+
queue.push(absoluteEntryPath);
|
|
388
|
+
} else if (target.extensions.includes(path.extname(entry.name))) {
|
|
389
|
+
files.push(path.relative(root, absoluteEntryPath).split(path.sep).join('/'));
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
const excludedPaths = new Set(target.excludePaths ?? []);
|
|
394
|
+
return files.filter(file => !excludedPaths.has(file)).toSorted();
|
|
395
|
+
};
|
|
396
|
+
const isJsxNameCharacter = character =>
|
|
397
|
+
typeof character === 'string' && /[\w:.-]/u.test(character);
|
|
398
|
+
const skipQuotedSource = (source, start) => {
|
|
399
|
+
const quote = source[start];
|
|
400
|
+
let cursor = start + 1;
|
|
401
|
+
while (cursor < source.length) {
|
|
402
|
+
if (source[cursor] === '\\') {
|
|
403
|
+
cursor += 2;
|
|
404
|
+
} else if (source[cursor] === quote) {
|
|
405
|
+
return cursor + 1;
|
|
406
|
+
} else {
|
|
407
|
+
cursor += 1;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return cursor;
|
|
411
|
+
};
|
|
412
|
+
const skipSourceComment = (source, start) => {
|
|
413
|
+
if (source[start + 1] === '/') {
|
|
414
|
+
const lineEnd = source.indexOf('\n', start + 2);
|
|
415
|
+
return lineEnd === -1 ? source.length : lineEnd + 1;
|
|
416
|
+
}
|
|
417
|
+
if (source[start + 1] === '*') {
|
|
418
|
+
const commentEnd = source.indexOf('*/', start + 2);
|
|
419
|
+
return commentEnd === -1 ? source.length : commentEnd + 2;
|
|
420
|
+
}
|
|
421
|
+
return start;
|
|
422
|
+
};
|
|
423
|
+
const findJsxAttribute = (source, matcher) => {
|
|
424
|
+
const opening = `<${matcher.elementName}`;
|
|
425
|
+
let elementIndex = source.indexOf(opening);
|
|
426
|
+
while (elementIndex !== -1) {
|
|
427
|
+
const elementBoundary = source[elementIndex + opening.length];
|
|
428
|
+
if (!isJsxNameCharacter(elementBoundary)) {
|
|
429
|
+
let cursor = elementIndex + opening.length;
|
|
430
|
+
let expressionDepth = 0;
|
|
431
|
+
while (cursor < source.length) {
|
|
432
|
+
const character = source[cursor];
|
|
433
|
+
if (character === "'" || character === '"' || character === '`') {
|
|
434
|
+
cursor = skipQuotedSource(source, cursor);
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
if (character === '/') {
|
|
438
|
+
const afterComment = skipSourceComment(source, cursor);
|
|
439
|
+
if (afterComment !== cursor) {
|
|
440
|
+
cursor = afterComment;
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
if (character === '{') {
|
|
445
|
+
expressionDepth += 1;
|
|
446
|
+
cursor += 1;
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
if (character === '}') {
|
|
450
|
+
expressionDepth = Math.max(0, expressionDepth - 1);
|
|
451
|
+
cursor += 1;
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
if (character === '>' && expressionDepth === 0) {
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
457
|
+
if (
|
|
458
|
+
expressionDepth === 0 &&
|
|
459
|
+
source.startsWith(matcher.attributeName, cursor) &&
|
|
460
|
+
!isJsxNameCharacter(source[cursor - 1]) &&
|
|
461
|
+
!isJsxNameCharacter(source[cursor + matcher.attributeName.length])
|
|
462
|
+
) {
|
|
463
|
+
let equalsIndex = cursor + matcher.attributeName.length;
|
|
464
|
+
while (/\s/u.test(source[equalsIndex] ?? '')) {
|
|
465
|
+
equalsIndex += 1;
|
|
466
|
+
}
|
|
467
|
+
if (source[equalsIndex] === '=') {
|
|
468
|
+
return { index: cursor };
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
cursor += 1;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
elementIndex = source.indexOf(opening, elementIndex + opening.length);
|
|
475
|
+
}
|
|
476
|
+
return null;
|
|
477
|
+
};
|
|
478
|
+
const findGeneratedSurfacePolicyMatch = (source, pattern) =>
|
|
479
|
+
pattern.structuralMatcher?.kind === 'jsx-attribute'
|
|
480
|
+
? findJsxAttribute(source, pattern.structuralMatcher)
|
|
481
|
+
: new RegExp(pattern.expression, pattern.flags).exec(source);
|
|
482
|
+
const assertGeneratedSurfacePolicy = () => {
|
|
483
|
+
for (const rule of workspaceValidationContract.generatedSurfacePolicy.rules) {
|
|
484
|
+
const files = rule.paths.flatMap(generatedSurfacePolicyFiles).toSorted();
|
|
485
|
+
for (const relativePath of files) {
|
|
486
|
+
const source = readText(relativePath);
|
|
487
|
+
for (const pattern of rule.patterns) {
|
|
488
|
+
const match = findGeneratedSurfacePolicyMatch(source, pattern);
|
|
489
|
+
assertSelfCheck(
|
|
490
|
+
match === null,
|
|
491
|
+
`generated surface policy ${rule.id}.${pattern.id}`,
|
|
492
|
+
`${pattern.diagnostic} Found forbidden source at ${relativePath}:${match?.index ?? 0}`,
|
|
493
|
+
pattern.fixArea,
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if ((workspaceValidationContract.cohort?.additionalShellIds ?? []).length > 0) {
|
|
499
|
+
for (const field of [
|
|
500
|
+
'additionalShellOwnerIds',
|
|
501
|
+
'additionalShellDeliveryUnitIds',
|
|
502
|
+
'additionalShellDegradedStateIds',
|
|
503
|
+
'additionalShellBuildMarkerIds',
|
|
504
|
+
]) {
|
|
505
|
+
assertSameJson(
|
|
506
|
+
workspaceValidationContract.cohort?.[field],
|
|
507
|
+
workspaceValidationContract.cohort.additionalShellIds,
|
|
508
|
+
`workspace validation contract ${field}`,
|
|
509
|
+
'restore every generated additional-shell cohort',
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
assertSameIdCohort(
|
|
513
|
+
workspaceValidationContract.cohort?.additionalShellManifests,
|
|
514
|
+
workspaceValidationContract.cohort.additionalShellIds,
|
|
515
|
+
'workspace validation contract additional-shell manifests',
|
|
516
|
+
'restore every generated additional-shell package manifest',
|
|
517
|
+
);
|
|
518
|
+
assertSameIdCohort(
|
|
519
|
+
workspaceValidationContract.additionalShells,
|
|
520
|
+
workspaceValidationContract.cohort.additionalShellIds,
|
|
521
|
+
'workspace validation contract additional-shell records',
|
|
522
|
+
'restore every generated additional-shell contract record',
|
|
523
|
+
);
|
|
524
|
+
} else {
|
|
525
|
+
assert(
|
|
526
|
+
workspaceValidationContract.cohort?.additionalShellManifests === undefined,
|
|
527
|
+
'Single-shell workspace must not declare additional-shell manifests',
|
|
528
|
+
);
|
|
529
|
+
assert(
|
|
530
|
+
workspaceValidationContract.additionalShells === undefined,
|
|
531
|
+
'Single-shell workspace must not declare additional-shell records',
|
|
532
|
+
);
|
|
533
|
+
for (const field of [
|
|
534
|
+
'additionalShellOwnerIds',
|
|
535
|
+
'additionalShellDeliveryUnitIds',
|
|
536
|
+
'additionalShellDegradedStateIds',
|
|
537
|
+
'additionalShellBuildMarkerIds',
|
|
538
|
+
]) {
|
|
539
|
+
assert(
|
|
540
|
+
workspaceValidationContract.cohort?.[field] === undefined,
|
|
541
|
+
`Single-shell workspace must not declare ${field}`,
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
const compactConfigPolicyView = config => ({
|
|
547
|
+
schemaVersion: config.schemaVersion,
|
|
548
|
+
profile: config.profile,
|
|
549
|
+
workspace: config.workspace,
|
|
550
|
+
features: config.features,
|
|
551
|
+
deploy: config.deploy,
|
|
552
|
+
moduleFederation: config.moduleFederation,
|
|
553
|
+
backendFederation: config.backendFederation,
|
|
554
|
+
agentSkills: config.agentSkills,
|
|
555
|
+
tooling: config.tooling,
|
|
556
|
+
});
|
|
557
|
+
const assertStructuredWorkspaceMetadata = ({
|
|
558
|
+
ultramodernConfig,
|
|
559
|
+
topology,
|
|
560
|
+
ownership,
|
|
561
|
+
overlay,
|
|
562
|
+
}) => {
|
|
563
|
+
const observedMetadata = [
|
|
564
|
+
{
|
|
565
|
+
contract: workspaceValidationContract.metadata.compactConfig,
|
|
566
|
+
value: ultramodernConfig,
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
contract: workspaceValidationContract.metadata.referenceTopology,
|
|
570
|
+
value: topology,
|
|
571
|
+
},
|
|
572
|
+
{
|
|
573
|
+
contract: workspaceValidationContract.metadata.ownership,
|
|
574
|
+
value: ownership,
|
|
575
|
+
},
|
|
576
|
+
{
|
|
577
|
+
contract: workspaceValidationContract.metadata.developmentOverlay,
|
|
578
|
+
value: overlay,
|
|
579
|
+
},
|
|
580
|
+
];
|
|
581
|
+
|
|
582
|
+
for (const entry of observedMetadata) {
|
|
583
|
+
assert(
|
|
584
|
+
entry.value !== null &&
|
|
585
|
+
typeof entry.value === 'object' &&
|
|
586
|
+
!Array.isArray(entry.value),
|
|
587
|
+
`${entry.contract.path} must contain a JSON object`,
|
|
588
|
+
);
|
|
589
|
+
assert(
|
|
590
|
+
Number.isInteger(entry.value.schemaVersion),
|
|
591
|
+
`${entry.contract.path} must declare an integer schemaVersion`,
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const observedSchemaVersions = new Set(
|
|
596
|
+
observedMetadata.map(entry => entry.value.schemaVersion),
|
|
597
|
+
);
|
|
598
|
+
assert(
|
|
599
|
+
observedSchemaVersions.size === 1,
|
|
600
|
+
`Mixed workspace metadata schema versions: ${observedMetadata
|
|
601
|
+
.map(entry => `${entry.contract.path}=${entry.value.schemaVersion}`)
|
|
602
|
+
.join(', ')}`,
|
|
603
|
+
);
|
|
604
|
+
for (const entry of observedMetadata) {
|
|
605
|
+
assert(
|
|
606
|
+
entry.value.schemaVersion === entry.contract.schemaVersion,
|
|
607
|
+
`Unsupported workspace metadata schemaVersion ${entry.value.schemaVersion} at ${entry.contract.path}; expected ${entry.contract.schemaVersion}`,
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
assertObject(
|
|
612
|
+
ultramodernConfig.packageSource,
|
|
613
|
+
`${compactConfigPath} packageSource`,
|
|
614
|
+
'restore generated compact package-source metadata',
|
|
615
|
+
);
|
|
616
|
+
for (const field of
|
|
617
|
+
workspaceValidationContract.legacy.forbiddenCompactConfigFields) {
|
|
618
|
+
assert(
|
|
619
|
+
!Object.hasOwn(ultramodernConfig, field),
|
|
620
|
+
`Stale legacy field ${compactConfigPath}.${field} is forbidden`,
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
for (const field of
|
|
624
|
+
workspaceValidationContract.legacy.forbiddenPackageSourceFields) {
|
|
625
|
+
assert(
|
|
626
|
+
!Object.hasOwn(ultramodernConfig.packageSource, field),
|
|
627
|
+
`Stale legacy field ${compactConfigPath}.packageSource.${field} is forbidden`,
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
for (const field of workspaceValidationContract.legacy.forbiddenTopologyFields) {
|
|
631
|
+
assert(
|
|
632
|
+
!Object.hasOwn(topology, field),
|
|
633
|
+
`Stale legacy field ${workspaceValidationContract.metadata.referenceTopology.path}.${field} is forbidden`,
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
assertSameIdCohort(
|
|
638
|
+
ultramodernConfig.topology?.apps,
|
|
639
|
+
workspaceValidationContract.cohort.appIds,
|
|
640
|
+
`${compactConfigPath} topology.apps`,
|
|
641
|
+
'restore the complete generated app cohort',
|
|
642
|
+
);
|
|
643
|
+
assertSameIdCohort(
|
|
644
|
+
ultramodernConfig.moduleFederation?.apps,
|
|
645
|
+
workspaceValidationContract.cohort.appIds,
|
|
646
|
+
`${compactConfigPath} moduleFederation.apps`,
|
|
647
|
+
'restore the complete generated Module Federation app cohort',
|
|
648
|
+
);
|
|
649
|
+
assertSameIdCohort(
|
|
650
|
+
ultramodernConfig.backendFederation?.apps,
|
|
651
|
+
workspaceValidationContract.cohort.backendAppIds,
|
|
652
|
+
`${compactConfigPath} backendFederation.apps`,
|
|
653
|
+
'restore the complete generated backend app cohort',
|
|
654
|
+
);
|
|
655
|
+
assertSameIdCohort(
|
|
656
|
+
topology.verticals,
|
|
657
|
+
workspaceValidationContract.cohort.verticalIds,
|
|
658
|
+
`${workspaceValidationContract.metadata.referenceTopology.path} verticals`,
|
|
659
|
+
'restore the complete generated vertical cohort',
|
|
660
|
+
);
|
|
661
|
+
assertSameIdCohort(
|
|
662
|
+
topology.sharedPackages,
|
|
663
|
+
workspaceValidationContract.cohort.sharedPackageIds,
|
|
664
|
+
`${workspaceValidationContract.metadata.referenceTopology.path} sharedPackages`,
|
|
665
|
+
'restore the complete generated shared package cohort',
|
|
666
|
+
);
|
|
667
|
+
assertSameIdCohort(
|
|
668
|
+
topology.shell?.moduleFederation?.remotes,
|
|
669
|
+
workspaceValidationContract.topology.referenceTopology.shell.moduleFederation.remotes.map(
|
|
670
|
+
remote => remote.id,
|
|
671
|
+
),
|
|
672
|
+
`${workspaceValidationContract.metadata.referenceTopology.path} shell.moduleFederation.remotes`,
|
|
673
|
+
'restore the complete generated shell remote cohort',
|
|
674
|
+
);
|
|
675
|
+
assertSameIdCohort(
|
|
676
|
+
ownership.owners,
|
|
677
|
+
workspaceValidationContract.cohort.ownerIds,
|
|
678
|
+
`${workspaceValidationContract.metadata.ownership.path} owners`,
|
|
679
|
+
'restore the complete generated ownership cohort',
|
|
680
|
+
);
|
|
681
|
+
|
|
682
|
+
for (const manifest of workspaceValidationContract.cohort.packageManifests) {
|
|
683
|
+
assertExists(manifest.path);
|
|
684
|
+
const packageJson = readJson(manifest.path);
|
|
685
|
+
assert(
|
|
686
|
+
packageJson.name === manifest.packageName,
|
|
687
|
+
`${manifest.path} package name must be ${manifest.packageName}`,
|
|
688
|
+
);
|
|
689
|
+
if (manifest.role === 'shell' || manifest.role === 'vertical') {
|
|
690
|
+
assert(
|
|
691
|
+
packageJson.modernjs?.appId === manifest.id,
|
|
692
|
+
`${manifest.path} modernjs.appId must be ${manifest.id}`,
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
if (expectedReleaseCohort) {
|
|
697
|
+
const releaseCohortContract = workspaceValidationContract.metadata.releaseCohort;
|
|
698
|
+
assertSelfCheck(
|
|
699
|
+
releaseCohortContract?.path === '.modernjs/release-cohort.json',
|
|
700
|
+
'authenticated release cohort projection',
|
|
701
|
+
'Expected release-cohort metadata path is missing or invalid',
|
|
702
|
+
'.modernjs/release-cohort.json',
|
|
703
|
+
);
|
|
704
|
+
assertSameJson(
|
|
705
|
+
readJson(releaseCohortContract.path),
|
|
706
|
+
expectedReleaseCohort,
|
|
707
|
+
'authenticated release cohort projection',
|
|
708
|
+
releaseCohortContract.path,
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
};
|
|
712
|
+
const assertStructuredWorkspaceMetadataSemantics = ({
|
|
713
|
+
ultramodernConfig,
|
|
714
|
+
topology,
|
|
715
|
+
ownership,
|
|
716
|
+
overlay,
|
|
717
|
+
}) => {
|
|
718
|
+
assertSameJson(
|
|
719
|
+
compactConfigPolicyView(ultramodernConfig),
|
|
720
|
+
workspaceValidationContract.policy.compactConfig,
|
|
721
|
+
`${compactConfigPath} policy`,
|
|
722
|
+
'restore generated compact workspace policy metadata',
|
|
723
|
+
);
|
|
724
|
+
assertSameJson(
|
|
725
|
+
ultramodernConfig.topology,
|
|
726
|
+
workspaceValidationContract.topology.compactConfig,
|
|
727
|
+
`${compactConfigPath} topology`,
|
|
728
|
+
'restore the complete generated compact topology cohort',
|
|
729
|
+
);
|
|
730
|
+
assertSameJson(
|
|
731
|
+
topology,
|
|
732
|
+
workspaceValidationContract.topology.referenceTopology,
|
|
733
|
+
workspaceValidationContract.metadata.referenceTopology.path,
|
|
734
|
+
'restore the complete generated reference topology',
|
|
735
|
+
);
|
|
736
|
+
assertSameJson(
|
|
737
|
+
ownership,
|
|
738
|
+
workspaceValidationContract.topology.ownership,
|
|
739
|
+
workspaceValidationContract.metadata.ownership.path,
|
|
740
|
+
'restore the complete generated ownership topology',
|
|
741
|
+
);
|
|
742
|
+
assertSameJson(
|
|
743
|
+
overlay,
|
|
744
|
+
workspaceValidationContract.topology.developmentOverlay,
|
|
745
|
+
workspaceValidationContract.metadata.developmentOverlay.path,
|
|
746
|
+
'restore the complete generated development topology',
|
|
747
|
+
);
|
|
748
|
+
};
|
|
87
749
|
const findById = (entries, id) =>
|
|
88
750
|
Array.isArray(entries) ? entries.find(entry => entry?.id === id) : undefined;
|
|
89
751
|
const generatedContractLabel = compactConfigPath;
|
|
@@ -136,6 +798,40 @@ const buildMarkerFor = app =>
|
|
|
136
798
|
.update(`${packageScope}:${app.packageSuffix}:${app.id}:0.1.0`)
|
|
137
799
|
.digest('hex')
|
|
138
800
|
.slice(0, 16);
|
|
801
|
+
const deliveryUnitIdentityFixArea =
|
|
802
|
+
'regenerate vertical identity from delivery-unit record; do not hand-edit surface markers';
|
|
803
|
+
const deliveryUnitBlock = record => ({
|
|
804
|
+
schemaVersion: record?.schemaVersion,
|
|
805
|
+
kind: record?.kind,
|
|
806
|
+
unitId: record?.unitId,
|
|
807
|
+
packageName: record?.packageName,
|
|
808
|
+
version: record?.version,
|
|
809
|
+
buildMarker: record?.buildMarker,
|
|
810
|
+
sourceRevision: record?.sourceRevision,
|
|
811
|
+
});
|
|
812
|
+
const expectedCompactAppFor = id =>
|
|
813
|
+
workspaceValidationContract.topology.compactConfig?.apps?.find(
|
|
814
|
+
entry => entry?.id === id,
|
|
815
|
+
);
|
|
816
|
+
const expectedDeliveryUnitFor = vertical => {
|
|
817
|
+
const expectedApp = expectedCompactAppFor(vertical.id);
|
|
818
|
+
return (
|
|
819
|
+
expectedApp?.backendFederation?.deliveryUnit ??
|
|
820
|
+
expectedApp?.deliveryUnit ??
|
|
821
|
+
vertical.deliveryUnit
|
|
822
|
+
);
|
|
823
|
+
};
|
|
824
|
+
const assertBuildFacadeExport = (source, exportName, sourcePath, contract) => {
|
|
825
|
+
const exportPattern = new RegExp(
|
|
826
|
+
`export const ${exportName} = ${sourcePath.replace(/\./gu, '\\.')};`,
|
|
827
|
+
);
|
|
828
|
+
assertSelfCheck(
|
|
829
|
+
exportPattern.test(source),
|
|
830
|
+
contract,
|
|
831
|
+
`${exportName} must re-export the canonical ultramodern-build.json artifact rather than hand-forking fields`,
|
|
832
|
+
deliveryUnitIdentityFixArea,
|
|
833
|
+
);
|
|
834
|
+
};
|
|
139
835
|
const normalizeCompactApp = rawApp => {
|
|
140
836
|
const id = String(rawApp.id);
|
|
141
837
|
const kind = rawApp.kind === 'vertical' ? 'vertical' : 'shell';
|
|
@@ -175,6 +871,10 @@ const normalizeCompactApp = rawApp => {
|
|
|
175
871
|
consumer => typeof consumer === 'string',
|
|
176
872
|
)
|
|
177
873
|
: ['shell-super-app', id],
|
|
874
|
+
// Preserve the API protocol so the synthesized generated contract can
|
|
875
|
+
// branch between the REST (`shared/api.ts` + `./api/client`) and RPC
|
|
876
|
+
// (`shared/rpc.ts` + `./api/rpc-client`) surfaces.
|
|
877
|
+
protocol: rawApp.api.protocol === 'rpc' ? 'rpc' : 'rest',
|
|
178
878
|
}
|
|
179
879
|
: undefined;
|
|
180
880
|
|
|
@@ -210,6 +910,14 @@ const normalizeCompactApp = rawApp => {
|
|
|
210
910
|
verticalRefs: Array.isArray(moduleFederation.verticalRefs)
|
|
211
911
|
? moduleFederation.verticalRefs.filter(ref => typeof ref === 'string')
|
|
212
912
|
: [],
|
|
913
|
+
deliveryUnit:
|
|
914
|
+
rawApp.deliveryUnit && typeof rawApp.deliveryUnit === 'object'
|
|
915
|
+
? rawApp.deliveryUnit
|
|
916
|
+
: undefined,
|
|
917
|
+
backendFederation:
|
|
918
|
+
rawApp.backendFederation && typeof rawApp.backendFederation === 'object'
|
|
919
|
+
? rawApp.backendFederation
|
|
920
|
+
: undefined,
|
|
213
921
|
api,
|
|
214
922
|
};
|
|
215
923
|
};
|
|
@@ -287,7 +995,7 @@ const createPublicSurface = app => {
|
|
|
287
995
|
metadataExport: './src/routes/ultramodern-route-metadata',
|
|
288
996
|
generator: 'scripts/generate-public-surface-assets.mts',
|
|
289
997
|
outputRoot: 'dist/public',
|
|
290
|
-
|
|
998
|
+
cloudflareBuildOutputRoot: 'dist-cloudflare/public',
|
|
291
999
|
privateRoutePolicy: 'omit-from-generated-public-surface',
|
|
292
1000
|
files:
|
|
293
1001
|
publicRoutes.length > 0
|
|
@@ -394,7 +1102,10 @@ const createCloudflareRoutes = app => ({
|
|
|
394
1102
|
ssr: '/en',
|
|
395
1103
|
mfManifest: '/mf-manifest.json',
|
|
396
1104
|
locale: `/locales/en/${appNamespace(app)}.json`,
|
|
397
|
-
|
|
1105
|
+
// Mirrors policy.ts:76 — the Cloudflare proof carries a REST readiness route
|
|
1106
|
+
// only for `rest` units. An `rpc` unit exposes its `/rpc` route instead and
|
|
1107
|
+
// must not emit (or require) a REST readiness path.
|
|
1108
|
+
...(app.api && app.api.protocol === 'rest'
|
|
398
1109
|
? {
|
|
399
1110
|
apiReadiness: `${app.api.prefix}/${app.api.stem}/readiness`,
|
|
400
1111
|
}
|
|
@@ -404,7 +1115,7 @@ const createCloudflareDeploy = app => ({
|
|
|
404
1115
|
target: 'cloudflare',
|
|
405
1116
|
workerName: expectedWorkerName(app.packageSuffix),
|
|
406
1117
|
publicUrlEnv: `ULTRAMODERN_PUBLIC_URL_${toEnvSegment(app.id)}`,
|
|
407
|
-
compatibilityDate:
|
|
1118
|
+
compatibilityDate: expectedCloudflareCompatibilityDate,
|
|
408
1119
|
compatibilityFlags: ['nodejs_compat', 'global_fetch_strictly_public'],
|
|
409
1120
|
assetsBinding: 'ASSETS',
|
|
410
1121
|
routes: createCloudflareRoutes(app),
|
|
@@ -486,7 +1197,7 @@ const createAppConfigContract = app => ({
|
|
|
486
1197
|
'zephyrRspackPlugin',
|
|
487
1198
|
],
|
|
488
1199
|
dev: {
|
|
489
|
-
assetPrefix: '/',
|
|
1200
|
+
assetPrefix: app.kind === 'shell' ? '/' : 'app-public-origin',
|
|
490
1201
|
},
|
|
491
1202
|
output: {
|
|
492
1203
|
assetPrefix: {
|
|
@@ -585,7 +1296,7 @@ const createAppContract = (app, apps) => ({
|
|
|
585
1296
|
target: 'cloudflare',
|
|
586
1297
|
cloudflare: createCloudflareDeploy(app),
|
|
587
1298
|
worker: {
|
|
588
|
-
compatibilityDate:
|
|
1299
|
+
compatibilityDate: expectedCloudflareCompatibilityDate,
|
|
589
1300
|
name: expectedWorkerName(app.packageSuffix),
|
|
590
1301
|
security: expectedCloudflareSecurity,
|
|
591
1302
|
ssr: true,
|
|
@@ -593,7 +1304,7 @@ const createAppContract = (app, apps) => ({
|
|
|
593
1304
|
},
|
|
594
1305
|
ssr: app.moduleFederationSsr
|
|
595
1306
|
? {
|
|
596
|
-
mode: '
|
|
1307
|
+
mode: 'stream',
|
|
597
1308
|
moduleFederationAppSSR: true,
|
|
598
1309
|
}
|
|
599
1310
|
: undefined,
|
|
@@ -625,37 +1336,55 @@ const createAppContract = (app, apps) => ({
|
|
|
625
1336
|
: {}),
|
|
626
1337
|
exposes: app.exposes,
|
|
627
1338
|
dts: {
|
|
628
|
-
compilerInstance: 'tsgo',
|
|
1339
|
+
compilerInstance: 'effect-tsgo',
|
|
629
1340
|
displayErrorInTerminal: true,
|
|
630
1341
|
tsConfigPath: './tsconfig.mf-types.json',
|
|
631
1342
|
},
|
|
632
1343
|
browserSafeExposesOnly: true,
|
|
633
1344
|
},
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
1345
|
+
marker: {
|
|
1346
|
+
appId: app.id,
|
|
1347
|
+
packageName: app.package ?? packageNameFor(packageScope, app.packageSuffix),
|
|
1348
|
+
version: '0.1.0',
|
|
1349
|
+
build: app.deliveryUnit?.buildMarker ?? buildMarkerFor(app),
|
|
1350
|
+
deployProfile: 'cloudflare-ssr-mf-effect-v1',
|
|
1351
|
+
uiSurface: 'ui',
|
|
1352
|
+
...(app.api ? { apiSurface: 'api' } : {}),
|
|
642
1353
|
},
|
|
643
1354
|
...(app.api
|
|
644
1355
|
? {
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
1356
|
+
// An `rpc` unit exposes the Effect RpcGroup contract/client and the
|
|
1357
|
+
// `/rpc` route instead of the REST `shared/api.ts` + `./api/client`
|
|
1358
|
+
// surface, so it carries no OpenAPI/readiness/domain-operation metadata.
|
|
1359
|
+
api:
|
|
1360
|
+
(app.api.protocol ?? 'rest') === 'rpc'
|
|
1361
|
+
? {
|
|
1362
|
+
runtime: 'effect',
|
|
1363
|
+
import: '@modern-js/plugin-bff/effect-edge',
|
|
1364
|
+
prefix: app.api.prefix,
|
|
1365
|
+
protocol: 'rpc',
|
|
1366
|
+
strictEffectApproach: true,
|
|
1367
|
+
workerEntry: 'worker/__modern_bff_effect.js',
|
|
1368
|
+
contract: './api',
|
|
1369
|
+
client: './api/rpc-client',
|
|
1370
|
+
group: toCamelCase(app.api.stem),
|
|
1371
|
+
rpc: { path: '/rpc', serialization: 'json' },
|
|
1372
|
+
rpcPath: `${app.api.prefix}/rpc`,
|
|
1373
|
+
}
|
|
1374
|
+
: {
|
|
1375
|
+
runtime: 'effect',
|
|
1376
|
+
import: '@modern-js/plugin-bff/effect-edge',
|
|
1377
|
+
prefix: app.api.prefix,
|
|
1378
|
+
openapi: '/openapi.json',
|
|
1379
|
+
strictEffectApproach: true,
|
|
1380
|
+
workerEntry: 'worker/__modern_bff_effect.js',
|
|
1381
|
+
contract: './api',
|
|
1382
|
+
client: './api/client',
|
|
1383
|
+
readiness: createEffectReadiness(app),
|
|
1384
|
+
requestContext: createEffectRequestContext(),
|
|
1385
|
+
domainOperations: createEffectDomainOperations(app),
|
|
1386
|
+
...createEffectOperationContract(app),
|
|
1387
|
+
},
|
|
659
1388
|
}
|
|
660
1389
|
: {}),
|
|
661
1390
|
});
|
|
@@ -720,13 +1449,26 @@ const createModernPackageAliases = packageSourceConfig => {
|
|
|
720
1449
|
]),
|
|
721
1450
|
);
|
|
722
1451
|
};
|
|
723
|
-
const
|
|
724
|
-
const source = config.packageSource
|
|
725
|
-
|
|
1452
|
+
const createPackageSourceView = config => {
|
|
1453
|
+
const source = config.packageSource;
|
|
1454
|
+
assert(
|
|
1455
|
+
source !== null && typeof source === 'object' && !Array.isArray(source),
|
|
1456
|
+
`${compactConfigPath} packageSource must be a JSON object`,
|
|
1457
|
+
);
|
|
1458
|
+
assert(
|
|
1459
|
+
source.strategy === 'workspace' || source.strategy === 'install',
|
|
1460
|
+
`${compactConfigPath} packageSource.strategy must be workspace or install`,
|
|
1461
|
+
);
|
|
1462
|
+
if (source.strategy === 'install') {
|
|
1463
|
+
assert(
|
|
1464
|
+
typeof source.modernPackageVersion === 'string' &&
|
|
1465
|
+
source.modernPackageVersion.length > 0,
|
|
1466
|
+
`${compactConfigPath} install package source must declare modernPackageVersion`,
|
|
1467
|
+
);
|
|
1468
|
+
}
|
|
1469
|
+
const strategy = source.strategy;
|
|
726
1470
|
const specifier =
|
|
727
|
-
strategy === 'install'
|
|
728
|
-
? source.modernPackageVersion
|
|
729
|
-
: 'workspace:*';
|
|
1471
|
+
strategy === 'install' ? source.modernPackageVersion : 'workspace:*';
|
|
730
1472
|
const aliases = createModernPackageAliases(source);
|
|
731
1473
|
return {
|
|
732
1474
|
schemaVersion: 1,
|
|
@@ -762,16 +1504,124 @@ const synthesizeGeneratedContractFromCompact = config => {
|
|
|
762
1504
|
apps: apps.map(app => createAppContract(app, apps)),
|
|
763
1505
|
};
|
|
764
1506
|
};
|
|
765
|
-
const readPackageSourceView = config => {
|
|
766
|
-
return synthesizePackageSourceFromCompact(config);
|
|
767
|
-
};
|
|
768
1507
|
const readGeneratedContractView = config => {
|
|
769
1508
|
return synthesizeGeneratedContractFromCompact(config);
|
|
770
1509
|
};
|
|
771
1510
|
const expectedManifestUrl = vertical =>
|
|
772
1511
|
`http://localhost:${vertical.port}/mf-manifest.json`;
|
|
773
1512
|
const expectedApiUrl = vertical =>
|
|
774
|
-
`http://localhost:${vertical.port}${vertical.apiPrefix}
|
|
1513
|
+
`http://localhost:${vertical.port}${vertical.apiPrefix}${
|
|
1514
|
+
vertical.apiProtocol === 'rpc' ? '/rpc' : ''
|
|
1515
|
+
}`;
|
|
1516
|
+
const expectedBackendFederationName = vertical => `${vertical.mfName}Backend`;
|
|
1517
|
+
const expectedBackendManifestUrl = vertical =>
|
|
1518
|
+
`http://localhost:${vertical.port}/backend-mf-manifest.json`;
|
|
1519
|
+
const expectedBackendContainerEntry = vertical =>
|
|
1520
|
+
`http://localhost:${vertical.port}/backendRemoteEntry.cjs`;
|
|
1521
|
+
const expectedBackendManifestEnv = vertical =>
|
|
1522
|
+
`VERTICAL_${toEnvSegment(vertical.domain ?? vertical.id)}_BACKEND_MF_MANIFEST`;
|
|
1523
|
+
const expectedPublicUrlEnv = vertical =>
|
|
1524
|
+
`ULTRAMODERN_PUBLIC_URL_${toEnvSegment(vertical.id)}`;
|
|
1525
|
+
const expectedCloudflareWorkerName = vertical =>
|
|
1526
|
+
toKebabCase(`${packageScope}-${vertical.packageSuffix ?? vertical.id}`).slice(
|
|
1527
|
+
0,
|
|
1528
|
+
63,
|
|
1529
|
+
);
|
|
1530
|
+
const backendFederationSubset = backendFederation => ({
|
|
1531
|
+
role: backendFederation?.role,
|
|
1532
|
+
name: backendFederation?.name,
|
|
1533
|
+
runtimeFramework: backendFederation?.runtimeFramework,
|
|
1534
|
+
strictEffectApproach: backendFederation?.strictEffectApproach,
|
|
1535
|
+
exposeRuntime: backendFederation?.exposes?.['./effect-api']?.runtime,
|
|
1536
|
+
exposeReadiness: backendFederation?.exposes?.['./effect-api']?.readiness,
|
|
1537
|
+
versionBoundary: {
|
|
1538
|
+
invariant: backendFederation?.versionBoundary?.invariant,
|
|
1539
|
+
uiManifestUrl: backendFederation?.versionBoundary?.ui?.manifestUrl,
|
|
1540
|
+
apiReadiness: backendFederation?.versionBoundary?.api?.readiness,
|
|
1541
|
+
},
|
|
1542
|
+
cloudflare: {
|
|
1543
|
+
kind: backendFederation?.executionSurfaces?.cloudflare?.kind,
|
|
1544
|
+
workerName: backendFederation?.executionSurfaces?.cloudflare?.workerName,
|
|
1545
|
+
publicUrlEnv:
|
|
1546
|
+
backendFederation?.executionSurfaces?.cloudflare?.publicUrlEnv,
|
|
1547
|
+
zephyrRuntime:
|
|
1548
|
+
backendFederation?.executionSurfaces?.cloudflare?.zephyr?.runtime,
|
|
1549
|
+
},
|
|
1550
|
+
node: {
|
|
1551
|
+
kind: backendFederation?.executionSurfaces?.node?.kind,
|
|
1552
|
+
remoteName: backendFederation?.executionSurfaces?.node?.remoteName,
|
|
1553
|
+
manifestEnv: backendFederation?.executionSurfaces?.node?.manifestEnv,
|
|
1554
|
+
manifestUrl: backendFederation?.executionSurfaces?.node?.manifestUrl,
|
|
1555
|
+
containerEntry:
|
|
1556
|
+
backendFederation?.executionSurfaces?.node?.containerEntry,
|
|
1557
|
+
remoteType: backendFederation?.executionSurfaces?.node?.remoteType,
|
|
1558
|
+
expose: backendFederation?.executionSurfaces?.node?.expose,
|
|
1559
|
+
},
|
|
1560
|
+
compatibility: {
|
|
1561
|
+
contractVersion: backendFederation?.compatibility?.contractVersion,
|
|
1562
|
+
},
|
|
1563
|
+
topLevelManifestUrl: backendFederation?.manifestUrl,
|
|
1564
|
+
topLevelContainerEntry: backendFederation?.containerEntry,
|
|
1565
|
+
});
|
|
1566
|
+
const expectedBackendFederationSubset = vertical => ({
|
|
1567
|
+
role: 'microvertical-server',
|
|
1568
|
+
name: expectedBackendFederationName(vertical),
|
|
1569
|
+
runtimeFramework: 'effect',
|
|
1570
|
+
strictEffectApproach: true,
|
|
1571
|
+
exposeRuntime: `${vertical.path}/api/index.ts`,
|
|
1572
|
+
// The RPC surface exposes no REST readiness endpoint, so its backend
|
|
1573
|
+
// federation contract omits the readiness probes (G7a).
|
|
1574
|
+
exposeReadiness:
|
|
1575
|
+
vertical.apiProtocol === 'rpc'
|
|
1576
|
+
? undefined
|
|
1577
|
+
: `${vertical.apiPrefix}/${vertical.stem}/readiness`,
|
|
1578
|
+
versionBoundary: {
|
|
1579
|
+
invariant: 'web-and-api-same-build',
|
|
1580
|
+
uiManifestUrl: expectedManifestUrl(vertical),
|
|
1581
|
+
apiReadiness:
|
|
1582
|
+
vertical.apiProtocol === 'rpc'
|
|
1583
|
+
? undefined
|
|
1584
|
+
: `${vertical.apiPrefix}/${vertical.stem}/readiness`,
|
|
1585
|
+
},
|
|
1586
|
+
cloudflare: {
|
|
1587
|
+
kind: 'cloudflare-worker-snapshot',
|
|
1588
|
+
workerName: expectedCloudflareWorkerName(vertical),
|
|
1589
|
+
publicUrlEnv: expectedPublicUrlEnv(vertical),
|
|
1590
|
+
zephyrRuntime: 'ssr-worker',
|
|
1591
|
+
},
|
|
1592
|
+
node: {
|
|
1593
|
+
kind: 'node-mf-runtime',
|
|
1594
|
+
remoteName: expectedBackendFederationName(vertical),
|
|
1595
|
+
manifestEnv: expectedBackendManifestEnv(vertical),
|
|
1596
|
+
manifestUrl: expectedBackendManifestUrl(vertical),
|
|
1597
|
+
containerEntry: expectedBackendContainerEntry(vertical),
|
|
1598
|
+
remoteType: 'commonjs-module',
|
|
1599
|
+
expose: './effect-api',
|
|
1600
|
+
},
|
|
1601
|
+
compatibility: {
|
|
1602
|
+
contractVersion: 'microvertical-server-effect-v1',
|
|
1603
|
+
},
|
|
1604
|
+
topLevelManifestUrl: undefined,
|
|
1605
|
+
topLevelContainerEntry: undefined,
|
|
1606
|
+
});
|
|
1607
|
+
const serverExecutionSubset = serverExecution => ({
|
|
1608
|
+
apiBaseUrl: serverExecution?.apiBaseUrl,
|
|
1609
|
+
versionBoundary: serverExecution?.versionBoundary,
|
|
1610
|
+
cloudflareKind: serverExecution?.cloudflare?.kind,
|
|
1611
|
+
cloudflareWorkerName: serverExecution?.cloudflare?.workerName,
|
|
1612
|
+
nodeKind: serverExecution?.node?.kind,
|
|
1613
|
+
nodeManifestUrl: serverExecution?.node?.manifestUrl,
|
|
1614
|
+
nodeContainerEntry: serverExecution?.node?.containerEntry,
|
|
1615
|
+
});
|
|
1616
|
+
const expectedServerExecutionSubset = vertical => ({
|
|
1617
|
+
apiBaseUrl: expectedApiUrl(vertical),
|
|
1618
|
+
versionBoundary: 'web-and-api-same-build',
|
|
1619
|
+
cloudflareKind: 'cloudflare-worker-snapshot',
|
|
1620
|
+
cloudflareWorkerName: expectedCloudflareWorkerName(vertical),
|
|
1621
|
+
nodeKind: 'node-mf-runtime',
|
|
1622
|
+
nodeManifestUrl: expectedBackendManifestUrl(vertical),
|
|
1623
|
+
nodeContainerEntry: expectedBackendContainerEntry(vertical),
|
|
1624
|
+
});
|
|
775
1625
|
const remoteContractSubset = remote => ({
|
|
776
1626
|
id: remote?.id,
|
|
777
1627
|
name: remote?.name,
|
|
@@ -792,25 +1642,78 @@ const requiredMicroVerticalPaths = vertical => [
|
|
|
792
1642
|
`${vertical.path}/tsconfig.json`,
|
|
793
1643
|
`${vertical.path}/tsconfig.mf-types.json`,
|
|
794
1644
|
`${vertical.path}/modern.config.ts`,
|
|
795
|
-
`${vertical.path}/module-federation.config.ts`,
|
|
796
|
-
`${vertical.path}/api/index.ts`,
|
|
797
|
-
`${vertical.path}/shared/api.ts`,
|
|
798
|
-
`${vertical.path}/src/api/${vertical.stem}-client.ts`,
|
|
799
1645
|
`${vertical.path}/src/modern-app-env.d.ts`,
|
|
800
1646
|
`${vertical.path}/src/modern.runtime.ts`,
|
|
801
|
-
`${vertical.path}/src/federation-entry.tsx`,
|
|
802
|
-
...vertical.componentPaths,
|
|
803
1647
|
`${vertical.path}/locales/en/translation.json`,
|
|
804
1648
|
`${vertical.path}/locales/en/${vertical.namespace}.json`,
|
|
805
1649
|
`${vertical.path}/locales/cs/translation.json`,
|
|
806
1650
|
`${vertical.path}/locales/cs/${vertical.namespace}.json`,
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
1651
|
+
...(vertical.emitsUi
|
|
1652
|
+
? [
|
|
1653
|
+
`${vertical.path}/module-federation.config.ts`,
|
|
1654
|
+
`${vertical.path}/src/federation-entry.tsx`,
|
|
1655
|
+
...vertical.componentPaths,
|
|
1656
|
+
`${vertical.path}/src/routes/index.css`,
|
|
1657
|
+
`${vertical.path}/src/routes/layout.tsx`,
|
|
1658
|
+
`${vertical.path}/src/routes/ultramodern-route-head.tsx`,
|
|
1659
|
+
`${vertical.path}/src/routes/ultramodern-route-metadata.ts`,
|
|
1660
|
+
`${vertical.path}/src/routes/[lang]/page.tsx`,
|
|
1661
|
+
...(vertical.exposes.includes('./Widget')
|
|
1662
|
+
? [
|
|
1663
|
+
`${vertical.path}/src/routes/[lang]/_mf/fragment/widget/page.tsx`,
|
|
1664
|
+
]
|
|
1665
|
+
: []),
|
|
1666
|
+
...vertical.routePagePaths,
|
|
1667
|
+
...vertical.routeMetaPaths,
|
|
1668
|
+
]
|
|
1669
|
+
: []),
|
|
1670
|
+
...(vertical.emitsApi
|
|
1671
|
+
? [
|
|
1672
|
+
`${vertical.path}/backend-federation.config.ts`,
|
|
1673
|
+
`${vertical.path}/api/effect-api.ts`,
|
|
1674
|
+
`${vertical.path}/api/index.ts`,
|
|
1675
|
+
`${vertical.path}/${vertical.apiContractPath}`,
|
|
1676
|
+
`${vertical.path}/${vertical.apiClientPath}`,
|
|
1677
|
+
]
|
|
1678
|
+
: []),
|
|
1679
|
+
];
|
|
1680
|
+
// UI/MF artifacts an `api-only` unit must NOT emit (headless invariant), and
|
|
1681
|
+
// API/BFF artifacts a `ui-only`/Horizontal Remote unit must NOT emit.
|
|
1682
|
+
const forbiddenMicroVerticalPaths = vertical => [
|
|
1683
|
+
...(vertical.emitsUi
|
|
1684
|
+
? []
|
|
1685
|
+
: [
|
|
1686
|
+
`${vertical.path}/module-federation.config.ts`,
|
|
1687
|
+
`${vertical.path}/src/federation-entry.tsx`,
|
|
1688
|
+
// The federated demo `./Widget` component (descriptors.ts:127 ->
|
|
1689
|
+
// remoteComponentOutputPath) is a UI-only artifact; a headless api-only
|
|
1690
|
+
// unit exposes no browser component and must not ship it.
|
|
1691
|
+
`${vertical.path}/src/components/${vertical.domain ?? vertical.id}-widget.tsx`,
|
|
1692
|
+
`${vertical.path}/src/routes/layout.tsx`,
|
|
1693
|
+
`${vertical.path}/src/routes/[lang]/page.tsx`,
|
|
1694
|
+
`${vertical.path}/src/routes/[lang]/_mf/fragment/widget/page.tsx`,
|
|
1695
|
+
// A headless api-only unit renders no browser surface, so it must not
|
|
1696
|
+
// ship route components, the colocated route-metadata/head modules, or
|
|
1697
|
+
// the colocated `[lang]/route.meta.ts` route-meta file.
|
|
1698
|
+
`${vertical.path}/src/routes/[lang]/route.meta.ts`,
|
|
1699
|
+
`${vertical.path}/src/routes/ultramodern-route-head.tsx`,
|
|
1700
|
+
`${vertical.path}/src/routes/ultramodern-route-metadata.ts`,
|
|
1701
|
+
`${vertical.path}/src/routes/index.css`,
|
|
1702
|
+
]),
|
|
1703
|
+
...(vertical.emitsApi
|
|
1704
|
+
? []
|
|
1705
|
+
: [
|
|
1706
|
+
`${vertical.path}/shared/api.ts`,
|
|
1707
|
+
// A ui-only/Horizontal Remote unit carries no API contract in either
|
|
1708
|
+
// protocol (neither the REST `shared/api.ts` nor the RPC `shared/rpc.ts`)
|
|
1709
|
+
// and no generated API client.
|
|
1710
|
+
`${vertical.path}/shared/rpc.ts`,
|
|
1711
|
+
`${vertical.path}/backend-federation.config.ts`,
|
|
1712
|
+
`${vertical.path}/api/index.ts`,
|
|
1713
|
+
`${vertical.path}/api/effect-api.ts`,
|
|
1714
|
+
`${vertical.path}/src/api/${vertical.domain ?? vertical.id}-client.ts`,
|
|
1715
|
+
`${vertical.path}/src/api/${vertical.domain ?? vertical.id}-rpc-client.ts`,
|
|
1716
|
+
]),
|
|
814
1717
|
];
|
|
815
1718
|
const assertRequiredVerticalFile = vertical => relativePath => {
|
|
816
1719
|
assertSelfCheck(
|
|
@@ -820,6 +1723,14 @@ const assertRequiredVerticalFile = vertical => relativePath => {
|
|
|
820
1723
|
'restore the generated MicroVertical files or rerun the MicroVertical generator',
|
|
821
1724
|
);
|
|
822
1725
|
};
|
|
1726
|
+
const assertForbiddenVerticalFile = vertical => relativePath => {
|
|
1727
|
+
assertSelfCheck(
|
|
1728
|
+
!fs.existsSync(path.join(root, relativePath)),
|
|
1729
|
+
`forbidden files for ${vertical.id}`,
|
|
1730
|
+
`Unexpected ${relativePath} for a ${vertical.surfaceProfile} unit`,
|
|
1731
|
+
`remove ${relativePath}; a ${vertical.surfaceProfile} unit does not emit this surface`,
|
|
1732
|
+
);
|
|
1733
|
+
};
|
|
823
1734
|
const assertMicroVerticalContractGraph = ({
|
|
824
1735
|
generatedContract,
|
|
825
1736
|
topology,
|
|
@@ -829,8 +1740,16 @@ const assertMicroVerticalContractGraph = ({
|
|
|
829
1740
|
}) => {
|
|
830
1741
|
const expectedVerticalIds = fullStackVerticals.map(vertical => vertical.id);
|
|
831
1742
|
const expectedAppIds = ['shell-super-app', ...expectedVerticalIds];
|
|
832
|
-
const
|
|
833
|
-
|
|
1743
|
+
const expectedShellVerticalIds = expectedPrimaryShellVerticalIds;
|
|
1744
|
+
const expectedShellRemotes = expectedShellVerticalIds.flatMap(
|
|
1745
|
+
verticalId => {
|
|
1746
|
+
const vertical = fullStackVerticals.find(
|
|
1747
|
+
candidate => candidate.id === verticalId,
|
|
1748
|
+
);
|
|
1749
|
+
return vertical === undefined
|
|
1750
|
+
? []
|
|
1751
|
+
: [expectedRemoteContractSubset(vertical)];
|
|
1752
|
+
},
|
|
834
1753
|
);
|
|
835
1754
|
|
|
836
1755
|
assertObject(
|
|
@@ -881,7 +1800,7 @@ const assertMicroVerticalContractGraph = ({
|
|
|
881
1800
|
|
|
882
1801
|
assertSameJson(
|
|
883
1802
|
topology.shell.verticalRefs ?? [],
|
|
884
|
-
|
|
1803
|
+
expectedShellVerticalIds,
|
|
885
1804
|
'topology/reference-topology.json shell.verticalRefs',
|
|
886
1805
|
'restore generated topology shell references',
|
|
887
1806
|
);
|
|
@@ -912,7 +1831,7 @@ const assertMicroVerticalContractGraph = ({
|
|
|
912
1831
|
);
|
|
913
1832
|
assertSameJson(
|
|
914
1833
|
shellContract.moduleFederation?.verticalRefs ?? [],
|
|
915
|
-
|
|
1834
|
+
expectedShellVerticalIds,
|
|
916
1835
|
`${generatedContractLabel} shell moduleFederation.verticalRefs`,
|
|
917
1836
|
'regenerate the generated shell Module Federation contract',
|
|
918
1837
|
);
|
|
@@ -925,11 +1844,11 @@ const assertMicroVerticalContractGraph = ({
|
|
|
925
1844
|
assertSameJson(
|
|
926
1845
|
shellContract.ssr,
|
|
927
1846
|
{
|
|
928
|
-
mode: '
|
|
1847
|
+
mode: 'stream',
|
|
929
1848
|
moduleFederationAppSSR: true,
|
|
930
1849
|
},
|
|
931
1850
|
`${generatedContractLabel} shell SSR contract`,
|
|
932
|
-
'restore generated
|
|
1851
|
+
'restore generated streaming SSR Module Federation settings',
|
|
933
1852
|
);
|
|
934
1853
|
|
|
935
1854
|
for (const vertical of fullStackVerticals) {
|
|
@@ -964,10 +1883,14 @@ const assertMicroVerticalContractGraph = ({
|
|
|
964
1883
|
remoteContractSubset,
|
|
965
1884
|
),
|
|
966
1885
|
},
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
1886
|
+
...(vertical.emitsApi
|
|
1887
|
+
? {
|
|
1888
|
+
api: {
|
|
1889
|
+
prefix: topologyEntry.api?.bff?.prefix,
|
|
1890
|
+
serverEntry: topologyEntry.api?.serverEntry,
|
|
1891
|
+
},
|
|
1892
|
+
}
|
|
1893
|
+
: {}),
|
|
971
1894
|
},
|
|
972
1895
|
{
|
|
973
1896
|
kind: 'vertical',
|
|
@@ -980,15 +1903,71 @@ const assertMicroVerticalContractGraph = ({
|
|
|
980
1903
|
verticalRefs: expectedRefs,
|
|
981
1904
|
remotes: expectedRemoteSubsetsForRefs(expectedRefs),
|
|
982
1905
|
},
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1906
|
+
...(vertical.emitsApi
|
|
1907
|
+
? {
|
|
1908
|
+
api: {
|
|
1909
|
+
prefix: vertical.apiPrefix,
|
|
1910
|
+
serverEntry: `${vertical.path}/api/index.ts`,
|
|
1911
|
+
},
|
|
1912
|
+
}
|
|
1913
|
+
: {}),
|
|
987
1914
|
},
|
|
988
1915
|
`topology/reference-topology.json verticals.${vertical.id}`,
|
|
989
1916
|
'restore generated topology vertical entries',
|
|
990
1917
|
);
|
|
991
|
-
|
|
1918
|
+
if (vertical.emitsApi) {
|
|
1919
|
+
assertSameJson(
|
|
1920
|
+
backendFederationSubset(topologyEntry.backendFederation),
|
|
1921
|
+
expectedBackendFederationSubset(vertical),
|
|
1922
|
+
`topology/reference-topology.json verticals.${vertical.id}.backendFederation`,
|
|
1923
|
+
'restore generated MicroVertical server execution contract',
|
|
1924
|
+
);
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
if (vertical.deliveryUnit) {
|
|
1928
|
+
const compactApp = findById(
|
|
1929
|
+
ultramodernConfig.topology?.apps,
|
|
1930
|
+
vertical.id,
|
|
1931
|
+
);
|
|
1932
|
+
const expectedDeliveryUnit = deliveryUnitBlock(
|
|
1933
|
+
expectedDeliveryUnitFor(vertical),
|
|
1934
|
+
);
|
|
1935
|
+
assertSameJson(
|
|
1936
|
+
deliveryUnitBlock(compactApp?.deliveryUnit),
|
|
1937
|
+
expectedDeliveryUnit,
|
|
1938
|
+
`${generatedContractLabel} topology.apps.${vertical.id}.deliveryUnit`,
|
|
1939
|
+
deliveryUnitIdentityFixArea,
|
|
1940
|
+
);
|
|
1941
|
+
if (vertical.emitsApi) {
|
|
1942
|
+
assertSameJson(
|
|
1943
|
+
deliveryUnitBlock(compactApp?.backendFederation?.deliveryUnit),
|
|
1944
|
+
expectedDeliveryUnit,
|
|
1945
|
+
`${generatedContractLabel} topology.apps.${vertical.id}.backendFederation.deliveryUnit`,
|
|
1946
|
+
deliveryUnitIdentityFixArea,
|
|
1947
|
+
);
|
|
1948
|
+
}
|
|
1949
|
+
assertSameJson(
|
|
1950
|
+
deliveryUnitBlock(topologyEntry.deliveryUnit),
|
|
1951
|
+
expectedDeliveryUnit,
|
|
1952
|
+
`topology/reference-topology.json verticals.${vertical.id}.deliveryUnit`,
|
|
1953
|
+
deliveryUnitIdentityFixArea,
|
|
1954
|
+
);
|
|
1955
|
+
if (vertical.emitsApi) {
|
|
1956
|
+
assertSameJson(
|
|
1957
|
+
deliveryUnitBlock(topologyEntry.backendFederation?.deliveryUnit),
|
|
1958
|
+
expectedDeliveryUnit,
|
|
1959
|
+
`topology/reference-topology.json verticals.${vertical.id}.backendFederation.deliveryUnit`,
|
|
1960
|
+
deliveryUnitIdentityFixArea,
|
|
1961
|
+
);
|
|
1962
|
+
assertSelfCheck(
|
|
1963
|
+
topologyEntry.backendFederation?.versionBoundary?.identityRoot === 'deliveryUnit',
|
|
1964
|
+
`topology/reference-topology.json verticals.${vertical.id}.backendFederation.versionBoundary.identityRoot`,
|
|
1965
|
+
`Expected "deliveryUnit", found ${formatJson(topologyEntry.backendFederation?.versionBoundary?.identityRoot)}`,
|
|
1966
|
+
deliveryUnitIdentityFixArea,
|
|
1967
|
+
);
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
|
|
992
1971
|
assertObject(
|
|
993
1972
|
ownershipEntry,
|
|
994
1973
|
`topology/ownership.json owners.${vertical.id}`,
|
|
@@ -1013,31 +1992,67 @@ const assertMicroVerticalContractGraph = ({
|
|
|
1013
1992
|
`topology/local-overlays/development.json ports.${vertical.id}`,
|
|
1014
1993
|
'restore generated local development port overlay',
|
|
1015
1994
|
);
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1995
|
+
if (vertical.emitsUi) {
|
|
1996
|
+
assertSameJson(
|
|
1997
|
+
overlay.manifests[vertical.id],
|
|
1998
|
+
expectedManifestUrl(vertical),
|
|
1999
|
+
`topology/local-overlays/development.json manifests.${vertical.id}`,
|
|
2000
|
+
'restore generated local Module Federation manifest overlay',
|
|
2001
|
+
);
|
|
2002
|
+
}
|
|
2003
|
+
if (vertical.emitsApi) {
|
|
2004
|
+
assertSameJson(
|
|
2005
|
+
overlay.apis[vertical.id],
|
|
2006
|
+
expectedApiUrl(vertical),
|
|
2007
|
+
`topology/local-overlays/development.json apis.${vertical.id}`,
|
|
2008
|
+
'restore generated local API overlay',
|
|
2009
|
+
);
|
|
2010
|
+
assertSameJson(
|
|
2011
|
+
serverExecutionSubset(overlay.serverExecution?.[vertical.id]),
|
|
2012
|
+
expectedServerExecutionSubset(vertical),
|
|
2013
|
+
`topology/local-overlays/development.json serverExecution.${vertical.id}`,
|
|
2014
|
+
'restore generated local MicroVertical server execution overlay',
|
|
2015
|
+
);
|
|
2016
|
+
}
|
|
1028
2017
|
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
2018
|
+
|
|
2019
|
+
// Plain workspace dependencies and Zephyr metadata are gated
|
|
2020
|
+
// INDEPENDENTLY (G2a/G28): every shell depends on each API-emitting
|
|
2021
|
+
// vertical (its vertical-clients.ts re-exports the client) regardless of
|
|
2022
|
+
// UI composition, while Zephyr metadata follows UI composition refs only.
|
|
2023
|
+
const shellPackagesForDependencyChecks = [
|
|
2024
|
+
{
|
|
2025
|
+
id: 'shell-super-app',
|
|
2026
|
+
path: 'apps/shell-super-app',
|
|
2027
|
+
pkg: shellPackage,
|
|
2028
|
+
uiRefs: expectedShellVerticalIds,
|
|
2029
|
+
},
|
|
2030
|
+
...expectedAdditionalShells.map(additionalShell => ({
|
|
2031
|
+
id: additionalShell.id,
|
|
2032
|
+
path: additionalShell.path,
|
|
2033
|
+
pkg: readJson(`${additionalShell.path}/package.json`),
|
|
2034
|
+
uiRefs: additionalShell.verticalRefs ?? [],
|
|
2035
|
+
})),
|
|
2036
|
+
];
|
|
2037
|
+
for (const shellEntry of shellPackagesForDependencyChecks) {
|
|
2038
|
+
const composed = shellEntry.uiRefs.includes(vertical.id);
|
|
2039
|
+
if (vertical.emitsApi || composed) {
|
|
2040
|
+
assertSameJson(
|
|
2041
|
+
shellEntry.pkg.dependencies?.[vertical.packageName],
|
|
2042
|
+
'workspace:*',
|
|
2043
|
+
`${shellEntry.path}/package.json dependencies.${vertical.packageName}`,
|
|
2044
|
+
'restore shell dependency for the MicroVertical consumer',
|
|
2045
|
+
);
|
|
2046
|
+
}
|
|
2047
|
+
if (composed) {
|
|
2048
|
+
assertSameJson(
|
|
2049
|
+
shellEntry.pkg['zephyr:dependencies']?.[vertical.zephyrAlias],
|
|
2050
|
+
`${vertical.packageName}@workspace:*`,
|
|
2051
|
+
`${shellEntry.path}/package.json zephyr:dependencies.${vertical.zephyrAlias}`,
|
|
2052
|
+
'restore shell Zephyr dependency metadata for the MicroVertical',
|
|
2053
|
+
);
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
1041
2056
|
|
|
1042
2057
|
assertObject(
|
|
1043
2058
|
contractEntry,
|
|
@@ -1057,11 +2072,15 @@ const assertMicroVerticalContractGraph = ({
|
|
|
1057
2072
|
remoteContractSubset,
|
|
1058
2073
|
),
|
|
1059
2074
|
},
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
2075
|
+
...(vertical.emitsApi
|
|
2076
|
+
? {
|
|
2077
|
+
api: {
|
|
2078
|
+
prefix: contractEntry.api?.prefix,
|
|
2079
|
+
contract: contractEntry.api?.contract,
|
|
2080
|
+
client: contractEntry.api?.client,
|
|
2081
|
+
},
|
|
2082
|
+
}
|
|
2083
|
+
: {}),
|
|
1065
2084
|
ssr: contractEntry.ssr,
|
|
1066
2085
|
},
|
|
1067
2086
|
{
|
|
@@ -1074,13 +2093,19 @@ const assertMicroVerticalContractGraph = ({
|
|
|
1074
2093
|
verticalRefs: expectedRefs,
|
|
1075
2094
|
remotes: expectedRemoteSubsetsForRefs(expectedRefs),
|
|
1076
2095
|
},
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
2096
|
+
...(vertical.emitsApi
|
|
2097
|
+
? {
|
|
2098
|
+
api: {
|
|
2099
|
+
prefix: vertical.apiPrefix,
|
|
2100
|
+
// Protocol-aware exports: `rest` -> `./api`/`./api/client`,
|
|
2101
|
+
// `rpc` -> `./api`/`./api/rpc-client`.
|
|
2102
|
+
contract: vertical.apiContractExport,
|
|
2103
|
+
client: vertical.apiClientExport,
|
|
2104
|
+
},
|
|
2105
|
+
}
|
|
2106
|
+
: {}),
|
|
1082
2107
|
ssr: {
|
|
1083
|
-
mode: '
|
|
2108
|
+
mode: 'stream',
|
|
1084
2109
|
moduleFederationAppSSR: true,
|
|
1085
2110
|
},
|
|
1086
2111
|
},
|
|
@@ -1116,19 +2141,34 @@ const assertProjectReferenceEmitConfig = (tsConfig, packagePath) => {
|
|
|
1116
2141
|
);
|
|
1117
2142
|
};
|
|
1118
2143
|
const assertTsConfigReferenceGraph = () => {
|
|
2144
|
+
const baseTsConfig = readJson('tsconfig.base.json');
|
|
1119
2145
|
const rootTsConfig = readJson('tsconfig.json');
|
|
1120
2146
|
const shellTsConfig = readJson('apps/shell-super-app/tsconfig.json');
|
|
1121
2147
|
const shellMfTypesTsConfig = readJson(
|
|
1122
2148
|
'apps/shell-super-app/tsconfig.mf-types.json',
|
|
1123
2149
|
);
|
|
2150
|
+
const additionalShellPaths = (
|
|
2151
|
+
workspaceValidationContract.structuralShellPolicy?.shells ?? []
|
|
2152
|
+
)
|
|
2153
|
+
.filter(shell => shell.id !== 'shell-super-app')
|
|
2154
|
+
.map(shell => shell.packageDir);
|
|
1124
2155
|
const expectedRootReferences = [
|
|
1125
2156
|
...sharedPackagePaths,
|
|
1126
2157
|
'apps/shell-super-app',
|
|
1127
2158
|
...fullStackVerticals.map(vertical => vertical.path),
|
|
2159
|
+
...additionalShellPaths,
|
|
1128
2160
|
].map(referencePath => ({ path: referencePath }));
|
|
1129
2161
|
const expectedShellReferences = [
|
|
1130
2162
|
...sharedPackagePaths,
|
|
1131
|
-
...
|
|
2163
|
+
...(topology.shell?.verticalRefs ?? [])
|
|
2164
|
+
.map(verticalRef =>
|
|
2165
|
+
fullStackVerticals.find(candidate => candidate.id === verticalRef),
|
|
2166
|
+
)
|
|
2167
|
+
.filter(Boolean)
|
|
2168
|
+
// The shell only project-references verticals whose API client types it
|
|
2169
|
+
// imports; UI-only remotes are federated at runtime, not type-referenced.
|
|
2170
|
+
.filter(vertical => vertical.emitsApi)
|
|
2171
|
+
.map(vertical => vertical.path),
|
|
1132
2172
|
].map(referencePath =>
|
|
1133
2173
|
referenceFrom('apps/shell-super-app', referencePath),
|
|
1134
2174
|
);
|
|
@@ -1151,6 +2191,13 @@ const assertTsConfigReferenceGraph = () => {
|
|
|
1151
2191
|
'apps/shell-super-app/tsconfig.json references',
|
|
1152
2192
|
'restore the generated shell project-reference graph',
|
|
1153
2193
|
);
|
|
2194
|
+
assert(baseTsConfig.compilerOptions?.skipLibCheck !== true, 'tsconfig.base.json must not use skipLibCheck');
|
|
2195
|
+
assertSameJson(
|
|
2196
|
+
shellTsConfig.include ?? [],
|
|
2197
|
+
['src', 'locales/**/*.json', 'package.json', 'shared'],
|
|
2198
|
+
'apps/shell-super-app/tsconfig.json include',
|
|
2199
|
+
'restore the generated shell typecheck boundary',
|
|
2200
|
+
);
|
|
1154
2201
|
assertProjectReferenceEmitConfig(
|
|
1155
2202
|
shellTsConfig,
|
|
1156
2203
|
'apps/shell-super-app',
|
|
@@ -1158,7 +2205,7 @@ const assertTsConfigReferenceGraph = () => {
|
|
|
1158
2205
|
assertSameJson(
|
|
1159
2206
|
shellMfTypesTsConfig,
|
|
1160
2207
|
{
|
|
1161
|
-
extends: '
|
|
2208
|
+
extends: '../../tsconfig.base.json',
|
|
1162
2209
|
include: ['src/modern-app-env.d.ts'],
|
|
1163
2210
|
},
|
|
1164
2211
|
'apps/shell-super-app/tsconfig.mf-types.json',
|
|
@@ -1191,23 +2238,77 @@ const assertTsConfigReferenceGraph = () => {
|
|
|
1191
2238
|
`${vertical.path}/tsconfig.json references`,
|
|
1192
2239
|
'restore the generated MicroVertical project-reference graph',
|
|
1193
2240
|
);
|
|
2241
|
+
assertSameJson(
|
|
2242
|
+
verticalTsConfig.include ?? [],
|
|
2243
|
+
[
|
|
2244
|
+
'src',
|
|
2245
|
+
'locales/**/*.json',
|
|
2246
|
+
'package.json',
|
|
2247
|
+
'shared',
|
|
2248
|
+
...(vertical.emitsApi ? ['api'] : []),
|
|
2249
|
+
],
|
|
2250
|
+
`${vertical.path}/tsconfig.json include`,
|
|
2251
|
+
'restore the generated MicroVertical typecheck boundary',
|
|
2252
|
+
);
|
|
1194
2253
|
assertProjectReferenceEmitConfig(verticalTsConfig, vertical.path);
|
|
1195
2254
|
assertSameJson(
|
|
1196
2255
|
verticalMfTypesTsConfig,
|
|
1197
2256
|
{
|
|
1198
|
-
extends: '
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
2257
|
+
extends: '../../tsconfig.base.json',
|
|
2258
|
+
// A headless (api-only) unit exposes no Module Federation surface, so
|
|
2259
|
+
// its DTS boundary only covers the ambient env declarations (G2a).
|
|
2260
|
+
include: vertical.emitsUi
|
|
2261
|
+
? [
|
|
2262
|
+
'src/federation-entry.tsx',
|
|
2263
|
+
...vertical.componentPaths.map(componentPath =>
|
|
2264
|
+
componentPath.replace(`${vertical.path}/`, ''),
|
|
2265
|
+
),
|
|
2266
|
+
'src/modern-app-env.d.ts',
|
|
2267
|
+
]
|
|
2268
|
+
: ['src/modern-app-env.d.ts'],
|
|
1206
2269
|
},
|
|
1207
2270
|
`${vertical.path}/tsconfig.mf-types.json`,
|
|
1208
2271
|
'restore the generated MicroVertical Module Federation DTS boundary',
|
|
1209
2272
|
);
|
|
1210
2273
|
}
|
|
2274
|
+
|
|
2275
|
+
for (const shell of expectedAdditionalShells) {
|
|
2276
|
+
const shellTsConfig = readJson(`${shell.path}/tsconfig.json`);
|
|
2277
|
+
const shellMfTypesTsConfig = readJson(
|
|
2278
|
+
`${shell.path}/tsconfig.mf-types.json`,
|
|
2279
|
+
);
|
|
2280
|
+
const expectedShellReferences = [
|
|
2281
|
+
...sharedPackagePaths,
|
|
2282
|
+
...(shell.verticalRefs ?? [])
|
|
2283
|
+
.map(verticalRef =>
|
|
2284
|
+
fullStackVerticals.find(candidate => candidate.id === verticalRef),
|
|
2285
|
+
)
|
|
2286
|
+
.filter(Boolean)
|
|
2287
|
+
.map(referencedVertical => referencedVertical.path),
|
|
2288
|
+
].map(referencePath => referenceFrom(shell.path, referencePath));
|
|
2289
|
+
assertSameJson(
|
|
2290
|
+
shellTsConfig.references ?? [],
|
|
2291
|
+
expectedShellReferences,
|
|
2292
|
+
`${shell.path}/tsconfig.json references`,
|
|
2293
|
+
'restore the generated additional-shell project-reference graph',
|
|
2294
|
+
);
|
|
2295
|
+
assertSameJson(
|
|
2296
|
+
shellTsConfig.include ?? [],
|
|
2297
|
+
['src', 'locales/**/*.json', 'package.json', 'shared'],
|
|
2298
|
+
`${shell.path}/tsconfig.json include`,
|
|
2299
|
+
'restore the generated additional-shell typecheck boundary',
|
|
2300
|
+
);
|
|
2301
|
+
assertProjectReferenceEmitConfig(shellTsConfig, shell.path);
|
|
2302
|
+
assertSameJson(
|
|
2303
|
+
shellMfTypesTsConfig,
|
|
2304
|
+
{
|
|
2305
|
+
extends: '../../tsconfig.base.json',
|
|
2306
|
+
include: ['src/modern-app-env.d.ts'],
|
|
2307
|
+
},
|
|
2308
|
+
`${shell.path}/tsconfig.mf-types.json`,
|
|
2309
|
+
'restore the generated additional-shell Module Federation DTS boundary',
|
|
2310
|
+
);
|
|
2311
|
+
}
|
|
1211
2312
|
};
|
|
1212
2313
|
const packageJsonFiles = startDir => {
|
|
1213
2314
|
const files = [];
|
|
@@ -1216,7 +2317,7 @@ const packageJsonFiles = startDir => {
|
|
|
1216
2317
|
const current = queue.shift();
|
|
1217
2318
|
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
1218
2319
|
if (
|
|
1219
|
-
|
|
2320
|
+
['.git', '.output', '.zerops', 'dist', 'node_modules', 'repos'].includes(
|
|
1220
2321
|
entry.name,
|
|
1221
2322
|
)
|
|
1222
2323
|
) {
|
|
@@ -1235,35 +2336,52 @@ const packageJsonFiles = startDir => {
|
|
|
1235
2336
|
const modernDependencyNames = packageJson =>
|
|
1236
2337
|
[
|
|
1237
2338
|
...new Set(
|
|
1238
|
-
[
|
|
2339
|
+
[
|
|
2340
|
+
'dependencies',
|
|
2341
|
+
'devDependencies',
|
|
2342
|
+
'optionalDependencies',
|
|
2343
|
+
'peerDependencies',
|
|
2344
|
+
]
|
|
1239
2345
|
.flatMap(section => Object.keys(packageJson[section] ?? {}))
|
|
1240
2346
|
.filter(packageName => packageName.startsWith('@modern-js/')),
|
|
1241
2347
|
),
|
|
1242
2348
|
];
|
|
1243
2349
|
const assertModernPackageCohort = () => {
|
|
1244
|
-
const modernPackageNames =
|
|
1245
|
-
assert(
|
|
1246
|
-
Array.isArray(modernPackageNames) && modernPackageNames.length > 0,
|
|
1247
|
-
'Package source metadata must list the Modern package cohort',
|
|
1248
|
-
);
|
|
1249
|
-
for (const packageName of modernPackageNames) {
|
|
1250
|
-
assert(
|
|
1251
|
-
typeof packageName === 'string' && packageName.startsWith('@modern-js/'),
|
|
1252
|
-
`Package source metadata contains invalid Modern package name ${packageName}`,
|
|
1253
|
-
);
|
|
1254
|
-
}
|
|
1255
|
-
|
|
2350
|
+
const modernPackageNames = workspaceValidationContract.cohort.modernPackages;
|
|
1256
2351
|
const modernPackageNameSet = new Set(modernPackageNames);
|
|
2352
|
+
const observedModernPackageNames = new Set();
|
|
2353
|
+
const observedAppIds = [];
|
|
2354
|
+
// Additional shells (G28) are their own Delivery Units registered in the
|
|
2355
|
+
// additive `config.shells` collection and gated by the structural thin-shell
|
|
2356
|
+
// policy; they are deliberately kept out of the strict topology.apps cohort,
|
|
2357
|
+
// so their package manifests are excluded from the app-id cohort walk here.
|
|
2358
|
+
const additionalShellAppIds = new Set(
|
|
2359
|
+
(workspaceValidationContract.structuralShellPolicy?.shells ?? [])
|
|
2360
|
+
.map(shell => shell.id)
|
|
2361
|
+
.filter(id => id !== 'shell-super-app'),
|
|
2362
|
+
);
|
|
1257
2363
|
for (const packageJsonPath of packageJsonFiles(root)) {
|
|
1258
2364
|
const relativePath = path.relative(root, packageJsonPath).split(path.sep).join('/');
|
|
1259
2365
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
2366
|
+
if (
|
|
2367
|
+
typeof packageJson.modernjs?.appId === 'string' &&
|
|
2368
|
+
!additionalShellAppIds.has(packageJson.modernjs.appId)
|
|
2369
|
+
) {
|
|
2370
|
+
observedAppIds.push(packageJson.modernjs.appId);
|
|
2371
|
+
}
|
|
1260
2372
|
for (const packageName of modernDependencyNames(packageJson)) {
|
|
2373
|
+
observedModernPackageNames.add(packageName);
|
|
1261
2374
|
assert(
|
|
1262
2375
|
modernPackageNameSet.has(packageName),
|
|
1263
2376
|
`${relativePath} declares ${packageName} outside package source metadata`,
|
|
1264
2377
|
);
|
|
1265
2378
|
}
|
|
1266
|
-
for (const section of [
|
|
2379
|
+
for (const section of [
|
|
2380
|
+
'dependencies',
|
|
2381
|
+
'devDependencies',
|
|
2382
|
+
'optionalDependencies',
|
|
2383
|
+
'peerDependencies',
|
|
2384
|
+
]) {
|
|
1267
2385
|
for (const packageName of modernPackageNames) {
|
|
1268
2386
|
const actual = packageJson[section]?.[packageName];
|
|
1269
2387
|
if (actual !== undefined) {
|
|
@@ -1275,6 +2393,25 @@ const assertModernPackageCohort = () => {
|
|
|
1275
2393
|
}
|
|
1276
2394
|
}
|
|
1277
2395
|
}
|
|
2396
|
+
|
|
2397
|
+
for (const packageName of modernPackageNames) {
|
|
2398
|
+
assert(
|
|
2399
|
+
observedModernPackageNames.has(packageName),
|
|
2400
|
+
`Modern package cohort is missing ${packageName}`,
|
|
2401
|
+
);
|
|
2402
|
+
}
|
|
2403
|
+
assertUniqueStrings(observedAppIds, 'generated app package manifests');
|
|
2404
|
+
// The app-id cohort is a SET: observedAppIds is discovered by a
|
|
2405
|
+
// filesystem-ordered package.json walk, while cohort.appIds is in generation
|
|
2406
|
+
// insertion order. Compare order-insensitively (every other cohort check uses
|
|
2407
|
+
// the same sorted comparison via assertSameIdCohort) so a multi-vertical
|
|
2408
|
+
// workspace whose insertion order differs from filesystem order still passes.
|
|
2409
|
+
assertSameJson(
|
|
2410
|
+
[...observedAppIds].toSorted(),
|
|
2411
|
+
[...workspaceValidationContract.cohort.appIds].toSorted(),
|
|
2412
|
+
'generated app package manifest cohort',
|
|
2413
|
+
'restore every generated app package manifest',
|
|
2414
|
+
);
|
|
1278
2415
|
};
|
|
1279
2416
|
const assertPublicSurfaceAssets = (appPath, publicRoutes) => {
|
|
1280
2417
|
for (const relativePath of publicSurfaceManagedSourceAssetPaths) {
|
|
@@ -1286,7 +2423,8 @@ const assertPublicSurfaceContract = (appId, publicSurface) => {
|
|
|
1286
2423
|
assert(publicSurface?.artifactLifecycle === 'build-and-deploy-output', `${appId} public surface artifacts must be build/deploy outputs`);
|
|
1287
2424
|
assert(publicSurface?.generator === 'scripts/generate-public-surface-assets.mts', `${appId} public surface generator script is incorrect`);
|
|
1288
2425
|
assert(publicSurface?.outputRoot === 'dist/public', `${appId} public surface dist outputRoot is incorrect`);
|
|
1289
|
-
assert(publicSurface?.
|
|
2426
|
+
assert(publicSurface?.cloudflareBuildOutputRoot === 'dist-cloudflare/public', `${appId} public surface Cloudflare build outputRoot is incorrect`);
|
|
2427
|
+
assert(!('cloudflareOutputRoot' in (publicSurface ?? {})), `${appId} public surface must not target final .output directly`);
|
|
1290
2428
|
assert(!('staticRoot' in (publicSurface ?? {})), `${appId} public surface must not point at source config/public`);
|
|
1291
2429
|
assert((publicSurface?.files ?? []).includes('robots.txt'), `${appId} public surface must always emit robots.txt`);
|
|
1292
2430
|
assert(publicSurface?.contentExpansion?.authoring === 'route-owned-esm-provider', `${appId} public content expansion authoring is incorrect`);
|
|
@@ -1349,21 +2487,41 @@ const extractAssetPrefixExpression = modernConfig => {
|
|
|
1349
2487
|
assert(match?.groups?.expression, 'modern.config.ts must assign assetPrefix');
|
|
1350
2488
|
return match.groups.expression;
|
|
1351
2489
|
};
|
|
1352
|
-
const
|
|
2490
|
+
const assertTargetIsolatedBuildArtifacts = (appId, modernConfig) => {
|
|
1353
2491
|
assert(
|
|
1354
|
-
modernConfig.includes("const
|
|
1355
|
-
modernConfig.includes(
|
|
2492
|
+
modernConfig.includes("const buildTarget = cloudflareDeployEnabled ? 'cloudflare' : 'web';") &&
|
|
2493
|
+
modernConfig.includes("const buildOutputRoot = cloudflareDeployEnabled ? 'dist-cloudflare' : 'dist';") &&
|
|
2494
|
+
modernConfig.includes('const buildTempDirectory = `node_modules/.modern-js-${appId}-${buildTarget}`;') &&
|
|
2495
|
+
modernConfig.includes('const buildCacheDirectory = `node_modules/.cache/rspack-${appId}-${buildTarget}`;') &&
|
|
2496
|
+
modernConfig.includes('root: buildOutputRoot,') &&
|
|
2497
|
+
modernConfig.includes('tempDir: buildTempDirectory,') &&
|
|
2498
|
+
modernConfig.includes('cacheDigest: [appId, buildTarget],') &&
|
|
1356
2499
|
modernConfig.includes('cacheDirectory: buildCacheDirectory,'),
|
|
1357
|
-
`${appId} must
|
|
2500
|
+
`${appId} must isolate build output, Modern temp files, and Rspack cache by app and build target`,
|
|
1358
2501
|
);
|
|
1359
2502
|
};
|
|
1360
2503
|
const assertCloudflareBuildSkipsDeployRebuild = (appId, packageJson) => {
|
|
2504
|
+
const cloudflareBuild = packageJson.scripts?.['cloudflare:build'] ?? '';
|
|
2505
|
+
const buildCommand = 'MODERNJS_DEPLOY=cloudflare modern build';
|
|
2506
|
+
const publicSurfaceCommand = `--app ${appId} --target cloudflare-dist`;
|
|
2507
|
+
const deployCommand =
|
|
2508
|
+
'MODERNJS_DEPLOY=cloudflare modern deploy --skip-build';
|
|
1361
2509
|
assert(
|
|
1362
|
-
|
|
1363
|
-
'ULTRAMODERN_ZEPHYR=false MODERNJS_DEPLOY=cloudflare modern deploy --skip-build',
|
|
1364
|
-
),
|
|
2510
|
+
cloudflareBuild.includes(deployCommand),
|
|
1365
2511
|
`${appId} cloudflare:build must deploy with --skip-build after the explicit Cloudflare build`,
|
|
1366
2512
|
);
|
|
2513
|
+
assert(
|
|
2514
|
+
cloudflareBuild.includes(publicSurfaceCommand) &&
|
|
2515
|
+
cloudflareBuild.indexOf(buildCommand) <
|
|
2516
|
+
cloudflareBuild.indexOf(publicSurfaceCommand) &&
|
|
2517
|
+
cloudflareBuild.indexOf(publicSurfaceCommand) <
|
|
2518
|
+
cloudflareBuild.indexOf(deployCommand),
|
|
2519
|
+
`${appId} cloudflare:build must generate public-surface assets in the explicit Cloudflare build output before deploy`,
|
|
2520
|
+
);
|
|
2521
|
+
assert(
|
|
2522
|
+
!/--target (?:dist|cloudflare)(?=\s|$)/u.test(cloudflareBuild),
|
|
2523
|
+
`${appId} cloudflare:build must not target the Node dist or final Cloudflare output directly`,
|
|
2524
|
+
);
|
|
1367
2525
|
};
|
|
1368
2526
|
const stripYamlInlineComment = value => {
|
|
1369
2527
|
let quote = undefined;
|
|
@@ -1478,6 +2636,9 @@ const requiredPaths = [
|
|
|
1478
2636
|
'.gitignore',
|
|
1479
2637
|
'package.json',
|
|
1480
2638
|
'pnpm-workspace.yaml',
|
|
2639
|
+
`patches/@module-federation__modern-js-v3@${expectedModuleFederationVersion}.patch`,
|
|
2640
|
+
`patches/@module-federation__bridge-react@${expectedModuleFederationVersion}.patch`,
|
|
2641
|
+
'patches/effect-schema-error-type-id.patch',
|
|
1481
2642
|
'tsconfig.json',
|
|
1482
2643
|
'tsconfig.base.json',
|
|
1483
2644
|
'oxlint.config.ts',
|
|
@@ -1485,8 +2646,6 @@ const requiredPaths = [
|
|
|
1485
2646
|
'.github/renovate.json',
|
|
1486
2647
|
'.github/workflows/ultramodern-workspace-gates.yml',
|
|
1487
2648
|
'.agents/agent-reference-repos.json',
|
|
1488
|
-
'.codex/skills-lock.json',
|
|
1489
|
-
'.codex/rstackjs-agent-skills-LICENSE',
|
|
1490
2649
|
'topology/reference-topology.json',
|
|
1491
2650
|
'topology/ownership.json',
|
|
1492
2651
|
'topology/local-overlays/development.json',
|
|
@@ -1494,13 +2653,18 @@ const requiredPaths = [
|
|
|
1494
2653
|
'scripts/bootstrap-agent-skills.mts',
|
|
1495
2654
|
'scripts/check-ultramodern-api-boundaries.mts',
|
|
1496
2655
|
'scripts/check-ultramodern-i18n-boundaries.mts',
|
|
2656
|
+
...(hasBackendSurfaces ? ['scripts/generate-node-backend-federation.mts'] : []),
|
|
1497
2657
|
'scripts/generate-public-surface-assets.mts',
|
|
2658
|
+
'scripts/generate-tanstack-routes.mts',
|
|
1498
2659
|
'scripts/proof-cloudflare-version.mts',
|
|
2660
|
+
...(hasDeliveryUnits ? ['scripts/proof-workerd-ssr.mts'] : []),
|
|
2661
|
+
...(hasBackendSurfaces ? ['scripts/proof-node-backend-federation.mts'] : []),
|
|
1499
2662
|
'scripts/setup-agent-reference-repos.mts',
|
|
1500
2663
|
'scripts/ultramodern-performance-readiness.config.mjs',
|
|
1501
2664
|
'scripts/ultramodern-performance-readiness.mts',
|
|
1502
2665
|
'scripts/ultramodern-typecheck.mts',
|
|
1503
2666
|
'scripts/validate-ultramodern-workspace.mts',
|
|
2667
|
+
'scripts/verify-cloudflare-output.mts',
|
|
1504
2668
|
'apps/shell-super-app/package.json',
|
|
1505
2669
|
'apps/shell-super-app/tsconfig.json',
|
|
1506
2670
|
'apps/shell-super-app/tsconfig.mf-types.json',
|
|
@@ -1515,10 +2679,13 @@ const requiredPaths = [
|
|
|
1515
2679
|
`apps/shell-super-app/locales/cs/${shellNamespace}.json`,
|
|
1516
2680
|
'apps/shell-super-app/src/routes/index.css',
|
|
1517
2681
|
'apps/shell-super-app/src/routes/layout.tsx',
|
|
2682
|
+
'apps/shell-super-app/src/routes/shell-frame.tsx',
|
|
1518
2683
|
'apps/shell-super-app/src/routes/ultramodern-route-head.tsx',
|
|
1519
2684
|
'apps/shell-super-app/src/routes/ultramodern-route-metadata.ts',
|
|
2685
|
+
'apps/shell-super-app/src/routes/vertical-components.tsx',
|
|
2686
|
+
requiredShellWorkerCompositionPath('apps/shell-super-app'),
|
|
1520
2687
|
'apps/shell-super-app/src/routes/[lang]/page.tsx',
|
|
1521
|
-
...
|
|
2688
|
+
...shellRouteMetaPaths,
|
|
1522
2689
|
'packages/shared-contracts/package.json',
|
|
1523
2690
|
'packages/shared-contracts/src/index.ts',
|
|
1524
2691
|
'packages/shared-contracts/tsconfig.json',
|
|
@@ -1534,12 +2701,10 @@ for (const vertical of fullStackVerticals) {
|
|
|
1534
2701
|
|
|
1535
2702
|
if (tailwindEnabled) {
|
|
1536
2703
|
requiredPaths.push(
|
|
1537
|
-
'apps/shell-super-app/postcss.config.mjs',
|
|
1538
2704
|
'apps/shell-super-app/tailwind.config.ts',
|
|
1539
|
-
...fullStackVerticals
|
|
1540
|
-
|
|
1541
|
-
`${vertical.path}/tailwind.config.ts
|
|
1542
|
-
]),
|
|
2705
|
+
...fullStackVerticals
|
|
2706
|
+
.filter(vertical => vertical.emitsUi)
|
|
2707
|
+
.flatMap(vertical => [`${vertical.path}/tailwind.config.ts`]),
|
|
1543
2708
|
);
|
|
1544
2709
|
}
|
|
1545
2710
|
|
|
@@ -1549,10 +2714,86 @@ for (const vertical of fullStackVerticals) {
|
|
|
1549
2714
|
requiredMicroVerticalPaths(vertical).forEach(
|
|
1550
2715
|
assertRequiredVerticalFile(vertical),
|
|
1551
2716
|
);
|
|
2717
|
+
// Reject profile-foreign surfaces planted into a unit (e.g. a browser MF
|
|
2718
|
+
// config in a headless api-only unit, or an API contract in a UI-only unit).
|
|
2719
|
+
forbiddenMicroVerticalPaths(vertical).forEach(
|
|
2720
|
+
assertForbiddenVerticalFile(vertical),
|
|
2721
|
+
);
|
|
2722
|
+
}
|
|
2723
|
+
for (const shell of expectedAdditionalShells) {
|
|
2724
|
+
requiredPaths.push(
|
|
2725
|
+
`${shell.path}/package.json`,
|
|
2726
|
+
`${shell.path}/tsconfig.json`,
|
|
2727
|
+
`${shell.path}/tsconfig.mf-types.json`,
|
|
2728
|
+
`${shell.path}/modern.config.ts`,
|
|
2729
|
+
`${shell.path}/module-federation.config.ts`,
|
|
2730
|
+
`${shell.path}/src/modern-app-env.d.ts`,
|
|
2731
|
+
`${shell.path}/src/modern.runtime.ts`,
|
|
2732
|
+
`${shell.path}/src/api/vertical-clients.ts`,
|
|
2733
|
+
`${shell.path}/locales/en/translation.json`,
|
|
2734
|
+
`${shell.path}/locales/en/${shellNamespace}.json`,
|
|
2735
|
+
`${shell.path}/locales/cs/translation.json`,
|
|
2736
|
+
`${shell.path}/locales/cs/${shellNamespace}.json`,
|
|
2737
|
+
`${shell.path}/src/routes/index.css`,
|
|
2738
|
+
`${shell.path}/src/routes/layout.tsx`,
|
|
2739
|
+
`${shell.path}/src/routes/shell-frame.tsx`,
|
|
2740
|
+
`${shell.path}/src/routes/vertical-components.tsx`,
|
|
2741
|
+
requiredShellWorkerCompositionPath(shell.path),
|
|
2742
|
+
`${shell.path}/src/routes/ultramodern-route-head.tsx`,
|
|
2743
|
+
`${shell.path}/src/routes/ultramodern-route-metadata.ts`,
|
|
2744
|
+
`${shell.path}/src/routes/[lang]/page.tsx`,
|
|
2745
|
+
...shellRouteMetaPaths.map(relativePath =>
|
|
2746
|
+
relativePath.replace(/^apps\/shell-super-app/u, shell.path),
|
|
2747
|
+
),
|
|
2748
|
+
);
|
|
2749
|
+
if (tailwindEnabled) {
|
|
2750
|
+
requiredPaths.push(`${shell.path}/tailwind.config.ts`);
|
|
2751
|
+
}
|
|
1552
2752
|
}
|
|
1553
2753
|
for (const requiredPath of requiredPaths) {
|
|
1554
2754
|
assertExists(requiredPath);
|
|
1555
2755
|
}
|
|
2756
|
+
// Agent skills metadata may live under .agents/ (agents-standard layout) or
|
|
2757
|
+
// .codex/ (legacy scaffold default).
|
|
2758
|
+
assertAnyOf(['.agents/skills-lock.json', '.codex/skills-lock.json']);
|
|
2759
|
+
assertAnyOf([
|
|
2760
|
+
'.agents/rstackjs-agent-skills-LICENSE',
|
|
2761
|
+
'.codex/rstackjs-agent-skills-LICENSE',
|
|
2762
|
+
]);
|
|
2763
|
+
const pnpmWorkspace = readText('pnpm-workspace.yaml');
|
|
2764
|
+
assert(
|
|
2765
|
+
pnpmWorkspace.includes(`'@effect/opentelemetry': ${expectedEffectVersion}`),
|
|
2766
|
+
'pnpm-workspace.yaml must override @effect/opentelemetry to the generated Effect cohort',
|
|
2767
|
+
);
|
|
2768
|
+
assert(
|
|
2769
|
+
pnpmWorkspace.includes(`effect: ${expectedEffectVersion}`),
|
|
2770
|
+
'pnpm-workspace.yaml must override effect to the generated Effect cohort',
|
|
2771
|
+
);
|
|
2772
|
+
assert(
|
|
2773
|
+
pnpmWorkspace.includes(
|
|
2774
|
+
`'@module-federation/modern-js-v3@${expectedModuleFederationVersion}': patches/@module-federation__modern-js-v3@${expectedModuleFederationVersion}.patch`,
|
|
2775
|
+
),
|
|
2776
|
+
'pnpm-workspace.yaml must patch the generated Module Federation Modern.js integration cohort',
|
|
2777
|
+
);
|
|
2778
|
+
assert(
|
|
2779
|
+
pnpmWorkspace.includes(
|
|
2780
|
+
`'@module-federation/bridge-react@${expectedModuleFederationVersion}': patches/@module-federation__bridge-react@${expectedModuleFederationVersion}.patch`,
|
|
2781
|
+
),
|
|
2782
|
+
'pnpm-workspace.yaml must patch the generated Module Federation React bridge cohort',
|
|
2783
|
+
);
|
|
2784
|
+
assert(
|
|
2785
|
+
[
|
|
2786
|
+
`'effect@${expectedEffectVersion}'`,
|
|
2787
|
+
`effect@${expectedEffectVersion}`,
|
|
2788
|
+
].some(effectPatchKey =>
|
|
2789
|
+
pnpmWorkspace.includes(
|
|
2790
|
+
`${effectPatchKey}: patches/effect-schema-error-type-id.patch`,
|
|
2791
|
+
),
|
|
2792
|
+
),
|
|
2793
|
+
'pnpm-workspace.yaml must patch the generated Effect declaration cohort',
|
|
2794
|
+
);
|
|
2795
|
+
assertWorkspaceValidationContract(workspaceValidationContract);
|
|
2796
|
+
assertGeneratedSurfacePolicy();
|
|
1556
2797
|
for (const oldRemotePath of oldRemotePaths) {
|
|
1557
2798
|
assertNotExists(oldRemotePath);
|
|
1558
2799
|
}
|
|
@@ -1561,13 +2802,23 @@ for (const retiredMetadataPath of retiredMetadataPaths) {
|
|
|
1561
2802
|
}
|
|
1562
2803
|
const rootPackage = readJson('package.json');
|
|
1563
2804
|
const ultramodernConfig = readJson(compactConfigPath);
|
|
2805
|
+
const topology = readJson(
|
|
2806
|
+
workspaceValidationContract.metadata.referenceTopology.path,
|
|
2807
|
+
);
|
|
2808
|
+
const ownership = readJson(workspaceValidationContract.metadata.ownership.path);
|
|
2809
|
+
const overlay = readJson(
|
|
2810
|
+
workspaceValidationContract.metadata.developmentOverlay.path,
|
|
2811
|
+
);
|
|
2812
|
+
assertStructuredWorkspaceMetadata({
|
|
2813
|
+
ultramodernConfig,
|
|
2814
|
+
topology,
|
|
2815
|
+
ownership,
|
|
2816
|
+
overlay,
|
|
2817
|
+
});
|
|
1564
2818
|
const bridgeConfig =
|
|
1565
2819
|
ultramodernConfig?.bridge?.enabled === true ? ultramodernConfig.bridge : undefined;
|
|
1566
|
-
const packageSource =
|
|
2820
|
+
const packageSource = createPackageSourceView(ultramodernConfig);
|
|
1567
2821
|
const generatedContract = readGeneratedContractView(ultramodernConfig);
|
|
1568
|
-
const topology = readJson('topology/reference-topology.json');
|
|
1569
|
-
const ownership = readJson('topology/ownership.json');
|
|
1570
|
-
const overlay = readJson('topology/local-overlays/development.json');
|
|
1571
2822
|
const shellPackage = readJson('apps/shell-super-app/package.json');
|
|
1572
2823
|
|
|
1573
2824
|
assertMicroVerticalContractGraph({
|
|
@@ -1577,6 +2828,12 @@ assertMicroVerticalContractGraph({
|
|
|
1577
2828
|
overlay,
|
|
1578
2829
|
shellPackage,
|
|
1579
2830
|
});
|
|
2831
|
+
assertStructuredWorkspaceMetadataSemantics({
|
|
2832
|
+
ultramodernConfig,
|
|
2833
|
+
topology,
|
|
2834
|
+
ownership,
|
|
2835
|
+
overlay,
|
|
2836
|
+
});
|
|
1580
2837
|
assertTsConfigReferenceGraph();
|
|
1581
2838
|
|
|
1582
2839
|
assert(rootPackage.private === true, 'Root package must be private');
|
|
@@ -1611,6 +2868,594 @@ assert(rootPackage.modernjs?.packageSource?.strategy === packageSource.strategy,
|
|
|
1611
2868
|
assert(packageSource.strategy === 'workspace' || packageSource.strategy === 'install', 'Package source strategy must be workspace or install');
|
|
1612
2869
|
assert(packageSource.strategy === 'install' || packageSource.modernPackages?.specifier === 'workspace:*', 'Workspace package source must be explicitly backed by workspace:*');
|
|
1613
2870
|
assertModernPackageCohort();
|
|
2871
|
+
const assertStructuralShellPolicy = () => {
|
|
2872
|
+
const policy = workspaceValidationContract.structuralShellPolicy;
|
|
2873
|
+
const compositionPolicy =
|
|
2874
|
+
workspaceValidationContract.federatedCompositionSourcePolicy;
|
|
2875
|
+
if (!policy && !compositionPolicy) {
|
|
2876
|
+
return;
|
|
2877
|
+
}
|
|
2878
|
+
const collectSourceFiles = absoluteDir => {
|
|
2879
|
+
const files = [];
|
|
2880
|
+
const queue = [absoluteDir];
|
|
2881
|
+
while (queue.length > 0) {
|
|
2882
|
+
const current = queue.shift();
|
|
2883
|
+
let entries;
|
|
2884
|
+
try {
|
|
2885
|
+
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
2886
|
+
} catch {
|
|
2887
|
+
continue;
|
|
2888
|
+
}
|
|
2889
|
+
for (const entry of entries) {
|
|
2890
|
+
const absoluteEntry = path.join(current, entry.name);
|
|
2891
|
+
if (entry.isDirectory()) {
|
|
2892
|
+
queue.push(absoluteEntry);
|
|
2893
|
+
} else if (/\.(?:ts|tsx|mts|cts|js|jsx|mjs|cjs)$/u.test(entry.name)) {
|
|
2894
|
+
files.push(absoluteEntry);
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
return files.toSorted();
|
|
2899
|
+
};
|
|
2900
|
+
// Strip comments before scanning imports so the structural gate reasons
|
|
2901
|
+
// about executable source only. A specifier that lives inside a comment
|
|
2902
|
+
// (`// import x from '../../../verticals/catalog/src/internal'` or a /* */
|
|
2903
|
+
// block) must not trip the gate, and — conversely — a real import carrying a
|
|
2904
|
+
// magic comment (`import(/* webpackChunkName */ '...')`) must still be
|
|
2905
|
+
// detected once the comment is removed. The scanner is string-literal aware,
|
|
2906
|
+
// so a `//` inside a string (e.g. `'http://example.com'`) is preserved and
|
|
2907
|
+
// never mistaken for a comment start. It is also regex-literal aware, so a
|
|
2908
|
+
// quote inside a regex (`const q = /['"]/;`) never opens a phantom string
|
|
2909
|
+
// that would swallow a later real comment, and template-interpolation aware,
|
|
2910
|
+
// so a comment inside `${ … }` is stripped like any other code. Regex-vs-
|
|
2911
|
+
// division is resolved from the previous significant token; `<`/`>` are
|
|
2912
|
+
// deliberately excluded so JSX closing tags (`</div>`) are never mistaken for
|
|
2913
|
+
// regex literals. Dependency-free by construction.
|
|
2914
|
+
const stripSourceComments = code => {
|
|
2915
|
+
const isIdentifierChar = ch => ch !== '' && /[A-Za-z0-9_$]/u.test(ch);
|
|
2916
|
+
// Punctuation after which a `/` opens a regex literal (not a division op).
|
|
2917
|
+
// `<`/`>` are intentionally omitted to keep JSX closing/self-closing tags
|
|
2918
|
+
// (`</div>`, `<br/>`) parsing as code rather than as regex.
|
|
2919
|
+
const regexPrecedingPunct = new Set([
|
|
2920
|
+
'(', ',', '=', '[', '{', ';', ':', '!', '&', '|', '?', '+', '-', '*',
|
|
2921
|
+
'%', '^', '~',
|
|
2922
|
+
]);
|
|
2923
|
+
// Keywords after which a `/` opens a regex literal.
|
|
2924
|
+
const regexPrecedingKeywords = new Set([
|
|
2925
|
+
'return', 'typeof', 'instanceof', 'in', 'of', 'new', 'delete', 'void',
|
|
2926
|
+
'do', 'else', 'yield', 'await', 'case',
|
|
2927
|
+
]);
|
|
2928
|
+
let result = '';
|
|
2929
|
+
let state = 'code';
|
|
2930
|
+
// Last significant (non-whitespace) code character emitted and the trailing
|
|
2931
|
+
// identifier word — together they decide regex-vs-division for a `/`.
|
|
2932
|
+
let lastSignificant = '';
|
|
2933
|
+
let currentWord = '';
|
|
2934
|
+
let regexInClass = false;
|
|
2935
|
+
// Stack of open `${ … }` interpolations; each entry is the `{ }` nesting
|
|
2936
|
+
// depth reached inside that interpolation so the matching close brace pops
|
|
2937
|
+
// back to template text. Comments inside an interpolation are code.
|
|
2938
|
+
const interpolations = [];
|
|
2939
|
+
const regexCanFollow = () => {
|
|
2940
|
+
if (lastSignificant === '') {
|
|
2941
|
+
return true;
|
|
2942
|
+
}
|
|
2943
|
+
if (regexPrecedingPunct.has(lastSignificant)) {
|
|
2944
|
+
return true;
|
|
2945
|
+
}
|
|
2946
|
+
if (isIdentifierChar(lastSignificant)) {
|
|
2947
|
+
return regexPrecedingKeywords.has(currentWord);
|
|
2948
|
+
}
|
|
2949
|
+
return false;
|
|
2950
|
+
};
|
|
2951
|
+
const emitCodeChar = ch => {
|
|
2952
|
+
result += ch;
|
|
2953
|
+
if (/\s/u.test(ch)) {
|
|
2954
|
+
return;
|
|
2955
|
+
}
|
|
2956
|
+
if (isIdentifierChar(ch)) {
|
|
2957
|
+
currentWord += ch;
|
|
2958
|
+
} else {
|
|
2959
|
+
currentWord = '';
|
|
2960
|
+
}
|
|
2961
|
+
lastSignificant = ch;
|
|
2962
|
+
};
|
|
2963
|
+
for (let index = 0; index < code.length; index += 1) {
|
|
2964
|
+
const char = code[index];
|
|
2965
|
+
const next = index + 1 < code.length ? code[index + 1] : '';
|
|
2966
|
+
if (state === 'code') {
|
|
2967
|
+
if (char === '/' && next === '/') {
|
|
2968
|
+
state = 'line';
|
|
2969
|
+
index += 1;
|
|
2970
|
+
} else if (char === '/' && next === '*') {
|
|
2971
|
+
state = 'block';
|
|
2972
|
+
result += ' ';
|
|
2973
|
+
index += 1;
|
|
2974
|
+
} else if (char === '/' && regexCanFollow()) {
|
|
2975
|
+
state = 'regex';
|
|
2976
|
+
regexInClass = false;
|
|
2977
|
+
emitCodeChar(char);
|
|
2978
|
+
} else if (char === "'") {
|
|
2979
|
+
state = 'single';
|
|
2980
|
+
result += char;
|
|
2981
|
+
} else if (char === '"') {
|
|
2982
|
+
state = 'double';
|
|
2983
|
+
result += char;
|
|
2984
|
+
} else if (char === '`') {
|
|
2985
|
+
state = 'template';
|
|
2986
|
+
result += char;
|
|
2987
|
+
} else if (
|
|
2988
|
+
char === '}' &&
|
|
2989
|
+
interpolations.length > 0 &&
|
|
2990
|
+
interpolations[interpolations.length - 1] === 0
|
|
2991
|
+
) {
|
|
2992
|
+
// The closing brace of the current `${ … }` interpolation: resume the
|
|
2993
|
+
// enclosing template literal text.
|
|
2994
|
+
interpolations.pop();
|
|
2995
|
+
state = 'template';
|
|
2996
|
+
result += char;
|
|
2997
|
+
lastSignificant = char;
|
|
2998
|
+
currentWord = '';
|
|
2999
|
+
} else {
|
|
3000
|
+
if (interpolations.length > 0) {
|
|
3001
|
+
if (char === '{') {
|
|
3002
|
+
interpolations[interpolations.length - 1] += 1;
|
|
3003
|
+
} else if (char === '}') {
|
|
3004
|
+
interpolations[interpolations.length - 1] -= 1;
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
3007
|
+
emitCodeChar(char);
|
|
3008
|
+
}
|
|
3009
|
+
continue;
|
|
3010
|
+
}
|
|
3011
|
+
if (state === 'line') {
|
|
3012
|
+
if (char === '\n') {
|
|
3013
|
+
state = 'code';
|
|
3014
|
+
result += char;
|
|
3015
|
+
}
|
|
3016
|
+
continue;
|
|
3017
|
+
}
|
|
3018
|
+
if (state === 'block') {
|
|
3019
|
+
if (char === '*' && next === '/') {
|
|
3020
|
+
state = 'code';
|
|
3021
|
+
index += 1;
|
|
3022
|
+
} else if (char === '\n') {
|
|
3023
|
+
result += char;
|
|
3024
|
+
}
|
|
3025
|
+
continue;
|
|
3026
|
+
}
|
|
3027
|
+
if (state === 'regex') {
|
|
3028
|
+
// Regex body passes through as code. Honor escapes and character
|
|
3029
|
+
// classes (`[...]`), where `/` need not be escaped, so the literal ends
|
|
3030
|
+
// only at the true closing `/`.
|
|
3031
|
+
if (char === '\\') {
|
|
3032
|
+
result += char + next;
|
|
3033
|
+
index += 1;
|
|
3034
|
+
continue;
|
|
3035
|
+
}
|
|
3036
|
+
if (char === '[') {
|
|
3037
|
+
regexInClass = true;
|
|
3038
|
+
} else if (char === ']') {
|
|
3039
|
+
regexInClass = false;
|
|
3040
|
+
} else if (char === '/' && !regexInClass) {
|
|
3041
|
+
state = 'code';
|
|
3042
|
+
result += char;
|
|
3043
|
+
lastSignificant = char;
|
|
3044
|
+
currentWord = '';
|
|
3045
|
+
continue;
|
|
3046
|
+
}
|
|
3047
|
+
result += char;
|
|
3048
|
+
continue;
|
|
3049
|
+
}
|
|
3050
|
+
if (state === 'template') {
|
|
3051
|
+
if (char === '\\') {
|
|
3052
|
+
result += char + next;
|
|
3053
|
+
index += 1;
|
|
3054
|
+
continue;
|
|
3055
|
+
}
|
|
3056
|
+
if (char === '`') {
|
|
3057
|
+
state = 'code';
|
|
3058
|
+
result += char;
|
|
3059
|
+
lastSignificant = char;
|
|
3060
|
+
currentWord = '';
|
|
3061
|
+
continue;
|
|
3062
|
+
}
|
|
3063
|
+
if (char === '$' && next === '{') {
|
|
3064
|
+
// Enter a `${ … }` interpolation: its body is code, so re-enter the
|
|
3065
|
+
// full scanner (nesting-aware) to strip comments inside it.
|
|
3066
|
+
interpolations.push(0);
|
|
3067
|
+
state = 'code';
|
|
3068
|
+
result += '${';
|
|
3069
|
+
index += 1;
|
|
3070
|
+
lastSignificant = '{';
|
|
3071
|
+
currentWord = '';
|
|
3072
|
+
continue;
|
|
3073
|
+
}
|
|
3074
|
+
result += char;
|
|
3075
|
+
continue;
|
|
3076
|
+
}
|
|
3077
|
+
// Inside a single- or double-quoted string literal: preserve every
|
|
3078
|
+
// character, honor escapes so a closing quote is not misread, and never
|
|
3079
|
+
// treat `//` or `/*` as comments.
|
|
3080
|
+
if (char === '\\') {
|
|
3081
|
+
result += char + next;
|
|
3082
|
+
index += 1;
|
|
3083
|
+
continue;
|
|
3084
|
+
}
|
|
3085
|
+
if (
|
|
3086
|
+
(state === 'single' && char === "'") ||
|
|
3087
|
+
(state === 'double' && char === '"')
|
|
3088
|
+
) {
|
|
3089
|
+
state = 'code';
|
|
3090
|
+
result += char;
|
|
3091
|
+
lastSignificant = char;
|
|
3092
|
+
currentWord = '';
|
|
3093
|
+
continue;
|
|
3094
|
+
}
|
|
3095
|
+
result += char;
|
|
3096
|
+
}
|
|
3097
|
+
return result;
|
|
3098
|
+
};
|
|
3099
|
+
for (const shell of policy?.shells ?? []) {
|
|
3100
|
+
for (const forbidden of policy?.forbiddenPathClasses ?? []) {
|
|
3101
|
+
assert(
|
|
3102
|
+
!fs.existsSync(path.join(root, shell.packageDir, forbidden.path)),
|
|
3103
|
+
selfCheckFailure(
|
|
3104
|
+
`structural thin-shell ${forbidden.id}`,
|
|
3105
|
+
`${forbidden.diagnostic} Forbidden artifact ${shell.packageDir}/${forbidden.path} exists`,
|
|
3106
|
+
`remove forbidden thin-shell artifact ${shell.packageDir}/${forbidden.path}`,
|
|
3107
|
+
),
|
|
3108
|
+
);
|
|
3109
|
+
}
|
|
3110
|
+
const srcAbsolute = path.join(root, shell.srcDir);
|
|
3111
|
+
if (!fs.existsSync(srcAbsolute)) {
|
|
3112
|
+
continue;
|
|
3113
|
+
}
|
|
3114
|
+
for (const file of collectSourceFiles(srcAbsolute)) {
|
|
3115
|
+
const source = stripSourceComments(fs.readFileSync(file, 'utf-8'));
|
|
3116
|
+
const relative = path.relative(root, file).split(path.sep).join('/');
|
|
3117
|
+
for (const pattern of policy?.forbiddenImportPatterns ?? []) {
|
|
3118
|
+
const match = new RegExp(pattern.expression, pattern.flags).exec(source);
|
|
3119
|
+
assert(
|
|
3120
|
+
match === null,
|
|
3121
|
+
selfCheckFailure(
|
|
3122
|
+
`structural thin-shell ${pattern.id}`,
|
|
3123
|
+
`${pattern.diagnostic} Found forbidden import at ${relative}:${match?.index ?? 0}`,
|
|
3124
|
+
'consume only published surfaces from the thin shell',
|
|
3125
|
+
),
|
|
3126
|
+
);
|
|
3127
|
+
}
|
|
3128
|
+
}
|
|
3129
|
+
}
|
|
3130
|
+
|
|
3131
|
+
const runtimeModuleSpecifiers = source => {
|
|
3132
|
+
const specifiers = [];
|
|
3133
|
+
const matchAll = expression => {
|
|
3134
|
+
for (const match of source.matchAll(expression)) {
|
|
3135
|
+
specifiers.push(match.at(-1));
|
|
3136
|
+
}
|
|
3137
|
+
};
|
|
3138
|
+
for (const match of source.matchAll(/\b(?:import|export)\s+([^;'"`]+?)\s+from\s*['"]([^'"]+)['"]/gu)) {
|
|
3139
|
+
if (!match[1].trimStart().startsWith('type ')) {
|
|
3140
|
+
specifiers.push(match[2]);
|
|
3141
|
+
}
|
|
3142
|
+
}
|
|
3143
|
+
matchAll(/\bimport\s*['"]([^'"]+)['"]/gu);
|
|
3144
|
+
matchAll(/\bimport\s*\(\s*['"]([^'"]+)['"]/gu);
|
|
3145
|
+
matchAll(/\brequire\s*\(\s*['"]([^'"]+)['"]/gu);
|
|
3146
|
+
return specifiers;
|
|
3147
|
+
};
|
|
3148
|
+
const remoteImplementationFor = (specifier, remotes) =>
|
|
3149
|
+
remotes.find(remote => {
|
|
3150
|
+
const packageSubpath = specifier.startsWith(`${remote.packageName}/`)
|
|
3151
|
+
? specifier.slice(remote.packageName.length + 1)
|
|
3152
|
+
: undefined;
|
|
3153
|
+
if (
|
|
3154
|
+
specifier === remote.packageName ||
|
|
3155
|
+
(packageSubpath !== undefined && !packageSubpath.startsWith('api'))
|
|
3156
|
+
) {
|
|
3157
|
+
return true;
|
|
3158
|
+
}
|
|
3159
|
+
const normalizedSpecifier = specifier.replaceAll('\\', '/');
|
|
3160
|
+
return normalizedSpecifier.includes(`${remote.directory}/`);
|
|
3161
|
+
});
|
|
3162
|
+
|
|
3163
|
+
for (const host of compositionPolicy?.hosts ?? []) {
|
|
3164
|
+
const srcAbsolute = path.join(root, host.srcDir);
|
|
3165
|
+
if (!fs.existsSync(srcAbsolute)) {
|
|
3166
|
+
continue;
|
|
3167
|
+
}
|
|
3168
|
+
for (const file of collectSourceFiles(srcAbsolute)) {
|
|
3169
|
+
const source = stripSourceComments(fs.readFileSync(file, 'utf-8'));
|
|
3170
|
+
const relative = path.relative(root, file).split(path.sep).join('/');
|
|
3171
|
+
for (const pattern of compositionPolicy.forbiddenSourcePatterns ?? []) {
|
|
3172
|
+
const match = new RegExp(pattern.expression, pattern.flags).exec(source);
|
|
3173
|
+
assert(
|
|
3174
|
+
match === null,
|
|
3175
|
+
selfCheckFailure(
|
|
3176
|
+
`federated composition ${pattern.id}`,
|
|
3177
|
+
`${pattern.diagnostic} Found forbidden source at ${relative}:${match?.index ?? 0}`,
|
|
3178
|
+
'compose remote rendering through framework Module Federation primitives',
|
|
3179
|
+
),
|
|
3180
|
+
);
|
|
3181
|
+
}
|
|
3182
|
+
// Declaration files cannot execute. Ambient federation declarations may
|
|
3183
|
+
// re-export a workspace component solely to preserve its public prop
|
|
3184
|
+
// type, so treating their specifiers as runtime implementation imports
|
|
3185
|
+
// produces a false positive.
|
|
3186
|
+
if (/\.d\.(?:ts|mts|cts)$/u.test(relative)) {
|
|
3187
|
+
continue;
|
|
3188
|
+
}
|
|
3189
|
+
for (const specifier of runtimeModuleSpecifiers(source)) {
|
|
3190
|
+
const remote = remoteImplementationFor(specifier, host.remotes ?? []);
|
|
3191
|
+
assert(
|
|
3192
|
+
remote === undefined,
|
|
3193
|
+
selfCheckFailure(
|
|
3194
|
+
'federated composition remote-runtime-package-import',
|
|
3195
|
+
`Host ${host.id} imports remote render implementation ${specifier} from ${remote?.id} at ${relative}`,
|
|
3196
|
+
'use import type for contracts or compose the implementation through Module Federation',
|
|
3197
|
+
),
|
|
3198
|
+
);
|
|
3199
|
+
}
|
|
3200
|
+
}
|
|
3201
|
+
}
|
|
3202
|
+
};
|
|
3203
|
+
assertStructuralShellPolicy();
|
|
3204
|
+
const assertAdditionalShellCohort = () => {
|
|
3205
|
+
const primaryShellConfig = findById(
|
|
3206
|
+
ultramodernConfig.topology?.apps,
|
|
3207
|
+
'shell-super-app',
|
|
3208
|
+
);
|
|
3209
|
+
const overlayPorts = overlay.ports ?? {};
|
|
3210
|
+
const configuredPorts = [
|
|
3211
|
+
...Object.entries(overlayPorts).map(([id, port]) => ({ id, port })),
|
|
3212
|
+
...(!Object.hasOwn(overlayPorts, 'shell-super-app')
|
|
3213
|
+
? [{ id: 'shell-super-app', port: primaryShellConfig?.port }]
|
|
3214
|
+
: []),
|
|
3215
|
+
...expectedAdditionalShells.map(shell => ({
|
|
3216
|
+
id: shell.id,
|
|
3217
|
+
port: shell.port,
|
|
3218
|
+
})),
|
|
3219
|
+
];
|
|
3220
|
+
const portsByValue = new Map();
|
|
3221
|
+
for (const { id, port } of configuredPorts) {
|
|
3222
|
+
assert(
|
|
3223
|
+
typeof port === 'number' && Number.isFinite(port),
|
|
3224
|
+
`Configured development port for ${id} must be finite`,
|
|
3225
|
+
);
|
|
3226
|
+
const previous = portsByValue.get(port);
|
|
3227
|
+
assert(
|
|
3228
|
+
previous === undefined,
|
|
3229
|
+
`Duplicate configured development port ${port} for ${previous} and ${id}`,
|
|
3230
|
+
);
|
|
3231
|
+
portsByValue.set(port, id);
|
|
3232
|
+
}
|
|
3233
|
+
|
|
3234
|
+
if (expectedAdditionalShellIds.length === 0) {
|
|
3235
|
+
assert(
|
|
3236
|
+
ultramodernConfig.shells === undefined,
|
|
3237
|
+
'Single-shell workspace must not declare config.shells',
|
|
3238
|
+
);
|
|
3239
|
+
return;
|
|
3240
|
+
}
|
|
3241
|
+
|
|
3242
|
+
assertSameIdCohort(
|
|
3243
|
+
ultramodernConfig.shells,
|
|
3244
|
+
expectedAdditionalShellIds,
|
|
3245
|
+
`${compactConfigPath} shells`,
|
|
3246
|
+
'restore every configured additional shell',
|
|
3247
|
+
);
|
|
3248
|
+
const configuredShellRecords = ultramodernConfig.shells;
|
|
3249
|
+
const configuredShellById = new Map(
|
|
3250
|
+
configuredShellRecords.map(shell => [shell.id, shell]),
|
|
3251
|
+
);
|
|
3252
|
+
assertUniqueStrings(
|
|
3253
|
+
[
|
|
3254
|
+
primaryShellConfig?.moduleFederation?.name,
|
|
3255
|
+
...expectedAdditionalShells.map(shell => shell.mfName),
|
|
3256
|
+
],
|
|
3257
|
+
'configured shell Module Federation identities',
|
|
3258
|
+
);
|
|
3259
|
+
assertUniqueStrings(
|
|
3260
|
+
expectedAdditionalShells.map(
|
|
3261
|
+
shell => shell.deliveryUnit?.buildMarker,
|
|
3262
|
+
),
|
|
3263
|
+
'configured shell build markers',
|
|
3264
|
+
);
|
|
3265
|
+
|
|
3266
|
+
const configuredOrigins = [...portsByValue.keys()]
|
|
3267
|
+
.toSorted((left, right) => left - right)
|
|
3268
|
+
.map(port => `http://localhost:${port}`);
|
|
3269
|
+
|
|
3270
|
+
for (const shell of expectedAdditionalShells) {
|
|
3271
|
+
const configShell = configuredShellById.get(shell.id);
|
|
3272
|
+
assertObject(
|
|
3273
|
+
configShell,
|
|
3274
|
+
`${compactConfigPath} shells.${shell.id}`,
|
|
3275
|
+
'restore the generated additional-shell config record',
|
|
3276
|
+
);
|
|
3277
|
+
assertSameJson(
|
|
3278
|
+
{
|
|
3279
|
+
id: configShell.id,
|
|
3280
|
+
name: configShell.name,
|
|
3281
|
+
kind: configShell.kind,
|
|
3282
|
+
package: configShell.package,
|
|
3283
|
+
path: configShell.path,
|
|
3284
|
+
port: configShell.port,
|
|
3285
|
+
portEnv: configShell.portEnv,
|
|
3286
|
+
mfName: configShell.mfName,
|
|
3287
|
+
verticalRefs: configShell.verticalRefs,
|
|
3288
|
+
owner: configShell.owner,
|
|
3289
|
+
deliveryUnit: configShell.deliveryUnit,
|
|
3290
|
+
moduleFederation: configShell.moduleFederation,
|
|
3291
|
+
},
|
|
3292
|
+
{
|
|
3293
|
+
id: shell.id,
|
|
3294
|
+
name: shell.id.replace(/^shell-/u, ''),
|
|
3295
|
+
kind: 'shell',
|
|
3296
|
+
package: shell.packageName,
|
|
3297
|
+
path: shell.path,
|
|
3298
|
+
port: shell.port,
|
|
3299
|
+
portEnv: shell.portEnv,
|
|
3300
|
+
mfName: shell.mfName,
|
|
3301
|
+
verticalRefs: shell.verticalRefs,
|
|
3302
|
+
owner: shell.owner,
|
|
3303
|
+
deliveryUnit: shell.deliveryUnit,
|
|
3304
|
+
moduleFederation: shell.moduleFederation,
|
|
3305
|
+
},
|
|
3306
|
+
`${compactConfigPath} shells.${shell.id}`,
|
|
3307
|
+
'restore the complete additional-shell config record',
|
|
3308
|
+
);
|
|
3309
|
+
assert(
|
|
3310
|
+
!Object.hasOwn(overlay.ports ?? {}, shell.id),
|
|
3311
|
+
`${shell.id} port must stay in config.shells, not the development overlay`,
|
|
3312
|
+
);
|
|
3313
|
+
const owner = configShell.owner;
|
|
3314
|
+
assertObject(
|
|
3315
|
+
owner,
|
|
3316
|
+
`${compactConfigPath} shells.${shell.id}.owner`,
|
|
3317
|
+
'record exactly one owner for every configured Delivery Unit',
|
|
3318
|
+
);
|
|
3319
|
+
assert(
|
|
3320
|
+
['team', 'agent', 'agent-team'].includes(owner.kind) &&
|
|
3321
|
+
typeof owner.id === 'string' &&
|
|
3322
|
+
owner.id.length > 0,
|
|
3323
|
+
`${compactConfigPath} shells.${shell.id}.owner must identify one accountable owner`,
|
|
3324
|
+
);
|
|
3325
|
+
assertSameJson(
|
|
3326
|
+
owner,
|
|
3327
|
+
shell.owner,
|
|
3328
|
+
`${compactConfigPath} shells.${shell.id}.owner`,
|
|
3329
|
+
'restore the generated additional-shell owner attribution',
|
|
3330
|
+
);
|
|
3331
|
+
assertObject(
|
|
3332
|
+
configShell.deliveryUnit,
|
|
3333
|
+
`${compactConfigPath} shells.${shell.id}.deliveryUnit`,
|
|
3334
|
+
'restore the generated additional-shell Delivery Unit identity',
|
|
3335
|
+
);
|
|
3336
|
+
assert(
|
|
3337
|
+
typeof configShell.deliveryUnit.unitId === 'string' &&
|
|
3338
|
+
configShell.deliveryUnit.unitId.length > 0 &&
|
|
3339
|
+
typeof configShell.deliveryUnit.buildMarker === 'string' &&
|
|
3340
|
+
configShell.deliveryUnit.buildMarker.length > 0,
|
|
3341
|
+
`${compactConfigPath} shells.${shell.id}.deliveryUnit must carry unitId and buildMarker`,
|
|
3342
|
+
);
|
|
3343
|
+
|
|
3344
|
+
const packagePath = `${shell.path}/package.json`;
|
|
3345
|
+
const packageJson = readJson(packagePath);
|
|
3346
|
+
assert(packageJson.name === shell.packageName, `${shell.id} package name is incorrect`);
|
|
3347
|
+
assert(packageJson.modernjs?.appId === shell.id, `${shell.id} package modernjs.appId is incorrect`);
|
|
3348
|
+
assert(packageJson.modernjs?.role === 'shell', `${shell.id} package modernjs.role must be shell`);
|
|
3349
|
+
assert(packageJson.scripts?.['cloudflare:deploy'] === 'ULTRAMODERN_CLOUDFLARE_REQUIRE_PUBLIC_URLS=true pnpm run cloudflare:build && wrangler deploy --config .output/wrangler.json', `${shell.id} must expose cloudflare:deploy`);
|
|
3350
|
+
assert(packageJson.scripts?.['cloudflare:proof']?.includes(`--app ${shell.id}`), `${shell.id} must expose cloudflare:proof`);
|
|
3351
|
+
|
|
3352
|
+
const buildArtifact = readJson(`${shell.path}/shared/ultramodern-build.json`);
|
|
3353
|
+
const buildSource = readText(`${shell.path}/shared/ultramodern-build.ts`);
|
|
3354
|
+
assert(buildArtifact.deliveryUnit?.appId === shell.id, `${shell.id} build artifact appId is incorrect`);
|
|
3355
|
+
assert(buildArtifact.deliveryUnit?.buildMarker === shell.deliveryUnit?.buildMarker, `${shell.id} build marker is not participating in the build artifact`);
|
|
3356
|
+
assertSameJson(
|
|
3357
|
+
deliveryUnitBlock(configShell.deliveryUnit),
|
|
3358
|
+
deliveryUnitBlock(shell.deliveryUnit),
|
|
3359
|
+
`${compactConfigPath} shells.${shell.id}.deliveryUnit`,
|
|
3360
|
+
deliveryUnitIdentityFixArea,
|
|
3361
|
+
);
|
|
3362
|
+
assertSameJson(
|
|
3363
|
+
deliveryUnitBlock(buildArtifact.deliveryUnit),
|
|
3364
|
+
deliveryUnitBlock(shell.deliveryUnit),
|
|
3365
|
+
`${shell.path}/shared/ultramodern-build.json deliveryUnit`,
|
|
3366
|
+
deliveryUnitIdentityFixArea,
|
|
3367
|
+
);
|
|
3368
|
+
assert(
|
|
3369
|
+
buildSource.includes('export const ultramodernDeliveryUnit = ultramodernBuildArtifact.deliveryUnit;'),
|
|
3370
|
+
`${shell.path}/shared/ultramodern-build.ts must expose the shell delivery-unit identity`,
|
|
3371
|
+
);
|
|
3372
|
+
assertBuildFacadeExport(
|
|
3373
|
+
buildSource,
|
|
3374
|
+
'ultramodernUiMarker',
|
|
3375
|
+
'ultramodernBuildArtifact.surfaces.ui',
|
|
3376
|
+
`${shell.path}/shared/ultramodern-build.ts ultramodernUiMarker`,
|
|
3377
|
+
);
|
|
3378
|
+
assert(
|
|
3379
|
+
shell.degradedState?.appId === shell.id &&
|
|
3380
|
+
shell.degradedState?.status === 'degraded',
|
|
3381
|
+
`${shell.id} degraded-state contract must identify its own shell`,
|
|
3382
|
+
);
|
|
3383
|
+
|
|
3384
|
+
const modernConfig = readText(`${shell.path}/modern.config.ts`);
|
|
3385
|
+
const moduleFederationConfig = readText(
|
|
3386
|
+
`${shell.path}/module-federation.config.ts`,
|
|
3387
|
+
);
|
|
3388
|
+
const runtimeConfig = readText(`${shell.path}/src/modern.runtime.ts`);
|
|
3389
|
+
const styles = readText(`${shell.path}/src/routes/index.css`);
|
|
3390
|
+
const shellFrame = readText(`${shell.path}/src/routes/shell-frame.tsx`);
|
|
3391
|
+
const routePage = readText(`${shell.path}/src/routes/[lang]/page.tsx`);
|
|
3392
|
+
const remoteComponents = readText(
|
|
3393
|
+
`${shell.path}/src/routes/vertical-components.tsx`,
|
|
3394
|
+
);
|
|
3395
|
+
const workerRemoteComponents = readText(
|
|
3396
|
+
requiredShellWorkerCompositionPath(shell.path),
|
|
3397
|
+
);
|
|
3398
|
+
assert(modernConfig.includes(`const appId = '${shell.id}';`), `${shell.id} modern.config.ts appId is incorrect`);
|
|
3399
|
+
assert(modernConfig.includes(`const port = Number(getBuildConfigEnvironment('${shell.portEnv}') ?? ${shell.port});`), `${shell.id} modern.config.ts port is incorrect`);
|
|
3400
|
+
assert(modernConfig.includes(`uniqueName('${shell.mfName}')`), `${shell.id} modern.config.ts Rspack identity is incorrect`);
|
|
3401
|
+
assert(moduleFederationConfig.includes(`name: '${shell.mfName}'`), `${shell.id} Module Federation container name is incorrect`);
|
|
3402
|
+
assert(new RegExp(`appId:\\s*['"]${shell.id}['"]`, 'u').test(runtimeConfig), `${shell.id} runtime boundary metadata must identify its own shell`);
|
|
3403
|
+
assert(routePage.includes('ShellFrame') && routePage.includes('VerticalShowcase'), `${shell.id} route page must use its own shell composition host`);
|
|
3404
|
+
assert(remoteComponents.includes(`data-modern-boundary-id="${shell.mfName}"`), `${shell.id} remote composition boundary must use its own MF identity`);
|
|
3405
|
+
assert(workerRemoteComponents.includes('DistributedSsrBoundary'), `${shell.id} Worker SSR must use distributed fragment boundaries`);
|
|
3406
|
+
assert(!workerRemoteComponents.includes('@module-federation') && !workerRemoteComponents.includes('import('), `${shell.id} Worker SSR must not include native Module Federation runtime or remote imports`);
|
|
3407
|
+
if (tailwindEnabled) {
|
|
3408
|
+
assert(styles.includes(`prefix(${shell.tailwindPrefix})`), `${shell.id} styles must use its shell-specific Tailwind prefix`);
|
|
3409
|
+
}
|
|
3410
|
+
assert(shellFrame.includes(`${shell.tailwindPrefix}:`), `${shell.id} shell-frame must use its shell-specific Tailwind prefix`);
|
|
3411
|
+
if ((shell.verticalRefs ?? []).length > 0) {
|
|
3412
|
+
assert(
|
|
3413
|
+
shell.degradedState.required === true,
|
|
3414
|
+
`${shell.id} degraded-state contract must be required for remote consumption`,
|
|
3415
|
+
);
|
|
3416
|
+
assert(remoteComponents.includes(`${shell.tailwindPrefix}:text-red-900`), `${shell.id} degraded fallback must report its own shell identity`);
|
|
3417
|
+
assert(remoteComponents.includes('fallback: <RemoteUnavailable />'), `${shell.id} consumption points must have a degraded fallback`);
|
|
3418
|
+
assert(!remoteComponents.includes('data-modern-boundary-id="shellSuperApp"'), `${shell.id} degraded fallback must not report the primary shell`);
|
|
3419
|
+
}
|
|
3420
|
+
}
|
|
3421
|
+
|
|
3422
|
+
for (const appPath of [
|
|
3423
|
+
'apps/shell-super-app',
|
|
3424
|
+
...expectedAdditionalShells.map(shell => shell.path),
|
|
3425
|
+
...fullStackVerticals.map(vertical => vertical.path),
|
|
3426
|
+
]) {
|
|
3427
|
+
const modernConfig = readText(`${appPath}/modern.config.ts`);
|
|
3428
|
+
for (const origin of configuredOrigins) {
|
|
3429
|
+
assert(
|
|
3430
|
+
modernConfig.includes(`'${origin}'`),
|
|
3431
|
+
`${appPath} MF dev CORS must allow configured origin ${origin}`,
|
|
3432
|
+
);
|
|
3433
|
+
}
|
|
3434
|
+
assert(modernConfig.includes('credentials: false'), `${appPath} MF asset CORS must disable credentials`);
|
|
3435
|
+
assert(!modernConfig.includes('credentials: true'), `${appPath} MF asset CORS must not enable credentials`);
|
|
3436
|
+
assert(!/origin:\s*(?:true|\*|['"]\*['"])/u.test(modernConfig), `${appPath} MF dev CORS must not reflect arbitrary origins`);
|
|
3437
|
+
}
|
|
3438
|
+
|
|
3439
|
+
// Zerops artifacts exist whenever the workspace has delivery units at all
|
|
3440
|
+
// (ui-only and horizontal-remote units deploy too); a shell-only workspace
|
|
3441
|
+
// must not carry one.
|
|
3442
|
+
if (!hasDeliveryUnits) {
|
|
3443
|
+
assertNotExists('zerops.yaml');
|
|
3444
|
+
} else {
|
|
3445
|
+
assertExists('zerops.yaml');
|
|
3446
|
+
const zeropsYaml = readText('zerops.yaml');
|
|
3447
|
+
assert(
|
|
3448
|
+
zeropsYaml.includes(`setup: ${quoteYamlString('shell-super-app')}`),
|
|
3449
|
+
'shell-super-app must have a Zerops service',
|
|
3450
|
+
);
|
|
3451
|
+
for (const shell of expectedAdditionalShells) {
|
|
3452
|
+
assert(zeropsYaml.includes(`setup: ${quoteYamlString(shell.id)}`), `${shell.id} must have a Zerops service`);
|
|
3453
|
+
assert(zeropsYaml.includes(`start: cd ${quoteShellValue(`.zerops/runtime/${shell.id}`)} && npm run serve`), `${shell.id} Zerops service start command is missing`);
|
|
3454
|
+
assert(zeropsYaml.includes(` ${shell.portEnv}: ${quoteYamlString(String(shell.port))}`), `${shell.id} Zerops service port environment is missing`);
|
|
3455
|
+
}
|
|
3456
|
+
}
|
|
3457
|
+
};
|
|
3458
|
+
assertAdditionalShellCohort();
|
|
1614
3459
|
assert(
|
|
1615
3460
|
rootPackage.devDependencies?.['@modern-js/create'] ===
|
|
1616
3461
|
expectedModernPackageSpecifier('@modern-js/create'),
|
|
@@ -1621,6 +3466,11 @@ assert(
|
|
|
1621
3466
|
expectedModernPackageSpecifier('@modern-js/code-tools'),
|
|
1622
3467
|
'Root must depend on @modern-js/code-tools through package source metadata',
|
|
1623
3468
|
);
|
|
3469
|
+
assert(
|
|
3470
|
+
rootPackage.devDependencies?.['@modern-js/plugin-bff'] ===
|
|
3471
|
+
expectedModernPackageSpecifier('@modern-js/plugin-bff'),
|
|
3472
|
+
'Root must depend on @modern-js/plugin-bff for Node backend federation proof',
|
|
3473
|
+
);
|
|
1624
3474
|
if (packageSource.strategy === 'install') {
|
|
1625
3475
|
const installSpecifier = packageSource.modernPackages?.specifier;
|
|
1626
3476
|
assert(
|
|
@@ -1680,20 +3530,90 @@ if (bridgeConfig) {
|
|
|
1680
3530
|
}
|
|
1681
3531
|
assert(rootPackage.scripts?.['bridge:check'], 'Bridge workspaces must expose bridge:check');
|
|
1682
3532
|
} else {
|
|
1683
|
-
assert(rootPackage.scripts?.typecheck === 'node ./scripts/ultramodern-typecheck.mts --
|
|
3533
|
+
assert(rootPackage.scripts?.typecheck === 'node ./scripts/ultramodern-typecheck.mts --project tsconfig.json', 'Root typecheck must run TS-Go across the root project reference graph');
|
|
1684
3534
|
}
|
|
1685
3535
|
assert(rootPackage.scripts?.['contract:check'] === 'node ./scripts/validate-ultramodern-workspace.mts', 'Root must expose contract:check');
|
|
1686
3536
|
assert(rootPackage.scripts?.['api:check'] === 'node ./scripts/check-ultramodern-api-boundaries.mts', 'Root must expose api:check');
|
|
1687
3537
|
assert(rootPackage.scripts?.['i18n:boundaries'] === 'node ./scripts/check-ultramodern-i18n-boundaries.mts', 'Root must expose i18n:boundaries');
|
|
1688
3538
|
assert(rootPackage.scripts?.['performance:readiness'] === 'node ./scripts/ultramodern-performance-readiness.mts', 'Root must expose default-on performance readiness diagnostics');
|
|
3539
|
+
if (hasBackendSurfaces) {
|
|
3540
|
+
assert(rootPackage.scripts?.['node:backend-federation:generate'] === 'node ./scripts/generate-node-backend-federation.mts', 'Root must expose local Node backend federation artifact generation');
|
|
3541
|
+
assert(
|
|
3542
|
+
rootPackage.scripts?.['node:proof'] ===
|
|
3543
|
+
'node ./scripts/proof-node-backend-federation.mts',
|
|
3544
|
+
'Root must expose read-only node:proof for already-built backend federation Effect modules',
|
|
3545
|
+
);
|
|
3546
|
+
} else {
|
|
3547
|
+
assert(rootPackage.scripts?.['node:backend-federation:generate'] === undefined, 'Root must not expose backend federation generation without an API surface');
|
|
3548
|
+
assert(rootPackage.scripts?.['node:proof'] === undefined, 'Root must not expose node:proof without an API surface');
|
|
3549
|
+
}
|
|
3550
|
+
if (hasDeliveryUnits) {
|
|
3551
|
+
assert(rootPackage.scripts?.['zerops:materialize'] === 'node ./scripts/materialize-zerops-runtime.mjs', 'Root must expose Zerops runtime materialization script');
|
|
3552
|
+
assert(rootPackage.scripts?.['cloudflare:ssr-proof'] === 'node ./scripts/proof-workerd-ssr.mts', 'Root must expose workerd distributed SSR composition proof');
|
|
3553
|
+
assert(rootPackage.scripts?.['cloudflare:build']?.endsWith('&& pnpm cloudflare:ssr-proof'), 'Root Cloudflare build must finish with workerd distributed SSR composition proof');
|
|
3554
|
+
} else {
|
|
3555
|
+
assert(rootPackage.scripts?.['zerops:materialize'] === undefined, 'Root must not expose Zerops materialization in a shell-only workspace');
|
|
3556
|
+
assert(rootPackage.scripts?.['cloudflare:ssr-proof'] === undefined, 'Root must not expose workerd SSR proof in a shell-only workspace');
|
|
3557
|
+
}
|
|
3558
|
+
assertNotExists('scripts/generate-node-backend-federation.mjs');
|
|
3559
|
+
assertNotExists('scripts/proof-node-backend-federation.mjs');
|
|
3560
|
+
assertNotExists('scripts/verify-cloudflare-output.mjs');
|
|
3561
|
+
assertNotExists('scripts/generate-tanstack-routes.mjs');
|
|
1689
3562
|
assert(
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
3563
|
+
rootPackage.scripts?.check?.includes('pnpm api:check') &&
|
|
3564
|
+
!rootPackage.scripts.check.includes('pnpm node:proof') &&
|
|
3565
|
+
rootPackage.scripts.check.endsWith(
|
|
3566
|
+
bridgeConfig
|
|
3567
|
+
? '&& pnpm performance:readiness && pnpm bridge:check'
|
|
3568
|
+
: '&& pnpm performance:readiness',
|
|
3569
|
+
),
|
|
3570
|
+
'Root check must remain static while running default-on performance readiness diagnostics and bridge gates when configured',
|
|
1696
3571
|
);
|
|
3572
|
+
if (hasDeliveryUnits) {
|
|
3573
|
+
const zeropsYaml = readText('zerops.yaml');
|
|
3574
|
+
assert(zeropsYaml.includes('zerops:'), 'Zerops manifest must include zerops services');
|
|
3575
|
+
assert(zeropsYaml.includes(`setup: ${quoteYamlString('shell-super-app')}`), 'Zerops manifest must include shell service');
|
|
3576
|
+
assert(zeropsYaml.includes(`base: ${quoteYamlString('nodejs@26')}`), 'Zerops manifest must use generated Node runtime major');
|
|
3577
|
+
assert(zeropsYaml.includes('deployFiles:'), 'Zerops manifest must deploy package-pruned runtime directories');
|
|
3578
|
+
assert(
|
|
3579
|
+
zeropsYaml.includes(`start: cd ${quoteShellValue('.zerops/runtime/shell-super-app')} && npm run serve`),
|
|
3580
|
+
'Zerops shell service must start from materialized runtime package',
|
|
3581
|
+
);
|
|
3582
|
+
assert(
|
|
3583
|
+
zeropsYaml.includes('SHELL_SUPER_APP_PORT:') &&
|
|
3584
|
+
zeropsYaml.includes('ULTRAMODERN_ZEROPS_SERVICE:'),
|
|
3585
|
+
'Zerops manifest must expose service identity and ports',
|
|
3586
|
+
);
|
|
3587
|
+
for (const vertical of fullStackVerticals) {
|
|
3588
|
+
assert(zeropsYaml.includes(`setup: ${quoteYamlString(vertical.id)}`), `${vertical.id} must have a Zerops service`);
|
|
3589
|
+
assert(
|
|
3590
|
+
zeropsYaml.includes(`~/.local/bin/mise exec -- pnpm run zerops:materialize -- --app ${quoteShellValue(vertical.id)} --package ${quoteShellValue(vertical.packageName)} --package-dir ${quoteShellValue(vertical.path)}`),
|
|
3591
|
+
`${vertical.id} Zerops service must materialize its runtime package`,
|
|
3592
|
+
);
|
|
3593
|
+
// Only REST-backed (or headless-less UI) units expose an HTTP readiness
|
|
3594
|
+
// probe; the RPC surface has no REST readiness endpoint (G7a).
|
|
3595
|
+
if (!vertical.emitsApi || vertical.apiProtocol === 'rest') {
|
|
3596
|
+
const zeropsReadinessPath = vertical.emitsApi
|
|
3597
|
+
? `${vertical.apiPrefix}/${vertical.stem}/readiness`
|
|
3598
|
+
: '/';
|
|
3599
|
+
assert(
|
|
3600
|
+
zeropsYaml.includes(`path: ${quoteYamlString(zeropsReadinessPath)}`),
|
|
3601
|
+
`${vertical.id} BFF readiness path must use generated API prefix`,
|
|
3602
|
+
);
|
|
3603
|
+
}
|
|
3604
|
+
}
|
|
3605
|
+
const zeropsMaterializer = readText('scripts/materialize-zerops-runtime.mjs');
|
|
3606
|
+
assert(zeropsMaterializer.includes("MODERNJS_DEPLOY: 'node'"), 'Zerops materializer must use Modern.js Node deploy target');
|
|
3607
|
+
assert(/'deploy',\s*'--skip-build'/.test(zeropsMaterializer), 'Zerops materializer must pass --skip-build directly to Modern deploy');
|
|
3608
|
+
assert(zeropsMaterializer.includes('normalizeRuntimePackageDependencies'), 'Zerops materializer must normalize generated Modern package aliases before installing dependencies');
|
|
3609
|
+
assert(zeropsMaterializer.includes('officialPackageName'), 'Zerops materializer must add official Modern.js npm aliases for generated runtime imports');
|
|
3610
|
+
assert(zeropsMaterializer.includes('installRuntimeDependencies'), 'Zerops materializer must install dependencies outside the workspace copy runtime node_modules');
|
|
3611
|
+
assert(zeropsMaterializer.includes('copyWorkspacePackage'), 'Zerops materializer must preserve generated local workspace package dependencies');
|
|
3612
|
+
assert(zeropsMaterializer.includes('makeWorkspacePackageRuntimeSafe'), 'Zerops materializer must rewrite copied workspace packages to runtime-safe JavaScript exports');
|
|
3613
|
+
assert(zeropsMaterializer.includes("serve: runtimePackage.scripts?.serve ?? 'node index.js'"), 'Zerops materializer must preserve runtime serve script fallback');
|
|
3614
|
+
assert(/'install',\s*'--omit=dev'/.test(zeropsMaterializer), 'Zerops materializer must omit dev dependencies from runtime installs');
|
|
3615
|
+
assert(zeropsMaterializer.includes("'--legacy-peer-deps'"), 'Zerops materializer must tolerate generated-workspace peer dependency ranges in npm runtime install');
|
|
3616
|
+
}
|
|
1697
3617
|
const performanceReadinessConfig = readText('scripts/ultramodern-performance-readiness.config.mjs');
|
|
1698
3618
|
const assertToolWrapper = (scriptPath, command) => {
|
|
1699
3619
|
const source = readText(scriptPath);
|
|
@@ -1705,7 +3625,6 @@ const assertToolWrapper = (scriptPath, command) => {
|
|
|
1705
3625
|
assert(performanceReadinessConfig.includes('UltramodernPerformanceReadinessDiagnosticsConfig'), 'Performance readiness config must carry the typed opt-out surface');
|
|
1706
3626
|
assert(performanceReadinessConfig.includes('enabled: true'), 'Performance readiness diagnostics must be default-on');
|
|
1707
3627
|
assert(performanceReadinessConfig.includes("failOn: 'framework-invariant'"), 'Performance readiness diagnostics must only fail framework invariants by default');
|
|
1708
|
-
assertToolWrapper('scripts/validate-ultramodern-workspace.mts', 'validate');
|
|
1709
3628
|
assertToolWrapper('scripts/ultramodern-performance-readiness.mts', 'performance-readiness');
|
|
1710
3629
|
const i18nBoundaryScript = readText('scripts/check-ultramodern-i18n-boundaries.mts');
|
|
1711
3630
|
assertToolWrapper('scripts/ultramodern-typecheck.mts', 'typecheck');
|
|
@@ -1720,12 +3639,18 @@ assert(rootPackage.scripts?.['cloudflare:proof'] === 'node ./scripts/proof-cloud
|
|
|
1720
3639
|
assert(rootPackage.scripts?.['migrate:strict-effect'] === 'node ./scripts/migrate-strict-effect.mts', 'Root must expose migrate:strict-effect');
|
|
1721
3640
|
assert(rootPackage.scripts?.['skills:install'] === 'node ./scripts/bootstrap-agent-skills.mts', 'Root must expose skills:install');
|
|
1722
3641
|
assert(rootPackage.scripts?.['skills:check'] === 'node ./scripts/bootstrap-agent-skills.mts --check', 'Root must expose skills:check');
|
|
1723
|
-
assert(rootPackage.scripts?.postinstall === "
|
|
3642
|
+
assert(rootPackage.scripts?.postinstall === "node ./scripts/bootstrap-agent-skills.mts --postinstall && oxfmt . '!repos/**'", 'Root postinstall must run the default-on Codex skills bootstrap, format installed skills, and leave reference repository installs explicit');
|
|
1724
3643
|
assert(rootPackage.scripts?.['agents:refs:install'] === 'node ./scripts/setup-agent-reference-repos.mts', 'Root must expose agents:refs:install as the explicit reference repo installer');
|
|
1725
3644
|
const agentSkillsBootstrap = readText('scripts/bootstrap-agent-skills.mts');
|
|
1726
3645
|
assertToolWrapper('scripts/assert-mf-types.mts', 'mf-types');
|
|
3646
|
+
if (hasBackendSurfaces) {
|
|
3647
|
+
assertToolWrapper('scripts/generate-node-backend-federation.mts', 'backend-federation-generate');
|
|
3648
|
+
assertToolWrapper('scripts/proof-node-backend-federation.mts', 'backend-federation-proof');
|
|
3649
|
+
}
|
|
1727
3650
|
assertToolWrapper('scripts/generate-public-surface-assets.mts', 'public-surface');
|
|
3651
|
+
assertToolWrapper('scripts/generate-tanstack-routes.mts', 'routes-generate');
|
|
1728
3652
|
assertToolWrapper('scripts/proof-cloudflare-version.mts', 'cloudflare-proof');
|
|
3653
|
+
assertToolWrapper('scripts/verify-cloudflare-output.mts', 'cloudflare-output-verify');
|
|
1729
3654
|
assertToolWrapper('scripts/migrate-strict-effect.mts', 'migrate-strict-effect');
|
|
1730
3655
|
assertToolWrapper('scripts/bootstrap-agent-skills.mts', 'skills');
|
|
1731
3656
|
assert(!agentSkillsBootstrap.includes("run('brew'") && !agentSkillsBootstrap.includes('runShell('), 'Agent skills bootstrap must never invoke system package managers');
|
|
@@ -1735,10 +3660,9 @@ assert(agentReferenceRepoSetup.includes("commitInstallerChanges('Initialize Ultr
|
|
|
1735
3660
|
assert(agentReferenceRepoSetup.includes("commitInstallerChanges('Record agent reference repo manifest')"), 'Agent reference repo manifest commit must use the installer commit helper');
|
|
1736
3661
|
|
|
1737
3662
|
const expectedAppIds = ['shell-super-app', ...fullStackVerticals.map(vertical => vertical.id)];
|
|
1738
|
-
const expectedCloudflareCompatibilityDate = '{{cloudflareCompatibilityDate}}';
|
|
1739
3663
|
const expectedCloudflareCompatibilityFlags = ['nodejs_compat', 'global_fetch_strictly_public'];
|
|
1740
3664
|
assert(
|
|
1741
|
-
|
|
3665
|
+
sameJson(generatedContract.apps?.map(app => app.id), expectedAppIds),
|
|
1742
3666
|
'Generated contract must contain shell plus the full-stack verticals',
|
|
1743
3667
|
);
|
|
1744
3668
|
assert(generatedContract.cssFederation?.sharedDesignTokens?.owner?.id === 'shared-design-tokens', 'CSS federation must declare shared design token ownership');
|
|
@@ -1763,38 +3687,57 @@ assert(generatedContract.performanceReadiness?.mode === 'diagnostic', 'Performan
|
|
|
1763
3687
|
assert(generatedContract.performanceReadiness?.report?.script === 'scripts/ultramodern-performance-readiness.mts', 'Performance readiness contract must point at the generated script');
|
|
1764
3688
|
assert(generatedContract.performanceReadiness?.report?.deterministic === true, 'Performance readiness reports must be deterministic');
|
|
1765
3689
|
assert(generatedContract.performanceReadiness?.optOut?.env === 'ULTRAMODERN_PERFORMANCE_READINESS_DIAGNOSTICS=false', 'Performance readiness env opt-out is incorrect');
|
|
1766
|
-
assert(
|
|
3690
|
+
assert(sameJson(generatedContract.performanceReadiness?.signals?.map(signal => signal.id), expectedPerformanceReadinessSignals), 'Performance readiness signal ids are incorrect');
|
|
1767
3691
|
|
|
1768
3692
|
const shellModernConfig = readText('apps/shell-super-app/modern.config.ts');
|
|
1769
3693
|
const shellModuleFederationConfig = readText('apps/shell-super-app/module-federation.config.ts');
|
|
3694
|
+
const shellModernAppEnv = readText('apps/shell-super-app/src/modern-app-env.d.ts');
|
|
3695
|
+
const gitignore = readText('.gitignore');
|
|
1770
3696
|
const shellRouteHead = readText('apps/shell-super-app/src/routes/ultramodern-route-head.tsx');
|
|
1771
3697
|
const shellRouteMetadata = readText('apps/shell-super-app/src/routes/ultramodern-route-metadata.ts');
|
|
3698
|
+
assert(/^\.mf\/$/mu.test(gitignore), 'Generated .gitignore must ignore root Module Federation diagnostics');
|
|
3699
|
+
assert(/^\*\*\/\.mf\/$/mu.test(gitignore), 'Generated .gitignore must ignore per-app Module Federation diagnostics');
|
|
3700
|
+
assert(/^dist-cloudflare\/$/mu.test(gitignore), 'Generated .gitignore must ignore Cloudflare build output');
|
|
3701
|
+
assert(/^\.output\/$/mu.test(gitignore), 'Generated .gitignore must ignore root final deployment output');
|
|
3702
|
+
assert(/^\*\*\/\.output\/$/mu.test(gitignore), 'Generated .gitignore must ignore per-app final deployment output');
|
|
3703
|
+
assert(/^\*\*\/src\/modern-tanstack\/$/mu.test(gitignore), 'Generated .gitignore must ignore framework-owned TanStack router output');
|
|
3704
|
+
assert(/^\*\*\/\.tsgo\.\*\.resolved\.json$/mu.test(gitignore), 'Generated .gitignore must ignore transient TS-Go resolution output');
|
|
3705
|
+
assert(shellModernAppEnv.includes('/// <reference types="@modern-js/app-tools/types" />'), 'Shell app env must reference the framework-owned app ambient type bundle while remaining an ambient declaration file');
|
|
3706
|
+
assert(/declare const ULTRAMODERN_SITE_URL: string;/u.test(shellModernAppEnv), 'Shell app env must keep generated globals explicit in ambient scope');
|
|
3707
|
+
assert(!shellModernAppEnv.includes("declare module '*.svg'"), 'Shell app env must not redeclare framework-owned svg asset modules');
|
|
3708
|
+
assert(!shellModernAppEnv.includes("declare module '*.css'"), 'Shell app env must not redeclare framework-owned css asset modules');
|
|
1772
3709
|
assert(shellRouteMetadata.includes('@generated by @modern-js/create'), 'Shell route metadata compatibility manifest must be marked generated');
|
|
1773
3710
|
assert(shellRouteMetadata.includes("authoring: 'colocated-route-meta'"), 'Shell route metadata manifest must advertise colocated authoring');
|
|
1774
3711
|
const expectedZephyrDependencies = Object.fromEntries(
|
|
1775
|
-
|
|
1776
|
-
vertical.
|
|
1777
|
-
|
|
1778
|
-
|
|
3712
|
+
expectedPrimaryShellVerticalIds.map(verticalId => {
|
|
3713
|
+
const vertical = fullStackVerticals.find(
|
|
3714
|
+
candidate => candidate.id === verticalId,
|
|
3715
|
+
);
|
|
3716
|
+
assert(vertical, `Missing primary-shell vertical ${verticalId}`);
|
|
3717
|
+
return [
|
|
3718
|
+
vertical.zephyrAlias,
|
|
3719
|
+
`${vertical.packageName}@workspace:*`,
|
|
3720
|
+
];
|
|
3721
|
+
}),
|
|
1779
3722
|
);
|
|
1780
3723
|
assert(
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
'Shell Zephyr dependencies must reference every vertical package',
|
|
3724
|
+
sameJson(shellPackage['zephyr:dependencies'], expectedZephyrDependencies),
|
|
3725
|
+
'Shell Zephyr dependencies must reference every primary-shell vertical package',
|
|
1784
3726
|
);
|
|
1785
3727
|
assert(shellPackage.devDependencies?.['@modern-js/app-tools'] === expectedModernPackageSpecifier('@modern-js/app-tools'), 'Shell app-tools dependency must match package source metadata');
|
|
1786
3728
|
assert(shellPackage.dependencies?.['@modern-js/plugin-bff'] === expectedModernPackageSpecifier('@modern-js/plugin-bff'), 'Shell plugin-bff dependency must match package source metadata');
|
|
1787
3729
|
assert(shellPackage.dependencies?.['@modern-js/plugin-i18n'] === expectedModernPackageSpecifier('@modern-js/plugin-i18n'), 'Shell plugin-i18n dependency must match package source metadata');
|
|
1788
3730
|
assert(shellPackage.dependencies?.['@modern-js/plugin-tanstack'] === expectedModernPackageSpecifier('@modern-js/plugin-tanstack'), 'Shell plugin-tanstack dependency must match package source metadata');
|
|
1789
3731
|
assert(shellPackage.dependencies?.['@modern-js/runtime'] === expectedModernPackageSpecifier('@modern-js/runtime'), 'Shell runtime dependency must match package source metadata');
|
|
1790
|
-
|
|
3732
|
+
assert(shellPackage.scripts?.['cloudflare:deploy'] === 'ULTRAMODERN_CLOUDFLARE_REQUIRE_PUBLIC_URLS=true pnpm run cloudflare:build && wrangler deploy --config .output/wrangler.json', 'Shell must expose cloudflare:deploy');
|
|
3733
|
+
assertTargetIsolatedBuildArtifacts('shell-super-app', shellModernConfig);
|
|
1791
3734
|
assertCloudflareBuildSkipsDeployRebuild('shell-super-app', shellPackage);
|
|
1792
3735
|
const shellContract = generatedContract.apps?.find(app => app.id === 'shell-super-app');
|
|
1793
3736
|
assert(shellContract?.deploy?.cloudflare?.workerName === expectedWorkerName('shell-super-app'), 'Shell Cloudflare workerName is incorrect');
|
|
1794
3737
|
assert(shellContract?.deploy?.cloudflare?.publicUrlEnv === 'ULTRAMODERN_PUBLIC_URL_SHELL_SUPER_APP', 'Shell Cloudflare public URL env is incorrect');
|
|
1795
3738
|
assert(shellContract?.deploy?.cloudflare?.compatibilityDate === expectedCloudflareCompatibilityDate, 'Shell Cloudflare compatibilityDate is incorrect');
|
|
1796
|
-
assert(
|
|
1797
|
-
assert(
|
|
3739
|
+
assert(sameJson(shellContract?.deploy?.cloudflare?.compatibilityFlags, expectedCloudflareCompatibilityFlags), 'Shell Cloudflare compatibility flags are incorrect');
|
|
3740
|
+
assert(sameJson(shellContract?.deploy?.cloudflare?.security, expectedCloudflareSecurity), 'Shell Cloudflare security contract is incorrect');
|
|
1798
3741
|
assertCloudflareQualityGates('shell-super-app', shellContract?.deploy?.cloudflare?.qualityGates);
|
|
1799
3742
|
assert(shellContract?.deploy?.worker?.compatibilityDate === expectedCloudflareCompatibilityDate, 'Shell worker compatibilityDate is incorrect');
|
|
1800
3743
|
assert(shellContract?.deploy?.worker?.name === expectedWorkerName('shell-super-app'), 'Shell worker name is incorrect');
|
|
@@ -1813,15 +3756,15 @@ assert(shellModernConfig.includes("assetPrefix: '/'"), 'Shell modern.config.ts m
|
|
|
1813
3756
|
assert(shellModernConfig.includes('assetPrefix,'), 'Shell modern.config.ts must wire output.assetPrefix to the derived asset prefix');
|
|
1814
3757
|
assert(shellContract?.config?.dev?.assetPrefix === '/', 'Shell dev asset prefix must stay origin-relative');
|
|
1815
3758
|
assert(shellContract?.config?.output?.assetPrefix?.default === '/', 'Shell asset prefix must default to origin-relative paths');
|
|
1816
|
-
assert(
|
|
3759
|
+
assert(sameJson(shellContract?.config?.output?.assetPrefix?.envFallbackOrder, ['MODERN_ASSET_PREFIX', 'ULTRAMODERN_ASSET_PREFIX']), 'Shell asset prefix env fallback order is incorrect');
|
|
1817
3760
|
assert(shellContract?.config?.output?.disableTsChecker === false, 'Shell must keep the framework TypeScript checker enabled');
|
|
1818
3761
|
assert(shellContract?.config?.performance?.readinessDiagnostics?.default === 'enabled', 'Shell performance readiness diagnostics must be default-on');
|
|
1819
3762
|
assert(shellContract?.config?.performance?.readinessDiagnostics?.failOn === 'framework-invariant', 'Shell performance readiness diagnostics must only fail framework invariants by default');
|
|
1820
3763
|
assert(shellContract?.config?.performance?.readinessDiagnostics?.optOut?.env === 'ULTRAMODERN_PERFORMANCE_READINESS_DIAGNOSTICS=false', 'Shell performance readiness env opt-out is incorrect');
|
|
1821
|
-
assert(
|
|
3764
|
+
assert(sameJson(shellContract?.config?.source?.siteUrl?.envFallbackOrder, ['MODERN_PUBLIC_SITE_URL', 'ULTRAMODERN_PUBLIC_URL_SHELL_SUPER_APP', 'ULTRAMODERN_CLOUDFLARE_WORKERS_DEV_SUBDOMAIN', 'SHELL_SUPER_APP_PORT']), 'Shell site URL env fallback order is incorrect');
|
|
1822
3765
|
assert(shellContract?.config?.rspack?.output?.uniqueName === 'shellSuperApp', 'Shell Rspack uniqueName is incorrect');
|
|
1823
3766
|
assert(shellContract?.config?.rspack?.output?.chunkLoadingGlobal === expectedChunkLoadingGlobal('shellSuperApp'), 'Shell Rspack chunkLoadingGlobal is incorrect');
|
|
1824
|
-
assert(shellContract?.moduleFederation?.dts?.compilerInstance === 'tsgo', 'Shell must keep mandatory DTS compiler');
|
|
3767
|
+
assert(shellContract?.moduleFederation?.dts?.compilerInstance === 'effect-tsgo', 'Shell must keep mandatory DTS compiler');
|
|
1825
3768
|
assert(shellContract?.moduleFederation?.dts?.tsConfigPath === './tsconfig.mf-types.json', 'Shell must keep dedicated Module Federation DTS tsconfig');
|
|
1826
3769
|
assert(shellModuleFederationConfig.includes("tsConfigPath: './tsconfig.mf-types.json'"), 'Shell Module Federation config must use the dedicated DTS tsconfig');
|
|
1827
3770
|
assert(topology.shell?.cloudflare?.workerName === expectedWorkerName('shell-super-app'), 'Shell topology Cloudflare workerName is incorrect');
|
|
@@ -1839,7 +3782,7 @@ assert(shellContract?.routes?.privateByDefault === true, 'Shell routes must be p
|
|
|
1839
3782
|
assert(shellContract?.routes?.metadataAuthoring === 'colocated-route-meta', 'Shell route metadata authoring mode is incorrect');
|
|
1840
3783
|
assert(shellContract?.routes?.generatedManifest === true, 'Shell route metadata manifest must be generated');
|
|
1841
3784
|
assert(shellContract?.routes?.publicnessDefault === 'private-app-screen', 'Shell route publicness default is incorrect');
|
|
1842
|
-
assert(
|
|
3785
|
+
assert(sameJson(shellContract?.routes?.publicRoutes ?? [], []), 'Shell must not expose generated public routes by default');
|
|
1843
3786
|
assertPublicHeadContract('shell-super-app', shellContract?.routes?.publicHead, shellRouteHead);
|
|
1844
3787
|
assertPublicSurfaceContract('shell-super-app', shellContract?.routes?.publicSurface);
|
|
1845
3788
|
assert(
|
|
@@ -1848,7 +3791,7 @@ assert(
|
|
|
1848
3791
|
);
|
|
1849
3792
|
assertPublicSurfaceAssets('apps/shell-super-app', shellContract?.routes?.publicRoutes ?? []);
|
|
1850
3793
|
assert(
|
|
1851
|
-
topology.shell?.verticalRefs?.join(',') ===
|
|
3794
|
+
topology.shell?.verticalRefs?.join(',') === expectedPrimaryShellVerticalIds.join(','),
|
|
1852
3795
|
'Topology shell verticalRefs must match generated verticals',
|
|
1853
3796
|
);
|
|
1854
3797
|
assert(topology.verticals?.length === fullStackVerticals.length, 'Topology must contain only generated verticals');
|
|
@@ -1858,23 +3801,104 @@ assert(!('effectServices' in topology), 'Default APIs must be vertical-owned, no
|
|
|
1858
3801
|
for (const vertical of fullStackVerticals) {
|
|
1859
3802
|
const packageJson = readJson(`${vertical.path}/package.json`);
|
|
1860
3803
|
const modernConfig = readText(`${vertical.path}/modern.config.ts`);
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
const
|
|
1864
|
-
|
|
1865
|
-
|
|
3804
|
+
// The browser Module Federation config and colocated route surfaces only
|
|
3805
|
+
// exist for UI-emitting units; a headless api-only unit emits none (G2a/P4).
|
|
3806
|
+
const moduleFederationConfig = vertical.emitsUi
|
|
3807
|
+
? readText(`${vertical.path}/module-federation.config.ts`)
|
|
3808
|
+
: '';
|
|
3809
|
+
const modernAppEnv = readText(`${vertical.path}/src/modern-app-env.d.ts`);
|
|
3810
|
+
const routeHead = vertical.emitsUi
|
|
3811
|
+
? readText(`${vertical.path}/src/routes/ultramodern-route-head.tsx`)
|
|
3812
|
+
: '';
|
|
3813
|
+
const routeMetadata = vertical.emitsUi
|
|
3814
|
+
? readText(`${vertical.path}/src/routes/ultramodern-route-metadata.ts`)
|
|
3815
|
+
: '';
|
|
3816
|
+
const ultramodernBuildSource = readText(`${vertical.path}/shared/ultramodern-build.ts`);
|
|
3817
|
+
const ultramodernBuildArtifact = readJson(`${vertical.path}/shared/ultramodern-build.json`);
|
|
3818
|
+
if (vertical.deliveryUnit) {
|
|
3819
|
+
const topologyEntry = findById(topology.verticals, vertical.id);
|
|
3820
|
+
const expectedDeliveryUnit = deliveryUnitBlock(
|
|
3821
|
+
expectedDeliveryUnitFor(vertical, topologyEntry),
|
|
3822
|
+
);
|
|
3823
|
+
const buildLabel = `${vertical.path}/shared/ultramodern-build.json deliveryUnit`;
|
|
3824
|
+
assertSelfCheck(
|
|
3825
|
+
ultramodernBuildSource.includes('export const ultramodernDeliveryUnit'),
|
|
3826
|
+
buildLabel,
|
|
3827
|
+
'Missing ultramodernDeliveryUnit export',
|
|
3828
|
+
deliveryUnitIdentityFixArea,
|
|
3829
|
+
);
|
|
3830
|
+
const buildIdentity = ultramodernBuildArtifact.deliveryUnit ?? {};
|
|
3831
|
+
assertSelfCheck(
|
|
3832
|
+
buildIdentity.buildMarker === expectedDeliveryUnit.buildMarker,
|
|
3833
|
+
buildLabel,
|
|
3834
|
+
`Expected build "${expectedDeliveryUnit.buildMarker}", found ${formatJson(buildIdentity.buildMarker)}`,
|
|
3835
|
+
deliveryUnitIdentityFixArea,
|
|
3836
|
+
);
|
|
3837
|
+
assertSelfCheck(
|
|
3838
|
+
buildIdentity.unitId === expectedDeliveryUnit.unitId,
|
|
3839
|
+
buildLabel,
|
|
3840
|
+
`Expected unitId "${expectedDeliveryUnit.unitId}", found ${formatJson(buildIdentity.unitId)}`,
|
|
3841
|
+
deliveryUnitIdentityFixArea,
|
|
3842
|
+
);
|
|
3843
|
+
assertSelfCheck(
|
|
3844
|
+
buildIdentity.packageName === expectedDeliveryUnit.packageName,
|
|
3845
|
+
buildLabel,
|
|
3846
|
+
`Expected packageName "${expectedDeliveryUnit.packageName}", found ${formatJson(buildIdentity.packageName)}`,
|
|
3847
|
+
deliveryUnitIdentityFixArea,
|
|
3848
|
+
);
|
|
3849
|
+
assertSelfCheck(
|
|
3850
|
+
buildIdentity.version === expectedDeliveryUnit.version,
|
|
3851
|
+
buildLabel,
|
|
3852
|
+
`Expected version "${expectedDeliveryUnit.version}", found ${formatJson(buildIdentity.version)}`,
|
|
3853
|
+
deliveryUnitIdentityFixArea,
|
|
3854
|
+
);
|
|
3855
|
+
assertBuildFacadeExport(
|
|
3856
|
+
ultramodernBuildSource,
|
|
3857
|
+
'ultramodernUiMarker',
|
|
3858
|
+
'ultramodernBuildArtifact.surfaces.ui',
|
|
3859
|
+
`${vertical.path}/shared/ultramodern-build.ts ultramodernUiMarker`,
|
|
3860
|
+
);
|
|
3861
|
+
assertBuildFacadeExport(
|
|
3862
|
+
ultramodernBuildSource,
|
|
3863
|
+
'ultramodernApiMarker',
|
|
3864
|
+
'ultramodernBuildArtifact.surfaces.api',
|
|
3865
|
+
`${vertical.path}/shared/ultramodern-build.ts ultramodernApiMarker`,
|
|
3866
|
+
);
|
|
3867
|
+
}
|
|
3868
|
+
assert(modernAppEnv.includes('/// <reference types="@modern-js/app-tools/types" />'), `${vertical.id} app env must reference the framework-owned app ambient type bundle while remaining an ambient declaration file`);
|
|
3869
|
+
assert(/declare const ULTRAMODERN_SITE_URL: string;/u.test(modernAppEnv), `${vertical.id} app env must keep generated globals explicit in ambient scope`);
|
|
3870
|
+
assert(!modernAppEnv.includes("declare module '*.svg'"), `${vertical.id} app env must not redeclare framework-owned svg asset modules`);
|
|
3871
|
+
assert(!modernAppEnv.includes("declare module '*.css'"), `${vertical.id} app env must not redeclare framework-owned css asset modules`);
|
|
3872
|
+
if (vertical.emitsUi) {
|
|
3873
|
+
assert(routeMetadata.includes('@generated by @modern-js/create'), `${vertical.id} route metadata compatibility manifest must be marked generated`);
|
|
3874
|
+
assert(routeMetadata.includes("authoring: 'colocated-route-meta'"), `${vertical.id} route metadata manifest must advertise colocated authoring`);
|
|
3875
|
+
}
|
|
1866
3876
|
assert(packageJson.name === vertical.packageName, `${vertical.id} package name is incorrect`);
|
|
1867
3877
|
assert(packageJson.scripts?.['cloudflare:deploy'] === 'ULTRAMODERN_CLOUDFLARE_REQUIRE_PUBLIC_URLS=true pnpm run cloudflare:build && wrangler deploy --config .output/wrangler.json', `${vertical.id} must expose cloudflare:deploy`);
|
|
1868
|
-
|
|
3878
|
+
assertTargetIsolatedBuildArtifacts(vertical.id, modernConfig);
|
|
1869
3879
|
assertCloudflareBuildSkipsDeployRebuild(vertical.id, packageJson);
|
|
1870
3880
|
assert(packageJson.scripts?.['cloudflare:proof']?.includes(`--app ${vertical.id}`), `${vertical.id} must expose cloudflare:proof`);
|
|
1871
3881
|
assert(packageJson.devDependencies?.['@modern-js/app-tools'] === expectedModernPackageSpecifier('@modern-js/app-tools'), `${vertical.id} app-tools dependency must match package source metadata`);
|
|
1872
|
-
|
|
3882
|
+
if (vertical.emitsApi) {
|
|
3883
|
+
assert(packageJson.dependencies?.['@modern-js/plugin-bff'] === expectedModernPackageSpecifier('@modern-js/plugin-bff'), `${vertical.id} plugin-bff dependency must match package source metadata`);
|
|
3884
|
+
}
|
|
1873
3885
|
assert(packageJson.dependencies?.['@modern-js/plugin-i18n'] === expectedModernPackageSpecifier('@modern-js/plugin-i18n'), `${vertical.id} plugin-i18n dependency must match package source metadata`);
|
|
1874
3886
|
assert(packageJson.dependencies?.['@modern-js/plugin-tanstack'] === expectedModernPackageSpecifier('@modern-js/plugin-tanstack'), `${vertical.id} plugin-tanstack dependency must match package source metadata`);
|
|
1875
3887
|
assert(packageJson.dependencies?.['@modern-js/runtime'] === expectedModernPackageSpecifier('@modern-js/runtime'), `${vertical.id} runtime dependency must match package source metadata`);
|
|
1876
|
-
|
|
1877
|
-
|
|
3888
|
+
if (vertical.emitsApi) {
|
|
3889
|
+
assert(packageJson.exports?.[vertical.apiClientExport] === `./${vertical.apiClientPath}`, `${vertical.id} must export its API client`);
|
|
3890
|
+
assert(packageJson.exports?.['./api'] === `./${vertical.apiContractPath}`, `${vertical.id} must export its API contract`);
|
|
3891
|
+
// API protocol exclusivity (G7a): an RPC unit ships only the RPC contract
|
|
3892
|
+
// and `${stem}-rpc-client`; a REST unit ships only the REST contract and
|
|
3893
|
+
// `${stem}-client`. Neither may carry the other protocol's surface.
|
|
3894
|
+
if (vertical.apiProtocol === 'rpc') {
|
|
3895
|
+
assert(!fs.existsSync(path.join(root, `${vertical.path}/src/api/${vertical.stem}-client.ts`)), `${vertical.id} RPC unit must not emit the REST API client`);
|
|
3896
|
+
assert(!fs.existsSync(path.join(root, `${vertical.path}/shared/api.ts`)), `${vertical.id} RPC unit must not emit the REST API contract`);
|
|
3897
|
+
} else {
|
|
3898
|
+
assert(!fs.existsSync(path.join(root, `${vertical.path}/src/api/${vertical.stem}-rpc-client.ts`)), `${vertical.id} REST unit must not emit the RPC API client`);
|
|
3899
|
+
assert(!fs.existsSync(path.join(root, `${vertical.path}/shared/rpc.ts`)), `${vertical.id} REST unit must not emit the RPC API contract`);
|
|
3900
|
+
}
|
|
3901
|
+
}
|
|
1878
3902
|
const expectedVerticalZephyrDependencies = Object.fromEntries(
|
|
1879
3903
|
fullStackVerticals
|
|
1880
3904
|
.filter(candidate => vertical.verticalRefs.includes(candidate.id))
|
|
@@ -1884,8 +3908,7 @@ for (const vertical of fullStackVerticals) {
|
|
|
1884
3908
|
]),
|
|
1885
3909
|
);
|
|
1886
3910
|
assert(
|
|
1887
|
-
|
|
1888
|
-
JSON.stringify(expectedVerticalZephyrDependencies),
|
|
3911
|
+
sameJson(packageJson['zephyr:dependencies'], expectedVerticalZephyrDependencies),
|
|
1889
3912
|
`${vertical.id} Zephyr dependencies must match declared vertical refs`,
|
|
1890
3913
|
);
|
|
1891
3914
|
|
|
@@ -1895,8 +3918,8 @@ for (const vertical of fullStackVerticals) {
|
|
|
1895
3918
|
assert(contractEntry?.deploy?.cloudflare?.workerName === expectedWorkerName(vertical.id), `${vertical.id} Cloudflare workerName is incorrect`);
|
|
1896
3919
|
assert(contractEntry?.deploy?.cloudflare?.publicUrlEnv === `ULTRAMODERN_PUBLIC_URL_${vertical.id.replace(/-/g, '_').toUpperCase()}`, `${vertical.id} Cloudflare public URL env is incorrect`);
|
|
1897
3920
|
assert(contractEntry?.deploy?.cloudflare?.compatibilityDate === expectedCloudflareCompatibilityDate, `${vertical.id} Cloudflare compatibilityDate is incorrect`);
|
|
1898
|
-
assert(
|
|
1899
|
-
assert(
|
|
3921
|
+
assert(sameJson(contractEntry?.deploy?.cloudflare?.compatibilityFlags, expectedCloudflareCompatibilityFlags), `${vertical.id} Cloudflare compatibility flags are incorrect`);
|
|
3922
|
+
assert(sameJson(contractEntry?.deploy?.cloudflare?.security, expectedCloudflareSecurity), `${vertical.id} Cloudflare security contract is incorrect`);
|
|
1900
3923
|
assertCloudflareQualityGates(vertical.id, contractEntry?.deploy?.cloudflare?.qualityGates);
|
|
1901
3924
|
assert(contractEntry?.deploy?.worker?.compatibilityDate === expectedCloudflareCompatibilityDate, `${vertical.id} worker compatibilityDate is incorrect`);
|
|
1902
3925
|
assert(contractEntry?.deploy?.worker?.name === expectedWorkerName(vertical.id), `${vertical.id} worker name is incorrect`);
|
|
@@ -1912,37 +3935,64 @@ for (const vertical of fullStackVerticals) {
|
|
|
1912
3935
|
assert(!verticalAssetPrefixExpression.includes('configuredSiteUrl') && !verticalAssetPrefixExpression.includes('MODERN_PUBLIC_SITE_URL'), `${vertical.id} asset prefix must not fall back to MODERN_PUBLIC_SITE_URL`);
|
|
1913
3936
|
assert(modernConfig.includes(`envValue('ULTRAMODERN_PUBLIC_URL_${vertical.id.replace(/-/g, '_').toUpperCase()}')`), `${vertical.id} asset prefix must read its per-app public URL`);
|
|
1914
3937
|
assert(modernConfig.includes('inferredCloudflareUrl'), `${vertical.id} asset prefix must support workers.dev origin inference`);
|
|
1915
|
-
assert(
|
|
3938
|
+
assert(/dev:\s*\{[\s\S]*?\/\/ Remote dev manifests must publish an absolute publicPath[\s\S]*?assetPrefix,/u.test(modernConfig), `${vertical.id} modern.config.ts must publish dev assets from its own remote origin`);
|
|
1916
3939
|
assert(modernConfig.includes('assetPrefix,'), `${vertical.id} modern.config.ts must wire output.assetPrefix to the derived asset prefix`);
|
|
1917
|
-
assert(contractEntry?.config?.dev?.assetPrefix === '
|
|
3940
|
+
assert(contractEntry?.config?.dev?.assetPrefix === 'app-public-origin', `${vertical.id} dev asset prefix must default to its app public origin`);
|
|
1918
3941
|
assert(contractEntry?.config?.output?.assetPrefix?.default === 'app-public-origin', `${vertical.id} asset prefix must default to its app public origin`);
|
|
1919
|
-
assert(
|
|
3942
|
+
assert(sameJson(contractEntry?.config?.output?.assetPrefix?.envFallbackOrder, ['MODERN_ASSET_PREFIX', 'ULTRAMODERN_ASSET_PREFIX']), `${vertical.id} asset prefix env fallback order is incorrect`);
|
|
1920
3943
|
assert(contractEntry?.config?.output?.disableTsChecker === false, `${vertical.id} must keep the framework TypeScript checker enabled`);
|
|
1921
3944
|
assert(contractEntry?.config?.performance?.readinessDiagnostics?.default === 'enabled', `${vertical.id} performance readiness diagnostics must be default-on`);
|
|
1922
3945
|
assert(contractEntry?.config?.performance?.readinessDiagnostics?.failOn === 'framework-invariant', `${vertical.id} performance readiness diagnostics must only fail framework invariants by default`);
|
|
1923
3946
|
assert(contractEntry?.config?.performance?.readinessDiagnostics?.optOut?.config === 'scripts/ultramodern-performance-readiness.config.mjs', `${vertical.id} performance readiness opt-out config is incorrect`);
|
|
1924
|
-
|
|
3947
|
+
if (vertical.emitsApi) {
|
|
3948
|
+
// Per policy.ts:76 the Cloudflare proof advertises a REST readiness route
|
|
3949
|
+
// only for `rest` units; an `rpc` unit must not carry one.
|
|
3950
|
+
if (vertical.apiProtocol === 'rpc') {
|
|
3951
|
+
assert(contractEntry?.deploy?.cloudflare?.routes?.apiReadiness === undefined, `${vertical.id} rpc unit must not carry a REST Cloudflare readiness route`);
|
|
3952
|
+
} else {
|
|
3953
|
+
assert(contractEntry?.deploy?.cloudflare?.routes?.apiReadiness === `${vertical.apiPrefix}/${vertical.stem}/readiness`, `${vertical.id} Cloudflare proof readiness route is incorrect`);
|
|
3954
|
+
}
|
|
3955
|
+
}
|
|
1925
3956
|
assert(contractEntry?.config?.rspack?.output?.uniqueName === vertical.mfName, `${vertical.id} Rspack uniqueName is incorrect`);
|
|
1926
3957
|
assert(contractEntry?.config?.rspack?.output?.chunkLoadingGlobal === expectedChunkLoadingGlobal(vertical.mfName), `${vertical.id} Rspack chunkLoadingGlobal is incorrect`);
|
|
1927
3958
|
assert(contractEntry?.moduleFederation?.name === vertical.mfName, `${vertical.id} MF name is incorrect`);
|
|
1928
|
-
assert(
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
3959
|
+
assert(sameJson(contractEntry?.moduleFederation?.exposes, vertical.exposes), `${vertical.id} MF exposes are incorrect`);
|
|
3960
|
+
// The browser Module Federation DTS surface and its config file only exist
|
|
3961
|
+
// for UI-emitting units; a headless api-only unit federates no browser types.
|
|
3962
|
+
if (vertical.emitsUi) {
|
|
3963
|
+
assert(contractEntry?.moduleFederation?.dts?.compilerInstance === 'effect-tsgo', `${vertical.id} must keep mandatory DTS compiler`);
|
|
3964
|
+
assert(contractEntry?.moduleFederation?.dts?.tsConfigPath === './tsconfig.mf-types.json', `${vertical.id} must keep dedicated Module Federation DTS tsconfig`);
|
|
3965
|
+
assert(moduleFederationConfig.includes("tsConfigPath: './tsconfig.mf-types.json'"), `${vertical.id} Module Federation config must use the dedicated DTS tsconfig`);
|
|
3966
|
+
}
|
|
3967
|
+
assert(sameJson(contractEntry?.moduleFederation?.verticalRefs ?? [], vertical.verticalRefs), `${vertical.id} MF verticalRefs are incorrect`);
|
|
1933
3968
|
assert(
|
|
1934
|
-
|
|
1935
|
-
|
|
3969
|
+
sameJson(
|
|
3970
|
+
(contractEntry?.moduleFederation?.remotes ?? []).map(remote => remote.id),
|
|
3971
|
+
vertical.verticalRefs,
|
|
3972
|
+
),
|
|
1936
3973
|
`${vertical.id} MF consumed verticals are incorrect`,
|
|
1937
3974
|
);
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
3975
|
+
// API contract surface is only present for API-bearing units.
|
|
3976
|
+
if (vertical.emitsApi) {
|
|
3977
|
+
assert(contractEntry?.api?.prefix === vertical.apiPrefix, `${vertical.id} API prefix is incorrect`);
|
|
3978
|
+
assert(contractEntry?.api?.group === vertical.group, `${vertical.id} API group is incorrect`);
|
|
3979
|
+
assert(contractEntry?.api?.runtime === 'effect', `${vertical.id} API runtime must be Effect`);
|
|
3980
|
+
assert(contractEntry?.api?.strictEffectApproach === true, `${vertical.id} strictEffectApproach must be enabled`);
|
|
3981
|
+
assert(contractEntry?.api?.contract === vertical.apiContractExport, `${vertical.id} API contract export is incorrect`);
|
|
3982
|
+
assert(contractEntry?.api?.client === vertical.apiClientExport, `${vertical.id} API client export is incorrect`);
|
|
3983
|
+
if (vertical.apiProtocol === 'rpc') {
|
|
3984
|
+
// An `rpc` unit records the `/rpc` route/serialization; it must not carry
|
|
3985
|
+
// REST readiness or domain-operation semantics.
|
|
3986
|
+
assert(contractEntry?.api?.protocol === 'rpc', `${vertical.id} generated contract API protocol must be rpc`);
|
|
3987
|
+
assert(contractEntry?.api?.rpc?.path === '/rpc', `${vertical.id} generated contract RPC route path is incorrect`);
|
|
3988
|
+
assert(contractEntry?.api?.rpcPath === `${vertical.apiPrefix}/rpc`, `${vertical.id} generated contract RPC path is incorrect`);
|
|
3989
|
+
} else {
|
|
3990
|
+
assert(contractEntry?.api?.readiness?.endpoint === `/${vertical.stem}/readiness`, `${vertical.id} readiness endpoint is incorrect`);
|
|
3991
|
+
assert(contractEntry?.api?.operations?.readiness?.path === `/${vertical.stem}/readiness`, `${vertical.id} readiness operation is missing`);
|
|
3992
|
+
assert(contractEntry?.api?.requestContext?.propagatedHeaders?.includes('traceparent'), `${vertical.id} trace context propagation is missing`);
|
|
3993
|
+
assert(Object.keys(contractEntry?.api?.domainOperations ?? {}).length >= 3, `${vertical.id} domain operations are missing`);
|
|
3994
|
+
}
|
|
3995
|
+
}
|
|
1946
3996
|
assert(contractEntry?.i18n?.languages?.includes('en') && contractEntry?.i18n?.languages?.includes('cs'), `${vertical.id} must declare i18n languages`);
|
|
1947
3997
|
assert(contractEntry?.i18n?.namespace === vertical.namespace, `${vertical.id} i18n namespace is incorrect`);
|
|
1948
3998
|
assert(
|
|
@@ -1956,23 +4006,32 @@ for (const vertical of fullStackVerticals) {
|
|
|
1956
4006
|
assert(contractEntry?.routes?.privateByDefault === true, `${vertical.id} routes must be private by default`);
|
|
1957
4007
|
assert(contractEntry?.routes?.publicnessDefault === 'private-app-screen', `${vertical.id} route publicness default is incorrect`);
|
|
1958
4008
|
assert(JSON.stringify(contractEntry?.routes?.publicRoutes ?? []) === '[]', `${vertical.id} must not expose generated public routes by default`);
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
(contractEntry?.routes?.
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
4009
|
+
// Public head/surface and owned browser routes only exist for UI-emitting
|
|
4010
|
+
// units; a headless api-only unit renders no route head or public surface.
|
|
4011
|
+
if (vertical.emitsUi) {
|
|
4012
|
+
assertPublicHeadContract(vertical.id, contractEntry?.routes?.publicHead, routeHead);
|
|
4013
|
+
assertPublicSurfaceContract(vertical.id, contractEntry?.routes?.publicSurface);
|
|
4014
|
+
assert(
|
|
4015
|
+
(contractEntry?.routes?.owned ?? []).every(route => route.public === false && route.indexable === false && route.publicSurface === 'private-app-screen' && typeof route.descriptionKey === 'string'),
|
|
4016
|
+
`${vertical.id} owned routes must be non-indexable private app screens by default and include description keys`,
|
|
4017
|
+
);
|
|
4018
|
+
assertPublicSurfaceAssets(vertical.path, contractEntry?.routes?.publicRoutes ?? []);
|
|
4019
|
+
}
|
|
4020
|
+
// CSS federation (a federated browser stylesheet keyed off the federation
|
|
4021
|
+
// entry) only applies to UI-emitting units; a headless api-only unit owns no
|
|
4022
|
+
// vertical CSS surface.
|
|
4023
|
+
if (vertical.emitsUi) {
|
|
4024
|
+
assert(contractEntry?.styling?.federation?.owner?.id === vertical.id, `${vertical.id} CSS federation owner is missing`);
|
|
4025
|
+
assert(contractEntry?.styling?.federation?.role === 'vertical-css', `${vertical.id} must own only vertical CSS`);
|
|
4026
|
+
assert(contractEntry?.styling?.federation?.rootSelector === `[data-app-id="${vertical.id}"]`, `${vertical.id} CSS root selector is incorrect`);
|
|
4027
|
+
assert(contractEntry?.styling?.federation?.classPrefix === `${vertical.tailwindPrefix}:`, `${vertical.id} CSS class prefix is incorrect`);
|
|
4028
|
+
assert(contractEntry?.styling?.federation?.layers?.owned?.includes(`ultramodern-vertical-${vertical.domain}`), `${vertical.id} vertical CSS layer is missing`);
|
|
4029
|
+
assert(!contractEntry?.styling?.federation?.layers?.owned?.includes('ultramodern-shell-base'), `${vertical.id} must not own shell base CSS`);
|
|
4030
|
+
assert(contractEntry?.styling?.federation?.entrypoints?.federationEntry === 'src/federation-entry.tsx', `${vertical.id} CSS contract must include federation entry`);
|
|
4031
|
+
assert(contractEntry?.styling?.federation?.assets?.shared?.some(asset => asset.endsWith('/shared-design-tokens/tokens.css')), `${vertical.id} must import shared design token CSS`);
|
|
4032
|
+
assert(contractEntry?.styling?.federation?.dedupe?.runtimeLoad === 'once-per-content-hash', `${vertical.id} CSS dedupe strategy is incorrect`);
|
|
4033
|
+
assert(contractEntry?.styling?.federation?.ssr?.verticalCss === 'federated-manifest-owned-css', `${vertical.id} SSR CSS loading contract is incorrect`);
|
|
4034
|
+
}
|
|
1976
4035
|
|
|
1977
4036
|
const topologyEntry = topology.verticals?.find(verticalEntry => verticalEntry.id === vertical.id);
|
|
1978
4037
|
assert(topologyEntry?.kind === 'vertical', `${vertical.id} topology kind is incorrect`);
|
|
@@ -1981,16 +4040,135 @@ for (const vertical of fullStackVerticals) {
|
|
|
1981
4040
|
assert(topologyEntry?.moduleFederation?.name === vertical.mfName, `${vertical.id} topology MF name is incorrect`);
|
|
1982
4041
|
assert(JSON.stringify(topologyEntry?.moduleFederation?.exposes) === JSON.stringify(vertical.exposes), `${vertical.id} topology exposes are incorrect`);
|
|
1983
4042
|
assert(JSON.stringify(topologyEntry?.moduleFederation?.verticalRefs ?? []) === JSON.stringify(vertical.verticalRefs), `${vertical.id} topology verticalRefs are incorrect`);
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
4043
|
+
// API/BFF topology metadata only exists for API-bearing units; and the REST
|
|
4044
|
+
// readiness/domain-operation surface is absent for the RPC protocol (G7a).
|
|
4045
|
+
if (vertical.emitsApi) {
|
|
4046
|
+
assert(topologyEntry?.api?.bff?.prefix === vertical.apiPrefix, `${vertical.id} topology API prefix is incorrect`);
|
|
4047
|
+
assert(topologyEntry?.api?.bff?.strictEffectApproach === true, `${vertical.id} topology strictEffectApproach is incorrect`);
|
|
4048
|
+
assert(topologyEntry?.api?.serverEntry === `${vertical.path}/api/index.ts`, `${vertical.id} topology server entry is incorrect`);
|
|
4049
|
+
if (vertical.apiProtocol !== 'rpc') {
|
|
4050
|
+
assert(topologyEntry?.api?.readiness?.endpoint === `/${vertical.stem}/readiness`, `${vertical.id} topology readiness endpoint is incorrect`);
|
|
4051
|
+
assert(Object.keys(topologyEntry?.api?.domainOperations ?? {}).length >= 3, `${vertical.id} topology domain operations are missing`);
|
|
4052
|
+
}
|
|
4053
|
+
}
|
|
4054
|
+
|
|
4055
|
+
if (vertical.deliveryUnit) {
|
|
4056
|
+
const expectedDeliveryUnit = deliveryUnitBlock(
|
|
4057
|
+
expectedDeliveryUnitFor(vertical, topologyEntry),
|
|
4058
|
+
);
|
|
4059
|
+
const compactAppEntry = ultramodernConfig.topology?.apps?.find(
|
|
4060
|
+
entry => entry?.id === vertical.id,
|
|
4061
|
+
);
|
|
4062
|
+
// The backend-federation delivery-unit mirror only exists for API-bearing
|
|
4063
|
+
// units; a UI-only vertical carries just the app-level delivery unit.
|
|
4064
|
+
if (vertical.emitsApi) {
|
|
4065
|
+
const compactBackendDeliveryUnit = deliveryUnitBlock(
|
|
4066
|
+
compactAppEntry?.backendFederation?.deliveryUnit,
|
|
4067
|
+
);
|
|
4068
|
+
assertSameJson(
|
|
4069
|
+
deliveryUnitBlock(compactAppEntry?.deliveryUnit),
|
|
4070
|
+
compactBackendDeliveryUnit,
|
|
4071
|
+
`${compactConfigPath} topology.apps.${vertical.id}.deliveryUnit`,
|
|
4072
|
+
deliveryUnitIdentityFixArea,
|
|
4073
|
+
);
|
|
4074
|
+
assertSameJson(
|
|
4075
|
+
compactBackendDeliveryUnit,
|
|
4076
|
+
expectedDeliveryUnit,
|
|
4077
|
+
`${compactConfigPath} topology.apps.${vertical.id}.backendFederation.deliveryUnit`,
|
|
4078
|
+
deliveryUnitIdentityFixArea,
|
|
4079
|
+
);
|
|
4080
|
+
assertSelfCheck(
|
|
4081
|
+
compactAppEntry?.backendFederation?.versionBoundary?.identityRoot === 'deliveryUnit',
|
|
4082
|
+
`${compactConfigPath} topology.apps.${vertical.id}.backendFederation.versionBoundary.identityRoot`,
|
|
4083
|
+
`Expected "deliveryUnit", found ${formatJson(compactAppEntry?.backendFederation?.versionBoundary?.identityRoot)}`,
|
|
4084
|
+
deliveryUnitIdentityFixArea,
|
|
4085
|
+
);
|
|
4086
|
+
}
|
|
4087
|
+
assertSameJson(
|
|
4088
|
+
deliveryUnitBlock(compactAppEntry?.deliveryUnit),
|
|
4089
|
+
expectedDeliveryUnit,
|
|
4090
|
+
`${compactConfigPath} topology.apps.${vertical.id}.deliveryUnit`,
|
|
4091
|
+
deliveryUnitIdentityFixArea,
|
|
4092
|
+
);
|
|
4093
|
+
assertSameJson(
|
|
4094
|
+
deliveryUnitBlock(compactAppEntry?.deliveryUnit),
|
|
4095
|
+
deliveryUnitBlock(topologyEntry?.deliveryUnit),
|
|
4096
|
+
`${compactConfigPath} vs topology/reference-topology.json verticals.${vertical.id}.deliveryUnit`,
|
|
4097
|
+
deliveryUnitIdentityFixArea,
|
|
4098
|
+
);
|
|
4099
|
+
}
|
|
1989
4100
|
|
|
1990
4101
|
assert(ownership.owners?.some(owner => owner.id === vertical.id && owner.path === vertical.path), `${vertical.id} ownership entry is missing`);
|
|
1991
4102
|
assert(overlay.ports?.[vertical.id], `${vertical.id} development port is missing`);
|
|
1992
|
-
|
|
1993
|
-
|
|
4103
|
+
if (vertical.emitsUi) {
|
|
4104
|
+
assert(overlay.manifests?.[vertical.id]?.includes('/mf-manifest.json'), `${vertical.id} development manifest is missing`);
|
|
4105
|
+
}
|
|
4106
|
+
if (vertical.emitsApi) {
|
|
4107
|
+
assert(overlay.apis?.[vertical.id]?.endsWith(vertical.apiProtocol === 'rpc' ? `${vertical.apiPrefix}/rpc` : vertical.apiPrefix), `${vertical.id} development API URL is missing`);
|
|
4108
|
+
}
|
|
4109
|
+
}
|
|
4110
|
+
|
|
4111
|
+
// Delivery-unit identity for ALL unit kinds (G29). Every workspace app —
|
|
4112
|
+
// shell, UI-only vertical, and API-bearing vertical — is an indivisible
|
|
4113
|
+
// delivery unit and must carry one consistent identity record across the
|
|
4114
|
+
// compact config, the reference topology, and its generated build artifact
|
|
4115
|
+
// (ADR-0019: one delivery-unit record, one build marker).
|
|
4116
|
+
for (const expectedApp of workspaceValidationContract.topology.compactConfig?.apps ?? []) {
|
|
4117
|
+
const unitLabel = `delivery-unit identity for ${expectedApp.id}`;
|
|
4118
|
+
const expectedDeliveryUnit = deliveryUnitBlock(expectedApp.deliveryUnit);
|
|
4119
|
+
assertSelfCheck(
|
|
4120
|
+
typeof expectedDeliveryUnit.unitId === 'string' && expectedDeliveryUnit.unitId.length > 0 &&
|
|
4121
|
+
typeof expectedDeliveryUnit.buildMarker === 'string' && expectedDeliveryUnit.buildMarker.length > 0,
|
|
4122
|
+
unitLabel,
|
|
4123
|
+
`Every unit kind must declare a delivery-unit record; found ${formatJson(expectedApp.deliveryUnit)}`,
|
|
4124
|
+
deliveryUnitIdentityFixArea,
|
|
4125
|
+
);
|
|
4126
|
+
|
|
4127
|
+
const compactAppEntry = ultramodernConfig.topology?.apps?.find(
|
|
4128
|
+
entry => entry?.id === expectedApp.id,
|
|
4129
|
+
);
|
|
4130
|
+
assertSameJson(
|
|
4131
|
+
compactAppEntry?.deploy?.cloudflare,
|
|
4132
|
+
expectedApp.deploy?.cloudflare,
|
|
4133
|
+
`${compactConfigPath} topology.apps.${expectedApp.id}.deploy.cloudflare`,
|
|
4134
|
+
'regenerate the app Cloudflare deployment contract; do not add local proof-only smoke checks',
|
|
4135
|
+
);
|
|
4136
|
+
assertSameJson(
|
|
4137
|
+
deliveryUnitBlock(compactAppEntry?.deliveryUnit),
|
|
4138
|
+
expectedDeliveryUnit,
|
|
4139
|
+
`${compactConfigPath} topology.apps.${expectedApp.id}.deliveryUnit`,
|
|
4140
|
+
deliveryUnitIdentityFixArea,
|
|
4141
|
+
);
|
|
4142
|
+
|
|
4143
|
+
const topologyUnitEntry =
|
|
4144
|
+
expectedApp.kind === 'shell'
|
|
4145
|
+
? topology.shell
|
|
4146
|
+
: topology.verticals?.find(entry => entry?.id === expectedApp.id);
|
|
4147
|
+
assertSameJson(
|
|
4148
|
+
deliveryUnitBlock(topologyUnitEntry?.deliveryUnit),
|
|
4149
|
+
expectedDeliveryUnit,
|
|
4150
|
+
`topology/reference-topology.json ${expectedApp.kind === 'shell' ? 'shell' : `verticals.${expectedApp.id}`}.deliveryUnit`,
|
|
4151
|
+
deliveryUnitIdentityFixArea,
|
|
4152
|
+
);
|
|
4153
|
+
|
|
4154
|
+
const appPath = expectedApp.path;
|
|
4155
|
+
const buildArtifactPath = `${appPath}/shared/ultramodern-build.json`;
|
|
4156
|
+
assertExists(buildArtifactPath);
|
|
4157
|
+
const buildIdentity = readJson(buildArtifactPath).deliveryUnit ?? {};
|
|
4158
|
+
assertSelfCheck(
|
|
4159
|
+
buildIdentity.unitId === expectedDeliveryUnit.unitId &&
|
|
4160
|
+
buildIdentity.buildMarker === expectedDeliveryUnit.buildMarker,
|
|
4161
|
+
`${buildArtifactPath} deliveryUnit`,
|
|
4162
|
+
`Expected ${formatJson({ unitId: expectedDeliveryUnit.unitId, buildMarker: expectedDeliveryUnit.buildMarker })}, found ${formatJson({ unitId: buildIdentity.unitId, buildMarker: buildIdentity.buildMarker })}`,
|
|
4163
|
+
deliveryUnitIdentityFixArea,
|
|
4164
|
+
);
|
|
4165
|
+
const buildModuleSource = readText(`${appPath}/shared/ultramodern-build.ts`);
|
|
4166
|
+
assertSelfCheck(
|
|
4167
|
+
buildModuleSource.includes('export const ultramodernDeliveryUnit'),
|
|
4168
|
+
`${appPath}/shared/ultramodern-build.ts`,
|
|
4169
|
+
'Missing ultramodernDeliveryUnit export',
|
|
4170
|
+
deliveryUnitIdentityFixArea,
|
|
4171
|
+
);
|
|
1994
4172
|
}
|
|
1995
4173
|
|
|
1996
4174
|
console.log('UltraModern workspace scaffold validated');
|