@yak/solid 0.2.0 → 0.3.0

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
@@ -43,7 +43,7 @@ import type {
43
43
  StaticStyleProcessor,
44
44
  StyleObject,
45
45
  } from "./publicStyledApi.js";
46
- import { $PROXY, createMemo } from "solid-js";
46
+ import { $PROXY, createMemo, merge, omit } from "solid-js";
47
47
  // the helpers solid's compiled jsx calls; this file hand-writes what the compiler would emit for a tag
48
48
  import {
49
49
  ChildProperties,
@@ -339,8 +339,8 @@ const combineProps = (props: Props, newProps: Props | null | undefined): Props =
339
339
  // descriptors, not values: a spread would run every author getter here,
340
340
  // and a children getter renders (twice, and with the wrong hydration keys)
341
341
  const descriptors = {
342
- ...Object.getOwnPropertyDescriptors(props),
343
- ...Object.getOwnPropertyDescriptors(newProps),
342
+ ...rehomedDescriptors(props),
343
+ ...rehomedDescriptors(newProps),
344
344
  };
345
345
  // an equal class counts as nothing: own attrs get the combined props and
346
346
  // may hand the same class back, merging it again would duplicate it
@@ -363,6 +363,35 @@ const valueDescriptor = (value: unknown): PropertyDescriptor => ({
363
363
  writable: true,
364
364
  });
365
365
 
366
+ /**
367
+ * a getter is only defined for a read through its own object: solid's
368
+ * compiled props getters (2.0.0-rc.10, `hoistProps`) reach their captured
369
+ * values through `this`, so a descriptor moved to another object reads
370
+ * nothing. re-homing keeps the laziness (nothing is read here) and reads
371
+ * the source when the target is read; a data descriptor passes as it is
372
+ */
373
+ const rehome = (
374
+ source: object,
375
+ key: PropertyKey,
376
+ descriptor: PropertyDescriptor,
377
+ ): PropertyDescriptor =>
378
+ descriptor.get || descriptor.set
379
+ ? {
380
+ get: () => (source as any)[key],
381
+ set: descriptor.set && ((value: unknown) => ((source as any)[key] = value)),
382
+ enumerable: descriptor.enumerable,
383
+ configurable: true,
384
+ }
385
+ : descriptor;
386
+
387
+ const rehomedDescriptors = (source: object): PropertyDescriptorMap => {
388
+ const descriptors = Object.getOwnPropertyDescriptors(source) as PropertyDescriptorMap;
389
+ for (const key of Reflect.ownKeys(descriptors)) {
390
+ descriptors[key as string] = rehome(source, key, descriptors[key as string]);
391
+ }
392
+ return descriptors;
393
+ };
394
+
366
395
  const hasGetter = (object: object): boolean => {
367
396
  for (const key of Object.getOwnPropertyNames(object)) {
368
397
  if (!("value" in Object.getOwnPropertyDescriptor(object, key)!)) return true;
@@ -754,140 +783,102 @@ const createElementRenderer = (tag: string, attrString: string): TargetRenderer
754
783
  };
755
784
 
756
785
  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);
786
+ // no untrack anywhere below: a component body runs untracked
787
+ if (!meta.attrsAddKeys && !($PROXY in props)) {
788
+ // a fixed key set read by one spread: copying the few descriptors is
789
+ // cheaper than a view. measured on the client with omit() + the source
790
+ // list instead: +12–15% hydrate and mount on dynamic-prop tags (an
791
+ // omit view, spread()'s source walk and their garbage, per element)
792
+ const bound = copyProps(props, meta);
793
+ spread(el, bound, !("children" in bound));
794
+ } else {
795
+ // attrs or a reactive spread can add keys — children too — so the target
796
+ // reads views. the sources as an array, like a compiled <tag {...a} {...b}>:
797
+ // spread() reads a function source inline, no memo and no hydration id.
798
+ // that matters here: the server writer for a tag builds none of this, so
799
+ // an owner created only on the client would shift every later element's id
800
+ spread(el, targetSources(props, meta), false);
801
+ }
762
802
  // replay events once the element's bindings are ready
763
803
  runHydrationEvents();
764
804
  return el;
765
805
  };
766
806
 
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
- };
807
+ /** the props a component target receives: the sources as one merge() view */
808
+ const targetProps = (props: Props, meta: RenderMeta): Props =>
809
+ merge(...targetSources(props, meta)) as Props;
781
810
 
782
811
  /**
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`
812
+ * the client tag path when the keys cannot change: the computed class/style
813
+ * getters, then the author's props copied in without reading them — a
814
+ * getter like icon={<Icon />} renders a child and takes hydration keys, so it
815
+ * is re-homed (see `rehome`) and read by the spread in its own order
786
816
  */
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
817
+ const copyProps = (props: Props, meta: RenderMeta): Props => {
818
+ const out = clientExtras(props, meta);
819
+ // string keys only, like solid's omit(): a symbol never reaches the target.
820
+ // skip drops class (and style with a memo), the keys the extras own
791
821
  for (const key of Object.getOwnPropertyNames(props)) {
792
822
  if (meta.skip(key)) continue;
793
823
  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 });
824
+ if ("value" in descriptor && descriptor.enumerable) out[key] = descriptor.value;
825
+ else Object.defineProperty(out, key, rehome(props, key, descriptor));
819
826
  }
820
827
  return out;
821
828
  };
822
829
 
823
830
  /**
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
831
+ * what the target sees, as the sources solid's own primitives take: omit()
832
+ * with the skip predicate over the author's props ($-props, theme, and the
833
+ * keys attrs bake), the attrs the memo produced as a function source when
834
+ * they can add keys, and the computed class/style last. views, not a copy:
835
+ * a getter like icon={<Icon />} renders a child and takes hydration keys,
836
+ * so it is read by the target in its own order, once. on the server the
837
+ * values are final and go in as data properties; on the client as getters
838
+ * over the memo
828
839
  */
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;
840
+ const targetSources = (props: Props, meta: RenderMeta): [Props, TargetAttrs, Props] => {
841
+ const { skip, compute, classOf } = meta;
842
+ const rest = omit(props, skip as (key: string | symbol) => boolean);
843
+ let extras: Props;
844
+ if (isServer) {
845
+ extras = {};
846
+ if (compute) {
847
+ const computed = compute();
848
+ extras.class = computed.class;
849
+ if (computed.style !== undefined) extras.style = computed.style;
850
+ } else extras.class = classOf(props);
851
+ } else extras = clientExtras(props, meta);
852
+ if (!meta.attrsAddKeys) return [rest, undefined, extras];
853
+ // meta.attrsAddKeys, not the destructured copy: the read narrows meta.compute
854
+ const attrsSource = () => {
855
+ const attrs = meta.compute().attrs;
856
+ return attrs ? omit(attrs, skip as (key: string | symbol) => boolean) : undefined;
857
+ };
858
+ return [rest, attrsSource, extras];
835
859
  };
836
860
 
861
+ /** the attrs source, or none when attrs cannot add keys (baked, or no attrs) */
862
+ type TargetAttrs = (() => Props | undefined) | undefined;
863
+
837
864
  /**
838
- * attrs and reactive spreads can add or remove keys; the $PROXY mark keeps
839
- * downstream omit() calls reactive
865
+ * client only: the computed class and style as getters over the memo. object
866
+ * literals, two shapes: a getter defined on a literal takes V8's boilerplate
867
+ * path
840
868
  */
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] };
869
+ const clientExtras = (props: Props, { compute, classOf }: RenderMeta): Props =>
870
+ compute
871
+ ? {
872
+ get class() {
873
+ // compute().class is accessed lazily and includes props.class in its results
874
+ return compute().class;
875
+ },
876
+ get style() {
877
+ return compute().style;
878
+ },
889
879
  }
890
- return Reflect.getOwnPropertyDescriptor(target, key);
891
- },
892
- });
893
- };
880
+ : {
881
+ get class() {
882
+ return classOf(props);
883
+ },
884
+ };