@maiyunnet/kebab 9.18.2 → 9.19.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/index.d.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  * --- 本文件用来定义每个目录实体地址的常量 ---
6
6
  */
7
7
  /** --- 当前系统版本号 --- */
8
- export declare const VER = "9.18.2";
8
+ export declare const VER = "9.19.1";
9
9
  /** --- 框架根目录,以 / 结尾 --- */
10
10
  export declare const ROOT_PATH: string;
11
11
  /** --- 框架的 LIB,以 / 结尾 --- */
package/index.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * --- 本文件用来定义每个目录实体地址的常量 ---
7
7
  */
8
8
  /** --- 当前系统版本号 --- */
9
- export const VER = '9.18.2';
9
+ export const VER = '9.19.1';
10
10
  // --- 服务端用的路径 ---
11
11
  const imu = decodeURIComponent(import.meta.url).replace('file://', '').replace(/^\/(\w:)/, '$1');
12
12
  /** --- /xxx/xxx --- */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maiyunnet/kebab",
3
- "version": "9.18.2",
3
+ "version": "9.19.1",
4
4
  "description": "Simple, easy-to-use, and fully-featured Node.js framework that is ready-to-use out of the box.",
5
5
  "type": "module",
6
6
  "keywords": [
package/sys/ctr.d.ts CHANGED
@@ -19,6 +19,8 @@ export declare function clearLocaleData(): void;
19
19
  export type TValibotResponse<TIssue extends v.BaseIssue<unknown>> = kebab.Json[] | ((issues: [TIssue, ...TIssue[]]) => kebab.Json[]);
20
20
  /** --- 在 issue 路径完整后,根据 Kebab 生成的语言键翻译消息 --- */
21
21
  export type TValibotTranslate = (key: string, issue: v.BaseIssue<unknown>) => string;
22
+ /** --- HTTP 重定向状态码 --- */
23
+ export type TRedirectHttpCode = 301 | 302 | 303 | 307 | 308;
22
24
  /** --- Valibot 同步校验选项 --- */
23
25
  export interface IValibotOptions<TIssue extends v.BaseIssue<unknown>> {
24
26
  /** --- 传给 Valibot 的校验配置 --- */
@@ -58,6 +60,21 @@ export type TValibotResult<TSchema extends v.GenericSchema, TIssue extends v.Bas
58
60
  readonly issues: [TIssue, ...TIssue[]];
59
61
  readonly response: kebab.Json[];
60
62
  };
63
+ /** --- React SSR 页面选项 --- */
64
+ export interface IReactPageOptions {
65
+ /** --- 是否注入客户端水合 bundle,默认 true;设为 false 时仅输出服务端渲染的 HTML --- */
66
+ 'hydrate'?: boolean;
67
+ /**
68
+ * --- 路由模式,不传则不注入任何 Router,组件自行管理路由(如 MemoryRouter)或无路由 ---
69
+ * --- 'browser':服务端用 StaticRouter,客户端用 BrowserRouter,地址栏与路由联动 ---
70
+ * --- 组件本身只需使用 Routes/Route/Link 等,不要包含任何 Router 包裹层 ---
71
+ */
72
+ 'router'?: 'browser';
73
+ /** --- BrowserRouter 的 basename,相对于 urlBase,默认空字符串 --- */
74
+ 'routerBase'?: string;
75
+ /** --- 静态资源基础路径,覆盖 config.set.staticPath(支持 CDN) --- */
76
+ 'staticPath'?: string;
77
+ }
61
78
  /**
62
79
  * --- 判断当前 HTTP 请求是否仍可响应 ---
63
80
  * @param req 请求对象
@@ -213,42 +230,20 @@ export declare class Ctr {
213
230
  /**
214
231
  * --- 加载 React 全页面进行 SSR 渲染,组件需渲染完整 HTML 文档(含 html/head/body),无需 EJS ---
215
232
  * --- 框架自动注入 props:_urlBase/_urlFull/_urlStc/_staticVer/_staticPath/_staticPathFull ---
216
- * --- 多语言:自动注入 _locale(当前语言名)和 _localeData(已载语言包的合并键值对) ---
233
+ * --- 多语言:自动注入 _locale(当前语言名)和 _localeData(当前语种已载语言包的合并键值对) ---
217
234
  * --- 组件内创建:const l = (key: string, ...args: string[]): string => { let i = 0; return (_localeData[key] ?? key).replace(/\?/g, () => args[i++] ?? ''); }; ---
218
235
  * @param path 页面组件路径(相对于 stc/ 目录,不含扩展名,tsx 编译后的 .js)
219
236
  * @param props 传入组件的 props,框架常量自动合并,整体序列化为内联 JSON 供客户端水合复用
220
237
  * @param opt 可选配置
221
238
  */
222
- protected _loadReactPage(path: string, props?: Record<string, kebab.Json>, opt?: {
223
- /** --- 是否注入客户端水合脚本(import map + hydrateRoot),默认 true --- */
224
- 'hydrate'?: boolean;
225
- /** --- react/react-dom/react-router-dom 版本号,用于 esm.sh CDN,默认 19 --- */
226
- 'reactVer'?: string;
227
- /**
228
- * --- 路由模式,不传则不注入任何 Router,组件自行管理路由(如 MemoryRouter)或无路由 ---
229
- * --- 'browser':服务端用 StaticRouter,客户端用 BrowserRouter,地址栏与路由联动 ---
230
- * --- 组件本身只需使用 Routes/Route/Link 等,不要包含任何 Router 包裹层 ---
231
- */
232
- 'router'?: 'browser';
233
- /**
234
- * --- BrowserRouter 的 basename,相对于 urlBase,默认空字符串 ---
235
- * --- 例如组件挂载在 /test/react-router-page,则填 'test/react-router-page' ---
236
- */
237
- 'routerBase'?: string;
238
- /**
239
- * --- 静态资源基础路径,覆盖 config.set.staticPath,用于指定 CDN 或自定义路径 ---
240
- * --- 影响 _staticPath prop 以及水合脚本中 JS 文件的 URL 前缀 ---
241
- */
242
- 'staticPath'?: string;
243
- }): Promise<string>;
244
- /**
245
- * --- 递归扫描 JS 文件中的 import 语句,收集第三方 bare specifier ---
246
- * @param filePath 当前要扫描的文件绝对路径
247
- * @param scannedFiles 已扫描文件集(去重用)
248
- * @param extraImports 收集到的第三方包名集合
249
- * @param builtinImports 内置 import map,已有条目不重复添加
250
- */
251
- private _scanImports;
239
+ protected _loadReactPage(path: string, props?: Record<string, kebab.Json>, opt?: IReactPageOptions): Promise<string>;
240
+ /**
241
+ * --- 记录 React 页面错误并返回脱敏响应 ---
242
+ * @param message 调试信息
243
+ * @param detail 日志详情
244
+ * @returns 脱敏后的 500 页面
245
+ */
246
+ private _reactPageError;
252
247
  /**
253
248
  * --- 设置校验错误返回值 ---
254
249
  * @param rtn 返回值数组
@@ -343,10 +338,11 @@ export declare class Ctr {
343
338
  */
344
339
  protected _loadData<T>(path: string): Promise<T | false | null>;
345
340
  /**
346
- * --- 跳转(302临时跳转),支持相对本项目根路径的路径或绝对路径 ---
341
+ * --- 跳转,支持相对本项目根路径的路径或绝对路径 ---
347
342
  * @param location 相对或绝对网址
343
+ * @param httpCode HTTP 重定向状态码,默认 302
348
344
  */
349
- protected _location(location: string): false;
345
+ protected _location(location: string, httpCode?: TRedirectHttpCode): false;
350
346
  /**
351
347
  * --- 开启 Session ---
352
348
  * @param link Kv 或 Db 实例
package/sys/ctr.js CHANGED
@@ -264,35 +264,35 @@ export class Ctr {
264
264
  /**
265
265
  * --- 加载 React 全页面进行 SSR 渲染,组件需渲染完整 HTML 文档(含 html/head/body),无需 EJS ---
266
266
  * --- 框架自动注入 props:_urlBase/_urlFull/_urlStc/_staticVer/_staticPath/_staticPathFull ---
267
- * --- 多语言:自动注入 _locale(当前语言名)和 _localeData(已载语言包的合并键值对) ---
267
+ * --- 多语言:自动注入 _locale(当前语言名)和 _localeData(当前语种已载语言包的合并键值对) ---
268
268
  * --- 组件内创建:const l = (key: string, ...args: string[]): string => { let i = 0; return (_localeData[key] ?? key).replace(/\?/g, () => args[i++] ?? ''); }; ---
269
269
  * @param path 页面组件路径(相对于 stc/ 目录,不含扩展名,tsx 编译后的 .js)
270
270
  * @param props 传入组件的 props,框架常量自动合并,整体序列化为内联 JSON 供客户端水合复用
271
271
  * @param opt 可选配置
272
272
  */
273
273
  async _loadReactPage(path, props = {}, opt = {}) {
274
- // --- 约定:传入路径不含 .page 后缀,框架自动补全(对应 build 命令的 *.page.tsx 约定)---
275
- // --- 组件 JS 从 stc 目录读取,浏览器同样通过 staticPath(支持 CDN)下载 ---
276
- const componentPath = this._config.const.rootPath + 'stc/' + path + '.page.js';
274
+ // --- 校验服务端组件与客户端水合产物 ---
275
+ const hydrate = opt.hydrate !== false;
276
+ const pagePath = `${this._config.const.rootPath}stc/${path}.page`;
277
+ const componentPath = `${pagePath}.js`;
277
278
  if (!await lFs.isFile(componentPath)) {
278
- lCore.debug(`[CTR][_loadReactPage] componentPath ${componentPath} is not a file`);
279
- lCore.log(this, `[CTR][_loadReactPage] componentPath ${componentPath} is not a file`, '-error');
280
- return '';
279
+ return this._reactPageError(`componentPath ${componentPath} is not a file`);
280
+ }
281
+ const bundlePath = `${pagePath}.bundle.js`;
282
+ if (hydrate && !await lFs.isFile(bundlePath)) {
283
+ return this._reactPageError(`bundlePath ${bundlePath} is not a file; run "npx kebab build -d ${this._config.const.rootPath}stc"`);
281
284
  }
282
285
  try {
283
- const reactDomServer = await import('react-dom/server');
286
+ // --- 并行加载 SSR 依赖与页面组件 ---
284
287
  const importPath = componentPath.startsWith('/')
285
288
  ? componentPath
286
289
  : `file:///${componentPath.replace(/\\/g, '/')}`;
287
- const mod = await import(importPath);
288
- const component = mod.default;
289
- const react = await import('react');
290
- // --- 语言包数据:合并所有已加载包的键值,供组件使用 _localeData 实现多语言 ---
291
- const localeData = {};
292
- for (const pkg in this._localeData) {
293
- Object.assign(localeData, this._localeData[pkg]);
294
- }
295
- // --- 把框架常量合并进 props,与 _loadView 行为一致 ---
290
+ const [react, reactDomServer, page] = await Promise.all([
291
+ import('react'),
292
+ import('react-dom/server'),
293
+ import(importPath),
294
+ ]);
295
+ // --- 合并框架 Props 与路由配置 ---
296
296
  const staticPath = opt.staticPath ?? this._config.set.staticPath;
297
297
  const fullProps = {
298
298
  ...props,
@@ -303,127 +303,49 @@ export class Ctr {
303
303
  '_staticPath': staticPath,
304
304
  '_staticPathFull': this._config.set.staticPathFull,
305
305
  '_locale': this._locale,
306
- '_localeData': localeData,
306
+ '_localeData': { ...(this._localeData[this._locale] ?? {}) },
307
307
  };
308
- // --- 框架自动注入的 HTML 片段,用户组件无需手动渲染 ---
309
- let headInject = '';
310
- let bodyInject = '';
311
- if (opt.hydrate !== false) {
312
- const reactVer = opt.reactVer ?? '19';
313
- const esm = 'https://esm.sh/';
314
- // --- 检查是否有 npx kebab build 生成的自包含预构建包 ---
315
- const bundlePath = this._config.const.rootPath + 'stc/' + path + '.page.bundle.js';
316
- const hasBundle = await lFs.isFile(bundlePath);
317
- if (opt.router === 'browser') {
318
- // --- BrowserRouter 模式:_routerBase 注入 props,供水合脚本读取 ---
319
- const base = opt.routerBase ?? '';
320
- const routerBase = this._config.const.urlBase + base.replace(/^\//, '');
321
- fullProps['_routerBase'] = routerBase.replace(/\/$/, '');
322
- }
323
- // --- propsJson 在渲染前序列化,框架直接注入 HTML,组件无需手动渲染 ---
324
- const propsJson = lText.stringifyJson(fullProps).replace(/<\/script>/gi, '<\\/script>');
325
- let hydrateScript;
326
- if (hasBundle) {
327
- // --- bundle 模式:bundle 自包含 React + 水合逻辑,无需 import map ---
328
- const clientUrl = `${staticPath}${path}.page.bundle.js?v=${this._config.set.staticVer}`;
329
- hydrateScript = `import'${clientUrl}';`;
330
- }
331
- else {
332
- // --- 开发模式(tsc 编译 .js):通过 esm.sh import map 解析 bare import ---
333
- const clientUrl = `${staticPath}${path}.page.js?v=${this._config.set.staticVer}`;
334
- // --- 内置 import map 条目(React 生态核心包)---
335
- const builtinImports = {
336
- 'react': `${esm}react@${reactVer}`,
337
- 'react-dom': `${esm}react-dom@${reactVer}`,
338
- 'react-dom/client': `${esm}react-dom@${reactVer}/client`,
339
- 'react/jsx-runtime': `${esm}react@${reactVer}/jsx-runtime`,
340
- 'react-router-dom': `${esm}react-router-dom@7?external=react,react-dom`,
341
- };
342
- // --- 自动扫描入口 JS 及其相对引用,收集所有第三方 bare specifier ---
343
- const scannedFiles = new Set();
344
- const extraImports = new Set();
345
- await this._scanImports(componentPath, scannedFiles, extraImports, builtinImports);
346
- // --- 第三方包统一通过 esm.sh 解析,external react/react-dom 避免重复加载 ---
347
- for (const pkg of extraImports) {
348
- builtinImports[pkg] = `${esm}${pkg}?external=react,react-dom`;
349
- }
350
- // --- import map 注入到 </head> 前 ---
351
- headInject = `<script type="importmap">${lText.stringifyJson({ 'imports': builtinImports })}</script>`;
352
- // --- BrowserRouter 模式多一段 Router 导入与包裹层 ---
353
- const routerImport = opt.router === 'browser' ? `import{BrowserRouter}from'react-router-dom';` : '';
354
- const routerCreate = opt.router === 'browser'
355
- ? `createElement(BrowserRouter,{basename:p._routerBase},createElement(App,p))`
356
- : `createElement(App,p)`;
357
- hydrateScript =
358
- `import{hydrateRoot}from'react-dom/client';` +
359
- `import{createElement}from'react';` +
360
- routerImport +
361
- `import App from'${clientUrl}';` +
362
- `const p=JSON.parse(document.getElementById('__kebab_props__').textContent);` +
363
- `hydrateRoot(document,${routerCreate});`;
364
- }
365
- // --- props JSON + 水合脚本注入到 </body> 前 ---
366
- bodyInject =
367
- `<script id="__kebab_props__" type="application/json">${propsJson}</script>` +
368
- `<script type="module">${hydrateScript}</script>`;
308
+ if (opt.router === 'browser') {
309
+ const routerBase = `${this._config.const.urlBase}${(opt.routerBase ?? '').replace(/^\//, '')}`;
310
+ fullProps['_routerBase'] = routerBase.replace(/\/$/, '');
369
311
  }
370
- // --- BrowserRouter 模式:服务端用 StaticRouter 渲染,与客户端的 BrowserRouter 等价 ---
371
- // --- component 来自动态 import,TypeScript 无法精确推断,需要明确限定 element 类型 ---
372
- let element = react.createElement(component, fullProps);
312
+ // --- 创建页面元素,BrowserRouter 模式下由 StaticRouter 包裹 ---
313
+ const component = page.default;
314
+ const pageElement = react.createElement(component, fullProps);
315
+ let element = pageElement;
373
316
  if (opt.router === 'browser') {
374
- // --- StaticRouter 在 react-router-dom v7 中从主包直接导出,无需 /server 子路径 ---
375
- const lReactRouter = await import('react-router-dom');
376
- const reqUrl = this._req.url ?? '/';
377
- element = react.createElement(lReactRouter.StaticRouter, {
378
- 'location': reqUrl,
317
+ const reactRouter = await import('react-router-dom');
318
+ element = react.createElement(reactRouter.StaticRouter, {
319
+ 'location': this._req.url ?? '/',
379
320
  'basename': fullProps['_routerBase'],
380
- }, react.createElement(component, fullProps));
321
+ }, pageElement);
381
322
  }
382
- // --- 框架将 import map 注入 </head> 前,props JSON + 水合脚本注入 </body> 前 ---
383
- let html = '<!DOCTYPE html>' + reactDomServer.renderToString(element);
384
- if (opt.hydrate !== false) {
385
- html = html.replace('</head>', headInject + '</head>');
386
- html = html.replace('</body>', bodyInject + '</body>');
323
+ // --- 渲染完整 HTML,并按需注入客户端水合数据与 bundle ---
324
+ const html = `<!DOCTYPE html>${reactDomServer.renderToString(element)}`;
325
+ if (!hydrate) {
326
+ return html;
387
327
  }
388
- return html;
328
+ const propsJson = lText.stringifyJson(fullProps).replace(/<\/script>/gi, '<\\/script>');
329
+ const clientUrl = `${staticPath}${path}.page.bundle.js?v=${this._config.set.staticVer}`;
330
+ const propsScript = `<script id="__kebab_props__" type="application/json">${propsJson}</script>`;
331
+ const hydrateScript = `<script type="module">import'${clientUrl}';</script>`;
332
+ return html.replace('</body>', `${propsScript}${hydrateScript}</body>`);
389
333
  }
390
334
  catch (e) {
391
- lCore.debug(`[CTR][_loadReactPage] ${e.message ?? ''}`);
392
- lCore.log(this, '[CTR][_loadReactPage] ' + lText.stringifyError(e), '-error');
393
- return '';
335
+ return this._reactPageError(e.message ?? '', lText.stringifyError(e));
394
336
  }
395
337
  }
396
338
  /**
397
- * --- 递归扫描 JS 文件中的 import 语句,收集第三方 bare specifier ---
398
- * @param filePath 当前要扫描的文件绝对路径
399
- * @param scannedFiles 已扫描文件集(去重用)
400
- * @param extraImports 收集到的第三方包名集合
401
- * @param builtinImports 内置 import map,已有条目不重复添加
339
+ * --- 记录 React 页面错误并返回脱敏响应 ---
340
+ * @param message 调试信息
341
+ * @param detail 日志详情
342
+ * @returns 脱敏后的 500 页面
402
343
  */
403
- async _scanImports(filePath, scannedFiles, extraImports, builtinImports) {
404
- if (scannedFiles.has(filePath)) {
405
- return;
406
- }
407
- scannedFiles.add(filePath);
408
- const src = await lFs.getContent(filePath, 'utf8');
409
- if (!src) {
410
- return;
411
- }
412
- const re = /\bfrom\s*['"]([^'"]+)['"]/g;
413
- let m;
414
- while ((m = re.exec(src)) !== null) {
415
- const spec = m[1];
416
- if (spec.startsWith('./') || spec.startsWith('../')) {
417
- // --- 相对引用:解析为绝对路径后递归扫描 ---
418
- const dir = filePath.substring(0, filePath.lastIndexOf('/') + 1);
419
- const resolved = new URL(spec, 'file://' + dir).pathname;
420
- await this._scanImports(resolved, scannedFiles, extraImports, builtinImports);
421
- }
422
- else if (!spec.startsWith('/') && !spec.startsWith('http') && !(spec in builtinImports)) {
423
- // --- 第三方 bare specifier:加入 import map ---
424
- extraImports.add(spec);
425
- }
426
- }
344
+ _reactPageError(message, detail = message) {
345
+ lCore.debug(`[CTR][_loadReactPage] ${message}`);
346
+ lCore.log(this, `[CTR][_loadReactPage] ${detail}`, '-error');
347
+ this._httpCode = 500;
348
+ return '<h1>500 Server Error</h1><hr>Kebab';
427
349
  }
428
350
  /**
429
351
  * --- 设置校验错误返回值 ---
@@ -854,13 +776,15 @@ export class Ctr {
854
776
  return json;
855
777
  }
856
778
  /**
857
- * --- 跳转(302临时跳转),支持相对本项目根路径的路径或绝对路径 ---
779
+ * --- 跳转,支持相对本项目根路径的路径或绝对路径 ---
858
780
  * @param location 相对或绝对网址
781
+ * @param httpCode HTTP 重定向状态码,默认 302
859
782
  */
860
- _location(location) {
783
+ _location(location, httpCode = 302) {
784
+ this._httpCode = httpCode;
861
785
  if (this._res) {
862
786
  this._res.setHeader('location', lText.urlResolve(this._config.const.urlBase, location));
863
- // this._res.writeHead(302); Kebab 中要在最后设置,否则会报错:ERR_HTTP_HEADERS_SENT
787
+ // this._res.writeHead(httpCode); Kebab 中要在最后设置,否则会报错:ERR_HTTP_HEADERS_SENT
864
788
  }
865
789
  return false;
866
790
  }
package/sys/route.js CHANGED
@@ -581,6 +581,8 @@ export async function run(data) {
581
581
  data.res.setHeader('expires', new Date(Date.now() + cacheTTL * 1_000).toUTCString());
582
582
  data.res.setHeader('cache-control', 'max-age=' + cacheTTL.toString());
583
583
  }
584
+ // --- 记录控制器是否显式设置状态码;未设置时,带 Location 的空响应仍兼容为 302 ---
585
+ const hasCustomHttpCode = httpCode !== 0;
584
586
  // --- 设置自定义 hcode ---
585
587
  if (httpCode === 0) {
586
588
  httpCode = 200;
@@ -591,7 +593,7 @@ export async function run(data) {
591
593
  // --- 已经自行输出过 writeHead,可能自行处理了内容,如 pipe,则不再 writeHead ---
592
594
  }
593
595
  else {
594
- lCore.writeHead(data.res, data.res.getHeader('location') ? 302 : httpCode);
596
+ lCore.writeHead(data.res, data.res.getHeader('location') && !hasCustomHttpCode ? 302 : httpCode);
595
597
  }
596
598
  if (!data.res.writableEnded) {
597
599
  // --- 如果当前还没结束,则强制关闭连接,一切 pipe 请自行在方法中 await,否则会被中断 ---
@@ -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/netPost1', { 'a': '1', 'b': '2', 'c': ['1', '2', '3'] });
2025
- echo.push(`<pre>lUndici.post('${this._internalUrl}test/netPost1', { 'a': '1', 'b': '2', 'c': ['1', '2', '3'] });</pre>
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/netPostString1', 'HeiHei');
2037
- echo.push(`<pre>lUndici.post('${this._internalUrl}test/netPostString1', 'HeiHei');</pre>
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/netPost1').post().data({ 'a': '2', 'b': '0', 'c': ['0', '1', '3'] }).request();
2049
- echo.push(`<pre>lUndici.open('${this._internalUrl}test/netPost1').post().data({ 'a': '2', 'b': '0', 'c': ['0', '1', '3'] }).request();</pre>
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
- * 其他一切由框架自动处理:SSR 渲染、import map、props 注入、客户端水合。
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
- * 其他一切由框架自动处理:SSR 渲染、import map、props 注入、客户端水合。
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 }) {
@@ -5,7 +5,8 @@
5
5
  * 1. 这个 .page.tsx 文件(组件 + IProps)
6
6
  * 2. 一个 <link> 标签引入 CSS(如果不需要样式可省略)
7
7
  * 3. 在 Ctr 方法里调用 _loadReactPage('view/hello', props)
8
- * 其他一切由框架自动处理:SSR 渲染、import map、props 注入、客户端水合。
8
+ * 4. 执行 node ./source/main build -d source/www/example/stc 生成客户端水合 bundle
9
+ * 其他一切由框架自动处理:SSR 渲染、props 注入、客户端水合。
9
10
  */
10
11
  import { useState, type ReactElement } from 'react';
11
12
 
@@ -15,13 +15,13 @@
15
15
  * 3. 嵌套路由通过 Outlet 渲染子路由内容
16
16
  *
17
17
  * 【编译方式】
18
- * tsc watch 会自动编译本文件为同路径的 .js,无需额外命令。
19
- * 如需打包为 .bundle.js,执行:node ./source/main build
18
+ * tsc watch 会自动编译本文件为同路径的 .js,供服务端 SSR 使用。
19
+ * 客户端水合必须执行:node ./source/main build -d source/www/example/stc
20
20
  *
21
21
  * 【Tailwind CSS 构建】
22
22
  * 需提前执行 node ./source/main build -d source/www/example/stc 生成 CSS 产物。
23
23
  * 框架通过 _staticPath 和 _staticVer 自动拼接带版本号 URL;
24
- * import map、props JSON、水合脚本均由框架自动注入 HTML,组件无需手动处理。
24
+ * props JSON、水合脚本均由框架自动注入 HTML,组件无需手动处理。
25
25
  */
26
26
  interface IUser {
27
27
  'id': string;
@@ -16,13 +16,13 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
16
16
  * 3. 嵌套路由通过 Outlet 渲染子路由内容
17
17
  *
18
18
  * 【编译方式】
19
- * tsc watch 会自动编译本文件为同路径的 .js,无需额外命令。
20
- * 如需打包为 .bundle.js,执行:node ./source/main build
19
+ * tsc watch 会自动编译本文件为同路径的 .js,供服务端 SSR 使用。
20
+ * 客户端水合必须执行:node ./source/main build -d source/www/example/stc
21
21
  *
22
22
  * 【Tailwind CSS 构建】
23
23
  * 需提前执行 node ./source/main build -d source/www/example/stc 生成 CSS 产物。
24
24
  * 框架通过 _staticPath 和 _staticVer 自动拼接带版本号 URL;
25
- * import map、props JSON、水合脚本均由框架自动注入 HTML,组件无需手动处理。
25
+ * props JSON、水合脚本均由框架自动注入 HTML,组件无需手动处理。
26
26
  */
27
27
  import { useState, useEffect } from 'react';
28
28
  import { Routes, Route, Link, NavLink, useParams, useNavigate, useLocation, Outlet } from 'react-router-dom';
@@ -15,13 +15,13 @@
15
15
  * 3. 嵌套路由通过 Outlet 渲染子路由内容
16
16
  *
17
17
  * 【编译方式】
18
- * tsc watch 会自动编译本文件为同路径的 .js,无需额外命令。
19
- * 如需打包为 .bundle.js,执行:node ./source/main build
18
+ * tsc watch 会自动编译本文件为同路径的 .js,供服务端 SSR 使用。
19
+ * 客户端水合必须执行:node ./source/main build -d source/www/example/stc
20
20
  *
21
21
  * 【Tailwind CSS 构建】
22
22
  * 需提前执行 node ./source/main build -d source/www/example/stc 生成 CSS 产物。
23
23
  * 框架通过 _staticPath 和 _staticVer 自动拼接带版本号 URL;
24
- * import map、props JSON、水合脚本均由框架自动注入 HTML,组件无需手动处理。
24
+ * props JSON、水合脚本均由框架自动注入 HTML,组件无需手动处理。
25
25
  */
26
26
 
27
27
  import { useState, useEffect } from 'react';
@@ -336,7 +336,6 @@ export default function ReactRouterPage({
336
336
  <meta charSet="UTF-8" />
337
337
  <meta name="viewport" content="width=device-width, initial-scale=1" />
338
338
  <title suppressHydrationWarning>{title}</title>
339
- {/* --- import map 由框架自动注入在此标签前,无需手动添加 --- */}
340
339
  {/* --- dev: 需先执行 node ./source/main build -d source/www/example/stc 生成 CSS --- */}
341
340
  <link rel="stylesheet" href={`${_urlStc}view/react-router.page.css?v=${_staticVer}`} />
342
341
  </head>
@@ -9,26 +9,22 @@
9
9
  *
10
10
  * 【使用方式】
11
11
  * 在 Ctr 方法中调用 _loadReactPage('view/react', { ...props }),
12
- * 框架自动将 import map、props JSON、水合脚本注入 HTML,组件只写业务逻辑,无需任何框架样板代码。
12
+ * 框架自动将 props JSON、水合脚本注入 HTML,组件只写业务逻辑,无需任何框架样板代码。
13
13
  *
14
14
  * 【多页面 / 共用组件】
15
15
  * 将公共 UI(如 Label/Input/Checkbox)放在 stc/lib/ui/ 目录,各页面 import 进来。
16
16
  * 这和 shadcn/ui 推荐的 components/ui/ 目录结构完全一致。
17
- * tsc watch 会自动将所有 .tsx 编译为同路径的 .js,无需打包工具。
17
+ * tsc watch 会自动将所有 .tsx 编译为同路径的 .js,供服务端 SSR 使用。
18
18
  *
19
19
  * 【编译方式】
20
20
  * 本文件 (.tsx) 已集成到 source/tsconfig.json 的 include 中,
21
- * 开启 `tsc: watch - source/tsconfig.json` 即可自动编译,无需额外命令。
21
+ * 开启 `tsc: watch - source/tsconfig.json` 即可自动编译服务端文件。
22
22
  * 编译输出:stc/view/react.page.js(由 tsc 覆盖写入,勿手动编辑 .js 文件)
23
+ * 客户端水合需执行:node ./source/main build -d source/www/example/stc
23
24
  *
24
25
  * 【运行时机】
25
26
  * - 服务端(Node.js):_loadReactPage() 调用 renderToString(),产出完整 HTML 字符串。
26
- * - 客户端(浏览器):import map 将 bare import 解析到 esm.sh,hydrateRoot 接管 document。
27
- * - 两端使用同一份 JS 文件:服务端从磁盘读,浏览器从静态 URL 下载。
28
- *
29
- * 【第三方包 import map 自动解析】
30
- * 开发模式下,框架会自动扫描入口 JS 及其相对引用中的所有 bare specifier,
31
- * 并通过 esm.sh CDN 自动生成 import map 条目,无需手动配置。
27
+ * - 客户端(浏览器):加载 kebab build 生成的 bundle,由 hydrateRoot 接管 document。
32
28
  *
33
29
  * 【限制】
34
30
  * - 不能包含 Node.js 专属代码(fs/path/lDb 等),数据必须通过 props 传入。