From 065adc255a5b3028f7608d14f94588088523732e Mon Sep 17 00:00:00 2001 From: guybedford Date: Tue, 15 Sep 2015 18:55:18 +0200 Subject: [PATCH] jspm 0.17 upgrades --- lib/deps-transformer.js | 98 +++ lib/node-conversion.js | 827 +++++++----------- lib/npm.js | 336 +++++-- package.json | 5 +- .../deps-transformer/process-detection1.js | 51 ++ .../deps-transformer/process-detection2.js | 3 + test/unit.js | 40 +- 7 files changed, 796 insertions(+), 564 deletions(-) create mode 100644 lib/deps-transformer.js create mode 100644 test/fixtures/deps-transformer/process-detection1.js create mode 100644 test/fixtures/deps-transformer/process-detection2.js diff --git a/lib/deps-transformer.js b/lib/deps-transformer.js new file mode 100644 index 0000000..7d1c919 --- /dev/null +++ b/lib/deps-transformer.js @@ -0,0 +1,98 @@ +var traceur = require('traceur'); + +var ParseTreeTransformer = traceur.get('codegeneration/ParseTreeTransformer.js').ParseTreeTransformer; +var ScopeTransformer = traceur.get('codegeneration/ScopeTransformer.js').ScopeTransformer; + + +var Script = traceur.get('syntax/trees/ParseTrees.js').Script; +var parseStatements = traceur.get('codegeneration/PlaceholderParser.js').parseStatements; +var STRING = traceur.get('syntax/TokenType.js').STRING; +var LiteralExpression = traceur.get('syntax/trees/ParseTrees.js').LiteralExpression; +var LiteralToken = traceur.get('syntax/LiteralToken.js').LiteralToken; + + +module.exports = function(source) { + var output = { requires: [], usesProcess: false, usesBuffer: false }; + + var compiler = new traceur.Compiler({ script: true }); + try { + var tree = compiler.parse(source); + } + catch(e) { + return output; + } + + var transformer; + + // sets output.requires + transformer = new CJSDepsTransformer(); + transformer.transformAny(tree); + output.requires = transformer.requires; + + // sets output.usesProcess + transformer = new GlobalUsageTransformer('process'); + transformer.transformAny(tree); + output.usesProcess = transformer.usesGlobal; + + if (transformer.processMember && !transformer.usesGlobal) + console.log('CONFLICt'); + + // sets output.usesBuffer + transformer = new GlobalUsageTransformer('Buffer'); + transformer.transformAny(tree); + output.usesBuffer = transformer.usesGlobal; + + return output; +}; + + +function CJSDepsTransformer() { + this.requires = []; + return ParseTreeTransformer.apply(this, arguments); +} +CJSDepsTransformer.prototype = Object.create(ParseTreeTransformer.prototype); + +CJSDepsTransformer.prototype.transformCallExpression = function(tree) { + if (!tree.operand.identifierToken || tree.operand.identifierToken.value != 'require') + return ParseTreeTransformer.prototype.transformCallExpression.call(this, tree); + + // found a require + var args = tree.args.args; + if (args.length && args[0].type == 'LITERAL_EXPRESSION' && args.length == 1) + this.requires.push(args[0].literalToken.processedValue); + + return ParseTreeTransformer.prototype.transformCallExpression.call(this, tree); +}; + +function GlobalUsageTransformer(varName) { + this.usesGlobal = false; + return ScopeTransformer.apply(this, arguments); +} +GlobalUsageTransformer.prototype = Object.create(ScopeTransformer.prototype); +GlobalUsageTransformer.prototype.transformIdentifierExpression = function(tree) { + if (tree.identifierToken.value == this.varName_) + this.usesGlobal = true; + return ScopeTransformer.prototype.transformIdentifierExpression.apply(this, arguments); +}; +GlobalUsageTransformer.prototype.sameTreeIfNameInLoopInitializer_ = function(tree) { + try { + tree = ScopeTransformer.prototype.sameTreeIfNameInLoopInitializer_.call(this, tree); + } + catch(e) {} + return tree; +}; +GlobalUsageTransformer.prototype.getDoNotRecurse = function(tree) { + var doNotRecurse; + try { + doNotRecurse = ScopeTransformer.prototype.getDoNotRecurse.call(this, tree); + } + catch(e) {} + return doNotRecurse; +}; +GlobalUsageTransformer.prototype.transformBlock = function(tree) { + try { + tree = ScopeTransformer.prototype.transformBlock.call(this, tree); + } + catch(e) {} + return tree; +}; \ No newline at end of file diff --git a/lib/node-conversion.js b/lib/node-conversion.js index 2733ed6..b5752b6 100644 --- a/lib/node-conversion.js +++ b/lib/node-conversion.js @@ -1,539 +1,390 @@ var Promise = require('rsvp').Promise; var asp = require('rsvp').denodeify; -var nodeSemver = require('semver'); var fs = require('graceful-fs'); var path = require('path'); -var glob = require('glob'); -var npmResolve = require('resolve'); - -var nodeBuiltins = { - 'assert': 'github:jspm/nodelibs-assert@^0.1.0', - 'buffer': 'github:jspm/nodelibs-buffer@^0.1.0', - 'child_process': 'github:jspm/nodelibs-child_process@^0.1.0', - 'cluster': 'github:jspm/nodelibs-cluster@^0.1.0', - 'console': 'github:jspm/nodelibs-console@^0.1.0', - 'constants': 'github:jspm/nodelibs-constants@^0.1.0', - 'crypto': 'github:jspm/nodelibs-crypto@^0.1.0', - 'dgram': 'github:jspm/nodelibs-dgram@^0.1.0', - 'dns': 'github:jspm/nodelibs-dns@^0.1.0', - 'domain': 'github:jspm/nodelibs-domain@^0.1.0', - 'events': 'github:jspm/nodelibs-events@^0.1.1', - 'fs': 'github:jspm/nodelibs-fs@^0.1.0', - 'http': 'github:jspm/nodelibs-http@^1.7.0', - 'https': 'github:jspm/nodelibs-https@^0.1.0', - 'module': 'github:jspm/nodelibs-module@^0.1.0', - 'net': 'github:jspm/nodelibs-net@^0.1.0', - 'os': 'github:jspm/nodelibs-os@^0.1.0', - 'path': 'github:jspm/nodelibs-path@^0.1.0', - 'process': 'github:jspm/nodelibs-process@^0.1.0', - 'punycode': 'github:jspm/nodelibs-punycode@^0.1.0', - 'querystring': 'github:jspm/nodelibs-querystring@^0.1.0', - 'readline': 'github:jspm/nodelibs-readline@^0.1.0', - 'repl': 'github:jspm/nodelibs-repl@^0.1.0', - 'stream': 'github:jspm/nodelibs-stream@^0.1.0', - 'string_decoder': 'github:jspm/nodelibs-string_decoder@^0.1.0', - 'sys': 'github:jspm/nodelibs-util@^0.1.0', - 'timers': 'github:jspm/nodelibs-timers@^0.1.0', - 'tls': 'github:jspm/nodelibs-tls@^0.1.0', - 'tty': 'github:jspm/nodelibs-tty@^0.1.0', - 'url': 'github:jspm/nodelibs-url@^0.1.0', - 'util': 'github:jspm/nodelibs-util@^0.1.0', - 'vm': 'github:jspm/nodelibs-vm@^0.1.0', - 'zlib': 'github:jspm/nodelibs-zlib@^0.1.0' +var url = require('url'); +var readdirp = require('readdirp'); + +var nodeCoreModules = { + 'assert': 'npm:assert@1.3.0', + 'buffer': 'npm:buffer@^3.4.3', + 'child_process': '@node/child_process', + 'cluster': '@node/cluster', + 'console': 'npm:console-browserify@^1.1.0', + 'constants': 'npm:constants-browserify@^1.0.0', + 'crypto': 'npm:crypto-browserify@^3.9.14', + 'dgram': '@node/dgram', + 'dns': '@node/dns', + 'domain': 'npm:domain-browser@^1.1.4', + 'events': 'npm:events@^1.0.2', + 'fs': 'nodelibs/fs', + 'http': 'npm:stream-http@^1.5.0', + 'https': 'npm:https-browserify@0.0.1', + 'module': '@node/module', + 'net': 'npm:net-browserify@^0.2.1', + 'os': 'npm:os-browserify@^0.1.2', + 'path': 'npm:path-browserify@^0.0.0', + 'process': 'npm:process@^0.11.2', + 'punycode': 'npm:punycode@^1.3.2', + 'querystring': 'npm:querystring@^0.2.0', + 'readline': '@node/readline', + 'repl': '@node/repl', + 'stream': 'npm:stream-browserify@^2.0.1', + 'string_decoder': 'npm:string_decoder@^0.10.31', + 'sys': 'npm:util@^0.10.3', + 'timers': 'npm:timers-browserify@^1.4.1', + 'tls': 'npm:tls-browserify@^0.1.3', + 'tty': 'npm:tty-browserify@^0.0.0', + 'url': 'npm:url@^0.11.0', + 'util': 'npm:util@^0.10.3', + 'vm': 'npm:vm-browserify@^0.0.4', + 'zlib': 'npm:browserify-zlib@^0.1.4' }; -var jsonPlugin = exports.jsonPlugin = 'github:systemjs/plugin-json@^0.1.0'; - -exports.convertPackage = function(pjson, dir) { +exports.convertPackage = function(pjson, dir, ui) { var packageName = pjson.name; - // prepare any aliases we need to create - var aliases = {}; - - if (typeof pjson.browser == 'object') { - var curAlias; - var curTarget; - for (var module in pjson.browser) { - curAlias = module; - curTarget = pjson.browser[module]; - - if (typeof curTarget != 'string') - continue; - - // only looking at local aliases here - if (curAlias.substr(0, 2) != './') - continue; - - if (curAlias.substr(0, 2) == './') - curAlias = curAlias.substr(2); - if (curAlias.substr(curAlias.length - 3, 3) == '.js') - curAlias = curAlias.substr(0, curAlias.length - 3); - - if (curTarget.substr(curTarget.length - 3, 3) == '.js') - curTarget = curTarget.substr(0, curTarget.length - 3); - - aliases[curAlias] = curTarget; - } - } - - var buildErrors = []; - var newDeps = {}; - - return asp(glob)(dir + path.sep + '**' + path.sep + '*.js') - .then(function(files) { - - // we store the list of directory files to make - // only writing after this step to avoid incorrect internal resolutions - var directoryFiles = []; - - return Promise.all(files.map(function(file) { - var filename = path.relative(dir, file).replace(/\\/g, '/'); - - // skip files in the ignore paths - // NB this can be removed with https://github.com/jspm/jspm-cli/issues/345 - if (pjson.ignore) { - if (pjson.ignore.some(function(path) { - return filename.substr(0, path.length) == path && (filename.substr(path.length, 1) == '/' || filename.substr(path.length, 1) === ''); - })) - return; + // if already configured for SystemJS, then use that + if (pjson.systemjs) + return pjson; + + var packageConfig = { dependencies: {} }; + + // check every file in this package and return the file structure + // and all internal require('x') statements that are made + return new Promise(function(resolve, reject) { + var fileTree = {}; + readdirp({ root: dir, entryType: 'both' }, function(entry) { + var listingName = entry.path; + if (entry.stat.isDirectory()) + listingName += '/'; + fileTree[listingName] = true; + }, function(err) { + if (err) + reject(err); + else + resolve(fileTree); + }); + }) + .then(function(fileTree) { + // format + if (pjson.format && pjson.format != 'cjs') + this.ui.log('warn', 'Package `' + packageName + '` has a format set, which is being ignored by the jspm Node resolution conversion.\n' + + 'Set %jspmNodeConversion: false% in the packge config for the package to skip this process.'); + + // defaultExtension + if (pjson.defaultExtension && pjson.defaultExtension != 'js') + this.ui.log('warn', 'Package `' + packageName + '` has a defaultExtension set, which is being ignored by the jspm Node resolution conversion.\n' + + 'Set %jspmNodeConversion: false% in the packge config for the package to skip this process.'); + + // main + packageConfig.main = nodeResolve(typeof pjson.main == 'string' && pjson.main || 'index.js', '', fileTree); + + // format + packageConfig.format = 'cjs'; + + // meta + packageConfig.meta = { + '*.json': { + loader: 'nodelibs/json' + } + }; + for (var m in pjson.meta) + packageConfig.meta[m] = pjson.meta[m]; + + // map + packageConfig.map = {}; + for (var m in pjson.map) + packageConfig.map[m] = pjson.map[m]; + + // browser main mapping + var browserMain = nodeResolve(typeof pjson.browser == 'string' && pjson.browser || typeof pjson.browserify == 'string' && pjson.browserify || packageConfig.main, '', fileTree); + if (browserMain != packageConfig.main && !packageConfig.map[packageConfig.main]) + packageConfig.map['./' + packageConfig.main] = { + browser: './' + browserMain + }; + + // convert pjson browser -> map config + if (typeof pjson.browser == 'object') + for (var b in pjson.browser) { + // dont replace any existing map config + if (packageConfig.map[b]) + continue; + + var mapping = pjson.browser[b]; + + if (mapping === false) + mapping = '@empty'; + else if (typeof mapping == 'string') + mapping = './' + nodeResolve(mapping, '', fileTree); + else + continue; + + packageConfig.map[b] = { + browser: mapping + }; } - filename = filename.substr(0, filename.length - 3); - var source; - var changed = false; - - return Promise.resolve() - - .then(function() { - // if its an "index.js" file, then check if we can create a directory shortcut for it - var parts = filename.split('/'); - if (parts.pop() != 'index') - return; - var dirName = parts.join(path.sep); - var dirModule = path.resolve(dir, dirName) + '.js'; - return new Promise(function(resolve, reject) { - fs.exists(dirModule, resolve); - }) - .then(function(exists) { - if (exists) - return; - directoryFiles.push(dirModule); - }); - }) - .then(function() { - return asp(fs.readFile)(file); + var coreDeps = []; + + /* + * Comprehensive internal resolution differences between SystemJS and Node + * for internal package requires (that is ignoring package.json, node_modules) + * + * 1. Directory requires won't resolve to index.js in the directory + * 2. Files not ending in js, that are js will add extensions + * 3. A file by the name file.js.js loaded as file.js will not add extensions + * 4. A file by the name file.json.js loaded as file.json will not add extensions + * 5. Browserify mappings will affect folders by the same name (./file.js/...) + * + * Currently we cater to (1 - 4) above by parsing all CommonJS requires of all JS files in + * the package and where a resolution matches one of these cases, we include meta: true + * config for files and maps to the index.js for directories. + * + * It may turn out to be useful to do this for external requires as well, in which + * case we can switch this algorithm to a comprehensive correction configuration + * being constructed against the entire fileTree to handle all resolution differences. + */ + var resolutionMap = {}; + var metas = {}; + return Promise.all(Object.keys(fileTree).filter(function(file) { + return file[file.length - 1] != '/'; + }).map(function(fileName) { + + var meta = readMeta(fileName, packageConfig.meta); + + // skip parsing files set to a non-cjs format + if (meta.format && meta.format != 'cjs') + return; + + return parseNodeRequires(path.resolve(dir, fileName)) + .catch(function(err) { + err.stack = 'Error parsing ' + fileName + '\n' + err.stack; + throw err; }) - - .then(function(_source) { - source = _source.toString(); - - // at this point, only alter the source file if we're certain it is CommonJS in Node-style - - // first check if we have format meta - var meta = source.match(metaRegEx); - var metadata = {}; - if (meta) { - var metaParts = meta[0].match(metaPartRegEx); - for (var i = 0; i < metaParts.length; i++) { - var len = metaParts[i].length; - - var firstChar = metaParts[i].substr(0, 1); - if (metaParts[i].substr(len - 1, 1) == ';') - len--; - - if (firstChar != '"' && firstChar != "'") - continue; - - var metaString = metaParts[i].substr(1, metaParts[i].length - 3); - - var metaName = metaString.substr(0, metaString.indexOf(' ')); - if (metaName) { - var metaValue = metaString.substr(metaName.length + 1, metaString.length - metaName.length - 1); - - if (metadata[metaName] instanceof Array) - metadata[metaName].push(metaValue); - else - metadata[metaName] = metaValue; - } - } - } - - if (pjson.format != 'cjs' && !metadata.format) - return; - - if (metadata.format && metadata.format != 'cjs') - return; - - if (pjson.shim && pjson.shim[filename]) - return; - - if (source.match(cmdCommentRegEx)) - source = '//' + source; - - // Note an alternative here would be to use https://github.com/substack/insert-module-globals - var usesBuffer = source.match(bufferRegEx), usesProcess = source.match(processRegEx); - - // the buffer and process nodelibs modules themselves don't wrap themselves - if (pjson.name == 'buffer') - usesBuffer = false; - if (pjson.name == 'process') - usesProcess = false; - - if (usesBuffer || usesProcess) { - changed = true; - source = "(function(" + (usesBuffer && 'Buffer' || '') + (usesBuffer && usesProcess && ", " || '') + (usesProcess && 'process' || '') + ") {" + source + - "\n})(" + (usesBuffer && "require('buffer').Buffer" || '') + (usesBuffer && usesProcess && ", " || '') + (usesProcess && "require('process')" || '') + ");"; - } - - // remap require statements, with mappings: - // require('file.json') -> require('file.json!') - // finally we map builtins to the adjusted module - var baseDir = path.dirname(path.relative(dir, file)); - - // only remap if commonjs - return require('systemjs-builder/compilers/cjs').remap(source, function(dep) { - var relPath = path.join(baseDir, dep.replace(/\//g, path.sep)).replace(/\\/g, '/'); - var firstPart = dep.split('/').splice(0, dep.substr(0, 1) == '@' ? 2 : 1).join('/'); - - // packages can import themselves by name - if (firstPart == packageName) { - // note mains work here because the npmResolve below picks up the jspm-generated main file - var depPath = path.join(dir, dep.replace(firstPart, '').replace(/\//g, path.sep)); - dep = path.relative(path.dirname(file), depPath).replace(/\\/g, '/'); - if (dep.substr(0, 1) != '.') - dep = './' + dep; + .then(function(parsed) { + var requires = parsed.requires, + usesProcess = parsed.usesProcess, + usesBuffer = parsed.usesBuffer; + + // add process and buffer globals config for files that need it + if (usesProcess || usesBuffer) { + var meta = metas[fileName] = typeof metas[fileName] == 'object' ? metas[fileName] : {}; + meta.globals = meta.globals || {}; + + if (usesProcess) { + meta.globals.process = 'process'; + if (coreDeps.indexOf('process') == -1) + coreDeps.push('process'); } - - // first check if this is an alias - if (aliases[relPath]) { - changed = true; - dep = path.relative(baseDir, aliases[relPath].replace(/\//g, path.sep)).replace(/\\/g, '/'); - if (dep.substr(0, 1) != '.') - dep = './' + dep; + if (usesBuffer) { + meta.globals.Buffer = 'buffer'; + if (coreDeps.indexOf('buffer') == -1) + coreDeps.push('buffer'); } + } - // check if it is a Node builtin - else if (!pjson.dependencies[firstPart] && nodeBuiltins[firstPart]) { - changed = true; - // only add explicit builtin dependency for production code - if (!filename.match(/^(test|tests|support|example)\//)) - newDeps[firstPart] = nodeBuiltins[firstPart]; - return firstPart; + requires.forEach(function(req) { + // package require by own name + if (req.substr(0, packageName.length) == packageName && req[packageName.length] == '/' || req.length == packageName.length) { + resolutionMap[packageName] = '.'; + return; } - // if not a package, check for internal resolution - // run the NodeJS resolver, to know which file we should get - else if (!pjson.dependencies[firstPart]) { - try { - var resolved = npmResolve.sync(dep, { basedir: path.dirname(file), extensions: ['.js', '.json'] }); - // ensure that we don't backtrack too deep? - // not an issue since installing into ~/.jspm/packages - - dep = path.relative(path.dirname(file), resolved).replace(/\\/g, '/'); - if (dep.substr(0, 1) != '.') - dep = './' + dep; - - relPath = path.relative(dir, resolved); - if (relPath.substr(relPath.length - 3, 3) == '.js') - relPath = relPath.substr(0, relPath.length - 3); + // if it is a package require, note if we have a new core dep + if (req[0] != '.') { + var coreResolution = nodeCoreModules[req]; + // non-browser core module + if (coreResolution) { + // non-browser core module + if (coreResolution.indexOf(':') == -1) + resolutionMap[req] = coreResolution; + else if (coreDeps.indexOf(req) == -1) + coreDeps.push(req); } - catch(e) {} - } - - // check if it is an alias again - if (aliases[relPath]) { - changed = true; - dep = path.relative(baseDir, aliases[relPath].replace(/\//g, path.sep)).replace(/\\/g, '/'); - if (dep.substr(0, 1) != '.') - dep = './' + dep; - } - - // now that we have resolved the dependency, do extension alterations - if (dep.substr(dep.length - 5, 5) == '.json') { - changed = true; - newDeps['systemjs-json'] = jsonPlugin; - return dep + '!systemjs-json'; + return; } - // disable directory requires - if (dep.substr(dep.length - 1, 1) == '/') { - changed = true; - dep = dep.substr(0, dep.length - 1); - } + var nodeResolved = nodeResolve(req, fileName, fileTree); - // remove js extensions - if (dep.substr(dep.length - 3, 3) == '.js' && dep.indexOf('/') != -1) { - changed = true; - return dep.substr(0, dep.length - 3); - } + // if it didn't resolve, ignore it + if (!nodeResolved) + return; - return dep; - }, file) - .then(function(output) { - source = output.source; - }); - }) - .then(function(output) { - Object.keys(newDeps).forEach(function(dep) { - pjson.dependencies[dep] = newDeps[dep]; + // 1. directory resolution + if (nodeResolved.substr(nodeResolved.length - 8) == 'index.js' && req.substr(req.length - 8) != 'index.js') + resolutionMap['./' + nodeResolved.substr(0, nodeResolved.length - 8)] = './' + nodeResolved; + + // 2. non js file extension + else if (nodeResolved.substr(nodeResolved.length - 3, 3) != '.js' && nodeResolved.substr(nodeResolved.length - 5, 5) != '.json') + metas[nodeResolved] = metas[nodeResolved] || true; + + // 3. file.js.js + else if (nodeResolved.substr(nodeResolved.length - 6, 6) == '.js.js') + metas[nodeResolved] = metas[nodeResolved] || true; + + // 4. file.json.js + else if (nodeResolved.substr(nodeResolved.length - 8, 8) == '.json.js') + metas[nodeResolved] = metas[nodeResolved] || true; }); - if (!changed) - return; - - return asp(fs.writeFile)(file, source); - }, function(err) { - buildErrors.push(err); }); })) .then(function() { - // write directory forwarding files for external directory requires - return Promise.all(directoryFiles.map(function(dirFile) { - var dirName = dirFile.split(path.sep).pop(); - dirName = dirName.substr(0, dirName.length - 3).replace(/\\/g, '/'); - return fs.writeFile(dirFile, "module.exports = require('./" + dirName + "/index');\n"); - })); + + // add core dependencies + coreDeps.sort().forEach(function(dep) { + if (!packageConfig.map[dep] && !packageConfig.dependencies[dep] && nodeCoreModules[dep].indexOf(':') != -1) + packageConfig.dependencies[dep] = nodeCoreModules[dep]; + }); + + // merge in require resolution map + Object.keys(resolutionMap).sort().forEach(function(m) { + if (!packageConfig.map[m]) + packageConfig.map[m] = resolutionMap[m]; + }); + + // add metas + Object.keys(metas).sort().forEach(function(m) { + if (!packageConfig.meta[m]) + packageConfig.meta[m] = metas[m]; + }); + + return packageConfig; }); - }) - .then(function() { - return buildErrors; }); -} - -var bufferRegEx = /(?:^|[^$_a-zA-Z\xA0-\uFFFF.])Buffer/; -var processRegEx = /(?:^|[^$_a-zA-Z\xA0-\uFFFF.])process/; +}; -var metaRegEx = /^(\s*\/\*.*\*\/|\s*\/\/[^\n]*|\s*"[^"]+"\s*;?|\s*'[^']+'\s*;?)+/; -var metaPartRegEx = /\/\*.*\*\/|\/\/[^\n]*|"[^"]+"\s*;?|'[^']+'\s*;?/g; +var metaRegEx = /^(\s*\/\*[^\*]*(\*(?!\/)[^\*]*)*\*\/|\s*\/\/[^\n]*|\s*"[^"]+"\s*;?|\s*'[^']+'\s*;?)+/; +var metaPartRegEx = /\/\*[^\*]*(\*(?!\/)[^\*]*)*\*\/|\/\/[^\n]*|"[^"]+"\s*;?|'[^']+'\s*;?/g; var cmdCommentRegEx = /^\s*#/; -// convert NodeJS or Bower dependencies into jspm-compatible dependencies -var githubRegEx = /^git(\+[^:]+)?:\/\/github.com\/(.+)/; -var githubHttpRegEx = /^https?:\/\/github\.com\/([^\/]+\/[^\/]+)\/archive\/([^\/]+)\.(tar\.gz|zip)$/; -var protocolRegEx = /^[^\:\/]+:\/\//; -var semverRegEx = /^(\d+)(?:\.(\d+)(?:\.(\d+)(?:-([\da-z-]+(?:\.[\da-z-]+)*)(?:\+([\da-z-]+(?:\.[\da-z-]+)*))?)?)?)?$/i; -function parseDependencies(dependencies, ui) { - // do dependency parsing - var outDependencies = {}; - var process = function(d) { - var dep = dependencies[d]; - - var match, name, version = ''; - - // 1. git://github.com/name/repo.git#version -> github:name/repo@version - if ((match = dep.match(githubRegEx))) { - dep = match[2]; - name = 'github:' + dep.split('#')[0]; - version = dep.split('#')[1] || '*'; - if (version.substr(0, 1) == 'v' && version.substr(1).match(semverRegEx)) - version = version.substr(1); - if (name.substr(name.length - 4, 4) == '.git') - name = name.substr(0, name.length - 4); - ui.log('warn', 'npm dependency `' + name + '@' + version + '` will likely only work if its GitHub repo has %registry: npm% in its package.json'); - } +function parseNodeRequires(filePath) { + var output = { + requires: [], + usesProcess: false, + usesBuffer: false + }; - // 2. https?://github.com/name/repo/archive/v?[semver].tar.gz -> github:name/repo@[semver] - else if ((match = dep.match(githubHttpRegEx))) { - name = 'github:' + match[1]; - version = match[2]; - if (version.substr(0, 1) == 'v' && version.substr(1).match(semverRegEx)) - version = version.substr(1); - } + return asp(fs.readFile)(filePath) + .catch(function(err) { + throw new Error('Error reading file ' + filePath + ', ' + err.code); + }) + .then(function(source) { + source = source.toString(); - // 3. url:// -> not supported - else if (dep.match(protocolRegEx)) - throw 'npm dependency format ' + dep + ' not currently supported by jspm. Post an issue if required.'; + // first check if we have format meta + var meta = source.match(metaRegEx); + if (meta) { + var metaParts = meta[0].match(metaPartRegEx); - // 4. name/repo#version -> github:name/repo@version - else if (dep.split('/').length == 2) { - name = 'github:' + dep.split('#')[0]; - version = dep.split('#')[1] || '*'; - } + for (var i = 0; i < metaParts.length; i++) { + var curPart = metaParts[i]; + var len = curPart.length; - // 5. version -> name@version - else { - name = d; - version = dep; - } + var firstChar = curPart.substr(0, 1); + if (curPart.substr(len - 1, 1) == ';') + len--; - // otherwise, we convert an npm range into something jspm-compatible - // if it is an exact semver, or a tag, just use it directly - if (!nodeSemver.valid(version)) { - var range; - - // comma space is allowed on npm for some reason - version = version.replace(/, /g, ' '); - - if (!version || version == 'latest' || version == '*') - version = '*'; - - // if we have a semver or fuzzy range, just keep as-is - else if (version.indexOf(/[ <>=]/) != -1 || !version.substr(1).match(semverRegEx) || !version.substr(0, 1).match(/[\^\~]/)) - range = nodeSemver.validRange(version); - - if (range == '*') - version = '*'; - - else if (range) { - // if it has OR semantics, we only support the last range - if (range.indexOf('||') != -1) - range = range.split('||').pop(); - - var rangeParts = range.split(' '); - - // convert AND statements into a single lower bound and upper bound - // enforcing the lower bound as inclusive and the upper bound as exclusive - var lowerBound, upperBound, lEq, uEq; - for (var i = 0; i < rangeParts.length; i++) { - var part = rangeParts[i]; - var a = part.charAt(0); - var b = part.charAt(1); - - // get the version - var v = part; - if (b == '=') - v = part.substr(2); - else if (a == '>' || a == '<' || a == '=') - v = part.substr(1); - - // and the operator - var gt = a == '>'; - var lt = a == '<'; - - if (gt) { - // take the highest lower bound - if (!lowerBound || nodeSemver.gt(lowerBound, v)) { - lowerBound = v; - lEq = b == '='; - } - } - else if (lt) { - // take the lowest upper bound - if (!upperBound || nodeSemver.lt(upperBound, v)) { - upperBound = v; - uEq = b == '='; - } - } - else { - // equality - lowerBound = upperBound = (part.substr(0, 1) == '=' ? part.substr(1) : part); - lEq = uEq = true; - break; - } - } + if (firstChar != '"' && firstChar != "'") + continue; - // for some reason nodeSemver adds "-0" when not appropriate - if (lowerBound && lowerBound.substr(lowerBound.length - 2, 2) == '-0') - lowerBound = lowerBound.substr(0, lowerBound.length - 2); - if (upperBound && upperBound.substr(upperBound.length - 2, 2) == '-0') - upperBound = upperBound.substr(0, upperBound.length - 2); - - var lowerSemver, upperSemver; - - if (lowerBound) { - lowerSemver = lowerBound.match(semverRegEx); - lowerSemver[1] = parseInt(lowerSemver[1], 10); - lowerSemver[2] = parseInt(lowerSemver[2], 10); - lowerSemver[3] = parseInt(lowerSemver[3], 10); - if (!lEq) { - if (!lowerSemver[4]) - lowerSemver[4] = '0'; - // NB support incrementing existing preleases - } - } + var metaString = curPart.substr(1, curPart.length - 3); - if (upperBound) { - upperSemver = upperBound.match(semverRegEx); - upperSemver[1] = parseInt(upperSemver[1], 10); - upperSemver[2] = parseInt(upperSemver[2], 10); - upperSemver[3] = parseInt(upperSemver[3], 10); - } + // skip any processing if this has format-level meta + if (metaString == 'format global' || metaString == 'format amd' || metaString == 'format register' || metaString == 'bundle' || metaString == 'format esm') + return output; + } + } - if (!upperBound && !lowerBound) { - version = ''; - } + if (source.match(cmdCommentRegEx)) + source = '//' + source; - // if not upperBound, then just treat as a wildcard - else if (!upperBound) { - version = '*'; - } + // attempt to create a syntax tree and parse out require statements, Buffer and process usage + return require('./deps-transformer')(source); + }); +} - // if no lowerBound, use the upperBound directly, with sensible decrementing if necessary - else if (!lowerBound) { +// pulled out of SystemJS internals... +function readMeta(pkgPath, pkgMeta) { + var meta = {}; + + // apply wildcard metas + var bestDepth = 0; + var wildcardIndex; + for (var module in pkgMeta) { + wildcardIndex = module.indexOf('*'); + if (wildcardIndex === -1) + continue; + if (module.substr(0, wildcardIndex) === pkgPath.substr(0, wildcardIndex) + && module.substr(wildcardIndex + 1) === pkgPath.substr(pkgPath.length - module.length + wildcardIndex + 1)) { + var depth = module.split('/').length; + if (depth > bestDepth) + bestDepth = depth; + extendMeta(meta, pkgMeta[module], bestDepth != depth); + } + } - if (uEq) { - version = upperBound; - } + // apply exact meta + if (meta[pkgPath]) + extendMeta(load.metadata, meta[pkgPath]); - else { - if (!upperSemver[4]) { - if (upperSemver[3] > 0) { - upperSemver[3]--; - } - else if (upperSemver[2] > 0) { - upperSemver[2]--; - upperSemver[3] = 0; - } - else if (upperSemver[1] > 0) { - upperSemver[1]--; - upperSemver[2] = 0; - upperSemver[3] = 0; - } - } - else { - upperSemver[4] = undefined; - } - version = getVersion(upperSemver); - } - } + return meta; +} +function extendMeta(a, b, prepend) { + for (var p in b) { + var val = b[p]; + if (!(p in a)) + a[p] = val; + else if (val instanceof Array && a[p] instanceof Array) + a[p] = [].concat(prepend ? val : a[p]).concat(prepend ? a[p] : val); + else if (typeof val == 'object' && typeof a[p] == 'object') + a[p] = extend(extend({}, a[p]), val, prepend); + else if (!prepend) + a[p] = val; + } +} +function extend(a, b, prepend) { + for (var p in b) { + if (!prepend || !(p in a)) + a[p] = b[p]; + } + return a; +} - else { - // if upper bound is inclusive, use it - if (uEq) - version = upperBound; +/* + * Given a file tree stat, work out the resolution for a package + * name is a path within the package, parent is also a path within the package + * fileTree is keyed by path, with true as the value. Folders are indicated by trailling / + * All paths are assumed '/' separated for this implementation + */ +function nodeResolve(name, parent, fileTree) { + // leave absolute paths undisturbed + if (name[0] == '/') + return; - // if upper bound is exact major - else if (upperSemver[2] === 0 && upperSemver[3] === 0 && !upperSemver[4]) { + // relative paths are resolved relatively and statted + if (name.substr(0, 2) == './' || name.substr(0, 3) == '../' && parent.indexOf('/') != -1) { + name = url.resolve('/' + parent, name).substr(1); - // if previous major is 0 - if (upperSemver[1] - 1 === 0) { - version = '0'; - } - else { - // if lower bound is major below, we are semver compatible - if (lowerSemver[1] == upperSemver[1] - 1) - version = '^' + getVersion(lowerSemver); - // otherwise we are semver compatible with the previous exact major - else - version = '^' + (upperSemver[1] - 1); - } - } - // if upper bound is exact minor - else if (upperSemver[3] === 0 && !upperSemver[4]) { - // if lower bound is minor below, we are fuzzy compatible - if (lowerSemver[2] == upperSemver[2] - 1) - version = '~' + getVersion(lowerSemver); - // otherwise we are fuzzy compatible with previous - else - version = '~' + upperSemver[1] + '.' + (upperSemver[2] - 1) + '.0'; - } - // if upper bound is exact version -> use exact - else - throw 'Unable to translate npm version ' + version + ' into a jspm range.'; - } - } - } + if (fileTree[name]) + return name; - outDependencies[d] = name + (version ? '@' + version : ''); - }; - for (var d in dependencies) process(d); - return outDependencies; -} + if (fileTree[name + '.js']) + return name + '.js'; -function getVersion(semver) { - return semver[1] + '.' + semver[2] + '.' + semver[3] + (semver[4] ? '-' + semver[4] : ''); -} -exports.parseDependencies = parseDependencies; + if (fileTree[name + 'json']) + return name + '.json'; + + // no file match -> try loading as a folder + var folderName = name + (name[name.length - 1] == '/' ? '' : '/'); + + if (fileTree[folderName]) + return folderName + 'index.js'; + } + // plain name -> package resolution + return name; +} \ No newline at end of file diff --git a/lib/npm.js b/lib/npm.js index 5e57da4..de11904 100644 --- a/lib/npm.js +++ b/lib/npm.js @@ -9,6 +9,7 @@ var mkdirp = require('mkdirp'); var Npmrc = require('./npmrc'); var auth = require('./auth'); +var nodeSemver = require('semver'); var nodeConversion = require('./node-conversion'); var Npmrc = require('./npmrc'); @@ -124,7 +125,7 @@ NPMLocation.configure = function(config, ui) { }); }; -NPMLocation.packageFormat = /^@[^\/]+\/[^\/]+|^[^@\/][^\/]+/; +NPMLocation.packageNameFormats = ['*', '@*/*']; NPMLocation.prototype = { @@ -221,94 +222,58 @@ NPMLocation.prototype = { }); }, - getPackageConfig: function(repo, version, hash, pjson) { - if (!pjson) + getPackageConfig: function(repo, version, hash, packageConfig) { + if (!packageConfig) throw 'Package.json meta not provided in endpoint request'; - if (hash && pjson.dist.shasum != hash) + if (hash && packageConfig.dist.shasum != hash) throw 'Package.json lookup hash mismatch'; - return clone(pjson); + return clone(packageConfig); }, - processPackageConfig: function(pjson, packageName) { - if (pjson.jspmNodeConversion === false) - return pjson; + processPackageConfig: function(packageConfig, packageName) { + if (packageConfig.jspmNodeConversion === false) + return packageConfig; // peer dependencies are just dependencies in jspm - pjson.dependencies = pjson.dependencies || {}; - if (pjson.peerDependencies) { - for (d in pjson.peerDependencies) - pjson.dependencies[d] = pjson.peerDependencies[d]; + packageConfig.dependencies = packageConfig.dependencies || {}; + if (packageConfig.peerDependencies) { + for (d in packageConfig.peerDependencies) + packageConfig.dependencies[d] = packageConfig.peerDependencies[d]; } // warn if using jspm-style dependencies at this point - for (var d in pjson.dependencies) - if (!pjson.dependencies[d].match(/^(https?|git)[:+]/) && pjson.dependencies[d].indexOf(':') > 0) - throw 'Package.json dependency %' + d + '% set to `' + pjson.dependencies[d] + '`, which is not a valid dependency format for npm.' + for (var d in packageConfig.dependencies) + if (!packageConfig.dependencies[d].match(/^(https?|git)[:+]/) && packageConfig.dependencies[d].indexOf(':') > 0) + throw 'Package.json dependency %' + d + '% set to `' + packageConfig.dependencies[d] + '`, which is not a valid dependency format for npm.' + '\nIt\'s advisable to publish jspm-style packages to GitHub or another `registry` so conventions are clear.'; + '\nTo skip npm compatibility install with %jspm install ' + packageName + '-o "{jspmNodeConversion: false}"%.' + packageConfig.dependencies = parseDependencies(packageConfig.dependencies, this.ui); - pjson.dependencies = nodeConversion.parseDependencies(pjson.dependencies, this.ui); - - pjson.format = pjson.format || 'cjs'; - - // json mains become plugins - if (pjson.main && typeof pjson.main == 'string' && pjson.main.substr(pjson.main.length - 5, 5) == '.json') { - pjson.main += '!systemjs-json'; - pjson.dependencies['systemjs-json'] = nodeConversion.jsonPlugin; - } + // every npm package depends on github:jspm/nodelibs (a peer dependency in due course) + if (!packageConfig.dependencies['nodelibs']) + packageConfig.dependencies['nodelibs'] = 'github:jspm/nodelibs@^0.1.0'; // ignore directory flattening for NodeJS, as npm doesn't do it // we do allow if there was an override through the jspm property though - if (!pjson.jspm || !pjson.jspm.directories) - delete pjson.directories; - - // ignore node_modules by default when processing - if (!(pjson.ignore instanceof Array)) - pjson.ignore = []; - pjson.ignore.push('node_modules'); - - if (pjson.files && pjson.files instanceof Array && pjson.files.indexOf('package.json') == -1) - pjson.files.push('package.json'); - + if (!packageConfig.jspm || !packageConfig.jspm.directories) + delete packageConfig.directories; - // if there is a "browser" object, convert it into map config for browserify support - if (typeof pjson.browserify == 'string') - pjson.main = pjson.browserify; - if (typeof pjson.browser == 'string') - pjson.main = pjson.browser; + // ignore node_modules and test folders by default + if (!(packageConfig.ignore instanceof Array)) + packageConfig.ignore = []; + packageConfig.ignore.push('node_modules', 'test'); - if (typeof pjson.browser == 'object') { - pjson.map = pjson.map || {}; - for (var b in pjson.browser) { - var mapping = pjson.browser[b]; + // keep the package.json around if we're doing files filtering + if (packageConfig.files && packageConfig.files instanceof Array && packageConfig.files.indexOf('package.json') == -1) + packageConfig.files.push('package.json'); - if (mapping === false) { - mapping = '@empty'; - } - else if (typeof mapping == 'string') { - if (b.substr(b.length - 3, 3) == '.js') - b = b.substr(0, b.length - 3); - if (mapping.substr(mapping.length - 3, 3) == '.js') - mapping = mapping.substr(0, mapping.length - 3); - - // we handle relative maps during the build phase - if (b.substr(0, 2) == './') - continue; - } - else - continue; - - pjson.map[b] = pjson.map[b] || mapping; - } - } - - return pjson; + return packageConfig; }, - download: function(repo, version, hash, versionData, outDir) { + download: function(repo, version, hash, versionData, targetDir) { var self = this; var doAuth = this.alwaysAuth || repo[0] == '@'; @@ -333,7 +298,7 @@ NPMLocation.prototype = { npmRes .pipe(gzip) - .pipe(tar.Extract({ path: outDir, strip: 1 })) + .pipe(tar.Extract({ path: targetDir, strip: 1 })) .on('error', reject) .on('end', resolve); @@ -350,13 +315,242 @@ NPMLocation.prototype = { }); }, - build: function(pjson, dir) { - if (pjson.jspmNodeConversion === false) - return; + processPackage: function(packageConfig, packageName, packageDir) { + if (packageConfig.jspmNodeConversion === false) + return packageConfig; // apply static conversions - return nodeConversion.convertPackage(pjson, dir); + return nodeConversion.convertPackage(packageConfig, packageDir, this.ui); } }; +// convert NodeJS or Bower dependencies into jspm-compatible dependencies +var githubRegEx = /^git(\+[^:]+)?:\/\/github.com\/(.+)/; +var githubHttpRegEx = /^https?:\/\/github\.com\/([^\/]+\/[^\/]+)\/archive\/([^\/]+)\.(tar\.gz|zip)$/; +var protocolRegEx = /^[^\:\/]+:\/\//; +var semverRegEx = /^(\d+)(?:\.(\d+)(?:\.(\d+)(?:-([\da-z-]+(?:\.[\da-z-]+)*)(?:\+([\da-z-]+(?:\.[\da-z-]+)*))?)?)?)?$/i; +function parseDependencies(dependencies, ui) { + // do dependency parsing + var outDependencies = {}; + var process = function(d) { + var dep = dependencies[d]; + + var match, name, version = ''; + + // 1. git://github.com/name/repo.git#version -> github:name/repo@version + if ((match = dep.match(githubRegEx))) { + dep = match[2]; + name = 'github:' + dep.split('#')[0]; + version = dep.split('#')[1] || '*'; + if (version.substr(0, 1) == 'v' && version.substr(1).match(semverRegEx)) + version = version.substr(1); + if (name.substr(name.length - 4, 4) == '.git') + name = name.substr(0, name.length - 4); + ui.log('warn', 'npm dependency `' + name + '@' + version + '` will likely only work if its GitHub repo has %registry: npm% in its package.json'); + } + + // 2. https?://github.com/name/repo/archive/v?[semver].tar.gz -> github:name/repo@[semver] + else if ((match = dep.match(githubHttpRegEx))) { + name = 'github:' + match[1]; + version = match[2]; + if (version.substr(0, 1) == 'v' && version.substr(1).match(semverRegEx)) + version = version.substr(1); + } + + // 3. url:// -> not supported + else if (dep.match(protocolRegEx)) + throw 'npm dependency format ' + dep + ' not currently supported by jspm. Post an issue if required.'; + + // 4. name/repo#version -> github:name/repo@version + else if (dep.split('/').length == 2) { + name = 'github:' + dep.split('#')[0]; + version = dep.split('#')[1] || '*'; + } + + // 5. version -> name@version + else { + name = d; + version = dep; + } + + // otherwise, we convert an npm range into something jspm-compatible + // if it is an exact semver, or a tag, just use it directly + if (!nodeSemver.valid(version)) { + var range; + + // comma space is allowed on npm for some reason + version = version.replace(/, /g, ' '); + + if (!version || version == 'latest' || version == '*') + version = '*'; + + // if we have a semver or fuzzy range, just keep as-is + else if (version.indexOf(/[ <>=]/) != -1 || !version.substr(1).match(semverRegEx) || !version.substr(0, 1).match(/[\^\~]/)) + range = nodeSemver.validRange(version); + + if (range == '*') + version = '*'; + + else if (range) { + // if it has OR semantics, we only support the last range + if (range.indexOf('||') != -1) + range = range.split('||').pop(); + + var rangeParts = range.split(' '); + + // convert AND statements into a single lower bound and upper bound + // enforcing the lower bound as inclusive and the upper bound as exclusive + var lowerBound, upperBound, lEq, uEq; + for (var i = 0; i < rangeParts.length; i++) { + var part = rangeParts[i]; + var a = part.charAt(0); + var b = part.charAt(1); + + // get the version + var v = part; + if (b == '=') + v = part.substr(2); + else if (a == '>' || a == '<' || a == '=') + v = part.substr(1); + + // and the operator + var gt = a == '>'; + var lt = a == '<'; + + if (gt) { + // take the highest lower bound + if (!lowerBound || nodeSemver.gt(lowerBound, v)) { + lowerBound = v; + lEq = b == '='; + } + } + else if (lt) { + // take the lowest upper bound + if (!upperBound || nodeSemver.lt(upperBound, v)) { + upperBound = v; + uEq = b == '='; + } + } + else { + // equality + lowerBound = upperBound = (part.substr(0, 1) == '=' ? part.substr(1) : part); + lEq = uEq = true; + break; + } + } + + // for some reason nodeSemver adds "-0" when not appropriate + if (lowerBound && lowerBound.substr(lowerBound.length - 2, 2) == '-0') + lowerBound = lowerBound.substr(0, lowerBound.length - 2); + if (upperBound && upperBound.substr(upperBound.length - 2, 2) == '-0') + upperBound = upperBound.substr(0, upperBound.length - 2); + + var lowerSemver, upperSemver; + + if (lowerBound) { + lowerSemver = lowerBound.match(semverRegEx); + lowerSemver[1] = parseInt(lowerSemver[1], 10); + lowerSemver[2] = parseInt(lowerSemver[2], 10); + lowerSemver[3] = parseInt(lowerSemver[3], 10); + if (!lEq) { + if (!lowerSemver[4]) + lowerSemver[4] = '0'; + // NB support incrementing existing preleases + } + } + + if (upperBound) { + upperSemver = upperBound.match(semverRegEx); + upperSemver[1] = parseInt(upperSemver[1], 10); + upperSemver[2] = parseInt(upperSemver[2], 10); + upperSemver[3] = parseInt(upperSemver[3], 10); + } + + if (!upperBound && !lowerBound) { + version = ''; + } + + // if not upperBound, then just treat as a wildcard + else if (!upperBound) { + version = '*'; + } + + // if no lowerBound, use the upperBound directly, with sensible decrementing if necessary + else if (!lowerBound) { + + if (uEq) { + version = upperBound; + } + + else { + if (!upperSemver[4]) { + if (upperSemver[3] > 0) { + upperSemver[3]--; + } + else if (upperSemver[2] > 0) { + upperSemver[2]--; + upperSemver[3] = 0; + } + else if (upperSemver[1] > 0) { + upperSemver[1]--; + upperSemver[2] = 0; + upperSemver[3] = 0; + } + } + else { + upperSemver[4] = undefined; + } + version = getVersion(upperSemver); + } + } + + else { + // if upper bound is inclusive, use it + if (uEq) + version = upperBound; + + // if upper bound is exact major + else if (upperSemver[2] === 0 && upperSemver[3] === 0 && !upperSemver[4]) { + + // if previous major is 0 + if (upperSemver[1] - 1 === 0) { + version = '0'; + } + else { + // if lower bound is major below, we are semver compatible + if (lowerSemver[1] == upperSemver[1] - 1) + version = '^' + getVersion(lowerSemver); + // otherwise we are semver compatible with the previous exact major + else + version = '^' + (upperSemver[1] - 1); + } + } + // if upper bound is exact minor + else if (upperSemver[3] === 0 && !upperSemver[4]) { + // if lower bound is minor below, we are fuzzy compatible + if (lowerSemver[2] == upperSemver[2] - 1) + version = '~' + getVersion(lowerSemver); + // otherwise we are fuzzy compatible with previous + else + version = '~' + upperSemver[1] + '.' + (upperSemver[2] - 1) + '.0'; + } + // if upper bound is exact version -> use exact + else + throw 'Unable to translate npm version ' + version + ' into a jspm range.'; + } + } + } + + outDependencies[d] = name + (version ? '@' + version : ''); + }; + for (var d in dependencies) process(d); + return outDependencies; +} +// export for unit testing +NPMLocation.parseDependencies = parseDependencies; + +function getVersion(semver) { + return semver[1] + '.' + semver[2] + '.' + semver[3] + (semver[4] ? '-' + semver[4] : ''); +} + module.exports = NPMLocation; diff --git a/package.json b/package.json index 5c06e58..67c28e7 100644 --- a/package.json +++ b/package.json @@ -16,15 +16,14 @@ "url": "https://github.com/jspm/npm/issues" }, "dependencies": { - "glob": "^5.0.10", "graceful-fs": "^3.0.8", "mkdirp": "^0.5.1", + "readdirp": "^2.0.0", "request": "~2.58.0", - "resolve": "^1.1.6", "rmdir": "~1.1.0", "rsvp": "^3.0.18", "semver": "^5.0.1", - "systemjs-builder": "^0.14.0", + "traceur": "0.0.91", "tar": "~1.0.3", "which": "^1.1.1" }, diff --git a/test/fixtures/deps-transformer/process-detection1.js b/test/fixtures/deps-transformer/process-detection1.js new file mode 100644 index 0000000..3b93d68 --- /dev/null +++ b/test/fixtures/deps-transformer/process-detection1.js @@ -0,0 +1,51 @@ +/** + * Copyright 2013-2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule keyMirror + * @typechecks static-only + */ + +'use strict'; + +var invariant = require("./invariant"); + +/** + * Constructs an enumeration with keys equal to their value. + * + * For example: + * + * var COLORS = keyMirror({blue: null, red: null}); + * var myColor = COLORS.blue; + * var isColorValid = !!COLORS[myColor]; + * + * The last line could not be performed if the values of the generated enum were + * not equal to their keys. + * + * Input: {key1: val1, key2: val2} + * Output: {key1: key1, key2: key2} + * + * @param {object} obj + * @return {object} + */ +var keyMirror = function(obj) { + var ret = {}; + var key; + ("production" !== process.env.NODE_ENV ? invariant( + obj instanceof Object && !Array.isArray(obj), + 'keyMirror(...): Argument must be an object.' + ) : invariant(obj instanceof Object && !Array.isArray(obj))); + for (key in obj) { + if (!obj.hasOwnProperty(key)) { + continue; + } + ret[key] = key; + } + return ret; +}; + +module.exports = keyMirror; diff --git a/test/fixtures/deps-transformer/process-detection2.js b/test/fixtures/deps-transformer/process-detection2.js new file mode 100644 index 0000000..55aa52a --- /dev/null +++ b/test/fixtures/deps-transformer/process-detection2.js @@ -0,0 +1,3 @@ +(function(p) { + console.log(p); +})(process); \ No newline at end of file diff --git a/test/unit.js b/test/unit.js index 24bc6ef..b82953c 100644 --- a/test/unit.js +++ b/test/unit.js @@ -1,10 +1,43 @@ -var nodeConversion = require('../lib/node-conversion'); +var parseDependencies = require('../lib/npm').parseDependencies; +var depsTransformer = require('../lib/deps-transformer'); +var fs = require('fs'); + +function arrayEqual(a, b) { + return !a.some(function(val, index) { + return b.indexOf(val) != index; + }); +} + +function testDepsTransformer(name, expectedOutput) { + var source = fs.readFileSync('./fixtures/deps-transformer/' + name + '.js'); + var output = depsTransformer(source.toString()); + + if (output.usesProcess !== expectedOutput.usesProcess) + throw 'Deps detection of ' + name + ' expected to find process ' + expectedOutput.usesProcess; + + if (output.usesBuffer !== expectedOutput.usesBuffer) + throw 'Deps detection of ' + name + ' expected to find buffer ' + expectedOutput.usesBuffer; + + if (!arrayEqual(output.requires.sort(), expectedOutput.requires.sort())) + throw 'Deps detection of ' + name + ' has requires ' + JSON.stringify(output.requires) + ', but expected ' + JSON.stringify(expectedOutput.requires); +} + +testDepsTransformer('process-detection1', { + requires: ['./invariant'], + usesProcess: true, + usesBuffer: false +}); +testDepsTransformer('process-detection2', { + requires: [], + usesProcess: true, + usesBuffer: false +}); function testDependency(name, value, expectedName, expectedValue) { var deps = {}; deps[name] = value; - deps = nodeConversion.parseDependencies(deps); + deps = parseDependencies(deps); for (var p in deps) { if (p != expectedName) @@ -37,4 +70,7 @@ testDependency('@scoped/react', '>=0.12.0', '@scoped/react', '@scoped/react@*'); testDependency('get-size', '>=1.1.4 <1.3', 'get-size', 'get-size@~1.2.0'); + + + console.log('Unit tests passed');