Skip to content

Minifier and import-detection fixes, faster transforms, pnpm 12 - #429

Merged
quantizor merged 18 commits into
mainfrom
fix/line-comment-string-aware
Oct 22, 2026
Merged

quantizor merged 18 commits into
mainfrom
fix/line-comment-string-aware

Conversation

@quantizor

@quantizor quantizor commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes

Follow-up to #427.

Minification dropping rules. A line comment after a string containing an apostrophe, escaped quote, or parenthesis survived minification. styled-components then treated the rest of the joined template as a comment and dropped it:

const Quote = styled.p`
  content: "it's"; // apostrophe
  color: red;      // ← this and everything below was dropped
`

The same miscount could strip spaces inside a later string (' a ; b ' became ' a;b '). The minifier now follows strings and parentheses the way the styled-components runtime does.

Finding the styled-components import. Several shapes were missed or mishandled. Each is now covered by a fixture:

  • An import inside a TypeScript or Flow declare module block was treated as real and could hide the actual import.
  • import * as styled lost styled.default.x decoration once the file also used the css prop or a default import.
  • styled.default(Component), through a namespace import or require, emitted broken code (withConfig.withConfig(...)).
  • import type / import typeof counted as the real import.
  • The css prop crashed in CommonJS script files without a styled-components import, or with the require after the prop.
  • import { styled as sc } and const sc = require('styled-components') weren't recognized under their real names. A bare global styled beside a require is still recognized, so existing componentIds don't move.

Faster transforms

The plugin's own time per file drops about 2–4× on typical files, and much more on CommonJS files with many css props (one large generated file went from about 1.8 s to about 65 ms). Emitted code is unchanged except that a file whose css props need styled-components injected now gets one import instead of one alias per prop.

  • Import lookup reads the file's top-level statements instead of walking the whole file each time.
  • css props share one injected default import per file.
  • Whether a call is styled is checked once per call instead of up to four times.
  • topLevelImportPaths patterns compile once per config.

pnpm bench compares any git ref against the working tree over generated inputs and fails if the outputs differ. pnpm bench:profile prints a CPU profile summary.

Tooling

  • pnpm 12 with the global virtual store for local worktrees. CI uses the per-project store, because the global one fails to install on Windows runners.
  • Tests now also run on Windows, and .gitattributes keeps checkouts LF.
  • Fixture error snapshots hold only the message and code frame, with the working directory shown as <cwd> on every OS. This comes from a small patch to babel-test, and a test fails if any fixture contains an absolute path.
  • Jest is scoped to test/.
  • AGENTS.md is refreshed for contributors' tools.

@changeset-bot

changeset-bot Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 33c6157

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 Patch

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

@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 — the minifier's string and paren tracking is rewritten to match the styled-components runtime, plus a pnpm 12 migration.

  • String-aware comment stripping — stripLineComment is now a single pass that tracks the open quote (honoring backslash escapes) and counts parens only outside strings. A line comment after content: "it's" or content: "(" is now stripped instead of swallowing the rest of the joined template. Paren depth still resets per line, so an unmatched ) from a previous line is ignored, as before.
  • String-aware symbol compression — compressSymbols tracks quote state across fragments via the new quoteOpenAfter helper instead of counting quote characters on the accumulated output, so whitespace inside a later string (' a ; b ') is preserved. This also drops the O(n²) rescan.
  • Tests — adds regression coverage for other-quote-type strings, escaped quotes, parens inside strings, multi-line selector and at-rule preludes, and compressSymbols quote tracking.
  • Tooling — pnpm 12.14.0 with virtualStoreType: global, pnpm/action-setup@v6.1.0, and Jest scoped to test/.

I verified the full suite passes (76 tests) and pnpm build compiles. I also fuzzed the old and new implementations of both functions across quote/paren/backslash/symbol alphabets: every divergence involves escaped quotes or backslashes outside a string, where the new behavior is the more CSS-correct one, and none is a regression on realistic input.

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

@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 — the seven commits since the prior pullfrog review are performance refactors to import lookup and the css-prop transform, plus regression fixtures, sitting alongside the already-reviewed minify fix.

  • Faster import lookup — importLocalName reads Program.body directly for top-level imports and only falls back to a full Program.traverse when a TS/Flow module declaration (TSModuleDeclaration/DeclareModule, including export-wrapped) is present.
  • One injected import per file — transpileCssProp caches the injected styled-components default import on state.file, so a file whose css props need an injection now gets one import instead of one alias per prop. This is the only emitted-code shape change and is runtime-equivalent.
  • Single isStyled check per call — displayNameAndId curries isStyled once per plugin and extracts calleeNeedsConfig, replacing four repeated isStyled calls and the inline OR chain with an equivalent predicate.
  • Matchers compiled once — useTopLevelImportPathMatchers memoizes compiled picomatch matchers per topLevelImportPaths value in a WeakMap.
  • Regression fixtures — pin import lookup for ambient module declarations and exported ambient namespaces, the single injected import, and multiple css props under a namespace import.

I checked the refactors for behavior drift by compiling a 25-snippet corpus (tagged/call/attrs/withConfig shapes, namespace and rebind forms, require/interop/sequence shapes, TS ambient modules, and css props) across five configs with the plugin built at ffa23b0 versus c032536. The only output divergences are the intended one-import-per-file cleanup; every other result is byte-identical. The full suite (80 tests) and pnpm build both pass.

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

@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 critical issues — one minor documentation consistency note.

Reviewed changes — docs-only delta on top of the previously reviewed minify fix and perf refactors.

  • Added .changeset/faster-transforms.md describing the transform speedup.
  • Added an AGENTS.md testing rule noting the css-prop fixture family needs one fixture built without @babel/preset-env (the CommonJS transform folds repeated imports into one require, hiding import-shape bugs).

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

Comment thread .changeset/faster-transforms.md

@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 — the six commits since the prior pullfrog review fix two import-detection bugs and record them.

  • Ambient imports ignored — importLocalName now scans only the top level of Program.body, so a styled-components import nested in a TypeScript or Flow declare module / declare namespace body no longer masquerades as a real binding. Components in files with such ambient imports get their displayName/componentId back.
  • Namespace imports tracked separately — the new importNamespaceNames records import * as X from 'styled-components' bindings, so X.default.y is recognized even when a default import is also present or injected by the css-prop transform.
  • Regression fixtures — pin ambient imports in TypeScript declare module, Flow declare module, exported ambient namespaces, and import-free files; namespace-alongside-default and aliased-namespace-with-css-prop; three existing fixture outputs shift counter values where previously-unrecognized components now receive config, which is the intended effect.
  • Docs — two patch changesets for the fixes, shortened changeset wording, and an AGENTS.md clarification that a changeset may state its own release's measured speedup.

I ran the full suite (84 passed) and pnpm build (17 files compiled). The ambient fix reads only top-level declarations and the namespace arm is gated behind the existing X.default. member shape, so neither widens recognition beyond genuine styled-components bindings.

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

@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 — the two commits since the prior pullfrog review add a benchmark/profiler toolchain and document it.

  • A/B benchmark — scripts/bench/index.js transforms generated inputs (small/medium/large/large-cjs) with the plugin built at a git ref (--base, default origin/main) and the working tree, interleaves runs, and reports each variant's median time minus the no-plugin baseline. It exits non-zero when outputs differ, and hints at --normalize-aliases when only the injected _styledN aliases differ.
  • Input generator and build helpers — gen.js synthesizes realistic styled, css-prop, and CommonJS inputs; build-ref.js extracts src at a ref via git archive and compiles it in a temp dir linked against the repo's node_modules, never writing the tracked lib/.
  • Profiler — scripts/bench/profile.js records a .cpuprofile over one generated input and ranks self time, inclusive time, and the nearest plugin callers of a target frame.
  • Docs — AGENTS.md gains the pnpm bench and pnpm bench:profile commands; the performance rule now points at pnpm bench.

I smoke-tested both. pnpm bench --rounds 1 flags the intended one-import-per-file output change on large-cjs.js with the alias hint, and --normalize-aliases clears it while showing current far below base; pnpm bench:profile --input small --runs 3 prints the ranked frames. The full suite and pnpm build still pass.

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

@quantizor
quantizor force-pushed the fix/line-comment-string-aware branch from 6502e13 to 4a48ca7 Compare October 22, 2026 07:28
@quantizor quantizor changed the title fix(minify): follow strings and parens the way the runtime does Minifier and import-detection fixes, faster transforms, pnpm 12 Oct 22, 2026

@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 — the four commits since the prior pullfrog review make the fixture snapshots machine-path-free and run CI on Windows.

  • Machine-path-free error snapshots — patches/babel-test@0.2.4.patch makes babel-test snapshot the error message (which carries Babel's code frame) instead of the stack, replaces the working directory with <cwd> in either separator style and any drive-letter case, and rewrites the rest of each such path with forward slashes. I confirmed the patch applies cleanly to the pinned babel-test@0.2.4, that its sha256 matches the patchedDependencies hash in pnpm-lock.yaml, and that the normalization collapses both C:\... and /home/... cwds to <cwd>/....
  • Absolute-path leak test — test/index.test.js scans every output.js/error.js for POSIX, drive-letter, and UNC absolute paths and fails the suite on any leak.
  • Cross-platform CI — the test workflow gains a windows-latest/Node 24 job beside Ubuntu 22/24, sets PNPM_CONFIG_VIRTUAL_STORE_TYPE=project (pnpm's documented env override for virtualStoreType, which otherwise fails on Windows with the global layout), and a new .gitattributes (* text=auto eol=lf) keeps checkouts LF so snapshots match. The same env is set on the release workflow.
  • Docs — AGENTS.md records the no-machine-paths snapshot rule and the CI virtual-store override.

I ran the suite with the patch applied (86 passed) and dry-run verified the patch against the installed babel-test. The Windows job on this head was still pending at review time, so the Windows snapshot path is verified by the normalization behavior rather than a green run.

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

@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 — the six commits added since the prior pullfrog review (the branch was rebased; the net delta is four import-detection fixes plus formatting and docs) each close a real detection gap and ship a fixture.

  • Type-only imports ignored — importLocalName and importNamespaceNames skip importKind: 'type' | 'typeof' at both declaration and specifier level, so import type { styled } no longer counts as the real binding (and the css prop no longer emits a call to an erased identifier).
  • Real local names — a named styled import now resolves to specifier.local.name, so import { styled as sc } is recognized; importLocalName('default') starts from the require variable name, and a globalStyledAlias keeps a bare global styled resolving beside a require.
  • styled.default(Component) call forms — isNamespaceDefault recognizes ns.default on the default local, the global alias, or a namespace local across member, call, and (0, ns.default)(...) sequence shapes, so these no longer emit withConfig.withConfig(...).
  • Script-mode css prop — the injected default import is tracked on state.file and a require() binding is treated as "no callable default", so a CommonJS file with the require after the prop no longer crashes and reuses one injected import.
  • Formatting and docs — src/ reformatted with Prettier (no semantic change) and AGENTS.md refreshed.

I rebuilt the base plugin (origin/main) and the working tree through scripts/bench/build-ref and diffed them across the new shapes: the only divergences are the intended fixes (named aliases now decorated, call forms no longer doubled, type-only imports left alone, script-mode css prop injecting a real default). I also checked the require + global styled + css-prop interaction and it is byte-identical to main, so the injected-name override does not regress it. Full suite passes (98 passed) and pnpm build compiles.

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

@quantizor
quantizor merged commit bc47e42 into main Oct 22, 2026
6 checks passed
@quantizor
quantizor deleted the fix/line-comment-string-aware branch October 22, 2026 15:19
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.

1 participant