Skip to content

Commit

Permalink
feat: add koa universal ssr
Browse files Browse the repository at this point in the history
  • Loading branch information
jonathonadams committed Sep 4, 2020
1 parent 09350b0 commit 0a9a74f
Show file tree
Hide file tree
Showing 18 changed files with 339 additions and 118 deletions.
29 changes: 29 additions & 0 deletions angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -1929,6 +1929,35 @@
"style": "scss"
}
}
},
"common-universal-engine": {
"root": "libs/common/universal-engine",
"sourceRoot": "libs/common/universal-engine/src",
"projectType": "library",
"schematics": {},
"architect": {
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"libs/common/universal-engine/tsconfig.lib.json",
"libs/common/universal-engine/tsconfig.spec.json"
],
"exclude": [
"**/node_modules/**",
"!libs/common/universal-engine/**/*"
]
}
},
"test": {
"builder": "@nrwl/jest:jest",
"options": {
"jestConfig": "libs/common/universal-engine/jest.config.js",
"tsConfig": "libs/common/universal-engine/tsconfig.spec.json",
"passWithNoTests": true
}
}
}
}
},
"cli": {
Expand Down
130 changes: 61 additions & 69 deletions apps/demo/demo-web/server.ts
Original file line number Diff line number Diff line change
@@ -1,87 +1,79 @@
import 'zone.js/dist/zone-node';

import { ngExpressEngine } from '@nguniversal/express-engine';
import express from 'express';
import { existsSync } from 'fs';
import { join } from 'path';
// import helmet from 'helmet';
import { ngKoaEngine } from '@ztp/common/universal-engine';
import Koa from 'koa';
// @ts-ignore
import serve from 'koa-static';
// @ts-ignore
import helmet from 'koa-helmet';

import { AppServerModule } from './src/main.server';
import { APP_BASE_HREF } from '@angular/common';
import { existsSync } from 'fs';

// The Express app is exported so that it can be used by serverless Functions.
// The Koa app is exported so that it can be used by serverless Functions.
export function app() {
const server = express();
// const distFolder = join(process.cwd(), 'dist/apps/demo/demo-web');
const distFolder = join(process.cwd(), 'dist/browser');
const indexHtml = existsSync(join(distFolder, 'index.original.html'))
const server = new Koa();
const distFolder = join(process.cwd(), 'dist/apps/demo/demo-web');
// const distFolder = join(process.cwd(), 'dist/browser');
const indexFilename = existsSync(join(distFolder, 'index.original.html'))
? 'index.original.html'
: 'index';
: 'index.html';
const indexPath = join(distFolder, indexFilename);

// server.use((req, res, next) => {
// return helmet({
// expectCt: { enforce: true },
// hsts: {
// maxAge: 63072000, // two years
// includeSubDomains: true,
// preload: true,
// },
// contentSecurityPolicy: {
// directives: {
// 'default-src': ["'self'"],
// 'connect-src': [
// 'https://zero-to-production.dev',
// 'https://*.zero-to-production.dev',
// ],
// 'worker-src': [`'self'`],
// 'script-src': [`'self'`, 'cdnjs.cloudflare.com'],
// 'img-src': [`'self'`, 'ssl.gstatic.com'],
// 'style-src': [
// `'unsafe-inline'`, // TODO -> Remove this, limitation of angular at the moment
// `'self'`,
// 'fonts.googleapis.com',
// 'cdnjs.cloudflare.com',
// ],
// 'style-src-elem': [
// `'unsafe-inline'`, // TODO -> Remove this, limitation of angular at the moment
// `'self'`,
// 'fonts.googleapis.com',
// 'cdnjs.cloudflare.com',
// ],
// 'font-src': ['fonts.gstatic.com'],
// 'upgrade-insecure-requests': [],
// },
// },
// })(req, res, next);
// });

// Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
server.engine(
'html',
ngExpressEngine({
bootstrap: AppServerModule,
}) as any
// TODO -> Remove the 'unsafe-inline', limitation of angular at the moment
server.use(
helmet({
expectCt: { enforce: true },
hsts: {
maxAge: 63072000, // two years
includeSubDomains: true,
preload: true,
},
contentSecurityPolicy: {
directives: {
'default-src': ["'self'"],
'connect-src': [
'https://zero-to-production.dev',
'https://*.zero-to-production.dev',
],
'worker-src': [`'self'`],
'script-src': [`'self'`, 'cdnjs.cloudflare.com'],
'img-src': [`'self'`, 'ssl.gstatic.com'],
'style-src': [
`'unsafe-inline'`,
`'self'`,
'fonts.googleapis.com',
'cdnjs.cloudflare.com',
],
// 'style-src-elem': [
// `'unsafe-inline'`,
// `'self'`,
// 'fonts.googleapis.com',
// 'cdnjs.cloudflare.com',
// ],
'font-src': ['fonts.gstatic.com'],
},
},
})
);

server.set('view engine', 'html');
server.set('views', distFolder);
// Universal Koa engine
const render = ngKoaEngine({
bootstrap: AppServerModule,
});

// Example Express Rest API endpoints
// app.get('/api/**', (req, res) => { });
// Serve static files from /browser
server.get(
'*.*',
express.static(distFolder, {
maxAge: '1y',
server.use(
serve(distFolder, {
// Do not serve the index file because it needs to be rendered by the Universal engine
index: false,
maxAge: 86400 * 365 * 1000,
})
);

// All regular routes use the Universal engine
server.get('*', (req, res) => {
res.render(indexHtml, {
req,
providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }],
});
server.use(async (ctx) => {
await render(indexPath, ctx);
});

return server;
Expand All @@ -93,7 +85,7 @@ function run() {
// Start up the Node server
const server = app();
server.listen(port, () => {
console.log(`Node Express server listening on http://localhost:${port}`);
console.log(`Node Koa server listening on http://localhost:${port}`);
});
}

Expand Down
2 changes: 1 addition & 1 deletion apps/demo/demo-web/src/app/app.server.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
import { AppModule } from './app.module';
import { AppComponent } from './app.component';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { UniversalInterceptor } from '@ztp/common/data-access';
import { UniversalInterceptor } from '@ztp/common/universal-engine';
import {
LOGIN_PAGE,
REGISTER_PAGE,
Expand Down
1 change: 0 additions & 1 deletion libs/common/data-access/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,3 @@ export { CommonDataAccessModule } from './lib/common-data-access.module';
export { ApiService } from './lib/api/api.service';
export { GraphQLService } from './lib/graphql/graphql-service';
export { ApolloUtilsService } from './lib/graphql/apollo-utils.service';
export { UniversalInterceptor } from './lib/universal-interceptor';
5 changes: 5 additions & 0 deletions libs/common/universal-engine/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Original pull request
https://github.com/angular/universal/pull/1714

At the time of creating this, google has yet to decide to include this in the universal repo.
If it will be included in the future, these files will be removed.
14 changes: 14 additions & 0 deletions libs/common/universal-engine/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module.exports = {
name: 'common-universal-engine',
preset: '../../../jest.config.js',
globals: {
'ts-jest': {
tsConfig: '<rootDir>/tsconfig.spec.json',
},
},
transform: {
'^.+\\.[tj]sx?$': 'ts-jest',
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'],
coverageDirectory: '../../../coverage/libs/common/universal-engine',
};
3 changes: 3 additions & 0 deletions libs/common/universal-engine/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { ngKoaEngine } from './lib/engine';
export { REQUEST, RESPONSE } from './lib/tokens';
export { UniversalInterceptor } from './lib/universal-interceptor';
68 changes: 68 additions & 0 deletions libs/common/universal-engine/src/lib/engine.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { NgModuleFactory, StaticProvider, Type } from '@angular/core';
import {
ɵCommonEngine as CommonEngine,
ɵRenderOptions as RenderOptions,
} from '@nguniversal/common/engine';
import { REQUEST, RESPONSE } from './tokens';
import { Context, Request, Response } from 'koa';

/**
* These are the allowed options for the engine
*/
export interface NgSetupOptions {
bootstrap: Type<{}> | NgModuleFactory<{}>;
providers?: StaticProvider[];
}

/**
* This is a Koa engine for handling Angular Applications
*/
export function ngKoaEngine(setupOptions: Readonly<NgSetupOptions>) {
const engine = new CommonEngine(
setupOptions.bootstrap,
setupOptions.providers
);

return async (
filePath: string,
ctx: Context,
options: Readonly<Partial<RenderOptions>> = {}
) => {
const moduleOrFactory = options.bootstrap || setupOptions.bootstrap;
if (!moduleOrFactory) {
throw new Error(
'You must pass in a NgModule or NgModuleFactory to be bootstrapped'
);
}

const { request, response } = ctx;
const renderOptions: RenderOptions = Object.assign(
{ bootstrap: setupOptions.bootstrap },
options
);

renderOptions.url =
renderOptions.url ||
`${request.protocol}://${request.get('host') || ''}${
request.originalUrl
}`;
renderOptions.documentFilePath = renderOptions.documentFilePath || filePath;
renderOptions.providers = (renderOptions.providers || []).concat(
getReqResProviders(request, response)
);

ctx.body = await engine.render(renderOptions);
};
}

/**
* Get providers of the request and response
*/
function getReqResProviders(req: Request, res?: Response): StaticProvider[] {
const providers: StaticProvider[] = [{ provide: REQUEST, useValue: req }];
if (res) {
providers.push({ provide: RESPONSE, useValue: res });
}

return providers;
}
5 changes: 5 additions & 0 deletions libs/common/universal-engine/src/lib/tokens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { InjectionToken } from '@angular/core';
import { Request, Response } from 'koa';

export const REQUEST = new InjectionToken<Request>('REQUEST');
export const RESPONSE = new InjectionToken<Response>('RESPONSE');
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import {
HttpHandler,
HttpRequest,
} from '@angular/common/http';
import { Request } from 'express';
import { REQUEST } from '@nguniversal/express-engine/tokens';
import { Request } from 'koa';
import { REQUEST } from './tokens';

/**
* In a Universal app, HTTP URLs must be absolute (for example, https://my-server.com/api/heroes).
Expand Down
13 changes: 13 additions & 0 deletions libs/common/universal-engine/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"extends": "../../../tsconfig.base.json",
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}
12 changes: 12 additions & 0 deletions libs/common/universal-engine/tsconfig.lib.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "commonjs",
"outDir": "../../../dist/out-tsc",
"declaration": true,
"rootDir": "./src",
"types": ["node"]
},
"exclude": ["**/*.spec.ts"],
"include": ["**/*.ts"]
}
15 changes: 15 additions & 0 deletions libs/common/universal-engine/tsconfig.spec.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"module": "commonjs",
"types": ["jest", "node"]
},
"include": [
"**/*.spec.ts",
"**/*.spec.tsx",
"**/*.spec.js",
"**/*.spec.jsx",
"**/*.d.ts"
]
}
5 changes: 5 additions & 0 deletions libs/common/universal-engine/tslint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"extends": "../../../tslint.json",
"linterOptions": { "exclude": ["!**/*"] },
"rules": {}
}
Loading

0 comments on commit 0a9a74f

Please sign in to comment.