extract.js 12 KB

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