@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
package/dist/cjs/config.js
CHANGED
|
@@ -28,15 +28,19 @@ var __webpack_require__ = {};
|
|
|
28
28
|
var __webpack_exports__ = {};
|
|
29
29
|
__webpack_require__.r(__webpack_exports__);
|
|
30
30
|
__webpack_require__.d(__webpack_exports__, {
|
|
31
|
+
createRemoteManifestUrl: ()=>remote_address_js_namespaceObject.createRemoteManifestUrl,
|
|
31
32
|
getBuildConfigEnvironment: ()=>public_js_namespaceObject.getBuildConfigEnvironment,
|
|
32
33
|
resolveEffectTsgoCompiler: ()=>public_js_namespaceObject.resolveEffectTsgoCompiler,
|
|
33
34
|
withBuildConfigEnvironment: ()=>public_js_namespaceObject.withBuildConfigEnvironment
|
|
34
35
|
});
|
|
35
36
|
const public_js_namespaceObject = require("./build-config/public.js");
|
|
37
|
+
const remote_address_js_namespaceObject = require("./build-config/remote-address.js");
|
|
38
|
+
exports.createRemoteManifestUrl = __webpack_exports__.createRemoteManifestUrl;
|
|
36
39
|
exports.getBuildConfigEnvironment = __webpack_exports__.getBuildConfigEnvironment;
|
|
37
40
|
exports.resolveEffectTsgoCompiler = __webpack_exports__.resolveEffectTsgoCompiler;
|
|
38
41
|
exports.withBuildConfigEnvironment = __webpack_exports__.withBuildConfigEnvironment;
|
|
39
42
|
for(var __rspack_i in __webpack_exports__)if (-1 === [
|
|
43
|
+
"createRemoteManifestUrl",
|
|
40
44
|
"getBuildConfigEnvironment",
|
|
41
45
|
"resolveEffectTsgoCompiler",
|
|
42
46
|
"withBuildConfigEnvironment"
|
|
@@ -48,6 +48,10 @@ const dependencyKeys = [
|
|
|
48
48
|
'dependencies',
|
|
49
49
|
'optionalDependencies'
|
|
50
50
|
];
|
|
51
|
+
const rootDependencyKeys = [
|
|
52
|
+
...dependencyKeys,
|
|
53
|
+
'devDependencies'
|
|
54
|
+
];
|
|
51
55
|
const exactVersionPattern = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u;
|
|
52
56
|
async function readPackageJson(filePath) {
|
|
53
57
|
return JSON.parse(await promises_default().readFile(filePath, 'utf8'));
|
|
@@ -63,14 +67,34 @@ function parseNpmAlias(aliasName, specifier) {
|
|
|
63
67
|
targetVersion: target.slice(versionSeparator + 1)
|
|
64
68
|
};
|
|
65
69
|
}
|
|
66
|
-
function aliasesFromPackageJson(packageJson) {
|
|
70
|
+
function aliasesFromPackageJson(packageJson, keys = dependencyKeys) {
|
|
67
71
|
const aliases = [];
|
|
68
|
-
for (const dependencyKey of
|
|
72
|
+
for (const dependencyKey of keys)for (const [aliasName, specifier] of Object.entries(packageJson[dependencyKey] ?? {})){
|
|
69
73
|
const alias = parseNpmAlias(aliasName, specifier);
|
|
70
74
|
if (alias) aliases.push(alias);
|
|
71
75
|
}
|
|
72
76
|
return aliases;
|
|
73
77
|
}
|
|
78
|
+
async function catalogAliasesFromInstalledPackages(appDirectory, packageJson) {
|
|
79
|
+
const aliases = [];
|
|
80
|
+
for (const dependencyKey of rootDependencyKeys)for (const [aliasName, specifier] of Object.entries(packageJson[dependencyKey] ?? {})){
|
|
81
|
+
if (!specifier.startsWith('catalog:')) continue;
|
|
82
|
+
let installedPackage;
|
|
83
|
+
try {
|
|
84
|
+
installedPackage = await readPackageJson(external_node_path_default().join(appDirectory, 'node_modules', aliasName, 'package.json'));
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if ('ENOENT' === error.code) continue;
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
if (!installedPackage.name || !installedPackage.version) throw new Error(`Installed catalog package ${aliasName} has no identity`);
|
|
90
|
+
if (installedPackage.name !== aliasName) aliases.push({
|
|
91
|
+
aliasName,
|
|
92
|
+
targetName: installedPackage.name,
|
|
93
|
+
targetVersion: installedPackage.version
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return aliases;
|
|
97
|
+
}
|
|
74
98
|
async function collectPackageRecords(nodeModulesDirectory) {
|
|
75
99
|
const records = [];
|
|
76
100
|
const visitedNodeModules = new Set();
|
|
@@ -192,7 +216,8 @@ async function preserveNpmAliases({ appDirectory, outputDirectory, implicitAlias
|
|
|
192
216
|
const records = await collectPackageRecords(external_node_path_default().join(outputDirectory, 'node_modules'));
|
|
193
217
|
const appPackageJson = await readPackageJson(external_node_path_default().join(appDirectory, 'package.json'));
|
|
194
218
|
const rootAliases = [
|
|
195
|
-
...aliasesFromPackageJson(appPackageJson),
|
|
219
|
+
...aliasesFromPackageJson(appPackageJson, rootDependencyKeys),
|
|
220
|
+
...await catalogAliasesFromInstalledPackages(appDirectory, appPackageJson),
|
|
196
221
|
...implicitAliases
|
|
197
222
|
].filter((alias)=>alias.aliasName !== alias.targetName);
|
|
198
223
|
const emittedRootAliases = [];
|
|
@@ -61,6 +61,11 @@ const MICROVERTICAL_RELEASE_IDENTITY_CARRIERS_KIND = 'ultramodern-release-identi
|
|
|
61
61
|
const MICROVERTICAL_RELEASE_IDENTITY_CARRIERS_SCHEMA_VERSION = 1;
|
|
62
62
|
const COMPILED_MODULE_PATTERN = /\.(?:c|m)?js$/u;
|
|
63
63
|
const EFFECT_BFF_WORKER_PATTERN = /^worker\/__modern_bff_effect\.(?:c|m)?js$/u;
|
|
64
|
+
const EFFECT_BFF_WORKER_SUPPORT_PATTERN = /^worker\/__modern_worker_(?:runtime|shared)_*\.(?:c|m)?js$/u;
|
|
65
|
+
const isEffectBffWorkerArtifact = (logicalPath)=>EFFECT_BFF_WORKER_PATTERN.test(logicalPath) || EFFECT_BFF_WORKER_SUPPORT_PATTERN.test(logicalPath);
|
|
66
|
+
const CRAWLER_POLICY_PATH = 'public/robots.txt';
|
|
67
|
+
const isPublicMetadataPath = (logicalPath)=>'public/_headers' === logicalPath || /^public\/\.well-known\/[^/]+\.json$/u.test(logicalPath);
|
|
68
|
+
const PUBLIC_BUILD_ARTIFACT_PATH = `public/${backend_federation_contracts_namespaceObject.ULTRAMODERN_BUILD_ARTIFACT_FILE}`;
|
|
64
69
|
const isRecord = (value)=>'object' == typeof value && null !== value && !Array.isArray(value);
|
|
65
70
|
const flattenStrings = (value)=>'string' == typeof value ? [
|
|
66
71
|
value
|
|
@@ -181,7 +186,7 @@ const readBuildArtifact = async (distDirectory, requirePromotable = true)=>{
|
|
|
181
186
|
if (!(0, backend_federation_contracts_namespaceObject.isUltramodernBuildArtifact)(artifact)) throw new Error(`[ultramodern-release-envelope] ${backend_federation_contracts_namespaceObject.ULTRAMODERN_BUILD_ARTIFACT_FILE} is missing or invalid.`);
|
|
182
187
|
if ('workspace' === artifact.deliveryUnit.sourceRevision) {
|
|
183
188
|
if (!requirePromotable) return;
|
|
184
|
-
throw new Error('[ultramodern-release-envelope] sourceRevision "workspace" cannot produce a promotable
|
|
189
|
+
throw new Error('[ultramodern-release-envelope] sourceRevision "workspace" cannot produce a promotable envelope.');
|
|
185
190
|
}
|
|
186
191
|
return artifact;
|
|
187
192
|
};
|
|
@@ -251,7 +256,7 @@ const releaseArtifact = (logicalPath, runtime)=>({
|
|
|
251
256
|
const writeReleaseIdentityCarrierMetadata = async (artifactRoot, identity, carrierPaths)=>{
|
|
252
257
|
const surfacesByPath = new Map();
|
|
253
258
|
for (const [surface, logicalPaths] of Object.entries(carrierPaths)){
|
|
254
|
-
if (0 === logicalPaths.length) throw new Error(`[ultramodern-release-envelope] ${surface} has no declared release-identity carrier artifact.`);
|
|
259
|
+
if (0 === logicalPaths.length && 'ssr' !== surface && 'uiClient' !== surface) throw new Error(`[ultramodern-release-envelope] ${surface} has no declared release-identity carrier artifact.`);
|
|
255
260
|
for (const logicalPath of logicalPaths){
|
|
256
261
|
const surfaces = surfacesByPath.get(logicalPath) ?? new Set();
|
|
257
262
|
surfaces.add(surface);
|
|
@@ -291,12 +296,12 @@ const writeReleaseIdentityCarrierMetadata = async (artifactRoot, identity, carri
|
|
|
291
296
|
}, null, 2)}\n`);
|
|
292
297
|
return releaseArtifact(MICROVERTICAL_RELEASE_IDENTITY_CARRIERS_PATH, 'release-identity-metadata');
|
|
293
298
|
};
|
|
294
|
-
const createReleaseArtifactInputs = async (distDirectory, identity, target)=>{
|
|
299
|
+
const createReleaseArtifactInputs = async (distDirectory, identity, target, apiOnly)=>{
|
|
295
300
|
const files = await collectFiles(distDirectory);
|
|
296
|
-
if ('node' === target && files.some(
|
|
301
|
+
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.');
|
|
297
302
|
const uiClientPaths = files.filter((logicalPath)=>{
|
|
298
303
|
const topLevel = logicalPath.split('/')[0];
|
|
299
|
-
return 'static' === topLevel || 'html' === topLevel || 'public' === topLevel || [
|
|
304
|
+
return 'static' === topLevel || 'html' === topLevel || 'public' === topLevel && logicalPath !== CRAWLER_POLICY_PATH && !isPublicMetadataPath(logicalPath) || [
|
|
300
305
|
'index.html',
|
|
301
306
|
'mf-manifest.json',
|
|
302
307
|
'mf-stats.json',
|
|
@@ -305,16 +310,17 @@ const createReleaseArtifactInputs = async (distDirectory, identity, target)=>{
|
|
|
305
310
|
'loadable-stats.json'
|
|
306
311
|
].includes(logicalPath);
|
|
307
312
|
});
|
|
308
|
-
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) && !
|
|
313
|
+
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));
|
|
309
314
|
const nodeApiEntryPaths = files.filter((logicalPath)=>logicalPath.startsWith('api/') && COMPILED_MODULE_PATTERN.test(logicalPath));
|
|
310
|
-
const apiBackendPaths = 'node' === target ? files.filter((logicalPath)=>(logicalPath.startsWith('api/') || logicalPath.startsWith('shared/')) && COMPILED_MODULE_PATTERN.test(logicalPath)) : files.filter(
|
|
311
|
-
if (
|
|
312
|
-
if (0 ===
|
|
313
|
-
if (
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
315
|
+
const apiBackendPaths = 'node' === target ? files.filter((logicalPath)=>(logicalPath.startsWith('api/') || logicalPath.startsWith('shared/')) && COMPILED_MODULE_PATTERN.test(logicalPath)) : files.filter(isEffectBffWorkerArtifact);
|
|
316
|
+
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.');
|
|
317
|
+
if (!apiOnly && 0 === uiClientPaths.length) throw new Error(`[ultramodern-release-envelope] ${target} full-stack MicroVertical has no UI/client artifacts.`);
|
|
318
|
+
if (!apiOnly && 0 === ssrPaths.length) throw new Error(`[ultramodern-release-envelope] ${target} full-stack MicroVertical has no ${'node' === target ? 'Node' : 'workerd'} SSR artifacts.`);
|
|
319
|
+
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'}.`);
|
|
320
|
+
const clientExecutionPaths = apiOnly ? [] : await manifestReferencedClientModules(distDirectory, files);
|
|
321
|
+
if (!apiOnly && 0 === clientExecutionPaths.length) throw new Error('[ultramodern-release-envelope] UI/client manifest references no compiled execution module.');
|
|
322
|
+
const ssrExecutionPaths = apiOnly ? [] : await routeReferencedSsrModules(distDirectory, files, target);
|
|
323
|
+
if (!apiOnly && 0 === ssrExecutionPaths.length) throw new Error(`[ultramodern-release-envelope] route manifest references no emitted ${'node' === target ? 'Node' : 'Cloudflare'} SSR execution module.`);
|
|
318
324
|
const runtimeByPath = new Map();
|
|
319
325
|
const add = (paths, runtime)=>{
|
|
320
326
|
for (const logicalPath of paths){
|
|
@@ -324,6 +330,8 @@ const createReleaseArtifactInputs = async (distDirectory, identity, target)=>{
|
|
|
324
330
|
}
|
|
325
331
|
};
|
|
326
332
|
add(uiClientPaths, 'browser');
|
|
333
|
+
add(files.filter((logicalPath)=>logicalPath === CRAWLER_POLICY_PATH), 'crawler-policy');
|
|
334
|
+
add(files.filter(isPublicMetadataPath), 'public-metadata');
|
|
327
335
|
add(ssrPaths, 'node' === target ? 'nodejs' : 'workerd');
|
|
328
336
|
add(apiBackendPaths, 'node' === target ? 'nodejs' : 'workerd-effect');
|
|
329
337
|
add([
|
|
@@ -381,15 +389,14 @@ const pathExists = async (filePath)=>{
|
|
|
381
389
|
}
|
|
382
390
|
};
|
|
383
391
|
const emitFrameworkMicroVerticalReleaseEnvelope = async ({ apiOnly, distDirectory, requirePromotable = true, target })=>{
|
|
384
|
-
if (apiOnly) return;
|
|
385
392
|
const backendManifestPath = external_node_path_default().join(distDirectory, backend_federation_contracts_namespaceObject.BACKEND_FEDERATION_MANIFEST_FILE);
|
|
386
393
|
const backendContainerPath = external_node_path_default().join(distDirectory, backend_federation_contracts_namespaceObject.BACKEND_FEDERATION_REMOTE_ENTRY_FILE);
|
|
387
394
|
const [hasBackendManifest, hasBackendContainer] = await Promise.all([
|
|
388
395
|
pathExists(backendManifestPath),
|
|
389
396
|
pathExists(backendContainerPath)
|
|
390
397
|
]);
|
|
391
|
-
if (!hasBackendManifest && !hasBackendContainer) return;
|
|
392
|
-
if (!hasBackendManifest || !hasBackendContainer) throw new Error('[ultramodern-release-envelope]
|
|
398
|
+
if (!hasBackendManifest && !hasBackendContainer && !apiOnly) return;
|
|
399
|
+
if (!hasBackendManifest || !hasBackendContainer) throw new Error('[ultramodern-release-envelope] MicroVertical backend federation manifest and container must be emitted together.');
|
|
393
400
|
const buildArtifact = await readBuildArtifact(distDirectory, requirePromotable);
|
|
394
401
|
if (!buildArtifact) return;
|
|
395
402
|
const identity = {
|
|
@@ -399,7 +406,7 @@ const emitFrameworkMicroVerticalReleaseEnvelope = async ({ apiOnly, distDirector
|
|
|
399
406
|
releaseVersion: buildArtifact.deliveryUnit.version
|
|
400
407
|
};
|
|
401
408
|
await readBackendManifest(distDirectory, identity);
|
|
402
|
-
const { artifacts, surfaces } = await createReleaseArtifactInputs(distDirectory, identity, target);
|
|
409
|
+
const { artifacts, surfaces } = await createReleaseArtifactInputs(distDirectory, identity, target, apiOnly);
|
|
403
410
|
const envelope = await (0, external_index_js_namespaceObject.createMicroVerticalReleaseEnvelope)({
|
|
404
411
|
artifactRoot: distDirectory,
|
|
405
412
|
target,
|
|
@@ -430,19 +437,16 @@ const readFrameworkMicroVerticalReleaseEnvelope = async ({ artifactRoot, expecte
|
|
|
430
437
|
});
|
|
431
438
|
};
|
|
432
439
|
const verifyBuildOutputReleaseEnvelope = async (distDirectory, expectedTarget)=>{
|
|
433
|
-
const [hasEnvelope, hasBuildArtifact
|
|
440
|
+
const [hasEnvelope, hasBuildArtifact] = await Promise.all([
|
|
434
441
|
pathExists(external_node_path_default().join(distDirectory, MICROVERTICAL_RELEASE_ENVELOPE_PATH)),
|
|
435
|
-
pathExists(external_node_path_default().join(distDirectory, backend_federation_contracts_namespaceObject.ULTRAMODERN_BUILD_ARTIFACT_FILE))
|
|
436
|
-
pathExists(external_node_path_default().join(distDirectory, backend_federation_contracts_namespaceObject.BACKEND_FEDERATION_MANIFEST_FILE)),
|
|
437
|
-
pathExists(external_node_path_default().join(distDirectory, backend_federation_contracts_namespaceObject.BACKEND_FEDERATION_REMOTE_ENTRY_FILE))
|
|
442
|
+
pathExists(external_node_path_default().join(distDirectory, backend_federation_contracts_namespaceObject.ULTRAMODERN_BUILD_ARTIFACT_FILE))
|
|
438
443
|
]);
|
|
439
|
-
const isFullStackMicroVertical = hasBuildArtifact && (hasBackendManifest || hasBackendEntry);
|
|
440
444
|
return readFrameworkMicroVerticalReleaseEnvelope({
|
|
441
445
|
artifactRoot: distDirectory,
|
|
442
446
|
...expectedTarget ? {
|
|
443
447
|
expectedTarget
|
|
444
448
|
} : {},
|
|
445
|
-
required: hasEnvelope ||
|
|
449
|
+
required: hasEnvelope || hasBuildArtifact
|
|
446
450
|
});
|
|
447
451
|
};
|
|
448
452
|
const verifyNodeReleaseEnvelopeStaging = async ({ outputDirectory })=>readFrameworkMicroVerticalReleaseEnvelope({
|
|
@@ -450,7 +454,7 @@ const verifyNodeReleaseEnvelopeStaging = async ({ outputDirectory })=>readFramew
|
|
|
450
454
|
expectedTarget: 'node',
|
|
451
455
|
required: true
|
|
452
456
|
});
|
|
453
|
-
const createNodeStagedReleaseArtifactInputs = async (outputDirectory, identity)=>{
|
|
457
|
+
const createNodeStagedReleaseArtifactInputs = async (outputDirectory, identity, apiOnly)=>{
|
|
454
458
|
const files = await collectFiles(outputDirectory);
|
|
455
459
|
for (const requiredPath of [
|
|
456
460
|
'index.js',
|
|
@@ -464,7 +468,7 @@ const createNodeStagedReleaseArtifactInputs = async (outputDirectory, identity)=
|
|
|
464
468
|
]);
|
|
465
469
|
const uiClientPaths = files.filter((logicalPath)=>{
|
|
466
470
|
const topLevel = logicalPath.split('/')[0];
|
|
467
|
-
return 'static' === topLevel || 'html' === topLevel || 'public' === topLevel || [
|
|
471
|
+
return 'static' === topLevel || 'html' === topLevel || 'public' === topLevel && logicalPath !== CRAWLER_POLICY_PATH && !isPublicMetadataPath(logicalPath) || [
|
|
468
472
|
'index.html',
|
|
469
473
|
'mf-manifest.json',
|
|
470
474
|
'mf-stats.json',
|
|
@@ -475,6 +479,7 @@ const createNodeStagedReleaseArtifactInputs = async (outputDirectory, identity)=
|
|
|
475
479
|
});
|
|
476
480
|
const ssrPaths = files.filter((logicalPath)=>'index.js' === logicalPath || logicalPath.startsWith('bundles/') && COMPILED_MODULE_PATTERN.test(logicalPath));
|
|
477
481
|
const apiBackendPaths = files.filter((logicalPath)=>(logicalPath.startsWith('api/') || logicalPath.startsWith('shared/')) && COMPILED_MODULE_PATTERN.test(logicalPath));
|
|
482
|
+
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.');
|
|
478
483
|
for (const [surface, paths] of [
|
|
479
484
|
[
|
|
480
485
|
'UI/client',
|
|
@@ -482,22 +487,24 @@ const createNodeStagedReleaseArtifactInputs = async (outputDirectory, identity)=
|
|
|
482
487
|
],
|
|
483
488
|
[
|
|
484
489
|
'Node SSR',
|
|
485
|
-
ssrPaths
|
|
490
|
+
apiOnly ? [] : ssrPaths
|
|
486
491
|
],
|
|
487
492
|
[
|
|
488
493
|
'Node API/backend',
|
|
489
494
|
apiBackendPaths
|
|
490
495
|
]
|
|
491
|
-
])if (0 === paths.length) throw new Error(`[ultramodern-release-envelope] final Node staging has no ${surface} artifacts.`);
|
|
492
|
-
const clientExecutionPaths = await manifestReferencedClientModules(outputDirectory, files);
|
|
493
|
-
const ssrExecutionPaths = await routeReferencedSsrModules(outputDirectory, files, 'node');
|
|
494
|
-
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.');
|
|
496
|
+
])if (0 === paths.length && (!apiOnly || 'Node API/backend' === surface)) throw new Error(`[ultramodern-release-envelope] final Node staging has no ${surface} artifacts.`);
|
|
497
|
+
const clientExecutionPaths = apiOnly ? [] : await manifestReferencedClientModules(outputDirectory, files);
|
|
498
|
+
const ssrExecutionPaths = apiOnly ? [] : await routeReferencedSsrModules(outputDirectory, files, 'node');
|
|
499
|
+
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.');
|
|
495
500
|
const runtimeByPath = new Map(files.filter((logicalPath)=>!backendFederationPaths.has(logicalPath)).map((logicalPath)=>[
|
|
496
501
|
logicalPath,
|
|
497
502
|
'nodejs-deployment'
|
|
498
503
|
]));
|
|
499
504
|
for (const logicalPath of uiClientPaths)runtimeByPath.set(logicalPath, 'browser');
|
|
500
|
-
|
|
505
|
+
if (files.includes(CRAWLER_POLICY_PATH)) runtimeByPath.set(CRAWLER_POLICY_PATH, 'crawler-policy');
|
|
506
|
+
for (const logicalPath of files.filter(isPublicMetadataPath))runtimeByPath.set(logicalPath, 'public-metadata');
|
|
507
|
+
for (const logicalPath of apiOnly ? [] : ssrPaths)runtimeByPath.set(logicalPath, 'nodejs');
|
|
501
508
|
for (const logicalPath of apiBackendPaths)runtimeByPath.set(logicalPath, 'nodejs');
|
|
502
509
|
runtimeByPath.set(backend_federation_contracts_namespaceObject.BACKEND_FEDERATION_MANIFEST_FILE, 'module-federation-manifest');
|
|
503
510
|
runtimeByPath.set(backend_federation_contracts_namespaceObject.BACKEND_FEDERATION_REMOTE_ENTRY_FILE, 'nodejs');
|
|
@@ -507,7 +514,7 @@ const createNodeStagedReleaseArtifactInputs = async (outputDirectory, identity)=
|
|
|
507
514
|
backend_federation_contracts_namespaceObject.BACKEND_FEDERATION_MANIFEST_FILE,
|
|
508
515
|
backend_federation_contracts_namespaceObject.BACKEND_FEDERATION_REMOTE_ENTRY_FILE
|
|
509
516
|
],
|
|
510
|
-
ssr: [
|
|
517
|
+
ssr: apiOnly ? [] : [
|
|
511
518
|
...new Set([
|
|
512
519
|
...ssrExecutionPaths,
|
|
513
520
|
...ssrPaths.filter((logicalPath)=>'index.js' !== logicalPath)
|
|
@@ -526,7 +533,7 @@ const createNodeStagedReleaseArtifactInputs = async (outputDirectory, identity)=
|
|
|
526
533
|
uiClient: [
|
|
527
534
|
...uiClientPaths
|
|
528
535
|
].sort((left, right)=>left.localeCompare(right)),
|
|
529
|
-
ssr: [
|
|
536
|
+
ssr: apiOnly ? [] : [
|
|
530
537
|
...ssrPaths
|
|
531
538
|
].sort((left, right)=>left.localeCompare(right)),
|
|
532
539
|
apiBackend: [
|
|
@@ -542,7 +549,7 @@ const createNodeStagedReleaseArtifactInputs = async (outputDirectory, identity)=
|
|
|
542
549
|
const emitNodeStagedReleaseEnvelope = async ({ distDirectory, outputDirectory })=>{
|
|
543
550
|
const source = await verifyBuildOutputReleaseEnvelope(distDirectory, 'node');
|
|
544
551
|
if (!source) return;
|
|
545
|
-
const { artifacts, surfaces } = await createNodeStagedReleaseArtifactInputs(outputDirectory, source.identity);
|
|
552
|
+
const { artifacts, surfaces } = await createNodeStagedReleaseArtifactInputs(outputDirectory, source.identity, 0 === source.surfaces.uiClient.length);
|
|
546
553
|
const staged = await (0, external_index_js_namespaceObject.createMicroVerticalReleaseEnvelope)({
|
|
547
554
|
artifactRoot: outputDirectory,
|
|
548
555
|
target: 'node',
|
|
@@ -560,21 +567,25 @@ const stageCloudflareReleaseEnvelope = async ({ distDirectory, outputDirectory }
|
|
|
560
567
|
if (await pathExists(stagedEnvelopePath)) throw new Error('[ultramodern-release-envelope] Cloudflare staging contains a stale envelope before final output generation.');
|
|
561
568
|
return source;
|
|
562
569
|
};
|
|
563
|
-
const createCloudflareStagedReleaseArtifactInputs = async (outputDirectory, identity)=>{
|
|
570
|
+
const createCloudflareStagedReleaseArtifactInputs = async (outputDirectory, identity, apiOnly)=>{
|
|
564
571
|
const files = await collectFiles(outputDirectory);
|
|
565
572
|
for (const requiredPath of [
|
|
566
573
|
'server/index.mjs',
|
|
567
574
|
'server/modern-worker-manifest.json',
|
|
568
575
|
'wrangler.json',
|
|
569
576
|
'package.json',
|
|
570
|
-
|
|
577
|
+
...apiOnly ? [] : [
|
|
578
|
+
'worker/package.json'
|
|
579
|
+
]
|
|
571
580
|
])if (!files.includes(requiredPath)) throw new Error(`[ultramodern-release-envelope] final Cloudflare staging is missing ${requiredPath}.`);
|
|
572
581
|
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.');
|
|
573
582
|
const backendManifestPath = `public/${backend_federation_contracts_namespaceObject.BACKEND_FEDERATION_MANIFEST_FILE}`;
|
|
574
583
|
const backendContainerPath = `public/${backend_federation_contracts_namespaceObject.BACKEND_FEDERATION_REMOTE_ENTRY_FILE}`;
|
|
575
|
-
const apiBackendPaths = files.filter(
|
|
576
|
-
|
|
577
|
-
const
|
|
584
|
+
const apiBackendPaths = files.filter(isEffectBffWorkerArtifact);
|
|
585
|
+
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.');
|
|
586
|
+
const ssrPaths = files.filter((logicalPath)=>(logicalPath.startsWith('server/') || logicalPath.startsWith('worker/')) && COMPILED_MODULE_PATTERN.test(logicalPath) && !isEffectBffWorkerArtifact(logicalPath));
|
|
587
|
+
const uiClientPaths = files.filter((logicalPath)=>logicalPath.startsWith('public/') && logicalPath !== CRAWLER_POLICY_PATH && !isPublicMetadataPath(logicalPath) && logicalPath !== PUBLIC_BUILD_ARTIFACT_PATH && logicalPath !== backendManifestPath && logicalPath !== backendContainerPath);
|
|
588
|
+
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.');
|
|
578
589
|
for (const [surface, paths] of [
|
|
579
590
|
[
|
|
580
591
|
'UI/client',
|
|
@@ -582,27 +593,29 @@ const createCloudflareStagedReleaseArtifactInputs = async (outputDirectory, iden
|
|
|
582
593
|
],
|
|
583
594
|
[
|
|
584
595
|
'Cloudflare SSR',
|
|
585
|
-
ssrPaths
|
|
596
|
+
apiOnly ? [] : ssrPaths
|
|
586
597
|
],
|
|
587
598
|
[
|
|
588
599
|
'Cloudflare API/backend',
|
|
589
600
|
apiBackendPaths
|
|
590
601
|
]
|
|
591
|
-
])if (0 === paths.length) throw new Error(`[ultramodern-release-envelope] final Cloudflare staging has no ${surface} artifacts.`);
|
|
602
|
+
])if (0 === paths.length && (!apiOnly || 'Cloudflare API/backend' === surface)) throw new Error(`[ultramodern-release-envelope] final Cloudflare staging has no ${surface} artifacts.`);
|
|
592
603
|
for (const logicalPath of [
|
|
593
604
|
backendManifestPath,
|
|
594
605
|
backendContainerPath
|
|
595
606
|
])if (!files.includes(logicalPath)) throw new Error(`[ultramodern-release-envelope] final Cloudflare staging is missing ${logicalPath}.`);
|
|
596
|
-
const clientExecutionPaths = await manifestReferencedClientModules(outputDirectory, files, 'public/mf-manifest.json');
|
|
597
|
-
const ssrExecutionPaths = await routeReferencedSsrModules(outputDirectory, files, 'cloudflare', 'server/route.json');
|
|
598
|
-
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.');
|
|
607
|
+
const clientExecutionPaths = apiOnly ? [] : await manifestReferencedClientModules(outputDirectory, files, 'public/mf-manifest.json');
|
|
608
|
+
const ssrExecutionPaths = apiOnly ? [] : await routeReferencedSsrModules(outputDirectory, files, 'cloudflare', 'server/route.json');
|
|
609
|
+
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.');
|
|
599
610
|
const runtimeByPath = new Map();
|
|
600
611
|
for (const logicalPath of files)runtimeByPath.set(logicalPath, 'cloudflare-deployment');
|
|
601
612
|
const add = (paths, runtime)=>{
|
|
602
613
|
for (const logicalPath of paths)runtimeByPath.set(logicalPath, runtime);
|
|
603
614
|
};
|
|
604
615
|
add(uiClientPaths, 'browser');
|
|
605
|
-
|
|
616
|
+
if (files.includes(CRAWLER_POLICY_PATH)) runtimeByPath.set(CRAWLER_POLICY_PATH, 'crawler-policy');
|
|
617
|
+
add(files.filter(isPublicMetadataPath), 'public-metadata');
|
|
618
|
+
add(apiOnly ? [] : ssrPaths, 'workerd');
|
|
606
619
|
add(apiBackendPaths, 'workerd-effect');
|
|
607
620
|
add([
|
|
608
621
|
backendManifestPath
|
|
@@ -616,7 +629,7 @@ const createCloudflareStagedReleaseArtifactInputs = async (outputDirectory, iden
|
|
|
616
629
|
backendManifestPath,
|
|
617
630
|
backendContainerPath
|
|
618
631
|
],
|
|
619
|
-
ssr: [
|
|
632
|
+
ssr: apiOnly ? [] : [
|
|
620
633
|
...new Set([
|
|
621
634
|
...ssrExecutionPaths,
|
|
622
635
|
...ssrPaths.filter((logicalPath)=>logicalPath.startsWith('worker/'))
|
|
@@ -635,7 +648,7 @@ const createCloudflareStagedReleaseArtifactInputs = async (outputDirectory, iden
|
|
|
635
648
|
uiClient: [
|
|
636
649
|
...uiClientPaths
|
|
637
650
|
].sort((left, right)=>left.localeCompare(right)),
|
|
638
|
-
ssr: [
|
|
651
|
+
ssr: apiOnly ? [] : [
|
|
639
652
|
...ssrPaths
|
|
640
653
|
].sort((left, right)=>left.localeCompare(right)),
|
|
641
654
|
apiBackend: [
|
|
@@ -651,7 +664,7 @@ const createCloudflareStagedReleaseArtifactInputs = async (outputDirectory, iden
|
|
|
651
664
|
const emitCloudflareStagedReleaseEnvelope = async ({ distDirectory, outputDirectory })=>{
|
|
652
665
|
const source = await verifyBuildOutputReleaseEnvelope(distDirectory, 'cloudflare');
|
|
653
666
|
if (!source) return;
|
|
654
|
-
const { artifacts, surfaces } = await createCloudflareStagedReleaseArtifactInputs(outputDirectory, source.identity);
|
|
667
|
+
const { artifacts, surfaces } = await createCloudflareStagedReleaseArtifactInputs(outputDirectory, source.identity, 0 === source.surfaces.uiClient.length);
|
|
655
668
|
const staged = await (0, external_index_js_namespaceObject.createMicroVerticalReleaseEnvelope)({
|
|
656
669
|
artifactRoot: outputDirectory,
|
|
657
670
|
target: 'cloudflare',
|
|
@@ -232,7 +232,7 @@ const assertUniqueSortedArtifacts = (artifacts, location)=>{
|
|
|
232
232
|
if (paths.some((logicalPath, index)=>logicalPath !== sorted[index])) throw new Error(`${location} must be sorted by logicalPath.`);
|
|
233
233
|
};
|
|
234
234
|
const assertSurfacePaths = (value, location)=>{
|
|
235
|
-
if (!Array.isArray(value)
|
|
235
|
+
if (!Array.isArray(value)) throw new Error(`${location} must be an array of artifact paths.`);
|
|
236
236
|
const paths = value.map((item, index)=>assertNormalizedLogicalPath(item, `${location}[${index}]`));
|
|
237
237
|
const sorted = [
|
|
238
238
|
...paths
|
|
@@ -254,10 +254,16 @@ const assertSurfaces = (value)=>{
|
|
|
254
254
|
'manifest',
|
|
255
255
|
'container'
|
|
256
256
|
], 'surfaces.backendFederation');
|
|
257
|
+
const uiClient = assertSurfacePaths(surfaces.uiClient, 'surfaces.uiClient');
|
|
258
|
+
const ssr = assertSurfacePaths(surfaces.ssr, 'surfaces.ssr');
|
|
259
|
+
const apiBackend = assertSurfacePaths(surfaces.apiBackend, 'surfaces.apiBackend');
|
|
260
|
+
if (uiClient.length > 0 && 0 === ssr.length) throw new Error('surfaces.ssr must contain at least one artifact path when UI/client is declared.');
|
|
261
|
+
if (ssr.length > 0 && 0 === uiClient.length) throw new Error('surfaces.uiClient must contain at least one artifact path when SSR is declared.');
|
|
262
|
+
if (0 === apiBackend.length) throw new Error('surfaces.apiBackend must contain at least one artifact path.');
|
|
257
263
|
return {
|
|
258
|
-
uiClient
|
|
259
|
-
ssr
|
|
260
|
-
apiBackend
|
|
264
|
+
uiClient,
|
|
265
|
+
ssr,
|
|
266
|
+
apiBackend,
|
|
261
267
|
backendFederation: {
|
|
262
268
|
manifest: assertNormalizedLogicalPath(backendFederation.manifest, 'surfaces.backendFederation.manifest'),
|
|
263
269
|
container: assertNormalizedLogicalPath(backendFederation.container, 'surfaces.backendFederation.container')
|
|
@@ -170,8 +170,13 @@ const createBackendManifest = (workspaceRoot, distDirectory, app, entryIntegrity
|
|
|
170
170
|
source: {
|
|
171
171
|
module: sourceModule
|
|
172
172
|
},
|
|
173
|
-
|
|
174
|
-
|
|
173
|
+
...'rpc' === app.apiProtocol ? {
|
|
174
|
+
rpcPath: app.rpcPath,
|
|
175
|
+
rpcSerialization: 'json'
|
|
176
|
+
} : {
|
|
177
|
+
readinessPath: `${app.apiPrefix}/${app.apiStem}/readiness`,
|
|
178
|
+
openapiPath: `${app.apiPrefix}/openapi.json`
|
|
179
|
+
},
|
|
175
180
|
...app.deliveryUnit ? {
|
|
176
181
|
deliveryUnit: app.deliveryUnit
|
|
177
182
|
} : {},
|
|
@@ -3,7 +3,8 @@ import promises from "node:fs/promises";
|
|
|
3
3
|
import node_path from "node:path";
|
|
4
4
|
import { resolveUltramodernSourceRevision } from "@modern-js/app-tools-extensions/release-identity";
|
|
5
5
|
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";
|
|
6
|
-
const
|
|
6
|
+
const REFERENCE_TOPOLOGY_PATH = 'topology/reference-topology.json';
|
|
7
|
+
const DEVELOPMENT_OVERLAY_PATH = 'topology/local-overlays/development.json';
|
|
7
8
|
const normalizeRelativePath = (value)=>value.replace(/\\/gu, '/').replace(/^\.\/+/u, '');
|
|
8
9
|
const isRecord = (value)=>'object' == typeof value && null !== value;
|
|
9
10
|
const readJsonFile = async (filePath)=>JSON.parse(await promises.readFile(filePath, 'utf8'));
|
|
@@ -31,15 +32,25 @@ const readBuildIdentity = async (appDirectory)=>{
|
|
|
31
32
|
const toPascalCase = (value)=>value.split(/[^a-zA-Z0-9]+/u).filter(Boolean).map((part)=>part.charAt(0).toUpperCase() + part.slice(1)).join('');
|
|
32
33
|
const stringValue = (value)=>'string' == typeof value && value.length > 0 ? value : void 0;
|
|
33
34
|
const createBackendName = (app, id)=>{
|
|
34
|
-
const configuredName = stringValue(app.backendFederation?.name) ?? stringValue(app.backendFederation?.executionSurfaces?.node?.remoteName)
|
|
35
|
+
const configuredName = stringValue(app.backendFederation?.name) ?? stringValue(app.backendFederation?.executionSurfaces?.node?.remoteName);
|
|
35
36
|
if (configuredName) return configuredName;
|
|
36
37
|
const mfName = stringValue(app.moduleFederation?.name);
|
|
37
38
|
return mfName ? `${mfName}Backend` : `vertical${toPascalCase(id)}Backend`;
|
|
38
39
|
};
|
|
40
|
+
const rebaseDefaultLocalUrl = (value, defaultPort, publicOrigin)=>{
|
|
41
|
+
let url;
|
|
42
|
+
try {
|
|
43
|
+
url = new URL(value);
|
|
44
|
+
} catch {
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
if ('http:' !== url.protocol || 'localhost' !== url.hostname || Number(url.port) !== defaultPort || url.username || url.password) return value;
|
|
48
|
+
return `${publicOrigin}${url.pathname}${url.search}${url.hash}`;
|
|
49
|
+
};
|
|
39
50
|
const findWorkspaceRoot = (appDirectory)=>{
|
|
40
51
|
let current = appDirectory;
|
|
41
52
|
while(true){
|
|
42
|
-
if (existsSync(node_path.join(current,
|
|
53
|
+
if (existsSync(node_path.join(current, REFERENCE_TOPOLOGY_PATH))) return current;
|
|
43
54
|
const parent = node_path.dirname(current);
|
|
44
55
|
if (parent === current) return;
|
|
45
56
|
current = parent;
|
|
@@ -63,54 +74,90 @@ const createStampedDeliveryUnit = (input)=>{
|
|
|
63
74
|
if (!validation.ok) return;
|
|
64
75
|
return deliveryUnitContractBlock(record);
|
|
65
76
|
};
|
|
66
|
-
const
|
|
67
|
-
const id = stringValue(
|
|
68
|
-
const appPath = stringValue(
|
|
69
|
-
if (!id || 'vertical' !==
|
|
70
|
-
const directory =
|
|
77
|
+
const createAppFromTopology = (workspaceRoot, appDirectory, topologyApp, overlay, packageManifest)=>{
|
|
78
|
+
const id = stringValue(topologyApp.id);
|
|
79
|
+
const appPath = stringValue(topologyApp.path);
|
|
80
|
+
if (!id || !appPath || 'vertical' !== topologyApp.kind || !isRecord(topologyApp.api) || !isRecord(topologyApp.backendFederation)) return;
|
|
81
|
+
const directory = normalizeRelativePath(appPath);
|
|
71
82
|
if (node_path.resolve(workspaceRoot, directory) !== node_path.resolve(appDirectory)) return;
|
|
72
|
-
const
|
|
73
|
-
if (
|
|
74
|
-
const
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
-
|
|
83
|
+
const defaultPort = overlay.ports?.[id];
|
|
84
|
+
if ('number' != typeof defaultPort || !Number.isInteger(defaultPort) || defaultPort < 1 || defaultPort > 65535) throw new Error(`[backend-federation-build] Invalid development port for ${id}.`);
|
|
85
|
+
const portEnv = stringValue(topologyApp.portEnv) ?? `VERTICAL_${(stringValue(topologyApp.domain) ?? id).replace(/[^a-zA-Z0-9]/gu, '_').toUpperCase()}_PORT`;
|
|
86
|
+
const port = Number(process.env[portEnv] ?? defaultPort);
|
|
87
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error(`[backend-federation-build] Invalid ${portEnv} development port for ${id}.`);
|
|
88
|
+
const publicUrlEnv = stringValue(topologyApp.cloudflare?.publicUrlEnv);
|
|
89
|
+
const configuredPublicUrl = publicUrlEnv ? process.env[publicUrlEnv]?.trim() : void 0;
|
|
90
|
+
let publicOrigin = `http://localhost:${port}`;
|
|
91
|
+
if (configuredPublicUrl) {
|
|
92
|
+
let url;
|
|
93
|
+
try {
|
|
94
|
+
url = new URL(configuredPublicUrl);
|
|
95
|
+
} catch {
|
|
96
|
+
throw new Error(`[backend-federation-build] Invalid ${publicUrlEnv} public URL for ${id}.`);
|
|
97
|
+
}
|
|
98
|
+
if (![
|
|
99
|
+
'http:',
|
|
100
|
+
'https:'
|
|
101
|
+
].includes(url.protocol) || url.username || url.password) throw new Error(`[backend-federation-build] Invalid ${publicUrlEnv} public URL for ${id}.`);
|
|
102
|
+
publicOrigin = url.origin;
|
|
103
|
+
}
|
|
104
|
+
const apiPrefix = stringValue(topologyApp.api.bff?.prefix) ?? `/${id}-api`;
|
|
105
|
+
const apiProtocol = topologyApp.api.protocol ?? 'rest';
|
|
106
|
+
if ('rest' !== apiProtocol && 'rpc' !== apiProtocol) throw new Error(`[backend-federation-build] Unsupported API protocol for ${id}.`);
|
|
107
|
+
const rpcPath = 'rpc' === apiProtocol ? stringValue(topologyApp.api.rpcPath) : void 0;
|
|
108
|
+
if ('rpc' === apiProtocol && (!rpcPath || 'json' !== topologyApp.api.rpcSerialization)) throw new Error(`[backend-federation-build] Missing JSON RPC route metadata for ${id}.`);
|
|
109
|
+
const apiStem = stringValue(topologyApp.api.stem) ?? id;
|
|
110
|
+
const backendName = createBackendName(topologyApp, id);
|
|
111
|
+
const nodeOverlay = overlay.serverExecution?.[id]?.node;
|
|
112
|
+
if (!isRecord(nodeOverlay)) throw new Error(`[backend-federation-build] Missing declared Node server execution for ${id}.`);
|
|
113
|
+
const manifestUrl = rebaseDefaultLocalUrl(stringValue(nodeOverlay?.manifestUrl) ?? `http://localhost:${defaultPort}/${BACKEND_FEDERATION_MANIFEST_FILE}`, defaultPort, publicOrigin);
|
|
114
|
+
const containerEntry = rebaseDefaultLocalUrl(stringValue(nodeOverlay?.containerEntry) ?? `http://localhost:${defaultPort}/${BACKEND_FEDERATION_REMOTE_ENTRY_FILE}`, defaultPort, publicOrigin);
|
|
115
|
+
const configuredRemoteType = stringValue(nodeOverlay?.remoteType) ?? stringValue(topologyApp.backendFederation?.executionSurfaces?.node?.remoteType);
|
|
80
116
|
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}.`);
|
|
81
117
|
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}.`);
|
|
82
118
|
const remoteType = 'commonjs-module';
|
|
83
|
-
const packageName = stringValue(
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
119
|
+
const packageName = stringValue(packageManifest.name);
|
|
120
|
+
const version = stringValue(packageManifest.version);
|
|
121
|
+
if (stringValue(topologyApp.package) !== packageName || !version) throw new Error(`[backend-federation-build] Topology package identity must match package.json for ${id}.`);
|
|
122
|
+
const declaredUiManifestUrl = stringValue(overlay.manifests?.[id]);
|
|
123
|
+
const uiManifestUrl = declaredUiManifestUrl ? rebaseDefaultLocalUrl(declaredUiManifestUrl, defaultPort, publicOrigin) : void 0;
|
|
124
|
+
const topologyDeliveryUnit = isRecord(topologyApp.deliveryUnit) ? {
|
|
125
|
+
unitId: stringValue(topologyApp.deliveryUnit.unitId),
|
|
126
|
+
buildMarker: stringValue(topologyApp.deliveryUnit.buildMarker),
|
|
127
|
+
sourceRevision: stringValue(topologyApp.deliveryUnit.sourceRevision),
|
|
128
|
+
packageName: stringValue(topologyApp.deliveryUnit.packageName),
|
|
129
|
+
version: stringValue(topologyApp.deliveryUnit.version)
|
|
91
130
|
} : void 0;
|
|
92
131
|
return {
|
|
93
132
|
id,
|
|
94
133
|
directory,
|
|
95
134
|
packageName,
|
|
135
|
+
version,
|
|
96
136
|
port,
|
|
97
137
|
apiPrefix,
|
|
138
|
+
apiProtocol,
|
|
139
|
+
rpcPath,
|
|
98
140
|
apiStem,
|
|
99
141
|
backendName,
|
|
100
142
|
manifestUrl,
|
|
101
143
|
containerEntry,
|
|
102
144
|
remoteType,
|
|
103
145
|
uiManifestUrl,
|
|
104
|
-
|
|
146
|
+
topologyDeliveryUnit
|
|
105
147
|
};
|
|
106
148
|
};
|
|
107
149
|
const findBackendFederationApp = async (workspaceRoot, appDirectory)=>{
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
150
|
+
const topologyPath = node_path.join(workspaceRoot, REFERENCE_TOPOLOGY_PATH);
|
|
151
|
+
const topology = await readJsonFile(topologyPath);
|
|
152
|
+
if (!isRecord(topology) || !Array.isArray(topology.verticals)) throw new Error(`[backend-federation-build] Invalid declared topology at ${topologyPath}.`);
|
|
153
|
+
const overlayPath = node_path.join(workspaceRoot, DEVELOPMENT_OVERLAY_PATH);
|
|
154
|
+
const overlay = await readJsonFile(overlayPath);
|
|
155
|
+
if (!isRecord(overlay) || !isRecord(overlay.ports)) throw new Error(`[backend-federation-build] Invalid development overlay at ${overlayPath}.`);
|
|
156
|
+
const packageManifest = await readJsonFile(node_path.join(appDirectory, 'package.json'));
|
|
157
|
+
for (const app of topology.verticals){
|
|
158
|
+
const resolved = createAppFromTopology(workspaceRoot, appDirectory, app, overlay, packageManifest);
|
|
113
159
|
if (resolved) return resolved;
|
|
114
160
|
}
|
|
161
|
+
throw new Error(`[backend-federation-build] ${appDirectory} is missing from the declared reference topology.`);
|
|
115
162
|
};
|
|
116
|
-
export {
|
|
163
|
+
export { DEVELOPMENT_OVERLAY_PATH, REFERENCE_TOPOLOGY_PATH, buildArtifactPathFor, createAppFromTopology, createStampedDeliveryUnit, findBackendFederationApp, findWorkspaceRoot, normalizeRelativePath, readBuildIdentity, resolveWorkspaceSourceRevision };
|