@zenfs/dom 1.0.7 → 1.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/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/xml.d.ts +54 -0
- package/dist/xml.js +177 -0
- package/package.json +3 -5
- package/readme.md +45 -9
- /package/dist/{Storage.d.ts → storage.d.ts} +0 -0
- /package/dist/{Storage.js → storage.js} +0 -0
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/xml.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { File, StatsLike } from '@zenfs/core';
|
|
2
|
+
import { FileSystem, Stats } from '@zenfs/core';
|
|
3
|
+
export interface XMLOptions {
|
|
4
|
+
/**
|
|
5
|
+
* The root `fs` element
|
|
6
|
+
*/
|
|
7
|
+
root?: Element;
|
|
8
|
+
}
|
|
9
|
+
declare const XMLFS_base: import("@zenfs/core").Mixin<typeof FileSystem, import("utilium").ExtractProperties<FileSystem, (...args: any[]) => Promise<unknown>>>;
|
|
10
|
+
export declare class XMLFS extends XMLFS_base {
|
|
11
|
+
/**
|
|
12
|
+
* @inheritdoc XMLOptions.root
|
|
13
|
+
*/
|
|
14
|
+
readonly root: Element;
|
|
15
|
+
constructor(
|
|
16
|
+
/**
|
|
17
|
+
* @inheritdoc XMLOptions.root
|
|
18
|
+
*/
|
|
19
|
+
root?: Element);
|
|
20
|
+
renameSync(oldPath: string, newPath: string): void;
|
|
21
|
+
statSync(path: string): Stats;
|
|
22
|
+
openFileSync(path: string, flag: string): File;
|
|
23
|
+
createFileSync(path: string, flag: string, mode: number): File;
|
|
24
|
+
unlinkSync(path: string): void;
|
|
25
|
+
rmdirSync(path: string): void;
|
|
26
|
+
mkdirSync(path: string, mode: number): void;
|
|
27
|
+
readdirSync(path: string): string[];
|
|
28
|
+
linkSync(target: string, link: string): void;
|
|
29
|
+
syncSync(path: string, data: Uint8Array, stats: Readonly<Stats>): void;
|
|
30
|
+
toString(): string;
|
|
31
|
+
protected get(syscall: string, path: string): Element;
|
|
32
|
+
protected create(syscall: string, path: string, stats?: Partial<StatsLike<number>>): Element;
|
|
33
|
+
protected add(syscall: string, node: Element, path: string, contents?: boolean): void;
|
|
34
|
+
protected remove(syscall: string, node: Element, path: string, contents?: boolean): void;
|
|
35
|
+
}
|
|
36
|
+
declare const _XML: {
|
|
37
|
+
name: string;
|
|
38
|
+
options: {
|
|
39
|
+
root: {
|
|
40
|
+
type: "object";
|
|
41
|
+
required: false;
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
isAvailable(): boolean;
|
|
45
|
+
create(options: XMLOptions): XMLFS;
|
|
46
|
+
};
|
|
47
|
+
type _XML = typeof _XML;
|
|
48
|
+
export interface XML extends _XML {
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* @experimental
|
|
52
|
+
*/
|
|
53
|
+
export declare const XML: XML;
|
|
54
|
+
export {};
|
package/dist/xml.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { decodeRaw, encodeRaw, Errno, ErrnoError, FileSystem, PreloadFile, Stats, Sync } from '@zenfs/core';
|
|
2
|
+
import { S_IFDIR, S_IFREG } from '@zenfs/core/emulation/constants.js';
|
|
3
|
+
import { basename, dirname } from '@zenfs/core/path';
|
|
4
|
+
const statsLikeKeys = ['size', 'mode', 'atimeMs', 'mtimeMs', 'ctimeMs', 'birthtimeMs', 'uid', 'gid', 'ino', 'nlink'];
|
|
5
|
+
function get_stats(node) {
|
|
6
|
+
const stats = {};
|
|
7
|
+
for (const key of statsLikeKeys) {
|
|
8
|
+
const value = node.getAttribute(key);
|
|
9
|
+
stats[key] = value != null ? parseInt(value, 16) : undefined;
|
|
10
|
+
}
|
|
11
|
+
return new Stats(stats);
|
|
12
|
+
}
|
|
13
|
+
function set_stats(node, stats) {
|
|
14
|
+
for (const key of statsLikeKeys) {
|
|
15
|
+
if (stats[key] != undefined) {
|
|
16
|
+
node.setAttribute(key, stats[key].toString(16));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function get_paths(node, contents = false) {
|
|
21
|
+
let paths;
|
|
22
|
+
try {
|
|
23
|
+
const raw = contents ? node.textContent : node.getAttribute('paths');
|
|
24
|
+
paths = JSON.parse(raw || '[]');
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
paths = [];
|
|
28
|
+
}
|
|
29
|
+
return paths;
|
|
30
|
+
}
|
|
31
|
+
export class XMLFS extends Sync(FileSystem) {
|
|
32
|
+
constructor(
|
|
33
|
+
/**
|
|
34
|
+
* @inheritdoc XMLOptions.root
|
|
35
|
+
*/
|
|
36
|
+
root = new DOMParser().parseFromString('<fs></fs>', 'application/xml').documentElement) {
|
|
37
|
+
super();
|
|
38
|
+
this.root = root;
|
|
39
|
+
try {
|
|
40
|
+
this.mkdirSync('/', 0o777);
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
const error = e;
|
|
44
|
+
if (error.code != 'EEXIST')
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
renameSync(oldPath, newPath) {
|
|
49
|
+
const node = this.get('rename', oldPath);
|
|
50
|
+
this.remove('rename', node, oldPath);
|
|
51
|
+
this.add('rename', node, newPath);
|
|
52
|
+
}
|
|
53
|
+
statSync(path) {
|
|
54
|
+
return get_stats(this.get('stat', path));
|
|
55
|
+
}
|
|
56
|
+
openFileSync(path, flag) {
|
|
57
|
+
const node = this.get('openFile', path);
|
|
58
|
+
return new PreloadFile(this, path, flag, get_stats(node), encodeRaw(node.textContent));
|
|
59
|
+
}
|
|
60
|
+
createFileSync(path, flag, mode) {
|
|
61
|
+
const stats = new Stats({ mode: mode | S_IFREG });
|
|
62
|
+
this.create('createFile', path, stats);
|
|
63
|
+
return new PreloadFile(this, path, flag, stats);
|
|
64
|
+
}
|
|
65
|
+
unlinkSync(path) {
|
|
66
|
+
const node = this.get('unlink', path);
|
|
67
|
+
if (get_stats(node).isDirectory())
|
|
68
|
+
throw ErrnoError.With('EISDIR', path, 'unlink');
|
|
69
|
+
this.remove('unlink', node, path);
|
|
70
|
+
}
|
|
71
|
+
rmdirSync(path) {
|
|
72
|
+
const node = this.get('rmdir', path);
|
|
73
|
+
if (node.textContent?.length)
|
|
74
|
+
throw ErrnoError.With('ENOTEMPTY', path, 'rmdir');
|
|
75
|
+
if (!get_stats(node).isDirectory())
|
|
76
|
+
throw ErrnoError.With('ENOTDIR', path, 'rmdir');
|
|
77
|
+
this.remove('rmdir', node, path);
|
|
78
|
+
}
|
|
79
|
+
mkdirSync(path, mode) {
|
|
80
|
+
const node = this.create('mkdir', path, { mode: mode | S_IFDIR });
|
|
81
|
+
node.textContent = '[]';
|
|
82
|
+
}
|
|
83
|
+
readdirSync(path) {
|
|
84
|
+
const node = this.get('readdir', path);
|
|
85
|
+
if (!get_stats(node).isDirectory())
|
|
86
|
+
throw ErrnoError.With('ENOTDIR', path, 'rmdir');
|
|
87
|
+
try {
|
|
88
|
+
return JSON.parse(node.textContent);
|
|
89
|
+
}
|
|
90
|
+
catch (e) {
|
|
91
|
+
throw new ErrnoError(Errno.EIO, 'Invalid directory listing: ' + e, path, 'readdir');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
linkSync(target, link) {
|
|
95
|
+
const node = this.get('link', target);
|
|
96
|
+
this.add('link', node, link);
|
|
97
|
+
}
|
|
98
|
+
syncSync(path, data, stats) {
|
|
99
|
+
const node = this.get('sync', path);
|
|
100
|
+
node.textContent = decodeRaw(data);
|
|
101
|
+
set_stats(node, stats);
|
|
102
|
+
}
|
|
103
|
+
toString() {
|
|
104
|
+
return new XMLSerializer().serializeToString(this.root);
|
|
105
|
+
}
|
|
106
|
+
get(syscall, path) {
|
|
107
|
+
const nodes = this.root.children;
|
|
108
|
+
if (!nodes)
|
|
109
|
+
throw ErrnoError.With('EIO', path, syscall);
|
|
110
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
111
|
+
if (get_paths(nodes[i]).includes(path))
|
|
112
|
+
return nodes[i];
|
|
113
|
+
}
|
|
114
|
+
throw ErrnoError.With('ENOENT', path, syscall);
|
|
115
|
+
}
|
|
116
|
+
create(syscall, path, stats = {}) {
|
|
117
|
+
if (this.existsSync(path))
|
|
118
|
+
throw ErrnoError.With('EEXIST', path, syscall);
|
|
119
|
+
const node = document.createElement('file');
|
|
120
|
+
this.add(syscall, node, path);
|
|
121
|
+
set_stats(node, new Stats(stats));
|
|
122
|
+
this.root.append(node);
|
|
123
|
+
return node;
|
|
124
|
+
}
|
|
125
|
+
add(syscall, node, path, contents = false) {
|
|
126
|
+
const paths = get_paths(node, contents);
|
|
127
|
+
paths.push(path);
|
|
128
|
+
if (contents) {
|
|
129
|
+
node.textContent = JSON.stringify(paths);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
node.setAttribute('paths', JSON.stringify(paths));
|
|
133
|
+
node.setAttribute('nlink', paths.length.toString(16));
|
|
134
|
+
if (path != '/') {
|
|
135
|
+
const parent = this.get(syscall, dirname(path));
|
|
136
|
+
this.add(syscall, parent, basename(path), true);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
remove(syscall, node, path, contents = false) {
|
|
140
|
+
const paths = get_paths(node, contents);
|
|
141
|
+
const i = paths.indexOf(path);
|
|
142
|
+
if (i == -1)
|
|
143
|
+
return;
|
|
144
|
+
paths.splice(i, 1);
|
|
145
|
+
if (contents) {
|
|
146
|
+
node.textContent = JSON.stringify(paths);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (!paths.length) {
|
|
150
|
+
node.remove();
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
node.setAttribute('paths', JSON.stringify(paths));
|
|
154
|
+
node.setAttribute('nlink', paths.length.toString(16));
|
|
155
|
+
}
|
|
156
|
+
if (path != '/') {
|
|
157
|
+
const parent = this.get(syscall, dirname(path));
|
|
158
|
+
this.remove(syscall, parent, basename(path), true);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const _XML = {
|
|
163
|
+
name: 'XML',
|
|
164
|
+
options: {
|
|
165
|
+
root: { type: 'object', required: false },
|
|
166
|
+
},
|
|
167
|
+
isAvailable() {
|
|
168
|
+
return true;
|
|
169
|
+
},
|
|
170
|
+
create(options) {
|
|
171
|
+
return new XMLFS(options.root);
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
/**
|
|
175
|
+
* @experimental
|
|
176
|
+
*/
|
|
177
|
+
export const XML = _XML;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zenfs/dom",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "DOM backends for ZenFS",
|
|
5
5
|
"funding": {
|
|
6
6
|
"type": "individual",
|
|
@@ -47,6 +47,8 @@
|
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@eslint/js": "^9.12.0",
|
|
49
49
|
"eslint": "^9.12.0",
|
|
50
|
+
"fake-indexeddb": "^6.0.0",
|
|
51
|
+
"file-system-access": "^1.0.4",
|
|
50
52
|
"globals": "^15.10.0",
|
|
51
53
|
"prettier": "^3.2.5",
|
|
52
54
|
"tsx": "^4.19.2",
|
|
@@ -57,10 +59,6 @@
|
|
|
57
59
|
"peerDependencies": {
|
|
58
60
|
"@zenfs/core": "^1.6.4"
|
|
59
61
|
},
|
|
60
|
-
"optionalDependencies": {
|
|
61
|
-
"fake-indexeddb": "^6.0.0",
|
|
62
|
-
"file-system-access": "^1.0.4"
|
|
63
|
-
},
|
|
64
62
|
"keywords": [
|
|
65
63
|
"filesystem",
|
|
66
64
|
"node",
|
package/readme.md
CHANGED
|
@@ -2,23 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
[ZenFS](https://github.com/zen-fs/core) backends for DOM APIs. DOM APIs are _only_ available natively in browsers.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
> Please read the ZenFS core documentation!
|
|
5
|
+
Please read the ZenFS core documentation!
|
|
7
6
|
|
|
8
7
|
## Backends
|
|
9
8
|
|
|
10
|
-
- `WebStorage
|
|
11
|
-
- `IndexedDB
|
|
12
|
-
- `WebAccess
|
|
9
|
+
- `WebStorage` stores files in a `Storage` object, like `localStorage` and `sessionStorage`.
|
|
10
|
+
- `IndexedDB` stores files into an `IndexedDB` object database.
|
|
11
|
+
- `WebAccess` uses the [File System Access API](https://developer.mozilla.org/Web/API/File_System_API).
|
|
12
|
+
- `XML` uses an `XMLDocument` to store files, which can be appended to the DOM.
|
|
13
13
|
|
|
14
14
|
For more information, see the [API documentation](https://zen-fs.github.io/dom).
|
|
15
15
|
|
|
16
16
|
## Usage
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
> The examples are written in ESM.
|
|
20
|
-
> For CJS, you can `require` the package.
|
|
21
|
-
> If using a browser environment, you can use a `<script>` with `type=module` (you may need to use import maps)
|
|
18
|
+
You can use the backends from `@zenfs/dom` just like the backends from `@zenfs/core`:
|
|
22
19
|
|
|
23
20
|
```js
|
|
24
21
|
import { configure, fs } from '@zenfs/core';
|
|
@@ -33,3 +30,42 @@ if (!fs.existsSync('/test.txt')) {
|
|
|
33
30
|
const contents = fs.readFileSync('/test.txt', 'utf-8');
|
|
34
31
|
console.log(contents);
|
|
35
32
|
```
|
|
33
|
+
|
|
34
|
+
#### `XML`
|
|
35
|
+
|
|
36
|
+
The `XML` backend can be used to create a file system which lives in the DOM:
|
|
37
|
+
|
|
38
|
+
```html
|
|
39
|
+
<!-- ... -->
|
|
40
|
+
<fs />
|
|
41
|
+
<!-- ... -->
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
import { configure, fs } from '@zenfs/core';
|
|
46
|
+
import { XML } from '@zenfs/dom';
|
|
47
|
+
|
|
48
|
+
await configureSingle({
|
|
49
|
+
backend: XML,
|
|
50
|
+
root: document.querySelector('fs'), // root is optional
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
fs.writeFileSync('/test.txt', 'This is in the DOM!');
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
If you choose to add the root element to the DOM by appending it, you will likely want to hide its contents (`display:none` works well).
|
|
57
|
+
|
|
58
|
+
The `root` option is not required. If you choose not to pass in a `root`, you can always append it to the DOM later:
|
|
59
|
+
|
|
60
|
+
```js
|
|
61
|
+
import { configure, fs, mounts } from '@zenfs/core';
|
|
62
|
+
import { XML } from '@zenfs/dom';
|
|
63
|
+
|
|
64
|
+
await configureSingle({ backend: XML });
|
|
65
|
+
|
|
66
|
+
const { root } = mounts.get('/');
|
|
67
|
+
|
|
68
|
+
document.body.append(root);
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
This may disrupt use cases that involve saving the HTML file locally and loading it later, since a new element is created when configuring. In contrast, when using an existing element and passing in a `root`, the existing element's contents will be preserved.
|
|
File without changes
|
|
File without changes
|