Repository navigation
chore: modernize dependencies and fix css prop import detection - #418
Conversation
🦋 Changeset detectedLatest commit: 5a4f15c The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
There was a problem hiding this comment.
Pull request overview
This PR updates the plugin’s toolchain/dependency stack (Babel, Jest, Prettier, etc.), removes the runtime lodash dependency, and fixes a css-prop transform bug where repeated/incorrect default-import insertion could break isStyled() matching (dropping displayName/componentId). It also ensures styled-component transforms are applied to AST nodes injected by the css-prop transform in cases where Babel’s outer traversal doesn’t revisit newly inserted nodes.
Changes:
- Modernize dependencies and CI/runtime requirements (Babel ~7.26, Jest 29.7, Prettier 3.x, Node matrix updates).
- Fix css prop import detection to avoid repeatedly re-inserting / re-aliasing the
styled-componentsdefault import. - Remove
lodashby inlining a smalldifference()helper and update test fixtures/snapshots accordingly.
Reviewed changes
Copilot reviewed 17 out of 18 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| test/fixtures/transpile-template-literals-without-config/output.js | Snapshot update reflecting newer Babel helper output. |
| test/fixtures/transpile-template-literals-with-config/output.js | Snapshot update reflecting newer Babel helper output. |
| test/fixtures/transpile-css-prop/output.js | Snapshot update reflecting updated helper emission and uid numbering. |
| test/fixtures/transpile-css-prop-all-options-on/output.js | Snapshot update reflecting updated helper emission and regenerated componentId numbering. |
| test/fixtures/transpile-css-prop-add-require/output.js | Snapshot update reflecting updated helper emission / export emission. |
| test/fixtures/transpile-css-prop-add-import/output.js | Snapshot update reflecting updated helper emission / export emission. |
| test/fixtures/transformed-imports-with-jsx-member-expressions/output.js | Snapshot update reflecting updated Babel wildcard interop helper emission. |
| test/fixtures/pre-transpiled/.babelrc | Switch class properties plugin to non-deprecated transform plugin in fixture config. |
| test/fixtures/minify-css-to-use-with-transpilation/output.js | Snapshot update reflecting updated helper emission. |
| test/fixtures/add-display-names/.babelrc | Switch class properties plugin to non-deprecated transform plugin in fixture config. |
| src/visitors/transpileCssProp.js | Fix default-import binding detection; eagerly run styled visitors on injected css-prop declarations. |
| src/minify/index.js | Replace lodash/difference usage with an inline difference() helper. |
| package.json | Dependency bumps, drop lodash, add @babel/core peerDep, add Node engines, rename prepublish to prepublishOnly. |
| .nvmrc | Update default dev Node version to v22. |
| .github/workflows/test.yml | Modernize CI Node matrix and GitHub Actions versions. |
| .github/workflows/codeql-analysis.yml | Update CodeQL GitHub Actions to v3 and checkout to v4. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Drop yarn 3 (.yarn/, .yarnrc.yml, yarn.lock) for pnpm via the packageManager field, with pnpm-workspace.yaml setting minimumReleaseAge to 4320 minutes (3 days) as a supply-chain guard
- Bump Babel deps to ^7.26 across the board; replace the deprecated @babel/plugin-proposal-class-properties with @babel/plugin-transform-class-properties (and update fixture .babelrc files accordingly)
- Bump prettier to ^3.4, rimraf to ^6.0, picomatch to ^4.0, jest to ^29.7, styled-components dev dep to ^6.1.14
- Drop the lodash runtime dependency; inline a four-line difference() helper for the single call site in src/minify
- Add @babel/core to peerDependencies (fixes peer-dep warnings under strict resolvers)
- Add @changesets/cli devDep (pinned 2.31.0), .changeset/{README.md,config.json} scaffolding, changeset/changeset-publish scripts, and a release workflow driven by changesets/action@v1
- Rename prepublish to prepublishOnly; switch internal script references from yarn to pnpm
- Refresh fixture snapshots to absorb cosmetic Babel helper renames (e.g. obj → e) and uid-numbering changes introduced by the newer Babel
- pnpm 11.x requires Node 22.13+, so the matrix drops Node 16/18/19 in favor of the current LTS line (22/24) - actions/checkout@v3 → v4, actions/setup-node@v3 → v4 (with cache: pnpm), github/codeql-action@v2 → v3 - Add pnpm/action-setup@v4 to the test job - .nvmrc bumped to v22 to match
79c0616 to
bcd33e0
Compare
…ase usages
The guard around `addDefault('styled-components')` in transpileCssProp checked an Identifier object as if it were a string key (`!bindings[importName]`), which was always truthy. Under newer @babel/helper-module-imports this means every css={…} usage adds a fresh aliased default import (`_styled`, `_styled1`, …), and importLocalName's cache then reports the alias as the local default name. Downstream isStyled() stops matching the user's `styled.div`, so displayName and componentId quietly disappear. Repeated css={…} usage in a single file with an existing `import styled from 'styled-components'` was effectively broken.
- Replace the broken guard with one that resolves importName to a string and checks `bindings[importBindingName]`
- Eagerly apply the styled-component visitors (minify/displayNameAndId/templateLiterals/pureAnnotation) on the variable inserted by the css-prop transform, since Babel does not reliably re-visit nodes pushed onto Program from inside a Program-enter sub-traversal
- Preserve `computed` and `shorthand` on the rewritten ObjectProperty so that `css={{ [foo]: bar }}` with a non-primitive value keeps its bracketed key (previously failed Babel's validator with "Property key of ObjectProperty expected node to be of a type […] but instead got 'MemberExpression'")
- Thread the visitor path through transpileCssProp's local getName/getNameExpression so the error path uses the right buildCodeFrameError (previously referenced an out-of-scope `path` and would throw ReferenceError instead of the friendly error)
- Drop a trailing isStyled arm that crashed when reached (dereferenced tag.object on a CallExpression); fixing the typo correctly exposed a cascading addConfig over-application not in scope here, no fixture reached the arm
- Extract the shared styled-visitor dispatch into src/visitors/process.js so index.js and the css-prop eager pass share one definition
- Move importLocalName's cache from a process-wide module-level map (cross-file bleed and unbounded growth in long-running watchers) to state.file.set/get; drop the cacheIdentifier no-op option
- Hoist isStyled's repeated importLocalName('default', state) calls into a single local; pre-curry every visitor at plugin init so per-node visits skip closure allocation
- Drop the dead isInjectGlobalHelper detector (styled-components v2 API, removed years ago)
- Add regression fixtures: css-prop-with-existing-styled-import, namespace-default-tag-form, css-prop-object-computed-key-with-nonprimitive-value
bcd33e0 to
1bf7ccb
Compare
…d TaggedTemplate tags
When transpileTemplateLiterals is off, both the css-prop pre-numbering pass and Babel's main TaggedTemplateExpression traversal fire displayNameAndId on the same user-written styled component. The existing idempotency guards only covered the CallExpression shape (via getExistingConfig), so a TaggedTemplate whose tag was already wrapped with .withConfig would get wrapped again, incrementing the componentId counter every time and producing nested .withConfig chains.
Add an early-exit when path.node.tag is `X.withConfig({...})` and the existing arguments object already carries displayName/componentId. Cover with a regression fixture (css-prop-mixed-with-transpile-template-literals-disabled).
Also rewrites the changeset to describe user-facing outcomes instead of internals.
When the only styled-components binding in scope is a namespace import (`import * as styled from 'styled-components'`), the css-prop transform was emitting `styled("div")…`, which fails at runtime because the namespace object isn't callable — the callable lives on `styled.default`. Targeting `<ns>.default` directly triggers the recursive `isStyled` cascade that re-wraps tags with `.withConfig` over and over (the same shape removed in an earlier commit), so instead treat a namespace-only binding as equivalent to "no usable default" and let `addDefault` inject a fresh callable import to route the css-prop component through.
The local-name scan in `importLocalName` also needed a small fix to stop the namespace specifier from clobbering a previously-discovered default specifier in the same file — otherwise the post-`addDefault` bypass-cache lookup would still resolve to the namespace name.
Also tightens two small things from the same review pass: the `Cannot infer name` error message now points at a fully-qualified `https://github.com/...` URL so it auto-links in terminals, and `taggedTagAlreadyConfigured` is called as `(t, path)` instead of `(t)(path)` to avoid a fresh closure allocation per visit in the hot path.
Adds a regression fixture (css-prop-with-namespace-import) covering the namespace + css-prop combination.
…sting withConfig
`taggedTagAlreadyConfigured` was reading `prop.key.name` on every entry of the withConfig argument's `.properties`, which crashed (`Cannot read properties of undefined (reading 'name')`) on shapes like `styled.div.withConfig({ ...cfg })\`...\`` because SpreadElement has no `.key`. Same crash for string-literal keys.
Guard the predicate with `t.isObjectProperty(prop) && t.isIdentifier(prop.key)` before reading `.key.name`. Red/green verified with the new `withconfig-spread-and-css-prop` regression fixture (covers both `{ ...cfg }` and `{ 'string-key': … }` shapes mixed with a css-prop usage that triggers the pre-numbering pass).
Also clarifies the changeset rule in AGENTS.md: declaring an implicit peer dependency that consumers must already have in order to use the plugin is not by itself a major release; only changes that actually break working consumers are.
…ithConfig predicate
The previous commit guarded `taggedTagAlreadyConfigured` against SpreadElement and non-Identifier keys, but three identical `properties.some(prop => […].includes(prop.key.name))` checks remained — two inside `addConfig` and one in the outer visitor's `styled(x).withConfig({…})` arm — and would still throw on `withConfig({ ...spread })` configurations.
Extract a shared `hasDisplayNameOrComponentId` predicate that filters to ObjectProperty entries with Identifier keys, and use it everywhere.
Red/green verified with a pre-transpiled call-form fixture (`withconfig-spread-with-transpile`) that previously crashed inside the outer visitor predicate.
…literal keys
`hasDisplayNameOrComponentId` only matched non-computed Identifier keys, so
`withConfig({ 'displayName': 'X' })`, `withConfig({ ['displayName']: 'X' })`,
and the corresponding `componentId` shapes were treated as not-yet-configured.
The visitor then wrapped tagged templates with a second `.withConfig({...})`
whose auto-generated `displayName` overrode the user's value at runtime (later
withConfig calls compose and the last one wins), and on the pre-transpiled
call form it pushed straight into the user's argument object, emitting a
duplicate `displayName` key.
Recognize StringLiteral keys (covering both quoted and computed-string forms)
in addition to plain Identifier keys, and skip computed Identifier keys (e.g.
`{ [varName]: 'X' }`) since those resolve dynamically.
Adds the `withconfig-string-literal-displayname` regression fixture covering
all four shapes against the pre-fix tree.
Propagating `property.shorthand` produced an ObjectProperty with
`shorthand: true` but a non-Identifier value (`p.$_cssN`) on inputs like
`css={{ color }}`, violating the Babel invariant that shorthand requires
`value` to be the same Identifier as `key`. Babel's codegen happens to be
lenient and emits the long form anyway, so no snapshot changes, but stricter
validators or downstream plugins that branch on `shorthand` could trip.
Force-clear the flag on the rewrite branch; the surrounding fixtures
(including the `color` shorthand in `transpile-css-prop`) cover the output.
The pre-numbering pass previously ran after pushing the new styled-component declaration, so the just-injected node was visited twice: once by the Program traverse and once by the explicit loop over insertedPaths. The second pass was a no-op thanks to the displayNameAndId idempotency guards, but the redundant traverse + visitor invocation still ran for every css prop in the file. Swap the order: pre-number existing user-written components first, then inject, then visit the inserted declaration exactly once. Snapshot tests confirm the counter assignments are unchanged.
Summary
.yarn/,.yarnrc.yml,yarn.lockand addpnpm-lock.yaml.changeset/config.json,README.md, release workflow driven bychangesets/action@v1)pnpm-workspace.yamlwithminimumReleaseAge: 4320(3-day supply-chain delay for fresh dependency versions)@babel/plugin-proposal-class-propertieswith@babel/plugin-transform-class-propertieslodashruntime dependency in favor of a small inlinedifference()helper@babel/coretopeerDependencies(fixes peer-dep warnings under strict resolvers)actions/*@v4,github/codeql-action@v3,.nvmrc→ v22, renameprepublish→prepublishOnlyengines.nodeconstraint and.babelrctarget stays at node 12 — consumers on older Node continue to work, this is the dev-environment matrix onlytranspileCssPropwhere the guard aroundaddDefault('styled-components')always fired (it checked an Identifier object as if it were a string key). Under newer@babel/helper-module-importsthis poisonedimportLocalName's cache with a fresh alias like_styled26, soisStyled()stopped matching user-writtenstyled.divand droppeddisplayName/componentIdentirely on files that had both an existingimport styled from 'styled-components'and one or morecss={…}usagesProgramfrom inside aProgram-enter sub-traversal — and extract the dispatch (processTaggedTemplate/processCallExpression) intosrc/visitors/process.jssoindex.jsand the css-prop visitor share one definitioncomputedandshorthandflags on rewritten ObjectProperty in the css-prop object reducer, socss={{ [computedKey]: nonPrimitiveValue }}no longer fails Babel's validator withProperty key of ObjectProperty expected node to be of a type [...] but instead got "MemberExpression"ReferenceErrorintranspileCssProp's localgetName/getNameExpressionhelpers — thethrow path.buildCodeFrameError(...)referenced an out-of-scopepathvariableisStyledarm that crashed when reached (dereferencedtag.objecton a CallExpression); fixing it correctly exposed a cascadingaddConfigover-application out of scope here, and no fixture reached the armimportLocalName's cache from a module-level map (cross-file bleed in long-running watchers, unbounded growth) to per-filestate.file.set/get; drop thecacheIdentifierno-op optionisStyled's repeatedimportLocalName('default', state)calls into a single localdisplayNameAndId(t),minify(t), etc.) so per-node visits skip closure allocationisInjectGlobalHelperdetector (styled-components v2 API, removed years ago)css-prop-with-existing-styled-import,namespace-default-tag-form,css-prop-object-computed-key-with-nonprimitive-valuestyled-components v7 compatibility
The v7 prerelease keeps the same
main/module/react-nativeentry points and named exports the plugin detects — no plugin-side changes required. The full test suite passes againststyled-components@7.0.0-prerelease-*as well as^6.1.Test plan
pnpm testpasses (52/52) againststyled-components@^6.1.14pnpm testpasses (52/52) againststyled-components@7.0.0-prerelease-*obj→e) and generator uid numbering — no semantic plugin-output changesFixes #400
Fixes #405
Fixes #407