@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 +26 -32
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/package.json +1 -1
- package/sys/ctr.d.ts +2 -12
- package/sys/ctr.js +23 -94
- 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/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.
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
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/package.json
CHANGED
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
|
-
/** ---
|
|
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
|
-
// ---
|
|
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
|
-
// ---
|
|
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
|
-
|
|
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"
|
|
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
|
-
// --- 框架将
|
|
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 返回值数组
|
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 }) {
|
|
@@ -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 { useState, type ReactElement } from 'react';
|
|
11
12
|
|
|
@@ -15,13 +15,13 @@
|
|
|
15
15
|
* 3. 嵌套路由通过 Outlet 渲染子路由内容
|
|
16
16
|
*
|
|
17
17
|
* 【编译方式】
|
|
18
|
-
* tsc watch 会自动编译本文件为同路径的 .js
|
|
19
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
* 框架自动将
|
|
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
|
-
* -
|
|
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
|
-
* 框架自动将
|
|
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
|
-
* -
|
|
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.
|
|
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.
|
|
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
|
-
* 框架自动将
|
|
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
|
-
* -
|
|
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
|
-
|
|
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
|
-
{/*
|
|
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
|
-
|
|
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
|
|