@lwrjs/core 0.15.0-alpha.9 → 0.15.1
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/build/cjs/__mocks__/undici.cjs +25 -0
- package/build/cjs/index.cjs +2 -2
- package/build/cjs/middleware/asset-middleware.cjs +12 -0
- package/build/cjs/middleware/bundle-middleware.cjs +1 -1
- package/build/cjs/middleware/request-processor-middleware.cjs +20 -3
- package/build/cjs/middleware/utils/compression.cjs +16 -0
- package/build/cjs/middleware/utils/error-handling.cjs +1 -1
- package/build/cjs/middleware/view-middleware.cjs +107 -15
- package/build/cjs/tools/static-generation.cjs +26 -7
- package/build/cjs/tools/utils/stream.cjs +2 -0
- package/build/es/index.js +2 -2
- package/build/es/middleware/asset-middleware.js +17 -0
- package/build/es/middleware/bundle-middleware.js +3 -1
- package/build/es/middleware/request-processor-middleware.js +28 -5
- package/build/es/middleware/utils/compression.d.ts +2 -0
- package/build/es/middleware/utils/compression.js +6 -0
- package/build/es/middleware/utils/error-handling.js +1 -1
- package/build/es/middleware/view-middleware.js +136 -16
- package/build/es/tools/static-generation.d.ts +4 -0
- package/build/es/tools/static-generation.js +41 -7
- package/build/es/tools/utils/stream.d.ts +1 -1
- package/build/es/tools/utils/stream.js +2 -0
- package/package.json +34 -34
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __markAsModule = (target) => __defProp(target, "__esModule", {value: true});
|
|
3
|
+
var __export = (target, all) => {
|
|
4
|
+
for (var name in all)
|
|
5
|
+
__defProp(target, name, {get: all[name], enumerable: true});
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
// packages/@lwrjs/core/src/__mocks__/undici.ts
|
|
9
|
+
__markAsModule(exports);
|
|
10
|
+
__export(exports, {
|
|
11
|
+
Pool: () => Pool
|
|
12
|
+
});
|
|
13
|
+
var Pool = class {
|
|
14
|
+
async request() {
|
|
15
|
+
return Promise.resolve({
|
|
16
|
+
statusCode: 200,
|
|
17
|
+
body: {
|
|
18
|
+
text: () => Promise.resolve('{"one":1}'),
|
|
19
|
+
json: () => Promise.resolve({one: 1})
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
close() {
|
|
24
|
+
}
|
|
25
|
+
};
|
package/build/cjs/index.cjs
CHANGED
|
@@ -109,9 +109,9 @@ var LwrApp = class {
|
|
|
109
109
|
this.config = appConfig;
|
|
110
110
|
this.runtimeEnvironment = runtimeEnvironment;
|
|
111
111
|
this.globalData = globalData;
|
|
112
|
-
const {basePath, serverType} = this.config;
|
|
112
|
+
const {basePath, serverType, caseSensitiveRoutes} = this.config;
|
|
113
113
|
this.serverType = serverType;
|
|
114
|
-
this.app = (0, import_server.createInternalServer)(serverType, {basePath});
|
|
114
|
+
this.app = (0, import_server.createInternalServer)(serverType, {basePath, caseSensitiveRoutes});
|
|
115
115
|
this.server = this.app.createHttpServer();
|
|
116
116
|
this.use = this.app.use.bind(this.app);
|
|
117
117
|
this.all = this.app.all.bind(this.app);
|
|
@@ -26,6 +26,7 @@ __markAsModule(exports);
|
|
|
26
26
|
__export(exports, {
|
|
27
27
|
assetMiddleware: () => assetMiddleware
|
|
28
28
|
});
|
|
29
|
+
var import_shared_utils = __toModule(require("@lwrjs/shared-utils"));
|
|
29
30
|
var import_path = __toModule(require("path"));
|
|
30
31
|
var import_diagnostics = __toModule(require("@lwrjs/diagnostics"));
|
|
31
32
|
var import_instrumentation = __toModule(require("@lwrjs/instrumentation"));
|
|
@@ -81,6 +82,12 @@ function createAssetMiddleware(context) {
|
|
|
81
82
|
res.setHeader("Cache-control", "public, max-age=12895706, immutable");
|
|
82
83
|
} else if (runtimeEnvironment.immutableAssets) {
|
|
83
84
|
res.setHeader("Cache-control", "public, max-age=60");
|
|
85
|
+
const extraAssetHeaders = parseHeaderStringToObject((0, import_shared_utils.getFeatureFlags)().EXPERIMENTAL_ASSET_HEADERS);
|
|
86
|
+
if (extraAssetHeaders) {
|
|
87
|
+
for (const [key, value] of Object.entries(extraAssetHeaders)) {
|
|
88
|
+
res.setHeader(key, value);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
84
91
|
}
|
|
85
92
|
res.status(200).stream(asset.stream());
|
|
86
93
|
} catch (error) {
|
|
@@ -111,3 +118,8 @@ function sendRedirect(res, assetUri) {
|
|
|
111
118
|
});
|
|
112
119
|
res.sendStatus(302);
|
|
113
120
|
}
|
|
121
|
+
function parseHeaderStringToObject(assetHeadersString) {
|
|
122
|
+
if (typeof assetHeadersString === "string") {
|
|
123
|
+
return Object.fromEntries(assetHeadersString.split(";").filter(Boolean).map((e) => e.split(":").map((s) => s.trim())).filter((pair) => pair.length === 2 && pair[0]));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -84,7 +84,7 @@ function createBundleMiddleware(context) {
|
|
|
84
84
|
if (signature !== import_shared_utils.LATEST_SIGNATURE) {
|
|
85
85
|
res.setHeader("Cache-control", "public, max-age=31536000, immutable");
|
|
86
86
|
}
|
|
87
|
-
res.status(200).type("application/javascript").send(bundleDefinition.
|
|
87
|
+
res.status(200).type("application/javascript").send(await bundleDefinition.getCode());
|
|
88
88
|
};
|
|
89
89
|
}
|
|
90
90
|
function createSourceMapMiddleware(context) {
|
|
@@ -45,7 +45,7 @@ function requestProcessorMiddleware(app, context) {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
requestClass = req.headers[MRT_REQUEST_CLASS_KEY];
|
|
48
|
-
requestDepth = (0, import_shared_utils.
|
|
48
|
+
requestDepth = (0, import_shared_utils.parseRequestDepth)(req.headers, req.query);
|
|
49
49
|
const forwarded = req.headers["forwarded"];
|
|
50
50
|
const host = req.headers["host"];
|
|
51
51
|
const forwardedProto = req.headers["x-forwarded-proto"];
|
|
@@ -57,14 +57,14 @@ function requestProcessorMiddleware(app, context) {
|
|
|
57
57
|
});
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
|
-
if (requestDepth && requestDepth >
|
|
60
|
+
if (requestDepth && requestDepth > 1) {
|
|
61
61
|
import_diagnostics.logger.error({
|
|
62
62
|
label: "request-processor-middleware",
|
|
63
63
|
message: `Lambda SSR request cycle detected: ${req.originalUrl}`
|
|
64
64
|
});
|
|
65
65
|
return res.status(400).send("Request depth limit reached");
|
|
66
66
|
}
|
|
67
|
-
if (req.headers && typeof requestClass === "string") {
|
|
67
|
+
if (req.headers && requestClass && typeof requestClass === "string") {
|
|
68
68
|
const parsedRequestClass = parseRequestClass(requestClass);
|
|
69
69
|
import_diagnostics.logger.debug({
|
|
70
70
|
label: `request-processor-middleware`,
|
|
@@ -72,6 +72,20 @@ function requestProcessorMiddleware(app, context) {
|
|
|
72
72
|
});
|
|
73
73
|
const pathValue = parsedRequestClass?.basePath || basePath || "";
|
|
74
74
|
req.basePath = pathValue === "" || pathValue.indexOf("/") === 0 ? pathValue : `/${pathValue}`;
|
|
75
|
+
const expressRequest = req.req;
|
|
76
|
+
if (expressRequest?.url && parsedRequestClass?.basePath) {
|
|
77
|
+
const {pathname, search} = new URL(expressRequest.url, "http://localhost");
|
|
78
|
+
if (pathname.startsWith(parsedRequestClass.basePath)) {
|
|
79
|
+
const newPath = pathname.slice(parsedRequestClass.basePath.length) || "/";
|
|
80
|
+
expressRequest.url = newPath + search;
|
|
81
|
+
expressRequest.originalUrl = newPath + search;
|
|
82
|
+
} else {
|
|
83
|
+
import_diagnostics.logger.warn({
|
|
84
|
+
label: `request-processor-middleware`,
|
|
85
|
+
message: `The URL requested for doesn't start with the Base path`
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
75
89
|
} else {
|
|
76
90
|
import_diagnostics.logger.debug({
|
|
77
91
|
label: `request-processor-middleware`,
|
|
@@ -83,6 +97,9 @@ function requestProcessorMiddleware(app, context) {
|
|
|
83
97
|
});
|
|
84
98
|
}
|
|
85
99
|
function parseRequestClass(requestClass) {
|
|
100
|
+
if (!requestClass) {
|
|
101
|
+
return {};
|
|
102
|
+
}
|
|
86
103
|
const keyValuePairs = requestClass.split(";");
|
|
87
104
|
const parsed = {};
|
|
88
105
|
for (const pair of keyValuePairs) {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __markAsModule = (target) => __defProp(target, "__esModule", {value: true});
|
|
3
|
+
var __export = (target, all) => {
|
|
4
|
+
for (var name in all)
|
|
5
|
+
__defProp(target, name, {get: all[name], enumerable: true});
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
// packages/@lwrjs/core/src/middleware/utils/compression.ts
|
|
9
|
+
__markAsModule(exports);
|
|
10
|
+
__export(exports, {
|
|
11
|
+
getMrtCompressionThreshold: () => getMrtCompressionThreshold
|
|
12
|
+
});
|
|
13
|
+
var MAX_LAMBDA_RESPONSE_SIZE = 6 * 1024 * 1024;
|
|
14
|
+
function getMrtCompressionThreshold() {
|
|
15
|
+
return MAX_LAMBDA_RESPONSE_SIZE - 5 * 1024 * 1024;
|
|
16
|
+
}
|
|
@@ -35,7 +35,7 @@ function createReturnStatus(error, url) {
|
|
|
35
35
|
if (error instanceof import_diagnostics.LwrUnresolvableError) {
|
|
36
36
|
return {status: 404, message: error.message};
|
|
37
37
|
}
|
|
38
|
-
return {status: 500, message: import_diagnostics.descriptions.SERVER.SERVER_ERROR(url).message};
|
|
38
|
+
return {status: 500, message: `${import_diagnostics.descriptions.SERVER.SERVER_ERROR(url).message}: ${error.message}`};
|
|
39
39
|
}
|
|
40
40
|
function handleErrors(middleware) {
|
|
41
41
|
return async (req, res, next) => {
|
|
@@ -26,16 +26,22 @@ __markAsModule(exports);
|
|
|
26
26
|
__export(exports, {
|
|
27
27
|
viewMiddleware: () => viewMiddleware
|
|
28
28
|
});
|
|
29
|
+
var import_util = __toModule(require("util"));
|
|
30
|
+
var import_url = __toModule(require("url"));
|
|
29
31
|
var import_diagnostics = __toModule(require("@lwrjs/diagnostics"));
|
|
30
32
|
var import_router = __toModule(require("@lwrjs/router"));
|
|
31
33
|
var import_shared_utils = __toModule(require("@lwrjs/shared-utils"));
|
|
32
34
|
var import_instrumentation = __toModule(require("@lwrjs/instrumentation"));
|
|
33
35
|
var import_error_handling = __toModule(require("./utils/error-handling.cjs"));
|
|
34
36
|
var import_view_registry = __toModule(require("@lwrjs/view-registry"));
|
|
37
|
+
var import_instrumentation2 = __toModule(require("@lwrjs/instrumentation"));
|
|
38
|
+
var import_zlib = __toModule(require("zlib"));
|
|
39
|
+
var import_compression = __toModule(require("./utils/compression.cjs"));
|
|
35
40
|
function createViewMiddleware(route, errorRoutes, context, viewHandler) {
|
|
36
41
|
const errorRoute = errorRoutes.find((route2) => route2.status === 500);
|
|
37
42
|
const appConfig = context.appConfig;
|
|
38
43
|
const {environment: environmentConfig} = appConfig;
|
|
44
|
+
const mrtCompressionThreshold = (0, import_compression.getMrtCompressionThreshold)();
|
|
39
45
|
return async (req, res) => {
|
|
40
46
|
if (!req.validateEnvironmentRequest(appConfig)) {
|
|
41
47
|
res.status(400);
|
|
@@ -61,7 +67,10 @@ function createViewMiddleware(route, errorRoutes, context, viewHandler) {
|
|
|
61
67
|
...defaultRuntimeParams,
|
|
62
68
|
url: viewRequest.url,
|
|
63
69
|
params: viewRequest.params,
|
|
64
|
-
query: viewRequest.query
|
|
70
|
+
query: viewRequest.query,
|
|
71
|
+
cookie: req.headers?.cookie,
|
|
72
|
+
trueClientIP: req.headers && req.headers["True-Client-IP"],
|
|
73
|
+
coreProxy: req.getCoreProxy(appConfig.coreProxy ?? void 0, route.bootstrap?.proxyForSSR)
|
|
65
74
|
};
|
|
66
75
|
const resolve = req.isJsonRequest() ? viewHandler.getViewJson : viewHandler.getViewContent;
|
|
67
76
|
let viewResponse;
|
|
@@ -77,9 +86,13 @@ function createViewMiddleware(route, errorRoutes, context, viewHandler) {
|
|
|
77
86
|
basePath: runtimeParams.basePath,
|
|
78
87
|
locale: runtimeParams.locale
|
|
79
88
|
}
|
|
80
|
-
}, (span) => {
|
|
89
|
+
}, async (span) => {
|
|
81
90
|
traceId = span.traceId;
|
|
82
|
-
|
|
91
|
+
const res2 = await resolve.call(viewHandler, viewRequest, route, runtimeEnvironment, runtimeParams);
|
|
92
|
+
span.setAttributes({
|
|
93
|
+
size: byteSize(res2.body)
|
|
94
|
+
});
|
|
95
|
+
return res2;
|
|
83
96
|
});
|
|
84
97
|
resolvedRoute = route;
|
|
85
98
|
} catch (err) {
|
|
@@ -97,12 +110,23 @@ function createViewMiddleware(route, errorRoutes, context, viewHandler) {
|
|
|
97
110
|
return resolve.call(viewHandler, viewRequest, errorRoute, runtimeEnvironment, runtimeParams);
|
|
98
111
|
});
|
|
99
112
|
resolvedRoute = errorRoute;
|
|
113
|
+
} finally {
|
|
114
|
+
if (traceId?.length) {
|
|
115
|
+
res.setHeader("x-trace-id", traceId);
|
|
116
|
+
res.set({
|
|
117
|
+
"Server-Timing": (0, import_instrumentation2.getTraceCollector)().getSpansInTrace(traceId)
|
|
118
|
+
});
|
|
119
|
+
(0, import_instrumentation2.getTraceCollector)().dropTrace(traceId);
|
|
120
|
+
}
|
|
100
121
|
}
|
|
101
122
|
if (req.isSiteGeneration()) {
|
|
102
123
|
res.setSiteGenerationMetadata(viewResponse.metadata);
|
|
103
124
|
}
|
|
104
125
|
res.type("text/html");
|
|
105
126
|
if (viewResponse.headers) {
|
|
127
|
+
if ((0, import_shared_utils.isLocalDev)()) {
|
|
128
|
+
delete viewResponse.headers["content-security-policy"];
|
|
129
|
+
}
|
|
106
130
|
res.set(viewResponse.headers);
|
|
107
131
|
}
|
|
108
132
|
if (!res.hasHeader("cache-control")) {
|
|
@@ -111,12 +135,48 @@ function createViewMiddleware(route, errorRoutes, context, viewHandler) {
|
|
|
111
135
|
res.setHeader("cache-control", `public, max-age=${cacheTtl}`);
|
|
112
136
|
}
|
|
113
137
|
}
|
|
114
|
-
|
|
115
|
-
|
|
138
|
+
let status = resolvedRoute.status || viewResponse.status || 200;
|
|
139
|
+
const viewDefinitionStatus = viewResponse.metadata?.viewDefinition?.status;
|
|
140
|
+
if (viewResponse.status === 301 || viewResponse.status === 302) {
|
|
141
|
+
status = viewResponse.status;
|
|
142
|
+
} else if (viewDefinitionStatus && viewDefinitionStatus.code) {
|
|
143
|
+
const origStatus = status;
|
|
144
|
+
const {code, location} = viewDefinitionStatus;
|
|
145
|
+
const isRedirect = code === 301 || code == 302;
|
|
146
|
+
status = code;
|
|
147
|
+
if (isRedirect && location && (0, import_shared_utils.isURL)(location) || location?.startsWith("/")) {
|
|
148
|
+
res.set({
|
|
149
|
+
location: addRedirectQueryParam(location, (0, import_shared_utils.parseRequestDepth)(req.headers, req.query))
|
|
150
|
+
});
|
|
151
|
+
} else {
|
|
152
|
+
status = origStatus;
|
|
153
|
+
import_diagnostics.logger.warn(`[view-middleware] Ignoring invalid location header: "${location}"`);
|
|
154
|
+
}
|
|
116
155
|
}
|
|
117
|
-
const status = resolvedRoute.status || viewResponse.status || 200;
|
|
118
156
|
res.status(status);
|
|
119
|
-
|
|
157
|
+
const viewResponseBody = viewResponse.body;
|
|
158
|
+
let shouldCompress, viewResponseSize;
|
|
159
|
+
if ((0, import_shared_utils.isLambdaEnv)() && typeof viewResponseBody === "string") {
|
|
160
|
+
viewResponseSize = Buffer.byteLength(viewResponseBody, "utf-8");
|
|
161
|
+
shouldCompress = Buffer.byteLength(viewResponseBody, "utf-8") >= mrtCompressionThreshold;
|
|
162
|
+
}
|
|
163
|
+
if (shouldCompress) {
|
|
164
|
+
const start = performance.now();
|
|
165
|
+
let compressionMethod;
|
|
166
|
+
if (req.headers["accept-encoding"]?.includes("gzip")) {
|
|
167
|
+
compressionMethod = import_zlib.gzipSync;
|
|
168
|
+
res.setHeader("Content-Encoding", "gzip");
|
|
169
|
+
} else {
|
|
170
|
+
res.setHeader("Content-Encoding", "br");
|
|
171
|
+
compressionMethod = import_zlib.brotliCompressSync;
|
|
172
|
+
}
|
|
173
|
+
const compressedView = compressionMethod(viewResponseBody);
|
|
174
|
+
const end = performance.now();
|
|
175
|
+
import_diagnostics.logger.warn(`View size (${viewResponseSize}) compressed due to Lambda response limit. Compression took ${Math.round(end - start)} ms`);
|
|
176
|
+
res.send(compressedView);
|
|
177
|
+
} else {
|
|
178
|
+
res.send(viewResponse.body);
|
|
179
|
+
}
|
|
120
180
|
};
|
|
121
181
|
}
|
|
122
182
|
function createConfigMiddleware(routes, context, viewHandler) {
|
|
@@ -133,7 +193,7 @@ function createConfigMiddleware(routes, context, viewHandler) {
|
|
|
133
193
|
const query = {};
|
|
134
194
|
if (url.indexOf("?") !== -1) {
|
|
135
195
|
requestPath = url.substring(0, url.indexOf("?"));
|
|
136
|
-
const searchParams = new URLSearchParams(url.substring(url.indexOf("?")));
|
|
196
|
+
const searchParams = new import_url.URLSearchParams(url.substring(url.indexOf("?")));
|
|
137
197
|
for (const [key, value] of searchParams.entries()) {
|
|
138
198
|
query[key] = value;
|
|
139
199
|
}
|
|
@@ -165,7 +225,6 @@ function createConfigMiddleware(routes, context, viewHandler) {
|
|
|
165
225
|
res.setHeader("cache-control", `public, max-age=${cacheTtl}`);
|
|
166
226
|
}
|
|
167
227
|
}
|
|
168
|
-
res.status(200);
|
|
169
228
|
res.type("application/javascript");
|
|
170
229
|
res.status(viewResponse.status || 200);
|
|
171
230
|
res.send(viewResponse.body);
|
|
@@ -199,7 +258,7 @@ function viewMiddleware(app, context) {
|
|
|
199
258
|
const localizedPath = `/:locale(${supportedStr})${routePath}`;
|
|
200
259
|
paths.push(localizedPath);
|
|
201
260
|
});
|
|
202
|
-
addDefaultLocaleRedirects(i18n.defaultLocale, defaultLocalePaths, app);
|
|
261
|
+
addDefaultLocaleRedirects(i18n.defaultLocale, defaultLocalePaths, i18n.defaultRedirectParams, app);
|
|
203
262
|
}
|
|
204
263
|
}
|
|
205
264
|
import_diagnostics.logger.debug({label: `view-middleware`, message: `Add view paths ${paths}`});
|
|
@@ -208,14 +267,47 @@ function viewMiddleware(app, context) {
|
|
|
208
267
|
app.get((0, import_shared_utils.getClientBootstrapConfigurationRoutes)(), (0, import_error_handling.handleErrors)(createConfigMiddleware(routes, context, viewHandler)));
|
|
209
268
|
app.get("/" + app.getRegexWildcard(), (0, import_error_handling.handleErrors)(createNotFoundMiddleware(errorRoutes, context, viewHandler)));
|
|
210
269
|
}
|
|
211
|
-
function addDefaultLocaleRedirects(defaultLocale, defaultLocalePaths, app) {
|
|
270
|
+
function addDefaultLocaleRedirects(defaultLocale, defaultLocalePaths, defaultRedirectParams, app) {
|
|
212
271
|
import_diagnostics.logger.debug({label: `view-middleware`, message: `Add default localized paths ${defaultLocalePaths}`});
|
|
213
|
-
app.get(defaultLocalePaths, (req, res) => {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
272
|
+
app.get(defaultLocalePaths, (req, res, next) => {
|
|
273
|
+
if (!req.originalUrl?.startsWith(`/${defaultLocale}`)) {
|
|
274
|
+
import_diagnostics.logger.warn({
|
|
275
|
+
label: "view-middleware",
|
|
276
|
+
message: `Attempted to redirect to a URL that did not start with the default locale: /${defaultLocale} ${req.originalUrl}`
|
|
277
|
+
});
|
|
278
|
+
return next();
|
|
279
|
+
}
|
|
280
|
+
const [originalPath, queryString] = req.originalUrl.split("?");
|
|
281
|
+
let modifiedPath = originalPath.replace(`/${defaultLocale}`, "");
|
|
282
|
+
if (req.basePath) {
|
|
283
|
+
modifiedPath = `${req.basePath}${modifiedPath}`;
|
|
217
284
|
}
|
|
285
|
+
const queryParams = new import_url.URLSearchParams(queryString);
|
|
286
|
+
if (defaultRedirectParams) {
|
|
287
|
+
Object.entries(defaultRedirectParams).forEach(([key, value]) => {
|
|
288
|
+
if (queryParams.has(key) && queryParams.get(key) !== value) {
|
|
289
|
+
queryParams.set(key, value);
|
|
290
|
+
} else if (!queryParams.has(key)) {
|
|
291
|
+
queryParams.set(key, value);
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
modifiedPath = queryParams.toString() ? `${modifiedPath}?${queryParams.toString()}` : modifiedPath;
|
|
218
296
|
res.setHeader("Location", modifiedPath);
|
|
219
297
|
return res.sendStatus(301);
|
|
220
298
|
});
|
|
221
299
|
}
|
|
300
|
+
function addRedirectQueryParam(redirectUrl, depth) {
|
|
301
|
+
const fakeOrigin = "http://parse.com";
|
|
302
|
+
const url = (0, import_shared_utils.isURL)(redirectUrl) ? new URL(redirectUrl) : new URL(`${fakeOrigin}${redirectUrl}`);
|
|
303
|
+
url.searchParams.set(import_shared_utils.REQUEST_DEPTH_KEY, String(depth + 1));
|
|
304
|
+
return url.toString().replace(fakeOrigin, "");
|
|
305
|
+
}
|
|
306
|
+
function byteSize(body) {
|
|
307
|
+
if (Buffer.isBuffer(body)) {
|
|
308
|
+
return body.length;
|
|
309
|
+
} else if (typeof body === "string") {
|
|
310
|
+
return new import_util.TextEncoder().encode(body).length;
|
|
311
|
+
}
|
|
312
|
+
return -1;
|
|
313
|
+
}
|
|
@@ -143,6 +143,7 @@ var SiteGenerator = class {
|
|
|
143
143
|
let context;
|
|
144
144
|
context = await dispatcher.dispatchUrl(url, "GET", siteConfig.locale);
|
|
145
145
|
if (context?.fs?.headers?.Location) {
|
|
146
|
+
this.saveServerBundles(siteConfig, context.fs.metadata?.viewDefinition?.viewRecord.serverBundles);
|
|
146
147
|
const redirectUrl = context?.fs?.headers?.Location;
|
|
147
148
|
url = redirectUrl;
|
|
148
149
|
const redirectContext = await dispatcher.dispatchUrl(url, "GET", siteConfig.locale);
|
|
@@ -204,7 +205,7 @@ var SiteGenerator = class {
|
|
|
204
205
|
}
|
|
205
206
|
}
|
|
206
207
|
if (moduleDefinition.bundleRecord) {
|
|
207
|
-
this.addBundleToSiteMetadata(moduleDefinition, url, siteConfig);
|
|
208
|
+
this.addBundleToSiteMetadata(moduleDefinition, url, false, siteConfig);
|
|
208
209
|
}
|
|
209
210
|
}
|
|
210
211
|
const uris = context.fs?.metadata?.resolvedUris || [];
|
|
@@ -229,15 +230,15 @@ var SiteGenerator = class {
|
|
|
229
230
|
siteBundles[specifier] = bundleMetadata;
|
|
230
231
|
}
|
|
231
232
|
}
|
|
232
|
-
addBundleToSiteMetadata(bundleDefinition, url, siteConfig) {
|
|
233
|
+
addBundleToSiteMetadata(bundleDefinition, url, ssr, siteConfig) {
|
|
233
234
|
if (siteConfig.siteMetadata) {
|
|
234
235
|
const locale = siteConfig.locale;
|
|
235
|
-
const specifier = (0, import_site_metadata.getSiteBundleId)(bundleDefinition, locale, siteConfig.i18n);
|
|
236
|
-
const imports = bundleDefinition.bundleRecord.imports?.map((moduleRef) => (0, import_site_metadata.getSiteBundleId)(moduleRef, locale, siteConfig.i18n)) || [];
|
|
237
|
-
const dynamicImports = bundleDefinition.bundleRecord.dynamicImports?.map((moduleRef) => (0, import_site_metadata.getSiteBundleId)(moduleRef, locale, siteConfig.i18n));
|
|
236
|
+
const specifier = (0, import_site_metadata.getSiteBundleId)(bundleDefinition, locale, ssr, siteConfig.i18n);
|
|
237
|
+
const imports = bundleDefinition.bundleRecord.imports?.map((moduleRef) => (0, import_site_metadata.getSiteBundleId)(moduleRef, locale, false, siteConfig.i18n)) || [];
|
|
238
|
+
const dynamicImports = bundleDefinition.bundleRecord.dynamicImports?.map((moduleRef) => (0, import_site_metadata.getSiteBundleId)(moduleRef, locale, false, siteConfig.i18n));
|
|
238
239
|
const includedModules = bundleDefinition.bundleRecord.includedModules?.map((moduleRef) => {
|
|
239
240
|
const moduleId = (0, import_shared_utils.explodeSpecifier)(moduleRef);
|
|
240
|
-
return (0, import_site_metadata.getSiteBundleId)(moduleId, locale, siteConfig.i18n);
|
|
241
|
+
return (0, import_site_metadata.getSiteBundleId)(moduleId, locale, false, siteConfig.i18n);
|
|
241
242
|
}) || [];
|
|
242
243
|
const version = bundleDefinition.version === import_shared_utils.VERSION_NOT_PROVIDED ? void 0 : bundleDefinition.version;
|
|
243
244
|
const bundleMetadata = {
|
|
@@ -363,6 +364,7 @@ var SiteGenerator = class {
|
|
|
363
364
|
dispatchRequests.push(this.dispatchJSResourceRecursive(jsUri, dispatcher, siteConfig));
|
|
364
365
|
}
|
|
365
366
|
}
|
|
367
|
+
this.saveServerBundles(siteConfig, viewDefinition.viewRecord.serverBundles);
|
|
366
368
|
const bootstrapResources = viewDefinition.viewRecord.bootstrapModule?.resources || [];
|
|
367
369
|
for (const resource of bootstrapResources) {
|
|
368
370
|
if (!resource.inline) {
|
|
@@ -549,7 +551,8 @@ var SiteGenerator = class {
|
|
|
549
551
|
const endpoints = {
|
|
550
552
|
uris: {
|
|
551
553
|
legacyDefault: (0, import_shared_utils.getModuleUriPrefix)(runtimeEnvironment, {locale}),
|
|
552
|
-
mapping: (0, import_shared_utils.getMappingUriPrefix)(runtimeEnvironment, {locale})
|
|
554
|
+
mapping: (0, import_shared_utils.getMappingUriPrefix)(runtimeEnvironment, {locale}),
|
|
555
|
+
server: (0, import_shared_utils.getModuleUriPrefix)({...runtimeEnvironment, bundle: true}, {locale}).replace("/bundle/", "/bundle-server/")
|
|
553
556
|
}
|
|
554
557
|
};
|
|
555
558
|
return {
|
|
@@ -615,6 +618,22 @@ ${mergeIndex}
|
|
|
615
618
|
this.addAdditionalImportMetadataToViewConfig(siteConfig);
|
|
616
619
|
await siteConfig.siteMetadata?.persistSiteMetadata();
|
|
617
620
|
}
|
|
621
|
+
async saveServerBundles(siteConfig, bundles) {
|
|
622
|
+
if (bundles?.size) {
|
|
623
|
+
const {endpoints, outputDir} = siteConfig;
|
|
624
|
+
bundles.forEach(async (bundle) => {
|
|
625
|
+
const {specifier, version} = bundle;
|
|
626
|
+
const vSpecifier = (0, import_shared_utils.getSpecifier)({
|
|
627
|
+
specifier,
|
|
628
|
+
version: version ? (0, import_shared_utils.normalizeVersionToUri)(version) : void 0
|
|
629
|
+
});
|
|
630
|
+
const url = `${endpoints?.uris.server}${vSpecifier}/s/${(0, import_shared_utils.signBundle)(bundle)}/bundle_${(0, import_shared_utils.prettyModuleUriSuffix)(specifier)}.js`;
|
|
631
|
+
this.addBundleToSiteMetadata(bundle, url, true, siteConfig);
|
|
632
|
+
(0, import_dir.createResourceDir)((0, import_path.dirname)(url), outputDir);
|
|
633
|
+
await (0, import_stream.writeResponse)({fs: {body: await bundle.getCode()}}, (0, import_path.join)(outputDir, url));
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
}
|
|
618
637
|
};
|
|
619
638
|
var static_generation_default = SiteGenerator;
|
|
620
639
|
var ViewImportMetadataImpl = class {
|
|
@@ -38,6 +38,8 @@ async function writeStream(readStream, fullPath) {
|
|
|
38
38
|
});
|
|
39
39
|
}
|
|
40
40
|
async function writeResponse(context, fullPath) {
|
|
41
|
+
if (import_fs.default.existsSync(fullPath))
|
|
42
|
+
return;
|
|
41
43
|
if (context.fs?.stream) {
|
|
42
44
|
const stream = context.fs?.stream;
|
|
43
45
|
await writeStream(stream, fullPath);
|
package/build/es/index.js
CHANGED
|
@@ -98,9 +98,9 @@ export class LwrApp {
|
|
|
98
98
|
this.config = appConfig;
|
|
99
99
|
this.runtimeEnvironment = runtimeEnvironment;
|
|
100
100
|
this.globalData = globalData;
|
|
101
|
-
const { basePath, serverType } = this.config;
|
|
101
|
+
const { basePath, serverType, caseSensitiveRoutes } = this.config;
|
|
102
102
|
this.serverType = serverType;
|
|
103
|
-
this.app = createInternalServer(serverType, { basePath });
|
|
103
|
+
this.app = createInternalServer(serverType, { basePath, caseSensitiveRoutes });
|
|
104
104
|
this.server = this.app.createHttpServer();
|
|
105
105
|
this.use = this.app.use.bind(this.app);
|
|
106
106
|
this.all = this.app.all.bind(this.app);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getFeatureFlags } from '@lwrjs/shared-utils';
|
|
1
2
|
import path from 'path';
|
|
2
3
|
import { DiagnosticsError } from '@lwrjs/diagnostics';
|
|
3
4
|
import { RequestHandlerSpan, getTracer } from '@lwrjs/instrumentation';
|
|
@@ -63,6 +64,12 @@ function createAssetMiddleware(context) {
|
|
|
63
64
|
}
|
|
64
65
|
else if (runtimeEnvironment.immutableAssets) {
|
|
65
66
|
res.setHeader('Cache-control', 'public, max-age=60');
|
|
67
|
+
const extraAssetHeaders = parseHeaderStringToObject(getFeatureFlags().EXPERIMENTAL_ASSET_HEADERS);
|
|
68
|
+
if (extraAssetHeaders) {
|
|
69
|
+
for (const [key, value] of Object.entries(extraAssetHeaders)) {
|
|
70
|
+
res.setHeader(key, value);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
66
73
|
}
|
|
67
74
|
res.status(200).stream(asset.stream());
|
|
68
75
|
}
|
|
@@ -105,4 +112,14 @@ function sendRedirect(res, assetUri) {
|
|
|
105
112
|
});
|
|
106
113
|
res.sendStatus(302);
|
|
107
114
|
}
|
|
115
|
+
function parseHeaderStringToObject(assetHeadersString) {
|
|
116
|
+
if (typeof assetHeadersString === 'string') {
|
|
117
|
+
// convert to cache sring header:value to object
|
|
118
|
+
return Object.fromEntries(assetHeadersString
|
|
119
|
+
.split(';')
|
|
120
|
+
.filter(Boolean)
|
|
121
|
+
.map((e) => e.split(':').map((s) => s.trim()))
|
|
122
|
+
.filter((pair) => pair.length === 2 && pair[0]));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
108
125
|
//# sourceMappingURL=asset-middleware.js.map
|
|
@@ -56,7 +56,9 @@ function createBundleMiddleware(context) {
|
|
|
56
56
|
if (signature !== LATEST_SIGNATURE) {
|
|
57
57
|
res.setHeader('Cache-control', 'public, max-age=31536000, immutable');
|
|
58
58
|
}
|
|
59
|
-
res.status(200)
|
|
59
|
+
res.status(200)
|
|
60
|
+
.type('application/javascript')
|
|
61
|
+
.send(await bundleDefinition.getCode());
|
|
60
62
|
};
|
|
61
63
|
}
|
|
62
64
|
function createSourceMapMiddleware(context) {
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
*/
|
|
9
9
|
import { logger } from '@lwrjs/diagnostics';
|
|
10
|
-
import { REQUEST_DEPTH_HEADER, isLambdaEnv,
|
|
10
|
+
import { REQUEST_DEPTH_HEADER, isLambdaEnv, parseRequestDepth } from '@lwrjs/shared-utils';
|
|
11
11
|
const MRT_REQUEST_CLASS = 'X-Mobify-Request-Class';
|
|
12
12
|
const MRT_REQUEST_CLASS_KEY = MRT_REQUEST_CLASS.toLowerCase();
|
|
13
13
|
export function requestProcessorMiddleware(app, context) {
|
|
@@ -27,7 +27,7 @@ export function requestProcessorMiddleware(app, context) {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
requestClass = req.headers[MRT_REQUEST_CLASS_KEY];
|
|
30
|
-
requestDepth =
|
|
30
|
+
requestDepth = parseRequestDepth(req.headers, req.query);
|
|
31
31
|
const forwarded = req.headers['forwarded'];
|
|
32
32
|
const host = req.headers['host'];
|
|
33
33
|
const forwardedProto = req.headers['x-forwarded-proto'];
|
|
@@ -40,8 +40,7 @@ export function requestProcessorMiddleware(app, context) {
|
|
|
40
40
|
});
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
-
|
|
44
|
-
if (requestDepth && requestDepth > 0) {
|
|
43
|
+
if (requestDepth && requestDepth > 1) {
|
|
45
44
|
logger.error({
|
|
46
45
|
label: 'request-processor-middleware',
|
|
47
46
|
message: `Lambda SSR request cycle detected: ${req.originalUrl}`,
|
|
@@ -49,7 +48,7 @@ export function requestProcessorMiddleware(app, context) {
|
|
|
49
48
|
// Return 400 Too Many Requests status
|
|
50
49
|
return res.status(400).send('Request depth limit reached');
|
|
51
50
|
}
|
|
52
|
-
if (req.headers && typeof requestClass === 'string') {
|
|
51
|
+
if (req.headers && requestClass && typeof requestClass === 'string') {
|
|
53
52
|
const parsedRequestClass = parseRequestClass(requestClass);
|
|
54
53
|
logger.debug({
|
|
55
54
|
label: `request-processor-middleware`,
|
|
@@ -58,6 +57,26 @@ export function requestProcessorMiddleware(app, context) {
|
|
|
58
57
|
const pathValue = parsedRequestClass?.basePath || basePath || '';
|
|
59
58
|
// If base path is '' or starts with / leave it alone
|
|
60
59
|
req.basePath = pathValue === '' || pathValue.indexOf('/') === 0 ? pathValue : `/${pathValue}`;
|
|
60
|
+
const expressRequest = req.req;
|
|
61
|
+
// This section is code added for the 103 hints support. If CDN passes us a basePath in the header
|
|
62
|
+
// If the basePath is at the start of the URL we need to remove it so we can match the expected route.
|
|
63
|
+
if (expressRequest?.url && parsedRequestClass?.basePath) {
|
|
64
|
+
// Separate the path and the query param using dummy local host here since we do not use it
|
|
65
|
+
const { pathname, search } = new URL(expressRequest.url, 'http://localhost');
|
|
66
|
+
if (pathname.startsWith(parsedRequestClass.basePath)) {
|
|
67
|
+
// Remove the basePath from the pathname
|
|
68
|
+
const newPath = pathname.slice(parsedRequestClass.basePath.length) || '/';
|
|
69
|
+
// Reconstruct the URL with the modified path and original query string
|
|
70
|
+
expressRequest.url = newPath + search;
|
|
71
|
+
expressRequest.originalUrl = newPath + search;
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
logger.warn({
|
|
75
|
+
label: `request-processor-middleware`,
|
|
76
|
+
message: `The URL requested for doesn't start with the Base path`,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
61
80
|
}
|
|
62
81
|
else {
|
|
63
82
|
logger.debug({
|
|
@@ -71,6 +90,10 @@ export function requestProcessorMiddleware(app, context) {
|
|
|
71
90
|
});
|
|
72
91
|
}
|
|
73
92
|
function parseRequestClass(requestClass) {
|
|
93
|
+
// If there is no requestClass do not bother parsing
|
|
94
|
+
if (!requestClass) {
|
|
95
|
+
return {};
|
|
96
|
+
}
|
|
74
97
|
// Split the Forwarded header into individual key-value pairs
|
|
75
98
|
const keyValuePairs = requestClass.split(';');
|
|
76
99
|
// Create an object to store the parsed values
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// currently documented AWS Lambda limit, 6MB
|
|
2
|
+
const MAX_LAMBDA_RESPONSE_SIZE = 6 * 1024 * 1024;
|
|
3
|
+
export function getMrtCompressionThreshold() {
|
|
4
|
+
return MAX_LAMBDA_RESPONSE_SIZE - 5 * 1024 * 1024; // Trigger compression within 1MB of limit
|
|
5
|
+
}
|
|
6
|
+
//# sourceMappingURL=compression.js.map
|
|
@@ -8,7 +8,7 @@ function createReturnStatus(error, url) {
|
|
|
8
8
|
if (error instanceof LwrUnresolvableError) {
|
|
9
9
|
return { status: 404, message: error.message };
|
|
10
10
|
}
|
|
11
|
-
return { status: 500, message: descriptions.SERVER.SERVER_ERROR(url).message };
|
|
11
|
+
return { status: 500, message: `${descriptions.SERVER.SERVER_ERROR(url).message}: ${error.message}` };
|
|
12
12
|
}
|
|
13
13
|
export function handleErrors(middleware) {
|
|
14
14
|
return async (req, res, next) => {
|
|
@@ -1,13 +1,19 @@
|
|
|
1
|
+
import { TextEncoder } from 'util';
|
|
2
|
+
import { URLSearchParams } from 'url';
|
|
1
3
|
import { descriptions, logger } from '@lwrjs/diagnostics';
|
|
2
4
|
import { getClientRoutes } from '@lwrjs/router';
|
|
3
|
-
import { decodeViewPath, extractRequestParams, getClientBootstrapConfigurationRoutes, shortestTtl, } from '@lwrjs/shared-utils';
|
|
5
|
+
import { decodeViewPath, extractRequestParams, getClientBootstrapConfigurationRoutes, isURL, parseRequestDepth, REQUEST_DEPTH_KEY, isLocalDev, shortestTtl, isLambdaEnv, } from '@lwrjs/shared-utils';
|
|
4
6
|
import { RequestHandlerSpan, getTracer } from '@lwrjs/instrumentation';
|
|
5
7
|
import { handleErrors } from './utils/error-handling.js';
|
|
6
8
|
import { LwrViewHandler } from '@lwrjs/view-registry';
|
|
9
|
+
import { getTraceCollector } from '@lwrjs/instrumentation';
|
|
10
|
+
import { brotliCompressSync, gzipSync } from 'zlib';
|
|
11
|
+
import { getMrtCompressionThreshold } from './utils/compression.js';
|
|
7
12
|
function createViewMiddleware(route, errorRoutes, context, viewHandler) {
|
|
8
13
|
const errorRoute = errorRoutes.find((route) => route.status === 500);
|
|
9
14
|
const appConfig = context.appConfig;
|
|
10
15
|
const { environment: environmentConfig } = appConfig;
|
|
16
|
+
const mrtCompressionThreshold = getMrtCompressionThreshold();
|
|
11
17
|
return async (req, res) => {
|
|
12
18
|
if (!req.validateEnvironmentRequest(appConfig)) {
|
|
13
19
|
res.status(400);
|
|
@@ -36,6 +42,9 @@ function createViewMiddleware(route, errorRoutes, context, viewHandler) {
|
|
|
36
42
|
url: viewRequest.url,
|
|
37
43
|
params: viewRequest.params,
|
|
38
44
|
query: viewRequest.query,
|
|
45
|
+
cookie: req.headers?.cookie,
|
|
46
|
+
trueClientIP: req.headers && req.headers['True-Client-IP'],
|
|
47
|
+
coreProxy: req.getCoreProxy(appConfig.coreProxy ?? undefined, route.bootstrap?.proxyForSSR),
|
|
39
48
|
};
|
|
40
49
|
const resolve = req.isJsonRequest() ? viewHandler.getViewJson : viewHandler.getViewContent;
|
|
41
50
|
let viewResponse;
|
|
@@ -51,9 +60,14 @@ function createViewMiddleware(route, errorRoutes, context, viewHandler) {
|
|
|
51
60
|
basePath: runtimeParams.basePath,
|
|
52
61
|
locale: runtimeParams.locale,
|
|
53
62
|
},
|
|
54
|
-
}, (span) => {
|
|
63
|
+
}, async (span) => {
|
|
55
64
|
traceId = span.traceId;
|
|
56
|
-
|
|
65
|
+
const res = await resolve.call(viewHandler, viewRequest, route, runtimeEnvironment, runtimeParams);
|
|
66
|
+
// Add the view size metric
|
|
67
|
+
span.setAttributes({
|
|
68
|
+
size: byteSize(res.body),
|
|
69
|
+
});
|
|
70
|
+
return res;
|
|
57
71
|
});
|
|
58
72
|
resolvedRoute = route;
|
|
59
73
|
}
|
|
@@ -74,6 +88,15 @@ function createViewMiddleware(route, errorRoutes, context, viewHandler) {
|
|
|
74
88
|
});
|
|
75
89
|
resolvedRoute = errorRoute;
|
|
76
90
|
}
|
|
91
|
+
finally {
|
|
92
|
+
if (traceId?.length) {
|
|
93
|
+
res.setHeader('x-trace-id', traceId);
|
|
94
|
+
res.set({
|
|
95
|
+
'Server-Timing': getTraceCollector().getSpansInTrace(traceId),
|
|
96
|
+
});
|
|
97
|
+
getTraceCollector().dropTrace(traceId);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
77
100
|
if (req.isSiteGeneration()) {
|
|
78
101
|
res.setSiteGenerationMetadata(viewResponse.metadata);
|
|
79
102
|
}
|
|
@@ -81,6 +104,11 @@ function createViewMiddleware(route, errorRoutes, context, viewHandler) {
|
|
|
81
104
|
res.type('text/html');
|
|
82
105
|
// the default content type can be overridden if specified by the view response
|
|
83
106
|
if (viewResponse.headers) {
|
|
107
|
+
if (isLocalDev()) {
|
|
108
|
+
// Disable CSP for local dev
|
|
109
|
+
// TODO we should find a cleaner way to accomplish this but has to be here for now
|
|
110
|
+
delete viewResponse.headers['content-security-policy'];
|
|
111
|
+
}
|
|
84
112
|
res.set(viewResponse.headers);
|
|
85
113
|
}
|
|
86
114
|
// pick the shortest TTL between the view response and route object
|
|
@@ -90,12 +118,58 @@ function createViewMiddleware(route, errorRoutes, context, viewHandler) {
|
|
|
90
118
|
res.setHeader('cache-control', `public, max-age=${cacheTtl}`);
|
|
91
119
|
}
|
|
92
120
|
}
|
|
93
|
-
|
|
94
|
-
|
|
121
|
+
let status = resolvedRoute.status || viewResponse.status || 200;
|
|
122
|
+
const viewDefinitionStatus = viewResponse.metadata?.viewDefinition?.status;
|
|
123
|
+
if (viewResponse.status === 301 || viewResponse.status === 302) {
|
|
124
|
+
// route handle redirect status takes precedence
|
|
125
|
+
status = viewResponse.status;
|
|
126
|
+
}
|
|
127
|
+
else if (viewDefinitionStatus && viewDefinitionStatus.code) {
|
|
128
|
+
const origStatus = status;
|
|
129
|
+
const { code, location } = viewDefinitionStatus;
|
|
130
|
+
const isRedirect = code === 301 || code == 302;
|
|
131
|
+
status = code;
|
|
132
|
+
if ((isRedirect && location && isURL(location)) || location?.startsWith('/')) {
|
|
133
|
+
res.set({
|
|
134
|
+
location: addRedirectQueryParam(location, parseRequestDepth(req.headers, req.query)),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
// reset the status in the event of an ivalid location when redirecting
|
|
139
|
+
status = origStatus;
|
|
140
|
+
logger.warn(`[view-middleware] Ignoring invalid location header: "${location}"`);
|
|
141
|
+
}
|
|
95
142
|
}
|
|
96
|
-
const status = resolvedRoute.status || viewResponse.status || 200;
|
|
97
143
|
res.status(status);
|
|
98
|
-
|
|
144
|
+
// LWR@MRT 254 temporary safeguard for "dupe styles" issue causing huge base docs
|
|
145
|
+
// Re-evaluate for removal once we determine it is no longer needed: W-17201070
|
|
146
|
+
const viewResponseBody = viewResponse.body;
|
|
147
|
+
let shouldCompress, viewResponseSize;
|
|
148
|
+
if (isLambdaEnv() && typeof viewResponseBody === 'string') {
|
|
149
|
+
viewResponseSize = Buffer.byteLength(viewResponseBody, 'utf-8');
|
|
150
|
+
shouldCompress = Buffer.byteLength(viewResponseBody, 'utf-8') >= mrtCompressionThreshold;
|
|
151
|
+
}
|
|
152
|
+
// Conditionally apply compression if size is near the threshold
|
|
153
|
+
if (shouldCompress) {
|
|
154
|
+
const start = performance.now();
|
|
155
|
+
let compressionMethod;
|
|
156
|
+
if (req.headers['accept-encoding']?.includes('gzip')) {
|
|
157
|
+
compressionMethod = gzipSync;
|
|
158
|
+
res.setHeader('Content-Encoding', 'gzip');
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
// Default to Brotli compression even if the header is missing.
|
|
162
|
+
res.setHeader('Content-Encoding', 'br');
|
|
163
|
+
compressionMethod = brotliCompressSync;
|
|
164
|
+
}
|
|
165
|
+
const compressedView = compressionMethod(viewResponseBody);
|
|
166
|
+
const end = performance.now();
|
|
167
|
+
logger.warn(`View size (${viewResponseSize}) compressed due to Lambda response limit. Compression took ${Math.round(end - start)} ms`);
|
|
168
|
+
res.send(compressedView);
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
res.send(viewResponse.body);
|
|
172
|
+
}
|
|
99
173
|
};
|
|
100
174
|
}
|
|
101
175
|
function createConfigMiddleware(routes, context, viewHandler) {
|
|
@@ -151,7 +225,6 @@ function createConfigMiddleware(routes, context, viewHandler) {
|
|
|
151
225
|
res.setHeader('cache-control', `public, max-age=${cacheTtl}`);
|
|
152
226
|
}
|
|
153
227
|
}
|
|
154
|
-
res.status(200);
|
|
155
228
|
res.type('application/javascript');
|
|
156
229
|
res.status(viewResponse.status || 200);
|
|
157
230
|
res.send(viewResponse.body);
|
|
@@ -193,7 +266,7 @@ export function viewMiddleware(app, context) {
|
|
|
193
266
|
const localizedPath = `/:locale(${supportedStr})${routePath}`;
|
|
194
267
|
paths.push(localizedPath);
|
|
195
268
|
});
|
|
196
|
-
addDefaultLocaleRedirects(i18n.defaultLocale, defaultLocalePaths, app);
|
|
269
|
+
addDefaultLocaleRedirects(i18n.defaultLocale, defaultLocalePaths, i18n.defaultRedirectParams, app);
|
|
197
270
|
}
|
|
198
271
|
}
|
|
199
272
|
logger.debug({ label: `view-middleware`, message: `Add view paths ${paths}` });
|
|
@@ -208,18 +281,65 @@ export function viewMiddleware(app, context) {
|
|
|
208
281
|
/**
|
|
209
282
|
* Adds a 301 redirect if attempting to route with default locale as the path prefix
|
|
210
283
|
*/
|
|
211
|
-
function addDefaultLocaleRedirects(defaultLocale, defaultLocalePaths, app) {
|
|
284
|
+
function addDefaultLocaleRedirects(defaultLocale, defaultLocalePaths, defaultRedirectParams, app) {
|
|
212
285
|
logger.debug({ label: `view-middleware`, message: `Add default localized paths ${defaultLocalePaths}` });
|
|
213
|
-
app.get(defaultLocalePaths, (req, res) => {
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
286
|
+
app.get(defaultLocalePaths, (req, res, next) => {
|
|
287
|
+
// This middleware should only have been called with paths that start with /{defaultLocale}.
|
|
288
|
+
// If somehow that is not the case log a warning and do not re-direct
|
|
289
|
+
if (!req.originalUrl?.startsWith(`/${defaultLocale}`)) {
|
|
290
|
+
logger.warn({
|
|
291
|
+
label: 'view-middleware',
|
|
292
|
+
message: `Attempted to redirect to a URL that did not start with the default locale: /${defaultLocale} ${req.originalUrl}`,
|
|
293
|
+
});
|
|
294
|
+
return next();
|
|
295
|
+
}
|
|
296
|
+
// Separate the path and query string from the original URL
|
|
297
|
+
const [originalPath, queryString] = req.originalUrl.split('?');
|
|
298
|
+
// Remove the default locale from the original path
|
|
299
|
+
let modifiedPath = originalPath.replace(`/${defaultLocale}`, '');
|
|
300
|
+
// Ensure modifiedPath starts with req.basePath if set
|
|
301
|
+
if (req.basePath) {
|
|
302
|
+
modifiedPath = `${req.basePath}${modifiedPath}`;
|
|
219
303
|
}
|
|
304
|
+
// Parse existing query parameters into an object
|
|
305
|
+
const queryParams = new URLSearchParams(queryString);
|
|
306
|
+
// Merge in defaultRedirectParams, replacing values if the key exists with a different value
|
|
307
|
+
if (defaultRedirectParams) {
|
|
308
|
+
Object.entries(defaultRedirectParams).forEach(([key, value]) => {
|
|
309
|
+
// If the key exists but has a different value, replace it
|
|
310
|
+
if (queryParams.has(key) && queryParams.get(key) !== value) {
|
|
311
|
+
queryParams.set(key, value);
|
|
312
|
+
}
|
|
313
|
+
else if (!queryParams.has(key)) {
|
|
314
|
+
// Add the parameter if it doesn't exist
|
|
315
|
+
queryParams.set(key, value);
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
// Rebuild the modified path, adding query params only if they exist
|
|
320
|
+
modifiedPath = queryParams.toString() ? `${modifiedPath}?${queryParams.toString()}` : modifiedPath;
|
|
220
321
|
// Perform a 301 redirect to the modified URL
|
|
221
322
|
res.setHeader('Location', modifiedPath);
|
|
222
323
|
return res.sendStatus(301);
|
|
223
324
|
});
|
|
224
325
|
}
|
|
326
|
+
function addRedirectQueryParam(redirectUrl, depth) {
|
|
327
|
+
// add a request depth query param to the URL
|
|
328
|
+
// the depth header cannot be used since headers cannot be added to a redirect
|
|
329
|
+
const fakeOrigin = 'http://parse.com';
|
|
330
|
+
const url = isURL(redirectUrl) ? new URL(redirectUrl) : new URL(`${fakeOrigin}${redirectUrl}`);
|
|
331
|
+
url.searchParams.set(REQUEST_DEPTH_KEY, String(depth + 1));
|
|
332
|
+
return url.toString().replace(fakeOrigin, '');
|
|
333
|
+
}
|
|
334
|
+
// Get number of bytes from a string. Different char encodings can effect size per char.
|
|
335
|
+
function byteSize(body) {
|
|
336
|
+
if (Buffer.isBuffer(body)) {
|
|
337
|
+
return body.length;
|
|
338
|
+
}
|
|
339
|
+
else if (typeof body === 'string') {
|
|
340
|
+
return new TextEncoder().encode(body).length; // Get byte size of the string
|
|
341
|
+
}
|
|
342
|
+
// Return -1 if JSON or undefined
|
|
343
|
+
return -1;
|
|
344
|
+
}
|
|
225
345
|
//# sourceMappingURL=view-middleware.js.map
|
|
@@ -114,6 +114,10 @@ export default class SiteGenerator {
|
|
|
114
114
|
* Capture additional metadata collected during the processing of a route or additional module
|
|
115
115
|
*/
|
|
116
116
|
private captureAdditionalRouteMetadata;
|
|
117
|
+
/**
|
|
118
|
+
* Save the server bundles gathered during view generation to the file system and metadata
|
|
119
|
+
*/
|
|
120
|
+
private saveServerBundles;
|
|
117
121
|
}
|
|
118
122
|
export declare class ViewImportMetadataImpl implements ViewImportMetadata {
|
|
119
123
|
private existing;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { performance } from 'perf_hooks';
|
|
2
2
|
import { logger } from '@lwrjs/diagnostics';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
// createIntegrityHash,
|
|
5
|
+
getSpecifier, getFeatureFlags, hashContent, isSelfUrl, getModuleUriPrefix, getMappingUriPrefix, isExternalUrl, mimeLookup, getViewUri, sortLocalesByFallback, VERSION_NOT_PROVIDED, PROTOCOL_FILE, normalizeFromFileURL, isExternalSpecifier, explodeSpecifier, prettyModuleUriSuffix, normalizeVersionToUri, signBundle, } from '@lwrjs/shared-utils';
|
|
4
6
|
import { SiteMetadataImpl, getSiteBundleId, getSiteResourceId } from '@lwrjs/static/site-metadata';
|
|
5
7
|
import { join, dirname, extname, normalize } from 'path';
|
|
6
8
|
import fs from 'fs-extra';
|
|
@@ -166,6 +168,7 @@ export default class SiteGenerator {
|
|
|
166
168
|
context = await dispatcher.dispatchUrl(url, 'GET', siteConfig.locale);
|
|
167
169
|
// Handle 302 redirect if applicable
|
|
168
170
|
if (context?.fs?.headers?.Location) {
|
|
171
|
+
this.saveServerBundles(siteConfig, context.fs.metadata?.viewDefinition?.viewRecord.serverBundles);
|
|
169
172
|
const redirectUrl = context?.fs?.headers?.Location;
|
|
170
173
|
url = redirectUrl;
|
|
171
174
|
const redirectContext = await dispatcher.dispatchUrl(url, 'GET', siteConfig.locale);
|
|
@@ -211,6 +214,12 @@ export default class SiteGenerator {
|
|
|
211
214
|
createResourceDir(dirname(normalizedUrl), outputDir);
|
|
212
215
|
const ext = extname(normalizedUrl);
|
|
213
216
|
const fullPath = join(outputDir, `${normalizedUrl}${ext ? '' : '.js'}`);
|
|
217
|
+
// TEMP: Code to force client bundles to fail fast during SSR for manual testing
|
|
218
|
+
// if (context.fs?.body) {
|
|
219
|
+
// const body = `if (typeof window === 'undefined') throw new Error('This is a client bundle! ${specifier}');\n${context.fs.body}`;
|
|
220
|
+
// if (moduleDefinition) moduleDefinition.integrity = createIntegrityHash(body);
|
|
221
|
+
// context.fs.body = body;
|
|
222
|
+
// }
|
|
214
223
|
await writeResponse(context, fullPath);
|
|
215
224
|
// Build up a list of dispatch requests to kick off in parallel
|
|
216
225
|
const dispatchRequests = [];
|
|
@@ -266,7 +275,7 @@ export default class SiteGenerator {
|
|
|
266
275
|
}
|
|
267
276
|
// If this is a bundle add it to the bundle metadata
|
|
268
277
|
if (moduleDefinition.bundleRecord) {
|
|
269
|
-
this.addBundleToSiteMetadata(moduleDefinition, url, siteConfig);
|
|
278
|
+
this.addBundleToSiteMetadata(moduleDefinition, url, false, siteConfig);
|
|
270
279
|
}
|
|
271
280
|
}
|
|
272
281
|
// Bundles with unresolved module uris
|
|
@@ -296,15 +305,15 @@ export default class SiteGenerator {
|
|
|
296
305
|
siteBundles[specifier] = bundleMetadata;
|
|
297
306
|
}
|
|
298
307
|
}
|
|
299
|
-
addBundleToSiteMetadata(bundleDefinition, url, siteConfig) {
|
|
308
|
+
addBundleToSiteMetadata(bundleDefinition, url, ssr, siteConfig) {
|
|
300
309
|
if (siteConfig.siteMetadata) {
|
|
301
310
|
const locale = siteConfig.locale;
|
|
302
|
-
const specifier = getSiteBundleId(bundleDefinition, locale, siteConfig.i18n);
|
|
303
|
-
const imports = bundleDefinition.bundleRecord.imports?.map((moduleRef) => getSiteBundleId(moduleRef, locale, siteConfig.i18n)) || [];
|
|
304
|
-
const dynamicImports = bundleDefinition.bundleRecord.dynamicImports?.map((moduleRef) => getSiteBundleId(moduleRef, locale, siteConfig.i18n));
|
|
311
|
+
const specifier = getSiteBundleId(bundleDefinition, locale, ssr, siteConfig.i18n);
|
|
312
|
+
const imports = bundleDefinition.bundleRecord.imports?.map((moduleRef) => getSiteBundleId(moduleRef, locale, false, siteConfig.i18n)) || [];
|
|
313
|
+
const dynamicImports = bundleDefinition.bundleRecord.dynamicImports?.map((moduleRef) => getSiteBundleId(moduleRef, locale, false, siteConfig.i18n));
|
|
305
314
|
const includedModules = bundleDefinition.bundleRecord.includedModules?.map((moduleRef) => {
|
|
306
315
|
const moduleId = explodeSpecifier(moduleRef);
|
|
307
|
-
return getSiteBundleId(moduleId, locale, siteConfig.i18n);
|
|
316
|
+
return getSiteBundleId(moduleId, locale, false, siteConfig.i18n);
|
|
308
317
|
}) || [];
|
|
309
318
|
const version = bundleDefinition.version === VERSION_NOT_PROVIDED ? undefined : bundleDefinition.version;
|
|
310
319
|
const bundleMetadata = {
|
|
@@ -482,6 +491,8 @@ export default class SiteGenerator {
|
|
|
482
491
|
dispatchRequests.push(this.dispatchJSResourceRecursive(jsUri, dispatcher, siteConfig));
|
|
483
492
|
}
|
|
484
493
|
}
|
|
494
|
+
// Server bundles
|
|
495
|
+
this.saveServerBundles(siteConfig, viewDefinition.viewRecord.serverBundles);
|
|
485
496
|
// Bootstrap Resources
|
|
486
497
|
const bootstrapResources = viewDefinition.viewRecord.bootstrapModule?.resources || [];
|
|
487
498
|
for (const resource of bootstrapResources) {
|
|
@@ -730,6 +741,7 @@ export default class SiteGenerator {
|
|
|
730
741
|
// legacy globalThis.LWR.importMappings.default
|
|
731
742
|
legacyDefault: getModuleUriPrefix(runtimeEnvironment, { locale }),
|
|
732
743
|
mapping: getMappingUriPrefix(runtimeEnvironment, { locale }),
|
|
744
|
+
server: getModuleUriPrefix({ ...runtimeEnvironment, bundle: true }, { locale }).replace('/bundle/', '/bundle-server/'),
|
|
733
745
|
},
|
|
734
746
|
};
|
|
735
747
|
return {
|
|
@@ -810,6 +822,28 @@ export default class SiteGenerator {
|
|
|
810
822
|
// Save site meta data
|
|
811
823
|
await siteConfig.siteMetadata?.persistSiteMetadata();
|
|
812
824
|
}
|
|
825
|
+
/**
|
|
826
|
+
* Save the server bundles gathered during view generation to the file system and metadata
|
|
827
|
+
*/
|
|
828
|
+
async saveServerBundles(siteConfig, bundles) {
|
|
829
|
+
if (bundles?.size) {
|
|
830
|
+
const { endpoints, outputDir } = siteConfig;
|
|
831
|
+
bundles.forEach(async (bundle) => {
|
|
832
|
+
const { specifier, version } = bundle;
|
|
833
|
+
const vSpecifier = getSpecifier({
|
|
834
|
+
specifier,
|
|
835
|
+
version: version ? normalizeVersionToUri(version) : undefined,
|
|
836
|
+
});
|
|
837
|
+
const url = `${endpoints?.uris.server}${vSpecifier}/s/${signBundle(bundle)}/bundle_${prettyModuleUriSuffix(specifier)}.js`;
|
|
838
|
+
this.addBundleToSiteMetadata(bundle, url, true, siteConfig);
|
|
839
|
+
createResourceDir(dirname(url), outputDir);
|
|
840
|
+
// TEMP: Code to force server bundles to fail fast on the client during manual testing
|
|
841
|
+
// const body = `if (typeof window !== 'undefined') throw new Error('This is a server bundle! ${specifier}');\n${await bundle.getCode()}`;
|
|
842
|
+
// await writeResponse({ fs: { body } }, join(outputDir, url));
|
|
843
|
+
await writeResponse({ fs: { body: await bundle.getCode() } }, join(outputDir, url));
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
}
|
|
813
847
|
}
|
|
814
848
|
// Class used to track import metadata for a view
|
|
815
849
|
export class ViewImportMetadataImpl {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import type { Readable } from 'stream';
|
|
3
3
|
import type { FsContext } from '@lwrjs/types';
|
|
4
4
|
export declare function writeStream(readStream: Readable, fullPath: string): Promise<void>;
|
|
5
|
-
export declare function writeResponse(context: FsContext, fullPath: string): Promise<void>;
|
|
5
|
+
export declare function writeResponse(context: Pick<FsContext, 'fs'>, fullPath: string): Promise<void>;
|
|
6
6
|
/**
|
|
7
7
|
* @param {*} o - Item to check if it's an object
|
|
8
8
|
* @returns {boolean}
|
|
@@ -8,6 +8,8 @@ export async function writeStream(readStream, fullPath) {
|
|
|
8
8
|
});
|
|
9
9
|
}
|
|
10
10
|
export async function writeResponse(context, fullPath) {
|
|
11
|
+
if (fs.existsSync(fullPath))
|
|
12
|
+
return;
|
|
11
13
|
if (context.fs?.stream) {
|
|
12
14
|
const stream = context.fs?.stream;
|
|
13
15
|
await writeStream(stream, fullPath);
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
7
|
-
"version": "0.15.
|
|
7
|
+
"version": "0.15.1",
|
|
8
8
|
"homepage": "https://developer.salesforce.com/docs/platform/lwr/overview",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
@@ -43,46 +43,46 @@
|
|
|
43
43
|
"build": "tsc -b"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@lwrjs/app-service": "0.15.
|
|
47
|
-
"@lwrjs/asset-registry": "0.15.
|
|
48
|
-
"@lwrjs/asset-transformer": "0.15.
|
|
49
|
-
"@lwrjs/base-view-provider": "0.15.
|
|
50
|
-
"@lwrjs/base-view-transformer": "0.15.
|
|
51
|
-
"@lwrjs/client-modules": "0.15.
|
|
52
|
-
"@lwrjs/config": "0.15.
|
|
53
|
-
"@lwrjs/diagnostics": "0.15.
|
|
54
|
-
"@lwrjs/esbuild": "0.15.
|
|
55
|
-
"@lwrjs/fs-asset-provider": "0.15.
|
|
56
|
-
"@lwrjs/fs-watch": "0.15.
|
|
57
|
-
"@lwrjs/html-view-provider": "0.15.
|
|
58
|
-
"@lwrjs/instrumentation": "0.15.
|
|
59
|
-
"@lwrjs/loader": "0.15.
|
|
60
|
-
"@lwrjs/lwc-module-provider": "0.15.
|
|
61
|
-
"@lwrjs/lwc-ssr": "0.15.
|
|
62
|
-
"@lwrjs/markdown-view-provider": "0.15.
|
|
63
|
-
"@lwrjs/module-bundler": "0.15.
|
|
64
|
-
"@lwrjs/module-registry": "0.15.
|
|
65
|
-
"@lwrjs/npm-module-provider": "0.15.
|
|
66
|
-
"@lwrjs/nunjucks-view-provider": "0.15.
|
|
67
|
-
"@lwrjs/o11y": "0.15.
|
|
68
|
-
"@lwrjs/resource-registry": "0.15.
|
|
69
|
-
"@lwrjs/router": "0.15.
|
|
70
|
-
"@lwrjs/server": "0.15.
|
|
71
|
-
"@lwrjs/shared-utils": "0.15.
|
|
72
|
-
"@lwrjs/static": "0.15.
|
|
73
|
-
"@lwrjs/view-registry": "0.15.
|
|
46
|
+
"@lwrjs/app-service": "0.15.1",
|
|
47
|
+
"@lwrjs/asset-registry": "0.15.1",
|
|
48
|
+
"@lwrjs/asset-transformer": "0.15.1",
|
|
49
|
+
"@lwrjs/base-view-provider": "0.15.1",
|
|
50
|
+
"@lwrjs/base-view-transformer": "0.15.1",
|
|
51
|
+
"@lwrjs/client-modules": "0.15.1",
|
|
52
|
+
"@lwrjs/config": "0.15.1",
|
|
53
|
+
"@lwrjs/diagnostics": "0.15.1",
|
|
54
|
+
"@lwrjs/esbuild": "0.15.1",
|
|
55
|
+
"@lwrjs/fs-asset-provider": "0.15.1",
|
|
56
|
+
"@lwrjs/fs-watch": "0.15.1",
|
|
57
|
+
"@lwrjs/html-view-provider": "0.15.1",
|
|
58
|
+
"@lwrjs/instrumentation": "0.15.1",
|
|
59
|
+
"@lwrjs/loader": "0.15.1",
|
|
60
|
+
"@lwrjs/lwc-module-provider": "0.15.1",
|
|
61
|
+
"@lwrjs/lwc-ssr": "0.15.1",
|
|
62
|
+
"@lwrjs/markdown-view-provider": "0.15.1",
|
|
63
|
+
"@lwrjs/module-bundler": "0.15.1",
|
|
64
|
+
"@lwrjs/module-registry": "0.15.1",
|
|
65
|
+
"@lwrjs/npm-module-provider": "0.15.1",
|
|
66
|
+
"@lwrjs/nunjucks-view-provider": "0.15.1",
|
|
67
|
+
"@lwrjs/o11y": "0.15.1",
|
|
68
|
+
"@lwrjs/resource-registry": "0.15.1",
|
|
69
|
+
"@lwrjs/router": "0.15.1",
|
|
70
|
+
"@lwrjs/server": "0.15.1",
|
|
71
|
+
"@lwrjs/shared-utils": "0.15.1",
|
|
72
|
+
"@lwrjs/static": "0.15.1",
|
|
73
|
+
"@lwrjs/view-registry": "0.15.1",
|
|
74
74
|
"chokidar": "^3.6.0",
|
|
75
75
|
"esbuild": "^0.9.7",
|
|
76
76
|
"fs-extra": "^11.2.0",
|
|
77
77
|
"path-to-regexp": "^6.2.2",
|
|
78
|
-
"qs": "^6.
|
|
79
|
-
"rollup": "^2.
|
|
78
|
+
"qs": "^6.13.0",
|
|
79
|
+
"rollup": "^2.79.2",
|
|
80
80
|
"ws": "^8.18.0"
|
|
81
81
|
},
|
|
82
82
|
"devDependencies": {
|
|
83
|
-
"@lwrjs/types": "0.15.
|
|
83
|
+
"@lwrjs/types": "0.15.1",
|
|
84
84
|
"@types/ws": "^8.5.12",
|
|
85
|
-
"memfs": "^4.
|
|
85
|
+
"memfs": "^4.13.0"
|
|
86
86
|
},
|
|
87
87
|
"peerDependencies": {
|
|
88
88
|
"lwc": ">= 2.x"
|
|
@@ -93,5 +93,5 @@
|
|
|
93
93
|
"volta": {
|
|
94
94
|
"extends": "../../../package.json"
|
|
95
95
|
},
|
|
96
|
-
"gitHead": "
|
|
96
|
+
"gitHead": "9ae4002fdab4f55caea2f8de8a7139a6b64ebad1"
|
|
97
97
|
}
|