addImport(source: string, imported: string, name?: string = imported): Object { let alias = `${source}:${imported}`; let id = this.dynamicImportIds[alias]; if (!id) { source = this.resolveModuleSource(source); id = this.dynamicImportIds[alias] = this.scope.generateUidIdentifier(name); let specifiers = []; if (imported === "*") { specifiers.push(t.importNamespaceSpecifier(id)); } else if (imported === "default") { specifiers.push(t.importDefaultSpecifier(id)); } else { specifiers.push(t.importSpecifier(id, t.identifier(imported))); } let declar = t.importDeclaration(specifiers, t.stringLiteral(source)); declar._blockHoist = 3; this.path.unshiftContainer("body", declar); } return id; }
const imports = components.map((component, i) => { return t.importDeclaration( [t.importSpecifier( t.identifier(`${component.export.name}_${i}`), t.identifier(component.export.name) )], t.stringLiteral(component.fullPath) ); });
(node, importWrapper = defaultWrapperName, importFrom = 'cycle-hmr') => { const importLiteral = importWrapper const importLocalLiteral = getWrapperName(importWrapper) let importIdentifier = t.identifier(importLiteral) let importLocalIdentifier = t.identifier(importLocalLiteral) const importDeclaration = t.importDeclaration([ t.importSpecifier(importLocalIdentifier, importIdentifier) ], t.stringLiteral(importFrom)); node.body.unshift(importDeclaration); }
return __awaiter(this, void 0, void 0, function* () { const bundleSource = babel.program([]); const sourceAnalysis = yield bundler.analyzer.analyze([...assignedBundle.bundle.files]); for (const resolvedSourceUrl of [...assignedBundle.bundle.files].sort()) { const moduleDocument = analyzer_utils_1.getAnalysisDocument(sourceAnalysis, resolvedSourceUrl); const moduleExports = es6_module_utils_1.getModuleExportNames(moduleDocument); const starExportName = es6_module_utils_1.getOrSetBundleModuleExportName(assignedBundle, resolvedSourceUrl, '*'); bundleSource.body.push(babel.importDeclaration([babel.importNamespaceSpecifier(babel.identifier(starExportName))], babel.stringLiteral(resolvedSourceUrl))); if (moduleExports.size > 0) { bundleSource.body.push(babel.exportNamedDeclaration(undefined, [babel.exportSpecifier(babel.identifier(starExportName), babel.identifier(starExportName))])); bundleSource.body.push(babel.exportNamedDeclaration(undefined, [...moduleExports].map((e) => babel.exportSpecifier(babel.identifier(e), babel.identifier(es6_module_utils_1.getOrSetBundleModuleExportName(assignedBundle, resolvedSourceUrl, e)))), babel.stringLiteral(resolvedSourceUrl))); } } const { code } = babel_generator_1.default(bundleSource); return code; });
function transform(options) { if (options.adapter) { adapter_1.setAdapter(options.adapter); } if (adapter_1.Adapter.type === "swan" /* swan */) { constant_1.setLoopOriginal('privateOriginal'); } constant_1.THIRD_PARTY_COMPONENTS.clear(); const code = options.isTyped ? ts.transpile(options.code, { jsx: ts.JsxEmit.Preserve, target: ts.ScriptTarget.ESNext, importHelpers: true, noEmitHelpers: true }) : options.code; options.env = Object.assign({ 'process.env.TARO_ENV': options.adapter || 'weapp' }, options.env || {}); options_1.setTransformOptions(options); utils_1.setting.sourceCode = code; // babel-traverse 无法生成 Hub // 导致 Path#getSource|buildCodeFrameError 都无法直接使用 // 原因大概是 babylon.parse 没有生成 File 实例导致 scope 和 path 原型上都没有 `file` // 将来升级到 babel@7 可以直接用 parse 而不是 transform const ast = babel_core_1.transform(code, options_1.buildBabelTransformOptions()).ast; if (options.isNormal) { return { ast }; } // transformFromAst(ast, code) let result; const componentSourceMap = new Map(); const imageSource = new Set(); const importSources = new Set(); let componentProperies = []; let mainClass; let storeName; let renderMethod; let isImportTaro = false; babel_traverse_1.default(ast, { TemplateLiteral(path) { const nodes = []; const { quasis, expressions } = path.node; let index = 0; if (path.parentPath.isTaggedTemplateExpression()) { return; } for (const elem of quasis) { if (elem.value.cooked) { nodes.push(t.stringLiteral(elem.value.cooked)); } if (index < expressions.length) { const expr = expressions[index++]; if (!t.isStringLiteral(expr, { value: '' })) { nodes.push(expr); } } } // + 号连接符必须保证第一和第二个 node 都是字符串 if (!t.isStringLiteral(nodes[0]) && !t.isStringLiteral(nodes[1])) { nodes.unshift(t.stringLiteral('')); } let root = nodes[0]; for (let i = 1; i < nodes.length; i++) { root = t.binaryExpression('+', root, nodes[i]); } path.replaceWith(root); }, ClassDeclaration(path) { mainClass = path; const superClass = utils_1.getSuperClassCode(path); if (superClass) { try { componentProperies = transform({ isRoot: false, isApp: false, code: superClass.code, isTyped: true, sourcePath: superClass.sourcePath, outputPath: superClass.sourcePath }).componentProperies; } catch (error) { // } } }, ClassExpression(path) { mainClass = path; }, ClassMethod(path) { if (t.isIdentifier(path.node.key) && path.node.key.name === 'render') { renderMethod = path; } }, IfStatement(path) { const consequent = path.get('consequent'); if (!consequent.isBlockStatement()) { consequent.replaceWith(t.blockStatement([ consequent.node ])); } }, CallExpression(path) { const callee = path.get('callee'); if (utils_1.isContainJSXElement(path)) { return; } if (callee.isReferencedMemberExpression()) { const id = utils_1.findFirstIdentifierFromMemberExpression(callee.node); const property = callee.node.property; if (t.isIdentifier(property) && property.name.startsWith('on')) { const funcExpr = path.findParent(p => p.isFunctionExpression()); if (funcExpr && funcExpr.isFunctionExpression()) { const taroAPI = funcExpr.findParent(p => p.isCallExpression() && t.isMemberExpression(p.node.callee) && t.isIdentifier(p.node.callee.object, { name: 'Taro' })); if (taroAPI && taroAPI.isCallExpression()) { throw utils_1.codeFrameError(funcExpr.node, '在回调函数使用从 props 传递的函数时,请把回调函数改造为箭头函数并一直使用 `this` 取值'); } } } const calleeIds = getIdsFromMemberProps(callee.node); if (t.isIdentifier(id) && id.name.startsWith('on') && "alipay" /* alipay */ !== adapter_1.Adapter.type) { const fullPath = buildFullPathThisPropsRef(id, calleeIds, path); if (fullPath) { path.replaceWith(t.callExpression(fullPath, path.node.arguments)); } } } if (callee.isReferencedIdentifier()) { const id = callee.node; const ids = [id.name]; if (t.isIdentifier(id) && id.name.startsWith('on')) { const funcExpr = path.findParent(p => p.isFunctionExpression()); if (funcExpr && funcExpr.isFunctionExpression()) { const taroAPI = funcExpr.findParent(p => p.isCallExpression() && t.isMemberExpression(p.node.callee) && t.isIdentifier(p.node.callee.object, { name: 'Taro' })); if (taroAPI && taroAPI.isCallExpression()) { throw utils_1.codeFrameError(funcExpr.node, '在回调函数使用从 props 传递的函数时,请把回调函数改造为箭头函数并一直使用 `this` 取值'); } } const fullPath = buildFullPathThisPropsRef(id, ids, path); if (fullPath) { path.replaceWith(t.callExpression(fullPath, path.node.arguments)); } } } }, // JSXIdentifier (path) { // const parentPath = path.parentPath // if (!parentPath.isJSXAttribute()) { // return // } // const element = parentPath.parentPath // if (!element.isJSXOpeningElement()) { // return // } // const elementName = element.get('name') // if (!elementName.isJSXIdentifier()) { // return // } // if (DEFAULT_Component_SET.has(elementName.node.name)) { // return // } // const expr = parentPath.get('value.expression') // }, JSXElement(path) { const assignment = path.findParent(p => p.isAssignmentExpression()); if (assignment && assignment.isAssignmentExpression() && !options.isTyped) { const left = assignment.node.left; if (t.isIdentifier(left)) { const binding = assignment.scope.getBinding(left.name); if (binding && binding.scope === assignment.scope) { if (binding.path.isVariableDeclarator()) { binding.path.node.init = path.node; assignment.remove(); } else { throw utils_1.codeFrameError(path.node, '同一个作用域的JSX 变量延时赋值没有意义。详见:https://github.com/NervJS/taro/issues/550'); } } } } const switchStatement = path.findParent(p => p.isSwitchStatement()); if (switchStatement && switchStatement.isSwitchStatement()) { const { discriminant, cases } = switchStatement.node; const ifStatement = cases.map((Case, index) => { const [consequent] = Case.consequent; if (!t.isBlockStatement(consequent)) { throw utils_1.codeFrameError(switchStatement.node, '含有 JSX 的 switch case 语句必须每种情况都用花括号 `{}` 包裹结果'); } const block = t.blockStatement(consequent.body.filter(b => !t.isBreakStatement(b))); if (index !== cases.length - 1 && t.isNullLiteral(Case.test)) { throw utils_1.codeFrameError(Case, '含有 JSX 的 switch case 语句只有最后一个 case 才能是 default'); } const test = Case.test === null ? t.nullLiteral() : t.binaryExpression('===', discriminant, Case.test); return { block, test }; }).reduceRight((ifStatement, item) => { if (t.isNullLiteral(item.test)) { ifStatement.alternate = item.block; return ifStatement; } const newStatement = t.ifStatement(item.test, item.block, t.isBooleanLiteral(ifStatement.test, { value: false }) ? ifStatement.alternate : ifStatement); return newStatement; }, t.ifStatement(t.booleanLiteral(false), t.blockStatement([]))); switchStatement.insertAfter(ifStatement); switchStatement.remove(); } const isForStatement = (p) => p && (p.isForStatement() || p.isForInStatement() || p.isForOfStatement()); const forStatement = path.findParent(isForStatement); if (isForStatement(forStatement)) { throw utils_1.codeFrameError(forStatement.node, '不行使用 for 循环操作 JSX 元素,详情:https://github.com/NervJS/taro/blob/master/packages/eslint-plugin-taro/docs/manipulate-jsx-as-array.md'); } const loopCallExpr = path.findParent(p => utils_1.isArrayMapCallExpression(p)); if (loopCallExpr && loopCallExpr.isCallExpression()) { const [func] = loopCallExpr.node.arguments; if (t.isArrowFunctionExpression(func) && !t.isBlockStatement(func.body)) { func.body = t.blockStatement([ t.returnStatement(func.body) ]); } } }, JSXOpeningElement(path) { const { name } = path.node.name; const binding = path.scope.getBinding(name); if (process.env.NODE_ENV !== 'test' && constant_1.DEFAULT_Component_SET.has(name) && binding && binding.kind === 'module') { const bindingPath = binding.path; if (bindingPath.parentPath.isImportDeclaration()) { const source = bindingPath.parentPath.node.source; if (source.value !== constant_1.COMPONENTS_PACKAGE_NAME) { throw utils_1.codeFrameError(bindingPath.parentPath.node, `内置组件名: '${name}' 只能从 ${constant_1.COMPONENTS_PACKAGE_NAME} 引入。`); } } } if (name === 'Provider') { const modules = path.scope.getAllBindings('module'); const providerBinding = Object.values(modules).some((m) => m.identifier.name === 'Provider'); if (providerBinding) { path.node.name = t.jSXIdentifier('view'); const store = path.node.attributes.find(attr => attr.name.name === 'store'); if (store && t.isJSXExpressionContainer(store.value) && t.isIdentifier(store.value.expression)) { storeName = store.value.expression.name; } path.node.attributes = []; } } if (constant_1.IMAGE_COMPONENTS.has(name)) { for (const attr of path.node.attributes) { if (attr.name.name === 'src') { if (t.isStringLiteral(attr.value)) { imageSource.add(attr.value.value); } else if (t.isJSXExpressionContainer(attr.value)) { if (t.isStringLiteral(attr.value.expression)) { imageSource.add(attr.value.expression.value); } } } } } }, JSXAttribute(path) { const { name, value } = path.node; if (options.jsxAttributeNameReplace) { for (const r in options.jsxAttributeNameReplace) { if (options.jsxAttributeNameReplace.hasOwnProperty(r)) { const element = options.jsxAttributeNameReplace[r]; if (t.isJSXIdentifier(name, { name: r })) { path.node.name = t.jSXIdentifier(element); } } } } if (!t.isJSXIdentifier(name) || value === null || t.isStringLiteral(value) || t.isJSXElement(value)) { return; } const expr = value.expression; const exprPath = path.get('value.expression'); const classDecl = path.findParent(p => p.isClassDeclaration()); const classDeclName = classDecl && classDecl.isClassDeclaration() && lodash_1.get(classDecl, 'node.id.name', ''); let isConverted = false; if (classDeclName) { isConverted = classDeclName === '_C' || classDeclName.endsWith('Tmpl'); } if (!t.isBinaryExpression(expr, { operator: '+' }) && !t.isLiteral(expr) && name.name === 'style' && !isConverted) { const jsxID = path.findParent(p => p.isJSXOpeningElement()).get('name'); if (jsxID && jsxID.isJSXIdentifier() && constant_1.DEFAULT_Component_SET.has(jsxID.node.name)) { exprPath.replaceWith(t.callExpression(t.identifier(constant_1.INTERNAL_INLINE_STYLE), [expr])); } } if (name.name.startsWith('on')) { if (exprPath.isReferencedIdentifier()) { const ids = [expr.name]; const fullPath = buildFullPathThisPropsRef(expr, ids, path); if (fullPath) { exprPath.replaceWith(fullPath); } } if (exprPath.isReferencedMemberExpression()) { const id = utils_1.findFirstIdentifierFromMemberExpression(expr); const ids = getIdsFromMemberProps(expr); if (t.isIdentifier(id)) { const fullPath = buildFullPathThisPropsRef(id, ids, path); if (fullPath) { exprPath.replaceWith(fullPath); } } } // @TODO: bind 的处理待定 } }, ImportDeclaration(path) { const source = path.node.source.value; if (importSources.has(source)) { throw utils_1.codeFrameError(path.node, '无法在同一文件重复 import 相同的包。'); } else { importSources.add(source); } const names = []; if (source === constant_1.TARO_PACKAGE_NAME) { isImportTaro = true; path.node.specifiers.push(t.importSpecifier(t.identifier(constant_1.INTERNAL_SAFE_GET), t.identifier(constant_1.INTERNAL_SAFE_GET)), t.importSpecifier(t.identifier(constant_1.INTERNAL_GET_ORIGNAL), t.identifier(constant_1.INTERNAL_GET_ORIGNAL)), t.importSpecifier(t.identifier(constant_1.INTERNAL_INLINE_STYLE), t.identifier(constant_1.INTERNAL_INLINE_STYLE)), t.importSpecifier(t.identifier(constant_1.GEL_ELEMENT_BY_ID), t.identifier(constant_1.GEL_ELEMENT_BY_ID))); } if (source === constant_1.REDUX_PACKAGE_NAME || source === constant_1.MOBX_PACKAGE_NAME) { path.node.specifiers.forEach((s, index, specs) => { if (s.local.name === 'Provider') { specs.splice(index, 1); specs.push(t.importSpecifier(t.identifier('setStore'), t.identifier('setStore'))); } }); } path.traverse({ ImportDefaultSpecifier(path) { const name = path.node.local.name; names.push(name); }, ImportSpecifier(path) { const name = path.node.imported.name; names.push(name); if (source === constant_1.TARO_PACKAGE_NAME && name === 'Component') { path.node.local = t.identifier('__BaseComponent'); } } }); componentSourceMap.set(source, names); } }); if (!isImportTaro) { ast.program.body.unshift(t.importDeclaration([ t.importDefaultSpecifier(t.identifier('Taro')), t.importSpecifier(t.identifier(constant_1.INTERNAL_SAFE_GET), t.identifier(constant_1.INTERNAL_SAFE_GET)), t.importSpecifier(t.identifier(constant_1.INTERNAL_GET_ORIGNAL), t.identifier(constant_1.INTERNAL_GET_ORIGNAL)), t.importSpecifier(t.identifier(constant_1.INTERNAL_INLINE_STYLE), t.identifier(constant_1.INTERNAL_INLINE_STYLE)) ], t.stringLiteral('@tarojs/taro'))); } if (!mainClass) { throw new Error('未找到 Taro.Component 的类定义'); } mainClass.node.body.body.forEach(handleThirdPartyComponent); const storeBinding = mainClass.scope.getBinding(storeName); mainClass.scope.rename('Component', '__BaseComponent'); if (storeBinding) { const statementPath = storeBinding.path.getStatementParent(); if (statementPath) { ast.program.body.forEach((node, index, body) => { if (node === statementPath.node) { body.splice(index + 1, 0, t.expressionStatement(t.callExpression(t.identifier('setStore'), [ t.identifier(storeName) ]))); } }); } } resetTSClassProperty(mainClass.node.body.body); if (options.isApp) { renderMethod.replaceWith(t.classMethod('method', t.identifier('_createData'), [], t.blockStatement([]))); return { ast }; } result = new class_1.Transformer(mainClass, options.sourcePath, componentProperies).result; result.code = babel_generator_1.default(ast).code; result.ast = ast; const lessThanSignReg = new RegExp(constant_1.lessThanSignPlacehold, 'g'); result.compressedTemplate = result.template; result.template = html_1.prettyPrint(result.template, { max_char: 0, unformatted: process.env.NODE_ENV === 'test' ? [] : ['text'] }); result.template = result.template.replace(lessThanSignReg, '<'); result.imageSrcs = Array.from(imageSource); return result; }
function processOthers (code, filePath, fileType) { let ast = wxTransformer({ code, sourcePath: filePath, isNormal: true, isTyped: Util.REG_TYPESCRIPT.test(filePath), adapter: 'h5' }).ast let taroImportDefaultName let hasAddNervJsImportDefaultName = false let hasJSX = false let isPage = fileType === FILE_TYPE.PAGE let hasComponentDidMount = false let hasComponentDidShow = false ast = babel.transformFromAst(ast, '', { plugins: [ [require('babel-plugin-danger-remove-unused-import'), { ignore: ['@tarojs/taro', 'react', 'nervjs'] }] ] }).ast const ClassDeclarationOrExpression = { enter (astPath) { const node = astPath.node if (!node.superClass) return if ( node.superClass.type === 'MemberExpression' && node.superClass.object.name === taroImportDefaultName ) { node.superClass.object.name = taroImportDefaultName if (node.id === null) { const renameComponentClassName = '_TaroComponentClass' astPath.replaceWith( t.classExpression( t.identifier(renameComponentClassName), node.superClass, node.body, node.decorators || [] ) ) } //@fix } else if (node.superClass.name === 'Component' || node.superClass.name === 'WeElement') { resetTSClassProperty(node.body.body) if (node.id === null) { const renameComponentClassName = '_TaroComponentClass' astPath.replaceWith( t.classExpression( t.identifier(renameComponentClassName), node.superClass, node.body, node.decorators || [] ) ) } } } } const programExitVisitor = { ClassBody: { exit (astPath) { if (!hasComponentDidMount) { astPath.pushContainer('body', t.classMethod( 'method', t.identifier('componentDidMount'), [], t.blockStatement([]), false, false)) } if (!hasComponentDidShow) { astPath.pushContainer('body', t.classMethod( 'method', t.identifier('componentDidShow'), [], t.blockStatement([]), false, false)) } //@fix astPath.unshiftContainer('body', t.classProperty( t.identifier('static css'), t.identifier('___css') )) } } } traverse(ast, { ClassExpression: ClassDeclarationOrExpression, ClassDeclaration: ClassDeclarationOrExpression, ClassMethod: isPage ? { exit (astPath) { const node = astPath.node const key = node.key const keyName = getObjKey(key) if (keyName === 'componentDidMount') { hasComponentDidMount = true } else if (keyName === 'componentDidShow') { hasComponentDidShow = true } } } : {}, //@fix ClassBody:{ enter (astPath) { astPath.unshiftContainer('body', t.classProperty( t.identifier('static css'), t.identifier('___css') )) } }, ImportDeclaration: { enter (astPath) { const node = astPath.node const source = node.source let value = source.value const specifiers = node.specifiers //@fix if(value.endsWith('.css')){ let css = fs.readFileSync(filePath.replace('.tsx','.css').replace('.js','.css'), 'utf-8').replace(/\/\*[^*]*\*+([^/][^*]*\*+)*\//g, '') //page里需要注入 appcss if(filePath.indexOf('/src/pages/')!==-1||filePath.indexOf('\\src\\pages\\')!==-1){ css = appCSS + css } astPath.replaceWith(t.variableDeclaration('const',[t.variableDeclarator(t.identifier(`___css`),t.callExpression(t.identifier('Omi.rpx'),[t.stringLiteral(compileWxss(css))]),)])) return } if (Util.isAliasPath(value, pathAlias)) { source.value = value = Util.replaceAliasPath(filePath, value, pathAlias) } if (!Util.isNpmPkg(value)) { if (Util.REG_SCRIPTS.test(value)) { const realPath = path.resolve(filePath, '..', value) const dirname = path.dirname(realPath) const extname = path.extname(realPath) const removeExtPath = path.join(dirname, path.basename(realPath, extname)) node.source = t.stringLiteral(Util.promoteRelativePath(path.relative(filePath, removeExtPath)).replace(/\\/g, '/')) } } else if (value === PACKAGES['@tarojs/taro']) { let specifier = specifiers.find(item => item.type === 'ImportDefaultSpecifier') if (specifier) { hasAddNervJsImportDefaultName = true taroImportDefaultName = specifier.local.name specifier.local.name = nervJsImportDefaultName } else if (!hasAddNervJsImportDefaultName) { //@fix //hasAddNervJsImportDefaultName = true // node.specifiers.unshift( // t.importDefaultSpecifier(t.identifier(nervJsImportDefaultName)) // ) } const taroApisSpecifiers = [] const deletedIdx = [] specifiers.forEach((item, index) => { if (item.imported && taroApis.indexOf(item.imported.name) >= 0) { taroApisSpecifiers.push(t.importSpecifier(t.identifier(item.local.name), t.identifier(item.imported.name))) deletedIdx.push(index) } }) _.pullAt(specifiers, deletedIdx) source.value = PACKAGES['nervjs'] if (taroApisSpecifiers.length) { astPath.insertBefore(t.importDeclaration(taroApisSpecifiers, t.stringLiteral(PACKAGES['@tarojs/taro-h5']))) } if (!specifiers.length) { astPath.remove() } } else if (value === PACKAGES['@tarojs/redux']) { source.value = PACKAGES['@tarojs/redux-h5'] } else if (value === PACKAGES['@tarojs/mobx']) { source.value = PACKAGES['@tarojs/mobx-h5'] } } }, JSXElement: { enter (astPath) { hasJSX = true } }, Program: { exit (astPath) { if (isPage) { astPath.traverse(programExitVisitor) } const node = astPath.node if (hasJSX && !hasAddNervJsImportDefaultName) { //@fix // node.body.unshift( // t.importDeclaration([ // t.importDefaultSpecifier(t.identifier(nervJsImportDefaultName)) // ], t.stringLiteral(PACKAGES['nervjs'])) // ) } if (taroImportDefaultName) { const importTaro = toAst(`import ${taroImportDefaultName} from '${PACKAGES['@tarojs/taro-h5']}'`) node.body.unshift(importTaro) } } } }) const generateCode = generate(ast, { jsescOption: { minimal: true } }).code return { code: generateCode, ast } }
function processEntry (code, filePath) { let ast = wxTransformer({ code, sourcePath: filePath, isNormal: true, isTyped: Util.REG_TYPESCRIPT.test(filePath), adapter: 'h5' }).ast let taroImportDefaultName let providorImportName let storeName let renderCallCode let hasAddNervJsImportDefaultName = false let hasConstructor = false let hasComponentWillMount = false let hasComponentDidMount = false let hasComponentDidShow = false let hasComponentDidHide = false let hasComponentWillUnmount = false let hasJSX = false let hasState = false const initPxTransformNode = toAst(`Taro.initPxTransform(${JSON.stringify(pxTransformConfig)})`) const additionalConstructorNode = toAst(`Taro._set$app(this)`) ast = babel.transformFromAst(ast, '', { plugins: [ [require('babel-plugin-danger-remove-unused-import'), { ignore: ['@tarojs/taro', 'react', 'nervjs'] }] ] }).ast const ClassDeclarationOrExpression = { enter (astPath) { const node = astPath.node if (!node.superClass) return if ( node.superClass.type === 'MemberExpression' && node.superClass.object.name === taroImportDefaultName ) { node.superClass.object.name = taroImportDefaultName if (node.id === null) { const renameComponentClassName = '_TaroComponentClass' astPath.replaceWith( t.classExpression( t.identifier(renameComponentClassName), node.superClass, node.body, node.decorators || [] ) ) } //@fix } else if (node.superClass.name === 'Component' || node.superClass.name === 'WeElement') { resetTSClassProperty(node.body.body) if (node.id === null) { const renameComponentClassName = '_TaroComponentClass' astPath.replaceWith( t.classExpression( t.identifier(renameComponentClassName), node.superClass, node.body, node.decorators || [] ) ) } } } } /** * ProgramExit使用的visitor * 负责修改render函数的内容,在componentDidMount中增加componentDidShow调用,在componentWillUnmount中增加componentDidHide调用。 */ const programExitVisitor = { ClassMethod: { exit (astPath) { const node = astPath.node const key = node.key const keyName = getObjKey(key) let funcBody const isRender = keyName === 'render' const isComponentWillMount = keyName === 'componentWillMount' const isComponentDidMount = keyName === 'componentDidMount' const isComponentWillUnmount = keyName === 'componentWillUnmount' const isConstructor = keyName === 'constructor' const basename = JSON.stringify(addLeadingSlash(stripTrailingSlash(routerBasename))) if (isRender) { const routes = pages.map((v, k) => { const absPagename = addLeadingSlash(v) const relPagename = `.${absPagename}` const chunkName = relPagename.split('/').filter(v => !/^(pages|\.)$/i.test(v)).join('_') return createRoute({ absPagename, relPagename, chunkName, isIndex: k === 0 }) }) //@fix funcBody = `<o-router mode={${JSON.stringify(routerMode)}} publicPath={${JSON.stringify(routerMode === 'hash' ? '/' : publicPath)}} routes={[${routes.join(',')}]} customRoutes={${JSON.stringify(customRoutes)}} basename={${basename}} />` /* 插入Tabbar */ if (tabBar) { const homePage = pages[0] || '' if (tabbarPos === 'top') { funcBody = ` <${tabBarContainerComponentName}> <${tabBarComponentName} conf={${tabBarConfigName}} homePage="${homePage}" router={${taroImportDefaultName}}/> <${tabBarPanelComponentName}> ${funcBody} </${tabBarPanelComponentName}> </${tabBarContainerComponentName}>` } else { funcBody = ` <${tabBarContainerComponentName}> <${tabBarPanelComponentName}> ${funcBody} </${tabBarPanelComponentName}> <${tabBarComponentName} mode={${JSON.stringify(routerMode)}} conf={this.state.${tabBarConfigName}} homePage="${homePage}" router={${taroImportDefaultName}} basename={${basename}} /> </${tabBarContainerComponentName}>` } } /* 插入<Provider /> */ if (providerComponentName && storeName) { // 使用redux 或 mobx funcBody = ` <${providorImportName} store={${storeName}}> ${funcBody} </${providorImportName}>` } /* 插入<Router /> */ node.body = toAst(`{return (${funcBody});}`, { preserveComments: true }) } if (tabBar && isComponentWillMount) { const initTabBarApisCallNode = toAst(`Taro.initTabBarApis(this, Taro)`) astPath.get('body').pushContainer('body', initTabBarApisCallNode) } // if (hasConstructor && isConstructor) { // astPath.get('body').pushContainer('body', additionalConstructorNode) // } if (hasComponentDidShow && isComponentDidMount) { const componentDidShowCallNode = toAst(`this.componentDidShow()`) astPath.get('body').pushContainer('body', componentDidShowCallNode) } if (hasComponentDidHide && isComponentWillUnmount) { const componentDidHideCallNode = toAst(`this.componentDidHide()`) astPath.get('body').unshiftContainer('body', componentDidHideCallNode) } } }, ClassProperty: { exit (astPath) { const node = astPath.node const key = node.key const value = node.value if (key.name !== 'state' || !t.isObjectExpression(value)) return astPath.node.value.properties.push(t.objectProperty( t.identifier(tabBarConfigName), tabBar )) } }, ClassBody: { exit (astPath) { if (hasComponentDidShow && !hasComponentDidMount) { astPath.pushContainer('body', t.classMethod( 'method', t.identifier('componentDidMount'), [], t.blockStatement([]), false, false)) } if (hasComponentDidHide && !hasComponentWillUnmount) { astPath.pushContainer('body', t.classMethod( 'method', t.identifier('componentWillUnmount'), [], t.blockStatement([]), false, false)) } //@fix // if (!hasConstructor) { // astPath.pushContainer('body', t.classMethod( // 'method', t.identifier('constructor'), [t.identifier('props'), t.identifier('context')], // t.blockStatement([toAst('super(props, context)'), additionalConstructorNode]), false, false)) // } if (tabBar) { if (!hasComponentWillMount) { astPath.pushContainer('body', t.classMethod( 'method', t.identifier('componentWillMount'), [], t.blockStatement([]), false, false)) } if (!hasState) { astPath.unshiftContainer('body', t.classProperty( t.identifier('state'), t.objectExpression([]) )) } } } } } /** * ClassProperty使用的visitor * 负责收集config中的pages,收集tabbar的position,替换icon。 */ const classPropertyVisitor = { ObjectProperty (astPath) { const node = astPath.node const key = node.key const value = node.value const keyName = getObjKey(key) if (keyName === 'pages' && t.isArrayExpression(value)) { const subPackageParent = astPath.findParent(isUnderSubPackages) let root = '' if (subPackageParent) { /* 在subPackages属性下,说明是分包页面,需要处理root属性 */ const rootNode = astPath.parent.properties.find(v => { return getObjKey(v.key) === 'root' }) root = rootNode ? rootNode.value.value : '' } value.elements.forEach(v => { const pagePath = `${root}/${v.value}`.replace(/\/{2,}/g, '/') pages.push(pagePath.replace(/^\//, '')) }) } else if (keyName === 'tabBar' && t.isObjectExpression(value)) { // tabBar tabBar = value value.properties.forEach(node => { if (node.keyName === 'position') tabbarPos = node.value.value }) } else if ((keyName === 'iconPath' || keyName === 'selectedIconPath') && t.isStringLiteral(value)) { astPath.replaceWith( t.objectProperty(t.stringLiteral(keyName), t.callExpression(t.identifier('require'), [t.stringLiteral(`./${value.value}`)])) ) } } } traverse(ast, { ClassExpression: ClassDeclarationOrExpression, ClassDeclaration: ClassDeclarationOrExpression, ClassProperty: { enter (astPath) { const node = astPath.node const key = node.key const value = node.value const keyName = getObjKey(key) if (keyName === 'state') hasState = true if (keyName !== 'config' || !t.isObjectExpression(value)) return astPath.traverse(classPropertyVisitor) } }, ImportDeclaration: { enter (astPath) { const node = astPath.node const source = node.source const specifiers = node.specifiers let value = source.value //@fix if(value.endsWith('.css')){ appCSS = fs.readFileSync(filePath.replace('.tsx','.css').replace('.js','.css'), 'utf-8').replace(/\/\*[^*]*\*+([^/][^*]*\*+)*\//g, '') // astPath.replaceWith(t.variableDeclaration('const',[t.variableDeclarator(t.identifier(`___css`),t.stringLiteral(appCSS))])) astPath.remove() return } if (Util.isAliasPath(value, pathAlias)) { source.value = value = Util.replaceAliasPath(filePath, value, pathAlias) } if (!Util.isNpmPkg(value)) { if (value.indexOf('.') === 0) { const pathArr = value.split('/') if (pathArr.indexOf('pages') >= 0) { astPath.remove() } else if (Util.REG_SCRIPTS.test(value)) { const realPath = path.resolve(filePath, '..', value) const dirname = path.dirname(realPath) const extname = path.extname(realPath) const removeExtPath = path.join(dirname, path.basename(realPath, extname)) node.source = t.stringLiteral(Util.promoteRelativePath(path.relative(filePath, removeExtPath)).replace(/\\/g, '/')) } } return } if (value === PACKAGES['@tarojs/taro']) { let specifier = specifiers.find(item => item.type === 'ImportDefaultSpecifier') if (specifier) { hasAddNervJsImportDefaultName = true taroImportDefaultName = specifier.local.name specifier.local.name = nervJsImportDefaultName } else if (!hasAddNervJsImportDefaultName) { hasAddNervJsImportDefaultName = true //@fix // node.specifiers.unshift( // t.importDefaultSpecifier(t.identifier(nervJsImportDefaultName)) // ) } const taroApisSpecifiers = [] const deletedIdx = [] specifiers.forEach((item, index) => { if (item.imported && taroApis.indexOf(item.imported.name) >= 0) { taroApisSpecifiers.push(t.importSpecifier(t.identifier(item.local.name), t.identifier(item.imported.name))) deletedIdx.push(index) } }) _.pullAt(specifiers, deletedIdx) source.value = PACKAGES['nervjs'] if (taroApisSpecifiers.length) { astPath.insertBefore(t.importDeclaration(taroApisSpecifiers, t.stringLiteral(PACKAGES['@tarojs/taro-h5']))) } if (!specifiers.length) { astPath.remove() } } else if (value === PACKAGES['@tarojs/redux']) { const specifier = specifiers.find(item => { return t.isImportSpecifier(item) && item.imported.name === providerComponentName }) if (specifier) { providorImportName = specifier.local.name } else { providorImportName = providerComponentName specifiers.push(t.importSpecifier(t.identifier(providerComponentName), t.identifier(providerComponentName))) } source.value = PACKAGES['@tarojs/redux-h5'] } else if (value === PACKAGES['@tarojs/mobx']) { const specifier = specifiers.find(item => { return t.isImportSpecifier(item) && item.imported.name === providerComponentName }) if (specifier) { providorImportName = specifier.local.name } else { providorImportName = providerComponentName specifiers.push(t.importSpecifier(t.identifier(providerComponentName), t.identifier(providerComponentName))) } source.value = PACKAGES['@tarojs/mobx-h5'] } } }, CallExpression: { enter (astPath) { const node = astPath.node const callee = node.callee const calleeName = callee.name const parentPath = astPath.parentPath if (t.isMemberExpression(callee)) { if (callee.object.name === taroImportDefaultName && callee.property.name === 'render') { callee.object.name = nervJsImportDefaultName renderCallCode = generate(astPath.node).code astPath.remove() } } else { if (calleeName === setStoreFuncName) { if (parentPath.isAssignmentExpression() || parentPath.isExpressionStatement() || parentPath.isVariableDeclarator()) { parentPath.remove() } } } } }, ClassMethod: { exit (astPath) { const node = astPath.node const key = node.key const keyName = getObjKey(key) if (keyName === 'constructor') { hasConstructor = true } else if (keyName === 'componentWillMount') { hasComponentWillMount = true } else if (keyName === 'componentDidMount') { hasComponentDidMount = true } else if (keyName === 'componentDidShow') { hasComponentDidShow = true } else if (keyName === 'componentDidHide') { hasComponentDidHide = true } else if (keyName === 'componentWillUnmount') { hasComponentWillUnmount = true } } }, JSXElement: { enter (astPath) { hasJSX = true } }, JSXOpeningElement: { enter (astPath) { if (astPath.node.name.name === 'Provider') { for (let v of astPath.node.attributes) { if (v.name.name !== 'store') continue storeName = v.value.expression.name break } } } }, Program: { exit (astPath) { const importNervjsNode = t.importDefaultSpecifier(t.identifier(nervJsImportDefaultName)) const importRouterNode = toAst(`import '${PACKAGES['@tarojs/router']}'`) //@fix const importMpNode = toAst(`import './libs/mp'`) const importTaroH5Node = toAst(`import ${taroImportDefaultName} from '${PACKAGES['@tarojs/taro-h5']}'`) const importComponentNode = toAst(`import { View, ${tabBarComponentName}, ${tabBarContainerComponentName}, ${tabBarPanelComponentName}} from '${PACKAGES['@tarojs/components']}'`) const lastImportIndex = _.findLastIndex(astPath.node.body, t.isImportDeclaration) const lastImportNode = astPath.get(`body.${lastImportIndex > -1 ? lastImportIndex : 0}`) const extraNodes = [ //@fix //importTaroH5Node, importMpNode, importRouterNode, //@fix //initPxTransformNode ] astPath.traverse(programExitVisitor) if (hasJSX && !hasAddNervJsImportDefaultName) { //@fix //extraNodes.unshift(importNervjsNode) } if (tabBar) { extraNodes.unshift(importComponentNode) } lastImportNode.insertAfter(extraNodes) if (renderCallCode) { const renderCallNode = toAst(renderCallCode) astPath.pushContainer('body', renderCallNode) } } } }) const generateCode = generate(ast, { jsescOption: { minimal: true } }).code return { code: generateCode, ast } }
function parseAst (type, ast, sourceFilePath, filePath) { 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, index) => { obj[p.key.name] = traverseObjectNode(p.value) }) return obj } if (node.type === 'ObjectExpression') { const properties = node.properties obj = {} properties.forEach((p, index) => { 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 traverse(ast, { ClassDeclaration (astPath) { const node = astPath.node if (node.superClass) { if (node.superClass.name === 'Component' || (node.superClass.type === 'MemberExpression' && node.superClass.object.name === taroImportDefaultName)) { needExportDefault = true 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 } } } }, ClassProperty (astPath) { const node = astPath.node if (node.key.name === 'config') { configObj = traverseObjectNode(node) if (type === PARSE_AST_TYPE.ENTRY) { appConfig = configObj } astPath.remove() } }, ImportDeclaration (astPath) { const node = astPath.node const source = node.source let value = source.value const valueExtname = path.extname(value) if (Util.isNpmPkg(value) && notExistNpmList.indexOf(value) < 0) { if (value === taroJsComponents) { 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 } } }) } source.value = getExactedNpmFilePath(value, filePath) astPath.replaceWith(t.importDeclaration(node.specifiers, node.source)) } } else if (path.isAbsolute(value)) { Util.printLog(Util.pocessTypeEnum.ERROR, '引用文件', `文件 ${sourceFilePath} 中引用 ${value} 是绝对路径!`) } 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() } }, 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 { 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 } } }) } }) } args[0].value = getExactedNpmFilePath(value, filePath) 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 (callee.object.name === taroImportDefaultName && callee.property.name === 'render') { astPath.remove() } } else if (callee.name === 'require') { const args = node.arguments let value = args[0].value const valueExtname = path.extname(value) 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 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') { const superClass = declaration.superClass if (superClass && (superClass.name === 'Component' || (superClass.type === 'MemberExpression' && superClass.object.name === taroImportDefaultName))) { needExportDefault = true if (declaration.id === null) { componentClassName = '_TaroComponentClass' } else if (declaration.id.name === 'App') { componentClassName = '_App' } else { componentClassName = declaration.id.name } astPath.replaceWith(t.classDeclaration(t.identifier(componentClassName), superClass, declaration.body, declaration.decorators || [])) } } 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 isPage = false const pages = appConfig.pages let importPath = path.resolve(path.dirname(sourceFilePath), value) importPath = Util.resolveScriptPath(importPath) pages.forEach(page => { if (path.normalize(importPath).indexOf(path.normalize(page)) >= 0) { isPage = true } }) if (isPage) { 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)) { 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 = convertArrayToAstExpression(obj) } else { objArr = convertObjectToAstExpression(obj) } astPath.replaceWith(t.variableDeclaration('const', [t.variableDeclarator(t.identifier(defaultSpecifier), t.objectExpression(objArr))])) } else { astPath.remove() } } } 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 } }) if (defaultSpecifier) { astPath.replaceWith(t.variableDeclaration('const', [t.variableDeclarator(t.identifier(defaultSpecifier), t.stringLiteral(vpath.replace(sourceDir, '').replace(/\\/g, '/')))])) } else { astPath.remove() } } else if (!valueExtname) { let vpath = Util.resolveScriptPath(path.resolve(sourceFilePath, '..', value)) const 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 isPage = false const pages = appConfig.pages let importPath = path.resolve(path.dirname(sourceFilePath), value) importPath = Util.resolveScriptPath(importPath) pages.forEach(page => { if (path.normalize(importPath).indexOf(path.normalize(page)) >= 0) { isPage = true } }) if (isPage) { 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 = convertArrayToAstExpression(obj) } else { objArr = convertObjectToAstExpression(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)) { 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) } astPath.replaceWith(t.stringLiteral(vpath.replace(sourceDir, '').replace(/\\/g, '/'))) } else if (!valueExtname) { let vpath = Util.resolveScriptPath(path.resolve(sourceFilePath, '..', value)) const 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 = getExactedNpmFilePath(taroWeappFramework, filePath) let insert switch (type) { case PARSE_AST_TYPE.ENTRY: insert = template(`App(require('${taroWeappFrameworkPath}').default.createApp(${exportVariableName}))`, babylonConfig)() node.body.push(insert) break case PARSE_AST_TYPE.PAGE: insert = template(`Page(require('${taroWeappFrameworkPath}').default.createPage(${exportVariableName}, { path: '${sourceFilePath.replace(appPath + path.sep, '').replace(/\\/g, '/')}' }))`, babylonConfig)() node.body.push(insert) break default: break } } } }) return { code: generate(ast).code, styleFiles, scriptFiles, jsonFiles, configObj, mediaFiles, componentClassName } }
export const importReact = () => { return t.importDeclaration([ t.importDefaultSpecifier(t.identifier('React')), ], t.stringLiteral('react')) }
return modules.map(opts => { return t.importDeclaration( [t.importDefaultSpecifier(t.identifier(opts.identifier))], t.stringLiteral(opts.module) ); });
function extractStyles({ src, styleGroups, namedStyleGroups, sourceFileName, whitelistedModules, cacheObject, parserPlugins, addCSSRequire, errorCallback, extremelyLiteMode, }) { invariant(typeof src === 'string', '`src` must be a string of javascript'); invariant( typeof sourceFileName === 'string' && path.isAbsolute(sourceFileName), '`sourceFileName` must be an absolute path to a .js file' ); invariant( typeof cacheObject === 'object' && cacheObject !== null, '`cacheObject` must be an object' ); if (typeof styleGroups !== 'undefined') { invariant( Array.isArray(styleGroups), '`styleGroups` must be an array of style prop objects' ); } if (typeof namedStyleGroups !== 'undefined') { invariant( typeof namedStyleGroups === 'object' && namedStyleGroups !== null, '`namedStyleGroups` must be an object of style prop objects keyed by className' ); } if (typeof whitelistedModules !== 'undefined') { invariant( Array.isArray(whitelistedModules), '`whitelistedModules` must be an array of paths to modules that are OK to require' ); } if (typeof errorCallback !== 'undefined') { invariant( typeof errorCallback === 'function', '`errorCallback` is expected to be a function' ); } else { errorCallback = console.warn; } if (typeof addCSSRequire === 'undefined') { addCSSRequire = true; } const sourceDir = path.dirname(sourceFileName); // Using a map for (officially supported) guaranteed insertion order const cssMap = new Map(); const ast = parse(src, parserPlugins); let jsxstyleSrc; const validComponents = {}; let useImportSyntax = false; let hasValidComponents = false; let needsRuntimeJsxstyle = false; if (typeof extremelyLiteMode === 'string') { jsxstyleSrc = extremelyLiteMode === 'react' ? 'jsxstyle' : `jsxstyle-${extremelyLiteMode}`; Object.assign(validComponents, liteComponents); hasValidComponents = true; } // Find jsxstyle require in program root ast.program.body = ast.program.body.filter(item => { if (t.isVariableDeclaration(item)) { for (let idx = -1, len = item.declarations.length; ++idx < len; ) { const dec = item.declarations[idx]; if ( // var ... !t.isVariableDeclarator(dec) || // var {...} !t.isObjectPattern(dec.id) || // var {x} = require(...) !t.isCallExpression(dec.init) || !t.isIdentifier(dec.init.callee) || dec.init.callee.name !== 'require' || // var {x} = require('one-thing') dec.init.arguments.length !== 1 || !t.isStringLiteral(dec.init.arguments[0]) ) { continue; } // var {x} = require('jsxstyle') if (!JSXSTYLE_SOURCES.hasOwnProperty(dec.init.arguments[0].value)) { continue; } if (jsxstyleSrc) { invariant( jsxstyleSrc === dec.init.arguments[0].value, 'Expected duplicate `require` to be from "%s", received "%s"', jsxstyleSrc, dec.init.arguments[0].value ); } for (let idx = -1, len = dec.id.properties.length; ++idx < len; ) { const prop = dec.id.properties[idx]; if ( !t.isObjectProperty(prop) || !t.isIdentifier(prop.key) || !t.isIdentifier(prop.value) ) { continue; } // only add uppercase identifiers to validComponents if ( prop.key.name[0] !== prop.key.name[0].toUpperCase() || prop.value.name[0] !== prop.value.name[0].toUpperCase() ) { continue; } // map imported name to source component name validComponents[prop.value.name] = prop.key.name; hasValidComponents = true; } jsxstyleSrc = dec.init.arguments[0].value; // if this is the only variable declaration, remove it // TODO: handle weird const a = 1, b = 2; maybe if (len === 1) return false; } } else if (t.isImportDeclaration(item)) { // omfg everyone please just use import syntax // not imported from jsxstyle? byeeee if ( !t.isStringLiteral(item.source) || !JSXSTYLE_SOURCES.hasOwnProperty(item.source.value) ) { return true; } if (jsxstyleSrc) { invariant( jsxstyleSrc === item.source.value, 'Expected duplicate `import` to be from "%s", received "%s"', jsxstyleSrc, item.source.value ); } jsxstyleSrc = item.source.value; useImportSyntax = true; for (let idx = -1, len = item.specifiers.length; ++idx < len; ) { const specifier = item.specifiers[idx]; if ( !t.isImportSpecifier(specifier) || !t.isIdentifier(specifier.imported) || !t.isIdentifier(specifier.local) ) { continue; } if ( specifier.imported.name[0] !== specifier.imported.name[0].toUpperCase() || specifier.local.name[0] !== specifier.local.name[0].toUpperCase() ) { continue; } validComponents[specifier.local.name] = specifier.imported.name; hasValidComponents = true; } // remove import return false; } return true; }); // jsxstyle isn't included anywhere, so let's bail if (!jsxstyleSrc || !hasValidComponents) { return { js: src, css: '', cssFileName: null, ast, map: null, }; } // class or className? const classPropName = jsxstyleSrc === 'jsxstyle-preact' ? 'class' : 'className'; // Generate a UID that's unique in the program scope let boxComponentName; traverse(ast, { Program(path) { boxComponentName = path.scope.generateUid('Box'); }, }); traverse(ast, { JSXElement(path) { const node = path.node.openingElement; if ( // skip non-identifier opening elements (member expressions, etc.) !t.isJSXIdentifier(node.name) || // skip non-jsxstyle components !validComponents.hasOwnProperty(node.name.name) ) { return; } // Remember the source component const originalNodeName = node.name.name; const src = validComponents[originalNodeName]; const removeAllTrace = liteComponents.hasOwnProperty(originalNodeName); if (!removeAllTrace) { node.name.name = boxComponentName; } const defaultProps = componentStyles[src]; invariant(defaultProps, `jsxstyle component <${src} /> does not exist!`); const propKeys = Object.keys(defaultProps); // looping backwards because of unshift for (let idx = propKeys.length; --idx >= 0; ) { const prop = propKeys[idx]; const value = defaultProps[prop]; if (value == null || value === '') { continue; } let valueEx; if (typeof value === 'number') { valueEx = t.jSXExpressionContainer(t.numericLiteral(value)); } else if (typeof value === 'string') { valueEx = t.stringLiteral(value); } else { continue; } node.attributes.unshift(t.jSXAttribute(t.jSXIdentifier(prop), valueEx)); } // Generate scope object at this level const staticNamespace = getStaticBindingsForScope( path.scope, whitelistedModules, sourceFileName ); const evalContext = vm.createContext(staticNamespace); let lastSpreadIndex = null; const flattenedAttributes = []; node.attributes.forEach(attr => { if (t.isJSXSpreadAttribute(attr)) { if (canEvaluate(staticNamespace, attr.argument)) { const spreadValue = vm.runInContext( generate(attr.argument).code, evalContext ); if (typeof spreadValue !== 'object' || spreadValue === null) { lastSpreadIndex = flattenedAttributes.push(attr) - 1; } else { for (const k in spreadValue) { const value = spreadValue[k]; if (typeof value === 'number') { flattenedAttributes.push( t.jSXAttribute( t.jSXIdentifier(k), t.jSXExpressionContainer(t.numericLiteral(value)) ) ); } else if (value === null) { // why would you ever do this flattenedAttributes.push( t.jSXAttribute( t.jSXIdentifier(k), t.jSXExpressionContainer(t.nullLiteral()) ) ); } else { // toString anything else // TODO: is this a bad idea flattenedAttributes.push( t.jSXAttribute( t.jSXIdentifier(k), t.jSXExpressionContainer(t.stringLiteral('' + value)) ) ); } } } } else { lastSpreadIndex = flattenedAttributes.push(attr) - 1; } } else { flattenedAttributes.push(attr); } }); node.attributes = flattenedAttributes; let propsAttributes; const staticAttributes = {}; let inlinePropCount = 0; const staticTernaries = []; node.attributes = node.attributes.filter((attribute, idx) => { if ( // keep the weirdos !attribute.name || !attribute.name.name || // haven't hit the last spread operator idx < lastSpreadIndex ) { inlinePropCount++; return true; } const name = attribute.name.name; const value = t.isJSXExpressionContainer(attribute.value) ? attribute.value.expression : attribute.value; // if one or more spread operators are present and we haven't hit the last one yet, the prop stays inline if (lastSpreadIndex !== null && idx <= lastSpreadIndex) { inlinePropCount++; return true; } // pass ref, key, and style props through untouched if (UNTOUCHED_PROPS.hasOwnProperty(name)) { return true; } // className prop will be handled below if (name === classPropName) { return true; } // validate component prop if (name === 'component') { if (t.isLiteral(value) && typeof value.value === 'string') { const char1 = value.value[0]; // component="article" if (char1 === char1.toUpperCase()) { // an uppercase string with be turned into a component, which is not what we want. // TODO: look into transforming to React.createElement. // main downside is that further transformations that rely on JSX won't work. inlinePropCount++; } } else if (t.isIdentifier(value)) { const char1 = value.name[0]; // component={Avatar} if (char1 === char1.toLowerCase()) { // a lowercase identifier will be transformed to a DOM element. that's not good. inlinePropCount++; } } else if (t.isMemberExpression(value)) { // component={variable.prop} } else { // TODO: extract more complex expressions out as separate var errorCallback( '`component` prop value was not handled by extractStyles: %s', generate(value).code ); inlinePropCount++; } return true; } // pass key and style props through untouched if (UNTOUCHED_PROPS.hasOwnProperty(name)) { return true; } if (name === 'props') { if (!value) { errorCallback('`props` prop does not have a value'); inlinePropCount++; return true; } if (t.isObjectExpression(value)) { let errorCount = 0; const attributes = []; for (const k in value.properties) { const propObj = value.properties[k]; if (t.isObjectProperty(propObj)) { let key; if (t.isIdentifier(propObj.key)) { key = propObj.key.name; } else if (t.isStringLiteral(propObj.key)) { // starts with a-z or _ followed by a-z, -, or _ if (/^\w[\w-]+$/.test(propObj.key.value)) { key = propObj.key.value; } else { errorCallback( '`props` prop contains an invalid key: `%s`', propObj.key.value ); errorCount++; continue; } } else { errorCallback( 'unhandled object property key type: `%s`', propObj.type ); errorCount++; } if (ALL_SPECIAL_PROPS.hasOwnProperty(key)) { errorCallback( '`props` prop cannot contain `%s` as it is used by jsxstyle and will be overwritten.', key ); errorCount++; continue; } if (t.isStringLiteral(propObj.value)) { // convert literal value back to literal to ensure it has double quotes (siiiigh) attributes.push( t.jSXAttribute( t.jSXIdentifier(key), t.stringLiteral(propObj.value.value) ) ); } else { // wrap everything else in a JSXExpressionContainer attributes.push( t.jSXAttribute( t.jSXIdentifier(key), t.jSXExpressionContainer(propObj.value) ) ); } } else if (t.isSpreadProperty(propObj)) { attributes.push(t.jSXSpreadAttribute(propObj.argument)); } else { errorCallback( 'unhandled object property value type: `%s`', propObj.type ); errorCount++; } } if (errorCount > 0) { inlinePropCount++; } else { propsAttributes = attributes; } return true; } if ( // if it's not an object, spread it // props={wow()} t.isCallExpression(value) || // props={wow.cool} t.isMemberExpression(value) || // props={wow} t.isIdentifier(value) ) { propsAttributes = [t.jSXSpreadAttribute(value)]; return true; } // if props prop is weird-looking, leave it and warn errorCallback('props prop is an unhandled type: `%s`', value.type); inlinePropCount++; return true; } if (name === 'mediaQueries') { if (canEvaluateObject(staticNamespace, value)) { staticAttributes[name] = vm.runInContext( // parens so V8 doesn't parse the object like a block '(' + generate(value).code + ')', evalContext ); } else if (canEvaluate(staticNamespace, value)) { staticAttributes[name] = vm.runInContext( generate(value).code, evalContext ); } else { inlinePropCount++; return true; } return false; } // if value can be evaluated, extract it and filter it out if (canEvaluate(staticNamespace, value)) { staticAttributes[name] = vm.runInContext( generate(value).code, evalContext ); return false; } if (t.isConditionalExpression(value)) { // if both sides of the ternary can be evaluated, extract them if ( canEvaluate(staticNamespace, value.consequent) && canEvaluate(staticNamespace, value.alternate) ) { staticTernaries.push({ name, ternary: value }); // mark the prop as extracted staticAttributes[name] = null; return false; } } else if (t.isLogicalExpression(value)) { // convert a simple logical expression to a ternary with a null alternate if ( value.operator === '&&' && canEvaluate(staticNamespace, value.right) ) { staticTernaries.push({ name, ternary: { test: value.left, consequent: value.right, alternate: t.nullLiteral(), }, }); staticAttributes[name] = null; return false; } } if (removeAllTrace) { errorCallback( 'jsxstyle-loader encountered a dynamic prop (`%s`) on a lite ' + 'jsxstyle component (`%s`). If you would like to pass dynamic ' + 'styles to this component, specify them in the `style` prop.', generate(attribute).code, originalNodeName ); return false; } // if we've made it this far, the prop stays inline inlinePropCount++; return true; }); let classNamePropValue; const classNamePropIndex = node.attributes.findIndex( attr => attr.name && attr.name.name === classPropName ); if (classNamePropIndex > -1 && Object.keys(staticAttributes).length > 0) { classNamePropValue = getPropValueFromAttributes( classPropName, node.attributes ); node.attributes.splice(classNamePropIndex, 1); } // if all style props have been extracted, jsxstyle component can be // converted to a div or the specified component if (inlinePropCount === 0) { const propsPropIndex = node.attributes.findIndex( attr => attr.name && attr.name.name === 'props' ); // deal with props prop if (propsPropIndex > -1) { if (propsAttributes) { propsAttributes.forEach(a => node.attributes.push(a)); } // delete props prop node.attributes.splice(propsPropIndex, 1); } const componentPropIndex = node.attributes.findIndex( attr => attr.name && attr.name.name === 'component' ); if (componentPropIndex > -1) { const attribute = node.attributes[componentPropIndex]; const componentPropValue = t.isJSXExpressionContainer(attribute.value) ? attribute.value.expression : attribute.value; if ( t.isLiteral(componentPropValue) && typeof componentPropValue.value === 'string' ) { node.name.name = componentPropValue.value; } else if (t.isIdentifier(componentPropValue)) { node.name.name = componentPropValue.name; } else if (t.isMemberExpression(componentPropValue)) { node.name.name = generate(componentPropValue).code; } // remove component prop from attributes node.attributes.splice(componentPropIndex, 1); } else { node.name.name = 'div'; } } else { needsRuntimeJsxstyle = true; if (lastSpreadIndex !== null) { // if only some style props were extracted AND additional props are spread onto the component, // add the props back with null values to prevent spread props from incorrectly overwriting the extracted prop value Object.keys(staticAttributes).forEach(attr => { node.attributes.push( t.jSXAttribute( t.jSXIdentifier(attr), t.jSXExpressionContainer(t.nullLiteral()) ) ); }); } } if (path.node.closingElement) { path.node.closingElement.name.name = node.name.name; } const stylesByClassName = getStylesByClassName( styleGroups, namedStyleGroups, staticAttributes, cacheObject ); const extractedStyleClassNames = Object.keys(stylesByClassName).join(' '); const classNameObjects = []; if (classNamePropValue) { if (canEvaluate(null, classNamePropValue)) { // TODO: don't use canEvaluate here, need to be more specific classNameObjects.push( t.stringLiteral( vm.runInNewContext(generate(classNamePropValue).code) ) ); } else { classNameObjects.push(classNamePropValue); } } if (staticTernaries.length > 0) { const ternaryObj = extractStaticTernaries( staticTernaries, evalContext, cacheObject ); // ternaryObj is null if all of the extracted ternaries have falsey consequents and alternates if (ternaryObj !== null) { // add extracted styles by className to existing object Object.assign(stylesByClassName, ternaryObj.stylesByClassName); classNameObjects.push(ternaryObj.ternaryExpression); } } if (extractedStyleClassNames) { classNameObjects.push(t.stringLiteral(extractedStyleClassNames)); } const classNamePropValueForReals = classNameObjects.reduce((acc, val) => { if (!acc) { if ( // pass conditional expressions through t.isConditionalExpression(val) || // pass non-null literals through (t.isLiteral(val) && val.value !== null) ) { return val; } return t.logicalExpression('||', val, t.stringLiteral('')); } const accIsString = t.isLiteral(acc) && typeof acc.value === 'string'; let inner; if (t.isLiteral(val)) { if (typeof val.value === 'string') { if (accIsString) { // join adjacent string literals return t.stringLiteral(`${acc.value} ${val.value}`); } inner = t.stringLiteral(` ${val.value}`); } else { inner = t.binaryExpression('+', t.stringLiteral(' '), val); } } else if ( t.isConditionalExpression(val) || t.isBinaryExpression(val) ) { if (accIsString) { return t.binaryExpression( '+', t.stringLiteral(`${acc.value} `), val ); } inner = t.binaryExpression('+', t.stringLiteral(' '), val); } else if (t.isIdentifier(val) || t.isMemberExpression(val)) { // identifiers and member expressions make for reasonable ternaries inner = t.conditionalExpression( val, t.binaryExpression('+', t.stringLiteral(' '), val), t.stringLiteral('') ); } else { if (accIsString) { return t.binaryExpression( '+', t.stringLiteral(`${acc.value} `), t.logicalExpression('||', val, t.stringLiteral('')) ); } // use a logical expression for more complex prop values inner = t.binaryExpression( '+', t.stringLiteral(' '), t.logicalExpression('||', val, t.stringLiteral('')) ); } return t.binaryExpression('+', acc, inner); }, null); if (classNamePropValueForReals) { if ( t.isLiteral(classNamePropValueForReals) && typeof classNamePropValueForReals.value === 'string' ) { node.attributes.push( t.jSXAttribute( t.jSXIdentifier(classPropName), t.stringLiteral(classNamePropValueForReals.value) ) ); } else { node.attributes.push( t.jSXAttribute( t.jSXIdentifier(classPropName), t.jSXExpressionContainer(classNamePropValueForReals) ) ); } } const lineNumbers = node.loc.start.line + (node.loc.start.line !== node.loc.end.line ? `-${node.loc.end.line}` : ''); const comment = util.format( '/* %s:%s (%s) */', sourceFileName.replace(process.cwd(), '.'), lineNumbers, originalNodeName ); for (const className in stylesByClassName) { if (cssMap.has(className)) { if (comment) { const val = cssMap.get(className); val.commentTexts.push(comment); cssMap.set(className, val); } } else { let css = ''; const styleProps = stylesByClassName[className]; // get object of style objects const styleObjects = getStyleKeysForProps(styleProps, true); delete styleObjects.classNameKey; const styleObjectKeys = Object.keys(styleObjects).sort(); for (let idx = -1, len = styleObjectKeys.length; ++idx < len; ) { const k = styleObjectKeys[idx]; const item = styleObjects[k]; let itemCSS = `.${className}` + (item.pseudoclass ? ':' + item.pseudoclass : '') + (item.pseudoelement ? '::' + item.pseudoelement : '') + ` {${item.styles}}`; if (item.mediaQuery) { itemCSS = `@media ${item.mediaQuery} { ${itemCSS} }`; } css += itemCSS + '\n'; } cssMap.set(className, { css, commentTexts: [comment] }); } } }, }); const css = Array.from(cssMap.values()) .map(n => n.commentTexts.map(t => `${t}\n`).join('') + n.css) .join(''); // path.parse doesn't exist in the webpack'd bundle but path.dirname and path.basename do. const baseName = path.basename(sourceFileName, '.js'); const cssRelativeFileName = `./${baseName}.jsxstyle.css`; const cssFileName = path.join(sourceDir, cssRelativeFileName); // Conditionally add Box import/require to the top of the document if (needsRuntimeJsxstyle) { if (useImportSyntax) { ast.program.body.unshift( t.importDeclaration( [ t.importSpecifier( t.identifier(boxComponentName), t.identifier('Box') ), ], t.stringLiteral(jsxstyleSrc) ) ); } else { ast.program.body.unshift( t.variableDeclaration('var', [ t.variableDeclarator( t.identifier(boxComponentName), t.memberExpression( t.callExpression(t.identifier('require'), [ t.stringLiteral(jsxstyleSrc), ]), t.identifier('Box') ) ), ]) ); } } // append require/import statement to the document if (css !== '') { if (useImportSyntax) { ast.program.body.unshift( t.importDeclaration([], t.stringLiteral(cssRelativeFileName)) ); } else { ast.program.body.unshift( t.expressionStatement( t.callExpression(t.identifier('require'), [ t.stringLiteral(cssRelativeFileName), ]) ) ); } } const result = generate( ast, { fileName: sourceFileName, retainLines: false, compact: 'auto', concise: false, sourceMaps: true, sourceFileName, quotes: 'single', }, src ); return { js: result.code, css, cssFileName, ast: result.ast, map: result.map, }; }