wucaishi-generative-react-skill 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/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # wucaishi-generative-react-skill
2
+
3
+ Codex skill for Wucaishi component package workflow, HTML-to-React component generation, build version confirmation, and Aliyun OSS upload.
4
+
5
+ ## Install
6
+
7
+ Run with npx:
8
+
9
+ ```bash
10
+ npx wucaishi-generative-react-skill@latest
11
+ ```
12
+
13
+ The installer copies this skill into:
14
+
15
+ ```text
16
+ ~/.codex/skills/component-package-workflow-zh
17
+ ```
18
+
19
+ If `CODEX_HOME` is set, it installs into:
20
+
21
+ ```text
22
+ $CODEX_HOME/skills/component-package-workflow-zh
23
+ ```
24
+
25
+ ## Included Skills
26
+
27
+ - `component-package-workflow-zh`
28
+ - `react-component-spec-zh`
29
+ - `html-template-to-react-components-zh`
30
+ - `build-version-confirm-zh`
31
+ - `upload-aliyun-oss-zh`
32
+
33
+ ## Installer Options
34
+
35
+ ```bash
36
+ npx wucaishi-generative-react-skill@latest -- --help
37
+ npx wucaishi-generative-react-skill@latest -- --target /path/to/skills/component-package-workflow-zh
38
+ npx wucaishi-generative-react-skill@latest -- --no-backup
39
+ ```
40
+
41
+ By default, if a target skill already exists, the installer creates a timestamped backup next to it before replacing it.
42
+
43
+ ## Publish
44
+
45
+ ```bash
46
+ npm login
47
+ npm pack --dry-run
48
+ npm publish
49
+ ```
50
+
51
+ Before publishing, inspect the package contents and make sure no local credentials, tokens, or machine-specific files are included.
package/SKILL.md ADDED
@@ -0,0 +1,196 @@
1
+ ---
2
+ name: component-package-workflow-zh
3
+ description: 当任务涉及新建组件包、修改组件包、打包组件包、上传组件包、发布组件包、打包发布、上传发布、同步组件汇总文档或上传总文档时使用。输出和说明使用中文。
4
+ ---
5
+
6
+ # 组件包流程 Skill
7
+
8
+ 这个 Skill 定义的是组件包开发与发布流程,不是通用仓库协作说明。
9
+
10
+ 只要任务涉及以下任一情形,就应优先使用本 Skill:
11
+
12
+ - 新建组件包
13
+ - 修改组件包
14
+ - 打包组件包
15
+ - 上传组件包
16
+ - 发布组件包
17
+ - 打包发布
18
+ - 上传发布
19
+ - 同步组件汇总文档
20
+ - 上传组件汇总文档
21
+
22
+ ## 使用方式
23
+
24
+ 处理组件包任务时,默认按下面顺序推进,不要跳步:
25
+
26
+ 1. 生成组件包模板
27
+ 2. 自定义开发组件包
28
+ 3. 发布组件包
29
+
30
+ 如果任务只是其中某一段,也应先检查前置阶段是否已经满足,再继续后续动作。
31
+
32
+ ## 子 Skill 路由
33
+
34
+ 本 Skill 是父 Skill。命中后按任务内容继续加载对应子 Skill:
35
+
36
+ - 当任务涉及生成、编写、修改、编辑或重构 React 组件时,继续读取 `subskills/react-component-spec-zh/SKILL.md`
37
+ - 当任务涉及从移动端 HTML 预览、HTML 模板、模板说明 Markdown、场景模块表格中抽取独立 React 组件时,继续读取 `subskills/html-template-to-react-components-zh/SKILL.md`,并先按该子 Skill 确认 HTML 文件和 Markdown 文件,不要自行猜测输入文件
38
+ - 当任务涉及 `build`、打包、生成发布产物、准备发布、发布线上、打包发布、发布,或任何会产出可交付构建结果的命令时,继续读取 `subskills/build-version-confirm-zh/SKILL.md`
39
+ - 当任务涉及发布组件包、打包发布、上传发布、上传组件包、上传 `dist/index.js`、同步组件包构建产物到 OSS,或校验组件包线上地址时,继续读取 `subskills/upload-aliyun-oss-zh/SKILL.md`
40
+
41
+ 如果一个任务同时涉及组件开发、打包和上传发布,这些子 Skill 都要读取,并且执行顺序应服从本 Skill 的主流程。
42
+
43
+ ## 第一阶段:生成组件包模板
44
+
45
+ 当用户要求开发一个新组件包时,先生成组件包模板,再进入自定义开发。
46
+
47
+ ### 组件包名称确认
48
+
49
+ 当用户要求新建或开发一个新组件包,但没有明确给出组件包名称时,不要自行推断或直接执行模板生成命令。应先用中文反问用户,请用户提供组件包名称,确认后再继续后续流程。
50
+
51
+ 推荐提问方式:
52
+
53
+ - 请先告诉我本次要创建的组件包名称,发布到平台的组件名必须使用 `<中文场景或模板>_<中文组件名>_<english_component_code>`,例如 `会议总结_一句话看懂_meeting_minutes_summary`。其中 `english_component_code` 用于组件目录、`package.json.name` 和 OSS 路径。
54
+
55
+ ### 平台组件命名规则
56
+
57
+ 发布到平台、组件名称存在性校验接口、`src/manifest.json` 的 `name` 字段,统一使用下面格式:
58
+
59
+ ```text
60
+ <中文场景或模板>_<中文组件名>_<english_component_code>
61
+ ```
62
+
63
+ 示例:
64
+
65
+ ```text
66
+ 会议总结_一句话看懂_meeting_minutes_summary
67
+ ```
68
+
69
+ 要求:
70
+
71
+ - `<中文场景或模板>` 使用模板或业务场景中文名称。
72
+ - `<中文组件名>` 使用组件中文展示名称。
73
+ - `<english_component_code>` 使用小写英文、数字和下划线,表达组件机器语义,例如 `meeting_minutes_summary`。
74
+ - 创建本地目录、执行 `npx create-wucaishi-cpn@latest`、填写 `package.json.name` 和生成 OSS prefix 时,默认使用 `english_component_code` 转成 kebab-case,例如 `meeting-minutes-summary`。
75
+ - `src/manifest.json.name` 必须保留完整平台组件名,例如 `会议总结_一句话看懂_meeting_minutes_summary`,不能只写英文 code。
76
+ - 如果用户只提供中文名称或只提供英文 code,必须先追问补齐完整格式,不要自行编造缺失部分。
77
+
78
+ ### 组件名称存在性校验
79
+
80
+ 创建组件模板前,必须先调用名称存在性校验接口,不能跳过。
81
+
82
+ 固定接口:
83
+
84
+ - 登录接口:`POST https://prism-stone-pre.byering.com/api/web/auth/login`
85
+ - 登录账号:由用户提供,不能使用默认账号
86
+ - 登录密码:由用户提供,不能使用默认密码
87
+ - 校验接口:`POST https://prism-stone-pre.byering.com/api/admin/deliverable-h5-components/exists`
88
+ - 校验入参:`{"name":"组件包名称"}`
89
+ - 返回语义:`entity.exists` 表示组件名称是否已经存在。
90
+
91
+ 执行规则:
92
+
93
+ - 当用户给出组件名称后,先读取本地已保存的登录账号和密码;如果本地没有,再提示用户输入登录账号和密码。
94
+ - 用户提供账号密码后,先登录获取 Bearer Token;登录成功后把账号和密码保存到本地,供后续相关操作直接使用。
95
+ - 本地凭据保存路径固定为 `~/.wucaishi-component-workflow/auth.json`,文件权限应限制为 `600`。
96
+ - 不要在 skill、脚本或命令示例中写入默认账号、默认密码或个人凭据。
97
+ - 如果接口返回 `entity.exists === true`,表示组件名称已存在,必须停止创建并提示:`组件名称已存在,请更换组件名称`。
98
+ - 如果接口返回 `entity.exists === false`,表示组件名称可用,继续执行模板创建。
99
+ - 如果接口失败或返回结构异常,先说明接口错误,不要继续创建模板。
100
+
101
+ 推荐命令:
102
+
103
+ ```bash
104
+ node .agents/skills/component-package-workflow-zh/scripts/check_component_name_exists.mjs \
105
+ --name 组件包名 \
106
+ --phone 登录账号 \
107
+ --password 登录密码
108
+ ```
109
+
110
+ 如果本地已经保存过账号密码,后续可直接执行:
111
+
112
+ ```bash
113
+ node .agents/skills/component-package-workflow-zh/scripts/check_component_name_exists.mjs \
114
+ --name 组件包名
115
+ ```
116
+
117
+ ### 模板创建规则
118
+
119
+ 直接在当前仓库目录执行以下命令生成组件包基础模板:
120
+
121
+ ```bash
122
+ npx create-wucaishi-cpn@latest 组件包名
123
+ ```
124
+
125
+ 要求:
126
+
127
+ - `组件包名` 使用有明确语义的名字
128
+ - 如果用户没有输入组件包名称,必须先反问用户,不要擅自命名
129
+ - 创建模板前必须先完成组件名称存在性校验,确认 `entity.exists === false`
130
+ - 执行名称校验前,如果本地没有已保存凭据,且用户没有提供登录账号和密码,必须先向用户索要,不要使用默认凭据
131
+ - 不要跳过模板创建步骤直接手写整套组件包
132
+ - 如果模板已经生成,优先在模板基础上修改
133
+
134
+ ## 第二阶段:自定义开发组件包
135
+
136
+ 在模板基础上继续开发组件包,包括但不限于:
137
+
138
+ - 调整组件结构
139
+ - 定义或修改 Props
140
+ - 编写样式和交互逻辑
141
+ - 按 H5 移动端设计稿、截图参考或用户给出的视觉素材提取组件结构与数据边界
142
+ - 生成或修改组件后,启动本地预览并打开预览页面进行查看与确认
143
+ - 完成预览后,明确把本地预览地址反馈给用户
144
+ - 补齐导出内容
145
+ - 补齐组件包文档
146
+
147
+ ### 开发要求
148
+
149
+ - 优先保留模板目录结构,不要无故推倒重建
150
+ - 组件包是可复用交付物,不要按一次性页面片段的方式编写
151
+ - 当组件面向 H5 或基于截图生成时,必须继续遵循 React 子 Skill 中的移动端适配、375 设计稿基准和截图解析规则
152
+ - 如果组件对外提供使用说明,组件文档统一维护在 `README.md`
153
+ - 页面生成或完成修改后,应主动启动本地预览,打开预览页面检查实际效果,并把本地预览地址明确告知用户,再继续后续流程
154
+ - 开发完成并完成必要自检后再进入打包,不要边写边发布
155
+
156
+ ## 第三阶段:发布组件包
157
+
158
+ 当用户要求打包组件包、上传组件包、发布、发布组建、打包发布、同步更新总文档或上传总文档时,统一按本阶段内的各子阶段顺序推进,不要跳步执行。
159
+
160
+ ### 阶段一:打包组件包
161
+
162
+ 当用户要求打包组件包时,按以下顺序执行:
163
+
164
+ 1. 确认组件包开发已经完成
165
+ 2. 确认必要文档已经同步,并完成必要自检
166
+ 3. 先继续读取 `subskills/build-version-confirm-zh/SKILL.md`,并让用户确认本次要如何更新版本号
167
+ 4. 完成版本同步后,执行打包命令
168
+ 5. 打包成功后再进入上传流程
169
+
170
+ 当前仓库可用的打包命令为:
171
+
172
+ ```bash
173
+ npm run build
174
+ ```
175
+
176
+ 打包约束:
177
+
178
+ - 打包失败时先修复问题,再重新打包
179
+ - 没有完成开发或自检时,不要直接打包交付
180
+ - 只要进入打包流程,就应继续读取 `subskills/build-version-confirm-zh/SKILL.md`,先完成版本确认和版本更新,再执行构建
181
+
182
+ ### 阶段二:上传组件包
183
+
184
+ 当用户要求上传组件包时,上传流程统一通过 `subskills/upload-aliyun-oss-zh/SKILL.md` 执行,不再在父 Skill 内重复维护另一套上传步骤。
185
+
186
+ 上传约束:
187
+
188
+ - 不要在上传目标不明确时擅自发布
189
+ - 不要跳过打包直接上传
190
+ - 进入组件包上传流程时,必须继续读取 `subskills/upload-aliyun-oss-zh/SKILL.md`,并完全按该子 Skill 的规则执行
191
+ - 如果上传规则需要调整,应优先修改子 Skill,而不是在父 Skill 里追加平行规则
192
+
193
+ ## 当前已知命令
194
+
195
+ - 组件包生成命令:`npx create-wucaishi-cpn@latest 组件包名`
196
+ - 当前打包命令:`npm run build`
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { cp, mkdir, rm, rename, stat } from "node:fs/promises";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { homedir } from "node:os";
7
+
8
+ const SKILL_NAME = "component-package-workflow-zh";
9
+ const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
10
+
11
+ function printHelp() {
12
+ console.log(`usage: wucaishi-generative-react-skill [options]
13
+
14
+ Install the component-package-workflow-zh Codex skill.
15
+
16
+ options:
17
+ -h, --help Show help
18
+ --target PATH Install to a custom skill directory
19
+ --no-backup Replace an existing target without creating a backup
20
+
21
+ environment:
22
+ CODEX_HOME Defaults to ~/.codex when not set`);
23
+ }
24
+
25
+ function parseArgs(argv) {
26
+ const args = {
27
+ target: null,
28
+ backup: true,
29
+ };
30
+
31
+ for (let i = 0; i < argv.length; i += 1) {
32
+ const arg = argv[i];
33
+ const nextValue = () => {
34
+ const value = argv[i + 1];
35
+ if (!value || value.startsWith("--")) {
36
+ throw new Error(`${arg} requires a value`);
37
+ }
38
+ i += 1;
39
+ return value;
40
+ };
41
+
42
+ if (arg === "-h" || arg === "--help") {
43
+ printHelp();
44
+ process.exit(0);
45
+ } else if (arg === "--target") {
46
+ args.target = nextValue();
47
+ } else if (arg === "--no-backup") {
48
+ args.backup = false;
49
+ } else {
50
+ throw new Error(`Unknown option: ${arg}`);
51
+ }
52
+ }
53
+
54
+ return args;
55
+ }
56
+
57
+ async function pathExists(pathValue) {
58
+ try {
59
+ await stat(pathValue);
60
+ return true;
61
+ } catch {
62
+ return false;
63
+ }
64
+ }
65
+
66
+ function defaultTarget() {
67
+ const codexHome = process.env.CODEX_HOME || join(homedir(), ".codex");
68
+ return join(codexHome, "skills", SKILL_NAME);
69
+ }
70
+
71
+ function timestamp() {
72
+ return new Date()
73
+ .toISOString()
74
+ .replace(/[-:]/g, "")
75
+ .replace(/\..+$/, "")
76
+ .replace("T", "-");
77
+ }
78
+
79
+ async function copySkill(target) {
80
+ await mkdir(dirname(target), { recursive: true });
81
+ await mkdir(target, { recursive: true });
82
+
83
+ for (const entry of ["SKILL.md", "scripts", "subskills"]) {
84
+ await cp(join(PACKAGE_ROOT, entry), join(target, entry), {
85
+ recursive: true,
86
+ force: true,
87
+ });
88
+ }
89
+ }
90
+
91
+ async function main() {
92
+ const args = parseArgs(process.argv.slice(2));
93
+ const target = resolve(args.target || defaultTarget());
94
+
95
+ if (await pathExists(target)) {
96
+ if (args.backup) {
97
+ const backupTarget = `${target}.backup-${timestamp()}`;
98
+ await rename(target, backupTarget);
99
+ console.log(`已备份已有 skill:${backupTarget}`);
100
+ } else {
101
+ await rm(target, { recursive: true, force: true });
102
+ }
103
+ }
104
+
105
+ await copySkill(target);
106
+
107
+ console.log(`已安装 ${SKILL_NAME} 到:${target}`);
108
+ console.log("请重启 Codex 或刷新 skills 后使用。");
109
+ }
110
+
111
+ main().catch((error) => {
112
+ console.error(`安装失败:${error.message}`);
113
+ process.exit(1);
114
+ });
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "wucaishi-generative-react-skill",
3
+ "version": "0.1.0",
4
+ "description": "Codex skill for Wucaishi component package workflow and HTML-to-React generation.",
5
+ "type": "module",
6
+ "bin": {
7
+ "wucaishi-generative-react-skill": "bin/install.mjs"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "SKILL.md",
12
+ "scripts",
13
+ "subskills",
14
+ "README.md"
15
+ ],
16
+ "engines": {
17
+ "node": ">=18"
18
+ },
19
+ "keywords": [
20
+ "codex-skill",
21
+ "react",
22
+ "html-to-react",
23
+ "component-workflow",
24
+ "wucaishi"
25
+ ],
26
+ "license": "UNLICENSED"
27
+ }
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises';
4
+ import { homedir } from 'node:os';
5
+ import { dirname, join } from 'node:path';
6
+
7
+ const API_BASE = 'https://prism-stone-pre.byering.com/api';
8
+ const AUTH_CACHE_PATH = join(homedir(), '.wucaishi-component-workflow', 'auth.json');
9
+
10
+ function fail(message, code = 1) {
11
+ console.error(`错误:${message}`);
12
+ process.exit(code);
13
+ }
14
+
15
+ function parseArgs(argv) {
16
+ const args = {
17
+ name: null,
18
+ phone: null,
19
+ password: null,
20
+ };
21
+
22
+ for (let i = 0; i < argv.length; i += 1) {
23
+ const arg = argv[i];
24
+ const nextValue = () => {
25
+ const value = argv[i + 1];
26
+ if (!value || value.startsWith('--')) {
27
+ fail(`${arg} 缺少参数值`);
28
+ }
29
+ i += 1;
30
+ return value;
31
+ };
32
+
33
+ if (arg === '-h' || arg === '--help') {
34
+ printHelp();
35
+ process.exit(0);
36
+ } else if (arg === '--name') {
37
+ args.name = nextValue();
38
+ } else if (arg === '--phone') {
39
+ args.phone = nextValue();
40
+ } else if (arg === '--password') {
41
+ args.password = nextValue();
42
+ } else {
43
+ fail(`未知参数:${arg}`);
44
+ }
45
+ }
46
+
47
+ if (!args.name || !args.name.trim()) {
48
+ fail('必须传入 --name 组件名');
49
+ }
50
+
51
+ args.name = args.name.trim();
52
+ args.phone = args.phone ? args.phone.trim() : null;
53
+ args.password = args.password ? args.password.trim() : null;
54
+ return args;
55
+ }
56
+
57
+ function printHelp() {
58
+ console.log(`usage: check_component_name_exists.mjs --name COMPONENT_NAME [options]
59
+
60
+ 创建组件模板前校验组件名称是否已存在。
61
+
62
+ options:
63
+ -h, --help 显示帮助
64
+ --name COMPONENT_NAME 组件名,必填
65
+ --phone PHONE 登录账号,首次登录必填;之后可读取本地缓存
66
+ --password PASSWORD 登录密码,首次登录必填;之后可读取本地缓存`);
67
+ }
68
+
69
+ async function loadSavedCredentials() {
70
+ try {
71
+ const raw = await readFile(AUTH_CACHE_PATH, 'utf8');
72
+ const saved = JSON.parse(raw);
73
+ if (saved && typeof saved.phone === 'string' && typeof saved.password === 'string') {
74
+ return {
75
+ phone: saved.phone.trim(),
76
+ password: saved.password.trim(),
77
+ };
78
+ }
79
+ } catch {
80
+ return null;
81
+ }
82
+ return null;
83
+ }
84
+
85
+ async function saveCredentials(phone, password) {
86
+ const payload = JSON.stringify({ phone, password }, null, 2);
87
+ await mkdir(dirname(AUTH_CACHE_PATH), { recursive: true, mode: 0o700 });
88
+ await writeFile(AUTH_CACHE_PATH, payload, { mode: 0o600 });
89
+ await chmod(AUTH_CACHE_PATH, 0o600);
90
+ }
91
+
92
+ async function resolveCredentials(args) {
93
+ const saved = await loadSavedCredentials();
94
+ const phone = args.phone || saved?.phone;
95
+ const password = args.password || saved?.password;
96
+
97
+ if (!phone || !password) {
98
+ fail(`缺少登录账号或密码。请先传入 --phone 登录账号 --password 登录密码,登录成功后会保存到本地:${AUTH_CACHE_PATH}`);
99
+ }
100
+
101
+ return { phone, password };
102
+ }
103
+
104
+ async function requestJson(method, url, { body, token } = {}) {
105
+ const headers = { Accept: 'application/json' };
106
+ const init = { method, headers };
107
+ if (body !== undefined) {
108
+ headers['Content-Type'] = 'application/json';
109
+ init.body = JSON.stringify(body);
110
+ }
111
+ if (token) {
112
+ headers.Authorization = `Bearer ${token}`;
113
+ }
114
+
115
+ let response;
116
+ try {
117
+ response = await fetch(url, init);
118
+ } catch (error) {
119
+ fail(`请求失败:${method} ${url}\n${error.message}`);
120
+ }
121
+
122
+ const raw = await response.text();
123
+ if (!response.ok) {
124
+ fail(`HTTP ${response.status}:${method} ${url}\n${raw}`);
125
+ }
126
+
127
+ try {
128
+ return JSON.parse(raw);
129
+ } catch {
130
+ fail(`接口返回不是 JSON:${method} ${url}\n${raw}`);
131
+ }
132
+ }
133
+
134
+ function entityOrFail(payload, apiName) {
135
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
136
+ fail(`${apiName} 返回不是对象:${String(payload)}`);
137
+ }
138
+ if (String(payload.code) !== '10000') {
139
+ fail(`${apiName} 返回失败:${JSON.stringify(payload, null, 2)}`);
140
+ }
141
+ if (!payload.entity || typeof payload.entity !== 'object' || Array.isArray(payload.entity)) {
142
+ fail(`${apiName} 返回缺少 entity:${JSON.stringify(payload, null, 2)}`);
143
+ }
144
+ return payload.entity;
145
+ }
146
+
147
+ async function login(phone, password) {
148
+ const body = { phone, password };
149
+ const payload = await requestJson('POST', `${API_BASE}/web/auth/login`, { body });
150
+ const entity = entityOrFail(payload, '测试登录');
151
+ if (!entity.accessToken) {
152
+ fail(`测试登录返回缺少 entity.accessToken:${JSON.stringify(payload, null, 2)}`);
153
+ }
154
+ return entity.accessToken;
155
+ }
156
+
157
+ async function checkExists(token, name) {
158
+ const payload = await requestJson('POST', `${API_BASE}/admin/deliverable-h5-components/exists`, {
159
+ body: { name },
160
+ token,
161
+ });
162
+ const entity = entityOrFail(payload, '组件名称存在性校验');
163
+ if (typeof entity.exists !== 'boolean') {
164
+ fail(`组件名称存在性校验返回缺少 boolean entity.exists:${JSON.stringify(payload, null, 2)}`);
165
+ }
166
+ return entity.exists;
167
+ }
168
+
169
+ async function main() {
170
+ const args = parseArgs(process.argv.slice(2));
171
+ const credentials = await resolveCredentials(args);
172
+ console.log(`API 域名:${API_BASE}`);
173
+ console.log(`登录账号:${credentials.phone}`);
174
+ console.log(`待校验组件名:${args.name}`);
175
+
176
+ const token = await login(credentials.phone, credentials.password);
177
+ await saveCredentials(credentials.phone, credentials.password);
178
+ const exists = await checkExists(token, args.name);
179
+
180
+ if (exists) {
181
+ fail(`组件名称已存在,请更换组件名称:${args.name}`, 2);
182
+ }
183
+
184
+ console.log(`组件名称可用:${args.name}`);
185
+ }
186
+
187
+ main().catch((error) => fail(error.stack || error.message || String(error)));
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync, readdirSync, statSync } from "node:fs";
4
+ import { join, relative } from "node:path";
5
+
6
+ const root = process.argv[2] || "src";
7
+ const MAX_FONT_PX = 24;
8
+ const allowLargeFontMarker = "allow-large-font";
9
+
10
+ function walk(dir) {
11
+ const entries = readdirSync(dir);
12
+ return entries.flatMap((entry) => {
13
+ const path = join(dir, entry);
14
+ const stat = statSync(path);
15
+ if (stat.isDirectory()) {
16
+ return walk(path);
17
+ }
18
+ return /\.(styles\.ts|styles\.tsx|tsx|ts)$/.test(entry) ? [path] : [];
19
+ });
20
+ }
21
+
22
+ function collectNumbers(pattern, text) {
23
+ return Array.from(text.matchAll(pattern), (match) => Number.parseFloat(match[1])).filter(Number.isFinite);
24
+ }
25
+
26
+ function isFontSizeLine(line) {
27
+ return /font-size\s*:/.test(line);
28
+ }
29
+
30
+ const findings = [];
31
+
32
+ for (const file of walk(root)) {
33
+ const source = readFileSync(file, "utf8");
34
+ const lines = source.split(/\r?\n/);
35
+
36
+ lines.forEach((line, index) => {
37
+ if (!isFontSizeLine(line) || line.includes(allowLargeFontMarker)) {
38
+ return;
39
+ }
40
+
41
+ const trimmed = line.trim();
42
+ const largePx = collectNumbers(/(\d+(?:\.\d+)?)px\b/g, line).filter((value) => value > MAX_FONT_PX);
43
+ const largeVwFn = collectNumbers(/\bvw\(\s*(\d+(?:\.\d+)?)\s*\)/g, line).filter((value) => value > MAX_FONT_PX);
44
+ const hasBareVwFunction = /\bvw\(\s*\d+(?:\.\d+)?\s*\)/.test(line) && !/\bfluidFont\(/.test(line) && !/\bclamp\(/.test(line);
45
+ const hasBareVwUnit = /\d+(?:\.\d+)?vw\b/.test(line) && !/\bclamp\(/.test(line);
46
+
47
+ if (largePx.length > 0) {
48
+ findings.push({
49
+ file,
50
+ line: index + 1,
51
+ reason: `font-size 超过 ${MAX_FONT_PX}px:${largePx.join(", ")}`,
52
+ text: trimmed,
53
+ });
54
+ }
55
+
56
+ if (largeVwFn.length > 0) {
57
+ findings.push({
58
+ file,
59
+ line: index + 1,
60
+ reason: `font-size 的 vw() 输入超过 ${MAX_FONT_PX}px:${largeVwFn.join(", ")}`,
61
+ text: trimmed,
62
+ });
63
+ }
64
+
65
+ if (hasBareVwFunction || hasBareVwUnit) {
66
+ findings.push({
67
+ file,
68
+ line: index + 1,
69
+ reason: "font-size 不应裸用无边界 vw,改用 fluidFont() 或 clamp()",
70
+ text: trimmed,
71
+ });
72
+ }
73
+ });
74
+ }
75
+
76
+ if (findings.length > 0) {
77
+ console.error("H5 font size check failed:");
78
+ for (const finding of findings) {
79
+ console.error(`${relative(process.cwd(), finding.file)}:${finding.line} ${finding.reason}`);
80
+ console.error(` ${finding.text}`);
81
+ }
82
+ process.exit(1);
83
+ }
84
+
85
+ console.log("H5 font size check passed.");