regor 1.4.5 → 1.4.6

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 CHANGED
@@ -97,6 +97,132 @@ HTML:
97
97
  </div>
98
98
  ```
99
99
 
100
+ ## Component Props Validation
101
+
102
+ Regor components can validate incoming props at runtime inside `context(head)`.
103
+
104
+ This is opt-in and local to the component author:
105
+
106
+ - it does not change `defineComponent(...)`
107
+ - it validates only the keys you list
108
+ - it throws immediately on the first invalid prop
109
+ - it does not coerce values
110
+ - it does not mutate `head.props`
111
+
112
+ Use `head.validateProps(...)` together with `pval`:
113
+
114
+ ```ts
115
+ import { defineComponent, html, pval } from 'regor'
116
+
117
+ type EditorCard = {
118
+ title: string
119
+ count?: number
120
+ mode: 'create' | 'edit'
121
+ summary?: string
122
+ }
123
+
124
+ const editorCard = defineComponent<EditorCard>(
125
+ html`<article>{{ summary }}</article>`,
126
+ {
127
+ props: ['title', 'count', 'mode'],
128
+ context: (head) => {
129
+ head.validateProps({
130
+ title: pval.isString,
131
+ count: pval.optional(pval.isNumber),
132
+ mode: pval.oneOf(['create', 'edit'] as const),
133
+ })
134
+
135
+ return {
136
+ ...head.props,
137
+ summary: `${head.props.title}:${head.props.mode}:${head.props.count ?? 'none'}`,
138
+ }
139
+ },
140
+ },
141
+ )
142
+ ```
143
+
144
+ ### Built-in validators
145
+
146
+ ```ts
147
+ import { pval } from 'regor'
148
+
149
+ pval.isString
150
+ pval.isNumber
151
+ pval.isBoolean
152
+ pval.isClass(MyClass)
153
+ pval.optional(pval.isString)
154
+ pval.nullable(pval.isNumber)
155
+ pval.oneOf(['create', 'edit'] as const)
156
+ pval.arrayOf(pval.isString)
157
+ pval.shape({ title: pval.isString, count: pval.isNumber })
158
+ pval.refOf(pval.isString)
159
+ ```
160
+
161
+ ### Dynamic bindings and refs
162
+
163
+ Single-prop dynamic bindings like `:title="titleRef"` flow into component props as refs.
164
+ When validating those runtime values, use `pval.refOf(...)`:
165
+
166
+ ```ts
167
+ type CardProps = {
168
+ title: Ref<string>
169
+ summary?: string
170
+ }
171
+
172
+ const card = defineComponent<CardProps>(html`<h3>{{ summary }}</h3>`, {
173
+ props: ['title'],
174
+ context: (head) => {
175
+ head.validateProps({
176
+ title: pval.refOf(pval.isString),
177
+ })
178
+
179
+ return {
180
+ ...head.props,
181
+ summary: head.props.title(),
182
+ }
183
+ },
184
+ })
185
+ ```
186
+
187
+ For object-style `:context="{ ... }"` values, validate the plain runtime shape directly:
188
+
189
+ ```ts
190
+ head.validateProps({
191
+ meta: pval.shape({
192
+ slug: pval.isString,
193
+ }),
194
+ })
195
+ ```
196
+
197
+ ### Custom validators
198
+
199
+ Users can provide their own validators as long as they match the `PropValidator<T>` signature:
200
+
201
+ ```ts
202
+ import { type PropValidator } from 'regor'
203
+
204
+ const isNonEmptyString: PropValidator<string> = (value, name) => {
205
+ if (typeof value !== 'string' || value.trim() === '') {
206
+ throw new Error(`Invalid prop "${name}": expected non-empty string.`)
207
+ }
208
+ }
209
+
210
+ head.validateProps({
211
+ title: isNonEmptyString,
212
+ })
213
+ ```
214
+
215
+ Custom validators can also use the third `head` argument:
216
+
217
+ ```ts
218
+ const startsWithPrefix: PropValidator<string> = (value, name, head) => {
219
+ const ctx = head.requireContext(AppServices)
220
+ if (typeof value !== 'string' || !value.startsWith(ctx.prefix)) {
221
+ throw new Error(`Invalid prop "${name}": expected prefixed value.`)
222
+ }
223
+ }
224
+ ```
225
+
100
226
  ## Table Templates and Components
101
227
 
102
228
  Regor preprocesses table-related templates to keep markup valid when using
@@ -223,6 +349,7 @@ These directives empower you to create dynamic and interactive user interfaces,
223
349
 
224
350
  - **`createApp`** Similar to Vue's `createApp`, it initializes a Regor application instance.
225
351
  - **`defineComponent`** Creates a Regor component instance.
352
+ - **`pval`** Built-in component prop validators used with `head.validateProps(...)`.
226
353
  - **`toFragment`** Converts a JSON template to a document fragment.
227
354
  - **`toJsonTemplate`** Converts a DOM element to a JSON template.
228
355
 
package/dist/regor.d.ts CHANGED
@@ -1,5 +1,58 @@
1
1
  // Generated by dts-bundle-generator v9.5.1
2
2
 
3
+ /**
4
+ * Assertion-style runtime validator used by `head.validateProps(...)`.
5
+ *
6
+ * A validator should throw when the value is invalid and return normally when
7
+ * the value satisfies the expected runtime contract.
8
+ *
9
+ * @typeParam TValue - Value type asserted by the validator when it succeeds.
10
+ * @param value - Raw incoming prop value.
11
+ * @param name - Prop name or nested path currently being validated.
12
+ * @param head - Current component head, useful for context-aware validation.
13
+ */
14
+ export type PropValidator<TValue = unknown> = (value: unknown, name: string, head: ComponentHead<any>) => asserts value is TValue;
15
+ export type ValidationSchemaLike = Record<string, PropValidator<any>>;
16
+ /**
17
+ * Validation schema shape suggested by `ComponentHead<T>.props`.
18
+ *
19
+ * Every key is optional so component authors can validate only the subset they
20
+ * care about. Editor completion is still driven by the known prop keys.
21
+ */
22
+ export type PropValidationSchemaFor<TProps extends object> = {
23
+ [TKey in keyof TProps]?: PropValidator<TProps[TKey]>;
24
+ };
25
+ /**
26
+ * Infers the asserted value types from a validation schema.
27
+ *
28
+ * Keys whose values are not validators are ignored.
29
+ */
30
+ export type InferPropValidationSchema<TSchema extends Record<string, unknown>> = {
31
+ [TKey in keyof TSchema as TSchema[TKey] extends PropValidator<any> ? TKey : never]: TSchema[TKey] extends PropValidator<infer TValue> ? TValue : never;
32
+ };
33
+ /**
34
+ * Built-in prop-validator namespace used with `head.validateProps(...)`.
35
+ *
36
+ * Example:
37
+ * ```ts
38
+ * head.validateProps({
39
+ * title: pval.isString,
40
+ * count: pval.optional(pval.isNumber),
41
+ * })
42
+ * ```
43
+ */
44
+ export declare const pval: {
45
+ readonly isString: PropValidator<string>;
46
+ readonly isNumber: PropValidator<number>;
47
+ readonly isBoolean: PropValidator<boolean>;
48
+ readonly isClass: <TValue extends object>(ctor: abstract new (...args: any[]) => TValue) => PropValidator<TValue>;
49
+ readonly optional: <TValue>(validator: PropValidator<TValue>) => PropValidator<TValue | undefined>;
50
+ readonly nullable: <TValue>(validator: PropValidator<TValue>) => PropValidator<TValue | null>;
51
+ readonly oneOf: <const TValue extends readonly unknown[]>(values: TValue) => PropValidator<TValue[number]>;
52
+ readonly arrayOf: <TValue>(validator: PropValidator<TValue>) => PropValidator<TValue[]>;
53
+ readonly shape: <TSchema extends ValidationSchemaLike>(schema: TSchema) => PropValidator<InferPropValidationSchema<TSchema>>;
54
+ readonly refOf: <TValue>(validator: PropValidator<TValue>) => PropValidator<AnyRef>;
55
+ };
3
56
  export type ContextClass<TValue extends object> = abstract new (...args: never[]) => TValue;
4
57
  /**
5
58
  * Runtime metadata passed to a component's `context(head)` factory.
@@ -170,6 +223,29 @@ export declare class ComponentHead<TContext extends IRegorContext | object = IRe
170
223
  * @throws Error when no matching instance exists at the requested occurrence.
171
224
  */
172
225
  requireContext<TValue extends object>(constructor: ContextClass<TValue>, occurrence?: number): TValue;
226
+ /**
227
+ * Validates selected incoming props using assertion-style validators.
228
+ *
229
+ * Only keys listed in `schema` are checked. Validation throws immediately
230
+ * on the first invalid prop and does not mutate `head.props`.
231
+ *
232
+ * The schema is keyed from `head.props`, so editor completion can suggest
233
+ * known prop names while still allowing you to validate only a subset.
234
+ *
235
+ * Validators typically come from `pval`, but custom user validators are also
236
+ * supported.
237
+ *
238
+ * Example:
239
+ * ```ts
240
+ * head.validateProps({
241
+ * title: pval.isString,
242
+ * count: pval.optional(pval.isNumber),
243
+ * })
244
+ * ```
245
+ *
246
+ * @param schema - Validators to apply to selected incoming props.
247
+ */
248
+ validateProps<TSchema extends PropValidationSchemaFor<TContext>>(schema: TSchema): asserts this is ComponentHead<TContext & InferPropValidationSchema<TSchema>>;
173
249
  /**
174
250
  * Unmounts this component instance by removing nodes between `start` and `end`
175
251
  * and calling unmount lifecycle handlers for captured contexts.
@@ -87,6 +87,7 @@ __export(index_exports, {
87
87
  onUnmounted: () => onUnmounted,
88
88
  pause: () => pause,
89
89
  persist: () => persist,
90
+ pval: () => pval,
90
91
  raw: () => raw,
91
92
  ref: () => ref,
92
93
  removeNode: () => removeNode,
@@ -418,6 +419,37 @@ var ComponentHead = class {
418
419
  `${constructor} was not found in the context stack at occurrence ${occurrence}.`
419
420
  );
420
421
  }
422
+ /**
423
+ * Validates selected incoming props using assertion-style validators.
424
+ *
425
+ * Only keys listed in `schema` are checked. Validation throws immediately
426
+ * on the first invalid prop and does not mutate `head.props`.
427
+ *
428
+ * The schema is keyed from `head.props`, so editor completion can suggest
429
+ * known prop names while still allowing you to validate only a subset.
430
+ *
431
+ * Validators typically come from `pval`, but custom user validators are also
432
+ * supported.
433
+ *
434
+ * Example:
435
+ * ```ts
436
+ * head.validateProps({
437
+ * title: pval.isString,
438
+ * count: pval.optional(pval.isNumber),
439
+ * })
440
+ * ```
441
+ *
442
+ * @param schema - Validators to apply to selected incoming props.
443
+ */
444
+ validateProps(schema) {
445
+ const props = this.props;
446
+ for (const name in schema) {
447
+ const validator = schema[name];
448
+ if (!validator) continue;
449
+ const validateProp = validator;
450
+ validateProp(props[name], name, this);
451
+ }
452
+ }
421
453
  /**
422
454
  * Unmounts this component instance by removing nodes between `start` and `end`
423
455
  * and calling unmount lifecycle handlers for captured contexts.
@@ -3003,11 +3035,11 @@ var patchAttribute = (el, key, value, previousKey) => {
3003
3035
  }
3004
3036
  return;
3005
3037
  }
3006
- const isBoolean = key in booleanAttributes;
3007
- if (isNullOrUndefined(value) || isBoolean && !includeBooleanAttr(value)) {
3038
+ const isBoolean2 = key in booleanAttributes;
3039
+ if (isNullOrUndefined(value) || isBoolean2 && !includeBooleanAttr(value)) {
3008
3040
  el.removeAttribute(key);
3009
3041
  } else {
3010
- el.setAttribute(key, isBoolean ? "" : value);
3042
+ el.setAttribute(key, isBoolean2 ? "" : value);
3011
3043
  }
3012
3044
  };
3013
3045
 
@@ -3257,7 +3289,7 @@ var decimalSeparators = /[.,' ·٫]/;
3257
3289
  var handleInputAndTextArea = (el, flags, getModelRef, parsedValue) => {
3258
3290
  const isLazy = flags.lazy;
3259
3291
  const eventType = isLazy ? "change" : "input";
3260
- const isNumber = isNumberInput(el);
3292
+ const isNumber2 = isNumberInput(el);
3261
3293
  const trimmer = () => {
3262
3294
  if (!flags.trim && !getFlags(parsedValue()[1]).trim) return;
3263
3295
  el.value = el.value.trim();
@@ -3287,7 +3319,7 @@ var handleInputAndTextArea = (el, flags, getModelRef, parsedValue) => {
3287
3319
  if (!target || target.composing) return;
3288
3320
  let value = target.value;
3289
3321
  const flags2 = getFlags(parsedValue()[1]);
3290
- if (isNumber || flags2.number || flags2.int) {
3322
+ if (isNumber2 || flags2.number || flags2.int) {
3291
3323
  if (flags2.int) {
3292
3324
  value = parseInt(value);
3293
3325
  } else {
@@ -4602,12 +4634,12 @@ var Jsep = class {
4602
4634
  this.__gobbleSpaces();
4603
4635
  let ch = this.__code;
4604
4636
  while (ch === PERIOD_CODE || ch === OBRACK_CODE || ch === OPAREN_CODE || ch === QUMARK_CODE) {
4605
- let optional;
4637
+ let optional2;
4606
4638
  if (ch === QUMARK_CODE) {
4607
4639
  if (this.__expr.charCodeAt(this.__index + 1) !== PERIOD_CODE) {
4608
4640
  break;
4609
4641
  }
4610
- optional = true;
4642
+ optional2 = true;
4611
4643
  this.__index += 2;
4612
4644
  this.__gobbleSpaces();
4613
4645
  ch = this.__code;
@@ -4633,7 +4665,7 @@ var Jsep = class {
4633
4665
  callee: node
4634
4666
  };
4635
4667
  } else {
4636
- if (optional) {
4668
+ if (optional2) {
4637
4669
  this.__index--;
4638
4670
  }
4639
4671
  this.__gobbleSpaces();
@@ -4644,7 +4676,7 @@ var Jsep = class {
4644
4676
  property: this.__gobbleIdentifier()
4645
4677
  };
4646
4678
  }
4647
- if (optional) {
4679
+ if (optional2) {
4648
4680
  node.optional = true;
4649
4681
  }
4650
4682
  this.__gobbleSpaces();
@@ -6174,6 +6206,113 @@ var defineComponent = (template, options = {}) => {
6174
6206
  };
6175
6207
  };
6176
6208
 
6209
+ // src/app/propValidators.ts
6210
+ var fail = (name, message) => {
6211
+ throw new Error(`Invalid prop "${name}": ${message}.`);
6212
+ };
6213
+ var describeValue = (value) => {
6214
+ var _a;
6215
+ if (value === null) return "null";
6216
+ if (value === void 0) return "undefined";
6217
+ if (typeof value === "string") return "string";
6218
+ if (typeof value === "number") return "number";
6219
+ if (typeof value === "boolean") return "boolean";
6220
+ if (typeof value === "bigint") return "bigint";
6221
+ if (typeof value === "symbol") return "symbol";
6222
+ if (typeof value === "function") return "function";
6223
+ if (isArray(value)) return "array";
6224
+ if (value instanceof Date) return "Date";
6225
+ if (value instanceof RegExp) return "RegExp";
6226
+ if (value instanceof Map) return "Map";
6227
+ if (value instanceof Set) return "Set";
6228
+ const ctorName = (_a = value == null ? void 0 : value.constructor) == null ? void 0 : _a.name;
6229
+ return ctorName && ctorName !== "Object" ? ctorName : "object";
6230
+ };
6231
+ var formatLiteral = (value) => {
6232
+ if (typeof value === "string") return `"${value}"`;
6233
+ if (typeof value === "number" || typeof value === "boolean") {
6234
+ return String(value);
6235
+ }
6236
+ if (value === null) return "null";
6237
+ if (value === void 0) return "undefined";
6238
+ return describeValue(value);
6239
+ };
6240
+ var isString2 = (value, name) => {
6241
+ if (typeof value !== "string") fail(name, "expected string");
6242
+ };
6243
+ var isNumber = (value, name) => {
6244
+ if (typeof value !== "number") fail(name, "expected number");
6245
+ };
6246
+ var isBoolean = (value, name) => {
6247
+ if (typeof value !== "boolean") fail(name, "expected boolean");
6248
+ };
6249
+ var isClass = (ctor) => {
6250
+ return (value, name) => {
6251
+ if (!(value instanceof ctor)) {
6252
+ fail(name, `expected instance of ${ctor.name || "provided class"}`);
6253
+ }
6254
+ };
6255
+ };
6256
+ var optional = (validator) => {
6257
+ return (value, name, head) => {
6258
+ if (value === void 0) return;
6259
+ validator(value, name, head);
6260
+ };
6261
+ };
6262
+ var nullable = (validator) => {
6263
+ return (value, name, head) => {
6264
+ if (value === null) return;
6265
+ validator(value, name, head);
6266
+ };
6267
+ };
6268
+ var oneOf = (values) => {
6269
+ return (value, name) => {
6270
+ if (values.includes(value)) return;
6271
+ fail(
6272
+ name,
6273
+ `expected one of ${values.map((x) => formatLiteral(x)).join(", ")}`
6274
+ );
6275
+ };
6276
+ };
6277
+ var arrayOf = (validator) => {
6278
+ return (value, name, head) => {
6279
+ if (!isArray(value)) fail(name, "expected array");
6280
+ const items = value;
6281
+ for (let i = 0; i < items.length; ++i) {
6282
+ validator(items[i], `${name}[${i}]`, head);
6283
+ }
6284
+ };
6285
+ };
6286
+ var shape = (schema) => {
6287
+ return (value, name, head) => {
6288
+ if (!isObject(value)) fail(name, "expected object");
6289
+ const record = value;
6290
+ for (const key in schema) {
6291
+ const validator = schema[key];
6292
+ validator(record[key], `${name}.${key}`, head);
6293
+ }
6294
+ };
6295
+ };
6296
+ var refOf = (validator) => {
6297
+ return (value, name, head) => {
6298
+ if (!isRef(value)) fail(name, "expected ref");
6299
+ const refValue = value;
6300
+ validator(refValue(), `${name}.value`, head);
6301
+ };
6302
+ };
6303
+ var pval = {
6304
+ isString: isString2,
6305
+ isNumber,
6306
+ isBoolean,
6307
+ isClass,
6308
+ optional,
6309
+ nullable,
6310
+ oneOf,
6311
+ arrayOf,
6312
+ shape,
6313
+ refOf
6314
+ };
6315
+
6177
6316
  // src/composition/ContextRegistry.ts
6178
6317
  var ContextRegistry = class {
6179
6318
  constructor() {