Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/named-import-helper-order.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'babel-plugin-styled-components': patch
---

Helpers imported before `styled` in a named import are recognized now. In `import { css, styled } from 'styled-components'`, `css` and `keyframes` are minified, and with pure annotations turned on they are marked pure.
5 changes: 5 additions & 0 deletions .changeset/require-non-tag-exports.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'babel-plugin-styled-components': patch
---

A binding that is the styled factory keeps display names and component ids for properties this plugin does not recognize. On styled-components v7 that factory is a proxy, so `styled.box`, `styled.foo`, and `styled.feNotAFilter` are real components. `const styled = require('styled-components')`, `import sc from 'styled-components'`, `sc.default`, `ns.default`, and `__importDefault(require('styled-components')).default` are that factory. Known non-tag exports stay plain, including `sc.css`, `sc.keyframes`, `sc.createGlobalStyle`, `sc['createGlobalStyle']`, `sc.isStyledComponent`, and `sc.default.css`. Names on `Object.prototype` or `Function.prototype` stay plain too, including `sc.hasOwnProperty`, `sc.toString`, `sc.bind`, `sc.__lookupGetter__`, `sc.name`, and `sc.__proto__`. A computed key is resolved when it is a string, a template whose parts all fold to strings, or a const initialized to one of those, so `const name = 'css'; sc[name]`, `` sc[`css`] ``, and `` sc[`${'css'}`] `` stay plain, while `const css = 'div'; sc[css]` and `sc.default[css]` keep their ids. A key this plugin cannot resolve stays plain on `require()` and on an alias that is not named `styled`, so `let dynamic = 'css'; sc[dynamic]` returns the helper instead of throwing. `styled[dynamic]` on an import named `styled` is still decorated. A `let` or `var` that starts as the factory keeps its display name on a straight-line use written before the reassignment. A use inside a nested function, or inside a loop that also reassigns the binding, is left alone, so that function and the next loop iteration do not throw.
10 changes: 10 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Commands
- `pnpm build`: compile `src/` to `lib/`; run it to verify every set of changes. Don't start a dev server unless asked
- `pnpm bench [--base <ref>] [--config default|all|bare] [--rounds N] [--normalize-aliases]`: interleaved A/B of the plugin at `<ref>` (default `origin/main`) against the working tree over generated inputs; fails when outputs differ
- `pnpm bench:profile [--input small|medium|large|large-cjs]`: CPU profile with the plugin frames that drive Babel's work
- `node scripts/compat.js [--base <ref>]`: styled-components v5, v6, and v7 prerelease smoke against the working tree and `<ref>` (default `origin/main`)
- `pnpm style`: Prettier over `src/`
- `pnpm changeset`: add a release note

Expand Down Expand Up @@ -86,6 +87,15 @@ Testing
- Avoid snapshot churn. A change rewrites only the snapshots its behavior requires, and a hand-written expected output is changed only when the expectation itself was wrong, with the reason in the commit.
- Jest `roots` is `test/`, so agent worktrees under `.claude/worktrees/` stay out of the run.

Review

- Every behavior change is checked against styled-components v5 (latest 5.x), v6 (latest 6.x), and v7 (latest prerelease dist-tag). `node scripts/compat.js` compiles smoke programs with the branch and with main, then executes the output (`renderToString` plus `ServerStyleSheet`) against each version. Generated CSS, displayName, and componentId match main except the intended change. No runtime errors.
- Module formats: the plugin loads from CJS configs (`babel.config.js`, `.babelrc`) and ESM configs (`babel.config.mjs`, a `"type": "module"` project). Input covers ESM (default, named, aliased, and namespace imports) and CJS (`require`, `require(...).default`, destructured require). Babel's modules transform is checked both ways (preset-env `modules: false` and `'commonjs'`). Transformed output runs as native ESM and as CJS.
- Known interop facts: v5 and v6 expose styled as `module.exports.default` and have no `exports` map, so a default import under native Node ESM yields the module object (a packaging limit, not a plugin bug). v7 `require()` returns the callable styled and that function is a proxy, so an unknown key is a component. A binding that is the factory (require, a default import, a named `styled` import, `.default` / `['default']` of a require or a namespace, and an interop default helper) decorates unknown properties and skips known non-tag exports and any key for which `name in Object.prototype || name in Function.prototype` (no DOM or SVG tag is on those prototypes). A namespace import uses the same member rules, because a CommonJS transform binds it to the module exports. A local that shadows the binding, or a use that appears after a reassignment, never receives `.withConfig`. A straight-line use in the binding's own scope that is written before the reassignment keeps its display name and component id. A use inside a nested function, or inside a loop that also reassigns the binding, stays plain, because that code can run after the assignment. A computed key resolves when it is a string literal, a template whose parts all fold to strings, or a const binding of those, and then uses the skip set. A key that does not resolve stays plain on a require or on an alias whose name is not the styled import's local name. `styled[dynamic]` and a default import's own local name stay decorated, matching the previous plugin, and that can throw if the runtime value is a helper.
- A change to `isStyled` or the helper detectors is judged only after generating the full shape matrix: every import form (default, named, aliased, namespace, require, interop helper) x access form (dot, `.default`, computed literal, computed variable, call, `.attrs` / `.withConfig` chain, sequence) x export (intrinsic tags, known helpers, object methods, unknown names) x option preset. `node scripts/compat.js` runs a compact version of that matrix against v5, v6, and the latest v7 prerelease. A difference from the last release is a regression unless it is the intended fix.
- Output changes (hash shifts, newly recognized forms, minify differences) need a changeset line even when intended.
- Perf: run `pnpm bench` on main and the branch for changes that touch visitors or detectors. A slower branch is a regression.

Performance

- Hot-path code earns microbenchmarks before claiming it is optimized: A/B against the prior code at several input sizes (`pnpm bench`). Its run-to-run noise is large on big inputs, so repeat a run before trusting a single-digit difference. Babel plugins run per file per build, so per-node allocation and repeated AST walks matter.
Expand Down
Loading
Loading