@bleedingdev/modern-js-app-tools-extensions 3.8.3-ultramodern.6 → 3.9.0-ultramodern.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/dist/cjs/backend-federation-build/codegen.js +256 -0
  2. package/dist/cjs/backend-federation-build/config.js +189 -0
  3. package/dist/cjs/backend-federation-build/emit.js +150 -0
  4. package/dist/cjs/backend-federation-build/index.js +53 -0
  5. package/dist/cjs/build-config/build-environment.js +268 -0
  6. package/dist/cjs/build-config/precompress/plugin.js +124 -0
  7. package/dist/cjs/build-config/precompress/types.js +18 -0
  8. package/dist/cjs/build-config/public.js +46 -0
  9. package/dist/cjs/cloudflare/constants.js +1 -1
  10. package/dist/cjs/cloudflare/delivery-unit.js +7 -16
  11. package/dist/cjs/cloudflare-output-contract.js +37 -1
  12. package/dist/cjs/cloudflare-output-verifier/identity.js +6 -6
  13. package/dist/cjs/cloudflare-output-verifier/worker-bundles.js +4 -1
  14. package/dist/cjs/config.js +29 -1
  15. package/dist/cjs/deploy-output/npmAliases.js +226 -0
  16. package/dist/cjs/deploy-output/plugin.js +76 -0
  17. package/dist/cjs/deploy-output/target.js +48 -0
  18. package/dist/cjs/native-type-checker.js +193 -0
  19. package/dist/cjs/policy-defaults.js +101 -0
  20. package/dist/cjs/release-envelope/framework-output.js +77 -32
  21. package/dist/cjs/release-envelope/plugin.js +5 -3
  22. package/dist/cjs/release-identity.js +3 -6
  23. package/dist/cjs/runtime-package-resolution.js +118 -0
  24. package/dist/esm/backend-federation-build/codegen.mjs +205 -0
  25. package/dist/esm/backend-federation-build/config.mjs +116 -0
  26. package/dist/esm/backend-federation-build/emit.mjs +99 -0
  27. package/dist/esm/backend-federation-build/index.mjs +1 -0
  28. package/dist/esm/build-config/build-environment.mjs +224 -0
  29. package/dist/esm/build-config/precompress/plugin.mjs +76 -0
  30. package/dist/esm/build-config/precompress/types.mjs +0 -0
  31. package/dist/esm/build-config/public.mjs +1 -0
  32. package/dist/esm/cloudflare/constants.mjs +1 -1
  33. package/dist/esm/cloudflare/delivery-unit.mjs +1 -10
  34. package/dist/esm/cloudflare-output-contract.mjs +35 -2
  35. package/dist/esm/cloudflare-output-verifier/identity.mjs +1 -1
  36. package/dist/esm/cloudflare-output-verifier/worker-bundles.mjs +5 -2
  37. package/dist/esm/config.mjs +1 -0
  38. package/dist/esm/deploy-output/npmAliases.mjs +174 -0
  39. package/dist/esm/deploy-output/plugin.mjs +28 -0
  40. package/dist/esm/deploy-output/target.mjs +10 -0
  41. package/dist/esm/native-type-checker.mjs +142 -0
  42. package/dist/esm/policy-defaults.mjs +45 -0
  43. package/dist/esm/release-envelope/framework-output.mjs +51 -6
  44. package/dist/esm/release-envelope/plugin.mjs +5 -3
  45. package/dist/esm/release-identity.mjs +3 -6
  46. package/dist/esm/runtime-package-resolution.mjs +60 -0
  47. package/dist/esm-node/backend-federation-build/codegen.mjs +206 -0
  48. package/dist/esm-node/backend-federation-build/config.mjs +117 -0
  49. package/dist/esm-node/backend-federation-build/emit.mjs +100 -0
  50. package/dist/esm-node/backend-federation-build/index.mjs +2 -0
  51. package/dist/esm-node/build-config/build-environment.mjs +225 -0
  52. package/dist/esm-node/build-config/precompress/plugin.mjs +77 -0
  53. package/dist/esm-node/build-config/precompress/types.mjs +1 -0
  54. package/dist/esm-node/build-config/public.mjs +2 -0
  55. package/dist/esm-node/cloudflare/constants.mjs +1 -1
  56. package/dist/esm-node/cloudflare/delivery-unit.mjs +1 -10
  57. package/dist/esm-node/cloudflare-output-contract.mjs +35 -2
  58. package/dist/esm-node/cloudflare-output-verifier/identity.mjs +1 -1
  59. package/dist/esm-node/cloudflare-output-verifier/worker-bundles.mjs +5 -2
  60. package/dist/esm-node/config.mjs +1 -0
  61. package/dist/esm-node/deploy-output/npmAliases.mjs +175 -0
  62. package/dist/esm-node/deploy-output/plugin.mjs +31 -0
  63. package/dist/esm-node/deploy-output/target.mjs +11 -0
  64. package/dist/esm-node/native-type-checker.mjs +143 -0
  65. package/dist/esm-node/policy-defaults.mjs +46 -0
  66. package/dist/esm-node/release-envelope/framework-output.mjs +51 -6
  67. package/dist/esm-node/release-envelope/plugin.mjs +5 -3
  68. package/dist/esm-node/release-identity.mjs +3 -6
  69. package/dist/esm-node/runtime-package-resolution.mjs +61 -0
  70. package/dist/types/backend-federation-build/codegen.d.ts +86 -0
  71. package/dist/types/backend-federation-build/config.d.ts +102 -0
  72. package/dist/types/backend-federation-build/emit.d.ts +21 -0
  73. package/dist/types/backend-federation-build/index.d.ts +2 -0
  74. package/dist/types/build-config/build-environment.d.ts +22 -0
  75. package/dist/types/build-config/precompress/plugin.d.ts +4 -0
  76. package/dist/types/build-config/precompress/types.d.ts +19 -0
  77. package/dist/types/build-config/public.d.ts +2 -0
  78. package/dist/types/cloudflare/artifacts.d.ts +1 -1
  79. package/dist/types/cloudflare/constants.d.ts +2 -2
  80. package/dist/types/cloudflare/delivery-unit.d.ts +2 -2
  81. package/dist/types/cloudflare/i18n-worker.d.ts +1 -1
  82. package/dist/types/cloudflare/index.d.ts +1 -1
  83. package/dist/types/cloudflare/security-policies.d.ts +1 -1
  84. package/dist/types/cloudflare/types.d.ts +1 -1
  85. package/dist/types/cloudflare/utils.d.ts +1 -1
  86. package/dist/types/cloudflare/worker-manifest.d.ts +4 -4
  87. package/dist/types/cloudflare/wrangler-config.d.ts +2 -2
  88. package/dist/types/cloudflare-output-contract.d.ts +2 -1
  89. package/dist/types/cloudflare-output-plan.d.ts +1 -1
  90. package/dist/types/cloudflare-output-verifier/identity.d.ts +2 -2
  91. package/dist/types/cloudflare-output-verifier/index.d.ts +5 -5
  92. package/dist/types/cloudflare-output-verifier/manifest.d.ts +1 -1
  93. package/dist/types/cloudflare-output-verifier/mutation-policy.d.ts +1 -1
  94. package/dist/types/cloudflare-output-verifier/worker-bundles.d.ts +1 -1
  95. package/dist/types/config.d.ts +2 -0
  96. package/dist/types/deploy-output/npmAliases.d.ts +14 -0
  97. package/dist/types/deploy-output/plugin.d.ts +15 -0
  98. package/dist/types/deploy-output/target.d.ts +6 -0
  99. package/dist/types/index.d.ts +10 -10
  100. package/dist/types/native-type-checker.d.ts +15 -0
  101. package/dist/types/policy-defaults.d.ts +46 -0
  102. package/dist/types/precompress/LICENSE +20 -0
  103. package/dist/types/precompress/index.d.ts +76 -0
  104. package/dist/types/release-envelope/canonical.d.ts +1 -1
  105. package/dist/types/release-envelope/framework-output.d.ts +3 -2
  106. package/dist/types/release-envelope/index.d.ts +3 -3
  107. package/dist/types/runtime-package-resolution.d.ts +28 -0
  108. package/package.json +72 -14
  109. package/src/backend-federation-build/codegen.ts +245 -0
  110. package/src/backend-federation-build/config.ts +356 -0
  111. package/src/backend-federation-build/emit.ts +202 -0
  112. package/src/backend-federation-build/index.ts +2 -0
  113. package/src/build-config/build-environment.ts +404 -0
  114. package/src/build-config/precompress/plugin.ts +140 -0
  115. package/src/build-config/precompress/types.ts +26 -0
  116. package/src/build-config/public.ts +6 -0
  117. package/src/cloudflare/constants.ts +1 -1
  118. package/src/cloudflare/delivery-unit.ts +3 -20
  119. package/src/cloudflare/worker-manifest.ts +1 -0
  120. package/src/cloudflare-builder.ts +1 -0
  121. package/src/cloudflare-output-contract.ts +37 -0
  122. package/src/cloudflare-output-verifier/identity.ts +1 -1
  123. package/src/cloudflare-output-verifier/worker-bundles.ts +5 -3
  124. package/src/config.ts +7 -0
  125. package/src/deploy-output/npmAliases.ts +333 -0
  126. package/src/deploy-output/plugin.ts +49 -0
  127. package/src/deploy-output/target.ts +23 -0
  128. package/src/native-type-checker.ts +183 -0
  129. package/src/policy-defaults.ts +127 -0
  130. package/src/release-envelope/framework-output.ts +135 -4
  131. package/src/release-envelope/plugin.ts +15 -3
  132. package/src/release-identity.ts +3 -2
  133. package/src/runtime-package-resolution.ts +115 -0
@@ -0,0 +1,245 @@
1
+ import path from 'node:path';
2
+ import {
3
+ BACKEND_FEDERATION_EFFECT_EXPOSE as BACKEND_EXPOSE,
4
+ BACKEND_FEDERATION_MANIFEST_FILE as BACKEND_MANIFEST_FILE,
5
+ BACKEND_FEDERATION_REMOTE_ENTRY_FILE as BACKEND_REMOTE_ENTRY_FILE,
6
+ BACKEND_FEDERATION_CONTRACT_VERSION as CONTRACT_VERSION,
7
+ formatBackendFederationValidationErrors,
8
+ BACKEND_FEDERATION_NODE_ADAPTER_VERSION as NODE_ADAPTER_VERSION,
9
+ validateBackendFederationManifest,
10
+ } from '@modern-js/backend-federation-contracts';
11
+ import type { BackendFederationEntryIntegrity } from '@modern-js/server-runtime-extensions/backend-federation-security';
12
+ import { build } from 'esbuild';
13
+ import { type BackendFederationApp, normalizeRelativePath } from './config';
14
+
15
+ const createRelativeImportSpecifier = (
16
+ fromDirectory: string,
17
+ toFile: string,
18
+ ) => {
19
+ const relativePath = normalizeRelativePath(
20
+ path.relative(fromDirectory, toFile),
21
+ );
22
+ return relativePath.startsWith('.') ? relativePath : `./${relativePath}`;
23
+ };
24
+
25
+ const createPublicPath = (containerEntry: string, fallbackPort: number) => {
26
+ try {
27
+ return new URL('.', containerEntry).href;
28
+ } catch {
29
+ const slashIndex = containerEntry.lastIndexOf('/');
30
+ if (slashIndex >= 0) {
31
+ return containerEntry.slice(0, slashIndex + 1);
32
+ }
33
+
34
+ return `http://localhost:${fallbackPort}/`;
35
+ }
36
+ };
37
+
38
+ export const createBackendRemoteEntrySource = async (
39
+ workspaceRoot: string,
40
+ app: BackendFederationApp,
41
+ effectApiPath: string,
42
+ entryPath: string,
43
+ ) => {
44
+ const effectApiSpecifier = createRelativeImportSpecifier(
45
+ path.dirname(effectApiPath),
46
+ effectApiPath,
47
+ );
48
+ const containerMetadata = {
49
+ buildVersion: app.buildVersion,
50
+ expose: BACKEND_EXPOSE,
51
+ name: app.backendName,
52
+ packageName: app.packageName,
53
+ remoteType: app.remoteType,
54
+ source: normalizeRelativePath(path.relative(workspaceRoot, effectApiPath)),
55
+ version: app.version,
56
+ ...(app.unitId ? { unitId: app.unitId } : {}),
57
+ ...(app.sourceRevision ? { sourceRevision: app.sourceRevision } : {}),
58
+ };
59
+ const compatibility = {
60
+ build: app.buildVersion,
61
+ contractVersion: CONTRACT_VERSION,
62
+ nodeAdapterVersion: NODE_ADAPTER_VERSION,
63
+ packageName: app.packageName,
64
+ ...(app.unitId ? { unitId: app.unitId } : {}),
65
+ ...(app.sourceRevision ? { sourceRevision: app.sourceRevision } : {}),
66
+ };
67
+ const source = `// Generated by Modern.js backend federation build.
68
+ import * as exposedNamespace from ${JSON.stringify(effectApiSpecifier)};
69
+
70
+ const exposedModule = {
71
+ ...exposedNamespace,
72
+ backendFederationContract: {
73
+ ...(exposedNamespace.backendFederationContract ?? {}),
74
+ compatibility: {
75
+ ...(exposedNamespace.backendFederationContract?.compatibility ?? {}),
76
+ ...${JSON.stringify(compatibility)},
77
+ },
78
+ name: ${JSON.stringify(app.backendName)},
79
+ role: 'microvertical-server',
80
+ runtimeFramework: 'effect',
81
+ strictEffectApproach: true,
82
+ },
83
+ };
84
+ const exposedModules = {
85
+ ${JSON.stringify(BACKEND_EXPOSE)}: () => Promise.resolve(exposedModule),
86
+ };
87
+ const __modernjsBackendFederation = ${JSON.stringify(containerMetadata)};
88
+
89
+ function init() {}
90
+
91
+ function get(id) {
92
+ const load = exposedModules[id];
93
+ if (!load) {
94
+ throw new Error(\`Unexpected backend federation expose: \${id}\`);
95
+ }
96
+
97
+ return async () => load();
98
+ }
99
+
100
+ module.exports = { __modernjsBackendFederation, get, init };
101
+ `;
102
+ const result = await build({
103
+ absWorkingDir: path.dirname(effectApiPath),
104
+ bundle: true,
105
+ format: 'cjs',
106
+ logLevel: 'silent',
107
+ platform: 'node',
108
+ stdin: {
109
+ contents: source,
110
+ loader: 'ts',
111
+ resolveDir: path.dirname(effectApiPath),
112
+ sourcefile: path.basename(entryPath),
113
+ },
114
+ target: 'node20',
115
+ write: false,
116
+ });
117
+ const output = result.outputFiles[0];
118
+ if (!output) {
119
+ throw new Error(
120
+ '[backend-federation-build] CommonJS backend federation container emitted no output.',
121
+ );
122
+ }
123
+ return output.text;
124
+ };
125
+
126
+ export const createBackendManifest = (
127
+ workspaceRoot: string,
128
+ distDirectory: string,
129
+ app: BackendFederationApp,
130
+ entryIntegrity: BackendFederationEntryIntegrity,
131
+ ) => {
132
+ const sourceModule = `${app.directory}/api/effect-api.ts`;
133
+ const publicPath = createPublicPath(app.containerEntry, app.port);
134
+
135
+ const manifest = {
136
+ schemaVersion: 1,
137
+ id: app.backendName,
138
+ name: app.backendName,
139
+ version: app.version,
140
+ buildVersion: app.buildVersion,
141
+ metaData: {
142
+ name: app.backendName,
143
+ type: 'backend',
144
+ buildInfo: {
145
+ buildVersion: app.buildVersion,
146
+ buildName: app.packageName,
147
+ },
148
+ remoteEntry: {
149
+ name: BACKEND_REMOTE_ENTRY_FILE,
150
+ path: '',
151
+ type: app.remoteType,
152
+ },
153
+ globalName: app.backendName,
154
+ publicPath,
155
+ ssrRemoteEntry: {
156
+ name: BACKEND_REMOTE_ENTRY_FILE,
157
+ path: '',
158
+ type: app.remoteType,
159
+ },
160
+ ssrPublicPath: publicPath,
161
+ },
162
+ entry: {
163
+ ...entryIntegrity,
164
+ file: BACKEND_REMOTE_ENTRY_FILE,
165
+ path: normalizeRelativePath(
166
+ path.relative(
167
+ workspaceRoot,
168
+ path.join(distDirectory, BACKEND_REMOTE_ENTRY_FILE),
169
+ ),
170
+ ),
171
+ type: app.remoteType,
172
+ url: app.containerEntry,
173
+ },
174
+ exposes: [
175
+ {
176
+ id: `${app.backendName}:${BACKEND_EXPOSE}`,
177
+ name: BACKEND_EXPOSE,
178
+ path: '',
179
+ assets: {
180
+ js: {
181
+ async: [],
182
+ sync: [BACKEND_REMOTE_ENTRY_FILE],
183
+ },
184
+ css: {
185
+ async: [],
186
+ sync: [],
187
+ },
188
+ },
189
+ },
190
+ ],
191
+ shared: [],
192
+ backendFederation: {
193
+ role: 'microvertical-server',
194
+ appId: app.id,
195
+ name: app.backendName,
196
+ runtimeFramework: 'effect',
197
+ strictEffectApproach: true,
198
+ contractVersion: CONTRACT_VERSION,
199
+ nodeAdapterVersion: NODE_ADAPTER_VERSION,
200
+ remoteType: app.remoteType,
201
+ expose: BACKEND_EXPOSE,
202
+ manifestUrl: app.manifestUrl,
203
+ containerEntry: app.containerEntry,
204
+ source: {
205
+ module: sourceModule,
206
+ },
207
+ readinessPath: `${app.apiPrefix}/${app.apiStem}/readiness`,
208
+ openapiPath: `${app.apiPrefix}/openapi.json`,
209
+ ...(app.deliveryUnit ? { deliveryUnit: app.deliveryUnit } : {}),
210
+ versionBoundary: {
211
+ invariant: 'web-and-api-same-build',
212
+ packageName: app.packageName,
213
+ version: app.version,
214
+ buildVersion: app.buildVersion,
215
+ ...(app.uiManifestUrl ? { uiManifestUrl: app.uiManifestUrl } : {}),
216
+ ...(app.deliveryUnit
217
+ ? {
218
+ deliveryUnit: {
219
+ unitId: app.deliveryUnit.unitId,
220
+ buildMarker: app.deliveryUnit.buildMarker,
221
+ sourceRevision: app.deliveryUnit.sourceRevision,
222
+ },
223
+ }
224
+ : {}),
225
+ },
226
+ },
227
+ };
228
+
229
+ const validation = validateBackendFederationManifest(manifest, {
230
+ path: 'backendFederationManifest',
231
+ requireEffectExpose: true,
232
+ requireEffectRuntime: true,
233
+ requireVersionFields: true,
234
+ });
235
+
236
+ if (!validation.ok) {
237
+ throw new Error(
238
+ `[backend-federation-build] Invalid backend federation manifest: ${formatBackendFederationValidationErrors(
239
+ validation.errors,
240
+ )}.`,
241
+ );
242
+ }
243
+
244
+ return manifest;
245
+ };
@@ -0,0 +1,356 @@
1
+ import { existsSync } from 'node:fs';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { resolveUltramodernSourceRevision } from '@modern-js/app-tools-extensions/release-identity';
5
+ import {
6
+ BACKEND_FEDERATION_MANIFEST_FILE as BACKEND_MANIFEST_FILE,
7
+ BACKEND_FEDERATION_REMOTE_ENTRY_FILE as BACKEND_REMOTE_ENTRY_FILE,
8
+ DELIVERY_UNIT_DEPLOY_PROFILE,
9
+ DELIVERY_UNIT_KIND,
10
+ DELIVERY_UNIT_SCHEMA_VERSION,
11
+ type DeliveryUnitContractBlock,
12
+ type DeliveryUnitRecord,
13
+ deliveryUnitContractBlock,
14
+ isUltramodernBuildArtifact,
15
+ ULTRAMODERN_BUILD_ARTIFACT_PATH,
16
+ type UltramodernBuildArtifact,
17
+ validateDeliveryUnitRecord,
18
+ validateUltramodernBuildArtifact,
19
+ } from '@modern-js/backend-federation-contracts';
20
+
21
+ export const COMPACT_CONFIG_PATH = '.modernjs/ultramodern.json';
22
+
23
+ export type CompactApp = {
24
+ id?: unknown;
25
+ kind?: unknown;
26
+ path?: unknown;
27
+ package?: unknown;
28
+ port?: unknown;
29
+ api?: {
30
+ prefix?: unknown;
31
+ stem?: unknown;
32
+ };
33
+ moduleFederation?: {
34
+ name?: unknown;
35
+ manifestUrl?: unknown;
36
+ };
37
+ backendFederation?: {
38
+ name?: unknown;
39
+ versionBoundary?: {
40
+ ui?: {
41
+ manifestUrl?: unknown;
42
+ };
43
+ };
44
+ executionSurfaces?: {
45
+ node?: {
46
+ remoteName?: unknown;
47
+ manifestUrl?: unknown;
48
+ containerEntry?: unknown;
49
+ remoteType?: unknown;
50
+ };
51
+ };
52
+ };
53
+ deliveryUnit?: {
54
+ unitId?: unknown;
55
+ buildMarker?: unknown;
56
+ sourceRevision?: unknown;
57
+ packageName?: unknown;
58
+ version?: unknown;
59
+ };
60
+ serverExecution?: {
61
+ node?: {
62
+ remoteName?: unknown;
63
+ manifestUrl?: unknown;
64
+ containerEntry?: unknown;
65
+ remoteType?: unknown;
66
+ };
67
+ };
68
+ };
69
+
70
+ export type CompactConfig = {
71
+ topology?: {
72
+ apps?: CompactApp[];
73
+ };
74
+ };
75
+
76
+ export type BackendFederationApp = {
77
+ id: string;
78
+ directory: string;
79
+ packageName?: string;
80
+ version?: string;
81
+ buildVersion?: string;
82
+ unitId?: string;
83
+ sourceRevision?: string;
84
+ deliveryUnit?: DeliveryUnitContractBlock;
85
+ port: number;
86
+ apiPrefix: string;
87
+ apiStem: string;
88
+ backendName: string;
89
+ manifestUrl: string;
90
+ containerEntry: string;
91
+ remoteType: string;
92
+ uiManifestUrl?: string;
93
+ compactDeliveryUnit?: {
94
+ unitId?: string;
95
+ buildMarker?: string;
96
+ sourceRevision?: string;
97
+ packageName?: string;
98
+ version?: string;
99
+ };
100
+ };
101
+
102
+ export type BackendFederationBuildIdentity = {
103
+ appId?: string;
104
+ packageName?: string;
105
+ version?: string;
106
+ buildVersion?: string;
107
+ unitId?: string;
108
+ sourceRevision?: string;
109
+ artifact?: UltramodernBuildArtifact;
110
+ };
111
+
112
+ export const normalizeRelativePath = (value: string) =>
113
+ value.replace(/\\/gu, '/').replace(/^\.\/+/u, '');
114
+
115
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
116
+ typeof value === 'object' && value !== null;
117
+
118
+ const readJsonFile = async <T>(filePath: string): Promise<T> =>
119
+ JSON.parse(await fs.readFile(filePath, 'utf8')) as T;
120
+
121
+ export const buildArtifactPathFor = (appDirectory: string) =>
122
+ path.join(appDirectory, ULTRAMODERN_BUILD_ARTIFACT_PATH);
123
+
124
+ export const readBuildIdentity = async (
125
+ appDirectory: string,
126
+ ): Promise<BackendFederationBuildIdentity> => {
127
+ const buildArtifactPath = buildArtifactPathFor(appDirectory);
128
+ if (existsSync(buildArtifactPath)) {
129
+ const artifact = await readJsonFile<unknown>(buildArtifactPath);
130
+ const artifactValidation = validateUltramodernBuildArtifact(
131
+ artifact,
132
+ buildArtifactPath,
133
+ );
134
+ if (!artifactValidation.ok) {
135
+ throw new Error(
136
+ `[backend-federation-build] Invalid delivery-unit build artifact at ${buildArtifactPath}.`,
137
+ );
138
+ }
139
+ if (!isUltramodernBuildArtifact(artifact)) {
140
+ throw new Error(
141
+ `[backend-federation-build] Invalid delivery-unit build artifact at ${buildArtifactPath}.`,
142
+ );
143
+ }
144
+
145
+ const deliveryUnit = artifact.deliveryUnit;
146
+ return {
147
+ artifact,
148
+ appId: deliveryUnit.appId,
149
+ buildVersion: deliveryUnit.buildMarker,
150
+ packageName: deliveryUnit.packageName,
151
+ version: deliveryUnit.version,
152
+ unitId: deliveryUnit.unitId,
153
+ sourceRevision: deliveryUnit.sourceRevision,
154
+ };
155
+ }
156
+
157
+ throw new Error(
158
+ `[backend-federation-build] Missing delivery-unit build artifact at ${buildArtifactPath}.`,
159
+ );
160
+ };
161
+
162
+ const toPascalCase = (value: string) =>
163
+ value
164
+ .split(/[^a-zA-Z0-9]+/u)
165
+ .filter(Boolean)
166
+ .map(part => part.charAt(0).toUpperCase() + part.slice(1))
167
+ .join('');
168
+
169
+ const stringValue = (value: unknown) =>
170
+ typeof value === 'string' && value.length > 0 ? value : undefined;
171
+
172
+ const createBackendName = (app: CompactApp, id: string) => {
173
+ const configuredName =
174
+ stringValue(app.backendFederation?.name) ??
175
+ stringValue(app.backendFederation?.executionSurfaces?.node?.remoteName) ??
176
+ stringValue(app.serverExecution?.node?.remoteName);
177
+
178
+ if (configuredName) {
179
+ return configuredName;
180
+ }
181
+
182
+ const mfName = stringValue(app.moduleFederation?.name);
183
+ return mfName ? `${mfName}Backend` : `vertical${toPascalCase(id)}Backend`;
184
+ };
185
+
186
+ export const findWorkspaceRoot = (appDirectory: string) => {
187
+ let current = appDirectory;
188
+
189
+ while (true) {
190
+ if (existsSync(path.join(current, COMPACT_CONFIG_PATH))) {
191
+ return current;
192
+ }
193
+
194
+ const parent = path.dirname(current);
195
+ if (parent === current) {
196
+ return undefined;
197
+ }
198
+ current = parent;
199
+ }
200
+ };
201
+
202
+ export const resolveWorkspaceSourceRevision = async (workspaceRoot: string) => {
203
+ return resolveUltramodernSourceRevision(workspaceRoot);
204
+ };
205
+
206
+ export const createStampedDeliveryUnit = (input: {
207
+ appId: string;
208
+ unitId?: string;
209
+ buildMarker?: string;
210
+ packageName?: string;
211
+ version?: string;
212
+ sourceRevision: string;
213
+ }): DeliveryUnitContractBlock | undefined => {
214
+ if (
215
+ !input.unitId ||
216
+ !input.buildMarker ||
217
+ !input.packageName ||
218
+ !input.version
219
+ ) {
220
+ return undefined;
221
+ }
222
+
223
+ const record = {
224
+ schemaVersion: DELIVERY_UNIT_SCHEMA_VERSION,
225
+ kind: DELIVERY_UNIT_KIND,
226
+ appId: input.appId,
227
+ unitId: input.unitId,
228
+ packageName: input.packageName,
229
+ version: input.version,
230
+ buildMarker: input.buildMarker,
231
+ sourceRevision: input.sourceRevision,
232
+ deployProfile: DELIVERY_UNIT_DEPLOY_PROFILE,
233
+ };
234
+ const validation = validateDeliveryUnitRecord(record);
235
+
236
+ if (!validation.ok) {
237
+ return undefined;
238
+ }
239
+
240
+ return deliveryUnitContractBlock(record as DeliveryUnitRecord);
241
+ };
242
+
243
+ export const createAppFromCompactMetadata = (
244
+ workspaceRoot: string,
245
+ appDirectory: string,
246
+ compactApp: CompactApp,
247
+ ): BackendFederationApp | undefined => {
248
+ const id = stringValue(compactApp.id);
249
+ const appPath = stringValue(compactApp.path);
250
+
251
+ if (!id || compactApp.kind !== 'vertical' || !isRecord(compactApp.api)) {
252
+ return undefined;
253
+ }
254
+
255
+ const directory = appPath
256
+ ? normalizeRelativePath(appPath)
257
+ : `verticals/${id}`;
258
+ if (path.resolve(workspaceRoot, directory) !== path.resolve(appDirectory)) {
259
+ return undefined;
260
+ }
261
+
262
+ const port =
263
+ typeof compactApp.port === 'number' ? compactApp.port : undefined;
264
+ if (port === undefined) {
265
+ return undefined;
266
+ }
267
+
268
+ const apiPrefix = stringValue(compactApp.api.prefix) ?? `/${id}-api`;
269
+ const apiStem = stringValue(compactApp.api.stem) ?? id;
270
+ const backendName = createBackendName(compactApp, id);
271
+ const manifestUrl =
272
+ stringValue(
273
+ compactApp.backendFederation?.executionSurfaces?.node?.manifestUrl,
274
+ ) ??
275
+ stringValue(compactApp.serverExecution?.node?.manifestUrl) ??
276
+ `http://localhost:${port}/${BACKEND_MANIFEST_FILE}`;
277
+ const containerEntry =
278
+ stringValue(
279
+ compactApp.backendFederation?.executionSurfaces?.node?.containerEntry,
280
+ ) ??
281
+ stringValue(compactApp.serverExecution?.node?.containerEntry) ??
282
+ `http://localhost:${port}/${BACKEND_REMOTE_ENTRY_FILE}`;
283
+ const configuredRemoteType =
284
+ stringValue(
285
+ compactApp.backendFederation?.executionSurfaces?.node?.remoteType,
286
+ ) ?? stringValue(compactApp.serverExecution?.node?.remoteType);
287
+ if (
288
+ configuredRemoteType !== undefined &&
289
+ configuredRemoteType !== 'commonjs-module'
290
+ ) {
291
+ throw new Error(
292
+ `[backend-federation-build] Node backend federation remoteType must be "commonjs-module"; received "${configuredRemoteType}" for ${id}.`,
293
+ );
294
+ }
295
+ if (
296
+ !new URL(containerEntry).pathname.endsWith(`/${BACKEND_REMOTE_ENTRY_FILE}`)
297
+ ) {
298
+ throw new Error(
299
+ `[backend-federation-build] Node backend federation containerEntry must end with "/${BACKEND_REMOTE_ENTRY_FILE}"; received "${containerEntry}" for ${id}.`,
300
+ );
301
+ }
302
+ const remoteType = 'commonjs-module';
303
+ const packageName = stringValue(compactApp.package);
304
+ const uiManifestUrl =
305
+ stringValue(
306
+ compactApp.backendFederation?.versionBoundary?.ui?.manifestUrl,
307
+ ) ?? stringValue(compactApp.moduleFederation?.manifestUrl);
308
+ const compactDeliveryUnit = isRecord(compactApp.deliveryUnit)
309
+ ? {
310
+ unitId: stringValue(compactApp.deliveryUnit.unitId),
311
+ buildMarker: stringValue(compactApp.deliveryUnit.buildMarker),
312
+ sourceRevision: stringValue(compactApp.deliveryUnit.sourceRevision),
313
+ packageName: stringValue(compactApp.deliveryUnit.packageName),
314
+ version: stringValue(compactApp.deliveryUnit.version),
315
+ }
316
+ : undefined;
317
+
318
+ return {
319
+ id,
320
+ directory,
321
+ packageName,
322
+ port,
323
+ apiPrefix,
324
+ apiStem,
325
+ backendName,
326
+ manifestUrl,
327
+ containerEntry,
328
+ remoteType,
329
+ uiManifestUrl,
330
+ compactDeliveryUnit,
331
+ };
332
+ };
333
+
334
+ export const findBackendFederationApp = async (
335
+ workspaceRoot: string,
336
+ appDirectory: string,
337
+ ) => {
338
+ const compactConfigPath = path.join(workspaceRoot, COMPACT_CONFIG_PATH);
339
+ const compactConfig = await readJsonFile<CompactConfig>(compactConfigPath);
340
+ const apps = Array.isArray(compactConfig.topology?.apps)
341
+ ? compactConfig.topology.apps
342
+ : [];
343
+
344
+ for (const app of apps) {
345
+ const resolved = createAppFromCompactMetadata(
346
+ workspaceRoot,
347
+ appDirectory,
348
+ app,
349
+ );
350
+ if (resolved) {
351
+ return resolved;
352
+ }
353
+ }
354
+
355
+ return undefined;
356
+ };