@collectui/bdui 0.1.9 → 0.1.11
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/package.json +1 -1
- package/src/add.js +22 -6
- package/src/hints.js +23 -0
- package/src/init.js +133 -29
- package/src/mcp.js +36 -9
- package/src/registry.js +4 -1
- package/src/write.js +28 -1
package/package.json
CHANGED
package/src/add.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { spawn } from "child_process";
|
|
4
4
|
import ora from "ora";
|
|
5
5
|
import c from "./color.js";
|
|
6
|
+
import { itemHints } from "./hints.js";
|
|
6
7
|
import { detectProject, installCommand } from "./project.js";
|
|
7
8
|
import { fetchIndex, fetchItem } from "./registry.js";
|
|
8
9
|
import { writeItemFiles } from "./write.js";
|
|
@@ -95,6 +96,7 @@ export async function cmdAdd(names, flags) {
|
|
|
95
96
|
const deps = new Map(); // pkgName → 带 版本的 spec(去重,优先带版本形式)
|
|
96
97
|
const nextImports = new Set();
|
|
97
98
|
let wrote = 0;
|
|
99
|
+
let skippedExisting = 0;
|
|
98
100
|
writeItemFiles.lastWroteStyles = false;
|
|
99
101
|
|
|
100
102
|
for (const item of resolved.values()) {
|
|
@@ -110,7 +112,12 @@ export async function cmdAdd(names, flags) {
|
|
|
110
112
|
for (const m of (f.content ?? "").matchAll(/from\s+["'](next\/[a-z-]+)/g))
|
|
111
113
|
nextImports.add(m[1]);
|
|
112
114
|
}
|
|
113
|
-
|
|
115
|
+
const r = await writeItemFiles(item, outRoot, flags);
|
|
116
|
+
wrote += r.wrote;
|
|
117
|
+
skippedExisting += r.skippedExisting;
|
|
118
|
+
// 源码特征提示(不写死:特征随官方源码演进,官方修好提示自动消失)
|
|
119
|
+
for (const h of itemHints(item))
|
|
120
|
+
console.log(c.yellow(` ${item.name}: ${h}`));
|
|
114
121
|
}
|
|
115
122
|
|
|
116
123
|
if (deps.size) {
|
|
@@ -139,11 +146,20 @@ export async function cmdAdd(names, flags) {
|
|
|
139
146
|
}
|
|
140
147
|
console.log(c.green(`\n✓ ${wrote} file(s) written.`));
|
|
141
148
|
if (wrote === 0) {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
149
|
+
if (skippedExisting > 0) {
|
|
150
|
+
// 全部跳过=已装过,不是上游坏了(盲测 r2 P7 误报「may be broken」)
|
|
151
|
+
console.log(
|
|
152
|
+
c.yellow(
|
|
153
|
+
` already installed (${skippedExisting} file(s) exist) — pass --overwrite to update`
|
|
154
|
+
)
|
|
155
|
+
);
|
|
156
|
+
} else {
|
|
157
|
+
console.log(
|
|
158
|
+
c.yellow(
|
|
159
|
+
" warn nothing was written — every file was skipped, or the registry entry ships no component source (upstream entry may be broken)"
|
|
160
|
+
)
|
|
161
|
+
);
|
|
162
|
+
}
|
|
147
163
|
}
|
|
148
164
|
// Next 项目里 next/link 是一等公民,提示反而是噪音(盲测 #10)
|
|
149
165
|
if (nextImports.size && !project.deps?.next) {
|
package/src/hints.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// 条目提示(2026-10-26 盲测 r2 P2)——基于内容特征检测,不写死条目名:
|
|
2
|
+
// 官方源码随时会变,特征还在才提示,官方修好提示自动消失(用户铁律:
|
|
3
|
+
// 官方源码透传不改写,上游问题顶多提示,且提示不能写死)。
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 按条目当前源码特征给出安装/使用提示;无特征命中返回 []。
|
|
7
|
+
* @param {object} item registry 条目(files[].content)
|
|
8
|
+
* @returns {string[]}
|
|
9
|
+
*/
|
|
10
|
+
export function itemHints(item) {
|
|
11
|
+
const hints = [];
|
|
12
|
+
const src = (item.files ?? []).map((f) => f.content ?? "").join("\n");
|
|
13
|
+
|
|
14
|
+
// render 期读当前时间(new Date() / react-aria .toDate()):在 Next.js
|
|
15
|
+
// Cache Components 下,这类组件参与 prerender 时可能炸 build(是否炸取
|
|
16
|
+
// 决于该组件是否在预渲染路径渲染——实测未渲染于预渲染页时不炸)
|
|
17
|
+
if (/\bnew Date\(\)|\.toDate\(/.test(src)) {
|
|
18
|
+
hints.push(
|
|
19
|
+
"Note: this item reads the current time while rendering (per current registry source). Under Next.js Cache Components it can fail `next build` prerendering when it renders during prerender — wrap in <Suspense> if that happens (or preemptively)."
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
return hints;
|
|
23
|
+
}
|
package/src/init.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// `bdui init` — 对齐 boardui init:装 foundations(cx/theme/typography/globals)
|
|
2
2
|
// + agent 规则(AGENTS.md + .cursor/rules),内容全部来自 registry(与官方同源)
|
|
3
3
|
|
|
4
|
+
import { execSync } from "child_process";
|
|
4
5
|
import { appendFileSync, existsSync, readFileSync, writeFileSync } from "fs";
|
|
5
6
|
import ora from "ora";
|
|
6
7
|
import path from "path";
|
|
@@ -32,21 +33,27 @@ function frameworkSetup(project, cwd) {
|
|
|
32
33
|
// Next.js:官方模板自带 @/ alias 与 @types/node;仅补缺失的 tailwind 管道。
|
|
33
34
|
// postcss 配置是硬前提:模板没带 Tailwind 时 postcss.config 不存在,
|
|
34
35
|
// @tailwindcss/postcss 装了也不接管,组件样式静默失效(next build 只给
|
|
35
|
-
// "Unknown at rule: @apply" 警告仍报成功)——缺配置就补 v4
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"postcss
|
|
41
|
-
"postcss.config.ts",
|
|
42
|
-
].some((f) => existsSync(path.join(cwd, f)));
|
|
36
|
+
// "Unknown at rule: @apply" 警告仍报成功)——缺配置就补 v4 标准形态。
|
|
37
|
+
// 反向同理(CNA 16 默认 @tailwindcss/turbopack 管道):已走 turbopack
|
|
38
|
+
// 就不再注入 postcss,避免双管道并存困惑(盲测 r2 P13)
|
|
39
|
+
const hasTurbopackTw = has("@tailwindcss/turbopack");
|
|
40
|
+
if (!(hasTurbopackTw || has("@tailwindcss/postcss")))
|
|
41
|
+
pkgs.push("@tailwindcss/postcss");
|
|
43
42
|
const writes = [];
|
|
44
|
-
if (!
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
43
|
+
if (!hasTurbopackTw) {
|
|
44
|
+
const hasPostcssCfg = [
|
|
45
|
+
"postcss.config.mjs",
|
|
46
|
+
"postcss.config.js",
|
|
47
|
+
"postcss.config.cjs",
|
|
48
|
+
"postcss.config.ts",
|
|
49
|
+
].some((f) => existsSync(path.join(cwd, f)));
|
|
50
|
+
if (!hasPostcssCfg)
|
|
51
|
+
writes.push({
|
|
52
|
+
file: "postcss.config.mjs",
|
|
53
|
+
content:
|
|
54
|
+
'const config = {\n plugins: { "@tailwindcss/postcss": {} },\n};\n\nexport default config;\n',
|
|
55
|
+
});
|
|
56
|
+
}
|
|
50
57
|
return { pkgs, notes, writes };
|
|
51
58
|
}
|
|
52
59
|
const viteConfig = [
|
|
@@ -85,9 +92,9 @@ export async function cmdInit(flags) {
|
|
|
85
92
|
throw new Error("No package.json found — run inside your app project.");
|
|
86
93
|
|
|
87
94
|
// alias 不在此行展示:tsconfig 的 @/ 别名与实际落位(components/base/<group>)
|
|
88
|
-
// 口径不同,曾让客户误读(
|
|
95
|
+
// 口径不同,曾让客户误读(盲测 #6);落位在 ✓ 行明示。src= 是内部术语,给人话(r2 P10)
|
|
89
96
|
console.log(
|
|
90
|
-
`${c.cyan("init")} detected ${project.packageManager},
|
|
97
|
+
`${c.cyan("init")} detected ${project.packageManager}, source root: ${project.usesSrcDir ? "src/" : "project root"}`
|
|
91
98
|
);
|
|
92
99
|
|
|
93
100
|
// npm 依赖:foundation 用到的 + 可选 pro 依赖(官方 --pro 同款集)+ 框架适配
|
|
@@ -113,7 +120,7 @@ export async function cmdInit(flags) {
|
|
|
113
120
|
deps.set(pkgName, dep);
|
|
114
121
|
}
|
|
115
122
|
}
|
|
116
|
-
wrote += await writeItemFiles(item, outRoot, flags);
|
|
123
|
+
wrote += (await writeItemFiles(item, outRoot, flags)).wrote;
|
|
117
124
|
}
|
|
118
125
|
|
|
119
126
|
// 框架管道配置(postcss.config.mjs 等):样式静默失效的根因修复(盲测 #1)
|
|
@@ -139,9 +146,23 @@ export async function cmdInit(flags) {
|
|
|
139
146
|
` ${c.green("add")} .gitignore += .mcp.json (license key leak guard)`
|
|
140
147
|
);
|
|
141
148
|
}
|
|
149
|
+
// 已跟踪的 .mcp.json:gitignore 挡不住,防"已安全"错觉(盲测 r2 P8)
|
|
150
|
+
try {
|
|
151
|
+
execSync("git ls-files --error-unmatch .mcp.json", {
|
|
152
|
+
cwd,
|
|
153
|
+
stdio: "ignore",
|
|
154
|
+
});
|
|
155
|
+
console.log(
|
|
156
|
+
c.yellow(
|
|
157
|
+
" warn .mcp.json is already tracked by git — .gitignore does not untrack it. Run: git rm --cached .mcp.json (and rotate the key if it was ever pushed)"
|
|
158
|
+
)
|
|
159
|
+
);
|
|
160
|
+
} catch {}
|
|
142
161
|
}
|
|
143
162
|
|
|
144
|
-
//
|
|
163
|
+
// 样式入口接线:优先「替换」模板旧 globals 导入而非并排追加——两份
|
|
164
|
+
// @import \"tailwindcss\" = Tailwind 双编译(~75KB 冗余 CSS)+旧 body Arial
|
|
165
|
+
// 覆盖 BoardUI 排版(盲测 r2 P3);旧文件非 Tailwind 入口才走追加(盲测 #7)
|
|
145
166
|
const layoutFile = [
|
|
146
167
|
"app/layout.tsx",
|
|
147
168
|
"app/layout.ts",
|
|
@@ -151,23 +172,101 @@ export async function cmdInit(flags) {
|
|
|
151
172
|
.map((f) => path.join(project.sourceRoot, f))
|
|
152
173
|
.find((p) => existsSync(p));
|
|
153
174
|
let stylesWired = false;
|
|
175
|
+
let fontsHint = "";
|
|
154
176
|
if (layoutFile) {
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const
|
|
160
|
-
|
|
177
|
+
const rel = path.relative(cwd, layoutFile);
|
|
178
|
+
let content = readFileSync(layoutFile, "utf8");
|
|
179
|
+
if (!content.includes("@/styles/globals.css")) {
|
|
180
|
+
const lines = content.split("\n");
|
|
181
|
+
const oldIdx = lines.findIndex((l) =>
|
|
182
|
+
/^\s*import\s+["']\.\/globals\.css["'];?\s*$/.test(l)
|
|
183
|
+
);
|
|
184
|
+
const oldGlobals = path.join(project.sourceRoot, "app/globals.css");
|
|
185
|
+
const oldContent = existsSync(oldGlobals)
|
|
186
|
+
? readFileSync(oldGlobals, "utf8")
|
|
187
|
+
: "";
|
|
188
|
+
// 接管条件(r3 🔴 实测:no-tailwind CNA 的 body Arial 直接声明压过
|
|
189
|
+
// html 继承的 --font-inter,只追加不接管=全站 Arial):旧 globals 是
|
|
190
|
+
// Tailwind 入口或 CNA 默认存量(--background/--foreground 签名,含
|
|
191
|
+
// tailwind/no-tailwind 两形态)即替换;不像模板的自定义文件才追加
|
|
192
|
+
const oldIsTw = oldContent.includes('@import "tailwindcss"');
|
|
193
|
+
const oldIsCnaStock =
|
|
194
|
+
oldContent.includes("--background") &&
|
|
195
|
+
oldContent.includes("--foreground");
|
|
196
|
+
if (oldIdx !== -1 && (oldIsTw || oldIsCnaStock)) {
|
|
197
|
+
lines[oldIdx] = 'import "@/styles/globals.css";';
|
|
198
|
+
console.log(
|
|
199
|
+
` ${c.green("add")} ${rel}: replaced "./globals.css" → "@/styles/globals.css" (old file was a stock scaffold stylesheet whose body rules would override BoardUI; merge any custom styles into BoardUI's)`
|
|
200
|
+
);
|
|
201
|
+
} else {
|
|
202
|
+
// 插到最后一个顶层 import 之后;没有 import 则插到文件头
|
|
203
|
+
let at = 0;
|
|
204
|
+
for (let i = 0; i < lines.length; i++)
|
|
205
|
+
if (/^import[\s{"']/.test(lines[i])) at = i + 1;
|
|
206
|
+
lines.splice(at, 0, 'import "@/styles/globals.css";');
|
|
207
|
+
console.log(
|
|
208
|
+
` ${c.green("add")} ${rel}: import "@/styles/globals.css"`
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
content = lines.join("\n");
|
|
212
|
+
}
|
|
213
|
+
stylesWired = true;
|
|
214
|
+
// 字体接线(r2 P4):theme.css 期望 --font-inter/--font-mono-source 由
|
|
215
|
+
// next/font 暴露,模板默认 Geist 不定义它们,注入 Inter + JetBrains Mono
|
|
216
|
+
if (!content.includes("--font-inter")) {
|
|
217
|
+
// 清理模板残留的 Geist 字体(注入 Inter 后纯属浪费带宽,r3 伴随问题)
|
|
218
|
+
const geistImport =
|
|
219
|
+
'import { Geist, Geist_Mono } from "next/font/google";';
|
|
220
|
+
if (content.includes(geistImport) && content.includes("geistSans")) {
|
|
221
|
+
content = content
|
|
222
|
+
.replace(geistImport + "\n", "")
|
|
223
|
+
.replace(/const geistSans = Geist\(\{[\s\S]*?\}\);?\n+/, "")
|
|
224
|
+
.replace(/const geistMono = Geist_Mono\(\{[\s\S]*?\}\);?\n+/, "")
|
|
225
|
+
.replace(/\$\{geistSans\.variable\}\s*/g, "")
|
|
226
|
+
.replace(/\$\{geistMono\.variable\}\s*/g, "");
|
|
227
|
+
console.log(
|
|
228
|
+
` ${c.green("add")} ${rel}: removed leftover Geist fonts (Inter takes over)`
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
const lines = content.split("\n");
|
|
161
232
|
let at = 0;
|
|
162
233
|
for (let i = 0; i < lines.length; i++)
|
|
163
234
|
if (/^import[\s{"']/.test(lines[i])) at = i + 1;
|
|
164
|
-
lines.splice(
|
|
165
|
-
|
|
166
|
-
|
|
235
|
+
lines.splice(
|
|
236
|
+
at,
|
|
237
|
+
0,
|
|
238
|
+
'import { Inter, JetBrains_Mono } from "next/font/google";',
|
|
239
|
+
'const bduiInter = Inter({ subsets: ["latin"], variable: "--font-inter" });',
|
|
240
|
+
'const bduiMono = JetBrains_Mono({ subsets: ["latin"], variable: "--font-mono-source" });'
|
|
241
|
+
);
|
|
242
|
+
let next = lines.join("\n");
|
|
243
|
+
const vars = "${bduiInter.variable} ${bduiMono.variable}";
|
|
244
|
+
const tag = next.match(/<html[^>]*>/);
|
|
245
|
+
if (!tag) {
|
|
246
|
+
fontsHint =
|
|
247
|
+
"could not find <html> in the layout — attach bduiInter/bduiMono variable classNames manually";
|
|
248
|
+
} else if (tag[0].includes("className={`")) {
|
|
249
|
+
next = next.replace("className={`", `className={\`${vars} `);
|
|
250
|
+
} else {
|
|
251
|
+
const cm = tag[0].match(/className="([^"]*)"/);
|
|
252
|
+
if (cm) {
|
|
253
|
+
next = next.replace(
|
|
254
|
+
`className="${cm[1]}"`,
|
|
255
|
+
"className={`" + (cm[1] ? `${cm[1]} ` : "") + vars + "`}"
|
|
256
|
+
);
|
|
257
|
+
} else {
|
|
258
|
+
next = next.replace(
|
|
259
|
+
tag[0],
|
|
260
|
+
tag[0].replace(/>$/, ` className={\`${vars}\`}>`)
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
content = next;
|
|
167
265
|
console.log(
|
|
168
|
-
` ${c.green("add")} ${
|
|
266
|
+
` ${c.green("add")} ${rel}: Inter + JetBrains Mono via next/font (--font-inter / --font-mono-source)`
|
|
169
267
|
);
|
|
170
268
|
}
|
|
269
|
+
writeFileSync(layoutFile, content);
|
|
171
270
|
}
|
|
172
271
|
|
|
173
272
|
// agent 规则(AGENTS.md + .cursor/rules/boardui.mdc)
|
|
@@ -190,13 +289,18 @@ export async function cmdInit(flags) {
|
|
|
190
289
|
throw e;
|
|
191
290
|
}
|
|
192
291
|
}
|
|
292
|
+
// relative 同目录返回 ""/拼接句号曾显示成 "written to .."(盲测 r2 P10)
|
|
293
|
+
const dest = path.relative(cwd, outRoot);
|
|
193
294
|
console.log(
|
|
194
295
|
c.green(
|
|
195
|
-
`\n✓ ${wrote} foundation file(s) written to ${
|
|
296
|
+
`\n✓ ${wrote} foundation file(s) written to ${
|
|
297
|
+
dest === "" || dest === "." ? "the project root" : dest
|
|
298
|
+
}.`
|
|
196
299
|
)
|
|
197
300
|
);
|
|
198
301
|
for (const note of setup.notes) console.log(c.yellow(`\n${note}\n`));
|
|
199
302
|
for (const line of ruleLines) console.log(line);
|
|
303
|
+
if (fontsHint) console.log(c.yellow(` warn fonts: ${fontsHint}`));
|
|
200
304
|
console.log(
|
|
201
305
|
stylesWired
|
|
202
306
|
? ` ${c.dim('Note: BoardUI styles wired into your root layout (import "@/styles/globals.css").')}`
|
package/src/mcp.js
CHANGED
|
@@ -8,6 +8,7 @@ import path from "path";
|
|
|
8
8
|
import { createInterface } from "readline";
|
|
9
9
|
import { z } from "zod";
|
|
10
10
|
import { cmdAdd } from "./add.js";
|
|
11
|
+
import { itemHints } from "./hints.js";
|
|
11
12
|
import { detectProject } from "./project.js";
|
|
12
13
|
import { fetchIndex, fetchItem } from "./registry.js";
|
|
13
14
|
|
|
@@ -184,16 +185,40 @@ async function dispatch(name, args, captured, serverKey) {
|
|
|
184
185
|
`internal dependencies: ${(item.registryDependencies ?? []).join(", ") || "none"}`,
|
|
185
186
|
`files: ${(item.files ?? []).map((f) => f.path).join(", ")}`,
|
|
186
187
|
];
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
188
|
+
// 全量源码输出(盲测 r2 P6:工具描述承诺 every file 却只回第一个)。
|
|
189
|
+
// 超大组件(整页模板)按文件逐个给,累计超限后剩余只列路径——
|
|
190
|
+
// 需要时 agent 可 install_components 落盘后再读文件。
|
|
191
|
+
const SRC_BUDGET = 150_000;
|
|
192
|
+
let used = 0;
|
|
193
|
+
for (const f of item.files ?? []) {
|
|
194
|
+
if (!f.content) continue;
|
|
195
|
+
if (used + f.content.length > SRC_BUDGET) {
|
|
196
|
+
lines.push(
|
|
197
|
+
"",
|
|
198
|
+
`(remaining files omitted for size: ${
|
|
199
|
+
(item.files ?? [])
|
|
200
|
+
.filter(
|
|
201
|
+
(x) =>
|
|
202
|
+
x.content &&
|
|
203
|
+
x !== f &&
|
|
204
|
+
used + x.content?.length > SRC_BUDGET
|
|
205
|
+
)
|
|
206
|
+
.map((x) => x.path)
|
|
207
|
+
.join(", ") || f.path
|
|
208
|
+
} — install_components writes them to disk)`
|
|
209
|
+
);
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
used += f.content.length;
|
|
213
|
+
const lang = (f.path ?? "").endsWith(".ts")
|
|
214
|
+
? "ts"
|
|
215
|
+
: (f.path ?? "").endsWith(".css")
|
|
216
|
+
? "css"
|
|
217
|
+
: "tsx";
|
|
218
|
+
lines.push("", `## Source: ${f.path}`, "```" + lang, f.content, "```");
|
|
196
219
|
}
|
|
220
|
+
// 源码特征提示(内容检测,不写死;官方修好自动消失)
|
|
221
|
+
for (const h of itemHints(item)) lines.push("", `> ${h}`);
|
|
197
222
|
return lines.filter(Boolean).join("\n");
|
|
198
223
|
}
|
|
199
224
|
|
|
@@ -219,6 +244,8 @@ async function dispatch(name, args, captured, serverKey) {
|
|
|
219
244
|
}
|
|
220
245
|
if (!(meta.example || meta.snippets?.length))
|
|
221
246
|
lines.push("", "No usage examples recorded for this item.");
|
|
247
|
+
// 源码特征提示(不写死条目名:特征随官方源码演进,官方修好自动消失)
|
|
248
|
+
for (const h of itemHints(item)) lines.push("", `> ${h}`);
|
|
222
249
|
return lines.filter((x) => x !== undefined).join("\n");
|
|
223
250
|
}
|
|
224
251
|
|
package/src/registry.js
CHANGED
|
@@ -57,6 +57,9 @@ export async function fetchItem(name, key) {
|
|
|
57
57
|
} catch {}
|
|
58
58
|
throw new Error(message);
|
|
59
59
|
}
|
|
60
|
-
|
|
60
|
+
const item = await response.json();
|
|
61
|
+
// pro 通道拿到的条目缺 pro:true(服务端不回显)——补上,落盘输出标 (pro)(盲测 r2 P13)
|
|
62
|
+
item.pro = true;
|
|
63
|
+
return item;
|
|
61
64
|
}
|
|
62
65
|
}
|
package/src/write.js
CHANGED
|
@@ -27,6 +27,7 @@ async function confirmOverwrite(target, flags, state) {
|
|
|
27
27
|
|
|
28
28
|
export async function writeItemFiles(item, outRoot, flags = {}) {
|
|
29
29
|
let wrote = 0;
|
|
30
|
+
let skippedExisting = 0;
|
|
30
31
|
let wroteStyles = false;
|
|
31
32
|
const state = { all: false };
|
|
32
33
|
for (const file of item.files ?? []) {
|
|
@@ -39,6 +40,7 @@ export async function writeItemFiles(item, outRoot, flags = {}) {
|
|
|
39
40
|
}
|
|
40
41
|
const target = path.join(outRoot, rel);
|
|
41
42
|
if (existsSync(target) && !(await confirmOverwrite(target, flags, state))) {
|
|
43
|
+
skippedExisting++;
|
|
42
44
|
console.log(
|
|
43
45
|
` ${pc.yellow("skip")} ${path.relative(process.cwd(), target)} (exists)`
|
|
44
46
|
);
|
|
@@ -52,7 +54,32 @@ export async function writeItemFiles(item, outRoot, flags = {}) {
|
|
|
52
54
|
` ${pc.green("add")} ${path.relative(process.cwd(), target)}${item.pro ? pc.cyan(" (pro)") : ""}`
|
|
53
55
|
);
|
|
54
56
|
}
|
|
57
|
+
// 二进制 assets(base64):Pro 模板的头像/logo 等图片资源(盲测 r2 P1)
|
|
58
|
+
for (const asset of item.assets ?? []) {
|
|
59
|
+
const rel = path.normalize(asset.path ?? "");
|
|
60
|
+
if (rel.startsWith("..") || path.isAbsolute(rel)) {
|
|
61
|
+
console.log(` ${pc.red("skip")} unsafe path in asset: ${asset.path}`);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const target = path.join(outRoot, "public", rel);
|
|
65
|
+
if (existsSync(target) && !(await confirmOverwrite(target, flags, state))) {
|
|
66
|
+
skippedExisting++;
|
|
67
|
+
console.log(
|
|
68
|
+
` ${pc.yellow("skip")} ${path.relative(process.cwd(), target)} (exists)`
|
|
69
|
+
);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
mkdirSync(path.dirname(target), { recursive: true });
|
|
73
|
+
writeFileSync(target, Buffer.from(asset.content ?? "", "base64"));
|
|
74
|
+
wrote++;
|
|
75
|
+
console.log(
|
|
76
|
+
` ${pc.green("add")} ${path.relative(
|
|
77
|
+
process.cwd(),
|
|
78
|
+
target
|
|
79
|
+
)}${item.pro ? pc.cyan(" (pro)") : ""} (asset)`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
55
82
|
writeItemFiles.lastWroteStyles =
|
|
56
83
|
writeItemFiles.lastWroteStyles || wroteStyles;
|
|
57
|
-
return wrote;
|
|
84
|
+
return { wrote, skippedExisting };
|
|
58
85
|
}
|