labull-framework 0.1.23 → 0.1.25
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/dist/magic-block.js +1522 -1340
- package/dist/style.css +1 -1
- package/dist/types/magic-block-localfs.d.ts +99 -2
- package/dist/types/magic-block-manifest.d.ts +10 -3
- package/dist/types/magic-block-real.d.ts +1 -1
- package/package.json +1 -1
|
@@ -64,9 +64,66 @@ export interface WriteFailure {
|
|
|
64
64
|
export interface WriteResult {
|
|
65
65
|
written: number;
|
|
66
66
|
failed: WriteFailure[];
|
|
67
|
-
/** 一共排了几个文件(= `written + failed.length`,进度条的分母)。 */
|
|
67
|
+
/** 一共排了几个文件(= `written + skipped + trusted + failed.length`,进度条的分母)。 */
|
|
68
68
|
total: number;
|
|
69
|
+
/** 读过一眼、确认盘上那份一模一样,因此没写的文件数(`verify` 打开时才有)。 */
|
|
70
|
+
skipped: number;
|
|
71
|
+
/** **账本命中**、一下都没碰的文件数(见 `WriteOptions.trust`)。 */
|
|
72
|
+
trusted: number;
|
|
73
|
+
/**
|
|
74
|
+
* 这一趟之后"哪个路径上是哪一份内容"是**确定**的(写过的 + 读过的 + 账本命中的)。
|
|
75
|
+
* 🔴 调用方把它当**下一趟的账本**存起来(见 `WriteOptions.trust`):这就是"重复导出秒出"的来源。
|
|
76
|
+
* ⚠️ 失败的那些**不在里面**(盘上那份不是我们写的,不能算数)。
|
|
77
|
+
*/
|
|
78
|
+
fingerprints: Map<string, string>;
|
|
69
79
|
}
|
|
80
|
+
/** `writePackage` 的开关。 */
|
|
81
|
+
export interface WriteOptions {
|
|
82
|
+
/**
|
|
83
|
+
* 写之前**先读一眼**:盘上那份与要写的一模一样就**不写**(`skipped` 计数)。
|
|
84
|
+
*
|
|
85
|
+
* 🔴 这是"重复导出"那个闭环里最划算的一件事,而且**不改任何语义**:正文一字不差时
|
|
86
|
+
* "写"与"不写"落盘的字节完全相同(只是 mtime 不动)。实测(真实开发包 136 个文件):
|
|
87
|
+
* 全写 205 ms → 全跳过 53 ms;改 3 个文件后再导 70 ms。
|
|
88
|
+
* 🔴 **为什么值得**:`createWritable()` 不是"就地改几个字节"——浏览器要**另写一份临时文件再原子改名**,
|
|
89
|
+
* 所以一次写的代价远大于一次读。一个块的代码改一行,没必要把另外 135 个文件重写一遍。
|
|
90
|
+
* 🔴 **代价**:多一次 `getFileHandle`(+ 命中时一次 `getFile().text()`)。读不到(没这个文件)
|
|
91
|
+
* 就退回"照写",所以**空目录上开着它也不亏**(实测 68 ms → 62 ms,在噪声里)。
|
|
92
|
+
* ⚠️ 读得到、比不了(不是文本 / 权限不对)也算"不比" ⇒ 照写:那种情况本来就该由写那一步报错。
|
|
93
|
+
* ⚠️ 刚 `clearChildren()` 过的目录可以传 `false`:那里**一个文件都不存在**,读那一趟纯属白问。
|
|
94
|
+
*/
|
|
95
|
+
verify?: boolean;
|
|
96
|
+
/**
|
|
97
|
+
* **账本**:`包内相对路径 → 内容指纹`,记的是**上一趟我们亲手铺在那儿的**那一份
|
|
98
|
+
* (`WriteResult.fingerprints`)。指纹一样 + `existing` 说盘上真有这一份 ⇒ **一下都不碰**。
|
|
99
|
+
*
|
|
100
|
+
* 🔴 为什么还要它(`verify` 已经是"读一眼再决定"了):**问题在"读一眼"这个动作本身**。
|
|
101
|
+
* 真实开发包 136 个文件,每份要 `getFileHandle` + `getFile` + `text()` 三次往返 ——
|
|
102
|
+
* 文件多起来之后(15 个块)这一趟就是几秒。而"上一趟铺的就是这一份"这件事我们**自己知道**,
|
|
103
|
+
* 不必每次去问盘。实测(用户真实项目 13 个块 / 136 个文件):93 个块-文件那一档
|
|
104
|
+
* "逐字节核对"要 3 次往返/文件,账本命中是 **0 次**。
|
|
105
|
+
* 🔴 **代价是一条语义上的让步(必须让用户知道)**:账本命中就**不再看盘**,所以
|
|
106
|
+
* ① 用户在自己编辑器里改过、又没读回库的那些文件**会保留他的改动**(不会被盖掉 ——
|
|
107
|
+
* 这通常是好事,但"导出=覆盖"这句话就不再完全成立);
|
|
108
|
+
* ② 因此**必须**同时给 `existing`(`listExisting()` 的返回值):少了它,用户手动删掉的
|
|
109
|
+
* 文件会被我们"跳过"成永远缺失。
|
|
110
|
+
* 🔴 账本只是**加速**,不是判据:丢了、换了目录、刷新了页面 ⇒ 自动退回"读一眼逐字节比对",
|
|
111
|
+
* 结论一模一样,只是慢一次。
|
|
112
|
+
*/
|
|
113
|
+
trust?: ReadonlyMap<string, string>;
|
|
114
|
+
/** `listExisting()` 的结果:这一包里哪些路径**盘上真有**(`trust` 命中时用它确认文件还在)。 */
|
|
115
|
+
existing?: ReadonlySet<string>;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* 正文的**内容指纹**(同一路径上"是不是我们上次写的那一份")。
|
|
119
|
+
*
|
|
120
|
+
* 🔴 **不是安全判据**,所以不必上 SHA-256:这里只回答"和上一趟那一份一样吗",
|
|
121
|
+
* 撞车的后果仅仅是**少写一次**(文件停在我们上一趟写的内容上),不是写错内容。
|
|
122
|
+
* 53 位 + 长度,几百个文件的规模上撞车概率可以忽略。
|
|
123
|
+
* 🔴 **同步**:它在进写盘循环**之前**把整包算完(2 MB 上下 ≈ 20 ms),
|
|
124
|
+
* 换成 `crypto.subtle` 就得把一串 Promise 织进那个并发循环里 —— 不值得。
|
|
125
|
+
*/
|
|
126
|
+
export declare function contentDigest(text: string): string;
|
|
70
127
|
/**
|
|
71
128
|
* 把一整个开发包写进用户选的目录(`{相对路径: 正文}`)。
|
|
72
129
|
*
|
|
@@ -74,11 +131,13 @@ export interface WriteResult {
|
|
|
74
131
|
* 这个函数自己**不删任何东西**(`clearChildren()` 才是删那一步),它只管写。
|
|
75
132
|
* 🔴 **并发写 + 目录句柄缓存**:原来是一个文件一个文件地等、每个文件都从根重新走一遍目录,
|
|
76
133
|
* 几十个文件(含 540 KB 的主题那份)要串行好几趟目录树。
|
|
134
|
+
* ⚠️ 实测(真实开发包 136 个文件)并发 8 / 16 / 32 **一样快**:浏览器那一侧本来就是一件件做的,
|
|
135
|
+
* 把口子开更大不解决问题 —— 真正省时间的是"**不写**"(见 `WriteOptions.verify`)。
|
|
77
136
|
* 🔴 **一个文件写失败不许把整批吞掉**:失败收进 `failed` 回给调用方(这一层的判据与后端
|
|
78
137
|
* `export.write_dir` 一致 —— "一个块失败只报它自己"),调用方把它们显示出来。
|
|
79
138
|
* 原来是一抛全停:排在前面的文件已经落盘了,用户却只看到一句"失败",不知道铺了多少。
|
|
80
139
|
*/
|
|
81
|
-
export declare function writePackage(dir: LocalDir, files: Record<string, string>, onProgress?: (done: number, total: number) => void): Promise<WriteResult>;
|
|
140
|
+
export declare function writePackage(dir: LocalDir, files: Record<string, string>, onProgress?: (done: number, total: number) => void, options?: WriteOptions): Promise<WriteResult>;
|
|
82
141
|
/** 选中目录里都有什么(顶层项 + 递归数出来的文件 / 目录数)—— 导出前"要不要清空"那个确认框用。 */
|
|
83
142
|
export interface DirInventory {
|
|
84
143
|
/** 顶层项(名字 + 是不是目录)。 */
|
|
@@ -114,6 +173,44 @@ export declare function inspectDirectory(dir: LocalDir, rules: ScanRules): Promi
|
|
|
114
173
|
* ⚠️ 一个删不掉就照原样抛(**与串行一样**):已经删掉的那些留在那儿,"删了几项"由调用方自己看到。
|
|
115
174
|
*/
|
|
116
175
|
export declare function clearChildren(dir: LocalDir): Promise<number>;
|
|
176
|
+
/**
|
|
177
|
+
* **只删"这一包不要的东西"**(导出时那个「清空」用的扫帚)。
|
|
178
|
+
*
|
|
179
|
+
* 🔴 它与 `clearChildren()` 的**结果一样**:铺完之后目录里正好只剩这一包。但过程省得多 ——
|
|
180
|
+
* 包里那些**盘上已有**的文件原地留着(读一眼、一样就不动,见 `WriteOptions.verify`),
|
|
181
|
+
* 只删包里**没有**的东西(上一趟留下的垃圾、用户自己放进去的文件)。
|
|
182
|
+
* 🔴 **为什么值得**:`createWritable()` 不是就地改几个字节(浏览器要另写一份临时文件再原子改名),
|
|
183
|
+
* 所以"全删再全铺"里的每一次写都是白花;实测(真实开发包 136 个文件):
|
|
184
|
+
* 全删重铺 228 ms → 只删多余 + 复用没变的 79 ms。
|
|
185
|
+
* 🔴 **只删 `dir` 里面的东西**(与 `clearChildren` 同一条边界),而"要留哪些"由**这一包自己**
|
|
186
|
+
* 算出来(`files` 的键逐级拼成集合)—— 不是从盘上读到什么再拼一次路径,所以"删到别处去"
|
|
187
|
+
* 在实现上就不可能。
|
|
188
|
+
* 🔴 **形状冲突也顺手清掉**:盘上那个位置是**目录**而包里要的是文件(或反过来)⇒ 它不在
|
|
189
|
+
* "这一包要的文件 / 目录"集合里,于是被删掉,紧接着由写那一步按正确的形状建出来。
|
|
190
|
+
* (这是"全删再铺"顺带保证的一件事,这里必须显式保住:否则同名目录会让那一次写直接失败。)
|
|
191
|
+
*
|
|
192
|
+
* :return: `{ removed, existing }` —— 删掉的**条目数**(每层各算一个;被整棵删掉的目录算它自己
|
|
193
|
+
* 那一个,与 `clearChildren` 报的"几项"同一个口径)+ 这一包里**盘上真有**的那些路径
|
|
194
|
+
* (顺手数出来的,`WriteOptions.existing` 直接用,省掉再多走一遍目录树)。
|
|
195
|
+
*/
|
|
196
|
+
export declare function pruneTo(dir: LocalDir, files: Record<string, string>): Promise<PruneResult>;
|
|
197
|
+
/** `pruneTo()` 的结果。 */
|
|
198
|
+
export interface PruneResult {
|
|
199
|
+
/** 删掉的条目数(每层各算一个)。 */
|
|
200
|
+
removed: number;
|
|
201
|
+
/** 这一包里**盘上真有**的那些文件路径(顺手数出来的)。 */
|
|
202
|
+
existing: Set<string>;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* 这一包里**哪些路径盘上真有**(只读:一个字节都不改)。
|
|
206
|
+
*
|
|
207
|
+
* 🔴 它是"账本快路"的**存在性判据**(`WriteOptions.trust` + `existing`):账本只说"我们上一趟
|
|
208
|
+
* 把这一份铺在那儿了",而"现在还在不在"只有盘知道 —— 少了这一步,用户手动删掉的文件会被
|
|
209
|
+
* 我们一直跳过(永远缺失)。
|
|
210
|
+
* 🔴 **只走这一包要的那些目录**(不是整棵树):一个块文件夹有 5-10 个文件但只有 2-3 层目录,
|
|
211
|
+
* 于是"问清 136 个文件在不在"只要几十次目录遍历,而不是 136×3 次文件往返。
|
|
212
|
+
*/
|
|
213
|
+
export declare function listExisting(dir: LocalDir, files: Record<string, string>): Promise<Set<string>>;
|
|
117
214
|
/**
|
|
118
215
|
* 选中的目录**本身**就是代码那一层时当场说清(`workspace/workspace/…` 是没人想要的)。
|
|
119
216
|
*
|
|
@@ -2,8 +2,15 @@
|
|
|
2
2
|
export declare const BLOCK_ENTRY = "index.open.html";
|
|
3
3
|
/** 块包清单(必备件)。与后端 `sdk.BLOCK_MANIFEST` 逐字一致。 */
|
|
4
4
|
export declare const BLOCK_MANIFEST = "magic-block.open.yaml";
|
|
5
|
-
/**
|
|
6
|
-
export declare const MANIFEST_KEYS: readonly ["id", "module", "name", "brief"];
|
|
5
|
+
/** 清单认得的键(顺序就是文档里的顺序)。🔴 `detail` 是**可选**的第五个键。 */
|
|
6
|
+
export declare const MANIFEST_KEYS: readonly ["id", "module", "name", "brief", "detail"];
|
|
7
|
+
/** 必须写全的那四个键(= `MANIFEST_KEYS` 去掉可选的 `detail`)。
|
|
8
|
+
*
|
|
9
|
+
* 🔴 为什么 `detail` **不进必填这一档**:它是给人读的详情介绍(一段话,比 `brief` 那句长),
|
|
10
|
+
* 存量块的清单里一个都没有 —— 判"缺少 detail"等于让库里每一个块在保存 / 体检时集体判不合格。
|
|
11
|
+
* 🔴 认得的键与必填的键**必须分开两处**:合成一处的话,报文会把可选键说成必填。
|
|
12
|
+
* 与后端 `validate.MANIFEST_REQUIRED` 逐字对齐。 */
|
|
13
|
+
export declare const MANIFEST_REQUIRED: readonly ["id", "module", "name", "brief"];
|
|
7
14
|
export type ManifestKey = (typeof MANIFEST_KEYS)[number];
|
|
8
15
|
/** 可读名的字符上限(与后端 `NAME_MAX` 同一份)。 */
|
|
9
16
|
export declare const NAME_MAX = 40;
|
|
@@ -15,7 +22,7 @@ export declare function validName(value: string): boolean;
|
|
|
15
22
|
export type Manifest = Partial<Record<ManifestKey, string>>;
|
|
16
23
|
/** 一个块包的校验结果。`files` 是提交时**原样回给后端**的那一份,这里只读不写。 */
|
|
17
24
|
export interface BlockValidation {
|
|
18
|
-
/**
|
|
25
|
+
/** 清单里读出来的键(没读全就是空对象)。 */
|
|
19
26
|
manifest: Manifest;
|
|
20
27
|
/** 逐条问题,中文、可直接显示给用户。 */
|
|
21
28
|
issues: string[];
|
|
@@ -3,6 +3,6 @@ export { default as MagicBlockConsole } from './magic-block-MagicBlockConsole.vu
|
|
|
3
3
|
export { loadBlockRender, onBlockRenderRefresh, refreshBlockRender, type BlockRender } from './magic-block-render';
|
|
4
4
|
export { BLOCK_BACKEND_FILE, BLOCK_ENTRY, buildPreview } from './magic-block-preview';
|
|
5
5
|
export { CLIENT_SOURCE, installBridge, type BridgeDeps } from './magic-block-bridge';
|
|
6
|
-
export { BLOCK_MANIFEST, MANIFEST_KEYS, NAME_MAX, checkSelection, parseManifest, validFileName, validName, validateBlock, type BlockValidation, type Manifest, type ManifestKey, type SelectionCheck, } from './magic-block-manifest';
|
|
6
|
+
export { BLOCK_MANIFEST, MANIFEST_KEYS, MANIFEST_REQUIRED, NAME_MAX, checkSelection, parseManifest, validFileName, validName, validateBlock, type BlockValidation, type Manifest, type ManifestKey, type SelectionCheck, } from './magic-block-manifest';
|
|
7
7
|
export { API_BASE } from './auth-real';
|
|
8
8
|
export { declaredMissing, declaredUuidOf, declaredUuidsOf, type DeclaredEntry, type DeclaredRoute, } from './magic-block-declared';
|