@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.
- package/dist/cjs/cloudflare/artifacts.js +8 -19
- package/dist/cjs/cloudflare/index.js +3 -1
- package/dist/cjs/deploy-output/plugin.js +22 -2
- package/dist/cjs/deploy-output/public-assets.js +146 -0
- package/dist/cjs/release-envelope/framework-output.js +24 -8
- package/dist/cjs/release-envelope/plugin.js +9 -1
- package/dist/cjs/templates/cloudflare-entry.005-worker-dispatch.mjs +52 -10
- package/dist/esm/cloudflare/artifacts.mjs +8 -19
- package/dist/esm/cloudflare/index.mjs +3 -1
- package/dist/esm/deploy-output/plugin.mjs +18 -1
- package/dist/esm/deploy-output/public-assets.mjs +82 -0
- package/dist/esm/release-envelope/framework-output.mjs +24 -8
- package/dist/esm/release-envelope/plugin.mjs +9 -1
- package/dist/esm/templates/cloudflare-entry.005-worker-dispatch.mjs +52 -10
- package/dist/esm-node/cloudflare/artifacts.mjs +8 -19
- package/dist/esm-node/cloudflare/index.mjs +3 -1
- package/dist/esm-node/deploy-output/plugin.mjs +18 -1
- package/dist/esm-node/deploy-output/public-assets.mjs +83 -0
- package/dist/esm-node/release-envelope/framework-output.mjs +24 -8
- package/dist/esm-node/release-envelope/plugin.mjs +9 -1
- package/dist/esm-node/templates/cloudflare-entry.005-worker-dispatch.mjs +52 -10
- package/dist/types/cloudflare/artifacts.d.ts +7 -6
- package/dist/types/config.d.ts +24 -0
- package/dist/types/deploy-output/plugin.d.ts +26 -0
- package/dist/types/deploy-output/public-assets.d.ts +51 -0
- package/dist/types/release-envelope/framework-output.d.ts +6 -2
- package/dist/types/release-envelope/plugin.d.ts +4 -0
- package/package.json +14 -14
- package/src/cloudflare/artifacts.ts +22 -43
- package/src/cloudflare/index.ts +4 -2
- package/src/config.ts +26 -0
- package/src/deploy-output/plugin.ts +54 -0
- package/src/deploy-output/public-assets.ts +224 -0
- package/src/release-envelope/framework-output.ts +49 -1
- package/src/release-envelope/plugin.ts +22 -1
- package/src/templates/cloudflare-entry.005-worker-dispatch.mjs +52 -10
|
@@ -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
|
+
};
|
|
@@ -43,6 +43,28 @@ const isPublicMetadataPath = (logicalPath: string) =>
|
|
|
43
43
|
logicalPath === 'public/_headers' ||
|
|
44
44
|
/^public\/\.well-known\/[^/]+\.json$/u.test(logicalPath);
|
|
45
45
|
const PUBLIC_BUILD_ARTIFACT_PATH = `public/${ULTRAMODERN_BUILD_ARTIFACT_FILE}`;
|
|
46
|
+
// Files an app declared through `deploy.<target>.publicAssets`. The deploy
|
|
47
|
+
// output stages them from app-root sources; they are served data, not a
|
|
48
|
+
// UI/client surface, so API-only units may carry them.
|
|
49
|
+
const DECLARED_PUBLIC_ASSET_RUNTIME = 'public-asset';
|
|
50
|
+
|
|
51
|
+
const resolveDeclaredPublicAssets = (
|
|
52
|
+
files: readonly string[],
|
|
53
|
+
declaredPublicAssets: readonly string[],
|
|
54
|
+
target: 'Node' | 'Cloudflare',
|
|
55
|
+
) => {
|
|
56
|
+
const staged = new Set(files);
|
|
57
|
+
const declared = new Set<string>();
|
|
58
|
+
for (const logicalPath of declaredPublicAssets) {
|
|
59
|
+
if (!logicalPath.startsWith('public/') || !staged.has(logicalPath)) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`[ultramodern-release-envelope] final ${target} staging has no declared public asset file ${JSON.stringify(logicalPath)}.`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
declared.add(logicalPath);
|
|
65
|
+
}
|
|
66
|
+
return declared;
|
|
67
|
+
};
|
|
46
68
|
|
|
47
69
|
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
48
70
|
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
@@ -902,8 +924,14 @@ const createNodeStagedReleaseArtifactInputs = async (
|
|
|
902
924
|
outputDirectory: string,
|
|
903
925
|
identity: MicroVerticalReleaseIdentity,
|
|
904
926
|
apiOnly: boolean,
|
|
927
|
+
declaredPublicAssets: readonly string[],
|
|
905
928
|
) => {
|
|
906
929
|
const files = await collectFiles(outputDirectory);
|
|
930
|
+
const declaredPublicAssetPaths = resolveDeclaredPublicAssets(
|
|
931
|
+
files,
|
|
932
|
+
declaredPublicAssets,
|
|
933
|
+
'Node',
|
|
934
|
+
);
|
|
907
935
|
for (const requiredPath of [
|
|
908
936
|
'index.js',
|
|
909
937
|
'package.json',
|
|
@@ -928,7 +956,8 @@ const createNodeStagedReleaseArtifactInputs = async (
|
|
|
928
956
|
topLevel === 'html' ||
|
|
929
957
|
(topLevel === 'public' &&
|
|
930
958
|
logicalPath !== CRAWLER_POLICY_PATH &&
|
|
931
|
-
!isPublicMetadataPath(logicalPath)
|
|
959
|
+
!isPublicMetadataPath(logicalPath) &&
|
|
960
|
+
!declaredPublicAssetPaths.has(logicalPath)) ||
|
|
932
961
|
[
|
|
933
962
|
'index.html',
|
|
934
963
|
'mf-manifest.json',
|
|
@@ -992,6 +1021,9 @@ const createNodeStagedReleaseArtifactInputs = async (
|
|
|
992
1021
|
for (const logicalPath of uiClientPaths) {
|
|
993
1022
|
runtimeByPath.set(logicalPath, 'browser');
|
|
994
1023
|
}
|
|
1024
|
+
for (const logicalPath of declaredPublicAssetPaths) {
|
|
1025
|
+
runtimeByPath.set(logicalPath, DECLARED_PUBLIC_ASSET_RUNTIME);
|
|
1026
|
+
}
|
|
995
1027
|
if (files.includes(CRAWLER_POLICY_PATH)) {
|
|
996
1028
|
runtimeByPath.set(CRAWLER_POLICY_PATH, 'crawler-policy');
|
|
997
1029
|
}
|
|
@@ -1058,9 +1090,12 @@ const createNodeStagedReleaseArtifactInputs = async (
|
|
|
1058
1090
|
};
|
|
1059
1091
|
|
|
1060
1092
|
export const emitNodeStagedReleaseEnvelope = async ({
|
|
1093
|
+
declaredPublicAssets = [],
|
|
1061
1094
|
distDirectory,
|
|
1062
1095
|
outputDirectory,
|
|
1063
1096
|
}: {
|
|
1097
|
+
/** Staged `deploy.node.publicAssets` files, relative to `outputDirectory`. */
|
|
1098
|
+
declaredPublicAssets?: readonly string[];
|
|
1064
1099
|
distDirectory: string;
|
|
1065
1100
|
outputDirectory: string;
|
|
1066
1101
|
}) => {
|
|
@@ -1072,6 +1107,7 @@ export const emitNodeStagedReleaseEnvelope = async ({
|
|
|
1072
1107
|
outputDirectory,
|
|
1073
1108
|
source.identity,
|
|
1074
1109
|
source.surfaces.uiClient.length === 0,
|
|
1110
|
+
declaredPublicAssets,
|
|
1075
1111
|
);
|
|
1076
1112
|
const staged = await createMicroVerticalReleaseEnvelope({
|
|
1077
1113
|
artifactRoot: outputDirectory,
|
|
@@ -1114,8 +1150,14 @@ const createCloudflareStagedReleaseArtifactInputs = async (
|
|
|
1114
1150
|
outputDirectory: string,
|
|
1115
1151
|
identity: MicroVerticalReleaseIdentity,
|
|
1116
1152
|
apiOnly: boolean,
|
|
1153
|
+
declaredPublicAssets: readonly string[],
|
|
1117
1154
|
) => {
|
|
1118
1155
|
const files = await collectFiles(outputDirectory);
|
|
1156
|
+
const declaredPublicAssetPaths = resolveDeclaredPublicAssets(
|
|
1157
|
+
files,
|
|
1158
|
+
declaredPublicAssets,
|
|
1159
|
+
'Cloudflare',
|
|
1160
|
+
);
|
|
1119
1161
|
for (const requiredPath of [
|
|
1120
1162
|
'server/index.mjs',
|
|
1121
1163
|
'server/modern-worker-manifest.json',
|
|
@@ -1160,6 +1202,7 @@ const createCloudflareStagedReleaseArtifactInputs = async (
|
|
|
1160
1202
|
logicalPath.startsWith('public/') &&
|
|
1161
1203
|
logicalPath !== CRAWLER_POLICY_PATH &&
|
|
1162
1204
|
!isPublicMetadataPath(logicalPath) &&
|
|
1205
|
+
!declaredPublicAssetPaths.has(logicalPath) &&
|
|
1163
1206
|
logicalPath !== PUBLIC_BUILD_ARTIFACT_PATH &&
|
|
1164
1207
|
logicalPath !== backendManifestPath &&
|
|
1165
1208
|
logicalPath !== backendContainerPath,
|
|
@@ -1229,6 +1272,7 @@ const createCloudflareStagedReleaseArtifactInputs = async (
|
|
|
1229
1272
|
}
|
|
1230
1273
|
};
|
|
1231
1274
|
add(uiClientPaths, 'browser');
|
|
1275
|
+
add([...declaredPublicAssetPaths], DECLARED_PUBLIC_ASSET_RUNTIME);
|
|
1232
1276
|
if (files.includes(CRAWLER_POLICY_PATH)) {
|
|
1233
1277
|
runtimeByPath.set(CRAWLER_POLICY_PATH, 'crawler-policy');
|
|
1234
1278
|
}
|
|
@@ -1285,9 +1329,12 @@ const createCloudflareStagedReleaseArtifactInputs = async (
|
|
|
1285
1329
|
};
|
|
1286
1330
|
|
|
1287
1331
|
export const emitCloudflareStagedReleaseEnvelope = async ({
|
|
1332
|
+
declaredPublicAssets = [],
|
|
1288
1333
|
distDirectory,
|
|
1289
1334
|
outputDirectory,
|
|
1290
1335
|
}: {
|
|
1336
|
+
/** Staged `deploy.worker.publicAssets` files, relative to `outputDirectory`. */
|
|
1337
|
+
declaredPublicAssets?: readonly string[];
|
|
1291
1338
|
distDirectory: string;
|
|
1292
1339
|
outputDirectory: string;
|
|
1293
1340
|
}) => {
|
|
@@ -1303,6 +1350,7 @@ export const emitCloudflareStagedReleaseEnvelope = async ({
|
|
|
1303
1350
|
outputDirectory,
|
|
1304
1351
|
source.identity,
|
|
1305
1352
|
source.surfaces.uiClient.length === 0,
|
|
1353
|
+
declaredPublicAssets,
|
|
1306
1354
|
);
|
|
1307
1355
|
const staged = await createMicroVerticalReleaseEnvelope({
|
|
1308
1356
|
artifactRoot: outputDirectory,
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import type { NodePublicAssetConfig } from '../config';
|
|
3
|
+
import {
|
|
4
|
+
NODE_PUBLIC_ASSET_SCOPE,
|
|
5
|
+
normalizeDeclaredPublicAssets,
|
|
6
|
+
resolveAddedDeclaredPublicAssetPaths,
|
|
7
|
+
} from '../deploy-output/public-assets';
|
|
2
8
|
import {
|
|
3
9
|
emitFrameworkMicroVerticalReleaseEnvelope,
|
|
4
10
|
emitNodeStagedReleaseEnvelope,
|
|
@@ -9,7 +15,10 @@ import {
|
|
|
9
15
|
type ReleaseEnvelopeTarget = 'cloudflare' | 'node' | string;
|
|
10
16
|
|
|
11
17
|
export interface ReleaseEnvelopeConfig {
|
|
12
|
-
deploy?: {
|
|
18
|
+
deploy?: {
|
|
19
|
+
target?: string;
|
|
20
|
+
node?: { publicAssets?: NodePublicAssetConfig[] };
|
|
21
|
+
};
|
|
13
22
|
}
|
|
14
23
|
|
|
15
24
|
export interface ReleaseEnvelopeAppContext {
|
|
@@ -71,6 +80,7 @@ export const createUltramodernReleaseEnvelopePlugin = <
|
|
|
71
80
|
'@modern-js/backend-federation-build',
|
|
72
81
|
'@modern-js/plugin-bff',
|
|
73
82
|
'@modern-js/deploy-output-aliases',
|
|
83
|
+
'@modern-js/deploy-output-public-assets',
|
|
74
84
|
],
|
|
75
85
|
post: ['@modern-js/plugin-deploy'],
|
|
76
86
|
setup(api) {
|
|
@@ -123,6 +133,17 @@ export const createUltramodernReleaseEnvelopePlugin = <
|
|
|
123
133
|
const { appDirectory, distDirectory } = api.getAppContext();
|
|
124
134
|
const outputDirectory = path.join(appDirectory, '.output');
|
|
125
135
|
const releaseEnvelope = await emitNodeStagedReleaseEnvelope({
|
|
136
|
+
// The Node deploy output is a copy of dist, so a declared file
|
|
137
|
+
// added output exactly when dist has no file at its path.
|
|
138
|
+
declaredPublicAssets: await resolveAddedDeclaredPublicAssetPaths({
|
|
139
|
+
appDirectory,
|
|
140
|
+
assets: normalizeDeclaredPublicAssets(
|
|
141
|
+
api.getNormalizedConfig().deploy?.node?.publicAssets,
|
|
142
|
+
NODE_PUBLIC_ASSET_SCOPE,
|
|
143
|
+
),
|
|
144
|
+
generatedRoot: distDirectory,
|
|
145
|
+
scope: NODE_PUBLIC_ASSET_SCOPE,
|
|
146
|
+
}),
|
|
126
147
|
distDirectory,
|
|
127
148
|
outputDirectory,
|
|
128
149
|
});
|
|
@@ -160,14 +160,46 @@ async function dispatchRouteWorker(route, request, env, ctx) {
|
|
|
160
160
|
);
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
|
|
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
|
|
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
|
|
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 (
|
|
214
|
+
if (matchedPathname === null) {
|
|
182
215
|
return request;
|
|
183
216
|
}
|
|
184
217
|
|
|
185
|
-
const nextPath =
|
|
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
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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(
|
|
392
|
+
return effectDispatcher.dispatch(canonicalRequest, { env });
|
|
351
393
|
}
|
|
352
394
|
|
|
353
395
|
const directHandler =
|