├── .babelrc.js ├── .eslintrc.json ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── demo ├── .gitignore ├── a.js ├── makefile.js ├── package-lock.json └── package.json ├── docs ├── .nojekyll ├── assets │ ├── css │ │ └── main.css │ ├── images │ │ ├── icons.png │ │ ├── icons@2x.png │ │ ├── widgets.png │ │ └── widgets@2x.png │ └── js │ │ ├── main.js │ │ └── search.js ├── classes │ ├── _context_.context.html │ ├── _db_data_base_.database.html │ ├── _fs_memfs_impl_.memoryfilesystem.html │ ├── _fs_mtime_.mtime.html │ ├── _fs_nodefs_impl_.nodefilesystem.html │ ├── _io_.io.html │ ├── _make_.make.html │ ├── _makefile_makefile_.makefile.html │ ├── _makefile_prerequisites_.prerequisites.html │ ├── _makefile_recipe_.recipe.html │ ├── _makefile_rule_.rule.html │ ├── _makefile_target_.target.html │ ├── _reporters_dot_reporter_.dotreporter.html │ ├── _reporters_text_reporter_.textreporter.html │ ├── _target_.target.html │ ├── _task_.task.html │ ├── _utils_graph_.directedgraph.html │ ├── _utils_logger_.logger.html │ └── _utils_queue_.queue.html ├── enums │ ├── _makefile_target_.targettype.html │ ├── _target_.targetstate.html │ ├── _utils_graph_.vertextype.html │ └── _utils_logger_.loglevel.html ├── globals.html ├── index.html ├── interfaces │ ├── _config_.config.html │ ├── _context_.contextoptions.html │ ├── _db_document_.document.html │ ├── _db_document_collection_.documentcollection.html │ ├── _fs_file_system_.filesystem.html │ ├── _fs_mtime_document_.mtimedocument.html │ ├── _fs_mtime_document_.mtimeentry.html │ ├── _make_.makeoptions.html │ ├── _makefile_prerequisites_.prerequisitearray.html │ ├── _reporters_reporter_.reporter.html │ └── _target_.targetoptions.html └── modules │ ├── _bin_makit_.html │ ├── _config_.html │ ├── _context_.html │ ├── _db_data_base_.html │ ├── _db_document_.html │ ├── _db_document_collection_.html │ ├── _db_index_.html │ ├── _fs_file_system_.html │ ├── _fs_memfs_impl_.html │ ├── _fs_mtime_.html │ ├── _fs_mtime_document_.html │ ├── _fs_nodefs_impl_.html │ ├── _fs_time_stamp_.html │ ├── _index_.html │ ├── _io_.html │ ├── _make_.html │ ├── _makefile_makefile_.html │ ├── _makefile_prerequisites_.html │ ├── _makefile_recipe_.html │ ├── _makefile_rude_.html │ ├── _makefile_rule_.html │ ├── _makefile_target_.html │ ├── _reporters_dot_reporter_.html │ ├── _reporters_reporter_.html │ ├── _reporters_text_reporter_.html │ ├── _target_.html │ ├── _task_.html │ ├── _utils_graph_.html │ ├── _utils_logger_.html │ ├── _utils_number_.html │ ├── _utils_promise_.html │ ├── _utils_queue_.html │ └── _utils_string_.html ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── bin │ └── makit.ts ├── config.ts ├── context.ts ├── db │ ├── data-base.ts │ ├── document-collection.ts │ ├── document.ts │ └── index.ts ├── fs │ ├── file-system.ts │ ├── memfs-impl.ts │ ├── mtime-document.ts │ ├── mtime.ts │ ├── nodefs-impl.ts │ └── time-stamp.ts ├── index.ts ├── io.ts ├── make.ts ├── makefile │ ├── makefile.ts │ ├── prerequisites.ts │ ├── recipe.ts │ ├── rude.ts │ ├── rule.ts │ └── target.ts ├── reporters │ ├── dot-reporter.ts │ ├── reporter.ts │ └── text-reporter.ts ├── target.ts ├── task.ts └── utils │ ├── graph.ts │ ├── logger.ts │ ├── number.ts │ ├── promise.ts │ ├── queue.ts │ └── string.ts ├── test ├── .eslintrc.json ├── e2e │ ├── async.spec.ts │ ├── circular.spec.ts │ ├── error.spec.ts │ ├── glob.spec.ts │ ├── graph.spec.ts │ ├── invalidate.spec.ts │ ├── local-files.spec.ts │ ├── local-make.spec.ts │ ├── pattern.spec.ts │ ├── recursive.spec.ts │ ├── rude.spec.ts │ └── simple.spec.ts ├── stub │ └── create-env.ts ├── tsconfig.json └── unit │ ├── config.spec.ts │ ├── context.spec.ts │ ├── db │ └── data-base.spec.ts │ ├── fs │ └── mtime.spec.ts │ ├── makefile │ ├── recipe.spec.ts │ └── target.spec.ts │ └── utils │ ├── graph.spec.ts │ └── number.spec.ts └── tsconfig.json /.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | "targets": { 7 | "node": "current" 8 | } 9 | } 10 | ], 11 | '@babel/preset-typescript' 12 | ], 13 | "plugins": [ 14 | "transform-class-properties", 15 | "@babel/plugin-syntax-bigint" 16 | ] 17 | }; -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["standard"], 3 | "env": { 4 | "es6": true, 5 | "browser": true, 6 | "node": true 7 | }, 8 | "parser": "@typescript-eslint/parser", 9 | "plugins": [ 10 | "standard", 11 | "@typescript-eslint", 12 | "promise" 13 | ], 14 | "rules": { 15 | "no-dupe-class-members": "off", 16 | "indent": ["error", 4], 17 | "no-unused-vars": "off", 18 | "no-use-before-define": "off", 19 | "no-useless-constructor": "off" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | dist/ 64 | 65 | test/e2e/*.out 66 | .makit.db 67 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: js 2 | node_js: 10 3 | jobs: 4 | include: 5 | - stage: test 6 | name: 'Unit Test (8)' 7 | node_js: 8 8 | install: npm ci 9 | script: npm test 10 | - stage: test 11 | name: 'Unit Test (10)' 12 | node_js: 10 13 | install: npm ci 14 | script: npm test 15 | - stage: test 16 | name: 'Unit Test (12)' 17 | node_js: 12 18 | install: npm ci 19 | script: npm test 20 | - stage: test 21 | name: 'Lint' 22 | node_js: 10 23 | install: npm ci 24 | script: npm run lint 25 | - stage: test 26 | name: 'Coverage' 27 | node_js: 10 28 | install: npm ci 29 | script: npm run coveralls 30 | - stage: release 31 | if: branch = master 32 | node_js: lts/* 33 | script: skip 34 | deploy: 35 | provider: script 36 | skip_cleanup: true 37 | script: npx semantic-release 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Jun Yang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # makit 2 | [![npm version](https://img.shields.io/npm/v/makit.svg)](https://www.npmjs.org/package/makit) 3 | [![downloads](https://img.shields.io/npm/dm/makit.svg)](https://www.npmjs.org/package/makit) 4 | [![Build Status](https://travis-ci.com/searchfe/makit.svg?branch=master)](https://travis-ci.com/searchfe/makit) 5 | [![Coveralls](https://img.shields.io/coveralls/searchfe/makit.svg)](https://coveralls.io/github/searchfe/makit?branch=master) 6 | [![dependencies](https://img.shields.io/david/searchfe/makit.svg)](https://david-dm.org/searchfe/makit) 7 | [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/searchfe/makit) 8 | [![GitHub issues](https://img.shields.io/github/issues-closed/searchfe/makit.svg)](https://github.com/searchfe/makit/issues) 9 | [![David](https://img.shields.io/david/searchfe/makit.svg)](https://david-dm.org/searchfe/makit) 10 | [![David Dev](https://img.shields.io/david/dev/searchfe/makit.svg)](https://david-dm.org/searchfe/makit?type=dev) 11 | [![DUB license](https://img.shields.io/dub/l/vibe-d.svg)](https://github.com/searchfe/makit/blob/master/LICENSE) 12 | [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#commits) 13 | 14 | Purposes and Principles: 15 | 16 | * Minimal Concepts. It's intended to be a general purpose build automation tool just like GNU Make. Do not introduce unnecessary concept to keep it simple and stupid. 17 | * Less Restrictions. It should be as open as GNU Make, makit doesn't expect recipes return anything, doesn't even requrie a recipe for its rule definition, and doesn't care about the actual output of recipes. 18 | * JavaScript Style. Recipes can be written as callback style, Promise style or just synchronous style. Automatic variables are replaced by camelCased equivalents. Wildcard in static patterns are replaced by JavaScript RegExp and globs. 19 | 20 | API Spec: 21 | 22 | ## Get Started 23 | 24 | Basically, the syntax is as simple as Makefiles but with a `.js` extension. A Makefile.js contains a set of rules, each of which consists of 3 parts: 25 | 26 | * **target**: either a filepath string, a glob string, or a RegExp object. 27 | * **prerequisites**: list of filepath strings, functions that return a list of strings, or list of strings and functions. functions here can be either sync or async. 28 | * **recipe**: an optional function, can be either sync or async. 29 | 30 | Suppose we have a makefile.js containing the following contents: 31 | 32 | ```javascript 33 | const { rule } = require('makit') 34 | 35 | rule('all', ['a1.min.js']) // default rule 36 | 37 | rule('a1.min.js', ['a.js'], function () { 38 | const src = readFileSync(this.dependencies[0], 'utf8') 39 | const dst = UglifyJS.minify(src).code 40 | writeFileSync(this.target, dst) 41 | }) 42 | ``` 43 | 44 | When we run `makit`(which is equivelant to `make all` cause `all` is the first rule), makit tries to make the target `all` which requires `a1.min.js` so the second rule will be applied firstly and its recipe is called to generate the target `a1.min.js`. The prerequisites for `all` has been fully resolved now and makit then tries to call its recipe, which is not defined for the above case so makit will just skip call the recipe and assume the target `all` is made successfully. 45 | 46 | See [/demo](https://github.com/searchfe/makit/tree/master/demo) directory for a working demo. 47 | For more details see the typedoc for [.rule()](https://searchfe.github.io/makit/modules/_index_.html#rule.) 48 | 49 | ## Config 50 | 51 | The `makit` CLI supports `--help` to print usage info: 52 | 53 | ``` 54 | makit.js [OPTION] ... 55 | 56 | Options: 57 | --version Show version number [boolean] 58 | --makefile, -m makefile path [string] 59 | --database, -d database file, will be used for cache invalidation 60 | [default: "./.makit.db"] 61 | --require, -r require a module before loading makefile.js or 62 | makefile.ts [array] [default: []] 63 | --verbose, -v set loglevel to verbose [boolean] 64 | --debug, -v set loglevel to debug [boolean] 65 | --loglevel, -l error, warning, info, verbose, debug 66 | [choices: 0, 1, 2, 3, 4] 67 | --graph, -g output dependency graph [boolean] [default: false] 68 | --help Show help [boolean] 69 | ``` 70 | 71 | Or specify in `package.json`: 72 | 73 | ```json 74 | { 75 | "name": "your package name", 76 | "dependencies": {}, 77 | "makit": { 78 | "loglevel": 2, 79 | "makefile": "makefile.ts", 80 | "require": ["ts-node/register"] 81 | } 82 | } 83 | ``` 84 | 85 | ## Async (Promise & Callbacks) 86 | 87 | When the recipe returns a Promise, that promise will be awaited. 88 | 89 | ```javascript 90 | rule('a1.min.js', ['a.js'], async function () { 91 | const src = await readFile(this.dependencies[0], 'utf8') 92 | const dst = UglifyJS.minify(src).code 93 | await writeFile(this.target, dst) 94 | }) 95 | // equivelent to 96 | rule('a1.min.js', ['a.js'], async ctx => { 97 | const src = await readFile(ctx.dependencies[0], 'utf8') 98 | const dst = UglifyJS.minify(src).code 99 | await writeFile(ctx.target, dst) 100 | }) 101 | ``` 102 | 103 | Callback style functions also work: 104 | 105 | ```javascript 106 | rule('clean', [], (ctx, done) => rimraf('{*.md5,*.min.js}', done)) 107 | ``` 108 | 109 | ## Dynamic Dependencies 110 | 111 | ```javascript 112 | // `makit a.js.md5` will make a.js.md5 from a.js 113 | rule('*.js.md5', ctx => ctx.target.replace('.md5', ''), async function () { 114 | const src = await this.readDependency(0) 115 | await this.writeTarget(md5(src)) 116 | }) 117 | ``` 118 | 119 | Similarly, async prerequisites functions, i.e. functions of return type `Promise` or `Promise`, are also supported. 120 | 121 | 122 | ## Matching Groups and Reference 123 | 124 | Makit uses [extglob](https://www.npmjs.com/package/extglob) to match target names. 125 | Furthermore it's extended to support match groups which can be referenced in prerequisites. 126 | 127 | ```javascript 128 | // `makit output/app/app.js` will make app.js.md5 from a.ts 129 | rule('(output/**)/(*).js', '$1/$2.ts', async function () { 130 | return this.writeTarget(tsc(await this.readDependency())) 131 | }) 132 | make('output/app/app.js') 133 | ``` 134 | 135 | ## Dynamic Prerequisites 136 | 137 | It's sometimes handy to call `make()` within the recipe, but global `make()` is not valid in recipes. 138 | For example the following rule is **NOT** valid: 139 | 140 | ```javascript 141 | const { rule, make } = require('makit') 142 | 143 | rule('bundle.js', 'a.js', async (ctx) => { 144 | await make('b.js') 145 | const js = (await ctx.readFile('a.js')) + (await ctx.readFile('b.js')) 146 | ctx.writeTarget(js) 147 | }) 148 | ``` 149 | 150 | We introduce `rude()` to facilitate this situation, a `ctx.make` API is available inside the recipe of `rude`: 151 | 152 | ```javascript 153 | const { rude } = require('makit') 154 | 155 | rude( 156 | 'bundle.js', [], async ctx => { 157 | await ctx.make('a.js') 158 | await ctx.make('b.js') 159 | const js = (await ctx.readFile('a.js')) + (await ctx.readFile('b.js')) 160 | return ctx.writeTarget(js) 161 | } 162 | ) 163 | ``` 164 | 165 | A pseudo rule with `bundle.js.rude.dep` as target, the contents of which is the actual dependencies, 166 | will be generated for each `rude()`. 167 | -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | *.min.js 2 | *.md5 -------------------------------------------------------------------------------- /demo/a.js: -------------------------------------------------------------------------------- 1 | function func () { 2 | console.log(1) 3 | } -------------------------------------------------------------------------------- /demo/makefile.js: -------------------------------------------------------------------------------- 1 | const UglifyJS = require('uglify-js') 2 | const md5 = require('md5') 3 | const { writeFileSync, writeFile, readFileSync, readFile } = require('fs-extra') 4 | const { rule } = require('..') 5 | const rimraf = require('rimraf') 6 | 7 | // default rule, `makit` 8 | rule('all', ['a1.min.js', 'a2.min.js', 'a.js.md5']) 9 | 10 | // clean rule, `makit clean` 11 | rule('clean', [], (ctx, done) => rimraf('{*.md5,*.min.js}', done)) 12 | 13 | // Plain JavaScript, `makit a1.min.js` 14 | rule('a1.min.js', ['a.js'], function () { 15 | const src = readFileSync(this.dependencies[0], 'utf8') 16 | const dst = UglifyJS.minify(src).code 17 | writeFileSync(this.target, dst) 18 | }) 19 | 20 | // Async 21 | rule('a1.min.js', ['a.js'], async function () { 22 | const src = await readFile(this.dependencies[0], 'utf8') 23 | const dst = UglifyJS.minify(src).code 24 | await writeFile(this.target, dst) 25 | }) 26 | 27 | // Using Utility API, `makit a2.min.js` 28 | rule('a2.min.js', ['a.js'], async function () { 29 | const src = await this.readDependency(0) 30 | const dst = UglifyJS.minify(src).code 31 | await this.writeTarget(dst) 32 | }) 33 | 34 | // glob 35 | rule('*.js.md5', ctx => ctx.target.replace('.md5', ''), async function () { 36 | const src = await this.readDependency(0) 37 | await this.writeTarget(md5(src)) 38 | }) 39 | -------------------------------------------------------------------------------- /demo/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "balanced-match": { 8 | "version": "1.0.0", 9 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 10 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" 11 | }, 12 | "brace-expansion": { 13 | "version": "1.1.11", 14 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 15 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 16 | "requires": { 17 | "balanced-match": "^1.0.0", 18 | "concat-map": "0.0.1" 19 | } 20 | }, 21 | "charenc": { 22 | "version": "0.0.2", 23 | "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", 24 | "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=" 25 | }, 26 | "commander": { 27 | "version": "2.20.3", 28 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", 29 | "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", 30 | "dev": true 31 | }, 32 | "concat-map": { 33 | "version": "0.0.1", 34 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 35 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" 36 | }, 37 | "crypt": { 38 | "version": "0.0.2", 39 | "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", 40 | "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=" 41 | }, 42 | "fs.realpath": { 43 | "version": "1.0.0", 44 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 45 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" 46 | }, 47 | "glob": { 48 | "version": "7.1.6", 49 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", 50 | "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", 51 | "requires": { 52 | "fs.realpath": "^1.0.0", 53 | "inflight": "^1.0.4", 54 | "inherits": "2", 55 | "minimatch": "^3.0.4", 56 | "once": "^1.3.0", 57 | "path-is-absolute": "^1.0.0" 58 | } 59 | }, 60 | "inflight": { 61 | "version": "1.0.6", 62 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 63 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 64 | "requires": { 65 | "once": "^1.3.0", 66 | "wrappy": "1" 67 | } 68 | }, 69 | "inherits": { 70 | "version": "2.0.4", 71 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 72 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 73 | }, 74 | "is-buffer": { 75 | "version": "1.1.6", 76 | "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", 77 | "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" 78 | }, 79 | "md5": { 80 | "version": "2.2.1", 81 | "resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz", 82 | "integrity": "sha1-U6s41f48iJG6RlMp6iP6wFQBJvk=", 83 | "requires": { 84 | "charenc": "~0.0.1", 85 | "crypt": "~0.0.1", 86 | "is-buffer": "~1.1.1" 87 | } 88 | }, 89 | "minimatch": { 90 | "version": "3.0.4", 91 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 92 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 93 | "requires": { 94 | "brace-expansion": "^1.1.7" 95 | } 96 | }, 97 | "once": { 98 | "version": "1.4.0", 99 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 100 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 101 | "requires": { 102 | "wrappy": "1" 103 | } 104 | }, 105 | "path-is-absolute": { 106 | "version": "1.0.1", 107 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 108 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" 109 | }, 110 | "rimraf": { 111 | "version": "3.0.0", 112 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.0.tgz", 113 | "integrity": "sha512-NDGVxTsjqfunkds7CqsOiEnxln4Bo7Nddl3XhS4pXg5OzwkLqJ971ZVAAnB+DDLnF76N+VnDEiBHaVV8I06SUg==", 114 | "requires": { 115 | "glob": "^7.1.3" 116 | } 117 | }, 118 | "source-map": { 119 | "version": "0.6.1", 120 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 121 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 122 | "dev": true 123 | }, 124 | "uglify-js": { 125 | "version": "3.6.8", 126 | "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.8.tgz", 127 | "integrity": "sha512-XhHJ3S3ZyMwP8kY1Gkugqx3CJh2C3O0y8NPiSxtm1tyD/pktLAkFZsFGpuNfTZddKDQ/bbDBLAd2YyA1pbi8HQ==", 128 | "dev": true, 129 | "requires": { 130 | "commander": "~2.20.3", 131 | "source-map": "~0.6.1" 132 | } 133 | }, 134 | "wrappy": { 135 | "version": "1.0.2", 136 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 137 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "", 6 | "main": "index.js", 7 | "scripts": { 8 | "build": "makit", 9 | "clean": "makit clean" 10 | }, 11 | "author": "harttle", 12 | "devDependencies": { 13 | "uglify-js": "^3.6.8" 14 | }, 15 | "dependencies": { 16 | "md5": "^2.2.1", 17 | "makit": "*", 18 | "rimraf": "^3.0.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/searchfe/makit/2598edc7076c835d8eeb8f96c5ecaffe1611b4cb/docs/.nojekyll -------------------------------------------------------------------------------- /docs/assets/images/icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/searchfe/makit/2598edc7076c835d8eeb8f96c5ecaffe1611b4cb/docs/assets/images/icons.png -------------------------------------------------------------------------------- /docs/assets/images/icons@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/searchfe/makit/2598edc7076c835d8eeb8f96c5ecaffe1611b4cb/docs/assets/images/icons@2x.png -------------------------------------------------------------------------------- /docs/assets/images/widgets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/searchfe/makit/2598edc7076c835d8eeb8f96c5ecaffe1611b4cb/docs/assets/images/widgets.png -------------------------------------------------------------------------------- /docs/assets/images/widgets@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/searchfe/makit/2598edc7076c835d8eeb8f96c5ecaffe1611b4cb/docs/assets/images/widgets@2x.png -------------------------------------------------------------------------------- /docs/modules/_db_document_.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | "db/document" | makit 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 62 |

External module "db/document"

63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |

Index

71 |
72 |
73 |
74 |

Interfaces

75 | 78 |
79 |
80 |
81 |
82 |
83 | 102 |
103 |
104 |
105 |
106 |

Legend

107 |
108 |
    109 |
  • Module
  • 110 |
  • Object literal
  • 111 |
  • Variable
  • 112 |
  • Function
  • 113 |
  • Function with type parameter
  • 114 |
  • Index signature
  • 115 |
  • Type alias
  • 116 |
  • Type alias with type parameter
  • 117 |
118 |
    119 |
  • Enumeration
  • 120 |
  • Enumeration member
  • 121 |
  • Property
  • 122 |
  • Method
  • 123 |
124 |
    125 |
  • Interface
  • 126 |
  • Interface with type parameter
  • 127 |
  • Constructor
  • 128 |
  • Property
  • 129 |
  • Method
  • 130 |
  • Index signature
  • 131 |
132 |
    133 |
  • Class
  • 134 |
  • Class with type parameter
  • 135 |
  • Constructor
  • 136 |
  • Property
  • 137 |
  • Method
  • 138 |
  • Accessor
  • 139 |
  • Index signature
  • 140 |
141 |
    142 |
  • Inherited constructor
  • 143 |
  • Inherited property
  • 144 |
  • Inherited method
  • 145 |
  • Inherited accessor
  • 146 |
147 |
    148 |
  • Protected property
  • 149 |
  • Protected method
  • 150 |
  • Protected accessor
  • 151 |
152 |
    153 |
  • Private property
  • 154 |
  • Private method
  • 155 |
  • Private accessor
  • 156 |
157 |
    158 |
  • Static property
  • 159 |
  • Static method
  • 160 |
161 |
162 |
163 |
164 |
165 |

Generated using TypeDoc

166 |
167 |
168 | 169 | 170 | 171 | -------------------------------------------------------------------------------- /docs/modules/_db_document_collection_.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | "db/document-collection" | makit 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 62 |

External module "db/document-collection"

63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |

Index

71 |
72 |
73 |
74 |

Interfaces

75 | 78 |
79 |
80 |
81 |
82 |
83 | 102 |
103 |
104 |
105 |
106 |

Legend

107 |
108 |
    109 |
  • Module
  • 110 |
  • Object literal
  • 111 |
  • Variable
  • 112 |
  • Function
  • 113 |
  • Function with type parameter
  • 114 |
  • Index signature
  • 115 |
  • Type alias
  • 116 |
  • Type alias with type parameter
  • 117 |
118 |
    119 |
  • Enumeration
  • 120 |
  • Enumeration member
  • 121 |
  • Property
  • 122 |
  • Method
  • 123 |
124 |
    125 |
  • Interface
  • 126 |
  • Interface with type parameter
  • 127 |
  • Constructor
  • 128 |
  • Property
  • 129 |
  • Method
  • 130 |
  • Index signature
  • 131 |
132 |
    133 |
  • Class
  • 134 |
  • Class with type parameter
  • 135 |
  • Constructor
  • 136 |
  • Property
  • 137 |
  • Method
  • 138 |
  • Accessor
  • 139 |
  • Index signature
  • 140 |
141 |
    142 |
  • Inherited constructor
  • 143 |
  • Inherited property
  • 144 |
  • Inherited method
  • 145 |
  • Inherited accessor
  • 146 |
147 |
    148 |
  • Protected property
  • 149 |
  • Protected method
  • 150 |
  • Protected accessor
  • 151 |
152 |
    153 |
  • Private property
  • 154 |
  • Private method
  • 155 |
  • Private accessor
  • 156 |
157 |
    158 |
  • Static property
  • 159 |
  • Static method
  • 160 |
161 |
162 |
163 |
164 |
165 |

Generated using TypeDoc

166 |
167 |
168 | 169 | 170 | 171 | -------------------------------------------------------------------------------- /docs/modules/_db_index_.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | "db/index" | makit 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 62 |

External module "db/index"

63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | 86 |
87 |
88 |
89 |
90 |

Legend

91 |
92 |
    93 |
  • Module
  • 94 |
  • Object literal
  • 95 |
  • Variable
  • 96 |
  • Function
  • 97 |
  • Function with type parameter
  • 98 |
  • Index signature
  • 99 |
  • Type alias
  • 100 |
  • Type alias with type parameter
  • 101 |
102 |
    103 |
  • Enumeration
  • 104 |
  • Enumeration member
  • 105 |
  • Property
  • 106 |
  • Method
  • 107 |
108 |
    109 |
  • Interface
  • 110 |
  • Interface with type parameter
  • 111 |
  • Constructor
  • 112 |
  • Property
  • 113 |
  • Method
  • 114 |
  • Index signature
  • 115 |
116 |
    117 |
  • Class
  • 118 |
  • Class with type parameter
  • 119 |
  • Constructor
  • 120 |
  • Property
  • 121 |
  • Method
  • 122 |
  • Accessor
  • 123 |
  • Index signature
  • 124 |
125 |
    126 |
  • Inherited constructor
  • 127 |
  • Inherited property
  • 128 |
  • Inherited method
  • 129 |
  • Inherited accessor
  • 130 |
131 |
    132 |
  • Protected property
  • 133 |
  • Protected method
  • 134 |
  • Protected accessor
  • 135 |
136 |
    137 |
  • Private property
  • 138 |
  • Private method
  • 139 |
  • Private accessor
  • 140 |
141 |
    142 |
  • Static property
  • 143 |
  • Static method
  • 144 |
145 |
146 |
147 |
148 |
149 |

Generated using TypeDoc

150 |
151 |
152 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /docs/modules/_fs_file_system_.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | "fs/file-system" | makit 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 62 |

External module "fs/file-system"

63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |

Index

71 |
72 |
73 |
74 |

Interfaces

75 | 78 |
79 |
80 |
81 |
82 |
83 | 102 |
103 |
104 |
105 |
106 |

Legend

107 |
108 |
    109 |
  • Module
  • 110 |
  • Object literal
  • 111 |
  • Variable
  • 112 |
  • Function
  • 113 |
  • Function with type parameter
  • 114 |
  • Index signature
  • 115 |
  • Type alias
  • 116 |
  • Type alias with type parameter
  • 117 |
118 |
    119 |
  • Enumeration
  • 120 |
  • Enumeration member
  • 121 |
  • Property
  • 122 |
  • Method
  • 123 |
124 |
    125 |
  • Interface
  • 126 |
  • Interface with type parameter
  • 127 |
  • Constructor
  • 128 |
  • Property
  • 129 |
  • Method
  • 130 |
  • Index signature
  • 131 |
132 |
    133 |
  • Class
  • 134 |
  • Class with type parameter
  • 135 |
  • Constructor
  • 136 |
  • Property
  • 137 |
  • Method
  • 138 |
  • Accessor
  • 139 |
  • Index signature
  • 140 |
141 |
    142 |
  • Inherited constructor
  • 143 |
  • Inherited property
  • 144 |
  • Inherited method
  • 145 |
  • Inherited accessor
  • 146 |
147 |
    148 |
  • Protected property
  • 149 |
  • Protected method
  • 150 |
  • Protected accessor
  • 151 |
152 |
    153 |
  • Private property
  • 154 |
  • Private method
  • 155 |
  • Private accessor
  • 156 |
157 |
    158 |
  • Static property
  • 159 |
  • Static method
  • 160 |
161 |
162 |
163 |
164 |
165 |

Generated using TypeDoc

166 |
167 |
168 | 169 | 170 | 171 | -------------------------------------------------------------------------------- /docs/modules/_fs_memfs_impl_.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | "fs/memfs-impl" | makit 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 62 |

External module "fs/memfs-impl"

63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |

Index

71 |
72 |
73 |
74 |

Classes

75 | 78 |
79 |
80 |
81 |
82 |
83 | 102 |
103 |
104 |
105 |
106 |

Legend

107 |
108 |
    109 |
  • Module
  • 110 |
  • Object literal
  • 111 |
  • Variable
  • 112 |
  • Function
  • 113 |
  • Function with type parameter
  • 114 |
  • Index signature
  • 115 |
  • Type alias
  • 116 |
  • Type alias with type parameter
  • 117 |
118 |
    119 |
  • Enumeration
  • 120 |
  • Enumeration member
  • 121 |
  • Property
  • 122 |
  • Method
  • 123 |
124 |
    125 |
  • Interface
  • 126 |
  • Interface with type parameter
  • 127 |
  • Constructor
  • 128 |
  • Property
  • 129 |
  • Method
  • 130 |
  • Index signature
  • 131 |
132 |
    133 |
  • Class
  • 134 |
  • Class with type parameter
  • 135 |
  • Constructor
  • 136 |
  • Property
  • 137 |
  • Method
  • 138 |
  • Accessor
  • 139 |
  • Index signature
  • 140 |
141 |
    142 |
  • Inherited constructor
  • 143 |
  • Inherited property
  • 144 |
  • Inherited method
  • 145 |
  • Inherited accessor
  • 146 |
147 |
    148 |
  • Protected property
  • 149 |
  • Protected method
  • 150 |
  • Protected accessor
  • 151 |
152 |
    153 |
  • Private property
  • 154 |
  • Private method
  • 155 |
  • Private accessor
  • 156 |
157 |
    158 |
  • Static property
  • 159 |
  • Static method
  • 160 |
161 |
162 |
163 |
164 |
165 |

Generated using TypeDoc

166 |
167 |
168 | 169 | 170 | 171 | -------------------------------------------------------------------------------- /docs/modules/_io_.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | "io" | makit 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 62 |

External module "io"

63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |

Index

71 |
72 |
73 |
74 |

Classes

75 |
    76 |
  • IO
  • 77 |
78 |
79 |
80 |
81 |
82 |
83 | 102 |
103 |
104 |
105 |
106 |

Legend

107 |
108 |
    109 |
  • Module
  • 110 |
  • Object literal
  • 111 |
  • Variable
  • 112 |
  • Function
  • 113 |
  • Function with type parameter
  • 114 |
  • Index signature
  • 115 |
  • Type alias
  • 116 |
  • Type alias with type parameter
  • 117 |
118 |
    119 |
  • Enumeration
  • 120 |
  • Enumeration member
  • 121 |
  • Property
  • 122 |
  • Method
  • 123 |
124 |
    125 |
  • Interface
  • 126 |
  • Interface with type parameter
  • 127 |
  • Constructor
  • 128 |
  • Property
  • 129 |
  • Method
  • 130 |
  • Index signature
  • 131 |
132 |
    133 |
  • Class
  • 134 |
  • Class with type parameter
  • 135 |
  • Constructor
  • 136 |
  • Property
  • 137 |
  • Method
  • 138 |
  • Accessor
  • 139 |
  • Index signature
  • 140 |
141 |
    142 |
  • Inherited constructor
  • 143 |
  • Inherited property
  • 144 |
  • Inherited method
  • 145 |
  • Inherited accessor
  • 146 |
147 |
    148 |
  • Protected property
  • 149 |
  • Protected method
  • 150 |
  • Protected accessor
  • 151 |
152 |
    153 |
  • Private property
  • 154 |
  • Private method
  • 155 |
  • Private accessor
  • 156 |
157 |
    158 |
  • Static property
  • 159 |
  • Static method
  • 160 |
161 |
162 |
163 |
164 |
165 |

Generated using TypeDoc

166 |
167 |
168 | 169 | 170 | 171 | -------------------------------------------------------------------------------- /docs/modules/_reporters_reporter_.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | "reporters/reporter" | makit 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 62 |

External module "reporters/reporter"

63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |

Index

71 |
72 |
73 |
74 |

Interfaces

75 | 78 |
79 |
80 |
81 |
82 |
83 | 102 |
103 |
104 |
105 |
106 |

Legend

107 |
108 |
    109 |
  • Module
  • 110 |
  • Object literal
  • 111 |
  • Variable
  • 112 |
  • Function
  • 113 |
  • Function with type parameter
  • 114 |
  • Index signature
  • 115 |
  • Type alias
  • 116 |
  • Type alias with type parameter
  • 117 |
118 |
    119 |
  • Enumeration
  • 120 |
  • Enumeration member
  • 121 |
  • Property
  • 122 |
  • Method
  • 123 |
124 |
    125 |
  • Interface
  • 126 |
  • Interface with type parameter
  • 127 |
  • Constructor
  • 128 |
  • Property
  • 129 |
  • Method
  • 130 |
  • Index signature
  • 131 |
132 |
    133 |
  • Class
  • 134 |
  • Class with type parameter
  • 135 |
  • Constructor
  • 136 |
  • Property
  • 137 |
  • Method
  • 138 |
  • Accessor
  • 139 |
  • Index signature
  • 140 |
141 |
    142 |
  • Inherited constructor
  • 143 |
  • Inherited property
  • 144 |
  • Inherited method
  • 145 |
  • Inherited accessor
  • 146 |
147 |
    148 |
  • Protected property
  • 149 |
  • Protected method
  • 150 |
  • Protected accessor
  • 151 |
152 |
    153 |
  • Private property
  • 154 |
  • Private method
  • 155 |
  • Private accessor
  • 156 |
157 |
    158 |
  • Static property
  • 159 |
  • Static method
  • 160 |
161 |
162 |
163 |
164 |
165 |

Generated using TypeDoc

166 |
167 |
168 | 169 | 170 | 171 | -------------------------------------------------------------------------------- /docs/modules/_task_.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | "task" | makit 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 62 |

External module "task"

63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |

Index

71 |
72 |
73 |
74 |

Classes

75 | 78 |
79 |
80 |
81 |
82 |
83 | 102 |
103 |
104 |
105 |
106 |

Legend

107 |
108 |
    109 |
  • Module
  • 110 |
  • Object literal
  • 111 |
  • Variable
  • 112 |
  • Function
  • 113 |
  • Function with type parameter
  • 114 |
  • Index signature
  • 115 |
  • Type alias
  • 116 |
  • Type alias with type parameter
  • 117 |
118 |
    119 |
  • Enumeration
  • 120 |
  • Enumeration member
  • 121 |
  • Property
  • 122 |
  • Method
  • 123 |
124 |
    125 |
  • Interface
  • 126 |
  • Interface with type parameter
  • 127 |
  • Constructor
  • 128 |
  • Property
  • 129 |
  • Method
  • 130 |
  • Index signature
  • 131 |
132 |
    133 |
  • Class
  • 134 |
  • Class with type parameter
  • 135 |
  • Constructor
  • 136 |
  • Property
  • 137 |
  • Method
  • 138 |
  • Accessor
  • 139 |
  • Index signature
  • 140 |
141 |
    142 |
  • Inherited constructor
  • 143 |
  • Inherited property
  • 144 |
  • Inherited method
  • 145 |
  • Inherited accessor
  • 146 |
147 |
    148 |
  • Protected property
  • 149 |
  • Protected method
  • 150 |
  • Protected accessor
  • 151 |
152 |
    153 |
  • Private property
  • 154 |
  • Private method
  • 155 |
  • Private accessor
  • 156 |
157 |
    158 |
  • Static property
  • 159 |
  • Static method
  • 160 |
161 |
162 |
163 |
164 |
165 |

Generated using TypeDoc

166 |
167 |
168 | 169 | 170 | 171 | -------------------------------------------------------------------------------- /docs/modules/_utils_queue_.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | "utils/queue" | makit 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 62 |

External module "utils/queue"

63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |

Index

71 |
72 |
73 |
74 |

Classes

75 | 78 |
79 |
80 |
81 |
82 |
83 | 102 |
103 |
104 |
105 |
106 |

Legend

107 |
108 |
    109 |
  • Module
  • 110 |
  • Object literal
  • 111 |
  • Variable
  • 112 |
  • Function
  • 113 |
  • Function with type parameter
  • 114 |
  • Index signature
  • 115 |
  • Type alias
  • 116 |
  • Type alias with type parameter
  • 117 |
118 |
    119 |
  • Enumeration
  • 120 |
  • Enumeration member
  • 121 |
  • Property
  • 122 |
  • Method
  • 123 |
124 |
    125 |
  • Interface
  • 126 |
  • Interface with type parameter
  • 127 |
  • Constructor
  • 128 |
  • Property
  • 129 |
  • Method
  • 130 |
  • Index signature
  • 131 |
132 |
    133 |
  • Class
  • 134 |
  • Class with type parameter
  • 135 |
  • Constructor
  • 136 |
  • Property
  • 137 |
  • Method
  • 138 |
  • Accessor
  • 139 |
  • Index signature
  • 140 |
141 |
    142 |
  • Inherited constructor
  • 143 |
  • Inherited property
  • 144 |
  • Inherited method
  • 145 |
  • Inherited accessor
  • 146 |
147 |
    148 |
  • Protected property
  • 149 |
  • Protected method
  • 150 |
  • Protected accessor
  • 151 |
152 |
    153 |
  • Private property
  • 154 |
  • Private method
  • 155 |
  • Private accessor
  • 156 |
157 |
    158 |
  • Static property
  • 159 |
  • Static method
  • 160 |
161 |
162 |
163 |
164 |
165 |

Generated using TypeDoc

166 |
167 |
168 | 169 | 170 | 171 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'setupFilesAfterEnv': ['jest-extended'] 3 | } 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "makit", 3 | "version": "1.4.1", 4 | "description": "Make in JavaScript done right!", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "scripts": { 8 | "clean": "rm -rf ./dist", 9 | "lint": "eslint 'src/**/*.ts' 'test/**/*.ts'", 10 | "check": "npm run test && npm run lint", 11 | "docs": "typedoc --out docs src && touch docs/.nojekyll", 12 | "build": "tsc && chmod a+x dist/bin/*", 13 | "watch": "tsc --watch", 14 | "unit": "jest test/unit", 15 | "e2e": "jest test/e2e", 16 | "test": "jest test", 17 | "coveralls": "jest --coverage && cat coverage/lcov.info | coveralls", 18 | "coverage": "jest --coverage", 19 | "version": "npm run build && npm run docs", 20 | "semantic-release": "semantic-release" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/searchfe/makit.git" 25 | }, 26 | "keywords": [ 27 | "Make", 28 | "JavaScript", 29 | "Done-Right" 30 | ], 31 | "author": "harttle ", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/searchfe/makit/issues" 35 | }, 36 | "homepage": "https://github.com/searchfe/makit#readme", 37 | "files": [ 38 | "bin", 39 | "dist", 40 | "index.d.ts", 41 | "*.json", 42 | "*.md" 43 | ], 44 | "bin": { 45 | "makit": "./dist/bin/makit.js" 46 | }, 47 | "devDependencies": { 48 | "@babel/core": "^7.6.0", 49 | "@babel/plugin-syntax-bigint": "^7.7.4", 50 | "@babel/preset-env": "^7.6.0", 51 | "@babel/preset-typescript": "^7.12.17", 52 | "@babel/runtime": "^7.6.0", 53 | "@jest/transform": "^24.9.0", 54 | "@semantic-release/changelog": "^3.0.2", 55 | "@semantic-release/commit-analyzer": "^6.1.0", 56 | "@semantic-release/git": "^7.0.8", 57 | "@semantic-release/npm": "^5.1.8", 58 | "@semantic-release/release-notes-generator": "^7.1.4", 59 | "@types/jest": "^24.0.25", 60 | "@types/lodash": "^4.14.149", 61 | "@types/memory-fs": "^0.3.2", 62 | "@types/node": "^13.1.7", 63 | "@types/treeify": "^1.0.0", 64 | "@types/yargs": "^13.0.3", 65 | "@typescript-eslint/eslint-plugin": "^2.34.0", 66 | "@typescript-eslint/parser": "^4.15.2", 67 | "babel-jest": "^24.9.0", 68 | "babel-plugin-transform-class-properties": "^6.24.1", 69 | "babel-preset-jest": "^24.9.0", 70 | "coveralls": "^3.0.7", 71 | "eslint": "^5.12.1", 72 | "eslint-config-standard": "^12.0.0", 73 | "eslint-plugin-import": "^2.15.0", 74 | "eslint-plugin-jest": "^23.0.3", 75 | "eslint-plugin-node": "^8.0.1", 76 | "eslint-plugin-promise": "^4.0.1", 77 | "eslint-plugin-standard": "^4.0.0", 78 | "jest": "^24.9.0", 79 | "jest-extended": "^0.11.2", 80 | "md5": "^2.2.1", 81 | "semantic-release": "^17.2.3", 82 | "typedoc": "^0.15.0" 83 | }, 84 | "dependencies": { 85 | "chalk": "^4.0.0", 86 | "extglob": "^3.0.0", 87 | "is-glob": "^4.0.1", 88 | "lodash": "^4.17.15", 89 | "memory-fs": "^0.5.0", 90 | "treeify": "^1.1.0", 91 | "typescript": "^4.1.5", 92 | "yargs": "^14.2.0" 93 | }, 94 | "release": { 95 | "branch": "master", 96 | "plugins": [ 97 | "@semantic-release/commit-analyzer", 98 | "@semantic-release/release-notes-generator", 99 | "@semantic-release/changelog", 100 | "@semantic-release/npm", 101 | [ 102 | "@semantic-release/git", 103 | { 104 | "assets": [ 105 | "pacakge.json", 106 | "docs", 107 | "CHANGELOG.md" 108 | ], 109 | "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" 110 | } 111 | ], 112 | "@semantic-release/github" 113 | ] 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/bin/makit.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import chalk from 'chalk' 4 | import yargs from 'yargs' 5 | import { Makefile } from '../makefile/makefile' 6 | import { existsSync } from 'fs' 7 | import { join } from 'path' 8 | import { Logger } from '../utils/logger' 9 | import { IO } from '../io' 10 | import { parse } from '../config' 11 | 12 | type OptionValue = string | undefined 13 | 14 | const argv = yargs.usage('$0 [OPTION] ...') 15 | .option('makefile', { 16 | alias: 'm', 17 | type: 'string', 18 | description: 'makefile path, defaults to "makefile.js"' 19 | }) 20 | .option('database', { 21 | alias: 'd', 22 | type: 'string', 23 | description: 'database file, will be used for cache invalidation, defaults to "./.makit.db"' 24 | }) 25 | .option('require', { 26 | alias: 'r', 27 | type: 'array', 28 | string: true, 29 | description: 'require a module before loading makefile.js or makefile.ts' 30 | }) 31 | .option('reporter', { 32 | type: 'string', 33 | choices: ['dot', 'text'], 34 | default: 'dot', 35 | description: '"dot", "text"' 36 | }) 37 | .option('verbose', { 38 | alias: 'v', 39 | type: 'boolean', 40 | description: 'set loglevel to verbose' 41 | }) 42 | .option('debug', { 43 | alias: 'v', 44 | type: 'boolean', 45 | description: 'set loglevel to debug' 46 | }) 47 | .option('loglevel', { 48 | alias: 'l', 49 | choices: [0, 1, 2, 3, 4], 50 | description: 'error, warning, info, verbose, debug' 51 | }) 52 | .option('graph', { 53 | alias: 'g', 54 | type: 'boolean', 55 | description: 'output dependency graph, defaults to false' 56 | }) 57 | .help('help') 58 | .conflicts('loglevel', 'verbose') 59 | .argv 60 | 61 | async function main () { 62 | const pkgjson = join(process.cwd(), 'package.json') 63 | const conf = parse(argv, existsSync(pkgjson) ? require(pkgjson) : {}) 64 | Logger.getOrCreate(conf.loglevel) 65 | IO.getOrCreateDataBase(conf.database) 66 | 67 | Logger.getOrCreate().info(chalk['cyan']('CONF'), conf.makefile) 68 | for (const specifier of conf.require || []) { 69 | require(require.resolve(specifier, { paths: [process.cwd()] })) 70 | } 71 | 72 | const makefile = global['makit'] = new Makefile(process.cwd(), conf.reporter) 73 | require(conf.makefile) 74 | 75 | const targets = argv._ 76 | await Promise.all(targets.length ? targets.map((target: string) => makefile.make(target)) : [makefile.make()]) 77 | if (conf.graph) { 78 | console.log(chalk['cyan']('TREE')) 79 | console.log(makefile.dependencyGraphString()) 80 | } 81 | } 82 | 83 | main().catch(err => console.error(err.stack)) 84 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import { LogLevel } from './utils/logger' 2 | import { resolve } from 'path' 3 | import { existsSync } from 'fs' 4 | import { DotReporter } from './reporters/dot-reporter' 5 | import { Reporter } from './reporters/reporter' 6 | import { TextReporter } from './reporters/text-reporter' 7 | 8 | type rawConfig = Partial<{ 9 | graph: boolean; 10 | makefile: string; 11 | database: string; 12 | require: string[]; 13 | loglevel: number; 14 | verbose: boolean; 15 | reporter: string; 16 | debug: boolean; 17 | }> 18 | 19 | export interface Config { 20 | loglevel: LogLevel; 21 | database: string; 22 | graph: boolean; 23 | makefile: string; 24 | reporter: Reporter; 25 | require: string[]; 26 | } 27 | 28 | const reporters = { 29 | dot: () => new DotReporter(), 30 | text: () => new TextReporter() 31 | } 32 | 33 | export function parse (args: rawConfig, pkg: { makit?: rawConfig }): Config { 34 | const defaults = { 35 | database: './.makit.db', 36 | makefile: ['makefile.js', 'makefile.ts'], 37 | graph: false 38 | } 39 | const raw = Object.assign(defaults, pkg.makit, args) 40 | 41 | const loglevel = raw.debug ? LogLevel.debug : (raw.verbose ? LogLevel.verbose : (raw.loglevel || LogLevel.default)) 42 | const database = raw.database as string 43 | const graph = raw.graph as boolean 44 | const require = raw.require as string[] 45 | const makefile = lookupMakefile(Array.isArray(raw.makefile) ? raw.makefile : [raw.makefile]) 46 | const reporter = reporters[raw.reporter!]() 47 | 48 | return { loglevel, database, graph, require, makefile, reporter } 49 | } 50 | 51 | function lookupMakefile (makefiles: string[]) { 52 | for (const makefile of makefiles) { 53 | if (existsSync(makefile)) return resolve(makefile) 54 | } 55 | throw new Error('makefile not found') 56 | } 57 | -------------------------------------------------------------------------------- /src/context.ts: -------------------------------------------------------------------------------- 1 | import { resolve, dirname } from 'path' 2 | import { MakeDirectoryOptions } from 'fs' 3 | import { Logger, hlTarget } from './utils/logger' 4 | import { FileSystem } from './fs/file-system' 5 | import { TimeStamp } from './fs/time-stamp' 6 | 7 | interface ContextOptions { 8 | target: string; 9 | match: RegExpExecArray | null; 10 | root: string; 11 | fs: FileSystem; 12 | make: (target: string) => Promise; 13 | } 14 | 15 | export class Context implements FileSystem { 16 | public readonly target: string 17 | public readonly match: RegExpExecArray | null 18 | public dependencies: string[] = [] 19 | public dynamicDependencies: string[] = [] 20 | public logger = Logger.getOrCreate() 21 | 22 | private readonly makeImpl: ContextOptions['make'] 23 | private readonly fs: FileSystem 24 | private readonly root: string 25 | 26 | constructor ({ target, match, root, fs, make }: ContextOptions) { 27 | this.root = root 28 | this.match = match 29 | this.target = target 30 | this.fs = fs 31 | this.makeImpl = make 32 | } 33 | 34 | public async make (target: string) { 35 | this.logger.debug('RUDE', 'context.make called with', hlTarget(target), 'while making', hlTarget(this.target)) 36 | this.dynamicDependencies.push(target) 37 | return this.makeImpl(target) 38 | } 39 | 40 | async outputFile (filepath: string, content: string | Buffer) { 41 | filepath = this.toFullPath(filepath) 42 | return this.writeFile(filepath, content).catch(async e => { 43 | if (e.code === 'ENOENT') { 44 | await this.mkdir(dirname(filepath), { recursive: true }) 45 | return this.writeFile(filepath, content) 46 | } 47 | throw e 48 | }) 49 | } 50 | 51 | outputFileSync (filepath: string, content: string | Buffer) { 52 | filepath = this.toFullPath(filepath) 53 | try { 54 | return this.fs.writeFileSync(filepath, content) 55 | } catch (e) { 56 | if (e.code === 'ENOENT') { 57 | this.fs.mkdirSync(dirname(filepath), { recursive: true }) 58 | return this.writeFileSync(filepath, content) 59 | } 60 | throw e 61 | } 62 | } 63 | 64 | async readDependency (i = 0): Promise { 65 | if (i >= this.dependencies.length) throw new Error(`cannot get ${i}th dependency,dependencieshis.deps.length} dependencies in total`) 66 | return this.readFile(this.dependencyFullPath(i)) 67 | } 68 | 69 | readDependencySync (i = 0): string { 70 | if (i >= this.dependencies.length) throw new Error(`cannot get ${i}th dependency,dependencieshis.deps.length} dependencies in total`) 71 | return this.readFileSync(this.dependencyFullPath(i), 'utf8') 72 | } 73 | 74 | targetFullPath () { 75 | return this.toFullPath(this.target) 76 | } 77 | 78 | targetPath () { 79 | return this.target 80 | } 81 | 82 | dependencyFullPath (i = 0): string { 83 | return this.toFullPath(this.dependencies[i]) 84 | } 85 | 86 | dependencyPath (i = 0): string { 87 | return this.dependencies[i] 88 | } 89 | 90 | writeTarget (content: string) { 91 | return this.outputFile(this.targetFullPath(), content) 92 | } 93 | 94 | writeTargetSync (content: string) { 95 | return this.outputFileSync(this.targetFullPath(), content) 96 | } 97 | 98 | toFullPath (filename: string) { 99 | return resolve(this.root, filename) 100 | } 101 | 102 | /** 103 | * FileSystem Implements 104 | */ 105 | async mkdir (filepath: string, options?: number | string | MakeDirectoryOptions | null) { 106 | return this.fs.mkdir(this.toFullPath(filepath), options) 107 | } 108 | 109 | mkdirSync (filepath: string, options: number | string | MakeDirectoryOptions | null) { 110 | return this.fs.mkdirSync(this.toFullPath(filepath), options) 111 | } 112 | 113 | async writeFile (filepath: string, content: string | Buffer) { 114 | return this.fs.writeFile(this.toFullPath(filepath), content) 115 | } 116 | 117 | writeFileSync (filepath: string, content: string | Buffer) { 118 | return this.fs.writeFileSync(this.toFullPath(filepath), content) 119 | } 120 | 121 | async readFile (filepath: string, encoding?: string): Promise 122 | async readFile (filepath: string, encoding = 'utf8'): Promise { 123 | return this.fs.readFile(this.toFullPath(filepath), encoding) 124 | } 125 | 126 | readFileSync (filepath: string, encoding: string): string 127 | readFileSync (filepath: string, encoding = 'utf8'): string | Buffer { 128 | return this.fs.readFileSync(this.toFullPath(filepath), encoding) 129 | } 130 | 131 | unlinkSync (filepath: string) { 132 | return this.fs.unlinkSync(this.toFullPath(filepath)) 133 | } 134 | 135 | unlink (filepath: string) { 136 | return this.fs.unlink(this.toFullPath(filepath)) 137 | } 138 | 139 | existsSync (filepath: string) { 140 | return this.fs.existsSync(this.toFullPath(filepath)) 141 | } 142 | 143 | utimes (filepath: string, atime: number, utime: number) { 144 | return this.fs.utimes(this.toFullPath(filepath), atime, utime) 145 | } 146 | 147 | utimesSync (filepath: string, atime: number, utime: number) { 148 | return this.fs.utimesSync(this.toFullPath(filepath), atime, utime) 149 | } 150 | 151 | stat (filepath: string) { 152 | return this.fs.stat(this.toFullPath(filepath)) 153 | } 154 | 155 | statSync (filepath: string) { 156 | return this.fs.statSync(this.toFullPath(filepath)) 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/db/data-base.ts: -------------------------------------------------------------------------------- 1 | import { FileSystem } from '../fs/file-system' 2 | import { inspect } from 'util' 3 | import { Logger } from '../utils/logger' 4 | import { humanReadable } from '../utils/number' 5 | import { DocumentCollection } from './document-collection' 6 | 7 | const l = Logger.getOrCreate() 8 | 9 | /** 10 | * 一个简易的 JSON 非关系性数据库。它的构成如下: 11 | * 12 | * * 一个 DataBase 对象由若干个 Document 构成 13 | * * 一个 Document 由若干个 Property 构成 14 | * 15 | * Note: sqlite 是各方面比较理想的替代品,但它有 Native Binding, 16 | * 能否成功安装会受网络、操作系统、Node 版本的影响,移植性不够。 17 | */ 18 | export class DataBase { 19 | private static instance: DataBase 20 | private data: DocumentCollection = {} 21 | private dirty = false 22 | 23 | constructor (private readonly filepath: string, private readonly fs: FileSystem) { 24 | this.readFromDisk() 25 | } 26 | 27 | /** 28 | * 查询文档属性 29 | * 30 | * @param doc 文档名 31 | * @param prop 属性名 32 | * @param defaultValue 如果没有,则返回的默认值 33 | */ 34 | query (doc: string, prop: string, defaultValue?: T) { 35 | if (!this.data[doc]) { 36 | return defaultValue 37 | } 38 | const value = this.data[doc][prop] 39 | return value !== undefined ? value : defaultValue 40 | } 41 | 42 | /** 43 | * 写入文档属性 44 | * 45 | * @param doc 文档名 46 | * @param prop 属性名 47 | * @param newValue 新的属性值 48 | */ 49 | write (doc: string, prop: string, newValue: T) { 50 | l.debug('DTBS', () => `setting ${doc}.${prop} to ${inspect(newValue)}`) 51 | this.dirty = true 52 | this.data[doc] = this.data[doc] || {} 53 | this.data[doc][prop] = newValue 54 | return this.data[doc][prop] 55 | } 56 | 57 | /** 58 | * 清空文档的所有属性,或清空数据库 59 | * 60 | * @param doc 文档名,如果不传则清空所有文档 61 | */ 62 | clear (doc?: string) { 63 | this.dirty = true 64 | if (doc) this.data[doc] = {} 65 | else this.data = {} 66 | } 67 | 68 | /** 69 | * 同步数据库到磁盘 70 | * 71 | * @throws 文件写入错误 72 | */ 73 | syncToDisk () { 74 | if (!this.dirty) { 75 | l.debug('DTBS', `documents clean, skip syncing`) 76 | return false 77 | } 78 | l.verbose('DTBS', () => `syncing to disk ${this.filepath}`) 79 | const data = Buffer.from(JSON.stringify(this.data), 'utf8') 80 | 81 | try { 82 | // Note: should be synchronous to handle exit event, 83 | // after which microtasks will not be scheduled or called. 84 | this.fs.writeFileSync(this.filepath, data) 85 | } catch (err) { 86 | err.message = 'Error sync to disk: ' + err.message 87 | throw err 88 | } 89 | 90 | l.verbose('DTBS', () => `${humanReadable(data.length)} bytes written to ${this.filepath}`) 91 | this.dirty = false 92 | return true 93 | } 94 | 95 | private readFromDisk () { 96 | let str: string, data: any 97 | 98 | try { 99 | str = this.fs.readFileSync(this.filepath, 'utf8') 100 | } catch (err) { 101 | if (err.code === 'ENOENT') { 102 | // ignore if not exists, will be created on first sync 103 | str = '{}' 104 | } else { 105 | throw err 106 | } 107 | } 108 | 109 | try { 110 | data = JSON.parse(str) 111 | } catch (err) { 112 | // ignore corrupted file, it will be regenerated anyway 113 | } 114 | 115 | if (typeof data === 'object' && data !== null) this.data = data 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/db/document-collection.ts: -------------------------------------------------------------------------------- 1 | import { Document } from './document' 2 | 3 | /** 4 | * Document 的集合 5 | */ 6 | export interface DocumentCollection { 7 | [key: string]: Document; 8 | } 9 | -------------------------------------------------------------------------------- /src/db/document.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Document: DataBase 中的数据单元。 3 | */ 4 | export interface Document { 5 | [key: string]: T; 6 | } 7 | -------------------------------------------------------------------------------- /src/db/index.ts: -------------------------------------------------------------------------------- 1 | export { Document } from './document' 2 | export { DocumentCollection } from './document-collection' 3 | export { DataBase } from './data-base' 4 | -------------------------------------------------------------------------------- /src/fs/file-system.ts: -------------------------------------------------------------------------------- 1 | import { Stats, MakeDirectoryOptions } from 'fs' 2 | 3 | /** 4 | * FileSystem API 集合。 5 | * 6 | * 是 memory-fs 和 Node.js 的交集。以下方法都是 makit 的依赖,要尽量少足够 makit 工作即可。 7 | */ 8 | export interface FileSystem { 9 | stat(path: string): Promise; 10 | statSync(path: string): Stats; 11 | 12 | readFile(path: string, encoding?: string): Promise; 13 | readFile(path: string, encoding?: string): Promise; 14 | 15 | readFileSync(path: string, encoding: string): string; 16 | readFileSync(path: string, encoding?: string): string | Buffer; 17 | 18 | writeFile(path: string, data: string | Buffer): Promise; 19 | writeFileSync(path: string, data: string | Buffer): void; 20 | 21 | mkdir(path: string, options?: number | string | MakeDirectoryOptions | null): Promise; 22 | mkdirSync(path: string, options?: number | string | MakeDirectoryOptions | null): void; 23 | 24 | unlink(path: string): Promise; 25 | unlinkSync(path: string): void; 26 | 27 | existsSync(path: string): boolean; 28 | 29 | utimes(path: string, atime: number, mtime: number): Promise; 30 | utimesSync(path: string, atime: number, mtime: number): void; 31 | } 32 | -------------------------------------------------------------------------------- /src/fs/memfs-impl.ts: -------------------------------------------------------------------------------- 1 | import { FileSystem } from './file-system' 2 | import { TimeStamp } from './time-stamp' 3 | import { resolve } from 'path' 4 | import { MakeDirectoryOptions, Stats } from 'fs' 5 | import MemoryFileSystemImpl from 'memory-fs' 6 | 7 | /** 8 | * memory-fs 对 FileSystem 接口的封装。目前这个类只用于跑测试。 9 | */ 10 | export class MemoryFileSystem implements FileSystem { 11 | private mtimes: Map = new Map() 12 | private fs = new MemoryFileSystemImpl() 13 | private now = 10000 14 | 15 | async mkdir (path: string, options: MakeDirectoryOptions = {}) { 16 | this.mkdirSync(path, options) 17 | } 18 | mkdirSync (path: string, options: MakeDirectoryOptions = {}) { 19 | const fullpath = resolve(path) 20 | options.recursive ? this.fs.mkdirpSync(fullpath) : this.fs.mkdirSync(fullpath) 21 | } 22 | 23 | readFile (path: string, encoding?: string) { 24 | return this.readFileSync(path, encoding) 25 | } 26 | readFileSync (path: string, encoding?: string) { 27 | const fullpath = resolve(path) 28 | return this.fs.readFileSync(fullpath, encoding) 29 | } 30 | 31 | async writeFile (path: string, data: string) { 32 | this.writeFileSync(path, data) 33 | } 34 | writeFileSync (path: string, data: any) { 35 | const fullpath = resolve(path) 36 | this.mtimes.set(fullpath, this.now++) 37 | return this.fs.writeFileSync(fullpath, data) 38 | } 39 | 40 | async stat (path: string) { 41 | return this.statSync(path) 42 | } 43 | 44 | statSync (path: string) { 45 | const fullpath = resolve(path) 46 | if (!this.mtimes.has(fullpath)) { 47 | const err = new Error(`file ${fullpath} not found`) as any 48 | err.code = 'ENOENT' 49 | throw err 50 | } 51 | return { 52 | mtime: new Date(this.mtimes.get(fullpath)!), 53 | mtimeMs: this.mtimes.get(fullpath) 54 | } as Stats 55 | } 56 | 57 | async unlink (path: string) { 58 | this.unlinkSync(path) 59 | } 60 | unlinkSync (path: string) { 61 | const fullpath = resolve(path) 62 | this.fs.unlinkSync(fullpath) 63 | } 64 | 65 | async exists (path: string) { 66 | return this.existsSync(path) 67 | } 68 | existsSync (path: string) { 69 | const fullpath = resolve(path) 70 | return this.fs.existsSync(fullpath) 71 | } 72 | 73 | async utimes (path: string, atime: number, mtime: number) { 74 | this.utimesSync(path, atime, mtime) 75 | } 76 | utimesSync (path: string, atime: number, mtime: number) { 77 | const fullpath = resolve(path) 78 | this.mtimes.set(fullpath, mtime) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/fs/mtime-document.ts: -------------------------------------------------------------------------------- 1 | import { Document } from '../db' 2 | import { TimeStamp } from './time-stamp' 3 | 4 | interface MTimeEntry { 5 | mtimeMs: TimeStamp; 6 | time: TimeStamp; 7 | } 8 | 9 | export interface MTimeDocument extends Document {} 10 | -------------------------------------------------------------------------------- /src/fs/mtime.ts: -------------------------------------------------------------------------------- 1 | import { TimeStamp } from './time-stamp' 2 | import { DataBase } from '../db' 3 | import { FileSystem } from './file-system' 4 | import { Logger, hlTarget } from '../utils/logger' 5 | 6 | /** 7 | * 获取单例日志对象 8 | */ 9 | const l = Logger.getOrCreate() 10 | 11 | /** 12 | * 表示文件不存在,时间戳设为最旧。让它和它的依赖处于 stale 状态。 13 | */ 14 | export const MTIME_NOT_EXIST: TimeStamp = -2 15 | 16 | /* 17 | * 表示依赖为空时,依赖的 mtime。要比所有存在的文件都旧, 18 | * 但比不存在的文件要新(因为无依赖的不存在文件也需要生成)。 19 | * 因此: 20 | * 21 | * MTIME_NOT_EXIST < MTIME_EMPTY_DEPENDENCY < mtimeNs < #now() 22 | */ 23 | export const MTIME_EMPTY_DEPENDENCY: TimeStamp = -1 24 | 25 | /** 26 | * 文件 mtime 的抽象 27 | * 28 | * 由于文件系统时间和 Node.js 的 Date API 不一致, 29 | * 会影响判断一个 target 是否 stale。 30 | * GNU Make 的 recipe 是独立进程而 makit 中是本地过程调用因此问题严重。 31 | * 一些实现细节: 32 | * 33 | * * mtime 使用正整数实现,特殊值取负数和 Infinity。 34 | * * 按 makit 第一次获知文件的顺序初始化 mtime。 35 | * * 同时提供 mtime 和严格递增的 now(),确保 now 和 mtime 一致。 36 | */ 37 | export class MTime { 38 | private static instance: MTime 39 | 40 | constructor (private readonly db: DataBase, private readonly fs: FileSystem) {} 41 | 42 | /** 43 | * 获取严格递增的当前时间戳 44 | * 45 | * @return 严格递增的当前时间戳 46 | */ 47 | now (): TimeStamp { 48 | const time = +this.db.query('meta', 'now', 0) + 1 49 | this.db.write('meta', 'now', time) 50 | return time 51 | } 52 | 53 | /** 54 | * 设置文件修改时间,不传则设置为现在 55 | * 56 | * @param fullpath 文件全路径 57 | * @param time 时间戳 58 | */ 59 | setModifiedTime (fullpath: string, time: TimeStamp = this.now()): TimeStamp { 60 | const mtimeMs = this.getModifiedTimeFromFileSystem(fullpath) 61 | if (mtimeMs === MTIME_NOT_EXIST) return mtimeMs 62 | 63 | this.db.write('mtime', fullpath, { mtimeMs, time }) 64 | l.debug('TIME', hlTarget(fullpath), `time set to`, time) 65 | return time 66 | } 67 | 68 | /** 69 | * 获取文件修改时间 70 | * 71 | * @param fullpath 文件全路径 72 | * @return 时间戳的 Promise 73 | */ 74 | getModifiedTime (fullpath: string): TimeStamp { 75 | const mtimeMs = this.getModifiedTimeFromFileSystem(fullpath) 76 | if (mtimeMs === MTIME_NOT_EXIST) return MTIME_NOT_EXIST 77 | 78 | let entry = this.queryAndValidate(fullpath, mtimeMs) 79 | if (!entry) { 80 | entry = { mtimeMs, time: this.now() } 81 | this.db.write('mtime', fullpath, entry) 82 | } 83 | return entry.time 84 | } 85 | 86 | private getModifiedTimeFromFileSystem (fullpath: string): TimeStamp { 87 | try { 88 | const { mtimeMs } = this.fs.statSync(fullpath) 89 | return mtimeMs 90 | } catch (error) { 91 | if (error.code === 'ENOENT') { 92 | return MTIME_NOT_EXIST 93 | } 94 | throw error 95 | } 96 | } 97 | 98 | /** 99 | * 简单的校验,就可以不依赖数据库的并发一致性。 100 | */ 101 | private queryAndValidate (fullpath: string, mtimeMs: TimeStamp) { 102 | const entry = this.db.query('mtime', fullpath) 103 | if (!entry || entry.mtimeMs !== mtimeMs) return null 104 | return entry 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/fs/nodefs-impl.ts: -------------------------------------------------------------------------------- 1 | import { FileSystem } from './file-system' 2 | import { promisify } from 'util' 3 | import * as fs from 'fs' 4 | 5 | const stat = promisify(fs.stat) 6 | const readFile = promisify(fs.readFile) 7 | const writeFile = promisify(fs.writeFile) 8 | const mkdir = promisify(fs.mkdir) 9 | const unlink = promisify(fs.unlink) 10 | const utimes = promisify(fs.utimes) 11 | 12 | export class NodeFileSystem implements FileSystem { 13 | stat (path: string): Promise { 14 | return stat(path) 15 | } 16 | statSync (path: string): fs.Stats { 17 | return fs.statSync(path) 18 | } 19 | 20 | readFile (path: string, encoding?: string): Promise 21 | readFile (path: string, encoding?: string): Promise { 22 | return readFile(path, encoding) 23 | } 24 | 25 | readFileSync (path: string, encoding: string): string; 26 | readFileSync (path: string, encoding?: string): string | Buffer { 27 | return fs.readFileSync(path, encoding) 28 | } 29 | 30 | writeFile (path: string, data: string): Promise { 31 | return writeFile(path, data) 32 | } 33 | writeFileSync (path: string, data: string): void { 34 | return fs.writeFileSync(path, data) 35 | } 36 | 37 | mkdir (path: string, options?: number | string | fs.MakeDirectoryOptions | null): Promise { 38 | return mkdir(path, options) 39 | } 40 | mkdirSync (path: string, options?: number | string | fs.MakeDirectoryOptions | null): void { 41 | return fs.mkdirSync(path, options) 42 | } 43 | 44 | unlink (path: string): Promise { 45 | return unlink(path) 46 | } 47 | unlinkSync (path: string) { 48 | return fs.unlinkSync(path) 49 | } 50 | 51 | existsSync (path: string) { 52 | return fs.existsSync(path) 53 | } 54 | 55 | utimes (path: string, atime: number, mtime: number): Promise { 56 | return utimes(path, atime, mtime) 57 | } 58 | utimesSync (path: string, atime: number, mtime: number) { 59 | return fs.utimesSync(path, atime, mtime) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/fs/time-stamp.ts: -------------------------------------------------------------------------------- 1 | export type TimeStamp = number 2 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Logger, LogLevel } from './utils/logger' 2 | import { RecipeDeclaration } from './makefile/recipe' 3 | import { IO } from './io' 4 | import { PrerequisitesDeclaration } from './makefile/prerequisites' 5 | 6 | export function setVerbose (val = true) { 7 | Logger.getOrCreate().setLevel(val ? LogLevel.verbose : LogLevel.default) 8 | } 9 | 10 | export function setDebug (val = true) { 11 | Logger.getOrCreate().setLevel(val ? LogLevel.debug : LogLevel.default) 12 | } 13 | 14 | export function setLoglevel (val: LogLevel) { 15 | Logger.getOrCreate().setLevel(val) 16 | } 17 | 18 | export function setRoot (val: string) { 19 | global['makit'].root = val 20 | } 21 | 22 | export function invalidate (target: string) { 23 | global['makit'].invalidate(target) 24 | } 25 | 26 | export function rule (target: string, prerequisites: PrerequisitesDeclaration, recipe?: RecipeDeclaration) { 27 | return global['makit'].addRule(target, prerequisites, recipe) 28 | } 29 | 30 | export function rude (target: string, prerequisites: PrerequisitesDeclaration, recipe?: RecipeDeclaration) { 31 | return global['makit'].addRude(target, prerequisites, recipe) 32 | } 33 | 34 | export function updateRule (target: string, prerequisites: PrerequisitesDeclaration, recipe?: RecipeDeclaration) { 35 | return global['makit'].updateRule(target, prerequisites, recipe) 36 | } 37 | 38 | export function updateOrAddRule (target: string, prerequisites: PrerequisitesDeclaration, recipe?: RecipeDeclaration) { 39 | return global['makit'].updateOrAddRule(target, prerequisites, recipe) 40 | } 41 | 42 | export function make (target: string) { 43 | return global['makit'].make(target) 44 | } 45 | 46 | export function disableCheckCircular () { 47 | global['makit'].disableCheckCircular = true 48 | } 49 | 50 | export { Makefile } from './makefile/makefile' 51 | 52 | export { Context } from './context' 53 | 54 | export { DirectedGraph } from './utils/graph' 55 | 56 | export { RecipeDeclaration } from './makefile/recipe' 57 | 58 | // Sync DB disk for normal exit 59 | process.on('exit', () => IO.getOrCreateDataBase().syncToDisk()) 60 | 61 | process.on('SIGINT', () => { 62 | IO.getOrCreateDataBase().syncToDisk() 63 | // Continue to exit, otherwise SIGINT is ignored 64 | process.exit(1) 65 | }) 66 | -------------------------------------------------------------------------------- /src/io.ts: -------------------------------------------------------------------------------- 1 | import { NodeFileSystem } from './fs/nodefs-impl' 2 | import { FileSystem } from './fs/file-system' 3 | import { MTime } from './fs/mtime' 4 | import { DataBase } from './db' 5 | 6 | export class IO { 7 | static fs: FileSystem 8 | static db?: DataBase 9 | static mtime?: MTime 10 | 11 | static getFileSystem (): FileSystem { 12 | if (!this.fs) this.fs = new NodeFileSystem() 13 | return this.fs 14 | } 15 | 16 | static getMTime ( 17 | db: DataBase = IO.getOrCreateDataBase(), 18 | fs: FileSystem = this.getFileSystem() 19 | ): MTime { 20 | if (!this.mtime) this.mtime = new MTime(db, fs) 21 | return this.mtime 22 | } 23 | 24 | static getOrCreateDataBase (filepath = '.makit.db', fs: FileSystem = this.getFileSystem()) { 25 | if (!this.db) this.db = new DataBase(filepath, fs) 26 | return this.db 27 | } 28 | 29 | static clearDataBase () { 30 | if (this.db) this.db.clear() 31 | } 32 | 33 | static resetFileSystem (newFS: FileSystem) { 34 | this.fs = newFS 35 | delete this.mtime 36 | delete this.db 37 | return { 38 | fs: newFS, 39 | db: this.getOrCreateDataBase(), 40 | mtime: this.getMTime() 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/make.ts: -------------------------------------------------------------------------------- 1 | import { Target } from './target' 2 | import { relation } from './utils/number' 3 | import { IO } from './io' 4 | import { MTIME_EMPTY_DEPENDENCY } from './fs/mtime' 5 | import { TimeStamp } from './fs/time-stamp' 6 | import { Rule } from './makefile/rule' 7 | import { isRudeDependencyFile } from './makefile/rude' 8 | import { Queue } from './utils/queue' 9 | import { Task } from './task' 10 | import { DirectedGraph } from './utils/graph' 11 | import { LogLevel, Logger, hlTarget } from './utils/logger' 12 | import { Reporter } from './reporters/reporter' 13 | 14 | const l = Logger.getOrCreate() 15 | 16 | export interface MakeOptions { 17 | root?: string; 18 | reporter: Reporter; 19 | disableCheckCircular?: boolean; 20 | matchRule: (target: string) => [Rule, RegExpExecArray] | null; 21 | } 22 | 23 | /** 24 | * 一个 Make 对象表示一次 make 25 | * 每次 make 的入口 target 是唯一的,其依赖图是一个有序图,用 checkCircular 来确保这一点 26 | */ 27 | export class Make { 28 | public dependencyGraph: DirectedGraph = new DirectedGraph() 29 | 30 | private targets: Map = new Map() 31 | private tasks: Map = new Map() 32 | private root: string 33 | private matchRule: (target: string) => [Rule, RegExpExecArray] | null 34 | private reporter: Reporter 35 | private disableCheckCircular: boolean 36 | private isMaking = false 37 | // ES Set 是有序集合(按照 add 顺序),在此用作队列顺便帮助去重 38 | private targetQueue: Queue = new Queue() 39 | 40 | constructor ({ 41 | root = process.cwd(), 42 | matchRule, 43 | disableCheckCircular, 44 | reporter 45 | }: MakeOptions) { 46 | this.root = root 47 | this.matchRule = matchRule 48 | this.reporter = reporter 49 | this.disableCheckCircular = disableCheckCircular || false 50 | } 51 | 52 | public async make (targetName: string, parent?: string): Promise { 53 | this.buildDependencyGraph(targetName, parent) 54 | for (const node of this.dependencyGraph.preOrder(targetName)) { 55 | const target = this.targets.get(node)! 56 | if (target.isReady()) this.scheduleTask(target) 57 | } 58 | l.verbose('GRAF', '0-indegree:', this.targetQueue) 59 | return new Promise((resolve, reject) => { 60 | const target = this.targets.get(targetName)! 61 | target.addPromise(resolve, reject) 62 | this.startMake() 63 | }) 64 | } 65 | 66 | private startMake () { 67 | if (this.isMaking) return 68 | this.isMaking = true 69 | 70 | while (this.targetQueue.size) { 71 | const task = this.targetQueue.pop()! 72 | if (task.isCanceled()) continue 73 | 74 | this.doMake(task.target) 75 | .then(() => { 76 | if (task.isCanceled()) return 77 | task.target.resolve() 78 | this.notifyDependants(task.target.name) 79 | }) 80 | .catch((err) => { 81 | if (task.isCanceled()) return 82 | // 让 target 以及依赖 target 的目标对应的 make promise 失败 83 | const dependants = this.dependencyGraph.getInVerticesRecursively(task.target.name) 84 | err['target'] = task.target.name 85 | for (const dependant of dependants) { 86 | this.targets.get(dependant)!.reject(err) 87 | } 88 | }) 89 | } 90 | this.isMaking = false 91 | } 92 | 93 | public invalidate (targetName: string) { 94 | const target = this.targets.get(targetName) 95 | 96 | // 还没编译到这个文件,或者这个文件根本不在依赖树里 97 | if (!target) return 98 | 99 | // 更新它的时间(用严格递增的虚拟时间来替代文件系统时间) 100 | target.updateMtime() 101 | 102 | const queue = new Set([target]) 103 | for (const node of queue) { 104 | for (const parent of this.dependencyGraph.getInVertices(node.name)) { 105 | const ptarget = this.targets.get(parent)! 106 | // 已经成功 make,意味着 parent 对 node 的依赖已经移除 107 | // 注意:不可用 isFinished,因为 isRejected() 的情况依赖并未移除 108 | if (node.isResolved()) ptarget.pendingDependencyCount++ 109 | queue.add(ptarget) 110 | } 111 | node.reset() 112 | } 113 | this.scheduleTask(target) 114 | this.startMake() 115 | } 116 | 117 | private buildDependencyGraph (node: string, parent?: string) { 118 | l.verbose('GRAF', 'node:', node, 'parent:', parent) 119 | this.dependencyGraph.addVertex(node) 120 | 121 | // 边 (parent, node) 不存在时才添加 122 | if (parent && !this.dependencyGraph.hasEdge(parent, node)) { 123 | this.dependencyGraph.addEdge(parent, node) 124 | if (!this.isResolved(node)) { 125 | this.targets.get(parent)!.pendingDependencyCount++ 126 | } 127 | } 128 | if (!this.disableCheckCircular) this.dependencyGraph.checkCircular(node) 129 | if (this.targets.has(node)) return 130 | 131 | const result = this.matchRule(node) 132 | const [rule, match] = result || [undefined, null] 133 | 134 | const target = this.createTarget({ target: node, match, rule }) 135 | this.targets.set(node, target) 136 | 137 | l.debug('DEPS', hlTarget(node), target.getDependencies()) 138 | for (const dep of target.getDependencies()) this.buildDependencyGraph(dep, node) 139 | } 140 | 141 | private isResolved (targetName: string) { 142 | const target = this.targets.get(targetName) 143 | return target && target.isResolved() 144 | } 145 | 146 | private async doMake (target: Target) { 147 | target.start() 148 | this.reporter.make(target) 149 | 150 | let dmtime = MTIME_EMPTY_DEPENDENCY 151 | for (const dep of this.dependencyGraph.getOutVerticies(target.name)) { 152 | dmtime = Math.max(dmtime, this.targets.get(dep)!.mtime!) 153 | } 154 | 155 | l.debug('TIME', hlTarget(target.name), () => `mtime(${target.mtime}) ${relation(target.mtime, dmtime)} dmtime(${dmtime})`) 156 | 157 | if (dmtime < target.mtime) { 158 | this.reporter.skip(target) 159 | } else { 160 | if (!target.rule) throw new Error(`no rule matched target: "${target.name}"`) 161 | await target.rule.recipe.make(target.ctx) 162 | if (target.rule.hasDynamicDependencies) await target.writeDependency() 163 | await target.updateMtime() 164 | this.reporter.made(target) 165 | } 166 | } 167 | 168 | private notifyDependants (targetName: string) { 169 | for (const dependant of this.dependencyGraph.getInVertices(targetName)) { 170 | const dependantTarget = this.targets.get(dependant)! 171 | --dependantTarget.pendingDependencyCount 172 | if (dependantTarget.isReady()) { 173 | this.scheduleTask(dependantTarget) 174 | this.startMake() 175 | } 176 | } 177 | } 178 | 179 | private scheduleTask (target: Target) { 180 | const task = new Task(target) 181 | if (this.tasks.has(target.name)) { 182 | this.tasks.get(target.name)!.cancel() 183 | } 184 | this.tasks.set(target.name, task) 185 | this.targetQueue.push(task) 186 | } 187 | 188 | private createTarget ({ target, match, rule }: { target: string; match: RegExpExecArray | null; rule?: Rule}) { 189 | return Target.create({ 190 | target, 191 | match, 192 | fs: IO.getFileSystem(), 193 | root: this.root, 194 | rule, 195 | make: (child: string) => this.make(child, target) 196 | }) 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/makefile/makefile.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from './rule' 2 | import { Make } from '../make' 3 | import { TimeStamp } from '../fs/time-stamp' 4 | import { Logger } from '../utils/logger' 5 | import { Prerequisites, PrerequisitesDeclaration } from './prerequisites' 6 | import { getDependencyFromTarget, clearDynamicDependencies, rudeExtname, dynamicPrerequisites } from './rude' 7 | import { Target, TargetDeclaration } from './target' 8 | import { cwd } from 'process' 9 | import { Recipe, RecipeDeclaration } from './recipe' 10 | import { Reporter } from '../reporters/reporter' 11 | import { DotReporter } from '../reporters/dot-reporter' 12 | 13 | const defaultRecipe = () => void (0) 14 | const logger = Logger.getOrCreate() 15 | 16 | /** 17 | * 给最终使用者的 makefile.js 中暴露的全局变量 makit 即为 Makefile 实例 18 | */ 19 | export class Makefile { 20 | public root: string 21 | public disableCheckCircular = false 22 | 23 | private ruleMap: Map = new Map() 24 | private fileTargetRules: Map = new Map() 25 | private matchingRules: Rule[] = [] 26 | private reporter: Reporter 27 | private makeImpl: Make 28 | 29 | constructor (root = cwd(), reporter: Reporter = new DotReporter()) { 30 | this.root = root 31 | this.reporter = reporter 32 | this.makeImpl = new Make({ 33 | root: this.root, 34 | reporter: this.reporter, 35 | matchRule: target => this.matchRule(target), 36 | disableCheckCircular: this.disableCheckCircular 37 | }) 38 | } 39 | 40 | public updateOrAddRule ( 41 | targetDecl: TargetDeclaration, 42 | prerequisitesDecl: PrerequisitesDeclaration = [], 43 | recipeDecl: RecipeDeclaration = defaultRecipe 44 | ) { 45 | if (this.ruleMap.has(targetDecl)) { 46 | this.updateRule(targetDecl, prerequisitesDecl, recipeDecl) 47 | } else { 48 | this.addRule(targetDecl, prerequisitesDecl, recipeDecl) 49 | } 50 | } 51 | 52 | public addRule ( 53 | targetDecl: TargetDeclaration, 54 | prerequisitesDecl: PrerequisitesDeclaration = [], 55 | recipeDecl: RecipeDeclaration = defaultRecipe 56 | ) { 57 | const target = new Target(targetDecl) 58 | const prerequisites = new Prerequisites(prerequisitesDecl) 59 | const recipe = new Recipe(recipeDecl) 60 | const rule = new Rule(target, prerequisites, recipe) 61 | logger.verbose('RULE', 'adding rule', rule) 62 | if (target.isFilePath()) { 63 | this.fileTargetRules.set(target.decl, rule) 64 | } else { 65 | this.matchingRules.push(rule) 66 | } 67 | this.ruleMap.set(target.decl, rule) 68 | return rule 69 | } 70 | 71 | public addRude ( 72 | targetDecl: TargetDeclaration, 73 | prerequisitesDecl: PrerequisitesDeclaration = [], 74 | recipeDecl: RecipeDeclaration = defaultRecipe 75 | ) { 76 | if (targetDecl instanceof RegExp) throw new Error('rude() for RegExp not supported yet') 77 | const rule = this.addRule(targetDecl, [prerequisitesDecl, '$0' + rudeExtname], recipeDecl) 78 | rule.hasDynamicDependencies = true 79 | 80 | this.addRule(getDependencyFromTarget(targetDecl), dynamicPrerequisites, clearDynamicDependencies) 81 | } 82 | 83 | public updateRule ( 84 | targetDecl: TargetDeclaration, 85 | prerequisitesDecl: PrerequisitesDeclaration = [], 86 | recipeDecl: RecipeDeclaration = defaultRecipe 87 | ) { 88 | const rule = this.ruleMap.get(targetDecl) 89 | if (!rule) { 90 | throw new Error(`rule for "${targetDecl}" not found`) 91 | } 92 | rule.prerequisites = new Prerequisites(prerequisitesDecl) 93 | rule.recipe = new Recipe(recipeDecl) 94 | } 95 | 96 | /** 97 | * makit 命令入口 98 | * 99 | * 命令行调用 makit 由本方法处理,具体的依赖递归解决由 Make 对象处理。 100 | */ 101 | public async make (target?: string): Promise { 102 | logger.resume() 103 | if (!target) { 104 | target = this.findFirstTargetOrThrow() 105 | } 106 | try { 107 | return await this.makeImpl.make(target) 108 | } catch (err) { 109 | // logger.suspend() 110 | if (err.target) { 111 | const chain = this.makeImpl.dependencyGraph.findPathToRoot(err.target) 112 | const target = chain.shift() 113 | err.message = `${err.message} while making "${target}"` 114 | for (const dep of chain) err.message += `\n required by "${dep}"` 115 | } 116 | throw err 117 | } 118 | } 119 | 120 | public invalidate (target: string) { 121 | this.makeImpl.invalidate(target) 122 | } 123 | 124 | public dependencyGraphString (): string { 125 | return this.makeImpl.dependencyGraph.toString() 126 | } 127 | 128 | private matchRule (target: string): [Rule, RegExpExecArray] | null { 129 | if (this.fileTargetRules.has(target)) { 130 | const match: RegExpExecArray = [target] as RegExpExecArray 131 | match.input = target 132 | match.index = 0 133 | return [this.fileTargetRules.get(target)!, match] 134 | } 135 | for (let i = this.matchingRules.length - 1; i >= 0; i--) { 136 | const rule = this.matchingRules[i] 137 | const match = rule.match(target) 138 | if (match) { 139 | return [rule, match] 140 | } 141 | } 142 | return null 143 | } 144 | 145 | private findFirstTarget (): string { 146 | return this.fileTargetRules.keys().next().value 147 | } 148 | 149 | private findFirstTargetOrThrow () { 150 | const target = this.findFirstTarget() 151 | if (!target) throw new Error('target not found') 152 | return target 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/makefile/prerequisites.ts: -------------------------------------------------------------------------------- 1 | import { Context } from '../context' 2 | import { inline } from '../utils/string' 3 | import { inspect } from 'util' 4 | 5 | const inspectSymbol = Symbol.for('nodejs.util.inspect.custom') || 'inspect' 6 | 7 | export type ResolvedItem = string | string[] 8 | export type Resolver = (context: Context) => ResolvedItem 9 | export type PrerequisitesDeclaration = ResolvedItem | Resolver | PrerequisiteArray 10 | export interface PrerequisiteArray extends Array {} 11 | 12 | export class Prerequisites { 13 | private dynamicDependencies: Map> = new Map() 14 | private decl: PrerequisitesDeclaration 15 | 16 | public constructor (decl: PrerequisitesDeclaration) { 17 | this.decl = decl 18 | } 19 | 20 | public getPrerequisites (ctx: Context): string[] { 21 | const results: string[] = [] 22 | this.addRequisites(ctx, this.decl, results) 23 | return results 24 | } 25 | 26 | addRequisites (ctx: Context, decl: PrerequisitesDeclaration, results: string[]) { 27 | if (typeof decl === 'string') { 28 | results.push(decl.match(/\$\d/) 29 | // 存在分组匹配,则从 match 数组获得匹配的分组 30 | ? decl.replace(/\$(\d+)/g, (_, i) => ctx.match![i]) 31 | : decl 32 | ) 33 | } else if (Array.isArray(decl)) { 34 | for (const item of decl) this.addRequisites(ctx, item, results) 35 | } else if (typeof decl === 'function') { 36 | this.addRequisites(ctx, decl(ctx), results) 37 | } else { 38 | throw new Error('invalid prerequisite:' + decl) 39 | } 40 | } 41 | 42 | [inspectSymbol] () { 43 | return inline(inspect(this.decl)) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/makefile/recipe.ts: -------------------------------------------------------------------------------- 1 | import { Context } from '../context' 2 | import { Callback } from '../utils/promise' 3 | import { TimeStamp } from '../fs/time-stamp' 4 | import { inline, limit } from '../utils/string' 5 | const inspectSymbol = Symbol.for('nodejs.util.inspect.custom') || 'inspect' 6 | 7 | export type RecipeDeclaration = 8 | (this: Context, ctx: Context, done?: Callback) 9 | => (any | Promise) 10 | 11 | export class Recipe { 12 | private fn: RecipeDeclaration 13 | 14 | constructor (fn: RecipeDeclaration) { 15 | this.fn = fn 16 | } 17 | 18 | public async make (context: Context): Promise { 19 | // Case: make('foo') 20 | if (this.fn.length < 2) { 21 | return this.fn.call(context, context) 22 | } 23 | 24 | // Case: make('foo', cb) 25 | return new Promise((resolve, reject) => { 26 | this.fn.call(context, context, (err, data) => { 27 | if (err) reject(err) 28 | else resolve(data) 29 | }) 30 | }) 31 | } 32 | 33 | [inspectSymbol] () { 34 | return limit(inline(this.fn.toString())) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/makefile/rude.ts: -------------------------------------------------------------------------------- 1 | import { Context } from '../context' 2 | import { Logger } from '../utils/logger' 3 | 4 | export const rudeExtname = '.rude.dep' 5 | 6 | export function getTargetFromDependency (dependencyFile: string) { 7 | return dependencyFile.slice(0, -rudeExtname.length) 8 | } 9 | 10 | export function getDependencyFromTarget (dependencyFile: string) { 11 | return dependencyFile + rudeExtname 12 | } 13 | 14 | export function isRudeDependencyFile (target: string) { 15 | return target.slice(-rudeExtname.length) === rudeExtname 16 | } 17 | 18 | export function dynamicPrerequisites (ctx: Context): string[] { 19 | const file = ctx.targetFullPath() 20 | 21 | let fileContent = '' 22 | try { 23 | fileContent = ctx.readFileSync(file, 'utf8') 24 | } catch (err) { 25 | if (err.code === 'ENOENT') return [] 26 | Logger.getOrCreate().verbose('dynamic deps', 'while reading', file, err) 27 | throw err 28 | } 29 | let json = [] 30 | try { 31 | json = JSON.parse(fileContent) 32 | } catch (err) { 33 | Logger.getOrCreate().warning('dynamic deps', 'corrupted', file, err.message, 'removing...') 34 | // remove corrupted dep file 35 | ctx.unlinkSync(file) 36 | } 37 | return json 38 | } 39 | 40 | export async function clearDynamicDependencies (ctx: Context) { 41 | // try { 42 | // // await ctx.unlink(ctx.targetFullPath()) 43 | // } catch (err) { 44 | // if (err.code === 'ENOENT') return 45 | // throw err 46 | // } 47 | } 48 | -------------------------------------------------------------------------------- /src/makefile/rule.ts: -------------------------------------------------------------------------------- 1 | import { Prerequisites } from './prerequisites' 2 | import { Target } from './target' 3 | import { Recipe } from './recipe' 4 | 5 | const inspect = Symbol.for('nodejs.util.inspect.custom') || 'inspect' 6 | 7 | export class Rule { 8 | public recipe: Recipe 9 | public target: Target 10 | public prerequisites: Prerequisites 11 | public hasDynamicDependencies = false 12 | 13 | constructor ( 14 | target: Target, 15 | prerequisites: Prerequisites, 16 | recipe: Recipe 17 | ) { 18 | this.target = target 19 | this.prerequisites = prerequisites 20 | this.recipe = recipe 21 | } 22 | 23 | public match (targetFile: string) { 24 | return this.target.exec(targetFile) 25 | } 26 | 27 | [inspect] () { 28 | let str = '\n' 29 | /** 30 | * Symbols are not allowed to index an object, 31 | * we need `suppressImplicitAnyIndexErrors` to suppress errors. 32 | * 33 | * see: https://github.com/microsoft/TypeScript/issues/1863 34 | */ 35 | str += this.target[inspect]() + ':' 36 | const deps = this.prerequisites[inspect]() 37 | if (deps) { 38 | str += ' ' + deps 39 | } 40 | if (this.hasDynamicDependencies) { 41 | if (deps) { 42 | str += ',' 43 | } 44 | str += ' [...dynamic]' 45 | } 46 | str += '\n' 47 | str += ' ' + this.recipe[inspect]() 48 | return str 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/makefile/target.ts: -------------------------------------------------------------------------------- 1 | import { inline } from '../utils/string' 2 | import { inspect } from 'util' 3 | 4 | const inspectSymbol = Symbol.for('nodejs.util.inspect.custom') || 'inspect' 5 | const extglob = require('extglob') 6 | const isGlob = require('is-glob') 7 | 8 | export type TargetDeclaration = string | RegExp 9 | export enum TargetType { 10 | glob, 11 | regexp, 12 | filepath 13 | } 14 | 15 | export class Target { 16 | private targetType: TargetType 17 | private _decl: TargetDeclaration 18 | private rTarget?: RegExp 19 | private glob?: string 20 | 21 | constructor (target: TargetDeclaration) { 22 | this._decl = target 23 | if (typeof target === 'string') { 24 | if (target.indexOf('(') > -1) { 25 | // Contains matching groups 26 | this.rTarget = new RegExp('^' + extglob(target).replace(/\(\?:/g, '(') + '$') 27 | } else { 28 | this.glob = target 29 | } 30 | } else { 31 | this.rTarget = target 32 | } 33 | this.targetType = target instanceof RegExp 34 | ? TargetType.regexp 35 | : ( 36 | isGlob(target) ? TargetType.glob : TargetType.filepath 37 | ) 38 | } 39 | 40 | public get decl () { 41 | return this._decl 42 | } 43 | 44 | public isFilePath (): this is {decl: string} { 45 | return this.targetType === TargetType.filepath 46 | } 47 | 48 | public exec (targetFile: string): RegExpExecArray | null { 49 | if (this.rTarget) return this.rTarget.exec(targetFile) 50 | return extglob.isMatch(targetFile, this.glob) ? Target.execArrayFromString(targetFile) : null 51 | } 52 | 53 | [inspectSymbol] () { 54 | return inline(inspect(this._decl)) 55 | } 56 | 57 | private static execArrayFromString (str: string): RegExpExecArray { 58 | const arr = [str] as RegExpExecArray 59 | arr.input = str 60 | arr.index = 0 61 | return arr 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/reporters/dot-reporter.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '../utils/logger' 2 | import { Reporter } from './reporter' 3 | import chalk from 'chalk' 4 | 5 | const grayDot = chalk.gray('.') 6 | const greenDot = chalk.green('.') 7 | 8 | export class DotReporter implements Reporter { 9 | constructor ( 10 | private l: Logger = Logger.getOrCreate() 11 | ) {} 12 | 13 | public make () { 14 | } 15 | 16 | public skip () { 17 | this.l.infoStr(grayDot) 18 | } 19 | 20 | public made () { 21 | this.l.infoStr(greenDot) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/reporters/reporter.ts: -------------------------------------------------------------------------------- 1 | import { Target } from '../target' 2 | 3 | export interface Reporter { 4 | /** 5 | * 表示开始 make 一个 target 6 | */ 7 | make (target: Target): void; 8 | 9 | /** 10 | * 表示一个正在 make 的 target 被直接跳过 11 | */ 12 | skip (target: Target): void; 13 | 14 | /** 15 | * 表示一个正在 make 的 target 的 recipe 被执行完成 16 | */ 17 | made (target: Target): void; 18 | } 19 | -------------------------------------------------------------------------------- /src/reporters/text-reporter.ts: -------------------------------------------------------------------------------- 1 | import { Logger, hlTarget } from '../utils/logger' 2 | import chalk from 'chalk' 3 | import { Target } from '../target' 4 | 5 | const skipLabel = chalk.gray('SKIP') 6 | const normalLabel = chalk.gray('MAKE') 7 | const madeLabel = chalk.green('MADE') 8 | 9 | export class TextReporter { 10 | constructor ( 11 | private l: Logger = Logger.getOrCreate() 12 | ) {} 13 | 14 | public make (target: Target) { 15 | this.l.verbose(normalLabel, hlTarget(target.name)) 16 | } 17 | 18 | public skip (target: Target) { 19 | this.l.info(skipLabel, hlTarget(target.name)) 20 | } 21 | 22 | public made (target: Target) { 23 | this.l.info(madeLabel, hlTarget(target.name)) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/target.ts: -------------------------------------------------------------------------------- 1 | import { Context } from './context' 2 | import { inspect } from 'util' 3 | import { FileSystem } from './fs/file-system' 4 | import { TimeStamp } from './fs/time-stamp' 5 | import { Logger, hlTarget } from './utils/logger' 6 | import { IO } from './io' 7 | import { isRudeDependencyFile, getDependencyFromTarget } from './makefile/rude' 8 | import { Rule } from './makefile/rule' 9 | 10 | const inspectKey = Symbol.for('nodejs.util.inspect.custom') || 'inspect' 11 | const logger = Logger.getOrCreate() 12 | 13 | interface TargetOptions { 14 | target: string; 15 | match: RegExpExecArray | null; 16 | root: string; 17 | fs: FileSystem; 18 | rule?: Rule; 19 | make: (target: string) => Promise; 20 | } 21 | 22 | enum TargetState { 23 | INIT = 0, 24 | STARTED = 1, 25 | RESOLVED = 2, 26 | REJECTED = 3 27 | } 28 | 29 | export class Target { 30 | name: string 31 | ctx: Context 32 | mtime: number 33 | state: TargetState = TargetState.INIT 34 | pendingDependencyCount = 0 35 | error?: Error 36 | rule?: Rule 37 | promises: [(t: TimeStamp) => void, (e: Error) => void][] = [] 38 | 39 | constructor (target: string, context: Context, mtime: number, rule?: Rule) { 40 | this.name = target 41 | this.ctx = context 42 | this.mtime = mtime 43 | this.rule = rule 44 | } 45 | 46 | public addPromise (resolve: (t: TimeStamp) => void, reject: (e: Error) => void) { 47 | if (this.state === TargetState.RESOLVED) resolve(this.mtime) 48 | else if (this.state === TargetState.REJECTED) reject(this.error!) 49 | else this.promises.push([resolve, reject]) 50 | } 51 | 52 | public resolve () { 53 | this.state = TargetState.RESOLVED 54 | while (this.promises.length) { 55 | const [resolve] = this.promises.shift()! 56 | resolve(this.mtime) 57 | } 58 | } 59 | 60 | public reject (err: Error) { 61 | this.state = TargetState.REJECTED 62 | while (this.promises.length) { 63 | const [, reject] = this.promises.shift()! 64 | reject(err) 65 | } 66 | this.error = err 67 | } 68 | 69 | public start () { 70 | this.state = TargetState.STARTED 71 | } 72 | 73 | public reset () { 74 | this.state = TargetState.INIT 75 | } 76 | 77 | public isStarted () { 78 | return this.state >= TargetState.STARTED 79 | } 80 | 81 | public isReady () { 82 | return this.state === TargetState.INIT && this.pendingDependencyCount <= 0 83 | } 84 | 85 | public isResolved () { 86 | return this.state === TargetState.RESOLVED 87 | } 88 | 89 | public isFinished () { 90 | return this.state === TargetState.RESOLVED || this.state === TargetState.REJECTED 91 | } 92 | 93 | public updateMtime () { 94 | this.mtime = IO.getMTime().setModifiedTime(this.ctx.targetFullPath()) 95 | } 96 | 97 | public static create ({ target, rule, match, root, fs, make }: TargetOptions) { 98 | const debug = target === '/Users/harttle/src/www-wise/cache/static/amd_modules/@searchfe/debug.js.inline' 99 | const context = new Context({ target, match, root, fs, make }) 100 | const mtime = IO.getMTime().getModifiedTime(context.targetFullPath()) 101 | 102 | if (rule) { 103 | context.dependencies.push( 104 | ...rule.prerequisites.getPrerequisites(context) 105 | ) 106 | } 107 | 108 | return new Target(target, context, mtime, rule) 109 | } 110 | 111 | public getDependencies () { 112 | return this.ctx.dependencies 113 | } 114 | 115 | public async writeDependency () { 116 | const filepath = getDependencyFromTarget(this.ctx.target) 117 | logger.debug('RUDE', 'writing', filepath, 'with', this.ctx.dynamicDependencies) 118 | await this.ctx.outputFile(filepath, JSON.stringify(this.ctx.dynamicDependencies)) 119 | await IO.getMTime().setModifiedTime(this.ctx.toFullPath(filepath)) 120 | } 121 | 122 | [inspectKey] () { 123 | const deps = [ 124 | ...this.ctx.dependencies, 125 | ...this.ctx.dynamicDependencies.map(dep => `${dep}(dynamic)`) 126 | ] 127 | return hlTarget(this.name) + ': ' + inspect(deps) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/task.ts: -------------------------------------------------------------------------------- 1 | import { Target } from './target' 2 | 3 | export class Task { 4 | public target: Target 5 | 6 | private canceled = false 7 | 8 | constructor (target: Target) { 9 | this.target = target 10 | } 11 | 12 | cancel () { 13 | this.canceled = true 14 | } 15 | 16 | isCanceled () { 17 | return this.canceled 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/graph.ts: -------------------------------------------------------------------------------- 1 | import { asTree } from 'treeify' 2 | 3 | const inspect = Symbol.for('nodejs.util.inspect.custom') || 'inspect' 4 | 5 | type Visitor = (vertex: T, stack: T[], visited: Set) => (void | false) 6 | 7 | type Tree = { [key: string]: Tree } 8 | 9 | enum VertexType { 10 | None = 0, 11 | In = 1, 12 | Out = 2, 13 | InOut = 3 14 | } 15 | 16 | export class DirectedGraph { 17 | private edges: Map> = new Map() 18 | private redges: Map> = new Map() 19 | private vertices: Map = new Map() 20 | private root?: T 21 | 22 | constructor (private readonly vertexToString: (v: T) => string = x => String(x) + '') {} 23 | 24 | /** 25 | * 增加一个点 26 | * 27 | * @param v 要增加的点 28 | * @param vertexType 点的类型(空、入、出、出入) 29 | */ 30 | addVertex (v: T, vertexType = VertexType.None) { 31 | const type = this.vertices.get(v) || VertexType.None 32 | this.vertices.set(v, type | vertexType) 33 | if (!this.root) this.root = v 34 | } 35 | 36 | /** 37 | * 增加一条边 38 | * 39 | * @param fr 边的起点 40 | * @param to 边的终点 41 | */ 42 | addEdge (fr: T, to: T) { 43 | if (!this.edges.has(fr)) { 44 | this.edges.set(fr, new Set()) 45 | } 46 | this.edges.get(fr)!.add(to) 47 | if (!this.redges.has(to)) { 48 | this.redges.set(to, new Set()) 49 | } 50 | this.redges.get(to)!.add(fr) 51 | 52 | this.addVertex(fr, VertexType.Out) 53 | this.addVertex(to, VertexType.In) 54 | } 55 | 56 | /** 57 | * 检查是否包含边 58 | * 59 | * @param fr 起点 60 | * @param to 终点 61 | * @return 包含返回 true 否则 false 62 | */ 63 | hasEdge (fr: T, to: T) { 64 | if (!this.edges.has(fr)) return false 65 | return this.edges.get(fr)!.has(to) 66 | } 67 | 68 | * getOutVerticies (u: T) { 69 | for (const v of this.edges.get(u) || []) yield v 70 | } 71 | 72 | * getInVertices (u: T) { 73 | for (const v of this.redges.get(u) || []) yield v 74 | } 75 | 76 | getOutDegree (u: T) { 77 | return this.edges.has(u) ? this.edges.get(u)!.size : 0 78 | } 79 | 80 | /** 81 | * 是否存在环状结构 82 | * 83 | * @return 如果存在返回一个 circuit,否则返回 null 84 | */ 85 | checkCircular (u: T, path: Set = new Set(), visited: Set = new Set()) { 86 | if (path.has(u)) { 87 | let patharr = [...path] 88 | patharr = [...patharr.slice(patharr.indexOf(u)), u] 89 | throw new Error(`Circular detected: ${patharr.join(' -> ')}`) 90 | } 91 | if (visited.has(u)) return 92 | else visited.add(u) 93 | 94 | path.add(u) 95 | for (const v of this.getOutVerticies(u)) { 96 | this.checkCircular(v, path, visited) 97 | } 98 | path.delete(u) 99 | } 100 | 101 | /** 102 | * 获取一条从 vertex 到 root 的路径 103 | * 104 | * @param vertex 路径的起点 105 | * @return 从 vertex 到 root 的路径 106 | */ 107 | findPathToRoot (vertex: T): T[] { 108 | const seen: Set = new Set() 109 | while (true) { 110 | // 出现循环引用时,数组收尾相同 111 | if (seen.has(vertex)) return [...seen, vertex] 112 | else seen.add(vertex) 113 | 114 | const parents = this.redges.get(vertex) 115 | if (!parents || !parents.size) break 116 | 117 | vertex = parents.values().next().value 118 | } 119 | return [...seen] 120 | } 121 | 122 | [inspect] () { 123 | return this.toString() 124 | } 125 | 126 | getInVerticesRecursively (target: T) { 127 | const dependants = new Set() 128 | const queue = [target] 129 | for (const node of queue) { 130 | if (dependants.has(node)) continue 131 | dependants.add(node) 132 | for (const parent of this.redges.get(node) || []) { 133 | queue.push(parent) 134 | } 135 | } 136 | return dependants 137 | } 138 | 139 | /** 140 | * 以第一个点为根的树的文本表示 141 | * 142 | * @return 树的 ASCII 文本表示 143 | */ 144 | toString () { 145 | if (!this.root) return '[Empty Tree]' 146 | const root = this.vertexToString(this.root) 147 | const tree = asTree(this.toTree(), false, false) 148 | return `${root}\n${tree}` 149 | } 150 | 151 | /** 152 | * 转化为 Plain Object 表示的树,用于 treeify 153 | * 154 | * 注意:使用前需要先调用 checkCircular(), 155 | * 或从数据上确保它是一棵树。 156 | * 157 | * @return 转为 Plain Object 的树状结构 158 | */ 159 | private toTree (root = this.root) { 160 | const tree: Tree = {} 161 | if (!root) throw new Error('root not found') 162 | for (const child of this.getOutVerticies(root)) { 163 | tree[this.vertexToString(child)] = this.toTree(child) 164 | } 165 | return tree 166 | } 167 | 168 | * preOrder (vertex: T, visited: Set = new Set()): IterableIterator { 169 | if (visited.has(vertex)) return 170 | else visited.add(vertex) 171 | 172 | yield vertex 173 | 174 | for (const child of this.getOutVerticies(vertex)) { 175 | yield * this.preOrder(child, visited) 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | 3 | export type FunctionMessage = () => string 4 | export type LogMessage = any | FunctionMessage 5 | 6 | export enum LogLevel { 7 | error = 0, 8 | warning = 1, 9 | info = 2, 10 | verbose = 3, 11 | debug = 4, 12 | default = 2 13 | } 14 | 15 | export function hlTarget (str: string) { 16 | return chalk['cyan'](str) 17 | } 18 | 19 | export class Logger { 20 | private static instance: Logger 21 | private suspended = false 22 | 23 | private constructor (private logLevel: LogLevel = LogLevel.default) { } 24 | 25 | public static getOrCreate (logLevel?: LogLevel) { 26 | if (!Logger.instance) { 27 | Logger.instance = new Logger(logLevel) 28 | } 29 | logLevel !== undefined && Logger.instance.setLevel(logLevel) 30 | return Logger.instance 31 | } 32 | 33 | public resume () { 34 | this.suspended = false 35 | } 36 | 37 | public suspend () { 38 | this.suspended = true 39 | } 40 | 41 | public setLevel (level: LogLevel) { 42 | if (level < LogLevel.error || level > LogLevel.debug) throw new Error('invalid loglevel') 43 | this.logLevel = level 44 | } 45 | 46 | public getLevel () { 47 | return this.logLevel 48 | } 49 | 50 | public error (title: string, ...args: LogMessage[]) { 51 | if (this.suspended || this.logLevel < LogLevel.error) return 52 | this.doLog(chalk.red(title), args) 53 | } 54 | 55 | public warning (title: string, ...args: LogMessage[]) { 56 | if (this.suspended || this.logLevel < LogLevel.warning) return 57 | this.doLog(chalk.yellow(title), args) 58 | } 59 | 60 | public info (title: string, ...args: LogMessage[]) { 61 | if (this.suspended || this.logLevel < LogLevel.info) return 62 | this.doLog(chalk.cyan(title), args) 63 | } 64 | 65 | public infoStr (str: string) { 66 | if (this.suspended || this.logLevel < LogLevel.info) return 67 | process.stdout.write(str) 68 | } 69 | 70 | public verbose (title: string, ...args: LogMessage[]) { 71 | if (this.suspended || this.logLevel < LogLevel.verbose) return 72 | this.doLog(chalk.gray(title), args) 73 | } 74 | 75 | public debug (title: string, ...args: LogMessage[]) { 76 | if (this.suspended || this.logLevel < LogLevel.debug) return 77 | this.doLog(chalk.magenta(title), args) 78 | } 79 | 80 | private doLog (title: string, args: LogMessage[]) { 81 | console.log(chalk.inverse(title), ...args.map(x => typeof x === 'function' ? x() : x)) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/utils/number.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 输出可读的数字,例如: 3 | * 4 | * 123456 -> 123,456 5 | * 1234.5 -> 1,234.5 6 | */ 7 | export function humanReadable (n: number) { 8 | const [wholePart, decimalPart] = String(n).split('.') 9 | let result = decimalPart ? '.' + decimalPart : '' 10 | for (let i = wholePart.length - 1; i >= 0; i--) { 11 | if ( 12 | wholePart[i] >= '0' && wholePart[i] <= '9' && 13 | i < wholePart.length - 1 && (wholePart.length - 1 - i) % 3 === 0 14 | ) { 15 | result = ',' + result 16 | } 17 | result = wholePart[i] + result 18 | } 19 | return result 20 | } 21 | 22 | /** 23 | * 数字关系的字符表示,用于日志 24 | */ 25 | export function relation (lhs: number, rhs: number) { 26 | if (lhs > rhs) return '>' 27 | if (lhs < rhs) return '<' 28 | if (lhs === rhs) return '=' 29 | return '?' 30 | } 31 | -------------------------------------------------------------------------------- /src/utils/promise.ts: -------------------------------------------------------------------------------- 1 | export function delay (milliSeconds: number) { 2 | return new Promise(resolve => { 3 | setTimeout(function () { 4 | resolve(milliSeconds) 5 | }, milliSeconds) 6 | }) 7 | } 8 | 9 | export type Callback = (err: null | Error, result?: T) => void 10 | -------------------------------------------------------------------------------- /src/utils/queue.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 简单的队列实现 3 | * 4 | * Array.prototype.shift 在数组较大时耗时明显增加,因此用 Set 实现。 5 | * 注意:重复元素入队会被忽略 6 | */ 7 | export class Queue { 8 | data: Set 9 | size = 0 10 | 11 | constructor () { 12 | this.data = new Set() 13 | } 14 | 15 | push (item: T): void { 16 | this.data.add(item) 17 | this.size = this.data.size 18 | } 19 | 20 | peek (): T | undefined { 21 | return this.data.values().next().value 22 | } 23 | 24 | pop (): T | undefined { 25 | if (!this.data.size) return 26 | const item = this.peek()! 27 | this.data.delete(item) 28 | this.size-- 29 | return item 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/utils/string.ts: -------------------------------------------------------------------------------- 1 | export function inline (str: string) { 2 | return str.split('\n').map(x => x.trim()).join(' ') 3 | } 4 | 5 | export function limit (str: string, len = 100) { 6 | return str.length > len ? str.slice(0, len - 3) + '...' : str 7 | } 8 | -------------------------------------------------------------------------------- /test/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../.eslintrc.json"], 3 | "env": { 4 | "jest/globals": true 5 | }, 6 | "globals": { 7 | "rule": true 8 | }, 9 | "plugins": [ 10 | "jest" 11 | ], 12 | "rules": { 13 | "no-new": "off" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/e2e/async.spec.ts: -------------------------------------------------------------------------------- 1 | import { Makefile } from '../../src/index' 2 | import { FileSystem } from '../../src/fs/file-system' 3 | import { createEnv } from '../stub/create-env' 4 | 5 | describe('async', function () { 6 | let fs: FileSystem 7 | let mk: Makefile 8 | beforeEach(() => { 9 | const env = createEnv({ logLevel: 1 }) 10 | fs = env.fs 11 | mk = env.mk 12 | }) 13 | 14 | it('should support async', async function () { 15 | fs.writeFileSync('a.js', 'a') 16 | mk.addRule('async.out', 'a.js', async function () { 17 | return this.writeTarget(this.dependencyPath()) 18 | }) 19 | await mk.make('async.out') 20 | 21 | expect(fs.readFileSync('async.out', 'utf8')).toEqual('a.js') 22 | }) 23 | 24 | it('should make one file only once', async function () { 25 | const recipe = jest.fn() 26 | 27 | mk.addRule('a', ['b', 'c']) 28 | mk.addRule('b', 'd') 29 | mk.addRule('c', 'd') 30 | mk.addRule('d', [], recipe as any) 31 | 32 | await mk.make('a') 33 | expect(recipe).toBeCalledTimes(1) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /test/e2e/circular.spec.ts: -------------------------------------------------------------------------------- 1 | import { Makefile } from '../../src/index' 2 | import { Logger, LogLevel } from '../../src/utils/logger' 3 | 4 | describe('circular', function () { 5 | beforeEach(function () { 6 | Logger.getOrCreate().setLevel(LogLevel.error) 7 | }) 8 | it('should detect circular', async function () { 9 | const mk = new Makefile(__dirname) 10 | 11 | mk.addRule('c1', 'c2') 12 | mk.addRule('c2', 'c3') 13 | mk.addRule('c3', 'c1') 14 | 15 | expect.assertions(1) 16 | await mk.make('c1').catch(e => { 17 | expect(e.message).toContain('Circular detected: c1 -> c2 -> c3 -> c1') 18 | }) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /test/e2e/error.spec.ts: -------------------------------------------------------------------------------- 1 | import { Makefile } from '../../src/index' 2 | import { FileSystem } from '../../src/fs/file-system' 3 | import { IO } from '../../src/io' 4 | import { Logger, LogLevel } from '../../src/utils/logger' 5 | import { MemoryFileSystem } from '../../src/fs/memfs-impl' 6 | 7 | describe('error', function () { 8 | let fs: FileSystem 9 | let mk: Makefile 10 | beforeEach(() => { 11 | fs = IO.resetFileSystem(new MemoryFileSystem()).fs 12 | fs.mkdirSync(process.cwd(), { recursive: true }) 13 | mk = new Makefile() 14 | Logger.getOrCreate().setLevel(LogLevel.error) 15 | }) 16 | 17 | it('should throw if no rule matched', async function () { 18 | expect.assertions(1) 19 | try { 20 | await mk.make('foo') 21 | } catch (err) { 22 | expect(err.message).toEqual('no rule matched target: "foo" while making "foo"') 23 | } 24 | }) 25 | 26 | it('should output parents when failed', async function () { 27 | expect.assertions(3) 28 | 29 | mk.addRule('foo', ['bar']) 30 | mk.addRule('bar', ['coo']) 31 | 32 | try { 33 | await mk.make('foo') 34 | } catch (err) { 35 | expect(err.message).toContain('while making "coo"') 36 | expect(err.message).toContain('required by "bar"') 37 | expect(err.message).toContain('required by "foo"') 38 | } 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /test/e2e/glob.spec.ts: -------------------------------------------------------------------------------- 1 | import { Makefile } from '../../src/index' 2 | import { MemoryFileSystem } from '../../src/fs/memfs-impl' 3 | import { IO } from '../../src/io' 4 | import { Logger, LogLevel } from '../../src/utils/logger' 5 | import md5 from 'md5' 6 | 7 | describe('glob', function () { 8 | let fs 9 | let mk: Makefile 10 | beforeEach(() => { 11 | fs = IO.resetFileSystem(new MemoryFileSystem()).fs 12 | fs.mkdirSync(process.cwd(), { recursive: true }) 13 | mk = new Makefile(process.cwd()) 14 | Logger.getOrCreate().setLevel(LogLevel.error) 15 | }) 16 | 17 | it('should support glob', async function () { 18 | const target = '*.md5.out' 19 | const prerequisites = () => 'a.js' 20 | const recipe = async function () { 21 | return this.writeTarget(md5(await this.readDependency())) 22 | } 23 | fs.writeFileSync('a.js', 'console.log(1)') 24 | mk.addRule(target, prerequisites, recipe) 25 | 26 | await mk.make('glob.md5.out') 27 | expect(fs.readFileSync('glob.md5.out', 'utf8')).toEqual('6114f5adc373accd7b2051bd87078f62') 28 | }) 29 | 30 | it('should support RegExp', async function () { 31 | const target = /\.md5\.out$/ 32 | const prerequisites = () => 'a.js' 33 | const recipe = async function () { 34 | return this.writeTarget(md5(await this.readDependency())) 35 | } 36 | fs.writeFileSync('a.js', 'console.log(1)') 37 | mk.addRule(target, prerequisites, recipe) 38 | 39 | await mk.make('glob.md5.out') 40 | expect(fs.readFileSync('glob.md5.out', 'utf8')).toEqual('6114f5adc373accd7b2051bd87078f62') 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /test/e2e/graph.spec.ts: -------------------------------------------------------------------------------- 1 | import { Makefile } from '../../src/index' 2 | import { createEnv } from '../stub/create-env' 3 | 4 | describe('graph', function () { 5 | let mk: Makefile 6 | beforeEach(() => { 7 | const env = createEnv({ logLevel: 1 }) 8 | mk = env.mk 9 | }) 10 | 11 | it('should print single target', async function () { 12 | mk.addRule('a.js', [], () => void (0)) 13 | await mk.make('a.js') 14 | expect(mk.dependencyGraphString()).toEqual('a.js\n') 15 | }) 16 | 17 | it('should print a single dependency', async function () { 18 | mk.addRule('a.min.js', ['a.js'], () => void (0)) 19 | mk.addRule('a.js', [], () => void (0)) 20 | 21 | await mk.make('a.min.js') 22 | expect(mk.dependencyGraphString()).toEqual(`a.min.js 23 | └─ a.js 24 | ` 25 | ) 26 | }) 27 | 28 | it('should print multiple dependencies', async function () { 29 | mk.addRule('a.min.js', ['a.js', 'b.js'], () => void (0)) 30 | mk.addRule('a.js', [], () => void (0)) 31 | mk.addRule('b.js', [], () => void (0)) 32 | 33 | await mk.make('a.min.js') 34 | expect(mk.dependencyGraphString()).toEqual(`a.min.js 35 | ├─ a.js 36 | └─ b.js 37 | ` 38 | ) 39 | }) 40 | 41 | it('should print recursive dependencies', async function () { 42 | mk.addRule('a.min.js', ['a.js'], () => void (0)) 43 | mk.addRule('a.js', ['b.js'], () => void (0)) 44 | mk.addRule('b.js', [], () => void (0)) 45 | 46 | await mk.make('a.min.js') 47 | expect(mk.dependencyGraphString()).toEqual(`a.min.js 48 | └─ a.js 49 | └─ b.js 50 | ` 51 | ) 52 | }) 53 | 54 | it('should print a subtree for each reference', async function () { 55 | mk.addRule('top', ['left', 'right'], () => void (0)) 56 | mk.addRule('left', ['bottom'], () => void (0)) 57 | mk.addRule('right', ['bottom'], () => void (0)) 58 | mk.addRule('bottom', ['bottom1', 'bottom2'], () => void (0)) 59 | mk.addRule('bottom1', [], () => void (0)) 60 | mk.addRule('bottom2', [], () => void (0)) 61 | 62 | await mk.make('top') 63 | expect(mk.dependencyGraphString()).toEqual(`top 64 | ├─ left 65 | │ └─ bottom 66 | │ ├─ bottom1 67 | │ └─ bottom2 68 | └─ right 69 | └─ bottom 70 | ├─ bottom1 71 | └─ bottom2 72 | ` 73 | ) 74 | }) 75 | }) 76 | -------------------------------------------------------------------------------- /test/e2e/invalidate.spec.ts: -------------------------------------------------------------------------------- 1 | import { Makefile } from '../../src/index' 2 | import { FileSystem } from '../../src/fs/file-system' 3 | import { createEnv } from '../stub/create-env' 4 | 5 | describe('invalidate', function () { 6 | let fs: FileSystem 7 | let mk: Makefile 8 | beforeEach(() => { 9 | const env = createEnv({ logLevel: 1 }) 10 | fs = env.fs 11 | mk = env.mk 12 | }) 13 | 14 | it('should remake if dependency file newly created', async function () { 15 | const recipeA = jest.fn() 16 | const recipeB = jest.fn() 17 | 18 | mk.addRule('a', ['b'], recipeA) 19 | mk.addRule('b', [], recipeB) 20 | await mk.make('a') 21 | expect(recipeA).toBeCalledTimes(1) 22 | expect(recipeB).toBeCalledTimes(1) 23 | 24 | fs.writeFileSync('b', 'x') 25 | mk.invalidate('b') 26 | 27 | await mk.make('a') 28 | expect(recipeA).toBeCalledTimes(2) 29 | expect(recipeB).toBeCalledTimes(1) 30 | }) 31 | 32 | it('should remake if dependency file updated', async function () { 33 | const recipe = jest.fn(ctx => ctx.writeTarget('_')) 34 | mk.addRule('foo', ['bar'], recipe) 35 | mk.addRule('bar', [], ctx => ctx.writeTarget('_')) 36 | await mk.make('foo') 37 | expect(recipe).toBeCalledTimes(1) 38 | 39 | fs.writeFileSync('bar', 'x') 40 | mk.invalidate('bar') 41 | 42 | await mk.make('foo') 43 | expect(recipe).toBeCalledTimes(2) 44 | }) 45 | 46 | it('should remake if recursive dependency file updated', async function () { 47 | const recipeFoo = jest.fn(ctx => ctx.writeTarget('_')) 48 | const recipeBar = jest.fn(ctx => ctx.writeTarget('_')) 49 | const recipeCoo = jest.fn(ctx => ctx.writeTarget('_')) 50 | mk.addRule('foo', ['bar'], recipeFoo) 51 | mk.addRule('bar', ['coo'], recipeBar) 52 | mk.addRule('coo', [], recipeCoo) 53 | await mk.make('foo') 54 | expect(recipeFoo).toBeCalledTimes(1) 55 | 56 | fs.writeFileSync('coo', 'x') 57 | mk.invalidate('coo') 58 | 59 | await mk.make('foo') 60 | expect(recipeFoo).toBeCalledTimes(2) 61 | }) 62 | 63 | it('should allow successive innvalidate', async function () { 64 | const recipeFoo = jest.fn(ctx => ctx.writeTarget('_')) 65 | const recipeBar = jest.fn(ctx => ctx.writeTarget('_')) 66 | const recipeCoo = jest.fn(ctx => ctx.writeTarget('_')) 67 | mk.addRule('foo', ['bar'], recipeFoo) 68 | mk.addRule('bar', ['coo'], recipeBar) 69 | mk.addRule('coo', [], recipeCoo) 70 | await mk.make('foo') 71 | expect(recipeFoo).toBeCalledTimes(1) 72 | 73 | fs.writeFileSync('coo', 'x') 74 | mk.invalidate('coo') 75 | mk.invalidate('coo') 76 | 77 | await mk.make('foo') 78 | expect(recipeFoo).toBeCalledTimes(2) 79 | }) 80 | }) 81 | -------------------------------------------------------------------------------- /test/e2e/local-files.spec.ts: -------------------------------------------------------------------------------- 1 | import { Makefile } from '../../src/index' 2 | import { delay } from '../../src/utils/promise' 3 | import { NodeFileSystem } from '../../src/fs/nodefs-impl' 4 | import { writeFileSync, statSync } from 'fs' 5 | import { removeSync } from 'fs-extra' 6 | import { createEnv } from '../stub/create-env' 7 | 8 | describe('local files', function () { 9 | const output0 = 'test/e2e/bundle0.js.out' 10 | const output1 = 'test/e2e/bundle1.js.out' 11 | const input0 = 'test/e2e/input0.js.out' 12 | const input1 = 'test/e2e/input1.js.out' 13 | beforeEach(() => { 14 | createEnv({ logLevel: 1, fs: new NodeFileSystem() }) 15 | removeSync(output0) 16 | removeSync(input0) 17 | removeSync(output1) 18 | removeSync(input1) 19 | }) 20 | 21 | it('should call recipe before make resolve', async () => { 22 | writeFileSync(input0, Math.random()) 23 | const mk = new Makefile() 24 | let recipeTimes = 0 25 | 26 | mk.addRule(output0, input0, ctx => { 27 | writeFileSync(ctx.targetFullPath(), statSync(ctx.dependencyFullPath()).mtimeMs) 28 | recipeTimes++ 29 | }) 30 | await mk.make(output0) 31 | expect(recipeTimes).toEqual(1) 32 | await mk.make(output0) 33 | expect(recipeTimes).toBeGreaterThanOrEqual(1) 34 | }) 35 | 36 | it('should remake if dependency changed', async () => { 37 | writeFileSync(input1, Math.random()) 38 | let mk = new Makefile() 39 | let recipeTimes = 0 40 | const recipe = ctx => { 41 | writeFileSync(ctx.targetFullPath(), statSync(ctx.dependencyFullPath()).mtimeMs) 42 | recipeTimes++ 43 | } 44 | 45 | mk.addRule(output1, input1, recipe) 46 | await mk.make(output1) 47 | 48 | // say, we touched that file manually before next make 49 | await delay(50) 50 | writeFileSync(input1, Math.random()) 51 | 52 | mk = new Makefile() 53 | mk.addRule(output1, input1, recipe) 54 | await mk.make(output1) 55 | expect(recipeTimes).toEqual(2) 56 | }) 57 | }) 58 | -------------------------------------------------------------------------------- /test/e2e/local-make.spec.ts: -------------------------------------------------------------------------------- 1 | import { Makefile } from '../../src/index' 2 | import { MemoryFileSystem } from '../../src/fs/memfs-impl' 3 | import { IO } from '../../src/io' 4 | import { Logger, LogLevel } from '../../src/utils/logger' 5 | 6 | describe('local make', function () { 7 | let fs 8 | beforeEach(() => { 9 | fs = IO.resetFileSystem(new MemoryFileSystem()).fs 10 | fs.mkdirSync(process.cwd(), { recursive: true }) 11 | Logger.getOrCreate().setLevel(LogLevel.error) 12 | }) 13 | 14 | it('should support call local make inside recipe', async function () { 15 | const mk = new Makefile() 16 | fs.writeFileSync('a.js', 'a') 17 | 18 | mk.addRule('b.out', 'a.js', ctx => ctx.writeTarget('B')) 19 | mk.addRule('c.out', 'a.js', async ctx => { 20 | await ctx.make('b.out') 21 | await ctx.writeTarget(await ctx.readFile('b.out')) 22 | }) 23 | 24 | await mk.make('c.out') 25 | expect(fs.readFileSync('c.out', 'utf8')).toEqual('B') 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /test/e2e/pattern.spec.ts: -------------------------------------------------------------------------------- 1 | import { Makefile } from '../../src/index' 2 | import { FileSystem } from '../../src/types/fs' 3 | import { createEnv } from '../stub/create-env' 4 | 5 | describe('patterns', function () { 6 | let fs: FileSystem 7 | let mk: Makefile 8 | beforeEach(() => { 9 | const env = createEnv({ logLevel: 1 }) 10 | fs = env.fs 11 | mk = env.mk 12 | }) 13 | 14 | it('should support matching groups', async function () { 15 | fs.mkdirSync('src/foo', { recursive: true }) 16 | fs.writeFileSync('src/foo/a.san.js', 'a') 17 | 18 | mk.addRule( 19 | '(src/**)/(*).md5', '$1/$2.san.js', 20 | async ctx => ctx.writeTarget(await ctx.readDependency()) 21 | ) 22 | 23 | await mk.make('src/foo/a.md5') 24 | expect(fs.readFileSync('src/foo/a.md5', 'utf8')).toEqual('a') 25 | }) 26 | 27 | it('should match from begin to end', async function () { 28 | mk.addRule('build/**.js', []) 29 | mk.addRule('build', [], () => { 30 | throw new Error('should not fire build!') 31 | }) 32 | await mk.make('build/a.js') 33 | 34 | mk.addRule('(src)/**.js', []) 35 | mk.addRule('s(rc)', [], () => { 36 | throw new Error('should not fire src!') 37 | }) 38 | await mk.make('src/a.js') 39 | }) 40 | 41 | it('should support groups in dependency array', async function () { 42 | const recipe = jest.fn() 43 | mk.addRule('src/(**).min.js', ['src/$1.js'], recipe) 44 | mk.addRule('src/a.js', [], () => void (0)) 45 | await mk.make('src/a.min.js') 46 | expect(recipe).toHaveBeenCalledWith(expect.objectContaining({ 47 | dependencies: ['src/a.js'] 48 | })) 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /test/e2e/recursive.spec.ts: -------------------------------------------------------------------------------- 1 | import { Makefile } from '../../src/index' 2 | import { FileSystem } from '../../src/fs/file-system' 3 | import { createEnv } from '../stub/create-env' 4 | 5 | describe('recursive', function () { 6 | let fs: FileSystem 7 | let mk: Makefile 8 | beforeEach(() => { 9 | const env = createEnv({ logLevel: 1 }) 10 | fs = env.fs 11 | mk = env.mk 12 | }) 13 | it('should recursively resolve prerequisites', async function () { 14 | mk.addRule('a.js', [], ctx => ctx.writeTarget('a.js')) 15 | mk.addRule('a.out', 'a.js', async ctx => ctx.writeTarget('a.out')) 16 | mk.addRule('b.out', 'a.out', async ctx => ctx.writeTarget(await ctx.readDependency())) 17 | 18 | await mk.make('b.out') 19 | expect(fs.readFileSync('b.out', 'utf8')).toEqual('a.out') 20 | }) 21 | 22 | it('should not rebuild if required twice', async function () { 23 | fs.writeFileSync('a.js', 'A') 24 | 25 | const a = jest.fn() 26 | const a2b = jest.fn() 27 | const a2c = jest.fn() 28 | const bc2d = jest.fn() 29 | 30 | mk.addRule('recursive.a.out', 'a.js', a) 31 | mk.addRule('recursive.b.out', 'recursive.a.out', a2b) 32 | mk.addRule('recursive.c.out', 'recursive.a.out', a2c) 33 | mk.addRule('recursive.d.out', ['recursive.b.out', 'recursive.c.out'], bc2d) 34 | 35 | await mk.make('recursive.d.out') 36 | 37 | expect(a).toHaveBeenCalledTimes(1) 38 | expect(a2b).toHaveBeenCalledTimes(1) 39 | expect(a2c).toHaveBeenCalledTimes(1) 40 | expect(bc2d).toHaveBeenCalledTimes(1) 41 | }) 42 | 43 | it('should remake if dependency file not exists', async function () { 44 | const recipeA = jest.fn() 45 | const recipeB = jest.fn() 46 | 47 | mk.addRule('a', ['b'], recipeA) 48 | mk.addRule('b', [], recipeB) 49 | await mk.make('a') 50 | expect(recipeA).toBeCalledTimes(1) 51 | expect(recipeB).toBeCalledTimes(1) 52 | 53 | mk = new Makefile() 54 | mk.addRule('a', ['b'], recipeA) 55 | mk.addRule('b', [], recipeB) 56 | await mk.make('a') 57 | expect(recipeA).toBeCalledTimes(2) 58 | expect(recipeB).toBeCalledTimes(2) 59 | }) 60 | 61 | it('should remake if recursive dependency file not exists', async function () { 62 | const recipeA = jest.fn() 63 | const recipeB = jest.fn() 64 | const recipeC = jest.fn() 65 | 66 | mk.addRule('a', ['b'], recipeA) 67 | mk.addRule('b', ['c'], recipeB) 68 | mk.addRule('c', [], recipeC) 69 | await mk.make('a') 70 | expect(recipeA).toBeCalledTimes(1) 71 | expect(recipeB).toBeCalledTimes(1) 72 | expect(recipeC).toBeCalledTimes(1) 73 | 74 | mk = new Makefile() 75 | mk.addRule('a', ['b'], recipeA) 76 | mk.addRule('b', ['c'], recipeB) 77 | mk.addRule('c', [], recipeC) 78 | await mk.make('a') 79 | expect(recipeA).toBeCalledTimes(2) 80 | expect(recipeB).toBeCalledTimes(2) 81 | expect(recipeC).toBeCalledTimes(2) 82 | }) 83 | 84 | it('should not remake if dependency file not updated', async function () { 85 | const recipe = jest.fn(ctx => ctx.writeTarget('_')) 86 | mk.addRule('foo', ['bar'], recipe) 87 | mk.addRule('bar', [], ctx => ctx.writeTarget('_')) 88 | 89 | await mk.make('foo') 90 | expect(recipe).toBeCalledTimes(1) 91 | 92 | mk = new Makefile() 93 | mk.addRule('foo', ['bar'], recipe) 94 | mk.addRule('bar', [], ctx => ctx.writeTarget('_')) 95 | await mk.make('foo') 96 | expect(recipe).toBeCalledTimes(1) 97 | }) 98 | 99 | it('should remake if dependency file updated', async function () { 100 | const recipe = jest.fn(ctx => ctx.writeTarget('_')) 101 | mk.addRule('foo', ['bar'], recipe) 102 | mk.addRule('bar', [], ctx => ctx.writeTarget('_')) 103 | await mk.make('foo') 104 | expect(recipe).toBeCalledTimes(1) 105 | 106 | fs.writeFileSync('bar', 'x') 107 | 108 | mk = new Makefile() 109 | mk.addRule('foo', ['bar'], recipe) 110 | mk.addRule('bar', [], ctx => ctx.writeTarget('_')) 111 | await mk.make('foo') 112 | expect(recipe).toBeCalledTimes(2) 113 | }) 114 | 115 | it('should not remake if recursive dependency file not updated', async function () { 116 | const recipeFoo = jest.fn(ctx => ctx.writeTarget('_')) 117 | const recipeBar = jest.fn(ctx => ctx.writeTarget('_')) 118 | const recipeCoo = jest.fn(ctx => ctx.writeTarget('_')) 119 | mk.addRule('foo', ['bar'], recipeFoo) 120 | mk.addRule('bar', ['coo'], recipeBar) 121 | mk.addRule('coo', [], recipeCoo) 122 | 123 | await mk.make('foo') 124 | expect(recipeFoo).toBeCalledTimes(1) 125 | 126 | await mk.make('foo') 127 | expect(recipeFoo).toBeCalledTimes(1) 128 | }) 129 | 130 | it('should remake if recursive dependency file updated', async function () { 131 | const recipeFoo = jest.fn(ctx => ctx.writeTarget('_')) 132 | const recipeBar = jest.fn(ctx => ctx.writeTarget('_')) 133 | const recipeCoo = jest.fn(ctx => ctx.writeTarget('_')) 134 | mk.addRule('foo', ['bar'], recipeFoo) 135 | mk.addRule('bar', ['coo'], recipeBar) 136 | mk.addRule('coo', [], recipeCoo) 137 | await mk.make('foo') 138 | expect(recipeFoo).toBeCalledTimes(1) 139 | 140 | fs.writeFileSync('coo', 'x') 141 | 142 | mk = new Makefile() 143 | mk.addRule('foo', ['bar'], recipeFoo) 144 | mk.addRule('bar', ['coo'], recipeBar) 145 | mk.addRule('coo', [], recipeCoo) 146 | await mk.make('foo') 147 | expect(recipeFoo).toBeCalledTimes(2) 148 | }) 149 | }) 150 | -------------------------------------------------------------------------------- /test/e2e/rude.spec.ts: -------------------------------------------------------------------------------- 1 | import { Makefile } from '../../src/index' 2 | import { FileSystem } from '../../src/fs/file-system' 3 | import { createEnv } from '../stub/create-env' 4 | 5 | describe('rude', function () { 6 | let fs: FileSystem 7 | let mk: Makefile 8 | beforeEach(() => { 9 | const env = createEnv({ logLevel: 1 }) 10 | fs = env.fs 11 | mk = env.mk 12 | }) 13 | 14 | it('should call corresponding recipe for ctx.make', async function () { 15 | mk.addRule('foo', [], async ctx => { 16 | ctx.writeTarget('_') 17 | await ctx.make('bar') 18 | }) 19 | 20 | const recipeBar = jest.fn() 21 | mk.addRule('bar', [], recipeBar) 22 | 23 | await mk.make('foo') 24 | expect(recipeBar).toBeCalledTimes(1) 25 | }) 26 | 27 | it('should remake if dynamic dependency updated', async function () { 28 | const recipeFoo = jest.fn(async ctx => { 29 | ctx.writeTarget('_') 30 | await ctx.make('bar') 31 | }) 32 | const recipeBar = jest.fn(ctx => ctx.writeTarget('_')) 33 | 34 | mk.addRude('foo', [], recipeFoo) 35 | mk.addRule('bar', [], recipeBar) 36 | 37 | await mk.make('foo') 38 | expect(recipeFoo).toBeCalledTimes(1) 39 | 40 | fs.writeFileSync('bar', 'x') 41 | 42 | mk = new Makefile() 43 | mk.addRude('foo', [], recipeFoo) 44 | mk.addRule('bar', [], recipeBar) 45 | await mk.make('foo') 46 | expect(recipeFoo).toBeCalledTimes(2) 47 | }) 48 | 49 | it('should not remake if dynamic dependency not updated', async function () { 50 | const recipeFoo = jest.fn(async ctx => { 51 | await ctx.make('bar') 52 | await ctx.writeTarget('_') 53 | }) 54 | const recipeBar = jest.fn(ctx => ctx.writeTarget('_')) 55 | 56 | mk.addRude('foo', [], recipeFoo) 57 | mk.addRule('bar', [], recipeBar) 58 | await mk.make('foo') 59 | expect(recipeFoo).toBeCalledTimes(1) 60 | expect(recipeBar).toBeCalledTimes(1) 61 | 62 | mk = new Makefile() 63 | mk.addRude('foo', [], recipeFoo) 64 | mk.addRule('bar', [], recipeBar) 65 | await mk.make('foo') 66 | expect(recipeFoo).toBeCalledTimes(1) 67 | expect(recipeBar).toBeCalledTimes(1) 68 | }) 69 | 70 | it('should remake if dependency updated', async function () { 71 | const recipeFoo = jest.fn(async ctx => { 72 | ctx.writeTarget('_') 73 | await ctx.make('bar') 74 | }) 75 | const recipeBar = jest.fn(ctx => ctx.writeTarget('_')) 76 | 77 | mk.addRude('foo', ['coo'], recipeFoo) 78 | mk.addRule('bar', [], recipeBar) 79 | mk.addRule('coo', [], ctx => ctx.writeTarget('coo')) 80 | await mk.make('foo') 81 | expect(recipeFoo).toBeCalledTimes(1) 82 | 83 | fs.writeFileSync('coo', 'COO') 84 | 85 | mk = new Makefile() 86 | mk.addRude('foo', ['coo'], recipeFoo) 87 | mk.addRule('bar', [], recipeBar) 88 | mk.addRule('coo', [], ctx => ctx.writeTarget('coo')) 89 | await mk.make('foo') 90 | expect(recipeFoo).toBeCalledTimes(2) 91 | }) 92 | }) 93 | -------------------------------------------------------------------------------- /test/e2e/simple.spec.ts: -------------------------------------------------------------------------------- 1 | import { Makefile } from '../../src/index' 2 | import { IO } from '../../src/io' 3 | import { Logger, LogLevel } from '../../src/utils/logger' 4 | import { MemoryFileSystem } from '../../src/fs/memfs-impl' 5 | import { FileSystem } from '../../src/fs/file-system' 6 | 7 | describe('simple', function () { 8 | let fs: FileSystem 9 | let mk: Makefile 10 | beforeEach(() => { 11 | fs = IO.resetFileSystem(new MemoryFileSystem()).fs 12 | fs.mkdirSync(process.cwd(), { recursive: true }) 13 | mk = new Makefile() 14 | Logger.getOrCreate().setLevel(LogLevel.error) 15 | }) 16 | 17 | it('should build simple transform', async function () { 18 | fs.writeFileSync('a.js', 'a') 19 | 20 | mk.addRule('simple.out', 'a.js', function () { 21 | fs.writeFileSync(this.targetFullPath(), this.dependencyPath()) 22 | }) 23 | await mk.make('simple.out') 24 | 25 | expect(fs.readFileSync('simple.out', 'utf8')).toEqual('a.js') 26 | }) 27 | 28 | it('should build the first task by default', async function () { 29 | fs.writeFileSync('a.js', 'a') 30 | 31 | mk.addRule('simple.out', 'a.js', ctx => { 32 | fs.writeFileSync(ctx.targetFullPath(), ctx.dependencyPath()) 33 | }) 34 | await mk.make() 35 | 36 | expect(fs.readFileSync('simple.out', 'utf8')).toEqual('a.js') 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /test/stub/create-env.ts: -------------------------------------------------------------------------------- 1 | import { Makefile } from '../../src/index' 2 | import { IO } from '../../src/io' 3 | import { Logger, LogLevel } from '../../src/utils/logger' 4 | import { MemoryFileSystem } from '../../src/fs/memfs-impl' 5 | import { FileSystem } from '../../src/fs/file-system' 6 | import { TextReporter } from '../../src/reporters/text-reporter' 7 | 8 | export function createEnv ({ 9 | fs = new MemoryFileSystem(), 10 | logLevel 11 | }: { 12 | fs?: FileSystem; 13 | logLevel?: LogLevel; 14 | }) { 15 | const { db } = IO.resetFileSystem(fs) 16 | const mk = new Makefile(undefined, new TextReporter()) 17 | 18 | if (!fs.existsSync(process.cwd())) { 19 | fs.mkdirSync(process.cwd(), { recursive: true }) 20 | } 21 | if (logLevel) Logger.getOrCreate().setLevel(logLevel) 22 | return { fs, db, mk } 23 | } 24 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "declaration": true, 5 | "skipLibCheck": true, 6 | "module": "CommonJS", 7 | "outDir": "dist", 8 | "noImplicitAny": false, 9 | "allowSyntheticDefaultImports": true, 10 | "moduleResolution": "Node", 11 | "resolveJsonModule": true, 12 | "downlevelIteration": true, 13 | "strict": false, 14 | "suppressImplicitAnyIndexErrors": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/unit/config.spec.ts: -------------------------------------------------------------------------------- 1 | import { parse } from '../../src/config' 2 | 3 | describe('config', () => { 4 | it('should merge CLI argv and package.json', () => { 5 | const conf = parse({ loglevel: 2, reporter: 'dot' }, { makit: { makefile: __filename } }) 6 | expect(conf).toHaveProperty('loglevel', 2) 7 | expect(conf).toHaveProperty('makefile', __filename) 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /test/unit/context.spec.ts: -------------------------------------------------------------------------------- 1 | import { Context } from '../../src/context' 2 | 3 | describe('Context', function () { 4 | it('should resolve dependencyFullPath from root', function () { 5 | const ctx = new Context({ root: '/foo', dependencies: ['a'], target: 'b', match: [] } as any) 6 | expect(ctx.targetFullPath()).toEqual('/foo/b') 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /test/unit/db/data-base.spec.ts: -------------------------------------------------------------------------------- 1 | import { DataBase } from '../../../src/db/data-base' 2 | import { Logger, LogMessage } from '../../../src/utils/logger' 3 | 4 | describe('DataBase', () => { 5 | describe('#new()', () => { 6 | it('should read correct file via fs.readFileSync()', () => { 7 | const readFileSync = jest.fn() 8 | new DataBase('some-file', { readFileSync } as any) 9 | expect(readFileSync).toBeCalledWith('some-file', 'utf8') 10 | }) 11 | 12 | it('should not throw for malformed file', () => { 13 | expect(() => new DataBase('some-file', { 14 | readFileSync: () => '({[<' 15 | } as any)).not.toThrow() 16 | }) 17 | 18 | it('should not throw when file not exist', () => { 19 | expect(() => new DataBase('some-file', { 20 | readFileSync: () => { 21 | const err = new Error() 22 | err['code'] = 'ENOENT' 23 | throw err 24 | } 25 | } as any)).not.toThrow() 26 | }) 27 | 28 | it('should throw when file not readable', () => { 29 | const err = new Error('permission denied') 30 | err['code'] = 'EACCES' 31 | expect(() => new DataBase('some-file', { 32 | readFileSync: () => { throw err } 33 | } as any)).toThrow(err) 34 | }) 35 | }) 36 | 37 | describe('#query()', () => { 38 | it('should return existing value', () => { 39 | const db = new DataBase('foo', { 40 | readFileSync: () => '{"doc": {"prop": "FOO"}}' 41 | } as any) 42 | expect(db.query('doc', 'prop')).toEqual('FOO') 43 | }) 44 | 45 | it('should return undefined for non-existed property', () => { 46 | const db = new DataBase('foo', { 47 | readFileSync: () => '{"doc": {"prop": "FOO"}}' 48 | } as any) 49 | expect(db.query('doc', 'prop1')).toBeUndefined() 50 | }) 51 | 52 | it('should return undefined for non-existed doc', () => { 53 | const db = new DataBase('foo', { 54 | readFileSync: () => '{"doc": {"prop": "FOO"}}' 55 | } as any) 56 | expect(db.query('doc1', 'prop')).toBeUndefined() 57 | }) 58 | 59 | it('should use default value if property not exist', () => { 60 | const db = new DataBase('foo', { 61 | readFileSync: () => '{"doc": {"prop": "FOO"}}' 62 | } as any) 63 | expect(db.query('doc', 'prop1', 'BAR')).toEqual('BAR') 64 | }) 65 | 66 | it('should use default value if document not exist', () => { 67 | const db = new DataBase('foo', { 68 | readFileSync: () => '{"doc": {"prop": "FOO"}}' 69 | } as any) 70 | expect(db.query('doc1', 'prop', 'BAR')).toEqual('BAR') 71 | }) 72 | }) 73 | 74 | describe('#write()', () => { 75 | it('should create if document not exist', () => { 76 | const db = new DataBase('foo', { 77 | readFileSync: () => '{}' 78 | } as any) 79 | db.write('foo', 'prop', 'BAR') 80 | expect(db.query('foo', 'prop')).toEqual('BAR') 81 | }) 82 | 83 | it('should create if property not exist', () => { 84 | const db = new DataBase('foo', { 85 | readFileSync: () => '{"doc": {}}' 86 | } as any) 87 | db.write('foo', 'prop', 2) 88 | expect(db.query('foo', 'prop')).toEqual(2) 89 | }) 90 | 91 | it('should overwrite existing property', () => { 92 | const db = new DataBase('foo', { 93 | readFileSync: () => '{"doc": {"prop": 1}}' 94 | } as any) 95 | db.write('foo', 'prop', 2) 96 | expect(db.query('foo', 'prop')).toEqual(2) 97 | }) 98 | }) 99 | 100 | describe('#clear()', () => { 101 | it('should clear specified document', () => { 102 | const db = new DataBase('foo', { 103 | readFileSync: () => JSON.stringify({ 104 | doc1: { foo: 'FOO' }, 105 | doc2: { bar: 'BAR' } 106 | }) 107 | } as any) 108 | db.clear('doc1') 109 | expect(db.query('doc1', 'foo')).toEqual(undefined) 110 | expect(db.query('doc2', 'bar')).toEqual('BAR') 111 | }) 112 | 113 | it('should clear all if document not specified', () => { 114 | const db = new DataBase('foo', { 115 | readFileSync: () => JSON.stringify({ 116 | doc1: { foo: 'FOO' }, 117 | doc2: { bar: 'BAR' } 118 | }) 119 | }) 120 | db.clear() 121 | expect(db.query('doc1', 'foo')).toEqual(undefined) 122 | expect(db.query('doc2', 'bar')).toEqual(undefined) 123 | }) 124 | }) 125 | 126 | describe('#syncToDisk()', () => { 127 | it('should write all documents via writeFileSync()', () => { 128 | const writeFileSync = jest.fn() 129 | const db = new DataBase('foo.db', { 130 | readFileSync: () => '{"doc": {"prop": "FOO"}}', 131 | writeFileSync 132 | } as any) 133 | db.write('doc1', 'prop', 'BAR') 134 | db.syncToDisk() 135 | expect(writeFileSync).toBeCalledWith( 136 | 'foo.db', 137 | Buffer.from('{"doc":{"prop":"FOO"},"doc1":{"prop":"BAR"}}') 138 | ) 139 | }) 140 | 141 | it('should skip write if no writes yet (not dirty)', () => { 142 | const writeFileSync = jest.fn() 143 | const db = new DataBase('foo.db', { 144 | readFileSync: () => '{"doc": {"prop": "FOO"}}', 145 | writeFileSync 146 | } as any) 147 | db.syncToDisk() 148 | expect(writeFileSync).not.toBeCalled() 149 | }) 150 | 151 | it('should skip write if already in sync (not dirty)', () => { 152 | const writeFileSync = jest.fn() 153 | const db = new DataBase('foo.db', { 154 | readFileSync: () => '{"doc": {"prop": "FOO"}}', 155 | writeFileSync 156 | } as any) 157 | db.write('doc1', 'prop', 'BAR') 158 | db.syncToDisk() 159 | expect(writeFileSync).toBeCalledTimes(1) 160 | db.syncToDisk() 161 | expect(writeFileSync).toBeCalledTimes(1) 162 | }) 163 | 164 | it('should write if dirty again', () => { 165 | const writeFileSync = jest.fn() 166 | const db = new DataBase('foo.db', { 167 | readFileSync: () => '{"doc": {"prop": "FOO"}}', 168 | writeFileSync 169 | } as any) 170 | db.write('doc1', 'prop', 'BAR') 171 | db.syncToDisk() 172 | expect(writeFileSync).toBeCalledTimes(1) 173 | db.write('doc1', 'prop', 'BAR') 174 | db.syncToDisk() 175 | expect(writeFileSync).toBeCalledTimes(2) 176 | }) 177 | }) 178 | describe('log', () => { 179 | const logger = Logger.getOrCreate() 180 | const db = new DataBase('foo.db', { 181 | readFileSync: jest.fn(), 182 | writeFileSync: jest.fn() 183 | } as any) 184 | let verbose: jest.MockInstance 185 | let debug: jest.MockInstance 186 | 187 | beforeEach(() => { 188 | verbose = jest.spyOn(logger, 'verbose') 189 | debug = jest.spyOn(logger, 'debug') 190 | }) 191 | afterEach(() => { 192 | verbose.mockRestore() 193 | debug.mockRestore() 194 | }) 195 | 196 | it('should debug output when setting property value', () => { 197 | db.write('doc1', 'prop', 'BAR') 198 | const args: any = debug.mock.calls[0] 199 | expect(args[0]).toEqual('DTBS') 200 | expect(args[1]()).toEqual(`setting doc1.prop to 'BAR'`) 201 | }) 202 | 203 | it('should verbose output when syncToDisk() begin', () => { 204 | db.write('doc1', 'prop', 'BAR') 205 | db.syncToDisk() 206 | const args: any = verbose.mock.calls[0] 207 | expect(args[0]).toEqual('DTBS') 208 | expect(args[1]()).toEqual('syncing to disk foo.db') 209 | }) 210 | 211 | it('should verbose output bytes synced', () => { 212 | db.write('doc1', 'prop', 'BAR') 213 | db.syncToDisk() 214 | const len = '{"doc1":{"prop":"BAR"}}'.length 215 | const args: any = verbose.mock.calls[1] 216 | expect(args[0]).toEqual('DTBS') 217 | expect(args[1]()).toEqual(`${len} bytes written to foo.db`) 218 | }) 219 | }) 220 | }) 221 | -------------------------------------------------------------------------------- /test/unit/fs/mtime.spec.ts: -------------------------------------------------------------------------------- 1 | import { MTime, MTIME_NOT_EXIST } from '../../../src/fs/mtime' 2 | import { DataBase } from '../../../src/db' 3 | import { FileSystem } from '../../../src/fs/file-system' 4 | import { MemoryFileSystem } from '../../../src/fs/memfs-impl' 5 | 6 | describe('MTime', () => { 7 | let fs: FileSystem 8 | beforeEach(() => { 9 | fs = new MemoryFileSystem() 10 | }) 11 | 12 | describe('#now()', () => { 13 | it('should strictly increase', () => { 14 | const mtime = new MTime(new DataBase('a.db', fs), fs) 15 | const [t1, t2, t3] = [mtime.now(), mtime.now(), mtime.now()] 16 | expect(t1).toBeLessThan(t2) 17 | expect(t2).toBeLessThan(t3) 18 | }) 19 | }) 20 | 21 | describe('#getModifiedTime()', () => { 22 | it('should respect to discovery order', async () => { 23 | fs.writeFileSync('/foo', 'foo') 24 | fs.writeFileSync('/bar', 'bar') 25 | 26 | const mtime = new MTime(new DataBase('a.db', fs), fs) 27 | const m1 = await mtime.getModifiedTime('/foo') 28 | const m2 = await mtime.getModifiedTime('/bar') 29 | 30 | expect(m1).toBeLessThan(m2) 31 | }) 32 | 33 | it('should strictly increase', async () => { 34 | fs.writeFileSync('/foo', 'foo') 35 | fs.writeFileSync('/bar', 'bar') 36 | 37 | const mtime = new MTime(new DataBase('a.db', fs), fs) 38 | const m1 = await mtime.getModifiedTime('/foo') 39 | const t = mtime.now() 40 | const m2 = await mtime.getModifiedTime('/bar') 41 | expect(m1).toBeLessThan(t) 42 | expect(t).toBeLessThan(m2) 43 | }) 44 | 45 | it('should use former mtime as long as valid', async () => { 46 | fs.writeFileSync('/foo', 'foo') 47 | 48 | const mtime = new MTime(new DataBase('a.db', fs), fs) 49 | const m1 = await mtime.getModifiedTime('/foo') 50 | const m2 = await mtime.getModifiedTime('/foo') 51 | 52 | expect(m1).toEqual(m2) 53 | }) 54 | 55 | it('should invalidate mtime if file changed', async () => { 56 | fs.writeFileSync('/foo', 'foo') 57 | 58 | const mtime = new MTime(new DataBase('a.db', fs), fs) 59 | const m1 = await mtime.getModifiedTime('/foo') 60 | fs.writeFileSync('/foo', 'FOO') 61 | const m2 = await mtime.getModifiedTime('/foo') 62 | expect(m2).toBeGreaterThan(m1) 63 | }) 64 | 65 | it('should return MTIME_NOT_EXIST if file not found', async () => { 66 | const mtime = new MTime(new DataBase('a.db', fs), fs) 67 | const m1 = await mtime.getModifiedTime('/foo') 68 | 69 | expect(m1).toEqual(MTIME_NOT_EXIST) 70 | }) 71 | }) 72 | 73 | describe('#setModifiedTime()', () => { 74 | it('should set modified time for existing file', async () => { 75 | fs.writeFileSync('/foo', 'foo') 76 | const mtime = new MTime(new DataBase('a.db', fs), fs) 77 | const setValue = await mtime.setModifiedTime('/foo', 5) 78 | const getValue = await mtime.getModifiedTime('/foo') 79 | 80 | expect(setValue).toEqual(5) 81 | expect(getValue).toEqual(5) 82 | }) 83 | 84 | it('should return MTIME_NOT_EXIST if file not found', async () => { 85 | const mtime = new MTime(new DataBase('a.db', fs), fs) 86 | const setValue = await mtime.setModifiedTime('/foo', 5) 87 | const getValue = await mtime.getModifiedTime('/foo') 88 | 89 | expect(setValue).toEqual(MTIME_NOT_EXIST) 90 | expect(getValue).toEqual(MTIME_NOT_EXIST) 91 | }) 92 | 93 | it('should set to now() if time not specified', async () => { 94 | fs.writeFileSync('/foo', 'foo') 95 | 96 | const mtime = new MTime(new DataBase('a.db', fs), fs) 97 | const setValue = await mtime.setModifiedTime('/foo') 98 | const then = mtime.now() - 1 99 | 100 | expect(setValue).toEqual(then) 101 | }) 102 | }) 103 | }) 104 | -------------------------------------------------------------------------------- /test/unit/makefile/recipe.spec.ts: -------------------------------------------------------------------------------- 1 | import { Recipe } from '../../../src/makefile/recipe' 2 | 3 | describe('Recipe', function () { 4 | const fakeContext: any = { 5 | targetFullPath: () => '' 6 | } 7 | it('should make a single return statement', async function () { 8 | const spy = jest.fn() 9 | const recipe = new Recipe(spy) 10 | await recipe.make(fakeContext) 11 | expect(spy).toHaveBeenCalledTimes(1) 12 | }) 13 | it('should make a promise', async function () { 14 | const spy = jest.fn(() => Promise.resolve('foo')) 15 | const recipe = new Recipe(spy) 16 | await recipe.make(fakeContext) 17 | expect(spy).toBeCalledTimes(1) 18 | }) 19 | it('should reject by promise', function () { 20 | expect.assertions(1) 21 | const recipe = new Recipe(() => Promise.reject(new Error('foo'))) 22 | return recipe.make(fakeContext).catch(e => expect(e.message).toEqual('foo')) 23 | }) 24 | it('should make a callback', async function () { 25 | const spy = jest.fn((ctx, done) => done(null)) 26 | const recipe = new Recipe(spy) 27 | await recipe.make(fakeContext) 28 | expect(spy).toBeCalledTimes(1) 29 | }) 30 | it('should reject by callback', function () { 31 | expect.assertions(1) 32 | const recipe = new Recipe((ctx, done) => done(new Error('foo'))) 33 | return recipe.make(fakeContext).catch(e => expect(e.message).toEqual('foo')) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /test/unit/makefile/target.spec.ts: -------------------------------------------------------------------------------- 1 | import { Target } from '../../../src/makefile/target' 2 | 3 | describe('Target', function () { 4 | it('should support exact match', function () { 5 | const t = new Target('a.ts') 6 | 7 | expect(t.exec('a.ts')).toInclude('a.ts') 8 | expect(t.exec('a.js')).toBeNull() 9 | }) 10 | 11 | it('should support wildcard', function () { 12 | const t = new Target('*.ts') 13 | expect(t.exec('a.ts')).toInclude('a.ts') 14 | expect(t.exec('a.js')).toBeNull() 15 | }) 16 | 17 | it('should support groups', function () { 18 | const t = new Target('(*)/(*).ts') 19 | expect(t.exec('foo/bar.ts')).toIncludeMultiple(['foo/bar.ts', 'foo', 'bar']) 20 | }) 21 | 22 | it('should support glob negation', function () { 23 | const t = new Target('src/!(*.ts)') 24 | expect(t.exec('src/a.js')).toInclude('src/a.js') 25 | expect(t.exec('src/b.ts')).toBeNull() 26 | }) 27 | 28 | it('should support glob negation with groups', function () { 29 | const t = new Target('(*)/!(*.ts)') 30 | expect(t.exec('src/a.js')).toIncludeMultiple(['src/a.js', 'src']) 31 | expect(t.exec('src/b.ts')).toBeNull() 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /test/unit/utils/graph.spec.ts: -------------------------------------------------------------------------------- 1 | import { DirectedGraph } from '../../../src/utils/graph' 2 | 3 | describe('DirectedGraph', function () { 4 | describe('.hasEdge()', function () { 5 | it('should return true if exists', function () { 6 | const g = new DirectedGraph() 7 | g.addEdge('a', 'b') 8 | expect(g.hasEdge('a', 'b')).toBeTruthy() 9 | }) 10 | 11 | it('should return false if from not exists', function () { 12 | const g = new DirectedGraph() 13 | g.addEdge('a', 'b') 14 | expect(g.hasEdge('b', 'c')).toBeFalsy() 15 | }) 16 | 17 | it('should return false if to not exists', function () { 18 | const g = new DirectedGraph() 19 | g.addEdge('a', 'b') 20 | expect(g.hasEdge('a', 'c')).toBeFalsy() 21 | }) 22 | }) 23 | 24 | describe('.checkCircular()', function () { 25 | it('should return false for empty graph', function () { 26 | const g = new DirectedGraph() 27 | expect(() => g.checkCircular('a')).not.toThrow() 28 | }) 29 | 30 | it('should return false if no circular', function () { 31 | const g = new DirectedGraph() 32 | g.addEdge('a', 'b') 33 | expect(() => g.checkCircular('a')).not.toThrow() 34 | }) 35 | 36 | it('should return true if it\'s circular', function () { 37 | const g = new DirectedGraph() 38 | g.addEdge('a', 'b') 39 | g.addEdge('b', 'c') 40 | g.addEdge('c', 'a') 41 | expect(() => g.checkCircular('a')).toThrow('a -> b -> c -> a') 42 | }) 43 | 44 | it('should return true if there\'s self-circle', function () { 45 | const g = new DirectedGraph() 46 | g.addEdge('a', 'a') 47 | expect(() => g.checkCircular('a')).toThrow('a -> a') 48 | }) 49 | }) 50 | 51 | describe('.findPathToRoot()', function () { 52 | it('should return single element for roots', function () { 53 | const g = new DirectedGraph() 54 | g.addEdge('a', 'b') 55 | expect(g.findPathToRoot('a')).toEqual(['a']) 56 | }) 57 | it('should return all ascendants', function () { 58 | const g = new DirectedGraph() 59 | g.addEdge('a', 'b') 60 | g.addEdge('b', 'c') 61 | expect(g.findPathToRoot('c')).toEqual(['c', 'b', 'a']) 62 | }) 63 | it('should return the first path for multiple parents', function () { 64 | const g = new DirectedGraph() 65 | g.addEdge('a', 'b') 66 | g.addEdge('b', 'c') 67 | g.addEdge('d', 'c') 68 | expect(g.findPathToRoot('c')).toEqual(['c', 'b', 'a']) 69 | }) 70 | it('should print circular if there is one', function () { 71 | const g = new DirectedGraph() 72 | g.addEdge('a', 'b') 73 | g.addEdge('b', 'c') 74 | g.addEdge('c', 'a') 75 | expect(g.findPathToRoot('a')).toEqual(['a', 'c', 'b', 'a']) 76 | }) 77 | }) 78 | 79 | describe('.toTree()', function () { 80 | it('should construct a tree for graph with a single edge', function () { 81 | const g = new DirectedGraph() 82 | g.addEdge('a', 'b') 83 | expect((g as any).toTree()).toEqual({ b: {} }) 84 | }) 85 | }) 86 | 87 | describe('.toString()', function () { 88 | it('should print a tree for graph with a single edge', function () { 89 | const g = new DirectedGraph() 90 | g.addEdge('a', 'b') 91 | expect(g.toString()).toEqual('a\n└─ b\n') 92 | }) 93 | }) 94 | }) 95 | -------------------------------------------------------------------------------- /test/unit/utils/number.spec.ts: -------------------------------------------------------------------------------- 1 | import { humanReadable, relation } from '../../../src/utils/number' 2 | 3 | describe('number', function () { 4 | describe('.humanReadable()', function () { 5 | it('should format 1000 to 1,000', function () { 6 | expect(humanReadable(1000)).toEqual('1,000') 7 | }) 8 | 9 | it('should format 1001000 to 1,001,000', function () { 10 | expect(humanReadable(1001000)).toEqual('1,001,000') 11 | }) 12 | 13 | it('should format 12345.67 to 12,345.67', function () { 14 | expect(humanReadable(12345.67)).toEqual('12,345.67') 15 | }) 16 | 17 | it('should format -1234 to -1,245', function () { 18 | expect(humanReadable(-1234)).toEqual('-1,234') 19 | }) 20 | }) 21 | 22 | describe('.relation()', function () { 23 | it('should return > if lhs is greater', function () { 24 | expect(relation(3, 2)).toEqual('>') 25 | }) 26 | 27 | it('should return < if rhs is greater', function () { 28 | expect(relation(3, 8)).toEqual('<') 29 | }) 30 | 31 | it('should return === when equal', function () { 32 | expect(relation(2, 2)).toEqual('=') 33 | }) 34 | 35 | it('should return ? when not comparable', function () { 36 | expect(relation(0, NaN)).toEqual('?') 37 | }) 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "declaration": true, 5 | "module": "CommonJS", 6 | "outDir": "dist", 7 | "esModuleInterop": true, 8 | "moduleResolution": "Node", 9 | "resolveJsonModule": true, 10 | "strict": true, 11 | "suppressImplicitAnyIndexErrors": true 12 | }, 13 | "include": [ "src"] 14 | } 15 | --------------------------------------------------------------------------------