shell-dsl 0.0.29 → 0.0.31
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 +33 -7
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/src/fs/index.cjs +4 -4
- package/dist/cjs/src/fs/index.cjs.map +3 -3
- package/dist/cjs/src/fs/memfs-adapter.cjs +7 -108
- package/dist/cjs/src/fs/memfs-adapter.cjs.map +3 -3
- package/dist/cjs/src/fs/real-fs.cjs +27 -123
- package/dist/cjs/src/fs/real-fs.cjs.map +3 -3
- package/dist/cjs/src/fs/{opfs-fs.cjs → web-fs.cjs} +61 -13
- package/dist/cjs/src/fs/web-fs.cjs.map +10 -0
- package/dist/cjs/src/index.cjs +4 -3
- package/dist/cjs/src/index.cjs.map +3 -3
- package/dist/cjs/src/utils/glob.cjs +129 -0
- package/dist/cjs/src/utils/glob.cjs.map +10 -0
- package/dist/cjs/src/utils/index.cjs +3 -1
- package/dist/cjs/src/utils/index.cjs.map +3 -3
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/src/fs/index.mjs +4 -4
- package/dist/mjs/src/fs/index.mjs.map +2 -2
- package/dist/mjs/src/fs/memfs-adapter.mjs +7 -108
- package/dist/mjs/src/fs/memfs-adapter.mjs.map +3 -3
- package/dist/mjs/src/fs/real-fs.mjs +27 -123
- package/dist/mjs/src/fs/real-fs.mjs.map +3 -3
- package/dist/mjs/src/fs/{opfs-fs.mjs → web-fs.mjs} +58 -10
- package/dist/mjs/src/fs/web-fs.mjs.map +10 -0
- package/dist/mjs/src/index.mjs +7 -6
- package/dist/mjs/src/index.mjs.map +3 -3
- package/dist/mjs/src/utils/glob.mjs +89 -0
- package/dist/mjs/src/utils/glob.mjs.map +10 -0
- package/dist/mjs/src/utils/index.mjs +3 -1
- package/dist/mjs/src/utils/index.mjs.map +3 -3
- package/dist/types/src/fs/index.d.ts +2 -2
- package/dist/types/src/fs/real-fs.d.ts +12 -5
- package/dist/types/src/fs/{opfs-fs.d.ts → web-fs.d.ts} +2 -2
- package/dist/types/src/index.d.ts +3 -2
- package/dist/types/src/utils/glob.d.ts +6 -0
- package/dist/types/src/utils/index.d.ts +1 -0
- package/package.json +1 -1
- package/dist/cjs/src/fs/opfs-fs.cjs.map +0 -10
- package/dist/mjs/src/fs/opfs-fs.mjs.map +0 -10
|
@@ -1,16 +1,30 @@
|
|
|
1
1
|
// src/fs/real-fs.ts
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import * as nodeFs from "node:fs/promises";
|
|
4
|
+
import { globVirtualFS } from "../utils/glob.mjs";
|
|
4
5
|
var defaultFS = { promises: nodeFs };
|
|
6
|
+
var nodePathOps = {
|
|
7
|
+
separator: path.sep,
|
|
8
|
+
resolve: (...paths) => path.resolve(...paths),
|
|
9
|
+
normalize: (filePath) => path.normalize(filePath),
|
|
10
|
+
join: (...paths) => path.join(...paths),
|
|
11
|
+
relative: (from, to) => path.relative(from, to),
|
|
12
|
+
isAbsolute: (filePath) => path.isAbsolute(filePath),
|
|
13
|
+
dirname: (filePath) => path.dirname(filePath),
|
|
14
|
+
basename: (filePath) => path.basename(filePath)
|
|
15
|
+
};
|
|
5
16
|
|
|
6
17
|
class FileSystem {
|
|
7
18
|
mountBase;
|
|
8
19
|
rules;
|
|
20
|
+
pathOps;
|
|
9
21
|
underlyingFs;
|
|
10
22
|
constructor(mountPath, permissions, fs) {
|
|
11
|
-
|
|
23
|
+
const underlyingFs = fs ?? defaultFS;
|
|
24
|
+
this.pathOps = underlyingFs.pathOps ?? nodePathOps;
|
|
25
|
+
this.mountBase = mountPath ? this.pathOps.resolve(mountPath) : null;
|
|
12
26
|
this.rules = this.compileRules(permissions ?? {});
|
|
13
|
-
this.underlyingFs =
|
|
27
|
+
this.underlyingFs = underlyingFs;
|
|
14
28
|
}
|
|
15
29
|
compileRules(permissions) {
|
|
16
30
|
return Object.entries(permissions).map(([pattern, permission]) => ({
|
|
@@ -60,7 +74,7 @@ class FileSystem {
|
|
|
60
74
|
}
|
|
61
75
|
resolveSafePath(virtualPath) {
|
|
62
76
|
if (this.mountBase === null) {
|
|
63
|
-
return
|
|
77
|
+
return this.pathOps.resolve(virtualPath);
|
|
64
78
|
}
|
|
65
79
|
const segments = virtualPath.split("/").filter(Boolean);
|
|
66
80
|
let depth = 0;
|
|
@@ -74,12 +88,12 @@ class FileSystem {
|
|
|
74
88
|
depth++;
|
|
75
89
|
}
|
|
76
90
|
}
|
|
77
|
-
const normalized =
|
|
91
|
+
const normalized = this.pathOps.normalize(virtualPath);
|
|
78
92
|
const relativePath = normalized.startsWith("/") ? normalized.slice(1) : normalized;
|
|
79
|
-
const realPath =
|
|
80
|
-
const resolved =
|
|
81
|
-
const relativeFromMount =
|
|
82
|
-
const escapesMount = relativeFromMount === ".." || relativeFromMount.startsWith(`..${
|
|
93
|
+
const realPath = this.pathOps.join(this.mountBase, relativePath);
|
|
94
|
+
const resolved = this.pathOps.resolve(realPath);
|
|
95
|
+
const relativeFromMount = this.pathOps.relative(this.mountBase, resolved);
|
|
96
|
+
const escapesMount = relativeFromMount === ".." || relativeFromMount.startsWith(`..${this.pathOps.separator}`) || this.pathOps.isAbsolute(relativeFromMount);
|
|
83
97
|
if (escapesMount) {
|
|
84
98
|
throw new Error(`Path traversal blocked: "${virtualPath}" escapes mount point`);
|
|
85
99
|
}
|
|
@@ -140,132 +154,22 @@ class FileSystem {
|
|
|
140
154
|
await this.underlyingFs.promises.rm(realPath, opts);
|
|
141
155
|
}
|
|
142
156
|
resolve(...paths) {
|
|
143
|
-
return
|
|
157
|
+
return this.pathOps.resolve("/", ...paths);
|
|
144
158
|
}
|
|
145
159
|
dirname(filePath) {
|
|
146
|
-
return
|
|
160
|
+
return this.pathOps.dirname(filePath);
|
|
147
161
|
}
|
|
148
162
|
basename(filePath) {
|
|
149
|
-
return
|
|
163
|
+
return this.pathOps.basename(filePath);
|
|
150
164
|
}
|
|
151
165
|
async glob(pattern, opts) {
|
|
152
166
|
const cwd = opts?.cwd ?? "/";
|
|
153
167
|
this.checkPermission(cwd, "read");
|
|
154
|
-
|
|
155
|
-
return matches.filter((p) => this.getPermission(p) !== "excluded").sort();
|
|
156
|
-
}
|
|
157
|
-
async expandGlob(pattern, cwd) {
|
|
158
|
-
const patterns = this.expandBraces(pattern);
|
|
159
|
-
const allMatches = [];
|
|
160
|
-
for (const pat of patterns) {
|
|
161
|
-
const matches = await this.matchPattern(pat, cwd);
|
|
162
|
-
allMatches.push(...matches);
|
|
163
|
-
}
|
|
164
|
-
return [...new Set(allMatches)].sort();
|
|
165
|
-
}
|
|
166
|
-
expandBraces(pattern) {
|
|
167
|
-
const braceMatch = pattern.match(/\{([^{}]+)\}/);
|
|
168
|
-
if (!braceMatch)
|
|
169
|
-
return [pattern];
|
|
170
|
-
const before = pattern.slice(0, braceMatch.index);
|
|
171
|
-
const after = pattern.slice(braceMatch.index + braceMatch[0].length);
|
|
172
|
-
const options = braceMatch[1].split(",");
|
|
173
|
-
const results = [];
|
|
174
|
-
for (const opt of options) {
|
|
175
|
-
const expanded = this.expandBraces(before + opt + after);
|
|
176
|
-
results.push(...expanded);
|
|
177
|
-
}
|
|
178
|
-
return results;
|
|
179
|
-
}
|
|
180
|
-
async matchPattern(pattern, cwd) {
|
|
181
|
-
const parts = pattern.split("/").filter((p) => p !== "");
|
|
182
|
-
const isAbsolute2 = pattern.startsWith("/");
|
|
183
|
-
const startDir = isAbsolute2 ? "/" : cwd;
|
|
184
|
-
return this.matchParts(parts, startDir);
|
|
185
|
-
}
|
|
186
|
-
async matchParts(parts, currentPath) {
|
|
187
|
-
if (parts.length === 0) {
|
|
188
|
-
return [currentPath];
|
|
189
|
-
}
|
|
190
|
-
const [part, ...rest] = parts;
|
|
191
|
-
if (part === "**") {
|
|
192
|
-
const results = [];
|
|
193
|
-
const withoutStar = await this.matchParts(rest, currentPath);
|
|
194
|
-
results.push(...withoutStar);
|
|
195
|
-
try {
|
|
196
|
-
const realPath = this.resolveSafePath(currentPath);
|
|
197
|
-
const entries = await this.underlyingFs.promises.readdir(realPath);
|
|
198
|
-
for (const entry of entries) {
|
|
199
|
-
const entryPath = path.posix.join(currentPath, String(entry));
|
|
200
|
-
try {
|
|
201
|
-
const entryRealPath = this.resolveSafePath(entryPath);
|
|
202
|
-
const stat = await this.underlyingFs.promises.stat(entryRealPath);
|
|
203
|
-
if (stat.isDirectory()) {
|
|
204
|
-
const subMatches = await this.matchParts(parts, entryPath);
|
|
205
|
-
results.push(...subMatches);
|
|
206
|
-
}
|
|
207
|
-
} catch {}
|
|
208
|
-
}
|
|
209
|
-
} catch {}
|
|
210
|
-
return results;
|
|
211
|
-
}
|
|
212
|
-
const regex = this.globToRegex(part);
|
|
213
|
-
try {
|
|
214
|
-
const realPath = this.resolveSafePath(currentPath);
|
|
215
|
-
const entries = await this.underlyingFs.promises.readdir(realPath);
|
|
216
|
-
const results = [];
|
|
217
|
-
for (const entry of entries) {
|
|
218
|
-
const entryName = String(entry);
|
|
219
|
-
if (regex.test(entryName)) {
|
|
220
|
-
const entryPath = path.posix.join(currentPath, entryName);
|
|
221
|
-
if (rest.length === 0) {
|
|
222
|
-
results.push(entryPath);
|
|
223
|
-
} else {
|
|
224
|
-
try {
|
|
225
|
-
const entryRealPath = this.resolveSafePath(entryPath);
|
|
226
|
-
const stat = await this.underlyingFs.promises.stat(entryRealPath);
|
|
227
|
-
if (stat.isDirectory()) {
|
|
228
|
-
const subMatches = await this.matchParts(rest, entryPath);
|
|
229
|
-
results.push(...subMatches);
|
|
230
|
-
}
|
|
231
|
-
} catch {}
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
return results;
|
|
236
|
-
} catch {
|
|
237
|
-
return [];
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
globToRegex(pattern) {
|
|
241
|
-
let regex = "^";
|
|
242
|
-
for (let i = 0;i < pattern.length; i++) {
|
|
243
|
-
const char = pattern[i];
|
|
244
|
-
if (char === "*") {
|
|
245
|
-
regex += "[^/]*";
|
|
246
|
-
} else if (char === "?") {
|
|
247
|
-
regex += "[^/]";
|
|
248
|
-
} else if (char === "[") {
|
|
249
|
-
let j = i + 1;
|
|
250
|
-
let classContent = "";
|
|
251
|
-
while (j < pattern.length && pattern[j] !== "]") {
|
|
252
|
-
classContent += pattern[j];
|
|
253
|
-
j++;
|
|
254
|
-
}
|
|
255
|
-
regex += `[${classContent}]`;
|
|
256
|
-
i = j;
|
|
257
|
-
} else if (".+^${}()|\\".includes(char)) {
|
|
258
|
-
regex += "\\" + char;
|
|
259
|
-
} else {
|
|
260
|
-
regex += char;
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
regex += "$";
|
|
264
|
-
return new RegExp(regex);
|
|
168
|
+
return globVirtualFS(this, pattern, { cwd });
|
|
265
169
|
}
|
|
266
170
|
}
|
|
267
171
|
export {
|
|
268
172
|
FileSystem
|
|
269
173
|
};
|
|
270
174
|
|
|
271
|
-
//# debugId=
|
|
175
|
+
//# debugId=093147B23E1E8F5A64756E2164756E21
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/fs/real-fs.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"import * as path from \"path\";\nimport * as nodeFs from \"node:fs/promises\";\nimport type { VirtualFS, FileStat } from \"../types.mjs\";\n\nexport type Permission = \"read-write\" | \"read-only\" | \"excluded\";\nexport type PermissionRules = Record<string, Permission>;\n\n// Minimal interface for the underlying fs (compatible with node:fs and memfs)\nexport interface UnderlyingFS {\n promises: {\n readFile(path: string): Promise<Buffer | Uint8Array | string>;\n readdir(path: string): Promise<string[]>;\n stat(path: string): Promise<{\n isFile(): boolean;\n isDirectory(): boolean;\n size: number;\n mtime: Date;\n }>;\n writeFile(path: string, data: Buffer | string): Promise<void>;\n appendFile(path: string, data: Buffer | string): Promise<void>;\n mkdir(path: string, opts?: { recursive?: boolean }): Promise<string | undefined | void>;\n rm(path: string, opts?: { recursive?: boolean; force?: boolean }): Promise<void>;\n };\n}\n\n// Default: use real node:fs\nconst defaultFS: UnderlyingFS = { promises: nodeFs };\n\ninterface CompiledRule {\n pattern: string;\n permission: Permission;\n specificity: number;\n}\n\nexport class FileSystem implements VirtualFS {\n private readonly mountBase: string | null;\n private readonly rules: CompiledRule[];\n protected readonly underlyingFs: UnderlyingFS;\n\n constructor(mountPath?: string, permissions?: PermissionRules, fs?: UnderlyingFS) {\n this.mountBase = mountPath ? path.resolve(mountPath) : null;\n this.rules = this.compileRules(permissions ?? {});\n this.underlyingFs = fs ?? defaultFS;\n }\n\n private compileRules(permissions: PermissionRules): CompiledRule[] {\n return Object.entries(permissions)\n .map(([pattern, permission]) => ({\n pattern,\n permission,\n specificity: this.calculateSpecificity(pattern),\n }))\n .sort((a, b) => b.specificity - a.specificity); // highest first\n }\n\n private calculateSpecificity(pattern: string): number {\n const segments = pattern.split(\"/\").filter(Boolean);\n let score = segments.length * 1000; // segment count is primary\n\n for (const seg of segments) {\n if (seg === \"**\") score += 0;\n else if (seg.includes(\"*\")) score += 1;\n else score += 10; // literal segment\n }\n return score;\n }\n\n public getPermission(virtualPath: string): Permission {\n const normalized = virtualPath.replace(/^\\/+/, \"\"); // strip leading slashes\n\n for (const rule of this.rules) {\n if (this.matchGlob(rule.pattern, normalized)) {\n return rule.permission;\n }\n }\n return \"read-write\"; // default\n }\n\n private matchGlob(pattern: string, filePath: string): boolean {\n // Convert glob to regex\n // ** matches any path segments, * matches within segment\n const regex = pattern\n .split(\"/\")\n .map((seg) => {\n if (seg === \"**\") return \".*\";\n return seg.replace(/\\*/g, \"[^/]*\").replace(/\\?/g, \"[^/]\");\n })\n .join(\"/\");\n return new RegExp(`^${regex}$`).test(filePath);\n }\n\n public checkPermission(virtualPath: string, operation: \"read\" | \"write\"): void {\n const perm = this.getPermission(virtualPath);\n\n if (perm === \"excluded\") {\n throw new Error(`Access denied: \"${virtualPath}\" is excluded`);\n }\n if (operation === \"write\" && perm === \"read-only\") {\n throw new Error(`Access denied: \"${virtualPath}\" is read-only`);\n }\n }\n\n private resolveSafePath(virtualPath: string): string {\n if (this.mountBase === null) {\n return path.resolve(virtualPath);\n }\n\n // Check for path traversal by tracking depth\n const segments = virtualPath.split(\"/\").filter(Boolean);\n let depth = 0;\n for (const seg of segments) {\n if (seg === \"..\") {\n depth--;\n if (depth < 0) {\n throw new Error(`Path traversal blocked: \"${virtualPath}\" escapes mount point`);\n }\n } else if (seg !== \".\") {\n depth++;\n }\n }\n\n const normalized = path.normalize(virtualPath);\n const relativePath = normalized.startsWith(\"/\") ? normalized.slice(1) : normalized;\n const realPath = path.join(this.mountBase, relativePath);\n const resolved = path.resolve(realPath);\n\n // Double-check containment (defense in depth), including root mounts.\n const relativeFromMount = path.relative(this.mountBase, resolved);\n const escapesMount =\n relativeFromMount === \"..\" ||\n relativeFromMount.startsWith(`..${path.sep}`) ||\n path.isAbsolute(relativeFromMount);\n if (escapesMount) {\n throw new Error(`Path traversal blocked: \"${virtualPath}\" escapes mount point`);\n }\n\n return resolved;\n }\n\n // Read operations\n async readFile(filePath: string): Promise<Buffer>;\n async readFile(filePath: string, encoding: BufferEncoding): Promise<string>;\n async readFile(filePath: string, encoding?: BufferEncoding): Promise<Buffer | string> {\n this.checkPermission(filePath, \"read\");\n const realPath = this.resolveSafePath(filePath);\n const content = await this.underlyingFs.promises.readFile(realPath);\n const buf = Buffer.from(content);\n return encoding ? buf.toString(encoding) : buf;\n }\n\n async readdir(dirPath: string): Promise<string[]> {\n this.checkPermission(dirPath, \"read\");\n const realPath = this.resolveSafePath(dirPath);\n const entries = await this.underlyingFs.promises.readdir(realPath);\n return entries.map(String);\n }\n\n async stat(filePath: string): Promise<FileStat> {\n this.checkPermission(filePath, \"read\");\n const realPath = this.resolveSafePath(filePath);\n const stats = await this.underlyingFs.promises.stat(realPath);\n return {\n isFile: () => stats.isFile(),\n isDirectory: () => stats.isDirectory(),\n size: stats.size,\n mtime: stats.mtime,\n };\n }\n\n async exists(filePath: string): Promise<boolean> {\n try {\n this.checkPermission(filePath, \"read\");\n const realPath = this.resolveSafePath(filePath);\n await this.underlyingFs.promises.stat(realPath);\n return true;\n } catch {\n return false;\n }\n }\n\n // Write operations\n async writeFile(filePath: string, data: Buffer | string): Promise<void> {\n this.checkPermission(filePath, \"write\");\n const realPath = this.resolveSafePath(filePath);\n await this.underlyingFs.promises.writeFile(realPath, data);\n }\n\n async appendFile(filePath: string, data: Buffer | string): Promise<void> {\n this.checkPermission(filePath, \"write\");\n const realPath = this.resolveSafePath(filePath);\n await this.underlyingFs.promises.appendFile(realPath, data);\n }\n\n async mkdir(dirPath: string, opts?: { recursive?: boolean }): Promise<void> {\n this.checkPermission(dirPath, \"write\");\n const realPath = this.resolveSafePath(dirPath);\n await this.underlyingFs.promises.mkdir(realPath, opts);\n }\n\n async rm(filePath: string, opts?: { recursive?: boolean; force?: boolean }): Promise<void> {\n this.checkPermission(filePath, \"write\");\n const realPath = this.resolveSafePath(filePath);\n await this.underlyingFs.promises.rm(realPath, opts);\n }\n\n // Path utilities (no permission check needed)\n resolve(...paths: string[]): string {\n return path.resolve(\"/\", ...paths);\n }\n\n dirname(filePath: string): string {\n return path.dirname(filePath);\n }\n\n basename(filePath: string): string {\n return path.basename(filePath);\n }\n\n // Glob with permission filtering\n async glob(pattern: string, opts?: { cwd?: string }): Promise<string[]> {\n const cwd = opts?.cwd ?? \"/\";\n this.checkPermission(cwd, \"read\");\n\n const matches = await this.expandGlob(pattern, cwd);\n\n // Filter out excluded paths\n return matches.filter((p) => this.getPermission(p) !== \"excluded\").sort();\n }\n\n // Glob expansion (similar to memfs-adapter implementation)\n private async expandGlob(pattern: string, cwd: string): Promise<string[]> {\n // Handle brace expansion first\n const patterns = this.expandBraces(pattern);\n const allMatches: string[] = [];\n\n for (const pat of patterns) {\n const matches = await this.matchPattern(pat, cwd);\n allMatches.push(...matches);\n }\n\n // Remove duplicates and sort\n return [...new Set(allMatches)].sort();\n }\n\n private expandBraces(pattern: string): string[] {\n const braceMatch = pattern.match(/\\{([^{}]+)\\}/);\n if (!braceMatch) return [pattern];\n\n const before = pattern.slice(0, braceMatch.index);\n const after = pattern.slice(braceMatch.index! + braceMatch[0].length);\n const options = braceMatch[1]!.split(\",\");\n\n const results: string[] = [];\n for (const opt of options) {\n const expanded = this.expandBraces(before + opt + after);\n results.push(...expanded);\n }\n return results;\n }\n\n private async matchPattern(pattern: string, cwd: string): Promise<string[]> {\n const parts = pattern.split(\"/\").filter((p) => p !== \"\");\n const isAbsolute = pattern.startsWith(\"/\");\n const startDir = isAbsolute ? \"/\" : cwd;\n\n return this.matchParts(parts, startDir);\n }\n\n private async matchParts(parts: string[], currentPath: string): Promise<string[]> {\n if (parts.length === 0) {\n return [currentPath];\n }\n\n const [part, ...rest] = parts;\n\n // Handle ** (recursive glob)\n if (part === \"**\") {\n const results: string[] = [];\n\n // Match current directory\n const withoutStar = await this.matchParts(rest, currentPath);\n results.push(...withoutStar);\n\n // Recurse into subdirectories\n try {\n const realPath = this.resolveSafePath(currentPath);\n const entries = await this.underlyingFs.promises.readdir(realPath);\n for (const entry of entries) {\n const entryPath = path.posix.join(currentPath, String(entry));\n try {\n const entryRealPath = this.resolveSafePath(entryPath);\n const stat = await this.underlyingFs.promises.stat(entryRealPath);\n if (stat.isDirectory()) {\n const subMatches = await this.matchParts(parts, entryPath);\n results.push(...subMatches);\n }\n } catch {\n // Skip inaccessible entries\n }\n }\n } catch {\n // Directory not readable\n }\n\n return results;\n }\n\n // Handle regular glob patterns\n const regex = this.globToRegex(part!);\n\n try {\n const realPath = this.resolveSafePath(currentPath);\n const entries = await this.underlyingFs.promises.readdir(realPath);\n const results: string[] = [];\n\n for (const entry of entries) {\n const entryName = String(entry);\n if (regex.test(entryName)) {\n const entryPath = path.posix.join(currentPath, entryName);\n if (rest.length === 0) {\n results.push(entryPath);\n } else {\n try {\n const entryRealPath = this.resolveSafePath(entryPath);\n const stat = await this.underlyingFs.promises.stat(entryRealPath);\n if (stat.isDirectory()) {\n const subMatches = await this.matchParts(rest, entryPath);\n results.push(...subMatches);\n }\n } catch {\n // Skip inaccessible entries\n }\n }\n }\n }\n\n return results;\n } catch {\n return [];\n }\n }\n\n private globToRegex(pattern: string): RegExp {\n let regex = \"^\";\n for (let i = 0; i < pattern.length; i++) {\n const char = pattern[i]!;\n if (char === \"*\") {\n regex += \"[^/]*\";\n } else if (char === \"?\") {\n regex += \"[^/]\";\n } else if (char === \"[\") {\n // Character class\n let j = i + 1;\n let classContent = \"\";\n while (j < pattern.length && pattern[j] !== \"]\") {\n classContent += pattern[j];\n j++;\n }\n regex += `[${classContent}]`;\n i = j;\n } else if (\".+^${}()|\\\\\".includes(char)) {\n regex += \"\\\\\" + char;\n } else {\n regex += char;\n }\n }\n regex += \"$\";\n return new RegExp(regex);\n }\n}\n"
|
|
5
|
+
"import * as path from \"path\";\nimport * as nodeFs from \"node:fs/promises\";\nimport type { VirtualFS, FileStat } from \"../types.mjs\";\nimport { globVirtualFS } from \"../utils/glob.mjs\";\n\nexport type Permission = \"read-write\" | \"read-only\" | \"excluded\";\nexport type PermissionRules = Record<string, Permission>;\nexport interface PathOps {\n readonly separator: string;\n resolve(...paths: string[]): string;\n normalize(path: string): string;\n join(...paths: string[]): string;\n relative(from: string, to: string): string;\n isAbsolute(path: string): boolean;\n dirname(path: string): string;\n basename(path: string): string;\n}\n\n// Minimal interface for the underlying fs (compatible with node:fs and memfs)\nexport interface UnderlyingFS {\n pathOps?: PathOps;\n promises: {\n readFile(path: string): Promise<Buffer | Uint8Array | string>;\n readdir(path: string): Promise<string[]>;\n stat(path: string): Promise<{\n isFile(): boolean;\n isDirectory(): boolean;\n size: number;\n mtime: Date;\n }>;\n writeFile(path: string, data: Buffer | string): Promise<void>;\n appendFile(path: string, data: Buffer | string): Promise<void>;\n mkdir(path: string, opts?: { recursive?: boolean }): Promise<string | undefined | void>;\n rm(path: string, opts?: { recursive?: boolean; force?: boolean }): Promise<void>;\n };\n}\n\n// Default: use real node:fs\nconst defaultFS: UnderlyingFS = { promises: nodeFs };\nconst nodePathOps: PathOps = {\n separator: path.sep,\n resolve: (...paths) => path.resolve(...paths),\n normalize: (filePath) => path.normalize(filePath),\n join: (...paths) => path.join(...paths),\n relative: (from, to) => path.relative(from, to),\n isAbsolute: (filePath) => path.isAbsolute(filePath),\n dirname: (filePath) => path.dirname(filePath),\n basename: (filePath) => path.basename(filePath),\n};\n\ninterface CompiledRule {\n pattern: string;\n permission: Permission;\n specificity: number;\n}\n\nexport class FileSystem implements VirtualFS {\n private readonly mountBase: string | null;\n private readonly rules: CompiledRule[];\n private readonly pathOps: PathOps;\n protected readonly underlyingFs: UnderlyingFS;\n\n constructor(mountPath?: string, permissions?: PermissionRules, fs?: UnderlyingFS) {\n const underlyingFs = fs ?? defaultFS;\n this.pathOps = underlyingFs.pathOps ?? nodePathOps;\n this.mountBase = mountPath ? this.pathOps.resolve(mountPath) : null;\n this.rules = this.compileRules(permissions ?? {});\n this.underlyingFs = underlyingFs;\n }\n\n private compileRules(permissions: PermissionRules): CompiledRule[] {\n return Object.entries(permissions)\n .map(([pattern, permission]) => ({\n pattern,\n permission,\n specificity: this.calculateSpecificity(pattern),\n }))\n .sort((a, b) => b.specificity - a.specificity); // highest first\n }\n\n private calculateSpecificity(pattern: string): number {\n const segments = pattern.split(\"/\").filter(Boolean);\n let score = segments.length * 1000; // segment count is primary\n\n for (const seg of segments) {\n if (seg === \"**\") score += 0;\n else if (seg.includes(\"*\")) score += 1;\n else score += 10; // literal segment\n }\n return score;\n }\n\n public getPermission(virtualPath: string): Permission {\n const normalized = virtualPath.replace(/^\\/+/, \"\"); // strip leading slashes\n\n for (const rule of this.rules) {\n if (this.matchGlob(rule.pattern, normalized)) {\n return rule.permission;\n }\n }\n return \"read-write\"; // default\n }\n\n private matchGlob(pattern: string, filePath: string): boolean {\n // Convert glob to regex\n // ** matches any path segments, * matches within segment\n const regex = pattern\n .split(\"/\")\n .map((seg) => {\n if (seg === \"**\") return \".*\";\n return seg.replace(/\\*/g, \"[^/]*\").replace(/\\?/g, \"[^/]\");\n })\n .join(\"/\");\n return new RegExp(`^${regex}$`).test(filePath);\n }\n\n public checkPermission(virtualPath: string, operation: \"read\" | \"write\"): void {\n const perm = this.getPermission(virtualPath);\n\n if (perm === \"excluded\") {\n throw new Error(`Access denied: \"${virtualPath}\" is excluded`);\n }\n if (operation === \"write\" && perm === \"read-only\") {\n throw new Error(`Access denied: \"${virtualPath}\" is read-only`);\n }\n }\n\n private resolveSafePath(virtualPath: string): string {\n if (this.mountBase === null) {\n return this.pathOps.resolve(virtualPath);\n }\n\n // Check for path traversal by tracking depth\n const segments = virtualPath.split(\"/\").filter(Boolean);\n let depth = 0;\n for (const seg of segments) {\n if (seg === \"..\") {\n depth--;\n if (depth < 0) {\n throw new Error(`Path traversal blocked: \"${virtualPath}\" escapes mount point`);\n }\n } else if (seg !== \".\") {\n depth++;\n }\n }\n\n const normalized = this.pathOps.normalize(virtualPath);\n const relativePath = normalized.startsWith(\"/\") ? normalized.slice(1) : normalized;\n const realPath = this.pathOps.join(this.mountBase, relativePath);\n const resolved = this.pathOps.resolve(realPath);\n\n // Double-check containment (defense in depth), including root mounts.\n const relativeFromMount = this.pathOps.relative(this.mountBase, resolved);\n const escapesMount =\n relativeFromMount === \"..\" ||\n relativeFromMount.startsWith(`..${this.pathOps.separator}`) ||\n this.pathOps.isAbsolute(relativeFromMount);\n if (escapesMount) {\n throw new Error(`Path traversal blocked: \"${virtualPath}\" escapes mount point`);\n }\n\n return resolved;\n }\n\n // Read operations\n async readFile(filePath: string): Promise<Buffer>;\n async readFile(filePath: string, encoding: BufferEncoding): Promise<string>;\n async readFile(filePath: string, encoding?: BufferEncoding): Promise<Buffer | string> {\n this.checkPermission(filePath, \"read\");\n const realPath = this.resolveSafePath(filePath);\n const content = await this.underlyingFs.promises.readFile(realPath);\n const buf = Buffer.from(content);\n return encoding ? buf.toString(encoding) : buf;\n }\n\n async readdir(dirPath: string): Promise<string[]> {\n this.checkPermission(dirPath, \"read\");\n const realPath = this.resolveSafePath(dirPath);\n const entries = await this.underlyingFs.promises.readdir(realPath);\n return entries.map(String);\n }\n\n async stat(filePath: string): Promise<FileStat> {\n this.checkPermission(filePath, \"read\");\n const realPath = this.resolveSafePath(filePath);\n const stats = await this.underlyingFs.promises.stat(realPath);\n return {\n isFile: () => stats.isFile(),\n isDirectory: () => stats.isDirectory(),\n size: stats.size,\n mtime: stats.mtime,\n };\n }\n\n async exists(filePath: string): Promise<boolean> {\n try {\n this.checkPermission(filePath, \"read\");\n const realPath = this.resolveSafePath(filePath);\n await this.underlyingFs.promises.stat(realPath);\n return true;\n } catch {\n return false;\n }\n }\n\n // Write operations\n async writeFile(filePath: string, data: Buffer | string): Promise<void> {\n this.checkPermission(filePath, \"write\");\n const realPath = this.resolveSafePath(filePath);\n await this.underlyingFs.promises.writeFile(realPath, data);\n }\n\n async appendFile(filePath: string, data: Buffer | string): Promise<void> {\n this.checkPermission(filePath, \"write\");\n const realPath = this.resolveSafePath(filePath);\n await this.underlyingFs.promises.appendFile(realPath, data);\n }\n\n async mkdir(dirPath: string, opts?: { recursive?: boolean }): Promise<void> {\n this.checkPermission(dirPath, \"write\");\n const realPath = this.resolveSafePath(dirPath);\n await this.underlyingFs.promises.mkdir(realPath, opts);\n }\n\n async rm(filePath: string, opts?: { recursive?: boolean; force?: boolean }): Promise<void> {\n this.checkPermission(filePath, \"write\");\n const realPath = this.resolveSafePath(filePath);\n await this.underlyingFs.promises.rm(realPath, opts);\n }\n\n // Path utilities (no permission check needed)\n resolve(...paths: string[]): string {\n return this.pathOps.resolve(\"/\", ...paths);\n }\n\n dirname(filePath: string): string {\n return this.pathOps.dirname(filePath);\n }\n\n basename(filePath: string): string {\n return this.pathOps.basename(filePath);\n }\n\n // Glob expansion\n async glob(pattern: string, opts?: { cwd?: string }): Promise<string[]> {\n const cwd = opts?.cwd ?? \"/\";\n this.checkPermission(cwd, \"read\");\n return globVirtualFS(this, pattern, { cwd });\n }\n}\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": ";AAAA;AACA;
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": ";AAAA;AACA;AAEA;AAmCA,IAAM,YAA0B,EAAE,UAAU,OAAO;AACnD,IAAM,cAAuB;AAAA,EAC3B,WAAgB;AAAA,EAChB,SAAS,IAAI,UAAe,aAAQ,GAAG,KAAK;AAAA,EAC5C,WAAW,CAAC,aAAkB,eAAU,QAAQ;AAAA,EAChD,MAAM,IAAI,UAAe,UAAK,GAAG,KAAK;AAAA,EACtC,UAAU,CAAC,MAAM,OAAY,cAAS,MAAM,EAAE;AAAA,EAC9C,YAAY,CAAC,aAAkB,gBAAW,QAAQ;AAAA,EAClD,SAAS,CAAC,aAAkB,aAAQ,QAAQ;AAAA,EAC5C,UAAU,CAAC,aAAkB,cAAS,QAAQ;AAChD;AAAA;AAQO,MAAM,WAAgC;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACE;AAAA,EAEnB,WAAW,CAAC,WAAoB,aAA+B,IAAmB;AAAA,IAChF,MAAM,eAAe,MAAM;AAAA,IAC3B,KAAK,UAAU,aAAa,WAAW;AAAA,IACvC,KAAK,YAAY,YAAY,KAAK,QAAQ,QAAQ,SAAS,IAAI;AAAA,IAC/D,KAAK,QAAQ,KAAK,aAAa,eAAe,CAAC,CAAC;AAAA,IAChD,KAAK,eAAe;AAAA;AAAA,EAGd,YAAY,CAAC,aAA8C;AAAA,IACjE,OAAO,OAAO,QAAQ,WAAW,EAC9B,IAAI,EAAE,SAAS,iBAAiB;AAAA,MAC/B;AAAA,MACA;AAAA,MACA,aAAa,KAAK,qBAAqB,OAAO;AAAA,IAChD,EAAE,EACD,KAAK,CAAC,GAAG,MAAM,EAAE,cAAc,EAAE,WAAW;AAAA;AAAA,EAGzC,oBAAoB,CAAC,SAAyB;AAAA,IACpD,MAAM,WAAW,QAAQ,MAAM,GAAG,EAAE,OAAO,OAAO;AAAA,IAClD,IAAI,QAAQ,SAAS,SAAS;AAAA,IAE9B,WAAW,OAAO,UAAU;AAAA,MAC1B,IAAI,QAAQ;AAAA,QAAM,SAAS;AAAA,MACtB,SAAI,IAAI,SAAS,GAAG;AAAA,QAAG,SAAS;AAAA,MAChC;AAAA,iBAAS;AAAA,IAChB;AAAA,IACA,OAAO;AAAA;AAAA,EAGF,aAAa,CAAC,aAAiC;AAAA,IACpD,MAAM,aAAa,YAAY,QAAQ,QAAQ,EAAE;AAAA,IAEjD,WAAW,QAAQ,KAAK,OAAO;AAAA,MAC7B,IAAI,KAAK,UAAU,KAAK,SAAS,UAAU,GAAG;AAAA,QAC5C,OAAO,KAAK;AAAA,MACd;AAAA,IACF;AAAA,IACA,OAAO;AAAA;AAAA,EAGD,SAAS,CAAC,SAAiB,UAA2B;AAAA,IAG5D,MAAM,QAAQ,QACX,MAAM,GAAG,EACT,IAAI,CAAC,QAAQ;AAAA,MACZ,IAAI,QAAQ;AAAA,QAAM,OAAO;AAAA,MACzB,OAAO,IAAI,QAAQ,OAAO,OAAO,EAAE,QAAQ,OAAO,MAAM;AAAA,KACzD,EACA,KAAK,GAAG;AAAA,IACX,OAAO,IAAI,OAAO,IAAI,QAAQ,EAAE,KAAK,QAAQ;AAAA;AAAA,EAGxC,eAAe,CAAC,aAAqB,WAAmC;AAAA,IAC7E,MAAM,OAAO,KAAK,cAAc,WAAW;AAAA,IAE3C,IAAI,SAAS,YAAY;AAAA,MACvB,MAAM,IAAI,MAAM,mBAAmB,0BAA0B;AAAA,IAC/D;AAAA,IACA,IAAI,cAAc,WAAW,SAAS,aAAa;AAAA,MACjD,MAAM,IAAI,MAAM,mBAAmB,2BAA2B;AAAA,IAChE;AAAA;AAAA,EAGM,eAAe,CAAC,aAA6B;AAAA,IACnD,IAAI,KAAK,cAAc,MAAM;AAAA,MAC3B,OAAO,KAAK,QAAQ,QAAQ,WAAW;AAAA,IACzC;AAAA,IAGA,MAAM,WAAW,YAAY,MAAM,GAAG,EAAE,OAAO,OAAO;AAAA,IACtD,IAAI,QAAQ;AAAA,IACZ,WAAW,OAAO,UAAU;AAAA,MAC1B,IAAI,QAAQ,MAAM;AAAA,QAChB;AAAA,QACA,IAAI,QAAQ,GAAG;AAAA,UACb,MAAM,IAAI,MAAM,4BAA4B,kCAAkC;AAAA,QAChF;AAAA,MACF,EAAO,SAAI,QAAQ,KAAK;AAAA,QACtB;AAAA,MACF;AAAA,IACF;AAAA,IAEA,MAAM,aAAa,KAAK,QAAQ,UAAU,WAAW;AAAA,IACrD,MAAM,eAAe,WAAW,WAAW,GAAG,IAAI,WAAW,MAAM,CAAC,IAAI;AAAA,IACxE,MAAM,WAAW,KAAK,QAAQ,KAAK,KAAK,WAAW,YAAY;AAAA,IAC/D,MAAM,WAAW,KAAK,QAAQ,QAAQ,QAAQ;AAAA,IAG9C,MAAM,oBAAoB,KAAK,QAAQ,SAAS,KAAK,WAAW,QAAQ;AAAA,IACxE,MAAM,eACJ,sBAAsB,QACtB,kBAAkB,WAAW,KAAK,KAAK,QAAQ,WAAW,KAC1D,KAAK,QAAQ,WAAW,iBAAiB;AAAA,IAC3C,IAAI,cAAc;AAAA,MAChB,MAAM,IAAI,MAAM,4BAA4B,kCAAkC;AAAA,IAChF;AAAA,IAEA,OAAO;AAAA;AAAA,OAMH,SAAQ,CAAC,UAAkB,UAAqD;AAAA,IACpF,KAAK,gBAAgB,UAAU,MAAM;AAAA,IACrC,MAAM,WAAW,KAAK,gBAAgB,QAAQ;AAAA,IAC9C,MAAM,UAAU,MAAM,KAAK,aAAa,SAAS,SAAS,QAAQ;AAAA,IAClE,MAAM,MAAM,OAAO,KAAK,OAAO;AAAA,IAC/B,OAAO,WAAW,IAAI,SAAS,QAAQ,IAAI;AAAA;AAAA,OAGvC,QAAO,CAAC,SAAoC;AAAA,IAChD,KAAK,gBAAgB,SAAS,MAAM;AAAA,IACpC,MAAM,WAAW,KAAK,gBAAgB,OAAO;AAAA,IAC7C,MAAM,UAAU,MAAM,KAAK,aAAa,SAAS,QAAQ,QAAQ;AAAA,IACjE,OAAO,QAAQ,IAAI,MAAM;AAAA;AAAA,OAGrB,KAAI,CAAC,UAAqC;AAAA,IAC9C,KAAK,gBAAgB,UAAU,MAAM;AAAA,IACrC,MAAM,WAAW,KAAK,gBAAgB,QAAQ;AAAA,IAC9C,MAAM,QAAQ,MAAM,KAAK,aAAa,SAAS,KAAK,QAAQ;AAAA,IAC5D,OAAO;AAAA,MACL,QAAQ,MAAM,MAAM,OAAO;AAAA,MAC3B,aAAa,MAAM,MAAM,YAAY;AAAA,MACrC,MAAM,MAAM;AAAA,MACZ,OAAO,MAAM;AAAA,IACf;AAAA;AAAA,OAGI,OAAM,CAAC,UAAoC;AAAA,IAC/C,IAAI;AAAA,MACF,KAAK,gBAAgB,UAAU,MAAM;AAAA,MACrC,MAAM,WAAW,KAAK,gBAAgB,QAAQ;AAAA,MAC9C,MAAM,KAAK,aAAa,SAAS,KAAK,QAAQ;AAAA,MAC9C,OAAO;AAAA,MACP,MAAM;AAAA,MACN,OAAO;AAAA;AAAA;AAAA,OAKL,UAAS,CAAC,UAAkB,MAAsC;AAAA,IACtE,KAAK,gBAAgB,UAAU,OAAO;AAAA,IACtC,MAAM,WAAW,KAAK,gBAAgB,QAAQ;AAAA,IAC9C,MAAM,KAAK,aAAa,SAAS,UAAU,UAAU,IAAI;AAAA;AAAA,OAGrD,WAAU,CAAC,UAAkB,MAAsC;AAAA,IACvE,KAAK,gBAAgB,UAAU,OAAO;AAAA,IACtC,MAAM,WAAW,KAAK,gBAAgB,QAAQ;AAAA,IAC9C,MAAM,KAAK,aAAa,SAAS,WAAW,UAAU,IAAI;AAAA;AAAA,OAGtD,MAAK,CAAC,SAAiB,MAA+C;AAAA,IAC1E,KAAK,gBAAgB,SAAS,OAAO;AAAA,IACrC,MAAM,WAAW,KAAK,gBAAgB,OAAO;AAAA,IAC7C,MAAM,KAAK,aAAa,SAAS,MAAM,UAAU,IAAI;AAAA;AAAA,OAGjD,GAAE,CAAC,UAAkB,MAAgE;AAAA,IACzF,KAAK,gBAAgB,UAAU,OAAO;AAAA,IACtC,MAAM,WAAW,KAAK,gBAAgB,QAAQ;AAAA,IAC9C,MAAM,KAAK,aAAa,SAAS,GAAG,UAAU,IAAI;AAAA;AAAA,EAIpD,OAAO,IAAI,OAAyB;AAAA,IAClC,OAAO,KAAK,QAAQ,QAAQ,KAAK,GAAG,KAAK;AAAA;AAAA,EAG3C,OAAO,CAAC,UAA0B;AAAA,IAChC,OAAO,KAAK,QAAQ,QAAQ,QAAQ;AAAA;AAAA,EAGtC,QAAQ,CAAC,UAA0B;AAAA,IACjC,OAAO,KAAK,QAAQ,SAAS,QAAQ;AAAA;AAAA,OAIjC,KAAI,CAAC,SAAiB,MAA4C;AAAA,IACtE,MAAM,MAAM,MAAM,OAAO;AAAA,IACzB,KAAK,gBAAgB,KAAK,MAAM;AAAA,IAChC,OAAO,cAAc,MAAM,SAAS,EAAE,IAAI,CAAC;AAAA;AAE/C;",
|
|
8
|
+
"debugId": "093147B23E1E8F5A64756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
|
@@ -1,8 +1,56 @@
|
|
|
1
|
-
// src/fs/
|
|
1
|
+
// src/fs/web-fs.ts
|
|
2
2
|
import { FileSystem } from "./real-fs.mjs";
|
|
3
3
|
var DIRECTORY_MTIME = new Date(0);
|
|
4
|
-
|
|
4
|
+
var WEB_PATH_OPS = {
|
|
5
|
+
separator: "/",
|
|
6
|
+
resolve(...paths) {
|
|
7
|
+
return normalizeWebPath(paths.join("/"));
|
|
8
|
+
},
|
|
9
|
+
normalize: normalizeWebPath,
|
|
10
|
+
join(...paths) {
|
|
11
|
+
const nonEmpty = paths.filter((segment) => segment.length > 0);
|
|
12
|
+
if (nonEmpty.length === 0) {
|
|
13
|
+
return ".";
|
|
14
|
+
}
|
|
15
|
+
return normalizeWebPath(nonEmpty.join("/"));
|
|
16
|
+
},
|
|
17
|
+
relative(from, to) {
|
|
18
|
+
const fromSegments = getPathSegments(from);
|
|
19
|
+
const toSegments = getPathSegments(to);
|
|
20
|
+
let shared = 0;
|
|
21
|
+
while (shared < fromSegments.length && shared < toSegments.length && fromSegments[shared] === toSegments[shared]) {
|
|
22
|
+
shared++;
|
|
23
|
+
}
|
|
24
|
+
const up = new Array(fromSegments.length - shared).fill("..");
|
|
25
|
+
const down = toSegments.slice(shared);
|
|
26
|
+
return [...up, ...down].join("/");
|
|
27
|
+
},
|
|
28
|
+
isAbsolute(path) {
|
|
29
|
+
return path.startsWith("/");
|
|
30
|
+
},
|
|
31
|
+
dirname(path) {
|
|
32
|
+
const normalized = normalizeWebPath(path);
|
|
33
|
+
if (normalized === "/") {
|
|
34
|
+
return "/";
|
|
35
|
+
}
|
|
36
|
+
const segments = getPathSegments(normalized);
|
|
37
|
+
if (segments.length <= 1) {
|
|
38
|
+
return "/";
|
|
39
|
+
}
|
|
40
|
+
return `/${segments.slice(0, -1).join("/")}`;
|
|
41
|
+
},
|
|
42
|
+
basename(path) {
|
|
43
|
+
const normalized = normalizeWebPath(path);
|
|
44
|
+
if (normalized === "/") {
|
|
45
|
+
return "";
|
|
46
|
+
}
|
|
47
|
+
const segments = getPathSegments(normalized);
|
|
48
|
+
return segments[segments.length - 1] ?? "";
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
function createWebUnderlyingFS(root) {
|
|
5
52
|
return {
|
|
53
|
+
pathOps: WEB_PATH_OPS,
|
|
6
54
|
promises: {
|
|
7
55
|
async readFile(path) {
|
|
8
56
|
const { parentSegments, name } = splitParent(path);
|
|
@@ -82,7 +130,7 @@ function createOPFSUnderlyingFS(root) {
|
|
|
82
130
|
const name = segments[segments.length - 1];
|
|
83
131
|
const exists = await entryExists(parent, name);
|
|
84
132
|
if (exists) {
|
|
85
|
-
throw new Error(`EEXIST: file already exists, mkdir '${
|
|
133
|
+
throw new Error(`EEXIST: file already exists, mkdir '${normalizeWebPath(path)}'`);
|
|
86
134
|
}
|
|
87
135
|
await parent.getDirectoryHandle(name, { create: true });
|
|
88
136
|
},
|
|
@@ -106,9 +154,9 @@ function createOPFSUnderlyingFS(root) {
|
|
|
106
154
|
};
|
|
107
155
|
}
|
|
108
156
|
|
|
109
|
-
class
|
|
157
|
+
class WebFileSystem extends FileSystem {
|
|
110
158
|
constructor(root, permissions) {
|
|
111
|
-
super("/", permissions,
|
|
159
|
+
super("/", permissions, createWebUnderlyingFS(root));
|
|
112
160
|
}
|
|
113
161
|
}
|
|
114
162
|
function createDirectoryStat() {
|
|
@@ -119,7 +167,7 @@ function createDirectoryStat() {
|
|
|
119
167
|
mtime: DIRECTORY_MTIME
|
|
120
168
|
};
|
|
121
169
|
}
|
|
122
|
-
function
|
|
170
|
+
function normalizeWebPath(path) {
|
|
123
171
|
const normalized = path.replace(/\\/g, "/");
|
|
124
172
|
const rawSegments = (normalized.startsWith("/") ? normalized : `/${normalized}`).split("/").filter(Boolean);
|
|
125
173
|
const segments = [];
|
|
@@ -135,7 +183,7 @@ function normalizeOpfsPath(path) {
|
|
|
135
183
|
return `/${segments.join("/")}`;
|
|
136
184
|
}
|
|
137
185
|
function getPathSegments(path) {
|
|
138
|
-
const normalized =
|
|
186
|
+
const normalized = normalizeWebPath(path);
|
|
139
187
|
return normalized.split("/").filter(Boolean);
|
|
140
188
|
}
|
|
141
189
|
function splitParent(path) {
|
|
@@ -200,8 +248,8 @@ function toWritableData(data) {
|
|
|
200
248
|
return out.buffer;
|
|
201
249
|
}
|
|
202
250
|
export {
|
|
203
|
-
|
|
204
|
-
|
|
251
|
+
createWebUnderlyingFS,
|
|
252
|
+
WebFileSystem
|
|
205
253
|
};
|
|
206
254
|
|
|
207
|
-
//# debugId=
|
|
255
|
+
//# debugId=A9C7F77B6DBC9D6064756E2164756E21
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/fs/web-fs.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"import { FileSystem, type PathOps, type PermissionRules, type UnderlyingFS } from \"./real-fs.mjs\";\n\nconst DIRECTORY_MTIME = new Date(0);\nconst WEB_PATH_OPS: PathOps = {\n separator: \"/\",\n resolve(...paths: string[]): string {\n return normalizeWebPath(paths.join(\"/\"));\n },\n normalize: normalizeWebPath,\n join(...paths: string[]): string {\n const nonEmpty = paths.filter((segment) => segment.length > 0);\n if (nonEmpty.length === 0) {\n return \".\";\n }\n return normalizeWebPath(nonEmpty.join(\"/\"));\n },\n relative(from: string, to: string): string {\n const fromSegments = getPathSegments(from);\n const toSegments = getPathSegments(to);\n let shared = 0;\n\n while (\n shared < fromSegments.length &&\n shared < toSegments.length &&\n fromSegments[shared] === toSegments[shared]\n ) {\n shared++;\n }\n\n const up = new Array(fromSegments.length - shared).fill(\"..\");\n const down = toSegments.slice(shared);\n return [...up, ...down].join(\"/\");\n },\n isAbsolute(path: string): boolean {\n return path.startsWith(\"/\");\n },\n dirname(path: string): string {\n const normalized = normalizeWebPath(path);\n if (normalized === \"/\") {\n return \"/\";\n }\n\n const segments = getPathSegments(normalized);\n if (segments.length <= 1) {\n return \"/\";\n }\n\n return `/${segments.slice(0, -1).join(\"/\")}`;\n },\n basename(path: string): string {\n const normalized = normalizeWebPath(path);\n if (normalized === \"/\") {\n return \"\";\n }\n\n const segments = getPathSegments(normalized);\n return segments[segments.length - 1] ?? \"\";\n },\n};\n\nexport function createWebUnderlyingFS(root: FileSystemDirectoryHandle): UnderlyingFS {\n return {\n pathOps: WEB_PATH_OPS,\n promises: {\n async readFile(path: string): Promise<Buffer> {\n const { parentSegments, name } = splitParent(path);\n const parent = await walkDirectory(root, parentSegments, false);\n const fileHandle = await parent.getFileHandle(name, { create: false });\n const file = await fileHandle.getFile();\n return Buffer.from(await file.arrayBuffer());\n },\n\n async readdir(path: string): Promise<string[]> {\n const dir = await walkDirectory(root, getPathSegments(path), false);\n const entries: string[] = [];\n for await (const [name] of dir.entries()) {\n entries.push(name);\n }\n return entries;\n },\n\n async stat(path: string): Promise<{\n isFile(): boolean;\n isDirectory(): boolean;\n size: number;\n mtime: Date;\n }> {\n const segments = getPathSegments(path);\n\n if (segments.length === 0) {\n return createDirectoryStat();\n }\n\n const { parentSegments, name } = splitParent(path);\n const parent = await walkDirectory(root, parentSegments, false);\n\n try {\n const fileHandle = await parent.getFileHandle(name, { create: false });\n const file = await fileHandle.getFile();\n return {\n isFile: () => true,\n isDirectory: () => false,\n size: file.size,\n mtime: new Date(file.lastModified ?? 0),\n };\n } catch (error) {\n if (!isNotFoundOrTypeMismatch(error)) throw error;\n }\n\n try {\n await parent.getDirectoryHandle(name, { create: false });\n return createDirectoryStat();\n } catch (error) {\n if (!isNotFoundOrTypeMismatch(error)) throw error;\n throw error;\n }\n },\n\n async writeFile(path: string, data: Buffer | string): Promise<void> {\n const { parentSegments, name } = splitParent(path);\n const parent = await walkDirectory(root, parentSegments, false);\n const fileHandle = await parent.getFileHandle(name, { create: true });\n const writable = await fileHandle.createWritable();\n await writable.write(toWritableData(data));\n await writable.close();\n },\n\n async appendFile(path: string, data: Buffer | string): Promise<void> {\n const { parentSegments, name } = splitParent(path);\n const parent = await walkDirectory(root, parentSegments, false);\n const fileHandle = await parent.getFileHandle(name, { create: true });\n const file = await fileHandle.getFile();\n const writable = await fileHandle.createWritable({ keepExistingData: true });\n await writable.write({\n type: \"write\",\n position: file.size,\n data: toWritableData(data),\n });\n await writable.close();\n },\n\n async mkdir(path: string, opts?: { recursive?: boolean }): Promise<void> {\n const segments = getPathSegments(path);\n if (segments.length === 0) {\n return;\n }\n\n if (opts?.recursive) {\n await walkDirectory(root, segments, true);\n return;\n }\n\n const parent = await walkDirectory(root, segments.slice(0, -1), false);\n const name = segments[segments.length - 1]!;\n const exists = await entryExists(parent, name);\n if (exists) {\n throw new Error(`EEXIST: file already exists, mkdir '${normalizeWebPath(path)}'`);\n }\n await parent.getDirectoryHandle(name, { create: true });\n },\n\n async rm(path: string, opts?: { recursive?: boolean; force?: boolean }): Promise<void> {\n const segments = getPathSegments(path);\n if (segments.length === 0) {\n throw new Error(\"EPERM: operation not permitted, rm '/'\");\n }\n\n const parent = await walkDirectory(root, segments.slice(0, -1), false);\n const name = segments[segments.length - 1]!;\n try {\n await parent.removeEntry(name, { recursive: opts?.recursive });\n } catch (error) {\n if (opts?.force && isNotFoundError(error)) {\n return;\n }\n throw error;\n }\n },\n },\n };\n}\n\nexport class WebFileSystem extends FileSystem {\n constructor(root: FileSystemDirectoryHandle, permissions?: PermissionRules) {\n super(\"/\", permissions, createWebUnderlyingFS(root));\n }\n}\n\nfunction createDirectoryStat() {\n return {\n isFile: () => false,\n isDirectory: () => true,\n size: 0,\n mtime: DIRECTORY_MTIME,\n };\n}\n\nfunction normalizeWebPath(path: string): string {\n const normalized = path.replace(/\\\\/g, \"/\");\n const rawSegments = (normalized.startsWith(\"/\") ? normalized : `/${normalized}`)\n .split(\"/\")\n .filter(Boolean);\n\n const segments: string[] = [];\n for (const segment of rawSegments) {\n if (segment === \".\") continue;\n if (segment === \"..\") {\n segments.pop();\n continue;\n }\n segments.push(segment);\n }\n\n return `/${segments.join(\"/\")}`;\n}\n\nfunction getPathSegments(path: string): string[] {\n const normalized = normalizeWebPath(path);\n return normalized.split(\"/\").filter(Boolean);\n}\n\nfunction splitParent(path: string): { parentSegments: string[]; name: string } {\n const segments = getPathSegments(path);\n if (segments.length === 0) {\n throw new Error(`Invalid file path: \"${path}\"`);\n }\n return {\n parentSegments: segments.slice(0, -1),\n name: segments[segments.length - 1]!,\n };\n}\n\nasync function walkDirectory(\n root: FileSystemDirectoryHandle,\n segments: string[],\n create: boolean\n): Promise<FileSystemDirectoryHandle> {\n let current = root;\n for (const segment of segments) {\n current = await current.getDirectoryHandle(segment, { create });\n }\n return current;\n}\n\nasync function entryExists(dir: FileSystemDirectoryHandle, name: string): Promise<boolean> {\n try {\n await dir.getFileHandle(name, { create: false });\n return true;\n } catch (error) {\n if (isTypeMismatchError(error)) return true;\n if (!isNotFoundError(error)) throw error;\n }\n\n try {\n await dir.getDirectoryHandle(name, { create: false });\n return true;\n } catch (error) {\n if (isTypeMismatchError(error)) return true;\n if (!isNotFoundError(error)) throw error;\n return false;\n }\n}\n\nfunction isNotFoundOrTypeMismatch(error: unknown): boolean {\n return isNotFoundError(error) || isTypeMismatchError(error);\n}\n\nfunction isNotFoundError(error: unknown): boolean {\n return getErrorName(error) === \"NotFoundError\";\n}\n\nfunction isTypeMismatchError(error: unknown): boolean {\n return getErrorName(error) === \"TypeMismatchError\";\n}\n\nfunction getErrorName(error: unknown): string | undefined {\n if (!error || typeof error !== \"object\") return undefined;\n const named = error as { name?: unknown };\n return typeof named.name === \"string\" ? named.name : undefined;\n}\n\nfunction toWritableData(data: Buffer | string): string | ArrayBuffer {\n if (typeof data === \"string\") {\n return data;\n }\n const out = new Uint8Array(data.length);\n out.set(data);\n return out.buffer;\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "A9C7F77B6DBC9D6064756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
package/dist/mjs/src/index.mjs
CHANGED
|
@@ -28,12 +28,12 @@ import { createVirtualFS } from "./fs/index.mjs";
|
|
|
28
28
|
import {
|
|
29
29
|
FileSystem,
|
|
30
30
|
ReadOnlyFileSystem,
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
WebFileSystem,
|
|
32
|
+
createWebUnderlyingFS
|
|
33
33
|
} from "./fs/index.mjs";
|
|
34
34
|
import { createStdin, StdinImpl } from "./io/index.mjs";
|
|
35
35
|
import { createStdout, createStderr, createPipe, OutputCollectorImpl, PipeBuffer } from "./io/index.mjs";
|
|
36
|
-
import { escape, escapeForInterpolation } from "./utils/index.mjs";
|
|
36
|
+
import { escape, escapeForInterpolation, globVirtualFS } from "./utils/index.mjs";
|
|
37
37
|
export {
|
|
38
38
|
tokenToString,
|
|
39
39
|
parse,
|
|
@@ -55,15 +55,17 @@ export {
|
|
|
55
55
|
isCaseNode,
|
|
56
56
|
isArithmeticNode,
|
|
57
57
|
isAndNode,
|
|
58
|
+
globVirtualFS,
|
|
58
59
|
escapeForInterpolation,
|
|
59
60
|
escape,
|
|
61
|
+
createWebUnderlyingFS,
|
|
60
62
|
createVirtualFS,
|
|
61
63
|
createStdout,
|
|
62
64
|
createStdin,
|
|
63
65
|
createStderr,
|
|
64
66
|
createShellDSL,
|
|
65
67
|
createPipe,
|
|
66
|
-
|
|
68
|
+
WebFileSystem,
|
|
67
69
|
StdinImpl,
|
|
68
70
|
ShellPromise,
|
|
69
71
|
ShellError,
|
|
@@ -73,7 +75,6 @@ export {
|
|
|
73
75
|
Parser,
|
|
74
76
|
ParseError,
|
|
75
77
|
OutputCollectorImpl,
|
|
76
|
-
OPFSFileSystem,
|
|
77
78
|
Lexer,
|
|
78
79
|
LexError,
|
|
79
80
|
Interpreter,
|
|
@@ -82,4 +83,4 @@ export {
|
|
|
82
83
|
BreakException
|
|
83
84
|
};
|
|
84
85
|
|
|
85
|
-
//# debugId=
|
|
86
|
+
//# debugId=6B9F05D80609142564756E2164756E21
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/index.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"// Main class exports\nexport { ShellDSL, createShellDSL, type Program } from \"./shell-dsl.mjs\";\nexport { ShellPromise, type ShellPromiseOptions } from \"./shell-promise.mjs\";\n\n// Types\nexport type {\n VirtualFS,\n FileStat,\n Command,\n CommandContext,\n Stdin,\n Stdout,\n Stderr,\n OutputCollector,\n ExecResult,\n ShellConfig,\n RawValue,\n} from \"./types.mjs\";\nexport { isRawValue } from \"./types.mjs\";\n\n// Errors\nexport { ShellError, LexError, ParseError } from \"./errors.mjs\";\n\n// Lexer\nexport { Lexer, lex, tokenToString } from \"./lexer/index.mjs\";\nexport type { Token, RedirectMode } from \"./lexer/index.mjs\";\n\n// Parser\nexport { Parser, parse } from \"./parser/index.mjs\";\nexport type {\n ASTNode,\n Redirect,\n CommandNode,\n PipelineNode,\n AndNode,\n OrNode,\n SequenceNode,\n LiteralNode,\n VariableNode,\n SubstitutionNode,\n GlobNode,\n ConcatNode,\n IfNode,\n ForNode,\n WhileNode,\n UntilNode,\n CaseNode,\n CaseClause,\n ArithmeticNode,\n} from \"./parser/index.mjs\";\nexport {\n isCommandNode,\n isPipelineNode,\n isAndNode,\n isOrNode,\n isSequenceNode,\n isLiteralNode,\n isVariableNode,\n isSubstitutionNode,\n isGlobNode,\n isConcatNode,\n isIfNode,\n isForNode,\n isWhileNode,\n isUntilNode,\n isCaseNode,\n isArithmeticNode,\n} from \"./parser/index.mjs\";\n\n// Interpreter\nexport { Interpreter, type InterpreterOptions, BreakException, ContinueException } from \"./interpreter/index.mjs\";\n\n// Filesystem\nexport { createVirtualFS } from \"./fs/index.mjs\";\nexport {\n FileSystem,\n ReadOnlyFileSystem,\n
|
|
5
|
+
"// Main class exports\nexport { ShellDSL, createShellDSL, type Program } from \"./shell-dsl.mjs\";\nexport { ShellPromise, type ShellPromiseOptions } from \"./shell-promise.mjs\";\n\n// Types\nexport type {\n VirtualFS,\n FileStat,\n Command,\n CommandContext,\n Stdin,\n Stdout,\n Stderr,\n OutputCollector,\n ExecResult,\n ShellConfig,\n RawValue,\n} from \"./types.mjs\";\nexport { isRawValue } from \"./types.mjs\";\n\n// Errors\nexport { ShellError, LexError, ParseError } from \"./errors.mjs\";\n\n// Lexer\nexport { Lexer, lex, tokenToString } from \"./lexer/index.mjs\";\nexport type { Token, RedirectMode } from \"./lexer/index.mjs\";\n\n// Parser\nexport { Parser, parse } from \"./parser/index.mjs\";\nexport type {\n ASTNode,\n Redirect,\n CommandNode,\n PipelineNode,\n AndNode,\n OrNode,\n SequenceNode,\n LiteralNode,\n VariableNode,\n SubstitutionNode,\n GlobNode,\n ConcatNode,\n IfNode,\n ForNode,\n WhileNode,\n UntilNode,\n CaseNode,\n CaseClause,\n ArithmeticNode,\n} from \"./parser/index.mjs\";\nexport {\n isCommandNode,\n isPipelineNode,\n isAndNode,\n isOrNode,\n isSequenceNode,\n isLiteralNode,\n isVariableNode,\n isSubstitutionNode,\n isGlobNode,\n isConcatNode,\n isIfNode,\n isForNode,\n isWhileNode,\n isUntilNode,\n isCaseNode,\n isArithmeticNode,\n} from \"./parser/index.mjs\";\n\n// Interpreter\nexport { Interpreter, type InterpreterOptions, BreakException, ContinueException } from \"./interpreter/index.mjs\";\n\n// Filesystem\nexport { createVirtualFS } from \"./fs/index.mjs\";\nexport {\n FileSystem,\n ReadOnlyFileSystem,\n WebFileSystem,\n createWebUnderlyingFS,\n type PathOps,\n type Permission,\n type PermissionRules,\n type UnderlyingFS,\n} from \"./fs/index.mjs\";\n\n// I/O\nexport { createStdin, StdinImpl } from \"./io/index.mjs\";\nexport { createStdout, createStderr, createPipe, OutputCollectorImpl, PipeBuffer } from \"./io/index.mjs\";\n\n// Utilities\nexport { escape, escapeForInterpolation, globVirtualFS } from \"./utils/index.mjs\";\nexport type { GlobVirtualFS, GlobOptions } from \"./utils/index.mjs\";\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": ";AACA;AACA;AAgBA;AAGA;AAGA;AAIA;AAsBA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAoBA;AAGA;AACA;AAAA;AAAA;AAAA;AAAA;AAAA;
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": ";AACA;AACA;AAgBA;AAGA;AAGA;AAIA;AAsBA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAoBA;AAGA;AACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAYA;AACA;AAGA;",
|
|
8
|
+
"debugId": "6B9F05D80609142564756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// src/utils/glob.ts
|
|
2
|
+
import { matchGlob } from "./match-glob.mjs";
|
|
3
|
+
async function globVirtualFS(fs, pattern, opts) {
|
|
4
|
+
const cwd = fs.resolve(opts?.cwd ?? "/");
|
|
5
|
+
const patterns = expandBraces(pattern);
|
|
6
|
+
const allMatches = [];
|
|
7
|
+
for (const expandedPattern of patterns) {
|
|
8
|
+
const matches = await matchPattern(fs, expandedPattern, cwd);
|
|
9
|
+
allMatches.push(...matches);
|
|
10
|
+
}
|
|
11
|
+
return [...new Set(allMatches)].sort();
|
|
12
|
+
}
|
|
13
|
+
function expandBraces(pattern) {
|
|
14
|
+
const braceMatch = pattern.match(/\{([^{}]+)\}/);
|
|
15
|
+
if (!braceMatch)
|
|
16
|
+
return [pattern];
|
|
17
|
+
const before = pattern.slice(0, braceMatch.index);
|
|
18
|
+
const after = pattern.slice(braceMatch.index + braceMatch[0].length);
|
|
19
|
+
const options = braceMatch[1].split(",");
|
|
20
|
+
const results = [];
|
|
21
|
+
for (const option of options) {
|
|
22
|
+
results.push(...expandBraces(before + option + after));
|
|
23
|
+
}
|
|
24
|
+
return results;
|
|
25
|
+
}
|
|
26
|
+
async function matchPattern(fs, pattern, cwd) {
|
|
27
|
+
const parts = pattern.split("/").filter(Boolean);
|
|
28
|
+
const startDir = pattern.startsWith("/") ? "/" : cwd;
|
|
29
|
+
return matchParts(fs, parts, startDir);
|
|
30
|
+
}
|
|
31
|
+
async function matchParts(fs, parts, currentPath) {
|
|
32
|
+
if (parts.length === 0) {
|
|
33
|
+
return await pathExists(fs, currentPath) ? [currentPath] : [];
|
|
34
|
+
}
|
|
35
|
+
const [part, ...rest] = parts;
|
|
36
|
+
if (part === "**") {
|
|
37
|
+
const results = await matchParts(fs, rest, currentPath);
|
|
38
|
+
try {
|
|
39
|
+
const entries = await fs.readdir(currentPath);
|
|
40
|
+
for (const entry of entries) {
|
|
41
|
+
const entryPath = fs.resolve(currentPath, entry);
|
|
42
|
+
try {
|
|
43
|
+
const stat = await fs.stat(entryPath);
|
|
44
|
+
if (stat.isDirectory()) {
|
|
45
|
+
results.push(...await matchParts(fs, parts, entryPath));
|
|
46
|
+
}
|
|
47
|
+
} catch {}
|
|
48
|
+
}
|
|
49
|
+
} catch {}
|
|
50
|
+
return results;
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
const entries = await fs.readdir(currentPath);
|
|
54
|
+
const results = [];
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
if (!matchGlob(part, entry))
|
|
57
|
+
continue;
|
|
58
|
+
const entryPath = fs.resolve(currentPath, entry);
|
|
59
|
+
if (rest.length === 0) {
|
|
60
|
+
if (await pathExists(fs, entryPath)) {
|
|
61
|
+
results.push(entryPath);
|
|
62
|
+
}
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
const stat = await fs.stat(entryPath);
|
|
67
|
+
if (stat.isDirectory()) {
|
|
68
|
+
results.push(...await matchParts(fs, rest, entryPath));
|
|
69
|
+
}
|
|
70
|
+
} catch {}
|
|
71
|
+
}
|
|
72
|
+
return results;
|
|
73
|
+
} catch {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function pathExists(fs, filePath) {
|
|
78
|
+
try {
|
|
79
|
+
await fs.stat(filePath);
|
|
80
|
+
return true;
|
|
81
|
+
} catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export {
|
|
86
|
+
globVirtualFS
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
//# debugId=EF2EBEB444AD8F0064756E2164756E21
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/utils/glob.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"import type { VirtualFS } from \"../types.mjs\";\nimport { matchGlob } from \"./match-glob.mjs\";\n\nexport type GlobVirtualFS = Pick<VirtualFS, \"readdir\" | \"stat\" | \"resolve\">;\n\nexport interface GlobOptions {\n cwd?: string;\n}\n\nexport async function globVirtualFS(fs: GlobVirtualFS, pattern: string, opts?: GlobOptions): Promise<string[]> {\n const cwd = fs.resolve(opts?.cwd ?? \"/\");\n const patterns = expandBraces(pattern);\n const allMatches: string[] = [];\n\n for (const expandedPattern of patterns) {\n const matches = await matchPattern(fs, expandedPattern, cwd);\n allMatches.push(...matches);\n }\n\n return [...new Set(allMatches)].sort();\n}\n\nfunction expandBraces(pattern: string): string[] {\n const braceMatch = pattern.match(/\\{([^{}]+)\\}/);\n if (!braceMatch) return [pattern];\n\n const before = pattern.slice(0, braceMatch.index);\n const after = pattern.slice(braceMatch.index! + braceMatch[0].length);\n const options = braceMatch[1]!.split(\",\");\n const results: string[] = [];\n\n for (const option of options) {\n results.push(...expandBraces(before + option + after));\n }\n\n return results;\n}\n\nasync function matchPattern(fs: GlobVirtualFS, pattern: string, cwd: string): Promise<string[]> {\n const parts = pattern.split(\"/\").filter(Boolean);\n const startDir = pattern.startsWith(\"/\") ? \"/\" : cwd;\n return matchParts(fs, parts, startDir);\n}\n\nasync function matchParts(fs: GlobVirtualFS, parts: string[], currentPath: string): Promise<string[]> {\n if (parts.length === 0) {\n return (await pathExists(fs, currentPath)) ? [currentPath] : [];\n }\n\n const [part, ...rest] = parts;\n\n if (part === \"**\") {\n const results = await matchParts(fs, rest, currentPath);\n\n try {\n const entries = await fs.readdir(currentPath);\n for (const entry of entries) {\n const entryPath = fs.resolve(currentPath, entry);\n try {\n const stat = await fs.stat(entryPath);\n if (stat.isDirectory()) {\n results.push(...(await matchParts(fs, parts, entryPath)));\n }\n } catch {\n // Skip entries we can't stat.\n }\n }\n } catch {\n // Directory not readable.\n }\n\n return results;\n }\n\n try {\n const entries = await fs.readdir(currentPath);\n const results: string[] = [];\n\n for (const entry of entries) {\n if (!matchGlob(part!, entry)) continue;\n\n const entryPath = fs.resolve(currentPath, entry);\n\n if (rest.length === 0) {\n if (await pathExists(fs, entryPath)) {\n results.push(entryPath);\n }\n continue;\n }\n\n try {\n const stat = await fs.stat(entryPath);\n if (stat.isDirectory()) {\n results.push(...(await matchParts(fs, rest, entryPath)));\n }\n } catch {\n // Skip entries we can't stat.\n }\n }\n\n return results;\n } catch {\n return [];\n }\n}\n\nasync function pathExists(fs: GlobVirtualFS, filePath: string): Promise<boolean> {\n try {\n await fs.stat(filePath);\n return true;\n } catch {\n return false;\n }\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": ";AACA;AAQA,eAAsB,aAAa,CAAC,IAAmB,SAAiB,MAAuC;AAAA,EAC7G,MAAM,MAAM,GAAG,QAAQ,MAAM,OAAO,GAAG;AAAA,EACvC,MAAM,WAAW,aAAa,OAAO;AAAA,EACrC,MAAM,aAAuB,CAAC;AAAA,EAE9B,WAAW,mBAAmB,UAAU;AAAA,IACtC,MAAM,UAAU,MAAM,aAAa,IAAI,iBAAiB,GAAG;AAAA,IAC3D,WAAW,KAAK,GAAG,OAAO;AAAA,EAC5B;AAAA,EAEA,OAAO,CAAC,GAAG,IAAI,IAAI,UAAU,CAAC,EAAE,KAAK;AAAA;AAGvC,SAAS,YAAY,CAAC,SAA2B;AAAA,EAC/C,MAAM,aAAa,QAAQ,MAAM,cAAc;AAAA,EAC/C,IAAI,CAAC;AAAA,IAAY,OAAO,CAAC,OAAO;AAAA,EAEhC,MAAM,SAAS,QAAQ,MAAM,GAAG,WAAW,KAAK;AAAA,EAChD,MAAM,QAAQ,QAAQ,MAAM,WAAW,QAAS,WAAW,GAAG,MAAM;AAAA,EACpE,MAAM,UAAU,WAAW,GAAI,MAAM,GAAG;AAAA,EACxC,MAAM,UAAoB,CAAC;AAAA,EAE3B,WAAW,UAAU,SAAS;AAAA,IAC5B,QAAQ,KAAK,GAAG,aAAa,SAAS,SAAS,KAAK,CAAC;AAAA,EACvD;AAAA,EAEA,OAAO;AAAA;AAGT,eAAe,YAAY,CAAC,IAAmB,SAAiB,KAAgC;AAAA,EAC9F,MAAM,QAAQ,QAAQ,MAAM,GAAG,EAAE,OAAO,OAAO;AAAA,EAC/C,MAAM,WAAW,QAAQ,WAAW,GAAG,IAAI,MAAM;AAAA,EACjD,OAAO,WAAW,IAAI,OAAO,QAAQ;AAAA;AAGvC,eAAe,UAAU,CAAC,IAAmB,OAAiB,aAAwC;AAAA,EACpG,IAAI,MAAM,WAAW,GAAG;AAAA,IACtB,OAAQ,MAAM,WAAW,IAAI,WAAW,IAAK,CAAC,WAAW,IAAI,CAAC;AAAA,EAChE;AAAA,EAEA,OAAO,SAAS,QAAQ;AAAA,EAExB,IAAI,SAAS,MAAM;AAAA,IACjB,MAAM,UAAU,MAAM,WAAW,IAAI,MAAM,WAAW;AAAA,IAEtD,IAAI;AAAA,MACF,MAAM,UAAU,MAAM,GAAG,QAAQ,WAAW;AAAA,MAC5C,WAAW,SAAS,SAAS;AAAA,QAC3B,MAAM,YAAY,GAAG,QAAQ,aAAa,KAAK;AAAA,QAC/C,IAAI;AAAA,UACF,MAAM,OAAO,MAAM,GAAG,KAAK,SAAS;AAAA,UACpC,IAAI,KAAK,YAAY,GAAG;AAAA,YACtB,QAAQ,KAAK,GAAI,MAAM,WAAW,IAAI,OAAO,SAAS,CAAE;AAAA,UAC1D;AAAA,UACA,MAAM;AAAA,MAGV;AAAA,MACA,MAAM;AAAA,IAIR,OAAO;AAAA,EACT;AAAA,EAEA,IAAI;AAAA,IACF,MAAM,UAAU,MAAM,GAAG,QAAQ,WAAW;AAAA,IAC5C,MAAM,UAAoB,CAAC;AAAA,IAE3B,WAAW,SAAS,SAAS;AAAA,MAC3B,IAAI,CAAC,UAAU,MAAO,KAAK;AAAA,QAAG;AAAA,MAE9B,MAAM,YAAY,GAAG,QAAQ,aAAa,KAAK;AAAA,MAE/C,IAAI,KAAK,WAAW,GAAG;AAAA,QACrB,IAAI,MAAM,WAAW,IAAI,SAAS,GAAG;AAAA,UACnC,QAAQ,KAAK,SAAS;AAAA,QACxB;AAAA,QACA;AAAA,MACF;AAAA,MAEA,IAAI;AAAA,QACF,MAAM,OAAO,MAAM,GAAG,KAAK,SAAS;AAAA,QACpC,IAAI,KAAK,YAAY,GAAG;AAAA,UACtB,QAAQ,KAAK,GAAI,MAAM,WAAW,IAAI,MAAM,SAAS,CAAE;AAAA,QACzD;AAAA,QACA,MAAM;AAAA,IAGV;AAAA,IAEA,OAAO;AAAA,IACP,MAAM;AAAA,IACN,OAAO,CAAC;AAAA;AAAA;AAIZ,eAAe,UAAU,CAAC,IAAmB,UAAoC;AAAA,EAC/E,IAAI;AAAA,IACF,MAAM,GAAG,KAAK,QAAQ;AAAA,IACtB,OAAO;AAAA,IACP,MAAM;AAAA,IACN,OAAO;AAAA;AAAA;",
|
|
8
|
+
"debugId": "EF2EBEB444AD8F0064756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
// src/utils/index.ts
|
|
2
2
|
import { escape, escapeForInterpolation } from "./escape.mjs";
|
|
3
3
|
import { expandEscapes } from "./expand-escapes.mjs";
|
|
4
|
+
import { globVirtualFS } from "./glob.mjs";
|
|
4
5
|
import {
|
|
5
6
|
createFlagParser
|
|
6
7
|
} from "./flag-parser.mjs";
|
|
7
8
|
import { matchGlob } from "./match-glob.mjs";
|
|
8
9
|
export {
|
|
9
10
|
matchGlob,
|
|
11
|
+
globVirtualFS,
|
|
10
12
|
expandEscapes,
|
|
11
13
|
escapeForInterpolation,
|
|
12
14
|
escape,
|
|
13
15
|
createFlagParser
|
|
14
16
|
};
|
|
15
17
|
|
|
16
|
-
//# debugId=
|
|
18
|
+
//# debugId=7E1A8F2BD8DEF55F64756E2164756E21
|