diff --git a/.npmrc b/.npmrc index c1ca392..1b8d617 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,2 @@ package-lock = false +@jsr:registry=https://npm.jsr.io diff --git a/packages/config-array/package.json b/packages/config-array/package.json index 2dfdd5e..94497de 100644 --- a/packages/config-array/package.json +++ b/packages/config-array/package.json @@ -47,6 +47,7 @@ "license": "Apache-2.0", "dependencies": { "@eslint/object-schema": "^2.1.4", + "@jsr/std__path": "^1.0.2", "debug": "^4.3.1", "minimatch": "^3.1.2" }, diff --git a/packages/config-array/src/config-array.js b/packages/config-array/src/config-array.js index b846b8c..9e7ad97 100644 --- a/packages/config-array/src/config-array.js +++ b/packages/config-array/src/config-array.js @@ -7,14 +7,14 @@ // Imports //------------------------------------------------------------------------------ -import path from "node:path"; +import * as posixPath from "@jsr/std__path/posix"; +import * as windowsPath from "@jsr/std__path/windows"; import minimatch from "minimatch"; import createDebug from "debug"; import { ObjectSchema } from "@eslint/object-schema"; import { baseSchema } from "./base-schema.js"; import { filesAndIgnoresSchema } from "./files-and-ignores-schema.js"; -import toNamespacedPath from "./to-namespaced-path.js"; //------------------------------------------------------------------------------ // Types @@ -90,8 +90,8 @@ const CONFIG_WITH_STATUS_UNCONFIGURED = Object.freeze({ status: "unconfigured", }); -// Match two leading dots followed by a path separator or the end of input. -const EXTERNAL_PATH_REGEX = new RegExp(`^\\.\\.($|\\${path.sep})`, "u"); +// Match two leading dots followed by a slash or the end of input. +const EXTERNAL_PATH_REGEX = /^\.\.(\/|$)/u; /** * Wrapper error for config validation errors that adds a name to the front of the @@ -352,7 +352,8 @@ function normalizeSync(items, context, extraConfigTypes) { * matcher. * @param {Array boolean)>} ignores The ignore patterns to check. * @param {string} filePath The absolute path of the file to check. - * @param {string} relativeFilePath The path of the file to check relative to the base path. + * @param {string} relativeFilePath The path of the file to check relative to the base path, + * using slash ("/") as a separator. * @returns {boolean} True if the path should be ignored and false if not. */ function shouldIgnorePath(ignores, filePath, relativeFilePath) { @@ -386,7 +387,8 @@ function shouldIgnorePath(ignores, filePath, relativeFilePath) { * Determines if a given file path is matched by a config based on * `ignores` only. * @param {string} filePath The absolute file path to check. - * @param {string} relativeFilePath The path of the file to check relative to the base path. + * @param {string} relativeFilePath The path of the file to check relative to the base path, + * using slash ("/") as a separator. * @param {Object} config The config object to check. * @returns {boolean} True if the file path is matched by the config, * false if not. @@ -484,6 +486,15 @@ function assertExtraConfigTypes(extraConfigTypes) { } } +/** + * Determines if an absolute path is a POSIX path. + * @param {string} path The absolute path to check. + * @returns {boolean} Whether the specified path is a POSIX path. + */ +function isPosixPath(path) { + return path.startsWith("/"); +} + //------------------------------------------------------------------------------ // Public Interface //------------------------------------------------------------------------------ @@ -509,7 +520,7 @@ export class ConfigArray extends Array { * @param {Iterable|Function|Object} configs An iterable yielding config * objects, or a config function, or a config object. * @param {Object} options The options for the ConfigArray. - * @param {string} [options.basePath=""] The path of the config file + * @param {string} [options.basePath=""] The absolute path of the config file directory. * @param {boolean} [options.normalized=false] Flag indicating if the * configs have already been normalized. * @param {Object} [options.schema] The additional schema @@ -553,6 +564,26 @@ export class ConfigArray extends Array { */ this.basePath = basePath; + /** + * The path-handling implementations depend on whether basePath is a Window or a Unix path, + * or unspecified. + * If the base path is not specified, files will never be considered "external" (outside the + * base path). This allows for some implementations to be heavily simplified. + */ + if (!basePath) { + this.path = { + dirname: null, + join: null, + relative: (from, to) => to, + SEPARATOR: "/", + toNamespacedPath: path => path, + }; + } else if (isPosixPath(basePath)) { + this.path = posixPath; + } else { + this.path = windowsPath; + } + assertExtraConfigTypes(extraConfigTypes); /** @@ -588,7 +619,8 @@ export class ConfigArray extends Array { // On Windows, `path.relative()` returns an absolute path when given two paths on different drives. // The namespaced base path is useful to make sure that calculated relative paths are always relative. // On Unix, it is identical to the base path. - this.namespacedBasePath = toNamespacedPath(basePath || process.cwd()); + this.namespacedBasePath = + basePath && this.path.toNamespacedPath(basePath); } /** @@ -794,10 +826,12 @@ export class ConfigArray extends Array { // check to see if the file is outside the base path - const relativeFilePath = path.relative( - this.namespacedBasePath, - toNamespacedPath(filePath), - ); + const relativeFilePath = this.path + .relative( + this.namespacedBasePath, + this.path.toNamespacedPath(filePath), + ) + .replaceAll(this.path.SEPARATOR, "/"); if (EXTERNAL_PATH_REGEX.test(relativeFilePath)) { debug(`No config for file ${filePath} outside of base path`); @@ -809,8 +843,13 @@ export class ConfigArray extends Array { // next check to see if the file should be ignored + // Use predetermined `dirname` implementation, or deduce from the argument. + const dirname = + this.path.dirname ?? + (isPosixPath(filePath) ? posixPath.dirname : windowsPath.dirname); + // check if this should be ignored due to its directory - if (this.isDirectoryIgnored(path.dirname(filePath))) { + if (this.isDirectoryIgnored(dirname(filePath))) { debug(`Ignoring ${filePath} based on directory pattern`); // cache and return result @@ -835,7 +874,7 @@ export class ConfigArray extends Array { this.forEach((config, index) => { if (!config.files) { if (!config.ignores) { - debug(`Anonymous universal config found for ${filePath}`); + debug(`Universal config found for ${filePath}`); matchingConfigIndices.push(index); return; } @@ -1013,10 +1052,12 @@ export class ConfigArray extends Array { isDirectoryIgnored(directoryPath) { assertNormalized(this); - let relativeDirectoryPath = path.relative( - this.namespacedBasePath, - toNamespacedPath(directoryPath), - ); + const relativeDirectoryPath = this.path + .relative( + this.namespacedBasePath, + this.path.toNamespacedPath(directoryPath), + ) + .replaceAll(this.path.SEPARATOR, "/"); // basePath directory can never be ignored if (relativeDirectoryPath === "") { @@ -1027,8 +1068,6 @@ export class ConfigArray extends Array { return true; } - relativeDirectoryPath = relativeDirectoryPath.replace(/\\/gu, "/"); - // first check the cache const cache = dataCache.get(this).directoryMatches; @@ -1040,6 +1079,11 @@ export class ConfigArray extends Array { let relativeDirectoryToCheck = ""; let result; + // Use predetermined `join` implementation, or deduce from the argument. + const join = + this.path.join ?? + (isPosixPath(directoryPath) ? posixPath.join : windowsPath.join); + /* * In order to get the correct gitignore-style ignores, where an * ignored parent directory cannot have any descendants unignored, @@ -1054,7 +1098,7 @@ export class ConfigArray extends Array { result = shouldIgnorePath( this.ignores, - path.join(this.basePath, relativeDirectoryToCheck), + join(this.basePath, relativeDirectoryToCheck), relativeDirectoryToCheck, ); diff --git a/packages/config-array/src/to-namespaced-path.js b/packages/config-array/src/to-namespaced-path.js deleted file mode 100644 index eb3bc5b..0000000 --- a/packages/config-array/src/to-namespaced-path.js +++ /dev/null @@ -1,35 +0,0 @@ -/** - * @fileoverview Polyfill for Node.js `path.toNamespacedPath` - * @license - * Copyright Node.js contributors: https://github.com/nodejs/node/blob/v22.3.0/LICENSE - */ - -import { resolve, sep } from "node:path"; - -/** - * @param {string} path A path to be converted into a namespace-prefixed path. - * @returns {string} A namespace-prefixed path. - */ -function win32ToNamespacedPath(path) { - if (typeof path !== "string" || path.length === 0) { - return path; - } - - const resolvedPath = resolve(path); - if (resolvedPath.length <= 2) { - return path; - } - - if (/^\\\\[^.?]/u.test(resolvedPath)) { - // Matched non-long UNC root, convert the path to a long UNC path - return `\\\\?\\UNC\\${resolvedPath.slice(2)}`; - } - if (/^[a-z]:\\/iu.test(resolvedPath)) { - // Matched device root, convert the path to a long UNC path - return `\\\\?\\${resolvedPath}`; - } - return resolvedPath; -} - -// On non-Windows systems, `toNamespacedPath` returns the argument as is. -export default sep === "\\" ? win32ToNamespacedPath : arg => arg; diff --git a/packages/config-array/tests/config-array.test.js b/packages/config-array/tests/config-array.test.js index 736fca7..96b349e 100644 --- a/packages/config-array/tests/config-array.test.js +++ b/packages/config-array/tests/config-array.test.js @@ -2047,12 +2047,7 @@ describe("ConfigArray", () => { }); }); - describe("on Windows", () => { - // Windows only - if (process.platform !== "win32") { - return; - } - + describe("Windows paths", () => { it('should return "matched" for a file in the base directory with different capitalization', () => { configs = new ConfigArray([{ files: ["**/*.js"] }], { basePath: "C:\\DIR", @@ -2079,19 +2074,18 @@ describe("ConfigArray", () => { ); }); - it('should return "external" for a file on a different drive when no base path is specified', () => { - const currentDriveLetter = process.cwd()[0]; - const otherDriveLetter = - currentDriveLetter === "X" ? "Y" : "X"; - const filePath = `${otherDriveLetter}:\\dir\\file.js`; - + it('should return "matched" for files on different drives when no base path is specified', () => { configs = new ConfigArray([{ files: ["**/*.js"] }]); configs.normalizeSync(); assert.strictEqual( - configs.getConfigStatus(filePath), - "external", + configs.getConfigStatus("X:\\dir1\\file.js"), + "matched", + ); + assert.strictEqual( + configs.getConfigStatus("Y:\\dir2\\file.js"), + "matched", ); });