Skip to content

chore: modernize dependencies and fix css prop import detection - #418

Merged
quantizor merged 16 commits into
mainfrom
claude/modernize-dependencies-dYmTD
May 20, 2026
Merged

quantizor merged 16 commits into
mainfrom
claude/modernize-dependencies-dYmTD

Conversation

@quantizor

@quantizor quantizor commented May 20, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • Switch the dev toolchain from yarn 3 to pnpm 11; drop .yarn/, .yarnrc.yml, yarn.lock and add pnpm-lock.yaml
  • Add @changesets/cli infra (.changeset/config.json, README.md, release workflow driven by changesets/action@v1)
  • Add pnpm-workspace.yaml with minimumReleaseAge: 4320 (3-day supply-chain delay for fresh dependency versions)
  • Bump Babel deps to ^7.26 and replace the deprecated @babel/plugin-proposal-class-properties with @babel/plugin-transform-class-properties
  • Bump prettier to ^3.4, rimraf to ^6.0, picomatch to ^4.0, jest to ^29.7
  • Drop the lodash runtime dependency in favor of a small inline difference() helper
  • Add @babel/core to peerDependencies (fixes peer-dep warnings under strict resolvers)
  • Modernize CI: Node 22/24 matrix (pnpm 11 needs Node 22.13+), actions/*@v4, github/codeql-action@v3, .nvmrc → v22, rename prepublish → prepublishOnly
  • No engines.node constraint and .babelrc target stays at node 12 — consumers on older Node continue to work, this is the dev-environment matrix only
  • Fix the longstanding bug in transpileCssProp where the guard around addDefault('styled-components') always fired (it checked an Identifier object as if it were a string key). Under newer @babel/helper-module-imports this poisoned importLocalName's cache with a fresh alias like _styled26, so isStyled() stopped matching user-written styled.div and dropped displayName/componentId entirely on files that had both an existing import styled from 'styled-components' and one or more css={…} usages
  • Eagerly apply the styled-component visitors 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 — and extract the dispatch (processTaggedTemplate / processCallExpression) into src/visitors/process.js so index.js and the css-prop visitor share one definition
  • Preserve computed and shorthand flags on rewritten ObjectProperty in the css-prop object reducer, so css={{ [computedKey]: nonPrimitiveValue }} no longer fails Babel's validator with Property key of ObjectProperty expected node to be of a type [...] but instead got "MemberExpression"
  • Fix a latent ReferenceError in transpileCssProp's local getName / getNameExpression helpers — the throw path.buildCodeFrameError(...) referenced an out-of-scope path variable
  • Drop a trailing isStyled arm that crashed when reached (dereferenced tag.object on a CallExpression); fixing it correctly exposed a cascading addConfig over-application out of scope here, and no fixture reached the arm
  • Move importLocalName's cache from a module-level map (cross-file bleed in long-running watchers, unbounded growth) to per-file 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 (displayNameAndId(t), minify(t), etc.) so per-node visits skip closure allocation
  • Drop the dead isInjectGlobalHelper detector (styled-components v2 API, removed years ago)
  • New regression fixtures: css-prop-with-existing-styled-import, namespace-default-tag-form, css-prop-object-computed-key-with-nonprimitive-value

styled-components v7 compatibility

The v7 prerelease keeps the same main/module/react-native entry points and named exports the plugin detects — no plugin-side changes required. The full test suite passes against styled-components@7.0.0-prerelease-* as well as ^6.1.

Test plan

  • pnpm test passes (52/52) against styled-components@^6.1.14
  • pnpm test passes (52/52) against styled-components@7.0.0-prerelease-*
  • Snapshot diffs limited to Babel runtime helper renames (obj → e) and generator uid numbering — no semantic plugin-output changes
  • CI runs green on Node 22/24

Fixes #400
Fixes #405
Fixes #407

Copilot AI review requested due to automatic review settings May 20, 2026 18:56
@changeset-bot

changeset-bot Bot commented May 20, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5a4f15c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
babel-plugin-styled-components Minor

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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-components default import.
  • Remove lodash by inlining a small difference() 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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 26 out of 31 changed files in this pull request and generated 5 comments.

Comment thread src/utils/detectors.js Resolved
Comment thread src/minify/index.js Resolved
Comment thread package.json Resolved
Comment thread .github/workflows/test.yml Resolved
Comment thread src/visitors/transpileCssProp.js Resolved

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 32 out of 37 changed files in this pull request and generated 2 comments.

Comment thread src/utils/detectors.js Resolved
Comment thread src/visitors/transpileCssProp.js Resolved

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 32 out of 37 changed files in this pull request and generated 1 comment.

Comment thread src/visitors/transpileCssProp.js Resolved
claude added 2 commits May 20, 2026 20:13
- 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
@quantizor
quantizor force-pushed the claude/modernize-dependencies-dYmTD branch from 79c0616 to bcd33e0 Compare May 20, 2026 20:14
claude added 2 commits May 20, 2026 20:30
…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
@quantizor
quantizor force-pushed the claude/modernize-dependencies-dYmTD branch from bcd33e0 to 1bf7ccb Compare May 20, 2026 20:30

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 35 out of 40 changed files in this pull request and generated 1 comment.

Comment thread src/visitors/transpileCssProp.js Resolved
…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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 39 out of 44 changed files in this pull request and generated 3 comments.

Comment thread src/visitors/transpileCssProp.js Resolved
Comment thread src/visitors/transpileCssProp.js Resolved
Comment thread src/visitors/displayNameAndId.js Resolved
claude added 2 commits May 20, 2026 21:14
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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 42 out of 47 changed files in this pull request and generated 2 comments.

Comment thread src/visitors/displayNameAndId.js Resolved
Comment thread .changeset/modernize-dependencies.md Resolved
…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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 45 out of 50 changed files in this pull request and generated 1 comment.

Comment thread src/visitors/displayNameAndId.js Resolved
claude added 2 commits May 20, 2026 22:01
…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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 51 out of 56 changed files in this pull request and generated 2 comments.

Comment thread src/visitors/transpileCssProp.js Resolved
Comment thread .github/workflows/release.yml Resolved
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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 51 out of 56 changed files in this pull request and generated 1 comment.

Comment thread src/visitors/transpileCssProp.js Resolved
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.
@quantizor
quantizor merged commit a40e3f7 into main May 20, 2026
2 checks passed
@quantizor
quantizor deleted the claude/modernize-dependencies-dYmTD branch May 20, 2026 23:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

replace lodash with lodash.difference Missing peerDependencies on babel-plugin-styled-components

3 participants