diff --git a/docs/dependencies.md b/docs/dependencies.md index 2d1fd4f63..95099c41c 100644 --- a/docs/dependencies.md +++ b/docs/dependencies.md @@ -8,3 +8,6 @@ available. It uses `Promise` when available and throws when `promises` property is accessed in an environment that does not support this ES2015 feature. + +It uses `BigInt` when available and throws when `bigint` option is used +in an environment that does not support this ESNext feature. diff --git a/package.json b/package.json index dc2829838..642821091 100644 --- a/package.json +++ b/package.json @@ -13,11 +13,11 @@ "test": "jest", "test:coverage": "jest --coverage", "test:watch": "jest --watch", - "watch": "watch 'npm run build' ./src", + "watch": "watch \"npm run build\" ./src", "semantic-release": "semantic-release", - "prettier": "prettier --ignore-path .gitignore --write 'src/**/*.{ts,js}'", - "prettier:diff": "prettier -l 'src/**/*.{ts,js}'", - "tslint": "tslint 'src/**/*.ts' -t verbose", + "prettier": "prettier --ignore-path .gitignore --write \"src/**/*.{ts,js}\"", + "prettier:diff": "prettier -l \"src/**/*.{ts,js}\"", + "tslint": "tslint \"src/**/*.ts\" -t verbose", "precommit": "pretty-quick --staged", "prepush": "yarn prettier:diff && yarn tslint" }, diff --git a/src/Stats.ts b/src/Stats.ts index 9f4280ab0..fd5187aac 100644 --- a/src/Stats.ts +++ b/src/Stats.ts @@ -3,62 +3,72 @@ import { constants } from './constants'; const { S_IFMT, S_IFDIR, S_IFREG, S_IFBLK, S_IFCHR, S_IFLNK, S_IFIFO, S_IFSOCK } = constants; +export type TStatNumber = number | BigInt; + /** * Statistics about a file/directory, like `fs.Stats`. */ export class Stats { - static build(node: Node) { + static build(node: Node, bigint: boolean = false) { const stats = new Stats(); const { uid, gid, atime, mtime, ctime } = node; + const getStatNumber = !bigint + ? number => number + : typeof BigInt === 'function' + ? BigInt + : () => { + throw new Error('BigInt is not supported in this environment.'); + }; + // Copy all values on Stats from Node, so that if Node values // change, values on Stats would still be the old ones, // just like in Node fs. - stats.uid = uid; - stats.gid = gid; + stats.uid = getStatNumber(uid); + stats.gid = getStatNumber(gid); stats.atime = atime; stats.mtime = mtime; stats.ctime = ctime; stats.birthtime = ctime; - stats.atimeMs = atime.getTime(); - stats.mtimeMs = mtime.getTime(); - const ctimeMs = ctime.getTime(); + stats.atimeMs = getStatNumber(atime.getTime()); + stats.mtimeMs = getStatNumber(mtime.getTime()); + const ctimeMs = getStatNumber(ctime.getTime()); stats.ctimeMs = ctimeMs; stats.birthtimeMs = ctimeMs; - stats.size = node.getSize(); - stats.mode = node.mode; - stats.ino = node.ino; - stats.nlink = node.nlink; + stats.size = getStatNumber(node.getSize()); + stats.mode = getStatNumber(node.mode); + stats.ino = getStatNumber(node.ino); + stats.nlink = getStatNumber(node.nlink); return stats; } - uid: number = 0; - gid: number = 0; + uid: TStatNumber = 0; + gid: TStatNumber = 0; - rdev: number = 0; - blksize: number = 4096; - ino: number = 0; - size: number = 0; - blocks: number = 1; + rdev: TStatNumber = 0; + blksize: TStatNumber = 4096; + ino: TStatNumber = 0; + size: TStatNumber = 0; + blocks: TStatNumber = 1; atime: Date = null; mtime: Date = null; ctime: Date = null; birthtime: Date = null; - atimeMs: number = 0.0; - mtimeMs: number = 0.0; - ctimeMs: number = 0.0; - birthtimeMs: number = 0.0; + atimeMs: TStatNumber = 0.0; + mtimeMs: TStatNumber = 0.0; + ctimeMs: TStatNumber = 0.0; + birthtimeMs: TStatNumber = 0.0; - dev: number = 0; - mode: number = 0; - nlink: number = 0; + dev: TStatNumber = 0; + mode: TStatNumber = 0; + nlink: TStatNumber = 0; private _checkModeProperty(property: number): boolean { return (this.mode & S_IFMT) === property; diff --git a/src/__tests__/volume.test.ts b/src/__tests__/volume.test.ts index 5e80140b9..a9fd4fe68 100644 --- a/src/__tests__/volume.test.ts +++ b/src/__tests__/volume.test.ts @@ -4,6 +4,10 @@ import Stats from '../Stats'; import Dirent from '../Dirent'; import { Volume, filenameToSteps, StatWatcher } from '../volume'; +// I did not find how to include '../bigint.d.ts' here! +type BigInt = number; +declare const BigInt: typeof Number; + describe('volume', () => { describe('filenameToSteps(filename): string[]', () => { it('/ -> []', () => { @@ -665,6 +669,14 @@ describe('volume', () => { expect(stats.isFile()).toBe(true); expect(stats.isDirectory()).toBe(false); }); + it('Returns file stats using BigInt', () => { + if (typeof BigInt === 'function') { + const stats = vol.lstatSync('/dojo.js', { bigint: true }); + expect(typeof stats.ino).toBe('bigint'); + } else { + expect(() => vol.lstatSync('/dojo.js', { bigint: true })).toThrowError(); + } + }); it('Stats on symlink returns results about the symlink', () => { vol.symlinkSync('/dojo.js', '/link.js'); const stats = vol.lstatSync('/link.js'); @@ -688,6 +700,14 @@ describe('volume', () => { expect(stats.isFile()).toBe(true); expect(stats.isDirectory()).toBe(false); }); + it('Returns file stats using BigInt', () => { + if (typeof BigInt === 'function') { + const stats = vol.statSync('/dojo.js', { bigint: true }); + expect(typeof stats.ino).toBe('bigint'); + } else { + expect(() => vol.statSync('/dojo.js', { bigint: true })).toThrowError(); + } + }); it('Stats on symlink returns results about the resolved file', () => { vol.symlinkSync('/dojo.js', '/link.js'); const stats = vol.statSync('/link.js'); @@ -723,6 +743,15 @@ describe('volume', () => { expect(stats.isFile()).toBe(true); expect(stats.isDirectory()).toBe(false); }); + it('Returns file stats using BigInt', () => { + const fd = vol.openSync('/dojo.js', 'r'); + if (typeof BigInt === 'function') { + const stats = vol.fstatSync(fd, { bigint: true }); + expect(typeof stats.ino).toBe('bigint'); + } else { + expect(() => vol.fstatSync(fd, { bigint: true })).toThrowError(); + } + }); }); describe('.fstat(fd, callback)', () => { xit('...', () => {}); diff --git a/src/bigint.d.ts b/src/bigint.d.ts new file mode 100644 index 000000000..80538a01a --- /dev/null +++ b/src/bigint.d.ts @@ -0,0 +1,6 @@ +// This definition file is here as a workaround and should be replaced +// by "esnext.bigint" library when TypeScript will support `BigInt` type. +// Track this at Microsoft/TypeScript#15096. + +type BigInt = number; +declare const BigInt: typeof Number; diff --git a/src/promises.ts b/src/promises.ts index 539a428ba..a0b715da4 100644 --- a/src/promises.ts +++ b/src/promises.ts @@ -14,6 +14,7 @@ import { IReadFileOptions, IRealpathOptions, IWriteFileOptions, + IStatOptions, } from './volume'; import Stats from './Stats'; import Dirent from './Dirent'; @@ -52,7 +53,7 @@ export interface IFileHandle { datasync(): Promise; read(buffer: Buffer | Uint8Array, offset: number, length: number, position: number): Promise; readFile(options?: IReadFileOptions | string): Promise; - stat(): Promise; + stat(options?: IStatOptions): Promise; truncate(len?: number): Promise; utimes(atime: TTime, mtime: TTime): Promise; write( @@ -76,7 +77,7 @@ export interface IPromisesAPI { lchmod(path: TFilePath, mode: TMode): Promise; lchown(path: TFilePath, uid: number, gid: number): Promise; link(existingPath: TFilePath, newPath: TFilePath): Promise; - lstat(path: TFilePath): Promise; + lstat(path: TFilePath, options?: IStatOptions): Promise; mkdir(path: TFilePath, options?: TMode | IMkdirOptions): Promise; mkdtemp(prefix: string, options?: IOptions): Promise; open(path: TFilePath, flags: TFlags, mode?: TMode): Promise; @@ -86,7 +87,7 @@ export interface IPromisesAPI { realpath(path: TFilePath, options?: IRealpathOptions | string): Promise; rename(oldPath: TFilePath, newPath: TFilePath): Promise; rmdir(path: TFilePath): Promise; - stat(path: TFilePath): Promise; + stat(path: TFilePath, options?: IStatOptions): Promise; symlink(target: TFilePath, path: TFilePath, type?: TSymlinkType): Promise; truncate(path: TFilePath, len?: number): Promise; unlink(path: TFilePath): Promise; @@ -132,8 +133,8 @@ export class FileHandle implements IFileHandle { return promisify(this.vol, 'readFile')(this.fd, options); } - stat(): Promise { - return promisify(this.vol, 'fstat')(this.fd); + stat(options?: IStatOptions): Promise { + return promisify(this.vol, 'fstat')(this.fd, options); } sync(): Promise { @@ -205,8 +206,8 @@ export default function createPromisesApi(vol: Volume): null | IPromisesAPI { return promisify(vol, 'link')(existingPath, newPath); }, - lstat(path: TFilePath): Promise { - return promisify(vol, 'lstat')(path); + lstat(path: TFilePath, options?: IStatOptions): Promise { + return promisify(vol, 'lstat')(path, options); }, mkdir(path: TFilePath, options?: TMode | IMkdirOptions): Promise { @@ -245,8 +246,8 @@ export default function createPromisesApi(vol: Volume): null | IPromisesAPI { return promisify(vol, 'rmdir')(path); }, - stat(path: TFilePath): Promise { - return promisify(vol, 'stat')(path); + stat(path: TFilePath, options?: IStatOptions): Promise { + return promisify(vol, 'stat')(path, options); }, symlink(target: TFilePath, path: TFilePath, type?: TSymlinkType): Promise { diff --git a/src/volume.ts b/src/volume.ts index 40c5a3038..3556b9a25 100644 --- a/src/volume.ts +++ b/src/volume.ts @@ -1,6 +1,6 @@ import * as pathModule from 'path'; import { Node, Link, File } from './node'; -import Stats from './Stats'; +import Stats, { TStatNumber } from './Stats'; import Dirent from './Dirent'; import { Buffer } from 'buffer'; import setImmediate from './setImmediate'; @@ -357,6 +357,20 @@ const readdirDefaults: IReaddirOptions = { const getReaddirOptions = optsGenerator(readdirDefaults); const getReaddirOptsAndCb = optsAndCbGenerator(getReaddirOptions); +// Options for `fs.fstat`, `fs.fstatSync`, `fs.lstat`, `fs.lstatSync`, `fs.stat`, and `fs.statSync` +export interface IStatOptions { + bigint?: boolean; +} +const statDefaults: IStatOptions = { + bigint: false, +}; +const getStatOptions: (options?: any) => IStatOptions = (options = {}) => extend({}, statDefaults, options); +const getStatOptsAndCb: (options: any, callback?: TCallback) => [IStatOptions, TCallback] = ( + options, + callback?, +) => + typeof options === 'function' ? [getStatOptions(), options] : [getStatOptions(options), validateCallback(callback)]; + // ---------------------------------------- Utility functions function getPathFromURLPosix(url): string { @@ -1416,21 +1430,24 @@ export class Volume { this.wrapAsync(this.realpathBase, [pathFilename, opts.encoding], callback); } - private lstatBase(filename: string): Stats { + private lstatBase(filename: string, bigint: boolean = false): Stats { const link: Link = this.getLink(filenameToSteps(filename)); if (!link) throwError(ENOENT, 'lstat', filename); - return Stats.build(link.getNode()); + return Stats.build(link.getNode(), bigint); } - lstatSync(path: TFilePath): Stats { - return this.lstatBase(pathToFilename(path)); + lstatSync(path: TFilePath, options?: IStatOptions): Stats { + return this.lstatBase(pathToFilename(path), getStatOptions(options).bigint); } - lstat(path: TFilePath, callback: TCallback) { - this.wrapAsync(this.lstatBase, [pathToFilename(path)], callback); + lstat(path: TFilePath, callback: TCallback); + lstat(path: TFilePath, options: IStatOptions, callback: TCallback); + lstat(path: TFilePath, a: TCallback | IStatOptions, b?: TCallback) { + const [opts, callback] = getStatOptsAndCb(a, b); + this.wrapAsync(this.lstatBase, [pathToFilename(path), opts.bigint], callback); } - private statBase(filename: string): Stats { + private statBase(filename: string, bigint: boolean = false): Stats { let link: Link = this.getLink(filenameToSteps(filename)); if (!link) throwError(ENOENT, 'stat', filename); @@ -1438,29 +1455,35 @@ export class Volume { link = this.resolveSymlinks(link); if (!link) throwError(ENOENT, 'stat', filename); - return Stats.build(link.getNode()); + return Stats.build(link.getNode(), bigint); } - statSync(path: TFilePath): Stats { - return this.statBase(pathToFilename(path)); + statSync(path: TFilePath, options?: IStatOptions): Stats { + return this.statBase(pathToFilename(path), getStatOptions(options).bigint); } - stat(path: TFilePath, callback: TCallback) { - this.wrapAsync(this.statBase, [pathToFilename(path)], callback); + stat(path: TFilePath, callback: TCallback); + stat(path: TFilePath, options: IStatOptions, callback: TCallback); + stat(path: TFilePath, a: TCallback | IStatOptions, b?: TCallback) { + const [opts, callback] = getStatOptsAndCb(a, b); + this.wrapAsync(this.statBase, [pathToFilename(path), opts.bigint], callback); } - private fstatBase(fd: number): Stats { + private fstatBase(fd: number, bigint: boolean = false): Stats { const file = this.getFileByFd(fd); if (!file) throwError(EBADF, 'fstat'); - return Stats.build(file.node); + return Stats.build(file.node, bigint); } - fstatSync(fd: number): Stats { - return this.fstatBase(fd); + fstatSync(fd: number, options?: IStatOptions): Stats { + return this.fstatBase(fd, getStatOptions(options).bigint); } - fstat(fd: number, callback: TCallback) { - this.wrapAsync(this.fstatBase, [fd], callback); + fstat(fd: number, callback: TCallback); + fstat(fd: number, options: IStatOptions, callback: TCallback); + fstat(fd: number, a: TCallback | IStatOptions, b?: TCallback) { + const [opts, callback] = getStatOptsAndCb(a, b); + this.wrapAsync(this.fstatBase, [fd, opts.bigint], callback); } private renameBase(oldPathFilename: string, newPathFilename: string) {