blob: 2719c3ab2750b55b11507f417d03cc88cf627c4a [file] [log] [blame]
// Copyright 2012 Selenium committers
//
// 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
//
// 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.
/**
* @fileoverview Script used to prepare WebDriverJS as a Node module.
*/
'use strict';
var assert = require('assert'),
fs = require('fs'),
path = require('path'),
vm = require('vm');
var optparse = require('./optparse');
var CLOSURE_BASE_REGEX = /^var goog = goog \|\| \{\};/;
var REQUIRE_REGEX = /^goog\.require\s*\(\s*[\'\"]([^\)]+)[\'\"]\s*\);?$/;
var PROVIDE_REGEX = /^goog\.provide\s*\(\s*[\'\"]([^\)]+)[\'\"]\s*\);?$/;
/**
* Map of file paths to a hash of what symbols that file provides and requires.
* @type {!Object.<{provides: !Array.<string>,
* requires: !Array.<string>}>}
*/
var FILE_INFO = {};
/**
* Map of symbol to path of the file that provides it.
* @type {!Object.<string>}
*/
var PROVIDERS = {};
/**
* Map of unprovided symbols to a list of files that require it.
* @type {!Object.<!Array.<string>>}
*/
var UNPROVIDED = {};
/**
* Maps file paths to their location in the lib/ directory.
* @type {!Object.<string>}
*/
var CONTENT_MAP = {};
/**
* Records a dependency on a symbol from a specific file.
* @param {string} path Path to the file that requires the symbol.
* @param {string} symbol The provided symbol.
*/
function addRequiredEdge(path, symbol) {
var provider = PROVIDERS[symbol];
if (!provider) {
UNPROVIDED[symbol] = UNPROVIDED[symbol] || [];
UNPROVIDED[symbol].push(path);
}
}
/**
* Records a symbol as being provided by a specific file.
* @param {string} path Path to the file that provides the symbol.
* @param {string} symbol The provided symbol.
*/
function updateProviders(path, symbol) {
var provider = PROVIDERS[symbol];
if (provider) {
throw Error('Duplicate provide: ' + symbol + ':' +
'\n ' + provider + '\n ' + path);
}
PROVIDERS[symbol] = path;
var pendingRequires = UNPROVIDED[symbol];
if (pendingRequires) {
delete UNPROVIDED[symbol];
pendingRequires.forEach(function(path) {
addRequiredEdge(path, symbol);
});
}
}
/**
* Parses a file for closure dependency info.
* @param {string} path Path to the file to parse.
*/
function parseFile(path) {
var contents = fs.readFileSync(path, 'utf8');
var info = {provides: [], requires: []};
FILE_INFO[path] = info;
contents.split(/\n/).forEach(function(line) {
var match = line.match(REQUIRE_REGEX);
if (match) {
info.requires.push(match[1]);
addRequiredEdge(path, match[1]);
} else if (match = line.match(PROVIDE_REGEX)) {
info.provides.push(match[1]);
updateProviders(path, match[1]);
} else if (line.match(CLOSURE_BASE_REGEX)) {
updateProviders(path, 'goog');
}
});
}
/**
* @param {!Array.<string>} filePaths Paths to the library files to resolve.
* @param {!Array.<string>} contentRoots Paths for the content roots.
*/
function processLibraryFiles(filePaths, contentRoots) {
var seen = {};
filePaths.forEach(function(filePath) {
if (seen[filePath]) return;
seen[filePath] = 1;
if (fs.statSync(filePath).isDirectory()) {
expandDir(filePath);
} else {
processFile(filePath);
}
});
function processFile(filePath) {
assert.ok(contentRoots.some(function(root) {
if (filePath.substring(0, root.length) === root) {
CONTENT_MAP[filePath] = filePath.substring(root.length + 1);
return true;
}
}), 'File does not belong to a content root: ' + filePath);
parseFile(filePath);
}
function expandDir(dirPath) {
if (path.basename(dirPath) === '.svn') return;
fs.readdirSync(dirPath).forEach(function(file) {
file = path.join(dirPath, file);
if (fs.statSync(file).isDirectory()) {
expandDir(file);
} else if (file.substring(file.length - 3) === '.js') {
processFile(file);
}
});
}
}
/**
* @param {!Array.<string>} filePaths The src file paths.
* @param {string} outputDirPath Path to the directory to copy src files to.
* @return {!Array.<string>} The list of all required symbols.
*/
function processSrcs(filePaths, outputDirPath) {
var symbols = {};
var currentFile;
var closure = vm.createContext({
exports: {},
require: function(module) {
assert.strictEqual('./_base', module, 'You may only import "./_base"');
return {
require: function(namespace) {
assert.ok(!!PROVIDERS[namespace],
'Missing provider for ' + namespace +
' (in ' + currentFile + ')');
symbols[namespace] = 1;
return {};
}
};
}
});
filePaths.forEach(function(filePath) {
currentFile = filePath;
var contents = fs.readFileSync(filePath, 'utf8');
if (path.basename(filePath) !== '_base.js') {
vm.runInContext(contents, closure, filePath);
}
var dest = path.join(outputDirPath, path.basename(filePath));
fs.writeFileSync(dest, contents, 'utf8');
});
return Object.keys(symbols);
}
function copyLibraries(outputDirPath, symbols) {
// Always copy over Closure base.
var base = PROVIDERS['goog'];
var googDirPath = path.join(outputDirPath, 'lib', CONTENT_MAP[base]);
googDirPath = path.dirname(googDirPath);
copy(base);
var depsFileContents = [
'// This file has been auto-generated; do not edit by hand'
];
var seenSymbols = {};
var seenFiles = {};
symbols.forEach(resolveDeps);
var depsPath = path.join(googDirPath, 'deps.js');
fs.writeFileSync(depsPath, depsFileContents.join('\n') + '\n', 'utf8');
function resolveDeps(symbol) {
if (seenSymbols[symbol]) return;
seenSymbols[symbol] = true;
if (UNPROVIDED[symbol]) {
throw Error('Missing provider for ' + JSON.stringify(symbol) +
'; required in\n ' + UNPROVIDED[symbol].join('\n '));
}
var file = PROVIDERS[symbol];
if (seenFiles[file]) return;
seenFiles[file] = true;
copy(file);
var outputPath = pathFor(file);
var relativePath = path.relative(googDirPath, outputPath);
depsFileContents.push([
'goog.addDependency(',
JSON.stringify(relativePath), ', ',
JSON.stringify(FILE_INFO[file].provides), ', ',
JSON.stringify(FILE_INFO[file].requires),
');'
].join(''));
FILE_INFO[file].requires.forEach(resolveDeps);
}
function pathFor(file) {
return path.join(outputDirPath, 'lib', CONTENT_MAP[file]);
}
function copy(filePath) {
var dest = pathFor(filePath);
copyFile(filePath, dest);
}
}
function copyFile(src, dest) {
createDirectoryIfNecessary(path.dirname(dest));
var contents = fs.readFileSync(src, 'utf8');
fs.writeFileSync(dest, contents, 'utf8');
}
function createDirectoryIfNecessary(dirPath) {
var toCreate = [];
var current = dirPath;
while (!fs.existsSync(current)) {
toCreate.push(path.basename(current));
current = path.dirname(current);
}
while (toCreate.length) {
current = path.join(current, toCreate.pop());
fs.mkdirSync(current);
}
}
function copyResources(outputDirPath, resources) {
resources.forEach(function(resource) {
var parts = resource.split(':', 2);
var src = path.resolve(parts[0]);
var dest = outputDirPath;
var isAbsolute = path.resolve(parts[1]) === parts[1];
if (!isAbsolute) {
dest = path.join(dest, 'lib');
}
dest = path.join(dest, parts[1]);
copyFile(src, dest);
});
}
function main() {
var parser = new optparse.OptionParser().
path('output', { help: 'Path to the output directory' }).
path('src', {
help: 'Path to a module source file that should be copied to the ' +
'main output directory',
list: true
}).
path('lib', {
help: 'Path to a library file that should be copied to the lib/ ' +
'sub-directory. This file will only be copied over if it is ' +
'included in the transitive closure of a src file\'s ' +
'dependencies. If a directory is specified, it will be ' +
'recursively scanned for its .js files',
list: true
}).
path('root', {
help: 'A content root for mapping input files to their location under' +
' lib/. Each lib file will have its path stripped of any leading' +
' content roots befor being copied to the lib/ directory. Each ' +
'lib file must belong to a directory under a content root.',
list: true
}).
string('resource', {
help: 'A resource which should be copied into the final module, in ' +
'the form of a ":" colon separated pair, the first part ' +
'designating the source, and the second its destination. If ' +
'the destination path is absolute, it is relative to the ' +
'module root, otherwise it will be treated relative to the ' +
'lib/ directory.',
list: true
});
parser.parse();
var options = parser.options;
processLibraryFiles(options.lib, options.root);
var requiredSymbols = processSrcs(options.src, options.output);
copyLibraries(options.output, requiredSymbols);
copyResources(options.output, options.resource);
}
assert.strictEqual(module, require.main, 'This module may not be included');
main();