@webjsdev/core 0.7.48 → 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/README.md +3 -2
- 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/index-browser.js +12 -4
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/package.json +1 -1
- package/src/attribute-reader.js +178 -0
- package/src/component.d.ts +10 -0
- package/src/component.js +89 -32
- package/src/html-entities.js +2179 -0
- package/src/render-client.js +4 -5
- package/src/render-server.js +192 -90
- package/src/router-client.js +28 -10
- package/src/webjs-config.d.ts +13 -2
- package/src/websocket-client.js +5 -2
package/index-browser.js
CHANGED
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
* Stripped:
|
|
9
9
|
* - `renderToString`, `renderToStream` (server SSR pipeline; reach
|
|
10
10
|
* for `@webjsdev/core/server` if you need them in a Node test).
|
|
11
|
-
* - `setCspNonceProvider`
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* - `setCspNonceProvider`, `setAssetUrlProvider` and
|
|
12
|
+
* `setFormActionResolver` (server-side wiring called by
|
|
13
|
+
* `@webjsdev/server`; the reader halves stay, because a layout
|
|
14
|
+
* can call `cspNonce()` / `asset()`).
|
|
14
15
|
*
|
|
15
16
|
* The framework's own SSR pipeline runs on Node and resolves the
|
|
16
17
|
* package via the package.json `"default"` condition, which still
|
|
@@ -52,7 +53,14 @@ export {
|
|
|
52
53
|
// the re-export still also evaluates the module, so the two are belt-and-
|
|
53
54
|
// suspenders, not a double-enable (ESM evaluates once, enable is idempotent).
|
|
54
55
|
import './src/router-client.js';
|
|
55
|
-
|
|
56
|
+
// `loadFrame` is here (and in `./index.js`, keeping the two symmetric) because
|
|
57
|
+
// `@webjsdev/core/client-router`'s `default` condition points at THIS bundle, so
|
|
58
|
+
// in dist mode the subpath resolves here while its published `types`
|
|
59
|
+
// (`src/router-client.d.ts`) declare all five public router functions. Without
|
|
60
|
+
// it, `import { loadFrame } from '@webjsdev/core/client-router'` type-checks,
|
|
61
|
+
// works in dev (where the subpath maps to `src/router-client.js`) and is
|
|
62
|
+
// `undefined` in production.
|
|
63
|
+
export { enableClientRouter, disableClientRouter, navigate, revalidate, loadFrame } from './src/router-client.js';
|
|
56
64
|
export { WebjsFrame } from './src/webjs-frame.js';
|
|
57
65
|
export { WebjsStream, renderStream } from './src/webjs-stream.js';
|
|
58
66
|
|
package/index.d.ts
CHANGED
|
@@ -81,7 +81,7 @@ export { repeat, isRepeat } from './src/repeat.js';
|
|
|
81
81
|
export { Suspense, isSuspense } from './src/suspense.js';
|
|
82
82
|
export { connectWS } from './src/websocket-client.js';
|
|
83
83
|
export { richFetch } from './src/rich-fetch.js';
|
|
84
|
-
export { enableClientRouter, disableClientRouter, revalidate } from './src/router-client.js';
|
|
84
|
+
export { enableClientRouter, disableClientRouter, revalidate, loadFrame } from './src/router-client.js';
|
|
85
85
|
|
|
86
86
|
// `navigate` is typed against the generated `Route` union (#258) rather than
|
|
87
87
|
// the JSDoc `string`. Until an app runs `webjs types`, `Route` resolves to
|
package/index.js
CHANGED
|
@@ -28,7 +28,7 @@ export {
|
|
|
28
28
|
stringify, parse,
|
|
29
29
|
serialize, deserialize,
|
|
30
30
|
} from './src/serialize.js';
|
|
31
|
-
export { enableClientRouter, disableClientRouter, navigate, revalidate } from './src/router-client.js';
|
|
31
|
+
export { enableClientRouter, disableClientRouter, navigate, revalidate, loadFrame } from './src/router-client.js';
|
|
32
32
|
export { WebjsFrame } from './src/webjs-frame.js';
|
|
33
33
|
export { WebjsStream, renderStream } from './src/webjs-stream.js';
|
|
34
34
|
|
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
|
*
|
|
@@ -157,6 +164,42 @@ function warnFunctionReflection(host, propName, attrName) {
|
|
|
157
164
|
);
|
|
158
165
|
}
|
|
159
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Warn that a value with no JSON representation was dropped rather than
|
|
169
|
+
* reflected (#1253).
|
|
170
|
+
*
|
|
171
|
+
* `JSON.stringify` throws for three reasons an app hits in practice: a cycle,
|
|
172
|
+
* a `BigInt`, and an author `toJSON()` that throws. All three mean the same
|
|
173
|
+
* thing here, that there is no string to put in the attribute, so all three
|
|
174
|
+
* get the same answer as a function does.
|
|
175
|
+
*
|
|
176
|
+
* The caught message IS included, unlike `warnFunctionReflection`, which
|
|
177
|
+
* withholds its value on purpose. A function's string form is its source,
|
|
178
|
+
* which is the leak that guard exists to prevent; `JSON.stringify`'s own
|
|
179
|
+
* message names the property path rather than a value, and the sibling
|
|
180
|
+
* `.prop=${val}` SSR drop in `render-server.js` already ends the same way.
|
|
181
|
+
*
|
|
182
|
+
* UNCONDITIONAL, for the reason recorded on `warnFunctionReflection` above: a
|
|
183
|
+
* `NODE_ENV` gate is folded to a constant by the dist build, which would make
|
|
184
|
+
* the SSR half unreachable in every published build, and the SSR half is where
|
|
185
|
+
* the component silently vanishes.
|
|
186
|
+
*
|
|
187
|
+
* @param {{ constructor: unknown, tagName?: string }} host
|
|
188
|
+
* @param {string} propName
|
|
189
|
+
* @param {string} attrName
|
|
190
|
+
* @param {string} [detail] the message `JSON.stringify` threw
|
|
191
|
+
*/
|
|
192
|
+
function warnUnserializableReflection(host, propName, attrName, detail) {
|
|
193
|
+
if (typeof console === 'undefined' || !console.warn) return;
|
|
194
|
+
const tag = tagOf(/** @type any */ (host.constructor)) || host.tagName?.toLowerCase() || 'unknown';
|
|
195
|
+
console.warn(
|
|
196
|
+
`[webjs] reflect:true property "${propName}" on <${tag}> holds a value `
|
|
197
|
+
+ `JSON.stringify cannot serialize (a cycle, a BigInt, or a throwing `
|
|
198
|
+
+ `toJSON), so it has no HTML attribute representation. Removing `
|
|
199
|
+
+ `"${attrName}" instead. Detail: ${detail}`
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
160
203
|
/**
|
|
161
204
|
* A minimal base for HTML Custom Elements that mirrors Lit's ergonomics
|
|
162
205
|
* while staying JSDoc-only and no-build.
|
|
@@ -745,12 +788,40 @@ class WebComponentBase extends Base {
|
|
|
745
788
|
} else if (value == null) {
|
|
746
789
|
this.removeAttribute(attrName);
|
|
747
790
|
} else if (decl.type === Object || decl.type === Array) {
|
|
791
|
+
// One rule about an unserializable reflected value, in two halves.
|
|
792
|
+
//
|
|
748
793
|
// A JSON-typed prop CARRYING a function is safe and stays whole:
|
|
749
794
|
// `JSON.stringify` drops a function to `null` in an array and omits
|
|
750
795
|
// the key in an object, so `[1, 2, fn]` serializes to `[1,2,null]`
|
|
751
796
|
// with no source and no data loss. Refusing here would discard the
|
|
752
797
|
// `1` and the `2` on a path that never leaked.
|
|
753
|
-
|
|
798
|
+
//
|
|
799
|
+
// A value `JSON.stringify` cannot serialize AT ALL is the other half
|
|
800
|
+
// (#1253): a cycle, a `BigInt`, or an author `toJSON()` that throws.
|
|
801
|
+
// The throw used to escape reflection entirely, so a property
|
|
802
|
+
// assignment threw from the setter, a client upgrade threw out of
|
|
803
|
+
// `connectedCallback` before the first render, and an SSR render was
|
|
804
|
+
// swallowed by per-component isolation, which surfaces an error box in
|
|
805
|
+
// dev and emits the component EMPTY at a 200 in PRODUCTION, where the
|
|
806
|
+
// cause is only in the server log. There is no string to put in the
|
|
807
|
+
// attribute, so the
|
|
808
|
+
// attribute goes, exactly as it does for a function. The `catch` IS
|
|
809
|
+
// the detection: `JSON.stringify` already walks the value, so its own
|
|
810
|
+
// failure covers all three causes, where a cycle-only pre-walk would
|
|
811
|
+
// miss the other two and pay a full traversal per reflection.
|
|
812
|
+
let serialized;
|
|
813
|
+
let serializable = true;
|
|
814
|
+
try {
|
|
815
|
+
serialized = JSON.stringify(value);
|
|
816
|
+
} catch (e) {
|
|
817
|
+
serializable = false;
|
|
818
|
+
this.removeAttribute(attrName);
|
|
819
|
+
warnUnserializableReflection(this, propName, attrName, e && e.message);
|
|
820
|
+
}
|
|
821
|
+
// Outside the `try` on purpose, so a genuine `setAttribute` failure
|
|
822
|
+
// (an invalid attribute name) still surfaces instead of being folded
|
|
823
|
+
// into the unserializable path.
|
|
824
|
+
if (serializable) this.setAttribute(attrName, serialized);
|
|
754
825
|
} else if (carriesFunction(value)) {
|
|
755
826
|
// The string fall-through is the one place a CARRIED function still
|
|
756
827
|
// leaks. `String([fn])` is `Array.prototype.join`, which runs
|
|
@@ -1077,36 +1148,22 @@ class WebComponentBase extends Base {
|
|
|
1077
1148
|
// resulting attributeChangedCallback to avoid infinite loops.
|
|
1078
1149
|
if (this.__reflectingAttribute) return;
|
|
1079
1150
|
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
//
|
|
1083
|
-
//
|
|
1084
|
-
//
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
//
|
|
1093
|
-
//
|
|
1094
|
-
//
|
|
1095
|
-
|
|
1096
|
-
const def = typeof raw === 'object' ? raw : { type: raw };
|
|
1097
|
-
|
|
1098
|
-
let v;
|
|
1099
|
-
if (def.converter && def.converter.fromAttribute) {
|
|
1100
|
-
v = def.converter.fromAttribute(value, def.type);
|
|
1101
|
-
} else if (def.type === Number) {
|
|
1102
|
-
v = value == null ? null : Number(value);
|
|
1103
|
-
} else if (def.type === Boolean) {
|
|
1104
|
-
v = value != null && value !== 'false';
|
|
1105
|
-
} else if (def.type === Object || def.type === Array) {
|
|
1106
|
-
try { v = value == null ? null : JSON.parse(value); } catch { v = value; }
|
|
1107
|
-
} else {
|
|
1108
|
-
v = value;
|
|
1109
|
-
}
|
|
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);
|
|
1110
1167
|
|
|
1111
1168
|
if (this[propName] !== v) {
|
|
1112
1169
|
this[propName] = v;
|