saven
Version:
1,233 lines (1,192 loc) • 75.5 kB
JavaScript
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