From 5fc32fb1ef5cbacd5e1c33ed9bef9187a03ff46b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=A5=BA?= Date: Thu, 26 Jul 2018 10:54:35 +0800 Subject: [PATCH] extract function (#23) --- extract.js | 333 ++++++++++++++++++-------------- test/fixtures/CssProp.jsx | 6 +- test/fixtures/glamorous.jsx | 17 +- test/fixtures/inline_styles.jsx | 23 ++- test/fixtures/react-emotion.jsx | 7 + test/react.js | 2 +- test/styled-components.js | 65 ++++++- 7 files changed, 285 insertions(+), 168 deletions(-) diff --git a/extract.js b/extract.js index 3110863..fd88141 100644 --- a/extract.js +++ b/extract.js @@ -1,53 +1,10 @@ "use strict"; const traverse = require("@babel/traverse").default; -const types = require("@babel/types"); +const t = require("@babel/types"); const parse = require("@babel/parser").parse; const getTemplate = require("./get-template"); const loadSyntax = require("postcss-syntax/load-syntax"); -const extractDeclarations = (path) => { - let declarations = []; - - path.traverse({ - ObjectExpression (p) { - if (!p.findParent(parent => parent.isObjectProperty())) { - p.node.properties.forEach((prop) => { - declarations = declarations.concat([prop]); - }); - } - }, - }); - - return declarations; -}; - -const isStyleModule = (node, references) => { - const nameSpace = []; - do { - if (node.name) { - nameSpace.unshift(node.name); - } else if (node.property && node.property.name) { - nameSpace.unshift(node.property.name); - } - - node = node.object || node.callee; - } while (node); - - if (nameSpace.length) { - if (references[nameSpace[0]]) { - nameSpace.unshift.apply(nameSpace, references[nameSpace.shift()]); - const modeId = nameSpace[0]; - const prefix = partSport[modeId]; - if ((prefix && prefix.every((name, i) => name === nameSpace[i + 1])) || supports[modeId]) { - return nameSpace; - } - } else if (/^(?:styled|StyleSheet)$/.test(nameSpace[0])) { - return nameSpace; - } - } - return false; -}; - const partSport = { // https://github.com/Khan/aphrodite aphrodite: [ @@ -91,6 +48,20 @@ const supports = { typestyle: true, }; +const plugins = [ + "jsx", + "typescript", + "objectRestSpread", + "decorators", + "classProperties", + "exportExtensions", + "asyncGenerators", + "functionBind", + "functionSent", + "dynamicImport", + "optionalCatchBinding", +]; + function getSourceType (filename) { if (filename && /\.m[tj]sx?$/.test(filename)) { return "module"; @@ -104,20 +75,7 @@ function getSourceType (filename) { } } -function getOptions (opts, attribute) { - const plugins = [ - "jsx", - "typescript", - "objectRestSpread", - "decorators", - "classProperties", - "exportExtensions", - "asyncGenerators", - "functionBind", - "functionSent", - "dynamicImport", - "optionalCatchBinding", - ]; +function getOptions (opts) { const filename = opts.from && opts.from.replace(/\?.*$/, ""); return { @@ -132,121 +90,196 @@ function literalParser (source, opts, styles) { try { ast = parse(source, getOptions(opts)); } catch (ex) { + // console.error(ex); return styles || []; } - const references = {}; - const objs = {}; - let objLiteral = []; - let tplLiteral = []; + const specifiers = new Map(); + const variableDeclarator = new Map(); + let objLiteral = new Set(); + let tplLiteral = new Set(); + const jobs = []; + + function addObjectJob (path) { + jobs.push(() => { + addObjectValue(path); + }); + } + + function addObjectValue (path) { + if (path.isIdentifier()) { + const identifier = path.scope.getBindingIdentifier(path.node.name); + if (identifier) { + path = variableDeclarator.get(identifier); + if (path) { + variableDeclarator.delete(identifier); + path.forEach(addObjectExpression); + } + } + } else { + addObjectExpression(path); + } + } - function getObjectExpression (path) { - let objectExpression; + function addObjectExpression (path) { if (path.isObjectExpression()) { - objectExpression = path; - } else if (path.isIdentifier()) { - const identifierName = path.node.name; - if (objs[identifierName]) { - objectExpression = objs[identifierName]; - delete objs[identifierName]; + path.get("properties").forEach(prop => { + if (prop.isSpreadElement()) { + addObjectValue(prop.get("argument")); + } + }); + objLiteral.add(path.node); + return path; + } + } + + function setSpecifier (id, nameSpace) { + if (t.isIdentifier(id)) { + specifiers.set(id.name, nameSpace); + specifiers.set(id, nameSpace); + } else if (t.isObjectPattern(id)) { + id.properties.forEach(property => { + if (t.isObjectProperty(property)) { + const key = property.key; + nameSpace = nameSpace.concat(key.name || key.value); + id = property.value; + } else { + id = property.argument; + } + setSpecifier(id, nameSpace); + }); + } else if (t.isArrayPattern(id)) { + id.elements.forEach((element, i) => { + setSpecifier(element, nameSpace.concat(String(i))); + }); + } + } + + function getNameSpace (path, nameSpace) { + let node = path.node; + if (path.isIdentifier() || path.isJSXIdentifier()) { + node = path.scope.getBindingIdentifier(node.name) || node; + const specifier = specifiers.get(node) || specifiers.get(node.name); + if (specifier) { + nameSpace.unshift.apply(nameSpace, specifier); + } else { + nameSpace.unshift(node.name); + } + } else { + if (node.name) { + getNameSpace(path.get("name"), nameSpace); + } else if (node.property) { + getNameSpace(path.get("property"), nameSpace); + } + if (node.object) { + getNameSpace(path.get("object"), nameSpace); + } else if (node.callee) { + getNameSpace(path.get("callee"), nameSpace); } } - return objectExpression; + + return nameSpace; } - function enter (path) { - if (path.isImportDeclaration()) { - const moduleId = path.node.source.value; - if ((moduleId in supports) || (moduleId in partSport)) { - path.node.specifiers.forEach(specifier => { - const localName = specifier.local.name; - references[localName] = [ - moduleId, - ]; - if (specifier.imported) { - references[localName].push(specifier.imported.name); - } - }); + function isStylePath (path) { + const nameSpace = getNameSpace(path, []).filter(Boolean); + if (nameSpace.length) { + if (/^(?:styled|StyleSheet)$/.test(nameSpace[0]) || supports[nameSpace[0]]) { + return nameSpace; } - } else if (path.isJSXAttribute()) { + + const prefix = partSport[nameSpace.shift()]; + + if (prefix && nameSpace.length >= prefix.length && prefix.every((name, i) => name === nameSpace[i])) { + return nameSpace; + } + } + + return false; + } + + const visitor = { + ImportDeclaration: (path) => { + const moduleId = path.node.source.value; + path.node.specifiers.forEach(specifier => { + const nameSpace = [moduleId]; + if (specifier.imported) { + nameSpace.push(specifier.imported.name); + } + setSpecifier(specifier.local, nameSpace); + }); + }, + JSXAttribute: (path) => { const attrName = path.node.name.name; if (attrName === "css") { - const element = path.findParent(p => p.isJSXOpeningElement()); - if (!element || !isStyleModule(element.node.name, references)) { + const elePath = path.findParent(p => p.isJSXOpeningElement()); + if (!isStylePath(elePath)) { return; } } else if (attrName !== "style") { return; } - if ((path = getObjectExpression(path.get("value.expression")))) { - objLiteral.push(path); - } - } else if (path.isObjectExpression()) { - if ( - path.parentPath.isVariableDeclarator() && - path.parent.id.type === "Identifier" - ) { - objs[path.parent.id.name] = path; - } - } else if (path.isTemplateLiteral()) { - if (path.parentPath.isTaggedTemplateExpression() && isStyleModule(path.parent.tag, references)) { - tplLiteral.push(path); + addObjectJob(path.get("value.expression")); + }, + VariableDeclarator: (path) => { + variableDeclarator.set(path.node.id, path.node.init ? [path.get("init")] : []); + }, + AssignmentExpression: (path) => { + if (t.isIdentifier(path.node.left) && t.isObjectExpression(path.node.right)) { + const identifier = path.scope.getBindingIdentifier(path.node.left.name); + const variable = variableDeclarator.get(identifier); + const valuePath = path.get("right"); + if (variable) { + variable.push(valuePath); + } else { + variableDeclarator.set(identifier, [valuePath]); + } } - } else if (path.isCallExpression()) { + }, + CallExpression: (path) => { const callee = path.node.callee; - if (callee.type === "Identifier" && callee.name === "require") { - const args = path.get("arguments"); - if (args && args.length && args[0].isStringLiteral()) { - const moduleId = args[0].container[0].value; - if ((moduleId in supports) || (moduleId in partSport)) { - const nameSpace = [moduleId]; - do { - if (path.parent.id) { - references[path.parent.id.name] = nameSpace; - break; - } else if (path.parent.property) { - nameSpace.push(path.parent.property.name); + if (t.isIdentifier(callee, { name: "require" }) && !path.scope.getBindingIdentifier(callee.name)) { + path.node.arguments.filter(t.isStringLiteral).forEach(arg => { + const moduleId = arg.value; + const nameSpace = [moduleId]; + let currPath = path; + do { + let id = currPath.parent.id; + if (!id) { + id = currPath.parent.left; + if (id) { + id = path.scope.getBindingIdentifier(id.name) || id; + } else { + if (t.isIdentifier(currPath.parent.property)) { + nameSpace.push(currPath.parent.property.name); + } + currPath = currPath.parentPath; + continue; } - path = path.parentPath; - } while (path); - } - } - } else if (isStyleModule(callee, references)) { + }; + setSpecifier(id, nameSpace); + break; + } while (currPath); + }); + } else if (isStylePath(path.get("callee"))) { path.get("arguments").forEach((arg) => { - if (arg.isFunction()) { - arg = arg.get("body"); - if (arg.isObjectExpression()) { - objLiteral.push(arg); - } else { - const rule = Object.assign( - {}, - types.objectExpression(extractDeclarations(arg)), - { loc: path.node.loc } - ); - path.replaceWith(rule); - objLiteral.push(path); - } - } else if ((arg = getObjectExpression(arg))) { - objLiteral.push(arg); - } + addObjectJob(arg.isFunction() ? arg.get("body") : arg); }); } - } - } - traverse(ast, { - enter (path) { - try { - enter(path); - } catch (ex) { - console.error(ex); + }, + TaggedTemplateExpression: (path) => { + if (isStylePath(path.get("tag"))) { + tplLiteral.add(path.node.quasi); } }, - }); + }; + + traverse(ast, visitor); + jobs.forEach(job => job()); - objLiteral = objLiteral.map(path => { + objLiteral = Array.from(objLiteral).map(endNode => { const objectSyntax = require("./object-syntax"); - const endNode = path.node; const syntax = objectSyntax(endNode); let startNode = endNode; if (startNode.leadingComments && startNode.leadingComments.length) { @@ -262,13 +295,13 @@ function literalParser (source, opts, styles) { }; }); - tplLiteral = tplLiteral.filter(path => ( + tplLiteral = Array.from(tplLiteral).filter(node => ( objLiteral.every(style => ( - path.node.start > style.endIndex || path.node.end < style.startIndex + node.start > style.endIndex || node.end < style.startIndex )) - )).map(path => { - const quasis = path.node.quasis; - const value = getTemplate(path.node, source); + )).map(node => { + const quasis = node.quasis; + const value = getTemplate(node, source); if (value.length === 1 && !value[0].trim()) { return; diff --git a/test/fixtures/CssProp.jsx b/test/fixtures/CssProp.jsx index aaab76e..caf1ea8 100644 --- a/test/fixtures/CssProp.jsx +++ b/test/fixtures/CssProp.jsx @@ -21,4 +21,8 @@ const App = props => ( ); -export default App; +export default { + React, + Div, + App, +}; diff --git a/test/fixtures/glamorous.jsx b/test/fixtures/glamorous.jsx index 20110c0..520d683 100644 --- a/test/fixtures/glamorous.jsx +++ b/test/fixtures/glamorous.jsx @@ -1,4 +1,3 @@ -import React from "react"; import glm from "glamorous"; const minWidth = 700; @@ -7,8 +6,8 @@ const Component1 = glm.a( /* start */ { // stylelint-disable-next-line - "unknownProperty": "1.8em", // must not trigger any warnings - unknownProperty: "1.8em", // must not trigger any warnings + "unknownProperty1": "1.8em", // must not trigger any warnings + unknownProperty2: "1.8em", // must not trigger any warnings [`unknownPropertyaa${a}`]: "1.8em", // must not trigger any warnings ["unknownProperty" + 1 + "a"]: "1.8em", // must not trigger any warnings display: "inline-block", @@ -49,10 +48,8 @@ const Component3 = glm.div({ ...Component2, }); -export default () => ( -
- - - -
-); +export default { + Component1, + Component2, + Component3, +}; diff --git a/test/fixtures/inline_styles.jsx b/test/fixtures/inline_styles.jsx index 3ec80ed..858440d 100644 --- a/test/fixtures/inline_styles.jsx +++ b/test/fixtures/inline_styles.jsx @@ -1,12 +1,25 @@ -const divStyle = { - color: "blue", - backgroundImage: "url(" + imgUrl + ")", +/* eslint comma-dangle: ["error", "never"] */ +/* global notExist */ +const imgUrl = "https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png"; +const baseStyle = { + color: "blue" }; - +let divStyle; require(); function HelloWorldComponent () { + divStyle = { + backgroundImage: `url(${imgUrl})`, + ...baseStyle + }; return
- Hello World! + Hello World! + Hello World!
; } + +divStyle = { + backgroundImage: `url(${imgUrl})` +}; + +module.exports = HelloWorldComponent; diff --git a/test/fixtures/react-emotion.jsx b/test/fixtures/react-emotion.jsx index e03e205..9af6533 100644 --- a/test/fixtures/react-emotion.jsx +++ b/test/fixtures/react-emotion.jsx @@ -1,3 +1,5 @@ +/* global render */ + import styled, { css } from "react-emotion"; const SomeComponent = styled("div")` display: flex; @@ -23,3 +25,8 @@ const myStyle = css` color: rebeccapurple; `; app.classList.add(myStyle); + +export default { + SomeComponent, + AnotherComponent, +}; diff --git a/test/react.js b/test/react.js index a7d2dc5..504d6b6 100644 --- a/test/react.js +++ b/test/react.js @@ -15,6 +15,6 @@ describe("react", () => { code = code.toString(); expect(document.toString(syntax)).to.equal(code); - expect(document.nodes).to.lengthOf(1); + expect(document.nodes).to.lengthOf(3); }); }); diff --git a/test/styled-components.js b/test/styled-components.js index 22f3171..d66d318 100644 --- a/test/styled-components.js +++ b/test/styled-components.js @@ -33,7 +33,7 @@ describe("styled-components", () => { it("empty template literal", () => { const code = [ "function test() {", - " console.log(`debug`)", + " alert`debug`", " return ``;", "}", "", @@ -169,4 +169,67 @@ describe("styled-components", () => { expect(document.nodes).to.have.lengthOf(1); expect(document.first.first).to.haveOwnProperty("prop", "margin-${/* sc-custom 'left' */ rtlSwitch}"); }); + + it("lazy assignment", () => { + const code = [ + "let myDiv;", + "myDiv = require(\"styled-components\").div;", + "myDiv`a{}`;", + ].join("\n"); + const document = syntax.parse(code, { + from: "lazy_assign.js", + }); + expect(document.toString()).to.equal(code); + expect(document.source).to.haveOwnProperty("lang", "jsx"); + expect(document.nodes).to.have.lengthOf(1); + }); + + it("lazy assignment without init", () => { + const code = [ + "myDiv = require(\"styled-components\").div;", + "myDiv`a{}`;", + ].join("\n"); + const document = syntax.parse(code, { + from: "lazy_assign_no_init.js", + }); + expect(document.toString()).to.equal(code); + expect(document.source).to.haveOwnProperty("lang", "jsx"); + expect(document.nodes).to.have.lengthOf(1); + }); + + it("array destructuring assignment", () => { + const code = [ + "const [", + "\tstyledDiv,", + "\t...c", + "] = require(\"styled-components\");", + "styledDiv`a{}`;", + ].join("\n"); + const document = syntax.parse(code, { + from: "arr_destructuring.js", + }); + expect(document.toString()).to.equal(code); + expect(document.source).to.haveOwnProperty("lang", "jsx"); + expect(document.nodes).to.have.lengthOf(1); + }); + + it("object destructuring assignment", () => { + const code = [ + "const {", + "\t// commit", + "\t['div']: styledDiv,", + "\ta,", + "\t...styled", + "} = require(\"styled-components\");", + "styledDiv`a{}`;", + "styled.div`a{}`;", + "a`a{}`;", + ].join("\n"); + const document = syntax.parse(code, { + from: "obj_destructuring.js", + }); + expect(document.toString()).to.equal(code); + expect(document.source).to.haveOwnProperty("lang", "jsx"); + expect(document.nodes).to.have.lengthOf(3); + }); });