diff --git a/.gitignore b/.gitignore index 4319f6807..eef986e8e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ bazel-genfiles bazel-grpc-web bazel-out bazel-testlogs +*.o +protoc-gen-* diff --git a/javascript/net/grpc/web/Makefile b/javascript/net/grpc/web/Makefile index e2f9af611..765f8fe83 100644 --- a/javascript/net/grpc/web/Makefile +++ b/javascript/net/grpc/web/Makefile @@ -13,7 +13,8 @@ # limitations under the License. CXX = g++ -CPPFLAGS += -I/usr/local/include -pthread +CPPFLAGS += -I/usr/local/include \ + -I../../../../third_party/grpc/third_party/protobuf/src -pthread CXXFLAGS += -std=c++11 LDFLAGS += -L/usr/local/lib -lprotoc -lprotobuf -lpthread -ldl diff --git a/javascript/net/grpc/web/grpc_generator.cc b/javascript/net/grpc/web/grpc_generator.cc index e3c77cd74..80ddbbce9 100644 --- a/javascript/net/grpc/web/grpc_generator.cc +++ b/javascript/net/grpc/web/grpc_generator.cc @@ -21,6 +21,7 @@ #include #include #include +#include using google::protobuf::Descriptor; using google::protobuf::FileDescriptor; @@ -32,6 +33,10 @@ using google::protobuf::compiler::ParseGeneratorParameter; using google::protobuf::compiler::PluginMain; using google::protobuf::io::Printer; using google::protobuf::io::ZeroCopyOutputStream; +using google::protobuf::HasSuffixString; +using google::protobuf::ReplaceCharacters; +using google::protobuf::StripPrefixString; +using google::protobuf::StripSuffixString; namespace grpc { namespace web { @@ -90,6 +95,75 @@ string LowercaseFirstLetter(string s) { return s; } +// The following function was copied from +// google/protobuf/src/google/protobuf/compiler/cpp/cpp_helpers.cc +string StripProto(const string& filename) { + if (HasSuffixString(filename, ".protodevel")) { + return StripSuffixString(filename, ".protodevel"); + } else { + return StripSuffixString(filename, ".proto"); + } +} + +// The following 3 functions were copied from +// google/protobuf/src/google/protobuf/compiler/js/js_generator.cc + +// Returns the name of the message with a leading dot and taking into account +// nesting, for example ".OuterMessage.InnerMessage", or returns empty if +// descriptor is null. This function does not handle namespacing, only message +// nesting. +string GetNestedMessageName(const Descriptor* descriptor) { + if (descriptor == NULL) { + return ""; + } + string result = StripPrefixString(descriptor->full_name(), + descriptor->file()->package()); + // Add a leading dot if one is not already present. + if (!result.empty() && result[0] != '.') { + result = "." + result; + } + return result; +} + +// Given a filename like foo/bar/baz.proto, returns the root directory +// path ../../ +string GetRootPath(const string& from_filename, const string& to_filename) { + if (to_filename.find("google/protobuf") == 0) { + // Well-known types (.proto files in the google/protobuf directory) are + // assumed to come from the 'google-protobuf' npm package. We may want to + // generalize this exception later by letting others put generated code in + // their own npm packages. + return "google-protobuf/"; + } + + size_t slashes = std::count(from_filename.begin(), from_filename.end(), '/'); + if (slashes == 0) { + return "./"; + } + string result = ""; + for (size_t i = 0; i < slashes; i++) { + result += "../"; + } + return result; +} + +// Returns the alias we assign to the module of the given .proto filename +// when importing. +string ModuleAlias(const string& filename) { + // This scheme could technically cause problems if a file includes any 2 of: + // foo/bar_baz.proto + // foo_bar_baz.proto + // foo_bar/baz.proto + // + // We'll worry about this problem if/when we actually see it. This name isn't + // exposed to users so we can change it later if we need to. + string basename = StripProto(filename); + ReplaceCharacters(&basename, "-", '$'); + ReplaceCharacters(&basename, "/", '_'); + ReplaceCharacters(&basename, ".", '_'); + return basename + "_pb"; +} + /* Finds all message types used in all services in the file, and returns them * as a map of fully qualified message type name to message descriptor */ std::map GetAllMessages(const FileDescriptor* file) { @@ -126,14 +200,20 @@ void PrintMessagesDeps(Printer* printer, const FileDescriptor* file) { } void PrintCommonJsMessagesDeps(Printer* printer, const FileDescriptor* file) { - std::map messages = GetAllMessages(file); std::map vars; + + for (int i = 0; i < file->dependency_count(); i++) { + const string& name = file->dependency(i)->name(); + vars["alias"] = ModuleAlias(name); + vars["dep_filename"] = GetRootPath(file->name(), name) + StripProto(name); + // we need to give each cross-file import an alias + printer->Print( + vars, + "\nvar $alias$ = require('$dep_filename$_pb.js')\n"); + } + string package = file->package(); - string filename = file->name(); - // Remove .proto extension - filename = filename.substr(0, filename.size() - 6); vars["package_name"] = package; - vars["filename"] = filename; printer->Print(vars, "const proto = {};\n"); if (!package.empty()) { @@ -149,6 +229,14 @@ void PrintCommonJsMessagesDeps(Printer* printer, const FileDescriptor* file) { } } + // need to import the messages from our own file + string filename = StripProto(file->name()); + size_t last_slash = filename.find_last_of('/'); + if (last_slash != string::npos) { + filename = filename.substr(last_slash + 1); + } + vars["filename"] = filename; + printer->Print( vars, "proto.$package_name$ = require('./$filename$_pb.js');\n\n"); @@ -221,7 +309,7 @@ void PrintMethodInfo(Printer* printer, std::map vars) { printer->Indent(); printer->Print( vars, - "proto.$out$,\n" + "$out_type$,\n" "/** @param {!proto.$in$} request */\n" "function(request) {\n"); printer->Print( @@ -230,7 +318,7 @@ void PrintMethodInfo(Printer* printer, std::map vars) { printer->Print("},\n"); printer->Print( vars, - ("proto.$out$." + GetDeserializeMethodName(vars["mode"]) + + ("$out_type$." + GetDeserializeMethodName(vars["mode"]) + "\n").c_str()); printer->Outdent(); printer->Print( @@ -349,8 +437,7 @@ class GrpcCodeGenerator : public CodeGenerator { } if (file_name.empty()) { - *error = "options: out is required"; - return false; + file_name = StripProto(file->name()) + "_grpc_pb.js"; } if (mode.empty()) { *error = "options: mode is required"; @@ -398,8 +485,8 @@ class GrpcCodeGenerator : public CodeGenerator { switch (import_style) { case ImportStyle::CLOSURE: printer.Print( - vars, - "goog.provide('proto.$package_dot$$service_name$Client');\n"); + vars, + "goog.provide('proto.$package_dot$$service_name$Client');\n"); break; case ImportStyle::COMMONJS: break; @@ -439,6 +526,15 @@ class GrpcCodeGenerator : public CodeGenerator { vars["method_name"] = method->name(); vars["in"] = method->input_type()->full_name(); vars["out"] = method->output_type()->full_name(); + if (import_style == ImportStyle::COMMONJS && + method->output_type()->file() != file) { + // Cross-file ref in CommonJS needs to use the module alias instead + // of the global name. + vars["out_type"] = ModuleAlias(method->output_type()->file()->name()) + + GetNestedMessageName(method->output_type()); + } else { + vars["out_type"] = "proto."+method->output_type()->full_name(); + } // Client streaming is not supported yet if (!method->client_streaming()) { diff --git a/packages/grpc-web/package.json b/packages/grpc-web/package.json index 8fca60de0..d2413d178 100644 --- a/packages/grpc-web/package.json +++ b/packages/grpc-web/package.json @@ -1,13 +1,20 @@ { "name": "grpc-web", "version": "0.3.0", - "description": "gRPC web runtime", + "author": "Google Inc.", + "description": "gRPC-Web JS Client Runtime Library", + "homepage": "https://grpc.io/", + "repository": { + "type": "git", + "url": "https://github.com/grpc/grpc-web.git" + }, + "bugs": "https://github.com/grpc/grpc-web/issues", "main": "index.js", "scripts": { "build": "node scripts/build.js", "prepare": "npm run build && require-self", "prepublishOnly": "npm run build", - "test": "mocha" + "test": "mocha --timeout 5000 \"./test/**/*_test.js\"" }, "license": "Apache-2.0", "devDependencies": { diff --git a/packages/grpc-web/scripts/build.js b/packages/grpc-web/scripts/build.js index bc0124acd..f8d52f90a 100644 --- a/packages/grpc-web/scripts/build.js +++ b/packages/grpc-web/scripts/build.js @@ -1,3 +1,21 @@ +/** + * + * Copyright 2018 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + const path = require("path"); const {exec} = require("child_process"); diff --git a/packages/grpc-web/test/closure_client.js b/packages/grpc-web/test/closure_client.js new file mode 100644 index 000000000..41cfda816 --- /dev/null +++ b/packages/grpc-web/test/closure_client.js @@ -0,0 +1,34 @@ +/** + * + * Copyright 2018 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +goog.provide('proto.grpc.gateway.testing.EchoAppClient'); + +goog.require('proto.grpc.gateway.testing.EchoServiceClient'); +goog.require('proto.grpc.gateway.testing.EchoRequest'); + + +proto.grpc.gateway.testing.EchoAppClient = function() { + this.client = + new proto.grpc.gateway.testing.EchoServiceClient('MyHostname', null, null); +} + +proto.grpc.gateway.testing.EchoAppClient.prototype.echo = function(msg, cb) { + var request = new proto.grpc.gateway.testing.EchoRequest(); + request.setMessage(msg); + this.client.echo(request, {}, cb); +} diff --git a/packages/grpc-web/test/common.js b/packages/grpc-web/test/common.js new file mode 100644 index 000000000..f66f19f47 --- /dev/null +++ b/packages/grpc-web/test/common.js @@ -0,0 +1,37 @@ +/** + * + * Copyright 2018 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +const fs = require('fs'); +const GENERATED_CODE_PATH = './generated'; + +var removeDirectory = function(path) { + if (fs.existsSync(path)) { + fs.readdirSync(path).forEach(function(file, index) { + var curPath = path + "/" + file; + if (fs.lstatSync(curPath).isDirectory()) { + removeDirectory(curPath); + } else { + fs.unlinkSync(curPath); + } + }); + fs.rmdirSync(path); + } +} + +exports.removeDirectory = removeDirectory; +exports.GENERATED_CODE_PATH = GENERATED_CODE_PATH; diff --git a/packages/grpc-web/test/generated_code_test.js b/packages/grpc-web/test/generated_code_test.js index 1ac16afa5..8ac27fe14 100644 --- a/packages/grpc-web/test/generated_code_test.js +++ b/packages/grpc-web/test/generated_code_test.js @@ -1,17 +1,37 @@ +/** + * + * Copyright 2018 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + const assert = require('assert'); const execSync = require('child_process').execSync; const commandExists = require('command-exists').sync; const fs = require('fs'); const path = require('path'); +const removeDirectory = require('./common.js').removeDirectory; +const GENERATED_CODE_PATH = require('./common.js').GENERATED_CODE_PATH; +const mockXmlHttpRequest = require('mock-xmlhttprequest'); -var MockXMLHttpRequest = require('mock-xmlhttprequest').newMockXhr(); -global.XMLHttpRequest = MockXMLHttpRequest; +var MockXMLHttpRequest; describe('protoc generated code', function() { const genCodePath = path.resolve(__dirname, './echo_pb.js'); - const genCodeCmd = `protoc -I=./test echo.proto \ ---js_out=import_style=commonjs:./test` + const genCodeCmd = 'protoc -I=./test/protos echo.proto ' + + '--js_out=import_style=commonjs:./test'; before(function() { if (!commandExists('protoc')) { @@ -46,13 +66,16 @@ describe('protoc generated code', function() { }); -describe('grpc-web plugin generated code (commonjs+grpcwebtext)', function() { +describe('grpc-web generated code (commonjs+grpcwebtext)', function() { + const oldXMLHttpRequest = global.XMLHttpRequest; + const protoGenCodePath = path.resolve(__dirname, './echo_pb.js'); const genCodePath = path.resolve(__dirname, './echo_grpc_pb.js'); - const genCodeCmd = `protoc -I=./test echo.proto \ ---js_out=import_style=commonjs:./test \ ---grpc-web_out=import_style=commonjs,mode=grpcwebtext,out=echo_grpc_pb.js:\ -./test`; + + const genCodeCmd = + 'protoc -I=./test/protos echo.proto ' + + '--js_out=import_style=commonjs:./test ' + + '--grpc-web_out=import_style=commonjs,mode=grpcwebtext:./test'; before(function() { ['protoc', 'protoc-gen-grpc-web'].map(prog => { @@ -69,6 +92,8 @@ describe('grpc-web plugin generated code (commonjs+grpcwebtext)', function() { if (fs.existsSync(genCodePath)) { fs.unlinkSync(genCodePath); } + MockXMLHttpRequest = mockXmlHttpRequest.newMockXhr() + global.XMLHttpRequest = MockXMLHttpRequest; }); afterEach(function() { @@ -78,6 +103,7 @@ describe('grpc-web plugin generated code (commonjs+grpcwebtext)', function() { if (fs.existsSync(genCodePath)) { fs.unlinkSync(genCodePath); } + global.XMLHttpRequest = oldXMLHttpRequest; }); it('should exist', function() { @@ -104,13 +130,14 @@ describe('grpc-web plugin generated code (commonjs+grpcwebtext)', function() { // a single 'aaa' string, encoded assert.equal('AAAAAAUKA2FhYQ==', xhr.body); assert.equal('MyHostname/grpc.gateway.testing.EchoService/Echo', - xhr.url); + xhr.url); assert.equal( - `accept: application/grpc-web-text\r\n\ -content-type: application/grpc-web-text\r\n\ -custom-header-1: value1\r\n\ -x-grpc-web: 1\r\n\ -x-user-agent: grpc-web-javascript/0.1\r\n`, xhr.requestHeaders.getAll()); + 'accept: application/grpc-web-text\r\n' + + 'content-type: application/grpc-web-text\r\n' + + 'custom-header-1: value1\r\n' + + 'x-grpc-web: 1\r\n' + + 'x-user-agent: grpc-web-javascript/0.1\r\n', + xhr.requestHeaders.getAll()); done(); }; echoService.echo(request, {'custom-header-1':'value1'}); @@ -125,14 +152,14 @@ x-user-agent: grpc-web-javascript/0.1\r\n`, xhr.requestHeaders.getAll()); request.setMessage('aaa'); MockXMLHttpRequest.onSend = function(xhr) { xhr.respond(200, {'Content-Type': 'application/grpc-web-text'}, - // a single data frame with 'aaa' message, encoded - 'AAAAAAUKA2FhYQ=='); + // a single data frame with 'aaa' message, encoded + 'AAAAAAUKA2FhYQ=='); }; echoService.echo(request, {'custom-header-1':'value1'}, - function(err, response) { - assert.equal('aaa', response.getMessage()); - done(); - }); + function(err, response) { + assert.equal('aaa', response.getMessage()); + done(); + }); }); it('should receive streaming response', function(done) { @@ -145,13 +172,14 @@ x-user-agent: grpc-web-javascript/0.1\r\n`, xhr.requestHeaders.getAll()); request.setMessageCount(3); MockXMLHttpRequest.onSend = function(xhr) { xhr.respond(200, {'Content-Type': 'application/grpc-web-text'}, - // 3 'aaa' messages in 3 data frames, encoded - 'AAAAAAUKA2FhYQAAAAAFCgNhYWEAAAAABQoDYWFh'); + // 3 'aaa' messages in 3 data frames, encoded + 'AAAAAAUKA2FhYQAAAAAFCgNhYWEAAAAABQoDYWFh'); }; var numMessagesReceived = 0; var p = new Promise(function(resolve, reject) { - var stream = echoService.serverStreamingEcho(request, - {'custom-header-1':'value1'}); + var stream = + echoService.serverStreamingEcho(request, + {'custom-header-1':'value1'}); stream.on('data', function(response) { numMessagesReceived++; assert.equal('aaa', response.getMessage()); @@ -175,14 +203,14 @@ x-user-agent: grpc-web-javascript/0.1\r\n`, xhr.requestHeaders.getAll()); request.setMessage('aaa'); MockXMLHttpRequest.onSend = function(xhr) { xhr.respond(200, {'Content-Type': 'application/grpc-web-text'}, - // a single data frame with an 'aaa' message, followed by, - // a trailer frame with content 'grpc-status:0' - 'AAAAAAUKA2FhYYAAAAAPZ3JwYy1zdGF0dXM6MA0K'); + // a single data frame with an 'aaa' message, followed by, + // a trailer frame with content 'grpc-status:0' + 'AAAAAAUKA2FhYYAAAAAPZ3JwYy1zdGF0dXM6MA0K'); }; var call = echoService.echo(request, {'custom-header-1':'value1'}, - function(err, response) { - assert.equal('aaa', response.getMessage()); - }); + function(err, response) { + assert.equal('aaa', response.getMessage()); + }); call.on('status', function(status) { assert.equal('object', typeof status.metadata); assert.equal(true, 'grpc-status' in status.metadata); @@ -200,13 +228,103 @@ x-user-agent: grpc-web-javascript/0.1\r\n`, xhr.requestHeaders.getAll()); request.setMessage('aaa'); MockXMLHttpRequest.onSend = function(xhr) { xhr.respond(200, {'Content-Type': 'application/grpc-web-text'}, - // a trailer frame with content 'grpc-status:10' - 'gAAAABBncnBjLXN0YXR1czoxMA0K'); + // a trailer frame with content 'grpc-status:10' + 'gAAAABBncnBjLXN0YXR1czoxMA0K'); }; var call = echoService.echo(request, {'custom-header-1':'value1'}, - function(err, response) { - assert.equal(10, err.code); - done(); - }); + function(err, response) { + assert.equal(10, err.code); + done(); + }); + }); +}); + +describe('grpc-web generated code (closure+grpcwebtext)', function() { + const oldXMLHttpRequest = global.XMLHttpRequest; + + const compiledCodePath = path.resolve(__dirname, './generated/compiled.js'); + const genCodeCmd = + 'protoc -I=./test/protos echo.proto ' + + '--js_out=import_style=closure:./test/generated ' + + '--grpc-web_out=import_style=closure,mode=grpcwebtext:./test/generated'; + const cwd = process.cwd(); + const jsPaths = [ + ".", + "../../../javascript", + "../../../third_party/closure-library", + "../../../third_party/grpc/third_party/protobuf/js", + ].map(jsPath => path.relative(cwd, path.resolve(__dirname, jsPath))); + const closureArgs = [].concat( + jsPaths.map(jsPath => `--js=${jsPath}`), + [ + `--entry_point=goog:proto.grpc.gateway.testing.EchoAppClient`, + `--dependency_mode=STRICT`, + `--js_output_file ./test/generated/compiled.js`, + `--output_wrapper="%output%module.exports = proto.grpc.gateway.testing;"`, + ] + ); + const closureCmd = "google-closure-compiler " + closureArgs.join(' '); + + before(function() { + ['protoc', 'protoc-gen-grpc-web'].map(prog => { + if (!commandExists(prog)) { + assert.fail(`${prog} is not installed`); + } + }); + if (!fs.existsSync(path.resolve( + __dirname, '../../../javascript/net/grpc/web/grpcwebclientbase.js'))) { + this.skip(); + } + removeDirectory(path.resolve(__dirname, GENERATED_CODE_PATH)); + fs.mkdirSync(path.resolve(__dirname, GENERATED_CODE_PATH)); + }); + + beforeEach(function() { + MockXMLHttpRequest = mockXmlHttpRequest.newMockXhr() + global.XMLHttpRequest = MockXMLHttpRequest; + }); + + afterEach(function() { + global.XMLHttpRequest = oldXMLHttpRequest; + }); + + after(function() { + removeDirectory(path.resolve(__dirname, GENERATED_CODE_PATH)); + }); + + it('should exist', function() { + execSync(genCodeCmd); + execSync(closureCmd); + assert.equal(true, fs.existsSync(compiledCodePath)); + }); + + it('should import', function() { + var compiled = require(compiledCodePath); + echoAppClient = new compiled.EchoAppClient(); + assert.equal('function', typeof echoAppClient.echo); + }); + + it('should send unary request', function(done) { + var compiled = require(compiledCodePath); + echoAppClient = new compiled.EchoAppClient(); + MockXMLHttpRequest.onSend = function(xhr) { + assert.equal("AAAAAAUKA2FiYw==", xhr.body); + done(); + } + echoAppClient.echo('abc', () => {}); + }); + + it('should receive unary response', function(done) { + var compiled = require(compiledCodePath); + echoAppClient = new compiled.EchoAppClient(); + MockXMLHttpRequest.onSend = function(xhr) { + assert.equal("AAAAAAUKA2FiYw==", xhr.body); + xhr.respond(200, {'Content-Type': 'application/grpc-web-text'}, + "AAAAAAUKA2FiYw=="); + } + echoAppClient.echo('abc', function(err, response) { + assert.equal("abc", response.getMessage()); + done(); + }); }); }); diff --git a/packages/grpc-web/test/plugin_test.js b/packages/grpc-web/test/plugin_test.js new file mode 100644 index 000000000..9e4e58d57 --- /dev/null +++ b/packages/grpc-web/test/plugin_test.js @@ -0,0 +1,167 @@ +/** + * + * Copyright 2018 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +const assert = require('assert'); +const execSync = require('child_process').execSync; +const commandExists = require('command-exists').sync; +const fs = require('fs'); +const path = require('path'); +const removeDirectory = require('./common.js').removeDirectory; +const GENERATED_CODE_PATH = require('./common.js').GENERATED_CODE_PATH; +const mockXmlHttpRequest = require('mock-xmlhttprequest'); + +var MockXMLHttpRequest; + + +describe('grpc-web plugin test, with subdirectories', function() { + const oldXMLHttpRequest = global.XMLHttpRequest; + + const genCodePath1 = path.resolve( + __dirname, GENERATED_CODE_PATH + '/myapi/v1/myapi_pb.js'); + const genCodePath2 = path.resolve( + __dirname, GENERATED_CODE_PATH + '/otherapi/v1/otherapi_pb.js'); + const genCodePath3 = path.resolve( + __dirname, GENERATED_CODE_PATH + '/myapi/v1/myapi_grpc_pb.js'); + + const genCodeCmd = + 'protoc -I=./test/protos ' + + './test/protos/myapi/v1/myapi.proto ' + + './test/protos/otherapi/v1/otherapi.proto ' + + '--js_out=import_style=commonjs:./test/generated ' + + '--grpc-web_out=import_style=commonjs,mode=grpcwebtext:./test/generated'; + + before(function() { + ['protoc', 'protoc-gen-grpc-web'].map(prog => { + if (!commandExists(prog)) { + assert.fail(`${prog} is not installed`); + } + }); + }); + + beforeEach(function() { + removeDirectory(path.resolve(__dirname, GENERATED_CODE_PATH)); + fs.mkdirSync(path.resolve(__dirname, GENERATED_CODE_PATH)); + MockXMLHttpRequest = mockXmlHttpRequest.newMockXhr() + global.XMLHttpRequest = MockXMLHttpRequest; + }); + + afterEach(function() { + removeDirectory(path.resolve(__dirname, GENERATED_CODE_PATH)); + global.XMLHttpRequest = oldXMLHttpRequest; + }); + + it('should exist', function() { + execSync(genCodeCmd); + assert.equal(true, fs.existsSync(genCodePath1)); + assert.equal(true, fs.existsSync(genCodePath2)); + assert.equal(true, fs.existsSync(genCodePath3)); + }); + + it('should import', function() { + execSync(genCodeCmd); + + const {OtherThing} = require(genCodePath2); + var otherThing = new OtherThing(); + otherThing.setValue('abc'); + assert.equal('abc', otherThing.getValue()); + + const {MyServiceClient} = require(genCodePath3); + var myClient = new MyServiceClient("MyHostname", null, null); + assert.equal('function', typeof myClient.doThis); + }); + + it('should send unary request', function(done) { + execSync(genCodeCmd); + + const {OtherThing} = require(genCodePath2); + var otherThing = new OtherThing(); + otherThing.setValue('abc'); + + const {MyServiceClient} = require(genCodePath3); + var myClient = new MyServiceClient("MyHostname", null, null); + + MockXMLHttpRequest.onSend = function(xhr) { + assert.equal("AAAAAAUKA2FiYw==", xhr.body); + assert.equal("MyHostname/myproject.myapi.v1.MyService/DoThis", xhr.url); + done(); + }; + myClient.doThis(otherThing); + }); +}); + + +describe('grpc-web plugin test, with multiple input files', function() { + const genCodePath1 = path.resolve( + __dirname, GENERATED_CODE_PATH + '/myapi/v1/myapi_pb.js'); + const genCodePath2 = path.resolve( + __dirname, GENERATED_CODE_PATH + '/otherapi/v1/otherapi_pb.js'); + const genCodePath3 = path.resolve( + __dirname, GENERATED_CODE_PATH + '/myapi/v1/myapi_grpc_pb.js'); + const genCodePath4 = path.resolve( + __dirname, GENERATED_CODE_PATH + '/myapi/v1/myapi-two_grpc_pb.js'); + + const genCodeCmd = + 'protoc -I=./test/protos ' + + './test/protos/myapi/v1/myapi.proto ' + + './test/protos/myapi/v1/myapi-two.proto ' + + './test/protos/otherapi/v1/otherapi.proto ' + + '--js_out=import_style=commonjs:./test/generated ' + + '--grpc-web_out=import_style=commonjs,mode=grpcwebtext:./test/generated'; + + before(function() { + ['protoc', 'protoc-gen-grpc-web'].map(prog => { + if (!commandExists(prog)) { + assert.fail(`${prog} is not installed`); + } + }); + }); + + beforeEach(function() { + removeDirectory(path.resolve(__dirname, GENERATED_CODE_PATH)); + fs.mkdirSync(path.resolve(__dirname, GENERATED_CODE_PATH)); + }); + + afterEach(function() { + removeDirectory(path.resolve(__dirname, GENERATED_CODE_PATH)); + }); + + it('should exist', function() { + execSync(genCodeCmd); + assert.equal(true, fs.existsSync(genCodePath1)); + assert.equal(true, fs.existsSync(genCodePath2)); + assert.equal(true, fs.existsSync(genCodePath3)); + assert.equal(true, fs.existsSync(genCodePath4)); + }); + + it('should import', function() { + execSync(genCodeCmd); + + const {OtherThing} = require(genCodePath2); + var otherThing = new OtherThing(); + otherThing.setValue('abc'); + assert.equal('abc', otherThing.getValue()); + + const {MyServiceClient} = require(genCodePath3); + var myClient = new MyServiceClient("MyHostname", null, null); + assert.equal('function', typeof myClient.doThis); + + const {MyServiceBClient} = require(genCodePath4); + var myClientB = new MyServiceBClient("MyHostname", null, null); + assert.equal('function', typeof myClientB.doThat); + }); +}); diff --git a/packages/grpc-web/test/echo.proto b/packages/grpc-web/test/protos/echo.proto similarity index 100% rename from packages/grpc-web/test/echo.proto rename to packages/grpc-web/test/protos/echo.proto diff --git a/packages/grpc-web/test/protos/myapi/v1/myapi-two.proto b/packages/grpc-web/test/protos/myapi/v1/myapi-two.proto new file mode 100644 index 000000000..54e95ec9c --- /dev/null +++ b/packages/grpc-web/test/protos/myapi/v1/myapi-two.proto @@ -0,0 +1,23 @@ +// Copyright 2018 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package myproject.myapi.v1; + +import "otherapi/v1/otherapi.proto"; + +service MyServiceB { + rpc DoThat(myproject.otherapi.v1.OtherThing) returns (myproject.otherapi.v1.OtherThing); +} diff --git a/packages/grpc-web/test/protos/myapi/v1/myapi.proto b/packages/grpc-web/test/protos/myapi/v1/myapi.proto new file mode 100644 index 000000000..03d9cc958 --- /dev/null +++ b/packages/grpc-web/test/protos/myapi/v1/myapi.proto @@ -0,0 +1,28 @@ +// Copyright 2018 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package myproject.myapi.v1; + +import "otherapi/v1/otherapi.proto"; + +message MyThing { + string message = 1; + myproject.otherapi.v1.OtherThing other_thing = 2; +} + +service MyService { + rpc DoThis(myproject.otherapi.v1.OtherThing) returns (myproject.otherapi.v1.OtherThing); +} diff --git a/packages/grpc-web/test/protos/otherapi/v1/otherapi.proto b/packages/grpc-web/test/protos/otherapi/v1/otherapi.proto new file mode 100644 index 000000000..ddcd0fa00 --- /dev/null +++ b/packages/grpc-web/test/protos/otherapi/v1/otherapi.proto @@ -0,0 +1,21 @@ +// Copyright 2018 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package myproject.otherapi.v1; + +message OtherThing { + string value = 1; +} diff --git a/scripts/kokoro.sh b/scripts/kokoro.sh index 273c47216..ffaa7dc87 100755 --- a/scripts/kokoro.sh +++ b/scripts/kokoro.sh @@ -53,5 +53,5 @@ source ./scripts/test-proxy.sh docker-compose down # Run unit tests from npm package -docker run grpc-web:prereqs /bin/bash \ +docker run --rm grpc-web:prereqs /bin/bash \ /github/grpc-web/scripts/docker-run-tests.sh