Skip to content

Commit

Permalink
feat: Support Windows UNC files. (#6671)
Browse files Browse the repository at this point in the history
  • Loading branch information
Jason3S authored Dec 15, 2024
1 parent 2ed706e commit b5c9115
Show file tree
Hide file tree
Showing 21 changed files with 215 additions and 95 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"cspell": "bin.mjs",
"cspell-tools": "cspell-tools.mjs"
},
"packageManager": "pnpm@9.4.0+sha512.f549b8a52c9d2b8536762f99c0722205efc5af913e77835dbccc3b0b0b2ca9e7dc8022b78062c17291c48e88749c70ce88eb5a74f1fa8c4bf5e18bb46c8bd83a",
"packageManager": "pnpm@9.15.0+sha512.76e2379760a4328ec4415815bcd6628dee727af3779aaa4c914e3944156c4299921a89f976381ee107d41f12cfa4b66681ca9c718f0668fa0831ed4c6d8ba56c",
"private": true,
"scripts": {
"bt": "pnpm run build && pnpm run test",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import assert from 'node:assert';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { fileURLToPath } from 'node:url';

import type { CSpellUserSettings, ImportFileRef, Source } from '@cspell/cspell-types';
import { CSpellConfigFile, CSpellConfigFileReaderWriter, ICSpellConfigFile, IO, TextFile } from 'cspell-config-lib';
Expand All @@ -21,6 +21,7 @@ import {
addTrailingSlash,
cwdURL,
resolveFileWithURL,
toFileDirURL,
toFilePathOrHref,
toFileUrl,
windowsDriveLetterToUpper,
Expand Down Expand Up @@ -224,7 +225,7 @@ export class ConfigLoader implements IConfigLoader {
pnpSettings?: PnPSettingsOptional,
): Promise<CSpellSettingsI> {
await this.onReady;
const ref = await this.resolveFilename(filename, relativeTo || pathToFileURL('./'));
const ref = await this.resolveFilename(filename, relativeTo || toFileDirURL('./'));
const entry = this.importSettings(ref, pnpSettings || defaultPnPSettings, []);
return entry.onReady;
}
Expand All @@ -233,7 +234,7 @@ export class ConfigLoader implements IConfigLoader {
filenameOrURL: string | URL,
relativeTo?: string | URL,
): Promise<CSpellConfigFile | Error> {
const ref = await this.resolveFilename(filenameOrURL.toString(), relativeTo || pathToFileURL('./'));
const ref = await this.resolveFilename(filenameOrURL.toString(), relativeTo || toFileDirURL('./'));
const url = toFileURL(ref.filename);
const href = url.href;
if (ref.error) return new ImportError(`Failed to read config file: "${ref.filename}"`, ref.error);
Expand Down
7 changes: 3 additions & 4 deletions packages/cspell-lib/src/lib/Settings/GlobalSettings.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { pathToFileURL } from 'node:url';

import type { CSpellSettings, CSpellSettingsWithSourceTrace } from '@cspell/cspell-types';
import type { CSpellConfigFile } from 'cspell-config-lib';
import { CSpellConfigFileInMemory, CSpellConfigFileJson } from 'cspell-config-lib';
import { toFileURL } from 'cspell-io';

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

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

if (found && found.config && found.filename) {
const cfg = found.config;
urlGlobal = pathToFileURL(found.filename);
urlGlobal = toFileURL(found.filename);

// Only populate globalConf is there are values.
if (cfg && Object.keys(cfg).length) {
Expand Down
8 changes: 4 additions & 4 deletions packages/cspell-lib/src/lib/Settings/resolveCwd.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { pathToFileURL } from 'node:url';
import { toFileDirURL, toFileURL } from '@cspell/url';

export class CwdUrlResolver {
#lastPath: string;
Expand All @@ -8,7 +8,7 @@ export class CwdUrlResolver {

constructor() {
this.#cwd = process.cwd();
this.#cwdUrl = pathToFileURL(this.#cwd);
this.#cwdUrl = toFileDirURL(this.#cwd);
this.#lastPath = this.#cwd;
this.#lastUrl = this.#cwdUrl;
}
Expand All @@ -17,12 +17,12 @@ export class CwdUrlResolver {
if (path === this.#lastPath) return this.#lastUrl;
if (path === this.#cwd) return this.#cwdUrl;
this.#lastPath = path;
this.#lastUrl = pathToFileURL(path);
this.#lastUrl = toFileURL(path);
return this.#lastUrl;
}

reset(cwd: string = process.cwd()) {
this.#cwd = cwd;
this.#cwdUrl = pathToFileURL(this.#cwd);
this.#cwdUrl = toFileDirURL(this.#cwd);
}
}
3 changes: 1 addition & 2 deletions packages/cspell-lib/src/lib/textValidation/docValidator.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import assert from 'node:assert';
import { pathToFileURL } from 'node:url';

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

const resolveImportsRelativeTo = toFileURL(
options.resolveImportsRelativeTo || pathToFileURL('./virtual.settings.json'),
options.resolveImportsRelativeTo || toFileURL('./virtual.settings.json'),
);

const settings = rawSettings.import?.length
Expand Down
5 changes: 2 additions & 3 deletions packages/cspell-lib/src/lib/util/Uri.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import assert from 'node:assert';
import { pathToFileURL } from 'node:url';

import { toFilePathOrHref, toFileURL, toURL } from '@cspell/url';
import { toFileDirURL, toFilePathOrHref, toFileURL, toURL } from '@cspell/url';
import { isUrlLike } from 'cspell-io';
import { URI, Utils } from 'vscode-uri';

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

const rootUrl = pathToFileURL('/');
const rootUrl = toFileDirURL('/');

export function uriToFilePath(uri: DocumentUri): string {
let url = documentUriToURL(uri);
Expand Down
13 changes: 9 additions & 4 deletions packages/cspell-lib/src/lib/util/resolveFile.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { createRequire } from 'node:module';
import * as os from 'node:os';
import * as path from 'node:path';
import { pathToFileURL } from 'node:url';
import { fileURLToPath } from 'node:url';

import { resolveGlobal } from '@cspell/cspell-resolver';
Expand All @@ -18,6 +17,7 @@ import {
isFileURL,
isURLLike,
resolveFileWithURL,
toFileDirURL,
toFilePathOrHref,
toFileUrl,
toURL,
Expand All @@ -42,6 +42,8 @@ export interface ResolveFileResult {

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

const debugMode = false;

export class FileResolver {
constructor(
private fs: VFileSystem,
Expand Down Expand Up @@ -174,12 +176,15 @@ export class FileResolver {

tryCreateRequire = (filename: string | URL, relativeTo: string | URL): ResolveFileResult | undefined => {
if (filename instanceof URL) return undefined;
const rel = !isURLLike(relativeTo) || isFileURL(relativeTo) ? relativeTo : pathToFileURL('./');
const require = createRequire(rel);
const rel = !isURLLike(relativeTo) || isFileURL(relativeTo) ? relativeTo : toFileDirURL('./');
try {
const require = createRequire(rel);
const r = require.resolve(filename);
return { filename: r, relativeTo: rel.toString(), found: true, method: 'tryCreateRequire' };
} catch {
} catch (error) {
if (debugMode) {
console.error('Error in tryCreateRequire: %o', { filename, rel, relativeTo, error: `${error}` });
}
return undefined;
}
};
Expand Down
9 changes: 3 additions & 6 deletions packages/cspell-lib/src/lib/util/url.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import path from 'node:path';
import { pathToFileURL } from 'node:url';

import { toFilePathOrHref, toFileURL } from '@cspell/url';
import { toFileDirURL, toFilePathOrHref, toFileURL } from '@cspell/url';

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

Expand All @@ -21,7 +18,7 @@ export {
* @returns URL for the source directory
*/
export function getSourceDirectoryUrl(): URL {
const srcDirectoryURL = pathToFileURL(path.join(srcDirectory, '/'));
const srcDirectoryURL = toFileDirURL(srcDirectory);
return srcDirectoryURL;
}

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

export function cwdURL(): URL {
return pathToFileURL('./');
return toFileDirURL('./');
}

export function toFileUrl(file: string | URL): URL {
Expand Down
28 changes: 21 additions & 7 deletions packages/cspell-url/src/FileUrlBuilder.mts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@ import assert from 'node:assert';
import Path from 'node:path';
import { pathToFileURL } from 'node:url';

import { pathWindowsDriveLetterToUpper, regExpWindowsPathDriveLetter, toFilePathOrHref } from './fileUrl.mjs';
import {
isFileURL,
isWindows,
isWindowsFileUrl,
isWindowsPathnameWithDriveLatter,
pathWindowsDriveLetterToUpper,
regExpWindowsPathDriveLetter,
toFilePathOrHref,
} from './fileUrl.mjs';
import {
addTrailingSlash,
isUrlLike,
Expand All @@ -12,8 +20,6 @@ import {
urlToUrlRelative,
} from './url.mjs';

export const isWindows = process.platform === 'win32';

const isWindowsPathRegEx = regExpWindowsPathDriveLetter;
const isWindowsPathname = regExpWindowsPath;

Expand Down Expand Up @@ -127,18 +133,26 @@ export class FileUrlBuilder {
*/
#toFileURL(filenameOrUrl: string | URL, relativeTo?: string | URL): URL {
if (typeof filenameOrUrl !== 'string') return filenameOrUrl;
if (isUrlLike(filenameOrUrl)) return new URL(filenameOrUrl);
if (isUrlLike(filenameOrUrl)) return normalizeWindowsUrl(new URL(filenameOrUrl));
relativeTo ??= this.cwd;
isWindows && (filenameOrUrl = filenameOrUrl.replaceAll('\\', '/'));
if (this.isAbsolute(filenameOrUrl) && isFileURL(relativeTo)) {
const pathname = this.normalizeFilePathForUrl(filenameOrUrl);
if (isWindowsFileUrl(relativeTo) && !isWindowsPathnameWithDriveLatter(pathname)) {
const relFilePrefix = relativeTo.toString().slice(0, 10);
return normalizeWindowsUrl(new URL(relFilePrefix + pathname));
}
return normalizeWindowsUrl(new URL('file://' + pathname));
}
if (isUrlLike(relativeTo)) {
const pathname = this.normalizeFilePathForUrl(filenameOrUrl);
return new URL(pathname, relativeTo);
return normalizeWindowsUrl(new URL(pathname, relativeTo));
}
// Resolve removes the trailing slash, so we need to add it back.
const appendSlash = filenameOrUrl.endsWith('/') ? '/' : '';
const pathname =
this.normalizeFilePathForUrl(this.path.resolve(relativeTo.toString(), filenameOrUrl)) + appendSlash;
return this.pathToFileURL(pathname, this.cwd);
return normalizeWindowsUrl(new URL('file://' + pathname));
}

/**
Expand All @@ -158,7 +172,7 @@ export class FileUrlBuilder {
}

#urlToFilePathOrHref(url: URL): string {
if (url.protocol !== ProtocolFile) return url.href;
if (url.protocol !== ProtocolFile || url.hostname) return url.href;
const p =
this.path === Path
? toFilePathOrHref(url)
Expand Down
17 changes: 16 additions & 1 deletion packages/cspell-url/src/FileUrlBuilder.test.mts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Path from 'node:path';
import url from 'node:url';
import url, { fileURLToPath, pathToFileURL } from 'node:url';

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

Expand Down Expand Up @@ -27,6 +27,21 @@ describe('FileUrlBuilder', () => {
expect(result).toEqual(expected);
});

test.each`
file | relativeTo | path | expected
${'.'} | ${undefined} | ${undefined} | ${pathToFileURL('./').href}
${'README.md'} | ${process.cwd()} | ${undefined} | ${pathToFileURL('README.md').href}
${import.meta.url} | ${process.cwd()} | ${Path.win32} | ${import.meta.url}
${'deeper/'} | ${'file:///E:/user/Test/project/'} | ${Path.win32} | ${'file:///E:/user/Test/project/deeper/'}
${'file://host/E$/user/test/project/'} | ${undefined} | ${Path.win32} | ${'file://host/E$/user/test/project/'}
${'../sibling'} | ${'file://host/E$/user/test/project/'} | ${Path.win32} | ${'file://host/E$/user/test/sibling'}
${fileURLToPath(import.meta.url)} | ${'file://host/E$/user/test/project/'} | ${Path.win32} | ${import.meta.url}
`('toFileURL $file $relativeTo', ({ file, relativeTo, path, expected }) => {
const builder = new FileUrlBuilder({ path });
const url = builder.toFileURL(file, relativeTo);
expect(url.href).toBe(expected);
});

test.each`
filePath | path | expected
${'.'} | ${undefined} | ${false}
Expand Down
43 changes: 40 additions & 3 deletions packages/cspell-url/src/fileUrl.mts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import { fileURLToPath } from 'node:url';
import { fileURLToPath, pathToFileURL } from 'node:url';

import { hasProtocol } from './url.mjs';

export const isWindows = process.platform === 'win32';

const windowsUrlPathRegExp = /^\/[a-zA-Z]:\//;

export function isWindowsPathnameWithDriveLatter(pathname: string): boolean {
return windowsUrlPathRegExp.test(pathname);
}

/**
* @param url - URL or string to check if it is a file URL.
* @returns true if the URL is a file URL.
Expand All @@ -16,15 +24,44 @@ export function isFileURL(url: URL | string): boolean {
* @returns path or href
*/
export function toFilePathOrHref(url: URL | string): string {
return isFileURL(url) ? toFilePath(url) : url.toString();
return isFileURL(url) && url.toString().startsWith('file:///') ? toFilePath(url) : url.toString();
}

function toFilePath(url: string | URL): string {
return pathWindowsDriveLetterToUpper(fileURLToPath(url));
try {
// Fix drive letter if necessary.
if (isWindows) {
const u = new URL(url);
if (!isWindowsPathnameWithDriveLatter(u.pathname)) {
const cwdUrl = pathToFileURL(process.cwd());
if (cwdUrl.hostname) {
return fileURLToPath(new URL(u.pathname, cwdUrl));
}
const drive = cwdUrl.pathname.split('/')[1];
u.pathname = `/${drive}${u.pathname}`;
return fileURLToPath(u);
}
}
return pathWindowsDriveLetterToUpper(fileURLToPath(url));
} catch {
// console.error('Failed to convert URL to path', url);
return url.toString();
}
}

export const regExpWindowsPathDriveLetter = /^([a-zA-Z]):[\\/]/;

export function pathWindowsDriveLetterToUpper(absoluteFilePath: string): string {
return absoluteFilePath.replace(regExpWindowsPathDriveLetter, (s) => s.toUpperCase());
}

const regExpWindowsFileUrl = /^file:\/\/\/[a-zA-Z]:\//;

/**
* Test if a url is a file url with a windows path. It does check for UNC paths.
* @param url - the url
* @returns true if the url is a file url with a windows path with a drive letter.
*/
export function isWindowsFileUrl(url: URL | string): boolean {
return regExpWindowsFileUrl.test(url.toString());
}
16 changes: 14 additions & 2 deletions packages/cspell-url/src/fileUrl.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { describe, expect, test } from 'vitest';

import { urlBasename } from './dataUrl.mjs';
import { normalizeFilePathForUrl, toFileDirURL, toFileURL } from './defaultFileUrlBuilder.mjs';
import { pathWindowsDriveLetterToUpper, toFilePathOrHref } from './fileUrl.mjs';
import { isWindows, isWindowsFileUrl, pathWindowsDriveLetterToUpper, toFilePathOrHref } from './fileUrl.mjs';
import { FileUrlBuilder } from './FileUrlBuilder.mjs';
import { isUrlLike, normalizeWindowsUrl, toURL, urlParent } from './url.mjs';

Expand Down Expand Up @@ -129,9 +129,21 @@ describe('util', () => {
${'data:application/json'} | ${'data:application/json'}
${'stdin:file.txt'} | ${'stdin:file.txt'}
${'stdin:/path/to/dir'} | ${'stdin:/path/to/dir'}
`('windowsDriveLetterToUpper $path', ({ path, expected }) => {
`('pathWindowsDriveLetterToUpper $path', ({ path, expected }) => {
expect(pathWindowsDriveLetterToUpper(path)).toEqual(expected);
});

test.each`
url | expected
${'d:\\user\\data\\file.md'} | ${false}
${'file:///c:/user/data/file.md'} | ${true}
${'data:application/json'} | ${false}
${'stdin:file.txt'} | ${false}
${'stdin:/path/to/dir'} | ${false}
${import.meta.url} | ${isWindows}
`('isWindowsFileUrl $url', ({ url, expected }) => {
expect(isWindowsFileUrl(url)).toEqual(expected);
});
});

function u(path: string, relativeURL?: string | URL) {
Expand Down
Loading

0 comments on commit b5c9115

Please sign in to comment.