@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/atoms.ts CHANGED
@@ -33,12 +33,14 @@ export const atoms = <T>(
33
33
  staticClasses.length > 0
34
34
  ? [
35
35
  (_, classes) => {
36
- staticClasses.forEach((cls) => classes.add(cls));
36
+ // author strings, not compiler names: the server writer must escape them
37
+ staticClasses.forEach((cls) => classes.add(cls, false));
37
38
  },
38
39
  ...dynamicFunctions,
39
40
  ]
40
41
  : dynamicFunctions;
41
42
 
42
- // @ts-expect-error the internal implementation of css is not typed
43
- return css(...runtimeFunctions);
43
+ // css() is typed for its compiled arguments; the public type describes the
44
+ // interpolation the author writes before compilation
45
+ return css(...runtimeFunctions) as unknown as ComponentStyles<T>;
44
46
  };
@@ -1,22 +1,33 @@
1
1
  import type { YakTheme } from "./index.ts";
2
2
  import type { Accessor } from "solid-js";
3
- import { ClassCollector, RuntimeStyleProcessor, StyleObject } from "./publicStyledApi.js";
3
+ import { ClassCollector, CompiledStyleProcessor, StyleObject } from "./publicStyledApi.js";
4
4
 
5
- export const yakComponentSymbol = Symbol("yak");
5
+ // Symbol.for, not Symbol(): the package ships two bundles (the public entry
6
+ // and the internal one the compiler imports) and both contain this module. a
7
+ // styled component created through one must be recognized by the other
8
+ export const yakComponentSymbol = Symbol.for("@yak/solid:component");
6
9
 
7
10
  /**
8
- * Collects class names as a single space-separated string.
9
- *
10
- * `add` is the render hot path and stays a plain string append;
11
- * `has`/`delete` keep a Set-like contract for the rare runtime
12
- * functions that remove classes again (e.g. atoms).
11
+ * the class names of one render, kept as one space-separated string in the
12
+ * order the style block adds them. add() is the hot path: an append, and a
13
+ * duplicate check only once a second name arrives. has() and delete() exist
14
+ * for runtime processors that take a name back out, such as an atom that
15
+ * reverts a class. a Set would need a split and a join per render for the
16
+ * same string
13
17
  */
14
18
  export class Classes implements ClassCollector {
15
19
  value: string;
20
+ /**
21
+ * false once a name came from the author, such as an atom. compiler names
22
+ * are safe to print as they are; serializeElement in styled.ts escapes the
23
+ * class once this is false
24
+ */
25
+ generated = true;
16
26
  constructor(initial?: string) {
17
27
  this.value = initial || "";
18
28
  }
19
- add(name: string) {
29
+ add(name: string, generated = true) {
30
+ if (!generated) this.generated = false;
20
31
  if (!this.value) {
21
32
  this.value = name;
22
33
  } else if (!this.has(name)) {
@@ -61,9 +72,9 @@ type CSSStyles<TProps = {}> = {
61
72
  style: { [key: string]: string | ((props: TProps) => string) };
62
73
  };
63
74
 
64
- type CSSFunction = <TProps = {}>(
75
+ export type CSSFunction = <TProps>(
65
76
  styles: TemplateStringsArray,
66
- ...values: CSSInterpolation<TProps & { theme: Accessor<YakTheme> }>[]
77
+ ...values: CSSInterpolation<NoInfer<TProps> & { theme: Accessor<YakTheme> }>[]
67
78
  ) => ComponentStyles<TProps>;
68
79
 
69
80
  export type NestedRuntimeStyleProcessor = (
@@ -76,59 +87,52 @@ export type NestedRuntimeStyleProcessor = (
76
87
  style?: StyleObject;
77
88
  }
78
89
  | void
90
+ | false
91
+ | null
92
+ | string
93
+ | number
79
94
  | NestedRuntimeStyleProcessor;
80
95
 
81
96
  /**
82
- * css() runtime factory of css``
97
+ * the runtime behind css``. the compiler rewrites every css`` and styled``
98
+ * call, so what arrives here is never the template the author wrote but its
99
+ * compiled form: class names, callbacks and css-variable maps (examples in
100
+ * the loop below). the public typings describe the call before compilation,
101
+ * which is why this function is internal: mocks/cssLiteral.ts exports the
102
+ * css the author sees, cast to the public type.
83
103
  *
84
- * /!\ @yak/solid transpiles css`` and styled``
85
- *
86
- * This changes the typings of the css`` and styled`` functions.
87
- * During development the user of @yak/solid wants to work with the
88
- * typings BEFORE compilation.
89
- *
90
- * Therefore this is only an internal function only and it must be cast to any
91
- * before exported to the user.
92
- *
93
- * The internal functioning of css`` is to return a single callback function that runs all functions
94
- * (or creates new ones if needed) that are passed as arguments. These functions receive the props, classes, and style object as arguments
95
- * and operate directly on the classes and style objects.
104
+ * it returns one processor, (props, classes, style) => void, that runs every
105
+ * compiled piece: class names go into the collector, css values into the
106
+ * style object
96
107
  */
97
- export function css<TProps>(
98
- styles: TemplateStringsArray,
99
- ...values: CSSInterpolation<NoInfer<TProps> & { theme: Accessor<YakTheme> }>[]
100
- ): ComponentStyles<TProps>;
101
- export function css<TProps>(...args: Array<any>): RuntimeStyleProcessor<TProps> {
102
- // Normally this could be an array of strings passed, but as we transpile the usage of css`` ourselves, we control the arguments
103
- // and ensure that only the first argument is a string (class name of the non-dynamic styles)
108
+ export function css<TProps>(...args: Array<any>): CompiledStyleProcessor<TProps> {
104
109
  let staticClass: string | undefined;
105
110
  const dynamicCssFunctions: NestedRuntimeStyleProcessor[] = [];
106
- for (const arg of args as Array<string | CSSFunction | CSSStyles<any>>) {
107
- // A CSS-module class name which got auto generated during build from static css
108
- // e.g. css`color: red;`
109
- // compiled -> css("yak31e4")
111
+ for (const arg of args as Array<string | NestedRuntimeStyleProcessor | CSSStyles<any>>) {
112
+ // static css became a css-module class name at build time:
113
+ // css`color: red;` -> css("yak31e4")
110
114
  if (typeof arg === "string") {
111
115
  staticClass = arg;
112
116
  }
113
- // Dynamic CSS e.g.
114
- // css`${props => props.active && css`color: red;`}`
115
- // compiled -> css((props: { active: boolean }) => props.active && css("yak31e4"))
117
+ // conditional css stays a callback that returns another compiled css():
118
+ // css`${props => props.active && css`color: red;`}`
119
+ // -> css(props => props.active && css("yak31e4"))
116
120
  else if (typeof arg === "function") {
117
- dynamicCssFunctions.push(arg as unknown as NestedRuntimeStyleProcessor);
121
+ dynamicCssFunctions.push(arg);
118
122
  }
119
- // Dynamic CSS with css variables e.g.
120
- // css`transform: translate(${props => props.x}, ${props => props.y});`
121
- // compiled -> css("yak31e4", { style: { "--yakVarX": props => props.x }, "--yakVarY": props => props.y }})
123
+ // a css value became a variable the callback fills at render time:
124
+ // css`transform: translate(${props => props.x});`
125
+ // -> css("yak31e4", { style: { "--yakVarX": props => props.x } })
122
126
  else if (typeof arg === "object" && "style" in arg) {
123
127
  dynamicCssFunctions.push((props, _, style) => {
124
128
  for (const key in arg.style) {
125
129
  const value = arg.style[key];
126
130
  if (typeof value === "function") {
127
131
  style[key as keyof StyleObject] = String(
128
- // The value for a css value can be a theme dependent function e.g.:
129
- // const borderColor = (props: { theme: { mode: "dark" | "light" } }) => props.theme === "dark" ? "black" : "white";
130
- // css`border-color: ${borderColor};`
131
- // Therefore the value has to be extracted recursively
132
+ // a callback may return another callback before it yields the
133
+ // value, such as a theme-dependent one:
134
+ // const color = (props) => props.theme().mode === "dark" ? "black" : "white";
135
+ // css`border-color: ${color};`
132
136
  recursivePropExecution(props, value),
133
137
  ) as never;
134
138
  } else {
@@ -139,10 +143,8 @@ export function css<TProps>(...args: Array<any>): RuntimeStyleProcessor<TProps>
139
143
  }
140
144
  }
141
145
 
142
- // Non Dynamic CSS
143
- // This is just an optimization for the common case where there are no dynamic css functions
144
- // `$dynamic: false` lets the styled runtime skip theme lookup and
145
- // style-object allocation entirely for static components
146
+ // no dynamic parts, the common case: $dynamic false lets styled() skip the
147
+ // theme lookup and the style object for the component
146
148
  if (dynamicCssFunctions.length === 0) {
147
149
  return Object.assign(
148
150
  (_: unknown, classes: ClassCollector) => {
@@ -150,8 +152,8 @@ export function css<TProps>(...args: Array<any>): RuntimeStyleProcessor<TProps>
150
152
  classes.add(staticClass);
151
153
  }
152
154
  },
153
- { $dynamic: false },
154
- );
155
+ { $dynamic: false as const },
156
+ ) satisfies CompiledStyleProcessor<TProps>;
155
157
  }
156
158
 
157
159
  return Object.assign(
@@ -160,15 +162,15 @@ export function css<TProps>(...args: Array<any>): RuntimeStyleProcessor<TProps>
160
162
  classes.add(staticClass);
161
163
  }
162
164
  for (let i = 0; i < dynamicCssFunctions.length; i++) {
163
- unwrapProps(props, dynamicCssFunctions[i], classes, allStyles);
165
+ runProcessor(props, dynamicCssFunctions[i], classes, allStyles);
164
166
  }
165
167
  },
166
- { $dynamic: true },
167
- );
168
+ { $dynamic: true as const },
169
+ ) satisfies CompiledStyleProcessor<TProps>;
168
170
  }
169
171
 
170
- // Dynamic CSS with runtime logic
171
- const unwrapProps = (
172
+ /** run one processor and fold what it returns into the collector and the style object */
173
+ const runProcessor = (
172
174
  props: unknown,
173
175
  fn: NestedRuntimeStyleProcessor,
174
176
  classes: ClassCollector,
@@ -186,7 +188,7 @@ const unwrapProps = (
186
188
  }
187
189
  if ("style" in result && result.style) {
188
190
  for (const key in result.style) {
189
- // This is hard for typescript to infer
191
+ // both objects use StyleObject; typescript loses the key/value relation in this loop
190
192
  style[key as keyof StyleObject] = result.style[key as keyof StyleObject] as any;
191
193
  }
192
194
  }
@@ -22,5 +22,3 @@ declare module "@solidjs/web" {
22
22
  }
23
23
  }
24
24
  }
25
-
26
- export {};
@@ -27,6 +27,7 @@ export const normalizeClass = (value: unknown): string => {
27
27
  * ```tsx
28
28
  * <div class={__yak_mergeClassNames("yX", active() && "active")} />
29
29
  * ```
30
+ * combineProps in styled.ts uses it for two attrs layers' classes as well.
30
31
  */
31
32
  export const mergeClasses = (yakClass: string, userClass: unknown): string | undefined => {
32
33
  const user = normalizeClass(userClass);
@@ -1,54 +1,82 @@
1
+ import { isServer } from "@solidjs/web";
1
2
  import { Classes } from "../cssLiteral.js";
2
- import { RuntimeStyleProcessor } from "../publicStyledApi.js";
3
+ import { RuntimeStyleProcessor, StyleObject } from "../publicStyledApi.js";
4
+ import { normalizeClass } from "./mergeClasses.js";
5
+
6
+ type Source = Record<PropertyKey, unknown>;
7
+
8
+ /** the last source that carries the key */
9
+ const lastWith = (sources: Source[], key: string): Source | undefined => {
10
+ for (let index = sources.length - 1; index >= 0; index--) {
11
+ if (key in sources[index]) return sources[index];
12
+ }
13
+ return undefined;
14
+ };
3
15
 
4
16
  /**
5
- * This is an internal helper function to merge relevant props of a native element with a css prop.
6
- * It's automatically added when using the `css` prop in a JSX element.
7
- * e.g.:
17
+ * Merges the relevant props of a native element with a css prop. The
18
+ * compiler adds it for the `css` prop:
19
+ * ```tsx
20
+ * <button class="a" {...props} style={s} css={css`color: green;`} />
21
+ * ```
22
+ * compiles to
8
23
  * ```tsx
9
- * <p
10
- * class="foo"
11
- * css={css`
12
- * color: green;
13
- * `}
14
- * {...{ style: { padding: "30px" }}}
15
- * />
24
+ * <button {...__yak_mergeCssProp(css("yak1"), { class: "a" }, props, { style: s })} />
25
+ * ```
16
26
  */
17
27
  export const mergeCssProp = (
18
- relevantProps: {
19
- class?: string;
20
- style?: Record<string, string>;
21
- } & Record<string, unknown>,
22
28
  cssProp: RuntimeStyleProcessor<unknown> | false | null | undefined,
29
+ ...sources: (Source | null | undefined)[]
23
30
  ) => {
24
- const classes = new Classes(relevantProps.class);
25
-
26
- const existingStyle = relevantProps.style;
27
- const style = existingStyle ? { ...existingStyle } : {};
28
-
29
- // a falsy css prop applies no styles, e.g. `css={on && css`...`}` with `on` false
30
- if (cssProp) {
31
- cssProp({}, classes, style);
31
+ const present = sources.filter((source): source is Source => source != null);
32
+ const out: Source = {};
33
+ for (const source of present) {
34
+ for (const key of Reflect.ownKeys(source)) {
35
+ // contributed below, from the last source that carries them
36
+ if (key === "class" || key === "style") continue;
37
+ const descriptor = Reflect.getOwnPropertyDescriptor(source, key)!;
38
+ if (descriptor.get || descriptor.set || !descriptor.enumerable) {
39
+ Object.defineProperty(out, key, descriptor);
40
+ } else {
41
+ out[key] = descriptor.value;
42
+ }
43
+ }
32
44
  }
45
+ // `in` finds the owner without invoking its getter
46
+ const classOwner = lastWith(present, "class");
47
+ const styleOwner = lastWith(present, "style");
33
48
 
34
- // Forward all other props (onClick, aria-*, id, …) untouched and only
35
- // override class/style with the merged result — the transform already
36
- // built `relevantProps` in JSX attribute order, so this preserves overrides.
37
- const result: Record<string, unknown> & {
38
- class?: string;
39
- style?: Record<string, string>;
40
- } = { ...relevantProps };
49
+ const mergedClass = (): string | undefined => {
50
+ const classes = new Classes(normalizeClass(classOwner?.class));
51
+ // a falsy css prop applies no styles, e.g. `css={on && css`...`}` with `on` false
52
+ if (cssProp) cssProp({}, classes, {} as StyleObject);
53
+ return classes.value || undefined;
54
+ };
55
+ const mergedStyle = (): StyleObject | undefined => {
56
+ const base = styleOwner?.style as StyleObject | undefined;
57
+ const style: StyleObject = base ? { ...base } : {};
58
+ if (cssProp) cssProp({}, new Classes(), style);
59
+ for (const _ in style) return style;
60
+ return undefined;
61
+ };
41
62
 
42
- if (Object.keys(style).length > 0) {
43
- result.style = style;
44
- } else {
45
- delete result.style;
63
+ if (isServer) {
64
+ // The server reads once, so the values are resolved here; a class or
65
+ // style read takes no hydration id. A key left undefined would make
66
+ // `ssrElement` write `class=""`, so only carried values are set.
67
+ const className = mergedClass();
68
+ if (className !== undefined) out.class = className;
69
+ const style = mergedStyle();
70
+ if (style !== undefined) out.style = style;
71
+ return out;
46
72
  }
47
- if (classes.value) {
48
- result.class = classes.value;
49
- } else {
50
- delete result.class;
73
+ // on the client the spread reads these inside its effect, so a signal read
74
+ // by a source or by the css prop updates the attribute in place
75
+ if (cssProp || classOwner) {
76
+ Object.defineProperty(out, "class", { get: mergedClass, enumerable: true, configurable: true });
51
77
  }
52
-
53
- return result;
78
+ if (cssProp || styleOwner) {
79
+ Object.defineProperty(out, "style", { get: mergedStyle, enumerable: true, configurable: true });
80
+ }
81
+ return out;
54
82
  };
@@ -1,6 +1,4 @@
1
- import type { css as cssInternal, NestedRuntimeStyleProcessor } from "../cssLiteral.js";
2
-
3
- export type { ComponentStyles, CSSInterpolation } from "../cssLiteral.js";
1
+ import type { CSSFunction, NestedRuntimeStyleProcessor } from "../cssLiteral.js";
4
2
 
5
3
  /**
6
4
  * Allows to use CSS styles in a styled or css block
@@ -14,7 +12,7 @@ export type { ComponentStyles, CSSInterpolation } from "../cssLiteral.js";
14
12
  * `;
15
13
  * ```
16
14
  */
17
- export const css: typeof cssInternal = (styles: TemplateStringsArray, ...args: unknown[]) => {
15
+ export const css: CSSFunction = (styles: TemplateStringsArray, ...args: unknown[]) => {
18
16
  // When called in yak files as a template tag (without SWC transformation),
19
17
  // return { __yak: rawCss } so the cross-file resolver can
20
18
  // extract the mixin value from evaluated .yak files.
@@ -2,7 +2,11 @@ import type { JSX } from "@solidjs/web";
2
2
  import { styled as StyledFactory } from "../styled.js";
3
3
 
4
4
  export const styled = /* @__PURE__ */ new Proxy(StyledFactory, {
5
- get(target, TagName: keyof JSX.IntrinsicElements) {
6
- return target(TagName);
5
+ get(target, key) {
6
+ // only a tag name becomes a factory. symbols, the function's own
7
+ // properties and `then` pass through, so a promise that resolves the
8
+ // factory does not take it for a thenable
9
+ if (typeof key !== "string" || key === "then" || key in target) return Reflect.get(target, key);
10
+ return target(key as keyof JSX.IntrinsicElements);
7
11
  },
8
12
  }) as typeof StyledFactory;
@@ -43,7 +43,7 @@ export interface StyledFn {
43
43
  */
44
44
  export interface YakComponent<T> extends AnyComponent<T> {
45
45
  // This is intentionally typed to hide the internal implementation details.
46
- [yakComponentSymbol]: [unknown, unknown, unknown, unknown];
46
+ [yakComponentSymbol]: readonly [unknown, unknown, unknown];
47
47
  }
48
48
 
49
49
  /**
@@ -143,14 +143,14 @@ export type FastOmit<T extends object, U extends string | number | symbol> = {
143
143
  };
144
144
 
145
145
  /**
146
- * Set-like collector for class names.
147
- *
148
- * Implemented as a string builder in the runtime (a Set<string>
149
- * split → Set → Array.from → join round-trip dominates render cost);
150
- * a real Set<string> also satisfies this interface.
146
+ * Collects the class names of one render. The runtime keeps them in one
147
+ * space-separated string (Classes in cssLiteral.ts, the only implementation):
148
+ * add() appends, has() and delete() let a runtime processor take a name back
149
+ * out. A Set would need a split and a join on every render for the same string.
151
150
  */
152
151
  export type ClassCollector = {
153
- add(name: string): void;
152
+ /** generated is false for an author string such as an atom; the server writer escapes those */
153
+ add(name: string, generated?: boolean): void;
154
154
  has(name: string): boolean;
155
155
  delete(name: string): void;
156
156
  };
@@ -168,6 +168,18 @@ export type RuntimeStyleProcessor<T> = ((
168
168
  style: StyleObject,
169
169
  ) => void) & { $dynamic?: boolean };
170
170
 
171
+ /** A class-only processor ignores props and needs no style object. */
172
+ export type StaticStyleProcessor = ((
173
+ props: unknown,
174
+ classes: ClassCollector,
175
+ style?: StyleObject,
176
+ ) => void) & { $dynamic: false };
177
+
178
+ /** css() marks its output so styled() can select the static render path. */
179
+ export type CompiledStyleProcessor<T> =
180
+ | StaticStyleProcessor
181
+ | (RuntimeStyleProcessor<T> & { $dynamic: true });
182
+
171
183
  /**
172
184
  * Utility type to keep the generic API of a component while still being able to use it in a selector
173
185
  */