Repository: streamich/memfs (ts)

Issue:
Typing mismatch between fs.mkdir and memfs.mkdir
Firstly and most importantly, huge thanks for this package, it's awesome.

I tried to drop in the output from `createFsFromVolume()` instead of the native `fs` and I hit a snag. There's a minor typing mismatch.

```
Conversion of type 'IFs' to type 'FS' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
  The types returned by 'promises.mkdir(...)' are incompatible between these types.
    Type 'Promise<void>' is not comparable to type 'Promise<string>'.
      Type 'void' is not comparable to type 'string'.ts(2352)
```

Looking into this a little, the node [fs.mkdir](https://nodejs.org/api/fs.html#fs_fs_mkdir_path_options_callback) and [fs.promises.mkdir](https://nodejs.org/api/fs.html#fs_fspromises_mkdir_path_options) optionally return the path to the first directory created when the `recursive: true` option is set. While `memfs.mkdir` always returns `void`.

Pull request description:
fix: `mkdir` return value
https://nodejs.org/dist/latest-v18.x/docs/api/fs.html#fsmkdirpath-options-callback

> The callback is given a possible exception and, if recursive is true, the first directory path created, (err[, path]). path can still be undefined when recursive is true, if no directory was created.

https://nodejs.org/dist/latest-v18.x/docs/api/fs.html#fsmkdirsyncpath-options

> Synchronously creates a directory. Returns undefined, or if recursive is true, the first directory path created.

Resolves #546

Patch:
diff --git a/src/volume.ts b/src/volume.ts
index cc098c67..eea9c5f4 100644
--- a/src/volume.ts
+++ b/src/volume.ts
@@ -225,9 +225,11 @@ function optsGenerator<TOpts>(defaults: TOpts): (opts) => TOpts {
   return options => getOptions(defaults, options);
 }
 
-function validateCallback(callback) {
+type AssertCallback<T> = T extends Function ? T : never;
+
+function validateCallback<T>(callback: T): AssertCallback<T> {
   if (typeof callback !== 'function') throw TypeError(ERRSTR.CB);
-  return callback;
+  return callback as AssertCallback<T>;
 }
 
 function optsAndCbGenerator<TOpts, TResult>(getOpts): (options, callback?) => [TOpts, TCallback<TResult>] {
@@ -431,7 +433,7 @@ if (isWin) {
 
 export function filenameToSteps(filename: string, base?: string): string[] {
   const fullPath = resolve(filename, base);
-  const fullPathSansSlash = fullPath.substr(1);
+  const fullPathSansSlash = fullPath.substring(1);
   if (!fullPathSansSlash) return [];
   return fullPathSansSlash.split(sep);
 }
@@ -726,7 +728,7 @@ export class Volume {
 
   // Generates 6 character long random string, used by `mkdtemp`.
   genRndStr() {
-    const str = (Math.random() + 1).toString(36).substr(2, 6);
+    const str = (Math.random() + 1).toString(36).substring(2, 8);
     if (str.length === 6) return str;
     else return this.genRndStr();
   }
@@ -1920,8 +1922,11 @@ export class Volume {
    * @param modeNum
    */
   private mkdirpBase(filename: string, modeNum: number) {
-    const steps = filenameToSteps(filename);
+    const fullPath = resolve(filename);
+    const fullPathSansSlash = fullPath.substring(1);
+    const steps = !fullPathSansSlash ? [] : fullPathSansSlash.split(sep);
     let link = this.root;
+    let created = false;
     for (let i = 0; i < steps.length; i++) {
       const step = steps[i];
 
@@ -1933,23 +1938,30 @@ export class Volume {
         else throw createError(ENOTDIR, 'mkdir', child.getPath());
       } else {
         link = link.createChild(step, this.createNode(true, modeNum));
+        created = true;
       }
     }
+    return created ? fullPath : undefined;
   }
 
+  mkdirSync(path: PathLike, options: IMkdirOptions & { recursive: true }): string | undefined;
+  mkdirSync(path: PathLike, options?: TMode | (IMkdirOptions & { recursive?: false })): void;
+  mkdirSync(path: PathLike, options?: TMode | IMkdirOptions): string | undefined;
   mkdirSync(path: PathLike, options?: TMode | IMkdirOptions) {
     const opts = getMkdirOptions(options);
     const modeNum = modeToNumber(opts.mode, 0o777);
     const filename = pathToFilename(path);
-    if (opts.recursive) this.mkdirpBase(filename, modeNum);
-    else this.mkdirBase(filename, modeNum);
+    if (opts.recursive) return this.mkdirpBase(filename, modeNum);
+    this.mkdirBase(filename, modeNum);
   }
 
   mkdir(path: PathLike, callback: TCallback<void>);
-  mkdir(path: PathLike, mode: TMode | IMkdirOptions, callback: TCallback<void>);
-  mkdir(path: PathLike, a: TCallback<void> | TMode | IMkdirOptions, b?: TCallback<void>) {
+  mkdir(path: PathLike, mode: TMode | (IMkdirOptions & { recursive?: false }), callback: TCallback<void>);
+  mkdir(path: PathLike, mode: IMkdirOptions & { recursive: true }, callback: TCallback<string>);
+  mkdir(path: PathLike, mode: TMode | IMkdirOptions, callback: TCallback<string>);
+  mkdir(path: PathLike, a: TCallback<void> | TMode | IMkdirOptions, b?: TCallback<string> | TCallback<void>) {
     const opts: TMode | IMkdirOptions = getMkdirOptions(a);
-    const callback: TCallback<void> = validateCallback(typeof a === 'function' ? a : b);
+    const callback = validateCallback(typeof a === 'function' ? a : b!);
     const modeNum = modeToNumber(opts.mode, 0o777);
     const filename = pathToFilename(path);
     if (opts.recursive) this.wrapAsync(this.mkdirpBase, [filename, modeNum], callback);
@@ -1958,14 +1970,14 @@ export class Volume {
 
   // legacy interface
   mkdirpSync(path: PathLike, mode?: TMode) {
-    this.mkdirSync(path, { mode, recursive: true });
+    return this.mkdirSync(path, { mode, recursive: true });
   }
 
-  mkdirp(path: PathLike, callback: TCallback<void>);
-  mkdirp(path: PathLike, mode: TMode, callback: TCallback<void>);
-  mkdirp(path: PathLike, a: TCallback<void> | TMode, b?: TCallback<void>) {
+  mkdirp(path: PathLike, callback: TCallback<string>);
+  mkdirp(path: PathLike, mode: TMode, callback: TCallback<string>);
+  mkdirp(path: PathLike, a: TCallback<string> | TMode, b?: TCallback<string>) {
     const mode: TMode | undefined = typeof a === 'function' ? undefined : a;
-    const callback: TCallback<void> = validateCallback(typeof a === 'function' ? a : b);
+    const callback = validateCallback(typeof a === 'function' ? a : b);
     this.mkdir(path, { mode, recursive: true }, callback);
   }
 


Test patch:
diff --git a/src/__tests__/volume.test.ts b/src/__tests__/volume.test.ts
index 317dfda0..298993b8 100644
--- a/src/__tests__/volume.test.ts
+++ b/src/__tests__/volume.test.ts
@@ -1008,7 +1008,7 @@ describe('volume', () => {
       });
       it('Create /dir1/dir2/dir3 recursively', () => {
         const vol = new Volume();
-        vol.mkdirSync('/dir1/dir2/dir3', { recursive: true });
+        const fullPath = vol.mkdirSync('/dir1/dir2/dir3', { recursive: true });
         const dir1 = tryGetChild(vol.root, 'dir1');
         const dir2 = tryGetChild(dir1, 'dir2');
         const dir3 = tryGetChild(dir2, 'dir3');
@@ -1018,6 +1018,9 @@ describe('volume', () => {
         expect(dir1.getNode().isDirectory()).toBe(true);
         expect(dir2.getNode().isDirectory()).toBe(true);
         expect(dir3.getNode().isDirectory()).toBe(true);
+        expect(fullPath).toBe('/dir1/dir2/dir3');
+        const dirAlreadyExists = vol.mkdirSync('/dir1/dir2/dir3', { recursive: true });
+        expect(dirAlreadyExists).toBe(undefined);
       });
     });
     describe('.mkdir(path[, mode], callback)', () => {