@swc/plugin-styled-components 14.0.0, @swc/core 1.16.2, cssProp: true.
When a css prop interpolates a member chain whose root is module-scoped but whose computed key is a local, the whole expression is hoisted into the generated styled component instead of being passed through as $_css. At runtime the local is out of scope, so the module throws ReferenceError: size is not defined on load.
Input
import styled from 'styled-components'
const Box = styled.div``
const SIZES = { small: { bottom: '8px' }, large: { bottom: '16px' } }
export const Avatar = ({ size }: { size: 'small' | 'large' }) => (
<Box css={`bottom: ${SIZES[size].bottom};`} />
)
Output (trimmed)
var _StyledBox = styled(Box).withConfig({ /* … */ })([
`bottom:`,
`;`
], SIZES[size].bottom); // `size` is not in scope here
export const Avatar = ({ size })=>_jsx(_StyledBox, {});
Expected
The same as babel-plugin-styled-components, and as this plugin already does for ${SIZES[size]} (no trailing property access):
var _StyledBox = styled(Box).withConfig({ /* … */ })([`bottom:`, `;`], (p)=>p.$_css);
export const Avatar = ({ size })=>_jsx(_StyledBox, { $_css: SIZES[size].bottom });
It looks like the closure check in the css-prop transform looks only at the root object of a member expression (SIZES, module scope) and misses identifiers in computed properties further along the chain. ${SIZES[size]} is handled correctly, so the fix for #194 appears to cover a computed member on the root but not deeper chains. Hoisting also places the styled component above const SIZES, which would hit the TDZ even for a fully static chain.
Repro
const { transformSync } = require('@swc/core')
console.log(transformSync(require('fs').readFileSync('input.tsx', 'utf8'), {
filename: 'input.tsx',
jsc: {
parser: { syntax: 'typescript', tsx: true },
target: 'es2022',
transform: { react: { runtime: 'automatic' } },
experimental: { plugins: [['@swc/plugin-styled-components', { cssProp: true }]] }
}
}).code)
We hit this moving a large Vite app from @vitejs/plugin-react + babel-plugin-styled-components to @vitejs/plugin-react-swc; it's the pattern SIZE_MAP[size].linked.bottom in an avatar component that first broke.
@swc/plugin-styled-components14.0.0,@swc/core1.16.2,cssProp: true.When a
cssprop interpolates a member chain whose root is module-scoped but whose computed key is a local, the whole expression is hoisted into the generated styled component instead of being passed through as$_css. At runtime the local is out of scope, so the module throwsReferenceError: size is not definedon load.Input
Output (trimmed)
Expected
The same as
babel-plugin-styled-components, and as this plugin already does for${SIZES[size]}(no trailing property access):It looks like the closure check in the css-prop transform looks only at the root object of a member expression (
SIZES, module scope) and misses identifiers in computed properties further along the chain.${SIZES[size]}is handled correctly, so the fix for #194 appears to cover a computed member on the root but not deeper chains. Hoisting also places the styled component aboveconst SIZES, which would hit the TDZ even for a fully static chain.Repro
We hit this moving a large Vite app from
@vitejs/plugin-react+babel-plugin-styled-componentsto@vitejs/plugin-react-swc; it's the patternSIZE_MAP[size].linked.bottomin an avatar component that first broke.