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

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 (213) 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 +251 -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.002-assets-routes.mjs +1 -1
  37. package/dist/cjs/templates/cloudflare-entry.004-rendering-css.mjs +5 -1
  38. package/dist/cjs/templates/cloudflare-entry.005-worker-dispatch.mjs +98 -10
  39. package/dist/cjs/templates/cloudflare-entry.006-fetch-handler.mjs +9 -2
  40. package/dist/cjs/templates/cloudflare-worker-route-data.mjs +1069 -0
  41. package/dist/esm/backend-federation-build/codegen.mjs +216 -0
  42. package/dist/esm/backend-federation-build/config.mjs +163 -0
  43. package/dist/esm/backend-federation-build/emit.mjs +103 -0
  44. package/dist/esm/backend-federation-build/index.mjs +1 -0
  45. package/dist/esm/build-config/build-environment.mjs +88 -0
  46. package/dist/esm/build-config/precompress/plugin.mjs +76 -0
  47. package/dist/esm/build-config/precompress/types.mjs +0 -0
  48. package/dist/esm/build-config/public.mjs +1 -0
  49. package/dist/esm/build-config/remote-address.mjs +20 -0
  50. package/dist/esm/cloudflare/artifacts.mjs +8 -19
  51. package/dist/esm/cloudflare/constants.mjs +1 -1
  52. package/dist/esm/cloudflare/delivery-unit.mjs +17 -36
  53. package/dist/esm/cloudflare/index.mjs +10 -2
  54. package/dist/esm/cloudflare/worker-manifest.mjs +13 -4
  55. package/dist/esm/cloudflare/wrangler-config.mjs +35 -7
  56. package/dist/esm/cloudflare-builder.mjs +245 -75
  57. package/dist/esm/cloudflare-output-contract.mjs +59 -2
  58. package/dist/esm/cloudflare-output-verifier/identity.mjs +27 -25
  59. package/dist/esm/cloudflare-output-verifier/index.mjs +10 -0
  60. package/dist/esm/cloudflare-output-verifier/manifest.mjs +46 -0
  61. package/dist/esm/cloudflare-output-verifier/mutation-policy.mjs +7 -1
  62. package/dist/esm/cloudflare-output-verifier/worker-bundles.mjs +15 -7
  63. package/dist/esm/config.mjs +3 -0
  64. package/dist/esm/deploy-output/npmAliases.mjs +199 -0
  65. package/dist/esm/deploy-output/plugin.mjs +46 -0
  66. package/dist/esm/deploy-output/public-assets.mjs +82 -0
  67. package/dist/esm/deploy-output/target.mjs +57 -0
  68. package/dist/esm/deploy-output/trace-files.mjs +38 -0
  69. package/dist/esm/native-type-checker.mjs +142 -0
  70. package/dist/esm/policy-defaults.mjs +45 -0
  71. package/dist/esm/release-envelope/framework-output.mjs +130 -56
  72. package/dist/esm/release-envelope/index.mjs +10 -4
  73. package/dist/esm/release-envelope/plugin.mjs +18 -13
  74. package/dist/esm/release-identity.mjs +5 -6
  75. package/dist/esm/runtime-package-resolution.mjs +60 -0
  76. package/dist/esm/templates/cloudflare-entry.002-assets-routes.mjs +1 -1
  77. package/dist/esm/templates/cloudflare-entry.004-rendering-css.mjs +5 -1
  78. package/dist/esm/templates/cloudflare-entry.005-worker-dispatch.mjs +98 -10
  79. package/dist/esm/templates/cloudflare-entry.006-fetch-handler.mjs +9 -2
  80. package/dist/esm/templates/cloudflare-worker-route-data.mjs +1069 -0
  81. package/dist/esm-node/backend-federation-build/codegen.mjs +217 -0
  82. package/dist/esm-node/backend-federation-build/config.mjs +164 -0
  83. package/dist/esm-node/backend-federation-build/emit.mjs +104 -0
  84. package/dist/esm-node/backend-federation-build/index.mjs +2 -0
  85. package/dist/esm-node/build-config/build-environment.mjs +89 -0
  86. package/dist/esm-node/build-config/precompress/plugin.mjs +77 -0
  87. package/dist/esm-node/build-config/precompress/types.mjs +1 -0
  88. package/dist/esm-node/build-config/public.mjs +2 -0
  89. package/dist/esm-node/build-config/remote-address.mjs +21 -0
  90. package/dist/esm-node/cloudflare/artifacts.mjs +8 -19
  91. package/dist/esm-node/cloudflare/constants.mjs +1 -1
  92. package/dist/esm-node/cloudflare/delivery-unit.mjs +17 -36
  93. package/dist/esm-node/cloudflare/index.mjs +10 -2
  94. package/dist/esm-node/cloudflare/worker-manifest.mjs +13 -4
  95. package/dist/esm-node/cloudflare/wrangler-config.mjs +35 -7
  96. package/dist/esm-node/cloudflare-builder.mjs +245 -75
  97. package/dist/esm-node/cloudflare-output-contract.mjs +59 -2
  98. package/dist/esm-node/cloudflare-output-verifier/identity.mjs +27 -25
  99. package/dist/esm-node/cloudflare-output-verifier/index.mjs +10 -0
  100. package/dist/esm-node/cloudflare-output-verifier/manifest.mjs +46 -0
  101. package/dist/esm-node/cloudflare-output-verifier/mutation-policy.mjs +7 -1
  102. package/dist/esm-node/cloudflare-output-verifier/worker-bundles.mjs +15 -7
  103. package/dist/esm-node/config.mjs +3 -0
  104. package/dist/esm-node/deploy-output/npmAliases.mjs +200 -0
  105. package/dist/esm-node/deploy-output/plugin.mjs +49 -0
  106. package/dist/esm-node/deploy-output/public-assets.mjs +83 -0
  107. package/dist/esm-node/deploy-output/target.mjs +58 -0
  108. package/dist/esm-node/deploy-output/trace-files.mjs +39 -0
  109. package/dist/esm-node/native-type-checker.mjs +143 -0
  110. package/dist/esm-node/policy-defaults.mjs +46 -0
  111. package/dist/esm-node/release-envelope/framework-output.mjs +130 -56
  112. package/dist/esm-node/release-envelope/index.mjs +10 -4
  113. package/dist/esm-node/release-envelope/plugin.mjs +18 -13
  114. package/dist/esm-node/release-identity.mjs +5 -6
  115. package/dist/esm-node/runtime-package-resolution.mjs +61 -0
  116. package/dist/esm-node/templates/cloudflare-entry.002-assets-routes.mjs +1 -1
  117. package/dist/esm-node/templates/cloudflare-entry.004-rendering-css.mjs +5 -1
  118. package/dist/esm-node/templates/cloudflare-entry.005-worker-dispatch.mjs +98 -10
  119. package/dist/esm-node/templates/cloudflare-entry.006-fetch-handler.mjs +9 -2
  120. package/dist/esm-node/templates/cloudflare-worker-route-data.mjs +1069 -0
  121. package/dist/types/backend-federation-build/codegen.d.ts +116 -0
  122. package/dist/types/backend-federation-build/config.d.ts +121 -0
  123. package/dist/types/backend-federation-build/emit.d.ts +21 -0
  124. package/dist/types/backend-federation-build/index.d.ts +2 -0
  125. package/dist/types/build-config/build-environment.d.ts +14 -0
  126. package/dist/types/build-config/precompress/plugin.d.ts +4 -0
  127. package/dist/types/build-config/precompress/types.d.ts +19 -0
  128. package/dist/types/build-config/public.d.ts +2 -0
  129. package/dist/types/build-config/remote-address.d.ts +3 -0
  130. package/dist/types/cloudflare/artifacts.d.ts +7 -6
  131. package/dist/types/cloudflare/constants.d.ts +1 -1
  132. package/dist/types/cloudflare/delivery-unit.d.ts +3 -3
  133. package/dist/types/cloudflare/worker-manifest.d.ts +2 -2
  134. package/dist/types/cloudflare/wrangler-config.d.ts +6 -0
  135. package/dist/types/cloudflare-builder.d.ts +49 -9
  136. package/dist/types/cloudflare-output-contract.d.ts +4 -1
  137. package/dist/types/cloudflare-output-verifier/identity.d.ts +1 -1
  138. package/dist/types/cloudflare-output-verifier/worker-bundles.d.ts +2 -2
  139. package/dist/types/config.d.ts +29 -1
  140. package/dist/types/deploy-output/npmAliases.d.ts +14 -0
  141. package/dist/types/deploy-output/plugin.d.ts +35 -0
  142. package/dist/types/deploy-output/public-assets.d.ts +51 -0
  143. package/dist/types/deploy-output/target.d.ts +29 -0
  144. package/dist/types/deploy-output/trace-files.d.ts +17 -0
  145. package/dist/types/native-type-checker.d.ts +15 -0
  146. package/dist/types/policy-defaults.d.ts +46 -0
  147. package/dist/types/precompress/LICENSE +20 -0
  148. package/dist/types/precompress/index.d.ts +76 -0
  149. package/dist/types/release-envelope/framework-output.d.ts +8 -3
  150. package/dist/types/release-envelope/plugin.d.ts +7 -7
  151. package/dist/types/runtime-package-resolution.d.ts +28 -0
  152. package/dist/types/templates/cloudflare-worker-route-data.d.mts +14 -0
  153. package/package.json +97 -15
  154. package/src/backend-federation-build/codegen.ts +254 -0
  155. package/src/backend-federation-build/config.ts +492 -0
  156. package/src/backend-federation-build/emit.ts +213 -0
  157. package/src/backend-federation-build/index.ts +2 -0
  158. package/src/build-config/build-environment.ts +160 -0
  159. package/src/build-config/precompress/plugin.ts +140 -0
  160. package/src/build-config/precompress/types.ts +26 -0
  161. package/src/build-config/public.ts +5 -0
  162. package/src/build-config/remote-address.ts +33 -0
  163. package/src/cloudflare/artifacts.ts +22 -43
  164. package/src/cloudflare/constants.ts +1 -1
  165. package/src/cloudflare/delivery-unit.ts +42 -53
  166. package/src/cloudflare/index.ts +24 -11
  167. package/src/cloudflare/worker-manifest.ts +20 -3
  168. package/src/cloudflare/wrangler-config.ts +86 -10
  169. package/src/cloudflare-builder.ts +472 -151
  170. package/src/cloudflare-output-contract.ts +70 -0
  171. package/src/cloudflare-output-verifier/identity.ts +5 -1
  172. package/src/cloudflare-output-verifier/index.ts +20 -0
  173. package/src/cloudflare-output-verifier/manifest.ts +77 -0
  174. package/src/cloudflare-output-verifier/mutation-policy.ts +11 -1
  175. package/src/cloudflare-output-verifier/worker-bundles.ts +32 -14
  176. package/src/config.ts +34 -6
  177. package/src/deploy-output/npmAliases.ts +382 -0
  178. package/src/deploy-output/plugin.ts +86 -0
  179. package/src/deploy-output/public-assets.ts +224 -0
  180. package/src/deploy-output/target.ts +106 -0
  181. package/src/deploy-output/trace-files.ts +67 -0
  182. package/src/native-type-checker.ts +183 -0
  183. package/src/policy-defaults.ts +127 -0
  184. package/src/release-envelope/framework-output.ts +346 -94
  185. package/src/release-envelope/index.ts +26 -5
  186. package/src/release-envelope/plugin.ts +43 -40
  187. package/src/release-identity.ts +13 -2
  188. package/src/runtime-package-resolution.ts +115 -0
  189. package/src/templates/cloudflare-entry.002-assets-routes.mjs +1 -1
  190. package/src/templates/cloudflare-entry.004-rendering-css.mjs +5 -1
  191. package/src/templates/cloudflare-entry.005-worker-dispatch.mjs +98 -10
  192. package/src/templates/cloudflare-entry.006-fetch-handler.mjs +9 -2
  193. package/src/templates/cloudflare-worker-route-data.mjs +1069 -0
  194. package/dist/cjs/templates/cloudflare-worker-fs-promises.mjs +0 -11
  195. package/dist/cjs/templates/cloudflare-worker-loadable-server.mjs +0 -281
  196. package/dist/cjs/templates/cloudflare-worker-mf-ssr-runtime-plugin.mjs +0 -14
  197. package/dist/cjs/templates/cloudflare-worker-path.mjs +0 -98
  198. package/dist/esm/templates/cloudflare-worker-fs-promises.mjs +0 -11
  199. package/dist/esm/templates/cloudflare-worker-loadable-server.mjs +0 -281
  200. package/dist/esm/templates/cloudflare-worker-mf-ssr-runtime-plugin.mjs +0 -14
  201. package/dist/esm/templates/cloudflare-worker-path.mjs +0 -98
  202. package/dist/esm-node/templates/cloudflare-worker-fs-promises.mjs +0 -11
  203. package/dist/esm-node/templates/cloudflare-worker-loadable-server.mjs +0 -281
  204. package/dist/esm-node/templates/cloudflare-worker-mf-ssr-runtime-plugin.mjs +0 -14
  205. package/dist/esm-node/templates/cloudflare-worker-path.mjs +0 -98
  206. package/dist/types/templates/cloudflare-worker-fs-promises.d.mts +0 -5
  207. package/dist/types/templates/cloudflare-worker-loadable-server.d.mts +0 -48
  208. package/dist/types/templates/cloudflare-worker-mf-ssr-runtime-plugin.d.mts +0 -13
  209. package/dist/types/templates/cloudflare-worker-path.d.mts +0 -21
  210. package/src/templates/cloudflare-worker-fs-promises.mjs +0 -11
  211. package/src/templates/cloudflare-worker-loadable-server.mjs +0 -281
  212. package/src/templates/cloudflare-worker-mf-ssr-runtime-plugin.mjs +0 -14
  213. package/src/templates/cloudflare-worker-path.mjs +0 -98
@@ -0,0 +1,382 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ type PackageJson = {
5
+ name?: string;
6
+ version?: string;
7
+ dependencies?: Record<string, string>;
8
+ devDependencies?: Record<string, string>;
9
+ optionalDependencies?: Record<string, string>;
10
+ [key: string]: unknown;
11
+ };
12
+
13
+ type PackageRecord = {
14
+ directory: string;
15
+ packageJson: PackageJson;
16
+ };
17
+
18
+ export type NpmAlias = {
19
+ aliasName: string;
20
+ targetName: string;
21
+ targetVersion: string;
22
+ };
23
+
24
+ const dependencyKeys = ['dependencies', 'optionalDependencies'] as const;
25
+ const rootDependencyKeys = [...dependencyKeys, 'devDependencies'] as const;
26
+ const exactVersionPattern =
27
+ /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u;
28
+
29
+ async function readPackageJson(filePath: string): Promise<PackageJson> {
30
+ return JSON.parse(await fs.readFile(filePath, 'utf8')) as PackageJson;
31
+ }
32
+
33
+ function parseNpmAlias(
34
+ aliasName: string,
35
+ specifier: string,
36
+ ): NpmAlias | undefined {
37
+ if (!specifier.startsWith('npm:')) {
38
+ return undefined;
39
+ }
40
+
41
+ const target = specifier.slice('npm:'.length);
42
+ const versionSeparator = target.lastIndexOf('@');
43
+ if (versionSeparator <= 0 || versionSeparator === target.length - 1) {
44
+ throw new Error(
45
+ `Invalid npm alias specifier for ${aliasName}: ${specifier}`,
46
+ );
47
+ }
48
+
49
+ return {
50
+ aliasName,
51
+ targetName: target.slice(0, versionSeparator),
52
+ targetVersion: target.slice(versionSeparator + 1),
53
+ };
54
+ }
55
+
56
+ function aliasesFromPackageJson(
57
+ packageJson: PackageJson,
58
+ keys: readonly (typeof rootDependencyKeys)[number][] = dependencyKeys,
59
+ ): NpmAlias[] {
60
+ const aliases: NpmAlias[] = [];
61
+ for (const dependencyKey of keys) {
62
+ for (const [aliasName, specifier] of Object.entries(
63
+ packageJson[dependencyKey] ?? {},
64
+ )) {
65
+ const alias = parseNpmAlias(aliasName, specifier);
66
+ if (alias) {
67
+ aliases.push(alias);
68
+ }
69
+ }
70
+ }
71
+ return aliases;
72
+ }
73
+
74
+ async function catalogAliasesFromInstalledPackages(
75
+ appDirectory: string,
76
+ packageJson: PackageJson,
77
+ ): Promise<NpmAlias[]> {
78
+ const aliases: NpmAlias[] = [];
79
+ for (const dependencyKey of rootDependencyKeys) {
80
+ for (const [aliasName, specifier] of Object.entries(
81
+ packageJson[dependencyKey] ?? {},
82
+ )) {
83
+ if (!specifier.startsWith('catalog:')) {
84
+ continue;
85
+ }
86
+ let installedPackage: PackageJson;
87
+ try {
88
+ installedPackage = await readPackageJson(
89
+ path.join(appDirectory, 'node_modules', aliasName, 'package.json'),
90
+ );
91
+ } catch (error) {
92
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
93
+ continue;
94
+ }
95
+ throw error;
96
+ }
97
+ if (!installedPackage.name || !installedPackage.version) {
98
+ throw new Error(
99
+ `Installed catalog package ${aliasName} has no identity`,
100
+ );
101
+ }
102
+ if (installedPackage.name !== aliasName) {
103
+ aliases.push({
104
+ aliasName,
105
+ targetName: installedPackage.name,
106
+ targetVersion: installedPackage.version,
107
+ });
108
+ }
109
+ }
110
+ }
111
+ return aliases;
112
+ }
113
+
114
+ async function collectPackageRecords(
115
+ nodeModulesDirectory: string,
116
+ ): Promise<PackageRecord[]> {
117
+ const records: PackageRecord[] = [];
118
+ const visitedNodeModules = new Set<string>();
119
+
120
+ const visitPackage = async (directory: string) => {
121
+ const packageJsonPath = path.join(directory, 'package.json');
122
+ try {
123
+ const packageJson = await readPackageJson(packageJsonPath);
124
+ if (packageJson.name && packageJson.version) {
125
+ records.push({ directory, packageJson });
126
+ }
127
+ } catch (error) {
128
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
129
+ throw error;
130
+ }
131
+ }
132
+ await visitNodeModules(path.join(directory, 'node_modules'));
133
+ };
134
+
135
+ const visitScope = async (scopeDirectory: string) => {
136
+ const entries = await fs.readdir(scopeDirectory, { withFileTypes: true });
137
+ for (const entry of entries) {
138
+ if (entry.isDirectory()) {
139
+ await visitPackage(path.join(scopeDirectory, entry.name));
140
+ }
141
+ }
142
+ };
143
+
144
+ const visitNdepeStore = async (storeDirectory: string) => {
145
+ const entries = await fs.readdir(storeDirectory, { withFileTypes: true });
146
+ for (const entry of entries) {
147
+ if (entry.isDirectory()) {
148
+ const entryPath = path.join(storeDirectory, entry.name);
149
+ if (entry.name.startsWith('@')) {
150
+ // ndepe preserves the slash in a scoped package name, so a
151
+ // multi-version package is stored as
152
+ // .ndepe/@scope/package@version/node_modules/@scope/package.
153
+ await visitScope(entryPath);
154
+ } else {
155
+ await visitNodeModules(path.join(entryPath, 'node_modules'));
156
+ }
157
+ }
158
+ }
159
+ };
160
+
161
+ const visitNodeModules = async (directory: string): Promise<void> => {
162
+ const resolvedDirectory = path.resolve(directory);
163
+ if (visitedNodeModules.has(resolvedDirectory)) {
164
+ return;
165
+ }
166
+ visitedNodeModules.add(resolvedDirectory);
167
+
168
+ let entries;
169
+ try {
170
+ entries = await fs.readdir(directory, { withFileTypes: true });
171
+ } catch (error) {
172
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
173
+ return;
174
+ }
175
+ throw error;
176
+ }
177
+
178
+ for (const entry of entries) {
179
+ if (!entry.isDirectory()) {
180
+ continue;
181
+ }
182
+ const entryPath = path.join(directory, entry.name);
183
+ if (entry.name === '.ndepe') {
184
+ await visitNdepeStore(entryPath);
185
+ } else if (entry.name.startsWith('@')) {
186
+ await visitScope(entryPath);
187
+ } else {
188
+ await visitPackage(entryPath);
189
+ }
190
+ }
191
+ };
192
+
193
+ await visitNodeModules(nodeModulesDirectory);
194
+ return records;
195
+ }
196
+
197
+ async function resolveAliasTarget(
198
+ ownerDirectory: string,
199
+ alias: NpmAlias,
200
+ outputDirectory: string,
201
+ ): Promise<PackageRecord | undefined> {
202
+ let directory = ownerDirectory;
203
+ const outputRoot = path.resolve(outputDirectory);
204
+ const resolvedOutputRoot = await fs.realpath(outputRoot);
205
+ while (
206
+ directory === outputRoot ||
207
+ directory.startsWith(`${outputRoot}${path.sep}`)
208
+ ) {
209
+ const candidate = path.join(directory, 'node_modules', alias.targetName);
210
+ try {
211
+ const resolvedCandidate = await fs.realpath(candidate);
212
+ if (
213
+ resolvedCandidate !== resolvedOutputRoot &&
214
+ !resolvedCandidate.startsWith(`${resolvedOutputRoot}${path.sep}`)
215
+ ) {
216
+ throw new Error(
217
+ `Cannot preserve npm alias ${alias.aliasName}: emitted target ${candidate} resolves outside deployment output to ${resolvedCandidate}`,
218
+ );
219
+ }
220
+ const packageJson = await readPackageJson(
221
+ path.join(candidate, 'package.json'),
222
+ );
223
+ if (packageJson.name !== alias.targetName || !packageJson.version) {
224
+ throw new Error(
225
+ `npm alias ${alias.aliasName} resolved ${candidate} to unexpected package ${String(packageJson.name)}`,
226
+ );
227
+ }
228
+ if (
229
+ exactVersionPattern.test(alias.targetVersion) &&
230
+ packageJson.version !== alias.targetVersion
231
+ ) {
232
+ return undefined;
233
+ }
234
+ // Keep the lexical path inside .output. realpath can cross an operating
235
+ // system alias such as macOS /var -> /private/var, which would turn the
236
+ // deployment link into a non-relocatable path outside the artifact.
237
+ return { directory: candidate, packageJson };
238
+ } catch (error) {
239
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
240
+ throw error;
241
+ }
242
+ }
243
+ if (directory === outputRoot) {
244
+ break;
245
+ }
246
+ directory = path.dirname(directory);
247
+ }
248
+ return undefined;
249
+ }
250
+
251
+ async function ensureAliasLink(
252
+ ownerDirectory: string,
253
+ alias: NpmAlias,
254
+ target: PackageRecord,
255
+ ) {
256
+ const aliasPath = path.join(ownerDirectory, 'node_modules', alias.aliasName);
257
+ await fs.mkdir(path.dirname(aliasPath), { recursive: true });
258
+
259
+ try {
260
+ const [existing, expected] = await Promise.all([
261
+ fs.realpath(aliasPath),
262
+ fs.realpath(target.directory),
263
+ ]);
264
+ if (existing === expected) {
265
+ return;
266
+ }
267
+ throw new Error(
268
+ `Cannot preserve npm alias ${alias.aliasName}: ${aliasPath} already resolves to ${existing}, expected ${expected}`,
269
+ );
270
+ } catch (error) {
271
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
272
+ throw error;
273
+ }
274
+ }
275
+
276
+ const linkTarget = path.relative(path.dirname(aliasPath), target.directory);
277
+ await fs.symlink(linkTarget, aliasPath, 'dir');
278
+ }
279
+
280
+ function aliasSpecifier(alias: NpmAlias) {
281
+ return `npm:${alias.targetName}@${alias.targetVersion}`;
282
+ }
283
+
284
+ export async function readPackageIdentity(
285
+ entryPath: string,
286
+ ): Promise<{ name: string; version: string }> {
287
+ let directory = path.dirname(entryPath);
288
+ const root = path.parse(directory).root;
289
+ while (directory !== root) {
290
+ try {
291
+ const packageJson = await readPackageJson(
292
+ path.join(directory, 'package.json'),
293
+ );
294
+ if (packageJson.name && packageJson.version) {
295
+ return { name: packageJson.name, version: packageJson.version };
296
+ }
297
+ } catch (error) {
298
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
299
+ throw error;
300
+ }
301
+ }
302
+ directory = path.dirname(directory);
303
+ }
304
+ throw new Error(
305
+ `Cannot find owning package for deployment entry ${entryPath}`,
306
+ );
307
+ }
308
+
309
+ export async function preserveNpmAliases({
310
+ appDirectory,
311
+ outputDirectory,
312
+ implicitAliases = [],
313
+ }: {
314
+ appDirectory: string;
315
+ outputDirectory: string;
316
+ implicitAliases?: NpmAlias[];
317
+ }) {
318
+ const records = await collectPackageRecords(
319
+ path.join(outputDirectory, 'node_modules'),
320
+ );
321
+
322
+ const appPackageJson = await readPackageJson(
323
+ path.join(appDirectory, 'package.json'),
324
+ );
325
+ const rootAliases = [
326
+ ...aliasesFromPackageJson(appPackageJson, rootDependencyKeys),
327
+ ...(await catalogAliasesFromInstalledPackages(
328
+ appDirectory,
329
+ appPackageJson,
330
+ )),
331
+ ...implicitAliases,
332
+ ].filter(alias => alias.aliasName !== alias.targetName);
333
+
334
+ const emittedRootAliases: NpmAlias[] = [];
335
+ for (const alias of rootAliases) {
336
+ const target = await resolveAliasTarget(
337
+ outputDirectory,
338
+ alias,
339
+ outputDirectory,
340
+ );
341
+ if (!target) {
342
+ if (implicitAliases.includes(alias)) {
343
+ throw new Error(
344
+ `Cannot preserve implicit npm alias ${alias.aliasName}: emitted target ${alias.targetName}@${alias.targetVersion} is missing`,
345
+ );
346
+ }
347
+ continue;
348
+ }
349
+ await ensureAliasLink(outputDirectory, alias, target);
350
+ emittedRootAliases.push(alias);
351
+ }
352
+
353
+ for (const record of records) {
354
+ for (const alias of aliasesFromPackageJson(record.packageJson).filter(
355
+ candidate => candidate.aliasName !== candidate.targetName,
356
+ )) {
357
+ const target = await resolveAliasTarget(
358
+ record.directory,
359
+ alias,
360
+ outputDirectory,
361
+ );
362
+ if (!target) {
363
+ // ndepe emits only runtime-traced packages. A package manifest can
364
+ // still declare type-only or otherwise unused aliases whose targets
365
+ // correctly are not part of this deployment output.
366
+ continue;
367
+ }
368
+ await ensureAliasLink(record.directory, alias, target);
369
+ }
370
+ }
371
+
372
+ const outputPackageJsonPath = path.join(outputDirectory, 'package.json');
373
+ const outputPackageJson = await readPackageJson(outputPackageJsonPath);
374
+ outputPackageJson.dependencies ??= {};
375
+ for (const alias of emittedRootAliases) {
376
+ outputPackageJson.dependencies[alias.aliasName] = aliasSpecifier(alias);
377
+ }
378
+ await fs.writeFile(
379
+ outputPackageJsonPath,
380
+ `${JSON.stringify(outputPackageJson, null, 2)}\n`,
381
+ );
382
+ }
@@ -0,0 +1,86 @@
1
+ import { createRequire } from 'node:module';
2
+ import path from 'node:path';
3
+ import type { NodePublicAssetConfig } from '../config';
4
+ import { preserveNpmAliases, readPackageIdentity } from './npmAliases';
5
+ import {
6
+ NODE_PUBLIC_ASSET_SCOPE,
7
+ normalizeDeclaredPublicAssets,
8
+ stageDeclaredPublicAssets,
9
+ } from './public-assets';
10
+ import { getDeployingTarget, type ResolvedDeployTarget } from './target';
11
+
12
+ export interface DeployOutputAppContext {
13
+ appDirectory: string;
14
+ metaName: string;
15
+ deployTarget?: ResolvedDeployTarget;
16
+ }
17
+
18
+ export interface DeployOutputPluginApi {
19
+ getAppContext(): DeployOutputAppContext;
20
+ onAfterDeploy(handler: () => Promise<void>): void;
21
+ }
22
+
23
+ export const createDeployOutputAliasesPlugin = () => ({
24
+ name: '@modern-js/deploy-output-aliases',
25
+ setup(api: DeployOutputPluginApi) {
26
+ api.onAfterDeploy(async () => {
27
+ const appContext = api.getAppContext();
28
+ if (getDeployingTarget(appContext) !== 'node') {
29
+ return;
30
+ }
31
+ const { appDirectory } = appContext;
32
+
33
+ const entry = createRequire(__filename).resolve('@modern-js/prod-server');
34
+ const identity = await readPackageIdentity(entry);
35
+ await preserveNpmAliases({
36
+ appDirectory,
37
+ outputDirectory: path.join(appDirectory, '.output'),
38
+ implicitAliases: [
39
+ {
40
+ aliasName: '@modern-js/prod-server',
41
+ targetName: identity.name,
42
+ targetVersion: identity.version,
43
+ },
44
+ ],
45
+ });
46
+ });
47
+ },
48
+ });
49
+
50
+ export interface DeployOutputPublicAssetsConfig {
51
+ deploy?: { node?: { publicAssets?: NodePublicAssetConfig[] } };
52
+ }
53
+
54
+ export interface DeployOutputPublicAssetsPluginApi {
55
+ getAppContext(): DeployOutputAppContext;
56
+ getNormalizedConfig(): DeployOutputPublicAssetsConfig;
57
+ onAfterDeploy(handler: () => Promise<void>): void;
58
+ }
59
+
60
+ /**
61
+ * Stage `deploy.node.publicAssets` into the Node deploy output. Cloudflare
62
+ * stages `deploy.worker.publicAssets` inside its deploy preset.
63
+ */
64
+ export const createDeployOutputPublicAssetsPlugin = () => ({
65
+ name: '@modern-js/deploy-output-public-assets',
66
+ setup(api: DeployOutputPublicAssetsPluginApi) {
67
+ api.onAfterDeploy(async () => {
68
+ const appContext = api.getAppContext();
69
+ if (getDeployingTarget(appContext) !== 'node') {
70
+ return;
71
+ }
72
+ const { appDirectory } = appContext;
73
+ const config = api.getNormalizedConfig();
74
+
75
+ await stageDeclaredPublicAssets({
76
+ appDirectory,
77
+ outputDirectory: path.join(appDirectory, '.output'),
78
+ assets: normalizeDeclaredPublicAssets(
79
+ config.deploy?.node?.publicAssets,
80
+ NODE_PUBLIC_ASSET_SCOPE,
81
+ ),
82
+ scope: NODE_PUBLIC_ASSET_SCOPE,
83
+ });
84
+ });
85
+ },
86
+ });
@@ -0,0 +1,224 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { normalizeRelativePath } from '../cloudflare/utils';
4
+
5
+ /**
6
+ * Deploy output directory that receives declared public assets on every
7
+ * target: Cloudflare Worker Static Assets and the Node deploy output.
8
+ */
9
+ export const DECLARED_PUBLIC_ASSET_DIRECTORY = 'public';
10
+
11
+ export interface DeclaredPublicAssetConfig {
12
+ from: string;
13
+ to: string;
14
+ }
15
+
16
+ export interface DeclaredPublicAssetScope {
17
+ /** Config path used in errors, for example `deploy.node.publicAssets`. */
18
+ label: string;
19
+ /** Output described in destination errors. */
20
+ output: string;
21
+ }
22
+
23
+ export interface DeclaredPublicAsset {
24
+ from: string;
25
+ to: string;
26
+ index: number;
27
+ }
28
+
29
+ export const CLOUDFLARE_PUBLIC_ASSET_SCOPE: DeclaredPublicAssetScope = {
30
+ label: 'deploy.worker.publicAssets',
31
+ output: 'Cloudflare public output',
32
+ };
33
+
34
+ export const NODE_PUBLIC_ASSET_SCOPE: DeclaredPublicAssetScope = {
35
+ label: 'deploy.node.publicAssets',
36
+ output: 'Node public output',
37
+ };
38
+
39
+ export const normalizeDeclaredPublicAssets = (
40
+ assets: readonly DeclaredPublicAssetConfig[] | undefined,
41
+ scope: DeclaredPublicAssetScope,
42
+ ): DeclaredPublicAsset[] =>
43
+ (assets ?? []).map((asset, index) => ({
44
+ from: normalizeRelativePath(
45
+ asset?.from,
46
+ `${scope.label}[${index}].from`,
47
+ 'app root',
48
+ ),
49
+ to: normalizeRelativePath(
50
+ asset?.to,
51
+ `${scope.label}[${index}].to`,
52
+ scope.output,
53
+ { allowRoot: true },
54
+ ),
55
+ index,
56
+ }));
57
+
58
+ interface DeclaredPublicAssetFile {
59
+ index: number;
60
+ /** Destination relative to the deploy output root, POSIX separators. */
61
+ logicalPath: string;
62
+ }
63
+
64
+ const isMissingPathError = (error: unknown) =>
65
+ error instanceof Error &&
66
+ 'code' in error &&
67
+ (error as NodeJS.ErrnoException).code === 'ENOENT';
68
+
69
+ const pathExists = async (filePath: string) => {
70
+ try {
71
+ await fs.lstat(filePath);
72
+ return true;
73
+ } catch (error) {
74
+ if (isMissingPathError(error)) {
75
+ return false;
76
+ }
77
+ throw error;
78
+ }
79
+ };
80
+
81
+ const listDeclaredPublicAssetFiles = async (
82
+ appDirectory: string,
83
+ assets: readonly DeclaredPublicAsset[],
84
+ scope: DeclaredPublicAssetScope,
85
+ ): Promise<DeclaredPublicAssetFile[]> => {
86
+ const files: DeclaredPublicAssetFile[] = [];
87
+ const owners = new Map<string, number>();
88
+ const add = (index: number, logicalPath: string) => {
89
+ const owner = owners.get(logicalPath);
90
+ if (owner !== undefined) {
91
+ throw new Error(
92
+ `${scope.label}[${index}] and ${scope.label}[${owner}] both stage ${JSON.stringify(logicalPath)}.`,
93
+ );
94
+ }
95
+ owners.set(logicalPath, index);
96
+ files.push({ index, logicalPath });
97
+ };
98
+ const visit = async (
99
+ asset: DeclaredPublicAsset,
100
+ sourcePath: string,
101
+ destination: string,
102
+ ): Promise<void> => {
103
+ const stat = await fs.lstat(sourcePath);
104
+ if (stat.isSymbolicLink()) {
105
+ throw new Error(
106
+ `${scope.label}[${asset.index}].from must not contain symbolic links: ${path
107
+ .relative(appDirectory, sourcePath)
108
+ .replace(/\\/gu, '/')}`,
109
+ );
110
+ }
111
+ if (stat.isFile()) {
112
+ if (destination === DECLARED_PUBLIC_ASSET_DIRECTORY) {
113
+ throw new Error(
114
+ `${scope.label}[${asset.index}].to must name a file when ${scope.label}[${asset.index}].from is a file.`,
115
+ );
116
+ }
117
+ add(asset.index, destination);
118
+ return;
119
+ }
120
+ if (!stat.isDirectory()) {
121
+ throw new Error(
122
+ `${scope.label}[${asset.index}].from must contain only files and directories: ${path
123
+ .relative(appDirectory, sourcePath)
124
+ .replace(/\\/gu, '/')}`,
125
+ );
126
+ }
127
+ const entries = (await fs.readdir(sourcePath)).sort();
128
+ for (const entry of entries) {
129
+ await visit(
130
+ asset,
131
+ path.join(sourcePath, entry),
132
+ path.posix.join(destination, entry),
133
+ );
134
+ }
135
+ };
136
+
137
+ for (const asset of assets) {
138
+ const sourcePath = path.join(appDirectory, asset.from);
139
+ if (!(await pathExists(sourcePath))) {
140
+ throw new Error(
141
+ `${scope.label}[${asset.index}].from does not exist: ${asset.from}`,
142
+ );
143
+ }
144
+ await visit(
145
+ asset,
146
+ sourcePath,
147
+ path.posix.join(DECLARED_PUBLIC_ASSET_DIRECTORY, asset.to),
148
+ );
149
+ }
150
+ return files;
151
+ };
152
+
153
+ /**
154
+ * Resolve the files declared public assets add to a deploy output that was
155
+ * copied from `generatedRoot`, relative to the deploy output root. Declared
156
+ * files that replace a generated file are not part of the result, matching
157
+ * {@link stageDeclaredPublicAssets}.
158
+ */
159
+ export const resolveAddedDeclaredPublicAssetPaths = async ({
160
+ appDirectory,
161
+ assets,
162
+ generatedRoot,
163
+ scope,
164
+ }: {
165
+ appDirectory: string;
166
+ assets: readonly DeclaredPublicAsset[];
167
+ generatedRoot: string;
168
+ scope: DeclaredPublicAssetScope;
169
+ }): Promise<string[]> => {
170
+ const added: string[] = [];
171
+ for (const file of await listDeclaredPublicAssetFiles(
172
+ appDirectory,
173
+ assets,
174
+ scope,
175
+ )) {
176
+ if (!(await pathExists(path.join(generatedRoot, file.logicalPath)))) {
177
+ added.push(file.logicalPath);
178
+ }
179
+ }
180
+ return added.sort();
181
+ };
182
+
183
+ /**
184
+ * Copy declared public assets into the deploy output after the framework has
185
+ * staged its own public files.
186
+ *
187
+ * A declared file may replace a generated public file, such as an app-owned
188
+ * `robots.txt`; the replaced file keeps its generated release classification.
189
+ *
190
+ * @returns the files the declarations added, relative to the deploy output
191
+ * root. The release envelope records these as declared public assets.
192
+ */
193
+ export const stageDeclaredPublicAssets = async ({
194
+ appDirectory,
195
+ outputDirectory,
196
+ assets,
197
+ scope,
198
+ }: {
199
+ appDirectory: string;
200
+ outputDirectory: string;
201
+ assets: readonly DeclaredPublicAsset[];
202
+ scope: DeclaredPublicAssetScope;
203
+ }): Promise<string[]> => {
204
+ const files = await listDeclaredPublicAssetFiles(appDirectory, assets, scope);
205
+ const added: string[] = [];
206
+ for (const file of files) {
207
+ const asset = assets.find(candidate => candidate.index === file.index)!;
208
+ const relativePath = path.posix.relative(
209
+ path.posix.join(DECLARED_PUBLIC_ASSET_DIRECTORY, asset.to),
210
+ file.logicalPath,
211
+ );
212
+ const destination = path.join(outputDirectory, file.logicalPath);
213
+ const replacesGeneratedFile = await pathExists(destination);
214
+ await fs.mkdir(path.dirname(destination), { recursive: true });
215
+ await fs.copyFile(
216
+ path.join(appDirectory, asset.from, relativePath),
217
+ destination,
218
+ );
219
+ if (!replacesGeneratedFile) {
220
+ added.push(file.logicalPath);
221
+ }
222
+ }
223
+ return added.sort();
224
+ };