UNPKG

@linaria/utils

Version:

Blazing fast zero-runtime CSS in JS library

235 lines (222 loc) 8.32 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.collectTemplateDependencies = collectTemplateDependencies; exports.extractExpression = extractExpression; var _template = require("@babel/template"); var _types = require("@babel/types"); var _logger = require("@linaria/logger"); var _createId = require("./createId"); var _findIdentifiers = _interopRequireDefault(require("./findIdentifiers")); var _getSource = require("./getSource"); var _hasMeta = require("./hasMeta"); var _scopeHelpers = require("./scopeHelpers"); var _types2 = require("./types"); var _valueToLiteral = require("./valueToLiteral"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } /* 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. */ function staticEval(ex, evaluate = false) { if (!evaluate) return undefined; const result = ex.evaluate(); if (result.confident && !(0, _hasMeta.hasMeta)(result.value)) { return [result.value]; } return undefined; } const expressionDeclarationTpl = (0, _template.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 = (0, _findIdentifiers.default)([ex], 'reference'); referencedIdentifiers.forEach(identifier => { if (identifier.isIdentifier()) { hoistIdentifier(identifier); } }); const bindingIdentifiers = (0, _findIdentifiers.default)([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 => { var _p$parentPath; return ((_p$parentPath = p.parentPath) === null || _p$parentPath === void 0 ? void 0 : _p$parentPath.isProgram()) === true; }); const declaration = { type: 'VariableDeclaration', kind: 'let', declarations: [(0, _types.cloneNode)(ex.node)] }; const [inserted] = statementInRoot.insertBefore(declaration); (0, _scopeHelpers.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. */ function extractExpression(ex, evaluate = false, imports = []) { if (ex.isLiteral() && ('value' in ex.node || ex.node.type === 'NullLiteral')) { return { ex: ex.node, kind: _types2.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 => { var _p$parentPath2; return ((_p$parentPath2 = p.parentPath) === null || _p$parentPath2 === void 0 ? void 0 : _p$parentPath2.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 (0, _findIdentifiers.default)([ex], 'reference').forEach(id => { if (!id.isIdentifier()) return; // Try to evaluate and inline them… const evaluatedId = staticEval(id, evaluate); if (evaluatedId) { (0, _scopeHelpers.mutate)(id, p => { p.replaceWith((0, _valueToLiteral.valueToLiteral)(evaluatedId[0], ex)); }); } else { // … or hoist them to the root scope hoistIdentifier(id); } }); } const kind = isFunction ? _types2.ValueType.FUNCTION : _types2.ValueType.LAZY; // Declare _expN const with the lazy expression const declaration = expressionDeclarationTpl({ expId: (0, _createId.createId)(expUid), expression: evaluated ? (0, _valueToLiteral.valueToLiteral)(evaluated[0], ex) : (0, _types.cloneNode)(ex.node) }); // Insert the declaration as close as possible to the original expression const [inserted] = statementInRoot.insertBefore(declaration); (0, _scopeHelpers.referenceAll)(inserted); rootScope.registerDeclaration(inserted); const importedFrom = []; function findImportSourceOfIdentifier(idPath) { var _idPath$scope$getBind, _imports$find; const exBindingIdentifier = (_idPath$scope$getBind = idPath.scope.getBinding(idPath.node.name)) === null || _idPath$scope$getBind === void 0 ? void 0 : _idPath$scope$getBind.identifier; const exImport = (_imports$find = imports.find(i => i.local.node === exBindingIdentifier)) !== null && _imports$find !== void 0 ? _imports$find : null; if (exImport) { importedFrom.push(exImport.source); } } if (ex.isIdentifier()) { findImportSourceOfIdentifier(ex); } else { ex.traverse({ Identifier: findImportSourceOfIdentifier }); } // Replace the expression with the _expN() call (0, _scopeHelpers.mutate)(ex, p => { p.replaceWith({ type: 'CallExpression', callee: (0, _createId.createId)(expUid), arguments: [] }); }); // eslint-disable-next-line no-param-reassign ex.node.loc = loc; // noinspection UnnecessaryLocalVariableJS const result = { kind, ex: (0, _createId.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 */ function collectTemplateDependencies(path, evaluate = false) { const quasi = path.get('quasi'); const quasis = quasi.get('quasis'); const expressions = quasi.get('expressions'); (0, _logger.debug)('template-parse:identify-expressions', expressions.length); const expressionValues = expressions.map(ex => { const buildCodeFrameError = ex.buildCodeFrameError.bind(ex); const source = (0, _getSource.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