@maiyunnet/kebab 9.18.2 → 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 CHANGED
@@ -1438,7 +1438,7 @@ index/variables/VER.md
1438
1438
 
1439
1439
  # Variable: VER
1440
1440
 
1441
- > `const` **VER**: `"9.18.2"` = `'9.18.2'`
1441
+ > `const` **VER**: `"9.19.0"` = `'9.19.0'`
1442
1442
 
1443
1443
  Defined in: [index.ts:10](https://github.com/maiyunnet/kebab/blob/master/index.ts#L10)
1444
1444
 
@@ -132834,7 +132834,7 @@ Defined in: [sys/ctr.ts:258](https://github.com/maiyunnet/kebab/blob/master/sys/
132834
132834
 
132835
132835
  > `protected` **\_checkInput**(`input`, `rule`, `rtn`): `boolean`
132836
132836
 
132837
- Defined in: [sys/ctr.ts:799](https://github.com/maiyunnet/kebab/blob/master/sys/ctr.ts#L799)
132837
+ Defined in: [sys/ctr.ts:721](https://github.com/maiyunnet/kebab/blob/master/sys/ctr.ts#L721)
132838
132838
 
132839
132839
  检测提交的数据类型
132840
132840
 
@@ -132868,7 +132868,7 @@ Defined in: [sys/ctr.ts:799](https://github.com/maiyunnet/kebab/blob/master/sys/
132868
132868
 
132869
132869
  > `protected` **\_checkXInput**(`input`, `rule`, `rtn`, `ignore?`): `boolean`
132870
132870
 
132871
- Defined in: [sys/ctr.ts:978](https://github.com/maiyunnet/kebab/blob/master/sys/ctr.ts#L978)
132871
+ Defined in: [sys/ctr.ts:900](https://github.com/maiyunnet/kebab/blob/master/sys/ctr.ts#L900)
132872
132872
 
132873
132873
  检测提交的数据类型(会检测 XSRF)
132874
132874
 
@@ -132908,7 +132908,7 @@ Defined in: [sys/ctr.ts:978](https://github.com/maiyunnet/kebab/blob/master/sys/
132908
132908
 
132909
132909
  > `protected` **\_cross**(`opt?`): `boolean`
132910
132910
 
132911
- Defined in: [sys/ctr.ts:1213](https://github.com/maiyunnet/kebab/blob/master/sys/ctr.ts#L1213)
132911
+ Defined in: [sys/ctr.ts:1135](https://github.com/maiyunnet/kebab/blob/master/sys/ctr.ts#L1135)
132912
132912
 
132913
132913
  开启跨域请求
132914
132914
 
@@ -132953,7 +132953,7 @@ Defined in: [sys/ctr.ts:1213](https://github.com/maiyunnet/kebab/blob/master/sys
132953
132953
 
132954
132954
  > `protected` **\_device**(): `"unknown"` \| `"android"` \| `"linux"` \| `"windows"` \| `"macintosh"` \| `"ipad"`
132955
132955
 
132956
- Defined in: [sys/ctr.ts:1023](https://github.com/maiyunnet/kebab/blob/master/sys/ctr.ts#L1023)
132956
+ Defined in: [sys/ctr.ts:945](https://github.com/maiyunnet/kebab/blob/master/sys/ctr.ts#L945)
132957
132957
 
132958
132958
  根据用户 ua 获取当前用户的设备类型
132959
132959
 
@@ -132967,7 +132967,7 @@ Defined in: [sys/ctr.ts:1023](https://github.com/maiyunnet/kebab/blob/master/sys
132967
132967
 
132968
132968
  > `protected` **\_enabledXsrf**(`opt?`): `void`
132969
132969
 
132970
- Defined in: [sys/ctr.ts:992](https://github.com/maiyunnet/kebab/blob/master/sys/ctr.ts#L992)
132970
+ Defined in: [sys/ctr.ts:914](https://github.com/maiyunnet/kebab/blob/master/sys/ctr.ts#L914)
132971
132971
 
132972
132972
  当前页面开启 XSRF 支持(主要检测 cookie 是否存在)
132973
132973
  如果当前页面有 CDN,请不要使用
@@ -132990,7 +132990,7 @@ Defined in: [sys/ctr.ts:992](https://github.com/maiyunnet/kebab/blob/master/sys/
132990
132990
 
132991
132991
  > `protected` **\_end**(`code?`, `reason?`): `void`
132992
132992
 
132993
- Defined in: [sys/ctr.ts:1323](https://github.com/maiyunnet/kebab/blob/master/sys/ctr.ts#L1323)
132993
+ Defined in: [sys/ctr.ts:1245](https://github.com/maiyunnet/kebab/blob/master/sys/ctr.ts#L1245)
132994
132994
 
132995
132995
  主动关闭当前 socket 连接
132996
132996
 
@@ -133018,7 +133018,7 @@ WebSocket 关闭码
133018
133018
 
133019
133019
  > `protected` **\_getBasicAuth**(`user`, `pwd`): `string`
133020
133020
 
133021
- Defined in: [sys/ctr.ts:1016](https://github.com/maiyunnet/kebab/blob/master/sys/ctr.ts#L1016)
133021
+ Defined in: [sys/ctr.ts:938](https://github.com/maiyunnet/kebab/blob/master/sys/ctr.ts#L938)
133022
133022
 
133023
133023
  获取 Auth 字符串,用于客户端提交
133024
133024
 
@@ -133046,7 +133046,7 @@ Defined in: [sys/ctr.ts:1016](https://github.com/maiyunnet/kebab/blob/master/sys
133046
133046
 
133047
133047
  > `protected` **\_getLocale**(): `string`
133048
133048
 
133049
- Defined in: [sys/ctr.ts:1204](https://github.com/maiyunnet/kebab/blob/master/sys/ctr.ts#L1204)
133049
+ Defined in: [sys/ctr.ts:1126](https://github.com/maiyunnet/kebab/blob/master/sys/ctr.ts#L1126)
133050
133050
 
133051
133051
  获取当前语言名
133052
133052
 
@@ -133060,7 +133060,7 @@ Defined in: [sys/ctr.ts:1204](https://github.com/maiyunnet/kebab/blob/master/sys
133060
133060
 
133061
133061
  > `protected` **\_getLocaleJsonString**(): `string`
133062
133062
 
133063
- Defined in: [sys/ctr.ts:1192](https://github.com/maiyunnet/kebab/blob/master/sys/ctr.ts#L1192)
133063
+ Defined in: [sys/ctr.ts:1114](https://github.com/maiyunnet/kebab/blob/master/sys/ctr.ts#L1114)
133064
133064
 
133065
133065
  根据当前后台语言包设置情况获取 JSON 字符串传输到前台
133066
133066
 
@@ -133112,7 +133112,7 @@ Defined in: [sys/ctr.ts:400](https://github.com/maiyunnet/kebab/blob/master/sys/
133112
133112
 
133113
133113
  > `protected` **\_handleFormData**(`events?`, `limits?`): `Promise`\<`boolean`\>
133114
133114
 
133115
- Defined in: [sys/ctr.ts:1332](https://github.com/maiyunnet/kebab/blob/master/sys/ctr.ts#L1332)
133115
+ Defined in: [sys/ctr.ts:1254](https://github.com/maiyunnet/kebab/blob/master/sys/ctr.ts#L1254)
133116
133116
 
133117
133117
  获取 formdata 的信息
133118
133118
 
@@ -133190,7 +133190,7 @@ multipart 字段与文件总数量,默认 1000
133190
133190
 
133191
133191
  > **\_l**(`key`, `data?`): `string`
133192
133192
 
133193
- Defined in: [sys/ctr.ts:1256](https://github.com/maiyunnet/kebab/blob/master/sys/ctr.ts#L1256)
133193
+ Defined in: [sys/ctr.ts:1178](https://github.com/maiyunnet/kebab/blob/master/sys/ctr.ts#L1178)
133194
133194
 
133195
133195
  获取语言包值
133196
133196
 
@@ -133216,7 +133216,7 @@ Defined in: [sys/ctr.ts:1256](https://github.com/maiyunnet/kebab/blob/master/sys
133216
133216
 
133217
133217
  > `protected` **\_loadData**\<`T`\>(`path`): `Promise`\<`false` \| `T` \| `null`\>
133218
133218
 
133219
- Defined in: [sys/ctr.ts:1092](https://github.com/maiyunnet/kebab/blob/master/sys/ctr.ts#L1092)
133219
+ Defined in: [sys/ctr.ts:1014](https://github.com/maiyunnet/kebab/blob/master/sys/ctr.ts#L1014)
133220
133220
 
133221
133221
  获取 data JSON 数据
133222
133222
 
@@ -133244,7 +133244,7 @@ Defined in: [sys/ctr.ts:1092](https://github.com/maiyunnet/kebab/blob/master/sys
133244
133244
 
133245
133245
  > `protected` **\_loadLocale**(`loc`, `pkg?`): `Promise`\<`boolean`\>
133246
133246
 
133247
- Defined in: [sys/ctr.ts:1143](https://github.com/maiyunnet/kebab/blob/master/sys/ctr.ts#L1143)
133247
+ Defined in: [sys/ctr.ts:1065](https://github.com/maiyunnet/kebab/blob/master/sys/ctr.ts#L1065)
133248
133248
 
133249
133249
  设定语言并加载语言包
133250
133250
 
@@ -133276,7 +133276,7 @@ Defined in: [sys/ctr.ts:444](https://github.com/maiyunnet/kebab/blob/master/sys/
133276
133276
 
133277
133277
  加载 React 全页面进行 SSR 渲染,组件需渲染完整 HTML 文档(含 html/head/body),无需 EJS
133278
133278
  框架自动注入 props:_urlBase/_urlFull/_urlStc/_staticVer/_staticPath/_staticPathFull
133279
- 多语言:自动注入 _locale(当前语言名)和 _localeData(已载语言包的合并键值对)
133279
+ 多语言:自动注入 _locale(当前语言名)和 _localeData(当前语种已载语言包的合并键值对)
133280
133280
  组件内创建:const l = (key: string, ...args: string[]): string => { let i = 0; return (_localeData[key] ?? key).replace(/\?/g, () => args[i++] ?? ''); };
133281
133281
 
133282
133282
  #### Parameters
@@ -133301,13 +133301,7 @@ Defined in: [sys/ctr.ts:444](https://github.com/maiyunnet/kebab/blob/master/sys/
133301
133301
 
133302
133302
  `boolean`
133303
133303
 
133304
- 是否注入客户端水合脚本(import map + hydrateRoot),默认 true
133305
-
133306
- ###### reactVer?
133307
-
133308
- `string`
133309
-
133310
- react/react-dom/react-router-dom 版本号,用于 esm.sh CDN,默认 19
133304
+ 是否注入客户端水合 bundle,默认 true;设为 false 时仅输出服务端渲染的 HTML
133311
133305
 
133312
133306
  ###### router?
133313
133307
 
@@ -133365,7 +133359,7 @@ Defined in: [sys/ctr.ts:417](https://github.com/maiyunnet/kebab/blob/master/sys/
133365
133359
 
133366
133360
  > `protected` **\_location**(`location`): `false`
133367
133361
 
133368
- Defined in: [sys/ctr.ts:1113](https://github.com/maiyunnet/kebab/blob/master/sys/ctr.ts#L1113)
133362
+ Defined in: [sys/ctr.ts:1035](https://github.com/maiyunnet/kebab/blob/master/sys/ctr.ts#L1035)
133369
133363
 
133370
133364
  跳转(302临时跳转),支持相对本项目根路径的路径或绝对路径
133371
133365
 
@@ -133387,7 +133381,7 @@ Defined in: [sys/ctr.ts:1113](https://github.com/maiyunnet/kebab/blob/master/sys
133387
133381
 
133388
133382
  > `protected` **\_ping**(`data?`): `boolean`
133389
133383
 
133390
- Defined in: [sys/ctr.ts:1306](https://github.com/maiyunnet/kebab/blob/master/sys/ctr.ts#L1306)
133384
+ Defined in: [sys/ctr.ts:1228](https://github.com/maiyunnet/kebab/blob/master/sys/ctr.ts#L1228)
133391
133385
 
133392
133386
  发送 socket ping
133393
133387
 
@@ -133409,7 +133403,7 @@ Defined in: [sys/ctr.ts:1306](https://github.com/maiyunnet/kebab/blob/master/sys
133409
133403
 
133410
133404
  > `protected` **\_pong**(`data?`): `boolean`
133411
133405
 
133412
- Defined in: [sys/ctr.ts:1314](https://github.com/maiyunnet/kebab/blob/master/sys/ctr.ts#L1314)
133406
+ Defined in: [sys/ctr.ts:1236](https://github.com/maiyunnet/kebab/blob/master/sys/ctr.ts#L1236)
133413
133407
 
133414
133408
  发送 socket pong
133415
133409
 
@@ -133431,7 +133425,7 @@ Defined in: [sys/ctr.ts:1314](https://github.com/maiyunnet/kebab/blob/master/sys
133431
133425
 
133432
133426
  > `protected` **\_startSession**(`link`, `auth?`, `opt?`): `Promise`\<`boolean`\>
133433
133427
 
133434
- Defined in: [sys/ctr.ts:1127](https://github.com/maiyunnet/kebab/blob/master/sys/ctr.ts#L1127)
133428
+ Defined in: [sys/ctr.ts:1049](https://github.com/maiyunnet/kebab/blob/master/sys/ctr.ts#L1049)
133435
133429
 
133436
133430
  开启 Session
133437
133431
 
@@ -133465,7 +133459,7 @@ Kv 或 Db 实例
133465
133459
 
133466
133460
  > `protected` **\_valibot**\<`TSchema`\>(`schema`, `input`, `options?`): [`TValibotResult`](../type-aliases/TValibotResult.md)\<`TSchema`\>
133467
133461
 
133468
- Defined in: [sys/ctr.ts:718](https://github.com/maiyunnet/kebab/blob/master/sys/ctr.ts#L718)
133462
+ Defined in: [sys/ctr.ts:640](https://github.com/maiyunnet/kebab/blob/master/sys/ctr.ts#L640)
133469
133463
 
133470
133464
  使用 Valibot schema 校验并解析输入,成功后 output 会自动推导类型
133471
133465
 
@@ -133507,7 +133501,7 @@ Valibot 配置和自定义客户端返回值
133507
133501
 
133508
133502
  > `protected` **\_valibotx**\<`TSchema`\>(`schema`, `input`, `options?`): [`TValibotResult`](../type-aliases/TValibotResult.md)\<`TSchema`, [`IValibotXsrfIssue`](../interfaces/IValibotXsrfIssue.md) \| [`InferIssue`](../../../lib/core/namespaces/v/type-aliases/InferIssue.md)\<`TSchema`\>\>
133509
133503
 
133510
- Defined in: [sys/ctr.ts:742](https://github.com/maiyunnet/kebab/blob/master/sys/ctr.ts#L742)
133504
+ Defined in: [sys/ctr.ts:664](https://github.com/maiyunnet/kebab/blob/master/sys/ctr.ts#L664)
133511
133505
 
133512
133506
  使用 Valibot schema 校验并解析输入,同时检测 XSRF
133513
133507
 
@@ -133549,7 +133543,7 @@ Valibot 配置、自定义客户端返回值和 XSRF 选项
133549
133543
 
133550
133544
  > `protected` **\_writeBinary**(`data`): `boolean`
133551
133545
 
133552
- Defined in: [sys/ctr.ts:1298](https://github.com/maiyunnet/kebab/blob/master/sys/ctr.ts#L1298)
133546
+ Defined in: [sys/ctr.ts:1220](https://github.com/maiyunnet/kebab/blob/master/sys/ctr.ts#L1220)
133553
133547
 
133554
133548
  发送 socket 二进制
133555
133549
 
@@ -133571,7 +133565,7 @@ Defined in: [sys/ctr.ts:1298](https://github.com/maiyunnet/kebab/blob/master/sys
133571
133565
 
133572
133566
  > `protected` **\_writeResult**(`data`): `boolean`
133573
133567
 
133574
- Defined in: [sys/ctr.ts:1290](https://github.com/maiyunnet/kebab/blob/master/sys/ctr.ts#L1290)
133568
+ Defined in: [sys/ctr.ts:1212](https://github.com/maiyunnet/kebab/blob/master/sys/ctr.ts#L1212)
133575
133569
 
133576
133570
  发送结果对象文本
133577
133571
 
@@ -133593,7 +133587,7 @@ Defined in: [sys/ctr.ts:1290](https://github.com/maiyunnet/kebab/blob/master/sys
133593
133587
 
133594
133588
  > `protected` **\_writeText**(`data`): `boolean`
133595
133589
 
133596
- Defined in: [sys/ctr.ts:1282](https://github.com/maiyunnet/kebab/blob/master/sys/ctr.ts#L1282)
133590
+ Defined in: [sys/ctr.ts:1204](https://github.com/maiyunnet/kebab/blob/master/sys/ctr.ts#L1204)
133597
133591
 
133598
133592
  发送 socket 文本
133599
133593
 
@@ -133615,7 +133609,7 @@ Defined in: [sys/ctr.ts:1282](https://github.com/maiyunnet/kebab/blob/master/sys
133615
133609
 
133616
133610
  > **getAuthorization**(): `false` \| \{ `pwd`: `string`; `type`: `"basic"`; `user`: `string`; \} \| \{ `token`: `string`; `type`: `"bearer"`; \}
133617
133611
 
133618
- Defined in: [sys/ctr.ts:1044](https://github.com/maiyunnet/kebab/blob/master/sys/ctr.ts#L1044)
133612
+ Defined in: [sys/ctr.ts:966](https://github.com/maiyunnet/kebab/blob/master/sys/ctr.ts#L966)
133619
133613
 
133620
133614
  通过 header 或 _auth 获取鉴权信息,支持 Basic Auth 和 Bearer Token
133621
133615
 
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.0";
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.0';
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.0",
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
@@ -213,17 +213,15 @@ export declare class Ctr {
213
213
  /**
214
214
  * --- 加载 React 全页面进行 SSR 渲染,组件需渲染完整 HTML 文档(含 html/head/body),无需 EJS ---
215
215
  * --- 框架自动注入 props:_urlBase/_urlFull/_urlStc/_staticVer/_staticPath/_staticPathFull ---
216
- * --- 多语言:自动注入 _locale(当前语言名)和 _localeData(已载语言包的合并键值对) ---
216
+ * --- 多语言:自动注入 _locale(当前语言名)和 _localeData(当前语种已载语言包的合并键值对) ---
217
217
  * --- 组件内创建:const l = (key: string, ...args: string[]): string => { let i = 0; return (_localeData[key] ?? key).replace(/\?/g, () => args[i++] ?? ''); }; ---
218
218
  * @param path 页面组件路径(相对于 stc/ 目录,不含扩展名,tsx 编译后的 .js)
219
219
  * @param props 传入组件的 props,框架常量自动合并,整体序列化为内联 JSON 供客户端水合复用
220
220
  * @param opt 可选配置
221
221
  */
222
222
  protected _loadReactPage(path: string, props?: Record<string, kebab.Json>, opt?: {
223
- /** --- 是否注入客户端水合脚本(import map + hydrateRoot),默认 true --- */
223
+ /** --- 是否注入客户端水合 bundle,默认 true;设为 false 时仅输出服务端渲染的 HTML --- */
224
224
  'hydrate'?: boolean;
225
- /** --- react/react-dom/react-router-dom 版本号,用于 esm.sh CDN,默认 19 --- */
226
- 'reactVer'?: string;
227
225
  /**
228
226
  * --- 路由模式,不传则不注入任何 Router,组件自行管理路由(如 MemoryRouter)或无路由 ---
229
227
  * --- 'browser':服务端用 StaticRouter,客户端用 BrowserRouter,地址栏与路由联动 ---
@@ -241,14 +239,6 @@ export declare class Ctr {
241
239
  */
242
240
  'staticPath'?: string;
243
241
  }): 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;
252
242
  /**
253
243
  * --- 设置校验错误返回值 ---
254
244
  * @param rtn 返回值数组
package/sys/ctr.js CHANGED
@@ -264,7 +264,7 @@ 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 供客户端水合复用
@@ -272,13 +272,23 @@ export class Ctr {
272
272
  */
273
273
  async _loadReactPage(path, props = {}, opt = {}) {
274
274
  // --- 约定:传入路径不含 .page 后缀,框架自动补全(对应 build 命令的 *.page.tsx 约定)---
275
- // --- 组件 JS 从 stc 目录读取,浏览器同样通过 staticPath(支持 CDN)下载 ---
275
+ // --- 服务端读取 tsc 生成的组件 JS,浏览器读取 kebab build 生成的 bundle ---
276
276
  const componentPath = this._config.const.rootPath + 'stc/' + path + '.page.js';
277
277
  if (!await lFs.isFile(componentPath)) {
278
278
  lCore.debug(`[CTR][_loadReactPage] componentPath ${componentPath} is not a file`);
279
279
  lCore.log(this, `[CTR][_loadReactPage] componentPath ${componentPath} is not a file`, '-error');
280
280
  return '';
281
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
+ }
282
292
  try {
283
293
  const reactDomServer = await import('react-dom/server');
284
294
  const importPath = componentPath.startsWith('/')
@@ -287,11 +297,8 @@ export class Ctr {
287
297
  const mod = await import(importPath);
288
298
  const component = mod.default;
289
299
  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
- }
300
+ // --- 语言包数据:当前语种的各命名包已由 _loadLocale 合并 ---
301
+ const localeData = { ...(this._localeData[this._locale] ?? {}) };
295
302
  // --- 把框架常量合并进 props,与 _loadView 行为一致 ---
296
303
  const staticPath = opt.staticPath ?? this._config.set.staticPath;
297
304
  const fullProps = {
@@ -305,67 +312,22 @@ export class Ctr {
305
312
  '_locale': this._locale,
306
313
  '_localeData': localeData,
307
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
+ }
308
321
  // --- 框架自动注入的 HTML 片段,用户组件无需手动渲染 ---
309
- let headInject = '';
310
322
  let bodyInject = '';
311
323
  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
324
  // --- propsJson 在渲染前序列化,框架直接注入 HTML,组件无需手动渲染 ---
324
325
  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
- }
326
+ const clientUrl = `${staticPath}${path}.page.bundle.js?v=${this._config.set.staticVer}`;
365
327
  // --- props JSON + 水合脚本注入到 </body> 前 ---
366
328
  bodyInject =
367
329
  `<script id="__kebab_props__" type="application/json">${propsJson}</script>` +
368
- `<script type="module">${hydrateScript}</script>`;
330
+ `<script type="module">import'${clientUrl}';</script>`;
369
331
  }
370
332
  // --- BrowserRouter 模式:服务端用 StaticRouter 渲染,与客户端的 BrowserRouter 等价 ---
371
333
  // --- component 来自动态 import,TypeScript 无法精确推断,需要明确限定 element 类型 ---
@@ -379,10 +341,9 @@ export class Ctr {
379
341
  'basename': fullProps['_routerBase'],
380
342
  }, react.createElement(component, fullProps));
381
343
  }
382
- // --- 框架将 import map 注入 </head> 前,props JSON + 水合脚本注入 </body> 前 ---
344
+ // --- 框架将 props JSON + 水合 bundle 注入到 </body> 前 ---
383
345
  let html = '<!DOCTYPE html>' + reactDomServer.renderToString(element);
384
346
  if (opt.hydrate !== false) {
385
- html = html.replace('</head>', headInject + '</head>');
386
347
  html = html.replace('</body>', bodyInject + '</body>');
387
348
  }
388
349
  return html;
@@ -393,38 +354,6 @@ export class Ctr {
393
354
  return '';
394
355
  }
395
356
  }
396
- /**
397
- * --- 递归扫描 JS 文件中的 import 语句,收集第三方 bare specifier ---
398
- * @param filePath 当前要扫描的文件绝对路径
399
- * @param scannedFiles 已扫描文件集(去重用)
400
- * @param extraImports 收集到的第三方包名集合
401
- * @param builtinImports 内置 import map,已有条目不重复添加
402
- */
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
- }
427
- }
428
357
  /**
429
358
  * --- 设置校验错误返回值 ---
430
359
  * @param rtn 返回值数组
@@ -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 传入。
@@ -10,26 +10,22 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
10
10
  *
11
11
  * 【使用方式】
12
12
  * 在 Ctr 方法中调用 _loadReactPage('view/react', { ...props }),
13
- * 框架自动将 import map、props JSON、水合脚本注入 HTML,组件只写业务逻辑,无需任何框架样板代码。
13
+ * 框架自动将 props JSON、水合脚本注入 HTML,组件只写业务逻辑,无需任何框架样板代码。
14
14
  *
15
15
  * 【多页面 / 共用组件】
16
16
  * 将公共 UI(如 Label/Input/Checkbox)放在 stc/lib/ui/ 目录,各页面 import 进来。
17
17
  * 这和 shadcn/ui 推荐的 components/ui/ 目录结构完全一致。
18
- * tsc watch 会自动将所有 .tsx 编译为同路径的 .js,无需打包工具。
18
+ * tsc watch 会自动将所有 .tsx 编译为同路径的 .js,供服务端 SSR 使用。
19
19
  *
20
20
  * 【编译方式】
21
21
  * 本文件 (.tsx) 已集成到 source/tsconfig.json 的 include 中,
22
- * 开启 `tsc: watch - source/tsconfig.json` 即可自动编译,无需额外命令。
22
+ * 开启 `tsc: watch - source/tsconfig.json` 即可自动编译服务端文件。
23
23
  * 编译输出:stc/view/react.page.js(由 tsc 覆盖写入,勿手动编辑 .js 文件)
24
+ * 客户端水合需执行:node ./source/main build -d source/www/example/stc
24
25
  *
25
26
  * 【运行时机】
26
27
  * - 服务端(Node.js):_loadReactPage() 调用 renderToString(),产出完整 HTML 字符串。
27
- * - 客户端(浏览器):import map 将 bare import 解析到 esm.sh,hydrateRoot 接管 document。
28
- * - 两端使用同一份 JS 文件:服务端从磁盘读,浏览器从静态 URL 下载。
29
- *
30
- * 【第三方包 import map 自动解析】
31
- * 开发模式下,框架会自动扫描入口 JS 及其相对引用中的所有 bare specifier,
32
- * 并通过 esm.sh CDN 自动生成 import map 条目,无需手动配置。
28
+ * - 客户端(浏览器):加载 kebab build 生成的 bundle,由 hydrateRoot 接管 document。
33
29
  *
34
30
  * 【限制】
35
31
  * - 不能包含 Node.js 专属代码(fs/path/lDb 等),数据必须通过 props 传入。
@@ -102,7 +98,7 @@ function ShadcnDemo() {
102
98
  e.preventDefault();
103
99
  setSubmitted(true);
104
100
  }
105
- return (_jsxs("div", { className: "space-y-4", children: [_jsxs(Card, { children: [_jsx("h2", { className: "font-semibold text-slate-900 mb-2", children: "shadcn/ui Components" }), _jsxs("p", { className: "text-slate-500 text-xs leading-relaxed", children: ["All components below are imported from ", _jsx("code", { className: "bg-slate-100 px-1 rounded", children: "stc/lib/ui/" }), ", following the same ", _jsx("code", { className: "bg-slate-100 px-1 rounded", children: "components/ui/" }), " structure recommended by shadcn/ui. Built on Radix UI primitives, styled with Tailwind CSS. In dev mode, the framework auto-scans all bare imports and generates the import map automatically."] })] }), _jsxs(Card, { children: [_jsx("h2", { className: "font-semibold text-slate-900 mb-4", children: "Label + Input" }), _jsxs("div", { className: "grid gap-4", children: [_jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: nameId, children: "Username" }), _jsx(Input, { id: nameId, type: "text", placeholder: "Enter your username", value: name, onChange: e => { setName(e.target.value); } })] }), _jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: emailId, children: "Email" }), _jsx(Input, { id: emailId, type: "email", placeholder: "you@example.com", value: email, onChange: e => { setEmail(e.target.value); } })] })] }), _jsx("p", { className: "text-xs text-slate-400 mt-3", children: "Click the Label text to focus the corresponding input (Radix UI a11y semantics)" })] }), _jsxs(Card, { children: [_jsx("h2", { className: "font-semibold text-slate-900 mb-4", children: "Checkbox" }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Checkbox, { id: agreeId, checked: agree, onCheckedChange: setAgree }), _jsx(Label, { htmlFor: agreeId, children: "I have read and agree to the Terms of Service" })] }), _jsxs("p", { className: "text-xs text-slate-400 mt-3", children: ["Value: ", _jsx("code", { className: "bg-slate-100 px-1 rounded", children: String(agree) }), "\u00A0(supports ", _jsx("code", { className: "bg-slate-100 px-1 rounded", children: "true | false | 'indeterminate'" }), " tri-state)"] })] }), _jsxs(Card, { children: [_jsx("h2", { className: "font-semibold text-slate-900 mb-4", children: "Switch" }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { id: newsletterId, checked: newsletter, onCheckedChange: setNewsletter }), _jsx(Label, { htmlFor: newsletterId, children: "Subscribe to product updates" })] }), _jsxs("p", { className: "text-xs text-slate-400 mt-3", children: ["Value: ", _jsx("code", { className: "bg-slate-100 px-1 rounded", children: newsletter ? 'true' : 'false' })] })] }), _jsxs(Card, { children: [_jsx("h2", { className: "font-semibold text-slate-900 mb-4", children: "Combined Form (Submit Demo)" }), submitted ? (_jsxs("div", { className: "space-y-2 text-sm", children: [_jsx("p", { className: "text-green-700 font-medium", children: "\u2713 Submitted! Received data:" }), _jsx("pre", { className: "bg-slate-50 border border-slate-200 rounded-lg p-3 text-xs text-slate-700 overflow-auto", children: JSON.stringify({ name, email, agree: Boolean(agree), newsletter }, null, 2) }), _jsx("button", { onClick: () => { setSubmitted(false); }, className: "text-xs text-blue-500 hover:underline cursor-pointer", children: "Reset" })] })) : (_jsxs("form", { onSubmit: handleSubmit, className: "space-y-4", children: [_jsxs("div", { className: "grid gap-1.5", children: [_jsxs(Label, { htmlFor: `${nameId}-form`, children: ["Username ", _jsx("span", { className: "text-red-500", children: "*" })] }), _jsx(Input, { id: `${nameId}-form`, required: true, placeholder: "At least 2 characters", value: name, onChange: e => { setName(e.target.value); } })] }), _jsxs("div", { className: "grid gap-1.5", children: [_jsxs(Label, { htmlFor: `${emailId}-form`, children: ["Email ", _jsx("span", { className: "text-red-500", children: "*" })] }), _jsx(Input, { id: `${emailId}-form`, type: "email", required: true, placeholder: "you@example.com", value: email, onChange: e => { setEmail(e.target.value); } })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Checkbox, { id: `${agreeId}-form`, checked: agree, onCheckedChange: setAgree, required: true }), _jsx(Label, { htmlFor: `${agreeId}-form`, children: "Agree to terms" })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { id: `${newsletterId}-form`, checked: newsletter, onCheckedChange: setNewsletter }), _jsx(Label, { htmlFor: `${newsletterId}-form`, children: "Subscribe" })] }), _jsx("button", { type: "submit", className: "inline-flex items-center px-4 py-2 rounded-lg bg-blue-500 hover:bg-blue-600 text-white text-sm font-medium cursor-pointer", children: "Submit" })] }))] })] }));
101
+ return (_jsxs("div", { className: "space-y-4", children: [_jsxs(Card, { children: [_jsx("h2", { className: "font-semibold text-slate-900 mb-2", children: "shadcn/ui Components" }), _jsxs("p", { className: "text-slate-500 text-xs leading-relaxed", children: ["All components below are imported from ", _jsx("code", { className: "bg-slate-100 px-1 rounded", children: "stc/lib/ui/" }), ", following the same ", _jsx("code", { className: "bg-slate-100 px-1 rounded", children: "components/ui/" }), " structure recommended by shadcn/ui. Built on Radix UI primitives, styled with Tailwind CSS. Client dependencies are bundled locally by the Kebab build command."] })] }), _jsxs(Card, { children: [_jsx("h2", { className: "font-semibold text-slate-900 mb-4", children: "Label + Input" }), _jsxs("div", { className: "grid gap-4", children: [_jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: nameId, children: "Username" }), _jsx(Input, { id: nameId, type: "text", placeholder: "Enter your username", value: name, onChange: e => { setName(e.target.value); } })] }), _jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: emailId, children: "Email" }), _jsx(Input, { id: emailId, type: "email", placeholder: "you@example.com", value: email, onChange: e => { setEmail(e.target.value); } })] })] }), _jsx("p", { className: "text-xs text-slate-400 mt-3", children: "Click the Label text to focus the corresponding input (Radix UI a11y semantics)" })] }), _jsxs(Card, { children: [_jsx("h2", { className: "font-semibold text-slate-900 mb-4", children: "Checkbox" }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Checkbox, { id: agreeId, checked: agree, onCheckedChange: setAgree }), _jsx(Label, { htmlFor: agreeId, children: "I have read and agree to the Terms of Service" })] }), _jsxs("p", { className: "text-xs text-slate-400 mt-3", children: ["Value: ", _jsx("code", { className: "bg-slate-100 px-1 rounded", children: String(agree) }), "\u00A0(supports ", _jsx("code", { className: "bg-slate-100 px-1 rounded", children: "true | false | 'indeterminate'" }), " tri-state)"] })] }), _jsxs(Card, { children: [_jsx("h2", { className: "font-semibold text-slate-900 mb-4", children: "Switch" }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { id: newsletterId, checked: newsletter, onCheckedChange: setNewsletter }), _jsx(Label, { htmlFor: newsletterId, children: "Subscribe to product updates" })] }), _jsxs("p", { className: "text-xs text-slate-400 mt-3", children: ["Value: ", _jsx("code", { className: "bg-slate-100 px-1 rounded", children: newsletter ? 'true' : 'false' })] })] }), _jsxs(Card, { children: [_jsx("h2", { className: "font-semibold text-slate-900 mb-4", children: "Combined Form (Submit Demo)" }), submitted ? (_jsxs("div", { className: "space-y-2 text-sm", children: [_jsx("p", { className: "text-green-700 font-medium", children: "\u2713 Submitted! Received data:" }), _jsx("pre", { className: "bg-slate-50 border border-slate-200 rounded-lg p-3 text-xs text-slate-700 overflow-auto", children: JSON.stringify({ name, email, agree: Boolean(agree), newsletter }, null, 2) }), _jsx("button", { onClick: () => { setSubmitted(false); }, className: "text-xs text-blue-500 hover:underline cursor-pointer", children: "Reset" })] })) : (_jsxs("form", { onSubmit: handleSubmit, className: "space-y-4", children: [_jsxs("div", { className: "grid gap-1.5", children: [_jsxs(Label, { htmlFor: `${nameId}-form`, children: ["Username ", _jsx("span", { className: "text-red-500", children: "*" })] }), _jsx(Input, { id: `${nameId}-form`, required: true, placeholder: "At least 2 characters", value: name, onChange: e => { setName(e.target.value); } })] }), _jsxs("div", { className: "grid gap-1.5", children: [_jsxs(Label, { htmlFor: `${emailId}-form`, children: ["Email ", _jsx("span", { className: "text-red-500", children: "*" })] }), _jsx(Input, { id: `${emailId}-form`, type: "email", required: true, placeholder: "you@example.com", value: email, onChange: e => { setEmail(e.target.value); } })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Checkbox, { id: `${agreeId}-form`, checked: agree, onCheckedChange: setAgree, required: true }), _jsx(Label, { htmlFor: `${agreeId}-form`, children: "Agree to terms" })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { id: `${newsletterId}-form`, checked: newsletter, onCheckedChange: setNewsletter }), _jsx(Label, { htmlFor: `${newsletterId}-form`, children: "Subscribe" })] }), _jsx("button", { type: "submit", className: "inline-flex items-center px-4 py-2 rounded-lg bg-blue-500 hover:bg-blue-600 text-white text-sm font-medium cursor-pointer", children: "Submit" })] }))] })] }));
106
102
  }
107
103
  // ─── 页面主组件 ────────────────────────────────────────────────────────────────
108
104
  /** --- Kebab React 全页面演示 --- */
@@ -153,7 +149,7 @@ export default function ReactPage({ title, serverTime, node, _urlBase, _urlStc,
153
149
  checkbox.tsx # shadcn Checkbox
154
150
  switch.tsx # shadcn Switch
155
151
  view/
156
- react-page.tsx # Page component (import + compose)` }), _jsxs("p", { className: "text-slate-500 text-xs mt-3", children: ["Mirrors the shadcn/ui", ' ', _jsx("code", { className: "bg-slate-100 px-1 rounded", children: "components/ui/" }), " convention. In dev mode, the framework auto-scans imports \u2014 no manual import map configuration needed."] })] })] })), tab === 'routing' && (_jsxs(MemoryRouter, { children: [_jsxs(Card, { children: [_jsx("h2", { className: "font-semibold text-slate-900 mb-1", children: "React Router Demo" }), _jsxs("p", { className: "text-slate-500 text-xs mb-4", children: ["Uses ", _jsx("code", { className: "bg-slate-100 px-1 rounded", children: "MemoryRouter" }), ' ', "to demonstrate route navigation, dynamic params (useParams), and programmatic navigation (useNavigate). Works on both server SSR and client hydration with no extra server configuration."] }), _jsxs(Routes, { children: [_jsx(Route, { path: "/", element: _jsx(RouterHome, {}) }), _jsx(Route, { path: "/about", element: _jsx(RouterAbout, {}) }), _jsx(Route, { path: "/user/:id", element: _jsx(RouterUser, {}) })] })] }), _jsxs(Card, { className: "mt-4", children: [_jsx("h2", { className: "font-semibold text-slate-900 mb-2", children: "Full-Page BrowserRouter Setup" }), _jsx("p", { className: "text-slate-500 text-xs mb-3", children: "To let React Router manage the real URL (e.g. /app, /app/about), replace MemoryRouter with BrowserRouter and route all sub-paths to the same Ctr method on the server:" }), _jsx("pre", { className: "bg-slate-50 border border-slate-200 rounded-lg p-4 text-xs overflow-auto leading-relaxed text-slate-700", children: `// 1. route.json: 将所有子路径路由到同一 Ctr 方法
152
+ react-page.tsx # Page component (import + compose)` }), _jsxs("p", { className: "text-slate-500 text-xs mt-3", children: ["Mirrors the shadcn/ui", ' ', _jsx("code", { className: "bg-slate-100 px-1 rounded", children: "components/ui/" }), " convention. Client dependencies are bundled locally by the Kebab build command."] })] })] })), tab === 'routing' && (_jsxs(MemoryRouter, { children: [_jsxs(Card, { children: [_jsx("h2", { className: "font-semibold text-slate-900 mb-1", children: "React Router Demo" }), _jsxs("p", { className: "text-slate-500 text-xs mb-4", children: ["Uses ", _jsx("code", { className: "bg-slate-100 px-1 rounded", children: "MemoryRouter" }), ' ', "to demonstrate route navigation, dynamic params (useParams), and programmatic navigation (useNavigate). Works on both server SSR and client hydration with no extra server configuration."] }), _jsxs(Routes, { children: [_jsx(Route, { path: "/", element: _jsx(RouterHome, {}) }), _jsx(Route, { path: "/about", element: _jsx(RouterAbout, {}) }), _jsx(Route, { path: "/user/:id", element: _jsx(RouterUser, {}) })] })] }), _jsxs(Card, { className: "mt-4", children: [_jsx("h2", { className: "font-semibold text-slate-900 mb-2", children: "Full-Page BrowserRouter Setup" }), _jsx("p", { className: "text-slate-500 text-xs mb-3", children: "To let React Router manage the real URL (e.g. /app, /app/about), replace MemoryRouter with BrowserRouter and route all sub-paths to the same Ctr method on the server:" }), _jsx("pre", { className: "bg-slate-50 border border-slate-200 rounded-lg p-4 text-xs overflow-auto leading-relaxed text-slate-700", children: `// 1. route.json: 将所有子路径路由到同一 Ctr 方法
157
153
  {
158
154
  "app": "ctr/app@reactPage",
159
155
  "app\\/.*": "ctr/app@reactPage"
@@ -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 传入。
@@ -210,7 +206,7 @@ function ShadcnDemo() {
210
206
  All components below are imported from <code className="bg-slate-100 px-1 rounded">stc/lib/ui/</code>,
211
207
  following the same <code className="bg-slate-100 px-1 rounded">components/ui/</code> structure recommended by shadcn/ui.
212
208
  Built on Radix UI primitives, styled with Tailwind CSS.
213
- In dev mode, the framework auto-scans all bare imports and generates the import map automatically.
209
+ Client dependencies are bundled locally by the Kebab build command.
214
210
  </p>
215
211
  </Card>
216
212
 
@@ -392,8 +388,7 @@ export default function ReactPage({ title, serverTime, node, _urlBase, _urlStc,
392
388
  <meta charSet="UTF-8" />
393
389
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
394
390
  <title>{title}</title>
395
- {/* import map 由框架自动注入在此标签前,无需手动添加 */}
396
- {/* dev: Tailwind Play CDN;prod: 将此行替换为 kebab build 编译的 CSS 文件 */}
391
+ {/* CSS 和客户端水合 bundle 均由 kebab build 生成 */}
397
392
  <link href={`${_urlStc}view/react.page.css?v=${_staticVer}`} rel="stylesheet" />
398
393
  </head>
399
394
  <body className="bg-slate-50 min-h-screen font-sans">
@@ -494,8 +489,7 @@ export default function ReactPage({ title, serverTime, node, _urlBase, _urlStc,
494
489
  <p className="text-slate-500 text-xs mt-3">
495
490
  Mirrors the shadcn/ui{' '}
496
491
  <code className="bg-slate-100 px-1 rounded">components/ui/</code> convention.
497
- In dev mode, the framework auto-scans imports —
498
- no manual import map configuration needed.
492
+ Client dependencies are bundled locally by the Kebab build command.
499
493
  </p>
500
494
  </Card>
501
495