@webjsdev/core 0.7.50 → 0.7.51

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.
Files changed (57) hide show
  1. package/dist/webjs-core-browser.js +5 -5
  2. package/dist/webjs-core-browser.js.map +3 -3
  3. package/dist/webjs-core-testing.js +20 -20
  4. package/dist/webjs-core-testing.js.map +3 -3
  5. package/dist/webjs-core.js +7 -7
  6. package/dist/webjs-core.js.map +3 -3
  7. package/index-browser.js +1 -1
  8. package/index.d.ts +1 -1
  9. package/index.js +1 -1
  10. package/package.json +1 -1
  11. package/src/component/lifecycle.js +1481 -0
  12. package/src/component/reactive.js +171 -0
  13. package/src/component/server-element.js +248 -0
  14. package/src/component.js +2 -1896
  15. package/src/render-client/parts.js +2007 -0
  16. package/src/render-client/reconciler.js +388 -0
  17. package/src/render-client/template-compiler.js +525 -0
  18. package/src/render-client.js +1 -2938
  19. package/src/render-server/attrs.js +160 -0
  20. package/src/render-server/dsd.js +445 -0
  21. package/src/render-server/html-scan.js +402 -0
  22. package/src/render-server/slots.js +213 -0
  23. package/src/render-server/stream.js +163 -0
  24. package/src/render-server/template-renderer.js +923 -0
  25. package/src/render-server/text.js +163 -0
  26. package/src/render-server.js +6 -2405
  27. package/src/router-client/anchors.js +44 -0
  28. package/src/router-client/boundaries.js +221 -0
  29. package/src/router-client/constants.js +187 -0
  30. package/src/router-client/diagnostics.js +377 -0
  31. package/src/router-client/dom-differ.js +709 -0
  32. package/src/router-client/dom-parse.js +150 -0
  33. package/src/router-client/events.js +139 -0
  34. package/src/router-client/fetch-apply.js +387 -0
  35. package/src/router-client/form-encoder.js +135 -0
  36. package/src/router-client/frames.js +329 -0
  37. package/src/router-client/head-merge.js +344 -0
  38. package/src/router-client/nav-error.js +105 -0
  39. package/src/router-client/navigator.js +781 -0
  40. package/src/router-client/prefetch.js +824 -0
  41. package/src/router-client/scroll.js +259 -0
  42. package/src/router-client/snapshot-cache.js +97 -0
  43. package/src/router-client/state.js +92 -0
  44. package/src/router-client/stream.js +206 -0
  45. package/src/router-client/swap.js +451 -0
  46. package/src/router-client/upgrade.js +261 -0
  47. package/src/router-client/view-transition.js +223 -0
  48. package/src/router-client.d.ts +10 -1
  49. package/src/router-client.js +75 -5345
  50. package/src/slot/assignment.js +304 -0
  51. package/src/slot/interception.js +558 -0
  52. package/src/slot/polyfills.js +365 -0
  53. package/src/slot/project.js +432 -0
  54. package/src/slot/sensors.js +303 -0
  55. package/src/slot/state.js +245 -0
  56. package/src/slot/symbols.js +85 -0
  57. package/src/slot.js +7 -2199
@@ -0,0 +1,171 @@
1
+ import { tagOf } from '../registry.js';
2
+
3
+ /**
4
+ * Default change detection: strict inequality.
5
+ * @param {unknown} a
6
+ * @param {unknown} b
7
+ * @returns {boolean}
8
+ */
9
+ export function defaultHasChanged(a, b) {
10
+ return a !== b;
11
+ }
12
+
13
+ /**
14
+ * `String(v)` that cannot itself throw.
15
+ *
16
+ * A rejection reason is whatever the author rejected with, and not every value
17
+ * converts to a primitive: `Object.create(null)` has no `toString`, and a
18
+ * revoked Proxy throws on any operation. Coercing one in an error-reporting
19
+ * path turned a reportable failure into a second, unreported one.
20
+ *
21
+ * @param {unknown} v
22
+ * @returns {string}
23
+ */
24
+ export function safeString(v) {
25
+ try {
26
+ return String(v);
27
+ } catch {
28
+ return Object.prototype.toString.call(v);
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Warn that a function value was dropped rather than reflected (#1169).
34
+ *
35
+ * A silently missing attribute is its own confusion, so say what went and why.
36
+ * The message deliberately does NOT include the value: printing the source is
37
+ * the leak this guard exists to prevent, and a server log is not always a
38
+ * private place.
39
+ *
40
+ * UNCONDITIONAL, matching the `.prop=${fn}` unserializable-value drop in
41
+ * `render-server.js`, which is the sibling path this guard mirrors. Two
42
+ * reasons not to gate it on a dev flag. It fires only on a genuine mistake (a
43
+ * function is never a meaningful attribute value), so there is no volume to
44
+ * suppress, and reflection runs per assignment rather than per frame.
45
+ *
46
+ * More decisively, a `NODE_ENV` gate does not survive the dist build.
47
+ * `scripts/build-framework-dist.js` runs esbuild with `platform: 'browser'`
48
+ * and `minify: true`, which substitutes `process.env.NODE_ENV` with
49
+ * `"production"` and folds the check to a constant. The SSR half of the
50
+ * warning would then be unreachable in every published build, which is how
51
+ * every installed app runs, and SSR is the half that matters most, since that
52
+ * is where the leaked source reached visitors.
53
+ *
54
+ * @param {{ constructor: unknown, tagName?: string }} host
55
+ * @param {string} propName
56
+ * @param {string} attrName
57
+ */
58
+ export function warnFunctionReflection(host, propName, attrName) {
59
+ if (typeof console === 'undefined' || !console.warn) return;
60
+ const tag = tagOf(/** @type any */ (host.constructor)) || host.tagName?.toLowerCase() || 'unknown';
61
+ console.warn(
62
+ `[webjs] reflect:true property "${propName}" on <${tag}> holds a function `
63
+ + `(or an array carrying one), which has no HTML attribute representation. `
64
+ + `Removing "${attrName}" instead of stringifying it (a stringified function `
65
+ + `writes its source into the page, so a server action's body would ship to `
66
+ + `the browser). Pass a string, or drop reflect on this property.`
67
+ );
68
+ }
69
+
70
+ /**
71
+ * Warn that a value with no JSON representation was dropped rather than
72
+ * reflected (#1253).
73
+ *
74
+ * `JSON.stringify` throws for three reasons an app hits in practice: a cycle,
75
+ * a `BigInt`, and an author `toJSON()` that throws. All three mean the same
76
+ * thing here, that there is no string to put in the attribute, so all three
77
+ * get the same answer as a function does.
78
+ *
79
+ * The caught message IS included, unlike `warnFunctionReflection`, which
80
+ * withholds its value on purpose. A function's string form is its source,
81
+ * which is the leak that guard exists to prevent; `JSON.stringify`'s own
82
+ * message names the property path rather than a value, and the sibling
83
+ * `.prop=${val}` SSR drop in `render-server.js` already ends the same way.
84
+ *
85
+ * UNCONDITIONAL, for the reason recorded on `warnFunctionReflection` above: a
86
+ * `NODE_ENV` gate is folded to a constant by the dist build, which would make
87
+ * the SSR half unreachable in every published build, and the SSR half is where
88
+ * the component silently vanishes.
89
+ *
90
+ * @param {{ constructor: unknown, tagName?: string }} host
91
+ * @param {string} propName
92
+ * @param {string} attrName
93
+ * @param {string} [detail] the message `JSON.stringify` threw
94
+ */
95
+ export function warnUnserializableReflection(host, propName, attrName, detail) {
96
+ if (typeof console === 'undefined' || !console.warn) return;
97
+ const tag = tagOf(/** @type any */ (host.constructor)) || host.tagName?.toLowerCase() || 'unknown';
98
+ console.warn(
99
+ `[webjs] reflect:true property "${propName}" on <${tag}> holds a value `
100
+ + `JSON.stringify cannot serialize (a cycle, a BigInt, or a throwing `
101
+ + `toJSON), so it has no HTML attribute representation. Removing `
102
+ + `"${attrName}" instead. Detail: ${detail}`
103
+ );
104
+ }
105
+
106
+ /**
107
+ * A minimal base for HTML Custom Elements that mirrors Lit's ergonomics
108
+ * while staying JSDoc-only and no-build.
109
+ *
110
+ * Subclasses declare:
111
+ * - `static properties`: attribute/property declarations with type info,
112
+ * reflection, custom converters, and internal-state mode
113
+ * - `static styles`: CSSResult or array thereof (only meaningful with
114
+ * `static shadow = true`; light-DOM components inherit global CSS)
115
+ * - `static shadow`: set `true` to opt in to shadow DOM (default: `false`
116
+ * → light DOM, so Tailwind / global CSS apply directly)
117
+ * - `render()`: returns a TemplateResult
118
+ *
119
+ * The tag name is not a static field: pass it to `.register('tag-name')`
120
+ * at the bottom of the file. Tag must contain a hyphen (HTML spec).
121
+ *
122
+ * Lifecycle (lit-aligned, called in order during each update cycle):
123
+ * 1. `shouldUpdate(changedProperties)`. Skip update if false.
124
+ * 2. `willUpdate(changedProperties)`. Safe to set properties; folds into this cycle.
125
+ * 3. controllers' `hostUpdate()`
126
+ * 4. `update(changedProperties)`. Default impl calls `render()` + commits.
127
+ * 5. controllers' `hostUpdated()`
128
+ * 6. `firstUpdated(changedProperties)`: once, on the first render only
129
+ * 7. `updated(changedProperties)`: every render commit
130
+ * 8. `updateComplete` promise resolves
131
+ *
132
+ * `changedProperties` is a `Map<string, unknown>` where each entry maps
133
+ * a property name to its previous value.
134
+ *
135
+ * MAINTAINER NOTE. Adding a new overridable lifecycle hook here means
136
+ * the display-only component elision analyser must learn about it too,
137
+ * or it will wrongly elide a component that now does client work. Add
138
+ * the hook name to `CLIENT_LIFECYCLE_HOOKS` in
139
+ * `packages/server/src/component-elision.js`. The guard test at
140
+ * `packages/server/test/elision/lifecycle-coverage.test.js` introspects
141
+ * this prototype and fails until you do.
142
+ *
143
+ * Usage:
144
+ * ```js
145
+ * import { signal } from '@webjsdev/core';
146
+ *
147
+ * const count = signal(0);
148
+ *
149
+ * class MyCounter extends WebComponent {
150
+ * render() {
151
+ * return html`<button @click=${() => count.set(count.get() + 1)}>${count.get()}</button>`;
152
+ * }
153
+ * }
154
+ * MyCounter.register('my-counter');
155
+ * ```
156
+ */
157
+
158
+ /**
159
+ * Helper to define properties with custom options.
160
+ *
161
+ * @param {any} [type]
162
+ * @param {any} [opts]
163
+ * @returns {any}
164
+ */
165
+ export function prop(type, opts = {}) {
166
+ if (type && typeof type === 'object' && !('call' in type)) {
167
+ opts = type;
168
+ type = undefined;
169
+ }
170
+ return { ...(type ? { type } : {}), ...opts };
171
+ }
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Inert `ElementInternals`-shaped object returned by the server shim's
3
+ * `attachInternals()`. Modeled on `@lit-labs/ssr-dom-shim`'s
4
+ * `ElementInternalsShim` (lit repo, `packages/labs/ssr-dom-shim/src/lib/
5
+ * element-internals.ts`): form-association, validity, and custom-state
6
+ * calls are no-ops at SSR (no form, no constraint validation, no `:state()`
7
+ * matching server-side), so a component that calls `this.attachInternals()`
8
+ * in its constructor renders instead of crashing. The browser runs the real
9
+ * `attachInternals()` on hydration.
10
+ *
11
+ * Deliberate deviation from lit: lit's `checkValidity` / `reportValidity`
12
+ * THROW on the server. WebJs returns `true` instead, to keep SSR
13
+ * progressive-enhancement-safe (a stray validity call in a constructor must
14
+ * not 500 the page); the browser does the real validation.
15
+ * @returns {any}
16
+ */
17
+ export function makeServerInternals() {
18
+ return {
19
+ states: new Set(),
20
+ shadowRoot: null,
21
+ form: null,
22
+ labels: [],
23
+ role: '',
24
+ willValidate: true,
25
+ validity: /** @type {any} */ ({}),
26
+ validationMessage: '',
27
+ setFormValue() {},
28
+ setValidity() {},
29
+ checkValidity() { return true; },
30
+ reportValidity() { return true; },
31
+ };
32
+ }
33
+
34
+ /**
35
+ * Server-side stand-in for `HTMLElement`. The SSR pipeline constructs
36
+ * component instances in Node, where `HTMLElement` does not exist, so the
37
+ * base class is this shim. It is modeled on `@lit-labs/ssr-dom-shim`'s
38
+ * `ElementShim` (lit repo, `packages/labs/ssr-dom-shim/src/index.ts`): the
39
+ * attribute methods (`getAttribute` / `setAttribute` / `hasAttribute` /
40
+ * `removeAttribute` / `toggleAttribute`, the `attributes` getter) are backed
41
+ * by a Map, so lit muscle-memory patterns that read attributes in `render()`
42
+ * or set them while deriving state work server-side; the SSR walker seeds
43
+ * the Map from the element's source attributes and reads it back to surface
44
+ * reflected/added attributes in the output. Event methods are no-ops (no
45
+ * server event loop), and `attachInternals()` returns the inert object
46
+ * above. The genuinely browser-only surface (`querySelector`, layout reads,
47
+ * `attachShadow`, `focus`) is deliberately absent and still throws at SSR,
48
+ * which the `no-browser-globals-in-render` rule and the SSR crash hint flag.
49
+ *
50
+ * Deliberate deviation from lit: this shim lowercases attribute names so
51
+ * `getAttribute('Foo')` after `setAttribute('foo', x)` resolves, matching how
52
+ * a real browser treats HTML attribute names as case-insensitive. lit's shim
53
+ * keys the Map by the raw name (a known fidelity gap in lit-labs).
54
+ */
55
+ export class ServerElement {
56
+ constructor() {
57
+ /**
58
+ * Backing store for the attribute methods. Keys are lowercased
59
+ * attribute names (HTML attributes are case-insensitive). Seeded by the
60
+ * SSR walker from the element's source attributes.
61
+ * @type {Map<string, string>}
62
+ */
63
+ this.__ssrAttrs = new Map();
64
+ /** @type {any} */
65
+ this.__internals = null;
66
+ }
67
+
68
+ /** Mirrors `Element.attributes`: an array of `{ name, value }`. */
69
+ get attributes() {
70
+ return [...this.__ssrAttrs].map(([name, value]) => ({ name, value }));
71
+ }
72
+
73
+ /** @param {string} name */
74
+ getAttribute(name) {
75
+ const v = this.__ssrAttrs.get(String(name).toLowerCase());
76
+ return v === undefined ? null : v;
77
+ }
78
+
79
+ /** @param {string} name @param {unknown} value */
80
+ setAttribute(name, value) {
81
+ // Emulate the browser casting all values to string (lit does the same).
82
+ this.__ssrAttrs.set(String(name).toLowerCase(), String(value));
83
+ }
84
+
85
+ /** @param {string} name */
86
+ removeAttribute(name) {
87
+ this.__ssrAttrs.delete(String(name).toLowerCase());
88
+ }
89
+
90
+ /** @param {string} name */
91
+ hasAttribute(name) {
92
+ return this.__ssrAttrs.has(String(name).toLowerCase());
93
+ }
94
+
95
+ /** @param {string} name @param {boolean} [force] */
96
+ toggleAttribute(name, force) {
97
+ // Steps mirror https://dom.spec.whatwg.org/#dom-element-toggleattribute
98
+ const key = String(name).toLowerCase();
99
+ const present = this.__ssrAttrs.has(key);
100
+ const next = force === undefined ? !present : force;
101
+ if (next) {
102
+ this.__ssrAttrs.set(key, '');
103
+ return true;
104
+ }
105
+ this.__ssrAttrs.delete(key);
106
+ return false;
107
+ }
108
+
109
+ /** @returns {string[]} */
110
+ getAttributeNames() {
111
+ return [...this.__ssrAttrs.keys()];
112
+ }
113
+
114
+ /**
115
+ * Minimal `Element.closest()` for SSR. The SSR walker threads the chain
116
+ * of enclosing custom-element instances into each instance
117
+ * (`__ssrAncestors`); this walks self-then-ancestors and returns the
118
+ * nearest whose tag matches. Only bare tag-name selectors are supported
119
+ * server-side (`closest('ui-tabs')`), which is what compound components
120
+ * need to read parent state for a correct first paint; anything more
121
+ * specific (class, attribute, or descendant selectors) returns null,
122
+ * matching the pre-shim behaviour. The browser runs the real `closest()`
123
+ * on hydration.
124
+ * @param {string} selector
125
+ * @returns {any}
126
+ */
127
+ closest(selector) {
128
+ const sel = String(selector).trim().toLowerCase();
129
+ // Tag-name selectors only at SSR; bail (null) on anything else.
130
+ if (!/^[a-z][a-z0-9-]*$/.test(sel)) return null;
131
+ if (this.__ssrTag === sel) return this;
132
+ const chain = this.__ssrAncestors;
133
+ if (!Array.isArray(chain)) return null;
134
+ for (let i = chain.length - 1; i >= 0; i--) {
135
+ if (chain[i] && chain[i].__ssrTag === sel) return chain[i];
136
+ }
137
+ return null;
138
+ }
139
+
140
+ /**
141
+ * `HTMLElement.dataset`: a live view over the element's `data-*`
142
+ * attributes, backed by the SSR attribute Map. Reading / writing
143
+ * `el.dataset.fooBar` maps to the `data-foo-bar` attribute (camelCase
144
+ * to kebab-case), so a `render()` that sets `this.dataset.state = 'on'`
145
+ * surfaces `data-state="on"` in the SSR'd host tag instead of crashing
146
+ * on an undefined `dataset`.
147
+ * @returns {Record<string, string>}
148
+ */
149
+ get dataset() {
150
+ if (this.__dataset) return this.__dataset;
151
+ const el = this;
152
+ const toAttr = (p) => 'data-' + String(p).replace(/[A-Z]/g, (m) => '-' + m.toLowerCase());
153
+ this.__dataset = new Proxy(/** @type {Record<string,string>} */ ({}), {
154
+ get(_t, prop) {
155
+ if (typeof prop !== 'string') return undefined;
156
+ const v = el.getAttribute(toAttr(prop));
157
+ return v === null ? undefined : v;
158
+ },
159
+ set(_t, prop, value) {
160
+ if (typeof prop === 'string') el.setAttribute(toAttr(prop), value);
161
+ return true;
162
+ },
163
+ has(_t, prop) {
164
+ return typeof prop === 'string' && el.hasAttribute(toAttr(prop));
165
+ },
166
+ deleteProperty(_t, prop) {
167
+ if (typeof prop === 'string') el.removeAttribute(toAttr(prop));
168
+ return true;
169
+ },
170
+ ownKeys() {
171
+ return el.getAttributeNames()
172
+ .filter((n) => n.startsWith('data-'))
173
+ .map((n) => n.slice(5).replace(/-([a-z])/g, (_m, c) => c.toUpperCase()));
174
+ },
175
+ getOwnPropertyDescriptor(_t, prop) {
176
+ if (typeof prop === 'string' && el.hasAttribute(toAttr(prop))) {
177
+ return { enumerable: true, configurable: true, value: el.getAttribute(toAttr(prop)) };
178
+ }
179
+ return undefined;
180
+ },
181
+ });
182
+ return this.__dataset;
183
+ }
184
+
185
+ // IDL properties that reflect to a content attribute. A render() that
186
+ // mutates these on the host (a light-DOM compound-component pattern, e.g.
187
+ // `this.className = ...`, `this.hidden = !active`) then surfaces the
188
+ // matching attribute in the SSR'd host tag, matching the browser.
189
+ get className() { return this.getAttribute('class') ?? ''; }
190
+ set className(v) { this.setAttribute('class', v); }
191
+ get hidden() { return this.hasAttribute('hidden'); }
192
+ set hidden(v) { this.toggleAttribute('hidden', !!v); }
193
+ get id() { return this.getAttribute('id') ?? ''; }
194
+ set id(v) { this.setAttribute('id', v); }
195
+ get title() { return this.getAttribute('title') ?? ''; }
196
+ set title(v) { this.setAttribute('title', v); }
197
+ get slot() { return this.getAttribute('slot') ?? ''; }
198
+ set slot(v) { this.setAttribute('slot', v); }
199
+ get role() { return this.getAttribute('role'); }
200
+ set role(v) { v == null ? this.removeAttribute('role') : this.setAttribute('role', v); }
201
+ get tabIndex() { const v = this.getAttribute('tabindex'); return v === null ? -1 : (Number.parseInt(v, 10) || 0); }
202
+ set tabIndex(v) { this.setAttribute('tabindex', String(v)); }
203
+
204
+ // No server event loop: listeners never fire at SSR. The no-op keeps a
205
+ // constructor that wires delegated listeners (a common lit pattern) from
206
+ // crashing; the browser re-runs the constructor on hydration where the
207
+ // real HTMLElement methods apply.
208
+ addEventListener() {}
209
+ removeEventListener() {}
210
+ dispatchEvent() { return true; }
211
+
212
+ /** @returns {any} */
213
+ attachInternals() {
214
+ // Match the browser (and lit's shim): a second attach is an error.
215
+ if (this.__internals !== null) {
216
+ throw new Error(
217
+ "Failed to execute 'attachInternals' on 'HTMLElement': " +
218
+ 'ElementInternals for the specified element was already attached.',
219
+ );
220
+ }
221
+ this.__internals = makeServerInternals();
222
+ return this.__internals;
223
+ }
224
+ }
225
+
226
+ // ARIAMixin IDL reflections (`el.ariaPressed = 'true'` writes aria-pressed).
227
+ // A render() that sets ARIA state via the IDL properties then surfaces the
228
+ // matching aria-* attribute in the SSR'd host tag, matching the browser. The
229
+ // IDL name maps to the content attribute by lowercasing the part after
230
+ // `aria` and prefixing `aria-` (ariaPressed -> aria-pressed).
231
+ const ARIA_IDL_PROPS = [
232
+ 'ariaAtomic', 'ariaAutoComplete', 'ariaBusy', 'ariaChecked', 'ariaColCount',
233
+ 'ariaColIndex', 'ariaColSpan', 'ariaCurrent', 'ariaDescription', 'ariaDisabled',
234
+ 'ariaExpanded', 'ariaHasPopup', 'ariaHidden', 'ariaInvalid', 'ariaKeyShortcuts',
235
+ 'ariaLabel', 'ariaLevel', 'ariaLive', 'ariaModal', 'ariaMultiLine',
236
+ 'ariaMultiSelectable', 'ariaOrientation', 'ariaPlaceholder', 'ariaPosInSet',
237
+ 'ariaPressed', 'ariaReadOnly', 'ariaRequired', 'ariaRoleDescription',
238
+ 'ariaRowCount', 'ariaRowIndex', 'ariaRowSpan', 'ariaSelected', 'ariaSetSize',
239
+ 'ariaSort', 'ariaValueMax', 'ariaValueMin', 'ariaValueNow', 'ariaValueText',
240
+ ];
241
+ for (const idl of ARIA_IDL_PROPS) {
242
+ const attr = 'aria-' + idl.slice(4).toLowerCase();
243
+ Object.defineProperty(ServerElement.prototype, idl, {
244
+ configurable: true,
245
+ get() { return this.getAttribute(attr); },
246
+ set(v) { v == null ? this.removeAttribute(attr) : this.setAttribute(attr, String(v)); },
247
+ });
248
+ }