@bleedingdev/modern-js-app-tools-extensions 3.8.3-ultramodern.2

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 (198) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +5 -0
  3. package/dist/cjs/cloudflare/artifacts.js +127 -0
  4. package/dist/cjs/cloudflare/constants.js +141 -0
  5. package/dist/cjs/cloudflare/delivery-unit.js +161 -0
  6. package/dist/cjs/cloudflare/i18n-worker.js +135 -0
  7. package/dist/cjs/cloudflare/index.js +156 -0
  8. package/dist/cjs/cloudflare/security-policies.js +205 -0
  9. package/dist/cjs/cloudflare/types.js +18 -0
  10. package/dist/cjs/cloudflare/utils.js +66 -0
  11. package/dist/cjs/cloudflare/worker-manifest.js +239 -0
  12. package/dist/cjs/cloudflare/wrangler-config.js +205 -0
  13. package/dist/cjs/cloudflare-builder.js +394 -0
  14. package/dist/cjs/cloudflare-output-contract.js +108 -0
  15. package/dist/cjs/cloudflare-output-plan.js +93 -0
  16. package/dist/cjs/cloudflare-output-verifier/identity.js +92 -0
  17. package/dist/cjs/cloudflare-output-verifier/index.js +188 -0
  18. package/dist/cjs/cloudflare-output-verifier/issues.js +54 -0
  19. package/dist/cjs/cloudflare-output-verifier/manifest.js +89 -0
  20. package/dist/cjs/cloudflare-output-verifier/mutation-policy.js +151 -0
  21. package/dist/cjs/cloudflare-output-verifier/worker-bundles.js +378 -0
  22. package/dist/cjs/config.js +18 -0
  23. package/dist/cjs/css-extract-runtime-plugin.js +74 -0
  24. package/dist/cjs/index.js +125 -0
  25. package/dist/cjs/read-template.js +56 -0
  26. package/dist/cjs/release-envelope/canonical.js +68 -0
  27. package/dist/cjs/release-envelope/framework-output.js +649 -0
  28. package/dist/cjs/release-envelope/index.js +399 -0
  29. package/dist/cjs/release-envelope/plugin.js +102 -0
  30. package/dist/cjs/release-envelope/types.js +51 -0
  31. package/dist/cjs/release-identity.js +110 -0
  32. package/dist/cjs/templates/cloudflare-entry.001-bootstrap-security.mjs +251 -0
  33. package/dist/cjs/templates/cloudflare-entry.002-assets-routes.mjs +191 -0
  34. package/dist/cjs/templates/cloudflare-entry.003-i18n-locales.mjs +514 -0
  35. package/dist/cjs/templates/cloudflare-entry.004-rendering-css.mjs +1194 -0
  36. package/dist/cjs/templates/cloudflare-entry.005-worker-dispatch.mjs +421 -0
  37. package/dist/cjs/templates/cloudflare-entry.006-fetch-handler.mjs +93 -0
  38. package/dist/cjs/templates/cloudflare-worker-fs-promises.mjs +11 -0
  39. package/dist/cjs/templates/cloudflare-worker-loadable-server.mjs +281 -0
  40. package/dist/cjs/templates/cloudflare-worker-mf-ssr-runtime-plugin.mjs +14 -0
  41. package/dist/cjs/templates/cloudflare-worker-path.mjs +98 -0
  42. package/dist/esm/cloudflare/artifacts.mjs +64 -0
  43. package/dist/esm/cloudflare/constants.mjs +34 -0
  44. package/dist/esm/cloudflare/delivery-unit.mjs +110 -0
  45. package/dist/esm/cloudflare/i18n-worker.mjs +97 -0
  46. package/dist/esm/cloudflare/index.mjs +108 -0
  47. package/dist/esm/cloudflare/security-policies.mjs +167 -0
  48. package/dist/esm/cloudflare/types.mjs +0 -0
  49. package/dist/esm/cloudflare/utils.mjs +12 -0
  50. package/dist/esm/cloudflare/worker-manifest.mjs +179 -0
  51. package/dist/esm/cloudflare/wrangler-config.mjs +151 -0
  52. package/dist/esm/cloudflare-builder.mjs +327 -0
  53. package/dist/esm/cloudflare-output-contract.mjs +31 -0
  54. package/dist/esm/cloudflare-output-plan.mjs +45 -0
  55. package/dist/esm/cloudflare-output-verifier/identity.mjs +54 -0
  56. package/dist/esm/cloudflare-output-verifier/index.mjs +133 -0
  57. package/dist/esm/cloudflare-output-verifier/issues.mjs +10 -0
  58. package/dist/esm/cloudflare-output-verifier/manifest.mjs +51 -0
  59. package/dist/esm/cloudflare-output-verifier/mutation-policy.mjs +99 -0
  60. package/dist/esm/cloudflare-output-verifier/worker-bundles.mjs +313 -0
  61. package/dist/esm/config.mjs +0 -0
  62. package/dist/esm/css-extract-runtime-plugin.mjs +32 -0
  63. package/dist/esm/index.mjs +10 -0
  64. package/dist/esm/read-template.mjs +5 -0
  65. package/dist/esm/release-envelope/canonical.mjs +21 -0
  66. package/dist/esm/release-envelope/framework-output.mjs +573 -0
  67. package/dist/esm/release-envelope/index.mjs +335 -0
  68. package/dist/esm/release-envelope/plugin.mjs +54 -0
  69. package/dist/esm/release-envelope/types.mjs +7 -0
  70. package/dist/esm/release-identity.mjs +66 -0
  71. package/dist/esm/templates/cloudflare-entry.001-bootstrap-security.mjs +251 -0
  72. package/dist/esm/templates/cloudflare-entry.002-assets-routes.mjs +191 -0
  73. package/dist/esm/templates/cloudflare-entry.003-i18n-locales.mjs +514 -0
  74. package/dist/esm/templates/cloudflare-entry.004-rendering-css.mjs +1194 -0
  75. package/dist/esm/templates/cloudflare-entry.005-worker-dispatch.mjs +421 -0
  76. package/dist/esm/templates/cloudflare-entry.006-fetch-handler.mjs +93 -0
  77. package/dist/esm/templates/cloudflare-worker-fs-promises.mjs +11 -0
  78. package/dist/esm/templates/cloudflare-worker-loadable-server.mjs +281 -0
  79. package/dist/esm/templates/cloudflare-worker-mf-ssr-runtime-plugin.mjs +14 -0
  80. package/dist/esm/templates/cloudflare-worker-path.mjs +98 -0
  81. package/dist/esm-node/cloudflare/artifacts.mjs +65 -0
  82. package/dist/esm-node/cloudflare/constants.mjs +35 -0
  83. package/dist/esm-node/cloudflare/delivery-unit.mjs +111 -0
  84. package/dist/esm-node/cloudflare/i18n-worker.mjs +98 -0
  85. package/dist/esm-node/cloudflare/index.mjs +109 -0
  86. package/dist/esm-node/cloudflare/security-policies.mjs +168 -0
  87. package/dist/esm-node/cloudflare/types.mjs +1 -0
  88. package/dist/esm-node/cloudflare/utils.mjs +13 -0
  89. package/dist/esm-node/cloudflare/worker-manifest.mjs +180 -0
  90. package/dist/esm-node/cloudflare/wrangler-config.mjs +152 -0
  91. package/dist/esm-node/cloudflare-builder.mjs +328 -0
  92. package/dist/esm-node/cloudflare-output-contract.mjs +32 -0
  93. package/dist/esm-node/cloudflare-output-plan.mjs +46 -0
  94. package/dist/esm-node/cloudflare-output-verifier/identity.mjs +55 -0
  95. package/dist/esm-node/cloudflare-output-verifier/index.mjs +134 -0
  96. package/dist/esm-node/cloudflare-output-verifier/issues.mjs +11 -0
  97. package/dist/esm-node/cloudflare-output-verifier/manifest.mjs +52 -0
  98. package/dist/esm-node/cloudflare-output-verifier/mutation-policy.mjs +100 -0
  99. package/dist/esm-node/cloudflare-output-verifier/worker-bundles.mjs +314 -0
  100. package/dist/esm-node/config.mjs +1 -0
  101. package/dist/esm-node/css-extract-runtime-plugin.mjs +33 -0
  102. package/dist/esm-node/index.mjs +11 -0
  103. package/dist/esm-node/read-template.mjs +9 -0
  104. package/dist/esm-node/release-envelope/canonical.mjs +22 -0
  105. package/dist/esm-node/release-envelope/framework-output.mjs +574 -0
  106. package/dist/esm-node/release-envelope/index.mjs +336 -0
  107. package/dist/esm-node/release-envelope/plugin.mjs +55 -0
  108. package/dist/esm-node/release-envelope/types.mjs +8 -0
  109. package/dist/esm-node/release-identity.mjs +67 -0
  110. package/dist/esm-node/templates/cloudflare-entry.001-bootstrap-security.mjs +251 -0
  111. package/dist/esm-node/templates/cloudflare-entry.002-assets-routes.mjs +191 -0
  112. package/dist/esm-node/templates/cloudflare-entry.003-i18n-locales.mjs +514 -0
  113. package/dist/esm-node/templates/cloudflare-entry.004-rendering-css.mjs +1194 -0
  114. package/dist/esm-node/templates/cloudflare-entry.005-worker-dispatch.mjs +421 -0
  115. package/dist/esm-node/templates/cloudflare-entry.006-fetch-handler.mjs +93 -0
  116. package/dist/esm-node/templates/cloudflare-worker-fs-promises.mjs +11 -0
  117. package/dist/esm-node/templates/cloudflare-worker-loadable-server.mjs +281 -0
  118. package/dist/esm-node/templates/cloudflare-worker-mf-ssr-runtime-plugin.mjs +14 -0
  119. package/dist/esm-node/templates/cloudflare-worker-path.mjs +98 -0
  120. package/dist/types/cloudflare/artifacts.d.ts +15 -0
  121. package/dist/types/cloudflare/constants.d.ts +24 -0
  122. package/dist/types/cloudflare/delivery-unit.d.ts +23 -0
  123. package/dist/types/cloudflare/i18n-worker.d.ts +22 -0
  124. package/dist/types/cloudflare/index.d.ts +2 -0
  125. package/dist/types/cloudflare/security-policies.d.ts +52 -0
  126. package/dist/types/cloudflare/types.d.ts +28 -0
  127. package/dist/types/cloudflare/utils.d.ts +6 -0
  128. package/dist/types/cloudflare/worker-manifest.d.ts +149 -0
  129. package/dist/types/cloudflare/wrangler-config.d.ts +51 -0
  130. package/dist/types/cloudflare-builder.d.ts +59 -0
  131. package/dist/types/cloudflare-output-contract.d.ts +14 -0
  132. package/dist/types/cloudflare-output-plan.d.ts +34 -0
  133. package/dist/types/cloudflare-output-verifier/identity.d.ts +8 -0
  134. package/dist/types/cloudflare-output-verifier/index.d.ts +18 -0
  135. package/dist/types/cloudflare-output-verifier/issues.d.ts +14 -0
  136. package/dist/types/cloudflare-output-verifier/manifest.d.ts +2 -0
  137. package/dist/types/cloudflare-output-verifier/mutation-policy.d.ts +14 -0
  138. package/dist/types/cloudflare-output-verifier/worker-bundles.d.ts +20 -0
  139. package/dist/types/config.d.ts +175 -0
  140. package/dist/types/css-extract-runtime-plugin.d.ts +6 -0
  141. package/dist/types/index.d.ts +10 -0
  142. package/dist/types/read-template.d.ts +2 -0
  143. package/dist/types/release-envelope/canonical.d.ts +5 -0
  144. package/dist/types/release-envelope/framework-output.d.ts +32 -0
  145. package/dist/types/release-envelope/index.d.ts +6 -0
  146. package/dist/types/release-envelope/plugin.d.ts +30 -0
  147. package/dist/types/release-envelope/types.d.ts +62 -0
  148. package/dist/types/release-identity.d.ts +16 -0
  149. package/dist/types/templates/cloudflare-entry.001-bootstrap-security.d.mts +1 -0
  150. package/dist/types/templates/cloudflare-entry.002-assets-routes.d.mts +1 -0
  151. package/dist/types/templates/cloudflare-entry.003-i18n-locales.d.mts +1 -0
  152. package/dist/types/templates/cloudflare-entry.004-rendering-css.d.mts +1 -0
  153. package/dist/types/templates/cloudflare-entry.005-worker-dispatch.d.mts +1 -0
  154. package/dist/types/templates/cloudflare-entry.006-fetch-handler.d.mts +4 -0
  155. package/dist/types/templates/cloudflare-worker-fs-promises.d.mts +5 -0
  156. package/dist/types/templates/cloudflare-worker-loadable-server.d.mts +48 -0
  157. package/dist/types/templates/cloudflare-worker-mf-ssr-runtime-plugin.d.mts +13 -0
  158. package/dist/types/templates/cloudflare-worker-path.d.mts +21 -0
  159. package/package.json +131 -0
  160. package/src/cloudflare/artifacts.ts +165 -0
  161. package/src/cloudflare/constants.ts +50 -0
  162. package/src/cloudflare/delivery-unit.ts +195 -0
  163. package/src/cloudflare/i18n-worker.ts +197 -0
  164. package/src/cloudflare/index.ts +259 -0
  165. package/src/cloudflare/security-policies.ts +194 -0
  166. package/src/cloudflare/types.ts +33 -0
  167. package/src/cloudflare/utils.ts +40 -0
  168. package/src/cloudflare/worker-manifest.ts +313 -0
  169. package/src/cloudflare/wrangler-config.ts +338 -0
  170. package/src/cloudflare-builder.ts +653 -0
  171. package/src/cloudflare-output-contract.ts +30 -0
  172. package/src/cloudflare-output-plan.ts +108 -0
  173. package/src/cloudflare-output-verifier/identity.ts +87 -0
  174. package/src/cloudflare-output-verifier/index.ts +277 -0
  175. package/src/cloudflare-output-verifier/issues.ts +54 -0
  176. package/src/cloudflare-output-verifier/manifest.ts +76 -0
  177. package/src/cloudflare-output-verifier/mutation-policy.ts +159 -0
  178. package/src/cloudflare-output-verifier/worker-bundles.ts +664 -0
  179. package/src/config.ts +196 -0
  180. package/src/css-extract-runtime-plugin.ts +70 -0
  181. package/src/index.ts +10 -0
  182. package/src/read-template.ts +8 -0
  183. package/src/release-envelope/canonical.ts +58 -0
  184. package/src/release-envelope/framework-output.ts +1121 -0
  185. package/src/release-envelope/index.ts +656 -0
  186. package/src/release-envelope/plugin.ts +125 -0
  187. package/src/release-envelope/types.ts +83 -0
  188. package/src/release-identity.ts +134 -0
  189. package/src/templates/cloudflare-entry.001-bootstrap-security.mjs +251 -0
  190. package/src/templates/cloudflare-entry.002-assets-routes.mjs +191 -0
  191. package/src/templates/cloudflare-entry.003-i18n-locales.mjs +514 -0
  192. package/src/templates/cloudflare-entry.004-rendering-css.mjs +1194 -0
  193. package/src/templates/cloudflare-entry.005-worker-dispatch.mjs +421 -0
  194. package/src/templates/cloudflare-entry.006-fetch-handler.mjs +93 -0
  195. package/src/templates/cloudflare-worker-fs-promises.mjs +11 -0
  196. package/src/templates/cloudflare-worker-loadable-server.mjs +281 -0
  197. package/src/templates/cloudflare-worker-mf-ssr-runtime-plugin.mjs +14 -0
  198. package/src/templates/cloudflare-worker-path.mjs +98 -0
@@ -0,0 +1,194 @@
1
+ import type { CloudflareWorkerSecurityConfig } from '../config';
2
+ import type { CloudflareModernConfig } from './types';
3
+
4
+ const DEFAULT_SECURITY_HEADERS = {
5
+ referrerPolicy: 'strict-origin-when-cross-origin',
6
+ contentTypeOptions: 'nosniff',
7
+ permissionsPolicy:
8
+ 'camera=(), geolocation=(), microphone=(), payment=(), usb=()',
9
+ } as const;
10
+ const DEFAULT_CORS_ALLOWED_METHODS = [
11
+ 'GET',
12
+ 'HEAD',
13
+ 'POST',
14
+ 'PUT',
15
+ 'PATCH',
16
+ 'DELETE',
17
+ 'OPTIONS',
18
+ ];
19
+ const DEFAULT_CSP_DIRECTIVES: Record<string, string[]> = {
20
+ 'base-uri': [`'self'`],
21
+ 'connect-src': [`'self'`, 'https:', 'http:', 'wss:', 'ws:'],
22
+ 'default-src': [`'self'`],
23
+ 'font-src': [`'self'`, 'data:', 'https:', 'http:'],
24
+ 'form-action': [`'self'`],
25
+ 'frame-ancestors': [`'self'`],
26
+ 'img-src': [`'self'`, 'data:', 'blob:', 'https:', 'http:'],
27
+ 'manifest-src': [`'self'`, 'https:', 'http:'],
28
+ 'object-src': [`'none'`],
29
+ 'script-src': [
30
+ `'self'`,
31
+ `'unsafe-inline'`,
32
+ `'unsafe-eval'`,
33
+ 'https:',
34
+ 'http:',
35
+ 'blob:',
36
+ ],
37
+ 'style-src': [`'self'`, `'unsafe-inline'`, 'https:', 'http:'],
38
+ 'worker-src': [`'self'`, 'blob:'],
39
+ };
40
+
41
+ const normalizeDirectiveValues = (value: string[] | string) => {
42
+ const values = Array.isArray(value) ? value : [value];
43
+
44
+ return [...new Set(values.map(entry => entry.trim()).filter(Boolean))];
45
+ };
46
+
47
+ const appendDirectiveValues = (
48
+ directives: Record<string, string[]>,
49
+ name: string,
50
+ values: string[] | undefined,
51
+ ) => {
52
+ if (!values?.length) {
53
+ return;
54
+ }
55
+
56
+ directives[name] = normalizeDirectiveValues([
57
+ ...(directives[name] ?? []),
58
+ ...values,
59
+ ]);
60
+ };
61
+
62
+ const createContentSecurityPolicy = (
63
+ config?: CloudflareWorkerSecurityConfig['contentSecurityPolicy'],
64
+ ) => {
65
+ const mode = config?.mode ?? 'report-only';
66
+
67
+ if (mode === 'off') {
68
+ return {
69
+ mode,
70
+ directives: {},
71
+ reason: config?.reason,
72
+ };
73
+ }
74
+
75
+ const directives = Object.fromEntries(
76
+ Object.entries(DEFAULT_CSP_DIRECTIVES).map(([name, values]) => [
77
+ name,
78
+ [...values],
79
+ ]),
80
+ );
81
+
82
+ for (const [name, value] of Object.entries(config?.directives ?? {})) {
83
+ if (value === false) {
84
+ delete directives[name];
85
+ } else {
86
+ directives[name] = normalizeDirectiveValues(value);
87
+ }
88
+ }
89
+
90
+ if (config?.frameAncestors === false) {
91
+ delete directives['frame-ancestors'];
92
+ } else if (config?.frameAncestors) {
93
+ directives['frame-ancestors'] = normalizeDirectiveValues(
94
+ config.frameAncestors,
95
+ );
96
+ }
97
+
98
+ appendDirectiveValues(directives, 'script-src', config?.additionalScriptSrc);
99
+ appendDirectiveValues(directives, 'style-src', config?.additionalStyleSrc);
100
+ appendDirectiveValues(
101
+ directives,
102
+ 'connect-src',
103
+ config?.additionalConnectSrc,
104
+ );
105
+ appendDirectiveValues(directives, 'img-src', config?.additionalImgSrc);
106
+
107
+ if (config?.reportUri) {
108
+ directives['report-uri'] = [config.reportUri];
109
+ }
110
+
111
+ return {
112
+ mode,
113
+ directives,
114
+ reason: config?.reason,
115
+ };
116
+ };
117
+
118
+ const createNoindexPolicy = (
119
+ noindex: CloudflareWorkerSecurityConfig['noindex'],
120
+ ) => {
121
+ if (noindex === false) {
122
+ return {
123
+ workersDev: false,
124
+ localhost: false,
125
+ previewHostnames: [],
126
+ };
127
+ }
128
+
129
+ if (noindex === true || noindex === undefined) {
130
+ return {
131
+ workersDev: true,
132
+ localhost: true,
133
+ previewHostnames: [],
134
+ };
135
+ }
136
+
137
+ return {
138
+ workersDev: noindex.workersDev ?? true,
139
+ localhost: noindex.localhost ?? true,
140
+ previewHostnames: noindex.previewHostnames ?? [],
141
+ reason: noindex.reason,
142
+ };
143
+ };
144
+
145
+ const createCloudflareWorkerCorsPolicy = (
146
+ cors: CloudflareWorkerSecurityConfig['cors'],
147
+ ) => ({
148
+ assets: cors?.assets ?? true,
149
+ allowedOrigins: normalizeDirectiveValues(cors?.allowedOrigins ?? []),
150
+ allowedMethods: cors?.allowedMethods?.length
151
+ ? normalizeDirectiveValues(
152
+ cors.allowedMethods.map(method => method.toUpperCase()),
153
+ )
154
+ : DEFAULT_CORS_ALLOWED_METHODS,
155
+ allowedHeaders: cors?.allowedHeaders?.length
156
+ ? normalizeDirectiveValues(cors.allowedHeaders)
157
+ : ['*'],
158
+ reason: cors?.reason,
159
+ });
160
+
161
+ export const createCloudflareWorkerSecurityPolicy = (
162
+ modernConfig: CloudflareModernConfig,
163
+ ) => {
164
+ const security = modernConfig.deploy?.worker?.security;
165
+
166
+ if (security?.enabled === false) {
167
+ return {
168
+ enabled: false,
169
+ cors: createCloudflareWorkerCorsPolicy(security.cors),
170
+ reason: security.reason,
171
+ };
172
+ }
173
+
174
+ return {
175
+ enabled: true,
176
+ headers: {
177
+ referrerPolicy:
178
+ security?.headers?.referrerPolicy ??
179
+ DEFAULT_SECURITY_HEADERS.referrerPolicy,
180
+ contentTypeOptions:
181
+ security?.headers?.contentTypeOptions ??
182
+ DEFAULT_SECURITY_HEADERS.contentTypeOptions,
183
+ permissionsPolicy:
184
+ security?.headers?.permissionsPolicy ??
185
+ DEFAULT_SECURITY_HEADERS.permissionsPolicy,
186
+ },
187
+ contentSecurityPolicy: createContentSecurityPolicy(
188
+ security?.contentSecurityPolicy,
189
+ ),
190
+ noindex: createNoindexPolicy(security?.noindex),
191
+ cors: createCloudflareWorkerCorsPolicy(security?.cors),
192
+ reason: security?.reason,
193
+ };
194
+ };
@@ -0,0 +1,33 @@
1
+ import type { BffUserConfig } from '@modern-js/server-core';
2
+ import type { ServerPlugin } from '@modern-js/types';
3
+ import type { CloudflareDeployConfig } from '../config';
4
+
5
+ export interface CloudflareAppContext {
6
+ apiOnly: boolean;
7
+ appDirectory: string;
8
+ distDirectory: string;
9
+ serverPlugins: ServerPlugin[];
10
+ }
11
+
12
+ export interface CloudflareModernConfig {
13
+ bff?: BffUserConfig;
14
+ deploy?: CloudflareDeployConfig;
15
+ }
16
+
17
+ export interface CloudflarePresetApi {
18
+ isPluginExists(name: string): boolean;
19
+ }
20
+
21
+ export type CloudflareDeployPreset = {
22
+ prepare?: () => Promise<void>;
23
+ writeOutput?: () => Promise<void>;
24
+ genEntry?: () => Promise<void>;
25
+ end?: () => Promise<void>;
26
+ };
27
+
28
+ export type CreateCloudflarePreset = (params: {
29
+ appContext: CloudflareAppContext;
30
+ modernConfig: CloudflareModernConfig;
31
+ api: CloudflarePresetApi;
32
+ needModernServer?: boolean;
33
+ }) => CloudflareDeployPreset;
@@ -0,0 +1,40 @@
1
+ import path from 'node:path';
2
+ import type { JsonValue } from '../config';
3
+
4
+ export const isRecord = (value: unknown): value is Record<string, unknown> =>
5
+ value !== null && typeof value === 'object' && !Array.isArray(value);
6
+
7
+ export const normalizeRelativePath = (
8
+ value: unknown,
9
+ label: string,
10
+ scope = 'app output',
11
+ options: { allowRoot?: boolean } = {},
12
+ ) => {
13
+ if (typeof value !== 'string') {
14
+ throw new Error(`${label} must be a relative path inside the ${scope}.`);
15
+ }
16
+
17
+ const normalized = value
18
+ .trim()
19
+ .replace(/\\/g, '/')
20
+ .replace(/^\.\/+/u, '')
21
+ .replace(/\/+$/u, '');
22
+ const segments = normalized.split('/');
23
+ const isRootDestination = normalized === '.';
24
+
25
+ if (
26
+ !normalized ||
27
+ path.isAbsolute(normalized) ||
28
+ (!options.allowRoot && isRootDestination) ||
29
+ segments.includes('..')
30
+ ) {
31
+ throw new Error(`${label} must be a relative path inside the ${scope}.`);
32
+ }
33
+
34
+ return normalized;
35
+ };
36
+
37
+ export const isJsonRecord = (
38
+ value: unknown,
39
+ ): value is Record<string, JsonValue> =>
40
+ typeof value === 'object' && value !== null && !Array.isArray(value);
@@ -0,0 +1,313 @@
1
+ import path from 'node:path';
2
+ import { fs as fse } from '@modern-js/utils';
3
+ import { readRouteSpec } from './artifacts';
4
+ import {
5
+ ASSETS_BINDING,
6
+ BFF_EFFECT_WORKER_DISPATCHER_EXPORT,
7
+ BFF_EFFECT_WORKER_ENTRY,
8
+ CLOUDFLARE_RUNTIME_TYPE,
9
+ CLOUDFLARE_WORKER_BUNDLE_FORMAT,
10
+ DEFAULT_SERVER_ONLY_PUBLIC_ASSET_EXCLUDES,
11
+ EFFECT_BFF_CLOUDFLARE_IMPORT_GUIDANCE,
12
+ LOADABLE_STATS_FILE,
13
+ PUBLIC_ASSETS_DIRECTORY,
14
+ ROUTE_MANIFEST_FILE,
15
+ ROUTE_SPEC_FILE,
16
+ ROUTE_SPEC_OUTPUT,
17
+ SERVER_BUNDLE_DIRECTORY,
18
+ WORKER_BUNDLE_DIRECTORY,
19
+ WORKER_ENTRY,
20
+ } from './constants';
21
+ import type { DeliveryUnitStamp } from './delivery-unit';
22
+ import { createI18nWorkerManifest } from './i18n-worker';
23
+ import { createCloudflareWorkerSecurityPolicy } from './security-policies';
24
+ import type { CloudflareAppContext, CloudflareModernConfig } from './types';
25
+ import { isRecord, normalizeRelativePath } from './utils';
26
+ import {
27
+ createWorkerManifestServiceBindings,
28
+ createWorkerServiceBindings,
29
+ } from './wrangler-config';
30
+
31
+ const createMissingEffectBffWorkerError = (
32
+ outputDirectory: string,
33
+ worker: string,
34
+ ) =>
35
+ new Error(
36
+ `Cloudflare Effect API runtime is configured, but the BFF worker bundle is missing: ${path.join(
37
+ outputDirectory,
38
+ worker,
39
+ )}. ${EFFECT_BFF_CLOUDFLARE_IMPORT_GUIDANCE}`,
40
+ );
41
+
42
+ const createEffectBffWorkerManifest = (
43
+ modernConfig: CloudflareModernConfig,
44
+ ) => {
45
+ const bff = modernConfig.bff;
46
+ const configuredPolicy = bff?.crossProjectPolicy;
47
+ if (configuredPolicy?.verifyProducerIdentity !== undefined) {
48
+ throw new Error(
49
+ 'Cloudflare Effect BFF cannot serialize bff.crossProjectPolicy.verifyProducerIdentity into the worker manifest. Configure a worker-native identity binding before enabling this policy at the edge.',
50
+ );
51
+ }
52
+ const crossProjectPolicy = {
53
+ ...configuredPolicy,
54
+ enabled: configuredPolicy?.enabled ?? Boolean(bff?.isCrossProjectServer),
55
+ requireEnvelope: configuredPolicy?.requireEnvelope ?? true,
56
+ requireOperationContext: configuredPolicy?.requireOperationContext ?? true,
57
+ requireOperationContextDetails:
58
+ configuredPolicy?.requireOperationContextDetails ?? true,
59
+ requireOperationSchemaHash:
60
+ configuredPolicy?.requireOperationSchemaHash ?? true,
61
+ requireOperationVersion: configuredPolicy?.requireOperationVersion ?? true,
62
+ allowUnknownOperations: configuredPolicy?.allowUnknownOperations ?? false,
63
+ expectedOperationContracts: {
64
+ ...(configuredPolicy?.expectedOperationContracts ?? {}),
65
+ },
66
+ };
67
+
68
+ const openapi = bff?.effect?.openapi;
69
+ const dataPlatform = bff?.effect?.dataPlatform;
70
+
71
+ return {
72
+ ...(openapi === undefined ? {} : { openapi }),
73
+ ...(dataPlatform === undefined ? {} : { dataPlatform }),
74
+ crossProjectPolicy,
75
+ };
76
+ };
77
+
78
+ const createModuleFederationWorkerManifest = async (
79
+ outputDirectory: string,
80
+ ) => {
81
+ const manifestPath = path.join(
82
+ outputDirectory,
83
+ PUBLIC_ASSETS_DIRECTORY,
84
+ 'mf-manifest.json',
85
+ );
86
+
87
+ if (!(await fse.pathExists(manifestPath))) {
88
+ return undefined;
89
+ }
90
+
91
+ const manifest = (await fse.readJson(manifestPath)) as unknown;
92
+ const name =
93
+ isRecord(manifest) && typeof manifest.name === 'string'
94
+ ? manifest.name
95
+ : undefined;
96
+ const manifestExposes =
97
+ isRecord(manifest) && Array.isArray(manifest.exposes)
98
+ ? manifest.exposes
99
+ : [];
100
+ const exposes = manifestExposes.filter(isRecord).flatMap(expose => {
101
+ if (typeof expose.path !== 'string') {
102
+ return [];
103
+ }
104
+ const assets = isRecord(expose.assets) ? expose.assets : undefined;
105
+ const css = assets && isRecord(assets.css) ? assets.css : undefined;
106
+ return [
107
+ {
108
+ path: expose.path,
109
+ css: [
110
+ ...(css && Array.isArray(css.sync) ? css.sync : []),
111
+ ...(css && Array.isArray(css.async) ? css.async : []),
112
+ ].filter(
113
+ (asset): asset is string =>
114
+ typeof asset === 'string' && asset.endsWith('.css'),
115
+ ),
116
+ },
117
+ ];
118
+ });
119
+
120
+ return name && exposes.length > 0 ? { name, exposes } : undefined;
121
+ };
122
+
123
+ export const createWorkerManifest = async (
124
+ outputDirectory: string,
125
+ modernConfig: CloudflareModernConfig,
126
+ appContext: CloudflareAppContext,
127
+ deliveryUnitStamp: DeliveryUnitStamp | undefined,
128
+ ) => {
129
+ const routeSpec = await readRouteSpec(outputDirectory);
130
+ const routes = await Promise.all(
131
+ routeSpec.routes.map(async (route: Record<string, any>) => {
132
+ const worker =
133
+ typeof route.worker === 'string' ? route.worker : undefined;
134
+
135
+ return {
136
+ urlPath: route.urlPath,
137
+ entryName: route.entryName,
138
+ entryPath: route.entryPath,
139
+ isSSR: Boolean(route.isSSR),
140
+ worker,
141
+ workerExists: worker
142
+ ? await fse.pathExists(path.join(outputDirectory, worker))
143
+ : false,
144
+ };
145
+ }),
146
+ );
147
+
148
+ const bffPrefix = modernConfig.bff?.prefix;
149
+ const primaryBffPrefix = Array.isArray(bffPrefix) ? bffPrefix[0] : bffPrefix;
150
+ const isEffectApi =
151
+ Boolean(modernConfig.bff) && modernConfig.bff?.runtimeFramework !== 'hono';
152
+ const effectApiWorkerExists = await fse.pathExists(
153
+ path.join(outputDirectory, BFF_EFFECT_WORKER_ENTRY),
154
+ );
155
+ const serviceBindings = createWorkerManifestServiceBindings(
156
+ createWorkerServiceBindings(modernConfig, undefined),
157
+ );
158
+ const moduleFederation =
159
+ await createModuleFederationWorkerManifest(outputDirectory);
160
+
161
+ if (isEffectApi && primaryBffPrefix && !effectApiWorkerExists) {
162
+ throw createMissingEffectBffWorkerError(
163
+ outputDirectory,
164
+ BFF_EFFECT_WORKER_ENTRY,
165
+ );
166
+ }
167
+ const effectBffManifest = isEffectApi
168
+ ? createEffectBffWorkerManifest(modernConfig)
169
+ : undefined;
170
+
171
+ return {
172
+ version: 1,
173
+ runtime: {
174
+ type: CLOUDFLARE_RUNTIME_TYPE,
175
+ entry: WORKER_ENTRY,
176
+ fetchExport: true,
177
+ nodeListen: false,
178
+ },
179
+ assets: {
180
+ binding: ASSETS_BINDING,
181
+ directory: `./${PUBLIC_ASSETS_DIRECTORY}`,
182
+ runWorkerFirst: true,
183
+ },
184
+ routeSpec: {
185
+ file: ROUTE_SPEC_OUTPUT,
186
+ routes,
187
+ },
188
+ workerBundles: {
189
+ directory: WORKER_BUNDLE_DIRECTORY,
190
+ format: CLOUDFLARE_WORKER_BUNDLE_FORMAT,
191
+ importableFromModuleWorker: true,
192
+ requestHandlerExport: 'requestHandler',
193
+ },
194
+ resources: {
195
+ loadableStats: LOADABLE_STATS_FILE,
196
+ routeManifest: ROUTE_MANIFEST_FILE,
197
+ },
198
+ security: createCloudflareWorkerSecurityPolicy(modernConfig),
199
+ ...(moduleFederation === undefined ? {} : { moduleFederation }),
200
+ ...(deliveryUnitStamp ? { deliveryUnit: deliveryUnitStamp } : {}),
201
+ i18n: createI18nWorkerManifest(routeSpec, appContext),
202
+ bff:
203
+ isEffectApi && primaryBffPrefix && effectApiWorkerExists
204
+ ? {
205
+ dispatcherExport: BFF_EFFECT_WORKER_DISPATCHER_EXPORT,
206
+ runtimeFramework: 'effect',
207
+ prefix: primaryBffPrefix,
208
+ worker: BFF_EFFECT_WORKER_ENTRY,
209
+ effect: effectBffManifest,
210
+ }
211
+ : undefined,
212
+ ...(serviceBindings === undefined ? {} : { serviceBindings }),
213
+ };
214
+ };
215
+
216
+ export const createWorkerModuleLoaders = (manifest: any) => {
217
+ const imports = new Map<string, string>();
218
+
219
+ for (const route of manifest.routeSpec.routes) {
220
+ if (route.worker && route.workerExists) {
221
+ const importPath = `../${String(route.worker).replace(/^\/+/u, '')}`;
222
+ imports.set(route.worker, `() => import(${JSON.stringify(importPath)})`);
223
+ }
224
+ }
225
+
226
+ if (manifest.bff?.worker) {
227
+ const importPath = `../${String(manifest.bff.worker).replace(/^\/+/u, '')}`;
228
+ imports.set(
229
+ manifest.bff.worker,
230
+ `() => import(${JSON.stringify(importPath)})`,
231
+ );
232
+ }
233
+
234
+ if (imports.size === 0) {
235
+ return '{}';
236
+ }
237
+
238
+ const loaders = [...imports.entries()].map(
239
+ ([worker, loader]) => `${JSON.stringify(worker)}: ${loader}`,
240
+ );
241
+
242
+ return `{\n${loaders.join(',\n')}\n}`;
243
+ };
244
+
245
+ export const getPublicAssetExcludes = (
246
+ appDirectory: string,
247
+ modernConfig: CloudflareModernConfig,
248
+ ) =>
249
+ [
250
+ ...DEFAULT_SERVER_ONLY_PUBLIC_ASSET_EXCLUDES.filter(directory => {
251
+ try {
252
+ return fse.statSync(path.join(appDirectory, directory)).isDirectory();
253
+ } catch {
254
+ return false;
255
+ }
256
+ }),
257
+ ...(modernConfig.deploy?.worker?.publicAssetExcludes ?? []),
258
+ ].map(entry =>
259
+ normalizeRelativePath(entry, 'deploy.worker.publicAssetExcludes'),
260
+ );
261
+
262
+ export const shouldCopyToPublicAssets = (
263
+ src: string,
264
+ distDirectory: string,
265
+ publicAssetExcludes: string[],
266
+ ) => {
267
+ const relativePath = path.relative(distDirectory, src);
268
+
269
+ if (!relativePath) {
270
+ return true;
271
+ }
272
+
273
+ const normalizedRelativePath = relativePath.replace(/\\/g, '/');
274
+ const [topLevelDirectory] = normalizedRelativePath.split('/');
275
+ const basename = normalizedRelativePath.split('/').pop() ?? '';
276
+
277
+ return (
278
+ normalizedRelativePath !== ROUTE_SPEC_FILE &&
279
+ topLevelDirectory !== WORKER_BUNDLE_DIRECTORY &&
280
+ topLevelDirectory !== SERVER_BUNDLE_DIRECTORY &&
281
+ basename !== '.env' &&
282
+ !basename.startsWith('.env.') &&
283
+ !publicAssetExcludes.some(
284
+ exclude =>
285
+ normalizedRelativePath === exclude ||
286
+ normalizedRelativePath.startsWith(`${exclude}/`),
287
+ )
288
+ );
289
+ };
290
+
291
+ export const shouldCopyToWorkerBundle = (
292
+ src: string,
293
+ workerBundleDirectory: string,
294
+ ) => {
295
+ const relativePath = path.relative(workerBundleDirectory, src);
296
+
297
+ if (!relativePath) {
298
+ return true;
299
+ }
300
+
301
+ const normalizedRelativePath = relativePath.replace(/\\/g, '/');
302
+ const basename = path.basename(normalizedRelativePath);
303
+
304
+ if (basename.startsWith('.') || normalizedRelativePath.includes('/.')) {
305
+ return false;
306
+ }
307
+
308
+ if (fse.statSync(src).isDirectory()) {
309
+ return true;
310
+ }
311
+
312
+ return ['.cjs', '.js', '.mjs'].includes(path.extname(normalizedRelativePath));
313
+ };