Skip to content

Commit b5c9115

Browse files
authored
feat: Support Windows UNC files. (#6671)
1 parent 2ed706e commit b5c9115

21 files changed

Lines changed: 215 additions & 95 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"cspell": "bin.mjs",
1010
"cspell-tools": "cspell-tools.mjs"
1111
},
12-
"packageManager": "pnpm@9.4.0+sha512.f549b8a52c9d2b8536762f99c0722205efc5af913e77835dbccc3b0b0b2ca9e7dc8022b78062c17291c48e88749c70ce88eb5a74f1fa8c4bf5e18bb46c8bd83a",
12+
"packageManager": "pnpm@9.15.0+sha512.76e2379760a4328ec4415815bcd6628dee727af3779aaa4c914e3944156c4299921a89f976381ee107d41f12cfa4b66681ca9c718f0668fa0831ed4c6d8ba56c",
1313
"private": true,
1414
"scripts": {
1515
"bt": "pnpm run build && pnpm run test",

packages/cspell-lib/src/lib/Settings/Controller/configLoader/configLoader.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import assert from 'node:assert';
22
import path from 'node:path';
3-
import { fileURLToPath, pathToFileURL } from 'node:url';
3+
import { fileURLToPath } from 'node:url';
44

55
import type { CSpellUserSettings, ImportFileRef, Source } from '@cspell/cspell-types';
66
import { CSpellConfigFile, CSpellConfigFileReaderWriter, ICSpellConfigFile, IO, TextFile } from 'cspell-config-lib';
@@ -21,6 +21,7 @@ import {
2121
addTrailingSlash,
2222
cwdURL,
2323
resolveFileWithURL,
24+
toFileDirURL,
2425
toFilePathOrHref,
2526
toFileUrl,
2627
windowsDriveLetterToUpper,
@@ -224,7 +225,7 @@ export class ConfigLoader implements IConfigLoader {
224225
pnpSettings?: PnPSettingsOptional,
225226
): Promise<CSpellSettingsI> {
226227
await this.onReady;
227-
const ref = await this.resolveFilename(filename, relativeTo || pathToFileURL('./'));
228+
const ref = await this.resolveFilename(filename, relativeTo || toFileDirURL('./'));
228229
const entry = this.importSettings(ref, pnpSettings || defaultPnPSettings, []);
229230
return entry.onReady;
230231
}
@@ -233,7 +234,7 @@ export class ConfigLoader implements IConfigLoader {
233234
filenameOrURL: string | URL,
234235
relativeTo?: string | URL,
235236
): Promise<CSpellConfigFile | Error> {
236-
const ref = await this.resolveFilename(filenameOrURL.toString(), relativeTo || pathToFileURL('./'));
237+
const ref = await this.resolveFilename(filenameOrURL.toString(), relativeTo || toFileDirURL('./'));
237238
const url = toFileURL(ref.filename);
238239
const href = url.href;
239240
if (ref.error) return new ImportError(`Failed to read config file: "${ref.filename}"`, ref.error);

packages/cspell-lib/src/lib/Settings/GlobalSettings.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
import { pathToFileURL } from 'node:url';
2-
31
import type { CSpellSettings, CSpellSettingsWithSourceTrace } from '@cspell/cspell-types';
42
import type { CSpellConfigFile } from 'cspell-config-lib';
53
import { CSpellConfigFileInMemory, CSpellConfigFileJson } from 'cspell-config-lib';
4+
import { toFileURL } from 'cspell-io';
65

76
import { getSourceDirectoryUrl, toFilePathOrHref } from '../util/url.js';
87
import { GlobalConfigStore } from './cfgStore.js';
@@ -24,7 +23,7 @@ export async function getRawGlobalSettings(): Promise<CSpellSettingsWST> {
2423
export async function getGlobalConfig(): Promise<CSpellConfigFile> {
2524
const name = 'CSpell Configstore';
2625
const configPath = getGlobalConfigPath();
27-
let urlGlobal = configPath ? pathToFileURL(configPath) : new URL('global-config.json', getSourceDirectoryUrl());
26+
let urlGlobal = configPath ? toFileURL(configPath) : new URL('global-config.json', getSourceDirectoryUrl());
2827

2928
const source: CSpellSettingsWST['source'] = {
3029
name,
@@ -39,7 +38,7 @@ export async function getGlobalConfig(): Promise<CSpellConfigFile> {
3938

4039
if (found && found.config && found.filename) {
4140
const cfg = found.config;
42-
urlGlobal = pathToFileURL(found.filename);
41+
urlGlobal = toFileURL(found.filename);
4342

4443
// Only populate globalConf is there are values.
4544
if (cfg && Object.keys(cfg).length) {
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { pathToFileURL } from 'node:url';
1+
import { toFileDirURL, toFileURL } from '@cspell/url';
22

33
export class CwdUrlResolver {
44
#lastPath: string;
@@ -8,7 +8,7 @@ export class CwdUrlResolver {
88

99
constructor() {
1010
this.#cwd = process.cwd();
11-
this.#cwdUrl = pathToFileURL(this.#cwd);
11+
this.#cwdUrl = toFileDirURL(this.#cwd);
1212
this.#lastPath = this.#cwd;
1313
this.#lastUrl = this.#cwdUrl;
1414
}
@@ -17,12 +17,12 @@ export class CwdUrlResolver {
1717
if (path === this.#lastPath) return this.#lastUrl;
1818
if (path === this.#cwd) return this.#cwdUrl;
1919
this.#lastPath = path;
20-
this.#lastUrl = pathToFileURL(path);
20+
this.#lastUrl = toFileURL(path);
2121
return this.#lastUrl;
2222
}
2323

2424
reset(cwd: string = process.cwd()) {
2525
this.#cwd = cwd;
26-
this.#cwdUrl = pathToFileURL(this.#cwd);
26+
this.#cwdUrl = toFileDirURL(this.#cwd);
2727
}
2828
}

packages/cspell-lib/src/lib/textValidation/docValidator.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import assert from 'node:assert';
2-
import { pathToFileURL } from 'node:url';
32

43
import { opConcatMap, opMap, pipeSync } from '@cspell/cspell-pipe/sync';
54
import type {
@@ -140,7 +139,7 @@ export class DocumentValidator {
140139
const { options, settings: rawSettings } = this;
141140

142141
const resolveImportsRelativeTo = toFileURL(
143-
options.resolveImportsRelativeTo || pathToFileURL('./virtual.settings.json'),
142+
options.resolveImportsRelativeTo || toFileURL('./virtual.settings.json'),
144143
);
145144

146145
const settings = rawSettings.import?.length

packages/cspell-lib/src/lib/util/Uri.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import assert from 'node:assert';
2-
import { pathToFileURL } from 'node:url';
32

4-
import { toFilePathOrHref, toFileURL, toURL } from '@cspell/url';
3+
import { toFileDirURL, toFilePathOrHref, toFileURL, toURL } from '@cspell/url';
54
import { isUrlLike } from 'cspell-io';
65
import { URI, Utils } from 'vscode-uri';
76

@@ -26,7 +25,7 @@ export function toUri(uriOrFile: string | Uri | URL): UriInstance {
2625
const isWindows = process.platform === 'win32';
2726
const hasDriveLetter = /^[a-zA-Z]:[\\/]/;
2827

29-
const rootUrl = pathToFileURL('/');
28+
const rootUrl = toFileDirURL('/');
3029

3130
export function uriToFilePath(uri: DocumentUri): string {
3231
let url = documentUriToURL(uri);

packages/cspell-lib/src/lib/util/resolveFile.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { createRequire } from 'node:module';
22
import * as os from 'node:os';
33
import * as path from 'node:path';
4-
import { pathToFileURL } from 'node:url';
54
import { fileURLToPath } from 'node:url';
65

76
import { resolveGlobal } from '@cspell/cspell-resolver';
@@ -18,6 +17,7 @@ import {
1817
isFileURL,
1918
isURLLike,
2019
resolveFileWithURL,
20+
toFileDirURL,
2121
toFilePathOrHref,
2222
toFileUrl,
2323
toURL,
@@ -42,6 +42,8 @@ export interface ResolveFileResult {
4242

4343
const regExpStartsWidthNodeModules = /^node_modules[/\\]/;
4444

45+
const debugMode = false;
46+
4547
export class FileResolver {
4648
constructor(
4749
private fs: VFileSystem,
@@ -174,12 +176,15 @@ export class FileResolver {
174176

175177
tryCreateRequire = (filename: string | URL, relativeTo: string | URL): ResolveFileResult | undefined => {
176178
if (filename instanceof URL) return undefined;
177-
const rel = !isURLLike(relativeTo) || isFileURL(relativeTo) ? relativeTo : pathToFileURL('./');
178-
const require = createRequire(rel);
179+
const rel = !isURLLike(relativeTo) || isFileURL(relativeTo) ? relativeTo : toFileDirURL('./');
179180
try {
181+
const require = createRequire(rel);
180182
const r = require.resolve(filename);
181183
return { filename: r, relativeTo: rel.toString(), found: true, method: 'tryCreateRequire' };
182-
} catch {
184+
} catch (error) {
185+
if (debugMode) {
186+
console.error('Error in tryCreateRequire: %o', { filename, rel, relativeTo, error: `${error}` });
187+
}
183188
return undefined;
184189
}
185190
};

packages/cspell-lib/src/lib/util/url.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
import path from 'node:path';
2-
import { pathToFileURL } from 'node:url';
3-
4-
import { toFilePathOrHref, toFileURL } from '@cspell/url';
1+
import { toFileDirURL, toFilePathOrHref, toFileURL } from '@cspell/url';
52

63
import { srcDirectory } from '../pkg-info.mjs';
74

@@ -21,7 +18,7 @@ export {
2118
* @returns URL for the source directory
2219
*/
2320
export function getSourceDirectoryUrl(): URL {
24-
const srcDirectoryURL = pathToFileURL(path.join(srcDirectory, '/'));
21+
const srcDirectoryURL = toFileDirURL(srcDirectory);
2522
return srcDirectoryURL;
2623
}
2724

@@ -35,7 +32,7 @@ export function relativeTo(path: string, relativeTo?: URL | string): URL {
3532
}
3633

3734
export function cwdURL(): URL {
38-
return pathToFileURL('./');
35+
return toFileDirURL('./');
3936
}
4037

4138
export function toFileUrl(file: string | URL): URL {

packages/cspell-url/src/FileUrlBuilder.mts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,15 @@ import assert from 'node:assert';
22
import Path from 'node:path';
33
import { pathToFileURL } from 'node:url';
44

5-
import { pathWindowsDriveLetterToUpper, regExpWindowsPathDriveLetter, toFilePathOrHref } from './fileUrl.mjs';
5+
import {
6+
isFileURL,
7+
isWindows,
8+
isWindowsFileUrl,
9+
isWindowsPathnameWithDriveLatter,
10+
pathWindowsDriveLetterToUpper,
11+
regExpWindowsPathDriveLetter,
12+
toFilePathOrHref,
13+
} from './fileUrl.mjs';
614
import {
715
addTrailingSlash,
816
isUrlLike,
@@ -12,8 +20,6 @@ import {
1220
urlToUrlRelative,
1321
} from './url.mjs';
1422

15-
export const isWindows = process.platform === 'win32';
16-
1723
const isWindowsPathRegEx = regExpWindowsPathDriveLetter;
1824
const isWindowsPathname = regExpWindowsPath;
1925

@@ -127,18 +133,26 @@ export class FileUrlBuilder {
127133
*/
128134
#toFileURL(filenameOrUrl: string | URL, relativeTo?: string | URL): URL {
129135
if (typeof filenameOrUrl !== 'string') return filenameOrUrl;
130-
if (isUrlLike(filenameOrUrl)) return new URL(filenameOrUrl);
136+
if (isUrlLike(filenameOrUrl)) return normalizeWindowsUrl(new URL(filenameOrUrl));
131137
relativeTo ??= this.cwd;
132138
isWindows && (filenameOrUrl = filenameOrUrl.replaceAll('\\', '/'));
139+
if (this.isAbsolute(filenameOrUrl) && isFileURL(relativeTo)) {
140+
const pathname = this.normalizeFilePathForUrl(filenameOrUrl);
141+
if (isWindowsFileUrl(relativeTo) && !isWindowsPathnameWithDriveLatter(pathname)) {
142+
const relFilePrefix = relativeTo.toString().slice(0, 10);
143+
return normalizeWindowsUrl(new URL(relFilePrefix + pathname));
144+
}
145+
return normalizeWindowsUrl(new URL('file://' + pathname));
146+
}
133147
if (isUrlLike(relativeTo)) {
134148
const pathname = this.normalizeFilePathForUrl(filenameOrUrl);
135-
return new URL(pathname, relativeTo);
149+
return normalizeWindowsUrl(new URL(pathname, relativeTo));
136150
}
137151
// Resolve removes the trailing slash, so we need to add it back.
138152
const appendSlash = filenameOrUrl.endsWith('/') ? '/' : '';
139153
const pathname =
140154
this.normalizeFilePathForUrl(this.path.resolve(relativeTo.toString(), filenameOrUrl)) + appendSlash;
141-
return this.pathToFileURL(pathname, this.cwd);
155+
return normalizeWindowsUrl(new URL('file://' + pathname));
142156
}
143157

144158
/**
@@ -158,7 +172,7 @@ export class FileUrlBuilder {
158172
}
159173

160174
#urlToFilePathOrHref(url: URL): string {
161-
if (url.protocol !== ProtocolFile) return url.href;
175+
if (url.protocol !== ProtocolFile || url.hostname) return url.href;
162176
const p =
163177
this.path === Path
164178
? toFilePathOrHref(url)

packages/cspell-url/src/FileUrlBuilder.test.mts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import Path from 'node:path';
2-
import url from 'node:url';
2+
import url, { fileURLToPath, pathToFileURL } from 'node:url';
33

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

@@ -27,6 +27,21 @@ describe('FileUrlBuilder', () => {
2727
expect(result).toEqual(expected);
2828
});
2929

30+
test.each`
31+
file | relativeTo | path | expected
32+
${'.'} | ${undefined} | ${undefined} | ${pathToFileURL('./').href}
33+
${'README.md'} | ${process.cwd()} | ${undefined} | ${pathToFileURL('README.md').href}
34+
${import.meta.url} | ${process.cwd()} | ${Path.win32} | ${import.meta.url}
35+
${'deeper/'} | ${'file:///E:/user/Test/project/'} | ${Path.win32} | ${'file:///E:/user/Test/project/deeper/'}
36+
${'file://host/E$/user/test/project/'} | ${undefined} | ${Path.win32} | ${'file://host/E$/user/test/project/'}
37+
${'../sibling'} | ${'file://host/E$/user/test/project/'} | ${Path.win32} | ${'file://host/E$/user/test/sibling'}
38+
${fileURLToPath(import.meta.url)} | ${'file://host/E$/user/test/project/'} | ${Path.win32} | ${import.meta.url}
39+
`('toFileURL $file $relativeTo', ({ file, relativeTo, path, expected }) => {
40+
const builder = new FileUrlBuilder({ path });
41+
const url = builder.toFileURL(file, relativeTo);
42+
expect(url.href).toBe(expected);
43+
});
44+
3045
test.each`
3146
filePath | path | expected
3247
${'.'} | ${undefined} | ${false}

0 commit comments

Comments
 (0)