@bleedingdev/modern-js-create 3.5.0-ultramodern.27 → 3.5.0-ultramodern.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/ultramodern-tooling/commands.cjs +497 -155
- package/dist/cjs/ultramodern-tooling/config.cjs +149 -8
- package/dist/cjs/ultramodern-workspace/add-vertical.cjs +25 -0
- package/dist/cjs/ultramodern-workspace/api.cjs +44 -0
- package/dist/cjs/ultramodern-workspace/backend-federation.cjs +282 -0
- package/dist/cjs/ultramodern-workspace/contracts.cjs +24 -8
- package/dist/cjs/ultramodern-workspace/delivery-unit-sync.cjs +157 -0
- package/dist/cjs/ultramodern-workspace/delivery-unit.cjs +88 -0
- package/dist/cjs/ultramodern-workspace/descriptors.cjs +24 -0
- package/dist/cjs/ultramodern-workspace/module-federation.cjs +122 -44
- package/dist/cjs/ultramodern-workspace/package-json.cjs +11 -34
- package/dist/cjs/ultramodern-workspace/pnpm-workspace-policy-plan.cjs +166 -0
- package/dist/cjs/ultramodern-workspace/tooling-command-catalog.cjs +153 -0
- package/dist/cjs/ultramodern-workspace/workspace-script-plan.cjs +175 -0
- package/dist/cjs/ultramodern-workspace/workspace-scripts.cjs +43 -62
- package/dist/cjs/ultramodern-workspace/workspace-validation-contract.cjs +97 -0
- package/dist/cjs/ultramodern-workspace/write-workspace.cjs +9 -3
- package/dist/cjs/ultramodern-workspace/zerops.cjs +100 -0
- package/dist/esm/ultramodern-tooling/commands.js +500 -158
- package/dist/esm/ultramodern-tooling/config.js +144 -9
- package/dist/esm/ultramodern-workspace/add-vertical.js +26 -1
- package/dist/esm/ultramodern-workspace/api.js +43 -2
- package/dist/esm/ultramodern-workspace/backend-federation.js +217 -0
- package/dist/esm/ultramodern-workspace/contracts.js +24 -8
- package/dist/esm/ultramodern-workspace/delivery-unit-sync.js +108 -0
- package/dist/esm/ultramodern-workspace/delivery-unit.js +31 -0
- package/dist/esm/ultramodern-workspace/descriptors.js +13 -1
- package/dist/esm/ultramodern-workspace/module-federation.js +121 -36
- package/dist/esm/ultramodern-workspace/package-json.js +11 -31
- package/dist/esm/ultramodern-workspace/pnpm-workspace-policy-plan.js +113 -0
- package/dist/esm/ultramodern-workspace/tooling-command-catalog.js +100 -0
- package/dist/esm/ultramodern-workspace/workspace-script-plan.js +110 -0
- package/dist/esm/ultramodern-workspace/workspace-scripts.js +35 -63
- package/dist/esm/ultramodern-workspace/workspace-validation-contract.js +59 -0
- package/dist/esm/ultramodern-workspace/write-workspace.js +11 -5
- package/dist/esm/ultramodern-workspace/zerops.js +62 -0
- package/dist/esm-node/ultramodern-tooling/commands.js +500 -158
- package/dist/esm-node/ultramodern-tooling/config.js +144 -9
- package/dist/esm-node/ultramodern-workspace/add-vertical.js +26 -1
- package/dist/esm-node/ultramodern-workspace/api.js +43 -2
- package/dist/esm-node/ultramodern-workspace/backend-federation.js +218 -0
- package/dist/esm-node/ultramodern-workspace/contracts.js +24 -8
- package/dist/esm-node/ultramodern-workspace/delivery-unit-sync.js +109 -0
- package/dist/esm-node/ultramodern-workspace/delivery-unit.js +32 -0
- package/dist/esm-node/ultramodern-workspace/descriptors.js +13 -1
- package/dist/esm-node/ultramodern-workspace/module-federation.js +121 -36
- package/dist/esm-node/ultramodern-workspace/package-json.js +11 -31
- package/dist/esm-node/ultramodern-workspace/pnpm-workspace-policy-plan.js +114 -0
- package/dist/esm-node/ultramodern-workspace/tooling-command-catalog.js +101 -0
- package/dist/esm-node/ultramodern-workspace/workspace-script-plan.js +111 -0
- package/dist/esm-node/ultramodern-workspace/workspace-scripts.js +35 -63
- package/dist/esm-node/ultramodern-workspace/workspace-validation-contract.js +60 -0
- package/dist/esm-node/ultramodern-workspace/write-workspace.js +11 -5
- package/dist/esm-node/ultramodern-workspace/zerops.js +63 -0
- package/dist/types/ultramodern-tooling/config.d.ts +6 -0
- package/dist/types/ultramodern-workspace/api.d.ts +1 -0
- package/dist/types/ultramodern-workspace/backend-federation.d.ts +11 -0
- package/dist/types/ultramodern-workspace/contracts.d.ts +1 -1
- package/dist/types/ultramodern-workspace/delivery-unit-sync.d.ts +6 -0
- package/dist/types/ultramodern-workspace/delivery-unit.d.ts +27 -0
- package/dist/types/ultramodern-workspace/descriptors.d.ts +4 -0
- package/dist/types/ultramodern-workspace/module-federation.d.ts +4 -9
- package/dist/types/ultramodern-workspace/package-json.d.ts +0 -1
- package/dist/types/ultramodern-workspace/pnpm-workspace-policy-plan.d.ts +39 -0
- package/dist/types/ultramodern-workspace/tooling-command-catalog.d.ts +18 -0
- package/dist/types/ultramodern-workspace/workspace-script-plan.d.ts +72 -0
- package/dist/types/ultramodern-workspace/workspace-scripts.d.ts +3 -0
- package/dist/types/ultramodern-workspace/workspace-validation-contract.d.ts +42 -0
- package/dist/types/ultramodern-workspace/zerops.d.ts +2 -0
- package/package.json +3 -3
- package/template-workspace/.gitignore.handlebars +1 -0
- package/template-workspace/scripts/bootstrap-agent-skills.mjs +11 -3
- package/templates/workspace-scripts/check-ultramodern-api-boundaries.mts +40 -2
- package/templates/workspace-scripts/generate-node-backend-federation.mjs +270 -0
- package/templates/workspace-scripts/materialize-zerops-runtime.mjs +448 -0
- package/templates/workspace-scripts/proof-cloudflare-version.mjs +234 -8
- package/templates/workspace-scripts/proof-node-backend-federation.mjs +729 -0
- package/templates/workspace-scripts/ultramodern-cloudflare-proof.mjs +188 -2
- package/templates/workspace-scripts/validate-ultramodern-workspace.mjs.handlebars +322 -8
|
@@ -20,8 +20,8 @@ function resolveModuleFederationPublicPath(publicPath, manifestUrl) {
|
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
async function fetchText(url) {
|
|
24
|
-
const response = await fetch(url);
|
|
23
|
+
async function fetchText(url, init) {
|
|
24
|
+
const response = await fetch(url, init);
|
|
25
25
|
return {
|
|
26
26
|
ok: response.ok,
|
|
27
27
|
status: response.status,
|
|
@@ -48,6 +48,52 @@ function parseMaybeJson(body) {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
function jsonPathValue(value, path) {
|
|
52
|
+
const segments = String(path ?? '')
|
|
53
|
+
.split('.')
|
|
54
|
+
.filter(Boolean);
|
|
55
|
+
let current = value;
|
|
56
|
+
|
|
57
|
+
for (const segment of segments) {
|
|
58
|
+
if (current === null || current === undefined) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (Array.isArray(current) && /^\d+$/u.test(segment)) {
|
|
63
|
+
current = current[Number(segment)];
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (typeof current === 'object' && Object.hasOwn(current, segment)) {
|
|
68
|
+
current = current[segment];
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return current;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function jsonValuesEqual(left, right) {
|
|
79
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function normalizeJsonSmokeExpectations(check) {
|
|
83
|
+
if (Array.isArray(check.expectations)) {
|
|
84
|
+
return check.expectations;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (check.expect && typeof check.expect === 'object') {
|
|
88
|
+
return Object.entries(check.expect).map(([path, equals]) => ({
|
|
89
|
+
path,
|
|
90
|
+
equals,
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
|
|
51
97
|
function markerFromJson(value) {
|
|
52
98
|
if (!value || typeof value !== 'object') {
|
|
53
99
|
return undefined;
|
|
@@ -687,6 +733,27 @@ function validateUiMarkerEvidence(evidence, app, ssr) {
|
|
|
687
733
|
status: uiMarker === app.marker?.build ? 'pass' : 'fail',
|
|
688
734
|
});
|
|
689
735
|
assert(uiMarker === app.marker?.build, `${app.id} UI marker mismatch`);
|
|
736
|
+
|
|
737
|
+
if (app.deliveryUnit) {
|
|
738
|
+
evidence.assertions.push({
|
|
739
|
+
type: 'delivery-unit-ui-marker',
|
|
740
|
+
unitId: app.deliveryUnit.unitId,
|
|
741
|
+
expected: app.deliveryUnit.surfaces?.ui?.buildMarker ?? app.deliveryUnit.buildMarker,
|
|
742
|
+
actual: uiMarker,
|
|
743
|
+
status:
|
|
744
|
+
uiMarker === (app.deliveryUnit.surfaces?.ui?.buildMarker ?? app.deliveryUnit.buildMarker)
|
|
745
|
+
? 'pass'
|
|
746
|
+
: 'fail',
|
|
747
|
+
});
|
|
748
|
+
assert(
|
|
749
|
+
uiMarker !== undefined,
|
|
750
|
+
`${app.id} delivery unit ${app.deliveryUnit.unitId} is declared but SSR response is missing its build marker`,
|
|
751
|
+
);
|
|
752
|
+
assert(
|
|
753
|
+
uiMarker === (app.deliveryUnit.surfaces?.ui?.buildMarker ?? app.deliveryUnit.buildMarker),
|
|
754
|
+
`${app.id} delivery unit ${app.deliveryUnit.unitId} UI surface build marker mismatch`,
|
|
755
|
+
);
|
|
756
|
+
}
|
|
690
757
|
}
|
|
691
758
|
|
|
692
759
|
function validateCssRootMarkerEvidence(evidence, app, ssr) {
|
|
@@ -857,6 +924,123 @@ async function validateReadinessEvidence(evidence, app, publicUrl, routes) {
|
|
|
857
924
|
`${app.id} Effect readiness returned HTTP ${readiness.status}`,
|
|
858
925
|
);
|
|
859
926
|
assert(apiMarker === app.marker?.build, `${app.id} API marker mismatch`);
|
|
927
|
+
|
|
928
|
+
if (app.deliveryUnit) {
|
|
929
|
+
evidence.assertions.push({
|
|
930
|
+
type: 'delivery-unit-api-marker',
|
|
931
|
+
route: routes.apiReadiness,
|
|
932
|
+
unitId: app.deliveryUnit.unitId,
|
|
933
|
+
expected: app.deliveryUnit.buildMarker,
|
|
934
|
+
actual: apiMarker,
|
|
935
|
+
status: apiMarker === app.deliveryUnit.buildMarker ? 'pass' : 'fail',
|
|
936
|
+
});
|
|
937
|
+
assert(
|
|
938
|
+
apiMarker !== undefined,
|
|
939
|
+
`${app.id} delivery unit ${app.deliveryUnit.unitId} is declared but readiness response is missing its build marker`,
|
|
940
|
+
);
|
|
941
|
+
assert(
|
|
942
|
+
apiMarker === app.deliveryUnit.buildMarker,
|
|
943
|
+
`${app.id} delivery unit ${app.deliveryUnit.unitId} API surface build marker mismatch`,
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
async function validateServiceBindingEvidence(evidence, app, publicUrl) {
|
|
950
|
+
const serviceBindings = app.deploy?.cloudflare?.serviceBindings ?? [];
|
|
951
|
+
|
|
952
|
+
for (const binding of serviceBindings) {
|
|
953
|
+
const response = await fetchText(joinUrl(publicUrl, binding.route));
|
|
954
|
+
const responseJson = parseMaybeJson(response.body);
|
|
955
|
+
const apiMarker = markerFromJson(responseJson);
|
|
956
|
+
const passed = response.ok && apiMarker === binding.expectedMarker;
|
|
957
|
+
|
|
958
|
+
evidence.assertions.push({
|
|
959
|
+
type: 'service-binding-api-marker',
|
|
960
|
+
appId: binding.appId,
|
|
961
|
+
binding: binding.binding,
|
|
962
|
+
route: binding.route,
|
|
963
|
+
expected: binding.expectedMarker,
|
|
964
|
+
actual: apiMarker,
|
|
965
|
+
status: passed ? 'pass' : 'fail',
|
|
966
|
+
statusCode: response.status,
|
|
967
|
+
});
|
|
968
|
+
assert(
|
|
969
|
+
response.ok,
|
|
970
|
+
`${app.id} service binding ${binding.binding} returned HTTP ${response.status}`,
|
|
971
|
+
);
|
|
972
|
+
assert(
|
|
973
|
+
apiMarker === binding.expectedMarker,
|
|
974
|
+
`${app.id} service binding ${binding.binding} API marker mismatch`,
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
async function validateJsonSmokeEvidence(evidence, app, publicUrl) {
|
|
980
|
+
const checks = app.deploy?.cloudflare?.jsonSmokeChecks ?? [];
|
|
981
|
+
|
|
982
|
+
for (const check of checks) {
|
|
983
|
+
const route = check.route;
|
|
984
|
+
assert(
|
|
985
|
+
typeof route === 'string' && route.length > 0,
|
|
986
|
+
`${app.id} JSON smoke check is missing a route`,
|
|
987
|
+
);
|
|
988
|
+
|
|
989
|
+
const method = String(check.method ?? 'GET').toUpperCase();
|
|
990
|
+
const requestBody =
|
|
991
|
+
check.body ?? check.payload ?? check.requestBody ?? undefined;
|
|
992
|
+
const response = await fetchText(joinUrl(publicUrl, route), {
|
|
993
|
+
method,
|
|
994
|
+
headers: {
|
|
995
|
+
accept: 'application/json',
|
|
996
|
+
...(requestBody === undefined
|
|
997
|
+
? {}
|
|
998
|
+
: { 'content-type': 'application/json' }),
|
|
999
|
+
},
|
|
1000
|
+
...(requestBody === undefined
|
|
1001
|
+
? {}
|
|
1002
|
+
: { body: JSON.stringify(requestBody) }),
|
|
1003
|
+
});
|
|
1004
|
+
const responseJson = parseMaybeJson(response.body);
|
|
1005
|
+
const checkId = check.id ?? route;
|
|
1006
|
+
|
|
1007
|
+
evidence.assertions.push({
|
|
1008
|
+
type: 'json-smoke-http',
|
|
1009
|
+
id: checkId,
|
|
1010
|
+
method,
|
|
1011
|
+
route,
|
|
1012
|
+
status: response.ok && responseJson !== undefined ? 'pass' : 'fail',
|
|
1013
|
+
statusCode: response.status,
|
|
1014
|
+
});
|
|
1015
|
+
assert(
|
|
1016
|
+
response.ok,
|
|
1017
|
+
`${app.id} JSON smoke ${checkId} returned HTTP ${response.status}`,
|
|
1018
|
+
);
|
|
1019
|
+
assert(
|
|
1020
|
+
responseJson !== undefined,
|
|
1021
|
+
`${app.id} JSON smoke ${checkId} did not return JSON`,
|
|
1022
|
+
);
|
|
1023
|
+
|
|
1024
|
+
for (const expectation of normalizeJsonSmokeExpectations(check)) {
|
|
1025
|
+
const actual = jsonPathValue(responseJson, expectation.path);
|
|
1026
|
+
const passed = jsonValuesEqual(actual, expectation.equals);
|
|
1027
|
+
|
|
1028
|
+
evidence.assertions.push({
|
|
1029
|
+
type: 'json-smoke-value',
|
|
1030
|
+
id: checkId,
|
|
1031
|
+
route,
|
|
1032
|
+
path: expectation.path,
|
|
1033
|
+
expected: expectation.equals,
|
|
1034
|
+
actual,
|
|
1035
|
+
status: passed ? 'pass' : 'fail',
|
|
1036
|
+
});
|
|
1037
|
+
assert(
|
|
1038
|
+
passed,
|
|
1039
|
+
`${app.id} JSON smoke ${checkId} expected ${expectation.path} to equal ${JSON.stringify(
|
|
1040
|
+
expectation.equals,
|
|
1041
|
+
)}`,
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
860
1044
|
}
|
|
861
1045
|
}
|
|
862
1046
|
|
|
@@ -869,6 +1053,8 @@ async function validateApp(app, publicUrl) {
|
|
|
869
1053
|
await validateModuleFederationManifestEvidence(evidence, app, publicUrl, routes);
|
|
870
1054
|
await validateI18nEvidence(evidence, app, publicUrl, routes);
|
|
871
1055
|
await validateReadinessEvidence(evidence, app, publicUrl, routes);
|
|
1056
|
+
await validateServiceBindingEvidence(evidence, app, publicUrl);
|
|
1057
|
+
await validateJsonSmokeEvidence(evidence, app, publicUrl);
|
|
872
1058
|
|
|
873
1059
|
return evidence;
|
|
874
1060
|
}
|
|
@@ -53,6 +53,14 @@ const assertExists = relativePath => {
|
|
|
53
53
|
const assertNotExists = relativePath => {
|
|
54
54
|
assert(!fs.existsSync(path.join(root, relativePath)), `Unexpected ${relativePath}`);
|
|
55
55
|
};
|
|
56
|
+
const assertAnyOf = relativePaths => {
|
|
57
|
+
assert(
|
|
58
|
+
relativePaths.some(relativePath =>
|
|
59
|
+
fs.existsSync(path.join(root, relativePath)),
|
|
60
|
+
),
|
|
61
|
+
`Missing one of: ${relativePaths.join(', ')}`,
|
|
62
|
+
);
|
|
63
|
+
};
|
|
56
64
|
const formatJson = value =>
|
|
57
65
|
value === undefined ? 'undefined' : JSON.stringify(value);
|
|
58
66
|
const selfCheckFailure = (contract, message, fixArea) =>
|
|
@@ -136,6 +144,28 @@ const buildMarkerFor = app =>
|
|
|
136
144
|
.update(`${packageScope}:${app.packageSuffix}:${app.id}:0.1.0`)
|
|
137
145
|
.digest('hex')
|
|
138
146
|
.slice(0, 16);
|
|
147
|
+
const deliveryUnitIdentityFixArea =
|
|
148
|
+
'regenerate vertical identity from delivery-unit record; do not hand-edit surface markers';
|
|
149
|
+
const deliveryUnitBlock = record => ({
|
|
150
|
+
schemaVersion: record?.schemaVersion,
|
|
151
|
+
kind: record?.kind,
|
|
152
|
+
unitId: record?.unitId,
|
|
153
|
+
packageName: record?.packageName,
|
|
154
|
+
version: record?.version,
|
|
155
|
+
buildMarker: record?.buildMarker,
|
|
156
|
+
sourceRevision: record?.sourceRevision,
|
|
157
|
+
});
|
|
158
|
+
const assertDeliveryUnitMarkerSpread = (source, exportName, contract) => {
|
|
159
|
+
const spreadPattern = new RegExp(
|
|
160
|
+
`export const ${exportName} = \\{\\s*\\.\\.\\.ultramodernDeliveryUnit,`,
|
|
161
|
+
);
|
|
162
|
+
assertSelfCheck(
|
|
163
|
+
spreadPattern.test(source),
|
|
164
|
+
contract,
|
|
165
|
+
`${exportName} must spread ...ultramodernDeliveryUnit rather than hand-forking per-surface fields`,
|
|
166
|
+
deliveryUnitIdentityFixArea,
|
|
167
|
+
);
|
|
168
|
+
};
|
|
139
169
|
const normalizeCompactApp = rawApp => {
|
|
140
170
|
const id = String(rawApp.id);
|
|
141
171
|
const kind = rawApp.kind === 'vertical' ? 'vertical' : 'shell';
|
|
@@ -772,6 +802,107 @@ const expectedManifestUrl = vertical =>
|
|
|
772
802
|
`http://localhost:${vertical.port}/mf-manifest.json`;
|
|
773
803
|
const expectedApiUrl = vertical =>
|
|
774
804
|
`http://localhost:${vertical.port}${vertical.apiPrefix}`;
|
|
805
|
+
const expectedBackendFederationName = vertical => `${vertical.mfName}Backend`;
|
|
806
|
+
const expectedBackendManifestUrl = vertical =>
|
|
807
|
+
`http://localhost:${vertical.port}/backend-mf-manifest.json`;
|
|
808
|
+
const expectedBackendContainerEntry = vertical =>
|
|
809
|
+
`http://localhost:${vertical.port}/backendRemoteEntry.mjs`;
|
|
810
|
+
const expectedBackendManifestEnv = vertical =>
|
|
811
|
+
`VERTICAL_${toEnvSegment(vertical.domain ?? vertical.id)}_BACKEND_MF_MANIFEST`;
|
|
812
|
+
const expectedPublicUrlEnv = vertical =>
|
|
813
|
+
`ULTRAMODERN_PUBLIC_URL_${toEnvSegment(vertical.id)}`;
|
|
814
|
+
const expectedCloudflareWorkerName = vertical =>
|
|
815
|
+
toKebabCase(`${packageScope}-${vertical.packageSuffix ?? vertical.id}`).slice(
|
|
816
|
+
0,
|
|
817
|
+
63,
|
|
818
|
+
);
|
|
819
|
+
const backendFederationSubset = backendFederation => ({
|
|
820
|
+
role: backendFederation?.role,
|
|
821
|
+
name: backendFederation?.name,
|
|
822
|
+
runtimeFramework: backendFederation?.runtimeFramework,
|
|
823
|
+
strictEffectApproach: backendFederation?.strictEffectApproach,
|
|
824
|
+
exposeRuntime: backendFederation?.exposes?.['./effect-api']?.runtime,
|
|
825
|
+
exposeReadiness: backendFederation?.exposes?.['./effect-api']?.readiness,
|
|
826
|
+
versionBoundary: {
|
|
827
|
+
invariant: backendFederation?.versionBoundary?.invariant,
|
|
828
|
+
uiManifestUrl: backendFederation?.versionBoundary?.ui?.manifestUrl,
|
|
829
|
+
apiReadiness: backendFederation?.versionBoundary?.api?.readiness,
|
|
830
|
+
},
|
|
831
|
+
cloudflare: {
|
|
832
|
+
kind: backendFederation?.executionSurfaces?.cloudflare?.kind,
|
|
833
|
+
workerName: backendFederation?.executionSurfaces?.cloudflare?.workerName,
|
|
834
|
+
publicUrlEnv:
|
|
835
|
+
backendFederation?.executionSurfaces?.cloudflare?.publicUrlEnv,
|
|
836
|
+
zephyrRuntime:
|
|
837
|
+
backendFederation?.executionSurfaces?.cloudflare?.zephyr?.runtime,
|
|
838
|
+
},
|
|
839
|
+
node: {
|
|
840
|
+
kind: backendFederation?.executionSurfaces?.node?.kind,
|
|
841
|
+
remoteName: backendFederation?.executionSurfaces?.node?.remoteName,
|
|
842
|
+
manifestEnv: backendFederation?.executionSurfaces?.node?.manifestEnv,
|
|
843
|
+
manifestUrl: backendFederation?.executionSurfaces?.node?.manifestUrl,
|
|
844
|
+
containerEntry:
|
|
845
|
+
backendFederation?.executionSurfaces?.node?.containerEntry,
|
|
846
|
+
remoteType: backendFederation?.executionSurfaces?.node?.remoteType,
|
|
847
|
+
expose: backendFederation?.executionSurfaces?.node?.expose,
|
|
848
|
+
},
|
|
849
|
+
compatibility: {
|
|
850
|
+
contractVersion: backendFederation?.compatibility?.contractVersion,
|
|
851
|
+
},
|
|
852
|
+
topLevelManifestUrl: backendFederation?.manifestUrl,
|
|
853
|
+
topLevelContainerEntry: backendFederation?.containerEntry,
|
|
854
|
+
});
|
|
855
|
+
const expectedBackendFederationSubset = vertical => ({
|
|
856
|
+
role: 'microvertical-server',
|
|
857
|
+
name: expectedBackendFederationName(vertical),
|
|
858
|
+
runtimeFramework: 'effect',
|
|
859
|
+
strictEffectApproach: true,
|
|
860
|
+
exposeRuntime: `${vertical.path}/api/index.ts`,
|
|
861
|
+
exposeReadiness: `${vertical.apiPrefix}/${vertical.stem}/readiness`,
|
|
862
|
+
versionBoundary: {
|
|
863
|
+
invariant: 'web-and-api-same-build',
|
|
864
|
+
uiManifestUrl: expectedManifestUrl(vertical),
|
|
865
|
+
apiReadiness: `${vertical.apiPrefix}/${vertical.stem}/readiness`,
|
|
866
|
+
},
|
|
867
|
+
cloudflare: {
|
|
868
|
+
kind: 'cloudflare-worker-snapshot',
|
|
869
|
+
workerName: expectedCloudflareWorkerName(vertical),
|
|
870
|
+
publicUrlEnv: expectedPublicUrlEnv(vertical),
|
|
871
|
+
zephyrRuntime: 'ssr-worker',
|
|
872
|
+
},
|
|
873
|
+
node: {
|
|
874
|
+
kind: 'node-mf-runtime',
|
|
875
|
+
remoteName: expectedBackendFederationName(vertical),
|
|
876
|
+
manifestEnv: expectedBackendManifestEnv(vertical),
|
|
877
|
+
manifestUrl: expectedBackendManifestUrl(vertical),
|
|
878
|
+
containerEntry: expectedBackendContainerEntry(vertical),
|
|
879
|
+
remoteType: 'module',
|
|
880
|
+
expose: './effect-api',
|
|
881
|
+
},
|
|
882
|
+
compatibility: {
|
|
883
|
+
contractVersion: 'microvertical-server-effect-v1',
|
|
884
|
+
},
|
|
885
|
+
topLevelManifestUrl: undefined,
|
|
886
|
+
topLevelContainerEntry: undefined,
|
|
887
|
+
});
|
|
888
|
+
const serverExecutionSubset = serverExecution => ({
|
|
889
|
+
apiBaseUrl: serverExecution?.apiBaseUrl,
|
|
890
|
+
versionBoundary: serverExecution?.versionBoundary,
|
|
891
|
+
cloudflareKind: serverExecution?.cloudflare?.kind,
|
|
892
|
+
cloudflareWorkerName: serverExecution?.cloudflare?.workerName,
|
|
893
|
+
nodeKind: serverExecution?.node?.kind,
|
|
894
|
+
nodeManifestUrl: serverExecution?.node?.manifestUrl,
|
|
895
|
+
nodeContainerEntry: serverExecution?.node?.containerEntry,
|
|
896
|
+
});
|
|
897
|
+
const expectedServerExecutionSubset = vertical => ({
|
|
898
|
+
apiBaseUrl: expectedApiUrl(vertical),
|
|
899
|
+
versionBoundary: 'web-and-api-same-build',
|
|
900
|
+
cloudflareKind: 'cloudflare-worker-snapshot',
|
|
901
|
+
cloudflareWorkerName: expectedCloudflareWorkerName(vertical),
|
|
902
|
+
nodeKind: 'node-mf-runtime',
|
|
903
|
+
nodeManifestUrl: expectedBackendManifestUrl(vertical),
|
|
904
|
+
nodeContainerEntry: expectedBackendContainerEntry(vertical),
|
|
905
|
+
});
|
|
775
906
|
const remoteContractSubset = remote => ({
|
|
776
907
|
id: remote?.id,
|
|
777
908
|
name: remote?.name,
|
|
@@ -791,9 +922,11 @@ const requiredMicroVerticalPaths = vertical => [
|
|
|
791
922
|
`${vertical.path}/package.json`,
|
|
792
923
|
`${vertical.path}/tsconfig.json`,
|
|
793
924
|
`${vertical.path}/tsconfig.mf-types.json`,
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
925
|
+
`${vertical.path}/modern.config.ts`,
|
|
926
|
+
`${vertical.path}/module-federation.config.ts`,
|
|
927
|
+
`${vertical.path}/backend-federation.config.ts`,
|
|
928
|
+
`${vertical.path}/api/effect-api.ts`,
|
|
929
|
+
`${vertical.path}/api/index.ts`,
|
|
797
930
|
`${vertical.path}/shared/api.ts`,
|
|
798
931
|
`${vertical.path}/src/api/${vertical.stem}-client.ts`,
|
|
799
932
|
`${vertical.path}/src/modern-app-env.d.ts`,
|
|
@@ -988,6 +1121,34 @@ const assertMicroVerticalContractGraph = ({
|
|
|
988
1121
|
`topology/reference-topology.json verticals.${vertical.id}`,
|
|
989
1122
|
'restore generated topology vertical entries',
|
|
990
1123
|
);
|
|
1124
|
+
assertSameJson(
|
|
1125
|
+
backendFederationSubset(topologyEntry.backendFederation),
|
|
1126
|
+
expectedBackendFederationSubset(vertical),
|
|
1127
|
+
`topology/reference-topology.json verticals.${vertical.id}.backendFederation`,
|
|
1128
|
+
'restore generated MicroVertical server execution contract',
|
|
1129
|
+
);
|
|
1130
|
+
|
|
1131
|
+
if (vertical.deliveryUnit) {
|
|
1132
|
+
const expectedDeliveryUnit = deliveryUnitBlock(vertical.deliveryUnit);
|
|
1133
|
+
assertSameJson(
|
|
1134
|
+
deliveryUnitBlock(topologyEntry.deliveryUnit),
|
|
1135
|
+
expectedDeliveryUnit,
|
|
1136
|
+
`topology/reference-topology.json verticals.${vertical.id}.deliveryUnit`,
|
|
1137
|
+
deliveryUnitIdentityFixArea,
|
|
1138
|
+
);
|
|
1139
|
+
assertSameJson(
|
|
1140
|
+
deliveryUnitBlock(topologyEntry.backendFederation?.deliveryUnit),
|
|
1141
|
+
expectedDeliveryUnit,
|
|
1142
|
+
`topology/reference-topology.json verticals.${vertical.id}.backendFederation.deliveryUnit`,
|
|
1143
|
+
deliveryUnitIdentityFixArea,
|
|
1144
|
+
);
|
|
1145
|
+
assertSelfCheck(
|
|
1146
|
+
topologyEntry.backendFederation?.versionBoundary?.identityRoot === 'deliveryUnit',
|
|
1147
|
+
`topology/reference-topology.json verticals.${vertical.id}.backendFederation.versionBoundary.identityRoot`,
|
|
1148
|
+
`Expected "deliveryUnit", found ${formatJson(topologyEntry.backendFederation?.versionBoundary?.identityRoot)}`,
|
|
1149
|
+
deliveryUnitIdentityFixArea,
|
|
1150
|
+
);
|
|
1151
|
+
}
|
|
991
1152
|
|
|
992
1153
|
assertObject(
|
|
993
1154
|
ownershipEntry,
|
|
@@ -1025,6 +1186,13 @@ const assertMicroVerticalContractGraph = ({
|
|
|
1025
1186
|
`topology/local-overlays/development.json apis.${vertical.id}`,
|
|
1026
1187
|
'restore generated local API overlay',
|
|
1027
1188
|
);
|
|
1189
|
+
assertSameJson(
|
|
1190
|
+
serverExecutionSubset(overlay.serverExecution?.[vertical.id]),
|
|
1191
|
+
expectedServerExecutionSubset(vertical),
|
|
1192
|
+
`topology/local-overlays/development.json serverExecution.${vertical.id}`,
|
|
1193
|
+
'restore generated local MicroVertical server execution overlay',
|
|
1194
|
+
);
|
|
1195
|
+
|
|
1028
1196
|
|
|
1029
1197
|
assertSameJson(
|
|
1030
1198
|
shellPackage.dependencies?.[vertical.packageName],
|
|
@@ -1230,7 +1398,7 @@ const packageJsonFiles = startDir => {
|
|
|
1230
1398
|
const current = queue.shift();
|
|
1231
1399
|
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
1232
1400
|
if (
|
|
1233
|
-
|
|
1401
|
+
['.git', '.output', '.zerops', 'dist', 'node_modules', 'repos'].includes(
|
|
1234
1402
|
entry.name,
|
|
1235
1403
|
)
|
|
1236
1404
|
) {
|
|
@@ -1513,8 +1681,6 @@ const requiredPaths = [
|
|
|
1513
1681
|
'.github/renovate.json',
|
|
1514
1682
|
'.github/workflows/ultramodern-workspace-gates.yml',
|
|
1515
1683
|
'.agents/agent-reference-repos.json',
|
|
1516
|
-
'.codex/skills-lock.json',
|
|
1517
|
-
'.codex/rstackjs-agent-skills-LICENSE',
|
|
1518
1684
|
'topology/reference-topology.json',
|
|
1519
1685
|
'topology/ownership.json',
|
|
1520
1686
|
'topology/local-overlays/development.json',
|
|
@@ -1522,13 +1688,17 @@ const requiredPaths = [
|
|
|
1522
1688
|
'scripts/bootstrap-agent-skills.mts',
|
|
1523
1689
|
'scripts/check-ultramodern-api-boundaries.mts',
|
|
1524
1690
|
'scripts/check-ultramodern-i18n-boundaries.mts',
|
|
1691
|
+
'scripts/generate-node-backend-federation.mts',
|
|
1525
1692
|
'scripts/generate-public-surface-assets.mts',
|
|
1693
|
+
'scripts/generate-tanstack-routes.mts',
|
|
1526
1694
|
'scripts/proof-cloudflare-version.mts',
|
|
1695
|
+
'scripts/proof-node-backend-federation.mts',
|
|
1527
1696
|
'scripts/setup-agent-reference-repos.mts',
|
|
1528
1697
|
'scripts/ultramodern-performance-readiness.config.mjs',
|
|
1529
1698
|
'scripts/ultramodern-performance-readiness.mts',
|
|
1530
1699
|
'scripts/ultramodern-typecheck.mts',
|
|
1531
1700
|
'scripts/validate-ultramodern-workspace.mts',
|
|
1701
|
+
'scripts/verify-cloudflare-output.mts',
|
|
1532
1702
|
'apps/shell-super-app/package.json',
|
|
1533
1703
|
'apps/shell-super-app/tsconfig.json',
|
|
1534
1704
|
'apps/shell-super-app/tsconfig.mf-types.json',
|
|
@@ -1581,6 +1751,13 @@ for (const vertical of fullStackVerticals) {
|
|
|
1581
1751
|
for (const requiredPath of requiredPaths) {
|
|
1582
1752
|
assertExists(requiredPath);
|
|
1583
1753
|
}
|
|
1754
|
+
// Agent skills metadata may live under .agents/ (agents-standard layout) or
|
|
1755
|
+
// .codex/ (legacy scaffold default).
|
|
1756
|
+
assertAnyOf(['.agents/skills-lock.json', '.codex/skills-lock.json']);
|
|
1757
|
+
assertAnyOf([
|
|
1758
|
+
'.agents/rstackjs-agent-skills-LICENSE',
|
|
1759
|
+
'.codex/rstackjs-agent-skills-LICENSE',
|
|
1760
|
+
]);
|
|
1584
1761
|
const pnpmWorkspace = readText('pnpm-workspace.yaml');
|
|
1585
1762
|
assert(
|
|
1586
1763
|
pnpmWorkspace.includes("'@effect/opentelemetry': {{effectVersion}}"),
|
|
@@ -1676,6 +1853,11 @@ assert(
|
|
|
1676
1853
|
expectedModernPackageSpecifier('@modern-js/code-tools'),
|
|
1677
1854
|
'Root must depend on @modern-js/code-tools through package source metadata',
|
|
1678
1855
|
);
|
|
1856
|
+
assert(
|
|
1857
|
+
rootPackage.devDependencies?.['@modern-js/plugin-bff'] ===
|
|
1858
|
+
expectedModernPackageSpecifier('@modern-js/plugin-bff'),
|
|
1859
|
+
'Root must depend on @modern-js/plugin-bff for Node backend federation proof',
|
|
1860
|
+
);
|
|
1679
1861
|
if (packageSource.strategy === 'install') {
|
|
1680
1862
|
const installSpecifier = packageSource.modernPackages?.specifier;
|
|
1681
1863
|
assert(
|
|
@@ -1735,21 +1917,60 @@ if (bridgeConfig) {
|
|
|
1735
1917
|
}
|
|
1736
1918
|
assert(rootPackage.scripts?.['bridge:check'], 'Bridge workspaces must expose bridge:check');
|
|
1737
1919
|
} else {
|
|
1738
|
-
assert(rootPackage.scripts?.typecheck === 'node ./scripts/ultramodern-typecheck.mts --
|
|
1920
|
+
assert(rootPackage.scripts?.typecheck === 'node ./scripts/ultramodern-typecheck.mts --project tsconfig.json', 'Root typecheck must run TS-Go across the root project reference graph');
|
|
1739
1921
|
}
|
|
1740
1922
|
assert(rootPackage.scripts?.['contract:check'] === 'node ./scripts/validate-ultramodern-workspace.mts', 'Root must expose contract:check');
|
|
1741
1923
|
assert(rootPackage.scripts?.['api:check'] === 'node ./scripts/check-ultramodern-api-boundaries.mts', 'Root must expose api:check');
|
|
1742
1924
|
assert(rootPackage.scripts?.['i18n:boundaries'] === 'node ./scripts/check-ultramodern-i18n-boundaries.mts', 'Root must expose i18n:boundaries');
|
|
1743
1925
|
assert(rootPackage.scripts?.['performance:readiness'] === 'node ./scripts/ultramodern-performance-readiness.mts', 'Root must expose default-on performance readiness diagnostics');
|
|
1926
|
+
assert(rootPackage.scripts?.['node:backend-federation:generate'] === 'node ./scripts/generate-node-backend-federation.mts', 'Root must expose local Node backend federation artifact generation');
|
|
1927
|
+
assert(rootPackage.scripts?.['zerops:materialize'] === 'node ./scripts/materialize-zerops-runtime.mjs', 'Root must expose Zerops runtime materialization script');
|
|
1928
|
+
assertNotExists('scripts/generate-node-backend-federation.mjs');
|
|
1929
|
+
assertNotExists('scripts/proof-node-backend-federation.mjs');
|
|
1930
|
+
assertNotExists('scripts/verify-cloudflare-output.mjs');
|
|
1931
|
+
assertNotExists('scripts/generate-tanstack-routes.mjs');
|
|
1932
|
+
assert(
|
|
1933
|
+
rootPackage.scripts?.['node:proof'] ===
|
|
1934
|
+
'node ./scripts/proof-node-backend-federation.mts',
|
|
1935
|
+
'Root must expose node:proof for backend federation Effect modules',
|
|
1936
|
+
);
|
|
1744
1937
|
assert(
|
|
1745
1938
|
bridgeConfig
|
|
1746
1939
|
? rootPackage.scripts?.check?.includes('pnpm api:check') &&
|
|
1940
|
+
rootPackage.scripts.check.includes('pnpm node:proof') &&
|
|
1747
1941
|
rootPackage.scripts.check.endsWith('&& pnpm performance:readiness && pnpm bridge:check')
|
|
1748
1942
|
: rootPackage.scripts?.check?.includes('pnpm api:check') &&
|
|
1943
|
+
rootPackage.scripts.check.includes('pnpm node:proof') &&
|
|
1749
1944
|
rootPackage.scripts.check.endsWith('&& pnpm performance:readiness'),
|
|
1750
|
-
'Root check must run default-on performance readiness diagnostics and bridge gates when configured',
|
|
1945
|
+
'Root check must run Node backend federation proof, default-on performance readiness diagnostics, and bridge gates when configured',
|
|
1946
|
+
);
|
|
1947
|
+
const zeropsYaml = readText('zerops.yaml');
|
|
1948
|
+
assert(zeropsYaml.includes('zerops:'), 'Zerops manifest must include zerops services');
|
|
1949
|
+
assert(zeropsYaml.includes('setup: shell-super-app'), 'Zerops manifest must include shell service');
|
|
1950
|
+
assert(zeropsYaml.includes('base: nodejs@26'), 'Zerops manifest must use generated Node runtime major');
|
|
1951
|
+
assert(zeropsYaml.includes('deployFiles:'), 'Zerops manifest must deploy package-pruned runtime directories');
|
|
1952
|
+
assert(
|
|
1953
|
+
zeropsYaml.includes('start: cd .zerops/runtime/shell-super-app && npm run serve'),
|
|
1954
|
+
'Zerops shell service must start from materialized runtime package',
|
|
1955
|
+
);
|
|
1956
|
+
assert(
|
|
1957
|
+
zeropsYaml.includes('SHELL_SUPER_APP_PORT:') &&
|
|
1958
|
+
zeropsYaml.includes('ULTRAMODERN_ZEROPS_SERVICE:'),
|
|
1959
|
+
'Zerops manifest must expose service identity and ports',
|
|
1751
1960
|
);
|
|
1961
|
+
for (const vertical of fullStackVerticals) {
|
|
1962
|
+
assert(zeropsYaml.includes(`setup: ${vertical.id}`), `${vertical.id} must have a Zerops service`);
|
|
1963
|
+
assert(
|
|
1964
|
+
zeropsYaml.includes(`~/.local/bin/mise exec -- pnpm run zerops:materialize -- --app ${vertical.id} --package ${vertical.packageName} --package-dir ${vertical.path}`),
|
|
1965
|
+
`${vertical.id} Zerops service must materialize its runtime package`,
|
|
1966
|
+
);
|
|
1967
|
+
assert(
|
|
1968
|
+
zeropsYaml.includes(`path: ${vertical.apiPrefix}/${vertical.stem}/readiness`),
|
|
1969
|
+
`${vertical.id} BFF readiness path must use generated API prefix`,
|
|
1970
|
+
);
|
|
1971
|
+
}
|
|
1752
1972
|
const performanceReadinessConfig = readText('scripts/ultramodern-performance-readiness.config.mjs');
|
|
1973
|
+
const zeropsMaterializer = readText('scripts/materialize-zerops-runtime.mjs');
|
|
1753
1974
|
const assertToolWrapper = (scriptPath, command) => {
|
|
1754
1975
|
const source = readText(scriptPath);
|
|
1755
1976
|
assert(source.includes('modern-js-create'), `${scriptPath} must delegate to modern-js-create`);
|
|
@@ -1760,6 +1981,16 @@ const assertToolWrapper = (scriptPath, command) => {
|
|
|
1760
1981
|
assert(performanceReadinessConfig.includes('UltramodernPerformanceReadinessDiagnosticsConfig'), 'Performance readiness config must carry the typed opt-out surface');
|
|
1761
1982
|
assert(performanceReadinessConfig.includes('enabled: true'), 'Performance readiness diagnostics must be default-on');
|
|
1762
1983
|
assert(performanceReadinessConfig.includes("failOn: 'framework-invariant'"), 'Performance readiness diagnostics must only fail framework invariants by default');
|
|
1984
|
+
assert(zeropsMaterializer.includes("MODERNJS_DEPLOY: 'node'"), 'Zerops materializer must use Modern.js Node deploy target');
|
|
1985
|
+
assert(/'deploy',\s*'--skip-build'/.test(zeropsMaterializer), 'Zerops materializer must pass --skip-build directly to Modern deploy');
|
|
1986
|
+
assert(zeropsMaterializer.includes('normalizeRuntimePackageDependencies'), 'Zerops materializer must normalize generated Modern package aliases before installing dependencies');
|
|
1987
|
+
assert(zeropsMaterializer.includes('officialPackageName'), 'Zerops materializer must add official Modern.js npm aliases for generated runtime imports');
|
|
1988
|
+
assert(zeropsMaterializer.includes('installRuntimeDependencies'), 'Zerops materializer must install dependencies outside the workspace copy runtime node_modules');
|
|
1989
|
+
assert(zeropsMaterializer.includes('copyWorkspacePackage'), 'Zerops materializer must preserve generated local workspace package dependencies');
|
|
1990
|
+
assert(zeropsMaterializer.includes('makeWorkspacePackageRuntimeSafe'), 'Zerops materializer must rewrite copied workspace packages to runtime-safe JavaScript exports');
|
|
1991
|
+
assert(zeropsMaterializer.includes("serve: runtimePackage.scripts?.serve ?? 'node index.js'"), 'Zerops materializer must preserve runtime serve script fallback');
|
|
1992
|
+
assert(zeropsMaterializer.includes("'install', '--omit=dev'"), 'Zerops materializer must omit dev dependencies from runtime installs');
|
|
1993
|
+
assert(zeropsMaterializer.includes("'--legacy-peer-deps'"), 'Zerops materializer must tolerate generated-workspace peer dependency ranges in npm runtime install');
|
|
1763
1994
|
assertToolWrapper('scripts/validate-ultramodern-workspace.mts', 'validate');
|
|
1764
1995
|
assertToolWrapper('scripts/ultramodern-performance-readiness.mts', 'performance-readiness');
|
|
1765
1996
|
const i18nBoundaryScript = readText('scripts/check-ultramodern-i18n-boundaries.mts');
|
|
@@ -1779,8 +2010,12 @@ assert(rootPackage.scripts?.postinstall === "oxfmt . '!repos/**' && node ./scrip
|
|
|
1779
2010
|
assert(rootPackage.scripts?.['agents:refs:install'] === 'node ./scripts/setup-agent-reference-repos.mts', 'Root must expose agents:refs:install as the explicit reference repo installer');
|
|
1780
2011
|
const agentSkillsBootstrap = readText('scripts/bootstrap-agent-skills.mts');
|
|
1781
2012
|
assertToolWrapper('scripts/assert-mf-types.mts', 'mf-types');
|
|
2013
|
+
assertToolWrapper('scripts/generate-node-backend-federation.mts', 'backend-federation-generate');
|
|
1782
2014
|
assertToolWrapper('scripts/generate-public-surface-assets.mts', 'public-surface');
|
|
2015
|
+
assertToolWrapper('scripts/generate-tanstack-routes.mts', 'routes-generate');
|
|
2016
|
+
assertToolWrapper('scripts/proof-node-backend-federation.mts', 'backend-federation-proof');
|
|
1783
2017
|
assertToolWrapper('scripts/proof-cloudflare-version.mts', 'cloudflare-proof');
|
|
2018
|
+
assertToolWrapper('scripts/verify-cloudflare-output.mts', 'cloudflare-output-verify');
|
|
1784
2019
|
assertToolWrapper('scripts/migrate-strict-effect.mts', 'migrate-strict-effect');
|
|
1785
2020
|
assertToolWrapper('scripts/bootstrap-agent-skills.mts', 'skills');
|
|
1786
2021
|
assert(!agentSkillsBootstrap.includes("run('brew'") && !agentSkillsBootstrap.includes('runShell('), 'Agent skills bootstrap must never invoke system package managers');
|
|
@@ -1927,6 +2162,54 @@ for (const vertical of fullStackVerticals) {
|
|
|
1927
2162
|
const modernAppEnv = readText(`${vertical.path}/src/modern-app-env.d.ts`);
|
|
1928
2163
|
const routeHead = readText(`${vertical.path}/src/routes/ultramodern-route-head.tsx`);
|
|
1929
2164
|
const routeMetadata = readText(`${vertical.path}/src/routes/ultramodern-route-metadata.ts`);
|
|
2165
|
+
const ultramodernBuildSource = readText(`${vertical.path}/shared/ultramodern-build.ts`);
|
|
2166
|
+
if (vertical.deliveryUnit) {
|
|
2167
|
+
const buildLabel = `${vertical.path}/shared/ultramodern-build.ts ultramodernDeliveryUnit`;
|
|
2168
|
+
assertSelfCheck(
|
|
2169
|
+
ultramodernBuildSource.includes('export const ultramodernDeliveryUnit'),
|
|
2170
|
+
buildLabel,
|
|
2171
|
+
'Missing ultramodernDeliveryUnit export',
|
|
2172
|
+
deliveryUnitIdentityFixArea,
|
|
2173
|
+
);
|
|
2174
|
+
const buildMatch = /\bbuild:\s*'([^']+)'/u.exec(ultramodernBuildSource);
|
|
2175
|
+
const unitIdMatch = /\bunitId:\s*'([^']+)'/u.exec(ultramodernBuildSource);
|
|
2176
|
+
const packageNameMatch = /\bpackageName:\s*'([^']+)'/u.exec(ultramodernBuildSource);
|
|
2177
|
+
const versionMatch = /\bversion:\s*'([^']+)'/u.exec(ultramodernBuildSource);
|
|
2178
|
+
assertSelfCheck(
|
|
2179
|
+
buildMatch?.[1] === vertical.deliveryUnit.buildMarker,
|
|
2180
|
+
buildLabel,
|
|
2181
|
+
`Expected build "${vertical.deliveryUnit.buildMarker}", found ${formatJson(buildMatch?.[1])}`,
|
|
2182
|
+
deliveryUnitIdentityFixArea,
|
|
2183
|
+
);
|
|
2184
|
+
assertSelfCheck(
|
|
2185
|
+
unitIdMatch?.[1] === vertical.deliveryUnit.unitId,
|
|
2186
|
+
buildLabel,
|
|
2187
|
+
`Expected unitId "${vertical.deliveryUnit.unitId}", found ${formatJson(unitIdMatch?.[1])}`,
|
|
2188
|
+
deliveryUnitIdentityFixArea,
|
|
2189
|
+
);
|
|
2190
|
+
assertSelfCheck(
|
|
2191
|
+
packageNameMatch?.[1] === vertical.deliveryUnit.packageName,
|
|
2192
|
+
buildLabel,
|
|
2193
|
+
`Expected packageName "${vertical.deliveryUnit.packageName}", found ${formatJson(packageNameMatch?.[1])}`,
|
|
2194
|
+
deliveryUnitIdentityFixArea,
|
|
2195
|
+
);
|
|
2196
|
+
assertSelfCheck(
|
|
2197
|
+
versionMatch?.[1] === vertical.deliveryUnit.version,
|
|
2198
|
+
buildLabel,
|
|
2199
|
+
`Expected version "${vertical.deliveryUnit.version}", found ${formatJson(versionMatch?.[1])}`,
|
|
2200
|
+
deliveryUnitIdentityFixArea,
|
|
2201
|
+
);
|
|
2202
|
+
assertDeliveryUnitMarkerSpread(
|
|
2203
|
+
ultramodernBuildSource,
|
|
2204
|
+
'ultramodernUiMarker',
|
|
2205
|
+
`${vertical.path}/shared/ultramodern-build.ts ultramodernUiMarker`,
|
|
2206
|
+
);
|
|
2207
|
+
assertDeliveryUnitMarkerSpread(
|
|
2208
|
+
ultramodernBuildSource,
|
|
2209
|
+
'ultramodernApiMarker',
|
|
2210
|
+
`${vertical.path}/shared/ultramodern-build.ts ultramodernApiMarker`,
|
|
2211
|
+
);
|
|
2212
|
+
}
|
|
1930
2213
|
assert(modernAppEnv.includes("import '@modern-js/app-tools/types';"), `${vertical.id} app env must import the framework-owned app ambient type bundle through module resolution`);
|
|
1931
2214
|
assert(/declare global \{\s*const ULTRAMODERN_SITE_URL: string;\s*\}/u.test(modernAppEnv), `${vertical.id} app env must keep generated globals explicit after importing app ambient types`);
|
|
1932
2215
|
assert(!modernAppEnv.includes("declare module '*.svg'"), `${vertical.id} app env must not redeclare framework-owned svg asset modules`);
|
|
@@ -2057,6 +2340,37 @@ for (const vertical of fullStackVerticals) {
|
|
|
2057
2340
|
assert(topologyEntry?.api?.readiness?.endpoint === `/${vertical.stem}/readiness`, `${vertical.id} topology readiness endpoint is incorrect`);
|
|
2058
2341
|
assert(Object.keys(topologyEntry?.api?.domainOperations ?? {}).length >= 3, `${vertical.id} topology domain operations are missing`);
|
|
2059
2342
|
|
|
2343
|
+
if (vertical.deliveryUnit) {
|
|
2344
|
+
const expectedDeliveryUnit = deliveryUnitBlock(vertical.deliveryUnit);
|
|
2345
|
+
const compactAppEntry = ultramodernConfig.topology?.apps?.find(
|
|
2346
|
+
entry => entry?.id === vertical.id,
|
|
2347
|
+
);
|
|
2348
|
+
assertSameJson(
|
|
2349
|
+
deliveryUnitBlock(compactAppEntry?.deliveryUnit),
|
|
2350
|
+
expectedDeliveryUnit,
|
|
2351
|
+
`${compactConfigPath} topology.apps.${vertical.id}.deliveryUnit`,
|
|
2352
|
+
deliveryUnitIdentityFixArea,
|
|
2353
|
+
);
|
|
2354
|
+
assertSameJson(
|
|
2355
|
+
deliveryUnitBlock(compactAppEntry?.backendFederation?.deliveryUnit),
|
|
2356
|
+
expectedDeliveryUnit,
|
|
2357
|
+
`${compactConfigPath} topology.apps.${vertical.id}.backendFederation.deliveryUnit`,
|
|
2358
|
+
deliveryUnitIdentityFixArea,
|
|
2359
|
+
);
|
|
2360
|
+
assertSelfCheck(
|
|
2361
|
+
compactAppEntry?.backendFederation?.versionBoundary?.identityRoot === 'deliveryUnit',
|
|
2362
|
+
`${compactConfigPath} topology.apps.${vertical.id}.backendFederation.versionBoundary.identityRoot`,
|
|
2363
|
+
`Expected "deliveryUnit", found ${formatJson(compactAppEntry?.backendFederation?.versionBoundary?.identityRoot)}`,
|
|
2364
|
+
deliveryUnitIdentityFixArea,
|
|
2365
|
+
);
|
|
2366
|
+
assertSameJson(
|
|
2367
|
+
deliveryUnitBlock(compactAppEntry?.deliveryUnit),
|
|
2368
|
+
deliveryUnitBlock(topologyEntry?.deliveryUnit),
|
|
2369
|
+
`${compactConfigPath} vs topology/reference-topology.json verticals.${vertical.id}.deliveryUnit`,
|
|
2370
|
+
deliveryUnitIdentityFixArea,
|
|
2371
|
+
);
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2060
2374
|
assert(ownership.owners?.some(owner => owner.id === vertical.id && owner.path === vertical.path), `${vertical.id} ownership entry is missing`);
|
|
2061
2375
|
assert(overlay.ports?.[vertical.id], `${vertical.id} development port is missing`);
|
|
2062
2376
|
assert(overlay.manifests?.[vertical.id]?.includes('/mf-manifest.json'), `${vertical.id} development manifest is missing`);
|