@tbox.cn/app-template-h5 0.1.4 → 0.1.6

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/README.md CHANGED
@@ -35,6 +35,8 @@ pnpm install
35
35
  | `PORT` | server | 服务端口(默认 8000) |
36
36
  | `HOST` | server | 监听地址(默认 0.0.0.0) |
37
37
  | `VITE_API_URL` | client | API base(空串/未设 = 同源相对路径);经 `configureRuntime` 注入 |
38
+ | `TBOX_DEV_TOKEN` | server | 沙箱 dev server 鉴权 token(方案 W;平台运行时注入,**本地无需设置**):形状 `^[A-Za-z0-9_-]{20,128}$`。在场 → protected 双面裁决——dev 面(凭据 + 浏览器导航/同源信号)完整 vite 能力,static 面(无凭据/跨源子资源)纯 dist + SSE 刷新(`GET /__tbox_dev_hot_reload`),零源码泄漏;缺席 → classic(与历史行为字节级等价)。形状不合规 → fail-closed 全 static |
39
+ | `TBOX_DEV_SSE_KEEPALIVE_MS` | server | protected 面 SSE 心跳间隔 ms(缺省 15000;smoke 覆盖用) |
38
40
 
39
41
  ## 验收
40
42
 
@@ -2,82 +2,122 @@
2
2
  * vite dev 入口(仅 tsx 运行:pnpm dev;物理不进 tsup bundle——forbid-vite 插件拦截)。
3
3
  * middlewareMode + 单端口 8000:HTTP 与 HMR WS 全走应用端口。
4
4
  * 部署形态一律走 dist/index.js(静态产物,零 HMR WS)。
5
+ *
6
+ * 方案 W(沙箱 dev server 鉴权保护):TBOX_DEV_TOKEN 在场 → protected 双面裁决
7
+ * (dev 面 = 凭据 + 导航/同源信号放行 vite;static 面 = 纯 dist + SSE 刷新,零源码泄漏);
8
+ * 缺席 → classic,与历史行为字节级等价。裁决机制真源 = SDK platform/dev-runtime
9
+ * (createTboxDevRuntime),本文件只做装配:vite 实例 + fs.allow 收窄 + 面无关自由区。
5
10
  */
6
11
  import { createServer } from 'http';
7
12
  import path from 'path';
8
- import { readFile } from 'node:fs/promises';
13
+ import { existsSync } from 'node:fs';
9
14
  import { fileURLToPath } from 'url';
10
- import { createServer as createViteServer } from 'vite';
11
- import { injectScript, installInspector, isReady } from '@tbox.cn/app-sdk-server/platform';
15
+ import { build as viteBuild, createServer as createViteServer, searchForWorkspaceRoot } from 'vite';
16
+ import {
17
+ createTboxDevRuntime,
18
+ injectScript,
19
+ installInspector,
20
+ isReady,
21
+ resolveDevTokenFromEnv,
22
+ } from '@tbox.cn/app-sdk-server/platform';
23
+ import { createLogger } from '@tbox.cn/app-sdk-server';
12
24
  import { app, start } from './app.js';
13
25
 
14
26
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
27
+ const serverLogger = createLogger('server');
15
28
  const clientRoot = path.resolve(__dirname, '../../client');
29
+ const appRoot = path.resolve(__dirname, '../..');
16
30
 
17
31
  // HMR WS 独立路径与协议常量(scripts/smoke-dev-hmr.sh 断言消费,不得改名):
18
- // dev 基础设施命名空间(双下划线),与业务命名空间解耦。
32
+ // dev 基础设施命名空间(双下划线),与业务命名空间解耦。协议白名单与 Spanner 子协议
33
+ // 补写内聚于 SDK handleUpgrade(本模板 upgrade 形态 = server.on 直连,见文件尾)。
19
34
  const HMR_WS_PATH = '/__vite_hmr';
20
35
 
21
36
  // HMR WS hub:永不 listen 的纯事件载体。hmr.server 在场 → vite 不自建独立端口,
22
37
  // 客户端按页面同源端口连接。vite 对 hub 仅 on/off('upgrade')。
23
38
  const hmrHub = createServer();
24
39
 
40
+ const tokenState = resolveDevTokenFromEnv(process.env);
41
+ const protectedMode = tokenState.mode === 'protected';
42
+
43
+ // protected 面 fs.allow 收窄(classic 恒默认不传——红线):client 源 + 工作区依赖真源
44
+ // (filter(existsSync):生成应用无 packages/ 时自动滤除,不影响沙箱半径)。
45
+ const workspaceRoot = searchForWorkspaceRoot(clientRoot);
46
+ const fsAllowNarrowed = protectedMode
47
+ ? [
48
+ ...new Set(
49
+ [
50
+ clientRoot,
51
+ path.join(appRoot, 'packages'),
52
+ path.join(appRoot, 'node_modules'),
53
+ path.join(workspaceRoot, 'packages'),
54
+ path.join(workspaceRoot, 'node_modules'),
55
+ ].filter((p) => existsSync(p)),
56
+ ),
57
+ ]
58
+ : undefined;
59
+
25
60
  const vite = await createViteServer({
26
61
  root: clientRoot,
27
62
  server: {
28
63
  middlewareMode: true,
29
64
  allowedHosts: true, // 沙箱域名 Host 透传放行(dev-only;dist 物理不含 vite)
30
- fs: { deny: ['.env', '.env.*', '*.{crt,pem}', '**/.git/**'] },
65
+ fs: {
66
+ ...(fsAllowNarrowed ? { allow: fsAllowNarrowed } : {}),
67
+ deny: ['.env', '.env.*', '*.{crt,pem}', '**/.git/**'],
68
+ },
31
69
  hmr: { server: hmrHub, path: HMR_WS_PATH },
32
70
  },
33
- appType: 'custom', // 不加 Vite 自己的 SPA fallback,由下方 app.use('*') 统一处理
71
+ appType: 'custom', // 不加 Vite 自己的 SPA fallback,HTML 路由由 runtime.final() 统一处理
34
72
  });
35
73
 
36
- // 平台微调(inspector):入口级接线(与 index.ts 对称),须先于 vite middlewares
37
- installInspector(app);
38
-
39
- const viteHmrPath = path.posix.join(vite.config.base, HMR_WS_PATH);
74
+ const runtime = createTboxDevRuntime({
75
+ vite: { middlewares: vite.middlewares, transformIndexHtml: vite.transformIndexHtml },
76
+ hmrHub,
77
+ hmrPath: path.posix.join(vite.config.base, HMR_WS_PATH),
78
+ tokenState,
79
+ clientRoot,
80
+ clientDist: path.join(clientRoot, 'dist'),
81
+ spaExcludePrefixes: ['/api'],
82
+ decorateHtml: (html) => (isReady() ? injectScript(html) : html),
83
+ logger: serverLogger,
84
+ sseKeepaliveMs: process.env.TBOX_DEV_SSE_KEEPALIVE_MS ? Number(process.env.TBOX_DEV_SSE_KEEPALIVE_MS) : undefined,
85
+ });
40
86
 
41
- app.use(vite.middlewares);
87
+ // 面裁决:dev 面 → vite middlewares;static 面 → next()(落 final 的 dist 静态)
88
+ app.use(runtime.gate);
42
89
 
43
- // HTML 路由兜底:非 /api 一律返回 index.html(经 Vite 转换 + inspector 注入)
44
- app.use('*', async (req, res, next) => {
45
- if (req.originalUrl.startsWith('/api')) {
46
- return next();
47
- }
48
- try {
49
- const rawHtml = await readFile(path.join(clientRoot, 'index.html'), 'utf-8');
50
- const template = await vite.transformIndexHtml(req.originalUrl, rawHtml);
51
- const html = isReady() ? injectScript(template) : template;
52
- res.status(200).set({ 'Content-Type': 'text/html' }).end(html);
53
- } catch (e) {
54
- next(e);
55
- }
90
+ // ── 自由区(模板差异 + 应用自定义 middleware;dev/static 两面恒可见)──
91
+ // 平台微调(inspector):入口级接线(RUNTIME_PATH 路由 + text/html 装配),两面共用
92
+ installInspector(app);
93
+ // 探针路由:smoke-dev-protected.sh ⑩ 断言两面可见性(dev 基础设施,dist 物理不含)
94
+ app.get('/__tbox_dev_freezone_probe', (_req, res) => {
95
+ res.type('text/plain').send('freezone-ok');
56
96
  });
97
+ // ── 自由区结束 ──
98
+
99
+ app.use(runtime.final);
100
+
101
+ // protected 面构建通知:watch 增量构建 → BUNDLE_END → SSE 广播 → static 面 reload。
102
+ // fire-and-forget;watch 启动 emptyOutDir 清空 dist 为已定价行为(首建完成前 static 面纯 404)。
103
+ if (protectedMode) {
104
+ viteBuild({ root: clientRoot, build: { watch: {}, sourcemap: false } })
105
+ .then((result) => {
106
+ const watcher = Array.isArray(result) ? result[0] : result;
107
+ if (!watcher || !('on' in watcher)) return;
108
+ watcher.on('event', (e) => {
109
+ if (e.code === 'BUNDLE_END') runtime.notifyBuildEnd();
110
+ });
111
+ })
112
+ .catch((err) => console.error('[dev] watch build failed:', err));
113
+ }
57
114
 
58
115
  const server = start();
59
116
 
60
- // 升级处理:认领路径 /__vite_hmr(smoke-dev-hmr.sh 断言消费,不得改名)+ 子协议白名单;
61
- // 其余一律销毁(本模板无业务 WS)。
62
- // 子协议补写(2026-09-21,同 agent/support 模板——Spanner 网关把 WS 升级的
63
- // Sec-WebSocket-Protocol 视为 hop-by-hop 在下游拨号时剥除,详见 tbox-app 仓
64
- // .agents/notes/implemented/bug-fix/2026-09-21-spanner-ws-subprotocol-strip-reload-loop.md):
65
- // 无协议头的 HMR 路径升级补写 vite-hmr 让下游握手成立——否则 destroy → WebKit(Safari/iOS
66
- // 支付宝·微信,宽松接受网关乐观 101)open 即断 → vite-ping 探测恒成 → location.reload()
67
- // 无限循环(Chrome/Blink 严格校验协议回显恒不 open 免疫)。其余非 vite 协议值仍销毁
68
- // (smoke ⑤⑥ 语义不变);网关透传修复落地后补写自动退化为死分支——长留不移除。
117
+ // 升级处理:唯一 WS 入口 = runtime.handleUpgrade(认领 /__vite_hmr + 子协议白名单 +
118
+ // protected token 校验失败 401;其余一律销毁——本模板无业务 WS,smoke-dev-hmr 断言消费)
69
119
  server.on('upgrade', (request, socket, head) => {
70
- const { pathname } = new URL(request.url || '/', 'http://localhost');
71
- if (pathname !== viteHmrPath) {
72
- socket.destroy();
73
- return;
74
- }
75
- const protocol = request.headers['sec-websocket-protocol'];
76
- if (!protocol) {
77
- request.headers['sec-websocket-protocol'] = 'vite-hmr';
78
- } else if (protocol !== 'vite-hmr' && protocol !== 'vite-ping') {
120
+ if (!runtime.handleUpgrade(request, socket, head)) {
79
121
  socket.destroy();
80
- return;
81
122
  }
82
- hmrHub.emit('upgrade', request, socket, head);
83
123
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tbox.cn/app-template-h5",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "tbox 移动 H5 全栈模板(单 workspace:apps/client + apps/server),运行时与页面微调经 @tbox.cn/app-sdk。",
5
5
  "type": "module",
6
6
  "engines": {
@@ -35,7 +35,7 @@
35
35
  "eslint-plugin-react-hooks": "5.2.0",
36
36
  "typescript-eslint": "8.67.0",
37
37
  "vite": "6.4.3",
38
- "@tbox.cn/app-sdk-server": "0.21.0"
38
+ "@tbox.cn/app-sdk-server": "0.22.0"
39
39
  },
40
40
  "scripts": {
41
41
  "dev": "pnpm --filter ./apps/server dev",
@@ -10,8 +10,8 @@ catalogs:
10
10
  specifier: 0.21.1
11
11
  version: 0.21.1
12
12
  '@tbox.cn/app-sdk-server':
13
- specifier: 0.21.0
14
- version: 0.21.0
13
+ specifier: 0.22.0
14
+ version: 0.22.0
15
15
  '@types/express':
16
16
  specifier: 4.17.25
17
17
  version: 4.17.25
@@ -82,7 +82,7 @@ importers:
82
82
  devDependencies:
83
83
  '@tbox.cn/app-sdk-server':
84
84
  specifier: 'catalog:'
85
- version: 0.21.0
85
+ version: 0.22.0
86
86
  eslint:
87
87
  specifier: 'catalog:'
88
88
  version: 9.39.5
@@ -140,7 +140,7 @@ importers:
140
140
  dependencies:
141
141
  '@tbox.cn/app-sdk-server':
142
142
  specifier: 'catalog:'
143
- version: 0.21.0
143
+ version: 0.22.0
144
144
  express:
145
145
  specifier: 'catalog:'
146
146
  version: 4.22.2
@@ -965,6 +965,10 @@ packages:
965
965
  resolution: {integrity: sha512-GFthajvUSE7tY6Y5kLJgg2dRuZQdLfxq/WAEHOh+Q7RTwg7mpuLsWiNhXISkXMAtpxg0RkSgtwyRq33QS0ay3w==}
966
966
  engines: {node: '>=20.0.0'}
967
967
 
968
+ '@tbox.cn/app-contracts@0.12.3':
969
+ resolution: {integrity: sha512-b71WtU1Kk9rC3lwaefb48ymKPgkdb7MJcJQ0lTeUZu/jSNHsqQ4ZZI9ZUbHQ/rzYvWmZbdxe5H94328jAfJoVA==}
970
+ engines: {node: '>=20.0.0'}
971
+
968
972
  '@tbox.cn/app-sdk-client@0.21.1':
969
973
  resolution: {integrity: sha512-hN+tIj8kh11PD0HMhq5KUs/nZYjxS+ps3F3HTTmOHWXNXzilYafFxeIWJ1bXKVm1Di5EUIjNaRPUDkP6HAcHvw==}
970
974
  engines: {node: '>=20.0.0'}
@@ -973,13 +977,17 @@ packages:
973
977
  resolution: {integrity: sha512-hvdcXntg4iJniVu9LEwo5A/vov3L08kRlJLTgStYRdAlH9AJXWLDxFITMNf7YJX8W+j9wjJmmS7Sv88iswxM+w==}
974
978
  engines: {node: '>=20.0.0'}
975
979
 
976
- '@tbox.cn/app-sdk-server@0.21.0':
977
- resolution: {integrity: sha512-buiwhnT46Jb1P99UJN1p0ATjRzhFIXIgqIYQDT4aT3fVbmMxWUuAPpFYhM3uTFGx5oPh18/ju3MVz0FxZ5DMCg==}
980
+ '@tbox.cn/app-sdk-core@0.20.2':
981
+ resolution: {integrity: sha512-KDcFdIrIEOlwDvIqBu29CzR5vDjt0BCADMBrgrd5WlRqjrPM1/jM/RcaRO6gRuElmQCp5LfsoAkVeYTZE/6rpw==}
982
+ engines: {node: '>=20.0.0'}
983
+
984
+ '@tbox.cn/app-sdk-server@0.22.0':
985
+ resolution: {integrity: sha512-St5cP43/y/pH/s6ubXX44Rf0wmysL19tZRvTWSUV/0EmO7ouoRmtTzsWHe+XnqkcUnRgfSYm/evRse+K9ATNhg==}
978
986
  engines: {node: '>=20.0.0'}
979
987
  hasBin: true
980
988
 
981
- '@tbox.cn/app-toolkit@0.5.0':
982
- resolution: {integrity: sha512-ShLYwxsMzhr8V5sjixo4GFGBZbAw4/8CoTnCigrbBcRLtydf2ezLrLDR9jHfi/d5fQworFR5ZBzI7xDSjvR2zA==}
989
+ '@tbox.cn/app-toolkit@0.7.3':
990
+ resolution: {integrity: sha512-Tdh1XkqRX0DGvTxflrfgJ4WFVW5wiq9cGOd7hpFrGzQ95mXFiSUKyQMgMtIum8kbReUq+aPI5hv8Z87AugZ1yQ==}
983
991
  engines: {node: '>=20.0.0'}
984
992
 
985
993
  '@types/babel__core@7.20.5':
@@ -2830,6 +2838,12 @@ snapshots:
2830
2838
  react: 18.3.1
2831
2839
  zod: 3.25.76
2832
2840
 
2841
+ '@tbox.cn/app-contracts@0.12.3':
2842
+ dependencies:
2843
+ '@ag-ui/core': 0.0.47
2844
+ react: 18.3.1
2845
+ zod: 3.25.76
2846
+
2833
2847
  '@tbox.cn/app-sdk-client@0.21.1':
2834
2848
  dependencies:
2835
2849
  '@tbox.cn/app-contracts': 0.12.0
@@ -2841,19 +2855,23 @@ snapshots:
2841
2855
  dependencies:
2842
2856
  '@tbox.cn/app-contracts': 0.12.0
2843
2857
 
2844
- '@tbox.cn/app-sdk-server@0.21.0':
2858
+ '@tbox.cn/app-sdk-core@0.20.2':
2859
+ dependencies:
2860
+ '@tbox.cn/app-contracts': 0.12.3
2861
+
2862
+ '@tbox.cn/app-sdk-server@0.22.0':
2845
2863
  dependencies:
2846
2864
  '@ag-ui/core': 0.0.47
2847
- '@tbox.cn/app-contracts': 0.12.0
2848
- '@tbox.cn/app-sdk-core': 0.20.0
2849
- '@tbox.cn/app-toolkit': 0.5.0
2865
+ '@tbox.cn/app-contracts': 0.12.3
2866
+ '@tbox.cn/app-sdk-core': 0.20.2
2867
+ '@tbox.cn/app-toolkit': 0.7.3
2850
2868
  express: 4.22.2
2851
2869
  jose: 6.2.8
2852
2870
  zod: 3.25.76
2853
2871
  transitivePeerDependencies:
2854
2872
  - supports-color
2855
2873
 
2856
- '@tbox.cn/app-toolkit@0.5.0':
2874
+ '@tbox.cn/app-toolkit@0.7.3':
2857
2875
  dependencies:
2858
2876
  ajv: 8.20.0
2859
2877
  zod: 3.25.76
@@ -33,13 +33,13 @@ catalog:
33
33
  "@types/node": 24.13.3
34
34
  express: 4.22.2
35
35
  "@types/express": 4.17.25
36
- "@tbox.cn/app-sdk-core": 0.20.1
36
+ "@tbox.cn/app-sdk-core": 0.20.2
37
37
  "@tbox.cn/app-sdk-client": 0.21.1
38
- "@tbox.cn/app-sdk-server": 0.21.0
39
- "@tbox.cn/app-agent-sdk-server": 0.1.2
40
- "@tbox.cn/app-agent-sdk-client": 0.1.1
41
- "@tbox.cn/app-contracts": 0.12.1
42
- "@tbox.cn/app-contracts-mall": 0.12.1
38
+ "@tbox.cn/app-sdk-server": 0.22.0
39
+ "@tbox.cn/app-agent-sdk-server": 0.1.4
40
+ "@tbox.cn/app-agent-sdk-client": 0.1.3
41
+ "@tbox.cn/app-contracts": 0.12.3
42
+ "@tbox.cn/app-contracts-mall": 0.12.4
43
43
  eslint: 9.39.5
44
44
  typescript-eslint: 8.67.0
45
45
  eslint-plugin-react-hooks: 5.2.0