okam-build
Version:
The build tool for Okam develop framework
560 lines (483 loc) • 15.9 kB
JavaScript
/**
* @file Build task manager
* @author sparklewhy@gmail.com
*/
'use strict';
/* eslint-disable fecs-min-vars-per-destructure */
const pathUtil = require('path');
const EventEmitter = require('events');
const {colors, Timer, merge, babel: babelUtil, file: fileUtil} = require('../util');
const loadProcessFiles = require('./load-process-files');
const CacheManager = require('./CacheManager');
const FileOutput = require('../generator/FileOutput');
const processor = require('../processor');
const npm = require('../processor/helper/npm');
const {getDefaultBabelProcessor} = require('../processor/helper/processor');
const ModuleResolver = require('./ModuleResolver');
const allAppTypes = require('./app-type');
const cleanBuild = require('./clean-build');
const initGlobalComponents = require('./global-component');
class BuildManager extends EventEmitter {
constructor(buildConf) {
super();
let {component, appType, logger, resolve} = buildConf;
Object.assign(this, {
buildConf,
logger,
appType,
componentConf: component,
componentExtname: component.extname
});
this.resolver = new ModuleResolver({
logger,
appType,
resolve,
extensions: [component.extname]
});
let env = process.env.NODE_ENV;
this.buildEnv = env;
this.envConfigKey = `_${appType}Env`;
this.isDev = env === 'dev' || env === 'development';
this.isProd = !env || env === 'prod' || env === 'production';
this.initBuildRules(buildConf);
this.runningTasks = [];
this.doneTasks = [];
this.waitingBuildFiles = [];
this.cache = new CacheManager({cacheDir: buildConf.cacheDir});
}
/**
* Initialize the imported global component definition
*
* @private
* @param {Object} componentConf the component config
*/
initGlobalComponents(componentConf) {
this.globalComponents = initGlobalComponents(
this.appType, componentConf, this.sourceDir
);
}
/**
* Initialize used processors
*
* @protected
* @param {Object} buildConf the build config
*/
initProcessor(buildConf) {
// register custom processors
processor.registerProcessor(buildConf.processors);
// register component file default processor
let componentConf = buildConf.component;
if (componentConf && componentConf.extname) {
processor.registerProcessor([{
name: 'component',
extnames: componentConf.extname
}]);
}
this.defaultBabelProcessorName = getDefaultBabelProcessor(
buildConf.processors
);
}
/**
* Initialize build rules
*
* @private
* @param {Object} buildConf the build config
*/
initBuildRules(buildConf) {
let extraConf = this.buildEnv && buildConf[this.buildEnv];
if (this.isDev) {
extraConf || (extraConf = buildConf.dev || buildConf.development);
}
else if (this.isProd) {
extraConf || (extraConf = buildConf.prod || buildConf.production);
}
let {rules: baseRules, processors: baseProcessors} = buildConf;
let {rules, processors} = extraConf || {};
rules && (rules = [].concat(baseRules, rules));
this.rules = rules || baseRules || [];
// add APP_TYPE process env variable replacement processor
this.rules.push({
match(file) {
return file.isScript;
},
processors: [
[
'replacement',
{'process.env.APP_TYPE': `"${this.appType}"`}
]
]
});
processors && (processors = merge({}, baseProcessors, processors));
buildConf.processors = processors || baseProcessors;
this.initProcessor(buildConf);
}
onAddNewFile(file) {
// replace module okam-core/na/index.js content using specified app env module
if (file.path.indexOf('node_modules/okam-core/src/na/index.js') !== -1) {
let naEnvModuleId = `../${this.appType}/env`;
file.content = `'use strict;'\nexport * from '${naEnvModuleId}';\n`;
}
else if (file.path.indexOf('node_modules/regenerator-runtime/runtime.js') !== -1) {
// fix regenerator-runtime>=0.13.2 throw exception in wx
let content = file.content.toString();
file.content = content.replace(
'Function("r", "regeneratorRuntime = r")(runtime);',
match => {
return `try {${match}} catch(e) {}`;
}
);
}
}
/**
* Load the files that will be processed
*/
loadFiles() {
let {
root,
sourceDir,
files,
buildFiles
} = loadProcessFiles(this.buildConf, this.logger);
this.files = files;
this.root = root;
this.sourceDir = sourceDir;
this.babelConfig = babelUtil.readBabelConfig(root);
this.waitingBuildFiles = buildFiles;
this.addNewFileHandler = this.onAddNewFile.bind(this);
files.on('addFile', this.addNewFileHandler);
let {output, wx2swan, designWidth} = this.buildConf;
this.compileContext = {
cache: this.cache,
resolve: npm.resolve.bind(null, this),
addFile: this.addNewFile.bind(this),
getFileByFullPath: this.getFileByFullPath.bind(this),
designWidth,
appType: this.appType,
allAppTypes,
logger: this.logger,
envConfigKey: this.envConfigKey,
sourceDir,
root,
output,
wx2swan,
componentExtname: this.componentExtname
};
this.initGlobalComponents(this.buildConf.component);
this.generator = new FileOutput(this, this.buildConf.output);
}
/**
* Add new file to process
*
* @param {string} fullPath the new file full path to add
* @return {Object}
*/
addNewFile(fullPath) {
let result = this.files.addFile(fullPath);
this.addNeedBuildFile(result);
return result;
}
/**
* Add file that need to build
*
* @param {Object} file the file need to recompile
* @param {boolean=} force whether force recompile if the file is compiled,
* by default false
*/
addNeedBuildFile(file, force = false) {
if (!force && file.compiled) {
return;
}
let reBuilds = this.waitingBuildFiles;
if (reBuilds.indexOf(file) === -1) {
reBuilds.push(file);
}
}
/**
* Resolve module id file path
*
* @param {string} requireModId the module id to require
* @param {string|Object} file the full file path or virtual file object
* to require the given module id
* @param {Object=} opts the extra resolve options
* @return {string}
*/
resolve(requireModId, file, opts) {
return this.resolver.resolve(requireModId, file, opts);
}
/**
* Get mini program native base class, e.g., App/Page/Component
*
* @return {?Object}
*/
getOutputAppBaseClass() {
return this.buildConf.output.appBaseClass;
}
/**
* Get the app base class init options
*
* @param {Object} file the file to process
* @param {Object} config the config info defined in config property
* @param {Object} opts the options
* @param {boolean=} opts.isApp whether is app instance init
* @param {boolean=} opts.isPage whether is page instance init
* @param {boolean=} opts.isComponent whether is component instance init
* @return {?Object}
*/
getAppBaseClassInitOptions(file, config, opts) {
// do nothing, subclass should provide implementation if needed
// for model framework
if (!opts.isApp && this.isEnableModelSupport()) {
return {
isSupportObserve: this.isEnableFrameworkExtension('data')
};
}
return null;
}
/**
* Get the filter transform options
*
* @return {?Object}
*/
getFilterTransformOptions() {
let enable = this.isEnableFilterSupport();
if (enable) {
let isUsingBabel7 = this.defaultBabelProcessorName === 'babel7';
return {
format: 'es6',
usingBabel6: !isUsingBabel7
};
}
return null;
}
/**
* Get the module path resolve to keep path extnames
*
* @return {?Array.<string>}
*/
getModulePathKeepExtnames() {
return null;
}
/**
* Compile file
*
* @param {Object} file the file to compile
* @param {Timer=} timer the timer to statistic the consume time to compile
* @return {boolean}
*/
compile(file, timer) {
timer || (timer = new Timer());
timer.restart();
let logger = this.logger;
let {watch: isWatchMode} = this.buildConf;
try {
processor.compile(file, this);
}
catch (ex) {
logger.error(
'process file fail',
colors.cyan(file.path),
colors.gray(timer.tick())
);
if (!isWatchMode) {
throw ex;
}
else {
logger.error('error stack:\n', ex.stack || ex.message || ex.toString());
return false;
}
}
logger.info('process', colors.cyan(file.path), colors.gray(timer.tick()));
return true;
}
/**
* Clear the old build output
*/
clear() {
let {logger, output: outputOpts} = this.buildConf;
let {dir: outputDir, pathMap: outputPathMap} = outputOpts;
logger.info('clean old build output...');
let projectConfig = outputPathMap.projectConfig;
let filter = (this.getClearFilter && this.getClearFilter())
|| (projectConfig ? [projectConfig] : []);
cleanBuild({
outputDir,
filter
});
}
/**
* Build dependencies files
*
* @param {Timer} t the build timer
* @return {boolean}
*/
buildDependencies(t) {
let buildFail = false;
// build files that need to compile
let waitingBuildFiles = this.waitingBuildFiles;
while (waitingBuildFiles.length) {
let f = waitingBuildFiles.shift();
if (!this.compile(f, t)) {
buildFail = true;
break;
}
}
return buildFail;
}
/**
* Start to build app
*
* @param {Timer} timer the build timer
* @return {Promise}
*/
build(timer) {
let logger = this.logger;
let t = new Timer();
// build files that need to compile
let buildFail = this.buildDependencies(t);
if (buildFail) {
return Promise.reject('error happen');
}
this.onBuildDone && this.onBuildDone();
logger.info('process files done:', colors.gray(timer.tick()));
return Promise.resolve();
}
release(files) {
return this.generator.release(files);
}
createFile(path) {
let file = this.files.getByPath(path);
file || (file = this.files.addFile({path}));
return file;
}
getFileByPath(path) {
return this.files.getByPath(path);
}
getFileByFullPath(fullPath) {
return this.files.getByFullPath(fullPath);
}
getBuildEnv() {
return this.isDev ? 'dev' : (this.isProd ? 'prod' : this.buildEnv);
}
isEnableFrameworkExtension(type) {
let framework = this.buildConf.framework;
return framework && framework.some(pluginType => {
if (Array.isArray(pluginType)) {
pluginType = pluginType[0];
}
return pluginType === type;
});
}
isEnableRefSupport() {
return this.isEnableFrameworkExtension('ref');
}
isEnableMixinSupport() {
return this.isEnableFrameworkExtension('behavior');
}
isEnableFilterSupport() {
return this.isEnableFrameworkExtension('filter');
}
isEnableModelSupport() {
return this.isEnableFrameworkExtension('model');
}
isEnableVHtmlSupport() {
return this.isEnableFrameworkExtension('vhtml');
}
getProcessFileCount() {
return this.files ? this.files.length : 0;
}
getProcessFiles() {
return this.files || [];
}
getFilesByDep(depPath) {
return this.files.getFilesByDep(depPath);
}
getBuildRules() {
return this.rules;
}
isExtractBabelHelper() {
return this.extactBabelHelper;
}
updateFileCompileResult(file, compileResult) {
if (!compileResult) {
return;
}
let {rext, content, deps, sourceMap, ast} = compileResult;
file.compiled = true;
file.content = content;
rext && (file.rext = rext);
ast && (file.ast = ast);
deps && deps.forEach(item => {
this.logger.debug('add dep', item);
if (!pathUtil.isAbsolute(item)) {
item = pathUtil.join(
pathUtil.dirname(file.fullPath), item
);
this.logger.debug('absolute dep', item);
}
let depFile = this.files.addFile(item);
file.addDeps(depFile.path);
this.addNeedBuildFile(depFile);
});
sourceMap && (file.sourceMap = sourceMap);
}
removeAsyncTask(promise) {
let tasks = this.runningTasks;
for (let i = tasks.length - 1; i >= 0; i--) {
let item = tasks[i];
if (item.promise === promise) {
tasks.splice(i, 1);
return item;
}
}
}
addAsyncTask(file, promise) {
file.processing = true;
promise.then(
null,
err => {
return {
err
};
}
).then(
res => {
file.processing = false;
this.removeAsyncTask(file);
if (!res || res.err) {
this.logger.error(
`process file ${file.path} async task error:`,
res && res.err
);
if (file.isImg) {
// output image file even if fail
this.emit('asyncDone', file);
}
else {
this.emit('asyncError', res ? res.err : 'error');
}
}
else {
this.updateFileCompileResult(file, res);
this.emit('asyncDone', file);
}
}
);
this.runningTasks.push({file, promise});
}
}
/**
* Create build manager
*
* @param {string} appType the app type to build
* @param {Object} buildConf the build config
* @return {BuildManager}
*/
BuildManager.create = function (appType, buildConf) {
let buildManagerPath = pathUtil.join(__dirname, appType, 'index.js');
let BuildClass = BuildManager;
if (fileUtil.isFileExists(buildManagerPath)) {
BuildClass = require(buildManagerPath);
}
return new BuildClass(buildConf);
};
module.exports = exports = BuildManager;