You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@thrift.apache.org by jk...@apache.org on 2019/06/21 10:38:41 UTC

[thrift] branch master updated: Implement episodic compilation for js code generation

This is an automated email from the ASF dual-hosted git repository.

jking pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/thrift.git


The following commit(s) were added to refs/heads/master by this push:
     new 3f0d444  Implement episodic compilation for js code generation
3f0d444 is described below

commit 3f0d4447919a7b358e60b09ab919bbe52758cc98
Author: Mustafa Senol Cosar <mu...@unscrambl.com>
AuthorDate: Fri Mar 1 18:57:09 2019 +0300

    Implement episodic compilation for js code generation
---
 compiler/cpp/Makefile.am                           |   2 +-
 compiler/cpp/src/thrift/generate/t_js_generator.cc | 186 ++++++++++++++++++---
 compiler/cpp/src/thrift/main.cc                    |   3 +
 compiler/cpp/src/thrift/parse/t_program.h          |  11 +-
 lib/nodejs/Makefile.am                             |   2 +
 .../test/episodic-code-generation-test/client.js   |  77 +++++++++
 .../episodic_compilation.package.json              |   3 +
 .../test/episodic-code-generation-test/server.js   |  50 ++++++
 lib/nodejs/test/testAll.sh                         |  68 +++++++-
 test/Service.thrift                                |  24 +++
 test/Types.thrift                                  |  23 +++
 11 files changed, 411 insertions(+), 38 deletions(-)

diff --git a/compiler/cpp/Makefile.am b/compiler/cpp/Makefile.am
index 16d4d3a..9a3aeda 100644
--- a/compiler/cpp/Makefile.am
+++ b/compiler/cpp/Makefile.am
@@ -65,7 +65,7 @@ thrift_SOURCES = src/thrift/audit/t_audit.cpp \
                  src/thrift/parse/t_type.h \
                  src/thrift/parse/t_typedef.cc \
                  src/thrift/parse/t_typedef.h \
-                 src/thrift/platform.h 
+                 src/thrift/platform.h
 
 # Specific client generator source
 thrift_SOURCES += src/thrift/generate/t_as3_generator.cc \
diff --git a/compiler/cpp/src/thrift/generate/t_js_generator.cc b/compiler/cpp/src/thrift/generate/t_js_generator.cc
index af402a4..88758b2 100644
--- a/compiler/cpp/src/thrift/generate/t_js_generator.cc
+++ b/compiler/cpp/src/thrift/generate/t_js_generator.cc
@@ -26,6 +26,7 @@
 #include <vector>
 #include <list>
 #include <cassert>
+#include <unordered_map>
 
 #include <stdlib.h>
 #include <sys/stat.h>
@@ -38,9 +39,11 @@ using std::ostream;
 using std::ostringstream;
 using std::string;
 using std::stringstream;
+using std::unordered_map;
 using std::vector;
 
 static const string endl = "\n"; // avoid ostream << std::endl flushes
+static const string episode_file_name = "thrift.js.episode";
 // largest consecutive integer representable by a double (2 ^ 53 - 1)
 static const int64_t max_safe_integer = 0x1fffffffffffff;
 // smallest consecutive number representable by a double (-2 ^ 53 + 1)
@@ -65,6 +68,7 @@ public:
     gen_jquery_ = false;
     gen_ts_ = false;
     gen_es6_ = false;
+    gen_episode_file_ = false;
 
     bool with_ns_ = false;
 
@@ -79,22 +83,26 @@ public:
         with_ns_ = true;
       } else if( iter->first.compare("es6") == 0) {
         gen_es6_ = true;
+      } else if( iter->first.compare("imports") == 0) {
+        parse_imports(program, iter->second);
+      } else if (iter->first.compare("thrift_package_output_directory") == 0) {
+        parse_thrift_package_output_directory(iter->second);
       } else {
-        throw "unknown option js:" + iter->first;
+        throw std::invalid_argument("unknown option js:" + iter->first);
       }
     }
 
     if (gen_es6_ && gen_jquery_) {
-      throw "Invalid switch: [-gen js:es6,jquery] options not compatible";
+      throw std::invalid_argument("invalid switch: [-gen js:es6,jquery] options not compatible");
     }
 
     if (gen_node_ && gen_jquery_) {
-      throw "Invalid switch: [-gen js:node,jquery] options not compatible, try: [-gen js:node -gen "
-            "js:jquery]";
+      throw std::invalid_argument("invalid switch: [-gen js:node,jquery] options not compatible, try: [-gen js:node -gen "
+            "js:jquery]");
     }
 
     if (!gen_node_ && with_ns_) {
-      throw "Invalid switch: [-gen js:with_ns] is only valid when using node.js";
+      throw std::invalid_argument("invalid switch: [-gen js:with_ns] is only valid when using node.js");
     }
 
     // Depending on the processing flags, we will update these to be ES6 compatible
@@ -207,6 +215,7 @@ public:
   std::string ts_service_includes();
   std::string render_includes();
   std::string render_ts_includes();
+  std::string get_import_path(t_program* program);
   std::string declare_field(t_field* tfield, bool init = false, bool obj = false);
   std::string function_signature(t_function* tfunction,
                                  std::string prefix = "",
@@ -215,6 +224,13 @@ public:
   std::string type_to_enum(t_type* ttype);
   std::string make_valid_nodeJs_identifier(std::string const& name);
 
+  /**
+   * Helper parser functions
+   */
+
+  void parse_imports(t_program* program, const std::string& imports_string);
+  void parse_thrift_package_output_directory(const std::string& thrift_package_output_directory);
+
   std::string autogen_comment() override {
     return std::string("//\n") + "// Autogenerated by Thrift Compiler (" + THRIFT_VERSION + ")\n"
            + "//\n" + "// DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING\n"
@@ -354,6 +370,11 @@ private:
   bool gen_es6_;
 
   /**
+   * True if we will generate an episode file.
+   */
+  bool gen_episode_file_;
+
+  /**
    * The name of the defined module(s), for TypeScript Definition Files.
    */
   string ts_module_;
@@ -364,6 +385,21 @@ private:
   bool no_ns_;
 
   /**
+   * The node modules to use when importing the previously generated files.
+   */
+  vector<string> imports;
+
+  /**
+   * Cache for imported modules.
+   */
+  unordered_map<string, string> module_name_2_import_path;
+
+  /**
+   * The prefix to use when generating the episode file.
+   */
+  string thrift_package_output_directory_;
+
+  /**
    * The variable decorator for "const" variables. Will default to "var" if in an incompatible language.
    */
   string js_const_type_;
@@ -381,6 +417,7 @@ private:
   /**
    * File streams
    */
+  ofstream_with_content_based_conditional_update f_episode_;
   ofstream_with_content_based_conditional_update f_types_;
   ofstream_with_content_based_conditional_update f_service_;
   ofstream_with_content_based_conditional_update f_types_ts_;
@@ -397,14 +434,23 @@ void t_js_generator::init_generator() {
   // Make output directory
   MKDIR(get_out_dir().c_str());
 
-  string outdir = get_out_dir();
+  const auto outdir = get_out_dir();
 
   // Make output file(s)
-  string f_types_name = outdir + program_->get_name() + "_types.js";
+  if (gen_episode_file_) {
+    const auto f_episode_file_path = outdir + episode_file_name;
+    f_episode_.open(f_episode_file_path);
+  }
+
+  const auto f_types_name = outdir + program_->get_name() + "_types.js";
   f_types_.open(f_types_name.c_str());
+  if (gen_episode_file_) {
+    const auto types_module = program_->get_name() + "_types";
+    f_episode_ << types_module << ":" << thrift_package_output_directory_ << "/" << types_module << endl;
+  }
 
   if (gen_ts_) {
-    string f_types_ts_name = outdir + program_->get_name() + "_types.d.ts";
+    const auto f_types_ts_name = outdir + program_->get_name() + "_types.d.ts";
     f_types_ts_.open(f_types_ts_name.c_str());
   }
 
@@ -500,8 +546,7 @@ string t_js_generator::render_includes() {
   if (gen_node_) {
     const vector<t_program*>& includes = program_->get_includes();
     for (auto include : includes) {
-      result += js_const_type_ + make_valid_nodeJs_identifier(include->get_name()) + "_ttypes = require('./" + include->get_name()
-                + "_types');\n";
+      result += js_const_type_ + make_valid_nodeJs_identifier(include->get_name()) + "_ttypes = require('" + get_import_path(include) + "');\n";
     }
     if (includes.size() > 0) {
       result += "\n";
@@ -522,8 +567,7 @@ string t_js_generator::render_ts_includes() {
   }
   const vector<t_program*>& includes = program_->get_includes();
   for (auto include : includes) {
-    result += "import " + make_valid_nodeJs_identifier(include->get_name()) + "_ttypes = require('./" + include->get_name()
-              + "_types');\n";
+    result += "import " + make_valid_nodeJs_identifier(include->get_name()) + "_ttypes = require('" + get_import_path(include) + "');\n";
   }
   if (includes.size() > 0) {
     result += "\n";
@@ -532,6 +576,21 @@ string t_js_generator::render_ts_includes() {
   return result;
 }
 
+string t_js_generator::get_import_path(t_program* program) {
+  const string import_file_name(program->get_name() + "_types");
+  if (program->get_recursive()) {
+    return "./" + import_file_name;
+  }
+
+  const string import_file_name_with_extension = import_file_name + ".js";
+
+    auto module_name_and_import_path_iterator = module_name_2_import_path.find(import_file_name);
+    if (module_name_and_import_path_iterator != module_name_2_import_path.end()) {
+      return module_name_and_import_path_iterator->second;
+    }
+    return "./" + import_file_name;
+}
+
 /**
  * Close up (or down) some filez.
  */
@@ -546,6 +605,9 @@ void t_js_generator::close_generator() {
     }
     f_types_ts_.close();
   }
+  if (gen_episode_file_){
+    f_episode_.close();
+  }
 }
 
 /**
@@ -657,7 +719,7 @@ string t_js_generator::render_const_value(t_type* type, t_const_value* value) {
       }
       break;
     default:
-      throw "compiler error: no const of base type " + t_base_type::t_base_name(tbase);
+      throw std::runtime_error("compiler error: no const of base type " + t_base_type::t_base_name(tbase));
     }
   } else if (type->is_enum()) {
     out << value->get_integer();
@@ -676,7 +738,7 @@ string t_js_generator::render_const_value(t_type* type, t_const_value* value) {
         }
       }
       if (field_type == NULL) {
-        throw "type error: " + type->get_name() + " has no field " + v_iter->first->get_string();
+        throw std::runtime_error("type error: " + type->get_name() + " has no field " + v_iter->first->get_string());
       }
       if (v_iter != val.begin())
         out << ",";
@@ -698,7 +760,7 @@ string t_js_generator::render_const_value(t_type* type, t_const_value* value) {
     for (v_iter = val.begin(); v_iter != val.end(); ++v_iter) {
       if (v_iter != val.begin())
         out << "," << endl;
-      
+
       if (ktype->is_base_type() && ((t_base_type*)get_true_type(ktype))->get_base() == t_base_type::TYPE_I64){
         out << indent() << "\"" << v_iter->first->get_integer() << "\"";
       } else {
@@ -846,7 +908,6 @@ void t_js_generator::generate_js_struct_definition(ostream& out,
         f_types_ts_ << ts_indent() << (*m_iter)->get_name() << ": "
                     << ts_get_type((*m_iter)->get_type()) << ";" << endl;
       }
-      
     }
   }
 
@@ -1124,6 +1185,9 @@ void t_js_generator::generate_js_struct_writer(ostream& out, t_struct* tstruct)
 void t_js_generator::generate_service(t_service* tservice) {
   string f_service_name = get_out_dir() + service_name_ + ".js";
   f_service_.open(f_service_name.c_str());
+  if (gen_episode_file_) {
+    f_episode_ << service_name_ << ":" << thrift_package_output_directory_ << "/" << service_name_ << endl;
+  }
 
   if (gen_ts_) {
     string f_service_ts_name = get_out_dir() + service_name_ + ".d.ts";
@@ -2136,7 +2200,7 @@ void t_js_generator::generate_deserialize_field(ostream& out,
   t_type* type = get_true_type(tfield->get_type());
 
   if (type->is_void()) {
-    throw "CANNOT GENERATE DESERIALIZE CODE FOR void TYPE: " + prefix + tfield->get_name();
+    throw std::runtime_error("CANNOT GENERATE DESERIALIZE CODE FOR void TYPE: " + prefix + tfield->get_name());
   }
 
   string name = prefix + tfield->get_name();
@@ -2152,7 +2216,7 @@ void t_js_generator::generate_deserialize_field(ostream& out,
       t_base_type::t_base tbase = ((t_base_type*)type)->get_base();
       switch (tbase) {
       case t_base_type::TYPE_VOID:
-        throw "compiler error: cannot serialize void field in a struct: " + name;
+        throw std::runtime_error("compiler error: cannot serialize void field in a struct: " + name);
         break;
       case t_base_type::TYPE_STRING:
         out << (type->is_binary() ? "readBinary()" : "readString()");
@@ -2176,7 +2240,7 @@ void t_js_generator::generate_deserialize_field(ostream& out,
         out << "readDouble()";
         break;
       default:
-        throw "compiler error: no JS name for base type " + t_base_type::t_base_name(tbase);
+        throw std::runtime_error("compiler error: no JS name for base type " + t_base_type::t_base_name(tbase));
       }
     } else if (type->is_enum()) {
       out << "readI32()";
@@ -2317,7 +2381,7 @@ void t_js_generator::generate_serialize_field(ostream& out, t_field* tfield, str
 
   // Do nothing for void types
   if (type->is_void()) {
-    throw "CANNOT GENERATE SERIALIZE CODE FOR void TYPE: " + prefix + tfield->get_name();
+    throw std::runtime_error("CANNOT GENERATE SERIALIZE CODE FOR void TYPE: " + prefix + tfield->get_name());
   }
 
   if (type->is_struct() || type->is_xception()) {
@@ -2338,7 +2402,7 @@ void t_js_generator::generate_serialize_field(ostream& out, t_field* tfield, str
       t_base_type::t_base tbase = ((t_base_type*)type)->get_base();
       switch (tbase) {
       case t_base_type::TYPE_VOID:
-        throw "compiler error: cannot serialize void field in a struct: " + name;
+        throw std::runtime_error("compiler error: cannot serialize void field in a struct: " + name);
         break;
       case t_base_type::TYPE_STRING:
         out << (type->is_binary() ? "writeBinary(" : "writeString(") << name << ")";
@@ -2362,7 +2426,7 @@ void t_js_generator::generate_serialize_field(ostream& out, t_field* tfield, str
         out << "writeDouble(" << name << ")";
         break;
       default:
-        throw "compiler error: no JS name for base type " + t_base_type::t_base_name(tbase);
+        throw std::runtime_error("compiler error: no JS name for base type " + t_base_type::t_base_name(tbase));
       }
     } else if (type->is_enum()) {
       out << "writeI32(" << name << ")";
@@ -2510,7 +2574,7 @@ string t_js_generator::declare_field(t_field* tfield, bool init, bool obj) {
         result += " = null";
         break;
       default:
-        throw "compiler error: no JS initializer for base type " + t_base_type::t_base_name(tbase);
+        throw std::runtime_error("compiler error: no JS initializer for base type " + t_base_type::t_base_name(tbase));
       }
     } else if (type->is_enum()) {
       result += " = null";
@@ -2589,7 +2653,7 @@ string t_js_generator::type_to_enum(t_type* type) {
     t_base_type::t_base tbase = ((t_base_type*)type)->get_base();
     switch (tbase) {
     case t_base_type::TYPE_VOID:
-      throw "NO T_VOID CONSTRUCT";
+      throw std::runtime_error("NO T_VOID CONSTRUCT");
     case t_base_type::TYPE_STRING:
       return "Thrift.Type.STRING";
     case t_base_type::TYPE_BOOL:
@@ -2617,7 +2681,7 @@ string t_js_generator::type_to_enum(t_type* type) {
     return "Thrift.Type.LIST";
   }
 
-  throw "INVALID TYPE IN type_to_enum: " + type->get_name();
+  throw std::runtime_error("INVALID TYPE IN type_to_enum: " + type->get_name());
 }
 
 /**
@@ -2785,10 +2849,80 @@ std::string t_js_generator::make_valid_nodeJs_identifier(std::string const& name
   return str;
 }
 
+void t_js_generator::parse_imports(t_program* program, const std::string& imports_string) {
+  if (program->get_recursive()) {
+    throw std::invalid_argument("[-gen js:imports=] option is not usable in recursive code generation mode");
+  }
+  std::stringstream sstream(imports_string);
+  std::string import;
+  while (std::getline(sstream, import, ':')) {
+    imports.emplace_back(import);
+  }
+  if (imports.empty()) {
+    throw std::invalid_argument("invalid usage: [-gen js:imports=] requires at least one path "
+          "(multiple paths are separated by ':')");
+  }
+  for (auto& import : imports) {
+    // Strip trailing '/'
+    if (!import.empty() && import[import.size() - 1] == '/') {
+      import = import.substr(0, import.size() - 1);
+    }
+    if (import.empty()) {
+      throw std::invalid_argument("empty paths are not allowed in imports");
+    }
+    std::ifstream episode_file;
+    string line;
+    const auto episode_file_path = import + "/" + episode_file_name;
+    episode_file.open(episode_file_path);
+    if (!episode_file) {
+      throw std::runtime_error("failed to open the file '" + episode_file_path + "'");
+    }
+    while (std::getline(episode_file, line)) {
+      const auto separator_position = line.find(':');
+      if (separator_position == string::npos) {
+        // could not find the separator in the line
+        throw std::runtime_error("the episode file '" + episode_file_path + "' is malformed, the line '" + line
+          + "' does not have a key:value separator ':'");
+      }
+      const auto module_name = line.substr(0, separator_position);
+      const auto import_path = line.substr(separator_position + 1);
+      if (module_name.empty()) {
+        throw std::runtime_error("the episode file '" + episode_file_path + "' is malformed, the module name is empty");
+      }
+      if (import_path.empty()) {
+        throw std::runtime_error("the episode file '" + episode_file_path + "' is malformed, the import path is empty");
+      }
+      const auto module_import_path = import.substr(import.find_last_of('/') + 1) + "/" + import_path;
+      const auto result = module_name_2_import_path.emplace(module_name, module_import_path);
+      if (!result.second) {
+        throw std::runtime_error("multiple providers of import path found for " + module_name
+          + "\n\t" + module_import_path + "\n\t" + result.first->second);
+      }
+    }
+  }
+}
+void t_js_generator::parse_thrift_package_output_directory(const std::string& thrift_package_output_directory) {
+  thrift_package_output_directory_ = thrift_package_output_directory;
+  // Strip trailing '/'
+  if (!thrift_package_output_directory_.empty() && thrift_package_output_directory_[thrift_package_output_directory_.size() - 1] == '/') {
+    thrift_package_output_directory_ = thrift_package_output_directory_.substr(0, thrift_package_output_directory_.size() - 1);
+  }
+  // Check that the thrift_package_output_directory is not empty after stripping
+  if (thrift_package_output_directory_.empty()) {
+    throw std::invalid_argument("the thrift_package_output_directory argument must not be empty");
+  } else {
+    gen_episode_file_ = true;
+  }
+}
+
 THRIFT_REGISTER_GENERATOR(js,
                           "Javascript",
                           "    jquery:          Generate jQuery compatible code.\n"
                           "    node:            Generate node.js compatible code.\n"
                           "    ts:              Generate TypeScript definition files.\n"
                           "    with_ns:         Create global namespace objects when using node.js\n"
-                          "    es6:             Create ES6 code with Promises\n")
+                          "    es6:             Create ES6 code with Promises\n"
+                          "    thrift_package_output_directory=<path>:\n"
+                          "                     Generate episode file and use the <path> as prefix\n"
+                          "    imports=<paths_to_modules>:\n"
+                          "                     ':' separated list of paths of modules that has episode files in their root\n")
diff --git a/compiler/cpp/src/thrift/main.cc b/compiler/cpp/src/thrift/main.cc
index 5b69dc5..03e0d6f 100644
--- a/compiler/cpp/src/thrift/main.cc
+++ b/compiler/cpp/src/thrift/main.cc
@@ -982,6 +982,7 @@ void parse(t_program* program, t_program* parent_program) {
 void generate(t_program* program, const vector<string>& generator_strings) {
   // Oooohh, recursive code generation, hot!!
   if (gen_recurse) {
+    program->set_recursive(true);
     const vector<t_program*>& includes = program->get_includes();
     for (auto include : includes) {
       // Propagate output path from parent to child programs
@@ -1017,6 +1018,8 @@ void generate(t_program* program, const vector<string>& generator_strings) {
     failure("Error: %s\n", s.c_str());
   } catch (const char* exc) {
     failure("Error: %s\n", exc);
+  } catch (const std::invalid_argument& invalid_argument_exception) {
+    failure("Error: %s\n", invalid_argument_exception.what());
   }
 }
 
diff --git a/compiler/cpp/src/thrift/parse/t_program.h b/compiler/cpp/src/thrift/parse/t_program.h
index 13cb26e..140dc35 100644
--- a/compiler/cpp/src/thrift/parse/t_program.h
+++ b/compiler/cpp/src/thrift/parse/t_program.h
@@ -58,9 +58,9 @@
 class t_program : public t_doc {
 public:
   t_program(std::string path, std::string name)
-    : path_(path), name_(name), out_path_("./"), out_path_is_absolute_(false), scope_(new t_scope) {}
+    : path_(path), name_(name), out_path_("./"), out_path_is_absolute_(false), scope_(new t_scope), recursive_(false) {}
 
-  t_program(std::string path) : path_(path), out_path_("./"), out_path_is_absolute_(false) {
+  t_program(std::string path) : path_(path), out_path_("./"), out_path_is_absolute_(false), recursive_(false) {
     name_ = program_name(path);
     scope_ = new t_scope();
   }
@@ -355,6 +355,10 @@ public:
 
   const std::vector<std::string>& get_c_includes() const { return c_includes_; }
 
+  void set_recursive(const bool recursive) { recursive_ = recursive; }
+
+  bool get_recursive() const { return recursive_; }
+
 private:
   // File path
   std::string path_;
@@ -400,6 +404,9 @@ private:
 
   // C extra includes
   std::vector<std::string> c_includes_;
+
+  // Recursive code generation
+  bool recursive_;
 };
 
 #endif
diff --git a/lib/nodejs/Makefile.am b/lib/nodejs/Makefile.am
index 091f768..71068b5 100755
--- a/lib/nodejs/Makefile.am
+++ b/lib/nodejs/Makefile.am
@@ -34,6 +34,8 @@ check: deps
 clean-local:
 	$(RM) -r test/gen-*
 	$(RM) -r $(top_srcdir)/node_modules
+	$(RM) -r test/episodic-code-generation-test/gen*
+	$(RM) -r test/episodic-code-generation-test/node_modules
 
 EXTRA_DIST = \
 	examples \
diff --git a/lib/nodejs/test/episodic-code-generation-test/client.js b/lib/nodejs/test/episodic-code-generation-test/client.js
new file mode 100644
index 0000000..55dc702
--- /dev/null
+++ b/lib/nodejs/test/episodic-code-generation-test/client.js
@@ -0,0 +1,77 @@
+#!/usr/bin/env node
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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
+ *
+ *   http://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 test = require("tape");
+const thrift = require("thrift");
+const program = require("commander");
+
+program
+  .option("--host <host>", "Set the thrift server host to connect", "localhost")
+  .option("--port <port>", "Set the thrift server port number to connect", 9090)
+  .parse(process.argv);
+
+const Service = require("./gen-2/second-episode/gen-nodejs/Service");
+const Types = require("types-package/first-episode/Types_types");
+
+const host = program.host;
+const port = program.port;
+
+const options = {
+  transport: thrift.TBufferedTransport,
+  protocol: thrift.TJSONProtocol
+};
+
+const connection = thrift.createConnection(host, port, options);
+const testDriver = function(client, callback) {
+  test("NodeJS episodic compilation client-server test", function(assert) {
+    const type1Object = new Types.Type1();
+    type1Object.number = 42;
+    type1Object.message = "The answer";
+    client.testEpisode(type1Object, function(err, response) {
+      assert.error(err, "no callback error");
+      assert.equal(response.number, type1Object.number + 1);
+      assert.equal(
+        response.message,
+        type1Object.message + " [Hello from the server]"
+      );
+      assert.end();
+      callback("Server successfully tested");
+    });
+  });
+};
+
+connection.on("error", function(err) {
+  assert(false, err);
+});
+
+const client = thrift.createClient(Service, connection);
+
+runTests();
+
+function runTests() {
+  testDriver(client, function(status) {
+    console.log(status);
+    connection.destroy();
+  });
+}
+
+exports.expressoTest = function() {};
diff --git a/lib/nodejs/test/episodic-code-generation-test/episodic_compilation.package.json b/lib/nodejs/test/episodic-code-generation-test/episodic_compilation.package.json
new file mode 100644
index 0000000..7a78b4b
--- /dev/null
+++ b/lib/nodejs/test/episodic-code-generation-test/episodic_compilation.package.json
@@ -0,0 +1,3 @@
+{
+    "name": "types-package"
+}
diff --git a/lib/nodejs/test/episodic-code-generation-test/server.js b/lib/nodejs/test/episodic-code-generation-test/server.js
new file mode 100644
index 0000000..2917b68
--- /dev/null
+++ b/lib/nodejs/test/episodic-code-generation-test/server.js
@@ -0,0 +1,50 @@
+#!/usr/bin/env node
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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
+ *
+ *   http://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 thrift = require("../../lib/thrift");
+const program = require("commander");
+
+program
+  .option("--port <port>", "Set the thrift server port", 9090)
+  .parse(process.argv);
+
+const Service = require("./gen-2/second-episode/gen-nodejs/Service");
+const Types = require("types-package/first-episode/Types_types");
+
+const port = program.port;
+
+const options = {
+  transport: thrift.TBufferedTransport,
+  protocol: thrift.TJSONProtocol
+};
+
+const ServiceHandler = {
+  testEpisode: function(receivedType1Object) {
+    const type1Object = new Types.Type1();
+    type1Object.number = receivedType1Object.number + 1;
+    type1Object.message =
+      receivedType1Object.message + " [Hello from the server]";
+    return type1Object;
+  }
+};
+
+const server = thrift.createServer(Service, ServiceHandler, options);
+server.listen(port);
diff --git a/lib/nodejs/test/testAll.sh b/lib/nodejs/test/testAll.sh
index e98b198..3ae88b3 100755
--- a/lib/nodejs/test/testAll.sh
+++ b/lib/nodejs/test/testAll.sh
@@ -23,6 +23,12 @@ fi
 
 DIR="$( cd "$( dirname "$0" )" && pwd )"
 
+EPISODIC_DIR=${DIR}/episodic-code-generation-test
+
+THRIFT_FILES_DIR=${DIR}/../../../test
+
+THRIFT_COMPILER=${DIR}/../../../compiler/cpp/thrift
+
 ISTANBUL="$DIR/../../../node_modules/istanbul/lib/cli.js"
 
 REPORT_PREFIX="${DIR}/../coverage/report"
@@ -54,26 +60,68 @@ testServer()
   return $RET
 }
 
+testEpisodicCompilation()
+{
+  RET=0
+  if [ -n "${COVER}" ]; then
+    ${ISTANBUL} cover ${EPISODIC_DIR}/server.js --dir ${REPORT_PREFIX}${COUNT} --handle-sigint &
+    COUNT=$((COUNT+1))
+  else
+    node ${EPISODIC_DIR}/server.js &
+  fi
+  SERVERPID=$!
+  sleep 0.1
+  if [ -n "${COVER}" ]; then
+    ${ISTANBUL} cover ${EPISODIC_DIR}/client.js --dir ${REPORT_PREFIX}${COUNT} || RET=1
+    COUNT=$((COUNT+1))
+  else
+    node ${EPISODIC_DIR}/client.js || RET=1
+  fi
+  kill -2 $SERVERPID || RET=1
+  wait $SERVERPID
+  return $RET
+}
+
 
 TESTOK=0
 
-#generating thrift code
+# generating Thrift code
 
-${DIR}/../../../compiler/cpp/thrift -o ${DIR} --gen js:node ${DIR}/../../../test/ThriftTest.thrift
-${DIR}/../../../compiler/cpp/thrift -o ${DIR} --gen js:node ${DIR}/../../../test/JsDeepConstructorTest.thrift
-${DIR}/../../../compiler/cpp/thrift -o ${DIR} --gen js:node ${DIR}/../../../test/Int64Test.thrift
+${THRIFT_COMPILER} -o ${DIR} --gen js:node ${THRIFT_FILES_DIR}/ThriftTest.thrift
+${THRIFT_COMPILER} -o ${DIR} --gen js:node ${THRIFT_FILES_DIR}/JsDeepConstructorTest.thrift
+${THRIFT_COMPILER} -o ${DIR} --gen js:node ${THRIFT_FILES_DIR}/Int64Test.thrift
 mkdir ${DIR}/gen-nodejs-es6
-${DIR}/../../../compiler/cpp/thrift -out ${DIR}/gen-nodejs-es6 --gen js:node,es6 ${DIR}/../../../test/ThriftTest.thrift
-${DIR}/../../../compiler/cpp/thrift -out ${DIR}/gen-nodejs-es6 --gen js:node,es6 ${DIR}/../../../test/JsDeepConstructorTest.thrift
-${DIR}/../../../compiler/cpp/thrift -out ${DIR}/gen-nodejs-es6 --gen js:node,es6 ${DIR}/../../../test/Int64Test.thrift
+${THRIFT_COMPILER} -out ${DIR}/gen-nodejs-es6 --gen js:node,es6 ${THRIFT_FILES_DIR}/ThriftTest.thrift
+${THRIFT_COMPILER} -out ${DIR}/gen-nodejs-es6 --gen js:node,es6 ${THRIFT_FILES_DIR}/JsDeepConstructorTest.thrift
+${THRIFT_COMPILER} -out ${DIR}/gen-nodejs-es6 --gen js:node,es6 ${THRIFT_FILES_DIR}/Int64Test.thrift
+
+# generate episodic compilation test code
+TYPES_PACKAGE=${EPISODIC_DIR}/node_modules/types-package
+
+# generate the first episode
+mkdir --parents ${EPISODIC_DIR}/gen-1/first-episode
+${THRIFT_COMPILER} -o ${EPISODIC_DIR}/gen-1/first-episode --gen js:node,thrift_package_output_directory=first-episode ${THRIFT_FILES_DIR}/Types.thrift
+
+# create a "package" from the first episode and "install" it, the episode file must be at the module root
+mkdir --parents ${TYPES_PACKAGE}/first-episode
+cp --force ${EPISODIC_DIR}/episodic_compilation.package.json ${TYPES_PACKAGE}/package.json
+cp --force ${EPISODIC_DIR}/gen-1/first-episode/gen-nodejs/Types_types.js ${TYPES_PACKAGE}/first-episode/
+cp --force ${EPISODIC_DIR}/gen-1/first-episode/gen-nodejs/thrift.js.episode ${TYPES_PACKAGE}
+
+# generate the second episode
+mkdir --parents ${EPISODIC_DIR}/gen-2/second-episode
+${THRIFT_COMPILER} -o ${EPISODIC_DIR}/gen-2/second-episode --gen js:node,imports=${TYPES_PACKAGE} ${THRIFT_FILES_DIR}/Service.thrift
+if [ -f ${EPISODIC_DIR}/gen-2/second-episode/Types_types.js ]; then
+  TESTOK=1
+fi
 
-#unit tests
+# unit tests
 
 node ${DIR}/binary.test.js || TESTOK=1
 node ${DIR}/int64.test.js || TESTOK=1
 node ${DIR}/deep-constructor.test.js || TESTOK=1
 
-#integration tests
+# integration tests
 
 for type in tcp multiplex websocket http
 do
@@ -91,6 +139,8 @@ do
   done
 done
 
+# episodic compilation test
+testEpisodicCompilation
 
 if [ -n "${COVER}" ]; then
   ${ISTANBUL} report --dir "${DIR}/../coverage" --include "${DIR}/../coverage/report*/coverage.json" lcov cobertura html
diff --git a/test/Service.thrift b/test/Service.thrift
new file mode 100644
index 0000000..6f4c9c7
--- /dev/null
+++ b/test/Service.thrift
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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
+ *
+ *   http://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.
+ */
+
+include "Types.thrift"
+
+service Service {
+  Types.Type1 testEpisode(1:Types.Type1 arg)
+}
diff --git a/test/Types.thrift b/test/Types.thrift
new file mode 100644
index 0000000..11069d9
--- /dev/null
+++ b/test/Types.thrift
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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
+ *
+ *   http://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.
+ */
+
+struct Type1 {
+  1: i32 number,
+  2: string message,
+}
\ No newline at end of file