@ybais/create-ybais-app 0.2.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 +21 -0
- package/dist/index.js +371 -0
- package/package.json +34 -0
- package/template/.cursor/rules/ybais-app.mdc +21 -0
- package/template/.cursor/skills/ybais-app/SKILL.md +140 -0
- package/template/.cursor/skills/ybais-app/reference.md +180 -0
- package/template/.env +2 -0
- package/template/.env.prod +8 -0
- package/template/.env.test +8 -0
- package/template/.mise/tasks/build +107 -0
- package/template/.oxlintrc.json +22 -0
- package/template/AGENTS.md +25 -0
- package/template/README.md +251 -0
- package/template/dist/assets/css/index-bQrzgOlr.css +1 -0
- package/template/dist/assets/js/dist-CX33qWwE.js +2 -0
- package/template/dist/assets/js/dist-legacy-BnxRxjXx.js +1 -0
- package/template/dist/assets/js/index-BNqkt-js.js +2 -0
- package/template/dist/assets/js/index-legacy-DPO2FCfH.js +1 -0
- package/template/dist/assets/js/polyfills-Dsaw-v00.js +1 -0
- package/template/dist/assets/js/polyfills-legacy-DyZJWH2U.js +1 -0
- package/template/dist/assets/js/rolldown-runtime-LhxUO5vB.js +1 -0
- package/template/dist/assets/js/rolldown-runtime-legacy-BptCTa2h.js +1 -0
- package/template/dist/assets/js/routes-D2ibRZk6.js +1 -0
- package/template/dist/assets/js/routes-legacy-DGEWjKnS.js +1 -0
- package/template/dist/assets/js/vconsole.min-BcOaunN2.js +1 -0
- package/template/dist/assets/js/vconsole.min-legacy-Bub-moxG.js +1 -0
- package/template/dist/favicon.svg +1 -0
- package/template/dist/index.html +29 -0
- package/template/dist/version.json +4 -0
- package/template/index.html +19 -0
- package/template/mise.toml +11 -0
- package/template/package.json +60 -0
- package/template/plugins/app-version.ts +88 -0
- package/template/pnpm-lock.yaml +3359 -0
- package/template/pnpm-workspace.yaml +7 -0
- package/template/postcss.config.js +24 -0
- package/template/public/favicon.svg +1 -0
- package/template/scripts/run-build.mjs +5 -0
- package/template/scripts/setup-mise.mjs +47 -0
- package/template/scripts/with-mise.mjs +107 -0
- package/template/src/api/alova.d.ts +7 -0
- package/template/src/api/alova.ts +50 -0
- package/template/src/api/index.ts +8 -0
- package/template/src/api/qiniu.ts +8 -0
- package/template/src/api/uploader.ts +25 -0
- package/template/src/components/NotFoundComponent/index.tsx +48 -0
- package/template/src/components/RouterErrorComponent/index.tsx +27 -0
- package/template/src/components/index.ts +2 -0
- package/template/src/lib/README.md +17 -0
- package/template/src/main.tsx +64 -0
- package/template/src/routeTree.gen.ts +59 -0
- package/template/src/router.ts +10 -0
- package/template/src/routes/__root.tsx +10 -0
- package/template/src/routes/index.tsx +39 -0
- package/template/src/styles/index.css +42 -0
- package/template/src/styles/variant.css +18 -0
- package/template/src/styles/webview-compat.css +49 -0
- package/template/src/utils/cn.ts +63 -0
- package/template/src/vite-env.d.ts +41 -0
- package/template/tailwind.config.js +14 -0
- package/template/tsconfig.app.json +37 -0
- package/template/tsconfig.json +7 -0
- package/template/tsconfig.node.json +28 -0
- package/template/vite.config.ts +131 -0
package/README.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# @ybais/create-ybais-app
|
|
2
|
+
|
|
3
|
+
Scaffold a YBAIS React + Vite WebView app(基础设施依赖 `@ybais/*`)。
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx @ybais/create-ybais-app my-app
|
|
7
|
+
# 等价:
|
|
8
|
+
npm create @ybais/ybais-app my-app
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Interactive prompts include:
|
|
12
|
+
|
|
13
|
+
- Test / Prod API base URLs
|
|
14
|
+
- **Enable vConsole?** (default no) — if yes, installs `vconsole` + `@ybais/vconsole`(仅 test;prod / master 杜绝)。If no, dependency and `@@vconsole` block are stripped.
|
|
15
|
+
- Run `pnpm install` now?
|
|
16
|
+
|
|
17
|
+
生成项目的 `@ybais/*` 依赖会写成 `^0.1.0`(需已发布到 npm)。Monorepo 内 template 使用 `workspace:*`。
|
|
18
|
+
|
|
19
|
+
旧包名 `create-ybais-app`(个人账号下)已迁移为 **`@ybais/create-ybais-app`**;请改用带 scope 的命令。
|
|
20
|
+
|
|
21
|
+
See the repository root README / [PUBLISH.md](../../PUBLISH.md) for development and publish instructions.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
import mri from "mri";
|
|
7
|
+
import * as p from "@clack/prompts";
|
|
8
|
+
import pc from "picocolors";
|
|
9
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const TEMPLATE_DIR = path.resolve(__dirname, "../template");
|
|
11
|
+
const DEFAULT_TEST_API = "https://api-test.example.com";
|
|
12
|
+
const DEFAULT_PROD_API = "https://api.example.com";
|
|
13
|
+
function printHelp() {
|
|
14
|
+
console.log(`
|
|
15
|
+
${pc.bold("@ybais/create-ybais-app")} — scaffold a YBAIS React + Vite app
|
|
16
|
+
|
|
17
|
+
${pc.dim("Usage:")}
|
|
18
|
+
npx @ybais/create-ybais-app [project-name] [options]
|
|
19
|
+
# 或: npm create @ybais/ybais-app [project-name]
|
|
20
|
+
|
|
21
|
+
${pc.dim("Options:")}
|
|
22
|
+
--overwrite Overwrite non-empty target directory without prompting
|
|
23
|
+
-h, --help Show help
|
|
24
|
+
|
|
25
|
+
${pc.dim("Fixed:")}
|
|
26
|
+
package manager pnpm · git init · mise branch build · @vitejs/plugin-legacy
|
|
27
|
+
|
|
28
|
+
${pc.dim("Optional:")}
|
|
29
|
+
pnpm install — asked during scaffold (default: no)
|
|
30
|
+
vConsole — asked during scaffold (default: no; test-only, never on master/prod)
|
|
31
|
+
`);
|
|
32
|
+
}
|
|
33
|
+
function isValidPackageName(name) {
|
|
34
|
+
return /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(name);
|
|
35
|
+
}
|
|
36
|
+
function toValidPackageName(name) {
|
|
37
|
+
return name
|
|
38
|
+
.trim()
|
|
39
|
+
.toLowerCase()
|
|
40
|
+
.replace(/\s+/g, "-")
|
|
41
|
+
.replace(/^[._]/, "")
|
|
42
|
+
.replace(/[^a-z0-9-~]+/g, "-");
|
|
43
|
+
}
|
|
44
|
+
function emptyDir(dir) {
|
|
45
|
+
if (!fs.existsSync(dir))
|
|
46
|
+
return;
|
|
47
|
+
for (const file of fs.readdirSync(dir)) {
|
|
48
|
+
if (file === ".git")
|
|
49
|
+
continue;
|
|
50
|
+
fs.rmSync(path.join(dir, file), { recursive: true, force: true });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function isDirEmpty(dir) {
|
|
54
|
+
if (!fs.existsSync(dir))
|
|
55
|
+
return true;
|
|
56
|
+
const files = fs.readdirSync(dir).filter((f) => f !== ".git");
|
|
57
|
+
return files.length === 0;
|
|
58
|
+
}
|
|
59
|
+
function copyTemplate(src, dest) {
|
|
60
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
61
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
62
|
+
const name = entry.name;
|
|
63
|
+
if (name === "node_modules" ||
|
|
64
|
+
name === "dist" ||
|
|
65
|
+
name === ".git" ||
|
|
66
|
+
name === "pnpm-lock.yaml" ||
|
|
67
|
+
name.endsWith(".local")) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const from = path.join(src, name);
|
|
71
|
+
const to = path.join(dest, name);
|
|
72
|
+
if (entry.isDirectory()) {
|
|
73
|
+
copyTemplate(from, to);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
fs.copyFileSync(from, to);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function writeEnvFiles(root, opts) {
|
|
81
|
+
fs.writeFileSync(path.join(root, ".env"), `# 所有模式共享的默认环境变量(可被 .env.[mode] 覆盖)\nVITE_APP_TITLE=${opts.title}\n`, "utf8");
|
|
82
|
+
fs.writeFileSync(path.join(root, ".env.test"), `VITE_APP_ENV=test\nVITE_API_BASE_URL=${opts.testApi}\n`, "utf8");
|
|
83
|
+
fs.writeFileSync(path.join(root, ".env.prod"), `VITE_APP_ENV=prod\nVITE_API_BASE_URL=${opts.prodApi}\n`, "utf8");
|
|
84
|
+
}
|
|
85
|
+
const YBAIS_PKG_VERSION = "^0.1.0";
|
|
86
|
+
const VCONSOLE_DEP = "^3.15.1";
|
|
87
|
+
function rewritePackageJson(root, packageName) {
|
|
88
|
+
const pkgPath = path.join(root, "package.json");
|
|
89
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
90
|
+
pkg.name = packageName;
|
|
91
|
+
pkg.version = "0.0.1";
|
|
92
|
+
pkg.dependencies ??= {};
|
|
93
|
+
// monorepo template 用 workspace:*;脚手架产出独立项目时改为 semver
|
|
94
|
+
for (const [dep, range] of Object.entries(pkg.dependencies)) {
|
|
95
|
+
if (dep.startsWith("@ybais/") && range.startsWith("workspace:")) {
|
|
96
|
+
pkg.dependencies[dep] = YBAIS_PKG_VERSION;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, "utf8");
|
|
100
|
+
}
|
|
101
|
+
/** 按脚手架选项保留或剥离 vConsole(依赖 / main 标记 / README 行) */
|
|
102
|
+
function applyVConsoleOption(root, enabled) {
|
|
103
|
+
const pkgPath = path.join(root, "package.json");
|
|
104
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
105
|
+
pkg.dependencies ??= {};
|
|
106
|
+
if (enabled) {
|
|
107
|
+
pkg.dependencies.vconsole = VCONSOLE_DEP;
|
|
108
|
+
pkg.dependencies["@ybais/vconsole"] ??= YBAIS_PKG_VERSION;
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
delete pkg.dependencies.vconsole;
|
|
112
|
+
delete pkg.dependencies["@ybais/vconsole"];
|
|
113
|
+
}
|
|
114
|
+
fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, "utf8");
|
|
115
|
+
const mainPath = path.join(root, "src/main.tsx");
|
|
116
|
+
let main = fs.readFileSync(mainPath, "utf8");
|
|
117
|
+
if (!enabled) {
|
|
118
|
+
main = main.replace(/\/\/ @@vconsole\n[\s\S]*?\/\/ @@\/vconsole\n/g, "");
|
|
119
|
+
}
|
|
120
|
+
fs.writeFileSync(mainPath, main, "utf8");
|
|
121
|
+
const libReadme = path.join(root, "src/lib/README.md");
|
|
122
|
+
if (fs.existsSync(libReadme)) {
|
|
123
|
+
let readme = fs.readFileSync(libReadme, "utf8");
|
|
124
|
+
if (enabled) {
|
|
125
|
+
if (!readme.includes("@ybais/vconsole")) {
|
|
126
|
+
readme = readme.replace("| VConsole | `@ybais/vconsole` |", "| VConsole | `@ybais/vconsole` |");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
readme = readme.replace(/\| VConsole \| `@ybais\/vconsole` \|\n/, "");
|
|
131
|
+
}
|
|
132
|
+
fs.writeFileSync(libReadme, readme, "utf8");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function rewriteIndexHtml(root, title) {
|
|
136
|
+
const htmlPath = path.join(root, "index.html");
|
|
137
|
+
let html = fs.readFileSync(htmlPath, "utf8");
|
|
138
|
+
html = html.replace(/<title>.*?<\/title>/, `<title>${title}</title>`);
|
|
139
|
+
fs.writeFileSync(htmlPath, html, "utf8");
|
|
140
|
+
}
|
|
141
|
+
function commandExists(cmd) {
|
|
142
|
+
const result = spawnSync(cmd, ["--version"], {
|
|
143
|
+
stdio: "ignore",
|
|
144
|
+
shell: process.platform === "win32",
|
|
145
|
+
});
|
|
146
|
+
return result.status === 0;
|
|
147
|
+
}
|
|
148
|
+
function run(cmd, args, cwd) {
|
|
149
|
+
const result = spawnSync(cmd, args, {
|
|
150
|
+
cwd,
|
|
151
|
+
stdio: "inherit",
|
|
152
|
+
shell: process.platform === "win32",
|
|
153
|
+
});
|
|
154
|
+
if (result.error) {
|
|
155
|
+
return { ok: false, error: result.error.message };
|
|
156
|
+
}
|
|
157
|
+
if (result.status !== 0) {
|
|
158
|
+
return { ok: false, error: `${cmd} exited with code ${result.status}` };
|
|
159
|
+
}
|
|
160
|
+
return { ok: true };
|
|
161
|
+
}
|
|
162
|
+
async function main() {
|
|
163
|
+
const argv = mri(process.argv.slice(2), {
|
|
164
|
+
boolean: ["overwrite", "help"],
|
|
165
|
+
alias: { h: "help" },
|
|
166
|
+
});
|
|
167
|
+
if (argv.help) {
|
|
168
|
+
printHelp();
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
console.log();
|
|
172
|
+
p.intro(pc.bgCyan(pc.black(" @ybais/create-ybais-app ")));
|
|
173
|
+
let targetName = argv._[0]?.trim();
|
|
174
|
+
if (!targetName) {
|
|
175
|
+
const result = await p.text({
|
|
176
|
+
message: "Project name:",
|
|
177
|
+
placeholder: "my-ybais-app",
|
|
178
|
+
defaultValue: "my-ybais-app",
|
|
179
|
+
validate(value) {
|
|
180
|
+
if (!value.trim())
|
|
181
|
+
return "Project name is required";
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
if (p.isCancel(result)) {
|
|
185
|
+
p.cancel("Cancelled.");
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
targetName = result.trim();
|
|
189
|
+
}
|
|
190
|
+
const targetDir = path.resolve(process.cwd(), targetName);
|
|
191
|
+
const dirName = path.basename(targetDir);
|
|
192
|
+
if (!isDirEmpty(targetDir)) {
|
|
193
|
+
let overwrite = argv.overwrite;
|
|
194
|
+
if (overwrite === undefined) {
|
|
195
|
+
const result = await p.select({
|
|
196
|
+
message: `Target directory ${pc.cyan(dirName)} is not empty. Overwrite?`,
|
|
197
|
+
options: [
|
|
198
|
+
{ value: "yes", label: "Yes, overwrite" },
|
|
199
|
+
{ value: "no", label: "No, cancel" },
|
|
200
|
+
],
|
|
201
|
+
});
|
|
202
|
+
if (p.isCancel(result)) {
|
|
203
|
+
p.cancel("Cancelled.");
|
|
204
|
+
process.exit(1);
|
|
205
|
+
}
|
|
206
|
+
overwrite = result === "yes";
|
|
207
|
+
}
|
|
208
|
+
if (!overwrite) {
|
|
209
|
+
p.cancel("Cancelled — directory not empty.");
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
emptyDir(targetDir);
|
|
213
|
+
}
|
|
214
|
+
let packageName = toValidPackageName(dirName);
|
|
215
|
+
if (!isValidPackageName(packageName)) {
|
|
216
|
+
const result = await p.text({
|
|
217
|
+
message: "Package name (npm):",
|
|
218
|
+
placeholder: "my-ybais-app",
|
|
219
|
+
defaultValue: "my-ybais-app",
|
|
220
|
+
validate(value) {
|
|
221
|
+
if (!isValidPackageName(value)) {
|
|
222
|
+
return "Invalid package name";
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
if (p.isCancel(result)) {
|
|
227
|
+
p.cancel("Cancelled.");
|
|
228
|
+
process.exit(1);
|
|
229
|
+
}
|
|
230
|
+
packageName = result.trim();
|
|
231
|
+
}
|
|
232
|
+
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
233
|
+
let testApiUrl = DEFAULT_TEST_API;
|
|
234
|
+
let prodApiUrl = DEFAULT_PROD_API;
|
|
235
|
+
let shouldInstall = false;
|
|
236
|
+
let enableVConsole = false;
|
|
237
|
+
if (interactive) {
|
|
238
|
+
const testApi = await p.text({
|
|
239
|
+
message: "Test API base URL (VITE_API_BASE_URL):",
|
|
240
|
+
placeholder: DEFAULT_TEST_API,
|
|
241
|
+
defaultValue: DEFAULT_TEST_API,
|
|
242
|
+
});
|
|
243
|
+
if (p.isCancel(testApi)) {
|
|
244
|
+
p.cancel("Cancelled.");
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
const prodApi = await p.text({
|
|
248
|
+
message: "Prod API base URL (VITE_API_BASE_URL):",
|
|
249
|
+
placeholder: DEFAULT_PROD_API,
|
|
250
|
+
defaultValue: DEFAULT_PROD_API,
|
|
251
|
+
});
|
|
252
|
+
if (p.isCancel(prodApi)) {
|
|
253
|
+
p.cancel("Cancelled.");
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
256
|
+
testApiUrl = String(testApi).trim() || DEFAULT_TEST_API;
|
|
257
|
+
prodApiUrl = String(prodApi).trim() || DEFAULT_PROD_API;
|
|
258
|
+
const vconsoleConfirm = await p.confirm({
|
|
259
|
+
message: "Enable vConsole for test builds? (never on prod / master; default off)",
|
|
260
|
+
initialValue: false,
|
|
261
|
+
});
|
|
262
|
+
if (p.isCancel(vconsoleConfirm)) {
|
|
263
|
+
p.cancel("Cancelled.");
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
enableVConsole = vconsoleConfirm === true;
|
|
267
|
+
const installConfirm = await p.confirm({
|
|
268
|
+
message: "Run pnpm install now?",
|
|
269
|
+
initialValue: false,
|
|
270
|
+
});
|
|
271
|
+
if (p.isCancel(installConfirm)) {
|
|
272
|
+
p.cancel("Cancelled.");
|
|
273
|
+
process.exit(1);
|
|
274
|
+
}
|
|
275
|
+
shouldInstall = installConfirm === true;
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
p.log.info("Non-interactive mode — using default API URLs; skipping vConsole and pnpm install");
|
|
279
|
+
}
|
|
280
|
+
p.note([
|
|
281
|
+
`${pc.dim("package manager")} pnpm (fixed)`,
|
|
282
|
+
`${pc.dim("vConsole")} ${enableVConsole ? "yes (test only)" : "no"}`,
|
|
283
|
+
`${pc.dim("pnpm install")} ${shouldInstall ? "yes" : "no"}`,
|
|
284
|
+
`${pc.dim("git init")} yes (fixed)`,
|
|
285
|
+
`${pc.dim("mise build")} yes (fixed)`,
|
|
286
|
+
`${pc.dim("plugin-legacy")} yes (fixed)`,
|
|
287
|
+
].join("\n"), "Scaffold options");
|
|
288
|
+
const s = p.spinner();
|
|
289
|
+
s.start("Copying template…");
|
|
290
|
+
if (!fs.existsSync(TEMPLATE_DIR)) {
|
|
291
|
+
s.stop("Template missing");
|
|
292
|
+
p.log.error(`Template not found at ${TEMPLATE_DIR}`);
|
|
293
|
+
process.exit(1);
|
|
294
|
+
}
|
|
295
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
296
|
+
copyTemplate(TEMPLATE_DIR, targetDir);
|
|
297
|
+
rewritePackageJson(targetDir, packageName);
|
|
298
|
+
applyVConsoleOption(targetDir, enableVConsole);
|
|
299
|
+
writeEnvFiles(targetDir, {
|
|
300
|
+
title: dirName,
|
|
301
|
+
testApi: testApiUrl,
|
|
302
|
+
prodApi: prodApiUrl,
|
|
303
|
+
});
|
|
304
|
+
rewriteIndexHtml(targetDir, dirName);
|
|
305
|
+
s.stop("Template copied");
|
|
306
|
+
if (shouldInstall) {
|
|
307
|
+
if (!commandExists("pnpm")) {
|
|
308
|
+
p.log.error("pnpm is required but was not found. Install: https://pnpm.io/installation");
|
|
309
|
+
p.outro(pc.red("Scaffold files are ready — install pnpm, then run pnpm install in the project."));
|
|
310
|
+
process.exit(1);
|
|
311
|
+
}
|
|
312
|
+
s.start("Installing dependencies with pnpm…");
|
|
313
|
+
const install = run("pnpm", ["install"], targetDir);
|
|
314
|
+
if (!install.ok) {
|
|
315
|
+
s.stop("pnpm install failed");
|
|
316
|
+
p.log.error(install.error ?? "pnpm install failed");
|
|
317
|
+
process.exit(1);
|
|
318
|
+
}
|
|
319
|
+
s.stop("Dependencies installed");
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
p.log.info("Skipped pnpm install");
|
|
323
|
+
}
|
|
324
|
+
if (commandExists("git")) {
|
|
325
|
+
s.start("Initializing git repository…");
|
|
326
|
+
const gitInit = run("git", ["init"], targetDir);
|
|
327
|
+
if (!gitInit.ok) {
|
|
328
|
+
s.stop("git init skipped");
|
|
329
|
+
p.log.warn(gitInit.error ?? "git init failed");
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
run("git", ["add", "-A"], targetDir);
|
|
333
|
+
const commit = spawnSync("git", ["commit", "-m", "chore: initial commit from create-ybais-app"], {
|
|
334
|
+
cwd: targetDir,
|
|
335
|
+
stdio: "ignore",
|
|
336
|
+
shell: process.platform === "win32",
|
|
337
|
+
env: {
|
|
338
|
+
...process.env,
|
|
339
|
+
GIT_AUTHOR_NAME: "create-ybais-app",
|
|
340
|
+
GIT_AUTHOR_EMAIL: "create-ybais-app@local",
|
|
341
|
+
GIT_COMMITTER_NAME: "create-ybais-app",
|
|
342
|
+
GIT_COMMITTER_EMAIL: "create-ybais-app@local",
|
|
343
|
+
},
|
|
344
|
+
});
|
|
345
|
+
if (commit.status === 0) {
|
|
346
|
+
s.stop("Git repository initialized");
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
s.stop("git commit skipped");
|
|
350
|
+
p.log.warn("git commit failed (repo still initialized)");
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
p.log.warn("git not found — skipped git init");
|
|
356
|
+
}
|
|
357
|
+
const relativeRaw = path.relative(process.cwd(), targetDir) || ".";
|
|
358
|
+
const relative = path.isAbsolute(relativeRaw) || relativeRaw.startsWith("..")
|
|
359
|
+
? targetDir
|
|
360
|
+
: relativeRaw;
|
|
361
|
+
const nextSteps = [pc.green("Done!"), "", ` ${pc.cyan("cd")} ${relative}`];
|
|
362
|
+
if (!shouldInstall) {
|
|
363
|
+
nextSteps.push(` ${pc.cyan("pnpm")} install`);
|
|
364
|
+
}
|
|
365
|
+
nextSteps.push(` ${pc.cyan("pnpm")} dev`, ` ${pc.cyan("pnpm")} build`, "", pc.dim("build 会自动处理 mise(trust + 按分支选环境),无需改配置"), pc.dim("若未安装 mise:curl https://mise.run | sh"));
|
|
366
|
+
p.outro(nextSteps.join("\n"));
|
|
367
|
+
}
|
|
368
|
+
main().catch((err) => {
|
|
369
|
+
console.error(err);
|
|
370
|
+
process.exit(1);
|
|
371
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ybais/create-ybais-app",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Scaffold a YBAIS React + Vite app (pnpm, mise, legacy WebView)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-ybais-app": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"template"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=26.4.0",
|
|
15
|
+
"pnpm": ">=11.9.0"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@clack/prompts": "^0.11.0",
|
|
19
|
+
"mri": "^1.2.0",
|
|
20
|
+
"picocolors": "^1.1.1"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "^22.15.30",
|
|
24
|
+
"typescript": "~5.8.3"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsc -p tsconfig.json",
|
|
31
|
+
"start": "pnpm build && node ./dist/index.js",
|
|
32
|
+
"dev": "tsc -p tsconfig.json --watch"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: YBAIS App WebView 脚手架硬性约定(路由、@ybais 包、pnpm、环境变量、提交 dist、WebView 最低兼容)
|
|
3
|
+
alwaysApply: true
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# YBAIS App 约定
|
|
7
|
+
|
|
8
|
+
- 运行环境:**App 内嵌在线 WebView**(非独立现代浏览器站)。
|
|
9
|
+
- WebView 最低兼容(与 `vite.config.ts` legacy `targets` / `cssTarget` 对齐):**iOS ≥ 12、Android ≥ 7、Chrome ≥ 55、Safari ≥ 12**,CSS 按 **chrome55**。保留 `@vitejs/plugin-legacy`;勿手写冲突的 `build.target`;图片默认勿强制 webp。改底线须同步改配置与 Skill/README。
|
|
10
|
+
- 包管理:pnpm(>=11.9.0);Node >=26.4.0(见 `package.json` engines)。`mise.toml` 用 major 模糊版本(`26` / `11`)取最新满足 engines 的线。构建:`pnpm build`(自动 trust mise,master→prod,否则 test)。
|
|
11
|
+
- 别名:`@/` → `src/`。基础设施用 `@ybais/*`(bridge / alova / qiniu-upload / updater / error-guard / clipboard / center-toast / crypto / vconsole);应用装配见 `src/api/alova.ts`、`src/api/uploader.ts`;Bridge Hooks 用 `@ybais/bridge/react`。
|
|
12
|
+
- 可恢复业务错误 / 接口失败走 `reportError` / `showApiError`(sonner error);**调试向提示走 `reportDebug`**(sonner warning,永不全屏)。
|
|
13
|
+
- **`showCenterToast` 仅用于面向用户的错误提示**(如「提交失败,请重试」);禁止用于 Bridge 调试、成功态、复制成功、加载中、Agent 随手提示。开发/调试一律 `reportDebug` / sonner / `console`。
|
|
14
|
+
- 错误分类见 `@ybais/error-guard`:`page` / `business` / `runtime` / `debug` / `chunk`。
|
|
15
|
+
- **vConsole**(若脚手架启用):仅 `VITE_APP_ENV=test` 且非 `master`;`?vconsole=1` / `enableVConsole()` 手动开;**prod / master 必须杜绝**。未启用则无 `vconsole` / `@ybais/vconsole` 依赖。
|
|
16
|
+
- 页面放 `src/routes/`,勿手改 `routeTree.gen.ts`。接口放 `src/api/`,经 `alovaInstance`。
|
|
17
|
+
- 仅 `VITE_*` 进客户端;密钥用 `.env.*.local`。同步改 `src/vite-env.d.ts`。
|
|
18
|
+
- 发版:构建后**提交并推送 `dist/`**(勿把 `dist` 写回 `.gitignore`);整包替换含 `version.json`。
|
|
19
|
+
- Native 通信用 bridge API,勿覆盖 `window` 上的命名回调代理;无 Bridge 时须 `.catch` / H5 回退。
|
|
20
|
+
- 布局按移动端 WebView(安全区、小屏),勿按桌面宽屏默认。
|
|
21
|
+
- 详细规范:阅读并遵循项目 Skill `.cursor/skills/ybais-app/SKILL.md`、`reference.md` 与根目录 `README.md`、`src/lib/README.md`。
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ybais-app
|
|
3
|
+
description: >-
|
|
4
|
+
Develop features in YBAIS App WebView scaffolds (create-ybais-app template):
|
|
5
|
+
React 18 + Vite + TanStack Router, JSBridge, Alova APIs, Qiniu upload, crypto
|
|
6
|
+
signing, production updater, and error-guard. Targets App-embedded WebView
|
|
7
|
+
with legacy floors ios>=12 / android>=7 / chrome>=55 / safari>=12. Use when
|
|
8
|
+
editing this project, adding routes or APIs, calling Native, uploading files,
|
|
9
|
+
changing env/build, handling page errors, WebView compatibility, or when the
|
|
10
|
+
user mentions bridge, alova, uploader, error-guard, @ybais, or create-ybais-app.
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# YBAIS App 开发规范
|
|
14
|
+
|
|
15
|
+
App 内嵌 **在线 WebView** H5(非独立浏览器 SPA 心智)。改代码前先遵循下列约定;细节读对应 README / `@ybais/*` 包文档,勿凭通用 React / 现代浏览器习惯覆盖本项目模式。
|
|
16
|
+
|
|
17
|
+
## WebView 最低兼容(与构建配置对齐)
|
|
18
|
+
|
|
19
|
+
权威来源:根目录 [`vite.config.ts`](../../../vite.config.ts) 中 `@vitejs/plugin-legacy` 的 `targets` 与 `build.cssTarget`。**改兼容底线时必须同步改该文件与本文档,勿只改一端。**
|
|
20
|
+
|
|
21
|
+
| 平台 / 引擎 | 最低版本 |
|
|
22
|
+
|-------------|----------|
|
|
23
|
+
| iOS(WKWebView / Safari) | **≥ 12** |
|
|
24
|
+
| Android(系统 WebView) | **≥ 7** |
|
|
25
|
+
| Chrome | **≥ 55** |
|
|
26
|
+
| Safari | **≥ 12** |
|
|
27
|
+
| CSS 压缩目标 | **`chrome55`**(禁止生成旧 Safari 吃不下的新 CSS 语法) |
|
|
28
|
+
|
|
29
|
+
构建侧已启用:`regenerator-runtime`、`abortcontroller-polyfill`(legacy + modern)、`web.queue-microtask` / `es.promise.all-settled` / `es.string.replace-all`(legacy + modern 显式)、`modernPolyfills`、`renderLegacyChunks`、Terser `ascii_only: true`。JS 的 `build.target` **不要**再手写,交给 legacy 插件接管。全局 CSS 补丁见 `src/styles/webview-compat.css`。
|
|
30
|
+
|
|
31
|
+
### 写代码时必须遵守
|
|
32
|
+
|
|
33
|
+
1. **保留** `@vitejs/plugin-legacy` 与上表 targets;禁止「为了包体积 / 现代语法」删掉或抬高底线,除非产品明确改 App 最低系统版本并同步改 `vite.config.ts`。
|
|
34
|
+
2. **图片**:上传 / 展示默认 **不要强制 webp**(七牛 imageView2 亦同);旧 WebView 对 webp 支持参差。
|
|
35
|
+
3. **API / 语法**:ES 语言特性可由 legacy + core-js 兜底;**Web API**(如 `AbortController`)不在 usage 自动检测内,须走 `vite.config` 的 `additionalLegacyPolyfills` / `additionalModernPolyfills`。新增依赖若使用未 polyfill 的 DOM API,须同步补配置并重建验证。避免依赖仅很新 Chromium 才有、且无降级的 API / CSS。
|
|
36
|
+
4. **布局**:按移动端 WebView 做安全区(`env(safe-area-inset-*)`)、触控与视口;勿按桌面宽屏默认设计。
|
|
37
|
+
5. **Bridge**:真实能力在 App 注入的 WebView;浏览器调试无 Bridge 时封装方法会 **reject**(`BridgeUnavailableError`,经 `configureBridge({ onUnavailable })` → `reportDebug`,**非**全屏阻断),须 `.catch` H5 回退。勿用 `showCenterToast` 做 Bridge 调试提示。
|
|
38
|
+
6. **相对路径**:`base: "./"`,静态资源与 `version.json` 按相对路径理解;部署须整包替换 `dist/`。
|
|
39
|
+
|
|
40
|
+
更多决策细节见 [reference.md](reference.md)「WebView 兼容」一节。
|
|
41
|
+
|
|
42
|
+
## 必读文档(按需打开)
|
|
43
|
+
|
|
44
|
+
| 场景 | 文档 |
|
|
45
|
+
|------|------|
|
|
46
|
+
| 工程总览 / 脚本 / 环境变量 | [README.md](../../../README.md) |
|
|
47
|
+
| `@ybais` 装配索引 | [src/lib/README.md](../../../src/lib/README.md) |
|
|
48
|
+
| 扩展清单 | [reference.md](reference.md) |
|
|
49
|
+
|
|
50
|
+
共享实现在 monorepo 的 `packages/@ybais/*`(或已发布的 `@ybais/*` npm 包)。
|
|
51
|
+
|
|
52
|
+
## 硬性约定
|
|
53
|
+
|
|
54
|
+
1. **包管理器只用 pnpm(>=11.9.0)**;Node `>=26.4.0`(见 `package.json` engines;`mise.toml` 用 `node = "26"` / `pnpm = "11"` 取最新 major 线)。构建用 `pnpm build`(脚本自动 trust mise,按分支选 test/prod),用户无需手改 mise 配置。
|
|
55
|
+
2. **发版提交 `dist/`**:`pnpm build`(或 `build:prod`)后将整包 `dist/` 提交并推送;WebView / 静态托管以仓库中的产物为准。勿把 `dist` 写回 `.gitignore`。
|
|
56
|
+
3. 路径别名 **`@/` → `src/`**;基础设施用 **`@ybais/<pkg>`** 按需导入;应用装配(env / goLogin)留在 `src/api/`;禁止平行再造一套 request/bridge/upload。
|
|
57
|
+
4. **仅 `VITE_` 环境变量**可进客户端;密钥用 `.env.*.local`,勿写入可提交的 `.env`。
|
|
58
|
+
5. **WebView 底线**:iOS ≥ 12 / Android ≥ 7 / Chrome ≥ 55 / Safari ≥ 12(见上文);勿去掉 legacy、勿手写冲突的 `build.target`、勿强制 webp。
|
|
59
|
+
6. `base: "./"`:静态资源与 `version.json` 按相对路径理解,部署须整包替换 `dist/`。
|
|
60
|
+
|
|
61
|
+
## 放哪里
|
|
62
|
+
|
|
63
|
+
| 需求 | 位置 |
|
|
64
|
+
|------|------|
|
|
65
|
+
| 页面 | `src/routes/*.tsx`(TanStack 文件路由,勿手改 `routeTree.gen.ts`) |
|
|
66
|
+
| 接口封装 | `src/api/`,内部用 `alovaInstance`(`src/api/alova.ts`) |
|
|
67
|
+
| 上传单例 | `src/api/uploader.ts`(`createUploader` + env) |
|
|
68
|
+
| 纯工具 | `src/utils/<name>.ts`(如 `cn`);`import { cn } from "@/utils/cn"` |
|
|
69
|
+
| 环境类型 | `src/vite-env.d.ts`(新增 `VITE_*` 时同步) |
|
|
70
|
+
| 样式 | Tailwind + `src/styles/`;center-toast 样式:`@ybais/center-toast/style.css` |
|
|
71
|
+
|
|
72
|
+
## 模块用法速查
|
|
73
|
+
|
|
74
|
+
### Bridge(H5 ↔ Native)
|
|
75
|
+
|
|
76
|
+
- `import { callNative, goLogin, onNative, configureBridge } from "@ybais/bridge"`
|
|
77
|
+
- 组件:`import { useNativeCall, useNativeCallback } from "@ybais/bridge/react"`
|
|
78
|
+
- `main.tsx` 已 `configureBridge({ onUnavailable: … reportDebug })`
|
|
79
|
+
- 浏览器无 Bridge 时会 **reject**,用 `.catch` 做 H5 回退
|
|
80
|
+
|
|
81
|
+
### Alova(HTTP)
|
|
82
|
+
|
|
83
|
+
- 实例在 `src/api/alova.ts`(`createYbaisAlova` + env + `onAuthFail`)
|
|
84
|
+
- `alovaInstance.Get/Post/...`;业务接口写在 `src/api/`
|
|
85
|
+
- Token / unwrap / businessCodeHandlers 从 `@ybais/alova` 或 `@/api/alova` 导入
|
|
86
|
+
- test/prod **打包**一律用 `VITE_API_BASE_URL`;仅 **dev** 走 `/proxyApi`
|
|
87
|
+
|
|
88
|
+
### Upload
|
|
89
|
+
|
|
90
|
+
- `import { uploader } from "@/api/uploader"`
|
|
91
|
+
- 或 `import { compressQiniuImageUrl } from "@ybais/qiniu-upload/image-process"`
|
|
92
|
+
- 需配置 `VITE_QINIU_AES_KEY` / `VITE_QINIU_AES_IV`;Token 见 `src/api/qiniu.ts`
|
|
93
|
+
|
|
94
|
+
### Clipboard
|
|
95
|
+
|
|
96
|
+
- `import { copyText } from "@ybais/clipboard"`
|
|
97
|
+
- `import { useCopyText } from "@ybais/clipboard/react"`
|
|
98
|
+
- **不要**直接依赖 `navigator.clipboard`
|
|
99
|
+
|
|
100
|
+
### Crypto
|
|
101
|
+
|
|
102
|
+
- `import { requestCrypto, md5 } from "@ybais/crypto"`
|
|
103
|
+
- key/secret 在 `src/api/alova.ts` 注入工厂
|
|
104
|
+
|
|
105
|
+
### Updater
|
|
106
|
+
|
|
107
|
+
- `main.tsx`:`startUpdater({ isProd: import.meta.env.PROD })`
|
|
108
|
+
- 自定义提示用 `onUpdate` + `silentReloadForVersion`
|
|
109
|
+
|
|
110
|
+
### Error-guard / Toast / 调试
|
|
111
|
+
|
|
112
|
+
- `setupErrorGuard({ isDev })` + `ErrorGuardProvider`
|
|
113
|
+
- 可恢复业务错误 → `reportError` / `showApiError`(**sonner error**)
|
|
114
|
+
- **调试错误 → `reportDebug`**;勿用 `showCenterToast`
|
|
115
|
+
- **`showCenterToast` 仅面向用户的错误文案**(`@ybais/center-toast`)
|
|
116
|
+
- **vConsole**:`@ybais/vconsole`;仅 test 且非 master
|
|
117
|
+
|
|
118
|
+
## 实现新功能时的检查清单
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
- [ ] 页面是否放在 src/routes,且未手改 routeTree.gen.ts
|
|
122
|
+
- [ ] 接口是否经 alovaInstance,并落在 src/api
|
|
123
|
+
- [ ] Native 调用是否用 @ybais/bridge API / Hooks
|
|
124
|
+
- [ ] 新 VITE_* 是否写入 env 文件 + vite-env.d.ts
|
|
125
|
+
- [ ] 是否从 @ybais/<pkg> 按需导入(未再建聚合 barrel)
|
|
126
|
+
- [ ] 可恢复错误 → reportError / showApiError;调试 → reportDebug;用户向失败文案 → showCenterToast(仅此)
|
|
127
|
+
- [ ] 发版是否已 pnpm build 并提交/推送最新 dist/
|
|
128
|
+
- [ ] WebView:iOS 12 / Android 7 心智;无强制 webp;legacy / cssTarget: chrome55
|
|
129
|
+
- [ ] 无 Bridge 时有 .catch 或 H5 回退
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## 常见命令
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
pnpm install
|
|
136
|
+
pnpm dev # test + --host
|
|
137
|
+
pnpm build # master→prod,否则 test(需 mise)
|
|
138
|
+
pnpm build:test|prod
|
|
139
|
+
pnpm lint
|
|
140
|
+
```
|