babel-plugin-styled-components
Version:
Improve the debugging experience and add server-side rendering support to styled-components
470 lines (453 loc) • 22.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.requireBindingName = exports.registerInjectedDefault = exports.noteStyledRequired = exports.isWithThemeHelper = exports.isValidTopLevelImport = exports.isUseTheme = exports.isStyled = exports.isPureHelper = exports.isKeyframesHelper = exports.isHelper = exports.isCreateGlobalStyleHelper = exports.isCSSHelper = exports.injectedDefaultName = exports.importLocalName = void 0;
var _getSequenceExpressionValue = _interopRequireDefault(require("./getSequenceExpressionValue"));
var _options = require("./options");
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
const VALID_TOP_LEVEL_IMPORT_PATHS = new Set(['styled-components', 'styled-components/no-tags', 'styled-components/native', 'styled-components/primitives']);
const isValidTopLevelImport = (x, state) => VALID_TOP_LEVEL_IMPORT_PATHS.has(x) || (0, _options.useTopLevelImportPathMatchers)(state).some(isMatch => isMatch(x));
// `import type` and `import typeof` (declaration or specifier) bind no value
exports.isValidTopLevelImport = isValidTopLevelImport;
const isTypeOnly = node => node.importKind === 'type' || node.importKind === 'typeof';
const IMPORT_NAMES_CACHE = 'styled-components-import-local-names';
const INJECTED_DEFAULT_NAME = 'styled-components-injected-default-name';
const REQUIRED_NAMES = 'styled-components-required-names';
// Names whose require() declarator assignStyledRequired has already walked.
// The css-prop pre-pass runs mid-traversal, and a require that sits below the
// first css prop must not be numbered there.
const noteStyledRequired = (state, name) => {
if (!name) return;
let names = state.file.get(REQUIRED_NAMES);
if (!names) {
names = new Set();
state.file.set(REQUIRED_NAMES, names);
}
names.add(name);
};
exports.noteStyledRequired = noteStyledRequired;
const styledRequireVisited = (state, name) => {
const names = state.file.get(REQUIRED_NAMES);
return !!name && !!names && names.has(name);
};
// The default binding the css-prop transform injected, which can be a
// `require` (script files) that no ImportDeclaration scan would find
const injectedDefaultName = state => state.file.get(INJECTED_DEFAULT_NAME);
exports.injectedDefaultName = injectedDefaultName;
const LEGACY_DEFAULT_LOCAL = 'styled-components-legacy-default-local';
const registerInjectedDefault = (state, name) => {
state.file.set(INJECTED_DEFAULT_NAME, name);
state.file.set(IMPORT_NAMES_CACHE, null);
state.file.set(LEGACY_DEFAULT_LOCAL, null);
};
exports.registerInjectedDefault = registerInjectedDefault;
const importLocalName = (name, state) => {
if (state.customImportName) return state.customImportName.name;
let cache = state.file.get(IMPORT_NAMES_CACHE);
if (!cache) {
cache = {};
state.file.set(IMPORT_NAMES_CACHE, cache);
}
if (name in cache) return cache[name];
let localName = state.styledRequired ? name === 'default' ? state.styledRequired : name : false;
// Only top-level declarations are real bindings; imports inside TypeScript
// and Flow `declare module` or `declare namespace` bodies are ambient
for (const node of state.file.path.node.body) {
if (node.type !== 'ImportDeclaration') continue;
if (isTypeOnly(node)) continue;
if (!isValidTopLevelImport(node.source.value, state)) continue;
for (const specifier of node.specifiers) {
if (isTypeOnly(specifier)) continue;
if (name === 'default' && specifier.type === 'ImportSpecifier' && specifier.imported.name === 'styled') {
localName = specifier.local.name;
}
if (specifier.type === 'ImportDefaultSpecifier') {
localName = specifier.local.name;
}
if (specifier.type === 'ImportSpecifier' && specifier.imported.name === name) {
localName = specifier.local.name;
}
// The namespace binding isn't directly callable, so prefer any
// other specifier (default or named) discovered in this file.
if (specifier.type === 'ImportNamespaceSpecifier' && !localName) {
localName = name === 'default' ? specifier.local.name : name;
}
}
}
if (name === 'default') {
const injected = injectedDefaultName(state);
if (injected) localName = injected;
}
cache[name] = localName;
return localName;
};
// Known non-tag exports. Object and function prototype names are not listed
// here; `isBuiltinPrototypeKey` covers them. None of those names are DOM or
// SVG tags (`name`, `length`, `caller`, and `arguments` included).
exports.importLocalName = importLocalName;
const OPEN_SKIP = new Set(['NativeStyleContext', 'ParentContext', 'SafeAreaInsetsProvider', 'ServerStyleSheet', 'StyleSheetConsumer', 'StyleSheetContext', 'StyleSheetManager', 'ThemeConsumer', 'ThemeContext', 'ThemeProvider', '__PRIVATE__', '__testOnly_verticalAlignCompanions', 'createGlobalStyle', 'createTheme', 'css', 'extractCSS', 'isStyledComponent', 'keyframes', 'matchMedia', 'setAnimationDebug', 'styled', 'stylisPluginRSC', 'toStyleSheet', 'useBreakpoint', 'useContainer', 'useContainerQuery', 'useMediaEnv', 'useMediaQuery', 'useNativeStyleContext', 'useParentContext', 'useTheme', 'version', 'withTheme']);
const INTEROP_REQUIRE = new Set(['_interopRequireDefault', '__importDefault']);
// A computed key whose value is known at compile time: a string, a template
// whose parts all fold to strings, or a const binding of one of those.
// `let`, `var`, destructuring, and imports do not fold.
const resolveStaticKey = (t, node, scope, seen) => {
const current = unwrapType(node);
if (!current) return null;
if (t.isStringLiteral(current)) return current.value;
if (t.isTemplateLiteral(current)) {
const quasis = current.quasis || [];
const expressions = current.expressions || [];
let out = '';
for (let i = 0; i < quasis.length; i++) {
const cooked = quasis[i] && quasis[i].value.cooked;
if (cooked == null) return null;
out += cooked;
if (i >= expressions.length) continue;
const part = resolveStaticKey(t, expressions[i], scope, seen);
if (part == null) return null;
out += part;
}
return out;
}
if (!t.isIdentifier(current) || !scope) return null;
const binding = scope.getBinding(current.name);
if (!binding || binding.kind !== 'const' || !binding.path.isVariableDeclarator()) {
return null;
}
if (seen && seen.has(binding)) return null;
const next = seen || new Set();
next.add(binding);
const resolved = resolveStaticKey(t, binding.path.node.init, binding.scope, next);
next.delete(binding);
return resolved;
};
// `sc.div` / `sc['div']` / `sc[\`div\`]` / `const tag = 'div'; sc[tag]` have a
// static name. Anything else is a dynamic lookup. The identifier's spelling
// is not a key.
const memberKey = (t, member, scope) => {
if (!t.isMemberExpression(member)) return null;
if (member.computed) {
const resolved = resolveStaticKey(t, member.property, scope);
return resolved == null ? {
kind: 'dynamic'
} : {
kind: 'static',
name: resolved
};
}
return t.isIdentifier(member.property) ? {
kind: 'static',
name: member.property.name
} : null;
};
const unwrapType = node => {
let current = node;
while (current && (current.type === 'TSAsExpression' || current.type === 'TSTypeAssertion' || current.type === 'TSSatisfiesExpression')) {
current = current.expression;
}
return current;
};
const isStyledRequireCall = (t, node, state) => {
if (!t.isCallExpression(node) || !t.isIdentifier(node.callee)) return false;
if (INTEROP_REQUIRE.has(node.callee.name) && node.arguments && node.arguments[0]) {
return isStyledRequireCall(t, node.arguments[0], state);
}
const arg = node.arguments && node.arguments[0];
return node.callee.name === 'require' && !!arg && t.isStringLiteral(arg) && isValidTopLevelImport(arg.value, state);
};
// Identifier bound with `require('styled-components')` or `.default` / `['default']`
// of that call (including the `_interopRequireDefault` / `__importDefault` wrap).
const requireBindingName = (t, id, init, state) => {
if (!t.isIdentifier(id)) return null;
let node = unwrapType(init);
if (!node) return null;
const peeled = staticDefaultObject(t, node);
if (peeled) node = peeled;
return isStyledRequireCall(t, node, state) ? id.name : null;
};
// `.default` / `['default']`, or null when this member is not that key.
exports.requireBindingName = requireBindingName;
const staticDefaultObject = (t, node, scope) => {
if (!t.isMemberExpression(node)) return null;
const key = memberKey(t, node, scope);
if (!key || key.kind !== 'static' || key.name !== 'default') return null;
return node.object;
};
const referencePathFor = (binding, referenceNode) => {
const paths = binding.referencePaths;
if (!paths || !referenceNode) return null;
for (let i = 0; i < paths.length; i++) {
if (paths[i].node === referenceNode) return paths[i];
}
return null;
};
const isLoopPath = path => path.isForStatement() || path.isWhileStatement() || path.isDoWhileStatement() || path.isForInStatement() || path.isForOfStatement();
// A function nested inside the binding's scope can run after a reassignment
// even when the function is written above it.
const inNestedFunction = (binding, refPath) => {
let scope = refPath.scope;
while (scope && scope !== binding.scope) {
if (scope.path && scope.path.isFunction()) return true;
scope = scope.parent;
}
return false;
};
// A loop that both reads the binding and assigns it runs the read again
// after the assignment. A read in a loop whose assignment is outside the
// loop still sees the factory on every iteration.
const loopReassigns = (binding, refPath) => {
const stop = binding.scope && binding.scope.path;
const violations = binding.constantViolations || [];
let path = refPath.parentPath;
while (path && path !== stop) {
if (isLoopPath(path)) {
for (let i = 0; i < violations.length; i++) {
const violation = violations[i];
if (violation === path || path.isAncestor(violation)) return true;
}
}
path = path.parentPath;
}
return false;
};
const assignmentPrecedes = (binding, referenceNode) => {
if (!referenceNode || typeof referenceNode.start !== 'number') return true;
return binding.constantViolations.some(violation => {
const start = violation.node && violation.node.start;
return typeof start === 'number' && start < referenceNode.start;
});
};
// True when this reference must not be treated as the factory. Straight-line
// uses in the binding's scope that sit before the assignment stay decorated.
const reassignmentUnsafe = (binding, referenceNode) => {
if (!binding || binding.constant !== false) return false;
const refPath = referencePathFor(binding, referenceNode);
if (refPath) {
if (inNestedFunction(binding, refPath)) return true;
if (loopReassigns(binding, refPath)) return true;
}
return assignmentPrecedes(binding, referenceNode);
};
const importedNameOf = (t, specifier) => {
const imported = specifier.imported;
if (t.isIdentifier(imported)) return imported.name;
if (t.isStringLiteral(imported)) return imported.value;
return null;
};
// 'open': the styled factory. Unknown members are decorated. Object methods
// and known non-tag exports are not. This is require(), a default import, a
// named `styled` import (any local name), `.default` of those, and the
// `_interopRequireDefault` / `__importDefault` helper.
// 'namespace': `import * as ns`. Same members as 'open': a CommonJS transform
// binds `ns` to the module exports, which is the factory on these packages.
// The call `ns(Comp)` is decorated too. A native ESM namespace is not
// callable, so that call throws with or without `.withConfig`.
// null: this name is not a styled binding at `referenceNode`.
const styledBindingKind = (name, scope, state, t, seen, referenceNode) => {
if (!name || !scope) return null;
const token = `${scope.uid}:${name}`;
if (seen && seen.has(token)) return null;
const binding = scope.getBinding(name);
if (!binding) {
if (state.customImportName && name === state.customImportName.name) {
return 'open';
}
// css-prop injects `var _styled = interop(require()).default` and then
// calls it. That binding is not visible to scope.getBinding, so the name
// registered for the injection is the styled function.
if (injectedDefaultName(state) === name) return 'open';
// A bundler global named `styled` next to a require() of the package.
if (name === 'styled' && state.styledRequired) return 'open';
return null;
}
if (reassignmentUnsafe(binding, referenceNode)) return null;
const bpath = binding.path;
if (bpath.isImportDefaultSpecifier() || bpath.isImportSpecifier() || bpath.isImportNamespaceSpecifier()) {
const decl = bpath.parentPath;
if (!decl.isImportDeclaration()) return null;
if (isTypeOnly(decl.node) || isTypeOnly(bpath.node)) return null;
if (!isValidTopLevelImport(decl.node.source.value, state)) return null;
if (bpath.isImportNamespaceSpecifier()) return 'namespace';
if (bpath.isImportSpecifier()) {
const importedName = importedNameOf(t, bpath.node);
if (importedName !== 'styled' && importedName !== 'default') return null;
}
return 'open';
}
if (!bpath.isVariableDeclarator() || !t.isIdentifier(bpath.node.id)) {
return null;
}
const init = unwrapType(bpath.node.init);
if (!init) return null;
// Noted only once assignStyledRequired has walked the declarator, so a
// require that sits below the first css prop is not numbered in that
// pre-pass. `.default` / `['default']` of the call is the same binding.
const peeled = staticDefaultObject(t, init, binding.scope);
if (isStyledRequireCall(t, init, state) || peeled && isStyledRequireCall(t, peeled, state)) {
return styledRequireVisited(state, bpath.node.id.name) ? 'open' : null;
}
const chain = seen || new Set();
chain.add(token);
if (peeled) {
// `const styled = sc.default` where `sc` is a require or a namespace.
if (t.isIdentifier(peeled)) {
const baseKind = styledBindingKind(peeled.name, binding.scope, state, t, chain, peeled);
if (baseKind === 'open' || baseKind === 'namespace') return 'open';
}
return null;
}
if (t.isIdentifier(init)) {
return styledBindingKind(init.name, binding.scope, state, t, chain, init);
}
return null;
};
// `in` is safe here: these prototypes are ordinary objects, and the check
// does not read the property. A name that is also a real tag would be kept,
// but no DOM or SVG tag is on either prototype.
const isBuiltinPrototypeKey = name => name in Object.prototype || name in Function.prototype;
// 2.3.0's default local: the string `'styled'` whenever a require() of the
// package exists, then overwritten by top-level imports. A named `styled`
// import sets the string `'styled'`, not the local alias. A default import
// sets that import's local name. A namespace sets its local name only when
// nothing else did.
const legacyDefaultLocal = (state, t) => {
if (state.customImportName) return state.customImportName.name;
const hadRequired = !!state.styledRequired;
const cached = state.file.get(LEGACY_DEFAULT_LOCAL);
if (cached && cached.hadRequired === hadRequired) return cached.value;
let localName = hadRequired ? 'styled' : null;
const body = state.file.path && state.file.path.node && state.file.path.node.body;
if (body) {
for (const node of body) {
if (node.type !== 'ImportDeclaration') continue;
if (isTypeOnly(node)) continue;
if (!isValidTopLevelImport(node.source.value, state)) continue;
for (const specifier of node.specifiers) {
if (isTypeOnly(specifier)) continue;
const imported = specifier.imported;
const importedName = !imported ? null : t.isIdentifier(imported) ? imported.name : t.isStringLiteral(imported) ? imported.value : null;
if (specifier.type === 'ImportSpecifier' && importedName === 'styled') {
localName = 'styled';
}
if (specifier.type === 'ImportDefaultSpecifier') {
localName = specifier.local.name;
}
if (specifier.type === 'ImportSpecifier' && importedName === 'default') {
localName = specifier.local.name;
}
if (specifier.type === 'ImportNamespaceSpecifier' && !localName) {
localName = specifier.local.name;
}
}
}
}
state.file.set(LEGACY_DEFAULT_LOCAL, {
hadRequired,
value: localName
});
return localName;
};
const resolvesToDefaultLocal = (name, defaultLocal, state, t) => {
if (!name || !defaultLocal) return false;
if (name === defaultLocal) return true;
const scope = state.file && state.file.path && state.file.path.scope;
if (!scope) return false;
const visited = new Set([name]);
let current = name;
while (true) {
const binding = scope.getBinding(current);
if (!binding || !binding.path.isVariableDeclarator()) return false;
const init = binding.path.node.init;
if (!init) return false;
let nextName = null;
if (t.isIdentifier(init)) {
nextName = init.name;
} else if ((init.type === 'TSAsExpression' || init.type === 'TSTypeAssertion') && t.isIdentifier(init.expression)) {
nextName = init.expression.name;
}
if (!nextName) return false;
if (nextName === defaultLocal) return true;
if (visited.has(nextName)) return false;
visited.add(nextName);
current = nextName;
}
};
const legacyDecoratesUnresolved = (objectName, state, t) => resolvesToDefaultLocal(objectName, legacyDefaultLocal(state, t), state, t);
const decorateKeyedMember = (kind, key, property, state, t, objectName) => {
if (!kind || !key) return false;
// An unresolved key is decorated only for the names 2.3.0 decorated
// (`styled[dynamic]`, a default import's local name). A require named
// something else, and an alias that is not that name, stay plain.
if (key.kind === 'dynamic') {
if (!legacyDecoratesUnresolved(objectName, state, t)) return false;
return !isHelper(t)(property, state);
}
if (key.kind !== 'static' || key.name === 'default') return false;
if (isBuiltinPrototypeKey(key.name)) return false;
if (kind === 'open' || kind === 'namespace') return !OPEN_SKIP.has(key.name);
return false;
};
const isDefaultMember = (t, node, scope) => {
if (!t.isMemberExpression(node) || !t.isIdentifier(node.object)) return false;
const key = memberKey(t, node, scope);
return !!key && key.kind === 'static' && key.name === 'default';
};
const isStyled = t => (tag, state, scope) => {
const lookup = scope || state.file && state.file.path && state.file.path.scope;
if (t.isCallExpression(tag) && t.isMemberExpression(tag.callee)) {
const key = memberKey(t, tag.callee, lookup);
// `.attrs()` / `.withConfig()` chains. `['default']` is the styled
// function, same as `.default`, so it is not peeled off here.
if (!(key && key.kind === 'static' && key.name === 'default')) {
return isStyled(t)(tag.callee.object, state, lookup);
}
} else if (t.isCallExpression(tag) && t.isSequenceExpression(tag.callee) && t.isMemberExpression((0, _getSequenceExpressionValue.default)(tag.callee))) {
const value = (0, _getSequenceExpressionValue.default)(tag.callee);
const key = memberKey(t, value, lookup);
if (!(key && key.kind === 'static' && key.name === 'default')) {
return isStyled(t)(value, state, lookup);
}
}
if (t.isCallExpression(tag)) {
const callee = t.isSequenceExpression(tag.callee) ? (0, _getSequenceExpressionValue.default)(tag.callee) : tag.callee;
if (t.isIdentifier(callee)) {
const kind = styledBindingKind(callee.name, lookup, state, t, null, callee);
return kind === 'open' || kind === 'namespace';
}
if (isDefaultMember(t, callee, lookup)) {
const kind = styledBindingKind(callee.object.name, lookup, state, t, null, callee.object);
return kind === 'open' || kind === 'namespace';
}
return false;
}
if (!t.isMemberExpression(tag)) return false;
if (isDefaultMember(t, tag.object, lookup) && t.isIdentifier(tag.object.object)) {
const kind = styledBindingKind(tag.object.object.name, lookup, state, t, null, tag.object.object);
if (!kind) return false;
const outer = memberKey(t, tag, lookup);
// `.default` is the factory, so its members use the open skip set.
// An unresolved computed key stays decorated, including when the
// variable is spelled like a helper.
if (outer && outer.kind === 'dynamic') return true;
return decorateKeyedMember('open', outer, tag.property, state, t, tag.object.object.name);
}
if (!t.isIdentifier(tag.object)) return false;
const kind = styledBindingKind(tag.object.name, lookup, state, t, null, tag.object);
return decorateKeyedMember(kind, memberKey(t, tag, lookup), tag.property, state, t, tag.object.name);
};
exports.isStyled = isStyled;
const isCSSHelper = t => (tag, state) => t.isIdentifier(tag) && tag.name === importLocalName('css', state);
exports.isCSSHelper = isCSSHelper;
const isCreateGlobalStyleHelper = t => (tag, state) => t.isIdentifier(tag) && tag.name === importLocalName('createGlobalStyle', state);
exports.isCreateGlobalStyleHelper = isCreateGlobalStyleHelper;
const isKeyframesHelper = t => (tag, state) => t.isIdentifier(tag) && tag.name === importLocalName('keyframes', state);
exports.isKeyframesHelper = isKeyframesHelper;
const isWithThemeHelper = t => (tag, state) => t.isIdentifier(tag) && tag.name === importLocalName('withTheme', state);
exports.isWithThemeHelper = isWithThemeHelper;
const isUseTheme = t => (tag, state) => t.isIdentifier(tag) && tag.name === importLocalName('useTheme', state);
exports.isUseTheme = isUseTheme;
const isHelper = t => (tag, state) => isCreateGlobalStyleHelper(t)(tag, state) || isCSSHelper(t)(tag, state) || isUseTheme(t)(tag, state) || isKeyframesHelper(t)(tag, state) || isWithThemeHelper(t)(tag, state);
exports.isHelper = isHelper;
const isPureHelper = t => (tag, state) => isCreateGlobalStyleHelper(t)(tag, state) || isCSSHelper(t)(tag, state) || isKeyframesHelper(t)(tag, state) || isUseTheme(t)(tag, state) || isWithThemeHelper(t)(tag, state);
exports.isPureHelper = isPureHelper;