UNPKG

saven

Version:
1,233 lines (1,192 loc) 75.5 kB
const fs = require('fs-extra') const os = require('os') const path = require('path') const chalk = require('chalk') const chokidar = require('chokidar') const wxTransformer = require('@tarojs/transformer-wx') const babel = require('babel-core') const traverse = require('babel-traverse').default const t = require('babel-types') const generate = require('babel-generator').default const template = require('babel-template') const autoprefixer = require('autoprefixer') const postcss = require('postcss') const pxtransform = require('postcss-pxtransform') const cssUrlParse = require('postcss-url') const minimatch = require('minimatch') const _ = require('lodash') const Util = require('./util') const CONFIG = require('./config') const npmProcess = require('./util/npm') const { resolveNpmFilesPath, resolveNpmPkgMainPath } = require('./util/resolve_npm_files') const babylonConfig = require('./config/babylon') const browserList = require('./config/browser_list') const defaultUglifyConfig = require('./config/uglify') const defaultBabelConfig = require('./config/babel') const astConvert = require('./util/ast_convert') const appPath = process.cwd() const configDir = path.join(appPath, Util.PROJECT_CONFIG) const projectConfig = require(configDir)(_.merge) const sourceDirName = projectConfig.sourceRoot || CONFIG.SOURCE_DIR const outputDirName = projectConfig.outputRoot || CONFIG.OUTPUT_DIR const sourceDir = path.join(appPath, sourceDirName) const outputDir = path.join(appPath, outputDirName) const entryFilePath = Util.resolveScriptPath(path.join(sourceDir, CONFIG.ENTRY)) const entryFileName = path.basename(entryFilePath) const outputEntryFilePath = path.join(outputDir, entryFileName) const pluginsConfig = projectConfig.plugins || {} const weappConf = projectConfig.weapp || {} const weappNpmConfig = Object.assign({ name: CONFIG.NPM_DIR, dir: null }, weappConf.npm) const appOutput = typeof weappConf.appOutput === 'boolean' ? weappConf.appOutput : true const notExistNpmList = [] const taroJsFramework = '@savenjs/saven' const taroWeappFramework = '@savenjs/saven-weapp' const taroJsComponents = '@savenjs/components' const taroJsRedux = '@savenjs/redux' let appConfig = {} const dependencyTree = {} const depComponents = {} const hasBeenBuiltComponents = [] const componentsBuildResult = {} const componentsNamedMap = {} const componentExportsMap = {} const wxssDepTree = {} let isBuildingScripts = {} let isBuildingStyles = {} let isCopyingFiles = {} let isProduction = false const NODE_MODULES = 'node_modules' const NODE_MODULES_REG = /(.*)node_modules/ const nodeModulesPath = path.join(appPath, NODE_MODULES) const PARSE_AST_TYPE = { ENTRY: 'ENTRY', PAGE: 'PAGE', COMPONENT: 'COMPONENT', NORMAL: 'NORMAL' } const DEVICE_RATIO = 'deviceRatio' const isWindows = os.platform() === 'win32' function getExactedNpmFilePath (npmName, filePath) { try { const npmInfo = resolveNpmFilesPath(npmName, isProduction, weappNpmConfig) const npmInfoMainPath = npmInfo.main let outputNpmPath if (Util.REG_STYLE.test(npmInfoMainPath)) { outputNpmPath = npmInfoMainPath } else { if (!weappNpmConfig.dir) { outputNpmPath = npmInfoMainPath.replace(NODE_MODULES, path.join(outputDirName, weappNpmConfig.name)) } else { const npmFilePath = npmInfoMainPath.replace(NODE_MODULES_REG, '') outputNpmPath = path.join(path.resolve(configDir, '..', weappNpmConfig.dir), weappNpmConfig.name, npmFilePath) } } const relativePath = path.relative(filePath, outputNpmPath) return Util.promoteRelativePath(relativePath) } catch (err) { if (notExistNpmList.indexOf(npmName) < 0) { notExistNpmList.push(npmName) } return npmName } } function processIfTaroEnv (astPath, node, a, b) { if (node[a].value !== Util.BUILD_TYPES.WEAPP) { const consequentSibling = astPath.getSibling('consequent') consequentSibling.set('body', []) } else { const alternateSibling = astPath.getSibling('alternate') if (alternateSibling.node) { alternateSibling.set('body', []) } } node[b] = t.stringLiteral(Util.BUILD_TYPES.WEAPP) } function parseAst (type, ast, depComponents, sourceFilePath, filePath, npmSkip = false) { const styleFiles = [] const scriptFiles = [] const jsonFiles = [] const mediaFiles = [] let configObj = {} let componentClassName = null let taroJsReduxConnect = null function traverseObjectNode (node, obj) { if (node.type === 'ClassProperty' || node.type === 'ObjectProperty') { const properties = node.value.properties obj = {} properties.forEach(p => { const key = t.isIdentifier(p.key) ? p.key.name : p.key.value obj[key] = traverseObjectNode(p.value) }) return obj } if (node.type === 'ObjectExpression') { const properties = node.properties obj = {} properties.forEach(p => { const key = t.isIdentifier(p.key) ? p.key.name : p.key.value obj[key] = traverseObjectNode(p.value) }) return obj } if (node.type === 'ArrayExpression') { return node.elements.map(item => traverseObjectNode(item)) } if (node.type === 'NullLiteral') { return null } return node.value } let taroImportDefaultName let needExportDefault = false let exportTaroReduxConnected = null const constantsReplaceList = Object.assign({}, Util.generateEnvList(projectConfig.env || {}), Util.generateConstantsList(projectConfig.defineConstants || {})) ast = babel.transformFromAst(ast, '', { plugins: [ [require('babel-plugin-danger-remove-unused-import'), { ignore: ['@tarojs/taro', 'react', 'nervjs'] }], [require('babel-plugin-transform-define').default, constantsReplaceList] ] }).ast traverse(ast, { ClassDeclaration (astPath) { const node = astPath.node let hasCreateData = false if (node.superClass) { astPath.traverse({ ClassMethod (astPath) { if (astPath.get('key').isIdentifier({ name: '_createData' })) { hasCreateData = true } } }) if (hasCreateData) { needExportDefault = true astPath.traverse({ ClassMethod (astPath) { const node = astPath.node if (node.kind === 'constructor') { astPath.traverse({ ExpressionStatement (astPath) { const node = astPath.node if (node.expression && node.expression.type === 'AssignmentExpression' && node.expression.operator === '=') { const left = node.expression.left if (left.type === 'MemberExpression' && left.object.type === 'ThisExpression' && left.property.type === 'Identifier' && left.property.name === 'config') { configObj = traverseObjectNode(node.expression.right) if (type === PARSE_AST_TYPE.ENTRY) { appConfig = configObj } astPath.remove() } } } }) } } }) if (node.id === null) { componentClassName = '_TaroComponentClass' astPath.replaceWith(t.classDeclaration(t.identifier(componentClassName), node.superClass, node.body, node.decorators || [])) } else if (node.id.name === 'App') { componentClassName = '_App' astPath.replaceWith(t.classDeclaration(t.identifier(componentClassName), node.superClass, node.body, node.decorators || [])) } else { componentClassName = node.id.name } } } }, ClassExpression (astPath) { const node = astPath.node if (node.superClass) { let hasCreateData = false astPath.traverse({ ClassMethod (astPath) { if (astPath.get('key').isIdentifier({ name: '_createData' })) { hasCreateData = true } } }) if (hasCreateData) { needExportDefault = true if (node.id === null) { componentClassName = '_TaroComponentClass' astPath.replaceWith(t.ClassExpression(t.identifier(componentClassName), node.superClass, node.body, node.decorators || [])) } else if (node.id.name === 'App') { componentClassName = '_App' astPath.replaceWith(t.ClassExpression(t.identifier(componentClassName), node.superClass, node.body, node.decorators || [])) } else { componentClassName = node.id.name } } } }, ClassProperty (astPath) { const node = astPath.node if (node.key.name === 'config') { configObj = traverseObjectNode(node) if (type === PARSE_AST_TYPE.ENTRY) { appConfig = configObj } astPath.remove() } }, IfStatement (astPath) { astPath.traverse({ BinaryExpression (astPath) { const node = astPath.node const left = node.left const right = node.right if (generate(left).code === 'process.env.TARO_ENV') { processIfTaroEnv(astPath, node, 'right', 'left') } else if (generate(right).code === 'process.env.TARO_ENV') { processIfTaroEnv(astPath, node, 'left', 'right') } } }) }, ImportDeclaration (astPath) { const node = astPath.node const source = node.source let value = source.value if (Util.isNpmPkg(value) && notExistNpmList.indexOf(value) < 0) { if (value === taroJsComponents) { astPath.remove() } else { let isDepComponent = false if (depComponents && depComponents.length) { depComponents.forEach(item => { if (item.path === value) { isDepComponent = true } }) } if (isDepComponent) { astPath.remove() } else { const specifiers = node.specifiers if (value === taroJsFramework) { let defaultSpecifier = null specifiers.forEach(item => { if (item.type === 'ImportDefaultSpecifier') { defaultSpecifier = item.local.name } }) if (defaultSpecifier) { taroImportDefaultName = defaultSpecifier } value = taroWeappFramework } else if (value === taroJsRedux) { specifiers.forEach(item => { if (item.type === 'ImportSpecifier') { const local = item.local if (local.type === 'Identifier' && local.name === 'connect') { taroJsReduxConnect = item.imported.name } } }) } if (!npmSkip) { source.value = getExactedNpmFilePath(value, filePath) } else { source.value = value } } } } else if (path.isAbsolute(value)) { Util.printLog(Util.pocessTypeEnum.ERROR, '引用文件', `文件 ${sourceFilePath} 中引用 ${value} 是绝对路径!`) } }, VariableDeclaration (astPath) { const node = astPath.node if (node.declarations.length === 1 && node.declarations[0].init && node.declarations[0].init.type === 'CallExpression' && node.declarations[0].init.callee && node.declarations[0].init.callee.name === 'require') { const init = node.declarations[0].init const args = init.arguments let value = args[0].value const id = node.declarations[0].id if (Util.isNpmPkg(value) && notExistNpmList.indexOf(value) < 0) { if (value === taroJsComponents) { astPath.remove() } else { let isDepComponent = false if (depComponents && depComponents.length) { depComponents.forEach(item => { if (item.path === value) { isDepComponent = true } }) } if (isDepComponent) { astPath.remove() } else { if (value === taroJsFramework && id.type === 'Identifier') { taroImportDefaultName = id.name value = taroWeappFramework } else if (value === taroJsRedux) { const declarations = node.declarations declarations.forEach(item => { const id = item.id if (id.type === 'ObjectPattern') { const properties = id.properties properties.forEach(p => { if (p.type === 'ObjectProperty') { if (p.value.type === 'Identifier' && p.value.name === 'connect') { taroJsReduxConnect = p.key.name } } }) } }) } if (!npmSkip) { args[0].value = getExactedNpmFilePath(value, filePath) } else { args[0].value = value } astPath.replaceWith(t.variableDeclaration(node.kind, [t.variableDeclarator(id, init)])) } } } } }, CallExpression (astPath) { const node = astPath.node const callee = node.callee if (t.isMemberExpression(callee)) { if (taroImportDefaultName && callee.object.name === taroImportDefaultName && callee.property.name === 'render') { astPath.remove() } } else if (callee.name === 'require') { const args = node.arguments let value = args[0].value if (Util.isNpmPkg(value) && notExistNpmList.indexOf(value) < 0) { if (Util.REG_STYLE.test(value)) { if (!npmSkip) { args[0].value = getExactedNpmFilePath(value, filePath) } else { args[0].value = value } } } if (path.isAbsolute(value)) { Util.printLog(Util.pocessTypeEnum.ERROR, '引用文件', `文件 ${sourceFilePath} 中引用 ${value} 是绝对路径!`) } } }, ExportDefaultDeclaration (astPath) { const node = astPath.node const declaration = node.declaration needExportDefault = false if ( declaration && (declaration.type === 'ClassDeclaration' || declaration.type === 'ClassExpression') ) { const superClass = declaration.superClass if (superClass) { let hasCreateData = false astPath.traverse({ ClassMethod (astPath) { if (astPath.get('key').isIdentifier({ name: '_createData' })) { hasCreateData = true } } }) if (hasCreateData) { needExportDefault = true if (declaration.id === null) { componentClassName = '_TaroComponentClass' } else if (declaration.id.name === 'App') { componentClassName = '_App' } else { componentClassName = declaration.id.name } const isClassDcl = declaration.type === 'ClassDeclaration' const classDclProps = [t.identifier(componentClassName), superClass, declaration.body, declaration.decorators || []] astPath.replaceWith(isClassDcl ? t.classDeclaration.apply(null, classDclProps) : t.classExpression.apply(null, classDclProps)) } } } else if (declaration.type === 'CallExpression') { const callee = declaration.callee if (callee && callee.type === 'CallExpression') { const subCallee = callee.callee if (subCallee.type === 'Identifier' && subCallee.name === taroJsReduxConnect) { const args = declaration.arguments if (args.length === 1 && args[0].name === componentClassName) { needExportDefault = true exportTaroReduxConnected = `${componentClassName}__Connected` astPath.replaceWith(t.variableDeclaration('const', [t.variableDeclarator(t.identifier(`${componentClassName}__Connected`), t.CallExpression(declaration.callee, declaration.arguments))])) } } } } }, Program: { exit (astPath) { astPath.traverse({ ImportDeclaration (astPath) { const node = astPath.node const source = node.source let value = source.value const valueExtname = path.extname(value) if (value.indexOf('.') === 0) { let importPath = path.resolve(path.dirname(sourceFilePath), value) importPath = Util.resolveScriptPath(importPath) if (isFileToBePage(importPath)) { astPath.remove() } else { let isDepComponent = false if (depComponents && depComponents.length) { depComponents.forEach(item => { const resolvePath = Util.resolveScriptPath(path.resolve(path.dirname(sourceFilePath), item.path)) const resolveValuePath = Util.resolveScriptPath(path.resolve(path.dirname(sourceFilePath), value)) if (resolvePath === resolveValuePath) { isDepComponent = true } }) } if (isDepComponent) { astPath.remove() } else if (Util.REG_SCRIPT.test(valueExtname) || Util.REG_TYPESCRIPT.test(valueExtname)) { const vpath = path.resolve(sourceFilePath, '..', value) let fPath = value if (fs.existsSync(vpath) && !NODE_MODULES_REG.test(vpath)) { fPath = vpath } if (scriptFiles.indexOf(fPath) < 0) { scriptFiles.push(fPath) } } else if (Util.REG_JSON.test(valueExtname)) { const vpath = path.resolve(sourceFilePath, '..', value) if (jsonFiles.indexOf(vpath) < 0) { jsonFiles.push(vpath) } if (fs.existsSync(vpath)) { const obj = JSON.parse(fs.readFileSync(vpath).toString()) const specifiers = node.specifiers let defaultSpecifier = null specifiers.forEach(item => { if (item.type === 'ImportDefaultSpecifier') { defaultSpecifier = item.local.name } }) if (defaultSpecifier) { let objArr = [t.nullLiteral()] if (Array.isArray(obj)) { objArr = t.arrayExpression(astConvert.array(obj)) } else { objArr = t.objectExpression(astConvert.obj(obj)) } astPath.replaceWith(t.variableDeclaration('const', [t.variableDeclarator(t.identifier(defaultSpecifier), objArr)])) } } } else if (Util.REG_FONT.test(valueExtname) || Util.REG_IMAGE.test(valueExtname) || Util.REG_MEDIA.test(valueExtname)) { const vpath = path.resolve(sourceFilePath, '..', value) if (!fs.existsSync(vpath)) { Util.printLog(Util.pocessTypeEnum.ERROR, '引用文件', `文件 ${sourceFilePath} 中引用 ${value} 不存在!`) return } if (mediaFiles.indexOf(vpath) < 0) { mediaFiles.push(vpath) } const specifiers = node.specifiers let defaultSpecifier = null specifiers.forEach(item => { if (item.type === 'ImportDefaultSpecifier') { defaultSpecifier = item.local.name } }) let sourceDirPath = sourceDir if (NODE_MODULES_REG.test(vpath)) { sourceDirPath = nodeModulesPath } if (defaultSpecifier) { astPath.replaceWith(t.variableDeclaration('const', [t.variableDeclarator(t.identifier(defaultSpecifier), t.stringLiteral(vpath.replace(sourceDirPath, '').replace(/\\/g, '/')))])) } else { astPath.remove() } } else if (Util.REG_STYLE.test(valueExtname)) { const stylePath = path.resolve(path.dirname(sourceFilePath), value) if (styleFiles.indexOf(stylePath) < 0) { styleFiles.push(stylePath) } astPath.remove() } else { let vpath = Util.resolveScriptPath(path.resolve(sourceFilePath, '..', value)) let outputVpath if (NODE_MODULES_REG.test(vpath)) { outputVpath = vpath.replace(nodeModulesPath, path.join(outputDir, weappNpmConfig.name)) } else { outputVpath = vpath.replace(sourceDir, outputDir) } let relativePath = path.relative(filePath, outputVpath) if (vpath && vpath !== sourceFilePath) { if (!fs.existsSync(vpath)) { Util.printLog(Util.pocessTypeEnum.ERROR, '引用文件', `文件 ${sourceFilePath} 中引用 ${value} 不存在!`) } else { if (fs.lstatSync(vpath).isDirectory()) { if (fs.existsSync(path.join(vpath, 'index.js'))) { vpath = path.join(vpath, 'index.js') relativePath = path.join(relativePath, 'index.js') } else { Util.printLog(Util.pocessTypeEnum.ERROR, '引用目录', `文件 ${sourceFilePath} 中引用了目录 ${value}!`) return } } if (scriptFiles.indexOf(vpath) < 0) { scriptFiles.push(vpath) } relativePath = Util.promoteRelativePath(relativePath) relativePath = relativePath.replace(path.extname(relativePath), '.js') source.value = relativePath astPath.replaceWith(t.importDeclaration(node.specifiers, node.source)) } } } } } }, CallExpression (astPath) { const node = astPath.node const callee = node.callee if (callee.name === 'require') { const args = node.arguments let value = args[0].value const valueExtname = path.extname(value) if (value.indexOf('.') === 0) { let importPath = path.resolve(path.dirname(sourceFilePath), value) importPath = Util.resolveScriptPath(importPath) if (isFileToBePage(importPath)) { if (astPath.parent.type === 'AssignmentExpression' || 'ExpressionStatement') { astPath.parentPath.remove() } else if (astPath.parent.type === 'VariableDeclarator') { astPath.parentPath.parentPath.remove() } else { astPath.remove() } } else { let isDepComponent = false if (depComponents && depComponents.length) { depComponents.forEach(item => { const resolvePath = Util.resolveScriptPath(path.resolve(path.dirname(sourceFilePath), item.path)) const resolveValuePath = Util.resolveScriptPath(path.resolve(path.dirname(sourceFilePath), value)) if (resolvePath === resolveValuePath) { isDepComponent = true } }) } if (isDepComponent) { if (astPath.parent.type === 'AssignmentExpression' || 'ExpressionStatement') { astPath.parentPath.remove() } else if (astPath.parent.type === 'VariableDeclarator') { astPath.parentPath.parentPath.remove() } else { astPath.remove() } } else if (Util.REG_STYLE.test(valueExtname)) { const stylePath = path.resolve(path.dirname(sourceFilePath), value) if (styleFiles.indexOf(stylePath) < 0) { styleFiles.push(stylePath) } if (astPath.parent.type === 'AssignmentExpression' || 'ExpressionStatement') { astPath.parentPath.remove() } else if (astPath.parent.type === 'VariableDeclarator') { astPath.parentPath.parentPath.remove() } else { astPath.remove() } } else if (Util.REG_JSON.test(valueExtname)) { const vpath = path.resolve(sourceFilePath, '..', value) if (jsonFiles.indexOf(vpath) < 0) { jsonFiles.push(vpath) } if (fs.existsSync(vpath)) { const obj = JSON.parse(fs.readFileSync(vpath).toString()) let objArr = [t.nullLiteral()] if (Array.isArray(obj)) { objArr = t.arrayExpression(astConvert.array(obj)) } else { objArr = t.objectExpression(astConvert.obj(obj)) } astPath.replaceWith(t.objectExpression(objArr)) } } else if (Util.REG_SCRIPT.test(valueExtname) || Util.REG_TYPESCRIPT.test(valueExtname)) { const vpath = path.resolve(sourceFilePath, '..', value) let fPath = value if (fs.existsSync(vpath) && !NODE_MODULES_REG.test(vpath)) { fPath = vpath } if (scriptFiles.indexOf(fPath) < 0) { scriptFiles.push(fPath) } } else if (Util.REG_FONT.test(valueExtname) || Util.REG_IMAGE.test(valueExtname) || Util.REG_MEDIA.test(valueExtname)) { const vpath = path.resolve(sourceFilePath, '..', value) if (mediaFiles.indexOf(vpath) < 0) { mediaFiles.push(vpath) } let sourceDirPath = sourceDir if (NODE_MODULES_REG.test(vpath)) { sourceDirPath = nodeModulesPath } astPath.replaceWith(t.stringLiteral(vpath.replace(sourceDirPath, '').replace(/\\/g, '/'))) } else { let vpath = Util.resolveScriptPath(path.resolve(sourceFilePath, '..', value)) let outputVpath if (NODE_MODULES_REG.test(vpath)) { outputVpath = vpath.replace(nodeModulesPath, path.join(outputDir, weappNpmConfig.name)) } else { outputVpath = vpath.replace(sourceDir, outputDir) } let relativePath = path.relative(filePath, outputVpath) if (vpath) { if (!fs.existsSync(vpath)) { Util.printLog(Util.pocessTypeEnum.ERROR, '引用文件', `文件 ${sourceFilePath} 中引用 ${value} 不存在!`) } else { if (fs.lstatSync(vpath).isDirectory()) { if (fs.existsSync(path.join(vpath, 'index.js'))) { vpath = path.join(vpath, 'index.js') relativePath = path.join(relativePath, 'index.js') } else { Util.printLog(Util.pocessTypeEnum.ERROR, '引用目录', `文件 ${sourceFilePath} 中引用了目录 ${value}!`) return } } if (scriptFiles.indexOf(vpath) < 0) { scriptFiles.push(vpath) } relativePath = Util.promoteRelativePath(relativePath) relativePath = relativePath.replace(path.extname(relativePath), '.js') args[0].value = relativePath } } } } } } } }) const node = astPath.node const exportVariableName = exportTaroReduxConnected || componentClassName if (needExportDefault) { const exportDefault = template(`export default ${exportVariableName}`, babylonConfig)() node.body.push(exportDefault) } const taroWeappFrameworkPath = !npmSkip ? getExactedNpmFilePath(taroWeappFramework, filePath) : taroWeappFramework switch (type) { case PARSE_AST_TYPE.ENTRY: const pxTransformConfig = { designWidth: projectConfig.designWidth || 750, } if (projectConfig.hasOwnProperty(DEVICE_RATIO)) { pxTransformConfig[DEVICE_RATIO] = projectConfig.deviceRatio } node.body.push(template(`App(require('${taroWeappFrameworkPath}').default.createApp(${exportVariableName}))`, babylonConfig)()) node.body.push(template(`Taro.initPxTransform(${JSON.stringify(pxTransformConfig)})`, babylonConfig)()) break case PARSE_AST_TYPE.PAGE: node.body.push(template(`Component(require('${taroWeappFrameworkPath}').default.createComponent(${exportVariableName}, true))`, babylonConfig)()) break case PARSE_AST_TYPE.COMPONENT: node.body.push(template(`Component(require('${taroWeappFrameworkPath}').default.createComponent(${exportVariableName}))`, babylonConfig)()) break default: break } } } }) return { code: unescape(generate(ast).code.replace(/\\u/g, '%u')), styleFiles, scriptFiles, jsonFiles, configObj, mediaFiles, componentClassName } } function parseComponentExportAst (ast, componentName, componentPath, componentType) { let componentRealPath = null let importExportName traverse(ast, { ExportNamedDeclaration (astPath) { const node = astPath.node const specifiers = node.specifiers const source = node.source if (source && source.type === 'StringLiteral') { specifiers.forEach(specifier => { const exported = specifier.exported if (_.kebabCase(exported.name) === componentName) { componentRealPath = Util.resolveScriptPath(path.resolve(path.dirname(componentPath), source.value)) } }) } else { specifiers.forEach(specifier => { const exported = specifier.exported if (_.kebabCase(exported.name) === componentName) { importExportName = exported.name } }) } }, ExportDefaultDeclaration (astPath) { const node = astPath.node const declaration = node.declaration if (componentType === 'default') { importExportName = declaration.name } }, IfStatement (astPath) { astPath.traverse({ BinaryExpression (astPath) { const node = astPath.node const left = node.left if (generate(left).code === 'process.env.TARO_ENV' && node.right.value === Util.BUILD_TYPES.WEAPP) { const consequentSibling = astPath.getSibling('consequent') consequentSibling.traverse({ CallExpression (astPath) { if (astPath.get('callee').isIdentifier({ name : 'require'})) { const arg = astPath.get('arguments')[0] if (t.isStringLiteral(arg.node)) { componentRealPath = Util.resolveScriptPath(path.resolve(path.dirname(componentPath), arg.node.value)) } } } }) } } }) }, CallExpression (astPath) { if (astPath.get('callee').isIdentifier({ name : 'require'})) { const arg = astPath.get('arguments')[0] if (t.isStringLiteral(arg.node)) { componentRealPath = Util.resolveScriptPath(path.resolve(path.dirname(componentPath), arg.node.value)) } } }, Program: { exit (astPath) { astPath.traverse({ ImportDeclaration (astPath) { const node = astPath.node const specifiers = node.specifiers const source = node.source if (importExportName) { specifiers.forEach(specifier => { const local = specifier.local if (local.name === importExportName) { componentRealPath = Util.resolveScriptPath(path.resolve(path.dirname(componentPath), source.value)) } }) } } }) } } }) return componentRealPath } function isFileToBeTaroComponent (code, sourcePath, outputPath) { const transformResult = wxTransformer({ code, sourcePath: sourcePath, outputPath: outputPath, isNormal: true, isTyped: Util.REG_TYPESCRIPT.test(sourcePath) }) const { ast } = transformResult let isTaroComponent = false traverse(ast, { ClassDeclaration (astPath) { astPath.traverse({ ClassMethod (astPath) { if (astPath.get('key').isIdentifier({ name: 'render' })) { astPath.traverse({ JSXElement () { isTaroComponent = true } }) } } }) }, ClassExpression (astPath) { astPath.traverse({ ClassMethod (astPath) { if (astPath.get('key').isIdentifier({ name: 'render' })) { astPath.traverse({ JSXElement () { isTaroComponent = true } }) } } }) } }) return { isTaroComponent, transformResult } } function isFileToBePage (filePath) { let isPage = false const extname = path.extname(filePath) const pages = appConfig.pages || [] const filePathWithoutExt = filePath.replace(extname, '') pages.forEach(page => { if (filePathWithoutExt === path.join(sourceDir, page)) { isPage = true } }) return isPage && Util.REG_SCRIPTS.test(extname) } function copyFilesFromSrcToOutput (files) { files.forEach(file => { let outputFilePath if (NODE_MODULES_REG.test(file)) { outputFilePath = file.replace(nodeModulesPath, path.join(outputDir, weappNpmConfig.name)) } else { outputFilePath = file.replace(sourceDir, outputDir) } if (isCopyingFiles[outputFilePath]) { return } isCopyingFiles[outputFilePath] = true let modifySrc = file.replace(appPath + path.sep, '') modifySrc = modifySrc.split(path.sep).join('/') let modifyOutput = outputFilePath.replace(appPath + path.sep, '') modifyOutput = modifyOutput.split(path.sep).join('/') Util.printLog(Util.pocessTypeEnum.COPY, '文件', modifyOutput) if (!fs.existsSync(file)) { Util.printLog(Util.pocessTypeEnum.ERROR, '文件', `${modifySrc} 不存在`) } else { fs.ensureDir(path.dirname(outputFilePath)) fs.copySync(file, outputFilePath) } }) } const babelConfig = _.mergeWith(defaultBabelConfig, pluginsConfig.babel, (objValue, srcValue) => { if (Array.isArray(objValue)) { return Array.from(new Set(objValue.concat(srcValue))) } }) async function compileScriptFile (content) { const compileScriptRes = await npmProcess.callPlugin('babel', content, entryFilePath, babelConfig) return compileScriptRes.code } function buildProjectConfig () { const projectConfigPath = path.join(appPath, 'project.config.json') if (!fs.existsSync(projectConfigPath)) { return } const origProjectConfig = fs.readJSONSync(projectConfigPath) fs.ensureDirSync(outputDir) fs.writeFileSync( path.join(outputDir, 'project.config.json'), JSON.stringify(Object.assign({}, origProjectConfig, { miniprogramRoot: './' }), null, 2) ) Util.printLog(Util.pocessTypeEnum.GENERATE, '工具配置', `${outputDirName}/project.config.json`) } async function buildEntry () { Util.printLog(Util.pocessTypeEnum.COMPILE, '入口文件', `${sourceDirName}/${entryFileName}`) const entryFileCode = fs.readFileSync(entryFilePath).toString() try { const transformResult = wxTransformer({ code: entryFileCode, sourcePath: entryFilePath, outputPath: outputEntryFilePath, isApp: true, isTyped: Util.REG_TYPESCRIPT.test(entryFilePath) }) // app.js的template忽略 const res = parseAst(PARSE_AST_TYPE.ENTRY, transformResult.ast, [], entryFilePath, outputEntryFilePath) let resCode = res.code resCode = await compileScriptFile(resCode) if (isProduction) { const uglifyPluginConfig = pluginsConfig.uglify || { enable: true } if (uglifyPluginConfig.enable) { const uglifyConfig = Object.assign(defaultUglifyConfig, uglifyPluginConfig.config || {}) const uglifyResult = npmProcess.callPluginSync('uglifyjs', resCode, entryFilePath, uglifyConfig) if (uglifyResult.error) { console.log(uglifyResult.error) } else { resCode = uglifyResult.code } } } if (appOutput) { fs.writeFileSync(path.join(outputDir, 'app.json'), JSON.stringify(res.configObj, null, 2)) Util.printLog(Util.pocessTypeEnum.GENERATE, '入口配置', `${outputDirName}/app.json`) fs.writeFileSync(path.join(outputDir, 'app.js'), resCode) Util.printLog(Util.pocessTypeEnum.GENERATE, '入口文件', `${outputDirName}/app.js`) } const fileDep = dependencyTree[entryFilePath] || {} // 编译依赖的脚本文件 if (Util.isDifferentArray(fileDep['script'], res.scriptFiles)) { compileDepScripts(res.scriptFiles) } // 编译样式文件 if (Util.isDifferentArray(fileDep['style'], res.styleFiles) && appOutput) { await compileDepStyles(path.join(outputDir, 'app.wxss'), res.styleFiles, false) Util.printLog(Util.pocessTypeEnum.GENERATE, '入口样式', `${outputDirName}/app.wxss`) } // 拷贝依赖文件 if (Util.isDifferentArray(fileDep['json'], res.jsonFiles)) { copyFilesFromSrcToOutput(res.jsonFiles) } // 处理res.configObj 中的tabBar配置 const tabBar = res.configObj.tabBar if (tabBar && typeof tabBar === 'object' && !Util.isEmptyObject(tabBar)) { const list = tabBar.list || [] let tabBarIcons = [] list.forEach(item => { if (item.iconPath) { tabBarIcons.push(item.iconPath) } if (item.selectedIconPath) { tabBarIcons.push(item.selectedIconPath) } }) tabBarIcons = tabBarIcons.map(item => path.resolve(sourceDir, item)) if (tabBarIcons && tabBarIcons.length) { res.mediaFiles = res.mediaFiles.concat(tabBarIcons) } } if (Util.isDifferentArray(fileDep['media'], res.mediaFiles)) { copyFilesFromSrcToOutput(res.mediaFiles) } fileDep['style'] = res.styleFiles fileDep['script'] = res.scriptFiles fileDep['json'] = res.jsonFiles fileDep['media'] = res.mediaFiles dependencyTree[entryFilePath] = fileDep return res.configObj } catch (err) { console.log(err) } } async function buildPages () { Util.printLog(Util.pocessTypeEnum.COMPILE, '所有页面') // 支持分包,解析子包页面 const pages = appConfig.pages || [] const subPackages = appConfig.subPackages if (subPackages && subPackages.length) { subPackages.forEach(item => { if (item.pages && item.pages.length) { const root = item.root item.pages.forEach(page => { let pagePath = `${root}/${page}` pagePath = pagePath.replace(/\/{2,}/g, '/') if (pages.indexOf(pagePath) < 0) { pages.push(pagePath) } }) } }) } const pagesPromises = pages.map(async page => { return buildSinglePage(page) }) await Promise.all(pagesPromises) } function transfromNativeComponents (configFile, componentConfig) { const usingComponents = componentConfig.usingComponents if (usingComponents && !Util.isEmptyObject(usingComponents)) { Object.keys(usingComponents).map(async item => { const componentPath = usingComponents[item] if (/^plugin\:\/\//.test(componentPath)) { // 小程序 plugin Util.printLog(Util.pocessTypeEnum.REFERENCE, '插件引用', `使用了插件 ${chalk.bold(componentPath)}`) return } const componentJSPath = Util.resolveScriptPath(path.resolve(path.dirname(configFile), componentPath)) const componentJSONPath = componentJSPath.replace(path.extname(componentJSPath), '.json') const componentWXMLPath = componentJSPath.replace(path.extname(componentJSPath), '.wxml') const componentWXSSPath = componentJSPath.replace(path.extname(componentJSPath), '.wxss') const outputComponentJSPath = componentJSPath.replace(sourceDir, outputDir).replace(path.extname(componentJSPath), '.js') if (fs.existsSync(componentJSPath)) { const componentJSContent = fs.readFileSync(componentJSPath).toString() if (componentJSContent.indexOf(taroJsFramework) >= 0 && !fs.existsSync(componentWXMLPath)) { return await buildDepComponents([componentJSPath]) } compileDepScripts([componentJSPath]) } else { return Util.printLog(Util.pocessTypeEnum.ERROR, '编译错误', `原生组件文件 ${componentJSPath} 不存在!`) } if (fs.existsSync(componentWXMLPath)) { const outputComponentWXMLPath = outputComponentJSPath.replace(path.extname(outputComponentJSPath), '.wxml') copyFileSync(componentWXMLPath, outputComponentWXMLPath) } if (fs.existsSync(componentWXSSPath)) { const outputComponentWXSSPath = outputComponentJSPath.replace(path.extname(outputComponentJSPath), '.wxss') await compileDepStyles(outputComponentWXSSPath, [componentWXSSPath], true) } if (fs.existsSync(componentJSONPath)) { const componentJSON = require(componentJSONPath) const outputComponentJSONPath = outputComponentJSPath.replace(path.extname(outputComponentJSPath), '.json') copyFileSync(componentJSONPath, outputComponentJSONPath) transfromNativeComponents(componentJSONPath, componentJSON) } }) } } // 小程序页面编译 async function buildSinglePage (page) { Util.printLog(Util.pocessTypeEnum.COMPILE, '页面文件', `${sourceDirName}/${page}`) const pagePath = path.join(sourceDir, `${page}`) let pageJs = Util.resolveScriptPath(pagePath) if (!fs.existsSync(pageJs)) { Util.printLog(Util.pocessTypeEnum.ERROR, '页面文件', `${sourceDirName}/${page} 不存在!`) return } const pageJsContent = fs.readFileSync(pageJs).toString() const outputPageJSPath = pageJs.replace(sourceDir, outputDir).replace(path.extname(pageJs), '.js') const outputPagePath = path.dirname(outputPageJSPath) const outputPageJSONPath = outputPageJSPath.replace(path.extname(outputPageJSPath), '.json') const outputPageWXMLPath = outputPageJSPath.replace(path.extname(outputPageJSPath), '.wxml') const outputPageWXSSPath = outputPageJSPath.replace(path.extname(outputPageJSPath), '.wxss') // 判断是不是小程序原生代码页面 const pageWXMLPath = pageJs.replace(path.extname(pageJs), '.wxml') if (fs.existsSync(pageWXMLPath) && pageJsContent.indexOf(taroJsFramework) < 0) { const pageJSONPath = pageJs.replace(path.extname(pageJs), '.json') const pageWXSSPath = pageJs.replace(path.extname(pageJs), '.wxss') if (fs.existsSync(pageJSONPath)) { const pageJSON = require(pageJSONPath) copyFileSync(pageJSONPath, outputPageJSONPath) transfromNativeComponents(pageJSONPath, pageJSON) } compileDepScripts([pageJs]) copyFileSync(pageWXMLPath, outputPageWXMLPath) if (fs.existsSync(pageWXSSPath)) { await compileDepStyles(outputPageWXSSPath, [pageWXSSPath], false) } return } try { const transformResult = wxTransformer({ code: pageJsContent, sourcePath: pageJs, outputPath: outputPageJSPath, isRoot: true, isTyped: Util.REG_TYPESCRIPT.test(pageJs) }) const pageDepComponents = transformResult.components const res = parseAst(PARSE_AST_TYPE.PAGE, transformResult.ast, pageDepComponents, pageJs, outputPageJSPath) let resCode = res.code resCode = await compileScriptFile(resCode) if (isProduction) { const uglifyPluginConfig = pluginsConfig.uglify || { enable: true } if (uglifyPluginConfig.enable) { const uglifyConfig = Object.assign(defaultUglifyConfig, uglifyPluginConfig.config || {}) const uglifyResult = npmProcess.callPluginSync('uglifyjs', resCode, outputPageJSPath, uglifyConfig) if (uglifyResult.error) { console.log(uglifyResult.error) } else { resCode = uglifyResult.code } } } fs.ensureDirSync(outputPagePath) const { usingComponents = {} } = res.configObj if (usingComponents && !Util.isEmptyObject(usingComponents)) { const keys = Object.keys(usingComponents) keys.forEach(item => { pageDepComponents.forEach(component => { if (_.camelCase(item) === _.camelCase(component.name)) { delete usingComponents[item] } }) }) transfromNativeComponents(outputPageJSONPath.replace(outputDir, sourceDir), res.configObj) } const fileDep = dependencyTree[pageJs] || {} // 编译依赖的组件文件 let buildDepComponentsResult = [] let realComponentsPathList = [] if (pageDepComponents.length) { realComponentsPathList = getRealComponentsPathList(pageJs, pageDepComponents) res.scriptFiles = res.scriptFiles.map(item => { for (let i = 0; i < realComponentsPathList.length; i++) { const componentObj = realComponentsPathList[i] const componentPath = componentObj.path if (item === componentPath) { return null } } return item }).filter(item => item) buildDepComponentsResult = await buildDepComponents(realComponentsPathList) } if (!Util.isEmptyObject(componentExportsMap) && realComponentsPathList.length) { const mapKeys = Object.keys(componentExportsMap) realComponentsPathList.forEach(component => { if (mapKeys.indexOf(component.path) >= 0) { const componentMap = componentExportsMap[component.path] componentMap.forEach(component => { pageDepComponents.forEach(depComponent => { if (depComponent.name === component.name) { let componentPath = component.path if (NODE_MODULES_REG.test(componentPath)) { componentPath = componentPath.replace(NODE_MODULES, weappNpmConfig.name) } const realPath = Util.promoteRelativePath(path.relative(pageJs, componentPath)) depComponent.path = realPath.replace(path.extname(realPath), '') } }) }) } }) } fs.writeFileSync(outputPageJSONPath, JSON.stringify(_.merge({}, buildUsingComponents(pageDepComponents), res.configObj), null, 2)) Util.printLog(Util.pocessTypeEnum.GENERATE, '页面JSON', `${outputDirName}/${page}.json`) fs.writeFileSync(outputPageJSPath, resCode) Util.printLog(Util.pocessTypeEnum.GENERATE, '页面JS', `${outputDirName}/${page}.js`) fs.writeFileSync(outputPageWXMLPath, transformResult.template) Util.printLog(Util.pocessTypeEnum.GENERATE, '页面WXML', `${outputDirName}/${page}.wxml`) // 编译依赖的脚本文件 if (Util.isDifferentArray(fileDep['script'], res.scriptFiles)) { compileDepScripts(res.scriptFiles) } // 编译样式文件 if (Util.isDifferentArray(fileDep['style'], res.styleFiles) || Util.isDifferentArray(depComponents[pageJs], pageDepComponents)) { Util.printLog(Util.pocessTypeEnum.GENERATE, '页面WXSS', `${outputDirName}/${page}.wxss`) const depStyleList = getDepStyleList(outputPageWXSSPath, buildDepComponentsResult) wxssDepTree[outputPageWXSSPath] = depStyleList await compileDepStyles(outputPageWXSSPath, res.styleFiles, false) } // 拷贝依赖文件 if (Util.isDifferentArray(fileDep['json'], res.jsonFiles)) { copyFilesFromSrcToOutput(res.jsonFiles) } if (Util.isDifferentArray(fileDep['media'], res.mediaFiles)) { copyFilesFromSrcToOutput(res.mediaFiles) } depComponents[pageJs] = pageDepComponents fileDep['style'] = res.styleFiles fileDep['script'] = res.scriptFiles fileDep['json'] = res.jsonFiles fileDep['media'] = res.mediaFiles dependencyTree[pageJs] = fileDep } catch (err) { Util.printLog(Util.pocessTypeEnum.ERROR, '页面编译', `页面${pagePath}编译失败!`) console.log(err) } } async function processStyleWithPostCSS (styleObj) { const useModuleConf = weappConf.module || {} const customPostcssConf = useModuleConf.postcss || {} const customPxtransformConf = Object.assign({ enable: true, config: {} }, customPostcss