Skip to content

Commit 0f9f042

Browse files
Jason3SCopilot
andauthored
fix: Add support for MemVfs to cspell-io (#8543)
Signed-off-by: Jason Dent <Jason3S@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 37e7092 commit 0f9f042

21 files changed

+413
-133
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { FileType } from '../models/Stats.js';
2+
3+
export class CFileType {
4+
constructor(readonly fileType: FileType) {}
5+
6+
isFile(): boolean {
7+
return this.fileType === FileType.File;
8+
}
9+
10+
isDirectory(): boolean {
11+
return this.fileType === FileType.Directory;
12+
}
13+
14+
isUnknown(): boolean {
15+
return !this.fileType;
16+
}
17+
18+
isSymbolicLink(): boolean {
19+
return !!(this.fileType & FileType.SymbolicLink);
20+
}
21+
}

packages/cspell-io/src/VirtualFS/CVFileSystem.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { describe, expect, test } from 'vitest';
22

3-
import { getDefaultVFileSystemCore } from '../CVirtualFS.js';
43
import { CVFileSystem } from './CVFileSystem.js';
4+
import { getDefaultVFileSystemCore } from './CVirtualFS.js';
55

66
describe('CVFileSystem', () => {
77
test('CVFileSystem', () => {

packages/cspell-io/src/VirtualFS/CVFileSystem.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import type { VFileSystem, VFileSystemCore, VFindUpPredicate, VFindUpURLOptions } from '../VFileSystem.js';
21
import { findUpFromUrl } from './findUpFromUrl.js';
2+
import type { VFileSystem, VFileSystemCore, VFindUpPredicate, VFindUpURLOptions } from './VFileSystem.js';
33

44
export class CVFileSystem implements VFileSystem {
55
#core: VFileSystemCore;
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { FileType, type Stats } from '../models/Stats.js';
2+
import { CFileType } from './CFileType.js';
3+
import type { VfsStat } from './VFileSystem.js';
4+
5+
export class CVfsStat extends CFileType implements VfsStat {
6+
constructor(private stat: Stats) {
7+
super(stat.fileType || FileType.Unknown);
8+
}
9+
10+
get size(): number {
11+
return this.stat.size;
12+
}
13+
14+
get mtimeMs(): number {
15+
return this.stat.mtimeMs;
16+
}
17+
18+
get eTag(): string | undefined {
19+
return this.stat.eTag;
20+
}
21+
}

packages/cspell-io/src/CVirtualFS.ts renamed to packages/cspell-io/src/VirtualFS/CVirtualFS.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
1-
import { urlOrReferenceToUrl } from './common/index.js';
2-
import type { CSpellIO } from './CSpellIO.js';
3-
import { getDefaultCSpellIO } from './CSpellIONode.js';
4-
import type { Disposable } from './models/index.js';
5-
import type { LogEvent } from './models/LogEvent.js';
1+
import { urlOrReferenceToUrl } from '../common/index.js';
2+
import type { CSpellIO } from '../CSpellIO.js';
3+
import { getDefaultCSpellIO } from '../CSpellIONode.js';
4+
import type { Disposable } from '../models/index.js';
5+
import type { LogEvent } from '../models/LogEvent.js';
6+
import { CVFileSystem } from './CVFileSystem.js';
7+
import { VFSErrorUnsupportedRequest } from './errors.js';
68
import type { UrlOrReference, VFileSystem, VFileSystemCore } from './VFileSystem.js';
79
import type { NextProvider, VFileSystemProvider, VirtualFS } from './VirtualFS.js';
810
import { debug } from './VirtualFS.js';
9-
import { CVFileSystem } from './VirtualFS/CVFileSystem.js';
1011
import {
11-
chopUrl,
12+
chopUrlAtNodeModules,
1213
cspellIOToFsProvider,
1314
CVfsDirEntry,
1415
rPad,
15-
VFSErrorUnsupportedRequest,
1616
WrappedProviderFs,
17-
} from './VirtualFS/WrappedProviderFs.js';
17+
} from './WrappedProviderFs.js';
1818

1919
class CVirtualFS implements VirtualFS {
2020
private readonly providers = new Set<VFileSystemProvider>();
@@ -39,7 +39,7 @@ class CVirtualFS implements VirtualFS {
3939
const id = event.traceID.toFixed(13).replaceAll(/\d{4}(?=\d)/g, '$&.');
4040
const msg = event.message ? `\n\t\t${event.message}` : '';
4141
const method = rPad(`${event.method}-${event.event}`, 16);
42-
this.log(`${method} ID:${id} ts:${event.ts.toFixed(13)} ${chopUrl(event.url)}${msg}`);
42+
this.log(`${method} ID:${id} ts:${event.ts.toFixed(13)} ${chopUrlAtNodeModules(event.url)}${msg}`);
4343
}
4444
};
4545

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { describe, expect, test } from 'vitest';
2+
3+
import type { FileResource } from '../models/FileResource.js';
4+
import { VFSNotFoundError, VFSNotSupported } from './errors.js';
5+
import { MemFileSystemProvider, MemVFileSystem } from './MemVfsProvider.js';
6+
7+
describe('MemFileSystemProvider', () => {
8+
test('should create a new MemFileSystemProvider', () => {
9+
using provider = new MemFileSystemProvider('test', 'cspell-vfs:');
10+
expect(provider).toBeInstanceOf(MemFileSystemProvider);
11+
});
12+
13+
test('should return the file system for the correct protocol', () => {
14+
const provider = new MemFileSystemProvider('test', 'cspell-vfs:');
15+
const fs = provider.getFileSystem(new URL('cspell-vfs://test/file.txt'));
16+
expect(fs).toBeInstanceOf(MemVFileSystem);
17+
expect(provider.memFS).toBeInstanceOf(MemVFileSystem);
18+
});
19+
20+
test('should return undefined for the wrong protocol', () => {
21+
const provider = new MemFileSystemProvider('test', 'cspell-vfs:');
22+
const fs = provider.getFileSystem(new URL('other-protocol://test/file.txt'));
23+
expect(fs).toBeUndefined();
24+
});
25+
});
26+
27+
describe('MemVFileSystem', () => {
28+
test('should create a new MemVFileSystem', () => {
29+
using memFS = new MemVFileSystem('test', 'cspell-vfs:');
30+
expect(memFS).toBeInstanceOf(MemVFileSystem);
31+
});
32+
test('should write and read a file', async () => {
33+
using memFS = new MemVFileSystem('test', 'cspell-vfs:');
34+
const file: FileResource = {
35+
url: new URL('cspell-vfs://test/file.txt'),
36+
content: new Uint8Array([1, 2, 3]),
37+
};
38+
const now = performance.now();
39+
await memFS.writeFile(file);
40+
const readFile = await memFS.readFile(file.url);
41+
expect(readFile).toEqual(file);
42+
const stats = memFS.stat(file);
43+
expect(stats.size).toEqual(file.content.length);
44+
expect(stats.mtimeMs).toBeGreaterThanOrEqual(now);
45+
});
46+
47+
test('should throw an error when reading a non-existent file', async () => {
48+
using memFS = new MemVFileSystem('test', 'cspell-vfs:');
49+
await expect(memFS.readFile(new URL('cspell-vfs://test/nonexistent.txt'))).rejects.toThrow(VFSNotFoundError);
50+
});
51+
52+
test('readDirectory should throw an error', async () => {
53+
using memFS = new MemVFileSystem('test', 'cspell-vfs:');
54+
await expect(memFS.readDirectory(new URL('cspell-vfs://test/'))).rejects.toThrowError(VFSNotSupported);
55+
});
56+
});
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { urlOrReferenceToUrl } from '../common/index.js';
2+
import type { Disposable } from '../models/index.js';
3+
import { type FileReference, type FileResource, FileType, type Stats } from '../models/index.js';
4+
import { VFSNotFoundError, VFSNotSupported } from './errors.js';
5+
import type { FileSystemProviderInfo, UrlOrReference, VfsDirEntry } from './VFileSystem.js';
6+
import { FSCapabilityFlags } from './VFileSystem.js';
7+
import type { VFileSystemProvider, VProviderFileSystem, VProviderFileSystemReadFileOptions } from './VirtualFS.js';
8+
9+
export class MemFileSystemProvider implements VFileSystemProvider, Disposable {
10+
readonly name: string;
11+
readonly protocol: string;
12+
#vfs: MemVFileSystem;
13+
14+
/**
15+
* @param name - the name of the provider, used for debugging and logging.
16+
* @param protocol - the protocol (end with a :), examples: `vfs:`, `cspell-vfs:`
17+
*/
18+
constructor(name: string, protocol: string) {
19+
this.name = name;
20+
this.protocol = protocol;
21+
this.#vfs = new MemVFileSystem(name, protocol);
22+
}
23+
24+
getFileSystem(url: URL): VProviderFileSystem | undefined {
25+
if (url.protocol !== this.protocol) {
26+
return undefined;
27+
}
28+
return this.#vfs;
29+
}
30+
31+
get memFS(): MemVFileSystem {
32+
return this.#vfs;
33+
}
34+
35+
dispose(): void {
36+
this.#vfs.dispose();
37+
}
38+
39+
[Symbol.dispose](): void {
40+
this.dispose();
41+
}
42+
}
43+
44+
interface MemVFileSystemEntry {
45+
file: FileResource;
46+
stats: Stats;
47+
}
48+
49+
export class MemVFileSystem implements VProviderFileSystem {
50+
readonly name: string;
51+
readonly protocol: string;
52+
readonly capabilities: FSCapabilityFlags = FSCapabilityFlags.ReadWrite | FSCapabilityFlags.Stat;
53+
#files: Map<string, MemVFileSystemEntry> = new Map();
54+
55+
constructor(name: string, protocol: string) {
56+
this.name = name;
57+
this.protocol = protocol;
58+
this.providerInfo = { name };
59+
this.#files = new Map();
60+
}
61+
62+
/**
63+
* Read a file.
64+
* @param url - URL to read
65+
* @param options - options for reading the file.
66+
* @returns A FileResource, the content will not be decoded. Use `.getText()` to get the decoded text.
67+
*/
68+
async readFile(url: UrlOrReference, _options?: VProviderFileSystemReadFileOptions): Promise<FileResource> {
69+
return this.#getEntryOrThrow(url).file;
70+
}
71+
72+
/**
73+
* Write a file
74+
* @param file - the file to write
75+
*/
76+
async writeFile(file: FileResource): Promise<FileReference> {
77+
const stats: Stats = {
78+
size: file.content.length,
79+
mtimeMs: performance.now(),
80+
fileType: FileType.File,
81+
};
82+
const u = urlOrReferenceToUrl(file);
83+
this.#files.set(u.href, { file, stats });
84+
return { url: file.url };
85+
}
86+
87+
/**
88+
* Get the stats for a url.
89+
* @param url - Url to fetch stats for.
90+
*/
91+
stat(url: UrlOrReference): Stats {
92+
return this.#getEntryOrThrow(url).stats;
93+
}
94+
95+
#getEntryOrThrow(url: UrlOrReference): MemVFileSystemEntry {
96+
const u = urlOrReferenceToUrl(url);
97+
const found = this.#files.get(u.href);
98+
if (!found) {
99+
throw new VFSNotFoundError(u);
100+
}
101+
return found;
102+
}
103+
104+
/**
105+
* Read the directory entries for a url.
106+
* The url should end with `/` to indicate a directory.
107+
* @param url - the url to read the directory entries for.
108+
*/
109+
async readDirectory(url: URL): Promise<VfsDirEntry[]> {
110+
throw new VFSNotSupported('readDirectory', url);
111+
}
112+
113+
/**
114+
* Information about the provider.
115+
* It is up to the provider to define what information is available.
116+
*/
117+
readonly providerInfo: FileSystemProviderInfo;
118+
readonly hasProvider: boolean = true;
119+
120+
dispose(): void {
121+
this.#files.clear();
122+
}
123+
124+
[Symbol.dispose](): void {
125+
this.dispose();
126+
}
127+
}

packages/cspell-io/src/VFileSystem.ts renamed to packages/cspell-io/src/VirtualFS/VFileSystem.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import type { BufferEncoding, DirEntry, FileReference, FileResource, Stats, TextFileResource } from './models/index.js';
1+
import type {
2+
BufferEncoding,
3+
DirEntry,
4+
FileReference,
5+
FileResource,
6+
Stats,
7+
TextFileResource,
8+
} from '../models/index.js';
29

310
export type UrlOrReference = URL | FileReference;
411

@@ -58,12 +65,12 @@ export interface VFileSystemCore {
5865
* The capabilities can be more restrictive than the general capabilities of the provider.
5966
* @param url - the url to try
6067
*/
61-
getCapabilities(url: URL): FSCapabilities;
68+
getCapabilities(url: URL): Readonly<FSCapabilities>;
6269
/**
6370
* Information about the provider.
6471
* It is up to the provider to define what information is available.
6572
*/
66-
providerInfo: FileSystemProviderInfo;
73+
providerInfo: Readonly<FileSystemProviderInfo>;
6774
/**
6875
* Indicates that a provider was found for the url.
6976
*/

packages/cspell-io/src/VirtualFS.ts renamed to packages/cspell-io/src/VirtualFS/VirtualFS.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { DirEntry, Disposable, FileReference, FileResource, Stats } from './models/index.js';
1+
import type { DirEntry, Disposable, FileReference, FileResource, Stats } from '../models/index.js';
22
import type {
33
FileSystemProviderInfo,
44
FSCapabilities,
@@ -67,7 +67,7 @@ export interface VProviderFileSystem extends Disposable {
6767
capabilities: FSCapabilityFlags;
6868

6969
/**
70-
* Get the capabilities for a URL. Make it possible for a provider to support more capabilities for a given url.
70+
* Get the capabilities for a URL. Makes it possible for a provider to support more capabilities for a given url.
7171
* These capabilities should be more restrictive than the general capabilities.
7272
* @param url - the url to try
7373
* @returns the capabilities for the url.

packages/cspell-io/src/VirtualFs.test.ts renamed to packages/cspell-io/src/VirtualFS/VirtualFs.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@ import { basename } from 'node:path';
33

44
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
55

6-
import { CFileResource } from './common/index.js';
6+
import { CFileResource } from '../common/index.js';
7+
import { FileType } from '../models/Stats.js';
8+
import { toFileURL, urlBasename } from '../node/file/url.js';
9+
import { pathToSample as ps } from '../test/test.helper.js';
710
import { createVirtualFS, getDefaultVFileSystem } from './CVirtualFS.js';
8-
import { FileType } from './models/Stats.js';
9-
import { toFileURL, urlBasename } from './node/file/url.js';
10-
import { pathToSample as ps } from './test/test.helper.js';
11+
import { VFSErrorUnsupportedRequest } from './errors.js';
1112
import { FSCapabilityFlags } from './VFileSystem.js';
1213
import type { VFileSystemProvider, VirtualFS, VProviderFileSystem } from './VirtualFS.js';
13-
import { VFSErrorUnsupportedRequest } from './VirtualFS/WrappedProviderFs.js';
1414

1515
const sc = (m: string) => expect.stringContaining(m);
1616
const oc = (...params: Parameters<typeof expect.objectContaining>) => expect.objectContaining(...params);

0 commit comments

Comments
 (0)