@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.
- package/LICENSE +0 -0
- package/README.md +0 -0
- package/bin/kebab.js +0 -0
- package/doc/kebab-rag.md +636 -626
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/lib/ai.d.ts +12 -10
- package/lib/ai.js +22 -15
- package/lib/buffer.d.ts +0 -0
- package/lib/buffer.js +0 -0
- package/lib/captcha/zcool-addict-italic.ttf +0 -0
- package/lib/captcha.d.ts +0 -0
- package/lib/captcha.js +0 -0
- package/lib/consistent.d.ts +0 -0
- package/lib/consistent.js +0 -0
- package/lib/core.d.ts +0 -0
- package/lib/core.js +0 -0
- package/lib/cron.d.ts +0 -0
- package/lib/cron.js +0 -0
- package/lib/crypto.d.ts +0 -0
- package/lib/crypto.js +0 -0
- package/lib/db/conn.d.ts +0 -0
- package/lib/db/conn.js +0 -0
- package/lib/db/pool.d.ts +0 -0
- package/lib/db/pool.js +0 -0
- package/lib/db/tran.d.ts +0 -0
- package/lib/db/tran.js +8 -8
- package/lib/db.d.ts +0 -0
- package/lib/db.js +0 -0
- package/lib/dns.d.ts +0 -0
- package/lib/dns.js +0 -0
- package/lib/fs.d.ts +0 -0
- package/lib/fs.js +0 -0
- package/lib/kv.d.ts +0 -0
- package/lib/kv.js +0 -0
- package/lib/lan.d.ts +0 -0
- package/lib/lan.js +0 -0
- package/lib/lang.d.ts +0 -0
- package/lib/lang.js +0 -0
- package/lib/net/cacert.pem +0 -0
- package/lib/net/formdata.d.ts +0 -0
- package/lib/net/formdata.js +0 -0
- package/lib/net/request.d.ts +0 -0
- package/lib/net/request.js +0 -0
- package/lib/net/response.d.ts +0 -0
- package/lib/net/response.js +0 -0
- package/lib/net.d.ts +0 -0
- package/lib/net.js +0 -0
- package/lib/ratelimit.d.ts +0 -0
- package/lib/ratelimit.js +0 -0
- package/lib/s3.d.ts +0 -0
- package/lib/s3.js +0 -0
- package/lib/scan.d.ts +0 -0
- package/lib/scan.js +0 -0
- package/lib/session.d.ts +0 -0
- package/lib/session.js +0 -0
- package/lib/socket.d.ts +0 -0
- package/lib/socket.js +0 -0
- package/lib/sql.d.ts +0 -0
- package/lib/sql.js +0 -0
- package/lib/ssh/sftp.d.ts +0 -0
- package/lib/ssh/sftp.js +0 -0
- package/lib/ssh/shell.d.ts +0 -0
- package/lib/ssh/shell.js +0 -0
- package/lib/ssh.d.ts +0 -0
- package/lib/ssh.js +0 -0
- package/lib/text/tld.json +0 -0
- package/lib/text.d.ts +0 -0
- package/lib/text.js +0 -0
- package/lib/time.d.ts +0 -0
- package/lib/time.js +0 -0
- package/lib/turnstile.d.ts +0 -0
- package/lib/turnstile.js +0 -0
- package/lib/vector.d.ts +0 -0
- package/lib/vector.js +0 -0
- package/lib/ws.d.ts +0 -0
- package/lib/ws.js +0 -0
- package/lib/zip.d.ts +0 -0
- package/lib/zip.js +0 -0
- package/lib/zlib.d.ts +0 -0
- package/lib/zlib.js +0 -0
- package/main.d.ts +0 -0
- package/main.js +0 -0
- package/package.json +9 -2
- package/sys/child.d.ts +0 -0
- package/sys/child.js +0 -0
- package/sys/cmd.d.ts +0 -0
- package/sys/cmd.js +71 -42
- package/sys/ctr.d.ts +8 -0
- package/sys/ctr.js +86 -36
- package/sys/master.d.ts +0 -0
- package/sys/master.js +0 -0
- package/sys/mod.d.ts +0 -0
- package/sys/mod.js +0 -0
- package/sys/monitor/watchdog.d.ts +0 -0
- package/sys/monitor/watchdog.js +0 -0
- package/sys/monitor.d.ts +0 -0
- package/sys/monitor.js +1 -1
- package/sys/route.d.ts +0 -0
- package/sys/route.js +0 -0
- package/www/example/ctr/main.d.ts +0 -0
- package/www/example/ctr/main.js +0 -0
- package/www/example/ctr/middle.d.ts +0 -0
- package/www/example/ctr/middle.js +0 -0
- package/www/example/ctr/test.d.ts +4 -0
- package/www/example/ctr/test.js +18 -5
- package/www/example/data/locale/en.test.json +0 -0
- package/www/example/data/locale/index.html +0 -0
- package/www/example/data/locale/ja.test.json +0 -0
- package/www/example/data/locale/sc.test.json +0 -0
- package/www/example/data/locale/tc.test.json +0 -0
- package/www/example/data/test.zip +0 -0
- package/www/example/kebab.json +0 -0
- package/www/example/mod/test.d.ts +0 -0
- package/www/example/mod/test.js +0 -0
- package/www/example/mod/testdata.d.ts +0 -0
- package/www/example/mod/testdata.js +0 -0
- package/www/example/route.json +0 -0
- package/www/example/stc/chunk-YJ3GYATF.js +81 -0
- package/www/example/stc/lib/ui/checkbox.d.ts +9 -0
- package/www/example/stc/lib/ui/checkbox.js +11 -0
- package/www/example/stc/lib/ui/checkbox.tsx +30 -0
- package/www/example/stc/lib/ui/input.d.ts +3 -0
- package/www/example/stc/lib/ui/input.js +10 -0
- package/www/example/stc/lib/ui/input.tsx +24 -0
- package/www/example/stc/lib/ui/label.d.ts +11 -0
- package/www/example/stc/lib/ui/label.js +13 -0
- package/www/example/stc/lib/ui/label.tsx +24 -0
- package/www/example/stc/lib/ui/switch.d.ts +9 -0
- package/www/example/stc/lib/ui/switch.js +11 -0
- package/www/example/stc/lib/ui/switch.tsx +31 -0
- package/www/example/stc/lib/utils.d.ts +7 -0
- package/www/example/stc/lib/utils.js +10 -0
- package/www/example/stc/view/hello.page.bundle.js +1 -0
- package/www/example/stc/view/hello.page.css +2 -0
- package/www/example/stc/view/hello.page.d.ts +17 -0
- package/www/example/stc/view/hello.page.js +15 -0
- package/www/example/stc/view/hello.page.tsx +49 -0
- package/www/example/stc/view/react-router.page.bundle.js +1 -0
- package/www/example/stc/view/react-router.page.css +2 -0
- package/www/example/stc/view/{react-router-page.d.ts → react-router.page.d.ts} +1 -1
- package/www/example/stc/view/{react-router-page.js → react-router.page.js} +1 -1
- package/www/example/stc/view/{react-router-page.tsx → react-router.page.tsx} +1 -1
- package/www/example/stc/view/react.page.bundle.js +26 -0
- package/www/example/stc/view/react.page.css +2 -0
- package/www/example/stc/view/{react-page.d.ts → react.page.d.ts} +16 -18
- package/www/example/stc/view/react.page.js +181 -0
- package/www/example/stc/view/{react-page.tsx → react.page.tsx} +259 -111
- package/www/example/view/test.ejs +0 -0
- package/www/example/ws/handler.d.ts +0 -0
- package/www/example/ws/handler.js +0 -0
- package/www/example/ws/main.d.ts +0 -0
- package/www/example/ws/main.js +0 -0
- package/www/example/ws/mproxy.d.ts +0 -0
- package/www/example/ws/mproxy.js +0 -0
- package/www/example/ws/rproxy.d.ts +0 -0
- package/www/example/ws/rproxy.js +0 -0
- package/www/example/ws/rsocket.d.ts +0 -0
- package/www/example/ws/rsocket.js +0 -0
- package/www/example/ws/test.d.ts +0 -0
- package/www/example/ws/test.js +0 -0
- package/www/example/stc/view/react-page.bundle.js +0 -97
- package/www/example/stc/view/react-page.css +0 -2
- package/www/example/stc/view/react-page.js +0 -136
- package/www/example/stc/view/react-router-page.bundle.js +0 -81
- 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
|
|
353
|
-
// ---
|
|
354
|
-
// ---
|
|
355
|
-
// ---
|
|
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
|
-
/** ---
|
|
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 (
|
|
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
|
-
// ---
|
|
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
|
-
// ---
|
|
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
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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
|
-
|
|
428
|
-
const
|
|
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
|
-
'
|
|
448
|
-
'contents': stdinContent,
|
|
449
|
-
'resolveDir': resolveDir,
|
|
450
|
-
'sourcefile': basename + '.hydrate.tsx',
|
|
451
|
-
'loader': 'tsx',
|
|
452
|
-
},
|
|
466
|
+
'entryPoints': tempFiles,
|
|
453
467
|
'bundle': true,
|
|
454
|
-
|
|
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
|
-
'
|
|
475
|
+
'outbase': stcRoot,
|
|
476
|
+
'outdir': stcRoot,
|
|
477
|
+
'entryNames': '[dir]/[name]',
|
|
478
|
+
'chunkNames': 'chunk-[hash]',
|
|
462
479
|
});
|
|
463
|
-
|
|
464
|
-
|
|
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
|
|
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
|
|
297
|
-
const clientUrl = `${staticPath}${path}.bundle.js?v=${this._config.set.staticVer}`;
|
|
298
|
-
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
// ---
|
|
331
|
-
|
|
332
|
-
|
|
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
|
-
|
|
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
|
package/sys/monitor/watchdog.js
CHANGED
|
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 (
|
|
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
|
package/www/example/ctr/main.js
CHANGED
|
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
|
*/
|
package/www/example/ctr/test.js
CHANGED
|
@@ -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><
|
|
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 &&
|
|
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 &&
|
|
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
|
|
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
|
|
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
|
package/www/example/kebab.json
CHANGED
|
File without changes
|
|
File without changes
|
package/www/example/mod/test.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/www/example/route.json
CHANGED
|
File without changes
|