// @ts-nocheck const path = require('path'); const fs = require('fs'); const os = require('os'); const chokidar = require('chokidar'); const _ = require('lodash'); const fetch = require('node-fetch'); const shelljs = require('shelljs'); const parser = require('@babel/parser'); const traverse = require('@babel/traverse').default; const t = require('@babel/types'); const glob = require('fast-glob'); const chalk = require('chalk'); const MiniCssExtractPlugin = require('@builder/pack/deps/mini-css-extract-plugin'); const openBrowser = require('@builder/pack/deps/react-dev-utils/openBrowser'); const AssetsInfoPlugin = require('./assetsInfoPlugin'); const DebugModePlugin = require('./debugModePlugin'); const configAssetsRule = (config, type, testReg, loaderOpts = {}) => { if (type === 'img') { config.module.rule(type) .test(testReg) .set('type', 'asset') .use('image-webpack-loader') .loader(require.resolve('image-webpack-loader')) .options({ mozjpeg: { progressive: true, }, optipng: { enabled: false, }, pngquant: { quality: [0.65, 0.9], speed: 4, }, gifsicle: { interlaced: false, }, webp: { quality: 75, }, }); } else { config.module.rule(type).test(testReg) .set('type', 'asset') .set('generator', { dataUrl: loaderOpts, }) .set('parser', { dataUrlCondition: { maxSize: 8 * 1024 // 8kb } }); } }; module.exports = ( { context, onGetWebpackConfig, registerUserConfig, applyMethod, onHook, }, ) => { // 第一项参数为插件 API 提供的能力 const { rootDir, command, userConfig, webpack, pkg } = context; const isWebpack4 = /^4\./.test(webpack.version); // 注册build.json自定义参数 [ { name: 'assetsInfo', validation: 'boolean', defaultValue: true, }, { name: 'removeConsoleExcludeMethod', validation: 'array', }, { name: 'distributedRouter', validation: 'boolean', }, { name: 'openMixteam', validation: 'boolean', }, ].forEach((args) => { registerUserConfig(args); }); onGetWebpackConfig((config) => { // 调整 chunkFilename 格式,默认的[id]会有缓存问题 config.output.chunkFilename('[chunkhash].js'); config.plugin('MiniCssExtractPlugin') .tap(([args]) => [ { ...args, chunkFilename: '[chunkhash].css', ignoreOrder: true, // 移除css顺序警告 }, ]); // 增加对本地字体文件的支持 ['scss', 'scss-module'].forEach(style => { const rule = config.module.rule(style); rule.use('resolve-url-loader') .loader(require.resolve('resolve-url-loader')) .options({ keepQuery: true, removeCR: true, }) .before('sass-loader'); // 添加global.scss的支持,ice框架似乎不支持 if (fs.existsSync(path.join(rootDir, './src/global.scss'))) { rule.use('sass-resources-loader') .loader(require.resolve('sass-resources-loader')) .options({ resources: path.join(rootDir, './src/global.scss'), }) .after('sass-loader'); } }); // 移除对基础组件库样式的解析,因基础库已经是postcss优化后的,无需再次解析,以提升性能 config.module.rule('scss').exclude.add(/\.teamix\/pages\/ui\/.+\.scss$/); // 针对基础组件库样式的解析 config.module .rule('teamix/ui').test(/\.teamix\/pages\/ui\/.+\.scss$/) .use('MiniCssExtractPlugin.loader') .loader(MiniCssExtractPlugin.loader) // compatible with commonjs syntax: const styles = require('./index.module.less') .options({ esModule: false, }) .end() .use('css-loader') .loader(require.resolve('@builder/pack/deps/css-loader/cjs.js')) .options({ sourceMap: true, }) .end() .use('sass-loader') .loader(require.resolve('@builder/pack/deps/sass-loader/cjs.js')) .end(); // 删除其他插件对于url-loader的配置,重新定义 ['woff2', 'ttf', 'eot', 'svg', 'img'].forEach(type => { config.module.rules.delete(type); }); if (isWebpack4) { config.module.rule('url-loader') .test(/\.(bmp|png|jpg|jpeg|gif|woff|woff2|eot|ttf|otf|svg|mp4|webm|wav|mp3|m4a|aac|oga)((\?|#).*)?$/) .use('url-loader') .loader(require.resolve('@builder/pack/deps/url-loader/cjs.js')) .options({ limit: 8192, name: 'assets/[hash].[ext]', }); } else { [ ['woff2', /\.woff2?$/, { mimetype: 'application/font-woff' }], ['ttf', /\.ttf$/, { mimetype: 'application/octet-stream' }], ['eot', /\.eot$/, { mimetype: 'application/vnd.ms-fontobject' }], ['svg', /\.svg$/, { mimetype: 'image/svg+xml' }], ['img', /\.(bmp|png|jpg|webp|jpeg|gif)$/i], ['media', /\.(mp4|webm|wav|mp3|m4a|aac|oga)$/i], ].forEach(([type, reg, opts]) => { configAssetsRule(config, type, reg, opts || {}); }); } if (!isWebpack4) { config.resolve.alias .set('~@teamix/ui', '@teamix/ui') .set('~@alicloudfe/components', '@@alicloudfe/components'); } // ice2.x 里 devPublicPath 并未实现该功能,以前使用的是webpack4 的默认继承能力,webpack5取消了默认继承 // 此处需要特殊处理下 if (command === 'start' && userConfig.devPublicPath && !isWebpack4) { const originalDevMiddleware = config.devServer.get('devMiddleware'); const newDevMiddleware = { ...originalDevMiddleware }; newDevMiddleware.publicPath = userConfig.devPublicPath; config.devServer.set('devMiddleware', newDevMiddleware); } if (command === 'start' && !isWebpack4) { // 指定 socket host,否则默认为空,则 / 指向当前的页面域名,该域名未提供相关 socket 接口 const originalClient = config.devServer.get('client'); const newClient = { ...originalClient }; if (typeof newClient.webSocketURL !== 'string') { newClient.webSocketURL.hostname = 'localhost'; config.devServer.set('client', newClient); } // ice2.x 未提供预检请求的 OPTIONS 方法,此处添加下 const originalDevServeBefore = config.devServer.get('onBeforeSetupMiddleware'); const devServerBefore = (...args) => { const [server] = args; const app = server.app; app.options('*', (req, res) => { res.send(); }); if (typeof originalDevServeBefore === 'function') { originalDevServeBefore(...args); } }; config.devServer.set('onBeforeSetupMiddleware', devServerBefore); } if (command === 'build' && userConfig.assetsInfo) { const buildDir = config.toConfig().output.path; config.plugin('AssetsInfoPlugin') .use(AssetsInfoPlugin, [{ assetsPath: buildDir }]); } // start 模式下增加开发模式插件 if (command === 'start') { config.plugin('DebugModePlugin') .use(DebugModePlugin); } if (command === 'build') { const { removeConsoleExcludeMethod = [] } = userConfig; // 添加 babel 插件 const tapFn = options => { const { plugins = [] } = options; return { ...options, plugins: [ ...plugins, [ require.resolve('babel-plugin-transform-remove-console'), { exclude: removeConsoleExcludeMethod, }, ], ], }; }; ['jsx', 'tsx'].forEach(type => { config.module.rule(type) .use('babel-loader') .tap(tapFn); }) } }); // teamix 依赖自更新逻辑 onHook('before.start.load', async () =>{ try { console.log('正在检查工程依赖更新'); const currentTeamixVersion = require('teamix/package.json').version; const currentPluginTeamixVersion = require('../package.json').version; const currentPluginAppCoreVersion = require('teamix-app-core/package.json').version; const versions = await Promise.all([ fetch('https://registry.npm.alibaba-inc.com/teamix').then(res => res.json()), fetch('https://registry.npm.alibaba-inc.com/build-plugin-teamix').then(res => res.json()), fetch('https://registry.npm.alibaba-inc.com/teamix-app-core').then(res => res.json()), ]); const hasNew = []; if (versions && versions.length) { if (versions[0] && versions[0]['dist-tags'] && versions[0]['dist-tags'].latest && currentTeamixVersion !== versions[0]['dist-tags'].latest) { hasNew.push('teamix'); } if (versions[1] && versions[1]['dist-tags'] && versions[1]['dist-tags'].latest && currentPluginTeamixVersion !== versions[1]['dist-tags'].latest) { hasNew.push('build-plugin-teamix'); } if (versions[2] && versions[2]['dist-tags'] && versions[2]['dist-tags'].latest && currentPluginAppCoreVersion !== versions[2]['dist-tags'].latest) { hasNew.push('teamix-app-core'); } if (hasNew.length) { shelljs.cd(rootDir); shelljs.exec(`tnpm update ${hasNew.join(' ')} --save-dev`); console.log('工程依赖已更新'); process.send({ type: 'RESTART_DEV' }); } } } catch (err) { } }); // 分布式路由生成 page 集合页面 const generatePageFile = () => { const pages = fs.readdirSync(path.join(rootDir, './src/pages')).filter(page => fs.statSync(path.join(rootDir, `./src/pages/${page}`)).isDirectory()) || []; const capitalizeAndCamelCase = (str) => { if (str) { return _.capitalize(_.camelCase(str)); } return str; }; // 页面模块的导入 const importPages = pages.map(page => `import ${capitalizeAndCamelCase(page)} from '../src/pages/${page}';`).join(os.EOL); // 页面模块的导出 let exportPages = pages.map(page => `${capitalizeAndCamelCase(page)},`).join(os.EOL); const sourceFile = path.join(__dirname, './templates/importExportPage.ejs'); const targetFile = path.join(rootDir, './.teamix/_pages.tsx'); applyMethod('addRenderFile', sourceFile, targetFile, { importPages, exportPages, }); }; if (userConfig.distributedRouter && command === 'start') { const watcher = chokidar.watch(path.join(rootDir, './src/pages/*'), { ignored: /(^|[\/\\])\../, // ignore dotfiles persistent: true, depth: 1, }); watcher.on('addDir', () => { generatePageFile(); }); watcher.on('unlinkDir', () => { generatePageFile(); }); } if (userConfig.distributedRouter && command === 'build') { generatePageFile(); } const { productName, pageName } = pkg; if (userConfig.openMixteam) { if (productName && pageName) { const allRoutes = []; const handleCode = (code, file) => { if (!code) { return; } let ast; try { ast = parser.parse(code, { sourceType: 'module', plugins: ['jsx', 'typescript'], }); } catch (_err) { console.log(file, _err); return; } if (ast) { traverse(ast, { ObjectExpression(p) { const { properties } = p.node; const target = properties.find(n => t.isStringLiteral(n.value) && n.key.name === 'path'); if (target && target.value && target.value.value) { allRoutes.push(target.value.value); } }, }); } }; // 获取 pages 下的 index 文件(分布式路由)和 routes 文件(集中式路由) const patterns = ['pages/*/index.{js,ts,jsx,tsx}', 'routes.{js,ts,jsx,tsx}']; const pathList = glob.sync(patterns, { cwd: path.join(rootDir, './src'), absolute: true, }); pathList.forEach(p => { const code = fs.readFileSync(p, 'utf-8'); handleCode(code, p); }); let url: string; onHook('after.start.devServer', async ({ devServer }) => { const { port = 3333 } = devServer.options ?? {}; // 跳转到第一个路由 const firstPath = allRoutes.find(route => { const pList = route?.split('/')?.filter(Boolean); return pList?.length === 1; }); url = `https://develop.mixteam.alibaba-inc.com/${productName}/playground${firstPath ?? ''}?port=${port}`; openBrowser(url); console.log(chalk.green(`打开页面链接:${url}`)); }); onHook('after.start.compile', (_serverUrl, isFirstCompile) => { // 多次提醒 if (!isFirstCompile && url) { console.log(chalk.green(`打开页面链接:${url}`)); } }) } else { console.log(chalk.red('**** 请在 package.json 中添加 productName(如 aso) 和 pageName(如 ecs)字段,对应页面地址 /${productName}/${pageName} ****')); } } };