@webjsdev/core 0.7.49 → 0.7.50
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/webjs-core-browser.js +4 -4
- package/dist/webjs-core-browser.js.map +3 -3
- package/dist/webjs-core-testing.js +18 -18
- package/dist/webjs-core-testing.js.map +3 -3
- package/dist/webjs-core.js +5 -5
- package/dist/webjs-core.js.map +3 -3
- package/package.json +1 -1
- package/src/attribute-reader.js +178 -0
- package/src/component.d.ts +10 -0
- package/src/component.js +24 -59
- package/src/html-entities.js +2179 -0
- package/src/render-client.js +4 -5
- package/src/render-server.js +183 -112
- package/src/webjs-config.d.ts +13 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webjsdev/core",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.50",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The runtime for WebJs, a full-stack JavaScript framework built on web components with server-side rendering and no build step. Ships the html and css template tags, the WebComponent base class, signals, directives, and the isomorphic renderers.",
|
|
6
6
|
"types": "./index.d.ts",
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE attribute reader, singular.
|
|
3
|
+
*
|
|
4
|
+
* `attributeChangedCallback` in `component.js` and `applyAttrsToInstance` in
|
|
5
|
+
* `render-server.js` are both thin callers of the two functions below, so the
|
|
6
|
+
* client and the SSR pass cannot drift on precedence, on a fallback, or on
|
|
7
|
+
* which attributes they read at all. They had drifted four ways before this
|
|
8
|
+
* module existed: the unparseable-JSON fallback (#1253), a missing converter
|
|
9
|
+
* arm (#1340), and three name-resolution gaps plus the entity decoding (#1341).
|
|
10
|
+
*
|
|
11
|
+
* The split between the two exports is the one place the sides legitimately
|
|
12
|
+
* differ. `resolveAttributeProperty` takes the name the PLATFORM would deliver,
|
|
13
|
+
* so the SSR caller lowercases its source name first and the client passes the
|
|
14
|
+
* browser's name straight through. Everything after that is common.
|
|
15
|
+
*
|
|
16
|
+
* WHY ITS OWN MODULE, rather than living beside the declaration semantics it
|
|
17
|
+
* interprets in `component.js`. That was the first shape, and `component.d.ts`
|
|
18
|
+
* cannot carry the declaration for it: `index.d.ts` re-exports that overlay
|
|
19
|
+
* with a bare `export *`, so a value declared there joins the ROOT public type
|
|
20
|
+
* surface of `@webjsdev/core` while `index.js` exports no such runtime value.
|
|
21
|
+
* An app writing `import { readAttributeValue } from '@webjsdev/core'` would
|
|
22
|
+
* then type-check and crash at load, which is exactly the phantom the #1031
|
|
23
|
+
* guard in `test/types/dts-no-phantom-exports.test.mjs` exists to catch, and it
|
|
24
|
+
* caught this one. Re-exporting the function from `index.js` would silence the
|
|
25
|
+
* guard by making an internal framework seam app-facing API, which is the wrong
|
|
26
|
+
* trade. A module with no `.d.ts` overlay keeps the seam internal and typed
|
|
27
|
+
* from its own JSDoc, which is how `escape.js` and `binding-prefixes.js`
|
|
28
|
+
* already work.
|
|
29
|
+
*
|
|
30
|
+
* It also drops an edge rather than adding one: `render-server.js` reaches the
|
|
31
|
+
* reader without importing `component.js` at all.
|
|
32
|
+
*
|
|
33
|
+
* lit solves the same problem structurally, though in the other direction.
|
|
34
|
+
* `@lit-labs/ssr`'s LitElementRenderer forwards its `attributeChangedCallback`
|
|
35
|
+
* into the element's own `_$attributeToProperty` through
|
|
36
|
+
* `lit-element/private-ssr-support.js`, so its server pass runs the browser's
|
|
37
|
+
* reader rather than a copy of it. lit can reach INTO the element because its
|
|
38
|
+
* elements are TypeScript with a generated declaration; WebJs ships buildless
|
|
39
|
+
* JS under a hand-written overlay, so the shared code moves out instead.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Read one attribute string into its declared property value.
|
|
44
|
+
*
|
|
45
|
+
* The caller owns only the one platform-specific step, lowercasing the source
|
|
46
|
+
* name at SSR; `resolveAttributeProperty` below owns NAME resolution and
|
|
47
|
+
* declaration normalisation for both sides, and this function owns the value.
|
|
48
|
+
* The two callers used to reach an attribute by different routes and NOT see
|
|
49
|
+
* the same attribute set, which #1341 closed.
|
|
50
|
+
*
|
|
51
|
+
* @param {import('./component.js').PropertyDeclaration} def normalised declaration (`{ type, … }`)
|
|
52
|
+
* @param {string|null} value the attribute text
|
|
53
|
+
*
|
|
54
|
+
* BOTH callers hand in an already-decoded value, so no branch decodes anything
|
|
55
|
+
* (#1341). The client's came out of the DOM, which decodes every character
|
|
56
|
+
* reference before any reader sees it; the SSR caller runs `decodeAttrEntities`
|
|
57
|
+
* once at its own call site, ahead of this function, over EVERY branch rather
|
|
58
|
+
* than only the ones that parse. This used to be a third `decode` parameter
|
|
59
|
+
* applied to the JSON and converter branches alone, which left a `String`-typed
|
|
60
|
+
* prop holding the raw source text at SSR while `getAttribute()` on the client
|
|
61
|
+
* returned the decoded one.
|
|
62
|
+
* @returns {unknown}
|
|
63
|
+
*/
|
|
64
|
+
export function readAttributeValue(def, value) {
|
|
65
|
+
if (def.converter && def.converter.fromAttribute) {
|
|
66
|
+
// Deliberately UNGUARDED, and deliberately first (#1340). An author who
|
|
67
|
+
// supplies a converter owns the whole read, which is the same rule
|
|
68
|
+
// `_reflectAttribute` already states for `toAttribute`: its guards sit
|
|
69
|
+
// AFTER the converter branch because a custom converter is
|
|
70
|
+
// author-controlled. So a converter that throws throws, on both sides. At
|
|
71
|
+
// SSR that lands in per-component error isolation (render-server.js), which
|
|
72
|
+
// surfaces an error box in dev and an empty element at a 200 in prod with
|
|
73
|
+
// the cause in the server log; on the client it escapes
|
|
74
|
+
// attributeChangedCallback during upgrade. Catching on one side only would
|
|
75
|
+
// manufacture a fresh divergence (an SSR paint holding a fallback against a
|
|
76
|
+
// browser holding the constructor value), which is worse than both sides
|
|
77
|
+
// failing. lit propagates too (reactive-element `_$attributeToProperty`).
|
|
78
|
+
//
|
|
79
|
+
// A converter RETURN that SSR cannot serialise needs no guard here either.
|
|
80
|
+
// Unreflected it is only a property, and a function in a text hole renders
|
|
81
|
+
// as nothing. Reflected, the #1169 function guard and the #1253
|
|
82
|
+
// unserializable guard in `_reflectAttribute` already run on the SSR side
|
|
83
|
+
// and cover whatever the converter produced.
|
|
84
|
+
//
|
|
85
|
+
// The converter is handed DECODED text, which matters more here than
|
|
86
|
+
// anywhere: the documented reason to write one is a type the built-in
|
|
87
|
+
// converters cannot parse (Date, Map, Set), those parse their input, and
|
|
88
|
+
// `escapeAttr` encodes every `"` in an emitted attribute, so an encoded
|
|
89
|
+
// `{"a":1}` would throw inside the author's converter and (since
|
|
90
|
+
// the throw is deliberately uncaught) render an empty component at a 200.
|
|
91
|
+
// Both callers now decode before calling, so it gets the same text on both
|
|
92
|
+
// sides for hand-written markup as well as for anything WebJs emitted
|
|
93
|
+
// (#1341); this used to be a `decode` argument applied on this branch and
|
|
94
|
+
// the JSON one.
|
|
95
|
+
return def.converter.fromAttribute(value, def.type);
|
|
96
|
+
}
|
|
97
|
+
if (def.type === Number) return value == null ? null : Number(value);
|
|
98
|
+
if (def.type === Boolean) return value != null && value !== 'false';
|
|
99
|
+
if (def.type === Object || def.type === Array) {
|
|
100
|
+
// An attribute that is not parseable JSON yields `null` rather than the raw
|
|
101
|
+
// string (#1253), because a STRING is never a valid value for a property
|
|
102
|
+
// the author declared `Object` or `Array`, whatever put it there. lit's
|
|
103
|
+
// `defaultConverter.fromAttribute` lands on the same `null`, for the reason
|
|
104
|
+
// its own comment gives: an element does not complain about being
|
|
105
|
+
// mis-configured. Both readers reach this line, so they cannot disagree.
|
|
106
|
+
//
|
|
107
|
+
// An attribute that was never PRESENT does not reach either reader, so such
|
|
108
|
+
// a prop simply keeps its constructor value.
|
|
109
|
+
//
|
|
110
|
+
// The two readers now see the SAME attribute set, so this fallback is not
|
|
111
|
+
// the only thing they agree on (#1341). Both resolve a name through
|
|
112
|
+
// `resolveAttributeProperty` below, which matches the same
|
|
113
|
+
// `d.attribute || hyphenate(k)` expression `observedAttributes` maps over
|
|
114
|
+
// and nothing else, so it skips a `state: true` prop and ignores a name that
|
|
115
|
+
// maps to no attribute-backed property exactly as the observed list does.
|
|
116
|
+
// The SSR caller lowercases its source name first, because the browser
|
|
117
|
+
// lowercases while parsing. And both are handed a value with every
|
|
118
|
+
// character reference already decoded, legacy semicolon-less names included.
|
|
119
|
+
if (value == null) return null;
|
|
120
|
+
try { return JSON.parse(value); } catch { return null; }
|
|
121
|
+
}
|
|
122
|
+
return value;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Kebab-case a property name for its default HTML attribute. @param {string} s */
|
|
126
|
+
function hyphenate(s) {
|
|
127
|
+
return s.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Resolve an attribute NAME to the property it feeds, and to that property's
|
|
132
|
+
* normalised declaration.
|
|
133
|
+
*
|
|
134
|
+
* `state: true` props are skipped, because they are absent from
|
|
135
|
+
* `observedAttributes` and so the browser never delivers their attribute to any
|
|
136
|
+
* reader. lit reaches the same outcome the same way, resolving through the very
|
|
137
|
+
* map `observedAttributes` is built from (`reactive-element.ts`
|
|
138
|
+
* `_$attributeToProperty`).
|
|
139
|
+
*
|
|
140
|
+
* `attrName` must already be the name the PLATFORM would deliver. The browser
|
|
141
|
+
* lowercases attribute names while parsing, so the SSR caller lowercases its
|
|
142
|
+
* source name before calling and the client caller passes the browser's name
|
|
143
|
+
* through untouched. That one step is the only thing that differs between the
|
|
144
|
+
* two sides, which is why it lives in the callers and not here. A custom
|
|
145
|
+
* `attribute` option is matched VERBATIM, as lit matches it and as a browser
|
|
146
|
+
* would, so a camelCase custom attribute stays unreachable from markup rather
|
|
147
|
+
* than being reachable at SSR only. Lowercasing the DECLARED side would make
|
|
148
|
+
* SSR read MORE than a browser can deliver, which is the bug this function
|
|
149
|
+
* exists to remove, so do not add it.
|
|
150
|
+
*
|
|
151
|
+
* The match is EXACTLY `d.attribute || hyphenate(k)`, the same expression
|
|
152
|
+
* `observedAttributes` maps over, and nothing else. A `props[attrName] ||
|
|
153
|
+
* props[camelCase(attrName)]` fallback used to sit after the loop, so a prop
|
|
154
|
+
* declaring a custom attribute also answered to its PROPERTY name. Both readers
|
|
155
|
+
* carried that code, which read like an agreement and was not one: the browser
|
|
156
|
+
* only ever calls in with a name from `observedAttributes`, and that list holds
|
|
157
|
+
* the declared attribute alone, so the fallback was unreachable on the client
|
|
158
|
+
* and live on the server. Measured, `open: prop(Boolean, { attribute:
|
|
159
|
+
* 'is-open' })` with `<my-el open>` SSR'd `true` and upgraded to the
|
|
160
|
+
* constructor value. Deriving the match from the same expression the observed
|
|
161
|
+
* list is built from is what makes the two sides agree by construction rather
|
|
162
|
+
* than by two copies staying in step (#1341).
|
|
163
|
+
*
|
|
164
|
+
* @param {any} Cls the component class
|
|
165
|
+
* @param {string} attrName
|
|
166
|
+
* @returns {{ propName: string, def: PropertyDeclaration } | undefined}
|
|
167
|
+
* `undefined` when the attribute maps to no attribute-backed property, in
|
|
168
|
+
* which case the caller must IGNORE the attribute entirely (#1341).
|
|
169
|
+
*/
|
|
170
|
+
export function resolveAttributeProperty(Cls, attrName) {
|
|
171
|
+
const props = (Cls && Cls.properties) || {};
|
|
172
|
+
for (const [k, decl] of Object.entries(props)) {
|
|
173
|
+
const d = typeof decl === 'object' && decl !== null ? decl : { type: decl };
|
|
174
|
+
if (d.state) continue;
|
|
175
|
+
if ((d.attribute || hyphenate(k)) === attrName) return { propName: k, def: d };
|
|
176
|
+
}
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
package/src/component.d.ts
CHANGED
|
@@ -213,3 +213,13 @@ export declare function prop<T>(
|
|
|
213
213
|
export declare function prop<T = string>(
|
|
214
214
|
opts?: PropertyDeclaration<T>
|
|
215
215
|
): PropertyDeclaration<T>;
|
|
216
|
+
|
|
217
|
+
// The shared attribute reader (`resolveAttributeProperty`, #1341) is NOT here,
|
|
218
|
+
// and nothing about it belongs here. It lives in `src/attribute-reader.js`,
|
|
219
|
+
// which has no `exports` entry, because two guards bracket every mapped entry
|
|
220
|
+
// and `./component` is one: #388 forces every runtime named export of
|
|
221
|
+
// `component.js` to be declared in this file, and #1031 forces every
|
|
222
|
+
// declaration reachable from `index.d.ts` (which re-exports this file with
|
|
223
|
+
// `export *`) to exist on `index.js` at runtime. Together they leave no way to
|
|
224
|
+
// add an INTERNAL named export to `component.js`. That module's header carries
|
|
225
|
+
// the full reasoning.
|
package/src/component.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { render as clientRender } from './render-client.js';
|
|
2
|
+
import { readAttributeValue } from './attribute-reader.js';
|
|
2
3
|
import { setActiveActionSignal } from './action-abort-client.js';
|
|
3
4
|
import { carriesFunction } from './form-action.js';
|
|
4
5
|
import { isCSS, adoptStyles } from './css.js';
|
|
5
6
|
import { register, tagOf } from './registry.js';
|
|
6
7
|
import { parse as deserializeProp } from './serialize.js';
|
|
7
8
|
import { Signal } from './signal.js';
|
|
9
|
+
import { resolveAttributeProperty } from './attribute-reader.js';
|
|
8
10
|
import {
|
|
9
11
|
captureAuthoredChildren,
|
|
10
12
|
adoptSSRAssignments,
|
|
@@ -85,7 +87,11 @@ const isBrowser = typeof window !== 'undefined' && typeof HTMLElement !== 'undef
|
|
|
85
87
|
*
|
|
86
88
|
* @property {{ fromAttribute: (value: string|null, type?: Function) => unknown, toAttribute: (value: unknown, type?: Function) => string|null }} [converter]
|
|
87
89
|
* Custom serialization/deserialization pair for the HTML attribute.
|
|
88
|
-
* `fromAttribute` is called
|
|
90
|
+
* `fromAttribute` is called by BOTH attribute readers through the shared
|
|
91
|
+
* `readAttributeValue` in `attribute-reader.js`: the client
|
|
92
|
+
* `attributeChangedCallback` and the SSR
|
|
93
|
+
* `applyAttrsToInstance` in `render-server.js` (#1340). So it runs
|
|
94
|
+
* server-side too, and must not touch browser globals.
|
|
89
95
|
* `toAttribute` is called when reflecting back to the attribute.
|
|
90
96
|
* If omitted, the built-in type-based coercion is used.
|
|
91
97
|
*/
|
|
@@ -100,6 +106,7 @@ function defaultHasChanged(a, b) {
|
|
|
100
106
|
return a !== b;
|
|
101
107
|
}
|
|
102
108
|
|
|
109
|
+
|
|
103
110
|
/**
|
|
104
111
|
* `String(v)` that cannot itself throw.
|
|
105
112
|
*
|
|
@@ -1141,64 +1148,22 @@ class WebComponentBase extends Base {
|
|
|
1141
1148
|
// resulting attributeChangedCallback to avoid infinite loops.
|
|
1142
1149
|
if (this.__reflectingAttribute) return;
|
|
1143
1150
|
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
//
|
|
1147
|
-
//
|
|
1148
|
-
//
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
//
|
|
1157
|
-
//
|
|
1158
|
-
//
|
|
1159
|
-
|
|
1160
|
-
const def = typeof raw === 'object' ? raw : { type: raw };
|
|
1161
|
-
|
|
1162
|
-
let v;
|
|
1163
|
-
if (def.converter && def.converter.fromAttribute) {
|
|
1164
|
-
v = def.converter.fromAttribute(value, def.type);
|
|
1165
|
-
} else if (def.type === Number) {
|
|
1166
|
-
v = value == null ? null : Number(value);
|
|
1167
|
-
} else if (def.type === Boolean) {
|
|
1168
|
-
v = value != null && value !== 'false';
|
|
1169
|
-
} else if (def.type === Object || def.type === Array) {
|
|
1170
|
-
// An attribute that is not parseable JSON yields `null` rather than the
|
|
1171
|
-
// raw string (#1253), because a STRING is never a valid value for a
|
|
1172
|
-
// property the author declared `Object` or `Array`, whatever put it
|
|
1173
|
-
// there. lit's `defaultConverter.fromAttribute` lands on the same `null`,
|
|
1174
|
-
// for the reason its own comment gives: an element does not complain
|
|
1175
|
-
// about being mis-configured.
|
|
1176
|
-
//
|
|
1177
|
-
// `applyAttrsToInstance` in `render-server.js` is the SSR counterpart of
|
|
1178
|
-
// THIS branch and yields `null` for the same input. They have to agree,
|
|
1179
|
-
// or an element SSRs holding one value and re-renders holding another.
|
|
1180
|
-
// The agreement is about an attribute that is PRESENT and unparseable;
|
|
1181
|
-
// an attribute that was never there does not reach either reader, so
|
|
1182
|
-
// such a prop simply keeps its constructor value.
|
|
1183
|
-
//
|
|
1184
|
-
// That agreement is about this FALLBACK, not a guarantee that the two
|
|
1185
|
-
// readers see the same attributes in the first place. They reach an
|
|
1186
|
-
// attribute by different routes (this one via `observedAttributes` and
|
|
1187
|
-
// the browser's own name-lowercasing, the SSR one by walking the parsed
|
|
1188
|
-
// source tag), and hand-written markup can land in the gaps between
|
|
1189
|
-
// those routes, so an author literal can still be read by one and not
|
|
1190
|
-
// the other. Those gaps predate #1253 and it neither causes nor closes
|
|
1191
|
-
// any of them; no attempt is made here to enumerate them, because every
|
|
1192
|
-
// attempt so far has been incomplete.
|
|
1193
|
-
//
|
|
1194
|
-
// Scoped to the default converter. A prop supplying its own
|
|
1195
|
-
// `converter.fromAttribute` is handled by the arm above and the SSR
|
|
1196
|
-
// reader has no converter arm at all, so the two sides already read such
|
|
1197
|
-
// a prop differently. That gap predates #1253 and is left alone.
|
|
1198
|
-
try { v = value == null ? null : JSON.parse(value); } catch { v = null; }
|
|
1199
|
-
} else {
|
|
1200
|
-
v = value;
|
|
1201
|
-
}
|
|
1151
|
+
// One name resolver for both sides (#1341). The browser only calls this for
|
|
1152
|
+
// a name in `observedAttributes`, and it has already lowercased that name
|
|
1153
|
+
// while parsing, so it passes straight through; the SSR caller lowercases
|
|
1154
|
+
// its source name first. `resolveAttributeProperty` also normalises the
|
|
1155
|
+
// declaration, so a bare-constructor shorthand (`count: Number`) and the
|
|
1156
|
+
// long form (`{ type: Number, … }`) both arrive here as `{ type, … }`.
|
|
1157
|
+
const resolved = resolveAttributeProperty(this.constructor, name);
|
|
1158
|
+
if (resolved === undefined) return;
|
|
1159
|
+
const { propName, def } = resolved;
|
|
1160
|
+
|
|
1161
|
+
// One reader for both sides, in `attribute-reader.js`: this and
|
|
1162
|
+
// `applyAttrsToInstance` in `render-server.js` call the same function, so
|
|
1163
|
+
// precedence and every fallback are shared rather than mirrored (#1340).
|
|
1164
|
+
// It takes an already-decoded value, which the DOM has done here and
|
|
1165
|
+
// `decodeAttrEntities` does at the SSR call site (#1341).
|
|
1166
|
+
const v = readAttributeValue(def, value);
|
|
1202
1167
|
|
|
1203
1168
|
if (this[propName] !== v) {
|
|
1204
1169
|
this[propName] = v;
|