Skip to content

feat: support Babel 8 - #431

Open
VibhuV1 wants to merge 3 commits into
styled-components:mainfrom
VibhuV1:feat/babel-8
Open

VibhuV1 wants to merge 3 commits into
styled-components:mainfrom
VibhuV1:feat/babel-8

Conversation

@VibhuV1

@VibhuV1 VibhuV1 commented Oct 30, 2026

Copy link
Copy Markdown

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: 8bed53b

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) | 𝕏

} else {
styled = t.callExpression(importName, [nameExpression])
styled = t.callExpression(importName, [
getNameExpression(elem.node.name, t, path),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

TAG_NAME_REGEXP (/^[a-z][a-z\d]*(\-[a-z][a-z\d]*)?$/) matches only names with a single hyphen followed by a letter, so valid custom-element names like <my-foo-bar> or <foo-1> fall into this branch and getNameExpression builds t.identifier("my-foo-bar"), which Babel 8 rejects with "my-foo-bar" is not a valid identifier name. On Babel 7 these didn't throw but emitted broken _styled(my-foo-bar) (invalid JS), so this PR makes the single-hyphen case work while leaving the multi-hyphen / digit-segment class crashing. Treating any lowercase or hyphenated name as a string tag (Babel's own JSX heuristic) closes it.

Technical details
# Custom-element names that miss TAG_NAME_REGEXP throw on Babel 8

## Affected sites
- `src/visitors/transpileCssProp.js:11` — `TAG_NAME_REGEXP` only allows one optional `-segment` and requires a letter right after each hyphen
- `src/visitors/transpileCssProp.js:108-110` — non-tag branch calls `getNameExpression`, whose `t.identifier(node.name)` throws on Babel 8 for invalid identifiers

## Required outcome
- A `css` prop on any valid custom element (`<my-foo-bar>`, `<foo-1>`, `<my-element>`) must produce `styled("name")` on both Babel 7 and Babel 8, without throwing.

## Repro (built `lib/`, Babel 8.0.9)
- `<my-element css="color: red" />` -> OK, `styled("my-element")`
- `<my-foo-bar css="color: red" />` -> throws `"my-foo-bar" is not a valid identifier name`
- `<foo-1 css="color: red" />` -> throws `"foo-1" is not a valid identifier name`

## Suggested approach (optional)
- Broaden the tag test, e.g. treat the name as a string tag when it contains a hyphen or starts lowercase (`/^[a-z]/-ish`), matching how `@babel/plugin-transform-react-jsx` classifies intrinsic elements. Keep the `t.identifier` path for actual component references.
- Add a multi-hyphen fixture (`<my-foo-bar>`) next to the new single-hyphen one.

## Open questions for the human (optional)
- Is treating all hyphenated lowercase names as string tags acceptable, given Babel 7 previously emitted `_styled(my-foo-bar)` for these?

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) | 𝕏

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

1 participant