Static-tag intrinsic: `<element tag={tag}>` for elements whose tag is fixed at definition time
メンテナーはふだん 1 日以内に返信
まだ誰も着手していません。
評価
- 難易度
- 5/5
- 見積もり時間
- 1週間以上
- 初心者へのやさしさ
- 45/100
- issue の種類
- 機能追加
- 明瞭さ
- おおむね明確
- 活発さ
- 活発
- 技術スタック
- babel, typescript
- 領域
- compilers, frontend, testing-qa
調査の方向性
Babel および Oxc コンパイラのエントリポイントと、リテラルタグおよび spread ルートに対する既存の DOM、SSR、universal の lowering から始めます。関連する hydration parity テストと IntrinsicElements typings を追跡します。完了とは、タグ形式が検証され一貫して lowering され、存在しないタグが失敗し、typing が strict で、すべての renderer で hydration シナリオが成功することです。
索引モデルが issue の本文から書いたものです。
説明
Problem
Libraries that build components around a native element whose tag is fixed at definition time — styled.li, styled("button"), any design-system primitive factory — have no good way to render that element:
dynamic(() => tag)is the only isomorphic path from a tag string to an element. It pays a factory memo plus a per-instance memo (and a hydration id) for a value that never changes. In yak-bench's browser hydrate matrix, the lane that renders the styled element throughdynamic()is 1.47× slower (geomean) than the same code with the element compiled from a literal tag; everything else in the two lanes is identical. One memo per element is the entire gap.- A runtime factory (
element(tag)returning a component) removes the memos but can't take spreads natively: a use-site{...rest}has to be merged into a props object and crossed through a component boundary beforespread()sees it, and static attributes are walked instead of compiled. - Hand-rolling it means an
isServerfork over internals (getNextElement,runHydrationEvents,sharedConfig.hydratingvsssrElement), which is what next-yak/next-yak#644 did.
Profiling the composition-heavy SSR cases (tabs) shows Solid's own rendering at ~7% of time and props plumbing (merge, omit, proxies, and the GC they generate) at ~55%; the styled-element seam is where most of that plumbing is introduced.
Proposal
A reserved intrinsic whose tag comes from an in-scope identifier:
function styled(tag) {
return props => (
<element tag={tag} {...filtered(props)} class={cls(props)}>
{props.children}
</element>
);
}
The compiler lowers it exactly as it would a literal tag, with createElement(tag) in place of the template clone:
- DOM:
hydrating ? getNextElement() : createElement(tag);spread(el, [filtered, { get class() {…} }])(the sources array from #3419/#3423);insert(el, () => props.children);runHydrationEvents()when hydrating. Two reactive nodes with children, one without, zero memos. Static child subtrees compile to their own templates and are appended; dynamic children go throughinsert. - SSR:
ssrElement(tag, [filtered, { class }], () => props.children, true)— one walk (#3418),_hkon the element. Hydration ids stay aligned because both sides do exactly what a compiled spread root does today. - Universal: renderer
createElement(tag)+spread.
The string element never reaches the DOM or the SSR output; the compiler consumes it. <element> without tag is a compile error, not a fall-through to a real tag.
Static rule
tag is an expression evaluated once, untracked, when the element is created — the same contract as <Provider value>. It is never reactive, and the compiler does not try to prove it constant (a binding in an enclosing scope can be reassigned; constancy isn't globally provable, so a scope rule would restrict real code while guaranteeing nothing). The lowering reads it exactly once (createElement(tag) / ssrElement(tag, …)); later changes to whatever it was read from are ignored, as with value.
<element>withouttagis a compile error, not a fall-through to a real tag.dynamic()/<Dynamic component>remain the answer for a tag that can change; the per-instance memo is the price of being dynamic. The two features stay vocabulary-distinct (as→Dynamic,tag→element), and theIntrinsicElements.elementdoc comment states the read-once rule so it shows on hover.
Why not <element:tag>
The namespaced form was considered first — it has the nice property that JSXNamespacedName can only hold a bare identifier, so props.as is syntactically impossible. Both parsers accept it and TS 6.0.3 types it through a template-literal index signature. But TS treats element:tag as the string "element:tag": the variable is never bound, so noUnusedLocals/noUnusedParameters flag tag (TS6133, verified), unbound identifiers aren't errors, and rename/go-to-definition are blind. That is exactly the use: directive experience. Putting the tag in an expression position (tag={tag}) gives TS the binding; the read-once rule is a documented contract, as it is for <Provider value>.
Naming
element: not in HTML, SVG, or MathML, nor in Solid'sIntrinsicElementsor either compiler's tag tables. The only history is the Web Components v0 declarative<element name="x-foo">, removed from the spec in August 2013 and never shipped unflagged. Custom elements can't claim it (hyphen required).tag, notname:nameis a real attribute on the elements most likely to be wrapped (input,button,select,textarea,form,iframe, …) and is routinely forwarded through the spread; there is no HTML/SVG/MathML attribute calledtag.isis out for the same reason (customized built-ins; forwarded).
Typing
IntrinsicElements.element: HTMLAttributes<HTMLElement> & { tag: string }. Necessarily generic (no per-tag narrowing — the tag is a variable), ref is HTMLElement. Attribute checking, children, and ref all work under --strict; the doc comment carries the static rule so it shows on hover.
Non-goals
- Compile-time folding of fully static styled elements into strings (what yak does for its React build). Separate question.
- A reactive or async
tag. That isDynamic. - A runtime
element(tag)factory. Not needed once the syntax exists;h/htmlalready take string tags.
Plan
Babel + Oxc: DOM, SSR, universal lowering (missing tag is an error), shared test expectations (parity). IntrinsicElements.element typing. Parity-harness hydration scenario (server ssrElement ↔ client getNextElement + spread sources). yak-bench prim-element overlay switched to it for the before/after against the dynamic() lane.
— Claude via Cursor
- 主要言語
- TypeScript
- スター
- 36.2k
- フォーク
- 1.1k
- 平均マージ
- 10時間 39分
- マージ済み PR(30日)
- 500
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートあり
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
solidjs/solid のほかの issue
-
難易度 4/5 3〜5日 初心者へのやさしさ 35/100
メンテナーはふだん 1 日以内に返信
-
難易度 3/5 1〜2日 初心者へのやさしさ 55/100
メンテナーはふだん 1 日以内に返信
-
難易度 4/5 3〜5日 初心者へのやさしさ 35/100
メンテナーはふだん 1 日以内に返信
-
[2.0 / next] Optimistic todo list with an "N left" memo loops forever when a toggle's save landsオープン
難易度 5/5 1週間以上 初心者へのやさしさ 15/100
メンテナーはふだん 1 日以内に返信
-
[2.0 / next] createOptimisticStore: add, delete, add with saves pending hands <For> an undefined rowオープン
難易度 4/5 3〜5日 初心者へのやさしさ 25/100
メンテナーはふだん 1 日以内に返信
似ている issue
-
ci-cd desktop size:low telemetry-improvement telemetry-round-1
難易度 2/5 1〜3時間 初心者へのやさしさ 66/100
vfarcic/dot-agent-deck#2079 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 75/100
メンテナーはふだん 1 日以内に返信
-
難易度 1/5 1時間未満 初心者へのやさしさ 83/100
メンテナーはふだん 1 日以内に返信
-
status:in-linear
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
メンテナーはふだん 1 日以内に返信
-
🔊 a11y
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
betagouv/mon-entreprise#4726 ·
メンテナーはふだん 9 日以内に返信