Skip to content

Commit

Permalink
Add code comments to server compiling code
Browse files Browse the repository at this point in the history
  • Loading branch information
Blaise Kal committed Jan 14, 2013
1 parent c4be391 commit da52219
Show file tree
Hide file tree
Showing 2 changed files with 125 additions and 11 deletions.
125 changes: 114 additions & 11 deletions build/lib/server_compile.js
Expand Up @@ -11,10 +11,15 @@ var fs = require('fs'),
* @constructor
*/
function ServerCompile() {
// The file that you would pass as the first parameter to node.
this.mainFile = __dirname + '/../../server/start.js';

// Files that should not be compiled.
this.excludeFiles = [
'config.example.js'
];

// Directories containing all node.js module files that should be compiled.
this.moduleDirs = [
__dirname + '/../../server/shared/',
__dirname + '/../../server/lib/'
Expand All @@ -29,14 +34,24 @@ module.exports = ServerCompile;

ServerCompile.prototype = {

// String that contains all processed code.
code: '',

// Array containing all files that should be compiled.
files: [],

// Array containing all required modules so far.
requiredStack: [],

// Array containing all node.js core modules.
coreModules: [],

/**
* Get all files configured in this.moduleDirs, and
* push them to the this.files array. Exclude any files
* that are in the this.excludeFiles array.
* !!IMPORTANT: All files should have a unique name.
*/
populateFiles: function() {
for (var i = 0, m = this.moduleDirs.length; i < m; i++) {
var files = fs.readdirSync(this.moduleDirs[i]);
Expand All @@ -48,9 +63,22 @@ ServerCompile.prototype = {
}
},

/**
* Inline require() wrapper function. It starts with the main file. This
* function reads the contents of that file and replaces every require()
* call with the contents of the JS module file that is being required.
* I call this process "inlining". This function performs the replace
* recursively, so if the inlined code contains a require() call, it will
* be replaced in the next iteration, unless it has already been inlined.
*/
inlineRequires: function() {
// Regular Expression for finding "var x = require('file.js');".
var requireRegExp = /[\w ]+ = require\('[\w\.\/_]+'\)(,|;)/g;

// Get the JS code from the main file.
this.code = String(fs.readFileSync(this.mainFile));

// Recursively replace all require() calls until they are all inlined.
while (requireRegExp.test(this.code)) {
var matches = this.code.match(requireRegExp);
for (var i = 0, m = matches.length; i < m; i++) {
Expand All @@ -59,71 +87,146 @@ ServerCompile.prototype = {
}
},

/**
* This function performs the actual replace of the require() string
* to the contents of the module's JS contents.
* @param {string} requireCall
*/
inlineRequire: function(requireCall) {
var moduleName, varName;
var moduleName, varName, repl;

// Transform the entire "var x = require('module.js');" call to
// "module.js", or "var http = require('http');" to "http".
moduleName = requireCall.match(/\/?[\w\.]+'/g)[0];
moduleName = moduleName.replace(/[\/']/g, '');

// Get the variable name to which the require() call was asigned.
// !!IMPORTANT (and that's why this library is a hack): Make sure that
// (A) you assign the same require() calls consistently to the same
// variable, and (B) make sure that you never assign two different
// modules to the same variable across your entire project. If you
// ignore A, you will get duplicate code. If you ignore B, you will
// get errors because one library was ignored. This is because we have
// one global namespace after we have inlined all require() modules.
varName = this.getVarName(requireCall);

// Module is already inlined
// Our array this.requiredStack contains a list of all files that have
// already been inlined. We only need to inline every file once, since
// they will all be added to the global namespace. This snippet
// will add a comment if the require() call was already inlined.
if (-1 !== this.requiredStack.indexOf(moduleName)) {
var repl = util.format('// %s (already inlined)', moduleName);
repl = util.format('// ALREADY INLINED: %s', moduleName);
this.code = this.code.replace(requireCall, repl);
}

// Core module
// Handle core modules. We detect a core module by checking if the
// require() call contains ".js". We do not inline core modules because
// they are too complex to inline. This snippet will comment out the
// require call, and add the core module to a list of required core
// modules. This list of core modules is then appended to the code AFTER
// compiling.
else if (!moduleName.match(/\.js/)) {
// Append to list of core modules
var coreModule = util.format(
'%s = require(\'%s\')', varName, moduleName
);
// Remove from code; core modules should not be compiled
this.code = this.code.replace(requireCall, '');
repl = util.format('// CORE MODULE: %s', moduleName);
// Comment-out core modules should not be compiled
this.code = this.code.replace(requireCall, repl);
this.requiredStack.push(moduleName);
this.coreModules.push(coreModule);
}

// Inline module
// Inline a module. This segment matches the module name with our list
// of files. If a match is found, we know the exact path where the file
// can be included from, so that we can get its contents.
else {
var jsContent, filePath = this.getFilePath(moduleName);

// Check if the module exists. If it doesn't, make sure to
// add the files to the this.files array.
if (!filePath) {
console.log('Module not found:', moduleName);
console.log('ERROR: Module not found:', moduleName);
return;
}

// Get the contents of the file
jsContent = String(fs.readFileSync(filePath));

// If module exports an object literal, use the reference name
// for inlining.
// Remove the module.exports piece from the module's JS:

// If the module exports an object literal, use the variable name
// to which the require() call was assigned as a prefix for the
// inlined code.
// Example module:
// module.exports = {foo:'bar'};
// Example require:
// var config = require('module.js');
// Result:
// var config = {foo:'bar'};
if (jsContent.match(/module\.exports = [^\w]/g)) {
jsContent = jsContent.replace(
/module\.exports =/,
util.format('var %s = ', varName)
);
} else {
// Module defines a named object. Comment-out the export.
// If the module exports a reference to a function, we ignore
// the variable name that was used for the require() call.
// Example module:
// var MyObject = function(){};
// module.exports = MyObject;
// Example require:
// var config = require('module.js');
// Result:
// var MyObject = function(){};
jsContent = jsContent.replace(/(module\.exports = .*)/, '// $1');
}

// !!IMPORTANT: This library currently does not support fragmented
// exports. If you use this library, make sure you have a single
// module.exports assignment.

// Add a comment to the pre-compiled code for debugging
jsContent = util.format('// INLINED: %s\n%s\n', moduleName, jsContent);

// Replace the require call with the contents of the module file
// that we have just adjusted a bit to remove the module.exports.
this.code = this.code.replace(requireCall, jsContent);
this.requiredStack.push(moduleName);
}
},

/**
* Clean up the code, handle an XSSNAKE-specific issue for
* files that are used by both the client and the server
*/
cleanCode: function() {
this.code = this.code.replace(/'use strict';/g, '');
this.code = this.code.replace(/XSS\.[\w]+ = module\.exports;/g, '');
},

/**
* Get the variable name from a require() call.
* Input:
* var foo = require('module.js');
* Output:
* foo
* @param {string} req
* @return {string}
*/
getVarName: function(req) {
return req.replace('var ', '').match(/([\w]+)/g)[0];
},

/**
* Get the file path for a module.
* Input:
* foo.js
* Output:
* /path/to/foo.js
* @param mod
* @return {*}
*/
getFilePath: function(mod) {
var files = this.files;
for (var i = 0, m = files.length; i < m; i++) {
Expand Down
11 changes: 11 additions & 0 deletions build/server.js
Expand Up @@ -6,8 +6,12 @@ var fs = require('fs');
var ServerCompile = require('./lib/server_compile.js');
/** @type {GccRest} */
var gcc = require('gcc-rest');

// Start compiling out server code.
var compile = new ServerCompile();

// Set Google Closure Compiler parameters. Make sure to add externs for all
// core node.js modules that you use!
gcc.params({
js_externs : String(fs.readFileSync(__dirname + '/lib/externs.js')),
output_info : ['compiled_code', 'errors', 'warnings', 'statistics'],
Expand All @@ -17,14 +21,21 @@ gcc.params({
warning_level : 'VERBOSE'
});

// Add a header to the compiled output.
gcc.header(util.format(
'// © %d Blaise Kal\n' +
'// Compiled using Google Closure Compiler on %s\n\n' +
'var %s;\n\n',
new Date().getFullYear(),
new Date().toUTCString(),

// Re-include all calls to core node.js modules.
compile.coreModules.join(',\n ')
));

// DEBUG: Uncomment the next line to write the inlined code to a file before
// it is compoiled by Google Closure Compiler.
fs.writeFileSync(__dirname + '/precompiled.js', compile.code);

gcc.addCode(compile.code);
gcc.output(__dirname + '/../server/compiled_start.js');

0 comments on commit da52219

Please sign in to comment.