Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 11 additions & 7 deletions packages/fs-core/src/Superblock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ export class Superblock {
this.root = root;
}

private filenameToSteps(filename: string): string[] {
return filenameToSteps(filename, this.process.cwd());
}

/** Fan-out of file system change events. Multiple consumers may subscribe. */
public readonly changes = new FanOut<FsEvent>();

Expand Down Expand Up @@ -222,7 +226,7 @@ export class Superblock {
steps = stepsOrFilenameOrLink.steps;
filename = pathSep + steps.join(pathSep);
} else if (typeof stepsOrFilenameOrLink === 'string') {
steps = filenameToSteps(stepsOrFilenameOrLink);
steps = this.filenameToSteps(stepsOrFilenameOrLink);
filename = stepsOrFilenameOrLink;
} else {
steps = stepsOrFilenameOrLink;
Expand Down Expand Up @@ -263,7 +267,7 @@ export class Superblock {
if (node.isSymlink() && (resolveSymlinks || i < steps.length - 1)) {
const resolvedPath = isAbsolute(node.symlink) ? node.symlink : pathJoin(dirname(curr.getPath()), node.symlink); // Relative to symlink's parent

steps = filenameToSteps(resolvedPath).concat(steps.slice(i + 1));
steps = this.filenameToSteps(resolvedPath).concat(steps.slice(i + 1));
curr = this.root;
i = 0;
continue;
Expand Down Expand Up @@ -351,7 +355,7 @@ export class Superblock {

getLinkParentAsDirOrThrow(filenameOrSteps: string | string[], funcName?: string): Link {
const steps: string[] = (
filenameOrSteps instanceof Array ? filenameOrSteps : filenameToSteps(filenameOrSteps)
filenameOrSteps instanceof Array ? filenameOrSteps : this.filenameToSteps(filenameOrSteps)
).slice(0, -1);
const filename: string = pathSep + steps.join(pathSep);
const link = this.getLinkOrThrow(filename, funcName);
Expand Down Expand Up @@ -524,7 +528,7 @@ export class Superblock {
modeNum: number | undefined,
resolveSymlinks: boolean = true,
): File {
const steps = filenameToSteps(filename);
const steps = this.filenameToSteps(filename);
let link: Link | null;
try {
link = resolveSymlinks ? this.getResolvedLinkOrThrow(filename, 'open') : this.getLinkOrThrow(filename, 'open');
Expand Down Expand Up @@ -666,7 +670,7 @@ export class Superblock {
};

public readonly symlink = (targetFilename: string, pathFilename: string): Link => {
const pathSteps = filenameToSteps(pathFilename);
const pathSteps = this.filenameToSteps(pathFilename);
// Check if directory exists, where we about to create a symlink.
let dirLink;
try {
Expand Down Expand Up @@ -745,7 +749,7 @@ export class Superblock {
};

public readonly mkdir = (filename: string, modeNum: number): void => {
const steps = filenameToSteps(filename);
const steps = this.filenameToSteps(filename);
// This will throw if user tries to create root dir `fs.mkdirSync('/')`.
if (!steps.length) throw createError(ERROR_CODE.EEXIST, 'mkdir', filename);
const dir = this.getLinkParentAsDirOrThrow(filename, 'mkdir');
Expand All @@ -763,7 +767,7 @@ export class Superblock {
*/
public readonly mkdirp = (filename: string, modeNum: number): string | undefined => {
let created = false;
const steps = filenameToSteps(filename);
const steps = this.filenameToSteps(filename);
let curr: Link | null = null;
let i = steps.length;
// Find the longest subpath of filename that still exists:
Expand Down
12 changes: 12 additions & 0 deletions packages/fs-node/src/__tests__/volume.process.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,16 @@ describe('Volume with custom process', () => {
expect(stat.gid).toBe(99);
});
});

describe('relative paths', () => {
it('resolves write operations against the custom process cwd', () => {
const customProcess = makeProcess({ cwd: () => '/app' });
const vol = Volume.fromJSON({ '/app': null }, '/', { process: customProcess });

vol.writeFileSync('data.txt', 'hello');

expect(vol.readFileSync('/app/data.txt', 'utf8')).toBe('hello');
expect(vol.readFileSync('data.txt', 'utf8')).toBe('hello');
});
});
});
36 changes: 35 additions & 1 deletion packages/memfs/src/__tests__/memfs.process.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { memfs } from '../index';
import { fs as defaultFs, memfs } from '../index';
import type { IProcess } from '../index';

const makeProcess = (overrides: Partial<IProcess> = {}): IProcess => ({
Expand Down Expand Up @@ -50,4 +50,38 @@ describe('memfs() with custom process', () => {
const p: IProcess = makeProcess();
expect(typeof p.cwd).toBe('function');
});

it('resolves relative createWriteStream paths against the default cwd', done => {
const { fs } = memfs();
const stream = fs.createWriteStream('downloaded.zip') as any;

stream.on('error', done);
stream.on('finish', () => {
expect(fs.readFileSync('/downloaded.zip', 'utf8')).toBe('content');
expect(fs.readFileSync('downloaded.zip', 'utf8')).toBe('content');
done();
});

stream.end('content');
});

it('resolves relative createWriteStream paths for the default fs export', done => {
const filename = '/default-create-write-stream.txt';
try {
defaultFs.unlinkSync(filename);
} catch (err) {
if ((err as any).code !== 'ENOENT') throw err;
}

const stream = defaultFs.createWriteStream('default-create-write-stream.txt') as any;

stream.on('error', done);
stream.on('finish', () => {
expect(defaultFs.readFileSync(filename, 'utf8')).toBe('content');
defaultFs.unlinkSync(filename);
done();
});

stream.end('content');
});
});
19 changes: 17 additions & 2 deletions packages/memfs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,22 @@ const { F_OK, R_OK, W_OK, X_OK } = constants;
export { DirectoryJSON, NestedDirectoryJSON, Volume };
export type { IProcess };

const getGlobalProcess = (): Partial<IProcess> => {
const maybeProcess = typeof globalThis !== 'undefined' ? (globalThis as any).process : undefined;
return maybeProcess ?? {};
};

const createProcess = (cwd: string, baseProcess: Partial<IProcess> = getGlobalProcess()): IProcess => ({
cwd: () => cwd,
platform: baseProcess.platform ?? 'linux',
emitWarning: baseProcess.emitWarning?.bind(baseProcess) ?? (() => {}),
env: baseProcess.env ?? {},
getuid: baseProcess.getuid?.bind(baseProcess),
getgid: baseProcess.getgid?.bind(baseProcess),
});

// Default volume.
export const vol = new Volume();
export const vol = Volume.fromNestedJSON({}, '/', { process: createProcess('/') });

export interface IFs extends Volume {
constants: typeof constants;
Expand Down Expand Up @@ -96,7 +110,8 @@ export const memfs = (
// Superblock use that process's cwd(). Otherwise default to '/' so the
// convenience function keeps its opinionated virtual-root default.
const cwd = opts.cwd ?? (opts.process ? undefined : '/');
const vol = Volume.fromNestedJSON(json, cwd, { process: opts.process });
const process = cwd === undefined ? opts.process : createProcess(cwd, opts.process);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we create process with createProcess() here? Why not just pass through opts.process as before?

const vol = Volume.fromNestedJSON(json, cwd, { process });
const fs = createFsFromVolume(vol);
return { fs, vol };
};
Expand Down
Loading