├── .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 | [](https://circleci.com/gh/patrickmichalina/fusing-angular-cli)
6 | [](https://greenkeeper.io/)
7 | [](https://david-dm.org/patrickmichalina/fusing-angular-cli)
8 | [](https://david-dm.org/patrickmichalina/fusing-angular-cli?type=dev)
9 | [](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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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' +
58 | componentName +
59 | '>'
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 |
--------------------------------------------------------------------------------