├── .editorconfig ├── .gitattributes ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .nycrc ├── ACKNOWLEDGEMENTS.md ├── CHANGELOG.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── LICENSE ├── README.md ├── bin └── aurelia-cli.js ├── docs └── MAINTAINERS.md ├── eslint.config.mjs ├── lib ├── build │ ├── amodro-trace │ │ ├── lib │ │ │ ├── lang.js │ │ │ ├── parse.js │ │ │ └── transform.js │ │ ├── read │ │ │ ├── cjs.js │ │ │ └── es.js │ │ └── write │ │ │ ├── all.js │ │ │ ├── defines.js │ │ │ ├── replace.js │ │ │ └── stubs.js │ ├── ast-matcher.js │ ├── bundle.js │ ├── bundled-source.js │ ├── bundler.js │ ├── dependency-description.js │ ├── dependency-inclusion.js │ ├── find-deps.js │ ├── index.js │ ├── inject-css.js │ ├── loader-plugin.js │ ├── loader.js │ ├── module-id-processor.js │ ├── package-analyzer.js │ ├── package-installer.js │ ├── source-inclusion.js │ ├── stub-module.js │ ├── utils.js │ └── webpack-reporter.js ├── cli-options.js ├── cli.js ├── commands │ ├── alias.json │ ├── config │ │ ├── command.js │ │ ├── command.json │ │ ├── configuration.js │ │ └── util.js │ ├── generate │ │ ├── command.js │ │ └── command.json │ ├── gulp.js │ ├── help │ │ ├── command.js │ │ └── command.json │ └── new │ │ ├── command.js │ │ └── command.json ├── configuration.js ├── file-system.js ├── get-tty-size.js ├── index.js ├── logger.js ├── package-managers │ ├── base-package-manager.js │ ├── npm.js │ └── yarn.js ├── pretty-choices.js ├── project-item.js ├── project.js ├── resources │ ├── logo.txt │ └── scripts │ │ ├── configure-bluebird-no-long-stacktraces.js │ │ ├── configure-bluebird.js │ │ └── keep-them.md ├── string.js └── ui.js ├── package.json ├── spec ├── helpers │ ├── polyfills.js │ └── reporter.js ├── lib │ ├── build │ │ ├── ast-matcher.spec.js │ │ ├── bundle.spec.js │ │ ├── bundled-source.spec.js │ │ ├── bundler.spec.js │ │ ├── dependency-description.spec.js │ │ ├── dependency-inclusion.spec.js │ │ ├── find-deps.spec.js │ │ ├── inject-css.spec.js │ │ ├── module-id-processor.spec.js │ │ ├── package-analyzer.spec.js │ │ ├── package-installer.spec.js │ │ ├── source-inclusion.spec.js │ │ ├── stub-module.spec.js │ │ └── utils.spec.js │ ├── cli-options.spec.ts │ ├── cli.spec.js │ ├── commands │ │ └── config │ │ │ ├── configuration.spec.js │ │ │ └── util.spec.js │ ├── configuration.spec.js │ ├── file-system.spec.js │ ├── project-item.spec.js │ ├── project.spec.js │ └── ui.spec.js ├── mocks │ ├── bundler.js │ ├── cli-options.js │ ├── mock-fs.js │ ├── package-analyzer.js │ ├── project-mock.js │ └── ui.js └── support │ └── jasmine.json └── wallaby.conf.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | # 2 space indentation 12 | [**.*] 13 | indent_style = space 14 | indent_size = 2 15 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set. 2 | * text eol=lf 3 | 4 | # Explicitly declare text files you want to always be normalized and converted 5 | # to native line endings on checkout. 6 | *.js text 7 | *.md text 8 | *.json text 9 | 10 | # Denote all files that are truly binary and should not be modified. 11 | *.png binary 12 | *.jpg binary 13 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | test: 13 | name: Nodejs ${{ matrix.node_version }} on ${{ matrix.os }} 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | matrix: 17 | node_version: ['18', '20'] 18 | os: [ubuntu-latest, windows-latest, macOS-latest] 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | - name: Use Node.js ${{ matrix.node_version }} 23 | uses: actions/setup-node@v3 24 | with: 25 | node-version: ${{ matrix.node_version }} 26 | - run: npm install 27 | - run: npm test 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | jspm_packages 3 | bower_components 4 | .idea 5 | .DS_STORE 6 | *.swp 7 | .blog 8 | dist 9 | /release-checks-results 10 | /.nyc_output 11 | /coverage 12 | /package-lock.json 13 | /yarn.lock 14 | /tmpdir* 15 | .npmrc 16 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "lib/**/*.js" 4 | ], 5 | "exclude": [ 6 | "lib/build/amodro-trace/lib/**/*.js" 7 | ], 8 | "reporter": [ 9 | "lcov", 10 | "text" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /ACKNOWLEDGEMENTS.md: -------------------------------------------------------------------------------- 1 | # Acknowledgements 2 | 3 | ## amodro-trace (embedded) 4 | 5 | MIT License 6 | ----------- 7 | 8 | Copyright (c) 2010-2015, The Dojo Foundation 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in 18 | all copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 26 | THE SOFTWARE. 27 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We'd love for you to contribute and to make this project even better than it is today! If this interests you, please begin by reading [our contributing guidelines](https://github.com/DurandalProject/about/blob/master/CONTRIBUTING.md). The contributing document will provide you with all the information you need to get started. Once you have read that, you will need to also [sign our CLA](http://goo.gl/forms/dI8QDDSyKR) before we can accept a Pull Request from you. More information on the process is included in the [contributor's guide](https://github.com/DurandalProject/about/blob/master/CONTRIBUTING.md). 4 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 19 | **I'm submitting a bug report** 20 | **I'm submitting a feature request** 21 | 22 | * **Library Version:** 23 | major.minor.patch-pre 24 | 25 | 26 | **Please tell us about your environment:** 27 | * **Operating System:** 28 | OSX 10.x|Linux (distro)|Windows [7|8|8.1|10] 29 | 30 | * **Node Version:** 31 | 6.2.0 32 | 36 | 37 | * **NPM Version:** 38 | 3.8.9 39 | 43 | 44 | * **Browser:** 45 | all | Chrome XX | Firefox XX | Edge XX | IE XX | Safari XX | Mobile Chrome XX | Android X.X Web Browser | iOS XX Safari | iOS XX UIWebView | iOS XX WKWebView 46 | 47 | * **Language:** 48 | all | TypeScript X.X | ESNext 49 | 50 | * **Loader/bundler:** 51 | all | Webpack | SystemJS | RequireJS 52 | 53 | **Current behavior:** 54 | 55 | * **What is the expected behavior?** 56 | 59 | 60 | * **What is the motivation / use case for changing the behavior?** 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2010 - 2016 Blue Spire Inc. 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 | # Aurelia CLI 2 | 3 | ![CI](https://github.com/aurelia/cli/workflows/CI/badge.svg) 4 | [![npm Version](https://img.shields.io/npm/v/aurelia-cli.svg)](https://www.npmjs.com/package/aurelia-cli) 5 | [![Join the chat at https://gitter.im/aurelia/discuss](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/aurelia/discuss?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 6 | [![Twitter](https://img.shields.io/twitter/follow/aureliaeffect.svg?style=social&label=Follow)](https://twitter.com/intent/follow?screen_name=aureliaeffect) 7 | 8 | This library is part of the [Aurelia](http://www.aurelia.io/) platform and contains its CLI implementation. 9 | To keep up to date on [Aurelia](http://www.aurelia.io/), please visit and subscribe to [the official blog](http://blog.aurelia.io/) and [our email list](http://eepurl.com/ces50j). We also invite you to [follow us on twitter](https://twitter.com/aureliaeffect). If you have questions look around our [Discourse forums](https://discourse.aurelia.io/), chat in our [community on Gitter](https://gitter.im/aurelia/discuss) or use [stack overflow](http://stackoverflow.com/search?q=aurelia). Documentation can be found [in our developer hub](http://aurelia.io/docs). 10 | 11 | ## Documentation 12 | 13 | You can read documentation on the cli [here](https://aurelia.io/docs/cli). If you would like to help improve this documentation, visit [aurelia/documentation](https://github.com/aurelia/documentation/tree/master/current/en-us/11.%20cli). 14 | 15 | ## Contributing 16 | 17 | Please see the [contributing guidelines](./CONTRIBUTING.md). 18 | 19 | ## Providing new feature to app skeleton 20 | 21 | App skeleton is no longer in this repo, it has been moved to a dedicated repo [aurelia/v1](https://github.com/aurelia/v1). Any contribution to app skeleton should go into [aurelia/v1](https://github.com/aurelia/v1). 22 | 23 | The `au new` command now simplify wraps `npx makes aurelia/v1`. Users can directly use that makes command to create new project. 24 | 25 | ## Building 26 | 27 | 1. Clone the aurelia-cli: `git clone https://github.com/aurelia/cli.git` 28 | 2. Go into the cli directory: `cd cli` 29 | 3. Run `npm install` 30 | 4. Link the cli with: `npm link` 31 | 5. Create a new project with `au new` or use an existing project. The linked CLI will be used to create the project. 32 | 6. In the project directory, run `npm link aurelia-cli`. The linked CLI will then be used for `au` commands such as `au run` 33 | 34 | ## Running the Tests 35 | 36 | Run `npm test` to run the unit tests. 37 | 38 | ## Release new aurelia-cli version 39 | 40 | Just run `npm version patch` (or minor or major) 41 | 42 | ## License 43 | 44 | MIT. 45 | -------------------------------------------------------------------------------- /bin/aurelia-cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const resolve = require('resolve'); 4 | 5 | const semver = require('semver'); 6 | const nodeVersion = process.versions.node; 7 | if (semver.lt(nodeVersion, '10.12.0')) { 8 | console.error(`You are running Node.js v${nodeVersion}. 9 | aurelia-cli requires Node.js v10.12.0 or above. 10 | Please upgrade to latest Node.js https://nodejs.org`); 11 | process.exit(1); 12 | } 13 | 14 | process.title = 'aurelia'; 15 | 16 | const userArgs = process.argv.slice(2); 17 | const commandName = userArgs[0]; 18 | const commandArgs = userArgs.slice(1); 19 | 20 | let originalBaseDir = process.cwd(); 21 | 22 | resolve('aurelia-cli', { 23 | basedir: originalBaseDir 24 | }, function(error, projectLocalCli) { 25 | let cli; 26 | 27 | if (commandName === 'new' || error) { 28 | cli = new (require('../lib/index').CLI); 29 | cli.options.runningGlobally = true; 30 | } else { 31 | cli = new (require(projectLocalCli).CLI); 32 | cli.options.runningLocally = true; 33 | } 34 | 35 | cli.options.originalBaseDir = originalBaseDir; 36 | 37 | cli.run(commandName, commandArgs).catch(err => { 38 | console.log(err); 39 | process.exit(1); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /docs/MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | Release process: 2 | 3 | - get latest 4 | - update version field in package.json + commit + tag 5 | - run `npm run version` -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from "globals"; 2 | import eslint from "@eslint/js"; 3 | 4 | export default [ 5 | { 6 | ignores: ["lib/build/amodro-trace", "**/dist"], 7 | }, 8 | eslint.configs.recommended, 9 | { 10 | languageOptions: { 11 | globals: { 12 | ...globals.node, 13 | ...globals.jasmine, 14 | }, 15 | 16 | ecmaVersion: 2019, 17 | sourceType: "commonjs", 18 | }, 19 | 20 | rules: { 21 | "no-prototype-builtins": 0, 22 | "no-console": 0, 23 | "getter-return": 0, 24 | "no-inner-declarations": 0, 25 | 26 | "comma-dangle": ["error", { 27 | arrays: "never", 28 | objects: "never", 29 | imports: "never", 30 | exports: "never", 31 | functions: "never", 32 | }], 33 | }, 34 | } 35 | ]; 36 | -------------------------------------------------------------------------------- /lib/build/amodro-trace/lib/lang.js: -------------------------------------------------------------------------------- 1 | var define = function(fn) { module.exports = fn(); }; 2 | 3 | /** 4 | * @license Copyright (c) 2010-2015, The Dojo Foundation All Rights Reserved. 5 | * Available via the MIT or new BSD license. 6 | * see: http://github.com/jrburke/requirejs for details 7 | */ 8 | 9 | /*jslint plusplus: true */ 10 | /*global define, java */ 11 | 12 | define(function () { 13 | 'use strict'; 14 | 15 | var lang, isJavaObj, 16 | hasOwn = Object.prototype.hasOwnProperty; 17 | 18 | function hasProp(obj, prop) { 19 | return hasOwn.call(obj, prop); 20 | } 21 | 22 | isJavaObj = function () { 23 | return false; 24 | }; 25 | 26 | //Rhino, but not Nashorn (detected by importPackage not existing) 27 | //Can have some strange foreign objects. 28 | if (typeof java !== 'undefined' && java.lang && java.lang.Object && typeof importPackage !== 'undefined') { 29 | isJavaObj = function (obj) { 30 | return obj instanceof java.lang.Object; 31 | }; 32 | } 33 | 34 | lang = { 35 | // makeJsArrayString added after porting to this project 36 | //Converts an JS array of strings to a string representation. 37 | //Not using JSON.stringify() for Rhino's sake. 38 | makeJsArrayString: function (ary) { 39 | return '["' + ary.map(function (item) { 40 | //Escape any double quotes, backslashes 41 | return lang.jsEscape(item); 42 | }).join('","') + '"]'; 43 | }, 44 | 45 | backSlashRegExp: /\\/g, 46 | ostring: Object.prototype.toString, 47 | 48 | isArray: Array.isArray || function (it) { 49 | return lang.ostring.call(it) === "[object Array]"; 50 | }, 51 | 52 | isFunction: function(it) { 53 | return lang.ostring.call(it) === "[object Function]"; 54 | }, 55 | 56 | isRegExp: function(it) { 57 | return it && it instanceof RegExp; 58 | }, 59 | 60 | hasProp: hasProp, 61 | 62 | //returns true if the object does not have an own property prop, 63 | //or if it does, it is a falsy value. 64 | falseProp: function (obj, prop) { 65 | return !hasProp(obj, prop) || !obj[prop]; 66 | }, 67 | 68 | //gets own property value for given prop on object 69 | getOwn: function (obj, prop) { 70 | return hasProp(obj, prop) && obj[prop]; 71 | }, 72 | 73 | _mixin: function(dest, source, override){ 74 | var name; 75 | for (name in source) { 76 | if(source.hasOwnProperty(name) && 77 | (override || !dest.hasOwnProperty(name))) { 78 | dest[name] = source[name]; 79 | } 80 | } 81 | 82 | return dest; // Object 83 | }, 84 | 85 | /** 86 | * mixin({}, obj1, obj2) is allowed. If the last argument is a boolean, 87 | * then the source objects properties are force copied over to dest. 88 | */ 89 | mixin: function(dest){ 90 | var parameters = Array.prototype.slice.call(arguments), 91 | override, i, l; 92 | 93 | if (!dest) { dest = {}; } 94 | 95 | if (parameters.length > 2 && typeof arguments[parameters.length-1] === 'boolean') { 96 | override = parameters.pop(); 97 | } 98 | 99 | for (i = 1, l = parameters.length; i < l; i++) { 100 | lang._mixin(dest, parameters[i], override); 101 | } 102 | return dest; // Object 103 | }, 104 | 105 | /** 106 | * Does a deep mix of source into dest, where source values override 107 | * dest values if a winner is needed. 108 | * @param {Object} dest destination object that receives the mixed 109 | * values. 110 | * @param {Object} source source object contributing properties to mix 111 | * in. 112 | * @return {[Object]} returns dest object with the modification. 113 | */ 114 | deepMix: function(dest, source) { 115 | lang.eachProp(source, function (value, prop) { 116 | if (typeof value === 'object' && value && 117 | !lang.isArray(value) && !lang.isFunction(value) && 118 | !(value instanceof RegExp)) { 119 | 120 | if (!dest[prop]) { 121 | dest[prop] = {}; 122 | } 123 | lang.deepMix(dest[prop], value); 124 | } else { 125 | dest[prop] = value; 126 | } 127 | }); 128 | return dest; 129 | }, 130 | 131 | /** 132 | * Does a type of deep copy. Do not give it anything fancy, best 133 | * for basic object copies of objects that also work well as 134 | * JSON-serialized things, or has properties pointing to functions. 135 | * For non-array/object values, just returns the same object. 136 | * @param {Object} obj copy properties from this object 137 | * @param {Object} [result] optional result object to use 138 | * @return {Object} 139 | */ 140 | deeplikeCopy: function (obj) { 141 | var type, result; 142 | 143 | if (lang.isArray(obj)) { 144 | result = []; 145 | obj.forEach(function(value) { 146 | result.push(lang.deeplikeCopy(value)); 147 | }); 148 | return result; 149 | } 150 | 151 | type = typeof obj; 152 | if (obj === null || obj === undefined || type === 'boolean' || 153 | type === 'string' || type === 'number' || lang.isFunction(obj) || 154 | lang.isRegExp(obj)|| isJavaObj(obj)) { 155 | return obj; 156 | } 157 | 158 | //Anything else is an object, hopefully. 159 | result = {}; 160 | lang.eachProp(obj, function(value, key) { 161 | result[key] = lang.deeplikeCopy(value); 162 | }); 163 | return result; 164 | }, 165 | 166 | delegate: (function () { 167 | // boodman/crockford delegation w/ cornford optimization 168 | function TMP() {} 169 | return function (obj, props) { 170 | TMP.prototype = obj; 171 | var tmp = new TMP(); 172 | TMP.prototype = null; 173 | if (props) { 174 | lang.mixin(tmp, props); 175 | } 176 | return tmp; // Object 177 | }; 178 | }()), 179 | 180 | /** 181 | * Helper function for iterating over an array. If the func returns 182 | * a true value, it will break out of the loop. 183 | */ 184 | each: function each(ary, func) { 185 | if (ary) { 186 | var i; 187 | for (i = 0; i < ary.length; i += 1) { 188 | if (func(ary[i], i, ary)) { 189 | break; 190 | } 191 | } 192 | } 193 | }, 194 | 195 | /** 196 | * Cycles over properties in an object and calls a function for each 197 | * property value. If the function returns a truthy value, then the 198 | * iteration is stopped. 199 | */ 200 | eachProp: function eachProp(obj, func) { 201 | var prop; 202 | for (prop in obj) { 203 | if (hasProp(obj, prop)) { 204 | if (func(obj[prop], prop)) { 205 | break; 206 | } 207 | } 208 | } 209 | }, 210 | 211 | //Similar to Function.prototype.bind, but the "this" object is specified 212 | //first, since it is easier to read/figure out what "this" will be. 213 | bind: function bind(obj, fn) { 214 | return function () { 215 | return fn.apply(obj, arguments); 216 | }; 217 | }, 218 | 219 | //Escapes a content string to be be a string that has characters escaped 220 | //for inclusion as part of a JS string. 221 | jsEscape: function (content) { 222 | return content.replace(/(["'\\])/g, '\\$1') 223 | .replace(/[\f]/g, "\\f") 224 | .replace(/[\b]/g, "\\b") 225 | .replace(/[\n]/g, "\\n") 226 | .replace(/[\t]/g, "\\t") 227 | .replace(/[\r]/g, "\\r"); 228 | } 229 | }; 230 | return lang; 231 | }); 232 | -------------------------------------------------------------------------------- /lib/build/amodro-trace/read/cjs.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Copyright (c) 2010-2015, The Dojo Foundation All Rights Reserved. 3 | * Available via the MIT or new BSD license. 4 | */ 5 | var parse = require('../lib/parse'); 6 | 7 | // modified to add forceWrap for dealing with 8 | // nodjs dist/commonjs/*js or dist/cjs/*.js 9 | // bypass r.js parse bug in usesCommonJs when checking 10 | // node_modules/@aspnet/signalr/dist/cjs/IHubProtocol.js 11 | // node_modules/@aspnet/signalr/dist/cjs/ILogger.js 12 | // https://github.com/requirejs/r.js/issues/980 13 | module.exports = function cjs(fileName, fileContents, forceWrap) { 14 | // Strip out comments. 15 | var preamble = '', 16 | commonJsProps = parse.usesCommonJs(fileName, fileContents); 17 | 18 | // First see if the module is not already RequireJS-formatted. 19 | if (!forceWrap && (parse.usesAmdOrRequireJs(fileName, fileContents) || !commonJsProps)) { 20 | return fileContents; 21 | } 22 | 23 | if (commonJsProps && (commonJsProps.dirname || commonJsProps.filename)) { 24 | preamble = 'var __filename = module.uri || \'\', ' + 25 | '__dirname = ' + 26 | '__filename.slice(0, __filename.lastIndexOf(\'/\') + 1); '; 27 | } 28 | 29 | // Construct the wrapper boilerplate. 30 | return 'define(function (require, exports, module) {' + 31 | preamble + 32 | fileContents + 33 | '\n});\n'; 34 | }; 35 | -------------------------------------------------------------------------------- /lib/build/amodro-trace/read/es.js: -------------------------------------------------------------------------------- 1 | const transform = require('@babel/core').transform; 2 | 3 | // use babel to translate native es module into AMD module 4 | module.exports = function es(fileName, fileContents) { 5 | return transform(fileContents, { 6 | babelrc: false, 7 | plugins: [['@babel/plugin-transform-modules-amd', {loose: true}]] 8 | }).code; 9 | }; 10 | -------------------------------------------------------------------------------- /lib/build/amodro-trace/write/all.js: -------------------------------------------------------------------------------- 1 | // The order of these transforms is informed by how they were done in the 2 | // requirejs optimizer. 3 | var transforms = [ 4 | require('./stubs'), 5 | require('./defines'), 6 | require('./replace') 7 | ]; 8 | 9 | /** 10 | * Chains all the default set of transforms to return one function to be used 11 | * for transform operations on traced module content. 12 | * @param {Object} options object for holding options. The same options object 13 | * is used and passed to all transforms. See individual transforms for their 14 | * options. 15 | * @return {Function} A function that can be used for multiple content transform 16 | * calls. 17 | */ 18 | module.exports = function all(options) { 19 | options = options || {}; 20 | 21 | var transformFns = transforms.map(function(transform) { 22 | return transform(options); 23 | }); 24 | 25 | return function(context, moduleName, filePath, contents) { 26 | contents = transformFns.reduce(function(contents, transformFn) { 27 | return transformFn(context, moduleName, filePath, contents); 28 | }, contents); 29 | 30 | return contents; 31 | }; 32 | 33 | }; 34 | -------------------------------------------------------------------------------- /lib/build/amodro-trace/write/defines.js: -------------------------------------------------------------------------------- 1 | var lang = require('../lib/lang'), 2 | parse = require('../lib/parse'), 3 | transform = require('../lib/transform'), 4 | falseProp = lang.falseProp, 5 | getOwn = lang.getOwn, 6 | makeJsArrayString = lang.makeJsArrayString; 7 | 8 | // options should include skipModuleInsertion and tracing for transform? 9 | 10 | /** 11 | * For modules that are inside a package config, this transform will write out 12 | * adapter define() entries for the package manin value, so that package config 13 | * is not needed to map 'packageName' to 'packageName/mainModuleId'. 14 | * @param {Object} options object for holding options. Supported options: 15 | * - wrapShim: if shim config is used for the module ID, then wrap it in a 16 | * function closure. This can be hazardous if the scripts assumes acces to 17 | * other variables at the top level. However, wrapping can be useful for shimmed 18 | * IDs that have dependencies, and where those dependencies may not be 19 | * immediately available or inlined with this shimmed script. 20 | * @return {Function} A function that can be used for multiple content transform 21 | * calls. 22 | */ 23 | function defines(options) { 24 | options = options || {}; 25 | 26 | return function(context, moduleName, filePath, _contents) { 27 | var namedModule, 28 | config = context.config, 29 | packageName = context.pkgsMainMap[moduleName]; 30 | 31 | if (packageName === 'moment') { 32 | // Expose moment to global var to improve compatibility with some legacy libs. 33 | // It also load momentjs up immediately. 34 | _contents = _contents.replace(/\bdefine\((\w+)\)/, (match, factoryName) => 35 | `(function(){var m=${factoryName}();if(typeof moment === 'undefined'){window.moment=m;} define(function(){return m;})})()` 36 | ); 37 | } 38 | 39 | function onFound(info) { 40 | if (info.foundId) { 41 | namedModule = info.foundId; 42 | } 43 | } 44 | 45 | let contents = toTransport(context, moduleName, 46 | filePath, _contents, onFound, options); 47 | 48 | //Some files may not have declared a require module, and if so, 49 | //put in a placeholder call so the require does not try to load them 50 | //after the module is processed. 51 | //If we have a name, but no defined module, then add in the placeholder. 52 | if (moduleName) { 53 | var shim = config.shim && (getOwn(config.shim, moduleName) || 54 | (packageName && getOwn(config.shim, packageName))); 55 | 56 | 57 | let amdProps = parse.usesAmdOrRequireJs(filePath, contents); 58 | // mimic requirejs runtime behaviour, 59 | // if no module defined, add an empty shim 60 | if (!shim && contents === _contents) { 61 | if (!amdProps || !amdProps.define) { 62 | shim = { deps: [] }; 63 | } 64 | } else if (shim && amdProps && amdProps.define) { 65 | // ignore shim for AMD/UMD 66 | shim = null; 67 | } 68 | 69 | if (shim) { 70 | if (options.wrapShim) { 71 | contents = '(function(root) {\n' + 72 | 'define("' + moduleName + '", ' + 73 | (shim.deps && shim.deps.length ? 74 | makeJsArrayString(shim.deps) + ', ' : '[], ') + 75 | 'function() {\n' + 76 | ' return (function() {\n' + 77 | contents + 78 | // Start with a \n in case last line is a comment 79 | // in the contents, like a sourceURL comment. 80 | '\n' + exportsFn(shim.exports, true) + 81 | '\n' + 82 | ' }).apply(root, arguments);\n' + 83 | '});\n' + 84 | '}(this));\n'; 85 | } else { 86 | contents += '\n' + 'define("' + moduleName + '", ' + 87 | (shim.deps && shim.deps.length ? 88 | makeJsArrayString(shim.deps) + ', ' : '') + 89 | exportsFn(shim.exports) + 90 | ');\n'; 91 | } 92 | } else { 93 | // we don't need placeholder in aurelia-cli bundle 94 | // contents += '\n' + 'define("' + moduleName + '", function(){});\n'; 95 | 96 | if (packageName && namedModule && namedModule !== packageName) { 97 | // for main module, if named module name doesn't match package name 98 | // make an alias from moduleName (not packageName) to namedModule 99 | contents += '\n;define("' + moduleName + '", ["' + namedModule + '"], function(m){return m;});\n'; 100 | } 101 | } 102 | } 103 | 104 | return contents; 105 | }; 106 | } 107 | 108 | function exportsFn(_exports, wrapShim) { 109 | if (_exports) { 110 | if (wrapShim) return 'return root.' + _exports + ' = ' + _exports +';'; 111 | else return '(function (global) {\n' + 112 | ' return function () {\n' + 113 | ' return global.' + _exports + ';\n' + 114 | ' };\n' + 115 | '}(this))'; 116 | } else { 117 | if (wrapShim) return ''; 118 | return 'function(){}'; 119 | } 120 | } 121 | 122 | /** 123 | * Modifies a define() call to make sure it has a module ID as the first 124 | * argument and to make sure the sugared define(function(require), {}) syntax 125 | * has an array of dependencies extracted and explicitly passed in, so that the 126 | * define() call works in JS environments that do not give the full function 127 | * bodies for Function.protototype.toString calls. 128 | * @param {Object} context loader context 129 | * @param {String} moduleName module ID 130 | * @param {String} filePath file path 131 | * @param {String} contents contents of the file for the given module ID. 132 | * @param {Object} options object for holding options. Supported options: 133 | * - logger: Object of logging functions. Currently only logger.warn is used 134 | * if a module output for an ID cannot be properly normalized for string 135 | * transport. 136 | * @return {String} transformed content. May not be different from 137 | * the input contents string. 138 | */ 139 | function toTransport(context, moduleName, 140 | filePath, contents, onFound, options) { 141 | options = options || {}; 142 | return transform.toTransport('', moduleName, filePath, 143 | contents, onFound, options); 144 | }; 145 | 146 | module.exports = defines; 147 | -------------------------------------------------------------------------------- /lib/build/amodro-trace/write/replace.js: -------------------------------------------------------------------------------- 1 | // browser replacement 2 | // https://github.com/defunctzombie/package-browser-field-spec 3 | // see bundled-source.js for more details 4 | 5 | // and also dep string cleanup 6 | // remove tailing '/', '.js' 7 | const meriyah = require('meriyah'); 8 | const astMatcher = require('../../ast-matcher').astMatcher; 9 | // it is definitely a named AMD module at this stage 10 | var amdDep = astMatcher('define(__str, [__anl_deps], __any)'); 11 | var cjsDep = astMatcher('require(__any_dep)'); 12 | var isUMD = astMatcher('typeof define === "function" && define.amd'); 13 | var isUMD2 = astMatcher('typeof define == "function" && define.amd'); 14 | 15 | module.exports = function stubs(options) { 16 | options = options || {}; 17 | 18 | return function(context, moduleName, filePath, contents) { 19 | const replacement = options.replacement; 20 | const toReplace = []; 21 | 22 | const _find = node => { 23 | if (node.type !== 'Literal') return; 24 | let dep = node.value; 25 | // remove tailing '/' 26 | if (dep.endsWith('/')) { 27 | dep = dep.slice(0, -1); 28 | } 29 | // remove tailing '.js', but only when dep is not 30 | // referencing a npm package main 31 | if (dep.endsWith('.js') && !isPackageName(dep)) { 32 | dep = dep.slice(0, -3); 33 | } 34 | // browser replacement; 35 | if (replacement && replacement[dep]) { 36 | dep = replacement[dep]; 37 | } 38 | 39 | if (node.value !== dep) { 40 | toReplace.push({ 41 | start: node.range[0], 42 | end: node.range[1], 43 | text: `'${dep}'` 44 | }); 45 | } 46 | }; 47 | 48 | // need node location 49 | const parsed = meriyah.parseScript(contents, {ranges: true, next: true, webcompat: true}); 50 | 51 | if (isUMD(parsed) || isUMD2(parsed)) { 52 | // Skip lib in umd format, because browersify umd build could 53 | // use require('./file.js') which we should not strip .js 54 | return contents; 55 | } 56 | 57 | const amdMatch = amdDep(parsed); 58 | if (amdMatch) { 59 | amdMatch.forEach(result => { 60 | result.match.deps.forEach(_find); 61 | }); 62 | } 63 | 64 | const cjsMatch = cjsDep(parsed); 65 | if (cjsMatch) { 66 | cjsMatch.forEach(result => { 67 | _find(result.match.dep); 68 | }); 69 | } 70 | 71 | // reverse sort by "start" 72 | toReplace.sort((a, b) => b.start - a.start); 73 | 74 | toReplace.forEach(r => { 75 | contents = modify(contents, r); 76 | }); 77 | 78 | return contents; 79 | }; 80 | }; 81 | 82 | function modify(contents, replacement) { 83 | return contents.slice(0, replacement.start) + 84 | replacement.text + 85 | contents.slice(replacement.end); 86 | } 87 | 88 | function isPackageName(path) { 89 | if (path.startsWith('.')) return false; 90 | const parts = path.split('/'); 91 | // package name, or scope package name 92 | return parts.length === 1 || (parts.length === 2 && parts[0].startsWith('@')); 93 | } 94 | -------------------------------------------------------------------------------- /lib/build/amodro-trace/write/stubs.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Replaces module content for a given set of module IDs with stub define calls. 3 | * @param {Object} options object for holding options. Supported options: 4 | * - stubModules: Array of module IDs to place in stubs. 5 | * @return {Function} A function that can be used for multiple content transform 6 | * calls. 7 | */ 8 | module.exports = function stubs(options) { 9 | options = options || {}; 10 | 11 | return function(context, moduleName, filePath, contents) { 12 | if (options.stubModules 13 | && (options.stubModules.indexOf(moduleName) !== -1 14 | || options.stubModules.indexOf(context.pkgsMainMap[moduleName]) !== -1)) { 15 | //Just want to insert a simple module definition instead 16 | //of the source module. Useful for plugins that inline 17 | //all their resources. 18 | //Slightly different content for plugins, to indicate 19 | //that dynamic loading will not work. 20 | return 'define({load: function(id){' + 21 | 'throw new Error("Dynamic load not allowed: " + id);}});'; 22 | } else { 23 | return contents; 24 | } 25 | }; 26 | 27 | }; 28 | -------------------------------------------------------------------------------- /lib/build/ast-matcher.js: -------------------------------------------------------------------------------- 1 | const meriyah = require('meriyah'); 2 | 3 | const STOP = false; 4 | const SKIP_BRANCH = 1; 5 | 6 | // ignore position info, and raw 7 | const IGNORED_KEYS = ['start', 'end', 'loc', 'location', 'locations', 'line', 'column', 'range', 'ranges', 'raw']; 8 | 9 | // From an meriyah example for traversing its ast. 10 | // modified to support branch skip. 11 | function traverse(object, visitor) { 12 | let child; 13 | if (!object) return; 14 | 15 | let r = visitor.call(null, object); 16 | if (r === STOP) return STOP; // stop whole traverse immediately 17 | if (r === SKIP_BRANCH) return; // skip going into AST branch 18 | 19 | for (let i = 0, keys = Object.keys(object); i < keys.length; i++) { 20 | let key = keys[i]; 21 | if (IGNORED_KEYS.indexOf(key) !== -1) continue; 22 | 23 | child = object[key]; 24 | if (typeof child === 'object' && child !== null) { 25 | if (traverse(child, visitor) === STOP) { 26 | return STOP; 27 | } 28 | } 29 | } 30 | } 31 | 32 | const ANY = 1; 33 | const ANL = 2; 34 | const STR = 3; 35 | const ARR = 4; 36 | 37 | function matchTerm(pattern) { 38 | let possible; 39 | if (pattern.type === 'Identifier') { 40 | possible = pattern.name.toString(); 41 | } else if (pattern.type === 'ExpressionStatement' && 42 | pattern.expression.type === 'Identifier') { 43 | possible = pattern.expression.name.toString(); 44 | } 45 | 46 | if (!possible || !possible.startsWith('__')) return; 47 | 48 | let type; 49 | if (possible === '__any' || possible.startsWith('__any_')) { 50 | type = ANY; 51 | } else if (possible === '__anl' || possible.startsWith('__anl_')) { 52 | type = ANL; 53 | } else if (possible === '__str' || possible.startsWith('__str_')) { 54 | type = STR; 55 | } else if (possible === '__arr' || possible.startsWith('__arr_')) { 56 | type = ARR; 57 | } 58 | 59 | if (type) return {type: type, name: possible.slice(6)}; 60 | } 61 | 62 | /** 63 | * Extract info from a partial meriyah syntax tree, see astMatcher for pattern format 64 | * @param pattern The pattern used on matching 65 | * @param part The target partial syntax tree 66 | * @return Returns named matches, or false. 67 | */ 68 | exports.extract = function(pattern, part) { 69 | if (!pattern) throw new Error('missing pattern'); 70 | // no match 71 | if (!part) return STOP; 72 | 73 | let term = matchTerm(pattern); 74 | if (term) { 75 | // if single __any 76 | if (term.type === ANY) { 77 | if (term.name) { 78 | // if __any_foo 79 | // get result {foo: astNode} 80 | let r = {}; 81 | r[term.name] = part; 82 | return r; 83 | } 84 | // always match 85 | return {}; 86 | 87 | // if single __str_foo 88 | } else if (term.type === STR) { 89 | if (part.type === 'Literal') { 90 | if (term.name) { 91 | // get result {foo: value} 92 | let r = {}; 93 | r[term.name] = part.value; 94 | return r; 95 | } 96 | // always match 97 | return {}; 98 | } 99 | // no match 100 | return STOP; 101 | } 102 | } 103 | 104 | 105 | if (Array.isArray(pattern)) { 106 | // no match 107 | if (!Array.isArray(part)) return STOP; 108 | 109 | if (pattern.length === 1) { 110 | let arrTerm = matchTerm(pattern[0]); 111 | if (arrTerm) { 112 | // if single __arr_foo 113 | if (arrTerm.type === ARR) { 114 | // find all or partial Literals in an array 115 | let arr = part.filter(it => it.type === 'Literal').map(it => it.value); 116 | if (arr.length) { 117 | if (arrTerm.name) { 118 | // get result {foo: array} 119 | let r = {}; 120 | r[arrTerm.name] = arr; 121 | return r; 122 | } 123 | // always match 124 | return {}; 125 | } 126 | // no match 127 | return STOP; 128 | } else if (arrTerm.type === ANL) { 129 | if (arrTerm.name) { 130 | // get result {foo: nodes array} 131 | let r = {}; 132 | r[arrTerm.name] = part; 133 | return r; 134 | } 135 | 136 | // always match 137 | return {}; 138 | } 139 | } 140 | } 141 | 142 | if (pattern.length !== part.length) { 143 | // no match 144 | return STOP; 145 | } 146 | } 147 | 148 | let allResult = {}; 149 | 150 | for (let i = 0, keys = Object.keys(pattern); i < keys.length; i++) { 151 | let key = keys[i]; 152 | if (IGNORED_KEYS.indexOf(key) !== -1) continue; 153 | 154 | let nextPattern = pattern[key]; 155 | let nextPart = part[key]; 156 | 157 | if (!nextPattern || typeof nextPattern !== 'object') { 158 | // primitive value. string or null 159 | if (nextPattern === nextPart) continue; 160 | 161 | // no match 162 | return STOP; 163 | } 164 | 165 | const result = exports.extract(nextPattern, nextPart); 166 | // no match 167 | if (result === STOP) return STOP; 168 | if (result) Object.assign(allResult, result); 169 | } 170 | 171 | return allResult; 172 | }; 173 | 174 | /** 175 | * Compile a pattern into meriyah syntax tree 176 | * @param pattern The pattern used on matching, can be a string or meriyah node 177 | * @return Returns an meriyah node to be used as pattern in extract(pattern, part) 178 | */ 179 | exports.compilePattern = function(pattern) { 180 | // pass meriyah syntax tree obj 181 | if (pattern && pattern.type) return pattern; 182 | 183 | if (typeof pattern !== 'string') { 184 | throw new Error('input pattern is neither a string nor an meriyah node.'); 185 | } 186 | 187 | let exp = meriyah.parseScript(pattern, {next: true, webcompat: true}); 188 | 189 | if (exp.type !== 'Program' || !exp.body) { 190 | throw new Error(`Not a valid expression: "${pattern}".`); 191 | } 192 | 193 | if (exp.body.length === 0) { 194 | throw new Error(`There is no statement in pattern "${pattern}".`); 195 | } 196 | 197 | if (exp.body.length > 1) { 198 | throw new Error(`Multiple statements is not supported "${pattern}".`); 199 | } 200 | 201 | exp = exp.body[0]; 202 | // get the real expression underneath 203 | if (exp.type === 'ExpressionStatement') exp = exp.expression; 204 | return exp; 205 | }; 206 | 207 | function ensureParsed(codeOrNode) { 208 | // bypass parsed node 209 | if (codeOrNode && codeOrNode.type) return codeOrNode; 210 | return meriyah.parseScript(codeOrNode, {next: true, webcompat: true}); 211 | } 212 | 213 | /** 214 | * Pattern matching using AST on JavaScript source code 215 | * @param pattern The pattern to be matched 216 | * @return Returns a function that takes source code string (or meriyah syntax tree) as input, produces matched result or undefined. 217 | * 218 | * __any matches any single node, but no extract 219 | * __anl matches array of nodes, but no extract 220 | * __str matches string literal, but no extract 221 | * __arr matches array of partial string literals, but no extract 222 | * __any_aName matches single node, return {aName: node} 223 | * __anl_aName matches array of nodes, return {aName: array_of_nodes} 224 | * __str_aName matches string literal, return {aName: value} 225 | * __arr_aName matches array, extract string literals, return {aName: [values]} 226 | * 227 | * note: __arr_aName can match partial array 228 | * [foo, "foo", lorem, "bar", lorem] => ["foo", "bar"] 229 | * 230 | * note: __anl, and __arr_* 231 | * use method(__anl) or method(__arr_a) to match method(a, "b"); 232 | * use method([__anl]) or method([__arr_a]) to match method([a, "b"]); 233 | * 234 | * Usage: 235 | * let m = astMatcher('__any.method(__str_foo, [__arr_opts])'); 236 | * m('au.method("a", ["b", "c"]); jq.method("d", ["e"])'); 237 | * 238 | * => [ 239 | * {match: {foo: "a", opts: ["b", "c"]}, node: } 240 | * {match: {foo: "d", opts: ["e"]}, node: } 241 | * ] 242 | */ 243 | exports.astMatcher = function(pattern) { 244 | let pat = exports.compilePattern(pattern); 245 | 246 | return function(jsStr) { 247 | let node = ensureParsed(jsStr); 248 | let matches = []; 249 | 250 | traverse(node, n => { 251 | let m = exports.extract(pat, n); 252 | if (m) { 253 | matches.push({ 254 | match: m, 255 | node: n // this is the full matching node 256 | }); 257 | // found a match, don't go deeper on this tree branch 258 | // return SKIP_BRANCH; 259 | // don't skip branch in order to catch both .m1() ad .m2() 260 | // astMater('__any.__any_m()')('a.m1().m2()') 261 | } 262 | }); 263 | 264 | return matches.length ? matches : undefined; 265 | }; 266 | }; 267 | 268 | /** 269 | * Dependency search for dummies, this is a high level api to simplify the usage of astMatcher 270 | * @param arguments Multiple patterns to match, instead of using __str_/__arr_, use __dep and __deps to match string or partial string array. 271 | * @return Returns a function that takes source code string (or meriyah syntax tree) as input, produces an array of string matched, or empty array. 272 | * 273 | * Usage: 274 | * let f = jsDepFinder('a(__dep)', '__any.globalResources([__deps])'); 275 | * f('a("a"); a("b"); config.globalResources(["./c", "./d"])'); 276 | * 277 | * => ['a', 'b', './c', './d'] 278 | */ 279 | exports.jsDepFinder = function() { 280 | if (arguments.length === 0) { 281 | throw new Error('No patterns provided.'); 282 | } 283 | 284 | let seed = 0; 285 | 286 | let patterns = Array.prototype.map.call(arguments, p => 287 | // replace __dep and __deps into 288 | // __str_1, __str_2, __arr_3 289 | // wantArr is the result of (s?) 290 | exports.compilePattern(p.replace(/__dep(s?)/g, (m, wantArr) => 291 | (wantArr ? '__arr_' : '__str_') + (++seed) 292 | )) 293 | ); 294 | 295 | let len = patterns.length; 296 | 297 | return function(jsStr) { 298 | let node = ensureParsed(jsStr); 299 | 300 | let deps = []; 301 | 302 | // directly use extract() instead of astMatcher() 303 | // for efficiency 304 | traverse(node, n => { 305 | for (let i = 0; i < len; i += 1) { 306 | let result = exports.extract(patterns[i], n); 307 | if (result) { 308 | // result is like {"1": "dep1", "2": ["dep2", "dep3"]} 309 | // we only want values 310 | Object.keys(result).forEach(k => { 311 | let d = result[k]; 312 | if (typeof d === 'string') deps.push(d); 313 | else deps.push.apply(deps, d); 314 | }); 315 | 316 | // found a match, don't try other pattern 317 | break; 318 | } 319 | } 320 | }); 321 | 322 | return deps; 323 | }; 324 | }; 325 | -------------------------------------------------------------------------------- /lib/build/dependency-description.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('../file-system'); 3 | const Utils = require('./utils'); 4 | 5 | exports.DependencyDescription = class { 6 | constructor(name, source) { 7 | this.name = name; 8 | this.source = source; 9 | } 10 | 11 | get mainId() { 12 | return this.name + '/' + this.loaderConfig.main; 13 | } 14 | 15 | get banner() { 16 | const {metadata, name} = this; 17 | const version = (metadata && metadata.version) || ''; 18 | return `package: ${version}${' '.repeat(version.length < 10 ? (10 - version.length) : 0)} ${name}`; 19 | } 20 | 21 | calculateMainPath(root) { 22 | let config = this.loaderConfig; 23 | let part = path.join(config.path, config.main); 24 | 25 | let ext = path.extname(part).toLowerCase(); 26 | if (!ext || Utils.knownExtensions.indexOf(ext) === -1) { 27 | part = part + '.js'; 28 | } 29 | 30 | return path.join(process.cwd(), root, part); 31 | } 32 | 33 | readMainFileSync(root) { 34 | let p = this.calculateMainPath(root); 35 | 36 | try { 37 | return fs.readFileSync(p).toString(); 38 | } catch { 39 | console.log('error', p); 40 | return ''; 41 | } 42 | } 43 | 44 | // https://github.com/defunctzombie/package-browser-field-spec 45 | browserReplacement() { 46 | const browser = this.metadata && this.metadata.browser; 47 | // string browser field is handled in package-analyzer 48 | if (!browser || typeof browser === 'string') return; 49 | 50 | let replacement = {}; 51 | 52 | for (let i = 0, keys = Object.keys(browser); i < keys.length; i++) { 53 | let key = keys[i]; 54 | // leave {".": "dist/index.js"} for main replacement 55 | if (key === '.') continue; 56 | let target = browser[key]; 57 | 58 | let sourceModule = filePathToModuleId(key); 59 | 60 | if (key.startsWith('.')) { 61 | sourceModule = './' + sourceModule; 62 | } 63 | 64 | if (typeof target === 'string') { 65 | let targetModule = filePathToModuleId(target); 66 | if (!targetModule.startsWith('.')) { 67 | targetModule = './' + targetModule; 68 | } 69 | replacement[sourceModule] = targetModule; 70 | } else { 71 | replacement[sourceModule] = false; 72 | } 73 | } 74 | 75 | return replacement; 76 | } 77 | }; 78 | 79 | function filePathToModuleId(filePath) { 80 | let moduleId = path.normalize(filePath).replace(/\\/g, '/'); 81 | 82 | if (moduleId.toLowerCase().endsWith('.js')) { 83 | moduleId = moduleId.slice(0, -3); 84 | } 85 | 86 | return moduleId; 87 | } 88 | -------------------------------------------------------------------------------- /lib/build/dependency-inclusion.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const SourceInclusion = require('./source-inclusion').SourceInclusion; 3 | const { minimatch } = require('minimatch'); 4 | const Utils = require('./utils'); 5 | const logger = require('aurelia-logging').getLogger('DependencyInclusion'); 6 | 7 | const knownNonJsExtensions = ['.json', '.css', '.svg', '.html']; 8 | 9 | exports.DependencyInclusion = class { 10 | constructor(bundle, description) { 11 | this.bundle = bundle; 12 | this.description = description; 13 | this.mainTraced = false; 14 | } 15 | 16 | traceMain() { 17 | if (this.mainTraced) return Promise.resolve(); 18 | 19 | this.mainTraced = true; 20 | let mainId = this.description.mainId; 21 | let ext = path.extname(mainId).toLowerCase(); 22 | let mainIsJs = !ext || knownNonJsExtensions.indexOf(ext) === -1; 23 | 24 | if (mainIsJs || ext === path.extname(this.description.name).toLowerCase()) { 25 | // only create alias when main is js file 26 | // or package name shares same extension (like normalize.css with main file normalize.css) 27 | this.bundle.addAlias(this.description.name, mainId); 28 | } 29 | 30 | let main = this.description.loaderConfig.main; 31 | if (mainIsJs && Utils.knownExtensions.indexOf(ext) === -1) { 32 | main += '.js'; 33 | } 34 | 35 | return this._tracePattern(main); 36 | } 37 | 38 | traceResources() { 39 | let work = Promise.resolve(); 40 | // when user import from 'lodash/map', 41 | // only bundle node_modules/lodash/map.js, 42 | // without bundle node_modules/lodash/lodash.js, 43 | // which in addition trace and bundle everything. 44 | if (!this.description.loaderConfig.lazyMain) { 45 | work = work.then(() => this.traceMain()); 46 | } 47 | 48 | let loaderConfig = this.description.loaderConfig; 49 | let resources = loaderConfig.resources; 50 | 51 | if (resources) { 52 | resources.forEach(x => { 53 | work = work.then(() => this._tracePattern(x)); 54 | }); 55 | } 56 | 57 | return work; 58 | } 59 | 60 | traceResource(resource) { 61 | let resolved = resolvedResource(resource, this.description, this._getProjectRoot()); 62 | 63 | if (!resolved) { 64 | logger.error(`Error: could not find "${resource}" in package ${this.description.name}`); 65 | return Promise.resolve(); 66 | } 67 | 68 | if (Utils.removeJsExtension(resolved) !== Utils.removeJsExtension(resource)) { 69 | // alias bootstrap/css/bootstrap.css to bootstrap/lib/css/bootstrap.css 70 | this.bundle.addAlias( 71 | this.description.name + '/' + Utils.removeJsExtension(resource), 72 | this.description.name + '/' + Utils.removeJsExtension(resolved) 73 | ); 74 | } 75 | 76 | let covered = this.bundle.includes.find(inclusion => 77 | inclusion.includedBy === this && 78 | minimatch(resolved, inclusion.pattern) 79 | ); 80 | 81 | if (covered) { 82 | return Promise.resolve(); 83 | } 84 | 85 | return this._tracePattern(resolved); 86 | } 87 | 88 | _tracePattern(resource) { 89 | let loaderConfig = this.description.loaderConfig; 90 | let bundle = this.bundle; 91 | let pattern = path.join(loaderConfig.path, resource); 92 | let inclusion = new SourceInclusion(bundle, pattern, this); 93 | let promise = inclusion.addAllMatchingResources(); 94 | bundle.includes.push(inclusion); 95 | bundle.requiresBuild = true; 96 | return promise; 97 | } 98 | 99 | // If all resources has same prefix like dist/type, 100 | // create conventional aliases like: 101 | // define('package/foo/bar', ['package/dist/type/foo/bar'], function(m) {return m;}); 102 | conventionalAliases() { 103 | let ids = []; 104 | this.bundle.includes.forEach(inclusion => { 105 | if (inclusion.includedBy === this) { 106 | ids.push.apply(ids, inclusion.getAllModuleIds()); 107 | } 108 | }); 109 | 110 | if (ids.length < 2) return {}; 111 | 112 | let nameLength = this.description.name.length; 113 | 114 | let commonLen = commonLength(ids); 115 | if (!commonLen || commonLen <= nameLength + 1 ) return {}; 116 | 117 | let aliases = {}; 118 | ids.forEach(id => { 119 | // for aurelia-templating-resources/dist/commonjs/if 120 | // compact name is aurelia-templating-resources/if 121 | let compactResource = id.slice(commonLen); 122 | if (compactResource) { 123 | let compactId = this.description.name + '/' + compactResource; 124 | aliases[compactId] = id; 125 | } 126 | }); 127 | 128 | return aliases; 129 | } 130 | 131 | trySubsume() { 132 | return false; 133 | } 134 | 135 | getAllModuleIds() { 136 | // placeholder 137 | // all module ids are provided by source inclusion, including main module 138 | return []; 139 | } 140 | 141 | getAllFiles() { 142 | return []; // return this.items; 143 | } 144 | 145 | _getProjectRoot() { 146 | return this.bundle.bundler.project.paths.root; 147 | } 148 | }; 149 | 150 | function resolvedResource(resource, description, projectRoot) { 151 | const base = path.resolve(projectRoot, description.loaderConfig.path); 152 | let mainShift = description.loaderConfig.main.split('/'); 153 | 154 | // when mainShift is [dist,commonjs] 155 | // try dist/commonjs/resource first 156 | // then dist/resource 157 | // then resource 158 | let resolved; 159 | 160 | do { 161 | mainShift.pop(); 162 | let res; 163 | if (mainShift.length) { 164 | res = mainShift.join('/') + '/' + resource; 165 | } else { 166 | res = resource; 167 | } 168 | 169 | resolved = validResource(res, base); 170 | if (resolved) break; 171 | } while (mainShift.length); 172 | 173 | return resolved; 174 | } 175 | 176 | function validResource(resource, base) { 177 | const resourcePath = path.resolve(base, resource); 178 | const loaded = Utils.nodejsLoad(resourcePath); 179 | if (loaded) return path.relative(base, loaded).replace(/\\/g, '/'); 180 | } 181 | 182 | function commonLength(ids) { 183 | let parts = ids[0].split('/'); 184 | let rest = ids.slice(1); 185 | parts.pop(); // ignore last part 186 | 187 | let common = ''; 188 | 189 | for (let i = 0, len = parts.length; i < len; i++) { 190 | let all = common + parts[i] + '/'; 191 | if (rest.every(id => id.startsWith(all))) { 192 | common = all; 193 | } else { 194 | break; 195 | } 196 | } 197 | 198 | return common.length; 199 | } 200 | -------------------------------------------------------------------------------- /lib/build/index.js: -------------------------------------------------------------------------------- 1 | const {Transform} = require('stream'); 2 | const Bundler = require('./bundler').Bundler; 3 | const PackageAnalyzer = require('./package-analyzer').PackageAnalyzer; 4 | const PackageInstaller = require('./package-installer').PackageInstaller; 5 | const cacheDir = require('./utils').cacheDir; 6 | const fs = require('fs'); 7 | 8 | let bundler; 9 | let project; 10 | let isUpdating = false; 11 | 12 | exports.src = function(p) { 13 | if (bundler) { 14 | isUpdating = true; 15 | return Promise.resolve(bundler); 16 | } 17 | 18 | project = p; 19 | return Bundler.create( 20 | project, 21 | new PackageAnalyzer(project), 22 | new PackageInstaller(project) 23 | ).then(b => bundler = b); 24 | }; 25 | 26 | exports.createLoaderCode = function(p) { 27 | const createLoaderCode = require('./loader').createLoaderCode; 28 | project = p || project; 29 | return buildLoaderConfig(project) 30 | .then(() => { 31 | let platform = project.build.targets[0]; 32 | return createLoaderCode(platform, bundler); 33 | }); 34 | }; 35 | 36 | exports.createLoaderConfig = function(p) { 37 | const createLoaderConfig = require('./loader').createLoaderConfig; 38 | project = p || project; 39 | 40 | return buildLoaderConfig(project) 41 | .then(() => { 42 | let platform = project.build.targets[0]; 43 | return createLoaderConfig(platform, bundler); 44 | }); 45 | }; 46 | 47 | exports.bundle = function() { 48 | return new Transform({ 49 | objectMode: true, 50 | transform: function(file, encoding, callback) { 51 | callback(null, capture(file)); 52 | } 53 | }); 54 | }; 55 | 56 | exports.dest = function(opts) { 57 | return bundler.build(opts) 58 | .then(() => bundler.write()); 59 | }; 60 | 61 | exports.clearCache = function() { 62 | // delete cache folder outside of cwd 63 | return fs.promises.rm(cacheDir, { recursive: true, force: true }); 64 | }; 65 | 66 | function buildLoaderConfig(p) { 67 | project = p || project; 68 | let configPromise = Promise.resolve(); 69 | 70 | if (!bundler) { 71 | //If a bundler doesn't exist then chances are we have not run through getting all the files, and therefore the "bundles" will not be complete 72 | configPromise = configPromise.then(() => { 73 | return Bundler.create( 74 | project, 75 | new PackageAnalyzer(project), 76 | new PackageInstaller(project) 77 | ).then(b => bundler = b); 78 | }); 79 | } 80 | 81 | return configPromise.then(() => { 82 | return bundler.build(); 83 | }); 84 | } 85 | 86 | function capture(file) { 87 | // ignore type declaration file generated by TypeScript compiler 88 | if (file.path.endsWith('d.ts')) return; 89 | 90 | if (isUpdating) { 91 | bundler.updateFile(file); 92 | } else { 93 | bundler.addFile(file); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /lib/build/inject-css.js: -------------------------------------------------------------------------------- 1 | /* global document */ 2 | let cssUrlMatcher = /url\s*\(\s*(?!['"]data)([^) ]+)\s*\)/gi; 3 | 4 | // copied from aurelia-templating-resources css-resource 5 | // This behaves differently from webpack's style-loader. 6 | // Here we change './hello.png' to 'foo/hello.png' if base address is 'foo/bar'. 7 | // Note 'foo/hello.png' is technically a relative path in css, 8 | // this is designed to work with aurelia-router setup. 9 | // We inject css into a style tag on html head, it means the 'foo/hello.png' 10 | // is related to current url (not css url on link tag), or tag in html 11 | // head (which is recommended setup of router if not using hash). 12 | function fixupCSSUrls(address, css) { 13 | if (typeof css !== 'string') { 14 | throw new Error(`Failed loading required CSS file: ${address}`); 15 | } 16 | return css.replace(cssUrlMatcher, (match, p1) => { 17 | let quote = p1.charAt(0); 18 | if (quote === '\'' || quote === '"') { 19 | p1 = p1.substr(1, p1.length - 2); 20 | } 21 | const absolutePath = absoluteModuleId(address, p1); 22 | if (absolutePath === p1) { 23 | return match; 24 | } 25 | return 'url(\'' + absolutePath + '\')'; 26 | }); 27 | } 28 | 29 | function absoluteModuleId(baseId, moduleId) { 30 | if (moduleId[0] !== '.') return moduleId; 31 | 32 | let parts = baseId.split('/'); 33 | parts.pop(); 34 | 35 | moduleId.split('/').forEach(p => { 36 | if (p === '.') return; 37 | if (p === '..') { 38 | parts.pop(); 39 | return; 40 | } 41 | parts.push(p); 42 | }); 43 | 44 | return parts.join('/'); 45 | } 46 | 47 | // copied from aurelia-pal-browser DOM.injectStyles 48 | function injectCSS(css, id) { 49 | if (typeof document === 'undefined' || !css) return; 50 | css = fixupCSSUrls(id, css); 51 | 52 | if (id) { 53 | let oldStyle = document.getElementById(id); 54 | if (oldStyle) { 55 | let isStyleTag = oldStyle.tagName.toLowerCase() === 'style'; 56 | 57 | if (isStyleTag) { 58 | oldStyle.innerHTML = css; 59 | return; 60 | } 61 | 62 | throw new Error('The provided id does not indicate a style tag.'); 63 | } 64 | } 65 | 66 | let node = document.createElement('style'); 67 | node.innerHTML = css; 68 | node.type = 'text/css'; 69 | 70 | if (id) { 71 | node.id = id; 72 | } 73 | 74 | document.head.appendChild(node); 75 | } 76 | 77 | injectCSS.fixupCSSUrls = fixupCSSUrls; 78 | 79 | module.exports = injectCSS; 80 | -------------------------------------------------------------------------------- /lib/build/loader-plugin.js: -------------------------------------------------------------------------------- 1 | const {moduleIdWithPlugin} = require('./utils'); 2 | 3 | exports.LoaderPlugin = class { 4 | constructor(type, config) { 5 | this.type = type; 6 | this.config = config; 7 | this.name = config.name; 8 | this.stub = config.stub; 9 | this.test = config.test ? new RegExp(config.test) : regExpFromExtensions(config.extensions); 10 | } 11 | 12 | matches(filePath) { 13 | return this.test.test(filePath); 14 | } 15 | 16 | transform(moduleId, filePath, contents) { 17 | contents = `define('${this.createModuleId(moduleId)}',[],function(){return ${JSON.stringify(contents)};});`; 18 | return contents; 19 | } 20 | 21 | createModuleId(moduleId) { 22 | // for backward compatibility, use 'text' as plugin name, 23 | // to not break existing app with additional json plugin in aurelia.json 24 | return moduleIdWithPlugin(moduleId, 'text', this.type); 25 | } 26 | }; 27 | 28 | function regExpFromExtensions(extensions) { 29 | return new RegExp('^.*(' + extensions.map(x => '\\' + x).join('|') + ')$'); 30 | } 31 | 32 | -------------------------------------------------------------------------------- /lib/build/loader.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const Configuration = require('../configuration').Configuration; 4 | 5 | exports.createLoaderCode = function createLoaderCode(platform, bundler) { 6 | let loaderCode; 7 | let loaderOptions = bundler.loaderOptions; 8 | 9 | switch (loaderOptions.type) { 10 | case 'require': 11 | loaderCode = 'requirejs.config(' + JSON.stringify(exports.createRequireJSConfig(platform, bundler), null, 2) + ')'; 12 | break; 13 | case 'system': 14 | loaderCode = 'window.define=SystemJS.amdDefine; window.require=window.requirejs=SystemJS.amdRequire; SystemJS.config(' + JSON.stringify(exports.createSystemJSConfig(platform, bundler), null, 2) + ');'; 15 | break; 16 | default: 17 | //TODO: Enhancement: Look at a designated folder for any custom configurations 18 | throw new Error(`Loader configuration style ${loaderOptions.type} is not supported.`); 19 | } 20 | 21 | return loaderCode; 22 | }; 23 | 24 | exports.createLoaderConfig = function createLoaderConfig(platform, bundler) { 25 | let loaderConfig; 26 | let loaderOptions = bundler.loaderOptions; 27 | 28 | switch (loaderOptions.type) { 29 | case 'require': 30 | loaderConfig = exports.createRequireJSConfig(platform, bundler); 31 | break; 32 | case 'system': 33 | loaderConfig = exports.createSystemJSConfig(platform); 34 | break; 35 | default: 36 | //TODO: Enhancement: Look at a designated folder for any custom configurations 37 | throw new Error(`Loader configuration style ${loaderOptions.type} is not supported.`); 38 | } 39 | 40 | return loaderConfig; 41 | }; 42 | 43 | exports.createRequireJSConfig = function createRequireJSConfig(platform, bundler) { 44 | let loaderOptions = bundler.loaderOptions; 45 | let loaderConfig = bundler.loaderConfig; 46 | let bundles = bundler.bundles; 47 | let configName = loaderOptions.configTarget; 48 | let bundleMetadata = {}; 49 | let includeBundles = shouldIncludeBundleMetadata(bundles, loaderOptions); 50 | let config = Object.assign({}, loaderConfig); 51 | let location = platform.baseUrl || platform.output; 52 | 53 | if (platform.useAbsolutePath) { 54 | location = platform.baseUrl ? location : '/' + location; 55 | } else { 56 | location = '../' + location; 57 | } 58 | 59 | for (let i = 0; i < bundles.length; ++i) { 60 | let currentBundle = bundles[i]; 61 | let currentName = currentBundle.config.name; 62 | let buildOptions = new Configuration(currentBundle.config.options, bundler.buildOptions.getAllOptions()); 63 | if (currentName === configName) { //skip over the vendor bundle 64 | continue; 65 | } 66 | 67 | if (includeBundles) { 68 | bundleMetadata[currentBundle.moduleId] = currentBundle.getBundledModuleIds(); 69 | } 70 | //If build revisions are enabled, append the revision hash to the appropriate module id 71 | config.paths[currentBundle.moduleId] = location + '/' + currentBundle.moduleId + (buildOptions.isApplicable('rev') && currentBundle.hash ? '-' + currentBundle.hash : ''); 72 | } 73 | 74 | if (includeBundles) { 75 | config.bundles = bundleMetadata; 76 | } 77 | 78 | return config; 79 | }; 80 | 81 | exports.createSystemJSConfig = function createSystemJSConfig(platform, bundler) { 82 | const loaderOptions = bundler.loaderOptions; 83 | const bundles = bundler.bundles; 84 | const configBundleName = loaderOptions.configTarget; 85 | const includeBundles = shouldIncludeBundleMetadata(bundles, loaderOptions); 86 | const location = platform.baseUrl || platform.output; 87 | const systemConfig = Object.assign({}, loaderOptions.config); 88 | 89 | const bundlesConfig = bundles.map(bundle => systemJSConfigForBundle(bundle, bundler, location, includeBundles)) 90 | .filter(bundle => bundle.name !== configBundleName) 91 | .reduce((c, bundle) => bundle.addBundleConfig(c), { map: { 'text': 'text' } }); 92 | 93 | return Object.assign(systemConfig, bundlesConfig); 94 | }; 95 | 96 | function shouldIncludeBundleMetadata(bundles, loaderOptions) { 97 | let setting = loaderOptions.includeBundleMetadataInConfig; 98 | 99 | if (typeof setting === 'string') { 100 | switch (setting.toLowerCase()) { 101 | case 'auto': 102 | return bundles.length > 1; 103 | case 'true': 104 | return true; 105 | default: 106 | return false; 107 | } 108 | } 109 | 110 | return setting === true; 111 | } 112 | 113 | function systemJSConfigForBundle(bundle, bundler, location, includeBundles) { 114 | const buildOptions = new Configuration(bundle.config.options, bundler.buildOptions.getAllOptions()); 115 | const mapTarget = location + '/' + bundle.moduleId + (buildOptions.isApplicable('rev') && bundle.hash ? '-' + bundle.hash : '') + path.extname(bundle.config.name); 116 | const moduleId = bundle.moduleId; 117 | const bundledModuleIds = bundle.getBundledModuleIds(); 118 | 119 | return { 120 | name: bundle.config.name, 121 | addBundleConfig: function(config) { 122 | config.map[moduleId] = mapTarget; 123 | if (includeBundles) { 124 | config.bundles = (config.bundles || {}); 125 | config.bundles[moduleId] = bundledModuleIds; 126 | } 127 | 128 | return config; 129 | } 130 | }; 131 | } 132 | -------------------------------------------------------------------------------- /lib/build/module-id-processor.js: -------------------------------------------------------------------------------- 1 | // if moduleId is above surface (default src/), the '../../' confuses hell out of 2 | // requirejs as it tried to understand it as a relative module id. 3 | // replace '..' with '__dot_dot__' to enforce absolute module id. 4 | const toDotDot = (moduleId) => moduleId.split('/').map(p => p === '..' ? '__dot_dot__' : p).join('/'); 5 | const fromDotDot = (moduleId) => moduleId.split('/').map(p => p === '__dot_dot__' ? '..' : p).join('/'); 6 | 7 | const getAliases = (moduleId, paths) => { 8 | const aliases = []; 9 | const _moduleId = fromDotDot(moduleId); 10 | for (let i = 0, keys = Object.keys(paths); i < keys.length; i++) { 11 | let key = keys[i]; 12 | let target = paths[key]; 13 | if (key === 'root') continue; 14 | if (key === target) continue; 15 | 16 | if (_moduleId.startsWith(target + '/')) { 17 | aliases.push({ 18 | fromId: toDotDot(key + _moduleId.slice(target.length)), 19 | toId: toDotDot(moduleId) 20 | }); 21 | } 22 | } 23 | 24 | return aliases; 25 | }; 26 | 27 | module.exports = { toDotDot, fromDotDot, getAliases }; 28 | -------------------------------------------------------------------------------- /lib/build/package-analyzer.js: -------------------------------------------------------------------------------- 1 | const fs = require('../file-system'); 2 | const path = require('path'); 3 | const DependencyDescription = require('./dependency-description').DependencyDescription; 4 | const logger = require('aurelia-logging').getLogger('PackageAnalyzer'); 5 | const Utils = require('./utils'); 6 | 7 | exports.PackageAnalyzer = class { 8 | constructor(project) { 9 | this.project = project; 10 | } 11 | 12 | analyze(packageName) { 13 | let description = new DependencyDescription(packageName, 'npm'); 14 | 15 | return loadPackageMetadata(this.project, description) 16 | .then(() => { 17 | if (!description.metadataLocation) { 18 | throw new Error(`Unable to find package metadata (package.json) of ${description.name}`); 19 | } 20 | }) 21 | .then(() => determineLoaderConfig(this.project, description)) 22 | .then(() => description); 23 | } 24 | 25 | reverseEngineer(loaderConfig) { 26 | loaderConfig = JSON.parse(JSON.stringify(loaderConfig)); 27 | let description = new DependencyDescription(loaderConfig.name); 28 | description.loaderConfig = loaderConfig; 29 | 30 | if (!loaderConfig.packageRoot && (!loaderConfig.path || loaderConfig.path.indexOf('node_modules') !== -1)) { 31 | description.source = 'npm'; 32 | } else { 33 | description.source = 'custom'; 34 | if (!loaderConfig.packageRoot) { 35 | fillUpPackageRoot(this.project, description); 36 | } 37 | } 38 | 39 | return loadPackageMetadata(this.project, description) 40 | .then(() => { 41 | if (!loaderConfig.path) { 42 | // fillup main and path 43 | determineLoaderConfig(this.project, description); 44 | } else { 45 | if (!loaderConfig.main) { 46 | if (description.source === 'custom' && loaderConfig.path === loaderConfig.packageRoot) { 47 | // fillup main and path 48 | determineLoaderConfig(this.project, description); 49 | } else { 50 | const fullPath = path.resolve(this.project.paths.root, loaderConfig.path); 51 | if (fullPath === description.location) { 52 | // fillup main and path 53 | determineLoaderConfig(this.project, description); 54 | return; 55 | } 56 | 57 | // break single path into main and dir 58 | let pathParts = path.parse(fullPath); 59 | 60 | // when path is node_modules/package/foo/bar 61 | // set path to node_modules/package 62 | // set main to foo/bar 63 | loaderConfig.path = path.relative(this.project.paths.root, description.location).replace(/\\/g, '/'); 64 | 65 | if (pathParts.dir.length > description.location.length + 1) { 66 | const main = path.join(pathParts.dir.slice(description.location.length + 1), Utils.removeJsExtension(pathParts.base)); 67 | loaderConfig.main = main.replace(/\\/g, '/'); 68 | } else if (pathParts.dir.length === description.location.length) { 69 | loaderConfig.main = Utils.removeJsExtension(pathParts.base).replace(/\\/g, '/'); 70 | } else { 71 | throw new Error(`Path: "${loaderConfig.path}" is not in: ${description.location}`); 72 | } 73 | } 74 | } else { 75 | loaderConfig.main = Utils.removeJsExtension(loaderConfig.main).replace(/\\/g, '/'); 76 | } 77 | } 78 | }) 79 | .then(() => description); 80 | } 81 | }; 82 | 83 | function fillUpPackageRoot(project, description) { 84 | let _path = description.loaderConfig.path; 85 | 86 | let ext = path.extname(_path).toLowerCase(); 87 | if (!ext || Utils.knownExtensions.indexOf(ext) === -1) { 88 | // main file could be non-js file like css/font-awesome.css 89 | _path += '.js'; 90 | } 91 | 92 | if (fs.isFile(path.resolve(project.paths.root, _path))) { 93 | description.loaderConfig.packageRoot = path.dirname(description.loaderConfig.path).replace(/\\/g, '/'); 94 | } 95 | 96 | if (!description.loaderConfig.packageRoot) { 97 | description.loaderConfig.packageRoot = description.loaderConfig.path; 98 | } 99 | } 100 | 101 | function loadPackageMetadata(project, description) { 102 | return setLocation(project, description) 103 | .then(() => { 104 | if (description.metadataLocation) { 105 | return fs.readFile(description.metadataLocation).then(data => { 106 | description.metadata = JSON.parse(data.toString()); 107 | }); 108 | } 109 | }) 110 | .catch(e => { 111 | logger.error(`Unable to load package metadata (package.json) of ${description.name}:`); 112 | logger.info(e); 113 | }); 114 | } 115 | 116 | // loaderConfig.path is simplified when use didn't provide explicit config. 117 | // In auto traced nodejs package, loaderConfig.path always matches description.location. 118 | // We then use auto-generated moduleId aliases in dependency-inclusion to make AMD 119 | // module system happy. 120 | function determineLoaderConfig(project, description) { 121 | let location = path.resolve(description.location); 122 | let mainPath = Utils.nodejsLoad(location); 123 | 124 | if (!description.loaderConfig) { 125 | description.loaderConfig = {name: description.name}; 126 | } 127 | 128 | description.loaderConfig.path = path.relative(project.paths.root, description.location).replace(/\\/g, '/'); 129 | 130 | if (mainPath) { 131 | description.loaderConfig.main = Utils.removeJsExtension(mainPath.slice(location.length + 1).replace(/\\/g, '/')); 132 | } else { 133 | logger.warn(`The "${description.name}" package has no valid main file, fall back to index.js.`); 134 | description.loaderConfig.main = 'index'; 135 | } 136 | } 137 | 138 | function setLocation(project, description) { 139 | switch (description.source) { 140 | case 'npm': 141 | return getPackageFolder(project, description) 142 | .then(packageFolder => { 143 | description.location = packageFolder; 144 | 145 | return tryFindMetadata(project, description); 146 | }); 147 | case 'custom': 148 | description.location = path.resolve(project.paths.root, description.loaderConfig.packageRoot); 149 | 150 | return tryFindMetadata(project, description); 151 | default: 152 | return Promise.reject(`The package source "${description.source}" is not supported.`); 153 | } 154 | } 155 | 156 | function tryFindMetadata(project, description) { 157 | return fs.stat(path.join(description.location, 'package.json')) 158 | .then(() => description.metadataLocation = path.join(description.location, 'package.json')) 159 | .catch(() => {}); 160 | } 161 | 162 | function getPackageFolder(project, description) { 163 | if (!description.loaderConfig || !description.loaderConfig.path) { 164 | return new Promise(resolve => { 165 | resolve(Utils.resolvePackagePath(description.name)); 166 | }); 167 | } 168 | 169 | return lookupPackageFolderRelativeStrategy(project.paths.root, description.loaderConfig.path); 170 | } 171 | 172 | // Looks for the node_modules folder from the root path of aurelia 173 | // with the defined loaderConfig. 174 | function lookupPackageFolderRelativeStrategy(root, relativePath) { 175 | let pathParts = relativePath.replace(/\\/g, '/').split('/'); 176 | let packageFolder = ''; 177 | let stopOnNext = false; 178 | 179 | for (let i = 0; i < pathParts.length; ++i) { 180 | let part = pathParts[i]; 181 | 182 | packageFolder = path.join(packageFolder, part); 183 | 184 | if (stopOnNext && !part.startsWith('@')) { 185 | break; 186 | } else if (part === 'node_modules') { 187 | stopOnNext = true; 188 | } 189 | } 190 | 191 | return Promise.resolve(path.resolve(root, packageFolder)); 192 | } 193 | -------------------------------------------------------------------------------- /lib/build/package-installer.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('../file-system'); 3 | const logger = require('aurelia-logging').getLogger('Package-installer'); 4 | 5 | exports.PackageInstaller = class { 6 | constructor(project) { 7 | this.project = project; 8 | } 9 | 10 | determinePackageManager() { 11 | if (this._packageManager) return this._packageManager; 12 | 13 | let packageManager = this.project.packageManager; 14 | 15 | if (!packageManager && fs.existsSync(path.resolve(process.cwd(), './yarn.lock'))) { 16 | // Have to make best guess on yarn. 17 | // If user is using yarn, then we use npm to install package, 18 | // it will highly likely end in error. 19 | packageManager = 'yarn'; 20 | } 21 | 22 | if (!packageManager) { 23 | packageManager = 'npm'; 24 | } 25 | 26 | this._packageManager = packageManager; 27 | return packageManager; 28 | } 29 | 30 | install(packages) { 31 | let packageManager = this.determinePackageManager(); 32 | let Ctor; 33 | 34 | logger.info(`Using '${packageManager}' to install the package(s). You can change this by setting the 'packageManager' property in the aurelia.json file to 'npm' or 'yarn'.`); 35 | 36 | try { 37 | Ctor = require(`../package-managers/${packageManager}`).default; 38 | } catch (e) { 39 | logger.error(`Could not load the ${packageManager} package installer. Falling back to NPM`, e); 40 | 41 | packageManager = 'npm'; 42 | Ctor = require(`../package-managers/${packageManager}`).default; 43 | } 44 | 45 | let installer = new Ctor(); 46 | 47 | logger.info(`[${packageManager}] installing ${packages}. It would take a while.`); 48 | 49 | return installer.install(packages); 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /lib/build/source-inclusion.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const mapStream = require('map-stream'); 3 | 4 | exports.SourceInclusion = class { 5 | constructor(bundle, pattern, includedBy) { 6 | this.bundle = bundle; 7 | this.orignalPattern = pattern; 8 | // source-inclusion could be included by a dependency-inclusion 9 | this.includedBy = includedBy; 10 | 11 | if (pattern[0] === '[' && pattern[pattern.length - 1] === ']') { 12 | // strip "[**/*.js]" into "**/*.js" 13 | // this format is obsolete, but kept for backwards compatibility 14 | pattern = pattern.slice(1, -1); 15 | } 16 | 17 | this.pattern = pattern; 18 | this.matcher = this.bundle.createMatcher(pattern); 19 | this.excludes = this.bundle.excludes; 20 | this.items = []; 21 | 22 | this.vfs = require('vinyl-fs'); 23 | } 24 | 25 | addItem(item) { 26 | item.includedBy = this; 27 | item.includedIn = this.bundle; 28 | this.items.push(item); 29 | } 30 | 31 | _isExcluded(item) { 32 | let found = this.excludes.findIndex(exclusion => { 33 | return exclusion.match(item.path); 34 | }); 35 | return found > -1; 36 | } 37 | 38 | trySubsume(item) { 39 | if (this.matcher.match(item.path) && !this._isExcluded(item)) { 40 | this.addItem(item); 41 | return true; 42 | } 43 | 44 | return false; 45 | } 46 | 47 | addAllMatchingResources() { 48 | return new Promise((resolve, reject) => { 49 | let bundler = this.bundle.bundler; 50 | let pattern = path.resolve(this._getProjectRoot(), this.pattern); 51 | 52 | let subsume = (file, cb) => { 53 | bundler.addFile(file, this); 54 | cb(null, file); 55 | }; 56 | 57 | this.vfs.src(pattern).pipe(mapStream(subsume)) 58 | .on('error', e => { 59 | console.log(`Error while adding all matching resources of pattern "${this.pattern}": ${e.message}`); 60 | reject(e); 61 | }) 62 | .on('end', resolve); 63 | }); 64 | } 65 | 66 | _getProjectRoot() { 67 | return this.bundle.bundler.project.paths.root; 68 | } 69 | 70 | getAllModuleIds() { 71 | return this.items.map(x => x.moduleId); 72 | } 73 | 74 | getAllFiles() { 75 | return this.items; 76 | } 77 | }; 78 | -------------------------------------------------------------------------------- /lib/build/stub-module.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const Utils = require('./utils'); 3 | const logger = require('aurelia-logging').getLogger('StubNodejs'); 4 | 5 | // stub core Node.js modules based on https://github.com/webpack/node-libs-browser/blob/master/index.js 6 | // no need stub for following modules, they got same name on npm package 7 | // 8 | // assert 9 | // buffer 10 | // events 11 | // punycode 12 | // process 13 | // string_decoder 14 | // url 15 | // util 16 | 17 | // fail on following core modules has no stub 18 | const UNAVAIABLE_CORE_MODULES = [ 19 | 'child_process', 20 | 'cluster', 21 | 'dgram', 22 | 'dns', 23 | // 'fs', 24 | 'net', 25 | 'readline', 26 | 'repl', 27 | 'tls' 28 | ]; 29 | 30 | const EMPTY_MODULE = 'define(function(){return {};});'; 31 | 32 | function resolvePath(packageName, root) { 33 | return path.relative(root, Utils.resolvePackagePath(packageName)).replace(/\\/g, '/'); 34 | } 35 | 36 | // note all paths here assumes local node_modules folder 37 | module.exports = function(moduleId, root) { 38 | // with subfix -browserify 39 | if (['crypto', 'https', 'os', 'path', 'stream', 'timers', 'tty', 'vm'].indexOf(moduleId) !== -1) { 40 | return {name: moduleId, path: resolvePath(`${moduleId}-browserify`, root)}; 41 | } 42 | 43 | if (moduleId === 'domain') { 44 | logger.warn('core Node.js module "domain" is deprecated'); 45 | return {name: 'domain', path: resolvePath('domain-browser', root)}; 46 | } 47 | 48 | if (moduleId === 'http') { 49 | return {name: 'http', path: resolvePath('stream-http', root)}; 50 | } 51 | 52 | if (moduleId === 'querystring') { 53 | return {name: 'querystring', path: resolvePath('querystring-browser-stub', root)}; 54 | } 55 | 56 | if (moduleId === 'fs') { 57 | return {name: 'fs', path: resolvePath('fs-browser-stub', root)}; 58 | } 59 | 60 | if (moduleId === 'sys') { 61 | logger.warn('core Node.js module "sys" is deprecated, the stub is disabled in CLI bundler due to conflicts with "util"'); 62 | } 63 | 64 | if (moduleId === 'zlib') { 65 | return {name: 'zlib', path: resolvePath('browserify-zlib', root)}; 66 | } 67 | 68 | if (UNAVAIABLE_CORE_MODULES.indexOf(moduleId) !== -1) { 69 | logger.warn(`No avaiable stub for core Node.js module "${moduleId}", stubbed with empty module`); 70 | return EMPTY_MODULE; 71 | } 72 | 73 | // https://github.com/defunctzombie/package-browser-field-spec 74 | // {"module-a": false} 75 | // replace with special placeholder __ignore__ 76 | if (moduleId === '__ignore__') { 77 | return EMPTY_MODULE; 78 | } 79 | 80 | if (moduleId === '__inject_css__') { 81 | return { 82 | name: '__inject_css__', 83 | path: resolvePath('aurelia-cli', root), 84 | main: 'lib/build/inject-css' 85 | }; 86 | } 87 | }; 88 | 89 | -------------------------------------------------------------------------------- /lib/build/utils.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const crypto = require('crypto'); 3 | const fs = require('../file-system'); 4 | const tmpDir = require('os').tmpdir(); 5 | 6 | exports.knownExtensions = ['.js', '.cjs', '.mjs', '.json', '.css', '.svg', '.html']; 7 | 8 | exports.couldMissGulpPreprocess = function(id) { 9 | const ext = path.extname(id).toLowerCase(); 10 | return ext && ext !== '.js' && ext !== '.html' && ext !== '.css'; 11 | }; 12 | 13 | function getPackagePaths() { 14 | // require.resolve(packageName) cannot resolve package has no main. 15 | // for instance: font-awesome v4.7.0 16 | // manually try resolve paths 17 | return [ 18 | // normal search from cli 19 | ...require.resolve.paths('not-core/'), 20 | // additional search from app's folder, this is necessary to support 21 | // lerna hoisting where cli is out of app's local node_modules folder. 22 | ...require('resolve/lib/node-modules-paths')(process.cwd(), {}) 23 | ]; 24 | } 25 | 26 | // resolve npm package path 27 | exports.resolvePackagePath = function(packageName) { 28 | const packagePaths = getPackagePaths(); 29 | for (let i = 0, len = packagePaths.length; i < len; i++) { 30 | const dirname = path.join(packagePaths[i], packageName); 31 | if (fs.isDirectory(dirname)) return dirname; 32 | } 33 | 34 | throw new Error(`cannot resolve npm package folder for "${packageName}"`); 35 | }; 36 | 37 | exports.moduleIdWithPlugin = function(moduleId, pluginName, type) { 38 | switch (type) { 39 | case 'require': 40 | return pluginName + '!' + moduleId; 41 | case 'system': 42 | return moduleId + '!' + pluginName; 43 | default: 44 | throw new Error(`Loader configuration style ${type} is not supported.`); 45 | } 46 | }; 47 | 48 | const CACHE_DIR = path.resolve(tmpDir, 'aurelia-cli-cache'); 49 | exports.cacheDir = CACHE_DIR; 50 | 51 | function cachedFilePath(hash) { 52 | const folder = hash.slice(0, 2); 53 | const fileName = hash.slice(2); 54 | return path.resolve(CACHE_DIR, folder, fileName); 55 | } 56 | 57 | exports.getCache = function(hash) { 58 | const filePath = cachedFilePath(hash); 59 | try { 60 | return JSON.parse(fs.readFileSync(filePath)); 61 | } catch { 62 | // ignore 63 | } 64 | }; 65 | 66 | exports.setCache = function(hash, object) { 67 | const filePath = cachedFilePath(hash); 68 | // async write 69 | fs.writeFile(filePath, JSON.stringify(object)); 70 | }; 71 | 72 | exports.runSequentially = function(tasks, cb) { 73 | let index = -1; 74 | let result = []; 75 | 76 | function exec() { 77 | index ++; 78 | 79 | if (index < tasks.length) { 80 | return cb(tasks[index], index).then(r => result.push(r)).then(exec); 81 | } 82 | 83 | return Promise.resolve(); 84 | } 85 | 86 | return exec().then(() => result); 87 | }; 88 | 89 | exports.generateHashedPath = function(pth, hash) { 90 | if (arguments.length !== 2) { 91 | throw new Error('`path` and `hash` required'); 92 | } 93 | 94 | return modifyFilename(pth, function(filename, ext) { 95 | return filename + '-' + hash + ext; 96 | }); 97 | }; 98 | 99 | exports.revertHashedPath = function(pth, hash) { 100 | if (arguments.length !== 2) { 101 | throw new Error('`path` and `hash` required'); 102 | } 103 | 104 | return modifyFilename(pth, function(filename, ext) { 105 | return filename.replace(new RegExp('-' + hash + '$'), '') + ext; 106 | }); 107 | }; 108 | 109 | exports.generateHash = function(bufOrStr) { 110 | return crypto.createHash('md5').update(bufOrStr).digest('hex'); 111 | }; 112 | 113 | exports.escapeForRegex = function(str) { 114 | let matchers = /[|\\{}()[\]^$+*?.]/g; 115 | return str.replace(matchers, '\\$&'); 116 | }; 117 | 118 | exports.createBundleFileRegex = function(bundleName) { 119 | return new RegExp(exports.escapeForRegex(bundleName) + '[^"\']*?\\.js', 'g'); 120 | }; 121 | 122 | function modifyFilename(pth, modifier) { 123 | if (arguments.length !== 2) { 124 | throw new Error('`path` and `modifier` required'); 125 | } 126 | 127 | if (Array.isArray(pth)) { 128 | return pth.map(function(el) { 129 | return modifyFilename(el, modifier); 130 | }); 131 | } 132 | 133 | let ext = path.extname(pth); 134 | return path.posix.join(path.dirname(pth), modifier(path.basename(pth, ext), ext)); 135 | } 136 | 137 | // https://nodejs.org/dist/latest-v10.x/docs/api/modules.html 138 | // after "high-level algorithm in pseudocode of what require.resolve() does" 139 | function nodejsLoadAsFile(resourcePath) { 140 | if (fs.isFile(resourcePath)) { 141 | return resourcePath; 142 | } 143 | 144 | if (fs.isFile(resourcePath + '.js')) { 145 | return resourcePath + '.js'; 146 | } 147 | 148 | if (fs.isFile(resourcePath + '.json')) { 149 | return resourcePath + '.json'; 150 | } 151 | // skip .node file that nobody uses 152 | } 153 | 154 | function nodejsLoadIndex(resourcePath) { 155 | if (!fs.isDirectory(resourcePath)) return; 156 | 157 | const indexJs = path.join(resourcePath, 'index.js'); 158 | if (fs.isFile(indexJs)) { 159 | return indexJs; 160 | } 161 | 162 | const indexJson = path.join(resourcePath, 'index.json'); 163 | if (fs.isFile(indexJson)) { 164 | return indexJson; 165 | } 166 | // skip index.node file that nobody uses 167 | } 168 | 169 | function nodejsLoadAsDirectory(resourcePath) { 170 | if (!fs.isDirectory(resourcePath)) return; 171 | 172 | const packageJson = path.join(resourcePath, 'package.json'); 173 | 174 | if (fs.isFile(packageJson)) { 175 | let metadata; 176 | try { 177 | metadata = JSON.parse(fs.readFileSync(packageJson)); 178 | } catch (err) { 179 | console.error(err); 180 | return; 181 | } 182 | let metaMain; 183 | // try 1.browser > 2.module > 3.main 184 | // the order is to target browser. 185 | // when aurelia-cli introduces multi-targets build, 186 | // it probably should use different order for electron app 187 | // for electron 1.module > 2.browser > 3.main 188 | if (typeof metadata.browser === 'string') { 189 | // use package.json browser field if possible. 190 | metaMain = metadata.browser; 191 | } else if (typeof metadata.browser === 'object' && typeof metadata.browser['.'] === 'string') { 192 | // use package.json browser mapping {".": "dist/index.js"} if possible. 193 | metaMain = metadata.browser['.']; 194 | } else if (typeof metadata.module === 'string' && 195 | !(metadata.name && metadata.name.startsWith('aurelia-'))) { 196 | // prefer es module format over cjs, just like webpack. 197 | // this improves compatibility with TypeScript. 198 | // ignores aurelia-* core npm packages as their module 199 | // field is pointing to es2015 folder. 200 | metaMain = metadata.module; 201 | } else if (typeof metadata.main === 'string') { 202 | metaMain = metadata.main; 203 | } 204 | 205 | let mainFile = metaMain || 'index'; 206 | const mainResourcePath = path.resolve(resourcePath, mainFile); 207 | return nodejsLoadAsFile(mainResourcePath) || nodejsLoadIndex(mainResourcePath); 208 | } 209 | 210 | return nodejsLoadIndex(resourcePath); 211 | } 212 | 213 | exports.nodejsLoad = function(resourcePath) { 214 | return nodejsLoadAsFile(resourcePath) || nodejsLoadAsDirectory(resourcePath); 215 | }; 216 | 217 | exports.removeJsExtension = function(filePath) { 218 | if (path.extname(filePath).toLowerCase() === '.js') { 219 | return filePath.slice(0, -3); 220 | } 221 | 222 | return filePath; 223 | }; 224 | 225 | -------------------------------------------------------------------------------- /lib/build/webpack-reporter.js: -------------------------------------------------------------------------------- 1 | module.exports = function reportReadiness(options) { 2 | const uri = createDomain(options); 3 | const yargs = require('yargs'); 4 | const argv = yargs.argv; 5 | argv.color = require('supports-color'); 6 | const useColor = argv.color; 7 | 8 | let startSentence = `Project is running at ${colorInfo(useColor, uri)}`; 9 | 10 | if (options.socket) { 11 | startSentence = `Listening to socket at ${colorInfo(useColor, options.socket)}`; 12 | } 13 | console.log((argv.progress ? '\n' : '') + startSentence); 14 | 15 | console.log(`webpack output is served from ${colorInfo(useColor, options.publicPath)}`); 16 | const contentBase = Array.isArray(options.contentBase) ? options.contentBase.join(', ') : options.contentBase; 17 | 18 | if (contentBase) { 19 | console.log(`Content not from webpack is served from ${colorInfo(useColor, contentBase)}`); 20 | } 21 | 22 | if (options.historyApiFallback) { 23 | console.log(`404s will fallback to ${colorInfo(useColor, options.historyApiFallback.index || '/index.html')}`); 24 | } 25 | }; 26 | 27 | function createDomain(opts) { 28 | const protocol = opts.https ? 'https' : 'http'; 29 | const url = require('url'); 30 | 31 | // the formatted domain (url without path) of the webpack server 32 | return opts.public ? `${protocol}://${opts.public}` : url.format({ 33 | protocol: protocol, 34 | hostname: opts.host, 35 | port: opts.socket ? 0 : opts.port.toString() 36 | }); 37 | } 38 | 39 | function colorInfo(useColor, msg) { 40 | if (useColor) { 41 | // Make text blue and bold, so it *pops* 42 | return `\u001b[1m\u001b[33m${msg}\u001b[39m\u001b[22m`; 43 | } 44 | return msg; 45 | } 46 | -------------------------------------------------------------------------------- /lib/cli-options.js: -------------------------------------------------------------------------------- 1 | const fs = require('./file-system'); 2 | 3 | function definedEnvironments() { 4 | let envs = []; 5 | // read user defined environment files 6 | let files; 7 | try { 8 | files = fs.readdirSync('aurelia_project/environments'); 9 | } catch { 10 | // ignore 11 | } 12 | files && files.forEach(file => { 13 | const m = file.match(/^(.+)\.(t|j)s$/); 14 | if (m) envs.push(m[1]); 15 | }); 16 | return envs; 17 | } 18 | 19 | exports.CLIOptions = class { 20 | constructor() { 21 | exports.CLIOptions.instance = this; 22 | } 23 | 24 | taskName() { 25 | let name = this.taskPath.split(/[/\\]/).pop(); 26 | let parts = name.split('.'); 27 | parts.pop(); 28 | return parts.join('.'); 29 | } 30 | 31 | getEnvironment() { 32 | if (this._env) return this._env; 33 | 34 | let env = this.getFlagValue('env') || process.env.NODE_ENV || 'dev'; 35 | const envs = definedEnvironments(); 36 | 37 | if (!envs.includes(env)) { 38 | // normalize NODE_ENV production/development (Node.js convention) to prod/dev 39 | // only if user didn't define production.js or development.js 40 | if (env === 'production' && envs.includes('prod')) { 41 | env = 'prod'; 42 | } else if (env === 'development' && envs.includes('dev')) { 43 | env = 'dev'; 44 | } else if (env !== 'dev') { 45 | // forgive missing aurelia_project/environments/dev.js as dev is our default env 46 | console.error(`The selected environment "${env}" is not defined in your aurelia_project/environments folder.`); 47 | process.exit(1); 48 | return; 49 | } 50 | } 51 | 52 | this._env = env; 53 | return env; 54 | } 55 | 56 | hasFlag(name, shortcut) { 57 | if (this.args) { 58 | let lookup = '--' + name; 59 | let found = this.args.indexOf(lookup) !== -1; 60 | 61 | if (found) { 62 | return true; 63 | } 64 | 65 | lookup = shortcut || ('-' + name[0]); 66 | found = this.args.indexOf(lookup) !== -1; 67 | 68 | if (found) { 69 | return true; 70 | } 71 | 72 | lookup = '-' + name; 73 | return this.args.indexOf(lookup) !== -1; 74 | } 75 | 76 | return false; 77 | } 78 | 79 | getFlagValue(name, shortcut) { 80 | if (this.args) { 81 | let lookup = '--' + name; 82 | let index = this.args.indexOf(lookup); 83 | 84 | if (index !== -1) { 85 | return this.args[index + 1] || null; 86 | } 87 | 88 | lookup = shortcut || ('-' + name[0]); 89 | index = this.args.indexOf(lookup); 90 | 91 | if (index !== -1) { 92 | return this.args[index + 1] || null; 93 | } 94 | 95 | lookup = '-' + name; 96 | index = this.args.indexOf(lookup); 97 | 98 | if (index !== -1) { 99 | return this.args[index + 1] || null; 100 | } 101 | 102 | return null; 103 | } 104 | 105 | return null; 106 | } 107 | 108 | static taskName() { 109 | return exports.CLIOptions.instance.taskName(); 110 | } 111 | 112 | static hasFlag(name, shortcut) { 113 | return exports.CLIOptions.instance.hasFlag(name, shortcut); 114 | } 115 | 116 | static getFlagValue(name, shortcut) { 117 | return exports.CLIOptions.instance.getFlagValue(name, shortcut); 118 | } 119 | 120 | static getEnvironment() { 121 | return exports.CLIOptions.instance.getEnvironment(); 122 | } 123 | }; 124 | -------------------------------------------------------------------------------- /lib/cli.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const Container = require('aurelia-dependency-injection').Container; 3 | const fs = require('./file-system'); 4 | const ui = require('./ui'); 5 | const Project = require('./project').Project; 6 | const CLIOptions = require('./cli-options').CLIOptions; 7 | const LogManager = require('aurelia-logging'); 8 | const Logger = require('./logger').Logger; 9 | 10 | exports.CLI = class { 11 | constructor(options) { 12 | this.options = options || new CLIOptions(); 13 | this.container = new Container(); 14 | this.ui = new ui.ConsoleUI(this.options); 15 | this.configureContainer(); 16 | this.logger = LogManager.getLogger('CLI'); 17 | } 18 | 19 | // Note: cannot use this.logger.error inside run() 20 | // because logger is not configured yet! 21 | // this.logger.error prints nothing in run(), 22 | // directly use this.ui.log. 23 | run(cmd, args) { 24 | const version = `${this.options.runningGlobally ? 'Global' : 'Local'} aurelia-cli v${require('../package.json').version}`; 25 | 26 | if (cmd === '--version' || cmd === '-v') { 27 | return this.ui.log(version); 28 | } 29 | 30 | return (cmd === 'new' ? Promise.resolve() : this._establishProject()) 31 | .then(project => { 32 | this.ui.log(version); 33 | 34 | if (project && this.options.runningLocally) { 35 | this.project = project; 36 | this.container.registerInstance(Project, project); 37 | } else if (project && this.options.runningGlobally) { 38 | this.ui.log('The current directory is likely an Aurelia-CLI project, but no local installation of Aurelia-CLI could be found. ' + 39 | '(Do you need to restore node modules using npm install?)'); 40 | return Promise.resolve(); 41 | } else if (!project && this.options.runningLocally) { 42 | this.ui.log('It appears that the Aurelia CLI is running locally from ' + __dirname + '. However, no project directory could be found. ' + 43 | 'The Aurelia CLI has to be installed globally (npm install -g aurelia-cli) and locally (npm install aurelia-cli) in an Aurelia CLI project directory'); 44 | return Promise.resolve(); 45 | } 46 | 47 | return this.createCommand(cmd, args) 48 | .then((command) => command.execute(args)); 49 | }); 50 | } 51 | 52 | configureLogger() { 53 | LogManager.addAppender(this.container.get(Logger)); 54 | let level = CLIOptions.hasFlag('debug') ? LogManager.logLevel.debug : LogManager.logLevel.info; 55 | LogManager.setLevel(level); 56 | } 57 | 58 | configureContainer() { 59 | this.container.registerInstance(CLIOptions, this.options); 60 | this.container.registerInstance(ui.UI, this.ui); 61 | } 62 | 63 | createCommand(commandText, commandArgs) { 64 | return new Promise(resolve => { 65 | if (!commandText) { 66 | resolve(this.createHelpCommand()); 67 | return; 68 | } 69 | 70 | let parts = commandText.split(':'); 71 | let commandModule = parts[0]; 72 | let commandName = parts[1] || 'default'; 73 | 74 | try { 75 | let alias = require('./commands/alias.json')[commandModule]; 76 | let found = this.container.get(require(`./commands/${alias || commandModule}/command`)); 77 | Object.assign(this.options, { args: commandArgs }); 78 | // need to configure logger after getting args 79 | this.configureLogger(); 80 | resolve(found); 81 | } catch { 82 | if (this.project) { 83 | this.project.resolveTask(commandModule).then(taskPath => { 84 | if (taskPath) { 85 | Object.assign(this.options, { 86 | taskPath: taskPath, 87 | args: commandArgs, 88 | commandName: commandName 89 | }); 90 | // need to configure logger after getting args 91 | this.configureLogger(); 92 | 93 | resolve(this.container.get(require('./commands/gulp'))); 94 | } else { 95 | this.ui.log(`Invalid Command: ${commandText}`); 96 | resolve(this.createHelpCommand()); 97 | } 98 | }); 99 | } else { 100 | this.ui.log(`Invalid Command: ${commandText}`); 101 | resolve(this.createHelpCommand()); 102 | } 103 | } 104 | }); 105 | } 106 | 107 | createHelpCommand() { 108 | return this.container.get(require('./commands/help/command')); 109 | } 110 | 111 | _establishProject() { 112 | return determineWorkingDirectory(process.cwd()) 113 | .then(dir => dir ? Project.establish(dir) : this.ui.log('No Aurelia project found.')); 114 | } 115 | }; 116 | 117 | function determineWorkingDirectory(dir) { 118 | let parent = path.join(dir, '..'); 119 | 120 | if (parent === dir) { 121 | return Promise.resolve(); // resolve to nothing 122 | } 123 | 124 | return fs.stat(path.join(dir, 'aurelia_project')) 125 | .then(() => dir) 126 | .catch(() => determineWorkingDirectory(parent)); 127 | } 128 | 129 | process.on('unhandledRejection', (reason) => { 130 | console.log('Uncaught promise rejection:'); 131 | console.log(reason); 132 | }); 133 | -------------------------------------------------------------------------------- /lib/commands/alias.json: -------------------------------------------------------------------------------- 1 | { 2 | "g":"generate", 3 | "h":"help", 4 | "n":"new" 5 | } 6 | -------------------------------------------------------------------------------- /lib/commands/config/command.js: -------------------------------------------------------------------------------- 1 | const UI = require('../../ui').UI; 2 | const CLIOptions = require('../../cli-options').CLIOptions; 3 | const Container = require('aurelia-dependency-injection').Container; 4 | const os = require('os'); 5 | 6 | const Configuration = require('./configuration'); 7 | const ConfigurationUtilities = require('./util'); 8 | 9 | module.exports = class { 10 | static inject() { return [Container, UI, CLIOptions]; } 11 | 12 | constructor(container, ui, options) { 13 | this.container = container; 14 | this.ui = ui; 15 | this.options = options; 16 | } 17 | 18 | execute(args) { 19 | this.config = new Configuration(this.options); 20 | this.util = new ConfigurationUtilities(this.options, args); 21 | let key = this.util.getArg(0) || ''; 22 | let value = this.util.getValue(this.util.getArg(1)); 23 | let save = !CLIOptions.hasFlag('no-save'); 24 | let backup = !CLIOptions.hasFlag('no-backup'); 25 | let action = this.util.getAction(value); 26 | 27 | this.displayInfo(`Performing configuration action '${action}' on '${key}'`, (value ? `with '${value}'` : '')); 28 | this.displayInfo(this.config.execute(action, key, value)); 29 | 30 | if (action !== 'get') { 31 | if (save) { 32 | this.config.save(backup).then((name) => { 33 | this.displayInfo('Configuration saved. ' + (backup ? `Backup file '${name}' created.` : 'No backup file was created.')); 34 | }); 35 | } else { 36 | this.displayInfo(`Action was '${action}', but no save was performed!`); 37 | } 38 | } 39 | } 40 | 41 | displayInfo(message) { 42 | return this.ui.log(message + os.EOL); 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /lib/commands/config/command.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "config", 3 | "description": "Gets or sets configuration for the Aurelia application.", 4 | "parameters": [ 5 | { 6 | "name": "key", 7 | "optional": true, 8 | "description": "The key you want to get or set. Supports hierarchies and array indexes, for example build.targets[0] and arrayWithArray[2].[1]" 9 | }, 10 | { 11 | "name": "value", 12 | "optional": true, 13 | "description": "The value you want to set the key to. Supports json, for example \"{ \\\"myKey\\\": \\\"myValue\\\" }\"" 14 | } 15 | ], 16 | "flags": [ 17 | { 18 | "name": "get", 19 | "description": "Gets the content of key, ignoring value parameter (the same as not specifying a value).", 20 | "type": "boolean" 21 | }, 22 | { 23 | "name": "set", 24 | "description": "Sets the content of key to value, replacing any existing content.", 25 | "type": "boolean" 26 | }, 27 | { 28 | "name": "clear", 29 | "description": "Deletes the key and all its content from the configuration.", 30 | "type": "boolean" 31 | }, 32 | { 33 | "name": "add", 34 | "description": "If value or existing content of the key is an array, adds value(s) to existing content. If value is an object, merges it into existing content of key.", 35 | "type": "boolean" 36 | }, 37 | { 38 | "name": "remove", 39 | "description": "If value or existing content of the key is an array, removes value(s) from existing content. If value or existing content of the key is an object, removes key(s) from existing content of key.", 40 | "type": "boolean" 41 | }, 42 | { 43 | "name": "no-save", 44 | "description": "Don't save the changes in the configuration file.", 45 | "type": "boolean" 46 | }, 47 | { 48 | "name": "no-backup", 49 | "description": "Don't create a backup configuration file before saving changes.", 50 | "type": "boolean" 51 | } 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /lib/commands/config/configuration.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const copySync = require('../../file-system').copySync; 3 | const readFileSync = require('../../file-system').readFileSync; 4 | const writeFile = require('../../file-system').writeFile; 5 | 6 | class Configuration { 7 | constructor(options) { 8 | this.options = options; 9 | this.aureliaJsonPath = options.originalBaseDir + '/aurelia_project/aurelia.json'; 10 | this.project = JSON.parse(readFileSync(this.aureliaJsonPath)); 11 | } 12 | 13 | configEntry(key, createKey) { 14 | let entry = this.project; 15 | let keys = key.split('.'); 16 | 17 | if (!keys[0]) { 18 | return entry; 19 | } 20 | 21 | while (entry && keys.length) { 22 | key = this.parsedKey(keys.shift()); 23 | if (entry[key.value] === undefined || entry[key.value] === null) { 24 | if (!createKey) { 25 | return entry[key.value]; 26 | } 27 | let checkKey = this.parsedKey(keys.length ? keys[0] : createKey); 28 | if (checkKey.index) { 29 | entry[key.value] = []; 30 | } else if (checkKey.key) { 31 | entry[key.value] = {}; 32 | } 33 | } 34 | entry = entry[key.value]; 35 | 36 | // TODO: Add support for finding objects based on input values? 37 | // TODO: Add support for finding string in array? 38 | } 39 | 40 | return entry; 41 | } 42 | 43 | parsedKey(key) { 44 | if (/\[(\d+)\]/.test(key)) { 45 | return { index: true, key: false, value: +(RegExp.$1) }; 46 | } 47 | 48 | return { index: false, key: true, value: key }; 49 | } 50 | 51 | normalizeKey(key) { 52 | const re = /([^.])\[/; 53 | while (re.exec(key)) { 54 | key = key.replace(re, RegExp.$1 + '.['); 55 | } 56 | 57 | let keys = key.split('.'); 58 | for (let i = 0; i < keys.length; i++) { 59 | if (/\[(\d+)\]/.test(keys[i])) { 60 | // console.log(`keys[${i}] is index: ${keys[i]}`); 61 | } else if (/\[(.+)\]/.test(keys[i])) { 62 | // console.log(`keys[${i}] is indexed name: ${keys[i]}`); 63 | keys[i] = RegExp.$1; 64 | } else { 65 | // console.log(`keys[${i}] is name: ${keys[i]}`); 66 | } 67 | } 68 | 69 | return keys.join('.'); 70 | } 71 | 72 | execute(action, key, value) { 73 | let originalKey = key; 74 | 75 | key = this.normalizeKey(key); 76 | 77 | if (action === 'get') { 78 | return `Configuration key '${key}' is:` + os.EOL + JSON.stringify(this.configEntry(key), null, 2); 79 | } 80 | 81 | let keys = key.split('.'); 82 | key = this.parsedKey(keys.pop()); 83 | let parent = keys.join('.'); 84 | 85 | if (action === 'set') { 86 | let entry = this.configEntry(parent, key.value); 87 | if (entry) { 88 | entry[key.value] = value; 89 | } else { 90 | console.log('Failed to set property', this.normalizeKey(originalKey), '!'); 91 | } 92 | } else if (action === 'clear') { 93 | let entry = this.configEntry(parent); 94 | if (entry && (key.value in entry)) { 95 | delete entry[key.value]; 96 | } else { 97 | console.log('No property', this.normalizeKey(originalKey), 'to clear!'); 98 | } 99 | } else if (action === 'add') { 100 | let entry = this.configEntry(parent, key.value); 101 | if (Array.isArray(entry[key.value]) && !Array.isArray(value)) { 102 | value = [value]; 103 | } if (Array.isArray(value) && !Array.isArray(entry[key.value])) { 104 | entry[key.value] = (entry ? [entry[key.value]] : []); 105 | } if (Array.isArray(value)) { 106 | entry[key.value].push.apply(entry[key.value], value); 107 | } else if (Object(value) === value) { 108 | if (Object(entry[key.value]) !== entry[key.value]) { 109 | entry[key.value] = {}; 110 | } 111 | Object.assign(entry[key.value], value); 112 | } else { 113 | entry[key.value] = value; 114 | } 115 | } else if (action === 'remove') { 116 | let entry = this.configEntry(parent); 117 | 118 | if (Array.isArray(entry) && key.index) { 119 | entry.splice(key.value, 1); 120 | } else if (Object(entry) === entry && key.key) { 121 | delete entry[key.value]; 122 | } else if (!entry) { 123 | console.log('No property', this.normalizeKey(originalKey), 'to remove from!'); 124 | } else { 125 | console.log("Can't remove value from", entry[key.value], '!'); 126 | } 127 | } 128 | key = this.normalizeKey(originalKey); 129 | return `Configuration key '${key}' is now:` + os.EOL + JSON.stringify(this.configEntry(key), null, 2); 130 | } 131 | 132 | save(backup) { 133 | if (backup === undefined) backup = true; 134 | 135 | const unique = new Date().toISOString().replace(/[T\D]/g, ''); 136 | let arr = this.aureliaJsonPath.split(/[\\/]/); 137 | const name = arr.pop(); 138 | const path = arr.join('/'); 139 | const bak = `${name}.${unique}.bak`; 140 | 141 | if (backup) { 142 | copySync(this.aureliaJsonPath, [path, bak].join('/')); 143 | } 144 | 145 | return writeFile(this.aureliaJsonPath, JSON.stringify(this.project, null, 2), 'utf8') 146 | .then(() => { return bak; }); 147 | } 148 | } 149 | 150 | module.exports = Configuration; 151 | -------------------------------------------------------------------------------- /lib/commands/config/util.js: -------------------------------------------------------------------------------- 1 | class ConfigurationUtilities { 2 | constructor(options, args) { 3 | this.options = options; 4 | this.args = args; 5 | } 6 | 7 | getArg(arg) { 8 | let args = this.args; 9 | if (args) { 10 | for (let i = 0; i < args.length; i++) { 11 | if (args[i].startsWith('--')) { 12 | arg++; 13 | } 14 | if (i === arg) { 15 | return args[i]; 16 | } 17 | } 18 | } 19 | } 20 | 21 | getValue(value) { 22 | if (value) { 23 | if (!value.startsWith('"') && 24 | !value.startsWith('[') && 25 | !value.startsWith('{')) { 26 | value = `"${value}"`; 27 | } 28 | value = JSON.parse(value); 29 | } 30 | return value; 31 | } 32 | 33 | getAction(value) { 34 | let actions = ['add', 'remove', 'set', 'clear', 'get']; 35 | for (let action of actions) { 36 | if (this.options.hasFlag(action)) { 37 | return action; 38 | } 39 | } 40 | if (!value) { 41 | return 'get'; 42 | } 43 | if (Array.isArray(value) || typeof value === 'object') { 44 | return 'add'; 45 | } 46 | return 'set'; 47 | } 48 | } 49 | 50 | module.exports = ConfigurationUtilities; 51 | -------------------------------------------------------------------------------- /lib/commands/generate/command.js: -------------------------------------------------------------------------------- 1 | const UI = require('../../ui').UI; 2 | const CLIOptions = require('../../cli-options').CLIOptions; 3 | const Container = require('aurelia-dependency-injection').Container; 4 | const Project = require('../../project').Project; 5 | const string = require('../../string'); 6 | const os = require('os'); 7 | 8 | module.exports = class { 9 | static inject() { return [Container, UI, CLIOptions, Project]; } 10 | 11 | constructor(container, ui, options, project) { 12 | this.container = container; 13 | this.ui = ui; 14 | this.options = options; 15 | this.project = project; 16 | } 17 | 18 | execute(args) { 19 | if (args.length < 1) { 20 | return this.displayGeneratorInfo('No Generator Specified. Available Generators:'); 21 | } 22 | 23 | this.project.installTranspiler(); 24 | 25 | return this.project.resolveGenerator(args[0]).then(generatorPath => { 26 | Object.assign(this.options, { 27 | generatorPath: generatorPath, 28 | args: args.slice(1) 29 | }); 30 | 31 | if (generatorPath) { 32 | let generator = this.project.getExport(require(generatorPath)); 33 | 34 | if (generator.inject) { 35 | generator = this.container.get(generator); 36 | generator = generator.execute.bind(generator); 37 | } 38 | 39 | return generator(); 40 | } 41 | 42 | return this.displayGeneratorInfo(`Invalid Generator: ${args[0]}. Available Generators:`); 43 | }); 44 | } 45 | 46 | displayGeneratorInfo(message) { 47 | return this.ui.displayLogo() 48 | .then(() => this.ui.log(message + os.EOL)) 49 | .then(() => this.project.getGeneratorMetadata()) 50 | .then(metadata => string.buildFromMetadata(metadata, this.ui.getWidth())) 51 | .then(str => this.ui.log(str)); 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /lib/commands/generate/command.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "generate", 3 | "description": "Generates code for common use cases in Aurelia applications.", 4 | "parameters": [ 5 | { 6 | "name": "generator-name", 7 | "description": "The name of the generator you want to run." 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /lib/commands/gulp.js: -------------------------------------------------------------------------------- 1 | const UI = require('../ui').UI; 2 | const CLIOptions = require('../cli-options').CLIOptions; 3 | const Container = require('aurelia-dependency-injection').Container; 4 | const Project = require('../project').Project; 5 | 6 | module.exports = class { 7 | static inject() { return [Container, UI, CLIOptions, Project]; } 8 | 9 | constructor(container, ui, options, project) { 10 | this.container = container; 11 | this.ui = ui; 12 | this.options = options; 13 | this.project = project; 14 | } 15 | 16 | execute() { 17 | return new Promise((resolve, reject) => { 18 | const gulp = require('gulp'); 19 | this.connectLogging(gulp); 20 | 21 | this.project.installTranspiler(); 22 | 23 | makeInjectable(gulp, 'series', this.container); 24 | makeInjectable(gulp, 'parallel', this.container); 25 | 26 | process.nextTick(() => { 27 | let task = this.project.getExport(require(this.options.taskPath), this.options.commandName); 28 | 29 | gulp.series(task)(error => { 30 | if (error) reject(error); 31 | else resolve(); 32 | }); 33 | }); 34 | }); 35 | } 36 | 37 | connectLogging(gulp) { 38 | gulp.on('start', e => { 39 | if (e.name[0] === '<') return; 40 | this.ui.log(`Starting '${e.name}'...`); 41 | }); 42 | 43 | gulp.on('stop', e => { 44 | if (e.name[0] === '<') return; 45 | this.ui.log(`Finished '${e.name}'`); 46 | }); 47 | 48 | gulp.on('error', e => this.ui.log(e)); 49 | } 50 | }; 51 | 52 | function makeInjectable(gulp, name, container) { 53 | let original = gulp[name]; 54 | 55 | gulp[name] = function() { 56 | let args = new Array(arguments.length); 57 | 58 | for (let i = 0, ii = arguments.length; i < ii; ++i) { 59 | let task = arguments[i]; 60 | 61 | if (task.inject) { 62 | let taskName = task.name; 63 | task = container.get(task); 64 | task = task.execute.bind(task); 65 | task.displayName = taskName; 66 | } 67 | 68 | args[i] = task; 69 | } 70 | 71 | return original.apply(gulp, args); 72 | }; 73 | } 74 | -------------------------------------------------------------------------------- /lib/commands/help/command.js: -------------------------------------------------------------------------------- 1 | const UI = require('../../ui').UI; 2 | const CLIOptions = require('../../cli-options').CLIOptions; 3 | const Optional = require('aurelia-dependency-injection').Optional; 4 | const Project = require('../../project').Project; 5 | const string = require('../../string'); 6 | 7 | module.exports = class { 8 | static inject() { return [CLIOptions, UI, Optional.of(Project)]; } 9 | 10 | constructor(options, ui, project) { 11 | this.options = options; 12 | this.ui = ui; 13 | this.project = project; 14 | } 15 | 16 | execute() { 17 | return this.ui.displayLogo() 18 | .then(() => { 19 | if (this.options.runningGlobally) { 20 | return this.getGlobalCommandText(); 21 | } 22 | 23 | return this.getLocalCommandText(); 24 | }).then(text => this.ui.log(text)); 25 | } 26 | 27 | getGlobalCommandText() { 28 | return string.buildFromMetadata([ 29 | require('../new/command.json'), 30 | require('./command.json') 31 | ], this.ui.getWidth()); 32 | } 33 | 34 | getLocalCommandText() { 35 | const commands = [ 36 | require('../generate/command.json'), 37 | require('../config/command.json'), 38 | require('./command.json') 39 | ]; 40 | 41 | return this.project.getTaskMetadata().then(metadata => { 42 | return string.buildFromMetadata(metadata.concat(commands), this.ui.getWidth()); 43 | }); 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /lib/commands/help/command.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "help", 3 | "description": "Displays the Aurelia CLI help." 4 | } 5 | -------------------------------------------------------------------------------- /lib/commands/new/command.js: -------------------------------------------------------------------------------- 1 | const {spawn} = require('child_process'); 2 | 3 | module.exports = class { 4 | async execute(args) { 5 | // Calls "npx makes aurelia/v1" 6 | // https://github.com/aurelia/v1 7 | spawn('npx', ['makes', 'aurelia/v1', ...args], {stdio: 'inherit', shell: true}); 8 | } 9 | }; 10 | 11 | -------------------------------------------------------------------------------- /lib/commands/new/command.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "new", 3 | "description": "Creates a new Aurelia application project. Wraps \"npx makes aurelia/v1\". https://github.com/aurelia/v1", 4 | "parameters": [ 5 | { 6 | "name": "project-name", 7 | "optional": true, 8 | "description": "The name to create the project with. If one isn't provided, the wizard will ask for one." 9 | } 10 | ], 11 | "flags": [ 12 | { 13 | "name": "here", 14 | "description": "Creates a new project with the current working directory as the root of the project instead of creating a new project folder.", 15 | "type": "boolean" 16 | }, 17 | { 18 | "name": "plugin", 19 | "description": "Creates a new Aurelia plugin project.", 20 | "type": "boolean" 21 | }, 22 | { 23 | "name": "select", 24 | "description": "Preselect features (e.g. --select cli-bundler,alameda,karma). This option also turns on unattended mode.", 25 | "type": "string" 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /lib/configuration.js: -------------------------------------------------------------------------------- 1 | const CLIOptions = require('./cli-options').CLIOptions; 2 | 3 | exports.Configuration = class { 4 | constructor(options, defaultOptions, environment) { 5 | this.options = Object.assign({}, defaultOptions, options); 6 | this.environment = environment || CLIOptions.getEnvironment(); 7 | } 8 | 9 | getAllOptions() { 10 | return this.options; 11 | } 12 | 13 | getValue(propPath) { 14 | let split = propPath.split('.'); 15 | let cur = this.options; 16 | 17 | for (let i = 0; i < split.length; i++) { 18 | if (!cur) { 19 | return undefined; 20 | } 21 | 22 | cur = cur[split[i]]; 23 | } 24 | 25 | if (typeof cur === 'object') { 26 | let keys = Object.keys(cur); 27 | let result = undefined; 28 | 29 | if (keys.indexOf('default') > -1 && typeof(cur.default) === 'object') { 30 | result = cur.default; 31 | } 32 | 33 | for (let i = 0; i < keys.length; i++) { 34 | if (this.matchesEnvironment(this.environment, keys[i])) { 35 | if (typeof(cur[keys[i]]) === 'object') { 36 | result = Object.assign(result || {}, cur[keys[i]]); 37 | } else { 38 | return cur[keys[i]]; 39 | } 40 | } 41 | } 42 | 43 | return result; 44 | } 45 | 46 | return cur; 47 | } 48 | 49 | isApplicable(propPath) { 50 | let value = this.getValue(propPath); 51 | 52 | if (!value) { 53 | return false; 54 | } 55 | 56 | if (typeof value === 'boolean') { 57 | return value; 58 | } 59 | 60 | if (typeof value === 'string') { 61 | return this.matchesEnvironment(this.environment, value); 62 | } 63 | 64 | if (typeof value === 'object') { 65 | return true; 66 | } 67 | 68 | return false; 69 | } 70 | 71 | matchesEnvironment(environment, value) { 72 | let parts = value.split('&').map(x => x.trim().toLowerCase()); 73 | return parts.indexOf(environment) !== -1; 74 | } 75 | }; 76 | -------------------------------------------------------------------------------- /lib/file-system.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const nodePath = require('path'); 3 | 4 | exports.fs = fs; 5 | 6 | /** 7 | * @deprecated 8 | * fs.exists() is deprecated. 9 | * See https://nodejs.org/api/fs.html#fs_fs_exists_path_callback. 10 | * Functions using it can also not be properly tested. 11 | */ 12 | exports.exists = function(path) { 13 | return new Promise(resolve => fs.exists(path, resolve)); 14 | }; 15 | 16 | exports.stat = function(path) { 17 | return new Promise((resolve, reject) => { 18 | fs.stat(path, (error, stats) => { 19 | if (error) reject(error); 20 | else resolve(stats); 21 | }); 22 | }); 23 | }; 24 | 25 | exports.existsSync = function(path) { 26 | return fs.existsSync(path); 27 | }; 28 | 29 | exports.mkdir = function(path) { 30 | return new Promise((resolve, reject) => { 31 | fs.mkdir(path, error => { 32 | if (error) reject(error); 33 | else resolve(); 34 | }); 35 | }); 36 | }; 37 | 38 | exports.mkdirp = function(path) { 39 | return new Promise((resolve, reject) => { 40 | fs.mkdir(path, {recursive: true}, error => { 41 | if (error) reject(error); 42 | else resolve(); 43 | }); 44 | }); 45 | }; 46 | 47 | exports.readdir = function(path) { 48 | return new Promise((resolve, reject) => { 49 | fs.readdir(path, (error, files) => { 50 | if (error) reject(error); 51 | else resolve(files); 52 | }); 53 | }); 54 | }; 55 | 56 | exports.appendFile = function(path, text, cb) { 57 | fs.appendFile(path, text, cb); 58 | }; 59 | 60 | exports.readdirSync = function(path) { 61 | return fs.readdirSync(path); 62 | }; 63 | 64 | exports.readFile = function(path, encoding) { 65 | if (encoding !== null) { 66 | encoding = encoding || 'utf8'; 67 | } 68 | 69 | return new Promise((resolve, reject) => { 70 | fs.readFile(path, encoding, (error, data) => { 71 | if (error) reject(error); 72 | else resolve(data); 73 | }); 74 | }); 75 | }; 76 | 77 | exports.readFileSync = fs.readFileSync; 78 | 79 | exports.readFileSync = function(path, encoding) { 80 | if (encoding !== null) { 81 | encoding = encoding || 'utf8'; 82 | } 83 | 84 | return fs.readFileSync(path, encoding); 85 | }; 86 | 87 | exports.copySync = function(sourceFile, targetFile) { 88 | fs.writeFileSync(targetFile, fs.readFileSync(sourceFile)); 89 | }; 90 | 91 | exports.resolve = function(path) { 92 | return nodePath.resolve(path); 93 | }; 94 | 95 | exports.join = function() { 96 | return nodePath.join.apply(this, Array.prototype.slice.call(arguments)); 97 | }; 98 | 99 | exports.statSync = function(path) { 100 | return fs.statSync(path); 101 | }; 102 | 103 | exports.isFile = function(path) { 104 | try { 105 | return fs.statSync(path).isFile(); 106 | } catch { 107 | // ignore 108 | return false; 109 | } 110 | }; 111 | 112 | exports.isDirectory = function(path) { 113 | try { 114 | return fs.statSync(path).isDirectory(); 115 | } catch { 116 | // ignore 117 | return false; 118 | } 119 | }; 120 | 121 | exports.writeFile = function(path, content, encoding) { 122 | return new Promise((resolve, reject) => { 123 | fs.mkdir(nodePath.dirname(path), {recursive: true}, err => { 124 | if (err) reject(err); 125 | else { 126 | fs.writeFile(path, content, encoding || 'utf8', error => { 127 | if (error) reject(error); 128 | else resolve(); 129 | }); 130 | } 131 | }); 132 | }); 133 | }; 134 | -------------------------------------------------------------------------------- /lib/get-tty-size.js: -------------------------------------------------------------------------------- 1 | const tty = require('tty'); 2 | 3 | let size; 4 | 5 | module.exports = function() { 6 | // Only run it once. 7 | if (size) return size; 8 | 9 | let width; 10 | let height; 11 | 12 | if (tty.isatty(1) && tty.isatty(2)) { 13 | if (process.stdout.getWindowSize) { 14 | width = process.stdout.getWindowSize(1)[0]; 15 | height = process.stdout.getWindowSize(1)[1]; 16 | } else if (tty.getWindowSize) { 17 | width = tty.getWindowSize()[1]; 18 | height = tty.getWindowSize()[0]; 19 | } else if (process.stdout.columns && process.stdout.rows) { 20 | height = process.stdout.rows; 21 | width = process.stdout.columns; 22 | } 23 | } else { 24 | width = 80; 25 | height = 100; 26 | } 27 | 28 | size = { height: height, width: width }; 29 | return size; 30 | }; 31 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | require('aurelia-polyfills'); 2 | 3 | exports.CLI = require('./cli').CLI; 4 | exports.CLIOptions = require('./cli-options').CLIOptions; 5 | exports.UI = require('./ui').UI; 6 | exports.Project = require('./project').Project; 7 | exports.ProjectItem = require('./project-item').ProjectItem; 8 | exports.build = require('./build'); 9 | exports.Configuration = require('./configuration').Configuration; 10 | exports.reportWebpackReadiness = require('./build/webpack-reporter'); 11 | exports.NPM = require('./package-managers/npm').NPM; 12 | exports.Yarn = require('./package-managers/yarn').Yarn; 13 | -------------------------------------------------------------------------------- /lib/logger.js: -------------------------------------------------------------------------------- 1 | const UI = require('./ui').UI; 2 | const c = require('ansi-colors'); 3 | 4 | exports.Logger = class { 5 | static inject() { return [UI]; } 6 | 7 | constructor(ui) { 8 | this.ui = ui; 9 | } 10 | 11 | debug(logger, message) { 12 | this.log(logger, c.bold('DEBUG'), message, arguments); 13 | } 14 | 15 | info(logger, message) { 16 | this.log(logger, c.bold('INFO'), message, arguments); 17 | } 18 | 19 | warn(logger, message) { 20 | this.log(logger, c.bgYellow('WARN'), message, arguments); 21 | } 22 | 23 | error(logger, message) { 24 | this.log(logger, c.bgRed('ERROR'), message, arguments); 25 | } 26 | 27 | log(logger, level, message, rest) { 28 | let msg = `${level} [${logger.id}] ${message}`; 29 | let args = Array.prototype.slice.call(rest, 2); 30 | 31 | if (args.length > 0) { 32 | msg += ` ${args.map(x => JSON.stringify(x)).join(' ')}`; 33 | } 34 | 35 | this.ui.log(msg); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /lib/package-managers/base-package-manager.js: -------------------------------------------------------------------------------- 1 | const {spawn} = require('child_process'); 2 | const npmWhich = require('npm-which'); 3 | const isWindows = process.platform === "win32"; 4 | 5 | exports.BasePackageManager = class { 6 | constructor(executableName) { 7 | this.executableName = executableName; 8 | } 9 | 10 | install(packages = [], workingDirectory = process.cwd(), command = 'install') { 11 | return this.run(command, packages, workingDirectory); 12 | } 13 | 14 | run(command, args = [], workingDirectory = process.cwd()) { 15 | let executable = this.getExecutablePath(workingDirectory); 16 | if (isWindows) { 17 | executable = JSON.stringify(executable); // Add quotes around path 18 | } 19 | 20 | return new Promise((resolve, reject) => { 21 | this.proc = spawn( 22 | executable, 23 | [command, ...args], 24 | { stdio: "inherit", cwd: workingDirectory, shell: isWindows } 25 | ) 26 | .on('close', resolve) 27 | .on('error', reject); 28 | }); 29 | } 30 | 31 | getExecutablePath(directory) { 32 | try { 33 | return npmWhich(directory).sync(this.executableName); 34 | } catch { 35 | return null; 36 | } 37 | } 38 | 39 | isAvailable(directory) { 40 | return !!this.getExecutablePath(directory); 41 | } 42 | }; 43 | 44 | exports.default = exports.BasePackageManager; 45 | -------------------------------------------------------------------------------- /lib/package-managers/npm.js: -------------------------------------------------------------------------------- 1 | const BasePackageManager = require('./base-package-manager').default; 2 | 3 | exports.NPM = class extends BasePackageManager { 4 | constructor() { 5 | super('npm'); 6 | } 7 | }; 8 | 9 | exports.default = exports.NPM; 10 | -------------------------------------------------------------------------------- /lib/package-managers/yarn.js: -------------------------------------------------------------------------------- 1 | const BasePackageManager = require('./base-package-manager').default; 2 | 3 | exports.Yarn = class extends BasePackageManager { 4 | constructor() { 5 | super('yarn'); 6 | } 7 | 8 | install(packages = [], workingDirectory = process.cwd()) { 9 | return super.install(packages, workingDirectory, !packages.length ? 'install' : 'add'); 10 | } 11 | }; 12 | 13 | exports.default = exports.Yarn; 14 | -------------------------------------------------------------------------------- /lib/pretty-choices.js: -------------------------------------------------------------------------------- 1 | const {wordWrap} = require('enquirer/lib/utils'); 2 | const getTtySize = require('./get-tty-size'); 3 | 4 | // Check all values, indent hint line. 5 | module.exports = function(...choices) { 6 | if (choices.length && Array.isArray(choices[0])) { 7 | choices = choices[0]; 8 | } 9 | return choices.map(c => { 10 | // for {role: 'separator'} 11 | if (c.role) return c; 12 | 13 | // displayName and description are for compatibility in lib/ui.js 14 | const value = c.value || c.displayName; 15 | const message = c.title || c.message || c.displayName; 16 | const hint = c.hint || c.description; 17 | 18 | if (typeof value !== 'string') { 19 | throw new Error(`Value type ${typeof value} is not supported. Only support string value.`); 20 | } 21 | 22 | const choice = { 23 | value: value, 24 | // TODO after https://github.com/enquirer/enquirer/issues/115 25 | // add ${idx + 1}. in front of message 26 | message: message, 27 | // https://github.com/enquirer/enquirer/issues/121 28 | name: c.message, 29 | if: c.if // used by lib/workflow/run-questionnaire 30 | }; 31 | 32 | if (hint) { 33 | // indent hint, need to adjust indent after ${idx + 1}. was turned on 34 | choice.hint = '\n' + wordWrap(hint, {indent: ' ', width: getTtySize().width}); 35 | } 36 | 37 | return choice; 38 | }); 39 | }; 40 | -------------------------------------------------------------------------------- /lib/project-item.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('./file-system'); 3 | const Utils = require('./build/utils'); 4 | 5 | // Legacy code, kept only for supporting `au generate` 6 | exports.ProjectItem = class { 7 | constructor(name, isDirectory) { 8 | this.name = name; 9 | this.isDirectory = !!isDirectory; 10 | } 11 | 12 | get children() { 13 | if (!this._children) { 14 | this._children = []; 15 | } 16 | 17 | return this._children; 18 | } 19 | 20 | add() { 21 | if (!this.isDirectory) { 22 | throw new Error('You cannot add items to a non-directory.'); 23 | } 24 | 25 | for (let i = 0; i < arguments.length; ++i) { 26 | let child = arguments[i]; 27 | 28 | if (this.children.indexOf(child) !== -1) { 29 | continue; 30 | } 31 | 32 | child.parent = this; 33 | this.children.push(child); 34 | } 35 | 36 | return this; 37 | } 38 | 39 | calculateRelativePath(fromLocation) { 40 | if (this === fromLocation) { 41 | return ''; 42 | } 43 | 44 | let parentRelativePath = (this.parent && this.parent !== fromLocation) 45 | ? this.parent.calculateRelativePath(fromLocation) 46 | : ''; 47 | 48 | return path.posix.join(parentRelativePath, this.name); 49 | } 50 | 51 | create(relativeTo) { 52 | let fullPath = relativeTo ? this.calculateRelativePath(relativeTo) : this.name; 53 | 54 | // Skip empty folder 55 | if (this.isDirectory && this.children.length) { 56 | return fs.stat(fullPath) 57 | .then(result => result) 58 | .catch(() => fs.mkdir(fullPath)) 59 | .then(() => Utils.runSequentially(this.children, child => child.create(fullPath))); 60 | } 61 | 62 | if (this.text) { 63 | return fs.writeFile(fullPath, this.text); 64 | } 65 | 66 | return Promise.resolve(); 67 | } 68 | 69 | 70 | setText(text) { 71 | this.text = text; 72 | return this; 73 | } 74 | 75 | getText() { 76 | return this.text; 77 | } 78 | 79 | static text(name, text) { 80 | return new exports.ProjectItem(name, false).setText(text); 81 | } 82 | 83 | static directory(p) { 84 | return new exports.ProjectItem(p, true); 85 | } 86 | }; 87 | -------------------------------------------------------------------------------- /lib/project.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('./file-system'); 3 | const _ = require('lodash'); 4 | const ProjectItem = require('./project-item').ProjectItem; 5 | 6 | exports.Project = class { 7 | static establish(dir) { 8 | process.chdir(dir); 9 | 10 | return fs.readFile(path.join(dir, 'aurelia_project', 'aurelia.json')).then(model => { 11 | return fs.readFile(path.join(dir, 'package.json')).then(pack => { 12 | return new exports.Project(dir, JSON.parse(model.toString()), JSON.parse(pack.toString())); 13 | }); 14 | }); 15 | } 16 | 17 | constructor(directory, model, pack) { 18 | this.directory = directory; 19 | this.model = model; 20 | this.package = pack; 21 | this.taskDirectory = path.join(directory, 'aurelia_project/tasks'); 22 | this.generatorDirectory = path.join(directory, 'aurelia_project/generators'); 23 | this.aureliaJSONPath = path.join(directory, 'aurelia_project', 'aurelia.json'); 24 | 25 | this.locations = Object.keys(model.paths).map(key => { 26 | this[key] = ProjectItem.directory(model.paths[key]); 27 | 28 | if (key !== 'root') { 29 | this[key] = ProjectItem.directory(model.paths[key]); 30 | this[key].parent = this.root; 31 | } 32 | 33 | return this[key]; 34 | }); 35 | this.locations.push(this.generators = ProjectItem.directory('aurelia_project/generators')); 36 | this.locations.push(this.tasks = ProjectItem.directory('aurelia_project/tasks')); 37 | } 38 | 39 | // Legacy code. This code and those ProjectItem.directory above, were kept only 40 | // for supporting `au generate` 41 | commitChanges() { 42 | return Promise.all(this.locations.map(x => x.create(this.directory))); 43 | } 44 | 45 | makeFileName(name) { 46 | return _.kebabCase(name); 47 | } 48 | 49 | makeClassName(name) { 50 | const camel = _.camelCase(name); 51 | return camel.slice(0, 1).toUpperCase() + camel.slice(1); 52 | } 53 | 54 | makeFunctionName(name) { 55 | return _.camelCase(name); 56 | } 57 | 58 | installTranspiler() { 59 | switch (this.model.transpiler.id) { 60 | case 'babel': 61 | installBabel.call(this); 62 | break; 63 | case 'typescript': 64 | installTypeScript(); 65 | break; 66 | default: 67 | throw new Error(`${this.model.transpiler.id} is not a supported transpiler.`); 68 | } 69 | } 70 | 71 | getExport(m, name) { 72 | return name ? m[name] : m.default; 73 | } 74 | 75 | getGeneratorMetadata() { 76 | return getMetadata(this.generatorDirectory); 77 | } 78 | 79 | getTaskMetadata() { 80 | return getMetadata(this.taskDirectory); 81 | } 82 | 83 | resolveGenerator(name) { 84 | let potential = path.join(this.generatorDirectory, `${name}${this.model.transpiler.fileExtension}`); 85 | return fs.stat(potential).then(() => potential).catch(() => null); 86 | } 87 | 88 | resolveTask(name) { 89 | let potential = path.join(this.taskDirectory, `${name}${this.model.transpiler.fileExtension}`); 90 | return fs.stat(potential).then(() => potential).catch(() => null); 91 | } 92 | }; 93 | 94 | function getMetadata(dir) { 95 | return fs.readdir(dir).then(files => { 96 | return Promise.all( 97 | files 98 | .sort() 99 | .map(file => path.join(dir, file)) 100 | .filter(file => path.extname(file) === '.json') 101 | .map(file => fs.readFile(file).then(data => JSON.parse(data.toString()))) 102 | ); 103 | }); 104 | } 105 | 106 | function installBabel() { 107 | require('@babel/register')({ 108 | babelrc: false, 109 | configFile: false, 110 | plugins: [ 111 | ['@babel/plugin-proposal-decorators', { legacy: true }], 112 | ['@babel/plugin-transform-class-properties', { loose: true }], 113 | ['@babel/plugin-transform-modules-commonjs', {loose: true}] 114 | ], 115 | only: [/aurelia_project/] 116 | }); 117 | } 118 | 119 | function installTypeScript() { 120 | let ts = require('typescript'); 121 | 122 | let json = require.extensions['.json']; 123 | delete require.extensions['.json']; 124 | 125 | require.extensions['.ts'] = function(module, filename) { 126 | let source = fs.readFileSync(filename); 127 | let result = ts.transpile(source, { 128 | module: ts.ModuleKind.CommonJS, 129 | declaration: false, 130 | noImplicitAny: false, 131 | noResolve: true, 132 | removeComments: true, 133 | noLib: false, 134 | emitDecoratorMetadata: true, 135 | experimentalDecorators: true 136 | }); 137 | 138 | return module._compile(result, filename); 139 | }; 140 | 141 | require.extensions['.json'] = json; 142 | } 143 | -------------------------------------------------------------------------------- /lib/resources/logo.txt: -------------------------------------------------------------------------------- 1 | _ _ ____ _ ___ 2 | __ _ _ _ _ __ ___| (_) __ _ / ___| | |_ _| 3 | / _` | | | | '__/ _ \ | |/ _` | | | | | | | 4 | | (_| | |_| | | | __/ | | (_| | | |___| |___ | | 5 | \__,_|\__,_|_| \___|_|_|\__,_| \____|_____|___| 6 | -------------------------------------------------------------------------------- /lib/resources/scripts/configure-bluebird-no-long-stacktraces.js: -------------------------------------------------------------------------------- 1 | //Configure Bluebird Promises. 2 | Promise.config({ 3 | longStackTraces: false, 4 | warnings: { 5 | wForgottenReturn: false 6 | } 7 | }); 8 | -------------------------------------------------------------------------------- /lib/resources/scripts/configure-bluebird.js: -------------------------------------------------------------------------------- 1 | //Configure Bluebird Promises. 2 | Promise.config({ 3 | warnings: { 4 | wForgottenReturn: false 5 | } 6 | }); 7 | -------------------------------------------------------------------------------- /lib/resources/scripts/keep-them.md: -------------------------------------------------------------------------------- 1 | Although new app doesn't use bluebird anymore, need to keep the two bluebird config files for their usage in existing apps. 2 | -------------------------------------------------------------------------------- /lib/string.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const c = require('ansi-colors'); 3 | const {wordWrap} = require('enquirer/lib/utils'); 4 | 5 | exports.buildFromMetadata = function(metadata, width) { 6 | let text = ''; 7 | metadata.forEach(json => text += transformCommandToStyledText(json, width)); 8 | return text; 9 | }; 10 | 11 | function transformCommandToStyledText(json, width) { 12 | const indent = ' '.repeat(4); 13 | 14 | let text = c.magenta.bold(json.name); 15 | width = width || 1000; 16 | 17 | if (json.parameters) { 18 | json.parameters.forEach(parameter => { 19 | if (parameter.optional) { 20 | text += ' ' + c.dim(parameter.name); 21 | } else { 22 | text += ' ' + parameter.name; 23 | } 24 | }); 25 | } 26 | 27 | if (json.flags) { 28 | json.flags.forEach(flag => { 29 | text += ' ' + c.yellow('--' + flag.name); 30 | 31 | if (flag.type !== 'boolean') { 32 | text += ' value'; 33 | } 34 | }); 35 | } 36 | 37 | text += os.EOL + os.EOL; 38 | text += wordWrap(json.description, {indent, width}); 39 | 40 | if (json.parameters) { 41 | json.parameters.forEach(parameter => { 42 | text += os.EOL + os.EOL; 43 | let parameterInfo = parameter.name; 44 | 45 | if (parameter.optional) { 46 | parameterInfo += ' (optional)'; 47 | } 48 | 49 | parameterInfo = c.blue(parameterInfo) + ' - ' + parameter.description; 50 | text += wordWrap(parameterInfo, {indent, width}); 51 | }); 52 | } 53 | 54 | if (json.flags) { 55 | json.flags.forEach(flag => { 56 | text += os.EOL + os.EOL; 57 | let flagInfo = c.yellow('--' + flag.name); 58 | flagInfo += ' - ' + flag.description; 59 | text += wordWrap(flagInfo, {indent, width}); 60 | }); 61 | } 62 | 63 | text += os.EOL + os.EOL; 64 | 65 | return text; 66 | } 67 | 68 | -------------------------------------------------------------------------------- /lib/ui.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const fs = require('./file-system'); 3 | const {wordWrap} = require('enquirer/lib/utils'); 4 | const getTtySize = require('./get-tty-size'); 5 | const {Input, Select} = require('enquirer'); 6 | const prettyChoices = require('./pretty-choices'); 7 | const {Writable} = require('stream'); 8 | const _ = require('lodash'); 9 | 10 | exports.UI = class { }; 11 | 12 | exports.ConsoleUI = class { 13 | constructor(cliOptions) { 14 | this.cliOptions = cliOptions; 15 | } 16 | 17 | log(text, indent) { 18 | if (indent !== undefined) { 19 | text = wordWrap(text, {indent, width: this.getWidth()}); 20 | } 21 | return new Promise(resolve => { 22 | console.log(text); 23 | resolve(); 24 | }); 25 | } 26 | 27 | ensureAnswer(answer, question, suggestion) { 28 | return this._ensureAnswer(answer, question, suggestion); 29 | } 30 | 31 | // _debug is used to pass in answers for prompts. 32 | async _ensureAnswer(answer, question, suggestion, _debug = []) { 33 | if (answer) return answer; 34 | return await this._question(question, suggestion, undefined, _debug); 35 | } 36 | 37 | question(question, options, defaultValue) { 38 | return this._question(question, options, defaultValue); 39 | } 40 | 41 | // _debug is used to pass in answers for prompts. 42 | async _question(question, options, defaultValue, _debug = []) { 43 | let opts; 44 | let PromptType; 45 | if (!options || typeof options === 'string') { 46 | opts = { 47 | message: question, 48 | initial: options || '', 49 | result: r => r.trim(), 50 | validate: r => _.trim(r) ? true : 'Please provide an answer' 51 | }; 52 | PromptType = Input; 53 | } else { 54 | options = options.filter(x => includeOption(this.cliOptions, x)); 55 | 56 | if (options.length === 1) { 57 | return options[0].value || options[0].displayName; 58 | } 59 | 60 | opts = { 61 | type: 'select', 62 | name: 'answer', 63 | message: question, 64 | initial: defaultValue || options[0].value || options[0].displayName, 65 | choices: prettyChoices(options), 66 | // https://github.com/enquirer/enquirer/issues/121#issuecomment-468413408 67 | result(name) { 68 | return this.map(name)[name]; 69 | } 70 | }; 71 | PromptType = Select; 72 | } 73 | 74 | if (_debug && _debug.length) { 75 | // Silent output in debug mode 76 | opts.stdout = new Writable({write(c, e, cb) {cb();}}); 77 | } 78 | 79 | return await _run(new PromptType(opts), _debug); 80 | } 81 | 82 | multiselect(question, options) { 83 | return this._multiselect(question, options); 84 | } 85 | 86 | // _debug is used to pass in answers for prompts. 87 | async _multiselect(question, options, _debug = []) { 88 | options = options.filter(x => includeOption(this.cliOptions, x)); 89 | 90 | const opts = { 91 | multiple: true, 92 | message: question, 93 | choices: prettyChoices(options), 94 | validate: results => results.length === 0 ? 'Need at least one selection' : true, 95 | // https://github.com/enquirer/enquirer/issues/121#issuecomment-468413408 96 | result(names) { 97 | return Object.values(this.map(names)); 98 | } 99 | }; 100 | 101 | if (_debug && _debug.length) { 102 | // Silent output in debug mode 103 | opts.stdout = new Writable({write(c, e, cb) {cb();}}); 104 | } 105 | 106 | return await _run(new Select(opts), _debug); 107 | } 108 | 109 | getWidth() { 110 | return getTtySize().width; 111 | } 112 | 113 | getHeight() { 114 | return getTtySize().height; 115 | } 116 | 117 | displayLogo() { 118 | if (this.getWidth() < 50) { 119 | return this.log('Aurelia CLI' + os.EOL); 120 | } 121 | 122 | let logoLocation = require.resolve('./resources/logo.txt'); 123 | 124 | return fs.readFile(logoLocation).then(logo => { 125 | this.log(logo.toString()); 126 | }); 127 | } 128 | }; 129 | 130 | function includeOption(cliOptions, option) { 131 | if (option.disabled) { 132 | return false; 133 | } 134 | 135 | if (option.flag) { 136 | return cliOptions.hasFlag(option.flag); 137 | } 138 | 139 | return true; 140 | } 141 | 142 | async function _run(prompt, _debug = []) { 143 | if (_debug && _debug.length) { 144 | prompt.once('run', async() => { 145 | for (let d = 0, dd = _debug.length; d < dd; d++) { 146 | let debugChoice = _debug[d]; 147 | 148 | if (typeof debugChoice === 'number') { 149 | // choice index is 1-based. 150 | while (debugChoice-- > 0) { 151 | if (debugChoice) { 152 | await prompt.keypress(null, {name: 'down'}); 153 | } else { 154 | await prompt.submit(); 155 | } 156 | } 157 | } else if (typeof debugChoice === 'string') { 158 | for (let i = 0, ii = debugChoice.length; i < ii; i++) { 159 | await prompt.keypress(debugChoice[i]); 160 | } 161 | await prompt.submit(); 162 | } else if (typeof debugChoice === 'function') { 163 | await debugChoice(prompt); 164 | } 165 | } 166 | }); 167 | } 168 | 169 | return await prompt.run(); 170 | } 171 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aurelia-cli", 3 | "version": "3.0.4", 4 | "description": "The command line tooling for Aurelia.", 5 | "keywords": [ 6 | "aurelia", 7 | "cli", 8 | "bundle", 9 | "scaffold" 10 | ], 11 | "homepage": "http://aurelia.io", 12 | "bugs": { 13 | "url": "https://github.com/aurelia/cli/issues" 14 | }, 15 | "bin": { 16 | "aurelia": "bin/aurelia-cli.js", 17 | "au": "bin/aurelia-cli.js" 18 | }, 19 | "scripts": { 20 | "lint": "eslint lib spec", 21 | "pretest": "npm run lint", 22 | "test": "jasmine", 23 | "coverage": "c8 jasmine", 24 | "test:watch": "nodemon -x 'npm test'", 25 | "preversion": "npm test", 26 | "version": "standard-changelog && git add CHANGELOG.md", 27 | "postversion": "git push && git push --tags && npm publish" 28 | }, 29 | "license": "MIT", 30 | "author": "Rob Eisenberg (http://robeisenberg.com/)", 31 | "main": "lib/index.js", 32 | "files": [ 33 | "bin", 34 | "lib" 35 | ], 36 | "repository": { 37 | "type": "git", 38 | "url": "https://github.com/aurelia/cli" 39 | }, 40 | "dependencies": { 41 | "@babel/core": "^7.26.0", 42 | "@babel/plugin-proposal-decorators": "^7.25.9", 43 | "@babel/plugin-transform-class-properties": "^7.25.9", 44 | "@babel/plugin-transform-modules-amd": "^7.25.9", 45 | "@babel/plugin-transform-modules-commonjs": "^7.25.9", 46 | "@babel/register": "^7.25.9", 47 | "ansi-colors": "^4.1.3", 48 | "assert": "^2.1.0", 49 | "aurelia-dependency-injection": "^1.6.1", 50 | "aurelia-logging": "^1.5.2", 51 | "aurelia-polyfills": "^1.3.4", 52 | "browserify-zlib": "^0.2.0", 53 | "buffer": "^6.0.3", 54 | "concat-with-sourcemaps": "^1.1.0", 55 | "console-browserify": "^1.2.0", 56 | "constants-browserify": "^1.0.0", 57 | "convert-source-map": "^2.0.0", 58 | "crypto-browserify": "^3.12.1", 59 | "domain-browser": "^5.7.0", 60 | "enquirer": "^2.4.1", 61 | "events": "^3.3.0", 62 | "fs-browser-stub": "^1.0.1", 63 | "gulp": "^4.0.2", 64 | "htmlparser2": "^9.1.0", 65 | "https-browserify": "^1.0.0", 66 | "lodash": "^4.17.21", 67 | "map-stream": "0.0.7", 68 | "meriyah": "^6.0.2", 69 | "minimatch": "^10.0.1", 70 | "npm-which": "^3.0.1", 71 | "os-browserify": "^0.3.0", 72 | "path-browserify": "1.0.1", 73 | "process": "^0.11.10", 74 | "punycode": "^2.3.1", 75 | "querystring-browser-stub": "^1.0.0", 76 | "readable-stream": "^4.5.2", 77 | "resolve": "^1.22.8", 78 | "semver": "^7.6.3", 79 | "stream-browserify": "^3.0.0", 80 | "stream-http": "^3.2.0", 81 | "string_decoder": "^1.3.0", 82 | "terser": "^5.36.0", 83 | "timers-browserify": "^2.0.12", 84 | "tty-browserify": "0.0.1", 85 | "typescript": "^5.6.3", 86 | "url": "^0.11.4", 87 | "util": "^0.12.5", 88 | "vm-browserify": "^1.1.2" 89 | }, 90 | "devDependencies": { 91 | "@types/node": "^22.8.1", 92 | "c8": "^10.1.2", 93 | "eslint": "^9.13.0", 94 | "globals": "^15.11.0", 95 | "jasmine": "^5.4.0", 96 | "jasmine-spec-reporter": "^7.0.0", 97 | "nodemon": "^3.1.7", 98 | "standard-changelog": "^6.0.0", 99 | "yargs": "^17.7.2" 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /spec/helpers/polyfills.js: -------------------------------------------------------------------------------- 1 | require('aurelia-polyfills'); 2 | -------------------------------------------------------------------------------- /spec/helpers/reporter.js: -------------------------------------------------------------------------------- 1 | const SpecReporter = require('jasmine-spec-reporter').SpecReporter; 2 | 3 | jasmine.getEnv().clearReporters(); // remove default reporter logs 4 | jasmine.getEnv().addReporter(new SpecReporter({ // add jasmine-spec-reporter 5 | spec: { 6 | displayPending: true 7 | } 8 | })); 9 | -------------------------------------------------------------------------------- /spec/lib/build/dependency-description.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const DependencyDescription = require('../../../lib/build/dependency-description').DependencyDescription; 3 | 4 | describe('The DependencyDescription', () => { 5 | let sut; 6 | 7 | beforeEach(() => { 8 | sut = new DependencyDescription('foo', 'npm'); 9 | }); 10 | 11 | it('gets mainId, calculates main path', () => { 12 | sut.loaderConfig = { 13 | name: 'foo', 14 | path: '../node_modules/foo', 15 | main: 'dist/bar'}; 16 | expect(sut.mainId).toBe('foo/dist/bar'); 17 | expect(sut.calculateMainPath('src')).toBe(path.join(process.cwd(), 'node_modules/foo/dist/bar.js')); 18 | }); 19 | 20 | it('gets empty browser replacement if no browser replacement defined', () => { 21 | sut.metadata = {}; 22 | expect(sut.browserReplacement()).toBeUndefined(); 23 | sut.metadata = {browser: 'dist/browser.js'}; 24 | expect(sut.browserReplacement()).toBeUndefined(); 25 | }); 26 | 27 | it('gets browser replacement', () => { 28 | sut.metadata = { 29 | browser: { 30 | 'module-a': false, 31 | 'module/b': 'shims/module/b.js', 32 | './server/only.js': './shims/server-only.js' 33 | } 34 | }; 35 | expect(sut.browserReplacement()).toEqual({ 36 | 'module-a': false, 37 | 'module/b': './shims/module/b', 38 | './server/only': './shims/server-only' 39 | }); 40 | }); 41 | 42 | it('gets browser replacement but leave . for main replacement', () => { 43 | sut.metadata = { 44 | browser: { 45 | "readable-stream": "./lib/readable-stream-browser.js", 46 | ".": "dist/jszip.min.js" 47 | } 48 | }; 49 | expect(sut.browserReplacement()).toEqual({ 50 | "readable-stream": "./lib/readable-stream-browser" 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /spec/lib/build/inject-css.spec.js: -------------------------------------------------------------------------------- 1 | const fixupCSSUrls = require('../../../lib/build/inject-css').fixupCSSUrls; 2 | 3 | // tests partly copied from 4 | // https://github.com/webpack-contrib/style-loader/blob/master/test/fixUrls.test.js 5 | describe('fixupCSSUrls', () => { 6 | it('throws on null/undefined', () => { 7 | expect(() => fixupCSSUrls('foo/bar', null)).toThrow(); 8 | expect(() => fixupCSSUrls('foo/bar', undefined)).toThrow(); 9 | }); 10 | 11 | it('Blank css is not modified', () => { 12 | const css = ''; 13 | expect(fixupCSSUrls('foo/bar', css)).toBe(css); 14 | }); 15 | 16 | it('No url is not modified', () => { 17 | const css = 'body { }'; 18 | expect(fixupCSSUrls('foo/bar', css)).toBe(css); 19 | }); 20 | 21 | it("Full url isn't changed (no quotes)", () => { 22 | const css = 'body { background-image:url ( http://example.com/bg.jpg ); }'; 23 | expect(fixupCSSUrls('foo/bar', css)).toBe(css); 24 | }); 25 | 26 | it("Full url isn't changed (no quotes, spaces)", () => { 27 | const css = 'body { background-image:url ( http://example.com/bg.jpg ); }'; 28 | expect(fixupCSSUrls('foo/bar', css)).toBe(css); 29 | }); 30 | 31 | it("Full url isn't changed (double quotes)", () => { 32 | const css = 'body { background-image:url("http://example.com/bg.jpg"); }'; 33 | expect(fixupCSSUrls('foo/bar', css)).toBe(css); 34 | }); 35 | 36 | it("Full url isn't changed (double quotes, spaces)", () => { 37 | const css = 'body { background-image:url ( "http://example.com/bg.jpg" ); }'; 38 | expect(fixupCSSUrls('foo/bar', css)).toBe(css); 39 | }); 40 | 41 | it("Full url isn't changed (single quotes)", () => { 42 | const css = 'body { background-image:url(\'http://example.com/bg.jpg\'); }'; 43 | expect(fixupCSSUrls('foo/bar', css)).toBe(css); 44 | }); 45 | 46 | it("Full url isn't changed (single quotes, spaces)", () => { 47 | const css = 'body { background-image:url ( \'http://example.com/bg.jpg\' ); }'; 48 | expect(fixupCSSUrls('foo/bar', css)).toBe(css); 49 | }); 50 | 51 | it('Multiple full urls are not changed', () => { 52 | const css = "body { background-image:url(http://example.com/bg.jpg); }\ndiv.main { background-image:url ( 'https://www.anothersite.com/another.png' ); }"; 53 | expect(fixupCSSUrls('foo/bar', css)).toBe(css); 54 | }); 55 | 56 | it("Http url isn't changed", function() { 57 | const css = 'body { background-image:url(http://example.com/bg.jpg); }'; 58 | expect(fixupCSSUrls('foo/bar', css)).toBe(css); 59 | }); 60 | 61 | it("Https url isn't changed", function() { 62 | const css = 'body { background-image:url(https://example.com/bg.jpg); }'; 63 | expect(fixupCSSUrls('foo/bar', css)).toBe(css); 64 | }); 65 | 66 | it("HTTPS url isn't changed", function() { 67 | const css = 'body { background-image:url(HTTPS://example.com/bg.jpg); }'; 68 | expect(fixupCSSUrls('foo/bar', css)).toBe(css); 69 | }); 70 | 71 | it("File url isn't changed", function() { 72 | const css = 'body { background-image:url(file:///example.com/bg.jpg); }'; 73 | expect(fixupCSSUrls('foo/bar', css)).toBe(css); 74 | }); 75 | 76 | it("Double slash url isn't changed", function() { 77 | const css = 'body { background-image:url(//example.com/bg.jpg); }'; 78 | expect(fixupCSSUrls('foo/bar', css)).toBe(css); 79 | }); 80 | 81 | it("Image data uri url isn't changed", function() { 82 | const css = 'body { background-image:url(data:image/png;base64,qsrwABYuwNkimqm3gAAAABJRU5ErkJggg==); }'; 83 | expect(fixupCSSUrls('foo/bar', css)).toBe(css); 84 | }); 85 | 86 | it("Font data uri url isn't changed", function() { 87 | const css = 'body { background-image:url(data:application/x-font-woff;charset=utf-8;base64,qsrwABYuwNkimqm3gAAAABJRU5ErkJggg); }'; 88 | expect(fixupCSSUrls('foo/bar', css)).toBe(css); 89 | }); 90 | 91 | it('Relative url with dot slash', function() { 92 | const css = 'body { background-image:url(./c/d/bg.jpg); }'; 93 | const expected = "body { background-image:url('foo/c/d/bg.jpg'); }"; 94 | expect(fixupCSSUrls('foo/bar', css)).toBe(expected); 95 | }); 96 | 97 | it('Multiple relative urls', function() { 98 | const css = 'body { background-image:URL ( "./bg.jpg" ); }\ndiv.main { background-image:url(../c/d/bg.jpg); }'; 99 | const expected = "body { background-image:url('foo/bg.jpg'); }\ndiv.main { background-image:url('c/d/bg.jpg'); }"; 100 | expect(fixupCSSUrls('foo/bar', css)).toBe(expected); 101 | }); 102 | 103 | it("url with hash isn't changed", function() { 104 | const css = 'body { background-image:url(#bg.jpg); }'; 105 | expect(fixupCSSUrls('foo/bar', css)).toBe(css); 106 | }); 107 | 108 | it('Empty url should be skipped', function() { 109 | let css = 'body { background-image:url(); }'; 110 | expect(fixupCSSUrls('foo/bar', css)).toBe(css); 111 | css = 'body { background-image:url( ); }'; 112 | expect(fixupCSSUrls('foo/bar', css)).toBe(css); 113 | css = 'body { background-image:url(\n); }'; 114 | expect(fixupCSSUrls('foo/bar', css)).toBe(css); 115 | css = 'body { background-image:url(\'\'); }'; 116 | expect(fixupCSSUrls('foo/bar', css)).toBe(css); 117 | css = 'body { background-image:url(\' \'); }'; 118 | expect(fixupCSSUrls('foo/bar', css)).toBe(css); 119 | css = 'body { background-image:url(""); }'; 120 | expect(fixupCSSUrls('foo/bar', css)).toBe(css); 121 | css = 'body { background-image:url(" "); }'; 122 | expect(fixupCSSUrls('foo/bar', css)).toBe(css); 123 | }); 124 | 125 | it("Rooted url isn't changed", function() { 126 | let css = 'body { background-image:url(/bg.jpg); }'; 127 | expect(fixupCSSUrls('foo/bar', css)).toBe(css); 128 | css = 'body { background-image:url(/a/b/bg.jpg); }'; 129 | expect(fixupCSSUrls('foo/bar', css)).toBe(css); 130 | }); 131 | 132 | it("Doesn't break inline SVG", function() { 133 | const css = "body { background-image:url('data:image/svg+xml;charset=utf-8,'); }"; 134 | expect(fixupCSSUrls('foo/bar', css)).toBe(css); 135 | }); 136 | it("Doesn't break inline SVG with HTML comment", function() { 137 | const css = "body { background-image:url('data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22utf-8%22%3F%3E%0A%3C!--%20Comment%20--%3E%0A%3Csvg%3E%3C%2Fsvg%3E%0A'); }"; 138 | expect(fixupCSSUrls('foo/bar', css)).toBe(css); 139 | }); 140 | }); 141 | -------------------------------------------------------------------------------- /spec/lib/build/module-id-processor.spec.js: -------------------------------------------------------------------------------- 1 | const { toDotDot, fromDotDot, getAliases } = require('../../../lib/build/module-id-processor'); 2 | 3 | describe('module-id-processor', () => { 4 | const moduleId = '../src/elements/hello-world.ts'; 5 | const escapedModuleId = '__dot_dot__/src/elements/hello-world.ts'; 6 | const paths = { 7 | 'resources': '../src', 8 | 'elements': '../src/elements' 9 | }; 10 | 11 | describe('toDotDot', () => { 12 | it('should replace ../ in module id', () => { 13 | expect(toDotDot(moduleId)).toEqual(escapedModuleId); 14 | }); 15 | 16 | it('should replace multiple ../ in module id', () => { 17 | expect(toDotDot('../' + moduleId)).toEqual('__dot_dot__/' + escapedModuleId); 18 | }); 19 | }); 20 | 21 | describe('fromDotDot', () => { 22 | it('should convert moduleId to original path', () => { 23 | expect(fromDotDot(escapedModuleId)).toEqual(moduleId); 24 | }); 25 | 26 | it('should replace multiple ../ in moduleId', () => { 27 | expect(fromDotDot('__dot_dot__/' + escapedModuleId)).toEqual('../' + moduleId); 28 | }); 29 | }); 30 | 31 | describe('getAliases', () => { 32 | it('should return a single match', () => { 33 | expect(getAliases('../src/hello-world.ts', paths)).toEqual([ 34 | { fromId: 'resources/hello-world.ts', toId: '__dot_dot__/src/hello-world.ts' } 35 | ]); 36 | }); 37 | 38 | it('should return an empty array when no match is found', () => { 39 | expect(getAliases('no/match/hello-world.ts', paths)).toEqual([]); 40 | }); 41 | 42 | it('should return multiple matches', () => { 43 | expect(getAliases(moduleId, paths)).toEqual([ 44 | { fromId: 'resources/elements/hello-world.ts', toId: '__dot_dot__/src/elements/hello-world.ts' }, 45 | { fromId: 'elements/hello-world.ts', toId: '__dot_dot__/src/elements/hello-world.ts' } 46 | ]); 47 | }); 48 | 49 | it('should support different aliases with same paths', () => { 50 | const duplicatePaths = { 51 | ...paths, 52 | '@resources': '../src' 53 | }; 54 | 55 | expect(getAliases(moduleId, duplicatePaths)).toEqual([ 56 | { fromId: 'resources/elements/hello-world.ts', toId: '__dot_dot__/src/elements/hello-world.ts' }, 57 | { fromId: 'elements/hello-world.ts', toId: '__dot_dot__/src/elements/hello-world.ts' }, 58 | { fromId: '@resources/elements/hello-world.ts', toId: '__dot_dot__/src/elements/hello-world.ts' } 59 | ]); 60 | }); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /spec/lib/build/package-installer.spec.js: -------------------------------------------------------------------------------- 1 | const mockfs = require('../../mocks/mock-fs'); 2 | const PackageInstaller = require('../../../lib/build/package-installer').PackageInstaller; 3 | 4 | describe('The PackageInstaller', () => { 5 | let project; 6 | let sut; 7 | 8 | describe('when there is no yarn.lock file', () => { 9 | beforeEach(() => { 10 | project = {}; 11 | sut = new PackageInstaller(project); 12 | const fsConfig = {}; 13 | mockfs(fsConfig); 14 | }); 15 | 16 | afterEach(() => { 17 | mockfs.restore(); 18 | }); 19 | 20 | it('uses npm by default', () => { 21 | expect(sut.determinePackageManager()).toBe('npm'); 22 | }); 23 | 24 | it('uses npm if specified in project', () => { 25 | project.packageManager = 'npm'; 26 | expect(sut.determinePackageManager()).toBe('npm'); 27 | }); 28 | 29 | it('uses yarn if specified in project', () => { 30 | project.packageManager = 'yarn'; 31 | expect(sut.determinePackageManager()).toBe('yarn'); 32 | }); 33 | }); 34 | 35 | describe('when there is yarn.lock file', () => { 36 | beforeEach(() => { 37 | project = {}; 38 | sut = new PackageInstaller(project); 39 | const fsConfig = { 'yarn.lock': 'some-content'}; 40 | mockfs(fsConfig); 41 | }); 42 | 43 | afterEach(() => { 44 | mockfs.restore(); 45 | }); 46 | 47 | it('uses yarn if project did not specify, and there is yarn.lock file', () => { 48 | expect(sut.determinePackageManager()).toBe('yarn'); 49 | }); 50 | 51 | it('uses npm if specified in project, despite yarn.lock file', () => { 52 | project.packageManager = 'npm'; 53 | expect(sut.determinePackageManager()).toBe('npm'); 54 | }); 55 | 56 | it('uses yarn if specified in project, and there is yarn.lock file', () => { 57 | project.packageManager = 'yarn'; 58 | expect(sut.determinePackageManager()).toBe('yarn'); 59 | }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /spec/lib/build/source-inclusion.spec.js: -------------------------------------------------------------------------------- 1 | const BundlerMock = require('../../mocks/bundler'); 2 | const SourceInclusion = require('../../../lib/build/source-inclusion').SourceInclusion; 3 | const mockfs = require('../../mocks/mock-fs'); 4 | const Minimatch = require('minimatch').Minimatch; 5 | const path = require('path'); 6 | 7 | describe('the SourceInclusion module', () => { 8 | let bundler; 9 | 10 | beforeEach(() => { 11 | bundler = new BundlerMock(); 12 | }); 13 | 14 | afterEach(() => { 15 | mockfs.restore(); 16 | }); 17 | 18 | it('captures pattern and excludes', () => { 19 | let bundle = { 20 | bundler: bundler, 21 | addAlias: jasmine.createSpy('addAlias'), 22 | includes: [], 23 | excludes: ['**/*.css'], 24 | createMatcher: function(pattern) { 25 | return new Minimatch(pattern, { 26 | dot: true 27 | }); 28 | } 29 | }; 30 | 31 | let sut = new SourceInclusion(bundle, 'foo*.js'); 32 | sut.trySubsume({path: 'foo-bar.js'}); 33 | expect(sut.items.length).toBe(1); 34 | expect(sut.items[0].path).toBe('foo-bar.js'); 35 | expect(sut.items[0].includedBy).toBe(sut); 36 | expect(sut.items[0].includedIn).toBe(bundle); 37 | 38 | sut.trySubsume({path: 'fo-bar.js'}); 39 | expect(sut.items.length).toBe(1); 40 | 41 | sut.trySubsume({path: 'foo-bar.css'}); 42 | expect(sut.items.length).toBe(1); 43 | }); 44 | 45 | it('captures [pattern] and excludes', () => { 46 | let bundle = { 47 | bundler: bundler, 48 | addAlias: jasmine.createSpy('addAlias'), 49 | includes: [], 50 | excludes: ['**/*.css'], 51 | createMatcher: function(pattern) { 52 | return new Minimatch(pattern, { 53 | dot: true 54 | }); 55 | } 56 | }; 57 | 58 | let sut = new SourceInclusion(bundle, '[foo*.js]'); 59 | sut.trySubsume({path: 'foo-bar.js'}); 60 | expect(sut.items.length).toBe(1); 61 | expect(sut.items[0].path).toBe('foo-bar.js'); 62 | expect(sut.items[0].includedBy).toBe(sut); 63 | expect(sut.items[0].includedIn).toBe(bundle); 64 | 65 | sut.trySubsume({path: 'fo-bar.js'}); 66 | expect(sut.items.length).toBe(1); 67 | 68 | sut.trySubsume({path: 'foo-bar.css'}); 69 | expect(sut.items.length).toBe(1); 70 | }); 71 | 72 | it('getAllModuleIds gets all module ids, getAllFiles gets all items', () => { 73 | let bundle = { 74 | bundler: bundler, 75 | addAlias: jasmine.createSpy('addAlias'), 76 | includes: [], 77 | excludes: ['**/*.css'], 78 | createMatcher: function(pattern) { 79 | return new Minimatch(pattern, { 80 | dot: true 81 | }); 82 | } 83 | }; 84 | 85 | let sut = new SourceInclusion(bundle, '**/*.js'); 86 | sut.trySubsume({path: 'foo-bar.js', moduleId: 'foo-bar.js'}); 87 | sut.trySubsume({path: 'fop/bar.js', moduleId: 'fop/bar.js'}); 88 | 89 | expect(sut.getAllModuleIds().sort()).toEqual(['foo-bar.js', 'fop/bar.js']); 90 | expect(sut.getAllFiles()).toEqual([ 91 | {path: 'foo-bar.js', moduleId: 'foo-bar.js', includedBy: sut, includedIn: bundle}, 92 | {path: 'fop/bar.js', moduleId: 'fop/bar.js', includedBy: sut, includedIn: bundle} 93 | ]); 94 | }); 95 | 96 | it('addAllMatchingResources adds all matching files', done => { 97 | let bundle = { 98 | bundler: bundler, 99 | addAlias: jasmine.createSpy('addAlias'), 100 | includes: [], 101 | excludes: ['**/*.css'], 102 | createMatcher: function(pattern) { 103 | return new Minimatch(pattern, { 104 | dot: true 105 | }); 106 | } 107 | }; 108 | 109 | let sut = new SourceInclusion(bundle, '../node_modules/foo/**/*.js'); 110 | sut._getProjectRoot = () => 'src'; 111 | mockfs({ 112 | 'node_modules/foo/foo-bar.js': 'some-content', 113 | 'node_modules/foo/fop/bar.js': 'some-content' 114 | }); 115 | 116 | sut.addAllMatchingResources() 117 | .then(() => { 118 | expect(bundler.addFile).toHaveBeenCalledTimes(2); 119 | let arg0 = bundler.addFile.calls.argsFor(0); 120 | let arg1 = bundler.addFile.calls.argsFor(1); 121 | 122 | expect(arg0[1]).toEqual(sut); 123 | expect(arg1[1]).toEqual(sut); 124 | 125 | expect(arg0[0].path).toEqual(path.resolve('node_modules/foo/foo-bar.js')); 126 | expect(arg1[0].path).toEqual(path.resolve('node_modules/foo/fop/bar.js')); 127 | done(); 128 | }) 129 | .catch(e => done.fail(e)); 130 | }); 131 | }); 132 | -------------------------------------------------------------------------------- /spec/lib/build/stub-module.spec.js: -------------------------------------------------------------------------------- 1 | const stubModule = require('../../../lib/build/stub-module'); 2 | 3 | describe('StubCoreNodejsModule', () => { 4 | it('stubs some core module with subfix -browserify', () => { 5 | expect(stubModule('os', 'src')).toEqual({ 6 | name: 'os', 7 | path: '../node_modules/os-browserify' 8 | }); 9 | }); 10 | 11 | it('stubs domain', () => { 12 | expect(stubModule('domain', 'src')).toEqual({ 13 | name: 'domain', 14 | path: '../node_modules/domain-browser' 15 | }); 16 | }); 17 | 18 | it('stubs http', () => { 19 | expect(stubModule('http', 'src')).toEqual({ 20 | name: 'http', 21 | path: '../node_modules/stream-http' 22 | }); 23 | }); 24 | 25 | it('stubs querystring', () => { 26 | expect(stubModule('querystring', 'src')).toEqual({ 27 | name: 'querystring', 28 | path: '../node_modules/querystring-browser-stub' 29 | }); 30 | }); 31 | 32 | it('stubs fs', () => { 33 | expect(stubModule('fs', 'src')).toEqual({ 34 | name: 'fs', 35 | path: '../node_modules/fs-browser-stub' 36 | }); 37 | }); 38 | 39 | it('ignores sys', () => { 40 | expect(stubModule('sys', 'src')).toBeUndefined(); 41 | }); 42 | 43 | it('stubModule stubs zlib', () => { 44 | expect(stubModule('zlib', 'src')).toEqual({ 45 | name: 'zlib', 46 | path: '../node_modules/browserify-zlib' 47 | }); 48 | }); 49 | 50 | it('stubs empty module for some core module', () => { 51 | expect(stubModule('dns', 'src')).toBe('define(function(){return {};});'); 52 | }); 53 | 54 | it('stubs empty module for __ignore__', () => { 55 | expect(stubModule('__ignore__', 'src')).toBe('define(function(){return {};});'); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /spec/lib/build/utils.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const mockfs = require('../../mocks/mock-fs'); 3 | const Utils = require('../../../lib/build/utils'); 4 | 5 | describe('the Utils.runSequentially function', () => { 6 | it('calls the callback function for all items', (d) => { 7 | let items = [{ name: 'first' }, { name: 'second' }]; 8 | let cb = jasmine.createSpy('cb').and.returnValue(Promise.resolve()); 9 | Utils.runSequentially(items, cb).then(() => { 10 | expect(cb.calls.count()).toBe(2); 11 | expect(cb.calls.argsFor(0)[0].name).toBe('first'); 12 | expect(cb.calls.argsFor(1)[0].name).toBe('second'); 13 | d(); 14 | }); 15 | }); 16 | 17 | it('runs in sequence', (d) => { 18 | let items = [{ name: 'first' }, { name: 'second' }, { name: 'third' }]; 19 | let cb = jasmine.createSpy('cb').and.callFake((item) => { 20 | return new Promise(resolve => { 21 | if (item.name === 'first' || item.name === 'second') { 22 | setTimeout(() => resolve(), 200); 23 | } else { 24 | resolve(); 25 | } 26 | }); 27 | }); 28 | Utils.runSequentially(items, cb).then(() => { 29 | expect(cb.calls.argsFor(0)[0].name).toBe('first'); 30 | expect(cb.calls.argsFor(1)[0].name).toBe('second'); 31 | expect(cb.calls.argsFor(2)[0].name).toBe('third'); 32 | d(); 33 | }); 34 | }); 35 | 36 | it('handles empty items array', (done) => { 37 | let items = []; 38 | Utils.runSequentially(items, () => {}) 39 | .catch(e => { 40 | done.fail(e, '', 'expected no error'); 41 | throw e; 42 | }) 43 | .then(() => { 44 | done(); 45 | }); 46 | }); 47 | }); 48 | 49 | describe('the Utils.createBundleFileRegex function', () => { 50 | it('matches script tag with double quotes', () => { 51 | expect(''.match(Utils.createBundleFileRegex('vendor-bundle'))).not.toBeFalsy(); 52 | 53 | expect(''.replace(Utils.createBundleFileRegex('vendor-bundle'), 'vendor-bundle-123.js')).toBe(''); 54 | 55 | // dev to prod 56 | expect(''.replace(Utils.createBundleFileRegex('app-bundle'), 'app-bundle-123.js').replace(Utils.createBundleFileRegex('vendor-bundle'), 'vendor-bundle-abc.js')).toBe(''); 57 | 58 | // prod to dev 59 | expect(''.replace(Utils.createBundleFileRegex('app-bundle'), 'app-bundle.js').replace(Utils.createBundleFileRegex('vendor-bundle'), 'vendor-bundle.js')).toBe(''); 60 | }); 61 | it('matches script tag with single quotes', () => { 62 | expect(''.match(Utils.createBundleFileRegex('vendor-bundle'))).not.toBeFalsy(); 63 | }); 64 | it('matches script tag without quotes', () => { 65 | expect(''.match(Utils.createBundleFileRegex('vendor-bundle'))).not.toBeFalsy(); 66 | }); 67 | it('does not match other bundles', () => { 68 | expect(''.match(Utils.createBundleFileRegex('vendor-bundle'))).toBeFalsy(); 69 | }); 70 | }); 71 | 72 | describe('the Utils.moduleIdWithPlugin function', () => { 73 | it('generates requirejs style module id', () => { 74 | expect(Utils.moduleIdWithPlugin('foo/bar', 'plugin', 'require')).toBe('plugin!foo/bar'); 75 | }); 76 | 77 | it('generates systemjs style module id', () => { 78 | expect(Utils.moduleIdWithPlugin('foo/bar', 'plugin', 'system')).toBe('foo/bar!plugin'); 79 | }); 80 | 81 | it('complains unknown type', () => { 82 | expect(() => Utils.moduleIdWithPlugin('foo/bar', 'plugin', 'unknown')).toThrow(); 83 | }); 84 | }); 85 | 86 | describe('the Utils.couldMissGulpPreprocess function', () => { 87 | it('returns false for js/html/css files', () => { 88 | expect(Utils.couldMissGulpPreprocess('foo/bar')).toBeFalsy(); 89 | expect(Utils.couldMissGulpPreprocess('foo/bar.js')).toBeFalsy(); 90 | expect(Utils.couldMissGulpPreprocess('foo/bar.html')).toBeFalsy(); 91 | expect(Utils.couldMissGulpPreprocess('bar.css')).toBeFalsy(); 92 | }); 93 | 94 | it('returns true for unknown file extension', () => { 95 | expect(Utils.couldMissGulpPreprocess('foo/bar.json')).toBeTruthy(); 96 | expect(Utils.couldMissGulpPreprocess('foo/bar.yaml')).toBeTruthy(); 97 | }); 98 | }); 99 | 100 | describe('the Utils.nodejsLoad function', () => { 101 | beforeEach(() => { 102 | const fsConfig = {}; 103 | mockfs(fsConfig); 104 | }); 105 | 106 | afterEach(() => { 107 | mockfs.restore(); 108 | }); 109 | 110 | it('load file first', () => { 111 | const fsConfig = {}; 112 | fsConfig[path.join('foo', 'bar')] = 'bar'; 113 | fsConfig[path.join('foo', 'bar.js')] = 'js'; 114 | fsConfig[path.join('foo', 'bar.json')] = 'json'; 115 | mockfs(fsConfig); 116 | expect(Utils.nodejsLoad(path.resolve('foo', 'bar'))).toBe(path.resolve('foo', 'bar')); 117 | }); 118 | 119 | it('load .js file first', () => { 120 | const fsConfig = {}; 121 | fsConfig[path.join('foo', 'bar.js')] = 'js'; 122 | fsConfig[path.join('foo', 'bar.json')] = 'json'; 123 | mockfs(fsConfig); 124 | expect(Utils.nodejsLoad(path.resolve('foo', 'bar'))).toBe(path.resolve('foo', 'bar.js')); 125 | expect(Utils.nodejsLoad(path.resolve('foo', 'bar.js'))).toBe(path.resolve('foo', 'bar.js')); 126 | expect(Utils.nodejsLoad(path.resolve('foo', 'bar.json'))).toBe(path.resolve('foo', 'bar.json')); 127 | }); 128 | 129 | it('load .json file', () => { 130 | const fsConfig = {}; 131 | fsConfig[path.join('foo', 'bar.json')] = 'json'; 132 | mockfs(fsConfig); 133 | expect(Utils.nodejsLoad(path.resolve('foo', 'bar'))).toBe(path.resolve('foo', 'bar.json')); 134 | expect(Utils.nodejsLoad(path.resolve('foo', 'bar.json'))).toBe(path.resolve('foo', 'bar.json')); 135 | expect(Utils.nodejsLoad(path.resolve('foo', 'bar.js'))).toBeUndefined(); 136 | }); 137 | 138 | it('load directory', () => { 139 | const fsConfig = {}; 140 | fsConfig[path.join('foo', 'bar', 'index.js')] = 'bar/index'; 141 | fsConfig[path.join('foo', 'bar', 'index.json')] = 'bar/index.json'; 142 | mockfs(fsConfig); 143 | expect(Utils.nodejsLoad(path.resolve('foo', 'bar'))).toBe(path.resolve('foo', 'bar', 'index.js')); 144 | }); 145 | 146 | it('load directory .json', () => { 147 | const fsConfig = {}; 148 | fsConfig[path.join('foo', 'bar', 'index.json')] = 'bar/index.json'; 149 | mockfs(fsConfig); 150 | expect(Utils.nodejsLoad(path.resolve('foo', 'bar'))).toBe(path.resolve('foo', 'bar', 'index.json')); 151 | }); 152 | 153 | it('load directory with package.json', () => { 154 | const fsConfig = {}; 155 | fsConfig[path.join('foo', 'bar', 'package.json')] = '{"main": "lo.js"}'; 156 | fsConfig[path.join('foo', 'bar', 'lo.js')] = 'bar/lo.js'; 157 | mockfs(fsConfig); 158 | expect(Utils.nodejsLoad(path.resolve('foo', 'bar'))).toBe(path.resolve('foo', 'bar', 'lo.js')); 159 | }); 160 | 161 | it('load directory with package.json browser field', () => { 162 | const fsConfig = {}; 163 | fsConfig[path.join('foo', 'bar', 'package.json')] = '{"main": "lo2.js", "browser": "lo.js"}'; 164 | fsConfig[path.join('foo', 'bar', 'lo.js')] = 'bar/lo.js'; 165 | mockfs(fsConfig); 166 | expect(Utils.nodejsLoad(path.resolve('foo', 'bar'))).toBe(path.resolve('foo', 'bar', 'lo.js')); 167 | }); 168 | 169 | it('load directory with package.json browser "." mapping', () => { 170 | const fsConfig = {}; 171 | fsConfig[path.join('foo', 'bar', 'package.json')] = '{"main": "lo2.js", "browser": {".": "lo.js"}}'; 172 | fsConfig[path.join('foo', 'bar', 'lo.js')] = 'bar/lo.js'; 173 | mockfs(fsConfig); 174 | expect(Utils.nodejsLoad(path.resolve('foo', 'bar'))).toBe(path.resolve('foo', 'bar', 'lo.js')); 175 | }); 176 | 177 | it('load directory with package.json browser field', () => { 178 | const fsConfig = {}; 179 | fsConfig[path.join('foo', 'bar', 'package.json')] = '{"main": "lo2.js", "module": "lo.js"}'; 180 | fsConfig[path.join('foo', 'bar', 'lo.js')] = 'bar/lo.js'; 181 | mockfs(fsConfig); 182 | expect(Utils.nodejsLoad(path.resolve('foo', 'bar'))).toBe(path.resolve('foo', 'bar', 'lo.js')); 183 | }); 184 | 185 | it('load directory with package.json, case2', () => { 186 | const fsConfig = {}; 187 | fsConfig[path.join('foo', 'bar', 'package.json')] = '{"main": "lo.js"}'; 188 | fsConfig[path.join('foo', 'bar', 'lo.js', 'index.js')] = 'bar/lo.js/index.js'; 189 | mockfs(fsConfig); 190 | expect(Utils.nodejsLoad(path.resolve('foo', 'bar'))).toBe(path.resolve('foo', 'bar', 'lo.js', 'index.js')); 191 | }); 192 | }); 193 | 194 | describe('the Utils.removeJsExtension function', () => { 195 | it('keep other extension', () => { 196 | expect(Utils.removeJsExtension('a.html')).toBe('a.html'); 197 | expect(Utils.removeJsExtension('c/d.css')).toBe('c/d.css'); 198 | expect(Utils.removeJsExtension('c/d.min')).toBe('c/d.min'); 199 | }); 200 | it('strips .js extension', () => { 201 | expect(Utils.removeJsExtension('a.js')).toBe('a'); 202 | expect(Utils.removeJsExtension('c/d.js')).toBe('c/d'); 203 | expect(Utils.removeJsExtension('c/d.min.js')).toBe('c/d.min'); 204 | }); 205 | }); 206 | -------------------------------------------------------------------------------- /spec/lib/cli-options.spec.ts: -------------------------------------------------------------------------------- 1 | const mockfs = require('../mocks/mock-fs'); 2 | 3 | describe('The cli-options', () => { 4 | let cliOptions; 5 | 6 | beforeEach(() => { 7 | const fsConfig = { 8 | 'aurelia_project/environments/dev.js': 'content', 9 | 'aurelia_project/environments/stage.js': 'content', 10 | 'aurelia_project/environments/prod.js': 'content' 11 | }; 12 | mockfs(fsConfig); 13 | 14 | cliOptions = new(require('../../lib/cli-options').CLIOptions)(); 15 | }); 16 | 17 | afterEach(() => { 18 | delete process.env.NODE_ENV; 19 | mockfs.restore(); 20 | }); 21 | 22 | describe('The CLIOptions', () => { 23 | it('gets the right task name', () => { 24 | const paths = [ 25 | 'C:\\some\\path to\\project\\aurelia_project\\tasks\\', 26 | '/some/path to/project/aurelia_project/tasks/' 27 | ]; 28 | const files = { 29 | 'run.ts': 'run', 30 | 'run.js': 'run' 31 | }; 32 | for (let path of paths) { 33 | for (let file of Object.keys(files)) { 34 | cliOptions.taskPath = `${path}${file}`; 35 | expect(cliOptions.taskName()).toBe(files[file]); 36 | } 37 | } 38 | }); 39 | 40 | it('gets env from arg --env', () => { 41 | cliOptions.args = ['build', '--env', 'prod']; 42 | expect(cliOptions.getEnvironment()).toBe('prod'); 43 | }); 44 | 45 | it('gets env from NODE_ENV', () => { 46 | process.env.NODE_ENV = 'dev'; 47 | cliOptions.args = ['build']; 48 | expect(cliOptions.getEnvironment()).toBe('dev'); 49 | }); 50 | 51 | it('normalizes env from production to prod', () => { 52 | cliOptions.args = ['build', '--env', 'production']; 53 | expect(cliOptions.getEnvironment()).toBe('prod'); 54 | }); 55 | 56 | it('does not normalizes env from production to prod if production.js is defined', () => { 57 | const fsConfig = { 58 | 'aurelia_project/environments/development.js': 'content', 59 | 'aurelia_project/environments/stage.js': 'content', 60 | 'aurelia_project/environments/production.js': 'content' 61 | }; 62 | mockfs(fsConfig); 63 | cliOptions.args = ['build', '--env', 'production']; 64 | expect(cliOptions.getEnvironment()).toBe('production'); 65 | }); 66 | 67 | it('normalizes env from development to dev', () => { 68 | cliOptions.args = ['build', '--env', 'development']; 69 | expect(cliOptions.getEnvironment()).toBe('dev'); 70 | }); 71 | 72 | it('does not normalizes env from development to dev if development.js is defined', () => { 73 | const fsConfig = { 74 | 'aurelia_project/environments/development.js': 'content', 75 | 'aurelia_project/environments/stage.js': 'content', 76 | 'aurelia_project/environments/production.js': 'content' 77 | }; 78 | mockfs(fsConfig); 79 | cliOptions.args = ['build', '--env', 'development']; 80 | expect(cliOptions.getEnvironment()).toBe('development'); 81 | }); 82 | 83 | it('terminates when env is not defined by an env file', () => { 84 | let spy = jasmine.createSpy('exit', process.exit); 85 | 86 | cliOptions.args = ['build', '--env', 'unknown']; 87 | cliOptions.getEnvironment(); 88 | expect(spy).toHaveBeenCalledWith(1); 89 | }); 90 | 91 | it('normalizes NODE_ENV from production to prod', () => { 92 | process.env.NODE_ENV = 'production'; 93 | cliOptions.args = ['build']; 94 | expect(cliOptions.getEnvironment()).toBe('prod'); 95 | }); 96 | 97 | it('does not normalizes NODE_ENV from production to prod if production.js is defined', () => { 98 | const fsConfig = { 99 | 'aurelia_project/environments/development.js': 'content', 100 | 'aurelia_project/environments/stage.js': 'content', 101 | 'aurelia_project/environments/production.js': 'content' 102 | }; 103 | mockfs(fsConfig); 104 | process.env.NODE_ENV = 'production'; 105 | cliOptions.args = ['build']; 106 | expect(cliOptions.getEnvironment()).toBe('production'); 107 | }); 108 | 109 | it('normalizes NODE_ENV from development to dev', () => { 110 | process.env.NODE_ENV = 'development'; 111 | cliOptions.args = ['build']; 112 | expect(cliOptions.getEnvironment()).toBe('dev'); 113 | }); 114 | 115 | it('does not normalizes env from development to dev if development.js is defined', () => { 116 | const fsConfig = { 117 | 'aurelia_project/environments/development.js': 'content', 118 | 'aurelia_project/environments/stage.js': 'content', 119 | 'aurelia_project/environments/production.js': 'content' 120 | }; 121 | mockfs(fsConfig); 122 | process.env.NODE_ENV = 'development'; 123 | cliOptions.args = ['build']; 124 | expect(cliOptions.getEnvironment()).toBe('development'); 125 | }); 126 | 127 | it('terminates when NODE_ENV is not defined by an env file', () => { 128 | let spy = jasmine.createSpy('exit', process.exit); 129 | 130 | process.env.NODE_ENV = 'unknown'; 131 | cliOptions.args = ['build']; 132 | cliOptions.getEnvironment(); 133 | expect(spy).toHaveBeenCalledWith(1); 134 | }); 135 | }); 136 | }); 137 | -------------------------------------------------------------------------------- /spec/lib/cli.spec.js: -------------------------------------------------------------------------------- 1 | const mockfs = require('../mocks/mock-fs'); 2 | 3 | describe('The cli', () => { 4 | let fs; 5 | let path; 6 | let cli; 7 | let Project; 8 | let project; 9 | 10 | let dir; 11 | let aureliaProject; 12 | 13 | beforeEach(() => { 14 | fs = require('../../lib/file-system'); 15 | path = require('path'); 16 | cli = new (require('../../lib/cli').CLI)(); 17 | Project = require('../../lib/project').Project; 18 | project = {}; 19 | 20 | dir = 'workspaces'; 21 | aureliaProject = 'aurelia_project'; 22 | const fsConfig = {}; 23 | fsConfig[dir] = {}; 24 | fsConfig['package.json'] = '{"version": "1.0.0"}'; 25 | mockfs(fsConfig); 26 | }); 27 | 28 | afterEach(() => { 29 | mockfs.restore(); 30 | }); 31 | 32 | describe('The _establishProject() function', () => { 33 | let establish; 34 | 35 | beforeEach(() => { 36 | establish = spyOn(Project, 'establish').and.returnValue(project); 37 | }); 38 | 39 | it('resolves to nothing', done => { 40 | cli._establishProject({}) 41 | .then(proj => { 42 | expect(proj).not.toBeDefined(); 43 | }) 44 | .catch(fail).then(done); 45 | }); 46 | 47 | it('calls and resolves to Project.establish()', done => { 48 | fs.mkdirp(path.join(process.cwd(), aureliaProject)) 49 | .then(() => cli._establishProject({ 50 | runningLocally: true 51 | })) 52 | .then(proj => { 53 | expect(Project.establish) 54 | .toHaveBeenCalledWith(path.join(process.cwd())); 55 | expect(proj).toBe(proj); 56 | }) 57 | .catch(fail).then(done); 58 | }); 59 | 60 | it('does not catch Project.establish()', done => { 61 | establish.and.callFake(() => new Promise((resolve, reject) => reject())); 62 | 63 | fs.mkdirp(path.join(process.cwd(), aureliaProject)) 64 | .then(() => cli._establishProject({ 65 | runningLocally: true 66 | })) 67 | .then(() => { 68 | fail('expected promise to be rejected.'); 69 | done(); 70 | }) 71 | .catch(done); 72 | }); 73 | 74 | it('logs \'No Aurelia project found.\'', done => { 75 | spyOn(cli.ui, 'log'); 76 | cli._establishProject({ 77 | runningLocally: true 78 | }).then(() => { 79 | expect(cli.ui.log).toHaveBeenCalledWith('No Aurelia project found.'); 80 | }).catch(fail).then(done); 81 | }); 82 | }); 83 | 84 | describe('The createHelpCommand() function', () => { 85 | it('gets the help command', () => { 86 | mockfs({ 87 | 'lib/commands/help/command.js': 'module.exports = {}', 88 | 'lib/string.js': 'module.exports = {}' 89 | }); 90 | 91 | spyOn(cli.container, 'get'); 92 | 93 | cli.createHelpCommand(); 94 | expect(cli.container.get) 95 | .toHaveBeenCalledWith(require('../../lib/commands/help/command')); 96 | }); 97 | }); 98 | 99 | describe('The configureContainer() function', () => { 100 | it('registers the instances', () => { 101 | const registerInstanceSpy = spyOn(cli.container, 'registerInstance'); 102 | 103 | cli.configureContainer(); 104 | 105 | expect(registerInstanceSpy.calls.count()).toBe(2); 106 | }); 107 | }); 108 | 109 | describe('The run() function', () => { 110 | function getVersionSpec(command) { 111 | return () => { 112 | beforeEach(() => { 113 | spyOn(cli.ui, 'log') 114 | .and.callFake(() => new Promise(resolve => resolve())); 115 | }); 116 | 117 | // Without real mockfs, it doesn't require the mocked package.json. 118 | xit('logs the cli version', () => { 119 | console.log('cwd', process.cwd()); 120 | cli.run(command); 121 | expect(cli.ui.log).toHaveBeenCalledWith('Local aurelia-cli v1.0.0'); 122 | }); 123 | 124 | it('returns an empty promise', done => { 125 | cli.run(command).then(resolved => { 126 | expect(resolved).not.toBeDefined(); 127 | }).catch(fail).then(done); 128 | }); 129 | }; 130 | } 131 | 132 | describe('The --version arg', getVersionSpec('--version')); 133 | 134 | describe('The -v arg', getVersionSpec('-v')); 135 | 136 | it('uses the _establishProject() function', done => { 137 | // const project = {}; 138 | spyOn(cli, '_establishProject').and.returnValue(new Promise(resolve => { 139 | resolve(project); 140 | })); 141 | spyOn(cli.container, 'registerInstance'); 142 | spyOn(cli, 'createCommand').and.returnValue(Promise.resolve({ execute: () => {} })); 143 | 144 | cli.run() 145 | .then(() => { 146 | expect(cli._establishProject).toHaveBeenCalled(); 147 | }).catch(fail).then(done); 148 | }); 149 | 150 | it('registers the project instance', done => { 151 | cli.options.runningLocally = true; 152 | 153 | spyOn(cli, '_establishProject').and.returnValue(new Promise(resolve => { 154 | resolve(project); 155 | })); 156 | 157 | spyOn(cli.container, 'registerInstance'); 158 | spyOn(cli, 'createCommand').and.returnValue(Promise.resolve({ execute: () => {} })); 159 | 160 | cli.run().then(() => { 161 | expect(cli.container.registerInstance) 162 | .toHaveBeenCalledWith(Project, project); 163 | }).catch(fail).then(done); 164 | }); 165 | 166 | it('creates the command', done => { 167 | const command = 'run'; 168 | const args = {}; 169 | spyOn(cli, 'createCommand').and.returnValue(Promise.resolve({ execute: () => {} })); 170 | 171 | cli.run(command, args).then(() => { 172 | expect(cli.createCommand).toHaveBeenCalledWith(command, args); 173 | }).catch(fail).then(done); 174 | }); 175 | 176 | it('executes the command', done => { 177 | const command = { 178 | execute: jasmine.createSpy('execute').and.returnValue(Promise.resolve({})) 179 | }; 180 | const args = {}; 181 | spyOn(cli, '_establishProject').and.returnValue(Promise.resolve(project)); 182 | spyOn(cli, 'createCommand').and.returnValue(Promise.resolve(command)); 183 | 184 | cli.run('run', args).then(() => { 185 | expect(command.execute).toHaveBeenCalledWith(args); 186 | }).catch(fail).then(done); 187 | }); 188 | 189 | it('fails gracefully when Aurelia-CLI is ran from a root folder (non-project directory)', done => { 190 | cli.options.runningLocally = true; 191 | const command = { 192 | execute: jasmine.createSpy('execute').and.returnValue(Promise.resolve({})) 193 | }; 194 | const args = {}; 195 | spyOn(cli, '_establishProject').and.returnValue(Promise.resolve(null)); // no project could be found 196 | spyOn(cli, 'createCommand').and.returnValue(Promise.resolve(command)); 197 | const errorSpy = spyOn(cli.ui, 'log'); 198 | 199 | cli.run('', args).then(() => { 200 | expect(command.execute).not.toHaveBeenCalledWith(args); 201 | expect(errorSpy).toHaveBeenCalledTimes(2); 202 | expect(errorSpy.calls.first().args[0]).toContain('Local aurelia-cli'); 203 | expect(errorSpy.calls.argsFor(1)[0]).toContain('It appears that the Aurelia CLI is running locally'); 204 | }).catch(fail).then(done); 205 | }); 206 | }); 207 | 208 | describe('The config command', () => { 209 | it('creates the command', done => { 210 | const command = 'config'; 211 | const args = {}; 212 | spyOn(cli, 'createCommand').and.returnValue(Promise.resolve({ execute: () => {} })); 213 | 214 | cli.run(command, args).then(() => { 215 | expect(cli.createCommand).toHaveBeenCalledWith(command, args); 216 | }).catch(fail).then(done); 217 | }); 218 | 219 | it('executes the command', done => { 220 | const command = { 221 | execute: jasmine.createSpy('execute').and.returnValue(Promise.resolve({})) 222 | }; 223 | const args = {}; 224 | spyOn(cli, '_establishProject').and.returnValue(new Promise(resolve => 225 | resolve(project) 226 | )); 227 | spyOn(cli, 'createCommand').and.returnValue(Promise.resolve(command)); 228 | 229 | cli.run('config', args).then(() => { 230 | expect(command.execute).toHaveBeenCalledWith(args); 231 | }).catch(fail).then(done); 232 | }); 233 | }); 234 | }); 235 | -------------------------------------------------------------------------------- /spec/lib/commands/config/configuration.spec.js: -------------------------------------------------------------------------------- 1 | const mockfs = require('../../../mocks/mock-fs'); 2 | 3 | describe('The config command - configuration', () => { 4 | const CLIOptions = require('../../../../lib/cli-options').CLIOptions; 5 | const Configuration = require('../../../../lib/commands/config/configuration'); 6 | let configuration; 7 | let project; 8 | let projectControl; 9 | beforeEach(() => { 10 | project = { 11 | 'top1': { 12 | 'middle': { 13 | 'bottom1': { 14 | 'rock': 'bottom' 15 | }, 16 | 'bottom2': [ 17 | 'also', 18 | 'bottom' 19 | ] 20 | } 21 | }, 22 | 'top2': ['one', 2, { 'three': 'four' }], 23 | 'top3': 'string3', 24 | 'top4': 4 25 | }; 26 | projectControl = JSON.parse(JSON.stringify(project)); 27 | 28 | mockfs({ 29 | 'aurelia_project': { 30 | 'aurelia.json': JSON.stringify(project) 31 | } 32 | }); 33 | CLIOptions.instance = new CLIOptions(); 34 | Object.assign(CLIOptions.instance, { originalBaseDir: '.' }); 35 | configuration = new Configuration(CLIOptions.instance); 36 | }); 37 | 38 | afterEach(() => { 39 | mockfs.restore(); 40 | }); 41 | 42 | it('normalizes keys', () => { 43 | const values = { 44 | 'one': 'one', 45 | 'one two three': 'one two three', 46 | 'one.two.three': 'one.two.three', 47 | 'one.two three': 'one.two three', 48 | 'one.[two three].four': 'one.two three.four', 49 | 'one[2].three': 'one.[2].three', 50 | 'one.[2].[3][four][5]': 'one.[2].[3].four.[5]', 51 | 'one.2.3[four][5]': 'one.2.3.four.[5]' 52 | }; 53 | for (let key of Object.keys(values)) { 54 | expect(configuration.normalizeKey(key)).toEqual(values[key]); 55 | } 56 | }); 57 | 58 | it('parses keys', () => { 59 | const values = { 60 | 'one': { index: false, key: true, value: 'one' }, 61 | '[2]': { index: true, key: false, value: 2 } 62 | }; 63 | for (let key of Object.keys(values)) { 64 | expect(configuration.parsedKey(key)).toEqual(values[key]); 65 | } 66 | }); 67 | 68 | it('gets values', () => { 69 | const values = { 70 | 'top1': project.top1, 71 | 'top1.middle': project.top1.middle, 72 | 'top2.[1]': project.top2[1], 73 | 'top1.middle.bottom2.[1]': project.top1.middle.bottom2[1], 74 | 'top2.[2].three': project.top2[2].three 75 | }; 76 | for (let key of Object.keys(values)) { 77 | expect(configuration.configEntry(key)).toEqual(values[key]); 78 | } 79 | }); 80 | 81 | it('gets the complete project without arguments', () => { 82 | let result = configuration.execute('get', ''); 83 | 84 | project = JSON.parse(result.slice(result.indexOf('\n'))); 85 | expect(project).toEqual(projectControl); 86 | }); 87 | 88 | it('gets a value', () => { 89 | let result = configuration.execute('get', 'top1'); 90 | 91 | project = JSON.parse(result.slice(result.indexOf('\n'))); 92 | expect(project).toEqual(projectControl.top1); 93 | }); 94 | 95 | it('sets a value', () => { 96 | configuration.execute('set', 'top1.middle2', 'added'); 97 | projectControl.top1.middle2 = 'added'; 98 | 99 | configuration.execute('set', 'top5.new', 'stuff'); 100 | projectControl.top5 = { 'new': 'stuff' }; 101 | 102 | configuration.execute('set', 'top2[2].three', 'fifth'); 103 | projectControl.top2[2].three = 'fifth'; 104 | 105 | expect(configuration.project).toEqual(projectControl); 106 | }); 107 | 108 | it('clears a value', () => { 109 | configuration.execute('clear', 'top1.middle'); 110 | delete projectControl.top1.middle; 111 | 112 | configuration.execute('clear', 'top2[2].three'); 113 | delete projectControl.top2[2].three; 114 | 115 | expect(configuration.project).toEqual(projectControl); 116 | }); 117 | 118 | it('adds a value', () => { 119 | configuration.execute('add', 'top1.middle2', 'added'); 120 | projectControl.top1.middle2 = 'added'; 121 | 122 | configuration.execute('add', 'top2[2].five', 'sixth'); 123 | projectControl.top2[2].five = 'sixth'; 124 | 125 | configuration.execute('add', 'top2', { seventh: 'eight' }); 126 | projectControl.top2.push({ seventh: 'eight' }); 127 | 128 | expect(configuration.project).toEqual(projectControl); 129 | }); 130 | 131 | it('removes a value', () => { 132 | configuration.execute('remove', 'top1.middle'); 133 | delete projectControl.top1.middle; 134 | 135 | configuration.execute('remove', 'top2[2]'); 136 | projectControl.top2.splice(2, 1); 137 | 138 | expect(configuration.project).toEqual(projectControl); 139 | }); 140 | }); 141 | -------------------------------------------------------------------------------- /spec/lib/commands/config/util.spec.js: -------------------------------------------------------------------------------- 1 | const mockfs = require('../../../mocks/mock-fs'); 2 | 3 | describe('The config command - util', () => { 4 | const CLIOptions = require('../../../../lib/cli-options').CLIOptions; 5 | const ConfigurationUtilities = require('../../../../lib/commands/config/util'); 6 | 7 | beforeEach(() => { 8 | mockfs({ 9 | 'aurelia_project/aurelia.json': '{ "build": {} }' 10 | }); 11 | CLIOptions.instance = new CLIOptions(); 12 | Object.assign(CLIOptions.instance, { originalBaseDir: '.' }); 13 | }); 14 | afterEach(() => { 15 | mockfs.restore(); 16 | }); 17 | 18 | it('gets the right arg withouth --flag', () => { 19 | const args = ['zero', 'one', 'two']; 20 | const configurationUtilities = new ConfigurationUtilities(CLIOptions, args); 21 | expect(configurationUtilities.getArg(1)).toBe('one'); 22 | }); 23 | 24 | it('gets the right arg with --flag', () => { 25 | const args = ['--zero', 'one', 'two']; 26 | const configurationUtilities = new ConfigurationUtilities(CLIOptions, args); 27 | expect(configurationUtilities.getArg(1)).toBe('two'); 28 | }); 29 | 30 | it('gets the right action', () => { 31 | const args = ['--zero', '--remove', '--two', 'three']; 32 | Object.assign(CLIOptions.instance, { args: args }); 33 | 34 | const configurationUtilities = new ConfigurationUtilities(CLIOptions, args); 35 | expect(configurationUtilities.getAction()).toBe('remove'); 36 | }); 37 | 38 | it('gets the right JSON values', () => { 39 | const args = {}; 40 | const values = { 41 | '123': '123', 42 | 'one two three': 'one two three', 43 | '"456"': '456', 44 | '{ "myKey": "myValue" }': { myKey: 'myValue' }, 45 | '[ "one", 2, "thre ee" ]': [ 'one', 2, 'thre ee' ] 46 | }; 47 | Object.assign(CLIOptions.instance, { args: args }); 48 | const configurationUtilities = new ConfigurationUtilities(CLIOptions, args); 49 | 50 | for (let key of Object.keys(values)) { 51 | expect(configurationUtilities.getValue(key)).toEqual(values[key]); 52 | } 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /spec/lib/configuration.spec.js: -------------------------------------------------------------------------------- 1 | const Configuration = require('../../lib/configuration').Configuration; 2 | const CLIOptionsMock = require('../mocks/cli-options'); 3 | 4 | describe('the Configuration module', () => { 5 | let cliOptionsMock; 6 | 7 | beforeEach(() => { 8 | cliOptionsMock = new CLIOptionsMock(); 9 | cliOptionsMock.attach(); 10 | }); 11 | 12 | afterEach(() => { 13 | cliOptionsMock.detach(); 14 | }); 15 | 16 | it('overrides default options with customized options', () => { 17 | let config = new Configuration({ fromString: true }, { fromString: false }); 18 | expect(config.getAllOptions().fromString).toBe(true); 19 | }); 20 | 21 | describe('the getAllOptions() function', () => { 22 | it('returns the entire options object', () => { 23 | let options = { 24 | rev: 'dev & prod', 25 | minify: true, 26 | inject: { dev: { value: true } } 27 | }; 28 | let config = new Configuration({}, options); 29 | expect(config.getAllOptions()).toEqual(options); 30 | }); 31 | }); 32 | 33 | describe('the getValue() function', () => { 34 | it('supports multi level options', () => { 35 | let options = new Configuration({}, { 36 | foo: { 37 | bar: { 38 | dev: { 39 | value: 'someValue' 40 | }, 41 | staging: { 42 | value: 'someOtherValue' 43 | } 44 | } 45 | } 46 | }, 'dev'); 47 | expect(options.getValue('foo.bar').value).toBe('someValue'); 48 | 49 | options = new Configuration({}, { 50 | foo: { 51 | bar: { 52 | 'dev & prod': { 53 | value: 'someValue' 54 | } 55 | } 56 | } 57 | }, 'dev'); 58 | expect(options.getValue('foo.bar').value).toBe('someValue'); 59 | 60 | options = new Configuration({}, { 61 | foo: { 62 | bar: { 63 | dev: { 64 | value: { 65 | options: true 66 | } 67 | } 68 | } 69 | } 70 | }, 'dev'); 71 | expect(options.getValue('foo.bar').value.options).toBe(true); 72 | 73 | options = new Configuration({}, { 74 | foo: { 75 | bar: 'abcd' 76 | } 77 | }, 'dev'); 78 | expect(options.getValue('foo.bar')).toBe('abcd'); 79 | }); 80 | 81 | it('supports one level options', () => { 82 | let options = new Configuration({}, { foo: 'someValue' }, 'dev'); 83 | expect(options.getValue('foo')).toBe('someValue'); 84 | }); 85 | 86 | it('returns undefined when property is not defined', () => { 87 | let options = new Configuration({}, { foo: 'someValue' }, 'dev'); 88 | expect(options.getValue('foobarbaz')).toBe(undefined); 89 | 90 | options = new Configuration({}, { foo: 'someValue' }, 'dev'); 91 | expect(options.getValue('foo.bar.baz')).toBe(undefined); 92 | 93 | options = new Configuration({}, { }, 'dev'); 94 | expect(options.getValue('foo.bar.baz')).toBe(undefined); 95 | }); 96 | 97 | it('applies default config, then environment config', () => { 98 | let options = new Configuration({}, { 99 | foo: { 100 | bar: { 101 | default: { 102 | cutoff: 15, 103 | maxLength: 5000 104 | }, 105 | dev: { 106 | maxLength: 3000 107 | } 108 | } 109 | } 110 | }, 'dev'); 111 | 112 | expect(options.getValue('foo.bar')).toEqual({ 113 | cutoff: 15, 114 | maxLength: 3000 115 | }); 116 | 117 | options = new Configuration({}, { 118 | foo: { 119 | bar: { 120 | dev: { 121 | maxLength: 3000 122 | } 123 | } 124 | } 125 | }, 'dev'); 126 | 127 | expect(options.getValue('foo.bar')).toEqual({ 128 | maxLength: 3000 129 | }); 130 | 131 | options = new Configuration({}, { 132 | foo: { 133 | bar: { 134 | dev: { 135 | maxLength: 3000 136 | }, 137 | 'dev & staging': { 138 | cutoff: 15 139 | } 140 | } 141 | } 142 | }, 'dev'); 143 | 144 | expect(options.getValue('foo.bar')).toEqual({ 145 | maxLength: 3000, 146 | cutoff: 15 147 | }); 148 | }); 149 | }); 150 | 151 | describe('isApplicable', () => { 152 | it('supports multi level options', () => { 153 | let options = new Configuration({}, { 154 | foo: { 155 | bar: { 156 | baz: 'dev & prod' 157 | } 158 | } 159 | }, 'dev'); 160 | expect(options.isApplicable('foo.bar.baz')).toBe(true); 161 | 162 | options = new Configuration({}, { 163 | foo: { 164 | bar: true 165 | } 166 | }, 'staging'); 167 | expect(options.isApplicable('foo.bar')).toBe(true); 168 | 169 | options = new Configuration({}, { 170 | foo: { 171 | bar: { 172 | dev: false, 173 | staging: true 174 | } 175 | } 176 | }, 'staging'); 177 | expect(options.isApplicable('foo.bar')).toBe(true); 178 | 179 | options = new Configuration({}, { 180 | foo: { 181 | bar: { 182 | dev: false, 183 | 'staging & prod': true 184 | } 185 | } 186 | }, 'staging'); 187 | expect(options.isApplicable('foo.bar')).toBe(true); 188 | 189 | options = new Configuration({}, { 190 | foo: { 191 | bar: false 192 | } 193 | }, 'staging'); 194 | expect(options.isApplicable('foo.bar')).toBe(false); 195 | }); 196 | 197 | it('returns false if env not found', () => { 198 | let options = new Configuration({}, { 199 | foo: { 200 | bar: { 201 | staging: { 202 | somevalue: 123 203 | } 204 | } 205 | } 206 | }, 'dev'); 207 | expect(options.isApplicable('foo.bar')).toBe(false); 208 | }); 209 | 210 | it('supports first level options', () => { 211 | let options = new Configuration({}, { 212 | foo: 'dev & prod' 213 | }, 'dev'); 214 | expect(options.isApplicable('foo')).toBe(true); 215 | 216 | options = new Configuration({}, { 217 | foo: 'dev & prod' 218 | }, 'staging'); 219 | expect(options.isApplicable('foo')).toBe(false); 220 | }); 221 | 222 | it('interprets strings', () => { 223 | let options = new Configuration({}, { foo: 'dev & prod' }, 'dev'); 224 | expect(options.isApplicable('foo')).toBe(true); 225 | 226 | options = new Configuration({}, { 227 | foo: { 228 | bar: { 229 | baz: 'dev & prod' 230 | } 231 | } 232 | }, 'dev'); 233 | expect(options.isApplicable('foo.bar.baz')).toBe(true); 234 | 235 | options = new Configuration({}, { foo: 'dev & prod' }, 'staging'); 236 | expect(options.isApplicable('foo')).toBe(false); 237 | 238 | options = new Configuration({}, { 239 | foo: { 240 | bar: { 241 | baz: 'dev & prod' 242 | } 243 | } 244 | }, 'staging'); 245 | expect(options.isApplicable('foo.bar.baz')).toBe(false); 246 | 247 | options = new Configuration({}, { 248 | foo: { 249 | bar: { 250 | 'dev & prod': { 251 | value: true 252 | } 253 | } 254 | } 255 | }, 'dev'); 256 | expect(options.isApplicable('foo.bar')).toBe(true); 257 | 258 | options = new Configuration({}, { 259 | foo: { 260 | bar: { 261 | 'dev & prod': true 262 | } 263 | } 264 | }, 'staging'); 265 | expect(options.isApplicable('foo.bar')).toBe(false); 266 | 267 | options = new Configuration({}, { 268 | foo: { 269 | bar: { 270 | 'dev & prod': true 271 | } 272 | } 273 | }, 'dev'); 274 | expect(options.isApplicable('foo.bar')).toBe(true); 275 | }); 276 | 277 | it('supports booleans', () => { 278 | let options = new Configuration({}, { foo: true }, 'dev'); 279 | expect(options.isApplicable('foo')).toBe(true); 280 | 281 | options = new Configuration({}, { foo: false }, 'dev'); 282 | expect(options.isApplicable('foo')).toBe(false); 283 | }); 284 | 285 | it('supports environments inside an object', () => { 286 | let options = new Configuration({}, { foo: { dev: true, staging: false } }, 'dev'); 287 | expect(options.isApplicable('foo')).toBe(true); 288 | 289 | options = new Configuration({}, { foo: { dev: false, staging: true } }, 'dev'); 290 | expect(options.isApplicable('foo')).toBe(false); 291 | }); 292 | }); 293 | }); 294 | -------------------------------------------------------------------------------- /spec/lib/file-system.spec.js: -------------------------------------------------------------------------------- 1 | const mockfs = require('../mocks/mock-fs'); 2 | 3 | const ERROR_CODES = { 4 | ENOENT: 'ENOENT', 5 | EEXIST: 'EEXIST' 6 | }; 7 | 8 | describe('The file-system module', () => { 9 | let path; 10 | let fs; 11 | 12 | let readDir; 13 | let readFile; 14 | let writeDir; 15 | let writeFile; 16 | 17 | beforeEach(() => { 18 | path = require('path'); 19 | fs = require('../../lib/file-system'); 20 | 21 | readDir = 'read'; 22 | readFile = { 23 | name: 'read.js', 24 | content: 'content' 25 | }; 26 | readFile.path = path.join(readDir, readFile.name); 27 | 28 | writeDir = 'write'; 29 | writeFile = { 30 | name: 'write.js', 31 | content: 'content' 32 | }; 33 | writeFile.path = path.join(writeDir, writeFile.name); 34 | 35 | const config = {}; 36 | config[readFile.path] = readFile.content; 37 | 38 | mockfs(config); 39 | }); 40 | 41 | afterEach(() => { 42 | mockfs.restore(); 43 | }); 44 | 45 | describe('The isFile function', () => { 46 | it('returns true for file', () => { 47 | expect(fs.isFile(readFile.path)).toBeTruthy(); 48 | }); 49 | 50 | it('returns false for directory', () => { 51 | expect(fs.isFile(readDir)).toBeFalsy(); 52 | }); 53 | 54 | it('returns false for non-existing file', () => { 55 | expect(fs.isFile(path.join(readDir, 'non-existing'))).toBeFalsy(); 56 | }); 57 | }); 58 | 59 | describe('The isDirectory function', () => { 60 | it('returns false for file', () => { 61 | expect(fs.isDirectory(readFile.path)).toBeFalsy(); 62 | }); 63 | 64 | it('returns true for directory', () => { 65 | expect(fs.isDirectory(readDir)).toBeTruthy(); 66 | }); 67 | 68 | it('returns false for non-existing file', () => { 69 | expect(fs.isDirectory(path.join(readDir, 'non-existing'))).toBeFalsy(); 70 | }); 71 | }); 72 | 73 | describe('The stat() function', () => { 74 | it('reads the stats for a directory', done => { 75 | fs.stat(readDir).then(stats => { 76 | expect(stats).toBeDefined(); 77 | }).catch(fail).then(done); 78 | }); 79 | 80 | it('reads the stats for a file', done => { 81 | fs.stat(readFile.path).then(stats => { 82 | expect(stats).toBeDefined(); 83 | }).catch(fail).then(done); 84 | }); 85 | 86 | it('rejects with an ENOENT error on a non-existing directory', done => { 87 | fs.stat(writeDir).then(() => { 88 | fail('expected promise to be rejected'); 89 | }).catch(e => { 90 | expect(e.code).toBe(ERROR_CODES.ENOENT); 91 | }).then(done); 92 | }); 93 | 94 | it('rejects with an ENOENT error on a non-existing file', done => { 95 | fs.stat(writeFile.path).then(() => { 96 | fail('expected promise to be rejected'); 97 | }).catch(e => { 98 | expect(e.code).toBe(ERROR_CODES.ENOENT); 99 | }).then(done); 100 | }); 101 | }); 102 | 103 | describe('The readdir() function', () => { 104 | it('reads a directory', done => { 105 | fs.readdir(readDir).then(files => { 106 | expect(files).toEqual([readFile.name]); 107 | }).catch(fail).then(done); 108 | }); 109 | 110 | it('rejects with ENOENT', done => { 111 | fs.readdir(writeDir).then(() => { 112 | fail('expected promise to be rejected'); 113 | }).catch(e => { 114 | expect(e.code).toBe(ERROR_CODES.ENOENT); 115 | }).then(done); 116 | }); 117 | }); 118 | 119 | describe('The mkdir() function', () => { 120 | it('makes a directory', done => { 121 | fs.mkdir(writeDir) 122 | .catch(fail) 123 | .then(() => fs.readdir(writeDir)) 124 | .catch(fail) 125 | .then((files) => { 126 | expect(files.length).toBe(0); 127 | done(); 128 | }); 129 | }); 130 | 131 | it('rejects with EEXIST', done => { 132 | fs.mkdir(readDir) 133 | .then(() => fail('expected promise to be rejected')) 134 | .catch(e => expect(e.code).toBe(ERROR_CODES.EEXIST)) 135 | .then(done); 136 | }); 137 | }); 138 | 139 | describe('The mkdirp() function', () => { 140 | it('makes deep directories', done => { 141 | fs.mkdirp(writeDir + readDir).then(() => { 142 | return fs.readdir(writeDir + readDir); 143 | }) 144 | .catch(fail) 145 | .then((files) => { 146 | expect(files.length).toBe(0); 147 | done(); 148 | }); 149 | }); 150 | }); 151 | 152 | describe('The readFile() function', () => { 153 | it('returns a promise resolving to the files content', done => { 154 | fs.readFile(readFile.path).then(content => { 155 | expect(content).toBe(readFile.content); 156 | }).catch(fail).then(done); 157 | }); 158 | 159 | it('returns a promise resolving to raw buffer of the files content when encoding is null', done => { 160 | fs.readFile(readFile.path, null).then(buf => { 161 | expect(Buffer.isBuffer(buf)).toBe(true); 162 | expect(buf.toString('utf8')).toBe(readFile.content); 163 | }).catch(fail).then(done); 164 | }); 165 | 166 | it('rejects with ENOENT error', done => { 167 | fs.readFile(writeFile.path).then(() => { 168 | fail('expected promise to be rejected'); 169 | }).catch(e => { 170 | expect(e.code).toBe(ERROR_CODES.ENOENT); 171 | done(); 172 | }); 173 | }); 174 | }); 175 | 176 | describe('The readFileSync() function', () => { 177 | it('returns the files content', () => { 178 | expect(fs.readFileSync(readFile.path)) 179 | .toBe(readFile.content); 180 | }); 181 | 182 | it('returns raw buffer of files content when encoding is null', () => { 183 | let buf = fs.readFileSync(readFile.path, null); 184 | expect(Buffer.isBuffer(buf)).toBe(true); 185 | expect(buf.toString('utf8')).toBe(readFile.content); 186 | }); 187 | 188 | it('throws an ENOENT error', () => { 189 | try { 190 | fs.readFileSync(writeFile.path); 191 | fail(`expected fs.readFileSync('${writeFile.path}') to throw`); 192 | } catch (e) { 193 | expect(e.code).toBe(ERROR_CODES.ENOENT); 194 | } 195 | }); 196 | }); 197 | 198 | describe('The writeFile() function', () => { 199 | it('creates a new file', done => { 200 | fs.writeFile(writeFile.path, writeFile.content).then(() => { 201 | return fs.readFile(writeFile.path); 202 | }).then(content => { 203 | expect(content).toBe(writeFile.content); 204 | done(); 205 | }); 206 | }); 207 | }); 208 | }); 209 | -------------------------------------------------------------------------------- /spec/lib/project-item.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('../../lib/file-system'); 3 | const mockfs = require('../mocks/mock-fs'); 4 | const {ProjectItem} = require('../../lib/project-item'); 5 | 6 | describe('The ProjectItem module', () => { 7 | it('ProjectItem.text() captures text', () => { 8 | const t = ProjectItem.text('file.js', 'lorem'); 9 | expect(t.name).toBe('file.js'); 10 | expect(t.text).toBe('lorem'); 11 | expect(t.isDirectory).toBe(false); 12 | expect(() => t.add(ProjectItem.text('file2.js', 'lorem'))).toThrow(); 13 | }); 14 | 15 | it('ProjectItem.directory() captures dir', () => { 16 | const t = ProjectItem.directory('dir'); 17 | expect(t.name).toBe('dir'); 18 | expect(t.text).toBeUndefined(); 19 | expect(t.isDirectory).toBe(true); 20 | const file = ProjectItem.text('file.js', 'lorem'); 21 | const folder = ProjectItem.directory('folder'); 22 | t.add(file).add(folder); 23 | expect(t.children.length).toBe(2); 24 | expect(t.children).toEqual([file, folder]); 25 | }); 26 | 27 | describe('Creates files', () => { 28 | let folder; 29 | beforeEach(() => { 30 | mockfs(); 31 | folder = ProjectItem.directory('folder'); 32 | folder.add(ProjectItem.text('file1.js', 'file1')); 33 | folder.add(ProjectItem.text('file2.js', 'file2')); 34 | folder.add(ProjectItem.directory('deepFolder').add(ProjectItem.text('file4.js', 'file4'))); 35 | }); 36 | 37 | afterEach(() => { 38 | mockfs.restore(); 39 | }); 40 | 41 | it('creates deep folders and files', async() => { 42 | await folder.create('root'); 43 | expect(fs.readdirSync('.')).toEqual(['folder']); 44 | expect(fs.readdirSync('folder').sort()).toEqual(['deepFolder', 'file1.js', 'file2.js']); 45 | expect(fs.readFileSync(path.join('folder', 'file1.js'))).toBe('file1'); 46 | expect(fs.readFileSync(path.join('folder', 'file2.js'))).toBe('file2'); 47 | expect(fs.readdirSync(path.join('folder', 'deepFolder')).sort()).toEqual(['file4.js']); 48 | expect(fs.readFileSync(path.join('folder', 'deepFolder', 'file4.js'))).toBe('file4'); 49 | }); 50 | 51 | it('overwrites existing file', async() => { 52 | mockfs({ 53 | 'folder': { 54 | 'file1.js': 'oldfile1', 55 | 'file3.js': 'oldfile3' 56 | } 57 | }); 58 | await folder.create('root'); 59 | expect(fs.readdirSync('.')).toEqual(['folder']); 60 | expect(fs.readdirSync('folder').sort()).toEqual(['deepFolder', 'file1.js', 'file2.js', 'file3.js']); 61 | expect(fs.readFileSync(path.join('folder', 'file1.js'))).toBe('file1'); 62 | expect(fs.readFileSync(path.join('folder', 'file2.js'))).toBe('file2'); 63 | expect(fs.readFileSync(path.join('folder', 'file3.js'))).toBe('oldfile3'); 64 | expect(fs.readdirSync(path.join('folder', 'deepFolder')).sort()).toEqual(['file4.js']); 65 | expect(fs.readFileSync(path.join('folder', 'deepFolder', 'file4.js'))).toBe('file4'); 66 | }); 67 | 68 | it('skips empty folder', async() => { 69 | folder.add(ProjectItem.directory('empty-folder')); 70 | await folder.create('root'); 71 | expect(fs.readdirSync('.')).toEqual(['folder']); 72 | expect(fs.readdirSync('folder').sort()).toEqual(['deepFolder', 'file1.js', 'file2.js']); 73 | expect(fs.readFileSync(path.join('folder', 'file1.js'))).toBe('file1'); 74 | expect(fs.readFileSync(path.join('folder', 'file2.js'))).toBe('file2'); 75 | expect(fs.readdirSync(path.join('folder', 'deepFolder')).sort()).toEqual(['file4.js']); 76 | expect(fs.readFileSync(path.join('folder', 'deepFolder', 'file4.js'))).toBe('file4'); 77 | }); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /spec/lib/project.spec.js: -------------------------------------------------------------------------------- 1 | const mockfs = require('../mocks/mock-fs'); 2 | 3 | describe('The project module', () => { 4 | let path; 5 | 6 | let fs; 7 | 8 | let Project; 9 | let project; 10 | 11 | beforeEach(() => { 12 | path = require('path'); 13 | 14 | fs = require('../../lib/file-system'); 15 | 16 | Project = require('../../lib/project').Project; 17 | 18 | mockfs(); 19 | 20 | project = new Project('', { 21 | paths: { }, 22 | transpiler: { 23 | fileExtension: '.js' 24 | } 25 | }); 26 | }); 27 | 28 | afterEach(() => { 29 | mockfs.restore(); 30 | }); 31 | 32 | it('creates project items for all paths in aurelia.json, using the root path as the parent directory', () => { 33 | let model = { 34 | paths: { 35 | 'root': 'src', 36 | 'resources': 'resources', 37 | 'elements': 'resources/elements' 38 | } 39 | }; 40 | 41 | project = new Project('', model); 42 | 43 | expect(hasProjectItem(project.locations, 'src', null)).toBe(true); 44 | expect(hasProjectItem(project.locations, 'resources', 'src')).toBe(true); 45 | expect(hasProjectItem(project.locations, 'resources/elements', 'src')).toBe(true); 46 | }); 47 | 48 | describe('The resolveGenerator() function', () => { 49 | it('resolves to teh generators location', done => { 50 | fs.writeFile('aurelia_project/generators/test.js', '') 51 | .then(() => project.resolveGenerator('test')) 52 | .then(location => { 53 | expect(location).toBe(path.join('aurelia_project', 'generators', 'test.js')); 54 | }).catch(fail).then(done); 55 | }); 56 | 57 | it('resolves to null', done => { 58 | project.resolveGenerator('test') 59 | .then(location => { 60 | expect(location).toBe(null); 61 | }).catch(fail).then(done); 62 | }); 63 | }); 64 | 65 | describe('The resolveTask() function', () => { 66 | it('resolves to the tasks location', done => { 67 | fs.writeFile('aurelia_project/tasks/test.js', '') 68 | .then(() => project.resolveTask('test')) 69 | .then(location => { 70 | expect(location).toBe(path.join('aurelia_project', 'tasks', 'test.js')); 71 | }).catch(fail).then(done); 72 | }); 73 | 74 | it('resolves to null', done => { 75 | project.resolveTask('test') 76 | .then(location => { 77 | expect(location).toBe(null); 78 | }).catch(fail).then(done); 79 | }); 80 | }); 81 | 82 | it('The makeFileName() function', () => { 83 | expect(project.makeFileName('Foo'), 'foo'); 84 | expect(project.makeFileName('foo'), 'foo'); 85 | expect(project.makeFileName('fooBar'), 'foo-bar'); 86 | expect(project.makeFileName('foo-bar'), 'foo-bar'); 87 | expect(project.makeFileName('FOO Bar'), 'foo-bar'); 88 | expect(project.makeFileName('_foo_bar_'), 'foo-bar'); 89 | }); 90 | 91 | it('The makeClassName() function', () => { 92 | expect(project.makeClassName('Foo'), 'Foo'); 93 | expect(project.makeClassName('foo'), 'foo'); 94 | expect(project.makeClassName('fooBar'), 'FooBar'); 95 | expect(project.makeClassName('foo-bar'), 'FooBar'); 96 | expect(project.makeClassName('FOO Bar'), 'FooBar'); 97 | expect(project.makeClassName('_foo_bar_'), 'FooBar'); 98 | }); 99 | 100 | it('The makeFunctionName() function', () => { 101 | expect(project.makeFunctionName('Foo'), 'foo'); 102 | expect(project.makeFunctionName('foo'), 'foo'); 103 | expect(project.makeFunctionName('fooBar'), 'fooBar'); 104 | expect(project.makeFunctionName('foo-bar'), 'fooBar'); 105 | expect(project.makeFunctionName('FOO Bar'), 'fooBar'); 106 | expect(project.makeFunctionName('_foo_bar_'), 'fooBar'); 107 | }); 108 | }); 109 | 110 | function hasProjectItem(locations, name, parent) { 111 | for (let i = 0; i < locations.length; i++) { 112 | if (locations[i].name === name) { 113 | if (!parent && !locations[i].parent) { 114 | return true; 115 | } 116 | 117 | if (locations[i].parent && locations[i].parent.name === parent) { 118 | return true; 119 | } 120 | } 121 | } 122 | 123 | return false; 124 | } 125 | -------------------------------------------------------------------------------- /spec/mocks/bundler.js: -------------------------------------------------------------------------------- 1 | const Configuration = require('../../lib/configuration').Configuration; 2 | const CLIOptions = require('../../lib/cli-options').CLIOptions; 3 | const ProjectMock = require('./project-mock'); 4 | const LoaderPlugin = require('../../lib/build/loader-plugin').LoaderPlugin; 5 | 6 | module.exports = class Bundler { 7 | constructor() { 8 | this.itemIncludedInBuild = jasmine.createSpy('itemIncludedInBuild'); 9 | this.interpretBuildOptions = jasmine.createSpy('interpretBuildOptions'); 10 | this.configureDependency = jasmine.createSpy('configureDependency'); 11 | this.addFile = jasmine.createSpy('addFile'); 12 | this.configTargetBundle = { 13 | addAlias: jasmine.createSpy('addAlias') 14 | }; 15 | 16 | CLIOptions.instance = new CLIOptions(); 17 | this.buildOptions = new Configuration({}, {}); 18 | this.project = new ProjectMock(); 19 | this.loaderOptions = { 20 | type: 'require', 21 | plugins: [new LoaderPlugin('require', { 22 | name: 'text', 23 | extensions: ['.html', '.css'] 24 | })] 25 | }; 26 | this.environment = 'dev'; 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /spec/mocks/cli-options.js: -------------------------------------------------------------------------------- 1 | let OriginalCLIOptions = require('../../lib/cli-options').CLIOptions; 2 | 3 | module.exports = class CLIOptionsMock { 4 | constructor() { 5 | this.originalFns = {}; 6 | if (!OriginalCLIOptions.instance) { 7 | // eslint-disable-next-line no-unused-vars 8 | let instance = new OriginalCLIOptions(); 9 | } 10 | } 11 | 12 | attach() { 13 | this.originalFns.getEnvironment = OriginalCLIOptions.prototype.getEnvironment; 14 | OriginalCLIOptions.getEnvironment = jasmine.createSpy('getEnvironment'); 15 | } 16 | 17 | detach() { 18 | OriginalCLIOptions.getEnvironment = this.originalFns.getEnvironment; 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /spec/mocks/mock-fs.js: -------------------------------------------------------------------------------- 1 | const { mkdtempSync, rmSync, mkdirSync, writeFileSync } = require('fs'); 2 | const { join, dirname } = require('path'); 3 | const tmpdir = mkdtempSync(join(__dirname, '..', '..', 'tmpdir-')); 4 | // By default work in a child folder. Some tests run against parent folder 5 | const defaultdir = join(tmpdir, 'a'); 6 | 7 | function fillFiles(fileTree, baseDir = defaultdir) { 8 | mkdirSync(baseDir, { recursive: true }); 9 | for (const key in fileTree) { 10 | const val = fileTree[key]; 11 | const p = join(baseDir, key); 12 | if (typeof val === 'string') { 13 | mkdirSync(dirname(p), { recursive: true }); 14 | writeFileSync(p, val); 15 | } else if (typeof val === 'object') { 16 | fillFiles(val, p); 17 | } 18 | } 19 | } 20 | 21 | let oldCwd; 22 | 23 | // Simple implementation of mockfs in local tmp dir. 24 | function mockfs(fileTree) { 25 | fillFiles(fileTree); 26 | if (!oldCwd) { 27 | oldCwd = process.cwd(); 28 | process.chdir(defaultdir); 29 | } 30 | } 31 | 32 | mockfs.restore = function() { 33 | if (oldCwd) { 34 | process.chdir(oldCwd); 35 | oldCwd = undefined; 36 | } 37 | rmSync(tmpdir, { force: true, recursive: true }); 38 | } 39 | 40 | process.on('exit', mockfs.restore); 41 | 42 | module.exports = mockfs; 43 | -------------------------------------------------------------------------------- /spec/mocks/package-analyzer.js: -------------------------------------------------------------------------------- 1 | module.exports = class PackageAnalyzer { 2 | constructor() { 3 | } 4 | }; 5 | -------------------------------------------------------------------------------- /spec/mocks/project-mock.js: -------------------------------------------------------------------------------- 1 | module.exports = class ProjectMock { 2 | constructor() { 3 | this.paths = { 4 | root: '' 5 | }; 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /spec/mocks/ui.js: -------------------------------------------------------------------------------- 1 | module.exports = class UI { 2 | constructor() { 3 | this.multiselect = jasmine.createSpy('multiselect').and.returnValue(Promise.resolve()); 4 | this.question = jasmine.createSpy('question').and.returnValue(Promise.resolve()); 5 | this.ensureAnswer = jasmine.createSpy('ensureAnswer').and.returnValue(Promise.resolve()); 6 | this.log = jasmine.createSpy('log'); 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /spec/support/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "spec", 3 | "spec_files": [ 4 | "**/*[sS]pec.js" 5 | ], 6 | "helpers": [ 7 | "helpers/**/*.js" 8 | ], 9 | "stopSpecOnExpectationFailure": false, 10 | "random": false 11 | } 12 | -------------------------------------------------------------------------------- /wallaby.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | return { 3 | files: [ 4 | 'lib/**/*.js', 5 | 'package.json', 6 | {pattern: 'spec/mocks/**/*', load: false}, 7 | {pattern: 'spec/helpers/polyfills.js', load: false} 8 | ], 9 | 10 | tests: [ 11 | 'spec/**/*[Ss]pec.js' 12 | ], 13 | 14 | env: { 15 | type: 'node' 16 | }, 17 | 18 | bootstrap: function(wallaby) { 19 | require('aurelia-polyfills'); 20 | }, 21 | 22 | testFramework: 'jasmine' 23 | }; 24 | }; 25 | --------------------------------------------------------------------------------