@webjsdev/core 0.7.21 → 0.7.23

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/index-browser.js CHANGED
@@ -24,7 +24,7 @@
24
24
 
25
25
  export { html, isTemplate, MARKER } from './src/html.js';
26
26
  export { css, isCSS, adoptStyles, stylesToString } from './src/css.js';
27
- export { WebComponent } from './src/component.js';
27
+ export { WebComponent, prop } from './src/component.js';
28
28
  export { register, lookup, lookupModuleUrl, isLazy, allTags, primeModuleUrl, tagOf } from './src/registry.js';
29
29
  export { render } from './src/render-client.js';
30
30
  export { escapeText, escapeAttr } from './src/escape.js';
@@ -38,6 +38,15 @@ export {
38
38
  stringify, parse,
39
39
  serialize, deserialize,
40
40
  } from './src/serialize.js';
41
+ // The client router auto-enables on bundle load: `router-client.js` calls
42
+ // `enableClientRouter()` at module end (idempotent). This is INTENTIONAL, so
43
+ // any page that loads `@webjsdev/core` (i.e. ships any component) gets client
44
+ // navigation with no per-app opt-in. A layout no longer needs to
45
+ // `import '@webjsdev/core/client-router'`. The bare side-effect import below
46
+ // pins that contract even if the named re-export is later refactored away;
47
+ // the re-export still also evaluates the module, so the two are belt-and-
48
+ // suspenders, not a double-enable (ESM evaluates once, enable is idempotent).
49
+ import './src/router-client.js';
41
50
  export { enableClientRouter, disableClientRouter, navigate, revalidate } from './src/router-client.js';
42
51
  export { WebjsFrame } from './src/webjs-frame.js';
43
52
  export { WebjsStream, renderStream } from './src/webjs-stream.js';
package/index.js CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  export { html, isTemplate, MARKER } from './src/html.js';
10
10
  export { css, isCSS, adoptStyles, stylesToString } from './src/css.js';
11
- export { WebComponent } from './src/component.js';
11
+ export { WebComponent, prop } from './src/component.js';
12
12
  export { register, 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';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webjsdev/core",
3
- "version": "0.7.21",
3
+ "version": "0.7.23",
4
4
  "type": "module",
5
5
  "description": "webjs core runtime - html/css tags, WebComponent base, isomorphic renderers",
6
6
  "types": "./index.d.ts",
@@ -22,7 +22,7 @@ export type PropertyConstructor<T = unknown> =
22
22
  | (new (...args: any[]) => T)
23
23
  | ((v: any) => T);
24
24
 
25
- /** Runtime-level property declaration: matches `static properties = { … }`. */
25
+ /** Runtime-level property declaration: one entry in `WebComponent({ … })`. */
26
26
  export interface PropertyDeclaration<T = unknown> {
27
27
  /** Constructor used for string → value coercion when the attribute changes. */
28
28
  type?: PropertyConstructor<T>;
@@ -52,27 +52,30 @@ export interface ReactiveController {
52
52
  /**
53
53
  * Base class for interactive web components.
54
54
  *
55
- * Declare runtime behaviour via `static properties = { … }` and type
56
- * each field with a sibling `declare foo: Foo`:
55
+ * Declare reactive properties via the `extends WebComponent({ … })`
56
+ * factory. It installs the reactive accessor AND types each field for
57
+ * you (no `declare`, no `static properties`):
57
58
  *
58
- * class StudentCard extends WebComponent {
59
- * static properties = { student: { type: Object } };
60
- * declare student: Student;
59
+ * class StudentCard extends WebComponent({ student: prop<Student>(Object) }) {
61
60
  * render() { return html`<p>${this.student.name}</p>`; }
62
61
  * }
63
62
  * StudentCard.register('student-card');
64
63
  *
65
- * The `declare` field has no runtime cost: it tells TypeScript the
66
- * field's type without emitting a class-field initializer that would
67
- * clobber the reactive accessor the framework installs via
68
- * `Object.defineProperty` in the constructor.
64
+ * A bare constructor type covers the common case (`WebComponent({ count:
65
+ * Number })` gives `this.count: number`); use the `prop()` helper for
66
+ * options (`prop(Number, { reflect: true })`) or to narrow the type
67
+ * (`prop<Student>(Object)`). Set defaults via the `default` option or in
68
+ * the constructor, never a class-field initializer (it runs after super()
69
+ * and clobbers the reactive accessor). A hand-written `static properties`
70
+ * in a class body is no longer supported and throws at construction.
69
71
  *
70
72
  * See the Editor Setup doc for tooling that gives attribute/tag
71
73
  * intelligence inside `html\`…\`` templates.
72
74
  */
73
- export abstract class WebComponent extends HTMLElement {
75
+ abstract class WebComponentBase extends HTMLElement {
74
76
  static shadow: boolean;
75
77
  static hydrate: 'visible' | undefined;
78
+ /** @internal Populated by the `WebComponent({ … })` factory, not by hand. */
76
79
  static properties: Record<string, PropertyDeclaration>;
77
80
  static styles: CSSResult | CSSResult[] | null;
78
81
  static lazy?: boolean;
@@ -84,8 +87,8 @@ export abstract class WebComponent extends HTMLElement {
84
87
  * Schedule a re-render. Optionally record a property change so lifecycle
85
88
  * hooks can branch on what changed via `changedProperties`.
86
89
  *
87
- * For instance-local state, prefer a reactive property (`static
88
- * properties = { foo: { state: true } }`) or a signal from
90
+ * For instance-local state, prefer a reactive property (`WebComponent({
91
+ * foo: prop({ state: true }) })`) or a signal from
89
92
  * `@webjsdev/core`'s `signal()`. The framework's built-in
90
93
  * SignalWatcher wires every signal read in `render()` to a re-render
91
94
  * automatically; explicit `requestUpdate()` is rarely needed.
@@ -129,3 +132,59 @@ export abstract class WebComponent extends HTMLElement {
129
132
  disconnectedCallback(): void;
130
133
  attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
131
134
  }
135
+
136
+ export type WebComponent = WebComponentBase;
137
+
138
+ export type Infer<C> =
139
+ C extends BooleanConstructor ? boolean :
140
+ C extends NumberConstructor ? number :
141
+ C extends StringConstructor ? string :
142
+ C extends new (...a: any[]) => infer T ? T :
143
+ C extends (v: any) => infer T ? T : unknown;
144
+
145
+ export type InferProps<S> = {
146
+ [K in keyof S]: S[K] extends PropertyDeclaration<infer T> ? T : Infer<S[K]>
147
+ };
148
+
149
+ /**
150
+ * The static surface every `WebComponent` constructor carries (the bare base,
151
+ * a factory-produced base, and a registered subclass alike). Declared ONCE
152
+ * here and referenced from both `WebComponentConstructor` and the factory-call
153
+ * return type below, so adding or changing a static can never let the two
154
+ * positions drift. Mirrors the `static` members on `WebComponentBase`.
155
+ */
156
+ interface WebComponentStatics {
157
+ shadow: boolean;
158
+ hydrate: 'visible' | undefined;
159
+ properties: Record<string, PropertyDeclaration>;
160
+ styles: CSSResult | CSSResult[] | null;
161
+ lazy?: boolean;
162
+ register(tag: string): void;
163
+ readonly observedAttributes: string[];
164
+ }
165
+
166
+ export interface WebComponentConstructor extends WebComponentStatics {
167
+ new (): WebComponentBase;
168
+ prototype: WebComponentBase;
169
+
170
+ <S extends Record<string, any>>(shape: S): WebComponentStatics & {
171
+ new (): WebComponentBase & InferProps<S>;
172
+ prototype: WebComponentBase & InferProps<S>;
173
+ };
174
+ }
175
+
176
+ export declare const WebComponent: WebComponentConstructor;
177
+
178
+ export declare function prop<C extends PropertyConstructor<any>>(
179
+ type: C,
180
+ opts?: Omit<PropertyDeclaration<Infer<C>>, 'type'>
181
+ ): PropertyDeclaration<Infer<C>>;
182
+
183
+ export declare function prop<T>(
184
+ type: PropertyConstructor<any>,
185
+ opts?: Omit<PropertyDeclaration<T>, 'type'>
186
+ ): PropertyDeclaration<T>;
187
+
188
+ export declare function prop<T = string>(
189
+ opts?: PropertyDeclaration<T>
190
+ ): PropertyDeclaration<T>;
package/src/component.js CHANGED
@@ -400,7 +400,18 @@ for (const idl of ARIA_IDL_PROPS) {
400
400
  // Base class choice: real HTMLElement on the browser, the shim on the server.
401
401
  const Base = isBrowser ? HTMLElement : /** @type {any} */ (ServerElement);
402
402
 
403
- export class WebComponent extends Base {
403
+ /**
404
+ * Marker stamped on the anonymous subclass the `WebComponent({...})` factory
405
+ * produces. It lets `_assertFactoryProperties` tell the framework's own
406
+ * factory-generated `static properties` (allowed) apart from a `static
407
+ * properties` a user wrote by hand in a class body (no longer allowed).
408
+ */
409
+ const FACTORY_PROPS = Symbol('webjs.factoryProps');
410
+
411
+ // Per-class memo so the constructor-time enforcement walk runs once per class.
412
+ const _propsChecked = new WeakSet();
413
+
414
+ class WebComponentBase extends Base {
404
415
  /** Whether to use shadow DOM. Default: false (light DOM). @type {boolean} */
405
416
  static shadow = false;
406
417
 
@@ -531,10 +542,45 @@ export class WebComponent extends Base {
531
542
  */
532
543
  this._isUpdating = false;
533
544
 
545
+ // Enforce the declare-free factory DX: a hand-written `static properties`
546
+ // in a class body is a hard error (use `extends WebComponent({ … })`).
547
+ this._assertFactoryProperties();
548
+
534
549
  // Install reactive property accessors for `static properties` declarations.
535
550
  this._initializeProperties();
536
551
  }
537
552
 
553
+ /**
554
+ * Throw if a class in this instance's constructor chain declares its own
555
+ * `static properties`. Reactive properties must be declared via the
556
+ * `extends WebComponent({ … })` factory, which stamps {@link FACTORY_PROPS}
557
+ * on the subclass it generates; a `static properties` written by hand in a
558
+ * class body carries no such marker and is rejected here (issue #598).
559
+ *
560
+ * The walk stops at {@link WebComponentBase} (whose `static properties = {}`
561
+ * default is internal) and is memoized per class so it runs once.
562
+ * @private
563
+ */
564
+ _assertFactoryProperties() {
565
+ const Ctor = /** @type {any} */ (this.constructor);
566
+ if (_propsChecked.has(Ctor)) return;
567
+ let C = Ctor;
568
+ while (C && C !== WebComponentBase) {
569
+ if (Object.hasOwn(C, 'properties') && !Object.hasOwn(C, FACTORY_PROPS)) {
570
+ const name = C.name || 'a component';
571
+ throw new Error(
572
+ `${name}: \`static properties\` is no longer supported. Declare reactive ` +
573
+ `properties via the factory instead: \`class ${name} extends WebComponent({ ` +
574
+ `count: Number })\`. Use the \`prop()\` helper for options ` +
575
+ `(\`prop(Number, { reflect: true })\`) and set defaults via the \`default\` ` +
576
+ `option or in the constructor. See https://docs.webjs.com/docs/components.`,
577
+ );
578
+ }
579
+ C = Object.getPrototypeOf(C);
580
+ }
581
+ _propsChecked.add(Ctor);
582
+ }
583
+
538
584
  /**
539
585
  * For every key in `static properties`, create a getter/setter pair on
540
586
  * the instance that coerces values, runs `hasChanged`, schedules updates,
@@ -893,8 +939,13 @@ export class WebComponent extends Base {
893
939
 
894
940
  const Ctor = /** @type any */ (this.constructor);
895
941
  const propName = camelCase(name);
896
- const def = Ctor.properties && (Ctor.properties[propName] || Ctor.properties[name]);
897
- if (!def) return;
942
+ const raw = Ctor.properties && (Ctor.properties[propName] || Ctor.properties[name]);
943
+ if (!raw) return;
944
+ // A declaration is either a full descriptor (`{ type: Number, … }`) or the
945
+ // bare-constructor shorthand the factory accepts (`count: Number`), in which
946
+ // case the value IS the type. Normalise so type-based coercion fires either
947
+ // way (matches `_initializeProperties`).
948
+ const def = typeof raw === 'object' ? raw : { type: raw };
898
949
 
899
950
  let v;
900
951
  if (def.converter && def.converter.fromAttribute) {
@@ -1553,3 +1604,41 @@ function hyphenate(s) {
1553
1604
  function camelCase(s) {
1554
1605
  return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
1555
1606
  }
1607
+
1608
+ /**
1609
+ * Dual-role WebComponent class and factory.
1610
+ * Can be extended directly: `class X extends WebComponent`
1611
+ * Or called as a class factory: `class X extends WebComponent({ count: Number })`
1612
+ *
1613
+ * @param {Record<string, any>} [properties]
1614
+ * @returns {any}
1615
+ */
1616
+ export function WebComponent(properties) {
1617
+ if (new.target) {
1618
+ return Reflect.construct(WebComponentBase, arguments, new.target);
1619
+ } else {
1620
+ return class extends WebComponentBase {
1621
+ static properties = properties;
1622
+ static [FACTORY_PROPS] = true;
1623
+ };
1624
+ }
1625
+ }
1626
+
1627
+ // Ensure static inheritance and instanceof checks work
1628
+ Object.setPrototypeOf(WebComponent, WebComponentBase);
1629
+ WebComponent.prototype = WebComponentBase.prototype;
1630
+
1631
+ /**
1632
+ * Helper to define properties with custom options.
1633
+ *
1634
+ * @param {any} [type]
1635
+ * @param {any} [opts]
1636
+ * @returns {any}
1637
+ */
1638
+ export function prop(type, opts = {}) {
1639
+ if (type && typeof type === 'object' && !('call' in type)) {
1640
+ opts = type;
1641
+ type = undefined;
1642
+ }
1643
+ return { ...(type ? { type } : {}), ...opts };
1644
+ }
@@ -1052,12 +1052,16 @@ function appendReflectedAttrs(opening, instance, presentAttrNames) {
1052
1052
  function applyAttrsToInstance(instance, attrs, Cls) {
1053
1053
  const props = Cls.properties || {};
1054
1054
  for (const [key, raw] of Object.entries(attrs)) {
1055
- const def = props[key] || props[camelCase(key)];
1055
+ const rawDef = props[key] || props[camelCase(key)];
1056
1056
  const propName = props[key] ? key : camelCase(key);
1057
- if (!def) {
1057
+ if (!rawDef) {
1058
1058
  instance[propName] = raw;
1059
1059
  continue;
1060
1060
  }
1061
+ // The factory accepts a bare constructor shorthand (`{ expanded: Boolean }`)
1062
+ // alongside the long form (`{ expanded: { type: Boolean } }`); normalize so
1063
+ // the type-based coercion below sees a `{ type }` object either way.
1064
+ const def = typeof rawDef === 'object' ? rawDef : { type: rawDef };
1061
1065
  if (def.type === Number) instance[propName] = Number(raw);
1062
1066
  else if (def.type === Boolean) instance[propName] = raw !== 'false';
1063
1067
  else if (def.type === Object || def.type === Array) {