├── .editorconfig ├── .eslintrc.json ├── .gitattributes ├── .github └── CONTRIBUTING.md ├── .gitignore ├── .npmignore ├── CLI.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── MIGRATION.md ├── README.md ├── bin └── index.js ├── commands ├── component │ ├── index.js │ └── templates │ │ ├── app.ts │ │ └── spec.ts ├── directive │ ├── index.js │ └── templates │ │ ├── app.ts │ │ └── test.ts ├── index.js ├── initial │ ├── index.js │ └── templates │ │ ├── DEVELOPMENT.md │ │ ├── README.md │ │ ├── __gitignore │ │ ├── __npmignore │ │ ├── examples │ │ ├── example.component.ts │ │ ├── example.main.ts │ │ ├── example.module.ts │ │ └── index.html │ │ ├── index.ts │ │ ├── karma.conf.js │ │ ├── package.json │ │ ├── src │ │ ├── index.ts │ │ ├── module.ts │ │ ├── test.js │ │ └── vendor.ts │ │ ├── tasks │ │ ├── build.js │ │ ├── copy-build.js │ │ ├── copy-globs.js │ │ ├── inline-resources.js │ │ ├── rollup-globals.js │ │ ├── rollup.js │ │ ├── tag-version.js │ │ └── test.js │ │ ├── tsconfig.doc.json │ │ ├── tsconfig.es2015.json │ │ ├── tsconfig.es5.json │ │ ├── tsconfig.json │ │ ├── tsconfig.test.json │ │ ├── tslint.json │ │ └── webpack │ │ ├── webpack.common.js │ │ ├── webpack.dev.js │ │ ├── webpack.test.js │ │ └── webpack.utils.js ├── logging.js ├── npm │ └── index.js ├── pipe │ ├── index.js │ └── templates │ │ ├── app.ts │ │ └── spec.ts ├── service │ ├── index.js │ └── templates │ │ ├── app.ts │ │ └── spec.ts ├── upgrade │ └── index.js └── utilities.js ├── index.js ├── package-lock.json ├── package.json ├── test ├── component.spec.js ├── directive.spec.js ├── initial.spec.js ├── npm.spec.js ├── pipe.spec.js ├── resolver.js ├── service.spec.js ├── test-utils.js ├── tools │ ├── logging.spec.js │ └── utilities │ │ ├── case-convert.spec.js │ │ ├── colorize.spec.js │ │ ├── files.spec.js │ │ ├── index.spec.js │ │ ├── inputs.spec.js │ │ └── options.spec.js └── upgrade.spec.js └── tools ├── logging.js └── utilities ├── case-convert.js ├── colorize.js ├── execute.js ├── files.js ├── index.js ├── inputs.js └── options.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["node"], 3 | "extends": ["eslint:recommended", "plugin:node/recommended"], 4 | "rules": { 5 | "node/exports-style": ["error", "module.exports"], 6 | "no-process-exit": "off" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Angular Librarian 2 | 3 | ## How to Contribute 4 | 5 | 1. [Fork the repo](https://github.com/gonzofish/angular-librarian/fork) 6 | 2. Create a branch in your repo with a descriptive name (ie, if you were fixing 7 | and issue with unit test performance, `test-performance` would be 8 | descriptive enough) 9 | 3. Code your fix 10 | 4. [Create a pull request](https://github.com/gonzofish/angular-librarian/compare) 11 | 5. Await response from the maintainers! 12 | 13 | ## The Codebase 14 | 15 | Angular Librarian is powered by the 16 | [`erector-set` library](https://github.com/gonzofish/erector-set). Erector Set 17 | provides a small interface to create scaffolding applications, like Angular 18 | Librarian. 19 | 20 | The flow of any command run through Angular Librarian goes like this: 21 | 22 | 1. User runs the command, which calls `index.js` 23 | 2. `index.js` parses the command, using `process.argv` (it takes commands from index 24 | 2 on) 25 | 1. The first command argument is the actual command, which is run through 26 | a switch. If the command is known, it returns the full name of the 27 | command (ie, `i` becomes `initial`). 28 | 2. If no command is provided, Angular Librarian will ask what command to 29 | run. 30 | 3. `index.js` uses a lookup, provided by `commands/index.js`, for to access the 31 | the functions for provided commands. Each function is the `index.js` file 32 | for that command's folder under `commands`. 33 | 4. The remaining arguments are passed to the function for the specified 34 | command. Each command processes the provided arguments differently, as fits 35 | that command. 36 | 5. All commands take user-provided answers (or command arguments) plus a set of 37 | templates and create the actual files that become part of the scaffolded 38 | project. 39 | 40 | For all commands, the templates will be in the `templates` directory of that 41 | command's directory. The only exception is the `upgrade` command which pulls 42 | its templates with from the `initial` command's `templates` directory. 43 | 44 | For more assistance, feel free to [create an issue with 45 | `[CONTRIBUTING]`](https://github.com/gonzofish/angular-librarian/issues/new?title=[CONTRIBUTING]%20) in the title. 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .nyc_output 3 | .vscode 4 | coverage 5 | *.tgz 6 | debug.log 7 | node_modules 8 | npm-debug.log* 9 | package 10 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .editorconfig 2 | .eslintrc.json 3 | .gitattributes 4 | .github 5 | .gitignore 6 | .npmignore 7 | .nyc_output 8 | .vscode 9 | *.log 10 | *.tgz 11 | CODE_OF_CONDUCT.md 12 | coverage 13 | debug.log 14 | MIGRATION.md 15 | node_modules 16 | npm-debug.log* 17 | package 18 | test 19 | -------------------------------------------------------------------------------- /CLI.md: -------------------------------------------------------------------------------- 1 | # Getting the CLI Accessible 2 | 3 | Since the `ngl` command is installed locally to each project, it is _not_ 4 | immediately available from the command line. To make it available, the command 5 | can be aliased for your environment. 6 | 7 | ## *nix Environments 8 | 9 | To add a persistent `ngl` command to your *nix environment, open up your `*rc` 10 | file (ie, `.zshrc`, `.bashrc`) and add the following: 11 | 12 | ```shell 13 | alias ngl=$(npm bin)/ngl 14 | ``` 15 | 16 | ## Windows Environments 17 | 18 | Windows is a bit trickier to add the command. To do so, create a `.cmd` file 19 | somewhere on your system, such as `aliases.cmd`. Any future aliases you wish to 20 | have can also be added to this file. 21 | 22 | Once created add the following: 23 | 24 | ```shell 25 | @echo off 26 | 27 | DOSKEY ngl=node_modules\.bin\ngl $* 28 | ``` 29 | 30 | Think of `DOSKEY` as analogous to `alias` in *nix environments. The issue left 31 | is that opening a command prompt won't run this alias command file like a 32 | `.bashrc` or `.zshrc` is run when those prompts start up. 33 | 34 | To get around this, add command line arguments to execute the `.cmd` file for 35 | your terminal program using 36 | 37 | ```shell 38 | cmd /k 39 | ``` 40 | 41 | Below are examples of getting this working with a couple different terminals. 42 | 43 | ### ConEmu & cmder 44 | 45 | In the two most popular terminal emulators, ConEmu and cmder, this can be done 46 | by opening up the settings, navigating to Startup > Tasks selecting the task to 47 | run a command prompt (usually named `{cmd}`) and changing the startup command 48 | from: 49 | 50 | ```shell 51 | cmd.exe /k 52 | ``` 53 | 54 | to 55 | 56 | ```shell 57 | cmd.exe /k & 58 | ``` 59 | 60 | For example, if the aliases file is at `C:\Users\me\my-aliases.cmd` and the 61 | current task is: 62 | 63 | ```shell 64 | cmd /k "%ConEmuDir% \..\init.bat" -new_console:d%USERPROFILE% 65 | ``` 66 | 67 | The updated task would be: 68 | 69 | ```shell 70 | cmd /k C:\Users\me\my-aliases.cmd & "%ConEmuDir% \..\init.bat" -new_console:d%USERPROFILE% 71 | ``` 72 | 73 | ### cmd 74 | 75 | The same effect can be achieved by using the default command-line tool, `cmd`. 76 | To do so, just follow the below steps: 77 | 78 | 1. Create a shortcut, anywhere on your system. 79 | 2. Right-click the shortcut, click Properties 80 | 3. In the "Target" field, change the command to: 81 | ```shell 82 | cmd /k 83 | ``` 84 | 85 | Using the example above, the shortcut command would be: 86 | 87 | ```shell 88 | cmd /k C:\Users\me\my-aliases.cmd 89 | ``` 90 | 91 | In the future, when opening a command prompt, use this shortcut to have access 92 | to `ngl`! 93 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at matt.fehskens@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Matt Fehskens 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 | -------------------------------------------------------------------------------- /MIGRATION.md: -------------------------------------------------------------------------------- 1 | # Migration Guides 2 | 3 | If this guide is not clear, feel free to [create an issue](https://github.com/gonzofish/angular-librarian/issues/new?title=[Migration]%20)! 4 | 5 | ## Custom Configurations 6 | 7 | For all migrations, first, make sure you have no altered any of the following 8 | files: 9 | 10 | - `karma.conf.js` 11 | - `tasks/rollup.js` 12 | - `webpack/*` 13 | 14 | If you have, it is **highly** suggested that you move whatever customizations 15 | you have done into configuration files per the 16 | [Custom Configurations](README.md#custom-config) section of the `README.md`. 17 | 18 | ## Upgrading 19 | 20 | Unless the migration states it, the best path of migration is to first use the 21 | built-in upgrade command in one of the following ways: 22 | 23 | ```shell 24 | ngl u 25 | ngl up 26 | ngl upgrade 27 | npm run g u 28 | npm run g up 29 | npm run g ugrade 30 | ``` 31 | 32 | After doing that, following the version-specific list of migration steps. 33 | 34 | **Again, if read the migration steps first before running this command. You 35 | may need to take steps before running the upgrade command!** 36 | 37 | ## Migrations 38 | 39 | - [1.0.0](#v1) 40 | - [1.0.0-beta.13](#1beta13) 41 | - [< 1.0.0-beta.12 to 1.0.0-beta.12](#1beta12) 42 | 43 | ### 1.0.0 44 | 45 | #### Upgrade Node Version 46 | 47 | In order to support the latest and greatest, Librarian now requires a version of 48 | Node >= 8.6 to support the spread operator on Objects. See 49 | [this issue](https://github.com/gonzofish/angular-librarian/issues/88) 50 | for the discussion. 51 | 52 | #### Removing np 53 | 54 | Older projects that upgrade to v1.0.0 should make sure that the `np` library is 55 | removed from the local project and installed globally. When Angular upgraded to 56 | version 5, it upgraded RxJS to v5.5.x--the `np` library will break if it uses 57 | RxJS past 5.4.3. 58 | 59 | To ensure `np` is not installed locally, after upgrading Librarian (see above): 60 | 61 | 1. Check that it is not installed: 62 | > ```shell 63 | > npm ls np 64 | > ``` 65 | 2. If `np` shows up in the `ls` command, remove it: 66 | > ```shell 67 | > npm rm --save-dev np 68 | > ``` 69 | 3. Install it globally: 70 | > ```shell 71 | > npm install -g np 72 | > ``` 73 | 74 | #### Typings 75 | 76 | For a while TypeScript has been leveraging the `@types` organization to manage 77 | type definitions. Prior to this it used `typings`. Using `typings` now gives a 78 | deprecation warning, so it can be removed from your projects with: 79 | 80 | ```shell 81 | npm rm --save-dev typings 82 | ``` 83 | 84 | While this shouldn't affect your project, it is always good to remove 85 | deprecated technologies. 86 | 87 | #### Rollup Failing 88 | 89 | If you are having issues similar to [issue #89](https://github.com/gonzofish/angular-librarian/issues/89), 90 | try deleting your `package-lock.json` file, 91 | [as suggested](https://github.com/gonzofish/angular-librarian/issues/89#issuecomment-362822908) 92 | by @SirDarquan. The lock file can cause old files to remain and not be overwritten (because 93 | its job is to create consistency), by deleting & performing `npm install` again, you may 94 | have success. 95 | 96 | ### 1.0.0-beta.13 97 | 98 | A small bug was introduced and not accounted for with `tasks/rollup.js`. Line 99 | 28 reads: 100 | 101 | ```javascript 102 | moduleName: librarianUtils.dashToCamel(nameParts.package), 103 | ``` 104 | 105 | But should read: 106 | 107 | ```javascript 108 | moduleName: librarianUtils.caseConvert.dashToCamel(nameParts.package), 109 | ``` 110 | 111 | The bug has been fixed but will not be available until the next release. 112 | 113 | ### From < 1.0.0-beta.12 to 1.0.0-beta.12 114 | 115 | To perform this migration, make the changes below. 116 | 117 | Note, should be the value of the `name` attribute in your 118 | `package.json` without scope (i.e, `@myscope/my-package` should be 119 | `my-package`). 120 | 121 | 1. Go into your `package.json` and set the `angular-librarian` version to 122 | `1.0.0-beta.12`, run `npm update`, then run `ngl upgrade` 123 | 1. Remove files: 124 | - `tsconfig.build.json` 125 | - `webpack/webpack.build.json` 126 | 2. Remove from `package.json`: 127 | - `scripts` 128 | - `build:aot` 129 | - `build:copy` 130 | - `build:pack` 131 | - `pretagVersion` 132 | - `jsnext:main` 133 | - `types` 134 | 3. Add to `package.json`: 135 | - `"es2015": "./.js"` 136 | - `"module": "./.es5.js"` 137 | - `"typings": "./.d.ts"` 138 | 4. Change in `package.json`: 139 | - `"main": "./.bundle.js"` to `"main": "./bundles/.umd.js"` 140 | - `"angular-librarian": ""` to `"angular-librarian": "1.0.0-beta.12"` 141 | 5. Change in build TS configs (`tsconfig.es5.json` and `tsconfig.es2015.json`): 142 | - `"flatModuleOutFile": "{{ packageName }}.js"` to 143 | `"flatModuleOutFile": ".js"` 144 | 6. Any Angular packages you may be pulling in, should be added to a 145 | `configs/rollup.config.js`, per the [Custom Configurations](README.md#custom-config) 146 | documentation. For instance, if you pull in `@angular/forms`, the Rollup config would 147 | be: 148 | 149 | ```javascript 150 | 'use strict'; 151 | 152 | module.exports = { 153 | external: [ 154 | '@angular/forms', 155 | ], 156 | globals: { 157 | '@angular/forms': 'ng.forms', 158 | } 159 | }; 160 | ``` 161 | 162 | Run `npm install` and/or `npm update` again, to ensure all dependencies are up-to-date. 163 | -------------------------------------------------------------------------------- /bin/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | const path = require('path'); 5 | const spawn = require('child_process').spawn; 6 | 7 | const cliArgs = Array.from(process.argv).slice(2); 8 | const execDir = path.resolve(__dirname, '..'); 9 | 10 | spawn('node', [execDir].concat(cliArgs), { stdio: 'inherit' }); 11 | -------------------------------------------------------------------------------- /commands/component/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const erector = require('erector-set'); 4 | const logging = require('../../tools/logging'); 5 | const path = require('path'); 6 | const utilities = require('../utilities'); 7 | 8 | const caseConvert = utilities.caseConvert; 9 | const colorize = utilities.colorize; 10 | const files = utilities.files; 11 | const inputs = utilities.inputs; 12 | const opts = utilities.options; 13 | let logger; 14 | let prefix; 15 | 16 | module.exports = function createComponent(rootDir, selector) { 17 | logger = logging.create('Component'); 18 | prefix = files.getSelectorPrefix('component-selector') ? `${files.getSelectorPrefix('component-selector')}-` : ''; 19 | 20 | const options = opts.parseOptions(Array.from(arguments).slice(hasSelector(selector) ? 2 : 1), [ 21 | 'default', 'defaults', 'd', 22 | 'example', 'examples', 'x', 23 | 'hooks', 'h', 24 | 'inline-styles', 'is', 25 | 'inline-template', 'it' 26 | ]); 27 | const useDefaults = opts.checkHasOption(options, ['default', 'defaults', 'd']); 28 | 29 | if (useDefaults) { 30 | return constructWithDefaults(rootDir, selector, options); 31 | } else { 32 | return inquire(rootDir, selector, options); 33 | } 34 | }; 35 | 36 | const constructWithDefaults = (rootDir, selector, options) => { 37 | if (hasSelector(selector) && caseConvert.checkIsDashFormat(selector)) { 38 | const selectorAnswers = getKnownSelector(selector); 39 | const styleAnswers = getKnownStyle(false, selectorAnswers); 40 | const templateAnswers = getKnownTemplate(false, selectorAnswers); 41 | const lifecycleAnswers = getKnownLifecycleHooks(''); 42 | const prefixAnswer = getPrefixAnswer(); 43 | 44 | const answers = selectorAnswers.concat( 45 | styleAnswers, 46 | templateAnswers, 47 | lifecycleAnswers, 48 | prefixAnswer 49 | ); 50 | 51 | return Promise.resolve() 52 | .then(() => construct(answers, options)); 53 | } else { 54 | return Promise.reject( 55 | 'A dash-case selector must be provided when using defaults' 56 | ); 57 | } 58 | }; 59 | 60 | const hasSelector = (selector) => selector && selector[0] !== '-'; 61 | 62 | const inquire = (rootDir, selector, options) => { 63 | const remaining = getRemainingQuestions(selector, options); 64 | const prefixAnswer = getPrefixAnswer(); 65 | const knownAnswers = remaining.answers.concat(prefixAnswer); 66 | ; 67 | const questions = remaining.questions.reduce((all, method) => all.concat(method(knownAnswers)), []); 68 | 69 | return erector.inquire(questions) 70 | .then((answers) => construct(knownAnswers.concat(answers), options)); 71 | }; 72 | 73 | const construct = (answers, options = {}) => { 74 | const forExamples = opts.checkIsForExamples(options); 75 | const templates = getTemplates(forExamples); 76 | const results = erector.construct(answers, templates); 77 | 78 | notifyUser(answers, forExamples); 79 | return results; 80 | }; 81 | 82 | const getPrefixAnswer = () => [{ name: 'prefix', answer: prefix }]; 83 | 84 | const getRemainingQuestions = (selectorName, options) => { 85 | const all = [ 86 | getSelector(selectorName), 87 | getStyle(options), 88 | getTemplate(options), 89 | getLifecycleHooks(options) 90 | ]; 91 | 92 | return all.reduce((groups, part) => { 93 | if (part.answers) { 94 | groups.answers = groups.answers.concat(part.answers); 95 | } else if (part.questions) { 96 | groups.questions = groups.questions.concat(part.questions); 97 | } 98 | 99 | return groups; 100 | }, { answers: [], questions: []}); 101 | }; 102 | 103 | const getSelector = (selector) => { 104 | if (caseConvert.checkIsDashFormat(selector)) { 105 | return { answers: getKnownSelector(selector) }; 106 | } else { 107 | return { 108 | questions: getSelectorQuestions 109 | }; 110 | } 111 | }; 112 | 113 | const getKnownSelector = (selector) => [ 114 | { name: 'selector', answer: selector }, 115 | { name: 'componentName', answer: caseConvert.dashToCap(selector) + 'Component' } 116 | ]; 117 | 118 | const getSelectorQuestions = () => [ 119 | { name: 'selector', question: 'What is the component selector (in dash-case)?', transform: (value) => caseConvert.checkIsDashFormat(value) ? value : null }, 120 | { name: 'componentName', transform: (value) => caseConvert.dashToCap(value) + 'Component', useAnswer: 'selector' } 121 | ]; 122 | 123 | const getStyle = (options) => { 124 | if (opts.checkHasOption(options, ['is', 'inline-styles'])) { 125 | return { answers: getKnownStyle(true, []) }; 126 | } else { 127 | return { 128 | questions: getStyleQuestions 129 | }; 130 | } 131 | }; 132 | 133 | const getKnownStyle = (inline, answers = []) => { 134 | const selector = answers.find((answer) => answer.name === 'selector'); 135 | 136 | return [ 137 | { answer: setInlineStyles(inline, selector ? [selector] : []), name: 'styles' }, 138 | { answer: pickStyleAttribute(selector ? selector.answer : ''), name: 'styleAttribute' } 139 | ]; 140 | }; 141 | 142 | const getStyleQuestions = (knownAnswers) => [ 143 | { allowBlank: true, name: 'styles', question: 'Use inline styles (y/N)?', transform: inputs.createYesNoValue('n', knownAnswers, setInlineStyles) }, 144 | { name: 'styleAttribute', useAnswer: 'styles', transform: pickStyleAttribute } 145 | ]; 146 | 147 | const getTemplate = (options) => { 148 | if (opts.checkHasOption(options, ['it', 'inline-template'])) { 149 | return { answers: getKnownTemplate(true, []) }; 150 | } else { 151 | return { 152 | questions: getTemplateQuestions 153 | }; 154 | } 155 | }; 156 | 157 | const getKnownTemplate = (inline, answers = []) => { 158 | const selector = answers.find((answer) => answer.name === 'selector'); 159 | 160 | return [ 161 | { answer: setInlineTemplate(inline, selector ? [selector] : []), name: 'template' }, 162 | { answer: pickTemplateAttribute(selector ? selector.answer : ''), name: 'templateAttribute' } 163 | ]; 164 | }; 165 | 166 | const getTemplateQuestions = (knownAnswers) => [ 167 | { allowBlank: true, name: 'template', question: 'Use inline template (y/N)?', transform: inputs.createYesNoValue('n', knownAnswers, setInlineTemplate) }, 168 | { name: 'templateAttribute', useAnswer: 'template', transform: pickTemplateAttribute } 169 | ]; 170 | 171 | const setInlineStyles = (value, answers) => { 172 | const selector = answers.find((answer) => answer.name === 'selector'); 173 | 174 | return value ? '``' : `'./${selector.answer}.component.scss'`; 175 | }; 176 | 177 | const setInlineTemplate = (value, answers) => { 178 | const selector = answers.find((answer) => answer.name === 'selector'); 179 | 180 | return value ? '``' : `'./${selector.answer}.component.html'`; 181 | }; 182 | 183 | const pickStyleAttribute = (value) => { 184 | let attribute = 'Urls'; 185 | 186 | if (value === '``') { 187 | attribute = 's'; 188 | } 189 | 190 | return 'style' + attribute; 191 | }; 192 | 193 | const pickTemplateAttribute = (value) => { 194 | let attribute = 'Url'; 195 | 196 | if (value === '``') { 197 | attribute = ''; 198 | } 199 | 200 | return 'template' + attribute; 201 | }; 202 | 203 | const getLifecycleHooks = (options) => { 204 | if ((opts.checkHasOption(options, ['hooks']) && options.hooks.length > 0) || 205 | (opts.checkHasOption(options, ['h']) && options.h.length > 0)) { 206 | return { answers: getKnownLifecycleHooks((options.h || options.hooks || '').join(',')) }; 207 | } else { 208 | return { questions: getLifecycleHookQuestions }; 209 | } 210 | }; 211 | 212 | const getKnownLifecycleHooks = (hooks) => { 213 | hooks = setLifecycleHooks(hooks); 214 | 215 | return [ 216 | { answer: hooks, name: 'hooks' }, 217 | { answer: setLifecycleImplements(hooks), name: 'implements' }, 218 | { answer: setLifecycleMethods(hooks), name: 'lifecycleNg' } 219 | ]; 220 | }; 221 | 222 | const getLifecycleHookQuestions = () => [ 223 | { allowBlank: true, name: 'hooks', question: 'Lifecycle hooks (comma-separated):', transform: setLifecycleHooks }, 224 | { name: 'implements', useAnswer: 'hooks', transform: setLifecycleImplements }, 225 | { name: 'lifecycleNg', useAnswer: 'hooks', transform: setLifecycleMethods } 226 | ]; 227 | 228 | const setLifecycleHooks = (value = '') => { 229 | const hooksArray = value.split(',') 230 | .map(getHookName) 231 | .filter((hook) => !!hook); 232 | const hooks = [...new Set(hooksArray)]; 233 | const comma = ', '; 234 | 235 | if (hooks.length > 0) { 236 | value = comma + hooks.join(comma); 237 | } else { 238 | value = ''; 239 | } 240 | 241 | return value; 242 | }; 243 | 244 | const getHookName = (hook) => { 245 | hook = hook.trim().toLowerCase(); 246 | 247 | switch (hook) { 248 | case 'changes': 249 | case 'onchanges': 250 | return 'OnChanges'; 251 | case 'check': 252 | case 'docheck': 253 | return 'DoCheck'; 254 | case 'destroy': 255 | case 'ondestroy': 256 | return 'OnDestroy'; 257 | case 'init': 258 | case 'oninit': 259 | return 'OnInit'; 260 | } 261 | }; 262 | 263 | const setLifecycleImplements = (value) => { 264 | let implementers = ''; 265 | 266 | if (value.length > 0) { 267 | implementers = ` implements ${value.replace(/^, /, '')}`; 268 | } 269 | 270 | return implementers; 271 | }; 272 | 273 | const setLifecycleMethods = (value) => { 274 | let methods = '\n'; 275 | 276 | if (value) { 277 | methods = value.replace(/^, /, '').split(',').reduce((result, method) => 278 | `${result}\n ng${method.trim()}() {\n }\n`, 279 | methods ); 280 | } 281 | 282 | return methods; 283 | }; 284 | 285 | const getTemplates = (forExamples) => { 286 | const codeDir = forExamples ? 'examples' : 'src'; 287 | const componentDir = files.resolver.create(codeDir, '{{ selector }}'); 288 | 289 | return files.getTemplates(files.resolver.root(), __dirname, [ 290 | { 291 | destination: componentDir('{{ selector }}.component.ts'), 292 | name: 'app.ts' 293 | }, 294 | { 295 | destination: componentDir('{{ selector }}.component.spec.ts'), 296 | name: 'spec.ts' 297 | }, 298 | { 299 | blank: true, 300 | check: checkForStylesFile, 301 | destination: componentDir('{{ selector }}.component.scss') 302 | }, 303 | { 304 | blank: true, 305 | check: checkForTemplateFile, 306 | destination: componentDir('{{ selector }}.component.html') 307 | } 308 | ]); 309 | }; 310 | 311 | const checkForStylesFile = (answers) => !checkIsInline(answers, 'styles'); 312 | const checkForTemplateFile = (answers) => !checkIsInline(answers, 'template'); 313 | const checkIsInline = (answers, type) => { 314 | const answer = answers.find((answer) => answer.name === type); 315 | 316 | return answer.answer === '``'; 317 | } 318 | 319 | const notifyUser = (answers, forExamples) => { 320 | const componentName = answers.find((answer) => answer.name === 'componentName'); 321 | const moduleLocation = forExamples ? 'examples/example' : 'src/*'; 322 | const selector = answers.find((answer) => answer.name === 'selector'); 323 | 324 | logger.info( 325 | colorize.colorize(`Don't forget to add the following to the `, 'green'), 326 | `${ moduleLocation }.module.ts `, 327 | colorize.colorize('file:', 'green') 328 | ); 329 | logger.info(colorize.colorize(` import { ${componentName.answer} } from './${selector.answer}/${selector.answer}.component';`, 'cyan')); 330 | logger.info( 331 | colorize.colorize('And to add ', 'green'), 332 | `${componentName.answer} `, 333 | colorize.colorize('to the NgModule declarations list', 'green') 334 | ); 335 | }; 336 | -------------------------------------------------------------------------------- /commands/component/templates/app.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component{{ hooks }} 3 | } from '@angular/core'; 4 | 5 | @Component({ 6 | selector: '{{ prefix }}{{ selector }}', 7 | {{ styleAttribute }}: [{{ styles }}], 8 | {{ templateAttribute }}: {{ template }} 9 | }) 10 | export class {{ componentName }}{{ implements }} { 11 | constructor() {} 12 | {{ lifecycleNg }}} 13 | -------------------------------------------------------------------------------- /commands/component/templates/spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | import { 3 | async, 4 | ComponentFixture, 5 | TestBed 6 | } from '@angular/core/testing'; 7 | import { {{ componentName }} } from './{{ selector }}.component'; 8 | 9 | describe('{{ componentName }}', () => { 10 | let component: {{ componentName }}; 11 | let fixture: ComponentFixture<{{ componentName }}>; 12 | 13 | beforeEach(async(() => { 14 | TestBed.configureTestingModule({ 15 | declarations: [ 16 | {{ componentName }} 17 | ] 18 | }); 19 | TestBed.compileComponents(); 20 | })); 21 | 22 | beforeEach(() => { 23 | fixture = TestBed.createComponent({{ componentName }}); 24 | component = fixture.componentInstance; 25 | }); 26 | 27 | it('should create the {{ selector }}', () => { 28 | expect(component).toBeTruthy(); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /commands/directive/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const erector = require('erector-set'); 4 | 5 | const logging = require('../../tools/logging'); 6 | const utilities = require('../utilities'); 7 | 8 | const caseConvert = utilities.caseConvert; 9 | const colorize = utilities.colorize; 10 | const files = utilities.files; 11 | const opts = utilities.options; 12 | const resolver = files.resolver; 13 | 14 | let logger; 15 | let prefix; 16 | 17 | module.exports = function createDirective(rootDir, name) { 18 | logger = logging.create('Directive'); 19 | prefix = files.getSelectorPrefix('directive-selector'); 20 | 21 | const providedOptions = Array.from(arguments).slice(name && name[0] !== '-' ? 2 : 1); 22 | const options = opts.parseOptions(providedOptions, [ 23 | 'example', 'examples', 'x' 24 | ]); 25 | const forExamples = opts.checkIsForExamples(options); 26 | 27 | if (caseConvert.checkIsDashFormat(name)) { 28 | return generateWithKnownName(name, forExamples); 29 | } else { 30 | return erector.inquire(getAllQuestions()) 31 | .then((answers) => construct(answers, forExamples)); 32 | } 33 | }; 34 | 35 | const generateWithKnownName = (name, forExamples) => Promise.resolve().then(() => { 36 | construct([ 37 | { name: 'directiveName', answer: name }, 38 | { name: 'className', answer: caseConvert.dashToCap(name) + 'Directive' }, 39 | { name: 'selector', answer: addSelectorPrefix(name) }, 40 | ], forExamples); 41 | }); 42 | 43 | const addSelectorPrefix = (value) => { 44 | if (!prefix) { 45 | return caseConvert.dashToCamel(value); 46 | } else { 47 | return `${prefix}${caseConvert.dashToPascal(value)}`; 48 | } 49 | } 50 | 51 | const getAllQuestions = () => [ 52 | { name: 'directiveName', question: 'Directive name (in dash-case):', transform: caseConvert.testIsDashFormat }, 53 | { name: 'selector', transform: addSelectorPrefix, useAnswer: 'directiveName' }, 54 | { name: 'className', transform: (value) => caseConvert.dashToCap(value) + 'Directive', useAnswer: 'directiveName' } 55 | ]; 56 | 57 | const construct = (answers, forExamples) => { 58 | const results = erector.construct(answers, getTemplates(forExamples)); 59 | 60 | notifyUser(answers, forExamples); 61 | 62 | return results; 63 | }; 64 | 65 | const getTemplates = (forExamples) => { 66 | const codeDir = forExamples ? 'examples' : 'src'; 67 | const directiveDir = resolver.create(codeDir, 'directives'); 68 | 69 | return files.getTemplates(resolver.root(), __dirname, [ 70 | { 71 | destination: directiveDir('{{ directiveName }}.directive.ts'), 72 | name: 'app.ts' 73 | }, 74 | { 75 | destination: directiveDir('{{ directiveName }}.directive.spec.ts'), 76 | name: 'test.ts' 77 | } 78 | ]); 79 | }; 80 | 81 | const notifyUser = (answers, forExamples) => { 82 | const className = answers.find((answer) => answer.name === 'className'); 83 | const moduleLocation = forExamples ? 'examples/example' : 'src/*'; 84 | const directiveName = answers.find((answer) => answer.name === 'directiveName'); 85 | 86 | logger.info( 87 | colorize.colorize(`Don't forget to add the following to the`, 'green'), 88 | `${ moduleLocation }.module.ts`, 89 | colorize.colorize('file:', 'green') 90 | ); 91 | logger.info( 92 | colorize.colorize(` import { ${ className.answer } } from './directives/${ directiveName.answer }.directive';`, 'cyan') 93 | ); 94 | logger.info( 95 | colorize.colorize('And to add', 'green'), 96 | `${ className.answer }`, 97 | colorize.colorize('to the NgModule declarations list', 'green') 98 | ); 99 | }; 100 | -------------------------------------------------------------------------------- /commands/directive/templates/app.ts: -------------------------------------------------------------------------------- 1 | import { Directive } from '@angular/core'; 2 | 3 | @Directive({ 4 | selector: '[{{ selector }}]' 5 | }) 6 | export class {{ className }} { 7 | constructor() {} 8 | } 9 | -------------------------------------------------------------------------------- /commands/directive/templates/test.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | import { 3 | async, 4 | TestBed 5 | } from '@angular/core/testing'; 6 | 7 | import { {{ className }} } from './{{ directiveName }}.directive'; 8 | 9 | describe('{{ className }}', () => { 10 | it('', () => { 11 | const directive = new {{ className }}(); 12 | 13 | expect(directive).toBeTruthy(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /commands/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | component: require('./component'), 3 | directive: require('./directive'), 4 | initial: require('./initial'), 5 | npm: require('./npm'), 6 | pipe: require('./pipe'), 7 | service: require('./service'), 8 | upgrade: require('./upgrade') 9 | }; 10 | -------------------------------------------------------------------------------- /commands/initial/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const childProcess = require('child_process'); 4 | const erector = require('erector-set'); 5 | const path = require('path'); 6 | 7 | const logging = require('../../tools/logging'); 8 | const utilities = require('../utilities'); 9 | 10 | const caseConvert = utilities.caseConvert; 11 | const colorize = utilities.colorize; 12 | const files = utilities.files; 13 | const inputs = utilities.inputs; 14 | const opts = utilities.options; 15 | 16 | let logger; 17 | 18 | module.exports = (rootDir, ...args) => { 19 | logger = logging.create('Initial'); 20 | 21 | return erector.inquire(getQuestions(), true, getPreviousTransforms()).then((answers) => { 22 | const options = opts.parseOptions(args, [ 23 | 'no-install', 'ni', 24 | ]); 25 | const srcDir = files.resolver.create('src'); 26 | const templateList = [ 27 | { destination: files.resolver.root('.gitignore'), name: '__gitignore' }, 28 | { destination: files.resolver.root('.npmignore'), name: '__npmignore' }, 29 | { name: 'DEVELOPMENT.md' }, 30 | { blank: true, name: 'examples/example.component.html' }, 31 | { blank: true, name: 'examples/example.component.scss' }, 32 | { name: 'examples/example.component.ts' }, 33 | { name: 'examples/example.main.ts' }, 34 | { name: 'examples/example.module.ts' }, 35 | { name: 'examples/index.html' }, 36 | { blank: true, name: 'examples/styles.scss' }, 37 | { name: 'index.ts' }, 38 | { name: 'karma.conf.js', overwrite: true }, 39 | { destination: srcDir('{{ packageName }}.module.ts'), name: 'src/module.ts' }, 40 | { name: 'package.json', update: 'json' }, 41 | { name: 'README.md' }, 42 | { destination: srcDir('index.ts'), name: 'src/index.ts' }, 43 | { destination: srcDir('test.js'), name: 'src/test.js', overwrite: true }, 44 | { name: 'tsconfig.json', overwrite: true }, 45 | { name: 'tsconfig.doc.json', overwrite: true }, 46 | { name: 'tsconfig.es2015.json', overwrite: true }, 47 | { name: 'tsconfig.es5.json', overwrite: true }, 48 | { name: 'tsconfig.test.json', overwrite: true }, 49 | { name: 'tslint.json', overwrite: true }, 50 | { destination: srcDir('vendor.ts'), name: 'src/vendor.ts' }, 51 | { name: 'tasks/build.js', overwrite: true }, 52 | { name: 'tasks/copy-build.js', overwrite: true }, 53 | { name: 'tasks/copy-globs.js', overwrite: true }, 54 | { name: 'tasks/inline-resources.js', overwrite: true }, 55 | { name: 'tasks/rollup.js', overwrite: true }, 56 | { name: 'tasks/rollup-globals.js', overwrite: true }, 57 | { name: 'tasks/tag-version.js', overwrite: true }, 58 | { name: 'tasks/test.js', overwrite: true }, 59 | { name: 'webpack/webpack.common.js', overwrite: true }, 60 | { name: 'webpack/webpack.dev.js', overwrite: true }, 61 | { name: 'webpack/webpack.test.js', overwrite: true }, 62 | { name: 'webpack/webpack.utils.js', overwrite: true } 63 | ]; 64 | const librarianVersion = files.librarianVersions.get(); 65 | const gitAnswer = answers.find((answer) => answer.name === 'git'); 66 | const templates = files.getTemplates(rootDir, __dirname, templateList); 67 | const results = erector.construct(answers.concat({ answer: librarianVersion, name: 'librarianVersion' }), templates); 68 | 69 | process.chdir(rootDir); 70 | if (gitAnswer.answer) { 71 | initGit(rootDir); 72 | } 73 | 74 | if (!('ni' in options || 'no-install' in options)) { 75 | logger.info(colorize.colorize('Installing Node modules', 'cyan')); 76 | execute('npm i'); 77 | logger.info(colorize.colorize('Node modules installed', 'green')); 78 | } 79 | 80 | process.chdir(__dirname); 81 | 82 | return results; 83 | }).catch((error) => logger.error(colorize.colorize(error.message, 'red'))); 84 | }; 85 | 86 | const getQuestions = () => { 87 | const defaultName = files.include(files.resolver.root('package.json')).name; 88 | 89 | return [ 90 | { defaultAnswer: defaultName, name: 'name', question: `Library name:`, transform: checkNameFormat }, 91 | { name: 'packageName', useAnswer: 'name', transform: extractPackageName }, 92 | { allowBlank: true, defaultAnswer: '', name: 'prefix', question: `Prefix (component/directive selector):` }, 93 | { defaultAnswer: (answers) => caseConvert.dashToWords(answers[1].answer), name: 'readmeTitle', question: 'README Title:' }, 94 | { name: 'repoUrl', question: 'Repository URL:' }, 95 | { name: 'git', question: 'Reinitialize Git project (y/N)?', transform: inputs.createYesNoValue('n') }, 96 | { name: 'moduleName', useAnswer: 'packageName', transform: (name) => caseConvert.dashToCap(name) + 'Module' }, 97 | { name: 'version', question: 'Version:' } 98 | ]; 99 | }; 100 | 101 | const checkNameFormat = (name) => { 102 | if (!name) { 103 | name = ''; 104 | } else if (!checkPackageName(name)) { 105 | const message = 106 | ' Package name must have no capitals or special\n' + 107 | ' characters and be one of the below formats:\n' + 108 | ' @scope/package-name\n' + 109 | ' package-name'; 110 | 111 | logger.error(colorize.colorize(message, 'red')); 112 | name = null; 113 | } 114 | 115 | return name; 116 | }; 117 | 118 | const checkPackageName = (name) => 119 | typeof name === 'string' && 120 | name.length > 0 && 121 | name.length <= 214 && 122 | name.trim() === name && 123 | name.toLowerCase() === name && 124 | /^[^._-]/.test(name) && 125 | /[^._-]$/.test(name) && 126 | /^(?:@[^/]+[/])?[^/]+$/.test(name) && 127 | /^[a-z0-9]*$/.test(name.replace(/(^@|[-/])/g, '')); 128 | 129 | const extractPackageName = (name) => { 130 | if (utilities.checkIsScopedName(name)) { 131 | name = name.split('/')[1]; 132 | } 133 | 134 | return name; 135 | }; 136 | 137 | const getPreviousTransforms = () => ({ 138 | git: inputs.convertYesNoValue 139 | }); 140 | 141 | const initGit = (rootDir) => { 142 | logger.info(colorize.colorize('Removing existing Git project', 'yellow')); 143 | files.deleteFolder(files.resolver.root('.git')); 144 | logger.info(colorize.colorize('Initializing new Git project', 'cyan')); 145 | execute('git init'); 146 | logger.info(colorize.colorize('Git project initialized', 'green')); 147 | }; 148 | 149 | const execute = (command) => { 150 | childProcess.execSync(command, { stdio: [0, 1, 2] }); 151 | }; 152 | -------------------------------------------------------------------------------- /commands/initial/templates/DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | # Developing {{ readmeTitle }} 2 | 3 | ## Tasks 4 | 5 | The following commands are available through `npm run` (or, if configured 6 | `ngl`): 7 | 8 | Command | Purpose 9 | --- | --- 10 | build | Runs code through build process via Angular compiler (ngc) 11 | g | Generate code files (see above) 12 | lint | Verify code matches linting rules 13 | start | Run Webpack's dev-server on project (can be run as `npm start`) 14 | [test](#unit) | Execute unit tests (can be run as `npm test `) 15 | tagVersion | Creates tag for new version and publishes 16 | 17 | ## Adding External Scripts 18 | 19 | To add an external script, add it with a `script-loader!` prefix to the 20 | `scripts` array of `entry` in `webpack/webpack.dev.js` (for the dev server) 21 | and add it to the files array of `karma.conf.js` (for testing). 22 | 23 | An example, adding the file at `node_modules/ext-dep/dist/dep.min.js`: 24 | 25 | ```javascript 26 | /** webpack.dev.js **/ 27 | module.exports = { 28 | // other config 29 | entry: { 30 | app: [ path.resolve(rootDir, 'examples', 'example.main') ], 31 | scripts: [ 32 | // this is the external script line 33 | 'script-loader!' + path.resolve(rootDir, 'node_modules/ext-dep/dep.min') 34 | ], 35 | vendor: [ path.resolve(rootDir, 'src', 'vendor')], 36 | styles: [ path.resolve(rootDir, 'examples', 'styles.scss') ] 37 | }, 38 | // rest of config 39 | }; 40 | 41 | /** karma.conf.js **/ 42 | module.exports = function (config) { 43 | config.set({ 44 | basePath: '', 45 | frameworks: ['jasmine'], 46 | files: [ 47 | // this is the external script line 48 | 'node_modules/hammerjs/hammer.min.js', 49 | { pattern: './src/test.js', watched: false } 50 | ], 51 | // rest of config 52 | }); 53 | }; 54 | ``` 55 | 56 | ## Unit Testing 57 | 58 | Unit testing is done using Karma and Webpack. The setup is all done during the `initialize` command. 59 | The provided testing commands will watch your files for changes. 60 | 61 | The two following command is provided by default: 62 | 63 | ```shell 64 | npm test 65 | ``` 66 | 67 | This command calls the script at `tasks/test.js` and runs the Karma test runner to execute the tests. 68 | Prior to running Karma, the `test` command looks for a command line argument, if the argument is known, 69 | it will run the associated configuration, otherwise it will run the default configuration. 70 | 71 | #### Configurations 72 | 73 | Type | Testing TypeScript 74 | --- | --- 75 | default | Run through PhantomJS one time with no file watching 76 | all | Run through Chrome & PhantomJS with files being watched & tests automatically re-run 77 | headless| Run through PhantomJS with files being watched & tests automatically re-run 78 | watch | Run through Chrome with files being watched & tests automatically re-run 79 | 80 | Note that Chrome still requires a manual refresh on the Debug tab to see updated test results. 81 | 82 | ## Packaging 83 | 84 | Packaging is as simple as publishing to NPM by doing 85 | 86 | ```shell 87 | npm run tagVersion 88 | ``` 89 | 90 | To test your packages output before publishing, you can run 91 | 92 | ```shell 93 | npm pack 94 | ``` 95 | 96 | Which will generate a compressed file containing your library as it will look when packaged up and 97 | published to NPM. The basic structure of a published library is: 98 | 99 | ``` 100 | |__bundles/ 101 | |__{{ name }}.umd.js 102 | |__{{ name }}.umd.js.map 103 | |__{{ name }}.umd.min.js 104 | |__{{ name }}.bundle.min.js.map 105 | |__index.d.ts 106 | |__package.json 107 | |__README.md 108 | |__*.d.ts 109 | |__{{ name }}.d.ts 110 | |__{{ name }}.module.d.ts 111 | |__{{ name }}.es5.js 112 | |__{{ name }}.es5.js.map 113 | |__{{ name }}.js 114 | |__{{ name }}.js.map 115 | |__{{ name }}.metadata.json 116 | ``` 117 | 118 | As you can see, the packaging removes any files specific to developing your 119 | library. It, more importantly, creates distribution files for usage with many 120 | different module systems. 121 | -------------------------------------------------------------------------------- /commands/initial/templates/README.md: -------------------------------------------------------------------------------- 1 | # {{ readmeTitle }} 2 | 3 | ## Usage -------------------------------------------------------------------------------- /commands/initial/templates/__gitignore: -------------------------------------------------------------------------------- 1 | build 2 | coverage 3 | dist 4 | debug.log 5 | node_modules 6 | out-tsc 7 | -------------------------------------------------------------------------------- /commands/initial/templates/__npmignore: -------------------------------------------------------------------------------- 1 | *.spec.ts 2 | *.tgz 3 | .erector 4 | .gitignore 5 | .npmignore 6 | .vscode 7 | build 8 | coverage 9 | debug.log 10 | DEVELOPMENT.md 11 | dist 12 | index.ts 13 | karma.conf.js 14 | node_modules 15 | out-tsc 16 | src 17 | tasks 18 | test.ts 19 | tsconfig.*json 20 | tslint.json 21 | typings 22 | typings.json 23 | vendor.ts 24 | webpack 25 | -------------------------------------------------------------------------------- /commands/initial/templates/examples/example.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'example-root', 5 | templateUrl: './example.component.html', 6 | styleUrls: [] 7 | }) 8 | export class ExampleComponent { } 9 | -------------------------------------------------------------------------------- /commands/initial/templates/examples/example.main.ts: -------------------------------------------------------------------------------- 1 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 2 | import { ExampleModule } from './example.module'; 3 | 4 | platformBrowserDynamic().bootstrapModule(ExampleModule); 5 | -------------------------------------------------------------------------------- /commands/initial/templates/examples/example.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | import { ExampleComponent } from './example.component'; 5 | import { {{ moduleName }} } from '../index'; 6 | 7 | @NgModule({ 8 | declarations: [ 9 | ExampleComponent 10 | ], 11 | imports: [ 12 | BrowserModule, 13 | {{ moduleName }} 14 | ], 15 | providers: [], 16 | bootstrap: [ExampleComponent] 17 | }) 18 | export class ExampleModule { } 19 | -------------------------------------------------------------------------------- /commands/initial/templates/examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ moduleName }} Tutorial 5 | 6 | 7 | 8 | 9 | 10 | Loading... 11 | 12 | -------------------------------------------------------------------------------- /commands/initial/templates/index.ts: -------------------------------------------------------------------------------- 1 | export * from './src'; 2 | -------------------------------------------------------------------------------- /commands/initial/templates/karma.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const erectorUtils = require('erector-set/src/utils'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | 7 | module.exports = function (config) { 8 | const base = { 9 | basePath: '', 10 | frameworks: ['jasmine'], 11 | files: [ 12 | { pattern: './src/test.js', watched: false } 13 | ], 14 | mime: { 15 | 'text/x-typescript': ['ts','tsx'] 16 | }, 17 | plugins: [ 18 | 'karma-chrome-launcher', 19 | 'karma-jasmine', 20 | 'karma-phantomjs-launcher', 21 | 'karma-coverage-istanbul-reporter', 22 | 'karma-sourcemap-loader', 23 | 'karma-webpack' 24 | ], 25 | preprocessors: { 26 | './src/test.js': ['webpack', 'sourcemap'] 27 | }, 28 | coverageIstanbulReporter: { 29 | dir: './coverage', 30 | fixWebpackSourcePaths: true, 31 | reports: ['html', 'lcovonly'] 32 | }, 33 | reporters: ['progress', 'coverage-istanbul'], 34 | port: 9876, 35 | colors: true, 36 | logLevel: config.LOG_INFO, 37 | autoWatch: true, 38 | browsers: ['Chrome', 'PhantomJS'], 39 | singleRun: false, 40 | webpackServer: { noInfo: true } 41 | }; 42 | const fullConfig = mergeCustomConfig(base, config); 43 | 44 | config.set(fullConfig); 45 | }; 46 | 47 | const mergeCustomConfig = (base, karmaConfig) => { 48 | const customConfigPath = path.resolve( 49 | __dirname, 50 | 'configs', 51 | 'karma.conf.js' 52 | ); 53 | let fullConfig = base; 54 | 55 | if (fs.existsSync(customConfigPath)) { 56 | fullConfig = mergeConfigs(base, require(customConfigPath), karmaConfig); 57 | } 58 | 59 | return fullConfig; 60 | }; 61 | 62 | const mergeConfigs = (base, custom, karmaConfig) => { 63 | let mergedConfig = base; 64 | 65 | if (erectorUtils.checkIsType(custom, 'function')) { 66 | custom = custom(karmaConfig); 67 | } 68 | 69 | if (custom) { 70 | const arrays = mergeConfigArrays(base, custom); 71 | const objects = mergeConfigObjects(base, custom); 72 | const primitives = mergeConfigPrimitives(base, custom); 73 | const customAttributes = Object.assign({}, arrays, objects, primitives); 74 | 75 | mergedConfig = Object.assign( 76 | {}, base, customAttributes 77 | ); 78 | } 79 | 80 | return mergedConfig; 81 | }; 82 | 83 | const mergeConfigArrays = (base, custom) => { 84 | const attributes = ['browsers', 'files', 'plugins', 'reporters']; 85 | return mergeConfigAttributes(base, custom, attributes, (baseAttribute, customAttribute) => 86 | erectorUtils.mergeDeep(baseAttribute, customAttribute) 87 | ); 88 | }; 89 | 90 | const mergeConfigObjects = (base, custom) => { 91 | const attributes = ['preprocessors']; 92 | return mergeConfigAttributes(base, custom, attributes, (baseAttribute, customAttribute) => 93 | Object.assign(customAttribute, baseAttribute) 94 | ); 95 | }; 96 | 97 | const mergeConfigPrimitives = (base, custom) => { 98 | const attributes = ['color', 'logLevel', 'port']; 99 | 100 | return mergeConfigAttributes(base, custom, attributes, (baseAttribute, customAttribute) => 101 | customAttribute 102 | ); 103 | }; 104 | 105 | const mergeConfigAttributes = (base, custom, attributes, callback) => { 106 | return attributes.reduce((config, attribute) => { 107 | if (attribute in custom) { 108 | config[attribute] = callback(base[attribute], custom[attribute]); 109 | } 110 | 111 | return config; 112 | }, {}); 113 | }; 114 | -------------------------------------------------------------------------------- /commands/initial/templates/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "{{ name }}", 3 | "version": "{{ version }}", 4 | "description": "{{ readmeTitle }}, an Angular library", 5 | "main": "./bundles/{{ packageName }}.umd.js", 6 | "module": "./{{ name }}.es5.js", 7 | "es2015": "./{{ name }}.js", 8 | "typings": "./{{ packageName }}.d.ts", 9 | "scripts": { 10 | "build": "node ./tasks/build && npm run compodoc", 11 | "g": "node ./node_modules/angular-librarian", 12 | "lint": "tslint ./src/**/*.ts", 13 | "postbuild": "rimraf build", 14 | "posttagVersion": "npm run build && npm publish dist", 15 | "prebuild": "rimraf dist out-tsc", 16 | "start": "webpack-dev-server --open --config ./webpack/webpack.dev.js", 17 | "tagVersion": "node ./tasks/tag-version", 18 | "test": "node ./tasks/test", 19 | "compodoc": "compodoc src -p tsconfig.doc.json --disableInternal --disablePrivate" 20 | }, 21 | "keywords": [], 22 | "author": "", 23 | "license": "ISC", 24 | "dependencies": { 25 | "@angular/common": "^5.0.0", 26 | "@angular/compiler": "^5.0.0", 27 | "@angular/core": "^5.0.0", 28 | "@angular/platform-browser": "^5.0.0", 29 | "@angular/platform-browser-dynamic": "^5.0.0", 30 | "core-js": "^2.4.1", 31 | "rxjs": "^5.5.2", 32 | "zone.js": "^0.8.14" 33 | }, 34 | "devDependencies": { 35 | "@angular/compiler-cli": "^5.0.0", 36 | "@compodoc/compodoc": "^1.0.7", 37 | "@types/jasmine": "^2.0.0", 38 | "@types/node": "^8.0.0", 39 | "angular-librarian": "{{ librarianVersion }}", 40 | "angular2-template-loader": "0.6.0", 41 | "awesome-typescript-loader": "^3.0.0", 42 | "codelyzer": "^4.0.0", 43 | "css-loader": "^0.28.0", 44 | "css-to-string-loader": "^0.1.0", 45 | "extract-text-webpack-plugin": "^3.0.0", 46 | "file-loader": "^1.0.0", 47 | "fs-extra": "^2.1.2", 48 | "html-webpack-plugin": "^2.0.0", 49 | "istanbul-instrumenter-loader": "^3.0.0", 50 | "jasmine-core": "^2.0.0", 51 | "jasmine-spec-reporter": "^4.0.0", 52 | "karma": "^1.0.0", 53 | "karma-chrome-launcher": "^2.0.0", 54 | "karma-coverage-istanbul-reporter": "^1.3.0", 55 | "karma-jasmine": "^1.0.2", 56 | "karma-phantomjs-launcher": "^1.0.2", 57 | "karma-sourcemap-loader": "^0.3.7", 58 | "karma-webpack": "^2.0.0", 59 | "node-sass": "^4.0.0", 60 | "phantomjs-prebuilt": "^2.1.7", 61 | "raw-loader": "^0.5.1", 62 | "rimraf": "^2.5.3", 63 | "rollup": "0.52.1", 64 | "rollup-plugin-commonjs": "^8.0.2", 65 | "rollup-plugin-node-resolve": "3.0.0", 66 | "rollup-plugin-sourcemaps": "0.4.2", 67 | "rollup-plugin-uglify": "2.0.1", 68 | "sass-loader": "^6.0.0", 69 | "script-loader": "^0.7.0", 70 | "semver": "^5.0.0", 71 | "source-map-loader": "^0.2.0", 72 | "style-loader": "^0.19.0", 73 | "tslint": "^5.0.0", 74 | "tslint-loader": "^3.0.0", 75 | "typescript": "~ 2.4.2", 76 | "url-loader": "^0.6.2", 77 | "webpack": "^3.0.0", 78 | "webpack-dev-server": "^2.0.0", 79 | "webpack-merge": "^0.14.0", 80 | "webpack-node-externals": "^1.5.4" 81 | }, 82 | "repository": { 83 | "url": "{{ repoUrl }}" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /commands/initial/templates/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './{{ packageName }}.module'; 2 | -------------------------------------------------------------------------------- /commands/initial/templates/src/module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | @NgModule({ 5 | declarations: [ 6 | 7 | ], 8 | exports: [ 9 | 10 | ], 11 | imports: [ 12 | CommonModule 13 | ] 14 | }) 15 | export class {{ moduleName }} { 16 | static forRoot() { 17 | return { 18 | ngModule: {{ moduleName }}, 19 | providers: [] 20 | }; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /commands/initial/templates/src/test.js: -------------------------------------------------------------------------------- 1 | require('core-js/es6'); 2 | require('core-js/es7'); 3 | require('zone.js/dist/zone'); 4 | require('zone.js/dist/long-stack-trace-zone'); 5 | require('zone.js/dist/async-test'); 6 | require('zone.js/dist/fake-async-test'); 7 | require('zone.js/dist/sync-test'); 8 | require('zone.js/dist/proxy'); 9 | require('zone.js/dist/jasmine-patch'); 10 | 11 | const browserTesting = require('@angular/platform-browser-dynamic/testing'); 12 | const coreTesting = require('@angular/core/testing'); 13 | const context = require.context('./', true, /\.spec\.ts$/); 14 | 15 | Error.stackTraceLimit = Infinity; 16 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 2000; 17 | 18 | coreTesting.TestBed.resetTestEnvironment(); 19 | coreTesting.TestBed.initTestEnvironment( 20 | browserTesting.BrowserDynamicTestingModule, 21 | browserTesting.platformBrowserDynamicTesting() 22 | ); 23 | 24 | context.keys().forEach(context); -------------------------------------------------------------------------------- /commands/initial/templates/src/vendor.ts: -------------------------------------------------------------------------------- 1 | // polyfills Angular 2 requires to be loaded BEFORE the application 2 | // only used in library development--not packaged 3 | import 'core-js/es6/symbol'; 4 | import 'core-js/es6/object'; 5 | import 'core-js/es6/function'; 6 | import 'core-js/es6/parse-int'; 7 | import 'core-js/es6/parse-float'; 8 | import 'core-js/es6/number'; 9 | import 'core-js/es6/math'; 10 | import 'core-js/es6/string'; 11 | import 'core-js/es6/date'; 12 | import 'core-js/es6/array'; 13 | import 'core-js/es6/regexp'; 14 | import 'core-js/es6/map'; 15 | import 'core-js/es6/set'; 16 | import 'core-js/es6/reflect'; 17 | 18 | import 'core-js/es7/reflect'; 19 | import 'zone.js/dist/zone'; 20 | -------------------------------------------------------------------------------- /commands/initial/templates/tasks/build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs-extra'); 4 | const ngc = require('@angular/compiler-cli/src/main').main; 5 | const librarianUtils = require('angular-librarian/commands/utilities'); 6 | const path = require('path'); 7 | 8 | const copyGlobs = require('./copy-globs'); 9 | const copyToBuild = require('./copy-build'); 10 | const inlineResources = require('./inline-resources'); 11 | const rollup = require('./rollup'); 12 | 13 | const colorize = librarianUtils.colorize; 14 | const execute = librarianUtils.execute; 15 | const rootDir = path.resolve(__dirname, '..'); 16 | const buildDir = path.resolve(rootDir, 'build'); 17 | const distDir = path.resolve(rootDir, 'dist'); 18 | const libName = require(path.resolve(rootDir, 'package.json')).name; 19 | const srcDir = path.resolve(rootDir, 'src'); 20 | const tscDir = path.resolve(rootDir, 'out-tsc'); 21 | const es5Dir = path.resolve(tscDir, 'lib-es5'); 22 | const es2015Dir = path.resolve(tscDir, 'lib-es2015'); 23 | 24 | const runPromise = (message, fn) => { 25 | return function() { 26 | console.info(colorize.colorize(message, 'cyan')); 27 | return fn().then(complete); 28 | }; 29 | }; 30 | 31 | const complete = (depth = 0) => { 32 | const spaces = ' '.repeat(depth); 33 | console.info(colorize.colorize(`${ spaces }> Complete`, 'green')); 34 | }; 35 | const evaluateExitCode = (exitCode) => { 36 | return exitCode === 0 ? Promise.resolve() : Promise.reject(); 37 | }; 38 | const getAngularCompilerVersion = () => { 39 | const npm = /^win/.test(process.platform) ? 'npm.cmd' : 'npm'; 40 | const lines = execute.execute(npm, ['list', '--depth=0', '@angular/compiler']).split(/\r?\n/); 41 | const compilerLine = lines.find((line) => line.indexOf('@angular/compiler@') !== -1); 42 | let version; 43 | 44 | if (compilerLine) { 45 | version = compilerLine.match(/\bangular\/compiler@[^\s]+\s?/) || ['']; 46 | version = version[0].trim().replace('angular/compiler@', ''); 47 | } 48 | 49 | if (!version || version === '(empty)') { 50 | Promise.reject('Angular Compiler is not installed!'); 51 | } 52 | 53 | return version; 54 | }; 55 | const compileCode = () => Promise.all([2015, 5].map((type) => { 56 | const compilerVersion = getAngularCompilerVersion(); 57 | const majorCompilerVersion = +compilerVersion.split('.')[0]; 58 | 59 | if (majorCompilerVersion >= 5) { 60 | const exitCode = ngc(['--project', path.resolve(rootDir, `tsconfig.es${ type }.json`)]); 61 | return evaluateExitCode(exitCode); 62 | } else { 63 | ngc({ project: path.resolve(rootDir, `tsconfig.es${ type }.json`)}) 64 | .then((exitCode) => 65 | evaluateExitCode(exitCode) 66 | ) 67 | } 68 | })); 69 | const copyMetadata = () => 70 | copyGlobs(['**/*.d.ts', '**/*.metadata.json'], es2015Dir, distDir); 71 | const copyPackageFiles = () => 72 | copyGlobs(['.npmignore', 'package.json', 'README.md'], rootDir, distDir) 73 | .then(() => { 74 | const contents = fs.readFileSync(path.resolve(distDir, 'package.json'), 'utf8'); 75 | 76 | return fs.writeFileSync(path.resolve(distDir, 'package.json'), contents.replace('"dependencies":', '"peerDependencies":')); 77 | }); 78 | const copySource = () => copyGlobs('**/*', srcDir, buildDir); 79 | const doInlining = () => inlineResources(buildDir, 'src'); 80 | const rollupBundles = () => rollup(libName, { 81 | dist: distDir, 82 | es2015: es2015Dir, 83 | es5: es5Dir, 84 | root: rootDir 85 | }); 86 | 87 | return Promise.resolve() 88 | .then(runPromise('Copying `src` files into `build`', copySource)) 89 | .then(runPromise('Inlining resources', doInlining)) 90 | .then(runPromise('Compiling code', compileCode)) 91 | .then(runPromise('Copying typings + metadata to `dist`', copyMetadata)) 92 | .then(runPromise('Generating bundles via rollup', rollupBundles)) 93 | .then(runPromise('Copying package files to `dist`', copyPackageFiles)) 94 | .catch((error) => { 95 | console.error('\x1b[31m%s\x1b[0m', '> Build failed\n'); 96 | console.error(error); 97 | process.exit(1); 98 | }); 99 | -------------------------------------------------------------------------------- /commands/initial/templates/tasks/copy-build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs-extra'); 4 | 5 | // copy all src files -> build 6 | const copyToBuild = (buildDir, sourceDir) => { 7 | fs.ensureDirSync(buildDir); 8 | fs.emptyDirSync(buildDir); 9 | fs.copySync(sourceDir, buildDir); 10 | }; 11 | 12 | module.exports = copyToBuild; 13 | 14 | if (!module.parent) { 15 | copyToBuild('./build', './src'); 16 | } 17 | -------------------------------------------------------------------------------- /commands/initial/templates/tasks/copy-globs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs-extra'); 4 | const glob = require('glob'); 5 | const path = require('path'); 6 | 7 | const copy = (globs, from, to) => { 8 | if (typeof globs === 'string') { 9 | globs = [globs]; 10 | } 11 | 12 | fs.ensureDir(to); 13 | return Promise.all( 14 | globs.map((fileGlob) => copyGlob(fileGlob, from, to)) 15 | ); 16 | }; 17 | 18 | const copyGlob = (fileGlob, from, to) => new Promise((resolve, reject) => { 19 | glob(fileGlob, { cwd: from, nodir: true }, (error, files) => { 20 | if (error) reject(error); 21 | 22 | files.forEach((file) => { 23 | const origin = path.resolve(from, file); 24 | const destination = path.resolve(to, file); 25 | const contents = fs.readFileSync(origin, 'utf8'); 26 | 27 | fs.ensureDirSync(path.dirname(destination)); 28 | fs.writeFileSync(destination, contents); 29 | }); 30 | 31 | resolve(); 32 | }); 33 | }); 34 | 35 | module.exports = copy; 36 | -------------------------------------------------------------------------------- /commands/initial/templates/tasks/inline-resources.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | // original code by the Angular Material 2 team 3 | 4 | const fs = require('fs'); 5 | const glob = require('glob'); 6 | const path = require('path'); 7 | const sass = require('node-sass'); 8 | 9 | const inlineResources = (globs, sourcePrefix) => { 10 | if (typeof globs === 'string') { 11 | globs = [globs]; 12 | } 13 | 14 | return Promise.all( 15 | globs.map((pattern) => replaceSource(pattern, sourcePrefix)) 16 | ); 17 | }; 18 | 19 | const replaceSource = (pattern, sourcePrefix) => { 20 | // pattern is a directory 21 | if (pattern.indexOf('*') === -1) { 22 | pattern = path.join(pattern, '**', '*'); 23 | } 24 | 25 | return new Promise((resolve, reject) => { 26 | glob(pattern, {}, (error, files) => { 27 | if (error) reject(Error); 28 | 29 | files.filter((name) => /\.ts$/.test(name)).forEach((filePath) => { 30 | try { 31 | inlineFileResources(filePath, sourcePrefix); 32 | } catch (readError) { 33 | reject(readError); 34 | } 35 | }); 36 | 37 | resolve(); 38 | }); 39 | }); 40 | }; 41 | 42 | const inlineFileResources = (filePath, sourcePrefix) => { 43 | const content = fs.readFileSync(filePath, 'utf8'); 44 | const inlineContents = inlineResourcesFromString(content, sourcePrefix, (url) => 45 | path.join(path.dirname(filePath), url) 46 | ); 47 | 48 | fs.writeFileSync(filePath, inlineContents); 49 | }; 50 | 51 | const inlineResourcesFromString = (content, sourcePrefix, callback) => [ 52 | inlineTemplate, inlineStyle, removeModuleId 53 | ].reduce((final, method) => method(final, sourcePrefix, callback), content); 54 | 55 | const inlineTemplate = (content, sourcePrefix, callback) => 56 | content.replace(/templateUrl:\s*'([^']+?\.html)'/g, (match, url) => { 57 | const mini = getMiniContents(url, sourcePrefix, callback); 58 | 59 | return `template: "${mini}"`; 60 | }); 61 | 62 | const inlineStyle = (content, sourcePrefix, callback) => 63 | content.replace(/styleUrls:\s*(\[[\s\S]*?\])/gm, (match, styleUrls) => { 64 | const urls = eval(styleUrls); // string -> array 65 | return 'styles: [' + urls.map((url) => { 66 | const mini = getMiniContents(url, sourcePrefix, callback); 67 | 68 | return `"${mini}"`; 69 | }).join(',\n') + ']'; 70 | }); 71 | 72 | const getMiniContents = (url, sourcePrefix, callback) => { 73 | const srcFile = callback(url); 74 | const file = srcFile.replace(/^dist/, sourcePrefix) 75 | const srcDir = file.slice(0, file.lastIndexOf(path.sep)); 76 | let template = ''; 77 | 78 | if (file.match(/\.s(a|c)ss$/)) { 79 | // convert SASS -> CSS 80 | template = sass.renderSync({ 81 | file, 82 | importer: (url) => handleSassImport(url, srcDir) 83 | }); 84 | template = template.css.toString(); 85 | } else { 86 | template = fs.readFileSync(file, 'utf8'); 87 | } 88 | 89 | return minifyText(template); 90 | }; 91 | 92 | const handleSassImport = (url, srcDir) => { 93 | const fullUrl = getFullSassUrl(url, srcDir); 94 | let isPartial = false; 95 | let validUrls = getSassUrls(fullUrl); 96 | 97 | // if we can't find the file, try to 98 | // see find it as a partial (underscore-prefixed) 99 | if (validUrls.length === 0) { 100 | validUrls = getSassUrls(fullUrl, true); 101 | isPartial = true; 102 | } 103 | 104 | const file = getSassImportUrl(validUrls); 105 | 106 | // CSS files don't get compiled in 107 | return /\.css$/.test(file) ? 108 | { contents: fs.readFileSync(file, 'utf8') } : 109 | { file }; 110 | }; 111 | 112 | const getSassUrls = (url, partial) => { 113 | let extensions = ['sass', 'scss']; 114 | 115 | if (!partial) { 116 | extensions = extensions.concat('', 'css'); 117 | } else { 118 | const lastSlash = url.lastIndexOf(path.sep); 119 | const urlDir = url.slice(0, lastSlash); 120 | const fileName = url.slice(lastSlash + 1); 121 | 122 | if (fileName[0] !== '_') { 123 | url = urlDir + path.sep + '_' + fileName; 124 | } 125 | } 126 | 127 | return extensions.reduce((valid, extension) => { 128 | const extensionUrl = verifyUrl(url, extension); 129 | 130 | if (extensionUrl) { 131 | valid = valid.concat(extensionUrl); 132 | } 133 | 134 | return valid; 135 | }, []); 136 | }; 137 | 138 | const verifyUrl = (url, extension) => { 139 | if (extension) { 140 | url = url + `.${ extension }`; 141 | } 142 | 143 | if (!fs.existsSync(url)) { 144 | url = null; 145 | } 146 | 147 | return url; 148 | } 149 | 150 | // convert ~-prefixed filenames to node_modules-prefixed 151 | // make all others relative to srcDir 152 | const getFullSassUrl = (url, srcDir) => 153 | /^~/.test(url) ? 154 | path.resolve('node_modules', url.slice(1)) : 155 | path.resolve(srcDir, url); 156 | 157 | const getSassImportUrl = (validUrls) => { 158 | if (validUrls.length !== 1) { 159 | let error = 'Cannot determine Sass/CSS file to process. '; 160 | 161 | if (validUrls.length === 0) { 162 | error = error + `\n There are no files matching ${ url }`; 163 | } else { 164 | error = error + 'Candidates:\n ' + validUrls.join('\n ') 165 | + '\nPlease delete or rename all but one of these files or specify the extension to use.'; 166 | } 167 | 168 | throw new Error(error); 169 | } 170 | 171 | return validUrls[0]; 172 | }; 173 | 174 | 175 | const minifyText = (text) => text 176 | .replace(/([\n\r]\s*)+/gm, ' ') 177 | .replace(/"/g, '\\"'); 178 | 179 | const removeModuleId = (content) => 180 | content.replace(/\s*moduleId:\s*module\.id\s*,?\s*/gm, ''); 181 | 182 | module.exports = inlineResources; 183 | 184 | if (!module.parent) { 185 | inlineResources('./build', 'src'); 186 | } 187 | -------------------------------------------------------------------------------- /commands/initial/templates/tasks/rollup.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const erectorUtils = require('erector-set/src/utils'); 4 | const fs = require('fs-extra'); 5 | const librarianUtils = require('angular-librarian/commands/utilities'); 6 | const path = require('path'); 7 | const rollup = require('rollup'); 8 | const rollupCommon = require('rollup-plugin-commonjs'); 9 | const rollupNodeResolve = require('rollup-plugin-node-resolve'); 10 | const rollupSourcemaps = require('rollup-plugin-sourcemaps'); 11 | const rollupUglify = require('rollup-plugin-uglify'); 12 | 13 | const rollupGlobals = require('./rollup-globals'); 14 | 15 | const doRollup = (libName, dirs) => { 16 | const nameParts = extractName(libName); 17 | const es5Entry = path.resolve(dirs.es5, `${nameParts.package}.js`); 18 | const es2015Entry = path.resolve(dirs.es2015, `${nameParts.package}.js`); 19 | const destinations = generateDestinations(dirs.dist, nameParts); 20 | const baseConfig = generateConfig({ 21 | input: es5Entry, 22 | external: Object.keys(rollupGlobals), 23 | globals: rollupGlobals, 24 | name: librarianUtils.caseConvert.dashToCamel(nameParts.package), 25 | onwarn: function rollupOnWarn(warning) { 26 | // keeps TypeScript this errors down 27 | if (warning.code !== 'THIS_IS_UNDEFINED') { 28 | console.warn(warning.message); 29 | } 30 | }, 31 | plugins: [ 32 | rollupNodeResolve({ 33 | jsnext: true, 34 | module: true 35 | }), 36 | rollupSourcemaps() 37 | ], 38 | sourcemap: true 39 | }, dirs.root); 40 | const fesm2015Config = Object.assign({}, baseConfig, { 41 | input: es2015Entry, 42 | output: { 43 | file: destinations.fesm2015, 44 | format: 'es' 45 | } 46 | }); 47 | const fesm5Config = Object.assign({}, baseConfig, { 48 | output: { 49 | file: destinations.fesm5, 50 | format: 'es' 51 | } 52 | }); 53 | const minUmdConfig = Object.assign({}, baseConfig, { 54 | output: { 55 | file: destinations.minUmd, 56 | format: 'umd' 57 | }, 58 | plugins: baseConfig.plugins.concat([rollupUglify({})]) 59 | }); 60 | const umdConfig = Object.assign({}, baseConfig, { 61 | output: { 62 | file: destinations.umd, 63 | format: 'umd' 64 | } 65 | }); 66 | 67 | const bundles = [ 68 | fesm2015Config, 69 | fesm5Config, 70 | minUmdConfig, 71 | umdConfig 72 | ].map(config => 73 | rollup.rollup(config) 74 | .then(bundle => bundle.write({ 75 | file: config.output.file, 76 | format: config.output.format, 77 | ...config 78 | })) 79 | ); 80 | 81 | return Promise.all(bundles); 82 | }; 83 | 84 | const extractName = (libName) => { 85 | const isScoped = librarianUtils.checkIsScopedName(libName); 86 | const nameParts = { 87 | package: libName, 88 | scope: undefined 89 | }; 90 | 91 | if (isScoped) { 92 | const parts = libName.split('/', 2); 93 | 94 | nameParts.package = parts[1]; 95 | nameParts.scope = parts[0]; 96 | } 97 | 98 | return nameParts; 99 | }; 100 | 101 | const generateDestinations = (dist, nameParts) => { 102 | const bundleDest = path.resolve(dist, 'bundles'); 103 | let fesmDest = path.resolve(dist); 104 | 105 | if (nameParts.scope) { 106 | fesmDest = path.resolve(fesmDest, nameParts.scope); 107 | fs.ensureDirSync(fesmDest); 108 | } 109 | 110 | return Object.freeze({ 111 | fesm2015: path.resolve(fesmDest, `${nameParts.package}.js`), 112 | fesm5: path.resolve(fesmDest, `${nameParts.package}.es5.js`), 113 | minUmd: path.resolve(bundleDest, `${nameParts.package}.umd.min.js`), 114 | umd: path.resolve(bundleDest, `${nameParts.package}.umd.js`) 115 | }); 116 | }; 117 | 118 | const generateConfig = (base, rootDir) => { 119 | let commonjsIncludes = ['node_modules/rxjs/**']; 120 | const customLocation = path.resolve(rootDir, 'configs', 'rollup.config.js'); 121 | 122 | if (fs.existsSync(customLocation)) { 123 | const custom = require(customLocation); 124 | const external = (custom.external || []).filter((external) => base.external.indexOf(external) === -1); 125 | const includes = (custom.commonjs || []).filter((include) => commonjsIncludes.indexOf(include) === -1); 126 | 127 | base.external = base.external.concat(external); 128 | base.globals = erectorUtils.mergeDeep(custom.globals, base.globals); 129 | commonjsIncludes = commonjsIncludes.concat(includes); 130 | } 131 | 132 | base.plugins.unshift( 133 | rollupCommon({ 134 | include: commonjsIncludes 135 | }) 136 | ); 137 | 138 | return base; 139 | }; 140 | 141 | module.exports = doRollup; 142 | -------------------------------------------------------------------------------- /commands/initial/templates/tasks/tag-version.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const childProcess = require('child_process'); 4 | 5 | module.exports = (type) => { 6 | const extraArgument = getCommand(type); 7 | 8 | return new Promise((resolve, reject) => { 9 | try { 10 | const output = childProcess.spawnSync( 11 | 'np', 12 | ['--no-publish'].concat(extraArgument), 13 | { stdio: 'inherit' } 14 | ); 15 | 16 | if (output.error) { 17 | throw output.error; 18 | } else if (output.status !== 0) { 19 | throw new Error('Execution of `np` errored, see above for more information.'); 20 | } else { 21 | resolve(); 22 | } 23 | } catch (error) { 24 | process.stderr.write(error.message); 25 | reject(error.message); 26 | process.exit(1); 27 | } 28 | }); 29 | }; 30 | 31 | const getCommand = (type) => { 32 | switch (type) { 33 | case 'nc': 34 | case 'no-cleanup': 35 | return '--no-cleanup'; 36 | case 'y': 37 | case 'yolo': 38 | return '--yolo'; 39 | default: 40 | return []; 41 | } 42 | } 43 | 44 | if (!module.parent) { 45 | return module.exports(process.argv[2]); 46 | } 47 | -------------------------------------------------------------------------------- /commands/initial/templates/tasks/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const Server = require('karma').Server; 6 | 7 | function run(type) { 8 | const config = getConfig(type); 9 | const server = new Server(config, function(exitCode) { 10 | process.exit(exitCode); 11 | }); 12 | 13 | server.start(); 14 | } 15 | 16 | function getConfig(type) { 17 | switch (type) { 18 | case 'headless': 19 | case 'hl': 20 | case 'h': 21 | return getHeadlessConfig(); 22 | case 'all': 23 | case 'a': 24 | return getAllConfig(); 25 | case 'watch': 26 | case 'w': 27 | return getWatchConfig(); 28 | default: 29 | return getSingleConfig(); 30 | } 31 | } 32 | 33 | function getSingleConfig() { 34 | let config = getHeadlessConfig(); 35 | 36 | config.singleRun = true; 37 | 38 | return config; 39 | } 40 | 41 | function getHeadlessConfig() { 42 | let config = getAllConfig(); 43 | 44 | config.browsers = ['PhantomJS']; 45 | 46 | return config; 47 | } 48 | 49 | function getWatchConfig() { 50 | let config = getAllConfig(true); 51 | 52 | config.browsers = ['Chrome']; 53 | 54 | return config; 55 | } 56 | 57 | const getAllConfig = (watch) => ({ 58 | configFile: path.resolve(__dirname, '..', 'karma.conf.js'), 59 | webpack: require(path.resolve(__dirname, '..', 'webpack', 'webpack.test.js'))(watch), 60 | }); 61 | 62 | module.exports = run; 63 | 64 | if (!module.parent) { 65 | run(process.argv[2]); 66 | } 67 | -------------------------------------------------------------------------------- /commands/initial/templates/tsconfig.doc.json: -------------------------------------------------------------------------------- 1 | { 2 | "angularCompilerOptions": { 3 | "strictMetadataEmit": true, 4 | "skipTemplateCodegen": true 5 | }, 6 | "compilerOptions": { 7 | "baseUrl": "", 8 | "declaration": true, 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "lib": ["es2015", "dom"], 12 | "mapRoot": "./", 13 | "module": "es2015", 14 | "moduleResolution": "node", 15 | "outDir": "./dist", 16 | "sourceMap": true, 17 | "target": "es5", 18 | "typeRoots": [ 19 | "./node_modules/@types" 20 | ] 21 | }, 22 | "awesomeTypescriptLoaderOptions": { 23 | "forkChecker": true 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /commands/initial/templates/tsconfig.es2015.json: -------------------------------------------------------------------------------- 1 | { 2 | "angularCompilerOptions": { 3 | "annotateForClosureCompiler": true, 4 | "flatModuleId": "{{ name }}", 5 | "flatModuleOutFile": "{{ packageName }}.js", 6 | "genDir": "../out-tsc/lib-gen-dir/", 7 | "strictMetadataEmit": true, 8 | "skipTemplateCodegen": true 9 | }, 10 | "compilerOptions": { 11 | "baseUrl": "", 12 | "declaration": true, 13 | "emitDecoratorMetadata": true, 14 | "experimentalDecorators": true, 15 | "lib": ["es2015", "dom"], 16 | "module": "es2015", 17 | "moduleResolution": "node", 18 | "outDir": "./out-tsc/lib-es2015/", 19 | "skipLibCheck": true, 20 | "sourceMap": true, 21 | "stripInternal": true, 22 | "suppressImplicitAnyIndexErrors": true, 23 | "target": "es2015", 24 | "typeRoots": [ 25 | "./node_modules/@types" 26 | ] 27 | }, 28 | "files": [ 29 | "./build/index.ts" 30 | ], 31 | "awesomeTypescriptLoaderOptions": { 32 | "forkChecker": true 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /commands/initial/templates/tsconfig.es5.json: -------------------------------------------------------------------------------- 1 | { 2 | "angularCompilerOptions": { 3 | "annotateForClosureCompiler": true, 4 | "flatModuleId": "{{ name }}", 5 | "flatModuleOutFile": "{{ packageName }}.js", 6 | "genDir": "../out-tsc/lib-gen-dir/", 7 | "strictMetadataEmit": true, 8 | "skipTemplateCodegen": true 9 | }, 10 | "compilerOptions": { 11 | "baseUrl": "", 12 | "declaration": true, 13 | "emitDecoratorMetadata": true, 14 | "experimentalDecorators": true, 15 | "lib": ["es2015", "dom"], 16 | "module": "es2015", 17 | "moduleResolution": "node", 18 | "outDir": "./out-tsc/lib-es5/", 19 | "skipLibCheck": true, 20 | "sourceMap": true, 21 | "stripInternal": true, 22 | "suppressImplicitAnyIndexErrors": true, 23 | "target": "es5", 24 | "typeRoots": [ 25 | "./node_modules/@types" 26 | ] 27 | }, 28 | "files": [ 29 | "./build/index.ts" 30 | ], 31 | "awesomeTypescriptLoaderOptions": { 32 | "forkChecker": true 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /commands/initial/templates/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "angularCompilerOptions": { 3 | "strictMetadataEmit": true, 4 | "skipTemplateCodegen": true 5 | }, 6 | "compilerOptions": { 7 | "baseUrl": "", 8 | "declaration": true, 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "lib": ["es2015", "dom"], 12 | "mapRoot": "./", 13 | "module": "es2015", 14 | "moduleResolution": "node", 15 | "outDir": "./dist", 16 | "sourceMap": true, 17 | "target": "es5", 18 | "typeRoots": [ 19 | "./node_modules/@types" 20 | ] 21 | }, 22 | "files": [ 23 | "./src/index.ts" 24 | ], 25 | "awesomeTypescriptLoaderOptions": { 26 | "forkChecker": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /commands/initial/templates/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "buildOnSave": false, 3 | "compilerOptions": { 4 | "emitDecoratorMetadata": true, 5 | "experimentalDecorators": true, 6 | "lib": ["es2015", "dom"], 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "removeComments": false, 10 | "sourceMap": true, 11 | "target": "es5", 12 | "typeRoots": [ 13 | "./node_modules/@types" 14 | ] 15 | }, 16 | "compileOnSave": false, 17 | "files": [ 18 | "./src/{{ packageName }}.module.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /commands/initial/templates/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "callable-types": true, 7 | "class-name": true, 8 | "comment-format": [ 9 | true, 10 | "check-space" 11 | ], 12 | "curly": true, 13 | "eofline": true, 14 | "forin": true, 15 | "import-blacklist": [true, "rxjs"], 16 | "import-spacing": true, 17 | "indent": [ 18 | true, 19 | "spaces" 20 | ], 21 | "interface-over-type-literal": true, 22 | "label-position": true, 23 | "max-line-length": [ 24 | true, 25 | 140 26 | ], 27 | "member-access": false, 28 | "member-ordering": [ 29 | true, 30 | { 31 | "order": [ 32 | "public-static-field", 33 | "public-instance-field", 34 | "public-constructor", 35 | "private-static-field", 36 | "private-instance-field", 37 | "private-constructor", 38 | "public-instance-method", 39 | "protected-instance-method", 40 | "private-instance-method" 41 | ] 42 | } 43 | ], 44 | "no-arg": true, 45 | "no-bitwise": true, 46 | "no-console": [ 47 | true, 48 | "debug", 49 | "info", 50 | "time", 51 | "timeEnd", 52 | "trace" 53 | ], 54 | "no-construct": true, 55 | "no-debugger": true, 56 | "no-duplicate-variable": true, 57 | "no-empty": false, 58 | "no-empty-interface": true, 59 | "no-eval": true, 60 | "no-inferrable-types": true, 61 | "no-shadowed-variable": true, 62 | "no-string-literal": false, 63 | "no-string-throw": true, 64 | "no-switch-case-fall-through": true, 65 | "no-trailing-whitespace": true, 66 | "no-unused-expression": true, 67 | "no-var-keyword": true, 68 | "object-literal-sort-keys": false, 69 | "one-line": [ 70 | true, 71 | "check-open-brace", 72 | "check-catch", 73 | "check-else", 74 | "check-whitespace" 75 | ], 76 | "prefer-const": true, 77 | "quotemark": [ 78 | true, 79 | "single" 80 | ], 81 | "radix": true, 82 | "semicolon": [ 83 | true, 84 | "always" 85 | ], 86 | "triple-equals": [ 87 | true, 88 | "allow-null-check" 89 | ], 90 | "typedef-whitespace": [ 91 | true, 92 | { 93 | "call-signature": "nospace", 94 | "index-signature": "nospace", 95 | "parameter": "nospace", 96 | "property-declaration": "nospace", 97 | "variable-declaration": "nospace" 98 | } 99 | ], 100 | "typeof-compare": true, 101 | "unified-signatures": true, 102 | "variable-name": false, 103 | "whitespace": [ 104 | true, 105 | "check-branch", 106 | "check-decl", 107 | "check-operator", 108 | "check-separator", 109 | "check-type" 110 | ], 111 | "directive-selector": [true, "attribute", "{{ prefix }}", "camelCase"], 112 | "component-selector": [true, "element", "{{ prefix }}", "kebab-case"], 113 | "use-input-property-decorator": true, 114 | "use-output-property-decorator": true, 115 | "use-host-property-decorator": true, 116 | "no-input-rename": true, 117 | "no-output-rename": true, 118 | "use-life-cycle-interface": true, 119 | "use-pipe-transform-interface": true, 120 | "component-class-suffix": true, 121 | "directive-class-suffix": true 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /commands/initial/templates/webpack/webpack.common.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const webpack = require('webpack'); 5 | const webpackMerge = require('webpack-merge'); 6 | 7 | const ExtractTextPlugin = require("extract-text-webpack-plugin"); 8 | const ContextReplacementPlugin = webpack.ContextReplacementPlugin; 9 | const LoaderOptionsPlugin = webpack.LoaderOptionsPlugin; 10 | 11 | const webpackUtils = require('./webpack.utils'); 12 | 13 | const getCommonConfig = (type) => { 14 | const tsconfigType = type !== 'dev' ? `.${ type }` : ''; 15 | 16 | return { 17 | module: { 18 | rules: [ 19 | { 20 | exclude: /node_modules/, 21 | test: /\.ts$/, 22 | use: [ 23 | 'awesome-typescript-loader?configFileName=' + webpackUtils.rootPath(`tsconfig${ tsconfigType }.json`), 24 | 'angular2-template-loader?keepUrl=true' 25 | ] 26 | }, 27 | { test: /\.html$/, use: 'raw-loader' }, 28 | { 29 | use: ['url-loader?limit=10000'], 30 | test: /\.(woff2?|ttf|eot|svg|jpg|jpeg|json|gif|png)(\?v=\d+\.\d+\.\d+)?$/ 31 | } 32 | ] 33 | }, 34 | performance: { hints: false }, 35 | plugins: [ 36 | new ContextReplacementPlugin( 37 | /angular(\\|\/)core(\\|\/)(@angular|esm5)/, 38 | __dirname 39 | ), 40 | new LoaderOptionsPlugin({ 41 | debug: true, 42 | options: { 43 | emitErrors: true 44 | } 45 | }), 46 | new ExtractTextPlugin("*.css") 47 | ], 48 | resolve: { 49 | extensions: [ '.js', '.ts' ], 50 | modules: [ webpackUtils.rootPath('node_modules') ] 51 | } 52 | }; 53 | }; 54 | 55 | module.exports = (type, typeConfig) => { 56 | const configs = [getCommonConfig(type), typeConfig]; 57 | const customConfigPath = webpackUtils.rootPath('configs', `webpack.${ type }.js`); 58 | 59 | if (fs.existsSync(customConfigPath)) { 60 | let customConfig = require(customConfigPath); 61 | 62 | if (Object.prototype.toString.call(customConfig) === '[object Function]') { 63 | customConfig = customConfig(); 64 | } 65 | 66 | configs.push(customConfig); 67 | } 68 | 69 | return webpackMerge.apply(null, configs); 70 | }; 71 | -------------------------------------------------------------------------------- /commands/initial/templates/webpack/webpack.dev.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const HtmlWebpack = require('html-webpack-plugin'); 4 | const webpack = require('webpack'); 5 | 6 | const ChunkWebpack = webpack.optimize.CommonsChunkPlugin; 7 | const webpackCommon = require('./webpack.common'); 8 | const webpackUtils = require('./webpack.utils'); 9 | 10 | const entryPoints = [ 11 | 'vendor', 12 | 'scripts', 13 | 'styles', 14 | 'app' 15 | ]; 16 | const examplePath = function examples() { 17 | return webpackUtils.relayArguments( 18 | webpackUtils.rootPath, 19 | 'examples', 20 | arguments 21 | ); 22 | }; 23 | 24 | module.exports = webpackCommon('dev', { 25 | devServer: { 26 | contentBase: webpackUtils.rootPath('dist'), 27 | port: 9000 28 | }, 29 | devtool: 'cheap-module-eval-source-map', 30 | entry: { 31 | app: [ examplePath('example.main') ], 32 | scripts: [], 33 | vendor: [ webpackUtils.srcPath('vendor') ], 34 | styles: [ examplePath('styles.scss') ] 35 | }, 36 | module: { 37 | rules: webpackUtils.buildRules({ 38 | cssExtract: examplePath(), 39 | sassLoader: examplePath('styles.scss') 40 | }, { 41 | include: examplePath(), 42 | test: /styles\.scss$/, 43 | use: ['style-loader', 'css-loader', 'sass-loader'] 44 | }) 45 | }, 46 | output: { 47 | filename: '[name].bundle.js', 48 | path: webpackUtils.rootPath('dist') 49 | }, 50 | plugins: [ 51 | new ChunkWebpack({ 52 | filename: 'vendor.bundle.js', 53 | minChunks: Infinity, 54 | name: 'vendor' 55 | }), 56 | new HtmlWebpack({ 57 | // shameless/shamefully stolen from Angular CLI 58 | chunksSortMode: function(left, right) { 59 | const leftIndex = entryPoints.indexOf(left.names[0]); 60 | const rightIndex = entryPoints.indexOf(right.names[0]); 61 | let direction = 0; 62 | 63 | if (leftIndex > rightIndex) { 64 | direction = 1; 65 | } else if (leftIndex < rightIndex) { 66 | direction = -1; 67 | } 68 | 69 | return direction; 70 | }, 71 | filename: 'index.html', 72 | inject: 'body', 73 | template: examplePath('index.html') 74 | }) 75 | ] 76 | }); 77 | 78 | -------------------------------------------------------------------------------- /commands/initial/templates/webpack/webpack.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const webpack = require('webpack'); 4 | 5 | const SourceMapDevToolPlugin = webpack.SourceMapDevToolPlugin; 6 | const webpackCommon = require('./webpack.common'); 7 | const webpackUtils = require('./webpack.utils'); 8 | 9 | module.exports = (watch) => { 10 | return webpackCommon('test', { 11 | devtool: watch ? 'inline-source-map' : 'cheap-module-eval-source-map', 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.s?css$/, 16 | use: ['raw-loader', 'css-loader', 'sass-loader'] 17 | }, 18 | { 19 | enforce: 'pre', 20 | exclude: /node_modules/, 21 | test: /\.ts$/, 22 | use: 'tslint-loader' 23 | }, 24 | { 25 | enforce: 'post', 26 | exclude: [ 27 | /node_modules/, 28 | /\.(e2e|spec\.)ts$/ 29 | ], 30 | test: /\.ts$/, 31 | use: 'istanbul-instrumenter-loader?esModules=true' 32 | } 33 | ] 34 | }, 35 | plugins: [ 36 | new SourceMapDevToolPlugin({ 37 | filename: null, 38 | test: /\.ts$/ 39 | }) 40 | ], 41 | resolve: { 42 | modules: [ webpackUtils.srcPath() ], 43 | moduleExtensions: ['-loader'] 44 | } 45 | }); 46 | }; 47 | -------------------------------------------------------------------------------- /commands/initial/templates/webpack/webpack.utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ExtractText = require('extract-text-webpack-plugin'); 4 | const path = require('path'); 5 | 6 | function rootPath() { 7 | const rootDir = path.resolve(__dirname, '..'); 8 | return relayArguments(path.resolve, rootDir, arguments); 9 | } 10 | exports.rootPath = rootPath; 11 | 12 | function srcPath() { 13 | return relayArguments(rootPath, 'src', arguments); 14 | }; 15 | exports.srcPath = srcPath; 16 | 17 | function relayArguments(method, prefix, args) { 18 | const fullArguments = [prefix].concat( 19 | Array.prototype.slice.apply(args) 20 | ); 21 | 22 | return method.apply(null, fullArguments); 23 | } 24 | exports.relayArguments = relayArguments; 25 | 26 | exports.buildRules = (excludes, extraRules) => { 27 | let cssExtractExcludes = [srcPath()]; 28 | let sassLoaderExcludes = [/node_modules/]; 29 | let rules; 30 | 31 | excludes = excludes || {}; 32 | if (excludes.cssExtract) { 33 | cssExtractExcludes = cssExtractExcludes.concat(excludes.cssExtract); 34 | } 35 | 36 | if (excludes.sassLoader) { 37 | sassLoaderExcludes = sassLoaderExcludes.concat(excludes.sassLoader); 38 | } 39 | 40 | rules = [ 41 | { 42 | exclude: cssExtractExcludes, 43 | test: /\.css$/, 44 | use: ExtractText.extract({ 45 | fallback: 'style-loader', 46 | use: 'css-loader?sourceMap' 47 | }) 48 | }, 49 | { 50 | exclude: /node_modules/, 51 | test: /\.css$/, 52 | use: ['css-to-string-loader', 'css-loader'] 53 | }, 54 | { 55 | exclude: sassLoaderExcludes, 56 | use: ['css-to-string-loader', 'css-loader', 'sass-loader'], 57 | test: /\.scss$/ 58 | } 59 | ]; 60 | 61 | if (extraRules) { 62 | rules = rules.concat(extraRules); 63 | } 64 | 65 | return rules; 66 | }; 67 | -------------------------------------------------------------------------------- /commands/logging.js: -------------------------------------------------------------------------------- 1 | module.exports = require('../tools/logging'); -------------------------------------------------------------------------------- /commands/npm/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const childProcess = require('child_process'); 4 | 5 | module.exports = function (rootDir, type) { 6 | const args = Array.from(arguments).slice(2); 7 | /* istanbul ignore next */ 8 | const cmd = /^win/.test(process.platform) ? 'npm.cmd' : 'npm'; 9 | 10 | return new Promise((resolve, reject) => { 11 | try { 12 | const output = childProcess.spawnSync( 13 | cmd, 14 | ['run'].concat(getNpmCommand(type, args)), 15 | { stdio: ['inherit', 'inherit', 'pipe'] } 16 | ); 17 | const stderr = output.stderr && output.stderr.toString(); 18 | 19 | if (output.error) { 20 | throw output.error; 21 | } else if (stderr) { 22 | throw new Error(stderr); 23 | } else if (output.status !== 0) { 24 | throw new Error(`Execution of "${ type }" errored, see above for more information.`); 25 | } else { 26 | resolve(); 27 | } 28 | } catch (error) { 29 | reject(error.message); 30 | } 31 | }); 32 | }; 33 | 34 | const getNpmCommand = (command, args) => { 35 | switch (command) { 36 | case 'b': 37 | case 'build': 38 | return 'build'; 39 | case 'l': 40 | case 'lint': 41 | return 'lint'; 42 | case 'pub': 43 | case 'publish': 44 | return ['tagVersion'].concat(args); 45 | case 'v': 46 | case 'serve': 47 | return 'start'; 48 | case 't': 49 | case 'test': 50 | return ['test'].concat(args); 51 | default: 52 | return ''; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /commands/pipe/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const erector = require('erector-set'); 4 | const path = require('path'); 5 | 6 | const logging = require('../logging'); 7 | const utilities = require('../utilities'); 8 | 9 | const caseConvert = utilities.caseConvert; 10 | const colorize = utilities.colorize; 11 | const files = utilities.files; 12 | const opts = utilities.options; 13 | let logger; 14 | 15 | module.exports = function createPipe(rootDir, name) { 16 | logger = logging.create('Pipe'); 17 | 18 | const providedOptions = Array.from(arguments).slice(name && name[0] !== '-' ? 2 : 1); 19 | const options = opts.parseOptions(providedOptions, [ 20 | 'example', 21 | 'examples', 22 | 'x' 23 | ]); 24 | const forExamples = opts.checkIsForExamples(options); 25 | 26 | if (caseConvert.checkIsDashFormat(name)) { 27 | return generateWithKnownPipeName(name, forExamples); 28 | } else { 29 | return erector.inquire(getAllQuestions()).then((answers) => 30 | construct(answers, forExamples) 31 | ); 32 | } 33 | }; 34 | 35 | const generateWithKnownPipeName = (name, forExamples) => { 36 | const knownAnswers = [ 37 | { name: 'filename', answer: name }, 38 | { name: 'pipeName', answer: caseConvert.dashToCamel(name) }, 39 | { name: 'className', answer: caseConvert.dashToCap(name) + 'Pipe'} 40 | ]; 41 | 42 | return Promise.resolve() 43 | .then(() => construct(knownAnswers, forExamples)); 44 | }; 45 | 46 | const construct = (answers, forExamples) => { 47 | const result = erector.construct(answers, getTemplates(forExamples)); 48 | notifyUser(answers, forExamples); 49 | 50 | return result; 51 | }; 52 | 53 | const getTemplates = (forExamples) => { 54 | const codeDir = forExamples ? 'examples' : 'src'; 55 | const pipesDir = files.resolver.create(codeDir, 'pipes'); 56 | 57 | 58 | return files.getTemplates(files.resolver.root(), __dirname, [ 59 | { 60 | destination: pipesDir('{{ filename }}.pipe.ts'), 61 | name: 'app.ts' 62 | }, 63 | { 64 | destination: pipesDir('{{ filename }}.pipe.spec.ts'), 65 | name: 'spec.ts' 66 | } 67 | ]); 68 | }; 69 | 70 | const getAllQuestions = () => [ 71 | { name: 'filename', question: 'Pipe name (in dash-case):', transform: (value) => caseConvert.checkIsDashFormat(value) ? value : null }, 72 | { name: 'pipeName', transform: caseConvert.dashToCamel, useAnswer: 'filename' }, 73 | { name: 'className', transform: (value) => caseConvert.dashToCap(value) + 'Pipe', useAnswer: 'filename' } 74 | ]; 75 | 76 | const notifyUser = (answers, forExamples) => { 77 | const className = answers.find((answer) => answer.name === 'className'); 78 | const filename = answers.find((answer) => answer.name === 'filename'); 79 | const moduleLocation = forExamples ? 'examples/example' : 'src/*'; 80 | 81 | logger.info( 82 | colorize.colorize(`Don't forget to add the following to the`, 'green'), 83 | `${ moduleLocation }.module.ts`, 84 | colorize.colorize('file:', 'green') 85 | ); 86 | logger.info( 87 | colorize.colorize(` import { ${className.answer} } from './pipes/${filename.answer}.pipe';`, 'cyan') 88 | ); 89 | logger.info( 90 | colorize.colorize('And to add', 'green'), 91 | className.answer, 92 | colorize.colorize('to the NgModule declarations list', 'green') 93 | ); 94 | }; 95 | -------------------------------------------------------------------------------- /commands/pipe/templates/app.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | @Pipe({ name: '{{ pipeName }}' }) 4 | export class {{ className }} implements PipeTransform { 5 | transform(value: any, args?: any): any { 6 | 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /commands/pipe/templates/spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { TestBed, async } from '@angular/core/testing'; 4 | import { {{ className }} } from './{{ filename }}.pipe'; 5 | 6 | describe('{{ className }}', () => { 7 | it('', () => { 8 | const pipe = new {{ className }}(); 9 | expect(pipe).toBeTruthy(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /commands/service/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const erector = require('erector-set'); 4 | const path = require('path'); 5 | 6 | const logging = require('../../tools/logging'); 7 | const { caseConvert, colorize, files, options } = require('../../tools/utilities'); 8 | 9 | let logger; 10 | 11 | module.exports = function createService(rootDir, name) { 12 | logger = logging.create('Service'); 13 | 14 | const providedOptions = Array.from(arguments).slice(name && name[0] !== '-' ? 2 : 1); 15 | const opts = options.parseOptions(providedOptions, [ 16 | 'example', 17 | 'examples', 18 | 'x' 19 | ]); 20 | const forExamples = options.checkIsForExamples(opts); 21 | 22 | if (caseConvert.checkIsDashFormat(name)) { 23 | return generateWithKnownName(name, forExamples); 24 | } else { 25 | return erector.inquire(getAllQuestions()) 26 | .then((answers) => construct(answers, forExamples)); 27 | } 28 | }; 29 | 30 | const generateWithKnownName = (name, forExamples) => { 31 | const knownAnswers = [ 32 | { name: 'filename', answer: name}, 33 | { name: 'serviceName', answer: caseConvert.dashToCap(name) + 'Service' } 34 | ]; 35 | 36 | return Promise.resolve() 37 | .then(() => construct(knownAnswers, forExamples)); 38 | } 39 | 40 | const getAllQuestions = () => [ 41 | { 42 | name: 'filename', 43 | question: 'Service name (in dash-case):', 44 | transform: (value) => caseConvert.checkIsDashFormat(value) ? value : null 45 | }, 46 | { 47 | name: 'serviceName', 48 | transform: (value) => caseConvert.dashToCap(value) + 'Service', 49 | useAnswer: 'filename' 50 | } 51 | ]; 52 | 53 | const construct = (answers, forExamples) => { 54 | const results = erector.construct(answers, getTemplates(forExamples)); 55 | 56 | notifyUser(answers, forExamples); 57 | return results; 58 | }; 59 | 60 | const getTemplates = (forExamples) => { 61 | const codeDir = forExamples ? 'examples' : 'src'; 62 | const servicesDir = files.resolver.create(codeDir, 'services'); 63 | 64 | return files.getTemplates(files.resolver.root(), __dirname, [ 65 | { 66 | destination: servicesDir('{{ filename }}.service.ts'), 67 | name: 'app.ts' 68 | }, 69 | { 70 | destination: servicesDir('{{ filename }}.service.spec.ts'), 71 | name: 'spec.ts' 72 | } 73 | ]); 74 | }; 75 | 76 | const notifyUser = (answers, forExamples) => { 77 | const moduleLocation = forExamples ? 'examples/example' : 'src/*'; 78 | const serviceName = answers.find((answer) => answer.name === 'serviceName'); 79 | const filename = answers.find((answer) => answer.name === 'filename'); 80 | 81 | logger.info( 82 | colorize.colorize(`Don't forget to add the following to the`, 'green'), 83 | `${ moduleLocation }.module.ts`, 84 | colorize.colorize('file:', 'green') 85 | ); 86 | logger.info( 87 | colorize.colorize(` import { ${serviceName.answer} } from './services/${filename.answer}.service';`, 'cyan') 88 | ); 89 | logger.info( 90 | colorize.colorize('And to add', 'green'), 91 | serviceName.answer, 92 | colorize.colorize('to the NgModule providers list or add as a provider to one or more components', 'green') 93 | ); 94 | }; 95 | -------------------------------------------------------------------------------- /commands/service/templates/app.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable() 4 | export class {{ serviceName }} { 5 | constructor() {} 6 | } 7 | -------------------------------------------------------------------------------- /commands/service/templates/spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-vars */ 2 | import { 3 | getTestBed, 4 | TestBed 5 | } from '@angular/core/testing'; 6 | 7 | import { {{ serviceName }} } from './{{ filename }}.service'; 8 | 9 | describe('{{ serviceName }}', () => { 10 | let service: {{ serviceName }}; 11 | 12 | beforeEach(() => { 13 | TestBed.configureTestingModule({ 14 | providers: [{{ serviceName }}] 15 | }); 16 | service = getTestBed().get({{ serviceName }}); 17 | }); 18 | 19 | it('', () => { 20 | expect(service).toBeTruthy(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /commands/upgrade/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const erector = require('erector-set'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const childProcess = require('child_process'); 7 | 8 | const logging = require('../../tools/logging'); 9 | const { colorize, files, execute, inputs } = require('../../tools/utilities'); 10 | const { librarianVersions } = files; 11 | 12 | let logger; 13 | 14 | module.exports = () => { 15 | logger = logging.create('Upgrade'); 16 | /* istanbul ignore next */ 17 | const npmCommand = /^win/.test(process.platform) ? 'npm.cmd' : 'npm'; 18 | 19 | return upgradeLibrarian(npmCommand) 20 | .then(() => upgradeFiles(npmCommand)); 21 | }; 22 | 23 | const upgradeLibrarian = (npmCommand) => { 24 | const version = librarianVersions.get(); 25 | 26 | if (!librarianVersions.checkIsBranch(version)) { 27 | return getLibrarianVersions(npmCommand) 28 | .then((versions) => installLibrarian(npmCommand, versions)); 29 | } else { 30 | return Promise.resolve() 31 | .then(() => upgradeBranchLibrarian(npmCommand, version)); 32 | } 33 | }; 34 | 35 | const getLibrarianVersions = (npm) => new Promise((resolve, reject) => { 36 | logger.info(colorize.colorize('Identifying the *newest* Angular Librarian version', 'blue')); 37 | const available = execute.execute(npm, ['show', 'angular-librarian', 'version']); 38 | logger.info(colorize.colorize('Identifying the *installed* Angular Librarian version', 'blue')); 39 | 40 | try { 41 | const installed = parseInstalledVersion(execute.execute(npm, ['list', '--depth=0', 'angular-librarian'])); 42 | 43 | resolve({ 44 | available, 45 | installed 46 | }); 47 | } catch (error) { 48 | reject(error.message); 49 | } 50 | }); 51 | 52 | const parseInstalledVersion = (installed) => { 53 | const lines = installed.split(/\r?\n/); 54 | const librarianLine = lines.find((line) => line.indexOf('angular-librarian@') !== -1); 55 | let version; 56 | 57 | if (librarianLine) { 58 | version = librarianLine.match(/\bangular-librarian@[^\s]+\s?/) || ['']; 59 | version = version[0].trim().replace('angular-librarian@', ''); 60 | } 61 | 62 | if (!version || version === '(empty)') { 63 | throw new Error('Angular Librarian is not installed. Not sure how that\'s possible!\n\n\tRun `npm i -D angular-librarian` to install'); 64 | } 65 | 66 | return version; 67 | }; 68 | 69 | const installLibrarian = (npm, { available, installed}) => { 70 | const update = require('semver').gt(available, installed); 71 | 72 | logger.info( 73 | colorize.colorize(' Upgrade of Angular Librarian is', 'yellow'), 74 | update ? '' : colorize.colorize('NOT', 'red'), 75 | colorize.colorize('required.', 'yellow') 76 | ); 77 | 78 | if (update) { 79 | logger.info(colorize.colorize(`Installing Angular Librarian ${ available }`, 'blue')); 80 | execute.execute(npm, ['i', '-D', `angular-librarian@${ available }`]); 81 | } 82 | }; 83 | 84 | const upgradeBranchLibrarian = (npm, version) => { 85 | logger.info(colorize.colorize('Upgrading Angular Librarian from:', 'blue')); 86 | logger.info(colorize.colorize(' ' + version, 'magenta')); 87 | 88 | execute.execute(npm, ['up', 'angular-librarian']); 89 | }; 90 | 91 | const upgradeFiles = (npmCommand) => erector.inquire([ 92 | { 93 | allowBlank: true, 94 | name: 'proceed', 95 | question: colorize.colorize('The following will overwrite some of the files in your project. Would you like to continue ', 'red') + 96 | '(y/N)' + 97 | colorize.colorize('?', 'red'), 98 | transform: inputs.createYesNoValue('n', []) 99 | } 100 | ]).then((answers) => { 101 | if (answers[0].answer) { 102 | updateFiles(); 103 | runPostUpgradeInstall(npmCommand); 104 | logger.info(colorize.colorize('Upgrade success!', 'green')); 105 | } else { 106 | logger.info(colorize.colorize(' Upgrade cancelled.', 'yellow')); 107 | } 108 | }); 109 | 110 | const updateFiles = () => { 111 | logger.info(colorize.colorize('Updating managed files to latest versions', 'blue')); 112 | const answers = getErectorAnswers().concat([ 113 | { answer: librarianVersions.get(), name: 'librarianVersion' }, 114 | { answer: files.getSelectorPrefix(), name: 'prefix' } 115 | ]); 116 | const srcDir = files.resolver.create('src'); 117 | const fileList = [ 118 | { destination: files.resolver.root('.gitignore'), name: '__gitignore', update: updateFlatFile }, 119 | { destination: files.resolver.root('.npmignore'), name: '__npmignore', update: updateFlatFile }, 120 | { name: 'DEVELOPMENT.md' }, 121 | { name: 'karma.conf.js', overwrite: true }, 122 | { name: 'package.json', update: updatePackageJson }, 123 | /* 124 | the TypeScript files should be 'json', but array merging 125 | on JSON duplicates values so ['es6', 'dom'] merged 126 | from two files would be ['es6', 'dom', 'es6', 'dom'] 127 | */ 128 | { name: 'tsconfig.es5.json', overwrite: true }, 129 | { name: 'tsconfig.es2015.json', overwrite: true }, 130 | { name: 'tsconfig.json', overwrite: true }, 131 | { name: 'tsconfig.doc.json', overwrite: true }, 132 | { name: 'tsconfig.test.json', overwrite: true }, 133 | { name: 'tslint.json', overwrite: true }, 134 | { destination: srcDir('test.js'), name: 'src/test.js', overwrite: true }, 135 | { name: 'tasks/build.js', overwrite: true }, 136 | { name: 'tasks/copy-build.js', overwrite: true }, 137 | { name: 'tasks/copy-globs.js', overwrite: true }, 138 | { name: 'tasks/inline-resources.js', overwrite: true }, 139 | { name: 'tasks/rollup.js', overwrite: true }, 140 | { name: 'tasks/rollup-globals.js', overwrite: true }, 141 | { name: 'tasks/tag-version.js', overwrite: true }, 142 | { name: 'tasks/test.js', overwrite: true }, 143 | { name: 'webpack/webpack.common.js', overwrite: true }, 144 | { name: 'webpack/webpack.dev.js', overwrite: true }, 145 | { name: 'webpack/webpack.test.js', overwrite: true }, 146 | { name: 'webpack/webpack.utils.js', overwrite: true } 147 | ]; 148 | const templates = files.getTemplates( 149 | files.resolver.root(), 150 | files.resolver.manual(__dirname, '..', 'initial'), 151 | fileList 152 | ); 153 | 154 | erector.construct(answers, templates); 155 | 156 | logger.info(colorize.colorize(' Files have been upgraded!', 'green')); 157 | }; 158 | 159 | const getErectorAnswers = () => { 160 | // we do this because packageName may not exist from older versions 161 | const pkg = files.include(files.resolver.root('package.json'), 'json'); 162 | let answers = files.open(files.resolver.root('.erector'), 'json'); 163 | let name = answers.find((answer) => answer.name === 'name').name; 164 | const hasPackageName = answers.find((answer) => answer.name === 'packageName'); 165 | 166 | if (name !== pkg.name) { 167 | name = pkg.name; 168 | } 169 | 170 | if (!hasPackageName) { 171 | answers.push({ 172 | answer: name, 173 | name: 'packageName' 174 | }); 175 | } 176 | 177 | return answers; 178 | }; 179 | 180 | const updatePackageJson = (existing, replacement) => { 181 | const merged = JSON.parse(erector.updaters.json(existing, replacement)); 182 | const exist = JSON.parse(existing); 183 | const alterFields = [ 184 | 'author', 'description', 'es2015', 185 | 'keywords', 'license', 'main', 186 | 'module', 'name', 'repository', 187 | 'typings', 'version' 188 | ]; 189 | 190 | alterFields.forEach((field) => { 191 | if (field in exist) { 192 | merged[field] = exist[field]; 193 | } 194 | }); 195 | 196 | return JSON.stringify(merged, null, 2); 197 | }; 198 | 199 | const updateFlatFile = (existing, replacement) => { 200 | const newline = /\r\n/.test(replacement) ? '\r\n' : '\n'; 201 | const replaceLines = replacement.split(/\r?\n/g); 202 | const missingLines = existing.split(/\r?\n/g).filter((line) => 203 | replaceLines.indexOf(line) === -1 204 | ); 205 | 206 | return replaceLines.concat(missingLines).join(newline); 207 | }; 208 | 209 | const runPostUpgradeInstall = (npm) => { 210 | logger.info( 211 | colorize.colorize('Updating NPM dependencies', 'blue') 212 | ); 213 | logger.info( 214 | colorize.colorize(' Running', 'yellow'), 215 | 'npm install' 216 | ); 217 | execute.execute(npm, ['i']); 218 | 219 | logger.info( 220 | colorize.colorize(' Running', 'yellow'), 221 | 'npm upgrade' 222 | ); 223 | execute.execute(npm, ['up']); 224 | }; 225 | -------------------------------------------------------------------------------- /commands/utilities.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('../tools/utilities'); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const commands = require('./commands'); 4 | const inquire = require('erector-set').inquire; 5 | 6 | const colorize = require('./tools/utilities/colorize'); 7 | const logging = require('./tools/logging'); 8 | 9 | const main = (cliArgs) => { 10 | const command = getCommandName(cliArgs[0]); 11 | const commandArgs = cliArgs.slice(command === 'npm' ? 0 : 1); 12 | const logger = logging.create('Librarian'); 13 | const rootDir = process.cwd(); 14 | 15 | if (typeof commands[command] === 'function') { 16 | commands[command].apply(null, [rootDir].concat(commandArgs)) 17 | .catch((error) => logger.error(colorize.colorize(error, 'red'))); 18 | } else { 19 | askForCommand(); 20 | } 21 | }; 22 | 23 | const getCommandName = (command) => { 24 | command = command ? command.replace(/^-+/, '') : command; 25 | 26 | switch (command) { 27 | case 'b': 28 | case 'build': 29 | case 'l': 30 | case 'lint': 31 | case 'pub': 32 | case 'publish': 33 | case 'serve': 34 | case 't': 35 | case 'test': 36 | case 'v': 37 | return 'npm'; 38 | case 'c': 39 | case 'component': 40 | return 'component'; 41 | case 'd': 42 | case 'directive': 43 | return 'directive'; 44 | case 'i': 45 | case 'init': 46 | case 'initial': 47 | return 'initial'; 48 | case 'p': 49 | case 'pipe': 50 | return 'pipe'; 51 | case 's': 52 | case 'service': 53 | return 'service'; 54 | case 'u': 55 | case 'up': 56 | case 'upgrade': 57 | return 'upgrade'; 58 | default: 59 | return ''; 60 | } 61 | }; 62 | 63 | const askForCommand = () => { 64 | inquire([{ 65 | name: 'command', 66 | question: 'What would you like to do?' 67 | }]) 68 | .then((answers) => main(answers[0].answer.split(/\s+/))); 69 | }; 70 | 71 | if (!module.parent) { 72 | main(process.argv.slice(2)); 73 | } 74 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-librarian", 3 | "version": "1.0.0", 4 | "description": "A scaffolding setup for Angular 2+ libraries", 5 | "main": "index.js", 6 | "bin": { 7 | "ngl": "bin/index.js" 8 | }, 9 | "scripts": { 10 | "coverage": "tap \"test/**/*.spec.js\" --cov --coverage-report=lcov", 11 | "local": "tap \"test/**/*.spec.js\"", 12 | "start": "node .", 13 | "test": "tap \"test/**/*.spec.js\" --coverage", 14 | "posttest": "tap --coverage-report=lcov && codecov", 15 | "watch": "nodemon -q -x \"npm run local\"" 16 | }, 17 | "publishConfig": { 18 | "registry": "https://registry.npmjs.org" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/gonzofish/angular-librarian.git" 23 | }, 24 | "keywords": [ 25 | "erector", 26 | "set", 27 | "generator", 28 | "angular", 29 | "webpack" 30 | ], 31 | "author": "Matt Fehskens ", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/gonzofish/angular-librarian/issues" 35 | }, 36 | "homepage": "https://github.com/gonzofish/angular-librarian#readme", 37 | "dependencies": { 38 | "erector-set": "1.0.0-beta.3" 39 | }, 40 | "devDependencies": { 41 | "eslint": "^4.7.2", 42 | "eslint-plugin-node": "^5.2.0", 43 | "nodemon": "^1.11.0", 44 | "semver": "^5.0.0", 45 | "sinon": "^2.4.1", 46 | "tap": "^10.7.1" 47 | }, 48 | "engines": { 49 | "node": ">=6.0.0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /test/directive.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const erector = require('erector-set'); 4 | const path = require('path'); 5 | const sinon = require('sinon'); 6 | const tap = require('tap'); 7 | 8 | const directive = require('../commands/directive'); 9 | const logging = require('../tools/logging'); 10 | const utilities = require('../tools/utilities'); 11 | 12 | const caseConvert = utilities.caseConvert; 13 | const files = utilities.files; 14 | const opts = utilities.options; 15 | const sandbox = sinon.sandbox.create(); 16 | 17 | const make = function() { 18 | return directive.apply(directive, ['./'].concat(Array.from(arguments))); 19 | }; 20 | 21 | let checkForExamples; 22 | let construct; 23 | let creator; 24 | let getTemplates; 25 | let inquire; 26 | let log; 27 | let logger; 28 | let parseOptions; 29 | let rooter; 30 | 31 | tap.test('command: directive', (suite) => { 32 | suite.beforeEach((done) => { 33 | erector.construct.setTestMode(true); 34 | 35 | checkForExamples = sandbox.stub(opts, 'checkIsForExamples'); 36 | construct = sandbox.stub(erector, 'construct'); 37 | creator = sandbox.stub(files.resolver, 'create'); 38 | getTemplates = sandbox.stub(files, 'getTemplates'); 39 | inquire = sandbox.stub(erector, 'inquire'); 40 | logger = sandbox.stub(logging, 'create'); 41 | log = sandbox.spy(); 42 | parseOptions = sandbox.stub(opts, 'parseOptions'); 43 | rooter = sandbox.stub(files.resolver, 'root'); 44 | 45 | creator.callsFake(function() { 46 | const createPath = Array.from(arguments).join('/'); 47 | return function() { 48 | return `/created/${ createPath }/` + Array.from(arguments).join('/'); 49 | }; 50 | }); 51 | getTemplates.returns('fake-templates'); 52 | inquire.rejects(); 53 | logger.returns({ 54 | info: log, 55 | log: log, 56 | error: log, 57 | warning: log 58 | }); 59 | parseOptions.returns({}); 60 | rooter.callsFake(function() { 61 | return '/root/' + Array.from(arguments).join('/'); 62 | }); 63 | 64 | done(); 65 | }); 66 | 67 | suite.afterEach((done) => { 68 | sandbox.restore(); 69 | done(); 70 | }); 71 | 72 | suite.test('should create a Directive logger', (test) => { 73 | test.plan(1); 74 | 75 | make().catch(() => { 76 | test.ok(logger.calledWith('Directive')); 77 | test.end(); 78 | }); 79 | }); 80 | 81 | suite.test('should parse options from arguments to check for examples', (test) => { 82 | const options = { pizza: [], p: [], i: [], steak: ['sauce'] }; 83 | test.plan(2); 84 | 85 | parseOptions.resetBehavior(); 86 | parseOptions.returns(options); 87 | 88 | make('--pizza', '--steak', '-pie').catch(() => { 89 | test.ok(parseOptions.calledWith( 90 | ['--pizza', '--steak', '-pie'], 91 | ['example', 'examples', 'x'] 92 | )); 93 | test.ok(checkForExamples.calledWith(options)); 94 | test.end(); 95 | }); 96 | }); 97 | 98 | suite.test('should ask no questions if a dash-case directive name is provided', (test) => { 99 | const answers = [ 100 | { answer: 'bacon-blast', name: 'directiveName' }, 101 | { answer: 'PascalCaseDirective', name: 'className' }, 102 | { answer: 'camelCase', name: 'selector' } 103 | ]; 104 | const checkDashFormat = sandbox.stub(caseConvert, 'checkIsDashFormat'); 105 | const dashToCamel = sandbox.stub(caseConvert, 'dashToCamel'); 106 | const dashToCap = sandbox.stub(caseConvert, 'dashToCap'); 107 | 108 | checkForExamples.returns(false); 109 | dashToCamel.returns('camelCase'); 110 | dashToCap.returns('PascalCase'); 111 | checkDashFormat.returns(true); 112 | 113 | test.plan(2); 114 | make('bacon-blast').then(() => { 115 | test.notOk(inquire.called); 116 | test.ok(construct.calledWith( 117 | answers, 118 | 'fake-templates' 119 | )); 120 | test.end(); 121 | }); 122 | }); 123 | 124 | suite.test('should prefix the `selector` answer if one has been set', (test) => { 125 | const answers = [ 126 | { answer: 'bacon-blast', name: 'directiveName' }, 127 | { answer: 'PascalCaseDirective', name: 'className' }, 128 | { answer: 'myPascalSelector', name: 'selector' } 129 | ]; 130 | const checkDashFormat = sandbox.stub(caseConvert, 'checkIsDashFormat'); 131 | const dashToCap = sandbox.stub(caseConvert, 'dashToCap'); 132 | const dashToPascal = sandbox.stub(caseConvert, 'dashToPascal'); 133 | 134 | sandbox.stub(files, 'getSelectorPrefix').returns('my'); 135 | 136 | checkForExamples.returns(false); 137 | dashToCap.returns('PascalCase'); 138 | dashToPascal.returns('PascalSelector'); 139 | checkDashFormat.returns(true); 140 | 141 | test.plan(2); 142 | make('bacon-blast').then(() => { 143 | test.notOk(inquire.called); 144 | test.ok(construct.calledWith( 145 | answers, 146 | 'fake-templates' 147 | )); 148 | test.end(); 149 | }); 150 | }); 151 | 152 | suite.test('should ask for a selector name in dash-case if NOT provided', (test) => { 153 | const dashToCap = sandbox.stub(caseConvert, 'dashToCap'); 154 | test.plan(2); 155 | 156 | dashToCap.returned('DashToCap'); 157 | 158 | make().catch(() => { 159 | const { transform } = inquire.lastCall.args[0][2]; 160 | 161 | test.ok(inquire.calledWith([ 162 | { 163 | name: 'directiveName', 164 | question: 'Directive name (in dash-case):', 165 | transform: caseConvert.testIsDashFormat 166 | }, 167 | { 168 | name: 'selector', 169 | transform: sinon.match.instanceOf(Function), 170 | useAnswer: 'directiveName' 171 | }, 172 | { 173 | name: 'className', 174 | transform: sinon.match.instanceOf(Function), 175 | useAnswer: 'directiveName' 176 | } 177 | ])); 178 | 179 | test.ok(transform('here-it-is'), 'DashToCapDirective'); 180 | 181 | test.end(1); 182 | }); 183 | }); 184 | 185 | suite.test('should generate a list of templates', (test) => { 186 | const answers = [ 187 | { 188 | answer: 'burger-blitz', 189 | name: 'directiveName' 190 | }, 191 | { 192 | answer: 'burgerBlitz', 193 | name: 'selector' 194 | }, 195 | { 196 | answer: 'BurgerBlitzDirective', 197 | name: 'className' 198 | } 199 | ]; 200 | const colorize = sandbox.stub(utilities.colorize, 'colorize'); 201 | const dirname = [process.cwd(), 'commands', 'directive'].join(path.sep); 202 | test.plan(5); 203 | 204 | colorize.callsFake((text, color) => 205 | `[${ color }]${ text }[/${ color }]` 206 | ); 207 | inquire.resetBehavior(); 208 | inquire.resolves(answers); 209 | 210 | make().then(() => { 211 | test.ok(getTemplates.calledWith( 212 | '/root/', 213 | dirname, 214 | [ 215 | { 216 | destination: '/created/src/directives/{{ directiveName }}.directive.ts', 217 | name: 'app.ts' 218 | }, 219 | { 220 | destination: '/created/src/directives/{{ directiveName }}.directive.spec.ts', 221 | name: 'test.ts' 222 | } 223 | ] 224 | )); 225 | test.equal(log.callCount, 3); 226 | test.ok(log.firstCall.calledWith( 227 | `[green]Don't forget to add the following to the[/green]`, 228 | 'src/*.module.ts', 229 | '[green]file:[/green]' 230 | )); 231 | test.ok(log.secondCall.calledWith( 232 | `[cyan] import { BurgerBlitzDirective } from './directives/burger-blitz.directive';[/cyan]` 233 | )); 234 | test.ok(log.thirdCall.calledWith( 235 | '[green]And to add[/green]', 236 | 'BurgerBlitzDirective', 237 | '[green]to the NgModule declarations list[/green]' 238 | )); 239 | 240 | test.end(); 241 | }); 242 | }); 243 | 244 | suite.test('should generate a list of templates for an examples target', (test) => { 245 | const answers = [ 246 | { 247 | answer: 'burger-blitz', 248 | name: 'directiveName' 249 | }, 250 | { 251 | answer: 'burgerBlitz', 252 | name: 'selector' 253 | }, 254 | { 255 | answer: 'BurgerBlitzDirective', 256 | name: 'className' 257 | } 258 | ]; 259 | const colorize = sandbox.stub(utilities.colorize, 'colorize'); 260 | const dirname = [process.cwd(), 'commands', 'directive'].join(path.sep); 261 | test.plan(5); 262 | 263 | checkForExamples.returns(true); 264 | colorize.callsFake((text, color) => 265 | `[${ color }]${ text }[/${ color }]` 266 | ); 267 | inquire.resetBehavior(); 268 | inquire.resolves(answers); 269 | 270 | make().then(() => { 271 | test.ok(getTemplates.calledWith( 272 | '/root/', 273 | dirname, 274 | [ 275 | { 276 | destination: '/created/examples/directives/{{ directiveName }}.directive.ts', 277 | name: 'app.ts' 278 | }, 279 | { 280 | destination: '/created/examples/directives/{{ directiveName }}.directive.spec.ts', 281 | name: 'test.ts' 282 | } 283 | ] 284 | )); 285 | test.equal(log.callCount, 3); 286 | test.ok(log.firstCall.calledWith( 287 | `[green]Don't forget to add the following to the[/green]`, 288 | 'examples/example.module.ts', 289 | '[green]file:[/green]' 290 | )); 291 | test.ok(log.secondCall.calledWith( 292 | `[cyan] import { BurgerBlitzDirective } from './directives/burger-blitz.directive';[/cyan]` 293 | )); 294 | test.ok(log.thirdCall.calledWith( 295 | '[green]And to add[/green]', 296 | 'BurgerBlitzDirective', 297 | '[green]to the NgModule declarations list[/green]' 298 | )); 299 | 300 | test.end(); 301 | }); 302 | }); 303 | 304 | suite.test('should scaffold the app & spec files', (test) => { 305 | const answers = [ 306 | { 307 | answer: 'burger-blitz', 308 | name: 'directiveName' 309 | }, 310 | { 311 | answer: 'burgerBlitz', 312 | name: 'selector' 313 | }, 314 | { 315 | answer: 'BurgerBlitzDirective', 316 | name: 'className' 317 | } 318 | ]; 319 | const appOutput = 320 | `import { Directive } from '@angular/core';\n` + 321 | `\n` + 322 | `@Directive({\n` + 323 | ` selector: '[burgerBlitz]'\n` + 324 | `})\n` + 325 | `export class BurgerBlitzDirective {\n` + 326 | ` constructor() {}\n` + 327 | `}\n`; 328 | const specOutput = 329 | `/* tslint:disable:no-unused-variable */\n` + 330 | `import {\n` + 331 | ` async,\n` + 332 | ` TestBed\n` + 333 | `} from '@angular/core/testing';\n` + 334 | `\n` + 335 | `import { BurgerBlitzDirective } from './burger-blitz.directive';\n` + 336 | `\n` + 337 | `describe('BurgerBlitzDirective', () => {\n` + 338 | ` it('', () => {\n` + 339 | ` const directive = new BurgerBlitzDirective();\n` + 340 | `\n` + 341 | ` expect(directive).toBeTruthy();\n` + 342 | ` });\n` + 343 | `});\n`; 344 | 345 | 346 | construct.callThrough(); 347 | 348 | getTemplates.resetBehavior(); 349 | getTemplates.callThrough(); 350 | inquire.resetBehavior(); 351 | inquire.resolves(answers); 352 | 353 | test.plan(1); 354 | 355 | make().then((result) => { 356 | test.deepEqual(result, [ 357 | // app.ts 358 | { 359 | destination: '/created/src/directives/burger-blitz.directive.ts', 360 | output: appOutput 361 | }, 362 | // spec.ts 363 | { 364 | destination: '/created/src/directives/burger-blitz.directive.spec.ts', 365 | output: specOutput 366 | } 367 | ]); 368 | test.end(); 369 | }); 370 | }); 371 | 372 | suite.end(); 373 | }); 374 | -------------------------------------------------------------------------------- /test/initial.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const childProcess = require('child_process'); 4 | const erector = require('erector-set'); 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | const process = require('process'); 8 | const sinon = require('sinon'); 9 | const tap = require('tap'); 10 | 11 | const initial = require('../commands/initial'); 12 | const logger = require('../commands/logging'); 13 | const utilities = require('../tools/utilities'); 14 | 15 | const caseConvert = utilities.caseConvert; 16 | const files = utilities.files; 17 | const inputs = utilities.inputs; 18 | const opts = utilities.options; 19 | 20 | const sandbox = sinon.sandbox.create(); 21 | 22 | tap.test('command: initial', (suite) => { 23 | let chdir; 24 | let execSync; 25 | let filesInclude; 26 | let filesVersions; 27 | let getTemplates; 28 | let mockErector; 29 | let mockLog; 30 | let mockLogger; 31 | let resolverCreate; 32 | let resolverRoot; 33 | 34 | suite.beforeEach((done) => { 35 | erector.construct.setTestMode(true); 36 | 37 | sandbox.stub(files.librarianVersions, 'checkIsBranch'); 38 | 39 | chdir = sandbox.stub(process, 'chdir'); 40 | execSync = sandbox.stub(childProcess, 'execSync'); 41 | filesInclude = sandbox.stub(files, 'include'); 42 | filesVersions = sandbox.stub(files.librarianVersions, 'get'); 43 | getTemplates = sandbox.stub(files, 'getTemplates'); 44 | mockLog = sandbox.spy(); 45 | mockLogger = sandbox.stub(logger, 'create'); 46 | resolverCreate = sandbox.stub(files.resolver, 'create'); 47 | resolverRoot = sandbox.stub(files.resolver, 'root'); 48 | 49 | filesInclude.callsFake((file) => { 50 | if (file.indexOf('package.json') !== -1) { 51 | return { name: 'fake-library' }; 52 | } 53 | }); 54 | filesVersions.returns('ice-cream'); 55 | mockErector = { 56 | construct: sandbox.stub(erector, 'construct'), 57 | inquire: sandbox.stub(erector, 'inquire') 58 | }; 59 | mockLogger.returns({ 60 | info: mockLog, 61 | error: mockLog 62 | }); 63 | resolverCreate.callsFake(function() { 64 | const createPath = Array.from(arguments).join('/'); 65 | return function() { 66 | return `/created/${ createPath }/` + Array.from(arguments).join('/'); 67 | }; 68 | }); 69 | resolverRoot.callsFake(function() { 70 | return '/root/' + Array.from(arguments).join('/'); 71 | }); 72 | 73 | done(); 74 | }); 75 | 76 | suite.afterEach((done) => { 77 | sandbox.restore(); 78 | 79 | done(); 80 | }); 81 | 82 | suite.test('should call erector.inquire with the questions to ask', (test) => { 83 | const createYesNo = sandbox.stub(inputs, 'createYesNoValue'); 84 | 85 | test.plan(26); 86 | 87 | mockErector.inquire.rejects(); 88 | createYesNo.returns('"no" function'); 89 | 90 | initial('./').then(() => { 91 | const questions = mockErector.inquire.lastCall.args[0]; 92 | 93 | // Is there a more concise way to do this with sinon & TAP? 94 | // Maybe use something like jasmine.any(Function)? 95 | test.equal(questions[0].defaultAnswer, 'fake-library'); 96 | test.equal(questions[0].name, 'name'); 97 | test.equal(questions[0].question, 'Library name:'); 98 | test.equal(typeof questions[0].transform, 'function'); 99 | 100 | test.equal(questions[1].name, 'packageName'); 101 | test.equal(typeof questions[1].transform, 'function'); 102 | test.equal(questions[1].useAnswer, 'name'); 103 | 104 | test.equal(typeof questions[2].defaultAnswer, 'string'); 105 | test.equal(questions[2].allowBlank, true); 106 | test.equal(questions[2].name, 'prefix'); 107 | test.equal(questions[2].question, 'Prefix (component/directive selector):'); 108 | 109 | test.equal(typeof questions[3].defaultAnswer, 'function'); 110 | test.equal(questions[3].name, 'readmeTitle'); 111 | test.equal(questions[3].question, 'README Title:'); 112 | 113 | test.equal(questions[4].name, 'repoUrl'); 114 | test.equal(questions[4].question, 'Repository URL:'); 115 | 116 | test.equal(questions[5].name, 'git'); 117 | test.equal(questions[5].question, 'Reinitialize Git project (y/N)?'); 118 | test.ok(createYesNo.calledWith('n')); 119 | test.equal(questions[5].transform, '"no" function'); 120 | 121 | test.equal(questions[6].name, 'moduleName'); 122 | test.equal(typeof questions[6].transform, 'function'); 123 | test.equal(questions[6].useAnswer, 'packageName'); 124 | 125 | test.equal(questions[7].name, 'version'); 126 | test.equal(questions[7].question, 'Version:'); 127 | 128 | test.ok(mockLog.calledWith('\x1b[31mError\x1b[0m')); 129 | 130 | test.end(); 131 | }); 132 | }); 133 | 134 | suite.test('should have a name question transform that tests for proper format', (test) => { 135 | test.plan(16); 136 | 137 | mockErector.inquire.rejects(); 138 | 139 | initial('./').then(() => { 140 | const { transform } = mockErector.inquire.lastCall.args[0][0]; 141 | 142 | test.equal(transform(), ''); 143 | test.equal(transform(123), null); 144 | test.equal(transform('a'.repeat(215)), null); 145 | test.equal(transform(' a '), null); 146 | test.equal(transform('A'), null); 147 | test.equal(transform('.a'), null); 148 | test.equal(transform('_a'), null); 149 | test.equal(transform('-a'), null); 150 | test.equal(transform('a.'), null); 151 | test.equal(transform('a_'), null); 152 | test.equal(transform('a-'), null); 153 | test.equal(transform('@scope/package/nope'), null); 154 | test.equal(transform('package/nope'), null); 155 | test.equal(transform('$houlnt-work'), null); 156 | test.equal(transform('@scope/package'), '@scope/package'); 157 | test.equal(transform('package-name12'), 'package-name12'); 158 | 159 | test.end(); 160 | }); 161 | }); 162 | 163 | suite.test('should have a packageName question transform that extracts the package name without a scope', (test) => { 164 | test.plan(2); 165 | 166 | mockErector.inquire.rejects(); 167 | 168 | initial('./').then(() => { 169 | const { transform } = mockErector.inquire.lastCall.args[0][1]; 170 | 171 | test.equal(transform('@myscope/package-name'), 'package-name'); 172 | test.equal(transform('my-package'), 'my-package'); 173 | 174 | test.end(); 175 | }); 176 | }); 177 | 178 | suite.test('should have a moduleName question transform that converts the packageName using caseConvert.dashToCap and adds a "Module" suffix', (test) => { 179 | const dashToCap = sandbox.stub(caseConvert, 'dashToCap'); 180 | 181 | test.plan(1); 182 | 183 | mockErector.inquire.rejects(); 184 | dashToCap.returns('WOW!'); 185 | 186 | initial('./').then(() => { 187 | const { transform } = mockErector.inquire.lastCall.args[0][6]; 188 | test.equal(transform('this is calm'), 'WOW!Module'); 189 | test.end(); 190 | }); 191 | }); 192 | 193 | suite.test('should have a readmeTitle question defaultAnswer that converts the packagenName into words', (test) => { 194 | const dashToWords = sandbox.stub(caseConvert, 'dashToWords'); 195 | 196 | test.plan(2); 197 | 198 | mockErector.inquire.rejects(); 199 | dashToWords.returns('Herds of Words'); 200 | 201 | initial('./').then(() => { 202 | const { defaultAnswer } = mockErector.inquire.lastCall.args[0][3]; 203 | 204 | test.equal(defaultAnswer([null, { answer: 'this-is-patrick' }]), 'Herds of Words'); 205 | test.ok(dashToWords.calledWith('this-is-patrick')); 206 | test.end(); 207 | }); 208 | }); 209 | 210 | suite.test('should parse command-line options', (test) => { 211 | test.plan(1); 212 | 213 | const parseOptions = sandbox.stub(opts, 'parseOptions'); 214 | 215 | mockErector.inquire.resolves([{ name: 'git' }]); 216 | parseOptions.returns({}); 217 | 218 | initial('./', 'pizza', 'eat', 'yum').then(() => { 219 | test.ok(parseOptions.calledWith(['pizza', 'eat', 'yum'], ['no-install', 'ni'])); 220 | test.end(); 221 | }); 222 | }); 223 | 224 | suite.test('should call files.getTemplates with the rootDir, command dir, & the list of templates', (test) => { 225 | const dirname = process.cwd() + path.sep + ['commands', 'initial'].join(path.sep); 226 | 227 | getTemplates.returns([]); 228 | mockErector.inquire.resolves([{ name: 'git' }]); 229 | 230 | test.plan(1); 231 | 232 | initial('./').then(() => { 233 | test.ok(getTemplates.calledWith('./', dirname, [ 234 | { destination: '/root/.gitignore', name: '__gitignore' }, 235 | { destination: '/root/.npmignore', name: '__npmignore' }, 236 | { name: 'DEVELOPMENT.md' }, 237 | { blank: true, name: 'examples/example.component.html' }, 238 | { blank: true, name: 'examples/example.component.scss' }, 239 | { name: 'examples/example.component.ts' }, 240 | { name: 'examples/example.main.ts' }, 241 | { name: 'examples/example.module.ts' }, 242 | { name: 'examples/index.html' }, 243 | { blank: true, name: 'examples/styles.scss' }, 244 | { name: 'index.ts' }, 245 | { name: 'karma.conf.js', overwrite: true }, 246 | { destination: '/created/src/{{ packageName }}.module.ts', name: 'src/module.ts' }, 247 | { name: 'package.json', update: 'json' }, 248 | { name: 'README.md' }, 249 | { destination: '/created/src/index.ts', name: 'src/index.ts' }, 250 | { destination: '/created/src/test.js', name: 'src/test.js', overwrite: true }, 251 | { name: 'tsconfig.json', overwrite: true }, 252 | { name: 'tsconfig.doc.json', overwrite: true }, 253 | { name: 'tsconfig.es2015.json', overwrite: true }, 254 | { name: 'tsconfig.es5.json', overwrite: true }, 255 | { name: 'tsconfig.test.json', overwrite: true }, 256 | { name: 'tslint.json', overwrite: true }, 257 | { destination: '/created/src/vendor.ts', name: 'src/vendor.ts' }, 258 | { name: 'tasks/build.js', overwrite: true }, 259 | { name: 'tasks/copy-build.js', overwrite: true }, 260 | { name: 'tasks/copy-globs.js', overwrite: true }, 261 | { name: 'tasks/inline-resources.js', overwrite: true }, 262 | { name: 'tasks/rollup.js', overwrite: true }, 263 | { name: 'tasks/rollup-globals.js', overwrite: true }, 264 | { name: 'tasks/tag-version.js', overwrite: true }, 265 | { name: 'tasks/test.js', overwrite: true }, 266 | { name: 'webpack/webpack.common.js', overwrite: true }, 267 | { name: 'webpack/webpack.dev.js', overwrite: true }, 268 | { name: 'webpack/webpack.test.js', overwrite: true }, 269 | { name: 'webpack/webpack.utils.js', overwrite: true } 270 | ])); 271 | 272 | test.end(); 273 | }); 274 | }); 275 | 276 | suite.test('should call erector.construct with the user\'s answers & the templates from files.getTemplates', (test) => { 277 | const answers = [ 278 | { answer: '@myscope/fake-library', name: 'name' }, 279 | { answer: 'fake-library', name: 'packageName' }, 280 | { answer: 'ngfl', name: 'prefix' }, 281 | { answer: 'Fake Library', name: 'readmeTitle' }, 282 | { answer: 'http://re.po', name: 'repoUrl' }, 283 | { answer: false, name: 'git' }, 284 | { answer: 'FakeLibraryModule', name: 'moduleName' }, 285 | { answer: '1.0.0', name: 'version' } 286 | ]; 287 | const dirname = process.cwd() + path.sep + ['commands', 'initial'].join(path.sep); 288 | const finalAnswers = answers.concat({ 289 | answer: 'ice-cream', 290 | name: 'librarianVersion' 291 | }); 292 | const templates = [ 293 | { destination: '/root/path', template: 'some/template/path' }, 294 | { desetination: '/root/blank-file', template: undefined } 295 | ]; 296 | 297 | test.plan(4); 298 | 299 | getTemplates.returns(templates); 300 | mockErector.inquire.resolves(answers); 301 | 302 | initial('./').then(() => { 303 | // quick check that chdir is being called 304 | test.ok(chdir.calledTwice); 305 | test.ok(chdir.calledWith('./')); 306 | test.ok(chdir.calledWith(dirname)); 307 | 308 | test.ok(mockErector.construct.calledWith(finalAnswers, templates)); 309 | test.end(); 310 | }); 311 | }); 312 | 313 | suite.test('should install NPM modules if the "no install" flag is NOT present', (test) => { 314 | const answers = [ 315 | { answer: '@myscope/fake-library', name: 'name' }, 316 | { answer: 'fake-library', name: 'packageName' }, 317 | { answer: 'ngfl', name: 'prefix' }, 318 | { answer: 'Fake Library', name: 'readmeTitle' }, 319 | { answer: 'http://re.po', name: 'repoUrl' }, 320 | { answer: false, name: 'git' }, 321 | { answer: 'FakeLibraryModule', name: 'moduleName' }, 322 | { answer: '1.0.0', name: 'version' } 323 | ]; 324 | const templates = [ 325 | { destination: '/root/path', template: 'some/template/path' }, 326 | { desetination: '/root/blank-file', template: undefined } 327 | ]; 328 | 329 | test.plan(3); 330 | 331 | getTemplates.returns(templates); 332 | mockErector.inquire.resolves(answers); 333 | 334 | initial('./').then(() => { 335 | test.ok(mockLog.calledWith('\x1b[36mInstalling Node modules\x1b[0m')); 336 | test.ok(execSync.calledWith( 337 | 'npm i', 338 | { stdio: [0, 1, 2] } 339 | )); 340 | test.ok(mockLog.calledWith('\x1b[32mNode modules installed\x1b[0m')); 341 | 342 | test.end(); 343 | }); 344 | }); 345 | 346 | suite.test('should initialize a Git project if the user answers yes to the Git question', (test) => { 347 | const answers = [ 348 | { answer: '@myscope/fake-library', name: 'name' }, 349 | { answer: 'fake-library', name: 'packageName' }, 350 | { answer: 'ngfl', name: 'prefix' }, 351 | { answer: 'Fake Library', name: 'readmeTitle' }, 352 | { answer: 'http://re.po', name: 'repoUrl' }, 353 | { answer: true, name: 'git' }, 354 | { answer: 'FakeLibraryModule', name: 'moduleName' }, 355 | { answer: '1.0.0', name: 'version' } 356 | ]; 357 | const templates = [ 358 | { destination: '/root/path', template: 'some/template/path' }, 359 | { desetination: '/root/blank-file', template: undefined } 360 | ]; 361 | const del = sandbox.stub(files, 'deleteFolder'); 362 | 363 | test.plan(6); 364 | 365 | getTemplates.returns(templates); 366 | mockErector.inquire.resolves(answers); 367 | 368 | // also tests --no-install flag 369 | initial('./', '--no-install').then(() => { 370 | test.ok(mockLog.calledWith('\x1b[33mRemoving existing Git project\x1b[0m')); 371 | test.ok(del.calledWith('/root/.git')); 372 | test.ok(mockLog.calledWith('\x1b[36mInitializing new Git project\x1b[0m')); 373 | test.ok(execSync.calledWith( 374 | 'git init', 375 | { stdio: [0, 1, 2] } 376 | )); 377 | test.notOk(execSync.calledWith( 378 | 'npm i', 379 | { stdio: [0, 1, 2] } 380 | )); 381 | test.ok(mockLog.calledWith('\x1b[32mGit project initialized\x1b[0m')); 382 | test.end(); 383 | }); 384 | }); 385 | 386 | suite.end(); 387 | }); 388 | -------------------------------------------------------------------------------- /test/npm.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const childProcess = require('child_process'); 4 | const sinon = require('sinon'); 5 | const tap = require('tap'); 6 | 7 | const colorize = require('../tools/utilities/colorize'); 8 | const logging = require('../tools/logging'); 9 | const npm = require('../commands/npm'); 10 | 11 | const cmd = /^win/.test(process.platform) ? 'npm.cmd' : 'npm'; 12 | const sandbox = sinon.sandbox.create(); 13 | 14 | tap.test('command: npm', (suite) => { 15 | const testRun = (test, type, args) => { 16 | npm.apply(npm, ['./'].concat(type)).then(() => {}) 17 | test.ok(spawn.calledWith( 18 | cmd, 19 | ['run'].concat(args), 20 | { stdio: ['inherit', 'inherit', 'pipe'] } 21 | )); 22 | }; 23 | 24 | let spawn; 25 | 26 | suite.beforeEach((done) => { 27 | sandbox.restore(); 28 | spawn = sandbox.stub(childProcess, 'spawnSync'); 29 | spawn.returns({ status: 0 }); 30 | done(); 31 | }); 32 | 33 | suite.afterEach((done) => { 34 | sandbox.restore(); 35 | done(); 36 | }); 37 | 38 | suite.test('should run the build command with b & build', (test) => { 39 | test.plan(2); 40 | 41 | testRun(test, 'b', 'build'); 42 | testRun(test, 'build', 'build'); 43 | 44 | test.end(); 45 | }); 46 | 47 | suite.test('should run the lint command with l & lint', (test) => { 48 | test.plan(2); 49 | 50 | testRun(test, 'l', 'lint'); 51 | testRun(test, 'lint', 'lint'); 52 | 53 | test.end(); 54 | }); 55 | 56 | suite.test('should run the publish command with pub & publish', (test) => { 57 | test.plan(2); 58 | 59 | testRun(test, ['pub', '1.0.1'], ['tagVersion', '1.0.1']); 60 | testRun(test, ['publish', '1.0.0'], ['tagVersion', '1.0.0']); 61 | 62 | test.end(); 63 | }); 64 | 65 | suite.test('should run the serve command with v & serve', (test) => { 66 | test.plan(2); 67 | 68 | testRun(test, 'v', 'start'); 69 | testRun(test, 'server', 'start'); 70 | 71 | test.end(); 72 | }); 73 | 74 | suite.test('should run the test command with t & test', (test) => { 75 | test.plan(2); 76 | 77 | testRun(test, ['t', 'watch'], ['test', 'watch']); 78 | testRun(test, ['test', 'headless'], ['test', 'headless']); 79 | 80 | test.end(); 81 | }); 82 | 83 | suite.test('should return a promise', (test) => { 84 | test.plan(1); 85 | 86 | npm('./', 'b').then(() => { 87 | test.ok(true); 88 | test.end(); 89 | }); 90 | }); 91 | 92 | suite.test('rejections', (subSuite) => { 93 | let color; 94 | let log; 95 | let logger; 96 | 97 | subSuite.beforeEach((done) => { 98 | color = sandbox.stub(colorize, 'colorize'); 99 | log = sandbox.spy(); 100 | logger = sandbox.stub(logging, 'create'); 101 | 102 | color.callsFake((text, color) => `[${ color }]${ text }[/${ color }]`); 103 | logger.returns({ 104 | error: log 105 | }); 106 | done(); 107 | }); 108 | 109 | subSuite.afterEach(() => { 110 | spawn.reset(); 111 | }); 112 | 113 | subSuite.test('should reject if an error happens', (test) => { 114 | spawn.callsFake(() => { 115 | throw new Error(`I'm afraid I've got some bad news!`); 116 | }); 117 | 118 | test.plan(1); 119 | 120 | npm('./').catch((error) => { 121 | test.equal(error, `I'm afraid I've got some bad news!`); 122 | test.end(); 123 | }); 124 | }); 125 | 126 | subSuite.test('should return output.error if it exists', (test) => { 127 | spawn.returns({ error: new Error(`I'm afraid I've got some bad news?`) }); 128 | test.plan(1); 129 | 130 | npm('./').catch((error) => { 131 | test.equal(error, `I'm afraid I've got some bad news?`); 132 | test.end(); 133 | }); 134 | }); 135 | 136 | subSuite.test('should return an error if stderr has a value', (test) => { 137 | spawn.returns({ stderr: { toString() { return `I'm afraid I've got some bad news.`; } } }); 138 | test.plan(1); 139 | 140 | npm('./').catch((error) => { 141 | test.equal(error, `I'm afraid I've got some bad news.`); 142 | test.end(); 143 | }); 144 | }); 145 | 146 | subSuite.test('should return an error if the output.status is not 0', (test) => { 147 | spawn.returns({ status: 1 }); 148 | test.plan(1); 149 | 150 | npm('./', 'pizza').catch((error) => { 151 | test.equal(error, `Execution of "pizza" errored, see above for more information.`); 152 | test.end(); 153 | }); 154 | }); 155 | 156 | subSuite.end(); 157 | }); 158 | 159 | suite.end(); 160 | }); 161 | -------------------------------------------------------------------------------- /test/pipe.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const sinon = require('sinon'); 4 | const tap = require('tap'); 5 | 6 | const pipe = require('../commands/pipe'); 7 | const testUtils = require('./test-utils'); 8 | 9 | const sandbox = sinon.sandbox.create(); 10 | const mockOnce = (util, method) => testUtils.mockOnce(sandbox, util, method); 11 | 12 | tap.test('command: pipe', (suite) => { 13 | let make; 14 | let mocks; 15 | 16 | suite.beforeEach((done) => { 17 | make = testUtils.makeMake(pipe); 18 | mocks = testUtils.mock(sandbox); 19 | done(); 20 | }); 21 | 22 | suite.afterEach((done) => { 23 | sandbox.restore(); 24 | done(); 25 | }); 26 | 27 | suite.test('should create a Pipe logger', (test) => { 28 | test.plan(1); 29 | 30 | make().catch(() => { 31 | test.ok(mocks.logger.calledWith('Pipe')); 32 | 33 | test.end(); 34 | }); 35 | }); 36 | 37 | suite.test('should parse options to check if the pipe is for examples', (test) => { 38 | const checkForExamples = mockOnce('options', 'checkIsForExamples'); 39 | const options = { burger: [], b: [] }; 40 | const { parseOptions } = mocks; 41 | test.plan(2); 42 | 43 | parseOptions.resetBehavior(); 44 | parseOptions.returns(options); 45 | 46 | make('--burger', 'b').catch(() => { 47 | test.ok(parseOptions.calledWith( 48 | ['--burger', 'b'], 49 | ['example', 'examples', 'x'] 50 | )); 51 | test.ok(checkForExamples.calledWith(options)); 52 | 53 | test.end(); 54 | }); 55 | }); 56 | 57 | suite.test('should check for a dash-formatted pipe name', (test) => { 58 | const checkDash = mockOnce('caseConvert', 'checkIsDashFormat'); 59 | 60 | test.plan(1); 61 | make('pizza').catch(() => { 62 | test.ok(checkDash.calledWith('pizza')); 63 | test.end(); 64 | }); 65 | }); 66 | 67 | suite.test('should ask all questions with a non-dash formatted pipe name', (test) => { 68 | const checkDash = mockOnce('caseConvert', 'checkIsDashFormat'); 69 | const dashToCamel = testUtils.getUtilMethod('caseConvert', 'dashToCamel'); 70 | const dashToCap = mockOnce('caseConvert', 'dashToCap'); 71 | const inquire = mocks.erector.inquire; 72 | 73 | dashToCap.returns('YouRule'); 74 | test.plan(4); 75 | make().catch(() => { 76 | const questions = inquire.lastCall.args[0]; 77 | test.ok(inquire.calledWith([ 78 | { 79 | name: 'filename', 80 | question: 'Pipe name (in dash-case):', 81 | transform: sinon.match.instanceOf(Function) 82 | }, 83 | { 84 | name: 'pipeName', 85 | transform: dashToCamel, 86 | useAnswer: 'filename' 87 | }, 88 | { 89 | name: 'className', 90 | transform: sinon.match.instanceOf(Function), 91 | useAnswer: 'filename' 92 | } 93 | ])); 94 | 95 | test.equal(questions[0].transform('faux'), null); 96 | checkDash.returns(true); 97 | test.equal(questions[0].transform('faux'), 'faux'); 98 | test.equal(questions[2].transform('faux'), 'YouRulePipe'); 99 | 100 | test.end(); 101 | }); 102 | }); 103 | 104 | suite.test('should ask no questions if a dash-case pipe name is provided', (test) => { 105 | const answers = [ 106 | { answer: 'donut-dance', name: 'filename' }, 107 | { answer: 'camelCase', name: 'pipeName' }, 108 | { answer: 'PascalCasePipe', name: 'className' } 109 | ]; 110 | const checkDash = mockOnce('caseConvert', 'checkIsDashFormat'); 111 | const dashToCamel = mockOnce('caseConvert', 'dashToCamel'); 112 | const dashToCap = mockOnce('caseConvert', 'dashToCap'); 113 | const { construct, inquire } = mocks.erector; 114 | 115 | dashToCamel.returns('camelCase'); 116 | dashToCap.returns('PascalCase'); 117 | checkDash.returns(true); 118 | test.plan(2); 119 | 120 | make('donut-dance').then(() => { 121 | test.notOk(inquire.called); 122 | test.ok(construct.calledWith( 123 | answers, 124 | 'fake-templates' 125 | )); 126 | test.end(); 127 | }); 128 | }); 129 | 130 | suite.test('should generate a list of templates', (test) => { 131 | const answers = [ 132 | { answer: 'donut-dance', name: 'filename' }, 133 | { answer: 'camelCase', name: 'pipeName' }, 134 | { answer: 'PascalCasePipe', name: 'className' } 135 | ]; 136 | const { inquire } = mocks.erector; 137 | const { getTemplates, log, resolver } = mocks; 138 | 139 | inquire.resetBehavior(); 140 | inquire.resolves(answers); 141 | test.plan(5); 142 | 143 | make().then(() => { 144 | test.ok(mocks.getTemplates.calledWith( 145 | '/root/', 146 | testUtils.getDirName('pipe'), 147 | [ 148 | { 149 | destination: '/created/src/pipes/{{ filename }}.pipe.ts', 150 | name: 'app.ts' 151 | }, 152 | { 153 | destination: '/created/src/pipes/{{ filename }}.pipe.spec.ts', 154 | name: 'spec.ts' 155 | } 156 | ] 157 | )); 158 | test.equal(log.callCount, 3); 159 | test.ok(log.firstCall.calledWith( 160 | `[green]Don't forget to add the following to the[/green]`, 161 | 'src/*.module.ts', 162 | '[green]file:[/green]' 163 | )); 164 | test.ok(log.secondCall.calledWith( 165 | `[cyan] import { PascalCasePipe } from './pipes/donut-dance.pipe';[/cyan]` 166 | )); 167 | test.ok(log.thirdCall.calledWith( 168 | '[green]And to add[/green]', 169 | 'PascalCasePipe', 170 | '[green]to the NgModule declarations list[/green]' 171 | )); 172 | 173 | test.end(); 174 | }); 175 | }); 176 | 177 | suite.test('should generate a list of templates for an examples target', (test) => { 178 | const answers = [ 179 | { answer: 'donut-dance', name: 'filename' }, 180 | { answer: 'camelCase', name: 'pipeName' }, 181 | { answer: 'PascalCasePipe', name: 'className' } 182 | ];const checkForExamples = mockOnce('options', 'checkIsForExamples'); 183 | const { inquire } = mocks.erector; 184 | const { getTemplates, log, resolver } = mocks; 185 | 186 | checkForExamples.returns(true); 187 | inquire.resetBehavior(); 188 | inquire.resolves(answers); 189 | test.plan(5); 190 | 191 | make().then(() => { 192 | test.ok(mocks.getTemplates.calledWith( 193 | '/root/', 194 | testUtils.getDirName('pipe'), 195 | [ 196 | { 197 | destination: '/created/examples/pipes/{{ filename }}.pipe.ts', 198 | name: 'app.ts' 199 | }, 200 | { 201 | destination: '/created/examples/pipes/{{ filename }}.pipe.spec.ts', 202 | name: 'spec.ts' 203 | } 204 | ] 205 | )); 206 | test.equal(log.callCount, 3); 207 | test.ok(log.firstCall.calledWith( 208 | `[green]Don't forget to add the following to the[/green]`, 209 | 'examples/example.module.ts', 210 | '[green]file:[/green]' 211 | )); 212 | test.ok(log.secondCall.calledWith( 213 | `[cyan] import { PascalCasePipe } from './pipes/donut-dance.pipe';[/cyan]` 214 | )); 215 | test.ok(log.thirdCall.calledWith( 216 | '[green]And to add[/green]', 217 | 'PascalCasePipe', 218 | '[green]to the NgModule declarations list[/green]' 219 | )); 220 | 221 | test.end(); 222 | }); 223 | }); 224 | 225 | suite.test('should generate the corresponding pipe & spec files', (test) => { 226 | const { erector, getTemplates } = mocks; 227 | const { construct, inquire } = erector; 228 | const answers = [ 229 | { answer: 'donut-dance', name: 'filename' }, 230 | { answer: 'camelCase', name: 'pipeName' }, 231 | { answer: 'PascalCasePipe', name: 'className' } 232 | ]; 233 | const appOutput = 234 | `import { Pipe, PipeTransform } from '@angular/core';\n` + 235 | `\n` + 236 | `@Pipe({ name: 'camelCase' })\n` + 237 | `export class PascalCasePipe implements PipeTransform {\n` + 238 | ` transform(value: any, args?: any): any {\n` + 239 | `\n` + 240 | ` }\n` + 241 | `}\n`;; 242 | const specOutput = 243 | `/* tslint:disable:no-unused-variable */\n` + 244 | `\n` + 245 | `import { TestBed, async } from '@angular/core/testing';\n` + 246 | `import { PascalCasePipe } from './donut-dance.pipe';\n` + 247 | `\n` + 248 | `describe('PascalCasePipe', () => {\n` + 249 | ` it('', () => {\n` + 250 | ` const pipe = new PascalCasePipe();\n` + 251 | ` expect(pipe).toBeTruthy();\n` + 252 | ` });\n` + 253 | `});\n`; 254 | 255 | construct.callThrough(); 256 | 257 | getTemplates.resetBehavior(); 258 | getTemplates.callThrough(); 259 | inquire.resetBehavior(); 260 | inquire.resolves(answers); 261 | 262 | test.plan(1); 263 | 264 | make().then((result) => { 265 | test.deepEqual(result, [ 266 | // app.ts 267 | { 268 | destination: '/created/src/pipes/donut-dance.pipe.ts', 269 | output: appOutput 270 | }, 271 | // spec.ts 272 | { 273 | destination: '/created/src/pipes/donut-dance.pipe.spec.ts', 274 | output: specOutput 275 | } 276 | ]); 277 | test.end(); 278 | }); 279 | }); 280 | 281 | suite.end(); 282 | }); 283 | -------------------------------------------------------------------------------- /test/resolver.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | module.exports = (filename) => 6 | require( 7 | filename.replace(path.sep + 'test', '') 8 | .replace('.spec', '') 9 | ); -------------------------------------------------------------------------------- /test/service.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const sinon = require('sinon'); 4 | const tap = require('tap'); 5 | 6 | const service = require('../commands/service'); 7 | const testUtils = require('./test-utils'); 8 | 9 | const sandbox = sinon.sandbox.create(); 10 | const mockOnce = (util, method) => testUtils.mockOnce(sandbox, util, method); 11 | 12 | tap.test('command: service', (suite) => { 13 | let make; 14 | let mocks; 15 | 16 | suite.beforeEach((done) => { 17 | make = testUtils.makeMake(service); 18 | mocks = testUtils.mock(sandbox); 19 | 20 | done(); 21 | }); 22 | 23 | suite.afterEach((done) => { 24 | sandbox.restore(); 25 | done(); 26 | }); 27 | 28 | suite.test('should create a Service logger', (test) => { 29 | test.plan(1); 30 | 31 | make().catch(() => { 32 | test.ok(mocks.logger.calledWith('Service')); 33 | test.end(); 34 | }); 35 | }); 36 | 37 | suite.test('should parse options to check if the service is for examples', (test) => { 38 | const checkForExamples = mockOnce('options', 'checkIsForExamples'); 39 | const options = { burger: [], b: [] }; 40 | const { parseOptions } = mocks; 41 | test.plan(2); 42 | 43 | parseOptions.resetBehavior(); 44 | parseOptions.returns(options); 45 | 46 | make('--burger', 'b').catch(() => { 47 | test.ok(parseOptions.calledWith( 48 | ['--burger', 'b'], 49 | ['example', 'examples', 'x'] 50 | )); 51 | test.ok(checkForExamples.calledWith(options)); 52 | 53 | test.end(); 54 | }); 55 | }); 56 | 57 | suite.test('should check for a dash-formatted service name', (test) => { 58 | const checkDash = mockOnce('caseConvert', 'checkIsDashFormat'); 59 | 60 | test.plan(1); 61 | make('pizza').catch(() => { 62 | test.ok(checkDash.calledWith('pizza')); 63 | test.end(); 64 | }); 65 | }); 66 | 67 | suite.test('should ask all questions with a non-dash formatted service name', (test) => { 68 | const checkDash = mockOnce('caseConvert', 'checkIsDashFormat'); 69 | const dashToCap = mockOnce('caseConvert', 'dashToCap'); 70 | const inquire = mocks.erector.inquire; 71 | 72 | dashToCap.returns('YouRule'); 73 | test.plan(4); 74 | make().catch(() => { 75 | const questions = inquire.lastCall.args[0]; 76 | test.ok(inquire.calledWith([ 77 | { 78 | name: 'filename', 79 | question: 'Service name (in dash-case):', 80 | transform: sinon.match.instanceOf(Function) 81 | }, 82 | { 83 | name: 'serviceName', 84 | transform: sinon.match.instanceOf(Function), 85 | useAnswer: 'filename' 86 | } 87 | ])); 88 | 89 | test.equal(questions[0].transform('faux'), null); 90 | checkDash.returns(true); 91 | test.equal(questions[0].transform('faux'), 'faux'); 92 | test.equal(questions[1].transform('faux'), 'YouRuleService'); 93 | 94 | test.end(); 95 | }); 96 | }); 97 | 98 | suite.test('should ask no questions if a dash-case serivce name is provided', (test) => { 99 | const answers = [ 100 | { answer: 'donut-dance', name: 'filename' }, 101 | { answer: 'PascalCaseService', name: 'serviceName' } 102 | ]; 103 | const checkDash = mockOnce('caseConvert', 'checkIsDashFormat'); 104 | const dashToCap = mockOnce('caseConvert', 'dashToCap'); 105 | const { construct, inquire } = mocks.erector; 106 | 107 | dashToCap.returns('PascalCase'); 108 | checkDash.returns(true); 109 | test.plan(2); 110 | 111 | make('donut-dance').then(() => { 112 | test.notOk(inquire.called); 113 | test.ok(construct.calledWith( 114 | answers, 115 | 'fake-templates' 116 | )); 117 | test.end(); 118 | }); 119 | }); 120 | 121 | suite.test('should generate a list of templates', (test) => { 122 | const answers = [ 123 | { answer: 'donut-dance', name: 'filename' }, 124 | { answer: 'PascalCaseService', name: 'serviceName' } 125 | ]; 126 | const { inquire } = mocks.erector; 127 | const { getTemplates, log, resolver } = mocks; 128 | 129 | inquire.resetBehavior(); 130 | inquire.resolves(answers); 131 | test.plan(5); 132 | 133 | make().then(() => { 134 | test.ok(mocks.getTemplates.calledWith( 135 | '/root/', 136 | testUtils.getDirName('service'), 137 | [ 138 | { 139 | destination: '/created/src/services/{{ filename }}.service.ts', 140 | name: 'app.ts' 141 | }, 142 | { 143 | destination: '/created/src/services/{{ filename }}.service.spec.ts', 144 | name: 'spec.ts' 145 | } 146 | ] 147 | )); 148 | test.equal(log.callCount, 3); 149 | test.ok(log.firstCall.calledWith( 150 | `[green]Don't forget to add the following to the[/green]`, 151 | 'src/*.module.ts', 152 | '[green]file:[/green]' 153 | )); 154 | test.ok(log.secondCall.calledWith( 155 | `[cyan] import { PascalCaseService } from './services/donut-dance.service';[/cyan]` 156 | )); 157 | test.ok(log.thirdCall.calledWith( 158 | '[green]And to add[/green]', 159 | 'PascalCaseService', 160 | '[green]to the NgModule providers list or add as a provider to one or more components[/green]' 161 | )); 162 | 163 | test.end(); 164 | }); 165 | }); 166 | 167 | suite.test('should generate a list of templates for an examples target', (test) => { 168 | const answers = [ 169 | { answer: 'donut-dance', name: 'filename' }, 170 | { answer: 'PascalCaseService', name: 'serviceName' } 171 | ]; 172 | const checkForExamples = mockOnce('options', 'checkIsForExamples'); 173 | const { inquire } = mocks.erector; 174 | const { getTemplates, log, resolver } = mocks; 175 | 176 | checkForExamples.returns(true); 177 | inquire.resetBehavior(); 178 | inquire.resolves(answers); 179 | test.plan(5); 180 | 181 | make().then(() => { 182 | test.ok(mocks.getTemplates.calledWith( 183 | '/root/', 184 | testUtils.getDirName('service'), 185 | [ 186 | { 187 | destination: '/created/examples/services/{{ filename }}.service.ts', 188 | name: 'app.ts' 189 | }, 190 | { 191 | destination: '/created/examples/services/{{ filename }}.service.spec.ts', 192 | name: 'spec.ts' 193 | } 194 | ] 195 | )); 196 | test.equal(log.callCount, 3); 197 | test.ok(log.firstCall.calledWith( 198 | `[green]Don't forget to add the following to the[/green]`, 199 | 'examples/example.module.ts', 200 | '[green]file:[/green]' 201 | )); 202 | test.ok(log.secondCall.calledWith( 203 | `[cyan] import { PascalCaseService } from './services/donut-dance.service';[/cyan]` 204 | )); 205 | test.ok(log.thirdCall.calledWith( 206 | '[green]And to add[/green]', 207 | 'PascalCaseService', 208 | '[green]to the NgModule providers list or add as a provider to one or more components[/green]' 209 | )); 210 | 211 | test.end(); 212 | }); 213 | }); 214 | 215 | suite.test('should generate the app & spec files', (test) => { 216 | const { erector, getTemplates } = mocks; 217 | const { construct, inquire } = erector; 218 | const answers = [ 219 | { answer: 'pizza-party', name: 'filename' }, 220 | { answer: 'PizzaPartyService', name: 'serviceName' } 221 | ]; 222 | const appOutput = 223 | `import { Injectable } from '@angular/core';\n` + 224 | `\n` + 225 | `@Injectable()\n` + 226 | `export class PizzaPartyService {\n` + 227 | ` constructor() {}\n` + 228 | `}\n`; 229 | const specOutput = 230 | `/* tslint:disable:no-unused-vars */\n` + 231 | `import {\n` + 232 | ` getTestBed,\n` + 233 | ` TestBed\n` + 234 | `} from '@angular/core/testing';\n` + 235 | `\n` + 236 | `import { PizzaPartyService } from './pizza-party.service';\n` + 237 | `\n` + 238 | `describe('PizzaPartyService', () => {\n` + 239 | ` let service: PizzaPartyService;\n` + 240 | `\n` + 241 | ` beforeEach(() => {\n` + 242 | ` TestBed.configureTestingModule({\n` + 243 | ` providers: [PizzaPartyService]\n` + 244 | ` });\n` + 245 | ` service = getTestBed().get(PizzaPartyService);\n` + 246 | ` });\n` + 247 | `\n` + 248 | ` it('', () => {\n` + 249 | ` expect(service).toBeTruthy();\n` + 250 | ` });\n` + 251 | `});\n`; 252 | 253 | construct.callThrough(); 254 | getTemplates.resetBehavior(); 255 | getTemplates.callThrough(); 256 | inquire.resetBehavior(); 257 | inquire.resolves(answers); 258 | 259 | test.plan(1); 260 | 261 | make().then((result) => { 262 | test.deepEqual(result, [ 263 | { 264 | destination: '/created/src/services/pizza-party.service.ts', 265 | output: appOutput 266 | }, 267 | { 268 | destination: '/created/src/services/pizza-party.service.spec.ts', 269 | output: specOutput 270 | } 271 | ]); 272 | test.end(); 273 | }); 274 | }); 275 | 276 | suite.end(); 277 | }); 278 | -------------------------------------------------------------------------------- /test/test-utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const erector = require('erector-set'); 4 | const path = require('path'); 5 | const sinon = require('sinon'); 6 | 7 | const logging = require('../tools/logging'); 8 | const utilities = require('../tools/utilities'); 9 | 10 | exports.getDirName = (command) => 11 | [process.cwd(), 'commands', command].join(path.sep); 12 | 13 | exports.getUtilMethod = (util, method) => 14 | utilities[util] && utilities[util][method]; 15 | 16 | exports.makeMake = (command) => 17 | function() { 18 | return command.apply(command, ['./'].concat(Array.from(arguments))); 19 | }; 20 | 21 | exports.mock = (sandbox) => { 22 | const mocks = { 23 | checkVersion: sandbox.stub(utilities.files.librarianVersions, 'checkIsBranch'), 24 | colorize: sandbox.stub(utilities.colorize, 'colorize'), 25 | erector: { 26 | construct: sandbox.stub(erector, 'construct'), 27 | inquire: sandbox.stub(erector, 'inquire') 28 | }, 29 | getTemplates: sandbox.stub(utilities.files, 'getTemplates'), 30 | getVersion: sandbox.stub(utilities.files.librarianVersions, 'get'), 31 | log: sandbox.spy(), 32 | logger: sandbox.stub(logging, 'create'), 33 | parseOptions: sandbox.stub(utilities.options, 'parseOptions'), 34 | resolver: { 35 | create: sandbox.stub(utilities.files.resolver, 'create'), 36 | manual: sandbox.stub(utilities.files.resolver, 'manual'), 37 | root: sandbox.stub(utilities.files.resolver, 'root'), 38 | } 39 | }; 40 | 41 | erector.construct.setTestMode(true); 42 | 43 | mocks.checkVersion.returns(false); 44 | mocks.colorize.callsFake((text, color) => 45 | `[${ color }]${ text }[/${ color }]` 46 | ); 47 | mocks.erector.inquire.rejects(); 48 | mocks.getTemplates.returns('fake-templates'); 49 | mocks.getVersion.returns('ice-cream'); 50 | mocks.logger.returns({ 51 | error: mocks.log, 52 | info: mocks.log, 53 | log: mocks.log, 54 | warning: mocks.log 55 | }); 56 | mocks.parseOptions.returns({}); 57 | mocks.resolver.create.callsFake(function() { 58 | const createPath = argsPath(arguments); 59 | return function() { 60 | return `/created/${ createPath }/` + argsPath(arguments); 61 | }; 62 | }); 63 | mocks.resolver.manual.callsFake(function() { 64 | return '/manual/' + argsPath(arguments); 65 | }); 66 | mocks.resolver.root.callsFake(function() { 67 | return '/root/' + argsPath(arguments); 68 | }); 69 | 70 | return mocks; 71 | }; 72 | 73 | exports.mockOnce = (sandbox, util, method) => 74 | sandbox.stub(utilities[util], method); 75 | 76 | const argsPath = (args) => Array.from(args).join('/'); 77 | 78 | exports.sinon = sinon; 79 | 80 | exports.mapQuestionsToQuestionName = (questions) => { 81 | const map = {}; 82 | 83 | questions.forEach(question => map[question.name] = question); 84 | 85 | return map; 86 | } 87 | -------------------------------------------------------------------------------- /test/tools/logging.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const sinon = require('sinon'); 4 | const tap = require('tap'); 5 | 6 | const logging = require('../resolver')(__filename); 7 | 8 | tap.test('#create', (suite) => { 9 | const create = logging.create; 10 | 11 | suite.test('should return an object with 4 log methods', (test) => { 12 | test.plan(4); 13 | 14 | const logger = create(); 15 | const methods = Object.keys(logger); 16 | 17 | methods.forEach((method) => { 18 | test.equal(typeof logger[method], 'function'); 19 | }); 20 | 21 | test.end(); 22 | }); 23 | 24 | suite.test('should call the analgous console method with any arguments', (test) => { 25 | test.plan(4); 26 | 27 | const errorSpy = sinon.stub(console, 'error'); 28 | const infoSpy = sinon.stub(console, 'info'); 29 | const logSpy = sinon.stub(console, 'log'); 30 | const warnSpy = sinon.stub(console, 'warn'); 31 | 32 | const logger = create('pizza'); 33 | 34 | logger.error('this is an error'); 35 | logger.info('here is some info'); 36 | logger.log('log message'); 37 | logger.warn('do not make me warn you'); 38 | 39 | test.ok(errorSpy.calledWith('[pizza]:', 'this is an error')); 40 | test.ok(infoSpy.calledWith('[pizza]:', 'here is some info')); 41 | test.ok(logSpy.calledWith('[pizza]:', 'log message')); 42 | test.ok(warnSpy.calledWith('[pizza]:', 'do not make me warn you')); 43 | 44 | errorSpy.reset(); 45 | infoSpy.reset(); 46 | logSpy.reset(); 47 | warnSpy.reset(); 48 | 49 | test.end(); 50 | }); 51 | 52 | suite.end(); 53 | }); 54 | -------------------------------------------------------------------------------- /test/tools/utilities/case-convert.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const sinon = require('sinon'); 4 | const tap = require('tap'); 5 | 6 | const caseConvert = require('../../resolver')(__filename); 7 | 8 | tap.test('#checkIsDashFormat', (suite) => { 9 | const check = caseConvert.checkIsDashFormat; 10 | 11 | suite.test('should return false if no value is provided', (test) => { 12 | test.plan(4); 13 | 14 | test.notOk(check()); 15 | test.notOk(check(null)); 16 | test.notOk(check(0)); 17 | test.notOk(check(false)); 18 | 19 | test.end(); 20 | }); 21 | 22 | suite.test('should return false if the value is not a string', (test) => { 23 | test.plan(3); 24 | 25 | test.notOk(check(1)); 26 | test.notOk(check(true)); 27 | test.notOk(check(function() {})); 28 | 29 | test.end(); 30 | }); 31 | 32 | suite.test('should return false if the string has no characters', (test) => { 33 | test.plan(1); 34 | 35 | test.notOk(check('')); 36 | 37 | test.end(); 38 | }); 39 | 40 | suite.test('should return false if it is not dash format (no special characters)', (test) => { 41 | test.plan(4); 42 | 43 | test.notOk(check(' ')); 44 | test.notOk(check('cant-end-in-a-')); 45 | test.notOk(check('no-!?$-chars')); 46 | test.notOk(check('123-cant-start-with-number')); 47 | 48 | test.end(); 49 | }); 50 | 51 | suite.test('should return true if it is dash format', (test) => { 52 | test.plan(2); 53 | 54 | test.ok(check('this-is-good')); 55 | test.ok(check('numbers-12-3-4-work')); 56 | 57 | test.end(); 58 | }); 59 | 60 | suite.end(); 61 | }); 62 | 63 | tap.test('#testIsDashFormat', (suite) => { 64 | const check = caseConvert.testIsDashFormat; 65 | 66 | suite.test('should return the value if it is dash format', (test) => { 67 | test.plan(2); 68 | 69 | test.equal(check('this-is-good'), 'this-is-good'); 70 | test.equal(check('this-15-good'), 'this-15-good'); 71 | 72 | test.end(); 73 | }); 74 | 75 | suite.test('should return null if the value is not dash format', (test) => { 76 | test.plan(12); 77 | 78 | test.equal(check(), null); 79 | test.equal(check(null), null); 80 | test.equal(check(0), null); 81 | test.equal(check(false), null); 82 | test.equal(check(1), null); 83 | test.equal(check(true), null); 84 | test.equal(check(function() {}), null); 85 | test.equal(check(''), null); 86 | test.equal(check(' '), null); 87 | test.equal(check('cant-end-in-a-'), null); 88 | test.equal(check('no-!?$-chars'), null); 89 | test.equal(check('123-cant-start-with-number'), null); 90 | 91 | test.end(); 92 | }); 93 | 94 | suite.end(); 95 | }); 96 | 97 | tap.test('#dashToCamel should convert a dash-case to camel case', (test) => { 98 | const convert = caseConvert.dashToCamel; 99 | 100 | test.plan(3); 101 | 102 | test.equal(convert('this-is-camel-case'), 'thisIsCamelCase'); 103 | test.equal(convert('this-is-camel-case', '~'), 'this~Is~Camel~Case'); 104 | test.equal(convert('startedAsACamel'), 'startedAsACamel'); 105 | 106 | test.end(); 107 | }); 108 | 109 | tap.test('#dashToPascal should convert a dash-case to PascalCase', (test) => { 110 | const convert = caseConvert.dashToPascal; 111 | 112 | test.plan(3); 113 | 114 | test.equal(convert('this-is-camel-case'), 'ThisIsCamelCase'); 115 | test.equal(convert('this-is-camel-case', '~'), 'This~Is~Camel~Case'); 116 | test.equal(convert('startedAsACamel'), 'StartedAsACamel'); 117 | 118 | test.end(); 119 | }); 120 | 121 | tap.test('#dashToCap should convert a dash-case to Pascal case', (test) => { 122 | const convert = caseConvert.dashToCap; 123 | 124 | test.plan(3); 125 | 126 | test.equal(convert('this-is-pascal-case'), 'ThisIsPascalCase'); 127 | test.equal(convert('this-is-pascal-case', '~'), 'This~Is~Pascal~Case'); 128 | test.equal(convert('StartedAsAPascal'), 'StartedAsAPascal'); 129 | 130 | test.end(); 131 | }); 132 | 133 | tap.test('#dashToWords should convert a dash-case to separate words', (test) => { 134 | const convert = caseConvert.dashToWords; 135 | 136 | test.plan(2); 137 | 138 | test.equal(convert('here-is-a-sentence'), 'Here Is A Sentence'); 139 | test.equal(convert('Nothing to change'), 'Nothing to change'); 140 | 141 | test.end(); 142 | }); 143 | -------------------------------------------------------------------------------- /test/tools/utilities/colorize.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const tap = require('tap'); 4 | 5 | const utilities = require('../../resolver')(__filename); 6 | 7 | let mockCwd; 8 | 9 | tap.test('#colorize should add color escape sequences to text', (test) => { 10 | const color = utilities.colorize; 11 | 12 | test.plan(6); 13 | 14 | test.equal( 15 | color('this is blue', 'blue'), 16 | '\x1b[34mthis is blue\x1b[0m' 17 | ); 18 | test.equal( 19 | color('this is cyan', 'cyan'), 20 | '\x1b[36mthis is cyan\x1b[0m' 21 | ); 22 | test.equal( 23 | color('this is green', 'green'), 24 | '\x1b[32mthis is green\x1b[0m' 25 | ); 26 | test.equal( 27 | color('this is red', 'red'), 28 | '\x1b[31mthis is red\x1b[0m' 29 | ); 30 | test.equal( 31 | color('this is yellow', 'yellow'), 32 | '\x1b[33mthis is yellow\x1b[0m' 33 | ); 34 | test.equal( 35 | color('this is default', 'unknown'), 36 | '\x1b[0mthis is default\x1b[0m' 37 | ); 38 | 39 | test.end(); 40 | }); -------------------------------------------------------------------------------- /test/tools/utilities/index.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const tap = require('tap'); 4 | 5 | const utilities = require('../../resolver')(__filename); 6 | 7 | tap.test('#checkIsScopedName', (test) => { 8 | const check = utilities.checkIsScopedName; 9 | 10 | test.plan(2); 11 | 12 | test.ok(check('@my-scoped/package')); 13 | test.notOk(check('my-package')); 14 | 15 | test.end(); 16 | }); 17 | -------------------------------------------------------------------------------- /test/tools/utilities/inputs.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const sinon = require('sinon'); 4 | const tap = require('tap'); 5 | 6 | const inputs = require('../../resolver')(__filename); 7 | 8 | tap.test('#convertYesNoValue', (suite) => { 9 | const convert = inputs.convertYesNoValue; 10 | 11 | suite.test('should return "Y" if `true` is the value', (test) => { 12 | test.plan(1); 13 | 14 | test.equal(convert(true), 'Y'); 15 | 16 | test.end(); 17 | }); 18 | 19 | suite.test('should return "N" if a falsey value is provided', (test) => { 20 | test.plan(4); 21 | 22 | test.equal(convert(), 'N'); 23 | test.equal(convert(null), 'N'); 24 | test.equal(convert(false), 'N'); 25 | test.equal(convert(0), 'N'); 26 | 27 | test.end(); 28 | }); 29 | 30 | suite.test('should return the value if it is boolean & a truthy value', (test) => { 31 | test.plan(2); 32 | 33 | test.equal(convert(12), 12); 34 | test.equal(convert('pizza'), 'pizza'); 35 | 36 | test.end(); 37 | }) 38 | 39 | suite.end(); 40 | }); 41 | 42 | tap.test('#createYesNoValue', (suite) => { 43 | const create = inputs.createYesNoValue; 44 | 45 | suite.test('should return a function', (test) => { 46 | test.plan(1); 47 | test.equal( 48 | typeof create(), 49 | 'function' 50 | ); 51 | test.end(); 52 | }); 53 | 54 | suite.test('should return true for a yes answer & false for a no answer', (test) => { 55 | const yesNo = create(); 56 | 57 | test.plan(6); 58 | 59 | test.ok(yesNo('y')); 60 | test.ok(yesNo('yes')); 61 | test.ok(yesNo('YeS')); 62 | test.notOk(yesNo('n')); 63 | test.notOk(yesNo('no')); 64 | test.notOk(yesNo('nO')); 65 | 66 | test.end(); 67 | }); 68 | 69 | suite.test('should use a default if provided', (test) => { 70 | const yesNo = create('y'); 71 | 72 | test.plan(2); 73 | 74 | test.notOk(yesNo('n')); 75 | test.ok(yesNo()); 76 | 77 | test.end(); 78 | }); 79 | 80 | suite.test('should call a followup function if provided', (test) => { 81 | const followup = sinon.spy(); 82 | const known = [{ answer: 'hey', name: 'greeting' }]; 83 | const yesNo = create('', known, followup); 84 | 85 | test.plan(3); 86 | 87 | yesNo('unknown'); 88 | test.notOk(followup.called); 89 | 90 | yesNo('n', []); 91 | test.ok(followup.calledWith(false, known)); 92 | 93 | const yesNoKnown = create('', undefined, followup); 94 | yesNoKnown('y', []); 95 | test.ok(followup.calledWith(true, [])); 96 | 97 | test.end(); 98 | }); 99 | 100 | suite.end(); 101 | }); 102 | -------------------------------------------------------------------------------- /test/tools/utilities/options.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const sinon = require('sinon'); 4 | const tap = require('tap'); 5 | 6 | const options = require('../../resolver')(__filename); 7 | 8 | tap.test('#checkIsForExamples', (test) => { 9 | const check = options.checkIsForExamples; 10 | 11 | test.plan(5); 12 | 13 | test.notOk(check({ apple: [], broccoli: [] })); 14 | test.ok({ example: [] }); 15 | test.ok({ examples: [] }); 16 | test.ok({ x: [] }); 17 | test.ok(check({ apple: [], examples: [] })); 18 | 19 | test.end(); 20 | }); 21 | 22 | tap.test('#parseOptions', (suite) => { 23 | const parse = options.parseOptions; 24 | // --this is an option -> this: [is, an, option] 25 | // -its -> { i: [], t: [], s: [] } 26 | // --word=arguments,two -> word: [arguments, two] 27 | 28 | suite.test('should split a single-dash argument into multiple options', (test) => { 29 | test.plan(1); 30 | 31 | test.deepEqual(parse(['-abc']), { 32 | a: [], 33 | b: [], 34 | c: [] 35 | }); 36 | 37 | test.end(); 38 | }); 39 | 40 | suite.test('should save double-dash arguments as a single option', (test) => { 41 | test.plan(1); 42 | 43 | test.deepEqual(parse(['-abc', '--pizza']), { 44 | a: [], 45 | b: [], 46 | c: [], 47 | pizza: [] 48 | }); 49 | 50 | test.end(); 51 | }); 52 | 53 | suite.test('should store comma-delimited arguments passed to double-dash after =', (test) => { 54 | test.plan(1); 55 | 56 | test.deepEqual(parse(['-abc', '--pizza=pepperoni,mushroom']), { 57 | a: [], 58 | b: [], 59 | c: [], 60 | pizza: ['pepperoni', 'mushroom'] 61 | }); 62 | 63 | test.end(); 64 | }); 65 | 66 | suite.test('should store space-delimited arguments provided after double-dash', (test) => { 67 | const candidates = [ 68 | 'nope', // will not be added 69 | '-abc', 70 | 'notthis', // also won't be added 71 | '--pizza', '', 'pepperoni', 'mushroom', 72 | '--burger=cheese' 73 | ]; 74 | test.plan(1); 75 | 76 | test.deepEqual(parse(candidates), { 77 | a: [], 78 | b: [], 79 | burger: ['cheese'], 80 | c: [], 81 | pizza: ['pepperoni', 'mushroom'] 82 | }); 83 | 84 | test.end(); 85 | }); 86 | 87 | suite.test('should convert primitives to their actual values (eg, true should be a boolean)', (test) => { 88 | const candidates = [ 89 | 'nope', // will not be added 90 | '-abc', 91 | 'notthis', // also won't be added 92 | '--eat=true', 'false', 93 | '--orders=12', 94 | '--pizza', '', 'pepperoni', 'mushroom', 95 | '--burger=cheese' 96 | ]; 97 | test.plan(1); 98 | 99 | test.deepEqual(parse(candidates), { 100 | a: [], 101 | b: [], 102 | burger: ['cheese'], 103 | c: [], 104 | eat: [true, false], 105 | orders: [12], 106 | pizza: ['pepperoni', 'mushroom'] 107 | }); 108 | 109 | test.end(); 110 | }); 111 | 112 | suite.test('should not store arguments if a they are not in the provided valid set', (test) => { 113 | const candidates = [ 114 | 'nope', 115 | '-abc', 116 | '--unknown', 'pow', 'how', 117 | '--pizza=pepperoni,mushroom,true' 118 | ]; 119 | const valid = ['a', 'b', 'c', 'pizza']; 120 | test.plan(1); 121 | 122 | test.deepEqual(parse(candidates, valid), { 123 | a: [], 124 | b: [], 125 | c: [], 126 | pizza: ['pepperoni', 'mushroom', true] 127 | }); 128 | 129 | test.end(); 130 | }) 131 | 132 | suite.end(); 133 | }); 134 | -------------------------------------------------------------------------------- /test/upgrade.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | 1. check librarian version 4 | 2. install? 5 | 3. inquire w/ do you want to proceed? 6 | 4. Cancelled 7 | 5. Update files 8 | 9 | */ 10 | 'use strict'; 11 | 12 | const erector = require('erector-set'); 13 | const path = require('path'); 14 | const semver = require('semver'); 15 | const sinon = require('sinon'); 16 | const tap = require('tap'); 17 | 18 | const ex = require('../tools/utilities/execute'); 19 | const up = require('../commands/upgrade'); 20 | const testUtils = require('./test-utils'); 21 | 22 | const sandbox = sinon.sandbox.create(); 23 | const mockOnce = (util, method) => testUtils.mockOnce(sandbox, util, method); 24 | const npm = /^win/.test(process.platform) ? 'npm.cmd' : 'npm'; 25 | 26 | 27 | tap.test('command: upgrade', (suite) => { 28 | let gt; 29 | let make; 30 | let mocks; 31 | let execute; 32 | 33 | suite.beforeEach((done) => { 34 | gt = sandbox.stub(semver, 'gt'); 35 | make = testUtils.makeMake(up); 36 | mocks = testUtils.mock(sandbox); 37 | execute = sandbox.stub(ex, 'execute'); 38 | 39 | execute.callsFake((cmd, args) => { 40 | switch (args[0]) { 41 | case 'list': 42 | return 'angular-librarian@300.0.0'; 43 | case 'show': 44 | return '300.0.1'; 45 | default: 46 | return ''; 47 | } 48 | }); 49 | gt.returns(false); 50 | 51 | done(); 52 | }); 53 | 54 | suite.afterEach((done) => { 55 | sandbox.restore(); 56 | done(); 57 | }); 58 | 59 | suite.test('should create a Upgrade logger', (test) => { 60 | test.plan(1); 61 | 62 | make().catch(() => { 63 | test.ok(mocks.logger.calledWith('Upgrade')); 64 | test.end(); 65 | }); 66 | }); 67 | 68 | suite.test('should throw an error if no librarian version is installed when checking versions', (test) => { 69 | test.plan(1); 70 | 71 | execute.resetBehavior(); 72 | execute.callsFake((cmd, args) => { 73 | switch (args[0]) { 74 | case 'show': 75 | return 'angular-librarian@300.0.1'; 76 | default: 77 | return ''; 78 | } 79 | }); 80 | 81 | make().catch((error) => { 82 | test.equal(error, 'Angular Librarian is not installed. Not sure how that\'s possible!\n\n\tRun `npm i -D angular-librarian` to install'); 83 | test.end(); 84 | }); 85 | }); 86 | 87 | suite.test('should check for if librarian needs to be upgraded', (test) => { 88 | const { log } = mocks; 89 | test.plan(6); 90 | 91 | make().catch(() => { 92 | test.ok(log.firstCall.calledWith('[blue]Identifying the *newest* Angular Librarian version[/blue]')); 93 | test.ok(execute.firstCall.calledWith( 94 | npm, 95 | ['show', 'angular-librarian', 'version'] 96 | )); 97 | test.ok(log.secondCall.calledWith('[blue]Identifying the *installed* Angular Librarian version[/blue]')); 98 | test.ok(execute.secondCall.calledWith( 99 | npm, 100 | ['list', '--depth=0', 'angular-librarian'] 101 | )); 102 | test.ok(gt.calledWith('300.0.1', '300.0.0')); 103 | test.ok(log.thirdCall.calledWith( 104 | '[yellow] Upgrade of Angular Librarian is[/yellow]', 105 | '[red]NOT[/red]', 106 | '[yellow]required.[/yellow]' 107 | )); 108 | 109 | test.end(); 110 | }); 111 | }); 112 | 113 | suite.test('should install librarian if there is a newer version available', (test) => { 114 | const { log } = mocks; 115 | 116 | gt.resetBehavior(); 117 | gt.returns(true); 118 | test.plan(2); 119 | 120 | make().catch(() => { 121 | test.ok(log.getCall(3).calledWith( 122 | '[blue]Installing Angular Librarian 300.0.1[/blue]' 123 | )); 124 | test.ok(execute.thirdCall.calledWith( 125 | npm, 126 | ['i', '-D', 'angular-librarian@300.0.1'] 127 | )); 128 | test.end(); 129 | }); 130 | }); 131 | 132 | suite.test('should upgrade the branch version if specified in the project package.json', (test) => { 133 | const { checkVersion, log } = mocks; 134 | 135 | checkVersion.resetBehavior(); 136 | checkVersion.returns(true); 137 | 138 | test.plan(3); 139 | 140 | make().catch(() => { 141 | test.ok(log.calledWith('[blue]Upgrading Angular Librarian from:[/blue]')); 142 | test.ok(log.calledWith('[magenta] ice-cream[/magenta]')); 143 | test.ok(execute.calledWith( 144 | npm, 145 | ['up', 'angular-librarian'] 146 | )); 147 | test.end(); 148 | }); 149 | }); 150 | 151 | suite.test('should call erector.inquire with a confirm file upgrade question', (test) => { 152 | const { inquire } = mocks.erector; 153 | const createYesNo = mockOnce('inputs', 'createYesNoValue'); 154 | 155 | createYesNo.returns('yes-no'); 156 | test.plan(2); 157 | 158 | make().catch(() => { 159 | test.ok(inquire.calledWith([ 160 | { 161 | allowBlank: true, 162 | name: 'proceed', 163 | question: '[red]The following will overwrite some of the files in your project. Would you like to continue [/red](y/N)[red]?[/red]', 164 | transform: 'yes-no' 165 | } 166 | ])); 167 | test.ok(createYesNo.calledWith('n', [])); 168 | test.end(); 169 | }); 170 | }); 171 | 172 | suite.test('should cancel the file upgrade when the user answers no', (test) => { 173 | const { construct, inquire } = mocks.erector; 174 | const { log } = mocks; 175 | const answers = [ 176 | { 177 | name: 'proceed', 178 | answer: false 179 | } 180 | ]; 181 | 182 | inquire.resetBehavior(); 183 | inquire.resolves(answers); 184 | test.plan(2); 185 | 186 | make().then(() => { 187 | test.ok(log.lastCall.calledWith( 188 | '[yellow] Upgrade cancelled.[/yellow]' 189 | )); 190 | test.notOk(construct.called); 191 | test.end(); 192 | }); 193 | }); 194 | 195 | suite.test('should upgrade the files & dependencies when the user answers yes', (test) => { 196 | const { construct, inquire } = mocks.erector; 197 | const { getTemplates, log } = mocks; 198 | const answers = [ 199 | { 200 | name: 'name', 201 | answer: 'my-package' 202 | } 203 | ]; 204 | const finalAnswers = answers.concat( 205 | { 206 | name: 'packageName', 207 | answer: 'my-package' 208 | }, 209 | { 210 | name: 'librarianVersion', 211 | answer: 'ice-cream' 212 | }, 213 | { 214 | name: 'prefix', 215 | answer: 'nglpf' 216 | } 217 | ); 218 | const inquireAnswers = [ 219 | { 220 | name: 'proceed', 221 | answer: true 222 | } 223 | ]; 224 | const dirname = [process.cwd(), 'commands', 'upgrade'].join(path.sep); 225 | const getPrefix = mockOnce('files', 'getSelectorPrefix'); 226 | const include = mockOnce('files', 'include'); 227 | const open = mockOnce('files', 'open'); 228 | 229 | getPrefix.returns('nglpf'); 230 | include.returns({ 231 | name: 'my-package' 232 | }); 233 | open.returns(answers); 234 | inquire.resetBehavior(); 235 | inquire.resolves(inquireAnswers); 236 | test.plan(6); 237 | 238 | make().then(() => { 239 | test.ok(getTemplates.calledWith( 240 | '/root/', 241 | `/manual/${ dirname }/../initial`, 242 | [ 243 | { 244 | destination: '/root/.gitignore', 245 | name: '__gitignore', 246 | update: sinon.match.instanceOf(Function) 247 | }, 248 | { 249 | destination: '/root/.npmignore', 250 | name: '__npmignore', 251 | update: sinon.match.instanceOf(Function) 252 | }, 253 | { name: 'DEVELOPMENT.md' }, 254 | { name: 'karma.conf.js', overwrite: true }, 255 | { 256 | name: 'package.json', 257 | update: sinon.match.instanceOf(Function) 258 | }, 259 | { name: 'tsconfig.es5.json', overwrite: true }, 260 | { name: 'tsconfig.es2015.json', overwrite: true }, 261 | { name: 'tsconfig.json', overwrite: true }, 262 | { name: 'tsconfig.doc.json', overwrite: true }, 263 | { name: 'tsconfig.test.json', overwrite: true }, 264 | { name: 'tslint.json', overwrite: true }, 265 | { 266 | destination: '/created/src/test.js', 267 | name: 'src/test.js', overwrite: true 268 | }, 269 | { name: 'tasks/build.js', overwrite: true }, 270 | { name: 'tasks/copy-build.js', overwrite: true }, 271 | { name: 'tasks/copy-globs.js', overwrite: true }, 272 | { name: 'tasks/inline-resources.js', overwrite: true }, 273 | { name: 'tasks/rollup.js', overwrite: true }, 274 | { name: 'tasks/rollup-globals.js', overwrite: true }, 275 | { name: 'tasks/tag-version.js', overwrite: true }, 276 | { name: 'tasks/test.js', overwrite: true }, 277 | { name: 'webpack/webpack.common.js', overwrite: true }, 278 | { name: 'webpack/webpack.dev.js', overwrite: true }, 279 | { name: 'webpack/webpack.test.js', overwrite: true }, 280 | { name: 'webpack/webpack.utils.js', overwrite: true } 281 | ] 282 | )); 283 | test.ok(construct.calledWith(finalAnswers, 'fake-templates')); 284 | test.ok(log.calledWith( 285 | '[blue]Updating managed files to latest versions[/blue]' 286 | )); 287 | test.ok(log.calledWith( 288 | '[green] Files have been upgraded![/green]' 289 | )); 290 | test.ok(execute.calledWith( 291 | npm, 292 | ['i'] 293 | )); 294 | test.ok(execute.calledWith( 295 | npm, 296 | ['up'] 297 | )); 298 | 299 | test.end(); 300 | }); 301 | }); 302 | 303 | suite.test('should update package.json by merging', (test) => { 304 | const { getTemplates } = mocks; 305 | const { inquire } = mocks.erector; 306 | const answers = [ 307 | { 308 | name: 'name', 309 | answer: 'my-package' 310 | } 311 | ]; 312 | const inquireAnswers = [ 313 | { 314 | name: 'proceed', 315 | answer: true 316 | } 317 | ]; 318 | const jsonUpdater = sandbox.stub(erector.updaters, 'json'); 319 | const json = JSON.stringify({ 320 | author: 'Nope', 321 | fake: 12, 322 | keywords: [], 323 | toppings: 'all' 324 | }); 325 | const existing = JSON.stringify({ 326 | author: 'Chef Angular', 327 | description: 'A tasty recipe tool', 328 | es2015: './recipe-rocker.js', 329 | keywords: ['food', 'angular'], 330 | license: 'MIT', 331 | main: './recipe-rocker.bundle.js', 332 | module: './module.js', 333 | name: 'Reciper Rocker', 334 | repository: { 335 | type: 'git', 336 | url: 'https://github.com/recipes/rocker.git' 337 | }, 338 | typings: './toppings.d.ts', 339 | version: '9.9.9' 340 | }); 341 | const include = mockOnce('files', 'include'); 342 | const open = mockOnce('files', 'open'); 343 | 344 | include.returns({ 345 | name: 'my-package' 346 | }); 347 | open.returns(answers); 348 | inquire.resetBehavior(); 349 | inquire.resolves(inquireAnswers); 350 | jsonUpdater.returns(json); 351 | test.plan(1); 352 | 353 | make().then(() => { 354 | const updatePackage = getTemplates.lastCall.args[2][4].update; 355 | test.equal(updatePackage(existing, ''), JSON.stringify({ 356 | author: 'Chef Angular', 357 | fake: 12, 358 | keywords: ['food', 'angular'], 359 | toppings: 'all', 360 | description: 'A tasty recipe tool', 361 | es2015: './recipe-rocker.js', 362 | license: 'MIT', 363 | main: './recipe-rocker.bundle.js', 364 | module: './module.js', 365 | name: 'Reciper Rocker', 366 | repository: { 367 | type: 'git', 368 | url: 'https://github.com/recipes/rocker.git' 369 | }, 370 | typings: './toppings.d.ts', 371 | version: '9.9.9' 372 | }, null, 2)); 373 | test.end(); 374 | }); 375 | }); 376 | 377 | suite.test('should update flat files by appending user-added entries', (test) => { 378 | const { getTemplates } = mocks; 379 | const { inquire } = mocks.erector; 380 | const answers = [ 381 | { 382 | name: 'name', 383 | answer: 'my-package' 384 | }, 385 | { 386 | name: 'packageName', 387 | answer: 'my-package' 388 | } 389 | ]; 390 | const inquireAnswers = [ 391 | { 392 | name: 'proceed', 393 | answer: true 394 | } 395 | ]; 396 | let existing = 'pizza\nburgers\nice cream'; 397 | const include = mockOnce('files', 'include'); 398 | const open = mockOnce('files', 'open'); 399 | 400 | include.returns({ 401 | name: 'big-package' 402 | }); 403 | open.returns(answers); 404 | inquire.resetBehavior(); 405 | inquire.resolves(inquireAnswers); 406 | test.plan(2); 407 | 408 | make().then(() => { 409 | const updateFlat = getTemplates.lastCall.args[2][0].update; 410 | test.equal(updateFlat(existing, 'pizza\nice cream\ntacos'), 'pizza\nice cream\ntacos\nburgers'); 411 | test.equal(updateFlat(existing, 'pizza\r\nice cream\r\ntacos'), 'pizza\r\nice cream\r\ntacos\r\nburgers'); 412 | test.end(); 413 | }); 414 | }); 415 | 416 | suite.end(); 417 | }); 418 | -------------------------------------------------------------------------------- /tools/logging.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.create = (prefix = '') => Object.freeze({ 4 | error() { notify('error', prefix, arguments); }, 5 | info() { notify('info', prefix, arguments); }, 6 | log() { notify('log', prefix, arguments); }, 7 | warn() { notify('warn', prefix, arguments); } 8 | }); 9 | 10 | const notify = (type, prefix, args) => { 11 | args = Array.prototype.slice.call(args); 12 | console[type].apply(console, [`[${ prefix }]:`].concat(args)); 13 | }; 14 | -------------------------------------------------------------------------------- /tools/utilities/case-convert.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.checkIsDashFormat = (value) => 4 | !!value && typeof value === 'string' && 5 | value.length > 0 && 6 | value.match(/^[a-z][a-z0-9]*(\-[a-z0-9]+)*[a-z0-9]$/i); 7 | 8 | exports.testIsDashFormat = (value) => 9 | exports.checkIsDashFormat(value) ? value : null; 10 | 11 | exports.dashToCamel = (value, replaceChar = '') => 12 | value.replace(/(-.)/g, (match) => 13 | match.replace('-', replaceChar).toUpperCase() 14 | ); 15 | 16 | exports.dashToPascal = (value, replaceChar = '') => { 17 | const dash = exports.dashToCamel(value, replaceChar); 18 | 19 | return dash[0].toUpperCase() + exports.dashToCamel(dash.slice(1)); 20 | } 21 | 22 | exports.dashToCap = (value, replaceChar = '') => 23 | value[0].toUpperCase() + 24 | exports.dashToCamel(value.slice(1), replaceChar); 25 | 26 | exports.dashToWords = (value) => 27 | exports.dashToCap(value, ' '); 28 | -------------------------------------------------------------------------------- /tools/utilities/colorize.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const colorMap = { 4 | blue: 34, 5 | cyan: 36, 6 | green: 32, 7 | magenta: 35, 8 | red: 31, 9 | reset: 0, 10 | yellow: 33 11 | }; 12 | 13 | const colorize = (text, color) => { 14 | color = color in colorMap ? colorMap[color] : colorMap.reset; 15 | 16 | return `\x1b[${ color }m${ text }\x1b[0m`; 17 | }; 18 | 19 | module.exports = { 20 | colorize 21 | }; 22 | -------------------------------------------------------------------------------- /tools/utilities/execute.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const childProcess = require('child_process'); 4 | 5 | /* istanbul ignore next */ 6 | exports.execute = (command, args) => { 7 | const result = childProcess.spawnSync(command, args || [], { stdio: 'pipe' }); 8 | 9 | return result && result.stdout && result.stdout.toString().trim(); 10 | }; 11 | -------------------------------------------------------------------------------- /tools/utilities/files.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | 6 | exports.deleteFolder = (folder) => { 7 | if (fs.existsSync(folder)) { 8 | fs.readdirSync(folder).forEach((file) => removePath(folder, file)); 9 | fs.rmdirSync(folder); 10 | } 11 | }; 12 | 13 | const removePath = (folder, file) => { 14 | const filepath = path.resolve(folder, file); 15 | 16 | if (fs.lstatSync(filepath).isDirectory()) { 17 | exports.deleteFolder(filepath); 18 | } else { 19 | fs.unlinkSync(filepath); 20 | } 21 | }; 22 | 23 | exports.getTemplates = (rootDir, directory, filenames) => filenames.map((filename) => ({ 24 | check: filename.check, 25 | destination: filename.destination || path.resolve(rootDir, filename.name), 26 | template: filename.blank ? undefined : path.resolve(directory, 'templates', filename.name), 27 | update: filename.update, 28 | overwrite: filename.overwrite 29 | })); 30 | 31 | /* istanbul ignore next */ 32 | exports.include = (file) => fs.existsSync(file) && require(file); 33 | 34 | exports.open = (file, json = false) => { 35 | let contents = fs.readFileSync(file, 'utf8'); 36 | 37 | if (json) { 38 | contents = JSON.parse(contents); 39 | } 40 | 41 | return contents; 42 | } 43 | 44 | exports.resolver = { 45 | create() { 46 | const base = resolvePath(this.root(), arguments); 47 | 48 | return function() { 49 | return resolvePath(base, arguments); 50 | }; 51 | }, 52 | manual() { 53 | const args = Array.from(arguments); 54 | return resolvePath(args[0], args.slice(1)); 55 | }, 56 | root() { 57 | return resolvePath(process.cwd(), arguments); 58 | } 59 | }; 60 | 61 | const resolvePath = (prefix, args) => { 62 | const argsList = Array.prototype.slice.call(args); 63 | 64 | return path.resolve.apply(path.resolve, [prefix].concat(argsList)); 65 | }; 66 | 67 | const getLibrarianVersion = () => { 68 | let version = getPackageLibrarianVersion(); 69 | 70 | if (!checkIsBranch(version)) { 71 | version = exports.include( 72 | exports.resolver.manual(__dirname, '..', '..', 'package.json') 73 | ).version; 74 | } 75 | 76 | return version; 77 | }; 78 | 79 | const getPackageLibrarianVersion = () => { 80 | const pkg = exports.include(exports.resolver.root('package.json')); 81 | let version; 82 | 83 | if (pkg) { 84 | version = getVersionFromPackage(pkg); 85 | } 86 | 87 | return version; 88 | }; 89 | 90 | const getVersionFromPackage = (pkg) => 91 | getPackageVersion(pkg, 'devDependencies') || 92 | getPackageVersion(pkg, 'dependencies'); 93 | 94 | const getPackageVersion = (pkg, attribute) => 95 | pkg[attribute] && 96 | 'angular-librarian' in pkg[attribute] && 97 | pkg[attribute]['angular-librarian']; 98 | 99 | const checkIsBranch = (version) => /^(git\+)?https?\:/.test(version); 100 | 101 | exports.librarianVersions = { 102 | checkIsBranch, 103 | get: getLibrarianVersion 104 | }; 105 | 106 | const getSelectorPrefixFromTslintRules = (selector) => { 107 | const tslint = exports.include(exports.resolver.root('tslint.json')); 108 | let prefix = ''; 109 | 110 | if (tslint && tslint.rules && tslint.rules[selector]) { 111 | prefix = getValueFromTslintRules(tslint, selector)[2]; 112 | } 113 | 114 | return prefix; 115 | }; 116 | 117 | const getValueFromTslintRules = (tslint, attribute) => 118 | tslint.rules[attribute]; 119 | 120 | exports.getSelectorPrefix = (selector) => 121 | getSelectorPrefixFromTslintRules(selector); 122 | -------------------------------------------------------------------------------- /tools/utilities/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.caseConvert = require('./case-convert'); 4 | exports.colorize = require('./colorize'); 5 | exports.execute = require('./execute'); 6 | exports.files = require('./files'); 7 | exports.inputs = require('./inputs'); 8 | exports.options = require('./options'); 9 | 10 | exports.checkIsScopedName = (name) => 11 | // @ 12 | // followed by 1+ non-/ 13 | // followed by / 14 | // folloer by 1+ non-/ 15 | /^@[^/]+[/][^/]+$/.test(name); -------------------------------------------------------------------------------- /tools/utilities/inputs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const erectorUtils = require('erector-set/src/utils'); 4 | 5 | exports.convertYesNoValue = (value) => { 6 | if (erectorUtils.checkIsType(value, 'boolean')) { 7 | value = value ? 'Y' : 'N'; 8 | } else if (!value) { 9 | value = 'N'; 10 | } 11 | 12 | return value; 13 | }; 14 | 15 | exports.createYesNoValue = (defaultValue, knownAnswers, followup) => (value, answers) => { 16 | const lookup = { n: false, y: true }; 17 | let result; 18 | 19 | if (typeof value === 'string' && value.match(/^(y(es)?|no?)$/i)) { 20 | value = value.slice(0, 1).toLowerCase(); 21 | } else if (!value && !!defaultValue) { 22 | value = defaultValue.toLowerCase(); 23 | } 24 | 25 | result = lookup[value]; 26 | if (result !== undefined && typeof followup === 'function') { 27 | result = followup(result, answers.concat(knownAnswers || [])); 28 | } 29 | 30 | return result; 31 | }; 32 | -------------------------------------------------------------------------------- /tools/utilities/options.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.checkIsForExamples = (options) => 4 | exports.checkHasOption(options, ['example', 'examples', 'x']); 5 | 6 | exports.checkHasOption = (options, valid) => 7 | !!valid.find((option) => option in options); 8 | 9 | exports.parseOptions = (candidates, valid) => 10 | createOptionsMap(formatOptions(candidates), valid); 11 | 12 | const formatOptions = (candidates) => candidates.reduce((options, candidate) => { 13 | candidate = candidate.trim(); 14 | 15 | if (candidate.substring(0, 2) === '--') { 16 | options = options.concat(candidate); 17 | } else if (candidate[0] === '-') { 18 | options = options.concat(candidate.substring(1).split('').map((option) => '-' + option)); 19 | } else { 20 | options = addValueToLastOption(options, candidate); 21 | } 22 | 23 | return options; 24 | }, []); 25 | 26 | const addValueToLastOption = (options, candidate) => { 27 | let option = options[options.length - 1]; 28 | let separator = ','; 29 | 30 | if (option && option.substring(0, 2) === '--' && candidate) { 31 | if (option.indexOf('=') === -1) { 32 | separator = '='; 33 | } 34 | 35 | options = options.slice(0, -1).concat(option + separator + candidate); 36 | } 37 | 38 | return options; 39 | }; 40 | 41 | const createOptionsMap = (candidates, valid) => candidates.reduce((options, candidate) => { 42 | const parts = candidate.split('='); 43 | const option = parts[0].replace(/^--?/, ''); 44 | const values = parts[1] ? parts[1].split(',').map(convertToType) : []; 45 | 46 | if (checkCanAddOption(options, option, valid)) { 47 | options[option] = values; 48 | } 49 | 50 | return options; 51 | }, {}); 52 | 53 | const convertToType = (value) => { 54 | try { 55 | // this will take ANYTHING, so a IIFE 56 | // will get parsed...but for an app like 57 | // this, it's ok 58 | value = eval(value); 59 | } catch (e) {} 60 | 61 | return value; 62 | }; 63 | 64 | const checkCanAddOption = (options, option, valid) => 65 | (!valid || valid.length === 0 || valid.indexOf(option) !== -1) && 66 | !options.hasOwnProperty(option); --------------------------------------------------------------------------------