jspm
Version:
Registry and format agnostic JavaScript package manager
670 lines (543 loc) • 21.1 kB
JavaScript
/*
* Copyright 2014-2015 Guy Bedford (http://guybedford.com)
*
* 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.
*/
require('core-js/es6/string');
var Promise = require('rsvp').Promise;
var asp = require('rsvp').denodeify;
var fs = require('graceful-fs');
var glob = require('glob');
var rimraf = require('rimraf');
var path = require('path');
var minimatch = require('minimatch');
var build = module.exports;
exports.buildPackage = function(dir, pjson, isCDN) {
return build.filterIgnoreAndFiles(dir, pjson.ignore, pjson.files)
// check if directories.dist exists
// if so collapse and do nothing further
.then(function() {
if (!pjson.directories || !pjson.directories.dist)
return;
return asp(fs.stat)(path.resolve(dir, pjson.directories.dist))
.then(function(stats) {
return stats && stats.isDirectory();
}, function() {
return false;
})
.then(function(dist) {
if (dist)
return build.collapseLibDir(dir, pjson.directories.dist).then(function() { return true; });
});
})
// check if directories.lib exists, if so collapse
.then(function(dist) {
if (dist)
return true;
if (!pjson.directories || !pjson.directories.lib)
return;
return asp(fs.stat)(path.resolve(dir, pjson.directories.lib))
.then(function(stats) {
return stats && stats.isDirectory();
}, function() {
return false;
})
.then(function(dist) {
if (dist)
return build.collapseLibDir(dir, pjson.directories.lib);
});
})
// finally, build
.then(function(hasDist) {
if (pjson.format || pjson.shim || pjson.buildConfig || (pjson.registry && pjson.dependencies) || pjson.map)
return build.compileDir(dir, {
format: pjson.format,
shim: pjson.shim,
dependencies: pjson.dependencies, // dependencies already parsed into jspm-compatible
removeJSExtensions: pjson.useJSExtensions,
map: pjson.map,
transpile: !hasDist && pjson.buildConfig && pjson.buildConfig.transpile,
minify: isCDN, // && !hasDist && pjson.buildConfig && (pjson.buildConfig.uglify || pjson.buildConfig.minify)
alwaysIncludeFormat: isCDN
});
});
};
exports.filterIgnoreAndFiles = function(dir, ignore, files) {
if (!ignore && !files)
return Promise.resolve();
return asp(glob)(dir + path.sep + '**' + path.sep + '*', {dot: true})
.then(function(allFiles) {
var removeFiles = [];
allFiles.forEach(function(file) {
var fileName = path.relative(dir, file).replace(/\\/g, '/');
// if files, remove all files except those in the files list
if (files && !files.some(function(keepFile) {
if (keepFile.startsWith('./'))
keepFile = keepFile.substr(2);
else if (keepFile.startsWith('/'))
keepFile = keepFile.substr(1);
// this file is in a keep dir, or a keep file, don't exclude
if (inDir(fileName, keepFile, false) || minimatch(fileName, keepFile))
return true;
}))
return removeFiles.push(fileName);
// if ignore, ensure removed
if (ignore && ignore.some(function(ignoreFile) {
if (ignoreFile.startsWith('./'))
ignoreFile = ignoreFile.substr(2);
else if (ignoreFile.startsWith('/'))
ignoreFile = ignoreFile.substr(1);
// this file is in an ignore dir or an ignore file, ignore
if (inDir(fileName, ignoreFile, false) || minimatch(fileName, ignoreFile))
return true;
}))
removeFiles.push(fileName);
});
return removeFiles.map(function(removeFile) {
return path.resolve(dir, removeFile);
})
// do removal of files in reverse order so that directories get removed properly
.reverse()
.reduce(function(removePromise, resolvedFile) {
return removePromise.then(function() {
return asp(fs.unlink)(resolvedFile)
.catch(function(e) {
if (e.code === 'ENOENT')
return;
if (e.code === 'EISDIR' || e.code === 'EPERM')
return asp(fs.rmdir)(resolvedFile)
.catch(function(e) {
if (e.code === 'ENOTEMPTY')
return;
throw e;
});
throw e;
});
});
}, Promise.resolve());
});
};
exports.collapseLibDir = function(dir, subDir) {
if (subDir.endsWith('/'))
subDir = subDir.substr(0, subDir.length - 1);
var tmpDir = path.resolve(dir, '..', '.tmp-' + dir.split(path.sep).pop());
// move subDir to tmpDir
return asp(fs.rename)(path.normalize(dir + path.sep + subDir), tmpDir)
// remove everything in dir
.then(function() {
return asp(rimraf)(dir);
})
// move subDir to dir
.then(function() {
return asp(fs.rename)(tmpDir, dir);
});
};
function inDir(fileName, dir, sep) {
return fileName.substr(0, dir.length) === dir && (sep === false || fileName.substr(dir.length - 1, 1) === path.sep);
}
function matchWithWildcard(matches, name) {
var curMatch;
var curMatchLength;
main:
for (var p in matches) {
if (!matches.hasOwnProperty(p))
continue;
var matchParts = p.split('/');
var nameParts = name.split('/');
if (matchParts.length !== nameParts.length)
continue;
var match;
for (var i = 0; i < matchParts.length; i++) {
// do wildcard matching on individual parts if necessary
if (matchParts[i].includes('*')) {
if (!(match = nameParts[i].match(new RegExp(matchParts[i].replace(/([^*\w])/g, '\\$1').replace(/(\*)/g, '(.*)')))))
continue main;
}
else if (nameParts[i] !== matchParts[i])
continue main;
}
// least wildcards in match wins
if (p.length >= curMatchLength)
continue;
curMatch = p;
curMatchLength = matchParts.length;
}
return curMatch;
}
// return the number of prefix parts (separated by '/') matching the name
// eg prefixMatchLength('jquery/some/thing', 'jquery') -> 1
function prefixMatchLength(name, prefix) {
var prefixParts = prefix.split('/');
var nameParts = name.split('/');
if (prefixParts.length > nameParts.length)
return 0;
for (var i = 0; i < prefixParts.length; i++)
if (nameParts[i] !== prefixParts[i])
return 0;
return prefixParts.length;
}
// while doing map, we also remove ".js" extensions where necessary
function applyMap(_name, map, baseFile, removeJSExtensions) {
var name = _name, pluginName;
if (name.includes('!')) {
pluginName = name.substr(name.indexOf('!') + 1);
name = name.substr(0, name.length - pluginName.length - 1);
pluginName = pluginName || name.substr(name.lastIndexOf('.') + 1);
pluginName = applyMap(pluginName, map, baseFile, false) || pluginName;
}
if (removeJSExtensions) {
if (name.startsWith('./') || name.split('/').length > 1) {
if (name.endsWith('.js'))
name = name.substr(0, name.length - 3);
}
}
for (var m in map) {
if (!map.hasOwnProperty(m))
continue;
var matchLength = prefixMatchLength(name, m);
if (!matchLength)
continue;
var subPath = name.split('/').splice(matchLength).join('/');
var toMap = map[m];
if (typeof toMap != 'string')
continue;
if (toMap.startsWith('./')) {
// add .js in case of matching directory name
toMap = path.relative(path.dirname(baseFile), toMap.substr(2) + '.js').replace(/\\/g, '/');
if (!toMap.startsWith('.'))
toMap = './' + toMap;
// remove .js
toMap = toMap.substr(0, toMap.length - 3);
}
return toMap + (subPath ? '/' + subPath : '') + (pluginName ? '!' + pluginName : '');
}
if (pluginName)
name += '!' + pluginName;
if (name !== _name)
return name;
}
// NB keep these up to date with SystemJS
var esmRegEx = /(^\s*|[}\);\n]\s*)(import\s+(['"]|(\*\s+as\s+)?[^"'\(\)\n;]+\s+from\s+['"]|\{)|export\s+\*\s+from\s+["']|export\s+(\{|default|function|class|var|const|let|async\s+function))/;
var esmDepRegEx = /(^|\}|\s)(from|import)\s*("([^"]+)"|'([^']+)')/g;
var amdRegEx = /(?:^|[^$_a-zA-Z\xA0-\uFFFF.])define\s*\(\s*("[^"]+"\s*,\s*|'[^']+'\s*,\s*)?\s*(\[(\s*(("[^"]+"|'[^']+')\s*,|\/\/.*\r?\n|\/\*(.|\s)*?\*\/))*(\s*("[^"]+"|'[^']+')\s*,?)?(\s*(\/\/.*\r?\n|\/\*(.|\s)*?\*\/))*\s*\]|function\s*|{|[_$a-zA-Z\xA0-\uFFFF][_$a-zA-Z0-9\xA0-\uFFFF]*\))/;
var amdDefineRegEx = /(?:^|[^$_a-zA-Z\xA0-\uFFFF.])define\s*\(\s*("[^"]+"\s*,|'[^']+'\s*,\s*)?(\[(\s*("[^"]+"|'[^']+')\s*,)*(\s*("[^"]+"|'[^']+')\s*)?\])?/g;
var cjsRequireRegEx = /(?:^\uFEFF?|[^$_a-zA-Z\xA0-\uFFFF."'])require\s*\(\s*("[^"\\]*(?:\\.[^"\\]*)*"|'[^'\\]*(?:\\.[^'\\]*)*')\s*\)/g;
var cjsExportsRegEx = /(?:^\uFEFF?|[^$_a-zA-Z\xA0-\uFFFF.]|module\.)(exports\s*\[['"]|\exports\s*\.)|(?:^\uFEFF?|[^$_a-zA-Z\xA0-\uFFFF.])module\.exports\s*\=/;
var registerRegEx = /System\.register/;
var metaRegEx = /^(\s*\/\*.*\*\/|\s*\/\/[^\n]*|\s*"[^"]+"\s*;?|\s*'[^']+'\s*;?)+/;
var metaPartRegEx = /\/\*.*\*\/|\/\/[^\n]*|"[^"]+"\s*;?|'[^']+'\s*;?/g;
// var commentRegEx = /(\/\*([\s\S]*?)\*\/|([^:]|^)\/\/(.*)$)/mg;
var initialCommentRegEx = /^\s*(\/\*|\/\/)/;
function detectFormat(source) {
// first checkout 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].endsWith(';'))
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 (metadata.format)
return { format: metadata.format, meta: true };
cjsExportsRegEx.lastIndex = 0;
cjsRequireRegEx.lastIndex = 0;
if (source.match(esmRegEx))
return { format: 'esm' };
if (source.match(registerRegEx))
return { format: 'register' };
if (source.match(amdRegEx))
return { format: 'amd' };
if (cjsRequireRegEx.exec(source) || cjsExportsRegEx.exec(source))
return { format: 'cjs' };
return { format: 'global' };
}
exports.detectFormat = detectFormat;
/*
options.format
options.shim
options.dependencies
options.map
options.removeJSExtensions
options.transpile
options.minify
options.sourceURLBase
options.alwaysIncludeFormat
*/
exports.compileDir = function(dir, options) {
dir = path.resolve(dir);
options.sourceURLBase = options.sourceURLBase || '';
// store a list of compile errors
var compileErrors = '';
// create the map config
// convert jspm options.dependencies into a requirable form
// and combine them into a new map object
var map = {}, optionsMap, optionsDependencies;
if (options.map)
optionsMap = options.map;
for (var m in optionsMap)
if (optionsMap.hasOwnProperty(m))
map[m] = optionsMap[m];
if (options.dependencies) {
optionsDependencies = options.dependencies;
for (var d in optionsDependencies) {
if (!optionsDependencies.hasOwnProperty(d))
continue;
// custom map overrides dependency map
if (map[d])
continue;
var curDep = optionsDependencies[d];
if (curDep.includes(':') || curDep.includes('@'))
map[d] = curDep;
else if (curDep && curDep !== '*')
map[d] = d + '@' + curDep;
else
map[d] = d;
}
}
var nl = '\n';
// glob each ".js" file
return asp(glob)(dir + path.sep + '**' + path.sep + '*.js')
.then(function(files) {
return Promise.all(files.map(function(file) {
var changed = false;
var sourceMap;
var format;
if (options.format && typeof options.format === 'string')
format = options.format.toLowerCase();
var relFile = path.relative(dir, file);
var relModule = relFile.substr(0, relFile.length - 3).replace(/\\/g, '/');
// first check if a symlink
return asp(fs.lstat)(file)
.then(function(stats) {
if (stats.isSymbolicLink())
return;
return asp(fs.readFile)(file)
.then(function(source) {
source += '';
return Promise.resolve()
// add shim config if necessary
.then(function() {
if (!options.shim)
return;
var match;
if (!(match = matchWithWildcard(options.shim, relModule)))
return;
var curShim = options.shim[match];
if (curShim instanceof Array)
curShim = { deps: curShim };
// NB backwards-compatible with shim.imports
curShim.deps = curShim.deps || curShim.imports;
if (typeof curShim.deps === 'string')
curShim.deps = [curShim.deps];
var depStr = '"format global";' + nl;
if (curShim.deps)
for (var i = 0; i < curShim.deps.length; i++)
depStr += '"deps ' + curShim.deps[i] + '";' + nl;
if (curShim.exports)
depStr += '"exports ' + curShim.exports + '";' + nl;
changed = true;
source = depStr + source;
return true;
})
// add any format hint if provided
// only add format hint if detection would fail
// also set the format here if not set
// NB all regexs should apply after removing comments
// also ideally format injection should be post-minification
// in case of minification quirks
.then(function(shimmed) {
// don't add format if already shimmed!
if (shimmed)
return;
var detected = detectFormat(source);
// don't rewrite meta
if (detected.meta) {
format = detected.format;
return;
}
if (format == 'es6')
format = 'esm';
if (options.alwaysIncludeFormat || !format || detected.format !== format) {
changed = true;
source = '"format ' + (format || detected.format) + '";' + nl + source;
}
if (!format)
format = detected.format;
})
// apply map config
.then(function() {
// ES Module
if (format === 'esm') {
source = source.replace(esmDepRegEx, function(statement, start, type, str, singleString, doubleString) {
var name = singleString || doubleString;
var mapped = applyMap(name, map, relFile, options.removeJSExtensions);
if (!mapped)
return statement;
changed = true;
return statement.replace(new RegExp('"' + name + '"|\'' + name + '\'', 'g'), '\'' + mapped + '\'');
});
}
// AMD
else if (format === 'amd') {
amdDefineRegEx.lastIndex = 0;
var defineStatement = amdDefineRegEx.exec(source);
if (defineStatement) {
if (!defineStatement[2])
return;
var depArray = eval(defineStatement[2]);
depArray = depArray.map(function(name) {
var mapped = applyMap(name, map, relFile, options.removeJSExtensions);
if (!mapped)
return name;
changed = true;
return mapped;
});
if (changed)
source = source.replace(defineStatement[2], JSON.stringify(depArray));
}
}
// CommonJS
else if (format === 'cjs') {
source = source.replace(cjsRequireRegEx, function(statement, singleString, doubleString) {
var name = singleString || doubleString;
name = name.substr(1, name.length - 2);
var mapped = applyMap(name, map, relFile, options.removeJSExtensions);
if (!mapped)
return statement;
changed = true;
return statement.replace(new RegExp('"' + name + '"|\'' + name + '\'', 'g'), '\'' + mapped + '\'');
});
}
// Global? (including shim?)
// else {
// }
})
// if changed, save these meta-updates into the original file
.then(function() {
// ensure there is a comment at the beginning of the file
// this is necessary to protect the source map when wrapping
if (!source.match(initialCommentRegEx)) {
source = '\/* *\/ \n' + source;
changed = true;
}
if (changed)
return asp(fs.writeFile)(file, source);
})
// transpile
.then(function() {
if (!options.transpile)
return;
var traceur = require('traceur');
traceur.options.sourceMaps = true;
traceur.options.modules = 'instantiate';
try {
var compiler = new traceur.Compiler({
moduleName: '',
modules: 'instantiate'
});
source = compiler.compile(source, relFile, path.basename(relFile.replace(/\.js$/, '.src.js')));
sourceMap = compiler.getSourceMap();
}
catch(e) {
// an error in one compiled file doesn't stop all compilation
if (!e.stack)
compileErrors += + '\n';
else
compileErrors += e.stack + '\n' + relFile + ': Unable to transpile ES Module\n';
}
})
// minify
.then(function() {
if (!options.minify)
return;
var uglify = require('uglify-js');
try {
var ast = uglify.parse(source, { filename: path.basename(relFile.replace(/\.js$/, '.src.js')) });
ast.figure_out_scope();
ast = ast.transform(uglify.Compressor({
warnings: false,
evaluate: false
}));
ast.figure_out_scope();
ast.compute_char_frequency();
ast.mangle_names({
except: ['require']
});
var source_map = uglify.SourceMap({
file: path.basename(relFile),
orig: sourceMap
});
source = ast.print_to_string({
ascii_only: true, // for some reason non-ascii broke esprima
comments: function(node, comment) {
return comment.line === 1 && comment.col === 0;
},
source_map: source_map
});
sourceMap = source_map.toString();
}
catch(e) {
// an error in one compiled file doesn't stop all compilation
compileErrors += relFile + ': Unable to minify file\n';
}
})
// finally, if compiled, rename to the new file with source maps
.then(function() {
if (!options.minify && !options.transpile)
return;
// rename the original with meta changes to .src.js
return asp(fs.rename)(file, file.replace(/\.js$/, '.src.js'))
// write .js as the current source, with a source map comment
.then(function() {
return asp(fs.writeFile)(file, source + '\n//# sourceMappingURL=' + relFile.split('/').pop() + '.map');
})
// write the source map to .js.map
.then(function() {
return asp(fs.writeFile)(file + '.map', sourceMap);
});
});
}, function(e) {
if (e.code === 'EISDIR')
return;
else
throw e;
});
}, function(e) {
// rethrow an error that wasn't a file read error
if (e.code === 'EISDIR')
return;
else
throw e;
});
}));
})
// output of compile promise is any compile errors
.then(function() {
return compileErrors;
});
};