@jxsuite/runtime 7.0.0 → 7.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/runtime",
3
- "version": "7.0.0",
3
+ "version": "7.1.0",
4
4
  "description": "JSON-native reactive web component runtime",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -33,12 +33,12 @@
33
33
  "upgrade": "bun update --latest && bun install"
34
34
  },
35
35
  "dependencies": {
36
- "@jxsuite/schema": "^6.0.0",
37
- "@vue/reactivity": "3.5.44"
36
+ "@jxsuite/schema": "^6.0.2",
37
+ "@vue/reactivity": "3.5.45"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@happy-dom/global-registrator": "^21.0.4",
41
- "@jxsuite/erase": "^0.3.0",
41
+ "@jxsuite/erase": "^0.3.2",
42
42
  "@jxsuite/test-support": "^0.0.0"
43
43
  }
44
44
  }
package/src/css.ts CHANGED
@@ -507,8 +507,8 @@ export interface CssRule {
507
507
  * It is NOT in {@link CssRule.text}, and that is the point. `text` is what a sheet inserts and
508
508
  * what {@link hashCss} interns, so prose there would make two identical rules two rules and put a
509
509
  * paragraph in every adopted sheet at runtime. A static emitter — the one writing a stylesheet a
510
- * person will read — renders it as a comment; every other consumer ignores it, exactly as a
511
- * document's own `$description` is ignored.
510
+ * person will read — renders it as a comment through {@link commentedRuleText}; every other
511
+ * consumer ignores it, exactly as a document's own `$description` is ignored.
512
512
  */
513
513
  description?: string;
514
514
  /** See {@link CssRuleTarget}. */
@@ -521,8 +521,8 @@ export interface CssRule {
521
521
  export interface CssBuildOptions {
522
522
  /**
523
523
  * The selector the style object hangs off. Every nested key resolves against it, so the runtime
524
- * passes a placeholder it substitutes afterwards and the compiler passes `#id` / `.jx-N` / a
525
- * tag.
524
+ * passes a placeholder it substitutes afterwards and the compiler passes an element's generated
525
+ * class or a component's tag.
526
526
  */
527
527
  scope?: string | null;
528
528
  /** The project's `$media` map, for resolving `@--name` keys. */
@@ -614,6 +614,25 @@ export function cssRuleText(
614
614
  return text;
615
615
  }
616
616
 
617
+ /**
618
+ * A rule as a static build writes it: its `$description` as a comment on the line above, then its
619
+ * text (spec.md §9.2). An undescribed rule is its text, byte for byte.
620
+ *
621
+ * CSS comments have no escape, so a comment terminator in the prose (an asterisk and then a slash)
622
+ * is written with a space between the two: breaking the sequence is the only way to keep the
623
+ * comment from ending early. The prose is otherwise verbatim, newlines included.
624
+ *
625
+ * @param {Pick<CssRule, "text" | "description">} rule
626
+ * @returns {string}
627
+ * @docs framework/concepts/styling
628
+ */
629
+ export function commentedRuleText(rule: Pick<CssRule, "text" | "description">): string {
630
+ if (rule.description === undefined) {
631
+ return rule.text;
632
+ }
633
+ return `/* ${rule.description.replaceAll("*/", "* /")} */\n${rule.text}`;
634
+ }
635
+
617
636
  /**
618
637
  * Serialize one `@keyframes` block: every stop, in authored order, inside ONE at-rule, wrapped in
619
638
  * its enclosing conditions.
@@ -815,6 +834,10 @@ export function buildStyleRules(style: JxStyle, options: CssBuildOptions = {}):
815
834
  return declarations;
816
835
  };
817
836
 
837
+ /** A block's `$description` as the member a rule carries, when it wrote a non-empty one. */
838
+ const describedBy = (description: JxStyle[string]): { description?: string } =>
839
+ isString(description) && description !== "" ? { description } : {};
840
+
818
841
  const emit = (
819
842
  conditions: readonly string[],
820
843
  selector: string | null,
@@ -842,10 +865,7 @@ export function buildStyleRules(style: JxStyle, options: CssBuildOptions = {}):
842
865
  target,
843
866
  key: hashCss(text),
844
867
  };
845
- if (isString(description) && description !== "") {
846
- rule.description = description;
847
- }
848
- rules.push(rule);
868
+ rules.push({ ...rule, ...describedBy(description) });
849
869
  };
850
870
 
851
871
  /**
@@ -884,6 +904,7 @@ export function buildStyleRules(style: JxStyle, options: CssBuildOptions = {}):
884
904
  return;
885
905
  }
886
906
  const text = cssKeyframesText(conditions, name, blocks);
907
+ // The block's own `$description` documents the rule; a stop's is ignored, as a stop is no rule.
887
908
  rules.push({
888
909
  blocks,
889
910
  conditions: [...conditions],
@@ -892,6 +913,7 @@ export function buildStyleRules(style: JxStyle, options: CssBuildOptions = {}):
892
913
  selector: null,
893
914
  target: "unscoped",
894
915
  text,
916
+ ...describedBy(block["$description"]),
895
917
  });
896
918
  };
897
919
 
package/src/runtime.ts CHANGED
@@ -5031,15 +5031,27 @@ export function isSignal(v: unknown): v is Ref<unknown> {
5031
5031
  * type. The default is the value the entry had before anyone set the attribute, which is what the
5032
5032
  * removal is asking to go back to.
5033
5033
  *
5034
- * @param defaults The declared defaults by state key; a key absent from it has none.
5034
+ * A COMPUTED entry is never written, by a change or by a removal: its value is produced, and
5035
+ * attribute text replacing a `$prototype` or `$src` entry's function or record is not something a
5036
+ * removal could give back. `declaredDefaults` records such an entry as present with the value
5037
+ * `undefined`, which is what this reads.
5038
+ *
5039
+ * Exported for the compiler, which inlines the rule into generated modules and calls it for the
5040
+ * prerender; `observed-attr-agreement.test.ts` there proves the copy agrees.
5041
+ *
5042
+ * @param defaults The declared defaults by state key; a key absent from it has none, and a key
5043
+ * present with `undefined` is computed.
5035
5044
  */
5036
- function absorbAttribute(
5045
+ export function absorbAttribute(
5037
5046
  state: JxScope,
5038
5047
  name: string,
5039
5048
  value: string | null,
5040
5049
  defaults?: ReadonlyMap<string, JxScopeValue>,
5041
5050
  ): void {
5042
5051
  const camelKey = name.replaceAll(/-([a-z])/g, (_: string, c: string) => c.toUpperCase());
5052
+ if (defaults?.has(camelKey) && defaults.get(camelKey) === undefined) {
5053
+ return;
5054
+ }
5043
5055
  const current = state[camelKey];
5044
5056
  if (isBoolean(current)) {
5045
5057
  // Presence IS the value for a boolean, so a removal is already spelled by the same rule.
@@ -5055,25 +5067,44 @@ function absorbAttribute(
5055
5067
  }
5056
5068
 
5057
5069
  /**
5058
- * The declared default of every state entry that has one, by state key.
5070
+ * The declared default of every state entry, by state key, read the way `buildScope` reads the
5071
+ * entry's shape:
5072
+ *
5073
+ * - An object with `default` declares that value.
5074
+ * - A computed entry (a marker of {@link COMPUTED_STATE_KEYS}, or a `${…}` template string) is
5075
+ * recorded as PRESENT with the value `undefined`: its value is produced, so it has no default and
5076
+ * an observed attribute never writes it ({@link absorbAttribute}).
5077
+ * - A pure type definition (`{ "type": "number" }`), which `buildScope` leaves out of state, is
5078
+ * absent: it declares no default, and recording the schema object as one wrote that object into
5079
+ * state when its attribute was removed.
5080
+ * - Anything else is its own default: the shorthand `{ "label": "none" }`, a plain object, an array.
5081
+ *
5082
+ * Exported for the compiler, which serializes the observed keys' defaults into generated modules;
5083
+ * `observed-attr-agreement.test.ts` there proves the emitted map equals this function's output.
5059
5084
  *
5060
5085
  * @param def The element definition
5061
5086
  * @returns {Map<string, JxScopeValue>} Key → default
5062
5087
  */
5063
- function declaredDefaults(def: JxElement): Map<string, JxScopeValue> {
5088
+ export function declaredDefaults(def: JxElement): Map<string, JxScopeValue> {
5064
5089
  const out = new Map<string, JxScopeValue>();
5065
5090
  for (const [key, entry] of Object.entries(def.state ?? {})) {
5066
- if (isNonNullObject(entry)) {
5091
+ if (isString(entry) && isTemplateString(entry)) {
5092
+ out.set(key, undefined);
5093
+ continue;
5094
+ }
5095
+ if (isNonNullObject(entry) && !Array.isArray(entry)) {
5067
5096
  if ("default" in entry) {
5068
5097
  out.set(key, entry.default);
5069
5098
  continue;
5070
5099
  }
5071
- // A computed entry has no default: its value is produced, and a removal leaves it alone.
5072
5100
  if (COMPUTED_STATE_KEYS.some((marker) => marker in entry)) {
5101
+ out.set(key, undefined);
5102
+ continue;
5103
+ }
5104
+ if (hasSchemaKeywords(entry)) {
5073
5105
  continue;
5074
5106
  }
5075
5107
  }
5076
- // The shorthand `{ "label": "none" }` IS the default, the same way `buildScope` reads it.
5077
5108
  out.set(key, entry);
5078
5109
  }
5079
5110
  return out;
@@ -5113,11 +5144,14 @@ const _privatePropWarned = new Set<string>();
5113
5144
  * declares one. What the accessor DOES do is write the attribute, so the attribute is the evidence
5114
5145
  * that somebody set it, whether that was `$props` before connection or the author in the document.
5115
5146
  *
5147
+ * Exported for the compiler, which inlines the rule into generated modules; a compiler test proves
5148
+ * the copy agrees.
5149
+ *
5116
5150
  * @param {HTMLElement} el - The instance being connected.
5117
5151
  * @param {string} key - A state key declared by the component definition.
5118
5152
  * @returns {boolean}
5119
5153
  */
5120
- function instanceSupplies(el: HTMLElement, key: string): boolean {
5154
+ export function instanceSupplies(el: HTMLElement, key: string): boolean {
5121
5155
  if (Object.hasOwn(el, key)) {
5122
5156
  return true;
5123
5157
  }
@@ -5199,6 +5233,7 @@ export {
5199
5233
  camelToKebab,
5200
5234
  COLOR_SCHEME_ATTR,
5201
5235
  COLOR_SCHEME_STORAGE_KEY,
5236
+ commentedRuleText,
5202
5237
  cssPropertyName,
5203
5238
  cssRuleText,
5204
5239
  hashCss,
@@ -5746,6 +5781,11 @@ export async function defineElement(source: string | JxDocument, baseUrl?: strin
5746
5781
  return;
5747
5782
  }
5748
5783
  if (this._jxInitialized) {
5784
+ /* Put back in a document (moved to another parent, or removed and inserted). Everything
5785
+ below happens to an instance once, `onMount` included (§16.4): a sidecar wires up what it
5786
+ was handed at the first insertion and is not handed it twice, which is what lets a
5787
+ slotted element, moved by its parent's slot distribution straight after it rendered,
5788
+ mount once. `onUnmount` still runs on every removal. */
5749
5789
  return;
5750
5790
  }
5751
5791
  this._jxInitialized = true;
@@ -5777,9 +5817,10 @@ export async function defineElement(source: string | JxDocument, baseUrl?: strin
5777
5817
  for an attribute set before connection too — but on an instance with no state yet, so it
5778
5818
  had nothing to write into, and `<jx-icon name="plus">` rendered with the default. Before
5779
5819
  the `$props` merges below, so a property a parent set still wins (spec §16.2). */
5820
+ const connectDefaults = declaredDefaults(def);
5780
5821
  for (const attr of observedAttrs) {
5781
5822
  if (this.hasAttribute(attr)) {
5782
- absorbAttribute(state, attr, this.getAttribute(attr));
5823
+ absorbAttribute(state, attr, this.getAttribute(attr), connectDefaults);
5783
5824
  // The raw string, not the coerced value: coercion is a function of it, so two equal
5784
5825
  // Strings are one input.
5785
5826
  supplied[attr] = this.getAttribute(attr);
@@ -5997,7 +6038,11 @@ export async function defineElement(source: string | JxDocument, baseUrl?: strin
5997
6038
  the ones a prop can write: a computed entry is what threw, and reading it here would
5998
6039
  throw again. Writes are coalesced into one microtask, because a parent sets its props one
5999
6040
  at a time. The same error is reported once, not once per attempt. */
6000
- const inputs = [...declaredDefaults(def).keys()].filter((key) => !isPrivateStateKey(key));
6041
+ const inputs = Object.keys(def.state ?? {}).filter(
6042
+ (key) =>
6043
+ !isPrivateStateKey(key) &&
6044
+ !(connectDefaults.has(key) && connectDefaults.get(key) === undefined),
6045
+ );
6001
6046
  let lastMessage = failure.message;
6002
6047
  let queued = false;
6003
6048
  const watch = effect(
@@ -6089,12 +6134,14 @@ export async function defineElement(source: string | JxDocument, baseUrl?: strin
6089
6134
  function writeInstanceProp(el: HTMLElement, key: string, resolved: JxScopeValue): void {
6090
6135
  const value = isString(resolved) ? canvasAssetValue(el.tagName, key, resolved) : resolved;
6091
6136
  elementProperties(el)[key] = value;
6092
- /* A declared prop that shares its name with an `HTMLElement` property (`title`, `lang`) gets
6093
- no forwarding accessor (§16.2), so the write above lands on the element and never reaches
6094
- the instance's state. Connection reads it once; a binding that changes AFTER connection
6095
- (a row whose data arrives later) was lost, and the instance went on drawing the value it
6096
- connected with. The binding is the parent's `$props`, addressed to the definition's prop,
6097
- so it writes that prop's state. */
6137
+ /* A prop that shares its name with an `HTMLElement` property (`title`, `lang`) is forwarded
6138
+ like any other once the instance has connected (§16.2): its accessor calls the platform's
6139
+ setter, then writes state, so the write above already reaches state there. The write below
6140
+ covers the instance that holds the name in its state with NO accessor for it, which is a
6141
+ name its live definition does not declare and it was not given at connection (an inherited
6142
+ project formula, §13.4). There the write above lands on the platform's property alone, and
6143
+ the instance would go on drawing the value it connected with. The binding is the parent's
6144
+ `$props`, addressed to the instance's state, so it writes that state. */
6098
6145
  const instanceState = instanceStateOf(el);
6099
6146
  if (instanceState && key in HTMLElement.prototype && Object.hasOwn(toRaw(instanceState), key)) {
6100
6147
  instanceState[key] = value;