UNPKG

cortex

Version:

Cortex is an npm-like package manager for browsers.

811 lines (684 loc) 20.6 kB
'use strict'; var build = exports; var fs = require('fs'); var fse = require('fs-extra'); var expand = require('fs-expand'); var node_path = require('path'); var semver = require('semver-extra'); var semver_helper = require('../util/semver'); var async = require('async'); var cortex_json = require('read-cortex-json'); var ln = require('../util/link'); var run_scripts = require('../util/run-scripts'); var makeArray = require('make-array'); var mix = require('mix2'); var builder = require('neuron-builder'); var ngraph = require('neuron-graph'); var neuron = require('neuronjs'); var util = require('util'); var MD5 = require('MD5'); // @param {Object} options // see ./lib/option/build.js for details build.run = function(options, callback) { this.MESSAGE = this.locale.require('command-build'); options.install = options['install-build']; options.prerelease = options.prerelease || this.profile.get('prerelease'); var self = this; var tasks = options.install ? [ 'simplely_read_cortex_json', 'read_cortex_config_js', 'prepare_build_type', 'run_preinstall_script', 'run_prebuild_script', 'build_process', 'server_link' ] : [ 'read_cortex_json', 'clean_cortex_json', 'read_cortex_config_js', 'prepare_build_type', 'run_preinstall_script', 'run_prebuild_script', // #478 // `cortex.main` and many other files might have not been generated before `cortex.scripts` // so, we clean cortex_json 'build_process', 'server_link', 'run_postbuild_script', 'run_postwatch_script' ]; async.eachSeries(tasks, function (task, done) { self[task](options, done); }, callback); }; build.read_cortex_config_js = function(options, callback){ // If install from cache, cortex.json will be enhanced json. var file = node_path.join(options.cwd, 'cortex.config.js'); fs.exists(file, function(exists){ options.build_config = exists ? require(file) : {}; callback(null); }); } build.prepare_build_type = function(options, callback){ var pkg = options.pkg; var entries = pkg.entries; var csses = pkg.css; var file = options.file; function hasFile(list){ return list.map(function(rel){ return node_path.join(options.cwd, rel); }).indexOf(file) > -1; } function isDir(){ var dir_paths = []; for(var dir in pkg.directories){ if(dir == 'dist'){continue;} dir_paths.push(node_path.join(options.cwd, pkg.directories[dir])); } if(!file){return false;} return dir_paths.some(function(dir){ return file.indexOf(dir) == 0; }); } if(hasFile(entries)){ options.build_type = 'js'; }else if(hasFile(csses)){ options.build_type = 'css'; }else if(isDir()){ options.build_type = 'dir'; }else{ options.build_type = 'full'; } callback(); }; build.simplely_read_cortex_json = function (options, callback) { // If install from cache, cortex.json will be enhanced json. var file = node_path.join(options.cwd, 'cortex.json'); var self = this; fse.readJson(file, function (err, json) { if (err) { return cb(err); } function cb (err, json) { if (err) { if (err.message) { err.message += ' File: "' + file + '"'; } return callback(err); } options.pkg = json; self.add_prerease(options, json); callback(null); } // Legacy // Before cortexjs/read-cortex-json#11, // the `main`, `css` and `entries` are not always existing. if (!('main' in json) || !util.isArray(json.css) || !util.isArray(json.entries)) { return cortex_json.clean(options.cwd, json, cb); } cb(null, json); }.bind(this)); }; build.read_cortex_json = function(options, callback) { var self = this; cortex_json.extra(options.cwd, function (err, pkg) { if (err) { return callback(err); } options.pkg = pkg; self.add_prerease(options, pkg); callback(null); }); }; build.add_prerease = function (options, pkg) { var pr = options.prerelease; if (pr) { var version = pkg.version; pkg.version = semver_helper.add_prerelease(version, pr); if (!semver.isStable(version) && !semver.isPrerelease(version)) { this.logger.warn( 'Package "' + pkg.name + '@' + version + '" is already a prerelease version, ' + 'but will be built as "' + pkg.version + '" according to option or config.' ); } } }; build.clean_cortex_json = function (options, callback) { cortex_json.clean(options.cwd, options.pkg, callback); }; // Run custom build scripts, such as 'grunt' build.run_preinstall_script = function(options, callback) { if (!options.preinstall) { return callback(null); } this.run_script('preinstall', options, callback); }; // Run custom build scripts, such as 'grunt' build.run_prebuild_script = function(options, callback) { // #436: if build when install, it should not run scripts.prebuild if (!options.prebuild) { return callback(null); } if(options.build_type != 'full'){ return callback(null); } this.run_script('prebuild', options, callback); }; build.run_postbuild_script = function(options, callback){ if (!options.prebuild) { return callback(null); } this.run_script('postbuild', options, callback); } // Hook for watcher first build // you can do stuffs like opening browser here build.run_postwatch_script = function(options, callback){ if (!options.prebuild || !options.init) { return callback(null); } this.run_script('postwatch', options, callback); } build.run_script = function(script, options, callback) { var pkg = options.pkg; var scripts = makeArray(pkg.scripts && pkg.scripts[script]) // skip empty scripts .filter(Boolean); if (!scripts.length) { return callback(null); } var self = this; this.logger.info('{{cyan run}} "scripts.' + script + '" ...'); run_scripts(scripts, options).on('spawn', function(command) { self.logger.info(' - {{cyan exec}} "' + command + '" ...'); }).on('close', function(code, signal) { if (code) { callback({ code: 'EBUILDSCRIPT', message: 'build step "scripts.' + script + '" executes as a failure. exit code: ' + code, data: { code: code, signal: signal, script: script } }); } else { callback(null); } }).on('error', function(err) { self.logger.warn('"scripts.' + script + '" let out an error: ' + err); }); }; build.build_process = function(options, callback) { var pkg = options.pkg; var to = node_path.join(options.dest, pkg.name, pkg.version); options.to = to; var basic_tasks = [ 'write_cortex_json', 'copy_shrinkwrap_json', 'build_engine' ]; // distribution directory var dist = options.pkg.directories && options.pkg.directories.dist; if (dist) { options.dist = dist; basic_tasks.push( 'check_dist' ); } else { basic_tasks.push( 'copy_csses', 'copy_directories', 'build_modules', 'write_md5_json', 'generate_md5_files', 'generate_config' ); } var self = this; if(options.build_type == 'full'){ basic_tasks.unshift('clean_dest'); } async.eachSeries(basic_tasks, function (task, done) { self[task](options, done); }, callback); }; // We should clean the dest folder before we build.clean_dest = function (options, callback) { fs.exists(options.to, function (exists) { if (!exists) { return callback(null); } // #477 // If we delete some files, // these files such as "cortex-shrinkwrap.json" and "src/*" should also be removed // from the dest folder. fse.remove(options.to, callback); }); }; build.is_facade_package = function(pkg){ var name = pkg.name; return name.indexOf("app-") == 0 || pkg.type == "app"; }; build.traverse_file_md5 = function(options, callback){ var to = options.dir; var ignore = options.ignore; var fullMD5 = options.fullMD5; var result = {}; function readdir(dir, callback){ fs.readdir(dir, function(err, list){ async.map(list, function(item, done){ if(item.match(ignore)){ return done(null); } var fullpath = node_path.join(dir, item); fs.stat(fullpath, function(err, stat){ if(err){ return done(err); } if(stat.isDirectory()){ readdir(fullpath, done); }else{ fs.readFile(fullpath, function(err, content){ if(err){ return done(err); } var md5 = MD5(content); if(!fullMD5){ md5 = md5.slice(0,8); } result[node_path.relative(to, fullpath)] = md5; done(null); }); } }); }, function(err){ callback(err); }); }); } readdir(to, function(err){ callback(err, result); }); } build.write_md5_json = function(options, callback){ if(!build.is_facade_package(options.pkg)){ return callback(null); } var to = options.to; var md5_file = node_path.join(options.to, 'md5.json'); var logger = this.logger; build.traverse_file_md5({ dir: to, ignore: "cortex.json", fullMD5: !!options["full-md5"] }, function(err, md5){ if(err){ return callback(err); } logger.info('{{cyan write}} ' + md5_file); fse.outputJson(md5_file, md5, callback); }); } build.generate_md5_files = function(options, callback){ if(!build.is_facade_package(options.pkg)){ return callback(null); } if(!options["generate-md5"]){ return callback(null); } var logger = this.logger; var to = options.to; var md5_file = node_path.join(options.to, 'md5.json'); function appendMD5(path, md5){ return } fse.readJson(md5_file, function(err, json){ if(err){ return callback(err); } var path_objs = Object.keys(json).map(function(path){ return { full_path: node_path.join(to, path), path: path } }); async.map(path_objs, function(path_obj, done){ var md5 = json[path_obj.path]; var ext = node_path.extname(path_obj.full_path); var md5_path = path_obj.full_path.split(ext)[0] + "_" + md5 + ext; logger.info("{{cyan copy}} " + md5_path); fse.copy(path_obj.full_path, md5_path, done) }, callback); }); } build.write_cortex_json = function (options, callback) { var cortex_file = node_path.join(options.to, 'cortex.json'); fse.outputJson(cortex_file, options.pkg, callback); }; build.copy_shrinkwrap_json = function (options, callback) { this.copy(options.cwd, options.to, 'cortex-shrinkwrap.json', callback); }; build.server_link = function (options, callback) { if (options.install) { return callback(null); } if(options.build_type != 'full'){ return callback(null); } var built_root = this.profile.get('built_root'); if (built_root === options.dest) { return callback(null); } var pkg_dir = node_path.join(options.pkg.name, options.pkg.version); var from = node_path.join(built_root, pkg_dir); var to = node_path.join(options.dest, pkg_dir); var logger = this.logger; ln.link(from, to, function (err) { if (err) { return callback(err); } logger.info('{{cyan link}} ' + from + ' -> ' + to); callback(null); }); }; build.check_dist = function (options, callback) { var rel_dist = options.dist; var dist = node_path.join(options.cwd, rel_dist); // if `dist` dir exists, skip building and just copy it fs.exists(dist, function(exists) { if (!exists) { // if `cortex.directories.dist` is declared, the dir must be existed. return callback({ code: 'DIST_NOT_FOUND', message: 'dist dir "' + dist + '" does not exist.', data: { dist: dist } }); } this.logger.info('dist dir "' + dist + '" found, {{cyan skip}} building ...'); this.copy_dist(rel_dist, options, callback); }.bind(this)); }; // copy dist dir to the destination dirs build.copy_dist = function(dist, options, callback) { var self = this; var tasks = options.tasks; var dist_dir = node_path.join(options.cwd, dist); self.copy(dist_dir, options.to, null, callback); }; // Builds JavaScript modules build.build_modules = function(options, callback) { var pkg = options.pkg; var build_type = options.build_type; if(!this._is_type(build_type, 'js')){ return callback(null); } var loaders = options.build_config.loaders || []; // `pkg.entries` must be an array var entries = [].concat(pkg.entries); if (pkg.main && !~entries.indexOf(pkg.main)) { entries.push(pkg.main); } if (!entries.length) { // Pure css package. return callback(null); } var cwd = options.cwd; var to = options.to; var self = this; if(build_type == 'js'){ entries = [node_path.relative(options.cwd, options.file)]; } async.eachSeries(entries, function (entry, done) { var from = node_path.join(cwd, entry); builder({ cwd: cwd, targetVersion: pkg.version, pkg: options.pkg, loaders: loaders }) .on('warn', function (warn) { self.logger.warn(warn.message || warn); }) .parse(from, function (err, content) { if (err) { return done(err); } var file_to = entry === pkg.main // It is a convention that main entry will built to <name>.js ? pkg.name + '.js' : entry; if (file_to != file_to.toLowerCase()) { self.logger.warn(util.format('"%s" contains uppercase characters', file_to)); file_to = file_to.toLowerCase(); } var path_to = node_path.join(to, file_to); fse.outputFile(path_to, content, function (err) { if (err) { return done(err); } self.logger.info('{{cyan write}} ' + path_to); done(null); }); }); }, callback); }; build._is_type = function(build_type, type){ // build_type is from step prepare filelist return build_type == 'full' || build_type == type; }; build.copy_csses = function (options, callback) { var css = options.pkg.css; var build_type = options.build_type; if (!css) { return callback(null); } if(!this._is_type(build_type, 'css')){ return callback(null); } var self = this; var to = options.to; if(build_type == 'css'){ css = [node_path.relative(options.cwd, options.file)]; } async.eachSeries(css, function (path, done) { var from = options.cwd; var css_path = node_path.join(from, path); self.copy(from, to, path, function (err) { if (err && err.code === 'SRC_NOT_FOUND') { err = { code: 'CSS_NOT_FOUND', message: '`pkg.css`, "' + path + '" is declared but not found.', data: { path: path } }; } if(err){ return done(err); } var csspath = node_path.resolve(from, path); fs.stat(csspath, function(err, stat){ if(err){return done(err);} if(stat.isFile()){ self.parse_css_images(csspath, function(err, image_paths){ if(err){return done(err);} async.eachSeries(image_paths, function(image_path, done){ var css_dir = node_path.dirname(csspath); var full_image_path = node_path.join(css_dir, image_path); self.copy(from, to, node_path.relative(from, full_image_path), done); }, done); }); }else{ done(null); } }); }, true); }, callback); }; build.parse_css_images = function(csspath, done){ var self = this; var image_path_cache = build.image_path_cache = build.image_path_cache || {}; fs.readFile(csspath, function(err, content){ if(err){return done(err);} /** * match * 1. url(a.png) * 2. url('http://i1.static.dp/s/c/i/b.png') * 3. url("./c.png") */ var reg = /url\(\s*(['"]?)([^"'\)]*)\1\s*\)/g; var m; var imgpath; var image_paths = []; function isRelative(imgpath) { return !/^https?:\/\//.test(imgpath); } function isDataURI(imgpath){ return imgpath.indexOf("data:") == 0; } while(m = reg.exec(content)){ imgpath = m[0].match(/url\(\s*(['"]?)([^"'\)]*)\1\s*\)/)[2]; if(isRelative(imgpath) && !isDataURI(imgpath) && !image_path_cache[imgpath]){ image_paths.push(imgpath); image_path_cache[imgpath] = true; } } done(null,image_paths); }); } build.copy_directories = function(options, callback) { var self = this; var pkg = options.pkg; var cwd = options.cwd; var directories = pkg.directories || {}; var to = options.to; var file = options.file; var build_type = options.build_type; if(!this._is_type(build_type, 'dir')){ return callback(null); } var tasks = [ // We only support `directories.src` for now. 'src', 'html', 'img', 'font' ].filter(function (dir) { var proj_dir = directories[dir]; if(!file){ return proj_dir; }else{ return proj_dir && file.indexOf(node_path.join(cwd, proj_dir)) == 0; } }); async.eachSeries(tasks, function (name, done) { var dir = directories[name]; self.copy(cwd, to, dir, function (err) { if (err && err.code === 'SRC_NOT_FOUND') { err = { code: 'DIR_NOT_FOUND', message: '`directories.' + name + '.' + dir + '` is defined in cortex.json, but not found.', data: { name: name, dir: dir } }; } done(err); }, true); }, callback); }; // Copy item from `from` to `to` // @param {String=} item If is undefined, will copy `from` to `to` // @param {Boolean} strict build.copy = function(from, to, item, callback, strict) { var self = this; if (from === to) { callback(null); return; } if (item) { if (item != item.toLowerCase()) { self.logger.warn(util.format('"%s" contains uppercase characters', item)); } item = item.toLowerCase(); from = node_path.join(from, item); to = node_path.join(to, item); } fs.exists(from, function (exists) { if (!exists) { // if strict and the source is not found, an error will throw. if (strict) { return callback({ code: 'SRC_NOT_FOUND' }); } else { return callback(null); } } self.logger.info('{{cyan copy}} ' + from + ' -> ' + to); var err; try{ fse.copySync(from, to); }catch(e){ err = e; } callback(err); }); }; build.build_engine = function (options, callback) { if (options.install) { return callback(null); } var dest = node_path.join(options.dest, 'neuron', neuron.version(), 'neuron.js'); var neuron_js = node_path.join(options.dest, 'neuron.js'); // Chrome on Windows could not open a symlink of a javascript file, // so we just write both of the files, and use no symlink. async.each([dest, neuron_js], function (to, done) { neuron.write(to, done, true); }, callback); }; build.generate_config = function (options, callback) { if (options.install || !options.config) { return callback(null); } var pkg = options.pkg; var config = {}; async.series([ function (done){ ngraph(pkg, { built_root: node_path.join(options.cwd, 'neurons'), cwd: options.cwd }, function(err, graph, shrinkwrap){ done(err, graph); }); }, function (done){ var md5_file = node_path.join(options.to, 'md5.json'); if(build.is_facade_package(options.pkg)){ fse.readJson(md5_file, done); }else{ done(null); } } ], function(err, results){ if(err){ return callback(err); } var graph = results[0]; var md5 = results[1]; var graph_entry = graph._; graph_entry[pkg.name + '@*'] = graph_entry[pkg.name + '@' + pkg.version]; var config_file = node_path.join(options.dest, 'config.js'); if(graph){config.graph = graph;} if(md5 && options["generate-md5"]){ config.hash = {}; config.hash[pkg.name + "@" + pkg.version] = md5; } fse.outputFile(config_file, 'neuron.config(' + JSON.stringify(config, null, 2) + ');', callback); }); };