@bleedingdev/modern-js-plugin-data-loader 3.2.0-ultramodern.0
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/LICENSE +21 -0
- package/README.md +26 -0
- package/dist/cjs/cli/createRequest.js +133 -0
- package/dist/cjs/cli/data.js +145 -0
- package/dist/cjs/cli/generateClient.js +66 -0
- package/dist/cjs/cli/loader.js +68 -0
- package/dist/cjs/common/constants.js +44 -0
- package/dist/cjs/runtime/errors.js +98 -0
- package/dist/cjs/runtime/index.js +157 -0
- package/dist/cjs/runtime/response.js +76 -0
- package/dist/esm/cli/createRequest.mjs +93 -0
- package/dist/esm/cli/data.mjs +111 -0
- package/dist/esm/cli/generateClient.mjs +22 -0
- package/dist/esm/cli/loader.mjs +34 -0
- package/dist/esm/common/constants.mjs +4 -0
- package/dist/esm/runtime/errors.mjs +52 -0
- package/dist/esm/runtime/index.mjs +114 -0
- package/dist/esm/runtime/response.mjs +42 -0
- package/dist/esm-node/cli/createRequest.mjs +94 -0
- package/dist/esm-node/cli/data.mjs +112 -0
- package/dist/esm-node/cli/generateClient.mjs +26 -0
- package/dist/esm-node/cli/loader.mjs +35 -0
- package/dist/esm-node/common/constants.mjs +5 -0
- package/dist/esm-node/runtime/errors.mjs +53 -0
- package/dist/esm-node/runtime/index.mjs +115 -0
- package/dist/esm-node/runtime/response.mjs +43 -0
- package/dist/types/cli/createRequest.d.ts +13 -0
- package/dist/types/cli/data.d.ts +11 -0
- package/dist/types/cli/generateClient.d.ts +5 -0
- package/dist/types/cli/loader.d.ts +12 -0
- package/dist/types/common/constants.d.ts +3 -0
- package/dist/types/runtime/errors.d.ts +60 -0
- package/dist/types/runtime/index.d.ts +5 -0
- package/dist/types/runtime/response.d.ts +11 -0
- package/package.json +79 -0
- package/rslib.config.mts +4 -0
- package/rstest.config.mts +18 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { serializeJson } from "@modern-js/runtime-utils/node";
|
|
2
|
+
import { TextEncoder } from "util";
|
|
3
|
+
function isTrackedPromise(value) {
|
|
4
|
+
return null != value && 'function' == typeof value.then && true === value._tracked;
|
|
5
|
+
}
|
|
6
|
+
const DEFERRED_VALUE_PLACEHOLDER_PREFIX = '__deferred_promise:';
|
|
7
|
+
function createDeferredReadableStream(deferredData, signal) {
|
|
8
|
+
const encoder = new TextEncoder();
|
|
9
|
+
const stream = new ReadableStream({
|
|
10
|
+
async start (controller) {
|
|
11
|
+
const criticalData = {};
|
|
12
|
+
const preresolvedKeys = [];
|
|
13
|
+
for (const [key, value] of Object.entries(deferredData.data))if (isTrackedPromise(value)) {
|
|
14
|
+
criticalData[key] = `${DEFERRED_VALUE_PLACEHOLDER_PREFIX}${key}`;
|
|
15
|
+
if (void 0 !== value._data || void 0 !== value._error) preresolvedKeys.push(key);
|
|
16
|
+
} else criticalData[key] = value;
|
|
17
|
+
controller.enqueue(encoder.encode(`${JSON.stringify(criticalData)}\n\n`));
|
|
18
|
+
for (const preresolvedKey of preresolvedKeys)enqueueTrackedPromise(controller, encoder, preresolvedKey, deferredData.data[preresolvedKey]);
|
|
19
|
+
const unsubscribe = deferredData.subscribe((aborted, settledKey)=>{
|
|
20
|
+
if (settledKey) enqueueTrackedPromise(controller, encoder, settledKey, deferredData.data[settledKey]);
|
|
21
|
+
});
|
|
22
|
+
await deferredData.resolveData(signal);
|
|
23
|
+
unsubscribe();
|
|
24
|
+
controller.close();
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
return stream;
|
|
28
|
+
}
|
|
29
|
+
function enqueueTrackedPromise(controller, encoder, settledKey, promise) {
|
|
30
|
+
if ('_error' in promise) {
|
|
31
|
+
const { _error } = promise;
|
|
32
|
+
controller.enqueue(encoder.encode(`error:${serializeJson({
|
|
33
|
+
[settledKey]: {
|
|
34
|
+
message: _error.message,
|
|
35
|
+
stack: _error.stack
|
|
36
|
+
}
|
|
37
|
+
})}\n\n`));
|
|
38
|
+
} else controller.enqueue(encoder.encode(`data:${JSON.stringify({
|
|
39
|
+
[settledKey]: promise._data ?? null
|
|
40
|
+
})}\n\n`));
|
|
41
|
+
}
|
|
42
|
+
export { createDeferredReadableStream };
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
import { redirect } from "@modern-js/runtime-utils/router";
|
|
3
|
+
import { compile } from "path-to-regexp";
|
|
4
|
+
import { CONTENT_TYPE_DEFERRED, DIRECT_PARAM, LOADER_ID_PARAM } from "../common/constants.mjs";
|
|
5
|
+
import { parseDeferredReadableStream } from "./data.mjs";
|
|
6
|
+
const getRequestUrl = ({ params, request, routeId })=>{
|
|
7
|
+
const url = new URL(request.url);
|
|
8
|
+
const toPath = compile(url.pathname, {
|
|
9
|
+
encode: encodeURIComponent
|
|
10
|
+
});
|
|
11
|
+
const newPathName = toPath(params);
|
|
12
|
+
url.pathname = newPathName;
|
|
13
|
+
url.searchParams.append(LOADER_ID_PARAM, routeId);
|
|
14
|
+
url.searchParams.append(DIRECT_PARAM, 'true');
|
|
15
|
+
return url;
|
|
16
|
+
};
|
|
17
|
+
const handleRedirectResponse = (res)=>{
|
|
18
|
+
const { headers } = res;
|
|
19
|
+
const location = headers.get('X-Modernjs-Redirect');
|
|
20
|
+
if (location) throw redirect(location);
|
|
21
|
+
return res;
|
|
22
|
+
};
|
|
23
|
+
const isDeferredResponse = (res)=>res.headers.get('Content-Type')?.match(CONTENT_TYPE_DEFERRED) && res.body;
|
|
24
|
+
const isRedirectResponse = (res)=>null != res.headers.get('X-Modernjs-Redirect');
|
|
25
|
+
const isErrorResponse = (res)=>null != res.headers.get('X-Modernjs-Error');
|
|
26
|
+
function isOtherErrorResponse(res) {
|
|
27
|
+
return res.status >= 400 && null == res.headers.get('X-Modernjs-Error') && null == res.headers.get('X-Modernjs-Catch') && null == res.headers.get('X-Modernjs-Response');
|
|
28
|
+
}
|
|
29
|
+
const isCatchResponse = (res)=>null != res.headers.get('X-Modernjs-Catch');
|
|
30
|
+
const handleErrorResponse = async (res)=>{
|
|
31
|
+
const data = await res.json();
|
|
32
|
+
const error = new Error(data.message);
|
|
33
|
+
error.stack = data.stack;
|
|
34
|
+
throw error;
|
|
35
|
+
};
|
|
36
|
+
const handleNetworkErrorResponse = async (res)=>{
|
|
37
|
+
const text = await res.text();
|
|
38
|
+
const error = new Error(text);
|
|
39
|
+
error.stack = void 0;
|
|
40
|
+
throw error;
|
|
41
|
+
};
|
|
42
|
+
const createRequest = (routeId, method = 'get')=>async ({ params, request })=>{
|
|
43
|
+
const url = getRequestUrl({
|
|
44
|
+
params,
|
|
45
|
+
request,
|
|
46
|
+
routeId
|
|
47
|
+
});
|
|
48
|
+
let res;
|
|
49
|
+
res = await fetch(url, {
|
|
50
|
+
method,
|
|
51
|
+
signal: request.signal
|
|
52
|
+
}).catch((error)=>{
|
|
53
|
+
throw error;
|
|
54
|
+
});
|
|
55
|
+
if (isRedirectResponse(res)) return handleRedirectResponse(res);
|
|
56
|
+
if (isErrorResponse(res)) return await handleErrorResponse(res);
|
|
57
|
+
if (isCatchResponse(res)) throw res;
|
|
58
|
+
if (isDeferredResponse(res)) {
|
|
59
|
+
const deferredData = await parseDeferredReadableStream(res.body);
|
|
60
|
+
return deferredData.data;
|
|
61
|
+
}
|
|
62
|
+
if (isOtherErrorResponse(res)) return await handleNetworkErrorResponse(res);
|
|
63
|
+
return res;
|
|
64
|
+
};
|
|
65
|
+
const createActionRequest = (routeId)=>async ({ params, request })=>{
|
|
66
|
+
const url = getRequestUrl({
|
|
67
|
+
params,
|
|
68
|
+
request,
|
|
69
|
+
routeId
|
|
70
|
+
});
|
|
71
|
+
const init = {
|
|
72
|
+
signal: request.signal
|
|
73
|
+
};
|
|
74
|
+
if ('GET' !== request.method) {
|
|
75
|
+
init.method = request.method;
|
|
76
|
+
const contentType = request.headers.get('Content-Type');
|
|
77
|
+
if (contentType && /\bapplication\/json\b/.test(contentType)) {
|
|
78
|
+
init.headers = {
|
|
79
|
+
'Content-Type': contentType
|
|
80
|
+
};
|
|
81
|
+
init.body = JSON.stringify(await request.json());
|
|
82
|
+
} else if (contentType && /\btext\/plain\b/.test(contentType)) {
|
|
83
|
+
init.headers = {
|
|
84
|
+
'Content-Type': contentType
|
|
85
|
+
};
|
|
86
|
+
init.body = await request.text();
|
|
87
|
+
} else if (contentType && /\bapplication\/x-www-form-urlencoded\b/.test(contentType)) init.body = new URLSearchParams(await request.text());
|
|
88
|
+
else init.body = await request.formData();
|
|
89
|
+
}
|
|
90
|
+
const res = await fetch(url, init);
|
|
91
|
+
if (!res.ok) throw res;
|
|
92
|
+
return res;
|
|
93
|
+
};
|
|
94
|
+
export { createActionRequest, createRequest, getRequestUrl };
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
import { AbortedDeferredError, DeferredData } from "@modern-js/runtime-utils/browser";
|
|
3
|
+
const DEFERRED_VALUE_PLACEHOLDER_PREFIX = '__deferred_promise:';
|
|
4
|
+
async function parseDeferredReadableStream(stream) {
|
|
5
|
+
if (!stream) throw new Error('parseDeferredReadableStream requires stream argument');
|
|
6
|
+
let deferredData;
|
|
7
|
+
const deferredResolvers = {};
|
|
8
|
+
try {
|
|
9
|
+
const sectionReader = readStreamSections(stream);
|
|
10
|
+
const initialSectionResult = await sectionReader.next();
|
|
11
|
+
const initialSection = initialSectionResult.value;
|
|
12
|
+
if (!initialSection) throw new Error('no critical data');
|
|
13
|
+
const criticalData = JSON.parse(initialSection);
|
|
14
|
+
if ('object' == typeof criticalData && null !== criticalData) {
|
|
15
|
+
for (const [eventKey, value] of Object.entries(criticalData))if ('string' == typeof value && value.startsWith(DEFERRED_VALUE_PLACEHOLDER_PREFIX)) {
|
|
16
|
+
deferredData = deferredData || {};
|
|
17
|
+
deferredData[eventKey] = new Promise((resolve, reject)=>{
|
|
18
|
+
deferredResolvers[eventKey] = {
|
|
19
|
+
resolve: (value)=>{
|
|
20
|
+
resolve(value);
|
|
21
|
+
delete deferredResolvers[eventKey];
|
|
22
|
+
},
|
|
23
|
+
reject: (error)=>{
|
|
24
|
+
reject(error);
|
|
25
|
+
delete deferredResolvers[eventKey];
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
(async ()=>{
|
|
32
|
+
try {
|
|
33
|
+
for await (const section of sectionReader){
|
|
34
|
+
const [event, ...sectionDataStrings] = section.split(':');
|
|
35
|
+
const sectionDataString = sectionDataStrings.join(':');
|
|
36
|
+
const data = JSON.parse(sectionDataString);
|
|
37
|
+
if ('data' === event) {
|
|
38
|
+
for (const [key, value] of Object.entries(data))if (deferredResolvers[key]) deferredResolvers[key].resolve(value);
|
|
39
|
+
} else if ('error' === event) for (const [key, value] of Object.entries(data)){
|
|
40
|
+
const err = new Error(value.message);
|
|
41
|
+
err.stack = value.stack;
|
|
42
|
+
if (deferredResolvers[key]) deferredResolvers[key].reject(err);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
for (const [key, resolver] of Object.entries(deferredResolvers))resolver.reject(new AbortedDeferredError(`Deferred ${key} will never resolved`));
|
|
46
|
+
} catch (error) {
|
|
47
|
+
for (const resolver of Object.values(deferredResolvers))resolver.reject(error);
|
|
48
|
+
}
|
|
49
|
+
})();
|
|
50
|
+
return new DeferredData({
|
|
51
|
+
...criticalData,
|
|
52
|
+
...deferredData
|
|
53
|
+
});
|
|
54
|
+
} catch (error) {
|
|
55
|
+
for (const resolver of Object.values(deferredResolvers))resolver.reject(error);
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async function* readStreamSections(stream) {
|
|
60
|
+
const reader = stream.getReader();
|
|
61
|
+
let buffer = [];
|
|
62
|
+
let sections = [];
|
|
63
|
+
let closed = false;
|
|
64
|
+
const encoder = new TextEncoder();
|
|
65
|
+
const decoder = new TextDecoder();
|
|
66
|
+
const readStreamSection = async ()=>{
|
|
67
|
+
if (sections.length > 0) return sections.shift();
|
|
68
|
+
while(!closed && 0 === sections.length){
|
|
69
|
+
const chunk = await reader.read();
|
|
70
|
+
if (chunk.done) {
|
|
71
|
+
closed = true;
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
buffer.push(chunk.value);
|
|
75
|
+
try {
|
|
76
|
+
const bufferedString = decoder.decode(mergeArrays(...buffer));
|
|
77
|
+
const splitSections = bufferedString.split('\n\n');
|
|
78
|
+
if (splitSections.length >= 2) {
|
|
79
|
+
sections.push(...splitSections.slice(0, -1));
|
|
80
|
+
buffer = [
|
|
81
|
+
encoder.encode(splitSections.slice(-1).join('\n\n'))
|
|
82
|
+
];
|
|
83
|
+
}
|
|
84
|
+
if (sections.length > 0) break;
|
|
85
|
+
} catch {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (sections.length > 0) return sections.shift();
|
|
90
|
+
if (buffer.length > 0) {
|
|
91
|
+
const bufferedString = decoder.decode(mergeArrays(...buffer));
|
|
92
|
+
sections = bufferedString.split('\n\n').filter((s)=>s);
|
|
93
|
+
buffer = [];
|
|
94
|
+
}
|
|
95
|
+
return sections.shift();
|
|
96
|
+
};
|
|
97
|
+
let section = await readStreamSection();
|
|
98
|
+
while(section){
|
|
99
|
+
yield section;
|
|
100
|
+
section = await readStreamSection();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function mergeArrays(...arrays) {
|
|
104
|
+
const out = new Uint8Array(arrays.reduce((total, arr)=>total + arr.length, 0));
|
|
105
|
+
let offset = 0;
|
|
106
|
+
for (const arr of arrays){
|
|
107
|
+
out.set(arr, offset);
|
|
108
|
+
offset += arr.length;
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
export { parseDeferredReadableStream };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { fileURLToPath as __rspack_fileURLToPath } from "node:url";
|
|
4
|
+
import { dirname as __rspack_dirname } from "node:path";
|
|
5
|
+
var generateClient_dirname = __rspack_dirname(__rspack_fileURLToPath(import.meta.url));
|
|
6
|
+
const generateClient = ({ inline, action, routeId })=>{
|
|
7
|
+
let requestCode = "";
|
|
8
|
+
const requestCreatorPath = path.join(generateClient_dirname, './createRequest').replace('/cjs/cli/', '/esm/cli/').replace(/\\/g, '/');
|
|
9
|
+
const importCode = `
|
|
10
|
+
import { createRequest, createActionRequest } from '${requestCreatorPath}';
|
|
11
|
+
`;
|
|
12
|
+
requestCode = inline ? action ? `
|
|
13
|
+
export const loader = createRequest('${routeId}');
|
|
14
|
+
export const action = createActionRequest('${routeId}')
|
|
15
|
+
` : `
|
|
16
|
+
export const loader = createRequest('${routeId}');
|
|
17
|
+
` : `
|
|
18
|
+
export default createRequest('${routeId}');
|
|
19
|
+
`;
|
|
20
|
+
const generatedCode = `
|
|
21
|
+
${importCode}
|
|
22
|
+
${requestCode}
|
|
23
|
+
`;
|
|
24
|
+
return generatedCode;
|
|
25
|
+
};
|
|
26
|
+
export { generateClient };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
import { logger } from "@modern-js/utils";
|
|
3
|
+
import { promisify } from "util";
|
|
4
|
+
import { generateClient } from "./generateClient.mjs";
|
|
5
|
+
async function loader(source) {
|
|
6
|
+
this.cacheable();
|
|
7
|
+
const target = this._compiler?.options.target;
|
|
8
|
+
const shouldSkip = (compileTarget)=>target === compileTarget || Array.isArray(target) && target.includes(compileTarget);
|
|
9
|
+
if (shouldSkip('node') || shouldSkip('webworker') || shouldSkip('async-node')) return source;
|
|
10
|
+
const { resourceQuery } = this;
|
|
11
|
+
const options = resourceQuery.slice(1).split('&').reduce((pre, cur)=>{
|
|
12
|
+
const [key, value] = cur.split('=');
|
|
13
|
+
if (key && value) pre[key] = 'true' === value ? true : 'false' === value ? false : value;
|
|
14
|
+
return pre;
|
|
15
|
+
}, {});
|
|
16
|
+
if (!options.loaderId || options.retain) return source;
|
|
17
|
+
if (options.clientData) {
|
|
18
|
+
const readFile = promisify(this.fs.readFile);
|
|
19
|
+
try {
|
|
20
|
+
const clientDataPath = this.resourcePath.includes('.loader.') ? this.resourcePath.replace('.loader.', '.data.client.') : this.resourcePath.replace('.data.', '.data.client.');
|
|
21
|
+
this.addDependency(clientDataPath);
|
|
22
|
+
const clientDataContent = await readFile(clientDataPath);
|
|
23
|
+
return clientDataContent;
|
|
24
|
+
} catch (error) {
|
|
25
|
+
if ('development' === process.env.NODE_ENV) logger.error(`Failed to read the clientData file ${options.clientData}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const code = generateClient({
|
|
29
|
+
inline: options.inline,
|
|
30
|
+
action: options.action,
|
|
31
|
+
routeId: options.routeId
|
|
32
|
+
});
|
|
33
|
+
return code;
|
|
34
|
+
}
|
|
35
|
+
export default loader;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
import { isRouteErrorResponse } from "@modern-js/runtime-utils/router";
|
|
3
|
+
function sanitizeError(error) {
|
|
4
|
+
if (error instanceof Error && 'development' !== process.env.NODE_ENV && 'test' !== process.env.NODE_ENV) {
|
|
5
|
+
const sanitized = new Error(error.message || 'Unexpected Server Error');
|
|
6
|
+
sanitized.stack = void 0;
|
|
7
|
+
return sanitized;
|
|
8
|
+
}
|
|
9
|
+
return error;
|
|
10
|
+
}
|
|
11
|
+
function sanitizeErrors(errors) {
|
|
12
|
+
return Object.entries(errors).reduce((acc, [routeId, error])=>Object.assign(acc, {
|
|
13
|
+
[routeId]: sanitizeError(error)
|
|
14
|
+
}), {});
|
|
15
|
+
}
|
|
16
|
+
function serializeError(error) {
|
|
17
|
+
const sanitized = sanitizeError(error);
|
|
18
|
+
return {
|
|
19
|
+
message: sanitized.message,
|
|
20
|
+
stack: sanitized.stack
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function serializeErrors(errors) {
|
|
24
|
+
if (!errors) return null;
|
|
25
|
+
const entries = Object.entries(errors);
|
|
26
|
+
const serialized = {};
|
|
27
|
+
for (const [key, val] of entries)if (isRouteErrorResponse(val)) serialized[key] = {
|
|
28
|
+
...val,
|
|
29
|
+
__type: 'RouteErrorResponse'
|
|
30
|
+
};
|
|
31
|
+
else if (val instanceof Error) {
|
|
32
|
+
const sanitized = sanitizeError(val);
|
|
33
|
+
serialized[key] = {
|
|
34
|
+
message: sanitized.message,
|
|
35
|
+
stack: sanitized.stack,
|
|
36
|
+
__type: 'Error',
|
|
37
|
+
...'Error' !== sanitized.name ? {
|
|
38
|
+
__subType: sanitized.name
|
|
39
|
+
} : {}
|
|
40
|
+
};
|
|
41
|
+
} else serialized[key] = val;
|
|
42
|
+
return serialized;
|
|
43
|
+
}
|
|
44
|
+
function errorResponseToJson(errorResponse) {
|
|
45
|
+
return Response.json(serializeError(errorResponse.error || new Error('Unexpected Server Error')), {
|
|
46
|
+
status: errorResponse.status,
|
|
47
|
+
statusText: errorResponse.statusText,
|
|
48
|
+
headers: {
|
|
49
|
+
'X-Modernjs-Error': 'yes'
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
export { errorResponseToJson, sanitizeError, sanitizeErrors, serializeError, serializeErrors };
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
import { transformNestedRoutes } from "@modern-js/runtime-utils/browser";
|
|
3
|
+
import { createRequestContext, reporterCtx, storage } from "@modern-js/runtime-utils/node";
|
|
4
|
+
import { DEFERRED_SYMBOL, createStaticHandler, isRouteErrorResponse } from "@modern-js/runtime-utils/router";
|
|
5
|
+
import { matchEntry } from "@modern-js/runtime-utils/server";
|
|
6
|
+
import { time } from "@modern-js/runtime-utils/time";
|
|
7
|
+
import { parseHeaders } from "@modern-js/runtime-utils/universal/request";
|
|
8
|
+
import { isPlainObject } from "@modern-js/utils/lodash";
|
|
9
|
+
import { LOADER_REPORTER_NAME } from "@modern-js/utils/universal/constants";
|
|
10
|
+
import { CONTENT_TYPE_DEFERRED, LOADER_ID_PARAM } from "../common/constants.mjs";
|
|
11
|
+
import { errorResponseToJson, serializeError } from "./errors.mjs";
|
|
12
|
+
import { createDeferredReadableStream } from "./response.mjs";
|
|
13
|
+
const redirectStatusCodes = new Set([
|
|
14
|
+
301,
|
|
15
|
+
302,
|
|
16
|
+
303,
|
|
17
|
+
307,
|
|
18
|
+
308
|
|
19
|
+
]);
|
|
20
|
+
function isRedirectResponse(status) {
|
|
21
|
+
return redirectStatusCodes.has(status);
|
|
22
|
+
}
|
|
23
|
+
function isResponse(value) {
|
|
24
|
+
return null != value && 'number' == typeof value.status && 'string' == typeof value.statusText && 'object' == typeof value.headers && void 0 !== value.body;
|
|
25
|
+
}
|
|
26
|
+
function convertModernRedirectResponse(headers, basename) {
|
|
27
|
+
const newHeaders = new Headers(headers);
|
|
28
|
+
let redirectUrl = headers.get('Location');
|
|
29
|
+
if ('/' !== basename) redirectUrl = redirectUrl.replace(basename, '');
|
|
30
|
+
newHeaders.set('X-Modernjs-Redirect', redirectUrl);
|
|
31
|
+
newHeaders.delete('Location');
|
|
32
|
+
return new Response(null, {
|
|
33
|
+
status: 204,
|
|
34
|
+
headers: newHeaders
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
function hasFileExtension(pathname) {
|
|
38
|
+
const lastSegment = pathname.split('/').pop() || '';
|
|
39
|
+
const dotIndex = lastSegment.lastIndexOf('.');
|
|
40
|
+
if (-1 === dotIndex) return false;
|
|
41
|
+
const extension = lastSegment.substring(dotIndex).toLowerCase();
|
|
42
|
+
return '.html' !== extension;
|
|
43
|
+
}
|
|
44
|
+
const handleRequest = async ({ request, serverRoutes, routes: routesConfig, context, onTiming })=>{
|
|
45
|
+
const url = new URL(request.url);
|
|
46
|
+
const routeId = url.searchParams.get(LOADER_ID_PARAM);
|
|
47
|
+
if (hasFileExtension(url.pathname)) return;
|
|
48
|
+
const entry = matchEntry(url.pathname, serverRoutes);
|
|
49
|
+
if (!routeId || !entry) return;
|
|
50
|
+
const basename = entry.urlPath;
|
|
51
|
+
const end = time();
|
|
52
|
+
const { reporter, loaderContext, monitors } = context;
|
|
53
|
+
const headersData = parseHeaders(request);
|
|
54
|
+
const activeDeferreds = new Map();
|
|
55
|
+
return storage.run({
|
|
56
|
+
headers: headersData,
|
|
57
|
+
monitors,
|
|
58
|
+
request,
|
|
59
|
+
activeDeferreds
|
|
60
|
+
}, async ()=>{
|
|
61
|
+
const routes = transformNestedRoutes(routesConfig);
|
|
62
|
+
const { queryRoute } = createStaticHandler(routes, {
|
|
63
|
+
basename
|
|
64
|
+
});
|
|
65
|
+
const requestContext = createRequestContext(loaderContext);
|
|
66
|
+
requestContext.set(reporterCtx, reporter);
|
|
67
|
+
let response;
|
|
68
|
+
try {
|
|
69
|
+
response = await queryRoute(request, {
|
|
70
|
+
routeId,
|
|
71
|
+
requestContext
|
|
72
|
+
});
|
|
73
|
+
if (isResponse(response) && isRedirectResponse(response.status)) response = convertModernRedirectResponse(response.headers, basename);
|
|
74
|
+
else if (isPlainObject(response) && (DEFERRED_SYMBOL in response || activeDeferreds.get(routeId))) {
|
|
75
|
+
let deferredData;
|
|
76
|
+
deferredData = DEFERRED_SYMBOL in response ? response[DEFERRED_SYMBOL] : activeDeferreds.get(routeId);
|
|
77
|
+
const body = createDeferredReadableStream(deferredData, request.signal);
|
|
78
|
+
const init = deferredData.init || {};
|
|
79
|
+
if (init.status && isRedirectResponse(init.status)) {
|
|
80
|
+
if (!init.headers) throw new Error('redirect response includes no headers');
|
|
81
|
+
response = convertModernRedirectResponse(new Headers(init.headers), basename);
|
|
82
|
+
} else {
|
|
83
|
+
const headers = new Headers(init.headers);
|
|
84
|
+
headers.set('Content-Type', `${CONTENT_TYPE_DEFERRED}; charset=UTF-8`);
|
|
85
|
+
init.headers = headers;
|
|
86
|
+
response = new Response(body, init);
|
|
87
|
+
}
|
|
88
|
+
} else response = isResponse(response) ? response : new Response(JSON.stringify(response), {
|
|
89
|
+
headers: {
|
|
90
|
+
'Content-Type': 'application/json; charset=utf-8'
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
const cost = end();
|
|
94
|
+
response.headers.set('X-Modernjs-Response', 'yes');
|
|
95
|
+
onTiming?.(`${LOADER_REPORTER_NAME}-navigation`, cost);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
if (isResponse(error)) {
|
|
98
|
+
error.headers.set('X-Modernjs-Catch', 'yes');
|
|
99
|
+
response = error;
|
|
100
|
+
} else if (isRouteErrorResponse(error)) response = errorResponseToJson(error);
|
|
101
|
+
else {
|
|
102
|
+
const errorInstance = error instanceof Error || error instanceof DOMException ? error : new Error('Unexpected Server Error');
|
|
103
|
+
response = new Response(JSON.stringify(serializeError(errorInstance)), {
|
|
104
|
+
status: 500,
|
|
105
|
+
headers: {
|
|
106
|
+
'X-Modernjs-Error': 'yes',
|
|
107
|
+
'Content-Type': 'application/json'
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return response;
|
|
113
|
+
});
|
|
114
|
+
};
|
|
115
|
+
export { handleRequest, hasFileExtension, isRedirectResponse, isResponse };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
import { serializeJson } from "@modern-js/runtime-utils/node";
|
|
3
|
+
import { TextEncoder } from "util";
|
|
4
|
+
function isTrackedPromise(value) {
|
|
5
|
+
return null != value && 'function' == typeof value.then && true === value._tracked;
|
|
6
|
+
}
|
|
7
|
+
const DEFERRED_VALUE_PLACEHOLDER_PREFIX = '__deferred_promise:';
|
|
8
|
+
function createDeferredReadableStream(deferredData, signal) {
|
|
9
|
+
const encoder = new TextEncoder();
|
|
10
|
+
const stream = new ReadableStream({
|
|
11
|
+
async start (controller) {
|
|
12
|
+
const criticalData = {};
|
|
13
|
+
const preresolvedKeys = [];
|
|
14
|
+
for (const [key, value] of Object.entries(deferredData.data))if (isTrackedPromise(value)) {
|
|
15
|
+
criticalData[key] = `${DEFERRED_VALUE_PLACEHOLDER_PREFIX}${key}`;
|
|
16
|
+
if (void 0 !== value._data || void 0 !== value._error) preresolvedKeys.push(key);
|
|
17
|
+
} else criticalData[key] = value;
|
|
18
|
+
controller.enqueue(encoder.encode(`${JSON.stringify(criticalData)}\n\n`));
|
|
19
|
+
for (const preresolvedKey of preresolvedKeys)enqueueTrackedPromise(controller, encoder, preresolvedKey, deferredData.data[preresolvedKey]);
|
|
20
|
+
const unsubscribe = deferredData.subscribe((aborted, settledKey)=>{
|
|
21
|
+
if (settledKey) enqueueTrackedPromise(controller, encoder, settledKey, deferredData.data[settledKey]);
|
|
22
|
+
});
|
|
23
|
+
await deferredData.resolveData(signal);
|
|
24
|
+
unsubscribe();
|
|
25
|
+
controller.close();
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
return stream;
|
|
29
|
+
}
|
|
30
|
+
function enqueueTrackedPromise(controller, encoder, settledKey, promise) {
|
|
31
|
+
if ('_error' in promise) {
|
|
32
|
+
const { _error } = promise;
|
|
33
|
+
controller.enqueue(encoder.encode(`error:${serializeJson({
|
|
34
|
+
[settledKey]: {
|
|
35
|
+
message: _error.message,
|
|
36
|
+
stack: _error.stack
|
|
37
|
+
}
|
|
38
|
+
})}\n\n`));
|
|
39
|
+
} else controller.enqueue(encoder.encode(`data:${JSON.stringify({
|
|
40
|
+
[settledKey]: promise._data ?? null
|
|
41
|
+
})}\n\n`));
|
|
42
|
+
}
|
|
43
|
+
export { createDeferredReadableStream };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const getRequestUrl: ({ params, request, routeId, }: {
|
|
2
|
+
params: Record<string, string>;
|
|
3
|
+
request: Request;
|
|
4
|
+
routeId: string;
|
|
5
|
+
}) => URL;
|
|
6
|
+
export declare const createRequest: (routeId: string, method?: string) => ({ params, request, }: {
|
|
7
|
+
params: Record<string, string>;
|
|
8
|
+
request: Request;
|
|
9
|
+
}) => Promise<Record<string, unknown> | Response>;
|
|
10
|
+
export declare const createActionRequest: (routeId: string) => ({ params, request, }: {
|
|
11
|
+
params: Record<string, string>;
|
|
12
|
+
request: Request;
|
|
13
|
+
}) => Promise<Response>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The following code is modified based on
|
|
3
|
+
* https://github.com/remix-run/remix/blob/2b5e1a72fc628d0408e27cf4d72e537762f1dc5b/packages/remix-react/data.ts
|
|
4
|
+
*
|
|
5
|
+
* MIT Licensed
|
|
6
|
+
* Author Michael Jackson
|
|
7
|
+
* Copyright 2021 Remix Software Inc.
|
|
8
|
+
* https://github.com/remix-run/remix/blob/2b5e1a72fc628d0408e27cf4d72e537762f1dc5b/LICENSE.md
|
|
9
|
+
*/
|
|
10
|
+
import { DeferredData } from '@modern-js/runtime-utils/browser';
|
|
11
|
+
export declare function parseDeferredReadableStream(stream: ReadableStream<Uint8Array>): Promise<DeferredData>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Rspack } from '@rsbuild/core';
|
|
2
|
+
type Context = {
|
|
3
|
+
mapFile: string;
|
|
4
|
+
loaderId: string;
|
|
5
|
+
clientData?: boolean;
|
|
6
|
+
action: boolean;
|
|
7
|
+
inline: boolean;
|
|
8
|
+
routeId: string;
|
|
9
|
+
retain: boolean;
|
|
10
|
+
};
|
|
11
|
+
export default function loader(this: Rspack.LoaderContext<Context>, source: string): Promise<any>;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The following code is modified based on
|
|
3
|
+
* https://github.com/remix-run/remix/blob/main/packages/remix-server-runtime/errors.ts
|
|
4
|
+
*
|
|
5
|
+
* MIT Licensed
|
|
6
|
+
* Author Michael Jackson
|
|
7
|
+
* Copyright 2021 Remix Software Inc.
|
|
8
|
+
* https://github.com/remix-run/remix/blob/main/LICENSE.md
|
|
9
|
+
*/
|
|
10
|
+
import type { ErrorResponse, StaticHandlerContext } from '@modern-js/runtime-utils/router';
|
|
11
|
+
/**
|
|
12
|
+
* This thing probably warrants some explanation.
|
|
13
|
+
*
|
|
14
|
+
* The whole point here is to emulate componentDidCatch for server rendering and
|
|
15
|
+
* data loading. It can get tricky. React can do this on component boundaries
|
|
16
|
+
* but doesn't support it for server rendering or data loading. We know enough
|
|
17
|
+
* with nested routes to be able to emulate the behavior (because we know them
|
|
18
|
+
* statically before rendering.)
|
|
19
|
+
*
|
|
20
|
+
* Each route can export an `ErrorBoundary`.
|
|
21
|
+
*
|
|
22
|
+
* - When rendering throws an error, the nearest error boundary will render
|
|
23
|
+
* (normal react componentDidCatch). This will be the route's own boundary, but
|
|
24
|
+
* if none is provided, it will bubble up to the parents.
|
|
25
|
+
* - When data loading throws an error, the nearest error boundary will render
|
|
26
|
+
* - When performing an action, the nearest error boundary for the action's
|
|
27
|
+
* route tree will render (no redirect happens)
|
|
28
|
+
*
|
|
29
|
+
* During normal react rendering, we do nothing special, just normal
|
|
30
|
+
* componentDidCatch.
|
|
31
|
+
*
|
|
32
|
+
* For server rendering, we mutate `renderBoundaryRouteId` to know the last
|
|
33
|
+
* layout that has an error boundary that tried to render. This emulates which
|
|
34
|
+
* layout would catch a thrown error. If the rendering fails, we catch the error
|
|
35
|
+
* on the server, and go again a second time with the emulator holding on to the
|
|
36
|
+
* information it needs to render the same error boundary as a dynamically
|
|
37
|
+
* thrown render error.
|
|
38
|
+
*
|
|
39
|
+
* When data loading, server or client side, we use the emulator to likewise
|
|
40
|
+
* hang on to the error and re-render at the appropriate layout (where a thrown
|
|
41
|
+
* error would have been caught by cDC).
|
|
42
|
+
*
|
|
43
|
+
* When actions throw, it all works the same. There's an edge case to be aware
|
|
44
|
+
* of though. Actions normally are required to redirect, but in the case of
|
|
45
|
+
* errors, we render the action's route with the emulator holding on to the
|
|
46
|
+
* error. If during this render a parent route/loader throws we ignore that new
|
|
47
|
+
* error and render the action's original error as deeply as possible. In other
|
|
48
|
+
* words, we simply ignore the new error and use the action's error in place
|
|
49
|
+
* because it came first, and that just wouldn't be fair to let errors cut in
|
|
50
|
+
* line.
|
|
51
|
+
*/
|
|
52
|
+
export declare function sanitizeError<T = unknown>(error: T): Error | T;
|
|
53
|
+
export declare function sanitizeErrors(errors: NonNullable<StaticHandlerContext['errors']>): {};
|
|
54
|
+
export type SerializedError = {
|
|
55
|
+
message: string;
|
|
56
|
+
stack?: string;
|
|
57
|
+
};
|
|
58
|
+
export declare function serializeError(error: Error): SerializedError;
|
|
59
|
+
export declare function serializeErrors(errors: StaticHandlerContext['errors']): StaticHandlerContext['errors'];
|
|
60
|
+
export declare function errorResponseToJson(errorResponse: ErrorResponse): Response;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ServerLoaderBundle } from '@modern-js/server-core';
|
|
2
|
+
export declare function isRedirectResponse(status: number): boolean;
|
|
3
|
+
export declare function isResponse(value: any): value is Response;
|
|
4
|
+
export declare function hasFileExtension(pathname: string): boolean;
|
|
5
|
+
export declare const handleRequest: ServerLoaderBundle['handleRequest'];
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The following code is modified based on
|
|
3
|
+
* https://github.com/remix-run/remix/blob/2b5e1a72fc628d0408e27cf4d72e537762f1dc5b/packages/remix-server-runtime/responses.ts
|
|
4
|
+
*
|
|
5
|
+
* MIT Licensed
|
|
6
|
+
* Author Michael Jackson
|
|
7
|
+
* Copyright 2021 Remix Software Inc.
|
|
8
|
+
* https://github.com/remix-run/remix/blob/2b5e1a72fc628d0408e27cf4d72e537762f1dc5b/LICENSE.md
|
|
9
|
+
*/
|
|
10
|
+
import type { DeferredData } from '@modern-js/runtime-utils/browser';
|
|
11
|
+
export declare function createDeferredReadableStream(deferredData: DeferredData, signal: AbortSignal): any;
|