@tbox.cn/app-template-h5 0.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/.gitignore +19 -0
- package/.nvmrc +1 -0
- package/.tbox/app.json +4 -0
- package/AGENTS.md +64 -0
- package/LEGAL.md +7 -0
- package/LICENSE +21 -0
- package/README.md +45 -0
- package/apps/client/index.html +35 -0
- package/apps/client/package.json +28 -0
- package/apps/client/src/business/App.test.tsx +18 -0
- package/apps/client/src/business/App.tsx +15 -0
- package/apps/client/src/main.tsx +36 -0
- package/apps/client/src/shims/node-stub.ts +14 -0
- package/apps/client/src/styles/base.css +61 -0
- package/apps/client/src/vite-env.d.ts +3 -0
- package/apps/client/tsconfig.json +11 -0
- package/apps/client/vite.config.ts +39 -0
- package/apps/server/package.json +28 -0
- package/apps/server/src/app.ts +35 -0
- package/apps/server/src/dev.ts +71 -0
- package/apps/server/src/index.ts +35 -0
- package/apps/server/tests/app.test.ts +17 -0
- package/apps/server/tsconfig.json +5 -0
- package/apps/server/tsup.config.ts +26 -0
- package/apps/server/vitest.config.ts +8 -0
- package/eslint.config.mjs +36 -0
- package/package.json +52 -0
- package/pnpm-lock.template.yaml +5636 -0
- package/pnpm-workspace.template.yaml +54 -0
- package/scripts/build.sh +32 -0
- package/scripts/check-workspace.mjs +120 -0
- package/scripts/dev.sh +12 -0
- package/scripts/prepare.sh +11 -0
- package/scripts/start.sh +12 -0
- package/scripts/stop.sh +60 -0
- package/tsconfig.base.json +21 -0
package/.gitignore
ADDED
package/.nvmrc
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
24
|
package/.tbox/app.json
ADDED
package/AGENTS.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
template-h5 是面向 AI 生成移动 H5 应用的单 workspace 模板:`apps/client`(React/Vite)+ `apps/server`(Express)。
|
|
4
|
+
|
|
5
|
+
> **工作区上下文**:仓库内本目录是根 workspace 成员子树,平台引用走 `catalog:`(in-repo 经根 overrides 源码直连 SDK);`tbox-app create` 从模板源(目录或 pack tgz)展开生成应用,`pnpm-workspace.yaml` 由 `pnpm-workspace.template.yaml` 改名生成。
|
|
6
|
+
|
|
7
|
+
## 工程结构
|
|
8
|
+
|
|
9
|
+
| 包 | 职责 |
|
|
10
|
+
|---|---|
|
|
11
|
+
| `apps/client` | React/Vite H5 页面与业务入口(@app/client) |
|
|
12
|
+
| `apps/server` | Express API:健康检查、静态托管、inspector(@app/server) |
|
|
13
|
+
| `scripts/` | 平台生命周期脚本(prepare/dev/start/stop/build 固定名契约)与工程守卫 |
|
|
14
|
+
|
|
15
|
+
## 常用命令
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
pnpm install
|
|
19
|
+
pnpm dev # 单进程 localhost:8000(API + 页面 + HMR)
|
|
20
|
+
pnpm typecheck
|
|
21
|
+
pnpm test
|
|
22
|
+
pnpm build
|
|
23
|
+
pnpm verify # typecheck + test + lint + build + check:inspector
|
|
24
|
+
pnpm check:inspector # inspector 接线守卫(随 @tbox.cn/app-sdk 分发)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
会话开始时,若 `node_modules` 缺失,先跑 `pnpm install`。
|
|
28
|
+
|
|
29
|
+
## SDK 复用点(优先复用,勿手写)
|
|
30
|
+
|
|
31
|
+
- `@tbox.cn/app-sdk/client`:`configureRuntime`(API base 注入,勿硬编码 host)、`httpGet/httpPost/createService`(HTTP 客户端)、`ErrorBoundary`、`createLogger`、`installGlobalErrorReporting`
|
|
32
|
+
- `@tbox.cn/app-sdk/server`:`createLogger`
|
|
33
|
+
- `@tbox.cn/app-sdk/platform`:`installInspector` + vite `code-inspect` 插件(页面微调基线,受 `check:inspector` 守卫)
|
|
34
|
+
- 不直接 import `@tbox-dev-js/sdk`——平台能力一律经 `@tbox.cn/app-sdk` 公开入口。
|
|
35
|
+
|
|
36
|
+
## 关键入口
|
|
37
|
+
|
|
38
|
+
```text
|
|
39
|
+
apps/client/src/main.tsx # 客户端根入口(configureRuntime + ErrorBoundary + render)
|
|
40
|
+
apps/client/src/business/App.tsx # 业务应用唯一入口(新应用从这里开始)
|
|
41
|
+
apps/server/src/index.ts # 静态入口(唯一 tsup entry:static + SPA + /api JSON 404)
|
|
42
|
+
apps/server/src/app.ts # 装配主体(express + /api/health + start())
|
|
43
|
+
apps/server/src/dev.ts # vite dev 入口(仅 tsx 运行;禁入 tsup bundle)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## 平台基础设施(守卫保护,勿删改)
|
|
47
|
+
|
|
48
|
+
- 单端口 8000(dev 与生产统一;端口经 `process.env.PORT || 8000`,lint 禁止硬编码)
|
|
49
|
+
- `installInspector` 接线 + `apps/client/index.html` 的 `/__vs_inspector__.js` 标签(页面微调契约)
|
|
50
|
+
- `GET /api/health` 恒返回 `{ "status": "ok" }`;未知 `/api/*` 恒 JSON 404
|
|
51
|
+
- 服务端凭证/环境变量不进 client bundle(客户端环境值仅 `import.meta.env.VITE_*` 且经 `configureRuntime` 注入)
|
|
52
|
+
|
|
53
|
+
## web-view 样式约定(嵌入支付宝小程序 web-view 壳时遵守)
|
|
54
|
+
|
|
55
|
+
1. **长度单位 PX 大写**——小写 `px` 被壳内字体缩放改写,大写跳过改写保持视觉尺寸。
|
|
56
|
+
2. **flex 子项设 `flex-shrink: 0`**——防壳内宽度不定导致元素被压缩。
|
|
57
|
+
3. **滚动容器隐藏滚动条**(`::-webkit-scrollbar { display: none }`)。
|
|
58
|
+
4. **颜色用 `rgba()` 传统语法**——避免 `color-mix`/`oklch` 在低版本内核不渲染。
|
|
59
|
+
|
|
60
|
+
## 边界
|
|
61
|
+
|
|
62
|
+
- 不创建嵌套 Vite 项目、第二个 `package.json` 或额外 lockfile(check:workspace 校验)。
|
|
63
|
+
- 无模块装配面:`tbox-app module add` 不适用于本模板(模块展开后仅告警、不装配)。
|
|
64
|
+
- 结构、路由、命令、依赖或环境变量变化时同步 README 与本文件。
|
package/LEGAL.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Legal Disclaimer
|
|
2
|
+
|
|
3
|
+
Within this source code, the comments in Chinese shall be the original, governing version. Any comment in other languages are for reference only. In the event of any conflict between the Chinese language version comments and other language version comments, the Chinese language version shall prevail.
|
|
4
|
+
|
|
5
|
+
法律免责声明
|
|
6
|
+
|
|
7
|
+
关于代码注释部分,中文注释为官方版本,其它语言注释仅做参考。中文注释可能与其它语言注释存在不一致,当中文注释与其它语言注释存在不一致时,请以中文注释为准。
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 tbox.cn
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# template-h5
|
|
2
|
+
|
|
3
|
+
tbox 移动 H5 全栈模板(包名 `@tbox.cn/app-template-h5`),单 workspace:`apps/client`(React/Vite)+ `apps/server`(Express)。运行时与页面微调经 `@tbox.cn/app-sdk`。
|
|
4
|
+
|
|
5
|
+
## 快速开始
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pnpm install
|
|
9
|
+
pnpm dev
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
开发期浏览器访问 `http://localhost:8000`(单端口:API + 页面 + HMR)。`server` 负责 API、健康检查与生产静态托管,`client` 负责 H5 页面与业务入口。
|
|
13
|
+
|
|
14
|
+
## 生成新应用
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
# 本地模板目录
|
|
18
|
+
tbox-app create . --name my-h5-app --source <tbox-app 仓>/templates/template-h5
|
|
19
|
+
# 或 registry(公开发布后)
|
|
20
|
+
tbox-app create . --name my-h5-app --source @tbox.cn/app-template-h5
|
|
21
|
+
pnpm install
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## 目录边界
|
|
25
|
+
|
|
26
|
+
- `apps/client/src/business/`:业务应用唯一入口
|
|
27
|
+
- `apps/server/src/index.ts`:静态入口(唯一 tsup entry)
|
|
28
|
+
- `apps/server/src/dev.ts`:vite dev 入口(仅本地开发)
|
|
29
|
+
- `scripts/`:平台生命周期脚本(prepare/dev/start/stop/build 固定名)
|
|
30
|
+
|
|
31
|
+
## 环境变量
|
|
32
|
+
|
|
33
|
+
| 变量 | 侧 | 说明 |
|
|
34
|
+
|---|---|---|
|
|
35
|
+
| `PORT` | server | 服务端口(默认 8000) |
|
|
36
|
+
| `HOST` | server | 监听地址(默认 0.0.0.0) |
|
|
37
|
+
| `VITE_API_URL` | client | API base(空串/未设 = 同源相对路径);经 `configureRuntime` 注入 |
|
|
38
|
+
|
|
39
|
+
## 验收
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
pnpm verify # typecheck + test + lint + build + check:inspector
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
更多工程约束见 [AGENTS.md](AGENTS.md)。
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<!-- 移动端视口;缩放锁定,配合 postcss-pxtorem 的 750→rem 换算 -->
|
|
6
|
+
<meta
|
|
7
|
+
name="viewport"
|
|
8
|
+
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
|
|
9
|
+
/>
|
|
10
|
+
<meta name="theme-color" content="#0f172a" />
|
|
11
|
+
<!-- rem 弹性根字号(threshold 450 / designWidth 750):移动端随屏宽缩放、PC 固定 50px 居中。
|
|
12
|
+
在 CSS 前同步执行,避免首屏字号闪动。 -->
|
|
13
|
+
<script>
|
|
14
|
+
(function () {
|
|
15
|
+
var THRESHOLD = 450,
|
|
16
|
+
DESIGN = 750;
|
|
17
|
+
function setRootFontSize() {
|
|
18
|
+
var w = document.documentElement.clientWidth;
|
|
19
|
+
document.documentElement.style.fontSize =
|
|
20
|
+
(w > THRESHOLD ? 50 : (100 / DESIGN) * w) + 'px';
|
|
21
|
+
}
|
|
22
|
+
setRootFontSize();
|
|
23
|
+
window.addEventListener('resize', setRootFontSize);
|
|
24
|
+
window.addEventListener('orientationchange', setRootFontSize);
|
|
25
|
+
})();
|
|
26
|
+
</script>
|
|
27
|
+
<title>H5 应用</title>
|
|
28
|
+
<!-- 平台微调(inspector) runtime:由 server 提供 GET /__vs_inspector__.js;dev 下经 Vite 代理到 server。 -->
|
|
29
|
+
<script src="/__vs_inspector__.js" defer></script>
|
|
30
|
+
</head>
|
|
31
|
+
<body>
|
|
32
|
+
<div id="root"></div>
|
|
33
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
34
|
+
</body>
|
|
35
|
+
</html>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@app/client",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "tsc --noEmit && vite build",
|
|
9
|
+
"typecheck": "tsc --noEmit",
|
|
10
|
+
"test": "vitest run"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@tbox.cn/app-sdk": "catalog:",
|
|
14
|
+
"react": "catalog:",
|
|
15
|
+
"react-dom": "catalog:"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/node": "catalog:",
|
|
19
|
+
"@types/react": "catalog:",
|
|
20
|
+
"@types/react-dom": "catalog:",
|
|
21
|
+
"@vitejs/plugin-react": "catalog:",
|
|
22
|
+
"happy-dom": "catalog:",
|
|
23
|
+
"postcss-pxtorem": "catalog:",
|
|
24
|
+
"typescript": "catalog:",
|
|
25
|
+
"vite": "catalog:",
|
|
26
|
+
"vitest": "catalog:"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { createRoot } from 'react-dom/client';
|
|
4
|
+
import { flushSync } from 'react-dom';
|
|
5
|
+
import { createElement } from 'react';
|
|
6
|
+
import BusinessApp from './App';
|
|
7
|
+
|
|
8
|
+
describe('BusinessApp', () => {
|
|
9
|
+
it('渲染中性入口标题', () => {
|
|
10
|
+
const container = document.createElement('div');
|
|
11
|
+
document.body.appendChild(container);
|
|
12
|
+
flushSync(() => {
|
|
13
|
+
createRoot(container).render(createElement(BusinessApp));
|
|
14
|
+
});
|
|
15
|
+
expect(container.querySelector('h1')?.textContent).toBe('你的移动应用,从这里开始');
|
|
16
|
+
container.remove();
|
|
17
|
+
});
|
|
18
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 业务应用唯一入口:中性单页起点,无占位标记。
|
|
3
|
+
* 新应用替换本组件;后端调用走 @tbox.cn/app-sdk/client 的 httpGet/httpPost(同源 /api/*)。
|
|
4
|
+
*/
|
|
5
|
+
export default function BusinessApp() {
|
|
6
|
+
return (
|
|
7
|
+
<main className="app-shell">
|
|
8
|
+
<section className="app-hero" aria-labelledby="app-title">
|
|
9
|
+
<p className="app-kicker">H5 应用</p>
|
|
10
|
+
<h1 id="app-title">你的移动应用,从这里开始</h1>
|
|
11
|
+
<p>品牌、页面与数据能力从这里构建——前后端同仓,SDK 提供运行时与页面微调基线。</p>
|
|
12
|
+
</section>
|
|
13
|
+
</main>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { StrictMode } from 'react';
|
|
2
|
+
import { createRoot } from 'react-dom/client';
|
|
3
|
+
import {
|
|
4
|
+
configureRuntime,
|
|
5
|
+
ErrorBoundary,
|
|
6
|
+
installGlobalErrorReporting,
|
|
7
|
+
} from '@tbox.cn/app-sdk/client';
|
|
8
|
+
import BusinessApp from './business/App';
|
|
9
|
+
import './styles/base.css';
|
|
10
|
+
|
|
11
|
+
// H1:环境值注入点(SDK 零 Vite 依赖)——VITE_API_URL 空串/未设 = 同源相对路径
|
|
12
|
+
configureRuntime({ apiBaseUrl: import.meta.env.VITE_API_URL as string | undefined });
|
|
13
|
+
|
|
14
|
+
// 全局错误兜底:未捕获异常/rejection → 统一日志行(上报扩展点见 SDK client logger)
|
|
15
|
+
installGlobalErrorReporting();
|
|
16
|
+
|
|
17
|
+
const el = document.getElementById('root');
|
|
18
|
+
|
|
19
|
+
if (el) {
|
|
20
|
+
createRoot(el).render(
|
|
21
|
+
<StrictMode>
|
|
22
|
+
<ErrorBoundary
|
|
23
|
+
fallback={
|
|
24
|
+
<main className="app-error">
|
|
25
|
+
<h1>页面暂时不可用</h1>
|
|
26
|
+
<button type="button" onClick={() => window.location.reload()}>
|
|
27
|
+
重新加载
|
|
28
|
+
</button>
|
|
29
|
+
</main>
|
|
30
|
+
}
|
|
31
|
+
>
|
|
32
|
+
<BusinessApp />
|
|
33
|
+
</ErrorBoundary>
|
|
34
|
+
</StrictMode>,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 浏览器端 fs / path 桩。
|
|
3
|
+
*
|
|
4
|
+
* @tbox-dev-js/sdk 的 ESM 顶层静态 import 了 Node 的 fs/path(用于 Node 环境的代码路径,
|
|
5
|
+
* 浏览器不会执行到),但静态 import 会让 vite 生产构建因"命名导出不存在"而失败。
|
|
6
|
+
* 用本桩 alias 掉 fs/path,提供同名空实现即可(浏览器永不调用这些路径)。
|
|
7
|
+
*/
|
|
8
|
+
export const existsSync = (): boolean => false;
|
|
9
|
+
export const readFileSync = (): string => '';
|
|
10
|
+
export const resolve = (...p: string[]): string => p.join('/');
|
|
11
|
+
export const join = (...p: string[]): string => p.join('/');
|
|
12
|
+
export const dirname = (p = ''): string => p.split('/').slice(0, -1).join('/');
|
|
13
|
+
|
|
14
|
+
export default { existsSync, readFileSync, resolve, join, dirname };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/* 基础样式三合一:tokens 变量 + 移动端基础 + motion。
|
|
2
|
+
变量值保持中性(slate 系),主题覆写由各应用自行扩展。 */
|
|
3
|
+
|
|
4
|
+
/* ===== tokens(变量) ===== */
|
|
5
|
+
:root {
|
|
6
|
+
--font-family: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
7
|
+
--color-background: #f8fafc;
|
|
8
|
+
--color-surface: #ffffff;
|
|
9
|
+
--color-text: #0f172a;
|
|
10
|
+
--color-muted: #64748b;
|
|
11
|
+
--color-primary: #0f766e;
|
|
12
|
+
--color-primary-strong: #115e59;
|
|
13
|
+
--color-accent: #f59e0b;
|
|
14
|
+
--color-border: #dbe4ea;
|
|
15
|
+
--color-focus: #f59e0b;
|
|
16
|
+
--content-width: 480px;
|
|
17
|
+
--space-1: 0.25rem;
|
|
18
|
+
--space-2: 0.5rem;
|
|
19
|
+
--space-3: 0.75rem;
|
|
20
|
+
--space-4: 1rem;
|
|
21
|
+
--space-5: 1.25rem;
|
|
22
|
+
--space-6: 1.5rem;
|
|
23
|
+
--space-8: 2rem;
|
|
24
|
+
--radius-sm: 0.5rem;
|
|
25
|
+
--radius-md: 0.75rem;
|
|
26
|
+
--shadow-soft: 0 20px 50px rgba(15, 23, 42, 0.08);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* ===== 基础(reset + 移动端基线) ===== */
|
|
30
|
+
:root { font-family: var(--font-family); color: var(--color-text); background: var(--color-background); font-synthesis: none; text-rendering: optimizeLegibility; }
|
|
31
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
32
|
+
html { min-width: 320px; min-height: 100%; background: var(--color-background); }
|
|
33
|
+
body { margin: 0; min-width: 320px; min-height: 100%; overflow-x: hidden; background: var(--color-background); }
|
|
34
|
+
body, button, input, textarea, select { font: inherit; }
|
|
35
|
+
button, a, input, textarea, select { -webkit-tap-highlight-color: transparent; }
|
|
36
|
+
button, a { touch-action: manipulation; }
|
|
37
|
+
button { min-height: 44px; cursor: pointer; }
|
|
38
|
+
button:disabled { cursor: wait; opacity: 0.65; }
|
|
39
|
+
img, video, canvas { display: block; max-width: 100%; }
|
|
40
|
+
img { height: auto; }
|
|
41
|
+
#root { min-height: 100dvh; }
|
|
42
|
+
@supports not (height: 100dvh) { #root { min-height: 100vh; } }
|
|
43
|
+
@media (min-width: 700px) {
|
|
44
|
+
body { display: flex; justify-content: center; background: #e2e8f0; }
|
|
45
|
+
#root { width: min(100%, 480px); min-height: 100dvh; background: var(--color-background); box-shadow: 0 0 0 1px rgba(15, 23, 42, 0.08); overflow-x: hidden; }
|
|
46
|
+
}
|
|
47
|
+
:focus-visible { outline: 3px solid var(--color-focus); outline-offset: 3px; }
|
|
48
|
+
|
|
49
|
+
/* ===== 入口类(中性起点,随应用改写) ===== */
|
|
50
|
+
.app-shell { min-height: 100dvh; padding: max(2rem, env(safe-area-inset-top)) var(--space-5) max(2rem, env(safe-area-inset-bottom)); }
|
|
51
|
+
.app-hero { padding: clamp(2.5rem, 12vh, 6rem) 0 var(--space-8); }
|
|
52
|
+
.app-kicker { display: inline-flex; align-items: center; gap: var(--space-2); color: var(--color-primary); font-size: 0.78rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; }
|
|
53
|
+
.app-hero h1 { max-width: 12ch; margin: var(--space-4) 0; font-size: 2.75rem; line-height: 1.02; }
|
|
54
|
+
.app-hero p { max-width: 34rem; margin: 0 0 var(--space-6); color: var(--color-muted); font-size: 1.05rem; line-height: 1.7; }
|
|
55
|
+
.app-error { display: grid; min-height: 100dvh; place-content: center; gap: var(--space-4); padding: var(--space-6); text-align: center; }
|
|
56
|
+
@media (max-width: 380px) { .app-hero h1 { font-size: 2.4rem; } }
|
|
57
|
+
|
|
58
|
+
/* ===== motion ===== */
|
|
59
|
+
.loading-dot { width: 0.65rem; height: 0.65rem; border-radius: 50%; background: currentColor; animation: pulse 1.2s ease-in-out infinite; }
|
|
60
|
+
@keyframes pulse { 0%, 100% { opacity: 0.35; transform: scale(0.8); } 50% { opacity: 1; transform: scale(1); } }
|
|
61
|
+
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; scroll-behavior: auto !important; transition-duration: 0.01ms !important; } }
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/// <reference types="vitest" />
|
|
2
|
+
import { defineConfig } from 'vite';
|
|
3
|
+
import react from '@vitejs/plugin-react';
|
|
4
|
+
import pxToRem from 'postcss-pxtorem';
|
|
5
|
+
import { resolve } from 'node:path';
|
|
6
|
+
// 平台微调配套 babel plugin(随 @tbox.cn/app-sdk 分发):dev 期给每个 JSX 注入源码位置属性,
|
|
7
|
+
// 供 inspector runtime(/__vs_inspector__.js) 点选元素→定位源码(check:inspector 断言点)。
|
|
8
|
+
import codeInspectPlugin from '@tbox.cn/app-sdk/platform/code-inspect';
|
|
9
|
+
|
|
10
|
+
const pxToRemPlugin = pxToRem({
|
|
11
|
+
rootValue: 100,
|
|
12
|
+
unitPrecision: 5,
|
|
13
|
+
propList: ['*'],
|
|
14
|
+
selectorBlackList: ['.no-rem'],
|
|
15
|
+
minPixelValue: 1,
|
|
16
|
+
mediaQuery: false,
|
|
17
|
+
// 排除 node_modules 内三方样式,但放行 pnpm 布局下的 @tbox.cn 模块 CSS(agent 同参)
|
|
18
|
+
exclude: /node_modules\/(?!\.pnpm|@tbox\.cn)/,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// dev 期页面由 apps/server dev.ts 的 vite middleware 托管(本文件被其加载);
|
|
22
|
+
// build 恒走静态产物。无 .tbox-dev.local 探针段(h5 无 dev 装配面,native launch)。
|
|
23
|
+
export default defineConfig(({ command }) => ({
|
|
24
|
+
plugins: [react({ babel: { plugins: command === 'serve' ? [codeInspectPlugin] : [] } })],
|
|
25
|
+
resolve: {
|
|
26
|
+
alias: {
|
|
27
|
+
'@': resolve(__dirname, 'src'),
|
|
28
|
+
// @tbox-dev-js/sdk 顶层静态 import fs/path(浏览器不执行)——生产构建以桩 alias(agent 同构)
|
|
29
|
+
fs: resolve(__dirname, 'src/shims/node-stub.ts'),
|
|
30
|
+
path: resolve(__dirname, 'src/shims/node-stub.ts'),
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
css: {
|
|
34
|
+
postcss: { plugins: [pxToRemPlugin] },
|
|
35
|
+
},
|
|
36
|
+
build: {
|
|
37
|
+
rollupOptions: { input: { index: resolve(__dirname, 'index.html') } },
|
|
38
|
+
},
|
|
39
|
+
}));
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@app/server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "tsx watch --exclude ../../apps/client src/dev.ts",
|
|
8
|
+
"build": "tsup",
|
|
9
|
+
"start": "node dist/index.js",
|
|
10
|
+
"test": "vitest run",
|
|
11
|
+
"typecheck": "tsc --noEmit"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@tbox.cn/app-sdk": "catalog:",
|
|
15
|
+
"express": "catalog:"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/express": "catalog:",
|
|
19
|
+
"@types/node": "catalog:",
|
|
20
|
+
"@types/supertest": "catalog:",
|
|
21
|
+
"supertest": "catalog:",
|
|
22
|
+
"tsx": "catalog:",
|
|
23
|
+
"tsup": "catalog:",
|
|
24
|
+
"typescript": "catalog:",
|
|
25
|
+
"vite": "catalog:",
|
|
26
|
+
"vitest": "catalog:"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 装配主体(模块级单例):express 链 + /api/health + start()。客户端供给由入口决定——
|
|
3
|
+
* 静态产物见 index.ts(唯一 tsup entry),vite dev 见 dev.ts(仅 tsx)。
|
|
4
|
+
* 平台微调(inspector)在入口级接线(index.ts / dev.ts),保持本模块测试纯净。
|
|
5
|
+
*/
|
|
6
|
+
import { createServer } from 'http';
|
|
7
|
+
import express from 'express';
|
|
8
|
+
import { createLogger } from '@tbox.cn/app-sdk/server';
|
|
9
|
+
|
|
10
|
+
export const app = express();
|
|
11
|
+
const logger = createLogger('server');
|
|
12
|
+
|
|
13
|
+
app.use(express.json());
|
|
14
|
+
|
|
15
|
+
// 健康检查契约(平台探活):GET /api/health → {"status":"ok"}
|
|
16
|
+
app.get('/api/health', (_req, res) => {
|
|
17
|
+
res.json({ status: 'ok' });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// 未知 /api/* 恒 JSON 404(不落 SPA 兜底)
|
|
21
|
+
app.use('/api', (_req, res) => {
|
|
22
|
+
res.status(404).json({ error: 'Not Found' });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export function start() {
|
|
26
|
+
const port = Number(process.env.PORT ?? 8000);
|
|
27
|
+
const host = process.env.HOST ?? '0.0.0.0';
|
|
28
|
+
const server = createServer(app);
|
|
29
|
+
server.on('error', (err) => {
|
|
30
|
+
logger.error('server error:', err);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
});
|
|
33
|
+
server.listen(port, host, () => logger.info(`listening on http://${host}:${port}`));
|
|
34
|
+
return server;
|
|
35
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vite dev 入口(仅 tsx 运行:pnpm dev;物理不进 tsup bundle——forbid-vite 插件拦截)。
|
|
3
|
+
* middlewareMode + 单端口 8000:HTTP 与 HMR WS 全走应用端口。
|
|
4
|
+
* 部署形态一律走 dist/index.js(静态产物,零 HMR WS)。
|
|
5
|
+
*/
|
|
6
|
+
import { createServer } from 'http';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { readFile } from 'node:fs/promises';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
import { createServer as createViteServer } from 'vite';
|
|
11
|
+
import { injectScript, installInspector, isReady } from '@tbox.cn/app-sdk/platform';
|
|
12
|
+
import { app, start } from './app.js';
|
|
13
|
+
|
|
14
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const clientRoot = path.resolve(__dirname, '../../client');
|
|
16
|
+
|
|
17
|
+
// HMR WS 独立路径与协议常量(scripts/smoke-dev-hmr.sh 断言消费,不得改名):
|
|
18
|
+
// dev 基础设施命名空间(双下划线),与业务命名空间解耦。
|
|
19
|
+
const HMR_WS_PATH = '/__vite_hmr';
|
|
20
|
+
|
|
21
|
+
// HMR WS hub:永不 listen 的纯事件载体。hmr.server 在场 → vite 不自建独立端口,
|
|
22
|
+
// 客户端按页面同源端口连接。vite 对 hub 仅 on/off('upgrade')。
|
|
23
|
+
const hmrHub = createServer();
|
|
24
|
+
|
|
25
|
+
const vite = await createViteServer({
|
|
26
|
+
root: clientRoot,
|
|
27
|
+
server: {
|
|
28
|
+
middlewareMode: true,
|
|
29
|
+
allowedHosts: true, // 沙箱域名 Host 透传放行(dev-only;dist 物理不含 vite)
|
|
30
|
+
fs: { deny: ['.env', '.env.*', '*.{crt,pem}', '**/.git/**'] },
|
|
31
|
+
hmr: { server: hmrHub, path: HMR_WS_PATH },
|
|
32
|
+
},
|
|
33
|
+
appType: 'custom', // 不加 Vite 自己的 SPA fallback,由下方 app.use('*') 统一处理
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// 平台微调(inspector):入口级接线(与 index.ts 对称),须先于 vite middlewares
|
|
37
|
+
installInspector(app);
|
|
38
|
+
|
|
39
|
+
const viteHmrPath = path.posix.join(vite.config.base, HMR_WS_PATH);
|
|
40
|
+
|
|
41
|
+
app.use(vite.middlewares);
|
|
42
|
+
|
|
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
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const server = start();
|
|
59
|
+
|
|
60
|
+
// 升级处理:vite-hmr 协议 + 认领路径 → hub;其余一律销毁(本模板无业务 WS)
|
|
61
|
+
server.on('upgrade', (request, socket, head) => {
|
|
62
|
+
const protocol = request.headers['sec-websocket-protocol'];
|
|
63
|
+
if (typeof protocol === 'string' && (protocol === 'vite-hmr' || protocol === 'vite-ping')) {
|
|
64
|
+
const { pathname } = new URL(request.url || '/', 'http://localhost');
|
|
65
|
+
if (pathname === viteHmrPath) {
|
|
66
|
+
hmrHub.emit('upgrade', request, socket, head);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
socket.destroy();
|
|
71
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 静态入口(唯一 tsup entry → dist/index.js):express.static 供给 client/dist。
|
|
3
|
+
* dist 形态恒静态——vite dev 仅存在于 src/dev.ts(tsx 运行,禁入 tsup entry,
|
|
4
|
+
* tsup.config.ts forbid-vite 插件拦截)。
|
|
5
|
+
*/
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { existsSync } from 'node:fs';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
import express from 'express';
|
|
10
|
+
import { createLogger } from '@tbox.cn/app-sdk/server';
|
|
11
|
+
import { installInspector } from '@tbox.cn/app-sdk/platform';
|
|
12
|
+
import { app, start } from './app.js';
|
|
13
|
+
|
|
14
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const logger = createLogger('server');
|
|
16
|
+
|
|
17
|
+
// 平台微调(inspector):点选元素→定位源码(接线契约由 pnpm check:inspector 守卫)。
|
|
18
|
+
// 入口级调用(index.ts / dev.ts 对称),app.ts 保持测试纯净。
|
|
19
|
+
installInspector(app);
|
|
20
|
+
|
|
21
|
+
const clientDistPath = path.join(__dirname, '../../client/dist');
|
|
22
|
+
if (!existsSync(path.join(clientDistPath, 'index.html'))) {
|
|
23
|
+
logger.warn(`client/dist 缺失(${clientDistPath})——页面将不可用;请先执行 pnpm build:client`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
app.use(express.static(clientDistPath));
|
|
27
|
+
app.get('*', (req, res) => {
|
|
28
|
+
if (!req.path.startsWith('/api')) {
|
|
29
|
+
res.sendFile(path.join(clientDistPath, 'index.html'));
|
|
30
|
+
} else {
|
|
31
|
+
res.status(404).json({ error: 'Not Found' });
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
start();
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import request from 'supertest';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { app } from '../src/app.js';
|
|
4
|
+
|
|
5
|
+
describe('HTTP API', () => {
|
|
6
|
+
it('GET /api/health → {"status":"ok"}(平台探活契约)', async () => {
|
|
7
|
+
const res = await request(app).get('/api/health');
|
|
8
|
+
expect(res.status).toBe(200);
|
|
9
|
+
expect(res.body).toEqual({ status: 'ok' });
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('未知 /api/* → JSON 404(不落 SPA 兜底)', async () => {
|
|
13
|
+
const res = await request(app).get('/api/nope');
|
|
14
|
+
expect(res.status).toBe(404);
|
|
15
|
+
expect(res.body).toEqual({ error: 'Not Found' });
|
|
16
|
+
});
|
|
17
|
+
});
|