CV #52het - 52 projekt

How to start an Angular project?

Author: deejayy, 2020-03-11, updated: 2020-05-04

Easy, just "ng new", innit?

ng new angular9-project-init

? Would you like to add Angular routing? Yes
? Which stylesheet format would you like to use? SCSS [ https://sass-lang.com/documentation/syntax#scss ]

Official documentation of ng new.

Now you'll have an Angular project set up:

This is sort of a checklist.

README.md

It is important to let the others (and by others you can mean yourself 2 years later) know what is this project about. Feel free to extend the already prepared README.md file with specific information about the application.

How-to README?

Working environment

Editor config

Create or extend the .editorconfig file with the following content:

# Editor configuration, see https://editorconfig.org
root = true

[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true

[*.md]
max_line_length = off
trim_trailing_whitespace = false

[*.ts]
quote_type = single

[*.html]
quote_type = double

You might want to install the corresponding plugin in your IDE (eg. "Editorconfig for VSCode")

More on editorconfig on the official page.

Github setup (optional)

If you are using github, you can prepare templates for verious activities in the .github directory:

Check github's guidelines on the topic.

AWS (optional)

Are you going to deploy in an AWS CloudFront environment? The CodePipeline may need a specification in the buildspec.yml file:

version: 0.2

phases:
  install:
    commands:
      - echo "Installing packages."
      - npm install
      - echo "Installation of packages is done"
  pre_build:
    commands:
      - echo "Placeholder for pre_build"
  build:
    commands:
      - echo "Build started on /Project Name Here/"
      - npm run build -- --configuration=${ENV}
      - echo "Build completed on /Project Name Here/"
  post_build:
    commands:
      - echo "Copying directory app to to s3 bucket ${BUCKET_NAME}"
      - aws s3 sync dist/ "s3://${BUCKET_NAME}/" --delete --sse
      - aws configure set preview.cloudfront true && aws cloudfront create-invalidation --distribution-id "${DISTRIBUTION_ID}" --paths /\*
      - echo "Post build finihsed"
artifacts:
  files:
    - dist/**/*
    - dist/*

How the buildspec file should look like according to AWS.

Project specific

Browser compatibility

Check browserslist if you have to support older browsers as well, here are some hints about how it works.

Testing with Jest

But why Jest? Because it is faster!

npm install --save-dev @angular-builders/jest @types/jest jest jest-html-reporter jest-preset-angular

Extend package.json with the following block:

  "jest": {
    "preset": "jest-preset-angular",
    "globals": {
      "ts-jest": {
        "tsConfig": "<rootDir>/tsconfig.spec.json",
        "stringifyContentPathRegex": "\\.html$",
        "diagnostics": false
      }
    },
    "setupFilesAfterEnv": [
      "<rootDir>/src/setupJest.ts"
    ],
    "moduleNameMapper": {
      "@app/(.*)": "<rootDir>/src/app/$1",
      "@env/(.*)": "<rootDir>/src/environments/$1"
    }
  },

Add new files for jest setup:

src/jestGlobalMocks.ts

export function storageMock() {
  const storage = {};

  return {
    setItem: (key: string, value?: any) => {
      storage[key] = value || '';
    },
    getItem: (key: string) => {
      return key in storage ? storage[key] : null;
    },
    removeItem: (key: string) => {
      delete storage[key];
    },
    get length() {
      return Object.keys(storage).length;
    },
    key: (index: number) => {
      const keys = Object.keys(storage);
      return keys[index] || null;
    },
  };
}

Object.defineProperty(window, 'localStorage', { value: storageMock });
Object.defineProperty(window, 'sessionStorage', { value: storageMock });
Object.defineProperty(window, 'gtag', { value: () => {} });
Object.defineProperty(window, 'ga', { value: () => {} });

src/jestGlobalMocks.spec.ts

import { storageMock } from './jestGlobalMocks';

describe('JestMocks', () => {
  it('localStorage must be empty', () => {
    const localStorage = storageMock();
    expect(localStorage.length).toEqual(0);
  });

  it('localStorage set item / get item', () => {
    const localStorage = storageMock();

    localStorage.setItem('key-empty');
    localStorage.setItem('key-number', 1);
    localStorage.setItem('key-string', 'value');
    localStorage.setItem('key-array', [ 2 ]);
    localStorage.setItem('key-object', { a: 'b' });

    expect(localStorage.getItem('key-not-found')).toEqual(null);
    expect(localStorage.getItem('key-empty')).toEqual('');
    expect(localStorage.getItem('key-number')).toEqual(1);
    expect(localStorage.getItem('key-string')).toEqual('value');
    expect(localStorage.getItem('key-array')).toEqual([ 2 ]);
    expect(localStorage.getItem('key-object')).toEqual({ a: 'b' });
  });

  it('localStorage set item / remove item', () => {
    const localStorage = storageMock();

    localStorage.setItem('key-number', 1);
    expect(localStorage.getItem('key-number')).toEqual(1);
    localStorage.removeItem('key-number');
    expect(localStorage.getItem('key-number')).toEqual(null);
  });

  it('localStorage set item / key', () => {
    const localStorage = storageMock();

    localStorage.setItem('key-number', 1);
    expect(localStorage.key(0)).toEqual('key-number');
    expect(localStorage.key(1)).toEqual(null);
  });
});

src/setupJest.ts

import 'jest-preset-angular';
import './jestGlobalMocks';

Now try out jest

jest jestGlobalMocks
 PASS  src/jestGlobalMocks.spec.ts
  JestMocks
    √ localStorage must be empty (3ms)
    √ localStorage set item / get item (2ms)
    √ localStorage set item / remove item (1ms)
    √ localStorage set item / key

Test Suites: 1 passed, 1 total
Tests:       4 passed, 4 total
Snapshots:   0 total
Time:        2.771s, estimated 4s
Ran all test suites matching /jestGlobalMocks/i.

Module path aliases

If you see "../" in any TypeScript file, you're doomed. Relative paths will cause a lot of headaches later. The solution is: module path aliases! Check out this intro about it!

There are two places where you need to maintain the list of module path aliases.

  1. package.json (for Jest, see above)
  2. tsconfig.json (for build, see below)

Check tsconfig.json / compilerOptions branch and add this:

"paths": {
  "@app/*": ["src/app/*"],
  "@env/*": ["src/environments/*"]
},

Version number

Open package.json and set the version value.

Trick: you can add this line to every environment.ts if you want to advertise the version number runtime:

import { version } from '../../package.json';

export const environment = {
  production: false,
  version: version,
};

Don't forget about letting the typescript compiler know that you want to import a json in a TS file, add these two lines to your tsconfig.json / compilerOptions:

"resolveJsonModule": true,
"esModuleInterop": true

You can add this version number to eg. some online tracking tool, like Google Analytics (check angulartics2 module).

Static code checking tools

Lint-staged, stylelint and commit hook. Because static check can improve the code quality significantly.

npm install --save-dev lint-staged husky stylelint stylelint-config-standard-scss stylelint-scss tslint tslint-clean-code tslint-microsoft-contrib

Add the following items to the package.json / scripts section:

    "lint": "ng lint && npm run lint:styles",
    "lint:styles": "stylelint --syntax=scss --config=./stylelint **/*.scss",
    "lint-staged": "lint-staged -r",
    "precommit": "npm run lint-staged"

... and this blocks to root:

  "lint-staged": {
    "*.ts": [
      "tslint",
      "jest --findRelatedTests"
    ],
    "*.scss": "stylelint --syntax=scss --config=./stylelint"
  },
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  },

stylelint.json

{
  "extends": "stylelint-config-standard-scss",
  "rules": {
    "at-rule-empty-line-before": [
      "always",
      {
        "except": [
          "blockless-after-same-name-blockless",
          "blockless-after-blockless",
          "first-nested"
        ],
        "ignoreAtRules": [
          "import",
          "include"
        ]
      }
    ],
    "at-rule-no-vendor-prefix": true,
    "at-rule-whitelist": [
      "mixin",
      "if",
      "function",
      "return",
      "import",
      "include",
      "media",
      "keyframes",
      "supports",
      "font-face"
    ],
    "block-no-empty": [
      true,
      {
        "ignore": [
          "comments"
        ]
      }
    ],
    "color-no-invalid-hex": true,
    "comment-no-empty": true,
    "declaration-block-no-duplicate-properties": true,
    "declaration-block-no-shorthand-property-overrides": true,
    "declaration-empty-line-before": "never",
    "font-family-no-duplicate-names": true,
    "font-family-no-missing-generic-family-keyword": true,
    "function-calc-no-invalid": true,
    "function-calc-no-unspaced-operator": true,
    "function-linear-gradient-no-nonstandard-direction": true,
    "keyframe-declaration-no-important": true,
    "media-feature-name-no-unknown": true,
    "no-descending-specificity": true,
    "no-duplicate-at-import-rules": true,
    "no-duplicate-selectors": true,
    "no-extra-semicolons": true,
    "no-invalid-double-slash-comments": true,
    "property-no-unknown": [
      true,
      {
        "ignoreProperties": [
          "scroll",
          "overflow-x",
          "overflow-y"
        ]
      }
    ],
    "property-no-vendor-prefix": [
      true,
      {
        "ignoreProperties": [
          "appearance"
        ]
      }
    ],
    "rule-empty-line-before": [
      "always",
      {
        "except": [
          "after-single-line-comment",
          "first-nested"
        ]
      }
    ],
    "selector-pseudo-class-no-unknown": true,
    "selector-pseudo-element-no-unknown": true,
    "selector-no-vendor-prefix": true,
    "selector-type-no-unknown": true,
    "string-no-newline": true,
    "unit-no-unknown": true
  }
}

Try it out with the command

npm run lint

tslint.json

{
  "rulesDirectory": [
    "tslint-microsoft-contrib",
    "codelyzer",
    "tslint-clean-code"
  ],
  "rules": {
    "array-type": [
      true,
      "array"
    ],
    "arrow-return-shorthand": false,
    "await-promise": false,
    "adjacent-overload-signatures": false,
    "align": [
      false
    ],
    "arrow-parens": false,
    "ban": false,
    "callable-types": true,
    "chai-prefer-contains-to-index-of": true,
    "chai-vague-errors": true,
    "class-name": true,
    "comment-format": [
      false
    ],
    "completed-docs": [
      false
    ],
    "curly": true,
    "cyclomatic-complexity": [
      false
    ],
    "eofline": false,
    "export-name": false, // true
    "file-header": [
      false
    ],
    "forin": true,
    "function-name": true,
    "import-blacklist": false,
    "import-spacing": true,
    "indent": [
      true,
      "spaces"
    ],
    "insecure-random": true,
    "interface-name": false,
    "interface-over-type-literal": false,
    "jquery-deferred-must-complete": true,
    "jsdoc-format": false,
    "label-position": true,
    "linebreak-style": [
      true,
      "LF"
    ],
    "max-classes-per-file": false,
    "max-file-line-count": [
      false
    ],
    "max-func-body-length": [
      true,
      100,
      {
        "ignore-parameters-to-function-regex": "describe"
      }
    ],
    "max-line-length": [
      true,
      140
    ],
    "member-access": true,
    "member-ordering": [
      false
    ],
    "missing-jsdoc": false, // true
    "missing-optional-annotation": false, // true,
    "min-class-cohesion": false,
    "mocha-avoid-only": true,
    "mocha-no-side-effect-code": true,
    "mocha-unneeded-done": true,
    "new-parens": true,
    "newspaper-order": false, // true
    "no-angle-bracket-type-assertion": false,
    "no-any": false,
    "no-arg": true,
    "no-backbone-get-set-outside-model": false, // true
    "no-banned-terms": true,
    "no-bitwise": true,
    "no-boolean-literal-compare": false,
    "no-conditional-assignment": true,
    "no-consecutive-blank-lines": [
      true
    ],
    "no-console": [
      true,
      "debug",
      "info",
      "log",
      "time",
      "timeEnd",
      "trace"
    ],
    "no-constant-condition": true,
    "no-construct": true,
    "no-control-regex": true,
    "no-debugger": true,
    "no-default-export": true,
    "no-delete-expression": true,
    "no-disable-auto-sanitization": true,
    "no-document-domain": true,
    "no-document-write": true,
    "no-duplicate-switch-case": true,
    "no-duplicate-parameter-names": true,
    "no-duplicate-super": true,
    "no-duplicate-variable": true,
    "no-empty": false, // true
    "no-empty-interface": false, // true
    "no-empty-line-after-opening-brace": false,
    "no-eval": true,
    "no-exec-script": true,
    "no-floating-promises": false,
    "no-for-in": true,
    "no-for-in-array": false,
    "no-function-expression": true,
    "no-http-string": [
      true,
      "http://localhost"
    ],
    "no-import-side-effect": false, // true
    "no-inferrable-types": false,
    "no-inferred-empty-object-type": false,
    "no-inner-html": true,
    "no-internal-module": false,
    "no-invalid-regexp": true,
    "no-invalid-template-strings": true,
    "no-invalid-this": true,
    "no-jquery-raw-elements": true,
    "no-magic-numbers": false,
    "no-mergeable-namespace": false,
    "no-missing-visibility-modifiers": true,
    "no-misused-new": true,
    "no-multiline-string": false,
    "no-multiple-var-decl": true,
    "no-namespace": false,
    "no-null-keyword": false,
    "no-octal-literal": true,
    "no-parameter-properties": false,
    "no-reference": true,
    "no-reference-import": true,
    "no-regex-spaces": true,
    "no-require-imports": true,
    "no-reserved-keywords": false, // true
    "no-shadowed-variable": true,
    "no-single-line-block-comment": true,
    "no-sparse-arrays": true,
    "no-stateless-class": false, // true
    "no-string-based-set-immediate": true,
    "no-string-based-set-interval": true,
    "no-string-based-set-timeout": true,
    "no-string-literal": true,
    "no-string-throw": true,
    "no-suspicious-comment": true,
    "no-switch-case-fall-through": true,
    "no-trailing-whitespace": true,
    "no-typeof-undefined": true,
    "no-unbound-method": false,
    "no-unnecessary-callback-wrapper": true,
    "no-unnecessary-field-initialization": true,
    "no-unnecessary-initializer": true,
    "no-unnecessary-local-variable": true,
    "no-unnecessary-override": true,
    "no-unnecessary-qualifier": false,
    "no-unnecessary-semicolons": true,
    "no-unsafe-any": false,
    "no-unsafe-finally": true,
    "no-unsupported-browser-code": false,
    "no-unused-expression": true,
    "no-var-keyword": true,
    "no-var-requires": true,
    "no-this-assignment": true,
    "no-void-expression": false,
    "no-with-statement": true,
    "non-literal-require": true,
    "object-literal-key-quotes": [
      true,
      "as-needed"
    ],
    "object-literal-shorthand": false,
    "object-literal-sort-keys": false,
    "one-line": [
      true,
      "check-open-brace",
      "check-catch",
      "check-else",
      "check-whitespace"
    ],
    "one-variable-per-declaration": true,
    "only-arrow-functions": [
      true,
      "allow-declarations",
      "allow-named-functions"
    ],
    "ordered-imports": [
      false
    ],
    "possible-timing-attack": true,
    "prefer-array-literal": true,
    "prefer-const": true,
    "prefer-for-of": false,
    "prefer-function-over-method": false,
    "prefer-method-signature": true,
    "prefer-template": false,
    "strict-type-predicates": false,
    "promise-function-async": false,
    "promise-must-complete": true,
    "quotemark": [
      true,
      "single",
      "avoid-escape"
    ],
    "radix": false,
    "react-a11y-anchors": true,
    "react-a11y-aria-unsupported-elements": true,
    "react-a11y-event-has-role": true,
    "react-a11y-image-button-has-alt": true,
    "react-a11y-img-has-alt": true,
    "react-a11y-lang": true,
    "react-a11y-meta": true,
    "react-a11y-props": true,
    "react-a11y-proptypes": true,
    "react-a11y-role": true,
    "react-a11y-role-has-required-aria-props": true,
    "react-a11y-role-supports-aria-props": true,
    "react-a11y-tabindex-no-positive": true,
    "react-a11y-titles": true,
    "react-anchor-blank-noopener": true,
    "react-iframe-missing-sandbox": true,
    "react-no-dangerous-html": true,
    "react-this-binding-issue": true,
    "react-tsx-curly-spacing": true,
    "react-unused-props-and-state": true,
    "restrict-plus-operands": false,
    "semicolon": [
      true,
      "always"
    ],
    "space-before-function-paren": false,
    "strict-boolean-expressions": false,
    "switch-default": false,
    "trailing-comma": [
      true,
      {
        "singleline": "never",
        "multiline": {
          "objects": "always",
          "arrays": "always",
          "functions": "always",
          "imports": "always",
          "exports": "always",
          "typeLiterals": "always"
        }
      }
    ],
    "triple-equals": [
      true,
      "allow-null-check"
    ],
    "typedef": [
      true,
      "parameter",
      "property-declaration"
    ],
    "typedef-whitespace": [
      false
    ],
    "underscore-consistent-invocation": true,
    "unified-signatures": true,
    "use-isnan": true,
    "use-named-parameter": true,
    "variable-name": true,
    "whitespace": [
      true,
      "check-branch",
      "check-decl",
      "check-operator",
      "check-separator",
      "check-type"
    ],
    "no-useless-files": true,
    "no-commented-out-code": false,
    "directive-selector": [
      true,
      "attribute",
      "app",
      "camelCase"
    ],
    "component-selector": [
      true,
      "element",
      "app",
      "kebab-case"
    ],
    "no-output-on-prefix": true,
    "no-input-rename": true,
    "no-output-rename": true,
    "use-life-cycle-interface": true,
    "use-pipe-transform-interface": true,
    "component-class-suffix": true,
    "directive-class-suffix": true
  }
}

Try it out with the command

tslint --project .

Prepare the app

CSS reset

npm install modern-css-reset

Add reset css to the build process: angular.json / projects.<project name>.architect.build.options.styles

"styles": [
  "node_modules/modern-css-reset/dist/reset.css",
  "src/styles.scss"
],

Thanks to Andy for the modern css reset.

Application theme and utils (css)

Some useful mixins for responsive design

src/mixins.scss

$breakpoints: (
  "phone": 400px,
  "phone-wide": 480px,
  "phablet": 560px,
  "tablet-small": 640px,
  "tablet": 768px,
  "tablet-wide": 1024px,
  "desktop": 1248px,
  "desktop-wide": 1440px
);

@mixin mq($width, $type: max) {
  @if map_has_key($breakpoints, $width) {
    $width: map_get($breakpoints, $width);

    @if $type == max {
      $width: $width - 1px;
    }

    @media only screen and (#{$type}-width: $width) {
      @content;
    }
  }
}

@function smart-scale($minwidth, $maxwwidth, $minscreen: 320, $maxscreen: 1440) {
  @return calc(#{$minwidth + 'px'} + (#{$maxwwidth} - #{$minwidth}) * ((100vw - #{$minscreen + 'px'}) / (#{$maxscreen} - #{$minscreen})));
}

The mq mixin will allow you to shorten the usual media query definitions in css, also adds some readable definitions for breakpoints which you can adjust later in a single place.

The smart-scale magic is to make a specific size relative between the minscreen and maxscreen in proportion with the minwidth and maxwidth. It can be used to scale font sizes, box widths, anything what you want to define in pixels.

Application theme with the colors and fonts, and all you want to put in. Use hsl color codes for better readability.

src/app-theme.scss

@import "mixins.scss";

$theme-color-1:  hsl(200, 80, 75%);
$theme-color-2:  hsl(200, 80, 70%);
$theme-color-3:  hsl(200, 80, 65%);
$theme-color-4:  hsl(200, 80, 60%);
$theme-color-5:  hsl(200, 80, 55%);
$theme-color-6:  hsl(200, 80, 50%);
$theme-color-7:  hsl(200, 80, 45%);
$theme-color-8:  hsl(200, 80, 40%);
$theme-color-9:  hsl(200, 80, 35%);
$theme-color-10: hsl(200, 80, 30%);

$theme-color: $theme-color-5;

$theme-light: $theme-color-9;
$theme-default: $theme-color;
$theme-hover: $theme-color-4;
$theme-pressed: $theme-color-2;

$theme-background: hsl(200, 100, 100%);

$warn-color-1:  hsl(0, 80, 75%);
$warn-color-2:  hsl(0, 80, 70%);
$warn-color-3:  hsl(0, 80, 65%);
$warn-color-4:  hsl(0, 80, 60%);
$warn-color-5:  hsl(0, 80, 55%);
$warn-color-6:  hsl(0, 80, 50%);
$warn-color-7:  hsl(0, 80, 45%);
$warn-color-8:  hsl(0, 80, 40%);
$warn-color-9:  hsl(0, 80, 35%);
$warn-color-10: hsl(0, 80, 30%);

$warn-color: $warn-color-5;

$warn-light: $warn-color-9;
$warn-default: $warn-color;
$warn-hover: $warn-color-4;
$warn-pressed: $warn-color-2;

$theme-font: Tahoma, sans-serif;

$black: rgba(0, 0, 0, 1);
$white: rgba(255, 255, 255, 1);

Add your theme to src/styles.scss:

@import "app-theme.scss";

You'll unfortunately need this import in every component's stylesheet, for that, this snippet is needed in the angular.json in order to make it work (place it under projects.<project name>.architect.build.options):

"stylePreprocessorOptions": {
  "includePaths": [
    "src"
  ]
},

Angular Material (optional)

npm install @angular/cdk @angular/material hammerjs

Add BrowserAnimationsModule to app.module.ts

import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

...

imports: [
  ...,
  BrowserAnimationsModule,
],

Add basic fonts to index.html

<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">

Add hammerjs to main.ts

import 'hammerjs';

Create an scss file for material theme:

src/material-theme.scss

@import "app-theme.scss";
@import "~@angular/material/theming";

@include mat-core();

$mat-app: (
  50:  $theme-color-1,
  100: $theme-color-2,
  200: $theme-color-3,
  300: $theme-color-4,
  400: $theme-color-5,
  500: $theme-color-6,
  600: $theme-color-7,
  700: $theme-color-8,
  800: $theme-color-9,
  900: $theme-color-10,
  A100: $theme-light,
  A200: $theme-default,
  A400: $theme-hover,
  A700: $theme-pressed,
  contrast: (
    50: $black,
    100: $black,
    200: $black,
    300: $black,
    400: $white,
    500: $white,
    600: $white,
    700: $white,
    800: $white,
    900: $white,
    A100: $black,
    A200: $white,
    A400: $white,
    A700: $white
  )
);

$mat-app-warn: (
  50: $warn-color-1,
  100: $warn-color-2,
  200: $warn-color-3,
  300: $warn-color-4,
  400: $warn-color-5,
  500: $warn-color-6,
  600: $warn-color-7,
  700: $warn-color-8,
  800: $warn-color-9,
  900: $warn-color-10,
  A100: $warn-light,
  A200: $warn-default,
  A400: $warn-hover,
  A700: $warn-pressed,
  contrast: (
    50: $black,
    100: $black,
    200: $black,
    300: $black,
    400: $black,
    500: $white,
    600: $white,
    700: $white,
    800: $white,
    900: $white,
    A100: $black,
    A200: $white,
    A400: $white,
    A700: $white
  )
);

$app-theme: mat-palette($mat-app);
$app-accent: mat-palette($mat-app, A200, A100, A400);
$app-warn: mat-palette($mat-app-warn, A200, A100, A400);
$app-theme: mat-light-theme($app-theme, $app-accent, $app-warn);

$custom-typography: mat-typography-config(
  $font-family: $theme-font,
  $headline: mat-typography-level(32px, 48px, 800),
  $caption: mat-typography-level(16px, 24px, 400),
  $body-1: mat-typography-level(16px, 24px, 400),
  $body-2: mat-typography-level(16px, 24px, 600)
);

@include angular-material-theme($app-theme);

body {
  color: $theme-color;
  font-size: 16px;
  font-family: $theme-font;
}

Add your material theme to styles.scss (replace the import of app-theme.scss):

@import "material-theme.scss";

Get rid of placeholders

Adjust app.component.html | ts | spec.ts files, remove placeholders, invalid test cases.

Run the first static check

And also fix test errors, if any

npm run lint
jest
ng build --prod

Take care of multiple environments (optional)

If you have other environments apart from local (environment.ts) and production (environment.prod.ts), create them and add them to angular.json as well.

Example:

"dev": {
  "fileReplacements": [
    {
      "replace": "src/environments/environment.ts",
      "with": "src/environments/environment.dev.ts"
    }
  ]
},
"staging": {
  "fileReplacements": [
    {
      "replace": "src/environments/environment.ts",
      "with": "src/environments/environment.staging.ts"
    }
  ],
  "optimization": true,
  "outputHashing": "all",
  "sourceMap": false,
  "extractCss": true,
  "namedChunks": false,
  "aot": true,
  "extractLicenses": true,
  "vendorChunk": false,
  "buildOptimizer": true
},

Recommended packages

Directory structure

Create the directory structure as per the article:

https://itnext.io/choosing-a-highly-scalable-folder-structure-in-angular-d987de65ec7