diff --git a/doc/api/path.md b/doc/api/path.md index c7a999cbb1ec22..c97b2fc89cdac1 100644 --- a/doc/api/path.md +++ b/doc/api/path.md @@ -67,12 +67,15 @@ example, `path.resolve('C:\\')` can potentially return a different result than -* `path` {string} +* `path` {string|URL} * `suffix` {string} An optional suffix to remove * Returns: {string} @@ -86,6 +89,9 @@ path.basename('/foo/bar/baz/asdf/quux.html'); path.basename('/foo/bar/baz/asdf/quux.html', '.html'); // Returns: 'quux' + +path.basename(new URL('file:///foo/bar/baz/asdf/quux.html'), '.html'); +// Returns: 'quux' ``` Although Windows usually treats file names, including file extensions, in a @@ -101,8 +107,8 @@ path.win32.basename('C:\\foo.HTML', '.html'); // Returns: 'foo.HTML' ``` -A [`TypeError`][] is thrown if `path` is not a string or if `suffix` is given -and is not a string. +A [`TypeError`][] is thrown if `path` is not a string/URL or if `suffix` is +given and is not a string. ## `path.delimiter` @@ -142,12 +148,15 @@ process.env.PATH.split(path.delimiter); -* `path` {string} +* `path` {string|URL} * Returns: {string} The `path.dirname()` method returns the directory name of a `path`, similar to @@ -157,21 +166,27 @@ the Unix `dirname` command. Trailing directory separators are ignored, see ```js path.dirname('/foo/bar/baz/asdf/quux'); // Returns: '/foo/bar/baz/asdf' + +path.dirname(new URL('file:///foo/bar/baz/asdf/quux')); +// Returns: '/foo/bar/baz/asdf' ``` -A [`TypeError`][] is thrown if `path` is not a string. +A [`TypeError`][] is thrown if `path` is not a string/URL. ## `path.extname(path)` -* `path` {string} +* `path` {string|URL} * Returns: {string} The `path.extname()` method returns the extension of the `path`, from the last @@ -187,6 +202,9 @@ path.extname('index.html'); path.extname('index.coffee.md'); // Returns: '.md' +path.extname(new URL('file:///index.coffee.md')); +// Returns: '.md' + path.extname('index.'); // Returns: '.' @@ -200,7 +218,7 @@ path.extname('.index.md'); // Returns: '.md' ``` -A [`TypeError`][] is thrown if `path` is not a string. +A [`TypeError`][] is thrown if `path` is not a string/URL. ## `path.format(pathObject)` @@ -283,9 +301,13 @@ path.format({ -* `path` {string} +* `path` {string|URL} * Returns: {boolean} The `path.isAbsolute()` method determines if `path` is an absolute path. @@ -296,6 +318,7 @@ For example, on POSIX: ```js path.isAbsolute('/foo/bar'); // true +path.isAbsolute(new URL('file:///foo/bar')); // true path.isAbsolute('/baz/..'); // true path.isAbsolute('qux/'); // false path.isAbsolute('.'); // false @@ -304,6 +327,7 @@ path.isAbsolute('.'); // false On Windows: ```js +path.isAbsolute(new URL('file:///foo/bar')); // true path.isAbsolute('//server'); // true path.isAbsolute('\\\\server'); // true path.isAbsolute('C:/foo/..'); // true @@ -313,15 +337,19 @@ path.isAbsolute('bar/baz'); // false path.isAbsolute('.'); // false ``` -A [`TypeError`][] is thrown if `path` is not a string. +A [`TypeError`][] is thrown if `path` is not a string/URL. ## `path.join([...paths])` -* `...paths` {string} A sequence of path segments +* `...paths` {string|URL} A sequence of path segments * Returns: {string} The `path.join()` method joins all given `path` segments together using the @@ -335,19 +363,26 @@ working directory. path.join('/foo', 'bar', 'baz/asdf', 'quux', '..'); // Returns: '/foo/bar/baz/asdf' +path.join(new URL('file:///foo'), 'bar', 'baz/asdf', 'quux', '..'); +// Returns: '/foo/bar/baz/asdf' + path.join('foo', {}, 'bar'); // Throws 'TypeError: Path must be a string. Received {}' ``` -A [`TypeError`][] is thrown if any of the path segments is not a string. +A [`TypeError`][] is thrown if any of the path segments is not a string/URL. ## `path.normalize(path)` -* `path` {string} +* `path` {string|URL} * Returns: {string} The `path.normalize()` method normalizes the given `path`, resolving `'..'` and @@ -374,6 +409,9 @@ For example, on POSIX: ```js path.normalize('/foo/bar//baz/asdf/quux/..'); // Returns: '/foo/bar/baz/asdf' + +path.normalize(new URL('file:///foo/bar//baz/asdf/quux/..')); +// Returns: '/foo/bar/baz/asdf' ``` On Windows: @@ -381,6 +419,9 @@ On Windows: ```js path.normalize('C:\\temp\\\\foo\\bar\\..\\'); // Returns: 'C:\\temp\\foo\\' + +path.normalize(new URL('file://C:\\temp\\\\foo\\bar\\..\\')); +// Returns: 'C:\\temp\\foo\\' ``` Since Windows recognizes multiple path separators, both separators will be @@ -391,15 +432,19 @@ path.win32.normalize('C:////temp\\\\/\\/\\/foo/bar'); // Returns: 'C:\\temp\\foo\\bar' ``` -A [`TypeError`][] is thrown if `path` is not a string. +A [`TypeError`][] is thrown if `path` is not a string/URL. ## `path.parse(path)` -* `path` {string} +* `path` {string|URL} * Returns: {Object} The `path.parse()` method returns an object whose properties represent @@ -458,7 +503,7 @@ path.parse('C:\\path\\dir\\file.txt'); (All spaces in the "" line should be ignored. They are purely for formatting.) ``` -A [`TypeError`][] is thrown if `path` is not a string. +A [`TypeError`][] is thrown if `path` is not a string/URL. ## `path.posix` @@ -482,6 +527,9 @@ The API is accessible via `require('node:path').posix` or `require('node:path/po -* `...paths` {string} A sequence of paths or path segments +* `...paths` {string|URL} A sequence of paths or path segments * Returns: {string} The `path.resolve()` method resolves a sequence of paths or path segments into @@ -548,6 +600,9 @@ of the current working directory. path.resolve('/foo/bar', './baz'); // Returns: '/foo/bar/baz' +path.resolve(new URL('file:///foo/bar'), './baz'); +// Returns: '/foo/bar/baz' + path.resolve('/foo/bar', '/tmp/file/'); // Returns: '/tmp/file' @@ -593,14 +648,18 @@ slashes (`\`). -* `path` {string} +* `path` {string|URL} * Returns: {string} On Windows systems only, returns an equivalent [namespace-prefixed path][] for -the given `path`. If `path` is not a string, `path` will be returned without -modifications. +the given `path`. If `path` is not a string/URL, `path` will be returned +without modifications. This method is meaningful only on Windows systems. On POSIX systems, the method is non-operational and always returns `path` without modifications. diff --git a/lib/internal/url.js b/lib/internal/url.js index 0e69ff52b5edef..8164eeb966ef4b 100644 --- a/lib/internal/url.js +++ b/lib/internal/url.js @@ -76,7 +76,6 @@ const { CHAR_PERCENT, CHAR_PLUS, } = require('internal/constants'); -const path = require('path'); const { validateFunction, @@ -84,6 +83,13 @@ const { const querystring = require('querystring'); +let lazyLoadPath; + +function lazyLoadPathSync() { + lazyLoadPath ??= require('path'); + return lazyLoadPath; +} + const { platform } = process; const isWindows = platform === 'win32'; @@ -1529,13 +1535,14 @@ function pathToFileURL(filepath) { RegExpPrototypeSymbolReplace(backslashRegEx, StringPrototypeSlice(filepath, hostnameEndIndex), '/')); return outURL; } - let resolved = path.resolve(filepath); + const { resolve, sep } = lazyLoadPathSync(); + let resolved = resolve(filepath); // path.resolve strips trailing slashes so we must add them back const filePathLast = StringPrototypeCharCodeAt(filepath, filepath.length - 1); if ((filePathLast === CHAR_FORWARD_SLASH || (isWindows && filePathLast === CHAR_BACKWARD_SLASH)) && - resolved[resolved.length - 1] !== path.sep) + resolved[resolved.length - 1] !== sep) resolved += '/'; // Call encodePathChars first to avoid encoding % again for ? and #. diff --git a/lib/path.js b/lib/path.js index 1a2b3e38eca03f..edb71b778cc728 100644 --- a/lib/path.js +++ b/lib/path.js @@ -47,6 +47,10 @@ const { validateString, } = require('internal/validators'); +const { + toPathIfFileURL, +} = require('internal/url'); + const platformIsWin32 = (process.platform === 'win32'); function isPathSeparator(code) { @@ -156,7 +160,7 @@ function _format(sep, pathObject) { const win32 = { /** * path.resolve([from ...], to) - * @param {...string} args + * @param {...(string|URL)} args * @returns {string} */ resolve(...args) { @@ -167,7 +171,7 @@ const win32 = { for (let i = args.length - 1; i >= -1; i--) { let path; if (i >= 0) { - path = args[i]; + path = toPathIfFileURL(args[i]); validateString(path, `paths[${i}]`); // Skip empty entries @@ -302,10 +306,11 @@ const win32 = { }, /** - * @param {string} path + * @param {string|URL} path * @returns {string} */ normalize(path) { + path = toPathIfFileURL(path); validateString(path, 'path'); const len = path.length; if (len === 0) @@ -400,10 +405,11 @@ const win32 = { }, /** - * @param {string} path + * @param {string|URL} path * @returns {boolean} */ isAbsolute(path) { + path = toPathIfFileURL(path); validateString(path, 'path'); const len = path.length; if (len === 0) @@ -419,7 +425,7 @@ const win32 = { }, /** - * @param {...string} args + * @param {...(string|URL)} args * @returns {string} */ join(...args) { @@ -429,7 +435,7 @@ const win32 = { let joined; let firstPart; for (let i = 0; i < args.length; ++i) { - const arg = args[i]; + const arg = toPathIfFileURL(args[i]); validateString(arg, 'path'); if (arg.length > 0) { if (joined === undefined) @@ -493,11 +499,13 @@ const win32 = { * from = 'C:\\orandea\\test\\aaa' * to = 'C:\\orandea\\impl\\bbb' * The output of the function should be: '..\\..\\impl\\bbb' - * @param {string} from - * @param {string} to + * @param {string|URL} from + * @param {string|URL} to * @returns {string} */ relative(from, to) { + from = toPathIfFileURL(from); + to = toPathIfFileURL(to); validateString(from, 'from'); validateString(to, 'to'); @@ -616,10 +624,11 @@ const win32 = { }, /** - * @param {string} path + * @param {string|URL} path * @returns {string} */ toNamespacedPath(path) { + path = toPathIfFileURL(path); // Note: this will *probably* throw somewhere. if (typeof path !== 'string' || path.length === 0) return path; @@ -651,10 +660,11 @@ const win32 = { }, /** - * @param {string} path + * @param {string|URL} path * @returns {string} */ dirname(path) { + path = toPathIfFileURL(path); validateString(path, 'path'); const len = path.length; if (len === 0) @@ -746,11 +756,12 @@ const win32 = { }, /** - * @param {string} path + * @param {string|URL} path * @param {string} [suffix] * @returns {string} */ basename(path, suffix) { + path = toPathIfFileURL(path); if (suffix !== undefined) validateString(suffix, 'ext'); validateString(path, 'path'); @@ -834,10 +845,11 @@ const win32 = { }, /** - * @param {string} path + * @param {string|URL} path * @returns {string} */ extname(path) { + path = toPathIfFileURL(path); validateString(path, 'path'); let start = 0; let startDot = -1; @@ -904,7 +916,7 @@ const win32 = { format: FunctionPrototypeBind(_format, null, '\\'), /** - * @param {string} path + * @param {string|URL} path * @returns {{ * dir: string; * root: string; @@ -914,6 +926,7 @@ const win32 = { * }} */ parse(path) { + path = toPathIfFileURL(path); validateString(path, 'path'); const ret = { root: '', dir: '', base: '', ext: '', name: '' }; @@ -1089,7 +1102,7 @@ const posixCwd = (() => { const posix = { /** * path.resolve([from ...], to) - * @param {...string} args + * @param {...(string|URL)} args * @returns {string} */ resolve(...args) { @@ -1097,7 +1110,7 @@ const posix = { let resolvedAbsolute = false; for (let i = args.length - 1; i >= -1 && !resolvedAbsolute; i--) { - const path = i >= 0 ? args[i] : posixCwd(); + const path = i >= 0 ? toPathIfFileURL(args[i]) : posixCwd(); validateString(path, `paths[${i}]`); // Skip empty entries @@ -1124,10 +1137,11 @@ const posix = { }, /** - * @param {string} path + * @param {string|URL} path * @returns {string} */ normalize(path) { + path = toPathIfFileURL(path); validateString(path, 'path'); if (path.length === 0) @@ -1153,17 +1167,18 @@ const posix = { }, /** - * @param {string} path + * @param {string|URL} path * @returns {boolean} */ isAbsolute(path) { + path = toPathIfFileURL(path); validateString(path, 'path'); return path.length > 0 && StringPrototypeCharCodeAt(path, 0) === CHAR_FORWARD_SLASH; }, /** - * @param {...string} args + * @param {...(string|URL)} args * @returns {string} */ join(...args) { @@ -1171,7 +1186,7 @@ const posix = { return '.'; let joined; for (let i = 0; i < args.length; ++i) { - const arg = args[i]; + const arg = toPathIfFileURL(args[i]); validateString(arg, 'path'); if (arg.length > 0) { if (joined === undefined) @@ -1186,11 +1201,13 @@ const posix = { }, /** - * @param {string} from - * @param {string} to + * @param {string|URL} from + * @param {string|URL} to * @returns {string} */ relative(from, to) { + from = toPathIfFileURL(from); + to = toPathIfFileURL(to); validateString(from, 'from'); validateString(to, 'to'); @@ -1263,19 +1280,21 @@ const posix = { }, /** - * @param {string} path + * @param {string|URL} path * @returns {string} */ toNamespacedPath(path) { + path = toPathIfFileURL(path); // Non-op on posix systems return path; }, /** - * @param {string} path + * @param {string|URL} path * @returns {string} */ dirname(path) { + path = toPathIfFileURL(path); validateString(path, 'path'); if (path.length === 0) return '.'; @@ -1302,11 +1321,12 @@ const posix = { }, /** - * @param {string} path + * @param {string|URL} path * @param {string} [suffix] * @returns {string} */ basename(path, suffix) { + path = toPathIfFileURL(path); if (suffix !== undefined) validateString(suffix, 'ext'); validateString(path, 'path'); @@ -1382,10 +1402,11 @@ const posix = { }, /** - * @param {string} path + * @param {string|URL} path * @returns {string} */ extname(path) { + path = toPathIfFileURL(path); validateString(path, 'path'); let startDot = -1; let startPart = 0; @@ -1440,7 +1461,7 @@ const posix = { format: FunctionPrototypeBind(_format, null, '/'), /** - * @param {string} path + * @param {string|URL} path * @returns {{ * dir: string; * root: string; @@ -1450,6 +1471,7 @@ const posix = { * }} */ parse(path) { + path = toPathIfFileURL(path); validateString(path, 'path'); const ret = { root: '', dir: '', base: '', ext: '', name: '' }; diff --git a/test/parallel/test-path-basename.js b/test/parallel/test-path-basename.js index b16f9e5d63a94b..b3970c0d517f74 100644 --- a/test/parallel/test-path-basename.js +++ b/test/parallel/test-path-basename.js @@ -2,6 +2,7 @@ require('../common'); const assert = require('assert'); const path = require('path'); +const url = require('url'); assert.strictEqual(path.basename(__filename), 'test-path-basename.js'); assert.strictEqual(path.basename(__filename, '.js'), 'test-path-basename'); @@ -35,6 +36,10 @@ assert.strictEqual(path.basename('/a/b'), 'b'); assert.strictEqual(path.basename('//a'), 'a'); assert.strictEqual(path.basename('a', 'a'), ''); +// URL inputs +assert.strictEqual(path.basename(url.pathToFileURL(__filename)), 'test-path-basename.js'); +assert.strictEqual(path.basename(url.pathToFileURL(__filename), '.js'), 'test-path-basename'); + // On Windows a backslash acts as a path separator. assert.strictEqual(path.win32.basename('\\dir\\basename.ext'), 'basename.ext'); assert.strictEqual(path.win32.basename('\\basename.ext'), 'basename.ext'); diff --git a/test/parallel/test-path-dirname.js b/test/parallel/test-path-dirname.js index d085cecb24f069..43df8e66e76d35 100644 --- a/test/parallel/test-path-dirname.js +++ b/test/parallel/test-path-dirname.js @@ -2,6 +2,7 @@ const common = require('../common'); const assert = require('assert'); const path = require('path'); +const url = require('url'); assert.strictEqual(path.dirname(__filename).substr(-13), common.isWindows ? 'test\\parallel' : 'test/parallel'); @@ -15,6 +16,9 @@ assert.strictEqual(path.posix.dirname('////'), '/'); assert.strictEqual(path.posix.dirname('//a'), '//'); assert.strictEqual(path.posix.dirname('foo'), '.'); +assert.strictEqual(path.posix.dirname(url.pathToFileURL('/a/b/')), '/a'); +assert.strictEqual(path.posix.dirname(url.pathToFileURL('/a/b')), '/a'); + assert.strictEqual(path.win32.dirname('c:\\'), 'c:\\'); assert.strictEqual(path.win32.dirname('c:\\foo'), 'c:\\'); assert.strictEqual(path.win32.dirname('c:\\foo\\'), 'c:\\'); @@ -57,3 +61,6 @@ assert.strictEqual(path.win32.dirname(''), '.'); assert.strictEqual(path.win32.dirname('/'), '/'); assert.strictEqual(path.win32.dirname('////'), '/'); assert.strictEqual(path.win32.dirname('foo'), '.'); + +assert.strictEqual(path.win32.dirname('file:///c:/'), 'c:\\'); +assert.strictEqual(path.win32.dirname('file:///c:/foo'), 'c:\\'); diff --git a/test/parallel/test-path-extname.js b/test/parallel/test-path-extname.js index be5a6316b0c7c3..a0b8e90126d95c 100644 --- a/test/parallel/test-path-extname.js +++ b/test/parallel/test-path-extname.js @@ -2,6 +2,7 @@ require('../common'); const assert = require('assert'); const path = require('path'); +const url = require('url'); const failures = []; const slashRE = /\//g; @@ -50,6 +51,7 @@ const testPaths = [ ['file//', ''], ['file./', '.'], ['file.//', '.'], + [url.pathToFileURL('/file.ext'), '.ext'], ]; for (const testPath of testPaths) { diff --git a/test/parallel/test-path-isabsolute.js b/test/parallel/test-path-isabsolute.js index 66b4f1ee51103a..7ffc1d1e69f555 100644 --- a/test/parallel/test-path-isabsolute.js +++ b/test/parallel/test-path-isabsolute.js @@ -2,6 +2,7 @@ require('../common'); const assert = require('assert'); const path = require('path'); +const url = require('url'); assert.strictEqual(path.win32.isAbsolute('/'), true); assert.strictEqual(path.win32.isAbsolute('//'), true); @@ -26,3 +27,6 @@ assert.strictEqual(path.posix.isAbsolute('/home/foo'), true); assert.strictEqual(path.posix.isAbsolute('/home/foo/..'), true); assert.strictEqual(path.posix.isAbsolute('bar/'), false); assert.strictEqual(path.posix.isAbsolute('./baz'), false); + +assert.strictEqual(path.posix.isAbsolute(url.pathToFileURL('bar/')), true); +assert.strictEqual(path.posix.isAbsolute(url.pathToFileURL('/home/foo')), true); diff --git a/test/parallel/test-path-makelong.js b/test/parallel/test-path-makelong.js index b0a4ebc6b30b62..0810bc5463b9d8 100644 --- a/test/parallel/test-path-makelong.js +++ b/test/parallel/test-path-makelong.js @@ -24,6 +24,7 @@ const common = require('../common'); const fixtures = require('../common/fixtures'); const assert = require('assert'); const path = require('path'); +const url = require('url'); if (common.isWindows) { const file = fixtures.path('a.js'); @@ -58,6 +59,8 @@ assert.strictEqual(path.posix.toNamespacedPath(true), true); assert.strictEqual(path.posix.toNamespacedPath(1), 1); assert.strictEqual(path.posix.toNamespacedPath(), undefined); assert.strictEqual(path.posix.toNamespacedPath(emptyObj), emptyObj); +assert.strictEqual(path.posix.toNamespacedPath(url.pathToFileURL('/foo/bar'), '/foo/bar')); + if (common.isWindows) { // These tests cause resolve() to insert the cwd, so we cannot test them from // non-Windows platforms (easily)