function buildRefTemplate(name, refName, loop, key) { const attrs = [ t.jSXAttribute(t.jSXIdentifier('is'), t.stringLiteral(name)), t.jSXAttribute(t.jSXIdentifier('data'), t.stringLiteral(`{{...${refName ? `${loop ? '' : '$$'}${refName}` : '__data'}}}`)) ]; if (key) { attrs.push(key); } return t.jSXElement(t.jSXOpeningElement(t.jSXIdentifier('template'), attrs), t.jSXClosingElement(t.jSXIdentifier('template')), []); }
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); } });
Object.keys(staticAttributes).forEach(attr => { node.attributes.push( t.jSXAttribute( t.jSXIdentifier(attr), t.jSXExpressionContainer(t.nullLiteral()) ) ); });
function setJSXAttr(jsx, name, value, path) { const element = jsx.openingElement; if (!t.isJSXIdentifier(element.name)) { return; } if (element.name.name === 'Block' || element.name.name === 'block' || !path) { jsx.openingElement.attributes.push(t.jSXAttribute(t.jSXIdentifier(name), value)); } else { const block = buildBlockElement(); setJSXAttr(block, name, value); block.children = [jsx]; path.node = block; } }
function buildJSXAttr(name, value) { return t.jSXAttribute(t.jSXIdentifier(name), t.jSXExpressionContainer(value)); }
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; });
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 buildBlockElement() { return t.jSXElement(t.jSXOpeningElement(t.jSXIdentifier('block'), []), t.jSXClosingElement(t.jSXIdentifier('block')), []); }
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, }; }