├── .gitignore ├── .jshintrc ├── .travis.yml ├── CHANGELOG.md ├── README.md ├── appveyor.yml ├── lib ├── entry.ts ├── index.ts └── util.ts ├── package.json ├── tests ├── entry-test.ts ├── fs-tree-test.ts └── util-test.ts ├── tsconfig.json ├── types ├── heimdalljs-logger.d.ts └── path-posix.d.ts └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | tmp 3 | *.js 4 | *.d.ts 5 | !types/ -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": { 3 | "console": true, 4 | "it": true, 5 | "describe": true, 6 | "beforeEach": true, 7 | "afterEach": true, 8 | "before": true, 9 | "after": true 10 | }, 11 | "proto": true, 12 | "strict": true, 13 | "indent": 2, 14 | "camelcase": true, 15 | "node": true, 16 | "browser": false, 17 | "boss": true, 18 | "curly": true, 19 | "latedef": "nofunc", 20 | "debug": false, 21 | "devel": false, 22 | "eqeqeq": true, 23 | "evil": true, 24 | "forin": false, 25 | "immed": false, 26 | "laxbreak": false, 27 | "newcap": true, 28 | "noarg": true, 29 | "noempty": false, 30 | "quotmark": true, 31 | "nonew": false, 32 | "nomen": false, 33 | "onevar": false, 34 | "plusplus": false, 35 | "regexp": false, 36 | "undef": true, 37 | "unused": "vars", 38 | "sub": true, 39 | "trailing": true, 40 | "white": false, 41 | "eqnull": true 42 | } 43 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "6" 5 | - "8" 6 | - "10" 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # master 2 | 3 | # 2.0.0 4 | 5 | * port to typescript 6 | 7 | # 1.0.2 8 | 9 | * fix type module export thanks @dfreeman 10 | 11 | # 1.0.1 12 | 13 | * fix errors in type definitions thanks @dfreeman 14 | 15 | # 1.0.0 16 | 17 | * no changes 18 | 19 | # v0.5.9 20 | 21 | * publish typescript types thanks @amiller-gh 22 | 23 | # v0.5.8 24 | 25 | * add typescript types thanks @amiller-gh 26 | 27 | # v0.5.7 28 | 29 | * avoid errors when attempting to unlink a file that has already been removed. 30 | 31 | # v0.5.5 32 | 33 | * add `Entry.fromStat` thanks @trentmwillis 34 | * add `applyPatch` and `calculateAndApplyPatch` thanks @trentmwillis 35 | 36 | # v0.5.4 37 | 38 | * Fix remove-before-add bug. Thanks @dfreeman for excellent bug investigation 39 | 40 | # v0.5.3 41 | 42 | * Add `FSTree.prototype.addEntries` thanks @chriseppstein 43 | 44 | # v0.5.2 45 | 46 | * bump version of heimdalljs-logger 47 | 48 | # v0.5.1 49 | 50 | * replace direct use of debug with heimdalljs-logger 51 | 52 | # v0.5.0 53 | 54 | * [BREAKING] drop `unlinkdir` and `linkDir` as operations. Downstream can 55 | implement this by examining entries, eg by marking them beforehand as 56 | broccoli-merge-trees does. 57 | * [BREAKING] `unlink` and `rmdir` operations are now passed the entry 58 | * [BREAKING] entries must be lexigraphicaly sorted by relative path. To do this 59 | implicitly, use `sortAndExpand`. 60 | * [BREAKING] entries must include intermediate directories. To do this 61 | implicitly, use `sortAndExpand`. 62 | * reworked implementation to diff via two sorted arrays 63 | * performance improvements 64 | * return entires as-provided, preserving user-specified metadata 65 | * directories in patches always end with a trailing slash 66 | * fixes various issues related to directory state transitions 67 | * directories can now receive `change` patches if user-supplied `meta` has 68 | property changes 69 | 70 | # v0.4.4 71 | 72 | * throw errors on duplicate entries (previous behavior was unspecified) 73 | 74 | # v0.4.2 75 | 76 | * coerce time to number before comparison 77 | 78 | # v0.4.1 79 | 80 | * add `:` in debug namespace for ecosystem consistency 81 | 82 | # v0.4.0 83 | 84 | * initial release 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fs-tree-diff [![Build Status](https://travis-ci.org/stefanpenner/fs-tree-diff.svg?branch=master)](https://travis-ci.org/stefanpenner/fs-tree-diff) [![Build status](https://ci.appveyor.com/api/projects/status/qmhx48hrquq08fam/branch/master?svg=true)](https://ci.appveyor.com/project/embercli/fs-tree-diff/branch/master) 2 | 3 | 4 | FSTree provides the means to calculate a patch (set of operations) between one file system tree and another. 5 | 6 | The possible operations are: 7 | 8 | * `unlink` – remove the specified file 9 | * `rmdir` – remove the specified folder 10 | * `mkdir` – create the specified folder 11 | * `create` – create the specified file 12 | * `change` – update the specified file to reflect changes 13 | 14 | The operations chosen aim to minimize the amount of IO required to apply a given patch. 15 | For example, a naive `rm -rf` of a directory tree is actually quite costly, as child directories 16 | must be recursively traversed, entries stated.. etc, all to figure out what first must be deleted. 17 | Since we patch from tree to tree, discovering new files is both wasteful and un-needed. 18 | 19 | The operations will also be provided in a correct order, allowing us to safely 20 | replay operations without having to first confirm the FS is as we expect. For 21 | example, `unlink`s for files will occur before a `rmdir` of those files' parent 22 | dir. Although the ordering will be safe, a specific order is not guaranteed. 23 | 24 | A simple example: 25 | 26 | ```js 27 | const FSTree = require('fs-tree-diff'); 28 | const current = FSTree.fromPaths([ 29 | 'a.js' 30 | ]); 31 | 32 | const next = FSTree.fromPaths([ 33 | 'b.js' 34 | ]); 35 | 36 | current.calculatePatch(next) === [ 37 | ['unlink', 'a.js', entryA], 38 | ['create', 'b.js', entryB] 39 | ]; 40 | ``` 41 | 42 | A slightly more complicated example: 43 | 44 | ```js 45 | const FSTree = require('fs-tree-diff'); 46 | const current = FSTree.fromPaths([ 47 | 'a.js', 48 | 'b/', 49 | 'b/f.js' 50 | ]); 51 | 52 | const next = FSTree.fromPaths([ 53 | 'b.js', 54 | 'b/', 55 | 'b/c/', 56 | 'b/c/d.js', 57 | 'b/e.js' 58 | ]); 59 | 60 | current.calculatePatch(next) === [ 61 | ['unlink', 'a.js', entryA], 62 | ['create', 'b.js', entryB], 63 | ['mkdir', 'b/c', entryBC], 64 | ['create', 'b/c/d.js', entryBCD], 65 | ['create', 'b/e.js', entryBE] 66 | ['unlink', 'b/f.js', entryBF], 67 | ] 68 | ``` 69 | 70 | Now, the above examples do not demonstrate `change` operations. This is because 71 | when providing only paths, we do not have sufficient information to check if 72 | one entry is merely different from another with the same relativePath. 73 | 74 | For this, FSTree supports more complex input structure. To demonstrate, we 75 | will use the [walk-sync](https://github.com/joliss/node-walk-sync) module, 76 | which provides higher fidelity input, allowing FSTree to also detect changes. 77 | (See also the documentation for 78 | [walkSync.entries](https://github.com/joliss/node-walk-sync#entries).) 79 | 80 | ```js 81 | const walkSync = require('walk-sync'); 82 | 83 | // path/to/root/foo.js 84 | // path/to/root/bar.js 85 | const current = new FSTree({ 86 | entries: walkSync.entries('path/to/root') 87 | }); 88 | 89 | writeFileSync('path/to/root/foo.js', 'new content'); 90 | writeFileSync('path/to/root/baz.js', 'new file'); 91 | 92 | const next = new FSTree({ 93 | entries: walkSync.entries('path/to/root') 94 | }); 95 | 96 | current.calculatePatch(next) === [ 97 | ['change', 'foo.js', entryFoo], // mtime + size changed, so this input is stale and needs updating. 98 | ['create', 'baz.js', entryBaz] // new file, so we should create it 99 | /* bar stays the same and is left inert*/ 100 | ]; 101 | ``` 102 | 103 | The entry objects provided depend on the operation. For `rmdir` and `unlink` 104 | operations, the current entry is provided. For `mkdir`, `change` and `create` 105 | operations the new entry is provided. 106 | 107 | ## API 108 | 109 | The public API is: 110 | 111 | - `FSTree.fromPaths` initialize a tree from an array of string paths. 112 | - `FSTree.fromEntries` initialize a tree from an array of `Entry` objects. 113 | Each entry must have the following properties (but may have more): 114 | 115 | - `relativePath` 116 | - `mode` 117 | - `size` 118 | - `mtime` 119 | - `FSTree.applyPatch(inputDir, outputDir, patch, delegate)` applies the given 120 | patch from the input directory to the output directory. You can optionally 121 | provide a delegate object to handle individual types of patch operations. 122 | - `FSTree.prototype.calculatePatch(newTree, isEqual)` calculate a patch against 123 | `newTree`. Optionally specify a custom `isEqual` (see Change Calculation). 124 | - `FSTree.prototype.calculateAndApplyPatch(newTree, inputDir, outputDir, delegate)` 125 | does a `calculatePatch` followed by `applyPatch`. 126 | - `FSTree.prototype.addEntries(entries, options)` adds entries to an 127 | existing tree. Options are the same as for `FSTree.fromEntries`. 128 | Entries added with the same path will overwrite any existing entries. 129 | - `FSTree.prototype.addPaths(paths, options)` adds paths to an 130 | existing tree. Options are the same as for `FSTree.fromPaths`. 131 | If entries already exist for any of the paths added, those entries will 132 | be updated. 133 | - `Entry.fromStat(relativePath, stat)` creates an `Entry` from a given path and 134 | [`fs.Stats`](https://nodejs.org/api/fs.html#fs_class_fs_stats) object. It can 135 | then be used with `fromEntries` or `addEntries`. 136 | 137 | 138 | The trees returned from `fromPaths` and `fromEntries` are relative to some base 139 | directory. `calculatePatch`, `applyPatch` and `calculateAndApplyPatch` all 140 | assume that the base directory has not changed. 141 | 142 | ## Input 143 | 144 | `FSTree.fromPaths`, `FSTree.fromEntries`, `FSTree.prototype.addPaths`, 145 | and `FSTree.prototype.addEntries` all validate their inputs. Inputs 146 | must be sorted, path-unique (i.e. two entries with the same `relativePath` but 147 | different `size`s would still be illegal input) and include intermediate 148 | directories. 149 | 150 | For example, the following input is **invalid** 151 | 152 | ```js 153 | FSTree.fromPaths([ 154 | // => missing a/ and a/b/ 155 | 'a/b/c.js' 156 | ]); 157 | ``` 158 | 159 | To have FSTree sort and expand (include intermediate directories) for you, add 160 | the option `sortAndExpand`). 161 | 162 | ```js 163 | FStree.fromPaths([ 164 | 'a/b/q/r/bar.js', 165 | 'a/b/c/d/foo.js', 166 | ], { sortAndExpand: true }); 167 | 168 | // The above is equivalent to 169 | 170 | FSTree.fromPaths([ 171 | 'a/', 172 | 'a/b/', 173 | 'a/b/c/', 174 | 'a/b/c/d/', 175 | 'a/b/c/d/foo.js', 176 | 'a/b/q/', 177 | 'a/b/q/r/', 178 | 'a/b/q/r/bar.js', 179 | ]); 180 | ``` 181 | 182 | ## Entry 183 | 184 | `FSTree.fromEntries` requires you to supply your own `Entry` objects. Your 185 | entry objects **must** contain the following properties: 186 | 187 | - `relativePath` 188 | - `mode` 189 | - `size` 190 | - `mtime` 191 | 192 | They must also implement the following API: 193 | 194 | - `isDirectory()` `true` *iff* this entry is a directory 195 | 196 | `FSTree.fromEntries` composes well with the output of `walkSync.entries`: 197 | 198 | ```js 199 | const walkSync = require('walk-sync'); 200 | 201 | // path/to/root/foo.js 202 | // path/to/root/bar.js 203 | const current = FSTree.fromEntries(walkSync.entries('path/to/root')); 204 | ``` 205 | 206 | ## Change Calculation 207 | 208 | When a prior entry has a `relativePath` that matches that of a current entry, a 209 | change operation is included if the new entry is different from the previous 210 | entry. This is determined by calling `isEqual`, the optional second argument 211 | to `calculatePatch`. If no `isEqual` is provided, a default `isEqual` is used. 212 | 213 | The default `isEqual` treats directories as always equal and files as different 214 | if any of the following properties have changed. 215 | 216 | - `mode` 217 | - `size` 218 | - `mtime` 219 | 220 | User specified `isEqual` will often want to use the default `isEqual`, so it is exported on `FSTree`. 221 | 222 | Example 223 | 224 | ```js 225 | const defaultIsEqual = FSTree.defaultIsEqual; 226 | 227 | function isEqualCheckingMeta(a, b) { 228 | return defaultIsEqual(a, b) && isMetaEqual(a, b); 229 | } 230 | 231 | function isMetaEqual(a, b) { 232 | // ... 233 | } 234 | ``` 235 | 236 | ## Patch Application 237 | 238 | When you want to apply changes from one tree to another easily, you can use the 239 | `FSTree.applyPatch` method. For example, given: 240 | 241 | ```js 242 | const patch = oldInputTree.calculatePatch(newInputTree); 243 | const inputDir = 'src'; 244 | const outputDir = 'dist'; 245 | FSTree.applyPatch(inputDir, outputDir, patch); 246 | ``` 247 | 248 | It will apply the patch changes to `dist` while using `src` as a reference for 249 | non-destructive operations (`mkdir`, `create`, `change`). If you want to calculate 250 | and apply a patch without any intermediate operations, you can do: 251 | 252 | ```js 253 | const inputDir = 'src'; 254 | const outputDir = 'dist'; 255 | oldInputTree.calculateAndApplyPatch(newInputTree, inputDir, outputDir); 256 | ``` 257 | 258 | You can optionally provide a delegate object to handle applying specific types 259 | of operations: 260 | 261 | ```js 262 | let createCount = 0; 263 | FSTree.applyPatch(inputDir, outputDir, patch, { 264 | create: function(inputPath, outputPath, relativePath) { 265 | createCount++; 266 | copy(inputPath, outputPath); 267 | } 268 | }); 269 | ``` 270 | 271 | The available delegate functions are the same as the supported operations: 272 | `unlink`, `rmdir`, `mkdir`, `create`, and `change`. Each delegate function 273 | receives the reference `inputPath`, the `outputPath`, and `relativePath` of the file 274 | or directory for which to apply the operation. 275 | 276 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # https://www.appveyor.com/docs/appveyor-yml/ 2 | 3 | # Test against these versions of Node.js. 4 | environment: 5 | MOCHA_REPORTER: "mocha-appveyor-reporter" 6 | matrix: 7 | - nodejs_version: "6" 8 | - nodejs_version: "8" 9 | - nodejs_version: "10" 10 | 11 | # Install scripts. (runs after repo cloning) 12 | install: 13 | - git rev-parse HEAD 14 | # Get the latest stable version of Node 0.STABLE.latest 15 | - ps: Install-Product node $env:nodejs_version 16 | - npm install mocha-appveyor-reporter 17 | # Typical npm stuff. 18 | - npm version 19 | - npm install 20 | 21 | cache: 22 | - '%APPDATA%\npm-cache' 23 | 24 | 25 | # Post-install test scripts. 26 | test_script: 27 | # Output useful info for debugging. 28 | - npm version 29 | - cmd: npm run test 30 | 31 | # Don't actually build. 32 | build: off 33 | 34 | # Set build version format here instead of in the admin panel. 35 | version: "{build}" 36 | -------------------------------------------------------------------------------- /lib/entry.ts: -------------------------------------------------------------------------------- 1 | const DIRECTORY_MODE = 16877; 2 | 3 | import fs = require('fs'); 4 | export interface BaseEntry { 5 | relativePath: string; 6 | isDirectory() : boolean; 7 | } 8 | 9 | export interface DefaultEntry extends BaseEntry { 10 | relativePath: string; 11 | mode?: number; 12 | size?: number; 13 | mtime?: number | Date; // All algorithms coerce to number 14 | isDirectory() : boolean; 15 | } 16 | 17 | export default class Entry implements DefaultEntry { 18 | relativePath: string; 19 | mode?: number; 20 | size?: number; 21 | mtime?: number | Date; // All algorithms coerce to number 22 | 23 | constructor(relativePath:string, size?:number, mtime?: number | Date, mode?: number) { 24 | if (mode === undefined) { 25 | const isDirectory = relativePath.charAt(relativePath.length - 1) === '/'; 26 | this.mode = isDirectory ? DIRECTORY_MODE : 0; 27 | } else { 28 | const modeType = typeof mode; 29 | if (modeType !== 'number') { 30 | throw new TypeError(`Expected 'mode' to be of type 'number' but was of type '${modeType}' instead.`); 31 | } 32 | this.mode = mode; 33 | } 34 | 35 | if (mtime !== undefined) { 36 | this.mtime = mtime; 37 | } 38 | 39 | this.relativePath = relativePath; 40 | this.size = size; 41 | } 42 | 43 | static isDirectory(entry: Entry) { 44 | if (entry.mode === undefined) { 45 | return false 46 | } else { 47 | return (entry.mode & 61440) === 16384 48 | } 49 | } 50 | 51 | static isFile(entry: Entry) { 52 | return !this.isDirectory(entry); 53 | } 54 | 55 | static fromStat(relativePath: string, stat: fs.Stats) { 56 | return new this(relativePath, stat.size, stat.mtime, stat.mode); 57 | } 58 | 59 | isDirectory() { 60 | return (this.constructor as typeof Entry).isDirectory(this); 61 | } 62 | }; 63 | 64 | 65 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | import fs = require('fs'); 2 | import path = require('path-posix'); 3 | import symlinkOrCopy = require('symlink-or-copy'); 4 | import Logger = require('heimdalljs-logger'); 5 | 6 | import Entry, { DefaultEntry, BaseEntry } from './entry'; 7 | import { 8 | sortAndExpand, 9 | validateSortedUnique 10 | } from './util'; 11 | 12 | declare namespace FSTree { 13 | export type Operand = 'unlink' | 'rmdir' | 'create' | 'change' | 'mkdir'; 14 | export type Operation = [Operand, string, DefaultEntry ] | [ Operand, string]; 15 | export type Patch = Operation[]; 16 | export type Entry = import('./entry').DefaultEntry; 17 | 18 | export interface Options { 19 | entries?: BaseEntry[], 20 | sortAndExpand?: boolean 21 | } 22 | 23 | export interface StaticOptions { 24 | sortAndExpand?: boolean 25 | } 26 | 27 | interface PatchDelegate { 28 | unlink?(inputPath: string, outputPath: string, relativePath: string) : void; 29 | rmdir?(inputPath: string, outputPath: string, relativePath: string) : void; 30 | mkdir?(inputPath: string, outputPath: string, relativePath: string) : void; 31 | change?(inputPath: string, outputPath: string, relativePath: string) : void; 32 | create?(inputPath: string, outputPath: string, relativePath: string) : void; 33 | } 34 | } 35 | 36 | const logger = Logger('fs-tree-diff:'); 37 | const ARBITRARY_START_OF_TIME = 0; 38 | const DEFAULT_DELEGATE: FSTree.PatchDelegate = { 39 | unlink(inputPath: string, outputPath: string, relativePath: string) { 40 | try { 41 | fs.unlinkSync(outputPath); 42 | } catch (e) { 43 | if (typeof e === 'object' && e !== null && e.code === 'ENOENT') { 44 | return; 45 | } 46 | throw e; 47 | } 48 | }, 49 | rmdir(inputPath: string, outputPath: string, relativePath: string) { 50 | fs.rmdirSync(outputPath) 51 | }, 52 | mkdir(inputPath: string, outputPath: string, relativePath: string) { 53 | fs.mkdirSync(outputPath); 54 | }, 55 | change(inputPath: string, outputPath: string, relativePath: string) { 56 | // We no-op if the platform can symlink, because we assume the output path 57 | // is already linked via a prior create operation. 58 | if (symlinkOrCopy.canSymlink) { 59 | return; 60 | } 61 | 62 | fs.unlinkSync(outputPath); 63 | symlinkOrCopy.sync(inputPath, outputPath); 64 | }, 65 | create(inputPath: string, outputPath: string, relativePath: string) { 66 | symlinkOrCopy.sync(inputPath, outputPath); 67 | } 68 | }; 69 | 70 | class FSTree { 71 | entries: T[] 72 | constructor(options: { entries?: T[], sortAndExpand?: boolean } = {}) { 73 | const entries = options.entries || []; 74 | 75 | if (options.sortAndExpand) { 76 | sortAndExpand(entries); 77 | } else { 78 | validateSortedUnique(entries); 79 | } 80 | 81 | this.entries = entries; 82 | } 83 | 84 | static fromPaths(paths: string[], options: FSTree.StaticOptions = {}) { 85 | const entries = paths.map(path => { 86 | return new Entry(path, 0, ARBITRARY_START_OF_TIME); 87 | }); 88 | 89 | return new this({ 90 | entries: entries, 91 | sortAndExpand: options.sortAndExpand, 92 | }); 93 | } 94 | 95 | static fromEntries(entries: T[], options: FSTree.StaticOptions = {}) { 96 | return new this({ 97 | entries: entries, 98 | sortAndExpand: options.sortAndExpand, 99 | }); 100 | } 101 | 102 | get size() { 103 | return this.entries.length; 104 | } 105 | 106 | addEntries(entries: T[], options?: FSTree.StaticOptions) { 107 | if (!Array.isArray(entries)) { 108 | throw new TypeError('entries must be an array'); 109 | } 110 | if (options !== null && typeof options === 'object' && options.sortAndExpand) { 111 | sortAndExpand(entries); 112 | } else { 113 | validateSortedUnique(entries); 114 | } 115 | let fromIndex = 0; 116 | let toIndex = 0; 117 | while (fromIndex < entries.length) { 118 | while (toIndex < this.entries.length && 119 | this.entries[toIndex].relativePath < entries[fromIndex].relativePath) { 120 | toIndex++; 121 | } 122 | if (toIndex < this.entries.length && 123 | this.entries[toIndex].relativePath === entries[fromIndex].relativePath) { 124 | this.entries.splice(toIndex, 1, entries[fromIndex++]); 125 | } else { 126 | this.entries.splice(toIndex++, 0, entries[fromIndex++]); 127 | } 128 | } 129 | } 130 | 131 | addPaths(paths: string[], options?: FSTree.StaticOptions) { 132 | const entries = paths.map(path => { 133 | // TODO: 134 | // addPths + a custom isEqual comparator + custom Entry types are actually incompatible 135 | // As a addPaths will not abide by the custom Entry type 136 | // and will make this.entries be contain a mixture of types. 137 | // isEqual's arguments will then be typed incorrectly 138 | // 139 | // We should likely just deprecate `addPaths` in-favor of addEntries, 140 | // which correctly externalizes the creation of entry 141 | return new Entry(path, 0, ARBITRARY_START_OF_TIME) as T; 142 | }); 143 | 144 | this.addEntries(entries, options); 145 | } 146 | 147 | forEach(fn: (entry: T, index: number, collection: T[]) => void, context: any) { 148 | this.entries.forEach(fn, context); 149 | } 150 | 151 | calculatePatch(theirFSTree: FSTree, isEqual?: (a: T, b: K) => boolean): FSTree.Patch { 152 | if (arguments.length > 1 && typeof isEqual !== 'function') { 153 | throw new TypeError('calculatePatch\'s second argument must be a function'); 154 | } 155 | 156 | // TODO: the TS here is strange 157 | if (typeof isEqual !== 'function') { 158 | isEqual = (this.constructor as typeof FSTree).defaultIsEqual; 159 | } 160 | 161 | const ours = this.entries; 162 | const theirs = theirFSTree.entries; 163 | const additions: FSTree.Patch = []; 164 | const removals: FSTree.Patch = []; 165 | 166 | let i = 0; 167 | let j = 0; 168 | 169 | let command; 170 | 171 | while (i < ours.length && j < theirs.length) { 172 | let x = ours[i]; 173 | let y = theirs[j]; 174 | 175 | if (x.relativePath < y.relativePath) { 176 | // ours 177 | i++; 178 | 179 | removals.push(removeOperation(x)); 180 | 181 | // remove additions 182 | } else if (x.relativePath > y.relativePath) { 183 | // theirs 184 | j++; 185 | additions.push(addOperation(y)); 186 | } else { 187 | if (!isEqual(x, y)) { 188 | command = updateOperation(y); 189 | 190 | if (x.isDirectory()) { 191 | removals.push(command); 192 | } else { 193 | additions.push(command); 194 | } 195 | } 196 | // both are the same 197 | i++; j++; 198 | } 199 | } 200 | 201 | // cleanup ours 202 | for (; i < ours.length; i++) { 203 | removals.push(removeOperation(ours[i])); 204 | } 205 | 206 | // cleanup theirs 207 | for (; j < theirs.length; j++) { 208 | additions.push(addOperation(theirs[j])); 209 | } 210 | 211 | return removals.reverse().concat(additions); 212 | } 213 | 214 | calculateAndApplyPatch(otherFSTree: FSTree, input: string, output: string, delegate?: FSTree.PatchDelegate) { 215 | (this.constructor as typeof FSTree).applyPatch(input, output, this.calculatePatch(otherFSTree), delegate); 216 | } 217 | 218 | static defaultIsEqual(entryA: DefaultEntry, entryB: DefaultEntry) { 219 | if (entryA.isDirectory() && entryB.isDirectory()) { 220 | // ignore directory changes by default 221 | return true; 222 | } 223 | 224 | let equal; 225 | if (entryA.size === entryB.size && entryA.mode === entryB.mode) { 226 | if (entryA.mtime === entryB.mtime) { 227 | equal = true; 228 | } else if (entryA.mtime === undefined || entryB.mtime === undefined) { 229 | equal = false; 230 | } else if (+entryA.mtime === +entryB.mtime) { 231 | equal = true; 232 | } else { 233 | equal = false; 234 | } 235 | } else { 236 | equal = false; 237 | } 238 | 239 | if (equal === false) { 240 | logger.info('invalidation reason: \nbefore %o\n entryB %o', entryA, entryB); 241 | } 242 | 243 | return equal; 244 | } 245 | 246 | static applyPatch(input: string, output: string, patch: FSTree.Patch, _delegate?: FSTree.PatchDelegate) { 247 | const delegate = { 248 | ...DEFAULT_DELEGATE, 249 | ..._delegate 250 | }; 251 | for (let i = 0; i < patch.length; i++) { 252 | applyOperation(input, output, patch[i], delegate); 253 | } 254 | } 255 | } 256 | 257 | function applyOperation(input: string, output: string, operation: FSTree.Operation, delegate: FSTree.PatchDelegate) { 258 | const methodName = operation[0]; 259 | const relativePath = operation[1]; 260 | const inputPath = path.join(input, relativePath); 261 | const outputPath = path.join(output, relativePath); 262 | 263 | const method = delegate[methodName]; 264 | 265 | if (typeof method === 'function') { 266 | method(inputPath, outputPath, relativePath); 267 | } else { 268 | throw new Error('Unable to apply patch operation: ' + methodName + '. The value of delegate.' + methodName + ' is of type ' + typeof method + ', and not a function. Check the `delegate` argument to `FSTree.prototype.applyPatch`.'); 269 | } 270 | } 271 | 272 | function addOperation(entry: FSTree.Entry) : FSTree.Operation { 273 | return [ 274 | entry.isDirectory() ? 'mkdir' : 'create', 275 | entry.relativePath, 276 | entry 277 | ]; 278 | } 279 | 280 | function removeOperation(entry: FSTree.Entry) : FSTree.Operation { 281 | return [ 282 | entry.isDirectory() ? 'rmdir' : 'unlink', 283 | entry.relativePath, 284 | entry 285 | ]; 286 | } 287 | 288 | function updateOperation(entry: FSTree.Entry) : FSTree.Operation { 289 | return [ 290 | 'change', 291 | entry.relativePath, 292 | entry 293 | ]; 294 | }; 295 | 296 | export = FSTree; 297 | -------------------------------------------------------------------------------- /lib/util.ts: -------------------------------------------------------------------------------- 1 | import Entry from './entry'; 2 | 3 | export function validateSortedUnique(entries: Entry[]) { 4 | for (let i = 1; i < entries.length; i++) { 5 | let previous = entries[i - 1].relativePath; 6 | let current = entries[i].relativePath; 7 | 8 | if (previous < current) { 9 | continue; 10 | } else { 11 | throw new Error('expected entries[' + (i -1) + ']: `' + previous + 12 | '` to be < entries[' + i + ']: `' + current + '`, but was not. Ensure your input is sorted and has no duplicate paths'); 13 | } 14 | } 15 | } 16 | 17 | export function commonPrefix(a: string, b: string, term?: string) { 18 | let max = Math.min(a.length, b.length); 19 | let end = -1; 20 | 21 | for(var i = 0; i < max; ++i) { 22 | if (a[i] !== b[i]) { 23 | break; 24 | } else if (a[i] === term) { 25 | end = i; 26 | } 27 | } 28 | 29 | return a.substr(0, end + 1); 30 | } 31 | 32 | export function basename(entry: Entry) { 33 | const path = entry.relativePath; 34 | const end = path.length - 2; 35 | for (let i = end; i >= 0; --i) { 36 | if (path[i] === '/') { 37 | return path.substr(0, i + 1); 38 | } 39 | } 40 | 41 | return ''; 42 | } 43 | 44 | export function computeImpliedEntries(basePath: string, relativePath: string) { 45 | let rv = []; 46 | 47 | for (var i=0; i < relativePath.length; ++i) { 48 | if (relativePath[i] === '/') { 49 | let path = basePath + relativePath.substr(0, i + 1); 50 | rv.push(new Entry(path, 0, 0)); 51 | } 52 | } 53 | 54 | return rv; 55 | } 56 | 57 | export function compareByRelativePath(entryA: Entry, entryB: Entry) { 58 | const pathA = entryA.relativePath; 59 | const pathB = entryB.relativePath; 60 | 61 | if (pathA < pathB) { 62 | return -1; 63 | } else if (pathA > pathB) { 64 | return 1; 65 | } 66 | 67 | return 0; 68 | } 69 | 70 | export function sortAndExpand(entries: Entry[]) { 71 | entries.sort(compareByRelativePath); 72 | 73 | let path = ''; 74 | 75 | for (let i=0; i a/ 85 | // a/b -> a/ 86 | const base = basename(entry); 87 | // base - path 88 | const entryBaseSansCommon = base.substr(path.length); 89 | // determine what intermediate directories are missing eg 90 | // path = a/b/ 91 | // entryBaseSansCommon = c/d/e/ 92 | // impliedEntries = [a/b/c/, a/b/c/d/, a/b/c/d/e/] 93 | const impliedEntries = computeImpliedEntries(path, entryBaseSansCommon); 94 | 95 | // actually add our implied entries to entries 96 | if (impliedEntries.length > 0) { 97 | entries.splice(i, 0, ...impliedEntries); 98 | i += impliedEntries.length; 99 | } 100 | 101 | // update path. Now that we've created all the intermediate directories, we 102 | // don't need to recreate them for subsequent entries. 103 | if (entry.isDirectory()) { 104 | path = entry.relativePath; 105 | } else { 106 | path = base; 107 | } 108 | } 109 | 110 | return entries; 111 | } 112 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fs-tree-diff", 3 | "version": "2.0.1", 4 | "description": "Backs out file tree changes", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "files": [ 8 | "lib/**/*.js", 9 | "lib/**/*.d.ts" 10 | ], 11 | "scripts": { 12 | "test": "npm run test:js", 13 | "test:js": "mocha tests/*-test.js", 14 | "test:js:debug": "mocha debug tests/*-test.js", 15 | "build": "tsc", 16 | "prepublish": "tsc" 17 | }, 18 | "keywords": [ 19 | "broccoli" 20 | ], 21 | "author": "Stefan Penner, David J. Hamilton, Chad Hietala", 22 | "license": "MIT", 23 | "dependencies": { 24 | "@types/symlink-or-copy": "^1.2.0", 25 | "heimdalljs-logger": "^0.1.7", 26 | "object-assign": "^4.1.0", 27 | "path-posix": "^1.0.0", 28 | "symlink-or-copy": "^1.1.8" 29 | }, 30 | "devDependencies": { 31 | "@types/chai": "^4.1.7", 32 | "@types/fs-extra": "^5.0.4", 33 | "@types/mocha": "^5.2.5", 34 | "@types/node": "^10.12.21", 35 | "chai": "^3.3.0", 36 | "fs-extra": "^1.0.0", 37 | "mocha": "^2.3.3", 38 | "typescript": "^3.3.3", 39 | "walk-sync": "^0.3.1" 40 | }, 41 | "engines": { 42 | "node": "6.* || 8.* || >= 10.*" 43 | }, 44 | "repository": { 45 | "type": "git", 46 | "url": "git://github.com/stefanpenner/fs-tree-diff.git" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/entry-test.ts: -------------------------------------------------------------------------------- 1 | import fs = require('fs-extra'); 2 | import chai = require('chai'); 3 | import Entry from '../lib/entry'; 4 | 5 | const { expect } = chai; 6 | const FIXTURE_DIR = 'fixture'; 7 | 8 | require('chai').config.truncateThreshold = 0; 9 | 10 | describe('Entry', function() { 11 | describe('constructor', function() { 12 | const size = 1337; 13 | const mtime = Date.now(); 14 | 15 | it('supports omitting mode for files', function() { 16 | const entry = new Entry('/foo.js', size, mtime); 17 | 18 | expect(entry.relativePath).to.equal('/foo.js'); 19 | expect(entry.size).to.equal(size); 20 | expect(entry.mtime).to.equal(mtime); 21 | expect(entry.mode).to.equal(0); 22 | expect(entry.isDirectory()).to.not.be.ok; 23 | }); 24 | 25 | it('supports omitting mode for directories', function() { 26 | const entry = new Entry('/foo/', size, mtime); 27 | 28 | expect(entry.relativePath).to.equal('/foo/'); 29 | expect(entry.size).to.equal(size); 30 | expect(entry.mtime).to.equal(mtime); 31 | expect(entry.mode).to.equal(16877); 32 | expect(entry.isDirectory()).to.be.ok; 33 | }); 34 | 35 | it('supports including manually defined mode', function() { 36 | const entry = new Entry('/foo.js', size, mtime, 1); 37 | 38 | expect(entry.relativePath).to.equal('/foo.js'); 39 | expect(entry.size).to.equal(size); 40 | expect(entry.mtime).to.equal(mtime); 41 | expect(entry.mode).to.equal(1); 42 | expect(entry.isDirectory()).to.not.be.ok; 43 | }); 44 | 45 | it('errors on a non-number mode', function() { 46 | expect(function() { 47 | // @ts-ignore 48 | return new Entry('/foo.js', size, mtime, '1'); 49 | }).to.throw(`Expected 'mode' to be of type 'number' but was of type 'string' instead.`); 50 | }); 51 | }); 52 | 53 | describe('.fromStat', function() { 54 | afterEach(function() { 55 | fs.removeSync(FIXTURE_DIR); 56 | }); 57 | 58 | it('creates a correct entry for a file', function() { 59 | const path = FIXTURE_DIR + '/index.js'; 60 | 61 | fs.outputFileSync(path, ''); 62 | 63 | try { 64 | 65 | const stat = fs.statSync(path); 66 | const entry = Entry.fromStat(path, stat); 67 | 68 | expect(entry.isDirectory()).to.not.be.ok; 69 | expect(entry.mode).to.equal(stat.mode); 70 | expect(entry.size).to.equal(stat.size); 71 | expect(entry.mtime).to.equal(stat.mtime); 72 | expect(entry.relativePath).to.equal(path); 73 | } finally { 74 | fs.unlinkSync(path); 75 | } 76 | }); 77 | 78 | it('creates a correct entry for a directory', function() { 79 | const path = FIXTURE_DIR + '/foo/'; 80 | 81 | fs.mkdirpSync(path); 82 | 83 | const stat = fs.statSync(path); 84 | const entry = Entry.fromStat(path, stat); 85 | 86 | expect(entry.isDirectory()).to.be.ok; 87 | expect(entry.mode).to.equal(stat.mode); 88 | expect(entry.size).to.equal(stat.size); 89 | expect(entry.mtime).to.equal(stat.mtime); 90 | expect(entry.relativePath).to.equal(path); 91 | }); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /tests/fs-tree-test.ts: -------------------------------------------------------------------------------- 1 | import fs = require('fs-extra'); 2 | import path = require('path'); 3 | import chai = require('chai'); 4 | import walkSync = require('walk-sync'); 5 | import FSTree = require('../lib/index'); 6 | import Entry from '../lib/entry'; 7 | 8 | const { expect } = chai; 9 | const context = describe; 10 | const { defaultIsEqual } = FSTree; 11 | let fsTree: FSTree; 12 | 13 | require('chai').config.truncateThreshold = 0; 14 | 15 | describe('FSTree', function() { 16 | function merge(x: T, y: K) { 17 | return {...x, ...y }; 18 | } 19 | 20 | function metaIsEqual(a: Entry, b: Entry) { 21 | const aMeta = (a as any).meta; 22 | const bMeta = (b as any).meta; 23 | const metaKeys = aMeta ? Object.keys(aMeta) : []; 24 | const otherMetaKeys = bMeta ? Object.keys(bMeta) : []; 25 | 26 | if (metaKeys.length !== Object.keys(otherMetaKeys).length) { 27 | return false; 28 | } else { 29 | for (let i=0; i(property: keyof T) { 86 | return function pluckProperty(item: T) { 87 | return item[property]; 88 | }; 89 | } 90 | 91 | it('can be instantiated', function() { 92 | expect(new FSTree()).to.be.an.instanceOf(FSTree); 93 | }); 94 | 95 | describe('.fromPaths', function() { 96 | it('creates empty trees', function() { 97 | fsTree = FSTree.fromPaths([ ]); 98 | expect(fsTree.size).to.eq(0); 99 | }); 100 | 101 | describe('input validation', function() { 102 | it('throws on duplicate', function() { 103 | expect(function() { 104 | FSTree.fromPaths([ 105 | 'a', 106 | 'a', 107 | ]); 108 | }).to.throw('expected entries[0]: `a` to be < entries[1]: `a`, but was not. Ensure your input is sorted and has no duplicate paths'); 109 | }); 110 | 111 | it('throws on unsorted', function() { 112 | expect(function() { 113 | FSTree.fromPaths([ 114 | 'b', 115 | 'a', 116 | ]); 117 | }).to.throw('expected entries[0]: `b` to be < entries[1]: `a`, but was not. Ensure your input is sorted and has no duplicate paths'); 118 | }); 119 | }); 120 | 121 | describe('options', function() { 122 | describe('sortAndExpand', function() { 123 | it('sorts input entries', function() { 124 | fsTree = FSTree.fromPaths([ 125 | 'foo/', 126 | 'foo/a.js', 127 | 'bar/', 128 | 'bar/b.js', 129 | ], { sortAndExpand: true }); 130 | 131 | expect(fsTree.entries.map(by('relativePath'))).to.deep.equal([ 132 | 'bar/', 133 | 'bar/b.js', 134 | 'foo/', 135 | 'foo/a.js', 136 | ]); 137 | }); 138 | 139 | it('expands intermediate directories implied by input entries', function() { 140 | fsTree = FSTree.fromPaths([ 141 | 'a/b/q/r/bar.js', 142 | 'a/b/c/d/foo.js', 143 | ], { sortAndExpand: true }); 144 | 145 | expect(fsTree.entries).to.deep.equal([ 146 | directory('a/'), 147 | directory('a/b/'), 148 | directory('a/b/c/'), 149 | directory('a/b/c/d/'), 150 | file('a/b/c/d/foo.js'), 151 | directory('a/b/q/'), 152 | directory('a/b/q/r/'), 153 | file('a/b/q/r/bar.js'), 154 | ]); 155 | }); 156 | 157 | it('does not mutate its input', function() { 158 | const paths = [ 159 | 'foo/', 160 | 'foo/a.js', 161 | 'bar/', 162 | 'bar/b.js', 163 | ]; 164 | 165 | fsTree = FSTree.fromPaths(paths, { sortAndExpand: true }); 166 | 167 | expect(paths).to.deep.equal([ 168 | 'foo/', 169 | 'foo/a.js', 170 | 'bar/', 171 | 'bar/b.js', 172 | ]); 173 | }); 174 | }); 175 | }); 176 | 177 | it('creates trees from paths', function() { 178 | fsTree = FSTree.fromPaths([ 179 | 'a.js', 180 | 'foo/', 181 | 'foo/a.js', 182 | ]); 183 | 184 | const result = fsTree.calculatePatch( 185 | FSTree.fromPaths([ 186 | 'a.js', 187 | 'foo/', 188 | 'foo/b.js', 189 | ]) 190 | ); 191 | 192 | expect(result).to.deep.equal([ 193 | ['unlink', 'foo/a.js', file('foo/a.js')], 194 | ['create', 'foo/b.js', file('foo/b.js')] 195 | ]); 196 | }); 197 | }); 198 | 199 | describe('.fromEntries', function() { 200 | 201 | describe('input validation', function() { 202 | it('throws on duplicate', function() { 203 | expect(function() { 204 | FSTree.fromEntries([ 205 | file('a', { size: 1, mtime: 1 }), 206 | file('a', { size: 1, mtime: 2 }), 207 | ]); 208 | }).to.throw('expected entries[0]: `a` to be < entries[1]: `a`, but was not. Ensure your input is sorted and has no duplicate paths'); 209 | }); 210 | 211 | it('throws on unsorted', function() { 212 | expect(function() { 213 | FSTree.fromEntries([ 214 | file('b'), 215 | file('a'), 216 | ]); 217 | }).to.throw('expected entries[0]: `b` to be < entries[1]: `a`, but was not. Ensure your input is sorted and has no duplicate paths'); 218 | }); 219 | }); 220 | 221 | it('creates empty trees', function() { 222 | fsTree = FSTree.fromEntries([ ]); 223 | expect(fsTree.size).to.eq(0); 224 | }); 225 | 226 | it('creates tree from entries', function() { 227 | const fsTree = FSTree.fromEntries([ 228 | file('a/b.js', { size: 1, mtime: 1 }), 229 | file('a/c.js', { size: 1, mtime: 1 }), 230 | file('c/d.js', { size: 1, mtime: 1 }), 231 | ]); 232 | 233 | expect(fsTree.size).to.eq(3); 234 | 235 | const result = fsTree.calculatePatch(FSTree.fromEntries([ 236 | file('a/b.js', { size: 1, mtime: 2 }), 237 | file('a/c.js', { size: 1, mtime: 1 }), 238 | file('c/d.js', { size: 1, mtime: 1 }), 239 | ])); 240 | 241 | expect(result).to.deep.equal([ 242 | ['change', 'a/b.js', file('a/b.js', { mtime: 2, size: 1 })] 243 | ]); 244 | }); 245 | }); 246 | 247 | describe('adding new entries', function() { 248 | context(".addEntries", function() { 249 | context('input validation', function() { 250 | it('requires an array', function() { 251 | expect(function() { 252 | // @ts-ignore 253 | FSTree.fromPaths([]).addEntries(file('a.js')); 254 | }).to.throw(TypeError, 'entries must be an array'); 255 | }); 256 | 257 | it('throws on duplicate', function() { 258 | expect(function() { 259 | FSTree.fromEntries([]).addEntries([ 260 | // these are ignored, as they provide runtime errors for non typescript users 261 | // @ts-ignore 262 | file('a', { size: 1, mtime: 1 }), 263 | // @ts-ignore 264 | file('a', { size: 1, mtime: 2 }), 265 | ]); 266 | }).to.throw('expected entries[0]: `a` to be < entries[1]: `a`, but was not. Ensure your input is sorted and has no duplicate paths'); 267 | }); 268 | 269 | it('throws on unsorted', function() { 270 | expect(function() { 271 | FSTree.fromEntries([]).addEntries([ 272 | // these are ignored, as they provide runtime errors for non typescript users 273 | // @ts-ignore 274 | file('b'), 275 | // @ts-ignore 276 | file('a'), 277 | ]); 278 | }).to.throw('expected entries[0]: `b` to be < entries[1]: `a`, but was not. Ensure your input is sorted and has no duplicate paths'); 279 | }); 280 | }); 281 | 282 | it('inserts one file into sorted location', function() { 283 | fsTree = FSTree.fromPaths([ 284 | 'a.js', 285 | 'foo/', 286 | 'foo/a.js', 287 | ]); 288 | 289 | fsTree.addEntries([file('b.js', { size: 1, mtime: 1 })]); 290 | 291 | expect(fsTree.entries.map(by('relativePath'))).to.deep.equal([ 292 | 'a.js', 293 | 'b.js', 294 | 'foo/', 295 | 'foo/a.js', 296 | ]); 297 | }); 298 | 299 | it('inserts several entries', function() { 300 | fsTree = FSTree.fromPaths([ 301 | 'a.js', 302 | 'foo/', 303 | 'foo/a.js', 304 | ]); 305 | 306 | fsTree.addEntries([ 307 | file('bar/b.js', { size: 10, mtime: 10 }), 308 | file('1.js'), 309 | file('foo/bip/img.jpg'), 310 | ], {sortAndExpand: true}); 311 | 312 | expect(fsTree.entries.map(by('relativePath'))).to.deep.equal([ 313 | '1.js', 314 | 'a.js', 315 | 'bar/', 316 | 'bar/b.js', 317 | 'foo/', 318 | 'foo/a.js', 319 | 'foo/bip/', 320 | 'foo/bip/img.jpg', 321 | ]); 322 | }); 323 | 324 | it('replaces duplicates', function() { 325 | fsTree = FSTree.fromPaths([ 326 | 'a.js', 327 | 'foo/', 328 | 'foo/a.js', 329 | ]); 330 | 331 | fsTree.addEntries([file('foo/a.js', { size: 10 })], {sortAndExpand: true}); 332 | 333 | expect(fsTree.entries.map(by('relativePath'))).to.deep.equal([ 334 | 'a.js', 335 | 'foo/', 336 | 'foo/a.js', 337 | ]); 338 | }); 339 | }); 340 | 341 | context(".addPaths", function() { 342 | it("passes through to .addEntries", function() { 343 | fsTree = FSTree.fromPaths([ 344 | 'a.js', 345 | 'foo/', 346 | 'foo/a.js', 347 | ]); 348 | 349 | fsTree.addPaths([ 350 | 'bar/b.js', 351 | '1.js', 352 | 'foo/bip/img.jpg' 353 | ], {sortAndExpand: true}); 354 | 355 | expect(fsTree.entries.map(by('relativePath'))).to.deep.equal([ 356 | '1.js', 357 | 'a.js', 358 | 'bar/', 359 | 'bar/b.js', 360 | 'foo/', 361 | 'foo/a.js', 362 | 'foo/bip/', 363 | 'foo/bip/img.jpg', 364 | ]); 365 | }); 366 | }); 367 | }); 368 | 369 | describe('#calculatePatch', function() { 370 | context('input validation', function() { 371 | expect(function() { 372 | // @ts-ignore 373 | FSTree.fromPaths([]).calculatePatch(FSTree.fromPaths([]), ''); 374 | }).to.throw(TypeError, 'calculatePatch\'s second argument must be a function'); 375 | }); 376 | 377 | context('from an empty tree', function() { 378 | beforeEach( function() { 379 | fsTree = new FSTree(); 380 | }); 381 | 382 | context('to an empty tree', function() { 383 | it('returns 0 operations', function() { 384 | expect(fsTree.calculatePatch(FSTree.fromPaths([]))).to.deep.equal([]); 385 | }); 386 | }); 387 | 388 | context('to a non-empty tree', function() { 389 | it('returns n create operations', function() { 390 | expect(fsTree.calculatePatch(FSTree.fromPaths([ 391 | 'bar/', 392 | 'bar/baz.js', 393 | 'foo.js', 394 | ]))).to.deep.equal([ 395 | ['mkdir', 'bar/', directory('bar/')], 396 | ['create', 'bar/baz.js', file('bar/baz.js')], 397 | ['create', 'foo.js', file('foo.js')], 398 | ]); 399 | }); 400 | }); 401 | }); 402 | 403 | context('from a simple non-empty tree', function() { 404 | beforeEach( function() { 405 | fsTree = FSTree.fromPaths([ 406 | 'bar/', 407 | 'bar/baz.js', 408 | 'foo.js', 409 | ]); 410 | }); 411 | 412 | context('to an empty tree', function() { 413 | it('returns n rm operations', function() { 414 | expect(fsTree.calculatePatch(FSTree.fromPaths([]))).to.deep.equal([ 415 | ['unlink', 'foo.js', file('foo.js')], 416 | ['unlink', 'bar/baz.js', file('bar/baz.js')], 417 | ['rmdir', 'bar/', directory('bar/')], 418 | ]); 419 | }); 420 | }); 421 | }); 422 | 423 | context('FSTree with entries', function() { 424 | context('of files', function() { 425 | beforeEach(function() { 426 | fsTree = new FSTree({ 427 | entries: [ 428 | directory('a/'), 429 | file('a/b.js', { mode: 0o666, size: 1, mtime: 1 }), 430 | file('a/c.js', { mode: 0o666, size: 1, mtime: 1 }), 431 | directory('c/'), 432 | file('c/d.js', { mode: 0o666, size: 1, mtime: 1, meta: { rev: 0 } }) 433 | ] 434 | }); 435 | }); 436 | 437 | it('detects additions', function() { 438 | let result = fsTree.calculatePatch(new FSTree({ 439 | entries: [ 440 | directory('a/'), 441 | file('a/b.js', { mode: 0o666, size: 1, mtime: 1 }), 442 | file('a/c.js', { mode: 0o666, size: 1, mtime: 1 }), 443 | file('a/j.js', { mode: 0o666, size: 1, mtime: 1 }), 444 | directory('c/'), 445 | file('c/d.js', { mode: 0o666, size: 1, mtime: 1, meta: { rev: 0 } }), 446 | ] 447 | })); 448 | 449 | expect(result).to.deep.equal([ 450 | ['create', 'a/j.js', file('a/j.js', { mode: 0o666, size: 1, mtime: 1 })] 451 | ]); 452 | }); 453 | 454 | it('detects removals', function() { 455 | let result = fsTree.calculatePatch(new FSTree({ 456 | entries: [ 457 | directory('a/'), 458 | entry({ relativePath: 'a/b.js', mode: 0o666, size: 1, mtime: 1 }) 459 | ] 460 | })); 461 | 462 | expect(result).to.deep.equal([ 463 | ['unlink', 'c/d.js', file('c/d.js', { mode: 0o666, size: 1, mtime: 1, meta: { rev: 0 } })], 464 | ['rmdir', 'c/', directory('c/')], 465 | ['unlink', 'a/c.js', file('a/c.js', { mode: 0o666, size: 1, mtime: 1 })], 466 | ]); 467 | }); 468 | 469 | it('detects file updates', function() { 470 | let entries = [ 471 | directory('a/'), 472 | file('a/b.js', { mode: 0o666, size: 1, mtime: 2 }), 473 | file('a/c.js', { mode: 0o666, size: 10, mtime: 1 }), 474 | directory('c/'), 475 | file('c/d.js', { mode: 0o666, size: 1, mtime: 1, meta: { rev: 1 } }), 476 | ]; 477 | 478 | let result = fsTree.calculatePatch(new FSTree({ 479 | entries: entries 480 | }), userProvidedIsEqual); 481 | 482 | expect(result).to.deep.equal([ 483 | ['change', 'a/b.js', entries[1]], 484 | ['change', 'a/c.js', entries[2]], 485 | ['change', 'c/d.js', entries[4]], 486 | ]); 487 | }); 488 | 489 | it('detects directory updates from user-supplied meta', function () { 490 | let entries = [ 491 | directory('a/', { meta: { link: true } }), 492 | file('a/b.js', { mode: 0o666, size: 1, mtime: 1 }), 493 | file('a/c.js', { mode: 0o666, size: 1, mtime: 1 }), 494 | directory('c/'), 495 | file('c/d.js', { mode: 0o666, size: 1, mtime: 1, meta: { rev: 0 } }) 496 | ]; 497 | 498 | let result = fsTree.calculatePatch(new FSTree({ 499 | entries: entries 500 | }), userProvidedIsEqual); 501 | 502 | expect(result).to.deep.equal([ 503 | ['change', 'a/', entries[0]] 504 | ]); 505 | }); 506 | 507 | it('passes the rhs user-supplied entry on updates', function () { 508 | let bEntry = file('a/b.js', { 509 | mode: 0o666, size: 1, mtime: 2, meta: { link: true } 510 | }); 511 | let entries = [ 512 | directory('a/'), 513 | bEntry, 514 | file('a/c.js', { mode: 0o666, size: 1, mtime: 1 }), 515 | directory('c/'), 516 | file('c/d.js', { mode: 0o666, size: 1, mtime: 1, meta: { rev: 0 } }), 517 | ]; 518 | 519 | let result = fsTree.calculatePatch(new FSTree({ 520 | entries: entries 521 | })); 522 | 523 | expect(result).to.deep.equal([ 524 | ['change', 'a/b.js', bEntry], 525 | ]); 526 | }); 527 | }); 528 | }); 529 | 530 | context('FSTree with updates at several different depths', function () { 531 | beforeEach( function() { 532 | fsTree = new FSTree({ 533 | entries: [ 534 | entry({ relativePath: 'a.js', mode: 0o666, size: 1, mtime: 1 }), 535 | entry({ relativePath: 'b.js', mode: 0o666, size: 1, mtime: 1 }), 536 | entry({ relativePath: 'one/a.js', mode: 0o666, size: 1, mtime: 1 }), 537 | entry({ relativePath: 'one/b.js', mode: 0o666, size: 1, mtime: 1 }), 538 | entry({ relativePath: 'one/two/a.js', mode: 0o666, size: 1, mtime: 1 }), 539 | entry({ relativePath: 'one/two/b.js', mode: 0o666, size: 1, mtime: 1 }), 540 | ] 541 | }); 542 | }); 543 | 544 | it('catches each update', function() { 545 | let result = fsTree.calculatePatch(new FSTree({ 546 | entries: [ 547 | entry({ relativePath: 'a.js', mode: 0o666, size: 1, mtime: 2 }), 548 | entry({ relativePath: 'b.js', mode: 0o666, size: 1, mtime: 1 }), 549 | entry({ relativePath: 'one/a.js', mode: 0o666, size: 10, mtime: 1 }), 550 | entry({ relativePath: 'one/b.js', mode: 0o666, size: 1, mtime: 1 }), 551 | entry({ relativePath: 'one/two/a.js', mode: 0o667, size: 1, mtime: 1 }), 552 | entry({ relativePath: 'one/two/b.js', mode: 0o666, size: 1, mtime: 1 }), 553 | ] 554 | })); 555 | 556 | expect(result).to.deep.equal([ 557 | ['change', 'a.js', entry({ relativePath: 'a.js', size: 1, mtime: 2, mode: 0o666 })], 558 | ['change', 'one/a.js', entry({ relativePath: 'one/a.js', size: 10, mtime: 1, mode: 0o666})], 559 | ['change', 'one/two/a.js', entry({ relativePath: 'one/two/a.js', mode: 0o667, size: 1, mtime: 1})], 560 | ]); 561 | }); 562 | }); 563 | 564 | context('with unchanged paths', function() { 565 | beforeEach( function() { 566 | fsTree = FSTree.fromPaths([ 567 | 'bar/', 568 | 'bar/baz.js', 569 | 'foo.js', 570 | ]); 571 | }); 572 | 573 | it('returns an empty changeset', function() { 574 | expect(fsTree.calculatePatch(FSTree.fromPaths([ 575 | 'bar/', 576 | 'bar/baz.js', 577 | 'foo.js' 578 | ]))).to.deep.equal([ 579 | // when we work with entries, will potentially return updates 580 | ]); 581 | }); 582 | }); 583 | 584 | context('from a non-empty tree', function() { 585 | beforeEach( function() { 586 | fsTree = FSTree.fromPaths([ 587 | 'bar/', 588 | 'bar/one.js', 589 | 'bar/two.js', 590 | 'foo/', 591 | 'foo/one.js', 592 | 'foo/two.js', 593 | ]); 594 | }); 595 | 596 | context('with removals', function() { 597 | it('reduces the rm operations', function() { 598 | expect(fsTree.calculatePatch(FSTree.fromPaths([ 599 | 'bar/', 600 | 'bar/two.js' 601 | ]))).to.deep.equal([ 602 | ['unlink', 'foo/two.js', file('foo/two.js')], 603 | ['unlink', 'foo/one.js', file('foo/one.js')], 604 | ['rmdir', 'foo/', directory('foo/')], 605 | ['unlink', 'bar/one.js', file('bar/one.js')], 606 | ]); 607 | }); 608 | }); 609 | 610 | context('with removals and additions', function() { 611 | it('works', function() { 612 | expect(fsTree.calculatePatch(FSTree.fromPaths([ 613 | 'bar/', 614 | 'bar/three.js' 615 | ]))).to.deep.equal([ 616 | ['unlink', 'foo/two.js', file('foo/two.js')], 617 | ['unlink', 'foo/one.js', file('foo/one.js')], 618 | ['rmdir', 'foo/', directory('foo/')], 619 | ['unlink', 'bar/two.js', file('bar/two.js')], 620 | ['unlink', 'bar/one.js', file('bar/one.js')], 621 | ['create', 'bar/three.js', file('bar/three.js')], 622 | ]); 623 | }); 624 | }); 625 | }); 626 | 627 | context('from a deep non-empty tree', function() { 628 | beforeEach( function() { 629 | fsTree = FSTree.fromPaths([ 630 | 'bar/', 631 | 'bar/quz/', 632 | 'bar/quz/baz.js', 633 | 'foo.js', 634 | ]); 635 | }); 636 | 637 | context('to an empty tree', function() { 638 | it('returns n rm operations', function() { 639 | expect(fsTree.calculatePatch(FSTree.fromPaths([]))).to.deep.equal([ 640 | ['unlink', 'foo.js', file('foo.js')], 641 | ['unlink', 'bar/quz/baz.js', file('bar/quz/baz.js')], 642 | ['rmdir', 'bar/quz/', directory('bar/quz/')], 643 | ['rmdir', 'bar/', directory('bar/')], 644 | ]); 645 | }); 646 | }); 647 | }); 648 | 649 | context('from a deep non-empty tree \w intermediate entry', function() { 650 | beforeEach( function() { 651 | fsTree = FSTree.fromPaths([ 652 | 'bar/', 653 | 'bar/foo.js', 654 | 'bar/quz/', 655 | 'bar/quz/baz.js', 656 | ]); 657 | }); 658 | 659 | context('to an empty tree', function() { 660 | it('returns one unlink operation', function() { 661 | expect(fsTree.calculatePatch(FSTree.fromPaths([ 662 | 'bar/', 663 | 'bar/quz/', 664 | 'bar/quz/baz.js' 665 | ]))).to.deep.equal([ 666 | ['unlink', 'bar/foo.js', file('bar/foo.js')] 667 | ]); 668 | }); 669 | }); 670 | }); 671 | 672 | context('another nested scenario', function() { 673 | beforeEach( function() { 674 | fsTree = FSTree.fromPaths([ 675 | 'subdir1/', 676 | 'subdir1/subsubdir1/', 677 | 'subdir1/subsubdir1/foo.png', 678 | 'subdir2/', 679 | 'subdir2/bar.css' 680 | ]); 681 | }); 682 | 683 | context('to an empty tree', function() { 684 | it('returns one unlink operation', function() { 685 | expect(fsTree.calculatePatch(FSTree.fromPaths([ 686 | 'subdir1/', 687 | 'subdir1/subsubdir1/', 688 | 'subdir1/subsubdir1/foo.png' 689 | ]))).to.deep.equal([ 690 | ['unlink', 'subdir2/bar.css', file('subdir2/bar.css')], 691 | ['rmdir', 'subdir2/', directory('subdir2/')] 692 | ]); 693 | }); 694 | }); 695 | }); 696 | 697 | context('folder => file', function() { 698 | beforeEach( function() { 699 | fsTree = FSTree.fromPaths([ 700 | 'subdir1/', 701 | 'subdir1/foo' 702 | ]); 703 | }); 704 | 705 | it('it unlinks the file, and rmdir the folder and then creates the file', function() { 706 | expect(fsTree.calculatePatch(FSTree.fromPaths([ 707 | 'subdir1' 708 | ]))).to.deep.equal([ 709 | ['unlink', 'subdir1/foo', file('subdir1/foo')], 710 | ['rmdir', 'subdir1/', directory('subdir1/')], 711 | ['create', 'subdir1', file('subdir1')], 712 | ]); 713 | }); 714 | }); 715 | 716 | context('file => folder', function() { 717 | beforeEach( function() { 718 | fsTree = FSTree.fromPaths([ 719 | 'subdir1' 720 | ]); 721 | }); 722 | 723 | it('it unlinks the file, and makes the folder and then creates the file', function() { 724 | expect(fsTree.calculatePatch(FSTree.fromPaths([ 725 | 'subdir1/', 726 | 'subdir1/foo' 727 | ]))).to.deep.equal([ 728 | ['unlink', 'subdir1', file('subdir1')], 729 | ['mkdir', 'subdir1/', directory('subdir1/')], 730 | ['create', 'subdir1/foo', file('subdir1/foo')] 731 | ]); 732 | }); 733 | }); 734 | 735 | context('folders', function() { 736 | beforeEach( function() { 737 | fsTree = FSTree.fromPaths([ 738 | 'dir/', 739 | 'dir2/', 740 | 'dir2/subdir1/', 741 | 'dir3/', 742 | 'dir3/subdir1/' 743 | ]); 744 | }); 745 | 746 | it('it unlinks the file, and makes the folder and then creates the file', function() { 747 | let result = fsTree.calculatePatch(FSTree.fromPaths([ 748 | 'dir2/', 749 | 'dir2/subdir1/', 750 | 'dir3/', 751 | 'dir4/', 752 | ])); 753 | 754 | expect(result).to.deep.equal([ 755 | ['rmdir', 'dir3/subdir1/', directory('dir3/subdir1/')], 756 | ['rmdir', 'dir/', directory('dir/')], 757 | // This no-op (rmdir dir3; mkdir dir3) is not fundamental: a future 758 | // iteration could reasonably optimize it away 759 | ['mkdir', 'dir4/', directory('dir4/')], 760 | ]); 761 | }); 762 | }); 763 | 764 | context('walk-sync like tree', function () { 765 | beforeEach( function() { 766 | fsTree = new FSTree({ 767 | entries: [ 768 | directory('parent/'), 769 | directory('parent/subdir/'), 770 | file('parent/subdir/a.js') 771 | ] 772 | }); 773 | }); 774 | 775 | it('moving a file out of a directory does not edit directory structure', function () { 776 | let newTree = new FSTree({ 777 | entries: [ 778 | directory('parent/'), 779 | file('parent/a.js'), 780 | directory('parent/subdir/'), 781 | ] 782 | }); 783 | let result = fsTree.calculatePatch(newTree); 784 | 785 | expect(result).to.deep.equal([ 786 | ['unlink', 'parent/subdir/a.js', file('parent/subdir/a.js')], 787 | ['create', 'parent/a.js', file('parent/a.js')], 788 | ]); 789 | }); 790 | 791 | it('moving a file out of a subdir and removing the subdir does not recreate parent', function () { 792 | let newTree = new FSTree({ 793 | entries: [ 794 | directory('parent/'), 795 | file('parent/a.js') 796 | ] 797 | }); 798 | let result = fsTree.calculatePatch(newTree); 799 | 800 | expect(result).to.deep.equal([ 801 | ['unlink', 'parent/subdir/a.js', file('parent/subdir/a.js')], 802 | ['rmdir', 'parent/subdir/', directory('parent/subdir/')], 803 | ['create', 'parent/a.js', file('parent/a.js')], 804 | ]); 805 | }); 806 | 807 | it('moving a file into nest subdir does not recreate subdir and parent', function () { 808 | let newTree = new FSTree({ 809 | entries: [ 810 | directory('parent/'), 811 | directory('parent/subdir/'), 812 | directory('parent/subdir/subdir/'), 813 | file('parent/subdir/subdir/a.js') 814 | ] 815 | }); 816 | let result = fsTree.calculatePatch(newTree); 817 | 818 | expect(result).to.deep.equal([ 819 | ['unlink', 'parent/subdir/a.js', file('parent/subdir/a.js')], 820 | ['mkdir', 'parent/subdir/subdir/', directory('parent/subdir/subdir/')], 821 | ['create', 'parent/subdir/subdir/a.js', file('parent/subdir/subdir/a.js')], 822 | ]); 823 | }); 824 | 825 | it('always remove files first if dir also needs to be removed', function() { 826 | let newTree = new FSTree({ 827 | entries: [ 828 | directory('parent/') 829 | ] 830 | }); 831 | 832 | let result = fsTree.calculatePatch(newTree); 833 | 834 | expect(result).to.deep.equal([ 835 | ['unlink', 'parent/subdir/a.js', file('parent/subdir/a.js')], 836 | ['rmdir', 'parent/subdir/', directory('parent/subdir/')] 837 | ]); 838 | }); 839 | 840 | it('renaming a subdir does not recreate parent', function () { 841 | let newTree = new FSTree({ 842 | entries: [ 843 | directory('parent/'), 844 | directory('parent/subdir2/'), 845 | file('parent/subdir2/a.js') 846 | ] 847 | }); 848 | 849 | let result = fsTree.calculatePatch(newTree); 850 | 851 | expect(result).to.deep.equal([ 852 | ['unlink', 'parent/subdir/a.js', file('parent/subdir/a.js')], 853 | ['rmdir', 'parent/subdir/', directory('parent/subdir/')], 854 | ['mkdir', 'parent/subdir2/', directory('parent/subdir2/')], 855 | ['create', 'parent/subdir2/a.js', file('parent/subdir2/a.js')], 856 | ]); 857 | }); 858 | }); 859 | }); 860 | 861 | describe('.applyPatch', function() { 862 | let inputDir = 'tmp/fixture/input'; 863 | let outputDir = 'tmp/fixture/output'; 864 | 865 | beforeEach(function() { 866 | fs.mkdirpSync(inputDir); 867 | fs.mkdirpSync(outputDir); 868 | }); 869 | 870 | afterEach(function() { 871 | fs.removeSync('tmp'); 872 | }); 873 | 874 | it('applies all types of operations', function() { 875 | let firstTree = FSTree.fromEntries(walkSync.entries(inputDir)); 876 | 877 | let fooIndex = path.join(inputDir, 'foo/index.js'); 878 | let barIndex = path.join(inputDir, 'bar/index.js'); 879 | let barOutput = path.join(outputDir, 'bar/index.js') 880 | 881 | fs.outputFileSync(fooIndex, 'foo'); // mkdir + create 882 | fs.outputFileSync(barIndex, 'bar'); // mkdir + create 883 | 884 | let secondTree = FSTree.fromEntries(walkSync.entries(inputDir)); 885 | let patch = firstTree.calculatePatch(secondTree); 886 | 887 | FSTree.applyPatch(inputDir, outputDir, patch); 888 | expect(walkSync(outputDir)).to.deep.equal([ 889 | 'bar/', 890 | 'bar/index.js', 891 | 'foo/', 892 | 'foo/index.js' 893 | ]); 894 | expect(fs.readFileSync(barOutput, 'utf-8')).to.equal('bar'); 895 | 896 | fs.removeSync(path.dirname(fooIndex)); // unlink + rmdir 897 | fs.outputFileSync(barIndex, 'boo'); // change 898 | 899 | let thirdTree = FSTree.fromEntries(walkSync.entries(inputDir)); 900 | patch = secondTree.calculatePatch(thirdTree); 901 | 902 | FSTree.applyPatch(inputDir, outputDir, patch); 903 | expect(walkSync(outputDir)).to.deep.equal([ 904 | 'bar/', 905 | 'bar/index.js' 906 | ]); 907 | expect(fs.readFileSync(barOutput, 'utf-8')).to.equal('boo'); 908 | }); 909 | 910 | it('does not bomb when trying to unlink an already removed file', function() { 911 | let firstTree = FSTree.fromEntries(walkSync.entries(inputDir)); 912 | 913 | let fooInput = path.join(inputDir, 'foo/index.js'); 914 | let barInput = path.join(inputDir, 'bar/index.js'); 915 | let barOutput = path.join(outputDir, 'bar/index.js') 916 | 917 | fs.outputFileSync(fooInput, 'foo'); // mkdir + create 918 | fs.outputFileSync(barInput, 'bar'); // mkdir + create 919 | 920 | let secondTree = FSTree.fromEntries(walkSync.entries(inputDir)); 921 | let patch = firstTree.calculatePatch(secondTree); 922 | 923 | // copy everything for setup 924 | FSTree.applyPatch(inputDir, outputDir, patch); 925 | expect(walkSync(outputDir)).to.deep.equal([ 926 | 'bar/', 927 | 'bar/index.js', 928 | 'foo/', 929 | 'foo/index.js' 930 | ]); 931 | 932 | fs.removeSync(barInput); // unlink + rmdir 933 | fs.removeSync(barOutput); // unlink + rmdir 934 | let thirdTree = FSTree.fromEntries(walkSync.entries(inputDir)); 935 | patch = secondTree.calculatePatch(thirdTree); 936 | 937 | FSTree.applyPatch(inputDir, outputDir, patch); 938 | expect(walkSync(outputDir)).to.deep.equal([ 939 | 'bar/', 940 | 'foo/', 941 | 'foo/index.js' 942 | ]); 943 | }); 944 | 945 | it('supports custom delegate methods', function() { 946 | let inputDir = 'tmp/fixture/input'; 947 | let outputDir = 'tmp/fixture/output'; 948 | 949 | let stats = { 950 | unlink: 0, 951 | rmdir: 0, 952 | mkdir: 0, 953 | change: 0, 954 | create: 0 955 | }; 956 | let delegate = { 957 | unlink() { 958 | stats.unlink++; 959 | }, 960 | rmdir() { 961 | stats.rmdir++; 962 | }, 963 | mkdir() { 964 | stats.mkdir++; 965 | }, 966 | change() { 967 | stats.change++; 968 | }, 969 | create() { 970 | stats.create++; 971 | } 972 | }; 973 | 974 | let patch: FSTree.Patch = [ 975 | [ 'mkdir', 'bar/' ], 976 | [ 'create', 'bar/index.js' ], 977 | [ 'mkdir', 'foo/' ], 978 | [ 'create', 'foo/index.js' ], 979 | [ 'unlink', 'foo/index.js' ], 980 | [ 'rmdir', 'foo/' ], 981 | [ 'change', 'bar/index.js' ] 982 | ]; 983 | 984 | FSTree.applyPatch(inputDir, outputDir, patch, delegate); 985 | 986 | expect(stats).to.deep.equal({ 987 | unlink: 1, 988 | rmdir: 1, 989 | mkdir: 2, 990 | change: 1, 991 | create: 2 992 | }); 993 | }); 994 | 995 | it('throws an error when a patch has an unknown operation type', function() { 996 | // @ts-ignore 997 | let patch: Patch = [ [ 'delete', '/foo.js' ] ]; 998 | expect(function() { 999 | FSTree.applyPatch('/fixture/input', '/fixture/output', patch) 1000 | }).to.throw('Unable to apply patch operation: delete. The value of delegate.delete is of type undefined, and not a function. Check the `delegate` argument to `FSTree.prototype.applyPatch`.'); 1001 | }); 1002 | }); 1003 | 1004 | describe('.calculateAndApplyPatch', function() { 1005 | let inputDir = 'tmp/fixture/input'; 1006 | let outputDir = 'tmp/fixture/output'; 1007 | 1008 | beforeEach(function() { 1009 | fs.mkdirpSync(inputDir); 1010 | fs.mkdirpSync(outputDir); 1011 | }); 1012 | 1013 | afterEach(function() { 1014 | fs.removeSync('tmp'); 1015 | }); 1016 | 1017 | it('calculates and applies a patch properly', function() { 1018 | let firstTree = FSTree.fromEntries(walkSync.entries(inputDir)); 1019 | 1020 | let fooIndex = path.join(inputDir, 'foo/index.js'); 1021 | let barIndex = path.join(inputDir, 'bar/index.js'); 1022 | let barOutput = path.join(outputDir, 'bar/index.js') 1023 | 1024 | fs.outputFileSync(fooIndex, 'foo'); 1025 | fs.outputFileSync(barIndex, 'bar'); 1026 | 1027 | let secondTree = FSTree.fromEntries(walkSync.entries(inputDir)); 1028 | firstTree.calculateAndApplyPatch(secondTree, inputDir, outputDir); 1029 | 1030 | expect(walkSync(outputDir)).to.deep.equal([ 1031 | 'bar/', 1032 | 'bar/index.js', 1033 | 'foo/', 1034 | 'foo/index.js' 1035 | ]); 1036 | }); 1037 | 1038 | it('calculates and applies a patch properly with custom delegates', function() { 1039 | const stats = { 1040 | mkdir: 0, 1041 | create: 0 1042 | }; 1043 | const delegate = { 1044 | mkdir() { 1045 | stats.mkdir++; 1046 | }, 1047 | create() { 1048 | stats.create++; 1049 | }, 1050 | unlink() {}, 1051 | rmdir() {}, 1052 | change() {} 1053 | }; 1054 | 1055 | let firstTree = FSTree.fromEntries(walkSync.entries(inputDir)); 1056 | 1057 | let fooIndex = path.join(inputDir, 'foo/index.js'); 1058 | let barIndex = path.join(inputDir, 'bar/index.js'); 1059 | 1060 | fs.outputFileSync(fooIndex, 'foo'); 1061 | fs.outputFileSync(barIndex, 'bar'); 1062 | 1063 | let secondTree = FSTree.fromEntries(walkSync.entries(inputDir)); 1064 | firstTree.calculateAndApplyPatch(secondTree, inputDir, outputDir, delegate); 1065 | 1066 | expect(stats).to.deep.equal({ 1067 | mkdir: 2, 1068 | create: 2 1069 | }); 1070 | }); 1071 | }); 1072 | 1073 | it('supports mixed entry types', function() { 1074 | class OtherEntry extends Entry { 1075 | get aliasedRelativePath() { 1076 | return this.relativePath; 1077 | } 1078 | } 1079 | const a = FSTree.fromEntries([new Entry('a')]) 1080 | const b = FSTree.fromEntries([new OtherEntry('b')]) 1081 | 1082 | expect(a.calculatePatch(b).map(x => x[0])).to.eql([ 'unlink', 'create']); 1083 | expect(a.calculatePatch(b, (x, y) => x.relativePath === y.aliasedRelativePath).map(x => x[0])).to.eql([ 'unlink', 'create']); 1084 | }) 1085 | }); 1086 | -------------------------------------------------------------------------------- /tests/util-test.ts: -------------------------------------------------------------------------------- 1 | import chai = require('chai'); 2 | import { 3 | commonPrefix, 4 | basename, 5 | computeImpliedEntries, 6 | sortAndExpand 7 | } from '../lib/util'; 8 | import Entry from '../lib/entry'; 9 | 10 | const { expect } = chai; 11 | 12 | require('chai').config.truncateThreshold = 0; 13 | 14 | describe('commonPrefix', function() { 15 | it('computes no common prefix if none exists', function() { 16 | expect(commonPrefix('a', 'b')).to.equal(''); 17 | }); 18 | 19 | it('computes the common prefix between two strings', function() { 20 | expect(commonPrefix('a/b/c/', 'a/b/c/d/e/f/', '/')).to.equal('a/b/c/'); 21 | }); 22 | 23 | it('strips the suffix (of the common prefix) after the last occurrence of the terminal character', function() { 24 | expect(commonPrefix('a/b/c/ohai', 'a/b/c/obai', '/')).to.equal('a/b/c/'); 25 | }); 26 | }); 27 | 28 | describe('basename', function() { 29 | it('computes the basename of files', function() { 30 | expect(basename(new Entry('a/b/c'))).to.equal('a/b/'); 31 | }); 32 | 33 | it('computes the basename of directories', function() { 34 | expect(basename(new Entry('a/b/c/'))).to.equal('a/b/'); 35 | }); 36 | }); 37 | 38 | describe('computeImpliedEntries', function() { 39 | it('computes implied entries', function() { 40 | var entries = computeImpliedEntries('a/b/', 'c/d/e/'); 41 | expect(entries).to.deep.equal([ 42 | new Entry('a/b/c/', 0, 0), 43 | new Entry('a/b/c/d/', 0, 0), 44 | new Entry('a/b/c/d/e/', 0, 0), 45 | ]); 46 | }); 47 | }); 48 | 49 | describe('sortAndExpand', function() { 50 | it('sorts and expands entries in place', function() { 51 | const entries = [ 52 | new Entry('a/b/q/r/bar.js'), 53 | new Entry('a/b/c/d/foo.js'), 54 | ]; 55 | 56 | const sortedAndExpandedEntries = sortAndExpand(entries); 57 | 58 | expect(entries).to.equal(sortedAndExpandedEntries); 59 | expect(sortedAndExpandedEntries.map(function(e) { return e.relativePath;})).to.deep.equal([ 60 | 'a/', 61 | 'a/b/', 62 | 'a/b/c/', 63 | 'a/b/c/d/', 64 | 'a/b/c/d/foo.js', 65 | 'a/b/q/', 66 | 'a/b/q/r/', 67 | 'a/b/q/r/bar.js', 68 | ]); 69 | expect(sortedAndExpandedEntries).to.deep.equal([ 70 | new Entry('a/', 0, 0), 71 | new Entry('a/b/', 0, 0), 72 | new Entry('a/b/c/', 0, 0), 73 | new Entry('a/b/c/d/', 0, 0), 74 | new Entry('a/b/c/d/foo.js'), 75 | new Entry('a/b/q/', 0, 0), 76 | new Entry('a/b/q/r/', 0, 0), 77 | new Entry('a/b/q/r/bar.js'), 78 | ]); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "strict": true, 5 | "moduleResolution": "node", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "paths": { 9 | "*": ["types/*"] 10 | }, 11 | "baseUrl": "." 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /types/heimdalljs-logger.d.ts: -------------------------------------------------------------------------------- 1 | declare function Logger(name: string) : { 2 | info: (message: string, a: any, b: any) => void 3 | } 4 | export = Logger; 5 | -------------------------------------------------------------------------------- /types/path-posix.d.ts: -------------------------------------------------------------------------------- 1 | export function join(a: string, b: string) : string; 2 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@types/chai@^4.1.7": 6 | version "4.1.7" 7 | resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.1.7.tgz#1b8e33b61a8c09cbe1f85133071baa0dbf9fa71a" 8 | integrity sha512-2Y8uPt0/jwjhQ6EiluT0XCri1Dbplr0ZxfFXUz+ye13gaqE8u5gL5ppao1JrUYr9cIip5S6MvQzBS7Kke7U9VA== 9 | 10 | "@types/fs-extra@^5.0.4": 11 | version "5.1.0" 12 | resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-5.1.0.tgz#2a325ef97901504a3828718c390d34b8426a10a1" 13 | integrity sha512-AInn5+UBFIK9FK5xc9yP5e3TQSPNNgjHByqYcj9g5elVBnDQcQL7PlO1CIRy2gWlbwK7UPYqi7vRvFA44dCmYQ== 14 | dependencies: 15 | "@types/node" "*" 16 | 17 | "@types/mocha@^5.2.5": 18 | version "5.2.7" 19 | resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-5.2.7.tgz#315d570ccb56c53452ff8638738df60726d5b6ea" 20 | integrity sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ== 21 | 22 | "@types/node@*": 23 | version "12.6.8" 24 | resolved "https://registry.yarnpkg.com/@types/node/-/node-12.6.8.tgz#e469b4bf9d1c9832aee4907ba8a051494357c12c" 25 | integrity sha512-aX+gFgA5GHcDi89KG5keey2zf0WfZk/HAQotEamsK2kbey+8yGKcson0hbK8E+v0NArlCJQCqMP161YhV6ZXLg== 26 | 27 | "@types/node@^10.12.21": 28 | version "10.14.13" 29 | resolved "https://registry.yarnpkg.com/@types/node/-/node-10.14.13.tgz#ac786d623860adf39a3f51d629480aacd6a6eec7" 30 | integrity sha512-yN/FNNW1UYsRR1wwAoyOwqvDuLDtVXnaJTZ898XIw/Q5cCaeVAlVwvsmXLX5PuiScBYwZsZU4JYSHB3TvfdwvQ== 31 | 32 | "@types/symlink-or-copy@^1.2.0": 33 | version "1.2.0" 34 | resolved "https://registry.yarnpkg.com/@types/symlink-or-copy/-/symlink-or-copy-1.2.0.tgz#4151a81b4052c80bc2becbae09f3a9ec010a9c7a" 35 | integrity sha512-Lja2xYuuf2B3knEsga8ShbOdsfNOtzT73GyJmZyY7eGl2+ajOqrs8yM5ze0fsSoYwvA6bw7/Qr7OZ7PEEmYwWg== 36 | 37 | assertion-error@^1.0.1: 38 | version "1.1.0" 39 | resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" 40 | integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== 41 | 42 | balanced-match@^1.0.0: 43 | version "1.0.0" 44 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" 45 | integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= 46 | 47 | brace-expansion@^1.1.7: 48 | version "1.1.11" 49 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" 50 | integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== 51 | dependencies: 52 | balanced-match "^1.0.0" 53 | concat-map "0.0.1" 54 | 55 | chai@^3.3.0: 56 | version "3.5.0" 57 | resolved "https://registry.yarnpkg.com/chai/-/chai-3.5.0.tgz#4d02637b067fe958bdbfdd3a40ec56fef7373247" 58 | integrity sha1-TQJjewZ/6Vi9v906QOxW/vc3Mkc= 59 | dependencies: 60 | assertion-error "^1.0.1" 61 | deep-eql "^0.1.3" 62 | type-detect "^1.0.0" 63 | 64 | commander@0.6.1: 65 | version "0.6.1" 66 | resolved "https://registry.yarnpkg.com/commander/-/commander-0.6.1.tgz#fa68a14f6a945d54dbbe50d8cdb3320e9e3b1a06" 67 | integrity sha1-+mihT2qUXVTbvlDYzbMyDp47GgY= 68 | 69 | commander@2.3.0: 70 | version "2.3.0" 71 | resolved "https://registry.yarnpkg.com/commander/-/commander-2.3.0.tgz#fd430e889832ec353b9acd1de217c11cb3eef873" 72 | integrity sha1-/UMOiJgy7DU7ms0d4hfBHLPu+HM= 73 | 74 | concat-map@0.0.1: 75 | version "0.0.1" 76 | resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 77 | integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= 78 | 79 | debug@2.2.0: 80 | version "2.2.0" 81 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" 82 | integrity sha1-+HBX6ZWxofauaklgZkE3vFbwOdo= 83 | dependencies: 84 | ms "0.7.1" 85 | 86 | debug@^2.2.0: 87 | version "2.6.9" 88 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" 89 | integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== 90 | dependencies: 91 | ms "2.0.0" 92 | 93 | deep-eql@^0.1.3: 94 | version "0.1.3" 95 | resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-0.1.3.tgz#ef558acab8de25206cd713906d74e56930eb69f2" 96 | integrity sha1-71WKyrjeJSBs1xOQbXTlaTDrafI= 97 | dependencies: 98 | type-detect "0.1.1" 99 | 100 | diff@1.4.0: 101 | version "1.4.0" 102 | resolved "https://registry.yarnpkg.com/diff/-/diff-1.4.0.tgz#7f28d2eb9ee7b15a97efd89ce63dcfdaa3ccbabf" 103 | integrity sha1-fyjS657nsVqX79ic5j3P2qPMur8= 104 | 105 | ensure-posix-path@^1.0.0: 106 | version "1.1.1" 107 | resolved "https://registry.yarnpkg.com/ensure-posix-path/-/ensure-posix-path-1.1.1.tgz#3c62bdb19fa4681544289edb2b382adc029179ce" 108 | integrity sha512-VWU0/zXzVbeJNXvME/5EmLuEj2TauvoaTz6aFYK1Z92JCBlDlZ3Gu0tuGR42kpW1754ywTs+QB0g5TP0oj9Zaw== 109 | 110 | escape-string-regexp@1.0.2: 111 | version "1.0.2" 112 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.2.tgz#4dbc2fe674e71949caf3fb2695ce7f2dc1d9a8d1" 113 | integrity sha1-Tbwv5nTnGUnK8/smlc5/LcHZqNE= 114 | 115 | fs-extra@^1.0.0: 116 | version "1.0.0" 117 | resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-1.0.0.tgz#cd3ce5f7e7cb6145883fcae3191e9877f8587950" 118 | integrity sha1-zTzl9+fLYUWIP8rjGR6Yd/hYeVA= 119 | dependencies: 120 | graceful-fs "^4.1.2" 121 | jsonfile "^2.1.0" 122 | klaw "^1.0.0" 123 | 124 | glob@3.2.11: 125 | version "3.2.11" 126 | resolved "https://registry.yarnpkg.com/glob/-/glob-3.2.11.tgz#4a973f635b9190f715d10987d5c00fd2815ebe3d" 127 | integrity sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0= 128 | dependencies: 129 | inherits "2" 130 | minimatch "0.3" 131 | 132 | graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9: 133 | version "4.2.0" 134 | resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.0.tgz#8d8fdc73977cb04104721cb53666c1ca64cd328b" 135 | integrity sha512-jpSvDPV4Cq/bgtpndIWbI5hmYxhQGHPC4d4cqBPb4DLniCfhJokdXhwhaDuLBGLQdvvRum/UiX6ECVIPvDXqdg== 136 | 137 | growl@1.9.2: 138 | version "1.9.2" 139 | resolved "https://registry.yarnpkg.com/growl/-/growl-1.9.2.tgz#0ea7743715db8d8de2c5ede1775e1b45ac85c02f" 140 | integrity sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8= 141 | 142 | heimdalljs-logger@^0.1.7: 143 | version "0.1.10" 144 | resolved "https://registry.yarnpkg.com/heimdalljs-logger/-/heimdalljs-logger-0.1.10.tgz#90cad58aabb1590a3c7e640ddc6a4cd3a43faaf7" 145 | integrity sha512-pO++cJbhIufVI/fmB/u2Yty3KJD0TqNPecehFae0/eps0hkZ3b4Zc/PezUMOpYuHFQbA7FxHZxa305EhmjLj4g== 146 | dependencies: 147 | debug "^2.2.0" 148 | heimdalljs "^0.2.6" 149 | 150 | heimdalljs@^0.2.6: 151 | version "0.2.6" 152 | resolved "https://registry.yarnpkg.com/heimdalljs/-/heimdalljs-0.2.6.tgz#b0eebabc412813aeb9542f9cc622cb58dbdcd9fe" 153 | integrity sha512-o9bd30+5vLBvBtzCPwwGqpry2+n0Hi6H1+qwt6y+0kwRHGGF8TFIhJPmnuM0xO97zaKrDZMwO/V56fAnn8m/tA== 154 | dependencies: 155 | rsvp "~3.2.1" 156 | 157 | inherits@2: 158 | version "2.0.4" 159 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" 160 | integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== 161 | 162 | jade@0.26.3: 163 | version "0.26.3" 164 | resolved "https://registry.yarnpkg.com/jade/-/jade-0.26.3.tgz#8f10d7977d8d79f2f6ff862a81b0513ccb25686c" 165 | integrity sha1-jxDXl32NefL2/4YqgbBRPMslaGw= 166 | dependencies: 167 | commander "0.6.1" 168 | mkdirp "0.3.0" 169 | 170 | jsonfile@^2.1.0: 171 | version "2.4.0" 172 | resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8" 173 | integrity sha1-NzaitCi4e72gzIO1P6PWM6NcKug= 174 | optionalDependencies: 175 | graceful-fs "^4.1.6" 176 | 177 | klaw@^1.0.0: 178 | version "1.3.1" 179 | resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439" 180 | integrity sha1-QIhDO0azsbolnXh4XY6W9zugJDk= 181 | optionalDependencies: 182 | graceful-fs "^4.1.9" 183 | 184 | lru-cache@2: 185 | version "2.7.3" 186 | resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.7.3.tgz#6d4524e8b955f95d4f5b58851ce21dd72fb4e952" 187 | integrity sha1-bUUk6LlV+V1PW1iFHOId1y+06VI= 188 | 189 | matcher-collection@^1.0.0: 190 | version "1.1.2" 191 | resolved "https://registry.yarnpkg.com/matcher-collection/-/matcher-collection-1.1.2.tgz#1076f506f10ca85897b53d14ef54f90a5c426838" 192 | integrity sha512-YQ/teqaOIIfUHedRam08PB3NK7Mjct6BvzRnJmpGDm8uFXpNr1sbY4yuflI5JcEs6COpYA0FpRQhSDBf1tT95g== 193 | dependencies: 194 | minimatch "^3.0.2" 195 | 196 | minimatch@0.3: 197 | version "0.3.0" 198 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-0.3.0.tgz#275d8edaac4f1bb3326472089e7949c8394699dd" 199 | integrity sha1-J12O2qxPG7MyZHIInnlJyDlGmd0= 200 | dependencies: 201 | lru-cache "2" 202 | sigmund "~1.0.0" 203 | 204 | minimatch@^3.0.2: 205 | version "3.0.4" 206 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" 207 | integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== 208 | dependencies: 209 | brace-expansion "^1.1.7" 210 | 211 | minimist@0.0.8: 212 | version "0.0.8" 213 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" 214 | integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= 215 | 216 | mkdirp@0.3.0: 217 | version "0.3.0" 218 | resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.0.tgz#1bbf5ab1ba827af23575143490426455f481fe1e" 219 | integrity sha1-G79asbqCevI1dRQ0kEJkVfSB/h4= 220 | 221 | mkdirp@0.5.1: 222 | version "0.5.1" 223 | resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" 224 | integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= 225 | dependencies: 226 | minimist "0.0.8" 227 | 228 | mocha@^2.3.3: 229 | version "2.5.3" 230 | resolved "https://registry.yarnpkg.com/mocha/-/mocha-2.5.3.tgz#161be5bdeb496771eb9b35745050b622b5aefc58" 231 | integrity sha1-FhvlvetJZ3HrmzV0UFC2IrWu/Fg= 232 | dependencies: 233 | commander "2.3.0" 234 | debug "2.2.0" 235 | diff "1.4.0" 236 | escape-string-regexp "1.0.2" 237 | glob "3.2.11" 238 | growl "1.9.2" 239 | jade "0.26.3" 240 | mkdirp "0.5.1" 241 | supports-color "1.2.0" 242 | to-iso-string "0.0.2" 243 | 244 | ms@0.7.1: 245 | version "0.7.1" 246 | resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098" 247 | integrity sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg= 248 | 249 | ms@2.0.0: 250 | version "2.0.0" 251 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 252 | integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= 253 | 254 | object-assign@^4.1.0: 255 | version "4.1.1" 256 | resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" 257 | integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= 258 | 259 | path-posix@^1.0.0: 260 | version "1.0.0" 261 | resolved "https://registry.yarnpkg.com/path-posix/-/path-posix-1.0.0.tgz#06b26113f56beab042545a23bfa88003ccac260f" 262 | integrity sha1-BrJhE/Vr6rBCVFojv6iAA8ysJg8= 263 | 264 | rsvp@~3.2.1: 265 | version "3.2.1" 266 | resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.2.1.tgz#07cb4a5df25add9e826ebc67dcc9fd89db27d84a" 267 | integrity sha1-B8tKXfJa3Z6Cbrxn3Mn9idsn2Eo= 268 | 269 | sigmund@~1.0.0: 270 | version "1.0.1" 271 | resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590" 272 | integrity sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA= 273 | 274 | supports-color@1.2.0: 275 | version "1.2.0" 276 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-1.2.0.tgz#ff1ed1e61169d06b3cf2d588e188b18d8847e17e" 277 | integrity sha1-/x7R5hFp0Gs88tWI4YixjYhH4X4= 278 | 279 | symlink-or-copy@^1.1.8: 280 | version "1.2.0" 281 | resolved "https://registry.yarnpkg.com/symlink-or-copy/-/symlink-or-copy-1.2.0.tgz#5d49108e2ab824a34069b68974486c290020b393" 282 | integrity sha512-W31+GLiBmU/ZR02Ii0mVZICuNEN9daZ63xZMPDsYgPgNjMtg+atqLEGI7PPI936jYSQZxoLb/63xos8Adrx4Eg== 283 | 284 | to-iso-string@0.0.2: 285 | version "0.0.2" 286 | resolved "https://registry.yarnpkg.com/to-iso-string/-/to-iso-string-0.0.2.tgz#4dc19e664dfccbe25bd8db508b00c6da158255d1" 287 | integrity sha1-TcGeZk38y+Jb2NtQiwDG2hWCVdE= 288 | 289 | type-detect@0.1.1: 290 | version "0.1.1" 291 | resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-0.1.1.tgz#0ba5ec2a885640e470ea4e8505971900dac58822" 292 | integrity sha1-C6XsKohWQORw6k6FBZcZANrFiCI= 293 | 294 | type-detect@^1.0.0: 295 | version "1.0.0" 296 | resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-1.0.0.tgz#762217cc06db258ec48908a1298e8b95121e8ea2" 297 | integrity sha1-diIXzAbbJY7EiQihKY6LlRIejqI= 298 | 299 | typescript@^3.3.3: 300 | version "3.5.3" 301 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.5.3.tgz#c830f657f93f1ea846819e929092f5fe5983e977" 302 | integrity sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g== 303 | 304 | walk-sync@^0.3.1: 305 | version "0.3.4" 306 | resolved "https://registry.yarnpkg.com/walk-sync/-/walk-sync-0.3.4.tgz#cf78486cc567d3a96b5b2237c6108017a5ffb9a4" 307 | integrity sha512-ttGcuHA/OBnN2pcM6johpYlEms7XpO5/fyKIr48541xXedan4roO8cS1Q2S/zbbjGH/BarYDAMeS2Mi9HE5Tig== 308 | dependencies: 309 | ensure-posix-path "^1.0.0" 310 | matcher-collection "^1.0.0" 311 | --------------------------------------------------------------------------------