From e7b68f5ab8d472301191085b24f857ec653466d6 Mon Sep 17 00:00:00 2001 From: GRIMMR3AP3R Date: Wed, 15 Aug 2018 16:02:12 -0500 Subject: [PATCH 01/31] style(code): update code formatting, style and ng fix This has an updated editorconfig to ensure C# styling. Project files have been updated with better code styling. There are a bunch of other little tweaks but the best fix is bringing back the use of NG. With this fix ng g c blah\blah will no longer bark about "skip-import option to skip importing components in NgModule". The problem was Angluar.json didn't convert properly. --- .editorconfig | 193 ++++++++++- ClientApp/app/_styles.scss | 24 +- ClientApp/app/_variables.scss | 6 +- ClientApp/app/app.component.scss | 8 +- ClientApp/app/app.component.ts | 184 +++++----- ClientApp/app/app.module.browser.ts | 50 ++- ClientApp/app/app.module.server.ts | 16 +- ClientApp/app/app.module.ts | 324 ++++++++++-------- .../components/navmenu/navmenu.component.scss | 10 +- .../components/navmenu/navmenu.component.ts | 27 +- .../user-detail/user-detail.component.html | 2 +- .../user-detail/user-detail.component.scss | 2 +- .../user-detail/user-detail.component.ts | 74 ++-- .../containers/counter/counter.component.html | 4 +- .../counter/counter.component.spec.ts | 38 +- .../containers/counter/counter.component.ts | 12 +- .../app/containers/home/home.component.html | 21 +- .../app/containers/home/home.component.ts | 29 +- .../app/containers/lazy/lazy.component.ts | 2 +- ClientApp/app/containers/lazy/lazy.module.ts | 6 +- .../ngx-bootstrap.component.html | 24 +- .../ngx-bootstrap.component.ts | 52 ++- .../not-found/not-found.component.html | 4 +- .../not-found/not-found.component.ts | 8 +- .../app/containers/users/users.component.scss | 16 +- .../app/containers/users/users.component.ts | 51 +-- ClientApp/app/models/User.ts | 6 +- ClientApp/app/shared/link.service.ts | 156 ++++----- ClientApp/app/shared/user.service.ts | 18 +- ClientApp/boot.browser.ts | 12 +- ClientApp/boot.server.PRODUCTION.ts | 23 +- ClientApp/boot.server.ts | 10 +- ClientApp/polyfills/browser.polyfills.ts | 4 +- ClientApp/polyfills/polyfills.ts | 35 +- ClientApp/polyfills/server.polyfills.ts | 5 +- ClientApp/test/boot-tests.js | 8 +- ClientApp/test/karma.conf.js | 11 +- ClientApp/test/webpack.config.test.js | 23 +- ClientApp/tsconfig.app.json | 6 +- ClientApp/tsconfig.spec.json | 14 +- Program.cs | 33 ++ Server/Controllers/HomeController.cs | 102 +++--- Server/Data/DbInitializer.cs | 54 ++- Server/Data/SpaDbContext.cs | 12 +- Server/Helpers/HttpRequestExtensions.cs | 82 ++--- Server/Models/IRequest.cs | 14 +- Server/Models/TransferData.cs | 14 +- Server/Models/User.cs | 13 +- Server/RestAPI/UsersController.cs | 182 +++++----- Startup.cs | 188 +++++----- angular.json | 82 ++++- webpack.additions.js | 6 +- webpack.config.js | 198 ++++++----- wwwroot/manifest.json | 6 +- 54 files changed, 1370 insertions(+), 1134 deletions(-) create mode 100644 Program.cs diff --git a/.editorconfig b/.editorconfig index f1cc3ad3..90bafbc1 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,15 +1,196 @@ -# http://editorconfig.org - +############################### +# Core EditorConfig Options # +############################### root = true +# All files [*] -charset = utf-8 indent_style = space -indent_size = 2 -end_of_line = lf + +# Code files +[*.{cs,csx,vb,vbx}] +indent_size = 4 insert_final_newline = true -trim_trailing_whitespace = true +charset = utf-8-bom [*.md] insert_final_newline = false trim_trailing_whitespace = false + +############################### +# .NET Coding Conventions # +############################### + +# Solution Files +[*.sln] +indent_style = tab + +# XML Project Files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] +indent_size = 2 + +# Configuration Files +[*.{json,xml,yml,config,props,targets,nuspec,resx,ruleset,vsixmanifest,vsct}] +indent_size = 2 + +# Dotnet Code Style Settings +# See https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference +# See http://kent-boogaart.com/blog/editorconfig-reference-for-c-developers +[*.{cs,csx,cake,vb}] +dotnet_sort_system_directives_first = true:warning +dotnet_style_coalesce_expression = true:warning +dotnet_style_collection_initializer = true:warning +dotnet_style_explicit_tuple_names = true:warning +dotnet_style_null_propagation = true:warning +dotnet_style_object_initializer = true:warning +dotnet_style_predefined_type_for_locals_parameters_members = true:warning +dotnet_style_predefined_type_for_member_access = true:warning +dotnet_style_qualification_for_event = true:warning +dotnet_style_qualification_for_field = true:warning +dotnet_style_qualification_for_method = true:warning +dotnet_style_qualification_for_property = true:warning + +# Naming Symbols +# constant_fields - Define constant fields +dotnet_naming_symbols.constant_fields.applicable_kinds = field +dotnet_naming_symbols.constant_fields.required_modifiers = const +# non_private_readonly_fields - Define public, internal and protected readonly fields +dotnet_naming_symbols.non_private_readonly_fields.applicable_accessibilities = public, internal, protected +dotnet_naming_symbols.non_private_readonly_fields.applicable_kinds = field +dotnet_naming_symbols.non_private_readonly_fields.required_modifiers = readonly +# static_readonly_fields - Define static and readonly fields +dotnet_naming_symbols.static_readonly_fields.applicable_kinds = field +dotnet_naming_symbols.static_readonly_fields.required_modifiers = static, readonly +# private_readonly_fields - Define private readonly fields +dotnet_naming_symbols.private_readonly_fields.applicable_accessibilities = private +dotnet_naming_symbols.private_readonly_fields.applicable_kinds = field +dotnet_naming_symbols.private_readonly_fields.required_modifiers = readonly +# public_internal_fields - Define public and internal fields +dotnet_naming_symbols.public_internal_fields.applicable_accessibilities = public, internal +dotnet_naming_symbols.public_internal_fields.applicable_kinds = field +# private_protected_fields - Define private and protected fields +dotnet_naming_symbols.private_protected_fields.applicable_accessibilities = private, protected +dotnet_naming_symbols.private_protected_fields.applicable_kinds = field +# public_symbols - Define any public symbol +dotnet_naming_symbols.public_symbols.applicable_accessibilities = public, internal, protected, protected_internal +dotnet_naming_symbols.public_symbols.applicable_kinds = method, property, event, delegate +# parameters - Defines any parameter +dotnet_naming_symbols.parameters.applicable_kinds = parameter +# non_interface_types - Defines class, struct, enum and delegate types +dotnet_naming_symbols.non_interface_types.applicable_kinds = class, struct, enum, delegate +# interface_types - Defines interfaces +dotnet_naming_symbols.interface_types.applicable_kinds = interface + +# Naming Styles +# camel_case - Define the camelCase style +dotnet_naming_style.camel_case.capitalization = camel_case +# pascal_case - Define the Pascal_case style +dotnet_naming_style.pascal_case.capitalization = pascal_case +# first_upper - The first character must start with an upper-case character +dotnet_naming_style.first_upper.capitalization = first_word_upper +# prefix_interface_interface_with_i - Interfaces must be PascalCase and the first character of an interface must be an 'I' +dotnet_naming_style.prefix_interface_interface_with_i.capitalization = pascal_case +dotnet_naming_style.prefix_interface_interface_with_i.required_prefix = I + +# Naming Rules +# Constant fields must be PascalCase +dotnet_naming_rule.constant_fields_must_be_pascal_case.severity = warning +dotnet_naming_rule.constant_fields_must_be_pascal_case.symbols = constant_fields +dotnet_naming_rule.constant_fields_must_be_pascal_case.style = pascal_case +# Public, internal and protected readonly fields must be PascalCase +dotnet_naming_rule.non_private_readonly_fields_must_be_pascal_case.severity = warning +dotnet_naming_rule.non_private_readonly_fields_must_be_pascal_case.symbols = non_private_readonly_fields +dotnet_naming_rule.non_private_readonly_fields_must_be_pascal_case.style = pascal_case +# Static readonly fields must be PascalCase +dotnet_naming_rule.static_readonly_fields_must_be_pascal_case.severity = warning +dotnet_naming_rule.static_readonly_fields_must_be_pascal_case.symbols = static_readonly_fields +dotnet_naming_rule.static_readonly_fields_must_be_pascal_case.style = pascal_case +# Private readonly fields must be camelCase +dotnet_naming_rule.private_readonly_fields_must_be_camel_case.severity = warning +dotnet_naming_rule.private_readonly_fields_must_be_camel_case.symbols = private_readonly_fields +dotnet_naming_rule.private_readonly_fields_must_be_camel_case.style = camel_case +# Public and internal fields must be PascalCase +dotnet_naming_rule.public_internal_fields_must_be_pascal_case.severity = warning +dotnet_naming_rule.public_internal_fields_must_be_pascal_case.symbols = public_internal_fields +dotnet_naming_rule.public_internal_fields_must_be_pascal_case.style = pascal_case +# Private and protected fields must be camelCase +dotnet_naming_rule.private_protected_fields_must_be_camel_case.severity = warning +dotnet_naming_rule.private_protected_fields_must_be_camel_case.symbols = private_protected_fields +dotnet_naming_rule.private_protected_fields_must_be_camel_case.style = camel_case +# Public members must be capitalized +dotnet_naming_rule.public_members_must_be_capitalized.severity = warning +dotnet_naming_rule.public_members_must_be_capitalized.symbols = public_symbols +dotnet_naming_rule.public_members_must_be_capitalized.style = first_upper +# Parameters must be camelCase +dotnet_naming_rule.parameters_must_be_camel_case.severity = warning +dotnet_naming_rule.parameters_must_be_camel_case.symbols = parameters +dotnet_naming_rule.parameters_must_be_camel_case.style = camel_case +# Class, struct, enum and delegates must be PascalCase +dotnet_naming_rule.non_interface_types_must_be_pascal_case.severity = warning +dotnet_naming_rule.non_interface_types_must_be_pascal_case.symbols = non_interface_types +dotnet_naming_rule.non_interface_types_must_be_pascal_case.style = pascal_case +# Interfaces must be PascalCase and start with an 'I' +dotnet_naming_rule.interface_types_must_be_prefixed_with_i.severity = warning +dotnet_naming_rule.interface_types_must_be_prefixed_with_i.symbols = interface_types +dotnet_naming_rule.interface_types_must_be_prefixed_with_i.style = prefix_interface_interface_with_i + +# C# Code Style Settings +# See https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference +# See http://kent-boogaart.com/blog/editorconfig-reference-for-c-developers +[*.{cs,csx,cake}] +# Indentation Options +csharp_indent_block_contents = true:warning +csharp_indent_braces = false:warning +csharp_indent_case_contents = true:warning +csharp_indent_labels = no_change:warning +csharp_indent_switch_labels = true:warning +# Style Options +csharp_style_conditional_delegate_call = true:warning +csharp_style_expression_bodied_accessors = true:warning +csharp_style_expression_bodied_constructors = true:warning +csharp_style_expression_bodied_indexers = true:warning +csharp_style_expression_bodied_methods = true:warning +csharp_style_expression_bodied_operators = true:warning +csharp_style_expression_bodied_properties = true:warning +csharp_style_inlined_variable_declaration = true:warning +csharp_style_pattern_matching_over_as_with_null_check = true:warning +csharp_style_pattern_matching_over_is_with_cast_check = true:warning +csharp_style_throw_expression = true:warning +csharp_style_var_elsewhere = true:warning +csharp_style_var_for_built_in_types = true:warning +csharp_style_var_when_type_is_apparent = true:warning +# New Line Options +csharp_new_line_before_catch = true:warning +csharp_new_line_before_else = true:warning +csharp_new_line_before_finally = true:warning +csharp_new_line_before_members_in_anonymous_types = true:warning +csharp_new_line_before_members_in_object_initializers = true:warning +# BUG: Warning level cannot be set https://github.com/dotnet/roslyn/issues/18010 +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true:warning +# Spacing Options +csharp_space_after_cast = false:warning +csharp_space_after_colon_in_inheritance_clause = true:warning +csharp_space_after_comma = true:warning +csharp_space_after_dot = false:warning +csharp_space_after_keywords_in_control_flow_statements = true:warning +csharp_space_after_semicolon_in_for_statement = true:warning +csharp_space_around_binary_operators = before_and_after:warning +csharp_space_around_declaration_statements = do_not_ignore:warning +csharp_space_before_colon_in_inheritance_clause = true:warning +csharp_space_before_comma = false:warning +csharp_space_before_dot = false:warning +csharp_space_before_semicolon_in_for_statement = false:warning +csharp_space_before_open_square_brackets = false:warning +csharp_space_between_empty_square_brackets = false:warning +csharp_space_between_method_declaration_name_and_open_parenthesis = false:warning +csharp_space_between_method_declaration_parameter_list_parentheses = false:warning +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false:warning +csharp_space_between_method_call_name_and_opening_parenthesis = false:warning +csharp_space_between_method_call_parameter_list_parentheses = false:warning +csharp_space_between_method_call_empty_parameter_list_parentheses = false:warning +csharp_space_between_parentheses = expressions:warning +csharp_space_between_square_brackets = false:warning +# Wrapping Options +csharp_preserve_single_line_blocks = true:warning +csharp_preserve_single_line_statements = false:warning diff --git a/ClientApp/app/_styles.scss b/ClientApp/app/_styles.scss index 42cf15fe..87ea278f 100644 --- a/ClientApp/app/_styles.scss +++ b/ClientApp/app/_styles.scss @@ -1,22 +1,26 @@ $body-bg: #f1f1f1; $body-color: #111; -$theme-colors: ( "primary": #216DAD); -$theme-colors: ( "accent": #669ECD); +$theme-colors: ( + 'primary': #216dad +); +$theme-colors: ( + 'accent': #669ecd +); - -@import "http://github.com/~bootstrap/scss/bootstrap"; +@import 'http://github.com/~bootstrap/scss/bootstrap'; .panel { - box-shadow: 0 3px 1px -2px rgba(0, 0, 0, .2), 0 2px 2px 0 rgba(0, 0, 0, .14), 0 1px 5px 0 rgba(0, 0, 0, .12); + box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14), + 0 1px 5px 0 rgba(0, 0, 0, 0.12); height: 100%; flex: 1; - background-color: rgba(255, 255, 255, .30); - border-radius: .25rem; + background-color: rgba(255, 255, 255, 0.3); + border-radius: 0.25rem; .title { background-color: #86afd0; - color: #FFFFFF; + color: #ffffff; text-align: center; - border-top-left-radius: .25rem; - border-top-right-radius: .25rem; + border-top-left-radius: 0.25rem; + border-top-right-radius: 0.25rem; } .body { display: flex; diff --git a/ClientApp/app/_variables.scss b/ClientApp/app/_variables.scss index 2d34745a..313015eb 100644 --- a/ClientApp/app/_variables.scss +++ b/ClientApp/app/_variables.scss @@ -1,6 +1,6 @@ -@import "http://github.com/styles"; -$header-height:50px; -$menu-max-width:25%; +@import 'http://github.com/styles'; +$header-height: 50px; +$menu-max-width: 25%; $navbar-default-bg: #312312; $light-orange: #ff8c00; $navbar-default-color: $light-orange; diff --git a/ClientApp/app/app.component.scss b/ClientApp/app/app.component.scss index 8705be92..1e7eea64 100644 --- a/ClientApp/app/app.component.scss +++ b/ClientApp/app/app.component.scss @@ -1,4 +1,4 @@ -@import "http://github.com/variables"; +@import 'http://github.com/variables'; /* *** Overall APP Styling can go here *** -------------------------------------------- Note: This Component has ViewEncapsulation.None so the styles will bleed out @@ -15,7 +15,7 @@ body { } h1 { - border-bottom: 3px theme-color("accent") solid; + border-bottom: 3px theme-color('accent') solid; font-size: 24px; } @@ -40,7 +40,7 @@ ul li { blockquote { margin: 25px 10px; padding: 10px 35px 10px 10px; - border-left: 10px color("green") solid; + border-left: 10px color('green') solid; background: $gray-100; } @@ -57,7 +57,7 @@ blockquote a:hover { margin-left: $menu-max-width; } h1 { - border-bottom: 5px #4189C7 solid; + border-bottom: 5px #4189c7 solid; font-size: 36px; } h2 { diff --git a/ClientApp/app/app.component.ts b/ClientApp/app/app.component.ts index 614cba3e..415c196a 100644 --- a/ClientApp/app/app.component.ts +++ b/ClientApp/app/app.component.ts @@ -1,101 +1,103 @@ - -import {mergeMap, map, filter} from 'rxjs/operators'; -import { Component, OnInit, OnDestroy, Inject, ViewEncapsulation, RendererFactory2, PLATFORM_ID, Injector } from '@angular/core'; -import { Router, NavigationEnd, ActivatedRoute, PRIMARY_OUTLET } from '@angular/router'; -import { Meta, Title, DOCUMENT, MetaDefinition } from '@angular/platform-browser'; -import { Subscription } from 'rxjs'; -import { isPlatformServer } from '@angular/common'; -import { LinkService } from './shared/link.service'; - +import { + Component, + Injector, + OnDestroy, + OnInit, + ViewEncapsulation +} from '@angular/core'; +import { Meta, Title } from '@angular/platform-browser'; +import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; +import { REQUEST } from '@nguniversal/aspnetcore-engine/tokens'; // i18n support import { TranslateService } from '@ngx-translate/core'; -import { REQUEST } from '@nguniversal/aspnetcore-engine/tokens'; +import { Subscription } from 'rxjs'; +import { filter, map, mergeMap } from 'rxjs/operators'; +import { LinkService } from './shared/link.service'; @Component({ - selector: 'app-root', - templateUrl: './app.component.html', - styleUrls: ['./app.component.scss'], - encapsulation: ViewEncapsulation.None + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.scss'], + encapsulation: ViewEncapsulation.None }) export class AppComponent implements OnInit, OnDestroy { - - // This will go at the END of your title for example "Home - Angular Universal..." <-- after the dash (-) - private endPageTitle: string = 'Angular Universal and ASP.NET Core Starter'; - // If no Title is provided, we'll use a default one before the dash(-) - private defaultPageTitle: string = 'My App'; - - private routerSub$: Subscription; - private request; - - constructor( - private router: Router, - private activatedRoute: ActivatedRoute, - private title: Title, - private meta: Meta, - private linkService: LinkService, - public translate: TranslateService, - private injector: Injector - ) { - // this language will be used as a fallback when a translation isn't found in the current language - translate.setDefaultLang('en'); - - // the lang to use, if the lang isn't available, it will use the current loader to get them - translate.use('en'); - - this.request = this.injector.get(REQUEST); - - console.log(`What's our REQUEST Object look like?`); - console.log(`The Request object only really exists on the Server, but on the Browser we can at least see Cookies`); - console.log(this.request); + // This will go at the END of your title for example "Home - Angular Universal..." <-- after the dash (-) + private endPageTitle: string = 'Angular Universal and ASP.NET Core Starter'; + // If no Title is provided, we'll use a default one before the dash(-) + private defaultPageTitle: string = 'My App'; + + private routerSub$: Subscription; + private request; + + constructor( + private router: Router, + private activatedRoute: ActivatedRoute, + private title: Title, + private meta: Meta, + private linkService: LinkService, + public translate: TranslateService, + private injector: Injector + ) { + // this language will be used as a fallback when a translation isn't found in the current language + translate.setDefaultLang('en'); + + // the lang to use, if the lang isn't available, it will use the current loader to get them + translate.use('en'); + + this.request = this.injector.get(REQUEST); + + console.log(`What's our REQUEST Object look like?`); + console.log( + `The Request object only really exists on the Server, but on the Browser we can at least see Cookies` + ); + console.log(this.request); + } + + ngOnInit() { + // Change "Title" on every navigationEnd event + // Titles come from the data.title property on all Routes (see app.routes.ts) + this._changeTitleOnNavigation(); + } + + ngOnDestroy() { + // Subscription clean-up + this.routerSub$.unsubscribe(); + } + + private _changeTitleOnNavigation() { + this.routerSub$ = this.router.events + .pipe( + filter(event => event instanceof NavigationEnd), + map(() => this.activatedRoute), + map(route => { + while (route.firstChild) route = route.firstChild; + return route; + }), + filter(route => route.outlet === 'primary'), + mergeMap(route => route.data) + ) + .subscribe(event => { + this._setMetaAndLinks(event); + }); + } + + private _setMetaAndLinks(event) { + // Set Title if available, otherwise leave the default Title + const title = event['title'] + ? `${event['title']} - ${this.endPageTitle}` + : `${this.defaultPageTitle} - ${this.endPageTitle}`; + + this.title.setTitle(title); + + const metaData = event['meta'] || []; + const linksData = event['links'] || []; + + for (let i = 0; i < metaData.length; i++) { + this.meta.updateTag(metaData[i]); } - ngOnInit() { - // Change "Title" on every navigationEnd event - // Titles come from the data.title property on all Routes (see app.routes.ts) - this._changeTitleOnNavigation(); + for (let i = 0; i < linksData.length; i++) { + this.linkService.addTag(linksData[i]); } - - ngOnDestroy() { - // Subscription clean-up - this.routerSub$.unsubscribe(); - } - - private _changeTitleOnNavigation() { - - this.routerSub$ = this.router.events.pipe( - filter(event => event instanceof NavigationEnd), - map(() => this.activatedRoute), - map(route => { - while (route.firstChild) route = route.firstChild; - return route; - }), - filter(route => route.outlet === 'primary'), - mergeMap(route => route.data),) - .subscribe((event) => { - this._setMetaAndLinks(event); - }); - } - - private _setMetaAndLinks(event) { - - // Set Title if available, otherwise leave the default Title - const title = event['title'] - ? `${event['title']} - ${this.endPageTitle}` - : `${this.defaultPageTitle} - ${this.endPageTitle}`; - - this.title.setTitle(title); - - const metaData = event['meta'] || []; - const linksData = event['links'] || []; - - for (let i = 0; i < metaData.length; i++) { - this.meta.updateTag(metaData[i]); - } - - for (let i = 0; i < linksData.length; i++) { - this.linkService.addTag(linksData[i]); - } - } - + } } - diff --git a/ClientApp/app/app.module.browser.ts b/ClientApp/app/app.module.browser.ts index f7371e80..5162c3ca 100644 --- a/ClientApp/app/app.module.browser.ts +++ b/ClientApp/app/app.module.browser.ts @@ -1,13 +1,9 @@ import { NgModule } from '@angular/core'; -import { BrowserModule } from '@angular/platform-browser'; -import { APP_BASE_HREF } from '@angular/common'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; - import { ORIGIN_URL, REQUEST } from '@nguniversal/aspnetcore-engine/tokens'; -import { AppModuleShared } from './app.module'; -import { AppComponent } from './app.component'; -import { BrowserTransferStateModule } from '@angular/platform-browser'; import { PrebootModule } from 'preboot'; +import { AppComponent } from './app.component'; +import { AppModuleShared } from './app.module'; export function getOriginUrl() { return window.location.origin; @@ -19,26 +15,26 @@ export function getRequest() { } @NgModule({ - bootstrap: [AppComponent], - imports: [ - PrebootModule.withConfig({ appRoot: 'app-root' }), - BrowserAnimationsModule, - - // Our Common AppModule - AppModuleShared + bootstrap: [AppComponent], + imports: [ + PrebootModule.withConfig({ appRoot: 'app-root' }), + BrowserAnimationsModule, - ], - providers: [ - { - // We need this for our Http calls since they'll be using an ORIGIN_URL provided in main.server - // (Also remember the Server requires Absolute URLs) - provide: ORIGIN_URL, - useFactory: (getOriginUrl) - }, { - // The server provides these in main.server - provide: REQUEST, - useFactory: (getRequest) - } - ] + // Our Common AppModule + AppModuleShared + ], + providers: [ + { + // We need this for our Http calls since they'll be using an ORIGIN_URL provided in main.server + // (Also remember the Server requires Absolute URLs) + provide: ORIGIN_URL, + useFactory: getOriginUrl + }, + { + // The server provides these in main.server + provide: REQUEST, + useFactory: getRequest + } + ] }) -export class AppModule { } +export class AppModule {} diff --git a/ClientApp/app/app.module.server.ts b/ClientApp/app/app.module.server.ts index 642a774c..e5fcb3de 100644 --- a/ClientApp/app/app.module.server.ts +++ b/ClientApp/app/app.module.server.ts @@ -1,13 +1,9 @@ import { NgModule } from '@angular/core'; -import { ServerModule } from '@angular/platform-server'; -import { BrowserModule } from '@angular/platform-browser'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; - -import { AppModuleShared } from './app.module'; -import { AppComponent } from './app.component'; -import { ServerTransferStateModule } from '@angular/platform-server'; - +import { ServerModule } from '@angular/platform-server'; import { PrebootModule } from 'preboot'; +import { AppComponent } from './app.component'; +import { AppModuleShared } from './app.module'; @NgModule({ bootstrap: [AppComponent], @@ -17,7 +13,7 @@ import { PrebootModule } from 'preboot'; ServerModule, PrebootModule.withConfig({ appRoot: 'app-root' }), - NoopAnimationsModule, + NoopAnimationsModule // HttpTransferCacheModule still needs fixes for 5.0 // Leave this commented out for now, as it breaks Server-renders @@ -26,7 +22,5 @@ import { PrebootModule } from 'preboot'; ] }) export class AppModule { - - constructor() { } - + constructor() {} } diff --git a/ClientApp/app/app.module.ts b/ClientApp/app/app.module.ts index 7cc9c4c0..a30f46d8 100644 --- a/ClientApp/app/app.module.ts +++ b/ClientApp/app/app.module.ts @@ -1,156 +1,212 @@ -import { NgModule, Inject } from '@angular/core'; -import { RouterModule, PreloadAllModules } from '@angular/router'; -import { CommonModule, APP_BASE_HREF } from '@angular/common'; -import { HttpModule, Http } from '@angular/http'; -import { HttpClientModule, HttpClient } from '@angular/common/http'; +import { CommonModule } from '@angular/common'; +import { HttpClient, HttpClientModule } from '@angular/common/http'; +import { NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { BrowserModule, BrowserTransferStateModule } from '@angular/platform-browser'; +import { + BrowserModule, + BrowserTransferStateModule +} from '@angular/platform-browser'; +import { PreloadAllModules, RouterModule } from '@angular/router'; +import { ORIGIN_URL } from '@nguniversal/aspnetcore-engine/tokens'; import { TransferHttpCacheModule } from '@nguniversal/common'; - -import { AccordionModule } from 'ngx-bootstrap'; - // i18n support -import { TranslateModule, TranslateLoader } from '@ngx-translate/core'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { TranslateHttpLoader } from '@ngx-translate/http-loader'; - +import { AccordionModule } from 'ngx-bootstrap'; import { AppComponent } from './app.component'; import { NavMenuComponent } from './components/navmenu/navmenu.component'; -import { HomeComponent } from './containers/home/home.component'; -import { UsersComponent } from './containers/users/users.component'; import { UserDetailComponent } from './components/user-detail/user-detail.component'; import { CounterComponent } from './containers/counter/counter.component'; -import { NotFoundComponent } from './containers/not-found/not-found.component'; +import { HomeComponent } from './containers/home/home.component'; import { NgxBootstrapComponent } from './containers/ngx-bootstrap-demo/ngx-bootstrap.component'; - +import { NotFoundComponent } from './containers/not-found/not-found.component'; +import { UsersComponent } from './containers/users/users.component'; import { LinkService } from './shared/link.service'; import { UserService } from './shared/user.service'; -import { ORIGIN_URL } from '@nguniversal/aspnetcore-engine/tokens'; export function createTranslateLoader(http: HttpClient, baseHref) { - // Temporary Azure hack - if (baseHref === null && typeof window !== 'undefined') { - baseHref = window.location.origin; - } - // i18n files are in `wwwroot/assets/` - return new TranslateHttpLoader(http, `${baseHref}/assets/i18n/`, '.json'); + // Temporary Azure hack + if (baseHref === null && typeof window !== 'undefined') { + baseHref = window.location.origin; + } + // i18n files are in `wwwroot/assets/` + return new TranslateHttpLoader(http, `${baseHref}/assets/i18n/`, '.json'); } @NgModule({ - declarations: [ - AppComponent, - NavMenuComponent, - CounterComponent, - UsersComponent, - UserDetailComponent, - HomeComponent, - NotFoundComponent, - NgxBootstrapComponent - ], - imports: [ - CommonModule, - BrowserModule.withServerTransition({ - appId: 'my-app-id' // make sure this matches with your Server NgModule - }), - HttpClientModule, - TransferHttpCacheModule, - BrowserTransferStateModule, - FormsModule, - ReactiveFormsModule, - AccordionModule.forRoot(), // You could also split this up if you don't want the Entire Module imported + declarations: [ + AppComponent, + NavMenuComponent, + CounterComponent, + UsersComponent, + UserDetailComponent, + HomeComponent, + NotFoundComponent, + NgxBootstrapComponent + ], + imports: [ + CommonModule, + BrowserModule.withServerTransition({ + appId: 'my-app-id' // make sure this matches with your Server NgModule + }), + HttpClientModule, + TransferHttpCacheModule, + BrowserTransferStateModule, + FormsModule, + ReactiveFormsModule, + AccordionModule.forRoot(), // You could also split this up if you don't want the Entire Module imported - // i18n support - TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useFactory: (createTranslateLoader), - deps: [HttpClient, [ORIGIN_URL]] - } - }), + // i18n support + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: createTranslateLoader, + deps: [HttpClient, [ORIGIN_URL]] + } + }), - // App Routing - RouterModule.forRoot([ - { - path: '', - redirectTo: 'home', - pathMatch: 'full' - }, - { - path: 'home', component: HomeComponent, + // App Routing + RouterModule.forRoot( + [ + { + path: '', + redirectTo: 'home', + pathMatch: 'full' + }, + { + path: 'home', + component: HomeComponent, - // *** SEO Magic *** - // We're using "data" in our Routes to pass in our <meta> <link> tag information - // Note: This is only happening for ROOT level Routes, you'd have to add some additional logic if you wanted this for Child level routing - // When you change Routes it will automatically append these to your document for you on the Server-side - // - check out app.component.ts to see how it's doing this - data: { - title: 'Homepage', - meta: [{ name: 'description', content: 'This is an example Description Meta tag!' }], - links: [ - { rel: 'canonical', href: 'http://blogs.example.com/blah/nice' }, - { rel: 'alternate', hreflang: 'es', href: 'http://es.example.com/' } - ] - } - }, - { - path: 'counter', component: CounterComponent, - data: { - title: 'Counter', - meta: [{ name: 'description', content: 'This is an Counter page Description!' }], - links: [ - { rel: 'canonical', href: 'http://blogs.example.com/counter/something' }, - { rel: 'alternate', hreflang: 'es', href: 'http://es.example.com/counter' } - ] - } - }, - { - path: 'users', component: UsersComponent, - data: { - title: 'Users REST example', - meta: [{ name: 'description', content: 'This is User REST API example page Description!' }], - links: [ - { rel: 'canonical', href: 'http://blogs.example.com/chat/something' }, - { rel: 'alternate', hreflang: 'es', href: 'http://es.example.com/users' } - ] - } - }, - { - path: 'ngx-bootstrap', component: NgxBootstrapComponent, - data: { - title: 'Ngx-bootstrap demo!!', - meta: [{ name: 'description', content: 'This is an Demo Bootstrap page Description!' }], - links: [ - { rel: 'canonical', href: 'http://blogs.example.com/bootstrap/something' }, - { rel: 'alternate', hreflang: 'es', href: 'http://es.example.com/bootstrap-demo' } - ] - } - }, + // *** SEO Magic *** + // We're using "data" in our Routes to pass in our <title> <meta> <link> tag information + // Note: This is only happening for ROOT level Routes, you'd have to add some additional logic if you wanted this for Child level routing + // When you change Routes it will automatically append these to your document for you on the Server-side + // - check out app.component.ts to see how it's doing this + data: { + title: 'Homepage', + meta: [ + { + name: 'description', + content: 'This is an example Description Meta tag!' + } + ], + links: [ + { rel: 'canonical', href: 'http://blogs.example.com/blah/nice' }, + { + rel: 'alternate', + hreflang: 'es', + href: 'http://es.example.com/' + } + ] + } + }, + { + path: 'counter', + component: CounterComponent, + data: { + title: 'Counter', + meta: [ + { + name: 'description', + content: 'This is an Counter page Description!' + } + ], + links: [ + { + rel: 'canonical', + href: 'http://blogs.example.com/counter/something' + }, + { + rel: 'alternate', + hreflang: 'es', + href: 'http://es.example.com/counter' + } + ] + } + }, + { + path: 'users', + component: UsersComponent, + data: { + title: 'Users REST example', + meta: [ + { + name: 'description', + content: 'This is User REST API example page Description!' + } + ], + links: [ + { + rel: 'canonical', + href: 'http://blogs.example.com/chat/something' + }, + { + rel: 'alternate', + hreflang: 'es', + href: 'http://es.example.com/users' + } + ] + } + }, + { + path: 'ngx-bootstrap', + component: NgxBootstrapComponent, + data: { + title: 'Ngx-bootstrap demo!!', + meta: [ + { + name: 'description', + content: 'This is an Demo Bootstrap page Description!' + } + ], + links: [ + { + rel: 'canonical', + href: 'http://blogs.example.com/bootstrap/something' + }, + { + rel: 'alternate', + hreflang: 'es', + href: 'http://es.example.com/bootstrap-demo' + } + ] + } + }, - { path: 'lazy', loadChildren: './containers/lazy/lazy.module#LazyModule'}, + { + path: 'lazy', + loadChildren: './containers/lazy/lazy.module#LazyModule' + }, - { - path: '**', component: NotFoundComponent, - data: { - title: '404 - Not found', - meta: [{ name: 'description', content: '404 - Error' }], - links: [ - { rel: 'canonical', href: 'http://blogs.example.com/bootstrap/something' }, - { rel: 'alternate', hreflang: 'es', href: 'http://es.example.com/bootstrap-demo' } - ] - } - } - ], { - // Router options - useHash: false, - preloadingStrategy: PreloadAllModules, - initialNavigation: 'enabled' - }) - ], - providers: [ - LinkService, - UserService, - TranslateModule - ], - bootstrap: [AppComponent] + { + path: '**', + component: NotFoundComponent, + data: { + title: '404 - Not found', + meta: [{ name: 'description', content: '404 - Error' }], + links: [ + { + rel: 'canonical', + href: 'http://blogs.example.com/bootstrap/something' + }, + { + rel: 'alternate', + hreflang: 'es', + href: 'http://es.example.com/bootstrap-demo' + } + ] + } + } + ], + { + // Router options + useHash: false, + preloadingStrategy: PreloadAllModules, + initialNavigation: 'enabled' + } + ) + ], + providers: [LinkService, UserService, TranslateModule], + bootstrap: [AppComponent] }) -export class AppModuleShared { -} +export class AppModuleShared {} diff --git a/ClientApp/app/components/navmenu/navmenu.component.scss b/ClientApp/app/components/navmenu/navmenu.component.scss index 462e9461..66115049 100644 --- a/ClientApp/app/components/navmenu/navmenu.component.scss +++ b/ClientApp/app/components/navmenu/navmenu.component.scss @@ -1,10 +1,10 @@ -@import "http://github.com/variables"; -// Mobile first styling. +@import 'http://github.com/variables'; +// Mobile first styling. /* Apply for small displays */ .navbar { position: fixed; - background-color: theme-color("primary"); + background-color: theme-color('primary'); top: 0; right: 0; left: 0; @@ -53,7 +53,7 @@ li a { li.link-active a, li.link-active a:hover, li.link-active a:focus { - background-color: theme-color("accent"); + background-color: theme-color('accent'); color: $body-bg; } @@ -81,7 +81,7 @@ li.link-active a:focus { .navbar, a, button { - color: theme-color("primary"); + color: theme-color('primary'); } .navbar-brand { padding: 15px 15px; diff --git a/ClientApp/app/components/navmenu/navmenu.component.ts b/ClientApp/app/components/navmenu/navmenu.component.ts index c5eb3e8c..d3dadb4d 100644 --- a/ClientApp/app/components/navmenu/navmenu.component.ts +++ b/ClientApp/app/components/navmenu/navmenu.component.ts @@ -1,23 +1,22 @@ import { Component } from '@angular/core'; @Component({ - selector: 'app-nav-menu', - templateUrl: './navmenu.component.html', - styleUrls: ['./navmenu.component.scss'] + selector: 'app-nav-menu', + templateUrl: './navmenu.component.html', + styleUrls: ['./navmenu.component.scss'] }) - export class NavMenuComponent { - collapse: string = 'collapse'; + collapse: string = 'collapse'; - collapseNavbar(): void { - if (this.collapse.length > 1) { - this.collapse = ''; - } else { - this.collapse = 'collapse'; - } + collapseNavbar(): void { + if (this.collapse.length > 1) { + this.collapse = ''; + } else { + this.collapse = 'collapse'; } + } - collapseMenu() { - this.collapse = 'collapse'; - } + collapseMenu() { + this.collapse = 'collapse'; + } } diff --git a/ClientApp/app/components/user-detail/user-detail.component.html b/ClientApp/app/components/user-detail/user-detail.component.html index 0b9ed7b1..f43b4bbb 100644 --- a/ClientApp/app/components/user-detail/user-detail.component.html +++ b/ClientApp/app/components/user-detail/user-detail.component.html @@ -1,6 +1,6 @@ <div class="panel"> <h2 class="title">{{user.name}} details:</h2> - <form class="body" [formGroup]="userForm"(ngSubmit)="updateUser()"> + <form class="body" [formGroup]="userForm" (ngSubmit)="updateUser()"> <div class="form-group"> <label for="userId">Id</label> <input type="input" formControlName="id" class="form-control" id="userId" placeholder="Id" readonly> diff --git a/ClientApp/app/components/user-detail/user-detail.component.scss b/ClientApp/app/components/user-detail/user-detail.component.scss index 15a7f257..a7809e89 100644 --- a/ClientApp/app/components/user-detail/user-detail.component.scss +++ b/ClientApp/app/components/user-detail/user-detail.component.scss @@ -1,4 +1,4 @@ -@import "http://github.com/variables"; +@import 'http://github.com/variables'; :host-context() { flex: 1; } diff --git a/ClientApp/app/components/user-detail/user-detail.component.ts b/ClientApp/app/components/user-detail/user-detail.component.ts index 2f2f5ffd..d14dbdc1 100644 --- a/ClientApp/app/components/user-detail/user-detail.component.ts +++ b/ClientApp/app/components/user-detail/user-detail.component.ts @@ -1,40 +1,52 @@ -import { Component, Input, Output, EventEmitter, OnInit, OnChanges, SimpleChanges } from '@angular/core'; +import { + Component, + EventEmitter, + Input, + OnChanges, + OnInit, + Output, + SimpleChanges +} from '@angular/core'; +import { FormControl, FormGroup } from '@angular/forms'; +import { debounceTime, distinctUntilChanged } from 'rxjs/operators'; import { IUser } from '../../models/User'; import { UserService } from '../../shared/user.service'; -import { FormControl, FormGroup } from '@angular/forms'; -import { debounce, debounceTime, distinctUntilChanged } from 'rxjs/operators'; @Component({ - selector: 'app-user-detail', - styleUrls: ['./user-detail.component.scss'], - templateUrl: './user-detail.component.html' + selector: 'app-user-detail', + styleUrls: ['./user-detail.component.scss'], + templateUrl: './user-detail.component.html' }) export class UserDetailComponent implements OnInit, OnChanges { + @Input() user: IUser; + @Output() userUpdate: EventEmitter<any> = new EventEmitter(); + userForm = new FormGroup({ + id: new FormControl(), + name: new FormControl() + }); + constructor(private userService: UserService) {} - @Input() user: IUser; - @Output() userUpdate: EventEmitter<any> = new EventEmitter(); - userForm = new FormGroup({ - id: new FormControl(), - name: new FormControl() - }); - constructor(private userService: UserService) { } - - ngOnInit() { - this.userForm.valueChanges.pipe( - debounceTime(400), - distinctUntilChanged() - ).subscribe(user => this.user = user); - } - ngOnChanges(changes: SimpleChanges) { - this.userForm.patchValue(this.user); - } + ngOnInit() { + this.userForm.valueChanges + .pipe( + debounceTime(400), + distinctUntilChanged() + ) + .subscribe(user => (this.user = user)); + } + ngOnChanges(changes: SimpleChanges) { + this.userForm.patchValue(this.user); + } - updateUser() { - this.userService.updateUser(this.userForm.value).subscribe(result => { - console.log('Put user result: ', result); - }, error => { - console.log(`There was an issue. ${error._body}.`); - }); - this.userUpdate.emit(this.user); - } + updateUser() { + this.userService.updateUser(this.userForm.value).subscribe( + result => { + console.log('Put user result: ', result); + }, + error => { + console.log(`There was an issue. ${error._body}.`); + } + ); + this.userUpdate.emit(this.user); + } } diff --git a/ClientApp/app/containers/counter/counter.component.html b/ClientApp/app/containers/counter/counter.component.html index cc2bbd59..d4dcafe4 100644 --- a/ClientApp/app/containers/counter/counter.component.html +++ b/ClientApp/app/containers/counter/counter.component.html @@ -2,6 +2,8 @@ <h1>Counter</h1> <p>This is a simple example of an Angular 2 component.</p> -<p>Current count: <strong>{{ currentCount }}</strong></p> +<p>Current count: + <strong>{{ currentCount }}</strong> +</p> <button (click)="incrementCounter()">Increment</button> diff --git a/ClientApp/app/containers/counter/counter.component.spec.ts b/ClientApp/app/containers/counter/counter.component.spec.ts index c1e54ed4..92202646 100644 --- a/ClientApp/app/containers/counter/counter.component.spec.ts +++ b/ClientApp/app/containers/counter/counter.component.spec.ts @@ -1,29 +1,27 @@ -/// <reference path="../../../../node_modules/@types/jasmine/index.d.ts" /> -import { assert } from 'chai'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { CounterComponent } from './counter.component'; -import { TestBed, async, ComponentFixture } from '@angular/core/testing'; let fixture: ComponentFixture<CounterComponent>; describe('Counter component', () => { - beforeEach(() => { - TestBed.configureTestingModule({ declarations: [CounterComponent] }); - fixture = TestBed.createComponent(CounterComponent); - fixture.detectChanges(); - }); + beforeEach(() => { + TestBed.configureTestingModule({ declarations: [CounterComponent] }); + fixture = TestBed.createComponent(CounterComponent); + fixture.detectChanges(); + }); - it('should display a title', async(() => { - const titleText = fixture.nativeElement.querySelector('h1').textContent; - expect(titleText).toEqual('Counter'); - })); + it('should display a title', async(() => { + const titleText = fixture.nativeElement.querySelector('h1').textContent; + expect(titleText).toEqual('Counter'); + })); - it('should start with count 0, then increments by 1 when clicked', async(() => { - const countElement = fixture.nativeElement.querySelector('strong'); - expect(countElement.textContent).toEqual('0'); + it('should start with count 0, then increments by 1 when clicked', async(() => { + const countElement = fixture.nativeElement.querySelector('strong'); + expect(countElement.textContent).toEqual('0'); - const incrementButton = fixture.nativeElement.querySelector('button'); - incrementButton.click(); - fixture.detectChanges(); - expect(countElement.textContent).toEqual('1'); - })); + const incrementButton = fixture.nativeElement.querySelector('button'); + incrementButton.click(); + fixture.detectChanges(); + expect(countElement.textContent).toEqual('1'); + })); }); diff --git a/ClientApp/app/containers/counter/counter.component.ts b/ClientApp/app/containers/counter/counter.component.ts index 5adb5195..42123687 100644 --- a/ClientApp/app/containers/counter/counter.component.ts +++ b/ClientApp/app/containers/counter/counter.component.ts @@ -1,13 +1,13 @@ import { Component } from '@angular/core'; @Component({ - selector: 'app-counter', - templateUrl: './counter.component.html' + selector: 'app-counter', + templateUrl: './counter.component.html' }) export class CounterComponent { - public currentCount = 0; + public currentCount = 0; - public incrementCounter() { - this.currentCount++; - } + public incrementCounter() { + this.currentCount++; + } } diff --git a/ClientApp/app/containers/home/home.component.html b/ClientApp/app/containers/home/home.component.html index 8f12abfc..f60e6d64 100644 --- a/ClientApp/app/containers/home/home.component.html +++ b/ClientApp/app/containers/home/home.component.html @@ -2,8 +2,10 @@ <blockquote> <strong>Enjoy the latest features from .NET Core & Angular 6.x!</strong> - <br> For more info check the repo here: <a href="https://github.com/MarkPieszak/aspnetcore-angular2-universal">AspNetCore-Angular-Universal repo</a> - <br><br> + <br> For more info check the repo here: + <a href="https://github.com/MarkPieszak/aspnetcore-angular2-universal">AspNetCore-Angular-Universal repo</a> + <br> + <br> </blockquote> <div class="row"> @@ -14,8 +16,7 @@ <h2>{{ 'HOME_FEATURE_LIST_TITLE' | translate }} </h2> <li> Angular 6.* front-end UI framework <ul> - <li>Angular **platform-server** (aka: Universal) - server-side rendering for SEO, deep-linking, and - incredible performance.</li> + <li>Angular **platform-server** (aka: Universal) - server-side rendering for SEO, deep-linking, and incredible performance.</li> <!--<li>HMR State Management - Don't lose your applications state during HMR!</li>--> <li>AoT (Ahead-of-time) production compilation for even faster Prod builds.</li> </ul> @@ -41,7 +42,13 @@ <h2>{{ 'HOME_FEATURE_LIST_TITLE' | translate }} </h2> <h2>{{ 'HOME_ISSUES_TITLE' | translate }}</h2> <ul> - <li><strong>Issues with this Starter?</strong> <br>Please post an issue here: <a href="https://github.com/MarkPieszak/aspnetcore-angular2-universal">AspNetCore-Angular2-Universal repo</a><br><br></li> + <li> + <strong>Issues with this Starter?</strong> + <br>Please post an issue here: + <a href="https://github.com/MarkPieszak/aspnetcore-angular2-universal">AspNetCore-Angular2-Universal repo</a> + <br> + <br> + </li> <!--<li><strong>Issues with <u>Universal</u> itself?</strong> <br>Please post an issue here: <a href="https://github.com/angular/universal">Angular Universal repo</a></li>--> </ul> </div> @@ -54,11 +61,11 @@ <h2>{{ 'HOME_ISSUES_TITLE' | translate }}</h2> <h2> {{ 'SWITCH_LANGUAGE' | translate }}</h2> <button class="btn btn-outline-secondary mr-2" (click)="setLanguage('en')"> - <span class="flag-icon flag-icon-us"></span> {{ 'ENGLISH' | translate }} + <span class="flag-icon flag-icon-us"></span> {{ 'ENGLISH' | translate }} </button> <button class="btn btn-outline-secondary" (click)="setLanguage('no')"> - <span class="flag-icon flag-icon-no"></span> {{ 'NORWEGIAN' | translate }} + <span class="flag-icon flag-icon-no"></span> {{ 'NORWEGIAN' | translate }} </button> </div> diff --git a/ClientApp/app/containers/home/home.component.ts b/ClientApp/app/containers/home/home.component.ts index abc2db05..fc689f24 100644 --- a/ClientApp/app/containers/home/home.component.ts +++ b/ClientApp/app/containers/home/home.component.ts @@ -1,25 +1,22 @@ -import { Component, OnInit, Inject } from '@angular/core'; - +import { Component, OnInit } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; @Component({ - selector: 'app-home', - templateUrl: './home.component.html' + selector: 'app-home', + templateUrl: './home.component.html' }) export class HomeComponent implements OnInit { + title: string = + 'Angular 6.x Universal & ASP.NET Core 2.1 advanced starter-kit'; - title: string = 'Angular 6.x Universal & ASP.NET Core 2.1 advanced starter-kit'; - - // Use "constructor"s only for dependency injection - constructor( - public translate: TranslateService - ) { } + // Use "constructor"s only for dependency injection + constructor(public translate: TranslateService) {} - // Here you want to handle anything with @Input()'s @Output()'s - // Data retrieval / etc - this is when the Component is "ready" and wired up - ngOnInit() { } + // Here you want to handle anything with @Input()'s @Output()'s + // Data retrieval / etc - this is when the Component is "ready" and wired up + ngOnInit() {} - public setLanguage(lang) { - this.translate.use(lang); - } + public setLanguage(lang) { + this.translate.use(lang); + } } diff --git a/ClientApp/app/containers/lazy/lazy.component.ts b/ClientApp/app/containers/lazy/lazy.component.ts index 97d260fd..53327733 100644 --- a/ClientApp/app/containers/lazy/lazy.component.ts +++ b/ClientApp/app/containers/lazy/lazy.component.ts @@ -10,4 +10,4 @@ import { Component } from '@angular/core'; </blockquote> ` }) -export class LazyComponent { } +export class LazyComponent {} diff --git a/ClientApp/app/containers/lazy/lazy.module.ts b/ClientApp/app/containers/lazy/lazy.module.ts index 8d468754..aa33a605 100644 --- a/ClientApp/app/containers/lazy/lazy.module.ts +++ b/ClientApp/app/containers/lazy/lazy.module.ts @@ -1,4 +1,4 @@ -import { NgModule, Component } from '@angular/core'; +import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; import { LazyComponent } from './lazy.component'; @@ -10,6 +10,4 @@ import { LazyComponent } from './lazy.component'; ]) ] }) -export class LazyModule { - -} +export class LazyModule {} diff --git a/ClientApp/app/containers/ngx-bootstrap-demo/ngx-bootstrap.component.html b/ClientApp/app/containers/ngx-bootstrap-demo/ngx-bootstrap.component.html index c57f2260..2b95c774 100644 --- a/ClientApp/app/containers/ngx-bootstrap-demo/ngx-bootstrap.component.html +++ b/ClientApp/app/containers/ngx-bootstrap-demo/ngx-bootstrap.component.html @@ -1,38 +1,35 @@ <h1>Ngx-bootstrap Demo:</h1> <blockquote> - <strong>Here we're using Bootstrap via <a href="https://github.com/valor-software/ngx-bootstrap">ngx-bootstrap</a>, which can even be rendered on the server!</strong> + <strong>Here we're using Bootstrap via + <a href="https://github.com/valor-software/ngx-bootstrap">ngx-bootstrap</a>, which can even be rendered on the server!</strong> <br> </blockquote> <hr> -<br><br> +<br> +<br> <h3>Bootstrap Accordion demo:</h3> <p> - <button type="button" class="btn btn-primary btn-sm" - (click)="group.isOpen = !group.isOpen"> + <button type="button" class="btn btn-primary btn-sm" (click)="group.isOpen = !group.isOpen"> Toggle last panel </button> - <button type="button" class="btn btn-primary btn-sm ml-2" - (click)="status.isFirstDisabled = ! status.isFirstDisabled"> + <button type="button" class="btn btn-primary btn-sm ml-2" (click)="status.isFirstDisabled = ! status.isFirstDisabled"> Enable / Disable first panel </button> </p> <div class="checkbox"> <label> - <input type="checkbox" [(ngModel)]="oneAtATime"> - Open only one at a time + <input type="checkbox" [(ngModel)]="oneAtATime"> Open only one at a time </label> </div> <accordion [closeOthers]="oneAtATime"> - <accordion-group heading="Static Header, initially expanded" - [isOpen]="status.isFirstOpen" - [isDisabled]="status.isFirstDisabled"> + <accordion-group heading="Static Header, initially expanded" [isOpen]="status.isFirstOpen" [isDisabled]="status.isFirstDisabled"> This content is straight in the template. </accordion-group> <accordion-group *ngFor="let group of groups" [heading]="group.title"> @@ -46,9 +43,8 @@ <h3>Bootstrap Accordion demo:</h3> <accordion-group #group [isOpen]="status.open"> <div accordion-heading> I can have markup, too! - <i class="pull-right glyphicon" - [ngClass]="{'glyphicon-chevron-down': group?.isOpen, 'glyphicon-chevron-right': !group?.isOpen}"></i> + <i class="pull-right glyphicon" [ngClass]="{'glyphicon-chevron-down': group?.isOpen, 'glyphicon-chevron-right': !group?.isOpen}"></i> </div> This is just some content to illustrate fancy headings. </accordion-group> -</accordion> \ No newline at end of file +</accordion> diff --git a/ClientApp/app/containers/ngx-bootstrap-demo/ngx-bootstrap.component.ts b/ClientApp/app/containers/ngx-bootstrap-demo/ngx-bootstrap.component.ts index a8ba7dec..d1122dad 100644 --- a/ClientApp/app/containers/ngx-bootstrap-demo/ngx-bootstrap.component.ts +++ b/ClientApp/app/containers/ngx-bootstrap-demo/ngx-bootstrap.component.ts @@ -1,36 +1,34 @@ import { Component } from '@angular/core'; @Component({ - selector: 'app-bootstrap', - templateUrl: './ngx-bootstrap.component.html' + selector: 'app-bootstrap', + templateUrl: './ngx-bootstrap.component.html' }) export class NgxBootstrapComponent { + public oneAtATime: boolean = true; + public items = ['Item 1', 'Item 2', 'Item 3']; - public oneAtATime: boolean = true; - public items = ['Item 1', 'Item 2', 'Item 3']; + public status = { + isFirstOpen: true, + isFirstDisabled: false, + open: false + }; - public status = { - isFirstOpen: true, - isFirstDisabled: false, - open: false - }; - - public groups = [ - { - title: 'Angular is neato gang!', - content: 'ASP.NET Core is too :)' - }, - { - title: 'Another One!', - content: 'Some content going here' - } - ]; - - // Use "constructor"s only for dependency injection - constructor() { } - - addItem(): void { - this.items.push(`Items ${this.items.length + 1}`); + public groups = [ + { + title: 'Angular is neato gang!', + content: 'ASP.NET Core is too :)' + }, + { + title: 'Another One!', + content: 'Some content going here' } + ]; + + // Use "constructor"s only for dependency injection + constructor() {} -} \ No newline at end of file + addItem(): void { + this.items.push(`Items ${this.items.length + 1}`); + } +} diff --git a/ClientApp/app/containers/not-found/not-found.component.html b/ClientApp/app/containers/not-found/not-found.component.html index 347dcb93..b0b7072d 100644 --- a/ClientApp/app/containers/not-found/not-found.component.html +++ b/ClientApp/app/containers/not-found/not-found.component.html @@ -1,8 +1,8 @@ <div class="wrapper"> <header class="header header--large"> <h1 class="title">Ahhhhhhhhhhh! This page doesn't exist</h1> - <h2 class="strapline">Not to worry. You can either head back to <a href="http://github.com/">our homepage</a>, or sit there and listen to a goat scream like - a human.</h2> + <h2 class="strapline">Not to worry. You can either head back to + <a href="http://github.com/">our homepage</a>, or sit there and listen to a goat scream like a human.</h2> </header> <div class="content fit-vid vid"> <iframe src="http://www.youtube.com/embed/SIaFtAKnqBU?vq=hd720&rel=0&showinfo=0&controls=0&iv_load_policy=3&loop=1&playlist=SIaFtAKnqBU&modestbranding=1&autoplay=1" diff --git a/ClientApp/app/containers/not-found/not-found.component.ts b/ClientApp/app/containers/not-found/not-found.component.ts index c59a5f8f..6bd9147a 100644 --- a/ClientApp/app/containers/not-found/not-found.component.ts +++ b/ClientApp/app/containers/not-found/not-found.component.ts @@ -1,11 +1,11 @@ import { Component, OnInit } from '@angular/core'; @Component({ - selector: 'app-not-found', - templateUrl: './not-found.component.html' + selector: 'app-not-found', + templateUrl: './not-found.component.html' }) export class NotFoundComponent implements OnInit { - constructor() { } + constructor() {} - ngOnInit() { } + ngOnInit() {} } diff --git a/ClientApp/app/containers/users/users.component.scss b/ClientApp/app/containers/users/users.component.scss index 85a47c26..67acfed0 100644 --- a/ClientApp/app/containers/users/users.component.scss +++ b/ClientApp/app/containers/users/users.component.scss @@ -1,4 +1,4 @@ -@import "http://github.com/variables"; +@import 'http://github.com/variables'; .users-wrapper { max-height: 65vh; overflow-y: auto; @@ -35,9 +35,9 @@ h1 { align-items: center; padding: 0px; &:hover { - color: #607D8B; + color: #607d8b; background-color: #e6e6e6; - left: .1em; + left: 0.1em; } &.selected { background-color: #7eaacd !important; @@ -45,7 +45,7 @@ h1 { } } -.users li>* { +.users li > * { display: flex; flex-direction: column; align-items: center; @@ -58,13 +58,13 @@ h1 { } .users li.selected:hover { - background-color: #BBD8DC !important; + background-color: #bbd8dc !important; color: white; } .user-id { @extend .input-group-text; - background-color: #607D8B; + background-color: #607d8b; color: white; width: 2.5rem; border: 0px; @@ -73,9 +73,7 @@ h1 { } .delete-user { - @extend .btn, - .badge, - .badge-secondary; + @extend .btn, .badge, .badge-secondary; height: 25px; width: 25px; margin-right: 10px; diff --git a/ClientApp/app/containers/users/users.component.ts b/ClientApp/app/containers/users/users.component.ts index 27a9bbff..b64ea0ee 100644 --- a/ClientApp/app/containers/users/users.component.ts +++ b/ClientApp/app/containers/users/users.component.ts @@ -1,13 +1,11 @@ import { - Component, OnInit, Inject -} from '@angular/core'; -import { - trigger, + animate, state, style, - animate, - transition + transition, + trigger } from '@angular/animations'; +import { Component, OnInit } from '@angular/core'; import { IUser } from '../../models/User'; import { UserService } from '../../shared/user.service'; @@ -31,14 +29,11 @@ import { UserService } from '../../shared/user.service'; ] }) export class UsersComponent implements OnInit { - users: IUser[]; selectedUser: IUser; // Use "constructor"s only for dependency injection - constructor( - private userService: UserService - ) { } + constructor(private userService: UserService) {} // Here you want to handle anything with @Input()'s @Output()'s // Data retrieval / etc - this is when the Component is "ready" and wired up @@ -55,27 +50,33 @@ export class UsersComponent implements OnInit { deleteUser(user) { this.clearUser(); - this.userService.deleteUser(user).subscribe(result => { - console.log('Delete user result: ', result); - let position = this.users.indexOf(user); - this.users.splice(position, 1); - }, error => { - console.log(`There was an issue. ${error._body}.`); - }); + this.userService.deleteUser(user).subscribe( + result => { + console.log('Delete user result: ', result); + let position = this.users.indexOf(user); + this.users.splice(position, 1); + }, + error => { + console.log(`There was an issue. ${error._body}.`); + } + ); } onUserUpdate(user: IUser) { - this.users[this.users.findIndex((u => u.id == user.id))] = user; + this.users[this.users.findIndex(u => u.id == user.id)] = user; } addUser(newUserName) { - this.userService.addUser(newUserName).subscribe(result => { - console.log('Post user result: ', result); - this.users.push(result); - this.selectedUser = result; - }, error => { - console.log(`There was an issue. ${error._body}.`); - }); + this.userService.addUser(newUserName).subscribe( + result => { + console.log('Post user result: ', result); + this.users.push(result); + this.selectedUser = result; + }, + error => { + console.log(`There was an issue. ${error._body}.`); + } + ); } clearUser() { diff --git a/ClientApp/app/models/User.ts b/ClientApp/app/models/User.ts index 53f9df3a..f140ecb1 100644 --- a/ClientApp/app/models/User.ts +++ b/ClientApp/app/models/User.ts @@ -1,4 +1,4 @@ export interface IUser { - id: number; - name: string; -} \ No newline at end of file + id: number; + name: string; +} diff --git a/ClientApp/app/shared/link.service.ts b/ClientApp/app/shared/link.service.ts index c5a2f16d..2e6754a4 100644 --- a/ClientApp/app/shared/link.service.ts +++ b/ClientApp/app/shared/link.service.ts @@ -6,90 +6,90 @@ * Soon there will be an overall HeadService within Angular that handles Meta/Link everything */ -import { Injectable, PLATFORM_ID, Optional, RendererFactory2, ViewEncapsulation, Inject } from '@angular/core'; -import { DOCUMENT } from '@angular/platform-browser'; import { isPlatformServer } from '@angular/common'; +import { + Inject, + Injectable, + PLATFORM_ID, + RendererFactory2, + ViewEncapsulation +} from '@angular/core'; +import { DOCUMENT } from '@angular/platform-browser'; @Injectable() export class LinkService { - - private isServer: boolean = isPlatformServer(this.platform_id); - - constructor( - private rendererFactory: RendererFactory2, - @Inject(DOCUMENT) private document, - @Inject(PLATFORM_ID) private platform_id - ) { - } - - /** - * Inject the State into the bottom of the <head> - */ - addTag(tag: LinkDefinition, forceCreation?: boolean) { - - try { - const renderer = this.rendererFactory.createRenderer(this.document, { - id: '-1', - encapsulation: ViewEncapsulation.None, - styles: [], - data: {} - }); - - const link = renderer.createElement('link'); - const head = this.document.head; - const selector = this._parseSelector(tag); - - if (head === null) { - throw new Error('<head> not found within DOCUMENT.'); - } - - Object.keys(tag).forEach((prop: string) => { - return renderer.setAttribute(link, prop, tag[prop]); - }); - - // [TODO]: get them to update the existing one (if it exists) ? - renderer.appendChild(head, link); - - } catch (e) { - console.error('Error within linkService : ', e); - } + private isServer: boolean = isPlatformServer(this.platform_id); + + constructor( + private rendererFactory: RendererFactory2, + @Inject(DOCUMENT) private document, + @Inject(PLATFORM_ID) private platform_id + ) {} + + /** + * Inject the State into the bottom of the <head> + */ + addTag(tag: LinkDefinition, forceCreation?: boolean) { + try { + const renderer = this.rendererFactory.createRenderer(this.document, { + id: '-1', + encapsulation: ViewEncapsulation.None, + styles: [], + data: {} + }); + + const link = renderer.createElement('link'); + const head = this.document.head; + const selector = this._parseSelector(tag); + + if (head === null) { + throw new Error('<head> not found within DOCUMENT.'); + } + + Object.keys(tag).forEach((prop: string) => { + return renderer.setAttribute(link, prop, tag[prop]); + }); + + // [TODO]: get them to update the existing one (if it exists) ? + renderer.appendChild(head, link); + } catch (e) { + console.error('Error within linkService : ', e); } - - // updateTag(tag: LinkDefinition, selector?: string) { - // if (!tag) return null; - // selector = selector || this._parseSelector(tag); - // const meta = this.getTag(selector); - // if (meta) { - // return this._setMetaElementAttributes(tag, meta); - // } - // return this._getOrCreateElement(tag, true); - // } - - // getTag(attrSelector: string): HTMLMetaElement { - // if (!attrSelector) return null; - // return this._dom.querySelector(this._doc, `meta[${attrSelector}]`); - // } - - private _parseSelector(tag: LinkDefinition): string { - // Possibly re-work this - const attr: string = tag.rel ? 'rel' : 'hreflang'; - return `${attr}="${tag[attr]}"`; - } - + } + + // updateTag(tag: LinkDefinition, selector?: string) { + // if (!tag) return null; + // selector = selector || this._parseSelector(tag); + // const meta = this.getTag(selector); + // if (meta) { + // return this._setMetaElementAttributes(tag, meta); + // } + // return this._getOrCreateElement(tag, true); + // } + + // getTag(attrSelector: string): HTMLMetaElement { + // if (!attrSelector) return null; + // return this._dom.querySelector(this._doc, `meta[${attrSelector}]`); + // } + + private _parseSelector(tag: LinkDefinition): string { + // Possibly re-work this + const attr: string = tag.rel ? 'rel' : 'hreflang'; + return `${attr}="${tag[attr]}"`; + } } - export declare type LinkDefinition = { - charset?: string; - crossorigin?: string; - href?: string; - hreflang?: string; - media?: string; - rel?: string; - rev?: string; - sizes?: string; - target?: string; - type?: string; + charset?: string; + crossorigin?: string; + href?: string; + hreflang?: string; + media?: string; + rel?: string; + rev?: string; + sizes?: string; + target?: string; + type?: string; } & { - [prop: string]: string; - }; \ No newline at end of file + [prop: string]: string; +}; diff --git a/ClientApp/app/shared/user.service.ts b/ClientApp/app/shared/user.service.ts index 31110e7e..85249249 100644 --- a/ClientApp/app/shared/user.service.ts +++ b/ClientApp/app/shared/user.service.ts @@ -1,21 +1,13 @@ -import { Injectable, Inject, Injector } from '@angular/core'; import { HttpClient } from '@angular/common/http'; -import { Http, URLSearchParams } from '@angular/http'; -import { APP_BASE_HREF } from '@angular/common'; +import { Injectable, Injector } from '@angular/core'; import { ORIGIN_URL } from '@nguniversal/aspnetcore-engine/tokens'; import { IUser } from '../models/User'; -import { Observable } from 'rxjs'; - @Injectable() export class UserService { - private baseUrl: string; - constructor( - private http: HttpClient, - private injector: Injector - ) { + constructor(private http: HttpClient, private injector: Injector) { this.baseUrl = this.injector.get(ORIGIN_URL); } @@ -31,11 +23,13 @@ export class UserService { return this.http.delete<IUser>(`${this.baseUrl}/api/users/` + user.id); } - updateUser(user: IUser){ + updateUser(user: IUser) { return this.http.put<IUser>(`${this.baseUrl}/api/users/` + user.id, user); } addUser(newUserName: string) { - return this.http.post<IUser>(`${this.baseUrl}/api/users`, { name: newUserName }); + return this.http.post<IUser>(`${this.baseUrl}/api/users`, { + name: newUserName + }); } } diff --git a/ClientApp/boot.browser.ts b/ClientApp/boot.browser.ts index 329344f7..c0ef55b6 100644 --- a/ClientApp/boot.browser.ts +++ b/ClientApp/boot.browser.ts @@ -1,16 +1,16 @@ -import './polyfills/browser.polyfills'; import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module.browser'; +import './polyfills/browser.polyfills'; // // Enable either Hot Module Reloading or production mode if (module['hot']) { - module['hot'].accept(); - module['hot'].dispose(() => { - modulePromise.then(appModule => appModule.destroy()); - }); + module['hot'].accept(); + module['hot'].dispose(() => { + modulePromise.then(appModule => appModule.destroy()); + }); } else { - enableProdMode(); + enableProdMode(); } const modulePromise = platformBrowserDynamic().bootstrapModule(AppModule); diff --git a/ClientApp/boot.server.PRODUCTION.ts b/ClientApp/boot.server.PRODUCTION.ts index 30f8deb0..b7bcbb7a 100644 --- a/ClientApp/boot.server.PRODUCTION.ts +++ b/ClientApp/boot.server.PRODUCTION.ts @@ -1,16 +1,19 @@ -import 'zone.js/dist/zone-node'; -import './polyfills/server.polyfills'; -import { enableProdMode } from '@angular/core'; +import { enableProdMode } from '@angular/core'; +import { + createTransferScript, + IEngineOptions, + ngAspnetCoreEngine +} from '@nguniversal/aspnetcore-engine'; import { createServerRenderer } from 'aspnet-prerendering'; +import 'zone.js/dist/zone-node'; +import './polyfills/server.polyfills'; // Grab the (Node) server-specific NgModule const { AppModuleNgFactory } = require('./app/app.module.server.ngfactory'); // <-- ignore this - this is Production only -import { ngAspnetCoreEngine, IEngineOptions, createTransferScript } from '@nguniversal/aspnetcore-engine'; enableProdMode(); -export default createServerRenderer((params) => { - +export default createServerRenderer(params => { // Platform-server provider configuration const setupOptions: IEngineOptions = { appSelector: '<app-root></app-root>', @@ -23,16 +26,16 @@ export default createServerRenderer((params) => { }; return ngAspnetCoreEngine(setupOptions).then(response => { - // Apply your transferData to response.globals response.globals.transferData = createTransferScript({ - someData: 'Transfer this to the client on the window.TRANSFER_CACHE {} object', + someData: + 'Transfer this to the client on the window.TRANSFER_CACHE {} object', fromDotnet: params.data.thisCameFromDotNET // example of data coming from dotnet, in HomeController }); - return ({ + return { html: response.html, // our <app-root> serialized globals: response.globals // all of our styles/scripts/meta-tags/link-tags for aspnet to serve up - }); + }; }); }); diff --git a/ClientApp/boot.server.ts b/ClientApp/boot.server.ts index c2c0fb47..72457842 100644 --- a/ClientApp/boot.server.ts +++ b/ClientApp/boot.server.ts @@ -1,11 +1,11 @@ -import 'zone.js/dist/zone-node'; -import './polyfills/server.polyfills'; -import { enableProdMode } from '@angular/core'; +import { enableProdMode } from '@angular/core'; +import { createTransferScript, IEngineOptions, ngAspnetCoreEngine } from '@nguniversal/aspnetcore-engine'; import { createServerRenderer } from 'aspnet-prerendering'; - +import 'zone.js/dist/zone-node'; // Grab the (Node) server-specific NgModule import { AppModule } from './app/app.module.server'; -import { ngAspnetCoreEngine, IEngineOptions, createTransferScript } from '@nguniversal/aspnetcore-engine'; +import './polyfills/server.polyfills'; + enableProdMode(); diff --git a/ClientApp/polyfills/browser.polyfills.ts b/ClientApp/polyfills/browser.polyfills.ts index 1db98ac8..207f7e06 100644 --- a/ClientApp/polyfills/browser.polyfills.ts +++ b/ClientApp/polyfills/browser.polyfills.ts @@ -1,5 +1,5 @@ +import 'reflect-metadata'; +import 'zone.js/dist/zone'; import './polyfills.ts'; -import 'zone.js/dist/zone'; -import 'reflect-metadata'; // import 'web-animations-js'; // Run `npm install --save web-animations-js`. diff --git a/ClientApp/polyfills/polyfills.ts b/ClientApp/polyfills/polyfills.ts index fd43e294..b577f1c4 100644 --- a/ClientApp/polyfills/polyfills.ts +++ b/ClientApp/polyfills/polyfills.ts @@ -1,30 +1,25 @@ - /*************************************************************************************************** * BROWSER POLYFILLS */ /** IE9, IE10 and IE11 requires all of the following polyfills. **/ - import 'core-js/es6/symbol'; - import 'core-js/es6/object'; - import 'core-js/es6/function'; - import 'core-js/es6/parse-int'; - import 'core-js/es6/parse-float'; - import 'core-js/es6/number'; - import 'core-js/es6/math'; - import 'core-js/es6/string'; - import 'core-js/es6/date'; - import 'core-js/es6/array'; - import 'core-js/es6/regexp'; - import 'core-js/es6/map'; - import 'core-js/es6/set'; - - /** */ - import 'reflect-metadata'; - +import 'core-js/es6/array'; +import 'core-js/es6/date'; +import 'core-js/es6/function'; +import 'core-js/es6/map'; +import 'core-js/es6/math'; +import 'core-js/es6/number'; +import 'core-js/es6/object'; +import 'core-js/es6/parse-float'; +import 'core-js/es6/parse-int'; /** IE10 and IE11 requires the following for NgClass support on SVG elements */ // import 'classlist.js'; // Run `npm install --save classlist.js`. - /** Evergreen browsers require these. **/ import 'core-js/es6/reflect'; +import 'core-js/es6/regexp'; +import 'core-js/es6/set'; +import 'core-js/es6/string'; +import 'core-js/es6/symbol'; import 'core-js/es7/reflect'; - +/** */ +import 'reflect-metadata'; diff --git a/ClientApp/polyfills/server.polyfills.ts b/ClientApp/polyfills/server.polyfills.ts index 7044895f..96b07953 100644 --- a/ClientApp/polyfills/server.polyfills.ts +++ b/ClientApp/polyfills/server.polyfills.ts @@ -1,3 +1,2 @@ -import './polyfills.ts'; - -import 'zone.js'; +import 'zone.js'; +import './polyfills.ts'; diff --git a/ClientApp/test/boot-tests.js b/ClientApp/test/boot-tests.js index 647b8c02..51eeb6d3 100644 --- a/ClientApp/test/boot-tests.js +++ b/ClientApp/test/boot-tests.js @@ -12,13 +12,15 @@ const testing = require('@angular/core/testing'); const testingBrowser = require('@angular/platform-browser-dynamic/testing'); // Prevent Karma from running prematurely -__karma__.loaded = function () {}; +__karma__.loaded = function() {}; // First, initialize the Angular testing environment -testing.getTestBed().initTestEnvironment( +testing + .getTestBed() + .initTestEnvironment( testingBrowser.BrowserDynamicTestingModule, testingBrowser.platformBrowserDynamicTesting() -); + ); // Then we find all the tests const context = require.context('../', true, /\.spec\.ts$/); diff --git a/ClientApp/test/karma.conf.js b/ClientApp/test/karma.conf.js index b54ce8f9..6ab0db4b 100644 --- a/ClientApp/test/karma.conf.js +++ b/ClientApp/test/karma.conf.js @@ -1,15 +1,12 @@ // Karma configuration file, see link for more information // https://karma-runner.github.io/0.13/config/configuration-file.html -module.exports = function (config) { +module.exports = function(config) { config.set({ basePath: '.', frameworks: ['jasmine'], exclude: [], - files: [ - '../../wwwroot/dist/vendor.js', - './boot-tests.js' - ], + files: ['../../wwwroot/dist/vendor.js', './boot-tests.js'], preprocessors: { './boot-tests.js': ['coverage', 'webpack', 'sourcemap'] }, @@ -45,7 +42,7 @@ module.exports = function (config) { }, // you can define custom flags customLaunchers: { - 'PhantomJS_custom': { + PhantomJS_custom: { base: 'PhantomJS', options: { windowName: 'test-window', @@ -53,7 +50,7 @@ module.exports = function (config) { webSecurityEnabled: false } }, - flags: ['--load-images=true'], + flags: ['--load-images=true'] // debug: true } }, diff --git a/ClientApp/test/webpack.config.test.js b/ClientApp/test/webpack.config.test.js index 3a14d96d..5cbcecc5 100644 --- a/ClientApp/test/webpack.config.test.js +++ b/ClientApp/test/webpack.config.test.js @@ -5,7 +5,7 @@ const webpack = require('webpack'); var path = require('path'); var rootPath = path.join.bind(path, path.resolve(__dirname, '../../')); -module.exports = function (options) { +module.exports = function(options) { return { devtool: 'inline-source-map', resolve: { @@ -13,7 +13,8 @@ module.exports = function (options) { modules: [rootPath('ClientApp'), 'node_modules'] }, module: { - rules: [{ + rules: [ + { enforce: 'pre', test: /\.js$/, loader: 'source-map-loader', @@ -24,7 +25,8 @@ module.exports = function (options) { }, { test: /\.ts$/, - use: [{ + use: [ + { loader: 'awesome-typescript-loader', query: { sourceMap: false, @@ -32,7 +34,7 @@ module.exports = function (options) { compilerOptions: { removeComments: true } - }, + } }, 'angular2-template-loader' ], @@ -58,13 +60,8 @@ module.exports = function (options) { esModules: true }, include: rootPath('ClientApp'), - exclude: [ - /ClientApp\\test/, - /\.(e2e|spec)\.ts$/, - /node_modules/ - ] + exclude: [/ClientApp\\test/, /\.(e2e|spec)\.ts$/, /node_modules/] } - ] }, plugins: [ @@ -91,8 +88,7 @@ module.exports = function (options) { * legacy options go here */ } - }), - + }) ], performance: { hints: false @@ -112,6 +108,5 @@ module.exports = function (options) { clearImmediate: false, setImmediate: false } - }; -} +}; diff --git a/ClientApp/tsconfig.app.json b/ClientApp/tsconfig.app.json index 3f094c64..2cf4ae53 100644 --- a/ClientApp/tsconfig.app.json +++ b/ClientApp/tsconfig.app.json @@ -6,9 +6,5 @@ "baseUrl": "", "types": [] }, - "exclude": [ - "test.ts", - "**/*.spec.ts", - "boot.server.PRODUCTION.ts" - ] + "exclude": ["test.ts", "**/*.spec.ts", "boot.server.PRODUCTION.ts"] } diff --git a/ClientApp/tsconfig.spec.json b/ClientApp/tsconfig.spec.json index de4e2a75..c8fc1d84 100644 --- a/ClientApp/tsconfig.spec.json +++ b/ClientApp/tsconfig.spec.json @@ -5,16 +5,8 @@ "module": "commonjs", "target": "es5", "baseUrl": "", - "types": [ - "jasmine", - "node" - ] + "types": ["jasmine", "node"] }, - "files": [ - "polyfills.ts" - ], - "include": [ - "**/*.spec.ts", - "**/*.d.ts" - ] + "files": ["polyfills.ts"], + "include": ["**/*.spec.ts", "**/*.d.ts"] } diff --git a/Program.cs b/Program.cs new file mode 100644 index 00000000..69e56419 --- /dev/null +++ b/Program.cs @@ -0,0 +1,33 @@ +using AspCoreServer; +using AspCoreServer.Data; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System; +using System.IO; + +public class Program { + public static void Main (string[] args) { + var host = BuildWebHost (args); + using (var scope = host.Services.CreateScope ()) { + var services = scope.ServiceProvider; + try { + var context = services.GetRequiredService<SpaDbContext>(); + DbInitializer.Initialize(context); + } catch (Exception ex) { + var logger = services.GetRequiredService<ILogger<Program>> (); + logger.LogError (ex, "An error occurred while seeding the database."); + } + } + + host.Run (); + } + public static IWebHost BuildWebHost (string[] args) => + WebHost.CreateDefaultBuilder (args) + .UseKestrel () + .UseContentRoot (Directory.GetCurrentDirectory ()) + .UseIISIntegration () + .UseStartup<Startup> () + .Build (); + } diff --git a/Server/Controllers/HomeController.cs b/Server/Controllers/HomeController.cs index 399174ef..18c423c6 100644 --- a/Server/Controllers/HomeController.cs +++ b/Server/Controllers/HomeController.cs @@ -1,63 +1,47 @@ -using System; -using System.Diagnostics; -using System.Threading.Tasks; -using Asp2017.Server.Helpers; -using Asp2017.Server.Models; +using Asp2017.Server.Helpers; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.NodeServices; -using Microsoft.AspNetCore.SpaServices.Prerendering; -using Microsoft.Extensions.DependencyInjection; - -namespace AspCoreServer.Controllers { - public class HomeController : Controller { - protected readonly IHostingEnvironment HostingEnvironment; - public HomeController (IHostingEnvironment hostingEnv) { - this.HostingEnvironment = hostingEnv; - } - - [HttpGet] - public async Task<IActionResult> Index () { - var prerenderResult = await Request.BuildPrerender (); - - ViewData["SpaHtml"] = prerenderResult.Html; // our <app-root /> from Angular - ViewData["Title"] = prerenderResult.Globals["title"]; // set our <title> from Angular - ViewData["Styles"] = prerenderResult.Globals["styles"]; // put styles in the correct place - ViewData["Scripts"] = prerenderResult.Globals["scripts"]; // scripts (that were in our header) - ViewData["Meta"] = prerenderResult.Globals["meta"]; // set our <meta> SEO tags - ViewData["Links"] = prerenderResult.Globals["links"]; // set our <link rel="canonical"> etc SEO tags - ViewData["TransferData"] = prerenderResult.Globals["transferData"]; // our transfer data set to window.TRANSFER_CACHE = {}; - if (!HostingEnvironment.IsDevelopment ()) { - ViewData["ServiceWorker"] = "<script>'serviceWorker'in navigator&&navigator.serviceWorker.register('/serviceworker')</script>"; - } - - return View (); - } - - [HttpGet] - [Route ("sitemap.xml")] - public IActionResult SitemapXml () { - String xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>"; - - xml += "<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9/">"; - xml += "<url>"; - xml += "<loc>http://localhost:4251/home</loc>"; - xml += "<lastmod>" + DateTime.Now.ToString ("yyyy-MM-dd") + "</lastmod>"; - xml += "</url>"; - xml += "<url>"; - xml += "<loc>http://localhost:4251/counter</loc>"; - xml += "<lastmod>" + DateTime.Now.ToString ("yyyy-MM-dd") + "</lastmod>"; - xml += "</url>"; - xml += "</urlset>"; - - return Content (xml, "text/xml"); - - } +using System; +using System.Threading.Tasks; - public IActionResult Error () { - return View (); +namespace AspCoreServer.Controllers +{ + public class HomeController : Controller { + protected readonly IHostingEnvironment HostingEnvironment; + public HomeController(IHostingEnvironment hostingEnv) => this.HostingEnvironment = hostingEnv; + + [HttpGet] + public async Task<IActionResult> Index () { + var prerenderResult = await this.Request.BuildPrerender (); + + this.ViewData["SpaHtml"] = prerenderResult.Html; // our <app-root /> from Angular + this.ViewData["Title"] = prerenderResult.Globals["title"]; // set our <title> from Angular + this.ViewData["Styles"] = prerenderResult.Globals["styles"]; // put styles in the correct place + this.ViewData["Scripts"] = prerenderResult.Globals["scripts"]; // scripts (that were in our header) + this.ViewData["Meta"] = prerenderResult.Globals["meta"]; // set our <meta> SEO tags + this.ViewData["Links"] = prerenderResult.Globals["links"]; // set our <link rel="canonical"> etc SEO tags + this.ViewData["TransferData"] = prerenderResult.Globals["transferData"]; // our transfer data set to window.TRANSFER_CACHE = {}; + if (!this.HostingEnvironment.IsDevelopment ()) { + this.ViewData["ServiceWorker"] = "<script>'serviceWorker'in navigator&&navigator.serviceWorker.register('/serviceworker')</script>"; + } + + return View (); + } + + [HttpGet] + [Route("sitemap.xml")] + public IActionResult SitemapXml() => Content($@"<?xml version=""1.0"" encoding=""utf-8""?> + <urlset xmlns=""http://www.sitemaps.org/schemas/sitemap/0.9""> + <url> + <loc>http://localhost:4251/home</loc> + <lastmod>{ DateTime.Now.ToString("yyyy-MM-dd")}</lastmod> + </url> + <url> + <loc>http://localhost:4251/counter</loc> + <lastmod>{DateTime.Now.ToString("yyyy-MM-dd")}</lastmod> + </url> + </urlset>", "text/xml"); + + public IActionResult Error() => View(); } - } -} \ No newline at end of file +} diff --git a/Server/Data/DbInitializer.cs b/Server/Data/DbInitializer.cs index 45b7498e..919c9282 100644 --- a/Server/Data/DbInitializer.cs +++ b/Server/Data/DbInitializer.cs @@ -1,43 +1,37 @@ using System; using System.Linq; +using AspCoreServer; +using AspCoreServer.Models; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; -using AspCoreServer.Models; -using AspCoreServer; -namespace AspCoreServer.Data -{ - public static class DbInitializer - { - public static void Initialize(SpaDbContext context) - { - context.Database.EnsureCreated(); +namespace AspCoreServer.Data { + public static class DbInitializer { + public static void Initialize (SpaDbContext context) { + context.Database.EnsureCreated (); - if (context.User.Any()) - { - return; // DB has been seeded + if (context.User.Any ()) { + return; // DB has been seeded } - var users = new User[] - { - new User(){Name = "Mark Pieszak"}, - new User(){Name = "Abrar Jahin"}, - new User(){Name = "hakonamatata"}, - new User(){Name = "LiverpoolOwen"}, - new User(){Name = "Ketrex"}, - new User(){Name = "markwhitfeld"}, - new User(){Name = "daveo1001"}, - new User(){Name = "paonath"}, - new User(){Name = "nalex095"}, - new User(){Name = "ORuban"}, - new User(){Name = "Gaulomatic"}, - new User(){Name = "GRIMMR3AP3R"} + var users = new User[] { + new User () { Name = "Mark Pieszak" }, + new User () { Name = "Abrar Jahin" }, + new User () { Name = "hakonamatata" }, + new User () { Name = "LiverpoolOwen" }, + new User () { Name = "Ketrex" }, + new User () { Name = "markwhitfeld" }, + new User () { Name = "daveo1001" }, + new User () { Name = "paonath" }, + new User () { Name = "nalex095" }, + new User () { Name = "ORuban" }, + new User () { Name = "Gaulomatic" }, + new User () { Name = "GRIMMR3AP3R" } }; - foreach (User s in users) - { - context.User.Add(s); + foreach (var s in users) { + context.User.Add (s); } - context.SaveChanges(); + context.SaveChanges (); } } } diff --git a/Server/Data/SpaDbContext.cs b/Server/Data/SpaDbContext.cs index e6fee5f7..0f90c230 100644 --- a/Server/Data/SpaDbContext.cs +++ b/Server/Data/SpaDbContext.cs @@ -1,14 +1,10 @@ using AspCoreServer.Models; using Microsoft.EntityFrameworkCore; -namespace AspCoreServer.Data -{ - public class SpaDbContext : DbContext - { - public SpaDbContext(DbContextOptions<SpaDbContext> options) - : base(options) - { - Database.EnsureCreated(); +namespace AspCoreServer.Data { + public class SpaDbContext : DbContext { + public SpaDbContext (DbContextOptions<SpaDbContext> options) : base (options) { + Database.EnsureCreated (); } //List of DB Models - Add your DB models here diff --git a/Server/Helpers/HttpRequestExtensions.cs b/Server/Helpers/HttpRequestExtensions.cs index 12648b60..81c613b9 100644 --- a/Server/Helpers/HttpRequestExtensions.cs +++ b/Server/Helpers/HttpRequestExtensions.cs @@ -1,3 +1,7 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; using Asp2017.Server.Models; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; @@ -5,61 +9,35 @@ using Microsoft.AspNetCore.NodeServices; using Microsoft.AspNetCore.SpaServices.Prerendering; using Microsoft.Extensions.DependencyInjection; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Asp2017.Server.Helpers -{ - public static class HttpRequestExtensions - { - public static IRequest AbstractRequestInfo(this HttpRequest request) - { - - IRequest requestSimplified = new IRequest(); - requestSimplified.cookies = request.Cookies; - requestSimplified.headers = request.Headers; - requestSimplified.host = request.Host; - - return requestSimplified; - } - - public static async Task<RenderToStringResult> BuildPrerender(this HttpRequest Request) - { - var nodeServices = Request.HttpContext.RequestServices.GetRequiredService<INodeServices>(); - var hostEnv = Request.HttpContext.RequestServices.GetRequiredService<IHostingEnvironment>(); - - var applicationBasePath = hostEnv.ContentRootPath; - var requestFeature = Request.HttpContext.Features.Get<IHttpRequestFeature>(); - var unencodedPathAndQuery = requestFeature.RawTarget; - var unencodedAbsoluteUrl = $"{Request.Scheme}://{Request.Host}{unencodedPathAndQuery}"; - - // ** TransferData concept ** - // Here we can pass any Custom Data we want ! - - // By default we're passing down Cookies, Headers, Host from the Request object here - TransferData transferData = new TransferData(); - transferData.request = Request.AbstractRequestInfo(); - transferData.thisCameFromDotNET = "Hi Angular it's asp.net :)"; - // Add more customData here, add it to the TransferData class - - //Prerender now needs CancellationToken - System.Threading.CancellationTokenSource cancelSource = new System.Threading.CancellationTokenSource(); - System.Threading.CancellationToken cancelToken = cancelSource.Token; - // Prerender / Serialize application (with Universal) - return await Prerenderer.RenderToString( +namespace Asp2017.Server.Helpers { + public static class HttpRequestExtensions { + public static IRequest AbstractRequestInfo(this HttpRequest request) => new IRequest() + { + cookies = request.Cookies, + headers = request.Headers, + host = request.Host + }; + + public static async Task<RenderToStringResult> BuildPrerender(this HttpRequest request) => + // Prerender / Serialize application (with Universal) + await Prerenderer.RenderToString( "/", - nodeServices, - cancelToken, - new JavaScriptModuleExport(applicationBasePath + "/ClientApp/dist/main-server"), - unencodedAbsoluteUrl, - unencodedPathAndQuery, - transferData, // Our simplified Request object & any other CustommData you want to send! + request.HttpContext.RequestServices.GetRequiredService<INodeServices>(), + new System.Threading.CancellationTokenSource().Token, + new JavaScriptModuleExport(request.HttpContext.RequestServices.GetRequiredService<IHostingEnvironment>().ContentRootPath + "/ClientApp/dist/main-server"), + $"{request.Scheme}://{request.Host}{request.HttpContext.Features.Get<IHttpRequestFeature>().RawTarget}", + request.HttpContext.Features.Get<IHttpRequestFeature>().RawTarget, + // ** TransferData concept ** + // Here we can pass any Custom Data we want ! + // By default we're passing down Cookies, Headers, Host from the Request object here + new TransferData + { + request = request.AbstractRequestInfo(), + thisCameFromDotNET = "Hi Angular it's asp.net :)" + }, // Our simplified Request object & any other CustommData you want to send! 30000, - Request.PathBase.ToString() + request.PathBase.ToString() ); } - } } diff --git a/Server/Models/IRequest.cs b/Server/Models/IRequest.cs index 407cf571..295c88fb 100644 --- a/Server/Models/IRequest.cs +++ b/Server/Models/IRequest.cs @@ -3,12 +3,10 @@ using System.Linq; using System.Threading.Tasks; -namespace Asp2017.Server.Models -{ - public class IRequest - { - public object cookies { get; set; } - public object headers { get; set; } - public object host { get; set; } - } +namespace Asp2017.Server.Models { + public class IRequest { + public object cookies { get; set; } + public object headers { get; set; } + public object host { get; set; } + } } diff --git a/Server/Models/TransferData.cs b/Server/Models/TransferData.cs index a89ab38d..c144d2df 100644 --- a/Server/Models/TransferData.cs +++ b/Server/Models/TransferData.cs @@ -3,13 +3,11 @@ using System.Linq; using System.Threading.Tasks; -namespace Asp2017.Server.Models -{ - public class TransferData - { - public dynamic request { get; set; } +namespace Asp2017.Server.Models { + public class TransferData { + public dynamic request { get; set; } - // Your data here ? - public object thisCameFromDotNET { get; set; } - } + // Your data here ? + public object thisCameFromDotNET { get; set; } + } } diff --git a/Server/Models/User.cs b/Server/Models/User.cs index fb3d74da..56e388bf 100644 --- a/Server/Models/User.cs +++ b/Server/Models/User.cs @@ -1,20 +1,17 @@ using System; using System.ComponentModel.DataAnnotations; -namespace AspCoreServer.Models -{ - public class User - { +namespace AspCoreServer.Models { + public class User { public int ID { get; set; } public string Name { get; set; } - [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)] - [DataType(DataType.Date)] + [DisplayFormat (DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)] + [DataType (DataType.Date)] public DateTime EntryTime { get; set; } //Setting Default value - public User() - { + public User () { EntryTime = DateTime.Now; } } diff --git a/Server/RestAPI/UsersController.cs b/Server/RestAPI/UsersController.cs index 3a7fe3d2..a23301e0 100644 --- a/Server/RestAPI/UsersController.cs +++ b/Server/RestAPI/UsersController.cs @@ -1,123 +1,95 @@ +using System; +using System.Linq; +using System.Threading.Tasks; using AspCoreServer.Data; using AspCoreServer.Models; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; -using System; -using System.Linq; -using System.Threading.Tasks; -namespace AspCoreServer.Controllers -{ - [Route("api/[controller]")] - public class UsersController : Controller - { - private readonly SpaDbContext _context; +namespace AspCoreServer.Controllers { + [Route ("api/[controller]")] + public class UsersController : Controller { + private readonly SpaDbContext context; - public UsersController(SpaDbContext context) - { - _context = context; - } + public UsersController(SpaDbContext context) => this.context = context; - [HttpGet] - public async Task<IActionResult> Get(int currentPageNo = 1, int pageSize = 20) - { - var users = await _context.User - .OrderByDescending(u => u.EntryTime) - .Skip((currentPageNo - 1) * pageSize) - .Take(pageSize) - .ToArrayAsync(); + [HttpGet] + public async Task<IActionResult> Get (int currentPageNo = 1, int pageSize = 20) { + var users = await this.context.User + .OrderByDescending (u => u.EntryTime) + .Skip ((currentPageNo - 1) * pageSize) + .Take (pageSize) + .ToArrayAsync (); - if (!users.Any()) - { - return NotFound("Users not Found"); - } - else - { - return Ok(users); - } - } + if (!users.Any ()) { + return NotFound ("Users not Found"); + } else { + return Ok (users); + } + } - [HttpGet("{id}")] - public async Task<IActionResult> Get(int id) - { - var user = await _context.User - .Where(u => u.ID == id) - .AsNoTracking() - .SingleOrDefaultAsync(m => m.ID == id); + [HttpGet ("{id}")] + public async Task<IActionResult> Get (int id) { + var user = await this.context.User + .Where (u => u.ID == id) + .AsNoTracking () + .SingleOrDefaultAsync (m => m.ID == id); - if (user == null) - { - return NotFound("User not Found"); - } - else - { - return Ok(user); - } - } + if (user == null) { + return NotFound ("User not Found"); + } else { + return Ok (user); + } + } - [HttpPost] - public async Task<IActionResult> Post([FromBody]User user) - { - if (!string.IsNullOrEmpty(user.Name)) - { - _context.Add(user); - await _context.SaveChangesAsync(); - return CreatedAtAction("Post", user); - } - else - { - return BadRequest("User's name was not given"); - } - } + [HttpPost] + public async Task<IActionResult> Post ([FromBody] User user) { + if (!string.IsNullOrEmpty (user.Name)) { + this.context.Add (user); + await this.context.SaveChangesAsync (); + return CreatedAtAction ("Post", user); + } else { + return BadRequest ("User's name was not given"); + } + } - [HttpPut("{id}")] - public async Task<IActionResult> Put(int id, [FromBody]User userUpdateValue) - { - try - { - userUpdateValue.EntryTime = DateTime.Now; + [HttpPut ("{id}")] + public async Task<IActionResult> Put (int id, [FromBody] User userUpdateValue) { + try { + userUpdateValue.EntryTime = DateTime.Now; - var userToEdit = await _context.User - .AsNoTracking() - .SingleOrDefaultAsync(m => m.ID == id); + var userToEdit = await context.User + .AsNoTracking () + .SingleOrDefaultAsync (m => m.ID == id); - if (userToEdit == null) - { - return NotFound("Could not update user as it was not Found"); + if (userToEdit == null) { + return NotFound ("Could not update user as it was not Found"); + } else { + this.context.Update (userUpdateValue); + await this.context.SaveChangesAsync (); + return Json ("Updated user - " + userUpdateValue.Name); + } + } catch (DbUpdateException) { + //Log the error (uncomment ex variable name and write a log.) + this.ModelState.AddModelError ("", "Unable to save changes. " + + "Try again, and if the problem persists, " + + "see your system administrator."); + return NotFound ("User not Found"); + } } - else - { - _context.Update(userUpdateValue); - await _context.SaveChangesAsync(); - return Json("Updated user - " + userUpdateValue.Name); - } - } - catch (DbUpdateException) - { - //Log the error (uncomment ex variable name and write a log.) - ModelState.AddModelError("", "Unable to save changes. " + - "Try again, and if the problem persists, " + - "see your system administrator."); - return NotFound("User not Found"); - } - } - [HttpDelete("{id}")] - public async Task<IActionResult> Delete(int id) - { - var userToRemove = await _context.User - .AsNoTracking() - .SingleOrDefaultAsync(m => m.ID == id); - if (userToRemove == null) - { - return NotFound("Could not delete user as it was not Found"); - } - else - { - _context.User.Remove(userToRemove); - await _context.SaveChangesAsync(); - return Json("Deleted user - " + userToRemove.Name); - } + [HttpDelete ("{id}")] + public async Task<IActionResult> Delete (int id) { + var userToRemove = await this.context.User + .AsNoTracking () + .SingleOrDefaultAsync (m => m.ID == id); + if (userToRemove == null) { + return NotFound ("Could not delete user as it was not Found"); + } else { + this.context.User.Remove (userToRemove); + await this.context.SaveChangesAsync (); + return Json ("Deleted user - " + userToRemove.Name); + } + } } - } } diff --git a/Startup.cs b/Startup.cs index bc77beb6..a1b0f70d 100644 --- a/Startup.cs +++ b/Startup.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using AspCoreServer.Data; using Microsoft.AspNetCore.Builder; @@ -14,112 +14,96 @@ using WebEssentials.AspNetCore.Pwa; namespace AspCoreServer { - public class Startup { - - public static void Main (string[] args) { - var host = new WebHostBuilder () - .UseKestrel () - .UseContentRoot (Directory.GetCurrentDirectory ()) - .UseIISIntegration () - .UseStartup<Startup> () - .Build (); - - host.Run (); - } - public Startup (IHostingEnvironment env) { - var builder = new ConfigurationBuilder () - .SetBasePath (env.ContentRootPath) - .AddJsonFile ("appsettings.json", optional : true, reloadOnChange : true) - .AddJsonFile ($"appsettings.{env.EnvironmentName}.json", optional : true) - .AddEnvironmentVariables (); - Configuration = builder.Build (); - } - - public IConfigurationRoot Configuration { get; } - - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices (IServiceCollection services) { - // Add framework services. - services.AddMvc (); - services.AddNodeServices (); - services.AddHttpContextAccessor (); - services.AddProgressiveWebApp (new PwaOptions { Strategy = ServiceWorkerStrategy.CacheFirst, RegisterServiceWorker = true, RegisterWebmanifest = true }, "manifest.json"); - - var connectionStringBuilder = new Microsoft.Data.Sqlite.SqliteConnectionStringBuilder { DataSource = "spa.db" }; - var connectionString = connectionStringBuilder.ToString (); - - services.AddDbContext<SpaDbContext> (options => - options.UseSqlite (connectionString)); - - // Register the Swagger generator, defining one or more Swagger documents - services.AddSwaggerGen (c => { - c.SwaggerDoc ("v1", new Info { Title = "Angular 6.0 Universal & ASP.NET Core advanced starter-kit web API", Version = "v1" }); - }); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure (IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, SpaDbContext context) { - loggerFactory.AddConsole (Configuration.GetSection ("Logging")); - loggerFactory.AddDebug (); - - // app.UseStaticFiles(); - - app.UseStaticFiles (new StaticFileOptions () { - OnPrepareResponse = c => { - //Do not add cache to json files. We need to have new versions when we add new translations. - - if (!c.Context.Request.Path.Value.Contains (".json")) { - c.Context.Response.GetTypedHeaders ().CacheControl = new CacheControlHeaderValue () { - MaxAge = TimeSpan.FromDays (30) // Cache everything except json for 30 days - }; - } else { - c.Context.Response.GetTypedHeaders ().CacheControl = new CacheControlHeaderValue () { - MaxAge = TimeSpan.FromMinutes (15) // Cache json for 15 minutes - }; - } + public class Startup { + public Startup (IHostingEnvironment env) { + var builder = new ConfigurationBuilder () + .SetBasePath (env.ContentRootPath) + .AddJsonFile ("appsettings.json", optional : true, reloadOnChange : true) + .AddJsonFile ($"appsettings.{env.EnvironmentName}.json", optional : true) + .AddEnvironmentVariables (); + this.Configuration = builder.Build (); } - }); - - DbInitializer.Initialize (context); - if (env.IsDevelopment ()) { - app.UseDeveloperExceptionPage (); - app.UseWebpackDevMiddleware (new WebpackDevMiddlewareOptions { - HotModuleReplacement = true, - HotModuleReplacementEndpoint = "/dist/" - }); - app.UseSwagger (); - app.UseSwaggerUI (c => { - c.SwaggerEndpoint ("/swagger/v1/swagger.json", "My API V1"); - }); + public IConfigurationRoot Configuration { get; } - // Enable middleware to serve swagger-ui (HTML, JS, CSS etc.), specifying the Swagger JSON endpoint. + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices (IServiceCollection services) { + // Add framework services. + services.AddMvc (); + services.AddNodeServices (); + services.AddHttpContextAccessor (); + services.AddProgressiveWebApp (new PwaOptions { Strategy = ServiceWorkerStrategy.CacheFirst, RegisterServiceWorker = true, RegisterWebmanifest = true }, "manifest.json"); - app.MapWhen (x => !x.Request.Path.Value.StartsWith ("/swagger", StringComparison.OrdinalIgnoreCase), builder => { - builder.UseMvc (routes => { - routes.MapSpaFallbackRoute ( - name: "spa-fallback", - defaults : new { controller = "Home", action = "Index" }); - }); - }); - } else { - app.UseMvc (routes => { - routes.MapRoute ( - name: "default", - template: "{controller=Home}/{action=Index}/{id?}"); + var connectionStringBuilder = new Microsoft.Data.Sqlite.SqliteConnectionStringBuilder { DataSource = "spa.db" }; + var connectionString = connectionStringBuilder.ToString(); - routes.MapRoute ( - "Sitemap", - "sitemap.xml", - new { controller = "Home", action = "SitemapXml" }); + services.AddDbContext<SpaDbContext>(options => + options.UseSqlite(connectionString)); - routes.MapSpaFallbackRoute ( - name: "spa-fallback", - defaults : new { controller = "Home", action = "Index" }); + // Register the Swagger generator, defining one or more Swagger documents + services.AddSwaggerGen (c => { + c.SwaggerDoc ("v1", new Info { Title = "Angular 6.0 Universal & ASP.NET Core advanced starter-kit web API", Version = "v1" }); + }); + } - }); - app.UseExceptionHandler ("/Home/Error"); - } + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure (IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, SpaDbContext context) { + loggerFactory.AddConsole (this.Configuration.GetSection ("Logging")); + loggerFactory.AddDebug (); + + // app.UseStaticFiles(); + + app.UseStaticFiles (new StaticFileOptions () { + OnPrepareResponse = c => { + //Do not add cache to json files. We need to have new versions when we add new translations. + c.Context.Response.GetTypedHeaders ().CacheControl = !c.Context.Request.Path.Value.Contains (".json") + ? new CacheControlHeaderValue () { + MaxAge = TimeSpan.FromDays (30) // Cache everything except json for 30 days + } + : new CacheControlHeaderValue () { + MaxAge = TimeSpan.FromMinutes (15) // Cache json for 15 minutes + }; + } + }); + + if (env.IsDevelopment ()) { + app.UseDeveloperExceptionPage (); + app.UseWebpackDevMiddleware (new WebpackDevMiddlewareOptions { + HotModuleReplacement = true, + HotModuleReplacementEndpoint = "/dist/" + }); + app.UseSwagger (); + app.UseSwaggerUI (c => { + c.SwaggerEndpoint ("/swagger/v1/swagger.json", "My API V1"); + }); + + // Enable middleware to serve swagger-ui (HTML, JS, CSS etc.), specifying the Swagger JSON endpoint. + + app.MapWhen (x => !x.Request.Path.Value.StartsWith ("/swagger", StringComparison.OrdinalIgnoreCase), builder => { + builder.UseMvc (routes => { + routes.MapSpaFallbackRoute ( + name: "spa-fallback", + defaults : new { controller = "Home", action = "Index" }); + }); + }); + } else { + app.UseMvc (routes => { + routes.MapRoute ( + name: "default", + template: "{controller=Home}/{action=Index}/{id?}"); + + routes.MapRoute ( + "Sitemap", + "sitemap.xml", + new { controller = "Home", action = "SitemapXml" }); + + routes.MapSpaFallbackRoute ( + name: "spa-fallback", + defaults : new { controller = "Home", action = "Index" }); + + }); + app.UseExceptionHandler ("/Home/Error"); + } + } } - } -} \ No newline at end of file +} diff --git a/angular.json b/angular.json index 42179045..b6c5377c 100644 --- a/angular.json +++ b/angular.json @@ -5,28 +5,52 @@ "projects": { "AspnetCore-Angular-Universal": { "root": "", - "sourceRoot": "src", + "sourceRoot": "ClientApp", "projectType": "application", + "prefix": "app", + "schematics": {}, "architect": { "build": { "builder": "@angular-devkit/build-angular:browser", "options": { - "outputPath": "dist/", - "index": "ClientApp/index.html", - "main": "ClientApp/main.ts", + "outputPath": "wwwroot/dist", + "main": "ClientApp/boot-browser.ts", + "polyfills": "ClientApp/polyfills/polyfills.ts", "tsConfig": "ClientApp/tsconfig.app.json", "assets": [], "styles": [], "scripts": [] }, - "configurations": {} + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "ClientApp/environments/environment.ts", + "with": "ClientApp/environments/environment.prod.ts" + } + ], + "optimization": true, + "outputHashing": "all", + "sourceMap": false, + "extractCss": true, + "namedChunks": false, + "aot": true, + "extractLicenses": true, + "vendorChunk": false, + "buildOptimizer": true + } + } }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", "options": { "browserTarget": "AspnetCore-Angular-Universal:build" }, - "configurations": {} + "configurations": { + "production": { + "browserTarget": "AspnetCore-Angular-Universal:build:production" + } + } }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", @@ -34,6 +58,18 @@ "browserTarget": "AspnetCore-Angular-Universal:build" } }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "ClientApp/test.ts", + "polyfills": "ClientApp/polyfills.ts", + "tsConfig": "ClientApp/tsconfig.spec.json", + "karmaConfig": "ClientApp/karma.conf.js", + "styles": ["ClientApp/styles.css"], + "scripts": [], + "assets": ["ClientApp/favicon.ico", "ClientApp/assets"] + } + }, "lint": { "builder": "@angular-devkit/build-angular:tslint", "options": { @@ -41,23 +77,31 @@ "ClientApp/tsconfig.app.json", "ClientApp/tsconfig.spec.json" ], - "exclude": [] + "exclude": ["**/node_modules/**"] } } } }, "AspnetCore-Angular-Universal-e2e": { - "root": "", - "sourceRoot": "", - "projectType": "application" + "root": "e2e/", + "projectType": "application", + "architect": { + "e2e": { + "builder": "@angular-devkit/build-angular:protractor", + "options": { + "protractorConfig": "e2e/protractor.conf.js", + "devServerTarget": "AspnetCore-Angular-Universal:serve" + } + }, + "lint": { + "builder": "@angular-devkit/build-angular:tslint", + "options": { + "tsConfig": "e2e/tsconfig.e2e.json", + "exclude": ["**/node_modules/**"] + } + } + } } }, - "defaultProject": "AspnetCore-Angular-Universal", - "schematics": { - "@schematics/angular:component": { - "spec": false, - "styleext": "scss" - }, - "@schematics/angular:directive": {} - } -} \ No newline at end of file + "defaultProject": "AspnetCore-Angular-Universal" +} diff --git a/webpack.additions.js b/webpack.additions.js index 62bf4bb3..9c63a2f8 100644 --- a/webpack.additions.js +++ b/webpack.additions.js @@ -7,12 +7,14 @@ // Shared rules[] we need to add const sharedModuleRules = [ // sass - { test: /\.scss$/, loaders: ['to-string-loader', 'css-loader', 'sass-loader'] }, + { + test: /\.scss$/, + loaders: ['to-string-loader', 'css-loader', 'sass-loader'] + }, // font-awesome { test: /\.(woff2?|ttf|eot|svg)$/, loader: 'url-loader?limit=10000' } ]; - module.exports = { sharedModuleRules }; diff --git a/webpack.config.js b/webpack.config.js index a7984c89..d8689752 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -13,18 +13,17 @@ const webpack = require('webpack'); const merge = require('webpack-merge'); const AngularCompilerPlugin = require('@ngtools/webpack').AngularCompilerPlugin; const CheckerPlugin = require('awesome-typescript-loader').CheckerPlugin; -const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; +const BundleAnalyzerPlugin = require('webpack-bundle-analyzer') + .BundleAnalyzerPlugin; const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); -const { - sharedModuleRules -} = require('./webpack.additions'); +const { sharedModuleRules } = require('./webpack.additions'); -module.exports = (env) => { +module.exports = env => { // Configuration in common to both client-side and server-side bundles const isDevBuild = !(env && env.prod); const sharedConfig = { - mode: isDevBuild ? "development" : "production", + mode: isDevBuild ? 'development' : 'production', stats: { modules: false }, @@ -37,9 +36,16 @@ module.exports = (env) => { publicPath: 'dist/' // Webpack dev middleware, if enabled, handles requests for this URL prefix }, module: { - rules: [{ + rules: [ + { test: /\.ts$/, - use: isDevBuild ? ['awesome-typescript-loader?silent=true', 'angular2-template-loader', 'angular2-router-loader'] : '@ngtools/webpack' + use: isDevBuild + ? [ + 'awesome-typescript-loader?silent=true', + 'angular2-template-loader', + 'angular2-router-loader' + ] + : '@ngtools/webpack' }, { test: /\.html$/, @@ -47,7 +53,10 @@ module.exports = (env) => { }, { test: /\.css$/, - use: ['to-string-loader', isDevBuild ? 'css-loader' : 'css-loader?minimize'] + use: [ + 'to-string-loader', + isDevBuild ? 'css-loader' : 'css-loader?minimize' + ] }, { test: /\.(png|jpg|jpeg|gif|svg)$/, @@ -73,42 +82,57 @@ module.exports = (env) => { context: __dirname, manifest: require('./wwwroot/dist/vendor-manifest.json') }) - ].concat(isDevBuild ? [ - // Plugins that apply in development builds only - new webpack.SourceMapDevToolPlugin({ - filename: '[file].map', // Remove this line if you prefer inline source maps - moduleFilenameTemplate: path.relative(clientBundleOutputDir, '[resourcePath]') // Point sourcemap entries to the original file locations on disk - }) - ] : [ - // new BundleAnalyzerPlugin(), - // Plugins that apply in production builds only - new AngularCompilerPlugin({ - mainPath: path.join(__dirname, 'ClientApp/boot.browser.ts'), - tsConfigPath: './tsconfig.json', - entryModule: path.join(__dirname, 'ClientApp/app/app.module.browser#AppModule'), - exclude: ['./**/*.server.ts'] - }) - ]), + ].concat( + isDevBuild + ? [ + // Plugins that apply in development builds only + new webpack.SourceMapDevToolPlugin({ + filename: '[file].map', // Remove this line if you prefer inline source maps + moduleFilenameTemplate: path.relative( + clientBundleOutputDir, + '[resourcePath]' + ) // Point sourcemap entries to the original file locations on disk + }) + ] + : [ + // new BundleAnalyzerPlugin(), + // Plugins that apply in production builds only + new AngularCompilerPlugin({ + mainPath: path.join(__dirname, 'ClientApp/boot.browser.ts'), + tsConfigPath: './tsconfig.json', + entryModule: path.join( + __dirname, + 'ClientApp/app/app.module.browser#AppModule' + ), + exclude: ['./**/*.server.ts'], + sourceMap: isDevBuild + }) + ] + ), devtool: isDevBuild ? 'cheap-eval-source-map' : false, node: { - fs: "empty" + fs: 'empty' }, optimization: { - minimizer: [].concat(isDevBuild ? [] : [ - // we specify a custom UglifyJsPlugin here to get source maps in production - new UglifyJsPlugin({ - cache: true, - parallel: true, - uglifyOptions: { - compress: false, - ecma: 6, - mangle: true, - keep_classnames: true, - keep_fnames: true - }, - sourceMap: true - }) - ]) + minimizer: [].concat( + isDevBuild + ? [] + : [ + // we specify a custom UglifyJsPlugin here to get source maps in production + new UglifyJsPlugin({ + cache: true, + parallel: true, + uglifyOptions: { + compress: false, + ecma: 6, + mangle: true, + keep_classnames: true, + keep_fnames: true + }, + sourceMap: true + }) + ] + ) } }); @@ -116,7 +140,9 @@ module.exports = (env) => { const serverBundleConfig = merge(sharedConfig, { // resolve: { mainFields: ['main'] }, entry: { - 'main-server': isDevBuild ? './ClientApp/boot.server.ts' : './ClientApp/boot.server.PRODUCTION.ts' + 'main-server': isDevBuild + ? './ClientApp/boot.server.ts' + : './ClientApp/boot.server.PRODUCTION.ts' }, plugins: [ new webpack.DllReferencePlugin({ @@ -125,27 +151,39 @@ module.exports = (env) => { sourceType: 'commonjs2', name: './vendor' }) - ].concat(isDevBuild ? [ - new webpack.ContextReplacementPlugin( - // fixes WARNING Critical dependency: the request of a dependency is an expression - /(.+)?angular(\\|\/)core(.+)?/, - path.join(__dirname, 'src'), // location of your src - {} // a map of your routes - ), - new webpack.ContextReplacementPlugin( - // fixes WARNING Critical dependency: the request of a dependency is an expression - /(.+)?express(\\|\/)(.+)?/, - path.join(__dirname, 'src'), {} - ) - ] : [ - // Plugins that apply in production builds only - new AngularCompilerPlugin({ - mainPath: path.join(__dirname, 'ClientApp/boot.server.PRODUCTION.ts'), - tsConfigPath: './tsconfig.json', - entryModule: path.join(__dirname, 'ClientApp/app/app.module.server#AppModule'), - exclude: ['./**/*.browser.ts'] - }) - ]), + ].concat( + isDevBuild + ? [ + new webpack.ContextReplacementPlugin( + // fixes WARNING Critical dependency: the request of a dependency is an expression + /(.+)?angular(\\|\/)core(.+)?/, + path.join(__dirname, 'src'), // location of your src + {} // a map of your routes + ), + new webpack.ContextReplacementPlugin( + // fixes WARNING Critical dependency: the request of a dependency is an expression + /(.+)?express(\\|\/)(.+)?/, + path.join(__dirname, 'src'), + {} + ) + ] + : [ + // Plugins that apply in production builds only + new AngularCompilerPlugin({ + mainPath: path.join( + __dirname, + 'ClientApp/boot.server.PRODUCTION.ts' + ), + tsConfigPath: './tsconfig.json', + entryModule: path.join( + __dirname, + 'ClientApp/app/app.module.server#AppModule' + ), + exclude: ['./**/*.browser.ts'], + sourceMap: isDevBuild + }) + ] + ), output: { libraryTarget: 'commonjs', path: path.join(__dirname, './ClientApp/dist') @@ -154,21 +192,25 @@ module.exports = (env) => { // switch to "inline-source-map" if you want to debug the TS during SSR devtool: isDevBuild ? 'cheap-eval-source-map' : false, optimization: { - minimizer: [].concat(isDevBuild ? [] : [ - // we specify a custom UglifyJsPlugin here to get source maps in production - new UglifyJsPlugin({ - cache: true, - parallel: true, - uglifyOptions: { - compress: false, - ecma: 6, - mangle: true, - keep_classnames: true, - keep_fnames: true - }, - sourceMap: true - }) - ]) + minimizer: [].concat( + isDevBuild + ? [] + : [ + // we specify a custom UglifyJsPlugin here to get source maps in production + new UglifyJsPlugin({ + cache: true, + parallel: true, + uglifyOptions: { + compress: false, + ecma: 6, + mangle: true, + keep_classnames: true, + keep_fnames: true + }, + sourceMap: true + }) + ] + ) } }); diff --git a/wwwroot/manifest.json b/wwwroot/manifest.json index fa6cd755..1f247be0 100644 --- a/wwwroot/manifest.json +++ b/wwwroot/manifest.json @@ -1,8 +1,10 @@ { "name": "ASP.NET Core 2.1 & Angular 6(+) Advanced Starter", "short_name": "aspnetcore-angular2-universal", - "description": "ASP.NET Core 2.1 & Angular 6(+) Advanced Starter - with Server-side prerendering (for Angular SEO)!", - "icons": [{ + "description": + "ASP.NET Core 2.1 & Angular 6(+) Advanced Starter - with Server-side prerendering (for Angular SEO)!", + "icons": [ + { "src": "/images/icon192x192.png", "sizes": "192x192" }, From 9071694afc5f3b9a2c4c7219d36a3ff65328e839 Mon Sep 17 00:00:00 2001 From: Martin Babacaev <martin.babacaev@gmail.com> Date: Wed, 15 Aug 2018 23:03:40 +0200 Subject: [PATCH 02/31] fix(build): add asp2017.sln to fix msbuild issues closes #658 --- .vscode/tasks.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 33256db7..da1cb069 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -8,7 +8,7 @@ "tasks": [ { "taskName": "build", - "args": [ ], + "args": ["Asp2017.sln"], "isBuildCommand": true, "showOutput": "silent", "problemMatcher": "$msCompile" From a939ad81c40a6d3c32e2874e67a310d197ab8f49 Mon Sep 17 00:00:00 2001 From: Tobias Punke <Gaulomatic@users.noreply.github.com> Date: Tue, 21 Aug 2018 15:58:08 +0200 Subject: [PATCH 03/31] chore(csproj): update .csproj to reflect changes in .NET Core 2.1 --- Asp2017.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Asp2017.csproj b/Asp2017.csproj index 7db490d3..7a28f3b4 100644 --- a/Asp2017.csproj +++ b/Asp2017.csproj @@ -9,7 +9,7 @@ </PropertyGroup> <ItemGroup> <!-- New Meta Package has SpaServices in It --> - <PackageReference Include="Microsoft.AspNetCore.All" Version="2.1.0" /> + <PackageReference Include="Microsoft.AspNetCore.All" /> <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="0.1.1646902" /> <PackageReference Include="NETStandard.Library" Version="2.0.3" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.1.0" /> From c212c17dc5ce7c0c53d84ce6241a3dd3bd18c1c9 Mon Sep 17 00:00:00 2001 From: Ivan <chasow@yandex.ru> Date: Sat, 1 Sep 2018 04:23:25 +0300 Subject: [PATCH 04/31] docs(readme): change Core 2.0 to Core 2.1 --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8ed5a749..a37cdd2a 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,10 @@ > Updated to the latest Angular 6.x <p align="center"> - <img src="http://github.com/docs/architecture.png" alt="ASP.NET Core 2.0 Angular 6+ Starter" title="ASP.NET Core 2.0 Angular 6+ Starter"> + <img src="http://github.com/docs/architecture.png" alt="ASP.NET Core 2.1 Angular 6+ Starter" title="ASP.NET Core 2.1 Angular 6+ Starter"> </p> -### Harness the power of Angular 6+, ASP.NET Core 2.0, now with SEO ! +### Harness the power of Angular 6+, ASP.NET Core 2.1, now with SEO ! Angular SEO in action: From 706809df76113e0037fab4c3da858e77e2f53039 Mon Sep 17 00:00:00 2001 From: Mark Pieszak <mpieszak84@gmail.com> Date: Thu, 20 Sep 2018 21:04:39 -0400 Subject: [PATCH 05/31] fix(build): fix major build and run-time errors closes #680 --- ClientApp/app/app.module.server.ts | 8 +++-- .../counter/counter.component.spec.ts | 2 ++ ClientApp/boot.browser.ts | 2 +- ClientApp/boot.server.PRODUCTION.ts | 12 +++---- ClientApp/boot.server.ts | 9 +++--- ClientApp/polyfills/browser.polyfills.ts | 5 ++- ClientApp/polyfills/polyfills.ts | 32 +++++++++++-------- ClientApp/polyfills/server.polyfills.ts | 4 ++- ClientApp/tsconfig.app.json | 6 +++- Views/Home/Index.cshtml | 1 + package.json | 19 ++++++++--- webpack.config.js | 6 ++-- 12 files changed, 66 insertions(+), 40 deletions(-) diff --git a/ClientApp/app/app.module.server.ts b/ClientApp/app/app.module.server.ts index e5fcb3de..06af629a 100644 --- a/ClientApp/app/app.module.server.ts +++ b/ClientApp/app/app.module.server.ts @@ -5,6 +5,8 @@ import { PrebootModule } from 'preboot'; import { AppComponent } from './app.component'; import { AppModuleShared } from './app.module'; +import { TransferHttpCacheModule, StateTransferInitializerModule } from '@nguniversal/common'; + @NgModule({ bootstrap: [AppComponent], imports: [ @@ -13,12 +15,12 @@ import { AppModuleShared } from './app.module'; ServerModule, PrebootModule.withConfig({ appRoot: 'app-root' }), - NoopAnimationsModule + NoopAnimationsModule, - // HttpTransferCacheModule still needs fixes for 5.0 + TransferHttpCacheModule, // still needs fixes for 5.0 // Leave this commented out for now, as it breaks Server-renders // Looking into fixes for this! - @MarkPieszak - // ServerTransferStateModule // <-- broken for the time-being with ASP.NET + // StateTransferInitializerModule // <-- broken for the time-being with ASP.NET ] }) export class AppModule { diff --git a/ClientApp/app/containers/counter/counter.component.spec.ts b/ClientApp/app/containers/counter/counter.component.spec.ts index 92202646..960f08d3 100644 --- a/ClientApp/app/containers/counter/counter.component.spec.ts +++ b/ClientApp/app/containers/counter/counter.component.spec.ts @@ -1,6 +1,8 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { CounterComponent } from './counter.component'; +import {} from 'jasmine'; + let fixture: ComponentFixture<CounterComponent>; describe('Counter component', () => { diff --git a/ClientApp/boot.browser.ts b/ClientApp/boot.browser.ts index c0ef55b6..68de6e5b 100644 --- a/ClientApp/boot.browser.ts +++ b/ClientApp/boot.browser.ts @@ -1,7 +1,7 @@ +import './polyfills/browser.polyfills'; import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module.browser'; -import './polyfills/browser.polyfills'; // // Enable either Hot Module Reloading or production mode if (module['hot']) { diff --git a/ClientApp/boot.server.PRODUCTION.ts b/ClientApp/boot.server.PRODUCTION.ts index b7bcbb7a..e3c33ff0 100644 --- a/ClientApp/boot.server.PRODUCTION.ts +++ b/ClientApp/boot.server.PRODUCTION.ts @@ -1,15 +1,11 @@ -import { enableProdMode } from '@angular/core'; -import { - createTransferScript, - IEngineOptions, - ngAspnetCoreEngine -} from '@nguniversal/aspnetcore-engine'; -import { createServerRenderer } from 'aspnet-prerendering'; -import 'zone.js/dist/zone-node'; +import 'zone.js/dist/zone-node'; import './polyfills/server.polyfills'; +import { enableProdMode } from '@angular/core'; +import { createServerRenderer } from 'aspnet-prerendering'; // Grab the (Node) server-specific NgModule const { AppModuleNgFactory } = require('./app/app.module.server.ngfactory'); // <-- ignore this - this is Production only +import { ngAspnetCoreEngine, IEngineOptions, createTransferScript } from '@nguniversal/aspnetcore-engine'; enableProdMode(); diff --git a/ClientApp/boot.server.ts b/ClientApp/boot.server.ts index 72457842..b6510018 100644 --- a/ClientApp/boot.server.ts +++ b/ClientApp/boot.server.ts @@ -1,11 +1,12 @@ -import { enableProdMode } from '@angular/core'; +import 'zone.js/dist/zone-node'; +import './polyfills/server.polyfills'; + +import { enableProdMode } from '@angular/core'; import { createTransferScript, IEngineOptions, ngAspnetCoreEngine } from '@nguniversal/aspnetcore-engine'; import { createServerRenderer } from 'aspnet-prerendering'; -import 'zone.js/dist/zone-node'; + // Grab the (Node) server-specific NgModule import { AppModule } from './app/app.module.server'; -import './polyfills/server.polyfills'; - enableProdMode(); diff --git a/ClientApp/polyfills/browser.polyfills.ts b/ClientApp/polyfills/browser.polyfills.ts index 207f7e06..9e6fe1d4 100644 --- a/ClientApp/polyfills/browser.polyfills.ts +++ b/ClientApp/polyfills/browser.polyfills.ts @@ -1,5 +1,8 @@ +// Note: * The order is IMPORTANT! * + +import './polyfills.ts'; + import 'reflect-metadata'; import 'zone.js/dist/zone'; -import './polyfills.ts'; // import 'web-animations-js'; // Run `npm install --save web-animations-js`. diff --git a/ClientApp/polyfills/polyfills.ts b/ClientApp/polyfills/polyfills.ts index b577f1c4..45b62d73 100644 --- a/ClientApp/polyfills/polyfills.ts +++ b/ClientApp/polyfills/polyfills.ts @@ -1,25 +1,29 @@ +// Note: * The order is IMPORTANT! * + /*************************************************************************************************** * BROWSER POLYFILLS */ /** IE9, IE10 and IE11 requires all of the following polyfills. **/ -import 'core-js/es6/array'; -import 'core-js/es6/date'; -import 'core-js/es6/function'; -import 'core-js/es6/map'; -import 'core-js/es6/math'; -import 'core-js/es6/number'; +import 'core-js/es6/symbol'; import 'core-js/es6/object'; -import 'core-js/es6/parse-float'; +import 'core-js/es6/function'; import 'core-js/es6/parse-int'; -/** IE10 and IE11 requires the following for NgClass support on SVG elements */ -// import 'classlist.js'; // Run `npm install --save classlist.js`. -/** Evergreen browsers require these. **/ -import 'core-js/es6/reflect'; +import 'core-js/es6/parse-float'; +import 'core-js/es6/number'; +import 'core-js/es6/math'; +import 'core-js/es6/string'; +import 'core-js/es6/date'; +import 'core-js/es6/array'; import 'core-js/es6/regexp'; +import 'core-js/es6/map'; import 'core-js/es6/set'; -import 'core-js/es6/string'; -import 'core-js/es6/symbol'; -import 'core-js/es7/reflect'; + /** */ import 'reflect-metadata'; + +/** IE10 and IE11 requires the following for NgClass support on SVG elements */ +// import 'classlist.js'; // Run `npm install --save classlist.js`. +/** Evergreen browsers require these. **/ +import 'core-js/es6/reflect'; +import 'core-js/es7/reflect'; \ No newline at end of file diff --git a/ClientApp/polyfills/server.polyfills.ts b/ClientApp/polyfills/server.polyfills.ts index 96b07953..68867b14 100644 --- a/ClientApp/polyfills/server.polyfills.ts +++ b/ClientApp/polyfills/server.polyfills.ts @@ -1,2 +1,4 @@ -import 'zone.js'; +// Note: * The order is IMPORTANT! * import './polyfills.ts'; + +import 'zone.js'; diff --git a/ClientApp/tsconfig.app.json b/ClientApp/tsconfig.app.json index 2cf4ae53..aec85d7d 100644 --- a/ClientApp/tsconfig.app.json +++ b/ClientApp/tsconfig.app.json @@ -6,5 +6,9 @@ "baseUrl": "", "types": [] }, - "exclude": ["test.ts", "**/*.spec.ts", "boot.server.PRODUCTION.ts"] + "exclude": [ + "test.ts", + "**/*.spec.ts", + "boot.server.PRODUCTION.ts" + ] } diff --git a/Views/Home/Index.cshtml b/Views/Home/Index.cshtml index b8f7a97f..d8882a29 100644 --- a/Views/Home/Index.cshtml +++ b/Views/Home/Index.cshtml @@ -9,4 +9,5 @@ <!-- Our webpack bundle --> <script src="http://github.com/~/dist/main-client.js" asp-append-version="true"></script> } + @Html.Raw(ViewData["ServiceWorker"]) diff --git a/package.json b/package.json index a7fcc1ca..c75c5113 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,13 @@ { - "name": "angular4-aspnetcore-universal", - "version": "1.0.0-rc3", + "name": "angular6-aspnetcore-universal", + "author": { + "name": "Mark Pieszak", + "email": "hello@devhelp.online", + "url": "http://devhelp.online/" + }, + "version": "1.0.0-rc4", "scripts": { - "clean:install": "npm run clean && rimraf ./node_modules ./bin ./obj ./package-lock.json && dotnet restore && npm i", + "clean:install": "npm run clean && rimraf ./node_modules ./bin ./obj ./package-lock.json && dotnet restore Asp2017.csproj && npm i", "lint": "tslint -p tsconfig.json", "test": "npm run build:vendor && karma start ClientApp/test/karma.conf.js", "test:watch": "npm run test -- --auto-watch --no-single-run", @@ -92,5 +97,11 @@ "uglifyjs-webpack-plugin": "^1.2.5", "webpack-bundle-analyzer": "^2.13.1", "webpack-cli": "^2.1.4" - } + }, + "license": "MIT", + "repository": { + "type": "github", + "url": "https://github.com/MarkPieszak/aspnetcore-angular2-universal" + }, + "readme": "https://github.com/MarkPieszak/aspnetcore-angular2-universal/blob/master/README.md" } diff --git a/webpack.config.js b/webpack.config.js index d8689752..3ea5c207 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -38,7 +38,7 @@ module.exports = env => { module: { rules: [ { - test: /\.ts$/, + test: /^(?!.*\.spec\.ts$).*\.ts$/, use: isDevBuild ? [ 'awesome-typescript-loader?silent=true', @@ -99,7 +99,7 @@ module.exports = env => { // Plugins that apply in production builds only new AngularCompilerPlugin({ mainPath: path.join(__dirname, 'ClientApp/boot.browser.ts'), - tsConfigPath: './tsconfig.json', + tsConfigPath: './ClientApp/tsconfig.app.json', entryModule: path.join( __dirname, 'ClientApp/app/app.module.browser#AppModule' @@ -174,7 +174,7 @@ module.exports = env => { __dirname, 'ClientApp/boot.server.PRODUCTION.ts' ), - tsConfigPath: './tsconfig.json', + tsConfigPath: './ClientApp/tsconfig.app.json', entryModule: path.join( __dirname, 'ClientApp/app/app.module.server#AppModule' From 6c71405a0e3cc9839c0091d58b2548e858090d66 Mon Sep 17 00:00:00 2001 From: emkialton <43580460+emkialton@users.noreply.github.com> Date: Thu, 27 Sep 2018 18:05:16 +0200 Subject: [PATCH 06/31] fix(prod build): fix production build closes #685 --- ClientApp/tsconfig.app.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ClientApp/tsconfig.app.json b/ClientApp/tsconfig.app.json index aec85d7d..a082833e 100644 --- a/ClientApp/tsconfig.app.json +++ b/ClientApp/tsconfig.app.json @@ -4,11 +4,11 @@ "outDir": "../out-tsc/app", "module": "es2015", "baseUrl": "", - "types": [] + "sourceMap": true, + "types": ["node"] }, "exclude": [ "test.ts", - "**/*.spec.ts", - "boot.server.PRODUCTION.ts" + "**/*.spec.ts" ] } From a1eb39b1e1c09edfeea19e62df2de743f63eecbe Mon Sep 17 00:00:00 2001 From: Steven Smith <stevensmith89@gmail.com> Date: Tue, 16 Oct 2018 12:09:43 -0700 Subject: [PATCH 07/31] docs(readme): README.md table of content links fix --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a37cdd2a..8b15d92e 100644 --- a/README.md +++ b/README.md @@ -38,10 +38,10 @@ This utilizes all the latest standards, no gulp, no bower, no typings, no manual * [Upcoming Features](#upcoming-features) * [Application Structure](#application-structure) * [Gotchas](#gotchas) -* [FAQ](#faq---also-check-out-the-faq-issues-label) +* [FAQ](#faq---also-check-out-the-faq-issues-label-and-the-how-to-issues-label) * [Special Thanks](#special-thanks) * [License](#license) -* [Consulting & Training](#looking-for-angular--aspnet-consulting--training--support) +* [Consulting & Training](#devhelponline---angular--aspnet---consulting--training--development) --- From 1d93c776ab7856dced281434fd49c794d287373f Mon Sep 17 00:00:00 2001 From: Mark Pieszak <mpieszak84@gmail.com> Date: Thu, 18 Oct 2018 10:47:20 -0400 Subject: [PATCH 08/31] fix(csproj): update entity framework to 2.1.1 closes #694 --- Asp2017.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Asp2017.csproj b/Asp2017.csproj index 7a28f3b4..b307acba 100644 --- a/Asp2017.csproj +++ b/Asp2017.csproj @@ -12,8 +12,8 @@ <PackageReference Include="Microsoft.AspNetCore.All" /> <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="0.1.1646902" /> <PackageReference Include="NETStandard.Library" Version="2.0.3" /> - <PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.1.0" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.0" /> + <PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.1.1" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.1" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="2.4.0" /> <PackageReference Include="WebEssentials.AspNetCore.PWA" Version="1.0.33" /> </ItemGroup> From b396d9b51b4ac28a6f26a6cf95ef7457fb1ee14a Mon Sep 17 00:00:00 2001 From: Mark Pieszak <mpieszak84@gmail.com> Date: Mon, 26 Nov 2018 17:10:24 -0500 Subject: [PATCH 09/31] docs(readme): update --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 8b15d92e..94ea42b6 100644 --- a/README.md +++ b/README.md @@ -460,6 +460,8 @@ Nothing's ever perfect, but please let me know by creating an issue (make sure t Copyright (c) 2016-2018 [Mark Pieszak](https://github.com/MarkPieszak) +[![Twitter Follow](https://img.shields.io/twitter/follow/MarkPieszak.svg?style=social)](https://twitter.com/MarkPieszak) + ---- # DevHelp.Online - Angular & ASP.NET - Consulting | Training | Development From 53eb5327171e0699d230a74d97bc90f4db353ca0 Mon Sep 17 00:00:00 2001 From: Mark Pieszak <mpieszak84@gmail.com> Date: Mon, 10 Dec 2018 19:05:25 -0500 Subject: [PATCH 10/31] fix(webpack-cli): update to fix webpack issue when bundling vendor closes #690 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c75c5113..a929eabe 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,7 @@ "tslint": "^5.10.0", "uglifyjs-webpack-plugin": "^1.2.5", "webpack-bundle-analyzer": "^2.13.1", - "webpack-cli": "^2.1.4" + "webpack-cli": "^3.1.2" }, "license": "MIT", "repository": { From 976505d800a8c7a9f0954a0e80d7ce4b67448bad Mon Sep 17 00:00:00 2001 From: Mark Pieszak <mpieszak84@gmail.com> Date: Mon, 10 Dec 2018 19:25:10 -0500 Subject: [PATCH 11/31] fix(preboot): pin to preboot6-beta.4 until patched closes #704 --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index a929eabe..56510a52 100644 --- a/package.json +++ b/package.json @@ -32,8 +32,8 @@ "@angular/platform-browser-dynamic": "^6.0.3", "@angular/platform-server": "^6.0.3", "@angular/router": "^6.0.3", - "@nguniversal/aspnetcore-engine": "^6.0.0", - "@nguniversal/common": "^6.0.0", + "@nguniversal/aspnetcore-engine": "^7.0.2", + "@nguniversal/common": "^7.0.2", "@ngx-translate/core": "^10.0.2", "@ngx-translate/http-loader": "^3.0.1", "@types/node": "^10.1.2", @@ -57,7 +57,7 @@ "ngx-bootstrap": "^3.0.0", "node-sass": "^4.9.0", "npm": "^6.1.0", - "preboot": "^6.0.0-beta.4", + "preboot": "6.0.0-beta.4", "raw-loader": "^0.5.1", "rimraf": "^2.6.2", "rxjs": "^6.2.0", From d0cae199ad386c5f35d8aede79ef2d48224d3661 Mon Sep 17 00:00:00 2001 From: Mark Pieszak <mpieszak84@gmail.com> Date: Mon, 10 Dec 2018 19:37:09 -0500 Subject: [PATCH 12/31] docs(home): add devhelp note --- .../app/containers/home/home.component.html | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/ClientApp/app/containers/home/home.component.html b/ClientApp/app/containers/home/home.component.html index f60e6d64..c2eed1af 100644 --- a/ClientApp/app/containers/home/home.component.html +++ b/ClientApp/app/containers/home/home.component.html @@ -3,7 +3,7 @@ <blockquote> <strong>Enjoy the latest features from .NET Core & Angular 6.x!</strong> <br> For more info check the repo here: - <a href="https://github.com/MarkPieszak/aspnetcore-angular2-universal">AspNetCore-Angular-Universal repo</a> + <a href="https://github.com/MarkPieszak/aspnetcore-angular2-universal" target="_blank">AspNetCore-Angular-Universal repo</a> <br> <br> </blockquote> @@ -45,16 +45,30 @@ <h2>{{ 'HOME_ISSUES_TITLE' | translate }}</h2> <li> <strong>Issues with this Starter?</strong> <br>Please post an issue here: - <a href="https://github.com/MarkPieszak/aspnetcore-angular2-universal">AspNetCore-Angular2-Universal repo</a> + <a href="https://github.com/MarkPieszak/aspnetcore-angular2-universal" target="_blank">AspNetCore-Angular2-Universal repo</a> <br> <br> </li> <!--<li><strong>Issues with <u>Universal</u> itself?</strong> <br>Please post an issue here: <a href="https://github.com/angular/universal">Angular Universal repo</a></li>--> </ul> + + <h2><a href="http://www.devhelp.online/" target="_blank">DevHelp.Online</a></h2> + + <strong>Consulting | Development | Training | Workshops</strong> + <br><br> + Get your Team or Application up to speed by working with some of the leading industry experts in JavaScript & ASP.NET!<br> + <br> + <strong>Follow us on Twitter!</strong><br><br> + <a href="http://www.twitter.com/DevHelpOnline" target="_blank">@DevHelpOnline</a> | + <a href="http://www.twitter.com/MarkPieszak" target="_blank">@MarkPieszak</a> + <br> + <br> + </div> </div> + <div class="row"> <div class="col-lg-12"> From cc1e9b8ccb3001bcfe8a3ce44cd539096fde6363 Mon Sep 17 00:00:00 2001 From: Mark Pieszak <mpieszak84@gmail.com> Date: Wed, 19 Dec 2018 12:48:25 -0500 Subject: [PATCH 13/31] docs(readme): update ie11 webpack config info related to https://github.com/MarkPieszak/aspnetcore-angular-universal/issues/706 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 94ea42b6..f7666391 100644 --- a/README.md +++ b/README.md @@ -427,7 +427,7 @@ import * as $ from 'jquery'; ### How can I support IE9 through IE11? -To support IE9 through IE11 open the `polyfills.ts` file in the `polyfills` folder and uncomment out the 'import polyfills' as needed. +To support IE9 through IE11 open the `polyfills.ts` file in the `polyfills` folder and uncomment out the 'import polyfills' as needed. ALSO - make sure that your `webpack.config` and `webpack.config.vendor` change option of `UglifyJsPlugin` from `ecma: 6` to **`ecma: 5`**. ---- From 57273e12bed52c3a970c6cd7ef7071c766abe028 Mon Sep 17 00:00:00 2001 From: Peter Blazejewicz <peterblazejewicz@users.noreply.github.com> Date: Sun, 10 Feb 2019 14:59:41 +0100 Subject: [PATCH 14/31] feat(update client): update client dependencies (#708) This commit: - updates Angular from 6.* to most recent 7.2 version - updates all related client and tooling dependencies - removes dependencies that are no longer user (like Chai related ones) Thanks! --- .../components/navmenu/navmenu.component.html | 2 +- .../app/containers/home/home.component.html | 4 +- .../app/containers/home/home.component.ts | 2 +- README.md | 18 +-- Startup.cs | 2 +- package.json | 105 +++++++++--------- wwwroot/manifest.json | 4 +- 7 files changed, 67 insertions(+), 70 deletions(-) diff --git a/ClientApp/app/components/navmenu/navmenu.component.html b/ClientApp/app/components/navmenu/navmenu.component.html index fdef2c65..ba9ff74e 100644 --- a/ClientApp/app/components/navmenu/navmenu.component.html +++ b/ClientApp/app/components/navmenu/navmenu.component.html @@ -1,5 +1,5 @@ <nav class="navbar navbar-expand-lg"> - <a [routerLink]="['/home']" class='navbar-brand'>Angular 6 Universal & ASP.NET Core</a> + <a [routerLink]="['/home']" class='navbar-brand'>Angular 7 Universal & ASP.NET Core</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#mainNav" aria-controls="mainNav" aria-expanded="false" aria-label="Toggle navigation" (click)="collapseNavbar()"> <i class="fas fa-bars"></i> diff --git a/ClientApp/app/containers/home/home.component.html b/ClientApp/app/containers/home/home.component.html index c2eed1af..26160f60 100644 --- a/ClientApp/app/containers/home/home.component.html +++ b/ClientApp/app/containers/home/home.component.html @@ -1,7 +1,7 @@ <h1>{{ title }}</h1> <blockquote> - <strong>Enjoy the latest features from .NET Core & Angular 6.x!</strong> + <strong>Enjoy the latest features from .NET Core & Angular 7.x!</strong> <br> For more info check the repo here: <a href="https://github.com/MarkPieszak/aspnetcore-angular2-universal" target="_blank">AspNetCore-Angular-Universal repo</a> <br> @@ -14,7 +14,7 @@ <h2>{{ 'HOME_FEATURE_LIST_TITLE' | translate }} </h2> <ul> <li>ASP.NET Core 2.1 :: ( Visual Studio 2017 )</li> <li> - Angular 6.* front-end UI framework + Angular 7.* front-end UI framework <ul> <li>Angular **platform-server** (aka: Universal) - server-side rendering for SEO, deep-linking, and incredible performance.</li> <!--<li>HMR State Management - Don't lose your applications state during HMR!</li>--> diff --git a/ClientApp/app/containers/home/home.component.ts b/ClientApp/app/containers/home/home.component.ts index fc689f24..850f7b8f 100644 --- a/ClientApp/app/containers/home/home.component.ts +++ b/ClientApp/app/containers/home/home.component.ts @@ -7,7 +7,7 @@ import { TranslateService } from '@ngx-translate/core'; }) export class HomeComponent implements OnInit { title: string = - 'Angular 6.x Universal & ASP.NET Core 2.1 advanced starter-kit'; + 'Angular 7.x Universal & ASP.NET Core 2.1 advanced starter-kit'; // Use "constructor"s only for dependency injection constructor(public translate: TranslateService) {} diff --git a/README.md b/README.md index f7666391..0f61aadf 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,25 @@ -# ASP.NET Core 2.1 & Angular 6(+) Advanced Starter - PWA & Server-side prerendering (for Angular SEO)! +# ASP.NET Core 2.1 & Angular 7(+) Advanced Starter - PWA & Server-side prerendering (for Angular SEO)! ## By [DevHelp.Online](http://www.DevHelp.Online) -> Updated to the latest Angular 6.x +> Updated to the latest Angular 7.x <p align="center"> - <img src="http://github.com/docs/architecture.png" alt="ASP.NET Core 2.1 Angular 6+ Starter" title="ASP.NET Core 2.1 Angular 6+ Starter"> + <img src="http://github.com/docs/architecture.png" alt="ASP.NET Core 2.1 Angular 7+ Starter" title="ASP.NET Core 2.1 Angular 7+ Starter"> </p> -### Harness the power of Angular 6+, ASP.NET Core 2.1, now with SEO ! +### Harness the power of Angular 7+, ASP.NET Core 2.1, now with SEO ! Angular SEO in action: <p align="center"> - <img src="http://github.com/docs/angular2-seo.png" alt="ASP.NET Core Angular6 SEO" title="ASP.NET Core Angular6 SEO"> + <img src="http://github.com/docs/angular2-seo.png" alt="ASP.NET Core Angular7 SEO" title="ASP.NET Core Angular7 SEO"> </p> ### What is this repo? Live Demo here: http://aspnetcore-angular2-universal.azurewebsites.net This repository is maintained by [Angular](https://github.com/angular/angular) and is meant to be an advanced starter -for both ASP.NET Core 2.1 using Angular 6.0+, not only for the client-side, but to be rendered on the server for instant +for both ASP.NET Core 2.1 using Angular 7.0+, not only for the client-side, but to be rendered on the server for instant application paints (Note: If you don't need SSR [read here](#faq) on how to disable it). This is meant to be a Feature-Rich Starter application containing all of the latest technologies, best build systems available, and include many real-world examples and libraries needed in todays Single Page Applications (SPAs). @@ -57,7 +57,7 @@ This utilizes all the latest standards, no gulp, no bower, no typings, no manual - Swagger WebAPI documentation when running in development mode - SignalR Chat demo! (Thanks to [@hakonamatata](https://github.com/hakonamatata)) -- **Angular 6.0.0** : +- **Angular 7.0.0** : - PWA (Progressive Web App) - (Minimal) Angular-CLI integration - This is to be used mainly for Generating Components/Services/etc. @@ -304,7 +304,7 @@ Take a look at the `_Layout.cshtml` file for example, notice how we let .NET han <head> <base href="http://github.com/" /> <!-- Title will be the one you set in your Angular application --> - <title>@ViewData["Title"] - AspNET.Core Angular 6.0.0 (+) starter + @ViewData["Title"] - AspNET.Core Angular 7.0.0 (+) starter @@ -345,7 +345,7 @@ Well now, your Client-side Angular will take over, and you'll have a fully funct - This repository uses ASP.Net Core 2.1, which has a hard requirement on .NET Core Runtime 2.1 and .NET Core SDK 2.1. Please install these items from [here](https://blogs.msdn.microsoft.com/dotnet/2018/05/30/announcing-net-core-2-1/?WT.mc_id=blog-twitter-timheuer) -> When building components in Angular 6 there are a few things to keep in mind. +> When building components in Angular 7 there are a few things to keep in mind. - Make sure you provide Absolute URLs when calling any APIs. (The server can't understand relative paths, so `/api/whatever` will fail). diff --git a/Startup.cs b/Startup.cs index a1b0f70d..8d978fd4 100644 --- a/Startup.cs +++ b/Startup.cs @@ -42,7 +42,7 @@ public void ConfigureServices (IServiceCollection services) { // Register the Swagger generator, defining one or more Swagger documents services.AddSwaggerGen (c => { - c.SwaggerDoc ("v1", new Info { Title = "Angular 6.0 Universal & ASP.NET Core advanced starter-kit web API", Version = "v1" }); + c.SwaggerDoc ("v1", new Info { Title = "Angular 7.0 Universal & ASP.NET Core advanced starter-kit web API", Version = "v1" }); }); } diff --git a/package.json b/package.json index 56510a52..476524e2 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "angular6-aspnetcore-universal", + "name": "angular7-aspnetcore-universal", "author": { "name": "Mark Pieszak", "email": "hello@devhelp.online", @@ -22,81 +22,78 @@ "clean": "rimraf wwwroot/dist clientapp/dist" }, "dependencies": { - "@angular/animations": "^6.0.3", - "@angular/common": "^6.0.3", - "@angular/compiler": "^6.0.3", - "@angular/core": "^6.0.3", - "@angular/forms": "^6.0.3", - "@angular/http": "^6.0.3", - "@angular/platform-browser": "^6.0.3", - "@angular/platform-browser-dynamic": "^6.0.3", - "@angular/platform-server": "^6.0.3", - "@angular/router": "^6.0.3", + "@angular/animations": "^7.2.0", + "@angular/common": "^7.2.0", + "@angular/compiler": "^7.2.0", + "@angular/core": "^7.2.0", + "@angular/forms": "^7.2.0", + "@angular/http": "^7.2.0", + "@angular/platform-browser": "^7.2.0", + "@angular/platform-browser-dynamic": "^7.2.0", + "@angular/platform-server": "^7.2.0", + "@angular/router": "^7.2.0", "@nguniversal/aspnetcore-engine": "^7.0.2", "@nguniversal/common": "^7.0.2", - "@ngx-translate/core": "^10.0.2", - "@ngx-translate/http-loader": "^3.0.1", - "@types/node": "^10.1.2", + "@ngx-translate/core": "^11.0.1", + "@ngx-translate/http-loader": "^4.0.0", + "@types/node": "^10.12.18", "angular2-router-loader": "^0.3.5", "angular2-template-loader": "^0.6.2", "aspnet-prerendering": "^3.0.1", - "aspnet-webpack": "^2.0.3", - "awesome-typescript-loader": "^5.0.0", - "bootstrap": "^4.1.1", - "core-js": "^2.5.6", - "css": "^2.2.3", - "css-loader": "^0.28.11", - "event-source-polyfill": "^0.0.12", + "aspnet-webpack": "^3.0.0", + "awesome-typescript-loader": "^5.2.1", + "bootstrap": "^4.2.1", + "core-js": "^2.6.1", + "css": "^2.2.4", + "css-loader": "^2.1.0", + "event-source-polyfill": "^1.0.5", "expose-loader": "^0.7.5", - "file-loader": "^1.1.11", + "file-loader": "^3.0.1", "html-loader": "^0.5.5", "isomorphic-fetch": "^2.2.1", "jquery": "^3.3.1", "json-loader": "^0.5.7", - "moment": "2.22.1", - "ngx-bootstrap": "^3.0.0", + "moment": "^2.23.0", + "ngx-bootstrap": "^3.1.4", "node-sass": "^4.9.0", - "npm": "^6.1.0", - "preboot": "6.0.0-beta.4", - "raw-loader": "^0.5.1", - "rimraf": "^2.6.2", - "rxjs": "^6.2.0", - "sass-loader": "^7.0.1", - "style-loader": "^0.21.0", + "preboot": "^6.0.0-beta.6", + "raw-loader": "^1.0.0", + "rimraf": "^2.6.3", + "rxjs": "^6.3.3", + "sass-loader": "^7.1.0", + "style-loader": "^0.23.1", "to-string-loader": "^1.1.5", - "typescript": "~2.7.2", - "url-loader": "^1.0.1", - "webpack": "^4.9.1", - "webpack-hot-middleware": "^2.22.2", - "webpack-merge": "^4.1.2", + "url-loader": "^1.1.2", + "webpack": "^4.28.1", + "webpack-hot-middleware": "^2.24.3", + "webpack-merge": "^4.2.1", "zone.js": "^0.8.26" }, "devDependencies": { "@angular-devkit/build-angular": "~0.6.6", - "@angular/cli": "^6.0.5", - "@angular/compiler-cli": "6.0.3", - "@ngtools/webpack": "^6.0.5", - "@types/chai": "^4.1.3", - "@types/jasmine": "^2.8.7", - "chai": "^4.1.2", - "codelyzer": "^3.1.2", + "@angular/cli": "^7.2.0", + "@angular/compiler-cli": "^7.2.0", + "@ngtools/webpack": "^7.1.4", + "@types/jasmine": "^3.3.5", + "codelyzer": "^4.5.0", "istanbul-instrumenter-loader": "^3.0.1", - "jasmine-core": "^2.5.2", - "karma": "^1.7.1", - "karma-chai": "^0.1.0", + "jasmine-core": "^3.3.0", + "jasmine-spec-reporter": "^4.2.1", + "karma": "^3.1.4", "karma-chrome-launcher": "^2.2.0", - "karma-coverage": "^1.1.1", - "karma-jasmine": "^1.1.2", + "karma-coverage": "^1.1.2", + "karma-jasmine": "^2.0.1", "karma-mocha-reporter": "^2.2.5", "karma-phantomjs-launcher": "^1.0.4", "karma-remap-coverage": "^0.1.5", "karma-sourcemap-loader": "^0.3.7", - "karma-webpack": "^2.0.4", - "mini-css-extract-plugin": "^0.4.0", - "tslint": "^5.10.0", - "uglifyjs-webpack-plugin": "^1.2.5", - "webpack-bundle-analyzer": "^2.13.1", - "webpack-cli": "^3.1.2" + "karma-webpack": "^3.0.5", + "mini-css-extract-plugin": "^0.5.0", + "tslint": "^5.12.0", + "typescript": "^3.1.3", + "uglifyjs-webpack-plugin": "^2.1.1", + "webpack-bundle-analyzer": "^3.0.3", + "webpack-cli": "^3.2.1" }, "license": "MIT", "repository": { diff --git a/wwwroot/manifest.json b/wwwroot/manifest.json index 1f247be0..ca1c07d8 100644 --- a/wwwroot/manifest.json +++ b/wwwroot/manifest.json @@ -1,8 +1,8 @@ { - "name": "ASP.NET Core 2.1 & Angular 6(+) Advanced Starter", + "name": "ASP.NET Core 2.1 & Angular 7(+) Advanced Starter", "short_name": "aspnetcore-angular2-universal", "description": - "ASP.NET Core 2.1 & Angular 6(+) Advanced Starter - with Server-side prerendering (for Angular SEO)!", + "ASP.NET Core 2.1 & Angular 7(+) Advanced Starter - with Server-side prerendering (for Angular SEO)!", "icons": [ { "src": "/images/icon192x192.png", From 49cfeba5dbdddeed478b5ec29c24defb2408a98c Mon Sep 17 00:00:00 2001 From: Peter Blazejewicz Date: Sun, 10 Feb 2019 15:02:59 +0100 Subject: [PATCH 15/31] feat(webpack): migrate to terser from uglifiy This fixes webpack build in production mode See @webpack/webpack#7908 Thanks! --- README.md | 2 +- package.json | 1 + webpack.config.js | 28 +++++++++++++--------------- webpack.config.vendor.js | 22 +++++++++++----------- 4 files changed, 26 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 0f61aadf..42fa2e49 100644 --- a/README.md +++ b/README.md @@ -427,7 +427,7 @@ import * as $ from 'jquery'; ### How can I support IE9 through IE11? -To support IE9 through IE11 open the `polyfills.ts` file in the `polyfills` folder and uncomment out the 'import polyfills' as needed. ALSO - make sure that your `webpack.config` and `webpack.config.vendor` change option of `UglifyJsPlugin` from `ecma: 6` to **`ecma: 5`**. +To support IE9 through IE11 open the `polyfills.ts` file in the `polyfills` folder and uncomment out the 'import polyfills' as needed. ALSO - make sure that your `webpack.config` and `webpack.config.vendor` change option of `TerserPlugin` from `ecma: 6` to **`ecma: 5`**. ---- diff --git a/package.json b/package.json index 476524e2..4978e1f8 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ "karma-sourcemap-loader": "^0.3.7", "karma-webpack": "^3.0.5", "mini-css-extract-plugin": "^0.5.0", + "terser-webpack-plugin": "^1.2.1", "tslint": "^5.12.0", "typescript": "^3.1.3", "uglifyjs-webpack-plugin": "^2.1.1", diff --git a/webpack.config.js b/webpack.config.js index 3ea5c207..249c4f59 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -15,7 +15,7 @@ const AngularCompilerPlugin = require('@ngtools/webpack').AngularCompilerPlugin; const CheckerPlugin = require('awesome-typescript-loader').CheckerPlugin; const BundleAnalyzerPlugin = require('webpack-bundle-analyzer') .BundleAnalyzerPlugin; -const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); +const TerserPlugin = require('terser-webpack-plugin'); const { sharedModuleRules } = require('./webpack.additions'); @@ -118,19 +118,17 @@ module.exports = env => { isDevBuild ? [] : [ - // we specify a custom UglifyJsPlugin here to get source maps in production - new UglifyJsPlugin({ - cache: true, - parallel: true, - uglifyOptions: { - compress: false, + // we specify a custom TerserPlugin here to get source maps in production + new TerserPlugin({ + sourceMap: true, + terserOptions: { + compress: true, ecma: 6, mangle: true, keep_classnames: true, - keep_fnames: true + keep_fnames: true, }, - sourceMap: true - }) + }), ] ) } @@ -196,18 +194,18 @@ module.exports = env => { isDevBuild ? [] : [ - // we specify a custom UglifyJsPlugin here to get source maps in production - new UglifyJsPlugin({ + // we specify a custom TerserPlugin here to get source maps in production + new TerserPlugin({ cache: true, parallel: true, - uglifyOptions: { + sourceMap: true, + terserOptions: { compress: false, ecma: 6, mangle: true, keep_classnames: true, - keep_fnames: true + keep_fnames: true, }, - sourceMap: true }) ] ) diff --git a/webpack.config.vendor.js b/webpack.config.vendor.js index 6b67e525..775910fb 100644 --- a/webpack.config.vendor.js +++ b/webpack.config.vendor.js @@ -2,7 +2,7 @@ const path = require('path'); const webpack = require('webpack'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const merge = require('webpack-merge'); -const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); +const TerserPlugin = require('terser-webpack-plugin'); const treeShakableModules = [ '@angular/animations', '@angular/common', @@ -95,18 +95,18 @@ module.exports = (env) => { ]), optimization: { minimizer: [].concat(isDevBuild ? [] : [ - // we specify a custom UglifyJsPlugin here to get source maps in production - new UglifyJsPlugin({ + // we specify a custom TerserPlugin here to get source maps in production + new TerserPlugin({ cache: true, parallel: true, - uglifyOptions: { + sourceMap: true, + terserOptions: { compress: false, ecma: 6, mangle: true, keep_classnames: true, - keep_fnames: true + keep_fnames: true, }, - sourceMap: true }) ]) } @@ -141,18 +141,18 @@ module.exports = (env) => { ].concat(isDevBuild ? [] : []), optimization: { minimizer: [].concat(isDevBuild ? [] : [ - // we specify a custom UglifyJsPlugin here to get source maps in production - new UglifyJsPlugin({ + // we specify a custom TerserPlugin here to get source maps in production + new TerserPlugin({ cache: true, parallel: true, - uglifyOptions: { + sourceMap: true, + terserOptions: { compress: false, ecma: 6, mangle: true, keep_classnames: true, - keep_fnames: true + keep_fnames: true, }, - sourceMap: true }) ]) } From 5e43331328e5b430538b00d092d733bbaed4f99a Mon Sep 17 00:00:00 2001 From: Peter Blazejewicz Date: Sun, 10 Feb 2019 15:03:25 +0100 Subject: [PATCH 16/31] feat(pwa): update icons, favicons and manifest (#710) - most common sizes added - manifest entries updated - favicon ships the same content as manifest icons Thanks! --- wwwroot/favicon.ico | Bin 85432 -> 10990 bytes wwwroot/images/icon144x144.png | Bin 0 -> 6192 bytes wwwroot/images/icon192x192.png | Bin 9823 -> 8346 bytes wwwroot/images/icon256x256.png | Bin 0 -> 11350 bytes wwwroot/images/icon36x36.png | Bin 0 -> 1739 bytes wwwroot/images/icon384x384.png | Bin 0 -> 20299 bytes wwwroot/images/icon48x48.png | Bin 0 -> 2228 bytes wwwroot/images/icon512x512.png | Bin 29105 -> 29204 bytes wwwroot/images/icon72x72.png | Bin 0 -> 3087 bytes wwwroot/images/icon96x96.png | Bin 0 -> 4050 bytes wwwroot/manifest.json | 37 +++++++++++++++++++++++++++++---- 11 files changed, 33 insertions(+), 4 deletions(-) create mode 100644 wwwroot/images/icon144x144.png create mode 100644 wwwroot/images/icon256x256.png create mode 100644 wwwroot/images/icon36x36.png create mode 100644 wwwroot/images/icon384x384.png create mode 100644 wwwroot/images/icon48x48.png create mode 100644 wwwroot/images/icon72x72.png create mode 100644 wwwroot/images/icon96x96.png diff --git a/wwwroot/favicon.ico b/wwwroot/favicon.ico index 6884543f9617d81e56ccd478989bec1ece300e3a..23015bba41706f1a8354ffc675cf328d6b9167dd 100644 GIT binary patch literal 10990 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 HVFdmUW_yrt diff --git a/wwwroot/images/icon144x144.png b/wwwroot/images/icon144x144.png new file mode 100644 index 0000000000000000000000000000000000000000..b53f37918280fa8c175bbe6096eb1c59324cd6af GIT binary patch literal 6192 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 literal 0 HcmV?d00001 diff --git a/wwwroot/images/icon192x192.png b/wwwroot/images/icon192x192.png index 7f020c3fae22ba0892bf3d6f6bfb919fceed35c9..6f4b36a1cf3a1656a11fdc0504b11ce72c338b48 100644 GIT binary patch delta 8279 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 E4;q)o-~a#s diff --git a/wwwroot/images/icon256x256.png b/wwwroot/images/icon256x256.png new file mode 100644 index 0000000000000000000000000000000000000000..66c420db1a6b17b9817f324ad935517d23954ae2 GIT binary patch literal 11350 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 literal 0 HcmV?d00001 diff --git a/wwwroot/images/icon36x36.png b/wwwroot/images/icon36x36.png new file mode 100644 index 0000000000000000000000000000000000000000..974921f1348df031b32d58461c6ceb67f6c836b2 GIT binary patch literal 1739 zcmV;+1~mDJP)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 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 delta 111 zcmbR8gmL3z#t9W%kJ#9`#5AuJmBwytx=|2rp=)5KYhWH?XkcYzWMyKkYhYq!V9+O! x#K*wEpjzS@QIe8al4_NkpOTqY$zWt)h^E01q9Nnb>DfRH44$rjF6*2UngEPYAbbD- diff --git a/wwwroot/images/icon72x72.png b/wwwroot/images/icon72x72.png new file mode 100644 index 0000000000000000000000000000000000000000..77fde9ebd279e4af01ff2030881cf9c4f8ae1c91 GIT binary patch literal 3087 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 literal 0 HcmV?d00001 diff --git a/wwwroot/manifest.json b/wwwroot/manifest.json index ca1c07d8..b386fa18 100644 --- a/wwwroot/manifest.json +++ b/wwwroot/manifest.json @@ -5,12 +5,41 @@ "ASP.NET Core 2.1 & Angular 7(+) Advanced Starter - with Server-side prerendering (for Angular SEO)!", "icons": [ { + "src": "/images/icon36x36.png", + "sizes": "36x36", + "type": "image/png" + }, { + "src": "/images/icon48x48.png", + "sizes": "48x48", + "type": "image/png" + }, { + "src": "/images/icon72x72.png", + "sizes": "72x72", + "type": "image/png" + }, { + "src": "/images/icon96x96.png", + "sizes": "96x96", + "type": "image/png" + }, { + "src": "/images/icon144x144.png", + "sizes": "144x144", + "type": "image/png" + }, { "src": "/images/icon192x192.png", - "sizes": "192x192" - }, - { + "sizes": "192x192", + "type": "image/png" + }, { + "src": "/images/icon256x256.png", + "sizes": "256x256", + "type": "image/png" + }, { + "src": "/images/icon384x384.png", + "sizes": "384x384", + "type": "image/png" + }, { "src": "/images/icon512x512.png", - "sizes": "512x512" + "sizes": "512x512", + "type": "image/png" } ], "background_color": "#3E4EB8", From e184ce9af8688dba3440a4d81cb5431640a52906 Mon Sep 17 00:00:00 2001 From: Peter Blazejewicz Date: Sun, 10 Feb 2019 15:03:43 +0100 Subject: [PATCH 17/31] feat(charset): move declaration to top (#711) Just to follow established routes: https://blogs.msdn.microsoft.com/ieinternals/2011/07/18/best-practice-get-your-head-in-order/ Thanks! --- Views/Shared/_Layout.cshtml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Views/Shared/_Layout.cshtml b/Views/Shared/_Layout.cshtml index eeebe090..e749bc3b 100644 --- a/Views/Shared/_Layout.cshtml +++ b/Views/Shared/_Layout.cshtml @@ -1,10 +1,10 @@ + @ViewData["Title"] - - + @Html.Raw(ViewData["Meta"]) @Html.Raw(ViewData["Links"]) From 8b0ec8ee3147e77f84f1e12509e8e5c0e9acc47f Mon Sep 17 00:00:00 2001 From: Mark Pieszak Date: Sun, 10 Feb 2019 10:34:00 -0500 Subject: [PATCH 18/31] chore(preboot): pin to beta4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4978e1f8..81f03d1c 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "moment": "^2.23.0", "ngx-bootstrap": "^3.1.4", "node-sass": "^4.9.0", - "preboot": "^6.0.0-beta.6", + "preboot": "^6.0.0-beta.4", "raw-loader": "^1.0.0", "rimraf": "^2.6.3", "rxjs": "^6.3.3", From b46107696e340511743e8a72f9ba694cc41fa8d4 Mon Sep 17 00:00:00 2001 From: Peter Blazejewicz Date: Thu, 28 Feb 2019 16:02:54 +0100 Subject: [PATCH 19/31] Update dependencies to be aligned with NG 7.2.0. Fixes #716 (#718) closes #716 This commit make changes in dependencies entries in package.json to be more aligned to package.json version generated by @angular/cli 7.2.0. This fixes the issue with outcome of script like build:prod Thanks! ```bash npm run clean:install > angular7-aspnetcore-universal@1.0.0-rc4 clean:install /Users/piotrblazejewicz/git/aspnetcore-angular2-universal > npm run clean && rimraf ./node_modules ./bin ./obj ./package-lock.json && dotnet restore Asp2017.csproj && npm i > angular7-aspnetcore-universal@1.0.0-rc4 clean /Users/piotrblazejewicz/git/aspnetcore-angular2-universal > rimraf wwwroot/dist clientapp/dist Restoring packages for /Users/piotrblazejewicz/git/aspnetcore-angular2-universal/Asp2017.csproj... Generating MSBuild file /Users/piotrblazejewicz/git/aspnetcore-angular2-universal/obj/Asp2017.csproj.nuget.g.props. Generating MSBuild file /Users/piotrblazejewicz/git/aspnetcore-angular2-universal/obj/Asp2017.csproj.nuget.g.targets. Restore completed in 1.95 sec for /Users/piotrblazejewicz/git/aspnetcore-angular2-universal/Asp2017.csproj. npm WARN deprecated circular-json@0.5.9: CircularJSON is in maintenance only, flatted is its successor. > fsevents@1.2.7 install /Users/piotrblazejewicz/git/aspnetcore-angular2-universal/node_modules/fsevents > node install node-pre-gyp WARN Using request for node-pre-gyp https download [fsevents] Success: "/Users/piotrblazejewicz/git/aspnetcore-angular2-universal/node_modules/fsevents/lib/binding/Release/node-v59-darwin-x64/fse.node" is installed via remote > node-sass@4.11.0 install /Users/piotrblazejewicz/git/aspnetcore-angular2-universal/node_modules/node-sass > node scripts/install.js Cached binary found at /Users/piotrblazejewicz/.npm/node-sass/4.11.0/darwin-x64-59_binding.node > phantomjs-prebuilt@2.1.16 install /Users/piotrblazejewicz/git/aspnetcore-angular2-universal/node_modules/phantomjs-prebuilt > node install.js Considering PhantomJS found at /usr/local/bin/phantomjs Found PhantomJS at /usr/local/bin/phantomjs ...verifying Writing location.js file PhantomJS is already installed on PATH at /usr/local/bin/phantomjs > node-sass@4.11.0 postinstall /Users/piotrblazejewicz/git/aspnetcore-angular2-universal/node_modules/node-sass > node scripts/build.js Binary found at /Users/piotrblazejewicz/git/aspnetcore-angular2-universal/node_modules/node-sass/vendor/darwin-x64-59/binding.node Testing binary Binary is fine npm notice created a lockfile as package-lock.json. You should commit this file. npm WARN bootstrap@4.3.1 requires a peer of popper.js@^1.14.7 but none is installed. You must install peer dependencies yourself. npm WARN karma-webpack@3.0.5 requires a peer of webpack@^2.0.0 || ^3.0.0 but none is installed. You must install peer dependencies yourself. added 1328 packages from 1266 contributors and audited 52380 packages in 40.119s found 1 low severity vulnerability run `npm audit fix` to fix them, or `npm audit` for details ``` ```bash npm run build:prod > angular7-aspnetcore-universal@1.0.0-rc4 build:prod /Users/piotrblazejewicz/git/aspnetcore-angular2-universal > npm run clean && npm run build:vendor -- --env.prod && npm run build:webpack -- --env.prod > angular7-aspnetcore-universal@1.0.0-rc4 clean /Users/piotrblazejewicz/git/aspnetcore-angular2-universal > rimraf wwwroot/dist clientapp/dist > angular7-aspnetcore-universal@1.0.0-rc4 build:vendor /Users/piotrblazejewicz/git/aspnetcore-angular2-universal > webpack --config webpack.config.vendor.js --progress --color "--env.prod" env = {"prod":true} [1] Hash: 0cd0fe463be445b08132ce687d8bdad25e6a9daf Version: webpack 4.29.5 Child Hash: 0cd0fe463be445b08132 Time: 6789ms Built at: 2019-02-26 18:32:15 Asset Size Chunks Chunk Names vendor.js 112 KiB 0 [emitted] vendor Entrypoint vendor = vendor.js Child Hash: ce687d8bdad25e6a9daf Time: 23102ms Built at: 2019-02-26 18:32:31 Asset Size Chunks Chunk Names vendor.js 2 MiB 0 [emitted] vendor Entrypoint vendor = vendor.js WARNING in ./node_modules/@angular/core/bundles/core.umd.js 18394:19-40 System.import() is deprecated and will be removed soon. Use import() instead. For more info visit https://webpack.js.org/guides/code-splitting/ @ dll vendor vendor[3] WARNING in ./node_modules/@angular/core/bundles/core.umd.js 18406:19-106 System.import() is deprecated and will be removed soon. Use import() instead. For more info visit https://webpack.js.org/guides/code-splitting/ @ dll vendor vendor[3] > angular7-aspnetcore-universal@1.0.0-rc4 build:webpack /Users/piotrblazejewicz/git/aspnetcore-angular2-universal > webpack --progress --color "--env.prod" [1] Hash: 283e428aa756b4bf9dc86d7e8bceb2c03b314740 Version: webpack 4.29.5 Child Hash: 283e428aa756b4bf9dc8 Time: 48150ms Built at: 2019-02-26 18:33:22 Asset Size Chunks Chunk Names 1.js 1.08 KiB 1 [emitted] main-client.js 1.78 MiB 0 [emitted] [big] main-client Entrypoint main-client [big] = main-client.js WARNING in ./node_modules/@angular/core/fesm5/core.js 18349:15-36 System.import() is deprecated and will be removed soon. Use import() instead. For more info visit https://webpack.js.org/guides/code-splitting/ @ ./ClientApp/boot.browser.ts 2:0-47 13:4-18 WARNING in ./node_modules/@angular/core/fesm5/core.js 18361:15-102 System.import() is deprecated and will be removed soon. Use import() instead. For more info visit https://webpack.js.org/guides/code-splitting/ @ ./ClientApp/boot.browser.ts 2:0-47 13:4-18 WARNING in asset size limit: The following asset(s) exceed the recommended size limit (244 KiB). This can impact web performance. Assets: main-client.js (1.78 MiB) WARNING in entrypoint size limit: The following entrypoint(s) combined asset size exceeds the recommended limit (244 KiB). This can impact web performance. Entrypoints: main-client (1.78 MiB) main-client.js Child Hash: 6d7e8bceb2c03b314740 Time: 49251ms Built at: 2019-02-26 18:33:24 Asset Size Chunks Chunk Names 1.js 1.21 KiB 1 [emitted] main-server.js 2.96 MiB 0 [emitted] main-server Entrypoint main-server = main-server.js WARNING in ./node_modules/@angular/core/fesm5/core.js 18349:15-36 System.import() is deprecated and will be removed soon. Use import() instead. For more info visit https://webpack.js.org/guides/code-splitting/ @ ./ClientApp/boot.server.PRODUCTION.ts 3:0-47 8:0-14 WARNING in ./node_modules/@angular/core/fesm5/core.js 18361:15-102 System.import() is deprecated and will be removed soon. Use import() instead. For more info visit https://webpack.js.org/guides/code-splitting/ @ ./ClientApp/boot.server.PRODUCTION.ts 3:0-47 8:0-14 ``` ```bash npm run build:dev > angular7-aspnetcore-universal@1.0.0-rc4 build:dev /Users/piotrblazejewicz/git/aspnetcore-angular2-universal > npm run build:vendor && npm run build:webpack > angular7-aspnetcore-universal@1.0.0-rc4 build:vendor /Users/piotrblazejewicz/git/aspnetcore-angular2-universal > webpack --config webpack.config.vendor.js --progress --color env = undefined [1] Hash: 3a685f0209a95b535a9f17a6a40f1c8908affc70 Version: webpack 4.29.5 Child Hash: 3a685f0209a95b535a9f Time: 6393ms Built at: 2019-02-26 18:31:26 Asset Size Chunks Chunk Names vendor.js 6.94 MiB vendor [emitted] vendor Entrypoint vendor = vendor.js WARNING in ./node_modules/@angular/core/fesm5/core.js 18349:15-36 System.import() is deprecated and will be removed soon. Use import() instead. For more info visit https://webpack.js.org/guides/code-splitting/ @ dll vendor vendor[3] WARNING in ./node_modules/@angular/core/fesm5/core.js 18361:15-102 System.import() is deprecated and will be removed soon. Use import() instead. For more info visit https://webpack.js.org/guides/code-splitting/ @ dll vendor vendor[3] Child Hash: 17a6a40f1c8908affc70 Time: 6359ms Built at: 2019-02-26 18:31:26 Asset Size Chunks Chunk Names vendor.js 7.05 MiB vendor [emitted] vendor Entrypoint vendor = vendor.js WARNING in ./node_modules/@angular/core/bundles/core.umd.js 18394:19-40 System.import() is deprecated and will be removed soon. Use import() instead. For more info visit https://webpack.js.org/guides/code-splitting/ @ dll vendor vendor[3] WARNING in ./node_modules/@angular/core/bundles/core.umd.js 18406:19-106 System.import() is deprecated and will be removed soon. Use import() instead. For more info visit https://webpack.js.org/guides/code-splitting/ @ dll vendor vendor[3] > angular7-aspnetcore-universal@1.0.0-rc4 build:webpack /Users/piotrblazejewicz/git/aspnetcore-angular2-universal > webpack --progress --color [1] Hash: b4d33732336411f4f0263367da7fc158b6733f0d Version: webpack 4.29.5 Child Hash: b4d33732336411f4f026 Time: 21055ms Built at: 2019-02-26 18:31:50 Asset Size Chunks Chunk Names 0.js 8.26 KiB 0 [emitted] 0.js.map 81 bytes 0 [emitted] main-client.js 4.07 MiB main-client [emitted] main-client main-client.js.map 7.96 KiB main-client [emitted] main-client Entrypoint main-client = main-client.js main-client.js.map Child Hash: 3367da7fc158b6733f0d Time: 22992ms Built at: 2019-02-26 18:31:52 Asset Size Chunks Chunk Names 0.js 8.2 KiB 0 [emitted] main-server.js 20.7 MiB main-server [emitted] main-server Entrypoint main-server = main-server.js WARNING in ./node_modules/@angular/core/fesm5/core.js 18349:15-36 System.import() is deprecated and will be removed soon. Use import() instead. For more info visit https://webpack.js.org/guides/code-splitting/ @ ./ClientApp/boot.server.ts 3:0-47 8:0-14 WARNING in ./node_modules/@angular/core/fesm5/core.js 18361:15-102 System.import() is deprecated and will be removed soon. Use import() instead. For more info visit https://webpack.js.org/guides/code-splitting/ @ ./ClientApp/boot.server.ts 3:0-47 8:0-14 ``` --- package.json | 76 ++++++++++++++++++++++++++-------------------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/package.json b/package.json index 81f03d1c..995977bb 100644 --- a/package.json +++ b/package.json @@ -22,28 +22,28 @@ "clean": "rimraf wwwroot/dist clientapp/dist" }, "dependencies": { - "@angular/animations": "^7.2.0", - "@angular/common": "^7.2.0", - "@angular/compiler": "^7.2.0", - "@angular/core": "^7.2.0", - "@angular/forms": "^7.2.0", - "@angular/http": "^7.2.0", - "@angular/platform-browser": "^7.2.0", - "@angular/platform-browser-dynamic": "^7.2.0", - "@angular/platform-server": "^7.2.0", - "@angular/router": "^7.2.0", - "@nguniversal/aspnetcore-engine": "^7.0.2", - "@nguniversal/common": "^7.0.2", + "@angular/animations": "~7.2.0", + "@angular/common": "~7.2.0", + "@angular/compiler": "~7.2.0", + "@angular/core": "~7.2.0", + "@angular/forms": "~7.2.0", + "@angular/http": "~7.2.0", + "@angular/platform-browser": "~7.2.0", + "@angular/platform-browser-dynamic": "~7.2.0", + "@angular/platform-server": "~7.2.0", + "@angular/router": "~7.2.0", + "@nguniversal/aspnetcore-engine": "^7.1.0", + "@nguniversal/common": "^7.1.0", "@ngx-translate/core": "^11.0.1", "@ngx-translate/http-loader": "^4.0.0", - "@types/node": "^10.12.18", + "@types/node": "^11.9.5", "angular2-router-loader": "^0.3.5", "angular2-template-loader": "^0.6.2", "aspnet-prerendering": "^3.0.1", "aspnet-webpack": "^3.0.0", "awesome-typescript-loader": "^5.2.1", - "bootstrap": "^4.2.1", - "core-js": "^2.6.1", + "bootstrap": "^4.3.1", + "core-js": "^2.6.5", "css": "^2.2.4", "css-loader": "^2.1.0", "event-source-polyfill": "^1.0.5", @@ -53,48 +53,48 @@ "isomorphic-fetch": "^2.2.1", "jquery": "^3.3.1", "json-loader": "^0.5.7", - "moment": "^2.23.0", - "ngx-bootstrap": "^3.1.4", - "node-sass": "^4.9.0", - "preboot": "^6.0.0-beta.4", + "moment": "^2.24.0", + "ngx-bootstrap": "^3.2.0", + "node-sass": "^4.11.0", + "preboot": "^7.0.0", "raw-loader": "^1.0.0", "rimraf": "^2.6.3", - "rxjs": "^6.3.3", + "rxjs": "~6.4.0", "sass-loader": "^7.1.0", "style-loader": "^0.23.1", "to-string-loader": "^1.1.5", "url-loader": "^1.1.2", - "webpack": "^4.28.1", + "webpack": "^4.29.5", "webpack-hot-middleware": "^2.24.3", "webpack-merge": "^4.2.1", - "zone.js": "^0.8.26" + "zone.js": "^0.8.29" }, "devDependencies": { - "@angular-devkit/build-angular": "~0.6.6", - "@angular/cli": "^7.2.0", - "@angular/compiler-cli": "^7.2.0", - "@ngtools/webpack": "^7.1.4", - "@types/jasmine": "^3.3.5", - "codelyzer": "^4.5.0", + "@angular-devkit/build-angular": "~0.13.3", + "@angular/cli": "~7.3.3", + "@angular/compiler-cli": "~7.2.0", + "@ngtools/webpack": "~7.3.3", + "@types/jasmine": "~2.8.8", + "codelyzer": "~4.5.0", "istanbul-instrumenter-loader": "^3.0.1", "jasmine-core": "^3.3.0", "jasmine-spec-reporter": "^4.2.1", - "karma": "^3.1.4", - "karma-chrome-launcher": "^2.2.0", - "karma-coverage": "^1.1.2", - "karma-jasmine": "^2.0.1", + "karma": "~4.0.0", + "karma-chrome-launcher": "~2.2.0", + "karma-coverage": "~1.1.2", + "karma-jasmine": "~2.0.1", "karma-mocha-reporter": "^2.2.5", "karma-phantomjs-launcher": "^1.0.4", "karma-remap-coverage": "^0.1.5", "karma-sourcemap-loader": "^0.3.7", "karma-webpack": "^3.0.5", "mini-css-extract-plugin": "^0.5.0", - "terser-webpack-plugin": "^1.2.1", - "tslint": "^5.12.0", - "typescript": "^3.1.3", - "uglifyjs-webpack-plugin": "^2.1.1", - "webpack-bundle-analyzer": "^3.0.3", - "webpack-cli": "^3.2.1" + "terser-webpack-plugin": "^1.2.3", + "tslint": "~5.11.0", + "typescript": "~3.2.2", + "uglifyjs-webpack-plugin": "^2.1.2", + "webpack-bundle-analyzer": "^3.0.4", + "webpack-cli": "^3.2.3" }, "license": "MIT", "repository": { From ec6b2f32bbaedb78eeaba9e4685a073aaa232ae2 Mon Sep 17 00:00:00 2001 From: Mark Pieszak Date: Mon, 11 Mar 2019 08:56:48 -0400 Subject: [PATCH 20/31] docs(readme): update --- README.md | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 42fa2e49..77d58eee 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ASP.NET Core 2.1 & Angular 7(+) Advanced Starter - PWA & Server-side prerendering (for Angular SEO)! -## By [DevHelp.Online](http://www.DevHelp.Online) +## By [Trilon.io](https://Trilon.io) > Updated to the latest Angular 7.x @@ -18,7 +18,7 @@ Angular SEO in action: ### What is this repo? Live Demo here: http://aspnetcore-angular2-universal.azurewebsites.net -This repository is maintained by [Angular](https://github.com/angular/angular) and is meant to be an advanced starter +This repository is maintained by [Trilon.io](https://Trilon.io) and the [Angular](https://github.com/angular/angular) Universal team and is meant to be an advanced starter for both ASP.NET Core 2.1 using Angular 7.0+, not only for the client-side, but to be rendered on the server for instant application paints (Note: If you don't need SSR [read here](#faq) on how to disable it). @@ -458,7 +458,7 @@ Nothing's ever perfect, but please let me know by creating an issue (make sure t [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](/LICENSE) -Copyright (c) 2016-2018 [Mark Pieszak](https://github.com/MarkPieszak) +Copyright (c) 2016-2019 [Mark Pieszak](https://github.com/MarkPieszak) [![Twitter Follow](https://img.shields.io/twitter/follow/MarkPieszak.svg?style=social)](https://twitter.com/MarkPieszak) @@ -466,16 +466,14 @@ Copyright (c) 2016-2018 [Mark Pieszak](https://github.com/MarkPieszak) # DevHelp.Online - Angular & ASP.NET - Consulting | Training | Development -Check out **[www.DevHelp.Online](http://DevHelp.Online)** for more info! Twitter [@DevHelpOnline](http://www.twitter.com/DevHelpOnline) +Check out **[Trilon.io](https://Trilon.io)** for more info! Twitter [@Trilon_io](http://www.twitter.com/Trilon_io) -Contact us at , and let's talk about your projects needs. +Contact us at , and let's talk about your projects needs.

- DevHelp.Online - Angular ASPNET JavaScript Consulting Development and Training + Trilon.io - Angular Universal, NestJS, JavaScript Application Consulting Development and Training

----- - -## Follow me online: +## Follow Trilon online: -Twitter: [@MarkPieszak](http://twitter.com/MarkPieszak) | Medium: [@MarkPieszak](https://medium.com/@MarkPieszak) +Twitter: [@Trilon_io](http://twitter.com/Trilon_io) From 1de9303af83ccd6aa8d7066fc886d3925e0858bb Mon Sep 17 00:00:00 2001 From: Mark Pieszak Date: Mon, 11 Mar 2019 09:24:32 -0400 Subject: [PATCH 21/31] docs: update --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 77d58eee..e6ed1417 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ This utilizes all the latest standards, no gulp, no bower, no typings, no manual * [FAQ](#faq---also-check-out-the-faq-issues-label-and-the-how-to-issues-label) * [Special Thanks](#special-thanks) * [License](#license) -* [Consulting & Training](#devhelponline---angular--aspnet---consulting--training--development) +* [Trilon - Consulting & Training](#trilon---angular--aspnet---consulting--training--development) --- @@ -464,7 +464,7 @@ Copyright (c) 2016-2019 [Mark Pieszak](https://github.com/MarkPieszak) ---- -# DevHelp.Online - Angular & ASP.NET - Consulting | Training | Development +# Trilon - Angular & ASP.NET - Consulting | Training | Development Check out **[Trilon.io](https://Trilon.io)** for more info! Twitter [@Trilon_io](http://www.twitter.com/Trilon_io) From c8dac96c2cad48e574810b3d4f8c8a7100e458cb Mon Sep 17 00:00:00 2001 From: Mark Pieszak Date: Tue, 12 Mar 2019 15:45:06 -0400 Subject: [PATCH 22/31] docs: update trilon info --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 995977bb..99ba5443 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { "name": "angular7-aspnetcore-universal", "author": { - "name": "Mark Pieszak", - "email": "hello@devhelp.online", - "url": "http://devhelp.online/" + "name": "Mark Pieszak | Trilon Consulting", + "email": "hello@trilon.io", + "url": "https://trilon.io/" }, "version": "1.0.0-rc4", "scripts": { From 2b9154407a8d19491640484926456356b7229676 Mon Sep 17 00:00:00 2001 From: Mark Pieszak Date: Tue, 12 Mar 2019 15:47:21 -0400 Subject: [PATCH 23/31] chore: trilon info --- ClientApp/app/containers/home/home.component.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ClientApp/app/containers/home/home.component.html b/ClientApp/app/containers/home/home.component.html index 26160f60..c3a92f5c 100644 --- a/ClientApp/app/containers/home/home.component.html +++ b/ClientApp/app/containers/home/home.component.html @@ -3,7 +3,7 @@
Enjoy the latest features from .NET Core & Angular 7.x!
For more info check the repo here: - AspNetCore-Angular-Universal repo + AspNetCore-Angular-Universal repo

@@ -52,14 +52,14 @@

{{ 'HOME_ISSUES_TITLE' | translate }}

-

DevHelp.Online

+

Trilon Consulting - Trilon.io

Consulting | Development | Training | Workshops

- Get your Team or Application up to speed by working with some of the leading industry experts in JavaScript & ASP.NET!
+ Get your Team or Application up to speed by working with some of the leading industry experts in JavaScript, Node / NestJS, & ASP.NET!

Follow us on Twitter!

- @DevHelpOnline | + @trilon_io | @MarkPieszak

From 83b0301b1db1ed34e1d16d8ef9fa4750c8b970d8 Mon Sep 17 00:00:00 2001 From: Mark Pieszak Date: Sun, 17 Mar 2019 11:55:39 -0400 Subject: [PATCH 24/31] docs: update --- README.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e6ed1417..b0b14e9f 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,15 @@ # ASP.NET Core 2.1 & Angular 7(+) Advanced Starter - PWA & Server-side prerendering (for Angular SEO)! -## By [Trilon.io](https://Trilon.io) +## Made with :heart: by [Trilon.io](https://Trilon.io) +

+ + Trilon.io - Angular Universal, NestJS, JavaScript Application Consulting Development and Training + +

+ +--- -> Updated to the latest Angular 7.x +## High-level architectural diagram

ASP.NET Core 2.1 Angular 7+ Starter @@ -471,7 +478,9 @@ Check out **[Trilon.io](https://Trilon.io)** for more info! Twitter [@Trilon_io] Contact us at , and let's talk about your projects needs.

- Trilon.io - Angular Universal, NestJS, JavaScript Application Consulting Development and Training + + Trilon.io - Angular Universal, NestJS, JavaScript Application Consulting Development and Training +

## Follow Trilon online: From 7e2468a7d01472acf15f430b50ca596a64a62d6b Mon Sep 17 00:00:00 2001 From: Mark Pieszak Date: Sun, 17 Mar 2019 11:57:52 -0400 Subject: [PATCH 25/31] docs: update --- README.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/README.md b/README.md index b0b14e9f..b6402f4b 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,12 @@ # ASP.NET Core 2.1 & Angular 7(+) Advanced Starter - PWA & Server-side prerendering (for Angular SEO)! -## Made with :heart: by [Trilon.io](https://Trilon.io) +### Made with :heart: by [Trilon.io](https://Trilon.io)

Trilon.io - Angular Universal, NestJS, JavaScript Application Consulting Development and Training

---- - -## High-level architectural diagram

ASP.NET Core 2.1 Angular 7+ Starter From 4aa16404f1b1a0dd1b4e70b7dd891b0006eabd9b Mon Sep 17 00:00:00 2001 From: Mark Pieszak Date: Mon, 18 Mar 2019 12:32:39 -0400 Subject: [PATCH 26/31] fix: rxjs pin to 6.2.2 closes #714 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 99ba5443..01bc52bc 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "preboot": "^7.0.0", "raw-loader": "^1.0.0", "rimraf": "^2.6.3", - "rxjs": "~6.4.0", + "rxjs": "6.2.2", "sass-loader": "^7.1.0", "style-loader": "^0.23.1", "to-string-loader": "^1.1.5", From 9cf8b7ba6ee15c01e2ec02a77421e7ee77eb6254 Mon Sep 17 00:00:00 2001 From: Mark Pieszak Date: Fri, 5 Apr 2019 12:17:42 -0400 Subject: [PATCH 27/31] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b6402f4b..8e933ca5 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ### Made with :heart: by [Trilon.io](https://Trilon.io)

- Trilon.io - Angular Universal, NestJS, JavaScript Application Consulting Development and Training + Trilon.io - Angular Universal, NestJS, JavaScript Application Consulting Development and Training

@@ -476,7 +476,7 @@ Contact us at , and let's talk about your projects needs.

- Trilon.io - Angular Universal, NestJS, JavaScript Application Consulting Development and Training + Trilon.io - Angular Universal, NestJS, JavaScript Application Consulting Development and Training

From 82dc1897c03f9546b613b72e4a72040b85ab54f1 Mon Sep 17 00:00:00 2001 From: Mark Pieszak Date: Fri, 5 Apr 2019 12:45:04 -0400 Subject: [PATCH 28/31] Update README.md --- README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8e933ca5..ecac2050 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,6 @@

- -

- ASP.NET Core 2.1 Angular 7+ Starter -

- ### Harness the power of Angular 7+, ASP.NET Core 2.1, now with SEO ! Angular SEO in action: @@ -20,6 +15,12 @@ Angular SEO in action: ASP.NET Core Angular7 SEO

+### Angular Universal Application Architecture + +

+ ASP.NET Core 2.1 Angular 7+ Starter +

+ ### What is this repo? Live Demo here: http://aspnetcore-angular2-universal.azurewebsites.net This repository is maintained by [Trilon.io](https://Trilon.io) and the [Angular](https://github.com/angular/angular) Universal team and is meant to be an advanced starter From b8ec2b0ff276a187e3821797fbd03201842d5311 Mon Sep 17 00:00:00 2001 From: Mark Pieszak Date: Fri, 5 Apr 2019 12:45:28 -0400 Subject: [PATCH 29/31] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index ecac2050..482cc672 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,8 @@

+--- + ### Harness the power of Angular 7+, ASP.NET Core 2.1, now with SEO ! Angular SEO in action: From 55f0105f807946cff515aaa0ef535de1329c2b9f Mon Sep 17 00:00:00 2001 From: Mark Pieszak Date: Sun, 7 Apr 2019 17:56:32 -0400 Subject: [PATCH 30/31] Update README.md --- README.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 482cc672..f52bdf4d 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,17 @@ # ASP.NET Core 2.1 & Angular 7(+) Advanced Starter - PWA & Server-side prerendering (for Angular SEO)! -### Made with :heart: by [Trilon.io](https://Trilon.io) -

+--- + +
+

- Trilon.io - Angular Universal, NestJS, JavaScript Application Consulting Development and Training + Trilon.io - Angular Universal, NestJS, JavaScript Application Consulting Development and Training

+ +

Made with :heart: by Trilon.io

+ --- ### Harness the power of Angular 7+, ASP.NET Core 2.1, now with SEO ! From 837b134516631afb3034d40278e2bc9f170acdba Mon Sep 17 00:00:00 2001 From: Detys Date: Mon, 27 May 2019 02:49:07 +0300 Subject: [PATCH 31/31] fix(seeding): seeding now follows .NET Core 2.0 best practices --- Program.cs | 60 ++++++++++++------- Server/Data/CoreEFStartup.cs | 17 ++++++ Server/Data/LoggingEFStartup.cs | 13 ++++ ...itializer.cs => SimpleContentEFStartup.cs} | 24 ++++---- 4 files changed, 80 insertions(+), 34 deletions(-) create mode 100644 Server/Data/CoreEFStartup.cs create mode 100644 Server/Data/LoggingEFStartup.cs rename Server/Data/{DbInitializer.cs => SimpleContentEFStartup.cs} (67%) diff --git a/Program.cs b/Program.cs index 69e56419..fdf46454 100644 --- a/Program.cs +++ b/Program.cs @@ -6,28 +6,42 @@ using Microsoft.Extensions.Logging; using System; using System.IO; +using System.Threading.Tasks; -public class Program { - public static void Main (string[] args) { - var host = BuildWebHost (args); - using (var scope = host.Services.CreateScope ()) { - var services = scope.ServiceProvider; - try { - var context = services.GetRequiredService(); - DbInitializer.Initialize(context); - } catch (Exception ex) { - var logger = services.GetRequiredService> (); - logger.LogError (ex, "An error occurred while seeding the database."); - } - } +public class Program +{ + public static async Task Main(string[] args) + { + var host = BuildWebHost(args); + using (var scope = host.Services.CreateScope()) + { + var services = scope.ServiceProvider; - host.Run (); - } - public static IWebHost BuildWebHost (string[] args) => - WebHost.CreateDefaultBuilder (args) - .UseKestrel () - .UseContentRoot (Directory.GetCurrentDirectory ()) - .UseIISIntegration () - .UseStartup () - .Build (); - } + try + { + await EnsureDataStorageIsReady(services); + + } catch (Exception ex) + { + var logger = services.GetRequiredService>(); + logger.LogError(ex, "An error occurred while seeding the database."); + } + } + + host.Run(); + } + public static IWebHost BuildWebHost(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseStartup() + .Build(); + + private static async Task EnsureDataStorageIsReady(IServiceProvider services) + { + await CoreEFStartup.InitializeDatabaseAsync(services); + await SimpleContentEFStartup.InitializeDatabaseAsync(services); + await LoggingEFStartup.InitializeDatabaseAsync(services); + } +} diff --git a/Server/Data/CoreEFStartup.cs b/Server/Data/CoreEFStartup.cs new file mode 100644 index 00000000..d3947ef1 --- /dev/null +++ b/Server/Data/CoreEFStartup.cs @@ -0,0 +1,17 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; + +namespace AspCoreServer.Data +{ + public static class CoreEFStartup + { + public static async Task InitializeDatabaseAsync(IServiceProvider services) + { + var context = services.GetRequiredService(); + + await context.Database.EnsureCreatedAsync(); + } + + } +} diff --git a/Server/Data/LoggingEFStartup.cs b/Server/Data/LoggingEFStartup.cs new file mode 100644 index 00000000..edf33498 --- /dev/null +++ b/Server/Data/LoggingEFStartup.cs @@ -0,0 +1,13 @@ +using System; +using System.Threading.Tasks; + +namespace AspCoreServer.Data +{ + public static class LoggingEFStartup + { + public static async Task InitializeDatabaseAsync(IServiceProvider services) + { + //Implent to your hearts' content + } + } +} diff --git a/Server/Data/DbInitializer.cs b/Server/Data/SimpleContentEFStartup.cs similarity index 67% rename from Server/Data/DbInitializer.cs rename to Server/Data/SimpleContentEFStartup.cs index 919c9282..db160dee 100644 --- a/Server/Data/DbInitializer.cs +++ b/Server/Data/SimpleContentEFStartup.cs @@ -1,16 +1,20 @@ using System; -using System.Linq; -using AspCoreServer; +using System.Threading.Tasks; using AspCoreServer.Models; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; -namespace AspCoreServer.Data { - public static class DbInitializer { - public static void Initialize (SpaDbContext context) { - context.Database.EnsureCreated (); +namespace AspCoreServer.Data +{ + public static class SimpleContentEFStartup + { + public static async Task InitializeDatabaseAsync(IServiceProvider services) + { + var context = services.GetRequiredService(); - if (context.User.Any ()) { + + if (await context.User.AnyAsync()) + { return; // DB has been seeded } var users = new User[] { @@ -27,11 +31,9 @@ public static void Initialize (SpaDbContext context) { new User () { Name = "Gaulomatic" }, new User () { Name = "GRIMMR3AP3R" } }; + await context.User.AddRangeAsync(users); - foreach (var s in users) { - context.User.Add (s); - } - context.SaveChanges (); + await context.SaveChangesAsync(); } } }