Skip to content
This repository was archived by the owner on Aug 16, 2022. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist",
"outputPath": "dist/browser",
"index": "src/index.html",
"main": "src/main.ts",
"tsConfig": "src/tsconfig.app.json",
Expand Down Expand Up @@ -99,6 +99,29 @@
"**/*.mock.ts"
]
}
},
"server": {
"builder": "@angular-devkit/build-angular:server",
"options": {
"outputPath": "dist/server",
"main": "src/main.server.ts",
"tsConfig": "src/tsconfig.server.json"
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"sourceMap": false,
"optimization": {
"scripts": false,
"styles": true
}
}
}
}
}
},
Expand Down
17 changes: 15 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@
"lint": "ng lint contentful-web",
"sass-lint": "sass-lint",
"sass-lint-html": "sass-lint -v --format html -o sass-lint.html",
"e2e": "ng e2e"
"e2e": "ng e2e",
"compile:server": "webpack --config webpack.server.config.js --progress --colors",
"serve:ssr": "node dist/server",
"build:ssr": "npm run build:client-and-server-bundles && npm run compile:server",
"build:client-and-server-bundles": "ng build --prod && ng run contentful-web:server:production --bundleDependencies all"
},
"private": true,
"dependencies": {
Expand All @@ -27,7 +31,11 @@
"@angular/forms": "~8.2.11",
"@angular/platform-browser": "~8.2.11",
"@angular/platform-browser-dynamic": "~8.2.11",
"@angular/platform-server": "~8.2.11",
"@angular/router": "~8.2.11",
"@nguniversal/common": "8.2.6",
"@nguniversal/express-engine": "v8.2.6",
"@nguniversal/module-map-ngfactory-loader": "v8.2.6",
"angulartics2": "8.1.0",
"apollo-cache-inmemory": "^1.6.2",
"apollo-client": "^2.6.3",
Expand All @@ -36,8 +44,10 @@
"apollo-link-http": "^1.5.15",
"core-js": "^3.1.4",
"es6-shim": "^0.35.3",
"express": "^4.15.2",
"graphql": "^14.4.2",
"graphql-tag": "~2.10.1",
"isomorphic-fetch": "^2.2.1",
"isomorphic-unfetch": "^3.0.0",
"lodash": "^4.17.10",
"normalize-scss": "~7.0.1",
Expand All @@ -51,6 +61,7 @@
"@angular/cli": "8.3.12",
"@angular/compiler-cli": "~8.2.11",
"@angular/language-service": "~8.2.11",
"@types/express": "^4.17.0",
"@types/jasmine": "~3.4.2",
"@types/lodash": "~4.14.133",
"@types/node": "^12.0.4",
Expand All @@ -67,9 +78,11 @@
"karma-jasmine-html-reporter": "~1.4.2",
"protractor": "5.4.2",
"sass-lint": "^1.12.1",
"ts-loader": "^5.2.0",
"ts-node": "^8.3.0",
"tslint": "^5.11.0",
"typescript": "~3.5.0",
"typescript-tslint-plugin": "^0.5.4"
"typescript-tslint-plugin": "^0.5.4",
"webpack-cli": "^3.1.0"
}
}
58 changes: 58 additions & 0 deletions server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* *** NOTE ON IMPORTING FROM ANGULAR AND NGUNIVERSAL IN THIS FILE ***
*
* If your application uses third-party dependencies, you'll need to
* either use Webpack or the Angular CLI's `bundleDependencies` feature
* in order to adequately package them for use on the server without a
* node_modules directory.
*
* However, due to the nature of the CLI's `bundleDependencies`, importing
* Angular in this file will create a different instance of Angular than
* the version in the compiled application code. This leads to unavoidable
* conflicts. Therefore, please do not explicitly import from @angular or
* @nguniversal in this file. You can export any needed resources
* from your application's main.server.ts file, as seen below with the
* import for `ngExpressEngine`.
*/

import 'zone.js/dist/zone-node';

import * as express from 'express';
import {join} from 'path';

// Express server
const app = express();

const PORT = process.env.PORT || 4000;
const DIST_FOLDER = join(process.cwd(), 'dist/browser');

// * NOTE :: leave this as require() since this file is built Dynamically from webpack
const {AppServerModuleNgFactory, LAZY_MODULE_MAP, ngExpressEngine, provideModuleMap} = require('./dist/server/main');

// Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
app.engine('html', ngExpressEngine({
bootstrap: AppServerModuleNgFactory,
providers: [
provideModuleMap(LAZY_MODULE_MAP)
]
}));

app.set('view engine', 'html');
app.set('views', DIST_FOLDER);

// Example Express Rest API endpoints
// app.get('/api/**', (req, res) => { });
// Serve static files from /browser
app.get('*.*', express.static(DIST_FOLDER, {
maxAge: '1y'
}));

// All regular routes use the Universal engine
app.get('*', (req, res) => {
res.render('index', { req });
});

// Start up the Node server
app.listen(PORT, () => {
console.log(`Node Express server listening on http://localhost:${PORT}`);
});
2 changes: 1 addition & 1 deletion src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ import { AnalyticsModule } from './analytics/analytics.module';
BaseComponent
],
imports: [
BrowserModule,
BrowserModule.withServerTransition({ appId: 'serverApp' }),
HttpClientModule,
CoreModule,
ContentBlocksModule,
Expand Down
16 changes: 16 additions & 0 deletions src/app/app.server.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { NgModule } from '@angular/core';
import { ServerModule } from '@angular/platform-server';

import { AppModule } from './app.module';
import { AppComponent } from './app.component';
import { ModuleMapLoaderModule } from '@nguniversal/module-map-ngfactory-loader';

@NgModule({
imports: [
AppModule,
ServerModule,
ModuleMapLoaderModule,
],
bootstrap: [AppComponent],
})
export class AppServerModule {}
125 changes: 125 additions & 0 deletions src/app/core/async-api-call.helper.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { Injectable } from '@angular/core';
import { Observable, Observer, Subscription } from 'rxjs';



@Injectable({
providedIn: 'root'
})
export class AsyncApiCallHelperService {

taskProcessor: MyAsyncTaskProcessor;
constructor() {
this.taskProcessor = new MyAsyncTaskProcessor();
}

doTask<T>(promise: Promise<T>) {
return <Observable<T>> this.taskProcessor.doTask(promise);
}
}

declare const Zone: any;

export abstract class ZoneMacroTaskWrapper<S, R> {
wrap(request: S): Observable<R> {
return new Observable((observer: Observer<R>) => {
let task;
let scheduled = false;
let sub: Subscription|null = null;
let savedResult: any = null;
let savedError: any = null;

// tslint:disable-next-line:no-shadowed-variable
const scheduleTask = (_task: any) => {
task = _task;
scheduled = true;

const delegate = this.delegate(request);
sub = delegate.subscribe(
res => savedResult = res,
err => {
if (!scheduled) {
throw new Error(
'An http observable was completed twice. This shouldn\'t happen, please file a bug.');
}
savedError = err;
scheduled = false;
task.invoke();
},
() => {
if (!scheduled) {
throw new Error(
'An http observable was completed twice. This shouldn\'t happen, please file a bug.');
}
scheduled = false;
task.invoke();
});
};

// tslint:disable-next-line:no-shadowed-variable
const cancelTask = (_task: any) => {
if (!scheduled) {
return;
}
scheduled = false;
if (sub) {
sub.unsubscribe();
sub = null;
}
};

const onComplete = () => {
if (savedError !== null) {
observer.error(savedError);
} else {
observer.next(savedResult);
observer.complete();
}
};

// MockBackend for Http is synchronous, which means that if scheduleTask is by
// scheduleMacroTask, the request will hit MockBackend and the response will be
// sent, causing task.invoke() to be called.
const _task = Zone.current.scheduleMacroTask(
'ZoneMacroTaskWrapper.subscribe', onComplete, {}, () => null, cancelTask);
scheduleTask(_task);

return () => {
if (scheduled && task) {
task.zone.cancelTask(task);
scheduled = false;
}
if (sub) {
sub.unsubscribe();
sub = null;
}
};
});
}

protected abstract delegate(request: S): Observable<R>;
}

export class MyAsyncTaskProcessor extends
ZoneMacroTaskWrapper<Promise<any>, any> {

constructor() { super(); }

// your public task invocation method signature
doTask(request: Promise<any>): Observable<any> {
// call via ZoneMacroTaskWrapper
return this.wrap(request);
}

// delegated raw implementation that will be called by ZoneMacroTaskWrapper
protected delegate(request: Promise<any>): Observable<any> {
return new Observable<any>((observer: Observer<any>) => {
// calling observer.next / complete / error
request
.then(result => {
observer.next(result);
observer.complete();
}).catch(error => observer.error(error));
});
}
}
17 changes: 13 additions & 4 deletions src/app/core/contentful.service.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Injectable, Inject } from '@angular/core';
import { Router } from '@angular/router';
import { InMemoryCache, IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
import { ApolloClient, NetworkStatus, QueryOptions } from 'apollo-client';
import { ApolloLink } from 'apollo-link';
import { onError } from 'apollo-link-error';
import { HttpLink } from 'apollo-link-http';
import { fetch } from 'isomorphic-fetch';
import { GraphQLError } from 'graphql';
import { from, Observable } from 'rxjs';

import { environment } from '../../environments/environment';
import { AsmEvent } from './interfaces/event.interface';
import { MenuItem } from './interfaces/menu.interface';
import { AsyncApiCallHelperService } from './async-api-call.helper.service';

@Injectable()
export class ContentfulService {
Expand All @@ -28,7 +30,8 @@ export class ContentfulService {

constructor(
private http: HttpClient,
private router: Router) {
private router: Router,
@Inject(AsyncApiCallHelperService) private processor: AsyncApiCallHelperService) {
}

// get Contentful Schema from api / backend
Expand Down Expand Up @@ -86,7 +89,7 @@ export class ContentfulService {
introspectionQueryResultData: await this.getContentfulSchema(this.event)
});

const httpLink: HttpLink = new HttpLink({ uri: `${environment.apiUrl}/en/${this.event.name}/graphql` });
const httpLink: HttpLink = new HttpLink({ fetch, uri: `${environment.apiUrl}/en/${this.event.name}/graphql` });

const link: ApolloLink = onError(({ graphQLErrors, networkError }): void => {
if (graphQLErrors) {
Expand Down Expand Up @@ -128,7 +131,13 @@ export class ContentfulService {
if (!this.client) {
this.throwError('Make sure that ContentfulService has been initialized before calling .query');
}
return this.client.query<T, unknown>(options);
return new Promise(resolve => {
this.processor
.doTask(this.client.query<T, unknown>(options))
.subscribe(result => {
resolve(result);
});
});
}

query$<T>(options: QueryOptions): Observable<T> {
Expand Down
14 changes: 12 additions & 2 deletions src/app/meta.resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { ActivatedRouteSnapshot, RouterStateSnapshot, Resolve } from '@angular/r
import { Title, Meta } from '@angular/platform-browser';
import { ContentfulService } from './core/contentful.service';
import { AsmEvent } from './core/interfaces/event.interface';
import { isPlatformBrowser } from '@angular/common';
import { PLATFORM_ID, Inject } from '@angular/core';

@Injectable()
export class MetaResolve implements Resolve<any> {
Expand All @@ -16,10 +18,14 @@ export class MetaResolve implements Resolve<any> {
'/assets/images/assembly-generic-image-7.jpg',
'/assets/images/assembly-generic-image-8.jpg',
];
isBrowser: boolean;

constructor(private contentful: ContentfulService,
private title: Title,
private meta: Meta) { }
private meta: Meta,
@Inject(PLATFORM_ID) platformId: string) {
this.isBrowser = isPlatformBrowser(platformId);
}

public async resolve(route: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Promise<any> {
Expand All @@ -30,7 +36,11 @@ export class MetaResolve implements Resolve<any> {
public setMetaTags(url: string, data?: any) {
const event: AsmEvent = this.contentful.getEvent();
let title = event.eventTitle || 'Assembly';
let image = location.origin + this.images[Math.floor(Math.random() * 8)];
let origin = '';
if (this.isBrowser) {
origin = location.origin;
}
let image = origin + this.images[Math.floor(Math.random() * 8)];
let description = 'Assembly is a bi-annual computer festival, esports event, demoscene and lan party in Helsinki, Finland.';
let type = 'website';
let publishedDate = '';
Expand Down
Loading