@ucdjs/fs-bridge 0.1.1-beta.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/LICENSE +21 -0
- package/README.md +119 -0
- package/dist/bridges/http.d.mts +12 -0
- package/dist/bridges/http.mjs +161 -0
- package/dist/bridges/node.d.mts +11 -0
- package/dist/bridges/node.mjs +115 -0
- package/dist/define-BfE46g0d.mjs +205 -0
- package/dist/errors.d.mts +27 -0
- package/dist/errors.mjs +48 -0
- package/dist/guards-CXuUenP_.d.mts +45 -0
- package/dist/index.d.mts +4 -0
- package/dist/index.mjs +67 -0
- package/dist/types-BRcSW-lJ.d.mts +266 -0
- package/package.json +74 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-PRESENT Lucas Nørgård
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# @ucdjs/fs-bridge
|
|
2
|
+
|
|
3
|
+
[![npm version][npm-version-src]][npm-version-href]
|
|
4
|
+
[![npm downloads][npm-downloads-src]][npm-downloads-href]
|
|
5
|
+
[![codecov][codecov-src]][codecov-href]
|
|
6
|
+
|
|
7
|
+
A collection of filesystem bridge implementations for the UCD project, providing both Node.js and HTTP-based file system access.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @ucdjs/fs-bridge
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
You can create your own filesystem bridge or use the preconfigured ones provided by this package.
|
|
18
|
+
|
|
19
|
+
### Creating a Custom Bridge
|
|
20
|
+
|
|
21
|
+
You can define a custom filesystem bridge using the `defineFileSystemBridge` function. This allows you to implement your own file system operations.
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { defineFileSystemBridge } from "@ucdjs/fs-bridge";
|
|
25
|
+
|
|
26
|
+
const MyFileSystemBridge = defineFileSystemBridge({
|
|
27
|
+
read: async (path) => {
|
|
28
|
+
// Implement your read logic here
|
|
29
|
+
},
|
|
30
|
+
write: async (path, content) => {
|
|
31
|
+
// Implement your write logic here
|
|
32
|
+
},
|
|
33
|
+
listdir: async (path, recursive = false) => {
|
|
34
|
+
// Implement your directory listing logic here
|
|
35
|
+
},
|
|
36
|
+
mkdir: async (path) => {
|
|
37
|
+
// Implement your directory creation logic here
|
|
38
|
+
},
|
|
39
|
+
exists: async (path) => {
|
|
40
|
+
// Implement your existence check logic here
|
|
41
|
+
},
|
|
42
|
+
stat: async (path) => {
|
|
43
|
+
// Implement your file stats retrieval logic here
|
|
44
|
+
},
|
|
45
|
+
rm: async (path, options) => {
|
|
46
|
+
// Implement your file/directory removal logic here
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Predefined Bridges
|
|
52
|
+
|
|
53
|
+
#### Node.js File System Bridge
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
import NodeFileSystemBridge from "@ucdjs/fs-bridge/bridges/node";
|
|
57
|
+
|
|
58
|
+
// Read a file
|
|
59
|
+
const content = await NodeFileSystemBridge.read("/path/to/file.txt");
|
|
60
|
+
|
|
61
|
+
// Write a file
|
|
62
|
+
await NodeFileSystemBridge.write("/path/to/file.txt", "Hello World");
|
|
63
|
+
|
|
64
|
+
// List directory contents
|
|
65
|
+
const files = await NodeFileSystemBridge.listdir("/path/to/dir");
|
|
66
|
+
const allFiles = await NodeFileSystemBridge.listdir("/path/to/dir", true); // recursive
|
|
67
|
+
|
|
68
|
+
// Create directory
|
|
69
|
+
await NodeFileSystemBridge.mkdir("/path/to/new/dir");
|
|
70
|
+
|
|
71
|
+
// Check if file exists
|
|
72
|
+
const exists = await NodeFileSystemBridge.exists("/path/to/file.txt");
|
|
73
|
+
|
|
74
|
+
// Get file stats
|
|
75
|
+
const stats = await NodeFileSystemBridge.stat("/path/to/file.txt");
|
|
76
|
+
console.log(stats.isFile()); // true/false
|
|
77
|
+
console.log(stats.isDirectory()); // true/false
|
|
78
|
+
console.log(stats.size); // file size in bytes
|
|
79
|
+
console.log(stats.mtime); // last modified date
|
|
80
|
+
|
|
81
|
+
// Remove file/directory
|
|
82
|
+
await NodeFileSystemBridge.rm("/path/to/file.txt");
|
|
83
|
+
await NodeFileSystemBridge.rm("/path/to/dir", { recursive: true, force: true });
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
#### HTTP File System Bridge
|
|
87
|
+
|
|
88
|
+
Read-only filesystem bridge for accessing files over HTTP/HTTPS:
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
import HTTPFileSystemBridge from "@ucdjs/fs-bridge/bridges/http";
|
|
92
|
+
|
|
93
|
+
const httpFS = HTTPFileSystemBridge({
|
|
94
|
+
baseUrl: "https://example.com/files/"
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Read remote file
|
|
98
|
+
const content = await httpFS.read("/data/file.txt");
|
|
99
|
+
|
|
100
|
+
// List remote directory
|
|
101
|
+
const files = await httpFS.listdir("/data/");
|
|
102
|
+
|
|
103
|
+
// Check if remote file exists
|
|
104
|
+
const exists = await httpFS.exists("/data/file.txt");
|
|
105
|
+
|
|
106
|
+
// Get remote file stats
|
|
107
|
+
const stats = await httpFS.stat("/data/file.txt");
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## 📄 License
|
|
111
|
+
|
|
112
|
+
Published under [MIT License](./LICENSE).
|
|
113
|
+
|
|
114
|
+
[npm-version-src]: https://img.shields.io/npm/v/@ucdjs/fs-bridge?style=flat&colorA=18181B&colorB=4169E1
|
|
115
|
+
[npm-version-href]: https://npmjs.com/package/@ucdjs/fs-bridge
|
|
116
|
+
[npm-downloads-src]: https://img.shields.io/npm/dm/@ucdjs/fs-bridge?style=flat&colorA=18181B&colorB=4169E1
|
|
117
|
+
[npm-downloads-href]: https://npmjs.com/package/@ucdjs/fs-bridge
|
|
118
|
+
[codecov-src]: https://img.shields.io/codecov/c/gh/ucdjs/ucd?style=flat&colorA=18181B&colorB=4169E1
|
|
119
|
+
[codecov-href]: https://codecov.io/gh/ucdjs/ucd
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { i as FileSystemBridgeFactory } from "../types-BRcSW-lJ.mjs";
|
|
2
|
+
import "../guards-CXuUenP_.mjs";
|
|
3
|
+
import "../index.mjs";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
//#region src/bridges/http.d.ts
|
|
7
|
+
declare const kHttpBridgeSymbol: unique symbol;
|
|
8
|
+
declare const HTTPFileSystemBridge: FileSystemBridgeFactory<z.ZodObject<{
|
|
9
|
+
baseUrl: z.ZodDefault<z.ZodCodec<z.ZodURL, z.ZodCustom<URL, URL>>>;
|
|
10
|
+
}, z.core.$strip>>;
|
|
11
|
+
//#endregion
|
|
12
|
+
export { HTTPFileSystemBridge as default, kHttpBridgeSymbol };
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { t as defineFileSystemBridge } from "../define-BfE46g0d.mjs";
|
|
2
|
+
import { createDebugger } from "@ucdjs-internal/shared";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { joinURL } from "@luxass/utils/path";
|
|
5
|
+
import { UCDJS_STORE_BASE_URL } from "@ucdjs/env";
|
|
6
|
+
import { FileEntrySchema } from "@ucdjs/schemas";
|
|
7
|
+
|
|
8
|
+
//#region src/bridges/http.ts
|
|
9
|
+
const debug = createDebugger("ucdjs:fs-bridge:http");
|
|
10
|
+
const kHttpBridgeSymbol = Symbol.for("@ucdjs/fs-bridge:http");
|
|
11
|
+
const API_BASE_URL_SCHEMA = z.codec(z.url({
|
|
12
|
+
protocol: /^https?$/,
|
|
13
|
+
hostname: z.regexes.hostname,
|
|
14
|
+
normalize: true
|
|
15
|
+
}), z.instanceof(URL), {
|
|
16
|
+
decode: (urlString) => new URL(urlString),
|
|
17
|
+
encode: (url) => url.href
|
|
18
|
+
}).default(new URL("/", UCDJS_STORE_BASE_URL));
|
|
19
|
+
const HTTPFileSystemBridge = defineFileSystemBridge({
|
|
20
|
+
meta: {
|
|
21
|
+
name: "HTTP File System Bridge",
|
|
22
|
+
description: "A file system bridge that interacts with a remote HTTP API to perform file system operations."
|
|
23
|
+
},
|
|
24
|
+
optionsSchema: z.object({ baseUrl: API_BASE_URL_SCHEMA }),
|
|
25
|
+
symbol: kHttpBridgeSymbol,
|
|
26
|
+
setup({ options, resolveSafePath }) {
|
|
27
|
+
const baseUrl = options.baseUrl;
|
|
28
|
+
return {
|
|
29
|
+
async read(path) {
|
|
30
|
+
debug?.("Reading file", { path });
|
|
31
|
+
const trimmedPath = path.trim();
|
|
32
|
+
if (trimmedPath.endsWith("/") && trimmedPath !== "/" && trimmedPath !== "./" && trimmedPath !== "../") {
|
|
33
|
+
debug?.("Rejected file path ending with '/'", { path });
|
|
34
|
+
throw new Error("Cannot read file: path ends with '/'");
|
|
35
|
+
}
|
|
36
|
+
const url = joinURL(baseUrl.origin, resolveSafePath(baseUrl.pathname, path));
|
|
37
|
+
debug?.("Fetching remote file", { url });
|
|
38
|
+
const response = await fetch(url);
|
|
39
|
+
if (!response.ok) {
|
|
40
|
+
debug?.("Failed to read remote file", {
|
|
41
|
+
url,
|
|
42
|
+
status: response.status,
|
|
43
|
+
statusText: response.statusText
|
|
44
|
+
});
|
|
45
|
+
throw new Error(`Failed to read remote file: ${response.statusText}`);
|
|
46
|
+
}
|
|
47
|
+
debug?.("Successfully read remote file", { url });
|
|
48
|
+
return response.text();
|
|
49
|
+
},
|
|
50
|
+
async listdir(path, recursive = false) {
|
|
51
|
+
debug?.("Listing directory", {
|
|
52
|
+
path,
|
|
53
|
+
recursive
|
|
54
|
+
});
|
|
55
|
+
const url = joinURL(baseUrl.origin, resolveSafePath(baseUrl.pathname, `/${path}`));
|
|
56
|
+
debug?.("Fetching directory listing", { url });
|
|
57
|
+
const response = await fetch(url, {
|
|
58
|
+
method: "GET",
|
|
59
|
+
headers: { Accept: "application/json" }
|
|
60
|
+
});
|
|
61
|
+
if (!response.ok) {
|
|
62
|
+
if (response.status === 404) {
|
|
63
|
+
debug?.("Directory not found, returning empty array", { url });
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
if (response.status === 403) {
|
|
67
|
+
debug?.("Directory access forbidden, returning empty array", { url });
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
if (response.status === 500) {
|
|
71
|
+
debug?.("Server error while listing directory", {
|
|
72
|
+
url,
|
|
73
|
+
status: response.status
|
|
74
|
+
});
|
|
75
|
+
throw new Error(`Server error while listing directory: ${response.statusText}`);
|
|
76
|
+
}
|
|
77
|
+
debug?.("Failed to list directory", {
|
|
78
|
+
url,
|
|
79
|
+
status: response.status,
|
|
80
|
+
statusText: response.statusText
|
|
81
|
+
});
|
|
82
|
+
throw new Error(`Failed to list directory: ${response.statusText} (${response.status})`);
|
|
83
|
+
}
|
|
84
|
+
const json = await response.json();
|
|
85
|
+
const result = z.array(FileEntrySchema).safeParse(json);
|
|
86
|
+
if (!result.success) {
|
|
87
|
+
debug?.("Invalid response schema from directory listing", {
|
|
88
|
+
url,
|
|
89
|
+
error: result.error.message
|
|
90
|
+
});
|
|
91
|
+
throw new Error(`Invalid response schema: ${result.error.message}`);
|
|
92
|
+
}
|
|
93
|
+
const data = result.data;
|
|
94
|
+
if (!recursive) {
|
|
95
|
+
debug?.("Returning non-recursive directory listing", {
|
|
96
|
+
path,
|
|
97
|
+
entryCount: data.length
|
|
98
|
+
});
|
|
99
|
+
return data.map((entry) => {
|
|
100
|
+
if (entry.type === "directory") return {
|
|
101
|
+
type: "directory",
|
|
102
|
+
name: entry.name,
|
|
103
|
+
path: entry.path,
|
|
104
|
+
children: []
|
|
105
|
+
};
|
|
106
|
+
return {
|
|
107
|
+
type: entry.type,
|
|
108
|
+
name: entry.name,
|
|
109
|
+
path: entry.path
|
|
110
|
+
};
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
debug?.("Processing recursive directory listing", {
|
|
114
|
+
path,
|
|
115
|
+
entryCount: data.length
|
|
116
|
+
});
|
|
117
|
+
const entries = [];
|
|
118
|
+
for (const entry of data) if (entry.type === "directory") {
|
|
119
|
+
const children = await this.listdir(entry.path, true);
|
|
120
|
+
entries.push({
|
|
121
|
+
type: "directory",
|
|
122
|
+
name: entry.name,
|
|
123
|
+
path: entry.path,
|
|
124
|
+
children
|
|
125
|
+
});
|
|
126
|
+
} else entries.push({
|
|
127
|
+
type: "file",
|
|
128
|
+
name: entry.name,
|
|
129
|
+
path: entry.path
|
|
130
|
+
});
|
|
131
|
+
debug?.("Completed recursive directory listing", {
|
|
132
|
+
path,
|
|
133
|
+
totalEntries: entries.length
|
|
134
|
+
});
|
|
135
|
+
return entries;
|
|
136
|
+
},
|
|
137
|
+
async exists(path) {
|
|
138
|
+
debug?.("Checking file existence", { path });
|
|
139
|
+
const url = joinURL(baseUrl.origin, resolveSafePath(baseUrl.pathname, path));
|
|
140
|
+
debug?.("Sending HEAD request", { url });
|
|
141
|
+
return fetch(url, { method: "HEAD" }).then((response) => {
|
|
142
|
+
debug?.("File existence check result", {
|
|
143
|
+
url,
|
|
144
|
+
exists: response.ok
|
|
145
|
+
});
|
|
146
|
+
return response.ok;
|
|
147
|
+
}).catch((err) => {
|
|
148
|
+
if (err instanceof Error && err.message.startsWith("[MSW]")) throw err;
|
|
149
|
+
debug?.("Error checking file existence", {
|
|
150
|
+
url,
|
|
151
|
+
error: err instanceof Error ? err.message : String(err)
|
|
152
|
+
});
|
|
153
|
+
return false;
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
//#endregion
|
|
161
|
+
export { HTTPFileSystemBridge as default, kHttpBridgeSymbol };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { i as FileSystemBridgeFactory } from "../types-BRcSW-lJ.mjs";
|
|
2
|
+
import "../guards-CXuUenP_.mjs";
|
|
3
|
+
import "../index.mjs";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
//#region src/bridges/node.d.ts
|
|
7
|
+
declare const NodeFileSystemBridge: FileSystemBridgeFactory<z.ZodObject<{
|
|
8
|
+
basePath: z.ZodString;
|
|
9
|
+
}, z.core.$strip>>;
|
|
10
|
+
//#endregion
|
|
11
|
+
export { NodeFileSystemBridge as default };
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { t as defineFileSystemBridge } from "../define-BfE46g0d.mjs";
|
|
2
|
+
import { createDebugger } from "@ucdjs-internal/shared";
|
|
3
|
+
import { assertNotUNCPath } from "@ucdjs/path-utils";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { appendTrailingSlash, prependLeadingSlash } from "@luxass/utils/path";
|
|
6
|
+
import fsp from "node:fs/promises";
|
|
7
|
+
import nodePath from "node:path";
|
|
8
|
+
|
|
9
|
+
//#region src/bridges/node.ts
|
|
10
|
+
const debug = createDebugger("ucdjs:fs-bridge:node");
|
|
11
|
+
/**
|
|
12
|
+
* Normalizes path separators to forward slashes for cross-platform consistency.
|
|
13
|
+
* On Windows, converts backslashes to forward slashes.
|
|
14
|
+
*/
|
|
15
|
+
function normalizePathSeparators(path) {
|
|
16
|
+
return path.replace(/\\/g, "/");
|
|
17
|
+
}
|
|
18
|
+
async function safeExists(path) {
|
|
19
|
+
try {
|
|
20
|
+
await fsp.stat(path);
|
|
21
|
+
return true;
|
|
22
|
+
} catch {
|
|
23
|
+
debug?.("File existence check failed", { path });
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const NodeFileSystemBridge = defineFileSystemBridge({
|
|
28
|
+
meta: {
|
|
29
|
+
name: "Node.js File System Bridge",
|
|
30
|
+
description: "A file system bridge that uses Node.js fs module to interact with the local file system."
|
|
31
|
+
},
|
|
32
|
+
optionsSchema: z.object({ basePath: z.string() }),
|
|
33
|
+
setup({ options, resolveSafePath }) {
|
|
34
|
+
assertNotUNCPath(options.basePath);
|
|
35
|
+
const basePath = nodePath.resolve(options.basePath);
|
|
36
|
+
return {
|
|
37
|
+
async read(path) {
|
|
38
|
+
const trimmedPath = path.trim();
|
|
39
|
+
if (trimmedPath.endsWith("/") && trimmedPath !== "/" && trimmedPath !== "./" && trimmedPath !== "../") throw new Error("Cannot read file: path ends with '/'");
|
|
40
|
+
const resolvedPath = resolveSafePath(basePath, path);
|
|
41
|
+
return fsp.readFile(resolvedPath, "utf-8");
|
|
42
|
+
},
|
|
43
|
+
async exists(path) {
|
|
44
|
+
return safeExists(resolveSafePath(basePath, path));
|
|
45
|
+
},
|
|
46
|
+
async listdir(path, recursive = false) {
|
|
47
|
+
const targetPath = resolveSafePath(basePath, path);
|
|
48
|
+
function formatEntryPath(relativeToRoot, isDirectory) {
|
|
49
|
+
const withLeadingSlash = prependLeadingSlash(relativeToRoot);
|
|
50
|
+
return isDirectory ? appendTrailingSlash(withLeadingSlash) : withLeadingSlash;
|
|
51
|
+
}
|
|
52
|
+
function createFSEntry(entry, relativeToRoot) {
|
|
53
|
+
const formattedPath = formatEntryPath(relativeToRoot, entry.isDirectory());
|
|
54
|
+
return entry.isDirectory() ? {
|
|
55
|
+
type: "directory",
|
|
56
|
+
name: entry.name,
|
|
57
|
+
path: formattedPath,
|
|
58
|
+
children: []
|
|
59
|
+
} : {
|
|
60
|
+
type: "file",
|
|
61
|
+
name: entry.name,
|
|
62
|
+
path: formattedPath
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (!recursive) return (await fsp.readdir(targetPath, { withFileTypes: true })).map((entry) => {
|
|
66
|
+
const absEntryPath = nodePath.join(targetPath, entry.name);
|
|
67
|
+
return createFSEntry(entry, normalizePathSeparators(nodePath.relative(basePath, absEntryPath)));
|
|
68
|
+
});
|
|
69
|
+
const allEntries = await fsp.readdir(targetPath, {
|
|
70
|
+
withFileTypes: true,
|
|
71
|
+
recursive: true
|
|
72
|
+
});
|
|
73
|
+
const entryMap = /* @__PURE__ */ new Map();
|
|
74
|
+
const rootEntries = [];
|
|
75
|
+
for (const entry of allEntries) {
|
|
76
|
+
const entryDirPath = entry.parentPath || entry.path;
|
|
77
|
+
const relativeToTargetDir = nodePath.relative(targetPath, entryDirPath);
|
|
78
|
+
const absEntryPath = nodePath.join(entryDirPath, entry.name);
|
|
79
|
+
const normalized = normalizePathSeparators(nodePath.relative(basePath, absEntryPath));
|
|
80
|
+
const fsEntry = createFSEntry(entry, normalized);
|
|
81
|
+
entryMap.set(normalized, fsEntry);
|
|
82
|
+
if (!relativeToTargetDir) rootEntries.push(fsEntry);
|
|
83
|
+
}
|
|
84
|
+
for (const [entryPath, entry] of entryMap) {
|
|
85
|
+
const parentPath = nodePath.dirname(entryPath);
|
|
86
|
+
if (parentPath && parentPath !== ".") {
|
|
87
|
+
const parent = entryMap.get(parentPath);
|
|
88
|
+
if (parent?.type === "directory") parent.children.push(entry);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return rootEntries;
|
|
92
|
+
},
|
|
93
|
+
async write(path, data, encoding = "utf-8") {
|
|
94
|
+
const trimmedPath = path.trim();
|
|
95
|
+
if (trimmedPath.endsWith("/") && trimmedPath !== "/" && trimmedPath !== "./" && trimmedPath !== "../") throw new Error("Cannot write file: path ends with '/'");
|
|
96
|
+
const resolvedPath = resolveSafePath(basePath, path);
|
|
97
|
+
const parentDir = nodePath.dirname(resolvedPath);
|
|
98
|
+
if (!await safeExists(parentDir)) await fsp.mkdir(parentDir, { recursive: true });
|
|
99
|
+
return fsp.writeFile(resolvedPath, data, { encoding });
|
|
100
|
+
},
|
|
101
|
+
async mkdir(path) {
|
|
102
|
+
await fsp.mkdir(resolveSafePath(basePath, path), { recursive: true });
|
|
103
|
+
},
|
|
104
|
+
async rm(path, options) {
|
|
105
|
+
return fsp.rm(resolveSafePath(basePath, path), {
|
|
106
|
+
recursive: options?.recursive ?? false,
|
|
107
|
+
force: options?.force ?? false
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
//#endregion
|
|
115
|
+
export { NodeFileSystemBridge as default };
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { BridgeBaseError, BridgeGenericError, BridgeSetupError, BridgeUnsupportedOperation } from "./errors.mjs";
|
|
2
|
+
import { createDebugger } from "@ucdjs-internal/shared";
|
|
3
|
+
import { PathUtilsBaseError, resolveSafePath } from "@ucdjs/path-utils";
|
|
4
|
+
import { HookableCore } from "hookable";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
|
|
7
|
+
//#region ../../node_modules/.pnpm/@luxass+msw-utils@0.6.0_msw@2.12.10_@types+node@25.2.2_typescript@5.9.3_/node_modules/@luxass/msw-utils/dist/runtime-guards.mjs
|
|
8
|
+
/**
|
|
9
|
+
* Checks if an error is an MSW internal error.
|
|
10
|
+
*
|
|
11
|
+
* MSW throws internal errors with the name "InternalError" and
|
|
12
|
+
* prefixes error messages with "[MSW]".
|
|
13
|
+
*
|
|
14
|
+
* @param {unknown} error - The error to check
|
|
15
|
+
* @returns {boolean} true if the error is from MSW
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* try {
|
|
20
|
+
* // some code that might throw MSW errors
|
|
21
|
+
* } catch (error) {
|
|
22
|
+
* if (isMSWError(error)) {
|
|
23
|
+
* console.log("This is an MSW error:", error.message);
|
|
24
|
+
* }
|
|
25
|
+
* }
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
function isMSWError(error) {
|
|
29
|
+
return error instanceof Error && (error.name === "InternalError" || error.message.startsWith("[MSW]"));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region src/utils.ts
|
|
34
|
+
const debug$1 = createDebugger("ucdjs:fs-bridge:utils");
|
|
35
|
+
/**
|
|
36
|
+
* @internal
|
|
37
|
+
*/
|
|
38
|
+
function inferOptionalCapabilitiesFromOperations(ops) {
|
|
39
|
+
return {
|
|
40
|
+
write: "write" in ops && typeof ops.write === "function",
|
|
41
|
+
mkdir: "mkdir" in ops && typeof ops.mkdir === "function",
|
|
42
|
+
rm: "rm" in ops && typeof ops.rm === "function"
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Constructs a hook payload object for a given file system bridge operation.
|
|
47
|
+
*
|
|
48
|
+
* This function normalizes arguments and results from different file system operations
|
|
49
|
+
* into their corresponding hook payload structures. It supports both "before" and "after"
|
|
50
|
+
* phases for operations that distinguish between them.
|
|
51
|
+
*
|
|
52
|
+
* @param {string} property - The name of the file system operation (e.g., "read", "write", "listdir", "exists", "mkdir", "rm")
|
|
53
|
+
* @param {string} phase - The hook execution phase: "before" runs before the operation, "after" runs after
|
|
54
|
+
* @param {unknown[]} args - The arguments passed to the original operation
|
|
55
|
+
* @param {unknown} result - The result from the operation (only used for "after" phase hooks)
|
|
56
|
+
* @returns A {@link HookPayload} object containing normalized data for the hook
|
|
57
|
+
*/
|
|
58
|
+
function getPayloadForHook(property, phase, args, result) {
|
|
59
|
+
debug$1?.(`Constructing hook payload for '${property}:${phase}'`);
|
|
60
|
+
switch (property) {
|
|
61
|
+
case "read": if (phase === "before") return { path: args[0] };
|
|
62
|
+
else return {
|
|
63
|
+
path: args[0],
|
|
64
|
+
content: result
|
|
65
|
+
};
|
|
66
|
+
case "write": if (phase === "before") return {
|
|
67
|
+
path: args[0],
|
|
68
|
+
content: args[1],
|
|
69
|
+
encoding: args[2]
|
|
70
|
+
};
|
|
71
|
+
else return { path: args[0] };
|
|
72
|
+
case "listdir": if (phase === "before") return {
|
|
73
|
+
path: args[0],
|
|
74
|
+
recursive: args[1] ?? false
|
|
75
|
+
};
|
|
76
|
+
else return {
|
|
77
|
+
path: args[0],
|
|
78
|
+
recursive: args[1] ?? false,
|
|
79
|
+
entries: result
|
|
80
|
+
};
|
|
81
|
+
case "exists": if (phase === "before") return { path: args[0] };
|
|
82
|
+
else return {
|
|
83
|
+
path: args[0],
|
|
84
|
+
exists: result
|
|
85
|
+
};
|
|
86
|
+
case "mkdir": return { path: args[0] };
|
|
87
|
+
case "rm": return {
|
|
88
|
+
path: args[0],
|
|
89
|
+
...args[1]
|
|
90
|
+
};
|
|
91
|
+
default: throw new BridgeGenericError(`Failed to construct hook payload for '${property}:${phase}' hook`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* @internal
|
|
96
|
+
*/
|
|
97
|
+
function createOperationWrapper(operationName, { hooks, operations }) {
|
|
98
|
+
const operation = operations[operationName];
|
|
99
|
+
if (operation == null || typeof operation !== "function") return async (...args) => {
|
|
100
|
+
debug$1?.("Attempted to call unsupported operation", { operation: operationName });
|
|
101
|
+
const error = new BridgeUnsupportedOperation(operationName);
|
|
102
|
+
await hooks.callHook("error", {
|
|
103
|
+
method: operationName,
|
|
104
|
+
path: args[0],
|
|
105
|
+
error,
|
|
106
|
+
args
|
|
107
|
+
});
|
|
108
|
+
throw error;
|
|
109
|
+
};
|
|
110
|
+
return async (...args) => {
|
|
111
|
+
try {
|
|
112
|
+
const beforePayload = getPayloadForHook(operationName, "before", args);
|
|
113
|
+
await hooks.callHook(`${operationName}:before`, beforePayload);
|
|
114
|
+
const result = await operation.apply(operations, args);
|
|
115
|
+
const afterPayload = getPayloadForHook(operationName, "after", args, result);
|
|
116
|
+
await hooks.callHook(`${operationName}:after`, afterPayload);
|
|
117
|
+
return result;
|
|
118
|
+
} catch (err) {
|
|
119
|
+
return handleError(operationName, args, err, hooks);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
async function handleError(operation, args, err, hooks) {
|
|
124
|
+
const normalizedError = (() => {
|
|
125
|
+
if (err instanceof Error) return err;
|
|
126
|
+
debug$1?.("Non-Error thrown in bridge operation", {
|
|
127
|
+
operation: String(operation),
|
|
128
|
+
error: String(err)
|
|
129
|
+
});
|
|
130
|
+
return new BridgeGenericError(`Non-Error thrown in '${String(operation)}' operation: ${String(err)}`, { cause: err });
|
|
131
|
+
})();
|
|
132
|
+
if (isMSWError(normalizedError)) {
|
|
133
|
+
debug$1?.("MSW error detected in bridge operation", {
|
|
134
|
+
operation: String(operation),
|
|
135
|
+
error: normalizedError.message
|
|
136
|
+
});
|
|
137
|
+
throw normalizedError;
|
|
138
|
+
}
|
|
139
|
+
await hooks.callHook("error", {
|
|
140
|
+
method: operation,
|
|
141
|
+
path: args[0],
|
|
142
|
+
error: normalizedError,
|
|
143
|
+
args
|
|
144
|
+
});
|
|
145
|
+
if (normalizedError instanceof BridgeBaseError || normalizedError instanceof PathUtilsBaseError) {
|
|
146
|
+
debug$1?.("Known error thrown in bridge operation", {
|
|
147
|
+
operation: String(operation),
|
|
148
|
+
error: normalizedError.message
|
|
149
|
+
});
|
|
150
|
+
throw normalizedError;
|
|
151
|
+
}
|
|
152
|
+
debug$1?.("Unexpected error in bridge operation", {
|
|
153
|
+
operation: String(operation),
|
|
154
|
+
error: normalizedError.message
|
|
155
|
+
});
|
|
156
|
+
throw new BridgeGenericError(`Unexpected error in '${String(operation)}' operation: ${normalizedError.message}`, { cause: normalizedError });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
//#endregion
|
|
160
|
+
//#region src/define.ts
|
|
161
|
+
const debug = createDebugger("ucdjs:fs-bridge:define");
|
|
162
|
+
function defineFileSystemBridge(fsBridge) {
|
|
163
|
+
return (...args) => {
|
|
164
|
+
const parsedOptions = (fsBridge.optionsSchema ?? z.never().optional()).safeParse(args[0]);
|
|
165
|
+
if (!parsedOptions.success) {
|
|
166
|
+
debug?.("Invalid options provided to file system bridge", { error: parsedOptions.error.message });
|
|
167
|
+
throw new Error(`Invalid options provided to file system bridge: ${parsedOptions.error.message}`);
|
|
168
|
+
}
|
|
169
|
+
const options = parsedOptions.data;
|
|
170
|
+
const { state } = fsBridge;
|
|
171
|
+
let bridgeOperations = null;
|
|
172
|
+
try {
|
|
173
|
+
bridgeOperations = fsBridge.setup({
|
|
174
|
+
options,
|
|
175
|
+
state: structuredClone(state) ?? {},
|
|
176
|
+
resolveSafePath
|
|
177
|
+
});
|
|
178
|
+
} catch (err) {
|
|
179
|
+
debug?.("Failed to setup file system bridge", { error: err instanceof Error ? err.message : String(err) });
|
|
180
|
+
throw new BridgeSetupError("Failed to setup file system bridge", err instanceof Error ? err : void 0);
|
|
181
|
+
}
|
|
182
|
+
const hooks = new HookableCore();
|
|
183
|
+
const optionalCapabilities = inferOptionalCapabilitiesFromOperations(bridgeOperations);
|
|
184
|
+
const baseWrapperOptions = {
|
|
185
|
+
hooks,
|
|
186
|
+
operations: bridgeOperations
|
|
187
|
+
};
|
|
188
|
+
const bridge = {
|
|
189
|
+
meta: fsBridge.meta,
|
|
190
|
+
optionalCapabilities,
|
|
191
|
+
hook: hooks.hook.bind(hooks),
|
|
192
|
+
read: createOperationWrapper("read", baseWrapperOptions),
|
|
193
|
+
exists: createOperationWrapper("exists", baseWrapperOptions),
|
|
194
|
+
listdir: createOperationWrapper("listdir", baseWrapperOptions),
|
|
195
|
+
write: createOperationWrapper("write", baseWrapperOptions),
|
|
196
|
+
mkdir: createOperationWrapper("mkdir", baseWrapperOptions),
|
|
197
|
+
rm: createOperationWrapper("rm", baseWrapperOptions)
|
|
198
|
+
};
|
|
199
|
+
if (fsBridge.symbol) bridge[fsBridge.symbol] = true;
|
|
200
|
+
return bridge;
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
//#endregion
|
|
205
|
+
export { defineFileSystemBridge as t };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { u as OptionalCapabilityKey } from "./types-BRcSW-lJ.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/errors.d.ts
|
|
4
|
+
declare abstract class BridgeBaseError extends Error {
|
|
5
|
+
constructor(message: string, options?: ErrorOptions);
|
|
6
|
+
}
|
|
7
|
+
declare class BridgeGenericError extends BridgeBaseError {
|
|
8
|
+
constructor(message: string, options?: ErrorOptions);
|
|
9
|
+
}
|
|
10
|
+
declare class BridgeSetupError extends BridgeBaseError {
|
|
11
|
+
readonly originalError?: Error;
|
|
12
|
+
constructor(message: string, originalError?: Error);
|
|
13
|
+
}
|
|
14
|
+
declare class BridgeUnsupportedOperation extends BridgeBaseError {
|
|
15
|
+
readonly capability: OptionalCapabilityKey;
|
|
16
|
+
constructor(capability: OptionalCapabilityKey);
|
|
17
|
+
}
|
|
18
|
+
declare class BridgeFileNotFound extends BridgeBaseError {
|
|
19
|
+
readonly path: string;
|
|
20
|
+
constructor(path: string);
|
|
21
|
+
}
|
|
22
|
+
declare class BridgeEntryIsDir extends BridgeBaseError {
|
|
23
|
+
readonly path: string;
|
|
24
|
+
constructor(path: string);
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
export { BridgeBaseError, BridgeEntryIsDir, BridgeFileNotFound, BridgeGenericError, BridgeSetupError, BridgeUnsupportedOperation };
|
package/dist/errors.mjs
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
//#region src/errors.ts
|
|
2
|
+
var BridgeBaseError = class extends Error {
|
|
3
|
+
constructor(message, options) {
|
|
4
|
+
super(message, options);
|
|
5
|
+
this.name = "BridgeBaseError";
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
var BridgeGenericError = class extends BridgeBaseError {
|
|
9
|
+
constructor(message, options) {
|
|
10
|
+
super(message, options);
|
|
11
|
+
this.name = "BridgeGenericError";
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
var BridgeSetupError = class extends BridgeBaseError {
|
|
15
|
+
originalError;
|
|
16
|
+
constructor(message, originalError) {
|
|
17
|
+
super(message, { cause: originalError });
|
|
18
|
+
this.name = "BridgeSetupError";
|
|
19
|
+
this.originalError = originalError;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
var BridgeUnsupportedOperation = class extends BridgeBaseError {
|
|
23
|
+
capability;
|
|
24
|
+
constructor(capability) {
|
|
25
|
+
super(`File system bridge does not support the '${capability}' capability.`);
|
|
26
|
+
this.name = "BridgeUnsupportedOperation";
|
|
27
|
+
this.capability = capability;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
var BridgeFileNotFound = class extends BridgeBaseError {
|
|
31
|
+
path;
|
|
32
|
+
constructor(path) {
|
|
33
|
+
super(`File or directory not found: ${path}`);
|
|
34
|
+
this.name = "BridgeFileNotFound";
|
|
35
|
+
this.path = path;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
var BridgeEntryIsDir = class extends BridgeBaseError {
|
|
39
|
+
path;
|
|
40
|
+
constructor(path) {
|
|
41
|
+
super(`Expected file but found directory: ${path}`);
|
|
42
|
+
this.name = "BridgeEntryIsDir";
|
|
43
|
+
this.path = path;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
//#endregion
|
|
48
|
+
export { BridgeBaseError, BridgeEntryIsDir, BridgeFileNotFound, BridgeGenericError, BridgeSetupError, BridgeUnsupportedOperation };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { i as FileSystemBridgeFactory, n as FileSystemBridge, s as FileSystemBridgeObject, u as OptionalCapabilityKey } from "./types-BRcSW-lJ.mjs";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
//#region src/assertions.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Asserts that a file system bridge supports the specified capability or capabilities.
|
|
7
|
+
*
|
|
8
|
+
* This function performs a runtime check to ensure the bridge has the required capabilities
|
|
9
|
+
* and acts as a type guard to narrow the bridge type to include the specified capabilities.
|
|
10
|
+
*
|
|
11
|
+
* @template {OptionalCapabilityKey} T - The capability key(s) to check for, extending OptionalCapabilityKey
|
|
12
|
+
* @param {FileSystemBridge} bridge - The file system bridge to check capabilities for
|
|
13
|
+
* @param {T | T[]} capabilityOrCapabilities - A single capability or array of capabilities to verify
|
|
14
|
+
* @throws {BridgeUnsupportedOperation} When the bridge doesn't support one or more of the specified capabilities
|
|
15
|
+
*/
|
|
16
|
+
declare function assertCapability<T extends OptionalCapabilityKey = never>(bridge: FileSystemBridge, capabilityOrCapabilities: T | T[]): asserts bridge is FileSystemBridge & Required<Pick<FileSystemBridge, T>>;
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region src/define.d.ts
|
|
19
|
+
declare function defineFileSystemBridge<TOptionsSchema extends z.ZodType = z.ZodNever, TState extends Record<string, unknown> = Record<string, unknown>>(fsBridge: FileSystemBridgeObject<TOptionsSchema, TState> & {
|
|
20
|
+
symbol?: symbol;
|
|
21
|
+
}): FileSystemBridgeFactory<TOptionsSchema>;
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region src/guards.d.ts
|
|
24
|
+
/**
|
|
25
|
+
* Checks whether a file system bridge supports the specified capability or capabilities.
|
|
26
|
+
*
|
|
27
|
+
* Performs a runtime check and acts as a type guard to narrow the bridge type
|
|
28
|
+
* when all required capabilities are present.
|
|
29
|
+
*
|
|
30
|
+
* @template {OptionalCapabilityKey} T - The capability key(s) to check for, extending OptionalCapabilityKey
|
|
31
|
+
* @param {FileSystemBridge} bridge - The file system bridge to check capabilities for
|
|
32
|
+
* @param {T | T[]} capabilityOrCapabilities - A single capability or array of capabilities to verify
|
|
33
|
+
*/
|
|
34
|
+
declare function hasCapability<T extends OptionalCapabilityKey = never>(bridge: FileSystemBridge, capabilityOrCapabilities: T | T[]): bridge is FileSystemBridge & Required<Pick<FileSystemBridge, T>>;
|
|
35
|
+
/**
|
|
36
|
+
* Checks whether a file system bridge is the built-in HTTP File System Bridge.
|
|
37
|
+
*
|
|
38
|
+
* Uses a symbol to identify the bridge type, making it safe against name changes.
|
|
39
|
+
*
|
|
40
|
+
* @param {FileSystemBridge} fs - The file system bridge to check
|
|
41
|
+
* @returns {boolean} True if the bridge is the built-in HTTP File System Bridge, false otherwise
|
|
42
|
+
*/
|
|
43
|
+
declare function isBuiltinHttpBridge(fs: FileSystemBridge): boolean;
|
|
44
|
+
//#endregion
|
|
45
|
+
export { assertCapability as i, isBuiltinHttpBridge as n, defineFileSystemBridge as r, hasCapability as t };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { a as FileSystemBridgeHooks, c as FileSystemBridgeOperations, i as FileSystemBridgeFactory, l as FileSystemBridgeRmOptions, n as FileSystemBridge, o as FileSystemBridgeMetadata, r as FileSystemBridgeArgs, t as FSEntry } from "./types-BRcSW-lJ.mjs";
|
|
2
|
+
import { i as assertCapability, n as isBuiltinHttpBridge, r as defineFileSystemBridge, t as hasCapability } from "./guards-CXuUenP_.mjs";
|
|
3
|
+
import { BridgeBaseError, BridgeEntryIsDir, BridgeFileNotFound, BridgeGenericError, BridgeSetupError, BridgeUnsupportedOperation } from "./errors.mjs";
|
|
4
|
+
export { BridgeBaseError, BridgeEntryIsDir, BridgeFileNotFound, BridgeGenericError, BridgeSetupError, BridgeUnsupportedOperation, type FSEntry, type FileSystemBridge, type FileSystemBridgeArgs, type FileSystemBridgeFactory, type FileSystemBridgeHooks, type FileSystemBridgeMetadata, type FileSystemBridgeOperations, type FileSystemBridgeRmOptions, assertCapability, defineFileSystemBridge, hasCapability, isBuiltinHttpBridge };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { BridgeBaseError, BridgeEntryIsDir, BridgeFileNotFound, BridgeGenericError, BridgeSetupError, BridgeUnsupportedOperation } from "./errors.mjs";
|
|
2
|
+
import { t as defineFileSystemBridge } from "./define-BfE46g0d.mjs";
|
|
3
|
+
import { kHttpBridgeSymbol } from "./bridges/http.mjs";
|
|
4
|
+
import { createDebugger } from "@ucdjs-internal/shared";
|
|
5
|
+
|
|
6
|
+
//#region src/assertions.ts
|
|
7
|
+
const debug$1 = createDebugger("ucdjs:fs-bridge:assertions");
|
|
8
|
+
/**
|
|
9
|
+
* Asserts that a file system bridge supports the specified capability or capabilities.
|
|
10
|
+
*
|
|
11
|
+
* This function performs a runtime check to ensure the bridge has the required capabilities
|
|
12
|
+
* and acts as a type guard to narrow the bridge type to include the specified capabilities.
|
|
13
|
+
*
|
|
14
|
+
* @template {OptionalCapabilityKey} T - The capability key(s) to check for, extending OptionalCapabilityKey
|
|
15
|
+
* @param {FileSystemBridge} bridge - The file system bridge to check capabilities for
|
|
16
|
+
* @param {T | T[]} capabilityOrCapabilities - A single capability or array of capabilities to verify
|
|
17
|
+
* @throws {BridgeUnsupportedOperation} When the bridge doesn't support one or more of the specified capabilities
|
|
18
|
+
*/
|
|
19
|
+
function assertCapability(bridge, capabilityOrCapabilities) {
|
|
20
|
+
const capabilitiesToCheck = Array.isArray(capabilityOrCapabilities) ? capabilityOrCapabilities : [capabilityOrCapabilities];
|
|
21
|
+
for (const capability of capabilitiesToCheck) if (!bridge.optionalCapabilities[capability]) {
|
|
22
|
+
debug$1?.("Bridge capability check failed", {
|
|
23
|
+
capability,
|
|
24
|
+
availableCapabilities: Object.keys(bridge.optionalCapabilities).filter((k) => bridge.optionalCapabilities[k])
|
|
25
|
+
});
|
|
26
|
+
throw new BridgeUnsupportedOperation(capability);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region src/guards.ts
|
|
32
|
+
const debug = createDebugger("ucdjs:fs-bridge:guards");
|
|
33
|
+
/**
|
|
34
|
+
* Checks whether a file system bridge supports the specified capability or capabilities.
|
|
35
|
+
*
|
|
36
|
+
* Performs a runtime check and acts as a type guard to narrow the bridge type
|
|
37
|
+
* when all required capabilities are present.
|
|
38
|
+
*
|
|
39
|
+
* @template {OptionalCapabilityKey} T - The capability key(s) to check for, extending OptionalCapabilityKey
|
|
40
|
+
* @param {FileSystemBridge} bridge - The file system bridge to check capabilities for
|
|
41
|
+
* @param {T | T[]} capabilityOrCapabilities - A single capability or array of capabilities to verify
|
|
42
|
+
*/
|
|
43
|
+
function hasCapability(bridge, capabilityOrCapabilities) {
|
|
44
|
+
const capabilitiesToCheck = Array.isArray(capabilityOrCapabilities) ? capabilityOrCapabilities : [capabilityOrCapabilities];
|
|
45
|
+
for (const capability of capabilitiesToCheck) if (!bridge.optionalCapabilities[capability]) {
|
|
46
|
+
debug?.("Bridge capability check failed", {
|
|
47
|
+
capability,
|
|
48
|
+
availableCapabilities: Object.keys(bridge.optionalCapabilities).filter((k) => bridge.optionalCapabilities[k])
|
|
49
|
+
});
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Checks whether a file system bridge is the built-in HTTP File System Bridge.
|
|
56
|
+
*
|
|
57
|
+
* Uses a symbol to identify the bridge type, making it safe against name changes.
|
|
58
|
+
*
|
|
59
|
+
* @param {FileSystemBridge} fs - The file system bridge to check
|
|
60
|
+
* @returns {boolean} True if the bridge is the built-in HTTP File System Bridge, false otherwise
|
|
61
|
+
*/
|
|
62
|
+
function isBuiltinHttpBridge(fs) {
|
|
63
|
+
return kHttpBridgeSymbol in fs && fs[kHttpBridgeSymbol] === true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
//#endregion
|
|
67
|
+
export { BridgeBaseError, BridgeEntryIsDir, BridgeFileNotFound, BridgeGenericError, BridgeSetupError, BridgeUnsupportedOperation, assertCapability, defineFileSystemBridge, hasCapability, isBuiltinHttpBridge };
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { HookableCore } from "hookable";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
//#region src/types.d.ts
|
|
5
|
+
interface FileSystemBridgeRmOptions {
|
|
6
|
+
/**
|
|
7
|
+
* If true, removes directories and their contents recursively
|
|
8
|
+
*/
|
|
9
|
+
recursive?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* If true, ignores errors if the path doesn't exist
|
|
12
|
+
*/
|
|
13
|
+
force?: boolean;
|
|
14
|
+
}
|
|
15
|
+
type FSEntry = {
|
|
16
|
+
type: "file";
|
|
17
|
+
name: string;
|
|
18
|
+
path: string;
|
|
19
|
+
} | {
|
|
20
|
+
type: "directory";
|
|
21
|
+
name: string;
|
|
22
|
+
path: string;
|
|
23
|
+
children: FSEntry[];
|
|
24
|
+
};
|
|
25
|
+
interface RequiredFileSystemBridgeOperations {
|
|
26
|
+
/**
|
|
27
|
+
* Reads the contents of a file.
|
|
28
|
+
* @param {string} path - The path to the file to read
|
|
29
|
+
* @returns {Promise<string>} A promise that resolves to the file contents as a string
|
|
30
|
+
*/
|
|
31
|
+
read: (path: string) => Promise<string>;
|
|
32
|
+
/**
|
|
33
|
+
* Lists the contents of a directory.
|
|
34
|
+
* @param {string} path - The path to the directory to list
|
|
35
|
+
* @param {boolean} [recursive=false] - If true, lists files in subdirectories as well
|
|
36
|
+
* @returns {Promise<FSEntry[]>} A promise that resolves to an array of file and directory entries
|
|
37
|
+
*/
|
|
38
|
+
listdir: (path: string, recursive?: boolean) => Promise<FSEntry[]>;
|
|
39
|
+
/**
|
|
40
|
+
* Checks if a file or directory exists.
|
|
41
|
+
* @param {string} path - The path to check for existence
|
|
42
|
+
* @returns {Promise<boolean>} A promise that resolves to true if the path exists, false otherwise
|
|
43
|
+
*/
|
|
44
|
+
exists: (path: string) => Promise<boolean>;
|
|
45
|
+
}
|
|
46
|
+
interface OptionalFileSystemBridgeOperations {
|
|
47
|
+
/**
|
|
48
|
+
* Writes data to a file.
|
|
49
|
+
* @param {string} path - The path to the file to write
|
|
50
|
+
* @param {string | Uint8Array} data - The data to write to the file
|
|
51
|
+
* @param {BufferEncoding} [encoding] - Optional encoding for the data (defaults to 'utf8')
|
|
52
|
+
* @returns {Promise<void>} A promise that resolves when the write operation is complete
|
|
53
|
+
*/
|
|
54
|
+
write?: (path: string, data: string | Uint8Array, encoding?: BufferEncoding) => Promise<void>;
|
|
55
|
+
/**
|
|
56
|
+
* Creates a directory.
|
|
57
|
+
* @param {string} path - The path of the directory to create
|
|
58
|
+
* @returns {Promise<void>} A promise that resolves when the directory is created
|
|
59
|
+
*/
|
|
60
|
+
mkdir?: (path: string) => Promise<void>;
|
|
61
|
+
/**
|
|
62
|
+
* Removes a file or directory.
|
|
63
|
+
* @param {string} path - The path to remove
|
|
64
|
+
* @param {FileSystemBridgeRmOptions} [options] - Optional configuration for removal
|
|
65
|
+
* @returns {Promise<void>} A promise that resolves when the removal is complete
|
|
66
|
+
*/
|
|
67
|
+
rm?: (path: string, options?: FileSystemBridgeRmOptions) => Promise<void>;
|
|
68
|
+
}
|
|
69
|
+
type FileSystemBridgeOperations = OptionalFileSystemBridgeOperations & RequiredFileSystemBridgeOperations;
|
|
70
|
+
type OptionalCapabilityKey = keyof OptionalFileSystemBridgeOperations;
|
|
71
|
+
type HasOptionalCapabilityMap = Record<OptionalCapabilityKey, boolean>;
|
|
72
|
+
type ResolveSafePathFn = (basePath: string, inputPath: string) => string;
|
|
73
|
+
type FileSystemBridgeSetupFn<TOptionsSchema extends z.ZodType, TState extends Record<string, unknown> = Record<string, unknown>> = (ctx: {
|
|
74
|
+
options: z.infer<TOptionsSchema>;
|
|
75
|
+
state: TState;
|
|
76
|
+
resolveSafePath: ResolveSafePathFn;
|
|
77
|
+
}) => FileSystemBridgeOperations;
|
|
78
|
+
interface FileSystemBridgeMetadata {
|
|
79
|
+
/**
|
|
80
|
+
* A unique name for the file system bridge
|
|
81
|
+
*/
|
|
82
|
+
name: string;
|
|
83
|
+
/**
|
|
84
|
+
* A brief description of the file system bridge
|
|
85
|
+
*/
|
|
86
|
+
description: string;
|
|
87
|
+
}
|
|
88
|
+
interface FileSystemBridgeObject<TOptionsSchema extends z.ZodType = z.ZodNever, TState extends Record<string, unknown> = Record<string, unknown>> {
|
|
89
|
+
/**
|
|
90
|
+
* Metadata about the file system bridge
|
|
91
|
+
*/
|
|
92
|
+
meta: FileSystemBridgeMetadata;
|
|
93
|
+
/**
|
|
94
|
+
* Zod schema for validating bridge options
|
|
95
|
+
*/
|
|
96
|
+
optionsSchema?: TOptionsSchema;
|
|
97
|
+
/**
|
|
98
|
+
* Optional state object for the file system bridge.
|
|
99
|
+
* This can be used to store any relevant state information
|
|
100
|
+
* that the bridge implementation may need.
|
|
101
|
+
* @type {Record<string, unknown>}
|
|
102
|
+
* @default {}
|
|
103
|
+
* @example
|
|
104
|
+
* ```ts
|
|
105
|
+
* import { FileSystemBridge } from "@ucdjs/fs-bridge";
|
|
106
|
+
*
|
|
107
|
+
* const fsBridge: FileSystemBridge = {
|
|
108
|
+
* state: {
|
|
109
|
+
* lastReadPath: "",
|
|
110
|
+
* },
|
|
111
|
+
* setup({ options, state }) {
|
|
112
|
+
* return {
|
|
113
|
+
* async read(path) {
|
|
114
|
+
* state.lastReadPath = path;
|
|
115
|
+
* // ... implementation
|
|
116
|
+
* }
|
|
117
|
+
* }
|
|
118
|
+
* }
|
|
119
|
+
* }
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
state?: TState;
|
|
123
|
+
/**
|
|
124
|
+
* Setup function that receives options, and state
|
|
125
|
+
* and returns the filesystem operations implementation
|
|
126
|
+
*/
|
|
127
|
+
setup: FileSystemBridgeSetupFn<TOptionsSchema, TState>;
|
|
128
|
+
}
|
|
129
|
+
interface FileSystemBridge extends FileSystemBridgeOperations {
|
|
130
|
+
/**
|
|
131
|
+
* The capabilities of this file system bridge.
|
|
132
|
+
*/
|
|
133
|
+
optionalCapabilities: HasOptionalCapabilityMap;
|
|
134
|
+
/**
|
|
135
|
+
* Metadata about this file system bridge.
|
|
136
|
+
*/
|
|
137
|
+
meta: FileSystemBridgeMetadata;
|
|
138
|
+
/**
|
|
139
|
+
* Hook system for listening to file system events.
|
|
140
|
+
*/
|
|
141
|
+
hook: HookableCore<FileSystemBridgeHooks>["hook"];
|
|
142
|
+
}
|
|
143
|
+
type FileSystemBridgeArgs<TOptionsSchema extends z.ZodType> = [z.input<TOptionsSchema>] extends [never] ? [] : undefined extends z.input<TOptionsSchema> ? [options?: z.input<TOptionsSchema>] : [options: z.input<TOptionsSchema>];
|
|
144
|
+
type FileSystemBridgeFactory<TOptionsSchema extends z.ZodType> = (...args: FileSystemBridgeArgs<TOptionsSchema>) => FileSystemBridge;
|
|
145
|
+
interface FileSystemBridgeHooks {
|
|
146
|
+
"error": (payload: {
|
|
147
|
+
/**
|
|
148
|
+
* The method that triggered the error.
|
|
149
|
+
*/
|
|
150
|
+
method: keyof FileSystemBridgeOperations;
|
|
151
|
+
/**
|
|
152
|
+
* The path involved in the operation.
|
|
153
|
+
*/
|
|
154
|
+
path: string;
|
|
155
|
+
/**
|
|
156
|
+
* The error that occurred.
|
|
157
|
+
*/
|
|
158
|
+
error: Error;
|
|
159
|
+
/**
|
|
160
|
+
* Optional arguments passed to the method.
|
|
161
|
+
*/
|
|
162
|
+
args?: unknown[];
|
|
163
|
+
}) => void;
|
|
164
|
+
"read:before": (payload: {
|
|
165
|
+
/**
|
|
166
|
+
* The path involved in the operation.
|
|
167
|
+
*/
|
|
168
|
+
path: string;
|
|
169
|
+
}) => void;
|
|
170
|
+
"read:after": (payload: {
|
|
171
|
+
/**
|
|
172
|
+
* The path involved in the operation.
|
|
173
|
+
*/
|
|
174
|
+
path: string;
|
|
175
|
+
/**
|
|
176
|
+
* The content being read or written.
|
|
177
|
+
*/
|
|
178
|
+
content: string;
|
|
179
|
+
}) => void;
|
|
180
|
+
"write:before": (payload: {
|
|
181
|
+
/**
|
|
182
|
+
* The path involved in the operation.
|
|
183
|
+
*/
|
|
184
|
+
path: string;
|
|
185
|
+
/**
|
|
186
|
+
* The content being read or written.
|
|
187
|
+
*/
|
|
188
|
+
content: string;
|
|
189
|
+
/**
|
|
190
|
+
* Optional encoding for string content.
|
|
191
|
+
*/
|
|
192
|
+
encoding?: BufferEncoding;
|
|
193
|
+
}) => void;
|
|
194
|
+
"write:after": (payload: {
|
|
195
|
+
/**
|
|
196
|
+
* The path involved in the operation.
|
|
197
|
+
*/
|
|
198
|
+
path: string;
|
|
199
|
+
}) => void;
|
|
200
|
+
"listdir:before": (payload: {
|
|
201
|
+
/**
|
|
202
|
+
* The path involved in the operation.
|
|
203
|
+
*/
|
|
204
|
+
path: string;
|
|
205
|
+
/**
|
|
206
|
+
* Whether the operation is recursive.
|
|
207
|
+
*/
|
|
208
|
+
recursive: boolean;
|
|
209
|
+
}) => void;
|
|
210
|
+
"listdir:after": (payload: {
|
|
211
|
+
/**
|
|
212
|
+
* The path involved in the operation.
|
|
213
|
+
*/
|
|
214
|
+
path: string;
|
|
215
|
+
/**
|
|
216
|
+
* Whether the operation is recursive.
|
|
217
|
+
*/
|
|
218
|
+
recursive: boolean;
|
|
219
|
+
/**
|
|
220
|
+
* The list of entries returned by the operation.
|
|
221
|
+
*/
|
|
222
|
+
entries: FSEntry[];
|
|
223
|
+
}) => void;
|
|
224
|
+
"exists:before": (payload: {
|
|
225
|
+
/**
|
|
226
|
+
* The path involved in the operation.
|
|
227
|
+
*/
|
|
228
|
+
path: string;
|
|
229
|
+
}) => void;
|
|
230
|
+
"exists:after": (payload: {
|
|
231
|
+
/**
|
|
232
|
+
* The path involved in the operation.
|
|
233
|
+
*/
|
|
234
|
+
path: string;
|
|
235
|
+
/**
|
|
236
|
+
* Whether the path exists.
|
|
237
|
+
*/
|
|
238
|
+
exists: boolean;
|
|
239
|
+
}) => void;
|
|
240
|
+
"mkdir:before": (payload: {
|
|
241
|
+
/**
|
|
242
|
+
* The path involved in the operation.
|
|
243
|
+
*/
|
|
244
|
+
path: string;
|
|
245
|
+
}) => void;
|
|
246
|
+
"mkdir:after": (payload: {
|
|
247
|
+
/**
|
|
248
|
+
* The path involved in the operation.
|
|
249
|
+
*/
|
|
250
|
+
path: string;
|
|
251
|
+
}) => void;
|
|
252
|
+
"rm:before": (payload: {
|
|
253
|
+
/**
|
|
254
|
+
* The path involved in the operation.
|
|
255
|
+
*/
|
|
256
|
+
path: string;
|
|
257
|
+
} & FileSystemBridgeRmOptions) => void;
|
|
258
|
+
"rm:after": (payload: {
|
|
259
|
+
/**
|
|
260
|
+
* The path involved in the operation.
|
|
261
|
+
*/
|
|
262
|
+
path: string;
|
|
263
|
+
} & FileSystemBridgeRmOptions) => void;
|
|
264
|
+
}
|
|
265
|
+
//#endregion
|
|
266
|
+
export { FileSystemBridgeHooks as a, FileSystemBridgeOperations as c, FileSystemBridgeFactory as i, FileSystemBridgeRmOptions as l, FileSystemBridge as n, FileSystemBridgeMetadata as o, FileSystemBridgeArgs as r, FileSystemBridgeObject as s, FSEntry as t, OptionalCapabilityKey as u };
|
package/package.json
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ucdjs/fs-bridge",
|
|
3
|
+
"version": "0.1.1-beta.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Lucas Nørgård",
|
|
7
|
+
"email": "lucasnrgaard@gmail.com",
|
|
8
|
+
"url": "https://luxass.dev"
|
|
9
|
+
},
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"homepage": "https://github.com/ucdjs/ucd",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/ucdjs/ucd.git",
|
|
15
|
+
"directory": "packages/fs-bridge"
|
|
16
|
+
},
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/ucdjs/ucd/issues"
|
|
19
|
+
},
|
|
20
|
+
"imports": {
|
|
21
|
+
"#internal:bridge/node": "./src/bridges/node.ts",
|
|
22
|
+
"#internal:bridge/http": "./src/bridges/http.ts"
|
|
23
|
+
},
|
|
24
|
+
"exports": {
|
|
25
|
+
".": "./dist/index.mjs",
|
|
26
|
+
"./bridges/http": "./dist/bridges/http.mjs",
|
|
27
|
+
"./bridges/node": "./dist/bridges/node.mjs",
|
|
28
|
+
"./errors": "./dist/errors.mjs",
|
|
29
|
+
"./package.json": "./package.json"
|
|
30
|
+
},
|
|
31
|
+
"types": "./dist/index.d.mts",
|
|
32
|
+
"files": [
|
|
33
|
+
"dist"
|
|
34
|
+
],
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=22.18"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@luxass/utils": "2.7.3",
|
|
40
|
+
"defu": "6.1.4",
|
|
41
|
+
"hookable": "6.0.1",
|
|
42
|
+
"pathe": "2.0.3",
|
|
43
|
+
"zod": "4.3.6",
|
|
44
|
+
"@ucdjs-internal/shared": "0.1.1-beta.1",
|
|
45
|
+
"@ucdjs/env": "0.1.1-beta.1",
|
|
46
|
+
"@ucdjs/path-utils": "0.1.1-beta.1",
|
|
47
|
+
"@ucdjs/schemas": "0.1.1-beta.1"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@luxass/eslint-config": "7.2.0",
|
|
51
|
+
"@luxass/msw-utils": "0.6.0",
|
|
52
|
+
"eslint": "10.0.0",
|
|
53
|
+
"publint": "0.3.17",
|
|
54
|
+
"tsdown": "0.20.3",
|
|
55
|
+
"tsx": "4.21.0",
|
|
56
|
+
"typescript": "5.9.3",
|
|
57
|
+
"vitest-testdirs": "4.4.2",
|
|
58
|
+
"@ucdjs-internal/shared": "0.1.1-beta.1",
|
|
59
|
+
"@ucdjs-tooling/tsconfig": "1.0.0",
|
|
60
|
+
"@ucdjs-tooling/tsdown-config": "1.0.0"
|
|
61
|
+
},
|
|
62
|
+
"publishConfig": {
|
|
63
|
+
"access": "public"
|
|
64
|
+
},
|
|
65
|
+
"scripts": {
|
|
66
|
+
"build": "tsdown --tsconfig=./tsconfig.build.json",
|
|
67
|
+
"dev": "tsdown --watch",
|
|
68
|
+
"clean": "git clean -xdf dist node_modules",
|
|
69
|
+
"lint": "eslint .",
|
|
70
|
+
"typecheck": "tsc --noEmit -p tsconfig.build.json",
|
|
71
|
+
"playground:node": "tsx --tsconfig=./tsconfig.json ./playgrounds/node-playground.ts",
|
|
72
|
+
"playground:http": "tsx --tsconfig=./tsconfig.json ./playgrounds/http-playground.ts"
|
|
73
|
+
}
|
|
74
|
+
}
|