Skip to content

styled-components: css prop hoists ${OBJ[local].prop} out of its closure (ReferenceError) #659

Description

@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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions