@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,195 @@
1
+ import path from 'node:path';
2
+ import { fs as fse } from '@modern-js/utils';
3
+ import {
4
+ type DeliveryUnitIdentity,
5
+ isUltramodernBuildArtifact,
6
+ nonEmptyString,
7
+ toDeliveryUnitIdentity,
8
+ ULTRAMODERN_BUILD_ARTIFACT_PATH,
9
+ ULTRAMODERN_BUILD_MODULE_PATH,
10
+ } from '@modern-js/utils/universal';
11
+ import { resolveUltramodernReleaseIdentity } from '../release-identity';
12
+ import { isRecord } from './utils';
13
+
14
+ const COMPACT_CONFIG_PATH = '.modernjs/ultramodern.json';
15
+
16
+ export type DeliveryUnitStamp = DeliveryUnitIdentity & {
17
+ surfaces: {
18
+ ui?: DeliveryUnitIdentity & { surface: 'ui' };
19
+ api?: DeliveryUnitIdentity & { surface: 'api' };
20
+ };
21
+ };
22
+
23
+ type CompactAppResolution = {
24
+ app?: Record<string, unknown>;
25
+ workspaceRoot: string;
26
+ };
27
+
28
+ const findWorkspaceRoot = async (
29
+ appDirectory: string,
30
+ ): Promise<string | undefined> => {
31
+ let current = path.resolve(appDirectory);
32
+
33
+ for (;;) {
34
+ if (await fse.pathExists(path.join(current, COMPACT_CONFIG_PATH))) {
35
+ return current;
36
+ }
37
+
38
+ const parent = path.dirname(current);
39
+ if (parent === current) {
40
+ return undefined;
41
+ }
42
+
43
+ current = parent;
44
+ }
45
+ };
46
+
47
+ const stampReleaseIdentity = (
48
+ identity: DeliveryUnitIdentity,
49
+ workspaceRoot: string,
50
+ ): DeliveryUnitIdentity => {
51
+ if (identity.sourceRevision !== 'workspace') {
52
+ return identity;
53
+ }
54
+ return {
55
+ ...identity,
56
+ ...resolveUltramodernReleaseIdentity({
57
+ generationBuildMarker: identity.buildMarker,
58
+ unitId: identity.unitId,
59
+ workspaceRoot,
60
+ }),
61
+ };
62
+ };
63
+
64
+ const resolveCompactApp = async (
65
+ appDirectory: string,
66
+ ): Promise<CompactAppResolution | undefined> => {
67
+ const workspaceRoot = await findWorkspaceRoot(appDirectory);
68
+ if (!workspaceRoot) {
69
+ return undefined;
70
+ }
71
+
72
+ let compactConfig: unknown;
73
+ try {
74
+ compactConfig = await fse.readJSON(
75
+ path.join(workspaceRoot, COMPACT_CONFIG_PATH),
76
+ );
77
+ } catch {
78
+ return { workspaceRoot };
79
+ }
80
+
81
+ if (!isRecord(compactConfig)) {
82
+ return { workspaceRoot };
83
+ }
84
+
85
+ const topology = isRecord(compactConfig.topology)
86
+ ? compactConfig.topology
87
+ : undefined;
88
+ const apps = Array.isArray(topology?.apps) ? topology.apps : undefined;
89
+ if (!apps) {
90
+ return { app: compactConfig, workspaceRoot };
91
+ }
92
+
93
+ const resolvedAppDirectory = path.resolve(appDirectory);
94
+ const app = apps.find(candidate => {
95
+ if (!isRecord(candidate)) {
96
+ return false;
97
+ }
98
+ const appPath = nonEmptyString(candidate.path);
99
+ return (
100
+ appPath !== undefined &&
101
+ path.resolve(workspaceRoot, appPath.replace(/^\.\/+/u, '')) ===
102
+ resolvedAppDirectory
103
+ );
104
+ });
105
+ return {
106
+ ...(isRecord(app) ? { app } : {}),
107
+ workspaceRoot,
108
+ };
109
+ };
110
+
111
+ const createDeliveryUnitStamp = (
112
+ identity: DeliveryUnitIdentity,
113
+ app?: Record<string, unknown>,
114
+ ): DeliveryUnitStamp => {
115
+ const surfaceProfile = nonEmptyString(app?.surfaceProfile);
116
+ const emitsUi = surfaceProfile !== 'api-only';
117
+ const emitsApi = surfaceProfile !== 'ui-only';
118
+
119
+ return {
120
+ ...identity,
121
+ surfaces: {
122
+ ...(emitsUi ? { ui: { ...identity, surface: 'ui' as const } } : {}),
123
+ ...(emitsApi ? { api: { ...identity, surface: 'api' as const } } : {}),
124
+ },
125
+ };
126
+ };
127
+
128
+ /**
129
+ * Resolve the delivery-unit record declared for this app by the workspace
130
+ * compact config (`.modernjs/ultramodern.json`). This is the topology source
131
+ * of truth the Cloudflare worker snapshot is verified against.
132
+ */
133
+ export const resolveTopologyDeliveryUnit = async (
134
+ appDirectory: string,
135
+ ): Promise<DeliveryUnitStamp | undefined> => {
136
+ const resolved = await resolveCompactApp(appDirectory);
137
+ if (!resolved?.app) {
138
+ return undefined;
139
+ }
140
+ const identity = toDeliveryUnitIdentity(resolved.app.deliveryUnit);
141
+ return identity
142
+ ? createDeliveryUnitStamp(
143
+ stampReleaseIdentity(identity, resolved.workspaceRoot),
144
+ resolved.app,
145
+ )
146
+ : undefined;
147
+ };
148
+
149
+ /**
150
+ * Resolve the delivery-unit identity actually bundled into the worker by
151
+ * parsing the generated `shared/ultramodern-build.ts` module. This is the
152
+ * worker snapshot / declared surface source that gets stamped into the manifest.
153
+ */
154
+ export const resolveWorkerDeliveryUnitStamp = async (
155
+ appDirectory: string,
156
+ ): Promise<DeliveryUnitStamp | undefined> => {
157
+ const buildArtifactPath = path.join(
158
+ appDirectory,
159
+ ULTRAMODERN_BUILD_ARTIFACT_PATH,
160
+ );
161
+ const buildModulePath = path.join(
162
+ appDirectory,
163
+ ULTRAMODERN_BUILD_MODULE_PATH,
164
+ );
165
+ let identity: DeliveryUnitIdentity | undefined;
166
+
167
+ if (await fse.pathExists(buildArtifactPath)) {
168
+ const artifact = await fse.readJSON(buildArtifactPath);
169
+ if (!isUltramodernBuildArtifact(artifact)) {
170
+ return undefined;
171
+ }
172
+ identity = toDeliveryUnitIdentity(artifact.deliveryUnit);
173
+ } else if (await fse.pathExists(buildModulePath)) {
174
+ console.warn(
175
+ `[cloudflare] ${buildArtifactPath} missing; falling back to legacy regex parsing of ${buildModulePath}. Regenerate the workspace to emit ultramodern-build.json.`,
176
+ );
177
+ const source = await fse.readFile(buildModulePath, 'utf8');
178
+ identity = toDeliveryUnitIdentity({
179
+ buildMarker: source.match(/\bbuild:\s*['"]([^'"]+)['"]/u)?.[1],
180
+ unitId: source.match(/\bunitId:\s*['"]([^'"]+)['"]/u)?.[1],
181
+ sourceRevision: source.match(
182
+ /\bsourceRevision:\s*['"]([^'"]+)['"]/u,
183
+ )?.[1],
184
+ });
185
+ }
186
+
187
+ if (!identity) {
188
+ return undefined;
189
+ }
190
+ const resolved = await resolveCompactApp(appDirectory);
191
+ if (resolved) {
192
+ identity = stampReleaseIdentity(identity, resolved.workspaceRoot);
193
+ }
194
+ return createDeliveryUnitStamp(identity, resolved?.app);
195
+ };
@@ -0,0 +1,197 @@
1
+ import type { CloudflareAppContext } from './types';
2
+ import { isRecord } from './utils';
3
+
4
+ const I18N_SERVER_PLUGIN_NAME = 'plugin-i18n/server';
5
+ const DEFAULT_I18NEXT_DETECTION_OPTIONS = {
6
+ order: [
7
+ 'querystring',
8
+ 'cookie',
9
+ 'localStorage',
10
+ 'header',
11
+ 'navigator',
12
+ 'htmlTag',
13
+ 'path',
14
+ 'subdomain',
15
+ ],
16
+ lookupQuerystring: 'lng',
17
+ lookupCookie: 'i18next',
18
+ lookupHeader: 'accept-language',
19
+ } as const;
20
+ const SERVER_SIDE_I18N_DETECTORS = new Set(['querystring', 'cookie', 'header']);
21
+
22
+ type ServerPluginConfig = NonNullable<
23
+ CloudflareAppContext['serverPlugins']
24
+ >[number];
25
+
26
+ type WorkerI18nDetection = {
27
+ order: string[];
28
+ lookupQuerystring: string;
29
+ lookupCookie: string;
30
+ lookupHeader: string;
31
+ };
32
+
33
+ type WorkerI18nEntry = {
34
+ i18nextDetector: boolean;
35
+ languages: string[];
36
+ fallbackLanguage: string;
37
+ detection: WorkerI18nDetection;
38
+ ignoreRedirectRoutes: string[];
39
+ staticRoutePrefixes: string[];
40
+ localisedUrls?: Record<string, Record<string, string>>;
41
+ };
42
+
43
+ const toStringArray = (value: unknown): string[] =>
44
+ Array.isArray(value)
45
+ ? value.filter((entry): entry is string => typeof entry === 'string')
46
+ : [];
47
+
48
+ const getI18nServerPlugin = (
49
+ appContext: CloudflareAppContext,
50
+ ): ServerPluginConfig | undefined =>
51
+ appContext.serverPlugins?.find(plugin =>
52
+ plugin.name.includes(I18N_SERVER_PLUGIN_NAME),
53
+ );
54
+
55
+ const removeLocaleDetectionByEntry = (config: Record<string, unknown>) => {
56
+ const { localeDetectionByEntry: _localeDetectionByEntry, ...rest } = config;
57
+ return rest;
58
+ };
59
+
60
+ const getEntryLocaleDetection = (
61
+ entryName: string,
62
+ localeDetection: Record<string, unknown>,
63
+ ) => {
64
+ const byEntry = isRecord(localeDetection.localeDetectionByEntry)
65
+ ? localeDetection.localeDetectionByEntry
66
+ : undefined;
67
+ const entryConfig = byEntry?.[entryName];
68
+ const globalConfig = removeLocaleDetectionByEntry(localeDetection);
69
+
70
+ return isRecord(entryConfig)
71
+ ? { ...globalConfig, ...entryConfig }
72
+ : globalConfig;
73
+ };
74
+
75
+ const normalizeI18nDetection = (detection: unknown): WorkerI18nDetection => {
76
+ const configured = isRecord(detection) ? detection : {};
77
+ const rawOrder = Array.isArray(configured.order)
78
+ ? configured.order
79
+ : DEFAULT_I18NEXT_DETECTION_OPTIONS.order;
80
+ const order = rawOrder.filter(
81
+ (entry): entry is string =>
82
+ typeof entry === 'string' && SERVER_SIDE_I18N_DETECTORS.has(entry),
83
+ );
84
+
85
+ return {
86
+ order: order.length > 0 ? order : ['querystring', 'cookie', 'header'],
87
+ lookupQuerystring:
88
+ typeof configured.lookupQuerystring === 'string' &&
89
+ configured.lookupQuerystring
90
+ ? configured.lookupQuerystring
91
+ : DEFAULT_I18NEXT_DETECTION_OPTIONS.lookupQuerystring,
92
+ lookupCookie:
93
+ typeof configured.lookupCookie === 'string' && configured.lookupCookie
94
+ ? configured.lookupCookie
95
+ : DEFAULT_I18NEXT_DETECTION_OPTIONS.lookupCookie,
96
+ lookupHeader:
97
+ typeof configured.lookupHeader === 'string' && configured.lookupHeader
98
+ ? configured.lookupHeader
99
+ : DEFAULT_I18NEXT_DETECTION_OPTIONS.lookupHeader,
100
+ };
101
+ };
102
+
103
+ const normalizeLocalisedUrls = (
104
+ localisedUrls: unknown,
105
+ ): Record<string, Record<string, string>> | undefined => {
106
+ if (!isRecord(localisedUrls) || Object.keys(localisedUrls).length === 0) {
107
+ return undefined;
108
+ }
109
+
110
+ const normalized: Record<string, Record<string, string>> = {};
111
+ for (const [canonicalPath, languageMap] of Object.entries(localisedUrls)) {
112
+ if (!isRecord(languageMap)) {
113
+ continue;
114
+ }
115
+
116
+ const normalizedLanguageMap = Object.fromEntries(
117
+ Object.entries(languageMap).filter(
118
+ (entry): entry is [string, string] =>
119
+ typeof entry[1] === 'string' && entry[1].length > 0,
120
+ ),
121
+ );
122
+
123
+ if (Object.keys(normalizedLanguageMap).length > 0) {
124
+ normalized[canonicalPath] = normalizedLanguageMap;
125
+ }
126
+ }
127
+
128
+ return Object.keys(normalized).length > 0 ? normalized : undefined;
129
+ };
130
+
131
+ const normalizeWorkerI18nEntry = (
132
+ entryName: string,
133
+ localeDetection: Record<string, unknown>,
134
+ staticRoutePrefixes: string[],
135
+ ): WorkerI18nEntry | undefined => {
136
+ const entryConfig = getEntryLocaleDetection(entryName, localeDetection);
137
+ const languages = toStringArray(entryConfig.languages);
138
+
139
+ if (entryConfig.localePathRedirect !== true || languages.length === 0) {
140
+ return undefined;
141
+ }
142
+
143
+ const fallbackLanguage =
144
+ typeof entryConfig.fallbackLanguage === 'string' &&
145
+ entryConfig.fallbackLanguage
146
+ ? entryConfig.fallbackLanguage
147
+ : 'en';
148
+ const ignoreRedirectRoutes = Array.isArray(entryConfig.ignoreRedirectRoutes)
149
+ ? toStringArray(entryConfig.ignoreRedirectRoutes)
150
+ : [];
151
+ const localisedUrls = normalizeLocalisedUrls(entryConfig.localisedUrls);
152
+
153
+ return {
154
+ i18nextDetector: entryConfig.i18nextDetector !== false,
155
+ languages,
156
+ fallbackLanguage,
157
+ detection: normalizeI18nDetection(entryConfig.detection),
158
+ ignoreRedirectRoutes,
159
+ staticRoutePrefixes,
160
+ ...(localisedUrls ? { localisedUrls } : {}),
161
+ };
162
+ };
163
+
164
+ export const createI18nWorkerManifest = (
165
+ routeSpec: { routes: Array<Record<string, unknown>> },
166
+ appContext: CloudflareAppContext,
167
+ ) => {
168
+ const i18nServerPlugin = getI18nServerPlugin(appContext);
169
+ const localeDetection = i18nServerPlugin?.options?.localeDetection;
170
+
171
+ if (!isRecord(localeDetection)) {
172
+ return undefined;
173
+ }
174
+
175
+ const staticRoutePrefixes = toStringArray(
176
+ i18nServerPlugin?.options?.staticRoutePrefixes,
177
+ );
178
+ const entries: Record<string, WorkerI18nEntry> = {};
179
+
180
+ for (const route of routeSpec.routes) {
181
+ if (typeof route.entryName !== 'string' || entries[route.entryName]) {
182
+ continue;
183
+ }
184
+
185
+ const entry = normalizeWorkerI18nEntry(
186
+ route.entryName,
187
+ localeDetection,
188
+ staticRoutePrefixes,
189
+ );
190
+
191
+ if (entry) {
192
+ entries[route.entryName] = entry;
193
+ }
194
+ }
195
+
196
+ return Object.keys(entries).length > 0 ? { entries } : undefined;
197
+ };
@@ -0,0 +1,259 @@
1
+ import path from 'node:path';
2
+ import { fs as fse } from '@modern-js/utils';
3
+ import { createCloudflareOutputPlan } from '../cloudflare-output-plan';
4
+ import { assertCloudflareOutput } from '../cloudflare-output-verifier/index';
5
+ import { readTemplate } from '../read-template';
6
+ import {
7
+ emitCloudflareStagedReleaseEnvelope,
8
+ emitFrameworkMicroVerticalReleaseEnvelope,
9
+ MICROVERTICAL_RELEASE_ENVELOPE_PATH,
10
+ stageCloudflareReleaseEnvelope,
11
+ verifyBuildOutputReleaseEnvelope,
12
+ verifyCloudflareReleaseEnvelopeStaging,
13
+ } from '../release-envelope/framework-output';
14
+ import {
15
+ copyCloudflareArtifacts,
16
+ copyCloudflareD1Migrations,
17
+ copyCloudflarePublicAssets,
18
+ getCloudflareArtifacts,
19
+ getCloudflarePublicAssets,
20
+ } from './artifacts';
21
+ import {
22
+ ROUTE_SPEC_FILE,
23
+ ROUTE_SPEC_OUTPUT,
24
+ WORKER_BUNDLE_DIRECTORY,
25
+ WORKER_ENTRY,
26
+ WORKER_MANIFEST,
27
+ WRANGLER_CONFIG_FILE,
28
+ } from './constants';
29
+ import {
30
+ resolveTopologyDeliveryUnit,
31
+ resolveWorkerDeliveryUnitStamp,
32
+ } from './delivery-unit';
33
+ import type { CreateCloudflarePreset } from './types';
34
+ import {
35
+ createWorkerManifest,
36
+ createWorkerModuleLoaders,
37
+ getPublicAssetExcludes,
38
+ shouldCopyToPublicAssets,
39
+ shouldCopyToWorkerBundle,
40
+ } from './worker-manifest';
41
+ import { createWranglerConfig } from './wrangler-config';
42
+
43
+ const CLOUDFLARE_ENTRY_TEMPLATE_FRAGMENTS = [
44
+ 'cloudflare-entry.001-bootstrap-security.mjs',
45
+ 'cloudflare-entry.002-assets-routes.mjs',
46
+ 'cloudflare-entry.003-i18n-locales.mjs',
47
+ 'cloudflare-entry.004-rendering-css.mjs',
48
+ 'cloudflare-entry.005-worker-dispatch.mjs',
49
+ 'cloudflare-entry.006-fetch-handler.mjs',
50
+ ] as const;
51
+
52
+ /**
53
+ * The Cloudflare entry is emitted as a raw module worker, not bundled by
54
+ * app-tools. Keep worker-runtime helpers inline unless the helper is emitted
55
+ * into `.output` or provided by a generated-worker dependency. Concatenate
56
+ * fragments without separators so existing generated output stays byte-identical.
57
+ */
58
+ const readCloudflareEntryTemplate = async () =>
59
+ (
60
+ await Promise.all(
61
+ CLOUDFLARE_ENTRY_TEMPLATE_FRAGMENTS.map(fragment =>
62
+ readTemplate(fragment),
63
+ ),
64
+ )
65
+ ).join('');
66
+
67
+ export const createCloudflarePreset: CreateCloudflarePreset = ({
68
+ api,
69
+ appContext,
70
+ modernConfig,
71
+ }) => {
72
+ const { apiOnly, appDirectory, distDirectory } = appContext;
73
+
74
+ const outputDirectory = path.join(appDirectory, '.output');
75
+ const outputPlan = createCloudflareOutputPlan(outputDirectory);
76
+ const publicDirectory = outputPlan.paths.publicAssets;
77
+ const workerEntryPath = outputPlan.paths.workerEntry;
78
+ const workerManifestPath = outputPlan.paths.workerManifest;
79
+ const routeSpecOutputPath = path.join(outputDirectory, ROUTE_SPEC_OUTPUT);
80
+ const wranglerConfigPath = outputPlan.paths.wranglerConfig;
81
+ const cloudflareArtifacts = getCloudflareArtifacts(modernConfig);
82
+ const publicAssetExcludes = getPublicAssetExcludes(
83
+ appDirectory,
84
+ modernConfig,
85
+ );
86
+ const releaseEnvelopeEnabled = api.isPluginExists(
87
+ '@modern-js/ultramodern-release-envelope',
88
+ );
89
+ let hasReleaseEnvelope = false;
90
+
91
+ return {
92
+ async prepare() {
93
+ await fse.remove(outputDirectory);
94
+ },
95
+ async writeOutput() {
96
+ if (
97
+ releaseEnvelopeEnabled &&
98
+ (await fse.pathExists(
99
+ path.join(distDirectory, MICROVERTICAL_RELEASE_ENVELOPE_PATH),
100
+ ))
101
+ ) {
102
+ await emitFrameworkMicroVerticalReleaseEnvelope({
103
+ apiOnly,
104
+ distDirectory,
105
+ target: 'cloudflare',
106
+ });
107
+ }
108
+ if (releaseEnvelopeEnabled) {
109
+ await verifyBuildOutputReleaseEnvelope(distDirectory, 'cloudflare');
110
+ }
111
+ await fse.copy(distDirectory, publicDirectory, {
112
+ filter: src => {
113
+ const relativePath = path
114
+ .relative(distDirectory, src)
115
+ .replace(/\\/gu, '/');
116
+ return (
117
+ relativePath !== 'release' &&
118
+ !relativePath.startsWith('release/') &&
119
+ relativePath !== 'public' &&
120
+ !relativePath.startsWith('public/') &&
121
+ shouldCopyToPublicAssets(src, distDirectory, publicAssetExcludes)
122
+ );
123
+ },
124
+ });
125
+ const generatedPublicDirectory = path.join(distDirectory, 'public');
126
+ if (await fse.pathExists(generatedPublicDirectory)) {
127
+ await fse.copy(generatedPublicDirectory, publicDirectory, {
128
+ filter: src => {
129
+ const relativePath = path
130
+ .relative(generatedPublicDirectory, src)
131
+ .replace(/\\/gu, '/');
132
+ return (
133
+ relativePath !== 'release' &&
134
+ !relativePath.startsWith('release/') &&
135
+ shouldCopyToPublicAssets(
136
+ src,
137
+ generatedPublicDirectory,
138
+ publicAssetExcludes,
139
+ )
140
+ );
141
+ },
142
+ });
143
+ }
144
+ await fse.ensureDir(path.dirname(workerEntryPath));
145
+ await fse.ensureDir(path.dirname(workerManifestPath));
146
+
147
+ const routeSpecSourcePath = path.join(distDirectory, ROUTE_SPEC_FILE);
148
+ if (await fse.pathExists(routeSpecSourcePath)) {
149
+ await fse.copy(routeSpecSourcePath, routeSpecOutputPath);
150
+ }
151
+
152
+ const workerBundleSourceDirectory = path.join(
153
+ distDirectory,
154
+ WORKER_BUNDLE_DIRECTORY,
155
+ );
156
+ const workerBundleOutputDirectory = outputPlan.paths.workerBundle;
157
+ if (await fse.pathExists(workerBundleSourceDirectory)) {
158
+ await fse.copy(
159
+ workerBundleSourceDirectory,
160
+ workerBundleOutputDirectory,
161
+ {
162
+ filter: src =>
163
+ shouldCopyToWorkerBundle(src, workerBundleSourceDirectory),
164
+ },
165
+ );
166
+ await fse.writeJSON(
167
+ outputPlan.paths.workerPackage,
168
+ outputPlan.packages.worker,
169
+ );
170
+ }
171
+ await copyCloudflareArtifacts(
172
+ appDirectory,
173
+ outputDirectory,
174
+ cloudflareArtifacts,
175
+ );
176
+ await copyCloudflarePublicAssets(
177
+ appDirectory,
178
+ publicDirectory,
179
+ getCloudflarePublicAssets(modernConfig),
180
+ );
181
+ await copyCloudflareD1Migrations(
182
+ appDirectory,
183
+ outputDirectory,
184
+ modernConfig,
185
+ );
186
+
187
+ await fse.writeJSON(
188
+ wranglerConfigPath,
189
+ createWranglerConfig(appDirectory, modernConfig),
190
+ {
191
+ spaces: 2,
192
+ },
193
+ );
194
+
195
+ const deliveryUnitStamp =
196
+ await resolveWorkerDeliveryUnitStamp(appDirectory);
197
+ await fse.writeJSON(
198
+ workerManifestPath,
199
+ await createWorkerManifest(
200
+ outputDirectory,
201
+ modernConfig,
202
+ appContext,
203
+ deliveryUnitStamp,
204
+ ),
205
+ {
206
+ spaces: 2,
207
+ },
208
+ );
209
+ await fse.writeJSON(
210
+ outputPlan.paths.outputPackage,
211
+ outputPlan.packages.output,
212
+ );
213
+ hasReleaseEnvelope =
214
+ releaseEnvelopeEnabled &&
215
+ Boolean(
216
+ await stageCloudflareReleaseEnvelope({
217
+ distDirectory,
218
+ outputDirectory,
219
+ }),
220
+ );
221
+ },
222
+ async genEntry() {
223
+ const template = await readCloudflareEntryTemplate();
224
+ const manifest = await fse.readJSON(workerManifestPath);
225
+
226
+ await fse.writeFile(
227
+ workerEntryPath,
228
+ template
229
+ .replace('p_workerManifest', JSON.stringify(manifest, null, 2))
230
+ .replace(
231
+ 'p_workerModuleLoaders',
232
+ createWorkerModuleLoaders(manifest),
233
+ ),
234
+ );
235
+ const topologyDeliveryUnit =
236
+ await resolveTopologyDeliveryUnit(appDirectory);
237
+ await assertCloudflareOutput({
238
+ outputDirectory,
239
+ importWorker: false,
240
+ ...(topologyDeliveryUnit ? { deliveryUnit: topologyDeliveryUnit } : {}),
241
+ });
242
+ if (hasReleaseEnvelope) {
243
+ await emitCloudflareStagedReleaseEnvelope({
244
+ distDirectory,
245
+ outputDirectory,
246
+ });
247
+ await verifyCloudflareReleaseEnvelopeStaging(outputDirectory);
248
+ } else if (
249
+ await fse.pathExists(
250
+ path.join(outputDirectory, MICROVERTICAL_RELEASE_ENVELOPE_PATH),
251
+ )
252
+ ) {
253
+ throw new Error(
254
+ '[ultramodern-release-envelope] Cloudflare staging contains an envelope not selected from this MicroVertical build.',
255
+ );
256
+ }
257
+ },
258
+ };
259
+ };