From edaa955764bae0a724783e4478010036eeab2bc7 Mon Sep 17 00:00:00 2001 From: Lexus Drumgold Date: Sat, 23 Nov 2024 21:47:17 -0500 Subject: [PATCH] feat!: url support Signed-off-by: Lexus Drumgold --- .dictionary.txt | 2 + .github/infrastructure.yml | 1 - .github/workflows/ci.yml | 1 - README.md | 6 +- __fixtures__/drive.mts | 15 + __fixtures__/env.mts | 20 - __tests__/plugins/chai-path.mts | 62 +++ __tests__/setup/chai.mts | 17 + __tests__/setup/env.mts | 15 - __tests__/utils/cwd-windows.mts | 21 + package.json | 4 + src/__snapshots__/index.e2e.snap | 2 + .../file-url-to-path-options.spec-d.mts | 13 + .../path-to-file-url-options.spec-d.mts | 20 + .../__tests__/relative-options.spec-d.mts | 13 + .../__tests__/resolve-with-options.spec-d.mts | 22 + .../__tests__/to-path-options.spec-d.mts | 20 + src/interfaces/file-url-to-path-options.mts | 17 + src/interfaces/index.mts | 11 + src/interfaces/path-to-file-url-options.mts | 22 + src/interfaces/pathe.mts | 397 ++++++++++++++---- src/interfaces/platform-options.mts | 2 +- src/interfaces/platform-path.mts | 171 +++++--- src/interfaces/relative-options.mts | 17 + src/interfaces/resolve-with-options.mts | 25 ++ src/interfaces/to-path-options.mts | 22 + src/internal/__tests__/can-parse-url.spec.mts | 25 ++ ...is-url.spec.mts => is-url-object.spec.mts} | 8 +- .../__tests__/validate-url-string.spec.mts | 39 ++ src/internal/can-parse-url.mts | 27 ++ .../{is-url.mts => is-url-object.mts} | 12 +- src/internal/normalize-string.mts | 2 - src/internal/validate-string.mts | 2 +- src/internal/validate-url-string.mts | 34 ++ src/lib/__snapshots__/add-ext.snap | 5 - src/lib/__snapshots__/change-ext.snap | 13 - src/lib/__snapshots__/extnames.snap | 12 +- src/lib/__snapshots__/remove-ext.snap | 13 - src/lib/__snapshots__/to-path.snap | 19 + src/lib/__snapshots__/to-posix.snap | 35 -- src/lib/__tests__/add-ext.spec.mts | 34 +- src/lib/__tests__/basename.spec.mts | 44 +- src/lib/__tests__/change-ext.spec.mts | 38 +- src/lib/__tests__/cwd.spec.mts | 19 +- src/lib/__tests__/dirname.spec.mts | 16 +- src/lib/__tests__/extname.spec.mts | 17 +- src/lib/__tests__/extnames.spec.mts | 9 +- src/lib/__tests__/file-url-to-path.spec.mts | 35 +- src/lib/__tests__/format.spec.mts | 2 +- src/lib/__tests__/is-absolute.spec.mts | 41 +- src/lib/__tests__/is-url.spec.mts | 20 + src/lib/__tests__/join.spec.mts | 2 +- .../matches-glob.functional.spec.mts | 22 +- src/lib/__tests__/normalize.spec.mts | 2 +- src/lib/__tests__/parse.spec.mts | 12 +- src/lib/__tests__/path-to-file-url.spec.mts | 2 +- src/lib/__tests__/relative.spec.mts | 32 +- src/lib/__tests__/remove-ext.spec.mts | 42 +- src/lib/__tests__/resolve-with.spec.mts | 97 +++-- src/lib/__tests__/resolve.functional.spec.mts | 5 +- src/lib/__tests__/root.spec.mts | 29 +- src/lib/__tests__/to-namespaced-path.spec.mts | 12 +- src/lib/__tests__/to-path.spec.mts | 32 ++ src/lib/__tests__/to-posix.spec.mts | 36 +- src/lib/add-ext.mts | 103 ++++- src/lib/basename.mts | 45 +- src/lib/change-ext.mts | 117 +++++- src/lib/cwd.mts | 7 +- src/lib/dirname.mts | 59 +-- src/lib/extname.mts | 45 +- src/lib/extnames.mts | 26 +- src/lib/file-url-to-path.mts | 34 +- src/lib/format-ext.mts | 14 +- src/lib/format.mts | 9 +- src/lib/index.mts | 2 + src/lib/is-absolute.mts | 35 +- src/lib/is-device-root.mts | 8 +- src/lib/is-sep.mts | 8 +- src/lib/is-url.mts | 26 ++ src/lib/join.mts | 6 +- src/lib/matches-glob.mts | 35 +- src/lib/normalize.mts | 184 ++++---- src/lib/parse.mts | 49 ++- src/lib/path-to-file-url.mts | 23 +- src/lib/relative.mts | 49 ++- src/lib/remove-ext.mts | 89 +++- src/lib/resolve-with.mts | 84 ++-- src/lib/resolve.mts | 7 +- src/lib/root.mts | 151 ++++--- src/lib/to-namespaced-path.mts | 27 +- src/lib/to-path.mts | 113 +++++ src/lib/to-posix.mts | 115 ++++- src/pathe.mts | 4 + tsconfig.typecheck.json | 1 + typings/chai/index.d.ts | 53 +++ vitest-env.d.mts | 6 +- vitest.config.mts | 2 +- yarn.lock | 40 +- 98 files changed, 2435 insertions(+), 925 deletions(-) create mode 100644 __fixtures__/drive.mts delete mode 100644 __fixtures__/env.mts create mode 100644 __tests__/plugins/chai-path.mts create mode 100644 __tests__/setup/chai.mts delete mode 100644 __tests__/setup/env.mts create mode 100644 __tests__/utils/cwd-windows.mts create mode 100644 src/interfaces/__tests__/file-url-to-path-options.spec-d.mts create mode 100644 src/interfaces/__tests__/path-to-file-url-options.spec-d.mts create mode 100644 src/interfaces/__tests__/relative-options.spec-d.mts create mode 100644 src/interfaces/__tests__/resolve-with-options.spec-d.mts create mode 100644 src/interfaces/__tests__/to-path-options.spec-d.mts create mode 100644 src/interfaces/file-url-to-path-options.mts create mode 100644 src/interfaces/path-to-file-url-options.mts create mode 100644 src/interfaces/relative-options.mts create mode 100644 src/interfaces/resolve-with-options.mts create mode 100644 src/interfaces/to-path-options.mts create mode 100644 src/internal/__tests__/can-parse-url.spec.mts rename src/internal/__tests__/{is-url.spec.mts => is-url-object.spec.mts} (77%) create mode 100644 src/internal/__tests__/validate-url-string.spec.mts create mode 100644 src/internal/can-parse-url.mts rename src/internal/{is-url.mts => is-url-object.mts} (84%) create mode 100644 src/internal/validate-url-string.mts delete mode 100644 src/lib/__snapshots__/add-ext.snap delete mode 100644 src/lib/__snapshots__/change-ext.snap delete mode 100644 src/lib/__snapshots__/remove-ext.snap create mode 100644 src/lib/__snapshots__/to-path.snap delete mode 100644 src/lib/__snapshots__/to-posix.snap create mode 100644 src/lib/__tests__/is-url.spec.mts create mode 100644 src/lib/__tests__/to-path.spec.mts create mode 100644 src/lib/is-url.mts create mode 100644 src/lib/to-path.mts create mode 100644 typings/chai/index.d.ts diff --git a/.dictionary.txt b/.dictionary.txt index da46787b..7bf03cb7 100644 --- a/.dictionary.txt +++ b/.dictionary.txt @@ -1,7 +1,9 @@ abar attw barx +cdir cefc +cindex codecov commitlintrc dbar diff --git a/.github/infrastructure.yml b/.github/infrastructure.yml index e9a672c1..a95ec590 100644 --- a/.github/infrastructure.yml +++ b/.github/infrastructure.yml @@ -32,7 +32,6 @@ branches: - context: gitguardian - context: lint - context: spelling - - context: test (22) - context: test (23) - context: typescript (5.6.3) - context: typescript (5.7.1-rc) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ae7b55a2..d157d304 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -309,7 +309,6 @@ jobs: matrix: node-version: - 23 - - 22 env: COVERAGE_SUMMARY: ./coverage/coverage-summary.json NODE_VERSION: ${{ matrix.node-version }} diff --git a/README.md b/README.md index e6d06ab6..f7dbec2f 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Universal drop-in replacement for [`node:path`][node-path] This package is a universal drop-in replacement for Node.js' [`path`][node-path] module. It enforces consistency between POSIX and Windows operating systems and also provides additional utilities for working -with file paths and extensions. +with file URLs, paths, and extensions. ## When should I use this? @@ -99,6 +99,7 @@ import { isAbsolute, isDeviceRoot, isSep, + isURL, join, matchesGlob, normalize, @@ -111,6 +112,7 @@ import { root, sep, toNamespacedPath, + toPath, toPosix } from '@flex-development/pathe' ``` @@ -134,6 +136,7 @@ This package exports the following identifiers: - [`isAbsolute`](./src/lib/is-absolute.mts) - [`isDeviceRoot`](./src/lib/is-device-root.mts) - [`isSep`](./src/lib/is-sep.mts) +- [`isURL`](./src/lib/is-url.mts) - [`join`](./src/lib/join.mts) - [`matchesGlob`](./src/lib/matches-glob.mts) - [`normalize`](./src/lib/normalize.mts) @@ -147,6 +150,7 @@ This package exports the following identifiers: - [`root`](./src/lib/root.mts) - [`sep`](./src/lib/sep.mts) - [`toNamespacedPath`](./src/lib/to-namespaced-path.mts) +- [`toPath`](./src/lib/to-path.mts) - [`toPosix`](./src/lib/to-posix.mts) - [`win32`](./src/pathe.mts) diff --git a/__fixtures__/drive.mts b/__fixtures__/drive.mts new file mode 100644 index 00000000..ee1db265 --- /dev/null +++ b/__fixtures__/drive.mts @@ -0,0 +1,15 @@ +/** + * @file Fixtures - DRIVE + * @module fixtures/drive + */ + +import type { DriveLetter } from '@flex-development/pathe' + +/** + * Windows drive letter. + * + * @const {DriveLetter} DRIVE + */ +const DRIVE: DriveLetter = 'T:' + +export default DRIVE diff --git a/__fixtures__/env.mts b/__fixtures__/env.mts deleted file mode 100644 index e5f55873..00000000 --- a/__fixtures__/env.mts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * @file Fixtures - env - * @module fixtures/env - */ - -import process from '#internal/process' - -/** - * Environment variables. - * - * @const {Record} env - */ -const env: Record = { - '=P:': 'P:' + process.cwd(), - '=Q:': 'Q:' + process.cwd(), - '=R:': 'R:' + process.cwd(), - '=Z:': 'A:' + process.cwd() -} - -export default env diff --git a/__tests__/plugins/chai-path.mts b/__tests__/plugins/chai-path.mts new file mode 100644 index 00000000..292880ed --- /dev/null +++ b/__tests__/plugins/chai-path.mts @@ -0,0 +1,62 @@ +/** + * @file Plugins - chaiPath + * @module tests/plugins/chaiPath + */ + +import type Chai from 'chai' +import path from 'node:path' + +export default plugin + +/** + * Chai assertion plugin for the Node.js [path][node-path] API. + * + * [node-path]: https://nodejs.org/api/path.html + * + * @see {@linkcode Chai.ChaiStatic} + * @see {@linkcode Chai.ChaiUtils} + * + * @param {ChaiStatic} chai + * `chai` export + * @param {Chai.ChaiUtils} utils + * `chai` utilities + * @return {undefined} + */ +function plugin(chai: Chai.ChaiStatic, utils: Chai.ChaiUtils): undefined { + utils.addMethod(chai.Assertion.prototype, extname.name, extname) + + return void 0 + + /** + * Assert the return value of {@linkcode path.extname}. + * + * @this {Chai.Assertion} + * + * @param {unknown} expected + * Expected file extension + * @return {undefined} + */ + function extname(this: Chai.Assertion, expected: unknown): undefined { + /** + * Subject of assertion. + * + * @const {string} subject + */ + const subject: string = utils.flag(this, 'object') + + /** + * File extension. + * + * @const {string} ext + */ + const actual: string = path.extname(subject) + + return void this.assert( + actual === expected, + 'expected extname of #{this} to be #{exp} but got #{act}', + 'expected extname of #{this} to not be #{act}', + expected, + actual + ) + } +} diff --git a/__tests__/setup/chai.mts b/__tests__/setup/chai.mts new file mode 100644 index 00000000..2e6b7030 --- /dev/null +++ b/__tests__/setup/chai.mts @@ -0,0 +1,17 @@ +/** + * @file Test Setup - chai + * @module tests/setup/chai + * @see https://chaijs.com + */ + +import chaiPath from '#tests/plugins/chai-path' +import chaiString from 'chai-string' +import { chai } from 'vitest' + +/** + * initialize chai plugins. + * + * @see https://github.com/onechiporenko/chai-string + */ +chai.use(chaiPath) +chai.use(chaiString) diff --git a/__tests__/setup/env.mts b/__tests__/setup/env.mts deleted file mode 100644 index ff03e0ab..00000000 --- a/__tests__/setup/env.mts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * @file Test Setup - env - * @module tests/setup/env - */ - -import env from '#fixtures/env' -import process from '#internal/process' - -afterAll(() => { - for (const key of Object.keys(env)) delete process.env[key] -}) - -beforeAll(() => { - process.env = { ...process.env, ...env } -}) diff --git a/__tests__/utils/cwd-windows.mts b/__tests__/utils/cwd-windows.mts new file mode 100644 index 00000000..0c3edec7 --- /dev/null +++ b/__tests__/utils/cwd-windows.mts @@ -0,0 +1,21 @@ +/** + * @file Test Utilities - cwdWindows + * @module tests/utils/cwdWindows + */ + +import DRIVE from '#fixtures/drive' +import { ok } from 'devlop' +import { posix, win32 } from 'node:path' + +/** + * Get the path to the current working directory as a windows drive path. + * + * @return {string} + * Absolute path to current working directory + */ +function cwdWindows(): string { + ok(typeof process.env['PWD'] === 'string', 'expected `process.env.PWD`') + return DRIVE + process.env['PWD'].replaceAll(posix.sep, win32.sep) +} + +export default cwdWindows diff --git a/package.json b/package.json index ba1b96e3..df0e1480 100644 --- a/package.json +++ b/package.json @@ -182,6 +182,8 @@ "@flex-development/tutils": "6.0.0-alpha.25", "@stylistic/eslint-plugin": "2.10.1", "@tsconfig/strictest": "2.0.5", + "@types/chai": "5.0.1", + "@types/chai-string": "1.4.5", "@types/eslint": "9.6.1", "@types/eslint__js": "8.42.3", "@types/is-ci": "3.0.4", @@ -192,6 +194,8 @@ "@vates/toggle-scripts": "1.0.0", "@vitest/coverage-v8": "2.1.4", "@vitest/ui": "2.1.4", + "chai": "5.1.2", + "chai-string": "1.5.0", "consola": "3.2.3", "cross-env": "7.0.3", "cspell": "8.16.0", diff --git a/src/__snapshots__/index.e2e.snap b/src/__snapshots__/index.e2e.snap index b8d7b5df..b331f810 100644 --- a/src/__snapshots__/index.e2e.snap +++ b/src/__snapshots__/index.e2e.snap @@ -17,6 +17,7 @@ exports[`e2e:pathe > should expose public api 1`] = ` "isAbsolute", "isDeviceRoot", "isSep", + "isURL", "join", "matchesGlob", "normalize", @@ -29,6 +30,7 @@ exports[`e2e:pathe > should expose public api 1`] = ` "root", "sep", "toNamespacedPath", + "toPath", "toPosix", "default", "posix", diff --git a/src/interfaces/__tests__/file-url-to-path-options.spec-d.mts b/src/interfaces/__tests__/file-url-to-path-options.spec-d.mts new file mode 100644 index 00000000..b8959c38 --- /dev/null +++ b/src/interfaces/__tests__/file-url-to-path-options.spec-d.mts @@ -0,0 +1,13 @@ +/** + * @file Unit Tests - FileUrlToPathOptions + * @module pathe/interfaces/tests/unit-d/FileUrlToPathOptions + */ + +import type TestSubject from '#interfaces/file-url-to-path-options' +import type { PlatformOptions } from '@flex-development/pathe' + +describe('unit-d:interfaces/FileUrlToPathOptions', () => { + it('should extend PlatformOptions', () => { + expectTypeOf().toMatchTypeOf() + }) +}) diff --git a/src/interfaces/__tests__/path-to-file-url-options.spec-d.mts b/src/interfaces/__tests__/path-to-file-url-options.spec-d.mts new file mode 100644 index 00000000..ff34cfbe --- /dev/null +++ b/src/interfaces/__tests__/path-to-file-url-options.spec-d.mts @@ -0,0 +1,20 @@ +/** + * @file Unit Tests - PathToFileUrlOptions + * @module pathe/interfaces/tests/unit-d/PathToFileUrlOptions + */ + +import type TestSubject from '#interfaces/path-to-file-url-options' +import type { + PlatformOptions, + ResolveWithOptions +} from '@flex-development/pathe' + +describe('unit-d:interfaces/PathToFileUrlOptions', () => { + it('should extend PlatformOptions', () => { + expectTypeOf().toMatchTypeOf() + }) + + it('should extend ResolveWithOptions', () => { + expectTypeOf().toMatchTypeOf() + }) +}) diff --git a/src/interfaces/__tests__/relative-options.spec-d.mts b/src/interfaces/__tests__/relative-options.spec-d.mts new file mode 100644 index 00000000..f0f59cdf --- /dev/null +++ b/src/interfaces/__tests__/relative-options.spec-d.mts @@ -0,0 +1,13 @@ +/** + * @file Unit Tests - RelativeOptions + * @module pathe/interfaces/tests/unit-d/RelativeOptions + */ + +import type TestSubject from '#interfaces/relative-options' +import type { ResolveWithOptions } from '@flex-development/pathe' + +describe('unit-d:interfaces/RelativeOptions', () => { + it('should extend ResolveWithOptions', () => { + expectTypeOf().toMatchTypeOf() + }) +}) diff --git a/src/interfaces/__tests__/resolve-with-options.spec-d.mts b/src/interfaces/__tests__/resolve-with-options.spec-d.mts new file mode 100644 index 00000000..6ac8716c --- /dev/null +++ b/src/interfaces/__tests__/resolve-with-options.spec-d.mts @@ -0,0 +1,22 @@ +/** + * @file Unit Tests - ResolveWithOptions + * @module pathe/interfaces/tests/unit-d/ResolveWithOptions + */ + +import type TestSubject from '#interfaces/resolve-with-options' +import type { Cwd } from '@flex-development/pathe' +import type { Nilable } from '@flex-development/tutils' + +describe('unit-d:interfaces/ResolveWithOptions', () => { + it('should match [cwd?: Cwd | null | undefined]', () => { + expectTypeOf() + .toHaveProperty('cwd') + .toEqualTypeOf>() + }) + + it('should match [env?: Partial> | null | undefined]', () => { + expectTypeOf() + .toHaveProperty('env') + .toEqualTypeOf>>>() + }) +}) diff --git a/src/interfaces/__tests__/to-path-options.spec-d.mts b/src/interfaces/__tests__/to-path-options.spec-d.mts new file mode 100644 index 00000000..4b7b3167 --- /dev/null +++ b/src/interfaces/__tests__/to-path-options.spec-d.mts @@ -0,0 +1,20 @@ +/** + * @file Unit Tests - ToPathOptions + * @module pathe/interfaces/tests/unit-d/ToPathOptions + */ + +import type TestSubject from '#interfaces/to-path-options' +import type { + FileUrlToPathOptions, + PlatformOptions +} from '@flex-development/pathe' + +describe('unit-d:interfaces/ToPathOptions', () => { + it('should extend FileUrlToPathOptions', () => { + expectTypeOf().toMatchTypeOf() + }) + + it('should extend PlatformOptions', () => { + expectTypeOf().toMatchTypeOf() + }) +}) diff --git a/src/interfaces/file-url-to-path-options.mts b/src/interfaces/file-url-to-path-options.mts new file mode 100644 index 00000000..86e9b0c4 --- /dev/null +++ b/src/interfaces/file-url-to-path-options.mts @@ -0,0 +1,17 @@ +/** + * @file Interfaces - FileUrlToPathOptions + * @module pathe/interfaces/FileUrlToPathOptions + */ + +import type { PlatformOptions } from '@flex-development/pathe' + +/** + * Options for converting `file:` URLs to paths. + * + * @see {@linkcode PlatformOptions} + * + * @extends {PlatformOptions} + */ +interface FileUrlToPathOptions extends PlatformOptions {} + +export type { FileUrlToPathOptions as default } diff --git a/src/interfaces/index.mts b/src/interfaces/index.mts index 30ff8b65..a7ab238f 100644 --- a/src/interfaces/index.mts +++ b/src/interfaces/index.mts @@ -3,11 +3,17 @@ * @module pathe/interfaces */ +export type { + default as FileUrlToPathOptions +} from '#interfaces/file-url-to-path-options' export type { default as FormatInputPathObject, default as PathObject } from '#interfaces/format-input-path-object' export type { default as ParsedPath } from '#interfaces/parsed-path' +export type { + default as PathToFileUrlOptions +} from '#interfaces/path-to-file-url-options' export type { default as Pathe } from '#interfaces/pathe' export type { default as PlatformOptions } from '#interfaces/platform-options' export type { default as PlatformPath } from '#interfaces/platform-path' @@ -17,3 +23,8 @@ export type { export type { default as WindowsPlatformPath } from '#interfaces/platform-path-windows' +export type { default as RelativeOptions } from '#interfaces/relative-options' +export type { + default as ResolveWithOptions +} from '#interfaces/resolve-with-options' +export type { default as ToPathOptions } from '#interfaces/to-path-options' diff --git a/src/interfaces/path-to-file-url-options.mts b/src/interfaces/path-to-file-url-options.mts new file mode 100644 index 00000000..b16e6b84 --- /dev/null +++ b/src/interfaces/path-to-file-url-options.mts @@ -0,0 +1,22 @@ +/** + * @file Interfaces - PathToFileUrlOptions + * @module pathe/interfaces/PathToFileUrlOptions + */ + +import type { + PlatformOptions, + ResolveWithOptions +} from '@flex-development/pathe' + +/** + * Options for converting paths to `file:` URLs. + * + * @see {@linkcode PlatformOptions} + * @see {@linkcode ResolveWithOptions} + * + * @extends {PlatformOptions} + * @extends {ResolveWithOptions} + */ +interface PathToFileUrlOptions extends PlatformOptions, ResolveWithOptions {} + +export type { PathToFileUrlOptions as default } diff --git a/src/interfaces/pathe.mts b/src/interfaces/pathe.mts index 0e7d3f3d..8b2dbc61 100644 --- a/src/interfaces/pathe.mts +++ b/src/interfaces/pathe.mts @@ -3,23 +3,23 @@ * @module pathe/interfaces/Pathe */ -import type extname from '#lib/extname' import type { - ErrInvalidArgType, ErrInvalidArgValue, ErrInvalidFileUrlHost, ErrInvalidFileUrlPath, ErrInvalidUrlScheme } from '@flex-development/errnode' import type { - Cwd, DeviceRoot, Dot, EmptyString, Ext, - PlatformOptions, + FileUrlToPathOptions, + PathToFileUrlOptions, PosixPlatformPath, - Sep + ResolveWithOptions, + Sep, + ToPathOptions } from '@flex-development/pathe' /** @@ -31,39 +31,117 @@ import type { */ interface Pathe extends PosixPlatformPath { /** - * Append a file extension to `path`. + * Append a file extension to `input`. * * Does nothing if a file extension is not provided, or the - * {@linkcode extname} of `path` is already `ext`. + * {@linkcode extname} of `input` is already `ext`. * - * @param {string} path - * Path to handle + * @this {void} + * + * @param {string} input + * The path or URL string to handle * @param {string | null | undefined} ext - * File extension to add + * The file extension to add * @return {string} - * `path` unmodified or with `ext` appended + * `input` unmodified or with new extension */ - addExt(this: void, path: string, ext: string | null | undefined): string + addExt(this: void, input: string, ext: string | null | undefined): string /** - * Change the file extension of `path`. + * Append a file extension to `url`. * - * Does nothing if a file extension isn't provided. - * If the file extension is an empty string, however, `path`'s file extension - * will be removed. + * Does nothing if a file extension is not provided, or the + * {@linkcode extname} of `url` is already `url`. * - * @param {string} path - * Path to handle + * @this {void} + * + * @param {URL} url + * The {@linkcode URL} to handle + * @param {string | null | undefined} ext + * The file extension to add + * @return {URL} + * `url` unmodified or with new extension + */ + addExt(this: void, url: URL, ext: string | null | undefined): URL + + /** + * Append a file extension to `input`. + * + * Does nothing if a file extension is not provided, or the + * {@linkcode extname} of `input` is already `ext`. + * + * @this {void} + * + * @param {URL | string} input + * The {@linkcode URL}, URL string, or path to handle + * @param {string | null | undefined} ext + * The file extension to add + * @return {URL | string} + * `input` unmodified or with new extension + */ + addExt( + this: void, + input: URL | string, + ext: string | null | undefined + ): URL | string + + /** + * Change the file extension of `input`. + * + * Does nothing if the file extension of `input` is already `ext`. + * + * @this {void} + * + * @param {string} input + * The path or URL string to handle * @param {string | null | undefined} [ext] - * File extension to add - * @return {string} `path` unmodified or with changed file extension - * @throws {TypeError} If `path` is not a string or `ext` is not a string + * The file extension to add + * @return {string} + * `input` unmodified or with changed file extension */ - changeExt(this: void, path: string, ext?: string | null | undefined): string + changeExt(this: void, input: string, ext?: string | null | undefined): string + + /** + * Change the file extension of `url`. + * + * Does nothing if the file extension of `url` is already `ext`. + * + * @this {void} + * + * @param {URL} url + * The {@linkcode URL} to handle + * @param {string | null | undefined} [ext] + * The file extension to add + * @return {URL} + * `url` unmodified or with changed file extension + */ + changeExt(this: void, url: URL, ext?: string | null | undefined): URL + + /** + * Change the file extension of `input`. + * + * Does nothing if the file extension of `input` is already `ext`. + * + * @this {void} + * + * @param {URL | string} input + * The {@linkcode URL}, URL string, or path to handle + * @param {string | null | undefined} [ext] + * The file extension to add + * @return {URL | string} + * `input` unmodified or with changed file extension + */ + changeExt( + this: void, + input: URL | string, + ext?: string | null | undefined + ): URL | string /** * Get the path to the current working directory. * + * @this {void} + * * @return {string} * Absolute path to current working directory */ @@ -79,41 +157,47 @@ interface Pathe extends PosixPlatformPath { readonly dot: Dot /** - * Get a list of file extensions for `path`. + * Get a list of file extensions for `input`. + * + * > 👉 **Note**: If `input` is a {@linkcode URL}, or can be parsed to a + * > `URL`, it will be converted to a path using {@linkcode toPath}. * * @see {@linkcode Ext} * @see {@linkcode extname} * - * @param {string} path - * Path to handle + * @this {void} + * + * @param {URL | string} input + * The {@linkcode URL}, URL string, or path to handle * @return {Ext[]} * List of extensions */ - extnames(path: string): Ext[] + extnames(this: void, input: URL | string): Ext[] /** * Convert a `file:` URL to a path. * - * @see {@linkcode ErrInvalidArgType} * @see {@linkcode ErrInvalidFileUrlHost} * @see {@linkcode ErrInvalidFileUrlPath} * @see {@linkcode ErrInvalidUrlScheme} - * @see {@linkcode PlatformOptions} + * @see {@linkcode FileUrlToPathOptions} + * + * @this {void} * * @param {URL | string} url - * The file URL string or URL object to convert to a path - * @param {PlatformOptions | null | undefined} [options] - * Platform options + * The `file:` URL object or string to convert to a path + * @param {FileUrlToPathOptions | null | undefined} [options] + * Conversion options * @return {string} * `url` as path - * @throws {ErrInvalidArgType} * @throws {ErrInvalidFileUrlHost} * @throws {ErrInvalidFileUrlPath} * @throws {ErrInvalidUrlScheme} */ fileURLToPath( + this: void, url: URL | string, - options?: PlatformOptions | null | undefined + options?: FileUrlToPathOptions | null | undefined ): string /** @@ -122,8 +206,10 @@ interface Pathe extends PosixPlatformPath { * @see {@linkcode EmptyString} * @see {@linkcode Ext} * + * @this {void} + * * @param {string | null | undefined} ext - * File extension to format + * The file extension to format * @return {EmptyString | Ext} * Formatted file extension or empty string */ @@ -134,8 +220,10 @@ interface Pathe extends PosixPlatformPath { * * @see {@linkcode DeviceRoot} * - * @param {unknown} [value] - * Value to check + * @this {void} + * + * @param {unknown} value + * The value to check * @return {value is DeviceRoot} * `true` if `value` is device root, `false` otherwise */ @@ -146,13 +234,27 @@ interface Pathe extends PosixPlatformPath { * * @see {@linkcode Sep} * - * @param {unknown} [value] - * Value to check + * @this {void} + * + * @param {unknown} value + * The value to check * @return {value is Sep} * `true` if `value` is path segment separator, `false` otherwise */ isSep(this: void, value: unknown): value is Sep + /** + * Check if `value` is a {@linkcode URL} or can be parsed to a `URL`. + * + * @this {void} + * + * @param {unknown} value + * The value to check + * @return {value is URL | string} + * `true` if `value` is a `URL` or can be parsed to a `URL` + */ + isURL(this: void, value: unknown): value is URL | string + /** * Convert a file `path` to a `file:` {@linkcode URL}. * @@ -167,35 +269,78 @@ interface Pathe extends PosixPlatformPath { * [419]: https://github.com/whatwg/url/issues/419 * * @see {@linkcode ErrInvalidArgValue} - * @see {@linkcode PlatformOptions} + * @see {@linkcode PathToFileUrlOptions} + * + * @this {void} * - * @param {URL | string} path - * Path to handle - * @param {PlatformOptions | null | undefined} [options] - * Platform options + * @param {string} path + * The path to handle + * @param {PathToFileUrlOptions | null | undefined} [options] + * Conversion options * @return {URL} * `path` as `file:` URL * @throws {ErrInvalidArgValue} */ pathToFileURL( + this: void, path: string, - options?: PlatformOptions | null | undefined + options?: PathToFileUrlOptions | null | undefined ): URL /** - * Remove the file extension of `path`. + * Remove the file extension of `input`. * - * Does nothing if `path` does not end with the provided file extension, or if - * a file extension is not provided. + * Does nothing if `input` does not end with the provided file extension, or + * if a file extension is not provided. * - * @param {string} path - * Path to handle + * @this {void} + * + * @param {string} input + * The path or URL string to handle * @param {string | null | undefined} ext - * File extension to remove + * The file extension to remove * @return {string} - * `path` unmodified or with `ext` removed + * `input` unmodified or with `ext` removed + */ + removeExt(this: void, input: string, ext: string | null | undefined): string + + /** + * Remove the file extension of `url`. + * + * Does nothing if `url` does not end with the provided file extension, or + * if a file extension is not provided. + * + * @this {void} + * + * @param {URL} url + * The {@linkcode URL} to handle + * @param {string | null | undefined} ext + * The file extension to remove + * @return {URL} + * `url` unmodified or with `ext` removed */ - removeExt(this: void, path: string, ext: string | null | undefined): string + removeExt(this: void, url: URL, ext: string | null | undefined): URL + + /** + * Remove the file extension of `input`. + * + * Does nothing if `input` does not end with the provided file extension, or + * if a file extension is not provided. + * + * @this {void} + * + * @param {URL | string} input + * The {@linkcode URL}, URL string, or path to handle + * @param {string | null | undefined} ext + * The file extension to remove + * @return {URL | string} + * `input` unmodified or with `ext` removed + */ + removeExt( + this: void, + input: URL | string, + ext: string | null | undefined + ): URL | string /** * Resolve a sequence of paths or path segments into an absolute path. @@ -218,52 +363,152 @@ interface Pathe extends PosixPlatformPath { * If no `path` segments are passed, the absolute path of the current working * directory is returned. * - * @see {@linkcode Cwd} + * @see {@linkcode ResolveWithOptions} + * + * @this {void} * * @param {ReadonlyArray | string} paths * Sequence of paths or path segments - * @param {Cwd | null | undefined} [cwd] - * Get the path to the current working directory - * @param {Partial> | null | undefined} [env] - * Environment variables + * @param {ResolveWithOptions | null | undefined} [options] + * Resolution options * @return {string} * Absolute path */ resolveWith( this: void, paths: string | readonly string[], - cwd?: Cwd | null | undefined, - env?: Partial> | null | undefined + options?: ResolveWithOptions | null | undefined ): string /** - * Get the root of `path`. + * Get the root of `input`. * - * @param {string} path - * Path to handle + * > 👉 **Note**: If `input` is a {@linkcode URL}, or can be parsed to a + * > `URL`, it will be converted to a path using {@linkcode toPath}. + * + * @this {void} + * + * @param {URL | string} input + * The {@linkcode URL}, URL string, or path to handle * @return {string} - * Root of `path` + * Root of `input` */ - root(this: void, path: string): string + root(this: void, input: URL | string): string /** - * Make `path` POSIX-compliant. + * Convert `input` to a path. * - * This includes: + * @see {@linkcode ToPathOptions} * - * - Converting Windows-style path delimiters (`;`) to POSIX (`:`) - * - Converting Windows-style path segment separators (`\`) to POSIX (`/`) + * @this {void} * - * @see https://nodejs.org/api/path.html#windows-vs-posix - * @see https://nodejs.org/api/path.html#pathdelimiter - * @see https://nodejs.org/api/path.html#pathsep - * - * @param {string} path - * Path to handle + * @param {URL | string} input + * The {@linkcode URL}, URL string, or path to convert + * @param {ToPathOptions | null | undefined} [options] + * Conversion options * @return {string} - * POSIX-compliant `path` + * `input` as path */ - toPosix(this: void, path: string): string + toPath( + this: void, + input: URL | string, + options?: ToPathOptions | null | undefined + ): string + + /** + * Convert a list of inputs to paths. + * + * @see {@linkcode ToPathOptions} + * + * @this {void} + * + * @param {ReadonlyArray} list + * The list of {@linkcode URL}s, URL strings, or paths to convert + * @param {ToPathOptions | null | undefined} [options] + * Conversion options + * @return {string[]} + * List of paths + */ + toPath( + this: void, + list: readonly (URL | string)[], + options?: ToPathOptions | null | undefined + ): string[] + + /** + * Convert inputs to paths. + * + * @see {@linkcode ToPathOptions} + * + * @this {void} + * + * @param {ReadonlyArray | URL | string} value + * The {@linkcode URL}, URL string, or path to convert, + * or list of such values + * @param {ToPathOptions | null | undefined} [options] + * Conversion options + * @return {string[] | string} + * `value` as path or new list of paths + */ + toPath( + this: void, + value: readonly (URL | string)[] | URL | string, + options?: ToPathOptions | null | undefined + ): string[] | string + + /** + * Make separators in `input` POSIX-compliant. + * + * Supports encoded separators (e.g. `%5C`, `%5c`). + * + * @template {URL | string} Input + * The URL or path to handle + * + * @this {void} + * + * @param {Input} input + * The {@linkcode URL}, URL string, or path to handle + * @return {Input} + * `input` with POSIX-compliant separators + */ + toPosix(this: void, input: Input): Input + + /** + * Make separators in `list` POSIX-compliant. + * + * Supports encoded separators (e.g. `%5C`, `%5c`). + * + * @template {(URL | string)[]} List + * The list to handle + * + * @this {void} + * + * @param {List} list + * The list of {@linkcode URL}s, URL strings, or paths to handle + * @return {List} + * `list` with POSIX-compliant separators + */ + toPosix(this: void, list: List): List + + /** + * Make separators in `value` POSIX-compliant. + * + * Supports encoded separators (e.g. `%5C`, `%5c`). + * + * @template {URL | string} Input + * The URL or path to handle + * + * @this {void} + * + * @param {Input | Input[]} value + * The {@linkcode URL}, URL string, or path to handle, or list of such values + * @return {Input | Input[]} + * `value` with POSIX-compliant separators + */ + toPosix( + this: void, + value: Input | Input[] + ): Input | Input[] } export type { Pathe as default } diff --git a/src/interfaces/platform-options.mts b/src/interfaces/platform-options.mts index 2ff1d5f3..48767591 100644 --- a/src/interfaces/platform-options.mts +++ b/src/interfaces/platform-options.mts @@ -1,6 +1,6 @@ /** * @file Interfaces - PlatformOptions - * @module pathe/lib/PlatformOptions + * @module pathe/interfaces/PlatformOptions */ /** diff --git a/src/interfaces/platform-path.mts b/src/interfaces/platform-path.mts index 62f08580..d022e80e 100644 --- a/src/interfaces/platform-path.mts +++ b/src/interfaces/platform-path.mts @@ -5,13 +5,15 @@ import type dot from '#lib/dot' import type resolveWith from '#lib/resolve-with' +import type toPath from '#lib/to-path' import type { - Cwd, Delimiter, EmptyString, Ext, FormatInputPathObject, ParsedPath, + RelativeOptions, + ResolveWithOptions, Sep } from '@flex-development/pathe' import type micromatch from 'micromatch' @@ -53,52 +55,71 @@ interface PlatformPath { readonly win32: PlatformPath /** - * Get the last portion of `path`, similar to the Unix `basename` command. + * Get the last portion of `input`, similar to the Unix `basename` command. * * Trailing [directory separators][sep] are ignored. * + * > 👉 **Note**: If `input` is a {@linkcode URL}, or can be parsed to a + * > `URL`, it will be converted to a path using {@linkcode toPath}. + * * [sep]: https://nodejs.org/api/path.html#pathsep * - * @param {string} path - * Path to handle + * @this {void} + * + * @param {URL | string} input + * The {@linkcode URL}, URL string, or path to handle * @param {string | null | undefined} [suffix] - * Suffix to remove + * The suffix to remove * @return {string} - * Last portion of `path` or empty string + * Last portion of `input` or empty string */ - basename(this: void, path: string, suffix?: string | null | undefined): string + basename( + this: void, + input: URL | string, + suffix?: string | null | undefined + ): string /** - * Get the directory name of `path`, similar to the Unix `dirname` command. + * Get the directory name of `input`, similar to the Unix `dirname` command. * * Trailing [directory separators][sep] are ignored. * + * > 👉 **Note**: If `input` is a {@linkcode URL}, or can be parsed to a + * > `URL`, it will be converted to a path using {@linkcode toPath}. + * * [sep]: https://nodejs.org/api/path.html#pathsep * - * @param {string} path - * Path to handle + * @this {void} + * + * @param {URL | string} input + * The {@linkcode URL}, URL string, or path to handle * @return {string} - * Directory name of `path` + * Directory name of `input` */ - dirname(this: void, path: string): string + dirname(this: void, input: URL | string): string /** - * Get the file extension of `path` from the last occurrence of the `.` (dot) - * character (`.`) to end of the string in the last portion of `path`. + * Get the file extension of `input` from the last occurrence of the `.` (dot) + * character (`.`) to end of the string in the last portion of `input`. * - * If there is no `.` in the last portion of `path`, or if there are no `.` + * If there is no `.` in the last portion of `input`, or if there are no `.` * characters other than the first character of the {@linkcode basename} of - * `path`, an empty string is returned. + * `input`, an empty string is returned. + * + * > 👉 **Note**: If `input` is a {@linkcode URL}, or can be parsed to a + * > `URL`, it will be converted to a path using {@linkcode toPath}. * * @see {@linkcode EmptyString} * @see {@linkcode Ext} * - * @param {string} path - * Path to handle + * @this {void} + * + * @param {URL | string} input + * The {@linkcode URL}, URL string, or path to handle * @return {EmptyString | Ext} - * Extension of `path` or empty string + * Extension of `input` or empty string */ - extname(this: void, path: string): EmptyString | Ext + extname(this: void, input: URL | string): EmptyString | Ext /** * Get a path string from an object. @@ -114,8 +135,10 @@ interface PlatformPath { * * @see {@linkcode FormatInputPathObject} * + * @this {void} + * * @param {FormatInputPathObject | null | undefined} pathObject - * Path object to handle + * The path object to handle * @param {string | null | undefined} [pathObject.base] * File name including extension (if any) * @param {string | null | undefined} [pathObject.dir] @@ -135,14 +158,19 @@ interface PlatformPath { ): string /** - * Determine if `path` is absolute. + * Determine if `input` is absolute. * - * @param {string} path - * Path to check + * > 👉 **Note**: If `input` is a {@linkcode URL}, or can be parsed to a + * > `URL`, it will be converted to a path using {@linkcode toPath}. + * + * @this {void} + * + * @param {URL | string} input + * The {@linkcode URL}, URL string, or path to check * @return {boolean} - * `true` if `path` is absolute, `false` otherwise + * `true` if `input` is absolute, `false` otherwise */ - isAbsolute(this: void, path: string): boolean + isAbsolute(this: void, input: URL | string): boolean /** * Join all path segments in `paths` using {@linkcode sep} as the delimiter @@ -152,30 +180,35 @@ interface PlatformPath { * If the joined path string is a zero-length string, {@linkcode dot} is * returned, representing the current working directory. * + * @this {void} + * * @param {string[]} paths - * Path segment sequence + * The path segment sequence * @return {string} * Path segment sequence as one path */ join(this: void, ...paths: string[]): string /** - * Check if `path` matches `pattern`. + * Check if `input` matches `pattern`. * * @see {@linkcode micromatch.Options} * @see {@linkcode micromatch.isMatch} * - * @param {string} path - * The path to glob-match against + * @this {void} + * + * @param {URL | string} input + * The {@linkcode URL}, URL string, or path to glob-match against * @param {string | string[]} pattern * Glob patterns to use for matching * @param {micromatch.Options | null | undefined} [options] * Options for matching * @return {boolean} - * `true` if `path` matches `pattern`, `false` otherwise + * `true` if `input` matches `pattern`, `false` otherwise */ matchesGlob( - path: string, + this: void, + input: URL | string, pattern: string | string[], options?: micromatch.Options | null | undefined ): boolean @@ -190,31 +223,32 @@ interface PlatformPath { * If `path` is a zero-length string, {@linkcode dot} is returned, * representing the current working directory. * + * @this {void} + * * @param {string} path - * Path to normalize + * The path to normalize * @return {string} * Normalized `path` */ normalize(this: void, path: string): string /** - * Create an object whose properties represent significant elements of `path`. - * Trailing directory separators are ignored. + * Create an object whose properties represent significant elements of + * `input`. Trailing directory separators are ignored. * - * > 👉 **Note**: Like Node.js, when `path` does not have a base (i.e. - * > `'file.mjs'`), `parsedPath.dir` is **not** equivalent to `dirname(path)`. - * > See [`nodejs/node#18655`][18655] for details. - * - * [18655]: https://github.com/nodejs/node#18655 + * > 👉 **Note**: If `input` is a {@linkcode URL}, or can be parsed to a + * > `URL`, it will be converted to a path using {@linkcode toPath}. * * @see {@linkcode ParsedPath} * - * @param {string} path - * Path to handle + * @this {void} + * + * @param {URL | string} input + * The {@linkcode URL}, URL string, or path to parse * @return {ParsedPath} * Significant elements of `path` */ - parse(this: void, path: string): ParsedPath + parse(this: void, input: URL | string): ParsedPath /** * Get the relative path from `from` to `to` based on the current working @@ -226,25 +260,30 @@ interface PlatformPath { * If a zero-length string is passed as `from` or `to`, the current working * directory will be used instead of the zero-length strings. * - * @see {@linkcode Cwd} + * > 👉 **Note**: If `from` or `to` is a {@linkcode URL}, or can be parsed to + * > a `URL`, they'll be converted to paths using {@linkcode toPath}. + * + * @see {@linkcode RelativeOptions} * - * @param {string} from - * Start path - * @param {string} to - * Destination path - * @param {Cwd | null | undefined} [cwd] - * Get the path to the current working directory - * @param {Partial> | null | undefined} [env] - * Environment variables + * @category + * core + * + * @this {void} + * + * @param {URL | string[] | string} from + * Start path, path segments, or URL + * @param {URL | string[] | string} to + * Destination path, path segments, or URL + * @param {RelativeOptions | null | undefined} [options] + * Relative path generation options * @return {string} * Relative path from `from` to `to` */ relative( this: void, - from: string, - to: string, - cwd?: Cwd | null | undefined, - env?: Partial> | null | undefined + from: URL | string[] | string, + to: URL | string[] | string, + options?: RelativeOptions | null | undefined ): string /** @@ -268,6 +307,8 @@ interface PlatformPath { * If no `path` segments are passed, the absolute path of the current working * directory is returned. * + * @this {void} + * * @param {string[]} paths * Sequence of paths or path segments * @return {string} @@ -278,19 +319,29 @@ interface PlatformPath { /** * Get an equivalent [namespace-prefixed path][namespace] for `path`. * - * > 👉 If `path` is not a [drive][drive] or [UNC][unc] path, it will be - * > returned without modifications. + * > 👉 **Note**: If `path` is not a [drive][drive] or [UNC][unc] path, it + * > will be returned without modifications. * * [drive]: https://learn.microsoft.com/windows/win32/fileio/naming-a-file#naming-conventions * [namespace]: https://docs.microsoft.com/windows/desktop/FileIO/naming-a-file#namespaces * [unc]: https://learn.microsoft.com/dotnet/standard/io/file-path-formats#unc-paths * + * @see {@linkcode ResolveWithOptions} + * + * @this {void} + * * @param {string} path - * Path to handle + * The path to handle + * @param {ResolveWithOptions | null | undefined} [options] + * Resolution options * @return {string} * Namespace-prefixed path or `path` without modifications */ - toNamespacedPath(this: void, path: string): string + toNamespacedPath( + this: void, + path: string, + options?: ResolveWithOptions | null | undefined + ): string } export type { PlatformPath as default } diff --git a/src/interfaces/relative-options.mts b/src/interfaces/relative-options.mts new file mode 100644 index 00000000..00973f54 --- /dev/null +++ b/src/interfaces/relative-options.mts @@ -0,0 +1,17 @@ +/** + * @file Interfaces - RelativeOptions + * @module pathe/interfaces/RelativeOptions + */ + +import type { ResolveWithOptions } from '@flex-development/pathe' + +/** + * Relative path generation options. + * + * @see {@linkcode ResolveWithOptions} + * + * @extends {ResolveWithOptions} + */ +interface RelativeOptions extends ResolveWithOptions {} + +export type { RelativeOptions as default } diff --git a/src/interfaces/resolve-with-options.mts b/src/interfaces/resolve-with-options.mts new file mode 100644 index 00000000..dae8e9c0 --- /dev/null +++ b/src/interfaces/resolve-with-options.mts @@ -0,0 +1,25 @@ +/** + * @file Interfaces - ResolveWithOptions + * @module pathe/interfaces/ResolveWithOptions + */ + +import type { Cwd } from '@flex-development/pathe' + +/** + * Path resolution options. + */ +interface ResolveWithOptions { + /** + * Get the path to the current working directory. + * + * @see {@linkcode Cwd} + */ + cwd?: Cwd | null | undefined + + /** + * Environment variables. + */ + env?: Partial> | null | undefined +} + +export type { ResolveWithOptions as default } diff --git a/src/interfaces/to-path-options.mts b/src/interfaces/to-path-options.mts new file mode 100644 index 00000000..3d4545f7 --- /dev/null +++ b/src/interfaces/to-path-options.mts @@ -0,0 +1,22 @@ +/** + * @file Interfaces - ToPathOptions + * @module pathe/interfaces/ToPathOptions + */ + +import type { + FileUrlToPathOptions, + PlatformOptions +} from '@flex-development/pathe' + +/** + * Options for converting values to paths. + * + * @see {@linkcode FileUrlToPathOptions} + * @see {@linkcode PlatformOptions} + * + * @extends {FileUrlToPathOptions} + * @extends {PlatformOptions} + */ +interface ToPathOptions extends FileUrlToPathOptions, PlatformOptions {} + +export type { ToPathOptions as default } diff --git a/src/internal/__tests__/can-parse-url.spec.mts b/src/internal/__tests__/can-parse-url.spec.mts new file mode 100644 index 00000000..4dbd90b8 --- /dev/null +++ b/src/internal/__tests__/can-parse-url.spec.mts @@ -0,0 +1,25 @@ +/** + * @file Unit Tests - canParseURL + * @module pathe/internal/tests/unit/canParseURL + */ + +import testSubject from '#internal/can-parse-url' +import dot from '#lib/dot' +import { posix, win32 } from 'node:path' + +describe('unit:internal/canParseURL', () => { + it.each>([ + ['t' + posix.delimiter + win32.sep + 'package.json'], + [dot.repeat(2) + posix.sep + 'to-path.mts'], + [dot], + [posix.sep + 'package.json'], + [posix.sep], + [win32.sep] + ])('should return `false` if `input` cannot be parsed to URL (%#)', input => { + expect(testSubject(input)).to.be.false + }) + + it('should return `true` if `input` can be parsed to URL', () => { + expect(testSubject(import.meta.url)).to.be.true + }) +}) diff --git a/src/internal/__tests__/is-url.spec.mts b/src/internal/__tests__/is-url-object.spec.mts similarity index 77% rename from src/internal/__tests__/is-url.spec.mts rename to src/internal/__tests__/is-url-object.spec.mts index 46a14383..1e7762ad 100644 --- a/src/internal/__tests__/is-url.spec.mts +++ b/src/internal/__tests__/is-url-object.spec.mts @@ -1,11 +1,11 @@ /** - * @file Unit Tests - isURL - * @module pathe/internal/tests/unit/isURL + * @file Unit Tests - isURLObject + * @module pathe/internal/tests/unit/isURLObject */ -import testSubject from '#internal/is-url' +import testSubject from '#internal/is-url-object' -describe('unit:internal/isURL', () => { +describe('unit:internal/isURLObject', () => { it.each>([ [null], ['https://github.com/flex-development/errnode'], diff --git a/src/internal/__tests__/validate-url-string.spec.mts b/src/internal/__tests__/validate-url-string.spec.mts new file mode 100644 index 00000000..4bfa82bd --- /dev/null +++ b/src/internal/__tests__/validate-url-string.spec.mts @@ -0,0 +1,39 @@ +/** + * @file Unit Tests - validateURLString + * @module pathe/internal/tests/unit/validateURLString + */ + +import testSubject from '#internal/validate-url-string' +import { codes, isNodeError, type NodeError } from '@flex-development/errnode' + +describe('unit:internal/validateURLString', () => { + let name: string + + beforeAll(() => { + name = 'value' + }) + + it('should return `true` if `value` is a `URL`', () => { + expect(testSubject(new URL(import.meta.url), name)).to.be.true + }) + + it('should return `true` if `value` is a string', () => { + expect(testSubject(import.meta.url, name)).to.be.true + }) + + it('should throw if `value` is not a `URL` or string', () => { + // Arrange + let error!: NodeError + + // Act + try { + testSubject(null, name) + } catch (e: unknown) { + error = e as typeof error + } + + // Expect + expect(error).to.satisfy(isNodeError) + expect(error).to.have.property('code', codes.ERR_INVALID_ARG_TYPE) + }) +}) diff --git a/src/internal/can-parse-url.mts b/src/internal/can-parse-url.mts new file mode 100644 index 00000000..a30c4b1a --- /dev/null +++ b/src/internal/can-parse-url.mts @@ -0,0 +1,27 @@ +/** + * @file Internal - canParseURL + * @module pathe/internal/canParseURL + */ + +import { DRIVE_PATH_REGEX } from '#internal/constants' + +/** + * Check if `value` can be parsed to a {@linkcode URL}. + * + * @internal + * + * @param {unknown} value + * The value to check + * @return {boolean} + * `true` if `value` can be parsed to a `URL` + */ +function canParseURL(value: unknown): boolean { + try { + new URL(value as URL | string) + return !(typeof value === 'string' && DRIVE_PATH_REGEX.test(value)) + } catch { + return false + } +} + +export default canParseURL diff --git a/src/internal/is-url.mts b/src/internal/is-url-object.mts similarity index 84% rename from src/internal/is-url.mts rename to src/internal/is-url-object.mts index 40cbf143..d34e0846 100644 --- a/src/internal/is-url.mts +++ b/src/internal/is-url-object.mts @@ -1,7 +1,7 @@ /** - * @file Internal - isURL - * @module pathe/internal/isURL - * @see https://github.com/nodejs/node/blob/v22.8.0/lib/internal/url.js#L756-L773 + * @file Internal - isURLObject + * @module pathe/internal/isURLObject + * @see https://github.com/nodejs/node/blob/v23.2.0/lib/internal/url.js#L755-L772 */ /** @@ -39,12 +39,14 @@ interface URLLike { * The `auth` and `path` properties are checked to distinguish between legacy * url instances and the WHATWG URL object. * + * @internal + * * @param {unknown} value * Value to check * @return {value is URLLike} * `true` if `value` looks like WHATWG URL object, `false` otherwise */ -function isURL(value: unknown): value is URLLike { +function isURLObject(value: unknown): value is URLLike { return Boolean( value !== null && typeof value === 'object' && @@ -61,4 +63,4 @@ function isURL(value: unknown): value is URLLike { ) } -export default isURL +export default isURLObject diff --git a/src/internal/normalize-string.mts b/src/internal/normalize-string.mts index f09c8b08..30314411 100644 --- a/src/internal/normalize-string.mts +++ b/src/internal/normalize-string.mts @@ -7,7 +7,6 @@ import validateString from '#internal/validate-string' import dot from '#lib/dot' import isSep from '#lib/is-sep' import sep from '#lib/sep' -import toPosix from '#lib/to-posix' /** * Normalize `path`. @@ -30,7 +29,6 @@ import toPosix from '#lib/to-posix' */ function normalizeString(path: string, allowAboveRoot: boolean): string { validateString(path, 'path') - path = toPosix(path) /** * Current character in {@link path}. diff --git a/src/internal/validate-string.mts b/src/internal/validate-string.mts index f1a66290..34e04892 100644 --- a/src/internal/validate-string.mts +++ b/src/internal/validate-string.mts @@ -18,7 +18,7 @@ import { * @param {string} name * Name of invalid argument or property * @return {value is string} - * `true` if `value` is a string, `false` otherwise + * `true` if `value` is a string * @throws {ErrInvalidArgType} * If `value` is not a string */ diff --git a/src/internal/validate-url-string.mts b/src/internal/validate-url-string.mts new file mode 100644 index 00000000..0c254578 --- /dev/null +++ b/src/internal/validate-url-string.mts @@ -0,0 +1,34 @@ +/** + * @file Internal - validateURLString + * @module pathe/internal/validateURLString + */ + +import isURLObject from '#internal/is-url-object' +import { + ERR_INVALID_ARG_TYPE, + type ErrInvalidArgType +} from '@flex-development/errnode' + +/** + * Check if `value` is a {@linkcode URL} or string. + * + * @internal + * + * @param {unknown} value + * Value to check + * @param {string} name + * Name of invalid argument or property + * @return {value is URL | string} + * `true` if `value` is a `URL` or string + * @throws {ErrInvalidArgType} + * If `value` is not a `URL` or string + */ +function validateURLString( + value: unknown, + name: string +): value is URL | string { + if (isURLObject(value) || typeof value === 'string') return true + throw new ERR_INVALID_ARG_TYPE(name, ['URL', 'string'], value) +} + +export default validateURLString diff --git a/src/lib/__snapshots__/add-ext.snap b/src/lib/__snapshots__/add-ext.snap deleted file mode 100644 index e0b7f254..00000000 --- a/src/lib/__snapshots__/add-ext.snap +++ /dev/null @@ -1,5 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`unit:lib/addExt > should return \`path\` with new extension ("add-ext", "mjs") 1`] = `"add-ext.mjs"`; - -exports[`unit:lib/addExt > should return \`path\` with new extension ("add-ext.d", ".mts") 1`] = `"add-ext.d.mts"`; diff --git a/src/lib/__snapshots__/change-ext.snap b/src/lib/__snapshots__/change-ext.snap deleted file mode 100644 index f08dc0f3..00000000 --- a/src/lib/__snapshots__/change-ext.snap +++ /dev/null @@ -1,13 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`unit:lib/changeExt > should return \`path\` with changed extension ("change-ext", "mjs") 1`] = `"change-ext.mjs"`; - -exports[`unit:lib/changeExt > should return \`path\` with changed extension ("change-ext.", ".mjs") 1`] = `"change-ext.mjs"`; - -exports[`unit:lib/changeExt > should return \`path\` with changed extension ("change-ext.min.cjs", ".mjs") 1`] = `"change-ext.min.mjs"`; - -exports[`unit:lib/changeExt > should return \`path\` with changed extension ("change-ext.mjs", "") 1`] = `"change-ext"`; - -exports[`unit:lib/changeExt > should return \`path\` with changed extension ("change-ext.mjs", null) 1`] = `"change-ext"`; - -exports[`unit:lib/changeExt > should return \`path\` with changed extension ("change-ext.mts", "d.mts") 1`] = `"change-ext.d.mts"`; diff --git a/src/lib/__snapshots__/extnames.snap b/src/lib/__snapshots__/extnames.snap index 5e8a67f8..c2798e27 100644 --- a/src/lib/__snapshots__/extnames.snap +++ b/src/lib/__snapshots__/extnames.snap @@ -22,6 +22,14 @@ exports[`unit:lib/extnames > should return list of extensions ("eslint.base.conf ] `; +exports[`unit:lib/extnames > should return list of extensions ("file:///tsconfig.lib.prod.json") 1`] = ` +[ + ".lib", + ".prod", + ".json", +] +`; + exports[`unit:lib/extnames > should return list of extensions ("grease.config.mjs") 1`] = ` [ ".config", @@ -29,8 +37,8 @@ exports[`unit:lib/extnames > should return list of extensions ("grease.config.mj ] `; -exports[`unit:lib/extnames > should return list of extensions ("src/lib/extnames.ts") 1`] = ` +exports[`unit:lib/extnames > should return list of extensions ("src/lib/extnames.mts") 1`] = ` [ - ".ts", + ".mts", ] `; diff --git a/src/lib/__snapshots__/remove-ext.snap b/src/lib/__snapshots__/remove-ext.snap deleted file mode 100644 index 25b1dff3..00000000 --- a/src/lib/__snapshots__/remove-ext.snap +++ /dev/null @@ -1,13 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`unit:lib/removeExt > should return \`path\` with extension removed ("remove-ext.d.mts", ".d.mts") 1`] = `"remove-ext"`; - -exports[`unit:lib/removeExt > should return \`path\` with extension removed ("remove-ext.d.mts", "d.mts") 1`] = `"remove-ext"`; - -exports[`unit:lib/removeExt > should return \`path\` with extension removed ("remove-ext.mjs", ".mjs") 1`] = `"remove-ext"`; - -exports[`unit:lib/removeExt > should return \`path\` with extension removed ("remove-ext.mjs", "mjs") 1`] = `"remove-ext"`; - -exports[`unit:lib/removeExt > should return \`path\` with extension removed ("remove-ext.mts", ".mts") 1`] = `"remove-ext"`; - -exports[`unit:lib/removeExt > should return \`path\` with extension removed ("remove-ext.mts", "mts") 1`] = `"remove-ext"`; diff --git a/src/lib/__snapshots__/to-path.snap b/src/lib/__snapshots__/to-path.snap new file mode 100644 index 00000000..dab7b78e --- /dev/null +++ b/src/lib/__snapshots__/to-path.snap @@ -0,0 +1,19 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`unit:lib/toPath > should return \`input\` as path (".") 1`] = `"."`; + +exports[`unit:lib/toPath > should return \`input\` as path ("../to-path.mts") 1`] = `"../to-path.mts"`; + +exports[`unit:lib/toPath > should return \`input\` as path ("/") 1`] = `"/"`; + +exports[`unit:lib/toPath > should return \`input\` as path ("/package.json") 1`] = `"/package.json"`; + +exports[`unit:lib/toPath > should return \`input\` as path ("\\\\") 1`] = `"\\"`; + +exports[`unit:lib/toPath > should return \`input\` as path ("file:///") 1`] = `"/"`; + +exports[`unit:lib/toPath > should return \`input\` as path ("file:///package.json") 1`] = `"/package.json"`; + +exports[`unit:lib/toPath > should return \`input\` as path ("node:test") 1`] = `"test"`; + +exports[`unit:lib/toPath > should return \`input\` as path ("t:\\\\package.json") 1`] = `"t:\\package.json"`; diff --git a/src/lib/__snapshots__/to-posix.snap b/src/lib/__snapshots__/to-posix.snap deleted file mode 100644 index 43728243..00000000 --- a/src/lib/__snapshots__/to-posix.snap +++ /dev/null @@ -1,35 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`unit:lib/toPosix > should return \`path\` with posix-compliant separators (0) 1`] = `"C:."`; - -exports[`unit:lib/toPosix > should return \`path\` with posix-compliant separators (1) 1`] = `"C:/"`; - -exports[`unit:lib/toPosix > should return \`path\` with posix-compliant separators (2) 1`] = `"C:"`; - -exports[`unit:lib/toPosix > should return \`path\` with posix-compliant separators (3) 1`] = `"C:/Windows/system32;C:/Windows;C:/Program Files/node/"`; - -exports[`unit:lib/toPosix > should return \`path\` with posix-compliant separators (4) 1`] = `"C:/abc"`; - -exports[`unit:lib/toPosix > should return \`path\` with posix-compliant separators (5) 1`] = `"C:/another_path/DIR/1/2/33//index"`; - -exports[`unit:lib/toPosix > should return \`path\` with posix-compliant separators (6) 1`] = `"C:/path/dir/index.html"`; - -exports[`unit:lib/toPosix > should return \`path\` with posix-compliant separators (7) 1`] = `"C:abc"`; - -exports[`unit:lib/toPosix > should return \`path\` with posix-compliant separators (8) 1`] = `"//?/UNC"`; - -exports[`unit:lib/toPosix > should return \`path\` with posix-compliant separators (9) 1`] = `"//?/UNC/server/share"`; - -exports[`unit:lib/toPosix > should return \`path\` with posix-compliant separators (10) 1`] = `"//server two/shared folder/file path.zip"`; - -exports[`unit:lib/toPosix > should return \`path\` with posix-compliant separators (11) 1`] = `"//server/share/file_path"`; - -exports[`unit:lib/toPosix > should return \`path\` with posix-compliant separators (12) 1`] = `"//user/admin$/system32"`; - -exports[`unit:lib/toPosix > should return \`path\` with posix-compliant separators (13) 1`] = `"/foo/C:"`; - -exports[`unit:lib/toPosix > should return \`path\` with posix-compliant separators (14) 1`] = `"another_path/DIR with spaces/1/2/33/index"`; - -exports[`unit:lib/toPosix > should return \`path\` with posix-compliant separators (15) 1`] = `"./file"`; - -exports[`unit:lib/toPosix > should return \`path\` with posix-compliant separators (16) 1`] = `"/"`; diff --git a/src/lib/__tests__/add-ext.spec.mts b/src/lib/__tests__/add-ext.spec.mts index 6cb7fa85..1f06dc6a 100644 --- a/src/lib/__tests__/add-ext.spec.mts +++ b/src/lib/__tests__/add-ext.spec.mts @@ -4,21 +4,31 @@ */ import testSubject from '#lib/add-ext' +import formatExt from '#lib/format-ext' +import pathToFileURL from '#lib/path-to-file-url' describe('unit:lib/addExt', () => { - it.each>([ - ['add-ext', 'mjs'], - ['add-ext.d', '.mts'] - ])('should return `path` with new extension (%j, %j)', (path, ext) => { - expect(testSubject(path, ext)).toMatchSnapshot() + it.each<[URL, string | null | undefined]>([ + [pathToFileURL('dist/lib/add-ext.d'), '.mts'], + [pathToFileURL('src/lib/add-ext'), 'mjs'] + ])('should return `input` with new extension (%#)', (input, ext) => { + // Act + const result = testSubject(input, ext) + + // Expect + expect(result).to.eq(input) + expect(result).to.have.property('pathname').with.extname(formatExt(ext)) + expect(result).to.have.property('href').endWith(result.pathname) }) - it.each>([ - ['add-ext', ''], - ['add-ext', null], - ['add-ext.d.mts', 'mts'], - ['add-ext.d.mts', '.mts'] - ])('should return `path` without modications (%j, %j)', (path, ext) => { - expect(testSubject(path, ext)).to.eq(path) + it.each<[URL, string | null | undefined]>([ + [pathToFileURL('dist/lib/add-ext.mjs'), ''], + [pathToFileURL('dist/lib/add-ext.d.mts'), '.mts'] + ])('should return `input` without modications (%#)', (input, ext) => { + // Arrange + const clone: URL = new URL(input) + + // Act + Expect + expect(testSubject(input, ext)).to.eq(input).and.eql(clone) }) }) diff --git a/src/lib/__tests__/basename.spec.mts b/src/lib/__tests__/basename.spec.mts index 4bf66e35..a93d4f44 100644 --- a/src/lib/__tests__/basename.spec.mts +++ b/src/lib/__tests__/basename.spec.mts @@ -1,16 +1,17 @@ /** * @file Unit Tests - basename * @module pathe/lib/tests/unit/basename - * @see https://github.com/nodejs/node/blob/v22.8.0/test/parallel/test-path-basename.js + * @see https://github.com/nodejs/node/blob/v23.2.0/test/parallel/test-path-basename.js */ import testSubject from '#lib/basename' +import toPath from '#lib/to-path' import toPosix from '#lib/to-posix' import { posix, win32 } from 'node:path' describe('unit:lib/basename', () => { describe('posix', () => { - it.each<[string]>([ + it.each<[Parameters[0]]>([ [''], ['//a'], ['/a/b'], @@ -21,12 +22,13 @@ describe('unit:lib/basename', () => { ['/dir/basename.ext'], ['basename.ext'], ['basename.ext/'], - ['basename.ext//'] - ])('should return last portion of `path` (%j)', path => { - expect(testSubject(path)).to.eq(posix.basename(path)) + ['basename.ext//'], + [new URL('file:///test-path-basename.js')] + ])('should return last portion of `input` (%j)', input => { + expect(testSubject(input)).to.eq(posix.basename(toPath(input))) }) - it.each<[string, string]>([ + it.each>([ ['.js', '.js'], ['/aaa/bbb', '/bbb'], ['/aaa/bbb', 'a/bbb'], @@ -46,16 +48,20 @@ describe('unit:lib/basename', () => { ['file.js', '.ts'], ['file.js.old', '.js.old'], ['js', '.js'] - ])('should return last portion of `path` without `suffix` (%j, %j)', ( - path, + ])('should return last portion of `input` without `suffix` (%j, %j)', ( + input, suffix ) => { - expect(testSubject(path, suffix)).to.eq(posix.basename(path, suffix)) + // Act + const result = testSubject(input, suffix) + + // Expect + expect(result).to.eq(posix.basename(toPath(input), suffix!)) }) }) describe('windows', () => { - it.each<[string]>([ + it.each<[Parameters[0]]>([ [''], ['//a'], ['/a/b'], @@ -82,13 +88,13 @@ describe('unit:lib/basename', () => { ['basename.ext//'], ['basename.ext\\'], ['basename.ext\\\\'], - ['file:stream'], - ['foo'] - ])('should return last portion of `path` (%j)', path => { - expect(testSubject(path)).to.eq(toPosix(win32.basename(path))) + ['foo'], + ['node:test/reporters'] + ])('should return last portion of `input` (%j)', input => { + expect(testSubject(input)).to.eq(toPosix(win32.basename(toPath(input)))) }) - it.each<[string, string]>([ + it.each>([ ['.js', '.js'], ['/aaa/bbb', '/bbb'], ['/aaa/bbb', 'a/bbb'], @@ -113,15 +119,15 @@ describe('unit:lib/basename', () => { ['file.js', '.ts'], ['file.js.old', '.js.old'], ['js', '.js'] - ])('should return last portion of `path` without `suffix` (%j, %j)', ( - path, + ])('should return last portion of `input` without `suffix` (%j, %j)', ( + input, suffix ) => { // Act - const result = testSubject(path, suffix) + const result = testSubject(input, suffix) // Expect - expect(result).to.eq(toPosix(win32.basename(path, suffix))) + expect(result).to.eq(toPosix(win32.basename(toPath(input), suffix!))) }) }) }) diff --git a/src/lib/__tests__/change-ext.spec.mts b/src/lib/__tests__/change-ext.spec.mts index 83474520..fec10b3a 100644 --- a/src/lib/__tests__/change-ext.spec.mts +++ b/src/lib/__tests__/change-ext.spec.mts @@ -4,16 +4,36 @@ */ import testSubject from '#lib/change-ext' +import formatExt from '#lib/format-ext' +import pathToFileURL from '#lib/path-to-file-url' +import type { EmptyString } from '@flex-development/pathe' describe('unit:lib/changeExt', () => { - it.each>([ - ['change-ext', 'mjs'], - ['change-ext.', '.mjs'], - ['change-ext.min.cjs', '.mjs'], - ['change-ext.mjs', ''], - ['change-ext.mjs', null], - ['change-ext.mts', 'd.mts'] - ])('should return `path` with changed extension (%j, %j)', (path, ext) => { - expect(testSubject(path, ext)).toMatchSnapshot() + it.each<[URL, string]>([ + [pathToFileURL('/change-ext'), 'mjs'], + [pathToFileURL('/change-ext.'), '.mjs'], + [pathToFileURL('/change-ext.min.cjs'), '.mjs'], + [pathToFileURL('/change-ext.mts'), 'd.mts'] + ])('should return `input` with changed extension (%#)', (input, ext) => { + // Act + const result = testSubject(input, ext) + + // Expect + expect(result).to.eq(input) + expect(result).to.have.property('pathname').endWith(formatExt(ext)) + expect(result).to.have.property('href').endWith(result.pathname) + }) + + it.each<[URL, (EmptyString | null | undefined)?]>([ + [pathToFileURL('/change-ext.cjs'), ''], + [pathToFileURL('/change-ext.mjs'), null] + ])('should return `input` without extension (%#)', (input, ext) => { + // Act + const result = testSubject(input, ext) + + // Expect + expect(result).to.eq(input) + expect(result).to.have.property('pathname').with.extname('') + expect(result).to.have.property('href').endWith(result.pathname) }) }) diff --git a/src/lib/__tests__/cwd.spec.mts b/src/lib/__tests__/cwd.spec.mts index 9b6b1dd1..760386c5 100644 --- a/src/lib/__tests__/cwd.spec.mts +++ b/src/lib/__tests__/cwd.spec.mts @@ -3,11 +3,26 @@ * @module pathe/lib/tests/unit/cwd */ +import DRIVE from '#fixtures/drive' import process from '#internal/process' import testSubject from '#lib/cwd' +import toPosix from '#lib/to-posix' +import cwdWindows from '#tests/utils/cwd-windows' describe('unit:lib/cwd', () => { - it('should return path to current working directory', () => { - expect(testSubject()).to.eq(process.cwd()) + describe('posix', () => { + it('should return path to current working directory', () => { + expect(testSubject()).to.eq(process.cwd()) + }) + }) + + describe('windows', () => { + beforeEach(() => { + vi.spyOn(process, 'cwd').mockImplementation(cwdWindows) + }) + + it('should return path to current working directory', () => { + expect(testSubject()).to.startWith(DRIVE).and.eq(toPosix(process.cwd())) + }) }) }) diff --git a/src/lib/__tests__/dirname.spec.mts b/src/lib/__tests__/dirname.spec.mts index b7a4bbb3..d904490a 100644 --- a/src/lib/__tests__/dirname.spec.mts +++ b/src/lib/__tests__/dirname.spec.mts @@ -1,10 +1,12 @@ /** * @file Unit Tests - dirname * @module pathe/lib/tests/unit/dirname - * @see https://github.com/nodejs/node/blob/v22.8.0/test/parallel/test-path-dirname.js + * @see https://github.com/nodejs/node/blob/v23.2.0/test/parallel/test-path-dirname.js */ import testSubject from '#lib/dirname' +import pathToFileURL from '#lib/path-to-file-url' +import toPath from '#lib/to-path' import toPosix from '#lib/to-posix' import { posix, win32 } from 'node:path' @@ -19,9 +21,10 @@ describe('unit:lib/dirname', () => { ['a'], ['foo'], [posix.sep.repeat(4)], - [posix.sep] - ])('should return directory name of `path` (%j)', path => { - expect(testSubject(path)).to.eq(posix.dirname(path)) + [posix.sep], + [pathToFileURL('/test/dirname.mjs')] + ])('should return directory name of `input` (%j)', input => { + expect(testSubject(input)).to.eq(posix.dirname(toPath(input))) }) }) @@ -60,14 +63,15 @@ describe('unit:lib/dirname', () => { ['c:foo\\bar\\'], ['c:foo\\bar\\baz'], ['dir\\file:stream'], + ['file:///test\\path-dirname.mjs'], ['file:stream'], ['foo'], [posix.sep.repeat(4)], [posix.sep], [win32.sep.repeat(2)], [win32.sep] - ])('should return directory name of `path` (%j)', path => { - expect(testSubject(path)).to.eq(toPosix(win32.dirname(path))) + ])('should return directory name of `input` (%j)', input => { + expect(testSubject(input)).to.eq(toPosix(win32.dirname(toPath(input)))) }) }) }) diff --git a/src/lib/__tests__/extname.spec.mts b/src/lib/__tests__/extname.spec.mts index 72d8bcc3..13740728 100644 --- a/src/lib/__tests__/extname.spec.mts +++ b/src/lib/__tests__/extname.spec.mts @@ -1,10 +1,11 @@ /** * @file Unit Tests - extname * @module pathe/lib/tests/unit/extname - * @see https://github.com/nodejs/node/blob/v22.8.0/test/parallel/test-path-extname.js + * @see https://github.com/nodejs/node/blob/v23.2.0/test/parallel/test-path-extname.js */ import testSubject from '#lib/extname' +import toPath from '#lib/to-path' import toPosix from '#lib/to-posix' import { posix, win32 } from 'node:path' @@ -53,9 +54,10 @@ describe('unit:lib/extname', () => { ['file.ext/'], ['file.ext//'], ['file/'], - ['file//'] - ])('should return extension of `path` (%j)', path => { - expect(testSubject(path)).to.eq(posix.extname(path)) + ['file//'], + [new URL(import.meta.url)] + ])('should return extension of `input` (%j)', input => { + expect(testSubject(input)).to.eq(posix.extname(toPath(input))) }) }) @@ -120,9 +122,10 @@ describe('unit:lib/extname', () => { ['file/'], ['file//'], ['file\\'], - ['file\\\\'] - ])('should return extension of `path` (%j)', path => { - expect(testSubject(path)).to.eq(toPosix(win32.extname(path))) + ['file\\\\'], + [import.meta.url.replaceAll(posix.sep, win32.sep)] + ])('should return extension of `input` (%j)', input => { + expect(testSubject(input)).to.eq(toPosix(win32.extname(toPath(input)))) }) }) }) diff --git a/src/lib/__tests__/extnames.spec.mts b/src/lib/__tests__/extnames.spec.mts index f3f957b9..b8f7cf7f 100644 --- a/src/lib/__tests__/extnames.spec.mts +++ b/src/lib/__tests__/extnames.spec.mts @@ -3,18 +3,21 @@ * @module pathe/lib/tests/unit/extnames */ +import dot from '#lib/dot' import testSubject from '#lib/extnames' +import sep from '#lib/sep' describe('unit:lib/extnames', () => { it.each>([ [''], - ['.'], ['.remarkignore'], ['.remarkrc.mjs'], - ['/'], ['eslint.base.config.mjs'], ['grease.config.mjs'], - ['src/lib/extnames.ts'] + ['src/lib/extnames.mts'], + [dot], + [new URL('file:///tsconfig.lib.prod.json')], + [sep] ])('should return list of extensions (%j)', path => { expect(testSubject(path)).toMatchSnapshot() }) diff --git a/src/lib/__tests__/file-url-to-path.spec.mts b/src/lib/__tests__/file-url-to-path.spec.mts index 4531525f..48b97922 100644 --- a/src/lib/__tests__/file-url-to-path.spec.mts +++ b/src/lib/__tests__/file-url-to-path.spec.mts @@ -1,7 +1,7 @@ /** * @file Unit Tests - fileURLToPath * @module pathe/lib/tests/unit/fileURLToPath - * @see https://github.com/nodejs/node/blob/v22.8.0/test/parallel/test-url-fileurltopath.js + * @see https://github.com/nodejs/node/blob/v23.2.0/test/parallel/test-url-fileurltopath.js */ import process from '#internal/process' @@ -32,28 +32,6 @@ describe('unit:lib/fileURLToPath', () => { expect(error).to.have.property('code', codes.ERR_INVALID_FILE_URL_PATH) }) - it.each<[unknown]>([ - [3], - [null], - [true], - [undefined], - [{}] - ])('should throw if `url` is not an `URL` or string (%#)', url => { - // Arrange - let error!: NodeError - - // Act - try { - testSubject(url as URL | string) - } catch (e: unknown) { - error = e as typeof error - } - - // Expect - expect(error).to.satisfy(isNodeError) - expect(error).to.have.property('code', codes.ERR_INVALID_ARG_TYPE) - }) - it('should throw if `url` protocol is not `file:`', () => { // Arrange let error!: NodeError @@ -118,6 +96,12 @@ describe('unit:lib/fileURLToPath', () => { }) describe('windows', () => { + let windows: true + + beforeAll(() => { + windows = true + }) + it.each>([ ['file:///C:/%E2%82%AC'], ['file:///C:/%F0%9F%9A%80'], @@ -142,9 +126,6 @@ describe('unit:lib/fileURLToPath', () => { ['file:///C:/foo=bar'], ['file://nas/My%20Docs/File.doc'] ])('should return `url` as path (%#)', url => { - // Arrange - const windows: boolean = true - // Act const result = testSubject(url, { windows }) @@ -158,7 +139,7 @@ describe('unit:lib/fileURLToPath', () => { // Act try { - testSubject(new URL('file:///?:/'), { windows: true }) + testSubject(new URL('file:///?:/'), { windows }) } catch (e: unknown) { error = e as typeof error } diff --git a/src/lib/__tests__/format.spec.mts b/src/lib/__tests__/format.spec.mts index 69b7e2cd..930ca22a 100644 --- a/src/lib/__tests__/format.spec.mts +++ b/src/lib/__tests__/format.spec.mts @@ -1,7 +1,7 @@ /** * @file Unit Tests - format * @module pathe/lib/tests/unit/format - * @see https://github.com/nodejs/node/blob/v22.8.0/test/parallel/test-path-parse-format.js + * @see https://github.com/nodejs/node/blob/v23.2.0/test/parallel/test-path-parse-format.js */ import testSubject from '#lib/format' diff --git a/src/lib/__tests__/is-absolute.spec.mts b/src/lib/__tests__/is-absolute.spec.mts index 853d1e22..57d952b1 100644 --- a/src/lib/__tests__/is-absolute.spec.mts +++ b/src/lib/__tests__/is-absolute.spec.mts @@ -1,10 +1,11 @@ /** * @file Unit Tests - isAbsolute * @module pathe/lib/tests/unit/isAbsolute - * @see https://github.com/nodejs/node/blob/v22.8.0/test/parallel/test-path-isabsolute.js + * @see https://github.com/nodejs/node/blob/v23.2.0/test/parallel/test-path-isabsolute.js */ import testSubject from '#lib/is-absolute' +import toPath from '#lib/to-path' import { posix, win32 } from 'node:path' describe('unit:lib/isAbsolute', () => { @@ -12,16 +13,26 @@ describe('unit:lib/isAbsolute', () => { it.each>([ [''], ['./baz'], - ['bar/'] - ])('should return `false` if `path` is not absolute (%j)', path => { - expect(testSubject(path)).to.be.false.and.eq(posix.isAbsolute(path)) + ['bar/'], + [new URL('node:path')] + ])('should return `false` if `input` is not absolute (%j)', input => { + // Act + const result = testSubject(input) + + // Expect + expect(result).to.be.false.and.eq(posix.isAbsolute(toPath(input))) }) it.each>([ ['/home/foo'], - ['/home/foo/..'] - ])('should return `true` if `path` is absolute (%j)', path => { - expect(testSubject(path)).to.be.true.and.eq(posix.isAbsolute(path)) + ['/home/foo/..'], + [new URL('file:///.npmrc')] + ])('should return `true` if `input` is absolute (%j)', input => { + // Act + const result = testSubject(input) + + // Expect + expect(result).to.be.true.and.eq(posix.isAbsolute(toPath(input))) }) }) @@ -36,8 +47,12 @@ describe('unit:lib/isAbsolute', () => { ['c:'], ['directory/directory'], ['directory\\directory'] - ])('should return `false` if `path` is not absolute (%j)', path => { - expect(testSubject(path)).to.be.false.and.eq(win32.isAbsolute(path)) + ])('should return `false` if `input` is not absolute (%j)', input => { + // Act + const result = testSubject(input) + + // Expect + expect(result).to.be.false.and.eq(win32.isAbsolute(toPath(input))) }) it.each>([ @@ -55,8 +70,12 @@ describe('unit:lib/isAbsolute', () => { ['c:/'], ['c://'], ['c:\\'] - ])('should return `true` if `path` is absolute (%j)', path => { - expect(testSubject(path)).to.be.true.and.eq(win32.isAbsolute(path)) + ])('should return `true` if `input` is absolute (%j)', input => { + // Act + const result = testSubject(input) + + // Expect + expect(result).to.be.true.and.eq(win32.isAbsolute(toPath(input))) }) }) }) diff --git a/src/lib/__tests__/is-url.spec.mts b/src/lib/__tests__/is-url.spec.mts new file mode 100644 index 00000000..8a7c147c --- /dev/null +++ b/src/lib/__tests__/is-url.spec.mts @@ -0,0 +1,20 @@ +/** + * @file Unit Tests - isURL + * @module pathe/lib/tests/unit/isURL + */ + +import testSubject from '#lib/is-url' + +describe('unit:lib/isURL', () => { + it('should return `false` if `value` is not url', () => { + expect(testSubject(null)).to.be.false + }) + + it('should return `true` if `value` can be parsed to URL', () => { + expect(testSubject(import.meta.url)).to.be.true + }) + + it('should return `true` if `value` is URL object', () => { + expect(testSubject(new URL(import.meta.url))).to.be.true + }) +}) diff --git a/src/lib/__tests__/join.spec.mts b/src/lib/__tests__/join.spec.mts index 9cef9b62..3f9bd3a3 100644 --- a/src/lib/__tests__/join.spec.mts +++ b/src/lib/__tests__/join.spec.mts @@ -1,7 +1,7 @@ /** * @file Unit Tests - join * @module pathe/lib/tests/unit/join - * @see https://github.com/nodejs/node/blob/v22.8.0/test/parallel/test-path-join.js + * @see https://github.com/nodejs/node/blob/v23.2.0/test/parallel/test-path-join.js */ import dot from '#lib/dot' diff --git a/src/lib/__tests__/matches-glob.functional.spec.mts b/src/lib/__tests__/matches-glob.functional.spec.mts index e463a8ba..7b6f969f 100644 --- a/src/lib/__tests__/matches-glob.functional.spec.mts +++ b/src/lib/__tests__/matches-glob.functional.spec.mts @@ -1,7 +1,7 @@ /** * @file Functional Tests - matchesGlob * @module pathe/lib/tests/functional/matchesGlob - * @see https://github.com/nodejs/node/blob/v22.8.0/test/parallel/test-path-glob.js + * @see https://github.com/nodejs/node/blob/v23.2.0/test/parallel/test-path-glob.js */ import process from '#internal/process' @@ -45,19 +45,19 @@ describe('functional:lib/matchesGlob', () => { ['foo\\bar\\baz\\boo', ['foo\\[bc-r]ar\\baz\\*']], ['foo\\barx\\baz', ['foo/bar[a-z]/baz']], ['foo\\barx\\baz', ['foo\\bar[a-z]\\baz']] - ])('should call `micromatch.isMatch` (%#)', (path, pattern, options) => { - // Arrange - const pat: typeof pattern = Array.isArray(pattern) - ? pattern - : toPosix(pattern) - + ])('should call `micromatch.isMatch` (%#)', (input, pattern, options) => { // Act - testSubject(path, pattern, options) + testSubject(input, pattern, options) // Expect expect(spy).toHaveBeenCalledOnce() - expect(spy.mock.lastCall?.[0]).to.eq(toPosix(path)) - expect(spy.mock.lastCall?.[1]).to.eq(pat) - expect(spy.mock.lastCall?.[2]).to.eql({ ...options, cwd: process.cwd() }) + expect(spy.mock.lastCall?.[0]).to.eq(toPosix(input)) + expect(spy.mock.lastCall?.[1]).to.eq(toPosix(pattern)) + expect(spy.mock.lastCall?.[2]).to.eql({ + ...options, + basename: options?.basename ?? true, + cwd: process.cwd(), + windows: false + }) }) }) diff --git a/src/lib/__tests__/normalize.spec.mts b/src/lib/__tests__/normalize.spec.mts index 29c6cbac..322130b6 100644 --- a/src/lib/__tests__/normalize.spec.mts +++ b/src/lib/__tests__/normalize.spec.mts @@ -1,7 +1,7 @@ /** * @file Unit Tests - normalize * @module pathe/lib/tests/unit/normalize - * @see https://github.com/nodejs/node/blob/v22.8.0/test/parallel/test-path-normalize.js + * @see https://github.com/nodejs/node/blob/v23.2.0/test/parallel/test-path-normalize.js */ import testSubject from '#lib/normalize' diff --git a/src/lib/__tests__/parse.spec.mts b/src/lib/__tests__/parse.spec.mts index 436c1d63..3a98c998 100644 --- a/src/lib/__tests__/parse.spec.mts +++ b/src/lib/__tests__/parse.spec.mts @@ -1,11 +1,12 @@ /** * @file Unit Tests - parse * @module pathe/lib/tests/unit/parse - * @see https://github.com/nodejs/node/blob/v22.8.0/test/parallel/test-path-parse-format.js + * @see https://github.com/nodejs/node/blob/v23.2.0/test/parallel/test-path-parse-format.js */ import dot from '#lib/dot' import testSubject from '#lib/parse' +import toPath from '#lib/to-path' import toPosix from '#lib/to-posix' import { posix, win32 } from 'node:path' @@ -25,14 +26,15 @@ describe('unit:lib/parse', () => { ['/home/user/a dir/another file.zip'], ['/home/user/a$$$dir//another file.zip'], ['/home/user/dir/file.txt'], + ['file:///home/user/dir/file.txt'], ['user/dir/another file.zip'], ['x'], [posix.sep + dot], [posix.sep.repeat(2)], [posix.sep.repeat(3)], [posix.sep] - ])('should return significant elements of `path` (%j)', path => { - expect(testSubject(path)).to.eql(posix.parse(path)) + ])('should return significant elements of `input` (%j)', input => { + expect(testSubject(input)).to.eql(posix.parse(toPath(input))) }) }) @@ -55,8 +57,8 @@ describe('unit:lib/parse', () => { ['another_path\\DIR with spaces\\1\\2\\33\\index'], [dot + '\\file'], [win32.sep] - ])('should return significant elements of `path` (%j)', path => { - expect(testSubject(path)).to.eql(win32.parse(toPosix(path))) + ])('should return significant elements of `input` (%j)', input => { + expect(testSubject(input)).to.eql(win32.parse(toPosix(toPath(input)))) }) }) }) diff --git a/src/lib/__tests__/path-to-file-url.spec.mts b/src/lib/__tests__/path-to-file-url.spec.mts index 5297770e..7b79a0fc 100644 --- a/src/lib/__tests__/path-to-file-url.spec.mts +++ b/src/lib/__tests__/path-to-file-url.spec.mts @@ -1,7 +1,7 @@ /** * @file Unit Tests - pathToFileURL * @module pathe/lib/tests/unit/pathToFileURL - * @see https://github.com/nodejs/node/blob/v22.8.0/test/parallel/test-url-pathtofileurl.js + * @see https://github.com/nodejs/node/blob/v23.2.0/test/parallel/test-url-pathtofileurl.js */ import testSubject from '#lib/path-to-file-url' diff --git a/src/lib/__tests__/relative.spec.mts b/src/lib/__tests__/relative.spec.mts index df42e26a..ae1102a2 100644 --- a/src/lib/__tests__/relative.spec.mts +++ b/src/lib/__tests__/relative.spec.mts @@ -1,16 +1,19 @@ /** * @file Unit Tests - relative * @module pathe/lib/tests/unit/relative - * @see https://github.com/nodejs/node/blob/v22.8.0/test/parallel/test-path-relative.js + * @see https://github.com/nodejs/node/blob/v23.2.0/test/parallel/test-path-relative.js */ +import process from '#internal/process' import testSubject from '#lib/relative' +import toPath from '#lib/to-path' import toPosix from '#lib/to-posix' +import cwdWindows from '#tests/utils/cwd-windows' import { posix, win32 } from 'node:path' describe('unit:lib/relative', () => { describe('posix', () => { - it.each>([ + it.each<[URL | string, URL | string]>([ ['', ''], ['/Users/a/web/b/test/mails', '/Users/a/web/b'], ['/baz', '/baz-quux'], @@ -27,15 +30,24 @@ describe('unit:lib/relative', () => { ['/var/lib', '/var'], ['/var/lib', '/var/apache'], ['/var/lib', '/var/lib'], + ['file:///package.json', new URL('file:///test/index.mjs')], [posix.sep, '/foo'], [posix.sep, '/var/lib'] - ])('should return relative path (%#)', (from, to, cwd) => { - expect(testSubject(from, to, cwd)).to.eq(posix.relative(from, to)) + ])('should return relative path (%j, %j)', (from, to) => { + // Act + const result = testSubject(from, to) + + // Expect + expect(result).to.eq(posix.relative(toPath(from), toPath(to))) }) }) describe('windows', () => { - it.each>([ + beforeEach(() => { + vi.spyOn(process, 'cwd').mockImplementation(cwdWindows) + }) + + it.each<[URL | string, URL | string]>([ ['C:\\', 'C:\\foo'], ['C:\\baz', 'C:\\baz-quux'], ['C:\\baz', '\\\\foo\\bar\\baz'], @@ -51,8 +63,12 @@ describe('unit:lib/relative', () => { ['P:\\', 'package.json'], ['P:\\foo\\bar\\baz\\quux', 'P:\\'], ['P:\\foo\\test', 'P:\\foo\\test\\bar\\package.json'], + ['T:\\foo\\bar', 'T:\\foo\\bar\\baz'], + ['T:\\', '\\package.json'], + ['T:\\', 'package.json'], ['\\\\foo\\bar', '\\\\foo\\bar\\baz'], ['\\\\foo\\bar\\baz', 'C:\\baz'], + ['\\\\foo\\bar\\baz', 'T:\\baz'], ['\\\\foo\\bar\\baz', '\\\\foo\\bar'], ['\\\\foo\\bar\\baz', '\\\\foo\\bar\\baz-quux'], ['\\\\foo\\bar\\baz-quux', '\\\\foo\\bar\\baz'], @@ -68,12 +84,12 @@ describe('unit:lib/relative', () => { ['c:/aaaa/bbbb', 'd:\\'], ['c:/aaaaa/', 'c:/aaaa/cccc'], ['c:/blah\\blah', 'd:/games'] - ])('should return relative path (%#)', (from, to, cwd) => { + ])('should return relative path (%j, %j)', (from, to) => { // Act - const result = testSubject(from, to, cwd, null) + const result = testSubject(from, to) // Expect - expect(result).to.eq(toPosix(win32.relative(from, to))) + expect(result).to.eq(toPosix(win32.relative(toPath(from), toPath(to)))) }) }) }) diff --git a/src/lib/__tests__/remove-ext.spec.mts b/src/lib/__tests__/remove-ext.spec.mts index 6eaf1606..6a7c9e4d 100644 --- a/src/lib/__tests__/remove-ext.spec.mts +++ b/src/lib/__tests__/remove-ext.spec.mts @@ -6,23 +6,33 @@ import testSubject from '#lib/remove-ext' describe('unit:lib/removeExt', () => { - it.each>([ - ['remove-ext.mjs', 'mjs'], - ['remove-ext.mts', 'mts'], - ['remove-ext.mjs', '.mjs'], - ['remove-ext.mts', '.mts'], - ['remove-ext.d.mts', 'd.mts'], - ['remove-ext.d.mts', '.d.mts'] - ])('should return `path` with extension removed (%j, %j)', (path, ext) => { - expect(testSubject(path, ext)).toMatchSnapshot() + it.each<[URL, string | null | undefined]>([ + [new URL('file:///remove-ext.d.mts'), '.d.mts'], + [new URL('file:///remove-ext.d.mts'), 'd.mts'], + [new URL('file:///remove-ext.mjs'), '.mjs'], + [new URL('file:///remove-ext.mjs'), 'mjs'], + [new URL('file:///remove-ext.mts'), '.mts'], + [new URL('file:///remove-ext.mts'), 'mts'] + ])('should return `input` with extension removed (%j, %j)', (input, ext) => { + // Act + const result = testSubject(input, ext) + + // Expect + expect(result).to.eq(input) + expect(result).to.have.property('pathname').with.extname('') + expect(result).to.have.property('href').endWith(result.pathname) }) - it.each>([ - ['remove-ext.cjs', ''], - ['remove-ext.cts', ' '], - ['remove-ext.mjs', null], - ['remove-ext.mts', undefined] - ])('should return `path` without modications (%j, %j)', (path, ext) => { - expect(testSubject(path, ext)).to.eq(path) + it.each<[URL, string | null | undefined]>([ + [new URL('file:///remove-ext.cjs'), ''], + [new URL('file:///remove-ext.cts'), ' '], + [new URL('file:///remove-ext.mjs'), null], + [new URL('file:///remove-ext.mts'), undefined] + ])('should return `input` without modications (%j, %j)', (input, ext) => { + // Arrange + const clone: URL = new URL(input) + + // Act + Expect + expect(testSubject(input, ext)).to.eq(input).and.eql(clone) }) }) diff --git a/src/lib/__tests__/resolve-with.spec.mts b/src/lib/__tests__/resolve-with.spec.mts index ba370131..ec18218d 100644 --- a/src/lib/__tests__/resolve-with.spec.mts +++ b/src/lib/__tests__/resolve-with.spec.mts @@ -1,94 +1,103 @@ /** * @file Unit Tests - resolveWith * @module pathe/lib/tests/unit/resolveWith - * @see https://github.com/nodejs/node/blob/v22.8.0/test/parallel/test-path-resolve.js + * @see https://github.com/nodejs/node/blob/v23.2.0/test/parallel/test-path-resolve.js */ +import DRIVE from '#fixtures/drive' +import process from '#internal/process' import dot from '#lib/dot' import testSubject from '#lib/resolve-with' import toPosix from '#lib/to-posix' +import cwdWindows from '#tests/utils/cwd-windows' +import { ok } from 'devlop' import { posix, win32 } from 'node:path' describe('unit:lib/resolveWith', () => { it('should return "." on `cwd()` failure', () => { // Arrange - process.cwd = () => '' - assert.strictEqual(process.cwd(), '') + vi.spyOn(process, 'cwd').mockImplementation(() => '') // Act + Expect - expect(testSubject([], process.cwd)) + expect(testSubject([], process)) .to.eq(posix.resolve()) .and.eq(win32.resolve()) .and.eq(dot) }) describe('posix', () => { - it.each<[Parameters[0]]>([ + it.each([ [''], ['package.json'], - [['', '']], - [['/foo/bar', './baz', '']], - [['/foo/bar', './baz']], - [['/foo/bar', '/tmp/file/']], - [['/foo/bar', dot.repeat(2), dot, './baz']], - [['/foo/tmp.3/', '../tmp.3/cycles/root.js']], - [['/some/dir', dot, '/absolute/']], - [['/var/lib', '../', 'file/']], - [['/var/lib', '/../', 'file/']], - [['a/b/c/', '../../..']], - [[posix.sep, '', '', '/path']], - [[posix.sep, '/path']], + ['', ''], + ['/foo/bar', './baz', ''], + ['/foo/bar', './baz'], + ['/foo/bar', '/tmp/file/'], + ['/foo/bar', dot.repeat(2), dot, './baz'], + ['/foo/tmp.3/', '../tmp.3/cycles/root.js'], + ['/some/dir', dot, '/absolute/'], + ['/var/lib', '../', 'file/'], + ['/var/lib', '/../', 'file/'], + ['a/b/c/', '../../..'], + [posix.sep, '', '', '/path'], + [posix.sep, '/path'], [dot], [posix.sep] - ])('should return absolute path (%#)', paths => { - // Act - const result = testSubject(paths, process.cwd) - - // Expect - expect(result).to.eq(posix.resolve(...[paths].flat())) + ])('should return absolute path (%#)', (...paths) => { + expect(testSubject(paths)).to.eq(posix.resolve(...paths)) }) }) describe('windows', () => { + beforeEach(() => { + vi.spyOn(process, 'cwd').mockImplementation(cwdWindows) + }) + it.each([ - [], - ['', ''], - [''], ['C:'], ['C:\\'], ['C:\\Windows\\long\\path/mixed', '../..', '..\\..\\reports'], ['C:\\Windows\\path\\only', '..\\..\\reports'], ['C:\\foo\\bar', '.\\baz'], ['C:\\foo\\tmp.3\\', '..\\tmp.3\\cycles\\root.js'], - ['Q:'], - ['Q:\\'], - ['Z:'], - ['Z:\\'], + ['T:\\foo\\tmp.3\\', '..\\tmp.3\\cycles\\root.js'], + ['\\\\.\\PHYSICALDRIVE0'], + ['\\\\?\\PHYSICALDRIVE0'], ['\\\\host\\share\\dir\\file.txt'], ['\\\\server\\share', '..', 'relative\\'], ['\\foo\\bar', '', '\\tmp\\file\\'], ['\\foo\\bar', '.\\baz'], ['\\foo\\bar', '\\tmp\\file\\'], ['\\foo\\bar', dot.repeat(2), dot, '.\\baz'], - ['c:\\', '\\\\'], - ['c:\\', '\\\\\\some\\\\dir'], - ['c:\\', '\\\\dir'], - ['c:\\', '\\\\server\\\\share'], - ['c:\\', '\\\\server\\share'], - ['c:\\blah\\blah', 'd:\\games', 'c:..\\a'], - ['c:\\ignore', 'c:\\some\\file'], - ['c:\\ignore', 'd:\\a\\b\\c\\d', '\\e.exe'], - ['d:', 'file.txt'], - ['d:\\ignore', 'd:some\\dir\\\\'], + ['c:'], + ['c:/', '//'], + ['c:/', '///some//dir'], + ['c:/', '//dir'], + ['c:/', '//server//share'], + ['c:/', '//server/share'], + ['c:/blah\\blah', 'd:/games', 'c:../a'], + ['c:/ignore', 'c:/some/file'], + ['c:/ignore', 'd:\\a/b\\c/d', '\\e.exe'], + ['d:/ignore', 'd:some/dir//'], ['foo', '\\\\host\\share\\dir\\file.txt'], ['package.json'], + ['t:'], + ['t:/', '//'], + ['t:/', '///some//dir'], + ['t:/', '//dir'], + ['t:/', '//server//share'], + ['t:/', '//server/share'], + ['t:/'], + ['t:/blah\\blah', 'd:/games', 't:../a'], + ['t:/ignore', 'c:/some/file'], + ['t:/ignore', 'd:\\a/b\\c/d', '\\e.exe'], [dot] ])('should return absolute path (%#)', (...paths) => { - // Act - const result = testSubject(paths, process.cwd, process.env) + // Arrange + ok(process.cwd().startsWith(DRIVE), 'expected cwd to start with `DRIVE`') - // Expect - expect(result).to.eq(toPosix(win32.resolve(...paths))) + // Act + Expect + expect(testSubject(paths)).to.eq(toPosix(win32.resolve(...paths))) }) }) }) diff --git a/src/lib/__tests__/resolve.functional.spec.mts b/src/lib/__tests__/resolve.functional.spec.mts index e448ab7d..bf20b3d3 100644 --- a/src/lib/__tests__/resolve.functional.spec.mts +++ b/src/lib/__tests__/resolve.functional.spec.mts @@ -3,7 +3,6 @@ * @module pathe/lib/tests/functional/resolve */ -import process from '#internal/process' import testSubject from '#lib/resolve' import * as resolveWith from '#lib/resolve-with' import sep from '#lib/sep' @@ -25,9 +24,7 @@ describe('functional:lib/resolve', () => { // Expect expect(spy).toHaveBeenCalledOnce() - expect(spy.mock.lastCall).to.have.property('length', 3) + expect(spy.mock.lastCall).to.have.property('length', 1) expect(spy.mock.lastCall?.[0]).to.have.ordered.members(paths) - expect(spy.mock.lastCall?.[1]).to.eq(process.cwd) - expect(spy.mock.lastCall?.[2]).to.eq(process.env) }) }) diff --git a/src/lib/__tests__/root.spec.mts b/src/lib/__tests__/root.spec.mts index b912780d..25b46091 100644 --- a/src/lib/__tests__/root.spec.mts +++ b/src/lib/__tests__/root.spec.mts @@ -1,11 +1,13 @@ /** * @file Unit Tests - root * @module pathe/lib/tests/unit/root - * @see https://github.com/nodejs/node/blob/v22.8.0/test/parallel/test-path-parse-format.js + * @see https://github.com/nodejs/node/blob/v23.2.0/test/parallel/test-path-parse-format.js */ +import DRIVE from '#fixtures/drive' import dot from '#lib/dot' import testSubject from '#lib/root' +import toPath from '#lib/to-path' import toPosix from '#lib/to-posix' import { posix, win32 } from 'node:path' @@ -25,26 +27,20 @@ describe('unit:lib/root', () => { ['/home/user/a dir/another file.zip'], ['/home/user/a$$$dir//another file.zip'], ['/home/user/dir/file.txt'], + ['file:'], + [new URL('file:///home/user/dir/file.txt')], ['user/dir/another file.zip'], [posix.sep + dot], [posix.sep.repeat(2)], [posix.sep.repeat(3)], [posix.sep] - ])('should return root of `path` (%j)', path => { - expect(testSubject(path)).to.eql(posix.parse(path).root) + ])('should return root of `input` (%j)', input => { + expect(testSubject(input)).to.eq(posix.parse(toPath(input)).root) }) }) describe('windows', () => { it.each>([ - ['C:' + dot.repeat(2)], - ['C:' + dot], - ['C:' + win32.sep], - ['C:'], - ['C:\\abc'], - ['C:\\another_path\\DIR\\1\\2\\33\\\\index'], - ['C:\\path\\dir\\index.html'], - ['C:abc'], ['\\\\?\\UNC'], ['\\\\?\\UNC\\server\\share'], ['\\\\server two\\shared folder\\file path.zip'], @@ -52,10 +48,17 @@ describe('unit:lib/root', () => { ['\\\\user\\admin$\\system32'], ['\\foo\\C:'], ['another_path\\DIR with spaces\\1\\2\\33\\index'], + [DRIVE + '\\abc'], + [DRIVE + '\\another_path\\DIR\\1\\2\\33\\\\index'], + [DRIVE + '\\path\\dir\\index.html'], + [DRIVE + dot.repeat(2)], + [DRIVE + dot], + [DRIVE + win32.sep], + [DRIVE], [dot + '\\file'], [win32.sep] - ])('should return root `path` (%j)', path => { - expect(testSubject(path)).to.eql(toPosix(win32.parse(path).root)) + ])('should return root `input` (%j)', input => { + expect(testSubject(input)).to.eq(toPosix(win32.parse(toPath(input)).root)) }) }) }) diff --git a/src/lib/__tests__/to-namespaced-path.spec.mts b/src/lib/__tests__/to-namespaced-path.spec.mts index 17686114..27fe5741 100644 --- a/src/lib/__tests__/to-namespaced-path.spec.mts +++ b/src/lib/__tests__/to-namespaced-path.spec.mts @@ -1,27 +1,35 @@ /** * @file Unit Tests - toNamespacedPath * @module pathe/lib/tests/unit/toNamespacedPath - * @see https://github.com/nodejs/node/blob/v22.8.0/test/parallel/test-path-makelong.js + * @see https://github.com/nodejs/node/blob/v23.2.0/test/parallel/test-path-makelong.js */ +import process from '#internal/process' import testSubject from '#lib/to-namespaced-path' import toPosix from '#lib/to-posix' +import cwdWindows from '#tests/utils/cwd-windows' import { win32 } from 'node:path' describe('unit:lib/toNamespacedPath', () => { describe('windows', () => { + beforeEach(() => { + vi.spyOn(process, 'cwd').mockImplementation(cwdWindows) + }) + it.each>([ [''], ['//foo//bar'], ['C:/foo'], ['C:\\foo'], + ['T:/bar'], + ['T:\\bar'], ['\\\\.\\pipe\\somepipe'], ['\\\\?\\UNC\\someserver\\someshare\\somefile'], ['\\\\?\\c:\\Windows/System'], ['\\\\?\\foo'], ['\\\\foo\\bar'], ['\\\\someserver\\someshare\\somefile'] - ])('should return namespace-prefixed path (%#)', path => { + ])('should return namespace-prefixed path (%j)', path => { // Act const result = testSubject(path) diff --git a/src/lib/__tests__/to-path.spec.mts b/src/lib/__tests__/to-path.spec.mts new file mode 100644 index 00000000..8e21ab94 --- /dev/null +++ b/src/lib/__tests__/to-path.spec.mts @@ -0,0 +1,32 @@ +/** + * @file Unit Tests - toPath + * @module pathe/lib/tests/unit/toPath + */ + +import dot from '#lib/dot' +import testSubject from '#lib/to-path' +import { posix, win32 } from 'node:path' + +describe('unit:lib/toPath', () => { + it.each<[URL | string]>([ + ['file:///package.json'], + ['t' + posix.delimiter + win32.sep + 'package.json'], + [dot.repeat(2) + posix.sep + 'to-path.mts'], + [dot], + [new URL('file:')], + [new URL('node:test')], + [posix.sep + 'package.json'], + [posix.sep], + [win32.sep] + ])('should return `input` as path (%j)', input => { + // Arrange + const list: readonly (URL | string)[] = [input] + + // Act + const result = testSubject(list) + + // Expect + expect(result).to.be.an('array').of.length(list.length).and.not.eq(list) + expect(result[0]).toMatchSnapshot() + }) +}) diff --git a/src/lib/__tests__/to-posix.spec.mts b/src/lib/__tests__/to-posix.spec.mts index 6a26ad3e..0926fdd0 100644 --- a/src/lib/__tests__/to-posix.spec.mts +++ b/src/lib/__tests__/to-posix.spec.mts @@ -3,30 +3,22 @@ * @module pathe/lib/tests/unit/toPosix */ -import dot from '#lib/dot' +import { sepWindows } from '#internal/constants' import testSubject from '#lib/to-posix' -import { win32 } from 'node:path' describe('unit:lib/toPosix', () => { - it.each>([ - ['C:' + dot], - ['C:' + win32.sep], - ['C:'], - ['C:\\Windows\\system32;C:\\Windows;C:\\Program Files\\node\\'], - ['C:\\abc'], - ['C:\\another_path\\DIR\\1\\2\\33\\\\index'], - ['C:\\path\\dir\\index.html'], - ['C:abc'], - ['\\\\?\\UNC'], - ['\\\\?\\UNC\\server\\share'], - ['\\\\server two\\shared folder\\file path.zip'], - ['\\\\server\\share\\file_path'], - ['\\\\user\\admin$\\system32'], - ['\\foo\\C:'], - ['another_path\\DIR with spaces\\1\\2\\33\\index'], - [dot + '\\file'], - [win32.sep] - ])('should return `path` with posix-compliant separators (%#)', path => { - expect(testSubject(path)).toMatchSnapshot() + it('should return `value` with posix separators', () => { + // Arrange + const url: URL = new URL('file:///C:\\path%5Cdir%5cindex.html') + const value: [URL] = [url] + + // Act + const result = testSubject(value) + + // Expect + expect(result).to.eq(value) + expect(result[0]).to.eq(url) + expect(result[0].pathname).to.not.include(sepWindows).and.not.match(/%5c/i) + expect(result[0].href).to.endWith(url.pathname) }) }) diff --git a/src/lib/add-ext.mts b/src/lib/add-ext.mts index 8c0d0a5c..24d513b4 100644 --- a/src/lib/add-ext.mts +++ b/src/lib/add-ext.mts @@ -4,14 +4,61 @@ */ import validateString from '#internal/validate-string' +import validateURLString from '#internal/validate-url-string' import extname from '#lib/extname' import formatExt from '#lib/format-ext' +export default addExt + +/** + * Append a file extension to `input`. + * + * Does nothing if a file extension is not provided, or the {@linkcode extname} + * of `input` is already `ext`. + * + * @category + * utils + * + * @this {void} + * + * @param {string} input + * The path or URL string to handle + * @param {string | null | undefined} ext + * The file extension to add + * @return {string} + * `input` unmodified or with new extension + */ +function addExt( + this: void, + input: string, + ext: string | null | undefined +): string + /** - * Append a file extension to `path`. + * Append a file extension to `url`. * * Does nothing if a file extension is not provided, or the {@linkcode extname} - * of `path` is already `ext`. + * of `url` is already `url`. + * + * @category + * utils + * + * @this {void} + * + * @param {URL} url + * The {@linkcode URL} to handle + * @param {string | null | undefined} ext + * The file extension to add + * @return {URL} + * `url` unmodified or with new extension + */ +function addExt(this: void, url: URL, ext: string | null | undefined): URL + +/** + * Append a file extension to `input`. + * + * Does nothing if a file extension is not provided, or the {@linkcode extname} + * of `input` is already `ext`. * * @example * addExt('file', null) // 'file' @@ -25,23 +72,47 @@ import formatExt from '#lib/format-ext' * @category * utils * - * @param {string} path - * Path to handle + * @this {void} + * + * @param {URL | string} input + * The {@linkcode URL}, URL string, or path to handle * @param {string | null | undefined} ext - * File extension to add - * @return {string} - * `path` unmodified or with new extension + * The file extension to add + * @return {URL | string} + * `input` unmodified or with new extension */ -function addExt(path: string, ext: string | null | undefined): string { - validateString(path, 'path') +function addExt( + this: void, + input: URL | string, + ext: string | null | undefined +): URL | string - if (ext !== null && ext !== undefined) { - validateString(ext, 'ext') - ext = formatExt(ext) +/** + * @this {void} + * + * @param {URL | string} input + * The {@linkcode URL}, URL string, or path to handle + * @param {string | null | undefined} ext + * The file extension to add + * @return {URL | string} + * `input` unmodified or with new extension + */ +function addExt( + this: void, + input: URL | string, + ext: string | null | undefined +): URL | string { + validateURLString(input, 'input') + + if (typeof input === 'string') { + if (ext !== null && ext !== undefined) { + validateString(ext, 'ext') + ext = formatExt(ext) + } + + if (ext && extname(input) !== ext) input += ext + return input } - if (!ext || extname(path) === ext) return path - return path + ext + return input.href = addExt(input.href, ext), input } - -export default addExt diff --git a/src/lib/basename.mts b/src/lib/basename.mts index 829443a2..363188cc 100644 --- a/src/lib/basename.mts +++ b/src/lib/basename.mts @@ -5,35 +5,46 @@ import { DRIVE_PATH_REGEX } from '#internal/constants' import validateString from '#internal/validate-string' +import validateURLString from '#internal/validate-url-string' import delimiter from '#lib/delimiter' import isSep from '#lib/is-sep' +import toPath from '#lib/to-path' import toPosix from '#lib/to-posix' /** - * Get the last portion of `path`, similar to the Unix `basename` command. + * Get the last portion of `input`, similar to the Unix `basename` command. * * Trailing [directory separators][sep] are ignored. * + * > 👉 **Note**: If `input` is a {@linkcode URL}, or can be parsed to a `URL`, + * > it will be converted to a path using {@linkcode toPath}. + * * [sep]: https://nodejs.org/api/path.html#pathsep * * @category * core * - * @param {string} path - * Path to handle + * @this {void} + * + * @param {URL | string} input + * The {@linkcode URL}, URL string, or path to handle * @param {string | null | undefined} [suffix] - * Suffix to remove + * The suffix to remove * @return {string} - * Last portion of `path` or empty string + * Last portion of `input` or empty string */ -function basename(path: string, suffix?: string | null | undefined): string { +function basename( + this: void, + input: URL | string, + suffix?: string | null | undefined +): string { if (suffix !== null && suffix !== undefined) { validateString(suffix, 'suffix') suffix = toPosix(suffix) } - validateString(path, 'path') - path = toPosix(path) + validateURLString(input, 'input') + input = toPosix(toPath(input)) /** * Start index of basename. @@ -58,14 +69,14 @@ function basename(path: string, suffix?: string | null | undefined): string { // check for drive path so as not to mistake the next path separator as an // extra separator at the end of the path that can be disregarded - if (DRIVE_PATH_REGEX.test(path)) start = path.indexOf(delimiter) + 1 + if (DRIVE_PATH_REGEX.test(input)) start = input.indexOf(delimiter) + 1 if ( typeof suffix === 'string' && suffix.length && - suffix.length <= path.length + suffix.length <= input.length ) { - if (path === suffix) return '' + if (input === suffix) return '' /** * Start index of file extension. @@ -81,13 +92,13 @@ function basename(path: string, suffix?: string | null | undefined): string { */ let firstNonSlashEnd: number = -1 - for (let i = path.length - 1; i >= start; --i) { + for (let i = input.length - 1; i >= start; --i) { /** * Current character. * * @const {string} char */ - const char: string = path[i]! + const char: string = input[i]! if (isSep(char)) { // stop if a non-trailing path separator was reached @@ -117,10 +128,10 @@ function basename(path: string, suffix?: string | null | undefined): string { } if (start === end) end = firstNonSlashEnd - else if (end === -1) end = path.length + else if (end === -1) end = input.length } else { - for (let i = path.length - 1; i >= start; --i) { - if (isSep(path[i])) { + for (let i = input.length - 1; i >= start; --i) { + if (isSep(input[i])) { if (!separator) { // exit if a non-trailing path separator was reached start = i + 1 @@ -137,7 +148,7 @@ function basename(path: string, suffix?: string | null | undefined): string { if (end === -1) return '' } - return path.slice(start, end) + return input.slice(start, end) } export default basename diff --git a/src/lib/change-ext.mts b/src/lib/change-ext.mts index fdc08e08..773e9b1d 100644 --- a/src/lib/change-ext.mts +++ b/src/lib/change-ext.mts @@ -4,14 +4,61 @@ */ import validateString from '#internal/validate-string' +import validateURLString from '#internal/validate-url-string' import addExt from '#lib/add-ext' import extname from '#lib/extname' import formatExt from '#lib/format-ext' import removeExt from '#lib/remove-ext' import type { EmptyString, Ext } from '@flex-development/pathe' +export default changeExt + +/** + * Change the file extension of `input`. + * + * Does nothing if the file extension of `input` is already `ext`. + * + * @category + * utils + * + * @this {void} + * + * @param {string} input + * The path or URL string to handle + * @param {string | null | undefined} [ext] + * The file extension to add + * @return {string} + * `input` unmodified or with changed file extension + */ +function changeExt( + this: void, + input: string, + ext?: string | null | undefined +): string + +/** + * Change the file extension of `url`. + * + * Does nothing if the file extension of `url` is already `ext`. + * + * @category + * utils + * + * @this {void} + * + * @param {URL} url + * The {@linkcode URL} to handle + * @param {string | null | undefined} [ext] + * The file extension to add + * @return {URL} + * `url` unmodified or with changed file extension + */ +function changeExt(this: void, url: URL, ext?: string | null | undefined): URL + /** - * Change the file extension of `path`. + * Change the file extension of `input`. + * + * Does nothing if the file extension of `input` is already `ext`. * * @example * changeExt('file') // 'file' @@ -27,32 +74,56 @@ import type { EmptyString, Ext } from '@flex-development/pathe' * @category * utils * - * @param {string} path - * Path to handle + * @this {void} + * + * @param {URL | string} input + * The {@linkcode URL}, URL string, or path to handle * @param {string | null | undefined} [ext] - * File extension to add - * @return {string} - * `path` unmodified or with changed file extension + * The file extension to add + * @return {URL | string} + * `input` unmodified or with changed file extension */ -function changeExt(path: string, ext?: string | null | undefined): string { - validateString(path, 'path') +function changeExt( + this: void, + input: URL | string, + ext?: string | null | undefined +): URL | string - if (ext !== null && ext !== undefined) { - validateString(ext, 'ext') - ext = formatExt(ext) - } +/** + * @this {void} + * + * @param {URL | string} input + * The {@linkcode URL}, URL string, or path to handle + * @param {string | null | undefined} [ext] + * The file extension to add + * @return {URL | string} + * `input` unmodified or with changed file extension + */ +function changeExt( + this: void, + input: URL | string, + ext?: string | null | undefined +): URL | string { + validateURLString(input, 'input') - /** - * File extension of {@linkcode path}. - * - * @const {EmptyString | Ext} extension - */ - const extension: EmptyString | Ext = extname(path) + if (typeof input === 'string') { + if (ext !== null && ext !== undefined) { + validateString(ext, 'ext') + ext = formatExt(ext) + } - path = removeExt(path, extension) + /** + * File extension of {@linkcode input}. + * + * @const {EmptyString | Ext} extension + */ + const extension: EmptyString | Ext = extname(input) - if (!ext) return path - return addExt(path, ext) -} + input = removeExt(input, extension) -export default changeExt + if (!ext) return input + return addExt(input, ext) + } + + return input.href = changeExt(input.href, ext), input +} diff --git a/src/lib/cwd.mts b/src/lib/cwd.mts index e4477016..4cbce0cc 100644 --- a/src/lib/cwd.mts +++ b/src/lib/cwd.mts @@ -4,6 +4,7 @@ */ import process from '#internal/process' +import toPosix from '#lib/to-posix' /** * Get the path to the current working directory. @@ -11,11 +12,13 @@ import process from '#internal/process' * @category * utils * + * @this {void} + * * @return {string} * Absolute path to current working directory */ -function cwd(): string { - return process.cwd() +function cwd(this: void): string { + return toPosix(process.cwd()) } export default cwd diff --git a/src/lib/dirname.mts b/src/lib/dirname.mts index e5a21c28..a894a4b0 100644 --- a/src/lib/dirname.mts +++ b/src/lib/dirname.mts @@ -4,34 +4,39 @@ */ import { DRIVE_PATH_REGEX } from '#internal/constants' -import validateString from '#internal/validate-string' +import validateURLString from '#internal/validate-url-string' import delimiter from '#lib/delimiter' import dot from '#lib/dot' import isSep from '#lib/is-sep' import sep from '#lib/sep' +import toPath from '#lib/to-path' import toPosix from '#lib/to-posix' /** - * Get the directory name of `path`, similar to the Unix `dirname` command. + * Get the directory name of `input`, similar to the Unix `dirname` command. * * Trailing [directory separators][sep] are ignored. * + * > 👉 **Note**: If `input` is a {@linkcode URL}, or can be parsed to a `URL`, + * > it will be converted to a path using {@linkcode toPath}. + * * [sep]: https://nodejs.org/api/path.html#pathsep * * @category * core * - * @param {string} path - * Path to handle + * @this {void} + * + * @param {URL | string} input + * The {@linkcode URL}, URL string, or path to handle * @return {string} - * Directory name of `path` + * Directory name of `input` */ -function dirname(path: string): string { - validateString(path, 'path') - path = toPosix(path) +function dirname(this: void, input: URL | string): string { + validateURLString(input, 'input') + input = toPosix(toPath(input)) - if (!path.length) return dot - if (path.length === 1) return isSep(path) ? path : dot + if (input.length <= 1) return isSep(input) ? input : dot /** * Index to begin searching for directory name. @@ -47,41 +52,41 @@ function dirname(path: string): string { */ let rootEnd: number = -1 - if (isSep(path[offset])) { + if (isSep(input[offset])) { rootEnd = offset = 1 - if (isSep(path[offset])) { + if (isSep(input[offset])) { /** - * Current position in {@linkcode path}. + * Current position in {@linkcode input}. * * @var {number} j */ let j: number = offset + 1 /** - * Last visited position in {@linkcode path}. + * Last visited position in {@linkcode input}. * * @var {number} last */ let last: number = j // match 1 or more non-path separators - while (j < path.length && !isSep(path[j])) j++ + while (j < input.length && !isSep(input[j])) j++ - if (j < path.length && j !== last) { + if (j < input.length && j !== last) { last = j // match 1 or more path separators - while (j < path.length && isSep(path[j])) j++ + while (j < input.length && isSep(input[j])) j++ - if (j < path.length && j !== last) { + if (j < input.length && j !== last) { last = j // match 1 or more non-path separators - while (j < path.length && !isSep(path[j])) j++ + while (j < input.length && !isSep(input[j])) j++ // matched UNC root only - if (j === path.length) return path + if (j === input.length) return input // matched UNC root with leftovers. // offset by 1 to include the separator after the UNC root to @@ -90,15 +95,15 @@ function dirname(path: string): string { } } } - } else if (DRIVE_PATH_REGEX.test(path)) { + } else if (DRIVE_PATH_REGEX.test(input)) { /** * Index of character after colon (`:`). * * @const {number} afterColon */ - const afterColon: number = path.indexOf(delimiter, offset) + 1 + const afterColon: number = input.indexOf(delimiter, offset) + 1 - rootEnd = offset = path.length > 2 && isSep(path[afterColon]) + rootEnd = offset = input.length > 2 && isSep(input[afterColon]) ? afterColon + 1 : afterColon } @@ -117,8 +122,8 @@ function dirname(path: string): string { */ let separator: boolean = true - for (let i = path.length - 1; i >= offset; --i) { - if (isSep(path[i])) { + for (let i = input.length - 1; i >= offset; --i) { + if (isSep(input[i])) { if (!separator) { end = i break @@ -131,9 +136,9 @@ function dirname(path: string): string { return end === -1 && rootEnd === -1 ? dot - : path[0] === sep && end === 1 + : input[0] === sep && end === 1 ? sep + sep - : path.slice(0, end === -1 ? rootEnd : end) + : input.slice(0, end === -1 ? rootEnd : end) } export default dirname diff --git a/src/lib/extname.mts b/src/lib/extname.mts index c66e74e7..907ae924 100644 --- a/src/lib/extname.mts +++ b/src/lib/extname.mts @@ -4,19 +4,24 @@ */ import { DRIVE_PATH_REGEX } from '#internal/constants' -import validateString from '#internal/validate-string' +import validateURLString from '#internal/validate-url-string' +import type basename from '#lib/basename' import dot from '#lib/dot' import isSep from '#lib/is-sep' +import toPath from '#lib/to-path' import toPosix from '#lib/to-posix' import type { EmptyString, Ext } from '@flex-development/pathe' /** - * Get the file extension of `path` from the last occurrence of the `.` (dot) - * character (`.`) to end of the string in the last portion of `path`. + * Get the file extension of `input` from the last occurrence of the `.` (dot) + * character (`.`) to end of the string in the last portion of `input`. * - * If there is no `.` in the last portion of `path`, or if there are no `.` + * If there is no `.` in the last portion of `input`, or if there are no `.` * characters other than the first character of the {@linkcode basename} of - * `path`, an empty string is returned. + * `input`, an empty string is returned. + * + * > 👉 **Note**: If `input` is a {@linkcode URL}, or can be parsed to a `URL`, + * > it will be converted to a path using {@linkcode toPath}. * * @see {@linkcode EmptyString} * @see {@linkcode Ext} @@ -24,16 +29,18 @@ import type { EmptyString, Ext } from '@flex-development/pathe' * @category * core * - * @param {string} path - * Path to handle + * @this {void} + * + * @param {URL | string} input + * The {@linkcode URL}, URL string, or path to handle * @return {EmptyString | Ext} - * Extension of `path` or empty string + * Extension of `input` or empty string */ -function extname(path: string): EmptyString | Ext { - validateString(path, 'path') - path = toPosix(path) +function extname(this: void, input: URL | string): EmptyString | Ext { + validateURLString(input, 'input') + input = toPosix(toPath(input)) - if (!path.includes(dot)) return '' + if (!input.includes(dot)) return '' /** * Index to begin searching for extension. @@ -43,7 +50,7 @@ function extname(path: string): EmptyString | Ext { let offset: number = 0 /** - * Start index of {@linkcode path}'s basename. + * Start index of {@linkcode input}'s basename. * * @var {number} part */ @@ -51,7 +58,7 @@ function extname(path: string): EmptyString | Ext { /** * State of characters, if any, before first dot character and after any path - * separators in {@linkcode path}. + * separators in {@linkcode input}. * * @var {number} predot */ @@ -80,18 +87,18 @@ function extname(path: string): EmptyString | Ext { // check for drive path so as not to mistake the next path separator as an // extra separator at the end of the path that can be disregarded - if (path.length >= 2 && DRIVE_PATH_REGEX.test(path)) { + if (input.length >= 2 && DRIVE_PATH_REGEX.test(input)) { offset = part = 2 } // get start and end indices of extension - for (let i = path.length - 1; i >= offset; --i) { + for (let i = input.length - 1; i >= offset; --i) { /** - * Current character in {@linkcode path}. + * Current character in {@linkcode input}. * * @const {string} char */ - const char: string = path[i]! + const char: string = input[i]! if (isSep(char)) { if (!separator) { @@ -131,7 +138,7 @@ function extname(path: string): EmptyString | Ext { return '' } - return path.slice(start, end) as Ext + return input.slice(start, end) as Ext } export default extname diff --git a/src/lib/extnames.mts b/src/lib/extnames.mts index 2e8ea57c..de1e457f 100644 --- a/src/lib/extnames.mts +++ b/src/lib/extnames.mts @@ -3,25 +3,33 @@ * @module pathe/lib/extnames */ -import validateString from '#internal/validate-string' -import dot from '#lib/dot' +import validateURLString from '#internal/validate-url-string' import extname from '#lib/extname' +import toPath from '#lib/to-path' import toPosix from '#lib/to-posix' import type { EmptyString, Ext } from '@flex-development/pathe' /** - * Get a list of file extensions for `path`. + * Get a list of file extensions for `input`. + * + * > 👉 **Note**: If `input` is a {@linkcode URL}, or can be parsed to a `URL`, + * > it will be converted to a path using {@linkcode toPath}. * * @see {@linkcode Ext} * @see {@linkcode extname} * - * @param {string} path - * Path to handle + * @category + * utils + * + * @this {void} + * + * @param {URL | string} input + * The {@linkcode URL}, URL string, or path to handle * @return {Ext[]} * List of extensions */ -function extnames(path: string): Ext[] { - validateString(path, 'path') +function extnames(this: void, input: URL | string): Ext[] { + validateURLString(input, 'input') /** * List of extensions. @@ -35,9 +43,9 @@ function extnames(path: string): Ext[] { * * @var {string} subpath */ - let subpath: string = toPosix(path) + let subpath: string = toPosix(toPath(input)) - while (subpath.includes(dot)) { + while (true) { /** * Current extension. * diff --git a/src/lib/file-url-to-path.mts b/src/lib/file-url-to-path.mts index 153e8a1b..832e1826 100644 --- a/src/lib/file-url-to-path.mts +++ b/src/lib/file-url-to-path.mts @@ -5,68 +5,64 @@ import { isWindows } from '#internal/constants' import domainToUnicode from '#internal/domain-to-unicode' -import isURL from '#internal/is-url' import process from '#internal/process' +import validateURLString from '#internal/validate-url-string' import isDeviceRoot from '#lib/is-device-root' import isSep from '#lib/is-sep' import sep from '#lib/sep' import toPosix from '#lib/to-posix' import { - ERR_INVALID_ARG_TYPE, ERR_INVALID_FILE_URL_HOST, ERR_INVALID_FILE_URL_PATH, ERR_INVALID_URL_SCHEME, - type ErrInvalidArgType, type ErrInvalidFileUrlHost, type ErrInvalidFileUrlPath, type ErrInvalidUrlScheme } from '@flex-development/errnode' -import type { PlatformOptions } from '@flex-development/pathe' +import type { FileUrlToPathOptions } from '@flex-development/pathe' /** * Convert a `file:` URL to a path. * - * @see {@linkcode ErrInvalidArgType} * @see {@linkcode ErrInvalidFileUrlHost} * @see {@linkcode ErrInvalidFileUrlPath} * @see {@linkcode ErrInvalidUrlScheme} - * @see {@linkcode PlatformOptions} + * @see {@linkcode FileUrlToPathOptions} * * @category * utils * + * @this {void} + * * @param {URL | string} url - * The file URL string or URL object to convert to a path - * @param {PlatformOptions | null | undefined} [options] - * Platform options + * The `file:` URL object or string to convert to a path + * @param {FileUrlToPathOptions | null | undefined} [options] + * Conversion options * @return {string} * `url` as path - * @throws {ErrInvalidArgType} * @throws {ErrInvalidFileUrlHost} * @throws {ErrInvalidFileUrlPath} * @throws {ErrInvalidUrlScheme} */ function fileURLToPath( + this: void, url: URL | string, - options?: PlatformOptions | null | undefined + options?: FileUrlToPathOptions | null | undefined ): string { - if (typeof url === 'string') url = new URL(url) + validateURLString(url, 'url') - if (!isURL(url)) { - throw new ERR_INVALID_ARG_TYPE('url', ['string', 'URL'], url) - } - - if (url.protocol !== 'file:') throw new ERR_INVALID_URL_SCHEME('file') + if (!String(url).startsWith('file:')) throw new ERR_INVALID_URL_SCHEME('file') + if (typeof url === 'string') url = new URL(url) /** * URL pathname. * * @var {string} pathname */ - let pathname: string = toPosix(url.pathname) + let pathname: string = toPosix(url).pathname // check for encoded separators - if (/(?:%2f)/i.test(pathname.replace(/(?:%5c)/gi, '%2f'))) { + if (/(?:%2f)/i.test(pathname)) { /** * Error message. * diff --git a/src/lib/format-ext.mts b/src/lib/format-ext.mts index aaed4d4a..9b20d4b5 100644 --- a/src/lib/format-ext.mts +++ b/src/lib/format-ext.mts @@ -4,6 +4,7 @@ */ import validateString from '#internal/validate-string' +import dot from '#lib/dot' import type { EmptyString, Ext } from '@flex-development/pathe' /** @@ -26,20 +27,25 @@ import type { EmptyString, Ext } from '@flex-development/pathe' * @category * utils * + * @this {void} + * * @param {string | null | undefined} ext - * File extension to format + * The file extension to format * @return {EmptyString | Ext} * Formatted file extension or empty string */ -function formatExt(ext: string | null | undefined): EmptyString | Ext { +function formatExt( + this: void, + ext: string | null | undefined +): EmptyString | Ext { if (ext !== null && ext !== undefined) { validateString(ext, 'ext') ext = ext.trim() } if (!ext) return '' - if (ext.startsWith('.')) return ext as Ext - return `.${ext}` + if (ext.startsWith(dot)) return ext as Ext + return `${dot}${ext}` } export default formatExt diff --git a/src/lib/format.mts b/src/lib/format.mts index 60621173..e93d17ab 100644 --- a/src/lib/format.mts +++ b/src/lib/format.mts @@ -27,8 +27,10 @@ import type { FormatInputPathObject } from '@flex-development/pathe' * @category * core * + * @this {void} + * * @param {FormatInputPathObject | null | undefined} pathObject - * Path object to handle + * The path object to handle * @param {string | null | undefined} [pathObject.base] * File name including extension (if any) * @param {string | null | undefined} [pathObject.dir] @@ -42,7 +44,10 @@ import type { FormatInputPathObject } from '@flex-development/pathe' * @return {string} * Path string */ -function format(pathObject: FormatInputPathObject | null | undefined): string { +function format( + this: void, + pathObject: FormatInputPathObject | null | undefined +): string { if (pathObject !== null && pathObject !== undefined) { validateObject(pathObject, 'pathObject') diff --git a/src/lib/index.mts b/src/lib/index.mts index 0b2a4b29..e4f5cd30 100644 --- a/src/lib/index.mts +++ b/src/lib/index.mts @@ -18,6 +18,7 @@ export { default as formatExt } from '#lib/format-ext' export { default as isAbsolute } from '#lib/is-absolute' export { default as isDeviceRoot } from '#lib/is-device-root' export { default as isSep } from '#lib/is-sep' +export { default as isURL } from '#lib/is-url' export { default as join } from '#lib/join' export { default as matchesGlob } from '#lib/matches-glob' export { default as normalize } from '#lib/normalize' @@ -30,4 +31,5 @@ export { default as resolveWith } from '#lib/resolve-with' export { default as root } from '#lib/root' export { default as sep } from '#lib/sep' export { default as toNamespacedPath } from '#lib/to-namespaced-path' +export { default as toPath } from '#lib/to-path' export { default as toPosix } from '#lib/to-posix' diff --git a/src/lib/is-absolute.mts b/src/lib/is-absolute.mts index e26665b7..2b6b2dc7 100644 --- a/src/lib/is-absolute.mts +++ b/src/lib/is-absolute.mts @@ -4,32 +4,45 @@ */ import { DRIVE_PATH_REGEX } from '#internal/constants' -import validateString from '#internal/validate-string' +import validateURLString from '#internal/validate-url-string' import isSep from '#lib/is-sep' +import toPath from '#lib/to-path' /** - * Determine if `path` is absolute. + * Determine if `input` is absolute. + * + * > 👉 **Note**: If `input` is a {@linkcode URL}, or can be parsed to a `URL`, + * > it will be converted to a path using {@linkcode toPath}. * * @example * isAbsolute('') // false * @example * isAbsolute('../') // false * @example - * isAbsolute(process.cwd()) // true + * isAbsolute(cwd()) // true + * @example + * isAbsolute(pathToFileURL(cwd())) // true + * @example + * isAbsolute(new URL('node:path')) // false * * @category * core * - * @param {string} path - * Path to check + * @this {void} + * + * @param {URL | string} input + * The {@linkcode URL}, URL string, or path to check * @return {boolean} - * `true` if `path` is absolute, `false` otherwise + * `true` if `input` is absolute, `false` otherwise */ -function isAbsolute(path: string): boolean { - validateString(path, 'path') - if (!path.length) return false - if (isSep(path[0])) return true - return path.length > 2 && DRIVE_PATH_REGEX.test(path) && isSep(path[2]) +function isAbsolute(this: void, input: URL | string): boolean { + validateURLString(input, 'input') + input = toPath(input) + + if (!input.length) return false + if (isSep(input[0])) return true + + return input.length > 2 && DRIVE_PATH_REGEX.test(input) && isSep(input[2]) } export default isAbsolute diff --git a/src/lib/is-device-root.mts b/src/lib/is-device-root.mts index 86f359e5..72730d4c 100644 --- a/src/lib/is-device-root.mts +++ b/src/lib/is-device-root.mts @@ -15,12 +15,14 @@ import type { DeviceRoot } from '@flex-development/pathe' * @category * utils * - * @param {unknown} [value] - * Value to check + * @this {void} + * + * @param {unknown} value + * The value to check * @return {value is DeviceRoot} * `true` if `value` is device root, `false` otherwise */ -function isDeviceRoot(value: unknown): value is DeviceRoot { +function isDeviceRoot(this: void, value: unknown): value is DeviceRoot { return ( typeof value === 'string' && value.length === 3 && diff --git a/src/lib/is-sep.mts b/src/lib/is-sep.mts index 4f57a8ec..bba9b6e3 100644 --- a/src/lib/is-sep.mts +++ b/src/lib/is-sep.mts @@ -15,12 +15,14 @@ import type { Sep } from '@flex-development/pathe' * @category * utils * - * @param {unknown} [value] - * Value to check + * @this {void} + * + * @param {unknown} value + * The value to check * @return {value is Sep} * `true` if `value` is path segment separator, `false` otherwise */ -function isSep(value: unknown): value is Sep { +function isSep(this: void, value: unknown): value is Sep { return value === sep || value === sepWindows } diff --git a/src/lib/is-url.mts b/src/lib/is-url.mts new file mode 100644 index 00000000..71f445aa --- /dev/null +++ b/src/lib/is-url.mts @@ -0,0 +1,26 @@ +/** + * @file isURL + * @module pathe/lib/isURL + */ + +import canParseURL from '#internal/can-parse-url' +import isURLObject from '#internal/is-url-object' + +/** + * Check if `value` is a {@linkcode URL} or can be parsed to a `URL`. + * + * @category + * utils + * + * @this {void} + * + * @param {unknown} value + * The value to check + * @return {value is URL | string} + * `true` if `value` is a `URL` or can be parsed to a `URL` + */ +function isURL(this: void, value: unknown): value is URL | string { + return isURLObject(value) || canParseURL(value) +} + +export default isURL diff --git a/src/lib/join.mts b/src/lib/join.mts index 30dbce0d..a110209a 100644 --- a/src/lib/join.mts +++ b/src/lib/join.mts @@ -20,12 +20,14 @@ import sep from '#lib/sep' * @category * core * + * @this {void} + * * @param {string[]} paths - * Path segment sequence + * The path segment sequence * @return {string} * Path segment sequence as one path */ -function join(...paths: string[]): string { +function join(this: void, ...paths: string[]): string { if (!paths.length) return dot /** diff --git a/src/lib/matches-glob.mts b/src/lib/matches-glob.mts index 25bd02c8..9484ab10 100644 --- a/src/lib/matches-glob.mts +++ b/src/lib/matches-glob.mts @@ -5,11 +5,12 @@ import process from '#internal/process' import validateString from '#internal/validate-string' +import validateURLString from '#internal/validate-url-string' import toPosix from '#lib/to-posix' import micromatch from 'micromatch' /** - * Check if `path` matches `pattern`. + * Check if `input` matches `pattern`. * * @see {@linkcode micromatch.Options} * @see {@linkcode micromatch.isMatch} @@ -17,21 +18,24 @@ import micromatch from 'micromatch' * @category * core * - * @param {string} path - * The path to glob-match against + * @this {void} + * + * @param {URL | string} input + * The {@linkcode URL}, URL string, or path to glob-match against * @param {string | string[]} pattern * Glob patterns to use for matching * @param {micromatch.Options | null | undefined} [options] * Options for matching * @return {boolean} - * `true` if `path` matches `pattern`, `false` otherwise + * `true` if `input` matches `pattern`, `false` otherwise */ function matchesGlob( - path: string, + this: void, + input: URL | string, pattern: string | string[], options?: micromatch.Options | null | undefined ): boolean { - validateString(path, 'path') + validateURLString(input, 'input') if (Array.isArray(pattern)) { /** @@ -41,25 +45,18 @@ function matchesGlob( */ let i: number = -1 - while (++i < pattern.length) { - /** - * Current pattern. - * - * @const {string} pat - */ - const pat: string = pattern[i]! - - validateString(pat, `pattern[${i}]`) - pattern[i] = toPosix(pat) - } + while (++i < pattern.length) validateString(pattern[i], `pattern[${i}]`) + pattern = toPosix(pattern) } else { validateString(pattern, 'pattern') pattern = toPosix(pattern) } - return micromatch.isMatch(toPosix(path), pattern, { + return micromatch.isMatch(toPosix(String(input)), pattern, { ...options, - cwd: options?.cwd ?? process.cwd() + basename: options?.basename ?? true, + cwd: options?.cwd ?? process.cwd(), + windows: false }) } diff --git a/src/lib/normalize.mts b/src/lib/normalize.mts index 2503d713..77fe4749 100644 --- a/src/lib/normalize.mts +++ b/src/lib/normalize.mts @@ -3,7 +3,7 @@ * @module pathe/lib/normalize */ -import { DRIVE_PATH_REGEX } from '#internal/constants' +import { DRIVE_PATH_REGEX, sepWindows } from '#internal/constants' import normalizeString from '#internal/normalize-string' import validateString from '#internal/validate-string' import dot from '#lib/dot' @@ -25,112 +25,134 @@ import toPosix from '#lib/to-posix' * @category * core * + * @this {void} + * * @param {string} path - * Path to normalize + * The path to normalize * @return {string} * Normalized `path` */ -function normalize(path: string): string { +function normalize(this: void, path: string): string { validateString(path, 'path') - if (!path.length) return dot - path = toPosix(path) - if (path.length === 1) return path - /** - * Absolute path check. + * Normalized path. * - * @const {boolean} absolute + * @var {string} normalized */ - const absolute: boolean = isAbsolute(path) - - /** - * Drive letter or UNC path component(s), if any. - * - * @var {string} device - */ - let device: string = '' - - /** - * End index of root. - * - * @var {number} rootEnd - */ - let rootEnd: number = 0 - - if (isSep(path[rootEnd])) { - rootEnd = 1 - - if (isSep(path[rootEnd])) { - /** - * Current position in {@linkcode path}. - * - * @var {number} j - */ - let j: number = rootEnd + 1 - - /** - * Last visited position in {@linkcode path}. - * - * @var {number} last - */ - let last: number = j - - // match 1 or more non-path separators - while (j < path.length && !isSep(path[j])) j++ - - if (j < path.length && j !== last) { + let normalized: string = '' + + if (path.length <= 1) { + normalized = path || dot + } else { + /** + * Absolute path check. + * + * @var {boolean} absolute + */ + let absolute: boolean = false + + /** + * Drive letter or UNC path component(s), if any. + * + * @var {string} device + */ + let device: string = '' + + /** + * End index of root. + * + * @var {number} rootEnd + */ + let rootEnd: number = 0 + + if (isSep(path[0])) { + absolute = true + + if (!isSep(path[1])) { + rootEnd = 1 + } else { /** - * Possible UNC path component. + * Current position in {@linkcode path}. * - * @const {string} host + * @var {number} j */ - const host: string = path.slice(last, j) + let j: number = 2 - // matched! - last = j + /** + * Last visited position in {@linkcode path}. + * + * @var {number} last + */ + let last: number = j - // match 1 or more path separators - while (j < path.length && isSep(path[j])) j++ + // match 1 or more non-path separators + while (j < path.length && !isSep(path[j])) j++ if (j < path.length && j !== last) { + /** + * Path component. + * + * @const {string} comp + */ + const comp: string = path.slice(last, j) + // matched! last = j - // match 1 or more non-path separators - while (j < path.length && !isSep(path[j])) j++ - - // matched unc root only - if (j === path.length) { - return `${sep}${sep}${host}${sep}${path.slice(last)}${sep}` - } - - // matched unc root with leftovers - if (j !== last) { - device = `${sep}${sep}${host}${sep}${path.slice(last, j)}` - rootEnd = j + // match 1 or more path separators + while (j < path.length && isSep(path[j])) j++ + + if (j < path.length && j !== last) { + // matched! + last = j + + // match 1 or more non-path separators + while (j < path.length && !isSep(path[j])) j++ + + if (j === path.length || j !== last) { + device = sepWindows.repeat(2) + comp + + if (comp === dot || comp === '?') { + // matched device root (i.e. `//./PHYSICALDRIVE0`) + rootEnd = 4 + } else if (j === path.length) { + // matched unc root only: return normalized version of UNC root + // since there is nothing left to process + device += sepWindows + path.slice(last) + sepWindows + return toPosix(device) + } else { + // matched unc root with leftovers + device += sepWindows + path.slice(last, j) + rootEnd = j + } + } } } } + } else if (DRIVE_PATH_REGEX.test(path)) { + device = path.slice(0, rootEnd = 2) + if ((absolute = isAbsolute(path))) rootEnd++ } - } else if (DRIVE_PATH_REGEX.test(path)) { - device = path.slice(0, rootEnd = 2) - if (absolute) rootEnd++ - } - /** - * Tail end of normalized path. - * - * @var {string} tail - */ - let tail: string = rootEnd < path.length - ? normalizeString(path.slice(rootEnd), !absolute) - : '' + /** + * Tail end of normalized path. + * + * @var {string} tail + */ + let tail: string = '' + + if (rootEnd < path.length) { + tail = normalizeString(path.slice(rootEnd), !absolute) + } + + if (!tail.length && !absolute) tail = dot + if (tail.length && isSep(path[path.length - 1])) tail += sep - if (!tail.length && !absolute) tail = dot - if (tail.length && isSep(path[path.length - 1])) tail += sep + normalized = device + (absolute ? sep : '') + tail + } - return `${device}${absolute ? sep : ''}${tail}` + return toPosix(normalized) } export default normalize diff --git a/src/lib/parse.mts b/src/lib/parse.mts index 2595df9d..2e412be8 100644 --- a/src/lib/parse.mts +++ b/src/lib/parse.mts @@ -4,52 +4,59 @@ */ import { DRIVE_PATH_REGEX } from '#internal/constants' -import validateString from '#internal/validate-string' +import validateURLString from '#internal/validate-url-string' import extname from '#lib/extname' import isDeviceRoot from '#lib/is-device-root' import isSep from '#lib/is-sep' import removeExt from '#lib/remove-ext' import root from '#lib/root' +import toPath from '#lib/to-path' import toPosix from '#lib/to-posix' import type { ParsedPath } from '@flex-development/pathe' /** - * Create an object whose properties represent significant elements of `path`. + * Create an object whose properties represent significant elements of `input`. * Trailing directory separators are ignored. * + * > 👉 **Note**: If `input` is a {@linkcode URL}, or can be parsed to a `URL`, + * > it will be converted to a path using {@linkcode toPath}. + * * @see {@linkcode ParsedPath} * * @category * core * - * @param {string} path - * Path to handle + * @this {void} + * + * @param {URL | string} input + * The {@linkcode URL}, URL string, or path to parse * @return {ParsedPath} * Significant elements of `path` */ -function parse(path: string): ParsedPath { - validateString(path, 'path') +function parse(this: void, input: URL | string): ParsedPath { + validateURLString(input, 'input') + input = toPath(input) /** - * Significant elements of {@linkcode path}. + * Significant elements of {@linkcode input}. * * @const {ParsedPath} parsed */ const parsed: ParsedPath = { base: '', dir: '', ext: '', name: '', root: '' } - if (path.length) { - path = toPosix(path) + if (input.length) { + input = toPosix(input) if ( - isSep(path) || - isDeviceRoot(path) || - path.length === 2 && DRIVE_PATH_REGEX.test(path) + isSep(input) || + isDeviceRoot(input) || + input.length === 2 && DRIVE_PATH_REGEX.test(input) ) { - parsed.root = parsed.dir = path - } else if (path.length === 1) { - parsed.base = parsed.name = path + parsed.root = parsed.dir = input + } else if (input.length === 1) { + parsed.base = parsed.name = input } else { - parsed.root = root(path) + parsed.root = root(input) /** * End index of {@linkcode parsed.base}. @@ -73,8 +80,8 @@ function parse(path: string): ParsedPath { let separator: boolean = true // get non-dir info - for (let i = path.length - 1; i >= parsed.root.length; --i) { - if (isSep(path[i])) { + for (let i = input.length - 1; i >= parsed.root.length; --i) { + if (isSep(input[i])) { // reached a path separator that was not part of a set of path // separators at the end of the string if (!separator) { @@ -92,13 +99,13 @@ function parse(path: string): ParsedPath { } if (endBase !== -1) { - parsed.base = path.slice(startBase, endBase) - parsed.ext = extname(path) + parsed.base = input.slice(startBase, endBase) + parsed.ext = extname(input) parsed.name = removeExt(parsed.base, parsed.ext) } parsed.dir = startBase && startBase !== parsed.root.length - ? path.slice(0, startBase - 1) + ? input.slice(0, startBase - 1) : parsed.root } } diff --git a/src/lib/path-to-file-url.mts b/src/lib/path-to-file-url.mts index 1856a46c..ff10a47e 100644 --- a/src/lib/path-to-file-url.mts +++ b/src/lib/path-to-file-url.mts @@ -7,14 +7,14 @@ import { isWindows } from '#internal/constants' import domainToASCII from '#internal/domain-to-ascii' import validateString from '#internal/validate-string' import isSep from '#lib/is-sep' -import resolve from '#lib/resolve' +import resolveWith from '#lib/resolve-with' import sep from '#lib/sep' import toPosix from '#lib/to-posix' import { ERR_INVALID_ARG_VALUE, type ErrInvalidArgValue } from '@flex-development/errnode' -import type { PlatformOptions } from '@flex-development/pathe' +import type { PathToFileUrlOptions } from '@flex-development/pathe' export default pathToFileURL @@ -32,22 +32,25 @@ export default pathToFileURL * [419]: https://github.com/whatwg/url/issues/419 * * @see {@linkcode ErrInvalidArgValue} - * @see {@linkcode PlatformOptions} + * @see {@linkcode PathToFileUrlOptions} * * @category * utils * - * @param {URL | string} path - * Path to handle - * @param {PlatformOptions | null | undefined} [options] - * Platform options + * @this {void} + * + * @param {string} path + * The path to handle + * @param {PathToFileUrlOptions | null | undefined} [options] + * Conversion options * @return {URL} * `path` as `file:` URL * @throws {ErrInvalidArgValue} */ function pathToFileURL( + this: void, path: string, - options?: PlatformOptions | null | undefined + options?: PathToFileUrlOptions | null | undefined ): URL { validateString(path, 'path') path = toPosix(path) @@ -115,7 +118,7 @@ function pathToFileURL( * * @var {string} resolved */ - let resolved: string = resolve(path) + let resolved: string = resolveWith(path, options) // resolve strips trailing slash -> add it back if (isSep(lastChar) && !isSep(resolved[resolved.length - 1])) resolved += sep @@ -139,7 +142,7 @@ function pathToFileURL( * @internal * * @param {string} path - * Path to handle + * The path to handle * @return {string} * `path` with special characters encoded */ diff --git a/src/lib/relative.mts b/src/lib/relative.mts index c0f5adf2..ad5e6331 100644 --- a/src/lib/relative.mts +++ b/src/lib/relative.mts @@ -4,12 +4,13 @@ */ import { DRIVE_PATH_REGEX } from '#internal/constants' -import validateString from '#internal/validate-string' +import validateURLString from '#internal/validate-url-string' import dot from '#lib/dot' import isSep from '#lib/is-sep' import resolveWith from '#lib/resolve-with' import sep from '#lib/sep' -import type { Cwd } from '@flex-development/pathe' +import toPath from '#lib/to-path' +import type { RelativeOptions } from '@flex-development/pathe' export default relative @@ -23,35 +24,45 @@ export default relative * If a zero-length string is passed as `from` or `to`, the current working * directory will be used instead of the zero-length strings. * - * @see {@linkcode Cwd} + * > 👉 **Note**: If `from` or `to` is a {@linkcode URL}, or can be parsed to a + * > `URL`, they'll be converted to paths using {@linkcode toPath}. + * + * @see {@linkcode RelativeOptions} * * @category * core * - * @param {string} from - * Start path - * @param {string} to - * Destination path - * @param {Cwd | null | undefined} [cwd] - * Get the path to the current working directory - * @param {Partial> | null | undefined} [env] - * Environment variables + * @this {void} + * + * @param {URL | string[] | string} from + * Start path, path segments, or URL + * @param {URL | string[] | string} to + * Destination path, path segments, or URL + * @param {RelativeOptions | null | undefined} [options] + * Relative path generation options * @return {string} * Relative path from `from` to `to` */ function relative( - from: string, - to: string, - cwd?: Cwd | null | undefined, - env?: Partial> | null | undefined + this: void, + from: URL | string[] | string, + to: URL | string[] | string, + options?: RelativeOptions | null | undefined ): string { - validateString(from, 'from') - validateString(to, 'to') + if (!Array.isArray(from)) { + validateURLString(from, 'from') + from = toPath(from) + } + + if (!Array.isArray(to)) { + validateURLString(to, 'to') + to = toPath(to) + } if (from === to) return '' - from = resolveWith(from, cwd, env) - to = resolveWith(to, cwd, env) + from = resolveWith(from, options) + to = resolveWith(to, options) if (from.toLowerCase() === to.toLowerCase()) return '' diff --git a/src/lib/remove-ext.mts b/src/lib/remove-ext.mts index fc148252..399ec645 100644 --- a/src/lib/remove-ext.mts +++ b/src/lib/remove-ext.mts @@ -4,14 +4,53 @@ */ import validateString from '#internal/validate-string' +import validateURLString from '#internal/validate-url-string' import formatExt from '#lib/format-ext' +export default removeExt + /** - * Remove the file extension of `path`. + * Remove the file extension of `input`. + * + * Does nothing if `input` does not end with the provided file extension, + * or if a file extension is not provided. + * + * @category + * utils * - * Does nothing if `path` does not end with the provided file extension, or if a + * @param {string} input + * The path or URL string to handle + * @param {string | null | undefined} ext + * The file extension to remove + * @return {string} + * `input` unmodified or with `ext` removed + */ +function removeExt(input: string, ext: string | null | undefined): string + +/** + * Remove the file extension of `url`. + * + * Does nothing if `url` does not end with the provided file extension, or if a * file extension is not provided. * + * @category + * utils + * + * @param {URL} url + * The {@linkcode URL} to handle + * @param {string | null | undefined} ext + * The file extension to remove + * @return {URL} + * `url` unmodified or with `ext` removed + */ +function removeExt(url: URL, ext: string | null | undefined): URL + +/** + * Remove the file extension of `input`. + * + * Does nothing if `input` does not end with the provided file extension, or if + * a file extension is not provided. + * * @example * removeExt('file') // 'file' * @example @@ -24,23 +63,41 @@ import formatExt from '#lib/format-ext' * @category * utils * - * @param {string} path - * Path to handle + * @param {URL | string} input + * The {@linkcode URL}, URL string, or path to handle * @param {string | null | undefined} ext - * File extension to remove - * @return {string} - * `path` unmodified or with `ext` removed + * The file extension to remove + * @return {URL | string} + * `input` unmodified or with `ext` removed */ -function removeExt(path: string, ext: string | null | undefined): string { - validateString(path, 'path') +function removeExt( + input: URL | string, + ext: string | null | undefined +): URL | string - if (ext !== null && ext !== undefined) { - validateString(ext, 'ext') - ext = formatExt(ext) +/** + * @param {URL | string} input + * The {@linkcode URL}, URL string, or path to handle + * @param {string | null | undefined} ext + * The file extension to remove + * @return {URL | string} + * `input` unmodified or with `ext` removed + */ +function removeExt( + input: URL | string, + ext: string | null | undefined +): URL | string { + validateURLString(input, 'input') + + if (typeof input === 'string') { + if (ext !== null && ext !== undefined) { + validateString(ext, 'ext') + ext = formatExt(ext) + } + + if (!ext || !input.endsWith(ext)) return input + return input.slice(0, input.lastIndexOf(ext)) } - if (!ext || !path.endsWith(ext)) return path - return path.slice(0, path.lastIndexOf(ext)) + return input.href = removeExt(input.href, ext), input } - -export default removeExt diff --git a/src/lib/resolve-with.mts b/src/lib/resolve-with.mts index af3bcb62..cbc569c4 100644 --- a/src/lib/resolve-with.mts +++ b/src/lib/resolve-with.mts @@ -3,7 +3,7 @@ * @module pathe/lib/resolveWith */ -import { DRIVE_PATH_REGEX } from '#internal/constants' +import { DRIVE_PATH_REGEX, sepWindows } from '#internal/constants' import normalizeString from '#internal/normalize-string' import process from '#internal/process' import validateString from '#internal/validate-string' @@ -11,7 +11,7 @@ import dot from '#lib/dot' import isSep from '#lib/is-sep' import sep from '#lib/sep' import toPosix from '#lib/to-posix' -import type { Cwd } from '@flex-development/pathe' +import type { Cwd, ResolveWithOptions } from '@flex-development/pathe' /** * Resolve a sequence of paths or path segments into an absolute path. @@ -34,25 +34,39 @@ import type { Cwd } from '@flex-development/pathe' * If no `path` segments are passed, the absolute path of the current working * directory is returned. * - * @see {@linkcode Cwd} + * @see {@linkcode ResolveWithOptions} * * @category * utils * + * @this {void} + * * @param {ReadonlyArray | string} paths * Sequence of paths or path segments - * @param {Cwd | null | undefined} [cwd] - * Get the path to the current working directory - * @param {Partial> | null | undefined} [env] - * Environment variables + * @param {ResolveWithOptions | null | undefined} [options] + * Resolution options * @return {string} * Absolute path */ function resolveWith( + this: void, paths: string | readonly string[], - cwd?: Cwd | null | undefined, - env?: Partial> | null | undefined + options?: ResolveWithOptions | null | undefined ): string { + /** + * Get the path to the current working directory. + * + * @var {Cwd | null | undefined} cwd + */ + let cwd: Cwd | null | undefined = options?.cwd + + /** + * Environment variables. + * + * @var {Partial> | null | undefined} env + */ + let env: Partial> | null | undefined = options?.env + if (typeof cwd !== 'function') cwd = process.cwd if (typeof env !== 'object' || env === null) env = process.env if (typeof paths === 'string') paths = [paths] @@ -89,12 +103,11 @@ function resolveWith( if (i >= 0) { path = paths[i]! validateString(path, `paths[${i}]`) - path = toPosix(path) // skip empty path segments if (!path.length) continue } else if (!resolvedDevice.length) { - path = toPosix(cwd()) + path = cwd() } else { /* * Windows has the concept of drive-specific current working directories. @@ -109,7 +122,7 @@ function resolveWith( * If a cwd was found, but doesn't point to the drive, we default to the * drive's root. */ - path = toPosix(env[`=${resolvedDevice}`] || cwd()) + path = env[`=${resolvedDevice}`] || cwd() // default to drive root if cwd was found but does not point to drive if ( @@ -119,7 +132,7 @@ function resolveWith( isSep(path[2]) ) ) { - path = `${resolvedDevice}` + path = resolvedDevice + sep } } @@ -144,11 +157,17 @@ function resolveWith( */ let rootEnd: number = 0 - if (isSep(path[rootEnd])) { + if (path.length === 1) { + if (isSep(path)) { + absolute = true + rootEnd = 1 + } + } else if (isSep(path[0])) { absolute = true - rootEnd++ - if (isSep(path[rootEnd])) { + if (!isSep(path[1])) { + rootEnd = 1 + } else { /** * Current position in {@linkcode path}. * @@ -168,27 +187,37 @@ function resolveWith( if (j < path.length && j !== last) { /** - * Possible UNC path component. + * Path component. * - * @const {string} host + * @const {string} comp */ - const host: string = path.slice(last, j) + const comp: string = path.slice(last, j) + // matched! last = j // match 1 or more path separators while (j < path.length && isSep(path[j])) j++ if (j < path.length && j !== last) { + // matched! last = j // match 1 or more non-path separators while (j < path.length && !isSep(path[j])) j++ - // matched unc root + // matched device root or unc root if (j === path.length || j !== last) { - device = `${sep}${sep}${host}${sep}${path.slice(last, j)}` - rootEnd = j + device = sepWindows.repeat(2) + comp + + if (comp !== dot && comp !== '?') { + // matched unc root + device += sepWindows + path.slice(last, j) + rootEnd = j + } else { + // matched device root (i.e. `//./PHYSICALDRIVE0`) + rootEnd = 4 + } } } } @@ -198,8 +227,9 @@ function resolveWith( device = path.slice(0, rootEnd) if (path.length > rootEnd && isSep(path[rootEnd])) { - rootEnd++ + // treat separator after drive name as absolute path indicator absolute = true + rootEnd++ } } @@ -225,9 +255,11 @@ function resolveWith( // normalized anyway in case of `cwd()` failure. resolvedTail = normalizeString(resolvedTail, !resolvedAbsolute) - return resolvedAbsolute - ? `${resolvedDevice}${sep}${resolvedTail}` - : `${resolvedDevice}${resolvedTail}` || dot + return toPosix( + resolvedAbsolute + ? `${resolvedDevice}${sep}${resolvedTail}` + : `${resolvedDevice}${resolvedTail}` || dot + ) } export default resolveWith diff --git a/src/lib/resolve.mts b/src/lib/resolve.mts index 4f89701c..cba48155 100644 --- a/src/lib/resolve.mts +++ b/src/lib/resolve.mts @@ -3,7 +3,6 @@ * @module pathe/lib/resolve */ -import process from '#internal/process' import resolveWith from '#lib/resolve-with' /** @@ -30,13 +29,15 @@ import resolveWith from '#lib/resolve-with' * @category * core * + * @this {void} + * * @param {string[]} paths * Sequence of paths or path segments * @return {string} * Absolute path */ -function resolve(...paths: string[]): string { - return resolveWith(paths, process.cwd, process.env) +function resolve(this: void, ...paths: string[]): string { + return resolveWith(paths) } export default resolve diff --git a/src/lib/root.mts b/src/lib/root.mts index b0d4d96f..7ba6bb8d 100644 --- a/src/lib/root.mts +++ b/src/lib/root.mts @@ -4,95 +4,108 @@ */ import { DRIVE_PATH_REGEX } from '#internal/constants' -import validateString from '#internal/validate-string' +import validateURLString from '#internal/validate-url-string' import delimiter from '#lib/delimiter' import isSep from '#lib/is-sep' +import toPath from '#lib/to-path' import toPosix from '#lib/to-posix' /** - * Get the root of `path`. + * Get the root of `input`. + * + * > 👉 **Note**: If `input` is a {@linkcode URL}, or can be parsed to a `URL`, + * > it will be converted to a path using {@linkcode toPath}. * * @category * utils * - * @param {string} path - * Path to handle + * @this {void} + * + * @param {URL | string} input + * The {@linkcode URL}, URL string, or path to handle * @return {string} - * Root of `path` + * Root of `input` */ -function root(path: string): string { - validateString(path, 'path') - - if (!path.length) return path - path = toPosix(path) - - // `path` is just a separator, exit early to avoid unnecessary work - if (path.length === 1 && isSep(path)) return path - - /** - * End index of root. - * - * @var {number} rootEnd - */ - let rootEnd: number = 0 - - if (isSep(path[rootEnd])) { - rootEnd = 1 - - if (isSep(path[rootEnd])) { - /** - * Current position in {@linkcode path}. - * - * @var {number} j - */ - let j: number = rootEnd + 1 - - /** - * Last visited position in {@linkcode path}. - * - * @var {number} last - */ - let last: number = j - - // match 1 or more non-path separators - while (j < path.length && !isSep(path[j])) j++ - - if (j < path.length && j !== last) { - last = j - - // match 1 or more path separators - while (j < path.length && isSep(path[j])) j++ - - if (j < path.length && j !== last) { +function root(this: void, input: URL | string): string { + validateURLString(input, 'input') + input = toPath(input) + + if (input.length) { + /** + * Boolean indicating {@linkcode input} is just a root. + * + * @var {boolean} onlyRoot + */ + let onlyRoot: boolean = false + + /** + * End index of root. + * + * @var {number} rootEnd + */ + let rootEnd: number = 0 + + if (isSep(input[rootEnd])) { + rootEnd = 1 + + if (isSep(input)) { + // `input` is just a separator + onlyRoot = true + } else if (isSep(input[rootEnd])) { + /** + * Current position in {@linkcode input}. + * + * @var {number} j + */ + let j: number = rootEnd + 1 + + /** + * Last visited position in {@linkcode input}. + * + * @var {number} last + */ + let last: number = j + + // match 1 or more non-path separators + while (j < input.length && !isSep(input[j])) j++ + + if (j < input.length && j !== last) { last = j - // match 1 or more non-path separators - while (j < path.length && !isSep(path[j])) j++ + // match 1 or more path separators + while (j < input.length && isSep(input[j])) j++ + + if (j < input.length && j !== last) { + last = j + + // match 1 or more non-path separators + while (j < input.length && !isSep(input[j])) j++ - // matched UNC root only - if (j === path.length) rootEnd = j - // matched UNC root with leftovers. - // offset by 1 to include the separator after the UNC root to - // treat it as a "normal root" on top of a (UNC) root - else if (j !== last) rootEnd = j + 1 + // matched UNC root only + if (j === input.length) rootEnd = j + // matched UNC root with leftovers. + // offset by 1 to include the separator after the UNC root to + // treat it as a "normal root" on top of a (UNC) root + else if (j !== last) rootEnd = j + 1 + } } } + } else if (DRIVE_PATH_REGEX.test(input)) { + rootEnd = input.indexOf(delimiter) + 1 + + if (input.length <= rootEnd) { + // `input` is just a drive root + onlyRoot = true + } else if (isSep(input[rootEnd])) { + // `input` is just a device root + if (input.length === ++rootEnd) onlyRoot = true + } } - } else if (DRIVE_PATH_REGEX.test(path)) { - rootEnd = path.indexOf(delimiter) + 1 - - // `path` is just a drive root, exit early to avoid unnecessary work - if (path.length <= rootEnd) return path - if (isSep(path[rootEnd])) { - rootEnd++ - - // `path` is just a device root, exit early to avoid unnecessary work - if (path.length === rootEnd) return path - } + if (rootEnd) return toPosix(onlyRoot ? input : input.slice(0, rootEnd)) } - return rootEnd ? path.slice(0, rootEnd) : '' + return '' } export default root diff --git a/src/lib/to-namespaced-path.mts b/src/lib/to-namespaced-path.mts index a4a33e55..93002dc4 100644 --- a/src/lib/to-namespaced-path.mts +++ b/src/lib/to-namespaced-path.mts @@ -7,28 +7,39 @@ import { DRIVE_PATH_REGEX } from '#internal/constants' import validateString from '#internal/validate-string' import dot from '#lib/dot' import isSep from '#lib/is-sep' -import resolve from '#lib/resolve' +import resolveWith from '#lib/resolve-with' import toPosix from '#lib/to-posix' +import type { ResolveWithOptions } from '@flex-development/pathe' /** * Get an equivalent [namespace-prefixed path][namespace] for `path`. * - * > 👉 If `path` is not a [drive][drive] or [UNC][unc] path, it will be - * > returned without modifications. + * > 👉 **Note**: If `path` is not a [drive][drive] or [UNC][unc] path, it will + * > be returned without modifications. * * [drive]: https://learn.microsoft.com/windows/win32/fileio/naming-a-file#naming-conventions * [namespace]: https://docs.microsoft.com/windows/desktop/FileIO/naming-a-file#namespaces * [unc]: https://learn.microsoft.com/dotnet/standard/io/file-path-formats#unc-paths * + * @see {@linkcode ResolveWithOptions} + * * @category * core * + * @this {void} + * * @param {string} path - * Path to handle + * The path to handle + * @param {ResolveWithOptions | null | undefined} [options] + * Resolution options * @return {string} * Namespace-prefixed path or `path` without modifications */ -function toNamespacedPath(path: string): string { +function toNamespacedPath( + this: void, + path: string, + options?: ResolveWithOptions | null | undefined +): string { validateString(path, 'path') if (path) { @@ -39,7 +50,7 @@ function toNamespacedPath(path: string): string { * * @const {string} resolved */ - const resolved: string = resolve(path) + const resolved: string = resolveWith(path, options) if (resolved.length > 2) { // matched non-long unc root -> convert the path to long unc path @@ -48,12 +59,12 @@ function toNamespacedPath(path: string): string { isSep(resolved[1]) && !['?', dot].includes(resolved[2]!) ) { - return `${toPosix('\\\\?\\UNC\\')}${resolved.slice(2)}` + return toPosix('\\\\?\\UNC\\') + resolved.slice(2) } // matched device root -> convert path to long unc path if (DRIVE_PATH_REGEX.test(resolved) && isSep(resolved[2])) { - return `${toPosix('\\\\?\\')}${resolved}` + return toPosix('\\\\?\\') + resolved } } diff --git a/src/lib/to-path.mts b/src/lib/to-path.mts new file mode 100644 index 00000000..757f5d96 --- /dev/null +++ b/src/lib/to-path.mts @@ -0,0 +1,113 @@ +/** + * @file toPath + * @module pathe/lib/toPath + */ + +import canParseURL from '#internal/can-parse-url' +import validateURLString from '#internal/validate-url-string' +import fileURLToPath from '#lib/file-url-to-path' +import type { ToPathOptions } from '@flex-development/pathe' + +export default toPath + +/** + * Convert `input` to a path. + * + * @see {@linkcode ToPathOptions} + * + * @category + * utils + * + * @this {void} + * + * @param {URL | string} input + * The {@linkcode URL}, URL string, or path to convert + * @param {ToPathOptions | null | undefined} [options] + * Conversion options + * @return {string} + * `input` as path + */ +function toPath( + this: void, + input: URL | string, + options?: ToPathOptions | null | undefined +): string + +/** + * Convert a list of inputs to paths. + * + * @see {@linkcode ToPathOptions} + * + * @category + * utils + * + * @this {void} + * + * @param {ReadonlyArray} list + * The list of {@linkcode URL}s, URL strings, or paths to convert + * @param {ToPathOptions | null | undefined} [options] + * Conversion options + * @return {string[]} + * List of paths + */ +function toPath( + this: void, + list: readonly (URL | string)[], + options?: ToPathOptions | null | undefined +): string[] + +/** + * Convert inputs to paths. + * + * @see {@linkcode ToPathOptions} + * + * @category + * utils + * + * @this {void} + * + * @param {ReadonlyArray | URL | string} value + * The {@linkcode URL}, URL string, or path to convert, or list of such values + * @param {ToPathOptions | null | undefined} [options] + * Conversion options + * @return {string[] | string} + * `value` as path or new list of paths + */ +function toPath( + this: void, + value: readonly (URL | string)[] | URL | string, + options?: ToPathOptions | null | undefined +): string[] | string + +/** + * @this {void} + * + * @param {ReadonlyArray | URL | string} value + * The {@linkcode URL}, URL string, or path to convert, or list of such values + * @param {ToPathOptions | null | undefined} [options] + * Conversion options + * @return {string[] | string} + * `value` as path or new list of paths + */ +function toPath( + this: void, + value: readonly (URL | string)[] | URL | string, + options?: ToPathOptions | null | undefined +): string[] | string { + if (Array.isArray(value)) { + return value.map((input, i) => { + return validateURLString(input, `value[${i}]`), toPath(input) + }) + } + + validateURLString(value, 'input') + + if (typeof value === 'string') { + if (!canParseURL(value)) return value + value = new URL(value) + } + + if (value.protocol === 'file:') return fileURLToPath(value, options) + + return value.pathname +} diff --git a/src/lib/to-posix.mts b/src/lib/to-posix.mts index a4426ec8..23896b4f 100644 --- a/src/lib/to-posix.mts +++ b/src/lib/to-posix.mts @@ -3,23 +3,116 @@ * @module pathe/lib/toPosix */ -import validateString from '#internal/validate-string' +import validateURLString from '#internal/validate-url-string' import sep from '#lib/sep' +export default toPosix + /** - * Make separators in `path` POSIX-compliant. + * Make separators in `input` POSIX-compliant. + * + * Supports encoded separators (e.g. `%5C`, `%5c`). * * @category * utils * - * @param {string} path - * Path to handle - * @return {string} - * `path` with POSIX-compliant separators + * @template {URL | string} Input + * The URL or path to handle + * + * @this {void} + * + * @param {Input} input + * The {@linkcode URL}, URL string, or path to handle + * @return {Input} + * `input` with POSIX-compliant separators */ -function toPosix(path: string): string { - validateString(path, 'path') - return path.replace(/\\/g, sep) -} +function toPosix(this: void, input: Input): Input -export default toPosix +/** + * Make separators in `list` POSIX-compliant. + * + * Supports encoded separators (e.g. `%5C`, `%5c`). + * + * @category + * utils + * + * @template {(URL | string)[]} List + * The list to handle + * + * @this {void} + * + * @param {List} list + * The list of {@linkcode URL}s, URL strings, or paths to handle + * @return {List} + * `list` with POSIX-compliant separators + */ +function toPosix(this: void, list: List): List + +/** + * Make separators in `value` POSIX-compliant. + * + * Supports encoded separators (e.g. `%5C`, `%5c`). + * + * @category + * utils + * + * @template {URL | string} Input + * The URL or path to handle + * + * @this {void} + * + * @param {Input | Input[]} value + * The {@linkcode URL}, URL string, or path to handle, or list of such values + * @return {Input | Input[]} + * `value` with POSIX-compliant separators + */ +function toPosix( + this: void, + value: Input | Input[] +): Input | Input[] + +/** + * @this {void} + * + * @param {(URL | string)[] | URL | string} value + * The {@linkcode URL}, URL string, or path to handle, or list of such values + * @return {(URL | string)[] | URL | string} + * `value` with POSIX-compliant separators + */ +function toPosix( + this: void, + value: (URL | string)[] | URL | string +): (URL | string)[] | URL | string { + if (Array.isArray(value)) { + /** + * Current index in {@linkcode value}. + * + * @var {number} i + */ + let i: number = -1 + + while (++i < value.length) { + /** + * The URL, URL string, or path to handle. + * + * @const {URL | string} input + */ + const input: URL | string = value[i]! + + validateURLString(input, `value[${i}]`) + value[i] = toPosix(input) + } + + return value + } + + validateURLString(value, 'value') + + if (typeof value === 'string') { + return value.replace(/\\/g, sep) + .replace(/(?:%5C)/g, '%2F') + .replace(/(?:%5c)/g, '%2f') + } + + return value.href = toPosix(value.href), value +} diff --git a/src/pathe.mts b/src/pathe.mts index 99272f69..b248c30d 100644 --- a/src/pathe.mts +++ b/src/pathe.mts @@ -20,6 +20,7 @@ import { isAbsolute, isDeviceRoot, isSep, + isURL, join, matchesGlob, normalize, @@ -32,6 +33,7 @@ import { root, sep, toNamespacedPath, + toPath, toPosix } from '#lib' import type { @@ -119,6 +121,7 @@ const pathe: Pathe = { isAbsolute, isDeviceRoot, isSep, + isURL, join, matchesGlob, normalize, @@ -132,6 +135,7 @@ const pathe: Pathe = { root, sep, toNamespacedPath, + toPath, toPosix, win32 } diff --git a/tsconfig.typecheck.json b/tsconfig.typecheck.json index 80c9485e..f6427ac9 100644 --- a/tsconfig.typecheck.json +++ b/tsconfig.typecheck.json @@ -9,6 +9,7 @@ "files": [ "typings/@faker-js/faker/global.d.ts", "typings/@types/node/process.d.ts", + "typings/chai/index.d.ts", "typings/punycode.js/index.d.ts", "typings/typescript/lib.es5.d.ts", "vitest-env.d.mts" diff --git a/typings/chai/index.d.ts b/typings/chai/index.d.ts new file mode 100644 index 00000000..295c3b84 --- /dev/null +++ b/typings/chai/index.d.ts @@ -0,0 +1,53 @@ +import type { EmptyString, Ext } from '@flex-development/pathe' + +declare global { + namespace Chai { + interface Assertion { + /** + * Execute an expression and check expectations. + * + * @see {@linkcode AssertionError} + * + * @param {unknown} expression + * The expression to be tested + * @param {Function | string} message + * Message to display if expression test fails, or a function that + * returns such a message + * @param {Function | string} negatedMessage + * Message to display if negated expression test fails, or a function + * that returns such a message + * @param {unknown} [expected] + * Expected value + * @param {unknown} [actual] + * Expresssion test result + * @param {boolean | undefined} [showDiff] + * Display diff in addition to message if expression test fails + * @return {undefined} + * @throws {AssertionError} + */ + assert( + expression: unknown, + message: Function | string, + negatedMessage: Function | string, + expected: unknown, + actual?: unknown, + showDiff?: boolean | undefined + ): undefined + + /** + * Assert the return value of [`path.extname`][extname]. + * + * [extname]: https://nodejs.org/api/path.html#pathextnamepath + * + * @see {@linkcode EmptyString} + * @see {@linkcode Ext} + * + * @param {EmptyString | Ext} ext + * Expected file extension + * @return {Assertion} + * Assertion object + */ + extname(ext: EmptyString | Ext): Assertion + } + } +} diff --git a/vitest-env.d.mts b/vitest-env.d.mts index a2476efb..a0835e88 100644 --- a/vitest-env.d.mts +++ b/vitest-env.d.mts @@ -2,13 +2,13 @@ interface ImportMetaEnv { readonly BASE_URL: string - readonly DEV: '1' | import('@flex-development/tutils').EmptyString + readonly DEV: '1' | import('@flex-development/pathe').EmptyString readonly GITHUB_TOKEN: string readonly MODE: string readonly NODE_ENV: string - readonly PROD: '1' | import('@flex-development/tutils').EmptyString + readonly PROD: '1' | import('@flex-development/pathe').EmptyString readonly PWD: string - readonly SSR: '1' | import('@flex-development/tutils').EmptyString + readonly SSR: '1' | import('@flex-development/pathe').EmptyString readonly TEST: 'true' readonly USER: string readonly VITEST: 'true' diff --git a/vitest.config.mts b/vitest.config.mts index 5286f66d..5e771d6c 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -123,7 +123,7 @@ function config(env: ConfigEnv): ViteUserConfig { } } }, - setupFiles: ['./__tests__/setup/env.mts', './__tests__/setup/faker.mts'], + setupFiles: ['./__tests__/setup/chai.mts', './__tests__/setup/faker.mts'], snapshotFormat: { callToJSON: true, min: false, diff --git a/yarn.lock b/yarn.lock index 8dc0760f..839f77f6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1780,6 +1780,8 @@ __metadata: "@flex-development/tutils": "npm:6.0.0-alpha.25" "@stylistic/eslint-plugin": "npm:2.10.1" "@tsconfig/strictest": "npm:2.0.5" + "@types/chai": "npm:5.0.1" + "@types/chai-string": "npm:1.4.5" "@types/eslint": "npm:9.6.1" "@types/eslint__js": "npm:8.42.3" "@types/is-ci": "npm:3.0.4" @@ -1791,6 +1793,8 @@ __metadata: "@vates/toggle-scripts": "npm:1.0.0" "@vitest/coverage-v8": "npm:2.1.4" "@vitest/ui": "npm:2.1.4" + chai: "npm:5.1.2" + chai-string: "npm:1.5.0" consola: "npm:3.2.3" cross-env: "npm:7.0.3" cspell: "npm:8.16.0" @@ -2534,6 +2538,24 @@ __metadata: languageName: node linkType: hard +"@types/chai-string@npm:1.4.5": + version: 1.4.5 + resolution: "@types/chai-string@npm:1.4.5" + dependencies: + "@types/chai": "npm:*" + checksum: 10/9bfceeab6afa67e904908032f459bd0cac94bd2fdf49160fd765c1b7e585b831bed08b7110904f5fdb6ec18c046f57912ede571c4bd654098ba488588db2476b + languageName: node + linkType: hard + +"@types/chai@npm:*, @types/chai@npm:5.0.1": + version: 5.0.1 + resolution: "@types/chai@npm:5.0.1" + dependencies: + "@types/deep-eql": "npm:*" + checksum: 10/0f829d4f4be06d6a32c9d89ac08c356df89bafc4b923d8b7fd56cf78d681f5fddfe7aa3391b747f076c57129428f4df694026f344ad3bf8bda65e2ca50c0fd37 + languageName: node + linkType: hard + "@types/concat-stream@npm:^2.0.0": version: 2.0.3 resolution: "@types/concat-stream@npm:2.0.3" @@ -2570,6 +2592,13 @@ __metadata: languageName: node linkType: hard +"@types/deep-eql@npm:*": + version: 4.0.2 + resolution: "@types/deep-eql@npm:4.0.2" + checksum: 10/249a27b0bb22f6aa28461db56afa21ec044fa0e303221a62dff81831b20c8530502175f1a49060f7099e7be06181078548ac47c668de79ff9880241968d43d0c + languageName: node + linkType: hard + "@types/es-abstract@npm:1.17.3": version: 1.17.3 resolution: "@types/es-abstract@npm:1.17.3" @@ -3573,7 +3602,16 @@ __metadata: languageName: node linkType: hard -"chai@npm:^5.1.2": +"chai-string@npm:1.5.0": + version: 1.5.0 + resolution: "chai-string@npm:1.5.0" + peerDependencies: + chai: ^4.1.2 + checksum: 10/39b9511525c99b9d378210897caf6352be34976c24f2e773680c2de4173d2071f3010ea0348bf681e2a201564524e28dd5541ebce8a181a455c8aeefe2a7bda3 + languageName: node + linkType: hard + +"chai@npm:5.1.2, chai@npm:^5.1.2": version: 5.1.2 resolution: "chai@npm:5.1.2" dependencies: