@statewalker/webrun-files-sqlite 0.10.2 → 0.10.3
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 +173 -135
- package/package.json +8 -4
package/README.md
CHANGED
|
@@ -1,179 +1,221 @@
|
|
|
1
1
|
# @statewalker/webrun-files-sqlite
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
## What it is
|
|
4
|
+
|
|
5
|
+
Two implementations of `FilesApi` from `@statewalker/webrun-files` that store files in a SQLite
|
|
6
|
+
database, behind one small SQL driver interface with drivers for `node:sqlite`, Cloudflare Durable
|
|
7
|
+
Object storage and Cloudflare D1:
|
|
8
|
+
|
|
9
|
+
- `SqliteFilesApi` streams content as fixed-size blocks (1 MiB by default, optionally deflated per
|
|
10
|
+
block), shares identical content between paths, and makes every metadata change one transaction.
|
|
11
|
+
Files of any size.
|
|
12
|
+
- `SqlarFilesApi` stores each file as one row of a [SQLite Archive](https://sqlite.org/sqlar.html),
|
|
13
|
+
readable by `sqlite3 -A`. Whole-file reads and writes.
|
|
14
|
+
|
|
15
|
+
## Why it exists
|
|
16
|
+
|
|
17
|
+
Durable Objects and D1 offer SQLite and nothing like a file system, and a single SQLite file is a
|
|
18
|
+
convenient, transactional store elsewhere too. Storing a file as one blob breaks there: Durable
|
|
19
|
+
Objects and D1 cap a row at 2 MB, and a blob is read and written whole. `SqliteFilesApi` cuts
|
|
20
|
+
content into blocks that stay under the cap and can be streamed and range-read. `SqlarFilesApi`
|
|
21
|
+
exists for the opposite need: an archive other SQLAR tools can open, in runtimes without a row
|
|
22
|
+
limit.
|
|
5
23
|
|
|
6
24
|
| | `SqliteFilesApi` | `SqlarFilesApi` |
|
|
7
25
|
| --- | --- | --- |
|
|
8
|
-
| Storage | paths
|
|
26
|
+
| Storage | paths -> contents -> blocks of at most 1.5 MiB | one row per file, SQLite Archive format |
|
|
9
27
|
| File size | unbounded; streamed block by block | one blob, read and written whole |
|
|
10
|
-
| Durable Objects / D1 | yes
|
|
28
|
+
| Durable Objects / D1 | yes, every row stays under the 2 MB limit | only files whose stored row is under 2 MB |
|
|
11
29
|
| Readable by `sqlite3 -A` | no | yes |
|
|
12
30
|
| Identical content | stored once, shared by every path | stored per path |
|
|
13
31
|
|
|
14
|
-
Use `SqliteFilesApi` by default
|
|
15
|
-
SQLAR tools, in a runtime without a row limit (Node, Deno, Bun, a browser SQLite).
|
|
32
|
+
Use `SqliteFilesApi` by default.
|
|
16
33
|
|
|
17
|
-
##
|
|
34
|
+
## How to use
|
|
18
35
|
|
|
19
36
|
```bash
|
|
20
|
-
|
|
37
|
+
pnpm add @statewalker/webrun-files-sqlite @statewalker/webrun-files
|
|
21
38
|
```
|
|
22
39
|
|
|
23
40
|
`pako` is not a dependency: pass the module to `pakoCodec` / `pakoDeflateCodec` if you use them.
|
|
24
41
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
passed in — so it bundles for any runtime.
|
|
42
|
+
One entry point, `@statewalker/webrun-files-sqlite`: ESM (`dist/esm/index.js`), CommonJS
|
|
43
|
+
(`dist/cjs/index.cjs`), types (`dist/index.d.ts`); sources in `src/`. The package imports no
|
|
44
|
+
database binding itself; you pass the handle in, so it bundles for any runtime.
|
|
29
45
|
|
|
30
46
|
```typescript
|
|
31
|
-
import { D1SqlDriver, DoSqlDriver, NodeSqlDriver } from
|
|
47
|
+
import { D1SqlDriver, DoSqlDriver, NodeSqlDriver, SqliteFilesApi } from '@statewalker/webrun-files-sqlite';
|
|
32
48
|
|
|
33
|
-
new NodeSqlDriver(new DatabaseSync(
|
|
34
|
-
new DoSqlDriver(this.ctx.storage);
|
|
35
|
-
new D1SqlDriver(env.DB);
|
|
49
|
+
new NodeSqlDriver(new DatabaseSync('files.db')); // node:sqlite
|
|
50
|
+
new DoSqlDriver(this.ctx.storage); // in a Durable Object: ctx.storage, not ctx.storage.sql
|
|
51
|
+
new D1SqlDriver(env.DB); // a D1 binding
|
|
52
|
+
|
|
53
|
+
const files = new SqliteFilesApi(driver, { blockSize: 1024 * 1024 });
|
|
54
|
+
await files.init(); // creates tables and indexes if missing; await it before any other call
|
|
36
55
|
```
|
|
37
56
|
|
|
38
|
-
|
|
57
|
+
### `SqliteFilesApi` options
|
|
39
58
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
59
|
+
| Option | Default | |
|
|
60
|
+
| --- | --- | --- |
|
|
61
|
+
| `compression` | `webDeflateCodec()` where `CompressionStream` exists, else none | `null` stores uncompressed; `pakoDeflateCodec(pako)` for runtimes without Compression Streams |
|
|
62
|
+
| `tablePrefix` | `"fs_"` | several file systems in one database |
|
|
63
|
+
| `blockSize` | 1 MiB | uncompressed bytes per block for new writes, 1 byte to 1.5 MiB |
|
|
45
64
|
|
|
46
|
-
|
|
47
|
-
/** Rows as objects keyed by column name. */
|
|
48
|
-
all<T>(sql: string, ...params: unknown[]): T[] | Promise<T[]>;
|
|
49
|
-
/** One statement, for its effect. */
|
|
50
|
-
run(sql: string, ...params: unknown[]): void | Promise<void>;
|
|
51
|
-
/**
|
|
52
|
-
* The statements in order, as one transaction: all apply or none do. Returns the number of rows
|
|
53
|
-
* each statement changed. No statement may depend on another's result.
|
|
54
|
-
*/
|
|
55
|
-
transaction(statements: SqlStatement[]): number[] | Promise<number[]>;
|
|
56
|
-
}
|
|
57
|
-
```
|
|
65
|
+
### `SqlarFilesApi` codecs (`codec` option)
|
|
58
66
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
67
|
+
| Codec | Uses | When |
|
|
68
|
+
| --- | --- | --- |
|
|
69
|
+
| `defaultCodec()` | `webCodec()` or `rawCodec()` | the default: `webCodec()` where `CompressionStream` exists, `rawCodec()` elsewhere |
|
|
70
|
+
| `webCodec({ minSize? })` | `CompressionStream("deflate")` | browsers, Workers, Node, Deno, Bun |
|
|
71
|
+
| `pakoCodec(pako, { minSize?, level? })` | an injected `pako` module | runtimes without Compression Streams |
|
|
72
|
+
| `rawCodec()` | nothing | never compresses, and refuses to read compressed rows |
|
|
63
73
|
|
|
64
|
-
##
|
|
74
|
+
## Examples
|
|
75
|
+
|
|
76
|
+
### Stream a large file through `SqliteFilesApi`
|
|
65
77
|
|
|
66
78
|
```typescript
|
|
67
|
-
import { DatabaseSync } from
|
|
68
|
-
import { NodeSqlDriver, SqliteFilesApi } from
|
|
79
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
80
|
+
import { NodeSqlDriver, SqliteFilesApi } from '@statewalker/webrun-files-sqlite';
|
|
69
81
|
|
|
70
|
-
const files = new SqliteFilesApi(new NodeSqlDriver(new DatabaseSync(
|
|
71
|
-
await files.init();
|
|
82
|
+
const files = new SqliteFilesApi(new NodeSqlDriver(new DatabaseSync('files.db')));
|
|
83
|
+
await files.init();
|
|
72
84
|
|
|
73
|
-
await files.write(
|
|
74
|
-
for await (const chunk of files.read(
|
|
85
|
+
await files.write('/video.mp4', chunks); // any Iterable or AsyncIterable of Uint8Array
|
|
86
|
+
for await (const chunk of files.read('/video.mp4', { start: 10_000_000, length: 65_536 })) {
|
|
75
87
|
// only the block holding this range is fetched and inflated
|
|
76
88
|
}
|
|
77
89
|
```
|
|
78
90
|
|
|
79
|
-
|
|
91
|
+
### Inside a Durable Object
|
|
80
92
|
|
|
81
|
-
|
|
93
|
+
```typescript
|
|
94
|
+
import { DoSqlDriver, SqliteFilesApi } from '@statewalker/webrun-files-sqlite';
|
|
82
95
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
bytes, and when it last changed (`size` and `hash` are NULL while it is being written).
|
|
87
|
-
- `fs_blocks(fid, shift, block)`, keyed by `(fid, shift)` — the content cut into blocks of
|
|
88
|
-
`block_size` uncompressed bytes (only the last one shorter); `shift` is the block's offset in the
|
|
89
|
-
uncompressed content, always a multiple of `block_size`. Each block of a `deflate` content is its
|
|
90
|
-
own zlib stream, so a range read fetches and inflates exactly the blocks the range touches, found
|
|
91
|
-
by key.
|
|
96
|
+
const files = new SqliteFilesApi(new DoSqlDriver(this.ctx.storage));
|
|
97
|
+
await files.init();
|
|
98
|
+
```
|
|
92
99
|
|
|
93
|
-
|
|
94
|
-
(same SHA-256, size, compression and block size) shares it too. A content and its blocks are deleted when the
|
|
95
|
-
last path pointing at it is removed or overwritten.
|
|
100
|
+
### Clean up interrupted writes
|
|
96
101
|
|
|
97
|
-
|
|
102
|
+
```typescript
|
|
103
|
+
const removed = await files.sweep({ olderThan: 60 * 60_000 }); // contents untouched for an hour
|
|
104
|
+
```
|
|
98
105
|
|
|
99
|
-
|
|
100
|
-
pulling for the next; `read` fetches a block only when its consumer asks for bytes beyond the
|
|
101
|
-
previous one. Memory is bounded by one block plus one source chunk, and the tests measure both
|
|
102
|
-
bounds. An `AbortSignal` passed to `read` is checked before every block.
|
|
106
|
+
### A portable archive with `SqlarFilesApi`
|
|
103
107
|
|
|
104
|
-
|
|
105
|
-
|
|
108
|
+
```typescript
|
|
109
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
110
|
+
import * as pako from 'pako';
|
|
111
|
+
import { readText, writeText } from '@statewalker/webrun-files';
|
|
112
|
+
import { NodeSqlDriver, pakoCodec, SqlarFilesApi } from '@statewalker/webrun-files-sqlite';
|
|
106
113
|
|
|
107
|
-
|
|
114
|
+
const files = new SqlarFilesApi(new NodeSqlDriver(new DatabaseSync('site.sqlar')), {
|
|
115
|
+
codec: pakoCodec(pako, { level: 9 }),
|
|
116
|
+
});
|
|
117
|
+
await files.init(); // creates the sqlar table if missing
|
|
108
118
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
| `compression` | `webDeflateCodec()` where `CompressionStream` exists, else none | `null` stores uncompressed; `pakoDeflateCodec(pako)` for runtimes without Compression Streams |
|
|
112
|
-
| `tablePrefix` | `"fs_"` | several file systems in one database |
|
|
113
|
-
| `blockSize` | 1 MiB | uncompressed bytes per block for new writes, 1 to 1.5 MiB (the cap keeps a deflated block under the 2 MB row limit). Larger means fewer rows — better where statements are costly, as on D1; smaller means cheaper small range reads. Each content records its own size, so changing this never affects existing files. |
|
|
119
|
+
await writeText(files, '/docs/index.md', '# Hello');
|
|
120
|
+
console.log(await readText(files, '/docs/index.md'));
|
|
114
121
|
|
|
115
|
-
|
|
122
|
+
await files.symlink('/latest.md', './docs/index.md');
|
|
123
|
+
await files.readlink('/latest.md'); // "./docs/index.md"
|
|
124
|
+
```
|
|
116
125
|
|
|
117
|
-
|
|
118
|
-
path contains the other.
|
|
119
|
-
- A write is invisible until complete: if it fails for any reason, the path keeps its previous
|
|
120
|
-
content and the partly written content is deleted.
|
|
121
|
-
- A read whose content is removed underneath it throws `changed during read`. A block whose
|
|
122
|
-
uncompressed length is not what its position implies throws `block at <shift> holds … bytes`,
|
|
123
|
-
before any wrong byte reaches the reader.
|
|
126
|
+
### Your own SQLite binding
|
|
124
127
|
|
|
125
|
-
|
|
128
|
+
Implement the driver interface:
|
|
126
129
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
storing each block, finishing a write, `remove`, `copy` and `move`. A failure inside any of them
|
|
130
|
-
leaves the database exactly as it was — `move` onto an existing target never loses the target.
|
|
130
|
+
```typescript
|
|
131
|
+
import type { SqlDriver, SqlStatement } from '@statewalker/webrun-files-sqlite';
|
|
131
132
|
|
|
132
|
-
|
|
133
|
-
|
|
133
|
+
interface SqlStatement {
|
|
134
|
+
sql: string;
|
|
135
|
+
params?: unknown[];
|
|
136
|
+
}
|
|
134
137
|
|
|
135
|
-
|
|
136
|
-
|
|
138
|
+
interface SqlDriver {
|
|
139
|
+
/** Rows as objects keyed by column name. */
|
|
140
|
+
all<T>(sql: string, ...params: unknown[]): T[] | Promise<T[]>;
|
|
141
|
+
/** One statement, for its effect. */
|
|
142
|
+
run(sql: string, ...params: unknown[]): void | Promise<void>;
|
|
143
|
+
/** The statements in order, as one transaction: all apply or none do. Returns rows changed per
|
|
144
|
+
* statement. No statement may depend on another's result. */
|
|
145
|
+
transaction(statements: SqlStatement[]): number[] | Promise<number[]>;
|
|
146
|
+
}
|
|
137
147
|
```
|
|
138
148
|
|
|
139
|
-
|
|
140
|
-
milliseconds, with their blocks. A write refreshes its content with every block, so only a write
|
|
141
|
-
stalled longer than `olderThan` can be swept — and it then fails rather than creating a broken path.
|
|
142
|
-
Choose a threshold above the longest pause a source may take. `sweep` never runs on its own.
|
|
149
|
+
## Internals
|
|
143
150
|
|
|
144
|
-
|
|
151
|
+
### The drivers and their transactions
|
|
145
152
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
153
|
+
The built-in drivers implement `transaction` with `BEGIN IMMEDIATE ... COMMIT` (node:sqlite),
|
|
154
|
+
`ctx.storage.transactionSync` (Durable Objects, where `sql.exec` rejects `BEGIN`) and `batch()` (D1).
|
|
155
|
+
Blobs come back as `Uint8Array`, `ArrayBuffer` or `number[]` depending on the runtime; both
|
|
156
|
+
implementations accept all three. The handle types are exported as `NodeSqliteDatabase`,
|
|
157
|
+
`DoStorage` and `D1Database`.
|
|
150
158
|
|
|
151
|
-
|
|
152
|
-
await files.init(); // creates the sqlar table if missing — await before anything else
|
|
159
|
+
### How `SqliteFilesApi` stores content
|
|
153
160
|
|
|
154
|
-
|
|
155
|
-
|
|
161
|
+
```
|
|
162
|
+
fs_paths(pid, path UNIQUE, fid, mtime) a path -> a content, or a directory (fid NULL)
|
|
163
|
+
|
|
|
164
|
+
v
|
|
165
|
+
fs_files(fid, size, compression, block_size, an immutable content: size, "none"/"deflate",
|
|
166
|
+
hash, updated) block size, SHA-256, last change
|
|
167
|
+
|
|
|
168
|
+
v
|
|
169
|
+
fs_blocks(fid, shift, block) the content in blocks of block_size bytes;
|
|
170
|
+
shift = offset in the uncompressed content
|
|
156
171
|
```
|
|
157
172
|
|
|
158
|
-
|
|
173
|
+
Each block of a `deflate` content is its own zlib stream, so a range read fetches and inflates
|
|
174
|
+
exactly the blocks it touches, found by key. `copy` adds path rows pointing at the same content; a
|
|
175
|
+
write whose bytes match an existing content (same SHA-256, size, compression and block size) shares
|
|
176
|
+
it too. A content and its blocks are deleted when the last path pointing at it goes.
|
|
159
177
|
|
|
160
|
-
###
|
|
178
|
+
### Why the block size is capped at 1.5 MiB
|
|
161
179
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
| `rawCodec()` | nothing | Default elsewhere; never compresses, and refuses to read compressed rows |
|
|
180
|
+
A deflated block can be slightly larger than its input. 1.5 MiB keeps even incompressible input
|
|
181
|
+
under the 2 MB row limit of Durable Objects and D1. Larger blocks mean fewer rows, which matters
|
|
182
|
+
where every statement costs (D1); smaller blocks make small range reads cheaper. Each content
|
|
183
|
+
records its own block size, so changing `blockSize` never affects existing files.
|
|
167
184
|
|
|
168
|
-
|
|
169
|
-
import * as pako from "pako";
|
|
170
|
-
const files = new SqlarFilesApi(driver, { codec: pakoCodec(pako) });
|
|
171
|
-
```
|
|
185
|
+
### Streaming and memory
|
|
172
186
|
|
|
173
|
-
|
|
174
|
-
|
|
187
|
+
`write` pulls the source only while it builds the current block and stores each block before
|
|
188
|
+
pulling the next; `read` fetches a block only when its consumer asks for bytes beyond the previous
|
|
189
|
+
one. Memory is bounded by one block plus one source chunk; the tests measure both bounds. An
|
|
190
|
+
`AbortSignal` passed to `read` is checked before every block. `list` reads `fs_paths` in pages of
|
|
191
|
+
256 by key, in index order, which is the `FilesApi` path order, and `{ after }` is where the first
|
|
192
|
+
page starts.
|
|
193
|
+
|
|
194
|
+
### Atomic steps, and why `sweep` exists
|
|
195
|
+
|
|
196
|
+
Streaming happens outside transactions. Every step that changes what a path shows is one
|
|
197
|
+
transaction: storing each block, finishing a write, `remove`, `copy` and `move`. A failure inside
|
|
198
|
+
any of them leaves the database as it was; `move` onto an existing target never loses the target.
|
|
199
|
+
A write stays invisible until it finishes: if it fails, the path keeps its previous content and the
|
|
200
|
+
partial content is deleted.
|
|
201
|
+
|
|
202
|
+
If the process dies mid-stream, the partial content is left behind, unreferenced. `sweep` deletes
|
|
203
|
+
contents that no path references and that have not been updated for `olderThan` milliseconds, with
|
|
204
|
+
their blocks. A write refreshes its content with every block, so only a write stalled longer than
|
|
205
|
+
`olderThan` can be swept, and it then fails rather than creating a broken path. Choose a threshold
|
|
206
|
+
above the longest pause a source may take. `sweep` never runs on its own.
|
|
207
|
+
|
|
208
|
+
### What `SqliteFilesApi` throws
|
|
209
|
+
|
|
210
|
+
- The constructor throws `SqliteFilesApi: blockSize must be an integer from 1 to 1572864, got ...`
|
|
211
|
+
and `SqliteFilesApi: tablePrefix must be a plain SQL identifier, got ...`.
|
|
212
|
+
- `copy` and `move` replace the target's subtree rather than merging into it, and throw when one
|
|
213
|
+
path contains the other.
|
|
214
|
+
- A read whose content is removed underneath it throws `changed during read`.
|
|
215
|
+
- A block whose uncompressed length does not match its position throws `block at <shift> holds ...
|
|
216
|
+
bytes`, before any wrong byte reaches the reader.
|
|
175
217
|
|
|
176
|
-
### How
|
|
218
|
+
### How `SqlarFilesApi` stores entries
|
|
177
219
|
|
|
178
220
|
| Entry | `name` | `sz` | `data` |
|
|
179
221
|
| --- | --- | --- | --- |
|
|
@@ -182,30 +224,26 @@ Inputs shorter than `minSize` (64 bytes by default) are stored without attemptin
|
|
|
182
224
|
| Directory | | `0` | `NULL` |
|
|
183
225
|
| Symlink | | `-1` | target as text |
|
|
184
226
|
|
|
185
|
-
`mtime` is stored in seconds; `lastModified` reports
|
|
186
|
-
|
|
187
|
-
directory
|
|
227
|
+
`mtime` is stored in seconds; `lastModified` reports milliseconds. Inputs shorter than `minSize`
|
|
228
|
+
(64 bytes by default) are stored without trying to compress. All codecs read and write the same
|
|
229
|
+
zlib format. Archives written by other tools without directory rows still behave as trees: a path
|
|
230
|
+
that is only a prefix of other names is a directory.
|
|
188
231
|
|
|
189
|
-
|
|
232
|
+
`FileKind` has no symlink, so `stats`, `exists`, `read` and `list` follow links as POSIX `stat()`
|
|
233
|
+
does. A dangling link, or a chain longer than 8, behaves as a missing path. `remove`, `copy` and
|
|
234
|
+
`move` act on the link itself. Only the final path component is resolved through symlinks.
|
|
190
235
|
|
|
191
|
-
|
|
192
|
-
`stat()` does. A dangling link, or a chain of more than 8, behaves as a missing path. `remove`,
|
|
193
|
-
`copy` and `move` act on the link itself. Two extension methods give access to links:
|
|
236
|
+
### `SqlarFilesApi` limits
|
|
194
237
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
238
|
+
- A file is one blob: `write` buffers it, `read` inflates it whole and yields one chunk.
|
|
239
|
+
- On Durable Objects and D1 the stored (possibly compressed) size of one file must stay under 2 MB.
|
|
240
|
+
- Multi-row mutations (`copy`, `move`, `remove` of a tree) are not wrapped in a transaction.
|
|
241
|
+
- `list` loads every row under the directory, then sorts the entries into path order before
|
|
242
|
+
applying `after`.
|
|
199
243
|
|
|
200
|
-
###
|
|
244
|
+
### Dependencies
|
|
201
245
|
|
|
202
|
-
-
|
|
203
|
-
- On Durable Objects and D1 a row may not exceed 2 MB, so the *stored* (possibly compressed) size
|
|
204
|
-
of one file is capped there. Use `SqliteFilesApi` in those runtimes.
|
|
205
|
-
- Multi-row mutations (`copy`, `move`, `remove` of a tree) are not wrapped in a transaction.
|
|
206
|
-
- Only the final path component is resolved through symlinks.
|
|
207
|
-
- `list` loads every row under the directory, then sorts the entries into path order (implicit
|
|
208
|
-
directories and resolved links do not sit where the rows do) before applying `after`.
|
|
246
|
+
`@statewalker/webrun-files` only. SQLite handles and `pako` are passed in by the caller.
|
|
209
247
|
|
|
210
248
|
## License
|
|
211
249
|
|
package/package.json
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@statewalker/webrun-files-sqlite",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "SQLite-backed FilesApi implementations",
|
|
7
|
-
"
|
|
7
|
+
"bugs": {
|
|
8
|
+
"url": "https://github.com/statewalker/webrun-files/issues"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/statewalker/webrun-files/tree/main/packages/webrun-files-sqlite#readme",
|
|
8
11
|
"author": {
|
|
9
12
|
"name": "Mikhail Kotelnikov",
|
|
10
13
|
"email": "mikhail.kotelnikov@gmail.com"
|
|
@@ -12,7 +15,8 @@
|
|
|
12
15
|
"license": "MIT",
|
|
13
16
|
"repository": {
|
|
14
17
|
"type": "git",
|
|
15
|
-
"url": "git
|
|
18
|
+
"url": "git+https://github.com/statewalker/webrun-files.git",
|
|
19
|
+
"directory": "packages/webrun-files-sqlite"
|
|
16
20
|
},
|
|
17
21
|
"main": "./dist/cjs/index.cjs",
|
|
18
22
|
"module": "./dist/esm/index.js",
|
|
@@ -30,7 +34,7 @@
|
|
|
30
34
|
"src"
|
|
31
35
|
],
|
|
32
36
|
"dependencies": {
|
|
33
|
-
"@statewalker/webrun-files": "^0.10.
|
|
37
|
+
"@statewalker/webrun-files": "^0.10.2"
|
|
34
38
|
},
|
|
35
39
|
"devDependencies": {
|
|
36
40
|
"@types/node": "^26.6.4",
|