├── .eslintignore ├── .eslintrc.yml ├── .github └── workflows │ └── nodeci.yml ├── .gitignore ├── .npmignore ├── Makefile ├── README.md ├── __tests__ └── test.js ├── docs └── README.md ├── index.js ├── package-lock.json └── package.json /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | flow-typed 3 | node_modules 4 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | extends: 3 | - 'airbnb-base' 4 | - 'plugin:jest/recommended' 5 | 6 | env: 7 | node: true 8 | jest: true 9 | es2020: true 10 | 11 | rules: 12 | import/extensions: 0 13 | -------------------------------------------------------------------------------- /.github/workflows/nodeci.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | strategy: 12 | matrix: 13 | node-version: [14.x, 15.x, 16.x, 17.x, 18.x] 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Use Node.js ${{ matrix.node-version }} 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | - name: Check node version 22 | run: node -v 23 | - name: Install 24 | run: make install 25 | - name: Run linter 26 | run: make lint 27 | - name: Run tests 28 | run: make test 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | *.log 4 | flow-typed 5 | .tern-port 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexlet-components/js-immutable-fs-trees/8a96ed7eeea802cd48ce04f99ae203dd25f3e24b/.npmignore -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | install: 2 | npm install 3 | 4 | docs: 5 | mkdir -p docs 6 | npm run documentation -- build index.js -f md > docs/README.md 7 | 8 | test: 9 | npm test -s 10 | 11 | lint: 12 | npx eslint . 13 | 14 | publish: 15 | npm publish --access public 16 | 17 | .PHONY: test docs 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # js-immutable-fs-trees 2 | 3 | [![github action status](https://github.com/hexlet-components/js-immutable-fs-trees/workflows/Node%20CI/badge.svg)](https://github.com/hexlet-components/js-immutable-fs-trees/actions) 4 | 5 | ## Install 6 | 7 | ```sh 8 | npm install @hexlet/immutable-fs-trees 9 | ``` 10 | 11 | ## Usage example 12 | 13 | ```javascript 14 | import { 15 | mkfile, mkdir, isDirectory, isFile, map, 16 | } from '@hexlet/immutable-fs-trees'; 17 | 18 | isFile(mkfile('config')); // true 19 | isDirectory(mkdir('etc')); // true 20 | 21 | const tree = mkdir('etc', [mkfile('config'), mkfile('hosts')]); 22 | 23 | const callbackFn = (node) => { 24 | const { name } = node; 25 | const newName = name.toUpperCase(); 26 | return { ...node, name: newName }; 27 | }; 28 | 29 | map(callbackFn, tree); 30 | // { 31 | // name: 'ETC', 32 | // children: [ 33 | // { name: 'CONFIG', meta: {}, type: 'file' }, 34 | // { name: 'HOSTS', meta: {}, type: 'file' } 35 | // ], 36 | // meta: {}, 37 | // type: 'directory', 38 | // } 39 | ``` 40 | 41 | For more information, see the [Full Documentation](https://github.com/hexlet-components/js-immutable-fs-trees/tree/master/docs) 42 | 43 | --- 44 | 45 | [![Hexlet Ltd. logo](https://raw.githubusercontent.com/Hexlet/assets/master/images/hexlet_logo128.png)](https://hexlet.io?utm_source=github&utm_medium=link&utm_campaign=js-immutable-fs-trees) 46 | 47 | This repository is created and maintained by the team and the community of Hexlet, an educational project. [Read more about Hexlet](https://hexlet.io?utm_source=github&utm_medium=link&utm_campaign=js-immutable-fs-trees). 48 | 49 | See most active contributors on [hexlet-friends](https://friends.hexlet.io/). 50 | -------------------------------------------------------------------------------- /__tests__/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { 4 | mkdir, 5 | mkfile, 6 | isFile, 7 | isDirectory, 8 | map, 9 | reduce, 10 | filter, 11 | } from '../index.js'; 12 | 13 | test('build', () => { 14 | const tree = mkdir('/', [mkdir('etc'), mkdir('usr'), mkfile('robots.txt')]); 15 | 16 | expect(tree).toEqual({ 17 | children: [ 18 | { 19 | children: [], 20 | meta: {}, 21 | name: 'etc', 22 | type: 'directory', 23 | }, 24 | { 25 | children: [], 26 | meta: {}, 27 | name: 'usr', 28 | type: 'directory', 29 | }, 30 | { 31 | meta: {}, 32 | name: 'robots.txt', 33 | type: 'file', 34 | }, 35 | ], 36 | meta: {}, 37 | name: '/', 38 | type: 'directory', 39 | }); 40 | }); 41 | 42 | test('isFile', () => { 43 | const node = mkfile('config.json'); 44 | expect(isFile(node)).toBeTruthy(); 45 | expect(isDirectory(node)).toBeFalsy(); 46 | }); 47 | 48 | test('isDirectory', () => { 49 | const node = mkdir('/'); 50 | expect(isDirectory(node)).toBeTruthy(); 51 | expect(isFile(node)).toBeFalsy(); 52 | }); 53 | 54 | test('reduce', () => { 55 | const tree = mkdir('/', [ 56 | mkdir('eTc', [ 57 | mkdir('NgiNx'), 58 | mkdir('CONSUL', [ 59 | mkfile('config.json'), 60 | ]), 61 | ]), 62 | mkfile('hOsts'), 63 | ]); 64 | const actual = reduce((acc) => acc + 1, tree, 0); 65 | expect(actual).toEqual(6); 66 | 67 | const actual2 = reduce((acc, n) => (isFile(n) ? acc + 1 : acc), tree, 0); 68 | expect(actual2).toEqual(2); 69 | 70 | const actual3 = reduce((acc, n) => (isDirectory(n) ? acc + 1 : acc), tree, 0); 71 | expect(actual3).toEqual(4); 72 | }); 73 | 74 | test('map', () => { 75 | const tree = mkdir('/', [ 76 | mkdir('eTc', [ 77 | mkdir('NgiNx'), 78 | mkdir('CONSUL', [ 79 | mkfile('config.json'), 80 | ]), 81 | ]), 82 | mkfile('hOsts'), 83 | ]); 84 | const actual = map((n) => ({ ...n, name: n.name.toUpperCase() }), tree); 85 | 86 | const expected = { 87 | children: [ 88 | { 89 | children: [ 90 | { 91 | children: [], meta: {}, name: 'NGINX', type: 'directory', 92 | }, 93 | { 94 | children: [{ meta: {}, name: 'CONFIG.JSON', type: 'file' }], 95 | meta: {}, 96 | name: 'CONSUL', 97 | type: 'directory', 98 | }, 99 | ], 100 | meta: {}, 101 | name: 'ETC', 102 | type: 'directory', 103 | }, 104 | { meta: {}, name: 'HOSTS', type: 'file' }, 105 | ], 106 | meta: {}, 107 | name: '/', 108 | type: 'directory', 109 | }; 110 | 111 | expect(actual).toEqual(expected); 112 | }); 113 | 114 | test('filter', () => { 115 | const tree = mkdir('/', [ 116 | mkdir('etc', [ 117 | mkdir('nginx', [ 118 | mkdir('conf.d'), 119 | ]), 120 | mkdir('consul', [ 121 | mkfile('config.json'), 122 | ]), 123 | ]), 124 | mkfile('hosts'), 125 | ]); 126 | const actual = filter((n) => isDirectory(n), tree); 127 | 128 | const expected = { 129 | children: [ 130 | { 131 | children: [ 132 | { 133 | children: [{ 134 | children: [], 135 | meta: {}, 136 | name: 'conf.d', 137 | type: 'directory', 138 | }], 139 | meta: {}, 140 | name: 'nginx', 141 | type: 'directory', 142 | }, 143 | { 144 | children: [], 145 | meta: {}, 146 | name: 'consul', 147 | type: 'directory', 148 | }, 149 | ], 150 | meta: {}, 151 | name: 'etc', 152 | type: 'directory', 153 | }, 154 | ], 155 | meta: {}, 156 | name: '/', 157 | type: 'directory', 158 | }; 159 | 160 | expect(actual).toEqual(expected); 161 | }); 162 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | 2 | > @hexlet/immutable-fs-trees@0.7.0 documentation /home/stas/Work/hexlet/hexlet-components/js-immutable-fs-trees 3 | > documentation "build" "index.js" "-f" "md" 4 | 5 | 6 | 7 | ### Table of Contents 8 | 9 | - [Node][1] 10 | - [Properties][2] 11 | - [mkfile][3] 12 | - [Parameters][4] 13 | - [Examples][5] 14 | - [mkdir][6] 15 | - [Parameters][7] 16 | - [Examples][8] 17 | - [getChildren][9] 18 | - [Parameters][10] 19 | - [Examples][11] 20 | - [getMeta][12] 21 | - [Parameters][13] 22 | - [Examples][14] 23 | - [getName][15] 24 | - [Parameters][16] 25 | - [Examples][17] 26 | - [isFile][18] 27 | - [Parameters][19] 28 | - [Examples][20] 29 | - [isDirectory][21] 30 | - [Parameters][22] 31 | - [Examples][23] 32 | - [map][24] 33 | - [Parameters][25] 34 | - [Examples][26] 35 | - [reduce][27] 36 | - [Parameters][28] 37 | - [Examples][29] 38 | - [filter][30] 39 | - [Parameters][31] 40 | - [Examples][32] 41 | 42 | ## Node 43 | 44 | Node 45 | 46 | Type: [Object][33] 47 | 48 | ### Properties 49 | 50 | - `name` **[string][34]** 51 | - `type` **(directory | file)** 52 | - `meta` **[Object][33]** – custom information 53 | 54 | ## mkfile 55 | 56 | Make file node 57 | 58 | ### Parameters 59 | 60 | - `name` **[string][34]** 61 | - `meta` (optional, default `{}`) 62 | 63 | ### Examples 64 | 65 | ```javascript 66 | mkfile('config.json'); 67 | // { 68 | // name: 'config.json', 69 | // meta: {}, 70 | // type: 'file', 71 | // } 72 | 73 | mkfile('config.json', { size: 1200 }); 74 | // { 75 | // name: 'config.json', 76 | // meta: { size: 1200 }, 77 | // type: 'file', 78 | // } 79 | ``` 80 | 81 | Returns **any** Node 82 | 83 | ## mkdir 84 | 85 | Make directory node 86 | 87 | ### Parameters 88 | 89 | - `name` **[string][34]** 90 | - `children` **[Array][35]<[Object][33]>** (optional, default `[]`) 91 | - `meta` (optional, default `{}`) 92 | 93 | ### Examples 94 | 95 | ```javascript 96 | mkdir('etc'); 97 | // { 98 | // name: 'etc', 99 | // children: [], 100 | // meta: {}, 101 | // type: 'directory', 102 | // } 103 | 104 | mkdir('etc', [mkfile('config'), mkfile('hosts')], { owner: 'user' }); 105 | // { 106 | // name: 'etc', 107 | // children: [ 108 | // { name: 'config', meta: {}, type: 'file' }, 109 | // { name: 'hosts', meta: {}, type: 'file' } 110 | // ], 111 | // meta: { owner: 'user' }, 112 | // type: 'directory', 113 | // } 114 | ``` 115 | 116 | ## getChildren 117 | 118 | Return children 119 | 120 | ### Parameters 121 | 122 | - `directory` 123 | 124 | ### Examples 125 | 126 | ```javascript 127 | getChildren(mkdir('etc')); // [] 128 | getChildren(mkdir('etc', [mkfile('name')])); // [] 129 | ``` 130 | 131 | ## getMeta 132 | 133 | Return meta 134 | 135 | ### Parameters 136 | 137 | - `node` 138 | 139 | ### Examples 140 | 141 | ```javascript 142 | getMeta(mkfile('etc')); // {} 143 | getMeta(mkfile('etc', { owner: 'root' })); // { owner: 'root' } 144 | ``` 145 | 146 | ## getName 147 | 148 | Return name 149 | 150 | ### Parameters 151 | 152 | - `node` 153 | 154 | ### Examples 155 | 156 | ```javascript 157 | getName(mkfile('etc')); // etc 158 | getName(mkdir('/')); // / 159 | ``` 160 | 161 | ## isFile 162 | 163 | Check is node a file 164 | 165 | ### Parameters 166 | 167 | - `node` 168 | 169 | ### Examples 170 | 171 | ```javascript 172 | isFile(mkfile('config')); // true 173 | isFile(mkdir('etc')); // false 174 | ``` 175 | 176 | ## isDirectory 177 | 178 | Check is node a directory 179 | 180 | ### Parameters 181 | 182 | - `node` 183 | 184 | ### Examples 185 | 186 | ```javascript 187 | isDirectory(mkdir('etc')); // true 188 | isDirectory(mkfile('config')); // false 189 | ``` 190 | 191 | ## map 192 | 193 | Map tree 194 | 195 | ### Parameters 196 | 197 | - `callbackFn` 198 | - `tree` 199 | 200 | ### Examples 201 | 202 | ```javascript 203 | const tree = mkdir('etc', [mkfile('config'), mkfile('hosts')]); 204 | 205 | const callbackFn = (node) => { 206 | const { name } = node; 207 | const newName = name.toUpperCase(); 208 | return { ...node, name: newName }; 209 | }; 210 | 211 | map(callbackFn, tree); 212 | // { 213 | // name: 'ETC', 214 | // children: [ 215 | // { name: 'CONFIG', meta: {}, type: 'file' }, 216 | // { name: 'HOSTS', meta: {}, type: 'file' } 217 | // ], 218 | // meta: {}, 219 | // type: 'directory', 220 | // } 221 | ``` 222 | 223 | ## reduce 224 | 225 | Reduce tree 226 | 227 | ### Parameters 228 | 229 | - `callbackFn` 230 | - `tree` 231 | - `acc` 232 | 233 | ### Examples 234 | 235 | ```javascript 236 | const tree = mkdir('etc', [mkfile('config'), mkfile('hosts')]); 237 | 238 | reduce((acc) => acc + 1, tree, 0); 239 | // 3 240 | 241 | reduce((acc, node) => [...acc, node.name], tree, []); 242 | // ['etc', 'config', 'hosts'] 243 | ``` 244 | 245 | ## filter 246 | 247 | Filter tree 248 | 249 | ### Parameters 250 | 251 | - `callbackFn` 252 | - `tree` 253 | 254 | ### Examples 255 | 256 | ```javascript 257 | const tree = mkdir('etc', [mkfile('CONFIG'), mkfile('hosts')]); 258 | 259 | const callbackFn = (node) => { 260 | const { name } = node; 261 | return name === name.toLowerCase(); 262 | }; 263 | 264 | filter(callbackFn, tree); 265 | // { 266 | // name: 'etc', 267 | // children: [ 268 | // { name: 'hosts', meta: {}, type: 'file' } 269 | // ], 270 | // meta: {}, 271 | // type: 'directory', 272 | // } 273 | ``` 274 | 275 | [1]: #node 276 | 277 | [2]: #properties 278 | 279 | [3]: #mkfile 280 | 281 | [4]: #parameters 282 | 283 | [5]: #examples 284 | 285 | [6]: #mkdir 286 | 287 | [7]: #parameters-1 288 | 289 | [8]: #examples-1 290 | 291 | [9]: #getchildren 292 | 293 | [10]: #parameters-2 294 | 295 | [11]: #examples-2 296 | 297 | [12]: #getmeta 298 | 299 | [13]: #parameters-3 300 | 301 | [14]: #examples-3 302 | 303 | [15]: #getname 304 | 305 | [16]: #parameters-4 306 | 307 | [17]: #examples-4 308 | 309 | [18]: #isfile 310 | 311 | [19]: #parameters-5 312 | 313 | [20]: #examples-5 314 | 315 | [21]: #isdirectory 316 | 317 | [22]: #parameters-6 318 | 319 | [23]: #examples-6 320 | 321 | [24]: #map 322 | 323 | [25]: #parameters-7 324 | 325 | [26]: #examples-7 326 | 327 | [27]: #reduce 328 | 329 | [28]: #parameters-8 330 | 331 | [29]: #examples-8 332 | 333 | [30]: #filter 334 | 335 | [31]: #parameters-9 336 | 337 | [32]: #examples-9 338 | 339 | [33]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object 340 | 341 | [34]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String 342 | 343 | [35]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array 344 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Node 5 | * @typedef {Object} Node 6 | * @property {string} name 7 | * @property {(directory | file)} type 8 | * @property {Object} meta – custom information 9 | */ 10 | 11 | /** 12 | * Make file node 13 | * @param {string} name 14 | * @returns Node 15 | * @example 16 | * mkfile('config.json'); 17 | * // { 18 | * // name: 'config.json', 19 | * // meta: {}, 20 | * // type: 'file', 21 | * // } 22 | * 23 | * mkfile('config.json', { size: 1200 }); 24 | * // { 25 | * // name: 'config.json', 26 | * // meta: { size: 1200 }, 27 | * // type: 'file', 28 | * // } 29 | */ 30 | export const mkfile = (name, meta = {}) => ({ 31 | name, 32 | meta, 33 | type: 'file', 34 | }); 35 | 36 | /** 37 | * Make directory node 38 | * 39 | * @param {string} name 40 | * @param {Object[]} children 41 | * @example 42 | * mkdir('etc'); 43 | * // { 44 | * // name: 'etc', 45 | * // children: [], 46 | * // meta: {}, 47 | * // type: 'directory', 48 | * // } 49 | * 50 | * mkdir('etc', [mkfile('config'), mkfile('hosts')], { owner: 'user' }); 51 | * // { 52 | * // name: 'etc', 53 | * // children: [ 54 | * // { name: 'config', meta: {}, type: 'file' }, 55 | * // { name: 'hosts', meta: {}, type: 'file' } 56 | * // ], 57 | * // meta: { owner: 'user' }, 58 | * // type: 'directory', 59 | * // } 60 | */ 61 | export const mkdir = (name, children = [], meta = {}) => ({ 62 | name, 63 | children, 64 | meta, 65 | type: 'directory', 66 | }); 67 | 68 | /** 69 | * Return children 70 | * 71 | * @example 72 | * getChildren(mkdir('etc')); // [] 73 | * getChildren(mkdir('etc', [mkfile('name')])); // [] 74 | */ 75 | export const getChildren = (directory) => directory.children; 76 | 77 | /** 78 | * Return meta 79 | * @example 80 | * getMeta(mkfile('etc')); // {} 81 | * getMeta(mkfile('etc', { owner: 'root' })); // { owner: 'root' } 82 | */ 83 | export const getMeta = (node) => node.meta; 84 | 85 | /** 86 | * Return name 87 | * 88 | * @example 89 | * getName(mkfile('etc')); // etc 90 | * getName(mkdir('/')); // / 91 | */ 92 | export const getName = (node) => node.name; 93 | 94 | /** 95 | * Check is node a file 96 | * @example 97 | * isFile(mkfile('config')); // true 98 | * isFile(mkdir('etc')); // false 99 | */ 100 | export const isFile = (node) => node.type === 'file'; 101 | 102 | /** 103 | * Check is node a directory 104 | * @example 105 | * isDirectory(mkdir('etc')); // true 106 | * isDirectory(mkfile('config')); // false 107 | */ 108 | export const isDirectory = (node) => node.type === 'directory'; 109 | 110 | /** 111 | * Map tree 112 | * @example 113 | * const tree = mkdir('etc', [mkfile('config'), mkfile('hosts')]); 114 | * 115 | * const callbackFn = (node) => { 116 | * const { name } = node; 117 | * const newName = name.toUpperCase(); 118 | * return { ...node, name: newName }; 119 | * }; 120 | * 121 | * map(callbackFn, tree); 122 | * // { 123 | * // name: 'ETC', 124 | * // children: [ 125 | * // { name: 'CONFIG', meta: {}, type: 'file' }, 126 | * // { name: 'HOSTS', meta: {}, type: 'file' } 127 | * // ], 128 | * // meta: {}, 129 | * // type: 'directory', 130 | * // } 131 | */ 132 | export const map = (callbackFn, tree) => { 133 | const updatedNode = callbackFn(tree); 134 | 135 | return isDirectory(tree) 136 | ? { ...updatedNode, children: tree.children.map((n) => map(callbackFn, n)) } 137 | : updatedNode; 138 | }; 139 | 140 | /** 141 | * Reduce tree 142 | * @example 143 | * const tree = mkdir('etc', [mkfile('config'), mkfile('hosts')]); 144 | * 145 | * reduce((acc) => acc + 1, tree, 0); 146 | * // 3 147 | * 148 | * reduce((acc, node) => [...acc, node.name], tree, []); 149 | * // ['etc', 'config', 'hosts'] 150 | */ 151 | export const reduce = (callbackFn, tree, acc) => { 152 | const newAcc = callbackFn(acc, tree); 153 | 154 | if (isFile(tree)) { 155 | return newAcc; 156 | } 157 | return tree.children.reduce((iAcc, n) => reduce(callbackFn, n, iAcc), newAcc); 158 | }; 159 | 160 | /** 161 | * Filter tree 162 | * @example 163 | * const tree = mkdir('etc', [mkfile('CONFIG'), mkfile('hosts')]); 164 | * 165 | * const callbackFn = (node) => { 166 | * const { name } = node; 167 | * return name === name.toLowerCase(); 168 | * }; 169 | * 170 | * filter(callbackFn, tree); 171 | * // { 172 | * // name: 'etc', 173 | * // children: [ 174 | * // { name: 'hosts', meta: {}, type: 'file' } 175 | * // ], 176 | * // meta: {}, 177 | * // type: 'directory', 178 | * // } 179 | */ 180 | export const filter = (callbackFn, tree) => { 181 | if (!callbackFn(tree)) { 182 | return null; 183 | } 184 | 185 | return isDirectory(tree) 186 | ? { ...tree, children: tree.children.map((n) => filter(callbackFn, n)).filter((v) => v) } 187 | : tree; 188 | }; 189 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hexlet/immutable-fs-trees", 3 | "version": "0.7.2", 4 | "description": "FileSystem Tree", 5 | "type": "module", 6 | "exports": "./index.js", 7 | "scripts": { 8 | "documentation": "documentation", 9 | "test": "NODE_OPTIONS=--experimental-vm-modules npx jest" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/hexlet-components/js-immutable-fs-trees.git" 14 | }, 15 | "author": "Kirill Mokevnin", 16 | "license": "ISC", 17 | "bugs": { 18 | "url": "https://github.com/hexlet-components/js-immutable-fs-trees/issues" 19 | }, 20 | "homepage": "https://github.com/hexlet-components/js-immutable-fs-trees#readme", 21 | "devDependencies": { 22 | "eslint": "^8.16.0", 23 | "eslint-config-airbnb-base": "^15.0.0", 24 | "eslint-plugin-import": "^2.26.0", 25 | "eslint-plugin-jest": "^26.2.2", 26 | "jest": "^28.1.0", 27 | "jest-cli": "^28.1.0", 28 | "documentation": "^14.0.0" 29 | } 30 | } 31 | --------------------------------------------------------------------------------