summaryrefslogtreecommitdiff
path: root/@linaria/packages/babel/src/evaluators/templateProcessor.ts
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2021-08-23 16:46:06 -0300
committerSebastian <sebasjm@gmail.com>2021-08-23 16:48:30 -0300
commit38acabfa6089ab8ac469c12b5f55022fb96935e5 (patch)
tree453dbf70000cc5e338b06201af1eaca8343f8f73 /@linaria/packages/babel/src/evaluators/templateProcessor.ts
parentf26125e039143b92dc0d84e7775f508ab0cdcaa8 (diff)
downloadnode-vendor-38acabfa6089ab8ac469c12b5f55022fb96935e5.tar.gz
node-vendor-38acabfa6089ab8ac469c12b5f55022fb96935e5.tar.bz2
node-vendor-38acabfa6089ab8ac469c12b5f55022fb96935e5.zip
added web vendorsHEADmaster
Diffstat (limited to '@linaria/packages/babel/src/evaluators/templateProcessor.ts')
-rw-r--r--@linaria/packages/babel/src/evaluators/templateProcessor.ts313
1 files changed, 313 insertions, 0 deletions
diff --git a/@linaria/packages/babel/src/evaluators/templateProcessor.ts b/@linaria/packages/babel/src/evaluators/templateProcessor.ts
new file mode 100644
index 0000000..0c910d4
--- /dev/null
+++ b/@linaria/packages/babel/src/evaluators/templateProcessor.ts
@@ -0,0 +1,313 @@
+/**
+ * This file handles transforming template literals to class names or styled components and generates CSS content.
+ * It uses CSS code from template literals and evaluated values of lazy dependencies stored in ValueCache.
+ */
+
+import type { Expression } from '@babel/types';
+import generator from '@babel/generator';
+
+import type { StyledMeta } from '@linaria/core';
+import { debug } from '@linaria/logger';
+import { units } from '../units';
+import type {
+ State,
+ StrictOptions,
+ TemplateExpression,
+ ValueCache,
+} from '../types';
+
+import isSerializable from '../utils/isSerializable';
+import throwIfInvalid from '../utils/throwIfInvalid';
+import stripLines from '../utils/stripLines';
+import toCSS from '../utils/toCSS';
+import getLinariaComment from '../utils/getLinariaComment';
+import { Core } from '../babel';
+
+// Match any valid CSS units followed by a separator such as ;, newline etc.
+const unitRegex = new RegExp(`^(${units.join('|')})(;|,|\n| |\\))`);
+
+type Interpolation = {
+ id: string;
+ node: Expression;
+ source: string;
+ unit: string;
+};
+
+function hasMeta(value: any): value is StyledMeta {
+ return value && typeof value === 'object' && (value as any).__linaria;
+}
+
+const processedPaths = new WeakSet();
+
+export default function getTemplateProcessor(
+ { types: t }: Core,
+ options: StrictOptions
+) {
+ return function process(
+ { styled, path }: TemplateExpression,
+ state: State,
+ valueCache: ValueCache
+ ) {
+ if (processedPaths.has(path)) {
+ // Do not process an expression
+ // if it is referenced in one template more than once
+ return;
+ }
+
+ processedPaths.add(path);
+
+ const { quasi } = path.node;
+
+ const interpolations: Interpolation[] = [];
+
+ // Check if the variable is referenced anywhere for basic DCE
+ // Only works when it's assigned to a variable
+ let isReferenced = true;
+
+ const [, slug, displayName, className] = getLinariaComment(path);
+
+ const parent = path.findParent(
+ (p) =>
+ t.isObjectProperty(p) ||
+ t.isJSXOpeningElement(p) ||
+ t.isVariableDeclarator(p)
+ );
+
+ if (parent) {
+ const parentNode = parent.node;
+ if (t.isVariableDeclarator(parentNode) && t.isIdentifier(parentNode.id)) {
+ const { referencePaths } = path.scope.getBinding(
+ parentNode.id.name
+ ) || { referencePaths: [] };
+
+ isReferenced = referencePaths.length !== 0;
+ }
+ }
+
+ // Serialize the tagged template literal to a string
+ let cssText = '';
+
+ const expressions = path.get('quasi').get('expressions');
+
+ quasi.quasis.forEach((el, i, self) => {
+ let appended = false;
+
+ if (i !== 0 && el.value.cooked) {
+ // Check if previous expression was a CSS variable that we replaced
+ // If it has a unit after it, we need to move the unit into the interpolation
+ // e.g. `var(--size)px` should actually be `var(--size)`
+ // So we check if the current text starts with a unit, and add the unit to the previous interpolation
+ // Another approach would be `calc(var(--size) * 1px), but some browsers don't support all units
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=956573
+ const matches = el.value.cooked.match(unitRegex);
+
+ if (matches) {
+ const last = interpolations[interpolations.length - 1];
+ const [, unit] = matches;
+
+ if (last && cssText.endsWith(`var(--${last.id})`)) {
+ last.unit = unit;
+ cssText += el.value.cooked.replace(unitRegex, '$2');
+ appended = true;
+ }
+ }
+ }
+
+ if (!appended) {
+ cssText += el.value.cooked;
+ }
+
+ const ex = expressions[i];
+
+ if (ex && !ex.isExpression()) {
+ throw ex.buildCodeFrameError(
+ `The expression '${generator(ex.node).code}' is not supported.`
+ );
+ }
+
+ if (ex) {
+ const { end } = ex.node.loc!;
+ const result = ex.evaluate();
+ const beforeLength = cssText.length;
+
+ // The location will be end of the current string to start of next string
+ const next = self[i + 1];
+ const loc = {
+ // +1 because the expressions location always shows 1 column before
+ start: { line: el.loc!.end.line, column: el.loc!.end.column + 1 },
+ end: next
+ ? { line: next.loc!.start.line, column: next.loc!.start.column }
+ : { line: end.line, column: end.column + 1 },
+ };
+
+ if (result.confident) {
+ throwIfInvalid(result.value, ex);
+
+ if (isSerializable(result.value)) {
+ // If it's a plain object or an array, convert it to a CSS string
+ cssText += stripLines(loc, toCSS(result.value));
+ } else {
+ cssText += stripLines(loc, result.value);
+ }
+
+ state.replacements.push({
+ original: loc,
+ length: cssText.length - beforeLength,
+ });
+ } else {
+ // Try to preval the value
+ if (
+ options.evaluate &&
+ !(t.isFunctionExpression(ex) || t.isArrowFunctionExpression(ex))
+ ) {
+ const value = valueCache.get(ex.node);
+ throwIfInvalid(value, ex);
+
+ // Skip the blank string instead of throw ing an error
+ if (value === '') {
+ return;
+ }
+
+ if (value && typeof value !== 'function') {
+ // Only insert text for non functions
+ // We don't touch functions because they'll be interpolated at runtime
+
+ if (hasMeta(value)) {
+ // If it's an React component wrapped in styled, get the class name
+ // Useful for interpolating components
+ cssText += `.${value.__linaria.className}`;
+ } else if (isSerializable(value)) {
+ cssText += stripLines(loc, toCSS(value));
+ } else {
+ // For anything else, assume it'll be stringified
+ cssText += stripLines(loc, value);
+ }
+
+ state.replacements.push({
+ original: loc,
+ length: cssText.length - beforeLength,
+ });
+
+ return;
+ }
+ }
+
+ if (styled) {
+ const id = `${slug}-${i}`;
+
+ interpolations.push({
+ id,
+ node: ex.node,
+ source: ex.getSource() || generator(ex.node).code,
+ unit: '',
+ });
+
+ cssText += `var(--${id})`;
+ } else {
+ // CSS custom properties can't be used outside components
+ throw ex.buildCodeFrameError(
+ `The CSS cannot contain JavaScript expressions when using the 'css' tag. To evaluate the expressions at build time, pass 'evaluate: true' to the babel plugin.`
+ );
+ }
+ }
+ }
+ });
+
+ let selector = `.${className}`;
+
+ if (styled) {
+ // If `styled` wraps another component and not a primitive,
+ // get its class name to create a more specific selector
+ // it'll ensure that styles are overridden properly
+ if (options.evaluate && t.isIdentifier(styled.component.node)) {
+ let value = valueCache.get(styled.component.node.name);
+ while (hasMeta(value)) {
+ selector += `.${value.__linaria.className}`;
+ value = value.__linaria.extends;
+ }
+ }
+
+ const props = [];
+
+ props.push(
+ t.objectProperty(t.identifier('name'), t.stringLiteral(displayName!))
+ );
+
+ props.push(
+ t.objectProperty(t.identifier('class'), t.stringLiteral(className!))
+ );
+
+ // If we found any interpolations, also pass them so they can be applied
+ if (interpolations.length) {
+ // De-duplicate interpolations based on the source and unit
+ // If two interpolations have the same source code and same unit,
+ // we don't need to use 2 custom properties for them, we can use a single one
+ const result: { [key: string]: Interpolation } = {};
+
+ interpolations.forEach((it) => {
+ const key = it.source + it.unit;
+
+ if (key in result) {
+ cssText = cssText.replace(
+ `var(--${it.id})`,
+ `var(--${result[key].id})`
+ );
+ } else {
+ result[key] = it;
+ }
+ });
+
+ props.push(
+ t.objectProperty(
+ t.identifier('vars'),
+ t.objectExpression(
+ Object.keys(result).map((key) => {
+ const { id, node, unit } = result[key];
+ const items = [node];
+
+ if (unit) {
+ items.push(t.stringLiteral(unit));
+ }
+
+ return t.objectProperty(
+ t.stringLiteral(id),
+ t.arrayExpression(items)
+ );
+ })
+ )
+ )
+ );
+ }
+
+ path.replaceWith(
+ t.callExpression(
+ t.callExpression(
+ t.identifier(state.file.metadata.localName || 'styled'),
+ [styled.component.node]
+ ),
+ [t.objectExpression(props)]
+ )
+ );
+
+ path.addComment('leading', '#__PURE__');
+ } else {
+ path.replaceWith(t.stringLiteral(className!));
+ }
+
+ if (!isReferenced && !cssText.includes(':global')) {
+ return;
+ }
+
+ debug(
+ 'evaluator:template-processor:extracted-rule',
+ `\n${selector} {${cssText}\n}`
+ );
+
+ state.rules[selector] = {
+ cssText,
+ className: className!,
+ displayName: displayName!,
+ start: path.parent?.loc?.start ?? null,
+ };
+ };
+}