@ucdjs/lockfile 0.1.1-beta.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-PRESENT Lucas Nørgård
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,131 @@
1
+ # @ucdjs/lockfile
2
+
3
+ Lockfile and snapshot management utilities for UCD stores.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @ucdjs/lockfile
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### Reading and Writing Lockfiles
14
+
15
+ ```typescript
16
+ import { NodeFileSystemBridge } from "@ucdjs/fs-bridge";
17
+ import { getLockfilePath, readLockfile, writeLockfile } from "@ucdjs/lockfile";
18
+
19
+ const fs = NodeFileSystemBridge({ basePath: "./store" });
20
+ const lockfilePath = getLockfilePath("./store");
21
+
22
+ // Read lockfile
23
+ const lockfile = await readLockfile(fs, lockfilePath);
24
+
25
+ // Write lockfile
26
+ await writeLockfile(fs, lockfilePath, {
27
+ lockfileVersion: 1,
28
+ versions: {
29
+ "16.0.0": {
30
+ path: "16.0.0/snapshot.json",
31
+ fileCount: 10,
32
+ totalSize: 1024,
33
+ },
34
+ },
35
+ });
36
+ ```
37
+
38
+ ### Reading and Writing Snapshots
39
+
40
+ ```typescript
41
+ import { getSnapshotPath, readSnapshot, writeSnapshot } from "@ucdjs/lockfile";
42
+
43
+ const basePath = "./store";
44
+ const version = "16.0.0";
45
+
46
+ // Read snapshot
47
+ const snapshot = await readSnapshot(fs, basePath, version);
48
+
49
+ // Write snapshot
50
+ await writeSnapshot(fs, basePath, version, {
51
+ unicodeVersion: "16.0.0",
52
+ files: {
53
+ "UnicodeData.txt": {
54
+ hash: "sha256:...",
55
+ size: 1024,
56
+ },
57
+ },
58
+ });
59
+ ```
60
+
61
+ ### Computing File Hashes
62
+
63
+ ```typescript
64
+ import { computeFileHash } from "@ucdjs/lockfile";
65
+
66
+ const content = "file content";
67
+ const hash = await computeFileHash(content);
68
+ // Returns: "sha256:..."
69
+ ```
70
+
71
+ ## API Reference
72
+
73
+ ### Lockfile Operations
74
+
75
+ - `canUseLockfile(fs: FileSystemBridge): boolean` - Check if bridge supports lockfile operations
76
+ - `readLockfile(fs: FileSystemBridge, lockfilePath: string): Promise<Lockfile>` - Read and validate lockfile
77
+ - `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
79
+
80
+ ### Snapshot Operations
81
+
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
85
+
86
+ ### Path Utilities
87
+
88
+ - `getLockfilePath(_basePath: string): string` - Get default lockfile path (`.ucd-store.lock`)
89
+ - `getSnapshotPath(basePath: string, version: string): string` - Get snapshot path for version
90
+
91
+ ### Hash Utilities
92
+
93
+ - `computeFileHash(content: string | Uint8Array): Promise<string>` - Compute SHA-256 hash
94
+
95
+ ### Error Types
96
+
97
+ - `LockfileInvalidError` - Thrown when a lockfile or snapshot is invalid
98
+ - `LockfileBridgeUnsupportedOperation` - Thrown when a filesystem bridge operation is not supported
99
+
100
+ ## Test Utilities
101
+
102
+ The package also exports test utilities for creating lockfiles and snapshots in tests:
103
+
104
+ ```typescript
105
+ import {
106
+ createEmptyLockfile,
107
+ createLockfile,
108
+ createLockfileEntry,
109
+ createSnapshot,
110
+ createSnapshotWithHashes,
111
+ } from "@ucdjs/lockfile/test-utils";
112
+
113
+ // Create an empty lockfile
114
+ const lockfile = createEmptyLockfile(["16.0.0", "15.1.0"]);
115
+
116
+ // Create a lockfile with custom options
117
+ const customLockfile = createLockfile(["16.0.0"], {
118
+ fileCounts: { "16.0.0": 10 },
119
+ totalSizes: { "16.0.0": 1024 },
120
+ });
121
+
122
+ // Create a snapshot
123
+ const snapshot = await createSnapshot("16.0.0", {
124
+ "UnicodeData.txt": "file content",
125
+ "Blocks.txt": "more content",
126
+ });
127
+ ```
128
+
129
+ ## License
130
+
131
+ MIT
@@ -0,0 +1,107 @@
1
+ //#region src/hash.ts
2
+ /**
3
+ * Checks if a line looks like a Unicode header line.
4
+ * Header lines typically contain:
5
+ * - Filename with version (e.g., "DerivedBinaryProperties-15.1.0.txt")
6
+ * - Date line (e.g., "Date: 2023-01-05")
7
+ * - Copyright line (e.g., "© 2023 Unicode®, Inc.")
8
+ *
9
+ * Uses simple string operations instead of complex regex to avoid ReDoS vulnerabilities.
10
+ *
11
+ * @internal
12
+ */
13
+ function isHeaderLine(line) {
14
+ 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
+ }
17
+ /**
18
+ * Strips the Unicode file header from content.
19
+ * The header typically contains:
20
+ * - Filename with version (e.g., "# DerivedBinaryProperties-15.1.0.txt")
21
+ * - Date line (e.g., "# Date: 2023-01-05, 20:34:33 GMT")
22
+ * - Copyright line (e.g., "# © 2023 Unicode®, Inc.")
23
+ *
24
+ * @param {string} content - The file content to strip the header from
25
+ * @returns {string} The content with the header stripped
26
+ */
27
+ function stripUnicodeHeader(content) {
28
+ const lines = content.split("\n");
29
+ let headerEndIndex = 0;
30
+ for (let i = 0; i < lines.length; i++) {
31
+ const line = lines[i];
32
+ if (line == null) continue;
33
+ const trimmedLine = line.trim();
34
+ if (trimmedLine === "" || trimmedLine === "#") continue;
35
+ if (trimmedLine.startsWith("#") && isHeaderLine(trimmedLine)) {
36
+ headerEndIndex = i + 1;
37
+ continue;
38
+ }
39
+ break;
40
+ }
41
+ if (headerEndIndex > 0) {
42
+ while (headerEndIndex < lines.length && lines[headerEndIndex]?.trim() === "") headerEndIndex++;
43
+ return lines.slice(headerEndIndex).join("\n");
44
+ }
45
+ return content;
46
+ }
47
+ /**
48
+ * Cached TextEncoder instance for UTF-8 string encoding.
49
+ *
50
+ * @internal
51
+ */
52
+ const textEncoder = new TextEncoder();
53
+ /**
54
+ * Hex characters for nibble-to-hex conversion.
55
+ *
56
+ * @internal
57
+ */
58
+ const HEX_CHARS = "0123456789abcdef";
59
+ /**
60
+ * Pre-computed lookup table for byte-to-hex conversion.
61
+ * Maps each byte value (0-255) to its two-character hex representation.
62
+ * Uses bit operations to extract high and low nibbles.
63
+ *
64
+ * @internal
65
+ */
66
+ const HEX_TABLE = [];
67
+ for (let i = 0; i < 256; i++) HEX_TABLE[i] = HEX_CHARS[i >> 4] + HEX_CHARS[i & 15];
68
+ /**
69
+ * Converts a Uint8Array to a hex string using pre-computed lookup table.
70
+ *
71
+ * @internal
72
+ */
73
+ function uint8ArrayToHex(bytes) {
74
+ let result = "";
75
+ for (let i = 0; i < bytes.length; i++) result += HEX_TABLE[bytes[i]];
76
+ return result;
77
+ }
78
+ /**
79
+ * Computes the SHA-256 hash of file content using Web Crypto API.
80
+ *
81
+ * @param {string | Uint8Array} content - The file content to hash
82
+ * @returns {Promise<string>} A promise that resolves to the hash in format "sha256:..."
83
+ * @throws {Error} When Web Crypto API is not available
84
+ */
85
+ async function computeFileHash(content) {
86
+ const data = typeof content === "string" ? textEncoder.encode(content) : content;
87
+ if (typeof crypto !== "undefined" && crypto.subtle && typeof crypto.subtle.digest === "function") {
88
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
89
+ return `sha256:${uint8ArrayToHex(new Uint8Array(hashBuffer))}`;
90
+ }
91
+ throw new Error("SHA-256 hashing is not available. Web Crypto API is required for hash computation.");
92
+ }
93
+ /**
94
+ * Computes the SHA-256 hash of file content after stripping the Unicode header.
95
+ * This is useful for comparing file content across versions, since the header
96
+ * contains version-specific information (version number, date, copyright year).
97
+ *
98
+ * @param {string} content - The file content to hash
99
+ * @returns {Promise<string>} A promise that resolves to the hash in format "sha256:..."
100
+ * @throws {Error} When Web Crypto API is not available
101
+ */
102
+ async function computeFileHashWithoutUCDHeader(content) {
103
+ return computeFileHash(stripUnicodeHeader(content));
104
+ }
105
+
106
+ //#endregion
107
+ export { computeFileHashWithoutUCDHeader as n, stripUnicodeHeader as r, computeFileHash as t };
@@ -0,0 +1,185 @@
1
+ import { FileSystemBridge } from "@ucdjs/fs-bridge";
2
+ import { Lockfile, LockfileInput, Snapshot } from "@ucdjs/schemas";
3
+
4
+ //#region src/errors.d.ts
5
+ /**
6
+ * Base error class for lockfile-related errors
7
+ */
8
+ declare class LockfileBaseError extends Error {
9
+ constructor(message: string);
10
+ }
11
+ /**
12
+ * Error thrown when a lockfile or snapshot is invalid or cannot be read
13
+ */
14
+ declare class LockfileInvalidError extends LockfileBaseError {
15
+ readonly lockfilePath: string;
16
+ readonly details: string[];
17
+ constructor({
18
+ lockfilePath,
19
+ message,
20
+ details
21
+ }: {
22
+ lockfilePath: string;
23
+ message: string;
24
+ details?: string[];
25
+ });
26
+ }
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
+ //#endregion
37
+ //#region src/hash.d.ts
38
+ /**
39
+ * Strips the Unicode file header from content.
40
+ * The header typically contains:
41
+ * - Filename with version (e.g., "# DerivedBinaryProperties-15.1.0.txt")
42
+ * - Date line (e.g., "# Date: 2023-01-05, 20:34:33 GMT")
43
+ * - Copyright line (e.g., "# © 2023 Unicode®, Inc.")
44
+ *
45
+ * @param {string} content - The file content to strip the header from
46
+ * @returns {string} The content with the header stripped
47
+ */
48
+ declare function stripUnicodeHeader(content: string): string;
49
+ /**
50
+ * Computes the SHA-256 hash of file content using Web Crypto API.
51
+ *
52
+ * @param {string | Uint8Array} content - The file content to hash
53
+ * @returns {Promise<string>} A promise that resolves to the hash in format "sha256:..."
54
+ * @throws {Error} When Web Crypto API is not available
55
+ */
56
+ declare function computeFileHash(content: string | Uint8Array): Promise<string>;
57
+ /**
58
+ * Computes the SHA-256 hash of file content after stripping the Unicode header.
59
+ * This is useful for comparing file content across versions, since the header
60
+ * contains version-specific information (version number, date, copyright year).
61
+ *
62
+ * @param {string} content - The file content to hash
63
+ * @returns {Promise<string>} A promise that resolves to the hash in format "sha256:..."
64
+ * @throws {Error} When Web Crypto API is not available
65
+ */
66
+ declare function computeFileHashWithoutUCDHeader(content: string): Promise<string>;
67
+ //#endregion
68
+ //#region src/lockfile.d.ts
69
+ /**
70
+ * Result of validating a lockfile
71
+ */
72
+ interface ValidateLockfileResult {
73
+ /** Whether the lockfile is valid */
74
+ valid: boolean;
75
+ /** The parsed lockfile data (only present if valid) */
76
+ data?: Lockfile;
77
+ /** Validation errors (only present if invalid) */
78
+ errors?: Array<{
79
+ /** The path to the field that failed validation */path: string; /** Human-readable error message */
80
+ message: string; /** Zod error code */
81
+ code: string;
82
+ }>;
83
+ }
84
+ /**
85
+ * Validates lockfile data against the schema without reading from filesystem.
86
+ * Useful for validating lockfile objects before writing or for CLI validation commands.
87
+ *
88
+ * @param {unknown} data - The data to validate as a lockfile
89
+ * @returns {ValidateLockfileResult} Validation result with parsed data or errors
90
+ *
91
+ * @example
92
+ * ```ts
93
+ * const result = validateLockfile(someData);
94
+ * if (result.valid) {
95
+ * console.log('Lockfile is valid:', result.data);
96
+ * } else {
97
+ * console.error('Validation errors:', result.errors);
98
+ * }
99
+ * ```
100
+ */
101
+ declare function validateLockfile(data: unknown): ValidateLockfileResult;
102
+ /**
103
+ * Checks if the filesystem bridge supports lockfile operations (requires write capability)
104
+ *
105
+ * @param {FileSystemBridge} fs - The filesystem bridge to check
106
+ * @returns {boolean} True if the bridge supports lockfile operations
107
+ */
108
+ declare function canUseLockfile(fs: FileSystemBridge): fs is FileSystemBridge & Required<Pick<FileSystemBridge, "write">>;
109
+ /**
110
+ * Reads and validates a lockfile from the filesystem.
111
+ *
112
+ * @param {FileSystemBridge} fs - Filesystem bridge to use for reading
113
+ * @param {string} lockfilePath - Path to the lockfile
114
+ * @returns {Promise<Lockfile>} A promise that resolves to the validated lockfile
115
+ * @throws {LockfileInvalidError} When the lockfile is invalid or missing
116
+ */
117
+ declare function readLockfile(fs: FileSystemBridge, lockfilePath: string): Promise<Lockfile>;
118
+ /**
119
+ * Writes a lockfile to the filesystem.
120
+ * If the filesystem bridge does not support write operations, the function
121
+ * will skip writing the lockfile and return without throwing.
122
+ *
123
+ * @param {FileSystemBridge} fs - Filesystem bridge to use for writing
124
+ * @param {string} lockfilePath - Path where the lockfile should be written
125
+ * @param {LockfileInput} lockfile - The lockfile data to write
126
+ * @returns {Promise<void>} A promise that resolves when the lockfile has been written
127
+ */
128
+ declare function writeLockfile(fs: FileSystemBridge, lockfilePath: string, lockfile: LockfileInput): Promise<void>;
129
+ /**
130
+ * Reads a lockfile or returns undefined if it doesn't exist or is invalid.
131
+ *
132
+ * @param {FileSystemBridge} fs - Filesystem bridge to use for reading
133
+ * @param {string} lockfilePath - Path to the lockfile
134
+ * @returns {Promise<Lockfile | undefined>} A promise that resolves to the lockfile or undefined
135
+ */
136
+ declare function readLockfileOrUndefined(fs: FileSystemBridge, lockfilePath: string): Promise<Lockfile | undefined>;
137
+ //#endregion
138
+ //#region src/paths.d.ts
139
+ /**
140
+ * Gets the lockfile filename.
141
+ * The lockfile is always named `.ucd-store.lock` and stored at the store root.
142
+ *
143
+ * @returns {string} The lockfile filename
144
+ */
145
+ declare function getLockfilePath(): string;
146
+ /**
147
+ * Gets the snapshot path for a given version (relative to store root).
148
+ * Snapshots are stored inside version directories: {version}/snapshot.json
149
+ *
150
+ * @param {string} version - The Unicode version
151
+ * @returns {string} The snapshot path relative to store root
152
+ */
153
+ declare function getSnapshotPath(version: string): string;
154
+ //#endregion
155
+ //#region src/snapshot.d.ts
156
+ /**
157
+ * Reads and validates a snapshot for a specific version.
158
+ *
159
+ * @param {FileSystemBridge} fs - Filesystem bridge to use for reading
160
+ * @param {string} version - The Unicode version
161
+ * @returns {Promise<Snapshot>} A promise that resolves to the validated snapshot
162
+ * @throws {LockfileInvalidError} When the snapshot is invalid or missing
163
+ */
164
+ declare function readSnapshot(fs: FileSystemBridge, version: string): Promise<Snapshot>;
165
+ /**
166
+ * Writes a snapshot for a specific version to the filesystem.
167
+ * Only works if the filesystem bridge supports write operations.
168
+ *
169
+ * @param {FileSystemBridge} fs - Filesystem bridge to use for writing
170
+ * @param {string} version - The Unicode version
171
+ * @param {Snapshot} snapshot - The snapshot data to write
172
+ * @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
+ */
175
+ declare function writeSnapshot(fs: FileSystemBridge, version: string, snapshot: Snapshot): Promise<void>;
176
+ /**
177
+ * Reads a snapshot or returns undefined if it doesn't exist or is invalid.
178
+ *
179
+ * @param {FileSystemBridge} fs - Filesystem bridge to use for reading
180
+ * @param {string} version - The Unicode version
181
+ * @returns {Promise<Snapshot | undefined>} A promise that resolves to the snapshot or undefined
182
+ */
183
+ declare function readSnapshotOrUndefined(fs: FileSystemBridge, version: string): Promise<Snapshot | undefined>;
184
+ //#endregion
185
+ export { LockfileBaseError, LockfileBridgeUnsupportedOperation, LockfileInvalidError, type ValidateLockfileResult, canUseLockfile, computeFileHash, computeFileHashWithoutUCDHeader, getLockfilePath, getSnapshotPath, readLockfile, readLockfileOrUndefined, readSnapshot, readSnapshotOrUndefined, stripUnicodeHeader, validateLockfile, writeLockfile, writeSnapshot };
package/dist/index.mjs ADDED
@@ -0,0 +1,275 @@
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";
4
+ import { LockfileSchema, SnapshotSchema } from "@ucdjs/schemas";
5
+ import { dirname, join } from "pathe";
6
+
7
+ //#region src/errors.ts
8
+ /**
9
+ * Base error class for lockfile-related errors
10
+ */
11
+ var LockfileBaseError = class LockfileBaseError extends Error {
12
+ constructor(message) {
13
+ super(message);
14
+ this.name = "LockfileBaseError";
15
+ Object.setPrototypeOf(this, LockfileBaseError.prototype);
16
+ }
17
+ };
18
+ /**
19
+ * Error thrown when a lockfile or snapshot is invalid or cannot be read
20
+ */
21
+ var LockfileInvalidError = class LockfileInvalidError extends LockfileBaseError {
22
+ lockfilePath;
23
+ details;
24
+ constructor({ lockfilePath, message, details }) {
25
+ super(`invalid lockfile at ${lockfilePath}: ${message}`);
26
+ this.name = "LockfileInvalidError";
27
+ this.lockfilePath = lockfilePath;
28
+ this.details = details || [];
29
+ Object.setPrototypeOf(this, LockfileInvalidError.prototype);
30
+ }
31
+ };
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
+ //#endregion
52
+ //#region src/lockfile.ts
53
+ const debug$1 = createDebugger("ucdjs:lockfile");
54
+ /**
55
+ * Validates lockfile data against the schema without reading from filesystem.
56
+ * Useful for validating lockfile objects before writing or for CLI validation commands.
57
+ *
58
+ * @param {unknown} data - The data to validate as a lockfile
59
+ * @returns {ValidateLockfileResult} Validation result with parsed data or errors
60
+ *
61
+ * @example
62
+ * ```ts
63
+ * const result = validateLockfile(someData);
64
+ * if (result.valid) {
65
+ * console.log('Lockfile is valid:', result.data);
66
+ * } else {
67
+ * console.error('Validation errors:', result.errors);
68
+ * }
69
+ * ```
70
+ */
71
+ function validateLockfile(data) {
72
+ const result = LockfileSchema.safeParse(data);
73
+ if (result.success) return {
74
+ valid: true,
75
+ data: result.data
76
+ };
77
+ return {
78
+ valid: false,
79
+ errors: result.error.issues.map((issue) => ({
80
+ path: issue.path.join("."),
81
+ message: issue.message,
82
+ code: issue.code
83
+ }))
84
+ };
85
+ }
86
+ /**
87
+ * Checks if the filesystem bridge supports lockfile operations (requires write capability)
88
+ *
89
+ * @param {FileSystemBridge} fs - The filesystem bridge to check
90
+ * @returns {boolean} True if the bridge supports lockfile operations
91
+ */
92
+ function canUseLockfile(fs) {
93
+ return hasCapability(fs, "write");
94
+ }
95
+ /**
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
102
+ */
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({
121
+ lockfilePath,
122
+ message: "lockfile is not valid JSON"
123
+ });
124
+ const parsedLockfile = LockfileSchema.safeParse(jsonData);
125
+ if (!parsedLockfile.success) {
126
+ debug$1?.("Failed to parse lockfile:", parsedLockfile.error.issues);
127
+ throw new LockfileInvalidError({
128
+ lockfilePath,
129
+ message: "lockfile does not match expected schema",
130
+ details: parsedLockfile.error.issues.map((issue) => issue.message)
131
+ });
132
+ }
133
+ debug$1?.("Successfully read lockfile");
134
+ return parsedLockfile.data;
135
+ }
136
+ /**
137
+ * Writes a lockfile to the filesystem.
138
+ * If the filesystem bridge does not support write operations, the function
139
+ * will skip writing the lockfile and return without throwing.
140
+ *
141
+ * @param {FileSystemBridge} fs - Filesystem bridge to use for writing
142
+ * @param {string} lockfilePath - Path where the lockfile should be written
143
+ * @param {LockfileInput} lockfile - The lockfile data to write
144
+ * @returns {Promise<void>} A promise that resolves when the lockfile has been written
145
+ */
146
+ async function writeLockfile(fs, lockfilePath, lockfile) {
147
+ if (!canUseLockfile(fs)) {
148
+ debug$1?.("Filesystem bridge does not support write operations, skipping lockfile write");
149
+ return;
150
+ }
151
+ debug$1?.("Writing lockfile to:", lockfilePath);
152
+ await fs.write(lockfilePath, JSON.stringify(lockfile, null, 2));
153
+ debug$1?.("Successfully wrote lockfile");
154
+ }
155
+ /**
156
+ * Reads a lockfile or returns undefined if it doesn't exist or is invalid.
157
+ *
158
+ * @param {FileSystemBridge} fs - Filesystem bridge to use for reading
159
+ * @param {string} lockfilePath - Path to the lockfile
160
+ * @returns {Promise<Lockfile | undefined>} A promise that resolves to the lockfile or undefined
161
+ */
162
+ async function readLockfileOrUndefined(fs, lockfilePath) {
163
+ return readLockfile(fs, lockfilePath).catch(() => {
164
+ debug$1?.("Failed to read lockfile, returning undefined");
165
+ });
166
+ }
167
+
168
+ //#endregion
169
+ //#region src/paths.ts
170
+ /**
171
+ * Gets the lockfile filename.
172
+ * The lockfile is always named `.ucd-store.lock` and stored at the store root.
173
+ *
174
+ * @returns {string} The lockfile filename
175
+ */
176
+ function getLockfilePath() {
177
+ return ".ucd-store.lock";
178
+ }
179
+ /**
180
+ * Gets the snapshot path for a given version (relative to store root).
181
+ * Snapshots are stored inside version directories: {version}/snapshot.json
182
+ *
183
+ * @param {string} version - The Unicode version
184
+ * @returns {string} The snapshot path relative to store root
185
+ */
186
+ function getSnapshotPath(version) {
187
+ return join(version, "snapshot.json");
188
+ }
189
+
190
+ //#endregion
191
+ //#region src/snapshot.ts
192
+ const debug = createDebugger("ucdjs:lockfile:snapshot");
193
+ /**
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
200
+ */
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({
220
+ lockfilePath: snapshotPath,
221
+ message: "snapshot is not valid JSON"
222
+ });
223
+ const parsedSnapshot = SnapshotSchema.safeParse(jsonData);
224
+ if (!parsedSnapshot.success) {
225
+ debug?.("Failed to parse snapshot:", parsedSnapshot.error.issues);
226
+ throw new LockfileInvalidError({
227
+ lockfilePath: snapshotPath,
228
+ message: "snapshot does not match expected schema",
229
+ details: parsedSnapshot.error.issues.map((issue) => issue.message)
230
+ });
231
+ }
232
+ debug?.("Successfully read snapshot");
233
+ return parsedSnapshot.data;
234
+ }
235
+ /**
236
+ * Writes a snapshot for a specific version to the filesystem.
237
+ * Only works if the filesystem bridge supports write operations.
238
+ *
239
+ * @param {FileSystemBridge} fs - Filesystem bridge to use for writing
240
+ * @param {string} version - The Unicode version
241
+ * @param {Snapshot} snapshot - The snapshot data to write
242
+ * @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
+ */
245
+ async function writeSnapshot(fs, version, snapshot) {
246
+ if (!canUseLockfile(fs)) {
247
+ debug?.("Filesystem bridge does not support write operations, skipping snapshot write");
248
+ return;
249
+ }
250
+ const snapshotPath = getSnapshotPath(version);
251
+ const snapshotDir = dirname(snapshotPath);
252
+ debug?.("Writing snapshot to:", snapshotPath);
253
+ 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
+ debug?.("Creating snapshot directory:", snapshotDir);
256
+ await fs.mkdir(snapshotDir);
257
+ }
258
+ await fs.write(snapshotPath, JSON.stringify(snapshot, null, 2));
259
+ debug?.("Successfully wrote snapshot");
260
+ }
261
+ /**
262
+ * Reads a snapshot or returns undefined if it doesn't exist or is invalid.
263
+ *
264
+ * @param {FileSystemBridge} fs - Filesystem bridge to use for reading
265
+ * @param {string} version - The Unicode version
266
+ * @returns {Promise<Snapshot | undefined>} A promise that resolves to the snapshot or undefined
267
+ */
268
+ async function readSnapshotOrUndefined(fs, version) {
269
+ return readSnapshot(fs, version).catch((err) => {
270
+ debug?.("Failed to read snapshot, returning undefined", err);
271
+ });
272
+ }
273
+
274
+ //#endregion
275
+ export { LockfileBaseError, LockfileBridgeUnsupportedOperation, LockfileInvalidError, canUseLockfile, computeFileHash, computeFileHashWithoutUCDHeader, getLockfilePath, getSnapshotPath, readLockfile, readLockfileOrUndefined, readSnapshot, readSnapshotOrUndefined, stripUnicodeHeader, validateLockfile, writeLockfile, writeSnapshot };
@@ -0,0 +1,103 @@
1
+ import { Lockfile, Snapshot } from "@ucdjs/schemas";
2
+
3
+ //#region src/test-utils/lockfile-builder.d.ts
4
+ interface CreateLockfileOptions {
5
+ /**
6
+ * Custom file counts per version
7
+ */
8
+ fileCounts?: Record<string, number>;
9
+ /**
10
+ * Custom total sizes per version (in bytes)
11
+ */
12
+ totalSizes?: Record<string, number>;
13
+ /**
14
+ * Custom snapshot paths per version
15
+ * If not provided, defaults to `{version}/snapshot.json`
16
+ */
17
+ snapshotPaths?: Record<string, string>;
18
+ /**
19
+ * Custom filters for the lockfile
20
+ */
21
+ filters?: {
22
+ disableDefaultExclusions?: boolean;
23
+ exclude?: string[];
24
+ include?: string[];
25
+ };
26
+ /**
27
+ * Custom createdAt timestamp for the lockfile
28
+ * If not provided, defaults to current date
29
+ */
30
+ createdAt?: Date;
31
+ /**
32
+ * Custom updatedAt timestamp for the lockfile
33
+ * If not provided, defaults to current date
34
+ */
35
+ updatedAt?: Date;
36
+ /**
37
+ * Custom timestamps per version entry
38
+ */
39
+ versionTimestamps?: Record<string, {
40
+ createdAt?: Date;
41
+ updatedAt?: Date;
42
+ }>;
43
+ }
44
+ /**
45
+ * Creates a lockfile entry for a single version
46
+ */
47
+ declare function createLockfileEntry(version: string, options?: {
48
+ fileCount?: number;
49
+ totalSize?: number;
50
+ snapshotPath?: string;
51
+ createdAt?: Date;
52
+ updatedAt?: Date;
53
+ }): Lockfile["versions"][string];
54
+ /**
55
+ * Creates an empty lockfile with the specified versions
56
+ * All versions will have fileCount: 0 and totalSize: 0
57
+ */
58
+ declare function createEmptyLockfile(versions: string[]): Lockfile;
59
+ /**
60
+ * Creates a lockfile with the specified versions and optional customizations
61
+ */
62
+ declare function createLockfile(versions: string[], options?: CreateLockfileOptions): Lockfile;
63
+ //#endregion
64
+ //#region src/test-utils/snapshot-builder.d.ts
65
+ interface CreateSnapshotOptions {
66
+ /**
67
+ * Pre-computed content hashes for files (hash without Unicode header)
68
+ * If not provided, will be computed using computeFileHashWithoutUCDHeader
69
+ */
70
+ hashes?: Record<string, string>;
71
+ /**
72
+ * Pre-computed file hashes for files (hash of complete file)
73
+ * If not provided, will be computed using computeFileHash
74
+ */
75
+ fileHashes?: Record<string, string>;
76
+ /**
77
+ * Pre-computed sizes for files (if not provided, will be computed from content)
78
+ */
79
+ sizes?: Record<string, number>;
80
+ }
81
+ /**
82
+ * Creates a snapshot for a version with the specified files
83
+ * Automatically computes hashes and sizes if not provided
84
+ *
85
+ * @param version - The Unicode version for this snapshot
86
+ * @param files - Map of file paths to file content
87
+ * @param options - Optional pre-computed values
88
+ */
89
+ declare function createSnapshot(version: string, files: Record<string, string>, options?: CreateSnapshotOptions): Promise<Snapshot>;
90
+ /**
91
+ * Creates a snapshot with pre-computed hashes and sizes
92
+ * Useful when you want to control the exact hash/size values
93
+ *
94
+ * @param version - The Unicode version for this snapshot
95
+ * @param files - Map of file paths to file metadata
96
+ */
97
+ declare function createSnapshotWithHashes(version: string, files: Record<string, {
98
+ hash: string;
99
+ fileHash: string;
100
+ size: number;
101
+ }>): Snapshot;
102
+ //#endregion
103
+ export { type CreateLockfileOptions, type CreateSnapshotOptions, createEmptyLockfile, createLockfile, createLockfileEntry, createSnapshot, createSnapshotWithHashes };
@@ -0,0 +1,104 @@
1
+ import { n as computeFileHashWithoutUCDHeader, t as computeFileHash } from "../hash-DYmMzCbf.mjs";
2
+
3
+ //#region src/test-utils/lockfile-builder.ts
4
+ /**
5
+ * Creates a lockfile entry for a single version
6
+ */
7
+ function createLockfileEntry(version, options) {
8
+ const now = /* @__PURE__ */ new Date();
9
+ return {
10
+ path: options?.snapshotPath ?? `${version}/snapshot.json`,
11
+ fileCount: options?.fileCount ?? 0,
12
+ totalSize: options?.totalSize ?? 0,
13
+ createdAt: options?.createdAt ?? now,
14
+ updatedAt: options?.updatedAt ?? now
15
+ };
16
+ }
17
+ /**
18
+ * Creates an empty lockfile with the specified versions
19
+ * All versions will have fileCount: 0 and totalSize: 0
20
+ */
21
+ function createEmptyLockfile(versions) {
22
+ const now = /* @__PURE__ */ new Date();
23
+ return {
24
+ lockfileVersion: 1,
25
+ createdAt: now,
26
+ updatedAt: now,
27
+ versions: Object.fromEntries(versions.map((version) => [version, createLockfileEntry(version, {
28
+ createdAt: now,
29
+ updatedAt: now
30
+ })]))
31
+ };
32
+ }
33
+ /**
34
+ * Creates a lockfile with the specified versions and optional customizations
35
+ */
36
+ function createLockfile(versions, options) {
37
+ const now = /* @__PURE__ */ new Date();
38
+ const createdAt = options?.createdAt ?? now;
39
+ const updatedAt = options?.updatedAt ?? now;
40
+ const lockfile = {
41
+ lockfileVersion: 1,
42
+ createdAt,
43
+ updatedAt,
44
+ versions: Object.fromEntries(versions.map((version) => {
45
+ const versionTimestamps = options?.versionTimestamps?.[version];
46
+ return [version, createLockfileEntry(version, {
47
+ fileCount: options?.fileCounts?.[version],
48
+ totalSize: options?.totalSizes?.[version],
49
+ snapshotPath: options?.snapshotPaths?.[version],
50
+ createdAt: versionTimestamps?.createdAt ?? createdAt,
51
+ updatedAt: versionTimestamps?.updatedAt ?? updatedAt
52
+ })];
53
+ }))
54
+ };
55
+ if (options?.filters) lockfile.filters = {
56
+ disableDefaultExclusions: options.filters.disableDefaultExclusions,
57
+ exclude: options.filters.exclude,
58
+ include: options.filters.include
59
+ };
60
+ return lockfile;
61
+ }
62
+
63
+ //#endregion
64
+ //#region src/test-utils/snapshot-builder.ts
65
+ /**
66
+ * Creates a snapshot for a version with the specified files
67
+ * Automatically computes hashes and sizes if not provided
68
+ *
69
+ * @param version - The Unicode version for this snapshot
70
+ * @param files - Map of file paths to file content
71
+ * @param options - Optional pre-computed values
72
+ */
73
+ async function createSnapshot(version, files, options) {
74
+ const snapshotFiles = {};
75
+ for (const [filePath, content] of Object.entries(files)) snapshotFiles[filePath] = {
76
+ hash: options?.hashes?.[filePath] ?? await computeFileHashWithoutUCDHeader(content),
77
+ fileHash: options?.fileHashes?.[filePath] ?? await computeFileHash(content),
78
+ size: options?.sizes?.[filePath] ?? new TextEncoder().encode(content).length
79
+ };
80
+ return {
81
+ unicodeVersion: version,
82
+ files: snapshotFiles
83
+ };
84
+ }
85
+ /**
86
+ * Creates a snapshot with pre-computed hashes and sizes
87
+ * Useful when you want to control the exact hash/size values
88
+ *
89
+ * @param version - The Unicode version for this snapshot
90
+ * @param files - Map of file paths to file metadata
91
+ */
92
+ function createSnapshotWithHashes(version, files) {
93
+ return {
94
+ unicodeVersion: version,
95
+ files: Object.fromEntries(Object.entries(files).map(([filePath, { hash, fileHash, size }]) => [filePath, {
96
+ hash,
97
+ fileHash,
98
+ size
99
+ }]))
100
+ };
101
+ }
102
+
103
+ //#endregion
104
+ export { createEmptyLockfile, createLockfile, createLockfileEntry, createSnapshot, createSnapshotWithHashes };
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@ucdjs/lockfile",
3
+ "version": "0.1.1-beta.1",
4
+ "type": "module",
5
+ "author": {
6
+ "name": "Lucas Nørgård",
7
+ "email": "lucasnrgaard@gmail.com",
8
+ "url": "https://luxass.dev"
9
+ },
10
+ "license": "MIT",
11
+ "homepage": "https://github.com/ucdjs/ucd",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/ucdjs/ucd.git",
15
+ "directory": "packages/lockfile"
16
+ },
17
+ "bugs": {
18
+ "url": "https://github.com/ucdjs/ucd/issues"
19
+ },
20
+ "exports": {
21
+ ".": "./dist/index.mjs",
22
+ "./test-utils": "./dist/test-utils/index.mjs",
23
+ "./package.json": "./package.json"
24
+ },
25
+ "types": "./dist/index.d.mts",
26
+ "files": [
27
+ "dist"
28
+ ],
29
+ "engines": {
30
+ "node": ">=22.18"
31
+ },
32
+ "dependencies": {
33
+ "pathe": "2.0.3",
34
+ "@ucdjs-internal/shared": "0.1.1-beta.1",
35
+ "@ucdjs/fs-bridge": "0.1.1-beta.1",
36
+ "@ucdjs/schemas": "0.1.1-beta.1"
37
+ },
38
+ "devDependencies": {
39
+ "@luxass/eslint-config": "7.2.0",
40
+ "@luxass/utils": "2.7.3",
41
+ "eslint": "10.0.0",
42
+ "publint": "0.3.17",
43
+ "tsdown": "0.20.3",
44
+ "typescript": "5.9.3",
45
+ "vitest-testdirs": "4.4.2",
46
+ "@ucdjs-tooling/tsdown-config": "1.0.0",
47
+ "@ucdjs-tooling/tsconfig": "1.0.0"
48
+ },
49
+ "publishConfig": {
50
+ "access": "public"
51
+ },
52
+ "scripts": {
53
+ "build": "tsdown --tsconfig=./tsconfig.build.json",
54
+ "dev": "tsdown --watch",
55
+ "clean": "git clean -xdf dist node_modules",
56
+ "lint": "eslint .",
57
+ "typecheck": "tsc --noEmit -p tsconfig.build.json"
58
+ }
59
+ }