Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature-request fs.fileWrite with option.mkdirp #33559

Closed
kaizhu256 opened this issue May 25, 2020 · 14 comments
Closed

feature-request fs.fileWrite with option.mkdirp #33559

kaizhu256 opened this issue May 25, 2020 · 14 comments
Labels
feature request Issues that request new features to be added to Node.js. fs Issues and PRs related to the fs subsystem / file system. stale

Comments

@kaizhu256
Copy link
Contributor

Is your feature request related to a problem? Please describe.

i am author of npm-package istanbul-lite.
when creating coverage-reports and artifacts,
its common to lazily write data to file-directories that have yet to be created.

Describe the solution you'd like

add extra boolean <options>.mkdirp to functions fs.writeFile and fs.writeFileSync,
that will lazily create missing file-directories when writing to file.
the common-use-case are:

  • lazily generate directories when writing coverage-report of instrumented files
  • lazily generate directories when writing artifacts in ci and testing
  • lazily generate directories when scaffolding new web-projects

Describe alternatives you've considered

i currently use this helper-function in all my projects,
to lazily generate directories when writing artifacts and coverage-reports.

    function fsWriteFileWithMkdirpSync (file, data) {
    /*
     * this function will sync write <data> to <file> with "mkdir -p"
     */
        let fs;
        // do nothing if module does not exist
        try {
            fs = require("fs");
        } catch (ignore) {
            return;
        }
        // try to write file
        try {
            fs.writeFileSync(file, data);
            return true;
        } catch (ignore) {
            // mkdir -p
            fs.mkdirSync(require("path").dirname(file), {
                recursive: true
            });
            // rewrite file
            fs.writeFileSync(file, data);
            return true;
        }
    };
@devsnek devsnek added feature request Issues that request new features to be added to Node.js. fs Issues and PRs related to the fs subsystem / file system. labels May 25, 2020
@devsnek
Copy link
Member

devsnek commented May 25, 2020

Given that you can just do this (recursive mkdir doesn't throw if the directory already exists):

fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, data);

I don't personally think adding the option is needed.

@kaizhu256
Copy link
Contributor Author

calling fs.mkdirSync(path.dirname(file), { recursive: true }); is expensive,
when generating 100+ artifacts/coverage-reports/scaffolding.

its more efficient to first attempt a normal fs.writeFile in try-catch, and lazily mkdir in the uncommon-case that directory doesn't exist.

@devsnek
Copy link
Member

devsnek commented May 25, 2020

I assume you don't have 100+ output directories though?

@kaizhu256
Copy link
Contributor Author

yes, common-case is far fewer directories than files

@benjamingr
Copy link
Member

This sounds pretty useful and I am not opposed in principle. I tried looking for it elsewhere but most libraries I saw just mkdirSync'd and writeFileSync'd. Can you maybe share some examples from libraries in the ecosystem that do this?

@kaizhu256
Copy link
Contributor Author

yea, i'm only aware of myself following practice of lazy-mkdirp-after-failed-write.

it seems official istanbuljs will eager mkdirSync before writing to file (https://github.com/istanbuljs/istanbuljs/blob/65b1331b52ca92f2aa3e7c41218698a57cba6a42/packages/istanbul-lib-report/lib/file-writer.js#L184)

but here are code-snippets from my own public-projects that will lazy-mkdirp-after-failed-write:

  • create html-coverage-report in arbitrary sub-directory dictated by instrumented-file's pathname
// https://github.com/kaizhu256/node-istanbul-lite/blob/63044848968528d677591e352d063eae8c76ada1/lib.istanbul.js#L11554
    writeFile: function (file, onError) {
        local.coverageReportHtml += local.writerData + "\n\n";
        if (!local.isBrowser && local.writerFile) {
            local.fsWriteFileWithMkdirpSync(local.writerFile, local.writerData);
        }
        local.writerData = "";
        local.writerFile = file;
        onError(local.writer);
    }
  • create test-report artifact
// https://github.com/kaizhu256/node-utility2/blob/0c44e424ce0bd20d7fc03e4711eeaaa888cbb166/lib.utility2.js#L6676
    // create test-report.html
    local.fsWriteFileWithMkdirpSync(
        "tmp/build/test-report.html",
        local.testReportMerge(testReport)
    );
  • test webserver by running it on local-port, download its assets, and save them as artifacts:
// https://github.com/kaizhu256/node-utility2/blob/0c44e424ce0bd20d7fc03e4711eeaaa888cbb166/lib.utility2.js#L3212
    local.onParallelList({
        list: [
            {
                file: "/LICENSE",
                url: "/LICENSE"
            }, {
                file: "/assets." + local.env.npm_package_nameLib + ".css",
                url: "/assets." + local.env.npm_package_nameLib + ".css"
            }, {
                file: "/assets." + local.env.npm_package_nameLib + ".html",
                url: "/index.html"
            }, {
                file: "/assets." + local.env.npm_package_nameLib + ".js",
                url: "/assets." + local.env.npm_package_nameLib + ".js"
...
        xhr = await local.httpFetch(local.serverLocalHost + opt2.url, {
            responseType: "raw"
        });
...
        local.fsWriteFileWithMkdirpSync(
            "tmp/build/app" + opt2.file,
            xhr.data
        );

@rickyes
Copy link
Contributor

rickyes commented May 26, 2020

Perhaps a function-type parameter can be provided, which is executed when the file opening fails.

@DavenportEmma
Copy link
Contributor

I'd like to give this a shot

@bcoe
Copy link
Contributor

bcoe commented Nov 16, 2020

@kaizhu256 I'm on the fence, writing:

fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, data);

Isn't too much of a hassle. However, I do appreciate us eliminating toil where possible.

Can you find an example of any other platforms that have this functionality, sometimes if I can't make my mind up about a feature, I find it useful to look at the standard libraries for a few other languages.

@kaizhu256
Copy link
Contributor Author

kaizhu256 commented Nov 16, 2020

@bcoe, the toil is actually greater, because as end-user, i want:

  1. have no performance-impact on fs.writeFile in common-case where mkdirp is not required
  2. silently ignore ENOENT error
// 1. have no performance-impact on fs.writeFile in common-case where mkdirp is not required
// by first attempting to fs.writeFile[Sync] with no mkdir
try {
    fs.writeFileSync(file, data);
} catch (errCaught) {
    // 2. silently ignore `ENOENT` error
    // and lazy-mkdirp after common-case fails
    // (where performance is usually not critical)
    if (errCaught?.code === 'ENOENT') {
        fs.mkdirSync(path.dirname(file), { recursive: true });
        fs.writeFileSync(file, data);
        return;
    }
    throw errCaught;
}

unfortunately, i'm not aware of file-write-with-lazy-mkdirp in other platforms. i do use this feature however, in my npm-packages istanbul-lite (create coverage-artifacts) and utility2 (create test-reports and build-artifacts):

https://github.com/kaizhu256/node-istanbul-lite/blob/2020.11.12/lib.istanbul.js#L408
https://github.com/kaizhu256/node-utility2/blob/2020.11.12/lib.utility2.js#L4054

the closest thing i'm aware of is posix "mkdir -p" (which ignores ENOENT):

#!/bin/sh
mkdir -p /tmp/aa/bb/ && printf 'hello world!\n' > /tmp/aa/bb/foo.txt

@kaizhu256
Copy link
Contributor Author

here's possible real-world application to npm-package istanbul-reports
removing external-dependency make-dir, and replacing it with built-in fs.writeFileSync and fs.openWithMkdirpSync:

# remove-dependency-make-dir.diff

diff --git a/monorepo-merge-reports.js b/monorepo-merge-reports.js
index 74a5497..f22e13a 100644
--- a/monorepo-merge-reports.js
+++ b/monorepo-merge-reports.js
@@ -5,12 +5,11 @@ const path = require('path');
 const { spawnSync } = require('child_process');

 const rimraf = require('rimraf');
-const makeDir = require('make-dir');
 const glob = require('glob');

 process.chdir(__dirname);
 rimraf.sync('.nyc_output');
-makeDir.sync('.nyc_output');
+require('fs').mkdirSync('.nyc_output');
 // Merge coverage data from each package so we can generate a complete report
 glob.sync('packages/*/.nyc_output').forEach(nycOutput => {
     const cwd = path.dirname(nycOutput);
diff --git a/packages/istanbul-lib-report/lib/file-writer.js b/packages/istanbul-lib-report/lib/file-writer.js
index de1154b..c071173 100644
--- a/packages/istanbul-lib-report/lib/file-writer.js
+++ b/packages/istanbul-lib-report/lib/file-writer.js
@@ -5,7 +5,6 @@
  */
 const path = require('path');
 const fs = require('fs');
-const mkdirp = require('make-dir');
 const supportsColor = require('supports-color');

 /**
@@ -157,14 +156,13 @@ class FileWriter {
             throw new Error(`Cannot write to absolute path: ${dest}`);
         }
         dest = path.resolve(this.baseDir, dest);
-        mkdirp.sync(path.dirname(dest));
         let contents;
         if (header) {
             contents = header + fs.readFileSync(source, 'utf8');
         } else {
             contents = fs.readFileSync(source);
         }
-        fs.writeFileSync(dest, contents);
+        fs.writeFileSync(dest, contents, { mkdirp: true });
     }

     /**
@@ -181,8 +179,7 @@ class FileWriter {
             throw new Error(`Cannot write to absolute path: ${file}`);
         }
         file = path.resolve(this.baseDir, file);
-        mkdirp.sync(path.dirname(file));
-        return new FileContentWriter(fs.openSync(file, 'w'));
+        return new FileContentWriter(fs.openWithMkdirpSync(file, 'w'));
     }
 }

diff --git a/packages/istanbul-lib-report/package.json b/packages/istanbul-lib-report/package.json
index 91cb16e..3cd8783 100644
--- a/packages/istanbul-lib-report/package.json
+++ b/packages/istanbul-lib-report/package.json
@@ -13,7 +13,6 @@
   },
   "dependencies": {
     "istanbul-lib-coverage": "^3.0.0",
-    "make-dir": "^3.0.0",
     "supports-color": "^7.1.0"
   },
   "devDependencies": {
diff --git a/packages/istanbul-lib-report/test/file-writer.test.js b/packages/istanbul-lib-report/test/file-writer.test.js
index 707785b..843e3a8 100644
--- a/packages/istanbul-lib-report/test/file-writer.test.js
+++ b/packages/istanbul-lib-report/test/file-writer.test.js
@@ -4,7 +4,6 @@
 const fs = require('fs');
 const path = require('path');
 const assert = require('chai').assert;
-const mkdirp = require('make-dir');
 const rimraf = require('rimraf');
 const FileWriter = require('../lib/file-writer');

@@ -14,7 +13,6 @@ describe('file-writer', () => {
     let writer;

     beforeEach(() => {
-        mkdirp.sync(dataDir);
         writer = new FileWriter(dataDir);
     });

@bcoe
Copy link
Contributor

bcoe commented Nov 17, 2020

@iansu and @joesepi have been working on a document outlining some of the future improvements to fs operations we'd like to make, as part of the @nodejs/tooling group (perhaps they could chime in too).

kaizhu256 added a commit to kaizhu256/node that referenced this issue Nov 17, 2020
... and new function fs.openWithMkdirp[Sync]
this feature is intended to improve ergonomics/simplify-scripting when:
- creating build-artifacts/coverage-files during ci
- scaffolding new web-projects
- cloning website with web-crawler

allowing user to lazily create ad-hoc directory-structures as need
during file-creation with ergonomic syntax:
```
fs.writeFileSync(
    "foo/bar/baz/qux.txt",
    "hello world!",
    { mkdirp: true } // will "mkdir -p" foo/bar/baz as needed
);
```

Fixes: nodejs#33559
kaizhu256 added a commit to kaizhu256/node that referenced this issue Jan 11, 2021
    - fs.open[Sync],
    - fs.writeFile[Sync],
    - fs.appendFile[Sync]

this feature is intended to improve ergonomics/simplify-scripting when:
- creating build-artifacts/coverage-files during ci
- scaffolding new web-projects
- cloning website with web-crawler

allowing user to lazily create ad-hoc directory-structures as need
during file-creation with ergonomic syntax:
```
fs.writeFileSync(
    "foo/bar/baz/qux.txt",
    "hello world!",
    { parents: true } // will lazily create parent foo/bar/baz/ as needed
);
```

fs: add new signature-form fs.open(path[, options], callback)
fs: add new signature-form fs.openSync(path[, options])

Fixes: nodejs#33559
kaizhu256 added a commit to kaizhu256/node that referenced this issue Jan 12, 2021
    - fs.open[Sync],
    - fs.writeFile[Sync],
    - fs.appendFile[Sync]

this feature is intended to improve ergonomics/simplify-scripting when:
- creating build-artifacts/coverage-files during ci
- scaffolding new web-projects
- cloning website with web-crawler

allowing user to lazily create ad-hoc directory-structures as need
during file-creation with ergonomic syntax:
```
fs.writeFileSync(
    "foo/bar/baz/qux.txt",
    "hello world!",
    { parents: true } // will lazily create parent foo/bar/baz/ as needed
);
```

fs: add new signature-form fs.open(path[, options], callback)
fs: add new signature-form fs.openSync(path[, options])

Fixes: nodejs#33559
kaizhu256 added a commit to kaizhu256/node that referenced this issue Jan 12, 2021
    - fs.open[Sync],
    - fs.writeFile[Sync],
    - fs.appendFile[Sync]
fs: add new signature-form fs.open(path[, options], callback)
fs: add new signature-form fs.openSync(path[, options])
fs: rename option `flags` to `flag` in fs.open for consistency with fs.writeFile[Sync]

this feature is intended to improve ergonomics/simplify-scripting when:
    - creating build-artifacts/coverage-files during ci
    - scaffolding new web-projects
    - cloning website with web-crawler
allowing user to lazily create ad-hoc directory-structures as need
during file-creation with ergonomic syntax:
```
fs.writeFileSync(
    "foo/bar/baz/qux.txt",
    "hello world!",
    { parents: true } // will lazily create parent foo/bar/baz/ as needed
);
```

Fixes: nodejs#33559
@github-actions
Copy link
Contributor

There has been no activity on this feature request for 5 months and it is unlikely to be implemented. It will be closed 6 months after the last non-automated comment.

For more information on how the project manages feature requests, please consult the feature request management document.

@github-actions github-actions bot added the stale label Mar 18, 2022
@github-actions
Copy link
Contributor

There has been no activity on this feature request and it is being closed. If you feel closing this issue is not the right thing to do, please leave a comment.

For more information on how the project manages feature requests, please consult the feature request management document.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature request Issues that request new features to be added to Node.js. fs Issues and PRs related to the fs subsystem / file system. stale
Projects
None yet
Development

No branches or pull requests

6 participants