@webjsdev/core 0.7.64 → 0.8.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.
Files changed (58) 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 +21 -22
  4. package/dist/webjs-core-testing.js.map +3 -3
  5. package/dist/webjs-core.js +6 -7
  6. package/dist/webjs-core.js.map +3 -3
  7. package/index-browser.js +6 -2
  8. package/index.d.ts +7 -3
  9. package/index.js +6 -2
  10. package/package.json +1 -1
  11. package/src/attribute-directives.d.ts +6 -0
  12. package/src/attribute-directives.js +49 -0
  13. package/src/attribute-reader.js +37 -3
  14. package/src/binding-prefixes.js +10 -41
  15. package/src/component/lifecycle.js +261 -101
  16. package/src/component/reactive.js +5 -4
  17. package/src/component.d.ts +11 -5
  18. package/src/directives.d.ts +2 -0
  19. package/src/directives.js +2 -0
  20. package/src/escape.js +12 -2
  21. package/src/form-action.js +7 -7
  22. package/src/html.d.ts +4 -0
  23. package/src/html.js +22 -1
  24. package/src/optimistic.js +3 -3
  25. package/src/registry.d.ts +5 -0
  26. package/src/registry.js +35 -4
  27. package/src/render-client/adopt-slots.js +245 -0
  28. package/src/render-client/parts.js +53 -19
  29. package/src/render-client/reconciler.js +28 -12
  30. package/src/render-client/template-compiler.js +19 -5
  31. package/src/render-client.d.ts +2 -1
  32. package/src/render-server/attrs.js +5 -4
  33. package/src/render-server/compile.js +433 -0
  34. package/src/render-server/dsd.js +11 -435
  35. package/src/render-server/errors.js +84 -0
  36. package/src/render-server/expand.js +346 -0
  37. package/src/render-server/html-scan.js +19 -358
  38. package/src/render-server/served-attrs.js +79 -0
  39. package/src/render-server/stream.js +11 -13
  40. package/src/render-server/template-renderer.js +13 -936
  41. package/src/render-server/walk.js +798 -0
  42. package/src/router-client/anchors.js +3 -1
  43. package/src/router-client/boundaries.js +15 -1
  44. package/src/router-client/constants.js +30 -0
  45. package/src/router-client/fetch-apply.js +21 -0
  46. package/src/router-client/frames.js +6 -3
  47. package/src/router-client/head-merge.js +58 -10
  48. package/src/router-client/navigator.js +4 -0
  49. package/src/router-client/owned-styles.js +372 -0
  50. package/src/router-client/prefetch.js +3 -0
  51. package/src/router-client/snapshot-cache.js +2 -12
  52. package/src/router-client/stylesheets.js +191 -0
  53. package/src/router-client/swap.js +49 -9
  54. package/src/sentinels.d.ts +2 -0
  55. package/src/sentinels.js +4 -0
  56. package/src/slot.js +1 -1
  57. package/src/webjs-config.d.ts +1 -1
  58. package/src/render-server/slots.js +0 -213
package/index-browser.js CHANGED
@@ -23,10 +23,10 @@
23
23
  * Keep this list in sync with `./index.js` for everything else.
24
24
  */
25
25
 
26
- export { html, isTemplate, MARKER } from './src/html.js';
26
+ export { html, svg, isTemplate, MARKER } from './src/html.js';
27
27
  export { css, isCSS, adoptStyles, stylesToString } from './src/css.js';
28
28
  export { WebComponent, prop } from './src/component.js';
29
- export { register, lookup, lookupModuleUrl, isLazy, allTags, primeModuleUrl, tagOf } from './src/registry.js';
29
+ export { lookup, lookupModuleUrl, isLazy, allTags, primeModuleUrl, tagOf } from './src/registry.js';
30
30
  export { render } from './src/render-client.js';
31
31
  export { escapeText, escapeAttr } from './src/escape.js';
32
32
  export { notFound, redirect, forbidden, unauthorized, isNotFound, isRedirect, isForbidden, isUnauthorized } from './src/nav.js';
@@ -100,3 +100,7 @@ export { createContext, ContextProvider, ContextConsumer, ContextRequestEvent }
100
100
 
101
101
  // Task controller, also available via '@webjsdev/core/task'
102
102
  export { Task, TaskStatus } from './src/task.js';
103
+
104
+ export { nothing, noChange } from './src/sentinels.js';
105
+
106
+ export { classMap, styleMap, ifDefined } from './src/attribute-directives.js';
package/index.d.ts CHANGED
@@ -74,13 +74,13 @@ export type {
74
74
  NonSerializable,
75
75
  } from './src/serializable.d.ts';
76
76
 
77
- export { html, isTemplate, MARKER } from './src/html.js';
77
+ export { html, svg, isTemplate, MARKER } from './src/html.js';
78
78
  // The `html\`\`` return type. A JSDoc typedef in html.js used across the
79
79
  // component / route / testing type surfaces; re-exported so an app or the
80
80
  // docs/website can annotate a value as `TemplateResult` (#772).
81
81
  export type { TemplateResult } from './src/html.js';
82
82
  export { css, isCSS, adoptStyles, stylesToString } from './src/css.js';
83
- export { register, lookup, lookupModuleUrl, isLazy, allTags, primeModuleUrl, tagOf } from './src/registry.js';
83
+ export { lookup, lookupModuleUrl, isLazy, allTags, primeModuleUrl, tagOf } from './src/registry.js';
84
84
  export { renderToString, renderToStream } from './src/render-server.js';
85
85
  export { render } from './src/render-client.js';
86
86
  export { escapeText, escapeAttr } from './src/escape.js';
@@ -152,10 +152,14 @@ export function optimistic<State>(
152
152
  ): OptimisticState<State, State>;
153
153
 
154
154
 
155
- // Legacy Imperative Signature (Signal-based rollback)
155
+ // Imperative signature (signal-based rollback)
156
156
  export function optimistic<T, R>(
157
157
  signal: { get(): T; set(v: T): void },
158
158
  value: T,
159
159
  action: () => Promise<R> | R,
160
160
  ): Promise<R>;
161
161
 
162
+
163
+ export { nothing, noChange } from './src/sentinels.js';
164
+
165
+ export { classMap, styleMap, ifDefined } from './src/attribute-directives.js';
package/index.js CHANGED
@@ -6,10 +6,10 @@
6
6
  * renderer is only reached on the server.
7
7
  */
8
8
 
9
- export { html, isTemplate, MARKER } from './src/html.js';
9
+ export { html, svg, isTemplate, MARKER } from './src/html.js';
10
10
  export { css, isCSS, adoptStyles, stylesToString } from './src/css.js';
11
11
  export { WebComponent, prop } from './src/component.js';
12
- export { register, lookup, lookupModuleUrl, isLazy, allTags, primeModuleUrl, tagOf } from './src/registry.js';
12
+ export { lookup, lookupModuleUrl, isLazy, allTags, primeModuleUrl, tagOf } from './src/registry.js';
13
13
  export { renderToString, renderToStream } from './src/render-server.js';
14
14
  export { render } from './src/render-client.js';
15
15
  export { escapeText, escapeAttr } from './src/escape.js';
@@ -68,3 +68,7 @@ export { createContext, ContextProvider, ContextConsumer, ContextRequestEvent }
68
68
 
69
69
  // Task controller, also available via '@webjsdev/core/task'
70
70
  export { Task, TaskStatus } from './src/task.js';
71
+
72
+ export { nothing, noChange } from './src/sentinels.js';
73
+
74
+ export { classMap, styleMap, ifDefined } from './src/attribute-directives.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webjsdev/core",
3
- "version": "0.7.64",
3
+ "version": "0.8.0",
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,6 @@
1
+ import { nothing } from './sentinels.js';
2
+ export interface ClassMapDirective { readonly _$webjs: 'class-map'; readonly value: Record<string, unknown>; }
3
+ export interface StyleMapDirective { readonly _$webjs: 'style-map'; readonly value: Record<string, string | number | null | undefined>; }
4
+ export function classMap(classes: Record<string, unknown>): ClassMapDirective;
5
+ export function styleMap(styles: Record<string, string | number | null | undefined>): StyleMapDirective;
6
+ export function ifDefined<T>(value: T): NonNullable<T> | typeof nothing;
@@ -0,0 +1,49 @@
1
+ import { nothing } from './sentinels.js';
2
+
3
+ /** Map class names to whether each should be present. @param {Record<string, unknown>} classes */
4
+ export function classMap(classes) { return { _$webjs: 'class-map', value: classes }; }
5
+ /** Map CSS property names to their values. @param {Record<string, string | number | null | undefined>} styles */
6
+ export function styleMap(styles) { return { _$webjs: 'style-map', value: styles }; }
7
+ /** Remove an attribute when its value is nullish. @template T @param {T} value */
8
+ export function ifDefined(value) { return value ?? nothing; }
9
+
10
+ export function isAttributeMap(value) {
11
+ return value && (value._$webjs === 'class-map' || value._$webjs === 'style-map');
12
+ }
13
+ export function cssName(name) { return name.startsWith('--') ? name : name.replace(/[A-Z]/g, (c) => '-' + c.toLowerCase()); }
14
+ export function attributeMapText(value, name) {
15
+ const classes = value._$webjs === 'class-map';
16
+ if (name !== (classes ? 'class' : 'style')) throw new Error('classMap/styleMap must be used in a class/style attribute.');
17
+ // Lit deliberately pads even an empty map to separate adjacent static classes.
18
+ return classes
19
+ ? ' ' + Object.keys(value.value).filter((key) => value.value[key]).join(' ') + ' '
20
+ : Object.entries(value.value).filter(([, v]) => v != null).map(([key, v]) => `${cssName(key)}:${v};`).join('');
21
+ }
22
+
23
+ /** Commit only the map-owned tokens, preserving static and externally added values. */
24
+ export function applyAttributeMap(part, value) {
25
+ if (part.kind !== 'attr' && part.kind !== 'attr-mixed') throw new Error('classMap/styleMap require an attribute binding.');
26
+ const classes = value._$webjs === 'class-map';
27
+ if (part.name !== (classes ? 'class' : 'style')) throw new Error('classMap/styleMap must be used in a class/style attribute.');
28
+ if (part.group && part.group.length !== 1) throw new Error('classMap/styleMap require a single expression.');
29
+ if (part.__mapKind !== value._$webjs) {
30
+ part.el.setAttribute(part.name, (part.statics || []).join(classes ? ' ' : ''));
31
+ part.__mapKind = value._$webjs;
32
+ part.__mapKeys = new Set();
33
+ part.__staticClasses = classes ? new Set(part.el.classList) : new Set();
34
+ }
35
+ const next = new Set();
36
+ for (const [key, v] of Object.entries(value.value)) {
37
+ if (classes) {
38
+ if (v && !part.__staticClasses.has(key)) { part.el.classList.add(key); next.add(key); }
39
+ } else if (v != null) {
40
+ const name = cssName(key); const text = String(v);
41
+ part.el.style.setProperty(name, text.replace(/\s*!important\s*$/, ''), /!important\s*$/.test(text) ? 'important' : '');
42
+ next.add(name);
43
+ }
44
+ }
45
+ for (const key of part.__mapKeys) if (!next.has(key)) {
46
+ if (classes) part.el.classList.remove(key); else part.el.style.removeProperty(key);
47
+ }
48
+ part.__mapKeys = next;
49
+ }
@@ -39,6 +39,20 @@
39
39
  * JS under a hand-written overlay, so the shared code moves out instead.
40
40
  */
41
41
 
42
+ /**
43
+ * Whether a declared property has an HTML attribute at all. lit's two ways to
44
+ * say no, both honoured: `state: true` (internal state, which lit normalizes
45
+ * to `attribute: false`) and `attribute: false` (a public, property-only
46
+ * input). A property without one is never observed, never read from a source
47
+ * attribute at SSR, and never reflected; a `.prop` binding still reaches it.
48
+ *
49
+ * @param {{ state?: boolean, attribute?: string | false } | null | undefined} decl
50
+ * @returns {boolean}
51
+ */
52
+ export function isAttributeBacked(decl) {
53
+ return !!decl && decl.state !== true && decl.attribute !== false;
54
+ }
55
+
42
56
  /**
43
57
  * Read one attribute string into its declared property value.
44
58
  *
@@ -110,7 +124,7 @@ export function readAttributeValue(def, value) {
110
124
  // The two readers now see the SAME attribute set, so this fallback is not
111
125
  // the only thing they agree on (#1341). Both resolve a name through
112
126
  // `resolveAttributeProperty` below, which matches the same names
113
- // `observedAttributes` lists (via `observedAttributeNames`) and nothing else, so it skips a `state: true` prop and ignores a name that
127
+ // `observedAttributes` lists (via `observedAttributeNames`) and nothing else, so it skips a `state: true` or `attribute: false` prop and ignores a name that
114
128
  // maps to no attribute-backed property exactly as the observed list does.
115
129
  // The SSR caller lowercases its source name first, because the browser
116
130
  // lowercases while parsing. And both are handed a value with every
@@ -130,7 +144,7 @@ function hyphenate(s) {
130
144
  * Resolve an attribute NAME to the property it feeds, and to that property's
131
145
  * normalised declaration.
132
146
  *
133
- * `state: true` props are skipped, because they are absent from
147
+ * `state: true` and `attribute: false` props are skipped, because they are absent from
134
148
  * `observedAttributes` and so the browser never delivers their attribute to any
135
149
  * reader. lit reaches the same outcome the same way, resolving through the very
136
150
  * map `observedAttributes` is built from (`reactive-element.ts`
@@ -212,11 +226,31 @@ export function observedAttributeNames(Cls) {
212
226
  * @returns {{ propName: string, def: PropertyDeclaration, primary: string, alias: string | undefined }[]}
213
227
  */
214
228
  function attributeBackedProps(Cls) {
229
+ // Memoized per class (#1646): the SSR walk resolves every source attribute
230
+ // of every instance through this list, and rebuilding it (one hyphenate per
231
+ // declared prop) was a measurable share of a component's render. A class's
232
+ // declarations never change after it is defined (a `static properties`
233
+ // block throws at runtime, and the factory fixes the set), so the first
234
+ // walk is the only one.
235
+ if (Cls && typeof Cls === 'function') {
236
+ const hit = BACKED_PROPS.get(Cls);
237
+ if (hit) return hit;
238
+ const built = buildAttributeBackedProps(Cls);
239
+ BACKED_PROPS.set(Cls, built);
240
+ return built;
241
+ }
242
+ return buildAttributeBackedProps(Cls);
243
+ }
244
+
245
+ /** @type {WeakMap<Function, { propName: string, def: any, primary: string, alias: string | undefined }[]>} */
246
+ const BACKED_PROPS = new WeakMap();
247
+
248
+ function buildAttributeBackedProps(Cls) {
215
249
  const props = (Cls && Cls.properties) || {};
216
250
  const out = [];
217
251
  for (const [k, decl] of Object.entries(props)) {
218
252
  const d = typeof decl === 'object' && decl !== null ? decl : { type: decl };
219
- if (d.state) continue;
253
+ if (!isAttributeBacked(d)) continue;
220
254
  const primary = d.attribute || hyphenate(k);
221
255
  const lower = k.toLowerCase();
222
256
  const alias = !d.attribute && lower !== primary ? lower : undefined;
@@ -1,3 +1,4 @@
1
+ import { nothing, noChange } from './sentinels.js';
1
2
  /**
2
3
  * The template attribute-binding prefixes the renderers recognise.
3
4
  *
@@ -42,48 +43,16 @@ export function isBindingPrefix(ch) {
42
43
  }
43
44
 
44
45
  /**
45
- * The HTML boolean attributes: present means on, absent means off, and the
46
- * value is meant to be empty. A boolean in a PLAIN hole on one of these renders
47
- * exactly like `?attr` (#1579), so `checked=${isDefault}` cannot serve the
48
- * `checked="false"` that HTML reads as checked. Lowercase, from the HTML spec's
49
- * attribute index.
50
- */
51
- const BOOLEAN_ATTRIBUTES = new Set([
52
- 'allowfullscreen', 'async', 'autofocus', 'autoplay', 'checked', 'controls',
53
- 'default', 'defer', 'disabled', 'formnovalidate', 'hidden', 'inert',
54
- 'ismap', 'itemscope', 'loop', 'multiple', 'muted', 'nomodule', 'novalidate',
55
- 'open', 'playsinline', 'readonly', 'required', 'reversed', 'selected',
56
- 'shadowrootclonable', 'shadowrootdelegatesfocus', 'shadowrootserializable',
57
- ]);
58
-
59
- /**
60
- * What a PLAIN attribute hole (`name=${value}`, unquoted, the whole value)
61
- * writes, or `null` when the attribute is omitted. The one rule both renderers
62
- * follow, so the server never serves an attribute the client then removes
63
- * (#1573, #1579):
64
- *
65
- * - `null` / `undefined` omit the attribute. This is the "omit" value, so a
66
- * conditional attribute is `aria-current=${active ? 'page' : null}`.
67
- * - `false` omits it too, EXCEPT on an `aria-*` attribute, where it writes
68
- * `"false"`. For a tri-state ARIA attribute (`aria-expanded`,
69
- * `aria-pressed`, `aria-checked`) `"false"` and absent mean different
70
- * things, so the value an author passes is the value the user agent gets.
71
- * - `true` on an HTML boolean attribute (`checked`, `selected`, `disabled`,
72
- * and the rest) writes the empty value, exactly as `?attr=${true}` does. Anywhere
73
- * else it is `"true"`.
74
- * - anything else is `String(value)`.
75
- *
76
- * A quoted or mixed value (`title="${x}"`, `class="a ${x}"`) is not a plain
77
- * hole: the author wrote the attribute statically, so it stays and a nullish
78
- * piece reads as empty text.
46
+ * Serialize an ordinary whole-value attribute expression for SSR and client
47
+ * commits. Nullish values become empty strings and booleans stringify.
48
+ * `nothing` omits the attribute. `noChange` omits it on initial SSR; client
49
+ * updates handle that sentinel before serialization to retain the prior value.
79
50
  *
80
- * @param {string} name the attribute name as written
81
- * @param {unknown} value the hole's value (`live()` already unwrapped)
51
+ * @param {string} _name attribute name (kept for the shared call signature)
52
+ * @param {unknown} value the expression value (`live()` already unwrapped)
82
53
  * @returns {string | null}
83
54
  */
84
- export function attrHoleValue(name, value) {
85
- if (value == null) return null;
86
- if (value === false) return /^aria-/i.test(name) ? 'false' : null;
87
- if (value === true && BOOLEAN_ATTRIBUTES.has(String(name).toLowerCase())) return '';
88
- return String(value);
55
+ export function attrHoleValue(_name, value) {
56
+ if (value === nothing || value === noChange) return null;
57
+ return String(value ?? '');
89
58
  }