diff --git a/packages/yarnpkg-fslib/sources/MountFS.ts b/packages/yarnpkg-fslib/sources/MountFS.ts index 655a0007eb4e..fc73a90fae6b 100644 --- a/packages/yarnpkg-fslib/sources/MountFS.ts +++ b/packages/yarnpkg-fslib/sources/MountFS.ts @@ -5,9 +5,10 @@ import {FakeFS, MkdirOptions, RmdirOptions, WriteFileOptions, OpendirOptions} import {Dirent, SymlinkType} from './FakeFS'; import {CreateReadStreamOptions, CreateWriteStreamOptions, BasePortableFakeFS, ExtractHintOptions, WatchFileOptions, WatchFileCallback, StatWatcher} from './FakeFS'; import {NodeFS} from './NodeFS'; +import {SubFS} from './SubFS'; import {watchFile, unwatchFile, unwatchAllFiles} from './algorithms/watchFile'; import * as errors from './errors'; -import {Filename, FSPath, npath, PortablePath} from './path'; +import {Filename, FSPath, npath, PortablePath, ppath} from './path'; // Only file descriptors prefixed by those values will be forwarded to the MountFS // instances. Note that the highest MOUNT_MAGIC bit MUST NOT be set, otherwise the @@ -80,6 +81,37 @@ export class MountFS extends BasePortableFakeFS { private notMount: Set = new Set(); private realPaths: Map = new Map(); + static createFolderMount({baseFs, mountPoint, targetPath}: {baseFs: FakeFS, mountPoint: PortablePath, targetPath: PortablePath}) { + const subFs = new SubFS(targetPath); + + const getMountPoint = (p: PortablePath) => { + const detectedMountPoint = p === mountPoint || p.startsWith(`${mountPoint}/`) ? p.slice(0, mountPoint.length) : null; + return detectedMountPoint as PortablePath; + }; + + const factoryPromise = async (baseFs: FakeFS, p: PortablePath) => { + return () => subFs; + }; + + const factorySync = (baseFs: FakeFS, p: PortablePath) => { + return subFs; + }; + + return new MountFS({ + baseFs, + + getMountPoint, + + factoryPromise, + factorySync, + + magicByte: 21, + maxAge: Infinity, + + typeCheck: null, + }); + } + constructor({baseFs = new NodeFS(), filter = null, magicByte = 0x2a, maxOpenFiles = Infinity, useCache = true, maxAge = 5000, typeCheck = constants.S_IFREG, getMountPoint, factoryPromise, factorySync}: MountFSOptions) { if (Math.floor(magicByte) !== magicByte || !(magicByte > 1 && magicByte <= 127)) throw new Error(`The magic byte must be set to a round value between 1 and 127 included`); @@ -819,8 +851,8 @@ export class MountFS extends BasePortableFakeFS { async readdirPromise(p: PortablePath, opts?: ReaddirOptions | null): Promise | DirentNoPath | PortablePath>> { return await this.makeCallPromise(p, async () => { return await this.baseFs.readdirPromise(p, opts as any); - }, async (mountFs, {subPath}) => { - return await mountFs.readdirPromise(subPath, opts as any); + }, async (mountFs, {archivePath, subPath}) => { + return this.fixReaddirPaths(mountFs.resolve(subPath), p, await mountFs.readdirPromise(subPath, opts as any), opts as any); }, { requireSubpath: false, }); @@ -839,13 +871,25 @@ export class MountFS extends BasePortableFakeFS { readdirSync(p: PortablePath, opts?: ReaddirOptions | null): Array | DirentNoPath | PortablePath> { return this.makeCallSync(p, () => { return this.baseFs.readdirSync(p, opts as any); - }, (mountFs, {subPath}) => { - return mountFs.readdirSync(subPath, opts as any); + }, (mountFs, {archivePath, subPath}) => { + return this.fixReaddirPaths(mountFs.resolve(subPath), p, mountFs.readdirSync(subPath, opts as any), opts as any); }, { requireSubpath: false, }); } + private fixReaddirPaths(privatePath: PortablePath, publicPath: PortablePath, data: Array | DirentNoPath | PortablePath>, opts?: {recursive?: boolean, withFileTypes?: boolean} | null) { + if (!opts?.withFileTypes) + return data; + + const entries = data as Array>; + + for (const entry of entries) + entry.path = ppath.join(publicPath, ppath.relative(privatePath, entry.path)); + + return entries; + } + async readlinkPromise(p: PortablePath) { return await this.makeCallPromise(p, async () => { return await this.baseFs.readlinkPromise(p); diff --git a/packages/yarnpkg-fslib/sources/NoopFS.ts b/packages/yarnpkg-fslib/sources/NoopFS.ts new file mode 100644 index 000000000000..4820d2afe0ca --- /dev/null +++ b/packages/yarnpkg-fslib/sources/NoopFS.ts @@ -0,0 +1,376 @@ +import {Stats, BigIntStats} from 'fs'; + +import {CreateReadStreamOptions, CreateWriteStreamOptions, FakeFS, ExtractHintOptions, WatchFileCallback, WatchFileOptions, StatWatcher, Dir, OpendirOptions, ReaddirOptions, DirentNoPath} from './FakeFS'; +import {Dirent, SymlinkType, StatSyncOptions, StatOptions} from './FakeFS'; +import {MkdirOptions, RmdirOptions, WriteFileOptions, WatchCallback, WatchOptions, Watcher} from './FakeFS'; +import {FSPath, Filename, PortablePath, ppath} from './path'; + +export class NoopFS extends FakeFS { + private readonly baseFs: FakeFS; + + constructor({baseFs}: {baseFs: FakeFS}) { + super(ppath); + + this.baseFs = baseFs; + } + + getExtractHint(hints: ExtractHintOptions) { + return this.baseFs.getExtractHint(hints); + } + + resolve(path: PortablePath) { + return this.baseFs.resolve(path); + } + + getRealPath() { + return this.baseFs.getRealPath(); + } + + async openPromise(p: PortablePath, flags: string, mode?: number) { + return this.baseFs.openPromise(p, flags, mode); + } + + openSync(p: PortablePath, flags: string, mode?: number) { + return this.baseFs.openSync(p, flags, mode); + } + + async opendirPromise(p: PortablePath, opts?: OpendirOptions): Promise> { + return Object.assign(await this.baseFs.opendirPromise(p, opts), {path: p}); + } + + opendirSync(p: PortablePath, opts?: OpendirOptions): Dir { + return Object.assign(this.baseFs.opendirSync(p, opts), {path: p}); + } + + async readPromise(fd: number, buffer: Buffer, offset?: number, length?: number, position?: number | null) { + return await this.baseFs.readPromise(fd, buffer, offset, length, position); + } + + readSync(fd: number, buffer: Buffer, offset: number, length: number, position: number) { + return this.baseFs.readSync(fd, buffer, offset, length, position); + } + + async writePromise(fd: number, buffer: Buffer, offset?: number, length?: number, position?: number): Promise; + async writePromise(fd: number, buffer: string, position?: number): Promise; + async writePromise(fd: number, buffer: Buffer | string, offset?: number, length?: number, position?: number): Promise { + if (typeof buffer === `string`) { + return await this.baseFs.writePromise(fd, buffer, offset); + } else { + return await this.baseFs.writePromise(fd, buffer, offset, length, position); + } + } + + writeSync(fd: number, buffer: Buffer, offset?: number, length?: number, position?: number): number; + writeSync(fd: number, buffer: string, position?: number): number; + writeSync(fd: number, buffer: Buffer | string, offset?: number, length?: number, position?: number) { + if (typeof buffer === `string`) { + return this.baseFs.writeSync(fd, buffer, offset); + } else { + return this.baseFs.writeSync(fd, buffer, offset, length, position); + } + } + + async closePromise(fd: number) { + return this.baseFs.closePromise(fd); + } + + closeSync(fd: number) { + this.baseFs.closeSync(fd); + } + + createReadStream(p: PortablePath | null, opts?: CreateReadStreamOptions) { + return this.baseFs.createReadStream(p !== null ? p : p, opts); + } + + createWriteStream(p: PortablePath | null, opts?: CreateWriteStreamOptions) { + return this.baseFs.createWriteStream(p !== null ? p : p, opts); + } + + async realpathPromise(p: PortablePath) { + return await this.baseFs.realpathPromise(p); + } + + realpathSync(p: PortablePath) { + return this.baseFs.realpathSync(p); + } + + async existsPromise(p: PortablePath) { + return this.baseFs.existsPromise(p); + } + + existsSync(p: PortablePath) { + return this.baseFs.existsSync(p); + } + + accessSync(p: PortablePath, mode?: number) { + return this.baseFs.accessSync(p, mode); + } + + async accessPromise(p: PortablePath, mode?: number) { + return this.baseFs.accessPromise(p, mode); + } + + // https://github.com/DefinitelyTyped/DefinitelyTyped/blob/51d793492d4c2e372b01257668dcd3afc58d7352/types/node/v16/fs.d.ts#L1042-L1059 + async statPromise(p: PortablePath): Promise; + async statPromise(p: PortablePath, opts: (StatOptions & { bigint?: false | undefined }) | undefined): Promise; + async statPromise(p: PortablePath, opts: StatOptions & { bigint: true }): Promise; + async statPromise(p: PortablePath, opts?: StatOptions): Promise { + return this.baseFs.statPromise(p, opts); + } + + // https://github.com/DefinitelyTyped/DefinitelyTyped/blob/51d793492d4c2e372b01257668dcd3afc58d7352/types/node/v16/fs.d.ts#L931-L967 + statSync(p: PortablePath): Stats; + statSync(p: PortablePath, opts?: StatSyncOptions & {bigint?: false | undefined, throwIfNoEntry: false}): Stats | undefined; + statSync(p: PortablePath, opts: StatSyncOptions & {bigint: true, throwIfNoEntry: false}): BigIntStats | undefined; + statSync(p: PortablePath, opts?: StatSyncOptions & {bigint?: false | undefined}): Stats; + statSync(p: PortablePath, opts: StatSyncOptions & {bigint: true}): BigIntStats; + statSync(p: PortablePath, opts: StatSyncOptions & {bigint: boolean, throwIfNoEntry?: false | undefined}): Stats | BigIntStats; + statSync(p: PortablePath, opts?: StatSyncOptions): Stats | BigIntStats | undefined { + return this.baseFs.statSync(p, opts); + } + + async fstatPromise(fd: number): Promise; + async fstatPromise(fd: number, opts: {bigint: true}): Promise; + async fstatPromise(fd: number, opts?: {bigint: boolean}): Promise; + async fstatPromise(fd: number, opts?: {bigint: boolean}) { + return this.baseFs.fstatPromise(fd, opts); + } + + fstatSync(fd: number): Stats; + fstatSync(fd: number, opts: {bigint: true}): BigIntStats; + fstatSync(fd: number, opts?: {bigint: boolean}): BigIntStats | Stats; + fstatSync(fd: number, opts?: {bigint: boolean}) { + return this.baseFs.fstatSync(fd, opts); + } + + // https://github.com/DefinitelyTyped/DefinitelyTyped/blob/51d793492d4c2e372b01257668dcd3afc58d7352/types/node/v16/fs.d.ts#L1042-L1059 + lstatPromise(p: PortablePath): Promise; + lstatPromise(p: PortablePath, opts: (StatOptions & { bigint?: false | undefined }) | undefined): Promise; + lstatPromise(p: PortablePath, opts: StatOptions & { bigint: true }): Promise; + lstatPromise(p: PortablePath, opts?: StatOptions): Promise { + return this.baseFs.lstatPromise(p, opts); + } + + // https://github.com/DefinitelyTyped/DefinitelyTyped/blob/51d793492d4c2e372b01257668dcd3afc58d7352/types/node/v16/fs.d.ts#L931-L967 + lstatSync(p: PortablePath): Stats; + lstatSync(p: PortablePath, opts?: StatSyncOptions & {bigint?: false | undefined, throwIfNoEntry: false}): Stats | undefined; + lstatSync(p: PortablePath, opts: StatSyncOptions & {bigint: true, throwIfNoEntry: false}): BigIntStats | undefined; + lstatSync(p: PortablePath, opts?: StatSyncOptions & {bigint?: false | undefined}): Stats; + lstatSync(p: PortablePath, opts: StatSyncOptions & {bigint: true}): BigIntStats; + lstatSync(p: PortablePath, opts: StatSyncOptions & { bigint: boolean, throwIfNoEntry?: false | undefined }): Stats | BigIntStats; + lstatSync(p: PortablePath, opts?: StatSyncOptions): Stats | BigIntStats | undefined { + return this.baseFs.lstatSync(p, opts); + } + + async fchmodPromise(fd: number, mask: number): Promise { + return this.baseFs.fchmodPromise(fd, mask); + } + + fchmodSync(fd: number, mask: number): void { + return this.baseFs.fchmodSync(fd, mask); + } + + async chmodPromise(p: PortablePath, mask: number) { + return this.baseFs.chmodPromise(p, mask); + } + + chmodSync(p: PortablePath, mask: number) { + return this.baseFs.chmodSync(p, mask); + } + + async fchownPromise(fd: number, uid: number, gid: number): Promise { + return this.baseFs.fchownPromise(fd, uid, gid); + } + + fchownSync(fd: number, uid: number, gid: number): void { + return this.baseFs.fchownSync(fd, uid, gid); + } + + async chownPromise(p: PortablePath, uid: number, gid: number) { + return this.baseFs.chownPromise(p, uid, gid); + } + + chownSync(p: PortablePath, uid: number, gid: number) { + return this.baseFs.chownSync(p, uid, gid); + } + + async renamePromise(oldP: PortablePath, newP: PortablePath) { + return this.baseFs.renamePromise(oldP, newP); + } + + renameSync(oldP: PortablePath, newP: PortablePath) { + return this.baseFs.renameSync(oldP, newP); + } + + async copyFilePromise(sourceP: PortablePath, destP: PortablePath, flags: number = 0) { + return this.baseFs.copyFilePromise(sourceP, destP, flags); + } + + copyFileSync(sourceP: PortablePath, destP: PortablePath, flags: number = 0) { + return this.baseFs.copyFileSync(sourceP, destP, flags); + } + + async appendFilePromise(p: FSPath, content: string | Uint8Array, opts?: WriteFileOptions) { + return this.baseFs.appendFilePromise(p, content, opts); + } + + appendFileSync(p: FSPath, content: string | Uint8Array, opts?: WriteFileOptions) { + return this.baseFs.appendFileSync(p, content, opts); + } + + async writeFilePromise(p: FSPath, content: string | NodeJS.ArrayBufferView, opts?: WriteFileOptions) { + return this.baseFs.writeFilePromise(p, content, opts); + } + + writeFileSync(p: FSPath, content: string | NodeJS.ArrayBufferView, opts?: WriteFileOptions) { + return this.baseFs.writeFileSync(p, content, opts); + } + + async unlinkPromise(p: PortablePath) { + return this.baseFs.unlinkPromise(p); + } + + unlinkSync(p: PortablePath) { + return this.baseFs.unlinkSync(p); + } + + async utimesPromise(p: PortablePath, atime: Date | string | number, mtime: Date | string | number) { + return this.baseFs.utimesPromise(p, atime, mtime); + } + + utimesSync(p: PortablePath, atime: Date | string | number, mtime: Date | string | number) { + return this.baseFs.utimesSync(p, atime, mtime); + } + + async lutimesPromise(p: PortablePath, atime: Date | string | number, mtime: Date | string | number) { + return this.baseFs.lutimesPromise(p, atime, mtime); + } + + lutimesSync(p: PortablePath, atime: Date | string | number, mtime: Date | string | number) { + return this.baseFs.lutimesSync(p, atime, mtime); + } + + async mkdirPromise(p: PortablePath, opts?: MkdirOptions) { + return this.baseFs.mkdirPromise(p, opts); + } + + mkdirSync(p: PortablePath, opts?: MkdirOptions) { + return this.baseFs.mkdirSync(p, opts); + } + + async rmdirPromise(p: PortablePath, opts?: RmdirOptions) { + return this.baseFs.rmdirPromise(p, opts); + } + + rmdirSync(p: PortablePath, opts?: RmdirOptions) { + return this.baseFs.rmdirSync(p, opts); + } + + async linkPromise(existingP: PortablePath, newP: PortablePath) { + return this.baseFs.linkPromise(existingP, newP); + } + + linkSync(existingP: PortablePath, newP: PortablePath) { + return this.baseFs.linkSync(existingP, newP); + } + + async symlinkPromise(target: PortablePath, p: PortablePath, type?: SymlinkType) { + return this.baseFs.symlinkPromise(target, p, type); + } + + symlinkSync(target: PortablePath, p: PortablePath, type?: SymlinkType) { + return this.baseFs.symlinkSync(target, p, type); + } + + async readFilePromise(p: FSPath, encoding?: null): Promise; + async readFilePromise(p: FSPath, encoding: BufferEncoding): Promise; + async readFilePromise(p: FSPath, encoding?: BufferEncoding | null): Promise; + async readFilePromise(p: FSPath, encoding?: BufferEncoding | null) { + return this.baseFs.readFilePromise(p, encoding); + } + + readFileSync(p: FSPath, encoding?: null): Buffer; + readFileSync(p: FSPath, encoding: BufferEncoding): string; + readFileSync(p: FSPath, encoding?: BufferEncoding | null): Buffer | string; + readFileSync(p: FSPath, encoding?: BufferEncoding | null) { + return this.baseFs.readFileSync(p, encoding); + } + + readdirPromise(p: PortablePath, opts?: null): Promise>; + readdirPromise(p: PortablePath, opts: {recursive?: false, withFileTypes: true}): Promise>; + readdirPromise(p: PortablePath, opts: {recursive?: false, withFileTypes?: false}): Promise>; + readdirPromise(p: PortablePath, opts: {recursive?: false, withFileTypes: boolean}): Promise>; + readdirPromise(p: PortablePath, opts: {recursive: true, withFileTypes: true}): Promise>>; + readdirPromise(p: PortablePath, opts: {recursive: true, withFileTypes?: false}): Promise>; + readdirPromise(p: PortablePath, opts: {recursive: true, withFileTypes: boolean}): Promise | PortablePath>>; + readdirPromise(p: PortablePath, opts: {recursive: boolean, withFileTypes: true}): Promise | DirentNoPath>>; + readdirPromise(p: PortablePath, opts: {recursive: boolean, withFileTypes?: false}): Promise>; + readdirPromise(p: PortablePath, opts: {recursive: boolean, withFileTypes: boolean}): Promise | DirentNoPath | PortablePath>>; + readdirPromise(p: PortablePath, opts?: ReaddirOptions | null): Promise | DirentNoPath | PortablePath | Filename>> { + return this.baseFs.readdirPromise(p, opts as any); + } + + readdirSync(p: PortablePath, opts?: null): Array; + readdirSync(p: PortablePath, opts: {recursive?: false, withFileTypes: true}): Array; + readdirSync(p: PortablePath, opts: {recursive?: false, withFileTypes?: false}): Array; + readdirSync(p: PortablePath, opts: {recursive?: false, withFileTypes: boolean}): Array; + readdirSync(p: PortablePath, opts: {recursive: true, withFileTypes: true}): Array>; + readdirSync(p: PortablePath, opts: {recursive: true, withFileTypes?: false}): Array; + readdirSync(p: PortablePath, opts: {recursive: true, withFileTypes: boolean}): Array | PortablePath>; + readdirSync(p: PortablePath, opts: {recursive: boolean, withFileTypes: true}): Array | DirentNoPath>; + readdirSync(p: PortablePath, opts: {recursive: boolean, withFileTypes?: false}): Array; + readdirSync(p: PortablePath, opts: {recursive: boolean, withFileTypes: boolean}): Array | DirentNoPath | PortablePath>; + readdirSync(p: PortablePath, opts?: ReaddirOptions | null): Array | DirentNoPath | PortablePath | Filename> { + return this.baseFs.readdirSync(p, opts as any); + } + + async readlinkPromise(p: PortablePath) { + return await this.baseFs.readlinkPromise(p); + } + + readlinkSync(p: PortablePath) { + return this.baseFs.readlinkSync(p); + } + + async truncatePromise(p: PortablePath, len?: number) { + return this.baseFs.truncatePromise(p, len); + } + + truncateSync(p: PortablePath, len?: number) { + return this.baseFs.truncateSync(p, len); + } + + async ftruncatePromise(fd: number, len?: number): Promise { + return this.baseFs.ftruncatePromise(fd, len); + } + + ftruncateSync(fd: number, len?: number): void { + return this.baseFs.ftruncateSync(fd, len); + } + + watch(p: PortablePath, cb?: WatchCallback): Watcher; + watch(p: PortablePath, opts: WatchOptions, cb?: WatchCallback): Watcher; + watch(p: PortablePath, a?: WatchOptions | WatchCallback, b?: WatchCallback) { + return this.baseFs.watch( + p, + // @ts-expect-error + a, + b, + ); + } + + watchFile(p: PortablePath, cb: WatchFileCallback): StatWatcher; + watchFile(p: PortablePath, opts: WatchFileOptions, cb: WatchFileCallback): StatWatcher; + watchFile(p: PortablePath, a: WatchFileOptions | WatchFileCallback, b?: WatchFileCallback) { + return this.baseFs.watchFile( + p, + // @ts-expect-error + a, + b, + ); + } + + unwatchFile(p: PortablePath, cb?: WatchFileCallback) { + return this.baseFs.unwatchFile(p, cb); + } +} diff --git a/packages/yarnpkg-fslib/sources/ProxiedFS.ts b/packages/yarnpkg-fslib/sources/ProxiedFS.ts index 5c69773b0f82..53d2d171481e 100644 --- a/packages/yarnpkg-fslib/sources/ProxiedFS.ts +++ b/packages/yarnpkg-fslib/sources/ProxiedFS.ts @@ -316,18 +316,20 @@ export abstract class ProxiedFS

extends FakeFS< return this.baseFs.readFileSync(this.fsMapToBase(p), encoding); } - readdirPromise(p: P, opts?: null): Promise>; - readdirPromise(p: P, opts: {recursive?: false, withFileTypes: true}): Promise>; - readdirPromise(p: P, opts: {recursive?: false, withFileTypes?: false}): Promise>; - readdirPromise(p: P, opts: {recursive?: false, withFileTypes: boolean}): Promise>; - readdirPromise(p: P, opts: {recursive: true, withFileTypes: true}): Promise>>; - readdirPromise(p: P, opts: {recursive: true, withFileTypes?: false}): Promise>; - readdirPromise(p: P, opts: {recursive: true, withFileTypes: boolean}): Promise | P>>; - readdirPromise(p: P, opts: {recursive: boolean, withFileTypes: true}): Promise | DirentNoPath>>; - readdirPromise(p: P, opts: {recursive: boolean, withFileTypes?: false}): Promise>; - readdirPromise(p: P, opts: {recursive: boolean, withFileTypes: boolean}): Promise | DirentNoPath | P>>; - readdirPromise(p: P, opts?: ReaddirOptions | null): Promise | DirentNoPath | P | Filename>> { - return this.baseFs.readdirPromise(this.mapToBase(p), opts as any); + async readdirPromise(p: P, opts?: null): Promise>; + async readdirPromise(p: P, opts: {recursive?: false, withFileTypes: true}): Promise>; + async readdirPromise(p: P, opts: {recursive?: false, withFileTypes?: false}): Promise>; + async readdirPromise(p: P, opts: {recursive?: false, withFileTypes: boolean}): Promise>; + async readdirPromise(p: P, opts: {recursive: true, withFileTypes: true}): Promise>>; + async readdirPromise(p: P, opts: {recursive: true, withFileTypes?: false}): Promise>; + async readdirPromise(p: P, opts: {recursive: true, withFileTypes: boolean}): Promise | P>>; + async readdirPromise(p: P, opts: {recursive: boolean, withFileTypes: true}): Promise | DirentNoPath>>; + async readdirPromise(p: P, opts: {recursive: boolean, withFileTypes?: false}): Promise>; + async readdirPromise(p: P, opts: {recursive: boolean, withFileTypes: boolean}): Promise | DirentNoPath | P>>; + async readdirPromise(p: P, opts?: ReaddirOptions | null): Promise | DirentNoPath | P | Filename>> { + const mappedP = this.mapToBase(p); + + return this.fixReaddir(p, mappedP, await this.baseFs.readdirPromise(mappedP, opts as any), opts); } readdirSync(p: P, opts?: null): Array; @@ -341,7 +343,20 @@ export abstract class ProxiedFS

extends FakeFS< readdirSync(p: P, opts: {recursive: boolean, withFileTypes?: false}): Array

; readdirSync(p: P, opts: {recursive: boolean, withFileTypes: boolean}): Array | DirentNoPath | P>; readdirSync(p: P, opts?: ReaddirOptions | null): Array | DirentNoPath | P | Filename> { - return this.baseFs.readdirSync(this.mapToBase(p), opts as any); + const mappedP = this.mapToBase(p); + + return this.fixReaddir(p, mappedP, this.baseFs.readdirSync(mappedP, opts as any), opts); + } + + private fixReaddir(p: P, mappedP: IP, result: Array, opts?: ReaddirOptions | null): Array | DirentNoPath | P | Filename> { + if (!opts?.withFileTypes) + return result; + + const items = result as Array>; + for (const item of items) + item.path = this.pathUtils.join(p, this.baseFs.pathUtils.relative(mappedP, item.path as any as IP) as any as P); + + return items; } async readlinkPromise(p: P) { diff --git a/packages/yarnpkg-fslib/sources/SubFS.ts b/packages/yarnpkg-fslib/sources/SubFS.ts new file mode 100644 index 000000000000..5ebcf5d03e36 --- /dev/null +++ b/packages/yarnpkg-fslib/sources/SubFS.ts @@ -0,0 +1,39 @@ +import {FakeFS} from './FakeFS'; +import {NodeFS} from './NodeFS'; +import {ProxiedFS} from './ProxiedFS'; +import {ppath, PortablePath} from './path'; + +export type SubFSOptions = { + baseFs?: FakeFS; +}; + +export class SubFS extends ProxiedFS { + private readonly target: PortablePath; + + protected readonly baseFs: FakeFS; + + constructor(target: PortablePath, {baseFs = new NodeFS()}: SubFSOptions = {}) { + super(ppath); + + this.target = this.pathUtils.resolve(PortablePath.root, target); + + this.baseFs = baseFs; + } + + getRealPath() { + return this.pathUtils.resolve(this.baseFs.getRealPath(), this.pathUtils.relative(PortablePath.root, this.target)); + } + + protected mapToBase(p: PortablePath): PortablePath { + return this.pathUtils.resolve(this.target, ppath.relative(PortablePath.root, ppath.resolve(PortablePath.root, p))); + } + + protected mapFromBase(p: PortablePath): PortablePath { + const relPath = this.pathUtils.relative(this.target, p); + + if (relPath.match(/^\.\.\/?/)) + throw new Error(`Path ${p} is outside of the jail`); + + return this.pathUtils.resolve(PortablePath.root, relPath); + } +} diff --git a/packages/yarnpkg-fslib/sources/TraceFS.ts b/packages/yarnpkg-fslib/sources/TraceFS.ts new file mode 100644 index 000000000000..f92fa152c7cf --- /dev/null +++ b/packages/yarnpkg-fslib/sources/TraceFS.ts @@ -0,0 +1,105 @@ +import util from 'util'; + +import {FakeFS} from './FakeFS'; +import {NoopFS} from './NoopFS'; +import {PortablePath, ppath} from './path'; + +const styleText: (color: string, text: string) => string = (util as any).styleText ?? ((color, text) => text); + +const shortenValue = (value: string, length: number): string => { + return value.length > length + ? `${value.slice(0, length)}...` + : value; +}; + +const shortenArray = (values: Array, length: number): string => { + return values.length > length + ? `[${values.slice(0, length).map(value => traceValue(value)).join(`, `)}, ...]` + : `[${values.map(value => traceValue(value)).join(`, `)}]`; +}; + +export const traceValue = (value: any) => { + if (value === null) + return styleText(`cyan`, `null`); + + if (typeof value === `string` && value.match(/^\/[a-z]+/i)) + return styleText(`magentaBright`, ppath.relative(ppath.cwd(), value as PortablePath)); + + if (typeof value === `string`) + return styleText(`green`, JSON.stringify(shortenValue(value, 80))); + + if (typeof value === `number`) + return styleText(`yellow`, value.toString()); + + if (typeof value === `boolean`) + return styleText(`magentaBright`, value.toString()); + + if (value instanceof Error && `code` in value) + return `${styleText(`red`, (value as any).code)}: ${styleText(`red`, JSON.stringify(shortenValue(value.message, 60)))}`; + + if (Array.isArray(value)) + return shortenArray(value, 3); + + if (typeof value === `object` && Buffer.isBuffer(value)) + return styleText(`blue`, `Buffer<${value.length}>`); + + if (typeof value === `object`) + return shortenValue(util.inspect(JSON.parse(JSON.stringify(value)), {compact: true}), 80); + + return `{}`; +}; + +const filter = process.env.TRACEFS_FILTER + ? new RegExp(process.env.TRACEFS_FILTER) + : null; + +const log = process.env.TRACEFS_STACKS === `1` + ? console.trace + : console.log; + +export const defaultTraceFn: TraceFn = (fnName, args, result) => { + if (filter && !JSON.stringify([args, result]).match(filter)) + return; + + log(`${styleText(`magenta`, `fs:`)} ${styleText(`gray`, `${fnName}(`)}${args.map(arg => traceValue(arg)).join(styleText(`grey`, `, `))}${styleText(`grey`, `) -> `)}${traceValue(result)}`); +}; + +export type TraceFn = (fnName: string, args: Array, result: any) => void; + +export class TraceFS extends NoopFS { + traceFn: TraceFn; + + constructor({baseFs, traceFn = defaultTraceFn}: {baseFs: FakeFS, traceFn?: TraceFn}) { + super({baseFs}); + + this.traceFn = traceFn; + } +} + +for (const fnName of Object.getOwnPropertyNames(NoopFS.prototype)) { + if (fnName.endsWith(`Promise`)) { + (TraceFS.prototype as any)[fnName] = async function (...args: Array) { + try { + const result = await (this.baseFs as any)[fnName](...args); + this.traceFn(fnName, args, result); + return result; + } catch (error) { + this.traceFn(fnName, args, error); + throw error; + } + }; + } + + if (fnName.endsWith(`Sync`)) { + (TraceFS.prototype as any)[fnName] = function (...args: Array) { + try { + const result = (this.baseFs as any)[fnName](...args); + this.traceFn(fnName, args, result); + return result; + } catch (error) { + this.traceFn(fnName, args, error); + throw error; + } + }; + } +} diff --git a/packages/yarnpkg-fslib/sources/index.ts b/packages/yarnpkg-fslib/sources/index.ts index 9589445928e0..b8f211a71a80 100644 --- a/packages/yarnpkg-fslib/sources/index.ts +++ b/packages/yarnpkg-fslib/sources/index.ts @@ -47,9 +47,11 @@ export {NoFS} from './NoFS'; export {NodeFS} from './NodeFS'; export {PosixFS} from './PosixFS'; export {ProxiedFS} from './ProxiedFS'; +export {TraceFS} from './TraceFS'; export {VirtualFS} from './VirtualFS'; -export {patchFs, extendFs} from './patchFs/patchFs'; +export {mountFolder} from './patchFs/mountFolder'; +export {applyFsLayer, patchFs, extendFs} from './patchFs/patchFs'; export {xfs} from './xfs'; export type {XFS} from './xfs'; diff --git a/packages/yarnpkg-fslib/sources/patchFs/mountFolder.ts b/packages/yarnpkg-fslib/sources/patchFs/mountFolder.ts new file mode 100644 index 000000000000..d667edecfaaa --- /dev/null +++ b/packages/yarnpkg-fslib/sources/patchFs/mountFolder.ts @@ -0,0 +1,15 @@ +import fs from 'fs'; + +import {MountFS} from '../MountFS'; +import {applyFsLayer} from '../patchFs/patchFs'; +import {PortablePath} from '../path'; + +export function mountFolder(origFs: typeof fs, mountPoint: PortablePath, targetPath: PortablePath) { + applyFsLayer(origFs, baseFs => { + return MountFS.createFolderMount({ + baseFs, + mountPoint, + targetPath, + }); + }); +} diff --git a/packages/yarnpkg-fslib/sources/patchFs/patchFs.ts b/packages/yarnpkg-fslib/sources/patchFs/patchFs.ts index 82aa0b83d9e4..3b9b766b752a 100644 --- a/packages/yarnpkg-fslib/sources/patchFs/patchFs.ts +++ b/packages/yarnpkg-fslib/sources/patchFs/patchFs.ts @@ -1,11 +1,13 @@ -import fs from 'fs'; -import {promisify} from 'util'; +import fs from 'fs'; +import {promisify} from 'util'; -import {FakeFS} from '../FakeFS'; -import {NodePathFS} from '../NodePathFS'; -import {NativePath} from '../path'; +import {FakeFS} from '../FakeFS'; +import {NodeFS} from '../NodeFS'; +import {NodePathFS} from '../NodePathFS'; +import {PosixFS} from '../PosixFS'; +import {NativePath, PortablePath} from '../path'; -import {FileHandle} from './FileHandle'; +import {FileHandle} from './FileHandle'; const SYNC_IMPLEMENTATIONS = new Set([ `accessSync`, @@ -136,6 +138,16 @@ type ReadArgumentsOptions = [ type ReadArgumentsCallback = [fd: number, callback: ReadCallback]; //#endregion +export function applyFsLayer(origFs: typeof fs, factory: (baseFs: FakeFS) => FakeFS) { + // We must copy the fs into a local, because otherwise + // 1. we would make the NodeFS instance use the function that we patched (infinite loop) + // 2. Object.create(fs) isn't enough, since it won't prevent the proto from being modified + const localFs = {...origFs}; + const nodeFs = new NodeFS(localFs); + + patchFs(fs, new PosixFS(factory(nodeFs))); +} + export function patchFs(patchedFs: typeof fs, fakeFs: FakeFS): void { // We wrap the `fakeFs` with a `NodePathFS` to add support for all path types supported by Node fakeFs = new NodePathFS(fakeFs); diff --git a/packages/yarnpkg-fslib/tests/MountFS.test.ts b/packages/yarnpkg-fslib/tests/MountFS.test.ts new file mode 100644 index 000000000000..e1cbe42df0c1 --- /dev/null +++ b/packages/yarnpkg-fslib/tests/MountFS.test.ts @@ -0,0 +1,18 @@ +import {CwdFS, MountFS, NodeFS, PortablePath, npath, ppath} from '../sources'; + +describe(`MountFS`, () => { + it(`should fix the dirent entries returned by readdir w/ withFileTypes`, () => { + const pkgDir = ppath.dirname(npath.toPortablePath(__dirname)); + + const mountFs = MountFS.createFolderMount({ + baseFs: new CwdFS(pkgDir, {baseFs: new NodeFS()}), + mountPoint: ppath.join(pkgDir, `tests`), + targetPath: ppath.join(pkgDir, `sources`), + }); + + const entries = mountFs.readdirSync(`tests` as PortablePath, {withFileTypes: true}); + const indexEntry = entries.find(entry => entry.name === `index.ts`); + + expect(indexEntry.path).toEqual(`tests` as PortablePath); + }); +}); diff --git a/packages/yarnpkg-fslib/tests/SubFS.test.ts b/packages/yarnpkg-fslib/tests/SubFS.test.ts new file mode 100644 index 000000000000..9bb964e0d453 --- /dev/null +++ b/packages/yarnpkg-fslib/tests/SubFS.test.ts @@ -0,0 +1,16 @@ +import {SubFS} from '../sources/SubFS'; +import {NodeFS, PortablePath, npath, ppath} from '../sources'; + +describe(`SubFS`, () => { + it(`should fix the dirent entries returned by readdir w/ withFileTypes`, () => { + const pkgDir = ppath.dirname(npath.toPortablePath(__dirname)); + + const nodeFs = new NodeFS(); + const subFs = new SubFS(pkgDir, {baseFs: nodeFs}); + + const entries = subFs.readdirSync(`tests` as PortablePath, {withFileTypes: true}); + const thisTestEntry = entries.find(entry => entry.name === `SubFS.test.ts`); + + expect(thisTestEntry.path).toEqual(`tests` as PortablePath); + }); +});