@linaria/utils
Version:
Blazing fast zero-runtime CSS in JS library
235 lines (222 loc) • 8.32 kB
JavaScript
"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