extract.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  1. 'use strict';
  2. const getTemplate = require('./get-template');
  3. const loadSyntax = require('postcss-syntax/load-syntax');
  4. const { parse, types, traverse, loadOptions } = require('@babel/core');
  5. const isStyleSheetCreate = expectAdjacentSibling(['create']);
  6. const supports = {
  7. // import styled from '@emotion/styled'
  8. // import { styled } from 'glamor/styled'
  9. // import { styled } from "styletron-react";
  10. // import { styled } from 'linaria/react';
  11. // import { styled } from '@material-ui/styles'
  12. styled: true,
  13. // import { style } from "typestyle";
  14. style: true,
  15. // import { StyleSheet, css } from 'aphrodite';
  16. // import styled, { css } from 'astroturf';
  17. // import { css } from 'lit-css';
  18. // import { css } from 'glamor'
  19. // require('css-light').css({color: 'red'});
  20. // import { css } from 'linaria';
  21. css: true,
  22. // import { StyleSheet, css } from 'aphrodite';
  23. // import { AppRegistry, StyleSheet, Text, View } from 'react-native';
  24. StyleSheet: isStyleSheetCreate,
  25. // import styled, { css } from 'astroturf';
  26. astroturf: true,
  27. // require('csjs')`css`;
  28. csjs: true,
  29. // require('cssobj')({color: 'red'})
  30. cssobj: true,
  31. // require('electron-css')({color: 'red'})
  32. 'electron-css': true,
  33. // import styled from "react-emotion";
  34. 'react-emotion': true,
  35. // import styled from 'preact-emotion'
  36. 'preact-emotion': true,
  37. // https://github.com/streamich/freestyler
  38. freestyler: true,
  39. // https://github.com/paypal/glamorous
  40. glamorous: true,
  41. // https://github.com/irom-io/i-css
  42. // "i-css": (i, nameSpace) => nameSpace[i + 1] === "addStyles" && nameSpace[i + 2] === "wrapper",
  43. // https://github.com/j2css/j2c
  44. j2c: expectAdjacentSibling(['inline', 'sheet']),
  45. // var styles = StyleSheet.create({color: 'red'})
  46. 'react-inline': isStyleSheetCreate,
  47. 'react-style': isStyleSheetCreate,
  48. // import reactCSS from 'reactcss'
  49. reactcss: true,
  50. // const StyledButton = injectSheet(styles)(Button)
  51. 'react-jss': true,
  52. // import styled from 'styled-components';
  53. 'styled-components': true,
  54. // import {withStyle} from "styletron-react";
  55. 'styletron-react': expectAdjacentSibling(['withStyle']),
  56. styling: true,
  57. // const rule = superstyle({ color: 'blue' })
  58. superstyle: true,
  59. // import { makeStyles } from '@material-ui/styles'
  60. styles: expectAdjacentSibling(['makeStyles']),
  61. };
  62. const plugins = [
  63. 'jsx',
  64. 'typescript',
  65. 'objectRestSpread',
  66. ['decorators', { decoratorsBeforeExport: false }],
  67. 'classProperties',
  68. 'exportExtensions',
  69. 'asyncGenerators',
  70. 'functionBind',
  71. 'functionSent',
  72. 'dynamicImport',
  73. 'optionalCatchBinding',
  74. ];
  75. function expectAdjacentSibling(names) {
  76. return (i, nameSpace) => names.some((name) => nameSpace[i + 1] === name);
  77. }
  78. function loadBabelOpts(opts) {
  79. const filename = opts.from && opts.from.replace(/\?.*$/, '');
  80. opts = {
  81. filename,
  82. parserOpts: {
  83. plugins,
  84. sourceFilename: filename,
  85. sourceType: filename && /\.m[tj]sx?$/.test(filename) ? 'module' : 'unambiguous',
  86. allowImportExportEverywhere: true,
  87. allowAwaitOutsideFunction: true,
  88. allowReturnOutsideFunction: true,
  89. allowSuperOutsideMethod: true,
  90. },
  91. };
  92. let fileOpts;
  93. try {
  94. fileOpts =
  95. filename &&
  96. loadOptions({
  97. filename,
  98. });
  99. } catch (ex) {
  100. //
  101. }
  102. for (const key in fileOpts) {
  103. if (Array.isArray(fileOpts[key]) && !fileOpts[key].length) {
  104. continue;
  105. }
  106. opts[key] = fileOpts[key];
  107. if (Array.isArray(fileOpts[key]) && Array.isArray(opts.parserOpts[key])) {
  108. // combine arrays for plugins
  109. opts.parserOpts[key] = opts.parserOpts[key].concat(fileOpts[key]);
  110. } else {
  111. // because some options need to be passed to parser also
  112. opts.parserOpts[key] = fileOpts[key];
  113. }
  114. }
  115. return opts;
  116. }
  117. function literalParser(source, opts, styles) {
  118. let ast;
  119. try {
  120. ast = parse(source, loadBabelOpts(opts));
  121. } catch (ex) {
  122. // console.error(ex);
  123. return styles || [];
  124. }
  125. const specifiers = new Map();
  126. const variableDeclarator = new Map();
  127. const objLiteral = new Set();
  128. const tplLiteral = new Set();
  129. const tplCallee = new Set();
  130. const jobs = [];
  131. function addObjectJob(path) {
  132. jobs.push(() => {
  133. addObjectValue(path);
  134. });
  135. }
  136. function addObjectValue(path) {
  137. if (path.isIdentifier()) {
  138. const identifier = path.scope.getBindingIdentifier(path.node.name);
  139. if (identifier) {
  140. path = variableDeclarator.get(identifier);
  141. if (path) {
  142. variableDeclarator.delete(identifier);
  143. path.forEach(addObjectExpression);
  144. }
  145. }
  146. } else {
  147. addObjectExpression(path);
  148. }
  149. }
  150. function addObjectExpression(path) {
  151. if (path.isObjectExpression()) {
  152. path.get('properties').forEach((prop) => {
  153. if (prop.isSpreadElement()) {
  154. addObjectValue(prop.get('argument'));
  155. }
  156. });
  157. objLiteral.add(path.node);
  158. return path;
  159. }
  160. }
  161. function setSpecifier(id, nameSpace) {
  162. nameSpace.unshift(
  163. ...nameSpace
  164. .shift()
  165. .replace(/^\W+/, '')
  166. .split(/[/\\]+/g),
  167. );
  168. if (types.isIdentifier(id)) {
  169. specifiers.set(id.name, nameSpace);
  170. specifiers.set(id, nameSpace);
  171. } else if (types.isObjectPattern(id)) {
  172. id.properties.forEach((property) => {
  173. if (types.isObjectProperty(property)) {
  174. const key = property.key;
  175. nameSpace = nameSpace.concat(key.name || key.value);
  176. id = property.value;
  177. } else {
  178. id = property.argument;
  179. }
  180. setSpecifier(id, nameSpace);
  181. });
  182. } else if (types.isArrayPattern(id)) {
  183. id.elements.forEach((element, i) => {
  184. setSpecifier(element, nameSpace.concat(String(i)));
  185. });
  186. }
  187. }
  188. function getNameSpace(path, nameSpace) {
  189. let node = path.node;
  190. if (path.isIdentifier() || path.isJSXIdentifier()) {
  191. node = path.scope.getBindingIdentifier(node.name) || node;
  192. const specifier = specifiers.get(node) || specifiers.get(node.name);
  193. if (specifier) {
  194. nameSpace.unshift(...specifier);
  195. } else {
  196. nameSpace.unshift(node.name);
  197. }
  198. } else {
  199. ['name', 'property', 'object', 'callee'].forEach((prop) => {
  200. node[prop] && getNameSpace(path.get(prop), nameSpace);
  201. });
  202. }
  203. return nameSpace;
  204. }
  205. function isStylePath(path) {
  206. return getNameSpace(path, []).some(function (name, ...args) {
  207. const result =
  208. name &&
  209. ((Object.prototype.hasOwnProperty.call(supports, name) && supports[name]) ||
  210. (Object.prototype.hasOwnProperty.call(opts.syntax.config, name) &&
  211. opts.syntax.config[name]));
  212. switch (typeof result) {
  213. case 'function': {
  214. return result.apply(this, args);
  215. }
  216. case 'boolean': {
  217. return result;
  218. }
  219. default: {
  220. return undefined;
  221. }
  222. }
  223. });
  224. }
  225. const visitor = {
  226. ImportDeclaration: (path) => {
  227. const moduleId = path.node.source.value;
  228. path.node.specifiers.forEach((specifier) => {
  229. const nameSpace = [moduleId];
  230. if (specifier.imported) {
  231. nameSpace.push(specifier.imported.name);
  232. }
  233. setSpecifier(specifier.local, nameSpace);
  234. });
  235. },
  236. JSXAttribute: (path) => {
  237. if (/^(?:css|style)$/.test(path.node.name.name)) {
  238. addObjectJob(path.get('value.expression'));
  239. }
  240. },
  241. VariableDeclarator: (path) => {
  242. variableDeclarator.set(path.node.id, path.node.init ? [path.get('init')] : []);
  243. },
  244. AssignmentExpression: (path) => {
  245. if (types.isIdentifier(path.node.left) && types.isObjectExpression(path.node.right)) {
  246. const identifier = path.scope.getBindingIdentifier(path.node.left.name);
  247. const variable = variableDeclarator.get(identifier);
  248. const valuePath = path.get('right');
  249. if (variable) {
  250. variable.push(valuePath);
  251. } else {
  252. variableDeclarator.set(identifier, [valuePath]);
  253. }
  254. }
  255. },
  256. CallExpression: (path) => {
  257. const callee = path.node.callee;
  258. if (
  259. types.isIdentifier(callee, { name: 'require' }) &&
  260. !path.scope.getBindingIdentifier(callee.name)
  261. ) {
  262. path.node.arguments.filter(types.isStringLiteral).forEach((arg) => {
  263. const moduleId = arg.value;
  264. const nameSpace = [moduleId];
  265. let currPath = path;
  266. do {
  267. let id = currPath.parent.id;
  268. if (!id) {
  269. id = currPath.parent.left;
  270. if (id) {
  271. id = path.scope.getBindingIdentifier(id.name) || id;
  272. } else {
  273. if (types.isIdentifier(currPath.parent.property)) {
  274. nameSpace.push(currPath.parent.property.name);
  275. }
  276. currPath = currPath.parentPath;
  277. continue;
  278. }
  279. }
  280. setSpecifier(id, nameSpace);
  281. break;
  282. } while (currPath);
  283. });
  284. } else if (!tplCallee.has(callee) && isStylePath(path.get('callee'))) {
  285. path.get('arguments').forEach((arg) => {
  286. addObjectJob(arg.isFunction() ? arg.get('body') : arg);
  287. });
  288. }
  289. },
  290. TaggedTemplateExpression: (path) => {
  291. if (isStylePath(path.get('tag'))) {
  292. tplLiteral.add(path.node.quasi);
  293. if (path.node.tag.callee) {
  294. tplCallee.add(path.node.tag.callee);
  295. }
  296. }
  297. },
  298. };
  299. traverse(ast, visitor);
  300. jobs.forEach((job) => job());
  301. const objLiteralStyles = Array.from(objLiteral).map((endNode) => {
  302. const objectSyntax = require('./object-syntax');
  303. let startNode = endNode;
  304. if (startNode.leadingComments && startNode.leadingComments.length) {
  305. startNode = startNode.leadingComments[0];
  306. }
  307. let startIndex = startNode.start;
  308. const before = source.slice(startNode.start - startNode.loc.start.column, startNode.start);
  309. if (/^\s+$/.test(before)) {
  310. startIndex -= before.length;
  311. }
  312. return {
  313. startIndex,
  314. endIndex: endNode.end,
  315. skipConvert: true,
  316. content: source,
  317. opts: {
  318. node: endNode,
  319. },
  320. syntax: objectSyntax,
  321. lang: 'object-literal',
  322. };
  323. });
  324. const tplLiteralStyles = [];
  325. Array.from(tplLiteral).forEach((node) => {
  326. if (
  327. objLiteralStyles.some((style) => style.startIndex <= node.end && node.start < style.endIndex)
  328. ) {
  329. return;
  330. }
  331. const quasis = node.quasis.map((node) => ({
  332. start: node.start,
  333. end: node.end,
  334. }));
  335. const style = {
  336. startIndex: quasis[0].start,
  337. endIndex: quasis[quasis.length - 1].end,
  338. content: getTemplate(node, source),
  339. };
  340. if (node.expressions.length) {
  341. const expressions = node.expressions.map((node) => ({
  342. start: node.start,
  343. end: node.end,
  344. }));
  345. style.syntax = loadSyntax(opts, __dirname);
  346. style.lang = 'template-literal';
  347. style.opts = {
  348. quasis,
  349. expressions,
  350. };
  351. } else {
  352. style.lang = 'css';
  353. }
  354. let parent = null;
  355. let targetStyles = tplLiteralStyles;
  356. while (targetStyles) {
  357. const target = targetStyles.find(
  358. (targetStyle) =>
  359. targetStyle.opts &&
  360. targetStyle.opts.expressions.some(
  361. (expr) => expr.start <= style.startIndex && style.endIndex < expr.end,
  362. ),
  363. );
  364. if (target) {
  365. parent = target;
  366. targetStyles = target.opts.templateLiteralStyles;
  367. } else {
  368. break;
  369. }
  370. }
  371. if (parent) {
  372. const templateLiteralStyles =
  373. parent.opts.templateLiteralStyles || (parent.opts.templateLiteralStyles = []);
  374. templateLiteralStyles.push(style);
  375. } else {
  376. tplLiteralStyles.push(style);
  377. }
  378. });
  379. return (styles || []).concat(objLiteralStyles).concat(tplLiteralStyles);
  380. }
  381. module.exports = literalParser;