@webjsdev/core 0.7.6 → 0.7.7
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 +3 -3
- package/dist/webjs-core-browser.js.map +2 -2
- package/dist/webjs-core-testing.js +2 -2
- package/dist/webjs-core-testing.js.map +2 -2
- package/dist/webjs-core.js +3 -3
- package/dist/webjs-core.js.map +2 -2
- package/index-browser.js +10 -2
- package/index.js +9 -2
- package/package.json +1 -1
- package/src/component.js +114 -0
- package/src/directives.js +7 -1
- package/src/render-server.js +22 -8
package/index-browser.js
CHANGED
|
@@ -48,8 +48,16 @@ export { WebjsFrame } from './src/webjs-frame.js';
|
|
|
48
48
|
// Signals (TC39 Stage-1 shape), also available via '@webjsdev/core/signals'
|
|
49
49
|
export { signal, computed, effect, batch, isSignal, Signal } from './src/signal.js';
|
|
50
50
|
|
|
51
|
-
// Directives, also available via '@webjsdev/core/directives'
|
|
52
|
-
|
|
51
|
+
// Directives, also available via '@webjsdev/core/directives'. The full
|
|
52
|
+
// lit-html-parity set is re-exported here so the dist browser bundle (which
|
|
53
|
+
// the `@webjsdev/core/directives` subpath collapses onto in dist mode) carries
|
|
54
|
+
// every directive, matching what src/directives.js exports in dev.
|
|
55
|
+
export {
|
|
56
|
+
unsafeHTML, isUnsafeHTML, live, isLive, keyed, isKeyed, guard, isGuard,
|
|
57
|
+
templateContent, isTemplateContent, ref, isRef, createRef, cache, isCache,
|
|
58
|
+
until, isUntil, asyncAppend, isAsyncAppend, asyncReplace, isAsyncReplace,
|
|
59
|
+
watch, isWatch,
|
|
60
|
+
} from './src/directives.js';
|
|
53
61
|
|
|
54
62
|
// Context Protocol, also available via '@webjsdev/core/context'
|
|
55
63
|
export { createContext, ContextProvider, ContextConsumer, ContextRequestEvent } from './src/context.js';
|
package/index.js
CHANGED
|
@@ -30,8 +30,15 @@ export { WebjsFrame } from './src/webjs-frame.js';
|
|
|
30
30
|
// Signals (TC39 Stage-1 shape), also available via '@webjsdev/core/signals'
|
|
31
31
|
export { signal, computed, effect, batch, isSignal, Signal } from './src/signal.js';
|
|
32
32
|
|
|
33
|
-
// Directives, also available via '@webjsdev/core/directives'
|
|
34
|
-
|
|
33
|
+
// Directives, also available via '@webjsdev/core/directives'. The full
|
|
34
|
+
// lit-html-parity set is re-exported so the bare specifier exposes the same
|
|
35
|
+
// directive surface in Node as the browser bundle does.
|
|
36
|
+
export {
|
|
37
|
+
unsafeHTML, isUnsafeHTML, live, isLive, keyed, isKeyed, guard, isGuard,
|
|
38
|
+
templateContent, isTemplateContent, ref, isRef, createRef, cache, isCache,
|
|
39
|
+
until, isUntil, asyncAppend, isAsyncAppend, asyncReplace, isAsyncReplace,
|
|
40
|
+
watch, isWatch,
|
|
41
|
+
} from './src/directives.js';
|
|
35
42
|
|
|
36
43
|
// Context Protocol, also available via '@webjsdev/core/context'
|
|
37
44
|
export { createContext, ContextProvider, ContextConsumer, ContextRequestEvent } from './src/context.js';
|
package/package.json
CHANGED
package/src/component.js
CHANGED
|
@@ -260,6 +260,96 @@ class ServerElement {
|
|
|
260
260
|
return [...this.__ssrAttrs.keys()];
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
+
/**
|
|
264
|
+
* Minimal `Element.closest()` for SSR. The SSR walker threads the chain
|
|
265
|
+
* of enclosing custom-element instances into each instance
|
|
266
|
+
* (`__ssrAncestors`); this walks self-then-ancestors and returns the
|
|
267
|
+
* nearest whose tag matches. Only bare tag-name selectors are supported
|
|
268
|
+
* server-side (`closest('ui-tabs')`), which is what compound components
|
|
269
|
+
* need to read parent state for a correct first paint; anything more
|
|
270
|
+
* specific (class, attribute, or descendant selectors) returns null,
|
|
271
|
+
* matching the pre-shim behaviour. The browser runs the real `closest()`
|
|
272
|
+
* on hydration.
|
|
273
|
+
* @param {string} selector
|
|
274
|
+
* @returns {any}
|
|
275
|
+
*/
|
|
276
|
+
closest(selector) {
|
|
277
|
+
const sel = String(selector).trim().toLowerCase();
|
|
278
|
+
// Tag-name selectors only at SSR; bail (null) on anything else.
|
|
279
|
+
if (!/^[a-z][a-z0-9-]*$/.test(sel)) return null;
|
|
280
|
+
if (this.__ssrTag === sel) return this;
|
|
281
|
+
const chain = this.__ssrAncestors;
|
|
282
|
+
if (!Array.isArray(chain)) return null;
|
|
283
|
+
for (let i = chain.length - 1; i >= 0; i--) {
|
|
284
|
+
if (chain[i] && chain[i].__ssrTag === sel) return chain[i];
|
|
285
|
+
}
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* `HTMLElement.dataset`: a live view over the element's `data-*`
|
|
291
|
+
* attributes, backed by the SSR attribute Map. Reading / writing
|
|
292
|
+
* `el.dataset.fooBar` maps to the `data-foo-bar` attribute (camelCase
|
|
293
|
+
* to kebab-case), so a `render()` that sets `this.dataset.state = 'on'`
|
|
294
|
+
* surfaces `data-state="on"` in the SSR'd host tag instead of crashing
|
|
295
|
+
* on an undefined `dataset`.
|
|
296
|
+
* @returns {Record<string, string>}
|
|
297
|
+
*/
|
|
298
|
+
get dataset() {
|
|
299
|
+
if (this.__dataset) return this.__dataset;
|
|
300
|
+
const el = this;
|
|
301
|
+
const toAttr = (p) => 'data-' + String(p).replace(/[A-Z]/g, (m) => '-' + m.toLowerCase());
|
|
302
|
+
this.__dataset = new Proxy(/** @type {Record<string,string>} */ ({}), {
|
|
303
|
+
get(_t, prop) {
|
|
304
|
+
if (typeof prop !== 'string') return undefined;
|
|
305
|
+
const v = el.getAttribute(toAttr(prop));
|
|
306
|
+
return v === null ? undefined : v;
|
|
307
|
+
},
|
|
308
|
+
set(_t, prop, value) {
|
|
309
|
+
if (typeof prop === 'string') el.setAttribute(toAttr(prop), value);
|
|
310
|
+
return true;
|
|
311
|
+
},
|
|
312
|
+
has(_t, prop) {
|
|
313
|
+
return typeof prop === 'string' && el.hasAttribute(toAttr(prop));
|
|
314
|
+
},
|
|
315
|
+
deleteProperty(_t, prop) {
|
|
316
|
+
if (typeof prop === 'string') el.removeAttribute(toAttr(prop));
|
|
317
|
+
return true;
|
|
318
|
+
},
|
|
319
|
+
ownKeys() {
|
|
320
|
+
return el.getAttributeNames()
|
|
321
|
+
.filter((n) => n.startsWith('data-'))
|
|
322
|
+
.map((n) => n.slice(5).replace(/-([a-z])/g, (_m, c) => c.toUpperCase()));
|
|
323
|
+
},
|
|
324
|
+
getOwnPropertyDescriptor(_t, prop) {
|
|
325
|
+
if (typeof prop === 'string' && el.hasAttribute(toAttr(prop))) {
|
|
326
|
+
return { enumerable: true, configurable: true, value: el.getAttribute(toAttr(prop)) };
|
|
327
|
+
}
|
|
328
|
+
return undefined;
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
return this.__dataset;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// IDL properties that reflect to a content attribute. A render() that
|
|
335
|
+
// mutates these on the host (a light-DOM compound-component pattern, e.g.
|
|
336
|
+
// `this.className = ...`, `this.hidden = !active`) then surfaces the
|
|
337
|
+
// matching attribute in the SSR'd host tag, matching the browser.
|
|
338
|
+
get className() { return this.getAttribute('class') ?? ''; }
|
|
339
|
+
set className(v) { this.setAttribute('class', v); }
|
|
340
|
+
get hidden() { return this.hasAttribute('hidden'); }
|
|
341
|
+
set hidden(v) { this.toggleAttribute('hidden', !!v); }
|
|
342
|
+
get id() { return this.getAttribute('id') ?? ''; }
|
|
343
|
+
set id(v) { this.setAttribute('id', v); }
|
|
344
|
+
get title() { return this.getAttribute('title') ?? ''; }
|
|
345
|
+
set title(v) { this.setAttribute('title', v); }
|
|
346
|
+
get slot() { return this.getAttribute('slot') ?? ''; }
|
|
347
|
+
set slot(v) { this.setAttribute('slot', v); }
|
|
348
|
+
get role() { return this.getAttribute('role'); }
|
|
349
|
+
set role(v) { v == null ? this.removeAttribute('role') : this.setAttribute('role', v); }
|
|
350
|
+
get tabIndex() { const v = this.getAttribute('tabindex'); return v === null ? -1 : (Number.parseInt(v, 10) || 0); }
|
|
351
|
+
set tabIndex(v) { this.setAttribute('tabindex', String(v)); }
|
|
352
|
+
|
|
263
353
|
// No server event loop: listeners never fire at SSR. The no-op keeps a
|
|
264
354
|
// constructor that wires delegated listeners (a common lit pattern) from
|
|
265
355
|
// crashing; the browser re-runs the constructor on hydration where the
|
|
@@ -282,6 +372,30 @@ class ServerElement {
|
|
|
282
372
|
}
|
|
283
373
|
}
|
|
284
374
|
|
|
375
|
+
// ARIAMixin IDL reflections (`el.ariaPressed = 'true'` writes aria-pressed).
|
|
376
|
+
// A render() that sets ARIA state via the IDL properties then surfaces the
|
|
377
|
+
// matching aria-* attribute in the SSR'd host tag, matching the browser. The
|
|
378
|
+
// IDL name maps to the content attribute by lowercasing the part after
|
|
379
|
+
// `aria` and prefixing `aria-` (ariaPressed -> aria-pressed).
|
|
380
|
+
const ARIA_IDL_PROPS = [
|
|
381
|
+
'ariaAtomic', 'ariaAutoComplete', 'ariaBusy', 'ariaChecked', 'ariaColCount',
|
|
382
|
+
'ariaColIndex', 'ariaColSpan', 'ariaCurrent', 'ariaDescription', 'ariaDisabled',
|
|
383
|
+
'ariaExpanded', 'ariaHasPopup', 'ariaHidden', 'ariaInvalid', 'ariaKeyShortcuts',
|
|
384
|
+
'ariaLabel', 'ariaLevel', 'ariaLive', 'ariaModal', 'ariaMultiLine',
|
|
385
|
+
'ariaMultiSelectable', 'ariaOrientation', 'ariaPlaceholder', 'ariaPosInSet',
|
|
386
|
+
'ariaPressed', 'ariaReadOnly', 'ariaRequired', 'ariaRoleDescription',
|
|
387
|
+
'ariaRowCount', 'ariaRowIndex', 'ariaRowSpan', 'ariaSelected', 'ariaSetSize',
|
|
388
|
+
'ariaSort', 'ariaValueMax', 'ariaValueMin', 'ariaValueNow', 'ariaValueText',
|
|
389
|
+
];
|
|
390
|
+
for (const idl of ARIA_IDL_PROPS) {
|
|
391
|
+
const attr = 'aria-' + idl.slice(4).toLowerCase();
|
|
392
|
+
Object.defineProperty(ServerElement.prototype, idl, {
|
|
393
|
+
configurable: true,
|
|
394
|
+
get() { return this.getAttribute(attr); },
|
|
395
|
+
set(v) { v == null ? this.removeAttribute(attr) : this.setAttribute(attr, String(v)); },
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
285
399
|
// Base class choice: real HTMLElement on the browser, the shim on the server.
|
|
286
400
|
const Base = isBrowser ? HTMLElement : /** @type {any} */ (ServerElement);
|
|
287
401
|
|
package/src/directives.js
CHANGED
|
@@ -12,11 +12,17 @@
|
|
|
12
12
|
* } from '@webjsdev/core/directives';
|
|
13
13
|
* ```
|
|
14
14
|
*
|
|
15
|
-
* `repeat()`
|
|
15
|
+
* `repeat()` is implemented in `./repeat.js`; it is re-exported here so the
|
|
16
|
+
* `@webjsdev/core/directives` subpath exposes the whole documented directive
|
|
17
|
+
* table in src/dev mode too (the dist browser bundle already carries it).
|
|
16
18
|
*
|
|
17
19
|
* @module directives
|
|
18
20
|
*/
|
|
19
21
|
|
|
22
|
+
// Re-export so the `/directives` subpath surface matches the documented table
|
|
23
|
+
// in both src and dist mode. The implementation stays in repeat.js.
|
|
24
|
+
export { repeat, isRepeat } from './repeat.js';
|
|
25
|
+
|
|
20
26
|
/* ================================================================
|
|
21
27
|
* unsafeHTML
|
|
22
28
|
* ================================================================ */
|
package/src/render-server.js
CHANGED
|
@@ -362,7 +362,7 @@ function browserMemberHint(e) {
|
|
|
362
362
|
* @param {SuspenseCtx} [ctx]
|
|
363
363
|
* @returns {Promise<string>}
|
|
364
364
|
*/
|
|
365
|
-
async function injectDSD(html, ctx) {
|
|
365
|
+
async function injectDSD(html, ctx, ancestors = []) {
|
|
366
366
|
const tags = allTags();
|
|
367
367
|
if (!tags.length) return html;
|
|
368
368
|
// Sort longest tag name first so the regex alternation tries the most
|
|
@@ -391,6 +391,12 @@ async function injectDSD(html, ctx) {
|
|
|
391
391
|
try {
|
|
392
392
|
const isShadow = /** @type any */ (Cls).shadow === true;
|
|
393
393
|
const instance = new /** @type any */ (Cls)();
|
|
394
|
+
// Thread the ancestor chain (the enclosing custom-element instances)
|
|
395
|
+
// so the server element shim's closest() can resolve a parent at SSR.
|
|
396
|
+
// Set before performServerUpdate so a willUpdate() that reads a parent
|
|
397
|
+
// via closest() sees the chain. Each child recursion below extends it.
|
|
398
|
+
instance.__ssrTag = tag;
|
|
399
|
+
instance.__ssrAncestors = ancestors;
|
|
394
400
|
const attrMap = parseAttrs(attrs);
|
|
395
401
|
// Decode `data-webjs-prop-*` attributes first (rich-typed values
|
|
396
402
|
// emitted for `.prop=${val}` bindings in the parent template),
|
|
@@ -416,13 +422,16 @@ async function injectDSD(html, ctx) {
|
|
|
416
422
|
// SSR. WebComponent instances expose performServerUpdate; bare
|
|
417
423
|
// Base-extending kit components (no lifecycle) do not, so it is guarded.
|
|
418
424
|
if (typeof instance.performServerUpdate === 'function') instance.performServerUpdate();
|
|
419
|
-
// Surface attributes the component set before render (reflected
|
|
420
|
-
// reflect:true props, or an explicit this.setAttribute in the
|
|
421
|
-
// constructor / willUpdate) that were not already in the source tag.
|
|
422
|
-
// Appending keeps the original tag byte-identical when nothing changed.
|
|
423
|
-
opening = appendReflectedAttrs(opening, instance, presentAttrNames);
|
|
424
425
|
let tpl = instance.render ? instance.render() : '';
|
|
425
426
|
if (tpl && typeof tpl.then === 'function') tpl = await tpl;
|
|
427
|
+
// Surface attributes the component set up to and including render()
|
|
428
|
+
// that were not already in the source tag: reflected reflect:true
|
|
429
|
+
// props, an explicit this.setAttribute in the constructor / willUpdate,
|
|
430
|
+
// or a host-attribute mutation inside render() itself (a light-DOM
|
|
431
|
+
// compound-component pattern, e.g. this.dataset.state / this.className /
|
|
432
|
+
// this.hidden on the host). Reading after render() captures all three.
|
|
433
|
+
// Appending keeps the original tag byte-identical when nothing changed.
|
|
434
|
+
opening = appendReflectedAttrs(opening, instance, presentAttrNames);
|
|
426
435
|
// Render the template to HTML. injectDSD recurses on the result so
|
|
427
436
|
// nested custom elements (e.g. <theme-toggle> inside <blog-shell>)
|
|
428
437
|
// get their own DSD pass.
|
|
@@ -432,7 +441,7 @@ async function injectDSD(html, ctx) {
|
|
|
432
441
|
// Shadow DOM: native <slot> stays as-is in the DSD template. The
|
|
433
442
|
// browser handles projection from the host's light-DOM children
|
|
434
443
|
// into the shadow tree natively. No framework substitution here.
|
|
435
|
-
const innerProcessed = await injectDSD(rawInner, ctx);
|
|
444
|
+
const innerProcessed = await injectDSD(rawInner, ctx, [...ancestors, instance]);
|
|
436
445
|
const rawStyles = /** @type any */ (Cls).styles;
|
|
437
446
|
const styleList = Array.isArray(rawStyles) ? rawStyles : rawStyles && isCSS(rawStyles) ? [rawStyles] : [];
|
|
438
447
|
const styleStr = stylesToString(styleList);
|
|
@@ -495,7 +504,7 @@ async function injectDSD(html, ctx) {
|
|
|
495
504
|
}
|
|
496
505
|
const partitioned = partitionAuthoredBySlot(authoredInner);
|
|
497
506
|
const innerWithSlots = substituteSlotsInRender(rawInner, partitioned);
|
|
498
|
-
const innerProcessed = await injectDSD(innerWithSlots, ctx);
|
|
507
|
+
const innerProcessed = await injectDSD(innerWithSlots, ctx, [...ancestors, instance]);
|
|
499
508
|
edits.push({
|
|
500
509
|
start: m.index,
|
|
501
510
|
end: closeEnd,
|
|
@@ -523,6 +532,11 @@ async function injectDSD(html, ctx) {
|
|
|
523
532
|
// independently against the original html, but those inner elements
|
|
524
533
|
// are processed by the recursive injectDSD call on innerWithSlots.
|
|
525
534
|
// Keeping both edits would double-process them and corrupt the output.
|
|
535
|
+
// A consequence: a nested instance's render() runs once per chain depth
|
|
536
|
+
// (the discarded top-level pass sees an empty ancestor chain, so its
|
|
537
|
+
// closest() reads null; the kept recursive pass has the real chain). The
|
|
538
|
+
// kept pass is the only output, and closest() is a read, so render() must
|
|
539
|
+
// stay pure at SSR (the standard SSR contract), not branch on side effects.
|
|
526
540
|
edits.sort((a, b) => a.start - b.start);
|
|
527
541
|
/** @type {{start:number, end:number, text:string}[]} */
|
|
528
542
|
const filtered = [];
|