@bleedingdev/modern-js-app-tools-extensions 3.9.0-ultramodern.11 → 3.9.0-ultramodern.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/dist/cjs/backend-federation-build/codegen.js +7 -2
  2. package/dist/cjs/backend-federation-build/config.js +86 -36
  3. package/dist/cjs/backend-federation-build/emit.js +17 -13
  4. package/dist/cjs/build-config/remote-address.js +56 -0
  5. package/dist/cjs/cloudflare/delivery-unit.js +16 -26
  6. package/dist/cjs/cloudflare/index.js +7 -1
  7. package/dist/cjs/cloudflare-builder.js +13 -3
  8. package/dist/cjs/cloudflare-output-contract.js +1 -0
  9. package/dist/cjs/cloudflare-output-verifier/identity.js +26 -24
  10. package/dist/cjs/cloudflare-output-verifier/index.js +10 -0
  11. package/dist/cjs/cloudflare-output-verifier/manifest.js +46 -0
  12. package/dist/cjs/cloudflare-output-verifier/mutation-policy.js +7 -1
  13. package/dist/cjs/config.js +4 -0
  14. package/dist/cjs/deploy-output/npmAliases.js +28 -3
  15. package/dist/cjs/release-envelope/framework-output.js +62 -49
  16. package/dist/cjs/release-envelope/index.js +10 -4
  17. package/dist/esm/backend-federation-build/codegen.mjs +7 -2
  18. package/dist/esm/backend-federation-build/config.mjs +78 -31
  19. package/dist/esm/backend-federation-build/emit.mjs +18 -14
  20. package/dist/esm/build-config/remote-address.mjs +18 -0
  21. package/dist/esm/cloudflare/delivery-unit.mjs +16 -26
  22. package/dist/esm/cloudflare/index.mjs +7 -1
  23. package/dist/esm/cloudflare-builder.mjs +13 -3
  24. package/dist/esm/cloudflare-output-contract.mjs +1 -0
  25. package/dist/esm/cloudflare-output-verifier/identity.mjs +26 -24
  26. package/dist/esm/cloudflare-output-verifier/index.mjs +10 -0
  27. package/dist/esm/cloudflare-output-verifier/manifest.mjs +46 -0
  28. package/dist/esm/cloudflare-output-verifier/mutation-policy.mjs +7 -1
  29. package/dist/esm/config.mjs +1 -0
  30. package/dist/esm/deploy-output/npmAliases.mjs +28 -3
  31. package/dist/esm/release-envelope/framework-output.mjs +62 -49
  32. package/dist/esm/release-envelope/index.mjs +10 -4
  33. package/dist/esm-node/backend-federation-build/codegen.mjs +7 -2
  34. package/dist/esm-node/backend-federation-build/config.mjs +78 -31
  35. package/dist/esm-node/backend-federation-build/emit.mjs +18 -14
  36. package/dist/esm-node/build-config/remote-address.mjs +19 -0
  37. package/dist/esm-node/cloudflare/delivery-unit.mjs +16 -26
  38. package/dist/esm-node/cloudflare/index.mjs +7 -1
  39. package/dist/esm-node/cloudflare-builder.mjs +13 -3
  40. package/dist/esm-node/cloudflare-output-contract.mjs +1 -0
  41. package/dist/esm-node/cloudflare-output-verifier/identity.mjs +26 -24
  42. package/dist/esm-node/cloudflare-output-verifier/index.mjs +10 -0
  43. package/dist/esm-node/cloudflare-output-verifier/manifest.mjs +46 -0
  44. package/dist/esm-node/cloudflare-output-verifier/mutation-policy.mjs +7 -1
  45. package/dist/esm-node/config.mjs +1 -0
  46. package/dist/esm-node/deploy-output/npmAliases.mjs +28 -3
  47. package/dist/esm-node/release-envelope/framework-output.mjs +62 -49
  48. package/dist/esm-node/release-envelope/index.mjs +10 -4
  49. package/dist/types/backend-federation-build/codegen.d.ts +30 -0
  50. package/dist/types/backend-federation-build/config.d.ts +32 -13
  51. package/dist/types/build-config/remote-address.d.ts +3 -0
  52. package/dist/types/cloudflare/delivery-unit.d.ts +1 -1
  53. package/dist/types/cloudflare-builder.d.ts +1 -0
  54. package/dist/types/cloudflare-output-contract.d.ts +1 -1
  55. package/dist/types/config.d.ts +1 -0
  56. package/package.json +20 -19
  57. package/src/backend-federation-build/codegen.ts +6 -2
  58. package/src/backend-federation-build/config.ts +207 -71
  59. package/src/backend-federation-build/emit.ts +33 -22
  60. package/src/build-config/remote-address.ts +31 -0
  61. package/src/cloudflare/delivery-unit.ts +39 -33
  62. package/src/cloudflare/index.ts +20 -6
  63. package/src/cloudflare-builder.ts +18 -4
  64. package/src/cloudflare-output-contract.ts +1 -0
  65. package/src/cloudflare-output-verifier/identity.ts +4 -0
  66. package/src/cloudflare-output-verifier/index.ts +20 -0
  67. package/src/cloudflare-output-verifier/manifest.ts +77 -0
  68. package/src/cloudflare-output-verifier/mutation-policy.ts +11 -1
  69. package/src/config.ts +1 -0
  70. package/src/deploy-output/npmAliases.ts +52 -3
  71. package/src/release-envelope/framework-output.ts +163 -90
  72. package/src/release-envelope/index.ts +26 -5
@@ -33,6 +33,16 @@ const MICROVERTICAL_RELEASE_IDENTITY_CARRIERS_SCHEMA_VERSION = 1;
33
33
 
34
34
  const COMPILED_MODULE_PATTERN = /\.(?:c|m)?js$/u;
35
35
  const EFFECT_BFF_WORKER_PATTERN = /^worker\/__modern_bff_effect\.(?:c|m)?js$/u;
36
+ const EFFECT_BFF_WORKER_SUPPORT_PATTERN =
37
+ /^worker\/__modern_worker_(?:runtime|shared)_*\.(?:c|m)?js$/u;
38
+ const isEffectBffWorkerArtifact = (logicalPath: string) =>
39
+ EFFECT_BFF_WORKER_PATTERN.test(logicalPath) ||
40
+ EFFECT_BFF_WORKER_SUPPORT_PATTERN.test(logicalPath);
41
+ const CRAWLER_POLICY_PATH = 'public/robots.txt';
42
+ const isPublicMetadataPath = (logicalPath: string) =>
43
+ logicalPath === 'public/_headers' ||
44
+ /^public\/\.well-known\/[^/]+\.json$/u.test(logicalPath);
45
+ const PUBLIC_BUILD_ARTIFACT_PATH = `public/${ULTRAMODERN_BUILD_ARTIFACT_FILE}`;
36
46
 
37
47
  const isRecord = (value: unknown): value is Record<string, unknown> =>
38
48
  typeof value === 'object' && value !== null && !Array.isArray(value);
@@ -345,7 +355,7 @@ const readBuildArtifact = async (
345
355
  return undefined;
346
356
  }
347
357
  throw new Error(
348
- '[ultramodern-release-envelope] sourceRevision "workspace" cannot produce a promotable full-stack envelope.',
358
+ '[ultramodern-release-envelope] sourceRevision "workspace" cannot produce a promotable envelope.',
349
359
  );
350
360
  }
351
361
  return artifact;
@@ -516,7 +526,11 @@ const writeReleaseIdentityCarrierMetadata = async (
516
526
  ReleaseIdentityCarrierSurface,
517
527
  string[],
518
528
  ][]) {
519
- if (logicalPaths.length === 0) {
529
+ if (
530
+ logicalPaths.length === 0 &&
531
+ surface !== 'ssr' &&
532
+ surface !== 'uiClient'
533
+ ) {
520
534
  throw new Error(
521
535
  `[ultramodern-release-envelope] ${surface} has no declared release-identity carrier artifact.`,
522
536
  );
@@ -591,12 +605,10 @@ const createReleaseArtifactInputs = async (
591
605
  distDirectory: string,
592
606
  identity: MicroVerticalReleaseIdentity,
593
607
  target: MicroVerticalReleaseTarget,
608
+ apiOnly: boolean,
594
609
  ) => {
595
610
  const files = await collectFiles(distDirectory);
596
- if (
597
- target === 'node' &&
598
- files.some(logicalPath => EFFECT_BFF_WORKER_PATTERN.test(logicalPath))
599
- ) {
611
+ if (target === 'node' && files.some(isEffectBffWorkerArtifact)) {
600
612
  throw new Error(
601
613
  '[ultramodern-release-envelope] Node target conflicts with a Cloudflare Effect API/BFF worker artifact; resolve the canonical deploy target instead of emitting a Node envelope for Cloudflare output.',
602
614
  );
@@ -606,7 +618,9 @@ const createReleaseArtifactInputs = async (
606
618
  return (
607
619
  topLevel === 'static' ||
608
620
  topLevel === 'html' ||
609
- topLevel === 'public' ||
621
+ (topLevel === 'public' &&
622
+ logicalPath !== CRAWLER_POLICY_PATH &&
623
+ !isPublicMetadataPath(logicalPath)) ||
610
624
  [
611
625
  'index.html',
612
626
  'mf-manifest.json',
@@ -628,7 +642,7 @@ const createReleaseArtifactInputs = async (
628
642
  logicalPath =>
629
643
  logicalPath.startsWith('worker/') &&
630
644
  COMPILED_MODULE_PATTERN.test(logicalPath) &&
631
- !EFFECT_BFF_WORKER_PATTERN.test(logicalPath),
645
+ !isEffectBffWorkerArtifact(logicalPath),
632
646
  );
633
647
  const nodeApiEntryPaths = files.filter(
634
648
  logicalPath =>
@@ -643,44 +657,48 @@ const createReleaseArtifactInputs = async (
643
657
  logicalPath.startsWith('shared/')) &&
644
658
  COMPILED_MODULE_PATTERN.test(logicalPath),
645
659
  )
646
- : files.filter(logicalPath =>
647
- EFFECT_BFF_WORKER_PATTERN.test(logicalPath),
648
- );
660
+ : files.filter(isEffectBffWorkerArtifact);
649
661
 
650
- if (uiClientPaths.length === 0) {
662
+ if (apiOnly && (uiClientPaths.length > 0 || ssrPaths.length > 0)) {
663
+ throw new Error(
664
+ '[ultramodern-release-envelope] API-only MicroVertical emitted an undeclared UI/client or SSR surface.',
665
+ );
666
+ }
667
+ if (!apiOnly && uiClientPaths.length === 0) {
651
668
  throw new Error(
652
669
  `[ultramodern-release-envelope] ${target} full-stack MicroVertical has no UI/client artifacts.`,
653
670
  );
654
671
  }
655
- if (ssrPaths.length === 0) {
672
+ if (!apiOnly && ssrPaths.length === 0) {
656
673
  throw new Error(
657
674
  `[ultramodern-release-envelope] ${target} full-stack MicroVertical has no ${target === 'node' ? 'Node' : 'workerd'} SSR artifacts.`,
658
675
  );
659
676
  }
660
677
  if (
661
678
  (target === 'node' && nodeApiEntryPaths.length === 0) ||
679
+ (target === 'cloudflare' &&
680
+ !files.some(logicalPath =>
681
+ EFFECT_BFF_WORKER_PATTERN.test(logicalPath),
682
+ )) ||
662
683
  apiBackendPaths.length === 0
663
684
  ) {
664
685
  throw new Error(
665
- `[ultramodern-release-envelope] ${target} full-stack MicroVertical has no actual ${target === 'node' ? 'compiled Node Effect API artifact' : 'Effect API/BFF worker artifact'}.`,
686
+ `[ultramodern-release-envelope] ${target} MicroVertical has no actual ${target === 'node' ? 'compiled Node Effect API artifact' : 'Effect API/BFF worker artifact'}.`,
666
687
  );
667
688
  }
668
689
 
669
- const clientExecutionPaths = await manifestReferencedClientModules(
670
- distDirectory,
671
- files,
672
- );
673
- if (clientExecutionPaths.length === 0) {
690
+ const clientExecutionPaths = apiOnly
691
+ ? []
692
+ : await manifestReferencedClientModules(distDirectory, files);
693
+ if (!apiOnly && clientExecutionPaths.length === 0) {
674
694
  throw new Error(
675
695
  '[ultramodern-release-envelope] UI/client manifest references no compiled execution module.',
676
696
  );
677
697
  }
678
- const ssrExecutionPaths = await routeReferencedSsrModules(
679
- distDirectory,
680
- files,
681
- target,
682
- );
683
- if (ssrExecutionPaths.length === 0) {
698
+ const ssrExecutionPaths = apiOnly
699
+ ? []
700
+ : await routeReferencedSsrModules(distDirectory, files, target);
701
+ if (!apiOnly && ssrExecutionPaths.length === 0) {
684
702
  throw new Error(
685
703
  `[ultramodern-release-envelope] route manifest references no emitted ${target === 'node' ? 'Node' : 'Cloudflare'} SSR execution module.`,
686
704
  );
@@ -699,6 +717,11 @@ const createReleaseArtifactInputs = async (
699
717
  }
700
718
  };
701
719
  add(uiClientPaths, 'browser');
720
+ add(
721
+ files.filter(logicalPath => logicalPath === CRAWLER_POLICY_PATH),
722
+ 'crawler-policy',
723
+ );
724
+ add(files.filter(isPublicMetadataPath), 'public-metadata');
702
725
  add(ssrPaths, target === 'node' ? 'nodejs' : 'workerd');
703
726
  add(apiBackendPaths, target === 'node' ? 'nodejs' : 'workerd-effect');
704
727
  add([BACKEND_FEDERATION_MANIFEST_FILE], 'module-federation-manifest');
@@ -766,9 +789,6 @@ export const emitFrameworkMicroVerticalReleaseEnvelope = async ({
766
789
  requirePromotable?: boolean;
767
790
  target: MicroVerticalReleaseTarget;
768
791
  }): Promise<MicroVerticalReleaseEnvelope | undefined> => {
769
- if (apiOnly) {
770
- return undefined;
771
- }
772
792
  const backendManifestPath = path.join(
773
793
  distDirectory,
774
794
  BACKEND_FEDERATION_MANIFEST_FILE,
@@ -781,12 +801,12 @@ export const emitFrameworkMicroVerticalReleaseEnvelope = async ({
781
801
  pathExists(backendManifestPath),
782
802
  pathExists(backendContainerPath),
783
803
  ]);
784
- if (!hasBackendManifest && !hasBackendContainer) {
804
+ if (!hasBackendManifest && !hasBackendContainer && !apiOnly) {
785
805
  return undefined;
786
806
  }
787
807
  if (!hasBackendManifest || !hasBackendContainer) {
788
808
  throw new Error(
789
- '[ultramodern-release-envelope] full-stack MicroVertical backend federation manifest and container must be emitted together.',
809
+ '[ultramodern-release-envelope] MicroVertical backend federation manifest and container must be emitted together.',
790
810
  );
791
811
  }
792
812
 
@@ -808,6 +828,7 @@ export const emitFrameworkMicroVerticalReleaseEnvelope = async ({
808
828
  distDirectory,
809
829
  identity,
810
830
  target,
831
+ apiOnly,
811
832
  );
812
833
  const envelope = await createMicroVerticalReleaseEnvelope({
813
834
  artifactRoot: distDirectory,
@@ -854,21 +875,14 @@ export const verifyBuildOutputReleaseEnvelope = async (
854
875
  distDirectory: string,
855
876
  expectedTarget?: MicroVerticalReleaseTarget,
856
877
  ) => {
857
- const [hasEnvelope, hasBuildArtifact, hasBackendManifest, hasBackendEntry] =
858
- await Promise.all([
859
- pathExists(path.join(distDirectory, MICROVERTICAL_RELEASE_ENVELOPE_PATH)),
860
- pathExists(path.join(distDirectory, ULTRAMODERN_BUILD_ARTIFACT_FILE)),
861
- pathExists(path.join(distDirectory, BACKEND_FEDERATION_MANIFEST_FILE)),
862
- pathExists(
863
- path.join(distDirectory, BACKEND_FEDERATION_REMOTE_ENTRY_FILE),
864
- ),
865
- ]);
866
- const isFullStackMicroVertical =
867
- hasBuildArtifact && (hasBackendManifest || hasBackendEntry);
878
+ const [hasEnvelope, hasBuildArtifact] = await Promise.all([
879
+ pathExists(path.join(distDirectory, MICROVERTICAL_RELEASE_ENVELOPE_PATH)),
880
+ pathExists(path.join(distDirectory, ULTRAMODERN_BUILD_ARTIFACT_FILE)),
881
+ ]);
868
882
  return readFrameworkMicroVerticalReleaseEnvelope({
869
883
  artifactRoot: distDirectory,
870
884
  ...(expectedTarget ? { expectedTarget } : {}),
871
- required: hasEnvelope || isFullStackMicroVertical,
885
+ required: hasEnvelope || hasBuildArtifact,
872
886
  });
873
887
  };
874
888
 
@@ -887,6 +901,7 @@ export const verifyNodeReleaseEnvelopeStaging = async ({
887
901
  const createNodeStagedReleaseArtifactInputs = async (
888
902
  outputDirectory: string,
889
903
  identity: MicroVerticalReleaseIdentity,
904
+ apiOnly: boolean,
890
905
  ) => {
891
906
  const files = await collectFiles(outputDirectory);
892
907
  for (const requiredPath of [
@@ -911,7 +926,9 @@ const createNodeStagedReleaseArtifactInputs = async (
911
926
  return (
912
927
  topLevel === 'static' ||
913
928
  topLevel === 'html' ||
914
- topLevel === 'public' ||
929
+ (topLevel === 'public' &&
930
+ logicalPath !== CRAWLER_POLICY_PATH &&
931
+ !isPublicMetadataPath(logicalPath)) ||
915
932
  [
916
933
  'index.html',
917
934
  'mf-manifest.json',
@@ -933,27 +950,35 @@ const createNodeStagedReleaseArtifactInputs = async (
933
950
  (logicalPath.startsWith('api/') || logicalPath.startsWith('shared/')) &&
934
951
  COMPILED_MODULE_PATTERN.test(logicalPath),
935
952
  );
953
+ if (
954
+ apiOnly &&
955
+ (uiClientPaths.length > 0 || ssrPaths.some(path => path !== 'index.js'))
956
+ ) {
957
+ throw new Error(
958
+ '[ultramodern-release-envelope] final API-only Node staging contains an undeclared UI/client or SSR surface.',
959
+ );
960
+ }
936
961
  for (const [surface, paths] of [
937
962
  ['UI/client', uiClientPaths],
938
- ['Node SSR', ssrPaths],
963
+ ['Node SSR', apiOnly ? [] : ssrPaths],
939
964
  ['Node API/backend', apiBackendPaths],
940
965
  ] as const) {
941
- if (paths.length === 0) {
966
+ if (paths.length === 0 && (!apiOnly || surface === 'Node API/backend')) {
942
967
  throw new Error(
943
968
  `[ultramodern-release-envelope] final Node staging has no ${surface} artifacts.`,
944
969
  );
945
970
  }
946
971
  }
947
- const clientExecutionPaths = await manifestReferencedClientModules(
948
- outputDirectory,
949
- files,
950
- );
951
- const ssrExecutionPaths = await routeReferencedSsrModules(
952
- outputDirectory,
953
- files,
954
- 'node',
955
- );
956
- if (clientExecutionPaths.length === 0 || ssrExecutionPaths.length === 0) {
972
+ const clientExecutionPaths = apiOnly
973
+ ? []
974
+ : await manifestReferencedClientModules(outputDirectory, files);
975
+ const ssrExecutionPaths = apiOnly
976
+ ? []
977
+ : await routeReferencedSsrModules(outputDirectory, files, 'node');
978
+ if (
979
+ !apiOnly &&
980
+ (clientExecutionPaths.length === 0 || ssrExecutionPaths.length === 0)
981
+ ) {
957
982
  throw new Error(
958
983
  '[ultramodern-release-envelope] final Node manifests must reference emitted UI/client and SSR execution modules.',
959
984
  );
@@ -967,7 +992,13 @@ const createNodeStagedReleaseArtifactInputs = async (
967
992
  for (const logicalPath of uiClientPaths) {
968
993
  runtimeByPath.set(logicalPath, 'browser');
969
994
  }
970
- for (const logicalPath of ssrPaths) {
995
+ if (files.includes(CRAWLER_POLICY_PATH)) {
996
+ runtimeByPath.set(CRAWLER_POLICY_PATH, 'crawler-policy');
997
+ }
998
+ for (const logicalPath of files.filter(isPublicMetadataPath)) {
999
+ runtimeByPath.set(logicalPath, 'public-metadata');
1000
+ }
1001
+ for (const logicalPath of apiOnly ? [] : ssrPaths) {
971
1002
  runtimeByPath.set(logicalPath, 'nodejs');
972
1003
  }
973
1004
  for (const logicalPath of apiBackendPaths) {
@@ -988,12 +1019,14 @@ const createNodeStagedReleaseArtifactInputs = async (
988
1019
  BACKEND_FEDERATION_MANIFEST_FILE,
989
1020
  BACKEND_FEDERATION_REMOTE_ENTRY_FILE,
990
1021
  ],
991
- ssr: [
992
- ...new Set([
993
- ...ssrExecutionPaths,
994
- ...ssrPaths.filter(logicalPath => logicalPath !== 'index.js'),
995
- ]),
996
- ],
1022
+ ssr: apiOnly
1023
+ ? []
1024
+ : [
1025
+ ...new Set([
1026
+ ...ssrExecutionPaths,
1027
+ ...ssrPaths.filter(logicalPath => logicalPath !== 'index.js'),
1028
+ ]),
1029
+ ],
997
1030
  uiClient: uiClientPaths.filter(logicalPath =>
998
1031
  COMPILED_MODULE_PATTERN.test(logicalPath),
999
1032
  ),
@@ -1010,7 +1043,9 @@ const createNodeStagedReleaseArtifactInputs = async (
1010
1043
  uiClient: [...uiClientPaths].sort((left, right) =>
1011
1044
  left.localeCompare(right),
1012
1045
  ),
1013
- ssr: [...ssrPaths].sort((left, right) => left.localeCompare(right)),
1046
+ ssr: apiOnly
1047
+ ? []
1048
+ : [...ssrPaths].sort((left, right) => left.localeCompare(right)),
1014
1049
  apiBackend: [...apiBackendPaths].sort((left, right) =>
1015
1050
  left.localeCompare(right),
1016
1051
  ),
@@ -1036,6 +1071,7 @@ export const emitNodeStagedReleaseEnvelope = async ({
1036
1071
  const { artifacts, surfaces } = await createNodeStagedReleaseArtifactInputs(
1037
1072
  outputDirectory,
1038
1073
  source.identity,
1074
+ source.surfaces.uiClient.length === 0,
1039
1075
  );
1040
1076
  const staged = await createMicroVerticalReleaseEnvelope({
1041
1077
  artifactRoot: outputDirectory,
@@ -1077,6 +1113,7 @@ export const stageCloudflareReleaseEnvelope = async ({
1077
1113
  const createCloudflareStagedReleaseArtifactInputs = async (
1078
1114
  outputDirectory: string,
1079
1115
  identity: MicroVerticalReleaseIdentity,
1116
+ apiOnly: boolean,
1080
1117
  ) => {
1081
1118
  const files = await collectFiles(outputDirectory);
1082
1119
  for (const requiredPath of [
@@ -1084,7 +1121,7 @@ const createCloudflareStagedReleaseArtifactInputs = async (
1084
1121
  'server/modern-worker-manifest.json',
1085
1122
  'wrangler.json',
1086
1123
  'package.json',
1087
- 'worker/package.json',
1124
+ ...(apiOnly ? [] : ['worker/package.json']),
1088
1125
  ]) {
1089
1126
  if (!files.includes(requiredPath)) {
1090
1127
  throw new Error(
@@ -1105,28 +1142,46 @@ const createCloudflareStagedReleaseArtifactInputs = async (
1105
1142
  }
1106
1143
  const backendManifestPath = `public/${BACKEND_FEDERATION_MANIFEST_FILE}`;
1107
1144
  const backendContainerPath = `public/${BACKEND_FEDERATION_REMOTE_ENTRY_FILE}`;
1108
- const apiBackendPaths = files.filter(logicalPath =>
1109
- EFFECT_BFF_WORKER_PATTERN.test(logicalPath),
1110
- );
1145
+ const apiBackendPaths = files.filter(isEffectBffWorkerArtifact);
1146
+ if (!files.some(logicalPath => EFFECT_BFF_WORKER_PATTERN.test(logicalPath))) {
1147
+ throw new Error(
1148
+ '[ultramodern-release-envelope] final Cloudflare staging has no actual Effect API/BFF worker artifact.',
1149
+ );
1150
+ }
1111
1151
  const ssrPaths = files.filter(
1112
1152
  logicalPath =>
1113
1153
  (logicalPath.startsWith('server/') ||
1114
1154
  logicalPath.startsWith('worker/')) &&
1115
1155
  COMPILED_MODULE_PATTERN.test(logicalPath) &&
1116
- !EFFECT_BFF_WORKER_PATTERN.test(logicalPath),
1156
+ !isEffectBffWorkerArtifact(logicalPath),
1117
1157
  );
1118
1158
  const uiClientPaths = files.filter(
1119
1159
  logicalPath =>
1120
1160
  logicalPath.startsWith('public/') &&
1161
+ logicalPath !== CRAWLER_POLICY_PATH &&
1162
+ !isPublicMetadataPath(logicalPath) &&
1163
+ logicalPath !== PUBLIC_BUILD_ARTIFACT_PATH &&
1121
1164
  logicalPath !== backendManifestPath &&
1122
1165
  logicalPath !== backendContainerPath,
1123
1166
  );
1167
+ if (
1168
+ apiOnly &&
1169
+ (uiClientPaths.length > 0 ||
1170
+ ssrPaths.some(path => path !== 'server/index.mjs'))
1171
+ ) {
1172
+ throw new Error(
1173
+ '[ultramodern-release-envelope] final API-only Cloudflare staging contains an undeclared UI/client or SSR surface.',
1174
+ );
1175
+ }
1124
1176
  for (const [surface, paths] of [
1125
1177
  ['UI/client', uiClientPaths],
1126
- ['Cloudflare SSR', ssrPaths],
1178
+ ['Cloudflare SSR', apiOnly ? [] : ssrPaths],
1127
1179
  ['Cloudflare API/backend', apiBackendPaths],
1128
1180
  ] as const) {
1129
- if (paths.length === 0) {
1181
+ if (
1182
+ paths.length === 0 &&
1183
+ (!apiOnly || surface === 'Cloudflare API/backend')
1184
+ ) {
1130
1185
  throw new Error(
1131
1186
  `[ultramodern-release-envelope] final Cloudflare staging has no ${surface} artifacts.`,
1132
1187
  );
@@ -1140,18 +1195,25 @@ const createCloudflareStagedReleaseArtifactInputs = async (
1140
1195
  }
1141
1196
  }
1142
1197
 
1143
- const clientExecutionPaths = await manifestReferencedClientModules(
1144
- outputDirectory,
1145
- files,
1146
- 'public/mf-manifest.json',
1147
- );
1148
- const ssrExecutionPaths = await routeReferencedSsrModules(
1149
- outputDirectory,
1150
- files,
1151
- 'cloudflare',
1152
- 'server/route.json',
1153
- );
1154
- if (clientExecutionPaths.length === 0 || ssrExecutionPaths.length === 0) {
1198
+ const clientExecutionPaths = apiOnly
1199
+ ? []
1200
+ : await manifestReferencedClientModules(
1201
+ outputDirectory,
1202
+ files,
1203
+ 'public/mf-manifest.json',
1204
+ );
1205
+ const ssrExecutionPaths = apiOnly
1206
+ ? []
1207
+ : await routeReferencedSsrModules(
1208
+ outputDirectory,
1209
+ files,
1210
+ 'cloudflare',
1211
+ 'server/route.json',
1212
+ );
1213
+ if (
1214
+ !apiOnly &&
1215
+ (clientExecutionPaths.length === 0 || ssrExecutionPaths.length === 0)
1216
+ ) {
1155
1217
  throw new Error(
1156
1218
  '[ultramodern-release-envelope] final Cloudflare manifests must reference emitted UI/client and SSR execution modules.',
1157
1219
  );
@@ -1167,7 +1229,11 @@ const createCloudflareStagedReleaseArtifactInputs = async (
1167
1229
  }
1168
1230
  };
1169
1231
  add(uiClientPaths, 'browser');
1170
- add(ssrPaths, 'workerd');
1232
+ if (files.includes(CRAWLER_POLICY_PATH)) {
1233
+ runtimeByPath.set(CRAWLER_POLICY_PATH, 'crawler-policy');
1234
+ }
1235
+ add(files.filter(isPublicMetadataPath), 'public-metadata');
1236
+ add(apiOnly ? [] : ssrPaths, 'workerd');
1171
1237
  add(apiBackendPaths, 'workerd-effect');
1172
1238
  add([backendManifestPath], 'module-federation-manifest');
1173
1239
  add([backendContainerPath], 'commonjs-module');
@@ -1178,12 +1244,16 @@ const createCloudflareStagedReleaseArtifactInputs = async (
1178
1244
  {
1179
1245
  apiBackend: apiBackendPaths,
1180
1246
  backendFederation: [backendManifestPath, backendContainerPath],
1181
- ssr: [
1182
- ...new Set([
1183
- ...ssrExecutionPaths,
1184
- ...ssrPaths.filter(logicalPath => logicalPath.startsWith('worker/')),
1185
- ]),
1186
- ],
1247
+ ssr: apiOnly
1248
+ ? []
1249
+ : [
1250
+ ...new Set([
1251
+ ...ssrExecutionPaths,
1252
+ ...ssrPaths.filter(logicalPath =>
1253
+ logicalPath.startsWith('worker/'),
1254
+ ),
1255
+ ]),
1256
+ ],
1187
1257
  uiClient: uiClientPaths.filter(logicalPath =>
1188
1258
  COMPILED_MODULE_PATTERN.test(logicalPath),
1189
1259
  ),
@@ -1200,7 +1270,9 @@ const createCloudflareStagedReleaseArtifactInputs = async (
1200
1270
  uiClient: [...uiClientPaths].sort((left, right) =>
1201
1271
  left.localeCompare(right),
1202
1272
  ),
1203
- ssr: [...ssrPaths].sort((left, right) => left.localeCompare(right)),
1273
+ ssr: apiOnly
1274
+ ? []
1275
+ : [...ssrPaths].sort((left, right) => left.localeCompare(right)),
1204
1276
  apiBackend: [...apiBackendPaths].sort((left, right) =>
1205
1277
  left.localeCompare(right),
1206
1278
  ),
@@ -1230,6 +1302,7 @@ export const emitCloudflareStagedReleaseEnvelope = async ({
1230
1302
  await createCloudflareStagedReleaseArtifactInputs(
1231
1303
  outputDirectory,
1232
1304
  source.identity,
1305
+ source.surfaces.uiClient.length === 0,
1233
1306
  );
1234
1307
  const staged = await createMicroVerticalReleaseEnvelope({
1235
1308
  artifactRoot: outputDirectory,
@@ -381,8 +381,8 @@ const assertUniqueSortedArtifacts = (
381
381
  };
382
382
 
383
383
  const assertSurfacePaths = (value: unknown, location: string): string[] => {
384
- if (!Array.isArray(value) || value.length === 0) {
385
- throw new Error(`${location} must contain at least one artifact path.`);
384
+ if (!Array.isArray(value)) {
385
+ throw new Error(`${location} must be an array of artifact paths.`);
386
386
  }
387
387
  const paths = value.map((item, index) =>
388
388
  assertNormalizedLogicalPath(item, `${location}[${index}]`),
@@ -413,10 +413,31 @@ const assertSurfaces = (value: unknown): MicroVerticalReleaseSurfaces => {
413
413
  ['manifest', 'container'],
414
414
  'surfaces.backendFederation',
415
415
  );
416
+ const uiClient = assertSurfacePaths(surfaces.uiClient, 'surfaces.uiClient');
417
+ const ssr = assertSurfacePaths(surfaces.ssr, 'surfaces.ssr');
418
+ const apiBackend = assertSurfacePaths(
419
+ surfaces.apiBackend,
420
+ 'surfaces.apiBackend',
421
+ );
422
+ if (uiClient.length > 0 && ssr.length === 0) {
423
+ throw new Error(
424
+ 'surfaces.ssr must contain at least one artifact path when UI/client is declared.',
425
+ );
426
+ }
427
+ if (ssr.length > 0 && uiClient.length === 0) {
428
+ throw new Error(
429
+ 'surfaces.uiClient must contain at least one artifact path when SSR is declared.',
430
+ );
431
+ }
432
+ if (apiBackend.length === 0) {
433
+ throw new Error(
434
+ 'surfaces.apiBackend must contain at least one artifact path.',
435
+ );
436
+ }
416
437
  return {
417
- uiClient: assertSurfacePaths(surfaces.uiClient, 'surfaces.uiClient'),
418
- ssr: assertSurfacePaths(surfaces.ssr, 'surfaces.ssr'),
419
- apiBackend: assertSurfacePaths(surfaces.apiBackend, 'surfaces.apiBackend'),
438
+ uiClient,
439
+ ssr,
440
+ apiBackend,
420
441
  backendFederation: {
421
442
  manifest: assertNormalizedLogicalPath(
422
443
  backendFederation.manifest,