@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 +21 -0
- package/README.md +131 -0
- package/dist/hash-DYmMzCbf.mjs +107 -0
- package/dist/index.d.mts +185 -0
- package/dist/index.mjs +275 -0
- package/dist/test-utils/index.d.mts +103 -0
- package/dist/test-utils/index.mjs +104 -0
- package/package.json +59 -0
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 };
|
package/dist/index.d.mts
ADDED
|
@@ -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
|
+
}
|