├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .mocharc.yml ├── LICENSE ├── README.md ├── lib └── fs.ts ├── package.json ├── test ├── .eslintrc.json └── index.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.js] 4 | indent_size = 2 5 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | tmp/ 4 | dist/ -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": "hexo/ts.js", 4 | "parserOptions": { 5 | "sourceType": "module", 6 | "ecmaVersion": 2020 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: monthly 7 | ignore: 8 | - dependency-name: "@types/node" 9 | - dependency-name: "*" 10 | update-types: ["version-update:semver-patch", "version-update:semver-minor"] 11 | open-pull-requests-limit: 20 12 | - package-ecosystem: github-actions 13 | directory: "/" 14 | schedule: 15 | interval: daily 16 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - "master" 7 | pull_request: 8 | 9 | env: 10 | default_node_version: 18 11 | 12 | jobs: 13 | test: 14 | name: Test 15 | needs: build 16 | runs-on: ${{ matrix.os }} 17 | strategy: 18 | matrix: 19 | os: [ubuntu-latest, windows-latest, macos-latest] 20 | node-version: ["18", "20", "22"] 21 | fail-fast: false 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: Setup Node.js ${{ matrix.node-version }} and Cache 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: npm 29 | cache-dependency-path: "package.json" 30 | 31 | - name: Install Dependencies 32 | run: npm install 33 | - name: Test 34 | run: npm run test 35 | 36 | coverage: 37 | name: Coverage 38 | needs: build 39 | runs-on: ubuntu-latest 40 | steps: 41 | - uses: actions/checkout@v4 42 | - name: Setup Node.js ${{env.default_node_version}} and Cache 43 | uses: actions/setup-node@v4 44 | with: 45 | node-version: ${{env.default_node_version}} 46 | cache: npm 47 | cache-dependency-path: "package.json" 48 | 49 | - name: Install Dependencies 50 | run: npm install 51 | - name: Coverage 52 | run: npm run test-cov 53 | - name: Coveralls 54 | uses: coverallsapp/github-action@v2 55 | with: 56 | github-token: ${{ secrets.github_token }} 57 | 58 | build: 59 | name: Build 60 | runs-on: ubuntu-latest 61 | steps: 62 | - uses: actions/checkout@v4 63 | - name: Setup Node.js ${{env.default_node_version}} and Cache 64 | uses: actions/setup-node@v4 65 | with: 66 | node-version: ${{env.default_node_version}} 67 | cache: npm 68 | cache-dependency-path: "package.json" 69 | 70 | - name: Install Dependencies 71 | run: npm install 72 | - name: Build 73 | run: npm run build 74 | 75 | lint: 76 | name: Lint 77 | runs-on: ubuntu-latest 78 | steps: 79 | - uses: actions/checkout@v4 80 | - name: Setup Node.js ${{env.default_node_version}} and Cache 81 | uses: actions/setup-node@v4 82 | with: 83 | node-version: ${{env.default_node_version}} 84 | cache: npm 85 | cache-dependency-path: "package.json" 86 | 87 | - name: Install Dependencies 88 | run: npm install 89 | - name: Lint 90 | run: npm run eslint 91 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | tmp/ 4 | *.log 5 | .idea/ 6 | coverage/ 7 | package-lock.json 8 | dist 9 | -------------------------------------------------------------------------------- /.mocharc.yml: -------------------------------------------------------------------------------- 1 | reporter: spec 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Tommy Chen 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hexo-fs 2 | 3 | [![CI](https://github.com/hexojs/hexo-fs/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/hexojs/hexo-fs/actions/workflows/ci.yml) 4 | [![NPM version](https://badge.fury.io/js/hexo-fs.svg)](https://www.npmjs.com/package/hexo-fs) 5 | [![Coverage Status](https://coveralls.io/repos/github/hexojs/hexo-fs/badge.svg)](https://coveralls.io/github/hexojs/hexo-fs) 6 | 7 | File system module for [Hexo]. 8 | 9 | ## Features 10 | 11 | - Support for both Promise and callback interface. 12 | - Use [graceful-fs] to avoid EMFILE error and various improvements. 13 | - Use [chokidar] for consistent file watching. 14 | 15 | ## Installation 16 | 17 | ``` bash 18 | $ npm install hexo-fs --save 19 | ``` 20 | 21 | ## Usage 22 | 23 | ``` js 24 | const fs = require('hexo-fs'); 25 | ``` 26 | 27 | > Some methods in the original fs module are not listed below, but they're available in hexo-fs. 28 | 29 | ### exists(path) 30 | 31 | Test whether or not the given `path` exists by checking with the file system. 32 | 33 | ### existsSync(path) 34 | 35 | Synchronous version of `fs.exists`. 36 | 37 | ### mkdirs(path) 38 | 39 | Creates a directory and its parent directories if they does not exist. 40 | 41 | ### mkdirsSync(path) 42 | 43 | Synchronous version of `fs.mkdirs`. 44 | 45 | ### writeFile(path, data, [options]) 46 | 47 | Writes data to a file. 48 | 49 | Option | Description | Default 50 | --- | --- | --- 51 | `encoding` | File encoding | utf8 52 | `mode` | Mode | 438 (0666 in octal) 53 | `flag` | Flag | w 54 | 55 | ### writeFileSync(path, data, [options]) 56 | 57 | Synchronous version of `fs.writeFile`. 58 | 59 | ### appendFile(path, data, [options]) 60 | 61 | Appends data to a file. 62 | 63 | Option | Description | Default 64 | --- | --- | --- 65 | `encoding` | File encoding | utf8 66 | `mode` | Mode | 438 (0666 in octal) 67 | `flag` | Flag | w 68 | 69 | ### appendFileSync(path, data, [options]) 70 | 71 | Synchronous version of `fs.appendFile`. 72 | 73 | ### copyFile(src, dest, [callback]) 74 | 75 | Copies a file from `src` to `dest`. 76 | 77 | ### copyDir(src, dest, [options]) 78 | 79 | Copies a directory from `src` to `dest`. It returns an array of copied files. 80 | 81 | Option | Description | Default 82 | --- | --- | --- 83 | `ignoreHidden` | Ignore hidden files | true 84 | `ignorePattern` | Ignore files which pass the regular expression | 85 | 86 | ### listDir(path, [options]) 87 | 88 | Lists files in a directory. 89 | 90 | Option | Description | Default 91 | --- | --- | --- 92 | `ignoreHidden` | Ignore hidden files | true 93 | `ignorePattern` | Ignore files which pass the regular expression | 94 | 95 | ### listDirSync(path, [options]) 96 | 97 | Synchronous version of `fs.listDir`. 98 | 99 | ### readFile(path, [options]) 100 | 101 | Reads the entire contents of a file. 102 | 103 | Option | Description | Default 104 | --- | --- | --- 105 | `encoding` | File encoding | utf8 106 | `flag` | Flag | r 107 | `escape` | Escape UTF BOM and line ending in the content | true 108 | 109 | ### readFileSync(path, [options]) 110 | 111 | Synchronous version of `fs.readFile`. 112 | 113 | ### emptyDir(path, [options]) 114 | 115 | Deletes all files in a directory. It returns an array of deleted files. 116 | 117 | Option | Description | Default 118 | --- | --- | --- 119 | `ignoreHidden` | Ignore hidden files | true 120 | `ignorePattern` | Ignore files which pass the regular expression | 121 | `exclude` | Ignore files in the array | 122 | 123 | ### emptyDirSync(path, [options]) 124 | 125 | Synchronous version of `fs.emptyDir`. 126 | 127 | ### rmdir(path) 128 | 129 | Removes a directory and all files in it. 130 | 131 | ### rmdirSync(path) 132 | 133 | Synchronous version of `fs.rmdir`. 134 | 135 | ### watch(path, [options]) 136 | 137 | Watches changes of a file or a directory. 138 | 139 | See [Chokidar API](https://github.com/paulmillr/chokidar#api) for more info. 140 | 141 | ### ensurePath(path) 142 | 143 | Ensures the given path is available to use or appends a number to the path. 144 | 145 | ### ensurePathSync(path) 146 | 147 | Synchronous version of `fs.ensurePath`. 148 | 149 | ### ensureWriteStream(path, [options]) 150 | 151 | Creates the parent directories if they does not exist and returns a writable stream. 152 | 153 | ### ensureWriteStreamSync(path, [options]) 154 | 155 | Synchronous version of `fs.ensureWriteStream`. 156 | 157 | ## License 158 | 159 | MIT 160 | 161 | [graceful-fs]: https://github.com/isaacs/node-graceful-fs 162 | [Hexo]: https://hexo.io/ 163 | [chokidar]: https://github.com/paulmillr/chokidar -------------------------------------------------------------------------------- /lib/fs.ts: -------------------------------------------------------------------------------- 1 | import type { Dirent, WriteFileOptions } from 'fs'; 2 | import chokidar, { ChokidarOptions } from 'chokidar'; 3 | import BlueBirdPromise from 'bluebird'; 4 | import { dirname, join, extname, basename } from 'path'; 5 | import { escapeRegExp } from 'hexo-util'; 6 | 7 | import fs from 'graceful-fs'; 8 | import type { Stream } from 'stream'; 9 | 10 | const fsPromises = fs.promises; 11 | 12 | const rEOL = /\r\n/g; 13 | 14 | export function exists(path: string) { 15 | if (!path) throw new TypeError('path is required!'); 16 | const promise = fsPromises.access(path).then(() => true, error => { 17 | if (error.code !== 'ENOENT') throw error; 18 | return false; 19 | }); 20 | 21 | return BlueBirdPromise.resolve(promise); 22 | } 23 | 24 | export function existsSync(path: string) { 25 | if (!path) throw new TypeError('path is required!'); 26 | 27 | try { 28 | fs.accessSync(path); 29 | } catch (err) { 30 | if (err.code !== 'ENOENT') throw err; 31 | return false; 32 | } 33 | 34 | return true; 35 | } 36 | 37 | export function mkdirs(path: string) { 38 | if (!path) throw new TypeError('path is required!'); 39 | 40 | return BlueBirdPromise.resolve(fsPromises.mkdir(path, { recursive: true })); 41 | } 42 | 43 | export function mkdirsSync(path: string) { 44 | if (!path) throw new TypeError('path is required!'); 45 | 46 | fs.mkdirSync(path, { recursive: true }); 47 | } 48 | 49 | function checkParent(path: string) { 50 | return BlueBirdPromise.resolve(fsPromises.mkdir(dirname(path), { recursive: true })); 51 | } 52 | 53 | export function writeFile( 54 | path: string, 55 | data?: string | NodeJS.ArrayBufferView | Iterable | AsyncIterable | Stream, 56 | options?: WriteFileOptions 57 | ) { 58 | if (!path) throw new TypeError('path is required!'); 59 | 60 | if (!data) data = ''; 61 | 62 | return checkParent(path) 63 | .then(() => fsPromises.writeFile(path, data, options)); 64 | } 65 | 66 | 67 | export function writeFileSync(path: string, data: string | NodeJS.ArrayBufferView, options?: WriteFileOptions) { 68 | if (!path) throw new TypeError('path is required!'); 69 | 70 | fs.mkdirSync(dirname(path), { recursive: true }); 71 | fs.writeFileSync(path, data, options); 72 | } 73 | 74 | export function appendFile( 75 | path: string, 76 | data: string | Uint8Array, 77 | options?: WriteFileOptions) { 78 | if (!path) throw new TypeError('path is required!'); 79 | 80 | return checkParent(path) 81 | .then(() => fsPromises.appendFile(path, data, options)); 82 | } 83 | 84 | export function appendFileSync(path: string, data: string | Uint8Array, options?: WriteFileOptions) { 85 | if (!path) throw new TypeError('path is required!'); 86 | 87 | fs.mkdirSync(dirname(path), { recursive: true }); 88 | fs.appendFileSync(path, data, options); 89 | } 90 | 91 | export function copyFile( 92 | src: string, dest: string, flags?: number) { 93 | if (!src) throw new TypeError('src is required!'); 94 | if (!dest) throw new TypeError('dest is required!'); 95 | 96 | return checkParent(dest) 97 | .then(() => fsPromises.copyFile(src, dest, flags)); 98 | } 99 | 100 | const trueFn = () => true as const; 101 | 102 | function ignoreHiddenFiles(ignore?: boolean) { 103 | if (!ignore) return trueFn; 104 | 105 | return ({ name }) => !name.startsWith('.'); 106 | } 107 | 108 | function ignoreFilesRegex(regex?: RegExp) { 109 | if (!regex) return trueFn; 110 | 111 | return ({ name }) => !regex.test(name); 112 | } 113 | 114 | function ignoreExcludeFiles(arr: string[], parent: string) { 115 | if (!arr || !arr.length) return trueFn; 116 | 117 | const set = new Set(arr); 118 | 119 | return ({ name }) => !set.has(join(parent, name)); 120 | } 121 | 122 | export type ReadDirOptions = { 123 | encoding?: BufferEncoding | null 124 | withFileTypes?: false 125 | ignoreHidden?: boolean 126 | ignorePattern?: RegExp 127 | } 128 | 129 | async function _readAndFilterDir( 130 | path: string, options: ReadDirOptions = {}): Promise { 131 | const { ignoreHidden = true, ignorePattern } = options; 132 | return (await fsPromises.readdir(path, { ...options, withFileTypes: true })) 133 | .filter(ignoreHiddenFiles(ignoreHidden)) 134 | .filter(ignoreFilesRegex(ignorePattern)); 135 | } 136 | 137 | function _readAndFilterDirSync(path: string, options?: ReadDirOptions) { 138 | const { ignoreHidden = true, ignorePattern } = options; 139 | return fs.readdirSync(path, { ...options, withFileTypes: true }) 140 | .filter(ignoreHiddenFiles(ignoreHidden)) 141 | .filter(ignoreFilesRegex(ignorePattern)); 142 | } 143 | 144 | async function _copyDirWalker( 145 | src: string, dest: string, results: string[], parent: string, 146 | options: ReadDirOptions) { 147 | return BlueBirdPromise.map(_readAndFilterDir(src, options), item => { 148 | const childSrc = join(src, item.name); 149 | const childDest = join(dest, item.name); 150 | const currentPath = join(parent, item.name); 151 | 152 | if (item.isDirectory()) { 153 | return _copyDirWalker(childSrc, childDest, results, currentPath, options); 154 | } 155 | results.push(currentPath); 156 | return copyFile(childSrc, childDest, 0); 157 | }); 158 | } 159 | 160 | export function copyDir( 161 | src: string, dest: string, options: ReadDirOptions = {}) { 162 | if (!src) throw new TypeError('src is required!'); 163 | if (!dest) throw new TypeError('dest is required!'); 164 | 165 | const results: string[] = []; 166 | 167 | return checkParent(dest) 168 | .then(() => _copyDirWalker(src, dest, results, '', options)) 169 | .return(results); 170 | } 171 | 172 | async function _listDirWalker( 173 | path: string, results: string[], parent?: string, options?: ReadDirOptions) { 174 | const promises = []; 175 | 176 | for (const item of await _readAndFilterDir(path, options)) { 177 | const currentPath = join(parent, item.name); 178 | 179 | if (item.isDirectory()) { 180 | promises.push( 181 | _listDirWalker(join(path, item.name), results, currentPath, options)); 182 | } else { 183 | results.push(currentPath); 184 | } 185 | } 186 | 187 | await BlueBirdPromise.all(promises); 188 | } 189 | 190 | export function listDir( 191 | path: string, options: ReadDirOptions = {}) { 192 | if (!path) throw new TypeError('path is required!'); 193 | 194 | const results: string[] = []; 195 | 196 | return BlueBirdPromise.resolve(_listDirWalker(path, results, '', options)) 197 | .return(results); 198 | } 199 | 200 | function _listDirSyncWalker( 201 | path: string, results: string[], parent: string, options: ReadDirOptions) { 202 | for (const item of _readAndFilterDirSync(path, options)) { 203 | const currentPath = join(parent, item.name); 204 | 205 | if (item.isDirectory()) { 206 | _listDirSyncWalker(join(path, item.name), results, currentPath, options); 207 | } else { 208 | results.push(currentPath); 209 | } 210 | } 211 | } 212 | 213 | export function listDirSync(path: string, options: ReadDirOptions = {}) { 214 | if (!path) throw new TypeError('path is required!'); 215 | const results: string[] = []; 216 | 217 | _listDirSyncWalker(path, results, '', options); 218 | 219 | return results; 220 | } 221 | 222 | export function escapeEOL(str: string) { 223 | return str.replace(rEOL, '\n'); 224 | } 225 | 226 | export function escapeBOM(str: string) { 227 | return str.charCodeAt(0) === 0xFEFF ? str.substring(1) : str; 228 | } 229 | 230 | export function escapeFileContent(content: string) { 231 | return escapeBOM(escapeEOL(content)); 232 | } 233 | 234 | export type ReadFileOptions = { encoding?: BufferEncoding | null; flag?: string; escape?: string } 235 | 236 | async function _readFile(path: string, options: ReadFileOptions | null = {}) { 237 | if (!Object.prototype.hasOwnProperty.call(options, 238 | 'encoding')) options.encoding = 'utf8'; 239 | 240 | const content = await fsPromises.readFile(path, options); 241 | 242 | if (options.escape == null || options.escape) { 243 | return escapeFileContent(content as string); 244 | } 245 | 246 | return content; 247 | } 248 | 249 | export function readFile(path: string): BlueBirdPromise; 250 | export function readFile(path: string, options?: ReadFileOptions | null): BlueBirdPromise; 251 | export function readFile( 252 | path: string, options?: ReadFileOptions | null) { 253 | if (!path) throw new TypeError('path is required!'); 254 | 255 | return BlueBirdPromise.resolve(_readFile(path, options)); 256 | } 257 | 258 | export function readFileSync(path: string): string; 259 | export function readFileSync(path: string, options?: ReadFileOptions): string | Buffer; 260 | export function readFileSync(path: string, options: ReadFileOptions = {}) { 261 | if (!path) throw new TypeError('path is required!'); 262 | 263 | if (!Object.prototype.hasOwnProperty.call(options, 264 | 'encoding')) options.encoding = 'utf8'; 265 | 266 | const content = fs.readFileSync(path, options); 267 | 268 | if (options.escape == null || options.escape) { 269 | return escapeFileContent(content as string); 270 | } 271 | 272 | return content; 273 | } 274 | 275 | async function _emptyDir( 276 | path: string, parent?: string, 277 | options?: ReadDirOptions & { exclude?: string[] }) { 278 | const entries = (await _readAndFilterDir(path, options)).filter( 279 | ignoreExcludeFiles(options.exclude, parent)); 280 | const results: string[] = []; 281 | 282 | await BlueBirdPromise.map(entries, (item: Dirent) => { 283 | const fullPath = join(path, item.name); 284 | const currentPath = join(parent, item.name); 285 | 286 | if (item.isDirectory()) { 287 | return _emptyDir(fullPath, currentPath, options).then(async files => { 288 | results.push(...files); 289 | if (!(await fsPromises.readdir(fullPath)).length) { 290 | return fsPromises.rmdir(fullPath); 291 | } 292 | }); 293 | } 294 | results.push(currentPath); 295 | return fsPromises.unlink(fullPath); 296 | }); 297 | 298 | return results; 299 | } 300 | 301 | export function emptyDir( 302 | path: string, options: ReadDirOptions & { exclude?: string[] } = {}) { 303 | if (!path) throw new TypeError('path is required!'); 304 | 305 | return BlueBirdPromise.resolve(_emptyDir(path, '', options)); 306 | } 307 | 308 | function _emptyDirSync( 309 | path: string, options: ReadDirOptions & { exclude?: string[] }, 310 | parent?: string) { 311 | const entries = _readAndFilterDirSync(path, options) 312 | .filter(ignoreExcludeFiles(options.exclude, parent)); 313 | 314 | const results: string[] = []; 315 | 316 | for (const item of entries) { 317 | const childPath = join(path, item.name); 318 | const currentPath = join(parent, item.name); 319 | 320 | if (item.isDirectory()) { 321 | const removed = _emptyDirSync(childPath, options, currentPath); 322 | 323 | if (!fs.readdirSync(childPath).length) { 324 | rmdirSync(childPath); 325 | } 326 | 327 | results.push(...removed); 328 | } else { 329 | fs.unlinkSync(childPath); 330 | results.push(currentPath); 331 | } 332 | } 333 | 334 | return results; 335 | } 336 | 337 | export function emptyDirSync( 338 | path: string, options: ReadDirOptions & { exclude?: string[] } = {}) { 339 | if (!path) throw new TypeError('path is required!'); 340 | 341 | return _emptyDirSync(path, options, ''); 342 | } 343 | 344 | async function _rmdir(path: string) { 345 | const files = fsPromises.readdir(path, { withFileTypes: true }); 346 | await BlueBirdPromise.map(files, (item: Dirent) => { 347 | const childPath = join(path, item.name); 348 | 349 | return item.isDirectory() ? _rmdir(childPath) : fsPromises.unlink( 350 | childPath); 351 | }); 352 | return fsPromises.rmdir(path); 353 | } 354 | 355 | export function rmdir(path: string) { 356 | if (!path) throw new TypeError('path is required!'); 357 | 358 | return BlueBirdPromise.resolve(_rmdir(path)); 359 | } 360 | 361 | function _rmdirSync(path: string) { 362 | const files = fs.readdirSync(path, { withFileTypes: true }); 363 | 364 | for (let i = 0, len = files.length; i < len; i++) { 365 | const item = files[i]; 366 | const childPath = join(path, item.name); 367 | 368 | if (item.isDirectory()) { 369 | _rmdirSync(childPath); 370 | } else { 371 | fs.unlinkSync(childPath); 372 | } 373 | } 374 | 375 | fs.rmdirSync(path); 376 | } 377 | 378 | export function rmdirSync(path: string) { 379 | if (!path) throw new TypeError('path is required!'); 380 | 381 | _rmdirSync(path); 382 | } 383 | 384 | export function watch( 385 | path: string | Array, options?: ChokidarOptions) { 386 | if (!path) throw new TypeError('path is required!'); 387 | 388 | const watcher = chokidar.watch(path, options); 389 | 390 | return new BlueBirdPromise((resolve, reject) => { 391 | watcher.on('ready', resolve); 392 | watcher.on('error', reject); 393 | }).thenReturn(watcher); 394 | } 395 | 396 | function _findUnusedPath(path: string, files: string[]): string { 397 | const ext = extname(path); 398 | const base = basename(path, ext); 399 | const regex = new RegExp(`^${escapeRegExp(base)}(?:-(\\d+))?${escapeRegExp(ext)}$`); 400 | let num = -1; 401 | 402 | for (let i = 0, len = files.length; i < len; i++) { 403 | const item = files[i]; 404 | const match = item.match(regex); 405 | 406 | if (match == null) continue; 407 | const matchNum = match[1] ? parseInt(match[1], 10) : 0; 408 | 409 | if (matchNum > num) { 410 | num = matchNum; 411 | } 412 | } 413 | 414 | return join(dirname(path), `${base}-${num + 1}${ext}`); 415 | } 416 | 417 | async function _ensurePath(path: string): Promise { 418 | if (!await exists(path)) return path; 419 | 420 | const files = await fsPromises.readdir(dirname(path)); 421 | return _findUnusedPath(path, files); 422 | } 423 | 424 | export function ensurePath(path: string) { 425 | if (!path) throw new TypeError('path is required!'); 426 | 427 | return BlueBirdPromise.resolve(_ensurePath(path)); 428 | } 429 | 430 | export function ensurePathSync(path: string) { 431 | if (!path) throw new TypeError('path is required!'); 432 | if (!fs.existsSync(path)) return path; 433 | 434 | const files = fs.readdirSync(dirname(path)); 435 | 436 | return _findUnusedPath(path, files); 437 | } 438 | 439 | export function ensureWriteStream(path: string, options?: BufferEncoding | { 440 | flags?: string; 441 | encoding?: BufferEncoding; 442 | fd?: number; 443 | mode?: number; 444 | autoClose?: boolean; 445 | emitClose?: boolean; 446 | start?: number; 447 | highWaterMark?: number; 448 | }) { 449 | if (!path) throw new TypeError('path is required!'); 450 | 451 | return checkParent(path) 452 | .then(() => fs.createWriteStream(path, options)); 453 | } 454 | 455 | export function ensureWriteStreamSync(path: string, options?: BufferEncoding | { 456 | flags?: string; 457 | encoding?: BufferEncoding; 458 | fd?: number; 459 | mode?: number; 460 | autoClose?: boolean; 461 | emitClose?: boolean; 462 | start?: number; 463 | highWaterMark?: number; 464 | }) { 465 | if (!path) throw new TypeError('path is required!'); 466 | 467 | fs.mkdirSync(dirname(path), { recursive: true }); 468 | return fs.createWriteStream(path, options); 469 | } 470 | 471 | // access 472 | ['F_OK', 'R_OK', 'W_OK', 'X_OK'].forEach(key => { 473 | Object.defineProperty(exports, key, { 474 | enumerable: true, 475 | value: fs.constants[key], 476 | writable: false 477 | }); 478 | }); 479 | 480 | export const access = BlueBirdPromise.promisify(fs.access); 481 | export const accessSync = fs.accessSync; 482 | 483 | // chmod 484 | export const chmod = BlueBirdPromise.promisify(fs.chmod); 485 | export const chmodSync = fs.chmodSync; 486 | export const fchmod = BlueBirdPromise.promisify(fs.fchmod); 487 | export const fchmodSync = fs.fchmodSync; 488 | export const lchmod = BlueBirdPromise.promisify(fs.lchmod); 489 | export const lchmodSync = fs.lchmodSync; 490 | 491 | // chown 492 | export const chown = BlueBirdPromise.promisify(fs.chown); 493 | export const chownSync = fs.chownSync; 494 | export const fchown = BlueBirdPromise.promisify(fs.fchown); 495 | export const fchownSync = fs.fchownSync; 496 | export const lchown = BlueBirdPromise.promisify(fs.lchown); 497 | export const lchownSync = fs.lchownSync; 498 | 499 | // close 500 | export const close = BlueBirdPromise.promisify(fs.close); 501 | export const closeSync = fs.closeSync; 502 | 503 | // createStream 504 | export const createReadStream = fs.createReadStream; 505 | export const createWriteStream = fs.createWriteStream; 506 | 507 | // fsync 508 | export const fsync = BlueBirdPromise.promisify(fs.fsync); 509 | export const fsyncSync = fs.fsyncSync; 510 | 511 | // link 512 | export const link = BlueBirdPromise.promisify(fs.link); 513 | export const linkSync = fs.linkSync; 514 | 515 | // mkdir 516 | export const mkdir = BlueBirdPromise.promisify(fs.mkdir); 517 | export const mkdirSync = fs.mkdirSync; 518 | 519 | // open 520 | export const open = BlueBirdPromise.promisify(fs.open); 521 | export const openSync = fs.openSync; 522 | 523 | // symlink 524 | export const symlink = BlueBirdPromise.promisify(fs.symlink); 525 | export const symlinkSync = fs.symlinkSync; 526 | 527 | // read 528 | export const read = BlueBirdPromise.promisify(fs.read); 529 | export const readSync = fs.readSync; 530 | 531 | // readdir 532 | export const readdir = BlueBirdPromise.promisify(fs.readdir); 533 | export const readdirSync = fs.readdirSync; 534 | 535 | // readlink 536 | export const readlink = BlueBirdPromise.promisify(fs.readlink); 537 | export const readlinkSync = fs.readlinkSync; 538 | 539 | // realpath 540 | export const realpath = BlueBirdPromise.promisify(fs.realpath); 541 | export const realpathSync = fs.realpathSync; 542 | 543 | // rename 544 | export const rename = BlueBirdPromise.promisify(fs.rename); 545 | export const renameSync = fs.renameSync; 546 | 547 | // stat 548 | export const stat = BlueBirdPromise.promisify(fs.stat); 549 | export const statSync = fs.statSync; 550 | export const fstat = BlueBirdPromise.promisify(fs.fstat); 551 | export const fstatSync = fs.fstatSync; 552 | export const lstat = BlueBirdPromise.promisify(fs.lstat); 553 | export const lstatSync = fs.lstatSync; 554 | 555 | // truncate 556 | export const truncate = BlueBirdPromise.promisify(fs.truncate); 557 | export const truncateSync = fs.truncateSync; 558 | export const ftruncate = BlueBirdPromise.promisify(fs.ftruncate); 559 | export const ftruncateSync = fs.ftruncateSync; 560 | 561 | // unlink 562 | export const unlink = BlueBirdPromise.promisify(fs.unlink); 563 | export const unlinkSync = fs.unlinkSync; 564 | 565 | // utimes 566 | export const utimes = BlueBirdPromise.promisify(fs.utimes); 567 | export const utimesSync = fs.utimesSync; 568 | export const futimes = BlueBirdPromise.promisify(fs.futimes); 569 | export const futimesSync = fs.futimesSync; 570 | 571 | // watch 572 | export const watchFile = fs.watchFile; 573 | export const unwatchFile = fs.unwatchFile; 574 | 575 | // write 576 | export const write = BlueBirdPromise.promisify(fs.write); 577 | export const writeSync = fs.writeSync; 578 | 579 | // Static classes 580 | export const Stats = fs.Stats; 581 | export const ReadStream = fs.ReadStream; 582 | export const WriteStream = fs.WriteStream; 583 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hexo-fs", 3 | "version": "5.0.0", 4 | "description": "File system module for Hexo.", 5 | "main": "./dist/fs.js", 6 | "scripts": { 7 | "prepublish ": "npm install && npm run clean && npm run build", 8 | "build": "tsc -b", 9 | "clean": "tsc -b --clean", 10 | "eslint": "eslint .", 11 | "test": "mocha test/index.ts --require ts-node/register", 12 | "test-cov": "c8 --reporter=lcovonly npm run test" 13 | }, 14 | "files": [ 15 | "dist/**" 16 | ], 17 | "types": "./dist/fs.d.ts", 18 | "repository": "hexojs/hexo-fs", 19 | "homepage": "https://hexo.io/", 20 | "keywords": [ 21 | "file", 22 | "file system", 23 | "fs", 24 | "hexo" 25 | ], 26 | "author": "Tommy Chen (https://zespia.tw)", 27 | "maintainers": [ 28 | "Abner Chou (https://abnerchou.me)" 29 | ], 30 | "license": "MIT", 31 | "dependencies": { 32 | "bluebird": "^3.7.2", 33 | "chokidar": "^4.0.3", 34 | "graceful-fs": "^4.2.10", 35 | "hexo-util": "^3.3.0" 36 | }, 37 | "devDependencies": { 38 | "@types/bluebird": "^3.5.36", 39 | "@types/chai": "^5.0.1", 40 | "@types/graceful-fs": "^4.1.5", 41 | "@types/mocha": "^10.0.6", 42 | "@types/node": "^22.10.2", 43 | "c8": "^10.1.3", 44 | "chai": "^4.3.6", 45 | "eslint": "^8.23.0", 46 | "eslint-config-hexo": "^5.0.0", 47 | "eslint-plugin-import": "^2.31.0", 48 | "mocha": "^11.0.1", 49 | "ts-node": "^10.9.1", 50 | "typescript": "^5.7.2" 51 | }, 52 | "engines": { 53 | "node": ">=18" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "hexo/ts-test", 3 | "rules": { 4 | "@typescript-eslint/no-var-requires": 0, 5 | "@typescript-eslint/ban-ts-comment": 0 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/index.ts: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import { join, dirname } from 'path'; 3 | import BlueBirdPromise from 'bluebird'; 4 | import * as fs from '../lib/fs'; 5 | import type { FSWatcher } from 'chokidar'; 6 | const should = chai.should(); 7 | 8 | function createDummyFolder(path: string) { 9 | const filesMap = { 10 | // Normal files in a hidden folder 11 | [join('.hidden', 'a.txt')]: 'a', 12 | [join('.hidden', 'b.js')]: 'b', 13 | // Normal folder in a hidden folder 14 | [join('.hidden', 'c', 'd')]: 'd', 15 | // Top-class files 16 | 'e.txt': 'e', 17 | 'f.js': 'f', 18 | // A hidden file 19 | '.g': 'g', 20 | // Files in a normal folder 21 | [join('folder', 'h.txt')]: 'h', 22 | [join('folder', 'i.js')]: 'i', 23 | // A hidden files in a normal folder 24 | [join('folder', '.j')]: 'j' 25 | }; 26 | return BlueBirdPromise.map(Object.keys(filesMap), key => fs.writeFile(join(path, key), filesMap[key])); 27 | } 28 | 29 | function createAnotherDummyFolder(path: string) { 30 | const filesMap = { 31 | [join('folder', '.txt')]: 'txt', 32 | [join('folder', '.js')]: 'js' 33 | }; 34 | return BlueBirdPromise.map(Object.keys(filesMap), key => fs.writeFile(join(path, key), filesMap[key])); 35 | } 36 | 37 | describe('fs', () => { 38 | const tmpDir = join(__dirname, 'fs_tmp'); 39 | 40 | before(() => fs.mkdirs(tmpDir)); 41 | 42 | after(() => fs.rmdir(tmpDir)); 43 | 44 | it('exists()', async () => { 45 | const exist = await fs.exists(tmpDir); 46 | exist.should.eql(true); 47 | }); 48 | 49 | it('exists() - path is required', async () => { 50 | try { 51 | // @ts-expect-error 52 | await fs.exists(); 53 | should.fail(); 54 | } catch (err) { 55 | err.message.should.eql('path is required!'); 56 | } 57 | }); 58 | 59 | it('existsSync()', () => { 60 | const exist = fs.existsSync(tmpDir); 61 | exist.should.eql(true); 62 | }); 63 | 64 | it('existsSync() - path is required', () => { 65 | try { 66 | // @ts-expect-error 67 | fs.existsSync(); 68 | should.fail(); 69 | } catch (err) { 70 | err.message.should.eql('path is required!'); 71 | } 72 | }); 73 | 74 | it('existsSync() - not exist', () => { 75 | const exist = fs.existsSync(join(__dirname, 'fs_tmp1')); 76 | exist.should.eql(false); 77 | }); 78 | 79 | it('mkdirs()', async () => { 80 | const target = join(tmpDir, 'a', 'b', 'c'); 81 | 82 | await fs.mkdirs(target); 83 | const exist = await fs.exists(target); 84 | exist.should.eql(true); 85 | 86 | await fs.rmdir(join(tmpDir, 'a')); 87 | }); 88 | 89 | it('mkdirs() - path is required', async () => { 90 | try { 91 | // @ts-expect-error 92 | await fs.mkdirs(); 93 | should.fail(); 94 | } catch (err) { 95 | err.message.should.eql('path is required!'); 96 | } 97 | }); 98 | 99 | it('mkdirsSync()', async () => { 100 | const target = join(tmpDir, 'a', 'b', 'c'); 101 | 102 | fs.mkdirsSync(target); 103 | 104 | const exist = await fs.exists(target); 105 | exist.should.eql(true); 106 | 107 | await fs.rmdir(join(tmpDir, 'a')); 108 | }); 109 | 110 | it('mkdirsSync() - path is required', () => { 111 | try { 112 | // @ts-expect-error 113 | fs.mkdirsSync(); 114 | should.fail(); 115 | } catch (err) { 116 | err.message.should.eql('path is required!'); 117 | } 118 | }); 119 | 120 | it('writeFile()', async () => { 121 | const target = join(tmpDir, 'a', 'b', 'test.txt'); 122 | const body = 'foo'; 123 | 124 | await fs.writeFile(target, body); 125 | const result = await fs.readFile(target); 126 | 127 | result.should.eql(body); 128 | 129 | await fs.rmdir(join(tmpDir, 'a')); 130 | }); 131 | 132 | it('writeFile() - path is required', async () => { 133 | try { 134 | // @ts-expect-error 135 | await fs.writeFile(); 136 | should.fail(); 137 | } catch (err) { 138 | err.message.should.eql('path is required!'); 139 | } 140 | }); 141 | 142 | it('writeFileSync()', async () => { 143 | const target = join(tmpDir, 'a', 'b', 'test.txt'); 144 | const body = 'foo'; 145 | 146 | fs.writeFileSync(target, body); 147 | 148 | const result = await fs.readFile(target); 149 | result.should.eql(body); 150 | 151 | await fs.rmdir(join(tmpDir, 'a')); 152 | }); 153 | 154 | it('writeFileSync() - path is required', () => { 155 | try { 156 | // @ts-expect-error 157 | fs.writeFileSync(); 158 | should.fail(); 159 | } catch (err) { 160 | err.message.should.eql('path is required!'); 161 | } 162 | }); 163 | 164 | it('appendFile()', async () => { 165 | const target = join(tmpDir, 'a', 'b', 'test.txt'); 166 | const body = 'foo'; 167 | const body2 = 'bar'; 168 | 169 | await fs.writeFile(target, body); 170 | await fs.appendFile(target, body2); 171 | 172 | const result = await fs.readFile(target); 173 | 174 | result.should.eql(body + body2); 175 | 176 | await fs.rmdir(join(tmpDir, 'a')); 177 | }); 178 | 179 | it('appendFile() - path is required', async () => { 180 | try { 181 | // @ts-expect-error 182 | await fs.appendFile(); 183 | should.fail(); 184 | } catch (err) { 185 | err.message.should.eql('path is required!'); 186 | } 187 | }); 188 | 189 | it('appendFileSync()', async () => { 190 | const target = join(tmpDir, 'a', 'b', 'test.txt'); 191 | const body = 'foo'; 192 | const body2 = 'bar'; 193 | 194 | await fs.writeFile(target, body); 195 | fs.appendFileSync(target, body2); 196 | 197 | const result = await fs.readFile(target); 198 | result.should.eql(body + body2); 199 | 200 | await fs.rmdir(join(tmpDir, 'a')); 201 | }); 202 | 203 | it('appendFileSync() - path is required', () => { 204 | try { 205 | // @ts-expect-error 206 | fs.appendFileSync(); 207 | should.fail(); 208 | } catch (err) { 209 | err.message.should.eql('path is required!'); 210 | } 211 | }); 212 | 213 | it('copyFile()', async () => { 214 | const src = join(tmpDir, 'test.txt'); 215 | const dest = join(tmpDir, 'a', 'b', 'test.txt'); 216 | const body = 'foo'; 217 | 218 | await fs.writeFile(src, body); 219 | await fs.copyFile(src, dest); 220 | 221 | const result = await fs.readFile(dest); 222 | result.should.eql(body); 223 | 224 | await BlueBirdPromise.all([ 225 | fs.unlink(src), 226 | fs.rmdir(join(tmpDir, 'a')) 227 | ]); 228 | }); 229 | 230 | it('copyFile() - src is required', async () => { 231 | try { 232 | // @ts-expect-error 233 | await fs.copyFile(); 234 | should.fail(); 235 | } catch (err) { 236 | err.message.should.eql('src is required!'); 237 | } 238 | }); 239 | 240 | it('copyFile() - dest is required', async () => { 241 | try { 242 | // @ts-expect-error 243 | await fs.copyFile('123'); 244 | should.fail(); 245 | } catch (err) { 246 | err.message.should.eql('dest is required!'); 247 | } 248 | }); 249 | 250 | it('copyDir()', async () => { 251 | const src = join(tmpDir, 'a'); 252 | const dest = join(tmpDir, 'b'); 253 | 254 | const filenames = [ 255 | 'e.txt', 256 | 'f.js', 257 | join('folder', 'h.txt'), 258 | join('folder', 'i.js') 259 | ]; 260 | 261 | await createDummyFolder(src); 262 | const files = await fs.copyDir(src, dest); 263 | files.should.eql(filenames); 264 | 265 | const result: string[] = []; 266 | for (const file of files) { 267 | const output = await fs.readFile(join(dest, file)) as string; 268 | result.push(output); 269 | } 270 | result.should.eql(['e', 'f', 'h', 'i']); 271 | 272 | await BlueBirdPromise.all([fs.rmdir(src), fs.rmdir(dest)]); 273 | }); 274 | 275 | it('copyDir() - src is required', async () => { 276 | try { 277 | // @ts-expect-error 278 | await fs.copyDir(); 279 | should.fail(); 280 | } catch (err) { 281 | err.message.should.eql('src is required!'); 282 | } 283 | }); 284 | 285 | it('copyDir() - dest is required', async () => { 286 | try { 287 | // @ts-expect-error 288 | await fs.copyDir('123'); 289 | should.fail(); 290 | } catch (err) { 291 | err.message.should.eql('dest is required!'); 292 | } 293 | }); 294 | 295 | it('copyDir() - ignoreHidden off', async () => { 296 | const src = join(tmpDir, 'a'); 297 | const dest = join(tmpDir, 'b'); 298 | 299 | const filenames = [ 300 | join('.hidden', 'a.txt'), 301 | join('.hidden', 'b.js'), 302 | join('.hidden', 'c', 'd'), 303 | 'e.txt', 304 | 'f.js', 305 | '.g', 306 | join('folder', 'h.txt'), 307 | join('folder', 'i.js'), 308 | join('folder', '.j') 309 | ]; 310 | 311 | await createDummyFolder(src); 312 | const files = await fs.copyDir(src, dest, { ignoreHidden: false }); 313 | files.should.have.members(filenames); 314 | 315 | const result: string[] = []; 316 | for (const file of files) { 317 | const output = await fs.readFile(join(dest, file)) as string; 318 | result.push(output); 319 | } 320 | result.should.have.members(['a', 'b', 'd', 'e', 'f', 'g', 'h', 'i', 'j']); 321 | 322 | await BlueBirdPromise.all([fs.rmdir(src), fs.rmdir(dest)]); 323 | }); 324 | 325 | it('copyDir() - ignorePattern', async () => { 326 | const src = join(tmpDir, 'a'); 327 | const dest = join(tmpDir, 'b'); 328 | 329 | const filenames = ['e.txt', join('folder', 'h.txt')]; 330 | 331 | await createDummyFolder(src); 332 | const files = await fs.copyDir(src, dest, { ignorePattern: /\.js/ }); 333 | files.should.eql(filenames); 334 | 335 | const result: string[] = []; 336 | for (const file of files) { 337 | const output = await fs.readFile(join(dest, file)) as string; 338 | result.push(output); 339 | } 340 | result.should.eql(['e', 'h']); 341 | 342 | await BlueBirdPromise.all([fs.rmdir(src), fs.rmdir(dest)]); 343 | }); 344 | 345 | it('listDir()', async () => { 346 | const expected = [ 347 | 'e.txt', 348 | 'f.js', 349 | join('folder', 'h.txt'), 350 | join('folder', 'i.js') 351 | ]; 352 | const target = join(tmpDir, 'test'); 353 | 354 | await createDummyFolder(target); 355 | const dir = await fs.listDir(target); 356 | dir.should.eql(expected); 357 | 358 | await fs.rmdir(target); 359 | }); 360 | 361 | it('listDir() - path is required', async () => { 362 | try { 363 | // @ts-expect-error 364 | await fs.listDir(); 365 | should.fail(); 366 | } catch (err) { 367 | err.message.should.eql('path is required!'); 368 | } 369 | }); 370 | 371 | it('listDir() - ignoreHidden off', async () => { 372 | const target = join(tmpDir, 'test'); 373 | 374 | const filenames = [ 375 | join('.hidden', 'a.txt'), 376 | join('.hidden', 'b.js'), 377 | join('.hidden', 'c', 'd'), 378 | 'e.txt', 379 | 'f.js', 380 | '.g', 381 | join('folder', 'h.txt'), 382 | join('folder', 'i.js'), 383 | join('folder', '.j') 384 | ]; 385 | 386 | await createDummyFolder(target); 387 | const dir = await fs.listDir(target, { ignoreHidden: false }); 388 | dir.should.have.members(filenames); 389 | 390 | await fs.rmdir(target); 391 | }); 392 | 393 | it('listDir() - ignorePattern', async () => { 394 | const target = join(tmpDir, 'test'); 395 | 396 | await createDummyFolder(target); 397 | const dir = await fs.listDir(target, { ignorePattern: /\.js/ }); 398 | dir.should.eql(['e.txt', join('folder', 'h.txt')]); 399 | 400 | await fs.rmdir(target); 401 | }); 402 | 403 | it('listDirSync()', async () => { 404 | const target = join(tmpDir, 'test'); 405 | 406 | const filenames = [ 407 | 'e.txt', 408 | 'f.js', 409 | join('folder', 'h.txt'), 410 | join('folder', 'i.js') 411 | ]; 412 | 413 | await createDummyFolder(target); 414 | const files = fs.listDirSync(target); 415 | files.should.eql(filenames); 416 | 417 | await fs.rmdir(target); 418 | }); 419 | 420 | it('listDirSync() - path is required', () => { 421 | try { 422 | // @ts-expect-error 423 | fs.listDirSync(); 424 | should.fail(); 425 | } catch (err) { 426 | err.message.should.eql('path is required!'); 427 | } 428 | }); 429 | 430 | it('listDirSync() - ignoreHidden off', async () => { 431 | const target = join(tmpDir, 'test'); 432 | 433 | const filenames = [ 434 | join('.hidden', 'a.txt'), 435 | join('.hidden', 'b.js'), 436 | join('.hidden', 'c', 'd'), 437 | 'e.txt', 438 | 'f.js', 439 | '.g', 440 | join('folder', 'h.txt'), 441 | join('folder', 'i.js'), 442 | join('folder', '.j') 443 | ]; 444 | 445 | await createDummyFolder(target); 446 | const files = fs.listDirSync(target, { ignoreHidden: false }); 447 | files.should.have.members(filenames); 448 | 449 | await fs.rmdir(target); 450 | }); 451 | 452 | it('listDirSync() - ignorePattern', async () => { 453 | const target = join(tmpDir, 'test'); 454 | 455 | await createDummyFolder(target); 456 | const files = fs.listDirSync(target, { ignorePattern: /\.js/ }); 457 | files.should.eql(['e.txt', join('folder', 'h.txt')]); 458 | 459 | await fs.rmdir(target); 460 | }); 461 | 462 | it('readFile()', async () => { 463 | const target = join(tmpDir, 'test.txt'); 464 | const body = 'test'; 465 | 466 | await fs.writeFile(target, body); 467 | const result = await fs.readFile(target); 468 | result.should.eql(body); 469 | 470 | await fs.unlink(target); 471 | }); 472 | 473 | it('readFile() - path is required', async () => { 474 | try { 475 | // @ts-expect-error 476 | await fs.readFile(); 477 | should.fail(); 478 | } catch (err) { 479 | err.message.should.eql('path is required!'); 480 | } 481 | }); 482 | 483 | it('readFile() - escape BOM', async () => { 484 | const target = join(tmpDir, 'test.txt'); 485 | const body = '\ufefffoo'; 486 | 487 | await fs.writeFile(target, body); 488 | const result = await fs.readFile(target); 489 | 490 | result.should.eql('foo'); 491 | 492 | await fs.unlink(target); 493 | }); 494 | 495 | it('readFile() - escape Windows line ending', async () => { 496 | const target = join(tmpDir, 'test.txt'); 497 | const body = 'foo\r\nbar'; 498 | 499 | await fs.writeFile(target, body); 500 | const result = await fs.readFile(target); 501 | result.should.eql('foo\nbar'); 502 | 503 | await fs.unlink(target); 504 | }); 505 | 506 | it('readFile() - do not escape', async () => { 507 | const target = join(tmpDir, 'test.txt'); 508 | const body = 'foo\r\nbar'; 509 | 510 | await fs.writeFile(target, body); 511 | const result = await fs.readFile(target, { escape: '' }); 512 | result.should.eql('foo\r\nbar'); 513 | 514 | await fs.unlink(target); 515 | }); 516 | 517 | it('readFileSync()', async () => { 518 | const target = join(tmpDir, 'test.txt'); 519 | const body = 'test'; 520 | 521 | await fs.writeFile(target, body); 522 | const result = fs.readFileSync(target); 523 | result.should.eql(body); 524 | 525 | await fs.unlink(target); 526 | }); 527 | 528 | it('readFileSync() - path is required', () => { 529 | try { 530 | // @ts-expect-error 531 | fs.readFileSync(); 532 | should.fail(); 533 | } catch (err) { 534 | err.message.should.eql('path is required!'); 535 | } 536 | }); 537 | 538 | it('readFileSync() - escape BOM', async () => { 539 | const target = join(tmpDir, 'test.txt'); 540 | const body = '\ufefffoo'; 541 | 542 | await fs.writeFile(target, body); 543 | const result = fs.readFileSync(target); 544 | result.should.eql('foo'); 545 | 546 | await fs.unlink(target); 547 | }); 548 | 549 | it('readFileSync() - escape Windows line ending', async () => { 550 | const target = join(tmpDir, 'test.txt'); 551 | const body = 'foo\r\nbar'; 552 | 553 | await fs.writeFile(target, body); 554 | const result = fs.readFileSync(target); 555 | result.should.eql('foo\nbar'); 556 | 557 | await fs.unlink(target); 558 | }); 559 | 560 | it('readFileSync() - do not escape', async () => { 561 | const target = join(tmpDir, 'test.txt'); 562 | const body = 'foo\r\nbar'; 563 | 564 | await fs.writeFile(target, body); 565 | const result = fs.readFileSync(target, { escape: '' }); 566 | result.should.eql('foo\r\nbar'); 567 | 568 | await fs.unlink(target); 569 | }); 570 | 571 | it('unlink()', async () => { 572 | const target = join(tmpDir, 'test-unlink'); 573 | 574 | await fs.writeFile(target, ''); 575 | let exist = await fs.exists(target); 576 | exist.should.eql(true); 577 | 578 | await fs.unlink(target); 579 | exist = await fs.exists(target); 580 | exist.should.eql(false); 581 | }); 582 | 583 | it('emptyDir()', async () => { 584 | const target = join(tmpDir, 'test'); 585 | 586 | const checkExistsMap = { 587 | [join('.hidden', 'a.txt')]: true, 588 | [join('.hidden', 'b.js')]: true, 589 | [join('.hidden', 'c', 'd')]: true, 590 | 'e.txt': false, 591 | 'f.js': false, 592 | '.g': true, 593 | [join('folder', 'h.txt')]: false, 594 | [join('folder', 'i.js')]: false, 595 | [join('folder', '.j')]: true 596 | }; 597 | 598 | await createDummyFolder(target); 599 | const files = await fs.emptyDir(target); 600 | files.should.eql([ 601 | 'e.txt', 602 | 'f.js', 603 | join('folder', 'h.txt'), 604 | join('folder', 'i.js') 605 | ]); 606 | 607 | const paths = Object.keys(checkExistsMap); 608 | for (const path of paths) { 609 | const exist = await fs.exists(join(target, path)); 610 | exist.should.eql(checkExistsMap[path]); 611 | } 612 | 613 | await fs.rmdir(target); 614 | }); 615 | 616 | it('emptyDir() - empty nothing', async () => { 617 | const target = join(tmpDir, 'test'); 618 | 619 | const checkExistsMap = { 620 | [join('folder', '.txt')]: true, 621 | [join('folder', '.js')]: true 622 | }; 623 | 624 | await createAnotherDummyFolder(target); 625 | const files = await fs.emptyDir(target); 626 | files.should.eql([]); 627 | 628 | const paths = Object.keys(checkExistsMap); 629 | for (const path of paths) { 630 | const exist = await fs.exists(join(target, path)); 631 | exist.should.eql(checkExistsMap[path]); 632 | } 633 | 634 | await fs.rmdir(target); 635 | }); 636 | 637 | it('emptyDir() - path is required', async () => { 638 | try { 639 | // @ts-expect-error 640 | await fs.emptyDir(); 641 | should.fail(); 642 | } catch (err) { 643 | err.message.should.eql('path is required!'); 644 | } 645 | }); 646 | 647 | it('emptyDir() - ignoreHidden off', async () => { 648 | const target = join(tmpDir, 'test'); 649 | 650 | const filenames = [ 651 | join('.hidden', 'a.txt'), 652 | join('.hidden', 'b.js'), 653 | join('.hidden', 'c', 'd'), 654 | 'e.txt', 655 | 'f.js', 656 | '.g', 657 | join('folder', 'h.txt'), 658 | join('folder', 'i.js'), 659 | join('folder', '.j') 660 | ]; 661 | 662 | await createDummyFolder(target); 663 | const files = await fs.emptyDir(target, { ignoreHidden: false }); 664 | files.should.have.members(filenames); 665 | 666 | for (const file of files) { 667 | const exist = await fs.exists(join(target, file)); 668 | exist.should.eql(false); 669 | } 670 | 671 | await fs.rmdir(target); 672 | }); 673 | 674 | it('emptyDir() - ignorePattern', async () => { 675 | const target = join(tmpDir, 'test'); 676 | 677 | const checkExistsMap = { 678 | [join('.hidden', 'a.txt')]: true, 679 | [join('.hidden', 'b.js')]: true, 680 | [join('.hidden', 'c', 'd')]: true, 681 | 'e.txt': false, 682 | 'f.js': true, 683 | '.g': true, 684 | [join('folder', 'h.txt')]: false, 685 | [join('folder', 'i.js')]: true, 686 | [join('folder', '.j')]: true 687 | }; 688 | 689 | await createDummyFolder(target); 690 | const files = await fs.emptyDir(target, { ignorePattern: /\.js/ }); 691 | files.should.eql(['e.txt', join('folder', 'h.txt')]); 692 | 693 | const paths = Object.keys(checkExistsMap); 694 | for (const path of paths) { 695 | const exist = await fs.exists(join(target, path)); 696 | exist.should.eql(checkExistsMap[path]); 697 | } 698 | 699 | await fs.rmdir(target); 700 | }); 701 | 702 | it('emptyDir() - exclude', async () => { 703 | const target = join(tmpDir, 'test'); 704 | 705 | const checkExistsMap = { 706 | [join('.hidden', 'a.txt')]: true, 707 | [join('.hidden', 'b.js')]: true, 708 | [join('.hidden', 'c', 'd')]: true, 709 | 'e.txt': true, 710 | 'f.js': false, 711 | '.g': true, 712 | [join('folder', 'h.txt')]: false, 713 | [join('folder', 'i.js')]: true, 714 | [join('folder', '.j')]: true 715 | }; 716 | 717 | await createDummyFolder(target); 718 | const files = await fs.emptyDir(target, { exclude: ['e.txt', join('folder', 'i.js')] }); 719 | files.should.eql(['f.js', join('folder', 'h.txt')]); 720 | 721 | const paths = Object.keys(checkExistsMap); 722 | for (const path of paths) { 723 | const exist = await fs.exists(join(target, path)); 724 | exist.should.eql(checkExistsMap[path]); 725 | } 726 | 727 | await fs.rmdir(target); 728 | }); 729 | 730 | it('emptyDirSync()', async () => { 731 | const target = join(tmpDir, 'test'); 732 | 733 | const checkExistsMap = { 734 | [join('.hidden', 'a.txt')]: true, 735 | [join('.hidden', 'b.js')]: true, 736 | [join('.hidden', 'c', 'd')]: true, 737 | 'e.txt': false, 738 | 'f.js': false, 739 | '.g': true, 740 | [join('folder', 'h.txt')]: false, 741 | [join('folder', 'i.js')]: false, 742 | [join('folder', '.j')]: true 743 | }; 744 | 745 | await createDummyFolder(target); 746 | const files = fs.emptyDirSync(target); 747 | files.should.eql([ 748 | 'e.txt', 749 | 'f.js', 750 | join('folder', 'h.txt'), 751 | join('folder', 'i.js') 752 | ]); 753 | 754 | const paths = Object.keys(checkExistsMap); 755 | for (const path of paths) { 756 | const exist = await fs.exists(join(target, path)); 757 | exist.should.eql(checkExistsMap[path]); 758 | } 759 | 760 | await fs.rmdir(target); 761 | }); 762 | 763 | it('emptyDirSync() - path is required', () => { 764 | try { 765 | // @ts-expect-error 766 | fs.emptyDirSync(); 767 | should.fail(); 768 | } catch (err) { 769 | err.message.should.eql('path is required!'); 770 | } 771 | }); 772 | 773 | it('emptyDirSync() - ignoreHidden off', async () => { 774 | const target = join(tmpDir, 'test'); 775 | 776 | const filenames = [ 777 | join('.hidden', 'a.txt'), 778 | join('.hidden', 'b.js'), 779 | join('.hidden', 'c', 'd'), 780 | 'e.txt', 781 | 'f.js', 782 | '.g', 783 | join('folder', 'h.txt'), 784 | join('folder', 'i.js'), 785 | join('folder', '.j') 786 | ]; 787 | 788 | await createDummyFolder(target); 789 | const files = fs.emptyDirSync(target, { ignoreHidden: false }); 790 | files.should.have.members(filenames); 791 | 792 | for (const file of files) { 793 | const exist = await fs.exists(join(target, file)); 794 | exist.should.eql(false); 795 | } 796 | 797 | await fs.rmdir(target); 798 | }); 799 | 800 | it('emptyDirSync() - ignorePattern', async () => { 801 | const target = join(tmpDir, 'test'); 802 | 803 | const checkExistsMap = { 804 | [join('.hidden', 'a.txt')]: true, 805 | [join('.hidden', 'b.js')]: true, 806 | [join('.hidden', 'c', 'd')]: true, 807 | 'e.txt': false, 808 | 'f.js': true, 809 | '.g': true, 810 | [join('folder', 'h.txt')]: false, 811 | [join('folder', 'i.js')]: true, 812 | [join('folder', '.j')]: true 813 | }; 814 | 815 | await createDummyFolder(target); 816 | const files = fs.emptyDirSync(target, { ignorePattern: /\.js/ }); 817 | files.should.eql(['e.txt', join('folder', 'h.txt')]); 818 | 819 | const paths = Object.keys(checkExistsMap); 820 | for (const path of paths) { 821 | const exist = await fs.exists(join(target, path)); 822 | exist.should.eql(checkExistsMap[path]); 823 | } 824 | 825 | await fs.rmdir(target); 826 | }); 827 | 828 | it('emptyDirSync() - exclude', async () => { 829 | const target = join(tmpDir, 'test'); 830 | 831 | const checkExistsMap = { 832 | [join('.hidden', 'a.txt')]: true, 833 | [join('.hidden', 'b.js')]: true, 834 | [join('.hidden', 'c', 'd')]: true, 835 | 'e.txt': true, 836 | 'f.js': false, 837 | '.g': true, 838 | [join('folder', 'h.txt')]: false, 839 | [join('folder', 'i.js')]: true, 840 | [join('folder', '.j')]: true 841 | }; 842 | 843 | await createDummyFolder(target); 844 | const files = fs.emptyDirSync(target, { exclude: ['e.txt', join('folder', 'i.js')] }); 845 | files.should.eql(['f.js', join('folder', 'h.txt')]); 846 | 847 | const paths = Object.keys(checkExistsMap); 848 | for (const path of paths) { 849 | const exist = await fs.exists(join(target, path)); 850 | exist.should.eql(checkExistsMap[path]); 851 | } 852 | 853 | await fs.rmdir(target); 854 | }); 855 | 856 | it('rmdir()', async () => { 857 | const target = join(tmpDir, 'test'); 858 | 859 | await createDummyFolder(target); 860 | await fs.rmdir(target); 861 | const exist = await fs.exists(target); 862 | exist.should.eql(false); 863 | }); 864 | 865 | it('rmdir() - path is required', async () => { 866 | try { 867 | // @ts-expect-error 868 | await fs.rmdir(); 869 | should.fail(); 870 | } catch (err) { 871 | err.message.should.eql('path is required!'); 872 | } 873 | }); 874 | 875 | it('rmdirSync()', async () => { 876 | const target = join(tmpDir, 'test'); 877 | 878 | await createDummyFolder(target); 879 | fs.rmdirSync(target); 880 | const exist = await fs.exists(target); 881 | exist.should.eql(false); 882 | }); 883 | 884 | it('rmdirSync() - path is required', () => { 885 | try { 886 | // @ts-expect-error 887 | fs.rmdirSync(); 888 | should.fail(); 889 | } catch (err) { 890 | err.message.should.eql('path is required!'); 891 | } 892 | }); 893 | 894 | it('watch()', async () => { 895 | const target = join(tmpDir, 'test.txt'); 896 | 897 | const testerWrap = (_watcher: FSWatcher) => new BlueBirdPromise((resolve, reject) => { 898 | _watcher.on('add', resolve).on('error', reject); 899 | }); 900 | 901 | const watcher = await fs.watch(tmpDir); 902 | const result = await BlueBirdPromise.all([ 903 | testerWrap(watcher), 904 | fs.writeFile(target, 'test') 905 | ]); 906 | result[0].should.eql(target); 907 | 908 | if (watcher) { 909 | watcher.close(); 910 | } 911 | await fs.unlink(target); 912 | }); 913 | 914 | it('watch() - path is required', async () => { 915 | try { 916 | // @ts-expect-error 917 | await fs.watch(); 918 | should.fail(); 919 | } catch (err) { 920 | err.message.should.eql('path is required!'); 921 | } 922 | }); 923 | 924 | it('ensurePath() - file exists', async () => { 925 | const target = join(tmpDir, 'test'); 926 | const filenames = ['foo.txt', 'foo-1.txt', 'foo-2.md', 'bar.txt']; 927 | 928 | await BlueBirdPromise.map(filenames, path => fs.writeFile(join(target, path))); 929 | const result = await fs.ensurePath(join(target, 'foo.txt')); 930 | result.should.eql(join(target, 'foo-2.txt')); 931 | 932 | await fs.rmdir(target); 933 | }); 934 | 935 | it('ensurePath() - file not exist', async () => { 936 | const target = join(tmpDir, 'foo.txt'); 937 | const result = await fs.ensurePath(target); 938 | result.should.eql(target); 939 | }); 940 | 941 | it('ensurePath() - path is required', async () => { 942 | try { 943 | // @ts-expect-error 944 | await fs.ensurePath(); 945 | should.fail(); 946 | } catch (err) { 947 | err.message.should.eql('path is required!'); 948 | } 949 | }); 950 | 951 | it('ensurePathSync() - file exists', async () => { 952 | const target = join(tmpDir, 'test'); 953 | const filenames = ['foo.txt', 'foo-1.txt', 'foo-2.md', 'bar.txt']; 954 | 955 | await BlueBirdPromise.map(filenames, path => fs.writeFile(join(target, path))); 956 | const path = fs.ensurePathSync(join(target, 'foo.txt')); 957 | path.should.eql(join(target, 'foo-2.txt')); 958 | 959 | await fs.rmdir(target); 960 | }); 961 | 962 | it('ensurePathSync() - file not exist', () => { 963 | const target = join(tmpDir, 'foo.txt'); 964 | const path = fs.ensurePathSync(target); 965 | 966 | path.should.eql(target); 967 | }); 968 | 969 | it('ensurePathSync() - path is required', () => { 970 | try { 971 | // @ts-expect-error 972 | fs.ensurePathSync(); 973 | should.fail(); 974 | } catch (err) { 975 | err.message.should.eql('path is required!'); 976 | } 977 | }); 978 | 979 | it('ensureWriteStream()', async () => { 980 | const { promisify } = require('util'); 981 | const streamFn = require('stream'); 982 | const finished = promisify(streamFn.finished); 983 | 984 | const target = join(tmpDir, 'foo', 'bar.txt'); 985 | 986 | const stream = await fs.ensureWriteStream(target); 987 | stream.path.should.eql(target); 988 | 989 | stream.end(); 990 | await finished(stream); 991 | 992 | await fs.unlink(target); 993 | }); 994 | 995 | it('ensureWriteStreamSync()', callback => { 996 | const target = join(tmpDir, 'foo', 'bar.txt'); 997 | const stream = fs.ensureWriteStreamSync(target); 998 | 999 | stream.path.should.eql(target); 1000 | 1001 | stream.on('error', callback); 1002 | stream.on('close', () => { 1003 | fs.rmdir(dirname(target)).asCallback(callback); 1004 | }); 1005 | 1006 | stream.end(); 1007 | }); 1008 | }); 1009 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "sourceMap": true, 6 | "outDir": "dist", 7 | "declaration": true, 8 | "esModuleInterop": true, 9 | "types": [ 10 | "node", 11 | "mocha" 12 | ] 13 | }, 14 | "include": [ 15 | "lib/fs.ts" 16 | ], 17 | "exclude": [ 18 | "node_modules" 19 | ] 20 | } 21 | --------------------------------------------------------------------------------