Skip to content

Commit 5206f49

Browse files
committed
refactor: move Path to core and add more logic.
1 parent e2b6e67 commit 5206f49

28 files changed

Lines changed: 518 additions & 241 deletions

File tree

packages/angular_devkit/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ export * from './json';
1010
export * from './logger';
1111
export * from './terminal';
1212
export * from './utils';
13+
export * from './virtual-fs';
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
export * from './path';
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
import { BaseException } from '@angular-devkit/core';
9+
10+
11+
export class InvalidPathException extends BaseException {
12+
constructor(path: string) { super(`Path ${JSON.stringify(path)} is invalid.`); }
13+
}
14+
export class PathMustBeAbsoluteException extends BaseException {
15+
constructor(path: string) { super(`Path ${JSON.stringify(path)} must be absolute.`); }
16+
}
17+
18+
19+
/**
20+
* A Path recognized by most methods in the DevKit.
21+
*/
22+
export type Path = string & {
23+
__PRIVATE_DEVKIT_PATH: void;
24+
};
25+
26+
27+
/**
28+
* The Separator for normalized path.
29+
* @type {Path}
30+
*/
31+
export const NormalizedSep = '/' as Path;
32+
33+
34+
/**
35+
* The root of a normalized path.
36+
* @type {Path}
37+
*/
38+
export const NormalizedRoot = NormalizedSep as Path;
39+
40+
41+
/**
42+
* Split a path into multiple path fragments. Each fragments except the last one will end with
43+
* a path separator.
44+
* @param {Path} path The path to split.
45+
* @returns {Path[]} An array of path fragments.
46+
*/
47+
export function split(path: Path): Path[] {
48+
const arr = path.split(NormalizedSep);
49+
50+
return arr.map((fragment, i) => fragment + (i < arr.length - 1 ? NormalizedSep : '')) as Path[];
51+
}
52+
53+
/**
54+
*
55+
*/
56+
export function extname(path: Path): string {
57+
const base = basename(path);
58+
const i = base.lastIndexOf('.');
59+
if (i < 1) {
60+
return '';
61+
} else {
62+
return base.substr(i);
63+
}
64+
}
65+
66+
/**
67+
* This is the equivalent of calling dirname() over and over, until the root, then getting the
68+
* basename.
69+
*
70+
* @example rootname('/a/b/c') == 'a'
71+
* @example rootname('a/b') == '.'
72+
* @param path The path to get the rootname from.
73+
* @returns {Path} The first directory name.
74+
*/
75+
export function rootname(path: Path): Path {
76+
const i = path.indexOf(NormalizedSep);
77+
if (!isAbsolute(path)) {
78+
return '.' as Path;
79+
} else if (i == -1) {
80+
return path;
81+
} else {
82+
return path.substr(path.lastIndexOf(NormalizedSep) + 1) as Path;
83+
}
84+
}
85+
86+
87+
/**
88+
* Return the basename of the path, as a Path. See path.basename
89+
*/
90+
export function basename(path: Path): Path {
91+
const i = path.lastIndexOf(NormalizedSep);
92+
if (i == -1) {
93+
return path;
94+
} else {
95+
return path.substr(path.lastIndexOf(NormalizedSep) + 1) as Path;
96+
}
97+
}
98+
99+
100+
/**
101+
* Return the dirname of the path, as a Path. See path.dirname
102+
*/
103+
export function dirname(path: Path): Path {
104+
const i = path.lastIndexOf(NormalizedSep);
105+
if (i == -1) {
106+
return '' as Path;
107+
} else {
108+
return normalize(path.substr(0, i));
109+
}
110+
}
111+
112+
113+
/**
114+
* Join multiple paths together, and normalize the result. Accepts strings that will be
115+
* normalized as well (but the original must be a path).
116+
*/
117+
export function join(p1: Path, ...others: string[]): Path {
118+
if (others.length > 0) {
119+
return normalize((p1 ? p1 + NormalizedSep : '') + others.join(NormalizedSep));
120+
} else {
121+
return p1;
122+
}
123+
}
124+
125+
126+
/**
127+
* Returns true if a path is absolute.
128+
*/
129+
export function isAbsolute(p: Path) {
130+
return p.startsWith(NormalizedSep);
131+
}
132+
133+
134+
/**
135+
* Returns a path such that `join(from, relative(from, to)) == to`.
136+
* Both paths must be absolute, otherwise it does not make much sense.
137+
*/
138+
export function relative(from: Path, to: Path): Path {
139+
if (!isAbsolute(from)) {
140+
throw new PathMustBeAbsoluteException(from);
141+
}
142+
if (!isAbsolute(to)) {
143+
throw new PathMustBeAbsoluteException(to);
144+
}
145+
146+
let p: string;
147+
148+
if (from == to) {
149+
p = '';
150+
} else {
151+
const splitFrom = from.split(NormalizedSep);
152+
const splitTo = to.split(NormalizedSep);
153+
154+
while (splitFrom.length > 0 && splitTo.length > 0 && splitFrom[0] == splitTo[0]) {
155+
splitFrom.shift();
156+
splitTo.shift();
157+
}
158+
159+
if (splitFrom.length == 0) {
160+
p = splitTo.join(NormalizedSep);
161+
} else {
162+
p = splitFrom.map(_ => '..').concat(splitTo).join(NormalizedSep);
163+
}
164+
}
165+
166+
return normalize(p);
167+
}
168+
169+
170+
/**
171+
* Returns a Path that is the resolution of p2, from p1. If p2 is absolute, it will return p2,
172+
* otherwise will join both p1 and p2.
173+
*/
174+
export function resolve(p1: Path, p2: Path) {
175+
if (isAbsolute(p2)) {
176+
return p2;
177+
} else {
178+
return join(p1, p2);
179+
}
180+
}
181+
182+
183+
/**
184+
* Normalize a string into a Path. This is the only mean to get a Path type from a string that
185+
* represents a system path. Normalization includes:
186+
* - Windows backslashes `\\` are replaced with `/`.
187+
* - Windows drivers are replaced with `/X/`, where X is the drive letter.
188+
* - Absolute paths starts with `/`.
189+
* - Multiple `/` are replaced by a single one.
190+
* - Path segments `.` are removed.
191+
* - Path segments `..` are resolved.
192+
* - If a path is absolute, having a `..` at the start is invalid (and will throw).
193+
*/
194+
export function normalize(path: string): Path {
195+
if (path == '' || path == '.') {
196+
return '' as Path;
197+
} else if (path == NormalizedRoot) {
198+
return NormalizedRoot;
199+
}
200+
201+
// Match absolute windows path.
202+
const original = path;
203+
if (path.match(/^[A-Z]:\\/)) {
204+
path = '\\' + path[0] + '\\' + path.substr(3);
205+
}
206+
207+
// We convert Windows paths as well here.
208+
const p = path.split(/[\/\\]/g);
209+
let relative = false;
210+
let i = 1;
211+
212+
// Special case the first one.
213+
if (p[0] != '') {
214+
p.unshift('.');
215+
relative = true;
216+
}
217+
218+
while (i < p.length) {
219+
if (p[i] == '.') {
220+
p.splice(i, 1);
221+
} else if (p[i] == '..') {
222+
if (i < 2 && !relative) {
223+
throw new InvalidPathException(original);
224+
} else if (i >= 2) {
225+
p.splice(i - 1, 2);
226+
i--;
227+
} else {
228+
i++;
229+
}
230+
} else if (p[i] == '') {
231+
p.splice(i, 1);
232+
} else {
233+
i++;
234+
}
235+
}
236+
237+
if (p.length == 1) {
238+
return p[0] == '' ? NormalizedSep : '' as Path;
239+
} else {
240+
if (p[0] == '.') {
241+
p.shift();
242+
}
243+
244+
return p.join(NormalizedSep) as Path;
245+
}
246+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
import { benchmark } from '@_/benchmark';
9+
import { join, normalize } from './path';
10+
11+
12+
const p1 = '/b/././a/tt/../../../a/b/./d/../c';
13+
const p2 = '/a/tt/../../../a/b/./d';
14+
15+
16+
describe('Virtual FS Path', () => {
17+
benchmark('normalize', () => normalize(p1));
18+
benchmark('join', () => join(normalize(p1), normalize(p2)));
19+
});

0 commit comments

Comments
 (0)