@yak/solid 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/runtime/styled.ts CHANGED
@@ -1,42 +1,161 @@
1
- import { css, CSSInterpolation, Classes, yakComponentSymbol } from "./cssLiteral.js";
1
+ // the compiler extracts the css at build time; this file only builds class
2
+ // and style per element, prints the tag on the server and binds it on the
3
+ // client. the map, for a reader who lands in the middle:
4
+ //
5
+ // styled() flattens a chain of styled components and picks one of two
6
+ // component shapes at definition. static: the class is fixed, there is no
7
+ // attrs function and the css block writes no style values at render time,
8
+ // so no memo and no theme read. dynamic: one memo per element resolves
9
+ // attrs, class and style.
10
+ //
11
+ // both hand the target a RenderMeta and a props view. on the server
12
+ // serializeElement (the writer) prints a tag, on the client bindElement
13
+ // spreads onto a cloned template, a component target gets targetProps
14
+ // (a copy or a proxy).
15
+ //
16
+ // the one rule every path keeps: a prop getter may render a child and take
17
+ // hydration keys of its own, so the element takes its key first and reads
18
+ // each getter once, in the target's order.
19
+ //
20
+ // words: author = the app code that writes the css block, renders the
21
+ // component and passes props; target = the tag or component under the
22
+ // styled layer; attrs = the .attrs() values, an object or a function of
23
+ // props; baked = object attrs printed into the tag at definition; view = a
24
+ // proxy over props that adds the theme or attrs; collector = the Classes
25
+ // instance a css block adds its names to; atom = a class name the author
26
+ // passes into a css block via atoms("..."), author text that needs
27
+ // escaping; reactive spread = a {...props} in jsx, which solid compiles to
28
+ // a proxy marked with $PROXY; hole = a slot in a server template that
29
+ // ssr() fills, possibly async; hydration key = the id the server prints on
30
+ // an element so the client can claim the same node
31
+ import { css, Classes, yakComponentSymbol } from "./cssLiteral.js";
2
32
  import type {
3
33
  AnyComponent,
4
34
  Attrs,
5
35
  AttrsMerged,
6
36
  Styled,
7
37
  YakComponent,
8
- AttrsFunction,
9
38
  StyledFn,
10
39
  HtmlTags,
11
40
  Substitute,
12
41
  StyledLiteral,
13
- RuntimeStyleProcessor,
42
+ CompiledStyleProcessor,
43
+ StaticStyleProcessor,
14
44
  StyleObject,
15
45
  } from "./publicStyledApi.js";
16
- import { createMemo, merge } from "solid-js";
17
- import { createComponent, Dynamic, type JSX } from "@solidjs/web";
18
- import { normalizeClass } from "./internals/mergeClasses.js";
19
- // the following import is not relative but the package-level "@yak/solid/context"
20
- // export: it keeps a single context instance shared between the bundled runtime
21
- // and user code (and lets the vite plugin alias the user's theme context)
46
+ import { $PROXY, createMemo } from "solid-js";
47
+ // the helpers solid's compiled jsx calls; this file hand-writes what the compiler would emit for a tag
48
+ import {
49
+ ChildProperties,
50
+ createComponent,
51
+ DOMWithState,
52
+ escape,
53
+ getNextElement,
54
+ insert,
55
+ isServer,
56
+ MathMLElements,
57
+ runHydrationEvents,
58
+ spread,
59
+ ssr,
60
+ ssrClassName,
61
+ ssrHydrationKey,
62
+ ssrStyle,
63
+ SVGElements,
64
+ template,
65
+ type JSX,
66
+ } from "@solidjs/web";
67
+ import { mergeClasses, normalizeClass } from "./internals/mergeClasses.js";
68
+ // the runtime and the app share one theme context; vite can alias this export
22
69
  import { useTheme } from "@yak/solid/context";
23
70
  import type { YakTheme } from "./context/index.js";
24
71
  import type { Accessor } from "solid-js";
25
72
 
26
- //
27
- // The `styled()` API without `styled.` syntax
28
- //
29
- // The API design is inspired by styled-components:
30
- // https://github.com/styled-components/styled-components/blob/main/packages/styled-components/src/constructors/styled.tsx
31
- // https://github.com/styled-components/styled-components/blob/main/packages/styled-components/src/models/StyledComponent.ts
32
- //
73
+ /** the props a styled component receives from its author; theme is an accessor (the provider's or the author's) */
74
+ type Props = {
75
+ class?: string;
76
+ style?: StyleObject | string;
77
+ theme?: Accessor<YakTheme>;
78
+ } & Record<PropertyKey, unknown>;
79
+
80
+ /**
81
+ * the compiled css block: adds its class names to the collector and, when
82
+ * $dynamic, style values for the props; the css itself is extracted at build time
83
+ */
84
+ type StyleProcessor = CompiledStyleProcessor<unknown>;
85
+
86
+ type RuntimeAttrsFn = (props: Props) => Props;
87
+
88
+ /** the .attrs() values: an object, or a function of the author's props */
89
+ type RuntimeAttrs = Props | RuntimeAttrsFn;
90
+
91
+ type ComputedStyles = {
92
+ class: string | undefined;
93
+ /** true when the class holds generated names only, nothing from the author or attrs; the writer prints it unescaped then */
94
+ generatedClass: boolean;
95
+ style: StyleObject | undefined;
96
+ attrs: Props | undefined;
97
+ };
98
+
99
+ /**
100
+ * the information to build class and style for one render
101
+ * kept outside of the component props so solid never copies or filters
102
+ * it when it walks the props object
103
+ */
104
+ type RenderMeta = StaticMeta | DynamicMeta;
105
+
106
+ /** static component (fixed class, no attrs function, no style values at render time): one meta per component, the class comes from classOf */
107
+ type StaticMeta = {
108
+ /** keys the target must never see; the rule is `skip` in yakStyled */
109
+ skip: (key: PropertyKey) => boolean;
110
+ compute: undefined;
111
+ classOf: (props: Props) => string | undefined;
112
+ attrsAddKeys: false;
113
+ /** the static class when every name in it is generated, so the writer prints it unescaped; undefined when an atom put author text in it */
114
+ unescapedClass: string | undefined;
115
+ };
116
+
33
117
  /**
34
- * Renders `props.component` (a tag name or component) with the remaining
35
- * props. Loosely typed on purpose: the runtime passes lazily-merged Proxy
36
- * props that no concrete component type can describe.
118
+ * dynamic component: one memo (run once on the server) holds class,
119
+ * style and attrs, one meta per element. the two meta literals (in
120
+ * createStaticComponent and createDynamicComponent) keep the same keys in
121
+ * the same order so every meta read sees one shape
37
122
  */
38
- const renderDynamic = (props: Record<PropertyKey, unknown>): JSX.Element =>
39
- createComponent(Dynamic as (props: Record<PropertyKey, unknown>) => JSX.Element, props);
123
+ type DynamicMeta = {
124
+ skip: (key: PropertyKey) => boolean;
125
+ compute: () => ComputedStyles;
126
+ classOf: undefined;
127
+ /**
128
+ * an attrs function may add keys at render time, so the target gets a
129
+ * proxy, not a copy. decided at definition: the memo's current attrs say
130
+ * nothing about later runs
131
+ */
132
+ attrsAddKeys: boolean;
133
+ unescapedClass: undefined;
134
+ };
135
+
136
+ /** what a styled component built on another yak component inherits from it */
137
+ type ComponentMetadata = readonly [
138
+ attrs: RuntimeAttrs | undefined,
139
+ styles: StyleProcessor,
140
+ target: AnyComponent<any> | string,
141
+ ];
142
+
143
+ /** renders one element of the flattened target: props are the author's raw props, meta says how to build class and style */
144
+ type TargetRenderer = (props: Props, meta: RenderMeta) => JSX.Element;
145
+
146
+ /** the loosely typed form of Styled that yakStyled implements; the public overloads live in publicStyledApi */
147
+ export type StyledInternal = <
148
+ T extends object,
149
+ TAttrsIn extends object = {},
150
+ TAttrsOut extends AttrsMerged<T, TAttrsIn> = AttrsMerged<T, TAttrsIn>,
151
+ >(
152
+ Component: AnyComponent<T> | YakComponent<T> | HtmlTags | string,
153
+ attrs?: Attrs<T, TAttrsIn, TAttrsOut>,
154
+ ) => StyledLiteral<Substitute<T, TAttrsIn>>;
155
+
156
+ /** solid's server regex (not exported); its VoidElements set lacks keygen and menuitem */
157
+ const VOID_ELEMENTS =
158
+ /^(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr)$/i;
40
159
 
41
160
  const styledFactory: StyledFn = (Component) =>
42
161
  Object.assign(yakStyled(Component), {
@@ -44,188 +163,426 @@ const styledFactory: StyledFn = (Component) =>
44
163
  });
45
164
 
46
165
  /**
47
- * The `styled` method works perfectly on all of your own or any third-party component,
48
- * as long as they attach the passed `class` prop to a DOM element.
49
- *
50
- * @usage
51
- *
52
- * ```tsx
53
- * const StyledLink = styled(Link)`
54
- * color: #BF4F74;
55
- * font-weight: bold;
56
- * `;
57
- * ```
166
+ * style a tag or a component that forwards its class prop. styled.div is
167
+ * compiled to styled("div"); the untransformed export in mocks/styled.ts
168
+ * adds the tag properties with a proxy
58
169
  */
59
170
  export const styled = styledFactory as Styled;
60
171
 
61
- type PropsWithClassAndStyle = {
62
- class?: string;
63
- style?: StyleObject | string;
64
- theme?: Accessor<YakTheme>;
65
- } & Record<string, unknown>;
66
-
67
172
  const yakStyled: StyledInternal = (Component, attrs) => {
68
- // Probe with a property read, not the `in` operator: in dev, solid-refresh
69
- // wraps registered components in a Proxy that forwards `get` to the live
70
- // implementation but has no `has` trap, so `in` would miss the symbol and
71
- // silently skip chain flattening (breaking attrs override order).
173
+ // solid-refresh forwards reads to the live component, but does not forward `in` checks
72
174
  const isYakComponent =
73
175
  typeof Component === "function" &&
74
176
  (Component as Partial<YakComponent<unknown>>)[yakComponentSymbol] !== undefined;
75
177
 
76
- // if the component that is wrapped is a yak component, we can extract the attrs function
77
- // and the dynamic style function to merge it with the current attrs function (or dynamic
78
- // style function) so that the sequence of the attrs functions is preserved
79
- const [, parentAttrsFn, parentRuntimeStylesFn, parentTarget] = isYakComponent
80
- ? ((Component as YakComponent<unknown>)[yakComponentSymbol] as unknown as [
81
- YakComponent<unknown>,
82
- ExtractAttrsFunction<typeof attrs>,
83
- RuntimeStyleProcessor<unknown>,
84
- AnyComponent<any> | string,
85
- ])
178
+ // the public tuple type hides the shape, one cast at the read
179
+ const [parentAttrs, parentRuntimeStylesFn, parentTarget] = isYakComponent
180
+ ? ((Component as YakComponent<unknown>)[yakComponentSymbol] as ComponentMetadata)
86
181
  : [];
87
182
 
88
- // the ultimate render target of the whole styled(styled(...)) chain:
89
- // attrs and style processors are already merged at construction time, so
90
- // a chain of N levels renders the target directly in ONE component instead
91
- // of re-entering every parent wrapper per element
92
- const targetComponent = (isYakComponent ? parentTarget : Component) as AnyComponent<any> | string;
183
+ // the chain renders its final target once, with all attrs and style processors combined
184
+ const targetComponent = parentTarget ?? Component;
93
185
 
94
- const mergedAttrsFn = buildRuntimeAttrsProcessor(attrs, parentAttrsFn);
186
+ const mergedAttrs = composeAttrs(attrs as RuntimeAttrs | undefined, parentAttrs);
95
187
 
96
188
  return (styles, ...values) => {
97
- // combine all interpolated logic into a single function
98
- // e.g. styled.button`color: ${props => props.color}; margin: ${props => props.margin};`
99
- const runtimeStylesFn = css(
100
- styles,
101
- ...(values as CSSInterpolation<unknown>[]),
102
- ) as RuntimeStyleProcessor<unknown>;
103
- const runtimeStyleProcessor = buildRuntimeStylesProcessor(
104
- runtimeStylesFn,
105
- parentRuntimeStylesFn,
106
- );
189
+ // the interpolations of the style block, e.g.
190
+ // styled.button`color: ${props => props.color}; margin: ${props => props.margin};`
191
+ // arrive compiled: styles and values hold class names, style callbacks
192
+ // and css-variable maps, no css text
193
+ const runtimeStylesFn = css(styles, ...values);
194
+ const runtimeStyleProcessor = composeStyles(runtimeStylesFn, parentRuntimeStylesFn);
107
195
 
108
- // decided once per styled component: tag names render through Dynamic
109
- // (which needs a `component` prop), component targets are called
110
- // directly so a user prop named `component` reaches them (Dynamic
111
- // would consume it). `targetProps` is merged BEFORE filterDomProps -
112
- // the filter proxy must stay the outermost layer (see filterDomProps)
113
196
  const isTag = typeof targetComponent === "string";
114
- const targetProps = isTag ? { component: targetComponent } : {};
115
- const renderTarget = isTag
116
- ? renderDynamic
117
- : (finalProps: Record<PropertyKey, unknown>) =>
118
- createComponent(
119
- targetComponent as (props: Record<PropertyKey, unknown>) => JSX.Element,
120
- finalProps,
121
- );
122
-
123
- // fast path for fully static components (no attrs, no dynamic styles;
124
- // the most common case): contribute the chain's class names through a
125
- // single reactive `class` getter and strip $-props; skips theme lookup,
126
- // memo creation and style handling entirely
127
- const Yak: AnyComponent<PropsWithClassAndStyle> =
128
- !mergedAttrsFn && !runtimeStyleProcessor.$dynamic
129
- ? (props) =>
130
- renderTarget(
131
- filterDomProps(
132
- merge(props, targetProps, {
133
- // reading props.class inside the getter keeps the class
134
- // binding reactive without re-creating the element
135
- get class(): string | undefined {
136
- const classes = new Classes(normalizeClass(props.class));
137
- runtimeStyleProcessor(props, classes, undefined as unknown as StyleObject);
138
- return classes.value || undefined;
139
- },
140
- }),
141
- ),
142
- )
143
- : (props) => {
144
- // the component body runs ONCE in Solid: all per-update work
145
- // lives in memos/getters so only the affected DOM bindings update
146
-
147
- // attrs functions and dynamic style functions receive the theme;
148
- // fully static components take the fast path above and never
149
- // subscribe to the theme context
150
- const theme = useTheme();
151
-
152
- // getter-preserving merge: compiled prop functions like
153
- // `({ $bottom }) => ...` destructure this proxy INSIDE the memo
154
- // below, so every read is tracked at its use-site
155
- const propsWithTheme = merge({ theme }, props) as PropsWithClassAndStyle & {
156
- theme: Accessor<YakTheme>;
157
- };
158
-
159
- // The first component which is not wrapped in a yak component executes all attrs
160
- // functions starting from the innermost yak component to the outermost (itself)
161
- const attrsProps = mergedAttrsFn
162
- ? createMemo(() => mergedAttrsFn(propsWithTheme as any) as PropsWithClassAndStyle)
163
- : undefined;
164
-
165
- // input for the style processor: props + theme + attrs output
166
- const styleInput = attrsProps ? merge(propsWithTheme, attrsProps) : propsWithTheme;
167
-
168
- // execute all functions inside the style literal
169
- // e.g. styled.button`color: ${props => props.color};`
170
- // only reads inside this memo re-run it; the element itself is
171
- // never re-created, only its class/style bindings update
172
- const computed = createMemo(() => {
173
- const attrsResult = attrsProps?.();
174
- const classes = new Classes(normalizeClass(props.class));
175
- const attrsClass = normalizeClass(attrsResult?.class);
176
- if (attrsClass) {
177
- classes.add(attrsClass);
178
- }
179
- // static processors never write style values, so the incoming
180
- // style object can be passed through without a defensive copy
181
- const style =
182
- runtimeStyleProcessor.$dynamic || attrsResult?.style
183
- ? { ...unwrapStyle(props.style), ...unwrapStyle(attrsResult?.style) }
184
- : unwrapStyle(props.style);
185
- runtimeStyleProcessor(styleInput, classes, style as StyleObject);
186
- return {
187
- class: classes.value || undefined,
188
- style: style && Object.keys(style).length > 0 ? style : undefined,
189
- };
190
- });
191
-
192
- const merged = merge(props, attrsProps ?? {}, {
193
- get class(): string | undefined {
194
- return computed().class;
195
- },
196
- get style(): StyleObject | undefined {
197
- return computed().style;
198
- },
199
- });
200
-
201
- // keep the theme prop only if an attrs function intentionally
202
- // replaced it; the context theme itself never reaches the target
203
- const allowTheme = () => {
204
- const attrsResult = attrsProps?.();
205
- return !!attrsResult && "theme" in attrsResult && attrsResult.theme !== theme;
206
- };
207
-
208
- // render the chain's target directly; parent wrappers contribute
209
- // only their (already merged) attrs and style processors
210
- return renderTarget(filterDomProps(merge(merged, targetProps), allowTheme));
211
- };
212
-
213
- return Object.assign(Yak, {
214
- [yakComponentSymbol]: [Yak, mergedAttrsFn, runtimeStyleProcessor, targetComponent] as [
215
- unknown,
216
- unknown,
217
- unknown,
218
- unknown,
219
- ],
197
+ // object attrs with plain attribute values bake into the tag's opening
198
+ // string and template; the component stays static. baked keys reach the
199
+ // dom only through that string: skip drops them from the target
200
+ const baked =
201
+ isTag &&
202
+ mergedAttrs &&
203
+ typeof mergedAttrs !== "function" &&
204
+ bakeable(targetComponent, mergedAttrs)
205
+ ? mergedAttrs
206
+ : undefined;
207
+ const attrString = baked ? bakeAttributes(baked) : "";
208
+ const renderTarget = createTargetRenderer(targetComponent, attrString);
209
+ // static: attrs are none or baked, and the css block writes no style values
210
+ const isStatic = (!mergedAttrs || !!baked) && !runtimeStyleProcessor.$dynamic;
211
+ // the target never sees $-props, the provider theme, or the author class
212
+ // (yak hands it the combined one); a dynamic component merges style in its
213
+ // memo, so the author's style key is dropped here and handed over combined.
214
+ // no symbol passes: solid's merge() flattens any object that answers its
215
+ // private $SOURCES key, which would hand a target's mergeProps() the
216
+ // unfiltered originals; solid's own omit() blocks that key the same way
217
+ const skip = (key: PropertyKey) =>
218
+ typeof key !== "string" ||
219
+ key.charCodeAt(0) === 36 /* $ */ ||
220
+ key === "class" ||
221
+ key === "theme" ||
222
+ (!isStatic && key === "style") ||
223
+ // a baked attr wins over the author's prop of the same name, even when its value is null
224
+ (baked !== undefined && Object.hasOwn(baked, key));
225
+ // baked attrs still run inside a dynamic component's memo: style callbacks
226
+ // read them through withAttrs
227
+ const attrsFn =
228
+ typeof mergedAttrs === "function" ? mergedAttrs : mergedAttrs && (() => mergedAttrs);
229
+ const Yak = isStatic
230
+ ? createStaticComponent(
231
+ isTag ? targetComponent : undefined,
232
+ renderTarget,
233
+ runtimeStyleProcessor,
234
+ skip,
235
+ attrString,
236
+ )
237
+ : createDynamicComponent(renderTarget, attrsFn, runtimeStyleProcessor, skip, !!baked);
238
+
239
+ const metadata: ComponentMetadata = [mergedAttrs, runtimeStyleProcessor, targetComponent];
240
+ return Object.assign(Yak, { [yakComponentSymbol]: metadata });
241
+ };
242
+ };
243
+
244
+ /**
245
+ * object attrs bake when every entry is a plain attribute with a primitive
246
+ * value. rejected keys are the union of what skip drops, what attribute()
247
+ * treats specially, and what solid sets as a dom property (DOMWithState:
248
+ * value, checked, ...), which a template would turn into a markup
249
+ * attribute. enumerable value keys only, so the three walks (here,
250
+ * bakeAttributes, skip) see the same keys; a getter blocks baking and runs
251
+ * at render time
252
+ */
253
+ const bakeable = (tag: string, attrs: Props): boolean => {
254
+ const stateful = DOMWithState[tag.toUpperCase()];
255
+ return Object.getOwnPropertyNames(attrs).every((key) => {
256
+ const descriptor = Object.getOwnPropertyDescriptor(attrs, key)!;
257
+ if (!descriptor.enumerable || !("value" in descriptor)) return false;
258
+ const type = typeof descriptor.value;
259
+ return (
260
+ (type === "string" || type === "number" || type === "boolean" || descriptor.value == null) &&
261
+ // the name goes into the template and the opening string unescaped; anything
262
+ // else (a computed key with markup in it) takes the writer, whose attribute() escapes names
263
+ ATTRIBUTE_NAME.test(key) &&
264
+ key.charCodeAt(0) !== 36 &&
265
+ key !== "class" &&
266
+ key !== "style" &&
267
+ key !== "theme" &&
268
+ key !== "ref" &&
269
+ !ChildProperties.has(key) &&
270
+ !key.startsWith("on") &&
271
+ !key.startsWith("prop:") &&
272
+ !(stateful && key in stateful)
273
+ );
274
+ });
275
+ };
276
+
277
+ /**
278
+ * the attribute string of baked attrs, built once at definition. the value
279
+ * rules follow attribute(), which prints one prop on the server; the escaping
280
+ * is local because the client build's escape is an empty stub, and the names
281
+ * passed the ATTRIBUTE_NAME check. `<` stays unescaped, harmless in a quoted value
282
+ */
283
+ const bakeAttributes = (attrs: Props): string => {
284
+ let result = "";
285
+ for (const key of Object.keys(attrs)) {
286
+ const value = attrs[key];
287
+ if (value == null || value === false) continue;
288
+ result +=
289
+ value === true || value === "" ? ` ${key}` : ` ${key}="${escapeAttribute(String(value))}"`;
290
+ }
291
+ return result;
292
+ };
293
+
294
+ const escapeAttribute = (value: string) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;");
295
+
296
+ /** an attribute name that needs no escaping in markup */
297
+ const ATTRIBUTE_NAME = /^[A-Za-z_:][\w:.-]*$/;
298
+
299
+ /** parent attrs first, then own attrs read and override that result */
300
+ const composeAttrs = (attrs?: RuntimeAttrs, parent?: RuntimeAttrs): RuntimeAttrs | undefined => {
301
+ if (!attrs) return parent;
302
+ if (!parent) return attrs;
303
+ // two objects without getters combine once here and stay an object; a
304
+ // getter must run per render, inside the memo
305
+ if (
306
+ typeof attrs !== "function" &&
307
+ typeof parent !== "function" &&
308
+ !hasGetter(attrs) &&
309
+ !hasGetter(parent)
310
+ ) {
311
+ return combineProps(parent, attrs);
312
+ }
313
+ const own = typeof attrs === "function" ? attrs : () => attrs;
314
+ const parentFn = typeof parent === "function" ? parent : () => parent;
315
+ return (props) => {
316
+ const parentProps = parentFn(props);
317
+ return combineProps(parentProps, own(combineProps(props, parentProps)));
318
+ };
319
+ };
320
+
321
+ /** parent styles before own styles, with one collector and one style object */
322
+ const composeStyles = (own: StyleProcessor, parent?: StyleProcessor): StyleProcessor => {
323
+ if (!parent) return own;
324
+ return Object.assign(
325
+ (props: unknown, classes: Parameters<StyleProcessor>[1], style: StyleObject) => {
326
+ parent(props, classes, style);
327
+ own(props, classes, style);
328
+ },
329
+ { $dynamic: own.$dynamic || parent.$dynamic },
330
+ ) as StyleProcessor;
331
+ };
332
+
333
+ /**
334
+ * attrs override props; class and style values combine, e.g.
335
+ * { class: "a", foo: 1 } and { class: "b", bar: 2 } -> { class: "a b", foo: 1, bar: 2 }
336
+ */
337
+ const combineProps = (props: Props, newProps: Props | null | undefined): Props => {
338
+ if (!newProps) return props;
339
+ // descriptors, not values: a spread would run every author getter here,
340
+ // and a children getter renders (twice, and with the wrong hydration keys)
341
+ const descriptors = {
342
+ ...Object.getOwnPropertyDescriptors(props),
343
+ ...Object.getOwnPropertyDescriptors(newProps),
344
+ };
345
+ // an equal class counts as nothing: own attrs get the combined props and
346
+ // may hand the same class back, merging it again would duplicate it
347
+ if (newProps.class && props.class !== newProps.class) {
348
+ descriptors.class = valueDescriptor(mergeClasses(normalizeClass(props.class), newProps.class));
349
+ }
350
+ if (newProps.style && props.style !== newProps.style) {
351
+ descriptors.style = valueDescriptor({
352
+ ...unwrapStyle(props.style),
353
+ ...unwrapStyle(newProps.style),
220
354
  });
355
+ }
356
+ return Object.defineProperties({}, descriptors) as Props;
357
+ };
358
+
359
+ const valueDescriptor = (value: unknown): PropertyDescriptor => ({
360
+ value,
361
+ enumerable: true,
362
+ configurable: true,
363
+ writable: true,
364
+ });
365
+
366
+ const hasGetter = (object: object): boolean => {
367
+ for (const key of Object.getOwnPropertyNames(object)) {
368
+ if (!("value" in Object.getOwnPropertyDescriptor(object, key)!)) return true;
369
+ }
370
+ return false;
371
+ };
372
+
373
+ /** static: no theme, no memo; one class per component, a children-only fast path for a tag */
374
+ const createStaticComponent = (
375
+ tag: string | undefined,
376
+ renderTarget: TargetRenderer,
377
+ processor: StaticStyleProcessor,
378
+ skip: (key: PropertyKey) => boolean,
379
+ attrString: string,
380
+ ): AnyComponent<Props> => {
381
+ const collected = new Classes();
382
+ // a static processor ignores its props argument, it only adds class names
383
+ processor(undefined, collected);
384
+ const staticClass = collected.value || undefined;
385
+ const classOf = (props: Props): string | undefined => {
386
+ const userClass = normalizeClass(props.class);
387
+ if (!userClass) return staticClass;
388
+ // the collector skips generated names the author's class already holds,
389
+ // and an atom may remove one
390
+ const classes = new Classes(userClass);
391
+ processor(props, classes);
392
+ return classes.value || undefined;
393
+ };
394
+ const renderChildrenOnly =
395
+ tag && !VOID_ELEMENTS.test(tag)
396
+ ? createChildrenOnlyRenderer(tag, staticClass, attrString)
397
+ : undefined;
398
+ const meta: RenderMeta = {
399
+ skip,
400
+ compute: undefined,
401
+ classOf,
402
+ attrsAddKeys: false,
403
+ unescapedClass: collected.generated ? staticClass : undefined,
221
404
  };
405
+ return (props) => {
406
+ // no props or only children: no skip walk, no props copy, no spread.
407
+ // a reactive spread can add props later and needs the full client binding;
408
+ // on the server props are read once so the proxy check does not matter
409
+ if (renderChildrenOnly && (isServer || !($PROXY in props))) {
410
+ const keys = Object.keys(props);
411
+ if (!keys.length || (keys.length === 1 && keys[0] === "children")) {
412
+ return renderChildrenOnly(props, keys.length !== 0);
413
+ }
414
+ }
415
+ return renderTarget(props, meta);
416
+ };
417
+ };
418
+
419
+ /** tag and class cached per component; only the children need a binding or serialization */
420
+ const createChildrenOnlyRenderer = (
421
+ tag: string,
422
+ className: string | undefined,
423
+ attrString: string,
424
+ ): ((props: Props, hasChildren: boolean) => JSX.Element) => {
425
+ if (isServer) {
426
+ const head = `<${tag}${attrString}`;
427
+ const open = `${className ? ` class="${ssrClassName(className)}"` : ""}>`;
428
+ const closing = `</${tag}>`;
429
+ const parts = [head, open, closing];
430
+ // as in childContent: script and style content is not escaped
431
+ const raw = tag === "script" || tag === "style";
432
+ return (props, hasChildren): { t: string } => {
433
+ // key first: the child getter may render
434
+ const hk = ssrHydrationKey();
435
+ const children = hasChildren ? (raw ? props.children : escape(props.children)) : undefined;
436
+ // text joins in place; anything else goes through ssr() like a compiled
437
+ // template's hole, which handles async children and text separators
438
+ const text = plainContent(children);
439
+ if (text !== undefined) return { t: `${head}${hk}${open}${text}${closing}` };
440
+ return ssr(parts, hk, children);
441
+ };
442
+ }
443
+ const create = createElementTemplate(tag, attrString, className);
444
+ return (props, hasChildren) => {
445
+ const el = getNextElement(create);
446
+ if (hasChildren) {
447
+ // static text arrives as a plain value, not a getter: one insert, no effect.
448
+ // a getter is dynamic and keeps the binding
449
+ const descriptor = Object.getOwnPropertyDescriptor(props, "children");
450
+ if (descriptor && "value" in descriptor) insert(el, descriptor.value);
451
+ else insert(el, () => props.children);
452
+ }
453
+ runHydrationEvents();
454
+ return el;
455
+ };
456
+ };
457
+
458
+ /** dynamic: the provider theme plus one memo per element for attrs, class and style */
459
+ const createDynamicComponent =
460
+ (
461
+ renderTarget: TargetRenderer,
462
+ attrsFn: RuntimeAttrsFn | undefined,
463
+ processor: StyleProcessor,
464
+ skip: (key: PropertyKey) => boolean,
465
+ baked: boolean,
466
+ ): AnyComponent<Props> =>
467
+ (props) => {
468
+ const theme = useTheme();
469
+ // style callbacks see an explicit theme prop before the provider theme
470
+ const propsWithTheme = withTheme(props, theme);
471
+ // one memo per element runs the attrs function and every interpolation
472
+ // of the style block, e.g. styled.button`color: ${props => props.color}`.
473
+ // only reads inside it re-run it; the element is never re-created.
474
+ // attrs and styles share the memo on purpose:
475
+ // a style-only prop change also reruns attrs, still cheaper than
476
+ // two memos per element
477
+ const compute = () => computeStyles(props, propsWithTheme, attrsFn, processor);
478
+ // the server has no updates, so a run-once closure replaces the memo:
479
+ // solid's server createMemo still builds an owner and a node per element.
480
+ // on the client the memo is transparent: it claims no hydration key
481
+ const computed = isServer ? once(compute) : createMemo(compute, { transparent: true });
482
+ // theme reaches the target only when attrs set their own; the provider
483
+ // accessor must not land on the dom element. identity, not `in`: an attrs
484
+ // function that spreads its input hands the provider accessor back
485
+ const allowTheme =
486
+ attrsFn &&
487
+ (() => {
488
+ const attrs = computed().attrs;
489
+ return !!attrs && "theme" in attrs && attrs.theme !== theme;
490
+ });
491
+ return renderTarget(props, {
492
+ skip: allowTheme ? (key) => (key === "theme" ? !allowTheme() : skip(key)) : skip,
493
+ compute: computed,
494
+ classOf: undefined,
495
+ // baked attrs sit in the template and cannot add keys, the copy path serves them
496
+ attrsAddKeys: !!attrsFn && !baked,
497
+ unescapedClass: undefined,
498
+ });
499
+ };
500
+
501
+ /** resolve attrs, then run the styles against that props view */
502
+ const computeStyles = (
503
+ props: Props,
504
+ propsWithTheme: Props,
505
+ attrsFn: RuntimeAttrsFn | undefined,
506
+ processor: StyleProcessor,
507
+ ): ComputedStyles => {
508
+ const attrs = attrsFn?.(propsWithTheme);
509
+ const authorClass = normalizeClass(props.class);
510
+ const classes = new Classes(authorClass);
511
+ const attrsClass = normalizeClass(attrs?.class);
512
+ if (attrsClass) classes.add(attrsClass);
513
+ // a static processor writes no style values, so the author's style object
514
+ // passes through without a copy
515
+ const style =
516
+ processor.$dynamic || attrs?.style
517
+ ? { ...unwrapStyle(props.style), ...unwrapStyle(attrs?.style) }
518
+ : unwrapStyle(props.style);
519
+ processor(
520
+ attrs ? withAttrs(propsWithTheme, attrs) : propsWithTheme,
521
+ classes,
522
+ style as StyleObject,
523
+ );
524
+ return {
525
+ class: classes.value || undefined,
526
+ generatedClass: !authorClass && !attrsClass && classes.generated,
527
+ style: style && hasKeys(style) ? style : undefined,
528
+ attrs,
529
+ };
530
+ };
531
+
532
+ /**
533
+ * backing object of a props view: the author's props, a provider theme to
534
+ * fill in when props has none, and attrs whose values win over props.
535
+ * one shared handler serves both views, so a view costs one proxy and one
536
+ * small object per element and no trap closures
537
+ */
538
+ type View = { props: Props; theme?: Accessor<YakTheme>; attrs?: Props };
539
+
540
+ const viewTraps: ProxyHandler<View> = {
541
+ get: ({ props, theme, attrs }, key) => {
542
+ if (key === "theme" && theme && !("theme" in props)) return theme;
543
+ if (attrs && key in attrs) return Reflect.get(attrs, key);
544
+ return Reflect.get(props, key);
545
+ },
546
+ // no props check for theme here: when props has one, Reflect.has says so anyway
547
+ has: ({ props, theme, attrs }, key) =>
548
+ (key === "theme" && !!theme) || (!!attrs && key in attrs) || Reflect.has(props, key),
549
+ ownKeys: ({ props, theme, attrs }) => {
550
+ const keys = new Set(Reflect.ownKeys(props));
551
+ if (attrs) for (const key of Reflect.ownKeys(attrs)) keys.add(key);
552
+ if (theme) keys.add("theme");
553
+ return [...keys];
554
+ },
555
+ getOwnPropertyDescriptor: ({ props, theme, attrs }, key) => {
556
+ if (key === "theme" && theme && !("theme" in props)) {
557
+ return { value: theme, enumerable: true, configurable: true };
558
+ }
559
+ const descriptor = Reflect.getOwnPropertyDescriptor(attrs && key in attrs ? attrs : props, key);
560
+ // a prop the view adds must report configurable, the backing object has no such key
561
+ return descriptor && { ...descriptor, configurable: true };
562
+ },
222
563
  };
223
564
 
224
565
  /**
225
- * Normalize a Solid style prop to an object the style processor can extend.
226
- * Solid allows string styles on elements; they are converted here so CSS custom
227
- * properties from dynamic values can be merged in (prefer object styles).
566
+ * the provider theme when props has none. a props proxy (a reactive spread)
567
+ * can gain or lose its theme key later, so it always gets the view; a plain
568
+ * object with a theme is used as is
228
569
  */
570
+ const withTheme = (props: Props, theme: Accessor<YakTheme>): Props =>
571
+ (!($PROXY in props) && "theme" in props
572
+ ? props
573
+ : new Proxy({ props, theme }, viewTraps)) as Props;
574
+
575
+ /** style interpolations read attrs over author props, getters only on demand */
576
+ const withAttrs = (props: Props, attrs: Props): Props =>
577
+ new Proxy({ props, attrs }, viewTraps) as Props;
578
+
579
+ /** server stand-in for createMemo: run once, no owner and no node */
580
+ const once = <T extends object>(fn: () => T): (() => T) => {
581
+ let value: T | undefined;
582
+ return () => (value ??= fn());
583
+ };
584
+
585
+ /** string styles become objects before css variables are added */
229
586
  const unwrapStyle = (style: StyleObject | string | undefined): StyleObject | undefined => {
230
587
  if (typeof style !== "string") {
231
588
  return style;
@@ -243,151 +600,294 @@ const unwrapStyle = (style: StyleObject | string | undefined): StyleObject | und
243
600
  return result as StyleObject;
244
601
  };
245
602
 
246
- const isBlockedProp = (key: PropertyKey, allowTheme?: () => boolean): boolean =>
247
- typeof key === "string" &&
248
- (key.startsWith("$") || (key === "theme" && !(allowTheme && allowTheme())));
603
+ /** no array for a yes/no answer */
604
+ const hasKeys = (object: object): boolean => {
605
+ for (const _ in object) return true;
606
+ return false;
607
+ };
608
+
609
+ /** the target's render path, chosen once per styled component */
610
+ const createTargetRenderer = (
611
+ target: AnyComponent<any> | string,
612
+ attrString: string,
613
+ ): TargetRenderer => {
614
+ if (typeof target !== "string") {
615
+ return (props, meta) => createComponent(target, targetProps(props, meta));
616
+ }
617
+ if (isServer) {
618
+ const head = `<${target}${attrString}`;
619
+ const closing = VOID_ELEMENTS.test(target) ? undefined : `</${target}>`;
620
+ return (props, meta) => serializeElement(target, head, closing, props, meta);
621
+ }
622
+ return createElementRenderer(target, attrString);
623
+ };
249
624
 
250
625
  /**
251
- * Remove all props that start with a $ sign (plus the internal theme) so
252
- * they reach neither DOM elements nor custom components.
253
- *
254
- * Implemented as a lazy Proxy instead of an eager copy: Solid's compiled
255
- * spread reads props through getters, and copying would read every prop
256
- * exactly once and freeze its value, killing reactivity.
626
+ * the server writer: writes the html string by hand instead of using ssrElement.
627
+ * ssrElement needs a filtered props object with getters to keep the
628
+ * read order, that costs more than writing directly
257
629
  */
258
- const filterDomProps = <T extends object>(props: T, allowTheme?: () => boolean): T =>
259
- new Proxy(props, {
260
- get: (target, key) => (isBlockedProp(key, allowTheme) ? undefined : Reflect.get(target, key)),
261
- has: (target, key) => !isBlockedProp(key, allowTheme) && Reflect.has(target, key),
262
- ownKeys: (target) => Reflect.ownKeys(target).filter((key) => !isBlockedProp(key, allowTheme)),
263
- getOwnPropertyDescriptor: (target, key) =>
264
- isBlockedProp(key, allowTheme) ? undefined : Reflect.getOwnPropertyDescriptor(target, key),
265
- });
266
-
267
- // util function to merge class names, as they are concatenated with a space
268
- const mergeClasses = (a?: string, b?: string) => {
269
- if (!a && !b) return undefined;
270
- if (!a) return b;
271
- if (!b) return a;
272
- return a + " " + b;
630
+ const serializeElement = (
631
+ tag: string,
632
+ head: string,
633
+ closing: string | undefined,
634
+ props: Props,
635
+ meta: RenderMeta,
636
+ ): { t: string } => {
637
+ // key first: a prop getter may render a child and take keys
638
+ const hk = ssrHydrationKey();
639
+ // one memo read for class, style and attrs. branch on meta.compute, not
640
+ // on computed, so the static writer makes no call here
641
+ let computed: ComputedStyles | undefined;
642
+ let className: string | undefined;
643
+ if (meta.compute) {
644
+ computed = meta.compute();
645
+ className = computed.class;
646
+ } else {
647
+ className = meta.classOf(props);
648
+ }
649
+ const attrs = computed?.attrs;
650
+ const style = computed?.style;
651
+ const skip = meta.skip;
652
+ let result = head + hk;
653
+ let children: unknown;
654
+ // a textarea's value is its content, like solid's ssrElement writes it
655
+ const textarea = tag === "textarea";
656
+ // author keys first, an attrs value wins; then the keys only attrs has.
657
+ // two loops on purpose: one loop over both key sets is slower on the
658
+ // attrs path, it needs a push and a second `in` per key.
659
+ // only the first child prop is read, and none on a void tag: a child
660
+ // getter may render and take hydration keys
661
+ for (const key of Object.keys(props)) {
662
+ if (skip(key)) continue;
663
+ const source = attrs && key in attrs ? attrs : props;
664
+ if (!isChildKey(key, textarea)) result += attribute(key, source[key]);
665
+ else if (children === undefined && closing) children = childContent(tag, key, source[key]);
666
+ }
667
+ if (attrs) {
668
+ for (const key of Object.keys(attrs)) {
669
+ if (skip(key) || key in props) continue;
670
+ if (!isChildKey(key, textarea)) result += attribute(key, attrs[key]);
671
+ else if (children === undefined && closing) children = childContent(tag, key, attrs[key]);
672
+ }
673
+ }
674
+ // generated class names need no escaping, an author or attrs class does
675
+ if (className !== undefined) {
676
+ const generated = computed ? computed.generatedClass : className === meta.unescapedClass;
677
+ result += ` class="${generated ? className : ssrClassName(className)}"`;
678
+ }
679
+ if (style !== undefined) result += ` style="${ssrStyle(style as Record<string, string>)}"`;
680
+ // a void tag has no children: the finished string is the node, no ssr() call.
681
+ // the space keeps an unquoted hydration key from swallowing the slash
682
+ if (!closing) return { t: result + " />" };
683
+ // text and finished nodes join in place, as solid's own resolver does.
684
+ // functions, arrays and nodes with pending holes go to ssr(): async wrap,
685
+ // error boundary routing, and the separator marker between adjacent
686
+ // text items so the client can claim two text nodes
687
+ const text = plainContent(children);
688
+ if (text !== undefined) return { t: result + ">" + text + closing };
689
+ return ssr([result + ">", closing], children);
273
690
  };
274
691
 
692
+ /** a prop that becomes the element's content: solid's child properties, and a textarea's value */
693
+ const isChildKey = (key: string, textarea: boolean) =>
694
+ ChildProperties.has(key) || (textarea && (key === "value" || key === "defaultValue"));
695
+
275
696
  /**
276
- * merge props and processed props (including class names and styles)
277
- * e.g.:\
278
- * `{ class: "a", foo: 1 }` and `{ class: "b", bar: 2 }` \
279
- * => `{ class: "a b", foo: 1, bar: 2 }`
697
+ * the string a child resolves to without ssr(), else undefined. a server
698
+ * node is { t: html, h?: pending async holes }; one with holes needs ssr()
280
699
  */
281
- const combineProps = <
282
- T extends {
283
- class?: string;
284
- style?: StyleObject | string;
285
- },
286
- TOther extends
287
- | {
288
- class?: string;
289
- style?: StyleObject | string;
290
- }
291
- | null
292
- | undefined,
293
- >(
294
- props: T,
295
- newProps: TOther,
296
- ) =>
297
- newProps
298
- ? (props.class === newProps.class || !newProps.class) &&
299
- (props.style === newProps.style || !newProps.style)
300
- ? // shortcut if no style and class merging is necessary
301
- {
302
- ...props,
303
- ...newProps,
304
- }
305
- : // merge class names and styles
306
- {
307
- ...props,
308
- ...newProps,
309
- class: mergeClasses(props.class, newProps.class),
310
- style: { ...unwrapStyle(props.style), ...unwrapStyle(newProps.style) },
311
- }
312
- : // if no new props are provided, no merging is necessary
313
- props;
700
+ const plainContent = (node: unknown): string | undefined => {
701
+ if (typeof node === "string") return node;
702
+ if (typeof node === "number") return String(node);
703
+ if (node == null || typeof node === "boolean") return "";
704
+ if (typeof node !== "object" || Array.isArray(node)) return undefined;
705
+ const server = node as { t?: unknown; h?: unknown[] };
706
+ if (server.h && server.h.length > 0) return undefined;
707
+ if (typeof server.t === "string") return server.t;
708
+ return undefined;
709
+ };
314
710
 
315
711
  /**
316
- * Merges the attrs function of the current component with the attrs function of the parent component
317
- * in order to preserve the sequence of the attrs functions.
318
- * Note: In theory, the parentAttrsFn can have different types for TAttrsIn and TAttrsOut
319
- * but as this is only used internally, we can ignore and simplify this case
320
- * @param attrs The attrs object or function of the current component (if any)
321
- * @param parentAttrsFn The attrs function of the parent/wrapped component (if any)
322
- * @returns A function that receives the props and returns the transformed props
712
+ * one server attribute with its leading space, the value rules of solid's
713
+ * ssrElement; bakeAttributes applies the same rules once at definition
323
714
  */
324
- const buildRuntimeAttrsProcessor = <
325
- T,
326
- TAttrsIn extends object,
327
- TAttrsOut extends AttrsMerged<T, TAttrsIn>,
328
- >(
329
- attrs?: Attrs<T, TAttrsIn, TAttrsOut>,
330
- parentAttrsFn?: AttrsFunction<T, TAttrsIn, TAttrsOut>,
331
- ): AttrsFunction<T, TAttrsIn, TAttrsOut> | undefined => {
332
- const ownAttrsFn = attrs && (typeof attrs === "function" ? attrs : () => attrs);
333
-
334
- if (ownAttrsFn && parentAttrsFn) {
335
- return (props) => {
336
- const parentProps = parentAttrsFn(props);
337
-
338
- // overwrite and merge the parent props with the props received from the attrs function
339
- // after they went through the parent attrs function.
340
- //
341
- // This makes sure the linearity of the attrs functions is preserved and all attrs function receive
342
- // the whole props object calculated from the previous attrs functions
343
- return combineProps(
344
- parentProps as any,
345
- ownAttrsFn(combineProps(props as any, parentProps as any) as any),
346
- ) as any;
347
- };
715
+ const attribute = (prop: string, value: unknown): string => {
716
+ if (prop === "style") return ` style="${ssrStyle(value as string)}"`;
717
+ // class never reaches here, skip drops it; the branch stays, the static
718
+ // writer is slower without it (jit code placement)
719
+ if (prop === "class") return ` class="${ssrClassName(value as string)}"`;
720
+ // refs, event handlers and prop: bindings only exist on the client
721
+ if (value == undefined || prop === "ref" || prop.startsWith("on") || prop.startsWith("prop:")) {
722
+ return "";
348
723
  }
724
+ if (typeof value === "boolean") return value ? ` ${escape(prop)}` : "";
725
+ return value === "" ? ` ${escape(prop)}` : ` ${escape(prop)}="${escape(value, true)}"`;
726
+ };
349
727
 
350
- return ownAttrsFn || parentAttrsFn;
728
+ /** raw markup stays as is, every other child value is escaped */
729
+ const childContent = (tag: string, prop: string, value: unknown): unknown =>
730
+ tag === "script" || tag === "style" || prop === "innerHTML" ? value : escape(value);
731
+
732
+ /** the cached template for a fixed tag; svg and mathml tags parse inside their namespace root */
733
+ const createElementTemplate = (tag: string, attrString: string, className?: string) => {
734
+ // atoms can put author names into the static class, so the template escapes it
735
+ const classAttribute = className ? ` class="${escapeAttribute(className)}"` : "";
736
+ const opening = `<${tag}${attrString}${classAttribute}>`;
737
+ // flag 2 returns firstChild.firstChild
738
+ // skips the <svg>/<math> wrapper we add so the child parses in its namespace
739
+ if (SVGElements.has(tag) && tag !== "svg") return template(`<svg>${opening}`, 2);
740
+ if (MathMLElements.has(tag) && tag !== "math") return template(`<math>${opening}`, 2);
741
+ return template(opening);
351
742
  };
352
743
 
353
744
  /**
354
- * Merges the runtime style function of the current component with the runtime style function of the parent component
355
- * in order to preserve the sequence of the attrs functions.
356
- * @param runtimeStylesFn The current runtime styles function
357
- * @param parentRuntimeStylesFn The parent runtime styles function
358
- * @returns The merged runtime styles function
745
+ * bind a fixed client tag without dynamic()'s per-element memo. hydration
746
+ * claims the server node by key; a fresh mount clones the cached template.
747
+ * solid's SVGElements set lacks a, script, style and title, so a fresh
748
+ * mount makes them html elements even inside an <svg>, as solid's own
749
+ * dynamic() does
359
750
  */
360
- const buildRuntimeStylesProcessor = <T>(
361
- runtimeStylesFn: RuntimeStyleProcessor<T>,
362
- parentRuntimeStylesFn?: RuntimeStyleProcessor<T>,
363
- ) => {
364
- if (runtimeStylesFn && parentRuntimeStylesFn) {
365
- const combined: RuntimeStyleProcessor<T> = Object.assign(
366
- (props: T, classes: Parameters<RuntimeStyleProcessor<T>>[1], style: StyleObject) => {
367
- parentRuntimeStylesFn(props, classes, style);
368
- runtimeStylesFn(props, classes, style);
369
- },
370
- // the chain is dynamic if any level is dynamic
371
- { $dynamic: runtimeStylesFn.$dynamic || parentRuntimeStylesFn.$dynamic },
372
- );
373
- return combined;
751
+ const createElementRenderer = (tag: string, attrString: string): TargetRenderer => {
752
+ const create = createElementTemplate(tag, attrString);
753
+ return (props, meta) => bindElement(getNextElement(create), props, meta);
754
+ };
755
+
756
+ const bindElement = (el: Element, props: Props, meta: RenderMeta): Element => {
757
+ const bound = targetProps(props, meta);
758
+ // a proxy can add children later, so it keeps the child binding
759
+ const skipChildren = !($PROXY in bound) && !("children" in bound);
760
+ // no untrack here: a component body runs untracked
761
+ spread(el, bound, skipChildren);
762
+ // replay events once the element's bindings are ready
763
+ runHydrationEvents();
764
+ return el;
765
+ };
766
+
767
+ /**
768
+ * the props the target sees: author props, attrs output, computed class and style
769
+ * $-props and the provider theme are hidden. solid's omit() takes fixed key
770
+ * names and $-props are open-ended, so the filter is hand-written
771
+ *
772
+ * plain copy when the keys can't change, proxy when attrs or a reactive
773
+ * spread can add keys and downstream omit() has to notice
774
+ * the copy stays unmarked on purpose, a $PROXY mark makes omit()/merge()
775
+ * wrap it again and every read gets slower
776
+ */
777
+ const targetProps = (props: Props, meta: RenderMeta): Record<PropertyKey, unknown> => {
778
+ if (!meta.attrsAddKeys && !($PROXY in props)) return copyProps(props, meta);
779
+ return proxyProps(props, meta);
780
+ };
781
+
782
+ /**
783
+ * copies descriptors without reading them: a getter like icon={<Icon />}
784
+ * renders a child and takes hydration keys, the target reads it in its own
785
+ * order. moving the getter is safe, solid's compiled getters do not depend on `this`
786
+ */
787
+ const copyProps = (props: Props, meta: RenderMeta): Record<PropertyKey, unknown> => {
788
+ const { compute, classOf } = meta;
789
+ const out: Record<PropertyKey, unknown> = {};
790
+ // string keys only, like solid's omit(): a symbol never reaches the target
791
+ for (const key of Object.getOwnPropertyNames(props)) {
792
+ if (meta.skip(key)) continue;
793
+ const descriptor = Reflect.getOwnPropertyDescriptor(props, key)!;
794
+ if ("value" in descriptor && descriptor.enumerable) {
795
+ out[key] = descriptor.value;
796
+ } else {
797
+ Object.defineProperty(out, key, descriptor);
798
+ }
799
+ }
800
+ if (isServer) {
801
+ // the values are final on the server: plain values take the value path
802
+ // in solid's omit() and merge() instead of a getter per read
803
+ if (compute) {
804
+ const computed = compute();
805
+ out.class = computed.class;
806
+ if (computed.style !== undefined) out.style = computed.style;
807
+ } else out.class = classOf(props);
808
+ return out;
809
+ }
810
+ const classFn = compute ? () => compute().class : () => classOf(props);
811
+ const styleFn = styleGetter(compute);
812
+ Object.defineProperty(out, "class", {
813
+ get: classFn,
814
+ enumerable: true,
815
+ configurable: true,
816
+ });
817
+ if (styleFn) {
818
+ Object.defineProperty(out, "style", { get: styleFn, enumerable: true, configurable: true });
374
819
  }
375
- return runtimeStylesFn || parentRuntimeStylesFn;
820
+ return out;
376
821
  };
377
822
 
378
823
  /**
379
- * Internal function where attrs are passed to be processed
824
+ * the style accessor a component target receives, or none
825
+ * on the server the value is final, and a target that spreads its props
826
+ * into ssrElement would write style="" for an undefined one; on the client
827
+ * the accessor stays so the memo can set a style later
380
828
  */
381
- export type StyledInternal = <
382
- T extends object,
383
- TAttrsIn extends object = {},
384
- TAttrsOut extends AttrsMerged<T, TAttrsIn> = AttrsMerged<T, TAttrsIn>,
385
- >(
386
- Component: AnyComponent<T> | YakComponent<T> | HtmlTags | string,
387
- attrs?: Attrs<T, TAttrsIn, TAttrsOut>,
388
- ) => StyledLiteral<Substitute<T, TAttrsIn>>;
829
+ const styleGetter = (
830
+ compute: (() => ComputedStyles) | undefined,
831
+ ): (() => StyleObject | undefined) | undefined => {
832
+ if (!compute) return undefined;
833
+ if (isServer && compute().style === undefined) return undefined;
834
+ return () => compute().style;
835
+ };
389
836
 
390
837
  /**
391
- * Utility type to extract the AttrsFunction from the Attrs type
838
+ * attrs and reactive spreads can add or remove keys; the $PROXY mark keeps
839
+ * downstream omit() calls reactive
392
840
  */
393
- export type ExtractAttrsFunction<T> = T extends (p: any) => any ? T : never;
841
+ const proxyProps = (props: Props, meta: RenderMeta): Record<PropertyKey, unknown> => {
842
+ // trap closures per element on purpose, not shared with viewTraps: solid's
843
+ // for-in hits two traps per key, so a backing object would add an
844
+ // indirection to each
845
+ const { skip, compute, classOf } = meta;
846
+ // meta.attrsAddKeys, not the destructured copy: the read narrows meta.compute
847
+ const attrsProps = () => (meta.attrsAddKeys ? meta.compute().attrs : undefined);
848
+ const classFn = compute ? () => compute().class : () => classOf(props);
849
+ const styleFn = styleGetter(compute);
850
+ const contributed = (key: PropertyKey) =>
851
+ key === "class" ? classFn : key === "style" ? styleFn : undefined;
852
+ const fromAttrs = (key: PropertyKey) => {
853
+ const attrs = attrsProps();
854
+ return attrs && key in attrs ? attrs : undefined;
855
+ };
856
+ return new Proxy(props, {
857
+ get(target, key, receiver) {
858
+ // the receiver, like solid's own props proxies: solid's spread walks
859
+ // a proxy's keys through its traps only when props[$PROXY] === props
860
+ if (key === $PROXY) return receiver;
861
+ const getter = contributed(key);
862
+ if (getter) return getter();
863
+ if (skip(key)) return undefined;
864
+ const attrs = fromAttrs(key);
865
+ return attrs ? attrs[key] : Reflect.get(target, key);
866
+ },
867
+ has(target, key) {
868
+ if (key === $PROXY) return true;
869
+ if (contributed(key)) return true;
870
+ if (skip(key)) return false;
871
+ return Reflect.has(target, key) || fromAttrs(key) !== undefined;
872
+ },
873
+ ownKeys(target) {
874
+ const keys = new Set<string | symbol>();
875
+ for (const key of Reflect.ownKeys(target)) if (!skip(key)) keys.add(key);
876
+ for (const key of Object.keys(attrsProps() ?? {})) if (!skip(key)) keys.add(key);
877
+ // class and style are always own keys, listed after the author keys and attrs
878
+ keys.add("class");
879
+ if (styleFn) keys.add("style");
880
+ return [...keys];
881
+ },
882
+ getOwnPropertyDescriptor(target, key) {
883
+ const getter = contributed(key);
884
+ if (getter) return { enumerable: true, configurable: true, get: getter };
885
+ if (skip(key)) return undefined;
886
+ const attrs = fromAttrs(key);
887
+ if (attrs) {
888
+ return { enumerable: true, configurable: true, get: () => attrsProps()?.[key] };
889
+ }
890
+ return Reflect.getOwnPropertyDescriptor(target, key);
891
+ },
892
+ });
893
+ };