@bleedingdev/modern-js-app-tools-extensions 3.9.0-ultramodern.2 → 3.9.0-ultramodern.21

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 (217) hide show
  1. package/dist/cjs/backend-federation-build/codegen.js +267 -0
  2. package/dist/cjs/backend-federation-build/config.js +239 -0
  3. package/dist/cjs/backend-federation-build/emit.js +154 -0
  4. package/dist/cjs/backend-federation-build/index.js +53 -0
  5. package/dist/cjs/build-config/build-environment.js +129 -0
  6. package/dist/cjs/build-config/precompress/plugin.js +124 -0
  7. package/dist/cjs/build-config/precompress/types.js +18 -0
  8. package/dist/cjs/build-config/public.js +43 -0
  9. package/dist/cjs/build-config/remote-address.js +58 -0
  10. package/dist/cjs/cloudflare/artifacts.js +8 -19
  11. package/dist/cjs/cloudflare/constants.js +1 -1
  12. package/dist/cjs/cloudflare/delivery-unit.js +23 -42
  13. package/dist/cjs/cloudflare/index.js +10 -2
  14. package/dist/cjs/cloudflare/worker-manifest.js +13 -4
  15. package/dist/cjs/cloudflare/wrangler-config.js +35 -7
  16. package/dist/cjs/cloudflare-builder.js +252 -75
  17. package/dist/cjs/cloudflare-output-contract.js +69 -3
  18. package/dist/cjs/cloudflare-output-verifier/identity.js +29 -27
  19. package/dist/cjs/cloudflare-output-verifier/index.js +10 -0
  20. package/dist/cjs/cloudflare-output-verifier/manifest.js +46 -0
  21. package/dist/cjs/cloudflare-output-verifier/mutation-policy.js +7 -1
  22. package/dist/cjs/cloudflare-output-verifier/worker-bundles.js +14 -6
  23. package/dist/cjs/config.js +34 -1
  24. package/dist/cjs/deploy-output/npmAliases.js +251 -0
  25. package/dist/cjs/deploy-output/plugin.js +97 -0
  26. package/dist/cjs/deploy-output/public-assets.js +146 -0
  27. package/dist/cjs/deploy-output/target.js +104 -0
  28. package/dist/cjs/deploy-output/trace-files.js +90 -0
  29. package/dist/cjs/native-type-checker.js +193 -0
  30. package/dist/cjs/policy-defaults.js +101 -0
  31. package/dist/cjs/release-envelope/framework-output.js +153 -79
  32. package/dist/cjs/release-envelope/index.js +10 -4
  33. package/dist/cjs/release-envelope/plugin.js +18 -13
  34. package/dist/cjs/release-identity.js +5 -6
  35. package/dist/cjs/runtime-package-resolution.js +118 -0
  36. package/dist/cjs/templates/cloudflare-entry.001-bootstrap-security.mjs +0 -3
  37. package/dist/cjs/templates/cloudflare-entry.002-assets-routes.mjs +1 -1
  38. package/dist/cjs/templates/cloudflare-entry.004-rendering-css.mjs +5 -1
  39. package/dist/cjs/templates/cloudflare-entry.005-worker-dispatch.mjs +98 -10
  40. package/dist/cjs/templates/cloudflare-entry.006-fetch-handler.mjs +9 -2
  41. package/dist/cjs/templates/cloudflare-worker-route-data.mjs +1069 -0
  42. package/dist/esm/backend-federation-build/codegen.mjs +216 -0
  43. package/dist/esm/backend-federation-build/config.mjs +163 -0
  44. package/dist/esm/backend-federation-build/emit.mjs +103 -0
  45. package/dist/esm/backend-federation-build/index.mjs +1 -0
  46. package/dist/esm/build-config/build-environment.mjs +88 -0
  47. package/dist/esm/build-config/precompress/plugin.mjs +76 -0
  48. package/dist/esm/build-config/precompress/types.mjs +0 -0
  49. package/dist/esm/build-config/public.mjs +1 -0
  50. package/dist/esm/build-config/remote-address.mjs +20 -0
  51. package/dist/esm/cloudflare/artifacts.mjs +8 -19
  52. package/dist/esm/cloudflare/constants.mjs +1 -1
  53. package/dist/esm/cloudflare/delivery-unit.mjs +17 -36
  54. package/dist/esm/cloudflare/index.mjs +10 -2
  55. package/dist/esm/cloudflare/worker-manifest.mjs +13 -4
  56. package/dist/esm/cloudflare/wrangler-config.mjs +35 -7
  57. package/dist/esm/cloudflare-builder.mjs +246 -75
  58. package/dist/esm/cloudflare-output-contract.mjs +59 -2
  59. package/dist/esm/cloudflare-output-verifier/identity.mjs +27 -25
  60. package/dist/esm/cloudflare-output-verifier/index.mjs +10 -0
  61. package/dist/esm/cloudflare-output-verifier/manifest.mjs +46 -0
  62. package/dist/esm/cloudflare-output-verifier/mutation-policy.mjs +7 -1
  63. package/dist/esm/cloudflare-output-verifier/worker-bundles.mjs +15 -7
  64. package/dist/esm/config.mjs +3 -0
  65. package/dist/esm/deploy-output/npmAliases.mjs +199 -0
  66. package/dist/esm/deploy-output/plugin.mjs +46 -0
  67. package/dist/esm/deploy-output/public-assets.mjs +82 -0
  68. package/dist/esm/deploy-output/target.mjs +57 -0
  69. package/dist/esm/deploy-output/trace-files.mjs +38 -0
  70. package/dist/esm/native-type-checker.mjs +142 -0
  71. package/dist/esm/policy-defaults.mjs +45 -0
  72. package/dist/esm/release-envelope/framework-output.mjs +130 -56
  73. package/dist/esm/release-envelope/index.mjs +10 -4
  74. package/dist/esm/release-envelope/plugin.mjs +18 -13
  75. package/dist/esm/release-identity.mjs +5 -6
  76. package/dist/esm/runtime-package-resolution.mjs +60 -0
  77. package/dist/esm/templates/cloudflare-entry.001-bootstrap-security.mjs +0 -3
  78. package/dist/esm/templates/cloudflare-entry.002-assets-routes.mjs +1 -1
  79. package/dist/esm/templates/cloudflare-entry.004-rendering-css.mjs +5 -1
  80. package/dist/esm/templates/cloudflare-entry.005-worker-dispatch.mjs +98 -10
  81. package/dist/esm/templates/cloudflare-entry.006-fetch-handler.mjs +9 -2
  82. package/dist/esm/templates/cloudflare-worker-route-data.mjs +1069 -0
  83. package/dist/esm-node/backend-federation-build/codegen.mjs +217 -0
  84. package/dist/esm-node/backend-federation-build/config.mjs +164 -0
  85. package/dist/esm-node/backend-federation-build/emit.mjs +104 -0
  86. package/dist/esm-node/backend-federation-build/index.mjs +2 -0
  87. package/dist/esm-node/build-config/build-environment.mjs +89 -0
  88. package/dist/esm-node/build-config/precompress/plugin.mjs +77 -0
  89. package/dist/esm-node/build-config/precompress/types.mjs +1 -0
  90. package/dist/esm-node/build-config/public.mjs +2 -0
  91. package/dist/esm-node/build-config/remote-address.mjs +21 -0
  92. package/dist/esm-node/cloudflare/artifacts.mjs +8 -19
  93. package/dist/esm-node/cloudflare/constants.mjs +1 -1
  94. package/dist/esm-node/cloudflare/delivery-unit.mjs +17 -36
  95. package/dist/esm-node/cloudflare/index.mjs +10 -2
  96. package/dist/esm-node/cloudflare/worker-manifest.mjs +13 -4
  97. package/dist/esm-node/cloudflare/wrangler-config.mjs +35 -7
  98. package/dist/esm-node/cloudflare-builder.mjs +246 -75
  99. package/dist/esm-node/cloudflare-output-contract.mjs +59 -2
  100. package/dist/esm-node/cloudflare-output-verifier/identity.mjs +27 -25
  101. package/dist/esm-node/cloudflare-output-verifier/index.mjs +10 -0
  102. package/dist/esm-node/cloudflare-output-verifier/manifest.mjs +46 -0
  103. package/dist/esm-node/cloudflare-output-verifier/mutation-policy.mjs +7 -1
  104. package/dist/esm-node/cloudflare-output-verifier/worker-bundles.mjs +15 -7
  105. package/dist/esm-node/config.mjs +3 -0
  106. package/dist/esm-node/deploy-output/npmAliases.mjs +200 -0
  107. package/dist/esm-node/deploy-output/plugin.mjs +49 -0
  108. package/dist/esm-node/deploy-output/public-assets.mjs +83 -0
  109. package/dist/esm-node/deploy-output/target.mjs +58 -0
  110. package/dist/esm-node/deploy-output/trace-files.mjs +39 -0
  111. package/dist/esm-node/native-type-checker.mjs +143 -0
  112. package/dist/esm-node/policy-defaults.mjs +46 -0
  113. package/dist/esm-node/release-envelope/framework-output.mjs +130 -56
  114. package/dist/esm-node/release-envelope/index.mjs +10 -4
  115. package/dist/esm-node/release-envelope/plugin.mjs +18 -13
  116. package/dist/esm-node/release-identity.mjs +5 -6
  117. package/dist/esm-node/runtime-package-resolution.mjs +61 -0
  118. package/dist/esm-node/templates/cloudflare-entry.001-bootstrap-security.mjs +0 -3
  119. package/dist/esm-node/templates/cloudflare-entry.002-assets-routes.mjs +1 -1
  120. package/dist/esm-node/templates/cloudflare-entry.004-rendering-css.mjs +5 -1
  121. package/dist/esm-node/templates/cloudflare-entry.005-worker-dispatch.mjs +98 -10
  122. package/dist/esm-node/templates/cloudflare-entry.006-fetch-handler.mjs +9 -2
  123. package/dist/esm-node/templates/cloudflare-worker-route-data.mjs +1069 -0
  124. package/dist/types/backend-federation-build/codegen.d.ts +116 -0
  125. package/dist/types/backend-federation-build/config.d.ts +121 -0
  126. package/dist/types/backend-federation-build/emit.d.ts +21 -0
  127. package/dist/types/backend-federation-build/index.d.ts +2 -0
  128. package/dist/types/build-config/build-environment.d.ts +14 -0
  129. package/dist/types/build-config/precompress/plugin.d.ts +4 -0
  130. package/dist/types/build-config/precompress/types.d.ts +19 -0
  131. package/dist/types/build-config/public.d.ts +2 -0
  132. package/dist/types/build-config/remote-address.d.ts +3 -0
  133. package/dist/types/cloudflare/artifacts.d.ts +7 -6
  134. package/dist/types/cloudflare/constants.d.ts +1 -1
  135. package/dist/types/cloudflare/delivery-unit.d.ts +3 -3
  136. package/dist/types/cloudflare/worker-manifest.d.ts +2 -2
  137. package/dist/types/cloudflare/wrangler-config.d.ts +6 -0
  138. package/dist/types/cloudflare-builder.d.ts +49 -9
  139. package/dist/types/cloudflare-output-contract.d.ts +4 -1
  140. package/dist/types/cloudflare-output-verifier/identity.d.ts +1 -1
  141. package/dist/types/cloudflare-output-verifier/worker-bundles.d.ts +2 -2
  142. package/dist/types/config.d.ts +29 -1
  143. package/dist/types/deploy-output/npmAliases.d.ts +14 -0
  144. package/dist/types/deploy-output/plugin.d.ts +35 -0
  145. package/dist/types/deploy-output/public-assets.d.ts +51 -0
  146. package/dist/types/deploy-output/target.d.ts +29 -0
  147. package/dist/types/deploy-output/trace-files.d.ts +17 -0
  148. package/dist/types/native-type-checker.d.ts +15 -0
  149. package/dist/types/policy-defaults.d.ts +46 -0
  150. package/dist/types/precompress/LICENSE +20 -0
  151. package/dist/types/precompress/index.d.ts +76 -0
  152. package/dist/types/release-envelope/framework-output.d.ts +8 -3
  153. package/dist/types/release-envelope/plugin.d.ts +7 -7
  154. package/dist/types/runtime-package-resolution.d.ts +28 -0
  155. package/dist/types/templates/cloudflare-worker-route-data.d.mts +14 -0
  156. package/package.json +97 -15
  157. package/src/backend-federation-build/codegen.ts +254 -0
  158. package/src/backend-federation-build/config.ts +492 -0
  159. package/src/backend-federation-build/emit.ts +213 -0
  160. package/src/backend-federation-build/index.ts +2 -0
  161. package/src/build-config/build-environment.ts +160 -0
  162. package/src/build-config/precompress/plugin.ts +140 -0
  163. package/src/build-config/precompress/types.ts +26 -0
  164. package/src/build-config/public.ts +5 -0
  165. package/src/build-config/remote-address.ts +33 -0
  166. package/src/cloudflare/artifacts.ts +22 -43
  167. package/src/cloudflare/constants.ts +1 -1
  168. package/src/cloudflare/delivery-unit.ts +42 -53
  169. package/src/cloudflare/index.ts +24 -11
  170. package/src/cloudflare/worker-manifest.ts +20 -3
  171. package/src/cloudflare/wrangler-config.ts +86 -10
  172. package/src/cloudflare-builder.ts +475 -151
  173. package/src/cloudflare-output-contract.ts +70 -0
  174. package/src/cloudflare-output-verifier/identity.ts +5 -1
  175. package/src/cloudflare-output-verifier/index.ts +20 -0
  176. package/src/cloudflare-output-verifier/manifest.ts +77 -0
  177. package/src/cloudflare-output-verifier/mutation-policy.ts +11 -1
  178. package/src/cloudflare-output-verifier/worker-bundles.ts +32 -14
  179. package/src/config.ts +34 -6
  180. package/src/deploy-output/npmAliases.ts +382 -0
  181. package/src/deploy-output/plugin.ts +86 -0
  182. package/src/deploy-output/public-assets.ts +224 -0
  183. package/src/deploy-output/target.ts +106 -0
  184. package/src/deploy-output/trace-files.ts +67 -0
  185. package/src/native-type-checker.ts +183 -0
  186. package/src/policy-defaults.ts +127 -0
  187. package/src/release-envelope/framework-output.ts +346 -94
  188. package/src/release-envelope/index.ts +26 -5
  189. package/src/release-envelope/plugin.ts +43 -40
  190. package/src/release-identity.ts +13 -2
  191. package/src/runtime-package-resolution.ts +115 -0
  192. package/src/templates/cloudflare-entry.001-bootstrap-security.mjs +0 -3
  193. package/src/templates/cloudflare-entry.002-assets-routes.mjs +1 -1
  194. package/src/templates/cloudflare-entry.004-rendering-css.mjs +5 -1
  195. package/src/templates/cloudflare-entry.005-worker-dispatch.mjs +98 -10
  196. package/src/templates/cloudflare-entry.006-fetch-handler.mjs +9 -2
  197. package/src/templates/cloudflare-worker-route-data.mjs +1069 -0
  198. package/dist/cjs/templates/cloudflare-worker-fs-promises.mjs +0 -11
  199. package/dist/cjs/templates/cloudflare-worker-loadable-server.mjs +0 -281
  200. package/dist/cjs/templates/cloudflare-worker-mf-ssr-runtime-plugin.mjs +0 -14
  201. package/dist/cjs/templates/cloudflare-worker-path.mjs +0 -98
  202. package/dist/esm/templates/cloudflare-worker-fs-promises.mjs +0 -11
  203. package/dist/esm/templates/cloudflare-worker-loadable-server.mjs +0 -281
  204. package/dist/esm/templates/cloudflare-worker-mf-ssr-runtime-plugin.mjs +0 -14
  205. package/dist/esm/templates/cloudflare-worker-path.mjs +0 -98
  206. package/dist/esm-node/templates/cloudflare-worker-fs-promises.mjs +0 -11
  207. package/dist/esm-node/templates/cloudflare-worker-loadable-server.mjs +0 -281
  208. package/dist/esm-node/templates/cloudflare-worker-mf-ssr-runtime-plugin.mjs +0 -14
  209. package/dist/esm-node/templates/cloudflare-worker-path.mjs +0 -98
  210. package/dist/types/templates/cloudflare-worker-fs-promises.d.mts +0 -5
  211. package/dist/types/templates/cloudflare-worker-loadable-server.d.mts +0 -48
  212. package/dist/types/templates/cloudflare-worker-mf-ssr-runtime-plugin.d.mts +0 -13
  213. package/dist/types/templates/cloudflare-worker-path.d.mts +0 -21
  214. package/src/templates/cloudflare-worker-fs-promises.mjs +0 -11
  215. package/src/templates/cloudflare-worker-loadable-server.mjs +0 -281
  216. package/src/templates/cloudflare-worker-mf-ssr-runtime-plugin.mjs +0 -14
  217. package/src/templates/cloudflare-worker-path.mjs +0 -98
@@ -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,
@@ -1,4 +1,14 @@
1
1
  import path from 'node:path';
2
+ import type { NodePublicAssetConfig } from '../config';
3
+ import {
4
+ NODE_PUBLIC_ASSET_SCOPE,
5
+ normalizeDeclaredPublicAssets,
6
+ resolveAddedDeclaredPublicAssetPaths,
7
+ } from '../deploy-output/public-assets';
8
+ import {
9
+ getDeployingTarget,
10
+ type ResolvedDeployTarget,
11
+ } from '../deploy-output/target';
2
12
  import {
3
13
  emitFrameworkMicroVerticalReleaseEnvelope,
4
14
  emitNodeStagedReleaseEnvelope,
@@ -6,10 +16,8 @@ import {
6
16
  verifyNodeReleaseEnvelopeStaging,
7
17
  } from './framework-output';
8
18
 
9
- type ReleaseEnvelopeTarget = 'cloudflare' | 'node' | string;
10
-
11
19
  export interface ReleaseEnvelopeConfig {
12
- deploy?: { target?: string };
20
+ deploy?: { node?: { publicAssets?: NodePublicAssetConfig[] } };
13
21
  }
14
22
 
15
23
  export interface ReleaseEnvelopeAppContext {
@@ -17,6 +25,7 @@ export interface ReleaseEnvelopeAppContext {
17
25
  appDirectory: string;
18
26
  distDirectory: string;
19
27
  metaName: string;
28
+ deployTarget?: ResolvedDeployTarget;
20
29
  }
21
30
 
22
31
  export interface ReleaseEnvelopePluginApi<
@@ -38,60 +47,46 @@ export interface ReleaseEnvelopePlugin<
38
47
  setup(api: ReleaseEnvelopePluginApi<Config>): void;
39
48
  }
40
49
 
41
- export type ResolveDeployTarget<
42
- Config extends ReleaseEnvelopeConfig = ReleaseEnvelopeConfig,
43
- > = (config: Config) => ReleaseEnvelopeTarget;
44
-
45
- const resolveActiveDeployTarget = <Config extends ReleaseEnvelopeConfig>(
46
- api: ReleaseEnvelopePluginApi<Config>,
47
- resolveDeployTarget: ResolveDeployTarget<Config>,
48
- ) => {
49
- const { metaName } = api.getAppContext();
50
- const config = api.getNormalizedConfig();
51
- if (
52
- metaName !== 'modern-js' &&
53
- !config.deploy?.target &&
54
- !process.env.MODERNJS_DEPLOY
55
- ) {
56
- return undefined;
57
- }
58
- return resolveDeployTarget(config);
59
- };
60
-
61
50
  export const createUltramodernReleaseEnvelopePlugin = <
62
51
  Config extends ReleaseEnvelopeConfig,
63
- >({
64
- resolveDeployTarget,
65
- }: {
66
- resolveDeployTarget: ResolveDeployTarget<Config>;
67
- }): ReleaseEnvelopePlugin<Config> => {
52
+ >(): ReleaseEnvelopePlugin<Config> => {
68
53
  return {
69
54
  name: '@modern-js/ultramodern-release-envelope',
70
- pre: ['@modern-js/backend-federation-build', '@modern-js/plugin-bff'],
55
+ pre: [
56
+ '@modern-js/backend-federation-build',
57
+ '@modern-js/plugin-bff',
58
+ '@modern-js/deploy-output-aliases',
59
+ '@modern-js/deploy-output-public-assets',
60
+ ],
71
61
  post: ['@modern-js/plugin-deploy'],
72
62
  setup(api) {
73
- const emitBuildEnvelope = async (target: 'node' | 'cloudflare') => {
63
+ const emitBuildEnvelope = async (
64
+ target: 'node' | 'cloudflare',
65
+ requirePromotable = true,
66
+ ) => {
74
67
  const { apiOnly, distDirectory } = api.getAppContext();
75
68
  await emitFrameworkMicroVerticalReleaseEnvelope({
76
69
  apiOnly,
77
70
  distDirectory,
71
+ requirePromotable,
78
72
  target,
79
73
  });
80
74
  };
81
75
 
82
76
  api.onAfterBuild(async () => {
83
- const configuredTarget = resolveDeployTarget(api.getNormalizedConfig());
77
+ const configuredTarget = api.getAppContext().deployTarget?.target;
84
78
  if (configuredTarget !== 'node' && configuredTarget !== 'cloudflare') {
85
79
  return;
86
80
  }
87
- await emitBuildEnvelope(configuredTarget);
81
+ // A plain `modern build` is the development path: a dirty or non-Git
82
+ // checkout resolves to the `workspace` source revision, which is not
83
+ // promotable but must still build. Deploy re-emits the envelope and
84
+ // verifies it, so the release gate keeps its teeth there.
85
+ await emitBuildEnvelope(configuredTarget, false);
88
86
  });
89
87
 
90
88
  api.onBeforeDeploy(async () => {
91
- const configuredTarget = resolveActiveDeployTarget(
92
- api,
93
- resolveDeployTarget,
94
- );
89
+ const configuredTarget = getDeployingTarget(api.getAppContext());
95
90
  if (configuredTarget !== 'node') {
96
91
  return;
97
92
  }
@@ -101,16 +96,24 @@ export const createUltramodernReleaseEnvelopePlugin = <
101
96
  });
102
97
 
103
98
  api.onAfterDeploy(async () => {
104
- const configuredTarget = resolveActiveDeployTarget(
105
- api,
106
- resolveDeployTarget,
107
- );
99
+ const configuredTarget = getDeployingTarget(api.getAppContext());
108
100
  if (configuredTarget !== 'node') {
109
101
  return;
110
102
  }
111
103
  const { appDirectory, distDirectory } = api.getAppContext();
112
104
  const outputDirectory = path.join(appDirectory, '.output');
113
105
  const releaseEnvelope = await emitNodeStagedReleaseEnvelope({
106
+ // The Node deploy output is a copy of dist, so a declared file
107
+ // added output exactly when dist has no file at its path.
108
+ declaredPublicAssets: await resolveAddedDeclaredPublicAssetPaths({
109
+ appDirectory,
110
+ assets: normalizeDeclaredPublicAssets(
111
+ api.getNormalizedConfig().deploy?.node?.publicAssets,
112
+ NODE_PUBLIC_ASSET_SCOPE,
113
+ ),
114
+ generatedRoot: distDirectory,
115
+ scope: NODE_PUBLIC_ASSET_SCOPE,
116
+ }),
114
117
  distDirectory,
115
118
  outputDirectory,
116
119
  });
@@ -10,13 +10,24 @@ export type UltramodernReleaseIdentity = {
10
10
  sourceRevision: string;
11
11
  };
12
12
 
13
+ // Inherited GIT_* variables (git hooks export GIT_DIR and GIT_INDEX_FILE)
14
+ // would make git describe some other repository than `workspaceRoot`.
15
+ const workspaceGitEnv = (): NodeJS.ProcessEnv =>
16
+ Object.fromEntries(
17
+ Object.entries(process.env).filter(
18
+ ([name]) => !name.toUpperCase().startsWith('GIT_'),
19
+ ),
20
+ );
21
+
13
22
  const gitOutput = (
14
23
  workspaceRoot: string,
15
24
  args: string[],
16
25
  ): string | undefined => {
17
26
  try {
18
- return execFileSync('git', ['-C', workspaceRoot, ...args], {
27
+ return execFileSync('git', args, {
28
+ cwd: workspaceRoot,
19
29
  encoding: 'utf8',
30
+ env: workspaceGitEnv(),
20
31
  stdio: ['ignore', 'pipe', 'ignore'],
21
32
  }).trim();
22
33
  } catch {
@@ -84,7 +95,7 @@ export const resolveUltramodernSourceRevision = (
84
95
  const revision = gitOutput(gitRoot, ['rev-parse', 'HEAD']);
85
96
  const status = gitOutput(gitRoot, [
86
97
  'status',
87
- '--porcelain=v1',
98
+ '--porcelain',
88
99
  '--untracked-files=all',
89
100
  ]);
90
101
  if (!revision || status === undefined || status.length > 0) {
@@ -0,0 +1,115 @@
1
+ import fs from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import type { RsbuildPlugin, Rspack } from '@rsbuild/core';
6
+
7
+ /**
8
+ * A generated `runtime-register.js` lives inside the *application*, so every
9
+ * runtime plugin it imports has to resolve from the application's own
10
+ * `node_modules`. Packages registered by a CLI plugin are dependencies of that
11
+ * CLI plugin, not of the application, and under an isolated (pnpm) linker the
12
+ * application cannot see them — the build fails with `Module not found`.
13
+ *
14
+ * Instead of freezing a single resolved file (which would lock one export
15
+ * condition and break the web/node split), the registering package contributes
16
+ * the `node_modules` directory that actually hosts the package as a *fallback*
17
+ * module directory. Bare specifiers keep their normal `exports` resolution, the
18
+ * application's own copy still wins when it declares the dependency, and both
19
+ * the workspace source layout and a published pnpm layout work.
20
+ */
21
+ export const findHostingModuleDirectory = (
22
+ packageName: string,
23
+ fromDirectory: string,
24
+ ): string | undefined => {
25
+ let directory = path.resolve(fromDirectory);
26
+ for (;;) {
27
+ const candidate = path.join(directory, 'node_modules');
28
+ if (fs.existsSync(path.join(candidate, packageName))) {
29
+ return candidate;
30
+ }
31
+ const parent = path.dirname(directory);
32
+ if (parent === directory) {
33
+ return undefined;
34
+ }
35
+ directory = parent;
36
+ }
37
+ };
38
+
39
+ /**
40
+ * Collect the fallback module directories for the given packages, starting the
41
+ * search at the module that registers them.
42
+ */
43
+ export const collectRuntimePackageModuleDirectories = (
44
+ packageNames: readonly string[],
45
+ registrarUrl: string,
46
+ ): string[] => {
47
+ const registrarDirectory = path.dirname(fileURLToPath(registrarUrl));
48
+ const directories: string[] = [];
49
+ for (const packageName of packageNames) {
50
+ const directory = findHostingModuleDirectory(
51
+ packageName,
52
+ registrarDirectory,
53
+ );
54
+ if (directory && !directories.includes(directory)) {
55
+ directories.push(directory);
56
+ }
57
+ }
58
+ return directories;
59
+ };
60
+
61
+ const appendModuleDirectories = (
62
+ config: Rspack.Configuration,
63
+ directories: readonly string[],
64
+ ) => {
65
+ if (directories.length === 0) {
66
+ return;
67
+ }
68
+ config.resolve ??= {};
69
+ const existing = config.resolve.modules ?? ['node_modules'];
70
+ // Appended, never prepended: the application's own upward `node_modules`
71
+ // search keeps priority, so a declared dependency still wins.
72
+ config.resolve.modules = [
73
+ ...existing,
74
+ ...directories.filter(directory => !existing.includes(directory)),
75
+ ];
76
+ };
77
+
78
+ /**
79
+ * Rsbuild plugin that adds the given directories as fallback module roots for
80
+ * every environment (web, node and worker bundles alike).
81
+ */
82
+ export const createRuntimePackageResolutionPlugin = (
83
+ directories: readonly string[],
84
+ ): RsbuildPlugin => ({
85
+ name: 'ultramodern:runtime-package-resolution',
86
+ setup(api) {
87
+ api.modifyRspackConfig({
88
+ order: 'post',
89
+ handler(config) {
90
+ appendModuleDirectories(config, directories);
91
+ },
92
+ });
93
+ api.onBeforeCreateCompiler({
94
+ order: 'post',
95
+ handler({ bundlerConfigs }) {
96
+ for (const config of bundlerConfigs) {
97
+ appendModuleDirectories(config, directories);
98
+ }
99
+ },
100
+ });
101
+ },
102
+ });
103
+
104
+ /** `true` when the package resolves from the registering module at all. */
105
+ export const canResolveRuntimePackage = (
106
+ packageName: string,
107
+ registrarUrl: string,
108
+ ): boolean => {
109
+ try {
110
+ createRequire(registrarUrl).resolve(packageName);
111
+ return true;
112
+ } catch {
113
+ return false;
114
+ }
115
+ };
@@ -24,9 +24,6 @@ const ASSET_CORS_HEADERS = {
24
24
  'access-control-allow-origin': '*',
25
25
  };
26
26
 
27
- globalThis.__dirname ??= '/';
28
- globalThis.__filename ??= '/index.js';
29
-
30
27
  function getAllowedAppCorsOrigin(request) {
31
28
  if (APP_CORS_ALLOWED_ORIGINS.length === 0) {
32
29
  return null;
@@ -20,7 +20,7 @@ function withAssetHeaders(response, request) {
20
20
  });
21
21
  }
22
22
 
23
- async function createCorsPreflightResponse(request, env) {
23
+ async function createCorsPreflightResponse(request, _env) {
24
24
  if (request.method !== 'OPTIONS') {
25
25
  return null;
26
26
  }
@@ -2,9 +2,13 @@ function createNoopMonitors() {
2
2
  const noop = () => {};
3
3
 
4
4
  return {
5
+ counter: noop,
5
6
  debug: noop,
6
7
  error: noop,
7
8
  info: noop,
9
+ push: noop,
10
+ timing: noop,
11
+ trace: noop,
8
12
  warn: noop,
9
13
  };
10
14
  }
@@ -1134,7 +1138,7 @@ async function withRouteCssLinks(
1134
1138
  seenCssHrefs.add(entry.preloadHref);
1135
1139
  return true;
1136
1140
  });
1137
- for (const { href, preloadHref } of uniqueCssEntries) {
1141
+ for (const { preloadHref } of uniqueCssEntries) {
1138
1142
  const preloadUrl = new URL(preloadHref);
1139
1143
  const preloadReference =
1140
1144
  preloadUrl.origin === requestOrigin
@@ -52,6 +52,52 @@ async function getRequestHandler(workerModule) {
52
52
  );
53
53
  }
54
54
 
55
+ const ROUTE_DATA_REQUEST_PARAM = '__loader';
56
+
57
+ // Route data requests (`?__loader=`) are answered by the entry's route data
58
+ // worker, like the Node server's data handler. Without a match the request
59
+ // falls through to page rendering, as it does on Node.
60
+ async function dispatchRouteDataRequest(route, request) {
61
+ const routeDataWorkerPath = route.routeDataWorker;
62
+ if (
63
+ !routeDataWorkerPath ||
64
+ !new URL(request.url).searchParams.has(ROUTE_DATA_REQUEST_PARAM)
65
+ ) {
66
+ return undefined;
67
+ }
68
+
69
+ const routeDataWorkerModule = await loadWorkerModule(routeDataWorkerPath);
70
+ const handleRouteDataRequest = routeDataWorkerModule
71
+ ? getRuntimeModule(routeDataWorkerModule).handleRouteDataRequest
72
+ : undefined;
73
+
74
+ if (typeof handleRouteDataRequest !== 'function') {
75
+ return new Response(
76
+ `Route data worker bundle has no handleRouteDataRequest export: ${routeDataWorkerPath}`,
77
+ {
78
+ status: 500,
79
+ headers: {
80
+ 'content-type': 'text/plain; charset=utf-8',
81
+ 'x-modern-js-route-data-worker': routeDataWorkerPath,
82
+ },
83
+ },
84
+ );
85
+ }
86
+
87
+ return handleRouteDataRequest({
88
+ request,
89
+ serverRoutes: MODERN_WORKER_MANIFEST.routeSpec.routes,
90
+ context: {
91
+ loaderContext: new Map(),
92
+ monitors: createNoopMonitors(),
93
+ reporter: {
94
+ reportTiming: () => {},
95
+ },
96
+ },
97
+ onTiming() {},
98
+ });
99
+ }
100
+
55
101
  async function dispatchRouteWorker(route, request, env, ctx) {
56
102
  const workerPath = route.worker;
57
103
  if (!workerPath) {
@@ -114,14 +160,46 @@ async function dispatchRouteWorker(route, request, env, ctx) {
114
160
  );
115
161
  }
116
162
 
117
- function matchesPrefix(pathname, prefix) {
163
+ // Worker Static Assets decode percent-encoded paths, so an encoded spelling
164
+ // of a mounted prefix (`/%70refix/...`, `/prefix%2F...`) would otherwise skip
165
+ // the BFF or service binding that owns the prefix and reach ASSETS directly.
166
+ // Resolve such spellings to the canonical pathname the owner sees; a
167
+ // backslash or undecodable spelling becomes a path the owner rejects.
168
+ function resolvePrefixPathname(pathname, prefix) {
118
169
  if (!prefix || prefix === '/') {
119
- return true;
170
+ return pathname;
120
171
  }
121
172
 
122
173
  const normalized = prefix.endsWith('/') ? prefix.slice(0, -1) : prefix;
174
+ let candidate = pathname;
175
+
176
+ for (let attempt = 0; attempt < 4; attempt += 1) {
177
+ if (candidate === normalized || candidate.startsWith(`${normalized}/`)) {
178
+ return candidate;
179
+ }
180
+ if (candidate.startsWith(`${normalized}\\`)) {
181
+ return `${normalized}/__invalid_encoded_path__`;
182
+ }
123
183
 
124
- return pathname === normalized || pathname.startsWith(`${normalized}/`);
184
+ let decoded;
185
+ try {
186
+ decoded = decodeURIComponent(candidate);
187
+ } catch {
188
+ return candidate.toLowerCase().startsWith(normalized.toLowerCase())
189
+ ? `${normalized}/__invalid_encoded_path__`
190
+ : null;
191
+ }
192
+ if (decoded === candidate) {
193
+ return null;
194
+ }
195
+ candidate = decoded;
196
+ }
197
+
198
+ return null;
199
+ }
200
+
201
+ function matchesPrefix(pathname, prefix) {
202
+ return resolvePrefixPathname(pathname, prefix) !== null;
125
203
  }
126
204
 
127
205
  function createRequestForMountedPrefix(request, prefix) {
@@ -131,12 +209,13 @@ function createRequestForMountedPrefix(request, prefix) {
131
209
 
132
210
  const url = new URL(request.url);
133
211
  const normalized = prefix.endsWith('/') ? prefix.slice(0, -1) : prefix;
212
+ const matchedPathname = resolvePrefixPathname(url.pathname, normalized);
134
213
 
135
- if (!matchesPrefix(url.pathname, normalized)) {
214
+ if (matchedPathname === null) {
136
215
  return request;
137
216
  }
138
217
 
139
- const nextPath = url.pathname.slice(normalized.length) || '/';
218
+ const nextPath = matchedPathname.slice(normalized.length) || '/';
140
219
  url.pathname = nextPath.startsWith('/') ? nextPath : `/${nextPath}`;
141
220
 
142
221
  return new Request(url, request);
@@ -263,12 +342,21 @@ function getEffectBffDispatcher(bff, runtime) {
263
342
  async function dispatchBffRequest(request, env) {
264
343
  const bff = MODERN_WORKER_MANIFEST.bff;
265
344
 
266
- if (
267
- !bff?.worker ||
268
- !matchesPrefix(new URL(request.url).pathname, bff.prefix)
269
- ) {
345
+ const requestUrl = new URL(request.url);
346
+ const matchedPathname = resolvePrefixPathname(
347
+ requestUrl.pathname,
348
+ bff?.prefix,
349
+ );
350
+ if (!bff?.worker || matchedPathname === null) {
270
351
  return null;
271
352
  }
353
+ const canonicalRequest =
354
+ matchedPathname === requestUrl.pathname
355
+ ? request
356
+ : new Request(
357
+ Object.assign(requestUrl, { pathname: matchedPathname }),
358
+ request,
359
+ );
272
360
  if (bff.runtimeFramework !== 'effect') {
273
361
  return createEffectBffDispatcherErrorResponse(
274
362
  bff,
@@ -301,7 +389,7 @@ async function dispatchBffRequest(request, env) {
301
389
  return createEffectBffDispatcherErrorResponse(bff, error);
302
390
  }
303
391
 
304
- return effectDispatcher.dispatch(request, { env });
392
+ return effectDispatcher.dispatch(canonicalRequest, { env });
305
393
  }
306
394
 
307
395
  const directHandler =
@@ -62,11 +62,18 @@ export default {
62
62
  }
63
63
 
64
64
  if (route?.worker) {
65
- const renderableRequest = createRenderableRequest(request);
65
+ // Route data requests keep their method, as on the Node server.
66
+ const routeDataResponse = await dispatchRouteDataRequest(route, request);
66
67
 
67
68
  return finalizeResponseForRequest(
68
69
  withAppCorsHeaders(
69
- await dispatchRouteWorker(route, renderableRequest, env, ctx),
70
+ routeDataResponse ??
71
+ (await dispatchRouteWorker(
72
+ route,
73
+ createRenderableRequest(request),
74
+ env,
75
+ ctx,
76
+ )),
70
77
  request,
71
78
  ),
72
79
  request,