Skip to content

Commit 4c7f1ca

Browse files
darsainphated
authored andcommitted
Breaking: Normalize paths (closes #80) (#101)
1 parent 634e6a3 commit 4c7f1ca

File tree

7 files changed

+507
-92
lines changed

7 files changed

+507
-92
lines changed

README.md

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ File.isCustomProp('path'); // false -> internal getter/setter
4545
Read more in [Extending Vinyl](#extending-vinyl).
4646

4747
### constructor(options)
48+
All internally managed paths (`cwd`, `base`, `path`, `[history]`) are normalized and remove a trailing separator.
49+
4850
#### options.cwd
4951
Type: `String`<br><br>Default: `process.cwd()`
5052

@@ -64,7 +66,7 @@ Path history. Has no effect if `options.path` is passed.
6466
Type: `Array`<br><br>Default: `options.path ? [options.path] : []`
6567

6668
#### options.stat
67-
The result of an fs.stat call. See [fs.Stats](http://nodejs.org/api/fs.html#fs_class_fs_stats) for more information.
69+
The result of an fs.stat call. This is how you mark the file as a directory. See [isDirectory()](#isDirectory) and [fs.Stats](http://nodejs.org/api/fs.html#fs_class_fs_stats) for more information.
6870

6971
Type: `fs.Stats`<br><br>Default: `null`
7072

@@ -92,6 +94,15 @@ Returns true if file.contents is a Stream.
9294
### isNull()
9395
Returns true if file.contents is null.
9496

97+
### isDirectory()
98+
Returns true if file is a directory. File is considered a directory when:
99+
100+
- `file.isNull()` is `true`
101+
- `file.stat` is an object
102+
- `file.stat.isDirectory()` returns `true`
103+
104+
When constructing a Vinyl object, pass in a valid `fs.Stats` object via `options.stat`. Some operations in Vinyl might need to know the file is a directory from the get go. If you are mocking the `fs.Stats` object, ensure it has the `isDirectory()` method.
105+
95106
### clone([opt])
96107
Returns a new File object with all attributes cloned.<br>By default custom attributes are deep-cloned.
97108

@@ -124,8 +135,14 @@ if (file.isBuffer()) {
124135
}
125136
```
126137

138+
### cwd
139+
Gets and sets current working directory. Defaults to `process.cwd`. Will always be normalized and remove a trailing separator.
140+
141+
### base
142+
Gets and sets base directory. Used for relative pathing (typically where a glob starts). When `null` or `undefined`, it simply proxies the `file.cwd` property. Will always be normalized and remove a trailing separator.
143+
127144
### path
128-
Absolute pathname string or `undefined`. Setting to a different value pushes the old value to `history`.
145+
Absolute pathname string or `undefined`. Setting to a different value pushes the old value to `history`. All new values are normalized and remove a trailing separator.
129146

130147
### history
131148
Array of `path` values the file object has had, from `history[0]` (original) through `history[history.length - 1]` (current). `history` and its elements should normally be treated as read-only and only altered indirectly by setting `path`.
@@ -146,7 +163,7 @@ console.log(file.relative); // file.coffee
146163
```
147164

148165
### dirname
149-
Gets and sets path.dirname for the file path.
166+
Gets and sets path.dirname for the file path. Will always be normalized and remove a trailing separator.
150167

151168
Example:
152169

@@ -225,6 +242,24 @@ console.log(file.extname); // .js
225242
console.log(file.path); // /test/file.js
226243
```
227244

245+
### symlink
246+
Path where the file points to in case it's a symbolic link. Will always be normalized and remove a trailing separator.
247+
248+
## Normalization and concatenation
249+
Since all properties are normalized in their setters, you can just concatenate with `/`, and normalization takes care of it properly on all platforms.
250+
251+
Example:
252+
253+
```javascript
254+
var file = new File();
255+
file.path = '/' + 'test' + '/' + 'foo.bar';
256+
console.log(file.path);
257+
// posix => /test/foo.bar
258+
// win32 => \\test\\foo.bar
259+
```
260+
261+
But never concatenate with `\`, since that is a valid filename character on posix system.
262+
228263
## Extending Vinyl
229264
When extending Vinyl into your own class with extra features, you need to think about a few things.
230265

index.js

Lines changed: 52 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,18 @@ var path = require('path');
22
var clone = require('clone');
33
var cloneStats = require('clone-stats');
44
var cloneBuffer = require('./lib/cloneBuffer');
5+
var stripTrailingSep = require('./lib/stripTrailingSep');
56
var isBuffer = require('./lib/isBuffer');
67
var isStream = require('./lib/isStream');
78
var isNull = require('./lib/isNull');
89
var inspectStream = require('./lib/inspectStream');
10+
var normalize = require('./lib/normalize');
911
var Stream = require('stream');
1012
var replaceExt = require('replace-ext');
1113

1214
var builtInFields = [
13-
'_contents', '_symlink', 'contents', 'stat', 'history', 'path', 'base', 'cwd',
15+
'_contents', '_symlink', 'contents', 'stat', 'history', 'path',
16+
'_base', 'base', '_cwd', 'cwd',
1417
];
1518

1619
function File(file) {
@@ -20,19 +23,22 @@ function File(file) {
2023
file = {};
2124
}
2225

23-
// Record path change
24-
var history = file.path ? [file.path] : file.history;
25-
this.history = history || [];
26-
27-
this.cwd = file.cwd || process.cwd();
28-
this.base = file.base || this.cwd;
29-
3026
// Stat = files stats object
3127
this.stat = file.stat || null;
3228

3329
// Contents = stream, buffer, or null if not read
3430
this.contents = file.contents || null;
3531

32+
// Replay path history to ensure proper normalization and trailing sep
33+
var history = file.path ? [file.path] : file.history || [];
34+
this.history = [];
35+
history.forEach(function(path) {
36+
self.path = path;
37+
});
38+
39+
this.cwd = file.cwd || process.cwd();
40+
this.base = file.base;
41+
3642
this._isVinyl = true;
3743

3844
this._symlink = null;
@@ -156,7 +162,7 @@ File.prototype.inspect = function() {
156162
var inspect = [];
157163

158164
// Use relative path if possible
159-
var filePath = (this.base && this.path) ? this.relative : this.path;
165+
var filePath = this.path ? this.relative : null;
160166

161167
if (filePath) {
162168
inspect.push('"' + filePath + '"');
@@ -195,12 +201,40 @@ Object.defineProperty(File.prototype, 'contents', {
195201
},
196202
});
197203

204+
Object.defineProperty(File.prototype, 'cwd', {
205+
get: function() {
206+
return this._cwd;
207+
},
208+
set: function(cwd) {
209+
if (!cwd || typeof cwd !== 'string') {
210+
throw new Error('cwd must be a non-empty string.');
211+
}
212+
this._cwd = stripTrailingSep(normalize(cwd));
213+
},
214+
});
215+
216+
Object.defineProperty(File.prototype, 'base', {
217+
get: function() {
218+
return this._base || this._cwd;
219+
},
220+
set: function(base) {
221+
if (base == null) {
222+
delete this._base;
223+
return;
224+
}
225+
if (typeof base !== 'string' || !base) {
226+
throw new Error('base must be a non-empty string, or null/undefined.');
227+
}
228+
base = stripTrailingSep(normalize(base));
229+
if (base !== this._cwd) {
230+
this._base = base;
231+
}
232+
},
233+
});
234+
198235
// TODO: Should this be moved to vinyl-fs?
199236
Object.defineProperty(File.prototype, 'relative', {
200237
get: function() {
201-
if (!this.base) {
202-
throw new Error('No base specified! Can not get relative.');
203-
}
204238
if (!this.path) {
205239
throw new Error('No path specified! Can not get relative.');
206240
}
@@ -222,7 +256,7 @@ Object.defineProperty(File.prototype, 'dirname', {
222256
if (!this.path) {
223257
throw new Error('No path specified! Can not set dirname.');
224258
}
225-
this.path = path.join(dirname, path.basename(this.path));
259+
this.path = path.join(dirname, this.basename);
226260
},
227261
});
228262

@@ -237,7 +271,7 @@ Object.defineProperty(File.prototype, 'basename', {
237271
if (!this.path) {
238272
throw new Error('No path specified! Can not set basename.');
239273
}
240-
this.path = path.join(path.dirname(this.path), basename);
274+
this.path = path.join(this.dirname, basename);
241275
},
242276
});
243277

@@ -253,7 +287,7 @@ Object.defineProperty(File.prototype, 'stem', {
253287
if (!this.path) {
254288
throw new Error('No path specified! Can not set stem.');
255289
}
256-
this.path = path.join(path.dirname(this.path), stem + this.extname);
290+
this.path = path.join(this.dirname, stem + this.extname);
257291
},
258292
});
259293

@@ -278,8 +312,9 @@ Object.defineProperty(File.prototype, 'path', {
278312
},
279313
set: function(path) {
280314
if (typeof path !== 'string') {
281-
throw new Error('path should be string');
315+
throw new Error('path should be a string.');
282316
}
317+
path = stripTrailingSep(normalize(path));
283318

284319
// Record history only when path changed
285320
if (path && path !== this.path) {
@@ -298,7 +333,7 @@ Object.defineProperty(File.prototype, 'symlink', {
298333
throw new Error('symlink should be a string');
299334
}
300335

301-
this._symlink = symlink;
336+
this._symlink = stripTrailingSep(normalize(symlink));
302337
},
303338
});
304339

lib/normalize.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
var normalize = require('path').normalize;
2+
3+
module.exports = function(str) {
4+
return str === '' ? str : normalize(str);
5+
};

lib/stripTrailingSep.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module.exports = function(str) {
2+
while (endsInSeparator(str)) {
3+
str = str.slice(0, -1);
4+
}
5+
return str;
6+
};
7+
8+
function endsInSeparator(str) {
9+
var last = str[str.length - 1];
10+
return str.length > 1 && (last === '/' || last === '\\');
11+
}

0 commit comments

Comments
 (0)