From 03357fd0deed0e6c78c50fa3a35ec01e1d39bc91 Mon Sep 17 00:00:00 2001 From: Adam Plumer <caerus.karu@gmail.com> Date: Sun, 20 May 2018 19:42:52 +1200 Subject: [PATCH] feat(grpc-engine): introduce package --- modules/grpc-engine/BUILD.bazel | 73 +++++++++++++++++++++++ modules/grpc-engine/README.md | 7 +++ modules/grpc-engine/index.ts | 8 +++ modules/grpc-engine/package.json | 28 +++++++++ modules/grpc-engine/public_api.ts | 8 +++ modules/grpc-engine/spec/index.spec.ts | 43 +++++++++++++ modules/grpc-engine/src/grpc-engine.proto | 24 ++++++++ modules/grpc-engine/src/main.ts | 59 ++++++++++++++++++ package.json | 5 +- 9 files changed, 254 insertions(+), 1 deletion(-) create mode 100644 modules/grpc-engine/BUILD.bazel create mode 100644 modules/grpc-engine/README.md create mode 100644 modules/grpc-engine/index.ts create mode 100644 modules/grpc-engine/package.json create mode 100644 modules/grpc-engine/public_api.ts create mode 100644 modules/grpc-engine/spec/index.spec.ts create mode 100644 modules/grpc-engine/src/grpc-engine.proto create mode 100644 modules/grpc-engine/src/main.ts diff --git a/modules/grpc-engine/BUILD.bazel b/modules/grpc-engine/BUILD.bazel new file mode 100644 index 0000000000..fcf0e217a2 --- /dev/null +++ b/modules/grpc-engine/BUILD.bazel @@ -0,0 +1,73 @@ +load("//tools:defaults.bzl", "ng_module", "ng_package", "ng_test_library", "jasmine_node_test") + +package(default_visibility = ["//visibility:public"]) + +#load("@build_bazel_rules_typescript//:defs.bzl", "ts_proto_library") + +#proto_library( +# name = "engine_proto", +# srcs = ["grpc-engine.proto"], +#) +# +#ts_proto_library( +# name = "engine_ts_proto", +# deps = [":engine_proto"], +#) + +filegroup( + name = "temp_proto", + srcs = ["src/grpc-engine.proto"] +) + +ng_module( + name = "grpc-engine", + srcs = glob([ + "*.ts", + "src/**/*.ts", + ]), + module_name = "@nguniversal/grpc-engine", + deps = [ + "//modules/common/engine", + "@ngudeps//grpc" + # ":engine_ts_proto" + ], +) + +ng_package( + name = "npm_package", + srcs = [ + ":package.json", + ":grpc-engine.proto", + ], + entry_point = "modules/grpc-engine/index.js", + readme_md = ":README.md", + tags = ["release"], + deps = [ + ":grpc-engine", + "@ngudeps//grpc", + ], +) + +ng_test_library( + name = "unit_test_lib", + srcs = glob([ + "spec/**/*.spec.ts", + ]), + deps = [ + ":grpc-engine", + "@ngudeps//grpc", + "@ngudeps//domino", + "@ngudeps//xhr2", + "@ngudeps//zone.js", + "@angular//packages/platform-browser", + "@angular//packages/platform-server", + ], +) + +jasmine_node_test( + name = "unit_test", + srcs = [ + ":unit_test_lib", + ":grpc-engine.proto", + ], +) diff --git a/modules/grpc-engine/README.md b/modules/grpc-engine/README.md new file mode 100644 index 0000000000..caf5b43b0b --- /dev/null +++ b/modules/grpc-engine/README.md @@ -0,0 +1,7 @@ +# Angular gRPC Engine + +This is a gRPC Engine for running Angular applications on the server for server side rendering + +## Usage + +To be added \ No newline at end of file diff --git a/modules/grpc-engine/index.ts b/modules/grpc-engine/index.ts new file mode 100644 index 0000000000..45965af3d3 --- /dev/null +++ b/modules/grpc-engine/index.ts @@ -0,0 +1,8 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +export * from './public_api'; diff --git a/modules/grpc-engine/package.json b/modules/grpc-engine/package.json new file mode 100644 index 0000000000..e45c41b049 --- /dev/null +++ b/modules/grpc-engine/package.json @@ -0,0 +1,28 @@ +{ + "name": "@nguniversal/grpc-engine", + "version": "0.0.0-PLACEHOLDER", + "description": "gRPC engine for running server Angular applications", + "license": "MIT", + "keywords": [ + "grpc", + "ssr", + "universal" + ], + "peerDependencies": { + "@angular/common": "NG_VERSION", + "@angular/core": "NG_VERSION", + "@angular/platform-server": "NG_VERSION", + "grpc": "^1.11.3" + }, + "ng-update": { + "packageGroup": "NG_UPDATE_PACKAGE_GROUP" + }, + "repository": { + "type": "git", + "url": "https://github.com/angular/universal" + }, + "bugs": { + "url": "https://github.com/angular/universal/issues" + }, + "homepage": "https://github.com/angular/universal" +} diff --git a/modules/grpc-engine/public_api.ts b/modules/grpc-engine/public_api.ts new file mode 100644 index 0000000000..ed3a321552 --- /dev/null +++ b/modules/grpc-engine/public_api.ts @@ -0,0 +1,8 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +export { startGRPCEngine } from './src/main'; diff --git a/modules/grpc-engine/spec/index.spec.ts b/modules/grpc-engine/spec/index.spec.ts new file mode 100644 index 0000000000..d210311194 --- /dev/null +++ b/modules/grpc-engine/spec/index.spec.ts @@ -0,0 +1,43 @@ +import { ServerModule } from '@angular/platform-server'; +import { NgModule, Component } from '@angular/core'; +import 'zone.js'; + +import { BrowserModule } from '@angular/platform-browser'; +import { startGRPCEngine } from '@nguniversal/grpc-engine'; +import {load, credentials} from 'grpc'; + +function createClient() { + const engineProto = load('../grpc-engine.proto').GRPCEngine; + const client = engineProto.SSR('localhost:9090', credentials.createInsecure()); + return client; +} + +export function makeTestingModule(template: string, component?: any): any { + @Component({ + selector: 'root', + template: template + }) + class MockComponent {} + @NgModule({ + imports: [ServerModule, BrowserModule.withServerTransition({appId: 'mock'})], + declarations: [component || MockComponent], + bootstrap: [component || MockComponent] + }) + class MockServerModule {} + return MockServerModule; +} + +describe('test runner', () => { + it('should render a basic template', async (done) => { + const template = `some template: ${new Date()}`; + const appModule = makeTestingModule(template); + const server = await startGRPCEngine(appModule); + const client = createClient(); + + client.render({id: 1, document: '<root></root>'}, async(_err: any, response: any) => { + expect(response.html).toContain(template); + await server.close(); + done(); + }); + }); +}); diff --git a/modules/grpc-engine/src/grpc-engine.proto b/modules/grpc-engine/src/grpc-engine.proto new file mode 100644 index 0000000000..1a461aa8a1 --- /dev/null +++ b/modules/grpc-engine/src/grpc-engine.proto @@ -0,0 +1,24 @@ +package grpcengine; +syntax = "proto3"; + + +service SSR { + rpc render(RenderOptions) returns (RenderResponse) {} +} + +message RenderOptions { + int32 id = 1; + string document = 2; + string documentFilename = 3; +} + +message RenderResponse { + int32 id = 1; + string html = 2; + optional Error error = 3; +} + +message Error { + string message = 1; + string stack = 2; +} diff --git a/modules/grpc-engine/src/main.ts b/modules/grpc-engine/src/main.ts new file mode 100644 index 0000000000..eb09a606e5 --- /dev/null +++ b/modules/grpc-engine/src/main.ts @@ -0,0 +1,59 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { + ɵCommonEngine as CommonEngine, + ɵRenderOptions as RenderOptions, +} from '@nguniversal/common/engine'; +import {NgModuleFactory, Type} from '@angular/core'; +import * as grpc from 'grpc'; + +export interface GRPCEngineServer { + close: () => void; +} + +export interface GRPCEngineRenderOptions extends RenderOptions { + id: number; +} + +export interface GRPCEngineResponse { + id: number; + html: string; +} + +export function startGRPCEngine( + moduleOrFactory: Type<{}> | NgModuleFactory<{}>, + host = 'localhost', + port = 9090 +): Promise<GRPCEngineServer> { + // needs to be a directory up so it lines up with deployment + const protoDescriptor = grpc.load('./grpc-engine.proto'); + return new Promise((resolve, _reject) => { + const engine = new CommonEngine(moduleOrFactory); + + const server = new grpc.Server(); + server.addProtoService(protoDescriptor.GRPCEngine.service, { + render: async (call: any, callback: any) => { + const renderOptions = call.request as GRPCEngineRenderOptions; + try { + const html = await engine.render(renderOptions); + callback(null, {id: renderOptions.id, html}); + } catch (error) { + callback(null, {id: renderOptions.id, error}); + } + } + }); + // TODO(Toxicable): how to take credentials as input? + server.bind(`${host}:${port}`, grpc.ServerCredentials.createInsecure()); + server.start(); + + resolve({ + close: () => new Promise((res, _rej) => server.tryShutdown(() => res())) + }); + }); +} + diff --git a/package.json b/package.json index cc35469f02..bc0eea80ce 100644 --- a/package.json +++ b/package.json @@ -128,6 +128,9 @@ "zone.js": "^0.8.26" }, "dependencies": { - "yarn": "^1.10.1" + "yarn": "^1.10.1", + "@types/ws": "^5.1.1", + "grpc": "^1.12.2", + "ws": "^5.1.1" } }