├── .circleci └── config.yml ├── .editorconfig ├── .gitignore ├── .tern-project ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── bin └── mango ├── docker ├── Dockerfile ├── Makefile └── hooks │ └── build ├── docs └── config.md ├── index.js ├── lib ├── helpers │ ├── checkerror.js │ ├── config.js │ ├── config │ │ ├── js.js │ │ ├── json.js │ │ └── yaml.js │ ├── error_handler.js │ ├── icons │ │ ├── failure.png │ │ ├── success.png │ │ └── warning.png │ ├── resolveData.js │ ├── runcmd.js │ ├── subtractPaths.js │ └── unique.js ├── mango.js └── tasks │ ├── buildstamp.js │ ├── images.js │ ├── scripts.js │ ├── sprites.js │ ├── static.js │ ├── styles.js │ ├── templates.js │ ├── watch_reload.js │ └── watch_sources.js ├── package-lock.json ├── package.json └── test └── test-lib.js /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | working_directory: ~/manGoweb/mango-cli 5 | docker: 6 | - image: circleci/node:14 7 | steps: 8 | - checkout 9 | - run: 10 | name: update-npm 11 | command: 'sudo npm install -g npm@7' 12 | - restore_cache: 13 | key: dependency-cache-{{ checksum "package.json" }} 14 | - run: 15 | name: install-npm 16 | command: npm install 17 | - save_cache: 18 | key: dependency-cache-{{ checksum "package.json" }} 19 | paths: 20 | - ./node_modules 21 | - run: 22 | name: test 23 | command: npm test 24 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = tab 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.{json,yml,yaml}] 13 | indent_style = space 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | dist 30 | .cache-loader 31 | 32 | -------------------------------------------------------------------------------- /.tern-project: -------------------------------------------------------------------------------- 1 | { 2 | "libs": [], 3 | "loadEagerly": [ 4 | "lib/**/*.js" 5 | ], 6 | "plugins": { 7 | "complete_strings": {}, 8 | "lint": {}, 9 | "doc_comment": { 10 | "fullDocs": true 11 | }, 12 | "node": {}, 13 | "es_modules": {} 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at info@mangoweb.cz. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 manGoweb s.r.o. (www.mangoweb.cz) 4 | Copyright (c) 2014 Matej Simek (www.matejsimek.com) 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | mango-cli [![CircleCI](https://circleci.com/gh/manGoweb/mango-cli/tree/master.svg?style=svg)](https://circleci.com/gh/manGoweb/mango-cli/tree/master) [![NPM downloads](https://img.shields.io/npm/dm/mango-cli.svg)](https://www.npmjs.com/package/mango-cli) [![Docker Pulls](https://img.shields.io/docker/pulls/mangoweb/mango-cli.svg)](https://hub.docker.com/r/mangoweb/mango-cli/) 4 | ========= 5 | 6 | Scaffold and build your projects way more faster than before. Preconfigured frontend devstack to the absolute perfection. Fully automated to save your precious time. Ready for any type of web project. 7 | 8 | **A little example project** is here: [manGoweb/mango-cli-example](https://github.com/mangoweb/mango-cli-example). 9 | 10 | 11 | - [Installation](#installation) 12 | - [Requirements](#requirements) 13 | - [Alternative methods](#alternative-methods) 14 | - [Usage](#usage) 15 | - [Project scaffolding and initialization](#project-scaffolding-and-initialization) 16 | - [Managing project dependencies](#managing-project-dependencies) 17 | - [Project build](#project-build) 18 | - [Configuration](docs/config.md) 19 | - [FAQ](https://github.com/manGoweb/mango-cli/wiki/FAQ) 20 | 21 | ## Under the hood 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 |
Styles
Stylusexpressive, robust, feature-rich CSS preprocessor
SassCSS with superpowers
Lessthe dynamic stylesheet language
Autoprefixer vendor prefixes based on the real usage
Clean-CSSFast and efficient CSS minifier
Templates
Pug (Jade)robust, elegant and feature rich template engine
Scripts
WebpackStatic module bundler for modern JavaScript applications
TypeScriptTyped superset of JavaScript
BabelUse next generation JavaScript today
ReactJavaScript library for building user interfaces from Facebook
SvelteCybernetically enhanced web apps
UglifyJSJavaScript minifier
Tools
BrowserSyncTime-saving synchronised browser testing
NPMNode.js package manager
GulpAutomated build tasks
ImageminSeamless image minification
Sourcemapsdebug like a pro
49 | 50 | ## Installation 51 | 52 | Install mango-cli once from `npm` and use it everywhere: 53 | 54 | ```sh 55 | npm install -g mango-cli 56 | ``` 57 | 58 | ### Requirements 59 | 60 | Before installation check that your system has these requirements: 61 | 62 | - [Node.js LTS (10.x)](https://nodejs.org/en/download/) 63 | - [Git](http://git-scm.com) executable in `PATH` 64 | 65 | #### Mac OS X 66 | 67 | * `python` (`v2.7` recommended, `v3.x.x` is __*not*__ supported) (already installed on Mac) 68 | * [Xcode](https://developer.apple.com/xcode/download/) 69 | * You also need to install the `Command Line Tools` via Xcode. You can find this under the menu `Xcode -> Preferences -> Downloads` 70 | * [libvips](https://jcupitt.github.io/libvips/) via Homebrew `brew install vips` 71 | 72 | #### Windows 73 | 74 | * [windows-build-tools](https://github.com/felixrieseberg/windows-build-tools) via `npm install -g --production windows-build-tools` (from an elevated PowerShell) 75 | * will install and configure *Python v2.7* and *Visual C++ Build Tools 2015* for you 76 | 77 | #### Linux 78 | 79 | * `python` (`v2.7` recommended, `v3.x.x` is __*not*__ supported) 80 | * `make` 81 | * A proper C/C++ compiler toolchain, like [GCC](https://gcc.gnu.org) 82 | 83 | 84 | ### Alternative methods 85 | 86 | #### Docker 87 | 88 | We also provide a Docker image `mangoweb/mango-cli` which is available on the [Docker HUB](https://hub.docker.com/r/mangoweb/mango-cli/) 89 | 90 | #### Pre-packed archives 91 | 92 | If you're still having problems with the installation, check out prepared [release packages](https://github.com/manGoweb/mango-cli/releases). 93 | 94 | Extract them locally and run `npm link` in the `mango-cli` folder (on Mac OS X you still need the `libvips` dependency though). 95 | 96 | 97 | ## Usage 98 | 99 | * `mango init` - scaffolding and initialization 100 | * `mango install` - dependency installation 101 | * `mango build` - production build 102 | * `mango dev` - development mode 103 | 104 | Feel free to use `mango [command] -h` for detailed instructions 105 | 106 | 107 | ### Project scaffolding and initialization 108 | 109 | ```sh 110 | mango init [options] [directory] 111 | ``` 112 | 113 | Forks a template into folder. 114 | 115 | Options: 116 | * `-s, --source [git_repository]` - git repository with a template to fork. Default is currently the [mango-cli-example](https://github.com/manGoweb/mango-cli-example) 117 | 118 | 119 | ### Managing project dependencies 120 | 121 | ```sh 122 | mango install [packages...] 123 | ``` 124 | 125 | Installs packages from NPM and stores them in `node_modules` folder, from where you can `require` them (thanks to browserify). 126 | Maintain current list in the `mango.yaml` config file under the `dependencies` section. 127 | 128 | 129 | ### Project build 130 | 131 | Assuming the config file `mango.yaml` is present in a current directory and contains: 132 | 133 | ```yaml 134 | styles: 135 | - styles/screen.styl 136 | scripts: 137 | - scripts/index.js 138 | images: 139 | - images/**/*.{jpg,png,svg} 140 | templates: 141 | - templates/**/*.pug 142 | static: 143 | - fonts/** 144 | dependencies: 145 | - jquery 146 | watch: 147 | - app/** 148 | dist_folder: dist 149 | ``` 150 | 151 | Config file can be in JSON or JS formats. `mango.json` gets parsed as a JSON file, `mango.config.js` gets required as-is. 152 | 153 | 154 | #### Production build 155 | 156 | ```sh 157 | mango build [tasks...] 158 | ``` 159 | 160 | All assets are compiled and minified into `dist_folder`, ready for production use. 161 | 162 | Options: 163 | * `[tasks...]` - run only specified tasks as `styles`, `scripts`, `images`, `templates`, `static` 164 | 165 | 166 | #### Development mode 167 | 168 | ```sh 169 | mango dev [http_proxy] 170 | ``` 171 | 172 | Starts BrowserSync server (or proxy server) and fs watch for assets change. 173 | 174 | 175 | ## Configuration 176 | 177 | More in [Configuration options](docs/config.md) docs... 178 | 179 | 180 | ## FAQ 181 | 182 | More in [the Wiki page...](https://github.com/manGoweb/mango-cli/wiki/FAQ) 183 | 184 | 185 | ## Copyright 186 | 187 | Copyright 2016-2018 [manGoweb s.r.o.](https://www.mangoweb.cz) Code released under [the MIT license](LICENSE). Evolved from [Frontbase](http://frontbase.org) devstack. 188 | -------------------------------------------------------------------------------- /bin/mango: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const argv = require('yargs') 3 | const checkError = require('../lib/helpers/checkerror') 4 | const Config = require('../lib/helpers/config') 5 | const log = require('better-console') 6 | const Mango = require('../lib/mango') 7 | const path = require('path') 8 | const pkg = require('../package.json') 9 | const runcmd = require('../lib/helpers/runcmd') 10 | const unique = require('../lib/helpers/unique') 11 | 12 | argv 13 | .command('init [directory]', 'initialize a new project', { 14 | source: { 15 | alias: 's', 16 | describe: 'Template repository to fork. Defaults to ' + pkg.config.default_fork_repo, 17 | default: pkg.config.default_fork_repo, 18 | } 19 | }, async function init(argv) { 20 | const dir = path.resolve(process.cwd(), argv.directory || '.') 21 | const mango = new Mango(dir) 22 | const source = argv.source || pkg.config.default_fork_repo 23 | log.time('~ initialization time') 24 | 25 | mango.init(source, async function(err) { 26 | log.timeEnd('~ initialization time') 27 | checkError(err) 28 | 29 | const config = await (new Config(dir)).get() 30 | if(config.hooks && config.hooks.init) { 31 | log.info('~ init hook: ' + config.hooks.init) 32 | runcmd(config.hooks.init, dir, function() { 33 | log.info('/>') 34 | }) 35 | } else { 36 | log.info('/>') 37 | } 38 | }) 39 | }) 40 | 41 | 42 | .command(['install [packages..]', 'i'], 'NPM install of passed packages or dependencies specified in config', { 43 | }, async function install(argv) { 44 | const packages = argv.packages 45 | const dir = process.cwd() 46 | const config = new Config() 47 | const mainconfig = await config.get(true) 48 | const fullconfig = await config.get() 49 | 50 | log.warn('DEPRECATED: Use package.json and npm install instead') 51 | 52 | const onFinish = function() { 53 | log.timeEnd('~ installation time') 54 | if(fullconfig.hooks && fullconfig.hooks.install) { 55 | log.info('~ install hook: ' + fullconfig.hooks.install) 56 | runcmd(fullconfig.hooks.install, null, function() { 57 | log.info('/>') 58 | }) 59 | } else { 60 | log.info('/>') 61 | } 62 | } 63 | 64 | const doInstall = function() { 65 | log.time('~ installation time') 66 | // Install only passed packages and update mango.json 67 | if(packages.length > 0) { 68 | var _config = { dependencies: packages } 69 | var mango = new Mango(dir, _config) 70 | 71 | mango.install(function(err) { 72 | checkError(err) 73 | if(!mainconfig.dependencies) mainconfig.dependencies = [] 74 | mainconfig.dependencies = unique(mainconfig.dependencies.concat(packages)) 75 | config.save(mainconfig) 76 | onFinish() 77 | }) 78 | } 79 | // Install packages specifies in mango.json 80 | else { 81 | const mango = new Mango(dir, fullconfig) 82 | mango.install(function(err) { 83 | checkError(err) 84 | onFinish() 85 | }) 86 | } 87 | } 88 | 89 | 90 | // Run preinstall hook first 91 | if(fullconfig.hooks && fullconfig.hooks.preinstall) { 92 | log.info('~ preinstall hook: ' + fullconfig.hooks.preinstall) 93 | runcmd(fullconfig.hooks.preinstall, null, doInstall) 94 | } 95 | // Run immediately 96 | else { 97 | doInstall() 98 | } 99 | 100 | }) 101 | 102 | 103 | .command(['build [tasks...]', 'b'], 'build project assets for production', { 104 | customConfig: { 105 | alias: 'c', 106 | describe: 'Read the config from custom file', 107 | }, 108 | reuseBuildstamp: { 109 | alias: 'rb', 110 | describe: 'Reuse buildstamp from previous build', 111 | boolean: true, 112 | }, 113 | blacklistTasks: { 114 | alias: 'bl', 115 | describe: 'Tasks not to run', 116 | default: '', 117 | }, 118 | }, async function build(argv) { 119 | const tasks = argv.tasks 120 | const blacklistTasks = argv.blacklistTasks.split(',') 121 | const config = await (new Config(null, argv.customConfig)).get(false, argv.reuseBuildstamp) 122 | const mango = new Mango(process.cwd(), config) 123 | 124 | log.log('mango-cli v' + pkg.version) 125 | 126 | var doBuild = function() { 127 | log.time('~ compilation time') 128 | mango.build(tasks, blacklistTasks, function(err) { 129 | log.timeEnd('~ compilation time') 130 | checkError(err) 131 | if(config.hooks && config.hooks.build) { 132 | log.info('~ build hook: ' + config.hooks.build) 133 | runcmd(config.hooks.build, null, function() { 134 | log.info('/>') 135 | }) 136 | } else { 137 | log.info('/>') 138 | } 139 | }) 140 | } 141 | 142 | // Run prebuild hook first 143 | if(config.hooks && config.hooks.prebuild) { 144 | log.info('~ prebuild hook: ' + config.hooks.prebuild) 145 | runcmd(config.hooks.prebuild, null, doBuild) 146 | } 147 | // Run immediately 148 | else { 149 | doBuild() 150 | } 151 | 152 | }) 153 | 154 | 155 | .command(['dev', 'd'], 'start a development mode and watch for assets change', { 156 | proxy: { 157 | alias: 'p', 158 | describe: 'Proxy to a server instead of starting built-in', 159 | } 160 | }, async function dev(argv) { 161 | const config = await (new Config()).get() 162 | const mango = new Mango(process.cwd(), config) 163 | 164 | log.log('mango-cli v' + pkg.version) 165 | 166 | const doDev = function() { 167 | mango.dev(argv.proxy, function(err) { 168 | checkError(err) 169 | if(config.hooks && config.hooks.dev) { 170 | log.info('~ dev hook: ' + config.hooks.dev) 171 | runcmd(config.hooks.dev, null, function() { 172 | log.info('/>') 173 | }) 174 | } else { 175 | log.info('/>') 176 | } 177 | }) 178 | } 179 | 180 | // Run predev hook first 181 | if(config.hooks && config.hooks.predev) { 182 | log.info('~ predev hook: ' + config.hooks.predev) 183 | runcmd(config.hooks.predev, null, doDev) 184 | } 185 | // Run immediately 186 | else { 187 | doDev() 188 | } 189 | 190 | }) 191 | 192 | .demandCommand(1) 193 | .help() 194 | .argv 195 | 196 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14 2 | 3 | LABEL maintainer="mangoweb" 4 | 5 | ARG VERSION=master 6 | RUN npm install -g npm@7 && npm install -g https://github.com/manGoweb/mango-cli.git#$VERSION 7 | 8 | # Optional development test step 9 | # RUN (cd /usr/local/lib/node_modules/mango-cli && npm install-test) 10 | 11 | ENTRYPOINT ["mango"] 12 | CMD ["build"] 13 | -------------------------------------------------------------------------------- /docker/Makefile: -------------------------------------------------------------------------------- 1 | ORG = mangoweb 2 | REPO = mango-cli 3 | TAG = latest 4 | 5 | all: build publish 6 | 7 | build: Dockerfile 8 | docker build --build-arg VERSION=$(TAG) -t $(REPO):$(TAG) . 9 | 10 | publish: Dockerfile 11 | docker tag $(REPO):$(TAG) $(ORG)/$(REPO):$(TAG) 12 | docker push $(ORG)/$(REPO):$(TAG) 13 | -------------------------------------------------------------------------------- /docker/hooks/build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # https://docs.docker.com/docker-hub/builds/advanced/#build-hook-examples 4 | echo "------ HOOK START - BUILD -------" 5 | echo "Building: $SOURCE_BRANCH" 6 | docker build --build-arg VERSION="$SOURCE_BRANCH" -f "$DOCKERFILE_PATH" -t "$IMAGE_NAME" / 7 | docker images 8 | echo "------ HOOK END - BUILD -------" 9 | -------------------------------------------------------------------------------- /docs/config.md: -------------------------------------------------------------------------------- 1 | # Configuration options 2 | 3 | This document describes all available configuration options of mango.yaml, mango.json or mango.config.js file. 4 | 5 | ## Source and destination 6 | 7 | * `src_folder` - a folder with all source files. Content of this folder is watched in dev mode for changes. This path is filtered from destination path. 8 | * **`dist_folder`** - build destination folder. The only one required option. 9 | * `dist_persistent_folder` - a folder to build all assets except html into, defaults to value of `dist_folder` 10 | 11 | ## Tasks 12 | 13 | All fields are array of filepath masks, relative from the mango config file file. 14 | 15 | * `styles` - stylesheets. Can be CSS, LESS, SASS, Stylus 16 | * `scripts` - javascript. Files are treated as modules which don't leak to global namespace. String `DEBUG` is replaced with true/false based on dev/build task. 17 | * `images` - image resources. Images are minified in dist build, but just copied in dev mode. 18 | * `static` - static resources. Static files are copied to dist folder. 19 | * `templates` - templates. Static HTML files or Jade|Pug templates. 20 | * `sprites` - svg symbols. Creating SVG sprites from multiple SVG files. 21 | * `buildstamp` - cache control. Renaming specified files in dist folder with build unique prefix. 22 | 23 | ## Dependecies 24 | 25 | * `dependencies` - an array of [NPM packages](https://www.npmjs.com). They are installed into `node_modules` folder with `mango install` command. Then they are available in scripts as `require('module')` calls. 26 | * `version` - semver string with required mango-cli version. Like `>=0.18` 27 | 28 | ## Data 29 | 30 | * `data` - object containing data supplied to templates in build time. 31 | 32 | Format: `"globalname": "any variable type OR filepath to JSON / YAML file"` 33 | 34 | If `globalname` match filename, its value gets assigned as current scope, overriding (but not clears) previous state 35 | 36 | ## Hooks 37 | 38 | * `hooks` - object with additional commands you need to run before/after certain actions. Prefix `pre` means before a task, no prefix means after a task finished. 39 | 40 | Format: `"hookname": "command line command"`
41 | Available hooks: `init`, `preinstall`, `install`, `prebuild`, `build`, `predev`, `dev`, `watch` 42 | 43 | 44 | ## Experimental options 45 | 46 | ### Local config 47 | 48 | All options can be overridden in `mango.local.yaml` (or `mango.local.json`) file. Handy for development and deployment. 49 | 50 | --- 51 | 52 | ### BrowserSync 53 | 54 | * `browsersync` - options passed to [BrowserSync](http://www.browsersync.io/docs/options/) dev server 55 | * `proxy` - start dev server in [proxy mode](http://www.browsersync.io/docs/options/#option-proxy) 56 | * `watch` - additional files to watch resulting in browser reload 57 | * `chokidar` - [extra options](https://github.com/paulmillr/chokidar#performance) passed to [chokidar](https://github.com/paulmillr/chokidar) file watcher. [Defaults...](https://github.com/manGoweb/mango-cli/blob/master/lib/mango.js#L221) 58 | 59 | --- 60 | 61 | ### Styles 62 | 63 | * `stylus` - options passed to Stylus compiler. Default sets `'include css': true` 64 | * `autoprefixer` - options passed to CSS [Autoprefixer](https://github.com/postcss/autoprefixer#options) 65 | * `cssmin` - options passed to [clean-css](https://github.com/jakubpawlowicz/clean-css#how-to-use-clean-css-api) in build task. 66 | * `disableSourcemaps` - skips source maps in dev mode when `false` 67 | 68 | --- 69 | 70 | ### Templates 71 | 72 | * `pug` - options passed to the [Pug compiler](https://pugjs.org/api/reference.html). Defaults are `pretty: true`, `cache: true`, `doctype: 'html'` 73 | * `templates` - it's elements can be string (glob) or object for generating multiple files from single template. The object needs to have two properties: `template` (containing filename of the template) and `data` (file containing json object where keys will become filenames for generated html and values will be passed to template in `fileData` variable) 74 | * Additional data passed to templates: `devmode` == mango dev, and `production` == mango build 75 | * You can also use `require()` inside a template 76 | 77 | --- 78 | 79 | ### Images 80 | 81 | * `images` - array of all images - element can be: 82 | * string (glob) of source image for minification or 83 | * object for resize 84 | * `src` - string (glob) source of images 85 | * `sizes` - array of widths (int) 86 | * `aspectRatio` - aspect ratio of image on output (float = width/height), if undefined or false aspect ratio of image is used 87 | * `options` - [output options](http://sharp.dimens.io/en/stable/api-output/#jpeg) for [sharp](http://sharp.dimens.io/en/stable/) resizing engine 88 | 89 | --- 90 | 91 | ### Scripts 92 | 93 | * `webpack` - extra options passed to [webpack configuration object](https://webpack.js.org/configuration/) 94 | 95 | --- 96 | 97 | ### Sprites 98 | 99 | * `sprites` - an array of objects. Each object contains: 100 | * `path` - to SVG files (e.g. `src/images/sources/foo/*.svg`) 101 | * `name` - (optional) a prefix to SVG ids in generated sprites and name of the file 102 | * `filename` - (optional) name of file to which will be sprites generated 103 | 104 | --- 105 | 106 | ### Build 107 | 108 | * `cleanup` - prevents dist_dir cleanup when `false` 109 | * `buildstamp` - an array of file paths. A copy of selected files is made with prefix unique to each build in filename. The prefix is available by `#{buildstamp}` in jade and stored in file `dist_folder/.buildstamp.txt` for other template engines. In development the prefix is empty. 110 | 111 | --- 112 | 113 | ### Extensions mapping 114 | 115 | File extensions are mapped to a certain task by default. This setting can be overridden in the mango config file. 116 | For example: 117 | 118 | ``` 119 | "mapping":{ 120 | "scripts": ["js", "jsx", "es6", "es", "coffee", "my.extension", "tpl.html"] 121 | } 122 | ``` 123 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/mango') -------------------------------------------------------------------------------- /lib/helpers/checkerror.js: -------------------------------------------------------------------------------- 1 | module.exports = function(error) { 2 | if(error) { 3 | console.error(error.toString()) 4 | process.exit(1) 5 | } 6 | } -------------------------------------------------------------------------------- /lib/helpers/config.js: -------------------------------------------------------------------------------- 1 | var log = require('better-console') 2 | var path = require('path') 3 | var fs = require('fs') 4 | 5 | var Yaml = require('./config/yaml') 6 | var Json = require('./config/json') 7 | var Js = require('./config/js') 8 | 9 | var CONFIG_FILENAME = 'mango' 10 | var CONFIG_FILENAME_LOCAL = 'mango.local' 11 | 12 | 13 | /** 14 | * Config helper 15 | * 16 | * @param {string} path to a folder with the config file 17 | */ 18 | var Config = module.exports = function(folder, custompath) { 19 | this.dir = folder || process.cwd() 20 | this.custompath = custompath 21 | this.defaults = { 22 | "src_folder": ".", 23 | "dist_folder": "dist", 24 | "dist_persistent_folder": null, 25 | "styles": null, 26 | "scripts": null, 27 | "images": null, 28 | "static": null, 29 | "templates": null, 30 | "sprites": null, 31 | "buildstamp": null, 32 | "dependencies": null, 33 | "watch": null 34 | } 35 | this.parsers = [Yaml, Json, Js] 36 | } 37 | 38 | /** 39 | * Get parser and resolved path 40 | * @param {boolean} local Take local config or general one 41 | * @return {object|null} 42 | */ 43 | Config.prototype._getParser = function (local) { 44 | var baseFilename = local ? CONFIG_FILENAME_LOCAL : CONFIG_FILENAME 45 | var returning = null 46 | this.parsers.forEach(function(parser) { 47 | parser.getExtensions().forEach(function (extension) { 48 | var resolvedPath = path.resolve(this.dir, baseFilename + '.' + extension) 49 | if(!returning && fs.existsSync(resolvedPath)) { 50 | returning = { 51 | parser: parser, 52 | path: resolvedPath 53 | } 54 | } 55 | }.bind(this)) 56 | }.bind(this)) 57 | if(returning) return returning 58 | } 59 | 60 | Config.prototype._getParsedConfig = async function (local) { 61 | var parser = this._getParser(local) 62 | return parser ? await parser.parser.get(parser.path) : null 63 | } 64 | 65 | /** 66 | * Get config object 67 | * @param {boolean} filter local config 68 | * @return {Object} config object 69 | */ 70 | Config.prototype.get = async function(filterLocal, reuseBuildstamp) { 71 | var k 72 | var config = {} 73 | var config_main 74 | var config_local 75 | 76 | try { 77 | if(!this.custompath) { 78 | config_main = await this._getParsedConfig(false) 79 | config_local = await this._getParsedConfig(true) 80 | } else { 81 | // If passed directly, don't look anywhere else 82 | config_main = await this._getCustom(this.custompath) 83 | } 84 | 85 | } catch (e) { 86 | log.error('Config file in invalid format: ', e.message) 87 | } 88 | 89 | if(!config_main){ 90 | log.error('Cannot load the config file mango.json or mango.yaml') 91 | process.exit(1) 92 | } 93 | 94 | if(this.defaults) { 95 | for(k in this.defaults) { 96 | config[k] = this.defaults[k] 97 | } 98 | } 99 | 100 | if(config_main) { 101 | for(k in config_main) { 102 | config[k] = config_main[k] 103 | } 104 | } 105 | 106 | if(config_local && (filterLocal === undefined || filterLocal !== true)) { 107 | for(k in config_local) { 108 | config[k] = config_local[k] 109 | } 110 | } 111 | 112 | if (config.src_folder === '.') { 113 | config.src_folder = this.dir 114 | } 115 | 116 | config.dist_folder = path.resolve(this.dir + '/' + config.dist_folder) 117 | if (!config.dist_persistent_folder) { 118 | config.dist_persistent_folder = config.dist_folder 119 | } else { 120 | config.dist_persistent_folder = path.resolve(this.dir + '/' + config.dist_persistent_folder) 121 | } 122 | 123 | if(reuseBuildstamp) { 124 | try { 125 | config.stamp = fs.readFileSync(config.dist_folder + '/.buildstamp.txt').toString().trim() 126 | } catch (e) { 127 | log.error('Failed to load .buildstamp.txt file from ' + config.dist_folder) 128 | process.exit(1) 129 | } 130 | } else { 131 | config.stamp = (Date.now() / 1000 | 0) + '-' 132 | } 133 | 134 | return config 135 | } 136 | 137 | Config.prototype._getCustom = async function(custompath) { 138 | var resolvedPath = path.resolve(this.dir, custompath) 139 | var parsedPath = path.parse(resolvedPath) 140 | var parser = null 141 | 142 | this.parsers.forEach((_parser) => { 143 | if(~_parser.getExtensions().indexOf(parsedPath.ext.substr(1))) { 144 | parser = _parser 145 | return false 146 | } 147 | }) 148 | 149 | if(!parser) throw new Error('Cannot read custom config file') 150 | 151 | return await parser.get(resolvedPath) 152 | 153 | } 154 | 155 | /** 156 | * Save config state to file 157 | * @param {Object} config state to save 158 | */ 159 | Config.prototype.save = function(config) { 160 | // Filter out null values 161 | for(var prop in config){ 162 | if(config.hasOwnProperty(prop) && !config[prop]){ 163 | delete config[prop]; 164 | } 165 | } 166 | 167 | // Find correct extension & parser 168 | var parser = this._getParser(false) 169 | if(parser) { 170 | parser.parser.save(parser.path, config) 171 | } else { 172 | log.error('No config found to write to.') 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /lib/helpers/config/js.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash') 2 | 3 | var Js = {} 4 | 5 | Js.getExtensions = function () { 6 | return ['config.js'] 7 | } 8 | 9 | Js.get = async function (path) { 10 | const config = require(path) 11 | 12 | if(_.isFunction(config)) { 13 | return await config() 14 | } 15 | 16 | return config 17 | } 18 | 19 | Js.save = function (path, config) { 20 | throw new Error('Saving the JS config is not supported.') 21 | } 22 | 23 | module.exports = Js 24 | -------------------------------------------------------------------------------- /lib/helpers/config/json.js: -------------------------------------------------------------------------------- 1 | var jf = require('jsonfile') 2 | 3 | var Json = {} 4 | 5 | Json.getExtensions = function () { 6 | return ['json'] 7 | } 8 | 9 | Json.get = function (path) { 10 | return jf.readFileSync(path, { throws: true }) 11 | } 12 | 13 | Json.save = function (path, config) { 14 | jf.spaces = 2 // set proper formatting 15 | jf.writeFile(path, config, function (err) { 16 | if(err) throw err 17 | }) 18 | } 19 | 20 | module.exports = Json 21 | -------------------------------------------------------------------------------- /lib/helpers/config/yaml.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var yaml = require('js-yaml') 3 | 4 | var Yaml = {} 5 | 6 | Yaml.getExtensions = function () { 7 | return ['yaml', 'yml'] 8 | } 9 | 10 | Yaml.get = function (path) { 11 | return yaml.load(fs.readFileSync(path, { throws: true })) 12 | } 13 | 14 | Yaml.save = function (path, config) { 15 | fs.writeFile(path, yaml.dump(config), function (err) { 16 | if(err) throw err 17 | }) 18 | } 19 | 20 | module.exports = Yaml 21 | -------------------------------------------------------------------------------- /lib/helpers/error_handler.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk') 2 | var notifier = require('node-notifier') 3 | var path = require('path') 4 | var pkg = require('../../package.json') 5 | 6 | var notify = function(title, message, type) { 7 | notifier.notify({ 8 | appName: pkg.config.appId, 9 | icon: path.join(__dirname, 'icons', type+'.png'), 10 | message: message, 11 | sound: type==='failure', 12 | title: title || 'mango-cli', 13 | }) 14 | } 15 | 16 | var printError = function(err) { 17 | console.log('\n') 18 | 19 | let description = err.messageFormatted || err.message || err.status || err 20 | if (err.name) { 21 | description = `${err.name}: ${description}` 22 | } 23 | const message = err.plugin ? `(${err.plugin}) ${description}` : description 24 | 25 | console.log(chalk.bold.red(`[!] ${chalk.bold(message)}`)) 26 | 27 | if (err.url) { 28 | console.log(chalk.cyan(err.url)) 29 | } 30 | 31 | // Path and location in the file 32 | if (err.loc) { 33 | console.log(`${path.relative(process.cwd(), err.loc.file || err.id )} (${err.loc.line}:${err.loc.column})`) 34 | } else if(err.line && err.columng && err.file) { 35 | console.log(`${path.relative(process.cwd(), err.file)} (${err.line}:${err.column})`) 36 | } else if (err.id) { 37 | console.log(path.relative(process.cwd(), err.id )) 38 | } 39 | 40 | // Code 41 | if(!err.messageFormatted && err.frame) { 42 | console.log(chalk.dim(err.frame)) 43 | } 44 | 45 | console.log('\n') 46 | } 47 | 48 | 49 | module.exports = function(){ 50 | var errorLast // Error object from last build 51 | var errorCurrent // Error object from current build 52 | 53 | return { 54 | fail: function(error) { 55 | printError(error) 56 | errorCurrent = error 57 | 58 | if (JSON.stringify(error) !== JSON.stringify(errorLast)) { 59 | notify(error.plugin, error.message, 'failure') // New error found 60 | } else { 61 | notify(error.plugin, error.message, 'warning') // Still not fixed 62 | } 63 | 64 | this.emit('end') 65 | }, 66 | done: function() { 67 | if (errorLast && !errorCurrent) { // Notify error fixed 68 | console.log(chalk.green('/> Error fixed')) 69 | notify(errorLast.plugin, 'Fixed', 'success') 70 | } 71 | 72 | errorLast = errorCurrent 73 | errorCurrent = false // Reset for next build 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /lib/helpers/icons/failure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manGoweb/mango-cli/58f91263a43aef3f3e0f1c5eaf18582b17ca4b04/lib/helpers/icons/failure.png -------------------------------------------------------------------------------- /lib/helpers/icons/success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manGoweb/mango-cli/58f91263a43aef3f3e0f1c5eaf18582b17ca4b04/lib/helpers/icons/success.png -------------------------------------------------------------------------------- /lib/helpers/icons/warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manGoweb/mango-cli/58f91263a43aef3f3e0f1c5eaf18582b17ca4b04/lib/helpers/icons/warning.png -------------------------------------------------------------------------------- /lib/helpers/resolveData.js: -------------------------------------------------------------------------------- 1 | var c = require('better-console') 2 | var fs = require('fs') 3 | var jf = require('jsonfile') 4 | var path = require('path') 5 | var yaml = require('js-yaml') 6 | 7 | var REjson = /\.json$/i 8 | var REyaml = /\.ya?ml$/i 9 | 10 | /** 11 | * Reads data from external JSON files 12 | * 13 | * @param {[type]} value [description] 14 | * @return {object} data 15 | */ 16 | module.exports = function resolveData(value, config) { 17 | var resolve = null 18 | 19 | if(typeof value === 'string') { 20 | // Test for json extension 21 | if(REjson.test(value)) { 22 | resolve = jf.readFileSync(path.resolve(config.dir, value), { throws: false }) 23 | if(resolve === null) { 24 | c.error('Error parsing JSON file ' + value) 25 | } 26 | // Test for yaml extension 27 | } else if(REyaml.test(value)) { 28 | resolve = yaml.load(fs.readFileSync(path.resolve(config.dir, value), { throws: false })) 29 | if(resolve === null) { 30 | c.error('Error parsing YAML file ' + value) 31 | } 32 | // Simple string value 33 | } else { 34 | resolve = value 35 | } 36 | 37 | // Direct value 38 | } else { 39 | resolve = value 40 | } 41 | 42 | return resolve 43 | } 44 | -------------------------------------------------------------------------------- /lib/helpers/runcmd.js: -------------------------------------------------------------------------------- 1 | var checkError = require('./checkerror') 2 | 3 | /** 4 | * Runs custom commnad 5 | * 6 | * @param {string} cmd [description] 7 | * @param {string} dir [description] 8 | * @param {Function} callback [description] 9 | */ 10 | module.exports = function(cmd, dir, callback) { 11 | var exec = require('child_process').exec 12 | exec(cmd, { cwd: dir || process.cwd() }, function puts(error, stdout, stderr) { 13 | console.log(stdout) 14 | checkError(error) 15 | if(callback) callback() 16 | }) 17 | } -------------------------------------------------------------------------------- /lib/helpers/subtractPaths.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | 3 | /** 4 | * @param minuend 5 | * @param subtrahend 6 | * @returns {string} 7 | */ 8 | module.exports = function (minuend, subtrahend) { 9 | var difference = minuend.substring(subtrahend.length) 10 | 11 | if (difference.indexOf(path.sep) === 0) { 12 | difference = difference.substring(path.sep.length) 13 | } 14 | return difference 15 | } 16 | -------------------------------------------------------------------------------- /lib/helpers/unique.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Remove duplicates from an array 3 | * 4 | * @param {Array} array 5 | * @return {Array} new array with unique values 6 | */ 7 | module.exports = function(array) { 8 | return array.reduce(function(accum, current) { 9 | if (accum.indexOf(current) < 0) { 10 | accum.push(current) 11 | } 12 | return accum 13 | }, []) 14 | } -------------------------------------------------------------------------------- /lib/mango.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const c = require('better-console') 3 | const minimatch = require('minimatch') 4 | const path = require('path') 5 | const pkg = require('../package.json') 6 | const semver = require('semver') 7 | const util = require('util') 8 | const fs = require('fs') 9 | const gulp = require('gulp') 10 | 11 | var Mango = module.exports = function(folder, config) { 12 | this.config = config || {} 13 | this.config.dir = folder ? path.resolve(folder) : folder 14 | this.config.devmode = false 15 | this.defaultTasks = ['styles', 'scripts', 'templates', 'images', 'sprites', 'static'] 16 | this.checkVersion() 17 | } 18 | 19 | 20 | Mango.prototype.checkVersion = function() { 21 | if(this.config.version && !semver.satisfies(pkg.version.replace(/\-.*/i, ''), this.config.version)){ 22 | c.error('Installed version of the mango-cli (' + pkg.version + ') doesn\'t satisfy the version specified in mango config file (' + this.config.version + ')') 23 | process.exit(1) 24 | } 25 | } 26 | 27 | Mango.prototype.init = function(forkTemplate, callback) { 28 | c.info('Initializing a new project') 29 | if((/^[a-z0-9_-]+\/[a-z0-9_-]+$/i).test(forkTemplate)) { 30 | forkTemplate = "git@github.com:" + forkTemplate + ".git" 31 | } 32 | this._forkRepository(this.config.dir, forkTemplate, function(err) { 33 | if(callback instanceof Function) callback(err) 34 | }) 35 | } 36 | 37 | Mango.prototype._forkRepository = function(destination, remote, callback) { 38 | c.log('~ forking', remote, 'to', destination) 39 | 40 | var git = require('gift') 41 | 42 | git.clone(remote, destination, function(err, repo) { 43 | if(err) { 44 | c.error('Failed to clone', err) 45 | if(callback instanceof Function) callback(err) 46 | return 47 | } 48 | 49 | repo.remote_remove('origin', function(err) { 50 | if(err) c.error('Failed to remove origin', err) 51 | if(callback instanceof Function) callback(err, repo) 52 | }) 53 | }) 54 | } 55 | 56 | Mango.prototype._initRepo = function(folder, callback) { 57 | c.log('~ initializing empty git repo in', folder) 58 | 59 | var fs = require('fs') 60 | var git = require('gift') 61 | 62 | if(!fs.existsSync(folder)) { 63 | fs.mkdirSync(folder) 64 | } 65 | 66 | git.init(folder, function(err, repo) { 67 | if(err) { 68 | c.error('Failed git repo initialization', err) 69 | } 70 | c.info('Repository initialized') 71 | if(callback instanceof Function) callback(err, repo) 72 | }) 73 | } 74 | 75 | Mango.prototype.showProjectTitle = function() { 76 | var dir_path = require('fs').realpathSync('.') 77 | var dir_name = require('path').basename(dir_path) 78 | 79 | process.title = dir_name 80 | } 81 | 82 | Mango.prototype.install = function(callback) { 83 | c.info('Installing NPM packages') 84 | 85 | if(!this.config.dependencies || !this.config.dependencies.length){ 86 | c.log('~ no dependencies to install') 87 | if(callback instanceof Function) callback() 88 | return 89 | } 90 | 91 | var nodeDir = path.resolve(this.config.dir + '/node_modules') 92 | 93 | try { 94 | fs.accessSync(nodeDir, fs.constants.F_OK) // Check if the dir exists 95 | } catch (err) { 96 | fs.mkdirSync(nodeDir) // Creating the dir so that node doesn't bubble up where we don't want it to. 97 | // See https://docs.npmjs.com/files/folders#more-information 98 | } 99 | 100 | // Serialize dependencies into string 101 | var runcmd = require('./helpers/runcmd') 102 | var cmd = `npm install --no-optional --prefix "${this.config.dir}/" ${this.config.dependencies.join(' ')}` 103 | // var cmd = `npm install --no-optional ${this.config.dependencies.join(' ')}` 104 | 105 | // Run in command line 106 | runcmd(cmd, this.config.dir, function() { 107 | if(callback instanceof Function) callback() 108 | }) 109 | } 110 | 111 | Mango.prototype._cleanupDir = function(dir) { 112 | if(dir && this.config.cleanup !== false){ 113 | c.warn('~ cleaning ' + dir + ' folder') 114 | var del = require('del') 115 | var path = require('path') 116 | var absDir = path.resolve(this.config.dir, dir) 117 | try { 118 | return del.sync(absDir, { force: true }) 119 | } catch(e) { 120 | c.error(e) 121 | } 122 | } 123 | return false 124 | } 125 | 126 | Mango.prototype._cleanup = function() { 127 | var status = this._cleanupDir(this.config.dist_folder) 128 | status &= this._cleanupDir(this.config.dist_persistent_folder) 129 | return status 130 | } 131 | 132 | Mango.prototype.build = function(tasks, blacklistTasks, callback) { 133 | c.info('Building project assets for production') 134 | 135 | this._registerGulpTasks(gulp, tasks, blacklistTasks) 136 | 137 | if(!tasks || !tasks.length){ 138 | this._cleanup() 139 | } 140 | 141 | gulp.task('compile')(callback) 142 | } 143 | 144 | Mango.prototype.dev = function(http_proxy, callback) { 145 | c.info('Starting development mode') 146 | 147 | this.showProjectTitle() 148 | 149 | gulp.on('task_stop', function (e) { 150 | var duration = Math.round(e.duration*10000)/10 151 | var duration_str = duration + ' ms' 152 | c.debug('~ ' + e.task + ' finished after', duration_str) 153 | }) 154 | 155 | this.config.devmode = true 156 | 157 | if(typeof http_proxy !== 'undefined') { 158 | this.config.proxy = http_proxy 159 | } 160 | 161 | gulp.task('watch:scripts', require('./tasks/scripts')(gulp, this.config)) 162 | 163 | this._registerGulpTasks(gulp, [], [ 'scripts' ]) 164 | this._cleanup() 165 | 166 | gulp.series('compile', gulp.parallel('watch:scripts', 'watch:sources', 'watch:reload'))(callback) 167 | } 168 | 169 | Mango.prototype._task = function(name, gulp) { 170 | var fn = require('./tasks/' + name)(gulp, this.config) 171 | gulp.task(name, fn) 172 | return name 173 | } 174 | 175 | Mango.prototype._registerGulpTasks = function(gulp, whitelist, blacklist) { 176 | // Filter tasks not on whitelist or with missing config entry 177 | var activeTasks = this.defaultTasks 178 | .filter(function (task) { 179 | if(whitelist && whitelist.length) { 180 | return whitelist.indexOf(task) != -1 ? task : false 181 | } else { 182 | return task 183 | } 184 | }) 185 | .filter(function(task) { 186 | if(blacklist && blacklist.length) { 187 | return blacklist.indexOf(task) != -1 ? false : task 188 | } else { 189 | return task 190 | } 191 | }) 192 | .filter(function(task) { 193 | if(this.config[task]){ 194 | return this._task(task, gulp) 195 | } 196 | return false 197 | }, this) 198 | 199 | this._task('buildstamp', gulp) 200 | 201 | // Watch tasks 202 | var watcher = this._watch.bind(this) 203 | gulp.task('watch:reload', require('./tasks/watch_reload')(gulp, this.config, watcher)) 204 | gulp.task('watch:sources', require('./tasks/watch_sources')(gulp, this.config, watcher)) 205 | 206 | // Join build tasks together 207 | // If using whitelist and buildstamp not present 208 | if((whitelist && whitelist.length && whitelist.indexOf('buildstamp') == -1) || (blacklist && blacklist.length && blacklist.indexOf('buildstamp') != -1)) { 209 | gulp.task('compile', gulp.parallel(activeTasks)) 210 | } else { 211 | gulp.task('compile', gulp.series(gulp.parallel(activeTasks), 'buildstamp')) 212 | } 213 | } 214 | 215 | // Single watch instance with multiple subscribers 216 | Mango.prototype._watch = function(glob, callback) { 217 | var basedir = this.config.dir 218 | 219 | if(!this.watcher){ 220 | var chokidar = require('chokidar') 221 | this.watcher = chokidar.watch(basedir, _.assign({}, { 222 | ignored: /[\/\\]\./, 223 | ignoreInitial: true, 224 | persistent: true, 225 | ignorePermissionErrors: true, 226 | usePolling: false, 227 | }, this.config.chokidar)) 228 | } 229 | 230 | this.watcher.on('all', function(event, filepath) { 231 | var rel = path.relative(basedir, filepath) 232 | 233 | if(minimatch(rel, glob)){ 234 | callback(rel) 235 | } 236 | }) 237 | 238 | return this.watcher 239 | } 240 | -------------------------------------------------------------------------------- /lib/tasks/buildstamp.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function (gulp, config) { 3 | 4 | return function (done) { 5 | if (!config.buildstamp || config.devmode) return done() 6 | var c = require('better-console') 7 | c.info('~ buildstamp') 8 | 9 | var rename = require('gulp-rename') 10 | var path = require('path') 11 | var file = require('gulp-file') 12 | 13 | var subtractPaths = require('../helpers/subtractPaths') 14 | 15 | var distFolderRelative = subtractPaths(config.dist_folder, config.dir) 16 | var pathToDistRelative = subtractPaths(config.dir, process.cwd()) 17 | 18 | var task = gulp.src(config.buildstamp, { 19 | base: path.join(pathToDistRelative, distFolderRelative), 20 | cwd: config.dir, 21 | allowEmpty: true, 22 | }) 23 | 24 | task = task.pipe(rename(function (filepath) { 25 | filepath.basename = config.stamp + filepath.basename 26 | })) 27 | 28 | task = task.pipe(file('.buildstamp.txt', config.stamp + '\n')) 29 | 30 | return task.pipe(gulp.dest(config.dist_folder)) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/tasks/images.js: -------------------------------------------------------------------------------- 1 | module.exports = function(gulp, config) { 2 | var errorHandler = require('../helpers/error_handler')() 3 | 4 | return function(done) { 5 | if(!config.images) return done() 6 | var c = require('better-console') 7 | c.info('~ images') 8 | 9 | var imagemin = require('gulp-imagemin') 10 | var plumber = require('gulp-plumber') 11 | var merge = require('merge-stream') 12 | var sharp = require('sharp') 13 | var path = require('path') 14 | var stream = require('stream') 15 | 16 | var task = merge() 17 | 18 | // Divide config to simple input and with options 19 | var imageObjects = [] // resizing 20 | var glob = [] // nothing 21 | for (var i = 0; i < config.images.length; i++) { 22 | var image = config.images[i] 23 | if(typeof image == 'object' && typeof image.src == 'string') { 24 | imageObjects.push(image) 25 | } else if (typeof image == "string") { 26 | glob.push(image) 27 | } else { 28 | c.error('Images: Unrecognised type of input') 29 | } 30 | } 31 | 32 | // Pipe images, with no transform 33 | if (glob.length) { 34 | task.add(gulp.src(glob, { base: config.src_folder, cwd: config.dir, allowEmpty: true })) 35 | } 36 | 37 | // Pipe images, which has to be transformed 38 | for (var i = 0; i < imageObjects.length; i++) { 39 | (function () { 40 | var imageObject = imageObjects[i] 41 | task.add(gulp.src(imageObject.src, { base: config.src_folder, cwd: config.dir, allowEmpty: true }) 42 | .pipe(function () { 43 | var transformStream = new stream.Transform({objectMode: true}) 44 | transformStream._transform = function (file, enc, callback) { 45 | file.imageData = imageObject 46 | callback(null, file) 47 | } 48 | return transformStream 49 | }()) 50 | ) 51 | })() 52 | } 53 | 54 | 55 | // Init plumber in devmode 56 | if(config.devmode){ 57 | task = task.pipe(plumber({ errorHandler: errorHandler.fail })) 58 | } 59 | 60 | // Multiply and rename images for transform 61 | task = task.pipe(function () { 62 | var transformStream = new stream.Transform({objectMode: true}); 63 | transformStream._transform = function (file, encoding, callback) { 64 | if(file.imageData) { 65 | for (var i = 0; i < file.imageData.sizes.length; i++) { 66 | var size = file.imageData.sizes[i] 67 | var fileClone = file.clone() 68 | var name = path.parse(fileClone.path) 69 | fileClone.path = path.join(name.dir, name.name + '-' + size + name.ext) 70 | fileClone.imageTransform = { 71 | width: size, 72 | height: file.imageData.aspectRatio ? Math.ceil(size/file.imageData.aspectRatio) : null, 73 | options: file.imageData.options || {}, 74 | } 75 | this.push(fileClone) 76 | } 77 | callback() 78 | } else { 79 | callback(null, file) 80 | } 81 | } 82 | return transformStream 83 | }()) 84 | 85 | 86 | // Transform images 87 | task = task.pipe(function () { 88 | var transformStream = new stream.Transform({objectMode: true}); 89 | transformStream._transform = function (file, encoding, callback) { 90 | if(file.imageTransform) { 91 | sharp(file.contents).resize(file.imageTransform.width, file.imageTransform.height, { withoutEnlargement: true }).jpeg(file.imageTransform.options).toBuffer(function (err, buffer, info) { 92 | if(err) console.error(err) 93 | file.contents = buffer 94 | callback(null, file) 95 | }) 96 | } else { 97 | callback(null, file) 98 | } 99 | } 100 | return transformStream 101 | }()) 102 | 103 | if(!config.devmode) { 104 | task = task.pipe(imagemin([ 105 | imagemin.gifsicle(), 106 | imagemin.mozjpeg({ progressive: true }), 107 | imagemin.optipng(), 108 | imagemin.svgo({ plugins: [{ removeViewBox: false }] }) 109 | ])) 110 | } 111 | 112 | if(config.devmode){ 113 | task.on('end', function() { 114 | errorHandler.done() 115 | }) 116 | } 117 | 118 | return task.pipe(gulp.dest(config.dist_persistent_folder)) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /lib/tasks/scripts.js: -------------------------------------------------------------------------------- 1 | const jsExtensions = [ '.js', '.jsx', '.es6', '.es' ] 2 | const tsExtensions = [ '.ts', '.tsx' ] 3 | const svelteExtensions = [ '.svelte' ] 4 | const otherExtensions = [ '.json' ] 5 | const resolveExtensions = [].concat(otherExtensions, jsExtensions, tsExtensions, svelteExtensions) 6 | const webpack = require('webpack') 7 | 8 | module.exports = function(gulp, config) { 9 | const errorHandler = require('../helpers/error_handler')() 10 | 11 | return function(done) { 12 | if(!config.scripts) return done() 13 | const c = require('better-console') 14 | 15 | if(config.devmode) { 16 | c.info('Starting webpack in watch mode...') 17 | } else { 18 | c.info('~ scripts') 19 | } 20 | 21 | const assign = require('lodash').assign 22 | const cache = require('gulp-cached') 23 | const path = require('path') 24 | const plumber = require('gulp-plumber') 25 | const named = require('vinyl-named') 26 | 27 | const gulpWebpack = require('webpack-stream') 28 | 29 | const babelOptions = { 30 | presets: [ 31 | [ require.resolve('@babel/preset-env'), { modules: false } ], 32 | require.resolve('@babel/preset-react'), 33 | ], 34 | plugins: [ 35 | [ require.resolve('@babel/plugin-syntax-decorators'), { legacy: true } ], 36 | [ require.resolve('@babel/plugin-proposal-decorators'), { legacy: true } ], 37 | '@babel/plugin-syntax-class-properties', 38 | '@babel/plugin-proposal-class-properties', 39 | '@babel/plugin-syntax-object-rest-spread', 40 | '@babel/plugin-proposal-object-rest-spread', 41 | '@babel/plugin-syntax-dynamic-import', 42 | ].map(plugin => (typeof plugin == 'string') ? require.resolve(plugin) : plugin), 43 | cacheDirectory: true, 44 | } 45 | 46 | const webpackConfig = assign({}, { 47 | mode: config.devmode ? 'development' : 'production', 48 | devtool: config.devmode ? 'cheap-source-map' : false, 49 | watch: config.devmode, 50 | watchOptions: { 51 | ignored: [ 'node_modules' ], 52 | }, 53 | stats: { 54 | assets: false, 55 | builtAt: false, 56 | entrypoints: false, 57 | children: false, 58 | performance: false, 59 | publicPath: false, 60 | timings: false, 61 | version: true, 62 | }, 63 | resolve: { 64 | extensions: resolveExtensions, 65 | mainFields: ['svelte', 'browser', 'module', 'main'], 66 | alias: { 67 | svelte: path.resolve('node_modules', 'svelte'), 68 | }, 69 | }, 70 | module: { 71 | rules: [ 72 | { test: /\.ya?ml$/, use: require.resolve('yml-loader') }, 73 | { test: /\.pug$/, use: require.resolve('pug-loader') }, 74 | { test: /\.svelte$/, use: require.resolve('svelte-loader') }, 75 | { test: /\.html?$/, 76 | use: { 77 | loader: require.resolve('html-loader'), 78 | options: { 79 | attributes: false, 80 | minimize: true, 81 | } 82 | } 83 | }, { 84 | // ES6+ 85 | test: function(filepath) { 86 | const ext = path.extname(filepath) 87 | return jsExtensions.indexOf(ext) !== -1 88 | }, 89 | use: [ 90 | { loader: require.resolve('babel-loader'), options: babelOptions }, 91 | ] 92 | }, { 93 | // TypeScript 94 | test: function(filepath) { 95 | const ext = path.extname(filepath) 96 | return tsExtensions.indexOf(ext) !== -1 97 | }, 98 | use: [ 99 | { loader: require.resolve('babel-loader'), options: babelOptions }, 100 | { loader: require.resolve('ts-loader') }, 101 | ] 102 | } 103 | ] 104 | }, 105 | plugins: [ 106 | new webpack.DefinePlugin({ 107 | 'DEBUG': JSON.stringify(!!config.devmode), // inject DEBUG variable 108 | 'BUILDSTAMP': (config.buildstamp && !config.devmode) ? JSON.stringify(config.stamp) : JSON.stringify(''), // inject BUILDSTAMP variable 109 | }), 110 | ], 111 | }, config.webpack || {}) 112 | 113 | // --- 114 | 115 | let task = gulp.src(config.scripts, { 116 | base: config.src_folder, 117 | cwd: config.dir, 118 | allowEmpty: true, 119 | }) 120 | 121 | // Init plumber in devmode 122 | if(config.devmode){ 123 | task = task 124 | .pipe(plumber({ errorHandler: errorHandler.fail })) 125 | } 126 | 127 | task = task 128 | .pipe(named(function(file) { 129 | const ext = path.extname(file.path) 130 | const relative = path.relative(config.src_folder, file.path) 131 | // Give file a name under the correct folder, without an extension 132 | return relative.replace(new RegExp(`\\${ext}$`, 'i'), '') 133 | })) 134 | .pipe(gulpWebpack(webpackConfig, webpack)) 135 | .pipe(cache('scripts', { optimizeMemory: true })) 136 | 137 | if(config.devmode){ 138 | task.on('end', function() { 139 | errorHandler.done() 140 | }) 141 | } 142 | 143 | return task.pipe(gulp.dest(config.dist_persistent_folder)) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /lib/tasks/sprites.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function(gulp, config) { 3 | 4 | return function(done) { 5 | if(!config.sprites) return done() 6 | var c = require('better-console') 7 | c.info('~ sprites') 8 | 9 | var path = require('path') 10 | var plumber = require('gulp-plumber') 11 | var svgSprite = require('gulp-svg-sprite') 12 | var merge = require('merge-stream') 13 | 14 | var tasks = merge() 15 | 16 | for(var i = 0; i < config.sprites.length; i++) { 17 | (function() { 18 | var prefix = config.sprites[i].name 19 | var generator = (function(name) { 20 | var namePath = name.split(path.sep) 21 | var fileName = namePath[namePath.length - 1].split('.') 22 | fileName.pop() // Remove file extension 23 | var id = 'shape-' + (prefix ? prefix + '-' : '') + fileName.join('.') 24 | return id 25 | 26 | }).bind(prefix) 27 | var filename = config.sprites[i].filename || 'shapes' + (config.sprites[i].name ? '-' + config.sprites[i].name : '') 28 | var task = gulp.src(config.sprites[i].path, { base: config.src_folder, cwd: config.dir, allowEmpty: true }) 29 | .pipe(svgSprite({ 30 | svg: { 31 | rootAttributes: { 32 | 'xmlns': 'http://www.w3.org/2000/svg', 33 | 'xmlns:xlink': 'http://www.w3.org/1999/xlink' 34 | } 35 | }, 36 | shape: { 37 | id: { 38 | generator: generator 39 | } 40 | }, 41 | mode: { 42 | symbol: { 43 | inline: true, 44 | dest: 'sprites', 45 | sprite: filename 46 | } 47 | } 48 | })) 49 | 50 | if(config.devmode){ 51 | task = task.pipe(plumber({ errorHandler: require('../helpers/error_handler') })) 52 | } 53 | tasks.add(task.pipe(gulp.dest(config.dist_persistent_folder))) 54 | })() 55 | } 56 | 57 | return tasks 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/tasks/static.js: -------------------------------------------------------------------------------- 1 | module.exports = function (gulp, config) { 2 | var errorHandler = require('../helpers/error_handler')() 3 | 4 | return function (done) { 5 | if (!config.static) return done() 6 | var c = require('better-console') 7 | c.info('~ static') 8 | 9 | var plumber = require('gulp-plumber') 10 | var rename = require('gulp-rename') 11 | var path = require('path') 12 | 13 | var task = gulp.src(config.static, { base: config.dir, cwd: config.dir, allowEmpty: true }) 14 | 15 | // Init plumber in devmode 16 | if (config.devmode) { 17 | task = task.pipe(plumber({ errorHandler: errorHandler.fail })) 18 | } 19 | 20 | var path_src = path.normalize(config.src_folder) 21 | 22 | task = task.pipe(rename(function (filepath) { 23 | // Extract src_folder from file paths 24 | if (filepath.dirname.indexOf(path_src) === 0) { 25 | filepath.dirname = filepath.dirname.substr(path_src.length) 26 | } 27 | })) 28 | 29 | if (config.devmode) { 30 | task.on('end', function () { 31 | errorHandler.done() 32 | }) 33 | } 34 | 35 | return task.pipe(gulp.dest(config.dist_persistent_folder)) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/tasks/styles.js: -------------------------------------------------------------------------------- 1 | module.exports = function(gulp, config) { 2 | var errorHandler = require('../helpers/error_handler')() 3 | 4 | return function(done) { 5 | if(!config.styles) return done() 6 | var c = require('better-console') 7 | c.info('~ styles') 8 | 9 | var autoprefixer = require('autoprefixer') 10 | var cache = require('gulp-cached') 11 | var cleancss = require('gulp-clean-css') 12 | var gulpFilter = require('gulp-filter') 13 | var less = require('gulp-less') 14 | var nib = require('nib') 15 | var path = require('path') 16 | var plumber = require('gulp-plumber') 17 | var postcss = require('gulp-postcss') 18 | var sass = require('gulp-sass') 19 | var sassGlob = require('gulp-sass-glob') 20 | var sourcemaps = require('gulp-sourcemaps') 21 | var stylus = require('gulp-stylus') 22 | var assign = require('lodash').assign 23 | 24 | var filterLess = gulpFilter('**/*.less', { restore: true }) 25 | var filterSass = gulpFilter('**/*.{scss,sass}', { restore: true }) 26 | var filterStylus = gulpFilter('**/*.styl', { restore: true }) 27 | 28 | var task = gulp.src(config.styles, { 29 | base: config.src_folder, 30 | cwd: config.dir, 31 | allowEmpty: true, 32 | }) 33 | 34 | var stylusOptions = assign({}, { 35 | use: nib(), 36 | define: { 37 | debug: config.devmode 38 | }, 39 | 'include css': true 40 | }, config.stylus | {}) 41 | 42 | var sassOptions = {} 43 | 44 | if(config.sassNodeModulesAlias) { 45 | sassOptions = assign({}, sassOptions, { 46 | importer: require('node-sass-package-importer')() 47 | }) 48 | } 49 | 50 | // Init sourcemaps and plumber in devmode 51 | if(config.devmode){ 52 | task = task.pipe(plumber({ errorHandler: errorHandler.fail })) 53 | 54 | if(config.disableSourcemaps !== true) { 55 | task = task.pipe(sourcemaps.init()) 56 | } 57 | } 58 | 59 | // Stylus part 60 | task = task.pipe(filterStylus) 61 | .pipe(stylus(stylusOptions)) 62 | .pipe(filterStylus.restore) 63 | // Sass part 64 | .pipe(filterSass) 65 | .pipe(sassGlob()) 66 | .pipe(sass(sassOptions)) 67 | .pipe(filterSass.restore) 68 | // LESS part 69 | .pipe(filterLess) 70 | .pipe(less()) 71 | .pipe(filterLess.restore) 72 | // Autoprefixer with custom options 73 | .pipe(cache('styles', { optimizeMemory: true })) 74 | .pipe(postcss([ 75 | autoprefixer(config.autoprefixer ? config.autoprefixer : { 76 | cascade: false, remove: false 77 | }) 78 | ])) 79 | 80 | // Minimalization OR sourcemaps goodness 81 | if(!config.devmode) { 82 | 83 | if(config.cssmin !== false) { 84 | task = task.pipe(cleancss(config.cssmin ? config.cssmin : { 85 | timeout: 30, // clean-css^3.4 86 | inlineTimeout: 30, // forward compatibility with clean-css^4.0 87 | advanced: false, 88 | })) 89 | } 90 | 91 | } else { 92 | if(config.disableSourcemaps !== true) { 93 | task = task.pipe(sourcemaps.write('.')) 94 | } 95 | } 96 | 97 | if(config.devmode){ 98 | task.on('end', function() { 99 | errorHandler.done() 100 | }) 101 | } 102 | 103 | return task.pipe(gulp.dest(config.dist_persistent_folder)) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /lib/tasks/templates.js: -------------------------------------------------------------------------------- 1 | module.exports = function(gulp, config) { 2 | var errorHandler = require('../helpers/error_handler')() 3 | var resolveData = require('../helpers/resolveData') 4 | 5 | return function(done) { 6 | if(!config.templates) return done() 7 | var c = require('better-console') 8 | c.info('~ templates') 9 | 10 | var _ = require('lodash') 11 | var s_ = require('stream') 12 | var cache = require('gulp-cached') 13 | var data = require('gulp-data') 14 | var frontMatter = require('gulp-front-matter') 15 | var gulpFilter = require('gulp-filter') 16 | var gulpif = require('gulp-if') 17 | var pug = require('pug') 18 | var gulpPug = require('gulp-pug') 19 | var jf = require('jsonfile') 20 | var path = require('path') 21 | var plumber = require('gulp-plumber') 22 | var merge = require('merge-stream') 23 | 24 | var filterPug = gulpFilter('**/*.{jade,pug}', { restore: true }) 25 | var filterStatic = gulpFilter('**/*.{html,htm}', { restore: true }) 26 | 27 | var sourceGlob = [] 28 | var templatesData = {} 29 | var masterTemplates = [] 30 | 31 | for (var i = 0; i < config.templates.length; i++) { 32 | var template = config.templates[i] 33 | // Master templates 34 | if(typeof template == 'object' && typeof template.template == 'string') { 35 | masterTemplates.push(template) 36 | // Single templates 37 | } else if (typeof template == "string") { 38 | sourceGlob.push(template) 39 | } else { 40 | c.error('Templates: Unrecognised type of input') 41 | } 42 | } 43 | 44 | // Try to read data from external JSON files or pass inline object 45 | if(config.data) { 46 | for(var key in config.data){ 47 | var value = config.data[key] 48 | var resolve = resolveData(value, config) 49 | if(resolve !== null) { 50 | value = resolve 51 | } 52 | templatesData[key] = value 53 | } 54 | } 55 | templatesData.require = require 56 | templatesData.devmode = config.devmode 57 | templatesData.production = !config.devmode 58 | templatesData.buildstamp = (config.buildstamp && !config.devmode) ? config.stamp : '' 59 | 60 | // Default Jade options 61 | var pugOptions = { 62 | pug: pug, 63 | pretty: true, 64 | cache: true, 65 | compileDebug: true, 66 | doctype: 'html', 67 | basedir: config.dir 68 | } 69 | if(config.pug) { 70 | _.assign(pugOptions, config.pug) 71 | } 72 | 73 | var source = merge() 74 | var sourceOpts = { 75 | // base: config.src_folder, <-- this causes templates not to be in the root folder 76 | cwd: config.dir, 77 | allowEmpty: true, 78 | } 79 | 80 | // Pipe all normal templates 81 | source.add(gulp.src(sourceGlob, sourceOpts)) 82 | 83 | // Pipe templates, which has to be multiplied 84 | _.each(masterTemplates, template => { 85 | var transformFnc = function() { 86 | var stream = new s_.Transform({ objectMode: true }) 87 | 88 | stream._transform = (file, enc, callback) => { 89 | file.templateData = template.data 90 | callback(null, file) 91 | } 92 | return stream 93 | } 94 | 95 | source.add(gulp.src(template.template, sourceOpts).pipe(transformFnc())) 96 | }) 97 | 98 | // Init plumber in devmode 99 | var task = source 100 | if(config.devmode){ 101 | task = task.pipe(plumber({ errorHandler: errorHandler.fail })) 102 | } 103 | 104 | task = task.pipe(frontMatter({ 105 | property: 'frontMatter', 106 | remove: true 107 | })) 108 | // Add data to files which needs that 109 | .pipe(filterPug) 110 | .pipe(function () { 111 | var transformStream = new s_.Transform({ objectMode: true }); 112 | transformStream._transform = function (file, encoding, callback) { 113 | if(file.templateData) { 114 | var filesData = resolveData(file.templateData, config) 115 | for (var index in filesData) { 116 | var fileClone = file.clone() 117 | var fileData = filesData[index] 118 | fileClone.path = path.join(fileClone.base, index) 119 | fileClone.data = { fileData: fileData, filePath: fileClone.path } 120 | this.push(fileClone) 121 | } 122 | callback() 123 | return 124 | } 125 | callback(null, file) 126 | } 127 | return transformStream 128 | }()) 129 | .pipe(data(function(file) { 130 | var name = path.basename(file.path) 131 | var data = _.cloneDeep(templatesData) 132 | 133 | data.__filename = file.path 134 | data.__dirname = path.dirname(file.path) 135 | 136 | // In case of name == key, assign value as global object 137 | if(typeof templatesData[name] !== 'undefined') { 138 | var obj = _.assign(data, templatesData[name], file.data) 139 | return obj 140 | } else { 141 | var obj = _.assign(data, file.data) 142 | return obj 143 | } 144 | })) 145 | .pipe(gulpPug(pugOptions)) 146 | .pipe(filterPug.restore) 147 | .pipe(cache('templates', { optimizeMemory: true })) 148 | 149 | if(config.devmode){ 150 | task.on('end', function() { 151 | errorHandler.done() 152 | }) 153 | } 154 | 155 | return task.pipe(gulp.dest(config.dist_folder)) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /lib/tasks/watch_reload.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = function(gulp, config, watch) { 4 | 5 | return function(done) { 6 | var c = require('better-console') 7 | c.info('Starting BrowserSync server...') 8 | 9 | var browsersync = require('browser-sync').create() 10 | var compress = require('compression') 11 | var fs = require('fs') 12 | 13 | var bsOptions = config.browsersync || {} 14 | 15 | // Switch between proxy and server modes 16 | if(config.proxy) { 17 | bsOptions.proxy = config.proxy 18 | } else if(config.snippet) { 19 | // snippet mode 20 | } else { 21 | bsOptions.server = { 22 | baseDir: config.dist_folder, 23 | middleware: [compress()] 24 | } 25 | } 26 | 27 | bsOptions.logLevel = bsOptions.logLevel || "silent" 28 | 29 | // Fix for Nette debug bar 30 | bsOptions.snippetOptions = { 31 | rule: { 32 | match: /]*>/i, 33 | fn: function (snippet, match) { 34 | if(match === '') { 35 | return match 36 | } 37 | return match + snippet; 38 | } 39 | } 40 | } 41 | 42 | // Start a browsersync server 43 | browsersync.init(bsOptions, function(err, bs) { 44 | var snippet_filename = path.resolve(config.dist_folder, '.mango-snippet.html'); 45 | if(config.snippet) { 46 | require('fs').writeFile(snippet_filename, bs.getOption('snippet'), function(){ 47 | process.on('exit', function(){ 48 | require('fs').unlinkSync(snippet_filename) 49 | }) 50 | }) 51 | } 52 | if(!err){ 53 | c.warn(`Server started on ${bs.options.get('urls').get('local')}, kill it with CTRL+C when you're done`) 54 | } 55 | }) 56 | 57 | // Now configude watch task 58 | var subtractPaths = require('../helpers/subtractPaths') 59 | var distFolderRelative = subtractPaths(config.dist_folder, config.dir) 60 | 61 | var glob = [ distFolderRelative + '/**/*.*' ] 62 | 63 | if(config.watch) { 64 | glob = glob.concat(config.watch) 65 | } 66 | 67 | // var options = { 68 | // base: config.dir, 69 | // read: false, 70 | // interval: 500 71 | // } 72 | 73 | var changeHandler = function(filepath) { 74 | // ignore *.map, *.gz files, *.d.ts 75 | if(~filepath.search(/\.(map|gz|d\.ts)$/i)) { 76 | return false 77 | } 78 | 79 | // files only 80 | try { 81 | 82 | if(!fs.existsSync(filepath) || !fs.lstatSync(filepath).isFile()) { 83 | return false 84 | } 85 | c.log('~ changed: ' + filepath) 86 | browsersync.reload(filepath) 87 | 88 | } catch(e) { 89 | c.error(e) 90 | } 91 | } 92 | 93 | for(var k in glob) { 94 | watch(glob[k], changeHandler) 95 | } 96 | 97 | done() 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /lib/tasks/watch_sources.js: -------------------------------------------------------------------------------- 1 | var mappingDefaults = { 2 | styles: ['css', 'less', 'scss', 'sass', 'styl'], 3 | templates: ['jade', 'pug', 'json', 'html', 'htm', 'yml', 'yaml'], 4 | images: ['jpg', 'jpeg', 'png', 'svg'], 5 | sprites: ['svg'] 6 | } 7 | 8 | 9 | module.exports = function(gulp, config, watch) { 10 | var mapping = require('lodash').merge({}, mappingDefaults, config.mapping || {}) 11 | 12 | var fileExtensions = [] 13 | var mappingRegEx = {} 14 | Object.keys(mapping).forEach(function(key) { 15 | mappingRegEx[key] = [] 16 | mappingRegEx[key] = mapping[key].map(function(i) { 17 | var ext = '.' + i.replace(/^\./, '') // remove . from the beginning of the string 18 | if(fileExtensions.indexOf(key) == -1) { 19 | fileExtensions.push(ext.substring(1)) 20 | } 21 | return new RegExp(ext + '$') 22 | }) 23 | }) 24 | 25 | return function(done) { 26 | var c = require('better-console') 27 | c.info('Watching sources for change and recompilation...') 28 | 29 | var log = require('better-console') 30 | var minimatch = require('minimatch') 31 | var path = require('path') 32 | var runcmd = require('../helpers/runcmd') 33 | var glob = '**/*.{' + fileExtensions.join() + '}' 34 | var subtractPaths = require('../helpers/subtractPaths') 35 | var distFolderRelative = subtractPaths(config.dist_folder, config.dir) 36 | var globDist = distFolderRelative + '/**/*.*' 37 | 38 | 39 | var completeHandler = function(e) { 40 | if(config.hooks && config.hooks.watch) { 41 | log.info('~ watch hook: ' + config.hooks.watch) 42 | runcmd(config.hooks.watch, config.dir, function() { 43 | log.info('/>') 44 | }) 45 | } 46 | } 47 | 48 | var changeHandler = function(filepath) { 49 | // Filter dist sources 50 | if (minimatch(filepath, globDist)) { 51 | return 52 | } 53 | 54 | config._lastChanged = filepath 55 | 56 | var handlers = [] 57 | 58 | Object.keys(mappingRegEx).forEach(function (key) { 59 | 60 | mappingRegEx[key].forEach(function(regexp) { 61 | if(filepath.match(regexp)) { 62 | handlers.push(key) 63 | } 64 | }) 65 | }) 66 | 67 | if (!handlers.length) { 68 | c.warn('! unknown extension', path.extname(filepath), filepath) 69 | return 70 | } 71 | 72 | handlers.forEach(function(handler) { 73 | try { 74 | gulp.task(handler)(completeHandler) 75 | } catch(e) { 76 | // task is not defined, we can silently ignore it 77 | } 78 | }) 79 | 80 | 81 | } 82 | 83 | watch(glob, changeHandler) 84 | 85 | done() 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mango-cli", 3 | "version": "3.9.6", 4 | "description": "Scaffold and build your projects way more faster than before. Preconfigured frontend devstack to the absolute perfection. Fully automated to save your precious time. Ready for any type of web project.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha" 8 | }, 9 | "bin": { 10 | "mango": "bin/mango" 11 | }, 12 | "preferglobal": true, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/manGoweb/mango-cli.git" 16 | }, 17 | "engines": { 18 | "node": ">=14", 19 | "npm": ">=7" 20 | }, 21 | "keywords": [ 22 | "browserify", 23 | "browsersync", 24 | "build", 25 | "clean-css", 26 | "coffeescript", 27 | "frontend", 28 | "generator", 29 | "gulp", 30 | "imagemin", 31 | "jade", 32 | "mango", 33 | "mangoweb", 34 | "react", 35 | "sass", 36 | "scaffolding", 37 | "sourcemaps", 38 | "stylus", 39 | "uglifyjs" 40 | ], 41 | "author": "manGoweb s.r.o. (www.mangoweb.cz)", 42 | "contributors": [ 43 | "Filip Chalupa (onset.github.io)", 44 | "Matěj Šimek (matejsimek.com)", 45 | "Viliam Kopecký (viliamkopecky.com)", 46 | "Vojta Staněk (stanekv.eu)" 47 | ], 48 | "license": "MIT", 49 | "bugs": { 50 | "url": "https://github.com/manGoweb/mango-cli/issues" 51 | }, 52 | "homepage": "https://github.com/manGoweb/mango-cli", 53 | "dependencies": { 54 | "@babel/core": "7.12.9", 55 | "@babel/plugin-proposal-class-properties": "7.12.1", 56 | "@babel/plugin-proposal-decorators": "7.12.1", 57 | "@babel/plugin-proposal-object-rest-spread": "7.12.1", 58 | "@babel/plugin-syntax-class-properties": "7.12.1", 59 | "@babel/plugin-syntax-decorators": "7.12.1", 60 | "@babel/plugin-syntax-dynamic-import": "7.8.3", 61 | "@babel/plugin-syntax-object-rest-spread": "7.8.3", 62 | "@babel/preset-env": "7.12.7", 63 | "@babel/preset-react": "7.12.7", 64 | "acorn": "^8.0.4", 65 | "autoprefixer": "^10.0.4", 66 | "babel-loader": "^8.2.2", 67 | "better-console": "^1.0.1", 68 | "browser-sync": "^2.26.13", 69 | "chalk": "^4.1.0", 70 | "chokidar": "^3.4.3", 71 | "compression": "^1.7.4", 72 | "del": "^6.0.0", 73 | "gift": "^0.10.2", 74 | "gulp": "^4.0.2", 75 | "gulp-cached": "^1.1.1", 76 | "gulp-clean-css": "^4.3.0", 77 | "gulp-data": "^1.3.1", 78 | "gulp-file": "^0.4.0", 79 | "gulp-filter": "^6.0.0", 80 | "gulp-front-matter": "^1.3.0", 81 | "gulp-if": "^3.0.0", 82 | "gulp-imagemin": "^7.1.0", 83 | "gulp-less": "^4.0.1", 84 | "gulp-plumber": "^1.2.1", 85 | "gulp-postcss": "^9.0.0", 86 | "gulp-pug": "^4.0.1", 87 | "gulp-rename": "^2.0.0", 88 | "gulp-sass": "^4.1.0", 89 | "gulp-sass-glob": "^1.1.0", 90 | "gulp-sourcemaps": "^3.0.0", 91 | "gulp-stylus": "^2.7.0", 92 | "gulp-svg-sprite": "^1.5.0", 93 | "html-loader": "^1.3.2", 94 | "js-yaml": "^3.14.0", 95 | "jsonfile": "^6.1.0", 96 | "lodash": "^4.17.20", 97 | "merge-stream": "^2.0.0", 98 | "minimatch": "^3.0.4", 99 | "natives": "^1.1.6", 100 | "nib": "^1.1.2", 101 | "node-notifier": "^8.0.0", 102 | "node-sass": "^7.0.3", 103 | "node-sass-package-importer": "^5.3.2", 104 | "postcss": "^8.1.14", 105 | "pug": "^3.0.0", 106 | "pug-loader": "^2.4.0", 107 | "run-sequence": "^2.2.1", 108 | "semver": "^7.3.4", 109 | "sharp": "^0.26.3", 110 | "svelte": "^3.31.0", 111 | "svelte-loader": "^2.13.6", 112 | "ts-loader": "^8.0.11", 113 | "typescript": "4.1.2", 114 | "vinyl-named": "^1.1.0", 115 | "webpack": "^5.9.0", 116 | "webpack-stream": "6.1.1", 117 | "yargs": "^16.1.1", 118 | "yml-loader": "^2.1.0" 119 | }, 120 | "optionalDependencies": {}, 121 | "config": { 122 | "appId": "cz.mangoweb.mango-cli", 123 | "default_fork_repo": "https://github.com/mangoweb/mango-cli-example.git", 124 | "tests_repo": "https://github.com/manGoweb/mango-cli-test.git" 125 | }, 126 | "devDependencies": { 127 | "mocha": "^8.2.1", 128 | "should": "^13.2.3" 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /test/test-lib.js: -------------------------------------------------------------------------------- 1 | var del = require('del') 2 | var fs = require('fs') 3 | var os = require('os') 4 | var path = require('path') 5 | var should = require('should') 6 | const runcmd = require('../lib/helpers/runcmd') 7 | 8 | console.log('Preparing temp folder...') 9 | var tmpDir = os.tmpdir() + path.sep 10 | var TEMP = fs.mkdtempSync(tmpDir) 11 | console.log('TEMP:', TEMP) 12 | 13 | var cleanup = function() { 14 | console.log('Clearing temp folder...', TEMP) 15 | del.sync(TEMP, { force: true }) 16 | } 17 | 18 | describe('Mango class', function() { 19 | var Mango = require('../lib/mango') 20 | var Config = require('../lib/helpers/config') 21 | 22 | describe('should create a new instance', function() { 23 | 24 | it('with no arguments', function() { 25 | new Mango() 26 | }) 27 | 28 | it('with only the folder argument', function() { 29 | new Mango(process.cwd()) 30 | }) 31 | 32 | it('with a folder argument and an empty object as the config parameter', function() { 33 | new Mango(process.cwd(), {}) 34 | }) 35 | 36 | }) 37 | 38 | describe('should in a temp directory', function() { 39 | var mango 40 | 41 | it('init a template', function(done) { 42 | this.timeout(15000) 43 | var pkg = require('../package') 44 | mango = new Mango(TEMP) 45 | mango.init(pkg.config.tests_repo, done) 46 | }) 47 | 48 | it('read the configuration file', async function() { 49 | var config = new Config(TEMP) 50 | mango = new Mango(TEMP, await config.get()) 51 | }) 52 | 53 | it('run init hooks', function(done) { 54 | this.timeout(60000) 55 | if(mango.config.hooks && mango.config.hooks.init) { 56 | runcmd(mango.config.hooks.init, mango.config.dir, done) 57 | } else { 58 | done() 59 | } 60 | }) 61 | 62 | it('build scripts', function(done) { 63 | this.timeout(120000) 64 | mango.build(['scripts'], [], done) 65 | }) 66 | 67 | it('build styles', function(done) { 68 | this.timeout(120000) 69 | mango.build(['styles'], [], done) 70 | }) 71 | 72 | it('build static', function(done) { 73 | this.timeout(120000) 74 | mango.build(['static'], [], done) 75 | }) 76 | 77 | it('build sprites', function(done) { 78 | this.timeout(120000) 79 | mango.build(['static'], [], done) 80 | }) 81 | 82 | it('build templates', function(done) { 83 | this.timeout(120000) 84 | mango.build(['templates'], [], done) 85 | }) 86 | 87 | it('build images', function(done) { 88 | this.timeout(120000) 89 | mango.build(['images'], [], done) 90 | }) 91 | 92 | it('run the production build tasks', function(done) { 93 | this.timeout(120000) 94 | mango.build([], [], done) 95 | }) 96 | 97 | after(cleanup) 98 | }) 99 | 100 | 101 | }) 102 | --------------------------------------------------------------------------------