This commit is contained in:
Krzysztof Józefowicz 2019-12-02 16:45:31 +01:00
parent 126ce61670
commit e4a91de738
38 changed files with 8820 additions and 0 deletions

13
front/.editorconfig Normal file
View File

@ -0,0 +1,13 @@
# 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

46
front/.gitignore vendored Normal file
View File

@ -0,0 +1,46 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/dist
/tmp
/out-tsc
# Only exists if Bazel was run
/bazel-out
# dependencies
/node_modules
# profiling files
chrome-profiler-events*.json
speed-measure-plugin*.json
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# misc
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
yarn-error.log
testem.log
/typings
# System Files
.DS_Store
Thumbs.db

7934
front/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

40
front/package.json Normal file
View File

@ -0,0 +1,40 @@
{
"name": "angular-8-registration-login-example",
"version": "1.0.0",
"repository": {
"type": "git",
"url": "https://github.com/cornflourblue/angular-8-registration-login-example.git"
},
"scripts": {
"build": "webpack --mode production",
"start": "webpack-dev-server --mode development --open"
},
"license": "MIT",
"dependencies": {
"@angular/common": "^8.0.0",
"@angular/compiler": "^8.0.0",
"@angular/core": "^8.0.0",
"@angular/forms": "^8.0.0",
"@angular/platform-browser": "^8.0.0",
"@angular/platform-browser-dynamic": "^8.0.0",
"@angular/router": "^8.0.0",
"core-js": "^3.1.3",
"rxjs": "^6.3.3",
"zone.js": "^0.9.1"
},
"devDependencies": {
"@types/node": "^12.0.7",
"angular2-template-loader": "^0.6.2",
"css-loader": "^2.1.1",
"html-loader": "^0.5.5",
"html-webpack-plugin": "^3.2.0",
"less": "^3.0.4",
"less-loader": "^5.0.0",
"style-loader": "^0.23.1",
"ts-loader": "^6.0.1",
"typescript": "^3.1.3",
"webpack": "^4.32.2",
"webpack-cli": "^3.1.2",
"webpack-dev-server": "^3.7.0"
}
}

View File

@ -0,0 +1 @@
<div *ngIf="message" [ngClass]="message.cssClass">{{message.text}}</div>

View File

@ -0,0 +1,32 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
import { AlertService } from '@/_services';
@Component({ selector: 'alert', templateUrl: 'alert.component.html' })
export class AlertComponent implements OnInit, OnDestroy {
private subscription: Subscription;
message: any;
constructor(private alertService: AlertService) { }
ngOnInit() {
this.subscription = this.alertService.getAlert()
.subscribe(message => {
switch (message && message.type) {
case 'success':
message.cssClass = 'alert alert-success';
break;
case 'error':
message.cssClass = 'alert alert-danger';
break;
}
this.message = message;
});
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
}

View File

@ -0,0 +1 @@
export * from './alert.component';

View File

@ -0,0 +1,4 @@
// global application styles
a {
cursor: pointer;
}

View File

@ -0,0 +1,24 @@
import { Injectable } from '@angular/core';
import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { AuthenticationService } from '@/_services';
@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
constructor(
private router: Router,
private authenticationService: AuthenticationService
) {}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
const currentUser = this.authenticationService.currentUserValue;
if (currentUser) {
// authorised so return true
return true;
}
// not logged in so redirect to login page with the return url
this.router.navigate(['/login'], { queryParams: { returnUrl: state.url }});
return false;
}
}

View File

@ -0,0 +1,24 @@
import { Injectable } from '@angular/core';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { AuthenticationService } from '@/_services';
@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
constructor(private authenticationService: AuthenticationService) {}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(request).pipe(catchError(err => {
if (err.status === 401) {
// auto logout if 401 response returned from api
this.authenticationService.logout();
location.reload(true);
}
const error = err.error.message || err.statusText;
return throwError(error);
}))
}
}

View File

@ -0,0 +1,109 @@
import { Injectable } from '@angular/core';
import { HttpRequest, HttpResponse, HttpHandler, HttpEvent, HttpInterceptor, HTTP_INTERCEPTORS } from '@angular/common/http';
import { Observable, of, throwError } from 'rxjs';
import { delay, mergeMap, materialize, dematerialize } from 'rxjs/operators';
// array in local storage for registered users
let users = JSON.parse(localStorage.getItem('users')) || [];
@Injectable()
export class FakeBackendInterceptor implements HttpInterceptor {
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const { url, method, headers, body } = request;
// wrap in delayed observable to simulate server api call
return of(null)
.pipe(mergeMap(handleRoute))
.pipe(materialize()) // call materialize and dematerialize to ensure delay even if an error is thrown (https://github.com/Reactive-Extensions/RxJS/issues/648)
.pipe(delay(500))
.pipe(dematerialize());
function handleRoute() {
switch (true) {
case url.endsWith('/users/authenticate') && method === 'POST':
return authenticate();
case url.endsWith('/users/register') && method === 'POST':
return register();
case url.endsWith('/users') && method === 'GET':
return getUsers();
case url.match(/\/users\/\d+$/) && method === 'DELETE':
return deleteUser();
default:
// pass through any requests not handled above
return next.handle(request);
}
}
// route functions
function authenticate() {
const { username, password } = body;
const user = users.find(x => x.username === username && x.password === password);
if (!user) return error('Username or password is incorrect');
return ok({
id: user.id,
username: user.username,
firstName: user.firstName,
lastName: user.lastName,
token: 'fake-jwt-token'
})
}
function register() {
const user = body
if (users.find(x => x.username === user.username)) {
return error('Username "' + user.username + '" is already taken')
}
user.id = users.length ? Math.max(...users.map(x => x.id)) + 1 : 1;
users.push(user);
localStorage.setItem('users', JSON.stringify(users));
return ok();
}
function getUsers() {
if (!isLoggedIn()) return unauthorized();
return ok(users);
}
function deleteUser() {
if (!isLoggedIn()) return unauthorized();
users = users.filter(x => x.id !== idFromUrl());
localStorage.setItem('users', JSON.stringify(users));
return ok();
}
// helper functions
function ok(body?) {
return of(new HttpResponse({ status: 200, body }))
}
function error(message) {
return throwError({ error: { message } });
}
function unauthorized() {
return throwError({ status: 401, error: { message: 'Unauthorised' } });
}
function isLoggedIn() {
return headers.get('Authorization') === 'Bearer fake-jwt-token';
}
function idFromUrl() {
const urlParts = url.split('/');
return parseInt(urlParts[urlParts.length - 1]);
}
}
}
export const fakeBackendProvider = {
// use fake backend in place of Http service for backend-less development
provide: HTTP_INTERCEPTORS,
useClass: FakeBackendInterceptor,
multi: true
};

View File

@ -0,0 +1,4 @@
export * from './auth.guard';
export * from './error.interceptor';
export * from './jwt.interceptor';
export * from './fake-backend';

View File

@ -0,0 +1,24 @@
import { Injectable } from '@angular/core';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
import { Observable } from 'rxjs';
import { AuthenticationService } from '@/_services';
@Injectable()
export class JwtInterceptor implements HttpInterceptor {
constructor(private authenticationService: AuthenticationService) {}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// add authorization header with jwt token if available
let currentUser = this.authenticationService.currentUserValue;
if (currentUser && currentUser.token) {
request = request.clone({
setHeaders: {
Authorization: `Bearer ${currentUser.token}`
}
});
}
return next.handle(request);
}
}

View File

@ -0,0 +1 @@
export * from './user';

View File

@ -0,0 +1,8 @@
export class User {
id: number;
username: string;
password: string;
firstName: string;
lastName: string;
token: string;
}

View File

@ -0,0 +1,43 @@
import { Injectable } from '@angular/core';
import { Router, NavigationStart } from '@angular/router';
import { Observable, Subject } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class AlertService {
private subject = new Subject<any>();
private keepAfterRouteChange = false;
constructor(private router: Router) {
// clear alert messages on route change unless 'keepAfterRouteChange' flag is true
this.router.events.subscribe(event => {
if (event instanceof NavigationStart) {
if (this.keepAfterRouteChange) {
// only keep for a single route change
this.keepAfterRouteChange = false;
} else {
// clear alert message
this.clear();
}
}
});
}
getAlert(): Observable<any> {
return this.subject.asObservable();
}
success(message: string, keepAfterRouteChange = false) {
this.keepAfterRouteChange = keepAfterRouteChange;
this.subject.next({ type: 'success', text: message });
}
error(message: string, keepAfterRouteChange = false) {
this.keepAfterRouteChange = keepAfterRouteChange;
this.subject.next({ type: 'error', text: message });
}
clear() {
// clear by calling subject.next() without parameters
this.subject.next();
}
}

View File

@ -0,0 +1,37 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { User } from '@/_models';
@Injectable({ providedIn: 'root' })
export class AuthenticationService {
private currentUserSubject: BehaviorSubject<User>;
public currentUser: Observable<User>;
constructor(private http: HttpClient) {
this.currentUserSubject = new BehaviorSubject<User>(JSON.parse(localStorage.getItem('currentUser')));
this.currentUser = this.currentUserSubject.asObservable();
}
public get currentUserValue(): User {
return this.currentUserSubject.value;
}
login(username, password) {
return this.http.post<any>(`${config.apiUrl}/users/authenticate`, { username, password })
.pipe(map(user => {
// store user details and jwt token in local storage to keep user logged in between page refreshes
localStorage.setItem('currentUser', JSON.stringify(user));
this.currentUserSubject.next(user);
return user;
}));
}
logout() {
// remove user from local storage and set current user to null
localStorage.removeItem('currentUser');
this.currentUserSubject.next(null);
}
}

View File

@ -0,0 +1,3 @@
export * from './alert.service';
export * from './authentication.service';
export * from './user.service';

View File

@ -0,0 +1,21 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { User } from '@/_models';
@Injectable({ providedIn: 'root' })
export class UserService {
constructor(private http: HttpClient) { }
getAll() {
return this.http.get<User[]>(`${config.apiUrl}/users`);
}
register(user: User) {
return this.http.post(`${config.apiUrl}/users/register`, user);
}
delete(id: number) {
return this.http.delete(`${config.apiUrl}/users/${id}`);
}
}

View File

@ -0,0 +1,20 @@
<!-- nav -->
<nav class="navbar navbar-expand navbar-dark bg-dark" *ngIf="currentUser">
<div class="navbar-nav">
<a class="nav-item nav-link" routerLink="/">Challenge accepted</a>
<a class="nav-item nav-link" (click)="logout()">Logout</a>
</div>
</nav>
<!-- main app container -->
<div class="jumbotron">
<div class="container">
<div class="row">
<div class="col-sm-6 offset-sm-3">
<alert></alert>
<router-outlet></router-outlet>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,24 @@
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { AuthenticationService } from './_services';
import { User } from './_models';
import './_content/app.less';
@Component({ selector: 'app', templateUrl: 'app.component.html' })
export class AppComponent {
currentUser: User;
constructor(
private router: Router,
private authenticationService: AuthenticationService
) {
this.authenticationService.currentUser.subscribe(x => this.currentUser = x);
}
logout() {
this.authenticationService.logout();
this.router.navigate(['/login']);
}
}

View File

@ -0,0 +1,40 @@
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
// used to create fake backend
import { fakeBackendProvider } from './_helpers';
import { appRoutingModule } from './app.routing';
import { JwtInterceptor, ErrorInterceptor } from './_helpers';
import { AppComponent } from './app.component';
import { HomeComponent } from './home';
import { LoginComponent } from './login';
import { RegisterComponent } from './register';
import { AlertComponent } from './_components';
@NgModule({
imports: [
BrowserModule,
ReactiveFormsModule,
HttpClientModule,
appRoutingModule
],
declarations: [
AppComponent,
HomeComponent,
LoginComponent,
RegisterComponent,
AlertComponent
],
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true },
// provider used to create fake backend
fakeBackendProvider
],
bootstrap: [AppComponent]
})
export class AppModule { };

View File

@ -0,0 +1,17 @@
import { Routes, RouterModule } from '@angular/router';
import { HomeComponent } from './home';
import { LoginComponent } from './login';
import { RegisterComponent } from './register';
import { AuthGuard } from './_helpers';
const routes: Routes = [
{ path: '', component: HomeComponent, canActivate: [AuthGuard] },
{ path: 'login', component: LoginComponent },
{ path: 'register', component: RegisterComponent },
// otherwise redirect to home
{ path: '**', redirectTo: '' }
];
export const appRoutingModule = RouterModule.forRoot(routes);

View File

@ -0,0 +1,26 @@
<h2>Hi {{currentUser.firstName}}, Challenge Accepted!</h2>
<h3>Obecne wyzwania:</h3>
<ul>
<li>
<a>Dwa litry coli w 20 sekund!</a>
<div class="btn-group" role="group" aria-label="..." style="padding: 10px;">
<button type="button" class="btn btn-default" style="background-color: blue; color: white;">Obstaw</button>
<button type="button" class="btn btn-default" style="background-color: green;">Podejmij</button>
</div>
</li>
<li>
<a>Zdobądź numer swojej nauczycielki!</a>
<div class="btn-group" role="group" aria-label="..." style="padding: 10px;">
<button type="button" class="btn btn-default" style="background-color: blue; color: white;">Obstaw</button>
<button type="button" class="btn btn-default" style="background-color: green;">Podejmij</button>
</div>
</li>
<li>
<a>Pokaz dupę</a>
<div class="btn-group" role="group" aria-label="..." style="padding: 10px;">
<button type="button" class="btn btn-default" style="background-color: blue; color: white;">Obstaw</button>
<button type="button" class="btn btn-default" style="background-color: green;">Podejmij</button>
</div>
</li>
</ul>

View File

@ -0,0 +1,34 @@
import { Component, OnInit } from '@angular/core';
import { first } from 'rxjs/operators';
import { User } from '@/_models';
import { UserService, AuthenticationService } from '@/_services';
@Component({ templateUrl: 'home.component.html' })
export class HomeComponent implements OnInit {
currentUser: User;
users = [];
constructor(
private authenticationService: AuthenticationService,
private userService: UserService
) {
this.currentUser = this.authenticationService.currentUserValue;
}
ngOnInit() {
this.loadAllUsers();
}
deleteUser(id: number) {
this.userService.delete(id)
.pipe(first())
.subscribe(() => this.loadAllUsers());
}
private loadAllUsers() {
this.userService.getAll()
.pipe(first())
.subscribe(users => this.users = users);
}
}

View File

@ -0,0 +1 @@
export * from './home.component';

View File

@ -0,0 +1 @@
export * from './login.component';

View File

@ -0,0 +1,25 @@
<h1 style="padding: 70px;">Challenge Accepted</h1>
<h2>Login</h2>
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
<div class="form-group">
<label for="username">Username</label>
<input type="text" formControlName="username" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.username.errors }" />
<div *ngIf="submitted && f.username.errors" class="invalid-feedback">
<div *ngIf="f.username.errors.required">Username is required</div>
</div>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" formControlName="password" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.password.errors }" />
<div *ngIf="submitted && f.password.errors" class="invalid-feedback">
<div *ngIf="f.password.errors.required">Password is required</div>
</div>
</div>
<div class="form-group">
<button [disabled]="loading" class="btn btn-primary">
<span *ngIf="loading" class="spinner-border spinner-border-sm mr-1"></span>
Login
</button>
<a routerLink="/register" class="btn btn-link">Register</a>
</div>
</form>

View File

@ -0,0 +1,64 @@
import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { first } from 'rxjs/operators';
import { AlertService, AuthenticationService } from '@/_services';
@Component({ templateUrl: 'login.component.html' })
export class LoginComponent implements OnInit {
loginForm: FormGroup;
loading = false;
submitted = false;
returnUrl: string;
constructor(
private formBuilder: FormBuilder,
private route: ActivatedRoute,
private router: Router,
private authenticationService: AuthenticationService,
private alertService: AlertService
) {
// redirect to home if already logged in
if (this.authenticationService.currentUserValue) {
this.router.navigate(['/']);
}
}
ngOnInit() {
this.loginForm = this.formBuilder.group({
username: ['', Validators.required],
password: ['', Validators.required]
});
// get return url from route parameters or default to '/'
this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/';
}
// convenience getter for easy access to form fields
get f() { return this.loginForm.controls; }
onSubmit() {
this.submitted = true;
// reset alerts on submit
this.alertService.clear();
// stop here if form is invalid
if (this.loginForm.invalid) {
return;
}
this.loading = true;
this.authenticationService.login(this.f.username.value, this.f.password.value)
.pipe(first())
.subscribe(
data => {
this.router.navigate([this.returnUrl]);
},
error => {
this.alertService.error(error);
this.loading = false;
});
}
}

View File

@ -0,0 +1 @@
export * from './register.component';

View File

@ -0,0 +1,39 @@
<h2>Register</h2>
<form [formGroup]="registerForm" (ngSubmit)="onSubmit()">
<div class="form-group">
<label for="firstName">First Name</label>
<input type="text" formControlName="firstName" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.firstName.errors }" />
<div *ngIf="submitted && f.firstName.errors" class="invalid-feedback">
<div *ngIf="f.firstName.errors.required">First Name is required</div>
</div>
</div>
<div class="form-group">
<label for="lastName">Last Name</label>
<input type="text" formControlName="lastName" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.lastName.errors }" />
<div *ngIf="submitted && f.lastName.errors" class="invalid-feedback">
<div *ngIf="f.lastName.errors.required">Last Name is required</div>
</div>
</div>
<div class="form-group">
<label for="username">Username</label>
<input type="text" formControlName="username" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.username.errors }" />
<div *ngIf="submitted && f.username.errors" class="invalid-feedback">
<div *ngIf="f.username.errors.required">Username is required</div>
</div>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" formControlName="password" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.password.errors }" />
<div *ngIf="submitted && f.password.errors" class="invalid-feedback">
<div *ngIf="f.password.errors.required">Password is required</div>
<div *ngIf="f.password.errors.minlength">Password must be at least 6 characters</div>
</div>
</div>
<div class="form-group">
<button [disabled]="loading" class="btn btn-primary">
<span *ngIf="loading" class="spinner-border spinner-border-sm mr-1"></span>
Register
</button>
<a routerLink="/login" class="btn btn-link">Cancel</a>
</div>
</form>

View File

@ -0,0 +1,63 @@
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { first } from 'rxjs/operators';
import { AlertService, UserService, AuthenticationService } from '@/_services';
@Component({ templateUrl: 'register.component.html' })
export class RegisterComponent implements OnInit {
registerForm: FormGroup;
loading = false;
submitted = false;
constructor(
private formBuilder: FormBuilder,
private router: Router,
private authenticationService: AuthenticationService,
private userService: UserService,
private alertService: AlertService
) {
// redirect to home if already logged in
if (this.authenticationService.currentUserValue) {
this.router.navigate(['/']);
}
}
ngOnInit() {
this.registerForm = this.formBuilder.group({
firstName: ['', Validators.required],
lastName: ['', Validators.required],
username: ['', Validators.required],
password: ['', [Validators.required, Validators.minLength(6)]]
});
}
// convenience getter for easy access to form fields
get f() { return this.registerForm.controls; }
onSubmit() {
this.submitted = true;
// reset alerts on submit
this.alertService.clear();
// stop here if form is invalid
if (this.registerForm.invalid) {
return;
}
this.loading = true;
this.userService.register(this.registerForm.value)
.pipe(first())
.subscribe(
data => {
this.alertService.success('Registration successful', true);
this.router.navigate(['/login']);
},
error => {
this.alertService.error(error);
this.loading = false;
});
}
}

14
front/src/index.html Normal file
View File

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<base href="/" />
<title>Angular 8 User Registration and Login Example</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- bootstrap css -->
<link href="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet" />
</head>
<body>
<app>Loading...</app>
</body>
</html>

6
front/src/main.ts Normal file
View File

@ -0,0 +1,6 @@
import './polyfills';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
platformBrowserDynamic().bootstrapModule(AppModule);

2
front/src/polyfills.ts Normal file
View File

@ -0,0 +1,2 @@
import 'core-js/features/reflect';
import 'zone.js/dist/zone';

2
front/src/typings.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
// so the typescript compiler doesn't complain about the global config object
declare var config: any;

13
front/tsconfig.json Normal file
View File

@ -0,0 +1,13 @@
{
"compilerOptions": {
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "ES5",
"baseUrl": "src",
"paths": {
"@/*": [
"app/*"
]
}
}
}

59
front/webpack.config.js Normal file
View File

@ -0,0 +1,59 @@
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');
module.exports = {
entry: './src/main.ts',
resolve: {
extensions: ['.ts', '.js'],
alias: {
'@': path.resolve(__dirname, 'src/app/'),
}
},
module: {
rules: [
{
test: /\.ts$/,
use: ['ts-loader', 'angular2-template-loader']
},
{
test: /\.html$/,
use: 'html-loader'
},
{
test: /\.less$/,
use: ['style-loader', 'css-loader', 'less-loader']
},
// workaround for warning: System.import() is deprecated and will be removed soon. Use import() instead.
{
test: /[\/\\]@angular[\/\\].+\.js$/,
parser: { system: true }
}
]
},
plugins: [
new HtmlWebpackPlugin({ template: './src/index.html' }),
new webpack.DefinePlugin({
// global app config object
config: JSON.stringify({
apiUrl: 'http://localhost:4000'
})
}),
// workaround for warning: Critical dependency: the request of a dependency is an expression
new webpack.ContextReplacementPlugin(
/\@angular(\\|\/)core(\\|\/)fesm5/,
path.resolve(__dirname, 'src')
)
],
optimization: {
splitChunks: {
chunks: 'all',
},
runtimeChunk: true
},
devServer: {
historyApiFallback: true
}
}