UNPKG

30.4 kBJavaScriptView Raw
1'use strict';
2
3const invariant = require('invariant');
4const path = require('path');
5const vm = require('vm');
6const util = require('util');
7
8const {
9 _getStyleKeysForProps: getStyleKeysForProps,
10 _defaults: jsxstyleDefaults,
11} = require('jsxstyle');
12
13const canEvaluate = require('./canEvaluate');
14const canEvaluateObject = require('./canEvaluateObject');
15const extractStaticTernaries = require('./extractStaticTernaries');
16const getPropValueFromAttributes = require('./getPropValueFromAttributes');
17const getStaticBindingsForScope = require('./getStaticBindingsForScope');
18const getStylesByClassName = require('../getStylesByClassName');
19const cssRelativeURL = require('../cssRelativeURL');
20
21const parse = require('./parse');
22const traverse = require('babel-traverse').default;
23const generate = require('babel-generator').default;
24const t = require('babel-types');
25
26// these props will be passed through as-is
27const UNTOUCHED_PROPS = {
28 ref: true,
29 key: true,
30 style: true,
31};
32
33// these props cannot appear in the props prop (so meta)
34const ALL_SPECIAL_PROPS = Object.assign(
35 {
36 component: true,
37 className: true,
38 },
39 UNTOUCHED_PROPS
40);
41
42const ucRegex = /([A-Z])/g;
43
44function extractStyles({
45 src,
46 styleGroups,
47 namedStyleGroups,
48 sourceFileName,
49 rootFileName,
50 whitelistedModules,
51 cacheObject,
52 parserPlugins,
53 addCSSRequire,
54 errorCallback,
55 extremelyLiteMode,
56}) {
57 invariant(typeof src === 'string', '`src` must be a string of javascript');
58
59 invariant(
60 typeof sourceFileName === 'string' && path.isAbsolute(sourceFileName),
61 '`sourceFileName` must be an absolute path to a .js file'
62 );
63
64 invariant(
65 typeof cacheObject === 'object' && cacheObject !== null,
66 '`cacheObject` must be an object'
67 );
68
69 if (typeof styleGroups !== 'undefined') {
70 invariant(
71 Array.isArray(styleGroups),
72 '`styleGroups` must be an array of style prop objects'
73 );
74 }
75
76 if (typeof namedStyleGroups !== 'undefined') {
77 invariant(
78 typeof namedStyleGroups === 'object' && namedStyleGroups !== null,
79 '`namedStyleGroups` must be an object of style prop objects keyed by className'
80 );
81 }
82
83 if (typeof whitelistedModules !== 'undefined') {
84 invariant(
85 Array.isArray(whitelistedModules),
86 '`whitelistedModules` must be an array of paths to modules that are OK to require'
87 );
88 }
89
90 if (typeof errorCallback !== 'undefined') {
91 invariant(
92 typeof errorCallback === 'function',
93 '`errorCallback` is expected to be a function'
94 );
95 } else {
96 errorCallback = console.warn;
97 }
98
99 if (typeof addCSSRequire === 'undefined') {
100 addCSSRequire = true;
101 }
102
103 const sourceDir = path.dirname(sourceFileName);
104 const rootDir = rootFileName ? path.dirname(rootFileName) : sourceDir;
105
106 // Using a map for (officially supported) guaranteed insertion order
107 const cssMap = new Map();
108
109 const ast = parse(src, parserPlugins);
110
111 let jsxstyleSrc;
112 let validComponents;
113
114 if (typeof extremelyLiteMode === 'string') {
115 jsxstyleSrc =
116 extremelyLiteMode === 'react'
117 ? 'jsxstyle/lite'
118 : `jsxstyle/lite/${extremelyLiteMode}`;
119 validComponents = validComponents || {};
120 for (const key in jsxstyleDefaults) {
121 const dashCaseName = key.replace(ucRegex, '-$1').toLowerCase().slice(1);
122 validComponents[dashCaseName] = key;
123 }
124 } else {
125 ast.program.body = ast.program.body.filter(item => {
126 if (t.isVariableDeclaration(item)) {
127 for (let idx = -1, len = item.declarations.length; ++idx < len; ) {
128 const dec = item.declarations[idx];
129
130 if (
131 // var ...
132 !t.isVariableDeclarator(dec) ||
133 // var {...}
134 !t.isObjectPattern(dec.id) ||
135 // var {x} = require(...)
136 !t.isCallExpression(dec.init) ||
137 !t.isIdentifier(dec.init.callee) ||
138 dec.init.callee.name !== 'require' ||
139 // var {x} = require('one-thing')
140 dec.init.arguments.length !== 1 ||
141 !t.isStringLiteral(dec.init.arguments[0])
142 ) {
143 continue;
144 }
145
146 // ignore spelunkers
147 if (dec.init.arguments[0].value.startsWith('jsxstyle/lib/')) {
148 continue;
149 }
150
151 // var {x} = require('jsxstyle')
152 // or
153 // var {x} = require('jsxstyle/thing')
154 if (
155 dec.init.arguments[0].value !== 'jsxstyle' &&
156 !dec.init.arguments[0].value.startsWith('jsxstyle/')
157 ) {
158 continue;
159 }
160
161 if (jsxstyleSrc) {
162 invariant(
163 jsxstyleSrc === dec.init.arguments[0].value,
164 'Expected duplicate `require` to be from "%s", received "%s"',
165 jsxstyleSrc,
166 dec.init.arguments[0].value
167 );
168 }
169
170 for (let idx = -1, len = dec.id.properties.length; ++idx < len; ) {
171 const prop = dec.id.properties[idx];
172 if (
173 !t.isObjectProperty(prop) ||
174 !t.isIdentifier(prop.key) ||
175 !t.isIdentifier(prop.value)
176 ) {
177 continue;
178 }
179
180 // only add uppercase identifiers to validComponents
181 if (
182 prop.key.name[0] !== prop.key.name[0].toUpperCase() ||
183 prop.value.name[0] !== prop.value.name[0].toUpperCase()
184 ) {
185 continue;
186 }
187
188 // map imported name to source component name
189 validComponents = validComponents || {};
190 validComponents[prop.value.name] = prop.key.name;
191 }
192
193 jsxstyleSrc = dec.init.arguments[0].value;
194
195 if (
196 jsxstyleSrc === 'jsxstyle/lite' ||
197 jsxstyleSrc.startsWith('jsxstyle/lite/')
198 ) {
199 return false;
200 }
201 }
202 } else if (t.isImportDeclaration(item)) {
203 // omfg everyone please just use import syntax
204
205 // ignore spelunkers
206 if (item.source.value.startsWith('jsxstyle/lib/')) {
207 return true;
208 }
209
210 // not imported from jsxstyle? byeeee
211 if (
212 !t.isStringLiteral(item.source) ||
213 (item.source.value !== 'jsxstyle' &&
214 !item.source.value.startsWith('jsxstyle/'))
215 ) {
216 return true;
217 }
218
219 if (jsxstyleSrc) {
220 invariant(
221 jsxstyleSrc === item.source.value,
222 'Expected duplicate `import` to be from "%s", received "%s"',
223 jsxstyleSrc,
224 item.source.value
225 );
226 }
227
228 jsxstyleSrc = item.source.value;
229
230 for (let idx = -1, len = item.specifiers.length; ++idx < len; ) {
231 const specifier = item.specifiers[idx];
232 if (
233 !t.isImportSpecifier(specifier) ||
234 !t.isIdentifier(specifier.imported) ||
235 !t.isIdentifier(specifier.local)
236 ) {
237 continue;
238 }
239
240 if (
241 specifier.imported.name[0] !==
242 specifier.imported.name[0].toUpperCase() ||
243 specifier.local.name[0] !== specifier.local.name[0].toUpperCase()
244 ) {
245 continue;
246 }
247
248 validComponents = validComponents || {};
249 validComponents[specifier.local.name] = specifier.imported.name;
250 }
251
252 if (
253 jsxstyleSrc === 'jsxstyle/lite' ||
254 jsxstyleSrc.startsWith('jsxstyle/lite/')
255 ) {
256 return false;
257 }
258 }
259 return true;
260 });
261
262 // jsxstyle isn't included anywhere, so let's bail
263 if (!jsxstyleSrc || !validComponents) {
264 return {
265 js: src,
266 css: '',
267 cssFileName: null,
268 ast,
269 map: null,
270 };
271 }
272 }
273
274 // class or className?
275 const classPropName =
276 jsxstyleSrc === 'jsxstyle/preact' || jsxstyleSrc === 'jsxstyle/lite/preact'
277 ? 'class'
278 : 'className';
279
280 const removeAllTrace =
281 jsxstyleSrc === 'jsxstyle/lite' || jsxstyleSrc.startsWith('jsxstyle/lite/');
282
283 const boxComponentName = 'Jsxstyle$Box';
284 if (!removeAllTrace) {
285 // Add Box require to the top of the document
286 // var Jsxstyle$Box = require('jsxstyle').Box;
287 ast.program.body.unshift(
288 t.variableDeclaration('var', [
289 t.variableDeclarator(
290 t.identifier(boxComponentName),
291 t.memberExpression(
292 t.callExpression(t.identifier('require'), [
293 t.stringLiteral(jsxstyleSrc),
294 ]),
295 t.identifier('Box')
296 )
297 ),
298 ])
299 );
300 }
301
302 const traverseOptions = {
303 JSXElement(path) {
304 const node = path.node.openingElement;
305
306 if (
307 // skip non-identifier opening elements (member expressions, etc.)
308 !t.isJSXIdentifier(node.name) ||
309 // skip non-jsxstyle components
310 !validComponents.hasOwnProperty(node.name.name)
311 ) {
312 return;
313 }
314
315 // Remember the source component
316 const originalNodeName = node.name.name;
317 const src = validComponents[originalNodeName];
318
319 if (!removeAllTrace) {
320 node.name.name = boxComponentName;
321 }
322
323 const defaultProps = jsxstyleDefaults[src];
324 invariant(defaultProps, `jsxstyle component <${src} /> does not exist!`);
325
326 const propKeys = Object.keys(defaultProps);
327 // looping backwards because of unshift
328 for (let idx = propKeys.length; --idx >= 0; ) {
329 const prop = propKeys[idx];
330 const value = defaultProps[prop];
331 if (value == null || value === '') {
332 continue;
333 }
334
335 let valueEx;
336 if (typeof value === 'number') {
337 valueEx = t.jSXExpressionContainer(t.numericLiteral(value));
338 } else if (typeof value === 'string') {
339 valueEx = t.stringLiteral(value);
340 } else {
341 continue;
342 }
343
344 node.attributes.unshift(t.jSXAttribute(t.jSXIdentifier(prop), valueEx));
345 }
346
347 // Generate scope object at this level
348 const staticNamespace = getStaticBindingsForScope(
349 path.scope,
350 whitelistedModules,
351 sourceFileName
352 );
353 const evalContext = vm.createContext(staticNamespace);
354
355 let lastSpreadIndex = null;
356 const flattenedAttributes = [];
357 node.attributes.forEach(attr => {
358 if (t.isJSXSpreadAttribute(attr)) {
359 if (canEvaluate(staticNamespace, attr.argument)) {
360 const spreadValue = vm.runInContext(
361 generate(attr.argument).code,
362 evalContext
363 );
364
365 if (typeof spreadValue !== 'object' || spreadValue === null) {
366 lastSpreadIndex = flattenedAttributes.push(attr) - 1;
367 } else {
368 for (const k in spreadValue) {
369 const value = spreadValue[k];
370
371 if (typeof value === 'number') {
372 flattenedAttributes.push(
373 t.jSXAttribute(
374 t.jSXIdentifier(k),
375 t.jSXExpressionContainer(t.numericLiteral(value))
376 )
377 );
378 } else if (value === null) {
379 // why would you ever do this
380 flattenedAttributes.push(
381 t.jSXAttribute(
382 t.jSXIdentifier(k),
383 t.jSXExpressionContainer(t.nullLiteral())
384 )
385 );
386 } else {
387 // toString anything else
388 // TODO: is this a bad idea
389 flattenedAttributes.push(
390 t.jSXAttribute(
391 t.jSXIdentifier(k),
392 t.jSXExpressionContainer(t.stringLiteral('' + value))
393 )
394 );
395 }
396 }
397 }
398 } else {
399 lastSpreadIndex = flattenedAttributes.push(attr) - 1;
400 }
401 } else {
402 flattenedAttributes.push(attr);
403 }
404 });
405
406 node.attributes = flattenedAttributes;
407
408 let propsAttributes;
409 const staticAttributes = {};
410 let inlinePropCount = 0;
411
412 const staticTernaries = [];
413
414 node.attributes = node.attributes.filter((attribute, idx) => {
415 if (
416 // keep the weirdos
417 !attribute.name ||
418 !attribute.name.name ||
419 // haven't hit the last spread operator
420 idx < lastSpreadIndex
421 ) {
422 inlinePropCount++;
423 return true;
424 }
425
426 const name = attribute.name.name;
427 const value = t.isJSXExpressionContainer(attribute.value)
428 ? attribute.value.expression
429 : attribute.value;
430
431 // if one or more spread operators are present and we haven't hit the last one yet, the prop stays inline
432 if (lastSpreadIndex !== null && idx <= lastSpreadIndex) {
433 inlinePropCount++;
434 return true;
435 }
436
437 // pass ref, key, and style props through untouched
438 if (UNTOUCHED_PROPS.hasOwnProperty(name)) {
439 return true;
440 }
441
442 // className prop will be handled below
443 if (name === classPropName) {
444 return true;
445 }
446
447 // validate component prop
448 if (name === 'component') {
449 if (t.isLiteral(value) && typeof value.value === 'string') {
450 const char1 = value.value[0];
451 // component="article"
452 if (char1 === char1.toUpperCase()) {
453 // an uppercase string with be turned into a component, which is not what we want.
454 // TODO: look into transforming to React.createElement.
455 // main downside is that further transformations that rely on JSX won't work.
456 inlinePropCount++;
457 }
458 } else if (t.isIdentifier(value)) {
459 const char1 = value.name[0];
460 // component={Avatar}
461 if (char1 === char1.toLowerCase()) {
462 // a lowercase identifier will be transformed to a DOM element. that's not good.
463 inlinePropCount++;
464 }
465 } else if (t.isMemberExpression(value)) {
466 // component={variable.prop}
467 } else {
468 // TODO: extract more complex expressions out as separate var
469 errorCallback(
470 '`component` prop value was not handled by extractStyles: %s',
471 generate(value).code
472 );
473 inlinePropCount++;
474 }
475 return true;
476 }
477
478 // pass key and style props through untouched
479 if (UNTOUCHED_PROPS.hasOwnProperty(name)) {
480 return true;
481 }
482
483 if (name === 'props') {
484 if (!value) {
485 errorCallback('`props` prop does not have a value');
486 inlinePropCount++;
487 return true;
488 }
489
490 if (t.isObjectExpression(value)) {
491 let errorCount = 0;
492 const attributes = [];
493
494 for (const k in value.properties) {
495 const propObj = value.properties[k];
496
497 if (t.isObjectProperty(propObj)) {
498 let key;
499
500 if (t.isIdentifier(propObj.key)) {
501 key = propObj.key.name;
502 } else if (t.isStringLiteral(propObj.key)) {
503 // starts with a-z or _ followed by a-z, -, or _
504 if (/^\w[\w-]+$/.test(propObj.key.value)) {
505 key = propObj.key.value;
506 } else {
507 errorCallback(
508 '`props` prop contains an invalid key: `%s`',
509 propObj.key.value
510 );
511 errorCount++;
512 continue;
513 }
514 } else {
515 errorCallback(
516 'unhandled object property key type: `%s`',
517 propObj.type
518 );
519 errorCount++;
520 }
521
522 if (ALL_SPECIAL_PROPS.hasOwnProperty(key)) {
523 errorCallback(
524 '`props` prop cannot contain `%s` as it is used by jsxstyle and will be overwritten.',
525 key
526 );
527 errorCount++;
528 continue;
529 }
530
531 if (t.isStringLiteral(propObj.value)) {
532 // convert literal value back to literal to ensure it has double quotes (siiiigh)
533 attributes.push(
534 t.jSXAttribute(
535 t.jSXIdentifier(key),
536 t.stringLiteral(propObj.value.value)
537 )
538 );
539 } else {
540 // wrap everything else in a JSXExpressionContainer
541 attributes.push(
542 t.jSXAttribute(
543 t.jSXIdentifier(key),
544 t.jSXExpressionContainer(propObj.value)
545 )
546 );
547 }
548 } else if (t.isSpreadProperty(propObj)) {
549 attributes.push(t.jSXSpreadAttribute(propObj.argument));
550 } else {
551 errorCallback(
552 'unhandled object property value type: `%s`',
553 propObj.type
554 );
555 errorCount++;
556 }
557 }
558
559 if (errorCount > 0) {
560 inlinePropCount++;
561 } else {
562 propsAttributes = attributes;
563 }
564
565 return true;
566 }
567
568 if (
569 // if it's not an object, spread it
570 // props={wow()}
571 t.isCallExpression(value) ||
572 // props={wow.cool}
573 t.isMemberExpression(value) ||
574 // props={wow}
575 t.isIdentifier(value)
576 ) {
577 propsAttributes = [t.jSXSpreadAttribute(value)];
578 return true;
579 }
580
581 // if props prop is weird-looking, leave it and warn
582 errorCallback('props prop is an unhandled type: `%s`', value.type);
583 inlinePropCount++;
584 return true;
585 }
586
587 if (name === 'mediaQueries') {
588 if (canEvaluateObject(staticNamespace, value)) {
589 staticAttributes[name] = vm.runInContext(
590 // parens so V8 doesn't parse the object like a block
591 '(' + generate(value).code + ')',
592 evalContext
593 );
594 } else if (canEvaluate(staticNamespace, value)) {
595 staticAttributes[name] = vm.runInContext(
596 generate(value).code,
597 evalContext
598 );
599 } else {
600 inlinePropCount++;
601 return true;
602 }
603 return false;
604 }
605
606 // if value can be evaluated, extract it and filter it out
607 if (canEvaluate(staticNamespace, value)) {
608 staticAttributes[name] = vm.runInContext(
609 generate(value).code,
610 evalContext
611 );
612 return false;
613 }
614
615 if (t.isConditionalExpression(value)) {
616 // if both sides of the ternary can be evaluated, extract them
617 if (
618 canEvaluate(staticNamespace, value.consequent) &&
619 canEvaluate(staticNamespace, value.alternate)
620 ) {
621 staticTernaries.push({ name, ternary: value });
622 // mark the prop as extracted
623 staticAttributes[name] = null;
624 return false;
625 }
626 } else if (t.isLogicalExpression(value)) {
627 // convert a simple logical expression to a ternary with a null alternate
628 if (
629 value.operator === '&&' &&
630 canEvaluate(staticNamespace, value.right)
631 ) {
632 staticTernaries.push({
633 name,
634 ternary: {
635 test: value.left,
636 consequent: value.right,
637 alternate: t.nullLiteral(),
638 },
639 });
640 staticAttributes[name] = null;
641 return false;
642 }
643 }
644
645 if (removeAllTrace) {
646 errorCallback(
647 'jsxstyle-loader encountered a dynamic prop (`%s`) on a jsxstyle ' +
648 'component that was imported from `%s`. If you would like to pass ' +
649 'dynamic styles to this component, specify them in the `style` prop.',
650 generate(attribute).code,
651 jsxstyleSrc
652 );
653 return false;
654 }
655
656 // if we've made it this far, the prop stays inline
657 inlinePropCount++;
658 return true;
659 });
660
661 let classNamePropValue;
662 const classNamePropIndex = node.attributes.findIndex(
663 attr => attr.name && attr.name.name === classPropName
664 );
665 if (classNamePropIndex > -1 && Object.keys(staticAttributes).length > 0) {
666 classNamePropValue = getPropValueFromAttributes(
667 classPropName,
668 node.attributes
669 );
670 node.attributes.splice(classNamePropIndex, 1);
671 }
672
673 // if all style props have been extracted, jsxstyle component can be
674 // converted to a div or the specified component
675 if (inlinePropCount === 0) {
676 const propsPropIndex = node.attributes.findIndex(
677 attr => attr.name && attr.name.name === 'props'
678 );
679 // deal with props prop
680 if (propsPropIndex > -1) {
681 if (propsAttributes) {
682 propsAttributes.forEach(a => node.attributes.push(a));
683 }
684 // delete props prop
685 node.attributes.splice(propsPropIndex, 1);
686 }
687
688 const componentPropIndex = node.attributes.findIndex(
689 attr => attr.name && attr.name.name === 'component'
690 );
691 if (componentPropIndex > -1) {
692 const attribute = node.attributes[componentPropIndex];
693 const componentPropValue = t.isJSXExpressionContainer(attribute.value)
694 ? attribute.value.expression
695 : attribute.value;
696
697 if (
698 t.isLiteral(componentPropValue) &&
699 typeof componentPropValue.value === 'string'
700 ) {
701 node.name.name = componentPropValue.value;
702 } else if (t.isIdentifier(componentPropValue)) {
703 node.name.name = componentPropValue.name;
704 } else if (t.isMemberExpression(componentPropValue)) {
705 node.name.name = generate(componentPropValue).code;
706 }
707
708 // remove component prop from attributes
709 node.attributes.splice(componentPropIndex, 1);
710 } else {
711 node.name.name = 'div';
712 }
713 } else {
714 if (lastSpreadIndex !== null) {
715 // if only some style props were extracted AND additional props are spread onto the component,
716 // add the props back with null values to prevent spread props from incorrectly overwriting the extracted prop value
717 Object.keys(staticAttributes).forEach(attr => {
718 node.attributes.push(
719 t.jSXAttribute(
720 t.jSXIdentifier(attr),
721 t.jSXExpressionContainer(t.nullLiteral())
722 )
723 );
724 });
725 }
726 }
727
728 if (path.node.closingElement) {
729 path.node.closingElement.name.name = node.name.name;
730 }
731
732 if (!addCSSRequire) {
733 for (const key in staticAttributes) {
734 const value = staticAttributes[key];
735 if (typeof value !== 'string' || value.indexOf('url(') === -1)
736 continue;
737 staticAttributes[key] = cssRelativeURL(value, sourceDir, rootDir);
738 }
739 }
740
741 const stylesByClassName = getStylesByClassName(
742 styleGroups,
743 namedStyleGroups,
744 staticAttributes,
745 cacheObject
746 );
747
748 const extractedStyleClassNames = Object.keys(stylesByClassName).join(' ');
749
750 const classNameObjects = [];
751
752 if (classNamePropValue) {
753 if (canEvaluate(null, classNamePropValue)) {
754 // TODO: don't use canEvaluate here, need to be more specific
755 classNameObjects.push(
756 t.stringLiteral(
757 vm.runInNewContext(generate(classNamePropValue).code)
758 )
759 );
760 } else {
761 classNameObjects.push(classNamePropValue);
762 }
763 }
764
765 if (staticTernaries.length > 0) {
766 const ternaryObj = extractStaticTernaries(
767 staticTernaries,
768 evalContext,
769 cacheObject
770 );
771
772 // ternaryObj is null if all of the extracted ternaries have falsey consequents and alternates
773 if (ternaryObj !== null) {
774 // add extracted styles by className to existing object
775 Object.assign(stylesByClassName, ternaryObj.stylesByClassName);
776 classNameObjects.push(ternaryObj.ternaryExpression);
777 }
778 }
779
780 if (extractedStyleClassNames) {
781 classNameObjects.push(t.stringLiteral(extractedStyleClassNames));
782 }
783
784 const classNamePropValueForReals = classNameObjects.reduce((acc, val) => {
785 if (!acc) {
786 if (
787 // pass conditional expressions through
788 t.isConditionalExpression(val) ||
789 // pass non-null literals through
790 (t.isLiteral(val) && val.value !== null)
791 ) {
792 return val;
793 }
794 return t.logicalExpression('||', val, t.stringLiteral(''));
795 }
796
797 const accIsString = t.isLiteral(acc) && typeof acc.value === 'string';
798
799 let inner;
800 if (t.isLiteral(val)) {
801 if (typeof val.value === 'string') {
802 if (accIsString) {
803 // join adjacent string literals
804 return t.stringLiteral(`${acc.value} ${val.value}`);
805 }
806 inner = t.stringLiteral(` ${val.value}`);
807 } else {
808 inner = t.binaryExpression('+', t.stringLiteral(' '), val);
809 }
810 } else if (
811 t.isConditionalExpression(val) ||
812 t.isBinaryExpression(val)
813 ) {
814 if (accIsString) {
815 return t.binaryExpression(
816 '+',
817 t.stringLiteral(`${acc.value} `),
818 val
819 );
820 }
821 inner = t.binaryExpression('+', t.stringLiteral(' '), val);
822 } else if (t.isIdentifier(val) || t.isMemberExpression(val)) {
823 // identifiers and member expressions make for reasonable ternaries
824 inner = t.conditionalExpression(
825 val,
826 t.binaryExpression('+', t.stringLiteral(' '), val),
827 t.stringLiteral('')
828 );
829 } else {
830 if (accIsString) {
831 return t.binaryExpression(
832 '+',
833 t.stringLiteral(`${acc.value} `),
834 t.logicalExpression('||', val, t.stringLiteral(''))
835 );
836 }
837 // use a logical expression for more complex prop values
838 inner = t.binaryExpression(
839 '+',
840 t.stringLiteral(' '),
841 t.logicalExpression('||', val, t.stringLiteral(''))
842 );
843 }
844 return t.binaryExpression('+', acc, inner);
845 }, null);
846
847 if (classNamePropValueForReals) {
848 if (
849 t.isLiteral(classNamePropValueForReals) &&
850 typeof classNamePropValueForReals.value === 'string'
851 ) {
852 node.attributes.push(
853 t.jSXAttribute(
854 t.jSXIdentifier(classPropName),
855 t.stringLiteral(classNamePropValueForReals.value)
856 )
857 );
858 } else {
859 node.attributes.push(
860 t.jSXAttribute(
861 t.jSXIdentifier(classPropName),
862 t.jSXExpressionContainer(classNamePropValueForReals)
863 )
864 );
865 }
866 }
867
868 const lineNumbers =
869 node.loc.start.line +
870 (node.loc.start.line !== node.loc.end.line
871 ? `-${node.loc.end.line}`
872 : '');
873
874 const comment = util.format(
875 '/* %s:%s (%s) */',
876 sourceFileName.replace(process.cwd(), '.'),
877 lineNumbers,
878 originalNodeName
879 );
880
881 for (const className in stylesByClassName) {
882 if (cssMap.has(className)) {
883 if (comment) {
884 const val = cssMap.get(className);
885 val.commentTexts.push(comment);
886 cssMap.set(className, val);
887 }
888 } else {
889 let css = '';
890 const styleProps = stylesByClassName[className];
891
892 // get object of style objects
893 const styleObjects = getStyleKeysForProps(styleProps, true);
894 delete styleObjects.classNameKey;
895 const styleObjectKeys = Object.keys(styleObjects).sort();
896
897 for (let idx = -1, len = styleObjectKeys.length; ++idx < len; ) {
898 const k = styleObjectKeys[idx];
899 const item = styleObjects[k];
900 let itemCSS =
901 `.${className}` +
902 (item.pseudoclass ? ':' + item.pseudoclass : '') +
903 (item.pseudoelement ? '::' + item.pseudoelement : '') +
904 ` {${item.styles}}`;
905
906 if (item.mediaQuery) {
907 itemCSS = `@media ${item.mediaQuery} { ${itemCSS} }`;
908 }
909 css += itemCSS + '\n';
910 }
911
912 cssMap.set(className, { css, commentTexts: [comment] });
913 }
914 }
915 },
916 };
917
918 traverse(ast, traverseOptions);
919
920 const css = Array.from(cssMap.values())
921 .map(n => n.commentTexts.map(t => `${t}\n`).join('') + n.css)
922 .join('');
923 // path.parse doesn't exist in the webpack'd bundle but path.dirname and path.basename do.
924 const baseName = path.basename(sourceFileName, '.js');
925 const cssRelativeFileName = `./${baseName}.jsxstyle.css`;
926 const cssFileName = path.join(sourceDir, cssRelativeFileName);
927
928 if (addCSSRequire && css !== '') {
929 // append require statement to the document
930 // TODO: make sure this doesn't break something valuable
931 ast.program.body.unshift(
932 t.expressionStatement(
933 t.callExpression(t.identifier('require'), [
934 t.stringLiteral(cssRelativeFileName),
935 ])
936 )
937 );
938 }
939
940 const result = generate(
941 ast,
942 {
943 fileName: sourceFileName,
944 retainLines: false,
945 compact: 'auto',
946 concise: false,
947 sourceMaps: true,
948 sourceFileName,
949 quotes: 'single',
950 },
951 src
952 );
953
954 return {
955 js: result.code,
956 css,
957 cssFileName,
958 ast: result.ast,
959 map: result.map,
960 };
961}
962
963module.exports = extractStyles;