Skip to content

sdds-infra: Тришейкинг оптимизация #2792

Draft
glebmachine wants to merge 7 commits into
salute-developers:devfrom
glebmachine:dev
Draft

sdds-infra: Тришейкинг оптимизация #2792
glebmachine wants to merge 7 commits into
salute-developers:devfrom
glebmachine:dev

Conversation

@glebmachine
Copy link
Copy Markdown
Collaborator

@glebmachine glebmachine commented May 21, 2026

Bundle size: tree-shake-friendly subpaths, модернизация SWC target и lazy heavy popup deps

TL;DR

Сокращение «плазма»-чанка в потребительском бандле с 1824 КБ raw / 598 КБ gz до 664 КБ raw / 271 КБ gz (−64% raw, −55% gz) за счёт семи независимых изменений в plasma-themes, plasma-icons и plasma-new-hope. Каждое изменение оформлено отдельным коммитом и может мерджиться независимо — все они аддитивны, без breaking changes для существующих импортов.

Мотивация

Замер бандла потребителя на Vite (rolldown) с rollup-plugin-visualizer показал, что чанк plasma (eager-загружаемый при первом рендере) тащит:

  • ~325 КБ raw / 28.6 КБ gz токенов из plasma-themes, хотя реально используется ~5% из них;
  • ~786 КБ raw / 347 КБ gz иконок из plasma-icons, при том что каждая иконка триплицируется (16/24/36 ассеты) даже когда size известен статически;
  • ~110 КБ gz plasma-new-hope, существенная часть которого — инлайн SWC-helper-ы (_object_spread, _define_property и т. п.) из-за target: es5 в общем swc.config.json.

Все три источника лишнего веса — следствия конфигурации публикации, а не дизайна компонентов. Фиксы делаются на уровне package.json exports, генератора иконок и SWC-конфигов; компонентный API не трогается.

Эффект на бандл

Шаг plasma chunk raw gz Δ raw Δ gz
Базовое состояние 1824.2 КБ 598.2 КБ
1. plasma-themesexports + ESM subpath 1505.7 КБ 571.1 КБ −318 −27
2. plasma-icons — single-size entrypoints 1016.7 КБ 367.1 КБ −489 −204
3. plasma-icons — SWC target=es2020 724.1 КБ 287.7 КБ −293 −79
4. plasma-new-hope — SWC target=es2020 713.1 КБ 286.4 КБ −11 −1.3
6. plasma-new-hope — lazy Draggable/Resizable 664.5 КБ 271.1 КБ −49 −15
Итого −1160 КБ (−64%) −327 КБ (−55%)

Шаги 6 и 7 (lazy popup deps + revert Polymorphic intersection) — добавлены после первичного review, описание ниже. Шаг 7 — type-only revert, на бандл не влияет, в таблице не отражён.

Замеры сняты на реальном чанке потребителя; каждый пакет публиковался через npm pack и устанавливался как file:-tarball, чтобы изолировать вклад каждого шага.


Содержимое по коммитам

Все семь коммитов независимы — порядок применения не критичен, любой можно cherry-pick-нуть отдельно.

1. feat(plasma-themes): expose ESM subpaths via exports field

Файл: packages/themes/plasma-themes/package.json.

Без поля exports потребитель резолвит @salutejs/plasma-themes/tokens и @salutejs/plasma-themes/tokens/<theme> напрямую в CJS-копию (tokens/index.js, tokens/<theme>/index.js), которая по структуре — единый файл с ~2k константами. Rollup/Vite CJS не tree-shake-ят такой формат — в бандл попадает всё.

ESM-копия (es/tokens/*, es/themes/*) уже собирается через tsconfig.es.json и публикуется тем же npm pack. Каждый токен в ней — export var X = 'var(--...)', идеальный кандидат для шейкинга.

Поле exports маршрутизирует subpath-ы на ESM-вариант. Покрытие:

  • . — корневой entry
  • ./tokens, ./tokens/* — токены и темы
  • ./themes, ./themes/* — корневые theme-файлы
  • ./es/themes/*legacy-шим для существующих в монорепе и публичных потребителей импортов вида @salutejs/plasma-themes/es/themes/<theme> (35 файлов внутри репы используют такой путь — сторибуки, mixins, ViewContainer.config.ts). Маппится на тот же ESM-файл, чтобы не сломать существующие сборки. После одной мажорной итерации может быть мигрирован на ./themes/*.
  • ./css/*, ./package.json — пассивный passthrough.

Эффект: plasma-themes-chunk потребителя 325 КБ raw / 28.6 КБ gz → 6.7 КБ raw / 1.5 КБ gz.

Совместимость: SemVer-минор. exports сужает «легальные» подпути до перечисленных, но все известные публичные пути в маппинге. Внутренние пути (src/*) намеренно недоступны — это закрепляет публичный контракт.

2. feat(plasma-icons): add single-size entrypoints /16, /24, /36

Файлы: packages/plasma-icons/package.json, packages/plasma-icons/scripts/utils.ts, packages/plasma-icons/scripts/generateReactComponents.ts.

Каждый IconX в Icons/IconX.tsx сейчас статически импортирует все три ассета:

import { Close as Icon16 } from '../Icon.assets.16/Close';
import { Close as Icon24 } from '../Icon.assets.24/Close';
import { Close as Icon36 } from '../Icon.assets.36/Close';
// ...
var IconComponent = getIconComponent(Icon16, Icon24, Icon36, sizeMap[size].size);

Tree-shaking не может выкинуть ни один размер — все три используются по имени. На приложении с ~80 иконками это ~786 КБ raw / 347 КБ gz триплицированных SVG.

Генератор теперь параллельно с Icons/IconX.tsx эмитит three single-size-варианта: Icons.16/IconX.tsx, Icons.24/IconX.tsx, Icons.36/IconX.tsx. Каждый тянет ровно один ассет:

// Icons.16/IconClose.tsx
import { Close as Asset } from '../Icon.assets.16/Close';
import { IconProps, IconRoot } from '../IconRoot';

export const IconClose: React.FC<IconProps> = ({ size = 'xs', color, className, style, ...rest }) => (
    <IconRoot className={className} size={size} color={color} style={style} icon={Asset} {...rest} />
);

Плюс три barrel-а index.16.ts, index.24.ts, index.36.ts — каждый экспортирует все компоненты под своим размером и реэкспортирует legacy-иконки из old/Icons/ (их размер фиксирован в SVG).

В package.json это публикуется через exports:

{
  "exports": {
    ".": { ... },
    "./16": { "import": "./es/scalable/index.16.js", "require": "./scalable/index.16.js", "types": "./scalable/index.16.d.ts" },
    "./24": { ... },
    "./36": { ... },
    "./css/*": "./css/*",
    "./package.json": "./package.json"
  }
}

Потребитель пишет import { IconClose } from '@salutejs/plasma-icons/24' если иконка используется в size="s" (24-px) — tree-shaking уберёт остальные две трети ассетов. Старый импорт @salutejs/plasma-icons без подпути продолжает работать без изменений.

Sber-aliased имена (IconSberXIconSbX) в single-size файлах получают тот же @deprecated JSDoc, что и в multi-size варианте — миграция через /16|/24|/36 работает идентично root barrel-у.

Внутренние подпути (./scalable/*, ./old/*) намеренно не открыты через exports — это защищает структуру пакета от случайного использования как публичного API.

Совместимость: SemVer-минор. Чистое добавление новых entry-point-ов. Существующий barrel @salutejs/plasma-icons не изменяется.

3. refactor(plasma-icons): mark root barrel exports as @deprecated

Файл: packages/plasma-icons/scripts/generateReactComponents.ts.

Без подсказки потребитель продолжит импортировать из @salutejs/plasma-icons и не получит выигрыша от single-size entrypoints. Генератор теперь оборачивает каждый ре-экспорт в root barrel-е JSDoc-блоком @deprecated с конкретной инструкцией миграции:

/**
 * @deprecated Импорт `IconClose` из `@salutejs/plasma-icons` тянет ассеты для всех 3 размеров (16/24/36),
 * что увеличивает бандл в ~3 раза и не поддаётся tree-shaking-у. Замените на
 * `import { IconClose } from '@salutejs/plasma-icons/16'` (или `/24`, `/36`) —
 * потребитель получит ровно один размер. Если `size` определяется в рантайме динамически,
 * подавите предупреждение локально (`// eslint-disable-next-line deprecation/deprecation`).
 */
export { IconClose } from './Icons/IconClose';

IDE/tsserver покажет strikethrough при использовании; ESLint-rule deprecation/deprecation ругнётся в lint. Для редких случаев действительно динамического size предупреждение глушится локально.

Build и runtime не затрагиваются — JSDoc живёт в .d.ts/source и не попадает в эмит.

Совместимость: SemVer-патч. Никакого изменения поведения; только IDE/lint-подсказки.

4. build(plasma-icons): switch SWC to local config with es2020 target

Файлы: packages/plasma-icons/.swcrc (новый), packages/plasma-icons/package.json.

Общий корневой swc.config.json использует target: es5. SWC при этом инлайнит helper-функции (_define_property, _object_spread, _object_without_properties, _object_without_properties_loose) в каждый файл компонента — ~80 строк на каждый из ~1.4k компонентов × 3 размера в новой single-size схеме.

Локальный .swcrc для plasma-icons повышает target до es2020:

{
    "jsc": {
        "target": "es2020",
        "parser": { "syntax": "typescript", "tsx": true },
        "transform": { "react": { "runtime": "classic" } },
        "experimental": {
            "plugins": [
                ["@swc/plugin-styled-components", { "displayName": true, "ssr": true }]
            ]
        }
    },
    "exclude": [".*\\.stories.tsx$", ".*\\.component-test.tsx$"]
}

Native spread/rest/optional-args не транспилируются — каждый сгенерированный компонент сжимается с ~80 строк до ~10. Плагин @swc/plugin-styled-components (displayName: true, ssr: true) сохранён из root-конфига, чтобы IconRoot собирался с SSR-стабильными class-name-хешами и debug-метками.

build:cjs/build:esm в package.json переключены на локальный ./.swcrc вместо ../../swc.config.json.

Альтернативаjsc.externalHelpers: true + добавление @swc/helpers в dependencies — отклонена, чтобы не добавлять runtime-зависимость. Современный target дополнительно соответствует ситуации: дизайн-система не поддерживает IE11/legacy уже несколько лет.

Эффект: plasma-icons в потребительском бандле (при использовании одного размера через subpath) 467 КБ raw / 191 КБ gz → 193 КБ raw / 99.9 КБ gz.

Совместимость: для строгих — major (формально меняется browser-target). На практике для design-system-потребителей — патч/минор, т. к. поддержки IE11 нет давно.

5. build(plasma-new-hope): bump SWC target to es2020 in sc/emotion configs

Файлы: packages/plasma-new-hope/swc-sc.config.json, packages/plasma-new-hope/swc-emotion.config.json.

Аналогично пункту 4, но для plasma-new-hope. Локальные SWC-конфиги переключены с target: es5 на target: es2020; дополнительно явно выставлен parser.tsx: true для консистентности (источники содержат JSX, без флага парсер ругался бы на потенциально-неоднозначные конструкции).

Эффект: plasma-new-hope-chunk 312 КБ raw → 302 КБ raw, 110 КБ gz → 108 КБ gz. Скромный, но «бесплатный» выигрыш — побочный эффект модернизации, не требующий поведенческих рисков.

Совместимость: аналогично пункту 4.

6. perf(plasma-new-hope): lazy-load react-draggable/re-resizable in Popup

Файлы: packages/plasma-new-hope/src/components/Popup/PopupRoot.tsx, packages/plasma-new-hope/src/components/Popup/ui/Resizable/Resizable.tsx, packages/plasma-new-hope/src/components/_Resizable/Resizable.tsx.

PopupRoot безусловно делал import Draggable from 'react-draggable', но рендерил его только если потребитель передавал draggable={true} (line 81 — ранее ранний return popupNode для не-draggable пути). Аналогично, обе копии Resizable (Popup/ui/Resizable и _Resizable) безусловно импортировали re-resizable, но в run-time использовали его только при resizable && !resizable.disabled. Импорты были hoisted на module-level, поэтому код drag/resize тянулся в чанк Popup-а у каждого потребителя — даже у Modal/Sheet/Notification/Drawer/Popover, которые в подавляющем большинстве используются без этих фич.

Все три места завёрнуты в React.lazy(() => import(...)) + Suspense с не-интерактивным поддеревом в fallback:

// PopupRoot.tsx
const Draggable = lazy(() => import('react-draggable').then((m) => ({ default: m.default })));
// ...
if (!draggable) return popupNode;
return (
    <Suspense fallback={popupNode}>
        <Draggable nodeRef={contentRef} ...>{popupNode}</Draggable>
    </Suspense>
);
// Resizable.tsx (обе копии)
const ReResizable = lazy(() => import('re-resizable').then((m) => ({ default: m.Resizable })));
// ...
if (!resizable || resizable.disabled) return <>{children}</>;
return (
    <Suspense fallback={<>{children}</>}>
        <ReResizable ref={resizableContainer} ...>{children}</ReResizable>
    </Suspense>
);

fallback рендерит то же поддерево без drag/resize-обёртки — для потребителя визуально нет flash или layout-shift; после resolve import() интерактив включается.

Эффект (на чанке потребителя gigachat-neo):

  • plasma chunk: 713.1 КБ raw / 286.4 КБ gz → 664.5 КБ raw / 271.1 КБ gz (−49 raw / −15 gz).
  • Появляются два новых on-demand чанка: cjs-* (react-draggable ~33 КБ raw / 10 КБ gz) и lib-* (re-resizable ~28 КБ raw / 7 КБ gz). Если потребитель никогда не передаёт draggable/resizable (типичный кейс для gigachat-neo) — чанки не запрашиваются вовсе.

Совместимость: SemVer-минор. Внешний API не меняется (draggable/resizable props работают идентично), но React-tree теперь содержит Suspense-границу внутри PopupRoot/Resizable. Это требует React 18+ (плазма уже на этом требовании). Для SSR Suspense fallback рендерится сразу, без visual diff с прежним поведением (т. к. fallback — то же дерево без обёртки).

7. revert(plasma-new-hope): drop strict intersection in PolymorphicForwardRefComponent

Файл: packages/plasma-new-hope/src/types/Polymorphic.ts.

Коммит 05833e4abc («feat(): fix types in Button») добавил вторую сигнатуру в intersection PolymorphicForwardRefComponent:

export type PolymorphicForwardRefComponent<DefaultElement extends ElementType, OwnProps = {}> = (<
    C extends ElementType = DefaultElement
>(
    props: PolymorphicComponentPropsWithRef<C, OwnProps>,
) => ReactElement | null) &
    ((props: PolymorphicComponentPropsWithRef<DefaultElement, OwnProps>) => ReactElement | null) & {
        displayName?: string;
    };

Вторая сигнатура сужает тип до конкретного DefaultElement (например, 'button'). В downstream-потребителях это ломает валидные patterns, где плазменные полиморфные компоненты используются как значение пропсов as=:

<PopupItemControl as={Button} view="clear" stretch>{...}</PopupItemControl>
// → TS2769: 'MakePolymorphic<...>' is not assignable to type '"button"'

TypeScript при сравнении с as: ElementType коллапсирует полиморфный тип к literal-default-element-у и репортит несоответствие. Та же проблема воспроизводится в styled(Other) с attrs({ as: Button }).

Коммит откатывает intersection к предыдущему виду:

export type PolymorphicForwardRefComponent<DefaultElement extends ElementType, OwnProps = {}> = (<
    C extends ElementType = DefaultElement
>(
    props: PolymorphicComponentPropsWithRef<C, OwnProps>,
) => ReactElement | null) & { displayName?: string };

Generic call-signature уже поддерживает TypeScript instantiation expressions вида Button<'a'> (см. оригинальный комментарий в файле), отдельная default-element-сигнатура не нужна для этого кейса. Если требовался конкретный случай, который покрывала вторая сигнатура, — см. репродукцию в обсуждении PR; сейчас она ломает больше, чем чинит.

Эффект: только на типы. Runtime/bundle не затронуты.

Совместимость: SemVer-патч (формально downgrade type-strictness, но фактически возвращение к ранее работавшему контракту, по которому собрались downstream-потребители).


Совместимость и migration plan

Изменение SemVer Затрагивает Действие потребителя
plasma-themes exports minor публичные subpath-ы ничего — прозрачно
plasma-icons /16/24/36 minor новые subpath-ы опционально — переключиться на single-size для веса
plasma-icons root @deprecated patch IDE/lint только подавить через ESLint или мигрировать
plasma-icons SWC es2020 minor (strict: major) browser-target убедиться что IE11 не в требованиях
plasma-new-hope SWC es2020 то же то же то же
plasma-new-hope lazy Draggable/Resizable minor React tree требуется React 18+ (уже peer)
plasma-new-hope revert Polymorphic intersection patch публичные типы ничего — возврат к ранее работавшему контракту

Никаких изменений компонентного API. Никаких изменений в сгенерированных .d.ts-сигнатурах публичных компонентов (кроме PolymorphicForwardRefComponent — там реверт к более liberal-варианту). Все изменения в exports — аддитивные либо сохраняют существующие пути через явный маппинг.

Михеев Глеб Андреевич and others added 5 commits May 21, 2026 17:41
Без `exports` потребитель резолвит `@salutejs/plasma-themes/tokens` и
`@salutejs/plasma-themes/tokens/<theme>` напрямую в CJS-копию (`tokens/*.js`),
которую Rollup/Vite не tree-shake-ит — в бандл попадают все ~2k токенов
независимо от того, сколько потребитель реально использует.

Маршрутизирует subpath-ы на уже собираемую ESM-копию (`es/tokens/*`,
`es/themes/*`), где каждый токен — отдельный `export var X = 'var(--...)'`,
который шейкается до фактически использованных.

Маппинг покрывает все известные публичные subpath-ы: `.`, `./tokens`,
`./tokens/*`, `./themes`, `./themes/*`, `./css/*`, `./package.json`.
Legacy-путь `./es/themes/*` (используется в сторибуках и пакетных
mixins по всей монорепе) оставлен в `exports` без `require`-варианта
для обратной совместимости — последующая миграция на `./themes/*`
рекомендуется, но не блокирует релиз.

Эффект на бандл потребителя (Vite): chunk `plasma-themes` 325 KB raw /
28.6 KB gz → 6.7 KB raw / 1.5 KB gz.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Главный barrel `@salutejs/plasma-icons` через `Icons/IconX.tsx`
статически импортирует все три ассета (`Icon.assets.16/24/36`) и
выбирает один в рантайме по prop `size`. Tree-shaking не может
выбросить неиспользуемые размеры — все три считаются «used by name».
На typical-приложении с ~80 иконками это даёт ~800 КБ raw / 350 КБ gz
лишнего веса (триплицированные SVG-ассеты).

Генератор теперь параллельно с `Icons/IconX.tsx` эмитит
single-size компоненты `Icons.16/IconX.tsx`, `Icons.24/IconX.tsx`,
`Icons.36/IconX.tsx`, каждый из которых импортирует ровно один
ассет своего размера. Плюс barrel-файлы `index.16.ts`, `index.24.ts`,
`index.36.ts` экспортируют все single-size компоненты + реэкспортируют
legacy-иконки из `old/Icons/` (у них размер фиксирован в SVG).

Публичный API расширен через `exports`:
- `@salutejs/plasma-icons/16` → 16-px вариант всех иконок
- `@salutejs/plasma-icons/24` → 24-px (`size="s"` по умолчанию)
- `@salutejs/plasma-icons/36` → 36-px

Старый импорт `@salutejs/plasma-icons` сохраняется без изменений.
Внутренние пути (`./scalable/*`, `./old/*`) намеренно НЕ
добавлены в `exports` — это закрывает их от случайного импорта
из-вне как часть публичного контракта.

Sber-aliased иконки (`IconSberX` → `IconSbX`) в single-size
файлах получают `@deprecated` JSDoc со ссылкой на канонический
`IconSbX` — миграция работает идентично root barrel-у.

Эффект на бандл потребителя при использовании одного размера:
~786 КБ raw → 193 КБ raw (с es2020-фиксом в следующем коммите).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Импорт `Icon<Name>` из `@salutejs/plasma-icons` тянет ассеты для всех
трёх размеров (`Icon.assets.16/24/36`) и выбирает один в рантайме —
конструкция не поддаётся tree-shaking-у и увеличивает вес иконки
в потребительском бандле примерно втрое.

Генератор теперь оборачивает каждый ре-экспорт в root barrel-е
JSDoc-блок `@deprecated` с конкретной инструкцией миграции на
`@salutejs/plasma-icons/16|24|36` для статических размеров.
IDE/tsserver покажет strikethrough при использовании из root-а.
ESLint `deprecation/deprecation` будет ругаться — для случаев
динамического `size` (когда миграция невозможна) предупреждение
подавляется локально.

Build/runtime не ломается — JSDoc живёт только в `.d.ts`/source
и не влияет на эмит.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Общий root `swc.config.json` использует `target: es5` — SWC при
этом инлайнит `_define_property`, `_object_spread`, `_object_without_properties`
и `_object_without_properties_loose` в каждый файл компонента
(~80 строк на каждый из ~1.4k компонентов × 3 размера в новой
схеме single-size). Хелперы пожимаются gzip-ом, но всё равно
оставляют десятки КБ.

Локальный `packages/plasma-icons/.swcrc` повышает `target` до
`es2020` — native `{ ...a }`-спред, опциональные аргументы,
optional-chaining не транспилируются. Это уменьшает каждый
сгенерированный компонент с ~80 строк до ~10.

Плагин `@swc/plugin-styled-components` (`displayName: true`,
`ssr: true`) сохранён из root-конфига — `IconRoot.tsx` собирается
с теми же SSR-стабильными class-name-хешами и debug-метками.

Build-scripts `build:cjs`/`build:esm` теперь указывают на
локальный `./.swcrc` вместо `../../swc.config.json`.

Эффект на бандл потребителя при использовании одного размера:
~467 КБ raw → 193 КБ raw / ~191 КБ gz → 99.9 КБ gz.

Альтернатива была `jsc.externalHelpers: true` + добавление
`@swc/helpers` в dependencies — выбрана модернизация target-а,
т. к. dev-зависимость не добавляется, а IE11/legacy уже не
поддерживается дизайн-системой.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…figs

Локальные SWC-конфиги `swc-sc.config.json` и `swc-emotion.config.json`
использовали `target: es5`. На каждый собранный модуль это даёт
inline-хелперы (`_object_spread`, `_define_property`, `_extends`
и т. п.), которые после rollup-агрегации остаются и в финальном
бандле потребителя десятками КБ.

Переключаем на `target: es2020` — native spread/rest/optional-args
сохраняются, бандл сжимается без потери поведения для
поддерживаемых браузеров (Chrome ≥ 90 / Firefox ≥ 88 / Safari ≥ 14 /
Edge ≥ 90). IE11 дизайн-системой не поддерживается, регрессии нет.

Дополнительно добавлен `parser.tsx: true` — главные source-файлы
содержат JSX (`*.tsx`), но `parser.syntax: typescript` без флага
`tsx` парсит их как обычный TS. На существующих файлах текущая
сборка работает за счёт расширения, но флаг делает конфиг
консистентным с `swc.config.json` plasma-icons.

Эффект на бандл потребителя: plasma-new-hope chunk
312 КБ raw → 302 КБ raw / 110 КБ gz → 108 КБ gz.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Yakutoc Yakutoc removed the request for review from Yeti-or May 22, 2026 03:42
@Yakutoc Yakutoc changed the title Draft: Тришейкинг оптимизация sdds-infra: Тришейкинг оптимизация May 22, 2026
@Yakutoc Yakutoc marked this pull request as draft May 22, 2026 03:55
Михеев Глеб Андреевич added 2 commits May 22, 2026 11:31
`PopupRoot.tsx` and `Popup/ui/Resizable/Resizable.tsx` (used by Modal/Sheet/
Notification/Drawer via Popup) plus `_Resizable/Resizable.tsx` (used by
Popover) unconditionally imported `react-draggable` / `re-resizable` even when
`draggable`/`resizable` props were not set — the most common case. This
pulled ~50 KB raw / ~13 KB gz of drag/resize code into the eager Popup
bundle for every consumer.

Wrap the heavy components in `React.lazy(() => import(...))` + `Suspense`
with the non-interactive subtree as fallback, so the chunks load on demand
only when drag/resize is actually requested.
…rdRefComponent

Commit 05833e4 ("feat(): fix types in Button") added a second function
signature intersection that narrows `as=`-polymorphism to the default element:

    & ((props: PolymorphicComponentPropsWithRef<DefaultElement, OwnProps>) => ReactElement | null)

In downstream consumers this breaks valid patterns like
`styled(Other)({ as: PlasmaButton })` and `<X as={PlasmaButton}>` —
TypeScript collapses the polymorphic call signature to the literal default
element ("button") and reports the polymorphic component as not assignable.

Revert to the pre-intersection form. The original generic call signature
already lets TS instantiate `Button<'a'>`-style usage without the extra
constraint.
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.

2 participants