@artemjs/vfskit 1.1.0

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 ADDED
@@ -0,0 +1,219 @@
1
+ <p align="center">
2
+ <img src="https://cdn.jsdelivr.net/gh/artemjs/vfskit@6dffa9f/assets/logo.svg" alt="vfskit" width="160" height="160">
3
+ </p>
4
+
5
+ <h1 align="center">vfskit</h1>
6
+
7
+ <p align="center">
8
+ <img src="https://img.shields.io/badge/version-1.1.0-7c8cff?style=flat-square" alt="version">
9
+ <img src="https://img.shields.io/badge/license-MIT-56e6c4?style=flat-square" alt="license">
10
+ <img src="https://img.shields.io/badge/TypeScript-strict-3178c6?style=flat-square" alt="typescript">
11
+ <img src="https://img.shields.io/badge/module-ESM-f0db4f?style=flat-square" alt="esm">
12
+ <img src="https://img.shields.io/badge/runtime%20deps-0-1f9d55?style=flat-square" alt="zero deps">
13
+ </p>
14
+
15
+ <p align="center">
16
+ <b>One <code>VFS</code> interface over any backend</b>: in-memory, real disk, S3, or your own.<br>
17
+ Composable adapters, encryption, caching, optimistic concurrency, and a browser&nbsp;&#8646;&nbsp;server bridge.
18
+ </p>
19
+
20
+ ---
21
+
22
+ vfskit wraps any kind of storage behind a single, small `VFS` interface, then lets you
23
+ **compose** behavior on top of it - encryption, caching, a remote bridge - and drive it from
24
+ the browser exactly as you would on the server. Anything you can read, write, and list
25
+ becomes a structured file system with files and metadata.
26
+
27
+ It ships in two faces under one brand:
28
+
29
+ - **`/vfskit`** (npm, Node) - the full kit: core + memory + node-fs + s3 + encrypt + cache + serve + remote.
30
+ - **`/vfskit-front`** (npm + jsDelivr, browser) - core + memory + encrypt + cache + a remote client.
31
+
32
+ Both expose **identical API names**, so your code looks the same on either side.
33
+
34
+ ## Install
35
+
36
+ ```sh
37
+ npm i @artemjs/vfskit # backend / Node
38
+ ```
39
+
40
+ ```js
41
+ // browser, no build step
42
+ import { remote, wsTransport } from 'https://cdn.jsdelivr.net/npm/@artemjs/vfskit-front/+esm'
43
+ ```
44
+
45
+ ## Everything is a VFS
46
+
47
+ <p align="center">
48
+ <img src="https://cdn.jsdelivr.net/gh/artemjs/vfskit@6dffa9f/assets/architecture.svg" alt="vfskit architecture" width="820">
49
+ </p>
50
+
51
+ ```ts
52
+ interface VFS {
53
+ read(path): Promise<Uint8Array>
54
+ write(path, data, opts?): Promise<void>
55
+ list(path, opts?): Promise<Entry[]>
56
+ stat(path): Promise<Stat>
57
+ exists(path): Promise<boolean>
58
+ mkdir(path, opts?): Promise<void>
59
+ remove(path, opts?): Promise<void>
60
+ move(from, to): Promise<void>
61
+ copy(from, to): Promise<void>
62
+ getMeta(path): Promise<Meta>
63
+ setMeta(path, meta): Promise<void>
64
+ watch(path, cb): Unsubscribe
65
+ capabilities(): Capabilities
66
+ }
67
+ ```
68
+
69
+ - **Adapters** implement `VFS` over a backend: `memory()`, `nodeFs(dir)`, `s3({ client })`.
70
+ - **Middleware** wraps a `VFS` and returns a `VFS`: `encrypt(vfs, { passphrase })`, `cache(vfs)`.
71
+ - **Bridge** connects them across the wire: `serve(vfs)` on the server, `remote(transport)` on the client.
72
+
73
+ Compose freely. `encrypt(remote(transport))` is end-to-end encryption - the server only ever
74
+ stores ciphertext.
75
+
76
+ ## Quick start
77
+
78
+ ```ts
79
+ import { memory, nodeFs, encrypt, serve, remote, toText } from '@artemjs/vfskit'
80
+
81
+ const store = encrypt(nodeFs('./data'), { passphrase: 'hunter2' })
82
+ await store.write('/notes/todo.md', '# buy milk', { meta: { tag: 'home' } })
83
+ console.log(toText(await store.read('/notes/todo.md')))
84
+ ```
85
+
86
+ Expose a backend, drive it from anywhere:
87
+
88
+ ```ts
89
+ // server
90
+ const server = serve(nodeFs('./data'))
91
+ // wire server.fetch (HTTP) or server.socket (WebSocket) into your runtime
92
+ ```
93
+
94
+ ```ts
95
+ // client (browser or Node)
96
+ import { remote, wsTransport } from '@artemjs/vfskit-front'
97
+ const fs = remote(wsTransport('ws://localhost:3000'))
98
+ await fs.write('/hello.txt', 'hi')
99
+ ```
100
+
101
+ ## Adapters
102
+
103
+ | Adapter | Where | Metadata | Notes |
104
+ | --- | --- | --- | --- |
105
+ | `memory()` | anywhere | native | reference implementation; synchronous `watch` |
106
+ | `nodeFs(dir)` | Node | sidecar `.vfskit/meta.json` | rooted at `dir`; native streaming; `watch` via `fs.watch` |
107
+ | `s3({ client, prefix?, pollMs? })` | Node | native object metadata | inject any `S3Like` client; POSIX dirs emulated with markers; `watch` by polling |
108
+
109
+ Every adapter passes the same conformance suite, so a new one "just works" once it does too.
110
+
111
+ ## Bring your own storage
112
+
113
+ vfskit is just an interface. To put *any* backend behind the same API, write a function that
114
+ returns a `VFS` - a plain object literal implementing the methods above - over your store
115
+ (a database, a KV cache, `localStorage`, a blob service, whatever):
116
+
117
+ ```ts
118
+ import { type VFS, normalize, toBytes, notFound } from '@artemjs/vfskit'
119
+
120
+ export function myVfs(store: MyStore): VFS {
121
+ return {
122
+ capabilities: () => ({ streaming: false, watch: false, atomicMove: false, nativeMeta: true, randomAccess: false, conditionalWrite: false }),
123
+ async read(path) { /* ... */ },
124
+ async write(path, data, opts) { /* ... */ },
125
+ // ...the rest of the interface
126
+ }
127
+ }
128
+ ```
129
+
130
+ Then validate it against the exact same battery every built-in adapter must pass:
131
+
132
+ ```ts
133
+ import { conformanceCases } from '@artemjs/vfskit/conformance'
134
+ import { describe, it } from 'vitest'
135
+
136
+ describe('my adapter', () => {
137
+ for (const c of conformanceCases) it(c.name, () => c.run(() => myVfs(new MyStore())))
138
+ })
139
+ ```
140
+
141
+ If it passes, your storage now works everywhere vfskit works - behind `encrypt(...)`, behind
142
+ `serve(...)`, driven by a browser `remote(...)`. A complete worked example (a key-value
143
+ backend) lives in [`examples/custom-adapter`](examples/custom-adapter). `conformanceCases` is
144
+ framework-agnostic (`{ name, run(makeVfs) }[]`), so you can drive it from any test runner.
145
+
146
+ ## Encryption
147
+
148
+ AES-256-GCM via WebCrypto. A raw key, or a passphrase derived per file with PBKDF2 (random
149
+ salt, 210k iterations). Tamper fails closed with a typed error. Content is encrypted by
150
+ default; metadata stays as the backend stores it.
151
+
152
+ ```ts
153
+ const vault = encrypt(memory(), { passphrase: 'open sesame' })
154
+ ```
155
+
156
+ ## Caching
157
+
158
+ `cache(vfs, { ttlMs? })` serves reads from an in-memory store (write-through,
159
+ subtree-invalidated on write/remove/move/copy). Wrap a `remote(...)` to avoid round-trips for
160
+ hot files:
161
+
162
+ ```ts
163
+ import { cache, remote, wsTransport } from '@artemjs/vfskit-front'
164
+ const fs = cache(remote(wsTransport(url)), { ttlMs: 5000 })
165
+ ```
166
+
167
+ Pass your own `store` to back the cache with anything (e.g. `localStorage`).
168
+
169
+ ## Concurrent writes
170
+
171
+ Adapters that report `conditionalWrite` give every file an opaque `version` token (via
172
+ `stat`). Pass it back as `ifMatch` to make a write succeed only if nobody changed the file in
173
+ between - otherwise it fails with a typed `CONFLICT`. `ifAbsent` makes a create-only write.
174
+
175
+ ```ts
176
+ const { version } = await fs.stat('/doc')
177
+ await fs.write('/doc', next, { ifMatch: version }) // CONFLICT if it moved on
178
+ await fs.write('/new', data, { ifAbsent: true }) // ALREADY_EXISTS if it exists
179
+ ```
180
+
181
+ Supported by `memory`, `nodeFs`, `s3`, and transparently over `remote(...)`.
182
+
183
+ ## Streaming
184
+
185
+ `readStream(vfs, path)` and `writeStream(vfs, path)` give Web `ReadableStream` /
186
+ `WritableStream` over any adapter - native where supported (`nodeFs` streams real file
187
+ handles), buffered otherwise, so the API is uniform:
188
+
189
+ ```ts
190
+ import { readStream, writeStream, collect, toBytes } from '@artemjs/vfskit'
191
+
192
+ const w = (await writeStream(fs, '/big.log')).getWriter()
193
+ await w.write(toBytes('line 1\n')); await w.close()
194
+ const all = await collect(await readStream(fs, '/big.log'))
195
+ ```
196
+
197
+ `encrypt` and `cache` buffer through their own `read`/`write`, so streaming stays correct
198
+ behind them (the stream still yields plaintext; the disk still holds ciphertext).
199
+
200
+ ## Transports
201
+
202
+ - `httpTransport(url)` - request/response; works on serverless/edge. No `watch`.
203
+ - `wsTransport(url)` - multiplexed; enables `watch`/events.
204
+
205
+ ## Errors
206
+
207
+ Typed hierarchy with stable wire codes, reconstructed on the client across the bridge:
208
+ `NOT_FOUND`, `ALREADY_EXISTS`, `NOT_A_DIRECTORY`, `IS_A_DIRECTORY`, `PERMISSION_DENIED`,
209
+ `UNSUPPORTED`, `CONFLICT`, `IO`. Detect with `isVfsError(e)` (brand-based - survives bundling
210
+ and the RPC round-trip).
211
+
212
+ ## Example
213
+
214
+ [`examples/cloud-ide`](examples/cloud-ide) - Monaco editing files on a real-disk VFS over a
215
+ WebSocket bridge, with per-user isolation. Swapping the backend to S3 is one line.
216
+
217
+ ## License
218
+
219
+ [MIT](LICENSE)
@@ -0,0 +1,10 @@
1
+ import { V as VFS } from './types-CMveBA-B.js';
2
+
3
+ interface ConformanceCase {
4
+ name: string;
5
+ run(make: () => VFS): Promise<void>;
6
+ }
7
+ declare const conformanceCases: ConformanceCase[];
8
+ declare function runConformance(make: () => VFS): void;
9
+
10
+ export { type ConformanceCase, conformanceCases, runConformance };
@@ -0,0 +1 @@
1
+ var h=new TextEncoder,b=new TextDecoder;function w(e){return typeof e=="string"?h.encode(e):e instanceof Uint8Array?e:new Uint8Array(e)}function s(e){return b.decode(e)}function d(e){let t=0;for(let i of e)t+=i.length;let a=new Uint8Array(t),n=0;for(let i of e)a.set(i,n),n+=i.length;return a}var m=Symbol.for("vfskit.VfsError"),f=class extends Error{code;path;[m]=!0;constructor(t,a,n){super(a),this.name="VfsError",this.code=t,this.path=n}};function u(e){return typeof e=="object"&&e!==null&&e[m]===!0}async function y(e,t,a){if(e.readStream)return e.readStream(t,a);let n=await e.read(t,a);return new ReadableStream({start(i){i.enqueue(n),i.close()}})}async function p(e,t,a){if(e.writeStream)return e.writeStream(t,a);let n=[];return new WritableStream({write(i){n.push(i)},async close(){await e.write(t,d(n),a)}})}async function l(e){let t=[],a=e.getReader();for(;;){let{done:n,value:i}=await a.read();if(n)break;i&&t.push(i)}return d(t)}function x(e){throw new Error("conformance: "+e)}function c(e,t="expected truthy"){e||x(t)}function r(e,t,a){e!==t&&x(a??`expected ${JSON.stringify(t)}, got ${JSON.stringify(e)}`)}async function o(e,t){let a;try{await e()}catch(n){a=n}c(u(a)&&a.code===t,`expected error ${t}, got ${u(a)?a.code:a}`)}async function S(e){let t;try{await e()}catch(a){t=a}c(t,"expected throw")}async function E(e,t=2e3){for(let n=0;n<t;n+=10){if(e())return;await new Promise(i=>setTimeout(i,10))}}var g=[{name:"writes and reads a file",async run(e){let t=e();await t.write("/a.txt","hello"),r(s(await t.read("/a.txt")),"hello")}},{name:"reports existence",async run(e){let t=e();r(await t.exists("/a.txt"),!1),await t.write("/a.txt","x"),r(await t.exists("/a.txt"),!0)}},{name:"stats a file",async run(e){let t=e();await t.write("/a.txt","hello");let a=await t.stat("/a.txt");r(a.type,"file"),c(a.size>0)}},{name:"throws NOT_FOUND for missing read",async run(e){let t=e();await o(()=>t.read("/nope"),"NOT_FOUND")}},{name:"lists directory children",async run(e){let t=e();await t.mkdir("/d"),await t.write("/d/a","1"),await t.write("/d/b","2"),r((await t.list("/d")).map(a=>a.name).sort().join(","),"a,b")}},{name:"lists recursively",async run(e){let t=e();await t.mkdir("/d"),await t.mkdir("/d/sub"),await t.write("/d/sub/a","1"),c((await t.list("/d",{recursive:!0})).some(a=>a.path==="/d/sub/a"))}},{name:"removes a file",async run(e){let t=e();await t.write("/a","1"),await t.remove("/a"),r(await t.exists("/a"),!1)}},{name:"requires recursive to remove a non-empty dir",async run(e){let t=e();await t.mkdir("/d"),await t.write("/d/a","1"),await S(()=>t.remove("/d")),await t.remove("/d",{recursive:!0}),r(await t.exists("/d"),!1)}},{name:"moves a file",async run(e){let t=e();await t.write("/a","1"),await t.move("/a","/b"),r(await t.exists("/a"),!1),r(s(await t.read("/b")),"1")}},{name:"copies a file",async run(e){let t=e();await t.write("/a","1"),await t.copy("/a","/b"),r(s(await t.read("/a")),"1"),r(s(await t.read("/b")),"1")}},{name:"stores and reads metadata",async run(e){let t=e();await t.write("/a","1",{meta:{tag:"x"}}),r((await t.getMeta("/a")).tag,"x"),await t.setMeta("/a",{tag:"y"}),r((await t.getMeta("/a")).tag,"y")}},{name:"emits watch events when supported",async run(e){let t=e();if(!t.capabilities().watch)return;let a=[],n=t.watch("/",i=>a.push(i.type+":"+i.path));await new Promise(i=>setTimeout(i,60)),await t.write("/a","1"),await E(()=>a.includes("create:/a")),n(),c(a.includes("create:/a"))}},{name:"moves a directory subtree",async run(e){let t=e();await t.mkdir("/d"),await t.mkdir("/d/sub"),await t.write("/d/sub/a","1"),await t.move("/d","/e"),r(await t.exists("/d"),!1),r(s(await t.read("/e/sub/a")),"1")}},{name:"copies a directory subtree deeply",async run(e){let t=e();await t.mkdir("/d"),await t.write("/d/a","1"),await t.copy("/d","/e"),await t.write("/e/a","2"),r(s(await t.read("/d/a")),"1"),r(s(await t.read("/e/a")),"2")}},{name:"isolates returned read buffers from the store",async run(e){let t=e();await t.write("/a","abc");let a=await t.read("/a");a[0]=0,r(s(await t.read("/a")),"abc")}},{name:"does not confuse sibling prefixes",async run(e){let t=e();await t.mkdir("/d"),await t.mkdir("/dx"),await t.write("/dx/a","1"),c(!(await t.list("/d")).some(a=>a.name==="a"))}},{name:"reports byte size matching written content",async run(e){let t=e();await t.write("/a","hello"),r((await t.stat("/a")).size,5)}},{name:"throws ALREADY_EXISTS creating an existing dir without recursive",async run(e){let t=e();await t.mkdir("/d"),await o(()=>t.mkdir("/d"),"ALREADY_EXISTS")}},{name:"preserves metadata when overwriting content without new meta",async run(e){let t=e();await t.write("/a","1",{meta:{tag:"x"}}),await t.write("/a","2"),r((await t.getMeta("/a")).tag,"x")}},{name:"throws NOT_A_DIRECTORY creating a directory under a file",async run(e){let t=e();await t.write("/f","1"),await o(()=>t.mkdir("/f/sub",{recursive:!0}),"NOT_A_DIRECTORY")}},{name:"throws ALREADY_EXISTS moving or copying onto an existing path",async run(e){let t=e();await t.write("/a","1"),await t.write("/b","2"),await o(()=>t.move("/a","/b"),"ALREADY_EXISTS"),await o(()=>t.copy("/a","/b"),"ALREADY_EXISTS")}},{name:"conditional write succeeds on matching version and CONFLICTs on stale (when capable)",async run(e){let t=e();if(!t.capabilities().conditionalWrite)return;await t.write("/a","1");let a=(await t.stat("/a")).version;c(typeof a=="string"&&a.length>0,"expected a version token"),await t.write("/a","2",{ifMatch:a}),r(s(await t.read("/a")),"2"),await o(()=>t.write("/a","3",{ifMatch:a}),"CONFLICT"),r(s(await t.read("/a")),"2")}},{name:"ifAbsent rejects overwriting an existing file (when capable)",async run(e){let t=e();t.capabilities().conditionalWrite&&(await t.write("/a","1"),await o(()=>t.write("/a","2",{ifAbsent:!0}),"ALREADY_EXISTS"),r(s(await t.read("/a")),"1"))}},{name:"streams content in and back out (helper, native or buffered)",async run(e){let t=e(),n=(await p(t,"/s.bin")).getWriter();await n.write(w("chunk-one;")),await n.write(w("chunk-two")),await n.close(),r(s(await l(await y(t,"/s.bin"))),"chunk-one;chunk-two"),r(s(await t.read("/s.bin")),"chunk-one;chunk-two")}}];function A(e){let t=globalThis;t.describe("vfs conformance",()=>{for(let a of g)t.it(a.name,()=>a.run(e))})}export{g as conformanceCases,A as runConformance};
@@ -0,0 +1,136 @@
1
+ import { B as BytesLike, V as VFS, R as ReadOpts, W as WriteOpts, M as Meta, a as WatchCb, U as Unsubscribe, C as Capabilities } from './types-CMveBA-B.js';
2
+ export { E as Entry, F as FileType, L as ListOpts, b as MkdirOpts, c as RemoveOpts, S as Stat, d as WatchEvent } from './types-CMveBA-B.js';
3
+
4
+ declare function normalize(p: string): string;
5
+ declare function join(...parts: string[]): string;
6
+ declare function dirname(p: string): string;
7
+ declare function basename(p: string): string;
8
+ declare function segments(p: string): string[];
9
+
10
+ type ErrorCode = 'NOT_FOUND' | 'ALREADY_EXISTS' | 'NOT_A_DIRECTORY' | 'IS_A_DIRECTORY' | 'PERMISSION_DENIED' | 'UNSUPPORTED' | 'CONFLICT' | 'IO';
11
+ declare const BRAND: unique symbol;
12
+ declare class VfsError extends Error {
13
+ code: ErrorCode;
14
+ path?: string;
15
+ readonly [BRAND] = true;
16
+ constructor(code: ErrorCode, message: string, path?: string);
17
+ }
18
+ declare const notFound: (p: string) => VfsError;
19
+ declare const alreadyExists: (p: string) => VfsError;
20
+ declare const notADirectory: (p: string) => VfsError;
21
+ declare const isADirectory: (p: string) => VfsError;
22
+ declare const permissionDenied: (p: string) => VfsError;
23
+ declare const unsupported: (op: string) => VfsError;
24
+ declare const conflict: (p: string) => VfsError;
25
+ declare const io: (message: string, path?: string) => VfsError;
26
+ declare function isVfsError(e: unknown): e is VfsError;
27
+
28
+ declare function toBytes(d: BytesLike): Uint8Array;
29
+ declare function toText(d: Uint8Array): string;
30
+ declare function concat(parts: Uint8Array[]): Uint8Array;
31
+
32
+ declare function readStream(vfs: VFS, path: string, opts?: ReadOpts): Promise<ReadableStream<Uint8Array>>;
33
+ declare function writeStream(vfs: VFS, path: string, opts?: WriteOpts): Promise<WritableStream<Uint8Array>>;
34
+ declare function collect(s: ReadableStream<Uint8Array>): Promise<Uint8Array>;
35
+
36
+ declare function memory(): VFS;
37
+
38
+ declare function nodeFs(root: string): VFS;
39
+
40
+ interface S3Object {
41
+ body: Uint8Array;
42
+ meta: Meta;
43
+ size: number;
44
+ mtime: number;
45
+ version?: string;
46
+ }
47
+ interface S3Like {
48
+ get(key: string): Promise<S3Object | null>;
49
+ put(key: string, body: Uint8Array, meta: Meta): Promise<void>;
50
+ del(key: string): Promise<void>;
51
+ head(key: string): Promise<{
52
+ size: number;
53
+ mtime: number;
54
+ meta: Meta;
55
+ version?: string;
56
+ } | null>;
57
+ list(prefix: string): Promise<{
58
+ key: string;
59
+ size: number;
60
+ mtime: number;
61
+ }[]>;
62
+ }
63
+ interface S3Opts {
64
+ client: S3Like;
65
+ prefix?: string;
66
+ pollMs?: number;
67
+ }
68
+ declare function memoryS3(): S3Like;
69
+ declare function s3(opts: S3Opts): VFS;
70
+
71
+ interface EncryptOpts {
72
+ key?: Uint8Array;
73
+ passphrase?: string;
74
+ }
75
+ declare function encrypt(inner: VFS, opts: EncryptOpts): VFS;
76
+
77
+ interface CacheStore {
78
+ get(k: string): {
79
+ data: Uint8Array;
80
+ exp: number;
81
+ } | undefined;
82
+ set(k: string, v: {
83
+ data: Uint8Array;
84
+ exp: number;
85
+ }): void;
86
+ delete(k: string): void;
87
+ keys(): Iterable<string>;
88
+ }
89
+ interface CacheOpts {
90
+ store?: CacheStore;
91
+ ttlMs?: number;
92
+ }
93
+ declare function cache(inner: VFS, opts?: CacheOpts): VFS;
94
+
95
+ interface Socket {
96
+ message(bytes: Uint8Array): Promise<void>;
97
+ close(): void;
98
+ }
99
+ interface Server {
100
+ handle(bytes: Uint8Array): Promise<Uint8Array>;
101
+ fetch(req: Request): Promise<Response>;
102
+ socket(send: (bytes: Uint8Array) => void): Socket;
103
+ }
104
+ declare function serve(vfs: VFS): Server;
105
+
106
+ interface Transport {
107
+ request(bytes: Uint8Array): Promise<Uint8Array>;
108
+ watch?(path: string, cb: WatchCb): Unsubscribe;
109
+ }
110
+ interface RemoteOpts {
111
+ transport: Transport;
112
+ capabilities?: Capabilities;
113
+ }
114
+ declare function remote(opts: Transport | RemoteOpts): VFS;
115
+
116
+ type FetchLike = (url: string, init: {
117
+ method: string;
118
+ body: Uint8Array;
119
+ }) => Promise<{
120
+ arrayBuffer(): Promise<ArrayBuffer>;
121
+ }>;
122
+ declare function httpTransport(url: string, fetchImpl?: FetchLike): Transport;
123
+
124
+ interface SocketLike {
125
+ send(data: Uint8Array): void;
126
+ readyState: number;
127
+ binaryType?: string;
128
+ onmessage: ((ev: {
129
+ data: any;
130
+ }) => void) | null;
131
+ onopen?: ((ev?: any) => void) | null;
132
+ addEventListener?(type: string, cb: (ev?: any) => void): void;
133
+ }
134
+ declare function wsTransport(url: string, factory?: () => SocketLike): Transport;
135
+
136
+ export { BRAND, BytesLike, type CacheOpts, type CacheStore, Capabilities, type EncryptOpts, type ErrorCode, type FetchLike, Meta, ReadOpts, type RemoteOpts, type S3Like, type S3Object, type S3Opts, type Server, type Socket, type SocketLike, type Transport, Unsubscribe, VFS, VfsError, WatchCb, WriteOpts, alreadyExists, basename, cache, collect, concat, conflict, dirname, encrypt, httpTransport, io, isADirectory, isVfsError, join, memory, memoryS3, nodeFs, normalize, notADirectory, notFound, permissionDenied, readStream, remote, s3, segments, serve, toBytes, toText, unsupported, writeStream, wsTransport };
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ function m(n){let r=[];for(let o of n.split("/"))if(!(o===""||o===".")){if(o===".."){r.pop();continue}r.push(o)}return"/"+r.join("/")}function Et(...n){return m(n.join("/"))}function V(n){let r=m(n),o=r.lastIndexOf("/");return o<=0?"/":r.slice(0,o)}function Mt(n){let r=m(n);return r.slice(r.lastIndexOf("/")+1)}function Dt(n){return m(n).split("/").filter(Boolean)}var Y=Symbol.for("vfskit.VfsError"),b=class extends Error{code;path;[Y]=!0;constructor(r,o,i){super(o),this.name="VfsError",this.code=r,this.path=i}},v=n=>new b("NOT_FOUND",`not found: ${n}`,n),S=n=>new b("ALREADY_EXISTS",`already exists: ${n}`,n),M=n=>new b("NOT_A_DIRECTORY",`not a directory: ${n}`,n),D=n=>new b("IS_A_DIRECTORY",`is a directory: ${n}`,n),Rt=n=>new b("PERMISSION_DENIED",`permission denied: ${n}`,n),z=n=>new b("UNSUPPORTED",`unsupported: ${n}`),F=n=>new b("CONFLICT",`version conflict: ${n}`,n),x=(n,r)=>new b("IO",n,r);function Pt(n){return typeof n=="object"&&n!==null&&n[Y]===!0}var et=new TextEncoder,rt=new TextDecoder;function O(n){return typeof n=="string"?et.encode(n):n instanceof Uint8Array?n:new Uint8Array(n)}function Ct(n){return rt.decode(n)}function L(n){let r=0;for(let c of n)r+=c.length;let o=new Uint8Array(r),i=0;for(let c of n)o.set(c,i),i+=c.length;return o}async function Vt(n,r,o){if(n.readStream)return n.readStream(r,o);let i=await n.read(r,o);return new ReadableStream({start(c){c.enqueue(i),c.close()}})}async function Lt(n,r,o){if(n.writeStream)return n.writeStream(r,o);let i=[];return new WritableStream({write(c){i.push(c)},async close(){await n.write(r,L(i),o)}})}async function It(n){let r=[],o=n.getReader();for(;;){let{done:i,value:c}=await o.read();if(i)break;c&&r.push(c)}return L(r)}var nt={streaming:!1,watch:!0,atomicMove:!0,nativeMeta:!0,randomAccess:!1,conditionalWrite:!0};function at(){let n=()=>Date.now(),r=0,o=()=>String(++r),i=new Map([["/",{type:"dir",meta:{},mtime:n(),ctime:n()}]]),c=new Set,u=(e,a)=>a===e||a.startsWith(e==="/"?"/":e+"/"),l=(e,a)=>{for(let t of c)u(t.base,a)&&t.cb({type:e,path:a})},d=e=>{let a=i.get(e);if(!a)throw v(e);return a},f=e=>{let a=V(e),t=i.get(a);if(!t)throw v(a);if(t.type!=="dir")throw M(a)};return{capabilities:()=>nt,async read(e){let a=m(e),t=d(a);if(t.type==="dir")throw D(a);return t.data.slice()},async write(e,a,t){let s=m(e);f(s);let y=i.get(s);if(y&&y.type==="dir")throw D(s);if(t?.ifAbsent&&y)throw S(s);if(t?.ifMatch!==void 0&&y?.version!==t.ifMatch)throw F(s);let p=y?y.ctime:n();i.set(s,{type:"file",data:O(a).slice(),meta:t?.meta?{...t.meta}:y?.meta??{},ctime:p,mtime:n(),version:o()}),l(y?"update":"create",s)},async list(e,a){let t=m(e);if(d(t).type!=="dir")throw M(t);let y=[];for(let[p,w]of i)p===t||!u(t,p)||!a?.recursive&&V(p)!==t||y.push({name:p.slice(p.lastIndexOf("/")+1),path:p,type:w.type});return y},async stat(e){let a=m(e),t=d(a);return{type:t.type,size:t.type==="file"?t.data.length:0,mtime:t.mtime,ctime:t.ctime,meta:{...t.meta},version:t.type==="file"?t.version:void 0}},async exists(e){return i.has(m(e))},async mkdir(e,a){let t=m(e);if(i.has(t)){if(a?.recursive)return;throw S(t)}if(a?.recursive){let s="";for(let y of t.split("/").filter(Boolean)){s+="/"+y;let p=i.get(s);if(p){if(p.type!=="dir")throw M(s);continue}i.set(s,{type:"dir",meta:{},mtime:n(),ctime:n()}),l("create",s)}return}f(t),i.set(t,{type:"dir",meta:{},mtime:n(),ctime:n()}),l("create",t)},async remove(e,a){let t=m(e);d(t);let s=[...i.keys()].filter(y=>y!==t&&u(t,y));if(s.length&&!a?.recursive)throw x("directory not empty",t);for(let y of[...s,t])i.delete(y),l("remove",y)},async move(e,a){let t=m(e),s=m(a);if(d(t),f(s),i.has(s))throw S(s);if(u(t,s))throw x("cannot move into itself",s);for(let y of[...i.keys()].filter(p=>p===t||u(t,p))){let p=i.get(y);i.delete(y),i.set(s+y.slice(t.length),p)}l("remove",t),l("create",s)},async copy(e,a){let t=m(e),s=m(a);if(d(t),f(s),i.has(s))throw S(s);for(let y of[...i.keys()].filter(p=>p===t||u(t,p))){let p=i.get(y);i.set(s+y.slice(t.length),p.type==="file"?{type:"file",data:p.data.slice(),meta:{...p.meta},mtime:p.mtime,ctime:p.ctime,version:o()}:{type:"dir",meta:{...p.meta},mtime:p.mtime,ctime:p.ctime})}l("create",s)},async getMeta(e){return{...d(m(e)).meta}},async setMeta(e,a){let t=d(m(e));t.meta={...a},t.mtime=n()},watch(e,a){let t={base:m(e),cb:a};return c.add(t),()=>{c.delete(t)}}}}import{promises as g,watch as $,createReadStream as it}from"fs";import{Readable as st}from"stream";import{join as ot,dirname as C}from"path";var ct={streaming:!0,watch:!0,atomicMove:!0,nativeMeta:!1,randomAccess:!1,conditionalWrite:!0},J="/.vfskit/meta.json",K="/.vfskit/ver.json";function yt(n){let r=e=>ot(n,m(e)),o=e=>e==="/.vfskit"||e.startsWith("/.vfskit/"),i=async()=>{try{return JSON.parse(new TextDecoder().decode(await g.readFile(r(J))))}catch{return{}}},c=async e=>{await g.mkdir(C(r(J)),{recursive:!0}),await g.writeFile(r(J),new TextEncoder().encode(JSON.stringify(e)))},u=async()=>{try{return JSON.parse(new TextDecoder().decode(await g.readFile(r(K))))}catch{return{}}},l=async e=>{await g.mkdir(C(r(K)),{recursive:!0}),await g.writeFile(r(K),new TextEncoder().encode(JSON.stringify(e)))},d=(e,a,t,s)=>{let y=!1;for(let p of Object.keys(e))(p===a||p.startsWith(a+"/"))&&(e[t+p.slice(a.length)]=e[p],s||delete e[p],y=!0);return y},f=async(e,a)=>{try{return await a()}catch(t){if(t instanceof b)throw t;let s=t.code;throw s==="ENOENT"?v(e):s==="EEXIST"?S(e):s==="ENOTDIR"?M(e):s==="EISDIR"?D(e):x(String(t.message??t),e)}};return{capabilities:()=>ct,async read(e){let a=m(e);return f(a,async()=>{if((await g.stat(r(a))).isDirectory())throw D(a);return new Uint8Array(await g.readFile(r(a)))})},async write(e,a,t){let s=m(e);await f(s,()=>g.access(C(r(s))).then(()=>{}));let y=await u();if(t?.ifAbsent||t?.ifMatch!==void 0){let p=await g.stat(r(s)).then(()=>!0,()=>!1);if(t.ifAbsent&&p)throw S(s);if(t.ifMatch!==void 0&&String(y[s]??"")!==t.ifMatch)throw F(s)}if(await f(s,()=>g.writeFile(r(s),O(a)).then(()=>{})),y[s]=(y[s]??0)+1,await l(y),t?.meta){let p=await i();p[s]=t.meta,await c(p)}},async list(e,a){let t=m(e),s=[],y=async p=>{for(let w of await g.readdir(r(p),{withFileTypes:!0})){let h=m(p+"/"+w.name);o(h)||(s.push({name:w.name,path:h,type:w.isDirectory()?"dir":"file"}),a?.recursive&&w.isDirectory()&&await y(h))}};return await f(t,async()=>{if(!(await g.stat(r(t))).isDirectory())throw M(t);await y(t)}),s},async stat(e){let a=m(e);return f(a,async()=>{let t=await g.stat(r(a)),s=t.isDirectory(),y=s?void 0:(await u())[a];return{type:s?"dir":"file",size:s?0:t.size,mtime:t.mtimeMs,ctime:t.ctimeMs,meta:(await i())[a]??{},version:y!=null?String(y):void 0}})},async exists(e){try{return await g.stat(r(m(e))),!0}catch{return!1}},async mkdir(e,a){let t=m(e);await f(t,()=>g.mkdir(r(t),{recursive:a?.recursive}).then(()=>{}))},async remove(e,a){let t=m(e);await f(t,async()=>{if((await g.stat(r(t))).isDirectory()){if((await g.readdir(r(t))).length&&!a?.recursive)throw x("directory not empty",t);await g.rm(r(t),{recursive:!0,force:!0})}else await g.rm(r(t))});let s=await i(),y=!1;for(let h of Object.keys(s))(h===t||h.startsWith(t+"/"))&&(delete s[h],y=!0);y&&await c(s);let p=await u(),w=!1;for(let h of Object.keys(p))(h===t||h.startsWith(t+"/"))&&(delete p[h],w=!0);w&&await l(p)},async move(e,a){let t=m(e),s=m(a);await f(t,async()=>{if(await g.access(C(r(s))),await g.stat(r(s)).then(()=>!0,()=>!1))throw S(s);await g.rename(r(t),r(s))});let y=await i();d(y,t,s,!1)&&await c(y);let p=await u();d(p,t,s,!1)&&await l(p)},async copy(e,a){let t=m(e),s=m(a);await f(t,async()=>{if(await g.access(C(r(s))),await g.stat(r(s)).then(()=>!0,()=>!1))throw S(s);await g.cp(r(t),r(s),{recursive:!0})});let y=await i();d(y,t,s,!0)&&await c(y);let p=await u();d(p,t,s,!0)&&await l(p)},async getMeta(e){let a=m(e);return await f(a,()=>g.stat(r(a)).then(()=>{})),(await i())[a]??{}},async setMeta(e,a){let t=m(e);await f(t,()=>g.stat(r(t)).then(()=>{}));let s=await i();s[t]=a,await c(s)},watch(e,a){let t=m(e),s=(p,w)=>{if(!w)return;let h=m(t+"/"+w.toString());o(h)||g.stat(r(h)).then(()=>a({type:p==="change"?"update":"create",path:h}),()=>a({type:"remove",path:h}))},y;try{y=$(r(t),{recursive:!0},s)}catch{try{y=$(r(t),s)}catch{return()=>{}}}return()=>y.close()},async readStream(e,a){let t=m(e);await f(t,async()=>{if((await g.stat(r(t))).isDirectory())throw D(t)});let s=it(r(t),a?.signal?{signal:a.signal}:{});return st.toWeb(s)},async writeStream(e,a){let t=m(e),s=await f(t,async()=>(await g.access(C(r(t))),g.open(r(t),"w")));return new WritableStream({async write(y){try{await s.write(y)}catch(p){throw await s.close().catch(()=>{}),p}},close:async()=>{await s.close();let y=await u();if(y[t]=(y[t]??0)+1,await l(y),a?.meta){let p=await i();p[t]=a.meta,await c(p)}},abort:async()=>{await s.close()}})}}}var pt={streaming:!1,watch:!0,atomicMove:!1,nativeMeta:!0,randomAccess:!1,conditionalWrite:!0},W=new Uint8Array(0);function ut(){let n=new Map,r=0;return{async get(o){let i=n.get(o);return i?{body:i.body.slice(),meta:{...i.meta},size:i.body.length,mtime:i.mtime,version:i.version}:null},async put(o,i,c){n.set(o,{body:i.slice(),meta:{...c},mtime:Date.now(),version:String(++r)})},async del(o){n.delete(o)},async head(o){let i=n.get(o);return i?{size:i.body.length,mtime:i.mtime,meta:{...i.meta},version:i.version}:null},async list(o){let i=[];for(let[c,u]of n)c.startsWith(o)&&i.push({key:c,size:u.body.length,mtime:u.mtime});return i}}}function mt(n){let r=n.client,o=n.prefix?m("/"+n.prefix).slice(1):"",i=e=>{let a=m(e).slice(1);return o?a?o+"/"+a:o:a},c=e=>i(e)+"/",u=e=>{let a=i(e);return a?a+"/":""},l=(e,a)=>a===e||a.startsWith(e==="/"?"/":e+"/"),d=async e=>m(e)==="/"?"dir":await r.head(i(e))?"file":await r.head(c(e))||(await r.list(u(e))).length?"dir":null,f=async e=>{let a=V(e),t=await d(a);if(t===null)throw v(a);if(t!=="dir")throw M(a)};return{capabilities:()=>pt,async read(e){let a=m(e),t=await r.get(i(a));if(!t)throw await d(a)==="dir"?D(a):v(a);return t.body.slice()},async write(e,a,t){let s=m(e);if(await d(s)==="dir")throw D(s);await f(s);let y=await r.head(i(s));if(t?.ifAbsent&&y)throw S(s);if(t?.ifMatch!==void 0&&(y?.version??"")!==t.ifMatch)throw F(s);await r.put(i(s),O(a),t?.meta??y?.meta??{})},async list(e,a){let t=m(e),s=await d(t);if(s===null)throw v(t);if(s!=="dir")throw M(t);let y=u(t),p=new Map;for(let w of await r.list(y)){let h=w.key.slice(y.length),T=h.endsWith("/");if(T&&(h=h.slice(0,-1)),h!=="")if(a?.recursive){let k=m(t+"/"+h),R=T?"dir":"file";(!p.has(k)||R==="dir")&&p.set(k,{name:h.split("/").pop(),path:k,type:R})}else{let k=h.split("/")[0],R=m(t+"/"+k),N=T||h.includes("/")?"dir":"file";(!p.has(R)||N==="dir")&&p.set(R,{name:k,path:R,type:N})}}return[...p.values()]},async stat(e){let a=m(e),t=await d(a);if(t===null)throw v(a);if(t==="file"){let y=await r.head(i(a));return{type:"file",size:y?.size??0,mtime:y?.mtime??0,ctime:y?.mtime??0,meta:y?.meta??{},version:y?.version}}let s=await r.head(c(a));return{type:"dir",size:0,mtime:s?.mtime??0,ctime:s?.mtime??0,meta:s?.meta??{}}},async exists(e){return await d(e)!==null},async mkdir(e,a){let t=m(e);if(await d(t)!==null){if(a?.recursive)return;throw S(t)}if(a?.recursive){let s=t.split("/").filter(Boolean),y="";for(let p of s){y+="/"+p;let w=await d(y);if(w==="file")throw M(y);w===null&&await r.put(c(y),W,{})}return}await f(t),await r.put(c(t),W,{})},async remove(e,a){let t=m(e),s=await d(t);if(s===null)throw v(t);if(s==="file"){await r.del(i(t));return}let y=await r.list(u(t));if(y.filter(w=>w.key!==c(t)).length&&!a?.recursive)throw x("directory not empty",t);for(let w of y)await r.del(w.key);await r.del(c(t))},async copy(e,a){let t=m(e),s=m(a);if(l(t,s))throw x("cannot copy into itself",s);let y=await d(t);if(y===null)throw v(t);if(await d(s)!==null)throw S(s);if(await f(s),y==="file"){let w=await r.get(i(t));w&&await r.put(i(s),w.body,w.meta);return}await r.put(c(s),W,{});let p=u(t);for(let w of await r.list(p)){let h=w.key.slice(p.length);if(h==="")continue;let T=u(s)+h;if(h.endsWith("/"))await r.put(T,W,{});else{let k=await r.get(w.key);k&&await r.put(T,k.body,k.meta)}}},async move(e,a){await this.copy(e,a),await this.remove(e,{recursive:!0})},async getMeta(e){let a=m(e),t=await d(a);if(t===null)throw v(a);return(await r.head(t==="file"?i(a):c(a)))?.meta??{}},async setMeta(e,a){let t=m(e),s=await d(t);if(s===null)throw v(t);if(s==="file"){let y=await r.get(i(t));await r.put(i(t),y?.body??W,a)}else await r.put(c(t),W,a)},watch(e,a){let t=m(e),s=u(t),y=i(t)?i(t)+"/":"",p=U=>m("/"+(o?U.slice(o.length+1):U)),w=new Map,h=!1,T=async()=>{let U=new Map;for(let E of await r.list(s))E.key!==y&&U.set(E.key,E.mtime);return U};T().then(U=>{w=U,h=!0});let k=!1,R=async()=>{if(!(!h||k)){k=!0;try{let U=await T();for(let[E,tt]of U)w.has(E)?w.get(E)!==tt&&a({type:"update",path:p(E)}):a({type:"create",path:p(E)});for(let E of w.keys())U.has(E)||a({type:"remove",path:p(E)});w=U}finally{k=!1}}},N=setInterval(()=>{R()},n.pollMs??200);return N.unref?.(),()=>clearInterval(N)}}}var j=new Uint8Array([86,75,1]),G=47,I=globalThis.crypto.subtle;function X(n){let r=new Uint8Array(n);return globalThis.crypto.getRandomValues(r),r}async function dt(n,r){let o=await I.importKey("raw",new TextEncoder().encode(n),"PBKDF2",!1,["deriveKey"]);return I.deriveKey({name:"PBKDF2",salt:r.slice(),iterations:21e4,hash:"SHA-256"},o,{name:"AES-GCM",length:256},!1,["encrypt","decrypt"])}function ft(n,r){if(!r.key&&!r.passphrase)throw new Error("encrypt: key or passphrase required");let o=r.key?I.importKey("raw",r.key.slice(),"AES-GCM",!1,["encrypt","decrypt"]):null,i=u=>o??dt(r.passphrase,u),c={...n.capabilities(),streaming:!1,randomAccess:!1};return{...n,readStream:void 0,writeStream:void 0,capabilities:()=>c,async stat(u){let l=await n.stat(u);return l.type==="file"?{...l,size:Math.max(0,l.size-G)}:l},async write(u,l,d){let f=X(16),e=X(12),a=await i(f),t=new Uint8Array(await I.encrypt({name:"AES-GCM",iv:e},a,O(l).slice()));await n.write(u,L([j,f,e,t]),d)},async read(u,l){let d=await n.read(u,l);if(d.length<G||d[0]!==j[0]||d[1]!==j[1]||d[2]!==j[2])throw x("invalid ciphertext",u);let f=await i(d.slice(3,19)),e;try{e=await I.decrypt({name:"AES-GCM",iv:d.slice(19,31)},f,d.slice(31))}catch{throw x("decryption failed",u)}return new Uint8Array(e)}}}function lt(n,r={}){let o=r.store??new Map,i=r.ttlMs??0,c=()=>i?Date.now():0,u=()=>i?Date.now()+i:1/0,l=(f,e)=>e===f||e.startsWith(f==="/"?"/":f+"/"),d=f=>{for(let e of[...o.keys()])l(f,e)&&o.delete(e)};return{...n,readStream:void 0,writeStream:void 0,async read(f,e){let a=m(f),t=o.get(a);if(t&&t.exp>c())return t.data.slice();let s=await n.read(a,e);return o.set(a,{data:s.slice(),exp:u()}),s.slice()},async write(f,e,a){let t=m(f),s=O(e).slice();await n.write(t,s,a),o.set(t,{data:s,exp:u()})},async remove(f,e){let a=m(f);await n.remove(a,e),d(a)},async move(f,e){let a=m(f),t=m(e);await n.move(a,t),d(a),d(t)},async copy(f,e){let a=m(f),t=m(e);await n.copy(a,t),d(t)}}}var wt=new TextEncoder,ht=new TextDecoder,B=new Uint8Array(0);function A(n,r=B){let o=wt.encode(JSON.stringify(n)),i=new Uint8Array(4+o.length+r.length);return new DataView(i.buffer).setUint32(0,o.length),i.set(o,4),i.set(r,4+o.length),i}function H(n){let o=new DataView(n.buffer,n.byteOffset,n.byteLength).getUint32(0);return{header:JSON.parse(ht.decode(n.subarray(4,4+o))),data:n.subarray(4+o)}}function Q(n,r,o=[],i){return A({m:n,p:r,a:o},i)}function gt(n){let{header:r,data:o}=H(n);return{method:r.m,path:r.p,args:r.a??[],data:o}}function Z(n){let{header:r,data:o}=H(n);return r.ok?{ok:!0,value:r.v,data:o}:{ok:!1,data:B,code:r.c,message:r.e,path:r.p}}async function _(n,r){let{method:o,path:i,args:c,data:u}=gt(r);try{switch(o){case"read":return A({ok:!0},await n.read(i,c[0]));case"write":return await n.write(i,u,c[0]),A({ok:!0});case"list":return A({ok:!0,v:await n.list(i,c[0])});case"stat":return A({ok:!0,v:await n.stat(i)});case"exists":return A({ok:!0,v:await n.exists(i)});case"mkdir":return await n.mkdir(i,c[0]),A({ok:!0});case"remove":return await n.remove(i,c[0]),A({ok:!0});case"move":return await n.move(i,c[0]),A({ok:!0});case"copy":return await n.copy(i,c[0]),A({ok:!0});case"getMeta":return A({ok:!0,v:await n.getMeta(i)});case"setMeta":return await n.setMeta(i,c[0]),A({ok:!0});default:throw z(o)}}catch(l){let d=l instanceof b?l:x(String(l?.message??l),i);return A({ok:!1,c:d.code,e:d.message,p:d.path})}}function P(n,r,o=B){let i=new Uint8Array(5+o.length);return i[0]=n,new DataView(i.buffer).setUint32(1,r),i.set(o,5),i}function q(n){let r=new DataView(n.buffer,n.byteOffset,n.byteLength);return{type:n[0],id:r.getUint32(1),payload:n.subarray(5)}}var bt=new TextEncoder,kt=new TextDecoder;function vt(n){return{handle:r=>_(n,r),async fetch(r){let o=new Uint8Array(await r.arrayBuffer());return new Response(await _(n,o),{headers:{"content-type":"application/octet-stream"}})},socket(r){let o=new Map;return{async message(i){let{type:c,id:u,payload:l}=q(i);if(c===0)r(P(1,u,await _(n,l)));else if(c===2){let{path:d}=JSON.parse(kt.decode(l));o.set(u,n.watch(d,f=>r(P(4,u,bt.encode(JSON.stringify(f))))))}else c===3&&(o.get(u)?.(),o.delete(u))},close(){for(let i of o.values())i();o.clear()}}}}}function St(n){let r=n.request?n:n.transport,o=n.capabilities??{streaming:!1,watch:!!r.watch,atomicMove:!1,nativeMeta:!0,randomAccess:!1,conditionalWrite:!0},i=async(c,u,l,d)=>{let f=l?[...l]:[];for(;f.length&&f[f.length-1]===void 0;)f.pop();let e=Z(await r.request(Q(c,u,f,d)));if(!e.ok)throw new b(e.code,e.message??"",e.path);return e};return{capabilities:()=>o,async read(c,u){return(await i("read",c,[u])).data},async write(c,u,l){await i("write",c,[l],O(u))},async list(c,u){return(await i("list",c,[u])).value},async stat(c){return(await i("stat",c)).value},async exists(c){return(await i("exists",c)).value},async mkdir(c,u){await i("mkdir",c,[u])},async remove(c,u){await i("remove",c,[u])},async move(c,u){await i("move",c,[u])},async copy(c,u){await i("copy",c,[u])},async getMeta(c){return(await i("getMeta",c)).value},async setMeta(c,u){await i("setMeta",c,[u])},watch(c,u){if(!r.watch)throw z("watch");return r.watch(c,u)}}}function xt(n,r){let o=r??((i,c)=>fetch(i,c));return{async request(i){let c=await o(n,{method:"POST",body:i});return new Uint8Array(await c.arrayBuffer())}}}var At=new TextEncoder,Ot=new TextDecoder;function Ut(n,r){let o=r?r():new WebSocket(n);try{o.binaryType="arraybuffer"}catch{}let i=new Map,c=new Map,u=0,l=o.readyState===1?Promise.resolve():new Promise(d=>{o.addEventListener?o.addEventListener("open",()=>d()):o.onopen=()=>d()});return o.onmessage=d=>{let{type:f,id:e,payload:a}=q(new Uint8Array(d.data));f===1?(i.get(e)?.(a),i.delete(e)):f===4&&c.get(e)?.(JSON.parse(Ot.decode(a)))},{async request(d){await l;let f=++u;return new Promise(e=>{i.set(f,e),o.send(P(0,f,d))})},watch(d,f){let e=++u;return c.set(e,f),l.then(()=>o.send(P(2,e,At.encode(JSON.stringify({path:d}))))),()=>{c.delete(e),l.then(()=>o.send(P(3,e,B)))}}}}export{Y as BRAND,b as VfsError,S as alreadyExists,Mt as basename,lt as cache,It as collect,L as concat,F as conflict,V as dirname,ft as encrypt,xt as httpTransport,x as io,D as isADirectory,Pt as isVfsError,Et as join,at as memory,ut as memoryS3,yt as nodeFs,m as normalize,M as notADirectory,v as notFound,Rt as permissionDenied,Vt as readStream,St as remote,mt as s3,Dt as segments,vt as serve,O as toBytes,Ct as toText,z as unsupported,Lt as writeStream,Ut as wsTransport};
@@ -0,0 +1,69 @@
1
+ type BytesLike = Uint8Array | ArrayBuffer | string;
2
+ type FileType = 'file' | 'dir';
3
+ interface Meta {
4
+ [k: string]: unknown;
5
+ }
6
+ interface Stat {
7
+ type: FileType;
8
+ size: number;
9
+ mtime: number;
10
+ ctime: number;
11
+ meta: Meta;
12
+ version?: string;
13
+ }
14
+ interface Entry {
15
+ name: string;
16
+ path: string;
17
+ type: FileType;
18
+ }
19
+ interface ReadOpts {
20
+ signal?: AbortSignal;
21
+ }
22
+ interface WriteOpts {
23
+ meta?: Meta;
24
+ signal?: AbortSignal;
25
+ ifMatch?: string;
26
+ ifAbsent?: boolean;
27
+ }
28
+ interface ListOpts {
29
+ recursive?: boolean;
30
+ }
31
+ interface MkdirOpts {
32
+ recursive?: boolean;
33
+ }
34
+ interface RemoveOpts {
35
+ recursive?: boolean;
36
+ }
37
+ interface Capabilities {
38
+ streaming: boolean;
39
+ watch: boolean;
40
+ atomicMove: boolean;
41
+ nativeMeta: boolean;
42
+ randomAccess: boolean;
43
+ conditionalWrite: boolean;
44
+ }
45
+ interface WatchEvent {
46
+ type: 'create' | 'update' | 'remove';
47
+ path: string;
48
+ }
49
+ type WatchCb = (e: WatchEvent) => void;
50
+ type Unsubscribe = () => void;
51
+ interface VFS {
52
+ read(path: string, opts?: ReadOpts): Promise<Uint8Array>;
53
+ write(path: string, data: BytesLike, opts?: WriteOpts): Promise<void>;
54
+ list(path: string, opts?: ListOpts): Promise<Entry[]>;
55
+ stat(path: string): Promise<Stat>;
56
+ exists(path: string): Promise<boolean>;
57
+ mkdir(path: string, opts?: MkdirOpts): Promise<void>;
58
+ remove(path: string, opts?: RemoveOpts): Promise<void>;
59
+ move(from: string, to: string): Promise<void>;
60
+ copy(from: string, to: string): Promise<void>;
61
+ getMeta(path: string): Promise<Meta>;
62
+ setMeta(path: string, meta: Meta): Promise<void>;
63
+ watch(path: string, cb: WatchCb): Unsubscribe;
64
+ capabilities(): Capabilities;
65
+ readStream?(path: string, opts?: ReadOpts): Promise<ReadableStream<Uint8Array>>;
66
+ writeStream?(path: string, opts?: WriteOpts): Promise<WritableStream<Uint8Array>>;
67
+ }
68
+
69
+ export type { BytesLike as B, Capabilities as C, Entry as E, FileType as F, ListOpts as L, Meta as M, ReadOpts as R, Stat as S, Unsubscribe as U, VFS as V, WriteOpts as W, WatchCb as a, MkdirOpts as b, RemoveOpts as c, WatchEvent as d };
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@artemjs/vfskit",
3
+ "version": "1.1.0",
4
+ "description": "Universal abstraction over any virtual file system - memory, disk, S3 and more - with composable adapters, encryption and a remote bridge. Backend kit.",
5
+ "keywords": [
6
+ "vfs",
7
+ "filesystem",
8
+ "fs",
9
+ "s3",
10
+ "storage",
11
+ "abstraction",
12
+ "virtual-file-system",
13
+ "encryption",
14
+ "remote",
15
+ "adapter",
16
+ "node"
17
+ ],
18
+ "license": "MIT",
19
+ "type": "module",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "import": "./dist/index.js"
24
+ },
25
+ "./conformance": {
26
+ "types": "./dist/conformance.d.ts",
27
+ "import": "./dist/conformance.js"
28
+ }
29
+ },
30
+ "main": "./dist/index.js",
31
+ "types": "./dist/index.d.ts",
32
+ "sideEffects": false,
33
+ "files": [
34
+ "dist",
35
+ "README.md"
36
+ ],
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/artemjs/vfskit.git",
40
+ "directory": "facades/vfskit"
41
+ },
42
+ "bugs": "https://github.com/artemjs/vfskit/issues",
43
+ "homepage": "https://github.com/artemjs/vfskit#readme",
44
+ "scripts": {
45
+ "build": "tsup"
46
+ },
47
+ "devDependencies": {
48
+ "@vfskit/core": "*",
49
+ "@vfskit/memory": "*",
50
+ "@vfskit/node-fs": "*",
51
+ "@vfskit/s3": "*",
52
+ "@vfskit/encrypt": "*",
53
+ "@vfskit/server": "*",
54
+ "@vfskit/remote": "*",
55
+ "@vfskit/transport-http": "*",
56
+ "@vfskit/transport-ws": "*",
57
+ "tsup": "^8.3.0",
58
+ "@vfskit/cache": "*"
59
+ },
60
+ "publishConfig": {
61
+ "access": "public"
62
+ }
63
+ }