Repository: streamich/memfs
Pull request #880 (2022-11-12 05:27:30)

feat(volume): now handling `Buffer` as volume content
![code.png](https://i.imgur.com/8xtxc1f.png)

## Context

Hi ! While trying to inject some `Buffer` into a `memfs` volume from the `fromJSON` method, I noticed it creates a virtual folder. I expected the method to accept `Buffer` as content but I noticed only `string` are "allowed".

This PR resolves cases when you want to inject raw binary data into your volumes. For example when you have some test archives to validate.

## Actions

I have written a small patch to fix the issue. I tried to make as few changes as possible. It comes with a new test and some adjustments to the documentation.

I hope it's enough for a quick merge. If not, feel free to share adjustments you would like to be done. I'll try to be as reactive as possible.

Best regards,

Stanley.

shorwood: > You need to apply prettier

Done.
G-Rath: @shorwood are you able to rebase this please?
shorwood: > @shorwood are you able to rebase this please?

Hi, it's done. Sorry for the close/reopen, I had to sync and cherry pick because rebase was taking too long on my PC. I have also taken the opportunity to merge the commit from #881 (since they are very similar) and reword them a bit for better understanding.

Awaiting your feedback.
shorwood: > @shorwood are you able to rebase this please?

Hi, it's done. Sorry for the close/reopen, I had to sync and cherry pick because rebase was taking too long on my PC. I have also taken the opportunity to merge the commit from #881 (since they are very similar) and reword them a bit for better understanding.

Awaiting your feedback.

Patch:
diff --git a/docs/node/reference.md b/docs/node/reference.md
index f08d8257..44cffcba 100644
--- a/docs/node/reference.md
+++ b/docs/node/reference.md
@@ -71,6 +71,7 @@ vol.fromJSON(
   {
     './index.js': '...',
     './package.json': '...',
+    './index.node': new Buffer(),
   },
   '/app',
 );
diff --git a/src/volume.ts b/src/volume.ts
index 88044c8f..c9e1cc77 100644
--- a/src/volume.ts
+++ b/src/volume.ts
@@ -188,13 +188,13 @@ function validateGid(gid: number) {
 }
 
 // ---------------------------------------- Volume
-type DirectoryContent = string | null;
+type DirectoryContent = string | Buffer | null;
 
-export interface DirectoryJSON {
-  [key: string]: DirectoryContent;
+export interface DirectoryJSON<T extends DirectoryContent = DirectoryContent> {
+  [key: string]: T;
 }
-export interface NestedDirectoryJSON {
-  [key: string]: DirectoryContent | NestedDirectoryJSON;
+export interface NestedDirectoryJSON<T extends DirectoryContent = DirectoryContent> {
+  [key: string]: T | NestedDirectoryJSON;
 }
 
 function flattenJSON(nestedJSON: NestedDirectoryJSON): DirectoryJSON {
@@ -206,7 +206,7 @@ function flattenJSON(nestedJSON: NestedDirectoryJSON): DirectoryJSON {
 
       const joinedPath = join(pathPrefix, path);
 
-      if (typeof contentOrNode === 'string') {
+      if (typeof contentOrNode === 'string' || contentOrNode instanceof Buffer) {
         flatJSON[joinedPath] = contentOrNode;
       } else if (typeof contentOrNode === 'object' && contentOrNode !== null && Object.keys(contentOrNode).length > 0) {
         // empty directories need an explicit entry and therefore get handled in `else`, non-empty ones are implicitly considered
@@ -526,7 +526,7 @@ export class Volume implements FsCallbackApi, FsSynchronousApi {
     });
   }
 
-  private _toJSON(link = this.root, json = {}, path?: string, asBuffer?: boolean): DirectoryJSON {
+  private _toJSON(link = this.root, json = {}, path?: string, asBuffer?: boolean): DirectoryJSON<string | null> {
     let isEmpty = true;
 
     let children = link.children;
@@ -568,7 +568,7 @@ export class Volume implements FsCallbackApi, FsSynchronousApi {
     return json;
   }
 
-  toJSON(paths?: PathLike | PathLike[], json = {}, isRelative = false, asBuffer = false): DirectoryJSON {
+  toJSON(paths?: PathLike | PathLike[], json = {}, isRelative = false, asBuffer = false): DirectoryJSON<string | null> {
     const links: Link[] = [];
 
     if (paths) {
@@ -595,7 +595,7 @@ export class Volume implements FsCallbackApi, FsSynchronousApi {
 
       filename = resolve(filename, cwd);
 
-      if (typeof data === 'string') {
+      if (typeof data === 'string' || data instanceof Buffer) {
         const dir = dirname(filename);
         this.mkdirpBase(dir, MODE.DIR);
 


Test patch:
diff --git a/src/__tests__/volume.test.ts b/src/__tests__/volume.test.ts
index 27933596..e482f9e9 100644
--- a/src/__tests__/volume.test.ts
+++ b/src/__tests__/volume.test.ts
@@ -274,6 +274,16 @@ describe('volume', () => {
         expect(stat.isDirectory()).toBe(true);
         expect(vol.readdirSync('/dir')).toEqual([]);
       });
+
+      it('supports using buffers for file content', () => {
+        const vol = new Volume();
+        const text = 'bip-boup';
+        const buffer = Buffer.from(text, 'utf-8');
+        vol.fromJSON({ '/buffer': buffer });
+        expect(vol.toJSON()).toStrictEqual({ '/buffer': text });
+        expect(vol.readFileSync('/buffer')).toStrictEqual(buffer);
+        expect(vol.readFileSync('/buffer', 'utf-8')).toStrictEqual(text);
+      });
     });
 
     describe('.fromNestedJSON(nestedJSON[, cwd])', () => {