UNPKG

chameleon-tool

Version:

chameleon 脚手架工具

721 lines (670 loc) 23.1 kB
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); } }) }) }