Skip to content

Commit 0c5f21d

Browse files
quantizorclaude
andauthored
fix: css-prop key shadowing, ts-jest detection, import aliasing (#421)
* fix(css-prop): treat plain object keys as literal property names A `css={{ position: 'absolute' }}` object key is a literal property name, not a reference to a binding, even when the local scope defines a same-named variable. The reducer in transpileCssProp incorrectly extracted such keys as prop interpolations, producing invalid output like `p.$_css: 'absolute'`. Require `property.computed` in the key-rewrite check so only `[expr]` keys are treated as scope references. Closes #409. * fix(detection): recognize TypeScript's __importDefault helper The plugin already understands Babel's `_interopRequireDefault(require(...))` shape and tracks the resulting local binding for downstream styled detection. TypeScript's compiler emits a functionally identical helper named `__importDefault`, which previously slipped past the check so files compiled through tsc / ts-jest never picked up displayName or componentId. Match either helper name in assignStyledRequired. Closes #406, #343. * feat(detection): follow local alias of the styled import TypeScript theme-typing setups re-bind the styled import through a local const so the type can be narrowed: const styled = baseStyled as ThemedStyledInterface<MyTheme> After type-stripping Babel sees a plain `const styled = baseStyled`. The detector previously only matched the exact import binding name, so `styled.div` no longer resolved and the declaration lost displayName / componentId. Walk single-identifier (and TSAsExpression / TSTypeAssertion) alias chains lazily when the direct-name check misses. Adds regression fixtures for the alias case (#238), the sc v6 named-import shape, and chained `withConfig(getConfig())` augmentation. * docs: state the tested matrix in README Adds a short Requirements section above Quick Start naming the versions exercised in CI. No enforcement change; the peer ranges in package.json remain unchanged so existing installs keep working. * feat(css-prop): add cssPropImportPath option for React Native targets The css-prop transform auto-injects `import _styled from 'styled-components'` when the file has no existing styled import. That hardcoded path was the only piece blocking React Native from using the css-prop syntax: the rest of the transform already produces the right shape, since PascalCase JSX names route through the identifier branch and emit `styled(View)` rather than `styled('View')`. Introduce a `cssPropImportPath` option naming the package to inject from. Defaults to `'styled-components'` (existing behavior). RN consumers can set it to `'styled-components/native'` so the injected import resolves to the correct runtime. Closes #272. --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent d0add03 commit 0c5f21d

27 files changed

Lines changed: 322 additions & 8 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'babel-plugin-styled-components': minor
3+
---
4+
5+
Add a `cssPropImportPath` option to control which package the css-prop transform auto-imports `styled` from when the file has no existing styled import. Defaults to `'styled-components'` (existing behavior). React Native targets can set it to `'styled-components/native'` so the auto-injected import resolves to the right runtime.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'babel-plugin-styled-components': minor
3+
---
4+
5+
Detect styled declarations that go through a local alias of the import, including the TypeScript theme-typing pattern `const styled = baseStyled as ThemedStyledInterface<MyTheme>`. After type-stripping Babel sees a plain `const styled = baseStyled`, and the detector now follows single-identifier alias chains so `styled.div` resolves back to the original import.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'babel-plugin-styled-components': patch
3+
---
4+
5+
Fix invalid output when a `css={{ ... }}` object key matches a local binding name (e.g. `({ position }) => <div css={{ position: 'absolute' }} />`). The reducer no longer mis-treats non-computed property names as scope references, so plain keys stay literal while only computed `[expr]` keys are extracted as prop interpolations.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'babel-plugin-styled-components': patch
3+
---
4+
5+
Recognize TypeScript's `__importDefault` interop helper alongside Babel's `_interopRequireDefault`. Files compiled through `tsc` / `ts-jest` (which emit `var sc_1 = __importDefault(require('styled-components'))`) now flow into the same detection path as Babel-compiled output, so styled declarations downstream pick up `displayName` and `componentId` as expected.

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ This plugin is a highly recommended supplement to the base styled-components lib
66
- better debugging through automatic annotation of your styled components based on their context in the file system, etc.
77
- various types of minification for styles and the tagged template literals styled-components uses
88

9+
## Requirements
10+
11+
This plugin is tested against:
12+
13+
- `@babel/core` `^7`
14+
- `styled-components` `>= 6` (earlier majors may still work but aren't exercised in CI)
15+
916
## Quick start
1017

1118
Install the plugin first:
@@ -22,6 +29,13 @@ Then add it to your babel configuration:
2229
}
2330
```
2431

32+
## Options
33+
34+
Full option reference lives on the [styled-components documentation site](https://www.styled-components.com/docs/tooling#babel-plugin). A couple worth flagging here:
35+
36+
- `topLevelImportPaths` (`string[]`): additional module specifiers whose `styled` export should be recognized alongside `styled-components`. Useful for libraries that re-export the styled-components API.
37+
- `cssPropImportPath` (`string`, default `'styled-components'`): which package the css-prop transform should auto-import `styled` from when the file doesn't already have a styled import. Set to `'styled-components/native'` for React Native targets.
38+
2539
## Changelog
2640

2741
See [Github Releases](https://github.com/styled-components/babel-plugin-styled-components/releases)

src/utils/detectors.js

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,37 @@ export const importLocalName = (name, state, options = {}) => {
7575
return localName
7676
}
7777

78+
// Follow `const X = Y` (and TS `const X = Y as Type`) chains so a local
79+
// re-binding of the styled import still resolves to it. Lazy: only walks
80+
// the chain when the direct-name check misses, so the hot path is unchanged.
81+
const resolvesToDefaultLocal = (name, defaultLocal, state, t) => {
82+
if (!name || !defaultLocal) return false
83+
if (name === defaultLocal) return true
84+
const scope = state.file.path.scope
85+
const visited = new Set([name])
86+
let current = name
87+
while (true) {
88+
const binding = scope.getBinding(current)
89+
if (!binding || !binding.path.isVariableDeclarator()) return false
90+
const init = binding.path.node.init
91+
if (!init) return false
92+
let nextName = null
93+
if (t.isIdentifier(init)) {
94+
nextName = init.name
95+
} else if (
96+
(init.type === 'TSAsExpression' || init.type === 'TSTypeAssertion') &&
97+
t.isIdentifier(init.expression)
98+
) {
99+
nextName = init.expression.name
100+
}
101+
if (!nextName) return false
102+
if (nextName === defaultLocal) return true
103+
if (visited.has(nextName)) return false
104+
visited.add(nextName)
105+
current = nextName
106+
}
107+
}
108+
78109
export const isStyled = t => (tag, state) => {
79110
if (
80111
t.isCallExpression(tag) &&
@@ -91,14 +122,16 @@ export const isStyled = t => (tag, state) => {
91122
return isStyled(t)(getSequenceExpressionValue(tag.callee), state)
92123
} else {
93124
const defaultLocal = importLocalName('default', state)
125+
const matchesDefault = name =>
126+
resolvesToDefaultLocal(name, defaultLocal, state, t)
94127
return (
95128
(t.isMemberExpression(tag) &&
96-
tag.object.name === defaultLocal &&
129+
matchesDefault(tag.object.name) &&
97130
!isHelper(t)(tag.property, state)) ||
98-
(t.isCallExpression(tag) && tag.callee.name === defaultLocal) ||
131+
(t.isCallExpression(tag) && matchesDefault(tag.callee.name)) ||
99132
(t.isCallExpression(tag) &&
100133
t.isSequenceExpression(tag.callee) &&
101-
getSequenceExpressionValue(tag.callee).name === defaultLocal) ||
134+
matchesDefault(getSequenceExpressionValue(tag.callee).name)) ||
102135
// styled.default.div``, styled.default.something() — require() forms
103136
(state.styledRequired &&
104137
t.isMemberExpression(tag) &&

src/utils/options.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,6 @@ export const useNamespace = state => {
2727
export const usePureAnnotation = state => getOption(state, 'pure', false)
2828

2929
export const useCssProp = state => getOption(state, 'cssProp', true)
30+
31+
export const useCssPropImportPath = state =>
32+
getOption(state, 'cssPropImportPath', 'styled-components')

src/visitors/assignStyledRequired.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ export default t => (path, state) => {
66
if (
77
t.isCallExpression(path.node.init) &&
88
t.isIdentifier(path.node.init.callee) &&
9-
init.callee.name === '_interopRequireDefault' &&
9+
// `_interopRequireDefault` is Babel's CJS interop helper; `__importDefault`
10+
// is TypeScript's. Both wrap a require() so the caller can reach `.default`.
11+
(init.callee.name === '_interopRequireDefault' ||
12+
init.callee.name === '__importDefault') &&
1013
init.arguments &&
1114
init.arguments[0]
1215
) {
13-
// _interopRequireDefault(require())
1416
init = path.node.init.arguments[0];
1517
}
1618

src/visitors/transpileCssProp.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Most of this code was taken from @satya164's babel-plugin-css-prop
22
import { addDefault } from '@babel/helper-module-imports'
33
import { importLocalName } from '../utils/detectors'
4-
import { useCssProp } from '../utils/options'
4+
import { useCssProp, useCssPropImportPath } from '../utils/options'
55
import { processCallExpression, processTaggedTemplate } from './process'
66

77
const TAG_NAME_REGEXP = /^[a-z][a-z\d]*(\-[a-z][a-z\d]*)?$/
@@ -68,7 +68,7 @@ export default t => {
6868
// not directly callable, so treat it the same as "no default binding" and
6969
// inject a fresh default import to use as the css-prop callee.
7070
if (!importBinding || importBindingIsNamespace) {
71-
addDefault(path, 'styled-components', {
71+
addDefault(path, useCssPropImportPath(state), {
7272
nameHint: 'styled',
7373
})
7474

@@ -174,8 +174,12 @@ export default t => {
174174
if (
175175
t.isMemberExpression(property.key) ||
176176
t.isCallExpression(property.key) ||
177-
// checking for css={{[something]: something}}
177+
// checking for css={{[something]: something}}; a plain (non-computed)
178+
// identifier key is a literal property name and never resolves to a
179+
// binding, even if the local scope happens to define a same-named
180+
// variable. Only computed keys reference the surrounding scope.
178181
(t.isIdentifier(property.key) &&
182+
property.computed &&
179183
path.scope.hasBinding(property.key.name) &&
180184
// but not a object reference shorthand like css={{ color }}
181185
(t.isIdentifier(property.value)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"plugins": [
3+
[
4+
"../../../src",
5+
{
6+
"ssr": false,
7+
"fileName": false,
8+
"transpileTemplateLiterals": false
9+
}
10+
]
11+
]
12+
}

0 commit comments

Comments
 (0)