@maiyunnet/kebab 9.18.1 → 9.19.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/doc/kebab-rag.md +288 -150
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/lib/undici/request.d.ts +10 -0
- package/lib/undici/request.js +16 -0
- package/lib/undici.d.ts +16 -0
- package/lib/undici.js +33 -1
- package/package.json +1 -1
- package/sys/child.js +1 -1
- package/sys/ctr.d.ts +9 -12
- package/sys/ctr.js +51 -95
- package/sys/route.d.ts +2 -1
- package/sys/route.js +48 -9
- package/www/example/ctr/test.js +6 -6
- package/www/example/stc/view/hello.page.d.ts +2 -1
- package/www/example/stc/view/hello.page.js +2 -1
- package/www/example/stc/view/hello.page.tsx +2 -1
- package/www/example/stc/view/react-router.page.d.ts +3 -3
- package/www/example/stc/view/react-router.page.js +3 -3
- package/www/example/stc/view/react-router.page.tsx +3 -4
- package/www/example/stc/view/react.page.d.ts +5 -9
- package/www/example/stc/view/react.page.js +7 -11
- package/www/example/stc/view/react.page.tsx +8 -14
package/index.d.ts
CHANGED
package/index.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* --- 本文件用来定义每个目录实体地址的常量 ---
|
|
7
7
|
*/
|
|
8
8
|
/** --- 当前系统版本号 --- */
|
|
9
|
-
export const VER = '9.
|
|
9
|
+
export const VER = '9.19.0';
|
|
10
10
|
// --- 服务端用的路径 ---
|
|
11
11
|
const imu = decodeURIComponent(import.meta.url).replace('file://', '').replace(/^\/(\w:)/, '$1');
|
|
12
12
|
/** --- /xxx/xxx --- */
|
package/lib/undici/request.d.ts
CHANGED
|
@@ -47,6 +47,16 @@ export declare class Request {
|
|
|
47
47
|
* @param timeout 秒
|
|
48
48
|
*/
|
|
49
49
|
timeout(timeout: number): this;
|
|
50
|
+
/**
|
|
51
|
+
* --- 设置空闲连接允许复用的最长时间 ---
|
|
52
|
+
* @param timeout 秒数;非正数或无效值按未设置处理;默认无服务端提示时为 4 秒,有提示时采用提示值减 2 秒且最多 600 秒
|
|
53
|
+
*/
|
|
54
|
+
keepAliveTimeout(timeout: number): this;
|
|
55
|
+
/**
|
|
56
|
+
* --- 设置连接建立后允许承接新请求的最长时间 ---
|
|
57
|
+
* @param timeout 秒数;非正数或无效值按未设置处理;达到后不再复用但不会中断在途请求,默认不限制
|
|
58
|
+
*/
|
|
59
|
+
reuseTimeout(timeout: number): this;
|
|
50
60
|
/**
|
|
51
61
|
* --- 设置网络异常后的重试次数 ---
|
|
52
62
|
* @param retry 重试次数,默认为 1;非幂等请求需由调用方保证安全
|
package/lib/undici/request.js
CHANGED
|
@@ -59,6 +59,22 @@ export class Request {
|
|
|
59
59
|
this._opt['timeout'] = timeout;
|
|
60
60
|
return this;
|
|
61
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* --- 设置空闲连接允许复用的最长时间 ---
|
|
64
|
+
* @param timeout 秒数;非正数或无效值按未设置处理;默认无服务端提示时为 4 秒,有提示时采用提示值减 2 秒且最多 600 秒
|
|
65
|
+
*/
|
|
66
|
+
keepAliveTimeout(timeout) {
|
|
67
|
+
this._opt['keepAliveTimeout'] = timeout;
|
|
68
|
+
return this;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* --- 设置连接建立后允许承接新请求的最长时间 ---
|
|
72
|
+
* @param timeout 秒数;非正数或无效值按未设置处理;达到后不再复用但不会中断在途请求,默认不限制
|
|
73
|
+
*/
|
|
74
|
+
reuseTimeout(timeout) {
|
|
75
|
+
this._opt['reuseTimeout'] = timeout;
|
|
76
|
+
return this;
|
|
77
|
+
}
|
|
62
78
|
/**
|
|
63
79
|
* --- 设置网络异常后的重试次数 ---
|
|
64
80
|
* @param retry 重试次数,默认为 1;非幂等请求需由调用方保证安全
|
package/lib/undici.d.ts
CHANGED
|
@@ -74,6 +74,10 @@ export declare function fetch(input: string | URL | Request, init?: RequestInit
|
|
|
74
74
|
'hosts'?: Record<string, string> | string;
|
|
75
75
|
/** --- 网络异常后的重试次数,默认 0;流式请求体不可重试,非幂等请求需由调用方保证安全 --- */
|
|
76
76
|
'retry'?: number;
|
|
77
|
+
/** --- 空闲连接允许复用的最长秒数;非正数或无效值按未设置处理;默认无服务端提示时为 4 秒,有提示时采用提示值减 2 秒且最多 600 秒 --- */
|
|
78
|
+
'keepAliveTimeout'?: number;
|
|
79
|
+
/** --- 连接建立后允许承接新请求的最长秒数;非正数或无效值按未设置处理;达到后不再复用但不会中断在途请求,默认不限制 --- */
|
|
80
|
+
'reuseTimeout'?: number;
|
|
77
81
|
}): Promise<Response>;
|
|
78
82
|
/**
|
|
79
83
|
* --- 发起一个请求 ---
|
|
@@ -152,6 +156,10 @@ export interface IRequestOptions {
|
|
|
152
156
|
};
|
|
153
157
|
/** --- 连接是否保持长连接(即是否允许复用),默认为 true --- */
|
|
154
158
|
'keep'?: boolean;
|
|
159
|
+
/** --- 空闲连接允许复用的最长秒数;非正数或无效值按未设置处理;默认无服务端提示时为 4 秒,有提示时采用提示值减 2 秒且最多 600 秒;自定义 Agent 由其自身配置 --- */
|
|
160
|
+
'keepAliveTimeout'?: number;
|
|
161
|
+
/** --- 连接建立后允许承接新请求的最长秒数;非正数或无效值按未设置处理;达到后不再复用但不会中断在途请求,默认不限制;自定义 Agent 由其自身配置 --- */
|
|
162
|
+
'reuseTimeout'?: number;
|
|
155
163
|
/** --- 复用池名/Agent,默认为 default --- */
|
|
156
164
|
'reuse'?: string | undici.ProxyAgent | undici.Agent;
|
|
157
165
|
/** --- cookie 托管对象 --- */
|
|
@@ -170,6 +178,10 @@ export interface IMproxyOptions {
|
|
|
170
178
|
'hosts'?: Record<string, string> | string;
|
|
171
179
|
'local'?: string;
|
|
172
180
|
'headers'?: THttpHeaders;
|
|
181
|
+
/** --- 空闲连接允许复用的最长秒数;非正数或无效值按未设置处理;默认无服务端提示时为 4 秒,有提示时采用提示值减 2 秒且最多 600 秒 --- */
|
|
182
|
+
'keepAliveTimeout'?: number;
|
|
183
|
+
/** --- 连接建立后允许承接新请求的最长秒数;非正数或无效值按未设置处理;达到后不再复用但不会中断在途请求,默认不限制 --- */
|
|
184
|
+
'reuseTimeout'?: number;
|
|
173
185
|
/** --- 过滤 header,返回 true 则留下 --- */
|
|
174
186
|
filter?: (h: string) => boolean;
|
|
175
187
|
/** --- 默认为 default --- */
|
|
@@ -184,6 +196,10 @@ export interface IRproxyOptions {
|
|
|
184
196
|
'hosts'?: Record<string, string> | string;
|
|
185
197
|
'local'?: string;
|
|
186
198
|
'headers'?: THttpHeaders;
|
|
199
|
+
/** --- 空闲连接允许复用的最长秒数;非正数或无效值按未设置处理;默认无服务端提示时为 4 秒,有提示时采用提示值减 2 秒且最多 600 秒 --- */
|
|
200
|
+
'keepAliveTimeout'?: number;
|
|
201
|
+
/** --- 连接建立后允许承接新请求的最长秒数;非正数或无效值按未设置处理;达到后不再复用但不会中断在途请求,默认不限制 --- */
|
|
202
|
+
'reuseTimeout'?: number;
|
|
187
203
|
/** --- 过滤 header,返回 true 则留下 --- */
|
|
188
204
|
filter?: (h: string) => boolean;
|
|
189
205
|
/** --- 正向 mproxy 代理,url 如 https://xxx/abc --- */
|
package/lib/undici.js
CHANGED
|
@@ -45,14 +45,31 @@ function normalizeRetry(retry) {
|
|
|
45
45
|
return (typeof retry === 'number') && Number.isFinite(retry) ?
|
|
46
46
|
Math.max(0, Math.floor(retry)) : 0;
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* --- 将 Agent 的秒数配置转换为毫秒 ---
|
|
50
|
+
* @param timeout 秒数
|
|
51
|
+
* @returns 未设置或数值无效时返回 undefined,否则返回毫秒数
|
|
52
|
+
*/
|
|
53
|
+
function normalizeAgentTimeout(timeout) {
|
|
54
|
+
if (timeout === undefined) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
const milliseconds = timeout * 1_000;
|
|
58
|
+
if (!Number.isFinite(milliseconds) || (milliseconds <= 0)) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
return milliseconds;
|
|
62
|
+
}
|
|
48
63
|
/** --- 获取或创建 undici.agent 对象 --- */
|
|
49
64
|
function getAgent(opt = {}) {
|
|
50
65
|
let k = opt.reuse ?? 'default';
|
|
51
66
|
if (typeof k !== 'string') {
|
|
52
67
|
return k;
|
|
53
68
|
}
|
|
69
|
+
const keepAliveTimeout = normalizeAgentTimeout(opt.keepAliveTimeout);
|
|
70
|
+
const reuseTimeout = normalizeAgentTimeout(opt.reuseTimeout);
|
|
54
71
|
if (k === 'default') {
|
|
55
|
-
// --- hosts/local/keep
|
|
72
|
+
// --- hosts/local/keep/连接时长均会影响 agent 行为,需生成独立的 key ---
|
|
56
73
|
const features = [];
|
|
57
74
|
if (opt.hosts) {
|
|
58
75
|
features.push(`h:${lCrypto.hashHmac('md5', lText.stringifyJson(opt.hosts))}`);
|
|
@@ -63,6 +80,12 @@ function getAgent(opt = {}) {
|
|
|
63
80
|
if (opt.keep === false) {
|
|
64
81
|
features.push('k:0');
|
|
65
82
|
}
|
|
83
|
+
if (keepAliveTimeout !== undefined) {
|
|
84
|
+
features.push(`ka:${keepAliveTimeout}`);
|
|
85
|
+
}
|
|
86
|
+
if (reuseTimeout !== undefined) {
|
|
87
|
+
features.push(`rt:${reuseTimeout}`);
|
|
88
|
+
}
|
|
66
89
|
if (features.length > 0) {
|
|
67
90
|
k = features.join('|');
|
|
68
91
|
}
|
|
@@ -74,6 +97,13 @@ function getAgent(opt = {}) {
|
|
|
74
97
|
lookup: buildDnsLookup(opt.hosts),
|
|
75
98
|
},
|
|
76
99
|
'pipelining': opt.keep === false ? 0 : 1,
|
|
100
|
+
...(keepAliveTimeout === undefined ? {} : {
|
|
101
|
+
'keepAliveTimeout': keepAliveTimeout,
|
|
102
|
+
'keepAliveMaxTimeout': keepAliveTimeout,
|
|
103
|
+
}),
|
|
104
|
+
...(reuseTimeout === undefined ? {} : {
|
|
105
|
+
'clientTtl': reuseTimeout,
|
|
106
|
+
}),
|
|
77
107
|
}));
|
|
78
108
|
}
|
|
79
109
|
return agents.get(k);
|
|
@@ -293,6 +323,8 @@ export async function fetch(input, init = {}) {
|
|
|
293
323
|
'hosts': init.hosts,
|
|
294
324
|
'mproxy': init.mproxy,
|
|
295
325
|
'retry': init.retry,
|
|
326
|
+
'keepAliveTimeout': init.keepAliveTimeout,
|
|
327
|
+
'reuseTimeout': init.reuseTimeout,
|
|
296
328
|
'signal': init.signal ?? undefined,
|
|
297
329
|
'follow': init.redirect === 'follow' ? 10 : 0,
|
|
298
330
|
};
|
package/package.json
CHANGED
package/sys/child.js
CHANGED
package/sys/ctr.d.ts
CHANGED
|
@@ -58,6 +58,13 @@ export type TValibotResult<TSchema extends v.GenericSchema, TIssue extends v.Bas
|
|
|
58
58
|
readonly issues: [TIssue, ...TIssue[]];
|
|
59
59
|
readonly response: kebab.Json[];
|
|
60
60
|
};
|
|
61
|
+
/**
|
|
62
|
+
* --- 判断当前 HTTP 请求是否仍可响应 ---
|
|
63
|
+
* @param req 请求对象
|
|
64
|
+
* @param res 响应对象
|
|
65
|
+
* @returns 客户端对应的请求流仍可响应时返回 true
|
|
66
|
+
*/
|
|
67
|
+
export declare function isHttpRequestAvailable(req: http2.Http2ServerRequest | http.IncomingMessage, res: http2.Http2ServerResponse | http.ServerResponse): boolean;
|
|
61
68
|
export declare class Ctr {
|
|
62
69
|
/** --- 路由参数序列数组 --- */
|
|
63
70
|
protected _param: string[];
|
|
@@ -206,17 +213,15 @@ export declare class Ctr {
|
|
|
206
213
|
/**
|
|
207
214
|
* --- 加载 React 全页面进行 SSR 渲染,组件需渲染完整 HTML 文档(含 html/head/body),无需 EJS ---
|
|
208
215
|
* --- 框架自动注入 props:_urlBase/_urlFull/_urlStc/_staticVer/_staticPath/_staticPathFull ---
|
|
209
|
-
* --- 多语言:自动注入 _locale(当前语言名)和 _localeData
|
|
216
|
+
* --- 多语言:自动注入 _locale(当前语言名)和 _localeData(当前语种已载语言包的合并键值对) ---
|
|
210
217
|
* --- 组件内创建:const l = (key: string, ...args: string[]): string => { let i = 0; return (_localeData[key] ?? key).replace(/\?/g, () => args[i++] ?? ''); }; ---
|
|
211
218
|
* @param path 页面组件路径(相对于 stc/ 目录,不含扩展名,tsx 编译后的 .js)
|
|
212
219
|
* @param props 传入组件的 props,框架常量自动合并,整体序列化为内联 JSON 供客户端水合复用
|
|
213
220
|
* @param opt 可选配置
|
|
214
221
|
*/
|
|
215
222
|
protected _loadReactPage(path: string, props?: Record<string, kebab.Json>, opt?: {
|
|
216
|
-
/** ---
|
|
223
|
+
/** --- 是否注入客户端水合 bundle,默认 true;设为 false 时仅输出服务端渲染的 HTML --- */
|
|
217
224
|
'hydrate'?: boolean;
|
|
218
|
-
/** --- react/react-dom/react-router-dom 版本号,用于 esm.sh CDN,默认 19 --- */
|
|
219
|
-
'reactVer'?: string;
|
|
220
225
|
/**
|
|
221
226
|
* --- 路由模式,不传则不注入任何 Router,组件自行管理路由(如 MemoryRouter)或无路由 ---
|
|
222
227
|
* --- 'browser':服务端用 StaticRouter,客户端用 BrowserRouter,地址栏与路由联动 ---
|
|
@@ -234,14 +239,6 @@ export declare class Ctr {
|
|
|
234
239
|
*/
|
|
235
240
|
'staticPath'?: string;
|
|
236
241
|
}): Promise<string>;
|
|
237
|
-
/**
|
|
238
|
-
* --- 递归扫描 JS 文件中的 import 语句,收集第三方 bare specifier ---
|
|
239
|
-
* @param filePath 当前要扫描的文件绝对路径
|
|
240
|
-
* @param scannedFiles 已扫描文件集(去重用)
|
|
241
|
-
* @param extraImports 收集到的第三方包名集合
|
|
242
|
-
* @param builtinImports 内置 import map,已有条目不重复添加
|
|
243
|
-
*/
|
|
244
|
-
private _scanImports;
|
|
245
242
|
/**
|
|
246
243
|
* --- 设置校验错误返回值 ---
|
|
247
244
|
* @param rtn 返回值数组
|
package/sys/ctr.js
CHANGED
|
@@ -19,6 +19,27 @@ export function clearLocaleData() {
|
|
|
19
19
|
localeFiles = [];
|
|
20
20
|
localeData = {};
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* --- 判断当前 HTTP 请求是否仍可响应 ---
|
|
24
|
+
* @param req 请求对象
|
|
25
|
+
* @param res 响应对象
|
|
26
|
+
* @returns 客户端对应的请求流仍可响应时返回 true
|
|
27
|
+
*/
|
|
28
|
+
export function isHttpRequestAvailable(req, res) {
|
|
29
|
+
if (req.destroyed && !req.complete) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
if (('stream' in req) && (req.stream.closed || req.stream.destroyed)) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
if (req.socket.destroyed || !req.socket.writable) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
if (res.destroyed || res.closed || res.writableEnded || !res.writable) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
22
43
|
export class Ctr {
|
|
23
44
|
/** --- 路由参数序列数组 --- */
|
|
24
45
|
_param = [];
|
|
@@ -69,7 +90,13 @@ export class Ctr {
|
|
|
69
90
|
}
|
|
70
91
|
/** --- 当前用户连接是否还在连接中 --- */
|
|
71
92
|
get _isAvail() {
|
|
72
|
-
|
|
93
|
+
if (this._socket) {
|
|
94
|
+
return this._socket.writable;
|
|
95
|
+
}
|
|
96
|
+
if (!this._res) {
|
|
97
|
+
return !this._req.socket.destroyed && this._req.socket.writable;
|
|
98
|
+
}
|
|
99
|
+
return isHttpRequestAvailable(this._req, this._res);
|
|
73
100
|
}
|
|
74
101
|
/** --- timeout 的 timer --- */
|
|
75
102
|
_timer;
|
|
@@ -237,7 +264,7 @@ export class Ctr {
|
|
|
237
264
|
/**
|
|
238
265
|
* --- 加载 React 全页面进行 SSR 渲染,组件需渲染完整 HTML 文档(含 html/head/body),无需 EJS ---
|
|
239
266
|
* --- 框架自动注入 props:_urlBase/_urlFull/_urlStc/_staticVer/_staticPath/_staticPathFull ---
|
|
240
|
-
* --- 多语言:自动注入 _locale(当前语言名)和 _localeData
|
|
267
|
+
* --- 多语言:自动注入 _locale(当前语言名)和 _localeData(当前语种已载语言包的合并键值对) ---
|
|
241
268
|
* --- 组件内创建:const l = (key: string, ...args: string[]): string => { let i = 0; return (_localeData[key] ?? key).replace(/\?/g, () => args[i++] ?? ''); }; ---
|
|
242
269
|
* @param path 页面组件路径(相对于 stc/ 目录,不含扩展名,tsx 编译后的 .js)
|
|
243
270
|
* @param props 传入组件的 props,框架常量自动合并,整体序列化为内联 JSON 供客户端水合复用
|
|
@@ -245,13 +272,23 @@ export class Ctr {
|
|
|
245
272
|
*/
|
|
246
273
|
async _loadReactPage(path, props = {}, opt = {}) {
|
|
247
274
|
// --- 约定:传入路径不含 .page 后缀,框架自动补全(对应 build 命令的 *.page.tsx 约定)---
|
|
248
|
-
// ---
|
|
275
|
+
// --- 服务端读取 tsc 生成的组件 JS,浏览器读取 kebab build 生成的 bundle ---
|
|
249
276
|
const componentPath = this._config.const.rootPath + 'stc/' + path + '.page.js';
|
|
250
277
|
if (!await lFs.isFile(componentPath)) {
|
|
251
278
|
lCore.debug(`[CTR][_loadReactPage] componentPath ${componentPath} is not a file`);
|
|
252
279
|
lCore.log(this, `[CTR][_loadReactPage] componentPath ${componentPath} is not a file`, '-error');
|
|
253
280
|
return '';
|
|
254
281
|
}
|
|
282
|
+
const bundlePath = this._config.const.rootPath + 'stc/' + path + '.page.bundle.js';
|
|
283
|
+
if ((opt.hydrate !== false) && !await lFs.isFile(bundlePath)) {
|
|
284
|
+
/** --- 完整错误仅写入服务端日志,避免向客户端暴露文件系统路径 --- */
|
|
285
|
+
const message = `[CTR][_loadReactPage] bundlePath ${bundlePath} is not a file; ` +
|
|
286
|
+
`run "npx kebab build -d ${this._config.const.rootPath}stc"`;
|
|
287
|
+
lCore.debug(message);
|
|
288
|
+
lCore.log(this, message, '-error');
|
|
289
|
+
this._httpCode = 500;
|
|
290
|
+
return '<h1>500 Server Error</h1><hr>Kebab';
|
|
291
|
+
}
|
|
255
292
|
try {
|
|
256
293
|
const reactDomServer = await import('react-dom/server');
|
|
257
294
|
const importPath = componentPath.startsWith('/')
|
|
@@ -260,11 +297,8 @@ export class Ctr {
|
|
|
260
297
|
const mod = await import(importPath);
|
|
261
298
|
const component = mod.default;
|
|
262
299
|
const react = await import('react');
|
|
263
|
-
// ---
|
|
264
|
-
const localeData = {};
|
|
265
|
-
for (const pkg in this._localeData) {
|
|
266
|
-
Object.assign(localeData, this._localeData[pkg]);
|
|
267
|
-
}
|
|
300
|
+
// --- 语言包数据:当前语种的各命名包已由 _loadLocale 合并 ---
|
|
301
|
+
const localeData = { ...(this._localeData[this._locale] ?? {}) };
|
|
268
302
|
// --- 把框架常量合并进 props,与 _loadView 行为一致 ---
|
|
269
303
|
const staticPath = opt.staticPath ?? this._config.set.staticPath;
|
|
270
304
|
const fullProps = {
|
|
@@ -278,67 +312,22 @@ export class Ctr {
|
|
|
278
312
|
'_locale': this._locale,
|
|
279
313
|
'_localeData': localeData,
|
|
280
314
|
};
|
|
315
|
+
if (opt.router === 'browser') {
|
|
316
|
+
// --- BrowserRouter 的 basename 同时用于服务端 StaticRouter 和客户端水合 ---
|
|
317
|
+
const base = opt.routerBase ?? '';
|
|
318
|
+
const routerBase = this._config.const.urlBase + base.replace(/^\//, '');
|
|
319
|
+
fullProps['_routerBase'] = routerBase.replace(/\/$/, '');
|
|
320
|
+
}
|
|
281
321
|
// --- 框架自动注入的 HTML 片段,用户组件无需手动渲染 ---
|
|
282
|
-
let headInject = '';
|
|
283
322
|
let bodyInject = '';
|
|
284
323
|
if (opt.hydrate !== false) {
|
|
285
|
-
const reactVer = opt.reactVer ?? '19';
|
|
286
|
-
const esm = 'https://esm.sh/';
|
|
287
|
-
// --- 检查是否有 npx kebab build 生成的自包含预构建包 ---
|
|
288
|
-
const bundlePath = this._config.const.rootPath + 'stc/' + path + '.page.bundle.js';
|
|
289
|
-
const hasBundle = await lFs.isFile(bundlePath);
|
|
290
|
-
if (opt.router === 'browser') {
|
|
291
|
-
// --- BrowserRouter 模式:_routerBase 注入 props,供水合脚本读取 ---
|
|
292
|
-
const base = opt.routerBase ?? '';
|
|
293
|
-
const routerBase = this._config.const.urlBase + base.replace(/^\//, '');
|
|
294
|
-
fullProps['_routerBase'] = routerBase.replace(/\/$/, '');
|
|
295
|
-
}
|
|
296
324
|
// --- propsJson 在渲染前序列化,框架直接注入 HTML,组件无需手动渲染 ---
|
|
297
325
|
const propsJson = lText.stringifyJson(fullProps).replace(/<\/script>/gi, '<\\/script>');
|
|
298
|
-
|
|
299
|
-
if (hasBundle) {
|
|
300
|
-
// --- bundle 模式:bundle 自包含 React + 水合逻辑,无需 import map ---
|
|
301
|
-
const clientUrl = `${staticPath}${path}.page.bundle.js?v=${this._config.set.staticVer}`;
|
|
302
|
-
hydrateScript = `import'${clientUrl}';`;
|
|
303
|
-
}
|
|
304
|
-
else {
|
|
305
|
-
// --- 开发模式(tsc 编译 .js):通过 esm.sh import map 解析 bare import ---
|
|
306
|
-
const clientUrl = `${staticPath}${path}.page.js?v=${this._config.set.staticVer}`;
|
|
307
|
-
// --- 内置 import map 条目(React 生态核心包)---
|
|
308
|
-
const builtinImports = {
|
|
309
|
-
'react': `${esm}react@${reactVer}`,
|
|
310
|
-
'react-dom': `${esm}react-dom@${reactVer}`,
|
|
311
|
-
'react-dom/client': `${esm}react-dom@${reactVer}/client`,
|
|
312
|
-
'react/jsx-runtime': `${esm}react@${reactVer}/jsx-runtime`,
|
|
313
|
-
'react-router-dom': `${esm}react-router-dom@7?external=react,react-dom`,
|
|
314
|
-
};
|
|
315
|
-
// --- 自动扫描入口 JS 及其相对引用,收集所有第三方 bare specifier ---
|
|
316
|
-
const scannedFiles = new Set();
|
|
317
|
-
const extraImports = new Set();
|
|
318
|
-
await this._scanImports(componentPath, scannedFiles, extraImports, builtinImports);
|
|
319
|
-
// --- 第三方包统一通过 esm.sh 解析,external react/react-dom 避免重复加载 ---
|
|
320
|
-
for (const pkg of extraImports) {
|
|
321
|
-
builtinImports[pkg] = `${esm}${pkg}?external=react,react-dom`;
|
|
322
|
-
}
|
|
323
|
-
// --- import map 注入到 </head> 前 ---
|
|
324
|
-
headInject = `<script type="importmap">${lText.stringifyJson({ 'imports': builtinImports })}</script>`;
|
|
325
|
-
// --- BrowserRouter 模式多一段 Router 导入与包裹层 ---
|
|
326
|
-
const routerImport = opt.router === 'browser' ? `import{BrowserRouter}from'react-router-dom';` : '';
|
|
327
|
-
const routerCreate = opt.router === 'browser'
|
|
328
|
-
? `createElement(BrowserRouter,{basename:p._routerBase},createElement(App,p))`
|
|
329
|
-
: `createElement(App,p)`;
|
|
330
|
-
hydrateScript =
|
|
331
|
-
`import{hydrateRoot}from'react-dom/client';` +
|
|
332
|
-
`import{createElement}from'react';` +
|
|
333
|
-
routerImport +
|
|
334
|
-
`import App from'${clientUrl}';` +
|
|
335
|
-
`const p=JSON.parse(document.getElementById('__kebab_props__').textContent);` +
|
|
336
|
-
`hydrateRoot(document,${routerCreate});`;
|
|
337
|
-
}
|
|
326
|
+
const clientUrl = `${staticPath}${path}.page.bundle.js?v=${this._config.set.staticVer}`;
|
|
338
327
|
// --- props JSON + 水合脚本注入到 </body> 前 ---
|
|
339
328
|
bodyInject =
|
|
340
329
|
`<script id="__kebab_props__" type="application/json">${propsJson}</script>` +
|
|
341
|
-
`<script type="module"
|
|
330
|
+
`<script type="module">import'${clientUrl}';</script>`;
|
|
342
331
|
}
|
|
343
332
|
// --- BrowserRouter 模式:服务端用 StaticRouter 渲染,与客户端的 BrowserRouter 等价 ---
|
|
344
333
|
// --- component 来自动态 import,TypeScript 无法精确推断,需要明确限定 element 类型 ---
|
|
@@ -352,10 +341,9 @@ export class Ctr {
|
|
|
352
341
|
'basename': fullProps['_routerBase'],
|
|
353
342
|
}, react.createElement(component, fullProps));
|
|
354
343
|
}
|
|
355
|
-
// --- 框架将
|
|
344
|
+
// --- 框架将 props JSON + 水合 bundle 注入到 </body> 前 ---
|
|
356
345
|
let html = '<!DOCTYPE html>' + reactDomServer.renderToString(element);
|
|
357
346
|
if (opt.hydrate !== false) {
|
|
358
|
-
html = html.replace('</head>', headInject + '</head>');
|
|
359
347
|
html = html.replace('</body>', bodyInject + '</body>');
|
|
360
348
|
}
|
|
361
349
|
return html;
|
|
@@ -366,38 +354,6 @@ export class Ctr {
|
|
|
366
354
|
return '';
|
|
367
355
|
}
|
|
368
356
|
}
|
|
369
|
-
/**
|
|
370
|
-
* --- 递归扫描 JS 文件中的 import 语句,收集第三方 bare specifier ---
|
|
371
|
-
* @param filePath 当前要扫描的文件绝对路径
|
|
372
|
-
* @param scannedFiles 已扫描文件集(去重用)
|
|
373
|
-
* @param extraImports 收集到的第三方包名集合
|
|
374
|
-
* @param builtinImports 内置 import map,已有条目不重复添加
|
|
375
|
-
*/
|
|
376
|
-
async _scanImports(filePath, scannedFiles, extraImports, builtinImports) {
|
|
377
|
-
if (scannedFiles.has(filePath)) {
|
|
378
|
-
return;
|
|
379
|
-
}
|
|
380
|
-
scannedFiles.add(filePath);
|
|
381
|
-
const src = await lFs.getContent(filePath, 'utf8');
|
|
382
|
-
if (!src) {
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
const re = /\bfrom\s*['"]([^'"]+)['"]/g;
|
|
386
|
-
let m;
|
|
387
|
-
while ((m = re.exec(src)) !== null) {
|
|
388
|
-
const spec = m[1];
|
|
389
|
-
if (spec.startsWith('./') || spec.startsWith('../')) {
|
|
390
|
-
// --- 相对引用:解析为绝对路径后递归扫描 ---
|
|
391
|
-
const dir = filePath.substring(0, filePath.lastIndexOf('/') + 1);
|
|
392
|
-
const resolved = new URL(spec, 'file://' + dir).pathname;
|
|
393
|
-
await this._scanImports(resolved, scannedFiles, extraImports, builtinImports);
|
|
394
|
-
}
|
|
395
|
-
else if (!spec.startsWith('/') && !spec.startsWith('http') && !(spec in builtinImports)) {
|
|
396
|
-
// --- 第三方 bare specifier:加入 import map ---
|
|
397
|
-
extraImports.add(spec);
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
357
|
/**
|
|
402
358
|
* --- 设置校验错误返回值 ---
|
|
403
359
|
* @param rtn 返回值数组
|
package/sys/route.d.ts
CHANGED
|
@@ -51,12 +51,13 @@ export declare function waitCtr(cctr: sCtr.Ctr): Promise<void>;
|
|
|
51
51
|
/**
|
|
52
52
|
* --- 获取 post 对象(通常已自动获取),如果是文件上传(formdata)的情况则不获取 ---
|
|
53
53
|
* @param req 请求对象
|
|
54
|
+
* @returns 解析后的 POST 数据,请求传输中断时返回 false
|
|
54
55
|
*/
|
|
55
56
|
export declare function getPost(req: http2.Http2ServerRequest | http.IncomingMessage): Promise<{
|
|
56
57
|
'input': string;
|
|
57
58
|
'raw': Record<string, any>;
|
|
58
59
|
'post': Record<string, any>;
|
|
59
|
-
}>;
|
|
60
|
+
} | false>;
|
|
60
61
|
/**
|
|
61
62
|
* --- 获取 formdata 的 post ---
|
|
62
63
|
* @param req 请求头
|
package/sys/route.js
CHANGED
|
@@ -453,6 +453,9 @@ export async function run(data) {
|
|
|
453
453
|
}
|
|
454
454
|
if (reqStartRtn === 1) {
|
|
455
455
|
const rawPost = await getPost(data.req);
|
|
456
|
+
if (!rawPost) {
|
|
457
|
+
return true;
|
|
458
|
+
}
|
|
456
459
|
// --- 原始 POST ---
|
|
457
460
|
middle.setPrototype('_rawPost', rawPost.raw);
|
|
458
461
|
// --- 原始 input ---
|
|
@@ -844,6 +847,7 @@ export async function waitCtr(cctr) {
|
|
|
844
847
|
/**
|
|
845
848
|
* --- 获取 post 对象(通常已自动获取),如果是文件上传(formdata)的情况则不获取 ---
|
|
846
849
|
* @param req 请求对象
|
|
850
|
+
* @returns 解析后的 POST 数据,请求传输中断时返回 false
|
|
847
851
|
*/
|
|
848
852
|
export function getPost(req) {
|
|
849
853
|
return new Promise(function (resolve) {
|
|
@@ -861,7 +865,28 @@ export function getPost(req) {
|
|
|
861
865
|
const maxPostSize = 50 * 1024 * 1024;
|
|
862
866
|
let buffer = Buffer.from('');
|
|
863
867
|
let overflow = false;
|
|
864
|
-
|
|
868
|
+
let finished = false;
|
|
869
|
+
let onData;
|
|
870
|
+
let onEnd;
|
|
871
|
+
let onError;
|
|
872
|
+
let onAborted;
|
|
873
|
+
let onClose;
|
|
874
|
+
const cleanup = () => {
|
|
875
|
+
req.off('data', onData);
|
|
876
|
+
req.off('end', onEnd);
|
|
877
|
+
req.off('error', onError);
|
|
878
|
+
req.off('aborted', onAborted);
|
|
879
|
+
req.off('close', onClose);
|
|
880
|
+
};
|
|
881
|
+
const finish = (result) => {
|
|
882
|
+
if (finished) {
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
finished = true;
|
|
886
|
+
cleanup();
|
|
887
|
+
resolve(result);
|
|
888
|
+
};
|
|
889
|
+
onData = (chunk) => {
|
|
865
890
|
if (overflow) {
|
|
866
891
|
return;
|
|
867
892
|
}
|
|
@@ -871,10 +896,10 @@ export function getPost(req) {
|
|
|
871
896
|
return;
|
|
872
897
|
}
|
|
873
898
|
buffer = Buffer.concat([buffer, chunk], buffer.length + chunk.length);
|
|
874
|
-
}
|
|
875
|
-
|
|
899
|
+
};
|
|
900
|
+
onEnd = () => {
|
|
876
901
|
if (overflow) {
|
|
877
|
-
|
|
902
|
+
finish({
|
|
878
903
|
'input': '',
|
|
879
904
|
'raw': {},
|
|
880
905
|
'post': {},
|
|
@@ -883,7 +908,7 @@ export function getPost(req) {
|
|
|
883
908
|
}
|
|
884
909
|
const s = buffer.toString();
|
|
885
910
|
if (!s) {
|
|
886
|
-
|
|
911
|
+
finish({
|
|
887
912
|
'input': '',
|
|
888
913
|
'raw': {},
|
|
889
914
|
'post': {},
|
|
@@ -894,14 +919,14 @@ export function getPost(req) {
|
|
|
894
919
|
if (ct.includes('json')) {
|
|
895
920
|
try {
|
|
896
921
|
const raw = lText.parseJson(s);
|
|
897
|
-
|
|
922
|
+
finish({
|
|
898
923
|
'input': s,
|
|
899
924
|
'raw': raw,
|
|
900
925
|
'post': lText.trimJson(raw),
|
|
901
926
|
});
|
|
902
927
|
}
|
|
903
928
|
catch {
|
|
904
|
-
|
|
929
|
+
finish({
|
|
905
930
|
'input': '',
|
|
906
931
|
'raw': {},
|
|
907
932
|
'post': {},
|
|
@@ -910,12 +935,26 @@ export function getPost(req) {
|
|
|
910
935
|
return;
|
|
911
936
|
}
|
|
912
937
|
const raw = lText.queryParse(s);
|
|
913
|
-
|
|
938
|
+
finish({
|
|
914
939
|
'input': s,
|
|
915
940
|
'raw': raw,
|
|
916
941
|
'post': lText.trimJson(raw),
|
|
917
942
|
});
|
|
918
|
-
}
|
|
943
|
+
};
|
|
944
|
+
onError = () => {
|
|
945
|
+
finish(false);
|
|
946
|
+
};
|
|
947
|
+
onAborted = () => {
|
|
948
|
+
finish(false);
|
|
949
|
+
};
|
|
950
|
+
onClose = () => {
|
|
951
|
+
finish(false);
|
|
952
|
+
};
|
|
953
|
+
req.on('data', onData);
|
|
954
|
+
req.on('end', onEnd);
|
|
955
|
+
req.on('error', onError);
|
|
956
|
+
req.on('aborted', onAborted);
|
|
957
|
+
req.on('close', onClose);
|
|
919
958
|
});
|
|
920
959
|
}
|
|
921
960
|
/**
|
package/www/example/ctr/test.js
CHANGED
|
@@ -2021,8 +2021,8 @@ error: ${JSON.stringify(res.error)}
|
|
|
2021
2021
|
}
|
|
2022
2022
|
async undiciPost() {
|
|
2023
2023
|
const echo = [];
|
|
2024
|
-
const res = await lUndici.post(this._internalUrl + 'test/
|
|
2025
|
-
echo.push(`<pre>lUndici.post('${this._internalUrl}test/
|
|
2024
|
+
const res = await lUndici.post(this._internalUrl + 'test/undiciPost1', { 'a': '1', 'b': '2', 'c': ['1', '2', '3'] });
|
|
2025
|
+
echo.push(`<pre>lUndici.post('${this._internalUrl}test/undiciPost1', { 'a': '1', 'b': '2', 'c': ['1', '2', '3'] });</pre>
|
|
2026
2026
|
headers: <pre>${JSON.stringify(res.headers, null, 4)}</pre>
|
|
2027
2027
|
content: <pre>${(await res.getContent())?.toString() ?? 'null'}</pre>
|
|
2028
2028
|
error: ${JSON.stringify(res.error)}`);
|
|
@@ -2033,8 +2033,8 @@ error: ${JSON.stringify(res.error)}`);
|
|
|
2033
2033
|
}
|
|
2034
2034
|
async undiciPostString() {
|
|
2035
2035
|
const echo = [];
|
|
2036
|
-
const res = await lUndici.post(this._internalUrl + 'test/
|
|
2037
|
-
echo.push(`<pre>lUndici.post('${this._internalUrl}test/
|
|
2036
|
+
const res = await lUndici.post(this._internalUrl + 'test/undiciPostString1', 'HeiHei');
|
|
2037
|
+
echo.push(`<pre>lUndici.post('${this._internalUrl}test/undiciPostString1', 'HeiHei');</pre>
|
|
2038
2038
|
headers: <pre>${JSON.stringify(res.headers, null, 4)}</pre>
|
|
2039
2039
|
content: <pre>${(await res.getContent())?.toString() ?? 'null'}</pre>
|
|
2040
2040
|
error: ${JSON.stringify(res.error)}`);
|
|
@@ -2045,8 +2045,8 @@ error: ${JSON.stringify(res.error)}`);
|
|
|
2045
2045
|
}
|
|
2046
2046
|
async undiciOpen() {
|
|
2047
2047
|
const echo = [];
|
|
2048
|
-
const res = await lUndici.open(this._internalUrl + 'test/
|
|
2049
|
-
echo.push(`<pre>lUndici.open('${this._internalUrl}test/
|
|
2048
|
+
const res = await lUndici.open(this._internalUrl + 'test/undiciPost1').post().data({ 'a': '2', 'b': '0', 'c': ['0', '1', '3'] }).request();
|
|
2049
|
+
echo.push(`<pre>lUndici.open('${this._internalUrl}test/undiciPost1').post().data({ 'a': '2', 'b': '0', 'c': ['0', '1', '3'] }).request();</pre>
|
|
2050
2050
|
headers: <pre>${JSON.stringify(res.headers, null, 4)}</pre>
|
|
2051
2051
|
content: <pre>${(await res.getContent())?.toString() ?? 'null'}</pre>
|
|
2052
2052
|
error: ${JSON.stringify(res.error)}`);
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* 1. 这个 .page.tsx 文件(组件 + IProps)
|
|
6
6
|
* 2. 一个 <link> 标签引入 CSS(如果不需要样式可省略)
|
|
7
7
|
* 3. 在 Ctr 方法里调用 _loadReactPage('view/hello', props)
|
|
8
|
-
*
|
|
8
|
+
* 4. 执行 node ./source/main build -d source/www/example/stc 生成客户端水合 bundle
|
|
9
|
+
* 其他一切由框架自动处理:SSR 渲染、props 注入、客户端水合。
|
|
9
10
|
*/
|
|
10
11
|
import { type ReactElement } from 'react';
|
|
11
12
|
interface IProps {
|
|
@@ -6,7 +6,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
6
6
|
* 1. 这个 .page.tsx 文件(组件 + IProps)
|
|
7
7
|
* 2. 一个 <link> 标签引入 CSS(如果不需要样式可省略)
|
|
8
8
|
* 3. 在 Ctr 方法里调用 _loadReactPage('view/hello', props)
|
|
9
|
-
*
|
|
9
|
+
* 4. 执行 node ./source/main build -d source/www/example/stc 生成客户端水合 bundle
|
|
10
|
+
* 其他一切由框架自动处理:SSR 渲染、props 注入、客户端水合。
|
|
10
11
|
*/
|
|
11
12
|
import { useState } from 'react';
|
|
12
13
|
export default function HelloPage({ _urlStc, _staticVer, greeting }) {
|