@bleedingdev/modern-js-app-tools-extensions 3.9.0-ultramodern.14 → 3.9.0-ultramodern.16

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 (36) hide show
  1. package/dist/cjs/cloudflare/artifacts.js +8 -19
  2. package/dist/cjs/cloudflare/index.js +3 -1
  3. package/dist/cjs/deploy-output/plugin.js +22 -2
  4. package/dist/cjs/deploy-output/public-assets.js +146 -0
  5. package/dist/cjs/release-envelope/framework-output.js +24 -8
  6. package/dist/cjs/release-envelope/plugin.js +9 -1
  7. package/dist/cjs/templates/cloudflare-entry.005-worker-dispatch.mjs +52 -10
  8. package/dist/esm/cloudflare/artifacts.mjs +8 -19
  9. package/dist/esm/cloudflare/index.mjs +3 -1
  10. package/dist/esm/deploy-output/plugin.mjs +18 -1
  11. package/dist/esm/deploy-output/public-assets.mjs +82 -0
  12. package/dist/esm/release-envelope/framework-output.mjs +24 -8
  13. package/dist/esm/release-envelope/plugin.mjs +9 -1
  14. package/dist/esm/templates/cloudflare-entry.005-worker-dispatch.mjs +52 -10
  15. package/dist/esm-node/cloudflare/artifacts.mjs +8 -19
  16. package/dist/esm-node/cloudflare/index.mjs +3 -1
  17. package/dist/esm-node/deploy-output/plugin.mjs +18 -1
  18. package/dist/esm-node/deploy-output/public-assets.mjs +83 -0
  19. package/dist/esm-node/release-envelope/framework-output.mjs +24 -8
  20. package/dist/esm-node/release-envelope/plugin.mjs +9 -1
  21. package/dist/esm-node/templates/cloudflare-entry.005-worker-dispatch.mjs +52 -10
  22. package/dist/types/cloudflare/artifacts.d.ts +7 -6
  23. package/dist/types/config.d.ts +24 -0
  24. package/dist/types/deploy-output/plugin.d.ts +26 -0
  25. package/dist/types/deploy-output/public-assets.d.ts +51 -0
  26. package/dist/types/release-envelope/framework-output.d.ts +6 -2
  27. package/dist/types/release-envelope/plugin.d.ts +4 -0
  28. package/package.json +14 -14
  29. package/src/cloudflare/artifacts.ts +22 -43
  30. package/src/cloudflare/index.ts +4 -2
  31. package/src/config.ts +26 -0
  32. package/src/deploy-output/plugin.ts +54 -0
  33. package/src/deploy-output/public-assets.ts +224 -0
  34. package/src/release-envelope/framework-output.ts +49 -1
  35. package/src/release-envelope/plugin.ts +22 -1
  36. package/src/templates/cloudflare-entry.005-worker-dispatch.mjs +52 -10
@@ -160,14 +160,46 @@ async function dispatchRouteWorker(route, request, env, ctx) {
160
160
  );
161
161
  }
162
162
 
163
- function matchesPrefix(pathname, prefix) {
163
+ // Worker Static Assets decode percent-encoded paths, so an encoded spelling
164
+ // of a mounted prefix (`/%70refix/...`, `/prefix%2F...`) would otherwise skip
165
+ // the BFF or service binding that owns the prefix and reach ASSETS directly.
166
+ // Resolve such spellings to the canonical pathname the owner sees; a
167
+ // backslash or undecodable spelling becomes a path the owner rejects.
168
+ function resolvePrefixPathname(pathname, prefix) {
164
169
  if (!prefix || prefix === '/') {
165
- return true;
170
+ return pathname;
166
171
  }
167
172
 
168
173
  const normalized = prefix.endsWith('/') ? prefix.slice(0, -1) : prefix;
174
+ let candidate = pathname;
175
+
176
+ for (let attempt = 0; attempt < 4; attempt += 1) {
177
+ if (candidate === normalized || candidate.startsWith(`${normalized}/`)) {
178
+ return candidate;
179
+ }
180
+ if (candidate.startsWith(`${normalized}\\`)) {
181
+ return `${normalized}/__invalid_encoded_path__`;
182
+ }
183
+
184
+ let decoded;
185
+ try {
186
+ decoded = decodeURIComponent(candidate);
187
+ } catch {
188
+ return candidate.toLowerCase().startsWith(normalized.toLowerCase())
189
+ ? `${normalized}/__invalid_encoded_path__`
190
+ : null;
191
+ }
192
+ if (decoded === candidate) {
193
+ return null;
194
+ }
195
+ candidate = decoded;
196
+ }
169
197
 
170
- return pathname === normalized || pathname.startsWith(`${normalized}/`);
198
+ return null;
199
+ }
200
+
201
+ function matchesPrefix(pathname, prefix) {
202
+ return resolvePrefixPathname(pathname, prefix) !== null;
171
203
  }
172
204
 
173
205
  function createRequestForMountedPrefix(request, prefix) {
@@ -177,12 +209,13 @@ function createRequestForMountedPrefix(request, prefix) {
177
209
 
178
210
  const url = new URL(request.url);
179
211
  const normalized = prefix.endsWith('/') ? prefix.slice(0, -1) : prefix;
212
+ const matchedPathname = resolvePrefixPathname(url.pathname, normalized);
180
213
 
181
- if (!matchesPrefix(url.pathname, normalized)) {
214
+ if (matchedPathname === null) {
182
215
  return request;
183
216
  }
184
217
 
185
- const nextPath = url.pathname.slice(normalized.length) || '/';
218
+ const nextPath = matchedPathname.slice(normalized.length) || '/';
186
219
  url.pathname = nextPath.startsWith('/') ? nextPath : `/${nextPath}`;
187
220
 
188
221
  return new Request(url, request);
@@ -309,12 +342,21 @@ function getEffectBffDispatcher(bff, runtime) {
309
342
  async function dispatchBffRequest(request, env) {
310
343
  const bff = MODERN_WORKER_MANIFEST.bff;
311
344
 
312
- if (
313
- !bff?.worker ||
314
- !matchesPrefix(new URL(request.url).pathname, bff.prefix)
315
- ) {
345
+ const requestUrl = new URL(request.url);
346
+ const matchedPathname = resolvePrefixPathname(
347
+ requestUrl.pathname,
348
+ bff?.prefix,
349
+ );
350
+ if (!bff?.worker || matchedPathname === null) {
316
351
  return null;
317
352
  }
353
+ const canonicalRequest =
354
+ matchedPathname === requestUrl.pathname
355
+ ? request
356
+ : new Request(
357
+ Object.assign(requestUrl, { pathname: matchedPathname }),
358
+ request,
359
+ );
318
360
  if (bff.runtimeFramework !== 'effect') {
319
361
  return createEffectBffDispatcherErrorResponse(
320
362
  bff,
@@ -347,7 +389,7 @@ async function dispatchBffRequest(request, env) {
347
389
  return createEffectBffDispatcherErrorResponse(bff, error);
348
390
  }
349
391
 
350
- return effectDispatcher.dispatch(request, { env });
392
+ return effectDispatcher.dispatch(canonicalRequest, { env });
351
393
  }
352
394
 
353
395
  const directHandler =
@@ -4,12 +4,13 @@ export declare const getCloudflareArtifacts: (modernConfig: CloudflareModernConf
4
4
  to: string;
5
5
  index: number;
6
6
  }[];
7
- export declare const getCloudflarePublicAssets: (modernConfig: CloudflareModernConfig) => {
8
- from: string;
9
- to: string;
10
- index: number;
11
- }[];
7
+ export declare const getCloudflarePublicAssets: (modernConfig: CloudflareModernConfig) => import("../deploy-output/public-assets.js").DeclaredPublicAsset[];
12
8
  export declare const copyCloudflareArtifacts: (appDirectory: string, outputDirectory: string, artifacts: ReturnType<typeof getCloudflareArtifacts>) => Promise<void>;
13
- export declare const copyCloudflarePublicAssets: (appDirectory: string, publicDirectory: string, publicAssets: ReturnType<typeof getCloudflarePublicAssets>) => Promise<void>;
9
+ /**
10
+ * Stage `deploy.worker.publicAssets` into Worker Static Assets.
11
+ *
12
+ * @returns the staged files relative to the Cloudflare output root.
13
+ */
14
+ export declare const copyCloudflarePublicAssets: (appDirectory: string, outputDirectory: string, publicAssets: ReturnType<typeof getCloudflarePublicAssets>) => Promise<string[]>;
14
15
  export declare const copyCloudflareD1Migrations: (appDirectory: string, outputDirectory: string, modernConfig: CloudflareModernConfig) => Promise<void>;
15
16
  export declare const readRouteSpec: (outputDirectory: string) => Promise<any>;
@@ -28,6 +28,17 @@ export interface CloudflareWorkerPublicAssetConfig {
28
28
  */
29
29
  to: string;
30
30
  }
31
+ export interface NodePublicAssetConfig {
32
+ /**
33
+ * Source file or directory, relative to the app root, to copy into the
34
+ * Node deploy output's public directory.
35
+ */
36
+ from: string;
37
+ /**
38
+ * Destination path relative to `.output/public`.
39
+ */
40
+ to: string;
41
+ }
31
42
  export interface CloudflareWorkerD1DatabaseConfig {
32
43
  /**
33
44
  * Worker binding name, for example `DB`.
@@ -176,3 +187,16 @@ export interface CloudflareDeployConfig {
176
187
  publicAssetExcludes?: string[];
177
188
  };
178
189
  }
190
+ export interface NodeDeployConfig {
191
+ node?: {
192
+ /**
193
+ * Additional app-root files or directories to stage under
194
+ * `.output/public` when `modern deploy` targets Node, the counterpart of
195
+ * `deploy.worker.publicAssets`. The Node server does not route these
196
+ * files; serve them from the app, for example from an API handler.
197
+ * Release envelopes record them as declared public assets, so API-only
198
+ * units may ship them.
199
+ */
200
+ publicAssets?: NodePublicAssetConfig[];
201
+ };
202
+ }
@@ -1,3 +1,4 @@
1
+ import type { NodePublicAssetConfig } from '../config.js';
1
2
  import { type DeployOutputConfig } from './target.js';
2
3
  export interface DeployOutputPluginApi {
3
4
  getAppContext(): {
@@ -13,3 +14,28 @@ export declare const createDeployOutputAliasesPlugin: ({ resolveDeployTarget, }?
13
14
  name: string;
14
15
  setup(api: DeployOutputPluginApi): void;
15
16
  };
17
+ export interface DeployOutputPublicAssetsConfig extends DeployOutputConfig {
18
+ deploy?: DeployOutputConfig['deploy'] & {
19
+ node?: {
20
+ publicAssets?: NodePublicAssetConfig[];
21
+ };
22
+ };
23
+ }
24
+ export interface DeployOutputPublicAssetsPluginApi {
25
+ getAppContext(): {
26
+ appDirectory: string;
27
+ metaName: string;
28
+ };
29
+ getNormalizedConfig(): DeployOutputPublicAssetsConfig;
30
+ onAfterDeploy(handler: () => Promise<void>): void;
31
+ }
32
+ /**
33
+ * Stage `deploy.node.publicAssets` into the Node deploy output. Cloudflare
34
+ * stages `deploy.worker.publicAssets` inside its deploy preset.
35
+ */
36
+ export declare const createDeployOutputPublicAssetsPlugin: ({ resolveDeployTarget, }?: {
37
+ resolveDeployTarget?: (config: DeployOutputConfig) => string;
38
+ }) => {
39
+ name: string;
40
+ setup(api: DeployOutputPublicAssetsPluginApi): void;
41
+ };
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Deploy output directory that receives declared public assets on every
3
+ * target: Cloudflare Worker Static Assets and the Node deploy output.
4
+ */
5
+ export declare const DECLARED_PUBLIC_ASSET_DIRECTORY = "public";
6
+ export interface DeclaredPublicAssetConfig {
7
+ from: string;
8
+ to: string;
9
+ }
10
+ export interface DeclaredPublicAssetScope {
11
+ /** Config path used in errors, for example `deploy.node.publicAssets`. */
12
+ label: string;
13
+ /** Output described in destination errors. */
14
+ output: string;
15
+ }
16
+ export interface DeclaredPublicAsset {
17
+ from: string;
18
+ to: string;
19
+ index: number;
20
+ }
21
+ export declare const CLOUDFLARE_PUBLIC_ASSET_SCOPE: DeclaredPublicAssetScope;
22
+ export declare const NODE_PUBLIC_ASSET_SCOPE: DeclaredPublicAssetScope;
23
+ export declare const normalizeDeclaredPublicAssets: (assets: readonly DeclaredPublicAssetConfig[] | undefined, scope: DeclaredPublicAssetScope) => DeclaredPublicAsset[];
24
+ /**
25
+ * Resolve the files declared public assets add to a deploy output that was
26
+ * copied from `generatedRoot`, relative to the deploy output root. Declared
27
+ * files that replace a generated file are not part of the result, matching
28
+ * {@link stageDeclaredPublicAssets}.
29
+ */
30
+ export declare const resolveAddedDeclaredPublicAssetPaths: ({ appDirectory, assets, generatedRoot, scope, }: {
31
+ appDirectory: string;
32
+ assets: readonly DeclaredPublicAsset[];
33
+ generatedRoot: string;
34
+ scope: DeclaredPublicAssetScope;
35
+ }) => Promise<string[]>;
36
+ /**
37
+ * Copy declared public assets into the deploy output after the framework has
38
+ * staged its own public files.
39
+ *
40
+ * A declared file may replace a generated public file, such as an app-owned
41
+ * `robots.txt`; the replaced file keeps its generated release classification.
42
+ *
43
+ * @returns the files the declarations added, relative to the deploy output
44
+ * root. The release envelope records these as declared public assets.
45
+ */
46
+ export declare const stageDeclaredPublicAssets: ({ appDirectory, outputDirectory, assets, scope, }: {
47
+ appDirectory: string;
48
+ outputDirectory: string;
49
+ assets: readonly DeclaredPublicAsset[];
50
+ scope: DeclaredPublicAssetScope;
51
+ }) => Promise<string[]>;
@@ -18,7 +18,9 @@ export declare const verifyNodeReleaseEnvelopeStaging: ({ outputDirectory, }: {
18
18
  distDirectory?: string;
19
19
  outputDirectory: string;
20
20
  }) => Promise<MicroVerticalReleaseEnvelope | undefined>;
21
- export declare const emitNodeStagedReleaseEnvelope: ({ distDirectory, outputDirectory, }: {
21
+ export declare const emitNodeStagedReleaseEnvelope: ({ declaredPublicAssets, distDirectory, outputDirectory, }: {
22
+ /** Staged `deploy.node.publicAssets` files, relative to `outputDirectory`. */
23
+ declaredPublicAssets?: readonly string[];
22
24
  distDirectory: string;
23
25
  outputDirectory: string;
24
26
  }) => Promise<MicroVerticalReleaseEnvelope | undefined>;
@@ -26,7 +28,9 @@ export declare const stageCloudflareReleaseEnvelope: ({ distDirectory, outputDir
26
28
  distDirectory: string;
27
29
  outputDirectory: string;
28
30
  }) => Promise<MicroVerticalReleaseEnvelope | undefined>;
29
- export declare const emitCloudflareStagedReleaseEnvelope: ({ distDirectory, outputDirectory, }: {
31
+ export declare const emitCloudflareStagedReleaseEnvelope: ({ declaredPublicAssets, distDirectory, outputDirectory, }: {
32
+ /** Staged `deploy.worker.publicAssets` files, relative to `outputDirectory`. */
33
+ declaredPublicAssets?: readonly string[];
30
34
  distDirectory: string;
31
35
  outputDirectory: string;
32
36
  }) => Promise<MicroVerticalReleaseEnvelope | undefined>;
@@ -1,7 +1,11 @@
1
+ import type { NodePublicAssetConfig } from '../config.js';
1
2
  type ReleaseEnvelopeTarget = 'cloudflare' | 'node' | string;
2
3
  export interface ReleaseEnvelopeConfig {
3
4
  deploy?: {
4
5
  target?: string;
6
+ node?: {
7
+ publicAssets?: NodePublicAssetConfig[];
8
+ };
5
9
  };
6
10
  }
7
11
  export interface ReleaseEnvelopeAppContext {
package/package.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "directory": "packages/solutions/app-tools-extensions"
12
12
  },
13
13
  "license": "MIT",
14
- "version": "3.9.0-ultramodern.14",
14
+ "version": "3.9.0-ultramodern.16",
15
15
  "files": [
16
16
  "dist",
17
17
  "src"
@@ -145,16 +145,16 @@
145
145
  "@babel/parser": "^8.0.6",
146
146
  "@babel/traverse": "^8.0.6",
147
147
  "@loadable/component": "5.16.7",
148
- "@modern-js/backend-federation-contracts": "npm:@bleedingdev/modern-js-backend-federation-contracts@3.9.0-ultramodern.14",
149
- "@modern-js/builder": "npm:@bleedingdev/modern-js-builder@3.9.0-ultramodern.14",
150
- "@modern-js/prod-server": "npm:@bleedingdev/modern-js-prod-server@3.9.0-ultramodern.14",
151
- "@modern-js/runtime-renderer-extensions": "npm:@bleedingdev/modern-js-runtime-renderer-extensions@3.9.0-ultramodern.14",
152
- "@modern-js/runtime-utils": "npm:@bleedingdev/modern-js-runtime-utils@3.9.0-ultramodern.14",
153
- "@modern-js/server-core": "npm:@bleedingdev/modern-js-server-core@3.9.0-ultramodern.14",
154
- "@modern-js/server-runtime-extensions": "npm:@bleedingdev/modern-js-server-runtime-extensions@3.9.0-ultramodern.14",
155
- "@modern-js/surface-resolution": "npm:@bleedingdev/modern-js-surface-resolution@3.9.0-ultramodern.14",
156
- "@modern-js/types": "npm:@bleedingdev/modern-js-types@3.9.0-ultramodern.14",
157
- "@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.9.0-ultramodern.14",
148
+ "@modern-js/backend-federation-contracts": "npm:@bleedingdev/modern-js-backend-federation-contracts@3.9.0-ultramodern.16",
149
+ "@modern-js/builder": "npm:@bleedingdev/modern-js-builder@3.9.0-ultramodern.16",
150
+ "@modern-js/prod-server": "npm:@bleedingdev/modern-js-prod-server@3.9.0-ultramodern.16",
151
+ "@modern-js/runtime-renderer-extensions": "npm:@bleedingdev/modern-js-runtime-renderer-extensions@3.9.0-ultramodern.16",
152
+ "@modern-js/runtime-utils": "npm:@bleedingdev/modern-js-runtime-utils@3.9.0-ultramodern.16",
153
+ "@modern-js/server-core": "npm:@bleedingdev/modern-js-server-core@3.9.0-ultramodern.16",
154
+ "@modern-js/server-runtime-extensions": "npm:@bleedingdev/modern-js-server-runtime-extensions@3.9.0-ultramodern.16",
155
+ "@modern-js/surface-resolution": "npm:@bleedingdev/modern-js-surface-resolution@3.9.0-ultramodern.16",
156
+ "@modern-js/types": "npm:@bleedingdev/modern-js-types@3.9.0-ultramodern.16",
157
+ "@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.9.0-ultramodern.16",
158
158
  "@swc/helpers": "^0.5.23",
159
159
  "compression-webpack-plugin": "^12.0.0",
160
160
  "esbuild": "^0.28.2",
@@ -164,9 +164,9 @@
164
164
  "@rsbuild/core": "2.2.9"
165
165
  },
166
166
  "devDependencies": {
167
- "@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.9.0-ultramodern.14",
168
- "@modern-js/plugin-bff-extensions": "npm:@bleedingdev/modern-js-plugin-bff-extensions@3.9.0-ultramodern.14",
169
- "@modern-js/plugin-data-loader": "npm:@bleedingdev/modern-js-plugin-data-loader@3.9.0-ultramodern.14",
167
+ "@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.9.0-ultramodern.16",
168
+ "@modern-js/plugin-bff-extensions": "npm:@bleedingdev/modern-js-plugin-bff-extensions@3.9.0-ultramodern.16",
169
+ "@modern-js/plugin-data-loader": "npm:@bleedingdev/modern-js-plugin-data-loader@3.9.0-ultramodern.16",
170
170
  "@module-federation/runtime-tools": "npm:@bleedingdev/mf-runtime-tools@2.9.1",
171
171
  "@rsbuild/core": "2.2.9",
172
172
  "@rslib/core": "1.0.2",
@@ -1,9 +1,11 @@
1
1
  import path from 'node:path';
2
2
  import { fs as fse } from '@modern-js/utils';
3
- import type {
4
- CloudflareWorkerArtifactConfig,
5
- CloudflareWorkerPublicAssetConfig,
6
- } from '../config';
3
+ import type { CloudflareWorkerArtifactConfig } from '../config';
4
+ import {
5
+ CLOUDFLARE_PUBLIC_ASSET_SCOPE,
6
+ normalizeDeclaredPublicAssets,
7
+ stageDeclaredPublicAssets,
8
+ } from '../deploy-output/public-assets';
7
9
  import {
8
10
  RESERVED_ARTIFACT_DESTINATION_DIRECTORIES,
9
11
  RESERVED_ARTIFACT_DESTINATION_FILES,
@@ -54,34 +56,12 @@ export const getCloudflareArtifacts = (modernConfig: CloudflareModernConfig) =>
54
56
  normalizeCloudflareArtifact,
55
57
  );
56
58
 
57
- const normalizeCloudflarePublicAsset = (
58
- asset: CloudflareWorkerPublicAssetConfig,
59
- index: number,
60
- ) => {
61
- const from = normalizeRelativePath(
62
- asset.from,
63
- `deploy.worker.publicAssets[${index}].from`,
64
- 'app root',
65
- );
66
- const to = normalizeRelativePath(
67
- asset.to,
68
- `deploy.worker.publicAssets[${index}].to`,
69
- 'Cloudflare public output',
70
- { allowRoot: true },
71
- );
72
-
73
- return {
74
- from,
75
- to,
76
- index,
77
- };
78
- };
79
-
80
59
  export const getCloudflarePublicAssets = (
81
60
  modernConfig: CloudflareModernConfig,
82
61
  ) =>
83
- (modernConfig.deploy?.worker?.publicAssets ?? []).map(
84
- normalizeCloudflarePublicAsset,
62
+ normalizeDeclaredPublicAssets(
63
+ modernConfig.deploy?.worker?.publicAssets,
64
+ CLOUDFLARE_PUBLIC_ASSET_SCOPE,
85
65
  );
86
66
 
87
67
  export const copyCloudflareArtifacts = async (
@@ -102,23 +82,22 @@ export const copyCloudflareArtifacts = async (
102
82
  }
103
83
  };
104
84
 
85
+ /**
86
+ * Stage `deploy.worker.publicAssets` into Worker Static Assets.
87
+ *
88
+ * @returns the staged files relative to the Cloudflare output root.
89
+ */
105
90
  export const copyCloudflarePublicAssets = async (
106
91
  appDirectory: string,
107
- publicDirectory: string,
92
+ outputDirectory: string,
108
93
  publicAssets: ReturnType<typeof getCloudflarePublicAssets>,
109
- ) => {
110
- for (const asset of publicAssets) {
111
- const sourcePath = path.join(appDirectory, asset.from);
112
-
113
- if (!(await fse.pathExists(sourcePath))) {
114
- throw new Error(
115
- `deploy.worker.publicAssets[${asset.index}].from does not exist: ${asset.from}`,
116
- );
117
- }
118
-
119
- await fse.copy(sourcePath, path.join(publicDirectory, asset.to));
120
- }
121
- };
94
+ ) =>
95
+ stageDeclaredPublicAssets({
96
+ appDirectory,
97
+ outputDirectory,
98
+ assets: publicAssets,
99
+ scope: CLOUDFLARE_PUBLIC_ASSET_SCOPE,
100
+ });
122
101
 
123
102
  export const copyCloudflareD1Migrations = async (
124
103
  appDirectory: string,
@@ -92,6 +92,7 @@ export const createCloudflarePreset: CreateCloudflarePreset = ({
92
92
  '@modern-js/ultramodern-release-envelope',
93
93
  );
94
94
  let hasReleaseEnvelope = false;
95
+ let declaredPublicAssets: string[] = [];
95
96
 
96
97
  return {
97
98
  async prepare() {
@@ -187,9 +188,9 @@ export const createCloudflarePreset: CreateCloudflarePreset = ({
187
188
  outputDirectory,
188
189
  cloudflareArtifacts,
189
190
  );
190
- await copyCloudflarePublicAssets(
191
+ declaredPublicAssets = await copyCloudflarePublicAssets(
191
192
  appDirectory,
192
- publicDirectory,
193
+ outputDirectory,
193
194
  getCloudflarePublicAssets(modernConfig),
194
195
  );
195
196
  await copyCloudflareD1Migrations(
@@ -255,6 +256,7 @@ export const createCloudflarePreset: CreateCloudflarePreset = ({
255
256
  });
256
257
  if (hasReleaseEnvelope) {
257
258
  await emitCloudflareStagedReleaseEnvelope({
259
+ declaredPublicAssets,
258
260
  distDirectory,
259
261
  outputDirectory,
260
262
  });
package/src/config.ts CHANGED
@@ -47,6 +47,18 @@ export interface CloudflareWorkerPublicAssetConfig {
47
47
  to: string;
48
48
  }
49
49
 
50
+ export interface NodePublicAssetConfig {
51
+ /**
52
+ * Source file or directory, relative to the app root, to copy into the
53
+ * Node deploy output's public directory.
54
+ */
55
+ from: string;
56
+ /**
57
+ * Destination path relative to `.output/public`.
58
+ */
59
+ to: string;
60
+ }
61
+
50
62
  export interface CloudflareWorkerD1DatabaseConfig {
51
63
  /**
52
64
  * Worker binding name, for example `DB`.
@@ -202,3 +214,17 @@ export interface CloudflareDeployConfig {
202
214
  publicAssetExcludes?: string[];
203
215
  };
204
216
  }
217
+
218
+ export interface NodeDeployConfig {
219
+ node?: {
220
+ /**
221
+ * Additional app-root files or directories to stage under
222
+ * `.output/public` when `modern deploy` targets Node, the counterpart of
223
+ * `deploy.worker.publicAssets`. The Node server does not route these
224
+ * files; serve them from the app, for example from an API handler.
225
+ * Release envelopes record them as declared public assets, so API-only
226
+ * units may ship them.
227
+ */
228
+ publicAssets?: NodePublicAssetConfig[];
229
+ };
230
+ }
@@ -1,6 +1,12 @@
1
1
  import { createRequire } from 'node:module';
2
2
  import path from 'node:path';
3
+ import type { NodePublicAssetConfig } from '../config';
3
4
  import { preserveNpmAliases, readPackageIdentity } from './npmAliases';
5
+ import {
6
+ NODE_PUBLIC_ASSET_SCOPE,
7
+ normalizeDeclaredPublicAssets,
8
+ stageDeclaredPublicAssets,
9
+ } from './public-assets';
4
10
  import {
5
11
  type DeployOutputConfig,
6
12
  resolveDeployTarget as defaultResolveDeployTarget,
@@ -47,3 +53,51 @@ export const createDeployOutputAliasesPlugin = ({
47
53
  });
48
54
  },
49
55
  });
56
+
57
+ export interface DeployOutputPublicAssetsConfig extends DeployOutputConfig {
58
+ deploy?: DeployOutputConfig['deploy'] & {
59
+ node?: { publicAssets?: NodePublicAssetConfig[] };
60
+ };
61
+ }
62
+
63
+ export interface DeployOutputPublicAssetsPluginApi {
64
+ getAppContext(): { appDirectory: string; metaName: string };
65
+ getNormalizedConfig(): DeployOutputPublicAssetsConfig;
66
+ onAfterDeploy(handler: () => Promise<void>): void;
67
+ }
68
+
69
+ /**
70
+ * Stage `deploy.node.publicAssets` into the Node deploy output. Cloudflare
71
+ * stages `deploy.worker.publicAssets` inside its deploy preset.
72
+ */
73
+ export const createDeployOutputPublicAssetsPlugin = ({
74
+ resolveDeployTarget = defaultResolveDeployTarget,
75
+ }: {
76
+ resolveDeployTarget?: (config: DeployOutputConfig) => string;
77
+ } = {}) => ({
78
+ name: '@modern-js/deploy-output-public-assets',
79
+ setup(api: DeployOutputPublicAssetsPluginApi) {
80
+ api.onAfterDeploy(async () => {
81
+ const { appDirectory, metaName } = api.getAppContext();
82
+ const config = api.getNormalizedConfig();
83
+ if (
84
+ (metaName !== 'modern-js' &&
85
+ !config.deploy?.target &&
86
+ !process.env.MODERNJS_DEPLOY) ||
87
+ resolveDeployTarget(config) !== 'node'
88
+ ) {
89
+ return;
90
+ }
91
+
92
+ await stageDeclaredPublicAssets({
93
+ appDirectory,
94
+ outputDirectory: path.join(appDirectory, '.output'),
95
+ assets: normalizeDeclaredPublicAssets(
96
+ config.deploy?.node?.publicAssets,
97
+ NODE_PUBLIC_ASSET_SCOPE,
98
+ ),
99
+ scope: NODE_PUBLIC_ASSET_SCOPE,
100
+ });
101
+ });
102
+ },
103
+ });