Skip to content

Commit 970f4bd

Browse files
fix: performance of utils/join function
1 parent d687ce5 commit 970f4bd

File tree

3 files changed

+216
-3
lines changed

3 files changed

+216
-3
lines changed

src/utils/join.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
// For some reason path.posix.join is undefined in webpack
22
// Also, this is just much smaller
33
import { normalizePath } from './normalizePath'
4+
import { posix } from './path'
5+
6+
export const join = posix.join
47

58
/** @internal */
6-
export function join(...parts: string[]) {
7-
return normalizePath(parts.map(normalizePath).join('/'))
8-
}
9+
// export function join(...parts: string[]) {
10+
// return posix.join(...parts);
11+
// //return normalizePath(parts.map(normalizePath).join('/'))
12+
// }

src/utils/path.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// Borrowed from:
2+
// https://github.com/nodejs/node/blob/main/lib/path.js
3+
4+
const CHAR_FORWARD_SLASH = 47; // /
5+
const CHAR_DOT = 46; // .
6+
7+
function isPosixPathSeparator(code: number) {
8+
return code === CHAR_FORWARD_SLASH;
9+
}
10+
11+
// Resolves . and .. elements in a path with directory names
12+
function normalizeString(
13+
path: string,
14+
allowAboveRoot: boolean,
15+
separator: string,
16+
isPathSeparator: (code: number) => boolean
17+
) {
18+
let res = '';
19+
let lastSegmentLength = 0;
20+
let lastSlash = -1;
21+
let dots = 0;
22+
let code = 0;
23+
for (let i = 0; i <= path.length; ++i) {
24+
if (i < path.length)
25+
code = path.charCodeAt(i);
26+
else if (isPathSeparator(code))
27+
break;
28+
else
29+
code = CHAR_FORWARD_SLASH;
30+
31+
if (isPathSeparator(code)) {
32+
if (lastSlash === i - 1 || dots === 1) {
33+
// NOOP
34+
} else if (dots === 2) {
35+
if (res.length < 2 || lastSegmentLength !== 2 ||
36+
res.charCodeAt(res.length - 1) !== CHAR_DOT ||
37+
res.charCodeAt(res.length - 2) !== CHAR_DOT) {
38+
if (res.length > 2) {
39+
const lastSlashIndex = res.lastIndexOf(separator);
40+
if (lastSlashIndex === -1) {
41+
res = '';
42+
lastSegmentLength = 0;
43+
} else {
44+
res = res.slice(0, lastSlashIndex);
45+
lastSegmentLength =
46+
res.length - 1 - res.lastIndexOf(separator);
47+
}
48+
lastSlash = i;
49+
dots = 0;
50+
continue;
51+
} else if (res.length !== 0) {
52+
res = '';
53+
lastSegmentLength = 0;
54+
lastSlash = i;
55+
dots = 0;
56+
continue;
57+
}
58+
}
59+
if (allowAboveRoot) {
60+
res += res.length > 0 ? `${separator}..` : '..';
61+
lastSegmentLength = 2;
62+
}
63+
} else {
64+
if (res.length > 0)
65+
res += `${separator}${path.slice(lastSlash + 1, i)}`;
66+
else
67+
res = path.slice(lastSlash + 1, i);
68+
lastSegmentLength = i - lastSlash - 1;
69+
}
70+
lastSlash = i;
71+
dots = 0;
72+
} else if (code === CHAR_DOT && dots !== -1) {
73+
++dots;
74+
} else {
75+
dots = -1;
76+
}
77+
}
78+
return res;
79+
}
80+
81+
/** @internal */
82+
export const posix = {
83+
normalize(path: string) {
84+
if (path.length === 0)
85+
return '.';
86+
87+
const isAbsolute =
88+
path.charCodeAt(0) === CHAR_FORWARD_SLASH;
89+
const trailingSeparator =
90+
path.charCodeAt(path.length - 1) === CHAR_FORWARD_SLASH;
91+
92+
// Normalize the path
93+
path = normalizeString(path, !isAbsolute, '/', isPosixPathSeparator);
94+
95+
if (path.length === 0) {
96+
if (isAbsolute)
97+
return '/';
98+
return trailingSeparator ? './' : '.';
99+
}
100+
if (trailingSeparator)
101+
path += '/';
102+
103+
return isAbsolute ? `/${path}` : path;
104+
},
105+
106+
join(...args: string[]) {
107+
if (args.length === 0)
108+
return '.';
109+
let joined;
110+
for (let i = 0; i < args.length; ++i) {
111+
const arg = args[i];
112+
113+
if (arg.length > 0) {
114+
if (joined === undefined)
115+
joined = arg;
116+
else
117+
joined += `/${arg}`;
118+
}
119+
}
120+
if (joined === undefined)
121+
return '.';
122+
return posix.normalize(joined);
123+
}
124+
}

tests/utils-join.test.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { join } from '../src/utils/join'
2+
3+
const path = require('path').posix || require('path')
4+
5+
describe('utils', () => {
6+
describe('join', () => {
7+
it('should join "good" paths the same as path.join', () => {
8+
const fixtures = [
9+
['/foo/bar', 'baz'],
10+
['foo/bar', 'baz'],
11+
['foo', 'bar', 'baz'],
12+
['/', 'foo', 'bar', 'baz'],
13+
['.', 'foo'],
14+
['foo', '.'],
15+
['.', '.'],
16+
['.', 'foo', '.'],
17+
['.', '.', '.'],
18+
['/', '.'],
19+
['/', '.git'],
20+
['.', '.git'],
21+
]
22+
23+
for (const fixture of fixtures) {
24+
expect(join(...fixture)).toEqual(path.join(...fixture))
25+
}
26+
})
27+
28+
it('should join degenerate paths the same as path.join in these cases', () => {
29+
// Tests adapted from path-browserify
30+
const fixtures = [
31+
[[], '.'],
32+
[['foo/x', './bar'], 'foo/x/bar'],
33+
[['foo/x/', './bar'], 'foo/x/bar'],
34+
[['foo/x/', '.', 'bar'], 'foo/x/bar'],
35+
[['.', '.', '.'], '.'],
36+
[['.', './', '.'], '.'],
37+
[['.', '/./', '.'], '.'],
38+
[['.', '/////./', '.'], '.'],
39+
[['.'], '.'],
40+
[['', '.'], '.'],
41+
[['foo', '/bar'], 'foo/bar'],
42+
[['foo', ''], 'foo'],
43+
[['foo', '', '/bar'], 'foo/bar'],
44+
[['/'], '/'],
45+
[['/', '.'], '/'],
46+
[[''], '.'],
47+
[['', ''], '.'],
48+
[['', 'foo'], 'foo'],
49+
[['', '', 'foo'], 'foo'],
50+
[[' /foo'], ' /foo'],
51+
[[' ', 'foo'], ' /foo'],
52+
[[' ', '.'], ' '],
53+
[[' ', ''], ' '],
54+
[['/', '/foo'], '/foo'],
55+
[['/', '//foo'], '/foo'],
56+
[['/', '', '/foo'], '/foo'],
57+
[['/', '.git'], '/.git']
58+
]
59+
for (const [args, result] of fixtures) {
60+
expect(join(...args)).toEqual(result)
61+
expect(join(...args)).toEqual(path.join(...args))
62+
}
63+
})
64+
65+
// TODO: After replacing join with Node.JS path.posix.join this will not work
66+
// it('should join degenerate paths differently from path.join in these cases', () => {
67+
// // Tests adapted from path-browserify
68+
// const disagreeFixtures = [
69+
// [['./'], '.'],
70+
// [['.', './'], '.'],
71+
// [['', '/foo'], 'foo'],
72+
// [['', '', '/foo'], 'foo'],
73+
// [['foo/', ''], 'foo'],
74+
// [['', '/', 'foo'], 'foo'],
75+
// [['', '/', '/foo'], 'foo'],
76+
// ]
77+
// for (const [args, expected] of disagreeFixtures) {
78+
// const actual = join(...args)
79+
80+
// expect(actual).toEqual(expected)
81+
// expect(actual).not.toEqual(path.join(...args))
82+
// }
83+
// })
84+
})
85+
})

0 commit comments

Comments
 (0)