Skip to content

feat: support Babel 8 - #431

Merged
quantizor merged 7 commits into
styled-components:mainfrom
VibhuV1:feat/babel-8
Nov 3, 2026
Merged

quantizor merged 7 commits into
styled-components:mainfrom
VibhuV1:feat/babel-8

Conversation

@VibhuV1

@VibhuV1 VibhuV1 commented Oct 30, 2026

Copy link
Copy Markdown
Contributor

Closes #425

Babel 8 is out, and installing this plugin next to @babel/core@8 failed with a peer dependency error. This PR makes the plugin work on Babel 8 while keeping Babel 7 output exactly the same.

What changed

  • Peer range widened to @babel/core: ^7.0.0 || ^8.0.0.
  • css prop crash fixed. Babel 8 removed the t.jSXIdentifier-style builder aliases, so any css prop threw t.jSXIdentifier is not a function. The plugin now uses t.jsxIdentifier, which Babel 7 has too.
  • No more @babel/plugin-syntax-jsx dependency. Its 7.x peer range only accepts Babel 7, so even with the wider range npm still warned ERESOLVE overriding peer dependency and strict installs failed. The plugin now sets the jsx parser option itself, the same few lines that package ran.
  • Build stays CommonJS. Babel 8's preset-env assumes ESM by default, so the root .babelrc now says modules: "commonjs". The built lib/ is byte-identical on Babel 7.

How it was tested

  • Babel 7: pnpm test passes, including a new test that every runtime dependency's @babel/core peer accepts both majors, and a new fixture for the TypeScript parser case.
  • Babel 8: Babel 8 is ESM-only and can't be loaded inside Jest, so pnpm build && pnpm test:babel8 runs the built plugin over the same fixture files with Babel 8. CI runs it after pnpm test.
  • Skipped on Babel 8, with the reason printed:
    • Fixtures that also run @babel/preset-env or other Babel transforms. Their snapshots are mostly Babel's own helpers and output defaults, which differ between majors. I diffed them by hand and the plugin's parts match.
    • One TypeScript fixture whose input (import inside declare namespace) Babel 8's parser now rejects, as TypeScript does.
  • Packed the plugin and installed it with plain npm install next to @babel/core@8, and next to @babel/core@7. No warnings, npm ls is clean, and a small styled-components file transforms identically on both, including componentId.
  • node scripts/compat.js (styled-components v5, v6, v7) and pnpm bench show no differences.

This was developed with AI assistance, and I've reviewed and tested every change.

This is part of my Hacktoberfest contribution. If you're happy with it, would you mind adding the hacktoberfest-accepted label? Thank you for maintaining this plugin!

Widen the @babel/core peer range to ^7.0.0 || ^8.0.0. Babel 8 removed
the t.jSX* builder aliases, so the css prop transform now uses t.jsx*.
Inline the JSX parser option instead of inheriting
@babel/plugin-syntax-jsx, whose 7.x peer range rejects Babel 8 and made
npm warn ERESOLVE. Pin preset-env modules to commonjs for the build so
lib/ stays CommonJS once it is built with Babel 8.

Babel 8 is ESM-only and cannot load inside Jest, so scripts/babel8.js
runs the built plugin over the fixtures on Babel 8 and CI runs it.

Closes styled-components#425
@changeset-bot

changeset-bot Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4e65dec

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

@socket-security

socket-security Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​babel/​preset-typescript@​8.0.71001007289100
Added@​babel/​core@​8.0.9991007995100

View full report

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

✅ No new issues found.

Reviewed changes

  • Widened @babel/core peer — ^7.0.0 || ^8.0.0, and moved @babel/plugin-syntax-jsx from dependencies to devDependencies since its ^7.0.0-0 peer was the install blocker.
  • Inlined the JSX syntax plugin — dropped inherits: @babel/plugin-syntax-jsx; src/index.js now pushes the jsx parser plugin from manipulateOptions, guarded on a typescript parser plugin being present. This is exactly the body of @babel/plugin-syntax-jsx@7.28.6 (confirmed in node_modules), so the parser behavior is unchanged.
  • Babel 8 css prop fix — src/visitors/transpileCssProp.js swaps the removed t.jSXIdentifier/jSXAttribute/jSXExpressionContainer builders for the lowercase jsx* names, which exist on both majors and emit an identical AST.
  • CommonJS build — .babelrc sets modules: "commonjs"; built lib/ stays CJS.
  • Babel 8 test harness — scripts/babel8.js runs the built lib/ over the fixture suite on Babel 8 outside Jest, plus a test:babel8 script, CI step, and AGENTS.md entry. Skips fixtures that also run preset-env/other transforms and one TS fixture Babel 8's parser rejects.
  • Regression coverage — new typescript-parser-plugin-with-options-keeps-angle-bracket-assertions fixture and a runtime dependencies test that would have failed while @babel/plugin-syntax-jsx was a runtime dependency.
  • Docs/metadata — README tested-against line and a minor changeset.

Verified locally: pnpm test (129 passed), pnpm build && pnpm test:babel8 (88 passed / 0 failed / 11 skipped on Babel 8.0.9), node scripts/compat.js (v5/v6/v7 all match base), and Prettier clean. One non-blocking note: the Babel 8 harness skips the headline transpile-css-prop*/add-display-names fixtures because they run preset-env, so their exact snapshots aren't exercised on Babel 8 — other css-prop fixtures do run and cover the jsxIdentifier change. The skip reasoning is documented in the script.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

Babel 8 validates t.identifier, so building an expression for a
hyphenated tag threw even though the tag path never uses it. Build it
only in the branch that needs it.

scripts/babel8.js skipped the fixtures that cover this. Fixtures that
use Babel's own presets or plugins now run with this plugin alone on
both majors and must match.

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

Important

The css-prop crash on Babel 8 is fixed for single-hyphen tag names, but custom-element names the tag regexp rejects still throw under Babel 8.

Reviewed changes

  • Lazy getNameExpression — src/visitors/transpileCssProp.js now builds the name expression inline inside the non-tag branch instead of unconditionally. Babel 8's t.identifier validates its argument, so the old unconditional t.identifier("button-ghost") threw "button-ghost" is not a valid identifier name; Babel 7 does not validate.
  • Expanded Babel 8 harness — scripts/babel8.js no longer skips fixtures that also run preset-env or other transforms. It now runs the plugin alone (presets stripped, only this plugin kept) on Babel 7 and Babel 8 and compares the two outputs, so the skipped count drops from 11 to 1 (only the TS fixture Babel 8's parser rejects).
  • New fixture — css-prop-on-hyphenated-element locks in the single-hyphen <button-ghost> case end to end.

Verified locally: pnpm test (130 passed) and pnpm build && pnpm test:babel8 (99 passed, 0 failed, 1 skipped on Babel 8.0.9).

The tag regexp is narrower than the set of valid custom-element names; the inline comment has the repro and the class of names it misses.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

Comment thread src/visitors/transpileCssProp.js Resolved
Babel 8's TypeScript preset no longer removes the jsx parser plugin, so
adding it broke angle-bracket assertions and generic arrow functions in
.ts files with "Unterminated JSX contents". That preset also no longer
adds jsx to .tsx files, so decide by file extension.

Run .ts and .tsx through the real Babel 8 preset in scripts/babel8.js.

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

ℹ️ No new issues in this delta. The prior review's custom-element tag-detection thread (src/visitors/transpileCssProp.js) is unchanged by these commits and remains open.

Reviewed changes

  • Extension-aware jsx parser guard — src/index.js manipulateOptions now also skips pushing jsx when opts.filename ends in .ts/.mts/.cts (/\.[cm]?ts$/), so Babel 8's TypeScript preset, which runs after this plugin and no longer strips jsx, cannot turn <T>value assertions into JSX parse errors. .tsx still gets jsx.
  • Babel 8 TypeScript harness — scripts/babel8.js now runs TYPESCRIPT_CASES (a.ts, a.tsx) through @babel/preset-typescript@8 against the built lib/, backed by the new babel8-preset-typescript devDependency alias.
  • Parser-option unit tests — test/index.test.js adds a jsx parser option block asserting .ts/.mts/.cts stay off and .tsx/.js turn on, using a typescript-plugin stub that runs after the plugin.

Verified locally: pnpm test (135 passed), pnpm build && pnpm test:babel8 (101 passed, 0 failed, 1 skipped on Babel 8.0.9), and the built plugin against the real @babel/preset-typescript@8 on Babel 8 for .ts and .tsx.

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

A name like my-foo-bar or foo-1 missed the tag pattern and was emitted as
styled(my-foo-bar), which Babel 7 printed as a subtraction and Babel 8
rejects as an invalid identifier.
Names starting lowercase or that are not valid identifiers (clipPath,
foo_bar, my-foo-bar) are elements, matching the JSX transform. A
hyphenated member such as Icons.arrow-up becomes Icons["arrow-up"]
instead of a subtraction.

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

✅ No new issues found. The prior review's custom-element tag-detection finding is fixed by ca1fff7/1d9bc52, and its thread is resolved.

Reviewed changes

  • Replaced regex tag detection with JSX's own rule — src/visitors/transpileCssProp.js now treats a JSXIdentifier as an intrinsic string tag when t.react.isCompatTag(name) || !t.isValidIdentifier(name), so <my-foo-bar>, <foo-1>, <clipPath>, and <foo_bar> compile to styled("...") instead of a broken variable reference. This closes the prior review's open finding.
  • Computed member access for hyphenated members — getNameExpression emits Icons["arrow-up"] for a member whose property is not a valid identifier, instead of the invalid Icons.arrow-up.
  • Expanded fixture — css-prop-on-intrinsic-element-names covers multi-hyphen, digit-segment, SVG camelCase, underscore, and hyphenated-member names end to end.
  • Harness/config cleanup — scripts/babel8.js moves the skip check before reading .babelrc, guards config.plugins, and renames pluginOnly -> matchesBabel7; the changesets split into a minor (Babel 8) and a patch (css-prop names), dropping the now-inaccurate "Babel 7 output is unchanged" line.

Verified locally: pnpm test (135 passed) and pnpm build && pnpm test:babel8 (101 passed, 0 failed, 1 skipped on Babel 8.0.9). Confirmed t.react.isCompatTag and t.isValidIdentifier exist and behave identically on @babel/types 7.29.0 and 8.0.8, that the new rule matches @babel/plugin-transform-react-jsx's effective behavior, and that the new fixture actually runs on Babel 8 (not skipped).

Pullfrog  | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@quantizor

Copy link
Copy Markdown
Collaborator

@VibhuV1 thank you!

<myButton css> after const myButton = styled.button styled the variable
before this branch; the JSX element rule turned it into the unknown DOM
element "myButton". A lowercase-first name that is not plain lowercase
now becomes a string tag only when nothing in scope defines it.

@quantizor quantizor left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thank you for the Babel 8 work and the thorough testing behind it!

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

✅ No new issues found.

Reviewed changes

  • Binding-aware tag detection — src/visitors/transpileCssProp.js now classifies a lowercase-starting element name as a string tag unless it is a valid identifier with no in-scope binding. This restores the pre-PR resolution for camelCase / underscore names like <myButton> and <my_card> (which the prior commit's isCompatTag-only rule had turned into styled("myButton")), while unbound <clipPath> / <foo_bar> / hyphenated names still become string tags. Pure-lowercase names stay string tags, matching main.
  • Regression fixture — css-prop-on-intrinsic-element-names adds bound myButton / my_card cases asserting styled(myButton) / styled(my_card), and the changeset text is corrected to describe that bound-name behavior.

Verified locally: pnpm test (135 passed) and pnpm build && pnpm test:babel8 (101 passed, 0 failed, 1 skipped on Babel 8.0.9). Independently brute-forced the new rule against main's TAG_NAME_REGEXP over all lowercase-starting names: no case flips a main string tag into an identifier, no invalid JS is emitted on the non-tag path, and every difference is a main bug being fixed. t.react.isCompatTag behaves identically on @babel/types 7.29.0 and 8.0.9, and the fixture round-trips.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@quantizor
quantizor merged commit 9e4200b into styled-components:main Nov 3, 2026
6 checks passed
@VibhuV1

VibhuV1 commented Nov 3, 2026

Copy link
Copy Markdown
Contributor Author

Thank you so much, @quantizor! This was my first contribution here and I learned a lot. Thanks too for handling the custom-element names on top of it. Happy to help with anything else Babel 8 related.

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.

Support Babel 8

2 participants