diff --git a/gulp-util/index.d.ts b/gulp-util/index.d.ts index feea14ec8e..1393fdfc3a 100644 --- a/gulp-util/index.d.ts +++ b/gulp-util/index.d.ts @@ -11,7 +11,7 @@ import vinyl = require('vinyl'); import chalk = require('chalk'); import through2 = require('through2'); -export class File extends vinyl { } +export { vinyl as File }; /** * Replaces a file extension in a path. Returns the new path. diff --git a/vinyl/index.d.ts b/vinyl/index.d.ts index ee3ae22bee..bfdb8e4bff 100644 --- a/vinyl/index.d.ts +++ b/vinyl/index.d.ts @@ -1,133 +1,327 @@ -// Type definitions for vinyl 1.2.0 +// Type definitions for vinyl 2.0.0 // Project: https://github.com/gulpjs/vinyl -// Definitions by: vvakame , jedmao +// Definitions by: vvakame , jedmao , Georgii Dolzhykov // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped /// +import * as fs from 'fs'; - -import fs = require("fs"); - -/** - * A virtual file format. - */ -declare class File { - constructor(options?: { - - /** - * Default: process.cwd() - */ - cwd?: string; - - /** - * Used for relative pathing. Typically where a glob starts. - */ - base?: string; - - /** - * Full path to the file. - */ - path?: string; - - /** - * Path history. Has no effect if options.path is passed. - */ - history?: string[]; - - /** - * The result of an fs.stat call. See fs.Stats for more information. - */ - stat?: fs.Stats; - - /** - * File contents. - * Type: Buffer, Stream, or null - */ - contents?: Buffer | NodeJS.ReadWriteStream; - }); +interface ConstructorOptions { + /** + * The current working directory of the file. Default: process.cwd() + */ + cwd?: string; /** - * Default: process.cwd() + * Used for relative pathing. Typically where a glob starts. Default: options.cwd */ - public cwd: string; - - /** - * Used for relative pathing. Typically where a glob starts. - */ - public dirname: string; - public basename: string; - public base: string; + base?: string; /** * Full path to the file. */ - public path: string; - public stat: fs.Stats; + path?: string; /** - * Gets and sets stem (filename without suffix) for the file path. + * Stores the path history. If `options.path` and `options.history` are both passed, + * `options.path` is appended to `options.history`. All `options.history` paths are + * normalized by the `file.path` setter. + * Default: `[]` (or `[options.path]` if `options.path` is passed) */ - public stem: string; + history?: string[]; /** - * Gets and sets path.extname for the file path + * The result of an fs.stat call. This is how you mark the file as a directory or + * symbolic link. See `isDirectory()`, `isSymbolic()` and `fs.Stats` for more information. + * http://nodejs.org/api/fs.html#fs_class_fs_stats */ - public extname: string; + stat?: fs.Stats; /** - * Array of path values the file object has had + * File contents. + * Type: `Buffer`, `Stream`, or null + * Default: null */ - public history: string[]; + contents?: Buffer | NodeJS.ReadableStream | null; /** - * Type: Buffer|Stream|null (Default: null) + * Any custom option properties will be directly assigned to the new Vinyl object. */ - public contents: Buffer | NodeJS.ReadableStream; + [customOption: string]: any; +} + +interface FileConstructor { + new (options: ConstructorOptions & { contents: null }): NullFile; + new (options: ConstructorOptions & { contents: Buffer }): BufferFile; + new (options: ConstructorOptions & { contents: NodeJS.ReadableStream }): StreamFile; + new (options?: ConstructorOptions): File; /** - * Returns path.relative for the file base and file path. + * Checks if a given object is a vinyl file. + */ + isVinyl(obj: any): obj is File; + + /** + * Checks if a property is not managed internally. + */ + isCustomProp(name: string): boolean; + + prototype: File; +} + +export = File; + +declare let File: FileConstructor; + +interface File { + /** + * Gets and sets the contents of the file. If set to a `Stream`, it is wrapped in + * a `cloneable-readable` stream. + * + * Throws when set to any value other than a `Stream`, a `Buffer` or `null`. + */ + contents: Buffer | NodeJS.ReadableStream | null; + + /** + * Gets and sets current working directory. Will always be normalized and have trailing + * separators removed. + * + * Throws when set to any value other than non-empty strings. + */ + cwd: string; + + // + /** + * 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 have trailing separators removed. + * + * Throws when set to any value other than non-empty strings or `null`/`undefined`. + * + * The setter's type is actually `string | null | undefined`, but TypeScript doesn't allow + * get/set accessors to be of different type. The property is declared as `string` for the + * compiler not to require useless null checks for the getter. (Hopefully, noone will need + * to assign `null` to this property.) + */ + base: string; + + /** + * Gets and sets the absolute pathname string or `undefined`. Setting to a different value + * appends the new path to `file.history`. If set to the same value as the current path, it + * is ignored. All new values are normalized and have trailing separators removed. + * + * Throws when set to any value other than a string. + * + * The getter is actually of type `string | undefined` whereas the setter is just `string`, + * however TypeScript doesn't allow get/set accessors to be of different type. See the + * comment for the `base` properties. + */ + path: string; + + /** + * Array of `file.path` values the Vinyl object has had, from `file.history[0]` (original) + * through `file.history[file.history.length - 1]` (current). `file.history` and its elements + * should normally be treated as read-only and only altered indirectly by setting `file.path`. + */ + readonly history: ReadonlyArray; + + /** + * Gets the result of `path.relative(file.base, file.path)`. + * + * Throws when set or when `file.path` is not set. + * * Example: - * var file = new File({ - * cwd: "/", - * base: "/test/", - * path: "/test/file.js" - * }); - * console.log(file.relative); // file.js + * + * ```js + * var file = new File({ + * cwd: '/', + * base: '/test/', + * path: '/test/file.js' + * }); + * + * console.log(file.relative); // file.js + * ``` */ - public relative: string; + relative: string; /** - * Returns true if file.contents is a Buffer. + * Gets and sets the dirname of `file.path`. Will always be normalized and have trailing + * separators removed. + * + * Throws when `file.path` is not set. + * + * Example: + * + * ```js + * var file = new File({ + * cwd: '/', + * base: '/test/', + * path: '/test/file.js' + * }); + * + * console.log(file.dirname); // /test + * + * file.dirname = '/specs'; + * + * console.log(file.dirname); // /specs + * console.log(file.path); // /specs/file.js + * ``` */ - public isBuffer(): boolean; + dirname: string; /** - * Returns true if file.contents is a Stream. + * Gets and sets the basename of `file.path`. + * + * Throws when `file.path` is not set. + * + * Example: + * + * ```js + * var file = new File({ + * cwd: '/', + * base: '/test/', + * path: '/test/file.js' + * }); + * + * console.log(file.basename); // file.js + * + * file.basename = 'file.txt'; + * + * console.log(file.basename); // file.txt + * console.log(file.path); // /test/file.txt + * ``` */ - public isStream(): boolean; + basename: string; /** - * Returns true if file.contents is null. + * Gets and sets stem (filename without suffix) of `file.path`. + * + * Throws when `file.path` is not set. + * + * Example: + * + * ```js + * var file = new File({ + * cwd: '/', + * base: '/test/', + * path: '/test/file.js' + * }); + * + * console.log(file.stem); // file + * + * file.stem = 'foo'; + * + * console.log(file.stem); // foo + * console.log(file.path); // /test/foo.js + * ``` */ - public isNull(): boolean; + stem: string; /** - * Returns true if this is a directory. + * Gets and sets extname of `file.path`. + * + * Throws when `file.path` is not set. + * + * Example: + * + * ```js + * var file = new File({ + * cwd: '/', + * base: '/test/', + * path: '/test/file.js' + * }); + * + * console.log(file.extname); // .js + * + * file.extname = '.txt'; + * + * console.log(file.extname); // .txt + * console.log(file.path); // /test/file.txt + * ``` */ - public isDirectory(): boolean; + extname: string; /** - * Returns a new File object with all attributes cloned. Custom attributes are deep-cloned. + * Gets and sets the path where the file points to if it's a symbolic link. Will always + * be normalized and have trailing separators removed. + * + * Throws when set to any value other than a string. */ - public clone(opts?: { contents?: boolean, deep?: boolean }): File; + symlink: string | null; + + stat: fs.Stats | null; + + [customProperty: string]: any; /** + * Returns `true` if the file contents are a `Buffer`, otherwise `false`. + */ + isBuffer(): this is BufferFile; + + /** + * Returns `true` if the file contents are a `Stream`, otherwise `false`. + */ + isStream(): this is StreamFile; + + /** + * Returns `true` if the file contents are `null`, otherwise `false`. + */ + isNull(): this is NullFile; + + /** + * Returns `true` if the file represents a directory, otherwise `false`. + * + * A file is considered a directory when: + * + * - `file.isNull()` is `true` + * - `file.stat` is an object + * - `file.stat.isDirectory()` returns `true` + * + * When constructing a Vinyl object, pass in a valid `fs.Stats` object via `options.stat`. + * If you are mocking the `fs.Stats` object, you may need to stub the `isDirectory()` method. + */ + isDirectory(): this is DirectoryFile; + + /** + * Returns `true` if the file represents a symbolic link, otherwise `false`. + * + * A file is considered symbolic when: + * + * - `file.isNull()` is `true` + * - `file.stat` is an object + * - `file.stat.isSymbolicLink()` returns `true` + * + * When constructing a Vinyl object, pass in a valid `fs.Stats` object via `options.stat`. + * If you are mocking the `fs.Stats` object, you may need to stub the `isSymbolicLink()` method. + */ + isSymbolic(): this is SymbolicFile; + + /** + * Returns a new Vinyl object with all attributes cloned. + * + * __By default custom attributes are cloned deeply.__ + * + * If `options` or `options.deep` is `false`, custom attributes will not be cloned deeply. + * + * If `file.contents` is a `Buffer` and `options.contents` is `false`, the `Buffer` reference + * will be reused instead of copied. + */ + clone(opts?: { contents?: boolean, deep?: boolean } | boolean): this; + + /** + * Returns a formatted-string interpretation of the Vinyl object. + * Automatically called by node's `console.log`. + */ + inspect(): string; + + /** + * @deprecated This method was removed in v2.0. * If file.contents is a Buffer, it will write it to the stream. * If file.contents is a Stream, it will pipe it to the stream. * If file.contents is null, it will do nothing. */ - public pipe( + pipe( stream: T, opts?: { /** @@ -135,21 +329,43 @@ declare class File { */ end?: boolean; }): T; - - /** - * Returns a pretty String interpretation of the File. Useful for console.log. - */ - public inspect(): string; - - /** - * Checks if a given object is a vinyl file. - */ - public static isVinyl(obj: any): boolean; - - /** - * Checks if a property is not managed internally. - */ - public static isCustomProp(name: string): boolean; } -export = File; +// See https://github.com/Microsoft/TypeScript/issues/11796 + +interface BufferFile extends File { + contents: Buffer; + isStream(): this is never; + isBuffer(): true; + isNull(): this is never; + isDirectory(): this is never; + isSymbolic(): this is never; +} + +interface StreamFile extends File { + contents: NodeJS.ReadableStream; + isStream(): true; + isBuffer(): this is never; + isNull(): this is never; + isDirectory(): this is never; + isSymbolic(): this is never; +} + +interface NullFile extends File { + contents: null; + isStream(): this is never; + isBuffer(): this is never; + isNull(): true; + isDirectory(): this is DirectoryFile; + isSymbolic(): this is SymbolicFile; +} + +interface DirectoryFile extends NullFile { + isDirectory(): true; + isSymbolic(): this is never; +} + +interface SymbolicFile extends NullFile { + isDirectory(): this is never; + isSymbolic(): true; +} diff --git a/vinyl/tsconfig.json b/vinyl/tsconfig.json index a4a0b34380..27dd21e240 100644 --- a/vinyl/tsconfig.json +++ b/vinyl/tsconfig.json @@ -3,7 +3,7 @@ "module": "commonjs", "target": "es6", "noImplicitAny": true, - "strictNullChecks": false, + "strictNullChecks": true, "baseUrl": "../", "typeRoots": [ "../" @@ -16,4 +16,4 @@ "index.d.ts", "vinyl-tests.ts" ] -} \ No newline at end of file +} diff --git a/vinyl/vinyl-tests.ts b/vinyl/vinyl-tests.ts index 9d478b602e..b5c5dd9993 100644 --- a/vinyl/vinyl-tests.ts +++ b/vinyl/vinyl-tests.ts @@ -1,681 +1,1643 @@ /// /// +/// -import File = require('../vinyl'); -import Stream = require('stream'); -import fs = require('fs'); +'use strict'; -declare var fakeStream: NodeJS.ReadWriteStream; +import * as fs from 'fs'; +import * as path from 'path'; +import expect from 'expect'; +var miss = require('mississippi'); +var cloneable = require('cloneable-readable'); -describe('File', () => { +import File = require('vinyl'); - describe('constructor()', () => { +/** + * Custom and private properties needed for tests. + * + * TODO: + * Is there a way to augment `File` (defined using `export =`) without introducing an extra + * interface and type assertions? + */ +interface TestFile extends File { + sourceMap?: any; + custom?: any; + _symlink?: string; + _contents?: Buffer | NodeJS.ReadableStream | null; + _cwd?: string; + _base?: string; +} - it('should default cwd to process.cwd', done => { +declare module 'fs' { + class Stats { } +} + +var pipe: (streams: [NodeJS.ReadableStream, NodeJS.WritableStream], cb: (err?: Error) => void) => void = miss.pipe; +var from: (values: any[]) => NodeJS.ReadableStream = miss.from; +var concat: (fn: (d: Buffer) => void) => NodeJS.WritableStream = miss.concat; +var isCloneable: (obj: any) => boolean = cloneable.isCloneable; + +var isWin = (process.platform === 'win32'); + +describe('File', function () { + + describe('isVinyl()', function () { + + it('returns true for a Vinyl object', function (done) { var file = new File(); - file.cwd.should.equal(process.cwd()); + var result = File.isVinyl(file); + expect(result).toEqual(true); done(); }); - it('should default base to cwd', done => { - var cwd = "/"; - var file = new File({cwd: cwd}); - file.basename.should.equal(cwd); + it('returns false for a normal object', function (done) { + var result = File.isVinyl({}); + expect(result).toEqual(false); done(); }); - it('should default base to cwd even when none is given', done => { + it('returns false for null', function (done) { + var result = File.isVinyl(null); + expect(result).toEqual(false); + done(); + }); + + it('returns false for a string', function (done) { + var result = File.isVinyl('foobar'); + expect(result).toEqual(false); + done(); + }); + + it('returns false for a String object', function (done) { + var result = File.isVinyl(new String('foobar')); + expect(result).toEqual(false); + done(); + }); + + it('returns false for a number', function (done) { + var result = File.isVinyl(1); + expect(result).toEqual(false); + done(); + }); + + it('returns false for a Number object', function (done) { + var result = File.isVinyl(new Number(1)); + expect(result).toEqual(false); + done(); + }); + + // This is based on current implementation + // A test was added to document and make aware during internal changes + // TODO: decide if this should be leak-able + it('returns true for a mocked object', function (done) { + var result = File.isVinyl({ _isVinyl: true }); + expect(result).toEqual(true); + done(); + }); + }); + + describe('defaults', function () { + + it('defaults cwd to process.cwd', function (done) { var file = new File(); - file.basename.should.equal(process.cwd()); + expect(file.cwd).toEqual(process.cwd()); done(); }); - it('should default path to null', done => { + it('defaults base to process.cwd', function (done) { var file = new File(); - should.not.exist(file.path); + expect(file.base).toEqual(process.cwd()); done(); }); - it('should default stat to null', done => { + it('defaults base to cwd property', function (done) { + var cwd = path.normalize('/'); + var file = new File({ cwd: cwd }); + expect(file.base).toEqual(cwd); + done(); + }); + + it('defaults path to null', function (done) { var file = new File(); - should.not.exist(file.stat); + expect(file.path).toNotExist(); + expect(file.path).toEqual(null); done(); }); - it('should default contents to null', done => { + it('defaults history to an empty array', function (done) { var file = new File(); - should.not.exist(file.contents); + expect(file.history).toEqual([]); done(); }); - it('should set base to given value', done => { - var val = "/"; - var file = new File({base: val}); - file.basename.should.equal(val); + it('defaults stat to null', function (done) { + var file = new File(); + expect(file.stat).toNotExist(); + expect(file.stat).toEqual(null); done(); }); - it('should set cwd to given value', done => { - var val = "/"; - var file = new File({cwd: val}); - file.cwd.should.equal(val); + it('defaults contents to null', function (done) { + var file = new File(); + expect(file.contents).toNotExist(); + expect(file.contents).toEqual(null); + done(); + }); + }); + + describe('constructor()', function () { + + it('sets base', function (done) { + var val = path.normalize('/'); + var file = new File({ base: val }); + expect(file.base).toEqual(val); done(); }); - it('should set path to given value', done => { - var val = "/test.coffee"; - var file = new File({path: val}); - file.path.should.equal(val); + it('sets cwd', function (done) { + var val = path.normalize('/'); + var file = new File({ cwd: val }); + expect(file.cwd).toEqual(val); done(); }); - it('should set stat to given value', done => { + it('sets path (and history)', function (done) { + var val = path.normalize('/test.coffee'); + var file = new File({ path: val }); + expect(file.path).toEqual(val); + expect(file.history).toEqual([val]); + done(); + }); + + it('sets history (and path)', function (done) { + var val = path.normalize('/test.coffee'); + var file = new File({ history: [val] }); + expect(file.path).toEqual(val); + expect(file.history).toEqual([val]); + done(); + }); + + it('sets stat', function (done) { var val = {}; - var file = new File({stat: val}); - file.stat.should.equal(val); + var file = new File({ stat: val as any as fs.Stats }); + expect(file.stat).toEqual(val); done(); }); - it('should set contents to given value', done => { - var val = new Buffer("test"); - var file = new File({contents: val}); - file.contents.should.equal(val); + it('sets contents', function (done) { + var val = new Buffer('test'); + var file = new File({ contents: val }); + expect(file.contents).toEqual(val); done(); }); - it('should default basename to cwd', done => { - var cwd = "/"; - var file = new File({cwd: cwd}); - file.basename.should.equal(cwd); + it('sets custom properties', function (done) { + var sourceMap = {}; + var file = new File({ sourceMap: sourceMap }) as TestFile; + expect(file.sourceMap).toEqual(sourceMap); done(); }); - it('should default basename to cwd even when none is given', done => { - var file = new File(); - file.basename.should.equal(process.cwd()); + it('normalizes path', function (done) { + var val = '/test/foo/../test.coffee'; + var expected = path.normalize(val); + var file = new File({ path: val }); + expect(file.path).toEqual(expected); + expect(file.history).toEqual([expected]); done(); }); - it('should set basename to given value', done => { - var val = "/"; - var file = new File({base: val}); - file.basename.should.equal(val); + it('normalizes and removes trailing separator from path', function (done) { + var val = '/test/foo/../foo/'; + var expected = path.normalize(val.slice(0, -1)); + var file = new File({ path: val }); + expect(file.path).toEqual(expected); done(); }); - it('should default extname to null', done => { - var cwd = "/"; - var file = new File({cwd: cwd}); - should.not.exist(file.path); + it('normalizes history', function (done) { + var val = [ + '/test/bar/../bar/test.coffee', + '/test/foo/../test.coffee', + ]; + var expected = val.map(function (p) { + return path.normalize(p); + }); + var file = new File({ history: val }); + expect(file.path).toEqual(expected[1]); + expect(file.history).toEqual(expected); done(); }); - it('should default dirname to null', done => { - var cwd = "/"; - var file = new File({cwd: cwd}); - should.not.exist(file.dirname); + it('normalizes and removes trailing separator from history', function (done) { + var val = [ + '/test/foo/../foo/', + '/test/bar/../bar/', + ]; + var expected = val.map(function (p) { + return path.normalize(p.slice(0, -1)); + }); + var file = new File({ history: val }); + expect(file.history).toEqual(expected); done(); }); - }); + it('appends path to history if both exist and different from last', function (done) { + var val = path.normalize('/test/baz/test.coffee'); + var history = [ + path.normalize('/test/bar/test.coffee'), + path.normalize('/test/foo/test.coffee'), + ]; + var file = new File({ path: val, history: history }); - describe('File.isVinyl()', () => { - it('should return true when an object is a Vinyl file', done => { - var file = new File(); - File.isVinyl(file).should.equal(true); + var expectedHistory = history.concat(val); + + expect(file.path).toEqual(val); + expect(file.history).toEqual(expectedHistory); done(); }); - it('should return false when an object is not a Vinyl file', done => { - File.isVinyl({}).should.equal(false); + it('does not append path to history if both exist and same as last', function (done) { + var val = path.normalize('/test/baz/test.coffee'); + var history = [ + path.normalize('/test/bar/test.coffee'), + path.normalize('/test/foo/test.coffee'), + val, + ]; + var file = new File({ path: val, history: history }); + + expect(file.path).toEqual(val); + expect(file.history).toEqual(history); + done(); + }); + + it('does not mutate history array passed in', function (done) { + var val = path.normalize('/test/baz/test.coffee'); + var history = [ + path.normalize('/test/bar/test.coffee'), + path.normalize('/test/foo/test.coffee'), + ]; + var historyCopy = Array.prototype.slice.call(history); + var file = new File({ path: val, history: history }); + + var expectedHistory = history.concat(val); + + expect(file.path).toEqual(val); + expect(file.history).toEqual(expectedHistory); + expect(history).toEqual(historyCopy); done(); }); }); - describe('File.isCustomProp()', () => { - it('should return true when a File property is not managed internally', done => { - File.isCustomProp('foobar').should.equal(true); + describe('isBuffer()', function () { + + it('returns true when the contents are a Buffer', function (done) { + var val = new Buffer('test'); + var file = new File({ contents: val }); + expect(file.isBuffer()).toEqual(true); done(); }); - it('should return false when a File property is managed internally', done => { - File.isCustomProp('cwd').should.equal(false); + it('returns false when the contents are a Stream', function (done) { + var val = from([]); + var file = new File({ contents: val }); + expect(file.isBuffer()).toEqual(false); + done(); + }); + + it('returns false when the contents are null', function (done) { + var file = new File({ contents: null }); + expect(file.isBuffer()).toEqual(false); done(); }); }); - describe('isBuffer()', () => { - it('should return true when the contents are a Buffer', done => { - var val = new Buffer("test"); - var file = new File({contents: val}); - file.isBuffer().should.equal(true); + describe('isStream()', function () { + + it('returns false when the contents are a Buffer', function (done) { + var val = new Buffer('test'); + var file = new File({ contents: val }); + expect(file.isStream()).toEqual(false); done(); }); - it('should return false when the contents are a Stream', done => { - var file = new File({ contents: fakeStream}); - file.isBuffer().should.equal(false); + it('returns true when the contents are a Stream', function (done) { + var val = from([]); + var file = new File({ contents: val }); + expect(file.isStream()).toEqual(true); done(); }); - it('should return false when the contents are a null', done => { - var file = new File({contents: null}); - file.isBuffer().should.equal(false); + it('returns false when the contents are null', function (done) { + var file = new File({ contents: null }); + expect(file.isStream()).toEqual(false); done(); }); }); - describe('isStream()', () => { - it('should return false when the contents are a Buffer', done => { - var val = new Buffer("test"); - var file = new File({contents: val}); - file.isStream().should.equal(false); + describe('isNull()', function () { + + it('returns false when the contents are a Buffer', function (done) { + var val = new Buffer('test'); + var file = new File({ contents: val }); + expect(file.isNull()).toEqual(false); done(); }); - it('should return true when the contents are a Stream', done => { - var file = new File({ contents: fakeStream}); - file.isStream().should.equal(true); + it('returns false when the contents are a Stream', function (done) { + var val = from([]); + var file = new File({ contents: val }); + expect(file.isNull()).toEqual(false); done(); }); - it('should return false when the contents are a null', done => { - var file = new File({contents: null}); - file.isStream().should.equal(false); + it('returns true when the contents are null', function (done) { + var file = new File({ contents: null }); + expect(file.isNull()).toEqual(true); done(); }); }); - describe('isNull()', () => { - it('should return false when the contents are a Buffer', done => { - var val = new Buffer("test"); - var file = new File({contents: val}); - file.isNull().should.equal(false); + describe('isDirectory()', function () { + var fakeStat = { + isDirectory: function () { + return true; + }, + } as any as fs.Stats; + + it('returns false when the contents are a Buffer', function (done) { + var val = new Buffer('test'); + var file = new File({ contents: val, stat: fakeStat }); + expect(file.isDirectory()).toEqual(false); done(); }); - it('should return false when the contents are a Stream', done => { - var file = new File({ contents: fakeStream}); - file.isNull().should.equal(false); + it('returns false when the contents are a Stream', function (done) { + var val = from([]); + var file = new File({ contents: val, stat: fakeStat }); + expect(file.isDirectory()).toEqual(false); done(); }); - it('should return true when the contents are a null', done => { - var file = new File({contents: null}); - file.isNull().should.equal(true); + it('returns true when the contents are null & stat.isDirectory is true', function (done) { + var file = new File({ contents: null, stat: fakeStat }); + expect(file.isDirectory()).toEqual(true); + done(); + }); + + it('returns false when stat exists but does not contain an isDirectory method', function (done) { + var file = new File({ contents: null, stat: {} as any as fs.Stats }); + expect(file.isDirectory()).toEqual(false); + done(); + }); + + it('returns false when stat does not exist', function (done) { + var file = new File({ contents: null }); + expect(file.isDirectory()).toEqual(false); done(); }); }); - describe('clone()', () => { - it('should copy all attributes over with Buffer', done => { + describe('isSymbolic()', function () { + var fakeStat = { + isSymbolicLink: function () { + return true; + }, + } as any as fs.Stats; + + it('returns false when the contents are a Buffer', function (done) { + var val = new Buffer('test'); + var file = new File({ contents: val, stat: fakeStat }); + expect(file.isSymbolic()).toEqual(false); + done(); + }); + + it('returns false when the contents are a Stream', function (done) { + var val = from([]); + var file = new File({ contents: val, stat: fakeStat }); + expect(file.isSymbolic()).toEqual(false); + done(); + }); + + it('returns true when the contents are null & stat.isSymbolicLink is true', function (done) { + var file = new File({ contents: null, stat: fakeStat }); + expect(file.isSymbolic()).toEqual(true); + done(); + }); + + it('returns false when stat exists but does not contain an isSymbolicLink method', function (done) { + var file = new File({ contents: null, stat: {} as any as fs.Stats }); + expect(file.isSymbolic()).toEqual(false); + done(); + }); + + it('returns false when stat does not exist', function (done) { + var file = new File({ contents: null }); + expect(file.isSymbolic()).toEqual(false); + done(); + }); + }); + + describe('clone()', function () { + + it('copies all attributes over with Buffer contents', function (done) { var options = { - cwd: "/", - base: "/test/", - path: "/test/test.coffee", - contents: new Buffer("test") + cwd: '/', + base: '/test/', + path: '/test/test.coffee', + contents: new Buffer('test'), }; var file = new File(options); var file2 = file.clone(); - file2.should.not.equal(file, 'refs should be different'); - file2.cwd.should.equal(file.cwd); - file2.basename.should.equal(file.basename); - file2.path.should.equal(file.path); - - let fileContents = file.contents; - let file2Contents = file2.contents; - - file2Contents.should.not.equal(fileContents, 'buffer ref should be different'); - - let fileUtf8Contents = fileContents instanceof Buffer ? - fileContents.toString('utf8') : - (fileContents).toString(); - let file2Utf8Contents = file2Contents instanceof Buffer ? - file2Contents.toString('utf8') : - (file2Contents).toString(); - - file2Utf8Contents.should.equal(fileUtf8Contents); + expect(file2).toNotBe(file); + expect(file2.cwd).toEqual(file.cwd); + expect(file2.base).toEqual(file.base); + expect(file2.path).toEqual(file.path); + expect(file2.contents).toNotBe(file.contents); + expect(file2.contents.toString('utf8')).toEqual(file.contents.toString('utf8')); done(); }); - it('should copy all attributes over with Stream', done => { + it('assigns Buffer content reference when contents option is false', function (done) { var options = { - cwd: "/", - base: "/test/", - path: "/test/test.coffee", - contents: fakeStream + cwd: '/', + base: '/test/', + path: '/test/test.js', + contents: new Buffer('test'), + }; + var file = new File(options); + + var copy1 = file.clone({ contents: false }); + expect(copy1.contents).toBe(file.contents); + + var copy2 = file.clone(); + expect(copy2.contents).toNotBe(file.contents); + + // TypeScript: expected compilation error + //var copy3 = file.clone({ contents: 'invalid' }); + //expect(copy3.contents).toNotBe(file.contents); + //done(); + }); + + it('copies all attributes over with Stream contents', function (done) { + var options = { + cwd: '/', + base: '/test/', + path: '/test/test.coffee', + contents: from(['wa', 'dup']), }; var file = new File(options); var file2 = file.clone(); - file2.should.not.equal(file, 'refs should be different'); - file2.cwd.should.equal(file.cwd); - file2.basename.should.equal(file.basename); - file2.path.should.equal(file.path); - file2.contents.should.equal(file.contents, 'stream ref should be the same'); - done(); + expect(file2).toNotBe(file); + expect(file2.cwd).toEqual(file.cwd); + expect(file2.base).toEqual(file.base); + expect(file2.path).toEqual(file.path); + expect(file2.contents).toNotBe(file.contents); + + var ends = 2; + var data: Buffer; + var data2: Buffer; + + function assert(err: any) { + if (err) { + done(err); + return; + } + + if (--ends === 0) { + expect(data).toNotBe(data2); + expect(data.toString('utf8')).toEqual(data2.toString('utf8')); + done(); + } + } + + pipe([ + file.contents, + concat(function (d) { + data = d; + }), + ], assert); + + pipe([ + file2.contents, + concat(function (d) { + data2 = d; + }), + ], assert); }); - it('should copy all attributes over with null', done => { + it('does not start flowing until all clones flows (data)', function (done) { var options = { - cwd: "/", - base: "/test/", - path: "/test/test.coffee", - contents: fakeStream + cwd: '/', + base: '/test/', + path: '/test/test.coffee', + contents: from(['wa', 'dup']), + }; + var file = new File(options); + var file2 = file.clone(); + var ends = 2; + + var data = ''; + var data2 = ''; + + function assert() { + if (--ends === 0) { + expect(data).toEqual(data2); + done(); + } + } + + // Start flowing file2 + file2.contents.on('data', function (chunk: Buffer) { + data2 += chunk.toString('utf8'); + }); + + process.nextTick(function () { + // Nothing was written yet + expect(data).toEqual(''); + expect(data2).toEqual(''); + + // Starts flowing file + file.contents.on('data', function (chunk: Buffer) { + data += chunk.toString('utf8'); + }); + }); + + file2.contents.on('end', assert); + file.contents.on('end', assert); + }); + + it('does not start flowing until all clones flows (readable)', function (done) { + var options = { + cwd: '/', + base: '/test/', + path: '/test/test.coffee', + contents: from(['wa', 'dup']), }; var file = new File(options); var file2 = file.clone(); - file2.should.not.equal(file, 'refs should be different'); - file2.cwd.should.equal(file.cwd); - file2.basename.should.equal(file.basename); - file2.path.should.equal(file.path); - should.not.exist(file2.contents); + var data2 = ''; + + function assert(data: Buffer) { + expect(data.toString('utf8')).toEqual(data2); + } + + // Start flowing file2 + file2.contents.on('readable', function (this: NodeJS.ReadableStream) { + var chunk: string | Buffer; + while ((chunk = this.read()) !== null) { + data2 += chunk.toString(); + } + }); + + pipe([ + file.contents, + concat(assert), + ], done); + }); + + it('copies all attributes over with null contents', function (done) { + var options = { + cwd: '/', + base: '/test/', + path: '/test/test.coffee', + contents: null, + }; + var file = new File(options); + var file2 = file.clone(); + + expect(file2).toNotBe(file); + expect(file2.cwd).toEqual(file.cwd); + expect(file2.base).toEqual(file.base); + expect(file2.path).toEqual(file.path); + expect(file2.contents).toNotExist(); done(); }); - it('should properly clone the `stat` property', done => { + it('properly clones the `stat` property', function (done) { var options = { - cwd: "/", - base: "/test/", - path: "/test/test.js", - contents: new Buffer("test"), - stat: fs.statSync(__filename) + cwd: '/', + base: '/test/', + path: '/test/test.js', + contents: new Buffer('test'), + stat: fs.statSync(__filename), }; var file = new File(options); var copy = file.clone(); - // ReSharper disable WrongExpressionStatement - copy.stat.isFile().should.be.true; - // ReSharper restore WrongExpressionStatement + expect(copy.stat).toExist(); + if (copy.stat != null) { + expect(copy.stat.isFile()).toEqual(true); + expect(copy.stat.isDirectory()).toEqual(false); + expect(file.stat).toBeAn(fs.Stats); + expect(copy.stat).toBeAn(fs.Stats); + } + done(); + }); + + it('properly clones the `history` property', function (done) { + var options = { + cwd: path.normalize('/'), + base: path.normalize('/test/'), + path: path.normalize('/test/test.js'), + contents: new Buffer('test'), + }; + + var file = new File(options); + var copy = file.clone(); + + expect(copy.history[0]).toEqual(options.path); + copy.path = 'lol'; + expect(file.path).toNotEqual(copy.path); + done(); + }); + + it('copies custom properties', function (done) { + var options = { + cwd: '/', + base: '/test/', + path: '/test/test.coffee', + contents: null, + custom: { meta: {} }, + }; + + var file = new File(options) as TestFile; + var file2 = file.clone(); + + expect(file2).toNotBe(file); + expect(file2.cwd).toEqual(file.cwd); + expect(file2.base).toEqual(file.base); + expect(file2.path).toEqual(file.path); + expect(file2.custom).toNotBe(file.custom); + expect(file2.custom.meta).toNotBe(file.custom.meta); + expect(file2.custom).toEqual(file.custom); + done(); + }); + + it('copies history', function (done) { + var options = { + cwd: '/', + base: '/test/', + path: '/test/test.coffee', + contents: null, + }; + var history = [ + path.normalize('/test/test.coffee'), + path.normalize('/test/test.js'), + path.normalize('/test/test-938di2s.js'), + ]; + + var file = new File(options); + file.path = history[1]; + file.path = history[2]; + var file2 = file.clone(); + + expect(file2.history).toEqual(history); + expect(file2.history).toNotBe(file.history); + expect(file2.path).toEqual(history[2]); + done(); + }); + + it('supports deep & shallow copy of all attributes', function (done) { + var options = { + cwd: '/', + base: '/test/', + path: '/test/test.coffee', + contents: null, + custom: { meta: {} }, + }; + + var file = new File(options) as TestFile; + + var file2 = file.clone(); + expect(file2.custom).toEqual(file.custom); + expect(file2.custom).toNotBe(file.custom); + expect(file2.custom.meta).toEqual(file.custom.meta); + expect(file2.custom.meta).toNotBe(file.custom.meta); + + var file3 = file.clone(true); + expect(file3.custom).toEqual(file.custom); + expect(file3.custom).toNotBe(file.custom); + expect(file3.custom.meta).toEqual(file.custom.meta); + expect(file3.custom.meta).toNotBe(file.custom.meta); + + var file4 = file.clone({ deep: true }); + expect(file4.custom).toEqual(file.custom); + expect(file4.custom).toNotBe(file.custom); + expect(file4.custom.meta).toEqual(file.custom.meta); + expect(file4.custom.meta).toNotBe(file.custom.meta); + + var file5 = file.clone(false); + expect(file5.custom).toEqual(file.custom); + expect(file5.custom).toBe(file.custom); + expect(file5.custom.meta).toEqual(file.custom.meta); + expect(file5.custom.meta).toBe(file.custom.meta); + + var file6 = file.clone({ deep: false }); + expect(file6.custom).toEqual(file.custom); + expect(file6.custom).toBe(file.custom); + expect(file6.custom.meta).toEqual(file.custom.meta); + expect(file6.custom.meta).toBe(file.custom.meta); done(); }); + + // TypeScript: known issue + // Compilation error: "Base constructors must all have the same return type." + // it('supports inheritance', function (done) { + // class ExtendedFile extends File { } + // var file = new ExtendedFile(); + // var file2 = file.clone(); + + // expect(file2).toNotBe(file); + // expect(file2.constructor).toBe(ExtendedFile); + // expect(file2).toBeAn(ExtendedFile); + // expect(file2).toBeA(File); + // expect(ExtendedFile.prototype.isPrototypeOf(file2)).toEqual(true); + // expect(File.prototype.isPrototypeOf(file2)).toEqual(true); + // done(); + // }); }); - describe('pipe()', () => { - it('should write to stream with Buffer', done => { - var options = { - cwd: "/", - base: "/test/", - path: "/test/test.coffee", - contents: new Buffer("test") - }; - var file = new File(options); - var stream = new Stream.PassThrough(); - stream.on('data', (chunk: any) => { - should.exist(chunk); - (chunk instanceof Buffer).should.equal(true, 'should write as a buffer'); - chunk.toString('utf8').should.equal(options.contents.toString('utf8')); - }); - stream.on('end', () => { - done(); - }); - var ret = file.pipe(stream); - ret.should.equal(stream, 'should return the stream'); - }); + describe('inspect()', function () { - it('should pipe to stream with Stream', done => { - var testChunk = new Buffer("test"); - var options = { - cwd: "/", - base: "/test/", - path: "/test/test.coffee", - contents: new Stream.PassThrough() - }; - var file = new File(options); - var stream = new Stream.PassThrough(); - stream.on('data', (chunk: any) => { - should.exist(chunk); - (chunk instanceof Buffer).should.equal(true, 'should write as a buffer'); - chunk.toString('utf8').should.equal(testChunk.toString('utf8')); - done(); - }); - var ret = file.pipe(stream); - ret.should.equal(stream, 'should return the stream'); - - let fileContents = file.contents; - if (fileContents instanceof Buffer) { - fileContents.write(testChunk.toString()); - } - }); - - it('should do nothing with null', done => { - var options = { - cwd: "/", - base: "/test/", - path: "/test/test.coffee", - contents: fakeStream - }; - var file = new File(options); - var stream = new Stream.PassThrough(); - stream.on('data', () => { - throw new Error("should not write"); - }); - stream.on('end', () => { - done(); - }); - var ret = file.pipe(stream); - ret.should.equal(stream, 'should return the stream'); - }); - - it('should write to stream with Buffer', done => { - var options = { - cwd: "/", - base: "/test/", - path: "/test/test.coffee", - contents: new Buffer("test") - }; - var file = new File(options); - var stream = new Stream.PassThrough(); - stream.on('data', (chunk: any) => { - should.exist(chunk); - (chunk instanceof Buffer).should.equal(true, 'should write as a buffer'); - chunk.toString('utf8').should.equal(options.contents.toString('utf8')); - done(); - }); - stream.on('end', () => { - throw new Error("should not end"); - }); - var ret = file.pipe(stream, {end: false}); - ret.should.equal(stream, 'should return the stream'); - }); - - it('should pipe to stream with Stream', done => { - var testChunk = new Buffer("test"); - var options = { - cwd: "/", - base: "/test/", - path: "/test/test.coffee", - contents: new Stream.PassThrough() - }; - var file = new File(options); - var stream = new Stream.PassThrough(); - stream.on('data', (chunk: any) => { - should.exist(chunk); - (chunk instanceof Buffer).should.equal(true, 'should write as a buffer'); - chunk.toString('utf8').should.equal(testChunk.toString('utf8')); - done(); - }); - stream.on('end', () => { - throw new Error("should not end"); - }); - var ret = file.pipe(stream, {end: false}); - ret.should.equal(stream, 'should return the stream'); - - let fileContents = file.contents; - if (fileContents instanceof Buffer) { - fileContents.write(testChunk.toString()); - } - }); - - it('should do nothing with null', done => { - var options = { - cwd: "/", - base: "/test/", - path: "/test/test.coffee", - contents: fakeStream - }; - var file = new File(options); - var stream = new Stream.PassThrough(); - stream.on('data', () => { - throw new Error("should not write"); - }); - stream.on('end', () => { - throw new Error("should not end"); - }); - var ret = file.pipe(stream, {end: false}); - ret.should.equal(stream, 'should return the stream'); - process.nextTick(done); - }); - }); - - describe('inspect()', () => { - it('should return correct format when no contents and no path', done => { + it('returns correct format when no contents and no path', function (done) { var file = new File(); - file.inspect().should.equal(''); + expect(file.inspect()).toEqual(''); done(); }); - it('should return correct format when Buffer and no path', done => { - var val = new Buffer("test"); - var file = new File({ - contents: val - }); - file.inspect().should.equal('>'); + it('returns correct format when Buffer contents and no path', function (done) { + var val = new Buffer('test'); + var file = new File({ contents: val }); + expect(file.inspect()).toEqual('>'); done(); }); - it('should return correct format when Buffer and relative path', done => { - var val = new Buffer("test"); + it('returns correct format when Buffer contents and relative path', function (done) { + var val = new Buffer('test'); var file = new File({ - cwd: "/", - base: "/test/", - path: "/test/test.coffee", - contents: val + cwd: '/', + base: '/test/', + path: '/test/test.coffee', + contents: val, }); - file.inspect().should.equal('>'); + expect(file.inspect()).toEqual('>'); done(); }); - it('should return correct format when Buffer and only path and no base', done => { - var val = new Buffer("test"); + it('returns correct format when Stream contents and relative path', function (done) { var file = new File({ - cwd: "/", - path: "/test/test.coffee", - contents: val + cwd: '/', + base: '/test/', + path: '/test/test.coffee', + contents: from([]), }); - delete file.basename; - file.inspect().should.equal('>'); + expect(file.inspect()).toEqual('>'); done(); }); - it('should return correct format when Stream and relative path', done => { + it('returns correct format when null contents and relative path', function (done) { var file = new File({ - cwd: "/", - base: "/test/", - path: "/test/test.coffee", - contents: new Stream.PassThrough() + cwd: '/', + base: '/test/', + path: '/test/test.coffee', + contents: null, }); - file.inspect().should.equal('>'); - done(); - }); - - it('should return correct format when null and relative path', done => { - var file = new File({ - cwd: "/", - base: "/test/", - path: "/test/test.coffee", - contents: null - }); - file.inspect().should.equal(''); + expect(file.inspect()).toEqual(''); done(); }); }); - describe('contents get/set', () => { - it('should work with Buffer', done => { - var val = new Buffer("test"); + describe('contents get/set', function () { + + it('returns _contents', function (done) { + var val = new Buffer('test'); + var file = new File() as TestFile; + file._contents = val; + expect(file.contents).toEqual(val); + done(); + }); + + it('sets _contents', function (done) { + var val = new Buffer('test'); + var file = new File() as TestFile; + file.contents = val; + expect(file._contents).toEqual(val); + done(); + }); + + it('sets a Buffer', function (done) { + var val = new Buffer('test'); var file = new File(); file.contents = val; - file.contents.should.equal(val); + expect(file.contents).toEqual(val); done(); }); - it('should work with Stream', done => { - var val = new Stream.PassThrough(); + it('wraps Stream in Cloneable', function (done) { + var val = from([]); var file = new File(); file.contents = val; - file.contents.should.equal(val); + expect(isCloneable(file.contents)).toEqual(true); done(); }); - it('should work with null', done => { + it('does not double wrap a Cloneable', function (done) { + var val = from([]); + var clone = cloneable(val); var file = new File(); - file.contents = null; - (file.contents === null).should.equal(true); + file.contents = clone; + expect((file.contents as any)._original).toBe(val); done(); }); - it('should not work with string', done => { - var val = "test"; + it('sets null', function (done) { + var val = null; var file = new File(); - try { - file.contents = new Buffer(val); - } catch (err) { - should.exist(err); - done(); + file.contents = val; + expect(file.contents).toEqual(null); + done(); + }); + + // TypeScript: expected compilation error + // it('does not set a string', function (done) { + // var val = 'test'; + // var file = new File(); + // function invalid() { + // file.contents = val; + // } + // expect(invalid).toThrow(); + // done(); + // }); + }); + + describe('cwd get/set', function () { + + it('returns _cwd', function (done) { + var val = '/test'; + var file = new File() as TestFile; + file._cwd = val; + expect(file.cwd).toEqual(val); + done(); + }); + + it('sets _cwd', function (done) { + var val = '/test'; + var file = new File() as TestFile; + file.cwd = val; + expect(file._cwd).toEqual(path.normalize(val)); + done(); + }); + + it('normalizes and removes trailing separator on set', function (done) { + var val = '/test/foo/../foo/'; + var expected = path.normalize(val.slice(0, -1)); + var file = new File(); + + file.cwd = val; + + expect(file.cwd).toEqual(expected); + + var val2 = '\\test\\foo\\..\\foo\\'; + var expected2 = path.normalize(isWin ? val2.slice(0, -1) : val2); + + file.cwd = val2; + + expect(file.cwd).toEqual(expected2); + done(); + }); + + // TypeScript: expected compilation error + // it('throws on set with invalid values', function (done) { + // var invalidValues = [ + // '', + // null, + // undefined, + // true, + // false, + // 0, + // Infinity, + // NaN, + // {}, + // [], + // ]; + // var file = new File(); + + // invalidValues.forEach(function (val) { + // function invalid() { + // file.cwd = val; + // } + // expect(invalid).toThrow('cwd must be a non-empty string.'); + // }); + + // done(); + // }); + }); + + describe('base get/set', function () { + + it('proxies cwd when omitted', function (done) { + var file = new File({ cwd: '/test' }); + expect(file.base).toEqual(file.cwd); + done(); + }); + + it('proxies cwd when same', function (done) { + var file = new File({ + cwd: '/test', + base: '/test', + }); + file.cwd = '/foo/'; + expect(file.base).toEqual(file.cwd); + + var file2 = new File({ + cwd: '/test', + }); + file2.base = '/test/'; + file2.cwd = '/foo/'; + expect(file2.base).toEqual(file.cwd); + done(); + }); + + // TypeScript: known issue, see the comment for the `base` property. + // it('proxies to cwd when null or undefined', function (done) { + // var file = new File({ + // cwd: '/foo', + // base: '/bar', + // }); + // expect(file.base).toNotEqual(file.cwd); + // file.base = null; + // expect(file.base).toEqual(file.cwd); + // file.base = '/bar/'; + // expect(file.base).toNotEqual(file.cwd); + // file.base = undefined; + // expect(file.base).toEqual(file.cwd); + // done(); + // }); + + it('returns _base', function (done) { + var val = '/test/'; + var file = new File() as TestFile; + file._base = val; + expect(file.base).toEqual(val); + done(); + }); + + it('sets _base', function (done) { + var val = '/test/foo'; + var file = new File() as TestFile; + file.base = val; + expect(file._base).toEqual(path.normalize(val)); + done(); + }); + + it('normalizes and removes trailing separator on set', function (done) { + var val = '/test/foo/../foo/'; + var expected = path.normalize(val.slice(0, -1)); + var file = new File(); + + file.base = val; + + expect(file.base).toEqual(expected); + + var val2 = '\\test\\foo\\..\\foo\\'; + var expected2 = path.normalize(isWin ? val2.slice(0, -1) : val2); + + file.base = val2; + + expect(file.base).toEqual(expected2); + done(); + }); + + // TypeScript: expected compilation error + // it('throws on set with invalid values', function (done) { + // var invalidValues = [ + // true, + // false, + // 1, + // 0, + // Infinity, + // NaN, + // '', + // {}, + // [], + // ]; + // var file = new File(); + + // invalidValues.forEach(function (val) { + // function invalid() { + // file.base = val; + // } + // expect(invalid).toThrow('base must be a non-empty string, or null/undefined.'); + // }); + + // done(); + // }); + }); + + describe('relative get/set', function () { + + it('throws on set', function (done) { + var file = new File(); + + function invalid() { + file.relative = 'test'; } - }); - }); - describe('relative get/set', () => { - it('should error on set', done => { + expect(invalid).toThrow('File.relative is generated from the base and path attributes. Do not modify it.'); + done(); + }); + + it('throws on get with no path', function (done) { var file = new File(); - try { - file.relative = "test"; - } catch (err) { - should.exist(err); - done(); + + function invalid() { + file.relative; } + + expect(invalid).toThrow('No path specified! Can not get relative.'); + done(); }); - it('should error on get when no base', done => { - var a: string; + it('returns a relative path from base', function (done) { + var file = new File({ + base: '/test/', + path: '/test/test.coffee', + }); + + expect(file.relative).toEqual('test.coffee'); + done(); + }); + + it('returns a relative path from cwd', function (done) { + var file = new File({ + cwd: '/', + path: '/test/test.coffee', + }); + + expect(file.relative).toEqual(path.normalize('test/test.coffee')); + done(); + }); + + it('does not append separator when directory', function (done) { + var file = new File({ + base: '/test', + path: '/test/foo/bar', + stat: { + isDirectory: function () { + return true; + }, + } as any as fs.Stats, + }); + + expect(file.relative).toEqual(path.normalize('foo/bar')); + done(); + }); + + it('does not append separator when symlink', function (done) { + var file = new File({ + base: '/test', + path: '/test/foo/bar', + stat: { + isSymbolicLink: function () { + return true; + }, + } as any as fs.Stats, + }); + + expect(file.relative).toEqual(path.normalize('foo/bar')); + done(); + }); + + it('does not append separator when directory & symlink', function (done) { + var file = new File({ + base: '/test', + path: '/test/foo/bar', + stat: { + isDirectory: function () { + return true; + }, + isSymbolicLink: function () { + return true; + }, + } as any as fs.Stats, + }); + + expect(file.relative).toEqual(path.normalize('foo/bar')); + done(); + }); + }); + + describe('dirname get/set', function () { + + it('throws on get with no path', function (done) { var file = new File(); - delete file.basename; - try { - // ReSharper disable once AssignedValueIsNeverUsed - a = file.relative; - } catch (err) { - should.exist(err); - done(); + + function invalid() { + file.dirname; } + + expect(invalid).toThrow('No path specified! Can not get dirname.'); + done(); }); - it('should error on get when no path', done => { - var a: string; + it('returns the dirname without trailing separator', function (done) { + var file = new File({ + cwd: '/', + base: '/test', + path: '/test/test.coffee', + }); + + expect(file.dirname).toEqual(path.normalize('/test')); + done(); + }); + + it('throws on set with no path', function (done) { var file = new File(); - try { - // ReSharper disable once AssignedValueIsNeverUsed - a = file.relative; - } catch (err) { - should.exist(err); - done(); + + function invalid() { + file.dirname = '/test'; } - }); - it('should return a relative path from base', done => { - var file = new File({ - cwd: "/", - base: "/test/", - path: "/test/test.coffee" - }); - file.relative.should.equal("test.coffee"); + expect(invalid).toThrow('No path specified! Can not set dirname.'); done(); }); - it('should return a relative path from cwd', done => { + it('replaces the dirname of the path', function (done) { var file = new File({ - cwd: "/", - path: "/test/test.coffee" + cwd: '/', + base: '/test/', + path: '/test/test.coffee', }); - file.relative.should.equal("test/test.coffee"); + + file.dirname = '/test/foo'; + expect(file.path).toEqual(path.normalize('/test/foo/test.coffee')); done(); }); }); - describe('path get/set', () => { + describe('basename get/set', function () { - it('should return an absolute path', done => { - var file = new File({ - cwd: "/", - base: "/test/", - path: "/test/test.coffee" - }); - file.path.should.equal("/test/test.coffee"); - done(); - }); - - }); - - describe('history get', () => { - it('should error on set', done => { + it('throws on get with no path', function (done) { var file = new File(); - try { - file.history = []; - } catch (err) { - should.exist(err); - done(); + + function invalid() { + return file.basename; } + + expect(invalid).toThrow('No path specified! Can not get basename.'); + done(); }); - it('should return an history', done => { + it('returns the basename of the path', function (done) { var file = new File({ - cwd: "/", - base: "/test/", - path: "/test/test.coffee" + cwd: '/', + base: '/test/', + path: '/test/test.coffee', }); - file.history.should.equal(["/test/test.coffee"]); + + expect(file.basename).toEqual('test.coffee'); done(); }); - }); - - describe('dirname get', () => { - - it('should return an dirname', done => { + it('does not append trailing separator when directory', function (done) { var file = new File({ - cwd: "/", - base: "/test/", - path: "/test/test.coffee" + path: '/test/foo', + stat: { + isDirectory: function () { + return true; + }, + } as any as fs.Stats, }); - file.dirname.should.equal("test"); + + expect(file.basename).toEqual('foo'); done(); }); - it('should set dirname to given value', done => { - var file = new File(); - file.dirname = ".ext" - file.dirname.should.equal(".ext") + it('does not append trailing separator when symlink', function (done) { + var file = new File({ + path: '/test/foo', + stat: { + isSymbolicLink: function () { + return true; + }, + } as any as fs.Stats, + }); + + expect(file.basename).toEqual('foo'); done(); }); - it('should set dirname to null', done => { + it('does not append trailing separator when directory & symlink', function (done) { + var file = new File({ + path: '/test/foo', + stat: { + isDirectory: function () { + return true; + }, + isSymbolicLink: function () { + return true; + }, + } as any as fs.Stats, + }); + + expect(file.basename).toEqual('foo'); + done(); + }); + + it('removes trailing separator', function (done) { + var file = new File({ + path: '/test/foo/', + }); + + expect(file.basename).toEqual('foo'); + done(); + }); + + it('removes trailing separator when directory', function (done) { + var file = new File({ + path: '/test/foo/', + stat: { + isDirectory: function () { + return true; + }, + } as any as fs.Stats, + }); + + expect(file.basename).toEqual('foo'); + done(); + }); + + it('removes trailing separator when symlink', function (done) { + var file = new File({ + path: '/test/foo/', + stat: { + isSymbolicLink: function () { + return true; + }, + } as any as fs.Stats, + }); + + expect(file.basename).toEqual('foo'); + done(); + }); + + it('removes trailing separator when directory & symlink', function (done) { + var file = new File({ + path: '/test/foo/', + stat: { + isDirectory: function () { + return true; + }, + isSymbolicLink: function () { + return true; + }, + } as any as fs.Stats, + }); + + expect(file.basename).toEqual('foo'); + done(); + }); + + it('throws on set with no path', function (done) { var file = new File(); - file.dirname = null - should.not.exist(file.dirname) + + function invalid() { + file.basename = 'test.coffee'; + } + + expect(invalid).toThrow('No path specified! Can not set basename.'); + done(); + }); + + it('replaces the basename of the path', function (done) { + var file = new File({ + cwd: '/', + base: '/test/', + path: '/test/test.coffee', + }); + + file.basename = 'foo.png'; + expect(file.path).toEqual(path.normalize('/test/foo.png')); done(); }); }); - describe('extname get/set', () => { + describe('stem get/set', function () { - it('should return an extname', done => { + it('throws on get with no path', function (done) { + var file = new File(); + + function invalid() { + file.stem; + } + + expect(invalid).toThrow('No path specified! Can not get stem.'); + done(); + }); + + it('returns the stem of the path', function (done) { var file = new File({ - cwd: "/", - base: "/test/", - path: "/test/test.coffee" + cwd: '/', + base: '/test/', + path: '/test/test.coffee', }); - file.dirname.should.equal(".coffee"); + + expect(file.stem).toEqual('test'); done(); }); - it('should set extname to given value', done => { + it('throws on set with no path', function (done) { var file = new File(); - file.extname = ".ext" - file.extname.should.equal(".ext") + + function invalid() { + file.stem = 'test.coffee'; + } + + expect(invalid).toThrow('No path specified! Can not set stem.'); done(); }); - it('should set extname to null', done => { - var file = new File(); - file.extname = null - should.not.exist(file.extname) + it('replaces the stem of the path', function (done) { + var file = new File({ + cwd: '/', + base: '/test/', + path: '/test/test.coffee', + }); + + file.stem = 'foo'; + expect(file.path).toEqual(path.normalize('/test/foo.coffee')); done(); }); }); + describe('extname get/set', function () { + + it('throws on get with no path', function (done) { + var file = new File(); + + function invalid() { + file.extname; + } + + expect(invalid).toThrow('No path specified! Can not get extname.'); + done(); + }); + + it('returns the extname of the path', function (done) { + var file = new File({ + cwd: '/', + base: '/test/', + path: '/test/test.coffee', + }); + + expect(file.extname).toEqual('.coffee'); + done(); + }); + + it('throws on set with no path', function (done) { + var file = new File(); + + function invalid() { + file.extname = '.coffee'; + } + + expect(invalid).toThrow('No path specified! Can not set extname.'); + done(); + }); + + it('replaces the extname of the path', function (done) { + var file = new File({ + cwd: '/', + base: '/test/', + path: '/test/test.coffee', + }); + + file.extname = '.png'; + expect(file.path).toEqual(path.normalize('/test/test.png')); + done(); + }); + }); + + describe('path get/set', function () { + + it('records path in history upon instantiation', function (done) { + var file = new File({ + cwd: '/', + path: '/test/test.coffee', + }); + var history = [ + path.normalize('/test/test.coffee'), + ]; + + expect(file.path).toEqual(history[0]); + expect(file.history).toEqual(history); + done(); + }); + + it('records path in history when set', function (done) { + var val = path.normalize('/test/test.js'); + var file = new File({ + cwd: '/', + path: '/test/test.coffee', + }); + var history = [ + path.normalize('/test/test.coffee'), + val, + ]; + + file.path = val; + expect(file.path).toEqual(val); + expect(file.history).toEqual(history); + + var val2 = path.normalize('/test/test.es6'); + history.push(val2); + + file.path = val2; + expect(file.path).toEqual(val2); + expect(file.history).toEqual(history); + done(); + }); + + it('does not record path in history when set to the current path', function (done) { + var val = path.normalize('/test/test.coffee'); + var file = new File({ + cwd: '/', + path: val, + }); + var history = [ + val, + ]; + + file.path = val; + file.path = val; + expect(file.path).toEqual(val); + expect(file.history).toEqual(history); + done(); + }); + + it('does not record path in history when set to empty string', function (done) { + var val = path.normalize('/test/test.coffee'); + var file = new File({ + cwd: '/', + path: val, + }); + var history = [ + val, + ]; + + file.path = ''; + expect(file.path).toEqual(val); + expect(file.history).toEqual(history); + done(); + }); + + // TypeScript: known issue, see the comment for the `base` property. + // it('throws on set with null path', function(done) { + // var file = new File(); + + // expect(file.path).toNotExist(); + // expect(file.history).toEqual([]); + + // function invalid() { + // file.path = null; + // } + + // expect(invalid).toThrow('path should be a string.'); + // done(); + // }); + + it('normalizes the path upon set', function (done) { + var val = '/test/foo/../test.coffee'; + var expected = path.normalize(val); + var file = new File(); + + file.path = val; + + expect(file.path).toEqual(expected); + expect(file.history).toEqual([expected]); + done(); + }); + + it('removes the trailing separator upon set', function (done) { + var file = new File(); + file.path = '/test/'; + + expect(file.path).toEqual(path.normalize('/test')); + expect(file.history).toEqual([path.normalize('/test')]); + done(); + }); + + it('removes the trailing separator upon set when directory', function (done) { + var file = new File({ + stat: { + isDirectory: function () { + return true; + }, + } as any as fs.Stats, + }); + file.path = '/test/'; + + expect(file.path).toEqual(path.normalize('/test')); + expect(file.history).toEqual([path.normalize('/test')]); + done(); + }); + + it('removes the trailing separator upon set when symlink', function (done) { + var file = new File({ + stat: { + isSymbolicLink: function () { + return true; + }, + } as any as fs.Stats, + }); + file.path = '/test/'; + + expect(file.path).toEqual(path.normalize('/test')); + expect(file.history).toEqual([path.normalize('/test')]); + done(); + }); + + it('removes the trailing separator upon set when directory & symlink', function (done) { + var file = new File({ + stat: { + isDirectory: function () { + return true; + }, + isSymbolicLink: function () { + return true; + }, + } as any as fs.Stats, + }); + file.path = '/test/'; + + expect(file.path).toEqual(path.normalize('/test')); + expect(file.history).toEqual([path.normalize('/test')]); + done(); + }); + }); + + describe('symlink get/set', function () { + + it('return null on get with no symlink', function (done) { + var file = new File(); + + expect(file.symlink).toEqual(null); + done(); + }); + + it('returns _symlink', function (done) { + var val = '/test/test.coffee'; + var file = new File() as TestFile; + file._symlink = val; + + expect(file.symlink).toEqual(val); + done(); + }); + + it('throws on set with non-string', function (done) { + var file = new File(); + + function invalid() { + file.symlink = null; + } + + expect(invalid).toThrow('symlink should be a string'); + done(); + }); + + it('sets _symlink', function (done) { + var val = '/test/test.coffee'; + var expected = path.normalize(val); + var file = new File() as TestFile; + file.symlink = val; + + expect(file._symlink).toEqual(expected); + done(); + }); + + it('allows relative symlink', function (done) { + var val = 'test.coffee'; + var file = new File(); + file.symlink = val; + + expect(file.symlink).toEqual(val); + done(); + }); + + it('normalizes and removes trailing separator upon set', function (done) { + var val = '/test/foo/../bar/'; + var expected = path.normalize(val.slice(0, -1)); + var file = new File(); + file.symlink = val; + + expect(file.symlink).toEqual(expected); + done(); + }); + }); });