├── .editorconfig ├── .gitignore ├── .node-version ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .stylelintrc ├── .vcmrc ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── ROADMAP.md ├── circle.yml ├── codecov.yml ├── docs ├── environment.md ├── favicons.md ├── firebase.md ├── http-cache-tags.md ├── user-agent.md └── window.md ├── fuse.ts ├── package-lock.json ├── package.json ├── spec └── utilities │ └── welcome.test.ts ├── src ├── assets │ ├── favicon.svg │ └── splash.txt ├── commands │ ├── build.ts │ ├── config.ts │ ├── create-common.ts │ ├── create-firebase.ts │ ├── create-ui-questions.ts │ ├── create.ts │ ├── deps.ts │ ├── favicon.ts │ ├── index.ts │ ├── lint.ts │ ├── serve.ts │ ├── test.ts │ └── update.ts ├── fusebox │ ├── compression.plugin.ts │ ├── ng.aot-factory.plugin.ts │ ├── ng.aot-relative.plugin.ts │ ├── ng.compiler.plugin.ts │ ├── ng.polyfill.plugin.ts │ ├── ng.prod.plugin.ts │ └── ng.sw.plugin.ts ├── generators │ ├── angular-core.gen.ts │ ├── angular-universal.gen.ts │ ├── config.gen.ts │ ├── declarations.gen.ts │ ├── deps.const.ts │ ├── env.gen.ts │ ├── gitignore.gen.ts │ ├── ide.gen.ts │ ├── package.gen.ts │ ├── tsconfig.gen.ts │ └── tslint.gen.ts ├── index.ts ├── modules │ ├── cookies │ │ ├── browser.ts │ │ ├── common.ts │ │ ├── cookies.browser.module.ts │ │ ├── cookies.browser.service.ts │ │ ├── cookies.server.module.ts │ │ ├── cookies.server.service.ts │ │ └── server.ts │ ├── environment │ │ ├── common.ts │ │ ├── environment.browser.module.ts │ │ ├── environment.server.module.ts │ │ ├── environment.service.ts │ │ └── index.ts │ ├── firebase │ │ ├── auth │ │ │ ├── app.auth.module.ts │ │ │ ├── app.common.ts │ │ │ ├── browser.auth.module.ts │ │ │ ├── browser.auth.service.ts │ │ │ ├── browser.common.ts │ │ │ ├── browser.ts │ │ │ ├── loading-container │ │ │ │ └── loading-container.component.ts │ │ │ ├── server.auth.module.ts │ │ │ ├── server.auth.service.ts │ │ │ ├── server.common.ts │ │ │ ├── server.ts │ │ │ └── tokens.ts │ │ ├── common │ │ │ ├── browser.ts │ │ │ └── server.ts │ │ ├── firebase.app.module.ts │ │ ├── firestore │ │ │ ├── browser.firebase.fs.common.ts │ │ │ ├── browser.firebase.fs.module.ts │ │ │ ├── browser.firebase.fs.service.ts │ │ │ ├── server.firebase.fs.common.ts │ │ │ ├── server.firebase.fs.module.ts │ │ │ └── server.firebase.fs.service.ts │ │ ├── index.ts │ │ └── rtdb │ │ │ ├── browser.firebase.rtdb.common.ts │ │ │ ├── browser.firebase.rtdb.module.ts │ │ │ ├── browser.firebase.rtdb.service.ts │ │ │ ├── server.firebase.rtdb.common.ts │ │ │ ├── server.firebase.rtdb.module.ts │ │ │ └── server.firebase.rtdb.service.ts │ ├── fusing-angular │ │ ├── browser.ts │ │ └── server.ts │ ├── http-cache-tag │ │ ├── http-cache-tag-interceptor.service.ts │ │ ├── http-cache-tag.server.module.ts │ │ └── index.ts │ ├── index.ts │ ├── not-found │ │ ├── index.ts │ │ ├── not-found.component.ts │ │ └── not-found.module.ts │ ├── response │ │ ├── browser.response.module.ts │ │ ├── browser.response.service.ts │ │ ├── browser.ts │ │ ├── common.ts │ │ ├── server.response.module.ts │ │ ├── server.response.service.ts │ │ └── server.ts │ ├── tsconfig.aot.json │ └── util │ │ ├── config-server.ts │ │ ├── external-link.directive.ts │ │ ├── header.service.ts │ │ ├── monads │ │ ├── index.ts │ │ └── maybe.ts │ │ ├── tokens.ts │ │ ├── user-agent.service.ts │ │ └── window │ │ ├── window-browser.module.ts │ │ ├── window-server.module.ts │ │ └── window.service.ts ├── templates │ ├── component │ │ └── component.ts.txt │ ├── core │ │ ├── app │ │ │ ├── app.component.html.txt │ │ │ ├── app.component.scss.txt │ │ │ ├── app.component.spec.ts.txt │ │ │ ├── app.component.ts.txt │ │ │ ├── app.module.ts.txt │ │ │ ├── app.routing.module.ts.txt │ │ │ ├── app.shared.module.ts.txt │ │ │ ├── favicon.svg.txt │ │ │ ├── home.component.ts.txt │ │ │ ├── index.pug.txt │ │ │ ├── index.ts │ │ │ └── ngsw.json.txt │ │ ├── assets │ │ │ ├── index.ts │ │ │ └── robots.txt │ │ ├── browser │ │ │ ├── app.browser.entry.aot.ts.txt │ │ │ ├── app.browser.entry.jit.ts.txt │ │ │ ├── app.browser.module.ts.txt │ │ │ └── index.ts │ │ └── server │ │ │ ├── index.ts │ │ │ ├── server.angular.module.ts.txt │ │ │ ├── server.app.ts.txt │ │ │ └── server.ts.txt │ ├── declarations.ts.txt │ ├── env.txt │ ├── favicon.ts │ ├── fusebox.ts │ ├── gitignore.txt │ ├── route-module │ │ ├── component.ts.txt │ │ ├── module.ts.txt │ │ └── routing.module.ts.txt │ ├── tsconfig.aot.json.txt │ ├── tsconfig.json.txt │ ├── tslint.json.txt │ ├── unit-tests │ │ ├── app-testing.module.ts.txt │ │ └── jest │ │ │ ├── AngularSnapshotSerializer.js │ │ │ ├── HTMLCommentSerializer.js │ │ │ ├── jest.setup.js │ │ │ ├── preprocessor.js │ │ │ └── vs-code.config.json │ └── vscode │ │ ├── launch.json.txt │ │ └── settings.json.txt └── utilities │ ├── clear.ts │ ├── create-folder.ts │ ├── environment-variables.ts │ ├── log.ts │ ├── read-config.ts │ ├── rx-favicon.ts │ ├── rx-fs.ts │ ├── sass.ts │ └── welcome.ts ├── tools ├── manual-typings │ ├── json.d.ts │ └── txt.d.ts ├── scripts │ └── fuse-shebang.ts └── setup │ └── mac.sh ├── tsconfig.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | insert_final_newline = false 15 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | .DS_Store 8 | dist 9 | .env 10 | .fusebox 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | 24 | # nyc test coverage 25 | .nyc_output 26 | 27 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 28 | .grunt 29 | 30 | # Bower dependency directory (https://bower.io/) 31 | bower_components 32 | 33 | # node-waf configuration 34 | .lock-wscript 35 | 36 | # Compiled binary addons (http://nodejs.org/api/addons.html) 37 | build/Release 38 | 39 | # Dependency directories 40 | node_modules/ 41 | jspm_packages/ 42 | 43 | # Typescript v1 declaration files 44 | typings/ 45 | 46 | # Optional npm cache directory 47 | .npm 48 | 49 | # Optional eslint cache 50 | .eslintcache 51 | 52 | # Optional REPL history 53 | .node_repl_history 54 | 55 | # Output of 'npm pack' 56 | *.tgz 57 | 58 | # Yarn Integrity file 59 | .yarn-integrity 60 | 61 | # dotenv environment variables file 62 | .env 63 | 64 | dist/ 65 | 66 | .DS_Store 67 | 68 | test-report.xml 69 | test-results.xml 70 | 71 | ngc 72 | .ngc 73 | .aot 74 | aot 75 | .e2e 76 | 77 | documentation 78 | 79 | src/config.json 80 | .serverless 81 | .dist/ 82 | .build/ 83 | fusing-angular-demo-app/ 84 | fusing-angular.json -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 10.7.0 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 10.7.0 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | package-lock.json 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | semi: false 2 | singleQuote: true -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "block-no-empty": null, 4 | "color-no-invalid-hex": true, 5 | "comment-empty-line-before": [ 6 | "always", { 7 | "ignore": ["stylelint-commands", "between-comments"] 8 | } 9 | ], 10 | "declaration-colon-space-after": "always", 11 | "indentation": 2, 12 | "max-empty-lines": 2, 13 | "unit-whitelist": ["em", "rem", "%", "px", "s", "ms", "vw", "vh", "deg"] 14 | } 15 | } -------------------------------------------------------------------------------- /.vcmrc: -------------------------------------------------------------------------------- 1 | { 2 | "types": [ 3 | "feat", 4 | "fix", 5 | "docs", 6 | "style", 7 | "refactor", 8 | "perf", 9 | "test", 10 | "build", 11 | "ci", 12 | "chore", 13 | "revert" 14 | ], 15 | "scope": { 16 | "required": false, 17 | "allowed": [ 18 | "*" 19 | ], 20 | "validate": false, 21 | "multiple": false 22 | }, 23 | "warnOnFail": false, 24 | "maxSubjectLength": 72, 25 | "subjectPattern": ".+", 26 | "subjectPatternErrorMsg": "subject does not match subject pattern!", 27 | "helpMessage": "", 28 | "autoFix": false 29 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "npm.enableScriptExplorer": true, 4 | "typescript.tsdk": "node_modules/typescript/lib" 5 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) Patrick Michalina 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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fusing Angular CLI 2 | 3 | _Faster CLI tool for Angular_ 4 | 5 | [![CircleCI](https://circleci.com/gh/patrickmichalina/fusing-angular-cli.svg?style=shield)](https://circleci.com/gh/patrickmichalina/fusing-angular-cli) 6 | [![Greenkeeper badge](https://badges.greenkeeper.io/patrickmichalina/fusing-angular-cli.svg)](https://greenkeeper.io/) 7 | [![dependencies Status](https://david-dm.org/patrickmichalina/fusing-angular-cli/status.svg)](https://david-dm.org/patrickmichalina/fusing-angular-cli) 8 | [![devDependencies Status](https://david-dm.org/patrickmichalina/fusing-angular-cli/dev-status.svg)](https://david-dm.org/patrickmichalina/fusing-angular-cli?type=dev) 9 | [![Fusebox-bundler](https://img.shields.io/badge/gitter-join%20chat%20%E2%86%92-brightgreen.svg)](https://gitter.im/fusing-angular-cli/Lobby) 10 | 11 | **WARNING: WORK IN PROGRESS** 12 | 13 | ## Prerequisites 14 | 15 | The CLI has dependencies that require Node 10.0.0 or higher, together with NPM 6.0.0 or higher. 16 | 17 | ## Table of Contents 18 | 19 | - [Features](#features) 20 | - [Roadmap](#roadmap) 21 | - [Installation](#installation) 22 | - [Usage](#usage) 23 | - [Updating Fusing Angular CLI](#updating-fusing-angular-cli) 24 | - [Developing Fusing Angular CLI](#developing-fusing-angular-cli) 25 | - [License](#license) 26 | 27 | ## Features 28 | 29 | - [x] Designed for use with [Angular Universal](https://universal.angular.io) (server rendered) 30 | - [x] Designed for use with [SCSS](https://sass-lang.com) 31 | - [x] [Jest](https://jestjs.io) test runner and code coverage 32 | - [x] Lazy Loaded modules 33 | - [ ] Route, Component, Directive, and Service generators 34 | - [x] Fully optimized production builds (brotli: ~125 kb, gzip: ~150 kb) 35 | - [x] Easy favicon generator 36 | - [x] Check code quality with built in Linter 37 | - [x] Visual Studio Code integration 38 | - [ ] Circle CI support 39 | - [ ] UI integrations (Material, Bootstrap, and Bulma) 40 | 41 | ## Roadmap 42 | 43 | You can learn more about what we aim to achieve by reading our [roadmap](ROADMAP.md) 44 | 45 | ## Installation 46 | 47 | ```bash 48 | npm install -g fusing-angular-cli 49 | ``` 50 | 51 | ## Usage 52 | 53 | ```bash 54 | fng help 55 | ``` 56 | 57 | ### Updating Fusing Angular CLI 58 | 59 | ```bash 60 | fng update 61 | ``` 62 | 63 | or 64 | 65 | ```bash 66 | npm i -g fusing-angular-cli@latest 67 | ``` 68 | 69 | ## Developing Fusing Angular CLI 70 | 71 | ### Bundling and runnng the code 72 | 73 | ```bash 74 | # to run local CLI 75 | $ npm start 76 | 77 | # to run local CLI w/ continuous testing 78 | $ npm run start.dev 79 | 80 | # issuing CLI commands to local build 81 | $ .build/fng [some commands go here] 82 | ``` 83 | 84 | ### Testing your code 85 | 86 | ```bash 87 | # test your code 88 | npm test 89 | ``` 90 | 91 | ## License 92 | 93 | MIT 94 | -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | ### MVP (v1) 2 | 3 | - [x] CLI project structure 4 | - [x] CLI build and release tooling 5 | - [x] Server (Universal) based Angular app schaffolding 6 | - [x] Serve command for both dev and production runtimes, ex: `fng serve --prod --sw --aot` 7 | - [x] Jest 8 | - [ ] Auto git init on project creation 9 | - [ ] Environment variable transfer from server to client 10 | - [ ] Favicon generation command, ex: `fng favicon` 11 | - [ ] Angular Material integration 12 | - [ ] Component/Pipe/Directive/Service stubb generator command, ex: `fng gen component my-awesome-comp` 13 | 14 | ### Future Work 15 | 16 | #### UI Options 17 | 18 | - [ ] Bootstrap 19 | - [ ] Bulma 20 | 21 | #### Backend Options 22 | 23 | - [ ] Firebase 24 | 25 | #### Continous Integration Options 26 | 27 | - [ ] Circle CI 28 | - [ ] Travis 29 | 30 | #### E2E Test Framework Options 31 | 32 | - [ ] Nightmare 33 | - [ ] Cypress 34 | 35 | #### Deployment Options 36 | 37 | The goal is to allow command line provisioning of new applications. 38 | 39 | - [ ] Heroku 40 | - [ ] Azure 41 | - [ ] Google Cloud (serverless) 42 | - [ ] AWS Lambda (serverless) 43 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | defaults: &defaults 2 | docker: 3 | - image: circleci/node:10.7.0 4 | 5 | version: 2 6 | jobs: 7 | build: 8 | <<: *defaults 9 | steps: 10 | - checkout 11 | - restore_cache: 12 | key: dependency-cache-{{ checksum "package.json" }} 13 | - run: 14 | name: Install npm 15 | command: npm install 16 | - run: 17 | name: Test 18 | command: npm test 19 | - save_cache: 20 | key: dependency-cache-{{ checksum "package.json" }} 21 | paths: 22 | - node_modules 23 | lint: 24 | <<: *defaults 25 | steps: 26 | - checkout 27 | - restore_cache: 28 | key: dependency-cache-{{ checksum "package.json" }} 29 | - run: 30 | name: Install npm 31 | command: npm install 32 | - run: 33 | name: Lint 34 | command: npm run lint 35 | semver: 36 | <<: *defaults 37 | steps: 38 | - checkout 39 | - restore_cache: 40 | key: dependency-cache-{{ checksum "package.json" }} 41 | - run: 42 | name: Install npm 43 | command: npm install 44 | - run: 45 | name: Semantic Release 46 | command: node_modules/.bin/semantic-release 47 | workflows: 48 | version: 2 49 | build_test_release: 50 | jobs: 51 | - build 52 | - lint: 53 | requires: 54 | - build 55 | - semver: 56 | requires: 57 | - build 58 | - lint 59 | filters: 60 | branches: 61 | only: master 62 | # - publish: 63 | # requires: 64 | # - build 65 | # - lint 66 | # filters: 67 | # branches: 68 | # only: develop -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | allow_coverage_offsets: true 3 | notify: 4 | require_ci_to_pass: true 5 | 6 | coverage: 7 | precision: 2 8 | round: down 9 | range: "70...100" 10 | ignore: 11 | - "tools" 12 | - ".vscode" 13 | - "fuse.ts" 14 | 15 | status: 16 | project: 17 | default: 18 | enabled: yes 19 | threshold: 0.25% 20 | patch: 21 | default: 22 | enabled: yes 23 | target: 0% 24 | changes: false -------------------------------------------------------------------------------- /docs/environment.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patrickmichalina/fusing-angular-cli/f9331d3d94dbc14d8e0d21ace554af5bbd7ae7e9/docs/environment.md -------------------------------------------------------------------------------- /docs/favicons.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patrickmichalina/fusing-angular-cli/f9331d3d94dbc14d8e0d21ace554af5bbd7ae7e9/docs/favicons.md -------------------------------------------------------------------------------- /docs/firebase.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patrickmichalina/fusing-angular-cli/f9331d3d94dbc14d8e0d21ace554af5bbd7ae7e9/docs/firebase.md -------------------------------------------------------------------------------- /docs/http-cache-tags.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patrickmichalina/fusing-angular-cli/f9331d3d94dbc14d8e0d21ace554af5bbd7ae7e9/docs/http-cache-tags.md -------------------------------------------------------------------------------- /docs/user-agent.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patrickmichalina/fusing-angular-cli/f9331d3d94dbc14d8e0d21ace554af5bbd7ae7e9/docs/user-agent.md -------------------------------------------------------------------------------- /docs/window.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patrickmichalina/fusing-angular-cli/f9331d3d94dbc14d8e0d21ace554af5bbd7ae7e9/docs/window.md -------------------------------------------------------------------------------- /fuse.ts: -------------------------------------------------------------------------------- 1 | import { FuseBox, QuantumPlugin, JSONPlugin, RawPlugin } from 'fuse-box' 2 | import { src, task } from 'fuse-box/sparky' 3 | import { resolve } from 'path' 4 | import { argv } from 'yargs' 5 | import { execSync } from 'child_process' 6 | import shabang from './tools/scripts/fuse-shebang' 7 | // import { unlinkSync } from 'fs' 8 | 9 | const appName = 'fng' 10 | const outputDir = '.build' 11 | const homeDir = './' 12 | const outputPath = `${outputDir}/${appName}` 13 | const absOutputPath = resolve(outputPath) 14 | const isProdBuild = argv.build 15 | 16 | const fuseConfig = FuseBox.init({ 17 | log: false, 18 | cache: !isProdBuild, 19 | target: 'server@es5', 20 | homeDir, 21 | output: `${outputDir}/$name`, 22 | globals: { 23 | default: '*' 24 | }, 25 | package: { 26 | name: 'default', 27 | main: outputPath 28 | }, 29 | plugins: [ 30 | JSONPlugin(), 31 | RawPlugin(['.txt']), 32 | isProdBuild && 33 | QuantumPlugin({ 34 | bakeApiIntoBundle: appName, 35 | treeshake: true, 36 | uglify: true 37 | }) 38 | ] 39 | }) 40 | 41 | const bundle = fuseConfig.bundle(appName) 42 | 43 | task('test', () => { 44 | bundle.test('[spec/**/**.ts]', {}) 45 | }) 46 | 47 | task('cp.jest', () => { 48 | return src('jest/**', { base: 'src/templates/unit-tests' }).dest('.build/') 49 | }) 50 | 51 | task('bundle', ['cp.jest', 'ng.modules'], () => { 52 | bundle.instructions('> [src/index.ts]') 53 | !isProdBuild && 54 | bundle.watch(`src/**`).completed(fp => shabang(fp.bundle, absOutputPath)) 55 | 56 | fuseConfig.run().then(bp => { 57 | const bundle = bp.bundles.get(appName) 58 | bundle && shabang(bundle, absOutputPath) 59 | }) 60 | }) 61 | 62 | task('ng.modules', () => { 63 | return new Promise((res, rej) => { 64 | const tsc = execSync( 65 | resolve('node_modules/.bin/ngc --p src/modules/tsconfig.aot.json') 66 | ).toString() 67 | return tsc ? rej() : res() 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fusing-angular-cli", 3 | "version": "0.0.0-development", 4 | "description": "Angular application generator and runner", 5 | "main": ".build/index.js", 6 | "ts:main": "index.ts", 7 | "typings": "index.ts", 8 | "license": "MIT", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/patrickmichalina/fusing-angular-cli" 12 | }, 13 | "author": { 14 | "name": "Patrick Michalina", 15 | "email": "patrickmichalina@mac.com", 16 | "url": "https://github.com/patrickmichalina/fusing-angular-cli" 17 | }, 18 | "bin": { 19 | "fng": ".build/fng" 20 | }, 21 | "files": [ 22 | ".build" 23 | ], 24 | "engines": { 25 | "node": ">= 10.0.0" 26 | }, 27 | "scripts": { 28 | "lint": "tslint --project tsconfig.json --config tslint.json", 29 | "test": "ts-node fuse test", 30 | "test.watch": "chokidar '(src|spec)/**/*.ts' -c 'ts-node fuse test' --initial --silent", 31 | "build": "ts-node fuse bundle --build", 32 | "prepublishOnly": "ts-node fuse bundle --build", 33 | "precommit": "pretty-quick --staged --no-semi --single-quote", 34 | "start": "ts-node fuse bundle", 35 | "start.dev": "ts-node fuse bundle & npm run test.watch" 36 | }, 37 | "devDependencies": { 38 | "@types/chalk": "^2.2.0", 39 | "@types/dotenv": "^4.0.3", 40 | "@types/favicons": "^5.1.0", 41 | "@types/iltorb": "^2.0.1", 42 | "@types/inquirer": "^0.0.42", 43 | "@types/node-sass": "^3.10.32", 44 | "@types/npm": "^2.0.29", 45 | "@types/ua-parser-js": "^0.7.32", 46 | "@types/yargs": "^11.1.1", 47 | "chokidar-cli": "^1.2.0", 48 | "condition-circle": "^2.0.1", 49 | "fuse-test-runner": "^1.0.16", 50 | "husky": "^0.14.3", 51 | "last-release-git": "0.0.3", 52 | "prettier": "^1.13.7", 53 | "pretty-quick": "^1.6.0", 54 | "semantic-release": "^15.8.1", 55 | "tslint-immutable": "^4.6.0", 56 | "validate-commit-msg": "^2.14.0" 57 | }, 58 | "dependencies": { 59 | "@angular/animations": "^6.1.0", 60 | "@angular/cdk": "^6.4.1", 61 | "@angular/common": "^6.1.0", 62 | "@angular/compiler": "^6.1.0", 63 | "@angular/compiler-cli": "^6.1.0", 64 | "@angular/core": "^6.1.0", 65 | "@angular/forms": "^6.1.0", 66 | "@angular/http": "^6.1.0", 67 | "@angular/material": "^6.4.1", 68 | "@angular/platform-browser": "^6.1.0", 69 | "@angular/platform-browser-dynamic": "^6.1.0", 70 | "@angular/platform-server": "^6.1.0", 71 | "@angular/router": "^6.1.0", 72 | "@angular/service-worker": "^6.1.0", 73 | "@nguniversal/common": "^6.0.0", 74 | "@nguniversal/express-engine": "^6.0.0", 75 | "@types/cookie-parser": "^1.4.1", 76 | "@types/express": "^4.16.0", 77 | "@types/fs-extra": "^5.0.4", 78 | "@types/jest": "^23.3.1", 79 | "@types/js-cookie": "^2.1.0", 80 | "@types/lru-cache": "^4.1.1", 81 | "@types/node": "^10.5.4", 82 | "@types/object-hash": "^1.2.0", 83 | "ajv": "^6.5.2", 84 | "angularfire2": "^5.0.0-rc.11", 85 | "chalk": "^2.4.1", 86 | "consolidate": "^0.15.1", 87 | "cookie-parser": "^1.4.3", 88 | "core-js": "^2.5.7", 89 | "date-fns": "^1.29.0", 90 | "dotenv": "^6.0.0", 91 | "express": "^4.16.3", 92 | "favicons": "^5.1.1", 93 | "firebase": "^5.3.0", 94 | "firebase-admin": "^5.13.1", 95 | "fs-extra": "^7.0.0", 96 | "fuse-box": "^3.4.0", 97 | "hammerjs": "^2.0.8", 98 | "iltorb": "^2.3.2", 99 | "inquirer": "^6.0.0", 100 | "jest": "23.4.1", 101 | "jest-junit-reporter": "^1.1.0", 102 | "jest-preset-angular": "^5.2.3", 103 | "jest-zone-patch": "0.0.8", 104 | "js-cookie": "^2.2.0", 105 | "lru-cache": "^4.1.3", 106 | "ms": "^2.1.1", 107 | "ng2-fused": "^0.5.1", 108 | "node-sass": "^4.9.2", 109 | "npm": "^6.2.0", 110 | "object-hash": "^1.3.0", 111 | "pug": "^2.0.3", 112 | "reload": "^2.3.0", 113 | "rxjs": "^6.2.2", 114 | "simple-git": "^1.96.0", 115 | "ts-jest": "22.4.6", 116 | "ts-node": "^7.0.0", 117 | "tslint": "^5.11.0", 118 | "typescript": "2.9.2", 119 | "ua-parser-js": "^0.7.18", 120 | "uglify-js": "^3.4.6", 121 | "yargs": "^12.0.1", 122 | "zone.js": "^0.8.26" 123 | }, 124 | "release": { 125 | "branch": "master", 126 | "verifyConditions": "condition-circle", 127 | "getLastRelease": "last-release-git" 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /spec/utilities/welcome.test.ts: -------------------------------------------------------------------------------- 1 | import { should } from 'fuse-test-runner' 2 | import { pathExistsDeep_ } from '../../src/utilities/rx-fs' 3 | import { first } from 'rxjs/operators' 4 | import { firebaseEnvConfigMap } from '../../src/generators/env.gen' 5 | import welcome from '../../src/utilities/welcome' 6 | 7 | // tslint:disable-next-line:no-class 8 | export class WelcomeToTheJungle { 9 | 'should be okay'() { 10 | const text = welcome() 11 | should(text).beString() 12 | } 13 | 14 | 'should return non-existing paths array'(done) { 15 | pathExistsDeep_('src/assets_no_exist/no_exist') 16 | .pipe(first()) 17 | .subscribe(paths => { 18 | should(paths).haveLength(2) 19 | should(new RegExp(/src\/assets_no_exist/g).test(paths[0])).beTrue() 20 | should( 21 | new RegExp(/src\/assets_no_exist\/no_exist/g).test(paths[1]) 22 | ).beTrue() 23 | done() 24 | }) 25 | } 26 | 27 | 'maps firebase config'() { 28 | const mapped = firebaseEnvConfigMap({ 29 | apiKey: 'MVa3fdsaWzxyfdEVdnhP', 30 | authDomain: 'firebaseapp.com', 31 | databaseUrl: 'firebaseio.com', 32 | messagingSenderId: 'consulting', 33 | projectId: 'appspot.com', 34 | storageBucket: '83984' 35 | }) 36 | should(mapped).equal(`FNG_FIREBASE_API_KEY=MVa3fdsaWzxyfdEVdnhP 37 | FNG_FIREBASE_AUTH_DOMAIN=firebaseapp.com 38 | FNG_FIREBASE_DATABASE_URL=firebaseio.com 39 | FNG_FIREBASE_PROJECT_ID=appspot.com 40 | FNG_FIREBASE_STORAGE_BUCKET=83984 41 | FNG_FIREBASE_MESSAGING_SENDER_ID=consulting`) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/assets/favicon.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/splash.txt: -------------------------------------------------------------------------------- 1 | _____ _ _ _ 2 | | ___| _ ___(_)_ __ __ _ / \ _ __ __ _ _ _| | __ _ _ __ 3 | | |_ | | | / __| | '_ \ / _` | / _ \ | '_ \ / _` | | | | |/ _` | '__| 4 | | _|| |_| \__ \ | | | | (_| | / ___ \| | | | (_| | |_| | | (_| | | 5 | |_| \__,_|___/_|_| |_|\__, | /_/ \_\_| |_|\__, |\__,_|_|\__,_|_| 6 | |___/ |___/ 7 | -------------------------------------------------------------------------------- /src/commands/build.ts: -------------------------------------------------------------------------------- 1 | import { command } from 'yargs' 2 | import { logInfo } from '../utilities/log' 3 | import { serve } from './serve' 4 | 5 | command( 6 | 'build [prod][sw]', 7 | 'build your application', 8 | args => { 9 | return args 10 | }, 11 | args => { 12 | logInfo('Launching Init Command') 13 | serve(args.prod, args.sw, true) 14 | } 15 | ) 16 | .option('prod', { 17 | default: false, 18 | description: 'Run with optimizations enabled' 19 | }) 20 | .option('sw', { 21 | default: false, 22 | description: 'Enable service-worker' 23 | }) 24 | -------------------------------------------------------------------------------- /src/commands/config.ts: -------------------------------------------------------------------------------- 1 | import { command } from 'yargs' 2 | import { take, tap } from 'rxjs/operators' 3 | import { 4 | logInfoWithBackground, 5 | logPrettyJson, 6 | logError 7 | } from '../utilities/log' 8 | import readConfig_ from '../utilities/read-config' 9 | 10 | command( 11 | 'config', 12 | 'show CLI configuration', 13 | args => { 14 | return args 15 | }, 16 | args => { 17 | config() 18 | } 19 | ) 20 | 21 | function displayMessage() { 22 | logInfoWithBackground('Viewing CLI configuration\n') 23 | } 24 | 25 | function config() { 26 | readConfig_() 27 | .pipe( 28 | tap(displayMessage), 29 | take(1) 30 | ) 31 | .subscribe(logPrettyJson, logError) 32 | } 33 | -------------------------------------------------------------------------------- /src/commands/create-common.ts: -------------------------------------------------------------------------------- 1 | import { Subject } from 'rxjs' 2 | 3 | export enum IDE { 4 | VISUAL_STUDIO_CODE = 'Visual Studio Code', 5 | OTHER = 'Other' 6 | } 7 | 8 | export interface QustionResponse { 9 | readonly name: string 10 | readonly answer: string | boolean 11 | } 12 | 13 | export interface AnswersDictionary { 14 | readonly fullname: string 15 | readonly shortname?: string 16 | readonly ide?: IDE 17 | readonly firebase?: boolean 18 | readonly firebaseApiKey?: string 19 | readonly firebaseAuthDomain?: string 20 | readonly firebaseDatabaseUrl?: string 21 | readonly firebaseProjectId?: string 22 | readonly firebaseStorageBucket?: string 23 | readonly firebaseMesssagingSenderId?: string 24 | readonly firebaseModules?: ReadonlyArray 25 | readonly googleAnalyticsTrackingId?: string 26 | readonly googleSiteVerificationCode?: string 27 | } 28 | 29 | export interface WorkingAnswersDictionary extends AnswersDictionary { 30 | readonly [key: string]: any 31 | } 32 | 33 | export interface QuestionWrapper { 34 | readonly question: { 35 | readonly name: string 36 | readonly message: string 37 | readonly default: string 38 | } 39 | readonly answerHandler: ( 40 | response: QustionResponse, 41 | current: WorkingAnswersDictionary, 42 | stream: Subject 43 | ) => void 44 | } 45 | 46 | export interface FirebaseConfig { 47 | readonly apiKey?: string 48 | readonly authDomain?: string 49 | readonly databaseUrl?: string 50 | readonly projectId?: string 51 | readonly storageBucket?: string 52 | readonly messagingSenderId?: string 53 | } 54 | -------------------------------------------------------------------------------- /src/commands/create-firebase.ts: -------------------------------------------------------------------------------- 1 | import { QustionResponse, WorkingAnswersDictionary } from './create-common' 2 | import { Subject } from 'rxjs' 3 | 4 | export const Q_INCLUDE_FIREBASE = { 5 | question: { 6 | type: 'confirm', 7 | name: 'firebase', 8 | message: 'Are you using Firebase?', 9 | default: false 10 | }, 11 | answerHandler: ( 12 | response: QustionResponse, 13 | current: WorkingAnswersDictionary, 14 | stream: Subject 15 | ) => { 16 | current.firebase 17 | ? stream.next(Q_FIREBASE_CONFIG_API_KEY.question) 18 | : stream.complete() 19 | } 20 | } 21 | 22 | export const Q_FIREBASE_CONFIG_API_KEY = { 23 | question: { 24 | type: 'input', 25 | name: 'firebaseApiKey', 26 | message: '\t[Firebase API Key]:' 27 | }, 28 | answerHandler: ( 29 | response: QustionResponse, 30 | current: WorkingAnswersDictionary, 31 | stream: Subject 32 | ) => { 33 | stream.next(Q_FIREBASE_CONFIG_AUTH_DOMAIN.question) 34 | } 35 | } 36 | 37 | export const Q_FIREBASE_CONFIG_AUTH_DOMAIN = { 38 | question: { 39 | type: 'input', 40 | name: 'firebaseAuthDomain', 41 | message: '\t[Firebase Auth Domain]:' 42 | }, 43 | answerHandler: ( 44 | response: QustionResponse, 45 | current: WorkingAnswersDictionary, 46 | stream: Subject 47 | ) => { 48 | stream.next(Q_FIREBASE_CONFIG_DATABASE_URL.question) 49 | } 50 | } 51 | 52 | export const Q_FIREBASE_CONFIG_DATABASE_URL = { 53 | question: { 54 | type: 'input', 55 | name: 'firebaseDatabaseUrl', 56 | message: '\t[Firebase Database URL]:' 57 | }, 58 | answerHandler: ( 59 | response: QustionResponse, 60 | current: WorkingAnswersDictionary, 61 | stream: Subject 62 | ) => { 63 | stream.next(Q_FIREBASE_CONFIG_PROJECT_ID.question) 64 | } 65 | } 66 | 67 | export const Q_FIREBASE_CONFIG_PROJECT_ID = { 68 | question: { 69 | type: 'input', 70 | name: 'firebaseProjectId', 71 | message: '\t[Firebase Project ID]:' 72 | }, 73 | answerHandler: ( 74 | response: QustionResponse, 75 | current: WorkingAnswersDictionary, 76 | stream: Subject 77 | ) => { 78 | stream.next(Q_FIREBASE_CONFIG_STORAGE_BUCKET.question) 79 | } 80 | } 81 | 82 | export const Q_FIREBASE_CONFIG_STORAGE_BUCKET = { 83 | question: { 84 | type: 'input', 85 | name: 'firebaseStorageBucket', 86 | message: '\t[Firebase Storage Bucket]:' 87 | }, 88 | answerHandler: ( 89 | response: QustionResponse, 90 | current: WorkingAnswersDictionary, 91 | stream: Subject 92 | ) => { 93 | stream.next(Q_FIREBASE_CONFIG_MESSAGING_SENDER_ID.question) 94 | } 95 | } 96 | 97 | export const Q_FIREBASE_CONFIG_MESSAGING_SENDER_ID = { 98 | question: { 99 | type: 'input', 100 | name: 'firebaseMesssagingSenderId', 101 | message: '\t[Firebase Messaging Sender ID]:' 102 | }, 103 | answerHandler: ( 104 | response: QustionResponse, 105 | current: WorkingAnswersDictionary, 106 | stream: Subject 107 | ) => { 108 | stream.next(Q_FIREBASE_CHOICES.question) 109 | } 110 | } 111 | 112 | export const Q_FIREBASE_CHOICES = { 113 | question: { 114 | type: 'checkbox', 115 | name: 'firebaseConfig', 116 | message: 'Which modules of Firebase to include?', 117 | choices: [ 118 | { 119 | name: 'Reat Time Database (RTDB)', 120 | value: 'rtdb', 121 | checked: true 122 | }, 123 | { 124 | name: 'Firestore', 125 | value: 'firestore', 126 | checked: true 127 | }, 128 | { 129 | name: 'Auth', 130 | value: 'auth', 131 | checked: false 132 | } 133 | ] 134 | }, 135 | answerHandler: ( 136 | response: QustionResponse, 137 | current: WorkingAnswersDictionary, 138 | stream: Subject 139 | ) => { 140 | stream.complete() 141 | } 142 | } 143 | 144 | export const Q_FIREBASE: ReadonlyArray = [ 145 | Q_INCLUDE_FIREBASE, 146 | Q_FIREBASE_CONFIG_API_KEY, 147 | Q_FIREBASE_CONFIG_AUTH_DOMAIN, 148 | Q_FIREBASE_CONFIG_DATABASE_URL, 149 | Q_FIREBASE_CONFIG_PROJECT_ID, 150 | Q_FIREBASE_CONFIG_STORAGE_BUCKET, 151 | Q_FIREBASE_CONFIG_MESSAGING_SENDER_ID, 152 | Q_FIREBASE_CHOICES 153 | ] 154 | -------------------------------------------------------------------------------- /src/commands/create-ui-questions.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patrickmichalina/fusing-angular-cli/f9331d3d94dbc14d8e0d21ace554af5bbd7ae7e9/src/commands/create-ui-questions.ts -------------------------------------------------------------------------------- /src/commands/create.ts: -------------------------------------------------------------------------------- 1 | import { prompt } from 'inquirer' 2 | import { command } from 'yargs' 3 | import { 4 | Subject, 5 | BehaviorSubject, 6 | of, 7 | forkJoin, 8 | Observable, 9 | Observer 10 | } from 'rxjs' 11 | import { 12 | startWith, 13 | shareReplay, 14 | first, 15 | map, 16 | tap, 17 | flatMap, 18 | filter, 19 | take 20 | } from 'rxjs/operators' 21 | import { log, logError, logInfoWithBackground } from '../utilities/log' 22 | import { pathExists_, mkDirAndContinueIfExists_ } from '../utilities/rx-fs' 23 | import { resolve } from 'path' 24 | import { generateCoreAngular } from '../generators/angular-core.gen' 25 | import generateGitIgnore from '../generators/gitignore.gen' 26 | import generateTsLint from '../generators/tslint.gen' 27 | import generateFngConfig from '../generators/config.gen' 28 | import clearTerminal from '../utilities/clear' 29 | import generatePackageFile from '../generators/package.gen' 30 | import { 31 | ANGULAR_UNIVERSAL_DEPS, 32 | ANGULAR_UNIVERSAL_EXPRESS_DEPS, 33 | ANGULAR_CORE_DEV_DEPS, 34 | ANGULAR_UNIVERSAL_DEV_DEPS 35 | } from '../generators/deps.const' 36 | import { load, commands } from 'npm' 37 | import generateTsConfig from '../generators/tsconfig.gen' 38 | import generateTsDeclartionFile from '../generators/declarations.gen' 39 | import generateDotEnv from '../generators/env.gen' 40 | import generateIdeStubs from '../generators/ide.gen' 41 | import { 42 | QuestionWrapper, 43 | QustionResponse, 44 | WorkingAnswersDictionary, 45 | AnswersDictionary, 46 | FirebaseConfig 47 | } from './create-common' 48 | import { Q_INCLUDE_FIREBASE, Q_FIREBASE } from './create-firebase' 49 | import { favicon_ } from './favicon' 50 | 51 | command( 52 | 'create [overwrite]', 53 | 'create a new application', 54 | args => args, 55 | args => { 56 | const force = args.o || false 57 | create(force) 58 | } 59 | ).option('overwrite', { 60 | alias: 'o', 61 | description: 'Overwrite existing application folder' 62 | }) 63 | 64 | const Q_FULL_NAME: QuestionWrapper = { 65 | question: { 66 | name: 'fullname', 67 | message: 'App Full Name:', 68 | default: 'fusing-angular-demo-app' 69 | }, 70 | answerHandler: ( 71 | response: QustionResponse, 72 | current: WorkingAnswersDictionary, 73 | stream: Subject 74 | ) => { 75 | stream.next(Q_SHORT_NAME.question) 76 | } 77 | } 78 | 79 | const Q_SHORT_NAME = { 80 | question: { 81 | name: 'shortname', 82 | message: 'App Short Name:', 83 | default: 'fusing-ng' 84 | }, 85 | answerHandler: ( 86 | response: QustionResponse, 87 | current: WorkingAnswersDictionary, 88 | stream: Subject 89 | ) => { 90 | stream.next(Q_GOOGLE_ANALYTICS_TRACKING_ID.question) 91 | } 92 | } 93 | 94 | const Q_GOOGLE_ANALYTICS_TRACKING_ID = { 95 | question: { 96 | name: 'googleAnalyticsTrackingId', 97 | message: 'Google Analytics Tracking ID (Optional):' 98 | }, 99 | answerHandler: ( 100 | response: QustionResponse, 101 | current: WorkingAnswersDictionary, 102 | stream: Subject 103 | ) => { 104 | stream.next(Q_GOOGLE_SITE_VERIFICATION_CODE.question) 105 | } 106 | } 107 | 108 | const Q_GOOGLE_SITE_VERIFICATION_CODE = { 109 | question: { 110 | name: 'googleSiteVerificationCode', 111 | message: 'Google Site Verification Code (Optional):' 112 | }, 113 | answerHandler: ( 114 | response: QustionResponse, 115 | current: WorkingAnswersDictionary, 116 | stream: Subject 117 | ) => { 118 | stream.next(Q_IDE.question) 119 | } 120 | } 121 | 122 | const Q_IDE = { 123 | question: { 124 | type: 'list', 125 | name: 'ide', 126 | message: 'Which IDE are you using?', 127 | default: 'Visual Studio Code', 128 | choices: ['Visual Studio Code', 'Other'] 129 | }, 130 | answerHandler: ( 131 | response: QustionResponse, 132 | current: WorkingAnswersDictionary, 133 | stream: Subject 134 | ) => { 135 | stream.next(Q_INCLUDE_FIREBASE.question) 136 | } 137 | } 138 | 139 | const Q_TEST_RUNNERS = { 140 | question: { 141 | type: 'list', 142 | name: 'test-runner', 143 | message: 'Application Short Name', 144 | choices: [{ name: 'Jest', value: 'jest' }, { name: 'None' }] 145 | }, 146 | answerHandler: ( 147 | response: QustionResponse, 148 | current: WorkingAnswersDictionary, 149 | stream: Subject 150 | ) => { 151 | stream.complete() 152 | } 153 | } 154 | 155 | const QUESTION_DICT = [ 156 | Q_FULL_NAME, 157 | Q_SHORT_NAME, 158 | Q_TEST_RUNNERS, 159 | Q_IDE, 160 | Q_GOOGLE_ANALYTICS_TRACKING_ID, 161 | Q_GOOGLE_SITE_VERIFICATION_CODE, 162 | ...Q_FIREBASE 163 | ].reduce( 164 | (acc, curr) => { 165 | return { ...acc, [curr.question.name]: curr } 166 | }, 167 | {} as { readonly [key: string]: QuestionWrapper } 168 | ) 169 | 170 | const source = new Subject() 171 | const finalConfigSource = new Subject() 172 | const collector = new BehaviorSubject({} as any) 173 | const prompts = source.pipe( 174 | startWith(Q_FULL_NAME.question), 175 | shareReplay() 176 | ) 177 | const finalConfig_ = finalConfigSource.pipe( 178 | map(() => collector.getValue()), 179 | first() 180 | ) 181 | 182 | interface IntermediateModel { 183 | readonly config: AnswersDictionary 184 | readonly shouldTerminate: boolean 185 | } 186 | 187 | function displayGeneratingAppText() { 188 | logInfoWithBackground('Generating App....\n') 189 | } 190 | 191 | function displayWarningApplicationAlreadyExists(res: IntermediateModel) { 192 | res.shouldTerminate && logError('Application already exists. exiting') 193 | } 194 | 195 | function mapWorkingAnswersToFinal(config: WorkingAnswersDictionary) { 196 | return { 197 | ...config 198 | } as AnswersDictionary 199 | } 200 | 201 | function checkConfigAndCanProceed(res: IntermediateModel): boolean { 202 | return !res.shouldTerminate 203 | } 204 | 205 | function toEnsureProjectDirectoryExists(mdl: IntermediateModel) { 206 | return mkDirAndContinueIfExists_(resolve(mdl.config.fullname)) 207 | } 208 | 209 | function checkIfProjectPathExists(overwrite: boolean) { 210 | return function(config: AnswersDictionary) { 211 | return overwrite ? of(false) : pathExists_(config.fullname) 212 | } 213 | } 214 | 215 | function test(name: string) { 216 | return function() { 217 | return npmInstall(name) 218 | } 219 | } 220 | 221 | function projectPathCheckToIntermediateModel( 222 | config: AnswersDictionary, 223 | shouldTerminate: boolean 224 | ) { 225 | return { 226 | config, 227 | shouldTerminate 228 | } 229 | } 230 | 231 | function genNpmPackageJson( 232 | name: string, 233 | isUniversalApp: boolean, 234 | overwrite = false 235 | ) { 236 | return generatePackageFile( 237 | { 238 | name, 239 | dependencies: { 240 | ...((isUniversalApp && ANGULAR_UNIVERSAL_DEPS) || {}), 241 | ...((isUniversalApp && ANGULAR_UNIVERSAL_EXPRESS_DEPS) || {}) 242 | }, 243 | devDependencies: { 244 | ...ANGULAR_CORE_DEV_DEPS, 245 | ...((isUniversalApp && ANGULAR_UNIVERSAL_DEV_DEPS) || {}) 246 | } 247 | }, 248 | overwrite, 249 | name 250 | ) 251 | } 252 | 253 | function npmInstall(name: string) { 254 | return Observable.create((obs: Observer) => { 255 | load( 256 | { 257 | global: false, 258 | prefix: name 259 | }, 260 | (err, npm) => { 261 | // tslint:disable-next-line:no-if-statement 262 | if (err) { 263 | logError(err.message) 264 | obs.error(err) 265 | obs.complete() 266 | } else { 267 | commands.install([name], err => { 268 | // tslint:disable-next-line:no-if-statement 269 | if (err) { 270 | logError(err.message) 271 | obs.error(err) 272 | obs.complete() 273 | } else { 274 | obs.complete() 275 | } 276 | }) 277 | } 278 | } 279 | ) 280 | }) 281 | } 282 | 283 | function create(overwriteExisting = false) { 284 | log('Create an Angular application\n') 285 | const prm = prompt(prompts as any) as any 286 | prm.ui.process.subscribe( 287 | function(response: QustionResponse) { 288 | const merged = { 289 | ...collector.getValue(), 290 | ...Object.keys(response).reduce(acc => { 291 | return { 292 | ...acc, 293 | [response.name]: response.answer 294 | } 295 | }, {}) 296 | } 297 | collector.next(merged) 298 | // TODO: haneld edge case of task not existing in dict 299 | QUESTION_DICT[response.name].answerHandler(response, merged, source) 300 | }, 301 | logError, 302 | () => finalConfigSource.next() 303 | ) 304 | 305 | // Once we have our final configuration for the app, lets go through the build steps 306 | finalConfig_ 307 | .pipe( 308 | tap(clearTerminal), 309 | tap(displayGeneratingAppText), 310 | map(mapWorkingAnswersToFinal), 311 | flatMap( 312 | checkIfProjectPathExists(overwriteExisting), 313 | projectPathCheckToIntermediateModel 314 | ), 315 | tap(displayWarningApplicationAlreadyExists), 316 | filter(checkConfigAndCanProceed), 317 | flatMap(toEnsureProjectDirectoryExists, im => im), 318 | flatMap(im => { 319 | const path = resolve(im.config.fullname) 320 | const faviOverrides = { 321 | appName: im.config.fullname, 322 | appShortName: im.config.shortname 323 | } 324 | const firebaseConfig: FirebaseConfig | undefined = im.config.firebase 325 | ? { 326 | apiKey: im.config.firebaseApiKey, 327 | authDomain: im.config.firebaseAuthDomain, 328 | databaseUrl: im.config.firebaseDatabaseUrl, 329 | messagingSenderId: im.config.firebaseMesssagingSenderId, 330 | projectId: im.config.firebaseProjectId, 331 | storageBucket: im.config.firebaseStorageBucket 332 | } 333 | : undefined 334 | return generateFngConfig(path, overwriteExisting, faviOverrides).pipe( 335 | flatMap(() => generateCoreAngular(im.config.fullname)), 336 | flatMap(() => 337 | forkJoin([ 338 | favicon_(path), 339 | generateGitIgnore(path, overwriteExisting), 340 | generateTsLint(path, overwriteExisting), 341 | generateDotEnv( 342 | path, 343 | overwriteExisting, 344 | firebaseConfig, 345 | im.config.googleAnalyticsTrackingId, 346 | im.config.googleSiteVerificationCode 347 | ), 348 | generateTsConfig(path, overwriteExisting), 349 | generateTsDeclartionFile(path, overwriteExisting), 350 | genNpmPackageJson( 351 | im.config.fullname, 352 | true, 353 | overwriteExisting 354 | ).pipe(flatMap(test(im.config.fullname))), 355 | im.config.ide 356 | ? generateIdeStubs(im.config.ide, path, overwriteExisting) 357 | : of(undefined) 358 | ]) 359 | ) 360 | ) 361 | }, im => im), 362 | take(1) 363 | ) 364 | .subscribe( 365 | res => { 366 | require('simple-git')() 367 | .init() 368 | .add('./*') 369 | .commit('init') 370 | }, 371 | err => { 372 | console.error(err) 373 | process.exit(1) 374 | } 375 | ) 376 | 377 | // prompt([ 378 | // // { 379 | // // type: 'list', 380 | // // name: 'test-runner', 381 | // // message: 'E2E Test Runner', 382 | // // choices: [ 383 | // // { 384 | // // name: 'Nightmare', 385 | // // value: 'nightmare', 386 | // // checked: true 387 | // // }, 388 | // // { 389 | // // name: 'None' 390 | // // } 391 | // // ] 392 | // // }, 393 | // // { 394 | // // type: 'list', 395 | // // name: 'deployments', 396 | // // message: 'Deployment Infrastructure', 397 | // // choices: [ 398 | // // { 399 | // // name: 'Heroku', 400 | // // value: 'heroku', 401 | // // checked: true 402 | // // }, 403 | // // { 404 | // // name: 'AWS Serverless', 405 | // // value: 'aws', 406 | // // disabled: 'in development' 407 | // // }, 408 | // // { 409 | // // name: 'Google Cloud Serverless', 410 | // // value: 'gcloud', 411 | // // disabled: 'in development' 412 | // // }, 413 | // // { 414 | // // name: 'None', 415 | // // value: 'none' 416 | // // } 417 | // // ] 418 | // // }, 419 | // // { 420 | // // type: 'checkbox', 421 | // // name: 'ide', 422 | // // message: 'IDE Configurations', 423 | // // choices: [ 424 | // // { 425 | // // name: 'Visual Studio Code', 426 | // // value: 'vscode', 427 | // // checked: true 428 | // // }, 429 | // // { 430 | // // name: 'Webstorm', 431 | // // value: 'webstorm', 432 | // // disabled: 'unavailable, in development' 433 | // // } 434 | // // ] 435 | // // }, 436 | // // { 437 | // // name: 'ga', 438 | // // message: 'Include Google Analytics?', 439 | // // type: 'expand', 440 | // // choices: [ 441 | // // { 442 | // // name: 'Yes', 443 | // // value: 'true', 444 | // // key: 'y' 445 | // // }, 446 | // // { 447 | // // key: 'n', 448 | // // name: 'No', 449 | // // value: 'false' 450 | // // }, 451 | // // ] 452 | // // }, 453 | // // { 454 | // // type: 'list', 455 | // // name: 'ui-lib', 456 | // // message: 'UI Library', 457 | // // choices: [ 458 | // // { 459 | // // name: 'Angular Material', 460 | // // value: 'material', 461 | // // checked: false 462 | // // }, 463 | // // { 464 | // // name: 'Bootstrap', 465 | // // value: 'bootstrap', 466 | // // checked: false 467 | // // }, 468 | // // { 469 | // // name: 'Bulma', 470 | // // value: 'bulma', 471 | // // checked: false 472 | // // }, 473 | // // { 474 | // // name: 'None', 475 | // // value: 'none', 476 | // // checked: true 477 | // // } 478 | // // ] 479 | // // }, 480 | // // { 481 | // // type: 'checkbox', 482 | // // name: 'build', 483 | // // message: 'Features', 484 | // // choices: [ 485 | // // { 486 | // // name: 'Enable Progressive Web App (PWA)', 487 | // // value: 'pwa', 488 | // // checked: false 489 | // // } 490 | // // ] 491 | // // }, 492 | // // { 493 | // // type: 'checkbox', 494 | // // name: 'packages', 495 | // // message: 'Additional Packages', 496 | // // choices: [ 497 | // // { 498 | // // name: 'Angular Flex-Layout', 499 | // // value: 'flex-layout', 500 | // // checked: false 501 | // // }, 502 | // // { 503 | // // name: 'Firebase', 504 | // // value: 'firebase', 505 | // // checked: false 506 | // // }, 507 | // // { 508 | // // name: 'Angularytics2', 509 | // // value: 'angularytics2', 510 | // // checked: false 511 | // // } 512 | // // ] 513 | // // } 514 | // ]) 515 | } 516 | -------------------------------------------------------------------------------- /src/commands/deps.ts: -------------------------------------------------------------------------------- 1 | // import { log } from '../utilities/log' 2 | // import * as pkg from '../../package.json' 3 | 4 | // function createTableString() { 5 | // const deps = pkg.dependencies as { readonly [key: string]: string } 6 | // const depsOfNote: ReadonlyArray = [ 7 | // 'fuse-box', 8 | // 'rxjs', 9 | // 'ts-node', 10 | // 'typesciprt' 11 | // ] 12 | // const rows = Object.keys(deps) 13 | // .filter(k => depsOfNote.some(b => b === k)) 14 | // .map(k => { 15 | // return [k, deps[k]] 16 | // }) 17 | // return rows 18 | // } 19 | 20 | // export default function() { 21 | // log('Dependencies') 22 | // log(createTableString()) 23 | // } 24 | -------------------------------------------------------------------------------- /src/commands/favicon.ts: -------------------------------------------------------------------------------- 1 | import { command } from 'yargs' 2 | import { logInfoWithBackground, logError } from '../utilities/log' 3 | import { flatMap, map, filter, tap, take, first } from 'rxjs/operators' 4 | import { rxFavicons } from '../utilities/rx-favicon' 5 | import { 6 | writeFile_, 7 | writeJsonFile_, 8 | mkDirAndContinueIfExists_ 9 | } from '../utilities/rx-fs' 10 | import { resolve } from 'path' 11 | import { forkJoin } from 'rxjs' 12 | import readConfig_, { 13 | FaviconConfig, 14 | FusingAngularConfig 15 | } from '../utilities/read-config' 16 | import { FavIconResponse } from 'favicons' 17 | 18 | command( 19 | 'favicon', 20 | 'generate favicons', 21 | args => { 22 | return args 23 | }, 24 | args => { 25 | favicon_('.') 26 | .pipe( 27 | first(), 28 | take(1) 29 | ) 30 | .subscribe(logFaviconComplete, logError) 31 | } 32 | ) 33 | 34 | function requireFaviconConfig(config: FusingAngularConfig) { 35 | return config && config.favicon 36 | ? true 37 | : (() => { 38 | throw new Error('Favicon configuration required.') 39 | })() 40 | } 41 | 42 | function mapFaviconConfig(config: FusingAngularConfig) { 43 | return config && config.favicon 44 | } 45 | 46 | function logFaviconStart() { 47 | logInfoWithBackground('Generating Favicons...') 48 | } 49 | 50 | function logDirectoryCheck() { 51 | logInfoWithBackground('Creating favicons directories...') 52 | } 53 | 54 | function logFaviconComplete() { 55 | logInfoWithBackground('Favicon generation complete!') 56 | } 57 | 58 | interface configModel { 59 | readonly config: FaviconConfig 60 | readonly result: FavIconResponse 61 | } 62 | 63 | function mapResponsesToWriteableObservables(baseDir = '') { 64 | return function(response: configModel) { 65 | return readConfig_(baseDir).pipe( 66 | map(config => { 67 | return { 68 | ...config, 69 | generatedMetaTags: response.result.html 70 | } 71 | }), 72 | flatMap(config => 73 | writeJsonFile_(resolve(baseDir, 'fusing-angular.json'), config, true) 74 | ), 75 | flatMap(() => 76 | mkDirAndContinueIfExists_(resolve(baseDir, `${response.config.output}`)) 77 | ), 78 | flatMap(() => { 79 | return forkJoin([ 80 | ...response.result.files.map(file => 81 | writeFile_( 82 | resolve(baseDir, `${response.config.output}/${file.name}`), 83 | file.contents 84 | ) 85 | ), 86 | ...response.result.images.map(file => 87 | writeFile_( 88 | resolve(baseDir, `${response.config.output}/${file.name}`), 89 | file.contents 90 | ) 91 | ) 92 | ]) 93 | }) 94 | ) 95 | } 96 | } 97 | 98 | export function favicon_(path?: string) { 99 | return readConfig_(path).pipe( 100 | tap(logFaviconStart), 101 | filter(requireFaviconConfig), 102 | map(mapFaviconConfig), 103 | flatMap(rxFavicons(path), (config: FaviconConfig, result) => ({ 104 | config, 105 | result 106 | })), 107 | tap(logDirectoryCheck), 108 | // flatMap( 109 | // response => mkDirDeep_(resolve(path || '', response.config.output)), 110 | // response => ({ ...response }) 111 | // ), 112 | flatMap(mapResponsesToWriteableObservables(path)) 113 | ) 114 | } 115 | -------------------------------------------------------------------------------- /src/commands/index.ts: -------------------------------------------------------------------------------- 1 | import './create' 2 | import './build' 3 | import './serve' 4 | import './test' 5 | import './lint' 6 | import './favicon' 7 | import './config' 8 | import './update' 9 | import { argv } from 'yargs' 10 | 11 | argv 12 | -------------------------------------------------------------------------------- /src/commands/lint.ts: -------------------------------------------------------------------------------- 1 | import { command } from 'yargs' 2 | import { logInfo, log, logError } from '../utilities/log' 3 | import { Linter, Configuration, ILinterOptions } from 'tslint' 4 | import { resolve } from 'path' 5 | import { readFile_ } from '../utilities/rx-fs' 6 | import { take } from 'rxjs/operators' 7 | import chalk from 'chalk' 8 | 9 | command( 10 | 'lint', 11 | 'check your code quality', 12 | args => { 13 | return args 14 | }, 15 | args => { 16 | lint() 17 | } 18 | ) 19 | 20 | function warnings(count: number) { 21 | const str = count.toString() 22 | return count > 0 23 | ? chalk.underline(chalk.bgYellow(` ${str} `)) 24 | : chalk.underline(` ${str} `) 25 | } 26 | 27 | function errors(count: number) { 28 | const str = count.toString() 29 | return count > 0 30 | ? chalk.underline(chalk.bgRed(` ${str} `)) 31 | : chalk.underline(` ${str} `) 32 | } 33 | 34 | function showErrorLoadingProject() { 35 | logError('Could not load entry file, are you in a project directory?') 36 | } 37 | 38 | function lint() { 39 | const fileName = resolve('src/server/server.ts') 40 | const configurationFilename = resolve('tslint.json') 41 | const options: ILinterOptions = { 42 | fix: false, 43 | formatter: 'json' 44 | } 45 | 46 | readFile_(fileName) 47 | .pipe(take(1)) 48 | .subscribe(fileContents => { 49 | const linter = new Linter(options) 50 | const configuration = Configuration.findConfiguration( 51 | configurationFilename, 52 | fileName 53 | ).results 54 | linter.lint(fileName, fileContents.toString(), configuration) 55 | 56 | logInfo('Linter\n') 57 | const result = linter.getResult() 58 | log(` Errors: `, errors(result.errorCount)) 59 | log(`Warnings: `, warnings(result.warningCount), '\n') 60 | 61 | result.failures.map(obj => obj.toJson()).forEach(json => { 62 | log( 63 | `${json.ruleSeverity}(${json.ruleName}): ${json.name} (${ 64 | json.startPosition.line 65 | }, ${json.startPosition.character}): ${json.failure}` 66 | ) 67 | }) 68 | 69 | log('\n') 70 | }, showErrorLoadingProject) 71 | } 72 | -------------------------------------------------------------------------------- /src/commands/serve.ts: -------------------------------------------------------------------------------- 1 | import { command } from 'yargs' 2 | import { take, tap } from 'rxjs/operators' 3 | import { logInfo } from '../utilities/log' 4 | import { 5 | FuseBox, 6 | JSONPlugin, 7 | QuantumPlugin, 8 | RawPlugin, 9 | SassPlugin, 10 | EnvPlugin, 11 | WebIndexPlugin, 12 | UglifyJSPlugin 13 | } from 'fuse-box' 14 | import { resolve } from 'path' 15 | import { NgProdPlugin } from '../fusebox/ng.prod.plugin' 16 | import { NgPolyfillPlugin } from '../fusebox/ng.polyfill.plugin' 17 | import { Ng2TemplatePlugin } from 'ng2-fused' 18 | import { FuseProcess } from 'fuse-box/FuseProcess' 19 | import { NgAotFactoryPlugin } from '../fusebox/ng.aot-factory.plugin' 20 | import { main as ngc } from '@angular/compiler-cli/src/main' 21 | import { CompressionPlugin } from '../fusebox/compression.plugin' 22 | import { appEnvironmentVariables } from '../utilities/environment-variables' 23 | import { renderSassDir } from '../utilities/sass' 24 | import { exec, execSync } from 'child_process' 25 | import { NgSwPlugin } from '../fusebox/ng.sw.plugin' 26 | import { copy } from 'fs-extra' 27 | import { NgAotRelativePlugin } from '../fusebox/ng.aot-relative.plugin' 28 | import clearTerminal from '../utilities/clear' 29 | import readConfig_ from '../utilities/read-config' 30 | 31 | command( 32 | 'serve [port][prod][sw]', 33 | 'serve your application', 34 | args => { 35 | return args 36 | }, 37 | args => { 38 | serve(args.prod, args.sw) 39 | } 40 | ) 41 | .option('prod', { 42 | default: false, 43 | description: 'Run with optimizations enabled' 44 | }) 45 | .option('sw', { 46 | default: false, 47 | description: 'Enable service-worker' 48 | }) 49 | .option('port', { 50 | default: 5000, 51 | description: 'Http server port number' 52 | }) 53 | 54 | function logServeCommandStart() { 55 | logInfo('Launching Serve Command') 56 | } 57 | 58 | export function serve( 59 | isProdBuild = false, 60 | isServiceWorkerEnabled = false, 61 | buildOnly = false 62 | ) { 63 | readConfig_() 64 | .pipe( 65 | tap(logServeCommandStart), 66 | take(1) 67 | ) 68 | .subscribe(config => { 69 | const cache = !isProdBuild 70 | const isAotBuild = isProdBuild 71 | const isLocalDev = !isProdBuild 72 | const log = config.fusebox.verbose || false 73 | const homeDir = resolve('.') 74 | const serverOutput = resolve(config.fusebox.server.outputDir) 75 | const browserOutput = resolve(config.fusebox.browser.outputDir) 76 | const modulesFolder = resolve(process.cwd(), 'node_modules') 77 | const watchDir = resolve(`${homeDir}/src/**`) 78 | const appName = 79 | (config.favicon && 80 | config.favicon.config && 81 | config.favicon.config.appName) || 82 | 'FUSING ANGULAR' 83 | const browserModule = isAotBuild 84 | ? config.fusebox.browser.aotBrowserModule 85 | : config.fusebox.browser.browserModule 86 | 87 | isAotBuild && ngc(['-p', resolve('tsconfig.aot.json')]) 88 | 89 | const fuseBrowser = FuseBox.init({ 90 | log, 91 | modulesFolder, 92 | homeDir, 93 | cache, 94 | hash: isProdBuild, 95 | output: `${browserOutput}/$name.js`, 96 | target: 'browser@es5', 97 | useTypescriptCompiler: true, 98 | plugins: [ 99 | isAotBuild && NgAotFactoryPlugin(), 100 | isAotBuild && 101 | NgAotRelativePlugin({ 102 | '"./not-found.component"': 'not-found/not-found.component', 103 | '"../response/browser.response.service"': 104 | 'response/browser.response.service' 105 | }), 106 | isServiceWorkerEnabled && NgSwPlugin(), 107 | Ng2TemplatePlugin(), 108 | ['*.component.html', RawPlugin()], 109 | WebIndexPlugin({ 110 | bundles: ['vendor', 'app'], 111 | path: 'js', 112 | target: '../index.html', 113 | template: resolve('src/app/index.pug'), 114 | engine: 'pug', 115 | locals: { 116 | pageTitle: appName, 117 | isLocalDev, 118 | faviconMeta: (config.generatedMetaTags || []).join('\n') 119 | } 120 | }), 121 | NgProdPlugin({ enabled: isProdBuild }), 122 | NgPolyfillPlugin(), 123 | [ 124 | '*.component.css', 125 | SassPlugin({ 126 | indentedSyntax: false, 127 | importer: true, 128 | sourceMap: false, 129 | outputStyle: 'compressed' 130 | } as any), 131 | RawPlugin() 132 | ], 133 | isProdBuild && 134 | QuantumPlugin({ 135 | warnings: false, 136 | uglify: config.fusebox.browser.prod.uglify, 137 | treeshake: config.fusebox.browser.prod.treeshake, 138 | bakeApiIntoBundle: 'vendor' 139 | }), 140 | CompressionPlugin() 141 | ] as any 142 | }) 143 | 144 | const fuseServer = FuseBox.init({ 145 | log, 146 | modulesFolder, 147 | target: 'server@es5', 148 | cache, 149 | homeDir, 150 | output: `${serverOutput}/$name.js`, 151 | plugins: [ 152 | EnvPlugin({ 153 | FUSING_ANGULAR: JSON.stringify(appEnvironmentVariables) 154 | }), 155 | JSONPlugin(), 156 | Ng2TemplatePlugin(), 157 | ['*.component.html', RawPlugin()], 158 | [ 159 | '*.component.css', 160 | SassPlugin({ 161 | indentedSyntax: false, 162 | importer: true, 163 | sourceMap: false, 164 | outputStyle: 'compressed' 165 | } as any), 166 | RawPlugin() 167 | ], 168 | NgProdPlugin({ 169 | enabled: true, 170 | fileTest: 'server.angular.module.ts' 171 | }), 172 | NgPolyfillPlugin({ 173 | isServer: true, 174 | fileTest: 'server.angular.module.ts' 175 | }) 176 | ] 177 | }) 178 | 179 | // tslint:disable-next-line:no-let 180 | let prevServerProcess: FuseProcess 181 | 182 | const fuseSw = FuseBox.init({ 183 | homeDir: resolve('node_modules/@angular/service-worker'), 184 | output: `${browserOutput}/$name.js`, 185 | target: 'browser@es5', 186 | plugins: [isProdBuild && UglifyJSPlugin(), CompressionPlugin()] as any 187 | }) 188 | fuseSw.bundle('ngsw-worker').instructions(' > [ngsw-worker.js]') 189 | 190 | // tslint:disable:no-if-statement 191 | const vendor = fuseBrowser.bundle('vendor') 192 | if (!buildOnly) vendor.watch(watchDir) 193 | vendor.instructions(` ~ ${browserModule}`).completed(fn => { 194 | isServiceWorkerEnabled && 195 | execSync( 196 | `node_modules/.bin/ngsw-config .dist/public src/app/ngsw.json` 197 | ) 198 | const serverBundle = fuseServer 199 | .bundle('server') 200 | .instructions(` > [${config.fusebox.server.serverModule}]`) 201 | 202 | if (!buildOnly) { 203 | serverBundle.completed(proc => { 204 | prevServerProcess && prevServerProcess.kill() 205 | clearTerminal() 206 | proc.start() 207 | prevServerProcess = proc 208 | }) 209 | } 210 | fuseServer.run() 211 | }) 212 | 213 | const appBundle = fuseBrowser.bundle('app') 214 | if (!buildOnly) appBundle.watch(watchDir) 215 | appBundle 216 | .instructions(` !> [${browserModule}]`) 217 | .splitConfig({ dest: '../js/modules' }) 218 | 219 | logInfo('Bundling your application, this may take some time...') 220 | 221 | renderSassDir() 222 | 223 | if (!buildOnly) { 224 | const sass = exec( 225 | 'node_modules/.bin/node-sass --watch src/**/*.scss --output-style compressed --output src/**' 226 | ) 227 | sass.on('error', err => { 228 | console.log(err) 229 | process.exit(1) 230 | }) 231 | sass.on('message', err => { 232 | console.error(err) 233 | process.exit(1) 234 | }) 235 | sass.stderr.on('data', err => { 236 | console.log(err) 237 | process.exit(1) 238 | }) 239 | } 240 | 241 | copy(resolve('src/assets'), resolve('.dist/public/assets')) 242 | .then(() => fuseSw.run()) 243 | .then(() => { 244 | fuseBrowser.run({ chokidar: { ignored: /^(.*\.scss$)*$/gim } }) 245 | }) 246 | .catch(() => { 247 | fuseSw.run().then(() => { 248 | fuseBrowser.run({ chokidar: { ignored: /^(.*\.scss$)*$/gim } }) 249 | }) 250 | }) 251 | }) 252 | } 253 | -------------------------------------------------------------------------------- /src/commands/test.ts: -------------------------------------------------------------------------------- 1 | import { command } from 'yargs' 2 | import { resolve } from 'path' 3 | const jest = require('jest') 4 | 5 | command( 6 | 'test [--watch] [--coverage]', 7 | 'run unit-tests on your project', 8 | args => { 9 | return args 10 | }, 11 | args => { 12 | const watch = args.watch || false 13 | const coverage = args.coverage || false 14 | test(watch, coverage) 15 | } 16 | ) 17 | .option('coverage', { 18 | default: false, 19 | description: 'report on code coverage' 20 | }) 21 | .option('watch', { 22 | default: false, 23 | description: 're-run tests when files change' 24 | }) 25 | 26 | function test(watchAll: boolean, coverage: boolean) { 27 | jest.runCLI( 28 | { 29 | watchAll, 30 | coverage, 31 | globals: JSON.stringify({ 32 | __TRANSFORM_HTML__: true, 33 | 'ts-jest': { 34 | tsConfigFile: resolve('tsconfig.json') 35 | } 36 | }), 37 | transform: JSON.stringify({ 38 | '^.+\\.(ts|js|html)$': resolve( 39 | 'node_modules/fusing-angular-cli/.build/jest/preprocessor.js' 40 | ) 41 | }), 42 | testMatch: [ 43 | '**/__tests__/**/*.+(ts|js)?(x)', 44 | '**/+(*.)+(spec|test).+(ts|js)?(x)' 45 | ], 46 | moduleFileExtensions: ['ts', 'js', 'html', 'json'], 47 | setupTestFrameworkScriptFile: resolve( 48 | 'node_modules/fusing-angular-cli/.build/jest/jest.setup.js' 49 | ), 50 | snapshotSerializers: [ 51 | resolve( 52 | 'node_modules/fusing-angular-cli/.build/jest/AngularSnapshotSerializer.js' 53 | ), 54 | resolve( 55 | 'node_modules/fusing-angular-cli/.build/jest/HTMLCommentSerializer.js' 56 | ) 57 | ], 58 | testResultsProcessor: resolve('node_modules/jest-junit-reporter'), 59 | collectCoverageFrom: [ 60 | 'src/**/*.{ts,html}', 61 | '!src/browser/app.browser.entry.aot.ts', 62 | '!src/browser/app.browser.entry.jit.ts' 63 | ] 64 | }, 65 | [resolve(__dirname, '../../')] 66 | ) 67 | } 68 | -------------------------------------------------------------------------------- /src/commands/update.ts: -------------------------------------------------------------------------------- 1 | import { command } from 'yargs' 2 | import { logInfo, logError } from '../utilities/log' 3 | import { load, commands } from 'npm' 4 | 5 | command( 6 | 'update', 7 | 'update the CLI to the latest version', 8 | args => { 9 | return args 10 | }, 11 | args => { 12 | update() 13 | } 14 | ) 15 | 16 | function update() { 17 | logInfo('Updating the CLI') 18 | load({ global: true }, () => { 19 | commands.install(['fusing-angular-cli@latest'], err => { 20 | err && logError(err) 21 | }) 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /src/fusebox/compression.plugin.ts: -------------------------------------------------------------------------------- 1 | import { Plugin, BundleProducer } from 'fuse-box' 2 | import { gzip, ZlibOptions } from 'zlib' 3 | import { compress } from 'iltorb' 4 | import { bindNodeCallback, forkJoin } from 'rxjs' 5 | import { Observable } from 'rxjs' 6 | import { readFile_ } from '../utilities/rx-fs' 7 | import { flatMap } from 'rxjs/operators' 8 | 9 | // tslint:disable:no-class 10 | // tslint:disable:no-this 11 | // tslint:disable:no-if-statement 12 | // tslint:disable:no-object-mutation 13 | // tslint:disable:readonly-keyword 14 | const defaults: CompressionPluginOptions = { 15 | enabled: true 16 | } 17 | 18 | export interface CompressionPluginOptions { 19 | enabled?: boolean 20 | fileTest?: string 21 | } 22 | 23 | function gzip_(buffer: Buffer, options?: ZlibOptions) { 24 | return (bindNodeCallback(gzip))(buffer, options) as Observable 25 | } 26 | 27 | function brotli_(buffer: Buffer) { 28 | return bindNodeCallback(compress)(buffer) 29 | } 30 | 31 | export class CompressionPluginClass implements Plugin { 32 | constructor(public opts: CompressionPluginOptions = defaults) { 33 | this.opts = { 34 | ...defaults, 35 | ...opts 36 | } 37 | } 38 | producerEnd?(producer: BundleProducer): any { 39 | return forkJoin( 40 | Array.from(producer.bundles) 41 | .map(bundle => bundle[1].context.output) 42 | .map(bundleOutput => { 43 | return readFile_(bundleOutput.lastWrittenPath).pipe( 44 | flatMap(file => { 45 | return forkJoin([ 46 | gzip_(file, { level: 9 }).pipe( 47 | flatMap(compressed => 48 | bundleOutput.writeToOutputFolder( 49 | `${bundleOutput.lastPrimaryOutput.relativePath}.gzip`, 50 | compressed 51 | ) 52 | ) 53 | ), 54 | brotli_(file).pipe( 55 | flatMap(compressed => 56 | bundleOutput.writeToOutputFolder( 57 | `${bundleOutput.lastPrimaryOutput.relativePath}.br`, 58 | compressed 59 | ) 60 | ) 61 | ) 62 | ]) 63 | }) 64 | ) 65 | }) 66 | ).toPromise() 67 | } 68 | } 69 | 70 | export const CompressionPlugin = (options?: CompressionPluginOptions) => 71 | new CompressionPluginClass(options) 72 | -------------------------------------------------------------------------------- /src/fusebox/ng.aot-factory.plugin.ts: -------------------------------------------------------------------------------- 1 | import { Plugin, File } from 'fuse-box' 2 | 3 | // tslint:disable:no-class 4 | // tslint:disable:no-this 5 | // tslint:disable:no-if-statement 6 | // tslint:disable:no-object-mutation 7 | // tslint:disable:readonly-keyword 8 | const defaults: NgAotFactoryPluginOptions = {} 9 | 10 | export interface NgAotFactoryPluginOptions {} 11 | 12 | export class NgAotFactoryPluginClass implements Plugin { 13 | constructor(public opts: NgAotFactoryPluginOptions = defaults) {} 14 | 15 | public test: RegExp = /.routing.module.js/ 16 | 17 | onTypescriptTransform?(file: File) { 18 | if (!this.test.test(file.relativePath)) return 19 | const regex1 = new RegExp(/.module'/, 'g') 20 | const regex2 = new RegExp(/Module\);/, 'g') 21 | file.contents = file.contents.replace(regex1, ".module.ngfactory'") 22 | file.contents = file.contents.replace(regex2, 'ModuleNgFactory);') 23 | } 24 | } 25 | 26 | export const NgAotFactoryPlugin = (options?: NgAotFactoryPluginOptions) => 27 | new NgAotFactoryPluginClass(options) 28 | -------------------------------------------------------------------------------- /src/fusebox/ng.aot-relative.plugin.ts: -------------------------------------------------------------------------------- 1 | import { Plugin, File } from 'fuse-box' 2 | 3 | // tslint:disable:no-class 4 | // tslint:disable:no-this 5 | // tslint:disable:no-if-statement 6 | // tslint:disable:no-object-mutation 7 | // tslint:disable:readonly-keyword 8 | export interface NgAotRelativePluginOptions { 9 | overwriteDict: Dict 10 | } 11 | 12 | interface Dict { 13 | [key: string]: string 14 | } 15 | 16 | function fromKeyValToSearchReplaceObject(obj: Dict) { 17 | return function(key: string) { 18 | return { 19 | search: new RegExp(key, 'g'), 20 | replace: `"fusing-angular-cli/.build/modules/src/modules/${obj[key]}"` 21 | } 22 | } 23 | } 24 | 25 | export class NgAotRelativePluginClass implements Plugin { 26 | constructor(public opts: Dict = {}) {} 27 | 28 | public test: RegExp = /.js/ 29 | 30 | onTypescriptTransform?(file: File) { 31 | Object.keys(this.opts) 32 | .map(fromKeyValToSearchReplaceObject(this.opts)) 33 | .filter(a => a.search.test(file.contents)) 34 | .forEach(a => { 35 | file.contents = file.contents.replace(a.search, a.replace) 36 | }) 37 | } 38 | } 39 | 40 | export const NgAotRelativePlugin = (dict?: Dict) => 41 | new NgAotRelativePluginClass(dict) 42 | -------------------------------------------------------------------------------- /src/fusebox/ng.compiler.plugin.ts: -------------------------------------------------------------------------------- 1 | import { main as ngc } from '@angular/compiler-cli/src/main' 2 | import { Plugin } from 'fuse-box' 3 | import { resolve } from 'path' 4 | 5 | // tslint:disable:no-class 6 | // tslint:disable:no-this 7 | // tslint:disable:no-if-statement 8 | // tslint:disable:no-object-mutation 9 | // tslint:disable:readonly-keyword 10 | const defaults: NgcPluginOptions = {} 11 | 12 | export interface NgcPluginOptions { 13 | enabled?: boolean 14 | } 15 | 16 | export class NgcPluginClass implements Plugin { 17 | constructor(private opts: NgcPluginOptions = defaults) {} 18 | 19 | bundleStart() { 20 | this.opts.enabled && 21 | ngc(['-p', resolve('tsconfig.aot.json')], err => { 22 | console.error(err) 23 | process.exit(1) 24 | }) 25 | } 26 | } 27 | 28 | export const NgCompilerPlugin = (options?: NgcPluginOptions) => 29 | new NgcPluginClass(options) 30 | -------------------------------------------------------------------------------- /src/fusebox/ng.polyfill.plugin.ts: -------------------------------------------------------------------------------- 1 | import { Plugin, File } from 'fuse-box' 2 | 3 | // tslint:disable:no-class 4 | // tslint:disable:no-this 5 | // tslint:disable:no-if-statement 6 | // tslint:disable:no-object-mutation 7 | // tslint:disable:readonly-keyword 8 | 9 | const defaults: NgPolyfillPluginOptions = {} 10 | 11 | export interface NgPolyfillPluginOptions { 12 | isServer?: boolean 13 | fileTest?: string 14 | } 15 | 16 | export const NG_POLY_BASE: ReadonlyArray = ['core-js/es7/reflect'] 17 | export const NG_POLY_SERVER: ReadonlyArray = [ 18 | ...NG_POLY_BASE, 19 | 'zone.js/dist/zone-node', 20 | 'zone.js/dist/long-stack-trace-zone' 21 | ] 22 | export const NG_POLY_BROWSER_IE: ReadonlyArray = [ 23 | 'core-js/es6/symbol', 24 | 'core-js/es6/object', 25 | 'core-js/es6/function', 26 | 'core-js/es6/parse-int', 27 | 'core-js/es6/parse-float', 28 | 'core-js/es6/number', 29 | 'core-js/es6/math', 30 | 'core-js/es6/string', 31 | 'core-js/es6/date', 32 | 'core-js/es6/array', 33 | 'core-js/es6/regexp', 34 | 'core-js/es6/map', 35 | 'core-js/es6/weak-map', 36 | 'core-js/es6/set' 37 | ] 38 | export const NG_POLY_BROWSER: ReadonlyArray = [ 39 | ...NG_POLY_BASE, 40 | 'zone.js/dist/zone' 41 | ] 42 | export const NG_POLY_BROWSER_IE_ANIMATIONS: ReadonlyArray = [ 43 | 'web-animations-js' 44 | ] 45 | 46 | const prepForTransform = (deps: ReadonlyArray) => { 47 | return deps 48 | .map(dep => { 49 | return `import '${dep}'` 50 | }) 51 | .join('\n') 52 | } 53 | 54 | export class NgPolyfillPluginClass implements Plugin { 55 | constructor(private opts: NgPolyfillPluginOptions = defaults) {} 56 | public test: RegExp = 57 | (this.opts.fileTest && new RegExp(this.opts.fileTest)) || 58 | /(app.browser.module.(ts|js))/ 59 | public dependencies: ['zone.js', 'core-js'] 60 | 61 | onTypescriptTransform(file: File) { 62 | if (!this.test.test(file.relativePath)) return 63 | file.contents = `${prepForTransform(this.buildSet())}\n${file.contents}` 64 | } 65 | 66 | buildSet() { 67 | if (this.opts.isServer) { 68 | return NG_POLY_SERVER 69 | } else { 70 | return NG_POLY_BROWSER 71 | } 72 | } 73 | } 74 | 75 | export const NgPolyfillPlugin = (options?: NgPolyfillPluginOptions) => 76 | new NgPolyfillPluginClass(options) 77 | -------------------------------------------------------------------------------- /src/fusebox/ng.prod.plugin.ts: -------------------------------------------------------------------------------- 1 | import { Plugin, File } from 'fuse-box' 2 | 3 | // tslint:disable:no-class 4 | // tslint:disable:no-this 5 | // tslint:disable:no-if-statement 6 | // tslint:disable:no-object-mutation 7 | // tslint:disable:readonly-keyword 8 | const defaults: NgProdPluginOptions = { 9 | enabled: true 10 | } 11 | 12 | export interface NgProdPluginOptions { 13 | enabled?: boolean 14 | fileTest?: string 15 | } 16 | 17 | export class NgProdPluginClass implements Plugin { 18 | constructor(public opts: NgProdPluginOptions = defaults) { 19 | this.opts = { 20 | ...defaults, 21 | ...opts 22 | } 23 | } 24 | 25 | public dependencies: ['@angular/core'] 26 | public test = this.regex || /app.browser.module.(ts|js)/ 27 | 28 | get regex() { 29 | return ( 30 | (this.opts && this.opts.fileTest && new RegExp(this.opts.fileTest)) || 31 | undefined 32 | ) 33 | } 34 | 35 | onTypescriptTransform(file: File) { 36 | if (!this.opts.enabled || !this.test.test(file.relativePath)) return 37 | file.contents = ` 38 | import { enableProdMode } from '@angular/core'; 39 | enableProdMode(); 40 | 41 | ${file.contents}` 42 | } 43 | } 44 | 45 | export const NgProdPlugin = (options?: NgProdPluginOptions) => 46 | new NgProdPluginClass(options) 47 | -------------------------------------------------------------------------------- /src/fusebox/ng.sw.plugin.ts: -------------------------------------------------------------------------------- 1 | import { File } from 'fuse-box/core/File' 2 | 3 | // tslint:disable:no-class 4 | // tslint:disable:no-this 5 | // tslint:disable:no-if-statement 6 | // tslint:disable:no-object-mutation 7 | // tslint:disable:readonly-keyword 8 | 9 | const defaults = {} 10 | 11 | export interface NgSwPluginOptions {} 12 | 13 | export class NgSwPluginClass { 14 | public test: RegExp = new RegExp('browser') 15 | 16 | constructor(public opts: NgSwPluginOptions = defaults) {} 17 | 18 | transform(file: File) { 19 | const regex = new RegExp(/enabled: false/, 'g') 20 | if (regex.test(file.contents)) { 21 | file.contents = file.contents.replace(regex, 'enabled: true') 22 | } 23 | } 24 | } 25 | 26 | export const NgSwPlugin = (options?: NgSwPluginOptions) => 27 | new NgSwPluginClass(options) 28 | -------------------------------------------------------------------------------- /src/generators/angular-core.gen.ts: -------------------------------------------------------------------------------- 1 | import { 2 | appModuleTemplate, 3 | appComponentTemplate, 4 | appRoutingModuleTemplate, 5 | appSharedModuleTemplate, 6 | appComponentCssTemplate, 7 | appComponentHtmlTemplate, 8 | appIndex, 9 | favicon, 10 | ngsw 11 | } from '../templates/core/app' 12 | import { writeFile_, mkDirAndContinueIfExists_ } from '../utilities/rx-fs' 13 | import { forkJoin } from 'rxjs' 14 | import { resolve } from 'path' 15 | import { flatMap } from 'rxjs/operators' 16 | import { 17 | browserModuleTemplate, 18 | browserAotEntryTemplate, 19 | browserJitEntryTemplate 20 | } from '../templates/core/browser' 21 | import { 22 | serverTemplate, 23 | serverModuleTemplate, 24 | serverAppTemplate 25 | } from '../templates/core/server' 26 | import { robots } from '../templates/core/assets' 27 | 28 | export function generateCoreAngular(projectDir: string) { 29 | return forkJoin([ 30 | generateCoreAngularApp(projectDir), 31 | generateCoreAngularBrowser(projectDir), 32 | generateCoreAngularServer(projectDir), 33 | generateCoreAngularAssets(projectDir) 34 | ]) 35 | } 36 | 37 | export function generateCoreAngularApp(projectDir: string, universal = true) { 38 | const root = resolve(`${projectDir}/src`) 39 | const baseDir = resolve(root, 'app') 40 | 41 | const appModulePrepped = appModuleTemplate 42 | .replace( 43 | /^.*#TransferHttpCacheModuleImport.*$/gm, 44 | universal 45 | ? "import { TransferHttpCacheModule } from '@nguniversal/common'" 46 | : '' 47 | ) 48 | .replace( 49 | /^.*#TransferHttpCacheModule.*$/gm, 50 | universal ? '\u0020\u0020\u0020\u0020TransferHttpCacheModule,' : '' 51 | ) 52 | 53 | return mkDirAndContinueIfExists_(root).pipe( 54 | flatMap(() => mkDirAndContinueIfExists_(baseDir)), 55 | flatMap(() => 56 | forkJoin([ 57 | writeFile_(`${baseDir}/app.module.ts`, appModulePrepped), 58 | writeFile_(`${baseDir}/app.shared.module.ts`, appSharedModuleTemplate), 59 | writeFile_( 60 | `${baseDir}/app.routing.module.ts`, 61 | appRoutingModuleTemplate 62 | ), 63 | writeFile_(`${baseDir}/app.component.ts`, appComponentTemplate), 64 | writeFile_(`${baseDir}/app.component.scss`, appComponentCssTemplate), // TODO: write component generator function instead 65 | writeFile_(`${baseDir}/app.component.html`, appComponentHtmlTemplate), 66 | writeFile_(`${baseDir}/index.pug`, appIndex), 67 | writeFile_(`${baseDir}/favicon.svg`, favicon), 68 | writeFile_(`${baseDir}/ngsw.json`, ngsw) 69 | ]) 70 | ) 71 | ) 72 | } 73 | 74 | export function generateCoreAngularBrowser(projectDir: string) { 75 | const root = resolve(`${projectDir}/src`) 76 | const baseDir = resolve(root, 'browser') 77 | return mkDirAndContinueIfExists_(root).pipe( 78 | flatMap(() => mkDirAndContinueIfExists_(baseDir)), 79 | flatMap(() => 80 | forkJoin([ 81 | writeFile_(`${baseDir}/app.browser.module.ts`, browserModuleTemplate), 82 | writeFile_( 83 | `${baseDir}/app.browser.entry.jit.ts`, 84 | browserJitEntryTemplate 85 | ), 86 | writeFile_( 87 | `${baseDir}/app.browser.entry.aot.ts`, 88 | browserAotEntryTemplate 89 | ) 90 | ]) 91 | ) 92 | ) 93 | } 94 | 95 | export function generateCoreAngularServer(projectDir: string) { 96 | const root = resolve(`${projectDir}/src`) 97 | const baseDir = resolve(root, 'server') 98 | return mkDirAndContinueIfExists_(root).pipe( 99 | flatMap(() => mkDirAndContinueIfExists_(baseDir)), 100 | flatMap(() => 101 | forkJoin([ 102 | writeFile_(`${baseDir}/server.angular.module.ts`, serverModuleTemplate), 103 | writeFile_(`${baseDir}/server.app.ts`, serverAppTemplate), 104 | writeFile_(`${baseDir}/server.ts`, serverTemplate) 105 | ]) 106 | ) 107 | ) 108 | } 109 | 110 | export function generateCoreAngularAssets(projectDir: string) { 111 | const root = resolve(`${projectDir}/src`) 112 | const baseDir = resolve(root, 'assets') 113 | return mkDirAndContinueIfExists_(root).pipe( 114 | flatMap(() => mkDirAndContinueIfExists_(baseDir)), 115 | flatMap(() => forkJoin([writeFile_(`${baseDir}/robots.txt`, robots)])) 116 | ) 117 | } 118 | -------------------------------------------------------------------------------- /src/generators/angular-universal.gen.ts: -------------------------------------------------------------------------------- 1 | export default function generate() { 2 | // 3 | } -------------------------------------------------------------------------------- /src/generators/config.gen.ts: -------------------------------------------------------------------------------- 1 | import { writeJsonFile_ } from '../utilities/rx-fs' 2 | import { resolve } from 'path' 3 | import { FAVICON_DEFAULTS as favicon } from '../templates/favicon' 4 | import { FUSEBOX_DEFAULTS as fusebox } from '../templates/fusebox' 5 | 6 | const configPath = 'fusing-angular.json' 7 | 8 | export default function generateFngConfig( 9 | path: string, 10 | overwrite = false, 11 | faviconOverride?: any 12 | ) { 13 | return writeJsonFile_( 14 | resolve(path, configPath), 15 | { 16 | favicon: { 17 | ...favicon, 18 | config: { 19 | ...favicon.config, 20 | ...faviconOverride 21 | } 22 | }, 23 | fusebox 24 | }, 25 | overwrite 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /src/generators/declarations.gen.ts: -------------------------------------------------------------------------------- 1 | import { writeFile_, writeFileSafely_ } from '../utilities/rx-fs' 2 | import { resolve } from 'path' 3 | import * as declarations from '../templates/declarations.ts.txt' 4 | 5 | const configPath = 'declarations.d.ts' 6 | 7 | export default function generateTsDeclartionFile( 8 | dir: string, 9 | overwrite = false 10 | ) { 11 | return overwrite 12 | ? writeFile_(resolve(dir, configPath), declarations) 13 | : writeFileSafely_(resolve(dir, configPath), declarations) 14 | } 15 | -------------------------------------------------------------------------------- /src/generators/deps.const.ts: -------------------------------------------------------------------------------- 1 | const ANGULAR_VERSION = '^6.1.0-rc.0' 2 | 3 | export const ANGULAR_CORE_DEV_DEPS = {} 4 | 5 | export const ANGULAR_CORE_DEPS = { 6 | '@angular/common': ANGULAR_VERSION, 7 | '@angular/compiler': ANGULAR_VERSION, 8 | '@angular/compiler-cli': ANGULAR_VERSION, 9 | '@angular/core': ANGULAR_VERSION, 10 | '@angular/http': ANGULAR_VERSION, 11 | '@angular/platform-browser': ANGULAR_VERSION, 12 | '@angular/platform-browser-dynamic': ANGULAR_VERSION, 13 | '@angular/router': ANGULAR_VERSION, 14 | 'core-js': '^2.5.7', 15 | rxjs: '^6.2.1', 16 | typescript: '2.7.2', 17 | 'zone.js': '^0.8.26' 18 | } 19 | 20 | export const ANGULAR_UNIVERSAL_DEPS = { 21 | '@angular/animations': ANGULAR_VERSION, 22 | '@angular/platform-server': ANGULAR_VERSION, 23 | '@nguniversal/common': '^6.0.0', 24 | '@nguniversal/express-engine': '^6.0.0' 25 | } 26 | 27 | export const ANGULAR_UNIVERSAL_DEV_DEPS = { 28 | '@types/cookie-parser': '^1.4.1', 29 | '@types/express': '^4.16.0', 30 | reload: '^2.3.0' 31 | } 32 | 33 | export const ANGULAR_UNIVERSAL_EXPRESS_DEPS = { 34 | 'cookie-parser': '^1.4.3', 35 | express: '^4.16.3' 36 | } 37 | -------------------------------------------------------------------------------- /src/generators/env.gen.ts: -------------------------------------------------------------------------------- 1 | import { writeFileSafely_, writeFile_ } from '../utilities/rx-fs' 2 | import { resolve } from 'path' 3 | import { FirebaseConfig } from '../commands/create-common' 4 | import * as env from '../templates/env.txt' 5 | 6 | const configPath = '.env' 7 | 8 | export function firebaseEnvConfigMap(config: FirebaseConfig) { 9 | return `FNG_FIREBASE_API_KEY=${config.apiKey} 10 | FNG_FIREBASE_AUTH_DOMAIN=${config.authDomain} 11 | FNG_FIREBASE_DATABASE_URL=${config.databaseUrl} 12 | FNG_FIREBASE_PROJECT_ID=${config.projectId} 13 | FNG_FIREBASE_STORAGE_BUCKET=${config.storageBucket} 14 | FNG_FIREBASE_MESSAGING_SENDER_ID=${config.messagingSenderId}` 15 | } 16 | 17 | export default function generateDotEnv( 18 | dir: string, 19 | overwrite = false, 20 | firebaseConfig?: FirebaseConfig, 21 | googleAnalyticsId?: string, 22 | googleSiteVerificationCode?: string 23 | ) { 24 | const resolvedFirebase = firebaseConfig 25 | ? env.replace('$FIREBASE', firebaseEnvConfigMap(firebaseConfig)) 26 | : env.replace('$FIREBASE', '') 27 | 28 | const resolvedGoogleAnalytics = googleAnalyticsId 29 | ? resolvedFirebase.replace( 30 | '$GOOGLE_ANALYTICS', 31 | `FNG_GOOGLE_ANALYTICS_TRACKING_ID=${googleAnalyticsId}` 32 | ) 33 | : resolvedFirebase.replace('$GOOGLE_ANALYTICS', '') 34 | 35 | const resolvedGoogleSite = googleSiteVerificationCode 36 | ? resolvedGoogleAnalytics.replace( 37 | '$GOOGLE_SITE_VERIFICATION', 38 | `FNG_GOOGLE_SITE_VERIFICATION_CODE=${googleSiteVerificationCode}` 39 | ) 40 | : resolvedGoogleAnalytics.replace('$GOOGLE_SITE_VERIFICATION', '') 41 | 42 | return overwrite 43 | ? writeFile_(resolve(dir, configPath), resolvedGoogleSite) 44 | : writeFileSafely_(resolve(dir, configPath), resolvedGoogleSite) 45 | } 46 | -------------------------------------------------------------------------------- /src/generators/gitignore.gen.ts: -------------------------------------------------------------------------------- 1 | import { writeFileSafely_, writeFile_ } from '../utilities/rx-fs' 2 | import { resolve } from 'path' 3 | import * as gitignore from '../templates/gitignore.txt' 4 | 5 | const configPath = '.gitignore' 6 | 7 | export default function generateGitIgnore(dir: string, overwrite = false) { 8 | return overwrite 9 | ? writeFile_(resolve(dir, configPath), gitignore) 10 | : writeFileSafely_(resolve(dir, configPath), gitignore) 11 | } 12 | -------------------------------------------------------------------------------- /src/generators/ide.gen.ts: -------------------------------------------------------------------------------- 1 | import { empty } from 'rxjs' 2 | import { resolve } from 'path' 3 | import { 4 | writeFileSafely_, 5 | writeFile_, 6 | mkDirAndContinueIfExists_ 7 | } from '../utilities/rx-fs' 8 | import { flatMap } from 'rxjs/operators' 9 | import * as vsCodeSettings from '../templates/vscode/settings.json.txt' 10 | import * as vsCodeLaunch from '../templates/vscode/launch.json.txt' 11 | import { IDE } from '../commands/create-common' 12 | 13 | const configPath = '.vscode/settings.json' 14 | const launchPath = '.vscode/launch.json' 15 | const dirRoot = '.vscode' 16 | 17 | function handleOther() { 18 | return empty() 19 | } 20 | 21 | function handleVSCode(dir: string, overwrite = false) { 22 | return overwrite 23 | ? mkDirAndContinueIfExists_(resolve(dir, dirRoot)).pipe( 24 | flatMap(() => writeFile_(resolve(dir, configPath), vsCodeSettings)), 25 | flatMap(() => writeFile_(resolve(dir, launchPath), vsCodeLaunch)) 26 | ) 27 | : mkDirAndContinueIfExists_(resolve(dir, dirRoot)).pipe( 28 | flatMap(() => 29 | writeFileSafely_(resolve(dir, configPath), vsCodeSettings) 30 | ), 31 | flatMap(() => writeFileSafely_(resolve(dir, launchPath), vsCodeLaunch)) 32 | ) 33 | } 34 | 35 | export default function generateIdeStubs( 36 | ide: IDE, 37 | dir: string, 38 | overwrite = false 39 | ) { 40 | switch (ide) { 41 | case IDE.OTHER: 42 | return handleOther() 43 | case IDE.VISUAL_STUDIO_CODE: 44 | return handleVSCode(dir, overwrite) 45 | default: 46 | return empty() 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/generators/package.gen.ts: -------------------------------------------------------------------------------- 1 | import { writeJsonFile_ } from '../utilities/rx-fs' 2 | import { resolve } from 'path' 3 | 4 | interface StringDictionary { 5 | readonly [key: string]: string 6 | } 7 | 8 | interface npmPackageConfig { 9 | readonly [key: string]: any 10 | readonly name: string 11 | readonly description?: string 12 | readonly license?: string 13 | readonly nodeVersionRange?: string 14 | readonly npmVersionRange?: string 15 | readonly version?: string 16 | readonly dependencies?: StringDictionary 17 | readonly devDependencies?: StringDictionary 18 | } 19 | 20 | function sortStringDict(dict: StringDictionary) { 21 | return Object.keys(dict) 22 | .sort() 23 | .reduce((acc, curr) => { 24 | return { 25 | ...acc, 26 | [curr]: dict[curr] 27 | } 28 | }, {}) 29 | } 30 | 31 | export default function generatePackageFile( 32 | _config: npmPackageConfig, 33 | overwrite = false, 34 | dirPath = '', 35 | filename = 'package.json' 36 | ) { 37 | const config: npmPackageConfig = { 38 | version: '0.0.0', 39 | license: 'UNLICENSED', 40 | description: 'Angular app scaffolded by Fusing-Angular-CLI', 41 | ..._config, 42 | dependencies: { 43 | 'fusing-angular-cli': '^0.2.x' 44 | }, 45 | engines: { 46 | node: '= 10.7.0', 47 | npm: '= 6.1.0' 48 | } 49 | } 50 | return writeJsonFile_( 51 | resolve(dirPath, filename), 52 | sortStringDict(config as StringDictionary), 53 | overwrite 54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /src/generators/tsconfig.gen.ts: -------------------------------------------------------------------------------- 1 | import { writeFile_, writeFileSafely_ } from '../utilities/rx-fs' 2 | import { resolve } from 'path' 3 | import { forkJoin } from 'rxjs' 4 | import * as tsconfig from '../templates/tsconfig.json.txt' 5 | import * as aotTsconfig from '../templates/tsconfig.aot.json.txt' 6 | 7 | const configPath = 'tsconfig.json' 8 | const configPath2 = 'tsconfig.aot.json' 9 | 10 | export default function generateTsConfig(dir: string, overwrite = false) { 11 | return overwrite 12 | ? forkJoin([ 13 | writeFile_(resolve(dir, configPath), tsconfig), 14 | writeFile_(resolve(dir, configPath2), aotTsconfig) 15 | ]) 16 | : forkJoin([ 17 | writeFileSafely_(resolve(dir, configPath), tsconfig), 18 | writeFileSafely_(resolve(dir, configPath2), aotTsconfig) 19 | ]) 20 | } 21 | -------------------------------------------------------------------------------- /src/generators/tslint.gen.ts: -------------------------------------------------------------------------------- 1 | import { writeFileSafely_, writeFile_ } from '../utilities/rx-fs' 2 | import { resolve } from 'path' 3 | import * as tslint from '../templates/tslint.json.txt' 4 | 5 | const configPath = 'tslint.json' 6 | 7 | export default function generateTsLint(dir: string, overwrite = false) { 8 | return overwrite 9 | ? writeFile_(resolve(dir, configPath), tslint) 10 | : writeFileSafely_(resolve(dir, configPath), tslint) 11 | } 12 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import welcome from './utilities/welcome' 2 | 3 | welcome() 4 | 5 | import './commands' 6 | -------------------------------------------------------------------------------- /src/modules/cookies/browser.ts: -------------------------------------------------------------------------------- 1 | export { CookiesBrowserModule } from './cookies.browser.module' 2 | export { CookieService } from './cookies.browser.service' 3 | -------------------------------------------------------------------------------- /src/modules/cookies/common.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs' 2 | import { CookieAttributes } from 'js-cookie' 3 | 4 | export interface KeyValue { 5 | readonly key: string 6 | readonly value: any 7 | } 8 | 9 | export interface ICookieService { 10 | readonly valueChange: Observable 11 | readonly valueChanges: Observable 12 | readonly targetValueChange: (key: string) => Observable 13 | readonly getAll: () => any 14 | readonly get: (name: string) => any 15 | readonly set: (name: string, value: any, options?: CookieAttributes) => void 16 | readonly remove: (name: string, options?: CookieAttributes) => void 17 | } 18 | 19 | export interface StringDict { 20 | readonly [key: string]: any 21 | } 22 | -------------------------------------------------------------------------------- /src/modules/cookies/cookies.browser.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | import { CookieService } from './cookies.browser.service' 3 | 4 | // tslint:disable-next-line:no-class 5 | @NgModule({ 6 | providers: [CookieService] 7 | }) 8 | export class CookiesBrowserModule {} 9 | -------------------------------------------------------------------------------- /src/modules/cookies/cookies.browser.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core' 2 | import { Subject } from 'rxjs' 3 | import { ICookieService, StringDict, KeyValue } from './common' 4 | import { CookieAttributes, getJSON, remove, set } from 'js-cookie' 5 | import { filter } from 'rxjs/operators' 6 | 7 | // tslint:disable:no-this 8 | // tslint:disable-next-line:no-class 9 | @Injectable() 10 | export class CookieService implements ICookieService { 11 | private readonly cookieSource = new Subject() 12 | private readonly changeSource = new Subject() 13 | public readonly valueChange = this.changeSource.asObservable() 14 | public readonly valueChanges = this.cookieSource.asObservable() 15 | 16 | targetValueChange(key: string) { 17 | return this.valueChange.pipe(filter(a => a && a.key === key)) 18 | } 19 | 20 | public set(name: string, value: any, opts?: CookieAttributes): void { 21 | set(name, value, opts) 22 | this.updateSource() 23 | this.broadcastChange(name) 24 | } 25 | 26 | public remove(name: string, opts?: CookieAttributes): void { 27 | remove(name, opts) 28 | this.updateSource() 29 | this.broadcastChange(name) 30 | } 31 | 32 | public get(name: string): any { 33 | return getJSON(name) 34 | } 35 | 36 | public getAll(): any { 37 | return getJSON() 38 | } 39 | 40 | private updateSource() { 41 | this.cookieSource.next(this.getAll()) 42 | } 43 | 44 | private broadcastChange(key: string) { 45 | this.changeSource.next({ 46 | key, 47 | value: this.get(key) 48 | }) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/modules/cookies/cookies.server.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | import { CookieService } from './cookies.browser.service' 3 | import { ServerCookieService } from './cookies.server.service' 4 | 5 | // tslint:disable-next-line:no-class 6 | @NgModule({ 7 | providers: [{ provide: CookieService, useClass: ServerCookieService }] 8 | }) 9 | export class CookiesServerModule {} 10 | -------------------------------------------------------------------------------- /src/modules/cookies/cookies.server.service.ts: -------------------------------------------------------------------------------- 1 | import { empty } from 'rxjs' 2 | import { REQUEST } from '@nguniversal/express-engine/tokens' 3 | import { Inject, Injectable } from '@angular/core' 4 | import { ICookieService } from './common' 5 | import * as express from 'express' 6 | 7 | // tslint:disable:no-this 8 | // tslint:disable-next-line:no-class 9 | @Injectable() 10 | export class ServerCookieService implements ICookieService { 11 | public readonly valueChange = empty() 12 | public readonly valueChanges = empty() 13 | 14 | constructor(@Inject(REQUEST) private req: express.Request) {} 15 | 16 | targetValueChange() { 17 | return empty() 18 | } 19 | 20 | public get(name: string): any { 21 | try { 22 | return JSON.parse(this.req.cookies[name]) 23 | } catch (err) { 24 | return this.req ? this.req.cookies[name] : undefined 25 | } 26 | } 27 | 28 | public getAll(): any { 29 | return this.req && this.req.cookies 30 | } 31 | 32 | public set(): void { 33 | // noop 34 | } 35 | 36 | public remove(): void { 37 | // noop 38 | } 39 | 40 | updateSource() { 41 | // noop 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/modules/cookies/server.ts: -------------------------------------------------------------------------------- 1 | export { ServerCookieService } from './cookies.server.service' 2 | export { CookiesServerModule } from './cookies.server.module' 3 | -------------------------------------------------------------------------------- /src/modules/environment/common.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core' 2 | import { makeStateKey } from '@angular/platform-browser' 3 | 4 | export const ENV_CONFIG = new InjectionToken('cfg.env') 5 | export const ENV_CONFIG_TS_KEY = makeStateKey('cfg.env.ts') 6 | -------------------------------------------------------------------------------- /src/modules/environment/environment.browser.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | import { EnvironmentService } from './environment.service' 3 | import { ENV_CONFIG, ENV_CONFIG_TS_KEY } from './common' 4 | import { TransferState } from '@angular/platform-browser' 5 | 6 | export function fuseBoxConfigFactory(ts: TransferState) { 7 | return ts.get(ENV_CONFIG_TS_KEY, {}) 8 | } 9 | 10 | // tslint:disable-next-line:no-class 11 | @NgModule({ 12 | providers: [ 13 | EnvironmentService, 14 | { 15 | provide: ENV_CONFIG, 16 | useFactory: fuseBoxConfigFactory, 17 | deps: [TransferState] 18 | } 19 | ] 20 | }) 21 | export class EnvironmentBrowserModule {} 22 | -------------------------------------------------------------------------------- /src/modules/environment/environment.server.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, APP_BOOTSTRAP_LISTENER, ApplicationRef } from '@angular/core' 2 | import { EnvironmentService, IBaseConfig } from './environment.service' 3 | import { ENV_CONFIG, ENV_CONFIG_TS_KEY } from './common' 4 | import { TransferState } from '@angular/platform-browser' 5 | import { filter, first, take } from 'rxjs/operators' 6 | import { REQUEST } from '@nguniversal/express-engine/tokens' 7 | 8 | function getConfigFromProcess() { 9 | return JSON.parse(process.env.FUSING_ANGULAR || '{}') 10 | } 11 | 12 | export function serverEnvConfigFactory() { 13 | return getConfigFromProcess() 14 | } 15 | 16 | // IF ENV CONTAINS SERVER_, remove from object 17 | function removeServerSpecific( 18 | obj: { readonly [key: string]: string }, 19 | filterKey = 'SERVER_' 20 | ) { 21 | return Object.keys(obj) 22 | .filter(key => !key.includes(filterKey)) 23 | .reduce((acc, curr) => { 24 | return { 25 | ...acc, 26 | [curr]: obj[curr] 27 | } 28 | }, {}) 29 | } 30 | 31 | export function onBootstrap( 32 | appRef: ApplicationRef, 33 | transferState: TransferState, 34 | req: any 35 | ) { 36 | return () => { 37 | appRef.isStable 38 | .pipe( 39 | filter(Boolean), 40 | first(), 41 | take(1) 42 | ) 43 | .subscribe(() => { 44 | transferState.set( 45 | ENV_CONFIG_TS_KEY, 46 | removeServerSpecific(getConfigFromProcess()) 47 | ) 48 | }) 49 | } 50 | } 51 | 52 | // tslint:disable-next-line:no-class 53 | @NgModule({ 54 | providers: [ 55 | EnvironmentService, 56 | { provide: ENV_CONFIG, useFactory: serverEnvConfigFactory }, 57 | { 58 | provide: APP_BOOTSTRAP_LISTENER, 59 | useFactory: onBootstrap, 60 | deps: [ApplicationRef, TransferState, REQUEST], 61 | multi: true 62 | } 63 | ] 64 | }) 65 | export class EnvironmentServerModule {} 66 | -------------------------------------------------------------------------------- /src/modules/environment/environment.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@angular/core' 2 | import { ENV_CONFIG } from './common' 3 | 4 | export interface IBaseConfig { 5 | readonly [key: string]: string | undefined 6 | readonly env?: string 7 | readonly port?: string 8 | } 9 | 10 | export interface IEnvironmentService { 11 | readonly config: TConfig 12 | } 13 | 14 | // tslint:disable-next-line:no-class 15 | @Injectable() 16 | export class EnvironmentService 17 | implements IEnvironmentService { 18 | constructor(@Inject(ENV_CONFIG) public config: TConfig = {} as TConfig) {} 19 | } 20 | -------------------------------------------------------------------------------- /src/modules/environment/index.ts: -------------------------------------------------------------------------------- 1 | export { ENV_CONFIG_TS_KEY, ENV_CONFIG } from './common' 2 | export { EnvironmentService } from './environment.service' 3 | export { EnvironmentBrowserModule } from './environment.browser.module' 4 | export { EnvironmentServerModule } from './environment.server.module' 5 | -------------------------------------------------------------------------------- /src/modules/firebase/auth/app.auth.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | import { AngularFireAuthModule } from 'angularfire2/auth' 3 | import { CookieService } from '../../cookies/browser' 4 | import { FirebaseUniversalAuthService } from './browser.auth.service' 5 | import { makeStateKey } from '@angular/platform-browser' 6 | import { 7 | FIREBASE_AUTH_COOKIE_STO_KEY, 8 | FIREBASE_AUTH_COOKIE_FACTORY, 9 | FIREBASE_AUTH_OBJ_TS 10 | } from './tokens' 11 | // import { FngFirebaseAuthLoadingComponent } from './loading-container/loading-container.component' 12 | 13 | // tslint:disable:no-this 14 | // tslint:disable-next-line:no-class 15 | @NgModule({ 16 | // declarations: [FngFirebaseAuthLoadingComponent], 17 | imports: [AngularFireAuthModule], 18 | exports: [AngularFireAuthModule], 19 | providers: [ 20 | FirebaseUniversalAuthService, 21 | { provide: FIREBASE_AUTH_COOKIE_FACTORY, useClass: CookieService }, 22 | { provide: FIREBASE_AUTH_COOKIE_STO_KEY, useValue: 'firebaseJWT' }, 23 | { provide: FIREBASE_AUTH_OBJ_TS, useValue: makeStateKey('fng.fb.auth.ts') } 24 | ] 25 | }) 26 | export class FirebaseAuthAppModule {} 27 | -------------------------------------------------------------------------------- /src/modules/firebase/auth/app.common.ts: -------------------------------------------------------------------------------- 1 | export { FirebaseAuthAppModule } from './app.auth.module' 2 | 3 | export interface ICookieGetSet { 4 | readonly set: (name: string, value: string) => string 5 | readonly remove: (name: string) => void 6 | } 7 | -------------------------------------------------------------------------------- /src/modules/firebase/auth/browser.auth.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, Inject } from '@angular/core' 2 | import { AngularFireAuth } from 'angularfire2/auth' 3 | import { 4 | flatMap, 5 | map, 6 | startWith, 7 | distinctUntilChanged, 8 | share 9 | } from 'rxjs/operators' 10 | import { FIREBASE_AUTH_COOKIE_STO_KEY } from './tokens' 11 | import { of } from 'rxjs' 12 | import { DOCUMENT } from '@angular/common' 13 | import { CookieService } from '../../cookies/browser' 14 | 15 | // tslint:disable:no-this 16 | 17 | function toExtractIdTokenFromUser(user: firebase.User | null) { 18 | return user ? user.getIdToken() : of(undefined).toPromise() 19 | } 20 | 21 | function storeJwtInCookies(cs: CookieService, storageKey: string) { 22 | return (jwt?: string) => { 23 | jwt ? cs.set(storageKey, jwt) : cs.remove(storageKey) 24 | } 25 | } 26 | 27 | // tslint:disable-next-line:no-class 28 | @NgModule() 29 | export class FirebaseAuthBrowserModule { 30 | readonly waitingOnLoginProcess_ = this.cs 31 | .targetValueChange('waitOnAuthResponse') 32 | .pipe( 33 | map(a => a.value), 34 | startWith(this.cs.get('waitOnAuthResponse')), 35 | map(val => (val && val === true ? true : false)), 36 | distinctUntilChanged(), 37 | share() 38 | ) 39 | 40 | constructor( 41 | public auth: AngularFireAuth, 42 | private cs: CookieService, 43 | @Inject(FIREBASE_AUTH_COOKIE_STO_KEY) stoKey: string, 44 | @Inject(DOCUMENT) private doc: HTMLDocument 45 | ) { 46 | auth.user 47 | .pipe(flatMap(toExtractIdTokenFromUser)) 48 | .subscribe(storeJwtInCookies(cs, stoKey)) 49 | 50 | this.waitingOnLoginProcess_.subscribe(v => { 51 | setTimeout(() => { 52 | const container = this.doc.querySelector( 53 | 'fng-firebase-spin-container' 54 | ) as HTMLDivElement | undefined 55 | // tslint:disable:no-if-statement 56 | if (container) { 57 | if (v) { 58 | container.style.display = 'block' 59 | } else { 60 | container.style.display = 'none' 61 | } 62 | } 63 | }, 0) 64 | }) 65 | 66 | this.auth.auth.getRedirectResult().then(() => { 67 | this.cs.remove('waitOnAuthResponse') 68 | }) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/modules/firebase/auth/browser.auth.service.ts: -------------------------------------------------------------------------------- 1 | import { AngularFireAuth } from 'angularfire2/auth' 2 | import { Injectable, Inject } from '@angular/core' 3 | import { startWith } from 'rxjs/operators' 4 | import { TransferState, StateKey } from '@angular/platform-browser' 5 | import { FIREBASE_AUTH_OBJ_TS } from './tokens' 6 | 7 | // tslint:disable:no-this 8 | // tslint:disable-next-line:no-class 9 | @Injectable() 10 | export class FirebaseUniversalAuthService { 11 | constructor( 12 | private auth: AngularFireAuth, 13 | private ts: TransferState, 14 | @Inject(FIREBASE_AUTH_OBJ_TS) private tsKey: StateKey 15 | ) {} 16 | 17 | readonly fbAuth = this.auth 18 | readonly user = this.auth.user.pipe( 19 | startWith(this.ts.get(this.tsKey, undefined)) 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /src/modules/firebase/auth/browser.common.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patrickmichalina/fusing-angular-cli/f9331d3d94dbc14d8e0d21ace554af5bbd7ae7e9/src/modules/firebase/auth/browser.common.ts -------------------------------------------------------------------------------- /src/modules/firebase/auth/browser.ts: -------------------------------------------------------------------------------- 1 | export { FirebaseUniversalAuthService } from './browser.auth.service' 2 | export { FirebaseAuthBrowserModule } from './browser.auth.module' 3 | -------------------------------------------------------------------------------- /src/modules/firebase/auth/loading-container/loading-container.component.ts: -------------------------------------------------------------------------------- 1 | // import { Component, ChangeDetectionStrategy } from '@angular/core' 2 | 3 | // // tslint:disable-next-line:no-class 4 | // @Component({ 5 | // selector: 'fng-auth-loading-container', 6 | // styles: [ 7 | // ':host{position:absolute;top:0;bottom:0;right:0;left:0;display:flex;justify-content:center;align-items:center;display:none}:host .transc{color:white;z-index:100;position:inherit;display:flex;justify-content:center;width:100%;height:100%;align-items:center}:host .auth-spin-overlay{z-index:99;background-color:white;height:100%;width:100%;position:inherit;animation:fadeIn .2s linear;opacity:1}@keyframes fadeIn{0%{opacity:0}100%{opacity:1}}' 8 | // ], 9 | // template: `
`, 10 | // changeDetection: ChangeDetectionStrategy.OnPush 11 | // }) 12 | // export class FngFirebaseAuthLoadingComponent {} 13 | -------------------------------------------------------------------------------- /src/modules/firebase/auth/server.auth.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, APP_INITIALIZER } from '@angular/core' 2 | import { AngularFireAuth } from 'angularfire2/auth' 3 | import { FirebaseServerAuth } from './server.auth.service' 4 | import { 5 | FIREBASE_AUTH_SERVER_ADMIN_APP, 6 | FIREBASE_AUTH_SERVER_USER_JWT 7 | } from './server.common' 8 | import { initializeApp, credential, auth, apps } from 'firebase-admin' 9 | import { EnvironmentService } from '../../environment' 10 | import { FIREBASE_AUTH_COOKIE_STO_KEY, FIREBASE_AUTH_OBJ_TS } from './tokens' 11 | import { CookieService } from '../../cookies/browser' 12 | import { TransferState, StateKey } from '@angular/platform-browser' 13 | import { take, tap } from 'rxjs/operators' 14 | import { FirebaseUniversalAuthService } from './browser.auth.service' 15 | 16 | function firebaseAdminAppAlreadyExists() { 17 | return apps.length ? true : false 18 | } 19 | 20 | function repairInlinePem(str?: string) { 21 | return str ? str.replace(/\\n/g, '\n') : '' 22 | } 23 | 24 | export function fbAdminFactory(es: EnvironmentService) { 25 | !firebaseAdminAppAlreadyExists() && 26 | initializeApp({ 27 | credential: credential.cert({ 28 | projectId: es.config.FIREBASE_PROJECT_ID, 29 | clientEmail: es.config.SERVER_FIREBASE_CLIENT_EMAIL, 30 | privateKey: repairInlinePem(es.config.SERVER_FIREBASE_PRIVATE_KEY) 31 | }), 32 | databaseURL: es.config.FIREBASE_DATABASE_URL 33 | }) 34 | return auth() 35 | } 36 | 37 | export function getUserJwt(cs: CookieService, key: string) { 38 | return cs.get(key) 39 | } 40 | 41 | export function onBootstrap( 42 | transferState: TransferState, 43 | auth: FirebaseServerAuth, 44 | tsKey: StateKey 45 | ) { 46 | return () => { 47 | return auth.user 48 | .pipe( 49 | take(1), 50 | tap(user => transferState.set(tsKey, user)) 51 | ) 52 | .toPromise() 53 | } 54 | } 55 | 56 | // tslint:disable-next-line:no-class 57 | @NgModule({ 58 | providers: [ 59 | FirebaseServerAuth, 60 | { provide: AngularFireAuth, useExisting: FirebaseServerAuth }, 61 | { 62 | provide: FirebaseUniversalAuthService, 63 | useExisting: FirebaseServerAuth 64 | }, 65 | { 66 | provide: FIREBASE_AUTH_SERVER_ADMIN_APP, 67 | useFactory: fbAdminFactory, 68 | deps: [EnvironmentService] 69 | }, 70 | { 71 | provide: FIREBASE_AUTH_SERVER_USER_JWT, 72 | useFactory: getUserJwt, 73 | deps: [CookieService, FIREBASE_AUTH_COOKIE_STO_KEY] 74 | }, 75 | { 76 | provide: APP_INITIALIZER, 77 | useFactory: onBootstrap, 78 | deps: [TransferState, FirebaseServerAuth, FIREBASE_AUTH_OBJ_TS], 79 | multi: true 80 | } 81 | ] 82 | }) 83 | export class FirebaseAuthServerModule {} 84 | -------------------------------------------------------------------------------- /src/modules/firebase/auth/server.auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Inject } from '@angular/core' 2 | import { 3 | FIREBASE_AUTH_SERVER_ADMIN_APP, 4 | FIREBASE_AUTH_SERVER_USER_JWT 5 | } from './server.common' 6 | import { auth } from 'firebase-admin' 7 | import { of } from 'rxjs' 8 | import { flatMap, catchError, map, tap } from 'rxjs/operators' 9 | import { TransferState, StateKey } from '@angular/platform-browser' 10 | import { FIREBASE_AUTH_OBJ_TS } from './tokens' 11 | 12 | function validateToken(auth: auth.Auth, jwt: string) { 13 | return of(auth.verifyIdToken(jwt)) 14 | } 15 | 16 | function byMappingItToUndefined(err: any) { 17 | return of(undefined) 18 | } 19 | 20 | function cacheToBrowser(tsCacheKey: StateKey, ts: TransferState) { 21 | return function(obj: any) { 22 | ts.set(tsCacheKey, obj) 23 | } 24 | } 25 | 26 | function toPsuedoUserObject(jwtObj: any) { 27 | return { 28 | isAnonymous: jwtObj.provider_id && jwtObj.provider_id === 'anonymous', 29 | uid: jwtObj.user_id, 30 | displayName: jwtObj.name, 31 | email: jwtObj.email, 32 | emailVerified: jwtObj.email_verified, 33 | photoURL: jwtObj.picture, 34 | phoneNumber: jwtObj.phone_number, 35 | providerId: jwtObj.firebase && jwtObj.firebase.sign_in_provider 36 | } 37 | } 38 | 39 | // tslint:disable:no-this 40 | // tslint:disable-next-line:no-class 41 | @Injectable() 42 | export class FirebaseServerAuth { 43 | constructor( 44 | private ts: TransferState, 45 | @Inject(FIREBASE_AUTH_OBJ_TS) private tsCacheKey: StateKey, 46 | @Inject(FIREBASE_AUTH_SERVER_ADMIN_APP) private authAdmin: auth.Auth, 47 | @Inject(FIREBASE_AUTH_SERVER_USER_JWT) private jwt: string 48 | ) {} 49 | 50 | readonly validatedToken_ = this.jwt 51 | ? validateToken(this.authAdmin, this.jwt).pipe( 52 | flatMap(a => a), 53 | map(toPsuedoUserObject), 54 | tap(cacheToBrowser(this.tsCacheKey, this.ts)), 55 | catchError(byMappingItToUndefined) 56 | ) 57 | : of(null) 58 | 59 | readonly user = this.validatedToken_ 60 | readonly authState = this.validatedToken_ 61 | readonly idTokenResult = of(this.jwt) 62 | readonly idToken = this.jwt 63 | } 64 | -------------------------------------------------------------------------------- /src/modules/firebase/auth/server.common.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core' 2 | import { auth } from 'firebase-admin' 3 | 4 | export const FIREBASE_AUTH_SERVER_ADMIN_APP = new InjectionToken( 5 | 'fng.fb.svr.auth.admin' 6 | ) 7 | export const FIREBASE_AUTH_SERVER_USER_JWT = new InjectionToken( 8 | 'fng.fb.svr.auth.jwt' 9 | ) 10 | -------------------------------------------------------------------------------- /src/modules/firebase/auth/server.ts: -------------------------------------------------------------------------------- 1 | export { FirebaseServerAuth } from './server.auth.service' 2 | export { FirebaseAuthServerModule } from './server.auth.module' 3 | -------------------------------------------------------------------------------- /src/modules/firebase/auth/tokens.ts: -------------------------------------------------------------------------------- 1 | import { ICookieGetSet } from './app.common' 2 | import { InjectionToken } from '@angular/core' 3 | import { StateKey } from '@angular/platform-browser' 4 | 5 | export const FIREBASE_AUTH_COOKIE_FACTORY = new InjectionToken( 6 | 'fng.auth.ck.get.set' 7 | ) 8 | export const FIREBASE_AUTH_COOKIE_STO_KEY = new InjectionToken( 9 | 'fng.auth.ck.sto' 10 | ) 11 | export const FIREBASE_AUTH_OBJ_TS = new InjectionToken>( 12 | 'fng.fb.auth.ts' 13 | ) 14 | -------------------------------------------------------------------------------- /src/modules/firebase/common/browser.ts: -------------------------------------------------------------------------------- 1 | import { 2 | TransferState, 3 | StateKey, 4 | makeStateKey 5 | } from '@angular/platform-browser' 6 | import { HttpParams } from '@angular/common/http' 7 | 8 | export function removeHttpInterceptorCache(ts: TransferState, key: string) { 9 | return function(val: T) { 10 | ts.remove(makeStateKey(`G.${key}`)) 11 | } 12 | } 13 | 14 | export function cacheInStateTransfer( 15 | ts: TransferState, 16 | key: StateKey 17 | ) { 18 | return function(val: T) { 19 | ts.set(key, val) 20 | } 21 | } 22 | 23 | export function getParams(fromObject = {} as any) { 24 | return new HttpParams({ 25 | fromObject: Object.keys(fromObject).reduce((acc, curr) => { 26 | return fromObject[curr] 27 | ? { 28 | ...acc, 29 | [curr]: fromObject[curr] 30 | } 31 | : { ...acc } 32 | }, {}) 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /src/modules/firebase/common/server.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core' 2 | import { createHash } from 'crypto' 3 | import { HttpParams } from '@angular/common/http' 4 | 5 | export interface LruCache { 6 | readonly get: (key: string) => T 7 | readonly set: (key: string, value: T) => T 8 | } 9 | 10 | export const LRU_CACHE = new InjectionToken('fng.lru') 11 | export const FIREBASE_USER_AUTH_TOKEN = new InjectionToken( 12 | 'fng.fb.svr.usr.auth' 13 | ) 14 | 15 | function sha256(data: string) { 16 | return createHash('sha256') 17 | .update(data) 18 | .digest('base64') 19 | } 20 | 21 | export function attemptToCacheInLru(key: string, lru?: LruCache) { 22 | return function(response?: any) { 23 | lru && response && lru.set(sha256(key), response) 24 | } 25 | } 26 | 27 | export function attemptToGetLruCachedValue(key: string, lru?: LruCache) { 28 | return lru && lru.get(sha256(key)) 29 | } 30 | 31 | export function getFullUrl(base: string, params: HttpParams) { 32 | const stringifiedParams = params.toString() 33 | return stringifiedParams ? `${base}?${params.toString()}` : base 34 | } 35 | -------------------------------------------------------------------------------- /src/modules/firebase/firebase.app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | import { 3 | FirebaseNameOrConfigToken, 4 | FirebaseOptionsToken, 5 | AngularFireModule 6 | } from 'angularfire2' 7 | import { EnvironmentService } from '../environment' 8 | import { FIREBASE_USER_AUTH_TOKEN } from './common/server' 9 | 10 | export interface IFirebaseEnvConfig { 11 | readonly [key: string]: string | undefined 12 | readonly FIREBASE_API_KEY: string 13 | readonly FIREBASE_AUTH_DOMAIN: string 14 | readonly FIREBASE_DATABASE_URL: string 15 | readonly FIREBASE_PROJECT_ID: string 16 | readonly FIREBASE_STORAGE_BUCKET: string 17 | readonly FIREBASE_MESSAGING_SENDER_ID: string 18 | } 19 | 20 | export function firebaseEnvironmentFactory( 21 | es: EnvironmentService 22 | ) { 23 | return { 24 | apiKey: es.config.FIREBASE_API_KEY, 25 | authDomain: es.config.FIREBASE_AUTH_DOMAIN, 26 | databaseURL: es.config.FIREBASE_DATABASE_URL, 27 | projectId: es.config.FIREBASE_PROJECT_ID, 28 | storageBucket: es.config.FIREBASE_STORAGE_BUCKET, 29 | messagingSenderId: es.config.FIREBASE_MESSAGING_SENDER_ID 30 | } 31 | } 32 | 33 | // tslint:disable-next-line:no-class 34 | @NgModule({ 35 | imports: [AngularFireModule], 36 | providers: [ 37 | { provide: FIREBASE_USER_AUTH_TOKEN, useValue: undefined }, 38 | { 39 | provide: FirebaseNameOrConfigToken, 40 | useValue: 'universal-webapp' 41 | }, 42 | { 43 | provide: FirebaseOptionsToken, 44 | useFactory: firebaseEnvironmentFactory, 45 | deps: [EnvironmentService] 46 | } 47 | ] 48 | }) 49 | export class FirebaseUniversalAppModule {} 50 | -------------------------------------------------------------------------------- /src/modules/firebase/firestore/browser.firebase.fs.common.ts: -------------------------------------------------------------------------------- 1 | import { AngularFirestore, QueryFn } from 'angularfire2/firestore' 2 | import { Observable } from 'rxjs' 3 | 4 | export interface IUniversalFirestoreService { 5 | readonly universalDoc: (path: string) => Observable 6 | readonly universalCollection: ( 7 | path: string, 8 | queryFn?: QueryFn 9 | ) => Observable> 10 | } 11 | 12 | export function extractFsHostFromLib(affs: AngularFirestore) { 13 | return (affs.firestore.app.options as any).projectId 14 | } 15 | -------------------------------------------------------------------------------- /src/modules/firebase/firestore/browser.firebase.fs.module.ts: -------------------------------------------------------------------------------- 1 | import { 2 | NgModule, 3 | ModuleWithProviders, 4 | Optional, 5 | SkipSelf 6 | } from '@angular/core' 7 | import { UniversalFirestoreService } from './browser.firebase.fs.service' 8 | import { AngularFirestoreModule } from 'angularfire2/firestore' 9 | 10 | // tslint:disable-next-line:no-class 11 | @NgModule({ 12 | imports: [AngularFirestoreModule], 13 | exports: [AngularFirestoreModule] 14 | }) 15 | export class FirebaseFsBrowserModule { 16 | static forRoot(): ModuleWithProviders { 17 | return { 18 | ngModule: FirebaseFsBrowserModule, 19 | providers: [UniversalFirestoreService] 20 | } 21 | } 22 | 23 | constructor( 24 | @Optional() 25 | @SkipSelf() 26 | parentModule: FirebaseFsBrowserModule 27 | ) { 28 | // tslint:disable-next-line:no-if-statement 29 | if (parentModule) 30 | throw new Error( 31 | 'FirebaseFsBrowserModule already loaded. Import in root module only.' 32 | ) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/modules/firebase/firestore/browser.firebase.fs.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | catchError, 3 | distinctUntilChanged, 4 | startWith, 5 | filter, 6 | take 7 | } from 'rxjs/operators' 8 | import { Injectable, ApplicationRef } from '@angular/core' 9 | import { TransferState } from '@angular/platform-browser' 10 | import { sha1 } from 'object-hash' 11 | import { of } from 'rxjs' 12 | import { AngularFirestore, QueryFn } from 'angularfire2/firestore' 13 | import { 14 | IUniversalFirestoreService, 15 | extractFsHostFromLib 16 | } from './browser.firebase.fs.common' 17 | import { 18 | makeFirestoreStateTransferKey, 19 | constructFsUrl 20 | } from './server.firebase.fs.common' 21 | 22 | // tslint:disable:no-this 23 | // tslint:disable-next-line:no-class 24 | @Injectable() 25 | export class UniversalFirestoreService implements IUniversalFirestoreService { 26 | constructor( 27 | private ts: TransferState, 28 | public afs: AngularFirestore, 29 | appRef: ApplicationRef 30 | ) { 31 | appRef.isStable 32 | .pipe( 33 | filter(Boolean), 34 | take(1) 35 | ) 36 | .subscribe(() => this.turnOffCache()) 37 | } 38 | 39 | // tslint:disable-next-line:readonly-keyword 40 | private readFromCache = true 41 | 42 | private turnOffCache() { 43 | // tslint:disable-next-line:no-object-mutation 44 | this.readFromCache = false 45 | } 46 | 47 | universalDoc(path: string) { 48 | const url = constructFsUrl(extractFsHostFromLib(this.afs), path) 49 | const cached = this.ts.get( 50 | makeFirestoreStateTransferKey(url), 51 | undefined 52 | ) 53 | 54 | const base = this.afs.doc(path).valueChanges() 55 | 56 | return this.readFromCache && cached 57 | ? base.pipe( 58 | startWith(cached as T), 59 | distinctUntilChanged((x, y) => sha1(x) === sha1(y)), 60 | catchError(err => of(undefined)) 61 | ) 62 | : base 63 | } 64 | 65 | universalCollection(path: string, queryFn?: QueryFn) { 66 | const url = constructFsUrl(extractFsHostFromLib(this.afs), path, true) 67 | 68 | const cached = this.ts.get>( 69 | makeFirestoreStateTransferKey(url), 70 | [] 71 | ) 72 | const base = this.afs.collection(path, queryFn).valueChanges() 73 | 74 | return this.readFromCache && cached.length > 0 75 | ? base.pipe( 76 | startWith(cached), 77 | distinctUntilChanged((x, y) => sha1(x) === sha1(y)), 78 | catchError(err => of(cached)) 79 | ) 80 | : base 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/modules/firebase/firestore/server.firebase.fs.common.ts: -------------------------------------------------------------------------------- 1 | import { makeStateKey } from '@angular/platform-browser' 2 | 3 | export function makeFirestoreStateTransferKey(fullUrl: string) { 4 | return makeStateKey(`FS.${fullUrl}`) 5 | } 6 | 7 | export function constructFsUrl(host: string, path?: string, runQuery = false) { 8 | return `https://firestore.googleapis.com/v1beta1/projects/${host}/databases/(default)/documents${ 9 | runQuery ? ':runQuery' : '/' + path 10 | }` 11 | } 12 | -------------------------------------------------------------------------------- /src/modules/firebase/firestore/server.firebase.fs.module.ts: -------------------------------------------------------------------------------- 1 | import { 2 | NgModule, 3 | ModuleWithProviders, 4 | Optional, 5 | SkipSelf 6 | } from '@angular/core' 7 | import { UniversalFirestoreService } from './browser.firebase.fs.service' 8 | import { ServerUniversalFirestoreService } from './server.firebase.fs.service' 9 | import { AngularFirestoreModule } from 'angularfire2/firestore' 10 | 11 | // tslint:disable-next-line:no-class 12 | @NgModule({ 13 | imports: [AngularFirestoreModule], 14 | exports: [AngularFirestoreModule] 15 | }) 16 | export class FirebaseFsServerModule { 17 | static forRoot(): ModuleWithProviders { 18 | return { 19 | ngModule: FirebaseFsServerModule, 20 | providers: [ 21 | { 22 | provide: UniversalFirestoreService, 23 | useClass: ServerUniversalFirestoreService 24 | } 25 | ] 26 | } 27 | } 28 | 29 | constructor( 30 | @Optional() 31 | @SkipSelf() 32 | parentModule: FirebaseFsServerModule 33 | ) { 34 | // tslint:disable-next-line:no-if-statement 35 | if (parentModule) 36 | throw new Error( 37 | 'FirebaseFsServerModule already loaded. Import in root module only.' 38 | ) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/modules/firebase/firestore/server.firebase.fs.service.ts: -------------------------------------------------------------------------------- 1 | import { TransferState } from '@angular/platform-browser' 2 | import { Injectable, Inject, Optional } from '@angular/core' 3 | import { AngularFirestore, QueryFn } from 'angularfire2/firestore' 4 | import { map, tap, take } from 'rxjs/operators' 5 | import { HttpClient } from '@angular/common/http' 6 | import { of } from 'rxjs' 7 | import { 8 | IUniversalFirestoreService, 9 | extractFsHostFromLib 10 | } from './browser.firebase.fs.common' 11 | import { 12 | FIREBASE_USER_AUTH_TOKEN, 13 | LRU_CACHE, 14 | LruCache, 15 | attemptToCacheInLru, 16 | attemptToGetLruCachedValue, 17 | getFullUrl 18 | } from '../common/server' 19 | import { 20 | removeHttpInterceptorCache, 21 | cacheInStateTransfer, 22 | getParams 23 | } from '../common/browser' 24 | import { 25 | makeFirestoreStateTransferKey, 26 | constructFsUrl 27 | } from './server.firebase.fs.common' 28 | import { sha1 } from 'object-hash' 29 | 30 | interface Field { 31 | readonly [key: string]: any 32 | } 33 | 34 | interface FieldPath { 35 | readonly segments: ReadonlyArray 36 | readonly offset: number 37 | readonly len: number 38 | } 39 | 40 | interface HttpResponseDocument { 41 | readonly name: string 42 | readonly fields: Field 43 | } 44 | 45 | interface HttpResponseDocumentWrapper { 46 | readonly document: HttpResponseDocument 47 | } 48 | 49 | interface OrderBy { 50 | readonly field: any 51 | readonly dir: any 52 | readonly isKeyOrderBy: boolean 53 | } 54 | 55 | interface RelationFilter { 56 | readonly field: FieldPath 57 | readonly op: { readonly name: string } 58 | readonly value: Object 59 | } 60 | 61 | function mapOrderBy(ordBy: OrderBy) { 62 | return { 63 | field: { 64 | fieldPath: ordBy.field.segments.pop() 65 | }, 66 | direction: ordBy.dir.name 67 | ? ordBy.dir.name === 'desc' 68 | ? 'DESCENDING' 69 | : 'ASCENDING' 70 | : 'DIRECTION_UNSPECIFIED' 71 | } 72 | } 73 | 74 | function mapOrderByCollection(ordBy: ReadonlyArray) { 75 | return ordBy.map(a => mapOrderBy(a)) 76 | } 77 | 78 | function coerceType(type: string, value: any): any { 79 | switch (type) { 80 | case 'booleanValue': 81 | return value 82 | case 'stringValue': 83 | return value 84 | case 'integerValue': 85 | return +value 86 | case 'arrayValue': 87 | return (value.values as ReadonlyArray).map(obj => { 88 | return Object.keys(obj).reduce((acc, curr, idx) => { 89 | return coerceType(curr, obj[curr]) 90 | }, {}) 91 | }) 92 | case 'mapValue': 93 | return reduceFields(value.fields) 94 | case 'nullValue': 95 | return undefined 96 | default: 97 | return undefined 98 | } 99 | } 100 | 101 | function reduceFields(fields: Field) { 102 | return Object.keys(fields).reduce((acc, curr) => { 103 | const converted = extractFieldType(fields[curr]) as any 104 | const innerKey = Object.keys(converted).pop() 105 | return { 106 | ...acc, 107 | [curr]: innerKey && converted[innerKey] 108 | } 109 | }, {}) as T 110 | } 111 | 112 | function extractFieldType(obj: any) { 113 | return Object.keys(obj).reduce((acc, curr) => { 114 | return { 115 | ...acc, 116 | [curr]: coerceType(curr, obj[curr]) 117 | } 118 | }, {}) 119 | } 120 | 121 | function getOpName(str: string) { 122 | switch (str) { 123 | case '==': 124 | return 'EQUAL' 125 | case '>': 126 | return 'GREATER_THAN' 127 | case '<': 128 | return 'LESS_THAN' 129 | case '>=': 130 | return 'GREATER_THAN_OR_EQUAL' 131 | case '<=': 132 | return 'LESS_THAN_OR_EQUAL' 133 | } 134 | } 135 | 136 | function lowerStrFirst(str: string) { 137 | return str.charAt(0).toLowerCase() + str.slice(1) 138 | } 139 | 140 | function mapFilterToFieldFilter(filter: RelationFilter) { 141 | return { 142 | fieldFilter: { 143 | field: { fieldPath: filter.field.segments.join('/') }, 144 | op: getOpName(filter.op.name), 145 | value: { 146 | [lowerStrFirst(filter.value.constructor.name)]: (filter.value as any) 147 | .internalValue 148 | } 149 | } 150 | } 151 | } 152 | 153 | function composeFilter(filters: ReadonlyArray) { 154 | return { 155 | compositeFilter: { 156 | op: 'AND', 157 | filters 158 | } 159 | } 160 | } 161 | 162 | // tslint:disable:no-this 163 | // tslint:disable-next-line:no-class 164 | @Injectable() 165 | export class ServerUniversalFirestoreService 166 | implements IUniversalFirestoreService { 167 | constructor( 168 | private http: HttpClient, 169 | private ts: TransferState, 170 | public afs: AngularFirestore, 171 | @Optional() 172 | @Inject(FIREBASE_USER_AUTH_TOKEN) 173 | private authToken?: string, 174 | @Optional() 175 | @Inject(LRU_CACHE) 176 | private lru?: LruCache 177 | ) {} 178 | 179 | universalDoc(path: string) { 180 | const url = constructFsUrl(extractFsHostFromLib(this.afs), path) 181 | const params = getParams({ auth: this.authToken }) 182 | const cacheKey = getFullUrl(url, params) 183 | const tsKey = makeFirestoreStateTransferKey(url) 184 | const cachedValue = attemptToGetLruCachedValue(cacheKey, this.lru) 185 | 186 | return cachedValue 187 | ? of(cachedValue).pipe(tap(cacheInStateTransfer(this.ts, tsKey))) 188 | : this.http.get(url).pipe( 189 | take(1), 190 | map(res => res.fields), 191 | map(reduceFields), 192 | tap(removeHttpInterceptorCache(this.ts, cacheKey)), 193 | tap(cacheInStateTransfer(this.ts, tsKey)), 194 | tap(attemptToCacheInLru(cacheKey, this.lru)) 195 | ) 196 | } 197 | 198 | universalCollection(path: string, queryFn?: QueryFn) { 199 | const url = constructFsUrl(extractFsHostFromLib(this.afs), undefined, true) 200 | const tsKey = makeFirestoreStateTransferKey(url) 201 | const ref = this.afs.firestore.collection(path) 202 | const query = (queryFn && queryFn(ref as any)) || ref 203 | const limit = (query as any)._query.limit 204 | const filters = (query as any)._query.filters as ReadonlyArray< 205 | RelationFilter 206 | > 207 | const orderBy = (query as any)._query.explicitOrderBy as ReadonlyArray< 208 | OrderBy 209 | > 210 | const cacheKey = sha1({ ...(query as any)._query, path }) 211 | const cachedValue = attemptToGetLruCachedValue(cacheKey, this.lru) 212 | const fieldFilters = filters.map(mapFilterToFieldFilter) 213 | const where = composeFilter(fieldFilters) 214 | 215 | const structuredQuery = { 216 | limit, 217 | from: [{ collectionId: path }], 218 | orderBy: mapOrderByCollection(orderBy), 219 | where 220 | } 221 | 222 | const fbToken = undefined //TODO: this.auth.getCustomFirebaseToken() 223 | const baseObs = 224 | fbToken !== undefined 225 | ? this.http.post( 226 | url, 227 | { structuredQuery }, 228 | { headers: { Authorization: `Bearer ${fbToken}` } } 229 | ) 230 | : this.http.post(url, { structuredQuery }) 231 | 232 | return cachedValue 233 | ? of(cachedValue).pipe(tap(cacheInStateTransfer(this.ts, tsKey))) 234 | : baseObs.pipe( 235 | take(1), 236 | map((docs: ReadonlyArray) => { 237 | return docs.filter(a => a.document).map(doc => { 238 | return reduceFields(doc.document.fields) as T 239 | }) as ReadonlyArray 240 | }), 241 | tap(cacheInStateTransfer(this.ts, tsKey)), 242 | tap(attemptToCacheInLru(cacheKey, this.lru)) 243 | ) 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /src/modules/firebase/index.ts: -------------------------------------------------------------------------------- 1 | export { FirebaseUniversalAppModule } from './firebase.app.module' 2 | export { FirebaseFsBrowserModule } from './firestore/browser.firebase.fs.module' 3 | export { 4 | UniversalFirestoreService 5 | } from './firestore/browser.firebase.fs.service' 6 | export { FirebaseFsServerModule } from './firestore/server.firebase.fs.module' 7 | export { 8 | ServerUniversalFirestoreService 9 | } from './firestore/server.firebase.fs.service' 10 | export { 11 | IUniversalFirestoreService 12 | } from './firestore/browser.firebase.fs.common' 13 | 14 | export { ServerUniversalRtDbService } from './rtdb/server.firebase.rtdb.service' 15 | export { UniversalRtDbService } from './rtdb/browser.firebase.rtdb.service' 16 | export { FirebaseRtDbBrowserModule } from './rtdb/browser.firebase.rtdb.module' 17 | export { FirebaseRtDbServerModule } from './rtdb/server.firebase.rtdb.module' 18 | export { IUniversalRtdbService } from './rtdb/browser.firebase.rtdb.common' 19 | export { LRU_CACHE, LruCache, FIREBASE_USER_AUTH_TOKEN } from './common/server' 20 | -------------------------------------------------------------------------------- /src/modules/firebase/rtdb/browser.firebase.rtdb.common.ts: -------------------------------------------------------------------------------- 1 | import { AngularFireDatabase } from 'angularfire2/database' 2 | import { Observable } from 'rxjs' 3 | import { QueryFn } from 'angularfire2/database' 4 | 5 | export interface IUniversalRtdbService { 6 | readonly universalObject: (path: string) => Observable 7 | readonly universalList: ( 8 | path: string, 9 | queryFn?: QueryFn 10 | ) => Observable> 11 | } 12 | 13 | export function extractRtDbHostFromLib(afRtDb: AngularFireDatabase) { 14 | return `https://${ 15 | (afRtDb.database.app.options as any).projectId 16 | }.firebaseio.com` 17 | } 18 | -------------------------------------------------------------------------------- /src/modules/firebase/rtdb/browser.firebase.rtdb.module.ts: -------------------------------------------------------------------------------- 1 | import { 2 | NgModule, 3 | ModuleWithProviders, 4 | Optional, 5 | SkipSelf 6 | } from '@angular/core' 7 | import { UniversalRtDbService } from './browser.firebase.rtdb.service' 8 | import { AngularFireDatabaseModule } from 'angularfire2/database' 9 | 10 | // tslint:disable-next-line:no-class 11 | @NgModule({ 12 | imports: [AngularFireDatabaseModule], 13 | exports: [AngularFireDatabaseModule] 14 | }) 15 | export class FirebaseRtDbBrowserModule { 16 | static forRoot(): ModuleWithProviders { 17 | return { 18 | ngModule: FirebaseRtDbBrowserModule, 19 | providers: [UniversalRtDbService] 20 | } 21 | } 22 | 23 | constructor( 24 | @Optional() 25 | @SkipSelf() 26 | parentModule: FirebaseRtDbBrowserModule 27 | ) { 28 | // tslint:disable-next-line:no-if-statement 29 | if (parentModule) 30 | throw new Error( 31 | 'FirebaseRtDbBrowserModule already loaded. Import in root module only.' 32 | ) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/modules/firebase/rtdb/browser.firebase.rtdb.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | catchError, 3 | filter, 4 | map, 5 | startWith, 6 | take, 7 | distinctUntilChanged 8 | } from 'rxjs/operators' 9 | import { ApplicationRef, Injectable } from '@angular/core' 10 | import { AngularFireDatabase, QueryFn } from 'angularfire2/database' 11 | import { makeStateKey, TransferState } from '@angular/platform-browser' 12 | import { Observable, of } from 'rxjs' 13 | import { sha1 } from 'object-hash' 14 | import { 15 | extractRtDbHostFromLib, 16 | IUniversalRtdbService 17 | } from './browser.firebase.rtdb.common' 18 | 19 | // tslint:disable:no-this 20 | // tslint:disable-next-line:no-class 21 | @Injectable() 22 | export class UniversalRtDbService implements IUniversalRtdbService { 23 | // tslint:disable-next-line:readonly-keyword 24 | readFromCache = true 25 | 26 | constructor( 27 | public angularFireDatabase: AngularFireDatabase, 28 | private ts: TransferState, 29 | appRef: ApplicationRef 30 | ) { 31 | appRef.isStable 32 | .pipe( 33 | filter(Boolean), 34 | take(1) 35 | ) 36 | .subscribe(() => this.turnOffCache()) 37 | } 38 | 39 | private turnOffCache() { 40 | // tslint:disable-next-line:no-object-mutation 41 | this.readFromCache = false 42 | } 43 | 44 | universalObject(path: string): Observable { 45 | const cached = this.ts.get(this.cacheKey(path), undefined) 46 | const base = this.angularFireDatabase 47 | .object(path) 48 | .valueChanges() 49 | .pipe( 50 | map(a => (a ? a : undefined)), 51 | catchError(() => of(undefined)) 52 | ) 53 | 54 | return !this.readFromCache 55 | ? base 56 | : base.pipe( 57 | startWith(cached), 58 | distinctUntilChanged((x, y) => (x && sha1(x)) === (y && sha1(y))) 59 | ) 60 | } 61 | 62 | universalList( 63 | path: string, 64 | queryFn?: QueryFn 65 | ): Observable> { 66 | // tslint:disable-next-line:readonly-array 67 | const cached = this.ts.get(this.cacheKey(path), []) 68 | const base = this.angularFireDatabase.list(path, queryFn).valueChanges() 69 | return this.readFromCache 70 | ? base.pipe( 71 | startWith(cached), 72 | distinctUntilChanged((x, y) => (x && sha1(x)) === (y && sha1(y))) 73 | ) 74 | : base 75 | } 76 | 77 | private cacheKey(path: string) { 78 | return makeStateKey( 79 | `RTDB.${extractRtDbHostFromLib(this.angularFireDatabase)}/${path}.json` 80 | ) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/modules/firebase/rtdb/server.firebase.rtdb.common.ts: -------------------------------------------------------------------------------- 1 | import { makeStateKey } from '@angular/platform-browser' 2 | 3 | export function makeRtDbStateTransferKey(fullUrl: string) { 4 | return makeStateKey(`RTDB.${fullUrl}`) 5 | } 6 | -------------------------------------------------------------------------------- /src/modules/firebase/rtdb/server.firebase.rtdb.module.ts: -------------------------------------------------------------------------------- 1 | import { 2 | NgModule, 3 | ModuleWithProviders, 4 | Optional, 5 | SkipSelf 6 | } from '@angular/core' 7 | import { ServerUniversalRtDbService } from './server.firebase.rtdb.service' 8 | import { UniversalRtDbService } from './browser.firebase.rtdb.service' 9 | import { AngularFireDatabaseModule } from 'angularfire2/database' 10 | 11 | // tslint:disable-next-line:no-class 12 | @NgModule({ 13 | imports: [AngularFireDatabaseModule], 14 | exports: [AngularFireDatabaseModule] 15 | }) 16 | export class FirebaseRtDbServerModule { 17 | static forRoot(): ModuleWithProviders { 18 | return { 19 | ngModule: FirebaseRtDbServerModule, 20 | providers: [ 21 | { 22 | provide: UniversalRtDbService, 23 | useClass: ServerUniversalRtDbService 24 | } 25 | ] 26 | } 27 | } 28 | 29 | constructor( 30 | @Optional() 31 | @SkipSelf() 32 | parentModule: FirebaseRtDbServerModule 33 | ) { 34 | // tslint:disable-next-line:no-if-statement 35 | if (parentModule) 36 | throw new Error( 37 | 'FirebaseRtDbServerModule already loaded. Import in root module only.' 38 | ) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/modules/firebase/rtdb/server.firebase.rtdb.service.ts: -------------------------------------------------------------------------------- 1 | import { catchError, take, tap, map } from 'rxjs/operators' 2 | import { 3 | AngularFireDatabase, 4 | QueryFn, 5 | PathReference 6 | } from 'angularfire2/database' 7 | import { Inject, Injectable, Optional } from '@angular/core' 8 | import { HttpClient, HttpResponse } from '@angular/common/http' 9 | import { 10 | LRU_CACHE, 11 | LruCache, 12 | attemptToGetLruCachedValue, 13 | attemptToCacheInLru, 14 | FIREBASE_USER_AUTH_TOKEN, 15 | getFullUrl 16 | } from '../common/server' 17 | import { Observable, of } from 'rxjs' 18 | import { TransferState } from '@angular/platform-browser' 19 | import { makeRtDbStateTransferKey } from './server.firebase.rtdb.common' 20 | import { 21 | cacheInStateTransfer, 22 | removeHttpInterceptorCache, 23 | getParams 24 | } from '../common/browser' 25 | import { IUniversalRtdbService } from './browser.firebase.rtdb.common' 26 | 27 | function constructFbUrl(db: AngularFireDatabase, path: string) { 28 | const query = db.database.ref(path) 29 | return `${query.toString()}.json` 30 | } 31 | 32 | function mapUndefined(err: any) { 33 | return of(undefined) 34 | } 35 | 36 | function mapEmptyList(err: any) { 37 | return of([] as ReadonlyArray) 38 | } 39 | 40 | // tslint:disable:no-this 41 | // tslint:disable-next-line:no-class 42 | @Injectable() 43 | export class ServerUniversalRtDbService implements IUniversalRtdbService { 44 | constructor( 45 | private http: HttpClient, 46 | private afdb: AngularFireDatabase, 47 | private ts: TransferState, 48 | @Optional() 49 | @Inject(FIREBASE_USER_AUTH_TOKEN) 50 | private authToken?: string, 51 | @Optional() 52 | @Inject(LRU_CACHE) 53 | private lru?: LruCache 54 | ) {} 55 | 56 | universalObject(path: string): Observable { 57 | const url = constructFbUrl(this.afdb, path) 58 | const params = getParams({ auth: this.authToken }) 59 | const cacheKey = getFullUrl(url, params) 60 | const cachedValue = attemptToGetLruCachedValue(cacheKey, this.lru) 61 | const tsKey = makeRtDbStateTransferKey(url) 62 | const baseObs = this.http.get>(url, { params }) 63 | 64 | return cachedValue 65 | ? of(cachedValue).pipe(tap(cacheInStateTransfer(this.ts, tsKey))) 66 | : baseObs.pipe( 67 | take(1), 68 | tap(removeHttpInterceptorCache(this.ts, cacheKey)), 69 | tap(cacheInStateTransfer(this.ts, tsKey)), 70 | tap(attemptToCacheInLru(cacheKey, this.lru)), 71 | catchError(mapUndefined) 72 | ) 73 | } 74 | 75 | // tslint:disable:readonly-array 76 | universalList(path: PathReference, queryFn?: QueryFn): Observable { 77 | const query = 78 | (queryFn && queryFn(this.afdb.database.ref(path.toString()))) || 79 | this.afdb.database.ref(path.toString()) 80 | const internalQueryParams = (query as any).queryParams_ 81 | const paramsFromString = internalQueryParams.toRestQueryStringParameters() 82 | const url = `${query.toString()}.json` 83 | const params = getParams({ ...paramsFromString, auth: this.authToken }) 84 | const cacheKey = getFullUrl(url, params) 85 | const tsKey = makeRtDbStateTransferKey(url) 86 | const baseObs = this.http.get(url, { params }) 87 | const cachedValue = attemptToGetLruCachedValue(cacheKey, this.lru) 88 | 89 | return cachedValue 90 | ? of(cachedValue).pipe(tap(cacheInStateTransfer(this.ts, tsKey))) 91 | : baseObs.pipe( 92 | take(1), 93 | tap(removeHttpInterceptorCache(this.ts, url)), 94 | map((val: any) => { 95 | return Array.isArray(val) 96 | ? val.filter(Boolean) 97 | : Object.keys(val).map(key => val[key]) 98 | }), 99 | tap(cacheInStateTransfer(this.ts, tsKey)), 100 | tap(attemptToCacheInLru(cacheKey, this.lru)), 101 | 102 | catchError(mapEmptyList) 103 | ) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/modules/fusing-angular/browser.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | import { WindowBrowserModule } from '../util/window/window-browser.module' 3 | import { CookiesBrowserModule } from '../cookies/browser' 4 | import { EnvironmentBrowserModule } from '../environment' 5 | import { ResponseBrowserModule } from '../response/browser' 6 | import { ServiceWorkerModule } from '@angular/service-worker' 7 | import { 8 | BrowserTransferStateModule, 9 | BrowserModule 10 | } from '@angular/platform-browser' 11 | 12 | // tslint:disable-next-line:no-class 13 | @NgModule({ 14 | imports: [ 15 | BrowserModule.withServerTransition({ appId: 'app-root' }), 16 | BrowserTransferStateModule, 17 | ServiceWorkerModule.register('/js/ngsw-worker.js', { 18 | enabled: false 19 | }), 20 | WindowBrowserModule.forRoot(), 21 | CookiesBrowserModule, 22 | EnvironmentBrowserModule, 23 | ResponseBrowserModule 24 | ], 25 | exports: [ 26 | BrowserModule, 27 | BrowserTransferStateModule, 28 | ServiceWorkerModule, 29 | WindowBrowserModule, 30 | CookiesBrowserModule, 31 | EnvironmentBrowserModule, 32 | ResponseBrowserModule 33 | ] 34 | }) 35 | export class FusingAngularBrowserModule {} 36 | -------------------------------------------------------------------------------- /src/modules/fusing-angular/server.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | import { WindowServerModule } from '../util/window/window-server.module' 3 | import { EnvironmentServerModule } from '../environment' 4 | import { CookiesServerModule } from '../cookies/server' 5 | import { ResponseServerModule } from '../response/server' 6 | import { 7 | ServerModule, 8 | ServerTransferStateModule 9 | } from '@angular/platform-server' 10 | 11 | // tslint:disable-next-line:no-class 12 | @NgModule({ 13 | imports: [ 14 | ServerTransferStateModule, 15 | WindowServerModule.forRoot({}), 16 | EnvironmentServerModule, 17 | CookiesServerModule, 18 | ResponseServerModule, 19 | ServerModule 20 | ], 21 | exports: [ 22 | ServerTransferStateModule, 23 | WindowServerModule, 24 | EnvironmentServerModule, 25 | CookiesServerModule, 26 | ResponseServerModule, 27 | ServerModule 28 | ] 29 | }) 30 | export class FusingAngularServerModule {} 31 | -------------------------------------------------------------------------------- /src/modules/http-cache-tag/http-cache-tag-interceptor.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@angular/core' 2 | import { 3 | HttpHandler, 4 | HttpInterceptor, 5 | HttpRequest, 6 | HttpResponse, 7 | HttpEvent 8 | } from '@angular/common/http' 9 | import { 10 | CACHE_TAG_CONFIG, 11 | CACHE_TAG_FACTORY, 12 | CacheFactory, 13 | CacheTagConfig 14 | } from './http-cache-tag.server.module' 15 | import { map } from 'rxjs/operators' 16 | import { Observable } from 'rxjs' 17 | 18 | // tslint:disable:no-class 19 | // tslint:disable:no-this 20 | // tslint:disable:no-if-statement 21 | @Injectable() 22 | export class HttpCacheTagInterceptor implements HttpInterceptor { 23 | constructor( 24 | @Inject(CACHE_TAG_CONFIG) private config: CacheTagConfig, 25 | @Inject(CACHE_TAG_FACTORY) private factory: CacheFactory 26 | ) { 27 | if (!config.headerKey) throw new Error('missing config.headerKey') 28 | if (!config.cacheableResponseCodes) 29 | throw new Error('missing config.cacheableResponseCodes') 30 | } 31 | 32 | isCacheableCode(code: number) { 33 | return this.config.cacheableResponseCodes.find(a => a === code) 34 | } 35 | 36 | isCacheableUrl(url: string | null) { 37 | if (!this.config.cacheableUrls || !url || url === null) return true 38 | return this.config.cacheableUrls.test(url) 39 | } 40 | 41 | intercept( 42 | req: HttpRequest, 43 | next: HttpHandler 44 | ): Observable> { 45 | return next.handle(req).pipe( 46 | map(event => { 47 | if ( 48 | event instanceof HttpResponse && 49 | this.isCacheableCode(event.status) && 50 | this.isCacheableUrl(event.url) 51 | ) { 52 | this.factory(event, this.config) 53 | } 54 | return event 55 | }) 56 | ) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/modules/http-cache-tag/http-cache-tag.server.module.ts: -------------------------------------------------------------------------------- 1 | import { 2 | InjectionToken, 3 | ModuleWithProviders, 4 | NgModule, 5 | Optional, 6 | SkipSelf 7 | } from '@angular/core' 8 | import { HTTP_INTERCEPTORS, HttpResponse } from '@angular/common/http' 9 | import { HttpCacheTagInterceptor } from './http-cache-tag-interceptor.service' 10 | 11 | export const CACHE_TAG_CONFIG = new InjectionToken( 12 | 'cfg.http.ct' 13 | ) 14 | export const CACHE_TAG_FACTORY = new InjectionToken( 15 | 'cfg.http.ctf' 16 | ) 17 | 18 | export interface CacheTagConfig { 19 | readonly headerKey: string 20 | readonly cacheableResponseCodes: ReadonlyArray 21 | readonly cacheableUrls?: RegExp 22 | } 23 | 24 | export type CacheFactory = ( 25 | httpResponse: HttpResponse, 26 | config: CacheTagConfig 27 | ) => void 28 | 29 | // tslint:disable-next-line:no-class 30 | @NgModule() 31 | export class HttpCacheTagModule { 32 | static forRoot( 33 | configProvider: any, 34 | factoryProvider: any 35 | ): ModuleWithProviders { 36 | return { 37 | ngModule: HttpCacheTagModule, 38 | providers: [ 39 | { 40 | provide: HttpCacheTagInterceptor, 41 | useClass: HttpCacheTagInterceptor, 42 | deps: [CACHE_TAG_CONFIG, CACHE_TAG_FACTORY] 43 | }, 44 | { 45 | provide: HTTP_INTERCEPTORS, 46 | useExisting: HttpCacheTagInterceptor, 47 | multi: true 48 | }, 49 | configProvider, 50 | factoryProvider 51 | ] 52 | } 53 | } 54 | 55 | constructor( 56 | @Optional() 57 | @SkipSelf() 58 | parentModule: HttpCacheTagModule 59 | ) { 60 | // tslint:disable-next-line:no-if-statement 61 | if (parentModule) 62 | throw new Error( 63 | 'HttpCachTageModule already loaded. Import in root module only.' 64 | ) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/modules/http-cache-tag/index.ts: -------------------------------------------------------------------------------- 1 | export { HttpCacheTagInterceptor } from './http-cache-tag-interceptor.service' 2 | export { 3 | HttpCacheTagModule, 4 | CACHE_TAG_CONFIG, 5 | CACHE_TAG_FACTORY, 6 | CacheFactory, 7 | CacheTagConfig 8 | } from './http-cache-tag.server.module' 9 | -------------------------------------------------------------------------------- /src/modules/index.ts: -------------------------------------------------------------------------------- 1 | export { WINDOW } from './util/tokens' 2 | 3 | export { IWindowService, WindowService } from './util/window/window.service' 4 | export { WindowServerModule } from './util/window/window-server.module' 5 | export { WindowBrowserModule } from './util/window/window-browser.module' 6 | -------------------------------------------------------------------------------- /src/modules/not-found/index.ts: -------------------------------------------------------------------------------- 1 | export { NotFoundComponent } from './not-found.component' 2 | export { NotFoundRoutingModule } from './not-found.module' 3 | -------------------------------------------------------------------------------- /src/modules/not-found/not-found.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ChangeDetectionStrategy } from '@angular/core' 2 | import { ResponseService } from '../response/browser' 3 | 4 | // tslint:disable-next-line:no-class 5 | @Component({ 6 | selector: 'not-found', 7 | template: '

Page Not Found

', 8 | changeDetection: ChangeDetectionStrategy.OnPush 9 | }) 10 | export class NotFoundComponent { 11 | constructor(rs: ResponseService) { 12 | rs.notFound() 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/modules/not-found/not-found.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | import { RouterModule, Routes } from '@angular/router' 3 | import { NotFoundComponent } from './not-found.component' 4 | 5 | export const routes: Routes = [{ path: '**', component: NotFoundComponent }] 6 | 7 | // tslint:disable-next-line:no-class 8 | @NgModule({ 9 | imports: [RouterModule.forRoot(routes)], 10 | declarations: [NotFoundComponent], 11 | exports: [RouterModule, NotFoundComponent] 12 | }) 13 | export class NotFoundRoutingModule {} 14 | -------------------------------------------------------------------------------- /src/modules/response/browser.response.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | import { ResponseService } from './browser.response.service' 3 | 4 | // tslint:disable-next-line:no-class 5 | @NgModule({ 6 | providers: [ResponseService] 7 | }) 8 | export class ResponseBrowserModule {} 9 | -------------------------------------------------------------------------------- /src/modules/response/browser.response.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core' 2 | import { IResponseService } from './common' 3 | 4 | // tslint:disable:no-this 5 | // tslint:disable:no-object-mutation 6 | // tslint:disable-next-line:no-class 7 | @Injectable() 8 | export class ResponseService implements IResponseService { 9 | set(): void { 10 | // noop on browser 11 | } 12 | 13 | ok(): void { 14 | // noop on browser 15 | } 16 | 17 | badRequest(): void { 18 | // noop on browser 19 | } 20 | 21 | unauthorized(): void { 22 | // noop on browser 23 | } 24 | 25 | paymentRequired(): void { 26 | // noop on browser 27 | } 28 | 29 | forbidden(): void { 30 | // noop on browser 31 | } 32 | 33 | notFound(): void { 34 | // noop on browser 35 | } 36 | 37 | error(): void { 38 | // noop on browser 39 | } 40 | 41 | notImplemented(): void { 42 | // noop on browser 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/modules/response/browser.ts: -------------------------------------------------------------------------------- 1 | export { ResponseBrowserModule } from './browser.response.module' 2 | export { ResponseService } from './browser.response.service' 3 | -------------------------------------------------------------------------------- /src/modules/response/common.ts: -------------------------------------------------------------------------------- 1 | export interface IResponseService { 2 | readonly set: (code: number, message?: string) => void 3 | readonly ok: (message?: string) => void 4 | readonly badRequest: (message?: string) => void 5 | readonly unauthorized: (message?: string) => void 6 | readonly paymentRequired: (message?: string) => void 7 | readonly forbidden: (message?: string) => void 8 | readonly notFound: (message?: string) => void 9 | readonly error: (message?: string) => void 10 | readonly notImplemented: (message?: string) => void 11 | } 12 | -------------------------------------------------------------------------------- /src/modules/response/server.response.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | import { ResponseService } from './browser.response.service' 3 | import { ServerResponseService } from './server.response.service' 4 | 5 | // tslint:disable-next-line:no-class 6 | @NgModule({ 7 | providers: [{ provide: ResponseService, useClass: ServerResponseService }] 8 | }) 9 | export class ResponseServerModule {} 10 | -------------------------------------------------------------------------------- /src/modules/response/server.response.service.ts: -------------------------------------------------------------------------------- 1 | import { RESPONSE } from '@nguniversal/express-engine/tokens' 2 | import { Inject, Injectable } from '@angular/core' 3 | import { IResponseService } from './common' 4 | import * as express from 'express' 5 | 6 | // tslint:disable:no-this 7 | // tslint:disable:no-object-mutation 8 | // tslint:disable-next-line:no-class 9 | @Injectable() 10 | export class ServerResponseService implements IResponseService { 11 | constructor(@Inject(RESPONSE) private response: express.Response) {} 12 | 13 | set(code: number, message?: string): void { 14 | this.response.statusCode = code 15 | this.response.statusMessage = message || '' 16 | } 17 | 18 | ok(): void { 19 | this.set(200) 20 | } 21 | 22 | badRequest(message = 'Bad Request'): void { 23 | this.set(400, message) 24 | } 25 | 26 | unauthorized(message = 'Unauthorized'): void { 27 | this.set(401, message) 28 | } 29 | 30 | paymentRequired(message = 'Payment Required'): void { 31 | this.set(402, message) 32 | } 33 | 34 | forbidden(message = 'Forbidden'): void { 35 | this.set(403, message) 36 | } 37 | 38 | notFound(message = 'Not Found'): void { 39 | this.set(404, message) 40 | } 41 | 42 | error(message = 'Internal Server Error'): void { 43 | this.set(500, message) 44 | } 45 | 46 | notImplemented(message = 'Not Implemented'): void { 47 | this.set(501, message) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/modules/response/server.ts: -------------------------------------------------------------------------------- 1 | export { ResponseServerModule } from './server.response.module' 2 | export { ServerResponseService } from './server.response.service' 3 | -------------------------------------------------------------------------------- /src/modules/tsconfig.aot.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "sourceMap": true, 6 | "moduleResolution": "node", 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "declaration": true, 10 | "outDir": "../../.build/modules", 11 | "lib": ["es2015", "dom"] 12 | }, 13 | "angularCompilerOptions": {} 14 | } 15 | -------------------------------------------------------------------------------- /src/modules/util/config-server.ts: -------------------------------------------------------------------------------- 1 | export function fngRawEnvironmentConfig() { 2 | return JSON.parse(process.env.FUSING_ANGULAR || '{}') as T 3 | } 4 | -------------------------------------------------------------------------------- /src/modules/util/external-link.directive.ts: -------------------------------------------------------------------------------- 1 | // import { Directive, ElementRef, Renderer2 } from '@angular/core' 2 | 3 | // // tslint:disable-next-line:no-class 4 | // @Directive({ 5 | // selector: 'a[fngExternalLink]' 6 | // }) 7 | // export class ExternalLinkDirective { 8 | // constructor(el: ElementRef, rd: Renderer2) { 9 | // const anchor = el.nativeElement as HTMLAnchorElement 10 | // rd.setAttribute(anchor, 'target', '_blank') 11 | // rd.setAttribute(anchor, 'rel', 'noopener') 12 | // } 13 | // } 14 | -------------------------------------------------------------------------------- /src/modules/util/header.service.ts: -------------------------------------------------------------------------------- 1 | import { RESPONSE } from '@nguniversal/express-engine/tokens' 2 | import { Inject, Injectable } from '@angular/core' 3 | import * as express from 'express' 4 | 5 | // tslint:disable:no-this 6 | // tslint:disable:no-object-mutation 7 | // tslint:disable-next-line:no-class 8 | @Injectable() 9 | export class HeaderService { 10 | constructor(@Inject(RESPONSE) private response: express.Response) {} 11 | getHeader(key: string): string | undefined { 12 | return this.response.getHeader(key) as string | undefined 13 | } 14 | 15 | setHeader(key: string, value: string): void { 16 | this.response.header(key, value) 17 | } 18 | 19 | setHeaders(dictionary: { readonly [key: string]: string }): void { 20 | Object.keys(dictionary).forEach(key => this.setHeader(key, dictionary[key])) 21 | } 22 | 23 | removeHeader(key: string): void { 24 | this.response.removeHeader(key) 25 | } 26 | 27 | appendHeader(key: string, value: string, delimiter = ','): void { 28 | const current = this.getHeader(key) 29 | 30 | // tslint:disable-next-line:no-if-statement 31 | if (!current) { 32 | this.setHeader(key, value) 33 | } else { 34 | const newValue = [...current.split(delimiter), value] 35 | .filter((el, i, a) => i === a.indexOf(el)) 36 | .join(delimiter) 37 | this.response.header(key, newValue) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/modules/util/monads/index.ts: -------------------------------------------------------------------------------- 1 | export { Maybe } from './maybe' 2 | -------------------------------------------------------------------------------- /src/modules/util/monads/maybe.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-this 2 | // tslint:disable-next-line:no-class 3 | export class Maybe { 4 | private constructor(private value?: T) {} 5 | 6 | static some(value: T) { 7 | // tslint:disable-next-line:no-if-statement 8 | if (!value) { 9 | throw Error('Provided value must not be empty') 10 | } 11 | return new Maybe(value) 12 | } 13 | 14 | static none() { 15 | return new Maybe(undefined) 16 | } 17 | 18 | static fromValue(value: T) { 19 | return value 20 | ? Maybe.some>(value as NonNullable) 21 | : Maybe.none>() 22 | } 23 | 24 | getOrElse(defaultValue: T) { 25 | return this.value === undefined ? defaultValue : this.value 26 | } 27 | 28 | map(f: (wrapped: T) => R): Maybe { 29 | return this.value === undefined 30 | ? Maybe.none() 31 | : Maybe.some(f(this.value)) 32 | } 33 | 34 | doIfSome(f: (wrapped: T) => R): void { 35 | this.value && f(this.value) 36 | } 37 | 38 | flattenMap(f: (wrapped: T) => Maybe): Maybe { 39 | return this.value === undefined ? Maybe.none() : f(this.value) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/modules/util/tokens.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core' 2 | 3 | export const WINDOW = new InjectionToken('fng.window') 4 | -------------------------------------------------------------------------------- /src/modules/util/user-agent.service.ts: -------------------------------------------------------------------------------- 1 | import { REQUEST } from '@nguniversal/express-engine/tokens' 2 | import { Inject, Injectable, PLATFORM_ID } from '@angular/core' 3 | import { UAParser } from 'ua-parser-js' 4 | import { Request } from 'express' 5 | import { isPlatformServer } from '@angular/common' 6 | import { WINDOW } from './tokens' 7 | 8 | export interface IUserAgentService { 9 | readonly userAgent: () => IUAParser.IResult 10 | readonly isiPhone: () => boolean 11 | readonly isiPad: () => boolean 12 | readonly isMobile: () => boolean 13 | readonly isTablet: () => boolean 14 | readonly isDesktop: () => boolean 15 | readonly isChrome: () => boolean 16 | readonly isFirefox: () => boolean 17 | readonly isSafari: () => boolean 18 | readonly isIE: () => boolean 19 | readonly isIE7: () => boolean 20 | readonly isIE8: () => boolean 21 | readonly isIE9: () => boolean 22 | readonly isIE10: () => boolean 23 | readonly isIE11: () => boolean 24 | readonly isWindows: () => boolean 25 | readonly isWindowsXP: () => boolean 26 | readonly isWindows7: () => boolean 27 | readonly isWindows8: () => boolean 28 | readonly isMac: () => boolean 29 | readonly isChromeOS: () => boolean 30 | readonly isiOS: () => boolean 31 | readonly isAndroid: () => boolean 32 | } 33 | 34 | // tslint:disable:no-class 35 | // tslint:disable:no-this 36 | @Injectable() 37 | export class UserAgentService implements IUserAgentService { 38 | constructor( 39 | @Inject(PLATFORM_ID) private platformId: any, 40 | @Inject(REQUEST) private req: Request, 41 | @Inject(WINDOW) private _window: Window 42 | ) {} 43 | 44 | public userAgent(): IUAParser.IResult { 45 | const ua = isPlatformServer(this.platformId) 46 | ? new UAParser(this.req.headers['user-agent'] as string | undefined) 47 | : new UAParser(this._window.navigator.userAgent) 48 | 49 | return ua.getResult() 50 | } 51 | 52 | isiPhone(): boolean { 53 | return this.userAgent().device.type === 'iPhone' 54 | } 55 | 56 | isiPad(): boolean { 57 | return this.userAgent().device.type === 'iPad' 58 | } 59 | 60 | isMobile(): boolean { 61 | return this.userAgent().device.type === 'mobile' 62 | } 63 | 64 | isTablet(): boolean { 65 | return this.userAgent().device.type === 'tablet' 66 | } 67 | 68 | isDesktop(): boolean { 69 | return !this.isTablet && !this.isMobile 70 | } 71 | 72 | isChrome(): boolean { 73 | return this.userAgent().browser.name === 'Chrome' 74 | } 75 | 76 | isFirefox(): boolean { 77 | return this.userAgent().browser.name === 'Firefox' 78 | } 79 | 80 | isSafari(): boolean { 81 | return this.userAgent().browser.name === 'Safari' 82 | } 83 | 84 | isIE(): boolean { 85 | return this.userAgent().browser.name === 'IE' 86 | } 87 | 88 | isIE7(): boolean { 89 | return this.isIE && this.userAgent().browser.major === '7' 90 | } 91 | 92 | isIE8(): boolean { 93 | return this.isIE && this.userAgent().browser.major === '8' 94 | } 95 | 96 | isIE9(): boolean { 97 | return this.isIE && this.userAgent().browser.major === '9' 98 | } 99 | 100 | isIE10(): boolean { 101 | return this.isIE && this.userAgent().browser.major === '10' 102 | } 103 | 104 | isIE11(): boolean { 105 | return this.isIE && this.userAgent().browser.major === '11' 106 | } 107 | 108 | isWindows(): boolean { 109 | return this.userAgent().os.name === 'Windows' 110 | } 111 | 112 | isWindowsXP(): boolean { 113 | return this.isWindows && this.userAgent().os.version === 'XP' 114 | } 115 | 116 | isWindows7(): boolean { 117 | return this.isWindows && this.userAgent().os.version === '7' 118 | } 119 | 120 | isWindows8(): boolean { 121 | return this.isWindows && this.userAgent().os.version === '8' 122 | } 123 | 124 | isMac(): boolean { 125 | return this.userAgent().os.name === 'Mac OS X' 126 | } 127 | 128 | isChromeOS(): boolean { 129 | return this.userAgent().os.name === 'Chromium OS' 130 | } 131 | 132 | isiOS(): boolean { 133 | return this.userAgent().os.name === 'iOS' 134 | } 135 | 136 | isAndroid(): boolean { 137 | return this.userAgent().os.name === 'Android' 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/modules/util/window/window-browser.module.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ModuleWithProviders, 3 | NgModule, 4 | Optional, 5 | SkipSelf 6 | } from '@angular/core' 7 | import { WINDOW } from '../tokens' 8 | import { WindowService } from './window.service' 9 | 10 | // tslint:disable-next-line:no-class 11 | @NgModule() 12 | export class WindowBrowserModule { 13 | static forRoot(): ModuleWithProviders { 14 | return { 15 | ngModule: WindowBrowserModule, 16 | providers: [{ provide: WINDOW, useValue: window }, WindowService] 17 | } 18 | } 19 | 20 | constructor( 21 | @Optional() 22 | @SkipSelf() 23 | parentModule: WindowBrowserModule 24 | ) { 25 | // tslint:disable-next-line:no-if-statement 26 | if (parentModule) 27 | throw new Error( 28 | 'WindowBrowserModule already loaded. Import in root module only.' 29 | ) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/modules/util/window/window-server.module.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ModuleWithProviders, 3 | NgModule, 4 | Optional, 5 | SkipSelf 6 | } from '@angular/core' 7 | import { WINDOW } from '../tokens' 8 | import { WindowService } from './window.service' 9 | 10 | // tslint:disable-next-line:no-class 11 | @NgModule() 12 | export class WindowServerModule { 13 | static forRoot(windowObject?: any): ModuleWithProviders { 14 | return { 15 | ngModule: WindowServerModule, 16 | providers: [ 17 | { provide: WINDOW, useValue: windowObject || {} }, 18 | WindowService 19 | ] 20 | } 21 | } 22 | 23 | constructor( 24 | @Optional() 25 | @SkipSelf() 26 | parentModule: WindowServerModule 27 | ) { 28 | // tslint:disable-next-line:no-if-statement 29 | if (parentModule) 30 | throw new Error( 31 | 'WindowServerModule already loaded. Import in root module only.' 32 | ) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/modules/util/window/window.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@angular/core' 2 | import { WINDOW } from '../tokens' 3 | 4 | export interface IWindowService { 5 | readonly window: () => Window & T 6 | } 7 | 8 | // tslint:disable:no-class 9 | // tslint:disable:no-this 10 | @Injectable() 11 | export class WindowService implements IWindowService { 12 | constructor(@Inject(WINDOW) private _window: any) {} 13 | 14 | public window(): Window & T { 15 | return this._window as Window & T 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/templates/component/component.ts.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patrickmichalina/fusing-angular-cli/f9331d3d94dbc14d8e0d21ace554af5bbd7ae7e9/src/templates/component/component.ts.txt -------------------------------------------------------------------------------- /src/templates/core/app/app.component.html.txt: -------------------------------------------------------------------------------- 1 |

Your New Angular App

2 | -------------------------------------------------------------------------------- /src/templates/core/app/app.component.scss.txt: -------------------------------------------------------------------------------- 1 | /* apply global styles here */ 2 | 3 | html { 4 | 5 | } -------------------------------------------------------------------------------- /src/templates/core/app/app.component.spec.ts.txt: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing' 2 | import { APP_BASE_HREF } from '@angular/common' 3 | import { Component } from '@angular/core' 4 | import { AppBrowserModule } from '../browser/app.browser.module' 5 | import { RouterModule } from '../../node_modules/@angular/router' 6 | import { AppComponent } from './app.component' 7 | 8 | @Component({ 9 | selector: 'test-cmp', 10 | template: '' 11 | }) 12 | class TestComponent { } 13 | 14 | describe('App component', () => { 15 | let fixture: ComponentFixture 16 | 17 | beforeEach(async(() => { 18 | TestBed.configureTestingModule({ 19 | imports: [ 20 | AppBrowserModule, 21 | RouterModule.forRoot([ 22 | { path: '', component: AppComponent } 23 | ]) 24 | ], 25 | declarations: [TestComponent], 26 | providers: [ 27 | { provide: APP_BASE_HREF, useValue: '/' } 28 | ] 29 | }).compileComponents() 30 | })) 31 | 32 | beforeEach(async(() => { 33 | fixture = TestBed.createComponent(TestComponent) 34 | })) 35 | 36 | afterEach(async(() => { 37 | TestBed.resetTestingModule() 38 | })) 39 | 40 | it('should build without a problem', async(() => { 41 | expect(fixture.nativeElement).toBeTruthy() 42 | expect(fixture.nativeElement).toMatchSnapshot() 43 | })) 44 | }) -------------------------------------------------------------------------------- /src/templates/core/app/app.component.ts.txt: -------------------------------------------------------------------------------- 1 | import { Component, ChangeDetectionStrategy, ViewEncapsulation } from '@angular/core' 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.css'], 7 | changeDetection: ChangeDetectionStrategy.OnPush, 8 | encapsulation: ViewEncapsulation.None 9 | }) 10 | export class AppComponent { 11 | } -------------------------------------------------------------------------------- /src/templates/core/app/app.module.ts.txt: -------------------------------------------------------------------------------- 1 | import { AppComponent } from './app.component' 2 | import { NgModule } from '@angular/core' 3 | import { TransferHttpCacheModule } from '@nguniversal/common' 4 | import { BrowserModule } from '@angular/platform-browser' 5 | import { AppRoutingModule } from './app.routing.module' 6 | import { SharedModule } from './app.shared.module' 7 | 8 | @NgModule({ 9 | declarations: [AppComponent], 10 | exports: [AppComponent], 11 | imports: [ 12 | AppRoutingModule, 13 | TransferHttpCacheModule, 14 | SharedModule.forRoot(), 15 | BrowserModule.withServerTransition({ appId: 'app-root' }), 16 | ] 17 | }) 18 | export class AppModule { } 19 | -------------------------------------------------------------------------------- /src/templates/core/app/app.routing.module.ts.txt: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | import { RouterModule, Routes } from '@angular/router' 3 | 4 | //export function homeModule() { 5 | // return import('./home/home.module').then(m => m.HomeModule) 6 | //} 7 | 8 | export const routes: Routes = [ 9 | // { path: '', loadChildren: homeModule }, 10 | ] 11 | 12 | @NgModule({ 13 | imports: [RouterModule.forRoot(routes, { initialNavigation: 'enabled' })], 14 | exports: [RouterModule] 15 | }) 16 | export class AppRoutingModule {} 17 | -------------------------------------------------------------------------------- /src/templates/core/app/app.shared.module.ts.txt: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common' 2 | import { RouterModule } from '@angular/router' 3 | import { HttpClientModule } from '@angular/common/http' 4 | import { NgModule, ModuleWithProviders } from '@angular/core' 5 | 6 | @NgModule({ 7 | imports: [ 8 | HttpClientModule, 9 | RouterModule, 10 | CommonModule, 11 | ], 12 | exports: [ 13 | CommonModule, 14 | RouterModule, 15 | HttpClientModule 16 | ] 17 | }) 18 | export class SharedModule { 19 | static forRoot(): ModuleWithProviders { 20 | return { 21 | ngModule: SharedModule 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/templates/core/app/favicon.svg.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/templates/core/app/home.component.ts.txt: -------------------------------------------------------------------------------- 1 | import { Component, ChangeDetectionStrategy } from '@angular/core' 2 | 3 | @Component({ 4 | selector: 'ng-home', 5 | template: `HOME COMPONENT`, 6 | changeDetection: ChangeDetectionStrategy.OnPush 7 | }) 8 | export class HomeComponent { 9 | } -------------------------------------------------------------------------------- /src/templates/core/app/index.pug.txt: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | head 4 | title= pageTitle 5 | base(href="/") 6 | meta(charset="utf-8") 7 | meta(name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0") 8 | != faviconMeta 9 | body 10 | app-root 11 | | $bundles 12 | if isLocalDev 13 | script(src="/reload/reload.js") -------------------------------------------------------------------------------- /src/templates/core/app/index.ts: -------------------------------------------------------------------------------- 1 | import * as appModuleTemplate from './app.module.ts.txt' 2 | import * as appComponentTemplate from './app.component.ts.txt' 3 | import * as appSharedModuleTemplate from './app.shared.module.ts.txt' 4 | import * as appRoutingModuleTemplate from './app.routing.module.ts.txt' 5 | import * as appComponentCssTemplate from './app.component.scss.txt' 6 | import * as appComponentHtmlTemplate from './app.component.html.txt' 7 | import * as homeComponentTemplate from './home.component.ts.txt' 8 | import * as appIndex from './index.pug.txt' 9 | import * as favicon from './favicon.svg.txt' 10 | import * as ngsw from './ngsw.json.txt' 11 | 12 | export { 13 | appModuleTemplate, 14 | appComponentTemplate, 15 | appSharedModuleTemplate, 16 | appRoutingModuleTemplate, 17 | homeComponentTemplate, 18 | appComponentCssTemplate, 19 | appComponentHtmlTemplate, 20 | appIndex, 21 | favicon, 22 | ngsw 23 | } 24 | -------------------------------------------------------------------------------- /src/templates/core/app/ngsw.json.txt: -------------------------------------------------------------------------------- 1 | { 2 | "index": "/index.html", 3 | "assetGroups": [ 4 | { 5 | "name": "app", 6 | "installMode": "prefetch", 7 | "resources": { 8 | "files": ["/index.html", "/ngsw-worker.js", "/js/**/**", "!/js/**/*.(br|gzip)"] 9 | } 10 | }, 11 | { 12 | "name": "assets", 13 | "installMode": "lazy", 14 | "updateMode": "lazy", 15 | "resources": { 16 | "files": ["/assets/**/**"] 17 | } 18 | }, 19 | { 20 | "name": "fonts", 21 | "resources": { 22 | "urls": [ 23 | "https://fonts.googleapis.com/**", 24 | "https://fonts.gstatic.com/**" 25 | ] 26 | } 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /src/templates/core/assets/index.ts: -------------------------------------------------------------------------------- 1 | import * as robots from './robots.txt' 2 | 3 | export { robots } 4 | -------------------------------------------------------------------------------- /src/templates/core/assets/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: -------------------------------------------------------------------------------- /src/templates/core/browser/app.browser.entry.aot.ts.txt: -------------------------------------------------------------------------------- 1 | import { platformBrowser } from '@angular/platform-browser' 2 | import { AppBrowserModuleNgFactory } from './app.browser.module.ngfactory' 3 | 4 | function domContentLoadedHandler() { 5 | platformBrowser() 6 | .bootstrapModuleFactory(AppBrowserModuleNgFactory) 7 | .catch(console.log) 8 | } 9 | 10 | document.addEventListener('DOMContentLoaded', domContentLoadedHandler) 11 | -------------------------------------------------------------------------------- /src/templates/core/browser/app.browser.entry.jit.ts.txt: -------------------------------------------------------------------------------- 1 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic' 2 | import { AppBrowserModule } from './app.browser.module' 3 | 4 | function domContentLoadedHandler() { 5 | platformBrowserDynamic() 6 | .bootstrapModule(AppBrowserModule) 7 | .catch(console.log) 8 | } 9 | 10 | document.addEventListener('DOMContentLoaded', domContentLoadedHandler) 11 | -------------------------------------------------------------------------------- /src/templates/core/browser/app.browser.module.ts.txt: -------------------------------------------------------------------------------- 1 | import { AppModule } from '../app/app.module' 2 | import { NgModule } from '@angular/core' 3 | import { AppComponent } from '../app/app.component' 4 | import { FusingAngularBrowserModule } from 'fusing-angular-cli/.build/modules/src/modules/fusing-angular/browser' 5 | 6 | @NgModule({ 7 | imports: [ 8 | FusingAngularBrowserModule, 9 | AppModule 10 | ], 11 | exports: [AppModule], 12 | bootstrap: [AppComponent] 13 | }) 14 | export class AppBrowserModule { } 15 | 16 | -------------------------------------------------------------------------------- /src/templates/core/browser/index.ts: -------------------------------------------------------------------------------- 1 | import * as browserModuleTemplate from './app.browser.module.ts.txt' 2 | import * as browserJitEntryTemplate from './app.browser.entry.jit.ts.txt' 3 | import * as browserAotEntryTemplate from './app.browser.entry.aot.ts.txt' 4 | 5 | export { 6 | browserModuleTemplate, 7 | browserJitEntryTemplate, 8 | browserAotEntryTemplate 9 | } 10 | -------------------------------------------------------------------------------- /src/templates/core/server/index.ts: -------------------------------------------------------------------------------- 1 | import * as serverTemplate from './server.ts.txt' 2 | import * as serverAppTemplate from './server.app.ts.txt' 3 | import * as serverModuleTemplate from './server.angular.module.ts.txt' 4 | 5 | export { 6 | serverTemplate, 7 | serverAppTemplate, 8 | serverModuleTemplate 9 | } -------------------------------------------------------------------------------- /src/templates/core/server/server.angular.module.ts.txt: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | import { AppModule } from '../app/app.module' 3 | import { AppComponent } from '../app/app.component' 4 | import { FusingAngularServerModule } from 'fusing-angular-cli/.build/modules/src/modules/fusing-angular/server' 5 | 6 | @NgModule({ 7 | imports: [ 8 | FusingAngularServerModule, 9 | AppModule 10 | ], 11 | exports: [AppModule], 12 | bootstrap: [AppComponent] 13 | }) 14 | export class AppServerModule { 15 | } 16 | -------------------------------------------------------------------------------- /src/templates/core/server/server.app.ts.txt: -------------------------------------------------------------------------------- 1 | import * as express from 'express' 2 | import * as cookieParser from 'cookie-parser' 3 | import * as lru from 'lru-cache' 4 | import { resolve } from 'path' 5 | import { ngExpressEngine } from '@nguniversal/express-engine' 6 | import { AppServerModule } from './server.angular.module' 7 | import { stat, createReadStream } from 'fs' 8 | import { LRU_CACHE } from 'fusing-angular-cli/.build/modules/src/modules/firebase' 9 | const ms = require('ms') 10 | 11 | const environment = JSON.parse(process.env.FUSING_ANGULAR || '{}') 12 | const isLocalDevelopmentServer = environment.ENV === 'dev' 13 | const LRU = new lru({ 14 | max: 500, 15 | maxAge: 1000 * 30 16 | }) 17 | 18 | // const xhr2 = require('xhr2') 19 | // tslint:disable-next-line:no-object-mutation 20 | // xhr2.prototype._restrictedHeaders.cookie = false 21 | 22 | const base = '' 23 | const expressApp = express() 24 | const dir = resolve(base, '.dist') 25 | const publicDir = `${dir}/public` 26 | 27 | require('reload')(expressApp) 28 | 29 | function seconds(time: string) { 30 | return ms(time) / 1000 31 | } 32 | 33 | function staticCacheOptionsGen(time: string, disableCacheForLocalDev = true) { 34 | return { 35 | index: false, 36 | setHeaders: (res: express.Response) => { 37 | res.setHeader( 38 | 'Expires', 39 | disableCacheForLocalDev 40 | ? new Date(Date.now() + seconds(time)).toUTCString() 41 | : new Date(Date.now()).toUTCString() 42 | ) 43 | res.setHeader( 44 | 'Cache-Control', 45 | disableCacheForLocalDev ? 'max-age=0' : `max-age=${seconds(time)}` 46 | ) 47 | } 48 | } 49 | } 50 | 51 | expressApp.use(cookieParser()) 52 | 53 | expressApp.set('x-powered-by', false) 54 | expressApp.set('etag', false) 55 | expressApp.set('view engine', 'html') 56 | expressApp.set('views', publicDir) 57 | 58 | expressApp.engine('html', ngExpressEngine({ 59 | bootstrap: AppServerModule, 60 | providers: [ 61 | { 62 | provide: LRU_CACHE, useValue: LRU 63 | } 64 | ] 65 | })) 66 | 67 | function returnBrEncoding(availableEncodings: string[]) { 68 | return availableEncodings.some(a => a === 'br') 69 | } 70 | 71 | function returnGzipEncoding(availableEncodings: string[]) { 72 | return availableEncodings.some(a => a === 'gzip') 73 | } 74 | 75 | function writeJsHeaders(res: express.Response, contentLength: number, type: string) { 76 | res.writeHead(200, { 77 | "Content-Type": "application/javascript", 78 | "Content-Encoding": type, 79 | "Content-Length": contentLength, 80 | "Cache-Control": isLocalDevelopmentServer 81 | ? "public, no-cache" 82 | : `public, max-age=${seconds('180d')}, s-maxage=${seconds('180d')}` 83 | }) 84 | } 85 | 86 | function checkReturnJsFile(filePath: string, res: express.Response, encoding: string, append = true) { 87 | const path = append 88 | ? `${filePath}.${encoding}` 89 | : filePath 90 | stat(path, (err, stats) => { 91 | if (err) { 92 | res.writeHead(404) 93 | res.end() 94 | } else { 95 | writeJsHeaders(res, stats.size, encoding) 96 | createReadStream(path).pipe(res) 97 | } 98 | }) 99 | } 100 | 101 | expressApp.use('/robots.txt', express.static(`${publicDir}/assets/robots.txt`, staticCacheOptionsGen('30d'))) 102 | expressApp.use('/assets', express.static(`${publicDir}/assets`, staticCacheOptionsGen('30d'))) 103 | expressApp.use('/favicon.ico', express.static(`${publicDir}/assets/favicons/favicon.ico`, staticCacheOptionsGen('30d'))) 104 | expressApp.use('/manifest.json', express.static(`${publicDir}/assets/favicons/manifest.json`, staticCacheOptionsGen('30d'))) 105 | expressApp.use('/js/ngsw.json', express.static(`${publicDir}/ngsw.json`, staticCacheOptionsGen('30d'))) 106 | 107 | expressApp.get('/js/**', (req, res) => { 108 | const encodings = (req.get('Accept-Encoding') || '').split(',').map(a => a.trim()) 109 | const filePath = resolve(`${publicDir}${req.path}`) 110 | 111 | if (isLocalDevelopmentServer) { 112 | checkReturnJsFile(filePath, res, 'identity', false) 113 | } else if (returnBrEncoding(encodings)) { 114 | checkReturnJsFile(filePath, res, 'br') 115 | } else if (returnGzipEncoding(encodings)) { 116 | checkReturnJsFile(filePath, res, 'gzip') 117 | } else { 118 | checkReturnJsFile(filePath, res, 'identity', false) 119 | } 120 | }) 121 | 122 | expressApp.get('**', (req, res) => { 123 | return res.render('index', { 124 | req, 125 | res 126 | }) 127 | }) 128 | 129 | export { expressApp } -------------------------------------------------------------------------------- /src/templates/core/server/server.ts.txt: -------------------------------------------------------------------------------- 1 | import { createServer } from 'http' 2 | import { expressApp } from './server.app' 3 | 4 | const config = JSON.parse(process.env.FUSING_ANGULAR || '{}') 5 | const server = createServer(expressApp) 6 | 7 | server.listen(config.PORT, () => { 8 | console.log(`Angular Universal server listening on port: ${config.PORT}`) 9 | }) 10 | -------------------------------------------------------------------------------- /src/templates/declarations.ts.txt: -------------------------------------------------------------------------------- 1 | declare module "*.json" { 2 | const value: any; 3 | export default value; 4 | } 5 | 6 | declare module "*./app.browser.module.ngfactory" { 7 | const value: any; 8 | export { AppBrowserModuleNgFactory }; 9 | } 10 | -------------------------------------------------------------------------------- /src/templates/env.txt: -------------------------------------------------------------------------------- 1 | FNG_ENV=dev 2 | FNG_PORT=5000 3 | $FIREBASE 4 | $GOOGLE_ANALYTICS 5 | $GOOGLE_SITE_VERIFICATION -------------------------------------------------------------------------------- /src/templates/favicon.ts: -------------------------------------------------------------------------------- 1 | export const FAVICON_DEFAULTS = { 2 | source: 'src/app/favicon.svg', 3 | output: 'src/assets/favicons', 4 | config: { 5 | appName: null, 6 | appDescription: null, 7 | developerName: null, 8 | developerURL: null, 9 | lang: 'en-US', 10 | background: '#fff', 11 | theme_color: '#fff', 12 | display: 'standalone', 13 | orientation: 'any', 14 | start_url: '/' 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/templates/fusebox.ts: -------------------------------------------------------------------------------- 1 | export const FUSEBOX_DEFAULTS = { 2 | verbose: false, 3 | browser: { 4 | outputDir: '.dist/public/js', 5 | browserModule: 'src/browser/app.browser.entry.jit.ts', 6 | aotBrowserModule: '.aot/src/browser/app.browser.entry.aot.js', 7 | prod: { 8 | uglify: true, 9 | treeshake: true 10 | } 11 | }, 12 | server: { 13 | outputDir: '.dist', 14 | serverModule: 'src/server/server.ts' 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/templates/gitignore.txt: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | .DS_Store 8 | dist 9 | .env 10 | .fusebox 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | 24 | # nyc test coverage 25 | .nyc_output 26 | 27 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 28 | .grunt 29 | 30 | # Bower dependency directory (https://bower.io/) 31 | bower_components 32 | 33 | # node-waf configuration 34 | .lock-wscript 35 | 36 | # Compiled binary addons (http://nodejs.org/api/addons.html) 37 | build/Release 38 | 39 | # Dependency directories 40 | node_modules/ 41 | jspm_packages/ 42 | 43 | # Typescript v1 declaration files 44 | typings/ 45 | 46 | # Optional npm cache directory 47 | .npm 48 | 49 | # Optional eslint cache 50 | .eslintcache 51 | 52 | # Optional REPL history 53 | .node_repl_history 54 | 55 | # Output of 'npm pack' 56 | *.tgz 57 | 58 | # Yarn Integrity file 59 | .yarn-integrity 60 | 61 | # dotenv environment variables file 62 | .env 63 | 64 | # thumbnails 65 | .DS_Store 66 | 67 | # build output 68 | .dist/ 69 | .build/ 70 | dist/ 71 | build/ 72 | 73 | # test output 74 | test-report.xml 75 | test-results.xml 76 | 77 | # Angular 78 | ngc 79 | aot 80 | .ngc 81 | .aot 82 | 83 | .serverless 84 | src/**/*.css -------------------------------------------------------------------------------- /src/templates/route-module/component.ts.txt: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core' 2 | 3 | @Component({ 4 | selector: 'pm-about', 5 | templateUrl: './about.component.html', 6 | styleUrls: ['./about.component.css'], 7 | changeDetection: ChangeDetectionStrategy.OnPush 8 | }) 9 | export class AboutComponent { 10 | } -------------------------------------------------------------------------------- /src/templates/route-module/module.ts.txt: -------------------------------------------------------------------------------- 1 | import { AboutRoutingModule } from './about-routing.module' 2 | import { AboutComponent } from './about.component' 3 | import { NgModule } from '@angular/core' 4 | import { SharedModule } from '../shared/shared.module' 5 | import { MatButtonModule } from '@angular/material' 6 | 7 | @NgModule({ 8 | imports: [AboutRoutingModule, SharedModule, MatButtonModule], 9 | declarations: [AboutComponent], 10 | exports: [AboutComponent] 11 | }) 12 | export class AboutModule {} 13 | -------------------------------------------------------------------------------- /src/templates/route-module/routing.module.ts.txt: -------------------------------------------------------------------------------- 1 | import { AboutComponent } from './about.component' 2 | import { NgModule } from '@angular/core' 3 | import { RouterModule } from '@angular/router' 4 | 5 | @NgModule({ 6 | imports: [ 7 | RouterModule.forChild([ 8 | { 9 | path: '', 10 | component: AboutComponent 11 | } 12 | ]) 13 | ], 14 | exports: [RouterModule] 15 | }) 16 | export class AboutRoutingModule {} -------------------------------------------------------------------------------- /src/templates/tsconfig.aot.json.txt: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "isolatedModules": false, 7 | "experimentalDecorators": true, 8 | "emitDecoratorMetadata": true, 9 | "declaration": false, 10 | "noImplicitAny": true, 11 | "noImplicitUseStrict": false, 12 | "strictNullChecks": true, 13 | "noEmitHelpers": false, 14 | "noLib": false, 15 | "noUnusedLocals": true, 16 | "outDir": ".aot", 17 | "allowSyntheticDefaultImports": false, 18 | "sourceMap": true, 19 | "lib": ["es6", "dom"], 20 | "skipLibCheck": true, 21 | "skipDefaultLibCheck": true 22 | }, 23 | "include": [ 24 | "src" 25 | ], 26 | "exclude": [ 27 | "src/browser/app.browser.entry.jit.ts" 28 | ], 29 | "angularCompilerOptions": { 30 | "genDir": ".aot", 31 | "skipMetadataEmit": true, 32 | "preserveWhitespaces": false, 33 | "enableIvy": false 34 | } 35 | } -------------------------------------------------------------------------------- /src/templates/tsconfig.json.txt: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "isolatedModules": false, 7 | "experimentalDecorators": true, 8 | "emitDecoratorMetadata": true, 9 | "declaration": false, 10 | "noImplicitAny": true, 11 | "noImplicitUseStrict": false, 12 | "strictNullChecks": true, 13 | "noEmitHelpers": false, 14 | "noLib": false, 15 | "noUnusedLocals": true, 16 | "outDir": "dist/", 17 | "allowSyntheticDefaultImports": false, 18 | "sourceMap": true, 19 | "lib": ["es6", "dom"], 20 | "skipLibCheck": true, 21 | "skipDefaultLibCheck": true 22 | }, 23 | "exclude": [ 24 | "./.vscode", 25 | "./.fusebox", 26 | "./.ngc", 27 | "./dist", 28 | "./node_modules", 29 | "./coverage" 30 | ] 31 | } -------------------------------------------------------------------------------- /src/templates/tslint.json.txt: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "newline-per-chained-call": false, 4 | "no-unused-variable": true, 5 | "prefer-object-spread": true, 6 | "no-var-keyword": true, 7 | "no-let": true, 8 | "no-parameter-reassignment": true, 9 | "readonly-keyword": true, 10 | "readonly-array": true, 11 | "no-object-mutation": true, 12 | "no-delete": true, 13 | "no-method-signature": false, 14 | "typedef": false, 15 | "indent": [true, "spaces", 2], 16 | "space-within-parens": [false], 17 | "object-literal-key-quotes": [false], 18 | "semicolon": [true, "never"], 19 | "align": false, 20 | "curly": false, 21 | "member-ordering": false, 22 | "member-access": [false, "no-public"], 23 | "newline-before-return": false, 24 | "array-type": false 25 | } 26 | } -------------------------------------------------------------------------------- /src/templates/unit-tests/app-testing.module.ts.txt: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | import { SharedModule } from '../client/app/shared/shared.module' 3 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations' 4 | 5 | @NgModule({ 6 | imports: [ 7 | SharedModule, 8 | BrowserAnimationsModule 9 | ], 10 | providers: [] 11 | }) 12 | export class AppTestingModule { } 13 | -------------------------------------------------------------------------------- /src/templates/unit-tests/jest/AngularSnapshotSerializer.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const printAttributes = (val, attributes, print, indent, colors, opts) => { 4 | return attributes 5 | .sort() 6 | .map(attribute => { 7 | return ( 8 | opts.spacing + 9 | indent(colors.prop.open + attribute + colors.prop.close + '=') + 10 | colors.value.open + 11 | (val.componentInstance[attribute] && 12 | val.componentInstance[attribute].constructor 13 | ? '{[Function ' + 14 | val.componentInstance[attribute].constructor.name + 15 | ']}' 16 | : `"${val.componentInstance[attribute]}"`) + 17 | colors.value.close 18 | ) 19 | }) 20 | .join('') 21 | } 22 | 23 | const print = (val, print, indent, opts, colors) => { 24 | let result = '' 25 | let componentAttrs = '' 26 | 27 | const componentName = val.componentRef._elDef.element.name 28 | const nodes = (val.componentRef._view.nodes || []) 29 | .filter(node => node && node.hasOwnProperty('renderElement')) 30 | .map(node => 31 | Array.from(node.renderElement.childNodes) 32 | .map(print) 33 | .join('') 34 | ) 35 | .join(opts.edgeSpacing) 36 | 37 | const attributes = Object.keys(val.componentInstance) 38 | 39 | if (attributes.length) { 40 | componentAttrs += printAttributes( 41 | val, 42 | attributes, 43 | print, 44 | indent, 45 | colors, 46 | opts 47 | ) 48 | } 49 | 50 | return ( 51 | '<' + 52 | componentName + 53 | componentAttrs + 54 | (componentAttrs.length ? '\n' : '') + 55 | '>\n' + 56 | indent(nodes) + 57 | '\n' 60 | ) 61 | } 62 | 63 | const test = val => 64 | val !== undefined && 65 | val !== null && 66 | typeof val === 'object' && 67 | Object.prototype.hasOwnProperty.call(val, 'componentRef') 68 | 69 | module.exports = { 70 | print: print, 71 | test: test 72 | } 73 | -------------------------------------------------------------------------------- /src/templates/unit-tests/jest/HTMLCommentSerializer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. 3 | * 4 | * This source code is licensed under the BSD-style license found in the 5 | * LICENSE file in the root directory of this source tree. An additional grant 6 | * of patent rights can be found in the PATENTS file in the same directory. 7 | * 8 | */ 9 | 10 | 'use strict' 11 | 12 | const HTML_ELEMENT_REGEXP = /Comment/ 13 | const test = value => 14 | value !== undefined && 15 | value !== null && 16 | value.nodeType === 8 && 17 | value.constructor !== undefined && 18 | HTML_ELEMENT_REGEXP.test(value.constructor.name) 19 | 20 | const print = () => '' 21 | 22 | module.exports = { 23 | print: print, 24 | test: test 25 | } 26 | -------------------------------------------------------------------------------- /src/templates/unit-tests/jest/jest.setup.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | require('core-js/es6/reflect') 4 | require('core-js/es7/reflect') 5 | require('zone.js/dist/zone.js') 6 | require('zone.js/dist/proxy.js') 7 | require('zone.js/dist/sync-test') 8 | require('zone.js/dist/async-test') 9 | require('zone.js/dist/fake-async-test') 10 | 11 | /** 12 | * Patch Jest's describe/test/beforeEach/afterEach functions so test code 13 | * always runs in a testZone (ProxyZone). 14 | */ 15 | 16 | if (Zone === undefined) { 17 | throw new Error('Missing: Zone (zone.js)') 18 | } 19 | if (jest === undefined) { 20 | throw new Error( 21 | 'Missing: jest.\n' + 22 | 'This patch must be included in a script called with ' + 23 | '`setupTestFrameworkScriptFile` in Jest config.' 24 | ) 25 | } 26 | if (jest['__zone_patch__'] === true) { 27 | throw new Error("'jest' has already been patched with 'Zone'.") 28 | } 29 | 30 | jest['__zone_patch__'] = true 31 | const SyncTestZoneSpec = Zone['SyncTestZoneSpec'] 32 | const ProxyZoneSpec = Zone['ProxyZoneSpec'] 33 | 34 | if (SyncTestZoneSpec === undefined) { 35 | throw new Error('Missing: SyncTestZoneSpec (zone.js/dist/sync-test)') 36 | } 37 | if (ProxyZoneSpec === undefined) { 38 | throw new Error('Missing: ProxyZoneSpec (zone.js/dist/proxy.js)') 39 | } 40 | 41 | const env = global 42 | const ambientZone = Zone.current 43 | 44 | // Create a synchronous-only zone in which to run `describe` blocks in order to 45 | // raise an error if any asynchronous operations are attempted 46 | // inside of a `describe` but outside of a `beforeEach` or `it`. 47 | const syncZone = ambientZone.fork(new SyncTestZoneSpec('jest.describe')) 48 | function wrapDescribeInZone(describeBody) { 49 | return () => syncZone.run(describeBody, null, arguments) 50 | } 51 | 52 | // Create a proxy zone in which to run `test` blocks so that the tests function 53 | // can retroactively install different zones. 54 | const testProxyZone = ambientZone.fork(new ProxyZoneSpec()) 55 | function wrapTestInZone(testBody) { 56 | if (testBody === undefined) { 57 | return 58 | } 59 | return testBody.length === 0 60 | ? () => testProxyZone.run(testBody, null) 61 | : done => testProxyZone.run(testBody, null, [done]) 62 | } 63 | 64 | ;['xdescribe', 'fdescribe', 'describe'].forEach(methodName => { 65 | const originaljestFn = env[methodName] 66 | env[methodName] = function(description, specDefinitions) { 67 | return originaljestFn.call( 68 | this, 69 | description, 70 | wrapDescribeInZone(specDefinitions) 71 | ) 72 | } 73 | if (methodName === 'describe') { 74 | env[methodName].only = env['fdescribe'] 75 | env[methodName].skip = env['xdescribe'] 76 | } 77 | }) 78 | ;['xit', 'fit', 'test', 'it'].forEach(methodName => { 79 | const originaljestFn = env[methodName] 80 | env[methodName] = function(description, specDefinitions, timeout) { 81 | arguments[1] = wrapTestInZone(specDefinitions) 82 | return originaljestFn.apply(this, arguments) 83 | } 84 | if (methodName === 'test' || methodName === 'it') { 85 | env[methodName].only = env['fit'] 86 | env[methodName].skip = env['xit'] 87 | } 88 | }) 89 | ;['beforeEach', 'afterEach', 'beforeAll', 'afterAll'].forEach(methodName => { 90 | const originaljestFn = env[methodName] 91 | env[methodName] = function(specDefinitions, timeout) { 92 | arguments[0] = wrapTestInZone(specDefinitions) 93 | return originaljestFn.apply(this, arguments) 94 | } 95 | }) 96 | 97 | // const AngularSnapshotSerializer = require('./AngularSnapshotSerializer'); 98 | // const HTMLCommentSerializer = require('./HTMLCommentSerializer'); 99 | const getTestBed = require('@angular/core/testing').getTestBed 100 | const BrowserDynamicTestingModule = require('@angular/platform-browser-dynamic/testing') 101 | .BrowserDynamicTestingModule 102 | const platformBrowserDynamicTesting = require('@angular/platform-browser-dynamic/testing') 103 | .platformBrowserDynamicTesting 104 | 105 | getTestBed().initTestEnvironment( 106 | BrowserDynamicTestingModule, 107 | platformBrowserDynamicTesting() 108 | ) 109 | 110 | const mock = () => { 111 | let storage = {} 112 | return { 113 | getItem: key => (key in storage ? storage[key] : null), 114 | setItem: (key, value) => (storage[key] = value || ''), 115 | removeItem: key => delete storage[key], 116 | clear: () => (storage = {}) 117 | } 118 | } 119 | // Object.defineProperty(window, 'Hammer', { value: {} }); 120 | Object.defineProperty(window, 'CSS', { value: mock() }) 121 | Object.defineProperty(window, 'matchMedia', { 122 | value: jest.fn(() => ({ matches: true })) 123 | }) 124 | Object.defineProperty(window, 'localStorage', { value: mock() }) 125 | Object.defineProperty(window, 'sessionStorage', { value: mock() }) 126 | Object.defineProperty(window, 'getComputedStyle', { 127 | value: () => { 128 | return { 129 | display: 'none', 130 | appearance: ['-webkit-appearance'] 131 | } 132 | } 133 | }) 134 | 135 | // For Angular Material 136 | Object.defineProperty(document.body.style, 'transform', { 137 | value: () => { 138 | return { 139 | enumerable: true, 140 | configurable: true 141 | } 142 | } 143 | }) 144 | window.Hammer = require('hammerjs') 145 | -------------------------------------------------------------------------------- /src/templates/unit-tests/jest/preprocessor.js: -------------------------------------------------------------------------------- 1 | const process = require('ts-jest/preprocessor.js').process 2 | const TEMPLATE_URL_REGEX = /templateUrl\s*:\s*('|")(\.\/){0,}(.*)('|")/g 3 | const STYLE_URLS_REGEX = /styleUrls\s*:\s*\[[^\]]*\]/g 4 | const ESCAPE_TEMPLATE_REGEX = /(\${|\`)/g 5 | 6 | module.exports.process = (src, path, config, transformOptions) => { 7 | if (path.endsWith('.html')) { 8 | src = src.replace(ESCAPE_TEMPLATE_REGEX, '\\$1') 9 | } 10 | src = src 11 | .replace(TEMPLATE_URL_REGEX, 'template: require($1./$3$4)') 12 | .replace(STYLE_URLS_REGEX, 'styles: []') 13 | return process(src, path, config, transformOptions) 14 | } 15 | -------------------------------------------------------------------------------- /src/templates/unit-tests/jest/vs-code.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "__TRANSFORM_HTML__": true, 4 | "ts-jest": { 5 | "tsConfigFile": "tsconfig.json" 6 | } 7 | }, 8 | "transform": { 9 | "^.+\\.(ts|js|html)$": 10 | "/node_modules/fusing-angular-cli/.build/jest/preprocessor.js" 11 | }, 12 | "testMatch": [ 13 | "**/__tests__/**/*.+(ts|js)?(x)", 14 | "**/+(*.)+(spec|test).+(ts|js)?(x)" 15 | ], 16 | "moduleFileExtensions": ["ts", "js", "html", "json"], 17 | "setupTestFrameworkScriptFile": 18 | "/node_modules/fusing-angular-cli/.build/jest/jest.setup.js", 19 | "snapshotSerializers": [ 20 | "/node_modules/fusing-angular-cli/.build/jest/AngularSnapshotSerializer.js", 21 | "/node_modules/fusing-angular-cli/.build/jest/HTMLCommentSerializer.js" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /src/templates/vscode/launch.json.txt: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "name": "Jest", 8 | "program": "${workspaceFolder}/node_modules/jest/bin/jest", 9 | "args": ["--config=${workspaceFolder}/node_modules/fusing-angular-cli/.build/jest/vs-code.config.json"], 10 | "console": "integratedTerminal", 11 | "internalConsoleOptions": "neverOpen" 12 | }, 13 | { 14 | "type": "node", 15 | "request": "launch", 16 | "name": "Jest (Current File)", 17 | "program": "${workspaceFolder}/node_modules/jest/bin/jest", 18 | "args": ["${file}", "--config=test.json/node_modules/fusing-angular-cli/.build/jest/vs-code.config.json"], 19 | "console": "integratedTerminal", 20 | "internalConsoleOptions": "neverOpen" 21 | } 22 | ] 23 | } -------------------------------------------------------------------------------- /src/templates/vscode/settings.json.txt: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "npm.enableScriptExplorer": true, 4 | "typescript.tsdk": "node_modules/typescript/lib", 5 | "files.exclude": { 6 | "**/*.js.map": true, 7 | "**/*.css": { 8 | "when": "$(basename).scss" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/utilities/clear.ts: -------------------------------------------------------------------------------- 1 | export default function clearTerminal() { 2 | process.stdout.write('\x1B[2J\x1B[0f') 3 | } 4 | -------------------------------------------------------------------------------- /src/utilities/create-folder.ts: -------------------------------------------------------------------------------- 1 | import { logError, log, logFileCreated } from './log' 2 | import { catchError, tap, take, flatMap } from 'rxjs/operators' 3 | import { pathExists_, mkDir_ } from './rx-fs' 4 | import { empty } from 'rxjs' 5 | 6 | function handleFileCollision(dirPath: string) { 7 | logError(`\nDirectory ${dirPath} alreay exists\n`) 8 | return empty() 9 | } 10 | 11 | function handleCreation(dirPath: string) { 12 | return mkDir_(dirPath) 13 | .pipe( 14 | catchError(err => { 15 | log(err) 16 | throw new Error(err) 17 | }), 18 | tap(() => logFileCreated(dirPath)) 19 | ) 20 | } 21 | 22 | export default function createFolder(dirPath: string) { 23 | return pathExists_(dirPath) 24 | .pipe( 25 | flatMap(exists => { 26 | return exists 27 | ? handleFileCollision(dirPath) 28 | : handleCreation(dirPath) 29 | }), 30 | take(1) 31 | ) 32 | } 33 | 34 | -------------------------------------------------------------------------------- /src/utilities/environment-variables.ts: -------------------------------------------------------------------------------- 1 | import { config as readDotEnv } from 'dotenv' 2 | 3 | export interface AppEnvironmentSettings { 4 | readonly PORT: string 5 | readonly ENV: string 6 | } 7 | 8 | interface StringDictionary { 9 | readonly [key: string]: string 10 | } 11 | 12 | const newEnv = readDotEnv() 13 | 14 | function keysWihoutCorrectPrefix(prefix = 'FNG_') { 15 | return function(key: string) { 16 | return key.includes(prefix) 17 | } 18 | } 19 | 20 | function toFngDictionaryObject(original: StringDictionary, prefix = 'FNG_') { 21 | return function(acc: Object, curr: string) { 22 | return { 23 | ...acc, 24 | [curr.replace(prefix, '')]: original[curr] 25 | } 26 | } 27 | } 28 | 29 | function cleanVars(dict = {}) { 30 | return Object.keys(dict) 31 | .filter(keysWihoutCorrectPrefix()) 32 | .reduce(toFngDictionaryObject(dict), {}) 33 | } 34 | 35 | export const appEnvironmentVariables = { 36 | ...cleanVars(process.env), 37 | ...cleanVars(newEnv.parsed) 38 | } as AppEnvironmentSettings 39 | -------------------------------------------------------------------------------- /src/utilities/log.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | 3 | export const log = console.log 4 | 5 | export function logPrettyJson(json?: Object) { 6 | log(JSON.stringify(json, undefined, 2)) 7 | } 8 | 9 | export function logError(msg: any) { 10 | log(chalk.bgRedBright(msg)) 11 | } 12 | 13 | export function logErrorWithPrefix(msg: any) { 14 | log(chalk.bgRedBright(`Error: ${msg}`)) 15 | } 16 | 17 | export function logInfo(msg: string) { 18 | log(chalk.blue(msg)) 19 | } 20 | 21 | export function logInfoWithBackground(msg: string) { 22 | log(chalk.bgBlue(msg)) 23 | } 24 | 25 | export function logFileCreated(path: string) { 26 | logInfo(`File created/updated at ${path}`) 27 | } 28 | -------------------------------------------------------------------------------- /src/utilities/read-config.ts: -------------------------------------------------------------------------------- 1 | import { map, catchError } from 'rxjs/operators' 2 | import { readFile_ } from './rx-fs' 3 | import * as favs from 'favicons' 4 | import { resolve } from 'path' 5 | 6 | const configPath = 'fusing-angular.json' 7 | 8 | export interface FaviconConfig { 9 | readonly source: string 10 | readonly output: string 11 | readonly config: favs.Configuration 12 | } 13 | 14 | export interface FuseBoxBaseConfig { 15 | readonly outputDir: string 16 | } 17 | 18 | export interface FuseBoxServerConfig extends FuseBoxBaseConfig { 19 | readonly serverModule: string 20 | } 21 | 22 | export interface FuseBoxBrowserConfig extends FuseBoxBaseConfig { 23 | readonly browserModule: string 24 | readonly aotBrowserModule: string 25 | readonly prod: { 26 | readonly uglify: boolean 27 | readonly treeshake: boolean 28 | } 29 | } 30 | 31 | export interface FuseBoxConfig { 32 | readonly server: FuseBoxServerConfig 33 | readonly browser: FuseBoxBrowserConfig 34 | readonly verbose: boolean 35 | } 36 | 37 | export interface EnvironmentConfig { 38 | readonly server: any 39 | readonly app: any 40 | } 41 | 42 | export interface FusingAngularConfig { 43 | readonly favicon: FaviconConfig 44 | readonly fusebox: FuseBoxConfig 45 | readonly environment: EnvironmentConfig 46 | readonly generatedMetaTags?: ReadonlyArray 47 | } 48 | 49 | export default function readConfig_(basePath = '') { 50 | return readFile_(resolve(basePath, configPath)).pipe( 51 | map(file => JSON.parse(file.toString())), 52 | map(obj => obj), // TODO: add validation handler here 53 | catchError(err => { 54 | throw new Error( 55 | 'Could not find fusing-angular.json configuration file.\nTry running "fng create" and creating a new application first.' 56 | ) 57 | }) 58 | ) 59 | } 60 | -------------------------------------------------------------------------------- /src/utilities/rx-favicon.ts: -------------------------------------------------------------------------------- 1 | import { bindCallback, Observable } from 'rxjs' 2 | import { FaviconConfig } from './read-config' 3 | import { resolve } from 'path' 4 | import * as favs from 'favicons' 5 | 6 | function callback(config: FaviconConfig) { 7 | return function(error: Error, response: favs.FavIconResponse) { 8 | return error 9 | ? (() => { 10 | throw error 11 | })() 12 | : response 13 | } 14 | } 15 | 16 | export function rxFavicons(baseDir = '.') { 17 | return function(config?: FaviconConfig) { 18 | const _config = { 19 | source: 20 | (config && resolve(baseDir, config.source)) || 21 | resolve(baseDir, 'src/app/favicon.svg'), 22 | configuration: { 23 | path: '/assets/favicons', 24 | ...(config && config.config) 25 | } 26 | } as any 27 | return bindCallback(favs as any, callback(_config))( 28 | _config.source, 29 | _config.configuration 30 | ) as Observable 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/utilities/rx-fs.ts: -------------------------------------------------------------------------------- 1 | import { writeFile, readFile, lstat, mkdir, mkdirSync } from 'fs' 2 | import { bindNodeCallback, of, forkJoin, Observable } from 'rxjs' 3 | import { tap, catchError, map, flatMap } from 'rxjs/operators' 4 | import { logError, logFileCreated } from './log' 5 | import { resolve } from 'path' 6 | 7 | function returnTrue() { 8 | return true 9 | } 10 | 11 | function returnFalse_() { 12 | return of(false) 13 | } 14 | 15 | export function writeFileSafely_(path: string, data: any, overwrite = false) { 16 | return overwrite 17 | ? writeFile_(path, data) 18 | : pathExists_(path).pipe( 19 | flatMap( 20 | exists => 21 | exists 22 | ? of(undefined).pipe( 23 | tap(() => logError(`Path ${path} already exists`)) 24 | ) 25 | : writeFile_(path, data) 26 | ) 27 | ) 28 | } 29 | 30 | export function writeFile_(path: string, data: any) { 31 | return bindNodeCallback(writeFile)(path, data).pipe( 32 | tap(() => logFileCreated(path)) 33 | ) 34 | } 35 | 36 | export function writeJsonFile_(path: string, obj: Object, overwrite = false) { 37 | return writeFileSafely_(path, JSON.stringify(obj, undefined, 2), overwrite) 38 | } 39 | 40 | export function readFile_(path: string) { 41 | return bindNodeCallback(readFile)(path) 42 | } 43 | 44 | export function pathExists_(path: string) { 45 | return bindNodeCallback(lstat)(path).pipe( 46 | map(returnTrue), 47 | catchError(returnFalse_) 48 | ) 49 | } 50 | 51 | function relativePathsToResolvedPaths( 52 | acc: ReadonlyArray, 53 | curr: string, 54 | idx: number 55 | ) { 56 | return [...acc, idx === 0 ? resolve(curr) : resolve(acc[idx - 1], curr)] 57 | } 58 | 59 | interface PathModel { 60 | readonly doesNotExist: boolean 61 | readonly exists: boolean 62 | readonly path: string 63 | } 64 | 65 | function mapPathExistenceToObs(path: string) { 66 | return pathExists_(path).pipe( 67 | map(exists => { 68 | return { 69 | doesNotExist: !exists, 70 | exists, 71 | path 72 | } 73 | }) 74 | ) 75 | } 76 | 77 | function filterOnlyNonExistingFolders(list: ReadonlyArray) { 78 | return list.filter(a => a.doesNotExist) 79 | } 80 | 81 | function reducePathModelToPath(list: ReadonlyArray) { 82 | return list.map(b => b.path) 83 | } 84 | 85 | export function pathExistsDeep_(path: string) { 86 | const paths_ = path 87 | .split('/') 88 | .reduce(relativePathsToResolvedPaths, []) 89 | .map(mapPathExistenceToObs) 90 | 91 | return forkJoin(paths_).pipe( 92 | map(filterOnlyNonExistingFolders), 93 | map(reducePathModelToPath) 94 | ) 95 | } 96 | 97 | export function mkDir_(path: string) { 98 | return bindNodeCallback(mkdir)(path) 99 | } 100 | 101 | export function mkDirAndContinueIfExists_(path: string) { 102 | return mkDir_(path).pipe( 103 | catchError( 104 | err => 105 | err.errno === -17 106 | ? of(undefined) 107 | : (() => { 108 | throw err 109 | })() 110 | ) 111 | ) 112 | } 113 | 114 | export function mkDirDeep_(path: string): Observable { 115 | return pathExistsDeep_(path).pipe(map(a => a.map(b => mkdirSync(b)))) 116 | } 117 | -------------------------------------------------------------------------------- /src/utilities/sass.ts: -------------------------------------------------------------------------------- 1 | import { readdirSync, statSync, writeFileSync } from 'fs' 2 | import { join, resolve } from 'path' 3 | import { renderSync } from 'node-sass' 4 | 5 | const read = (dir: string): ReadonlyArray => 6 | readdirSync(dir) 7 | .reduce( 8 | (files: ReadonlyArray, file) => 9 | statSync(join(dir, file)).isDirectory() 10 | ? files.concat(read(join(dir, file))) 11 | : files.concat(join(dir, file)), 12 | [] 13 | ) 14 | .filter(a => a.includes('.scss')) 15 | 16 | export function renderSassDir() { 17 | const sassFiles = read(resolve('.', 'src')) 18 | const results = sassFiles.map(mapToWriteableMetaData) 19 | 20 | results.forEach(res => { 21 | writeFileSync(res.resultPath, res.css, {}) 22 | }) 23 | } 24 | 25 | function mapToWriteableMetaData(filePath: string) { 26 | const rendered = renderSync({ 27 | file: filePath, 28 | includePaths: [resolve('node_modules'), resolve('src')], 29 | outputStyle: 'compressed' 30 | }) 31 | return { 32 | resultPath: filePath.replace('.scss', '.css'), 33 | css: rendered.css.toString() 34 | } 35 | } 36 | 37 | export function renderSingleSass(filePath: string) { 38 | const single = mapToWriteableMetaData(filePath) 39 | writeFileSync(single.resultPath, single.css, {}) 40 | } 41 | -------------------------------------------------------------------------------- /src/utilities/welcome.ts: -------------------------------------------------------------------------------- 1 | import clearTerminal from './clear' 2 | import { log } from './log' 3 | import * as splash from '../assets/splash.txt' 4 | 5 | export default function welcome() { 6 | clearTerminal() 7 | log(splash) 8 | return splash 9 | } 10 | -------------------------------------------------------------------------------- /tools/manual-typings/json.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.json' { 2 | const value: any 3 | export = value 4 | } 5 | -------------------------------------------------------------------------------- /tools/manual-typings/txt.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.txt' { 2 | const value: string 3 | export = value 4 | } 5 | -------------------------------------------------------------------------------- /tools/scripts/fuse-shebang.ts: -------------------------------------------------------------------------------- 1 | import { Bundle } from 'fuse-box' 2 | import { writeFileSync, chmodSync } from 'fs' 3 | import { logError } from '../../src/utilities/log' 4 | 5 | const sheBang = '#!/usr/bin/env node' 6 | 7 | export default function(bundle: Bundle, absOutputPath: string) { 8 | try { 9 | const final = `${sheBang}\n${bundle.generatedCode.toString()}` 10 | writeFileSync(absOutputPath, final) 11 | chmodSync(absOutputPath, '755') 12 | } catch (err) { 13 | logError(err) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tools/setup/mac.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # This script sets up the local development for Max OSX environments. 4 | # You should be able to run this script as many times as you want. 5 | 6 | # Check if Homebrew (https://brew.sh/) is installed 7 | ## https://github.com/mxcl/homebrew/wiki/installation 8 | which -s brew 9 | if [[ $? != 0 ]] ; then 10 | ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" 11 | else 12 | brew update 13 | fi 14 | 15 | # install non-npm dependencies 16 | brew install git node bash-completion 17 | brew upgrade 18 | 19 | # install nvm 20 | curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.11/install.sh | bash 21 | 22 | # for auto switching node version when switching folders in terminal/console 23 | npm install -g avn avn-nvm avn-n 24 | avn setup 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2015", 5 | "importHelpers": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "noUnusedLocals": true, 9 | "noImplicitAny": true, 10 | "strictNullChecks": true, 11 | "lib": ["es6", "dom"], 12 | "typeRoots": ["tools/manual-typings", "node_modules/@types"] 13 | }, 14 | "include": ["src/**/**.ts", "tools/manual-typings"] 15 | } 16 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint-immutable"], 3 | "rules": { 4 | "newline-per-chained-call": false, 5 | "no-unused-variable": false, 6 | "prefer-object-spread": true, 7 | "no-var-keyword": true, 8 | "no-parameter-reassignment": true, 9 | "typedef": false, 10 | "indent": [true, "spaces", 2], 11 | "space-within-parens": false, 12 | "object-literal-key-quotes": false, 13 | "semicolon": [true, "never"], 14 | "align": false, 15 | "curly": false, 16 | "member-ordering": false, 17 | "member-access": [false, "no-public"], 18 | "newline-before-return": false, 19 | "array-type": false, 20 | "readonly-keyword": true, 21 | "readonly-array": true, 22 | "no-let": true, 23 | "no-object-mutation": true, 24 | "no-delete": true, 25 | "no-method-signature": true, 26 | "no-this": true, 27 | "no-class": true, 28 | "no-expression-statement": false, 29 | "no-if-statement": true 30 | } 31 | } 32 | --------------------------------------------------------------------------------