@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/dist/runtime.js +39 -23
- package/dist/runtime.js.map +6 -6
- package/package.json +4 -4
- package/src/css.ts +30 -8
- package/src/runtime.ts +63 -16
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/runtime",
|
|
3
|
-
"version": "7.
|
|
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.
|
|
37
|
-
"@vue/reactivity": "3.5.
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
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 (
|
|
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 =
|
|
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
|
|
6093
|
-
|
|
6094
|
-
|
|
6095
|
-
|
|
6096
|
-
|
|
6097
|
-
|
|
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;
|