Skip to content

Commit 6bb30a5

Browse files
committed
feat: [pathToFileURL] url string support
Signed-off-by: Lexus Drumgold <unicornware@flexdevelopment.llc>
1 parent 77e0b13 commit 6bb30a5

File tree

5 files changed

+220
-84
lines changed

5 files changed

+220
-84
lines changed

src/interfaces/__tests__/path-to-file-url-options.spec-d.mts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type {
88
PlatformOptions,
99
ResolveWithOptions
1010
} from '@flex-development/pathe'
11+
import type { Nilable } from '@flex-development/tutils'
1112

1213
describe('unit-d:interfaces/PathToFileUrlOptions', () => {
1314
it('should extend PlatformOptions', () => {
@@ -17,4 +18,10 @@ describe('unit-d:interfaces/PathToFileUrlOptions', () => {
1718
it('should extend ResolveWithOptions', () => {
1819
expectTypeOf<TestSubject>().toMatchTypeOf<ResolveWithOptions>()
1920
})
21+
22+
it('should match [string?: boolean | null | undefined]', () => {
23+
expectTypeOf<TestSubject>()
24+
.toHaveProperty('string')
25+
.toEqualTypeOf<Nilable<boolean>>()
26+
})
2027
})

src/interfaces/path-to-file-url-options.mts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ import type {
1717
* @extends {PlatformOptions}
1818
* @extends {ResolveWithOptions}
1919
*/
20-
interface PathToFileUrlOptions extends PlatformOptions, ResolveWithOptions {}
20+
interface PathToFileUrlOptions extends PlatformOptions, ResolveWithOptions {
21+
/**
22+
* Return `file:` URL string?
23+
*/
24+
string?: boolean | null | undefined
25+
}
2126

2227
export type { PathToFileUrlOptions as default }

src/interfaces/pathe.mts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,40 @@ interface Pathe extends PosixPlatformPath {
283283
*/
284284
isURL(this: void, value: unknown): value is URL | string
285285

286+
/**
287+
* Convert a file `path` to a `file:` {@linkcode URL} string.
288+
*
289+
* > The following characters are percent-encoded when converting from file
290+
* > path to a `URL`:
291+
* >
292+
* > - %: Only character not encoded by the `pathname` setter
293+
* > - CR: Stripped out by the `pathname` setter (see [`whatwg/url#419`][419])
294+
* > - LF: Stripped out by the `pathname` setter (see [`whatwg/url#419`][419])
295+
* > - TAB: Stripped out by the `pathname` setter
296+
*
297+
* [419]: https://github.com/whatwg/url/issues/419
298+
*
299+
* @see {@linkcode ErrInvalidArgValue}
300+
* @see {@linkcode PathToFileUrlOptions}
301+
*
302+
* @this {void}
303+
*
304+
* @param {string} path
305+
* The path to handle
306+
* @param {Omit<PathToFileUrlOptions, 'string'> & { string: true }} options
307+
* Conversion options
308+
* @param {true} options.string
309+
* Return `file:` URL string?
310+
* @return {string}
311+
* `path` as `file:` URL string
312+
* @throws {ErrInvalidArgValue}
313+
*/
314+
pathToFileURL(
315+
this: void,
316+
path: string,
317+
options: Omit<PathToFileUrlOptions, 'string'> & { string: true }
318+
): string
319+
286320
/**
287321
* Convert a file `path` to a `file:` {@linkcode URL}.
288322
*

src/lib/__tests__/path-to-file-url.spec.mts

Lines changed: 82 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -4,87 +4,115 @@
44
* @see https://github.com/nodejs/node/blob/v23.2.0/test/parallel/test-url-pathtofileurl.js
55
*/
66

7+
import DRIVE from '#fixtures/drive'
8+
import { sepWindows } from '#internal/constants'
9+
import process from '#internal/process'
710
import testSubject from '#lib/path-to-file-url'
11+
import sep from '#lib/sep'
812
import toPosix from '#lib/to-posix'
13+
import cwdWindows from '#tests/utils/cwd-windows'
914
import { codes, isNodeError, type NodeError } from '@flex-development/errnode'
1015
import { pathToFileURL } from 'node:url'
1116

1217
describe('unit:lib/pathToFileURL', () => {
1318
describe('posix', () => {
1419
it.each<Parameters<typeof testSubject>>([
15-
['/FOO'],
16-
['/dir/'],
17-
['/dir/foo'],
18-
['/foo bar'],
19-
['/foo#bar'],
20-
['/foo%bar'],
21-
['/foo&bar'],
22-
['/foo'],
23-
['/foo.mjs'],
24-
['/foo:bar'],
25-
['/foo;bar'],
26-
['/foo=bar'],
27-
['/foo?bar'],
28-
['/foo\bbar'],
29-
['/foo\nbar'],
30-
['/foo\rbar'],
31-
['/foo\tbar'],
32-
['/fóóbàr'],
33-
['/€'],
34-
['/🚀']
35-
])('should return `path` as `file:` URL (%#)', path => {
20+
[sep + 'FOO'],
21+
[sep + 'dir/'],
22+
[sep + 'dir/foo'],
23+
[sep + 'foo bar'],
24+
[sep + 'foo#bar'],
25+
[sep + 'foo%bar'],
26+
[sep + 'foo&bar'],
27+
[sep + 'foo'],
28+
[sep + 'foo.mjs'],
29+
[sep + 'foo:bar']
30+
])('should return `path` as `file:` URL (%j)', path => {
3631
// Act
3732
const result = testSubject(path)
3833

3934
// Expect
4035
expect(result).to.eql(pathToFileURL(path, { windows: false }))
4136
})
37+
38+
it.each<Parameters<typeof testSubject>>([
39+
[sep + 'foo;bar'],
40+
[sep + 'foo=bar'],
41+
[sep + 'foo?bar'],
42+
[sep + 'foo\bbar'],
43+
[sep + 'foo\nbar'],
44+
[sep + 'foo\rbar'],
45+
[sep + 'foo\tbar'],
46+
[sep + 'fóóbàr'],
47+
[sep + '€'],
48+
[sep + '🚀']
49+
])('should return `path` as `file:` URL string (%j)', path => {
50+
// Act
51+
const result = testSubject(path, { string: true })
52+
53+
// Expect
54+
expect(result).to.eql(String(pathToFileURL(path, { windows: false })))
55+
})
4256
})
4357

4458
describe('windows', () => {
45-
it.each<Parameters<typeof testSubject>>([
46-
['C:\\FOO'],
47-
['C:\\dir\\'],
48-
['C:\\dir\\foo'],
49-
['C:\\foo bar'],
50-
['C:\\foo#bar'],
51-
['C:\\foo%bar'],
52-
['C:\\foo&bar'],
53-
['C:\\foo'],
54-
['C:\\foo.mjs'],
55-
['C:\\foo:bar'],
56-
['C:\\foo;bar'],
57-
['C:\\foo=bar'],
58-
['C:\\foo?bar'],
59-
['C:\\foo\\bar'],
60-
['C:\\foo\bbar'],
61-
['C:\\foo\nbar'],
62-
['C:\\foo\rbar'],
63-
['C:\\foo\tbar'],
64-
['C:\\fóóbàr'],
65-
['C:\\€'],
66-
['C:\\🚀'],
67-
['\\\\?\\C:\\path\\to\\file.txt'],
68-
['\\\\?\\UNC\\server\\share\\folder\\file.txt'],
69-
['\\\\nas\\My Docs\\File.doc'],
70-
['\\\\nas\\share\\path.txt']
71-
])('should return `path` as `file:` URL (%#)', path => {
72-
// Arrange
73-
const windows: boolean = true
74-
const url: URL = pathToFileURL(path, { windows })
59+
let windows: true
60+
61+
beforeAll(() => {
62+
windows = true
63+
})
7564

65+
beforeEach(() => {
66+
vi.spyOn(process, 'cwd').mockImplementation(cwdWindows)
67+
})
68+
69+
it.each<Parameters<typeof testSubject>>([
70+
[DRIVE + sepWindows + 'FOO'],
71+
[DRIVE + sepWindows + 'dir\\'],
72+
[DRIVE + sepWindows + 'dir\\foo'],
73+
[DRIVE + sepWindows + 'foo bar'],
74+
[DRIVE + sepWindows + 'foo#bar'],
75+
[DRIVE + sepWindows + 'foo%bar'],
76+
[DRIVE + sepWindows + 'foo&bar'],
77+
[DRIVE + sepWindows + 'foo'],
78+
[DRIVE + sepWindows + 'foo.mjs'],
79+
[DRIVE + sepWindows + 'foo:bar'],
80+
[DRIVE + sepWindows + 'foo;bar'],
81+
[DRIVE + sepWindows + 'foo=bar'],
82+
[DRIVE + sepWindows + 'foo?bar']
83+
])('should return `path` as `file:` URL (%j)', path => {
7684
// Act
7785
const result = testSubject(path, { windows })
7886

7987
// Expect
80-
expect(result.href).to.eq(toPosix(url.href))
81-
expect(result.pathname).to.eq(toPosix(url.pathname))
88+
expect(result).to.eql(toPosix(pathToFileURL(path, { windows })))
89+
})
90+
91+
it.each<Parameters<typeof testSubject>>([
92+
[DRIVE + sepWindows + '\\foo\\bar'],
93+
[DRIVE + sepWindows + '\\foo\bbar'],
94+
[DRIVE + sepWindows + '\\foo\nbar'],
95+
[DRIVE + sepWindows + '\\foo\rbar'],
96+
[DRIVE + sepWindows + '\\foo\tbar'],
97+
[DRIVE + sepWindows + '\\fóóbàr'],
98+
[DRIVE + sepWindows + '\\€'],
99+
[DRIVE + sepWindows + '\\🚀'],
100+
[sepWindows.repeat(2) + '?\\C:\\path\\to\\file.txt'],
101+
[sepWindows.repeat(2) + '?\\UNC\\server\\share\\folder\\file.txt'],
102+
[sepWindows.repeat(2) + 'nas\\My Docs\\File.doc'],
103+
[sepWindows.repeat(2) + 'nas\\share\\path.txt']
104+
])('should return `path` as `file:` URL string (%j)', path => {
105+
// Act
106+
const result = testSubject(path, { string: true, windows })
107+
108+
// Expect
109+
expect(result).to.eq(toPosix(String(pathToFileURL(path, { windows }))))
82110
})
83111

84112
it.each<Parameters<typeof testSubject>>([
85113
['\\\\host'],
86114
['\\\\\\no-server']
87-
])('should throw if UNC path hostname is invalid (%#)', path => {
115+
])('should throw if UNC path hostname is invalid (%j)', path => {
88116
// Arrange
89117
let error!: NodeError
90118

0 commit comments

Comments
 (0)