@bleedingdev/modern-js-app-tools-extensions 3.9.0-ultramodern.11 → 3.9.0-ultramodern.13
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/dist/cjs/backend-federation-build/codegen.js +7 -2
- package/dist/cjs/backend-federation-build/config.js +86 -36
- package/dist/cjs/backend-federation-build/emit.js +17 -13
- package/dist/cjs/build-config/remote-address.js +56 -0
- package/dist/cjs/cloudflare/delivery-unit.js +16 -26
- package/dist/cjs/cloudflare/index.js +7 -1
- package/dist/cjs/cloudflare-builder.js +13 -3
- package/dist/cjs/cloudflare-output-contract.js +1 -0
- package/dist/cjs/cloudflare-output-verifier/identity.js +26 -24
- package/dist/cjs/cloudflare-output-verifier/index.js +10 -0
- package/dist/cjs/cloudflare-output-verifier/manifest.js +46 -0
- package/dist/cjs/cloudflare-output-verifier/mutation-policy.js +7 -1
- package/dist/cjs/config.js +4 -0
- package/dist/cjs/deploy-output/npmAliases.js +28 -3
- package/dist/cjs/release-envelope/framework-output.js +62 -49
- package/dist/cjs/release-envelope/index.js +10 -4
- package/dist/esm/backend-federation-build/codegen.mjs +7 -2
- package/dist/esm/backend-federation-build/config.mjs +78 -31
- package/dist/esm/backend-federation-build/emit.mjs +18 -14
- package/dist/esm/build-config/remote-address.mjs +18 -0
- package/dist/esm/cloudflare/delivery-unit.mjs +16 -26
- package/dist/esm/cloudflare/index.mjs +7 -1
- package/dist/esm/cloudflare-builder.mjs +13 -3
- package/dist/esm/cloudflare-output-contract.mjs +1 -0
- package/dist/esm/cloudflare-output-verifier/identity.mjs +26 -24
- package/dist/esm/cloudflare-output-verifier/index.mjs +10 -0
- package/dist/esm/cloudflare-output-verifier/manifest.mjs +46 -0
- package/dist/esm/cloudflare-output-verifier/mutation-policy.mjs +7 -1
- package/dist/esm/config.mjs +1 -0
- package/dist/esm/deploy-output/npmAliases.mjs +28 -3
- package/dist/esm/release-envelope/framework-output.mjs +62 -49
- package/dist/esm/release-envelope/index.mjs +10 -4
- package/dist/esm-node/backend-federation-build/codegen.mjs +7 -2
- package/dist/esm-node/backend-federation-build/config.mjs +78 -31
- package/dist/esm-node/backend-federation-build/emit.mjs +18 -14
- package/dist/esm-node/build-config/remote-address.mjs +19 -0
- package/dist/esm-node/cloudflare/delivery-unit.mjs +16 -26
- package/dist/esm-node/cloudflare/index.mjs +7 -1
- package/dist/esm-node/cloudflare-builder.mjs +13 -3
- package/dist/esm-node/cloudflare-output-contract.mjs +1 -0
- package/dist/esm-node/cloudflare-output-verifier/identity.mjs +26 -24
- package/dist/esm-node/cloudflare-output-verifier/index.mjs +10 -0
- package/dist/esm-node/cloudflare-output-verifier/manifest.mjs +46 -0
- package/dist/esm-node/cloudflare-output-verifier/mutation-policy.mjs +7 -1
- package/dist/esm-node/config.mjs +1 -0
- package/dist/esm-node/deploy-output/npmAliases.mjs +28 -3
- package/dist/esm-node/release-envelope/framework-output.mjs +62 -49
- package/dist/esm-node/release-envelope/index.mjs +10 -4
- package/dist/types/backend-federation-build/codegen.d.ts +30 -0
- package/dist/types/backend-federation-build/config.d.ts +32 -13
- package/dist/types/build-config/remote-address.d.ts +3 -0
- package/dist/types/cloudflare/delivery-unit.d.ts +1 -1
- package/dist/types/cloudflare-builder.d.ts +1 -0
- package/dist/types/cloudflare-output-contract.d.ts +1 -1
- package/dist/types/config.d.ts +1 -0
- package/package.json +20 -19
- package/src/backend-federation-build/codegen.ts +6 -2
- package/src/backend-federation-build/config.ts +207 -71
- package/src/backend-federation-build/emit.ts +33 -22
- package/src/build-config/remote-address.ts +31 -0
- package/src/cloudflare/delivery-unit.ts +39 -33
- package/src/cloudflare/index.ts +20 -6
- package/src/cloudflare-builder.ts +18 -4
- package/src/cloudflare-output-contract.ts +1 -0
- package/src/cloudflare-output-verifier/identity.ts +4 -0
- package/src/cloudflare-output-verifier/index.ts +20 -0
- package/src/cloudflare-output-verifier/manifest.ts +77 -0
- package/src/cloudflare-output-verifier/mutation-policy.ts +11 -1
- package/src/config.ts +1 -0
- package/src/deploy-output/npmAliases.ts +52 -3
- package/src/release-envelope/framework-output.ts +163 -90
- package/src/release-envelope/index.ts +26 -5
|
@@ -9,6 +9,11 @@ const MICROVERTICAL_RELEASE_IDENTITY_CARRIERS_KIND = 'ultramodern-release-identi
|
|
|
9
9
|
const MICROVERTICAL_RELEASE_IDENTITY_CARRIERS_SCHEMA_VERSION = 1;
|
|
10
10
|
const COMPILED_MODULE_PATTERN = /\.(?:c|m)?js$/u;
|
|
11
11
|
const EFFECT_BFF_WORKER_PATTERN = /^worker\/__modern_bff_effect\.(?:c|m)?js$/u;
|
|
12
|
+
const EFFECT_BFF_WORKER_SUPPORT_PATTERN = /^worker\/__modern_worker_(?:runtime|shared)_*\.(?:c|m)?js$/u;
|
|
13
|
+
const isEffectBffWorkerArtifact = (logicalPath)=>EFFECT_BFF_WORKER_PATTERN.test(logicalPath) || EFFECT_BFF_WORKER_SUPPORT_PATTERN.test(logicalPath);
|
|
14
|
+
const CRAWLER_POLICY_PATH = 'public/robots.txt';
|
|
15
|
+
const isPublicMetadataPath = (logicalPath)=>'public/_headers' === logicalPath || /^public\/\.well-known\/[^/]+\.json$/u.test(logicalPath);
|
|
16
|
+
const PUBLIC_BUILD_ARTIFACT_PATH = `public/${ULTRAMODERN_BUILD_ARTIFACT_FILE}`;
|
|
12
17
|
const isRecord = (value)=>'object' == typeof value && null !== value && !Array.isArray(value);
|
|
13
18
|
const flattenStrings = (value)=>'string' == typeof value ? [
|
|
14
19
|
value
|
|
@@ -129,7 +134,7 @@ const readBuildArtifact = async (distDirectory, requirePromotable = true)=>{
|
|
|
129
134
|
if (!isUltramodernBuildArtifact(artifact)) throw new Error(`[ultramodern-release-envelope] ${ULTRAMODERN_BUILD_ARTIFACT_FILE} is missing or invalid.`);
|
|
130
135
|
if ('workspace' === artifact.deliveryUnit.sourceRevision) {
|
|
131
136
|
if (!requirePromotable) return;
|
|
132
|
-
throw new Error('[ultramodern-release-envelope] sourceRevision "workspace" cannot produce a promotable
|
|
137
|
+
throw new Error('[ultramodern-release-envelope] sourceRevision "workspace" cannot produce a promotable envelope.');
|
|
133
138
|
}
|
|
134
139
|
return artifact;
|
|
135
140
|
};
|
|
@@ -199,7 +204,7 @@ const releaseArtifact = (logicalPath, runtime)=>({
|
|
|
199
204
|
const writeReleaseIdentityCarrierMetadata = async (artifactRoot, identity, carrierPaths)=>{
|
|
200
205
|
const surfacesByPath = new Map();
|
|
201
206
|
for (const [surface, logicalPaths] of Object.entries(carrierPaths)){
|
|
202
|
-
if (0 === logicalPaths.length) throw new Error(`[ultramodern-release-envelope] ${surface} has no declared release-identity carrier artifact.`);
|
|
207
|
+
if (0 === logicalPaths.length && 'ssr' !== surface && 'uiClient' !== surface) throw new Error(`[ultramodern-release-envelope] ${surface} has no declared release-identity carrier artifact.`);
|
|
203
208
|
for (const logicalPath of logicalPaths){
|
|
204
209
|
const surfaces = surfacesByPath.get(logicalPath) ?? new Set();
|
|
205
210
|
surfaces.add(surface);
|
|
@@ -239,12 +244,12 @@ const writeReleaseIdentityCarrierMetadata = async (artifactRoot, identity, carri
|
|
|
239
244
|
}, null, 2)}\n`);
|
|
240
245
|
return releaseArtifact(MICROVERTICAL_RELEASE_IDENTITY_CARRIERS_PATH, 'release-identity-metadata');
|
|
241
246
|
};
|
|
242
|
-
const createReleaseArtifactInputs = async (distDirectory, identity, target)=>{
|
|
247
|
+
const createReleaseArtifactInputs = async (distDirectory, identity, target, apiOnly)=>{
|
|
243
248
|
const files = await collectFiles(distDirectory);
|
|
244
|
-
if ('node' === target && files.some(
|
|
249
|
+
if ('node' === target && files.some(isEffectBffWorkerArtifact)) throw new Error('[ultramodern-release-envelope] Node target conflicts with a Cloudflare Effect API/BFF worker artifact; resolve the canonical deploy target instead of emitting a Node envelope for Cloudflare output.');
|
|
245
250
|
const uiClientPaths = files.filter((logicalPath)=>{
|
|
246
251
|
const topLevel = logicalPath.split('/')[0];
|
|
247
|
-
return 'static' === topLevel || 'html' === topLevel || 'public' === topLevel || [
|
|
252
|
+
return 'static' === topLevel || 'html' === topLevel || 'public' === topLevel && logicalPath !== CRAWLER_POLICY_PATH && !isPublicMetadataPath(logicalPath) || [
|
|
248
253
|
'index.html',
|
|
249
254
|
'mf-manifest.json',
|
|
250
255
|
'mf-stats.json',
|
|
@@ -253,16 +258,17 @@ const createReleaseArtifactInputs = async (distDirectory, identity, target)=>{
|
|
|
253
258
|
'loadable-stats.json'
|
|
254
259
|
].includes(logicalPath);
|
|
255
260
|
});
|
|
256
|
-
const ssrPaths = 'node' === target ? files.filter((logicalPath)=>logicalPath.startsWith('bundles/') && COMPILED_MODULE_PATTERN.test(logicalPath)) : files.filter((logicalPath)=>logicalPath.startsWith('worker/') && COMPILED_MODULE_PATTERN.test(logicalPath) && !
|
|
261
|
+
const ssrPaths = 'node' === target ? files.filter((logicalPath)=>logicalPath.startsWith('bundles/') && COMPILED_MODULE_PATTERN.test(logicalPath)) : files.filter((logicalPath)=>logicalPath.startsWith('worker/') && COMPILED_MODULE_PATTERN.test(logicalPath) && !isEffectBffWorkerArtifact(logicalPath));
|
|
257
262
|
const nodeApiEntryPaths = files.filter((logicalPath)=>logicalPath.startsWith('api/') && COMPILED_MODULE_PATTERN.test(logicalPath));
|
|
258
|
-
const apiBackendPaths = 'node' === target ? files.filter((logicalPath)=>(logicalPath.startsWith('api/') || logicalPath.startsWith('shared/')) && COMPILED_MODULE_PATTERN.test(logicalPath)) : files.filter(
|
|
259
|
-
if (
|
|
260
|
-
if (0 ===
|
|
261
|
-
if (
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
263
|
+
const apiBackendPaths = 'node' === target ? files.filter((logicalPath)=>(logicalPath.startsWith('api/') || logicalPath.startsWith('shared/')) && COMPILED_MODULE_PATTERN.test(logicalPath)) : files.filter(isEffectBffWorkerArtifact);
|
|
264
|
+
if (apiOnly && (uiClientPaths.length > 0 || ssrPaths.length > 0)) throw new Error('[ultramodern-release-envelope] API-only MicroVertical emitted an undeclared UI/client or SSR surface.');
|
|
265
|
+
if (!apiOnly && 0 === uiClientPaths.length) throw new Error(`[ultramodern-release-envelope] ${target} full-stack MicroVertical has no UI/client artifacts.`);
|
|
266
|
+
if (!apiOnly && 0 === ssrPaths.length) throw new Error(`[ultramodern-release-envelope] ${target} full-stack MicroVertical has no ${'node' === target ? 'Node' : 'workerd'} SSR artifacts.`);
|
|
267
|
+
if ('node' === target && 0 === nodeApiEntryPaths.length || 'cloudflare' === target && !files.some((logicalPath)=>EFFECT_BFF_WORKER_PATTERN.test(logicalPath)) || 0 === apiBackendPaths.length) throw new Error(`[ultramodern-release-envelope] ${target} MicroVertical has no actual ${'node' === target ? 'compiled Node Effect API artifact' : 'Effect API/BFF worker artifact'}.`);
|
|
268
|
+
const clientExecutionPaths = apiOnly ? [] : await manifestReferencedClientModules(distDirectory, files);
|
|
269
|
+
if (!apiOnly && 0 === clientExecutionPaths.length) throw new Error('[ultramodern-release-envelope] UI/client manifest references no compiled execution module.');
|
|
270
|
+
const ssrExecutionPaths = apiOnly ? [] : await routeReferencedSsrModules(distDirectory, files, target);
|
|
271
|
+
if (!apiOnly && 0 === ssrExecutionPaths.length) throw new Error(`[ultramodern-release-envelope] route manifest references no emitted ${'node' === target ? 'Node' : 'Cloudflare'} SSR execution module.`);
|
|
266
272
|
const runtimeByPath = new Map();
|
|
267
273
|
const add = (paths, runtime)=>{
|
|
268
274
|
for (const logicalPath of paths){
|
|
@@ -272,6 +278,8 @@ const createReleaseArtifactInputs = async (distDirectory, identity, target)=>{
|
|
|
272
278
|
}
|
|
273
279
|
};
|
|
274
280
|
add(uiClientPaths, 'browser');
|
|
281
|
+
add(files.filter((logicalPath)=>logicalPath === CRAWLER_POLICY_PATH), 'crawler-policy');
|
|
282
|
+
add(files.filter(isPublicMetadataPath), 'public-metadata');
|
|
275
283
|
add(ssrPaths, 'node' === target ? 'nodejs' : 'workerd');
|
|
276
284
|
add(apiBackendPaths, 'node' === target ? 'nodejs' : 'workerd-effect');
|
|
277
285
|
add([
|
|
@@ -329,15 +337,14 @@ const pathExists = async (filePath)=>{
|
|
|
329
337
|
}
|
|
330
338
|
};
|
|
331
339
|
const emitFrameworkMicroVerticalReleaseEnvelope = async ({ apiOnly, distDirectory, requirePromotable = true, target })=>{
|
|
332
|
-
if (apiOnly) return;
|
|
333
340
|
const backendManifestPath = node_path.join(distDirectory, BACKEND_FEDERATION_MANIFEST_FILE);
|
|
334
341
|
const backendContainerPath = node_path.join(distDirectory, BACKEND_FEDERATION_REMOTE_ENTRY_FILE);
|
|
335
342
|
const [hasBackendManifest, hasBackendContainer] = await Promise.all([
|
|
336
343
|
pathExists(backendManifestPath),
|
|
337
344
|
pathExists(backendContainerPath)
|
|
338
345
|
]);
|
|
339
|
-
if (!hasBackendManifest && !hasBackendContainer) return;
|
|
340
|
-
if (!hasBackendManifest || !hasBackendContainer) throw new Error('[ultramodern-release-envelope]
|
|
346
|
+
if (!hasBackendManifest && !hasBackendContainer && !apiOnly) return;
|
|
347
|
+
if (!hasBackendManifest || !hasBackendContainer) throw new Error('[ultramodern-release-envelope] MicroVertical backend federation manifest and container must be emitted together.');
|
|
341
348
|
const buildArtifact = await readBuildArtifact(distDirectory, requirePromotable);
|
|
342
349
|
if (!buildArtifact) return;
|
|
343
350
|
const identity = {
|
|
@@ -347,7 +354,7 @@ const emitFrameworkMicroVerticalReleaseEnvelope = async ({ apiOnly, distDirector
|
|
|
347
354
|
releaseVersion: buildArtifact.deliveryUnit.version
|
|
348
355
|
};
|
|
349
356
|
await readBackendManifest(distDirectory, identity);
|
|
350
|
-
const { artifacts, surfaces } = await createReleaseArtifactInputs(distDirectory, identity, target);
|
|
357
|
+
const { artifacts, surfaces } = await createReleaseArtifactInputs(distDirectory, identity, target, apiOnly);
|
|
351
358
|
const envelope = await createMicroVerticalReleaseEnvelope({
|
|
352
359
|
artifactRoot: distDirectory,
|
|
353
360
|
target,
|
|
@@ -378,19 +385,16 @@ const readFrameworkMicroVerticalReleaseEnvelope = async ({ artifactRoot, expecte
|
|
|
378
385
|
});
|
|
379
386
|
};
|
|
380
387
|
const verifyBuildOutputReleaseEnvelope = async (distDirectory, expectedTarget)=>{
|
|
381
|
-
const [hasEnvelope, hasBuildArtifact
|
|
388
|
+
const [hasEnvelope, hasBuildArtifact] = await Promise.all([
|
|
382
389
|
pathExists(node_path.join(distDirectory, MICROVERTICAL_RELEASE_ENVELOPE_PATH)),
|
|
383
|
-
pathExists(node_path.join(distDirectory, ULTRAMODERN_BUILD_ARTIFACT_FILE))
|
|
384
|
-
pathExists(node_path.join(distDirectory, BACKEND_FEDERATION_MANIFEST_FILE)),
|
|
385
|
-
pathExists(node_path.join(distDirectory, BACKEND_FEDERATION_REMOTE_ENTRY_FILE))
|
|
390
|
+
pathExists(node_path.join(distDirectory, ULTRAMODERN_BUILD_ARTIFACT_FILE))
|
|
386
391
|
]);
|
|
387
|
-
const isFullStackMicroVertical = hasBuildArtifact && (hasBackendManifest || hasBackendEntry);
|
|
388
392
|
return readFrameworkMicroVerticalReleaseEnvelope({
|
|
389
393
|
artifactRoot: distDirectory,
|
|
390
394
|
...expectedTarget ? {
|
|
391
395
|
expectedTarget
|
|
392
396
|
} : {},
|
|
393
|
-
required: hasEnvelope ||
|
|
397
|
+
required: hasEnvelope || hasBuildArtifact
|
|
394
398
|
});
|
|
395
399
|
};
|
|
396
400
|
const verifyNodeReleaseEnvelopeStaging = async ({ outputDirectory })=>readFrameworkMicroVerticalReleaseEnvelope({
|
|
@@ -398,7 +402,7 @@ const verifyNodeReleaseEnvelopeStaging = async ({ outputDirectory })=>readFramew
|
|
|
398
402
|
expectedTarget: 'node',
|
|
399
403
|
required: true
|
|
400
404
|
});
|
|
401
|
-
const createNodeStagedReleaseArtifactInputs = async (outputDirectory, identity)=>{
|
|
405
|
+
const createNodeStagedReleaseArtifactInputs = async (outputDirectory, identity, apiOnly)=>{
|
|
402
406
|
const files = await collectFiles(outputDirectory);
|
|
403
407
|
for (const requiredPath of [
|
|
404
408
|
'index.js',
|
|
@@ -412,7 +416,7 @@ const createNodeStagedReleaseArtifactInputs = async (outputDirectory, identity)=
|
|
|
412
416
|
]);
|
|
413
417
|
const uiClientPaths = files.filter((logicalPath)=>{
|
|
414
418
|
const topLevel = logicalPath.split('/')[0];
|
|
415
|
-
return 'static' === topLevel || 'html' === topLevel || 'public' === topLevel || [
|
|
419
|
+
return 'static' === topLevel || 'html' === topLevel || 'public' === topLevel && logicalPath !== CRAWLER_POLICY_PATH && !isPublicMetadataPath(logicalPath) || [
|
|
416
420
|
'index.html',
|
|
417
421
|
'mf-manifest.json',
|
|
418
422
|
'mf-stats.json',
|
|
@@ -423,6 +427,7 @@ const createNodeStagedReleaseArtifactInputs = async (outputDirectory, identity)=
|
|
|
423
427
|
});
|
|
424
428
|
const ssrPaths = files.filter((logicalPath)=>'index.js' === logicalPath || logicalPath.startsWith('bundles/') && COMPILED_MODULE_PATTERN.test(logicalPath));
|
|
425
429
|
const apiBackendPaths = files.filter((logicalPath)=>(logicalPath.startsWith('api/') || logicalPath.startsWith('shared/')) && COMPILED_MODULE_PATTERN.test(logicalPath));
|
|
430
|
+
if (apiOnly && (uiClientPaths.length > 0 || ssrPaths.some((path)=>'index.js' !== path))) throw new Error('[ultramodern-release-envelope] final API-only Node staging contains an undeclared UI/client or SSR surface.');
|
|
426
431
|
for (const [surface, paths] of [
|
|
427
432
|
[
|
|
428
433
|
'UI/client',
|
|
@@ -430,22 +435,24 @@ const createNodeStagedReleaseArtifactInputs = async (outputDirectory, identity)=
|
|
|
430
435
|
],
|
|
431
436
|
[
|
|
432
437
|
'Node SSR',
|
|
433
|
-
ssrPaths
|
|
438
|
+
apiOnly ? [] : ssrPaths
|
|
434
439
|
],
|
|
435
440
|
[
|
|
436
441
|
'Node API/backend',
|
|
437
442
|
apiBackendPaths
|
|
438
443
|
]
|
|
439
|
-
])if (0 === paths.length) throw new Error(`[ultramodern-release-envelope] final Node staging has no ${surface} artifacts.`);
|
|
440
|
-
const clientExecutionPaths = await manifestReferencedClientModules(outputDirectory, files);
|
|
441
|
-
const ssrExecutionPaths = await routeReferencedSsrModules(outputDirectory, files, 'node');
|
|
442
|
-
if (0 === clientExecutionPaths.length || 0 === ssrExecutionPaths.length) throw new Error('[ultramodern-release-envelope] final Node manifests must reference emitted UI/client and SSR execution modules.');
|
|
444
|
+
])if (0 === paths.length && (!apiOnly || 'Node API/backend' === surface)) throw new Error(`[ultramodern-release-envelope] final Node staging has no ${surface} artifacts.`);
|
|
445
|
+
const clientExecutionPaths = apiOnly ? [] : await manifestReferencedClientModules(outputDirectory, files);
|
|
446
|
+
const ssrExecutionPaths = apiOnly ? [] : await routeReferencedSsrModules(outputDirectory, files, 'node');
|
|
447
|
+
if (!apiOnly && (0 === clientExecutionPaths.length || 0 === ssrExecutionPaths.length)) throw new Error('[ultramodern-release-envelope] final Node manifests must reference emitted UI/client and SSR execution modules.');
|
|
443
448
|
const runtimeByPath = new Map(files.filter((logicalPath)=>!backendFederationPaths.has(logicalPath)).map((logicalPath)=>[
|
|
444
449
|
logicalPath,
|
|
445
450
|
'nodejs-deployment'
|
|
446
451
|
]));
|
|
447
452
|
for (const logicalPath of uiClientPaths)runtimeByPath.set(logicalPath, 'browser');
|
|
448
|
-
|
|
453
|
+
if (files.includes(CRAWLER_POLICY_PATH)) runtimeByPath.set(CRAWLER_POLICY_PATH, 'crawler-policy');
|
|
454
|
+
for (const logicalPath of files.filter(isPublicMetadataPath))runtimeByPath.set(logicalPath, 'public-metadata');
|
|
455
|
+
for (const logicalPath of apiOnly ? [] : ssrPaths)runtimeByPath.set(logicalPath, 'nodejs');
|
|
449
456
|
for (const logicalPath of apiBackendPaths)runtimeByPath.set(logicalPath, 'nodejs');
|
|
450
457
|
runtimeByPath.set(BACKEND_FEDERATION_MANIFEST_FILE, 'module-federation-manifest');
|
|
451
458
|
runtimeByPath.set(BACKEND_FEDERATION_REMOTE_ENTRY_FILE, 'nodejs');
|
|
@@ -455,7 +462,7 @@ const createNodeStagedReleaseArtifactInputs = async (outputDirectory, identity)=
|
|
|
455
462
|
BACKEND_FEDERATION_MANIFEST_FILE,
|
|
456
463
|
BACKEND_FEDERATION_REMOTE_ENTRY_FILE
|
|
457
464
|
],
|
|
458
|
-
ssr: [
|
|
465
|
+
ssr: apiOnly ? [] : [
|
|
459
466
|
...new Set([
|
|
460
467
|
...ssrExecutionPaths,
|
|
461
468
|
...ssrPaths.filter((logicalPath)=>'index.js' !== logicalPath)
|
|
@@ -474,7 +481,7 @@ const createNodeStagedReleaseArtifactInputs = async (outputDirectory, identity)=
|
|
|
474
481
|
uiClient: [
|
|
475
482
|
...uiClientPaths
|
|
476
483
|
].sort((left, right)=>left.localeCompare(right)),
|
|
477
|
-
ssr: [
|
|
484
|
+
ssr: apiOnly ? [] : [
|
|
478
485
|
...ssrPaths
|
|
479
486
|
].sort((left, right)=>left.localeCompare(right)),
|
|
480
487
|
apiBackend: [
|
|
@@ -490,7 +497,7 @@ const createNodeStagedReleaseArtifactInputs = async (outputDirectory, identity)=
|
|
|
490
497
|
const emitNodeStagedReleaseEnvelope = async ({ distDirectory, outputDirectory })=>{
|
|
491
498
|
const source = await verifyBuildOutputReleaseEnvelope(distDirectory, 'node');
|
|
492
499
|
if (!source) return;
|
|
493
|
-
const { artifacts, surfaces } = await createNodeStagedReleaseArtifactInputs(outputDirectory, source.identity);
|
|
500
|
+
const { artifacts, surfaces } = await createNodeStagedReleaseArtifactInputs(outputDirectory, source.identity, 0 === source.surfaces.uiClient.length);
|
|
494
501
|
const staged = await createMicroVerticalReleaseEnvelope({
|
|
495
502
|
artifactRoot: outputDirectory,
|
|
496
503
|
target: 'node',
|
|
@@ -508,21 +515,25 @@ const stageCloudflareReleaseEnvelope = async ({ distDirectory, outputDirectory }
|
|
|
508
515
|
if (await pathExists(stagedEnvelopePath)) throw new Error('[ultramodern-release-envelope] Cloudflare staging contains a stale envelope before final output generation.');
|
|
509
516
|
return source;
|
|
510
517
|
};
|
|
511
|
-
const createCloudflareStagedReleaseArtifactInputs = async (outputDirectory, identity)=>{
|
|
518
|
+
const createCloudflareStagedReleaseArtifactInputs = async (outputDirectory, identity, apiOnly)=>{
|
|
512
519
|
const files = await collectFiles(outputDirectory);
|
|
513
520
|
for (const requiredPath of [
|
|
514
521
|
'server/index.mjs',
|
|
515
522
|
'server/modern-worker-manifest.json',
|
|
516
523
|
'wrangler.json',
|
|
517
524
|
'package.json',
|
|
518
|
-
|
|
525
|
+
...apiOnly ? [] : [
|
|
526
|
+
'worker/package.json'
|
|
527
|
+
]
|
|
519
528
|
])if (!files.includes(requiredPath)) throw new Error(`[ultramodern-release-envelope] final Cloudflare staging is missing ${requiredPath}.`);
|
|
520
529
|
if (files.some((logicalPath)=>'public/release' === logicalPath || logicalPath.startsWith('public/release/'))) throw new Error('[ultramodern-release-envelope] final Cloudflare envelope must remain private and cannot be staged under public/release.');
|
|
521
530
|
const backendManifestPath = `public/${BACKEND_FEDERATION_MANIFEST_FILE}`;
|
|
522
531
|
const backendContainerPath = `public/${BACKEND_FEDERATION_REMOTE_ENTRY_FILE}`;
|
|
523
|
-
const apiBackendPaths = files.filter(
|
|
524
|
-
|
|
525
|
-
const
|
|
532
|
+
const apiBackendPaths = files.filter(isEffectBffWorkerArtifact);
|
|
533
|
+
if (!files.some((logicalPath)=>EFFECT_BFF_WORKER_PATTERN.test(logicalPath))) throw new Error('[ultramodern-release-envelope] final Cloudflare staging has no actual Effect API/BFF worker artifact.');
|
|
534
|
+
const ssrPaths = files.filter((logicalPath)=>(logicalPath.startsWith('server/') || logicalPath.startsWith('worker/')) && COMPILED_MODULE_PATTERN.test(logicalPath) && !isEffectBffWorkerArtifact(logicalPath));
|
|
535
|
+
const uiClientPaths = files.filter((logicalPath)=>logicalPath.startsWith('public/') && logicalPath !== CRAWLER_POLICY_PATH && !isPublicMetadataPath(logicalPath) && logicalPath !== PUBLIC_BUILD_ARTIFACT_PATH && logicalPath !== backendManifestPath && logicalPath !== backendContainerPath);
|
|
536
|
+
if (apiOnly && (uiClientPaths.length > 0 || ssrPaths.some((path)=>'server/index.mjs' !== path))) throw new Error('[ultramodern-release-envelope] final API-only Cloudflare staging contains an undeclared UI/client or SSR surface.');
|
|
526
537
|
for (const [surface, paths] of [
|
|
527
538
|
[
|
|
528
539
|
'UI/client',
|
|
@@ -530,27 +541,29 @@ const createCloudflareStagedReleaseArtifactInputs = async (outputDirectory, iden
|
|
|
530
541
|
],
|
|
531
542
|
[
|
|
532
543
|
'Cloudflare SSR',
|
|
533
|
-
ssrPaths
|
|
544
|
+
apiOnly ? [] : ssrPaths
|
|
534
545
|
],
|
|
535
546
|
[
|
|
536
547
|
'Cloudflare API/backend',
|
|
537
548
|
apiBackendPaths
|
|
538
549
|
]
|
|
539
|
-
])if (0 === paths.length) throw new Error(`[ultramodern-release-envelope] final Cloudflare staging has no ${surface} artifacts.`);
|
|
550
|
+
])if (0 === paths.length && (!apiOnly || 'Cloudflare API/backend' === surface)) throw new Error(`[ultramodern-release-envelope] final Cloudflare staging has no ${surface} artifacts.`);
|
|
540
551
|
for (const logicalPath of [
|
|
541
552
|
backendManifestPath,
|
|
542
553
|
backendContainerPath
|
|
543
554
|
])if (!files.includes(logicalPath)) throw new Error(`[ultramodern-release-envelope] final Cloudflare staging is missing ${logicalPath}.`);
|
|
544
|
-
const clientExecutionPaths = await manifestReferencedClientModules(outputDirectory, files, 'public/mf-manifest.json');
|
|
545
|
-
const ssrExecutionPaths = await routeReferencedSsrModules(outputDirectory, files, 'cloudflare', 'server/route.json');
|
|
546
|
-
if (0 === clientExecutionPaths.length || 0 === ssrExecutionPaths.length) throw new Error('[ultramodern-release-envelope] final Cloudflare manifests must reference emitted UI/client and SSR execution modules.');
|
|
555
|
+
const clientExecutionPaths = apiOnly ? [] : await manifestReferencedClientModules(outputDirectory, files, 'public/mf-manifest.json');
|
|
556
|
+
const ssrExecutionPaths = apiOnly ? [] : await routeReferencedSsrModules(outputDirectory, files, 'cloudflare', 'server/route.json');
|
|
557
|
+
if (!apiOnly && (0 === clientExecutionPaths.length || 0 === ssrExecutionPaths.length)) throw new Error('[ultramodern-release-envelope] final Cloudflare manifests must reference emitted UI/client and SSR execution modules.');
|
|
547
558
|
const runtimeByPath = new Map();
|
|
548
559
|
for (const logicalPath of files)runtimeByPath.set(logicalPath, 'cloudflare-deployment');
|
|
549
560
|
const add = (paths, runtime)=>{
|
|
550
561
|
for (const logicalPath of paths)runtimeByPath.set(logicalPath, runtime);
|
|
551
562
|
};
|
|
552
563
|
add(uiClientPaths, 'browser');
|
|
553
|
-
|
|
564
|
+
if (files.includes(CRAWLER_POLICY_PATH)) runtimeByPath.set(CRAWLER_POLICY_PATH, 'crawler-policy');
|
|
565
|
+
add(files.filter(isPublicMetadataPath), 'public-metadata');
|
|
566
|
+
add(apiOnly ? [] : ssrPaths, 'workerd');
|
|
554
567
|
add(apiBackendPaths, 'workerd-effect');
|
|
555
568
|
add([
|
|
556
569
|
backendManifestPath
|
|
@@ -564,7 +577,7 @@ const createCloudflareStagedReleaseArtifactInputs = async (outputDirectory, iden
|
|
|
564
577
|
backendManifestPath,
|
|
565
578
|
backendContainerPath
|
|
566
579
|
],
|
|
567
|
-
ssr: [
|
|
580
|
+
ssr: apiOnly ? [] : [
|
|
568
581
|
...new Set([
|
|
569
582
|
...ssrExecutionPaths,
|
|
570
583
|
...ssrPaths.filter((logicalPath)=>logicalPath.startsWith('worker/'))
|
|
@@ -583,7 +596,7 @@ const createCloudflareStagedReleaseArtifactInputs = async (outputDirectory, iden
|
|
|
583
596
|
uiClient: [
|
|
584
597
|
...uiClientPaths
|
|
585
598
|
].sort((left, right)=>left.localeCompare(right)),
|
|
586
|
-
ssr: [
|
|
599
|
+
ssr: apiOnly ? [] : [
|
|
587
600
|
...ssrPaths
|
|
588
601
|
].sort((left, right)=>left.localeCompare(right)),
|
|
589
602
|
apiBackend: [
|
|
@@ -599,7 +612,7 @@ const createCloudflareStagedReleaseArtifactInputs = async (outputDirectory, iden
|
|
|
599
612
|
const emitCloudflareStagedReleaseEnvelope = async ({ distDirectory, outputDirectory })=>{
|
|
600
613
|
const source = await verifyBuildOutputReleaseEnvelope(distDirectory, 'cloudflare');
|
|
601
614
|
if (!source) return;
|
|
602
|
-
const { artifacts, surfaces } = await createCloudflareStagedReleaseArtifactInputs(outputDirectory, source.identity);
|
|
615
|
+
const { artifacts, surfaces } = await createCloudflareStagedReleaseArtifactInputs(outputDirectory, source.identity, 0 === source.surfaces.uiClient.length);
|
|
603
616
|
const staged = await createMicroVerticalReleaseEnvelope({
|
|
604
617
|
artifactRoot: outputDirectory,
|
|
605
618
|
target: 'cloudflare',
|
|
@@ -184,7 +184,7 @@ const assertUniqueSortedArtifacts = (artifacts, location)=>{
|
|
|
184
184
|
if (paths.some((logicalPath, index)=>logicalPath !== sorted[index])) throw new Error(`${location} must be sorted by logicalPath.`);
|
|
185
185
|
};
|
|
186
186
|
const assertSurfacePaths = (value, location)=>{
|
|
187
|
-
if (!Array.isArray(value)
|
|
187
|
+
if (!Array.isArray(value)) throw new Error(`${location} must be an array of artifact paths.`);
|
|
188
188
|
const paths = value.map((item, index)=>assertNormalizedLogicalPath(item, `${location}[${index}]`));
|
|
189
189
|
const sorted = [
|
|
190
190
|
...paths
|
|
@@ -206,10 +206,16 @@ const assertSurfaces = (value)=>{
|
|
|
206
206
|
'manifest',
|
|
207
207
|
'container'
|
|
208
208
|
], 'surfaces.backendFederation');
|
|
209
|
+
const uiClient = assertSurfacePaths(surfaces.uiClient, 'surfaces.uiClient');
|
|
210
|
+
const ssr = assertSurfacePaths(surfaces.ssr, 'surfaces.ssr');
|
|
211
|
+
const apiBackend = assertSurfacePaths(surfaces.apiBackend, 'surfaces.apiBackend');
|
|
212
|
+
if (uiClient.length > 0 && 0 === ssr.length) throw new Error('surfaces.ssr must contain at least one artifact path when UI/client is declared.');
|
|
213
|
+
if (ssr.length > 0 && 0 === uiClient.length) throw new Error('surfaces.uiClient must contain at least one artifact path when SSR is declared.');
|
|
214
|
+
if (0 === apiBackend.length) throw new Error('surfaces.apiBackend must contain at least one artifact path.');
|
|
209
215
|
return {
|
|
210
|
-
uiClient
|
|
211
|
-
ssr
|
|
212
|
-
apiBackend
|
|
216
|
+
uiClient,
|
|
217
|
+
ssr,
|
|
218
|
+
apiBackend,
|
|
213
219
|
backendFederation: {
|
|
214
220
|
manifest: assertNormalizedLogicalPath(backendFederation.manifest, 'surfaces.backendFederation.manifest'),
|
|
215
221
|
container: assertNormalizedLogicalPath(backendFederation.container, 'surfaces.backendFederation.container')
|
|
@@ -171,8 +171,13 @@ const createBackendManifest = (workspaceRoot, distDirectory, app, entryIntegrity
|
|
|
171
171
|
source: {
|
|
172
172
|
module: sourceModule
|
|
173
173
|
},
|
|
174
|
-
|
|
175
|
-
|
|
174
|
+
...'rpc' === app.apiProtocol ? {
|
|
175
|
+
rpcPath: app.rpcPath,
|
|
176
|
+
rpcSerialization: 'json'
|
|
177
|
+
} : {
|
|
178
|
+
readinessPath: `${app.apiPrefix}/${app.apiStem}/readiness`,
|
|
179
|
+
openapiPath: `${app.apiPrefix}/openapi.json`
|
|
180
|
+
},
|
|
176
181
|
...app.deliveryUnit ? {
|
|
177
182
|
deliveryUnit: app.deliveryUnit
|
|
178
183
|
} : {},
|
|
@@ -4,7 +4,8 @@ import promises from "node:fs/promises";
|
|
|
4
4
|
import node_path from "node:path";
|
|
5
5
|
import { resolveUltramodernSourceRevision } from "@modern-js/app-tools-extensions/release-identity";
|
|
6
6
|
import { BACKEND_FEDERATION_MANIFEST_FILE, BACKEND_FEDERATION_REMOTE_ENTRY_FILE, DELIVERY_UNIT_DEPLOY_PROFILE, DELIVERY_UNIT_KIND, DELIVERY_UNIT_SCHEMA_VERSION, ULTRAMODERN_BUILD_ARTIFACT_PATH, deliveryUnitContractBlock, isUltramodernBuildArtifact, validateDeliveryUnitRecord, validateUltramodernBuildArtifact } from "@modern-js/backend-federation-contracts";
|
|
7
|
-
const
|
|
7
|
+
const REFERENCE_TOPOLOGY_PATH = 'topology/reference-topology.json';
|
|
8
|
+
const DEVELOPMENT_OVERLAY_PATH = 'topology/local-overlays/development.json';
|
|
8
9
|
const normalizeRelativePath = (value)=>value.replace(/\\/gu, '/').replace(/^\.\/+/u, '');
|
|
9
10
|
const isRecord = (value)=>'object' == typeof value && null !== value;
|
|
10
11
|
const readJsonFile = async (filePath)=>JSON.parse(await promises.readFile(filePath, 'utf8'));
|
|
@@ -32,15 +33,25 @@ const readBuildIdentity = async (appDirectory)=>{
|
|
|
32
33
|
const toPascalCase = (value)=>value.split(/[^a-zA-Z0-9]+/u).filter(Boolean).map((part)=>part.charAt(0).toUpperCase() + part.slice(1)).join('');
|
|
33
34
|
const stringValue = (value)=>'string' == typeof value && value.length > 0 ? value : void 0;
|
|
34
35
|
const createBackendName = (app, id)=>{
|
|
35
|
-
const configuredName = stringValue(app.backendFederation?.name) ?? stringValue(app.backendFederation?.executionSurfaces?.node?.remoteName)
|
|
36
|
+
const configuredName = stringValue(app.backendFederation?.name) ?? stringValue(app.backendFederation?.executionSurfaces?.node?.remoteName);
|
|
36
37
|
if (configuredName) return configuredName;
|
|
37
38
|
const mfName = stringValue(app.moduleFederation?.name);
|
|
38
39
|
return mfName ? `${mfName}Backend` : `vertical${toPascalCase(id)}Backend`;
|
|
39
40
|
};
|
|
41
|
+
const rebaseDefaultLocalUrl = (value, defaultPort, publicOrigin)=>{
|
|
42
|
+
let url;
|
|
43
|
+
try {
|
|
44
|
+
url = new URL(value);
|
|
45
|
+
} catch {
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
if ('http:' !== url.protocol || 'localhost' !== url.hostname || Number(url.port) !== defaultPort || url.username || url.password) return value;
|
|
49
|
+
return `${publicOrigin}${url.pathname}${url.search}${url.hash}`;
|
|
50
|
+
};
|
|
40
51
|
const findWorkspaceRoot = (appDirectory)=>{
|
|
41
52
|
let current = appDirectory;
|
|
42
53
|
while(true){
|
|
43
|
-
if (existsSync(node_path.join(current,
|
|
54
|
+
if (existsSync(node_path.join(current, REFERENCE_TOPOLOGY_PATH))) return current;
|
|
44
55
|
const parent = node_path.dirname(current);
|
|
45
56
|
if (parent === current) return;
|
|
46
57
|
current = parent;
|
|
@@ -64,54 +75,90 @@ const createStampedDeliveryUnit = (input)=>{
|
|
|
64
75
|
if (!validation.ok) return;
|
|
65
76
|
return deliveryUnitContractBlock(record);
|
|
66
77
|
};
|
|
67
|
-
const
|
|
68
|
-
const id = stringValue(
|
|
69
|
-
const appPath = stringValue(
|
|
70
|
-
if (!id || 'vertical' !==
|
|
71
|
-
const directory =
|
|
78
|
+
const createAppFromTopology = (workspaceRoot, appDirectory, topologyApp, overlay, packageManifest)=>{
|
|
79
|
+
const id = stringValue(topologyApp.id);
|
|
80
|
+
const appPath = stringValue(topologyApp.path);
|
|
81
|
+
if (!id || !appPath || 'vertical' !== topologyApp.kind || !isRecord(topologyApp.api) || !isRecord(topologyApp.backendFederation)) return;
|
|
82
|
+
const directory = normalizeRelativePath(appPath);
|
|
72
83
|
if (node_path.resolve(workspaceRoot, directory) !== node_path.resolve(appDirectory)) return;
|
|
73
|
-
const
|
|
74
|
-
if (
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
const
|
|
80
|
-
|
|
84
|
+
const defaultPort = overlay.ports?.[id];
|
|
85
|
+
if ('number' != typeof defaultPort || !Number.isInteger(defaultPort) || defaultPort < 1 || defaultPort > 65535) throw new Error(`[backend-federation-build] Invalid development port for ${id}.`);
|
|
86
|
+
const portEnv = stringValue(topologyApp.portEnv) ?? `VERTICAL_${(stringValue(topologyApp.domain) ?? id).replace(/[^a-zA-Z0-9]/gu, '_').toUpperCase()}_PORT`;
|
|
87
|
+
const port = Number(process.env[portEnv] ?? defaultPort);
|
|
88
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error(`[backend-federation-build] Invalid ${portEnv} development port for ${id}.`);
|
|
89
|
+
const publicUrlEnv = stringValue(topologyApp.cloudflare?.publicUrlEnv);
|
|
90
|
+
const configuredPublicUrl = publicUrlEnv ? process.env[publicUrlEnv]?.trim() : void 0;
|
|
91
|
+
let publicOrigin = `http://localhost:${port}`;
|
|
92
|
+
if (configuredPublicUrl) {
|
|
93
|
+
let url;
|
|
94
|
+
try {
|
|
95
|
+
url = new URL(configuredPublicUrl);
|
|
96
|
+
} catch {
|
|
97
|
+
throw new Error(`[backend-federation-build] Invalid ${publicUrlEnv} public URL for ${id}.`);
|
|
98
|
+
}
|
|
99
|
+
if (![
|
|
100
|
+
'http:',
|
|
101
|
+
'https:'
|
|
102
|
+
].includes(url.protocol) || url.username || url.password) throw new Error(`[backend-federation-build] Invalid ${publicUrlEnv} public URL for ${id}.`);
|
|
103
|
+
publicOrigin = url.origin;
|
|
104
|
+
}
|
|
105
|
+
const apiPrefix = stringValue(topologyApp.api.bff?.prefix) ?? `/${id}-api`;
|
|
106
|
+
const apiProtocol = topologyApp.api.protocol ?? 'rest';
|
|
107
|
+
if ('rest' !== apiProtocol && 'rpc' !== apiProtocol) throw new Error(`[backend-federation-build] Unsupported API protocol for ${id}.`);
|
|
108
|
+
const rpcPath = 'rpc' === apiProtocol ? stringValue(topologyApp.api.rpcPath) : void 0;
|
|
109
|
+
if ('rpc' === apiProtocol && (!rpcPath || 'json' !== topologyApp.api.rpcSerialization)) throw new Error(`[backend-federation-build] Missing JSON RPC route metadata for ${id}.`);
|
|
110
|
+
const apiStem = stringValue(topologyApp.api.stem) ?? id;
|
|
111
|
+
const backendName = createBackendName(topologyApp, id);
|
|
112
|
+
const nodeOverlay = overlay.serverExecution?.[id]?.node;
|
|
113
|
+
if (!isRecord(nodeOverlay)) throw new Error(`[backend-federation-build] Missing declared Node server execution for ${id}.`);
|
|
114
|
+
const manifestUrl = rebaseDefaultLocalUrl(stringValue(nodeOverlay?.manifestUrl) ?? `http://localhost:${defaultPort}/${BACKEND_FEDERATION_MANIFEST_FILE}`, defaultPort, publicOrigin);
|
|
115
|
+
const containerEntry = rebaseDefaultLocalUrl(stringValue(nodeOverlay?.containerEntry) ?? `http://localhost:${defaultPort}/${BACKEND_FEDERATION_REMOTE_ENTRY_FILE}`, defaultPort, publicOrigin);
|
|
116
|
+
const configuredRemoteType = stringValue(nodeOverlay?.remoteType) ?? stringValue(topologyApp.backendFederation?.executionSurfaces?.node?.remoteType);
|
|
81
117
|
if (void 0 !== configuredRemoteType && 'commonjs-module' !== configuredRemoteType) throw new Error(`[backend-federation-build] Node backend federation remoteType must be "commonjs-module"; received "${configuredRemoteType}" for ${id}.`);
|
|
82
118
|
if (!new URL(containerEntry).pathname.endsWith(`/${BACKEND_FEDERATION_REMOTE_ENTRY_FILE}`)) throw new Error(`[backend-federation-build] Node backend federation containerEntry must end with "/${BACKEND_FEDERATION_REMOTE_ENTRY_FILE}"; received "${containerEntry}" for ${id}.`);
|
|
83
119
|
const remoteType = 'commonjs-module';
|
|
84
|
-
const packageName = stringValue(
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
120
|
+
const packageName = stringValue(packageManifest.name);
|
|
121
|
+
const version = stringValue(packageManifest.version);
|
|
122
|
+
if (stringValue(topologyApp.package) !== packageName || !version) throw new Error(`[backend-federation-build] Topology package identity must match package.json for ${id}.`);
|
|
123
|
+
const declaredUiManifestUrl = stringValue(overlay.manifests?.[id]);
|
|
124
|
+
const uiManifestUrl = declaredUiManifestUrl ? rebaseDefaultLocalUrl(declaredUiManifestUrl, defaultPort, publicOrigin) : void 0;
|
|
125
|
+
const topologyDeliveryUnit = isRecord(topologyApp.deliveryUnit) ? {
|
|
126
|
+
unitId: stringValue(topologyApp.deliveryUnit.unitId),
|
|
127
|
+
buildMarker: stringValue(topologyApp.deliveryUnit.buildMarker),
|
|
128
|
+
sourceRevision: stringValue(topologyApp.deliveryUnit.sourceRevision),
|
|
129
|
+
packageName: stringValue(topologyApp.deliveryUnit.packageName),
|
|
130
|
+
version: stringValue(topologyApp.deliveryUnit.version)
|
|
92
131
|
} : void 0;
|
|
93
132
|
return {
|
|
94
133
|
id,
|
|
95
134
|
directory,
|
|
96
135
|
packageName,
|
|
136
|
+
version,
|
|
97
137
|
port,
|
|
98
138
|
apiPrefix,
|
|
139
|
+
apiProtocol,
|
|
140
|
+
rpcPath,
|
|
99
141
|
apiStem,
|
|
100
142
|
backendName,
|
|
101
143
|
manifestUrl,
|
|
102
144
|
containerEntry,
|
|
103
145
|
remoteType,
|
|
104
146
|
uiManifestUrl,
|
|
105
|
-
|
|
147
|
+
topologyDeliveryUnit
|
|
106
148
|
};
|
|
107
149
|
};
|
|
108
150
|
const findBackendFederationApp = async (workspaceRoot, appDirectory)=>{
|
|
109
|
-
const
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
151
|
+
const topologyPath = node_path.join(workspaceRoot, REFERENCE_TOPOLOGY_PATH);
|
|
152
|
+
const topology = await readJsonFile(topologyPath);
|
|
153
|
+
if (!isRecord(topology) || !Array.isArray(topology.verticals)) throw new Error(`[backend-federation-build] Invalid declared topology at ${topologyPath}.`);
|
|
154
|
+
const overlayPath = node_path.join(workspaceRoot, DEVELOPMENT_OVERLAY_PATH);
|
|
155
|
+
const overlay = await readJsonFile(overlayPath);
|
|
156
|
+
if (!isRecord(overlay) || !isRecord(overlay.ports)) throw new Error(`[backend-federation-build] Invalid development overlay at ${overlayPath}.`);
|
|
157
|
+
const packageManifest = await readJsonFile(node_path.join(appDirectory, 'package.json'));
|
|
158
|
+
for (const app of topology.verticals){
|
|
159
|
+
const resolved = createAppFromTopology(workspaceRoot, appDirectory, app, overlay, packageManifest);
|
|
114
160
|
if (resolved) return resolved;
|
|
115
161
|
}
|
|
162
|
+
throw new Error(`[backend-federation-build] ${appDirectory} is missing from the declared reference topology.`);
|
|
116
163
|
};
|
|
117
|
-
export {
|
|
164
|
+
export { DEVELOPMENT_OVERLAY_PATH, REFERENCE_TOPOLOGY_PATH, buildArtifactPathFor, createAppFromTopology, createStampedDeliveryUnit, findBackendFederationApp, findWorkspaceRoot, normalizeRelativePath, readBuildIdentity, resolveWorkspaceSourceRevision };
|
|
@@ -6,7 +6,7 @@ import { resolveUltramodernReleaseIdentity } from "@modern-js/app-tools-extensio
|
|
|
6
6
|
import { BACKEND_FEDERATION_MANIFEST_FILE, BACKEND_FEDERATION_REMOTE_ENTRY_FILE, ULTRAMODERN_BUILD_ARTIFACT_FILE, stampUltramodernBuildArtifactIdentity } from "@modern-js/backend-federation-contracts";
|
|
7
7
|
import { createBackendFederationEntryIntegrity } from "@modern-js/server-runtime-extensions/backend-federation-security/node";
|
|
8
8
|
import { createBackendManifest, createBackendRemoteEntrySource } from "./codegen.mjs";
|
|
9
|
-
import {
|
|
9
|
+
import { REFERENCE_TOPOLOGY_PATH, buildArtifactPathFor, createStampedDeliveryUnit, findBackendFederationApp, findWorkspaceRoot, readBuildIdentity, resolveWorkspaceSourceRevision } from "./config.mjs";
|
|
10
10
|
const emitBackendFederationArtifacts = async (appDirectory, distDirectory)=>{
|
|
11
11
|
const workspaceRoot = findWorkspaceRoot(appDirectory);
|
|
12
12
|
if (!workspaceRoot) return;
|
|
@@ -16,35 +16,39 @@ const emitBackendFederationArtifacts = async (appDirectory, distDirectory)=>{
|
|
|
16
16
|
const app = await findBackendFederationApp(workspaceRoot, appDirectory);
|
|
17
17
|
if (!app) return;
|
|
18
18
|
const buildIdentity = await readBuildIdentity(appDirectory);
|
|
19
|
-
const
|
|
20
|
-
const
|
|
19
|
+
const topologyDeliveryUnit = app.topologyDeliveryUnit;
|
|
20
|
+
const hasTopologyDeliveryUnit = void 0 !== topologyDeliveryUnit && (void 0 !== topologyDeliveryUnit.unitId || void 0 !== topologyDeliveryUnit.buildMarker || void 0 !== topologyDeliveryUnit.packageName || void 0 !== topologyDeliveryUnit.version);
|
|
21
21
|
const hasBuildIdentity = void 0 !== buildIdentity.unitId || void 0 !== buildIdentity.buildVersion || void 0 !== buildIdentity.packageName || void 0 !== buildIdentity.version;
|
|
22
22
|
if (hasBuildIdentity) {
|
|
23
|
-
const
|
|
23
|
+
const topologyPath = node_path.join(workspaceRoot, REFERENCE_TOPOLOGY_PATH);
|
|
24
24
|
const buildIdentityPath = buildArtifactPathFor(appDirectory);
|
|
25
25
|
const mismatches = [];
|
|
26
26
|
const compare = (label, a, b, leftLabel = 'deliveryUnit')=>{
|
|
27
27
|
if (void 0 !== a && void 0 !== b && a !== b) mismatches.push(`${label}: ${leftLabel}=${a} vs ultramodern-build=${b}`);
|
|
28
28
|
};
|
|
29
29
|
compare('appId', app.id, buildIdentity.appId, 'topology');
|
|
30
|
-
if (
|
|
31
|
-
compare('unitId',
|
|
32
|
-
compare('buildMarker/build',
|
|
33
|
-
compare('packageName',
|
|
34
|
-
compare('version',
|
|
30
|
+
if (hasTopologyDeliveryUnit) {
|
|
31
|
+
compare('unitId', topologyDeliveryUnit?.unitId, buildIdentity.unitId);
|
|
32
|
+
compare('buildMarker/build', topologyDeliveryUnit?.buildMarker, buildIdentity.buildVersion);
|
|
33
|
+
compare('packageName', topologyDeliveryUnit?.packageName, buildIdentity.packageName);
|
|
34
|
+
compare('version', topologyDeliveryUnit?.version, buildIdentity.version);
|
|
35
35
|
}
|
|
36
|
-
|
|
36
|
+
compare('packageName', app.packageName, buildIdentity.packageName, 'package.json');
|
|
37
|
+
compare('packageName', topologyDeliveryUnit?.packageName, app.packageName, 'topology');
|
|
38
|
+
compare('version', app.version, buildIdentity.version, 'package.json');
|
|
39
|
+
compare('version', topologyDeliveryUnit?.version, app.version, 'topology');
|
|
40
|
+
if (mismatches.length > 0) throw new Error(`[backend-federation-build] Delivery-unit identity drift between ${topologyPath}, package.json and ${buildIdentityPath}: ${mismatches.join('; ')}`);
|
|
37
41
|
}
|
|
38
|
-
const unitId =
|
|
39
|
-
const generationBuildMarker =
|
|
42
|
+
const unitId = topologyDeliveryUnit?.unitId ?? buildIdentity.unitId;
|
|
43
|
+
const generationBuildMarker = topologyDeliveryUnit?.buildMarker ?? buildIdentity.buildVersion;
|
|
40
44
|
const sourceRevision = await resolveWorkspaceSourceRevision(workspaceRoot);
|
|
41
45
|
const buildVersion = generationBuildMarker && unitId ? resolveUltramodernReleaseIdentity({
|
|
42
46
|
generationBuildMarker,
|
|
43
47
|
unitId,
|
|
44
48
|
workspaceRoot
|
|
45
49
|
}).buildMarker : void 0;
|
|
46
|
-
const packageName =
|
|
47
|
-
const version =
|
|
50
|
+
const packageName = app.packageName;
|
|
51
|
+
const version = app.version;
|
|
48
52
|
const deliveryUnit = createStampedDeliveryUnit({
|
|
49
53
|
appId: app.id,
|
|
50
54
|
unitId,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
import { resolveBrowserManifestAddress } from "@modern-js/surface-resolution";
|
|
3
|
+
import { getBuildConfigEnvironment } from "./build-environment.mjs";
|
|
4
|
+
function createRemoteManifestUrl(options) {
|
|
5
|
+
const env = Object.fromEntries([
|
|
6
|
+
options.manifestEnv,
|
|
7
|
+
options.publicUrlEnv,
|
|
8
|
+
'MODERNJS_DEPLOY',
|
|
9
|
+
'ULTRAMODERN_CLOUDFLARE_WORKERS_DEV_SUBDOMAIN',
|
|
10
|
+
'ULTRAMODERN_CLOUDFLARE_REQUIRE_PUBLIC_URLS'
|
|
11
|
+
].map((name)=>[
|
|
12
|
+
name,
|
|
13
|
+
getBuildConfigEnvironment(name)
|
|
14
|
+
]));
|
|
15
|
+
const address = resolveBrowserManifestAddress(env, options, getBuildConfigEnvironment('NODE_ENV') ?? 'production');
|
|
16
|
+
if (!address.ok) throw new Error(`Remote ${options.mfName}: ${address.reason}`);
|
|
17
|
+
return env[options.manifestEnv]?.trim() || `${options.mfName}@${address.manifestUrl}`;
|
|
18
|
+
}
|
|
19
|
+
export { createRemoteManifestUrl };
|