@warlock.js/fs 4.1.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/README.md +76 -0
- package/cjs/index.cjs +424 -0
- package/cjs/index.cjs.map +1 -0
- package/esm/atomic.d.mts +22 -0
- package/esm/atomic.mjs +40 -0
- package/esm/atomic.mjs.map +1 -0
- package/esm/copy.d.mts +14 -0
- package/esm/copy.mjs +29 -0
- package/esm/copy.mjs.map +1 -0
- package/esm/dirs.d.mts +10 -0
- package/esm/dirs.mjs +18 -0
- package/esm/dirs.mjs.map +1 -0
- package/esm/exists.d.mts +44 -0
- package/esm/exists.mjs +86 -0
- package/esm/exists.mjs.map +1 -0
- package/esm/hash.d.mts +32 -0
- package/esm/hash.mjs +52 -0
- package/esm/hash.mjs.map +1 -0
- package/esm/index.d.mts +12 -0
- package/esm/index.mjs +13 -0
- package/esm/list.d.mts +20 -0
- package/esm/list.mjs +39 -0
- package/esm/list.mjs.map +1 -0
- package/esm/read.d.mts +16 -0
- package/esm/read.mjs +28 -0
- package/esm/read.mjs.map +1 -0
- package/esm/remove.d.mts +14 -0
- package/esm/remove.mjs +40 -0
- package/esm/remove.mjs.map +1 -0
- package/esm/rename.d.mts +9 -0
- package/esm/rename.mjs +17 -0
- package/esm/rename.mjs.map +1 -0
- package/esm/stats.d.mts +16 -0
- package/esm/stats.mjs +28 -0
- package/esm/stats.mjs.map +1 -0
- package/esm/write.d.mts +14 -0
- package/esm/write.mjs +29 -0
- package/esm/write.mjs.map +1 -0
- package/llms-full.txt +570 -0
- package/llms.txt +13 -0
- package/package.json +24 -0
- package/skills/hash-files/SKILL.md +122 -0
- package/skills/manage-directories/SKILL.md +140 -0
- package/skills/overview/SKILL.md +77 -0
- package/skills/read-and-write-files/SKILL.md +107 -0
- package/skills/write-atomically/SKILL.md +98 -0
package/llms-full.txt
ADDED
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
# Warlock FS — full skills
|
|
2
|
+
|
|
3
|
+
> Package: `@warlock.js/fs`
|
|
4
|
+
|
|
5
|
+
> Generated artifact. Concatenates every SKILL.md and reference file under `@warlock.js/fs/skills/`. Re-run `node scripts/generate-llms.mjs` after any change.
|
|
6
|
+
|
|
7
|
+
## hash-files `@warlock.js/fs/hash-files/SKILL.md`
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
name: hash-files
|
|
11
|
+
description: 'Compute hex digests — hashFile / hashFileAsync (streaming for large files), hashFileSmallAsync, hashString, hashBuffer. Defaults to SHA-256; supports sha1 / md5 / sha512. Triggers: `hashFile`, `hashFileAsync`, `hashFileSmallAsync`, `hashString`, `hashBuffer`, `HashAlgorithm`; "fingerprint a file for cache invalidation", "compute SHA-256 checksum", "compare two files for equality", "cache key from request input"; typical import `import { hashFileAsync, hashString } from "@warlock.js/fs"`. Skip: file IO — `@warlock.js/fs/read-and-write-files/SKILL.md`; competing libs `hasha`, `md5-file`, `crypto-hash`; native `node:crypto` `createHash`.'
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Compute file and content hashes
|
|
15
|
+
|
|
16
|
+
Hex-digest helpers backed by `node:crypto`. Picks the right strategy for the input size — streaming for files (memory stays flat), one-shot for in-memory content.
|
|
17
|
+
|
|
18
|
+
## Available algorithms
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
type HashAlgorithm = "sha256" | "sha1" | "md5" | "sha512";
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Default is `"sha256"` — the right choice for cache-bust, content-addressable storage, and fingerprinting. Pick `"md5"` only when matching an external system that requires it; never for security.
|
|
25
|
+
|
|
26
|
+
## Hash a file
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { hashFile, hashFileAsync } from "@warlock.js/fs";
|
|
30
|
+
|
|
31
|
+
// Streaming — constant memory regardless of file size
|
|
32
|
+
const fingerprint = await hashFileAsync("./bundle.js");
|
|
33
|
+
// → "8a7d3e2f9b4c..."
|
|
34
|
+
|
|
35
|
+
// Sync, with custom algorithm
|
|
36
|
+
const md5 = hashFile("./small.txt", "md5");
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`hashFileAsync` uses a stream, so a 1 GB file doesn't blow the heap. `hashFile` (sync) reads the whole file at once — fine for small files.
|
|
40
|
+
|
|
41
|
+
## Hash a small file in one shot
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { hashFileSmallAsync } from "@warlock.js/fs";
|
|
45
|
+
|
|
46
|
+
const digest = await hashFileSmallAsync("./icon.svg");
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`hashFileSmallAsync` reads the file in a single `readFile` call before hashing. Slightly faster than streaming when the file is < ~1 MB; **don't** use it on large files (it'll load the whole thing into memory).
|
|
50
|
+
|
|
51
|
+
| Use | Reach for |
|
|
52
|
+
| --- | --- |
|
|
53
|
+
| Streaming async (default for files) | `hashFileAsync` |
|
|
54
|
+
| Small file, slightly faster async | `hashFileSmallAsync` |
|
|
55
|
+
| Sync (CLI / config loader only) | `hashFile` |
|
|
56
|
+
| In-memory string | `hashString` |
|
|
57
|
+
| In-memory Buffer / Uint8Array | `hashBuffer` |
|
|
58
|
+
|
|
59
|
+
## Hash in-memory content
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { hashString, hashBuffer } from "@warlock.js/fs";
|
|
63
|
+
|
|
64
|
+
const stringDigest = hashString("hello world");
|
|
65
|
+
const bufferDigest = hashBuffer(Buffer.from([0x01, 0x02, 0x03]));
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Both sync, both default to SHA-256, both accept the algorithm override.
|
|
69
|
+
|
|
70
|
+
## Common shapes
|
|
71
|
+
|
|
72
|
+
### Cache key from request input
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
import { hashString } from "@warlock.js/fs";
|
|
76
|
+
|
|
77
|
+
const key = `report.${hashString(JSON.stringify(filters))}`;
|
|
78
|
+
await cache.set(key, report, "1h");
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Stable, short, collision-resistant. JSON stringification is the gotcha — key order matters; sort keys if the input might arrive in different orders.
|
|
82
|
+
|
|
83
|
+
### Bust a CDN cache when a build artifact changes
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import { hashFileAsync } from "@warlock.js/fs";
|
|
87
|
+
|
|
88
|
+
const digest = await hashFileAsync("./dist/bundle.js");
|
|
89
|
+
await renameFileAsync("./dist/bundle.js", `./dist/bundle.${digest.slice(0, 8)}.js`);
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
8 hex chars (≈32 bits) is enough for a single-app deployment — collisions on a per-build basis are vanishingly small.
|
|
93
|
+
|
|
94
|
+
### Skip work if content hasn't changed
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
import { hashFileAsync, fileExistsAsync, getFileAsync } from "@warlock.js/fs";
|
|
98
|
+
|
|
99
|
+
const inputDigest = await hashFileAsync("./input.json");
|
|
100
|
+
const cachedDigest = (await fileExistsAsync("./.last-input-digest"))
|
|
101
|
+
? await getFileAsync("./.last-input-digest")
|
|
102
|
+
: null;
|
|
103
|
+
|
|
104
|
+
if (inputDigest === cachedDigest) {
|
|
105
|
+
return; // input unchanged — skip the expensive pipeline
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
await runPipeline();
|
|
109
|
+
await putFileAsync("./.last-input-digest", inputDigest);
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Compare two files for equality
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
const same = (await hashFileAsync(a)) === (await hashFileAsync(b));
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Cheaper than `cmp`-byte-comparing two large files when the result is "yes/no different" — and you cache the digests for later comparisons against other candidates.
|
|
119
|
+
|
|
120
|
+
## See also
|
|
121
|
+
|
|
122
|
+
- [`@warlock.js/fs/read-and-write-files/SKILL.md`](@warlock.js/fs/read-and-write-files/SKILL.md) — reading files before hashing in-memory content
|
|
123
|
+
- [`@warlock.js/cache/use-cached-hof/SKILL.md`](@warlock.js/cache/use-cached-hof/SKILL.md) — building cache keys from hashes
|
|
124
|
+
|
|
125
|
+
## Things NOT to do
|
|
126
|
+
|
|
127
|
+
- Don't use MD5 or SHA-1 for security purposes (password hashing, signature verification). Both are broken cryptographically — they're fine for cache keys and non-adversarial fingerprinting, nothing more.
|
|
128
|
+
- Don't truncate the digest below 32 bits (8 hex chars) for collision-sensitive uses. Two builds with the same prefix do happen; full digest is 64 hex chars for SHA-256.
|
|
129
|
+
- Don't load a large file via `getFileAsync` and then `hashString` it — that defeats the streaming optimization. Use `hashFileAsync` directly.
|
|
130
|
+
- Don't expect digests to compare lexically meaningfully — they're random hex. For ordering, hash and then `bigint`-convert if you really need sort.
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
## manage-directories `@warlock.js/fs/manage-directories/SKILL.md`
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
name: manage-directories
|
|
137
|
+
description: 'Manage directories + files — ensureDirectory (mkdir -p), list / listFiles / listDirectories, copyFile / copyDirectory, renameFile, unlink (ENOENT-safe), removeDirectory (recursive force). Triggers: `ensureDirectoryAsync`, `listAsync`, `listFilesAsync`, `listDirectoriesAsync`, `copyFileAsync`, `copyDirectoryAsync`, `renameFileAsync`, `unlinkAsync`, `removeDirectoryAsync`; "create directory recursively", "list files in folder", "delete directory recursively", "copy or rename folder", "walk a tree"; typical import `import { ensureDirectoryAsync, listFilesAsync, removeDirectoryAsync } from "@warlock.js/fs"`. Skip: file IO — `@warlock.js/fs/read-and-write-files/SKILL.md`; atomic writes — `@warlock.js/fs/write-atomically/SKILL.md`; competing libs `fs-extra`, `mkdirp`, `rimraf`, `recursive-readdir`, `glob`; native `node:fs/promises`.'
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
# Manage directories and files on disk
|
|
141
|
+
|
|
142
|
+
Same two-suffix convention as the rest of `@warlock.js/fs` — `*Async` is async, the bare name is sync.
|
|
143
|
+
|
|
144
|
+
## Ensure a directory exists
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
import { ensureDirectoryAsync } from "@warlock.js/fs";
|
|
148
|
+
|
|
149
|
+
await ensureDirectoryAsync("./dist/cache/v2");
|
|
150
|
+
// recursively creates dist, dist/cache, dist/cache/v2 if missing
|
|
151
|
+
// no-op if everything already exists
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Idempotent. Pair it with anything that writes — though `putFileAsync` already does this internally, so you rarely need `ensureDirectory` for the immediate parent of a file you're about to write.
|
|
155
|
+
|
|
156
|
+
## List children
|
|
157
|
+
|
|
158
|
+
Three variants:
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
import { listAsync, listFilesAsync, listDirectoriesAsync } from "@warlock.js/fs";
|
|
162
|
+
|
|
163
|
+
await listAsync("./src"); // [files + subdirs] full paths
|
|
164
|
+
await listFilesAsync("./src"); // only regular files
|
|
165
|
+
await listDirectoriesAsync("./src"); // only directories
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Returns **full paths** (joined to the directory you passed in), not bare entry names. Pass them straight to other fs calls.
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
const components = await listFilesAsync("./src/components");
|
|
172
|
+
for (const file of components) {
|
|
173
|
+
// file = "./src/components/Button.tsx"
|
|
174
|
+
await processComponent(file);
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Only the immediate children — non-recursive. Recurse yourself with the directory variant + a stack/queue if you need deep traversal.
|
|
179
|
+
|
|
180
|
+
## Copy
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
import { copyFileAsync, copyDirectoryAsync } from "@warlock.js/fs";
|
|
184
|
+
|
|
185
|
+
// File — creates the destination's parent directories
|
|
186
|
+
await copyFileAsync("./dist/bundle.js", "./snapshot/v2/bundle.js");
|
|
187
|
+
|
|
188
|
+
// Directory — fully recursive
|
|
189
|
+
await copyDirectoryAsync("./public", "./dist/public");
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
`copyFile` creates the destination's parent directories. `copyDirectory` uses Node's recursive `cp` under the hood — preserves the tree, overwrites existing files.
|
|
193
|
+
|
|
194
|
+
## Rename / move
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
import { renameFileAsync } from "@warlock.js/fs";
|
|
198
|
+
|
|
199
|
+
await renameFileAsync("./tmp/foo.txt", "./final/foo.txt");
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Works on files and directories. Cross-device renames (e.g. `/tmp` → `/var`) may fail with `EXDEV` — the OS won't move across mounts. For cross-device, copy then delete.
|
|
203
|
+
|
|
204
|
+
## Delete
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
import { unlinkAsync, removeDirectoryAsync } from "@warlock.js/fs";
|
|
208
|
+
|
|
209
|
+
await unlinkAsync("./obsolete.txt"); // single file — ENOENT-safe
|
|
210
|
+
await removeDirectoryAsync("./dist"); // recursive + force — ENOENT-safe
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Both are **idempotent for missing targets** — calling on a path that doesn't exist is a no-op, not an error. Other errors (`EACCES`, `EBUSY`) still throw.
|
|
214
|
+
|
|
215
|
+
## Picking a delete shape
|
|
216
|
+
|
|
217
|
+
| Task | Reach for |
|
|
218
|
+
| --- | --- |
|
|
219
|
+
| Drop one file | `unlinkAsync(path)` |
|
|
220
|
+
| Drop a whole tree | `removeDirectoryAsync(path)` |
|
|
221
|
+
| Drop everything in a folder but keep the folder | `for (const f of await listAsync(dir)) await removeDirectoryAsync(f)` (file? unlink; dir? recurse) |
|
|
222
|
+
|
|
223
|
+
## Common shapes
|
|
224
|
+
|
|
225
|
+
### Snapshot a build output
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
import { ensureDirectoryAsync, copyDirectoryAsync, removeDirectoryAsync } from "@warlock.js/fs";
|
|
229
|
+
|
|
230
|
+
const target = `./snapshots/${Date.now()}`;
|
|
231
|
+
await ensureDirectoryAsync(target);
|
|
232
|
+
await copyDirectoryAsync("./dist", target);
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Reset a temp dir between runs
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
await removeDirectoryAsync("./tmp");
|
|
239
|
+
await ensureDirectoryAsync("./tmp");
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Walk every TS file under src
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
async function walk(dir: string): Promise<string[]> {
|
|
246
|
+
const entries = await listAsync(dir);
|
|
247
|
+
const results: string[] = [];
|
|
248
|
+
|
|
249
|
+
for (const entry of entries) {
|
|
250
|
+
if (await directoryExistsAsync(entry)) {
|
|
251
|
+
results.push(...(await walk(entry)));
|
|
252
|
+
} else if (entry.endsWith(".ts")) {
|
|
253
|
+
results.push(entry);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return results;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const tsFiles = await walk("./src");
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## See also
|
|
264
|
+
|
|
265
|
+
- [`@warlock.js/fs/read-and-write-files/SKILL.md`](@warlock.js/fs/read-and-write-files/SKILL.md) — text / JSON file IO and existence checks
|
|
266
|
+
- [`@warlock.js/fs/write-atomically/SKILL.md`](@warlock.js/fs/write-atomically/SKILL.md) — atomic writes when concurrent readers might see the file mid-write
|
|
267
|
+
- [`@warlock.js/fs/hash-files/SKILL.md`](@warlock.js/fs/hash-files/SKILL.md) — fingerprinting
|
|
268
|
+
|
|
269
|
+
## Things NOT to do
|
|
270
|
+
|
|
271
|
+
- Don't expect `listAsync` to recurse — it's intentionally one level. Write your own walker (see above) for deep traversal.
|
|
272
|
+
- Don't catch `ENOENT` on `unlink` / `removeDirectory` — both functions already swallow missing-target errors. If you're catching, you're handling a real error and should re-throw.
|
|
273
|
+
- Don't use `renameFile` across filesystems / mounts — `EXDEV` will surface. Copy + delete for cross-device.
|
|
274
|
+
- Don't list a directory you're concurrently modifying — readdir snapshots aren't atomic across concurrent writes.
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
## overview `@warlock.js/fs/overview/SKILL.md`
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
name: overview
|
|
281
|
+
description: 'Front-door orientation for `@warlock.js/fs` — filesystem primitives (read/write/JSON, dirs, copy/rename/delete, atomic writes, hashing, existence + stats). Two-suffix convention: `*Async` returns Promise, bare name is sync. Single canonical name per operation — no aliases. TRIGGER when: code imports anything from `@warlock.js/fs`; user asks "what does @warlock.js/fs do", "is fs the right package for X", "list all fs helpers", "fs sync vs async convention"; package.json adds `@warlock.js/fs`; user is choosing between fs vs `node:fs/promises`/`fs-extra`/`graceful-fs`. Skip: specific task already known — load the matching task skill directly (`@warlock.js/fs/read-and-write-files/SKILL.md`, `@warlock.js/fs/manage-directories/SKILL.md`, `@warlock.js/fs/write-atomically/SKILL.md`, `@warlock.js/fs/hash-files/SKILL.md`); the user is using plain `node:fs` and not touching fs imports.'
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
# `@warlock.js/fs` — overview
|
|
285
|
+
|
|
286
|
+
Thin, opinionated wrapper over `node:fs` and `node:fs/promises`. Same operations you'd write by hand against the Node primitives, but with consistent naming, parent-directory auto-creation, ENOENT-safe deletes, atomic writes, and streaming hashes — the boring-but-load-bearing utilities every backend grows by month two anyway.
|
|
287
|
+
|
|
288
|
+
## When to reach for it
|
|
289
|
+
|
|
290
|
+
- You're inside a `@warlock.js/*` project — every other framework package already depends on this one. Use it for consistency.
|
|
291
|
+
- You're outside Warlock but want one import that gives you sane defaults (auto-mkdir on writes, idempotent deletes, streaming hashes, atomic config writes).
|
|
292
|
+
- You're choosing between `node:fs` and a wrapper — and want a small, opinionated surface (~40 exports) rather than the kitchen sink of `fs-extra` or the patching-Node behavior of `graceful-fs`.
|
|
293
|
+
|
|
294
|
+
Skip if your code path is one-off and bare `node:fs/promises` is already imported elsewhere — there's no value in adding a dependency for a single `readFile`.
|
|
295
|
+
|
|
296
|
+
## Convention — read these once and you know the shape
|
|
297
|
+
|
|
298
|
+
- **`*Async` is async** (returns `Promise<…>`). Use these in server / runtime code.
|
|
299
|
+
- **Bare name is synchronous.** Use these in CLI tools, code generators, and one-shot scripts where blocking the loop doesn't matter.
|
|
300
|
+
- **One canonical name per operation.** No aliases (`unlinkAsync`, not `deleteAsync` or `removeAsync` for the same thing). Reach for the obvious name; if you don't find it, it doesn't exist.
|
|
301
|
+
- **Writes auto-create parent directories.** `putFileAsync("./a/b/c.txt", …)` works without a separate `ensureDirectory("./a/b")` first.
|
|
302
|
+
- **Deletes are ENOENT-safe.** `unlinkAsync` and `removeDirectoryAsync` no-op on missing targets. Other errors (`EACCES`, `EBUSY`) still throw.
|
|
303
|
+
|
|
304
|
+
## Skills index
|
|
305
|
+
|
|
306
|
+
Four task skills cover the surface. Load the one that matches what you're trying to do — don't load all four unless you're touring the package.
|
|
307
|
+
|
|
308
|
+
### [`read-and-write-files/SKILL.md`](@warlock.js/fs/read-and-write-files/SKILL.md)
|
|
309
|
+
|
|
310
|
+
Read and write text or JSON files; check existence and metadata. Covers
|
|
311
|
+
`getFile` / `getFileAsync` / `getJsonFile` / `getJsonFileAsync`,
|
|
312
|
+
`putFile` / `putFileAsync` / `putJsonFile` / `putJsonFileAsync`,
|
|
313
|
+
`pathExists` / `fileExists` / `directoryExists`,
|
|
314
|
+
`lastModified` / `stats`.
|
|
315
|
+
|
|
316
|
+
Load when reading or writing text or JSON, or gating creation on existence.
|
|
317
|
+
|
|
318
|
+
### [`manage-directories/SKILL.md`](@warlock.js/fs/manage-directories/SKILL.md)
|
|
319
|
+
|
|
320
|
+
Create, list, copy, move, and delete directories and files. Covers
|
|
321
|
+
`ensureDirectory(Async)`, `list(Async)` / `listFiles(Async)` / `listDirectories(Async)`,
|
|
322
|
+
`copyFile(Async)` / `copyDirectory(Async)`, `renameFile(Async)`,
|
|
323
|
+
`unlink(Async)`, `removeDirectory(Async)`.
|
|
324
|
+
|
|
325
|
+
Load when scaffolding, walking trees, snapshotting, cleaning, or moving files around.
|
|
326
|
+
|
|
327
|
+
### [`write-atomically/SKILL.md`](@warlock.js/fs/write-atomically/SKILL.md)
|
|
328
|
+
|
|
329
|
+
Write files so concurrent readers never see a half-written state. Covers
|
|
330
|
+
`atomicWriteAsync(path, content)` and `atomicWriteJsonAsync(path, value)`.
|
|
331
|
+
Sibling temp file + atomic rename — last-writer-wins on contention, no locking.
|
|
332
|
+
|
|
333
|
+
Load when writing a file that other processes / file watchers / build steps consume in parallel (config, manifest, state, lockfile).
|
|
334
|
+
|
|
335
|
+
### [`hash-files/SKILL.md`](@warlock.js/fs/hash-files/SKILL.md)
|
|
336
|
+
|
|
337
|
+
Compute hex digests for files (streaming) or in-memory content. Covers
|
|
338
|
+
`hashFile(Async)` (streaming — constant memory),
|
|
339
|
+
`hashFileSmallAsync` (one-shot for small files),
|
|
340
|
+
`hashString`, `hashBuffer`, plus the `HashAlgorithm` type.
|
|
341
|
+
Defaults to SHA-256; supports SHA-1 / MD5 / SHA-512.
|
|
342
|
+
|
|
343
|
+
Load when fingerprinting for cache invalidation, content-addressable storage, change detection, or file-equality comparison. Never for security (password hashing, signing).
|
|
344
|
+
|
|
345
|
+
## What this package deliberately doesn't do
|
|
346
|
+
|
|
347
|
+
- **Globbing.** Use `tinyglobby` / `fast-glob`. Adding a glob engine here would double the surface for one use case.
|
|
348
|
+
- **Watching.** Use `chokidar` or `node:fs.watch` directly. Watchers have their own lifecycle that doesn't fit the one-shot utility shape.
|
|
349
|
+
- **Permissions / chmod / chown.** Out of scope. Reach for `node:fs/promises`'s `chmod` / `chown` directly when you need them.
|
|
350
|
+
- **Streaming pipelines beyond hashing.** This isn't a general streams library; it's a wrapper for the common one-shot file operations.
|
|
351
|
+
|
|
352
|
+
## See also
|
|
353
|
+
|
|
354
|
+
- [`@warlock.js/core/warlock-conventions/SKILL.md`](@warlock.js/core/warlock-conventions/SKILL.md) — the parent framework's conventions; `fs` is one of its foundation packages.
|
|
355
|
+
- `mongez-agent-kit-authoring-skills` (load via agent-kit sync) — how this `overview/SKILL.md` becomes the front-door skill in `.claude/skills/warlock-js-fs-overview/`.
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
## read-and-write-files `@warlock.js/fs/read-and-write-files/SKILL.md`
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
name: read-and-write-files
|
|
362
|
+
description: 'Read and write files — getFile / getFileAsync / getJsonFile / putFile (auto-creates parent dirs), plus pathExists / fileExists / directoryExists / lastModified / stats. Triggers: `getFileAsync`, `getJsonFileAsync`, `putFileAsync`, `putJsonFileAsync`, `pathExists`, `fileExists`, `lastModifiedAsync`, `statsAsync`; "read a text file", "write a JSON file", "check if file exists"; typical import `import { getFileAsync, putJsonFileAsync, fileExists } from "@warlock.js/fs"`. Skip: atomic writes — `@warlock.js/fs/write-atomically/SKILL.md`; dirs + copy + delete — `@warlock.js/fs/manage-directories/SKILL.md`; hashing — `@warlock.js/fs/hash-files/SKILL.md`; competing libs `fs-extra`, `jsonfile`, `graceful-fs`; native `node:fs/promises`.'
|
|
363
|
+
---
|
|
364
|
+
|
|
365
|
+
# Read and write files
|
|
366
|
+
|
|
367
|
+
Thin, opinionated wrapper around `node:fs` and `node:fs/promises`. Two-suffix convention: `*Async` returns a Promise, the bare name is synchronous. No `Sync` suffix on the sync calls — that would mean you have to remember the inverse.
|
|
368
|
+
|
|
369
|
+
## Install
|
|
370
|
+
|
|
371
|
+
```bash
|
|
372
|
+
yarn add @warlock.js/fs
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
## Read
|
|
376
|
+
|
|
377
|
+
```ts
|
|
378
|
+
import { getFile, getFileAsync, getJsonFile, getJsonFileAsync } from "@warlock.js/fs";
|
|
379
|
+
|
|
380
|
+
// UTF-8 text
|
|
381
|
+
const config = await getFileAsync("./config.toml");
|
|
382
|
+
const sync = getFile("./config.toml");
|
|
383
|
+
|
|
384
|
+
// Parsed JSON, generic-typed
|
|
385
|
+
type Manifest = { version: string; files: string[] };
|
|
386
|
+
const manifest = await getJsonFileAsync<Manifest>("./manifest.json");
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
Behavior:
|
|
390
|
+
- All read functions return UTF-8 strings (or parsed JSON in the `JsonFile` variants).
|
|
391
|
+
- Throws if the file doesn't exist (`ENOENT`) or JSON is invalid. Don't try/catch for "file might not be there" — use `pathExists` / `fileExists` below.
|
|
392
|
+
|
|
393
|
+
## Write
|
|
394
|
+
|
|
395
|
+
```ts
|
|
396
|
+
import { putFile, putFileAsync, putJsonFile, putJsonFileAsync } from "@warlock.js/fs";
|
|
397
|
+
|
|
398
|
+
await putFileAsync("./dist/output.txt", "hello world");
|
|
399
|
+
await putJsonFileAsync("./dist/manifest.json", { version: "1.0.0" });
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
Behavior:
|
|
403
|
+
- Parent directories are created recursively — no need to `ensureDirectory` first.
|
|
404
|
+
- JSON variants pretty-print at 2-space indent. For minified output, stringify yourself and use the plain `putFile`.
|
|
405
|
+
- Overwrites existing files. For atomic write semantics (readers never see a half-written file), use [`write-atomically`](@warlock.js/fs/write-atomically/SKILL.md).
|
|
406
|
+
|
|
407
|
+
## Existence checks
|
|
408
|
+
|
|
409
|
+
Three variants — pick the strictest one that fits the question you're asking. Each has an async (`*Async`) and a sync form:
|
|
410
|
+
|
|
411
|
+
```ts
|
|
412
|
+
import { pathExistsAsync, fileExistsAsync, directoryExistsAsync } from "@warlock.js/fs";
|
|
413
|
+
|
|
414
|
+
await pathExistsAsync("./anything"); // true if file OR directory
|
|
415
|
+
await fileExistsAsync("./config.toml"); // true ONLY if a regular file
|
|
416
|
+
await directoryExistsAsync("./dist"); // true ONLY if a directory
|
|
417
|
+
|
|
418
|
+
// sync counterparts, same semantics
|
|
419
|
+
import { pathExists, fileExists, directoryExists } from "@warlock.js/fs";
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
Match the check to the target: gate a directory operation (listing, walking) on `directoryExistsAsync`, not `fileExistsAsync` — the latter resolves `false` for a folder and would skip it entirely. Reach for `pathExistsAsync` only when the type genuinely doesn't matter.
|
|
423
|
+
|
|
424
|
+
`fileExists*` and `directoryExists*` follow symlinks (they're `stat`-based, not `lstat`). Use them to gate creation logic instead of try-catching a `read`:
|
|
425
|
+
|
|
426
|
+
```ts
|
|
427
|
+
// ✅ Clearer intent
|
|
428
|
+
if (!(await fileExists("./config.toml"))) {
|
|
429
|
+
await putFileAsync("./config.toml", defaultConfig);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// ❌ Don't catch ENOENT as control flow
|
|
433
|
+
try {
|
|
434
|
+
await getFileAsync("./config.toml");
|
|
435
|
+
} catch {
|
|
436
|
+
await putFileAsync("./config.toml", defaultConfig);
|
|
437
|
+
}
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
## Metadata
|
|
441
|
+
|
|
442
|
+
```ts
|
|
443
|
+
import { lastModified, stats } from "@warlock.js/fs";
|
|
444
|
+
|
|
445
|
+
const mtime = await lastModifiedAsync("./bundle.js"); // Date
|
|
446
|
+
const all = await statsAsync("./bundle.js"); // fs.Stats
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
`lastModified` is sugar around `stat().mtime`. Reach for `stats` when you need size, mode bits, or other fields.
|
|
450
|
+
|
|
451
|
+
## When to pick sync vs async
|
|
452
|
+
|
|
453
|
+
- **Async by default** — everything in a Warlock server / app runtime should be async. The event loop stays free.
|
|
454
|
+
- **Sync only in CLI tools and config-loaders that run once** — startup config, code generators, scripts. The blocking call is fine when there's nothing else to do.
|
|
455
|
+
|
|
456
|
+
## See also
|
|
457
|
+
|
|
458
|
+
- [`@warlock.js/fs/manage-directories/SKILL.md`](@warlock.js/fs/manage-directories/SKILL.md) — directory listing, copying, removing, renaming
|
|
459
|
+
- [`@warlock.js/fs/write-atomically/SKILL.md`](@warlock.js/fs/write-atomically/SKILL.md) — safe writes for files that other readers depend on
|
|
460
|
+
- [`@warlock.js/fs/hash-files/SKILL.md`](@warlock.js/fs/hash-files/SKILL.md) — fingerprinting files
|
|
461
|
+
|
|
462
|
+
## Things NOT to do
|
|
463
|
+
|
|
464
|
+
- Don't call `putFileAsync` on a file that other processes / readers consume in parallel — use `atomicWriteAsync` from [`write-atomically`](@warlock.js/fs/write-atomically/SKILL.md) instead.
|
|
465
|
+
- Don't rely on `try { getFileAsync(...) } catch` for existence checks — `fileExists` is faster and reads better.
|
|
466
|
+
- Don't pass binary content to `putFile` / `putFileAsync` as a `Buffer` directly — these are text-only (UTF-8). For binaries, use `node:fs/promises`'s `writeFile` directly, or use `atomicWriteAsync` (which accepts `string | Buffer`).
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
## write-atomically `@warlock.js/fs/write-atomically/SKILL.md`
|
|
470
|
+
|
|
471
|
+
---
|
|
472
|
+
name: write-atomically
|
|
473
|
+
description: 'Atomic file writes via atomicWriteAsync(path, content) — writes to a uniquely-named sibling temp + rename onto target so readers see old or complete new content, never half-written. Triggers: `atomicWriteAsync`, `atomicWriteJsonAsync`; "atomic file write", "write config file safely with concurrent readers", "manifest written by build step", "state file across runs", "avoid half-written files"; typical import `import { atomicWriteAsync, atomicWriteJsonAsync } from "@warlock.js/fs"`. Skip: plain writes — `@warlock.js/fs/read-and-write-files/SKILL.md`; read-modify-write locking — `@warlock.js/cache/use-cache-lock/SKILL.md`; competing libs `write-file-atomic`, `steno`, `fs-extra` `outputFile`.'
|
|
474
|
+
---
|
|
475
|
+
|
|
476
|
+
# Atomic file writes
|
|
477
|
+
|
|
478
|
+
`atomicWriteAsync` is the safe replacement for `putFileAsync` when readers can see the file at any moment. Same parent-directory auto-creation; the difference is the write strategy.
|
|
479
|
+
|
|
480
|
+
## Why use it
|
|
481
|
+
|
|
482
|
+
`putFileAsync` writes directly to the destination. If a reader picks the file up while you're partway through the write, they see truncated content. That's fine for ephemeral logs; not fine for:
|
|
483
|
+
|
|
484
|
+
- **Config files watched by a dev server / linter.** Half-written config makes the watcher emit a spurious error.
|
|
485
|
+
- **Manifests consumed by another process.** Two-process pipelines deserialize and crash on partial JSON.
|
|
486
|
+
- **State files between runs of the same script.** A crash mid-write leaves you with a corrupt file you can't read on the next run.
|
|
487
|
+
|
|
488
|
+
`atomicWriteAsync` writes to a uniquely-named sibling temp file first, then `rename`s it onto the target. On most filesystems the rename is atomic — readers see the old content, then the new content, never anything in between.
|
|
489
|
+
|
|
490
|
+
## Shape
|
|
491
|
+
|
|
492
|
+
```ts
|
|
493
|
+
import { atomicWriteAsync, atomicWriteJsonAsync } from "@warlock.js/fs";
|
|
494
|
+
|
|
495
|
+
await atomicWriteAsync("./config.toml", configString);
|
|
496
|
+
await atomicWriteAsync("./binary.bin", Buffer.from([0x01, 0x02])); // accepts string OR Buffer
|
|
497
|
+
|
|
498
|
+
// JSON sugar — pretty-prints at 2-space indent
|
|
499
|
+
await atomicWriteJsonAsync("./manifest.json", { version: "1.0.0", files: [...] });
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
## What happens internally
|
|
503
|
+
|
|
504
|
+
```
|
|
505
|
+
1. mkdir(dir, recursive)
|
|
506
|
+
2. tempPath = `${dir}/.${name}.${randomHex(6)}.tmp` ← unique sibling temp
|
|
507
|
+
3. writeFile(tempPath, content)
|
|
508
|
+
4. rename(tempPath, filePath) ← atomic on POSIX, near-atomic on NTFS
|
|
509
|
+
on failure: unlink(tempPath)
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
The random 6-byte suffix prevents two concurrent writers from racing on the same temp file. The temp file lives in the **same directory** as the target so the rename is intra-mount (cross-mount rename would fall back to copy + unlink, which isn't atomic).
|
|
513
|
+
|
|
514
|
+
## Concurrent writers
|
|
515
|
+
|
|
516
|
+
Two `atomicWriteAsync` calls to the same target serialize at the rename. Whichever rename completes last wins. **No locking** — last-writer-wins is the contract.
|
|
517
|
+
|
|
518
|
+
If you need read-modify-write atomicity (each writer sees the previous writer's result), wrap the calls in a distributed lock — e.g. [`@warlock.js/cache/use-cache-lock/SKILL.md`](@warlock.js/cache/use-cache-lock/SKILL.md).
|
|
519
|
+
|
|
520
|
+
## Common shapes
|
|
521
|
+
|
|
522
|
+
### State file written across multiple runs
|
|
523
|
+
|
|
524
|
+
```ts
|
|
525
|
+
// On every successful run
|
|
526
|
+
await atomicWriteJsonAsync("./.cache/last-run.json", {
|
|
527
|
+
finishedAt: new Date().toISOString(),
|
|
528
|
+
buildId: process.env.BUILD_ID,
|
|
529
|
+
});
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
Crash partway through? Either the file has the previous run's content or the new run's content. Never garbage.
|
|
533
|
+
|
|
534
|
+
### Manifest emitted by a build step
|
|
535
|
+
|
|
536
|
+
```ts
|
|
537
|
+
const manifest = computeManifest(files);
|
|
538
|
+
await atomicWriteJsonAsync("./dist/manifest.json", manifest);
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
A reader (CDN purge script, deployment tool) that picks up `dist/manifest.json` while the build is mid-write doesn't crash.
|
|
542
|
+
|
|
543
|
+
### Config file watched by a dev server
|
|
544
|
+
|
|
545
|
+
```ts
|
|
546
|
+
const config = transformConfig(input);
|
|
547
|
+
await atomicWriteAsync("./config.toml", config);
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
The dev server's file watcher fires once after the rename, sees complete content. No double-event or partial-content noise.
|
|
551
|
+
|
|
552
|
+
## What it doesn't protect against
|
|
553
|
+
|
|
554
|
+
- **Filesystem corruption.** Power-loss between `writeFile` and `rename` leaves the temp file behind — that's `fsync` territory, not handled here. For ironclad durability, you'd need a `writeFile + fsync + rename + fsync(parent)` sequence; this helper skips the fsyncs for write speed.
|
|
555
|
+
- **Cross-filesystem renames.** If `dir` is a different mount from the target's actual storage (unusual), `rename` may fall back to copy + delete, which isn't atomic. Keep the temp on the same mount — the helper does this automatically.
|
|
556
|
+
- **Race conditions in your callers.** `atomicWriteAsync` makes the file write atomic; it doesn't serialize callers. Two callers stomping each other is your problem, not the helper's.
|
|
557
|
+
|
|
558
|
+
## See also
|
|
559
|
+
|
|
560
|
+
- [`@warlock.js/fs/read-and-write-files/SKILL.md`](@warlock.js/fs/read-and-write-files/SKILL.md) — `putFileAsync` for non-atomic writes
|
|
561
|
+
- [`@warlock.js/cache/use-cache-lock/SKILL.md`](@warlock.js/cache/use-cache-lock/SKILL.md) — distributed lock for read-modify-write protection
|
|
562
|
+
|
|
563
|
+
## Things NOT to do
|
|
564
|
+
|
|
565
|
+
- Don't use `atomicWriteAsync` when you don't need atomicity — `putFileAsync` is slightly faster (no rename round-trip). For ephemeral files, plain write is fine.
|
|
566
|
+
- Don't store the temp file outside the target directory. The helper picks the same dir on purpose so the rename is intra-mount; if you reach inside the source and change that, you lose the atomicity guarantee on cross-mount setups.
|
|
567
|
+
- Don't pair atomic writes with locked reads expecting consistency. A reader between the rename and your next write sees the intermediate complete state — that's the point of the helper. If you want every read to see a particular write, serialize with a lock.
|
|
568
|
+
- Don't sync after atomic write expecting "definitely persisted to disk" — the helper doesn't fsync. For durability guarantees, fsync the parent directory after the rename yourself.
|
|
569
|
+
|
|
570
|
+
|
package/llms.txt
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Warlock FS
|
|
2
|
+
|
|
3
|
+
> Package: `@warlock.js/fs`
|
|
4
|
+
|
|
5
|
+
> Filesystem primitives for Warlock.js — drop-in replacement for @warlock.js/fs.
|
|
6
|
+
|
|
7
|
+
## Skills
|
|
8
|
+
|
|
9
|
+
- [hash-files](@warlock.js/fs/hash-files/SKILL.md): Compute hex digests — hashFile / hashFileAsync (streaming for large files), hashFileSmallAsync, hashString, hashBuffer. Defaults to SHA-256; supports sha1 / md5 / sha512. Triggers: `hashFile`, `hashFileAsync`, `hashFileSmallAsync`, `hashString`, `hashBuffer`, `HashAlgorithm`; "fingerprint a file for cache invalidation", "compute SHA-256 checksum", "compare two files for equality", "cache key from request input"; typical import `import { hashFileAsync, hashString } from "@warlock.js/fs"`. Skip: file IO — `@warlock.js/fs/read-and-write-files/SKILL.md`; competing libs `hasha`, `md5-file`, `crypto-hash`; native `node:crypto` `createHash`.
|
|
10
|
+
- [manage-directories](@warlock.js/fs/manage-directories/SKILL.md): Manage directories + files — ensureDirectory (mkdir -p), list / listFiles / listDirectories, copyFile / copyDirectory, renameFile, unlink (ENOENT-safe), removeDirectory (recursive force). Triggers: `ensureDirectoryAsync`, `listAsync`, `listFilesAsync`, `listDirectoriesAsync`, `copyFileAsync`, `copyDirectoryAsync`, `renameFileAsync`, `unlinkAsync`, `removeDirectoryAsync`; "create directory recursively", "list files in folder", "delete directory recursively", "copy or rename folder", "walk a tree"; typical import `import { ensureDirectoryAsync, listFilesAsync, removeDirectoryAsync } from "@warlock.js/fs"`. Skip: file IO — `@warlock.js/fs/read-and-write-files/SKILL.md`; atomic writes — `@warlock.js/fs/write-atomically/SKILL.md`; competing libs `fs-extra`, `mkdirp`, `rimraf`, `recursive-readdir`, `glob`; native `node:fs/promises`.
|
|
11
|
+
- [overview](@warlock.js/fs/overview/SKILL.md): Front-door orientation for `@warlock.js/fs` — filesystem primitives (read/write/JSON, dirs, copy/rename/delete, atomic writes, hashing, existence + stats). Two-suffix convention: `*Async` returns Promise, bare name is sync. Single canonical name per operation — no aliases. TRIGGER when: code imports anything from `@warlock.js/fs`; user asks "what does @warlock.js/fs do", "is fs the right package for X", "list all fs helpers", "fs sync vs async convention"; package.json adds `@warlock.js/fs`; user is choosing between fs vs `node:fs/promises`/`fs-extra`/`graceful-fs`. Skip: specific task already known — load the matching task skill directly (`@warlock.js/fs/read-and-write-files/SKILL.md`, `@warlock.js/fs/manage-directories/SKILL.md`, `@warlock.js/fs/write-atomically/SKILL.md`, `@warlock.js/fs/hash-files/SKILL.md`); the user is using plain `node:fs` and not touching fs imports.
|
|
12
|
+
- [read-and-write-files](@warlock.js/fs/read-and-write-files/SKILL.md): Read and write files — getFile / getFileAsync / getJsonFile / putFile (auto-creates parent dirs), plus pathExists / fileExists / directoryExists / lastModified / stats. Triggers: `getFileAsync`, `getJsonFileAsync`, `putFileAsync`, `putJsonFileAsync`, `pathExists`, `fileExists`, `lastModifiedAsync`, `statsAsync`; "read a text file", "write a JSON file", "check if file exists"; typical import `import { getFileAsync, putJsonFileAsync, fileExists } from "@warlock.js/fs"`. Skip: atomic writes — `@warlock.js/fs/write-atomically/SKILL.md`; dirs + copy + delete — `@warlock.js/fs/manage-directories/SKILL.md`; hashing — `@warlock.js/fs/hash-files/SKILL.md`; competing libs `fs-extra`, `jsonfile`, `graceful-fs`; native `node:fs/promises`.
|
|
13
|
+
- [write-atomically](@warlock.js/fs/write-atomically/SKILL.md): Atomic file writes via atomicWriteAsync(path, content) — writes to a uniquely-named sibling temp + rename onto target so readers see old or complete new content, never half-written. Triggers: `atomicWriteAsync`, `atomicWriteJsonAsync`; "atomic file write", "write config file safely with concurrent readers", "manifest written by build step", "state file across runs", "avoid half-written files"; typical import `import { atomicWriteAsync, atomicWriteJsonAsync } from "@warlock.js/fs"`. Skip: plain writes — `@warlock.js/fs/read-and-write-files/SKILL.md`; read-modify-write locking — `@warlock.js/cache/use-cache-lock/SKILL.md`; competing libs `write-file-atomic`, `steno`, `fs-extra` `outputFile`.
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@warlock.js/fs",
|
|
3
|
+
"description": "Filesystem primitives for Warlock.js — drop-in replacement for @warlock.js/fs.",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "https://github.com/warlockjs/fs"
|
|
7
|
+
},
|
|
8
|
+
"version": "4.1.1",
|
|
9
|
+
"main": "./cjs/index.cjs",
|
|
10
|
+
"module": "./esm/index.mjs",
|
|
11
|
+
"types": "./esm/index.d.mts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"import": {
|
|
15
|
+
"types": "./esm/index.d.mts",
|
|
16
|
+
"default": "./esm/index.mjs"
|
|
17
|
+
},
|
|
18
|
+
"require": {
|
|
19
|
+
"types": "./esm/index.d.mts",
|
|
20
|
+
"default": "./cjs/index.cjs"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|