UNPKG

@linaria/utils

Version:

Blazing fast zero-runtime CSS in JS library

220 lines (208 loc) 7.25 kB
/* eslint @typescript-eslint/no-use-before-define: ["error", { "functions": false }] */ /** * This file is a visitor that checks TaggedTemplateExpressions and look for Linaria css or styled templates. * For each template it makes a list of dependencies, try to evaluate expressions, and if it is not possible, mark them as lazy dependencies. */ import { statement } from '@babel/template'; import { cloneNode } from '@babel/types'; import { debug } from '@linaria/logger'; import { createId } from './createId'; import findIdentifiers from './findIdentifiers'; import { getSource } from './getSource'; import { hasMeta } from './hasMeta'; import { mutate, referenceAll } from './scopeHelpers'; import { ValueType } from './types'; import { valueToLiteral } from './valueToLiteral'; function staticEval(ex, evaluate = false) { if (!evaluate) return undefined; const result = ex.evaluate(); if (result.confident && !hasMeta(result.value)) { return [result.value]; } return undefined; } const expressionDeclarationTpl = statement('const %%expId%% = /*#__PURE__*/ () => %%expression%%', { preserveComments: true }); const unsupported = (ex, reason) => ex.buildCodeFrameError(`This ${ex.isIdentifier() ? 'identifier' : 'expression'} cannot be used in the template${reason ? `, because it ${reason}` : ''}.`); function getUidInRootScope(path) { const { name } = path.node; const rootScope = path.scope.getProgramParent(); if (rootScope.hasBinding(name)) { return rootScope.generateUid(name); } return name; } function hoistVariableDeclarator(ex) { if (!ex.scope.parent) { // It is already in the root scope return; } const referencedIdentifiers = findIdentifiers([ex], 'reference'); referencedIdentifiers.forEach(identifier => { if (identifier.isIdentifier()) { hoistIdentifier(identifier); } }); const bindingIdentifiers = findIdentifiers([ex], 'declaration'); bindingIdentifiers.forEach(path => { const newName = getUidInRootScope(path); if (newName !== path.node.name) { path.scope.rename(path.node.name, newName); } }); const rootScope = ex.scope.getProgramParent(); const statementInRoot = ex.findParent(p => p.parentPath?.isProgram() === true); const declaration = { type: 'VariableDeclaration', kind: 'let', declarations: [cloneNode(ex.node)] }; const [inserted] = statementInRoot.insertBefore(declaration); referenceAll(inserted); rootScope.registerDeclaration(inserted); } function hoistIdentifier(idPath) { if (!idPath.isReferenced()) { throw unsupported(idPath); } const binding = idPath.scope.getBinding(idPath.node.name); if (!binding) { // It's something strange throw unsupported(idPath, 'is undefined'); } if (binding.kind === 'module') { // Modules are global by default return; } if (!['var', 'let', 'const', 'hoisted'].includes(binding.kind)) { // This is not a variable, we can't hoist it throw unsupported(binding.path, 'is a function parameter'); } const { scope, path: bindingPath } = binding; // parent here can be null or undefined in different versions of babel if (!scope.parent) { // The variable is already in the root scope return; } if (bindingPath.isVariableDeclarator()) { hoistVariableDeclarator(bindingPath); return; } throw unsupported(idPath); } /** * Only an expression that can be evaluated in the root scope can be * used in a Linaria template. This function tries to hoist the expression. * @param ex The expression to hoist. * @param evaluate If true, we try to statically evaluate the expression. * @param imports All the imports of the file. */ export function extractExpression(ex, evaluate = false, imports = []) { if (ex.isLiteral() && ('value' in ex.node || ex.node.type === 'NullLiteral')) { return { ex: ex.node, kind: ValueType.CONST, value: ex.node.type === 'NullLiteral' ? null : ex.node.value }; } const { loc } = ex.node; const rootScope = ex.scope.getProgramParent(); const statementInRoot = ex.findParent(p => p.parentPath?.isProgram() === true); const isFunction = ex.isFunctionExpression() || ex.isArrowFunctionExpression(); // Generate next _expN name const expUid = rootScope.generateUid('exp'); const evaluated = staticEval(ex, evaluate); if (!evaluated) { // If expression is not statically evaluable, // we need to hoist all its referenced identifiers // Collect all referenced identifiers findIdentifiers([ex], 'reference').forEach(id => { if (!id.isIdentifier()) return; // Try to evaluate and inline them… const evaluatedId = staticEval(id, evaluate); if (evaluatedId) { mutate(id, p => { p.replaceWith(valueToLiteral(evaluatedId[0], ex)); }); } else { // … or hoist them to the root scope hoistIdentifier(id); } }); } const kind = isFunction ? ValueType.FUNCTION : ValueType.LAZY; // Declare _expN const with the lazy expression const declaration = expressionDeclarationTpl({ expId: createId(expUid), expression: evaluated ? valueToLiteral(evaluated[0], ex) : cloneNode(ex.node) }); // Insert the declaration as close as possible to the original expression const [inserted] = statementInRoot.insertBefore(declaration); referenceAll(inserted); rootScope.registerDeclaration(inserted); const importedFrom = []; function findImportSourceOfIdentifier(idPath) { const exBindingIdentifier = idPath.scope.getBinding(idPath.node.name)?.identifier; const exImport = imports.find(i => i.local.node === exBindingIdentifier) ?? null; if (exImport) { importedFrom.push(exImport.source); } } if (ex.isIdentifier()) { findImportSourceOfIdentifier(ex); } else { ex.traverse({ Identifier: findImportSourceOfIdentifier }); } // Replace the expression with the _expN() call mutate(ex, p => { p.replaceWith({ type: 'CallExpression', callee: createId(expUid), arguments: [] }); }); // eslint-disable-next-line no-param-reassign ex.node.loc = loc; // noinspection UnnecessaryLocalVariableJS const result = { kind, ex: createId(expUid, loc), importedFrom }; return result; } /** * Collects, hoists, and makes lazy all expressions in the given template * If evaluate is true, it will try to evaluate the expressions */ export function collectTemplateDependencies(path, evaluate = false) { const quasi = path.get('quasi'); const quasis = quasi.get('quasis'); const expressions = quasi.get('expressions'); debug('template-parse:identify-expressions', expressions.length); const expressionValues = expressions.map(ex => { const buildCodeFrameError = ex.buildCodeFrameError.bind(ex); const source = getSource(ex); if (!ex.isExpression()) { throw buildCodeFrameError(`The expression '${source}' is not supported.`); } const extracted = extractExpression(ex, evaluate); return { ...extracted, source, buildCodeFrameError }; }); return [quasis.map(p => p.node), expressionValues]; } //# sourceMappingURL=collectTemplateDependencies.js.map