chameleon-tool
Version:
chameleon 脚手架工具
721 lines (670 loc) • 23.1 kB
JavaScript
var path = require('path')
var ExtractTextPlugin = require('cml-extract-css-webpack-plugin')
var fs = require('fs');
const fse = require('fs-extra');
const HtmlWebpackPlugin = require('html-webpack-plugin')
let webpostcssLoader = 'postcss-loader';
const portfinder = require('portfinder');
const analyzeTemplate = require('chameleon-template-parse').analyzeTemplate;
const cmlUtils = require('chameleon-tool-utils');
exports.getPostcssrcPath = function (type) {
return path.join(__dirname, `./postcss/${type}/.postcssrc.js`);
}
exports.cssLoaders = function (options) {
options = options || {}
var cssLoader = {
loader: 'css-loader',
options: {
minimize: options.minimize,
sourceMap: false
}
}
function getPostCssLoader(type) {
return {
loader: 'postcss-loader',
options: {
sourceMap: false,
config: {
path: exports.getPostcssrcPath(type)
}
}
}
}
function getMiniappLoader(type) {
// 把chameleon-css-loader需要在postcssloader之后处理,postcssloader先处理@import,否则@import文件中的内容不经过chameleon-css-loader
// chameleon-css-loader需要再最后处理,需要标准的css格式
return [
{
loader: 'chameleon-css-loader',
options: {
platform: 'miniapp',
cmlType: type
}
},
getPostCssLoader(type)
]
}
function addMediaLoader(loaders, type) {
loaders.push({
loader: 'chameleon-css-loader',
options: {
media: true,
cmlType: type
}
})
return loaders
}
// generate loader string to be used with extract text plugin
function generateLoaders(loader, loaderOptions) {
// 扩展流程
if (cml.config.get().extPlatform && ~Object.keys(cml.config.get().extPlatform).indexOf(options.type)) {
let extLoaders = [
{
loader: 'mvvm-style-loader'
},
getPostCssLoader('extend')
]
if (loader) {
extLoaders.push(
{
loader: loader + '-loader',
options: Object.assign({}, loaderOptions, {
sourceMap: false
})
}
)
}
addMediaLoader(extLoaders, options.type);
return extLoaders;
}
var loaders = [cssLoader];
let result = [];
if (options.type === 'web') {
// 把chameleon-css-loader需要在postcssloader之后处理,postcssloader先处理@import,否则@import文件中的内容不经过chameleon-css-loader
// chameleon-css-loader需要再最后处理,需要标准的css格式
loaders.push({
loader: 'chameleon-css-loader',
options: {
platform: 'web',
...cml.config.get().cmss
}
})
loaders.push(getPostCssLoader('web'))
}
if (~['wx', 'alipay', 'baidu', 'qq', 'tt'].indexOf(options.type)) {
loaders = loaders.concat(getMiniappLoader(options.type))
}
if (options.type === 'weex') {
// 把wchameleon-css-loader需要在postcssloader之后处理,postcssloader先处理@import,否则@import文件中的内容不经过chameleon-css-loader
// weex不能使用css-loader css-loader就开始添加module.exports模块化代码,而weex-vue-loader中内部会处理css字符串为对象
loaders = [
{
loader: 'chameleon-css-loader',
options: {
platform: 'weex'
}
},
getPostCssLoader('weex')
];
}
if (loader) {
loaders.push({
loader: loader + '-loader',
options: Object.assign({}, loaderOptions, {
sourceMap: false
})
})
}
if (options.media === 'export' && options.type !== 'wx' && options.mode !== 'production') {
loaders.push(getPostCssLoader('export'));
}
if (options.type === 'weex') {
result = loaders;
} else {
if (options.extract) {
result = ExtractTextPlugin.extract({
use: loaders,
fallback: 'vue-style-loader'
})
} else {
result = ['vue-style-loader'].concat(loaders)
}
}
addMediaLoader(result, options.type);
return result;
}
var result = {
css: generateLoaders('less'),
postcss: generateLoaders(),
less: generateLoaders('less'),
// sass: generateLoaders('sass', { indentedSyntax: true }),
// scss: generateLoaders('sass'),
stylus: generateLoaders('stylus'),
styl: generateLoaders('stylus'),
js: exports.getJsLoader() // 处理vue-loader weex-vue-loader chameleon-loader中的js
}
return result;
}
exports.getJsLoader = function () {
return {
loader: 'babel-loader',
options: {
filename: path.join(cml.root, 'chameleon.js')
}
}
}
// Generate loaders for standalone style files (outside of .vue)
exports.styleLoaders = function (options) {
var output = []
var loaders = exports.cssLoaders(options)
delete loaders.js;
for (var extension in loaders) {
if (loaders.hasOwnProperty(extension)) {
var loader = loaders[extension]
if (extension === 'css') {
output.push({
test: new RegExp('\\.' + extension + '$'),
exclude: new RegExp('min\\.' + extension + '$'),
use: loader
})
let minLoader = loader;
// min.css 为后缀的css不过postcss-loader
minLoader.splice(minLoader.indexOf(webpostcssLoader), 1);
output.push({
test: new RegExp('min\\.' + extension + '$'),
use: minLoader
})
} else {
output.push({
test: new RegExp('\\.' + extension + '$'),
use: loader
})
}
}
}
return output
}
exports.updateEntry = function (updateEntryConfig) {
try {
let { entry, cmlType, root, addEntry } = updateEntryConfig;
let { components, compileTagMap } = cml.utils.getBuildinComponents(cmlType, root);
let options = { buildInComponents: compileTagMap, usedBuildInTagMap: {}, cmlType};
let source = '';
Object.keys(entry).forEach(key => {
if (cml.utils.isFile(entry[key])) {
let content = fs.readFileSync(entry[key], 'utf-8');
let parts = cml.utils.splitParts({ content });
if (parts && parts.template.length) {
source = parts.template[0].tagContent;
options = analyzeTemplate(source, options)
}
}
});
let usedBuildInTagMap = options.usedBuildInTagMap;
// 收集用了哪些内置组件 usedBuildInTagMap:{button:'cml-buildin-button',radio:'cml-buildin-radio'}
let usedBuildInTagValues = Object.values(usedBuildInTagMap)
components = components.filter((component) => ~usedBuildInTagValues.indexOf(component.name));
components.forEach(item => {
addEntry(item.filePath)
});
} catch (e) {
console.log('updateEntry', e)
}
}
exports.getMiniAppEntryFunc = function (cmlType) {
return function () {
return exports.getMiniAppEntry(cmlType);
}
}
exports.getMiniAppEntry = function (cmlType) {
let options = cml.config.get()[cmlType][cml.media];
let root = cml.projectRoot;
let entry = {};
entry.common = ['chameleon-runtime/index.js', 'chameleon-store/index.js'];
if (cml.config.get().baseStyle[cmlType] === true) {
// 将 page.css index.css 作为入口文件,注意这里会在 compalation.assets 中产生一个 js 文件 一个css文件
let pageCssPath = path.join(cml.projectRoot, 'node_modules', `chameleon-runtime/src/platform/${cmlType}/style/page.css`)
// 兼容老的chameleon-runtime 版本没有 page.css 这个文件;
let hasPageCss = cmlUtils.isFile(pageCssPath);
entry['static/css/index'] = `chameleon-runtime/src/platform/${cmlType}/style/index.css`;
hasPageCss && (entry['static/css/page'] = `chameleon-runtime/src/platform/${cmlType}/style/page.css`);
}
let globalStyleConfig = cml.config.get().globalStyleConfig;
if (globalStyleConfig && globalStyleConfig.globalCssPath && cmlUtils.isFile(globalStyleConfig.globalCssPath)) {
entry['static/css/global'] = globalStyleConfig.globalCssPath
}
let projectPath = path.resolve(root, 'src');
// 记录已经添加的入口,防止重复循环添加
let hasEntryedPath = [];
// 组件导出
if (cml.media === 'export') {
let entryList = cml.config.get()[cmlType]['export'].entry;
let exportEntryFile = cml.utils.getExportEntry(cmlType, root, entryList);
exportEntryFile.forEach(item => {
addEntry(item);
})
} else {
if (options.babelPolyfill === true) {
entry.common.unshift(path.join(__dirname, 'default/miniappPolyfill.js'));
}
entry.app = path.join(projectPath, 'app/app.cml');
let appjson = cml.utils.getJsonFileContent(path.resolve(cml.projectRoot, 'src/app/app.cml'), cmlType)
appjson.pages && appjson.pages.forEach(item => {
let cmlFilePath = path.resolve(cml.projectRoot, 'src', item + '.cml')
addEntry(cmlFilePath)
})
let npmComponents = cml.utils.getNpmComponents(cmlType, root);
if (!cml.config.get().isBuildInProject && cml.media !== 'build') {
let buildInComponens = cml.utils.getBuildinComponents(cmlType, root).components;
npmComponents = npmComponents.concat(buildInComponens);
}
npmComponents.forEach(item => {
addEntry(item.filePath)
})
// subProject的入口
let subProject = cml.config.get().subProject;
if (subProject && subProject.length > 0) {
subProject.forEach(function(item) {
let npmName = cmlUtils.isString(item) ? item : item.npmName;
let npmRouterConfig = JSON.parse(fs.readFileSync(path.join(cml.projectRoot, 'node_modules', npmName, 'src/router.config.json'), {encoding: 'utf-8'}));
npmRouterConfig.routes && npmRouterConfig.routes.forEach(item => {
let routePath = item.path;
let cmlFilePath = path.join(root, 'node_modules', npmName, 'src', routePath + '.cml');
if (cml.utils.isFile(cmlFilePath)) {
addEntry(cmlFilePath);
} else {
cml.log.error(`${cmlFilePath} is not find!`);
}
})
})
}
}
exports.updateEntry({ entry, cmlType, root, addEntry });
return entry;
function addEntry(chameleonFilePath) {
// plugin://
if (!chameleonFilePath) {
return;
}
if (!cml.utils.isFile(chameleonFilePath)) {
return;
}
if (~hasEntryedPath.indexOf(chameleonFilePath)) {
return;
}
hasEntryedPath.push(chameleonFilePath);
let entryName = cml.utils.getPureEntryName(chameleonFilePath, cmlType, root);
// 小程序中有文件夹有@符号无法上传 决定生成样式文件路径
entryName = cml.utils.handleSpecialChar(entryName);
entry[entryName] = chameleonFilePath;
// 处理json文件中引用的组件作为入口,wxml文件
let targetObject = cml.utils.getJsonFileContent(chameleonFilePath, cmlType)
if (targetObject && targetObject.usingComponents) {
let usingComponents = targetObject.usingComponents;
Object.keys(usingComponents).forEach(key => {
let comPath = usingComponents[key];
let { filePath } = cml.utils.handleComponentUrl(root, chameleonFilePath, comPath, cmlType);
addEntry(filePath);
})
}
}
}
exports.getHtmlPluginConfig = function(options) {
let {filename, template, chunksort, entryName} = options;
var htmlConf = {
filename: filename,
template,
inject: 'body',
// hash: true,
chunks: ['manifest', 'vender', entryName],
chunksSortMode: function (a, b) {
var aIndex = chunksort.indexOf(a.names[0]);
var bIndex = chunksort.indexOf(b.names[0]);
aIndex = aIndex < 0 ? chunksort.length + 1 : aIndex;
bIndex = bIndex < 0 ? chunksort.length + 1 : bIndex;
return aIndex - bIndex;
}
}
if (options.console === true) {
htmlConf.console = true
htmlConf.consolejs = '/preview-assets/didiConsole-1.0.7.min.js'
}
return htmlConf
}
exports.getWebEntry = function (options) {
if (cml.media === 'export') {
return exports.getWebExportEntry();
}
exports.copyDefaultFile(options.root, 'web', options.media);
var entry = {};
entry.vender = ['vue', 'vuex', 'vue-router', path.resolve(cml.root, 'configs/web_global.js')];
if (options.babelPolyfill === true) {
entry.vender.unshift('@babel/polyfill');
}
// web端插入全局样式
if (cml.config.get().baseStyle.web === true) {
entry.vender.push('chameleon-runtime/src/platform/web/style/index.css')
}
// 注入用户全局样式
let globalStyleConfig = cml.config.get().globalStyleConfig;
if (globalStyleConfig && globalStyleConfig.globalCssPath && cmlUtils.isFile(globalStyleConfig.globalCssPath)) {
entry.vender.push(globalStyleConfig.globalCssPath)
}
if (cml.config.get().cmss.rem === true) {
entry.vender.unshift(path.resolve(cml.root, 'configs/default/rem.js'));
}
var htmlPlugins = [];
let entryConfig = cml.config.get().entry;
// 配置web入口
var entryFile;
if (entryConfig && entryConfig.web) {
if (cml.utils.isFile(entryConfig.web)) {
entryFile = entryConfig.web;
} else {
throw new Error('no such file: ' + entryConfig.web);
}
} else {
entryFile = path.join(cml.projectRoot, 'node_modules/chameleon-runtime/.temp/entry.js');
}
var entryName = exports.getEntryName();
// entry[entryName] = entryFile;
var chunksort = ['manifest', 'vender'];
let filename = `${entryName}.html`;
if (cml.config.get().templateType === 'smarty') {
filename = `template/${entryName}.tpl`;
}
let template;
if (entryConfig && entryConfig.template) {
if (cml.utils.isFile(entryConfig.template)) {
template = entryConfig.template;
} else {
throw new Error('no such file: ' + entryConfig.template);
}
} else {
if (cml.config.get().templateType === 'smarty') {
template = path.resolve(__dirname, './default/smarty_entry.html');
} else {
template = path.resolve(__dirname, './default/html_entry.html');
}
}
// ---->web端多页面构建
const {routerConfig} = cmlUtils.getRouterConfig();
let mpa = routerConfig.mpa;
if (mpa && mpa.webMpa && Array.isArray(mpa.webMpa)) { // 配置了web端多页面
let webMpa = mpa.webMpa;
for (let i = 0; i < webMpa.length ; i++) {
// 多入口增加
if (typeof webMpa[i].name === 'string') {
entry[`${webMpa[i].name}`] = `${entryFile}?query=${i}`;
let htmlConf = exports.getHtmlPluginConfig({
filename: cml.config.get().templateType === 'smarty' ? `template/${webMpa[i].name}.tpl` : `${webMpa[i].name}.html`,
template,
chunksort,
entryName: `${webMpa[i].name}`,
console: options.console
});
htmlPlugins.push(new HtmlWebpackPlugin(htmlConf));
} else {
entry[`${entryName}${i}`] = `${entryFile}?query=${i}`;
let htmlConf = exports.getHtmlPluginConfig({
filename: cml.config.get().templateType === 'smarty' ? `template/${entryName}${i}.tpl` : `${entryName}${i}.html`,
template,
chunksort,
entryName: `${entryName}${i}`,
console: options.console
});
htmlPlugins.push(new HtmlWebpackPlugin(htmlConf));
}
}
} else { // 没有配置web端多页面,那么还是走原来的逻辑
entry[entryName] = entryFile;
let htmlConf = exports.getHtmlPluginConfig({
filename, template, chunksort, entryName, console: options.console
})
htmlPlugins.push(new HtmlWebpackPlugin(htmlConf));
}
// ---->
if (options.media === 'dev') {
let origin = path.join(__dirname, './preview.html');
let target = path.join(cml.utils.getDevServerPath(), 'preview.html')
fse.copySync(origin, target, {
overwrite: true
})
}
return {
entry,
htmlPlugins
}
}
exports.getWebExportEntry = function () {
var entry = {};
let entryList = cml.config.get().web['export'].entry;
let exportEntryFile = cml.utils.getExportEntry('web', cml.projectRoot, entryList);
// 记录已经添加的入口,防止重复循环添加
let hasEntryedPath = [];
exportEntryFile.forEach(item => {
addEntry(item);
})
function addEntry(chameleonFilePath) {
// plugin://
if (!chameleonFilePath) {
return;
}
if (~hasEntryedPath.indexOf(chameleonFilePath)) {
return;
}
hasEntryedPath.push(chameleonFilePath);
let entryName = cml.utils.getPureEntryName(chameleonFilePath, 'web', cml.projectRoot);
entry[entryName] = chameleonFilePath;
}
return {
entry,
htmlPlugins: []
};
}
exports.getWeexEntry = function (options) {
if (options.media === 'export') {
return exports.getWeexExportEntry(options);
}
exports.copyDefaultFile(options.root, 'weex', options.media);
var entry = {};
var entryFile = [];
let entryConfig = cml.config.get().entry;
let entryJS = path.join(cml.projectRoot, 'node_modules/chameleon-runtime/.temp/entry.js');
if (entryConfig && entryConfig.weex) {
if (cml.utils.isFile(entryConfig.weex)) {
entryFile.push(entryConfig.weex)
} else {
throw new Error('no such file: ' + entryConfig.weex);
}
} else {
entryFile.push(entryJS);
}
if (options.media === 'dev') {
exports.copyWeexLiveLoadFile(options.root, 'weex', options.media);
entryFile.push(path.join(cml.projectRoot, 'node_modules/chameleon-runtime/.temp/weex_liveload_entry.js'));
}
if (options.babelPolyfill === true) {
entryFile.unshift(path.join(__dirname, 'default/miniappPolyfill.js'));
}
var entryName = exports.getEntryName();
const {routerConfig} = cmlUtils.getRouterConfig();
let mpa = routerConfig.mpa;
if (mpa && mpa.weexMpa && Array.isArray(mpa.weexMpa)) { // 配置了weex多页面
let weexMpa = mpa.weexMpa;
for (let i = 0; i < weexMpa.length ; i++) {
let newEntry = entryFile.map((item) => (item === entryJS) ? `${item}?query=${i}` : item);
if (typeof weexMpa[i].name === 'string') {
entry[`${weexMpa[i].name}`] = newEntry;
} else {
entry[`${entryName}${i}`] = newEntry;
}
}
} else { // 兼容原来的没有配置的情况
entry[`${entryName}`] = entryFile
}
return entry;
}
exports.getWeexExportEntry = function () {
var entry = {};
let entryList = cml.config.get().weex['export'].entry;
let exportEntryFile = cml.utils.getExportEntry('weex', cml.projectRoot, entryList);
// 记录已经添加的入口,防止重复循环添加
let hasEntryedPath = [];
exportEntryFile.forEach(item => {
addEntry(item);
})
function addEntry(chameleonFilePath) {
// plugin://
if (!chameleonFilePath) {
return;
}
if (~hasEntryedPath.indexOf(chameleonFilePath)) {
return;
}
hasEntryedPath.push(chameleonFilePath);
let entryName = cml.utils.getPureEntryName(chameleonFilePath, 'weex', cml.projectRoot);
entry[entryName] = chameleonFilePath;
}
return entry;
}
exports.getEntryName = function () {
return cml.config.get().projectName || 'main'
}
exports.getTempPath = function () {
return cml.utils.getTempRoot();
}
exports.getDevServerPath = function () {
return cml.utils.getDevServerPath();
}
let babelNpm = [
'chameleon-ui-builtin',
'cml-ui',
'chameleon-runtime',
'chameleon-store',
'chameleon-bridge',
'chameleon-scroll',
'chameleon-api',
'chameleon-tool-utils',
'chameleon-css-loader',
'chameleon-miniapp-target',
'chameleon-mixins',
'chameleon-template-parse',
'chameleon-templates',
'chameleon-vue-precompiler',
'chameleon-webpack-plugin',
'chameleon-dev-proxy',
'chameleon-linter',
'interface-loader',
'chameleon-url-loader',
'webpack-check-plugin',
'webpack-liveload-middleware',
'chameleon-weex-vue-loader',
'babel-plugin-chameleon-import',
'mvvm-interface-parser',
'chameleon-loader',
'mobx'
];
exports.getBabelPath = function () {
let babelPath = [
path.join(cml.projectRoot, 'src'),
path.join(cml.root, 'configs')
]
babelNpm.forEach(item => {
if (typeof item === 'string') {
babelPath.push(path.join(cml.projectRoot, 'node_modules', item))
babelPath.push(path.join(cml.root, 'node_modules', item))
} else if (item instanceof RegExp) {
babelPath.push(item)
}
})
let configBabelPath = cml.config.get().babelPath || [];
return configBabelPath.concat(babelPath);
}
exports.getExcludeBabelPath = function() {
let excludeBablePath = [/(\.min\.js)/, /node_modules\/core-js/];
let configExcludePath = cml.config.get().excludeBablePath || [];
return excludeBablePath.concat(configExcludePath);
}
exports.getGlobalCheckWhiteList = function () {
return [
/node_modules[\/\\](mobx|vuex)/
].concat(cml.config.get().globalCheckWhiteList)
}
let hasCopy = false;
exports.copyDefaultFile = function (dir, platform, media) {
// web和weex端同时启动时会拷贝两次,引起重新编译
if (hasCopy) {
return;
}
hasCopy = true;
// 创建路由文件
cml.utils.createRouterFile('web', media);
fse.copySync(path.resolve(__dirname, './default/router.js'), path.resolve(dir, 'node_modules/chameleon-runtime/.temp/router.js'), {
overwrite: true
})
fse.copySync(path.resolve(__dirname, './default/entry.js'), path.resolve(dir, 'node_modules/chameleon-runtime/.temp/entry.js'), {
overwrite: true
})
}
exports.copyWeexLiveLoadFile = function(dir, platform, media) {
fse.copySync(path.resolve(__dirname, './default/weex_liveload_entry.js'), path.resolve(dir, 'node_modules/chameleon-runtime/.temp/weex_liveload_entry.js'), {
overwrite: true
})
}
let webServerPort;
let weexLiveLoadPort;
exports.setFreePort = async function () {
if (webServerPort && weexLiveLoadPort) {
return;
}
if (cml.utils.is(cml.config.get().devPort, 'Number')) {
webServerPort = cml.config.get().devPort;
} else {
await portfinder.getPortPromise({
port: 8000, // minimum port
stopPort: 8999 // maximum port
})
.then((port) => {
webServerPort = port;
}, (err) => {
throw err;
});
}
await portfinder.getPortPromise({
port: 3000, // minimum port
stopPort: 3999 // maximum port
})
.then((port) => {
weexLiveLoadPort = port;
}, (err) => {
throw err;
})
}
exports.getFreePort = function () {
if (webServerPort && weexLiveLoadPort) {
return {
webServerPort,
weexLiveLoadPort
}
} else {
cml.log.error('请先调用setFreePort');
}
}
exports.addCahceLoader = (config, type) => {
const cacheUseLoader = {
loader: 'cache-loader',
options: {
cacheDirectory: path.resolve(process.cwd(), `node_modules/.cache/cache-loader/cache_${type}`)
}
}
const testee = ['.interface', '.cml', '.vue', '.js'];
config.module.rules.forEach(rule => {
testee.some(ext => {
const isRegExp = Object.prototype.toString.call(rule.test) === '[object RegExp]';
if (isRegExp && rule.test.test(ext)) {
return rule.use && rule.use.unshift(cacheUseLoader);
}
})
})
}