@ucdjs/lockfile 0.1.1-beta.7 → 0.1.1-beta.9

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 CHANGED
@@ -17,7 +17,7 @@ import { NodeFileSystemBridge } from "@ucdjs/fs-bridge";
17
17
  import { getLockfilePath, readLockfile, writeLockfile } from "@ucdjs/lockfile";
18
18
 
19
19
  const fs = NodeFileSystemBridge({ basePath: "./store" });
20
- const lockfilePath = getLockfilePath("./store");
20
+ const lockfilePath = getLockfilePath();
21
21
 
22
22
  // Read lockfile
23
23
  const lockfile = await readLockfile(fs, lockfilePath);
@@ -35,19 +35,37 @@ await writeLockfile(fs, lockfilePath, {
35
35
  });
36
36
  ```
37
37
 
38
+ ### Parsing Lockfiles Without a Filesystem Bridge
39
+
40
+ When you already have the lockfile content as a string (e.g., fetched from HTTP, read from a KV store, etc.), you can parse and validate it directly without needing a filesystem bridge:
41
+
42
+ ```typescript
43
+ import { parseLockfile, parseLockfileOrUndefined } from "@ucdjs/lockfile";
44
+
45
+ // Parse from a fetch response
46
+ const response = await fetch("https://ucdjs.dev/.ucd-store.lock");
47
+ const content = await response.text();
48
+ const lockfile = parseLockfile(content);
49
+
50
+ // Or use the non-throwing variant
51
+ const lockfileOrUndefined = parseLockfileOrUndefined(content);
52
+ if (lockfileOrUndefined) {
53
+ console.log("Lockfile version:", lockfileOrUndefined.lockfileVersion);
54
+ }
55
+ ```
56
+
38
57
  ### Reading and Writing Snapshots
39
58
 
40
59
  ```typescript
41
- import { getSnapshotPath, readSnapshot, writeSnapshot } from "@ucdjs/lockfile";
60
+ import { getSnapshotPath, parseSnapshot, parseSnapshotOrUndefined, readSnapshot, writeSnapshot } from "@ucdjs/lockfile";
42
61
 
43
- const basePath = "./store";
44
62
  const version = "16.0.0";
45
63
 
46
64
  // Read snapshot
47
- const snapshot = await readSnapshot(fs, basePath, version);
65
+ const snapshot = await readSnapshot(fs, version);
48
66
 
49
67
  // Write snapshot
50
- await writeSnapshot(fs, basePath, version, {
68
+ await writeSnapshot(fs, version, {
51
69
  unicodeVersion: "16.0.0",
52
70
  files: {
53
71
  "UnicodeData.txt": {
@@ -56,6 +74,14 @@ await writeSnapshot(fs, basePath, version, {
56
74
  },
57
75
  },
58
76
  });
77
+
78
+ // Parse snapshot content directly (without a filesystem bridge)
79
+ const response = await fetch("https://ucdjs.dev/16.0.0/snapshot.json");
80
+ const content = await response.text();
81
+ const parsedSnapshot = parseSnapshot(content);
82
+
83
+ // Or use the non-throwing variant
84
+ const parsedSnapshotOrUndefined = parseSnapshotOrUndefined(content);
59
85
  ```
60
86
 
61
87
  ### Computing File Hashes
@@ -68,6 +94,15 @@ const hash = await computeFileHash(content);
68
94
  // Returns: "sha256:..."
69
95
  ```
70
96
 
97
+ ## Overview
98
+
99
+ `@ucdjs/lockfile` manages the canonical persisted state for mirrored local UCD stores. Two artifacts define what's in a local store:
100
+
101
+ - **Lockfile** (`.ucd-store.lock`) - index of all mirrored Unicode versions, with their snapshot paths, file counts, and total sizes.
102
+ - **Snapshots** (`{version}/snapshot.json`) - per-version manifest listing every file, its hash, and size.
103
+
104
+ Together these are the source of truth for a local store. The `parseLockfile()` and `parseSnapshot()` utilities also accept content from remote sources (HTTP, KV stores) with the same shape, but those are read-only compatibility uses - not local store management.
105
+
71
106
  ## API Reference
72
107
 
73
108
  ### Lockfile Operations
@@ -75,27 +110,36 @@ const hash = await computeFileHash(content);
75
110
  - `canUseLockfile(fs: FileSystemBridge): boolean` - Check if bridge supports lockfile operations
76
111
  - `readLockfile(fs: FileSystemBridge, lockfilePath: string): Promise<Lockfile>` - Read and validate lockfile
77
112
  - `writeLockfile(fs: FileSystemBridge, lockfilePath: string, lockfile: Lockfile): Promise<void>` - Write lockfile
78
- - `readLockfileOrDefault(fs: FileSystemBridge, lockfilePath: string): Promise<Lockfile | undefined>` - Read lockfile or return undefined
113
+ - `readLockfileOrUndefined(fs: FileSystemBridge, lockfilePath: string): Promise<Lockfile | undefined>` - Read lockfile or return undefined
114
+ - `parseLockfile(content: string): Lockfile` - Parse and validate lockfile from a raw string
115
+ - `parseLockfileOrUndefined(content: string): Lockfile | undefined` - Parse lockfile from a raw string or return undefined
116
+ - `validateLockfile(data: unknown): ValidateLockfileResult` - Validate lockfile data without reading from filesystem
79
117
 
80
118
  ### Snapshot Operations
81
119
 
82
- - `readSnapshot(fs: FileSystemBridge, basePath: string, version: string): Promise<Snapshot>` - Read and validate snapshot
83
- - `writeSnapshot(fs: FileSystemBridge, basePath: string, version: string, snapshot: Snapshot): Promise<void>` - Write snapshot
84
- - `readSnapshotOrDefault(fs: FileSystemBridge, basePath: string, version: string): Promise<Snapshot | undefined>` - Read snapshot or return undefined
120
+ - `readSnapshot(fs: FileSystemBridge, version: string): Promise<Snapshot>` - Read and validate snapshot
121
+ - `writeSnapshot(fs: FileSystemBridge, version: string, snapshot: Snapshot): Promise<void>` - Write snapshot
122
+ - `readSnapshotOrUndefined(fs: FileSystemBridge, version: string): Promise<Snapshot | undefined>` - Read snapshot or return undefined
123
+ - `parseSnapshot(content: string): Snapshot` - Parse and validate snapshot from a raw string
124
+ - `parseSnapshotOrUndefined(content: string): Snapshot | undefined` - Parse snapshot from a raw string or return undefined
85
125
 
86
126
  ### Path Utilities
87
127
 
88
- - `getLockfilePath(_basePath: string): string` - Get default lockfile path (`.ucd-store.lock`)
89
- - `getSnapshotPath(basePath: string, version: string): string` - Get snapshot path for version
128
+ - `getLockfilePath(): string` - Get default lockfile path (`.ucd-store.lock`)
129
+ - `getSnapshotPath(version: string): string` - Get snapshot path for version
90
130
 
91
131
  ### Hash Utilities
92
132
 
93
133
  - `computeFileHash(content: string | Uint8Array): Promise<string>` - Compute SHA-256 hash
134
+ - `computeFileHashWithoutUCDHeader(content: string): Promise<string>` - Compute SHA-256 hash after stripping the Unicode file header (useful for comparing content across versions)
135
+ - `stripUnicodeHeader(content: string): string` - Strip the Unicode file header (filename, date, copyright lines) from content
136
+
137
+ In snapshot metadata, `fileHash` is always the hash of the exact file bytes. The `hash` field is the semantic comparison hash: text Unicode files strip the Unicode header first, while binary files use the same value as `fileHash`.
94
138
 
95
139
  ### Error Types
96
140
 
141
+ - `LockfileBaseError` - Base error class for all lockfile errors
97
142
  - `LockfileInvalidError` - Thrown when a lockfile or snapshot is invalid
98
- - `LockfileBridgeUnsupportedOperation` - Thrown when a filesystem bridge operation is not supported
99
143
 
100
144
  ## Test Utilities
101
145
 
@@ -1,4 +1,5 @@
1
1
  //#region src/hash.ts
2
+ const HEADER_FILE_VERSION_RE = /\d{1,3}\.\d{1,3}\.\d{1,3}\.txt$/;
2
3
  /**
3
4
  * Checks if a line looks like a Unicode header line.
4
5
  * Header lines typically contain:
@@ -12,7 +13,7 @@
12
13
  */
13
14
  function isHeaderLine(line) {
14
15
  const lower = line.toLowerCase();
15
- return lower.includes("date:") || line.includes("©") || lower.includes("unicode®") || lower.includes("unicode, inc") || /\d{1,3}\.\d{1,3}\.\d{1,3}\.txt$/.test(line);
16
+ return lower.includes("date:") || line.includes("©") || lower.includes("unicode®") || lower.includes("unicode, inc") || HEADER_FILE_VERSION_RE.test(line);
16
17
  }
17
18
  /**
18
19
  * Strips the Unicode file header from content.
@@ -102,6 +103,5 @@ async function computeFileHash(content) {
102
103
  async function computeFileHashWithoutUCDHeader(content) {
103
104
  return computeFileHash(stripUnicodeHeader(content));
104
105
  }
105
-
106
106
  //#endregion
107
- export { computeFileHashWithoutUCDHeader as n, stripUnicodeHeader as r, computeFileHash as t };
107
+ export { computeFileHashWithoutUCDHeader as n, stripUnicodeHeader as r, computeFileHash as t };
package/dist/index.d.mts CHANGED
@@ -1,5 +1,4 @@
1
- import { FileSystemBridge } from "@ucdjs/fs-bridge";
2
- import { Lockfile, LockfileInput, Snapshot } from "@ucdjs/schemas";
1
+ import { BackendEntry, Lockfile, LockfileInput, Snapshot } from "@ucdjs/schemas";
3
2
 
4
3
  //#region src/errors.d.ts
5
4
  /**
@@ -24,15 +23,6 @@ declare class LockfileInvalidError extends LockfileBaseError {
24
23
  details?: string[];
25
24
  });
26
25
  }
27
- /**
28
- * Error thrown when a filesystem bridge operation is not supported
29
- */
30
- declare class LockfileBridgeUnsupportedOperation extends LockfileBaseError {
31
- readonly operation: string;
32
- readonly requiredCapabilities: string[];
33
- readonly availableCapabilities: string[];
34
- constructor(operation: string, requiredCapabilities: string[], availableCapabilities: string[]);
35
- }
36
26
  //#endregion
37
27
  //#region src/hash.d.ts
38
28
  /**
@@ -65,6 +55,170 @@ declare function computeFileHash(content: string | Uint8Array): Promise<string>;
65
55
  */
66
56
  declare function computeFileHashWithoutUCDHeader(content: string): Promise<string>;
67
57
  //#endregion
58
+ //#region ../../node_modules/.pnpm/hookable@6.1.0/node_modules/hookable/dist/index.d.mts
59
+ //#region src/types.d.ts
60
+ type HookCallback = (...arguments_: any) => Promise<void> | void;
61
+ type HookKeys<T> = keyof T & string;
62
+ //#endregion
63
+ //#region src/hookable.d.ts
64
+ type InferCallback<HT, HN extends keyof HT> = HT[HN] extends HookCallback ? HT[HN] : never;
65
+ declare class HookableCore<HooksT extends Record<string, any> = Record<string, HookCallback>, HookNameT extends HookKeys<HooksT> = HookKeys<HooksT>> {
66
+ protected _hooks: {
67
+ [key: string]: HookCallback[] | undefined;
68
+ };
69
+ constructor();
70
+ hook<NameT extends HookNameT>(name: NameT, fn: InferCallback<HooksT, NameT>): () => void;
71
+ removeHook<NameT extends HookNameT>(name: NameT, function_: InferCallback<HooksT, NameT>): void;
72
+ callHook<NameT extends HookNameT>(name: NameT, ...args: Parameters<InferCallback<HooksT, NameT>>): Promise<any> | void;
73
+ } //#endregion
74
+ //#region src/utils.d.ts
75
+ type CreateTask = (name?: string) => {
76
+ run: (function_: () => Promise<any> | any) => Promise<any> | any;
77
+ };
78
+ declare global {
79
+ interface Console {
80
+ createTask?: CreateTask;
81
+ }
82
+ }
83
+ /** @deprecated */
84
+ //#endregion
85
+ //#region ../fs-backend/dist/types-B5lbklgx.d.mts
86
+ //#region src/types.d.ts
87
+ interface ListOptions {
88
+ recursive?: boolean;
89
+ }
90
+ interface RemoveOptions {
91
+ recursive?: boolean;
92
+ force?: boolean;
93
+ }
94
+ interface CopyOptions {
95
+ recursive?: boolean;
96
+ overwrite?: boolean;
97
+ }
98
+ type BackendEntry$1 = BackendEntry;
99
+ interface BackendStat {
100
+ type: BackendEntry$1["type"];
101
+ size: number;
102
+ mtime?: Date;
103
+ }
104
+ interface FileSystemBackendOperations {
105
+ read: (path: string) => Promise<string>;
106
+ readBytes: (path: string) => Promise<Uint8Array>;
107
+ list: (path: string, options?: ListOptions) => Promise<BackendEntry$1[]>;
108
+ /**
109
+ * Best-effort existence check.
110
+ *
111
+ * Backends may collapse "missing" and "could not determine existence" into
112
+ * `false`, especially for remote transports. Use `stat()` when you need
113
+ * error details instead of a lossy boolean.
114
+ */
115
+ exists: (path: string) => Promise<boolean>;
116
+ stat: (path: string) => Promise<BackendStat>;
117
+ }
118
+ interface FileSystemBackendMutableOperations {
119
+ write?: (path: string, data: string | Uint8Array) => Promise<void>;
120
+ mkdir?: (path: string) => Promise<void>;
121
+ remove?: (path: string, options?: RemoveOptions) => Promise<void>;
122
+ /**
123
+ * Copy a file or directory to the exact destination path within the same backend.
124
+ *
125
+ * File copies use `destinationPath` as an exact target path by default, but
126
+ * will copy into a destination directory when the destination ends with `/`
127
+ * or already exists as a directory. Directory copies require `recursive: true`.
128
+ */
129
+ copy?: (sourcePath: string, destinationPath: string, options?: CopyOptions) => Promise<void>;
130
+ }
131
+ interface FileSystemBackendMutableMethods {
132
+ write: (path: string, data: string | Uint8Array) => Promise<void>;
133
+ mkdir: (path: string) => Promise<void>;
134
+ remove: (path: string, options?: RemoveOptions) => Promise<void>;
135
+ copy: (sourcePath: string, destinationPath: string, options?: CopyOptions) => Promise<void>;
136
+ }
137
+ type FileSystemBackendFeature = keyof FileSystemBackendMutableOperations;
138
+ interface FileSystemBackendMeta {
139
+ name: string;
140
+ description?: string;
141
+ }
142
+ interface FileSystemBackend extends FileSystemBackendOperations, FileSystemBackendMutableMethods {
143
+ readonly features: ReadonlySet<FileSystemBackendFeature>;
144
+ readonly meta: FileSystemBackendMeta;
145
+ hook: HookableCore<BackendHooks>["hook"];
146
+ }
147
+ interface BackendErrorHookPayload {
148
+ op: keyof (FileSystemBackendOperations & FileSystemBackendMutableOperations);
149
+ path: string;
150
+ error: Error;
151
+ sourcePath?: string;
152
+ destinationPath?: string;
153
+ }
154
+ interface BackendHooks {
155
+ "error": (payload: BackendErrorHookPayload) => void;
156
+ "read:before": (payload: {
157
+ path: string;
158
+ }) => void;
159
+ "read:after": (payload: {
160
+ path: string;
161
+ content: string;
162
+ }) => void;
163
+ "readBytes:before": (payload: {
164
+ path: string;
165
+ }) => void;
166
+ "readBytes:after": (payload: {
167
+ path: string;
168
+ data: Uint8Array;
169
+ }) => void;
170
+ "list:before": (payload: {
171
+ path: string;
172
+ recursive: boolean;
173
+ }) => void;
174
+ "list:after": (payload: {
175
+ path: string;
176
+ recursive: boolean;
177
+ entries: BackendEntry$1[];
178
+ }) => void;
179
+ "exists:before": (payload: {
180
+ path: string;
181
+ }) => void;
182
+ "exists:after": (payload: {
183
+ path: string;
184
+ result: boolean;
185
+ }) => void;
186
+ "stat:before": (payload: {
187
+ path: string;
188
+ }) => void;
189
+ "stat:after": (payload: {
190
+ path: string;
191
+ stat: BackendStat;
192
+ }) => void;
193
+ "write:before": (payload: {
194
+ path: string;
195
+ data: string | Uint8Array;
196
+ }) => void;
197
+ "write:after": (payload: {
198
+ path: string;
199
+ }) => void;
200
+ "mkdir:before": (payload: {
201
+ path: string;
202
+ }) => void;
203
+ "mkdir:after": (payload: {
204
+ path: string;
205
+ }) => void;
206
+ "remove:before": (payload: {
207
+ path: string;
208
+ } & RemoveOptions) => void;
209
+ "remove:after": (payload: {
210
+ path: string;
211
+ } & RemoveOptions) => void;
212
+ "copy:before": (payload: {
213
+ sourcePath: string;
214
+ destinationPath: string;
215
+ } & CopyOptions) => void;
216
+ "copy:after": (payload: {
217
+ sourcePath: string;
218
+ destinationPath: string;
219
+ } & CopyOptions) => void;
220
+ } //#endregion
221
+ //#endregion
68
222
  //#region src/lockfile.d.ts
69
223
  /**
70
224
  * Result of validating a lockfile
@@ -100,40 +254,73 @@ interface ValidateLockfileResult {
100
254
  */
101
255
  declare function validateLockfile(data: unknown): ValidateLockfileResult;
102
256
  /**
103
- * Checks if the filesystem bridge supports lockfile operations (requires write capability)
257
+ * Checks if the filesystem backend supports lockfile operations (requires write & mkdir features)
104
258
  *
105
- * @param {FileSystemBridge} fs - The filesystem bridge to check
106
- * @returns {boolean} True if the bridge supports lockfile operations
259
+ * @param {FileSystemBackend} fs - The filesystem backend to check
260
+ * @returns {boolean} True if the filesystem supports lockfile operations
107
261
  */
108
- declare function canUseLockfile(fs: FileSystemBridge): fs is FileSystemBridge & Required<Pick<FileSystemBridge, "write">>;
262
+ declare function canUseLockfile(fs: FileSystemBackend): boolean;
109
263
  /**
110
264
  * Reads and validates a lockfile from the filesystem.
111
265
  *
112
- * @param {FileSystemBridge} fs - Filesystem bridge to use for reading
266
+ * @param {FileSystemBackend} fs - Filesystem backend to use for reading
113
267
  * @param {string} lockfilePath - Path to the lockfile
114
268
  * @returns {Promise<Lockfile>} A promise that resolves to the validated lockfile
115
269
  * @throws {LockfileInvalidError} When the lockfile is invalid or missing
116
270
  */
117
- declare function readLockfile(fs: FileSystemBridge, lockfilePath: string): Promise<Lockfile>;
271
+ declare function readLockfile(fs: FileSystemBackend, lockfilePath: string): Promise<Lockfile>;
118
272
  /**
119
273
  * Writes a lockfile to the filesystem.
120
- * If the filesystem bridge does not support write operations, the function
274
+ * If the filesystem backend does not support write operations, the function
121
275
  * will skip writing the lockfile and return without throwing.
122
276
  *
123
- * @param {FileSystemBridge} fs - Filesystem bridge to use for writing
277
+ * @param {FileSystemBackend} fs - Filesystem backend to use for writing
124
278
  * @param {string} lockfilePath - Path where the lockfile should be written
125
279
  * @param {LockfileInput} lockfile - The lockfile data to write
126
280
  * @returns {Promise<void>} A promise that resolves when the lockfile has been written
127
281
  */
128
- declare function writeLockfile(fs: FileSystemBridge, lockfilePath: string, lockfile: LockfileInput): Promise<void>;
282
+ declare function writeLockfile(fs: FileSystemBackend, lockfilePath: string, lockfile: LockfileInput): Promise<void>;
129
283
  /**
130
284
  * Reads a lockfile or returns undefined if it doesn't exist or is invalid.
131
285
  *
132
- * @param {FileSystemBridge} fs - Filesystem bridge to use for reading
286
+ * @param {FileSystemBackend} fs - Filesystem backend to use for reading
133
287
  * @param {string} lockfilePath - Path to the lockfile
134
288
  * @returns {Promise<Lockfile | undefined>} A promise that resolves to the lockfile or undefined
135
289
  */
136
- declare function readLockfileOrUndefined(fs: FileSystemBridge, lockfilePath: string): Promise<Lockfile | undefined>;
290
+ declare function readLockfileOrUndefined(fs: FileSystemBackend, lockfilePath: string): Promise<Lockfile | undefined>;
291
+ /**
292
+ * Parses and validates lockfile content from a raw string without requiring a filesystem bridge.
293
+ * Useful when lockfile content has been obtained from any source (HTTP, KV store, memory, etc.).
294
+ *
295
+ * @param {string} content - The raw lockfile content to parse
296
+ * @returns {Lockfile} The validated lockfile
297
+ * @throws {LockfileInvalidError} When the content is invalid or does not match the expected schema
298
+ *
299
+ * @example
300
+ * ```ts
301
+ * const response = await fetch("https://ucdjs.dev/.ucd-store.lock");
302
+ * const content = await response.text();
303
+ * const lockfile = parseLockfile(content);
304
+ * ```
305
+ */
306
+ declare function parseLockfile(content: string): Lockfile;
307
+ /**
308
+ * Parses and validates lockfile content from a raw string, returning undefined if parsing fails.
309
+ *
310
+ * @param {string} content - The raw lockfile content to parse
311
+ * @returns {Lockfile | undefined} The validated lockfile or undefined if parsing fails
312
+ *
313
+ * @example
314
+ * ```ts
315
+ * const response = await fetch("https://ucdjs.dev/.ucd-store.lock");
316
+ * const content = await response.text();
317
+ * const lockfile = parseLockfileOrUndefined(content);
318
+ * if (lockfile) {
319
+ * console.log("Lockfile version:", lockfile.lockfileVersion);
320
+ * }
321
+ * ```
322
+ */
323
+ declare function parseLockfileOrUndefined(content: string): Lockfile | undefined;
137
324
  //#endregion
138
325
  //#region src/paths.d.ts
139
326
  /**
@@ -156,30 +343,62 @@ declare function getSnapshotPath(version: string): string;
156
343
  /**
157
344
  * Reads and validates a snapshot for a specific version.
158
345
  *
159
- * @param {FileSystemBridge} fs - Filesystem bridge to use for reading
346
+ * @param {FileSystemBackend} fs - Filesystem backend to use for reading
160
347
  * @param {string} version - The Unicode version
161
348
  * @returns {Promise<Snapshot>} A promise that resolves to the validated snapshot
162
349
  * @throws {LockfileInvalidError} When the snapshot is invalid or missing
163
350
  */
164
- declare function readSnapshot(fs: FileSystemBridge, version: string): Promise<Snapshot>;
351
+ declare function readSnapshot(fs: FileSystemBackend, version: string): Promise<Snapshot>;
165
352
  /**
166
353
  * Writes a snapshot for a specific version to the filesystem.
167
- * Only works if the filesystem bridge supports write operations.
354
+ * Only works if the filesystem backend supports write operations.
168
355
  *
169
- * @param {FileSystemBridge} fs - Filesystem bridge to use for writing
356
+ * @param {FileSystemBackend} fs - Filesystem backend to use for writing
170
357
  * @param {string} version - The Unicode version
171
358
  * @param {Snapshot} snapshot - The snapshot data to write
172
359
  * @returns {Promise<void>} A promise that resolves when the snapshot has been written
173
- * @throws {LockfileBridgeUnsupportedOperation} When directory doesn't exist and mkdir is not available
174
360
  */
175
- declare function writeSnapshot(fs: FileSystemBridge, version: string, snapshot: Snapshot): Promise<void>;
361
+ declare function writeSnapshot(fs: FileSystemBackend, version: string, snapshot: Snapshot): Promise<void>;
176
362
  /**
177
363
  * Reads a snapshot or returns undefined if it doesn't exist or is invalid.
178
364
  *
179
- * @param {FileSystemBridge} fs - Filesystem bridge to use for reading
365
+ * @param {FileSystemBackend} fs - Filesystem backend to use for reading
180
366
  * @param {string} version - The Unicode version
181
367
  * @returns {Promise<Snapshot | undefined>} A promise that resolves to the snapshot or undefined
182
368
  */
183
- declare function readSnapshotOrUndefined(fs: FileSystemBridge, version: string): Promise<Snapshot | undefined>;
369
+ declare function readSnapshotOrUndefined(fs: FileSystemBackend, version: string): Promise<Snapshot | undefined>;
370
+ /**
371
+ * Parses and validates snapshot content from a raw string without requiring a filesystem bridge.
372
+ * Useful when snapshot content has been obtained from any source (HTTP, KV store, memory, etc.).
373
+ *
374
+ * @param {string} content - The raw snapshot content to parse
375
+ * @returns {Snapshot} The validated snapshot
376
+ * @throws {LockfileInvalidError} When the content is invalid or does not match the expected schema
377
+ *
378
+ * @example
379
+ * ```ts
380
+ * const response = await fetch("https://ucdjs.dev/16.0.0/snapshot.json");
381
+ * const content = await response.text();
382
+ * const snapshot = parseSnapshot(content);
383
+ * ```
384
+ */
385
+ declare function parseSnapshot(content: string): Snapshot;
386
+ /**
387
+ * Parses and validates snapshot content from a raw string, returning undefined if parsing fails.
388
+ *
389
+ * @param {string} content - The raw snapshot content to parse
390
+ * @returns {Snapshot | undefined} The validated snapshot or undefined if parsing fails
391
+ *
392
+ * @example
393
+ * ```ts
394
+ * const response = await fetch("https://ucdjs.dev/16.0.0/snapshot.json");
395
+ * const content = await response.text();
396
+ * const snapshot = parseSnapshotOrUndefined(content);
397
+ * if (snapshot) {
398
+ * console.log("Unicode version:", snapshot.unicodeVersion);
399
+ * }
400
+ * ```
401
+ */
402
+ declare function parseSnapshotOrUndefined(content: string): Snapshot | undefined;
184
403
  //#endregion
185
- export { LockfileBaseError, LockfileBridgeUnsupportedOperation, LockfileInvalidError, type ValidateLockfileResult, canUseLockfile, computeFileHash, computeFileHashWithoutUCDHeader, getLockfilePath, getSnapshotPath, readLockfile, readLockfileOrUndefined, readSnapshot, readSnapshotOrUndefined, stripUnicodeHeader, validateLockfile, writeLockfile, writeSnapshot };
404
+ export { LockfileBaseError, LockfileInvalidError, type ValidateLockfileResult, canUseLockfile, computeFileHash, computeFileHashWithoutUCDHeader, getLockfilePath, getSnapshotPath, parseLockfile, parseLockfileOrUndefined, parseSnapshot, parseSnapshotOrUndefined, readLockfile, readLockfileOrUndefined, readSnapshot, readSnapshotOrUndefined, stripUnicodeHeader, validateLockfile, writeLockfile, writeSnapshot };
package/dist/index.mjs CHANGED
@@ -1,9 +1,7 @@
1
- import { n as computeFileHashWithoutUCDHeader, r as stripUnicodeHeader, t as computeFileHash } from "./hash-DYmMzCbf.mjs";
2
- import { createDebugger, safeJsonParse, tryOr } from "@ucdjs-internal/shared";
3
- import { hasCapability } from "@ucdjs/fs-bridge";
1
+ import { n as computeFileHashWithoutUCDHeader, r as stripUnicodeHeader, t as computeFileHash } from "./hash-Bf5WIJe6.mjs";
2
+ import { createDebugger, safeJsonParse } from "@ucdjs-internal/shared";
4
3
  import { LockfileSchema, SnapshotSchema } from "@ucdjs/schemas";
5
4
  import { dirname, join } from "pathe";
6
-
7
5
  //#region src/errors.ts
8
6
  /**
9
7
  * Base error class for lockfile-related errors
@@ -29,25 +27,6 @@ var LockfileInvalidError = class LockfileInvalidError extends LockfileBaseError
29
27
  Object.setPrototypeOf(this, LockfileInvalidError.prototype);
30
28
  }
31
29
  };
32
- /**
33
- * Error thrown when a filesystem bridge operation is not supported
34
- */
35
- var LockfileBridgeUnsupportedOperation = class LockfileBridgeUnsupportedOperation extends LockfileBaseError {
36
- operation;
37
- requiredCapabilities;
38
- availableCapabilities;
39
- constructor(operation, requiredCapabilities, availableCapabilities) {
40
- let message = `Operation "${operation}" is not supported.`;
41
- if (requiredCapabilities.length > 0 || availableCapabilities.length > 0) message += ` Required capabilities: ${requiredCapabilities.join(", ")}. Available capabilities: ${availableCapabilities.join(", ")}`;
42
- super(message);
43
- this.name = "LockfileBridgeUnsupportedOperation";
44
- this.operation = operation;
45
- this.requiredCapabilities = requiredCapabilities;
46
- this.availableCapabilities = availableCapabilities;
47
- Object.setPrototypeOf(this, LockfileBridgeUnsupportedOperation.prototype);
48
- }
49
- };
50
-
51
30
  //#endregion
52
31
  //#region src/lockfile.ts
53
32
  const debug$1 = createDebugger("ucdjs:lockfile");
@@ -84,40 +63,21 @@ function validateLockfile(data) {
84
63
  };
85
64
  }
86
65
  /**
87
- * Checks if the filesystem bridge supports lockfile operations (requires write capability)
66
+ * Checks if the filesystem backend supports lockfile operations (requires write & mkdir features)
88
67
  *
89
- * @param {FileSystemBridge} fs - The filesystem bridge to check
90
- * @returns {boolean} True if the bridge supports lockfile operations
68
+ * @param {FileSystemBackend} fs - The filesystem backend to check
69
+ * @returns {boolean} True if the filesystem supports lockfile operations
91
70
  */
92
71
  function canUseLockfile(fs) {
93
- return hasCapability(fs, "write");
72
+ return fs.features.has("write") && fs.features.has("mkdir");
94
73
  }
95
74
  /**
96
- * Reads and validates a lockfile from the filesystem.
97
- *
98
- * @param {FileSystemBridge} fs - Filesystem bridge to use for reading
99
- * @param {string} lockfilePath - Path to the lockfile
100
- * @returns {Promise<Lockfile>} A promise that resolves to the validated lockfile
101
- * @throws {LockfileInvalidError} When the lockfile is invalid or missing
75
+ * Internal helper: parses and validates raw JSON content against the LockfileSchema.
76
+ * Throws LockfileInvalidError with the given `lockfilePath` context on failure.
102
77
  */
103
- async function readLockfile(fs, lockfilePath) {
104
- debug$1?.("Reading lockfile from:", lockfilePath);
105
- const lockfileData = await tryOr({
106
- try: fs.read(lockfilePath),
107
- err: (err) => {
108
- debug$1?.("Failed to read lockfile:", err);
109
- throw new LockfileInvalidError({
110
- lockfilePath,
111
- message: "lockfile could not be read"
112
- });
113
- }
114
- });
115
- if (!lockfileData) throw new LockfileInvalidError({
116
- lockfilePath,
117
- message: "lockfile is empty"
118
- });
119
- const jsonData = safeJsonParse(lockfileData);
120
- if (!jsonData) throw new LockfileInvalidError({
78
+ function parseLockfileFromContent(content, lockfilePath) {
79
+ const jsonData = safeJsonParse(content);
80
+ if (jsonData === null) throw new LockfileInvalidError({
121
81
  lockfilePath,
122
82
  message: "lockfile is not valid JSON"
123
83
  });
@@ -130,32 +90,64 @@ async function readLockfile(fs, lockfilePath) {
130
90
  details: parsedLockfile.error.issues.map((issue) => issue.message)
131
91
  });
132
92
  }
133
- debug$1?.("Successfully read lockfile");
134
93
  return parsedLockfile.data;
135
94
  }
136
95
  /**
96
+ * Reads and validates a lockfile from the filesystem.
97
+ *
98
+ * @param {FileSystemBackend} fs - Filesystem backend to use for reading
99
+ * @param {string} lockfilePath - Path to the lockfile
100
+ * @returns {Promise<Lockfile>} A promise that resolves to the validated lockfile
101
+ * @throws {LockfileInvalidError} When the lockfile is invalid or missing
102
+ */
103
+ async function readLockfile(fs, lockfilePath) {
104
+ debug$1?.("Reading lockfile from:", lockfilePath);
105
+ let lockfileData;
106
+ try {
107
+ lockfileData = await fs.read(lockfilePath);
108
+ } catch (err) {
109
+ debug$1?.("Failed to read lockfile:", err);
110
+ throw new LockfileInvalidError({
111
+ lockfilePath,
112
+ message: "lockfile could not be read"
113
+ });
114
+ }
115
+ if (!lockfileData) throw new LockfileInvalidError({
116
+ lockfilePath,
117
+ message: "lockfile is empty"
118
+ });
119
+ debug$1?.("Successfully read lockfile");
120
+ return parseLockfileFromContent(lockfileData, lockfilePath);
121
+ }
122
+ /**
137
123
  * Writes a lockfile to the filesystem.
138
- * If the filesystem bridge does not support write operations, the function
124
+ * If the filesystem backend does not support write operations, the function
139
125
  * will skip writing the lockfile and return without throwing.
140
126
  *
141
- * @param {FileSystemBridge} fs - Filesystem bridge to use for writing
127
+ * @param {FileSystemBackend} fs - Filesystem backend to use for writing
142
128
  * @param {string} lockfilePath - Path where the lockfile should be written
143
129
  * @param {LockfileInput} lockfile - The lockfile data to write
144
130
  * @returns {Promise<void>} A promise that resolves when the lockfile has been written
145
131
  */
146
132
  async function writeLockfile(fs, lockfilePath, lockfile) {
147
133
  if (!canUseLockfile(fs)) {
148
- debug$1?.("Filesystem bridge does not support write operations, skipping lockfile write");
134
+ debug$1?.("Filesystem does not support write operations, skipping lockfile write");
149
135
  return;
150
136
  }
151
137
  debug$1?.("Writing lockfile to:", lockfilePath);
138
+ const lockfileDir = dirname(lockfilePath);
139
+ debug$1?.("Writing lockfile to:", lockfilePath);
140
+ if (!await fs.exists(lockfileDir)) {
141
+ debug$1?.("Creating lockfile directory:", lockfileDir);
142
+ await fs.mkdir(lockfileDir);
143
+ }
152
144
  await fs.write(lockfilePath, JSON.stringify(lockfile, null, 2));
153
145
  debug$1?.("Successfully wrote lockfile");
154
146
  }
155
147
  /**
156
148
  * Reads a lockfile or returns undefined if it doesn't exist or is invalid.
157
149
  *
158
- * @param {FileSystemBridge} fs - Filesystem bridge to use for reading
150
+ * @param {FileSystemBackend} fs - Filesystem backend to use for reading
159
151
  * @param {string} lockfilePath - Path to the lockfile
160
152
  * @returns {Promise<Lockfile | undefined>} A promise that resolves to the lockfile or undefined
161
153
  */
@@ -164,7 +156,54 @@ async function readLockfileOrUndefined(fs, lockfilePath) {
164
156
  debug$1?.("Failed to read lockfile, returning undefined");
165
157
  });
166
158
  }
167
-
159
+ /**
160
+ * Parses and validates lockfile content from a raw string without requiring a filesystem bridge.
161
+ * Useful when lockfile content has been obtained from any source (HTTP, KV store, memory, etc.).
162
+ *
163
+ * @param {string} content - The raw lockfile content to parse
164
+ * @returns {Lockfile} The validated lockfile
165
+ * @throws {LockfileInvalidError} When the content is invalid or does not match the expected schema
166
+ *
167
+ * @example
168
+ * ```ts
169
+ * const response = await fetch("https://ucdjs.dev/.ucd-store.lock");
170
+ * const content = await response.text();
171
+ * const lockfile = parseLockfile(content);
172
+ * ```
173
+ */
174
+ function parseLockfile(content) {
175
+ debug$1?.("Parsing lockfile from content");
176
+ if (!content) throw new LockfileInvalidError({
177
+ lockfilePath: "<content>",
178
+ message: "lockfile is empty"
179
+ });
180
+ debug$1?.("Successfully parsed lockfile");
181
+ return parseLockfileFromContent(content, "<content>");
182
+ }
183
+ /**
184
+ * Parses and validates lockfile content from a raw string, returning undefined if parsing fails.
185
+ *
186
+ * @param {string} content - The raw lockfile content to parse
187
+ * @returns {Lockfile | undefined} The validated lockfile or undefined if parsing fails
188
+ *
189
+ * @example
190
+ * ```ts
191
+ * const response = await fetch("https://ucdjs.dev/.ucd-store.lock");
192
+ * const content = await response.text();
193
+ * const lockfile = parseLockfileOrUndefined(content);
194
+ * if (lockfile) {
195
+ * console.log("Lockfile version:", lockfile.lockfileVersion);
196
+ * }
197
+ * ```
198
+ */
199
+ function parseLockfileOrUndefined(content) {
200
+ try {
201
+ return parseLockfile(content);
202
+ } catch {
203
+ debug$1?.("Failed to parse lockfile, returning undefined");
204
+ return;
205
+ }
206
+ }
168
207
  //#endregion
169
208
  //#region src/paths.ts
170
209
  /**
@@ -186,37 +225,16 @@ function getLockfilePath() {
186
225
  function getSnapshotPath(version) {
187
226
  return join(version, "snapshot.json");
188
227
  }
189
-
190
228
  //#endregion
191
229
  //#region src/snapshot.ts
192
230
  const debug = createDebugger("ucdjs:lockfile:snapshot");
193
231
  /**
194
- * Reads and validates a snapshot for a specific version.
195
- *
196
- * @param {FileSystemBridge} fs - Filesystem bridge to use for reading
197
- * @param {string} version - The Unicode version
198
- * @returns {Promise<Snapshot>} A promise that resolves to the validated snapshot
199
- * @throws {LockfileInvalidError} When the snapshot is invalid or missing
232
+ * Internal helper: parses and validates raw JSON content against the SnapshotSchema.
233
+ * Throws LockfileInvalidError with the given `snapshotPath` context on failure.
200
234
  */
201
- async function readSnapshot(fs, version) {
202
- const snapshotPath = getSnapshotPath(version);
203
- debug?.("Reading snapshot from:", snapshotPath);
204
- const snapshotData = await tryOr({
205
- try: fs.read(snapshotPath),
206
- err: (err) => {
207
- debug?.("Failed to read snapshot:", err);
208
- throw new LockfileInvalidError({
209
- lockfilePath: snapshotPath,
210
- message: "snapshot could not be read"
211
- });
212
- }
213
- });
214
- if (!snapshotData) throw new LockfileInvalidError({
215
- lockfilePath: snapshotPath,
216
- message: "snapshot is empty"
217
- });
218
- const jsonData = safeJsonParse(snapshotData);
219
- if (!jsonData) throw new LockfileInvalidError({
235
+ function parseSnapshotFromContent(content, snapshotPath) {
236
+ const jsonData = safeJsonParse(content);
237
+ if (jsonData === null) throw new LockfileInvalidError({
220
238
  lockfilePath: snapshotPath,
221
239
  message: "snapshot is not valid JSON"
222
240
  });
@@ -229,29 +247,54 @@ async function readSnapshot(fs, version) {
229
247
  details: parsedSnapshot.error.issues.map((issue) => issue.message)
230
248
  });
231
249
  }
232
- debug?.("Successfully read snapshot");
233
250
  return parsedSnapshot.data;
234
251
  }
235
252
  /**
253
+ * Reads and validates a snapshot for a specific version.
254
+ *
255
+ * @param {FileSystemBackend} fs - Filesystem backend to use for reading
256
+ * @param {string} version - The Unicode version
257
+ * @returns {Promise<Snapshot>} A promise that resolves to the validated snapshot
258
+ * @throws {LockfileInvalidError} When the snapshot is invalid or missing
259
+ */
260
+ async function readSnapshot(fs, version) {
261
+ const snapshotPath = getSnapshotPath(version);
262
+ debug?.("Reading snapshot from:", snapshotPath);
263
+ let snapshotData;
264
+ try {
265
+ snapshotData = await fs.read(snapshotPath);
266
+ } catch (err) {
267
+ debug?.("Failed to read snapshot:", err);
268
+ throw new LockfileInvalidError({
269
+ lockfilePath: snapshotPath,
270
+ message: "snapshot could not be read"
271
+ });
272
+ }
273
+ if (!snapshotData) throw new LockfileInvalidError({
274
+ lockfilePath: snapshotPath,
275
+ message: "snapshot is empty"
276
+ });
277
+ debug?.("Successfully read snapshot");
278
+ return parseSnapshotFromContent(snapshotData, snapshotPath);
279
+ }
280
+ /**
236
281
  * Writes a snapshot for a specific version to the filesystem.
237
- * Only works if the filesystem bridge supports write operations.
282
+ * Only works if the filesystem backend supports write operations.
238
283
  *
239
- * @param {FileSystemBridge} fs - Filesystem bridge to use for writing
284
+ * @param {FileSystemBackend} fs - Filesystem backend to use for writing
240
285
  * @param {string} version - The Unicode version
241
286
  * @param {Snapshot} snapshot - The snapshot data to write
242
287
  * @returns {Promise<void>} A promise that resolves when the snapshot has been written
243
- * @throws {LockfileBridgeUnsupportedOperation} When directory doesn't exist and mkdir is not available
244
288
  */
245
289
  async function writeSnapshot(fs, version, snapshot) {
246
290
  if (!canUseLockfile(fs)) {
247
- debug?.("Filesystem bridge does not support write operations, skipping snapshot write");
291
+ debug?.("Filesystem does not support write operations, skipping snapshot write");
248
292
  return;
249
293
  }
250
294
  const snapshotPath = getSnapshotPath(version);
251
295
  const snapshotDir = dirname(snapshotPath);
252
296
  debug?.("Writing snapshot to:", snapshotPath);
253
297
  if (!await fs.exists(snapshotDir)) {
254
- if (!hasCapability(fs, "mkdir")) throw new LockfileBridgeUnsupportedOperation("writeSnapshot", ["mkdir"], Object.keys(fs.optionalCapabilities).filter((k) => fs.optionalCapabilities[k]));
255
298
  debug?.("Creating snapshot directory:", snapshotDir);
256
299
  await fs.mkdir(snapshotDir);
257
300
  }
@@ -261,7 +304,7 @@ async function writeSnapshot(fs, version, snapshot) {
261
304
  /**
262
305
  * Reads a snapshot or returns undefined if it doesn't exist or is invalid.
263
306
  *
264
- * @param {FileSystemBridge} fs - Filesystem bridge to use for reading
307
+ * @param {FileSystemBackend} fs - Filesystem backend to use for reading
265
308
  * @param {string} version - The Unicode version
266
309
  * @returns {Promise<Snapshot | undefined>} A promise that resolves to the snapshot or undefined
267
310
  */
@@ -270,6 +313,53 @@ async function readSnapshotOrUndefined(fs, version) {
270
313
  debug?.("Failed to read snapshot, returning undefined", err);
271
314
  });
272
315
  }
273
-
316
+ /**
317
+ * Parses and validates snapshot content from a raw string without requiring a filesystem bridge.
318
+ * Useful when snapshot content has been obtained from any source (HTTP, KV store, memory, etc.).
319
+ *
320
+ * @param {string} content - The raw snapshot content to parse
321
+ * @returns {Snapshot} The validated snapshot
322
+ * @throws {LockfileInvalidError} When the content is invalid or does not match the expected schema
323
+ *
324
+ * @example
325
+ * ```ts
326
+ * const response = await fetch("https://ucdjs.dev/16.0.0/snapshot.json");
327
+ * const content = await response.text();
328
+ * const snapshot = parseSnapshot(content);
329
+ * ```
330
+ */
331
+ function parseSnapshot(content) {
332
+ debug?.("Parsing snapshot from content");
333
+ if (!content) throw new LockfileInvalidError({
334
+ lockfilePath: "<content>",
335
+ message: "snapshot is empty"
336
+ });
337
+ debug?.("Successfully parsed snapshot");
338
+ return parseSnapshotFromContent(content, "<content>");
339
+ }
340
+ /**
341
+ * Parses and validates snapshot content from a raw string, returning undefined if parsing fails.
342
+ *
343
+ * @param {string} content - The raw snapshot content to parse
344
+ * @returns {Snapshot | undefined} The validated snapshot or undefined if parsing fails
345
+ *
346
+ * @example
347
+ * ```ts
348
+ * const response = await fetch("https://ucdjs.dev/16.0.0/snapshot.json");
349
+ * const content = await response.text();
350
+ * const snapshot = parseSnapshotOrUndefined(content);
351
+ * if (snapshot) {
352
+ * console.log("Unicode version:", snapshot.unicodeVersion);
353
+ * }
354
+ * ```
355
+ */
356
+ function parseSnapshotOrUndefined(content) {
357
+ try {
358
+ return parseSnapshot(content);
359
+ } catch {
360
+ debug?.("Failed to parse snapshot, returning undefined");
361
+ return;
362
+ }
363
+ }
274
364
  //#endregion
275
- export { LockfileBaseError, LockfileBridgeUnsupportedOperation, LockfileInvalidError, canUseLockfile, computeFileHash, computeFileHashWithoutUCDHeader, getLockfilePath, getSnapshotPath, readLockfile, readLockfileOrUndefined, readSnapshot, readSnapshotOrUndefined, stripUnicodeHeader, validateLockfile, writeLockfile, writeSnapshot };
365
+ export { LockfileBaseError, LockfileInvalidError, canUseLockfile, computeFileHash, computeFileHashWithoutUCDHeader, getLockfilePath, getSnapshotPath, parseLockfile, parseLockfileOrUndefined, parseSnapshot, parseSnapshotOrUndefined, readLockfile, readLockfileOrUndefined, readSnapshot, readSnapshotOrUndefined, stripUnicodeHeader, validateLockfile, writeLockfile, writeSnapshot };
@@ -1,5 +1,4 @@
1
- import { n as computeFileHashWithoutUCDHeader, t as computeFileHash } from "../hash-DYmMzCbf.mjs";
2
-
1
+ import { n as computeFileHashWithoutUCDHeader, t as computeFileHash } from "../hash-Bf5WIJe6.mjs";
3
2
  //#region src/test-utils/lockfile-builder.ts
4
3
  /**
5
4
  * Creates a lockfile entry for a single version
@@ -59,7 +58,6 @@ function createLockfile(versions, options) {
59
58
  };
60
59
  return lockfile;
61
60
  }
62
-
63
61
  //#endregion
64
62
  //#region src/test-utils/snapshot-builder.ts
65
63
  /**
@@ -99,6 +97,5 @@ function createSnapshotWithHashes(version, files) {
99
97
  }]))
100
98
  };
101
99
  }
102
-
103
100
  //#endregion
104
- export { createEmptyLockfile, createLockfile, createLockfileEntry, createSnapshot, createSnapshotWithHashes };
101
+ export { createEmptyLockfile, createLockfile, createLockfileEntry, createSnapshot, createSnapshotWithHashes };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ucdjs/lockfile",
3
- "version": "0.1.1-beta.7",
3
+ "version": "0.1.1-beta.9",
4
4
  "type": "module",
5
5
  "author": {
6
6
  "name": "Lucas Nørgård",
@@ -31,24 +31,27 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "pathe": "2.0.3",
34
- "@ucdjs/schemas": "0.1.1-beta.7",
35
- "@ucdjs-internal/shared": "0.1.1-beta.7",
36
- "@ucdjs/fs-bridge": "0.1.1-beta.7"
34
+ "@ucdjs-internal/shared": "0.1.1-beta.8",
35
+ "@ucdjs/schemas": "0.1.1-beta.8"
37
36
  },
38
37
  "devDependencies": {
39
- "@luxass/eslint-config": "7.2.1",
38
+ "@luxass/eslint-config": "7.4.1",
40
39
  "@luxass/utils": "2.7.3",
41
- "eslint": "10.0.2",
42
- "publint": "0.3.17",
43
- "tsdown": "0.20.3",
44
- "typescript": "5.9.3",
45
- "vitest-testdirs": "4.4.2",
40
+ "eslint": "10.1.0",
41
+ "publint": "0.3.18",
42
+ "tsdown": "0.21.4",
43
+ "typescript": "6.0.2",
44
+ "vitest-testdirs": "4.4.3",
46
45
  "@ucdjs-tooling/tsconfig": "1.0.0",
47
- "@ucdjs-tooling/tsdown-config": "1.0.0"
46
+ "@ucdjs-tooling/tsdown-config": "1.0.0",
47
+ "@ucdjs/fs-backend": "0.1.0-beta.1"
48
48
  },
49
49
  "publishConfig": {
50
50
  "access": "public"
51
51
  },
52
+ "inlinedDependencies": {
53
+ "hookable": "6.1.0"
54
+ },
52
55
  "scripts": {
53
56
  "build": "tsdown --tsconfig=./tsconfig.build.json",
54
57
  "dev": "tsdown --watch",