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