@orieljs/live 0.1.0-alpha.5 → 0.1.0-alpha.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/index.js CHANGED
@@ -1,15 +1,96 @@
1
1
  // Component.ts
2
- import { LiveManager, UrlGenerator, ValidationFactory } from "@orieljs/contracts";
2
+ import {
3
+ LiveManager,
4
+ UrlGenerator as UrlGenerator2,
5
+ ValidationFactory
6
+ } from "@orieljs/contracts";
3
7
  import { inject } from "@orieljs/container";
4
8
 
9
+ // errors.ts
10
+ var LiveError = class extends Error {
11
+ name = "LiveError";
12
+ kind = "Server";
13
+ status = 500;
14
+ };
15
+ var CorruptComponentPayloadError = class extends LiveError {
16
+ name = "CorruptComponentPayloadError";
17
+ kind = "Authorization";
18
+ status = 403;
19
+ constructor(component = "unknown") {
20
+ super(`Live encountered corrupt data when trying to hydrate the [${component}] component.`);
21
+ }
22
+ };
23
+ var MethodNotCallableError = class extends LiveError {
24
+ name = "MethodNotCallableError";
25
+ kind = "Authorization";
26
+ status = 403;
27
+ constructor(method, component) {
28
+ super(`Unable to call component method. Public method [${method}] not found on component [${component}].`);
29
+ }
30
+ };
31
+ var PropertyNotUpdatableError = class extends LiveError {
32
+ name = "PropertyNotUpdatableError";
33
+ kind = "Authorization";
34
+ status = 403;
35
+ constructor(path, component) {
36
+ super(`Cannot update property [${path}] on component [${component}]: it is not a public, unlocked property.`);
37
+ }
38
+ };
39
+ var LiveComponentNotFoundError = class extends LiveError {
40
+ name = "LiveComponentNotFoundError";
41
+ kind = "Http";
42
+ status = 404;
43
+ constructor(component) {
44
+ super(`Unable to find component: [${component}].`);
45
+ }
46
+ };
47
+ var InvalidLiveRequestError = class extends LiveError {
48
+ name = "InvalidLiveRequestError";
49
+ kind = "Http";
50
+ status = 400;
51
+ };
52
+ var InvalidUploadSignatureError = class extends LiveError {
53
+ name = "InvalidUploadSignatureError";
54
+ kind = "Http";
55
+ status = 401;
56
+ constructor() {
57
+ super("");
58
+ }
59
+ };
60
+ var TemporaryFileNotFoundError = class extends LiveError {
61
+ name = "TemporaryFileNotFoundError";
62
+ kind = "Http";
63
+ status = 404;
64
+ constructor(filename) {
65
+ super(`Unable to find the temporary upload [${filename}].`);
66
+ }
67
+ };
68
+ var FileNotPreviewableError = class extends LiveError {
69
+ name = "FileNotPreviewableError";
70
+ constructor(extension) {
71
+ super(`File with extension "${extension}" is not previewable. See the live.temporary_file_upload.preview_mimes config.`);
72
+ }
73
+ };
74
+ var UploadValidationError = class extends LiveError {
75
+ constructor(errors) {
76
+ super("The upload failed.");
77
+ this.errors = errors;
78
+ }
79
+ errors;
80
+ name = "UploadValidationError";
81
+ kind = "Validation";
82
+ status = 422;
83
+ errorBag = "default";
84
+ };
85
+
5
86
  // metadata.ts
6
87
  Symbol.metadata ??= /* @__PURE__ */ Symbol.for("Symbol.metadata");
7
88
  var LISTENERS = /* @__PURE__ */ Symbol.for("oriel:live.listeners");
8
89
  var LOCKED = /* @__PURE__ */ Symbol.for("oriel:live.locked");
9
90
  var COMPUTED = /* @__PURE__ */ Symbol.for("oriel:live.computed");
10
- function own(metadata, key, empty, copy) {
11
- if (!Object.hasOwn(metadata, key)) metadata[key] = metadata[key] === void 0 ? empty() : copy(metadata[key]);
12
- return metadata[key];
91
+ function own(metadata, key2, empty, copy) {
92
+ if (!Object.hasOwn(metadata, key2)) metadata[key2] = metadata[key2] === void 0 ? empty() : copy(metadata[key2]);
93
+ return metadata[key2];
13
94
  }
14
95
  function On(event) {
15
96
  return function(_method, context) {
@@ -52,8 +133,8 @@ function chainOf(cls) {
52
133
  }
53
134
  return chain;
54
135
  }
55
- function staticsOf(cls, key) {
56
- return chainOf(cls).flatMap((c) => Object.hasOwn(c, key) && c[key] !== void 0 ? [c[key]] : []);
136
+ function staticsOf(cls, key2) {
137
+ return chainOf(cls).flatMap((c) => Object.hasOwn(c, key2) && c[key2] !== void 0 ? [c[key2]] : []);
57
138
  }
58
139
  function listenersOf(cls) {
59
140
  const result = {};
@@ -75,6 +156,292 @@ function computedOf(cls) {
75
156
  return /* @__PURE__ */ new Set([...metadataOf(cls)[COMPUTED] ?? [], ...staticsOf(cls, "computed").flat()]);
76
157
  }
77
158
 
159
+ // security.ts
160
+ import { Collection as Collection2 } from "@orieljs/collections";
161
+
162
+ // synthesizers.ts
163
+ import { Collection } from "@orieljs/collections";
164
+
165
+ // TemporaryUploadedFile.ts
166
+ import { ConfigRepository, FilesystemManager, UrlGenerator } from "@orieljs/contracts";
167
+ import { currentContainer } from "@orieljs/container";
168
+ import { Str } from "@orieljs/support";
169
+ var TEMPORARY_UPLOAD_DEFAULTS = {
170
+ disk: null,
171
+ directory: "live-tmp",
172
+ rules: ["required", "file", "max:12288"],
173
+ middleware: "throttle:60,1",
174
+ preview_mimes: ["png", "gif", "bmp", "svg", "wav", "mp4", "mov", "avi", "wmv", "mp3", "m4a", "jpg", "jpeg", "mpga", "webp", "wma"],
175
+ max_upload_time: 5,
176
+ cleanup: true
177
+ };
178
+ function container() {
179
+ const current = currentContainer();
180
+ if (current === void 0) throw new LiveError("Live uploads need the application: use them inside a request, a command or a test.");
181
+ return current;
182
+ }
183
+ function uploadSetting(name) {
184
+ const config = container().bound(ConfigRepository) ? container().make(ConfigRepository) : void 0;
185
+ return config?.get(`live.temporary_file_upload.${name}`, null) ?? TEMPORARY_UPLOAD_DEFAULTS[name];
186
+ }
187
+ function temporaryDisk() {
188
+ return container().make(FilesystemManager).disk(uploadSetting("disk") ?? void 0);
189
+ }
190
+ function temporaryDirectory() {
191
+ return uploadSetting("directory").replace(/^\/+|\/+$/g, "");
192
+ }
193
+ var FILENAME = /^[A-Za-z0-9]{40}(?:\.[a-z0-9]+)?$/;
194
+ function isTemporaryFileData(value) {
195
+ if (typeof value !== "object" || value === null) return false;
196
+ const data = value;
197
+ return typeof data.filename === "string" && FILENAME.test(data.filename) && typeof data.name === "string" && typeof data.size === "number" && Number.isSafeInteger(data.size) && data.size >= 0 && typeof data.type === "string" && typeof data.extension === "string" && /^[a-z0-9]*$/.test(data.extension);
198
+ }
199
+ var HOUR = 36e5;
200
+ var TemporaryUploadedFile = class {
201
+ filename;
202
+ clientName;
203
+ mimeType;
204
+ size;
205
+ #extension;
206
+ constructor(data) {
207
+ if (!isTemporaryFileData(data)) throw new LiveError("A temporary upload needs a stored name, a name, a size, a type and an extension.");
208
+ this.filename = data.filename;
209
+ this.clientName = data.name;
210
+ this.mimeType = data.type;
211
+ this.size = data.size;
212
+ this.#extension = data.extension;
213
+ }
214
+ /** What the snapshot carries. */
215
+ toData() {
216
+ return { filename: this.filename, name: this.clientName, size: this.size, type: this.mimeType, extension: this.#extension };
217
+ }
218
+ /** The file's path on the temporary disk. */
219
+ get path() {
220
+ const directory = temporaryDirectory();
221
+ return directory === "" ? this.filename : `${directory}/${this.filename}`;
222
+ }
223
+ /** The extension of the upload's own type, as FormUploadedFile's extension(): what putFile() names a stored copy by. */
224
+ extension() {
225
+ return this.#extension;
226
+ }
227
+ /** The extension of the name the browser gave the file, lower-cased ('' without one). */
228
+ clientExtension() {
229
+ const dot = this.clientName.lastIndexOf(".");
230
+ return dot === -1 ? "" : this.clientName.slice(dot + 1).toLowerCase();
231
+ }
232
+ /** A random 40-character name with extension(), as FormUploadedFile's. */
233
+ hashName() {
234
+ const extension = this.extension();
235
+ return extension === "" ? Str.random(40) : `${Str.random(40)}.${extension}`;
236
+ }
237
+ async arrayBuffer() {
238
+ const bytes = await temporaryDisk().getBytes(this.path);
239
+ return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
240
+ }
241
+ async text() {
242
+ return new TextDecoder().decode(await temporaryDisk().getBytes(this.path));
243
+ }
244
+ /** The file's bytes from the temporary disk, opened on the first read. */
245
+ stream() {
246
+ let reader;
247
+ return new ReadableStream({
248
+ pull: async (controller) => {
249
+ reader ??= (await temporaryDisk().readStream(this.path)).getReader();
250
+ const { done, value } = await reader.read();
251
+ if (done) controller.close();
252
+ else controller.enqueue(value);
253
+ },
254
+ cancel: async (reason) => {
255
+ await reader?.cancel(reason);
256
+ }
257
+ });
258
+ }
259
+ /** Stores a copy under a random name in `directory`, on `disk` (by default the temporary disk, as Livewire's). */
260
+ store(directory, disk) {
261
+ return container().make(FilesystemManager).disk(disk ?? uploadSetting("disk") ?? void 0).putFile(directory, this);
262
+ }
263
+ storeAs(directory, name, disk) {
264
+ return container().make(FilesystemManager).disk(disk ?? uploadSetting("disk") ?? void 0).putFileAs(directory, this, name);
265
+ }
266
+ exists() {
267
+ return temporaryDisk().exists(this.path);
268
+ }
269
+ /** Removes the temporary file (a stored copy stays). */
270
+ delete() {
271
+ return temporaryDisk().delete(this.path);
272
+ }
273
+ /** Whether temporaryUrl() serves this file: its type is in `live.temporary_file_upload.preview_mimes`. */
274
+ isPreviewable() {
275
+ return uploadSetting("preview_mimes").includes(this.#extension);
276
+ }
277
+ /**
278
+ * A signed link to the file for `<img src>` or `<video src>` before it is stored. It lasts until the end of the hour
279
+ * at least 30 minutes away (Livewire's), so a re-render keeps the same link and the browser doesn't reload it.
280
+ */
281
+ temporaryUrl() {
282
+ if (!this.isPreviewable()) throw new FileNotPreviewableError(this.#extension);
283
+ const expires = new Date((Math.floor((Date.now() + 30 * 6e4) / HOUR) + 1) * HOUR - 1e3);
284
+ return container().make(UrlGenerator).temporarySignedRoute("live.preview-file", expires, { filename: this.filename }, false);
285
+ }
286
+ };
287
+
288
+ // synthesizers.ts
289
+ function isModel(value) {
290
+ if (typeof value !== "object" || value === null) return false;
291
+ const candidate = value;
292
+ return typeof candidate.getKey === "function" && typeof candidate.getKeyName === "function" && typeof candidate.getTable === "function" && typeof candidate.exists === "boolean";
293
+ }
294
+ var isPlainObject = (value) => {
295
+ const proto = Object.getPrototypeOf(value);
296
+ return proto === Object.prototype || proto === null;
297
+ };
298
+ var isTuple = (value) => Array.isArray(value) && value.length === 2 && typeof value[1] === "object" && value[1] !== null && !Array.isArray(value[1]) && typeof value[1].s === "string";
299
+ function classNameOf(registry, cls) {
300
+ return registry.nameOf(cls) ?? registry.register(cls);
301
+ }
302
+ function dehydrate(value, registry, path, seen = /* @__PURE__ */ new Set()) {
303
+ switch (typeof value) {
304
+ case "undefined":
305
+ return [null, { s: "u" }];
306
+ case "string":
307
+ case "boolean":
308
+ return value;
309
+ case "number":
310
+ if (!Number.isFinite(value)) throw new LiveError(`Property [${path}] is ${value}: Live state must be JSON (finite numbers only).`);
311
+ return value;
312
+ case "bigint":
313
+ case "symbol":
314
+ case "function":
315
+ throw new LiveError(`Property [${path}] holds a ${typeof value}: Live state may only hold primitives, arrays, plain objects, Dates, models and collections.`);
316
+ }
317
+ if (value === null) return null;
318
+ const object = value;
319
+ if (seen.has(object)) throw new LiveError(`Property [${path}] contains a circular reference.`);
320
+ seen.add(object);
321
+ try {
322
+ if (object instanceof Date) return [object.toISOString(), { s: "dt" }];
323
+ if (object instanceof TemporaryUploadedFile) return [object.toData(), { s: "fil" }];
324
+ if (Array.isArray(object)) return [object.map((item, i) => dehydrate(item, registry, `${path}.${i}`, seen)), { s: "arr" }];
325
+ if (isModel(object)) {
326
+ const name = classNameOf(registry, object.constructor);
327
+ if (!object.exists) throw new LiveError(`Property [${path}] holds an unsaved [${name}] model: only saved models can be kept between requests.`);
328
+ return [null, { s: "mdl", class: name, key: object.getKey() }];
329
+ }
330
+ if (object instanceof Collection) {
331
+ const items = object.all();
332
+ const first = items[0];
333
+ if (items.length > 0 && isModel(first) && items.every((item) => isModel(item) && item.constructor === first.constructor)) {
334
+ for (const [i, item] of items.entries()) {
335
+ if (!item.exists) throw new LiveError(`Property [${path}.${i}] holds an unsaved model.`);
336
+ }
337
+ const name = classNameOf(registry, first.constructor);
338
+ return [null, { s: "elcln", class: name, keys: items.map((item) => item.getKey()) }];
339
+ }
340
+ const meta = object.constructor === Collection ? { s: "clctn" } : { s: "clctn", class: classNameOf(registry, object.constructor) };
341
+ return [items.map((item, i) => dehydrate(item, registry, `${path}.${i}`, seen)), meta];
342
+ }
343
+ if (isPlainObject(object)) {
344
+ const out = {};
345
+ for (const [key2, item] of Object.entries(object)) out[key2] = dehydrate(item, registry, `${path}.${key2}`, seen);
346
+ return [out, { s: "obj" }];
347
+ }
348
+ const type = object.constructor?.name || "object";
349
+ throw new LiveError(
350
+ `Property [${path}] holds a [${type}] instance: Live state may only hold primitives, arrays, plain objects, Dates, models and collections.`
351
+ );
352
+ } finally {
353
+ seen.delete(object);
354
+ }
355
+ }
356
+ function modelClass(registry, name) {
357
+ if (typeof name !== "string" || !registry.has(name)) throw new LiveError(`Unable to hydrate the [${String(name)}] model: it is not registered.`);
358
+ return registry.resolve(name);
359
+ }
360
+ function collectionClass(registry, name) {
361
+ const cls = registry.has(name) ? registry.resolve(name) : void 0;
362
+ if (typeof cls !== "function" || !(cls.prototype instanceof Collection)) throw new LiveError(`Unable to hydrate the [${name}] collection: it is not registered.`);
363
+ return cls;
364
+ }
365
+ async function hydrate(value, registry) {
366
+ if (!isTuple(value)) return value;
367
+ const [inner, meta] = value;
368
+ switch (meta.s) {
369
+ case "u":
370
+ return void 0;
371
+ case "dt":
372
+ return new Date(String(inner));
373
+ case "arr":
374
+ return Promise.all(inner.map((item) => hydrate(item, registry)));
375
+ case "clctn": {
376
+ const items = await Promise.all(inner.map((item) => hydrate(item, registry)));
377
+ return meta.class === void 0 ? new Collection(items) : new (collectionClass(registry, meta.class))(items);
378
+ }
379
+ case "obj": {
380
+ const out = {};
381
+ for (const [key2, item] of Object.entries(inner)) {
382
+ Object.defineProperty(out, key2, { value: await hydrate(item, registry), enumerable: true, writable: true, configurable: true });
383
+ }
384
+ return out;
385
+ }
386
+ case "mdl":
387
+ return modelClass(registry, meta.class).findOrFail(meta.key);
388
+ case "fil":
389
+ if (!isTemporaryFileData(inner)) throw new LiveError("Unable to hydrate a temporary upload: its data is incomplete.");
390
+ return new TemporaryUploadedFile(inner);
391
+ case "elcln": {
392
+ const cls = modelClass(registry, meta.class);
393
+ const keys = meta.keys ?? [];
394
+ const model = new cls();
395
+ const keyName = model.getKeyName();
396
+ const found = [...await cls.query().whereIn(keyName, keys).get()];
397
+ const byKey = new Map(found.map((item) => [String(item.getKey()), item]));
398
+ const models = keys.map((key2) => byKey.get(String(key2))).filter((item) => item !== void 0);
399
+ return model.newCollection ? model.newCollection(models) : new Collection(models);
400
+ }
401
+ }
402
+ throw new LiveError(`Unknown snapshot value type [${String(meta.s)}].`);
403
+ }
404
+
405
+ // security.ts
406
+ var HOOKS = /* @__PURE__ */ new Set(["constructor", "render", "placeholder", "mount", "boot", "booted", "hydrate", "dehydrate", "updating", "updated", "rendering", "rendered", "exception"]);
407
+ var HOOK_PREFIX = /^(updating|updated|hydrate|dehydrate)[A-Z_]/;
408
+ var UNSAFE_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
409
+ var baseApi;
410
+ var isBaseApi = (name) => (baseApi ??= new Set(Object.getOwnPropertyNames(Component.prototype))).has(name);
411
+ var INDEX = /^(?:0|[1-9]\d*)$/;
412
+ function isArraySlot(array, segment) {
413
+ return INDEX.test(segment) && Number(segment) <= array.length;
414
+ }
415
+ function isCallable(instance, method) {
416
+ if (method === "" || method.startsWith("_") || method.startsWith("$") || method.startsWith("#")) return false;
417
+ if (HOOKS.has(method) || HOOK_PREFIX.test(method) || isBaseApi(method)) return false;
418
+ if (lockedOf(instance.constructor).has(method)) return false;
419
+ const isBase = (proto) => proto === Component.prototype || proto === Object.prototype;
420
+ for (let proto = Object.getPrototypeOf(instance); proto !== null && !isBase(proto); proto = Object.getPrototypeOf(proto)) {
421
+ const descriptor = Object.getOwnPropertyDescriptor(proto, method);
422
+ if (descriptor !== void 0) return typeof descriptor.value === "function";
423
+ }
424
+ return false;
425
+ }
426
+ function isUpdatable(instance, path) {
427
+ const segments = path.split(".");
428
+ const root = segments[0];
429
+ if (root === "" || root.startsWith("_") || segments.some((segment) => segment === "" || UNSAFE_SEGMENTS.has(segment))) return false;
430
+ if (!Object.hasOwn(instance, root)) return false;
431
+ const descriptor = Object.getOwnPropertyDescriptor(instance, root);
432
+ if (!descriptor.enumerable || descriptor.get !== void 0) return false;
433
+ const cls = instance.constructor;
434
+ if (lockedOf(cls).has(root) || computedOf(cls).has(root)) return false;
435
+ let node = descriptor.value;
436
+ for (const segment of segments.slice(1)) {
437
+ if (isModel(node) || node instanceof Collection2) return false;
438
+ if (Array.isArray(node) && !isArraySlot(node, segment)) return false;
439
+ if (typeof node !== "object" || node === null || !Object.hasOwn(node, segment)) return true;
440
+ node = node[segment];
441
+ }
442
+ return !isModel(node) && !(node instanceof Collection2);
443
+ }
444
+
78
445
  // Component.ts
79
446
  var COMPONENT = /* @__PURE__ */ Symbol.for("oriel:live.component");
80
447
  var states = /* @__PURE__ */ new WeakMap();
@@ -91,14 +458,24 @@ var ErrorBag = class {
91
458
  this.messages = messages;
92
459
  }
93
460
  messages;
94
- has(key) {
95
- return (this.messages[key]?.length ?? 0) > 0;
461
+ /** The field's own messages: a field named `constructor` or `toString` doesn't reach Object's. */
462
+ #own(key2) {
463
+ return Object.hasOwn(this.messages, key2) ? this.messages[key2] : void 0;
464
+ }
465
+ /** Adds to the component's errors, as addError() does. */
466
+ add(key2, message) {
467
+ const messages = this.#own(key2);
468
+ if (messages !== void 0) messages.push(message);
469
+ else Object.defineProperty(this.messages, key2, { value: [message], enumerable: true, writable: true, configurable: true });
96
470
  }
97
- first(key) {
98
- return key === void 0 ? this.all()[0] : this.messages[key]?.[0];
471
+ has(key2) {
472
+ return (this.#own(key2)?.length ?? 0) > 0;
99
473
  }
100
- get(key) {
101
- return [...this.messages[key] ?? []];
474
+ first(key2) {
475
+ return key2 === void 0 ? this.all()[0] : this.#own(key2)?.[0];
476
+ }
477
+ get(key2) {
478
+ return [...this.#own(key2) ?? []];
102
479
  }
103
480
  all() {
104
481
  return Object.values(this.messages).flat();
@@ -113,7 +490,7 @@ var ErrorBag = class {
113
490
  return this.all().length;
114
491
  }
115
492
  toObject() {
116
- return Object.fromEntries(Object.entries(this.messages).map(([key, messages]) => [key, [...messages]]));
493
+ return Object.fromEntries(Object.entries(this.messages).map(([key2, messages]) => [key2, [...messages]]));
117
494
  }
118
495
  };
119
496
  var PendingEvent = class {
@@ -140,6 +517,13 @@ var Component = class {
140
517
  static layout;
141
518
  /** The page title passed to the layout as `title`. */
142
519
  static title;
520
+ /**
521
+ * Livewire's #[Lazy]: every embed (and the full page) renders the placeholder first and mounts once the browser shows
522
+ * it. `<Live component={…} lazy={false} />` mounts one at once. The placeholder is the component's
523
+ * `placeholder(params)` method (TSX), else the `live.lazy_placeholder` view, else an empty `<div>`: give it the root
524
+ * element the component renders.
525
+ */
526
+ static lazy;
143
527
  /** Static fallbacks for runtimes without decorators: { event: 'method' }, locked names, computed getter names. */
144
528
  static listeners;
145
529
  static locked;
@@ -163,12 +547,18 @@ var Component = class {
163
547
  if (options.navigate === true) state.redirectUsingNavigate = true;
164
548
  else delete state.redirectUsingNavigate;
165
549
  }
166
- redirectRoute(name, parameters, options = {}) {
167
- this.redirect(inject(UrlGenerator).route(name, parameters), options);
550
+ /**
551
+ * redirect() to a named route: `this.redirectRoute('posts.show', post, { navigate: true })`. Once `oriel
552
+ * route:types` has run, the name and the route's parameters are checked, as route()'s are.
553
+ */
554
+ redirectRoute(name, ...args) {
555
+ const [parameters, options = {}] = args;
556
+ this.redirect(inject(UrlGenerator2).route(name, parameters), options);
168
557
  }
169
558
  /**
170
- * Validates the public state. On success clears the error bag and returns the validated data; on failure fills the
171
- * error bag and throws (the rest of the action is skipped and the component re-renders with `errors`).
559
+ * Validates the public state. On success clears the error bag and returns the validated data, typed from the rules
560
+ * (`await this.validate({ title: 'required|string' })` is `{ title: string }`); on failure fills the error bag and
561
+ * throws (the rest of the action is skipped and the component re-renders with `errors`).
172
562
  */
173
563
  async validate(rules, messages, attributes) {
174
564
  const validator = inject(ValidationFactory).make(Object.fromEntries(Object.entries(this)), rules, messages, attributes);
@@ -182,15 +572,14 @@ var Component = class {
182
572
  throw error;
183
573
  }
184
574
  }
185
- addError(key, message) {
186
- ;
187
- (stateOf(this).errors[key] ??= []).push(message);
575
+ addError(key2, message) {
576
+ this.errors.add(key2, message);
188
577
  }
189
578
  /** Clears every error, or only those of `keys`. */
190
579
  resetErrorBag(...keys) {
191
580
  const state = stateOf(this);
192
581
  if (keys.length === 0) state.errors = {};
193
- else for (const key of keys) delete state.errors[key];
582
+ else for (const key2 of keys) delete state.errors[key2];
194
583
  }
195
584
  get errors() {
196
585
  return new ErrorBag(stateOf(this).errors);
@@ -206,6 +595,23 @@ var Component = class {
206
595
  skipRender() {
207
596
  stateOf(this).skipRender = true;
208
597
  }
598
+ /**
599
+ * An action for an `o:` directive, typed against its method: `o:click={this.$action(this.delete, tag.id)}` writes
600
+ * `delete(5)`, and a string where `delete` takes a number is a type error. The arguments are written as JSON, the
601
+ * trailing `undefined` ones left out so the method's defaults apply. The method must be one of this component's
602
+ * actions (a public method of its class, not a hook and not @Locked); anything else throws while rendering.
603
+ */
604
+ $action(method, ...params) {
605
+ const name = typeof method === "function" ? method.name : "";
606
+ if (name === "" || this[name] !== method || !isCallable(this, name)) {
607
+ throw new LiveError(
608
+ `[${name === "" ? "anonymous function" : name}] is not an action of [${this.getName() || this.constructor.name}]: pass one of its public methods, as in this.$action(this.save).`
609
+ );
610
+ }
611
+ let count = params.length;
612
+ while (count > 0 && params[count - 1] === void 0) count--;
613
+ return count === 0 ? name : `${name}(${params.slice(0, count).map((param) => JSON.stringify(param) ?? "null").join(", ")})`;
614
+ }
209
615
  /** A full-page component as a route action: Route.get('/counter', Counter). Route parameters go to mount(). */
210
616
  async __invoke() {
211
617
  return inject(LiveManager).page(this);
@@ -214,13 +620,13 @@ var Component = class {
214
620
 
215
621
  // embed.ts
216
622
  import { LiveManager as LiveManager3 } from "@orieljs/contracts";
217
- import { inject as inject3 } from "@orieljs/container";
623
+ import { inject as inject5 } from "@orieljs/container";
218
624
 
219
625
  // Testable.ts
220
626
  import { AssertionError } from "assert";
221
627
  import { isDeepStrictEqual } from "util";
222
628
  import { LiveManager as LiveManager2, Session, token } from "@orieljs/contracts";
223
- import { Container, currentContainer, inject as inject2 } from "@orieljs/container";
629
+ import { Container, currentContainer as currentContainer2, inject as inject4 } from "@orieljs/container";
224
630
 
225
631
  // flash.ts
226
632
  function trackFlash(session, { saved = true } = {}) {
@@ -229,64 +635,23 @@ function trackFlash(session, { saved = true } = {}) {
229
635
  return (redirected) => {
230
636
  if (redirected) return;
231
637
  const flashed = session.get("_flash.new", []);
232
- const fresh = flashed.filter((key) => !before.has(key));
638
+ const fresh = flashed.filter((key2) => !before.has(key2));
233
639
  if (fresh.length === 0) return;
234
- session.put("_flash.new", flashed.filter((key) => before.has(key)));
640
+ session.put("_flash.new", flashed.filter((key2) => before.has(key2)));
235
641
  if (!saved) return session.forget(...fresh);
236
642
  const old = session.get("_flash.old", []);
237
- session.put("_flash.old", [...old, ...fresh.filter((key) => !old.includes(key))]);
643
+ session.put("_flash.old", [...old, ...fresh.filter((key2) => !old.includes(key2))]);
238
644
  };
239
645
  }
240
646
 
241
- // errors.ts
242
- var LiveError = class extends Error {
243
- name = "LiveError";
244
- kind = "Server";
245
- status = 500;
246
- };
247
- var CorruptComponentPayloadError = class extends LiveError {
248
- name = "CorruptComponentPayloadError";
249
- kind = "Authorization";
250
- status = 403;
251
- constructor(component = "unknown") {
252
- super(`Live encountered corrupt data when trying to hydrate the [${component}] component.`);
253
- }
254
- };
255
- var MethodNotCallableError = class extends LiveError {
256
- name = "MethodNotCallableError";
257
- kind = "Authorization";
258
- status = 403;
259
- constructor(method, component) {
260
- super(`Unable to call component method. Public method [${method}] not found on component [${component}].`);
261
- }
262
- };
263
- var PropertyNotUpdatableError = class extends LiveError {
264
- name = "PropertyNotUpdatableError";
265
- kind = "Authorization";
266
- status = 403;
267
- constructor(path, component) {
268
- super(`Cannot update property [${path}] on component [${component}]: it is not a public, unlocked property.`);
269
- }
270
- };
271
- var LiveComponentNotFoundError = class extends LiveError {
272
- name = "LiveComponentNotFoundError";
273
- kind = "Http";
274
- status = 404;
275
- constructor(component) {
276
- super(`Unable to find component: [${component}].`);
277
- }
278
- };
279
- var InvalidLiveRequestError = class extends LiveError {
280
- name = "InvalidLiveRequestError";
281
- kind = "Http";
282
- status = 400;
283
- };
284
-
285
647
  // html.ts
286
648
  var ESCAPES = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#039;" };
287
649
  function escapeAttribute(value) {
288
650
  return value.replace(/[&<>"']/g, (char) => ESCAPES[char]);
289
651
  }
652
+ function stripTags(html) {
653
+ return html.replace(/<[^>]*>/g, "");
654
+ }
290
655
  var VOID = /* @__PURE__ */ new Set(["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "source", "track", "wbr"]);
291
656
  var RAW_TEXT = /* @__PURE__ */ new Set(["script", "style", "textarea", "title"]);
292
657
  var TOKEN = /<\/([A-Za-z][\w:-]*)\s*>|<([A-Za-z][\w:-]*)(?:\s+[^\s"'<>/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'=<>`]+))?)*\s*(\/?)>/g;
@@ -333,6 +698,250 @@ function insertAttributesIntoRoot(html, attributes) {
333
698
  return match[1] + rendered + source.slice(match[1].length);
334
699
  }
335
700
 
701
+ // uploads.ts
702
+ import { ConfigRepository as ConfigRepository2, Encrypter, UrlGenerator as UrlGenerator3, ValidationFactory as ValidationFactory2 } from "@orieljs/contracts";
703
+ import { inject as inject2 } from "@orieljs/container";
704
+ import { FileType, Str as Str2 } from "@orieljs/support";
705
+
706
+ // checksum.ts
707
+ import { createHmac, timingSafeEqual } from "crypto";
708
+ function canonicalJson(value) {
709
+ if (Array.isArray(value)) return `[${value.map((item) => canonicalJson(item === void 0 ? null : item)).join(",")}]`;
710
+ if (typeof value === "object" && value !== null) {
711
+ const entries = Object.keys(value).sort().filter((key2) => value[key2] !== void 0).map((key2) => `${JSON.stringify(key2)}:${canonicalJson(value[key2])}`);
712
+ return `{${entries.join(",")}}`;
713
+ }
714
+ return JSON.stringify(value);
715
+ }
716
+ function checksum(key2, payload) {
717
+ return createHmac("sha256", key2).update(canonicalJson({ data: payload.data, memo: payload.memo })).digest("hex");
718
+ }
719
+ function verifyChecksum(key2, payload, given) {
720
+ if (typeof given !== "string") return false;
721
+ const expected = Buffer.from(checksum(key2, payload), "utf8");
722
+ const actual = Buffer.from(given, "utf8");
723
+ return expected.length === actual.length && timingSafeEqual(expected, actual);
724
+ }
725
+ function signatureOf(key2, purpose, payload) {
726
+ return createHmac("sha256", key2).update(`${purpose}|${payload}`).digest("base64url");
727
+ }
728
+ function signValue(key2, purpose, value) {
729
+ const payload = Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
730
+ return `${payload}.${signatureOf(key2, purpose, payload)}`;
731
+ }
732
+ function verifiedValue(key2, purpose, token2) {
733
+ if (typeof token2 !== "string") return void 0;
734
+ const dot = token2.lastIndexOf(".");
735
+ if (dot <= 0) return void 0;
736
+ const payload = token2.slice(0, dot);
737
+ const expected = Buffer.from(signatureOf(key2, purpose, payload), "utf8");
738
+ const given = Buffer.from(token2.slice(dot + 1), "utf8");
739
+ if (expected.length !== given.length || !timingSafeEqual(expected, given)) return void 0;
740
+ try {
741
+ return JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
742
+ } catch {
743
+ return void 0;
744
+ }
745
+ }
746
+
747
+ // uploads.ts
748
+ var UPLOAD_URI = "/oriel/live/upload-file";
749
+ var PREVIEW_URI = "/oriel/live/preview-file";
750
+ var REFERENCE = "live.upload";
751
+ var DAY = 24 * 36e5;
752
+ var PREVIEW_TYPES = {
753
+ png: "image/png",
754
+ gif: "image/gif",
755
+ bmp: "image/bmp",
756
+ svg: "image/svg+xml",
757
+ jpg: "image/jpeg",
758
+ jpeg: "image/jpeg",
759
+ webp: "image/webp",
760
+ avif: "image/avif",
761
+ wav: "audio/wav",
762
+ mp3: "audio/mpeg",
763
+ mpga: "audio/mpeg",
764
+ m4a: "audio/mp4",
765
+ wma: "audio/x-ms-wma",
766
+ mp4: "video/mp4",
767
+ mov: "video/quicktime",
768
+ avi: "video/x-msvideo",
769
+ wmv: "video/x-ms-wmv"
770
+ };
771
+ var key = () => inject2(Encrypter).getKey();
772
+ function temporaryFileReference(file) {
773
+ return signValue(key(), REFERENCE, file.toData());
774
+ }
775
+ function temporaryFileFromReference(reference, component) {
776
+ const data = verifiedValue(key(), REFERENCE, reference);
777
+ if (!isTemporaryFileData(data)) throw new CorruptComponentPayloadError(component);
778
+ return new TemporaryUploadedFile(data);
779
+ }
780
+ function uploadRules() {
781
+ const rules = uploadSetting("rules");
782
+ return Array.isArray(rules) ? [...rules] : rules.split("|");
783
+ }
784
+ function uploadMiddleware() {
785
+ const configured = [uploadSetting("middleware")].flat();
786
+ return configured.includes("web") ? configured : ["web", ...configured];
787
+ }
788
+ async function storeTemporaryFiles(files) {
789
+ const disk = temporaryDisk();
790
+ const directory = temporaryDirectory();
791
+ return Promise.all(
792
+ files.map(async (file) => {
793
+ const extension = file.extension();
794
+ const filename = extension === "" ? Str2.random(40) : `${Str2.random(40)}.${extension}`;
795
+ await disk.putFileAs(directory, file, filename);
796
+ return new TemporaryUploadedFile({ filename, name: file.clientName, size: file.size, type: file.mimeType, extension });
797
+ })
798
+ );
799
+ }
800
+ async function uploadedFileFrom(file) {
801
+ const detected = FileType.detectFileType(new Uint8Array(await file.slice(0, FileType.SNIFF_BYTES).arrayBuffer()));
802
+ const name = file.name ?? "";
803
+ const claimed = name.includes(".") ? name.slice(name.lastIndexOf(".") + 1).toLowerCase() : "";
804
+ const recognised = new Set(FileType.BINARY_AND_MARKUP_TYPES.flatMap(({ extensions }) => extensions));
805
+ const extension = detected !== FileType.TEXT && detected.extensions.length > 0 ? detected.extensions[0] : /^[a-z0-9]+$/.test(claimed) && !recognised.has(claimed) ? claimed : detected === FileType.TEXT ? "txt" : "";
806
+ return {
807
+ clientName: name,
808
+ mimeType: file.type === "" ? "application/octet-stream" : file.type,
809
+ size: file.size,
810
+ extension: () => extension,
811
+ clientExtension: () => claimed,
812
+ hashName: () => extension === "" ? Str2.random(40) : `${Str2.random(40)}.${extension}`,
813
+ arrayBuffer: () => file.arrayBuffer(),
814
+ text: () => file.text(),
815
+ stream: () => file.stream(),
816
+ store: () => Promise.reject(new Error("Store the temporary upload instead.")),
817
+ storeAs: () => Promise.reject(new Error("Store the temporary upload instead."))
818
+ };
819
+ }
820
+ function filesOf(request) {
821
+ const files = request.all().files;
822
+ if (files === void 0 || files === null) return [];
823
+ if (Array.isArray(files)) return files;
824
+ return typeof files === "object" && Object.getPrototypeOf(files) === Object.prototype ? Object.values(files) : [files];
825
+ }
826
+ async function handleUpload(request) {
827
+ if (!inject2(UrlGenerator3).hasValidSignature(request, false)) throw new InvalidUploadSignatureError();
828
+ const files = filesOf(request);
829
+ await inject2(ValidationFactory2).make({ files }, { "files.*": uploadRules() }).validated();
830
+ const stored = await storeTemporaryFiles(files);
831
+ return { paths: stored.map(temporaryFileReference) };
832
+ }
833
+ async function handlePreview(request, filename) {
834
+ if (!inject2(UrlGenerator3).hasValidSignature(request, false)) throw new InvalidUploadSignatureError();
835
+ const data = { filename, name: "", size: 0, type: "", extension: filename.includes(".") ? filename.slice(filename.lastIndexOf(".") + 1) : "" };
836
+ if (!isTemporaryFileData(data)) throw new TemporaryFileNotFoundError(filename);
837
+ const file = new TemporaryUploadedFile(data);
838
+ const disk = temporaryDisk();
839
+ if (!await disk.exists(file.path)) throw new TemporaryFileNotFoundError(filename);
840
+ const bytes = await disk.getBytes(file.path);
841
+ return new Response(bytes, {
842
+ headers: {
843
+ "Content-Type": PREVIEW_TYPES[data.extension] ?? "application/octet-stream",
844
+ "Content-Length": String(bytes.byteLength),
845
+ "Content-Disposition": `attachment; filename="${filename}"`,
846
+ "Content-Security-Policy": "sandbox; default-src 'none'",
847
+ "X-Content-Type-Options": "nosniff",
848
+ "Cache-Control": "private, max-age=3600"
849
+ }
850
+ });
851
+ }
852
+ async function cleanupOldUploads() {
853
+ if (uploadSetting("cleanup") === false) return;
854
+ const config = inject2(ConfigRepository2);
855
+ const disk = uploadSetting("disk") ?? config.get("filesystems.default", "local");
856
+ if (config.get(`filesystems.disks.${disk}.driver`, void 0) === "s3") return;
857
+ const storage = temporaryDisk();
858
+ const cutoff = Date.now() - DAY;
859
+ for (const path of await storage.files(temporaryDirectory())) {
860
+ try {
861
+ if ((await storage.lastModified(path)).getTime() < cutoff) await storage.delete(path);
862
+ } catch {
863
+ }
864
+ }
865
+ }
866
+
867
+ // WithFileUploads.ts
868
+ import { UrlGenerator as UrlGenerator4, ValidationFactory as ValidationFactory3 } from "@orieljs/contracts";
869
+ import { inject as inject3 } from "@orieljs/container";
870
+ var UPLOADS = /* @__PURE__ */ Symbol.for("oriel:live.uploads");
871
+ var UPLOAD_CALLS = /* @__PURE__ */ new Set(["_startUpload", "_finishUpload", "_uploadErrored", "_removeUpload"]);
872
+ function WithFileUploads(Base) {
873
+ class WithFileUploadsMixin extends Base {
874
+ }
875
+ Object.defineProperty(WithFileUploadsMixin.prototype, UPLOADS, { value: true });
876
+ return WithFileUploadsMixin;
877
+ }
878
+ function usesFileUploads(component) {
879
+ return component[UPLOADS] === true;
880
+ }
881
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
882
+ var read = (component, path) => path.split(".").reduce((node, segment) => isRecord(node) || Array.isArray(node) ? node[segment] : void 0, component);
883
+ function uploadErrors(name, errors, multiple) {
884
+ if (typeof errors === "string") {
885
+ try {
886
+ const replaced = errors.replace(multiple ? /files/gi : /files\.0/gi, name);
887
+ const bag = JSON.parse(replaced).errors;
888
+ if (isRecord(bag) && Object.values(bag).every((messages) => Array.isArray(messages) && messages.every((message) => typeof message === "string"))) {
889
+ return bag;
890
+ }
891
+ } catch {
892
+ }
893
+ }
894
+ return { [name]: [`The ${name} failed to upload.`] };
895
+ }
896
+ async function runUploadCall(component, method, params, update) {
897
+ if (!usesFileUploads(component)) {
898
+ throw new LiveError(`Cannot handle file upload without [WithFileUploads] on the [${stateOf(component).name}] component class.`);
899
+ }
900
+ const [name, value, multiple] = [String(params[0] ?? ""), params[1], params[2] === true];
901
+ switch (method) {
902
+ case "_startUpload":
903
+ component.skipRender();
904
+ return inject3(UrlGenerator4).temporarySignedRoute("live.upload-file", uploadSetting("max_upload_time") * 60, void 0, false);
905
+ case "_finishUpload": {
906
+ await cleanupOldUploads();
907
+ const files = (Array.isArray(value) ? value : []).map((reference) => temporaryFileFromReference(reference, stateOf(component).name));
908
+ const current = read(component, name);
909
+ const next = multiple ? files : Array.isArray(current) ? [...current, ...files] : files[0] ?? null;
910
+ await update(name, next);
911
+ return files.map((file) => file.filename);
912
+ }
913
+ case "_uploadErrored":
914
+ throw new UploadValidationError(uploadErrors(name, value, multiple));
915
+ case "_removeUpload": {
916
+ const filename = String(value ?? "");
917
+ const current = read(component, name);
918
+ if (Array.isArray(current)) {
919
+ const removed = current.filter((file) => file instanceof TemporaryUploadedFile && file.filename === filename);
920
+ for (const file of removed) await file.delete();
921
+ await update(
922
+ name,
923
+ current.filter((file) => !removed.includes(file))
924
+ );
925
+ } else if (current instanceof TemporaryUploadedFile && current.filename === filename) {
926
+ await current.delete();
927
+ await update(name, null);
928
+ }
929
+ return filename;
930
+ }
931
+ }
932
+ throw new LiveError(`Unknown upload call [${method}].`);
933
+ }
934
+ async function uploadForTest(files) {
935
+ try {
936
+ await inject3(ValidationFactory3).make({ files: [...files] }, { "files.*": uploadRules() }).validated();
937
+ } catch (error) {
938
+ const errors = error.errors;
939
+ if (error.kind !== "Validation" || !isRecord(errors)) throw error;
940
+ return { errors: JSON.stringify({ errors }) };
941
+ }
942
+ return { references: (await storeTemporaryFiles(files)).map(temporaryFileReference) };
943
+ }
944
+
336
945
  // Testable.ts
337
946
  var fail = (message, actual, expected) => {
338
947
  throw new AssertionError({ message, actual, expected, operator: "live" });
@@ -340,14 +949,14 @@ var fail = (message, actual, expected) => {
340
949
  var getPath = (target, path) => path.split(".").reduce((node, segment) => typeof node === "object" && node !== null ? node[segment] : void 0, target);
341
950
  var TestRequestScope = token("Testing.RequestScope");
342
951
  function requestScope() {
343
- const container = currentContainer() ?? Container.getInstance();
344
- return container?.bound(TestRequestScope) === true ? container.make(TestRequestScope) : (run) => run();
952
+ const container2 = currentContainer2() ?? Container.getInstance();
953
+ return container2?.bound(TestRequestScope) === true ? container2.make(TestRequestScope) : (run) => run();
345
954
  }
346
955
  function currentSession() {
347
- const container = currentContainer();
348
- if (container?.bound(Session) !== true) return void 0;
956
+ const container2 = currentContainer2();
957
+ if (container2?.bound(Session) !== true) return void 0;
349
958
  try {
350
- return container.make(Session);
959
+ return container2.make(Session);
351
960
  } catch {
352
961
  return void 0;
353
962
  }
@@ -367,11 +976,14 @@ var Testable = class _Testable {
367
976
  #html;
368
977
  #effects;
369
978
  #instance;
370
- /** Mounts `component` with `params`, as `<Live component={…} {...params} />` would. */
979
+ /**
980
+ * Mounts `component` with `params`, as `<Live component={…} {...params} />` would. A lazy component mounts at once
981
+ * (Livewire's withoutLazyLoading()); `{ lazy: true }` renders its placeholder, and `call('__lazyLoad', …)` loads it.
982
+ */
371
983
  static async create(component, params = {}) {
372
984
  const scope = requestScope();
373
- const manager = inject2(LiveManager2);
374
- return new _Testable(manager, scope, await scope(() => manager.mountComponent(component, params)));
985
+ const manager = inject4(LiveManager2);
986
+ return new _Testable(manager, scope, await scope(() => manager.mountComponent(component, { lazy: false, ...params })));
375
987
  }
376
988
  /** The component instance after the last request. */
377
989
  instance() {
@@ -389,9 +1001,24 @@ var Testable = class _Testable {
389
1001
  effects() {
390
1002
  return this.#effects;
391
1003
  }
1004
+ /**
1005
+ * Sets public properties through an update. A file (a FakeFile) or an array of files is uploaded as `o:model` on a
1006
+ * file input does it: the upload rules, the temporary disk, then a TemporaryUploadedFile (or an array) in the
1007
+ * property, or the upload's errors on it (Livewire's).
1008
+ */
392
1009
  async set(path, value) {
1010
+ const files = [value].flat();
1011
+ if (typeof path === "string" && files.length > 0 && files.every((file) => file instanceof Blob)) return this.upload(path, files, Array.isArray(value));
393
1012
  return this.#update(typeof path === "string" ? { [path]: value } : path, []);
394
1013
  }
1014
+ /** Uploads `files` into the property `name` (an array when `multiple`), as the browser's upload calls do. */
1015
+ async upload(name, files, multiple = false) {
1016
+ const info = files.map((file) => ({ name: file.name ?? "", size: file.size, type: file.type }));
1017
+ await this.#update({}, [{ method: "_startUpload", params: [name, info, multiple] }]);
1018
+ const outcome = await this.scope(async () => uploadForTest(await Promise.all(files.map((file) => uploadedFileFrom(file)))));
1019
+ if ("errors" in outcome) return this.#update({}, [{ method: "_uploadErrored", params: [name, outcome.errors, multiple] }]);
1020
+ return this.#update({}, [{ method: "_finishUpload", params: [name, outcome.references, multiple] }]);
1021
+ }
395
1022
  async call(method, ...params) {
396
1023
  return this.#update({}, [{ method, params }]);
397
1024
  }
@@ -441,6 +1068,17 @@ var Testable = class _Testable {
441
1068
  for (const value of values) if (!this.html().includes(value)) fail(`Failed asserting that [${value}] is in the rendered HTML.`, this.html(), value);
442
1069
  return this;
443
1070
  }
1071
+ /** Each value, HTML-escaped, appears in the rendered text: the HTML with its tags stripped (Livewire's assertSeeText). */
1072
+ assertSeeText(...values) {
1073
+ const text = stripTags(this.html());
1074
+ for (const value of values) if (!text.includes(escapeAttribute(value))) fail(`Failed asserting that [${value}] is in the rendered text.`, text, value);
1075
+ return this;
1076
+ }
1077
+ assertDontSeeText(...values) {
1078
+ const text = stripTags(this.html());
1079
+ for (const value of values) if (text.includes(escapeAttribute(value))) fail(`Failed asserting that [${value}] is not in the rendered text.`, text, value);
1080
+ return this;
1081
+ }
444
1082
  assertDispatched(name, params) {
445
1083
  const dispatched = (this.#effects.dispatches ?? []).filter((event) => event.name === name);
446
1084
  if (dispatched.length === 0) fail(`Failed asserting that an event [${name}] was dispatched.`, this.#effects.dispatches ?? [], name);
@@ -458,14 +1096,14 @@ var Testable = class _Testable {
458
1096
  const errors = stateOf(this.#instance).errors;
459
1097
  const wanted = [keys].flat();
460
1098
  if (wanted.length === 0 && Object.keys(errors).length === 0) fail("Component has no errors.", errors);
461
- for (const key of wanted) if ((errors[key]?.length ?? 0) === 0) fail(`Component missing error: ${key}`, errors, key);
1099
+ for (const key2 of wanted) if ((errors[key2]?.length ?? 0) === 0) fail(`Component missing error: ${key2}`, errors, key2);
462
1100
  return this;
463
1101
  }
464
1102
  /** No errors for the keys given (or none at all with no keys). */
465
1103
  assertHasNoErrors(keys = []) {
466
1104
  const errors = stateOf(this.#instance).errors;
467
1105
  const wanted = [keys].flat();
468
- const found = wanted.length === 0 ? Object.keys(errors).filter((key) => (errors[key]?.length ?? 0) > 0) : wanted.filter((key) => (errors[key]?.length ?? 0) > 0);
1106
+ const found = wanted.length === 0 ? Object.keys(errors).filter((key2) => (errors[key2]?.length ?? 0) > 0) : wanted.filter((key2) => (errors[key2]?.length ?? 0) > 0);
469
1107
  if (found.length > 0) fail(`Component has errors: "${found.join('", "')}"`, errors);
470
1108
  return this;
471
1109
  }
@@ -488,17 +1126,17 @@ var Testable = class _Testable {
488
1126
  };
489
1127
 
490
1128
  // embed.ts
491
- async function live(component, params = {}, key) {
492
- const html = await inject3(LiveManager3).mount(component, params, key === void 0 ? void 0 : String(key));
1129
+ async function live(component, params = {}, key2) {
1130
+ const html = await inject5(LiveManager3).mount(component, params, key2 === void 0 ? void 0 : String(key2));
493
1131
  return { toHtml: () => html };
494
1132
  }
495
- function LiveComponent({ component, "o:key": key, children: _children, ...params }) {
496
- return live(component, params, key);
1133
+ function LiveComponent({ component, "o:key": key2, children: _children, ...params }) {
1134
+ return live(component, params, key2);
497
1135
  }
498
1136
  var Live = Object.assign(LiveComponent, {
499
1137
  /** Registers a component under a name (components in app/Live are registered at boot). */
500
1138
  component(name, component) {
501
- inject3(LiveManager3).register(component, name);
1139
+ inject5(LiveManager3).register(component, name);
502
1140
  },
503
1141
  /** Livewire::test(): mounts `component` for assertions. */
504
1142
  test(component, params = {}) {
@@ -511,49 +1149,65 @@ import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
511
1149
  import { randomBytes } from "crypto";
512
1150
  import {
513
1151
  ClassRegistry,
514
- ConfigRepository,
1152
+ ConfigRepository as ConfigRepository4,
515
1153
  CurrentRoute as CurrentRoute2,
516
- Encrypter,
1154
+ Encrypter as Encrypter2,
517
1155
  HttpRequest,
518
1156
  Redirector,
519
1157
  Router,
520
1158
  Session as Session2,
521
- ViewFactory
1159
+ ViewFactory as ViewFactory2
522
1160
  } from "@orieljs/contracts";
523
- import { currentContainer as currentContainer2 } from "@orieljs/container";
524
- import { Str as Str2 } from "@orieljs/support";
1161
+ import { currentContainer as currentContainer3 } from "@orieljs/container";
1162
+ import { Str as Str4 } from "@orieljs/support";
525
1163
 
526
- // checksum.ts
527
- import { createHmac, timingSafeEqual } from "crypto";
528
- function canonicalJson(value) {
529
- if (Array.isArray(value)) return `[${value.map((item) => canonicalJson(item === void 0 ? null : item)).join(",")}]`;
530
- if (typeof value === "object" && value !== null) {
531
- const entries = Object.keys(value).sort().filter((key) => value[key] !== void 0).map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`);
532
- return `{${entries.join(",")}}`;
533
- }
534
- return JSON.stringify(value);
1164
+ // lazy.ts
1165
+ import { ConfigRepository as ConfigRepository3, ViewFactory } from "@orieljs/contracts";
1166
+ var PURPOSE = "live.lazy";
1167
+ function lazyMode(cls, params) {
1168
+ const { lazy } = params;
1169
+ if (lazy === "on-load") return "on-load";
1170
+ if (lazy === true || lazy === void 0 && cls.lazy === true) return "intersect";
1171
+ return void 0;
535
1172
  }
536
- function checksum(key, payload) {
537
- return createHmac("sha256", key).update(canonicalJson({ data: payload.data, memo: payload.memo })).digest("hex");
1173
+ async function placeholderHtml(container2, instance, params) {
1174
+ const method = instance.placeholder;
1175
+ const views = container2.make(ViewFactory);
1176
+ let output = typeof method === "function" ? await method.call(instance, params) : void 0;
1177
+ if (output === void 0) {
1178
+ const view = container2.bound(ConfigRepository3) ? container2.make(ConfigRepository3).get("live.lazy_placeholder", null) : null;
1179
+ output = view === null || view === void 0 ? "<div></div>" : views.make(view, params);
1180
+ }
1181
+ return (typeof output === "string" ? output : await views.renderToString(output)).trim();
538
1182
  }
539
- function verifyChecksum(key, payload, given) {
540
- if (typeof given !== "string") return false;
541
- const expected = Buffer.from(checksum(key, payload), "utf8");
542
- const actual = Buffer.from(given, "utf8");
543
- return expected.length === actual.length && timingSafeEqual(expected, actual);
1183
+ function lazyPayload(key2, registry, load) {
1184
+ const params = Object.fromEntries(Object.entries(load.params).map(([name, value]) => [name, dehydrate(value, registry, `${load.name}.${name}`)]));
1185
+ return signValue(key2, PURPOSE, { ...load, params });
1186
+ }
1187
+ async function lazyLoadOf(key2, registry, payload, component) {
1188
+ const load = verifiedValue(key2, PURPOSE, payload);
1189
+ if (load === void 0 || load.id !== component.id || load.name !== component.name || typeof load.tag !== "string" || typeof load.params !== "object" || load.params === null) {
1190
+ throw new CorruptComponentPayloadError(component.name);
1191
+ }
1192
+ return { params: await hydrate([load.params, { s: "obj" }], registry), tag: load.tag };
1193
+ }
1194
+ function assertSameRoot(name, placeholder, rendered) {
1195
+ if (placeholder !== rendered) {
1196
+ throw new LiveError(`The lazy component [${name}] renders a <${rendered}> where its placeholder has a <${placeholder}>: give both the same root element.`);
1197
+ }
544
1198
  }
545
1199
 
546
1200
  // naming.ts
547
1201
  import { existsSync, readdirSync } from "fs";
548
1202
  import { join, relative, sep } from "path";
549
1203
  import { pathToFileURL } from "url";
550
- import { Str } from "@orieljs/support";
1204
+ import { Str as Str3 } from "@orieljs/support";
551
1205
  function componentName(file) {
552
- return file.replace(/\.(tsx|ts|jsx|js|mts|mjs)$/, "").split(/[\\/]/).map((segment) => Str.kebab(segment)).join(".");
1206
+ return file.replace(/\.(tsx|ts|jsx|js|mts|mjs)$/, "").split(/[\\/]/).map((segment) => Str3.kebab(segment)).join(".");
553
1207
  }
554
1208
  function defaultName(cls) {
555
1209
  const name = cls.name;
556
- return typeof name === "string" && name !== "" ? Str.kebab(name) : void 0;
1210
+ return typeof name === "string" && name !== "" ? Str3.kebab(name) : void 0;
557
1211
  }
558
1212
  function componentFiles(directory) {
559
1213
  if (!existsSync(directory)) return [];
@@ -632,9 +1286,9 @@ function persistentPass(router, persistent, method, path) {
632
1286
  const seen = /* @__PURE__ */ new Set();
633
1287
  const middleware = expand(route.gatherMiddleware(), aliases, groups).filter((entry) => {
634
1288
  if (!ids.has(entry.cls)) ids.set(entry.cls, ids.size);
635
- const key = `${ids.get(entry.cls)}:${entry.parameters.join(",")}`;
636
- if (!wanted.has(entry.cls) || excluded.has(entry.cls) || seen.has(key)) return false;
637
- seen.add(key);
1289
+ const key2 = `${ids.get(entry.cls)}:${entry.parameters.join(",")}`;
1290
+ if (!wanted.has(entry.cls) || excluded.has(entry.cls) || seen.has(key2)) return false;
1291
+ seen.add(key2);
638
1292
  return true;
639
1293
  });
640
1294
  const early = new Set(expand(["web", "api"], aliases, groups).map((entry) => entry.cls));
@@ -646,38 +1300,38 @@ function persistentPass(router, persistent, method, path) {
646
1300
  function persistentMiddleware(router, persistent, method, path) {
647
1301
  return [...persistentPass(router, persistent, method, path)?.middleware ?? []];
648
1302
  }
649
- function currentRoute(container) {
650
- if (!container.bound(CurrentRoute)) return void 0;
1303
+ function currentRoute(container2) {
1304
+ if (!container2.bound(CurrentRoute)) return void 0;
651
1305
  try {
652
- return container.make(CurrentRoute);
1306
+ return container2.make(CurrentRoute);
653
1307
  } catch {
654
1308
  return void 0;
655
1309
  }
656
1310
  }
657
- async function throughMiddleware(container, request, pass, destination) {
658
- const original = currentRoute(container);
1311
+ async function throughMiddleware(container2, request, pass, destination) {
1312
+ const original = currentRoute(container2);
659
1313
  const page = { route: pass.route, parameters: { ...pass.values }, middleware: [] };
660
1314
  let swapped = false;
661
1315
  const show = () => {
662
- container.instance(CurrentRoute, page);
1316
+ container2.instance(CurrentRoute, page);
663
1317
  swapped = true;
664
1318
  };
665
1319
  const restore = () => {
666
1320
  if (!swapped) return;
667
- if (original === void 0) container.forgetInstance(CurrentRoute);
668
- else container.instance(CurrentRoute, original);
1321
+ if (original === void 0) container2.forgetInstance(CurrentRoute);
1322
+ else container2.instance(CurrentRoute, original);
669
1323
  swapped = false;
670
1324
  };
671
1325
  let ran;
672
1326
  const run = async (index) => {
673
- if (index === pass.at) Object.assign(page.parameters, await pass.router.substituteBindings(pass.route, pass.values, container));
1327
+ if (index === pass.at) Object.assign(page.parameters, await pass.router.substituteBindings(pass.route, pass.values, container2));
674
1328
  if (index === pass.middleware.length) {
675
1329
  restore();
676
1330
  ran = { result: await destination() };
677
1331
  return new Response(null, { status: 204 });
678
1332
  }
679
1333
  const { cls, parameters } = pass.middleware[index];
680
- const instance = container.make(cls);
1334
+ const instance = container2.make(cls);
681
1335
  const next = () => run(index + 1);
682
1336
  return instance.handle(request, next, ...parameters);
683
1337
  };
@@ -690,152 +1344,6 @@ async function throughMiddleware(container, request, pass, destination) {
690
1344
  }
691
1345
  }
692
1346
 
693
- // security.ts
694
- import { Collection as Collection2 } from "@orieljs/collections";
695
-
696
- // synthesizers.ts
697
- import { Collection } from "@orieljs/collections";
698
- function isModel(value) {
699
- if (typeof value !== "object" || value === null) return false;
700
- const candidate = value;
701
- return typeof candidate.getKey === "function" && typeof candidate.getKeyName === "function" && typeof candidate.getTable === "function" && typeof candidate.exists === "boolean";
702
- }
703
- var isPlainObject = (value) => {
704
- const proto = Object.getPrototypeOf(value);
705
- return proto === Object.prototype || proto === null;
706
- };
707
- var isTuple = (value) => Array.isArray(value) && value.length === 2 && typeof value[1] === "object" && value[1] !== null && !Array.isArray(value[1]) && typeof value[1].s === "string";
708
- function classNameOf(registry, cls) {
709
- return registry.nameOf(cls) ?? registry.register(cls);
710
- }
711
- function dehydrate(value, registry, path, seen = /* @__PURE__ */ new Set()) {
712
- switch (typeof value) {
713
- case "undefined":
714
- return [null, { s: "u" }];
715
- case "string":
716
- case "boolean":
717
- return value;
718
- case "number":
719
- if (!Number.isFinite(value)) throw new LiveError(`Property [${path}] is ${value}: Live state must be JSON (finite numbers only).`);
720
- return value;
721
- case "bigint":
722
- case "symbol":
723
- case "function":
724
- throw new LiveError(`Property [${path}] holds a ${typeof value}: Live state may only hold primitives, arrays, plain objects, Dates, models and collections.`);
725
- }
726
- if (value === null) return null;
727
- const object = value;
728
- if (seen.has(object)) throw new LiveError(`Property [${path}] contains a circular reference.`);
729
- seen.add(object);
730
- try {
731
- if (object instanceof Date) return [object.toISOString(), { s: "dt" }];
732
- if (Array.isArray(object)) return [object.map((item, i) => dehydrate(item, registry, `${path}.${i}`, seen)), { s: "arr" }];
733
- if (isModel(object)) {
734
- const name = classNameOf(registry, object.constructor);
735
- if (!object.exists) throw new LiveError(`Property [${path}] holds an unsaved [${name}] model: only saved models can be kept between requests.`);
736
- return [null, { s: "mdl", class: name, key: object.getKey() }];
737
- }
738
- if (object instanceof Collection) {
739
- const items = object.all();
740
- const first = items[0];
741
- if (items.length > 0 && isModel(first) && items.every((item) => isModel(item) && item.constructor === first.constructor)) {
742
- for (const [i, item] of items.entries()) {
743
- if (!item.exists) throw new LiveError(`Property [${path}.${i}] holds an unsaved model.`);
744
- }
745
- const name = classNameOf(registry, first.constructor);
746
- return [null, { s: "elcln", class: name, keys: items.map((item) => item.getKey()) }];
747
- }
748
- return [items.map((item, i) => dehydrate(item, registry, `${path}.${i}`, seen)), { s: "clctn" }];
749
- }
750
- if (isPlainObject(object)) {
751
- const out = {};
752
- for (const [key, item] of Object.entries(object)) out[key] = dehydrate(item, registry, `${path}.${key}`, seen);
753
- return [out, { s: "obj" }];
754
- }
755
- const type = object.constructor?.name || "object";
756
- throw new LiveError(
757
- `Property [${path}] holds a [${type}] instance: Live state may only hold primitives, arrays, plain objects, Dates, models and collections.`
758
- );
759
- } finally {
760
- seen.delete(object);
761
- }
762
- }
763
- function modelClass(registry, name) {
764
- if (typeof name !== "string" || !registry.has(name)) throw new LiveError(`Unable to hydrate the [${String(name)}] model: it is not registered.`);
765
- return registry.resolve(name);
766
- }
767
- async function hydrate(value, registry) {
768
- if (!isTuple(value)) return value;
769
- const [inner, meta] = value;
770
- switch (meta.s) {
771
- case "u":
772
- return void 0;
773
- case "dt":
774
- return new Date(String(inner));
775
- case "arr":
776
- return Promise.all(inner.map((item) => hydrate(item, registry)));
777
- case "clctn":
778
- return new Collection(await Promise.all(inner.map((item) => hydrate(item, registry))));
779
- case "obj": {
780
- const out = {};
781
- for (const [key, item] of Object.entries(inner)) {
782
- Object.defineProperty(out, key, { value: await hydrate(item, registry), enumerable: true, writable: true, configurable: true });
783
- }
784
- return out;
785
- }
786
- case "mdl":
787
- return modelClass(registry, meta.class).findOrFail(meta.key);
788
- case "elcln": {
789
- const cls = modelClass(registry, meta.class);
790
- const keys = meta.keys ?? [];
791
- const keyName = new cls().getKeyName();
792
- const found = [...await cls.query().whereIn(keyName, keys).get()];
793
- const byKey = new Map(found.map((model) => [String(model.getKey()), model]));
794
- return new Collection(keys.map((key) => byKey.get(String(key))).filter((model) => model !== void 0));
795
- }
796
- }
797
- throw new LiveError(`Unknown snapshot value type [${String(meta.s)}].`);
798
- }
799
-
800
- // security.ts
801
- var HOOKS = /* @__PURE__ */ new Set(["constructor", "render", "mount", "boot", "booted", "hydrate", "dehydrate", "updating", "updated", "rendering", "rendered", "exception"]);
802
- var HOOK_PREFIX = /^(updating|updated|hydrate|dehydrate)[A-Z_]/;
803
- var UNSAFE_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
804
- var BASE_API = new Set(Object.getOwnPropertyNames(Component.prototype));
805
- var INDEX = /^(?:0|[1-9]\d*)$/;
806
- function isArraySlot(array, segment) {
807
- return INDEX.test(segment) && Number(segment) <= array.length;
808
- }
809
- function isCallable(instance, method) {
810
- if (method === "" || method.startsWith("_") || method.startsWith("$") || method.startsWith("#")) return false;
811
- if (HOOKS.has(method) || HOOK_PREFIX.test(method) || BASE_API.has(method)) return false;
812
- if (lockedOf(instance.constructor).has(method)) return false;
813
- const isBase = (proto) => proto === Component.prototype || proto === Object.prototype;
814
- for (let proto = Object.getPrototypeOf(instance); proto !== null && !isBase(proto); proto = Object.getPrototypeOf(proto)) {
815
- const descriptor = Object.getOwnPropertyDescriptor(proto, method);
816
- if (descriptor !== void 0) return typeof descriptor.value === "function";
817
- }
818
- return false;
819
- }
820
- function isUpdatable(instance, path) {
821
- const segments = path.split(".");
822
- const root = segments[0];
823
- if (root === "" || root.startsWith("_") || segments.some((segment) => segment === "" || UNSAFE_SEGMENTS.has(segment))) return false;
824
- if (!Object.hasOwn(instance, root)) return false;
825
- const descriptor = Object.getOwnPropertyDescriptor(instance, root);
826
- if (!descriptor.enumerable || descriptor.get !== void 0) return false;
827
- const cls = instance.constructor;
828
- if (lockedOf(cls).has(root) || computedOf(cls).has(root)) return false;
829
- let node = descriptor.value;
830
- for (const segment of segments.slice(1)) {
831
- if (isModel(node) || node instanceof Collection2) return false;
832
- if (Array.isArray(node) && !isArraySlot(node, segment)) return false;
833
- if (typeof node !== "object" || node === null || !Object.hasOwn(node, segment)) return true;
834
- node = node[segment];
835
- }
836
- return !isModel(node) && !(node instanceof Collection2);
837
- }
838
-
839
1347
  // WithPagination.ts
840
1348
  import { AsyncLocalStorage } from "async_hooks";
841
1349
  var PAGINATED = /* @__PURE__ */ Symbol.for("oriel:live.pagination");
@@ -897,18 +1405,19 @@ var livePaginationState = {
897
1405
 
898
1406
  // LiveComponentManager.ts
899
1407
  var renderContext = new AsyncLocalStorage2();
1408
+ var loading = /* @__PURE__ */ new WeakMap();
900
1409
  var pageRequest = new AsyncLocalStorage2();
901
- function optional(container, token2) {
902
- if (!container.bound(token2)) return void 0;
1410
+ function optional(container2, token2) {
1411
+ if (!container2.bound(token2)) return void 0;
903
1412
  try {
904
- return container.make(token2);
1413
+ return container2.make(token2);
905
1414
  } catch {
906
1415
  return void 0;
907
1416
  }
908
1417
  }
909
- var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
910
- function setOwn(object, key, value) {
911
- Object.defineProperty(object, key, { value, enumerable: true, writable: true, configurable: true });
1418
+ var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1419
+ function setOwn(object, key2, value) {
1420
+ Object.defineProperty(object, key2, { value, enumerable: true, writable: true, configurable: true });
912
1421
  }
913
1422
  async function hook2(instance, name, ...args) {
914
1423
  const method = instance[name];
@@ -921,7 +1430,7 @@ var LiveComponentManager = class {
921
1430
  app;
922
1431
  /** The request scope when there is one (sessions, the current route), else the application. */
923
1432
  container() {
924
- return currentContainer2() ?? this.app;
1433
+ return currentContainer3() ?? this.app;
925
1434
  }
926
1435
  registry() {
927
1436
  return this.container().make(ClassRegistry);
@@ -952,10 +1461,10 @@ var LiveComponentManager = class {
952
1461
  return request.header("X-Oriel-Live") === "true";
953
1462
  }
954
1463
  /** The first render: HTML with o:id, o:snapshot and o:effects on the root element. */
955
- async mount(component, params = {}, key) {
1464
+ async mount(component, params = {}, key2) {
956
1465
  const context = renderContext.getStore();
957
1466
  if (context === void 0) return (await this.mountComponent(component, params)).html;
958
- const childKey = key ?? `lw-${context.index++}`;
1467
+ const childKey = key2 ?? `lw-${context.index++}`;
959
1468
  const existing = Object.hasOwn(context.previous, childKey) ? context.previous[childKey] : void 0;
960
1469
  if (existing !== void 0) {
961
1470
  setOwn(context.children, childKey, existing);
@@ -965,7 +1474,11 @@ var LiveComponentManager = class {
965
1474
  setOwn(context.children, childKey, [result.tag, result.snapshot.memo.id]);
966
1475
  return result.html;
967
1476
  }
968
- async mountComponent(component, params = {}, existing) {
1477
+ /**
1478
+ * Mounts a component: boot(), the params into its properties, mount(params), then its first render. A lazy one
1479
+ * (`lazy` in the params, or `static lazy`) renders its placeholder instead and mounts when the browser loads it.
1480
+ */
1481
+ async mountComponent(component, embedParams = {}, existing) {
969
1482
  const cls = typeof component === "string" ? this.resolve(component) : component;
970
1483
  if (!isComponentClass(cls)) throw new LiveError(`[${cls.name}] is not a Live component: it must extend Component.`);
971
1484
  const name = this.nameOf(cls);
@@ -973,7 +1486,10 @@ var LiveComponentManager = class {
973
1486
  const state = stateOf(instance);
974
1487
  state.id = randomBytes(15).toString("base64url");
975
1488
  state.name = name;
1489
+ const { lazy: _lazy, ...params } = embedParams;
1490
+ const lazy = lazyMode(cls, embedParams);
976
1491
  await hook2(instance, "boot");
1492
+ if (lazy !== void 0) return this.mountPlaceholder(instance, params, lazy);
977
1493
  for (const [property, value] of Object.entries(params)) {
978
1494
  if (Object.hasOwn(instance, property)) instance[property] = value;
979
1495
  }
@@ -990,6 +1506,30 @@ var LiveComponentManager = class {
990
1506
  const attributes = { "o:snapshot": JSON.stringify(snapshot), "o:effects": JSON.stringify(effects), "o:id": state.id };
991
1507
  return { instance, snapshot, effects, html: insertAttributesIntoRoot(html, attributes), tag };
992
1508
  }
1509
+ /**
1510
+ * A lazy component's first render (Livewire's): its placeholder, with the state it has before mount() and the mount
1511
+ * params signed for the `__lazyLoad` call the browser makes when it shows it (`o:lazy`, or `o:lazy-on-load`).
1512
+ */
1513
+ async mountPlaceholder(instance, params, lazy) {
1514
+ const state = stateOf(instance);
1515
+ const request = optional(this.container(), HttpRequest);
1516
+ const page = pageRequest.getStore() ?? { path: request?.path() ?? "", method: request?.method() ?? "GET" };
1517
+ const memo = { id: state.id, name: state.name, path: page.path, method: page.method, children: {}, errors: {} };
1518
+ const html = await placeholderHtml(this.container(), instance, params);
1519
+ const tag = rootTag(html, state.name);
1520
+ const effects = this.effects(instance);
1521
+ const listeners = Object.keys(listenersOf(instance.constructor));
1522
+ if (listeners.length > 0) effects.listeners = listeners;
1523
+ const snapshot = await this.snapshot(instance, memo);
1524
+ const payload = lazyPayload(this.key(), this.registry(), { id: state.id, name: state.name, tag, params });
1525
+ const attributes = {
1526
+ "o:snapshot": JSON.stringify(snapshot),
1527
+ "o:effects": JSON.stringify(effects),
1528
+ "o:id": state.id,
1529
+ [lazy === "on-load" ? "o:lazy-on-load" : "o:lazy"]: payload
1530
+ };
1531
+ return { instance, snapshot, effects, html: insertAttributesIntoRoot(html, attributes), tag };
1532
+ }
993
1533
  /** Applies each component's updates and calls, re-renders it and returns its new snapshot and effects. */
994
1534
  async update(request) {
995
1535
  const components = [];
@@ -1002,6 +1542,7 @@ var LiveComponentManager = class {
1002
1542
  }
1003
1543
  async updateComponent(snapshotJson, updates, calls) {
1004
1544
  const snapshot = this.verify(snapshotJson);
1545
+ const load = await this.lazyLoad(snapshot.memo, updates, calls);
1005
1546
  const cls = this.resolve(snapshot.memo.name);
1006
1547
  const instance = this.container().make(cls);
1007
1548
  const state = stateOf(instance);
@@ -1016,13 +1557,21 @@ var LiveComponentManager = class {
1016
1557
  this.prepareComputed(instance);
1017
1558
  const returns = [];
1018
1559
  await renderPaginated(instance, this.pageUrl(snapshot.memo.path), async () => {
1019
- await hook2(instance, "hydrate");
1560
+ if (load === void 0) await hook2(instance, "hydrate");
1020
1561
  try {
1562
+ if (load !== void 0) {
1563
+ for (const [property, value] of Object.entries(load.params)) {
1564
+ if (Object.hasOwn(instance, property)) instance[property] = value;
1565
+ }
1566
+ loading.set(instance, load.tag);
1567
+ await hook2(instance, "mount", load.params);
1568
+ returns.push(void 0);
1569
+ }
1021
1570
  for (const [path, value] of Object.entries(updates)) await this.applyUpdate(instance, path, value);
1022
- for (const call of calls) returns.push(await this.call(instance, call.method, call.params));
1571
+ for (const call of load === void 0 ? calls : []) returns.push(await this.call(instance, call.method, call.params));
1023
1572
  } catch (error) {
1024
1573
  const errors = error.errors;
1025
- if (error.kind !== "Validation" || !isRecord(errors)) throw error;
1574
+ if (error.kind !== "Validation" || !isRecord2(errors)) throw error;
1026
1575
  state.errors = { ...errors };
1027
1576
  }
1028
1577
  });
@@ -1033,12 +1582,24 @@ var LiveComponentManager = class {
1033
1582
  if (!state.skipRender) {
1034
1583
  ;
1035
1584
  ({ html, tag } = await this.render(instance, memo, snapshot.memo.children));
1585
+ const placeholder = loading.get(instance);
1586
+ if (placeholder !== void 0) assertSameRoot(state.name, placeholder, tag);
1036
1587
  html = insertAttributesIntoRoot(html, { "o:id": state.id });
1037
1588
  effects.html = html;
1038
1589
  }
1039
1590
  if (calls.length > 0) effects.returns = returns;
1040
1591
  return { instance, snapshot: await this.snapshot(instance, memo), effects, html, tag };
1041
1592
  }
1593
+ /**
1594
+ * The mount params and placeholder tag of a `__lazyLoad` request, or undefined for any other update. The load comes
1595
+ * alone (no updates, no other call: the browser sends it in a request of its own) and its payload must be the one
1596
+ * signed for this component: anything else is refused as corrupt (a 403) before the component boots.
1597
+ */
1598
+ async lazyLoad(memo, updates, calls) {
1599
+ if (!calls.some((call) => call.method === "__lazyLoad")) return void 0;
1600
+ if (calls.length !== 1 || Object.keys(updates).length > 0) throw new CorruptComponentPayloadError(memo.name);
1601
+ return lazyLoadOf(this.key(), this.registry(), calls[0].params[0], { id: memo.id, name: memo.name });
1602
+ }
1042
1603
  /**
1043
1604
  * The route action of POST /oriel/live/update. Each component's update runs behind the persistent middleware of the
1044
1605
  * route that rendered its page (Livewire's): `auth`, `verified`, `guest` and `can`, with the page route's parameters
@@ -1048,17 +1609,17 @@ var LiveComponentManager = class {
1048
1609
  */
1049
1610
  async handle(request) {
1050
1611
  if (!this.isLiveRequest(request)) throw new InvalidLiveRequestError("Live update requests need the X-Oriel-Live header.");
1051
- const container = this.container();
1052
- const persistent = optional(container, ConfigRepository)?.get("live.persistent_middleware", PERSISTENT_MIDDLEWARE) ?? PERSISTENT_MIDDLEWARE;
1612
+ const container2 = this.container();
1613
+ const persistent = optional(container2, ConfigRepository4)?.get("live.persistent_middleware", PERSISTENT_MIDDLEWARE) ?? PERSISTENT_MIDDLEWARE;
1053
1614
  const components = [];
1054
- const flashed = trackFlash(optional(container, Session2));
1615
+ const flashed = trackFlash(optional(container2, Session2));
1055
1616
  for (const component of this.payload(request).components) {
1056
1617
  const { memo } = this.verify(component.snapshot);
1057
- const pass = persistentPass(container.make(Router), persistent, memo.method, memo.path);
1618
+ const pass = persistentPass(container2.make(Router), persistent, memo.method, memo.path);
1058
1619
  const update = () => this.updateComponent(component.snapshot, component.updates, component.calls);
1059
1620
  const result = await pageRequest.run(
1060
1621
  { path: memo.path, method: memo.method },
1061
- () => pass === void 0 || pass.middleware.length === 0 ? update() : throughMiddleware(container, request, pass, update)
1622
+ () => pass === void 0 || pass.middleware.length === 0 ? update() : throughMiddleware(container2, request, pass, update)
1062
1623
  );
1063
1624
  if (result instanceof Response) return result;
1064
1625
  components.push({ snapshot: JSON.stringify(result.snapshot), effects: result.effects });
@@ -1068,19 +1629,19 @@ var LiveComponentManager = class {
1068
1629
  }
1069
1630
  /** A full-page component (Route.get('/counter', Counter)): mounted with the route parameters, inside the layout. */
1070
1631
  async page(component) {
1071
- const container = this.container();
1072
- const params = { ...optional(container, CurrentRoute2)?.parameters ?? {} };
1632
+ const container2 = this.container();
1633
+ const params = { ...optional(container2, CurrentRoute2)?.parameters ?? {} };
1073
1634
  const cls = component.constructor;
1074
1635
  const result = await this.mountComponent(cls, params, component);
1075
1636
  const redirect = result.effects.redirect;
1076
1637
  if (redirect !== void 0) {
1077
- return optional(container, Redirector)?.to(redirect) ?? new Response(null, { status: 302, headers: { Location: redirect } });
1638
+ return optional(container2, Redirector)?.to(redirect) ?? new Response(null, { status: 302, headers: { Location: redirect } });
1078
1639
  }
1079
- const layout = cls.layout ?? optional(container, ConfigRepository)?.get("live.layout") ?? "components.layouts.app";
1640
+ const layout = cls.layout ?? optional(container2, ConfigRepository4)?.get("live.layout") ?? "components.layouts.app";
1080
1641
  const html = result.html;
1081
1642
  const data = { children: { toHtml: () => html } };
1082
1643
  if (cls.title !== void 0) data.title = cls.title;
1083
- return container.make(ViewFactory).make(layout, data);
1644
+ return container2.make(ViewFactory2).make(layout, data);
1084
1645
  }
1085
1646
  /** Parses and checks a snapshot: a bad checksum throws before anything is hydrated. */
1086
1647
  verify(snapshotJson) {
@@ -1090,13 +1651,13 @@ var LiveComponentManager = class {
1090
1651
  } catch {
1091
1652
  throw new CorruptComponentPayloadError();
1092
1653
  }
1093
- if (!isRecord(snapshot) || !isRecord(snapshot.data) || !isRecord(snapshot.memo)) throw new CorruptComponentPayloadError();
1654
+ if (!isRecord2(snapshot) || !isRecord2(snapshot.data) || !isRecord2(snapshot.memo)) throw new CorruptComponentPayloadError();
1094
1655
  const memo = snapshot.memo;
1095
1656
  const name = typeof memo.name === "string" ? memo.name : "unknown";
1096
- const key = this.key();
1657
+ const key2 = this.key();
1097
1658
  let valid;
1098
1659
  try {
1099
- valid = verifyChecksum(key, { data: snapshot.data, memo: snapshot.memo }, snapshot.checksum);
1660
+ valid = verifyChecksum(key2, { data: snapshot.data, memo: snapshot.memo }, snapshot.checksum);
1100
1661
  } catch {
1101
1662
  valid = false;
1102
1663
  }
@@ -1104,7 +1665,7 @@ var LiveComponentManager = class {
1104
1665
  return snapshot;
1105
1666
  }
1106
1667
  key() {
1107
- return this.container().make(Encrypter).getKey();
1668
+ return this.container().make(Encrypter2).getKey();
1108
1669
  }
1109
1670
  /** The request body, read from the raw JSON when the request has it, so TrimStrings can't alter typed values. */
1110
1671
  payload(request) {
@@ -1115,9 +1676,9 @@ var LiveComponentManager = class {
1115
1676
  } catch {
1116
1677
  throw new InvalidLiveRequestError("The Live update request body is not valid JSON.");
1117
1678
  }
1118
- const components = isRecord(body) ? body.components : void 0;
1679
+ const components = isRecord2(body) ? body.components : void 0;
1119
1680
  const valid = Array.isArray(components) && components.every(
1120
- (component) => isRecord(component) && typeof component.snapshot === "string" && isRecord(component.updates) && Array.isArray(component.calls) && component.calls.every((call) => isRecord(call) && typeof call.method === "string" && Array.isArray(call.params))
1681
+ (component) => isRecord2(component) && typeof component.snapshot === "string" && isRecord2(component.updates) && Array.isArray(component.calls) && component.calls.every((call) => isRecord2(call) && typeof call.method === "string" && Array.isArray(call.params))
1121
1682
  );
1122
1683
  if (!valid) throw new InvalidLiveRequestError("The Live update request body is malformed.");
1123
1684
  return body;
@@ -1136,10 +1697,10 @@ var LiveComponentManager = class {
1136
1697
  async applyUpdate(instance, path, value) {
1137
1698
  if (!isUpdatable(instance, path)) throw new PropertyNotUpdatableError(path, stateOf(instance).name);
1138
1699
  const [root, ...rest] = path.split(".");
1139
- const key = rest.length > 0 ? rest.join(".") : void 0;
1140
- const studly = Str2.studly(root);
1700
+ const key2 = rest.length > 0 ? rest.join(".") : void 0;
1701
+ const studly = Str4.studly(root);
1141
1702
  await hook2(instance, "updating", path, value);
1142
- await hook2(instance, `updating${studly}`, value, key);
1703
+ await hook2(instance, `updating${studly}`, value, key2);
1143
1704
  const target = instance;
1144
1705
  if (rest.length === 0) target[root] = value;
1145
1706
  else {
@@ -1147,20 +1708,20 @@ var LiveComponentManager = class {
1147
1708
  if (Array.isArray(node2) && !isArraySlot(node2, segment)) throw new PropertyNotUpdatableError(path, stateOf(instance).name);
1148
1709
  };
1149
1710
  let node = target[root];
1150
- if (!isRecord(node) && !Array.isArray(node)) target[root] = node = {};
1711
+ if (!isRecord2(node) && !Array.isArray(node)) target[root] = node = {};
1151
1712
  for (const segment of rest.slice(0, -1)) {
1152
1713
  assertSlot(node, segment);
1153
1714
  const next = node[segment];
1154
- node = isRecord(next) || Array.isArray(next) ? next : node[segment] = {};
1715
+ node = isRecord2(next) || Array.isArray(next) ? next : node[segment] = {};
1155
1716
  }
1156
1717
  const last = rest.at(-1);
1157
1718
  assertSlot(node, last);
1158
1719
  Object.defineProperty(node, last, { value, enumerable: true, writable: true, configurable: true });
1159
1720
  }
1160
1721
  await hook2(instance, "updated", path, value);
1161
- await hook2(instance, `updated${studly}`, value, key);
1722
+ await hook2(instance, `updated${studly}`, value, key2);
1162
1723
  }
1163
- /** Runs one call from the browser: a magic action, an event, or a public method. Returns its return value. */
1724
+ /** Runs one call from the browser: a magic action, an event, an upload call, or a public method. Returns its return value. */
1164
1725
  async call(instance, method, params) {
1165
1726
  const name = stateOf(instance).name;
1166
1727
  switch (method) {
@@ -1170,7 +1731,7 @@ var LiveComponentManager = class {
1170
1731
  return this.applyUpdate(instance, String(params[0]), params[1]);
1171
1732
  case "$toggle": {
1172
1733
  const path = String(params[0]);
1173
- const current = path.split(".").reduce((node, segment) => isRecord(node) || Array.isArray(node) ? node[segment] : void 0, instance);
1734
+ const current = path.split(".").reduce((node, segment) => isRecord2(node) || Array.isArray(node) ? node[segment] : void 0, instance);
1174
1735
  return this.applyUpdate(instance, path, !current);
1175
1736
  }
1176
1737
  case "__dispatch": {
@@ -1178,11 +1739,12 @@ var LiveComponentManager = class {
1178
1739
  const listeners = listenersOf(instance.constructor);
1179
1740
  for (const listener of Object.hasOwn(listeners, String(event)) ? listeners[String(event)] : []) {
1180
1741
  if (!isCallable(instance, listener)) throw new MethodNotCallableError(listener, name);
1181
- await instance[listener](isRecord(detail) ? detail : {});
1742
+ await instance[listener](isRecord2(detail) ? detail : {});
1182
1743
  }
1183
1744
  return void 0;
1184
1745
  }
1185
1746
  }
1747
+ if (UPLOAD_CALLS.has(method)) return runUploadCall(instance, method, params, (path, value) => this.applyUpdate(instance, path, value));
1186
1748
  if (!isCallable(instance, method)) throw new MethodNotCallableError(method, name);
1187
1749
  return instance[method](...params);
1188
1750
  }
@@ -1198,7 +1760,7 @@ var LiveComponentManager = class {
1198
1760
  context,
1199
1761
  () => renderPaginated(instance, this.pageUrl(memo.path), async () => {
1200
1762
  const output = await instance.render();
1201
- return typeof output === "string" ? output : this.container().make(ViewFactory).renderToString(output);
1763
+ return typeof output === "string" ? output : this.container().make(ViewFactory2).renderToString(output);
1202
1764
  })
1203
1765
  );
1204
1766
  await hook2(instance, "rendered", html);
@@ -1243,16 +1805,16 @@ var LiveComponentManager = class {
1243
1805
  };
1244
1806
 
1245
1807
  // LiveServiceProvider.ts
1246
- import { ConfigRepository as ConfigRepository3, ConsoleKernel, HttpRequest as HttpRequest2, LiveManager as LiveManager4, PaginationState, Router as Router2 } from "@orieljs/contracts";
1247
- import { inject as inject4 } from "@orieljs/container";
1808
+ import { ConfigRepository as ConfigRepository6, ConsoleKernel, HttpRequest as HttpRequest2, LiveManager as LiveManager4, PaginationState, Router as Router2 } from "@orieljs/contracts";
1809
+ import { inject as inject6 } from "@orieljs/container";
1248
1810
  import { ServiceProvider } from "@orieljs/support";
1249
1811
 
1250
1812
  // commands.ts
1251
1813
  import { existsSync as existsSync2 } from "fs";
1252
1814
  import { mkdir, writeFile } from "fs/promises";
1253
1815
  import { dirname, join as join2, relative as relative2, sep as sep2 } from "path";
1254
- import { ConfigRepository as ConfigRepository2 } from "@orieljs/contracts";
1255
- import { Str as Str3 } from "@orieljs/support";
1816
+ import { ConfigRepository as ConfigRepository5 } from "@orieljs/contracts";
1817
+ import { Str as Str5 } from "@orieljs/support";
1256
1818
  var COMPONENT_STUB = `import { Component } from '@orieljs/framework'
1257
1819
 
1258
1820
  export default class {{ class }} extends Component {
@@ -1280,13 +1842,13 @@ function importPath(from, to) {
1280
1842
  }
1281
1843
  function registerLiveCommands(kernel, app) {
1282
1844
  kernel.command("make:live {name : The component name, e.g. Posts/CreatePost} {--test : Also create a test} {--force : Overwrite existing files}", async function() {
1283
- const segments = String(this.argument("name") ?? "").split(/[/\\.]+/).filter((segment) => segment !== "").map((segment) => Str3.studly(segment));
1845
+ const segments = String(this.argument("name") ?? "").split(/[/\\.]+/).filter((segment) => segment !== "").map((segment) => Str5.studly(segment));
1284
1846
  if (segments.length === 0 || !segments.every((segment) => CLASS_NAME.test(segment))) {
1285
1847
  this.error(`Component name [${String(this.argument("name"))}] is not valid: use names like Posts/CreatePost.`);
1286
1848
  return 1;
1287
1849
  }
1288
1850
  const className = segments.at(-1);
1289
- const config = app.bound(ConfigRepository2) ? app.make(ConfigRepository2) : void 0;
1851
+ const config = app.bound(ConfigRepository5) ? app.make(ConfigRepository5) : void 0;
1290
1852
  const directory = config?.get("live.class_path") ?? app.appPath("Live");
1291
1853
  const file = join2(directory, ...segments.slice(0, -1), `${className}.tsx`);
1292
1854
  const force = this.option("force") === true;
@@ -1303,7 +1865,7 @@ function registerLiveCommands(kernel, app) {
1303
1865
  this.error(`Test [${relative2(app.basePath(), testFile)}] already exists.`);
1304
1866
  return 1;
1305
1867
  }
1306
- const name = segments.map((segment) => Str3.kebab(segment)).join(".");
1868
+ const name = segments.map((segment) => Str5.kebab(segment)).join(".");
1307
1869
  const test = TEST_STUB.replaceAll("{{ class }}", () => className).replace("{{ component }}", () => importPath(testFile, file)).replace("{{ testCase }}", () => importPath(testFile, app.basePath(join2("tests", "TestCase.ts")))).replace("{{ name }}", () => name);
1308
1870
  await mkdir(dirname(testFile), { recursive: true });
1309
1871
  await writeFile(testFile, test);
@@ -1319,16 +1881,19 @@ var LiveServiceProvider = class extends ServiceProvider {
1319
1881
  register() {
1320
1882
  this.mergeConfigFrom({ class_path: this.app.appPath("Live"), layout: "components.layouts.app", persistent_middleware: [...PERSISTENT_MIDDLEWARE] }, "live");
1321
1883
  this.app.singleton(LiveComponentManager, () => new LiveComponentManager(this.app));
1322
- this.app.singleton(LiveManager4, (container) => container.make(LiveComponentManager));
1884
+ this.app.singleton(LiveManager4, (container2) => container2.make(LiveComponentManager));
1323
1885
  this.app.singleton(PaginationState, () => livePaginationState);
1324
1886
  }
1325
1887
  async boot() {
1326
1888
  const manager = this.app.make(LiveComponentManager);
1327
1889
  if (this.app.bound(Router2)) {
1328
- this.app.make(Router2).post(UPDATE_URI, () => manager.handle(inject4(HttpRequest2))).middleware("web").name("live.update");
1890
+ const router = this.app.make(Router2);
1891
+ router.post(UPDATE_URI, () => manager.handle(inject6(HttpRequest2))).middleware("web").name("live.update");
1892
+ router.post(UPLOAD_URI, () => handleUpload(inject6(HttpRequest2))).middleware(uploadMiddleware()).name("live.upload-file");
1893
+ router.get(`${PREVIEW_URI}/{filename}`, (filename) => handlePreview(inject6(HttpRequest2), filename)).middleware("web").name("live.preview-file");
1329
1894
  }
1330
1895
  if (this.app.bound(ConsoleKernel)) registerLiveCommands(this.app.make(ConsoleKernel), this.app);
1331
- const directory = this.app.make(ConfigRepository3).get("live.class_path", this.app.appPath("Live"));
1896
+ const directory = this.app.make(ConfigRepository6).get("live.class_path", this.app.appPath("Live"));
1332
1897
  for (const [name, exported] of await this.app.run(() => discover(directory))) {
1333
1898
  if (isComponentClass(exported)) manager.register(exported, name);
1334
1899
  }
@@ -1375,7 +1940,9 @@ export {
1375
1940
  Computed,
1376
1941
  CorruptComponentPayloadError,
1377
1942
  ErrorBag,
1943
+ FileNotPreviewableError,
1378
1944
  InvalidLiveRequestError,
1945
+ InvalidUploadSignatureError,
1379
1946
  Live,
1380
1947
  LiveComponentManager,
1381
1948
  LiveComponentNotFoundError,
@@ -1385,12 +1952,19 @@ export {
1385
1952
  MethodNotCallableError,
1386
1953
  On,
1387
1954
  PERSISTENT_MIDDLEWARE,
1955
+ PREVIEW_URI,
1388
1956
  Pagination,
1389
1957
  PendingEvent,
1390
1958
  PropertyNotUpdatableError,
1959
+ TEMPORARY_UPLOAD_DEFAULTS,
1960
+ TemporaryFileNotFoundError,
1961
+ TemporaryUploadedFile,
1391
1962
  TestRequestScope,
1392
1963
  Testable,
1393
1964
  UPDATE_URI,
1965
+ UPLOAD_URI,
1966
+ UploadValidationError,
1967
+ WithFileUploads,
1394
1968
  WithPagination,
1395
1969
  componentName,
1396
1970
  live,