@bleedingdev/modern-js-app-tools-extensions 3.9.0-ultramodern.9 → 3.9.3-ultramodern.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -0
- package/dist/cjs/backend-federation-build/codegen.js +29 -18
- package/dist/cjs/backend-federation-build/config.js +84 -45
- package/dist/cjs/backend-federation-build/emit.js +63 -22
- package/dist/cjs/build-config/build-environment.js +25 -162
- package/dist/cjs/build-config/internal-effect-discovery.js +143 -0
- package/dist/cjs/build-config/public.js +2 -5
- package/dist/cjs/build-config/remote-address.js +58 -0
- package/dist/cjs/cloudflare/artifacts.js +8 -19
- package/dist/cjs/cloudflare/delivery-unit.js +125 -43
- package/dist/cjs/cloudflare/index.js +14 -4
- package/dist/cjs/cloudflare/native-renderer.js +100 -0
- package/dist/cjs/cloudflare/renderer-identity.js +106 -0
- package/dist/cjs/cloudflare/worker-manifest.js +54 -13
- package/dist/cjs/cloudflare/worker-options.js +48 -0
- package/dist/cjs/cloudflare/wrangler-config.js +210 -13
- package/dist/cjs/cloudflare-builder.js +307 -89
- package/dist/cjs/cloudflare-output-contract.js +36 -4
- package/dist/cjs/cloudflare-output-verifier/identity.js +112 -23
- 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/cloudflare-output-verifier/worker-bundles.js +10 -5
- package/dist/cjs/config.js +10 -5
- package/dist/cjs/deploy-output/npmAliases.js +92 -6
- package/dist/cjs/deploy-output/plugin.js +27 -6
- package/dist/cjs/deploy-output/public-assets.js +146 -0
- package/dist/cjs/deploy-output/target.js +57 -1
- package/dist/cjs/deploy-output/trace-files.js +99 -0
- package/dist/cjs/module-federation-config/exposes.js +70 -0
- package/dist/cjs/module-federation-config/index.js +73 -0
- package/dist/cjs/module-federation-config/inspect.js +103 -0
- package/dist/cjs/module-federation-config/object-literal.js +87 -0
- package/dist/cjs/module-federation-config/syntax.js +151 -0
- package/dist/cjs/module-federation-config/types.js +18 -0
- package/dist/cjs/native-type-checker.js +130 -13
- package/dist/cjs/policy-defaults.js +38 -29
- package/dist/cjs/release-envelope/canonical.js +2 -9
- package/dist/cjs/release-envelope/contract.js +68 -0
- package/dist/cjs/release-envelope/framework-output.js +296 -105
- package/dist/cjs/release-envelope/index.js +150 -36
- package/dist/cjs/release-envelope/plugin.js +24 -15
- package/dist/cjs/release-envelope/renderer-output-stamp.js +170 -0
- package/dist/cjs/release-envelope/types.js +10 -8
- package/dist/cjs/release-identity.js +8 -5
- package/dist/cjs/renderer-build-identity.js +134 -0
- package/dist/cjs/templates/cloudflare-entry.001-bootstrap-security.mjs +14 -6
- package/dist/cjs/templates/cloudflare-entry.002-assets-routes.mjs +10 -2
- package/dist/cjs/templates/cloudflare-entry.004-rendering-css.mjs +403 -96
- package/dist/cjs/templates/cloudflare-entry.005-worker-dispatch.mjs +530 -103
- package/dist/cjs/templates/cloudflare-entry.006-fetch-handler.mjs +26 -7
- package/dist/cjs/templates/cloudflare-worker-route-data.mjs +1069 -0
- package/dist/cjs/workspace-topology.js +84 -0
- package/dist/esm/backend-federation-build/codegen.mjs +29 -18
- package/dist/esm/backend-federation-build/config.mjs +74 -38
- package/dist/esm/backend-federation-build/emit.mjs +64 -23
- package/dist/esm/build-config/build-environment.mjs +26 -160
- package/dist/esm/build-config/internal-effect-discovery.mjs +102 -0
- package/dist/esm/build-config/public.mjs +1 -1
- package/dist/esm/build-config/remote-address.mjs +20 -0
- package/dist/esm/cloudflare/artifacts.mjs +8 -19
- package/dist/esm/cloudflare/delivery-unit.mjs +124 -45
- package/dist/esm/cloudflare/index.mjs +15 -5
- package/dist/esm/cloudflare/native-renderer.mjs +48 -0
- package/dist/esm/cloudflare/renderer-identity.mjs +57 -0
- package/dist/esm/cloudflare/worker-manifest.mjs +55 -14
- package/dist/esm/cloudflare/worker-options.mjs +10 -0
- package/dist/esm/cloudflare/wrangler-config.mjs +203 -12
- package/dist/esm/cloudflare-builder.mjs +301 -89
- package/dist/esm/cloudflare-output-contract.mjs +29 -3
- package/dist/esm/cloudflare-output-verifier/identity.mjs +113 -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/cloudflare-output-verifier/worker-bundles.mjs +11 -6
- package/dist/esm/config.mjs +3 -1
- package/dist/esm/deploy-output/npmAliases.mjs +92 -6
- package/dist/esm/deploy-output/plugin.mjs +24 -6
- package/dist/esm/deploy-output/public-assets.mjs +82 -0
- package/dist/esm/deploy-output/target.mjs +50 -3
- package/dist/esm/deploy-output/trace-files.mjs +47 -0
- package/dist/esm/module-federation-config/exposes.mjs +18 -0
- package/dist/esm/module-federation-config/index.mjs +4 -0
- package/dist/esm/module-federation-config/inspect.mjs +65 -0
- package/dist/esm/module-federation-config/object-literal.mjs +43 -0
- package/dist/esm/module-federation-config/syntax.mjs +101 -0
- package/dist/esm/module-federation-config/types.mjs +0 -0
- package/dist/esm/native-type-checker.mjs +124 -13
- package/dist/esm/policy-defaults.mjs +36 -30
- package/dist/esm/release-envelope/canonical.mjs +2 -9
- package/dist/esm/release-envelope/contract.mjs +21 -0
- package/dist/esm/release-envelope/framework-output.mjs +297 -106
- package/dist/esm/release-envelope/index.mjs +149 -38
- package/dist/esm/release-envelope/plugin.mjs +24 -15
- package/dist/esm/release-envelope/renderer-output-stamp.mjs +112 -0
- package/dist/esm/release-envelope/types.mjs +2 -3
- package/dist/esm/release-identity.mjs +8 -5
- package/dist/esm/renderer-build-identity.mjs +82 -0
- package/dist/esm/templates/cloudflare-entry.001-bootstrap-security.mjs +14 -6
- package/dist/esm/templates/cloudflare-entry.002-assets-routes.mjs +10 -2
- package/dist/esm/templates/cloudflare-entry.004-rendering-css.mjs +403 -96
- package/dist/esm/templates/cloudflare-entry.005-worker-dispatch.mjs +530 -103
- package/dist/esm/templates/cloudflare-entry.006-fetch-handler.mjs +26 -7
- package/dist/esm/templates/cloudflare-worker-route-data.mjs +1069 -0
- package/dist/esm/workspace-topology.mjs +21 -0
- package/dist/esm-node/backend-federation-build/codegen.mjs +29 -18
- package/dist/esm-node/backend-federation-build/config.mjs +74 -38
- package/dist/esm-node/backend-federation-build/emit.mjs +64 -23
- package/dist/esm-node/build-config/build-environment.mjs +26 -160
- package/dist/esm-node/build-config/internal-effect-discovery.mjs +103 -0
- package/dist/esm-node/build-config/public.mjs +1 -1
- package/dist/esm-node/build-config/remote-address.mjs +21 -0
- package/dist/esm-node/cloudflare/artifacts.mjs +8 -19
- package/dist/esm-node/cloudflare/delivery-unit.mjs +124 -45
- package/dist/esm-node/cloudflare/index.mjs +15 -5
- package/dist/esm-node/cloudflare/native-renderer.mjs +49 -0
- package/dist/esm-node/cloudflare/renderer-identity.mjs +58 -0
- package/dist/esm-node/cloudflare/worker-manifest.mjs +55 -14
- package/dist/esm-node/cloudflare/worker-options.mjs +11 -0
- package/dist/esm-node/cloudflare/wrangler-config.mjs +203 -12
- package/dist/esm-node/cloudflare-builder.mjs +301 -89
- package/dist/esm-node/cloudflare-output-contract.mjs +29 -3
- package/dist/esm-node/cloudflare-output-verifier/identity.mjs +113 -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/cloudflare-output-verifier/worker-bundles.mjs +11 -6
- package/dist/esm-node/config.mjs +3 -1
- package/dist/esm-node/deploy-output/npmAliases.mjs +92 -6
- package/dist/esm-node/deploy-output/plugin.mjs +24 -6
- package/dist/esm-node/deploy-output/public-assets.mjs +83 -0
- package/dist/esm-node/deploy-output/target.mjs +50 -3
- package/dist/esm-node/deploy-output/trace-files.mjs +48 -0
- package/dist/esm-node/module-federation-config/exposes.mjs +19 -0
- package/dist/esm-node/module-federation-config/index.mjs +5 -0
- package/dist/esm-node/module-federation-config/inspect.mjs +66 -0
- package/dist/esm-node/module-federation-config/object-literal.mjs +44 -0
- package/dist/esm-node/module-federation-config/syntax.mjs +102 -0
- package/dist/esm-node/module-federation-config/types.mjs +1 -0
- package/dist/esm-node/native-type-checker.mjs +124 -13
- package/dist/esm-node/policy-defaults.mjs +36 -30
- package/dist/esm-node/release-envelope/canonical.mjs +2 -9
- package/dist/esm-node/release-envelope/contract.mjs +22 -0
- package/dist/esm-node/release-envelope/framework-output.mjs +297 -106
- package/dist/esm-node/release-envelope/index.mjs +149 -38
- package/dist/esm-node/release-envelope/plugin.mjs +24 -15
- package/dist/esm-node/release-envelope/renderer-output-stamp.mjs +113 -0
- package/dist/esm-node/release-envelope/types.mjs +2 -3
- package/dist/esm-node/release-identity.mjs +8 -5
- package/dist/esm-node/renderer-build-identity.mjs +83 -0
- package/dist/esm-node/templates/cloudflare-entry.001-bootstrap-security.mjs +14 -6
- package/dist/esm-node/templates/cloudflare-entry.002-assets-routes.mjs +10 -2
- package/dist/esm-node/templates/cloudflare-entry.004-rendering-css.mjs +403 -96
- package/dist/esm-node/templates/cloudflare-entry.005-worker-dispatch.mjs +530 -103
- package/dist/esm-node/templates/cloudflare-entry.006-fetch-handler.mjs +26 -7
- package/dist/esm-node/templates/cloudflare-worker-route-data.mjs +1069 -0
- package/dist/esm-node/workspace-topology.mjs +22 -0
- package/dist/types/backend-federation-build/codegen.d.ts +30 -0
- package/dist/types/backend-federation-build/config.d.ts +10 -57
- package/dist/types/backend-federation-build/emit.d.ts +12 -2
- package/dist/types/backend-federation-build/index.d.ts +1 -1
- package/dist/types/build-config/build-environment.d.ts +1 -9
- package/dist/types/build-config/internal-effect-discovery.d.ts +4 -0
- package/dist/types/build-config/public.d.ts +1 -1
- package/dist/types/build-config/remote-address.d.ts +3 -0
- package/dist/types/cloudflare/artifacts.d.ts +7 -6
- package/dist/types/cloudflare/constants.d.ts +1 -1
- package/dist/types/cloudflare/delivery-unit.d.ts +38 -7
- package/dist/types/cloudflare/native-renderer.d.ts +30 -0
- package/dist/types/cloudflare/renderer-identity.d.ts +25 -0
- package/dist/types/cloudflare/worker-manifest.d.ts +17 -2
- package/dist/types/cloudflare/worker-options.d.ts +20 -0
- package/dist/types/cloudflare/wrangler-config.d.ts +55 -2
- package/dist/types/cloudflare-builder.d.ts +51 -8
- package/dist/types/cloudflare-output-contract.d.ts +5 -3
- package/dist/types/cloudflare-output-verifier/identity.d.ts +7 -3
- package/dist/types/cloudflare-output-verifier/worker-bundles.d.ts +2 -2
- package/dist/types/config.d.ts +98 -13
- package/dist/types/deploy-output/plugin.d.ts +29 -9
- package/dist/types/deploy-output/public-assets.d.ts +51 -0
- package/dist/types/deploy-output/target.d.ts +28 -5
- package/dist/types/deploy-output/trace-files.d.ts +17 -0
- package/dist/types/module-federation-config/exposes.d.ts +11 -0
- package/dist/types/module-federation-config/index.d.ts +5 -0
- package/dist/types/module-federation-config/inspect.d.ts +2 -0
- package/dist/types/module-federation-config/object-literal.d.ts +4 -0
- package/dist/types/module-federation-config/syntax.d.ts +7 -0
- package/dist/types/module-federation-config/types.d.ts +20 -0
- package/dist/types/native-type-checker.d.ts +26 -3
- package/dist/types/policy-defaults.d.ts +12 -2
- package/dist/types/release-envelope/canonical.d.ts +5 -5
- package/dist/types/release-envelope/contract.d.mts +8 -0
- package/dist/types/release-envelope/framework-output.d.ts +23 -13
- package/dist/types/release-envelope/index.d.ts +17 -5
- package/dist/types/release-envelope/plugin.d.ts +10 -6
- package/dist/types/release-envelope/renderer-output-stamp.d.ts +34 -0
- package/dist/types/release-envelope/types.d.ts +42 -4
- package/dist/types/release-identity.d.ts +3 -2
- package/dist/types/renderer-build-identity.d.ts +30 -0
- package/dist/types/templates/cloudflare-worker-route-data.d.mts +14 -0
- package/dist/types/workspace-topology.d.ts +91 -0
- package/package.json +87 -27
- package/src/backend-federation-build/codegen.ts +28 -19
- package/src/backend-federation-build/config.ts +189 -123
- package/src/backend-federation-build/emit.ts +119 -38
- package/src/backend-federation-build/index.ts +4 -1
- package/src/build-config/build-environment.ts +59 -285
- package/src/build-config/internal-effect-discovery.ts +239 -0
- package/src/build-config/public.ts +0 -1
- package/src/build-config/remote-address.ts +33 -0
- package/src/cloudflare/artifacts.ts +22 -43
- package/src/cloudflare/delivery-unit.ts +283 -60
- package/src/cloudflare/index.ts +35 -19
- package/src/cloudflare/native-renderer.ts +117 -0
- package/src/cloudflare/renderer-identity.ts +171 -0
- package/src/cloudflare/worker-manifest.ts +108 -10
- package/src/cloudflare/worker-options.ts +60 -0
- package/src/cloudflare/wrangler-config.ts +390 -14
- package/src/cloudflare-builder.ts +543 -170
- package/src/cloudflare-output-contract.ts +45 -2
- package/src/cloudflare-output-verifier/identity.ts +138 -7
- 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/cloudflare-output-verifier/worker-bundles.ts +27 -11
- package/src/config.ts +101 -19
- package/src/deploy-output/npmAliases.ts +141 -4
- package/src/deploy-output/plugin.ts +55 -18
- package/src/deploy-output/public-assets.ts +224 -0
- package/src/deploy-output/target.ts +97 -14
- package/src/deploy-output/trace-files.ts +80 -0
- package/src/module-federation-config/exposes.ts +46 -0
- package/src/module-federation-config/index.ts +22 -0
- package/src/module-federation-config/inspect.ts +157 -0
- package/src/module-federation-config/object-literal.ts +76 -0
- package/src/module-federation-config/syntax.ts +212 -0
- package/src/module-federation-config/types.ts +22 -0
- package/src/native-type-checker.ts +233 -20
- package/src/policy-defaults.ts +47 -17
- package/src/release-envelope/canonical.ts +6 -18
- package/src/release-envelope/contract.mjs +28 -0
- package/src/release-envelope/framework-output.ts +681 -156
- package/src/release-envelope/index.ts +315 -60
- package/src/release-envelope/plugin.ts +54 -38
- package/src/release-envelope/renderer-output-stamp.ts +210 -0
- package/src/release-envelope/types.ts +75 -6
- package/src/release-identity.ts +19 -2
- package/src/renderer-build-identity.ts +168 -0
- package/src/templates/cloudflare-entry.001-bootstrap-security.mjs +14 -6
- package/src/templates/cloudflare-entry.002-assets-routes.mjs +10 -2
- package/src/templates/cloudflare-entry.004-rendering-css.mjs +403 -96
- package/src/templates/cloudflare-entry.005-worker-dispatch.mjs +530 -103
- package/src/templates/cloudflare-entry.006-fetch-handler.mjs +26 -7
- package/src/templates/cloudflare-worker-route-data.mjs +1069 -0
- package/src/workspace-topology.ts +125 -0
- package/dist/cjs/templates/cloudflare-worker-fs-promises.mjs +0 -11
- package/dist/cjs/templates/cloudflare-worker-loadable-server.mjs +0 -281
- package/dist/cjs/templates/cloudflare-worker-mf-ssr-runtime-plugin.mjs +0 -14
- package/dist/cjs/templates/cloudflare-worker-path.mjs +0 -98
- package/dist/esm/templates/cloudflare-worker-fs-promises.mjs +0 -11
- package/dist/esm/templates/cloudflare-worker-loadable-server.mjs +0 -281
- package/dist/esm/templates/cloudflare-worker-mf-ssr-runtime-plugin.mjs +0 -14
- package/dist/esm/templates/cloudflare-worker-path.mjs +0 -98
- package/dist/esm-node/templates/cloudflare-worker-fs-promises.mjs +0 -11
- package/dist/esm-node/templates/cloudflare-worker-loadable-server.mjs +0 -281
- package/dist/esm-node/templates/cloudflare-worker-mf-ssr-runtime-plugin.mjs +0 -14
- package/dist/esm-node/templates/cloudflare-worker-path.mjs +0 -98
- package/dist/types/templates/cloudflare-worker-fs-promises.d.mts +0 -5
- package/dist/types/templates/cloudflare-worker-loadable-server.d.mts +0 -48
- package/dist/types/templates/cloudflare-worker-mf-ssr-runtime-plugin.d.mts +0 -13
- package/dist/types/templates/cloudflare-worker-path.d.mts +0 -21
- package/src/templates/cloudflare-worker-fs-promises.mjs +0 -11
- package/src/templates/cloudflare-worker-loadable-server.mjs +0 -281
- package/src/templates/cloudflare-worker-mf-ssr-runtime-plugin.mjs +0 -14
- package/src/templates/cloudflare-worker-path.mjs +0 -98
|
@@ -2,8 +2,9 @@ import "node:module";
|
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import promises from "node:fs/promises";
|
|
4
4
|
import node_path from "node:path";
|
|
5
|
+
import { formatBackendFederationValidationErrors, immutableRendererRouterBindings, validateRendererIdentity, validateRendererProfile, validateRendererProfileCompatibility, validateRendererRouterBindings } from "@modern-js/backend-federation-contracts";
|
|
5
6
|
import { canonicalSerializeMicroVerticalReleaseEnvelope, digestMicroVerticalReleaseEnvelopePayload, releaseEnvelopePayload } from "./canonical.mjs";
|
|
6
|
-
import { MICROVERTICAL_RELEASE_ENVELOPE_KIND, MICROVERTICAL_RELEASE_ENVELOPE_SCHEMA_VERSION, MICROVERTICAL_RELEASE_TARGETS } from "./types.mjs";
|
|
7
|
+
import { MICROVERTICAL_RELEASE_ENVELOPE_KIND, MICROVERTICAL_RELEASE_ENVELOPE_SCHEMA_VERSION, MICROVERTICAL_RELEASE_TARGETS, SHELL_RELEASE_ENVELOPE_KIND } from "./types.mjs";
|
|
7
8
|
const SHA256_PATTERN = /^[a-f0-9]{64}$/u;
|
|
8
9
|
const SOURCE_REVISION_PATTERN = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u;
|
|
9
10
|
const TARGETS = new Set(MICROVERTICAL_RELEASE_TARGETS);
|
|
@@ -27,6 +28,10 @@ const assertTarget = (value, location = 'target')=>{
|
|
|
27
28
|
if (!TARGETS.has(target)) throw new Error(`${location} must be one of: ${MICROVERTICAL_RELEASE_TARGETS.join(', ')}.`);
|
|
28
29
|
return target;
|
|
29
30
|
};
|
|
31
|
+
const assertEnvelopeKind = (value)=>{
|
|
32
|
+
if (value !== MICROVERTICAL_RELEASE_ENVELOPE_KIND && value !== SHELL_RELEASE_ENVELOPE_KIND) throw new Error(`envelope.kind must be "${MICROVERTICAL_RELEASE_ENVELOPE_KIND}" or "${SHELL_RELEASE_ENVELOPE_KIND}".`);
|
|
33
|
+
return value;
|
|
34
|
+
};
|
|
30
35
|
const assertReleaseIdentity = (value, location = 'identity')=>{
|
|
31
36
|
const identity = assertRecord(value, location);
|
|
32
37
|
assertExactKeys(identity, [
|
|
@@ -185,7 +190,7 @@ const assertUniqueSortedArtifacts = (artifacts, location)=>{
|
|
|
185
190
|
if (paths.some((logicalPath, index)=>logicalPath !== sorted[index])) throw new Error(`${location} must be sorted by logicalPath.`);
|
|
186
191
|
};
|
|
187
192
|
const assertSurfacePaths = (value, location)=>{
|
|
188
|
-
if (!Array.isArray(value)
|
|
193
|
+
if (!Array.isArray(value)) throw new Error(`${location} must be an array of artifact paths.`);
|
|
189
194
|
const paths = value.map((item, index)=>assertNormalizedLogicalPath(item, `${location}[${index}]`));
|
|
190
195
|
const sorted = [
|
|
191
196
|
...paths
|
|
@@ -194,26 +199,51 @@ const assertSurfacePaths = (value, location)=>{
|
|
|
194
199
|
if (new Set(paths).size !== paths.length) throw new Error(`${location} must not contain duplicate artifact paths.`);
|
|
195
200
|
return paths;
|
|
196
201
|
};
|
|
197
|
-
const assertSurfaces = (value)=>{
|
|
202
|
+
const assertSurfaces = (value, kind)=>{
|
|
198
203
|
const surfaces = assertRecord(value, 'surfaces');
|
|
199
204
|
assertExactKeys(surfaces, [
|
|
200
205
|
'uiClient',
|
|
201
206
|
'ssr',
|
|
202
207
|
'apiBackend',
|
|
203
|
-
'backendFederation'
|
|
208
|
+
...kind !== SHELL_RELEASE_ENVELOPE_KIND && Object.hasOwn(surfaces, 'backendFederation') ? [
|
|
209
|
+
'backendFederation'
|
|
210
|
+
] : []
|
|
204
211
|
], 'surfaces');
|
|
205
|
-
const backendFederation = assertRecord(surfaces.backendFederation, 'surfaces.backendFederation');
|
|
206
|
-
assertExactKeys(backendFederation, [
|
|
212
|
+
const backendFederation = Object.hasOwn(surfaces, 'backendFederation') ? assertRecord(surfaces.backendFederation, 'surfaces.backendFederation') : void 0;
|
|
213
|
+
if (backendFederation) assertExactKeys(backendFederation, [
|
|
207
214
|
'manifest',
|
|
208
215
|
'container'
|
|
209
216
|
], 'surfaces.backendFederation');
|
|
217
|
+
const uiClient = assertSurfacePaths(surfaces.uiClient, 'surfaces.uiClient');
|
|
218
|
+
const ssr = assertSurfacePaths(surfaces.ssr, 'surfaces.ssr');
|
|
219
|
+
const apiBackend = assertSurfacePaths(surfaces.apiBackend, 'surfaces.apiBackend');
|
|
220
|
+
if (uiClient.length > 0 && 0 === ssr.length) throw new Error('surfaces.ssr must contain at least one artifact path when UI/client is declared.');
|
|
221
|
+
if (ssr.length > 0 && 0 === uiClient.length) throw new Error('surfaces.uiClient must contain at least one artifact path when SSR is declared.');
|
|
222
|
+
if (0 === apiBackend.length && (backendFederation || 0 === uiClient.length)) throw new Error('surfaces.apiBackend must contain at least one artifact path.');
|
|
223
|
+
if (kind === SHELL_RELEASE_ENVELOPE_KIND) {
|
|
224
|
+
if (0 === uiClient.length || 0 === ssr.length) throw new Error('Shell surfaces.uiClient and surfaces.ssr must each contain at least one artifact path.');
|
|
225
|
+
return {
|
|
226
|
+
kind,
|
|
227
|
+
surfaces: {
|
|
228
|
+
uiClient,
|
|
229
|
+
ssr,
|
|
230
|
+
apiBackend
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
if (apiBackend.length > 0 && !backendFederation) throw new Error('surfaces.backendFederation is required when API/backend is declared.');
|
|
210
235
|
return {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
236
|
+
kind,
|
|
237
|
+
surfaces: {
|
|
238
|
+
uiClient,
|
|
239
|
+
ssr,
|
|
240
|
+
apiBackend,
|
|
241
|
+
...backendFederation ? {
|
|
242
|
+
backendFederation: {
|
|
243
|
+
manifest: assertNormalizedLogicalPath(backendFederation.manifest, 'surfaces.backendFederation.manifest'),
|
|
244
|
+
container: assertNormalizedLogicalPath(backendFederation.container, 'surfaces.backendFederation.container')
|
|
245
|
+
}
|
|
246
|
+
} : {}
|
|
217
247
|
}
|
|
218
248
|
};
|
|
219
249
|
};
|
|
@@ -223,12 +253,14 @@ const assertSurfaceReferences = (artifacts, surfaces)=>{
|
|
|
223
253
|
uiClient: surfaces.uiClient,
|
|
224
254
|
ssr: surfaces.ssr,
|
|
225
255
|
apiBackend: surfaces.apiBackend,
|
|
226
|
-
'backendFederation
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
256
|
+
...'backendFederation' in surfaces && surfaces.backendFederation ? {
|
|
257
|
+
'backendFederation.manifest': [
|
|
258
|
+
surfaces.backendFederation.manifest
|
|
259
|
+
],
|
|
260
|
+
'backendFederation.container': [
|
|
261
|
+
surfaces.backendFederation.container
|
|
262
|
+
]
|
|
263
|
+
} : {}
|
|
232
264
|
}))for (const logicalPath of paths)if (!artifactPaths.has(logicalPath)) throw new Error(`surfaces.${surface} references unbound artifact "${logicalPath}".`);
|
|
233
265
|
};
|
|
234
266
|
const assertTargetSurfaceContract = (target, artifacts, surfaces)=>{
|
|
@@ -246,12 +278,14 @@ const assertTargetSurfaceContract = (target, artifacts, surfaces)=>{
|
|
|
246
278
|
assertRuntime(surfaces.uiClient, 'browser', 'UI/client');
|
|
247
279
|
assertRuntime(surfaces.ssr, 'node' === target ? 'nodejs' : 'workerd', 'SSR');
|
|
248
280
|
assertRuntime(surfaces.apiBackend, 'node' === target ? 'nodejs' : 'workerd-effect', 'API/backend');
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
281
|
+
if ('backendFederation' in surfaces && surfaces.backendFederation) {
|
|
282
|
+
assertRuntime([
|
|
283
|
+
surfaces.backendFederation.manifest
|
|
284
|
+
], 'module-federation-manifest', 'backend federation manifest');
|
|
285
|
+
assertRuntime([
|
|
286
|
+
surfaces.backendFederation.container
|
|
287
|
+
], 'node' === target ? 'nodejs' : 'commonjs-module', 'backend federation container');
|
|
288
|
+
}
|
|
255
289
|
};
|
|
256
290
|
const deepFreeze = (value)=>{
|
|
257
291
|
if (null !== value && 'object' == typeof value && !Object.isFrozen(value)) {
|
|
@@ -260,6 +294,51 @@ const deepFreeze = (value)=>{
|
|
|
260
294
|
}
|
|
261
295
|
return value;
|
|
262
296
|
};
|
|
297
|
+
const assertReleaseUi = (value, identity, surfaces)=>{
|
|
298
|
+
if (0 === surfaces.uiClient.length) {
|
|
299
|
+
if (void 0 !== value) throw new Error('envelope.ui is forbidden for an API-only release.');
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
const ui = assertRecord(value, 'envelope.ui');
|
|
303
|
+
assertExactKeys(ui, [
|
|
304
|
+
'rendererIdentity',
|
|
305
|
+
'rendererProfile',
|
|
306
|
+
'routerBindings'
|
|
307
|
+
], 'envelope.ui');
|
|
308
|
+
const routerBindings = assertRecord(ui.routerBindings, 'envelope.ui.routerBindings');
|
|
309
|
+
const errors = [
|
|
310
|
+
...validateRendererIdentity(ui.rendererIdentity, 'envelope.ui.rendererIdentity').errors,
|
|
311
|
+
...validateRendererProfile(ui.rendererProfile, 'envelope.ui.rendererProfile').errors,
|
|
312
|
+
...validateRendererRouterBindings(routerBindings, Object.keys(routerBindings), 'envelope.ui.routerBindings').errors
|
|
313
|
+
];
|
|
314
|
+
if (errors.length > 0) throw new Error(formatBackendFederationValidationErrors(errors));
|
|
315
|
+
const rendererIdentity = ui.rendererIdentity;
|
|
316
|
+
const rendererProfile = ui.rendererProfile;
|
|
317
|
+
if (!Object.hasOwn(routerBindings, rendererIdentity.entryName)) throw new Error('envelope.ui.routerBindings must include the primary rendererIdentity.entryName.');
|
|
318
|
+
if (rendererIdentity.buildId !== identity.buildMarker) throw new Error('envelope.ui.rendererIdentity.buildId must match identity.buildMarker.');
|
|
319
|
+
for (const field of [
|
|
320
|
+
'renderer',
|
|
321
|
+
'protocolVersion'
|
|
322
|
+
])if (rendererIdentity[field] !== rendererProfile[field]) throw new Error(`envelope.ui.rendererProfile.${field} must match rendererIdentity.${field}.`);
|
|
323
|
+
return {
|
|
324
|
+
rendererIdentity: {
|
|
325
|
+
...rendererIdentity
|
|
326
|
+
},
|
|
327
|
+
rendererProfile: {
|
|
328
|
+
...rendererProfile,
|
|
329
|
+
compiler: {
|
|
330
|
+
...rendererProfile.compiler
|
|
331
|
+
},
|
|
332
|
+
hydration: {
|
|
333
|
+
...rendererProfile.hydration
|
|
334
|
+
},
|
|
335
|
+
router: {
|
|
336
|
+
...rendererProfile.router
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
routerBindings: immutableRendererRouterBindings(routerBindings)
|
|
340
|
+
};
|
|
341
|
+
};
|
|
263
342
|
const assertEnvelope = (value)=>{
|
|
264
343
|
const envelope = assertRecord(value, 'envelope');
|
|
265
344
|
assertExactKeys(envelope, [
|
|
@@ -267,26 +346,35 @@ const assertEnvelope = (value)=>{
|
|
|
267
346
|
'kind',
|
|
268
347
|
'target',
|
|
269
348
|
'identity',
|
|
349
|
+
...Object.hasOwn(envelope, 'ui') ? [
|
|
350
|
+
'ui'
|
|
351
|
+
] : [],
|
|
270
352
|
'artifacts',
|
|
271
353
|
'surfaces',
|
|
272
354
|
'envelopeDigest'
|
|
273
355
|
], 'envelope');
|
|
274
356
|
if (envelope.schemaVersion !== MICROVERTICAL_RELEASE_ENVELOPE_SCHEMA_VERSION) throw new Error(`envelope.schemaVersion must be ${MICROVERTICAL_RELEASE_ENVELOPE_SCHEMA_VERSION}.`);
|
|
275
|
-
|
|
357
|
+
const kind = assertEnvelopeKind(envelope.kind);
|
|
276
358
|
if (!Array.isArray(envelope.artifacts) || 0 === envelope.artifacts.length) throw new Error('envelope.artifacts must contain at least one artifact.');
|
|
277
359
|
const artifacts = envelope.artifacts.map((artifact, index)=>assertReleaseArtifact(artifact, `envelope.artifacts[${index}]`));
|
|
278
360
|
assertUniqueSortedArtifacts(artifacts, 'envelope.artifacts');
|
|
279
361
|
const target = assertTarget(envelope.target, 'envelope.target');
|
|
280
|
-
const
|
|
362
|
+
const releaseSurfaces = assertSurfaces(envelope.surfaces, kind);
|
|
363
|
+
const { surfaces } = releaseSurfaces;
|
|
364
|
+
if (0 === surfaces.uiClient.length && Object.hasOwn(envelope, 'ui')) throw new Error('envelope.ui is forbidden for an API-only release.');
|
|
281
365
|
assertSurfaceReferences(artifacts, surfaces);
|
|
282
366
|
assertTargetSurfaceContract(target, artifacts, surfaces);
|
|
367
|
+
const identity = assertReleaseIdentity(envelope.identity, 'envelope.identity');
|
|
368
|
+
const ui = assertReleaseUi(envelope.ui, identity, surfaces);
|
|
283
369
|
const parsed = {
|
|
370
|
+
...releaseSurfaces,
|
|
284
371
|
schemaVersion: MICROVERTICAL_RELEASE_ENVELOPE_SCHEMA_VERSION,
|
|
285
|
-
kind: MICROVERTICAL_RELEASE_ENVELOPE_KIND,
|
|
286
372
|
target,
|
|
287
|
-
identity
|
|
373
|
+
identity,
|
|
374
|
+
...ui ? {
|
|
375
|
+
ui
|
|
376
|
+
} : {},
|
|
288
377
|
artifacts,
|
|
289
|
-
surfaces,
|
|
290
378
|
envelopeDigest: assertNonEmptyString(envelope.envelopeDigest, 'envelope.envelopeDigest')
|
|
291
379
|
};
|
|
292
380
|
if (!SHA256_PATTERN.test(parsed.envelopeDigest)) throw new Error('envelope.envelopeDigest must be a lowercase SHA-256 digest.');
|
|
@@ -294,36 +382,59 @@ const assertEnvelope = (value)=>{
|
|
|
294
382
|
if (parsed.envelopeDigest !== expectedDigest) throw new Error('envelope.envelopeDigest does not match canonical payload.');
|
|
295
383
|
return parsed;
|
|
296
384
|
};
|
|
297
|
-
|
|
385
|
+
async function createMicroVerticalReleaseEnvelope(input) {
|
|
298
386
|
const artifactRoot = await promises.realpath(node_path.resolve(input.artifactRoot));
|
|
299
387
|
const target = assertTarget(input.target);
|
|
300
388
|
const identity = assertReleaseIdentity(input.identity);
|
|
389
|
+
const kind = assertEnvelopeKind(void 0 === input.kind ? MICROVERTICAL_RELEASE_ENVELOPE_KIND : input.kind);
|
|
301
390
|
if (!Array.isArray(input.artifacts) || 0 === input.artifacts.length) throw new Error('artifacts must contain at least one artifact.');
|
|
302
391
|
const inputs = input.artifacts.map((artifact, index)=>assertArtifactInput(artifact, `artifacts[${index}]`));
|
|
303
392
|
const sortedInputs = [
|
|
304
393
|
...inputs
|
|
305
394
|
].sort((left, right)=>left.logicalPath.localeCompare(right.logicalPath));
|
|
306
395
|
assertUniqueSortedArtifacts(sortedInputs, 'artifacts');
|
|
307
|
-
const
|
|
396
|
+
const releaseSurfaces = assertSurfaces(input.surfaces, kind);
|
|
397
|
+
const { surfaces } = releaseSurfaces;
|
|
398
|
+
if (0 === surfaces.uiClient.length && Object.hasOwn(input, 'ui')) throw new Error('envelope.ui is forbidden for an API-only release.');
|
|
308
399
|
assertSurfaceReferences(sortedInputs, surfaces);
|
|
309
400
|
assertTargetSurfaceContract(target, sortedInputs, surfaces);
|
|
401
|
+
const ui = assertReleaseUi(input.ui, identity, surfaces);
|
|
310
402
|
const artifacts = await Promise.all(sortedInputs.map((artifact)=>readFinalArtifact(artifactRoot, artifact)));
|
|
311
403
|
assertTargetSurfaceContract(target, artifacts, surfaces);
|
|
312
404
|
const payload = {
|
|
405
|
+
...releaseSurfaces,
|
|
313
406
|
schemaVersion: MICROVERTICAL_RELEASE_ENVELOPE_SCHEMA_VERSION,
|
|
314
|
-
kind: MICROVERTICAL_RELEASE_ENVELOPE_KIND,
|
|
315
407
|
target,
|
|
316
408
|
identity,
|
|
317
|
-
|
|
318
|
-
|
|
409
|
+
...ui ? {
|
|
410
|
+
ui
|
|
411
|
+
} : {},
|
|
412
|
+
artifacts
|
|
319
413
|
};
|
|
320
414
|
return deepFreeze({
|
|
321
415
|
...payload,
|
|
322
416
|
envelopeDigest: digestMicroVerticalReleaseEnvelopePayload(payload)
|
|
323
417
|
});
|
|
324
|
-
}
|
|
325
|
-
|
|
418
|
+
}
|
|
419
|
+
async function verifyMicroVerticalReleaseEnvelope(value, options) {
|
|
326
420
|
const envelope = assertEnvelope(value);
|
|
421
|
+
if (void 0 !== options.expectedRendererProfile) {
|
|
422
|
+
const result = validateRendererProfileCompatibility(options.expectedRendererProfile, envelope.ui?.rendererProfile, 'envelope.ui.rendererProfile');
|
|
423
|
+
if (!result.ok) throw new Error(formatBackendFederationValidationErrors(result.errors));
|
|
424
|
+
}
|
|
425
|
+
if (void 0 !== options.expectedRendererIdentity) {
|
|
426
|
+
const expected = options.expectedRendererIdentity;
|
|
427
|
+
const result = validateRendererIdentity(expected, 'expectedRendererIdentity');
|
|
428
|
+
if (!result.ok) throw new Error(formatBackendFederationValidationErrors(result.errors));
|
|
429
|
+
for (const field of [
|
|
430
|
+
'renderer',
|
|
431
|
+
'protocolVersion',
|
|
432
|
+
'appId',
|
|
433
|
+
'entryName',
|
|
434
|
+
'buildId'
|
|
435
|
+
])if (envelope.ui?.rendererIdentity[field] !== expected[field]) throw new Error(`envelope.ui.rendererIdentity.${field} must match the consuming renderer identity.`);
|
|
436
|
+
}
|
|
437
|
+
if (void 0 !== options.expectedKind && envelope.kind !== options.expectedKind) throw new Error(`envelope.kind must be "${options.expectedKind}" for this release owner; received "${envelope.kind}".`);
|
|
327
438
|
if (void 0 !== options.expectedTarget && envelope.target !== options.expectedTarget) throw new Error(`envelope.target must be "${options.expectedTarget}" for this staging target; received "${envelope.target}".`);
|
|
328
439
|
const artifactRoot = await promises.realpath(node_path.resolve(options.artifactRoot));
|
|
329
440
|
for (const artifact of envelope.artifacts){
|
|
@@ -332,5 +443,5 @@ const verifyMicroVerticalReleaseEnvelope = async (value, options)=>{
|
|
|
332
443
|
if (!matches) throw new Error('file' === artifact.kind && 'file' === finalArtifact.kind ? `Artifact "${artifact.logicalPath}" digest does not match final artifact bytes.` : `Artifact "${artifact.logicalPath}" does not match its final filesystem binding.`);
|
|
333
444
|
}
|
|
334
445
|
return deepFreeze(envelope);
|
|
335
|
-
}
|
|
336
|
-
export { MICROVERTICAL_RELEASE_ENVELOPE_KIND, MICROVERTICAL_RELEASE_ENVELOPE_SCHEMA_VERSION, MICROVERTICAL_RELEASE_TARGETS, canonicalSerializeMicroVerticalReleaseEnvelope, createMicroVerticalReleaseEnvelope, verifyMicroVerticalReleaseEnvelope };
|
|
446
|
+
}
|
|
447
|
+
export { MICROVERTICAL_RELEASE_ENVELOPE_KIND, MICROVERTICAL_RELEASE_ENVELOPE_SCHEMA_VERSION, MICROVERTICAL_RELEASE_TARGETS, SHELL_RELEASE_ENVELOPE_KIND, canonicalSerializeMicroVerticalReleaseEnvelope, createMicroVerticalReleaseEnvelope, verifyMicroVerticalReleaseEnvelope };
|
|
@@ -1,49 +1,58 @@
|
|
|
1
1
|
import "node:module";
|
|
2
2
|
import node_path from "node:path";
|
|
3
|
+
import { NODE_PUBLIC_ASSET_SCOPE, normalizeDeclaredPublicAssets, resolveAddedDeclaredPublicAssetPaths } from "../deploy-output/public-assets.mjs";
|
|
4
|
+
import { getDeployingTarget } from "../deploy-output/target.mjs";
|
|
3
5
|
import { emitFrameworkMicroVerticalReleaseEnvelope, emitNodeStagedReleaseEnvelope, verifyBuildOutputReleaseEnvelope, verifyNodeReleaseEnvelopeStaging } from "./framework-output.mjs";
|
|
4
|
-
const
|
|
5
|
-
const { metaName } = api.getAppContext();
|
|
6
|
-
const config = api.getNormalizedConfig();
|
|
7
|
-
if ('modern-js' !== metaName && !config.deploy?.target && !process.env.MODERNJS_DEPLOY) return;
|
|
8
|
-
return resolveDeployTarget(config);
|
|
9
|
-
};
|
|
10
|
-
const createUltramodernReleaseEnvelopePlugin = ({ resolveDeployTarget })=>({
|
|
6
|
+
const createUltramodernReleaseEnvelopePlugin = ({ resolveRendererProfile } = {})=>({
|
|
11
7
|
name: '@modern-js/ultramodern-release-envelope',
|
|
12
8
|
pre: [
|
|
13
9
|
'@modern-js/backend-federation-build',
|
|
14
10
|
'@modern-js/plugin-bff',
|
|
15
|
-
'@modern-js/deploy-output-aliases'
|
|
11
|
+
'@modern-js/deploy-output-aliases',
|
|
12
|
+
'@modern-js/deploy-output-public-assets'
|
|
16
13
|
],
|
|
17
14
|
post: [
|
|
18
15
|
'@modern-js/plugin-deploy'
|
|
19
16
|
],
|
|
20
17
|
setup (api) {
|
|
21
|
-
const emitBuildEnvelope = async (target)=>{
|
|
22
|
-
const { apiOnly, distDirectory } = api.getAppContext();
|
|
18
|
+
const emitBuildEnvelope = async (target, requirePromotable = true)=>{
|
|
19
|
+
const { apiOnly, appDirectory, distDirectory } = api.getAppContext();
|
|
23
20
|
await emitFrameworkMicroVerticalReleaseEnvelope({
|
|
24
21
|
apiOnly,
|
|
22
|
+
appDirectory,
|
|
25
23
|
distDirectory,
|
|
26
|
-
|
|
24
|
+
role: api.getNormalizedConfig().deploy?.releaseEnvelopeRole,
|
|
25
|
+
requirePromotable,
|
|
26
|
+
target,
|
|
27
|
+
...!apiOnly && resolveRendererProfile ? {
|
|
28
|
+
expectedRendererProfile: resolveRendererProfile(api.getNormalizedConfig())
|
|
29
|
+
} : {}
|
|
27
30
|
});
|
|
28
31
|
};
|
|
29
32
|
api.onAfterBuild(async ()=>{
|
|
30
|
-
const configuredTarget =
|
|
33
|
+
const configuredTarget = api.getAppContext().deployTarget?.target;
|
|
31
34
|
if ('node' !== configuredTarget && 'cloudflare' !== configuredTarget) return;
|
|
32
|
-
await emitBuildEnvelope(configuredTarget);
|
|
35
|
+
await emitBuildEnvelope(configuredTarget, false);
|
|
33
36
|
});
|
|
34
37
|
api.onBeforeDeploy(async ()=>{
|
|
35
|
-
const configuredTarget =
|
|
38
|
+
const configuredTarget = getDeployingTarget(api.getAppContext());
|
|
36
39
|
if ('node' !== configuredTarget) return;
|
|
37
40
|
await emitBuildEnvelope(configuredTarget);
|
|
38
41
|
const { distDirectory } = api.getAppContext();
|
|
39
42
|
await verifyBuildOutputReleaseEnvelope(distDirectory, configuredTarget);
|
|
40
43
|
});
|
|
41
44
|
api.onAfterDeploy(async ()=>{
|
|
42
|
-
const configuredTarget =
|
|
45
|
+
const configuredTarget = getDeployingTarget(api.getAppContext());
|
|
43
46
|
if ('node' !== configuredTarget) return;
|
|
44
47
|
const { appDirectory, distDirectory } = api.getAppContext();
|
|
45
48
|
const outputDirectory = node_path.join(appDirectory, '.output');
|
|
46
49
|
const releaseEnvelope = await emitNodeStagedReleaseEnvelope({
|
|
50
|
+
declaredPublicAssets: await resolveAddedDeclaredPublicAssetPaths({
|
|
51
|
+
appDirectory,
|
|
52
|
+
assets: normalizeDeclaredPublicAssets(api.getNormalizedConfig().deploy?.node?.publicAssets, NODE_PUBLIC_ASSET_SCOPE),
|
|
53
|
+
generatedRoot: distDirectory,
|
|
54
|
+
scope: NODE_PUBLIC_ASSET_SCOPE
|
|
55
|
+
}),
|
|
47
56
|
distDirectory,
|
|
48
57
|
outputDirectory
|
|
49
58
|
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import promises from "node:fs/promises";
|
|
4
|
+
import node_path from "node:path";
|
|
5
|
+
import { isDeepStrictEqual } from "node:util";
|
|
6
|
+
import { ULTRAMODERN_BUILD_ARTIFACT_FILE, ULTRAMODERN_BUILD_ARTIFACT_PATH, assertUltramodernBuildArtifact, formatBackendFederationValidationErrors, stampUltramodernBuildArtifactIdentity, validateRendererIdentity, validateRendererProfile, validateRendererRouterBindings } from "@modern-js/backend-federation-contracts";
|
|
7
|
+
const validateFinalizedRendererBuild = (output, context, appId)=>{
|
|
8
|
+
const entryNames = context.entrypoints.map((entry)=>entry.entryName);
|
|
9
|
+
const primary = context.entrypoints.find((entry)=>entry.isMainEntry) ?? context.entrypoints[0];
|
|
10
|
+
const errors = [
|
|
11
|
+
...validateRendererIdentity(output.ui.rendererIdentity).errors,
|
|
12
|
+
...validateRendererProfile(output.ui.rendererProfile).errors,
|
|
13
|
+
...validateRendererRouterBindings(output.ui.routerBindings, entryNames, 'routerBindings').errors
|
|
14
|
+
];
|
|
15
|
+
if (errors.length) throw new Error(formatBackendFederationValidationErrors(errors));
|
|
16
|
+
if (!primary || new Set(entryNames).size !== entryNames.length || context.entrypoints.filter((entry)=>entry.isMainEntry).length > 1 || output.ui.rendererIdentity.entryName !== primary.entryName || output.ui.rendererIdentity.appId !== appId || output.ui.rendererIdentity.buildId !== output.buildMarker || output.ui.rendererIdentity.renderer !== output.ui.rendererProfile.renderer) throw new Error('Finalized renderer output conflicts with the actual application entries and release identity.');
|
|
17
|
+
};
|
|
18
|
+
const stampFinalizedRendererBuildArtifact = (artifact, output, context)=>{
|
|
19
|
+
assertUltramodernBuildArtifact(artifact);
|
|
20
|
+
const ui = artifact.surfaces.ui;
|
|
21
|
+
if (!ui) throw new Error('Finalized renderer output requires a declared UI artifact');
|
|
22
|
+
validateFinalizedRendererBuild(output, context, artifact.deliveryUnit.appId);
|
|
23
|
+
const drifted = [
|
|
24
|
+
!isDeepStrictEqual({
|
|
25
|
+
...ui.rendererIdentity,
|
|
26
|
+
buildId: output.ui.rendererIdentity.buildId
|
|
27
|
+
}, output.ui.rendererIdentity) && `rendererIdentity (captured entry ${ui.rendererIdentity.entryName}, built entry ${output.ui.rendererIdentity.entryName})`,
|
|
28
|
+
!isDeepStrictEqual(ui.rendererProfile, output.ui.rendererProfile) && 'rendererProfile',
|
|
29
|
+
!isDeepStrictEqual(ui.routerBindings, output.ui.routerBindings) && `routerBindings (captured entries ${Object.keys(ui.routerBindings).join(', ')}, built entries ${Object.keys(output.ui.routerBindings).join(', ')})`
|
|
30
|
+
].filter(Boolean);
|
|
31
|
+
if (drifted.length) throw new Error(`Finalized renderer output conflicts with the captured application profile or router bindings: ${drifted.join('; ')} differ from ${ULTRAMODERN_BUILD_ARTIFACT_PATH}. After changing modern.config entries, renderer or router, recapture it with \`ultramodern-create ultramodern sync-delivery-unit\`.`);
|
|
32
|
+
const identity = {
|
|
33
|
+
buildMarker: output.buildMarker,
|
|
34
|
+
sourceRevision: output.sourceRevision
|
|
35
|
+
};
|
|
36
|
+
const stamped = stampUltramodernBuildArtifactIdentity(artifact, identity);
|
|
37
|
+
return stampUltramodernBuildArtifactIdentity({
|
|
38
|
+
...stamped,
|
|
39
|
+
surfaces: {
|
|
40
|
+
...stamped.surfaces,
|
|
41
|
+
ui: {
|
|
42
|
+
...stamped.surfaces.ui,
|
|
43
|
+
...output.ui
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}, identity);
|
|
47
|
+
};
|
|
48
|
+
async function writeOwnedArtifact(file, bytes) {
|
|
49
|
+
await promises.mkdir(node_path.dirname(file), {
|
|
50
|
+
recursive: true
|
|
51
|
+
});
|
|
52
|
+
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
53
|
+
let created = false;
|
|
54
|
+
try {
|
|
55
|
+
const handle = await promises.open(temporary, 'wx');
|
|
56
|
+
created = true;
|
|
57
|
+
try {
|
|
58
|
+
await handle.writeFile(bytes);
|
|
59
|
+
} finally{
|
|
60
|
+
await handle.close();
|
|
61
|
+
}
|
|
62
|
+
await promises.rename(temporary, file);
|
|
63
|
+
} finally{
|
|
64
|
+
if (created) await promises.rm(temporary, {
|
|
65
|
+
force: true
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async function emitRendererBuildArtifact(context, options) {
|
|
70
|
+
const source = node_path.join(context.appDirectory, ULTRAMODERN_BUILD_ARTIFACT_PATH);
|
|
71
|
+
const bytes = await promises.readFile(source).catch((error)=>{
|
|
72
|
+
if ('ENOENT' === error.code) return;
|
|
73
|
+
throw error;
|
|
74
|
+
});
|
|
75
|
+
if (!bytes) {
|
|
76
|
+
const declaration = await promises.stat(source.replace(/\.json$/u, '.ts')).catch((error)=>{
|
|
77
|
+
if ('ENOENT' === error.code) return;
|
|
78
|
+
throw error;
|
|
79
|
+
});
|
|
80
|
+
if (declaration) throw new Error('Generated UI artifact declaration is missing its build carrier');
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const artifact = JSON.parse(bytes.toString('utf8'));
|
|
84
|
+
assertUltramodernBuildArtifact(artifact);
|
|
85
|
+
const finalized = await options.resolveRendererBuild(context);
|
|
86
|
+
const stamped = stampFinalizedRendererBuildArtifact(artifact, finalized, context);
|
|
87
|
+
const serialized = `${JSON.stringify(stamped, null, 2)}\n`;
|
|
88
|
+
for (const directory of [
|
|
89
|
+
context.distDirectory,
|
|
90
|
+
node_path.join(context.distDirectory, 'public')
|
|
91
|
+
])await writeOwnedArtifact(node_path.join(directory, ULTRAMODERN_BUILD_ARTIFACT_FILE), serialized);
|
|
92
|
+
return stamped;
|
|
93
|
+
}
|
|
94
|
+
function rendererBuildArtifactStampPlugin(options) {
|
|
95
|
+
if ('string' != typeof options.rendererBuildPlugin || !options.rendererBuildPlugin || options.rendererBuildPlugin.trim() !== options.rendererBuildPlugin || 'function' != typeof options.resolveRendererBuild) throw new Error('Renderer artifact stamping requires its owning metadata producer');
|
|
96
|
+
return {
|
|
97
|
+
name: '@modern-js/renderer-build-artifact-stamp',
|
|
98
|
+
pre: [
|
|
99
|
+
options.rendererBuildPlugin
|
|
100
|
+
],
|
|
101
|
+
required: [
|
|
102
|
+
options.rendererBuildPlugin
|
|
103
|
+
],
|
|
104
|
+
setup (api) {
|
|
105
|
+
api.onAfterBuild(async ()=>{
|
|
106
|
+
const context = api.getAppContext();
|
|
107
|
+
if (context.apiOnly) return;
|
|
108
|
+
await emitRendererBuildArtifact(context, options);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
export { emitRendererBuildArtifact, rendererBuildArtifactStampPlugin, stampFinalizedRendererBuildArtifact, validateFinalizedRendererBuild };
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import "node:module";
|
|
2
|
-
const MICROVERTICAL_RELEASE_ENVELOPE_SCHEMA_VERSION = 3;
|
|
3
|
-
const MICROVERTICAL_RELEASE_ENVELOPE_KIND = 'ultramodern-target-microvertical-release-envelope';
|
|
4
2
|
const MICROVERTICAL_RELEASE_TARGETS = [
|
|
5
3
|
'node',
|
|
6
4
|
'cloudflare'
|
|
7
5
|
];
|
|
8
|
-
export { MICROVERTICAL_RELEASE_ENVELOPE_KIND, MICROVERTICAL_RELEASE_ENVELOPE_SCHEMA_VERSION,
|
|
6
|
+
export { MICROVERTICAL_RELEASE_ENVELOPE_KIND, MICROVERTICAL_RELEASE_ENVELOPE_SCHEMA_VERSION, SHELL_RELEASE_ENVELOPE_KIND } from "./contract.mjs";
|
|
7
|
+
export { MICROVERTICAL_RELEASE_TARGETS };
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import "node:module";
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
|
+
const invokeGit = execFileSync;
|
|
4
5
|
const BUILD_MARKER_NAMESPACE = 'ultramodern-delivery-unit-release-build-marker:v1';
|
|
5
6
|
const PROMOTABLE_SOURCE_REVISION_PATTERN = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u;
|
|
7
|
+
const workspaceGitEnv = ()=>Object.fromEntries(Object.entries(process.env).filter(([name])=>!name.toUpperCase().startsWith('GIT_')));
|
|
6
8
|
const gitOutput = (workspaceRoot, args)=>{
|
|
7
9
|
try {
|
|
8
|
-
return
|
|
10
|
+
return invokeGit('git', args, {
|
|
9
11
|
cwd: workspaceRoot,
|
|
10
12
|
encoding: 'utf8',
|
|
13
|
+
env: workspaceGitEnv(),
|
|
11
14
|
stdio: [
|
|
12
15
|
'ignore',
|
|
13
16
|
'pipe',
|
|
@@ -19,9 +22,9 @@ const gitOutput = (workspaceRoot, args)=>{
|
|
|
19
22
|
}
|
|
20
23
|
};
|
|
21
24
|
const createUltramodernReleaseBuildMarker = ({ generationBuildMarker, sourceRevision, unitId })=>createHash('sha256').update(`${BUILD_MARKER_NAMESPACE}:${unitId}:${generationBuildMarker}:${sourceRevision}`).digest('hex').slice(0, 16);
|
|
22
|
-
const resolveUltramodernSourceRevision = (workspaceRoot = process.cwd(), configuredSourceRevision)=>{
|
|
25
|
+
const resolveUltramodernSourceRevision = (workspaceRoot = process.cwd(), configuredSourceRevision, environment = process.env)=>{
|
|
23
26
|
const configuredArgumentValue = configuredSourceRevision?.trim();
|
|
24
|
-
const configuredEnvironmentValue =
|
|
27
|
+
const configuredEnvironmentValue = environment.ULTRAMODERN_SOURCE_REVISION?.trim();
|
|
25
28
|
const configuredArgument = configuredArgumentValue && 'workspace' !== configuredArgumentValue ? configuredArgumentValue : void 0;
|
|
26
29
|
const configuredEnvironment = configuredEnvironmentValue && 'workspace' !== configuredEnvironmentValue ? configuredEnvironmentValue : void 0;
|
|
27
30
|
if (configuredArgument && configuredEnvironment && configuredArgument !== configuredEnvironment) throw new Error(`Configured source revision ${configuredArgument} does not match environment source revision ${configuredEnvironment}.`);
|
|
@@ -49,9 +52,9 @@ const resolveUltramodernSourceRevision = (workspaceRoot = process.cwd(), configu
|
|
|
49
52
|
if (configured && configured !== revision) throw new Error(`Configured source revision ${configured} does not match clean Git HEAD ${revision}.`);
|
|
50
53
|
return revision;
|
|
51
54
|
};
|
|
52
|
-
const resolveUltramodernReleaseIdentity = ({ generationBuildMarker, sourceRevision: configuredSourceRevision, unitId, workspaceRoot })=>{
|
|
55
|
+
const resolveUltramodernReleaseIdentity = ({ generationBuildMarker, sourceRevision: configuredSourceRevision, unitId, workspaceRoot, environment })=>{
|
|
53
56
|
const explicitSourceRevision = configuredSourceRevision?.trim();
|
|
54
|
-
const sourceRevision = resolveUltramodernSourceRevision(workspaceRoot, explicitSourceRevision);
|
|
57
|
+
const sourceRevision = resolveUltramodernSourceRevision(workspaceRoot, explicitSourceRevision, environment);
|
|
55
58
|
return {
|
|
56
59
|
buildMarker: 'workspace' === sourceRevision ? generationBuildMarker : createUltramodernReleaseBuildMarker({
|
|
57
60
|
generationBuildMarker,
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
import node_fs from "node:fs";
|
|
4
|
+
import node_path from "node:path";
|
|
5
|
+
import { immutableRendererRouterBindings, validateRendererProfile, validateRendererRouterBindings } from "@modern-js/backend-federation-contracts";
|
|
6
|
+
import { resolveUltramodernSourceRevision } from "./release-identity.mjs";
|
|
7
|
+
const LOCKFILES = [
|
|
8
|
+
'pnpm-lock.yaml',
|
|
9
|
+
'package-lock.json',
|
|
10
|
+
'yarn.lock',
|
|
11
|
+
'bun.lock',
|
|
12
|
+
'bun.lockb'
|
|
13
|
+
];
|
|
14
|
+
const processNonce = randomUUID();
|
|
15
|
+
const sha256 = (value)=>createHash('sha256').update(value).digest('hex');
|
|
16
|
+
function rendererProfileKey(profile) {
|
|
17
|
+
return sha256(JSON.stringify([
|
|
18
|
+
profile.renderer,
|
|
19
|
+
profile.protocolVersion,
|
|
20
|
+
profile.compiler,
|
|
21
|
+
profile.hydration,
|
|
22
|
+
profile.router,
|
|
23
|
+
Object.entries(profile.dependencies ?? {}).sort(([left], [right])=>left < right ? -1 : left > right ? 1 : 0)
|
|
24
|
+
]));
|
|
25
|
+
}
|
|
26
|
+
function lockfileHash(projectRoot) {
|
|
27
|
+
let directory = node_path.resolve(projectRoot);
|
|
28
|
+
for(;;){
|
|
29
|
+
for (const name of LOCKFILES)try {
|
|
30
|
+
return sha256(node_fs.readFileSync(node_path.join(directory, name)));
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if ('ENOENT' !== error.code) throw error;
|
|
33
|
+
}
|
|
34
|
+
const parent = node_path.dirname(directory);
|
|
35
|
+
if (parent === directory) return '';
|
|
36
|
+
directory = parent;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function resolveRendererBuildIdentities(options) {
|
|
40
|
+
const profile = {
|
|
41
|
+
renderer: options.profile.renderer,
|
|
42
|
+
protocolVersion: options.profile.protocolVersion,
|
|
43
|
+
compiler: options.profile.compiler,
|
|
44
|
+
hydration: options.profile.hydration,
|
|
45
|
+
router: options.profile.router
|
|
46
|
+
};
|
|
47
|
+
const validation = validateRendererProfile(profile);
|
|
48
|
+
if (!validation.ok || options.renderer !== profile.renderer) throw new Error(`Renderer/profile identity mismatch: ${JSON.stringify(validation.errors)}.`);
|
|
49
|
+
const entries = [
|
|
50
|
+
...options.entryNames
|
|
51
|
+
].sort();
|
|
52
|
+
if (0 === entries.length || new Set(entries).size !== entries.length || entries.some((value)=>!value || '__proto__' === value)) throw new Error('Renderer build identity requires unique nonempty entry names.');
|
|
53
|
+
const routerValidation = validateRendererRouterBindings(options.routerBindings, entries, 'routerBindings', options.routerFrameworks);
|
|
54
|
+
if (!routerValidation.ok) throw new Error(`Invalid renderer router bindings: ${JSON.stringify(routerValidation.errors)}.`);
|
|
55
|
+
const projectRoot = node_path.resolve(options.projectRoot);
|
|
56
|
+
const appId = options.appId ?? JSON.parse(node_fs.readFileSync(node_path.join(projectRoot, 'package.json'), 'utf8')).name;
|
|
57
|
+
if ('string' != typeof appId || !appId || appId.trim() !== appId) throw new Error('Renderer application identity requires a delivery artifact appId or a package name.');
|
|
58
|
+
const profileKey = rendererProfileKey(options.profile);
|
|
59
|
+
const sourceRevision = resolveUltramodernSourceRevision(projectRoot, options.sourceRevision);
|
|
60
|
+
const buildId = sha256(JSON.stringify([
|
|
61
|
+
profileKey,
|
|
62
|
+
lockfileHash(projectRoot),
|
|
63
|
+
'development' === options.mode || 'workspace' === sourceRevision ? processNonce : sourceRevision
|
|
64
|
+
]));
|
|
65
|
+
const identities = Object.fromEntries(entries.map((entryName)=>[
|
|
66
|
+
entryName,
|
|
67
|
+
Object.freeze({
|
|
68
|
+
renderer: options.renderer,
|
|
69
|
+
appId,
|
|
70
|
+
entryName,
|
|
71
|
+
protocolVersion: 1,
|
|
72
|
+
buildId
|
|
73
|
+
})
|
|
74
|
+
]));
|
|
75
|
+
return Object.freeze({
|
|
76
|
+
identities: Object.freeze(identities),
|
|
77
|
+
buildId,
|
|
78
|
+
profileKey,
|
|
79
|
+
sourceRevision,
|
|
80
|
+
routerBindings: immutableRendererRouterBindings(options.routerBindings)
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
export { rendererProfileKey, resolveRendererBuildIdentities };
|