@zenfs/core 0.11.2 → 0.12.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/dist/file.d.ts CHANGED
@@ -357,7 +357,7 @@ export declare class PreloadFile<FS extends FileSystem> extends File {
357
357
  * For the filesystems which do not sync to anything..
358
358
  */
359
359
  export declare class NoSyncFile<T extends FileSystem> extends PreloadFile<T> {
360
- constructor(_fs: T, _path: string, _flag: string, _stat: Stats, contents?: Uint8Array);
360
+ constructor(fs: T, path: string, flag: string, stats: Stats, contents?: Uint8Array);
361
361
  /**
362
362
  * Asynchronous sync. Doesn't do anything, simply calls the cb.
363
363
  */
package/dist/file.js CHANGED
@@ -474,8 +474,8 @@ export class PreloadFile extends File {
474
474
  * For the filesystems which do not sync to anything..
475
475
  */
476
476
  export class NoSyncFile extends PreloadFile {
477
- constructor(_fs, _path, _flag, _stat, contents) {
478
- super(_fs, _path, _flag, _stat, contents);
477
+ constructor(fs, path, flag, stats, contents) {
478
+ super(fs, path, flag, stats, contents);
479
479
  }
480
480
  /**
481
481
  * Asynchronous sync. Doesn't do anything, simply calls the cb.
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ export * from './error.js';
2
2
  export * from './backends/port/fs.js';
3
3
  export * from './backends/fetch.js';
4
4
  export * from './backends/memory.js';
5
- export * from './backends/Index.js';
5
+ export * from './backends/index/fs.js';
6
6
  export * from './backends/locked.js';
7
7
  export * from './backends/overlay.js';
8
8
  export * from './backends/store/fs.js';
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ export * from './error.js';
2
2
  export * from './backends/port/fs.js';
3
3
  export * from './backends/fetch.js';
4
4
  export * from './backends/memory.js';
5
- export * from './backends/Index.js';
5
+ export * from './backends/index/fs.js';
6
6
  export * from './backends/locked.js';
7
7
  export * from './backends/overlay.js';
8
8
  export * from './backends/store/fs.js';
package/dist/stats.d.ts CHANGED
@@ -12,45 +12,45 @@ export declare enum FileType {
12
12
  /**
13
13
  *
14
14
  */
15
- export interface StatsLike {
15
+ export interface StatsLike<T extends number | bigint = number | bigint> {
16
16
  /**
17
17
  * Size of the item in bytes.
18
18
  * For directories/symlinks, this is normally the size of the struct that represents the item.
19
19
  */
20
- size: number | bigint;
20
+ size: T;
21
21
  /**
22
22
  * Unix-style file mode (e.g. 0o644) that includes the item type
23
23
  * Type of the item can be FILE, DIRECTORY, SYMLINK, or SOCKET
24
24
  */
25
- mode: number | bigint;
25
+ mode: T;
26
26
  /**
27
27
  * time of last access, in milliseconds since epoch
28
28
  */
29
- atimeMs: number | bigint;
29
+ atimeMs: T;
30
30
  /**
31
31
  * time of last modification, in milliseconds since epoch
32
32
  */
33
- mtimeMs: number | bigint;
33
+ mtimeMs: T;
34
34
  /**
35
35
  * time of last time file status was changed, in milliseconds since epoch
36
36
  */
37
- ctimeMs: number | bigint;
37
+ ctimeMs: T;
38
38
  /**
39
39
  * time of file creation, in milliseconds since epoch
40
40
  */
41
- birthtimeMs: number | bigint;
41
+ birthtimeMs: T;
42
42
  /**
43
43
  * the id of the user that owns the file
44
44
  */
45
- uid: number | bigint;
45
+ uid: T;
46
46
  /**
47
47
  * the id of the group that owns the file
48
48
  */
49
- gid: number | bigint;
49
+ gid: T;
50
50
  /**
51
51
  * the ino
52
52
  */
53
- ino: number | bigint;
53
+ ino: T;
54
54
  }
55
55
  /**
56
56
  * Provides information about a particular entry in the file system.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenfs/core",
3
- "version": "0.11.2",
3
+ "version": "0.12.0",
4
4
  "description": "A filesystem in your browser",
5
5
  "main": "dist/index.js",
6
6
  "types": "src/index.ts",
@@ -2,7 +2,7 @@
2
2
  import { parseArgs } from 'util';
3
3
  import { statSync, readdirSync, writeFileSync } from 'fs';
4
4
  import { join } from 'path/posix';
5
- import { resolve } from 'path';
5
+ import { relative, resolve } from 'path';
6
6
  import { minimatch } from 'minimatch';
7
7
 
8
8
  const { values: options, positionals } = parseArgs({
@@ -38,10 +38,12 @@ if (options.quiet && options.verbose) {
38
38
  process.exit();
39
39
  }
40
40
 
41
- function pathToPosix(path) {
41
+ function fixSlash(path) {
42
42
  return path.replaceAll('\\', '/');
43
43
  }
44
44
 
45
+ const resolvedRoot = root || '.';
46
+
45
47
  const colors = {
46
48
  reset: 0,
47
49
  black: 30,
@@ -66,28 +68,31 @@ function color(color, text) {
66
68
  return `\x1b[${colors[color]}m${text}\x1b[0m`;
67
69
  }
68
70
 
69
- function listing(path, seen = new Set()) {
71
+ const entries = new Map();
72
+
73
+ function computeEntries(path) {
70
74
  try {
71
- if (options.verbose) console.log(`${color('blue', 'list')} ${path}`);
75
+ if (options.ignore.some(pattern => minimatch(path, pattern))) {
76
+ if (!options.quiet) console.log(`${color('yellow', 'skip')} ${path}`);
77
+ return;
78
+ }
79
+
72
80
  const stats = statSync(path);
81
+ entries.set('/' + relative(resolvedRoot, path), stats);
73
82
 
74
83
  if (stats.isFile()) {
75
- if (options.verbose) console.log(`${color('green', 'file')} ${path}`);
76
- return null;
84
+ if (options.verbose) {
85
+ console.log(`${color('green', 'file')} ${path}`);
86
+ }
87
+ return;
77
88
  }
78
89
 
79
- const entries = {};
80
90
  for (const file of readdirSync(path)) {
81
- const full = join(path, file);
82
- if (options.ignore.some(pattern => minimatch(full, pattern))) {
83
- if (!options.quiet) console.log(`${color('yellow', 'skip')} ${full}`);
84
- continue;
85
- }
86
-
87
- entries[file] = listing(full, seen);
91
+ computeEntries(join(path, file));
92
+ }
93
+ if (options.verbose) {
94
+ console.log(`${color('bright_green', ' dir')} ${path}`);
88
95
  }
89
- if (options.verbose) console.log(`${color('bright_green', ' dir')} ${path}`);
90
- return entries;
91
96
  } catch (e) {
92
97
  if (!options.quiet) {
93
98
  console.log(`${color('red', 'fail')} ${path}: ${e.message}`);
@@ -95,7 +100,14 @@ function listing(path, seen = new Set()) {
95
100
  }
96
101
  }
97
102
 
98
- const rootListing = listing(pathToPosix(root));
99
- if (!options.quiet) console.log('Generated listing for ' + pathToPosix(resolve(root)));
103
+ computeEntries(resolvedRoot);
104
+ if (!options.quiet) {
105
+ console.log('Generated listing for ' + fixSlash(resolve(root)));
106
+ }
107
+
108
+ const index = {
109
+ version: 1,
110
+ entries: Object.fromEntries(entries),
111
+ };
100
112
 
101
- writeFileSync(options.output, JSON.stringify(rootListing));
113
+ writeFileSync(options.output, JSON.stringify(index));
@@ -1,9 +1,9 @@
1
- import { ErrnoError, Errno } from '../error.js';
2
- import { NoSyncFile } from '../file.js';
1
+ import { Errno, ErrnoError } from '../error.js';
3
2
  import type { FileSystemMetadata } from '../filesystem.js';
4
3
  import { Stats } from '../stats.js';
5
- import { type ListingTree, FileIndex, type IndexFileInode, AsyncIndexFS } from './Index.js';
6
4
  import type { Backend } from './backend.js';
5
+ import { IndexFS } from './index/fs.js';
6
+ import type { IndexData } from './index/index.js';
7
7
 
8
8
  /**
9
9
  * Asynchronously download a file as a buffer or a JSON object.
@@ -37,20 +37,6 @@ async function fetchFile<T extends object>(path: string, type: 'buffer' | 'json'
37
37
  }
38
38
  }
39
39
 
40
- /**
41
- * Asynchronously retrieves the size of the given file in bytes.
42
- * @hidden
43
- */
44
- async function fetchSize(path: string): Promise<number> {
45
- const response = await fetch(path, { method: 'HEAD' }).catch(e => {
46
- throw new ErrnoError(Errno.EIO, e.message);
47
- });
48
- if (!response.ok) {
49
- throw new ErrnoError(Errno.EIO, 'fetch failed: HEAD response returned code ' + response.status);
50
- }
51
- return parseInt(response.headers.get('Content-Length') || '-1', 10);
52
- }
53
-
54
40
  /**
55
41
  * Configuration options for FetchFS.
56
42
  */
@@ -59,7 +45,7 @@ export interface FetchOptions {
59
45
  * URL to a file index as a JSON file or the file index object itself.
60
46
  * Defaults to `index.json`.
61
47
  */
62
- index?: string | ListingTree;
48
+ index?: string | IndexData;
63
49
 
64
50
  /** Used as the URL prefix for fetched files.
65
51
  * Default: Fetch files relative to the index.
@@ -68,59 +54,48 @@ export interface FetchOptions {
68
54
  }
69
55
 
70
56
  /**
71
- * A simple filesystem backed by HTTP using the fetch API.
57
+ * A simple filesystem backed by HTTP using the `fetch` API.
72
58
  *
73
59
  *
74
- * Listings objects look like the following:
60
+ * Index objects look like the following:
75
61
  *
76
62
  * ```json
77
63
  * {
78
- * "home": {
79
- * "jvilk": {
80
- * "someFile.txt": null,
81
- * "someDir": {
82
- * // Empty directory
83
- * }
84
- * }
85
- * }
64
+ * "version": 1,
65
+ * "entries": {
66
+ * "/home": { ... },
67
+ * "/home/jvilk": { ... },
68
+ * "/home/james": { ... }
69
+ * }
86
70
  * }
87
71
  * ```
88
72
  *
89
- * This example has the folder `/home/jvilk` with subfile `someFile.txt` and subfolder `someDir`.
73
+ * Each entry contains the stats associated with the file.
90
74
  */
91
- export class FetchFS extends AsyncIndexFS<Stats> {
92
- public readonly prefixUrl: string;
75
+ export class FetchFS extends IndexFS {
76
+ public readonly baseUrl: string;
93
77
 
94
- protected _init: Promise<void>;
95
-
96
- protected async _initialize(index: string | ListingTree): Promise<void> {
97
- if (typeof index != 'string') {
98
- this._index = FileIndex.FromListing(index);
78
+ public async ready(): Promise<void> {
79
+ if (this._isInitialized) {
99
80
  return;
100
81
  }
101
-
102
- try {
103
- const response = await fetch(index);
104
- this._index = FileIndex.FromListing((await response.json()) as ListingTree);
105
- } catch (e) {
106
- throw new ErrnoError(Errno.EINVAL, 'Invalid or unavailable file listing tree');
82
+ await super.ready();
83
+ /**
84
+ * Iterate over all of the files and cache their contents
85
+ */
86
+ for (const [path, stats] of this.index.files()) {
87
+ await this.getData(path, stats);
107
88
  }
108
89
  }
109
90
 
110
- public async ready(): Promise<void> {
111
- await this._init;
112
- }
113
-
114
91
  constructor({ index = 'index.json', baseUrl = '' }: FetchOptions) {
115
- super({});
92
+ super(typeof index != 'string' ? index : fetchFile<IndexData>(index, 'json'));
116
93
 
117
94
  // prefix url must end in a directory separator.
118
95
  if (baseUrl.at(-1) != '/') {
119
96
  baseUrl += '/';
120
97
  }
121
- this.prefixUrl = baseUrl;
122
-
123
- this._init = this._initialize(index);
98
+ this.baseUrl = baseUrl;
124
99
  }
125
100
 
126
101
  public metadata(): FileSystemMetadata {
@@ -131,76 +106,42 @@ export class FetchFS extends AsyncIndexFS<Stats> {
131
106
  };
132
107
  }
133
108
 
134
- public empty(): void {
135
- for (const file of this._index.files()) {
136
- delete file.data!.fileData;
137
- }
138
- }
139
-
140
109
  /**
141
- * Special function: Preload the given file into the index.
110
+ * Preload the given file into the index.
142
111
  * @param path
143
112
  * @param buffer
144
113
  */
145
- public preloadFile(path: string, buffer: Uint8Array): void {
146
- const inode = this._index.get(path)!;
147
- if (!inode) {
114
+ public preload(path: string, buffer: Uint8Array): void {
115
+ const stats = this.index.get(path);
116
+ if (!stats) {
148
117
  throw ErrnoError.With('ENOENT', path, 'preloadFile');
149
118
  }
150
- if (!inode.isFile()) {
119
+ if (!stats.isFile()) {
151
120
  throw ErrnoError.With('EISDIR', path, 'preloadFile');
152
121
  }
153
- const stats = inode.data!;
154
122
  stats.size = buffer.length;
155
123
  stats.fileData = buffer;
156
124
  }
157
125
 
158
- protected async statFileInode(inode: IndexFileInode<Stats>, path: string): Promise<Stats> {
159
- const stats = inode.data!;
160
- // At this point, a non-opened file will still have default stats from the listing.
161
- if (stats.size < 0) {
162
- stats.size = await this._fetchSize(path);
163
- }
164
-
165
- return stats;
166
- }
167
-
168
- protected async openFileInode(inode: IndexFileInode<Stats>, path: string, flag: string): Promise<NoSyncFile<this>> {
169
- const stats = inode.data!;
170
- // Use existing file contents. This maintains the previously-used flag.
126
+ /**
127
+ * @todo Be lazier about actually requesting the data?
128
+ */
129
+ protected async getData(path: string, stats: Stats): Promise<Uint8Array> {
171
130
  if (stats.fileData) {
172
- return new NoSyncFile(this, path, flag, new Stats(stats), stats.fileData);
131
+ return stats.fileData;
173
132
  }
174
- // @todo be lazier about actually requesting the file
175
- const data = await this._fetchFile(path, 'buffer');
176
- // we don't initially have file sizes
177
- stats.size = data.length;
133
+
134
+ const data = await fetchFile(this.baseUrl + (path.startsWith('/') ? path.slice(1) : path), 'buffer');
178
135
  stats.fileData = data;
179
- return new NoSyncFile(this, path, flag, new Stats(stats), data);
136
+ return data;
180
137
  }
181
138
 
182
- private _getRemotePath(filePath: string): string {
183
- if (filePath.charAt(0) === '/') {
184
- filePath = filePath.slice(1);
139
+ protected getDataSync(path: string, stats: Stats): Uint8Array {
140
+ if (stats.fileData) {
141
+ return stats.fileData;
185
142
  }
186
- return this.prefixUrl + filePath;
187
- }
188
-
189
- /**
190
- * Asynchronously download the given file.
191
- */
192
- protected _fetchFile(path: string, type: 'buffer'): Promise<Uint8Array>;
193
- protected _fetchFile(path: string, type: 'json'): Promise<object>;
194
- protected _fetchFile(path: string, type: 'buffer' | 'json'): Promise<object>;
195
- protected _fetchFile(path: string, type: 'buffer' | 'json'): Promise<object> {
196
- return fetchFile(this._getRemotePath(path), type);
197
- }
198
143
 
199
- /**
200
- * Only requests the HEAD content, for the file size.
201
- */
202
- protected _fetchSize(path: string): Promise<number> {
203
- return fetchSize(this._getRemotePath(path));
144
+ throw new ErrnoError(Errno.ENODATA, '', path, 'getData');
204
145
  }
205
146
  }
206
147
 
@@ -0,0 +1,113 @@
1
+ import type { Cred } from '../../cred.js';
2
+ import { ErrnoError, Errno } from '../../error.js';
3
+ import { NoSyncFile, isWriteable, flagToMode } from '../../file.js';
4
+ import { Readonly, FileSystem } from '../../filesystem.js';
5
+ import type { Stats } from '../../stats.js';
6
+ import { decode } from '../../utils.js';
7
+ import { Index, IndexData } from './index.js';
8
+
9
+ export abstract class IndexFS extends Readonly(FileSystem) {
10
+ protected index: Index = new Index();
11
+
12
+ protected _isInitialized: boolean = false;
13
+
14
+ public async ready(): Promise<void> {
15
+ await super.ready();
16
+ if (this._isInitialized) {
17
+ return;
18
+ }
19
+ this.index.fromJSON(await this.indexData);
20
+ this._isInitialized = true;
21
+ }
22
+
23
+ constructor(private indexData: IndexData | Promise<IndexData>) {
24
+ super();
25
+ }
26
+
27
+ public async reloadFiles(): Promise<void> {
28
+ for (const [path, stats] of this.index.files()) {
29
+ delete stats.fileData;
30
+ stats.fileData = await this.getData(path, stats);
31
+ }
32
+ }
33
+
34
+ public reloadFilesSync(): void {
35
+ for (const [path, stats] of this.index.files()) {
36
+ delete stats.fileData;
37
+ stats.fileData = this.getDataSync(path, stats);
38
+ }
39
+ }
40
+
41
+ public async stat(path: string): Promise<Stats> {
42
+ return this.statSync(path);
43
+ }
44
+
45
+ public statSync(path: string): Stats {
46
+ if (!this.index.has(path)) {
47
+ throw ErrnoError.With('ENOENT', path, 'stat');
48
+ }
49
+
50
+ return this.index.get(path)!;
51
+ }
52
+
53
+ public async openFile(path: string, flag: string, cred: Cred): Promise<NoSyncFile<this>> {
54
+ if (isWriteable(flag)) {
55
+ // You can't write to files on this file system.
56
+ throw new ErrnoError(Errno.EPERM, path);
57
+ }
58
+
59
+ // Check if the path exists, and is a file.
60
+ const stats = this.index.get(path);
61
+
62
+ if (!stats) {
63
+ throw ErrnoError.With('ENOENT', path, 'openFile');
64
+ }
65
+
66
+ if (!stats.hasAccess(flagToMode(flag), cred)) {
67
+ throw ErrnoError.With('EACCES', path, 'openFile');
68
+ }
69
+
70
+ return new NoSyncFile(this, path, flag, stats, stats.isDirectory() ? stats.fileData : await this.getData(path, stats));
71
+ }
72
+
73
+ public openFileSync(path: string, flag: string, cred: Cred): NoSyncFile<this> {
74
+ if (isWriteable(flag)) {
75
+ // You can't write to files on this file system.
76
+ throw new ErrnoError(Errno.EPERM, path);
77
+ }
78
+
79
+ // Check if the path exists, and is a file.
80
+ const stats = this.index.get(path);
81
+
82
+ if (!stats) {
83
+ throw ErrnoError.With('ENOENT', path, 'openFile');
84
+ }
85
+
86
+ if (!stats.hasAccess(flagToMode(flag), cred)) {
87
+ throw ErrnoError.With('EACCES', path, 'openFile');
88
+ }
89
+
90
+ return new NoSyncFile(this, path, flag, stats, stats.isDirectory() ? stats.fileData : this.getDataSync(path, stats));
91
+ }
92
+
93
+ public async readdir(path: string): Promise<string[]> {
94
+ return this.readdirSync(path);
95
+ }
96
+
97
+ public readdirSync(path: string): string[] {
98
+ // Check if it exists.
99
+ const stats = this.index.get(path);
100
+ if (!stats) {
101
+ throw ErrnoError.With('ENOENT', path, 'readdir');
102
+ }
103
+
104
+ if (!stats.isDirectory()) {
105
+ throw ErrnoError.With('ENOTDIR', path, 'readdir');
106
+ }
107
+
108
+ return JSON.parse(decode(stats.fileData));
109
+ }
110
+
111
+ protected abstract getData(path: string, stats: Stats): Promise<Uint8Array>;
112
+ protected abstract getDataSync(path: string, stats: Stats): Uint8Array;
113
+ }
@@ -0,0 +1,98 @@
1
+ import { isJSON } from 'utilium';
2
+ import { Errno, ErrnoError } from '../../error.js';
3
+ import { Stats, StatsLike } from '../../stats.js';
4
+ import { encode } from '../../utils.js';
5
+ import { basename, dirname } from '../../emulation/path.js';
6
+
7
+ export interface IndexData {
8
+ version: 1;
9
+ entries: Record<string, StatsLike<number>>;
10
+ }
11
+
12
+ export const version = 1;
13
+
14
+ /**
15
+ * An index of files
16
+ */
17
+ export class Index extends Map<string, Stats> {
18
+ public constructor() {
19
+ super();
20
+ }
21
+
22
+ /**
23
+ * Convience method
24
+ */
25
+ public files(): Map<string, Stats> {
26
+ const files = new Map<string, Stats>();
27
+ for (const [path, stats] of this) {
28
+ if (stats.isFile()) {
29
+ files.set(path, stats);
30
+ }
31
+ }
32
+ return files;
33
+ }
34
+
35
+ /**
36
+ * Converts the index to JSON
37
+ */
38
+ public toJSON(): IndexData {
39
+ return {
40
+ version,
41
+ entries: Object.fromEntries(this),
42
+ };
43
+ }
44
+
45
+ /**
46
+ * Converts the index to a string
47
+ */
48
+ public toString(): string {
49
+ return JSON.stringify(this.toJSON());
50
+ }
51
+
52
+ /**
53
+ * Returns the files in the directory `dir`.
54
+ * This is expensive so it is only called once per directory.
55
+ */
56
+ protected dirEntries(dir: string): string[] {
57
+ const entries = [];
58
+ for (const entry of this.keys()) {
59
+ if (dirname(entry) == dir) {
60
+ entries.push(basename(entry));
61
+ }
62
+ }
63
+ return entries;
64
+ }
65
+
66
+ /**
67
+ * Loads the index from JSON data
68
+ */
69
+ public fromJSON(json: IndexData): void {
70
+ if (json.version != version) {
71
+ throw new ErrnoError(Errno.EINVAL, 'Index version mismatch');
72
+ }
73
+
74
+ this.clear();
75
+
76
+ for (const [path, data] of Object.entries(json.entries)) {
77
+ const stats = new Stats(data);
78
+ if (stats.isDirectory()) {
79
+ stats.fileData = encode(JSON.stringify(this.dirEntries(path)));
80
+ }
81
+ this.set(path, stats);
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Parses an index from a string
87
+ */
88
+ public static parse(data: string): Index {
89
+ if (!isJSON(data)) {
90
+ throw new ErrnoError(Errno.EINVAL, 'Invalid JSON');
91
+ }
92
+
93
+ const json = JSON.parse(data);
94
+ const index = new Index();
95
+ index.fromJSON(json);
96
+ return index;
97
+ }
98
+ }
@@ -0,0 +1,3 @@
1
+ # IndexFS
2
+
3
+ The `IndexFS` class is a base class for other backends that uses an `Index` to store stats for files and directories. The `Index` class inherits from `Map` and stores information about files and directories in a file system.
package/src/file.ts CHANGED
@@ -701,8 +701,8 @@ export class PreloadFile<FS extends FileSystem> extends File {
701
701
  * For the filesystems which do not sync to anything..
702
702
  */
703
703
  export class NoSyncFile<T extends FileSystem> extends PreloadFile<T> {
704
- constructor(_fs: T, _path: string, _flag: string, _stat: Stats, contents?: Uint8Array) {
705
- super(_fs, _path, _flag, _stat, contents);
704
+ constructor(fs: T, path: string, flag: string, stats: Stats, contents?: Uint8Array) {
705
+ super(fs, path, flag, stats, contents);
706
706
  }
707
707
  /**
708
708
  * Asynchronous sync. Doesn't do anything, simply calls the cb.
package/src/index.ts CHANGED
@@ -2,7 +2,7 @@ export * from './error.js';
2
2
  export * from './backends/port/fs.js';
3
3
  export * from './backends/fetch.js';
4
4
  export * from './backends/memory.js';
5
- export * from './backends/Index.js';
5
+ export * from './backends/index/fs.js';
6
6
  export * from './backends/locked.js';
7
7
  export * from './backends/overlay.js';
8
8
  export * from './backends/store/fs.js';