@maiyunnet/kebab 9.0.1 → 9.1.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.
Files changed (166) hide show
  1. package/LICENSE +0 -0
  2. package/README.md +0 -0
  3. package/bin/kebab.js +0 -0
  4. package/doc/kebab-rag.md +636 -626
  5. package/index.d.ts +1 -1
  6. package/index.js +1 -1
  7. package/lib/ai.d.ts +12 -10
  8. package/lib/ai.js +22 -15
  9. package/lib/buffer.d.ts +0 -0
  10. package/lib/buffer.js +0 -0
  11. package/lib/captcha/zcool-addict-italic.ttf +0 -0
  12. package/lib/captcha.d.ts +0 -0
  13. package/lib/captcha.js +0 -0
  14. package/lib/consistent.d.ts +0 -0
  15. package/lib/consistent.js +0 -0
  16. package/lib/core.d.ts +0 -0
  17. package/lib/core.js +0 -0
  18. package/lib/cron.d.ts +0 -0
  19. package/lib/cron.js +0 -0
  20. package/lib/crypto.d.ts +0 -0
  21. package/lib/crypto.js +0 -0
  22. package/lib/db/conn.d.ts +0 -0
  23. package/lib/db/conn.js +0 -0
  24. package/lib/db/pool.d.ts +0 -0
  25. package/lib/db/pool.js +0 -0
  26. package/lib/db/tran.d.ts +0 -0
  27. package/lib/db/tran.js +8 -8
  28. package/lib/db.d.ts +0 -0
  29. package/lib/db.js +0 -0
  30. package/lib/dns.d.ts +0 -0
  31. package/lib/dns.js +0 -0
  32. package/lib/fs.d.ts +0 -0
  33. package/lib/fs.js +0 -0
  34. package/lib/kv.d.ts +0 -0
  35. package/lib/kv.js +0 -0
  36. package/lib/lan.d.ts +0 -0
  37. package/lib/lan.js +0 -0
  38. package/lib/lang.d.ts +0 -0
  39. package/lib/lang.js +0 -0
  40. package/lib/net/cacert.pem +0 -0
  41. package/lib/net/formdata.d.ts +0 -0
  42. package/lib/net/formdata.js +0 -0
  43. package/lib/net/request.d.ts +0 -0
  44. package/lib/net/request.js +0 -0
  45. package/lib/net/response.d.ts +0 -0
  46. package/lib/net/response.js +0 -0
  47. package/lib/net.d.ts +0 -0
  48. package/lib/net.js +0 -0
  49. package/lib/ratelimit.d.ts +0 -0
  50. package/lib/ratelimit.js +0 -0
  51. package/lib/s3.d.ts +0 -0
  52. package/lib/s3.js +0 -0
  53. package/lib/scan.d.ts +0 -0
  54. package/lib/scan.js +0 -0
  55. package/lib/session.d.ts +0 -0
  56. package/lib/session.js +0 -0
  57. package/lib/socket.d.ts +0 -0
  58. package/lib/socket.js +0 -0
  59. package/lib/sql.d.ts +0 -0
  60. package/lib/sql.js +0 -0
  61. package/lib/ssh/sftp.d.ts +0 -0
  62. package/lib/ssh/sftp.js +0 -0
  63. package/lib/ssh/shell.d.ts +0 -0
  64. package/lib/ssh/shell.js +0 -0
  65. package/lib/ssh.d.ts +0 -0
  66. package/lib/ssh.js +0 -0
  67. package/lib/text/tld.json +0 -0
  68. package/lib/text.d.ts +0 -0
  69. package/lib/text.js +0 -0
  70. package/lib/time.d.ts +0 -0
  71. package/lib/time.js +0 -0
  72. package/lib/turnstile.d.ts +0 -0
  73. package/lib/turnstile.js +0 -0
  74. package/lib/vector.d.ts +0 -0
  75. package/lib/vector.js +0 -0
  76. package/lib/ws.d.ts +0 -0
  77. package/lib/ws.js +0 -0
  78. package/lib/zip.d.ts +0 -0
  79. package/lib/zip.js +0 -0
  80. package/lib/zlib.d.ts +0 -0
  81. package/lib/zlib.js +0 -0
  82. package/main.d.ts +0 -0
  83. package/main.js +0 -0
  84. package/package.json +9 -2
  85. package/sys/child.d.ts +0 -0
  86. package/sys/child.js +0 -0
  87. package/sys/cmd.d.ts +0 -0
  88. package/sys/cmd.js +71 -42
  89. package/sys/ctr.d.ts +8 -0
  90. package/sys/ctr.js +86 -36
  91. package/sys/master.d.ts +0 -0
  92. package/sys/master.js +0 -0
  93. package/sys/mod.d.ts +0 -0
  94. package/sys/mod.js +0 -0
  95. package/sys/monitor/watchdog.d.ts +0 -0
  96. package/sys/monitor/watchdog.js +0 -0
  97. package/sys/monitor.d.ts +0 -0
  98. package/sys/monitor.js +1 -1
  99. package/sys/route.d.ts +0 -0
  100. package/sys/route.js +0 -0
  101. package/www/example/ctr/main.d.ts +0 -0
  102. package/www/example/ctr/main.js +0 -0
  103. package/www/example/ctr/middle.d.ts +0 -0
  104. package/www/example/ctr/middle.js +0 -0
  105. package/www/example/ctr/test.d.ts +4 -0
  106. package/www/example/ctr/test.js +18 -5
  107. package/www/example/data/locale/en.test.json +0 -0
  108. package/www/example/data/locale/index.html +0 -0
  109. package/www/example/data/locale/ja.test.json +0 -0
  110. package/www/example/data/locale/sc.test.json +0 -0
  111. package/www/example/data/locale/tc.test.json +0 -0
  112. package/www/example/data/test.zip +0 -0
  113. package/www/example/kebab.json +0 -0
  114. package/www/example/mod/test.d.ts +0 -0
  115. package/www/example/mod/test.js +0 -0
  116. package/www/example/mod/testdata.d.ts +0 -0
  117. package/www/example/mod/testdata.js +0 -0
  118. package/www/example/route.json +0 -0
  119. package/www/example/stc/chunk-YJ3GYATF.js +81 -0
  120. package/www/example/stc/lib/ui/checkbox.d.ts +9 -0
  121. package/www/example/stc/lib/ui/checkbox.js +11 -0
  122. package/www/example/stc/lib/ui/checkbox.tsx +30 -0
  123. package/www/example/stc/lib/ui/input.d.ts +3 -0
  124. package/www/example/stc/lib/ui/input.js +10 -0
  125. package/www/example/stc/lib/ui/input.tsx +24 -0
  126. package/www/example/stc/lib/ui/label.d.ts +11 -0
  127. package/www/example/stc/lib/ui/label.js +13 -0
  128. package/www/example/stc/lib/ui/label.tsx +24 -0
  129. package/www/example/stc/lib/ui/switch.d.ts +9 -0
  130. package/www/example/stc/lib/ui/switch.js +11 -0
  131. package/www/example/stc/lib/ui/switch.tsx +31 -0
  132. package/www/example/stc/lib/utils.d.ts +7 -0
  133. package/www/example/stc/lib/utils.js +10 -0
  134. package/www/example/stc/view/hello.page.bundle.js +1 -0
  135. package/www/example/stc/view/hello.page.css +2 -0
  136. package/www/example/stc/view/hello.page.d.ts +17 -0
  137. package/www/example/stc/view/hello.page.js +15 -0
  138. package/www/example/stc/view/hello.page.tsx +49 -0
  139. package/www/example/stc/view/react-router.page.bundle.js +1 -0
  140. package/www/example/stc/view/react-router.page.css +2 -0
  141. package/www/example/stc/view/{react-router-page.d.ts → react-router.page.d.ts} +1 -1
  142. package/www/example/stc/view/{react-router-page.js → react-router.page.js} +1 -1
  143. package/www/example/stc/view/{react-router-page.tsx → react-router.page.tsx} +1 -1
  144. package/www/example/stc/view/react.page.bundle.js +26 -0
  145. package/www/example/stc/view/react.page.css +2 -0
  146. package/www/example/stc/view/{react-page.d.ts → react.page.d.ts} +16 -18
  147. package/www/example/stc/view/react.page.js +181 -0
  148. package/www/example/stc/view/{react-page.tsx → react.page.tsx} +259 -111
  149. package/www/example/view/test.ejs +0 -0
  150. package/www/example/ws/handler.d.ts +0 -0
  151. package/www/example/ws/handler.js +0 -0
  152. package/www/example/ws/main.d.ts +0 -0
  153. package/www/example/ws/main.js +0 -0
  154. package/www/example/ws/mproxy.d.ts +0 -0
  155. package/www/example/ws/mproxy.js +0 -0
  156. package/www/example/ws/rproxy.d.ts +0 -0
  157. package/www/example/ws/rproxy.js +0 -0
  158. package/www/example/ws/rsocket.d.ts +0 -0
  159. package/www/example/ws/rsocket.js +0 -0
  160. package/www/example/ws/test.d.ts +0 -0
  161. package/www/example/ws/test.js +0 -0
  162. package/www/example/stc/view/react-page.bundle.js +0 -97
  163. package/www/example/stc/view/react-page.css +0 -2
  164. package/www/example/stc/view/react-page.js +0 -136
  165. package/www/example/stc/view/react-router-page.bundle.js +0 -81
  166. package/www/example/stc/view/react-router-page.css +0 -2
package/sys/cmd.js CHANGED
@@ -172,6 +172,8 @@ async function run() {
172
172
  config.ai['ALICN'].skey ??= '';
173
173
  config.ai['ALIAS'] ??= {};
174
174
  config.ai['ALIAS'].skey ??= '';
175
+ config.ai['ALINE'] ??= {};
176
+ config.ai['ALINE'].skey ??= '';
175
177
  config.ai['AZURE'] ??= {};
176
178
  config.ai['AZURE'].endpoint ??= '';
177
179
  config.ai['AZURE'].skey ??= '';
@@ -349,10 +351,11 @@ async function run() {
349
351
  process.exit();
350
352
  }
351
353
  if (cmds[0] === 'build') {
352
- // --- 构建命令:使用 esbuild 打包入口 .tsx 文件为自包含 bundle ---
353
- // --- 只打包「直属于指定目录」的 .tsx,子目录内的组件文件不会被打包 ---
354
- // --- 推荐目录结构:stc/view/app.tsx(入口)+ stc/view/pages/home.tsx(子组件)---
355
- // --- 用法:node ./source/main build [-d www/myapp/stc/view] ---
354
+ // --- 构建命令:使用 esbuild 打包 *.page.tsx 文件为自包含 bundle ---
355
+ // --- 约定:文件名以 .page.tsx 结尾的为页面入口,会被打包;其余 .tsx 均视为组件,不参与打包 ---
356
+ // --- 与 Next.js 的 page.tsx 约定同理,开发者可自由组织目录结构,框架靠文件名区分入口与组件 ---
357
+ // --- 推荐目录结构:stc/view/home.page.tsx(入口)+ stc/view/components/header.tsx(组件)---
358
+ // --- 用法:node ./source/main build [-d www/myapp/stc] ---
356
359
  let targetDir = '';
357
360
  for (let i = 1; i < cmds.length; i++) {
358
361
  if ((cmds[i] === '-d' || cmds[i] === '--dir') && cmds[i + 1]) {
@@ -360,7 +363,7 @@ async function run() {
360
363
  i++;
361
364
  }
362
365
  }
363
- /** --- 扫描目录下直属的 .tsx 文件(非递归),子目录内文件不作为打包入口 --- */
366
+ /** --- 递归扫描目录,收集所有 *.page.tsx 文件作为打包入口 --- */
364
367
  const entryPoints = [];
365
368
  const scanDir = async (dir) => {
366
369
  const items = await lFs.readDir(dir);
@@ -368,13 +371,16 @@ async function run() {
368
371
  if (item.name === '.' || item.name === '..') {
369
372
  continue;
370
373
  }
371
- if (!item.isDirectory() && item.name.endsWith('.tsx')) {
374
+ if (item.isDirectory()) {
375
+ await scanDir(`${dir}/${item.name}`);
376
+ }
377
+ else if (item.name.endsWith('.page.tsx')) {
372
378
  entryPoints.push(`${dir}/${item.name}`);
373
379
  }
374
380
  }
375
381
  };
376
382
  if (targetDir) {
377
- // --- 指定了目录,只扫描该目录下的直属 .tsx ---
383
+ // --- 指定了目录:递归扫描该目录下所有 *.page.tsx ---
378
384
  const absTarget = targetDir.startsWith('/') || /^[A-Za-z]:/.test(targetDir)
379
385
  ? targetDir
380
386
  : kebab.ROOT_CWD + targetDir.replace(/^\//, '');
@@ -388,8 +394,7 @@ async function run() {
388
394
  }
389
395
  }
390
396
  else {
391
- // --- 未指定目录:扫描 www/*/stc/ 及其直接子目录下的 .tsx ---
392
- // --- 即扫描 stc/*.tsx 和 stc/*/*.tsx(如 stc/view/app.tsx),不再深入 ---
397
+ // --- 未指定目录:递归扫描所有站点的 www/*/stc/ ---
393
398
  const wwwItems = await lFs.readDir(kebab.WWW_CWD);
394
399
  for (const wwwItem of wwwItems) {
395
400
  if (wwwItem.name === '.' || wwwItem.name === '..' || !wwwItem.isDirectory()) {
@@ -399,16 +404,7 @@ async function run() {
399
404
  if (!await lFs.isDir(stcPath)) {
400
405
  continue;
401
406
  }
402
- // --- 扫描 stc/ 直属文件 ---
403
407
  await scanDir(stcPath);
404
- // --- 扫描 stc/ 的直接子目录(如 view/),取其顶层 .tsx 作为入口 ---
405
- const stcItems = await lFs.readDir(stcPath);
406
- for (const stcItem of stcItems) {
407
- if (stcItem.name === '.' || stcItem.name === '..' || !stcItem.isDirectory()) {
408
- continue;
409
- }
410
- await scanDir(`${stcPath}/${stcItem.name}`);
411
- }
412
408
  }
413
409
  }
414
410
  if (entryPoints.length === 0) {
@@ -418,17 +414,32 @@ async function run() {
418
414
  }
419
415
  lCore.display('KEBAB', 'BUILD', `Found ${entryPoints.length} file(s).`);
420
416
  const esbuild = await import('esbuild');
417
+ // --- 按 stc/ 根目录分组:同一站点内所有页面合并为一次 esbuild 构建 ---
418
+ // --- 不同站点(不同 stc/)相互隔离,同站点内跨子目录页面共享 chunk ---
419
+ // --- splitting: true 自动将任意共享 import(React/Header/utils 等)提取为独立 chunk ---
420
+ /**
421
+ * --- 查找路径中最后一段名为 stc 的祖先目录 ---
422
+ */
423
+ const getStcRoot = (filePath) => {
424
+ const parts = filePath.split('/');
425
+ const idx = parts.lastIndexOf('stc');
426
+ return idx >= 0 ? parts.slice(0, idx + 1).join('/') : parts.slice(0, -1).join('/');
427
+ };
428
+ const byStc = new Map();
421
429
  for (const entry of entryPoints) {
422
- const outfile = entry.replace(/\.tsx$/, '.bundle.js');
423
- lCore.display('KEBAB', 'BUILD', entry.replace(kebab.ROOT_CWD, ''), '→', outfile.replace(kebab.ROOT_CWD, ''));
424
- try {
425
- /** --- 组件文件名(不含扩展名),用于生成 stdin 入口的 import 语句 --- */
430
+ const stcRoot = getStcRoot(entry);
431
+ if (!byStc.has(stcRoot)) {
432
+ byStc.set(stcRoot, []);
433
+ }
434
+ byStc.get(stcRoot).push(entry);
435
+ }
436
+ for (const [stcRoot, entries] of byStc) {
437
+ // --- 为每个页面写入水合入口临时文件(实际文件,splitting 不支持 stdin 多入口)---
438
+ const tempFiles = [];
439
+ for (const entry of entries) {
426
440
  const basename = entry.split('/').pop().replace(/\.tsx$/, '');
427
- /** --- 组件所在目录,esbuild 以此为基准解析相对 import --- */
428
- const resolveDir = entry.substring(0, entry.lastIndexOf('/'));
429
- // --- stdin 入口:含水合逻辑,_routerBase 在 props 中时自动包裹 BrowserRouter ---
430
- // --- React/react-dom/react-router-dom 全部打入 bundle,浏览器只加载一个 JS ---
431
- const stdinContent = [
441
+ const entryDir = entry.substring(0, entry.lastIndexOf('/'));
442
+ const hydrateCode = [
432
443
  `import{hydrateRoot}from'react-dom/client';`,
433
444
  `import{createElement}from'react';`,
434
445
  `import{BrowserRouter}from'react-router-dom';`,
@@ -443,31 +454,52 @@ async function run() {
443
454
  `}`,
444
455
  `}`,
445
456
  ].join('');
457
+ const tempFile = `${entryDir}/${basename}.hydrate.tsx`;
458
+ await lFs.putContent(tempFile, hydrateCode);
459
+ tempFiles.push(tempFile);
460
+ lCore.display('KEBAB', 'BUILD', entry.replace(kebab.ROOT_CWD, ''), '→', entry.replace(kebab.ROOT_CWD, '').replace(/\.tsx$/, '.bundle.js'));
461
+ }
462
+ // --- JS 构建:整站一次构建,splitting 按实际共享情况自动提取 chunk ---
463
+ // --- outbase = outdir = stcRoot,入口保留子目录结构,chunk 落在 stcRoot 根 ---
464
+ try {
446
465
  await esbuild.build({
447
- 'stdin': {
448
- 'contents': stdinContent,
449
- 'resolveDir': resolveDir,
450
- 'sourcefile': basename + '.hydrate.tsx',
451
- 'loader': 'tsx',
452
- },
466
+ 'entryPoints': tempFiles,
453
467
  'bundle': true,
454
- // --- 无 external:React 全部打入 bundle,生成自包含文件,浏览器只加载一个 JS ---
468
+ 'splitting': true,
455
469
  'format': 'esm',
456
470
  'jsx': 'automatic',
457
471
  'jsxImportSource': 'react',
458
472
  'platform': 'browser',
459
473
  'target': 'es2022',
460
474
  'minify': true,
461
- 'outfile': outfile,
475
+ 'outbase': stcRoot,
476
+ 'outdir': stcRoot,
477
+ 'entryNames': '[dir]/[name]',
478
+ 'chunkNames': 'chunk-[hash]',
462
479
  });
463
- lCore.display('KEBAB', 'BUILD', 'JS', outfile.replace(kebab.ROOT_CWD, ''));
464
- // --- 构建 Tailwind CSS:生成同名 .css 文件 ---
480
+ // --- *.hydrate.js 重命名为 *.bundle.js ---
481
+ for (const tempFile of tempFiles) {
482
+ const hydrateJs = tempFile.replace(/\.tsx$/, '.js');
483
+ const bundleJs = hydrateJs.replace(/\.hydrate\.js$/, '.bundle.js');
484
+ await lFs.rename(hydrateJs, bundleJs);
485
+ lCore.display('KEBAB', 'BUILD', 'JS', bundleJs.replace(kebab.ROOT_CWD, ''));
486
+ }
487
+ }
488
+ catch (e) {
489
+ lCore.display('KEBAB', 'BUILD', 'JS FAILED', stcRoot.replace(kebab.ROOT_CWD, ''), e.message ?? '');
490
+ }
491
+ finally {
492
+ // --- 清理临时水合入口文件 ---
493
+ for (const tempFile of tempFiles) {
494
+ await lFs.unlink(tempFile);
495
+ }
496
+ }
497
+ // --- CSS:每个入口单独构建 Tailwind ---
498
+ for (const entry of entries) {
465
499
  const cssOut = entry.replace(/\.tsx$/, '.css');
466
- /** --- 临时 CSS 输入文件,@source 扫描当前目录及子目录所有 tsx 文件 --- */
467
500
  const tmpCss = entry + '.__tw__.css';
468
501
  await lFs.putContent(tmpCss, `@import "tailwindcss";\n@source "./**/*.tsx";\n`);
469
502
  try {
470
- /** --- 直接用当前 Node.js 执行 CLI 的 JS 入口,跨平台无需 shell --- */
471
503
  const twJs = fileURLToPath(new URL('../node_modules/@tailwindcss/cli/dist/index.mjs', import.meta.url));
472
504
  await new Promise((resolve, reject) => {
473
505
  const proc = childProcess.spawn(process.execPath, [twJs, '-i', tmpCss, '-o', cssOut, '--minify'], { 'shell': false });
@@ -491,9 +523,6 @@ async function run() {
491
523
  await lFs.unlink(tmpCss);
492
524
  }
493
525
  }
494
- catch (e) {
495
- lCore.display('KEBAB', 'BUILD', 'FAILED', entry.replace(kebab.ROOT_CWD, ''), e.message ?? '');
496
- }
497
526
  }
498
527
  lCore.display('DONE');
499
528
  process.exit();
package/sys/ctr.d.ts CHANGED
@@ -185,6 +185,14 @@ export declare class Ctr {
185
185
  */
186
186
  'staticPath'?: string;
187
187
  }): Promise<string>;
188
+ /**
189
+ * --- 递归扫描 JS 文件中的 import 语句,收集第三方 bare specifier ---
190
+ * @param filePath 当前要扫描的文件绝对路径
191
+ * @param scannedFiles 已扫描文件集(去重用)
192
+ * @param extraImports 收集到的第三方包名集合
193
+ * @param builtinImports 内置 import map,已有条目不重复添加
194
+ */
195
+ private _scanImports;
188
196
  /**
189
197
  * --- 设置校验错误返回值 ---
190
198
  * @param rtn 返回值数组
package/sys/ctr.js CHANGED
@@ -249,8 +249,9 @@ export class Ctr {
249
249
  * @param opt 可选配置
250
250
  */
251
251
  async _loadReactPage(path, props = {}, opt = {}) {
252
+ // --- 约定:传入路径不含 .page 后缀,框架自动补全(对应 build 命令的 *.page.tsx 约定)---
252
253
  // --- 组件 JS 从 stc 目录读取,浏览器同样通过 staticPath(支持 CDN)下载 ---
253
- const componentPath = this._config.const.rootPath + 'stc/' + path + '.js';
254
+ const componentPath = this._config.const.rootPath + 'stc/' + path + '.page.js';
254
255
  if (!await lFs.isFile(componentPath)) {
255
256
  return '';
256
257
  }
@@ -280,56 +281,67 @@ export class Ctr {
280
281
  '_locale': this._locale,
281
282
  '_localeData': localeData,
282
283
  };
284
+ // --- 框架自动注入的 HTML 片段,用户组件无需手动渲染 ---
285
+ let headInject = '';
286
+ let bodyInject = '';
283
287
  if (opt.hydrate !== false) {
284
288
  const reactVer = opt.reactVer ?? '19';
285
289
  const esm = 'https://esm.sh/';
286
290
  // --- 检查是否有 npx kebab build 生成的自包含预构建包 ---
287
- const bundlePath = this._config.const.rootPath + 'stc/' + path + '.bundle.js';
291
+ const bundlePath = this._config.const.rootPath + 'stc/' + path + '.page.bundle.js';
288
292
  const hasBundle = await lFs.isFile(bundlePath);
289
293
  if (opt.router === 'browser') {
290
- // --- BrowserRouter 模式:_routerBase 注入 props,bundle 读取此值决定是否包裹 BrowserRouter ---
294
+ // --- BrowserRouter 模式:_routerBase 注入 props,供水合脚本读取 ---
291
295
  const base = opt.routerBase ?? '';
292
296
  const routerBase = this._config.const.urlBase + base.replace(/^\//, '');
293
297
  fullProps['_routerBase'] = routerBase.replace(/\/$/, '');
294
298
  }
299
+ // --- propsJson 在渲染前序列化,框架直接注入 HTML,组件无需手动渲染 ---
300
+ const propsJson = lText.stringifyJson(fullProps).replace(/<\/script>/gi, '<\\/script>');
301
+ let hydrateScript;
295
302
  if (hasBundle) {
296
- // --- bundle 模式:bundle 自包含 React + 水合逻辑,无需 import map,一个 JS 文件搞定 ---
297
- const clientUrl = `${staticPath}${path}.bundle.js?v=${this._config.set.staticVer}`;
298
- fullProps['_hydrateScript'] = `import'${clientUrl}';`;
303
+ // --- bundle 模式:bundle 自包含 React + 水合逻辑,无需 import map ---
304
+ const clientUrl = `${staticPath}${path}.page.bundle.js?v=${this._config.set.staticVer}`;
305
+ hydrateScript = `import'${clientUrl}';`;
299
306
  }
300
307
  else {
301
308
  // --- 开发模式(tsc 编译 .js):通过 esm.sh import map 解析 bare import ---
302
- const clientUrl = `${staticPath}${path}.js?v=${this._config.set.staticVer}`;
303
- fullProps['_importMapJson'] = lText.stringifyJson({
304
- 'imports': {
305
- 'react': `${esm}react@${reactVer}`,
306
- 'react-dom': `${esm}react-dom@${reactVer}`,
307
- 'react-dom/client': `${esm}react-dom@${reactVer}/client`,
308
- 'react/jsx-runtime': `${esm}react@${reactVer}/jsx-runtime`,
309
- 'react-router-dom': `${esm}react-router-dom@7?external=react,react-dom`,
310
- },
311
- });
312
- if (opt.router === 'browser') {
313
- fullProps['_hydrateScript'] =
314
- `import{hydrateRoot}from'react-dom/client';` +
315
- `import{createElement}from'react';` +
316
- `import{BrowserRouter}from'react-router-dom';` +
317
- `import App from'${clientUrl}';` +
318
- `const p=JSON.parse(document.getElementById('__kebab_props__').textContent);` +
319
- `hydrateRoot(document,createElement(BrowserRouter,{basename:p._routerBase},createElement(App,p)));`;
320
- }
321
- else {
322
- fullProps['_hydrateScript'] =
323
- `import{hydrateRoot}from'react-dom/client';` +
324
- `import{createElement}from'react';` +
325
- `import App from'${clientUrl}';` +
326
- `const p=JSON.parse(document.getElementById('__kebab_props__').textContent);` +
327
- `hydrateRoot(document,createElement(App,p));`;
309
+ const clientUrl = `${staticPath}${path}.page.js?v=${this._config.set.staticVer}`;
310
+ // --- 内置 import map 条目(React 生态核心包)---
311
+ const builtinImports = {
312
+ 'react': `${esm}react@${reactVer}`,
313
+ 'react-dom': `${esm}react-dom@${reactVer}`,
314
+ 'react-dom/client': `${esm}react-dom@${reactVer}/client`,
315
+ 'react/jsx-runtime': `${esm}react@${reactVer}/jsx-runtime`,
316
+ 'react-router-dom': `${esm}react-router-dom@7?external=react,react-dom`,
317
+ };
318
+ // --- 自动扫描入口 JS 及其相对引用,收集所有第三方 bare specifier ---
319
+ const scannedFiles = new Set();
320
+ const extraImports = new Set();
321
+ await this._scanImports(componentPath, scannedFiles, extraImports, builtinImports);
322
+ // --- 第三方包统一通过 esm.sh 解析,external react/react-dom 避免重复加载 ---
323
+ for (const pkg of extraImports) {
324
+ builtinImports[pkg] = `${esm}${pkg}?external=react,react-dom`;
328
325
  }
326
+ // --- import map 注入到 </head> 前 ---
327
+ headInject = `<script type="importmap">${lText.stringifyJson({ 'imports': builtinImports })}</script>`;
328
+ // --- BrowserRouter 模式多一段 Router 导入与包裹层 ---
329
+ const routerImport = opt.router === 'browser' ? `import{BrowserRouter}from'react-router-dom';` : '';
330
+ const routerCreate = opt.router === 'browser'
331
+ ? `createElement(BrowserRouter,{basename:p._routerBase},createElement(App,p))`
332
+ : `createElement(App,p)`;
333
+ hydrateScript =
334
+ `import{hydrateRoot}from'react-dom/client';` +
335
+ `import{createElement}from'react';` +
336
+ routerImport +
337
+ `import App from'${clientUrl}';` +
338
+ `const p=JSON.parse(document.getElementById('__kebab_props__').textContent);` +
339
+ `hydrateRoot(document,${routerCreate});`;
329
340
  }
330
- // --- _propsJson 序列化当前 fullProps(不含 _propsJson 本身,避免循环引用)---
331
- // --- 客户端水合时读取此 JSON,_propsJson 缺失,suppressHydrationWarning 处理差异 ---
332
- fullProps['_propsJson'] = lText.stringifyJson(fullProps).replace(/<\/script>/gi, '<\\/script>');
341
+ // --- props JSON + 水合脚本注入到 </body> 前 ---
342
+ bodyInject =
343
+ `<script id="__kebab_props__" type="application/json">${propsJson}</script>` +
344
+ `<script type="module">${hydrateScript}</script>`;
333
345
  }
334
346
  // --- BrowserRouter 模式:服务端用 StaticRouter 渲染,与客户端的 BrowserRouter 等价 ---
335
347
  // --- component 来自动态 import,TypeScript 无法精确推断,需要明确限定 element 类型 ---
@@ -343,13 +355,51 @@ export class Ctr {
343
355
  'basename': fullProps['_routerBase'],
344
356
  }, react.createElement(component, fullProps));
345
357
  }
346
- return '<!DOCTYPE html>' + reactDomServer.renderToString(element);
358
+ // --- 框架将 import map 注入 </head> 前,props JSON + 水合脚本注入 </body> 前 ---
359
+ let html = '<!DOCTYPE html>' + reactDomServer.renderToString(element);
360
+ if (opt.hydrate !== false) {
361
+ html = html.replace('</head>', headInject + '</head>');
362
+ html = html.replace('</body>', bodyInject + '</body>');
363
+ }
364
+ return html;
347
365
  }
348
366
  catch (e) {
349
367
  lCore.debug(`[CTR][_loadReactPage] ${e.message ?? ''}`);
350
368
  return '';
351
369
  }
352
370
  }
371
+ /**
372
+ * --- 递归扫描 JS 文件中的 import 语句,收集第三方 bare specifier ---
373
+ * @param filePath 当前要扫描的文件绝对路径
374
+ * @param scannedFiles 已扫描文件集(去重用)
375
+ * @param extraImports 收集到的第三方包名集合
376
+ * @param builtinImports 内置 import map,已有条目不重复添加
377
+ */
378
+ async _scanImports(filePath, scannedFiles, extraImports, builtinImports) {
379
+ if (scannedFiles.has(filePath)) {
380
+ return;
381
+ }
382
+ scannedFiles.add(filePath);
383
+ const src = await lFs.getContent(filePath, 'utf8');
384
+ if (!src) {
385
+ return;
386
+ }
387
+ const re = /\bfrom\s*['"]([^'"]+)['"]/g;
388
+ let m;
389
+ while ((m = re.exec(src)) !== null) {
390
+ const spec = m[1];
391
+ if (spec.startsWith('./') || spec.startsWith('../')) {
392
+ // --- 相对引用:解析为绝对路径后递归扫描 ---
393
+ const dir = filePath.substring(0, filePath.lastIndexOf('/') + 1);
394
+ const resolved = new URL(spec, 'file://' + dir).pathname;
395
+ await this._scanImports(resolved, scannedFiles, extraImports, builtinImports);
396
+ }
397
+ else if (!spec.startsWith('/') && !spec.startsWith('http') && !(spec in builtinImports)) {
398
+ // --- 第三方 bare specifier:加入 import map ---
399
+ extraImports.add(spec);
400
+ }
401
+ }
402
+ }
353
403
  /**
354
404
  * --- 设置校验错误返回值 ---
355
405
  * @param rtn 返回值数组
package/sys/master.d.ts CHANGED
File without changes
package/sys/master.js CHANGED
File without changes
package/sys/mod.d.ts CHANGED
File without changes
package/sys/mod.js CHANGED
File without changes
File without changes
File without changes
package/sys/monitor.d.ts CHANGED
File without changes
package/sys/monitor.js CHANGED
@@ -411,7 +411,7 @@ function logSpike(alerts, cpuPercent, cpuOs, eloopLag, blocked = false) {
411
411
  */
412
412
  function getOsCpuPercent() {
413
413
  const cpus = os.cpus();
414
- if (!lastOsCpus || lastOsCpus.length !== cpus.length) {
414
+ if (lastOsCpus?.length !== cpus.length) {
415
415
  lastOsCpus = cpus;
416
416
  return 0;
417
417
  }
package/sys/route.d.ts CHANGED
File without changes
package/sys/route.js CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -127,6 +127,10 @@ export default class extends sCtr.Ctr {
127
127
  * --- Rate Limit 限流测试 ---
128
128
  */
129
129
  ratelimit(): Promise<string>;
130
+ /**
131
+ * --- React 最简页面示例:展示一个 React 页面最少需要写哪些内容 ---
132
+ */
133
+ reactHelloPage(): Promise<string>;
130
134
  /**
131
135
  * --- React 全页模式测试:组件自主渲染完整 HTML 文档 + 客户端水合,无需 EJS ---
132
136
  */
@@ -98,6 +98,7 @@ export default class extends sCtr.Ctr {
98
98
  `<br><a href="${this._config.const.urlBase}test/react-page">View "test/react-page" (SSR)</a>`,
99
99
  `<br><a href="${this._config.const.urlBase}test/react-router-page">View "test/react-router-page" (SSR + BrowserRouter)</a>`,
100
100
  `<br><a href="${this._config.const.urlBase}test/react-router-page-data?path=/user">View "test/react-router-page-data?path=/user" (Data API)</a>`,
101
+ `<br><a href="${this._config.const.urlBase}test/react-hello-page">View "test/react-hello-page" (React Hello Page)</a>`,
101
102
  '<br><br><b>Return json:</b>',
102
103
  `<br><br><a href="${this._config.const.urlBase}test/json?type=1">View "test/json?type=1"</a>`,
103
104
  `<br><a href="${this._config.const.urlBase}test/json?type=2">View "test/json?type=2"</a>`,
@@ -161,7 +162,7 @@ export default class extends sCtr.Ctr {
161
162
  `<br><br><a href="${this._config.const.urlBase}test/crypto">View "test/crypto"</a>`,
162
163
  '<br><br><b>Db:</b>',
163
164
  `<br><br><a href="${this._config.const.urlBase}test/db">View "test/db"</a> <a href="${this._config.const.urlBase}test/db?s=pgsql">pgsql</a>`,
164
- `<br><br><a href="${this._config.const.urlBase}test/db-read">View "test/db-read"</a>`,
165
+ `<br><a href="${this._config.const.urlBase}test/db-read">View "test/db-read"</a>`,
165
166
  `<br><br><b>Vector:</b>`,
166
167
  `<br><br><a href="${this._config.const.urlBase}test/vector">View "test/vector"</a>`,
167
168
  '<br><br><b>Kv:</b>',
@@ -3519,7 +3520,9 @@ rtn.push(reader.readBCDString());</pre>${JSON.stringify(rtn)}`);
3519
3520
  // --- 文生图 ---
3520
3521
  let model = 'z-image-turbo';
3521
3522
  let size = [1280, 720];
3522
- if (ai.service !== lAi.ESERVICE.ALICN && ai.service !== lAi.ESERVICE.ALIAS) {
3523
+ if (ai.service !== lAi.ESERVICE.ALICN &&
3524
+ ai.service !== lAi.ESERVICE.ALIAS &&
3525
+ ai.service !== lAi.ESERVICE.ALINE) {
3523
3526
  switch (ai.service) {
3524
3527
  case lAi.ESERVICE.AZURE:
3525
3528
  case lAi.ESERVICE.AZURE2:
@@ -3562,7 +3565,9 @@ rtn.push(reader.readBCDString());</pre>${JSON.stringify(rtn)}`);
3562
3565
  let model = 'wan2.6-image';
3563
3566
  let size = [1280, 720];
3564
3567
  const prompt = '用图1的绘画风格重绘图2的场景,桌上增加一盘番茄炒蛋';
3565
- if (ai.service !== lAi.ESERVICE.ALICN && ai.service !== lAi.ESERVICE.ALIAS) {
3568
+ if (ai.service !== lAi.ESERVICE.ALICN &&
3569
+ ai.service !== lAi.ESERVICE.ALIAS &&
3570
+ ai.service !== lAi.ESERVICE.ALINE) {
3566
3571
  switch (ai.service) {
3567
3572
  default: {
3568
3573
  // --- 火山引擎 ---
@@ -3903,12 +3908,20 @@ send.addEventListener('click', async () => {
3903
3908
  echo.push(`Result: ${JSON.stringify(rtn2)}`);
3904
3909
  return echo.join('') + '<br><br>' + this._getEnd();
3905
3910
  }
3911
+ /**
3912
+ * --- React 最简页面示例:展示一个 React 页面最少需要写哪些内容 ---
3913
+ */
3914
+ async reactHelloPage() {
3915
+ return this._loadReactPage('view/hello', {
3916
+ 'greeting': 'Hello, Kebab React!',
3917
+ });
3918
+ }
3906
3919
  /**
3907
3920
  * --- React 全页模式测试:组件自主渲染完整 HTML 文档 + 客户端水合,无需 EJS ---
3908
3921
  */
3909
3922
  async reactPage() {
3910
3923
  // --- Ctr 方法负责数据准备(可以查数据库、调接口等),组件不包含任何服务端专属代码 ---
3911
- return this._loadReactPage('view/react-page', {
3924
+ return this._loadReactPage('view/react', {
3912
3925
  'title': 'Kebab React Full Page',
3913
3926
  'serverTime': lTime.format(this, 'Y-m-d H:i:s'),
3914
3927
  'node': process.version,
@@ -3952,7 +3965,7 @@ send.addEventListener('click', async () => {
3952
3965
  ? (reqUrl.substring(basePrefix.length) || '/')
3953
3966
  : '/';
3954
3967
  const routeData = this._getRouteData(routePath);
3955
- return this._loadReactPage('view/react-router-page', {
3968
+ return this._loadReactPage('view/react-router', {
3956
3969
  'title': 'Kebab React Router',
3957
3970
  'serverTime': lTime.format(this, 'Y-m-d H:i:s'),
3958
3971
  'node': process.version,
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes