├── .github ├── FUNDING.yml └── workflows │ └── tests.yml ├── .gitignore ├── LICENSE ├── demo ├── a b │ └── readme.md ├── a │ ├── b │ │ └── readme.md │ └── readme.md ├── noread.md ├── readme.md └── test.json ├── files.js ├── files.test.js ├── index.d.ts ├── index.types.ts ├── package.json └── readme.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: https://www.paypal.me/franciscopresencia/19 2 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | push: 5 | schedule: 6 | - cron: "0 0 1 1 *" # Runs every January 1st at 00:00 UTC 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | strategy: 13 | matrix: 14 | platform: [ubuntu-latest, macos-latest, windows-latest] 15 | node-version: [18.x, 20.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v1 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - name: Installing 24 | run: npm install 25 | - name: Testing 26 | run: npm test 27 | env: 28 | CI: true 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | package-lock.json 64 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Francisco Presencia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /demo/a b/readme.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/franciscop/files/0f25c67214e74e532c1bafbab1ec1b69ed346eec/demo/a b/readme.md -------------------------------------------------------------------------------- /demo/a/b/readme.md: -------------------------------------------------------------------------------- 1 | # Sub-sub-level 2 | -------------------------------------------------------------------------------- /demo/a/readme.md: -------------------------------------------------------------------------------- 1 | # Sub-level 2 | -------------------------------------------------------------------------------- /demo/noread.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/franciscop/files/0f25c67214e74e532c1bafbab1ec1b69ed346eec/demo/noread.md -------------------------------------------------------------------------------- /demo/readme.md: -------------------------------------------------------------------------------- 1 | # Hello! 2 | 3 | I am in first level 4 | -------------------------------------------------------------------------------- /demo/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "hello": "world" 3 | } 4 | -------------------------------------------------------------------------------- /files.js: -------------------------------------------------------------------------------- 1 | // The best filesystem for promises and array manipulation 2 | import run from "atocha"; 3 | import fs from "node:fs"; 4 | import fsp from "node:fs/promises"; 5 | import { homedir, tmpdir } from "node:os"; 6 | import path from "node:path"; 7 | import { Readable } from "node:stream"; 8 | import swear from "swear"; 9 | 10 | // Find whether it's Linux or Mac, where we can use `find` 11 | const mac = () => process.platform === "darwin"; 12 | const linux = () => process.platform === "linux"; 13 | 14 | // Retrieve the full, absolute path for the path 15 | const abs = swear(async (name = ".", base = process.cwd()) => { 16 | name = await name; 17 | base = await base; 18 | 19 | // Absolute paths do not need more absolutism 20 | if (path.isAbsolute(name)) return name; 21 | 22 | if (name.slice(0, 2) === "~/") { 23 | base = await home(); 24 | name = name.slice(2); 25 | } 26 | 27 | // We are off-base here; recover the viable base option 28 | if (!base || typeof base !== "string") { 29 | base = process.cwd(); 30 | } 31 | 32 | // Return the file/folder within the base 33 | return join(base, name); 34 | }); 35 | 36 | const copy = swear(async (src, dst) => { 37 | src = await abs(src); 38 | dst = await abs(dst); 39 | await mkdir(dir(dst)); 40 | await fsp.copyFile(src, dst); 41 | return dst; 42 | }); 43 | 44 | // Get the directory from path 45 | const dir = swear(async (name = ".") => { 46 | name = await abs(name); 47 | return path.dirname(name); 48 | }); 49 | 50 | // Check whether a filename exists or not 51 | const exists = swear(async (name) => { 52 | name = await abs(name); 53 | return fsp.access(name).then( 54 | () => true, 55 | () => false, 56 | ); 57 | }); 58 | 59 | // Get the home directory: https://stackoverflow.com/a/9081436/938236 60 | const home = swear((...args) => join(homedir(), ...args).then(mkdir)); 61 | 62 | // Put several path segments together 63 | const join = swear((...parts) => abs(path.join(...parts))); 64 | 65 | // List all the files in the folder 66 | const list = swear(async (dir) => { 67 | dir = await abs(dir); 68 | return swear(fsp.readdir(dir)).map((file) => abs(file, dir)); 69 | }); 70 | 71 | // Create a new directory in the specified path 72 | // Note: `recursive` flag on Node.js is ONLY for Mac and Windows (not Linux), so 73 | // it's totally worthless for us 74 | const mkdir = swear(async (name) => { 75 | name = await abs(name); 76 | 77 | // Create a recursive list of paths to create, from the highest to the lowest 78 | const list = name 79 | .split(path.sep) 80 | .map((part, i, all) => all.slice(0, i + 1).join(path.sep)) 81 | .filter(Boolean); 82 | 83 | // Build each nested path sequentially 84 | for (let path of list) { 85 | if (await exists(path)) continue; 86 | await fsp.mkdir(path).catch(() => null); 87 | } 88 | return name; 89 | }); 90 | 91 | const move = swear(async (src, dst) => { 92 | try { 93 | src = await abs(src); 94 | dst = await abs(dst); 95 | await mkdir(dir(dst)); 96 | await fsp.rename(src, dst); 97 | return dst; 98 | } catch (error) { 99 | // Some OS/environments don't allow move, so copy it first 100 | // and then remove the original 101 | if (error.code === "EXDEV") { 102 | await copy(src, dst); 103 | await remove(src); 104 | return dst; 105 | } else { 106 | throw error; 107 | } 108 | } 109 | }); 110 | 111 | // Get the path's filename 112 | const name = swear((file) => path.basename(file)); 113 | 114 | // Read the contents of a single file 115 | const read = swear(async (name, options = {}) => { 116 | name = await abs(name); 117 | const type = options && options.type ? options.type : "text"; 118 | if (type === "text") { 119 | return fsp.readFile(name, "utf-8").catch(() => null); 120 | } 121 | if (type === "json") { 122 | return read(name).then(JSON.parse); 123 | } 124 | if (type === "raw" || type === "buffer") { 125 | return fsp.readFile(name).catch(() => null); 126 | } 127 | if (type === "stream" || type === "web" || type === "webStream") { 128 | const file = await fsp.open(name); 129 | return file.readableWebStream(); 130 | } 131 | if (type === "node" || type === "nodeStream") { 132 | return fs.createReadStream(name); 133 | } 134 | }); 135 | 136 | // Delete a file or directory (recursively) 137 | const remove = swear(async (name) => { 138 | name = await abs(name); 139 | if (name === "/") throw new Error("Cannot remove the root folder `/`"); 140 | if (!(await exists(name))) return name; 141 | 142 | if (await stat(name).isDirectory()) { 143 | // Remove all content recursively 144 | await list(name).map(remove); 145 | await fsp.rmdir(name).catch(() => null); 146 | } else { 147 | await fsp.unlink(name).catch(() => null); 148 | } 149 | return name; 150 | }); 151 | 152 | const sep = path.sep; 153 | 154 | // Get some interesting info from the path 155 | const stat = swear(async (name) => { 156 | name = await abs(name); 157 | return fsp.lstat(name).catch(() => null); 158 | }); 159 | 160 | // Get a temporary folder 161 | const tmp = swear(async (...args) => { 162 | const path = await join(tmpdir(), ...args); 163 | return mkdir(path); 164 | }); 165 | 166 | // Perform a recursive walk 167 | const rWalk = (name) => { 168 | const file = abs(name); 169 | 170 | const deeper = async (file) => { 171 | if (await stat(file).isDirectory()) { 172 | return rWalk(file); 173 | } 174 | return [file]; 175 | }; 176 | 177 | // Note: list() already wraps the promise 178 | return list(file) 179 | .map(deeper) 180 | .reduce((all, arr) => all.concat(arr), []); 181 | }; 182 | 183 | // Attempt to make an OS walk, and fallback to the recursive one 184 | const walk = swear(async (name) => { 185 | name = await abs(name); 186 | if (!(await exists(name))) return []; 187 | if (linux() || mac()) { 188 | try { 189 | // Avoid double forward slash when it ends in "/" 190 | name = name.replace(/\/$/, ""); 191 | // Attempt to invoke run (command may fail for large directories) 192 | return await run(`find ${name} -type f`).split("\n").filter(Boolean); 193 | } catch (error) { 194 | // Fall back to rWalk() below 195 | } 196 | } 197 | return rWalk(name).filter(Boolean); 198 | }); 199 | 200 | // Create a new file with the specified contents 201 | const write = swear(async (name, body = "") => { 202 | name = await abs(name); 203 | // If it's a WebStream, convert it to a normal node stream 204 | if (body && body.pipeTo) { 205 | body = Readable.fromWeb(body); 206 | } 207 | if (body && body.then) { 208 | body = await body; 209 | } 210 | // If it's a type that is not a string nor a stream, convert it 211 | // into plain text with JSON.stringify 212 | if ( 213 | body && 214 | typeof body !== "string" && 215 | !body.pipe && 216 | !(body instanceof Buffer) 217 | ) { 218 | body = JSON.stringify(body); 219 | } 220 | await mkdir(dir(name)); 221 | await fsp.writeFile(name, body, "utf-8"); 222 | return name; 223 | }); 224 | 225 | const files = { 226 | abs, 227 | copy, 228 | dir, 229 | exists, 230 | home, 231 | join, 232 | list, 233 | mkdir, 234 | move, 235 | name, 236 | read, 237 | remove, 238 | rename: move, 239 | sep, 240 | stat, 241 | swear, 242 | tmp, 243 | walk, 244 | write, 245 | }; 246 | 247 | export { 248 | abs, 249 | copy, 250 | dir, 251 | exists, 252 | home, 253 | join, 254 | list, 255 | mkdir, 256 | move, 257 | name, 258 | read, 259 | remove, 260 | move as rename, 261 | sep, 262 | stat, 263 | swear, 264 | tmp, 265 | walk, 266 | write, 267 | }; 268 | 269 | export default files; 270 | -------------------------------------------------------------------------------- /files.test.js: -------------------------------------------------------------------------------- 1 | // Native file system and path 2 | import fs from "fs"; 3 | import path from "path"; 4 | import { dirname } from "path"; 5 | import { Readable } from "stream"; 6 | import { fileURLToPath } from "url"; 7 | import { promisify } from "util"; 8 | 9 | import cmd from "atocha"; 10 | import swear from "swear"; 11 | 12 | // fs-promises 13 | import { 14 | abs, 15 | copy, 16 | dir, 17 | exists, 18 | home, 19 | join, 20 | list, 21 | mkdir, 22 | move, 23 | name, 24 | read, 25 | remove, 26 | rename, 27 | sep, 28 | stat, 29 | tmp, 30 | walk, 31 | write, 32 | } from "."; 33 | 34 | const __filename = fileURLToPath(import.meta.url); 35 | const __dirname = dirname(__filename); 36 | 37 | // Find whether it's Linux or Mac, where we can use `find` 38 | const mac = () => process.platform === "darwin"; 39 | const linux = () => process.platform === "linux"; 40 | const windows = () => process.platform === "win32"; 41 | const unix = () => mac() || linux(); 42 | 43 | const root = linux() ? "/home/" : mac() ? "/Users/" : "C:\\projects"; 44 | 45 | describe("abs", () => { 46 | it("gets the defaults right", async () => { 47 | expect(await abs()).toBe(__dirname); 48 | expect(await abs("demo")).toBe(await join(__dirname, "/demo")); 49 | }); 50 | 51 | it("works with swear()", async () => { 52 | expect(await abs(swear("demo"))).toBe(await join(__dirname, "/demo")); 53 | }); 54 | 55 | it("get the absolute path of the passed args", async () => { 56 | expect(await abs("demo", process.cwd())).toBe( 57 | await join(__dirname, "/demo"), 58 | ); 59 | expect(await abs("demo", __dirname)).toBe(await join(__dirname, "/demo")); 60 | }); 61 | 62 | it("ignores the second parameter if not a string", async () => { 63 | expect(await abs("demo", 0)).toBe(await join(__dirname, "/demo")); 64 | expect(await abs("demo", 5)).toBe(await join(__dirname, "/demo")); 65 | expect(await abs("demo", true)).toBe(await join(__dirname, "/demo")); 66 | }); 67 | 68 | it("works with home", async () => { 69 | expect(await abs("~/")).toBe(await home()); 70 | expect(await abs("~/hello")).toBe((await home()) + "/hello"); 71 | expect(await abs("~/hello/")).toBe((await home()) + "/hello/"); 72 | }); 73 | }); 74 | 75 | describe("copy", () => { 76 | const src = "demo/a/readme.md"; 77 | 78 | // Create and destroy it for each test 79 | it("copy the file maintaining the original", async () => { 80 | const dst = "demo/abc.md"; 81 | await remove(dst); 82 | expect(await exists(src)).toBe(true); 83 | expect(await exists(dst)).toBe(false); 84 | 85 | const res = await copy(src, dst); 86 | expect(await exists(src)).toBe(true); 87 | expect(await exists(dst)).toBe(true); 88 | expect(res).toBe(await abs(dst)); 89 | await remove(dst); 90 | }); 91 | 92 | it("copy the file into a nested structure", async () => { 93 | const dst = "demo/copy/readme.md"; 94 | await remove(dst); 95 | 96 | expect(await exists(src)).toBe(true); 97 | expect(await exists(dst)).toBe(false); 98 | 99 | const res = await copy(src, dst); 100 | expect(await exists(src)).toBe(true); 101 | expect(await exists(dst)).toBe(true); 102 | expect(res).toBe(await abs(dst)); 103 | await remove("demo/copy"); 104 | }); 105 | }); 106 | 107 | describe("dir", () => { 108 | it("defaults to the current dir", async () => { 109 | expect(await dir()).toContain(root); 110 | }); 111 | 112 | it("returns the parent if already a path", async () => { 113 | expect(await dir("demo/a")).toContain(`files${sep}demo`); 114 | expect(await dir("demo/a")).not.toContain(`files${sep}demo${sep}a`); 115 | expect(await dir("demo/a/")).toContain(`files${sep}demo`); 116 | expect(await dir("demo/a/")).not.toContain(`files${sep}demo${sep}a`); 117 | }); 118 | 119 | it("works with swear()", async () => { 120 | expect(await dir(swear("demo/a/b/readme.md"))).toContain( 121 | `files${sep}demo${sep}a${sep}b`, 122 | ); 123 | }); 124 | 125 | it("can put the full folder path", async () => { 126 | expect(await dir("demo/a/b/readme.md")).toContain( 127 | `files${sep}demo${sep}a${sep}b`, 128 | ); 129 | expect(await dir(dir("demo/a/b/readme.md"))).not.toContain( 130 | `files${sep}demo${sep}a${sep}b`, 131 | ); 132 | expect(await dir(dir(dir("demo/a/b/readme.md")))).not.toContain( 133 | `files${sep}demo${sep}a`, 134 | ); 135 | }); 136 | 137 | it("can work with relative paths", async () => { 138 | expect(await dir("./demo/")).toBe( 139 | await abs("./demo/") 140 | .replace(/(\/|\\)$/, "") 141 | .split(sep) 142 | .slice(0, -1) 143 | .join(sep), 144 | ); 145 | }); 146 | }); 147 | 148 | describe("list", () => { 149 | it("defaults to the current folder", async () => { 150 | expect(await list()).toContain(__dirname + sep + "package.json"); 151 | }); 152 | 153 | it("works with swear()", async () => { 154 | expect(await list(swear(process.cwd()))).toContain( 155 | __dirname + sep + "package.json", 156 | ); 157 | }); 158 | 159 | it("can load the demo", async () => { 160 | const files = await list("demo", __dirname); 161 | expect(files).not.toContain(__dirname + sep + "package.json"); 162 | expect(files).toContain(__dirname + sep + `demo${sep}a`); 163 | expect(files).toContain(__dirname + sep + `demo${sep}readme.md`); 164 | }); 165 | }); 166 | 167 | describe("exists", () => { 168 | it("defaults to the current dir", async () => { 169 | expect(await exists()).toBe(true); 170 | }); 171 | 172 | it("works with swear()", async () => { 173 | expect(await exists()).toBe(true); 174 | }); 175 | 176 | it("can check the demo", async () => { 177 | expect(await exists("demo")).toBe(true); 178 | expect(await exists("demo/readme.md")).toBe(true); 179 | expect(await exists(await home("Documents"))).toBe(true); 180 | expect(await exists("aaa")).toBe(false); 181 | }); 182 | }); 183 | 184 | describe("home", () => { 185 | const homeDir = unix() ? "echo $HOME" : "echo %systemdrive%%homepath%"; 186 | it("uses the home directory", async () => { 187 | expect(await home()).toEqual(await cmd(homeDir)); 188 | // expect((await home()).slice(-1)).toBe('/'); 189 | }); 190 | 191 | it("works with swear()", async () => { 192 | expect(await home(swear(""))).toContain(await cmd(homeDir)); 193 | }); 194 | }); 195 | 196 | describe("join", () => { 197 | it("can do a simple join", async () => { 198 | expect(await join(__dirname, "demo")).toBe(path.join(__dirname, "demo")); 199 | }); 200 | 201 | it("works with swear()", async () => { 202 | expect(await join(swear(__dirname), swear("demo"))).toBe( 203 | path.join(__dirname, "demo"), 204 | ); 205 | }); 206 | }); 207 | 208 | describe("mkdir", () => { 209 | beforeEach(async () => 210 | promisify(fs.rmdir)(await abs("demo/b")).catch((err) => {}), 211 | ); 212 | afterEach(async () => 213 | promisify(fs.rmdir)(await abs("demo/b")).catch((err) => {}), 214 | ); 215 | 216 | it("create a new directory", async () => { 217 | expect(await exists("demo/b")).toBe(false); 218 | const res = await mkdir("demo/b"); 219 | expect(await exists("demo/b")).toBe(true); 220 | expect(res).toBe(await abs("demo/b")); 221 | }); 222 | 223 | it("does not throw if it already exists", async () => { 224 | expect(await exists("demo/a")).toBe(true); 225 | const res = await mkdir("demo/a"); 226 | expect(await exists("demo/a")).toBe(true); 227 | expect(res).toBe(await abs("demo/a")); 228 | }); 229 | 230 | it("creates it even if the parent does not exist", async () => { 231 | await remove("demo/c"); 232 | expect(await exists("demo/c")).toBe(false); 233 | const res = await mkdir("demo/c/d/e"); 234 | expect(await exists("demo/c/d/e")).toBe(true); 235 | expect(res).toBe(await abs("demo/c/d/e")); 236 | await remove("demo/c"); 237 | }); 238 | }); 239 | 240 | describe("move", () => { 241 | const src = "demo/move.txt"; 242 | 243 | // Create and destroy it for each test 244 | beforeEach(() => write(src, "hello")); 245 | afterEach(() => remove(src)); 246 | 247 | it("can simply move a file", async () => { 248 | const dst = "demo/move-zzz.txt"; 249 | expect(await exists(dst)).toBe(false); 250 | 251 | const res = await move(src, dst); 252 | expect(await exists(src)).toBe(false); 253 | expect(await exists(dst)).toBe(true); 254 | expect(res).toBe(await abs(dst)); 255 | await remove(dst); 256 | }); 257 | 258 | it("can work with nested folders", async () => { 259 | const dst = "demo/move/zzz.txt"; 260 | expect(await exists(dst)).toBe(false); 261 | 262 | const res = await move(src, dst); 263 | expect(await exists(src)).toBe(false); 264 | expect(await exists(dst)).toBe(true); 265 | expect(res).toBe(await abs(dst)); 266 | await remove("demo/move"); 267 | }); 268 | 269 | it("works with folders", async () => { 270 | const src = "demo/move"; 271 | const dst = "demo/moved"; 272 | 273 | await write("demo/move/test.txt", "hello"); 274 | expect(await exists(dst)).toBe(false); 275 | 276 | const res = await move(src, dst); 277 | expect(await exists(src)).toBe(false); 278 | expect(await exists(dst)).toBe(true); 279 | expect(res).toBe(await abs(dst)); 280 | await remove(dst); 281 | }); 282 | }); 283 | 284 | describe("name", () => { 285 | it("find the file name in the path", async () => { 286 | expect(await name("demo/abs.js")).toBe("abs.js"); 287 | }); 288 | 289 | it("works with swear()", async () => { 290 | expect(await name(swear("demo/abs.js"))).toBe("abs.js"); 291 | expect(await name(abs("demo/abs.js"))).toBe("abs.js"); 292 | }); 293 | 294 | it("performs well without extension", async () => { 295 | expect(await name("demo/abs")).toBe("abs"); 296 | expect(await name(abs("demo/abs"))).toBe("abs"); 297 | }); 298 | }); 299 | 300 | describe("read", () => { 301 | it("can read a markdown file", async () => { 302 | expect(await read("demo/readme.md")).toContain("# Hello!"); 303 | }); 304 | 305 | it("can webstream read it", async () => { 306 | const stream = await read("demo/readme.md", { type: "web" }); 307 | const enc = new TextDecoder("utf-8"); 308 | let str = ""; 309 | for await (const chunk of stream) { 310 | str += enc.decode(chunk); 311 | } 312 | expect(str).toContain("# Hello!"); 313 | }); 314 | 315 | it("can nodestream read it", async () => { 316 | const stream = await read("demo/readme.md", { type: "node" }); 317 | const enc = new TextDecoder("utf-8"); 318 | let str = ""; 319 | for await (const chunk of stream) { 320 | str += enc.decode(chunk); 321 | } 322 | expect(str).toContain("# Hello!"); 323 | }); 324 | 325 | it("works with swear()", async () => { 326 | expect(await read(swear("demo/readme.md"))).toContain("# Hello!"); 327 | }); 328 | 329 | it("is empty if it is not a file", async () => { 330 | expect(await read(swear("demo"))).toBe(null); 331 | }); 332 | 333 | it("can auto-parse json", async () => { 334 | expect(await read("demo/test.json", { type: "json" })).toEqual({ 335 | hello: "world", 336 | }); 337 | }); 338 | 339 | it("can json parse it", async () => { 340 | expect(await read("demo/test.json").then(JSON.parse)).toEqual({ 341 | hello: "world", 342 | }); 343 | }); 344 | 345 | it("can read a dir", async () => { 346 | expect(await list("demo/a").map(read)).toEqual([null, "# Sub-level\n"]); 347 | }); 348 | }); 349 | 350 | describe("remove", () => { 351 | it("removes a file", async () => { 352 | await write("demo/remove.md", "Hello!"); 353 | expect(await read("demo/remove.md")).toBe("Hello!"); 354 | const file = await remove("demo/remove.md"); 355 | expect(await exists("demo/remove.md")).toBe(false); 356 | expect(file).toBe(await abs("demo/remove.md")); 357 | }); 358 | 359 | it("removes a directory", async () => { 360 | await mkdir("demo/b"); 361 | expect(await exists("demo/b")).toBe(true); 362 | const file = await remove("demo/b"); 363 | expect(await exists("demo/b")).toBe(false); 364 | expect(file).toBe(await abs("demo/b")); 365 | }); 366 | 367 | it("removes a directory with files", async () => { 368 | await mkdir("demo/b"); 369 | await write("demo/b/remove.md", "Hello!"); 370 | expect(await exists("demo/b")).toBe(true); 371 | expect(await read("demo/b/remove.md")).toBe("Hello!"); 372 | const file = await remove("demo/b"); 373 | expect(await exists("demo/b")).toBe(false); 374 | expect(file).toBe(await abs("demo/b")); 375 | }); 376 | 377 | it("removes a directory with deeply nested files", async () => { 378 | await mkdir("demo/x"); 379 | await write("demo/x/remove.md", "Hello!"); 380 | await mkdir("demo/x/c"); 381 | await write("demo/x/c/remove.md", "Hello!"); 382 | expect(await exists("demo/x")).toBe(true); 383 | expect(await read("demo/x/remove.md")).toBe("Hello!"); 384 | expect(await exists("demo/x/c")).toBe(true); 385 | expect(await read("demo/x/c/remove.md")).toBe("Hello!"); 386 | const file = await remove("demo/x"); 387 | expect(await exists("demo/x")).toBe(false); 388 | expect(file).toBe(await abs("demo/x")); 389 | }); 390 | 391 | it("cannot remove the root", async () => { 392 | await expect(remove("/")).rejects.toThrow(/remove the root/); 393 | }); 394 | 395 | it("will ignore a non-existing file", async () => { 396 | expect(await exists("demo/d")).toBe(false); 397 | await expect(await remove("demo/d")).toEqual(await abs("demo/d")); 398 | }); 399 | }); 400 | 401 | describe("rename", () => { 402 | const src = "demo/rename.txt"; 403 | 404 | // Create and destroy it for each test 405 | beforeEach(() => write(src, "hello")); 406 | afterEach(() => remove(src)); 407 | 408 | it("can simply move a file", async () => { 409 | const dst = "demo/rename-zzz.txt"; 410 | expect(await exists(dst)).toBe(false); 411 | 412 | const res = await rename(src, dst); 413 | expect(await exists(src)).toBe(false); 414 | expect(await exists(dst)).toBe(true); 415 | expect(res).toBe(await abs(dst)); 416 | await remove(dst); 417 | }); 418 | 419 | it("can work with nested folders", async () => { 420 | const dst = "demo/rename/zzz.txt"; 421 | expect(await exists(dst)).toBe(false); 422 | 423 | const res = await rename(src, dst); 424 | expect(await exists(src)).toBe(false); 425 | expect(await exists(dst)).toBe(true); 426 | expect(res).toBe(await abs(dst)); 427 | await remove("demo/rename"); 428 | }); 429 | 430 | it("works with folders", async () => { 431 | const src = "demo/rename"; 432 | const dst = "demo/renamed"; 433 | 434 | await write("demo/rename/test.txt", "hello"); 435 | expect(await exists(dst)).toBe(false); 436 | 437 | const res = await rename(src, dst); 438 | expect(await exists(src)).toBe(false); 439 | expect(await exists(dst)).toBe(true); 440 | expect(res).toBe(await abs(dst)); 441 | await remove(dst); 442 | }); 443 | }); 444 | 445 | describe("stat", () => { 446 | it("defaults to the current dir", async () => { 447 | expect(await stat().isDirectory()).toBe(true); 448 | expect(await stat(process.cwd()).isDirectory()).toBe(true); 449 | expect(await stat(__dirname).isDirectory()).toBe(true); 450 | }); 451 | 452 | it("works with swear()", async () => { 453 | expect(await stat(swear(process.cwd())).isDirectory()).toBe(true); 454 | expect(await stat(swear(__dirname)).isDirectory()).toBe(true); 455 | }); 456 | 457 | it("can analyze whether a path is a directory or not", async () => { 458 | expect(await stat("demo").isDirectory()).toBe(true); 459 | expect(await stat("demo/readme.md").isDirectory()).toBe(false); 460 | expect(await stat(__filename).isDirectory()).toBe(false); 461 | }); 462 | 463 | it("can read some dates", async () => { 464 | const date = await stat("demo/readme.md").atime; 465 | expect(date.constructor.name).toBe("Date"); 466 | expect(date).toEqual(new Date(date)); 467 | }); 468 | }); 469 | 470 | describe("tmp", () => { 471 | it("works empty", async () => { 472 | if (linux()) { 473 | expect(await tmp()).toBe("/tmp"); 474 | } else if (mac()) { 475 | expect((await tmp()) + "/").toBe(await cmd("echo $TMPDIR")); 476 | } else if (windows()) { 477 | expect(await tmp()).toBe("C:\\Users\\appveyor\\AppData\\Local\\Temp\\1"); 478 | } else { 479 | console.log("Platform not supported officially"); 480 | } 481 | }); 482 | 483 | it("works with swear()", async () => { 484 | if (linux()) { 485 | expect(await tmp(swear("demo"))).toBe("/tmp/demo"); 486 | } else if (mac()) { 487 | expect(await tmp("demo")).toBe((await cmd("echo $TMPDIR")) + "demo"); 488 | } else if (windows()) { 489 | expect(await tmp("demo")).toBe( 490 | "C:\\Users\\appveyor\\AppData\\Local\\Temp\\1\\demo", 491 | ); 492 | } else { 493 | console.log("Platform not supported officially"); 494 | } 495 | }); 496 | 497 | it("works with a path", async () => { 498 | if (linux()) { 499 | expect(await tmp("demo")).toBe("/tmp/demo"); 500 | } else if (mac()) { 501 | expect(await tmp("demo")).toBe((await cmd("echo $TMPDIR")) + "demo"); 502 | } else if (windows()) { 503 | expect(await tmp("demo")).toBe( 504 | "C:\\Users\\appveyor\\AppData\\Local\\Temp\\1\\demo", 505 | ); 506 | } else { 507 | console.log("Platform not supported officially"); 508 | } 509 | }); 510 | 511 | it("can reset the doc", async () => { 512 | await tmp("demo").then(remove); 513 | expect(await tmp("demo").then(list)).toEqual([]); 514 | mkdir(await tmp("demo/a")); 515 | if (linux()) { 516 | expect(await tmp("demo").then(list)).toEqual(["/tmp/demo/a"]); 517 | } else if (mac()) { 518 | expect(await tmp("demo").then(list)).toEqual([ 519 | (await cmd("echo $TMPDIR")) + "demo/a", 520 | ]); 521 | } else if (windows()) { 522 | expect(await tmp("demo").then(list)).toEqual([ 523 | "C:\\Users\\appveyor\\AppData\\Local\\Temp\\1\\demo\\a", 524 | ]); 525 | } else { 526 | console.log("Platform not supported officially"); 527 | } 528 | await tmp("demo").then(remove).then(mkdir); 529 | expect(await tmp("demo").then(list)).toEqual([]); 530 | }); 531 | }); 532 | 533 | describe("walk", () => { 534 | it("defaults to the current directory", async () => { 535 | const dest = __dirname + sep + "package.json"; 536 | expect(await walk()).toContain(dest); 537 | expect(await walk(".")).toContain(dest); 538 | expect(await walk(process.cwd())).toContain(dest); 539 | expect(await walk(import.meta.dirname)).toContain(dest); 540 | }); 541 | 542 | it("avoid double slashes when ending on '/'", async () => { 543 | expect(await walk("./")).toContain(__dirname + sep + "package.json"); 544 | }); 545 | 546 | it("works with swear()", async () => { 547 | expect(await walk(swear(process.cwd()))).toContain( 548 | __dirname + sep + "package.json", 549 | ); 550 | }); 551 | 552 | it("is empty if it doesn not exist", async () => { 553 | expect(await walk("demo/c")).toEqual([]); 554 | }); 555 | 556 | it("can deep walk", async () => { 557 | const files = await walk("demo"); 558 | expect(files).toContain(__dirname + sep + `demo${sep}readme.md`); 559 | expect(files).toContain(__dirname + sep + `demo${sep}a${sep}readme.md`); 560 | expect(files).toContain( 561 | __dirname + sep + `demo${sep}a${sep}b${sep}readme.md`, 562 | ); 563 | }); 564 | }); 565 | 566 | describe("write", () => { 567 | beforeEach(async () => 568 | promisify(fs.unlink)(await abs("demo/deleteme.md")).catch((err) => {}), 569 | ); 570 | afterEach(async () => 571 | promisify(fs.unlink)(await abs("demo/deleteme.md")).catch((err) => {}), 572 | ); 573 | 574 | it("creates a new file", async () => { 575 | expect(await exists("demo/deleteme.md")).toBe(false); 576 | await write("demo/deleteme.md", "Hello!"); 577 | expect(await read("demo/deleteme.md")).toBe("Hello!"); 578 | expect(await exists("demo/deleteme.md")).toBe(true); 579 | }); 580 | 581 | it("creates a new file from JSON", async () => { 582 | expect(await exists("demo/deleteme.md")).toBe(false); 583 | await write("demo/deleteme.md", { hello: "world" }); 584 | expect(await read("demo/deleteme.md")).toBe('{"hello":"world"}'); 585 | expect(await exists("demo/deleteme.md")).toBe(true); 586 | }); 587 | 588 | it("creates a new file from a buffer", async () => { 589 | expect(await exists("demo/deleteme.md")).toBe(false); 590 | await write("demo/deleteme.md", Buffer.from("Hello!", "utf8")); 591 | expect(await read("demo/deleteme.md")).toBe("Hello!"); 592 | expect(await exists("demo/deleteme.md")).toBe(true); 593 | }); 594 | 595 | it("can accept a promise", async () => { 596 | expect(await exists("demo/deleteme.md")).toBe(false); 597 | await write("demo/deleteme.md", read("demo/a/readme.md")); 598 | expect(await read("demo/deleteme.md")).toBe("# Sub-level\n"); 599 | expect(await exists("demo/deleteme.md")).toBe(true); 600 | }); 601 | 602 | it("creates a new file from a Readable", async () => { 603 | expect(await exists("demo/deleteme.md")).toBe(false); 604 | const stream = new Readable(); 605 | stream.push("Hello!"); // the string you want 606 | stream.push(null); // indicates end-of-file basically - the end of the stream 607 | await write("demo/deleteme.md", stream); 608 | expect(await read("demo/deleteme.md")).toBe("Hello!"); 609 | expect(await exists("demo/deleteme.md")).toBe(true); 610 | }); 611 | 612 | it("creates a new file from a ReadableStream", async () => { 613 | expect(await exists("demo/deleteme.md")).toBe(false); 614 | const stream = new ReadableStream({ 615 | start(controller) { 616 | controller.enqueue("Hello!"); 617 | controller.close(); 618 | }, 619 | }); 620 | await write("demo/deleteme.md", stream); 621 | expect(await read("demo/deleteme.md")).toBe("Hello!"); 622 | expect(await exists("demo/deleteme.md")).toBe(true); 623 | }); 624 | 625 | it("creates a new empty file", async () => { 626 | expect(await exists("demo/deleteme.md")).toBe(false); 627 | await write("demo/deleteme.md"); 628 | expect(await exists("demo/deleteme.md")).toBe(true); 629 | }); 630 | }); 631 | 632 | describe("other tests", () => { 633 | it("can combine async inside another methods", async () => { 634 | expect(await list(tmp("demo/x"))).toEqual([]); 635 | }); 636 | 637 | it("can walk and filter and map", async () => { 638 | const files = await walk("demo") 639 | .filter((file) => /readme\.md$/.test(file)) 640 | .map(read); 641 | 642 | expect(files).toContain("# Sub-sub-level\n"); 643 | }); 644 | }); 645 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import { Readable } from "node:stream"; 2 | 3 | type fileName = string; 4 | type dirName = string; 5 | type pathName = fileName | dirName; 6 | type Types = "text" | "json" | "buffer" | "web" | "node"; 7 | type contentType = string | {} | [] | Buffer | Readable | ReadableStream; 8 | 9 | declare const abs: ( 10 | fileName?: fileName, 11 | basePath?: dirName 12 | ) => Promise; 13 | declare const copy: ( 14 | source: fileName, 15 | destination: fileName 16 | ) => Promise; 17 | declare const dir: (fileName?: fileName) => Promise; 18 | declare const exists: (fileName: fileName) => Promise; 19 | declare const home: (...paths: string[]) => Promise; 20 | declare const join: (...paths: string[]) => Promise; 21 | declare const list: (dirName?: dirName) => Promise; 22 | declare const mkdir: (dirName?: dirName) => Promise; 23 | declare const move: (src: fileName, dst: fileName) => Promise; 24 | declare const name: (fileName: fileName) => Promise; 25 | declare const read: ( 26 | fileName: fileName, 27 | { type }?: { type: Types } 28 | ) => Promise; 29 | declare const remove: (pathName?: pathName) => Promise; 30 | declare const stat: (fileName: fileName) => Promise; 31 | declare const swear: (arg: Promise | (() => Promise)) => Promise; 32 | declare const tmp: (...paths: string[]) => Promise; 33 | declare const walk: (dirName?: dirName) => Promise; 34 | declare const write: ( 35 | fileName: fileName, 36 | content: contentType 37 | ) => Promise; 38 | 39 | declare const Files: { 40 | abs: typeof abs; 41 | copy: typeof copy; 42 | dir: typeof dir; 43 | exists: typeof exists; 44 | home: typeof home; 45 | join: typeof join; 46 | list: typeof list; 47 | mkdir: typeof mkdir; 48 | move: typeof move; 49 | name: typeof name; 50 | read: typeof read; 51 | remove: typeof remove; 52 | rename: typeof move; 53 | stat: typeof stat; 54 | swear: typeof swear; 55 | tmp: typeof tmp; 56 | walk: typeof walk; 57 | write: typeof write; 58 | }; 59 | 60 | export { 61 | abs, 62 | copy, 63 | dir, 64 | exists, 65 | home, 66 | join, 67 | list, 68 | mkdir, 69 | move, 70 | name, 71 | read, 72 | remove, 73 | move as rename, 74 | stat, 75 | swear, 76 | tmp, 77 | walk, 78 | write, 79 | }; 80 | export default Files; 81 | -------------------------------------------------------------------------------- /index.types.ts: -------------------------------------------------------------------------------- 1 | import files, { 2 | abs, 3 | copy, 4 | dir, 5 | exists, 6 | home, 7 | join, 8 | list, 9 | mkdir, 10 | move, 11 | name, 12 | read, 13 | remove, 14 | rename, 15 | stat, 16 | swear, 17 | tmp, 18 | walk, 19 | write, 20 | } from "."; 21 | 22 | async function test() { 23 | await abs(); 24 | await abs("readme.md"); 25 | await abs("readme.md", "base/path/"); 26 | await copy("file1.md", "file2.md"); 27 | await dir(); 28 | await dir("readme.md"); 29 | await exists("readme.md"); 30 | await home(); 31 | await home("abc"); 32 | await home("abc", "def"); 33 | await join(); 34 | await join("abc"); 35 | await join("a", "b", "c"); 36 | await list(); 37 | await list("demo"); 38 | await mkdir(); 39 | await mkdir("hello"); 40 | await move("src", "dst"); 41 | await name("hello"); 42 | await read("file"); 43 | await remove(); 44 | await remove("folder"); 45 | await rename("src", "dst"); 46 | await stat("file"); 47 | await swear(async () => "abc"); 48 | await swear(Promise.resolve("abc")); 49 | await tmp(); 50 | await tmp("abc"); 51 | await tmp("abc", "def"); 52 | await walk(); 53 | await walk("abc"); 54 | await write("./readme.md", "hello"); 55 | await write("./readme.md", { hello: "world" }); 56 | await write("./readme.md", Buffer.from("Hello", "utf8")); 57 | } 58 | 59 | async function testBase() { 60 | await files.abs(); 61 | await files.abs("readme.md"); 62 | await files.abs("readme.md", "base/path/"); 63 | await files.copy("file1.md", "file2.md"); 64 | await files.dir(); 65 | await files.dir("readme.md"); 66 | await files.exists("readme.md"); 67 | await files.home(); 68 | await files.home("abc"); 69 | await files.home("abc", "def"); 70 | await files.join(); 71 | await files.join("abc"); 72 | await files.join("a", "b", "c"); 73 | await files.list(); 74 | await files.list("demo"); 75 | await files.mkdir(); 76 | await files.mkdir("hello"); 77 | await files.move("src", "dst"); 78 | await files.name("hello"); 79 | await files.read("file"); 80 | await files.remove(); 81 | await files.remove("folder"); 82 | await files.rename("src", "dst"); 83 | await files.stat("file"); 84 | await files.swear(async () => "abc"); 85 | await files.swear(Promise.resolve("abc")); 86 | await files.tmp(); 87 | await files.tmp("abc"); 88 | await files.tmp("abc", "def"); 89 | await files.walk(); 90 | await files.walk("abc"); 91 | await files.write("./readme.md", "hello"); 92 | await files.write("./readme.md", { hello: "world" }); 93 | await files.write("./readme.md", Buffer.from("Hello", "utf8")); 94 | } 95 | 96 | console.log(test, testBase); 97 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "files", 3 | "version": "3.0.2", 4 | "description": "Filesystem API easily usable with Promises and arrays", 5 | "author": "Francisco Presencia (https://francisco.io/)", 6 | "homepage": "https://github.com/franciscop/files#readme", 7 | "repository": "github:franciscop/files", 8 | "bugs": "https://github.com/franciscop/files/issues", 9 | "funding": "https://www.paypal.me/franciscopresencia/19", 10 | "license": "MIT", 11 | "scripts": { 12 | "start": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watchAll", 13 | "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js && npx check-dts" 14 | }, 15 | "keywords": [ 16 | "files", 17 | "fs", 18 | "filesystem", 19 | "promise", 20 | "promises", 21 | "async", 22 | "await" 23 | ], 24 | "type": "module", 25 | "main": "files.js", 26 | "types": "index.d.ts", 27 | "dependencies": { 28 | "atocha": "^2.0.0", 29 | "swear": "^1.1.2" 30 | }, 31 | "devDependencies": { 32 | "check-dts": "^0.7.2", 33 | "jest": "^29.7.0" 34 | }, 35 | "jest": { 36 | "transform": {} 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # 📁 Files [![npm install files](https://img.shields.io/badge/npm%20install-files-blue.svg)](https://www.npmjs.com/package/files) [![test badge](https://github.com/franciscop/files/workflows/tests/badge.svg)](https://github.com/franciscop/files/actions) 2 | 3 | A flexible filesystem API for Node and Bun: 4 | 5 | ```js 6 | import { read, walk } from "files"; 7 | 8 | // Find all of the readmes 9 | const readmes = await walk("demo") 10 | .filter(/\/readme\.md$/) // Works as expected! 11 | .map(read); 12 | 13 | console.log(readmes); 14 | // ['# files', '# sub-dir', ...] 15 | ``` 16 | 17 | - Works with **`'utf-8'`** by default. 18 | - Supports **Promises**, **WebStreams**, **NodeStreams** (and Buffers and JSON). 19 | - Extends promises [with `swear`](https://github.com/franciscop/swear) so you can chain operations easily. 20 | - **Absolute paths** with the root as the running script. 21 | - Ignores **the second parameter** if it's not an object so you can work with arrays better like `.map(read)`. 22 | 23 | It's an ideal library if you have to build scripts with many file and folder operations since it's made to simplify those. 24 | 25 | | function | description | 26 | | ------------------- | ------------------------------------------------------ | 27 | | [abs()](#abs) | retrieve the absolute path of the path | 28 | | [copy()](#copy) | copy a file while keeping the original | 29 | | [dir()](#dir) | get the directory of the path | 30 | | [exists()](#exists) | check whenever a file or folder exists | 31 | | [home()](#home) | get the home directory | 32 | | [join()](#join) | put several path parts together in a cross-browser way | 33 | | [list()](#list) | list all of the files and folders of the path | 34 | | [mkdir()](#mkdir) | create the specified directory | 35 | | [move()](#move) | copy a file while removing the original | 36 | | [name()](#name) | get the filename of the path | 37 | | [read()](#read) | read the file from the specified path | 38 | | [remove()](#remove) | remove a file or folder (recursively) | 39 | | [rename()](#rename) | _alias_ of [`.move()`](#move) | 40 | | [stat()](#stat) | get some information about the current file | 41 | | [swear()](#swear) | the promise wrapper that we use internally | 42 | | [tmp()](#tmp) | find the temporary directory or a folder inside | 43 | | [walk()](#walk) | recursively list all of the files and folders | 44 | | [write()](#write) | create a new file or put data into a file | 45 | 46 | ## Swear package 47 | 48 | All of the methods [follow the `swear`](https://github.com/franciscop/swear) promise extension. These are fully compatible with native promises: 49 | 50 | ```js 51 | // Using it as normal promises 52 | const all = await list("demo"); 53 | const devFiles = all.filter((file) => !/node_modules/.test(file)); 54 | // ['a.js', 'b.js', ...] 55 | ``` 56 | 57 | With the swear workflow, you can apply operations on the promise that will be queued and run on the eventual value: 58 | 59 | ```js 60 | const devFiles = await list("demo").filter( 61 | (file) => !/node_modules/.test(file) 62 | ); 63 | // ['a.js', 'b.js', ...] 64 | ``` 65 | 66 | See how we applied the `.filter()` straight into the output of `list()`. Then we have to await for the whole thing to resolve since `list()` is async. If this seems a bit confusing, read along the examples and try it yourself. 67 | 68 | For convenience, you can import and use `swear`: 69 | 70 | ```js 71 | import files, { swear } from "files"; 72 | 73 | files.swear(); 74 | swear(); 75 | ``` 76 | 77 | ## abs() 78 | 79 | ```js 80 | abs(path:string, root=process.cwd():string) => :string 81 | ``` 82 | 83 | Retrieve the absolute path of the passed argument relative of the directory running the script: 84 | 85 | ```js 86 | // cd ~/me/projects/files/ && node index.js 87 | 88 | console.log(await abs("demo")); 89 | // /home/me/projects/files/demo 90 | 91 | console.log(await abs("../../Documents")); 92 | // /home/me/Documents 93 | ``` 94 | 95 | It will return the same string if the path is already absolute. 96 | 97 | You can pass a second parameter to specify any base directory different from the executing environment: 98 | 99 | ```jsx 100 | // cd ~/me/projects/files && node ./demo/abs.js ⚠️ 101 | 102 | // default; Relative to the place where the script is run 103 | console.log(await abs("demo")); 104 | // /home/me/projects/files/demo 105 | 106 | // default; relative to the console location where the script is run 107 | console.log(await abs("demo", process.cwd())); 108 | // /home/me/projects/files/demo 109 | 110 | // relative to the current directory (./demo) 111 | console.log(await abs("demo", import.meta.url)); 112 | // /home/me/projects/files/demo/demo 113 | ``` 114 | 115 | If the second parameter is undefined, or if it's _not a string_, it will be completely ignored and the default of the current running dir will be used. This is great for looping on arrays or similar: 116 | 117 | ```js 118 | console.log(await list("demo").map(abs)); 119 | // [ '/home/me/projects/files/a', '/home/me/projects/files/b' ] 120 | ``` 121 | 122 | ## copy() 123 | 124 | ```js 125 | copy(source:string, destination:string) => :string 126 | ``` 127 | 128 | Copy the source file into the destination file, which can be in the same folder or in any other. It maintains the original. Returns the resulting file: 129 | 130 | ```js 131 | // cd ~/projects/files && node index.js 132 | 133 | console.log(await copy("demo/README.md", "demo/readme.md")); 134 | // /home/me/files/demo/readme.md 135 | 136 | console.log(await copy("demo/readme.md", "demo/docs/readme.md")); 137 | // /home/me/files/demo/docs/readme.md 138 | ``` 139 | 140 | Related methods: 141 | 142 | - [move()](#move): copy a file while removing the original 143 | 144 | ## dir() 145 | 146 | ```js 147 | dir(path:string) => :string 148 | ``` 149 | 150 | Returns the directory of the passed path: 151 | 152 | ```js 153 | console.log(await dir("~/hello/world.js")); 154 | // /home/me/hello 155 | ``` 156 | 157 | If the path is already a directory, it returns the one that contains it; its parent: 158 | 159 | ```js 160 | console.log(await dir("~/hello/")); 161 | // /home/me 162 | ``` 163 | 164 | ## exists() 165 | 166 | ```js 167 | exists(path:string) => :boolean 168 | ``` 169 | 170 | Check whenever a file or folder exists: 171 | 172 | ```js 173 | console.log(await exists("readme.md")); 174 | // true 175 | 176 | console.log(await exists("non-existing.md")); 177 | // false 178 | ``` 179 | 180 | This _cannot_ be used with `.filter()`, since in JS `.filter()` is sync and doesn't expect an array of promises to be returned. 181 | 182 | To filter based on whether it exists or not, extend it to an array of promises, then filter that asynchronously and finally retrieve the original file: 183 | 184 | ```js 185 | const keeper = (file) => exists(file).then((keep) => keep && file); 186 | const existing = await Promise.all(["a.md", "b.md"].map(keeper)); 187 | console.log(existing.filter((file) => file)); 188 | ``` 189 | 190 | > **Swear interface**: you can use `swear` to make your life a bit easier with its `.filter()`, which accepts promises: 191 | 192 | ```js 193 | import { swear } from "files"; 194 | console.log(await swear(["a.md", "b.md"]).filter(exists)); 195 | ``` 196 | 197 | ## home() 198 | 199 | ```js 200 | home(arg1:string, arg2:string, ...) => :string 201 | ``` 202 | 203 | Find the home directory if called without arguments, or the specified directory inside the home folder as specified in the arguments. 204 | 205 | ```js 206 | console.log(await home()); 207 | // /home/me/ 208 | 209 | console.log(await home("demo")); 210 | // /home/me/demo/ 211 | 212 | console.log(await home("demo", "a")); 213 | // /home/me/demo/a/ 214 | ``` 215 | 216 | It will create the specified folder if it does not exist yet. 217 | 218 | To make sure the new folder is empty, you can call `remove()` and `mkdir()` consecutively: 219 | 220 | ```js 221 | const dir = await home("demo").then(remove).then(mkdir); 222 | console.log(dir); 223 | // /home/me/demo/ (empty) 224 | ``` 225 | 226 | ## join() 227 | 228 | ```js 229 | join(arg1:string, arg2:string, ...) => :string 230 | ``` 231 | 232 | Put several path segments together in a cross-browser way and return the absolute path: 233 | 234 | ```js 235 | console.log(await join("demo", "a")); 236 | // /home/me/projects/files/demo/a 237 | ``` 238 | 239 | ## list() 240 | 241 | ```js 242 | list(path=process.cwd():string) => :Array(:string) 243 | ``` 244 | 245 | Get all of the files and folders of the specified directory into an array: 246 | 247 | ```js 248 | console.log(await list()); 249 | // ['/home/me/files/node_modules', '/home/me/files/demo/abs.js', ...] 250 | ``` 251 | 252 | To scan any other directory specify it as a parameter: 253 | 254 | ```js 255 | console.log(await list("demo")); 256 | // ['/home/me/files/demo/a', '/home/me/files/demo/abs.js', ...] 257 | ``` 258 | 259 | > **Swear interface**: you can iterate and treat the returned value as a normal array, except that you'll have to `await` at some point for the whole thing. 260 | 261 | ```js 262 | // Retrieve all of the files and filter for javascript 263 | console.log(await list().filter(/\.js$/)); 264 | // ['/home/me/projects/files/files.js', '/home/me/projects/files/files.test.js', ...] 265 | ``` 266 | 267 | Related methods: 268 | 269 | - [`walk()`](#walk) recursively list all of the files in a directory. Does not output directories. 270 | 271 | ## mkdir() 272 | 273 | ```js 274 | mkdir(path:string) => :string 275 | ``` 276 | 277 | Create the specified directory. If it already exists, do nothing. Returns the directory that was created. 278 | 279 | ```js 280 | // cd ~/projects/files && node index.js 281 | 282 | console.log(await mkdir("demo/b")); 283 | // /home/me/files/demo/b 284 | ``` 285 | 286 | Related methods: 287 | 288 | - [exists()](#exists): check whether a directory exists. 289 | - [remove()](#remove): remove a folder or file. 290 | - [list()](#list): read all the contents of a directory. 291 | 292 | ## move() 293 | 294 | ```js 295 | move(source:string, destination:string) => :string 296 | ``` 297 | 298 | Put the source file into the destination file. This can be just a rename or actually changing the folder where the file lives. Returns the resulting file: 299 | 300 | ```js 301 | // cd ~/projects/files && node index.js 302 | 303 | console.log(await move("demo/README.md", "demo/readme.md")); 304 | // /home/me/files/demo/readme.md 305 | 306 | console.log(await move("demo/readme.md", "demo/docs/readme.md")); 307 | // /home/me/files/demo/docs/readme.md 308 | ``` 309 | 310 | Related methods: 311 | 312 | - [copy()](#copy): copy a file while keeping the original 313 | 314 | ## name() 315 | 316 | ```js 317 | name(path:string) => :string 318 | ``` 319 | 320 | Get the filename of the passed path: 321 | 322 | ```js 323 | console.log(await name("~/hello/world.js")); 324 | // world.js 325 | ``` 326 | 327 | ## read() 328 | 329 | ```js 330 | read(path:string, { type: 'text' }) => :string 331 | ``` 332 | 333 | Read the specified file contents into a string: 334 | 335 | ```js 336 | console.log(await read("readme.md")); 337 | // # files ... 338 | 339 | console.log(await read("data.json").then(JSON.parse)); 340 | // { hello: "world" } 341 | ``` 342 | 343 | You can specify other types: 344 | 345 | - `text` (default): get the file as a plain string, useful for e.g. `.csv`, `.txt`, `.md`, `.html`, etc. 346 | - `raw` (Buffer): put the whole file into a Buffer, which is useful for raw file manipulation (like with [sharp](https://sharp.pixelplumbing.com/)) or binary delivery. 347 | - `json` (JSON.parse): reads the data as a string, and parses it with `JSON.parse()`. 348 | - `web` (WebStream): create a new WebStream, which allows for further processing with e.g. `fetch()`. 349 | - `node` (NodeStreams): create a traditional Node.js Stream, which allows for chunked processing with other Node.js utilities. 350 | 351 | ```js 352 | console.log(await read("data.json", { type: 'json' }); 353 | // { hello: "world" } 354 | 355 | const stream = await read("data.json", { type: 'web' }); 356 | stream.pipeTo(...); 357 | ``` 358 | 359 | File reads are relative as always to the executing script. It expects a single argument so you can easily put an array on it: 360 | 361 | ```js 362 | // Read two files manually 363 | console.log(await Promise.all(["a.md", "b.md"].map(read))); 364 | // ['# A', '# B'] 365 | 366 | // Read all markdown files in all subfolders (using Swear interface): 367 | console.log(await walk("demo").filter(/\.md$/).map(read)); 368 | // ['# A', '# B', ...] 369 | ``` 370 | 371 | It also follows the [`swear` specification](#swear-package), so you can chain normal string operations on it: 372 | 373 | ```js 374 | // Find all the secondary headers in a markdown file 375 | console.log(await read('readme.md').split('\n').filter(/^##\s+/)); 376 | // ['## abs()', '## dir()', ...] 377 | 378 | // Read all markdown files in all subfolders 379 | console.log(await walk().filter(/\.md$/).map(read))); 380 | // ['# A', '# B', ...] 381 | ``` 382 | 383 | ## remove() 384 | 385 | ```js 386 | remove(path:string) => :string 387 | ``` 388 | 389 | Remove a file or folder (recursively) and return the absolute path that was removed 390 | 391 | ```js 392 | console.log(await remove("readme.md")); 393 | // /home/me/projects/readme.md 394 | 395 | console.log(await remove("~/old-project")); 396 | // /home/me/old-project 397 | ``` 398 | 399 | Please be careful when using this, since there is no way of recovering deleted files. 400 | 401 | ## rename() 402 | 403 | > _alias_ of [`move()`](#move). 404 | 405 | ## stat() 406 | 407 | ```ts 408 | stat(path:string) => :Object({ 409 | isDirectory:fn, 410 | isFile:fn, 411 | atime:string, 412 | mtime:string, 413 | ... 414 | }) 415 | ``` 416 | 417 | Get some information about the current path: 418 | 419 | ```js 420 | console.log(await stat().isDirectory()); 421 | // true (the current directory) 422 | 423 | console.log(await stat("readme.md").isFile()); 424 | // true 425 | 426 | console.log(await stat("readme.md").atime); 427 | // 2018-08-27T23:42:16.206Z 428 | ``` 429 | 430 | ## swear() 431 | 432 | ```js 433 | swear(arg:any) => :any 434 | ``` 435 | 436 | This [is **the `swear` package**](https://www.npmjs.com/package/swear) exported here for convenience. It allows you to chain promises using the underlying value methods for convenience. 437 | 438 | Example: reading some specific files if they exist **without** swear: 439 | 440 | ```js 441 | const keeper = (file) => exists(file).then((keep) => keep && file); 442 | const existing = await Promise.all(["a.md", "b.md"].map(keeper)); 443 | console.log(existing.filter(Boolean).map(read)); 444 | ``` 445 | 446 | Reading the same files if they exist **with** swear: 447 | 448 | ```js 449 | console.log(await swear(["a.md", "b.md"]).filter(exists).map(read)); 450 | ``` 451 | 452 | ## tmp() 453 | 454 | ```js 455 | tmp(arg1:string) => :string 456 | ``` 457 | 458 | Find the temporary directory. Find a subfolder if an argument is passed: 459 | 460 | ```js 461 | console.log(await tmp()); 462 | // /tmp/ 463 | 464 | console.log(await tmp("demo")); 465 | // /tmp/demo/ 466 | 467 | console.log(await tmp("demo", "a")); 468 | // /tmp/demo/a/ 469 | ``` 470 | 471 | It will create the specified folder if it does not exist yet. 472 | 473 | To reuse a temp folder and make sure it's empty on each usage, you can call `remove()` and `mkdir()` consecutively: 474 | 475 | ```js 476 | const dir = await tmp("demo").then(remove).then(mkdir); 477 | console.log(dir); 478 | // /tmp/demo/ (empty) 479 | ``` 480 | 481 | ## walk() 482 | 483 | ```js 484 | walk(path:string) => :Array(:string) 485 | ``` 486 | 487 | Recursively list all of the files from the specified folder: 488 | 489 | ```js 490 | // Retrieve all files inside './demo' 491 | await walk("demo"); 492 | // ['/home/me/projects/files/demo/readme.md', '/home/me/projects/files/demo/a/readme.md', ...] 493 | ``` 494 | 495 | It will _not_ return directories. You can then use `filter` to filter e.g. by filename: 496 | 497 | ```js 498 | // Retrieve the content of all markdown files inside demo 499 | await walk("demo") 500 | .filter(/\.md$/) 501 | .map(read); 502 | // ['# Readme A', '# Me also', ...] 503 | ``` 504 | 505 | Note: you could also apply the regex straight in the filter since we are using [swear](#swear). 506 | 507 | ## write() 508 | 509 | ```js 510 | write(path:string, content:any) => :string 511 | ``` 512 | 513 | Create a new file or put data into a file that already exists. Returns the path of the file: 514 | 515 | ```js 516 | // Write to a file and then read its contents 517 | const path = await write("demo.txt", "Hello!"); 518 | 519 | // Write it as a webstream 520 | const res = await fetch(); 521 | await write("requested.txt", res.body); 522 | ``` 523 | 524 | You can pass simple text to write it to the file, which will automatically be written as UTF-8: 525 | 526 | ```js 527 | await write('demo.txt', 'Hello world'); 528 | ```` 529 | 530 | You can also pass a Readable WebStream or a Node.js Stream: 531 | 532 | ```js 533 | // Write it as a webstream 534 | const stream = new ReadableStream({ 535 | start(controller) { 536 | controller.enqueue("Hello!"); 537 | controller.close(); 538 | }, 539 | }); 540 | 541 | await write("webstream.txt", stream); 542 | 543 | // Write it as a node stream: 544 | const stream = new Readable(); 545 | stream.push("Hello!"); // the string you want 546 | stream.push(null); // indicates end-of-file basically - the end of the stream 547 | 548 | await write("demo/deleteme.md", stream); 549 | ``` 550 | 551 | If the folder of the target file doesn't exist it will create it. 552 | 553 | It accepts multiple types for the contect, specifically it accepts `string`, `Buffer`, `ReadableStream` (WebStreams), `Readable` (NodeStreams) or a serializable object/array that will be converted to JSON. 554 | --------------------------------------------------------------------------------