@open-mercato/cli 0.6.7-develop.6862.1.c11a64ce0a → 0.6.7
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/.turbo/turbo-build.log +4 -4
- package/build.mjs +7 -24
- package/dist/agentic/claude-code/hooks/entity-migration-check.ts +88 -32
- package/dist/agentic/codex/enforcement-rules.md +16 -2
- package/dist/agentic/cursor/hooks/entity-migration-check.mjs +50 -19
- package/dist/agentic/cursor/rules/entity-guard.mdc +21 -9
- package/dist/agentic/cursor/rules/generated-guard.mdc +5 -3
- package/dist/agentic/cursor/rules/open-mercato.mdc +29 -3
- package/dist/agentic/guides/cache.md +45 -0
- package/dist/agentic/guides/core.md +453 -0
- package/dist/agentic/guides/events.md +117 -0
- package/dist/agentic/guides/module-facts.json +3833 -115419
- package/dist/agentic/guides/module-system.md +127 -12
- package/dist/agentic/guides/modules/ai_assistant.md +28 -227
- package/dist/agentic/guides/modules/api_docs.md +2 -58
- package/dist/agentic/guides/modules/api_keys.md +7 -92
- package/dist/agentic/guides/modules/attachments.md +14 -119
- package/dist/agentic/guides/modules/audit_logs.md +10 -131
- package/dist/agentic/guides/modules/auth.md +33 -276
- package/dist/agentic/guides/modules/business_rules.md +14 -149
- package/dist/agentic/guides/modules/catalog.md +43 -475
- package/dist/agentic/guides/modules/channel_gmail.md +3 -82
- package/dist/agentic/guides/modules/channel_imap.md +3 -82
- package/dist/agentic/guides/modules/checkout.md +27 -299
- package/dist/agentic/guides/modules/communication_channels.md +33 -315
- package/dist/agentic/guides/modules/configs.md +8 -104
- package/dist/agentic/guides/modules/content.md +2 -57
- package/dist/agentic/guides/modules/currencies.md +17 -152
- package/dist/agentic/guides/modules/customer_accounts.md +45 -387
- package/dist/agentic/guides/modules/customers.md +85 -746
- package/dist/agentic/guides/modules/dashboards.md +10 -155
- package/dist/agentic/guides/modules/data_sync.md +16 -149
- package/dist/agentic/guides/modules/dictionaries.md +12 -107
- package/dist/agentic/guides/modules/directory.md +15 -148
- package/dist/agentic/guides/modules/entities.md +13 -145
- package/dist/agentic/guides/modules/events.md +2 -59
- package/dist/agentic/guides/modules/feature_toggles.md +9 -119
- package/dist/agentic/guides/modules/gateway_stripe.md +4 -92
- package/dist/agentic/guides/modules/generators.md +2 -45
- package/dist/agentic/guides/modules/inbox_ops.md +27 -207
- package/dist/agentic/guides/modules/integrations.md +16 -146
- package/dist/agentic/guides/modules/messages.md +24 -223
- package/dist/agentic/guides/modules/notifications.md +8 -96
- package/dist/agentic/guides/modules/onboarding.md +6 -77
- package/dist/agentic/guides/modules/payment_gateways.md +21 -164
- package/dist/agentic/guides/modules/perspectives.md +7 -77
- package/dist/agentic/guides/modules/planner.md +18 -136
- package/dist/agentic/guides/modules/portal.md +2 -93
- package/dist/agentic/guides/modules/progress.md +14 -104
- package/dist/agentic/guides/modules/query_index.md +15 -154
- package/dist/agentic/guides/modules/record_locks.md +21 -206
- package/dist/agentic/guides/modules/resources.md +30 -197
- package/dist/agentic/guides/modules/sales.md +84 -678
- package/dist/agentic/guides/modules/scheduler.md +14 -133
- package/dist/agentic/guides/modules/search.md +6 -170
- package/dist/agentic/guides/modules/security.md +35 -294
- package/dist/agentic/guides/modules/shipping_carriers.md +18 -134
- package/dist/agentic/guides/modules/sso.md +26 -151
- package/dist/agentic/guides/modules/staff.md +57 -369
- package/dist/agentic/guides/modules/storage_s3.md +5 -95
- package/dist/agentic/guides/modules/sync_akeneo.md +3 -85
- package/dist/agentic/guides/modules/sync_excel.md +6 -75
- package/dist/agentic/guides/modules/system_status_overlays.md +2 -60
- package/dist/agentic/guides/modules/translations.md +10 -104
- package/dist/agentic/guides/modules/webhooks.md +24 -203
- package/dist/agentic/guides/modules/wms.md +46 -441
- package/dist/agentic/guides/modules/workflows.md +43 -302
- package/dist/agentic/guides/queue.md +53 -0
- package/dist/agentic/guides/search.md +115 -0
- package/dist/agentic/guides/shared.md +108 -0
- package/dist/agentic/guides/ui.md +347 -0
- package/dist/agentic/shared/AGENTS.md.template +311 -124
- package/dist/agentic/shared/ai/agentic.config.json +0 -2
- package/dist/agentic/shared/ai/lessons.md +3 -26
- package/dist/agentic/shared/ai/skills/om-auto-continue-pr-loop/SKILL.md +27 -0
- package/dist/agentic/shared/ai/skills/om-auto-create-pr-loop/SKILL.md +27 -0
- package/dist/agentic/shared/ai/skills/om-auto-review-pr/SKILL.md +0 -4
- package/dist/agentic/shared/ai/skills/om-backend-ui-design/SKILL.md +241 -20
- package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/ui-components.md +260 -0
- package/dist/agentic/shared/ai/skills/om-data-model-design/SKILL.md +601 -16
- package/dist/agentic/shared/ai/skills/om-data-model-design/references/mikro-orm-cheatsheet.md +146 -0
- package/dist/agentic/shared/ai/skills/om-eject-and-customize/SKILL.md +310 -14
- package/dist/agentic/shared/ai/skills/om-help/SKILL.md +97 -15
- package/dist/agentic/shared/ai/skills/om-help/references/skills-catalog.md +84 -0
- package/dist/agentic/shared/ai/skills/om-help/references/workflow-sequences.md +193 -0
- package/dist/agentic/shared/ai/skills/om-implement-spec/SKILL.md +152 -33
- package/dist/agentic/shared/ai/skills/om-integration-builder/SKILL.md +708 -24
- package/dist/agentic/shared/ai/skills/om-integration-builder/STANDALONE.md +34 -0
- package/dist/agentic/shared/ai/skills/om-integration-builder/references/adapter-contracts.md +762 -0
- package/dist/agentic/shared/ai/skills/om-module-scaffold/SKILL.md +820 -20
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/naming-conventions.md +50 -0
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/navigation-patterns.md +97 -0
- package/dist/agentic/shared/ai/skills/om-system-extension/SKILL.md +843 -19
- package/dist/agentic/shared/ai/skills/om-system-extension/references/extension-contracts.md +271 -0
- package/dist/agentic/shared/ai/skills/om-trim-unused-modules/SKILL.md +63 -15
- package/dist/agentic/shared/ai/skills/om-troubleshooter/SKILL.md +467 -16
- package/dist/agentic/shared/ai/skills/om-troubleshooter/references/diagnostic-commands.md +101 -0
- package/dist/agentic/shared/ai/skills/tiers.json +21 -143
- package/dist/agentic/shared/ai/skills/tiers.schema.json +40 -52
- package/dist/agentic/shared/ai/specs/README.md +3 -4
- package/dist/agentic/shared/ai/specs/SPEC-000-template.md +10 -226
- package/dist/agentic/shared/ai/trackers/github.md +4 -108
- package/dist/agentic/shared/scripts/install-skills.sh +694 -4
- package/dist/bin.js +1 -13
- package/dist/bin.js.map +2 -2
- package/dist/lib/__integration__/TC-INT-008.spec.js +8 -88
- package/dist/lib/__integration__/TC-INT-008.spec.js.map +2 -2
- package/dist/lib/agentic-init.js +3 -13
- package/dist/lib/agentic-init.js.map +2 -2
- package/dist/lib/agentic-setup.js +186 -418
- package/dist/lib/agentic-setup.js.map +2 -2
- package/dist/lib/generators/extensions/injection-widgets.js +0 -20
- package/dist/lib/generators/extensions/injection-widgets.js.map +2 -2
- package/dist/lib/generators/module-facts-discovery.js +7 -41
- package/dist/lib/generators/module-facts-discovery.js.map +2 -2
- package/dist/lib/generators/module-facts.js +45 -1387
- package/dist/lib/generators/module-facts.js.map +3 -3
- package/dist/lib/generators/module-registry.js +5 -11
- package/dist/lib/generators/module-registry.js.map +2 -2
- package/dist/lib/resolver.js +0 -2
- package/dist/lib/resolver.js.map +2 -2
- package/dist/lib/testing/integration.js +0 -5
- package/dist/lib/testing/integration.js.map +2 -2
- package/dist/mercato.js +1 -23
- package/dist/mercato.js.map +2 -2
- package/package.json +11 -13
- package/src/bin.ts +1 -33
- package/src/lib/__integration__/TC-INT-008.spec.ts +9 -107
- package/src/lib/__tests__/agentic-init.test.ts +5 -41
- package/src/lib/agentic-init.ts +3 -15
- package/src/lib/agentic-setup.ts +224 -530
- package/src/lib/generators/__tests__/module-facts.bc-guard.test.ts +2 -88
- package/src/lib/generators/__tests__/module-facts.customers.fixture.test.ts +0 -1
- package/src/lib/generators/__tests__/module-facts.discovery.test.ts +10 -54
- package/src/lib/generators/__tests__/module-subset.test.ts +0 -18
- package/src/lib/generators/extensions/injection-widgets.ts +0 -51
- package/src/lib/generators/module-facts-discovery.ts +7 -58
- package/src/lib/generators/module-facts.ts +46 -1851
- package/src/lib/generators/module-registry.ts +6 -16
- package/src/lib/resolver.ts +0 -3
- package/src/lib/testing/__tests__/integration.test.ts +0 -4
- package/src/lib/testing/integration.ts +0 -5
- package/src/mercato.ts +1 -26
- package/dist/agentic/guides/ai-workflows.md +0 -76
- package/dist/agentic/guides/architecture.md +0 -107
- package/dist/agentic/guides/backend-ui.md +0 -80
- package/dist/agentic/guides/contracts.md +0 -92
- package/dist/agentic/guides/extensions.md +0 -121
- package/dist/agentic/guides/framework-extension-points.md +0 -36
- package/dist/agentic/guides/integrations.md +0 -81
- package/dist/agentic/guides/modules/design_system.md +0 -102
- package/dist/agentic/guides/spec-delivery.md +0 -22
- package/dist/agentic/guides/testing-debugging.md +0 -80
- package/dist/agentic/guides/upstream/AGENTS.md +0 -298
- package/dist/agentic/guides/upstream/BACKWARD_COMPATIBILITY.md +0 -316
- package/dist/agentic/guides/upstream/manifest.json +0 -8
- package/dist/agentic/shared/ai/harness/README.md +0 -50
- package/dist/agentic/shared/ai/harness/RELEASE.md +0 -57
- package/dist/agentic/shared/ai/harness/cases.json +0 -13680
- package/dist/agentic/shared/ai/harness/cases.schema.json +0 -133
- package/dist/agentic/shared/ai/harness/fixtures/index.json +0 -49
- package/dist/agentic/shared/ai/harness/fixtures/seeds.json +0 -162
- package/dist/agentic/shared/ai/harness/generated-code-review-policy.md +0 -21
- package/dist/agentic/shared/ai/harness/generated-code-review-response.schema.json +0 -96
- package/dist/agentic/shared/ai/harness/generated-code-review-result.schema.json +0 -172
- package/dist/agentic/shared/ai/harness/release-matrix.json +0 -158
- package/dist/agentic/shared/ai/harness/release-result.schema.json +0 -219
- package/dist/agentic/shared/ai/harness/result.schema.json +0 -76
- package/dist/agentic/shared/ai/harness/routing-response.schema.json +0 -12
- package/dist/agentic/shared/ai/harness/target-validation-result.schema.json +0 -73
- package/dist/agentic/shared/ai/harness/validators.json +0 -78
- package/dist/agentic/shared/ai/harness/writable-ast-oracles.mjs +0 -2121
- package/dist/agentic/shared/ai/harness/writable-behavior-oracles.mjs +0 -1096
- package/dist/agentic/shared/ai/lessons/_template.md +0 -16
- package/dist/agentic/shared/ai/review-checklist.md +0 -36
- package/dist/agentic/shared/ai/skills/om-auto-implement-spec/SKILL.md +0 -21
- package/dist/agentic/shared/ai/skills/om-auto-upgrade-0.6.6-to-0.6.7/SKILL.md +0 -98
- package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/crud-surfaces.md +0 -16
- package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/frontend-and-design-system.md +0 -53
- package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/page-and-navigation.md +0 -13
- package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/quality-states.md +0 -11
- package/dist/agentic/shared/ai/skills/om-build-workflow/SKILL.md +0 -30
- package/dist/agentic/shared/ai/skills/om-build-workflow/references/activity-contracts.md +0 -12
- package/dist/agentic/shared/ai/skills/om-build-workflow/references/durability-and-progress.md +0 -13
- package/dist/agentic/shared/ai/skills/om-build-workflow/references/workflow-design.md +0 -14
- package/dist/agentic/shared/ai/skills/om-create-ai-agent/SKILL.md +0 -28
- package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/attachments-and-overrides.md +0 -12
- package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/module-agents-and-tools.md +0 -22
- package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/orchestrator-agents.md +0 -12
- package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/surface-selector.md +0 -19
- package/dist/agentic/shared/ai/skills/om-data-model-design/references/integrity-and-concurrency.md +0 -19
- package/dist/agentic/shared/ai/skills/om-data-model-design/references/migration-workflow.md +0 -13
- package/dist/agentic/shared/ai/skills/om-data-model-design/references/schema-design.md +0 -14
- package/dist/agentic/shared/ai/skills/om-data-model-design/references/sensitive-data.md +0 -10
- package/dist/agentic/shared/ai/skills/om-eject-and-customize/references/decision-and-procedure.md +0 -17
- package/dist/agentic/shared/ai/skills/om-evolve-harness/SKILL.md +0 -29
- package/dist/agentic/shared/ai/skills/om-evolve-harness/references/case-template.md +0 -100
- package/dist/agentic/shared/ai/skills/om-evolve-harness/references/case-workflow.md +0 -16
- package/dist/agentic/shared/ai/skills/om-evolve-harness/references/owner-selection.md +0 -17
- package/dist/agentic/shared/ai/skills/om-framework-context/SKILL.md +0 -25
- package/dist/agentic/shared/ai/skills/om-framework-context/references/resolver-procedure.md +0 -11
- package/dist/agentic/shared/ai/skills/om-framework-context/references/skew-and-escalation.md +0 -13
- package/dist/agentic/shared/ai/skills/om-help/references/delivery-workflows.md +0 -18
- package/dist/agentic/shared/ai/skills/om-help/references/task-families.md +0 -20
- package/dist/agentic/shared/ai/skills/om-implement-spec/references/phases-and-gates.md +0 -33
- package/dist/agentic/shared/ai/skills/om-implement-spec/references/planning-and-progress.md +0 -40
- package/dist/agentic/shared/ai/skills/om-implement-spec/references/report-templates.md +0 -27
- package/dist/agentic/shared/ai/skills/om-implement-spec/references/spec-resolution.md +0 -24
- package/dist/agentic/shared/ai/skills/om-integration-builder/references/package-and-activation.md +0 -16
- package/dist/agentic/shared/ai/skills/om-integration-builder/references/provider-families.md +0 -14
- package/dist/agentic/shared/ai/skills/om-integration-builder/references/security-and-reliability.md +0 -15
- package/dist/agentic/shared/ai/skills/om-integration-builder/references/sync-and-files.md +0 -12
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/SKILL.md +0 -34
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/agents/openai.yaml +0 -4
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/agentic-setup.md +0 -11
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/input-normalization.md +0 -31
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/judge-workflow.md +0 -39
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/report-template.md +0 -43
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/rules.md +0 -11
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/api-and-domain.md +0 -19
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/business-one-shot-blueprints.md +0 -172
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/data-and-migrations.md +0 -12
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/discovery-surface-catalog.md +0 -83
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/module-surfaces.md +0 -21
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/planning.md +0 -11
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/runtime-cache-and-queues.md +0 -22
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/verification.md +0 -13
- package/dist/agentic/shared/ai/skills/om-share-this-session/SKILL.md +0 -41
- package/dist/agentic/shared/ai/skills/om-share-this-session/references/agentic-setup.md +0 -19
- package/dist/agentic/shared/ai/skills/om-share-this-session/references/bundle-preparation.md +0 -44
- package/dist/agentic/shared/ai/skills/om-share-this-session/references/consent-and-review.md +0 -39
- package/dist/agentic/shared/ai/skills/om-share-this-session/references/publication.md +0 -20
- package/dist/agentic/shared/ai/skills/om-share-this-session/references/report-templates.md +0 -57
- package/dist/agentic/shared/ai/skills/om-share-this-session/references/rules.md +0 -11
- package/dist/agentic/shared/ai/skills/om-share-this-session/scripts/prepare-share-bundle.mjs +0 -486
- package/dist/agentic/shared/ai/skills/om-system-extension/references/extension-branches.md +0 -18
- package/dist/agentic/shared/ai/skills/om-system-extension/references/mechanism-selector.md +0 -27
- package/dist/agentic/shared/ai/skills/om-system-extension/references/read-write-roundtrip.md +0 -13
- package/dist/agentic/shared/ai/skills/om-system-extension/references/unified-overrides.md +0 -41
- package/dist/agentic/shared/ai/skills/om-system-extension/references/verification.md +0 -11
- package/dist/agentic/shared/ai/skills/om-trim-unused-modules/references/dependency-and-redirect.md +0 -15
- package/dist/agentic/shared/ai/skills/om-troubleshooter/references/diagnosis-map.md +0 -16
- package/dist/agentic/shared/ai/skills/om-troubleshooter/references/regression-oracles.md +0 -14
- package/dist/agentic/shared/scripts/agent-harness-tool-server.mjs +0 -187
- package/dist/agentic/shared/scripts/check-lessons.mjs +0 -178
- package/dist/agentic/shared/scripts/evaluate-agent-harness.mjs +0 -2958
- package/dist/agentic/shared/scripts/execution-sandbox.mjs +0 -329
- package/dist/agentic/shared/scripts/framework-context.mjs +0 -888
- package/dist/agentic/shared/scripts/install-skills.mjs +0 -1029
- package/dist/agentic/shared/scripts/prepare-agent-harness-fixture.mjs +0 -222
- package/dist/agentic/shared/scripts/run-agent-harness-release.mjs +0 -1774
- package/dist/lib/generators/module-extension-facts.js +0 -1840
- package/dist/lib/generators/module-extension-facts.js.map +0 -7
- package/dist/lib/generators/module-fact-sources.js +0 -70
- package/dist/lib/generators/module-fact-sources.js.map +0 -7
- package/dist/lib/generators/module-override-targets.js +0 -688
- package/dist/lib/generators/module-override-targets.js.map +0 -7
- package/dist/lib/load-env.js +0 -24
- package/dist/lib/load-env.js.map +0 -7
- package/dist/lib/telemetry-init.js +0 -358
- package/dist/lib/telemetry-init.js.map +0 -7
- package/src/lib/__tests__/agentic-setup.ownership.test.ts +0 -386
- package/src/lib/__tests__/injection-widget-id-validation.test.ts +0 -184
- package/src/lib/__tests__/load-env.test.ts +0 -65
- package/src/lib/__tests__/telemetry-init.test.ts +0 -298
- package/src/lib/generators/__tests__/activation-adapter-coverage.test.ts +0 -92
- package/src/lib/generators/__tests__/module-extension-activations.test.ts +0 -762
- package/src/lib/generators/__tests__/module-extension-facts.test.ts +0 -404
- package/src/lib/generators/__tests__/module-facts.extension-hosts.test.ts +0 -108
- package/src/lib/generators/__tests__/module-facts.owned-contracts.test.ts +0 -561
- package/src/lib/generators/__tests__/module-facts.source-links.test.ts +0 -119
- package/src/lib/generators/__tests__/module-override-targets.roundtrip.test.ts +0 -149
- package/src/lib/generators/__tests__/module-override-targets.test.ts +0 -304
- package/src/lib/generators/module-extension-facts.ts +0 -2365
- package/src/lib/generators/module-fact-sources.ts +0 -108
- package/src/lib/generators/module-override-targets.ts +0 -863
- package/src/lib/load-env.ts +0 -30
- package/src/lib/telemetry-init.ts +0 -461
|
@@ -1,2365 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs'
|
|
2
|
-
import path from 'node:path'
|
|
3
|
-
import ts from 'typescript-js'
|
|
4
|
-
import {
|
|
5
|
-
CRUD_FORM_EXTENSION_SURFACES,
|
|
6
|
-
CRUD_FORM_LIFECYCLE_PHASES,
|
|
7
|
-
DATA_TABLE_EXTENSION_SURFACES,
|
|
8
|
-
} from '@open-mercato/shared/modules/widgets/extension-points'
|
|
9
|
-
import type {
|
|
10
|
-
ExtensionHostCapability,
|
|
11
|
-
ExtensionHostFamily,
|
|
12
|
-
ModuleContributionResolution,
|
|
13
|
-
ModuleExtensionActivation,
|
|
14
|
-
ModuleExtensionActivationKind,
|
|
15
|
-
ModuleExtensionContributionFact,
|
|
16
|
-
ModuleExtensionContributionBase,
|
|
17
|
-
ModuleExtensionContributionKind,
|
|
18
|
-
ModuleExtensionHostFact,
|
|
19
|
-
ModuleExtensionResolution,
|
|
20
|
-
ModuleExtensionSurfaceFacts,
|
|
21
|
-
ModuleExtensionTargetFact,
|
|
22
|
-
ModuleExtensionTargetRef,
|
|
23
|
-
ModuleExtensionUnresolvedFact,
|
|
24
|
-
ModuleFactSourceRef,
|
|
25
|
-
ModuleIncomingExtensionRef,
|
|
26
|
-
} from '@open-mercato/shared/modules/widgets/extension-points'
|
|
27
|
-
import { extractCommandIdsFromSource } from './module-registry'
|
|
28
|
-
import { scanModuleDir, SCAN_CONFIGS } from './scanner'
|
|
29
|
-
|
|
30
|
-
type StaticScalar = string | number | boolean | null
|
|
31
|
-
type StaticValue = StaticScalar | StaticValue[] | { [key: string]: StaticValue }
|
|
32
|
-
type StaticObject = { [key: string]: StaticValue }
|
|
33
|
-
type SpecializedRegistry = Extract<ModuleExtensionContributionFact, {
|
|
34
|
-
kind: 'specialized-registry'
|
|
35
|
-
}>['details']['registry']
|
|
36
|
-
|
|
37
|
-
export interface ExtensionFactEntityRef {
|
|
38
|
-
id: string
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface ExtensionFactEventRef {
|
|
42
|
-
id: string
|
|
43
|
-
clientBroadcast?: boolean
|
|
44
|
-
portalBroadcast?: boolean
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export interface ExtensionFactApiRouteRef {
|
|
48
|
-
path: string
|
|
49
|
-
methods: string[]
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export interface ExtractModuleExtensionFactsOptions {
|
|
53
|
-
moduleId: string
|
|
54
|
-
moduleRoot: string
|
|
55
|
-
sourceRoot: string
|
|
56
|
-
entities: readonly ExtensionFactEntityRef[]
|
|
57
|
-
events: readonly ExtensionFactEventRef[]
|
|
58
|
-
apiRoutes: readonly ExtensionFactApiRouteRef[]
|
|
59
|
-
searchEntities: readonly string[]
|
|
60
|
-
notifications?: readonly string[]
|
|
61
|
-
aiTools?: ReadonlyArray<{ name: string; sourcePath: string }>
|
|
62
|
-
aiAgents?: ReadonlyArray<{ id: string; sourcePath: string }>
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export interface CorrelateExtensionFactsOptions {
|
|
66
|
-
surfacesByModule: Readonly<Record<string, ModuleExtensionSurfaceFacts>>
|
|
67
|
-
entityIds: ReadonlySet<string>
|
|
68
|
-
eventIds: ReadonlySet<string>
|
|
69
|
-
apiRoutes: ReadonlySet<string>
|
|
70
|
-
commandIds?: ReadonlySet<string>
|
|
71
|
-
contributingModuleId?: string
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
type StaticContext = {
|
|
75
|
-
initializers: Map<string, ts.Expression | ts.FunctionDeclaration>
|
|
76
|
-
sourceFile: ts.SourceFile
|
|
77
|
-
resolving: Set<string>
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
type ApiExtensionHostIds = {
|
|
81
|
-
entityIds: string[]
|
|
82
|
-
/** Declaring `api/**` file per entity host id, so the host links to a real source. */
|
|
83
|
-
entitySourceFiles: Record<string, string>
|
|
84
|
-
commandIds: string[]
|
|
85
|
-
routeIds: string[]
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
type ModuleExtensionFactExtractionCache = {
|
|
89
|
-
sourceFiles: Map<string, ts.SourceFile | null>
|
|
90
|
-
sourceFilesBelow: Map<string, string[]>
|
|
91
|
-
apiExtensionHostIds: Map<string, ApiExtensionHostIds>
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
let activeExtractionCache: ModuleExtensionFactExtractionCache | null = null
|
|
95
|
-
|
|
96
|
-
/** @internal */
|
|
97
|
-
export function withModuleExtensionFactExtractionCache<Result>(operation: () => Result): Result {
|
|
98
|
-
if (activeExtractionCache) return operation()
|
|
99
|
-
activeExtractionCache = {
|
|
100
|
-
sourceFiles: new Map(),
|
|
101
|
-
sourceFilesBelow: new Map(),
|
|
102
|
-
apiExtensionHostIds: new Map(),
|
|
103
|
-
}
|
|
104
|
-
try {
|
|
105
|
-
return operation()
|
|
106
|
-
} finally {
|
|
107
|
-
activeExtractionCache = null
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const FRAMEWORK_PREFIXES = [
|
|
112
|
-
'dashboard:',
|
|
113
|
-
'menu:',
|
|
114
|
-
'notification:',
|
|
115
|
-
'integration:',
|
|
116
|
-
'integrations.detail:',
|
|
117
|
-
'system-status:',
|
|
118
|
-
'topbar:',
|
|
119
|
-
]
|
|
120
|
-
|
|
121
|
-
const FRAMEWORK_HOSTS: ModuleExtensionHostFact[] = [
|
|
122
|
-
frameworkHost('framework.backend.sidebar', 'menu:backend.sidebar', 'menu', ['menu-item']),
|
|
123
|
-
frameworkHost('framework.settings.sidebar', 'menu:settings.sidebar', 'menu', ['menu-item']),
|
|
124
|
-
frameworkHost('framework.profile.sidebar', 'menu:profile.sidebar', 'menu', ['menu-item']),
|
|
125
|
-
frameworkHost('framework.portal.sidebar', 'menu:portal.sidebar', 'menu', ['menu-item']),
|
|
126
|
-
frameworkHost('framework.dashboard', 'dashboard:*', 'dashboard', ['registry-contribution']),
|
|
127
|
-
frameworkHost('framework.notifications', 'notification:*', 'notification', ['registry-contribution']),
|
|
128
|
-
frameworkHost('framework.integration-detail', 'integrations.detail:{integrationId}', 'integration', ['render-widget']),
|
|
129
|
-
frameworkHost('framework.integration-detail-fallback', 'integrations.detail:tabs', 'integration', ['render-widget']),
|
|
130
|
-
frameworkHost('framework.backend-mutation', 'backend-mutation:global', 'generic', ['headless-widget']),
|
|
131
|
-
frameworkHost('framework.crud-form-wildcard', 'crud-form:*', 'crud-form', ['field-widget', 'lifecycle-handler']),
|
|
132
|
-
]
|
|
133
|
-
|
|
134
|
-
function frameworkHost(
|
|
135
|
-
key: string,
|
|
136
|
-
id: string,
|
|
137
|
-
family: ExtensionHostFamily,
|
|
138
|
-
capabilities: ExtensionHostCapability[],
|
|
139
|
-
): ModuleExtensionHostFact {
|
|
140
|
-
return {
|
|
141
|
-
key,
|
|
142
|
-
id,
|
|
143
|
-
resolution: id.includes('*') || id.includes('{') ? 'pattern' : 'framework',
|
|
144
|
-
family,
|
|
145
|
-
ownerModule: 'framework',
|
|
146
|
-
capabilities,
|
|
147
|
-
bound: true,
|
|
148
|
-
stability: 'frozen',
|
|
149
|
-
source: { kind: 'framework', path: 'packages/ui/src', symbol: key },
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function frameworkOverrideHost(
|
|
154
|
-
key: string,
|
|
155
|
-
mode: 'disable-replace' | 'replace' | 'additive',
|
|
156
|
-
): ModuleExtensionHostFact {
|
|
157
|
-
return {
|
|
158
|
-
key: `framework.module-override.${key}`,
|
|
159
|
-
id: `module-override:${key}`,
|
|
160
|
-
resolution: 'framework',
|
|
161
|
-
family: 'module-override',
|
|
162
|
-
ownerModule: 'framework',
|
|
163
|
-
capabilities: ['module-override'],
|
|
164
|
-
operations: [mode],
|
|
165
|
-
bound: true,
|
|
166
|
-
stability: 'stable',
|
|
167
|
-
source: {
|
|
168
|
-
kind: 'framework',
|
|
169
|
-
path: 'packages/shared/src/modules/overrides.ts',
|
|
170
|
-
symbol: `ModuleOverrides.${key}`,
|
|
171
|
-
},
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const FRAMEWORK_OVERRIDE_HOSTS = [
|
|
176
|
-
frameworkOverrideHost('ai.agents', 'disable-replace'),
|
|
177
|
-
frameworkOverrideHost('ai.tools', 'disable-replace'),
|
|
178
|
-
frameworkOverrideHost('ai.extensions', 'additive'),
|
|
179
|
-
frameworkOverrideHost('routes.api', 'disable-replace'),
|
|
180
|
-
frameworkOverrideHost('routes.pages', 'disable-replace'),
|
|
181
|
-
frameworkOverrideHost('events.subscribers', 'disable-replace'),
|
|
182
|
-
frameworkOverrideHost('workers', 'disable-replace'),
|
|
183
|
-
frameworkOverrideHost('widgets.injection', 'disable-replace'),
|
|
184
|
-
frameworkOverrideHost('widgets.components', 'disable-replace'),
|
|
185
|
-
frameworkOverrideHost('widgets.dashboard', 'disable-replace'),
|
|
186
|
-
frameworkOverrideHost('notifications.types', 'disable-replace'),
|
|
187
|
-
frameworkOverrideHost('notifications.handlers', 'disable-replace'),
|
|
188
|
-
frameworkOverrideHost('interceptors', 'disable-replace'),
|
|
189
|
-
frameworkOverrideHost('commandInterceptors', 'disable-replace'),
|
|
190
|
-
frameworkOverrideHost('enrichers', 'disable-replace'),
|
|
191
|
-
frameworkOverrideHost('guards', 'disable-replace'),
|
|
192
|
-
frameworkOverrideHost('cli', 'disable-replace'),
|
|
193
|
-
frameworkOverrideHost('setup', 'replace'),
|
|
194
|
-
frameworkOverrideHost('acl.features', 'disable-replace'),
|
|
195
|
-
frameworkOverrideHost('di', 'disable-replace'),
|
|
196
|
-
frameworkOverrideHost('encryption.maps', 'disable-replace'),
|
|
197
|
-
frameworkOverrideHost('nav.groupOrder', 'additive'),
|
|
198
|
-
] as const
|
|
199
|
-
|
|
200
|
-
function allFrameworkHosts(): ModuleExtensionHostFact[] {
|
|
201
|
-
return [...FRAMEWORK_HOSTS, ...FRAMEWORK_OVERRIDE_HOSTS]
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function sourceFile(filePath: string): ts.SourceFile | null {
|
|
205
|
-
const cached = activeExtractionCache?.sourceFiles.get(filePath)
|
|
206
|
-
if (cached !== undefined) return cached
|
|
207
|
-
if (!fs.existsSync(filePath)) return null
|
|
208
|
-
const scriptKind = filePath.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS
|
|
209
|
-
const parsed = ts.createSourceFile(filePath, fs.readFileSync(filePath, 'utf8'), ts.ScriptTarget.ES2020, true, scriptKind)
|
|
210
|
-
activeExtractionCache?.sourceFiles.set(filePath, parsed)
|
|
211
|
-
return parsed
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function unwrap(expression: ts.Expression): ts.Expression {
|
|
215
|
-
let current = expression
|
|
216
|
-
while (
|
|
217
|
-
ts.isAsExpression(current)
|
|
218
|
-
|| ts.isTypeAssertionExpression(current)
|
|
219
|
-
|| ts.isParenthesizedExpression(current)
|
|
220
|
-
|| ts.isSatisfiesExpression(current)
|
|
221
|
-
) {
|
|
222
|
-
current = current.expression
|
|
223
|
-
}
|
|
224
|
-
return current
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function propertyName(node: ts.PropertyName): string | null {
|
|
228
|
-
if (ts.isIdentifier(node) || ts.isStringLiteralLike(node) || ts.isNumericLiteral(node)) return node.text
|
|
229
|
-
return null
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function buildStaticContext(file: ts.SourceFile): StaticContext {
|
|
233
|
-
const initializers = new Map<string, ts.Expression | ts.FunctionDeclaration>()
|
|
234
|
-
const visit = (node: ts.Node): void => {
|
|
235
|
-
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
|
|
236
|
-
initializers.set(node.name.text, node.initializer)
|
|
237
|
-
}
|
|
238
|
-
if (ts.isFunctionDeclaration(node) && node.name && node.body) {
|
|
239
|
-
initializers.set(node.name.text, node)
|
|
240
|
-
}
|
|
241
|
-
node.forEachChild(visit)
|
|
242
|
-
}
|
|
243
|
-
file.forEachChild(visit)
|
|
244
|
-
return { initializers, sourceFile: file, resolving: new Set() }
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function entityRegistryValue(expression: ts.PropertyAccessExpression): string | null {
|
|
248
|
-
const parts: string[] = []
|
|
249
|
-
let current: ts.Expression = expression
|
|
250
|
-
while (ts.isPropertyAccessExpression(current)) {
|
|
251
|
-
parts.unshift(current.name.text)
|
|
252
|
-
current = current.expression
|
|
253
|
-
}
|
|
254
|
-
return ts.isIdentifier(current) && current.text === 'E' && parts.length === 2
|
|
255
|
-
? `${parts[0]}:${parts[1]}`
|
|
256
|
-
: null
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function staticTemplate(expression: ts.TemplateExpression, context: StaticContext): string | null {
|
|
260
|
-
let value = expression.head.text
|
|
261
|
-
for (const span of expression.templateSpans) {
|
|
262
|
-
const substitution = staticValue(span.expression, context)
|
|
263
|
-
if (typeof substitution !== 'string' && typeof substitution !== 'number') return null
|
|
264
|
-
value += `${substitution}${span.literal.text}`
|
|
265
|
-
}
|
|
266
|
-
return value
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
function staticObject(expression: ts.ObjectLiteralExpression, context: StaticContext): StaticObject {
|
|
270
|
-
const result: StaticObject = {}
|
|
271
|
-
for (const property of expression.properties) {
|
|
272
|
-
if (ts.isPropertyAssignment(property)) {
|
|
273
|
-
const name = propertyName(property.name)
|
|
274
|
-
if (!name) continue
|
|
275
|
-
const value = staticValue(property.initializer, context)
|
|
276
|
-
if (value !== undefined) result[name] = value
|
|
277
|
-
continue
|
|
278
|
-
}
|
|
279
|
-
if (ts.isShorthandPropertyAssignment(property)) {
|
|
280
|
-
const value = staticValue(property.name, context)
|
|
281
|
-
if (value !== undefined) result[property.name.text] = value
|
|
282
|
-
continue
|
|
283
|
-
}
|
|
284
|
-
if (ts.isMethodDeclaration(property)) {
|
|
285
|
-
const name = propertyName(property.name)
|
|
286
|
-
if (name) result[name] = true
|
|
287
|
-
continue
|
|
288
|
-
}
|
|
289
|
-
if (ts.isSpreadAssignment(property)) {
|
|
290
|
-
const value = staticValue(property.expression, context)
|
|
291
|
-
if (isStaticObject(value)) Object.assign(result, value)
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
return result
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
function staticValue(expression: ts.Expression, context: StaticContext): StaticValue | undefined {
|
|
298
|
-
const current = unwrap(expression)
|
|
299
|
-
if (ts.isStringLiteralLike(current) || ts.isNumericLiteral(current)) return current.text
|
|
300
|
-
if (current.kind === ts.SyntaxKind.TrueKeyword) return true
|
|
301
|
-
if (current.kind === ts.SyntaxKind.FalseKeyword) return false
|
|
302
|
-
if (current.kind === ts.SyntaxKind.NullKeyword) return null
|
|
303
|
-
if (ts.isNoSubstitutionTemplateLiteral(current)) return current.text
|
|
304
|
-
if (ts.isTemplateExpression(current)) return staticTemplate(current, context) ?? undefined
|
|
305
|
-
if (ts.isArrayLiteralExpression(current)) {
|
|
306
|
-
return current.elements.flatMap((element) => {
|
|
307
|
-
if (ts.isSpreadElement(element)) {
|
|
308
|
-
const spread = staticValue(element.expression, context)
|
|
309
|
-
return Array.isArray(spread) ? spread : []
|
|
310
|
-
}
|
|
311
|
-
const value = staticValue(element, context)
|
|
312
|
-
return value === undefined ? [] : [value]
|
|
313
|
-
})
|
|
314
|
-
}
|
|
315
|
-
if (ts.isObjectLiteralExpression(current)) return staticObject(current, context)
|
|
316
|
-
if (ts.isPropertyAccessExpression(current)) {
|
|
317
|
-
return entityRegistryValue(current) ?? undefined
|
|
318
|
-
}
|
|
319
|
-
if (ts.isIdentifier(current)) {
|
|
320
|
-
if (context.resolving.has(current.text)) return undefined
|
|
321
|
-
const initializer = context.initializers.get(current.text)
|
|
322
|
-
if (!initializer) return undefined
|
|
323
|
-
context.resolving.add(current.text)
|
|
324
|
-
const value = ts.isFunctionDeclaration(initializer) ? undefined : staticValue(initializer, context)
|
|
325
|
-
context.resolving.delete(current.text)
|
|
326
|
-
return value
|
|
327
|
-
}
|
|
328
|
-
if (ts.isCallExpression(current)) {
|
|
329
|
-
if (ts.isIdentifier(current.expression)) {
|
|
330
|
-
const callable = context.initializers.get(current.expression.text)
|
|
331
|
-
if (callable && (ts.isArrowFunction(callable) || ts.isFunctionExpression(callable) || ts.isFunctionDeclaration(callable))) {
|
|
332
|
-
const initializers = new Map(context.initializers)
|
|
333
|
-
callable.parameters.forEach((parameter, index) => {
|
|
334
|
-
if (ts.isIdentifier(parameter.name) && current.arguments[index]) {
|
|
335
|
-
initializers.set(parameter.name.text, current.arguments[index])
|
|
336
|
-
}
|
|
337
|
-
})
|
|
338
|
-
const childContext: StaticContext = {
|
|
339
|
-
initializers,
|
|
340
|
-
sourceFile: context.sourceFile,
|
|
341
|
-
resolving: new Set(context.resolving),
|
|
342
|
-
}
|
|
343
|
-
if (callable.body && ts.isExpression(callable.body)) return staticValue(callable.body, childContext)
|
|
344
|
-
const returnStatement = callable.body?.statements.find(ts.isReturnStatement)
|
|
345
|
-
if (returnStatement?.expression) return staticValue(returnStatement.expression, childContext)
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
const firstArgument = current.arguments[0]
|
|
349
|
-
if (firstArgument) {
|
|
350
|
-
const value = staticValue(firstArgument, context)
|
|
351
|
-
if (!isStaticObject(value) || !ts.isIdentifier(current.expression)) return value
|
|
352
|
-
if (current.expression.text === 'dataTableExtensionHost') return { family: 'data-table', ...value }
|
|
353
|
-
if (current.expression.text === 'crudFormExtensionHost') return { family: 'crud-form', ...value }
|
|
354
|
-
if (current.expression.text === 'componentExtensionHost') return { family: 'component-handle', ...value }
|
|
355
|
-
return value
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
if (ts.isConditionalExpression(current)) {
|
|
359
|
-
const left = staticValue(current.whenTrue, context)
|
|
360
|
-
const right = staticValue(current.whenFalse, context)
|
|
361
|
-
return left !== undefined && JSON.stringify(left) === JSON.stringify(right) ? left : undefined
|
|
362
|
-
}
|
|
363
|
-
return undefined
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
function isStaticObject(value: StaticValue | undefined): value is StaticObject {
|
|
367
|
-
return !!value && typeof value === 'object' && !Array.isArray(value)
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
function strings(value: StaticValue | undefined): string[] {
|
|
371
|
-
return Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === 'string') : []
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
function stringValue(value: StaticValue | undefined): string | undefined {
|
|
375
|
-
return typeof value === 'string' ? value : undefined
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
function numberValue(value: StaticValue | undefined): number | undefined {
|
|
379
|
-
if (typeof value === 'number') return value
|
|
380
|
-
if (typeof value === 'string' && /^\d+$/.test(value)) return Number(value)
|
|
381
|
-
return undefined
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
function booleanValue(value: StaticValue | undefined): boolean | undefined {
|
|
385
|
-
return typeof value === 'boolean' ? value : undefined
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
function portablePath(moduleRoot: string, sourceRoot: string, filePath: string): string {
|
|
389
|
-
return path.posix.join(sourceRoot, path.relative(moduleRoot, filePath).split(path.sep).join('/'))
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
function conventionPath(moduleRoot: string, relativePath: string): string | null {
|
|
393
|
-
const direct = path.join(moduleRoot, relativePath)
|
|
394
|
-
if (fs.existsSync(direct)) return direct
|
|
395
|
-
if (relativePath.endsWith('.ts')) {
|
|
396
|
-
const tsx = `${direct.slice(0, -3)}.tsx`
|
|
397
|
-
if (fs.existsSync(tsx)) return tsx
|
|
398
|
-
}
|
|
399
|
-
return null
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
export function readConventionObjectArray(filePath: string, exportName: string): StaticObject[] {
|
|
403
|
-
const file = sourceFile(filePath)
|
|
404
|
-
if (!file) return []
|
|
405
|
-
const context = buildStaticContext(file)
|
|
406
|
-
const initializer = context.initializers.get(exportName)
|
|
407
|
-
if (!initializer || ts.isFunctionDeclaration(initializer)) return []
|
|
408
|
-
const value = staticValue(initializer, context)
|
|
409
|
-
return Array.isArray(value) ? value.filter(isStaticObject) : []
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
function readRootObject(filePath: string, variableName: string): StaticObject | null {
|
|
413
|
-
const file = sourceFile(filePath)
|
|
414
|
-
if (!file) return null
|
|
415
|
-
const context = buildStaticContext(file)
|
|
416
|
-
const initializer = context.initializers.get(variableName)
|
|
417
|
-
if (!initializer || ts.isFunctionDeclaration(initializer)) return null
|
|
418
|
-
const value = staticValue(initializer, context)
|
|
419
|
-
return isStaticObject(value) ? value : null
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
function readCallObjectArguments(filePath: string, calleeNames: ReadonlySet<string>): Array<{
|
|
423
|
-
callee: string
|
|
424
|
-
value: StaticObject
|
|
425
|
-
}> {
|
|
426
|
-
const file = sourceFile(filePath)
|
|
427
|
-
if (!file) return []
|
|
428
|
-
const context = buildStaticContext(file)
|
|
429
|
-
const result: Array<{ callee: string; value: StaticObject }> = []
|
|
430
|
-
const visit = (node: ts.Node): void => {
|
|
431
|
-
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && calleeNames.has(node.expression.text)) {
|
|
432
|
-
const firstArgument = node.arguments[0]
|
|
433
|
-
const value = firstArgument ? staticValue(firstArgument, context) : undefined
|
|
434
|
-
if (isStaticObject(value)) result.push({ callee: node.expression.text, value })
|
|
435
|
-
}
|
|
436
|
-
node.forEachChild(visit)
|
|
437
|
-
}
|
|
438
|
-
file.forEachChild(visit)
|
|
439
|
-
return result
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
function sortHosts(hosts: ModuleExtensionHostFact[]): ModuleExtensionHostFact[] {
|
|
443
|
-
const sourceKey = (host: ModuleExtensionHostFact): string => host.source.kind === 'fact-ref'
|
|
444
|
-
? `${host.source.factSection}:${host.source.factKey}`
|
|
445
|
-
: `${host.source.path}:${host.source.symbol}`
|
|
446
|
-
return hosts.sort((left, right) =>
|
|
447
|
-
left.family.localeCompare(right.family)
|
|
448
|
-
|| left.id.localeCompare(right.id)
|
|
449
|
-
|| sourceKey(left).localeCompare(sourceKey(right))
|
|
450
|
-
|| left.key.localeCompare(right.key))
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
function sortContributions(contributions: ModuleExtensionContributionFact[]): ModuleExtensionContributionFact[] {
|
|
454
|
-
const targetKey = (contribution: ModuleExtensionContributionFact): string => contribution.targets
|
|
455
|
-
.map((entry) => entry.id)
|
|
456
|
-
.join('\u0000')
|
|
457
|
-
const sourceKey = (contribution: ModuleExtensionContributionFact): string =>
|
|
458
|
-
`${contribution.source.path}:${contribution.source.symbol ?? ''}`
|
|
459
|
-
return contributions.sort((left, right) =>
|
|
460
|
-
left.kind.localeCompare(right.kind)
|
|
461
|
-
|| left.id.localeCompare(right.id)
|
|
462
|
-
|| targetKey(left).localeCompare(targetKey(right))
|
|
463
|
-
|| sourceKey(left).localeCompare(sourceKey(right)))
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
function target(id: string, resolution: ModuleExtensionTargetFact['resolution'] = 'unresolved'): ModuleExtensionTargetFact {
|
|
467
|
-
return { id, resolution }
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
function factTarget(id: string, factSection: string, factKey = id): ModuleExtensionTargetFact {
|
|
471
|
-
return { id, resolution: 'fact-ref', factRef: { factSection, factKey } }
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
function openTarget(id: string): ModuleExtensionTargetFact {
|
|
475
|
-
return target(id)
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
function declarationSource(sourceRoot: string, symbol: string): ModuleExtensionHostFact['source'] {
|
|
479
|
-
return { kind: 'declaration', path: path.posix.join(sourceRoot, 'extension-points.ts'), symbol }
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
function hasDeclarationBinding(
|
|
483
|
-
moduleRoot: string,
|
|
484
|
-
hostKey: string,
|
|
485
|
-
declaration: StaticObject,
|
|
486
|
-
): boolean {
|
|
487
|
-
const relativeSource = stringValue(declaration.source)
|
|
488
|
-
if (!relativeSource) return false
|
|
489
|
-
const bindingFile = sourceFile(path.join(moduleRoot, relativeSource))
|
|
490
|
-
if (!bindingFile) return false
|
|
491
|
-
const escapedHostKey = hostKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
492
|
-
const bindingPattern = new RegExp(
|
|
493
|
-
`extensionPoints\\s*\\.\\s*hosts(?:\\s*\\.\\s*${escapedHostKey}|\\s*\\[\\s*['\"]${escapedHostKey}['\"]\\s*\\])`,
|
|
494
|
-
)
|
|
495
|
-
if (bindingPattern.test(bindingFile.text)) return true
|
|
496
|
-
return declaration.family === 'integration'
|
|
497
|
-
&& typeof declaration.pattern === 'string'
|
|
498
|
-
&& /resolveIntegrationDetailWidgetSpotId\s*\(/.test(bindingFile.text)
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
function baseHost(options: {
|
|
502
|
-
key: string
|
|
503
|
-
id: string
|
|
504
|
-
moduleId: string
|
|
505
|
-
family: ExtensionHostFamily
|
|
506
|
-
capabilities: ExtensionHostCapability[]
|
|
507
|
-
sourceRoot: string
|
|
508
|
-
declaration: StaticObject
|
|
509
|
-
bound?: boolean
|
|
510
|
-
resolution?: ModuleExtensionHostFact['resolution']
|
|
511
|
-
}): ModuleExtensionHostFact {
|
|
512
|
-
const { declaration } = options
|
|
513
|
-
const aliases = strings(declaration.aliases)
|
|
514
|
-
const fallbacks = strings(declaration.fallbacks)
|
|
515
|
-
const fact: ModuleExtensionHostFact = {
|
|
516
|
-
key: options.key,
|
|
517
|
-
id: options.id,
|
|
518
|
-
resolution: options.resolution ?? (options.id.includes('{') || options.id.includes('*') ? 'pattern' : 'exact'),
|
|
519
|
-
family: options.family,
|
|
520
|
-
ownerModule: options.moduleId,
|
|
521
|
-
capabilities: options.capabilities,
|
|
522
|
-
bound: options.bound ?? true,
|
|
523
|
-
stability: 'frozen',
|
|
524
|
-
source: declarationSource(options.sourceRoot, `extensionPoints.hosts.${options.key.split('.')[0]}`),
|
|
525
|
-
}
|
|
526
|
-
const contextContract = stringValue(declaration.contextContract)
|
|
527
|
-
const dataContract = stringValue(declaration.dataContract)
|
|
528
|
-
const scopeContract = stringValue(declaration.scopeContract)
|
|
529
|
-
const runtimeContract = stringValue(declaration.runtimeContract)
|
|
530
|
-
const activation = stringValue(declaration.activation) as ModuleExtensionHostFact['activation']
|
|
531
|
-
if (contextContract) fact.contextContract = contextContract
|
|
532
|
-
if (dataContract) fact.dataContract = dataContract
|
|
533
|
-
if (scopeContract) fact.scopeContract = scopeContract
|
|
534
|
-
if (runtimeContract) fact.runtimeContract = runtimeContract
|
|
535
|
-
if (activation) fact.activation = activation
|
|
536
|
-
if (aliases.length > 0) fact.aliases = aliases
|
|
537
|
-
if (fallbacks.length > 0) fact.fallbacks = fallbacks
|
|
538
|
-
return fact
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
function extractDeclaredHosts(options: ExtractModuleExtensionFactsOptions): {
|
|
542
|
-
hosts: ModuleExtensionHostFact[]
|
|
543
|
-
unresolved: ModuleExtensionUnresolvedFact[]
|
|
544
|
-
} {
|
|
545
|
-
const filePath = conventionPath(options.moduleRoot, 'extension-points.ts')
|
|
546
|
-
if (!filePath) return { hosts: [], unresolved: [] }
|
|
547
|
-
const declaration = readRootObject(filePath, 'extensionPoints')
|
|
548
|
-
const hostsObject = declaration && isStaticObject(declaration.hosts) ? declaration.hosts : null
|
|
549
|
-
if (!hostsObject) {
|
|
550
|
-
return {
|
|
551
|
-
hosts: [],
|
|
552
|
-
unresolved: [{
|
|
553
|
-
key: `${options.moduleId}.extension-points`,
|
|
554
|
-
source: { path: portablePath(options.moduleRoot, options.sourceRoot, filePath), symbol: 'extensionPoints' },
|
|
555
|
-
reason: 'unclassified-binding',
|
|
556
|
-
}],
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
const hosts: ModuleExtensionHostFact[] = []
|
|
561
|
-
const unresolved: ModuleExtensionUnresolvedFact[] = []
|
|
562
|
-
for (const hostKey of Object.keys(hostsObject).sort((left, right) => left.localeCompare(right))) {
|
|
563
|
-
const host = hostsObject[hostKey]
|
|
564
|
-
if (!isStaticObject(host)) continue
|
|
565
|
-
const family = stringValue(host.family) as ExtensionHostFamily | undefined
|
|
566
|
-
if (!family) continue
|
|
567
|
-
const declarationBound = hasDeclarationBinding(options.moduleRoot, hostKey, host)
|
|
568
|
-
if (!declarationBound) {
|
|
569
|
-
unresolved.push({
|
|
570
|
-
key: `${options.moduleId}.${hostKey}`,
|
|
571
|
-
source: {
|
|
572
|
-
path: stringValue(host.source) ?? portablePath(options.moduleRoot, options.sourceRoot, filePath),
|
|
573
|
-
symbol: `extensionPoints.hosts.${hostKey}`,
|
|
574
|
-
},
|
|
575
|
-
reason: 'unbound-declaration',
|
|
576
|
-
})
|
|
577
|
-
}
|
|
578
|
-
if (family === 'data-table') {
|
|
579
|
-
const tableId = stringValue(host.tableId)
|
|
580
|
-
if (!tableId) continue
|
|
581
|
-
const baseSpotId = stringValue(host.baseSpotId) ?? `data-table:${tableId}`
|
|
582
|
-
for (const surface of DATA_TABLE_EXTENSION_SURFACES) {
|
|
583
|
-
const usesBase = ['header', 'footer', 'toolbar', 'searchTrailing', 'emptyState'].includes(surface.key)
|
|
584
|
-
const id = surface.key === 'replacement'
|
|
585
|
-
? `data-table:${tableId}`
|
|
586
|
-
: `${usesBase ? baseSpotId : `data-table:${tableId}`}:${surface.suffix}`
|
|
587
|
-
hosts.push(baseHost({
|
|
588
|
-
key: `${hostKey}.${surface.key}`,
|
|
589
|
-
id,
|
|
590
|
-
moduleId: options.moduleId,
|
|
591
|
-
family,
|
|
592
|
-
capabilities: [...surface.capabilities],
|
|
593
|
-
sourceRoot: options.sourceRoot,
|
|
594
|
-
declaration: host,
|
|
595
|
-
bound: surface.bound && declarationBound,
|
|
596
|
-
}))
|
|
597
|
-
}
|
|
598
|
-
continue
|
|
599
|
-
}
|
|
600
|
-
if (family === 'crud-form') {
|
|
601
|
-
const entityId = stringValue(host.entityId)
|
|
602
|
-
if (!entityId) continue
|
|
603
|
-
const spotId = stringValue(host.spotId) ?? `crud-form:${entityId}`
|
|
604
|
-
for (const surface of CRUD_FORM_EXTENSION_SURFACES) {
|
|
605
|
-
const id = surface.suffix ? `${spotId}:${surface.suffix}` : spotId
|
|
606
|
-
const fact = baseHost({
|
|
607
|
-
key: `${hostKey}.${surface.key}`,
|
|
608
|
-
id,
|
|
609
|
-
moduleId: options.moduleId,
|
|
610
|
-
family,
|
|
611
|
-
capabilities: [...surface.capabilities],
|
|
612
|
-
sourceRoot: options.sourceRoot,
|
|
613
|
-
declaration: host,
|
|
614
|
-
bound: surface.bound && declarationBound,
|
|
615
|
-
})
|
|
616
|
-
if (surface.key === 'base') fact.phases = [...CRUD_FORM_LIFECYCLE_PHASES]
|
|
617
|
-
hosts.push(fact)
|
|
618
|
-
}
|
|
619
|
-
continue
|
|
620
|
-
}
|
|
621
|
-
const id = stringValue(host.spotId) ?? stringValue(host.pattern) ?? stringValue(host.componentId)
|
|
622
|
-
if (!id) {
|
|
623
|
-
unresolved.push({
|
|
624
|
-
key: `${options.moduleId}.${hostKey}`,
|
|
625
|
-
source: { path: portablePath(options.moduleRoot, options.sourceRoot, filePath), symbol: hostKey },
|
|
626
|
-
reason: 'dynamic-without-pattern',
|
|
627
|
-
})
|
|
628
|
-
continue
|
|
629
|
-
}
|
|
630
|
-
const capabilities = family === 'component-handle'
|
|
631
|
-
? ['component-replacement'] as ExtensionHostCapability[]
|
|
632
|
-
: strings(host.supported) as ExtensionHostCapability[]
|
|
633
|
-
const fact = baseHost({
|
|
634
|
-
key: hostKey,
|
|
635
|
-
id,
|
|
636
|
-
moduleId: options.moduleId,
|
|
637
|
-
family,
|
|
638
|
-
capabilities,
|
|
639
|
-
sourceRoot: options.sourceRoot,
|
|
640
|
-
declaration: host,
|
|
641
|
-
bound: declarationBound,
|
|
642
|
-
})
|
|
643
|
-
if (isStaticObject(host.parameters)) {
|
|
644
|
-
fact.patternParameters = Object.fromEntries(Object.entries(host.parameters).flatMap(([key, value]) => {
|
|
645
|
-
if (!isStaticObject(value) || typeof value.source !== 'string') return []
|
|
646
|
-
return [[key, {
|
|
647
|
-
source: value.source,
|
|
648
|
-
...(typeof value.pattern === 'string' ? { pattern: value.pattern } : {}),
|
|
649
|
-
}]]
|
|
650
|
-
}))
|
|
651
|
-
}
|
|
652
|
-
hosts.push(fact)
|
|
653
|
-
}
|
|
654
|
-
return { hosts: sortHosts(hosts), unresolved }
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
function factRefHost(options: {
|
|
658
|
-
key: string
|
|
659
|
-
id: string
|
|
660
|
-
moduleId: string
|
|
661
|
-
family: ExtensionHostFamily
|
|
662
|
-
capabilities: ExtensionHostCapability[]
|
|
663
|
-
factSection: string
|
|
664
|
-
/** Key inside `factSection`; defaults to the host id when the two coincide. */
|
|
665
|
-
factKey?: string
|
|
666
|
-
phases?: string[]
|
|
667
|
-
activation?: ModuleExtensionHostFact['activation']
|
|
668
|
-
scopeContract?: string
|
|
669
|
-
}): ModuleExtensionHostFact {
|
|
670
|
-
return {
|
|
671
|
-
key: options.key,
|
|
672
|
-
id: options.id,
|
|
673
|
-
resolution: 'fact-ref',
|
|
674
|
-
family: options.family,
|
|
675
|
-
ownerModule: options.moduleId,
|
|
676
|
-
capabilities: options.capabilities,
|
|
677
|
-
...(options.phases ? { phases: options.phases } : {}),
|
|
678
|
-
...(options.activation ? { activation: options.activation } : {}),
|
|
679
|
-
...(options.scopeContract ? { scopeContract: options.scopeContract } : {}),
|
|
680
|
-
bound: true,
|
|
681
|
-
stability: 'stable',
|
|
682
|
-
source: { kind: 'fact-ref', factSection: options.factSection, factKey: options.factKey ?? options.id },
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
function sourceFilesBelow(directory: string): string[] {
|
|
687
|
-
const cached = activeExtractionCache?.sourceFilesBelow.get(directory)
|
|
688
|
-
if (cached) return cached
|
|
689
|
-
if (!fs.existsSync(directory)) return []
|
|
690
|
-
const files: string[] = []
|
|
691
|
-
const visit = (current: string): void => {
|
|
692
|
-
for (const entry of fs.readdirSync(current, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
|
|
693
|
-
const entryPath = path.join(current, entry.name)
|
|
694
|
-
if (entry.isDirectory()) {
|
|
695
|
-
if (entry.name !== '__tests__' && entry.name !== '__integration__') visit(entryPath)
|
|
696
|
-
continue
|
|
697
|
-
}
|
|
698
|
-
if (entry.isFile() && /\.(?:ts|tsx)$/.test(entry.name) && !/\.(?:test|spec)\./.test(entry.name)) files.push(entryPath)
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
visit(directory)
|
|
702
|
-
activeExtractionCache?.sourceFilesBelow.set(directory, files)
|
|
703
|
-
return files
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
function apiRouteId(moduleId: string, moduleRoot: string, filePath: string): string | null {
|
|
707
|
-
const relative = path.relative(path.join(moduleRoot, 'api'), filePath).split(path.sep).join('/')
|
|
708
|
-
if (relative === 'interceptors.ts' || relative === 'openapi.ts') return null
|
|
709
|
-
const withoutExtension = relative.replace(/\.tsx?$/, '')
|
|
710
|
-
const suffix = withoutExtension.endsWith('/route') ? withoutExtension.slice(0, -6) : withoutExtension
|
|
711
|
-
return suffix && !suffix.includes('__tests__') ? `${moduleId.replace(/_/g, '-')}/${suffix}` : null
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
function apiExtensionHostIds(moduleId: string, moduleRoot: string): ApiExtensionHostIds {
|
|
715
|
-
const cacheKey = `${moduleId}\0${moduleRoot}`
|
|
716
|
-
const cached = activeExtractionCache?.apiExtensionHostIds.get(cacheKey)
|
|
717
|
-
if (cached) return cached
|
|
718
|
-
const entityIds = new Set<string>()
|
|
719
|
-
const entitySourceFiles: Record<string, string> = {}
|
|
720
|
-
const commandIds = new Set<string>()
|
|
721
|
-
const routeIds = new Set<string>()
|
|
722
|
-
for (const filePath of sourceFilesBelow(path.join(moduleRoot, 'api'))) {
|
|
723
|
-
const routeId = apiRouteId(moduleId, moduleRoot, filePath)
|
|
724
|
-
if (routeId) routeIds.add(routeId)
|
|
725
|
-
const file = sourceFile(filePath)
|
|
726
|
-
if (!file) continue
|
|
727
|
-
const context = buildStaticContext(file)
|
|
728
|
-
const visit = (node: ts.Node): void => {
|
|
729
|
-
if (ts.isPropertyAssignment(node)) {
|
|
730
|
-
const name = propertyName(node.name)
|
|
731
|
-
if (name === 'commandId') {
|
|
732
|
-
const id = stringValue(staticValue(node.initializer, context))
|
|
733
|
-
if (id) commandIds.add(id)
|
|
734
|
-
}
|
|
735
|
-
if (name === 'enrichers') {
|
|
736
|
-
const config = staticValue(node.initializer, context)
|
|
737
|
-
const entityId = isStaticObject(config) ? stringValue(config.entityId) : undefined
|
|
738
|
-
if (entityId) {
|
|
739
|
-
entityIds.add(entityId)
|
|
740
|
-
if (!entitySourceFiles[entityId]) entitySourceFiles[entityId] = filePath
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
if (
|
|
745
|
-
ts.isCallExpression(node)
|
|
746
|
-
&& ts.isIdentifier(node.expression)
|
|
747
|
-
&& node.expression.text === 'validateCrudMutationGuard'
|
|
748
|
-
) {
|
|
749
|
-
const input = node.arguments[1] ? staticValue(node.arguments[1], context) : undefined
|
|
750
|
-
const entityId = isStaticObject(input) ? stringValue(input.resourceKind) : undefined
|
|
751
|
-
if (entityId) {
|
|
752
|
-
entityIds.add(entityId)
|
|
753
|
-
if (!entitySourceFiles[entityId]) entitySourceFiles[entityId] = filePath
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
node.forEachChild(visit)
|
|
757
|
-
}
|
|
758
|
-
file.forEachChild(visit)
|
|
759
|
-
}
|
|
760
|
-
const result = {
|
|
761
|
-
entityIds: [...entityIds].sort((left, right) => left.localeCompare(right)),
|
|
762
|
-
entitySourceFiles,
|
|
763
|
-
commandIds: [...commandIds].sort((left, right) => left.localeCompare(right)),
|
|
764
|
-
routeIds: [...routeIds].sort((left, right) => left.localeCompare(right)),
|
|
765
|
-
}
|
|
766
|
-
activeExtractionCache?.apiExtensionHostIds.set(cacheKey, result)
|
|
767
|
-
return result
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
function moduleCommandIds(moduleRoot: string, apiCommandIds: readonly string[]): string[] {
|
|
771
|
-
const commandIds = new Set(apiCommandIds)
|
|
772
|
-
for (const filePath of sourceFilesBelow(path.join(moduleRoot, 'commands'))) {
|
|
773
|
-
if (path.basename(filePath).replace(/\.(?:ts|tsx)$/, '') === 'interceptors') continue
|
|
774
|
-
for (const id of extractCommandIdsFromSource(filePath)) commandIds.add(id)
|
|
775
|
-
}
|
|
776
|
-
return [...commandIds].sort((left, right) => left.localeCompare(right))
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
export function extractKnownApiRouteIds(moduleId: string, moduleRoot: string): string[] {
|
|
780
|
-
return apiExtensionHostIds(moduleId, moduleRoot).routeIds
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
export function extractKnownCommandIds(moduleId: string, moduleRoot: string): string[] {
|
|
784
|
-
const apiHosts = apiExtensionHostIds(moduleId, moduleRoot)
|
|
785
|
-
return moduleCommandIds(moduleRoot, apiHosts.commandIds)
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
const HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] as const
|
|
789
|
-
export type ApiInterceptorPhase = 'before' | 'after'
|
|
790
|
-
export type ApiRouteInterceptorBridge = {
|
|
791
|
-
method: (typeof HTTP_METHODS)[number]
|
|
792
|
-
phases: ApiInterceptorPhase[]
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
const API_INTERCEPTOR_BRIDGE_CALLS: Record<string, ApiInterceptorPhase> = {
|
|
796
|
-
runApiInterceptorsBefore: 'before',
|
|
797
|
-
runApiInterceptorsAfter: 'after',
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
function isHttpMethodName(value: string): value is (typeof HTTP_METHODS)[number] {
|
|
801
|
-
return (HTTP_METHODS as readonly string[]).includes(value)
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
/** HTTP method handlers a route file actually exports. */
|
|
805
|
-
function exportedHttpMethods(file: ts.SourceFile): Array<(typeof HTTP_METHODS)[number]> {
|
|
806
|
-
const methods = new Set<(typeof HTTP_METHODS)[number]>()
|
|
807
|
-
const consider = (name: string | undefined): void => {
|
|
808
|
-
if (name && isHttpMethodName(name)) methods.add(name)
|
|
809
|
-
}
|
|
810
|
-
const considerBindingName = (name: ts.BindingName): void => {
|
|
811
|
-
if (ts.isIdentifier(name)) {
|
|
812
|
-
consider(name.text)
|
|
813
|
-
return
|
|
814
|
-
}
|
|
815
|
-
for (const element of name.elements) {
|
|
816
|
-
if (ts.isBindingElement(element)) considerBindingName(element.name)
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
const visit = (node: ts.Node): void => {
|
|
820
|
-
if (ts.isVariableStatement(node) && node.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)) {
|
|
821
|
-
for (const declaration of node.declarationList.declarations) {
|
|
822
|
-
considerBindingName(declaration.name)
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
if (
|
|
826
|
-
ts.isFunctionDeclaration(node)
|
|
827
|
-
&& node.name
|
|
828
|
-
&& node.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)
|
|
829
|
-
) {
|
|
830
|
-
consider(node.name.text)
|
|
831
|
-
}
|
|
832
|
-
if (ts.isExportDeclaration(node) && node.exportClause && ts.isNamedExports(node.exportClause)) {
|
|
833
|
-
for (const element of node.exportClause.elements) consider(element.name.text)
|
|
834
|
-
}
|
|
835
|
-
node.forEachChild(visit)
|
|
836
|
-
}
|
|
837
|
-
file.forEachChild(visit)
|
|
838
|
-
return [...methods].sort((left, right) => left.localeCompare(right))
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
/**
|
|
842
|
-
* The interceptor pipeline runs only where a route wires it: `makeCrudRoute` runs
|
|
843
|
-
* both phases for every method it handles, and a hand-written route runs whichever
|
|
844
|
-
* of `runApiInterceptorsBefore` / `runApiInterceptorsAfter` it calls. A custom route
|
|
845
|
-
* that calls neither has no bridge, so an interceptor targeting it is never bound.
|
|
846
|
-
*/
|
|
847
|
-
function extractApiRouteInterceptorBridges(file: ts.SourceFile, context: StaticContext): ApiRouteInterceptorBridge[] {
|
|
848
|
-
const exportedMethods = exportedHttpMethods(file)
|
|
849
|
-
const phasesByMethod = new Map<(typeof HTTP_METHODS)[number], Set<ApiInterceptorPhase>>()
|
|
850
|
-
const addPhase = (method: (typeof HTTP_METHODS)[number], phase: ApiInterceptorPhase): void => {
|
|
851
|
-
const phases = phasesByMethod.get(method) ?? new Set<ApiInterceptorPhase>()
|
|
852
|
-
phases.add(phase)
|
|
853
|
-
phasesByMethod.set(method, phases)
|
|
854
|
-
}
|
|
855
|
-
const visit = (node: ts.Node): void => {
|
|
856
|
-
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
|
|
857
|
-
const calleeName = node.expression.text
|
|
858
|
-
if (calleeName === 'makeCrudRoute') {
|
|
859
|
-
for (const method of exportedMethods) {
|
|
860
|
-
addPhase(method, 'before')
|
|
861
|
-
addPhase(method, 'after')
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
const phase = API_INTERCEPTOR_BRIDGE_CALLS[calleeName]
|
|
865
|
-
if (phase) {
|
|
866
|
-
const callOptions = node.arguments[0] ? staticValue(node.arguments[0], context) : undefined
|
|
867
|
-
const declaredMethod = isStaticObject(callOptions) ? stringValue(callOptions.method) : undefined
|
|
868
|
-
const methods = declaredMethod && isHttpMethodName(declaredMethod) ? [declaredMethod] : exportedMethods
|
|
869
|
-
for (const method of methods) addPhase(method, phase)
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
node.forEachChild(visit)
|
|
873
|
-
}
|
|
874
|
-
file.forEachChild(visit)
|
|
875
|
-
return [...phasesByMethod.entries()]
|
|
876
|
-
.map(([method, phases]) => ({
|
|
877
|
-
method,
|
|
878
|
-
phases: [...phases].sort((left, right) => left.localeCompare(right)),
|
|
879
|
-
}))
|
|
880
|
-
.sort((left, right) => left.method.localeCompare(right.method))
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
/**
|
|
884
|
-
* Concrete runtime-id owners a module contributes as activation targets: its real
|
|
885
|
-
* `api/**` route ids (with the interceptor bridges each route actually wires) and
|
|
886
|
-
* its `commands/**` command ids (dispatched through the command bus). Used
|
|
887
|
-
* post-selection to synthesize `host-reference`/`owner-reference` bindings with a
|
|
888
|
-
* portable source.
|
|
889
|
-
*/
|
|
890
|
-
export function extractActivationTargetOwners(options: {
|
|
891
|
-
moduleId: string
|
|
892
|
-
moduleRoot: string
|
|
893
|
-
sourceRoot: string
|
|
894
|
-
}): {
|
|
895
|
-
apiRoutes: Array<{ id: string; source: ModuleFactSourceRef; bridges: ApiRouteInterceptorBridge[] }>
|
|
896
|
-
commands: Array<{ id: string; source: ModuleFactSourceRef }>
|
|
897
|
-
} {
|
|
898
|
-
const apiRoutes: Array<{ id: string; source: ModuleFactSourceRef; bridges: ApiRouteInterceptorBridge[] }> = []
|
|
899
|
-
for (const filePath of sourceFilesBelow(path.join(options.moduleRoot, 'api'))) {
|
|
900
|
-
const routeId = apiRouteId(options.moduleId, options.moduleRoot, filePath)
|
|
901
|
-
if (!routeId) continue
|
|
902
|
-
const file = sourceFile(filePath)
|
|
903
|
-
const bridges = file ? extractApiRouteInterceptorBridges(file, buildStaticContext(file)) : []
|
|
904
|
-
apiRoutes.push({
|
|
905
|
-
id: routeId,
|
|
906
|
-
source: { sourcePath: portablePath(options.moduleRoot, options.sourceRoot, filePath) },
|
|
907
|
-
bridges,
|
|
908
|
-
})
|
|
909
|
-
}
|
|
910
|
-
const commands: Array<{ id: string; source: ModuleFactSourceRef }> = []
|
|
911
|
-
const seenCommands = new Set<string>()
|
|
912
|
-
for (const filePath of sourceFilesBelow(path.join(options.moduleRoot, 'commands'))) {
|
|
913
|
-
if (path.basename(filePath).replace(/\.(?:ts|tsx)$/, '') === 'interceptors') continue
|
|
914
|
-
const source: ModuleFactSourceRef = { sourcePath: portablePath(options.moduleRoot, options.sourceRoot, filePath) }
|
|
915
|
-
for (const id of extractCommandIdsFromSource(filePath)) {
|
|
916
|
-
if (seenCommands.has(id)) continue
|
|
917
|
-
seenCommands.add(id)
|
|
918
|
-
commands.push({ id, source })
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
return { apiRoutes, commands }
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
function extractFactRefHosts(options: ExtractModuleExtensionFactsOptions): ModuleExtensionHostFact[] {
|
|
925
|
-
const hosts: ModuleExtensionHostFact[] = []
|
|
926
|
-
for (const entity of options.entities) {
|
|
927
|
-
hosts.push(factRefHost({
|
|
928
|
-
key: `entity.${entity.id}`,
|
|
929
|
-
id: entity.id,
|
|
930
|
-
moduleId: options.moduleId,
|
|
931
|
-
family: 'entity',
|
|
932
|
-
capabilities: ['response-enricher', 'query-enricher', 'mutation-guard', 'entity-extension'],
|
|
933
|
-
factSection: 'entities',
|
|
934
|
-
activation: 'host-opt-in',
|
|
935
|
-
scopeContract: 'tenant-and-organization',
|
|
936
|
-
}))
|
|
937
|
-
}
|
|
938
|
-
for (const route of options.apiRoutes) {
|
|
939
|
-
hosts.push(factRefHost({
|
|
940
|
-
key: `api-route.${route.path}`,
|
|
941
|
-
id: route.path,
|
|
942
|
-
moduleId: options.moduleId,
|
|
943
|
-
family: 'api-route',
|
|
944
|
-
capabilities: ['api-interceptor', 'mutation-guard'],
|
|
945
|
-
factSection: 'apiRoutes',
|
|
946
|
-
phases: ['before', 'after'],
|
|
947
|
-
activation: 'host-opt-in',
|
|
948
|
-
scopeContract: 'request-auth-scope',
|
|
949
|
-
}))
|
|
950
|
-
}
|
|
951
|
-
for (const event of options.events) {
|
|
952
|
-
const capabilities: ExtensionHostCapability[] = ['async-subscriber', 'sync-subscriber']
|
|
953
|
-
if (event.clientBroadcast) capabilities.push('browser-client')
|
|
954
|
-
if (event.portalBroadcast) capabilities.push('browser-portal')
|
|
955
|
-
hosts.push(factRefHost({
|
|
956
|
-
key: `event.${event.id}`,
|
|
957
|
-
id: event.id,
|
|
958
|
-
moduleId: options.moduleId,
|
|
959
|
-
family: 'event',
|
|
960
|
-
capabilities,
|
|
961
|
-
factSection: 'events',
|
|
962
|
-
scopeContract: 'tenant-organization-and-audience',
|
|
963
|
-
}))
|
|
964
|
-
}
|
|
965
|
-
for (const entityId of options.searchEntities) {
|
|
966
|
-
const eventStem = entityId.replace(':', '.')
|
|
967
|
-
for (const phase of ['querying', 'queried'] as const) {
|
|
968
|
-
hosts.push(factRefHost({
|
|
969
|
-
key: `query-lifecycle.${eventStem}.${phase}`,
|
|
970
|
-
id: `${eventStem}.${phase}`,
|
|
971
|
-
moduleId: options.moduleId,
|
|
972
|
-
family: 'query-lifecycle',
|
|
973
|
-
capabilities: ['sync-subscriber'],
|
|
974
|
-
factSection: 'searchEntities',
|
|
975
|
-
factKey: entityId,
|
|
976
|
-
phases: phase === 'querying' ? ['block', 'query-transform'] : ['result-transform', 'scope-reapply'],
|
|
977
|
-
activation: 'caller-opt-in',
|
|
978
|
-
scopeContract: 'tenant-and-organization-reapplied-after-result',
|
|
979
|
-
}))
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
const apiHosts = apiExtensionHostIds(options.moduleId, options.moduleRoot)
|
|
983
|
-
for (const entityId of apiHosts.entityIds) {
|
|
984
|
-
const declaringFile = apiHosts.entitySourceFiles[entityId]
|
|
985
|
-
const host = factRefHost({
|
|
986
|
-
key: `api-entity.${entityId}`,
|
|
987
|
-
id: entityId,
|
|
988
|
-
moduleId: options.moduleId,
|
|
989
|
-
family: 'entity',
|
|
990
|
-
capabilities: ['response-enricher', 'query-enricher', 'mutation-guard'],
|
|
991
|
-
factSection: 'apiRoutes',
|
|
992
|
-
activation: 'host-opt-in',
|
|
993
|
-
scopeContract: 'tenant-and-organization',
|
|
994
|
-
})
|
|
995
|
-
hosts.push(declaringFile
|
|
996
|
-
? {
|
|
997
|
-
...host,
|
|
998
|
-
source: {
|
|
999
|
-
kind: 'declaration',
|
|
1000
|
-
path: portablePath(options.moduleRoot, options.sourceRoot, declaringFile),
|
|
1001
|
-
symbol: entityId,
|
|
1002
|
-
},
|
|
1003
|
-
}
|
|
1004
|
-
: host)
|
|
1005
|
-
}
|
|
1006
|
-
return sortHosts([...new Map(hosts.map((host) => [`${host.family}:${host.id}`, host])).values()])
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
function contributionBase(
|
|
1010
|
-
id: string,
|
|
1011
|
-
sourcePath: string,
|
|
1012
|
-
sourceSymbol?: string,
|
|
1013
|
-
): ModuleExtensionContributionBase {
|
|
1014
|
-
return {
|
|
1015
|
-
id,
|
|
1016
|
-
targets: [],
|
|
1017
|
-
scopeContract: 'tenant-and-organization',
|
|
1018
|
-
source: { path: sourcePath, ...(sourceSymbol ? { symbol: sourceSymbol } : {}) },
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
function extractInjectionTable(options: ExtractModuleExtensionFactsOptions): ModuleExtensionContributionFact[] {
|
|
1023
|
-
const filePath = conventionPath(options.moduleRoot, 'widgets/injection-table.ts')
|
|
1024
|
-
if (!filePath) return []
|
|
1025
|
-
const table = readRootObject(filePath, 'injectionTable')
|
|
1026
|
-
if (!table) return []
|
|
1027
|
-
const sourcePath = portablePath(options.moduleRoot, options.sourceRoot, filePath)
|
|
1028
|
-
const facts: ModuleExtensionContributionFact[] = []
|
|
1029
|
-
for (const targetId of Object.keys(table).sort((left, right) => left.localeCompare(right))) {
|
|
1030
|
-
const entries = table[targetId]
|
|
1031
|
-
if (!Array.isArray(entries)) continue
|
|
1032
|
-
for (const entry of entries) {
|
|
1033
|
-
if (!isStaticObject(entry)) continue
|
|
1034
|
-
const widgetId = stringValue(entry.widgetId)
|
|
1035
|
-
if (!widgetId) continue
|
|
1036
|
-
const payload = targetId.endsWith(':columns') ? 'column'
|
|
1037
|
-
: targetId.endsWith(':row-actions') ? 'row-action'
|
|
1038
|
-
: targetId.endsWith(':bulk-actions') ? 'bulk-action'
|
|
1039
|
-
: targetId.endsWith(':filters') ? 'filter'
|
|
1040
|
-
: targetId.endsWith(':fields') ? 'field'
|
|
1041
|
-
: 'render'
|
|
1042
|
-
const base = contributionBase(`${widgetId}@${targetId}`, sourcePath, 'injectionTable')
|
|
1043
|
-
const shared = {
|
|
1044
|
-
...base,
|
|
1045
|
-
targets: [target(targetId)],
|
|
1046
|
-
features: strings(entry.features),
|
|
1047
|
-
placement: numberValue(entry.priority) !== undefined ? { priority: numberValue(entry.priority) } : undefined,
|
|
1048
|
-
}
|
|
1049
|
-
if (targetId.startsWith('data-table:')) {
|
|
1050
|
-
facts.push({
|
|
1051
|
-
...shared,
|
|
1052
|
-
kind: 'data-table',
|
|
1053
|
-
details: {
|
|
1054
|
-
payload: payload === 'field' ? 'render' : payload,
|
|
1055
|
-
tableId: targetId.replace(/^data-table:/, '').replace(/:(?:columns|row-actions|bulk-actions|filters|toolbar|header|footer|search-trailing)$/, ''),
|
|
1056
|
-
executionGuard: strings(entry.features).length > 0 ? 'both' : 'host',
|
|
1057
|
-
},
|
|
1058
|
-
})
|
|
1059
|
-
} else if (targetId.startsWith('crud-form:')) {
|
|
1060
|
-
facts.push({
|
|
1061
|
-
...shared,
|
|
1062
|
-
kind: 'crud-form',
|
|
1063
|
-
details: {
|
|
1064
|
-
payload: payload === 'field' ? 'field' : 'render',
|
|
1065
|
-
entityId: targetId.replace(/^crud-form:/, '').replace(/:(?:fields|header)$/, ''),
|
|
1066
|
-
requestHeaderCapability: payload === 'field',
|
|
1067
|
-
},
|
|
1068
|
-
})
|
|
1069
|
-
} else {
|
|
1070
|
-
facts.push({
|
|
1071
|
-
...shared,
|
|
1072
|
-
kind: 'widget',
|
|
1073
|
-
details: {
|
|
1074
|
-
payload: 'render',
|
|
1075
|
-
registryKey: widgetId,
|
|
1076
|
-
executionGuard: strings(entry.features).length > 0 ? 'both' : 'host',
|
|
1077
|
-
},
|
|
1078
|
-
})
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
|
-
}
|
|
1082
|
-
return facts
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
function extractObjectConvention(options: {
|
|
1086
|
-
module: ExtractModuleExtensionFactsOptions
|
|
1087
|
-
relativePath: string
|
|
1088
|
-
exportName: string
|
|
1089
|
-
exportAliases?: string[]
|
|
1090
|
-
build: (entry: StaticObject, sourcePath: string, index: number) => ModuleExtensionContributionFact | null
|
|
1091
|
-
}): ModuleExtensionContributionFact[] {
|
|
1092
|
-
const filePath = conventionPath(options.module.moduleRoot, options.relativePath)
|
|
1093
|
-
if (!filePath) return []
|
|
1094
|
-
const exportNames = [options.exportName, ...(options.exportAliases ?? [])]
|
|
1095
|
-
const entries = exportNames.reduce<StaticObject[]>((found, exportName) =>
|
|
1096
|
-
found.length > 0 ? found : readConventionObjectArray(filePath, exportName), [])
|
|
1097
|
-
const sourcePath = portablePath(options.module.moduleRoot, options.module.sourceRoot, filePath)
|
|
1098
|
-
return entries.flatMap((entry, index) => options.build(entry, sourcePath, index) ?? [])
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
|
-
function methodPhases(entry: StaticObject, map: ReadonlyArray<[string, string]>): string[] {
|
|
1102
|
-
return map.flatMap(([property, phase]) => entry[property] === true ? [phase] : [])
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
function extractEnrichers(options: ExtractModuleExtensionFactsOptions): ModuleExtensionContributionFact[] {
|
|
1106
|
-
return extractObjectConvention({
|
|
1107
|
-
module: options,
|
|
1108
|
-
relativePath: 'data/enrichers.ts',
|
|
1109
|
-
exportName: 'enrichers',
|
|
1110
|
-
build(entry, sourcePath, index) {
|
|
1111
|
-
const id = stringValue(entry.id) ?? `${options.moduleId}.enricher.${index}`
|
|
1112
|
-
const targetEntity = stringValue(entry.targetEntity)
|
|
1113
|
-
if (!targetEntity) return null
|
|
1114
|
-
const timeoutMs = numberValue(entry.timeout) ?? 2000
|
|
1115
|
-
const queryEngineConfig = isStaticObject(entry.queryEngine) ? entry.queryEngine : null
|
|
1116
|
-
// `enricher-registry` selects query-engine enrichers by `queryEngine.enabled === true`,
|
|
1117
|
-
// so a config object that omits it (or sets it false) is NOT query-enabled.
|
|
1118
|
-
const queryEngine = queryEngineConfig && booleanValue(queryEngineConfig.enabled) === true
|
|
1119
|
-
? queryEngineConfig
|
|
1120
|
-
: null
|
|
1121
|
-
const surfaces = [entry.enrichMany === true ? 'list' : null, entry.enrichOne === true ? 'detail' : null]
|
|
1122
|
-
.filter((value): value is 'list' | 'detail' => value !== null)
|
|
1123
|
-
const base = contributionBase(id, sourcePath, id)
|
|
1124
|
-
return {
|
|
1125
|
-
...base,
|
|
1126
|
-
kind: 'response-enricher',
|
|
1127
|
-
targets: [openTarget(targetEntity)],
|
|
1128
|
-
features: strings(entry.features),
|
|
1129
|
-
phases: surfaces,
|
|
1130
|
-
activation: queryEngine ? 'caller-opt-in' : 'host-opt-in',
|
|
1131
|
-
roundTripId: `${targetEntity}:read`,
|
|
1132
|
-
details: {
|
|
1133
|
-
targetEntity,
|
|
1134
|
-
surfaces,
|
|
1135
|
-
timeoutMs,
|
|
1136
|
-
fallback: entry.fallback === undefined ? 'none' : 'configured',
|
|
1137
|
-
critical: booleanValue(entry.critical) ?? false,
|
|
1138
|
-
cachePosture: 'rerun-on-list-cache-hit',
|
|
1139
|
-
...(queryEngine ? { queryEngine: {
|
|
1140
|
-
engines: strings(queryEngine.engines),
|
|
1141
|
-
applyOn: (strings(queryEngine.applyOn).length > 0 ? strings(queryEngine.applyOn) : ['list', 'detail'])
|
|
1142
|
-
.filter((value): value is 'list' | 'detail' => value === 'list' || value === 'detail'),
|
|
1143
|
-
activation: 'caller-opt-in' as const,
|
|
1144
|
-
} } : {}),
|
|
1145
|
-
},
|
|
1146
|
-
}
|
|
1147
|
-
},
|
|
1148
|
-
})
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
function extractApiInterceptors(options: ExtractModuleExtensionFactsOptions): ModuleExtensionContributionFact[] {
|
|
1152
|
-
return extractObjectConvention({
|
|
1153
|
-
module: options,
|
|
1154
|
-
relativePath: 'api/interceptors.ts',
|
|
1155
|
-
exportName: 'interceptors',
|
|
1156
|
-
build(entry, sourcePath, index) {
|
|
1157
|
-
const id = stringValue(entry.id) ?? `${options.moduleId}.api-interceptor.${index}`
|
|
1158
|
-
const targetRoute = stringValue(entry.targetRoute)
|
|
1159
|
-
if (!targetRoute) return null
|
|
1160
|
-
const phases = methodPhases(entry, [['before', 'before'], ['after', 'after']])
|
|
1161
|
-
.filter((phase): phase is 'before' | 'after' => phase === 'before' || phase === 'after')
|
|
1162
|
-
const methods = strings(entry.methods)
|
|
1163
|
-
const base = contributionBase(id, sourcePath, id)
|
|
1164
|
-
return {
|
|
1165
|
-
...base,
|
|
1166
|
-
kind: 'api-interceptor',
|
|
1167
|
-
targets: [openTarget(targetRoute)],
|
|
1168
|
-
phases,
|
|
1169
|
-
features: strings(entry.features),
|
|
1170
|
-
details: {
|
|
1171
|
-
route: targetRoute,
|
|
1172
|
-
methods,
|
|
1173
|
-
phases,
|
|
1174
|
-
activation: targetRoute.startsWith('/') ? 'custom-route-bridge' : 'crud-pipeline',
|
|
1175
|
-
timeoutMs: numberValue(entry.timeout) ?? 2000,
|
|
1176
|
-
failurePosture: booleanValue(entry.critical) ? 'fail-closed' : 'fallback',
|
|
1177
|
-
},
|
|
1178
|
-
}
|
|
1179
|
-
},
|
|
1180
|
-
})
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
function extractCommandInterceptors(options: ExtractModuleExtensionFactsOptions): ModuleExtensionContributionFact[] {
|
|
1184
|
-
return extractObjectConvention({
|
|
1185
|
-
module: options,
|
|
1186
|
-
relativePath: 'commands/interceptors.ts',
|
|
1187
|
-
exportName: 'interceptors',
|
|
1188
|
-
build(entry, sourcePath, index) {
|
|
1189
|
-
const id = stringValue(entry.id) ?? `${options.moduleId}.command-interceptor.${index}`
|
|
1190
|
-
const targetCommand = stringValue(entry.targetCommand)
|
|
1191
|
-
if (!targetCommand) return null
|
|
1192
|
-
const phases = methodPhases(entry, [
|
|
1193
|
-
['beforeExecute', 'before-execute'],
|
|
1194
|
-
['afterExecute', 'after-execute'],
|
|
1195
|
-
['beforeUndo', 'before-undo'],
|
|
1196
|
-
['afterUndo', 'after-undo'],
|
|
1197
|
-
]).filter((phase): phase is 'before-execute' | 'after-execute' | 'before-undo' | 'after-undo' =>
|
|
1198
|
-
phase === 'before-execute' || phase === 'after-execute' || phase === 'before-undo' || phase === 'after-undo')
|
|
1199
|
-
const base = contributionBase(id, sourcePath, id)
|
|
1200
|
-
return {
|
|
1201
|
-
...base,
|
|
1202
|
-
kind: 'command-interceptor',
|
|
1203
|
-
targets: [openTarget(targetCommand)],
|
|
1204
|
-
phases,
|
|
1205
|
-
features: strings(entry.features),
|
|
1206
|
-
details: { targetCommand, phases },
|
|
1207
|
-
}
|
|
1208
|
-
},
|
|
1209
|
-
})
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
function extractGuards(options: ExtractModuleExtensionFactsOptions): ModuleExtensionContributionFact[] {
|
|
1213
|
-
return extractObjectConvention({
|
|
1214
|
-
module: options,
|
|
1215
|
-
relativePath: 'data/guards.ts',
|
|
1216
|
-
exportName: 'guards',
|
|
1217
|
-
build(entry, sourcePath, index) {
|
|
1218
|
-
const id = stringValue(entry.id) ?? `${options.moduleId}.guard.${index}`
|
|
1219
|
-
const targetEntity = stringValue(entry.targetEntity)
|
|
1220
|
-
if (!targetEntity) return null
|
|
1221
|
-
const operations = strings(entry.operations)
|
|
1222
|
-
.filter((operation): operation is 'create' | 'update' | 'delete' =>
|
|
1223
|
-
operation === 'create' || operation === 'update' || operation === 'delete')
|
|
1224
|
-
const capabilities = [entry.validate === true ? 'block' : null, entry.validate === true ? 'rewrite' : null, entry.afterSuccess === true ? 'after-success' : null]
|
|
1225
|
-
.filter((value): value is 'block' | 'rewrite' | 'after-success' => value !== null)
|
|
1226
|
-
const base = contributionBase(id, sourcePath, id)
|
|
1227
|
-
return {
|
|
1228
|
-
...base,
|
|
1229
|
-
kind: 'mutation-guard',
|
|
1230
|
-
targets: [openTarget(targetEntity)],
|
|
1231
|
-
operations,
|
|
1232
|
-
features: strings(entry.features),
|
|
1233
|
-
roundTripId: `${targetEntity}:write`,
|
|
1234
|
-
details: { entityId: targetEntity, operations, capabilities, optimisticLock: 'preserved' },
|
|
1235
|
-
}
|
|
1236
|
-
},
|
|
1237
|
-
})
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
function extractEntityExtensions(options: ExtractModuleExtensionFactsOptions): ModuleExtensionContributionFact[] {
|
|
1241
|
-
return extractObjectConvention({
|
|
1242
|
-
module: options,
|
|
1243
|
-
relativePath: 'data/extensions.ts',
|
|
1244
|
-
exportName: 'entityExtensions',
|
|
1245
|
-
exportAliases: ['extensions'],
|
|
1246
|
-
build(entry, sourcePath, index) {
|
|
1247
|
-
const baseEntity = stringValue(entry.base)
|
|
1248
|
-
const extensionEntity = stringValue(entry.extension)
|
|
1249
|
-
const join = isStaticObject(entry.join) ? entry.join : null
|
|
1250
|
-
if (!baseEntity || !extensionEntity || !join) return null
|
|
1251
|
-
const id = `${options.moduleId}.entity-extension.${index}:${baseEntity}->${extensionEntity}`
|
|
1252
|
-
const contribution = contributionBase(id, sourcePath, 'extensions')
|
|
1253
|
-
return {
|
|
1254
|
-
...contribution,
|
|
1255
|
-
kind: 'entity-extension',
|
|
1256
|
-
targets: [openTarget(baseEntity)],
|
|
1257
|
-
details: {
|
|
1258
|
-
hostEntityId: baseEntity,
|
|
1259
|
-
extensionEntityId: extensionEntity,
|
|
1260
|
-
linkId: `${stringValue(join.baseKey) ?? 'id'}:${stringValue(join.extensionKey) ?? 'id'}`,
|
|
1261
|
-
scopeContract: 'tenant-and-organization',
|
|
1262
|
-
orphanContract: booleanValue(entry.required) ? 'required' : 'optional',
|
|
1263
|
-
},
|
|
1264
|
-
}
|
|
1265
|
-
},
|
|
1266
|
-
})
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
|
-
function extractSubscribers(options: ExtractModuleExtensionFactsOptions): ModuleExtensionContributionFact[] {
|
|
1270
|
-
const directory = path.join(options.moduleRoot, 'subscribers')
|
|
1271
|
-
if (!fs.existsSync(directory)) return []
|
|
1272
|
-
const facts: ModuleExtensionContributionFact[] = []
|
|
1273
|
-
for (const filePath of sourceFilesBelow(directory)) {
|
|
1274
|
-
const metadata = readRootObject(filePath, 'metadata')
|
|
1275
|
-
if (!metadata) continue
|
|
1276
|
-
const event = stringValue(metadata.event)
|
|
1277
|
-
if (!event) continue
|
|
1278
|
-
const relativeStem = path.relative(directory, filePath).replace(/\.tsx?$/, '').split(path.sep).join('/')
|
|
1279
|
-
const subscriberId = stringValue(metadata.id) ?? `${options.moduleId}:${relativeStem}`
|
|
1280
|
-
const persistent = booleanValue(metadata.persistent) ?? false
|
|
1281
|
-
const sync = booleanValue(metadata.sync) ?? !persistent
|
|
1282
|
-
const contribution = contributionBase(subscriberId, portablePath(options.moduleRoot, options.sourceRoot, filePath), 'metadata')
|
|
1283
|
-
facts.push({
|
|
1284
|
-
...contribution,
|
|
1285
|
-
kind: 'subscriber',
|
|
1286
|
-
targets: [openTarget(event)],
|
|
1287
|
-
phases: sync ? ['before-or-after'] : ['async-delivery'],
|
|
1288
|
-
details: {
|
|
1289
|
-
event,
|
|
1290
|
-
subscriberId,
|
|
1291
|
-
persistent,
|
|
1292
|
-
sync,
|
|
1293
|
-
...(numberValue(metadata.priority) !== undefined ? { priority: numberValue(metadata.priority) as number } : {}),
|
|
1294
|
-
},
|
|
1295
|
-
})
|
|
1296
|
-
}
|
|
1297
|
-
return facts
|
|
1298
|
-
}
|
|
1299
|
-
|
|
1300
|
-
function extractBrowserReactions(options: ExtractModuleExtensionFactsOptions): ModuleExtensionContributionFact[] {
|
|
1301
|
-
return options.events.flatMap((event) => {
|
|
1302
|
-
const transports = [event.clientBroadcast ? 'client' : null, event.portalBroadcast ? 'portal' : null]
|
|
1303
|
-
.filter((value): value is 'client' | 'portal' => value !== null)
|
|
1304
|
-
if (transports.length === 0) return []
|
|
1305
|
-
const contribution = contributionBase(`${event.id}.browser`, path.posix.join(options.sourceRoot, 'events.ts'), 'events')
|
|
1306
|
-
return [{
|
|
1307
|
-
...contribution,
|
|
1308
|
-
kind: 'browser-reaction' as const,
|
|
1309
|
-
targets: [factTarget(event.id, 'events')],
|
|
1310
|
-
details: {
|
|
1311
|
-
transports,
|
|
1312
|
-
hooks: transports.flatMap((transport) => transport === 'client' ? ['useAppEvent', 'useOperationProgress'] : ['usePortalAppEvent']),
|
|
1313
|
-
audienceScopeContract: 'tenant-organization-user-role-and-customer',
|
|
1314
|
-
},
|
|
1315
|
-
}]
|
|
1316
|
-
})
|
|
1317
|
-
}
|
|
1318
|
-
|
|
1319
|
-
function extractNotificationReactions(options: ExtractModuleExtensionFactsOptions): ModuleExtensionContributionFact[] {
|
|
1320
|
-
return extractObjectConvention({
|
|
1321
|
-
module: options,
|
|
1322
|
-
relativePath: 'notifications.handlers.ts',
|
|
1323
|
-
exportName: 'notificationHandlers',
|
|
1324
|
-
build(entry, sourcePath, index) {
|
|
1325
|
-
const notificationType = stringValue(entry.notificationType)
|
|
1326
|
-
if (!notificationType) return null
|
|
1327
|
-
const declaredId = stringValue(entry.id)
|
|
1328
|
-
const id = declaredId ?? `${options.moduleId}.notification-handler.${index}`
|
|
1329
|
-
const contribution = contributionBase(id, sourcePath, 'notificationHandlers')
|
|
1330
|
-
return {
|
|
1331
|
-
...contribution,
|
|
1332
|
-
kind: 'browser-reaction',
|
|
1333
|
-
targets: [factTarget(notificationType, 'notifications')],
|
|
1334
|
-
features: strings(entry.features),
|
|
1335
|
-
placement: numberValue(entry.priority) !== undefined ? { priority: numberValue(entry.priority) } : undefined,
|
|
1336
|
-
details: {
|
|
1337
|
-
transports: ['notification-effect'],
|
|
1338
|
-
hooks: ['useNotificationEffect'],
|
|
1339
|
-
audienceScopeContract: 'tenant-organization-user-role-and-customer',
|
|
1340
|
-
...(declaredId ? { overrideKey: declaredId } : {}),
|
|
1341
|
-
},
|
|
1342
|
-
}
|
|
1343
|
-
},
|
|
1344
|
-
})
|
|
1345
|
-
}
|
|
1346
|
-
|
|
1347
|
-
function extractComponentOverrides(options: ExtractModuleExtensionFactsOptions): ModuleExtensionContributionFact[] {
|
|
1348
|
-
return extractObjectConvention({
|
|
1349
|
-
module: options,
|
|
1350
|
-
relativePath: 'widgets/components.ts',
|
|
1351
|
-
exportName: 'componentOverrides',
|
|
1352
|
-
build(entry, sourcePath, index) {
|
|
1353
|
-
const targetDefinition = isStaticObject(entry.target) ? entry.target : null
|
|
1354
|
-
const handle = targetDefinition ? stringValue(targetDefinition.componentId) : undefined
|
|
1355
|
-
if (!handle) return null
|
|
1356
|
-
const mode = entry.wrapper !== undefined ? 'wrapper' : entry.props !== undefined ? 'props' : 'replace'
|
|
1357
|
-
const id = `${options.moduleId}.component-override.${index}:${handle}`
|
|
1358
|
-
const contribution = contributionBase(id, sourcePath, 'componentOverrides')
|
|
1359
|
-
return {
|
|
1360
|
-
...contribution,
|
|
1361
|
-
kind: 'component-override',
|
|
1362
|
-
targets: [target(handle)],
|
|
1363
|
-
details: {
|
|
1364
|
-
handle,
|
|
1365
|
-
mode,
|
|
1366
|
-
propsContract: stringValue(entry.propsContract) ?? 'component-props-schema',
|
|
1367
|
-
},
|
|
1368
|
-
}
|
|
1369
|
-
},
|
|
1370
|
-
})
|
|
1371
|
-
}
|
|
1372
|
-
|
|
1373
|
-
function extractSpecializedRegistries(options: ExtractModuleExtensionFactsOptions): ModuleExtensionContributionFact[] {
|
|
1374
|
-
const facts: ModuleExtensionContributionFact[] = []
|
|
1375
|
-
const add = (
|
|
1376
|
-
id: string,
|
|
1377
|
-
registry: SpecializedRegistry,
|
|
1378
|
-
sourcePath: string,
|
|
1379
|
-
specialistRoute: string,
|
|
1380
|
-
symbol = id,
|
|
1381
|
-
): void => {
|
|
1382
|
-
const contributionId = `${registry}:${id}`
|
|
1383
|
-
const contribution = contributionBase(contributionId, sourcePath, id)
|
|
1384
|
-
const fact: ModuleExtensionContributionFact = {
|
|
1385
|
-
...contribution,
|
|
1386
|
-
kind: 'specialized-registry',
|
|
1387
|
-
targets: [factTarget(id, specialistRoute)],
|
|
1388
|
-
source: { path: sourcePath, symbol },
|
|
1389
|
-
details: { registry, registryId: id, specialistRoute },
|
|
1390
|
-
}
|
|
1391
|
-
const existingIndex = facts.findIndex((entry) => entry.id === contributionId)
|
|
1392
|
-
if (existingIndex >= 0) facts[existingIndex] = fact
|
|
1393
|
-
else facts.push(fact)
|
|
1394
|
-
}
|
|
1395
|
-
for (const id of options.notifications ?? []) add(id, 'notification', path.posix.join(options.sourceRoot, 'notifications.ts'), 'notifications')
|
|
1396
|
-
for (const id of options.searchEntities) add(id, 'search', path.posix.join(options.sourceRoot, 'search.ts'), 'search')
|
|
1397
|
-
for (const tool of options.aiTools ?? []) add(tool.name, 'ai', tool.sourcePath, 'aiTools')
|
|
1398
|
-
for (const agent of options.aiAgents ?? []) add(agent.id, 'ai', agent.sourcePath, 'aiAgents')
|
|
1399
|
-
|
|
1400
|
-
const vectorPath = conventionPath(options.moduleRoot, 'vector.ts')
|
|
1401
|
-
if (vectorPath) {
|
|
1402
|
-
const vectorConfig = readRootObject(vectorPath, 'vectorConfig') ?? readRootObject(vectorPath, 'config')
|
|
1403
|
-
const sourcePath = portablePath(options.moduleRoot, options.sourceRoot, vectorPath)
|
|
1404
|
-
if (vectorConfig && Array.isArray(vectorConfig.entities)) {
|
|
1405
|
-
for (const entity of vectorConfig.entities) {
|
|
1406
|
-
if (!isStaticObject(entity)) continue
|
|
1407
|
-
const entityId = stringValue(entity.entityId)
|
|
1408
|
-
if (entityId) add(entityId, 'vector', sourcePath, 'vector', 'vectorConfig')
|
|
1409
|
-
}
|
|
1410
|
-
}
|
|
1411
|
-
}
|
|
1412
|
-
|
|
1413
|
-
const integrationPath = conventionPath(options.moduleRoot, 'integration.ts')
|
|
1414
|
-
if (integrationPath) {
|
|
1415
|
-
const sourcePath = portablePath(options.moduleRoot, options.sourceRoot, integrationPath)
|
|
1416
|
-
const addIntegration = (integration: StaticObject, symbol: string, bundleId?: string): void => {
|
|
1417
|
-
const integrationId = stringValue(integration.id)
|
|
1418
|
-
if (integrationId) {
|
|
1419
|
-
add(integrationId, 'integration', sourcePath, 'integrations', bundleId ? `${symbol}[${bundleId}]` : symbol)
|
|
1420
|
-
}
|
|
1421
|
-
const providerKey = stringValue(integration.providerKey)
|
|
1422
|
-
const category = stringValue(integration.category)
|
|
1423
|
-
const categoryRegistry = category === 'payment' ? 'payment'
|
|
1424
|
-
: category === 'shipping' ? 'shipping'
|
|
1425
|
-
: category === 'currency' ? 'currency'
|
|
1426
|
-
: null
|
|
1427
|
-
if (providerKey && categoryRegistry) {
|
|
1428
|
-
const specialistRoute = categoryRegistry === 'payment' ? 'paymentGateways'
|
|
1429
|
-
: categoryRegistry === 'shipping' ? 'shippingCarriers'
|
|
1430
|
-
: 'currencies'
|
|
1431
|
-
add(providerKey, categoryRegistry, sourcePath, specialistRoute, `${symbol}.providerKey`)
|
|
1432
|
-
}
|
|
1433
|
-
}
|
|
1434
|
-
const singular = readRootObject(integrationPath, 'integration')
|
|
1435
|
-
if (singular) addIntegration(singular, 'integration')
|
|
1436
|
-
for (const integration of readConventionObjectArray(integrationPath, 'integrations')) {
|
|
1437
|
-
addIntegration(integration, 'integrations')
|
|
1438
|
-
}
|
|
1439
|
-
const bundle = readRootObject(integrationPath, 'integrationBundle') ?? readRootObject(integrationPath, 'bundle')
|
|
1440
|
-
if (bundle) {
|
|
1441
|
-
const bundleId = stringValue(bundle.id) ?? 'bundle'
|
|
1442
|
-
const members = Array.isArray(bundle.integrations) ? bundle.integrations : []
|
|
1443
|
-
for (const member of members) {
|
|
1444
|
-
if (isStaticObject(member)) addIntegration(member, 'integrationBundle', bundleId)
|
|
1445
|
-
}
|
|
1446
|
-
}
|
|
1447
|
-
}
|
|
1448
|
-
|
|
1449
|
-
const workflowsPath = conventionPath(options.moduleRoot, 'workflows.ts')
|
|
1450
|
-
if (workflowsPath) {
|
|
1451
|
-
const config = readRootObject(workflowsPath, 'workflowsConfig')
|
|
1452
|
-
const sourcePath = portablePath(options.moduleRoot, options.sourceRoot, workflowsPath)
|
|
1453
|
-
if (config && Array.isArray(config.workflows)) {
|
|
1454
|
-
for (const workflow of config.workflows) {
|
|
1455
|
-
if (!isStaticObject(workflow)) continue
|
|
1456
|
-
const workflowId = stringValue(workflow.workflowId)
|
|
1457
|
-
if (workflowId) add(workflowId, 'workflow', sourcePath, 'workflows', 'workflowsConfig')
|
|
1458
|
-
}
|
|
1459
|
-
}
|
|
1460
|
-
}
|
|
1461
|
-
|
|
1462
|
-
const diPath = conventionPath(options.moduleRoot, 'di.ts')
|
|
1463
|
-
if (diPath) {
|
|
1464
|
-
const sourcePath = portablePath(options.moduleRoot, options.sourceRoot, diPath)
|
|
1465
|
-
const registrations = readCallObjectArguments(diPath, new Set([
|
|
1466
|
-
'registerPaymentGatewayDescriptor',
|
|
1467
|
-
'registerShippingAdapter',
|
|
1468
|
-
'registerCurrencyProvider',
|
|
1469
|
-
]))
|
|
1470
|
-
for (const registration of registrations) {
|
|
1471
|
-
const registry = registration.callee === 'registerPaymentGatewayDescriptor' ? 'payment'
|
|
1472
|
-
: registration.callee === 'registerShippingAdapter' ? 'shipping'
|
|
1473
|
-
: 'currency'
|
|
1474
|
-
const registryId = stringValue(registration.value.providerKey)
|
|
1475
|
-
?? stringValue(registration.value.key)
|
|
1476
|
-
?? stringValue(registration.value.id)
|
|
1477
|
-
if (!registryId) continue
|
|
1478
|
-
const specialistRoute = registry === 'payment' ? 'paymentGateways'
|
|
1479
|
-
: registry === 'shipping' ? 'shippingCarriers'
|
|
1480
|
-
: 'currencies'
|
|
1481
|
-
add(registryId, registry, sourcePath, specialistRoute, registration.callee)
|
|
1482
|
-
}
|
|
1483
|
-
}
|
|
1484
|
-
|
|
1485
|
-
const dashboardRoots = { appBase: path.join(options.moduleRoot, '__app__'), pkgBase: options.moduleRoot }
|
|
1486
|
-
for (const widgetFile of scanModuleDir(dashboardRoots, SCAN_CONFIGS.dashboardWidgets)) {
|
|
1487
|
-
const filePath = path.join(options.moduleRoot, 'widgets', 'dashboard', widgetFile.relPath)
|
|
1488
|
-
const widget = readRootObject(filePath, 'widget')
|
|
1489
|
-
const metadata = widget && isStaticObject(widget.metadata) ? widget.metadata : null
|
|
1490
|
-
if (!metadata) continue
|
|
1491
|
-
const widgetId = stringValue(metadata.id)
|
|
1492
|
-
if (!widgetId) continue
|
|
1493
|
-
const sourcePath = portablePath(options.moduleRoot, options.sourceRoot, filePath)
|
|
1494
|
-
const contribution = contributionBase(`dashboard:${widgetId}`, sourcePath, 'widget.metadata')
|
|
1495
|
-
facts.push({
|
|
1496
|
-
...contribution,
|
|
1497
|
-
kind: 'widget',
|
|
1498
|
-
targets: [target(`dashboard:${widgetId}`)],
|
|
1499
|
-
features: strings(metadata.features),
|
|
1500
|
-
details: {
|
|
1501
|
-
payload: 'dashboard',
|
|
1502
|
-
registryKey: widgetId,
|
|
1503
|
-
executionGuard: strings(metadata.features).length > 0 ? 'both' : 'host',
|
|
1504
|
-
},
|
|
1505
|
-
})
|
|
1506
|
-
}
|
|
1507
|
-
return facts
|
|
1508
|
-
}
|
|
1509
|
-
|
|
1510
|
-
/**
|
|
1511
|
-
* Closed activation-adapter registry (Spec 2). Each `ModuleExtensionActivationKind`
|
|
1512
|
-
* maps to the contribution kinds it can bind and to the mode by which a `bound`
|
|
1513
|
-
* resolution is proven:
|
|
1514
|
-
*
|
|
1515
|
-
* - `call-site-object` — the host module owns a distinct opt-in call site (a
|
|
1516
|
-
* `makeCrudRoute` option or a mutation-guard bridge). Presence of the option —
|
|
1517
|
-
* NOT mere entity/route existence — produces an emitted `ModuleExtensionActivation`
|
|
1518
|
-
* object. This is what makes "the entity could host X" (capability-only) different
|
|
1519
|
-
* from "this route actually activates X" (bound).
|
|
1520
|
-
* - `host-reference` — the runtime bridge is universal for any matching host
|
|
1521
|
-
* (every api route runs the interceptor pipeline; a bound widget/component host
|
|
1522
|
-
* proves consumption). No object is duplicated; `bound` is derived from the
|
|
1523
|
-
* existing bound host fact and the incoming row carries no `activationId`.
|
|
1524
|
-
* - `owner-reference` — like host-reference but proven by the target module owning
|
|
1525
|
-
* the concrete runtime id (command ids have no host fact of their own).
|
|
1526
|
-
*
|
|
1527
|
-
* The registry is exhaustive by construction: the `Record` key set is the full
|
|
1528
|
-
* `ModuleExtensionActivationKind` union, so adding a kind fails the typecheck until
|
|
1529
|
-
* it is classified. `activation-adapter-coverage.test.ts` re-checks this at runtime.
|
|
1530
|
-
*/
|
|
1531
|
-
type ActivationAdapterMode = 'call-site-object' | 'host-reference' | 'owner-reference'
|
|
1532
|
-
|
|
1533
|
-
export const ACTIVATION_ADAPTERS: Record<ModuleExtensionActivationKind, {
|
|
1534
|
-
contributionKinds: ModuleExtensionContributionKind[]
|
|
1535
|
-
mode: ActivationAdapterMode
|
|
1536
|
-
}> = {
|
|
1537
|
-
'crud-response-enricher': { contributionKinds: ['response-enricher'], mode: 'call-site-object' },
|
|
1538
|
-
'query-enricher': { contributionKinds: ['response-enricher'], mode: 'call-site-object' },
|
|
1539
|
-
'mutation-guard': { contributionKinds: ['mutation-guard'], mode: 'call-site-object' },
|
|
1540
|
-
'api-interceptor-bridge': { contributionKinds: ['api-interceptor'], mode: 'host-reference' },
|
|
1541
|
-
'command-interceptor-bridge': { contributionKinds: ['command-interceptor'], mode: 'owner-reference' },
|
|
1542
|
-
'widget-injection-consumer': { contributionKinds: ['widget', 'data-table', 'crud-form'], mode: 'host-reference' },
|
|
1543
|
-
'component-extension-consumer': { contributionKinds: ['component-override'], mode: 'host-reference' },
|
|
1544
|
-
'dashboard-host-consumer': { contributionKinds: ['widget'], mode: 'host-reference' },
|
|
1545
|
-
}
|
|
1546
|
-
|
|
1547
|
-
/**
|
|
1548
|
-
* Every UMES contribution kind classified against the activation registry: either
|
|
1549
|
-
* the ordered set of activation kinds that can bind it, or `capability-only` for
|
|
1550
|
-
* contributions that are always active by declaration/registration and never gain
|
|
1551
|
-
* a route/entity activation. Exhaustive by `Record` construction — a new kind fails
|
|
1552
|
-
* the typecheck until classified.
|
|
1553
|
-
*/
|
|
1554
|
-
export const CONTRIBUTION_ACTIVATION_CLASSIFICATION: Record<
|
|
1555
|
-
ModuleExtensionContributionKind,
|
|
1556
|
-
ModuleExtensionActivationKind[] | 'capability-only'
|
|
1557
|
-
> = {
|
|
1558
|
-
'response-enricher': ['crud-response-enricher', 'query-enricher'],
|
|
1559
|
-
'mutation-guard': ['mutation-guard'],
|
|
1560
|
-
'api-interceptor': ['api-interceptor-bridge'],
|
|
1561
|
-
'command-interceptor': ['command-interceptor-bridge'],
|
|
1562
|
-
'widget': ['widget-injection-consumer', 'dashboard-host-consumer'],
|
|
1563
|
-
'data-table': ['widget-injection-consumer'],
|
|
1564
|
-
'crud-form': ['widget-injection-consumer'],
|
|
1565
|
-
'component-override': ['component-extension-consumer'],
|
|
1566
|
-
'entity-extension': 'capability-only',
|
|
1567
|
-
'subscriber': 'capability-only',
|
|
1568
|
-
'browser-reaction': 'capability-only',
|
|
1569
|
-
'specialized-registry': 'capability-only',
|
|
1570
|
-
'module-override': 'capability-only',
|
|
1571
|
-
}
|
|
1572
|
-
|
|
1573
|
-
export const ALL_ACTIVATION_KINDS = Object.keys(ACTIVATION_ADAPTERS) as ModuleExtensionActivationKind[]
|
|
1574
|
-
export const ALL_CONTRIBUTION_KINDS = Object.keys(CONTRIBUTION_ACTIVATION_CLASSIFICATION) as ModuleExtensionContributionKind[]
|
|
1575
|
-
|
|
1576
|
-
/**
|
|
1577
|
-
* Verified mutation-guard bridge shapes. The canonical route helper nests the
|
|
1578
|
-
* resource under `input` (`runRouteMutationGuards({ …, input: { resourceKind } })`),
|
|
1579
|
-
* while the legacy helpers take the resource object directly. Module wrappers around
|
|
1580
|
-
* the canonical helper live under `lib/` as often as under `api/`, so both trees are
|
|
1581
|
-
* scanned.
|
|
1582
|
-
*/
|
|
1583
|
-
const MUTATION_GUARD_BRIDGE_ADAPTERS: Record<string, { resourceArgument: 'input-property' | 'any-argument' }> = {
|
|
1584
|
-
runRouteMutationGuards: { resourceArgument: 'input-property' },
|
|
1585
|
-
validateCrudMutationGuard: { resourceArgument: 'any-argument' },
|
|
1586
|
-
runMutationGuards: { resourceArgument: 'any-argument' },
|
|
1587
|
-
}
|
|
1588
|
-
|
|
1589
|
-
const MUTATION_GUARD_SOURCE_DIRECTORIES = ['api', 'lib'] as const
|
|
1590
|
-
|
|
1591
|
-
const MUTATION_GUARD_OPERATIONS = new Set(['create', 'update', 'delete', 'custom'])
|
|
1592
|
-
|
|
1593
|
-
/**
|
|
1594
|
-
* `'custom'` action endpoints are mapped by `toRegistryMutationOperation` onto the
|
|
1595
|
-
* closest registry operation, `'update'`.
|
|
1596
|
-
*/
|
|
1597
|
-
function toRegistryGuardOperation(operation: string): string {
|
|
1598
|
-
return operation === 'custom' ? 'update' : operation
|
|
1599
|
-
}
|
|
1600
|
-
|
|
1601
|
-
function readMutationGuardBridge(
|
|
1602
|
-
node: ts.CallExpression,
|
|
1603
|
-
context: StaticContext,
|
|
1604
|
-
): { entityId: string; operations: string[] } | null {
|
|
1605
|
-
const callee = node.expression
|
|
1606
|
-
const calleeName = ts.isIdentifier(callee)
|
|
1607
|
-
? callee.text
|
|
1608
|
-
: ts.isPropertyAccessExpression(callee)
|
|
1609
|
-
? callee.name.text
|
|
1610
|
-
: null
|
|
1611
|
-
const adapter = calleeName ? MUTATION_GUARD_BRIDGE_ADAPTERS[calleeName] : undefined
|
|
1612
|
-
if (!adapter) return null
|
|
1613
|
-
|
|
1614
|
-
const argumentObjects = node.arguments
|
|
1615
|
-
.map((argument) => staticValue(argument, context))
|
|
1616
|
-
.filter(isStaticObject)
|
|
1617
|
-
const resourceObjects = adapter.resourceArgument === 'input-property'
|
|
1618
|
-
? argumentObjects.flatMap((argumentObject) => (isStaticObject(argumentObject.input) ? [argumentObject.input] : []))
|
|
1619
|
-
: argumentObjects
|
|
1620
|
-
for (const resourceObject of resourceObjects) {
|
|
1621
|
-
const entityId = stringValue(resourceObject.resourceKind) ?? stringValue(resourceObject.entityId)
|
|
1622
|
-
if (!entityId) continue
|
|
1623
|
-
const declaredOperation = stringValue(resourceObject.operation)
|
|
1624
|
-
const operations = declaredOperation && MUTATION_GUARD_OPERATIONS.has(declaredOperation)
|
|
1625
|
-
? [toRegistryGuardOperation(declaredOperation)]
|
|
1626
|
-
: []
|
|
1627
|
-
return { entityId, operations }
|
|
1628
|
-
}
|
|
1629
|
-
return null
|
|
1630
|
-
}
|
|
1631
|
-
|
|
1632
|
-
function nodeLine(file: ts.SourceFile, node: ts.Node): number {
|
|
1633
|
-
return file.getLineAndCharacterOfPosition(node.getStart(file)).line + 1
|
|
1634
|
-
}
|
|
1635
|
-
|
|
1636
|
-
function activationId(host: ModuleExtensionTargetRef, kind: ModuleExtensionActivationKind): string {
|
|
1637
|
-
const method = host.method ? `:${host.method}` : ''
|
|
1638
|
-
return `${host.kind}:${host.id}${method}:${kind}`
|
|
1639
|
-
}
|
|
1640
|
-
|
|
1641
|
-
function sortActivations(activations: ModuleExtensionActivation[]): ModuleExtensionActivation[] {
|
|
1642
|
-
return [...activations].sort((left, right) => left.id.localeCompare(right.id))
|
|
1643
|
-
}
|
|
1644
|
-
|
|
1645
|
-
/**
|
|
1646
|
-
* Extract the `call-site-object` activations (CRUD response/query enrichers and
|
|
1647
|
-
* mutation guards) from a module's real `api/**` opt-in sites. Entity or route
|
|
1648
|
-
* presence alone never lands here — only a literal `enrichers: { entityId }`
|
|
1649
|
-
* option or a mutation-guard bridge call with a static `resourceKind`/`entityId`.
|
|
1650
|
-
*/
|
|
1651
|
-
/**
|
|
1652
|
-
* `<engine>.query(entityId, { …, extensions })` — the only call shape that enables the
|
|
1653
|
-
* query-engine enricher and sync query-lifecycle stages.
|
|
1654
|
-
*/
|
|
1655
|
-
function isQueryWithExtensionsCall(node: ts.Node, context: StaticContext): node is ts.CallExpression {
|
|
1656
|
-
if (!ts.isCallExpression(node)) return false
|
|
1657
|
-
const callee = node.expression
|
|
1658
|
-
const calleeName = ts.isPropertyAccessExpression(callee)
|
|
1659
|
-
? callee.name.text
|
|
1660
|
-
: ts.isIdentifier(callee)
|
|
1661
|
-
? callee.text
|
|
1662
|
-
: null
|
|
1663
|
-
if (calleeName !== 'query') return false
|
|
1664
|
-
const queryOptions = node.arguments[1] ? staticValue(node.arguments[1], context) : undefined
|
|
1665
|
-
return isStaticObject(queryOptions) && queryOptions.extensions !== undefined
|
|
1666
|
-
}
|
|
1667
|
-
|
|
1668
|
-
function extractCallSiteActivations(options: ExtractModuleExtensionFactsOptions): ModuleExtensionActivation[] {
|
|
1669
|
-
const activations: ModuleExtensionActivation[] = []
|
|
1670
|
-
const byId = new Map<string, ModuleExtensionActivation>()
|
|
1671
|
-
const push = (activation: ModuleExtensionActivation): void => {
|
|
1672
|
-
const existing = byId.get(activation.id)
|
|
1673
|
-
if (existing) {
|
|
1674
|
-
if (!existing.operations || !activation.operations) {
|
|
1675
|
-
delete existing.operations
|
|
1676
|
-
} else {
|
|
1677
|
-
existing.operations = [...new Set([...existing.operations, ...activation.operations])]
|
|
1678
|
-
.sort((left, right) => left.localeCompare(right))
|
|
1679
|
-
}
|
|
1680
|
-
return
|
|
1681
|
-
}
|
|
1682
|
-
byId.set(activation.id, activation)
|
|
1683
|
-
activations.push(activation)
|
|
1684
|
-
}
|
|
1685
|
-
const scannedFiles = MUTATION_GUARD_SOURCE_DIRECTORIES
|
|
1686
|
-
.flatMap((directory) => sourceFilesBelow(path.join(options.moduleRoot, directory)))
|
|
1687
|
-
for (const filePath of scannedFiles) {
|
|
1688
|
-
const file = sourceFile(filePath)
|
|
1689
|
-
if (!file) continue
|
|
1690
|
-
const context = buildStaticContext(file)
|
|
1691
|
-
const sourcePath = portablePath(options.moduleRoot, options.sourceRoot, filePath)
|
|
1692
|
-
const visit = (node: ts.Node): void => {
|
|
1693
|
-
// `enrichers: { entityId }` on a CRUD route runs `applyResponseEnrichers` after the
|
|
1694
|
-
// list/detail hook. It does NOT enable the query-engine enricher stage.
|
|
1695
|
-
if (ts.isPropertyAssignment(node) && propertyName(node.name) === 'enrichers') {
|
|
1696
|
-
const config = staticValue(node.initializer, context)
|
|
1697
|
-
const entityId = isStaticObject(config) ? stringValue(config.entityId) : undefined
|
|
1698
|
-
if (entityId) {
|
|
1699
|
-
const host: ModuleExtensionTargetRef = { kind: 'entity', id: entityId, moduleId: options.moduleId }
|
|
1700
|
-
push({
|
|
1701
|
-
id: activationId(host, 'crud-response-enricher'),
|
|
1702
|
-
kind: 'crud-response-enricher',
|
|
1703
|
-
host,
|
|
1704
|
-
contributionKinds: ['response-enricher'],
|
|
1705
|
-
source: { sourcePath, line: nodeLine(file, node) },
|
|
1706
|
-
})
|
|
1707
|
-
}
|
|
1708
|
-
}
|
|
1709
|
-
// The query-engine enricher stage runs only for a query that passes `extensions`,
|
|
1710
|
-
// so the bound call site is `queryEngine.query('<entityId>', { …, extensions })`.
|
|
1711
|
-
if (isQueryWithExtensionsCall(node, context)) {
|
|
1712
|
-
const entityId = stringValue(staticValue(node.arguments[0], context))
|
|
1713
|
-
if (entityId) {
|
|
1714
|
-
const host: ModuleExtensionTargetRef = { kind: 'entity', id: entityId, moduleId: options.moduleId }
|
|
1715
|
-
push({
|
|
1716
|
-
id: activationId(host, 'query-enricher'),
|
|
1717
|
-
kind: 'query-enricher',
|
|
1718
|
-
host,
|
|
1719
|
-
contributionKinds: ['response-enricher'],
|
|
1720
|
-
source: { sourcePath, line: nodeLine(file, node) },
|
|
1721
|
-
})
|
|
1722
|
-
}
|
|
1723
|
-
}
|
|
1724
|
-
if (ts.isCallExpression(node)) {
|
|
1725
|
-
const bridge = readMutationGuardBridge(node, context)
|
|
1726
|
-
if (bridge) {
|
|
1727
|
-
const host: ModuleExtensionTargetRef = { kind: 'entity', id: bridge.entityId, moduleId: options.moduleId }
|
|
1728
|
-
push({
|
|
1729
|
-
id: activationId(host, 'mutation-guard'),
|
|
1730
|
-
kind: 'mutation-guard',
|
|
1731
|
-
host,
|
|
1732
|
-
contributionKinds: ['mutation-guard'],
|
|
1733
|
-
...(bridge.operations.length > 0 ? { operations: bridge.operations } : {}),
|
|
1734
|
-
source: { sourcePath, line: nodeLine(file, node) },
|
|
1735
|
-
})
|
|
1736
|
-
}
|
|
1737
|
-
}
|
|
1738
|
-
node.forEachChild(visit)
|
|
1739
|
-
}
|
|
1740
|
-
file.forEachChild(visit)
|
|
1741
|
-
}
|
|
1742
|
-
return sortActivations(activations)
|
|
1743
|
-
}
|
|
1744
|
-
|
|
1745
|
-
export function extractModuleExtensionFacts(options: ExtractModuleExtensionFactsOptions): ModuleExtensionSurfaceFacts {
|
|
1746
|
-
const declared = extractDeclaredHosts(options)
|
|
1747
|
-
const hosts = sortHosts([...declared.hosts, ...extractFactRefHosts(options)])
|
|
1748
|
-
const contributions = sortContributions([
|
|
1749
|
-
...extractInjectionTable(options),
|
|
1750
|
-
...extractEnrichers(options),
|
|
1751
|
-
...extractApiInterceptors(options),
|
|
1752
|
-
...extractCommandInterceptors(options),
|
|
1753
|
-
...extractGuards(options),
|
|
1754
|
-
...extractEntityExtensions(options),
|
|
1755
|
-
...extractSubscribers(options),
|
|
1756
|
-
...extractBrowserReactions(options),
|
|
1757
|
-
...extractNotificationReactions(options),
|
|
1758
|
-
...extractComponentOverrides(options),
|
|
1759
|
-
...extractSpecializedRegistries(options),
|
|
1760
|
-
])
|
|
1761
|
-
return {
|
|
1762
|
-
hosts,
|
|
1763
|
-
contributions,
|
|
1764
|
-
unresolved: declared.unresolved,
|
|
1765
|
-
activations: extractCallSiteActivations(options),
|
|
1766
|
-
}
|
|
1767
|
-
}
|
|
1768
|
-
|
|
1769
|
-
function patternMatches(pattern: string, targetId: string): boolean {
|
|
1770
|
-
const escaped = pattern
|
|
1771
|
-
.replace(/[.+?^${}()|[\]\\]/g, '\\$&')
|
|
1772
|
-
.replace(/\\\{[^}]+\\\}/g, '[^:]+')
|
|
1773
|
-
.replace(/\*/g, '.*')
|
|
1774
|
-
return new RegExp(`^${escaped}$`).test(targetId)
|
|
1775
|
-
}
|
|
1776
|
-
|
|
1777
|
-
function targetModuleId(targetId: string): string | null {
|
|
1778
|
-
const withoutPrefix = targetId.replace(/^(?:data-table|crud-form|detail|portal-page):/, '')
|
|
1779
|
-
const match = /^([a-z][a-z0-9_]*)[.:]/.exec(withoutPrefix)
|
|
1780
|
-
return match?.[1] ?? null
|
|
1781
|
-
}
|
|
1782
|
-
|
|
1783
|
-
export function correlateExtensionTarget(
|
|
1784
|
-
targetFact: ModuleExtensionTargetFact,
|
|
1785
|
-
options: CorrelateExtensionFactsOptions,
|
|
1786
|
-
): ModuleExtensionTargetFact {
|
|
1787
|
-
if (targetFact.resolution !== 'unresolved') return targetFact
|
|
1788
|
-
const allHosts = Object.values(options.surfacesByModule).flatMap((surface) => surface.hosts)
|
|
1789
|
-
if (targetFact.id.includes('*') || targetFact.id.includes('{')) {
|
|
1790
|
-
const knownIds = [
|
|
1791
|
-
...allHosts.filter((host) => host.bound).map((host) => host.id),
|
|
1792
|
-
...options.entityIds,
|
|
1793
|
-
...options.eventIds,
|
|
1794
|
-
...options.apiRoutes,
|
|
1795
|
-
...(options.commandIds ?? []),
|
|
1796
|
-
]
|
|
1797
|
-
if (targetFact.id === '*' || knownIds.some((knownId) => patternMatches(targetFact.id, knownId))) {
|
|
1798
|
-
return { id: targetFact.id, resolution: 'pattern' }
|
|
1799
|
-
}
|
|
1800
|
-
}
|
|
1801
|
-
const exact = allHosts.find((host) => host.bound && (
|
|
1802
|
-
host.id === targetFact.id
|
|
1803
|
-
|| host.aliases?.includes(targetFact.id)
|
|
1804
|
-
|| (host.family === 'api-route' && host.id.endsWith(`/${targetFact.id}`))
|
|
1805
|
-
))
|
|
1806
|
-
if (exact) {
|
|
1807
|
-
return exact.source.kind === 'fact-ref'
|
|
1808
|
-
? factTarget(targetFact.id, exact.source.factSection, exact.source.factKey)
|
|
1809
|
-
: { id: targetFact.id, resolution: 'exact' }
|
|
1810
|
-
}
|
|
1811
|
-
const patterned = allHosts.find((host) => host.bound && host.resolution === 'pattern' && patternMatches(host.id, targetFact.id))
|
|
1812
|
-
if (patterned) return { id: targetFact.id, resolution: 'pattern' }
|
|
1813
|
-
const framework = allFrameworkHosts().find((host) => patternMatches(host.id, targetFact.id))
|
|
1814
|
-
?? (FRAMEWORK_PREFIXES.some((prefix) => targetFact.id.startsWith(prefix)) ? FRAMEWORK_HOSTS[0] : undefined)
|
|
1815
|
-
if (framework) return { id: targetFact.id, resolution: 'framework' }
|
|
1816
|
-
if (options.entityIds.has(targetFact.id)) {
|
|
1817
|
-
return { id: targetFact.id, resolution: 'fact-ref', factRef: { factSection: 'entities', factKey: targetFact.id } }
|
|
1818
|
-
}
|
|
1819
|
-
if (options.eventIds.has(targetFact.id)) {
|
|
1820
|
-
return { id: targetFact.id, resolution: 'fact-ref', factRef: { factSection: 'events', factKey: targetFact.id } }
|
|
1821
|
-
}
|
|
1822
|
-
if (options.commandIds?.has(targetFact.id)) {
|
|
1823
|
-
return factTarget(targetFact.id, 'ownedContracts.command')
|
|
1824
|
-
}
|
|
1825
|
-
const route = [...options.apiRoutes].find((apiRoute) => apiRoute === targetFact.id || apiRoute.endsWith(`/${targetFact.id}`))
|
|
1826
|
-
if (route) return { id: targetFact.id, resolution: 'fact-ref', factRef: { factSection: 'apiRoutes', factKey: route } }
|
|
1827
|
-
const ownerModule = targetModuleId(targetFact.id)
|
|
1828
|
-
if (ownerModule && !options.surfacesByModule[ownerModule]) {
|
|
1829
|
-
return { id: targetFact.id, resolution: 'optional-external', optionalOwnerPackage: `module:${ownerModule}` }
|
|
1830
|
-
}
|
|
1831
|
-
if (ownerModule && options.contributingModuleId && ownerModule !== options.contributingModuleId) {
|
|
1832
|
-
return { id: targetFact.id, resolution: 'optional-external', optionalOwnerPackage: `module:${ownerModule}` }
|
|
1833
|
-
}
|
|
1834
|
-
return targetFact
|
|
1835
|
-
}
|
|
1836
|
-
|
|
1837
|
-
export function correlateModuleExtensionFacts(options: CorrelateExtensionFactsOptions): Record<string, ModuleExtensionSurfaceFacts> {
|
|
1838
|
-
const result: Record<string, ModuleExtensionSurfaceFacts> = {}
|
|
1839
|
-
for (const moduleId of Object.keys(options.surfacesByModule).sort((left, right) => left.localeCompare(right))) {
|
|
1840
|
-
const surface = options.surfacesByModule[moduleId]
|
|
1841
|
-
const unresolved: ModuleExtensionUnresolvedFact[] = [...surface.unresolved]
|
|
1842
|
-
const contributions = surface.contributions.map((contribution) => {
|
|
1843
|
-
const targets = contribution.targets.map((entry) => correlateExtensionTarget(entry, {
|
|
1844
|
-
...options,
|
|
1845
|
-
contributingModuleId: moduleId,
|
|
1846
|
-
}))
|
|
1847
|
-
for (const entry of targets) {
|
|
1848
|
-
if (entry.resolution !== 'unresolved') continue
|
|
1849
|
-
unresolved.push({
|
|
1850
|
-
key: `${contribution.id}:${entry.id}`,
|
|
1851
|
-
source: contribution.source,
|
|
1852
|
-
reason: 'unresolved-first-party-target',
|
|
1853
|
-
})
|
|
1854
|
-
}
|
|
1855
|
-
return { ...contribution, targets }
|
|
1856
|
-
})
|
|
1857
|
-
result[moduleId] = {
|
|
1858
|
-
hosts: sortHosts([...surface.hosts]),
|
|
1859
|
-
contributions: sortContributions(contributions),
|
|
1860
|
-
unresolved: unresolved.sort((left, right) => left.key.localeCompare(right.key)),
|
|
1861
|
-
activations: sortActivations([...(surface.activations ?? [])]),
|
|
1862
|
-
}
|
|
1863
|
-
}
|
|
1864
|
-
return result
|
|
1865
|
-
}
|
|
1866
|
-
|
|
1867
|
-
type IncomingTargetOwner = {
|
|
1868
|
-
moduleId: string
|
|
1869
|
-
source: ModuleFactSourceRef
|
|
1870
|
-
/** Interceptor bridges the owning api route wires (api-route owners only). */
|
|
1871
|
-
bridges?: ApiRouteInterceptorBridge[]
|
|
1872
|
-
}
|
|
1873
|
-
|
|
1874
|
-
export interface CorrelateIncomingExtensionsOptions {
|
|
1875
|
-
surfacesByModule: Readonly<Record<string, ModuleExtensionSurfaceFacts>>
|
|
1876
|
-
/** Concrete runtime-id owner maps derived from the fully-selected module set. */
|
|
1877
|
-
apiRouteOwners?: ReadonlyMap<string, IncomingTargetOwner>
|
|
1878
|
-
commandOwners?: ReadonlyMap<string, IncomingTargetOwner>
|
|
1879
|
-
}
|
|
1880
|
-
|
|
1881
|
-
function normalizeRouteId(routeId: string): string {
|
|
1882
|
-
return routeId.replace(/^\//, '')
|
|
1883
|
-
}
|
|
1884
|
-
|
|
1885
|
-
/**
|
|
1886
|
-
* Owner marker for a contribution bound to a framework host (dashboard, menus,
|
|
1887
|
-
* notifications, …). Framework hosts have no module surface of their own, so the
|
|
1888
|
-
* activation is recorded on the contributor and never produces an incoming row.
|
|
1889
|
-
*/
|
|
1890
|
-
const FRAMEWORK_HOST_OWNER = 'framework'
|
|
1891
|
-
|
|
1892
|
-
/**
|
|
1893
|
-
* Framework host families each host-reference adapter may bind, so a dashboard
|
|
1894
|
-
* contribution resolves through `dashboard-host-consumer` rather than being claimed
|
|
1895
|
-
* by the generic widget-injection adapter that runs first.
|
|
1896
|
-
*/
|
|
1897
|
-
const FRAMEWORK_HOST_FAMILIES_BY_ACTIVATION: Partial<Record<ModuleExtensionActivationKind, ExtensionHostFamily[]>> = {
|
|
1898
|
-
'dashboard-host-consumer': ['dashboard'],
|
|
1899
|
-
'widget-injection-consumer': ['menu', 'generic', 'crud-form', 'data-table', 'notification', 'integration'],
|
|
1900
|
-
'component-extension-consumer': ['component-handle'],
|
|
1901
|
-
}
|
|
1902
|
-
|
|
1903
|
-
/**
|
|
1904
|
-
* Resolves a target id against the framework host catalog — exact ids first, then
|
|
1905
|
-
* the patterned entries (`dashboard:*`, `crud-form:*`, `integrations.detail:{id}`),
|
|
1906
|
-
* so a concrete `dashboard:<widget>` contribution reaches `framework.dashboard`.
|
|
1907
|
-
*/
|
|
1908
|
-
function findFrameworkHost(
|
|
1909
|
-
targetId: string,
|
|
1910
|
-
activationKind: ModuleExtensionActivationKind,
|
|
1911
|
-
): ModuleExtensionHostFact | null {
|
|
1912
|
-
const families = FRAMEWORK_HOST_FAMILIES_BY_ACTIVATION[activationKind]
|
|
1913
|
-
if (!families) return null
|
|
1914
|
-
const candidates = FRAMEWORK_HOSTS.filter((host) => families.includes(host.family))
|
|
1915
|
-
const exact = candidates.find((host) => host.id === targetId)
|
|
1916
|
-
if (exact) return exact
|
|
1917
|
-
return candidates.find((host) => host.resolution === 'pattern' && patternMatches(host.id, targetId)) ?? null
|
|
1918
|
-
}
|
|
1919
|
-
|
|
1920
|
-
/**
|
|
1921
|
-
* A call site that guards only some operations does not activate a contribution
|
|
1922
|
-
* declaring none of them. Either side leaving operations undeclared means "any".
|
|
1923
|
-
*/
|
|
1924
|
-
function operationsIntersect(
|
|
1925
|
-
activationOperations: readonly string[] | undefined,
|
|
1926
|
-
contributionOperations: readonly string[] | undefined,
|
|
1927
|
-
): boolean {
|
|
1928
|
-
if (!activationOperations || activationOperations.length === 0) return true
|
|
1929
|
-
if (!contributionOperations || contributionOperations.length === 0) return true
|
|
1930
|
-
return contributionOperations.some((operation) => activationOperations.includes(operation))
|
|
1931
|
-
}
|
|
1932
|
-
|
|
1933
|
-
function contributionSourceRef(contribution: ModuleExtensionContributionFact): ModuleFactSourceRef {
|
|
1934
|
-
return {
|
|
1935
|
-
sourcePath: contribution.source.path,
|
|
1936
|
-
...(contribution.source.symbol ? { exportName: contribution.source.symbol } : {}),
|
|
1937
|
-
}
|
|
1938
|
-
}
|
|
1939
|
-
|
|
1940
|
-
function targetRefKindFor(
|
|
1941
|
-
contribution: ModuleExtensionContributionFact,
|
|
1942
|
-
targetId: string,
|
|
1943
|
-
): ModuleExtensionTargetRef['kind'] {
|
|
1944
|
-
if (targetId === '*' || targetId.includes('*') || targetId.includes('{')) return 'wildcard'
|
|
1945
|
-
switch (contribution.kind) {
|
|
1946
|
-
case 'response-enricher':
|
|
1947
|
-
case 'mutation-guard':
|
|
1948
|
-
case 'entity-extension':
|
|
1949
|
-
return 'entity'
|
|
1950
|
-
case 'api-interceptor':
|
|
1951
|
-
return 'api-route'
|
|
1952
|
-
case 'command-interceptor':
|
|
1953
|
-
return 'command'
|
|
1954
|
-
case 'component-override':
|
|
1955
|
-
return 'component'
|
|
1956
|
-
case 'subscriber':
|
|
1957
|
-
return 'event'
|
|
1958
|
-
case 'browser-reaction':
|
|
1959
|
-
return contribution.details.transports.includes('notification-effect') ? 'notification' : 'event'
|
|
1960
|
-
case 'widget':
|
|
1961
|
-
case 'data-table':
|
|
1962
|
-
case 'crud-form':
|
|
1963
|
-
return 'widget-spot'
|
|
1964
|
-
case 'specialized-registry':
|
|
1965
|
-
case 'module-override':
|
|
1966
|
-
return 'module'
|
|
1967
|
-
default:
|
|
1968
|
-
return 'module'
|
|
1969
|
-
}
|
|
1970
|
-
}
|
|
1971
|
-
|
|
1972
|
-
/**
|
|
1973
|
-
* Builds the bidirectional incoming/resolution index after every module fact is
|
|
1974
|
-
* extracted and target-correlated. For each contribution target it records one
|
|
1975
|
-
* contributor-owned `ModuleContributionResolution` and, when a concrete target
|
|
1976
|
-
* owner exists, one target-owned `ModuleIncomingExtensionRef` per matching
|
|
1977
|
-
* activation. It never duplicates contribution behavior metadata — incoming rows
|
|
1978
|
-
* point back to the source-owned contribution by `contributorModuleId + contributionId`.
|
|
1979
|
-
*/
|
|
1980
|
-
export function correlateIncomingExtensions(
|
|
1981
|
-
options: CorrelateIncomingExtensionsOptions,
|
|
1982
|
-
): Record<string, ModuleExtensionSurfaceFacts> {
|
|
1983
|
-
const moduleIds = Object.keys(options.surfacesByModule).sort((left, right) => left.localeCompare(right))
|
|
1984
|
-
const apiRouteOwners = options.apiRouteOwners ?? new Map<string, IncomingTargetOwner>()
|
|
1985
|
-
const commandOwners = options.commandOwners ?? new Map<string, IncomingTargetOwner>()
|
|
1986
|
-
|
|
1987
|
-
// Index call-site activations by (kind, normalized host id).
|
|
1988
|
-
const activationIndex = new Map<string, Array<{ moduleId: string; activation: ModuleExtensionActivation }>>()
|
|
1989
|
-
const activationKey = (kind: ModuleExtensionActivationKind, hostKind: ModuleExtensionTargetRef['kind'], id: string): string => {
|
|
1990
|
-
const normId = hostKind === 'api-route' ? normalizeRouteId(id) : id
|
|
1991
|
-
return JSON.stringify([kind, hostKind, normId])
|
|
1992
|
-
}
|
|
1993
|
-
// Index bound hosts by their runtime id for host-reference resolution.
|
|
1994
|
-
const boundHostIndex = new Map<string, { moduleId: string; host: ModuleExtensionHostFact }>()
|
|
1995
|
-
|
|
1996
|
-
for (const moduleId of moduleIds) {
|
|
1997
|
-
const surface = options.surfacesByModule[moduleId]
|
|
1998
|
-
for (const activation of surface.activations ?? []) {
|
|
1999
|
-
const key = activationKey(activation.kind, activation.host.kind, activation.host.id)
|
|
2000
|
-
const bucket = activationIndex.get(key)
|
|
2001
|
-
if (bucket) bucket.push({ moduleId, activation })
|
|
2002
|
-
else activationIndex.set(key, [{ moduleId, activation }])
|
|
2003
|
-
}
|
|
2004
|
-
for (const host of surface.hosts) {
|
|
2005
|
-
if (!host.bound) continue
|
|
2006
|
-
if (!boundHostIndex.has(host.id)) boundHostIndex.set(host.id, { moduleId, host })
|
|
2007
|
-
}
|
|
2008
|
-
}
|
|
2009
|
-
|
|
2010
|
-
const incomingByModule: Record<string, ModuleIncomingExtensionRef[]> = {}
|
|
2011
|
-
const resolutionsByModule: Record<string, ModuleContributionResolution[]> = {}
|
|
2012
|
-
const syntheticActivations: Record<string, ModuleExtensionActivation[]> = {}
|
|
2013
|
-
for (const moduleId of moduleIds) {
|
|
2014
|
-
incomingByModule[moduleId] = []
|
|
2015
|
-
resolutionsByModule[moduleId] = []
|
|
2016
|
-
syntheticActivations[moduleId] = []
|
|
2017
|
-
}
|
|
2018
|
-
const registerSyntheticActivation = (ownerModule: string, activation: ModuleExtensionActivation): void => {
|
|
2019
|
-
if (!syntheticActivations[ownerModule]) syntheticActivations[ownerModule] = []
|
|
2020
|
-
if (!syntheticActivations[ownerModule].some((entry) => entry.id === activation.id)) {
|
|
2021
|
-
syntheticActivations[ownerModule].push(activation)
|
|
2022
|
-
}
|
|
2023
|
-
}
|
|
2024
|
-
|
|
2025
|
-
const findCallSiteActivations = (
|
|
2026
|
-
contributionKind: ModuleExtensionContributionKind,
|
|
2027
|
-
activationKinds: ModuleExtensionActivationKind[],
|
|
2028
|
-
targetRef: ModuleExtensionTargetRef,
|
|
2029
|
-
contributionOperations: readonly string[] | undefined,
|
|
2030
|
-
): Array<{ moduleId: string; activation: ModuleExtensionActivation }> => {
|
|
2031
|
-
const matches: Array<{ moduleId: string; activation: ModuleExtensionActivation }> = []
|
|
2032
|
-
for (const kind of activationKinds) {
|
|
2033
|
-
if (ACTIVATION_ADAPTERS[kind].mode !== 'call-site-object') continue
|
|
2034
|
-
const bucket = activationIndex.get(activationKey(kind, targetRef.kind, targetRef.id)) ?? []
|
|
2035
|
-
for (const entry of bucket) {
|
|
2036
|
-
if (!entry.activation.contributionKinds.includes(contributionKind)) continue
|
|
2037
|
-
if (!operationsIntersect(entry.activation.operations, contributionOperations)) continue
|
|
2038
|
-
matches.push(entry)
|
|
2039
|
-
}
|
|
2040
|
-
}
|
|
2041
|
-
return matches.sort((left, right) => left.activation.id.localeCompare(right.activation.id))
|
|
2042
|
-
}
|
|
2043
|
-
|
|
2044
|
-
for (const contributorModuleId of moduleIds) {
|
|
2045
|
-
const surface = options.surfacesByModule[contributorModuleId]
|
|
2046
|
-
for (const contribution of surface.contributions) {
|
|
2047
|
-
const classification = CONTRIBUTION_ACTIVATION_CLASSIFICATION[contribution.kind]
|
|
2048
|
-
for (const resolvedTarget of contribution.targets) {
|
|
2049
|
-
const targetKind = targetRefKindFor(contribution, resolvedTarget.id)
|
|
2050
|
-
const baseTarget: ModuleExtensionTargetRef = { kind: targetKind, id: resolvedTarget.id }
|
|
2051
|
-
|
|
2052
|
-
let resolution: ModuleExtensionResolution
|
|
2053
|
-
let ownerModule: string | null = null
|
|
2054
|
-
let boundActivationIds: string[] = []
|
|
2055
|
-
|
|
2056
|
-
if (targetKind === 'wildcard' || resolvedTarget.resolution === 'pattern') {
|
|
2057
|
-
resolution = 'wildcard'
|
|
2058
|
-
} else if (resolvedTarget.resolution === 'optional-external') {
|
|
2059
|
-
resolution = 'optional-target-missing'
|
|
2060
|
-
} else if (resolvedTarget.resolution === 'unresolved') {
|
|
2061
|
-
resolution = 'unresolved'
|
|
2062
|
-
} else {
|
|
2063
|
-
const activationKinds = classification === 'capability-only' ? [] : classification
|
|
2064
|
-
const callSiteMatches = findCallSiteActivations(
|
|
2065
|
-
contribution.kind,
|
|
2066
|
-
activationKinds,
|
|
2067
|
-
baseTarget,
|
|
2068
|
-
contribution.operations,
|
|
2069
|
-
)
|
|
2070
|
-
if (callSiteMatches.length > 0) {
|
|
2071
|
-
resolution = 'bound'
|
|
2072
|
-
ownerModule = callSiteMatches[0].moduleId
|
|
2073
|
-
boundActivationIds = callSiteMatches.map((entry) => entry.activation.id)
|
|
2074
|
-
} else {
|
|
2075
|
-
const referenceBinding = resolveReferenceBinding({
|
|
2076
|
-
contribution,
|
|
2077
|
-
activationKinds,
|
|
2078
|
-
targetRef: baseTarget,
|
|
2079
|
-
boundHostIndex,
|
|
2080
|
-
apiRouteOwners,
|
|
2081
|
-
commandOwners,
|
|
2082
|
-
})
|
|
2083
|
-
if (referenceBinding) {
|
|
2084
|
-
resolution = 'bound'
|
|
2085
|
-
// A framework host owns no module surface: record the activation on the
|
|
2086
|
-
// contributor and leave `ownerModule` unset so no incoming row is emitted.
|
|
2087
|
-
const isFrameworkHost = referenceBinding.ownerModule === FRAMEWORK_HOST_OWNER
|
|
2088
|
-
ownerModule = isFrameworkHost ? null : referenceBinding.ownerModule
|
|
2089
|
-
const activationOwner = isFrameworkHost ? contributorModuleId : referenceBinding.ownerModule
|
|
2090
|
-
for (const activation of referenceBinding.activations) {
|
|
2091
|
-
registerSyntheticActivation(activationOwner, activation)
|
|
2092
|
-
}
|
|
2093
|
-
boundActivationIds = referenceBinding.activations.map((activation) => activation.id)
|
|
2094
|
-
} else {
|
|
2095
|
-
const hostOwner = boundHostIndex.get(resolvedTarget.id)
|
|
2096
|
-
if (hostOwner) {
|
|
2097
|
-
resolution = 'capability-only'
|
|
2098
|
-
ownerModule = hostOwner.moduleId
|
|
2099
|
-
} else {
|
|
2100
|
-
resolution = 'capability-only'
|
|
2101
|
-
ownerModule = null
|
|
2102
|
-
}
|
|
2103
|
-
}
|
|
2104
|
-
}
|
|
2105
|
-
}
|
|
2106
|
-
|
|
2107
|
-
const resolutionTarget: ModuleExtensionTargetRef = ownerModule
|
|
2108
|
-
? { ...baseTarget, moduleId: ownerModule }
|
|
2109
|
-
: baseTarget
|
|
2110
|
-
resolutionsByModule[contributorModuleId].push({
|
|
2111
|
-
contributionId: contribution.id,
|
|
2112
|
-
target: resolutionTarget,
|
|
2113
|
-
resolution,
|
|
2114
|
-
activationIds: [...boundActivationIds].sort((left, right) => left.localeCompare(right)),
|
|
2115
|
-
})
|
|
2116
|
-
|
|
2117
|
-
// Incoming rows document contributions installed by OTHER modules onto a
|
|
2118
|
-
// target module's surface (cross-module discovery). Same-module targets are
|
|
2119
|
-
// already fully covered by the contributor-owned resolution row above, so
|
|
2120
|
-
// emitting a self-incoming row would only duplicate that fact and bloat the
|
|
2121
|
-
// generated context.
|
|
2122
|
-
if (ownerModule && ownerModule !== contributorModuleId && (resolution === 'bound' || resolution === 'capability-only')) {
|
|
2123
|
-
const source = contributionSourceRef(contribution)
|
|
2124
|
-
const incomingTarget: ModuleExtensionTargetRef = { ...baseTarget, moduleId: ownerModule }
|
|
2125
|
-
if (resolution === 'bound' && boundActivationIds.length > 0) {
|
|
2126
|
-
for (const activationRefId of boundActivationIds) {
|
|
2127
|
-
incomingByModule[ownerModule].push({
|
|
2128
|
-
contributionId: contribution.id,
|
|
2129
|
-
contributionKind: contribution.kind,
|
|
2130
|
-
contributorModuleId,
|
|
2131
|
-
target: incomingTarget,
|
|
2132
|
-
activationId: activationRefId,
|
|
2133
|
-
resolution: 'bound',
|
|
2134
|
-
source,
|
|
2135
|
-
})
|
|
2136
|
-
}
|
|
2137
|
-
} else {
|
|
2138
|
-
incomingByModule[ownerModule].push({
|
|
2139
|
-
contributionId: contribution.id,
|
|
2140
|
-
contributionKind: contribution.kind,
|
|
2141
|
-
contributorModuleId,
|
|
2142
|
-
target: incomingTarget,
|
|
2143
|
-
resolution,
|
|
2144
|
-
source,
|
|
2145
|
-
})
|
|
2146
|
-
}
|
|
2147
|
-
}
|
|
2148
|
-
}
|
|
2149
|
-
}
|
|
2150
|
-
}
|
|
2151
|
-
|
|
2152
|
-
const result: Record<string, ModuleExtensionSurfaceFacts> = {}
|
|
2153
|
-
for (const moduleId of moduleIds) {
|
|
2154
|
-
const surface = options.surfacesByModule[moduleId]
|
|
2155
|
-
result[moduleId] = {
|
|
2156
|
-
...surface,
|
|
2157
|
-
activations: sortActivations([...(surface.activations ?? []), ...syntheticActivations[moduleId]]),
|
|
2158
|
-
incoming: dedupeIncoming(incomingByModule[moduleId]),
|
|
2159
|
-
contributionResolutions: sortResolutions(resolutionsByModule[moduleId]),
|
|
2160
|
-
}
|
|
2161
|
-
}
|
|
2162
|
-
return result
|
|
2163
|
-
}
|
|
2164
|
-
|
|
2165
|
-
/**
|
|
2166
|
-
* Intersects an api-interceptor contribution's declared methods and phases with the
|
|
2167
|
-
* bridges its target route actually wires. A route with no bridge, a phase the route
|
|
2168
|
-
* never runs, or a method the route does not handle yields no binding, so the
|
|
2169
|
-
* contribution stays `capability-only` instead of being reported as active.
|
|
2170
|
-
*/
|
|
2171
|
-
function matchApiInterceptorBridges(
|
|
2172
|
-
contribution: ModuleExtensionContributionFact,
|
|
2173
|
-
owner: IncomingTargetOwner,
|
|
2174
|
-
): ApiRouteInterceptorBridge[] {
|
|
2175
|
-
if (contribution.kind !== 'api-interceptor') return []
|
|
2176
|
-
const bridges = owner.bridges ?? []
|
|
2177
|
-
const declaredMethods = contribution.details.methods ?? []
|
|
2178
|
-
const declaredPhases = contribution.phases ?? []
|
|
2179
|
-
const matched: ApiRouteInterceptorBridge[] = []
|
|
2180
|
-
for (const bridge of bridges) {
|
|
2181
|
-
if (declaredMethods.length > 0 && !declaredMethods.includes(bridge.method)) continue
|
|
2182
|
-
const phases = declaredPhases.length > 0
|
|
2183
|
-
? bridge.phases.filter((phase) => declaredPhases.includes(phase))
|
|
2184
|
-
: bridge.phases
|
|
2185
|
-
if (phases.length === 0) continue
|
|
2186
|
-
matched.push({ method: bridge.method, phases })
|
|
2187
|
-
}
|
|
2188
|
-
return matched
|
|
2189
|
-
}
|
|
2190
|
-
|
|
2191
|
-
function resolveReferenceBinding(input: {
|
|
2192
|
-
contribution: ModuleExtensionContributionFact
|
|
2193
|
-
activationKinds: ModuleExtensionActivationKind[]
|
|
2194
|
-
targetRef: ModuleExtensionTargetRef
|
|
2195
|
-
boundHostIndex: ReadonlyMap<string, { moduleId: string; host: ModuleExtensionHostFact }>
|
|
2196
|
-
apiRouteOwners: ReadonlyMap<string, IncomingTargetOwner>
|
|
2197
|
-
commandOwners: ReadonlyMap<string, IncomingTargetOwner>
|
|
2198
|
-
}): { ownerModule: string; activations: ModuleExtensionActivation[] } | null {
|
|
2199
|
-
for (const kind of input.activationKinds) {
|
|
2200
|
-
const adapter = ACTIVATION_ADAPTERS[kind]
|
|
2201
|
-
if (adapter.mode === 'call-site-object') continue
|
|
2202
|
-
if (!adapter.contributionKinds.includes(input.contribution.kind)) continue
|
|
2203
|
-
|
|
2204
|
-
if (adapter.mode === 'host-reference' && kind === 'api-interceptor-bridge') {
|
|
2205
|
-
const routeId = normalizeRouteId(input.targetRef.id)
|
|
2206
|
-
const owner = input.apiRouteOwners.get(routeId)
|
|
2207
|
-
if (!owner) continue
|
|
2208
|
-
const matchedBridges = matchApiInterceptorBridges(input.contribution, owner)
|
|
2209
|
-
if (matchedBridges.length === 0) continue
|
|
2210
|
-
return {
|
|
2211
|
-
ownerModule: owner.moduleId,
|
|
2212
|
-
activations: matchedBridges.map((bridge) => {
|
|
2213
|
-
const host: ModuleExtensionTargetRef = {
|
|
2214
|
-
kind: 'api-route',
|
|
2215
|
-
id: routeId,
|
|
2216
|
-
moduleId: owner.moduleId,
|
|
2217
|
-
method: bridge.method,
|
|
2218
|
-
}
|
|
2219
|
-
return {
|
|
2220
|
-
id: activationId(host, kind),
|
|
2221
|
-
kind,
|
|
2222
|
-
host,
|
|
2223
|
-
contributionKinds: ['api-interceptor'],
|
|
2224
|
-
phases: bridge.phases,
|
|
2225
|
-
source: owner.source,
|
|
2226
|
-
bridge: { factSection: 'apiRoutes', factKey: `/${routeId}` },
|
|
2227
|
-
}
|
|
2228
|
-
}),
|
|
2229
|
-
}
|
|
2230
|
-
}
|
|
2231
|
-
|
|
2232
|
-
if (adapter.mode === 'owner-reference' && kind === 'command-interceptor-bridge') {
|
|
2233
|
-
const owner = input.commandOwners.get(input.targetRef.id)
|
|
2234
|
-
if (!owner) continue
|
|
2235
|
-
const host: ModuleExtensionTargetRef = { kind: 'command', id: input.targetRef.id, moduleId: owner.moduleId }
|
|
2236
|
-
return {
|
|
2237
|
-
ownerModule: owner.moduleId,
|
|
2238
|
-
activations: [{
|
|
2239
|
-
id: activationId(host, kind),
|
|
2240
|
-
kind,
|
|
2241
|
-
host,
|
|
2242
|
-
contributionKinds: ['command-interceptor'],
|
|
2243
|
-
source: owner.source,
|
|
2244
|
-
bridge: { factSection: 'ownedContracts.command', factKey: input.targetRef.id },
|
|
2245
|
-
}],
|
|
2246
|
-
}
|
|
2247
|
-
}
|
|
2248
|
-
|
|
2249
|
-
if (adapter.mode === 'host-reference' && (kind === 'widget-injection-consumer' || kind === 'component-extension-consumer' || kind === 'dashboard-host-consumer')) {
|
|
2250
|
-
const frameworkHostFact = input.boundHostIndex.has(input.targetRef.id)
|
|
2251
|
-
? null
|
|
2252
|
-
: findFrameworkHost(input.targetRef.id, kind)
|
|
2253
|
-
const hostEntry = input.boundHostIndex.get(input.targetRef.id)
|
|
2254
|
-
?? (frameworkHostFact ? { moduleId: FRAMEWORK_HOST_OWNER, host: frameworkHostFact } : undefined)
|
|
2255
|
-
if (!hostEntry) continue
|
|
2256
|
-
const hostSource = hostReferenceSource(hostEntry.host)
|
|
2257
|
-
if (!hostSource) continue
|
|
2258
|
-
const host: ModuleExtensionTargetRef = { ...input.targetRef, moduleId: hostEntry.moduleId }
|
|
2259
|
-
return {
|
|
2260
|
-
ownerModule: hostEntry.moduleId,
|
|
2261
|
-
activations: [{
|
|
2262
|
-
id: activationId(host, kind),
|
|
2263
|
-
kind,
|
|
2264
|
-
host,
|
|
2265
|
-
contributionKinds: adapter.contributionKinds,
|
|
2266
|
-
source: hostSource,
|
|
2267
|
-
bridge: { factSection: 'hosts', factKey: hostEntry.host.key },
|
|
2268
|
-
}],
|
|
2269
|
-
}
|
|
2270
|
-
}
|
|
2271
|
-
}
|
|
2272
|
-
return null
|
|
2273
|
-
}
|
|
2274
|
-
|
|
2275
|
-
function hostReferenceSource(host: ModuleExtensionHostFact): ModuleFactSourceRef | null {
|
|
2276
|
-
if (host.source.kind === 'declaration' || host.source.kind === 'framework') {
|
|
2277
|
-
return { sourcePath: host.source.path, exportName: host.source.symbol }
|
|
2278
|
-
}
|
|
2279
|
-
return null
|
|
2280
|
-
}
|
|
2281
|
-
|
|
2282
|
-
function incomingIdentity(entry: ModuleIncomingExtensionRef): string {
|
|
2283
|
-
const target = JSON.stringify([entry.target.kind, entry.target.id])
|
|
2284
|
-
return JSON.stringify([entry.contributorModuleId, entry.contributionId, target, entry.activationId ?? entry.resolution])
|
|
2285
|
-
}
|
|
2286
|
-
|
|
2287
|
-
function dedupeIncoming(entries: ModuleIncomingExtensionRef[]): ModuleIncomingExtensionRef[] {
|
|
2288
|
-
const seen = new Set<string>()
|
|
2289
|
-
const unique: ModuleIncomingExtensionRef[] = []
|
|
2290
|
-
for (const entry of entries) {
|
|
2291
|
-
const identity = incomingIdentity(entry)
|
|
2292
|
-
if (seen.has(identity)) continue
|
|
2293
|
-
seen.add(identity)
|
|
2294
|
-
unique.push(entry)
|
|
2295
|
-
}
|
|
2296
|
-
return unique.sort((left, right) => incomingIdentity(left).localeCompare(incomingIdentity(right)))
|
|
2297
|
-
}
|
|
2298
|
-
|
|
2299
|
-
function resolutionIdentity(entry: ModuleContributionResolution): string {
|
|
2300
|
-
return JSON.stringify([entry.contributionId, entry.target.kind, entry.target.id, entry.resolution])
|
|
2301
|
-
}
|
|
2302
|
-
|
|
2303
|
-
function sortResolutions(entries: ModuleContributionResolution[]): ModuleContributionResolution[] {
|
|
2304
|
-
const seen = new Set<string>()
|
|
2305
|
-
const unique: ModuleContributionResolution[] = []
|
|
2306
|
-
for (const entry of entries) {
|
|
2307
|
-
const identity = resolutionIdentity(entry)
|
|
2308
|
-
if (seen.has(identity)) continue
|
|
2309
|
-
seen.add(identity)
|
|
2310
|
-
unique.push(entry)
|
|
2311
|
-
}
|
|
2312
|
-
return unique.sort((left, right) => resolutionIdentity(left).localeCompare(resolutionIdentity(right)))
|
|
2313
|
-
}
|
|
2314
|
-
|
|
2315
|
-
export function assertNoUnresolvedExtensionTargets(
|
|
2316
|
-
surfacesByModule: Readonly<Record<string, ModuleExtensionSurfaceFacts>>,
|
|
2317
|
-
): void {
|
|
2318
|
-
const unresolved = Object.entries(surfacesByModule).flatMap(([moduleId, surface]) =>
|
|
2319
|
-
surface.unresolved
|
|
2320
|
-
.filter((entry) => entry.reason === 'unresolved-first-party-target')
|
|
2321
|
-
.map((entry) => `${moduleId}:${entry.key}`),
|
|
2322
|
-
).sort((left, right) => left.localeCompare(right))
|
|
2323
|
-
if (unresolved.length > 0) {
|
|
2324
|
-
throw new Error(`[module-facts] unresolved first-party extension targets: ${unresolved.join(', ')}`)
|
|
2325
|
-
}
|
|
2326
|
-
}
|
|
2327
|
-
|
|
2328
|
-
/**
|
|
2329
|
-
* Supported merge/replace/disable modes for each dotted unified-override host,
|
|
2330
|
-
* derived from {@link FRAMEWORK_OVERRIDE_HOSTS} (the framework catalog authority
|
|
2331
|
-
* — spec 2026-08-02-module-facts-exact-override-targets). Keyed by the dotted
|
|
2332
|
-
* host id (e.g. `routes.api`, `ai.extensions`, `nav.groupOrder`) so the module
|
|
2333
|
-
* override-target adapters share one mode source of truth with the catalog.
|
|
2334
|
-
*/
|
|
2335
|
-
export function getFrameworkOverrideModes(): Record<string, 'disable-replace' | 'replace' | 'additive'> {
|
|
2336
|
-
const modes: Record<string, 'disable-replace' | 'replace' | 'additive'> = {}
|
|
2337
|
-
for (const host of FRAMEWORK_OVERRIDE_HOSTS) {
|
|
2338
|
-
const dotted = host.key.replace(/^framework\.module-override\./, '')
|
|
2339
|
-
const mode = host.operations?.[0]
|
|
2340
|
-
if (mode === 'disable-replace' || mode === 'replace' || mode === 'additive') modes[dotted] = mode
|
|
2341
|
-
}
|
|
2342
|
-
return modes
|
|
2343
|
-
}
|
|
2344
|
-
|
|
2345
|
-
export function getFrameworkExtensionHosts(): ModuleExtensionHostFact[] {
|
|
2346
|
-
return sortHosts(allFrameworkHosts().map((host) => ({
|
|
2347
|
-
...host,
|
|
2348
|
-
capabilities: [...host.capabilities],
|
|
2349
|
-
...(host.operations ? { operations: [...host.operations] } : {}),
|
|
2350
|
-
})))
|
|
2351
|
-
}
|
|
2352
|
-
|
|
2353
|
-
export function renderFrameworkExtensionPointsMarkdown(): string {
|
|
2354
|
-
const rows = getFrameworkExtensionHosts().map((host) =>
|
|
2355
|
-
`| ${host.id} | ${host.family} | ${[...host.capabilities, ...(host.operations ?? [])].join(', ')} | ${host.stability.toUpperCase()} |`,
|
|
2356
|
-
)
|
|
2357
|
-
return [
|
|
2358
|
-
'# Framework extension points (generated, do not edit)',
|
|
2359
|
-
'',
|
|
2360
|
-
'| ID / pattern | Family | Supports | Stability |',
|
|
2361
|
-
'|---|---|---|---|',
|
|
2362
|
-
...rows,
|
|
2363
|
-
'',
|
|
2364
|
-
].join('\n')
|
|
2365
|
-
}
|