@orieljs/live 0.1.0-alpha.6 → 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,5 +1,9 @@
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
 
5
9
  // errors.ts
@@ -45,15 +49,48 @@ var InvalidLiveRequestError = class extends LiveError {
45
49
  kind = "Http";
46
50
  status = 400;
47
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
+ };
48
85
 
49
86
  // metadata.ts
50
87
  Symbol.metadata ??= /* @__PURE__ */ Symbol.for("Symbol.metadata");
51
88
  var LISTENERS = /* @__PURE__ */ Symbol.for("oriel:live.listeners");
52
89
  var LOCKED = /* @__PURE__ */ Symbol.for("oriel:live.locked");
53
90
  var COMPUTED = /* @__PURE__ */ Symbol.for("oriel:live.computed");
54
- function own(metadata, key, empty, copy) {
55
- if (!Object.hasOwn(metadata, key)) metadata[key] = metadata[key] === void 0 ? empty() : copy(metadata[key]);
56
- 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];
57
94
  }
58
95
  function On(event) {
59
96
  return function(_method, context) {
@@ -96,8 +133,8 @@ function chainOf(cls) {
96
133
  }
97
134
  return chain;
98
135
  }
99
- function staticsOf(cls, key) {
100
- 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]] : []);
101
138
  }
102
139
  function listenersOf(cls) {
103
140
  const result = {};
@@ -124,6 +161,131 @@ import { Collection as Collection2 } from "@orieljs/collections";
124
161
 
125
162
  // synthesizers.ts
126
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
127
289
  function isModel(value) {
128
290
  if (typeof value !== "object" || value === null) return false;
129
291
  const candidate = value;
@@ -158,6 +320,7 @@ function dehydrate(value, registry, path, seen = /* @__PURE__ */ new Set()) {
158
320
  seen.add(object);
159
321
  try {
160
322
  if (object instanceof Date) return [object.toISOString(), { s: "dt" }];
323
+ if (object instanceof TemporaryUploadedFile) return [object.toData(), { s: "fil" }];
161
324
  if (Array.isArray(object)) return [object.map((item, i) => dehydrate(item, registry, `${path}.${i}`, seen)), { s: "arr" }];
162
325
  if (isModel(object)) {
163
326
  const name = classNameOf(registry, object.constructor);
@@ -174,11 +337,12 @@ function dehydrate(value, registry, path, seen = /* @__PURE__ */ new Set()) {
174
337
  const name = classNameOf(registry, first.constructor);
175
338
  return [null, { s: "elcln", class: name, keys: items.map((item) => item.getKey()) }];
176
339
  }
177
- return [items.map((item, i) => dehydrate(item, registry, `${path}.${i}`, seen)), { s: "clctn" }];
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];
178
342
  }
179
343
  if (isPlainObject(object)) {
180
344
  const out = {};
181
- for (const [key, item] of Object.entries(object)) out[key] = dehydrate(item, registry, `${path}.${key}`, seen);
345
+ for (const [key2, item] of Object.entries(object)) out[key2] = dehydrate(item, registry, `${path}.${key2}`, seen);
182
346
  return [out, { s: "obj" }];
183
347
  }
184
348
  const type = object.constructor?.name || "object";
@@ -193,6 +357,11 @@ function modelClass(registry, name) {
193
357
  if (typeof name !== "string" || !registry.has(name)) throw new LiveError(`Unable to hydrate the [${String(name)}] model: it is not registered.`);
194
358
  return registry.resolve(name);
195
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
+ }
196
365
  async function hydrate(value, registry) {
197
366
  if (!isTuple(value)) return value;
198
367
  const [inner, meta] = value;
@@ -203,31 +372,38 @@ async function hydrate(value, registry) {
203
372
  return new Date(String(inner));
204
373
  case "arr":
205
374
  return Promise.all(inner.map((item) => hydrate(item, registry)));
206
- case "clctn":
207
- return new Collection(await 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
+ }
208
379
  case "obj": {
209
380
  const out = {};
210
- for (const [key, item] of Object.entries(inner)) {
211
- Object.defineProperty(out, key, { value: await hydrate(item, registry), enumerable: true, writable: true, configurable: true });
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 });
212
383
  }
213
384
  return out;
214
385
  }
215
386
  case "mdl":
216
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);
217
391
  case "elcln": {
218
392
  const cls = modelClass(registry, meta.class);
219
393
  const keys = meta.keys ?? [];
220
- const keyName = new cls().getKeyName();
394
+ const model = new cls();
395
+ const keyName = model.getKeyName();
221
396
  const found = [...await cls.query().whereIn(keyName, keys).get()];
222
- const byKey = new Map(found.map((model) => [String(model.getKey()), model]));
223
- return new Collection(keys.map((key) => byKey.get(String(key))).filter((model) => model !== void 0));
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);
224
400
  }
225
401
  }
226
402
  throw new LiveError(`Unknown snapshot value type [${String(meta.s)}].`);
227
403
  }
228
404
 
229
405
  // security.ts
230
- var HOOKS = /* @__PURE__ */ new Set(["constructor", "render", "mount", "boot", "booted", "hydrate", "dehydrate", "updating", "updated", "rendering", "rendered", "exception"]);
406
+ var HOOKS = /* @__PURE__ */ new Set(["constructor", "render", "placeholder", "mount", "boot", "booted", "hydrate", "dehydrate", "updating", "updated", "rendering", "rendered", "exception"]);
231
407
  var HOOK_PREFIX = /^(updating|updated|hydrate|dehydrate)[A-Z_]/;
232
408
  var UNSAFE_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
233
409
  var baseApi;
@@ -282,14 +458,24 @@ var ErrorBag = class {
282
458
  this.messages = messages;
283
459
  }
284
460
  messages;
285
- has(key) {
286
- 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;
287
464
  }
288
- first(key) {
289
- return key === void 0 ? this.all()[0] : this.messages[key]?.[0];
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 });
290
470
  }
291
- get(key) {
292
- return [...this.messages[key] ?? []];
471
+ has(key2) {
472
+ return (this.#own(key2)?.length ?? 0) > 0;
473
+ }
474
+ first(key2) {
475
+ return key2 === void 0 ? this.all()[0] : this.#own(key2)?.[0];
476
+ }
477
+ get(key2) {
478
+ return [...this.#own(key2) ?? []];
293
479
  }
294
480
  all() {
295
481
  return Object.values(this.messages).flat();
@@ -304,7 +490,7 @@ var ErrorBag = class {
304
490
  return this.all().length;
305
491
  }
306
492
  toObject() {
307
- 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]]));
308
494
  }
309
495
  };
310
496
  var PendingEvent = class {
@@ -331,6 +517,13 @@ var Component = class {
331
517
  static layout;
332
518
  /** The page title passed to the layout as `title`. */
333
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;
334
527
  /** Static fallbacks for runtimes without decorators: { event: 'method' }, locked names, computed getter names. */
335
528
  static listeners;
336
529
  static locked;
@@ -354,12 +547,18 @@ var Component = class {
354
547
  if (options.navigate === true) state.redirectUsingNavigate = true;
355
548
  else delete state.redirectUsingNavigate;
356
549
  }
357
- redirectRoute(name, parameters, options = {}) {
358
- 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);
359
557
  }
360
558
  /**
361
- * Validates the public state. On success clears the error bag and returns the validated data; on failure fills the
362
- * 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`).
363
562
  */
364
563
  async validate(rules, messages, attributes) {
365
564
  const validator = inject(ValidationFactory).make(Object.fromEntries(Object.entries(this)), rules, messages, attributes);
@@ -373,15 +572,14 @@ var Component = class {
373
572
  throw error;
374
573
  }
375
574
  }
376
- addError(key, message) {
377
- ;
378
- (stateOf(this).errors[key] ??= []).push(message);
575
+ addError(key2, message) {
576
+ this.errors.add(key2, message);
379
577
  }
380
578
  /** Clears every error, or only those of `keys`. */
381
579
  resetErrorBag(...keys) {
382
580
  const state = stateOf(this);
383
581
  if (keys.length === 0) state.errors = {};
384
- else for (const key of keys) delete state.errors[key];
582
+ else for (const key2 of keys) delete state.errors[key2];
385
583
  }
386
584
  get errors() {
387
585
  return new ErrorBag(stateOf(this).errors);
@@ -422,13 +620,13 @@ var Component = class {
422
620
 
423
621
  // embed.ts
424
622
  import { LiveManager as LiveManager3 } from "@orieljs/contracts";
425
- import { inject as inject3 } from "@orieljs/container";
623
+ import { inject as inject5 } from "@orieljs/container";
426
624
 
427
625
  // Testable.ts
428
626
  import { AssertionError } from "assert";
429
627
  import { isDeepStrictEqual } from "util";
430
628
  import { LiveManager as LiveManager2, Session, token } from "@orieljs/contracts";
431
- import { Container, currentContainer, inject as inject2 } from "@orieljs/container";
629
+ import { Container, currentContainer as currentContainer2, inject as inject4 } from "@orieljs/container";
432
630
 
433
631
  // flash.ts
434
632
  function trackFlash(session, { saved = true } = {}) {
@@ -437,12 +635,12 @@ function trackFlash(session, { saved = true } = {}) {
437
635
  return (redirected) => {
438
636
  if (redirected) return;
439
637
  const flashed = session.get("_flash.new", []);
440
- const fresh = flashed.filter((key) => !before.has(key));
638
+ const fresh = flashed.filter((key2) => !before.has(key2));
441
639
  if (fresh.length === 0) return;
442
- session.put("_flash.new", flashed.filter((key) => before.has(key)));
640
+ session.put("_flash.new", flashed.filter((key2) => before.has(key2)));
443
641
  if (!saved) return session.forget(...fresh);
444
642
  const old = session.get("_flash.old", []);
445
- session.put("_flash.old", [...old, ...fresh.filter((key) => !old.includes(key))]);
643
+ session.put("_flash.old", [...old, ...fresh.filter((key2) => !old.includes(key2))]);
446
644
  };
447
645
  }
448
646
 
@@ -500,6 +698,250 @@ function insertAttributesIntoRoot(html, attributes) {
500
698
  return match[1] + rendered + source.slice(match[1].length);
501
699
  }
502
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
+
503
945
  // Testable.ts
504
946
  var fail = (message, actual, expected) => {
505
947
  throw new AssertionError({ message, actual, expected, operator: "live" });
@@ -507,14 +949,14 @@ var fail = (message, actual, expected) => {
507
949
  var getPath = (target, path) => path.split(".").reduce((node, segment) => typeof node === "object" && node !== null ? node[segment] : void 0, target);
508
950
  var TestRequestScope = token("Testing.RequestScope");
509
951
  function requestScope() {
510
- const container = currentContainer() ?? Container.getInstance();
511
- 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();
512
954
  }
513
955
  function currentSession() {
514
- const container = currentContainer();
515
- if (container?.bound(Session) !== true) return void 0;
956
+ const container2 = currentContainer2();
957
+ if (container2?.bound(Session) !== true) return void 0;
516
958
  try {
517
- return container.make(Session);
959
+ return container2.make(Session);
518
960
  } catch {
519
961
  return void 0;
520
962
  }
@@ -534,11 +976,14 @@ var Testable = class _Testable {
534
976
  #html;
535
977
  #effects;
536
978
  #instance;
537
- /** 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
+ */
538
983
  static async create(component, params = {}) {
539
984
  const scope = requestScope();
540
- const manager = inject2(LiveManager2);
541
- 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 })));
542
987
  }
543
988
  /** The component instance after the last request. */
544
989
  instance() {
@@ -556,9 +1001,24 @@ var Testable = class _Testable {
556
1001
  effects() {
557
1002
  return this.#effects;
558
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
+ */
559
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));
560
1012
  return this.#update(typeof path === "string" ? { [path]: value } : path, []);
561
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
+ }
562
1022
  async call(method, ...params) {
563
1023
  return this.#update({}, [{ method, params }]);
564
1024
  }
@@ -636,14 +1096,14 @@ var Testable = class _Testable {
636
1096
  const errors = stateOf(this.#instance).errors;
637
1097
  const wanted = [keys].flat();
638
1098
  if (wanted.length === 0 && Object.keys(errors).length === 0) fail("Component has no errors.", errors);
639
- 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);
640
1100
  return this;
641
1101
  }
642
1102
  /** No errors for the keys given (or none at all with no keys). */
643
1103
  assertHasNoErrors(keys = []) {
644
1104
  const errors = stateOf(this.#instance).errors;
645
1105
  const wanted = [keys].flat();
646
- 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);
647
1107
  if (found.length > 0) fail(`Component has errors: "${found.join('", "')}"`, errors);
648
1108
  return this;
649
1109
  }
@@ -666,17 +1126,17 @@ var Testable = class _Testable {
666
1126
  };
667
1127
 
668
1128
  // embed.ts
669
- async function live(component, params = {}, key) {
670
- 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));
671
1131
  return { toHtml: () => html };
672
1132
  }
673
- function LiveComponent({ component, "o:key": key, children: _children, ...params }) {
674
- return live(component, params, key);
1133
+ function LiveComponent({ component, "o:key": key2, children: _children, ...params }) {
1134
+ return live(component, params, key2);
675
1135
  }
676
1136
  var Live = Object.assign(LiveComponent, {
677
1137
  /** Registers a component under a name (components in app/Live are registered at boot). */
678
1138
  component(name, component) {
679
- inject3(LiveManager3).register(component, name);
1139
+ inject5(LiveManager3).register(component, name);
680
1140
  },
681
1141
  /** Livewire::test(): mounts `component` for assertions. */
682
1142
  test(component, params = {}) {
@@ -689,49 +1149,65 @@ import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
689
1149
  import { randomBytes } from "crypto";
690
1150
  import {
691
1151
  ClassRegistry,
692
- ConfigRepository,
1152
+ ConfigRepository as ConfigRepository4,
693
1153
  CurrentRoute as CurrentRoute2,
694
- Encrypter,
1154
+ Encrypter as Encrypter2,
695
1155
  HttpRequest,
696
1156
  Redirector,
697
1157
  Router,
698
1158
  Session as Session2,
699
- ViewFactory
1159
+ ViewFactory as ViewFactory2
700
1160
  } from "@orieljs/contracts";
701
- import { currentContainer as currentContainer2 } from "@orieljs/container";
702
- import { Str as Str2 } from "@orieljs/support";
1161
+ import { currentContainer as currentContainer3 } from "@orieljs/container";
1162
+ import { Str as Str4 } from "@orieljs/support";
703
1163
 
704
- // checksum.ts
705
- import { createHmac, timingSafeEqual } from "crypto";
706
- function canonicalJson(value) {
707
- if (Array.isArray(value)) return `[${value.map((item) => canonicalJson(item === void 0 ? null : item)).join(",")}]`;
708
- if (typeof value === "object" && value !== null) {
709
- const entries = Object.keys(value).sort().filter((key) => value[key] !== void 0).map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`);
710
- return `{${entries.join(",")}}`;
711
- }
712
- 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;
713
1172
  }
714
- function checksum(key, payload) {
715
- 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();
716
1182
  }
717
- function verifyChecksum(key, payload, given) {
718
- if (typeof given !== "string") return false;
719
- const expected = Buffer.from(checksum(key, payload), "utf8");
720
- const actual = Buffer.from(given, "utf8");
721
- 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
+ }
722
1198
  }
723
1199
 
724
1200
  // naming.ts
725
1201
  import { existsSync, readdirSync } from "fs";
726
1202
  import { join, relative, sep } from "path";
727
1203
  import { pathToFileURL } from "url";
728
- import { Str } from "@orieljs/support";
1204
+ import { Str as Str3 } from "@orieljs/support";
729
1205
  function componentName(file) {
730
- 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(".");
731
1207
  }
732
1208
  function defaultName(cls) {
733
1209
  const name = cls.name;
734
- return typeof name === "string" && name !== "" ? Str.kebab(name) : void 0;
1210
+ return typeof name === "string" && name !== "" ? Str3.kebab(name) : void 0;
735
1211
  }
736
1212
  function componentFiles(directory) {
737
1213
  if (!existsSync(directory)) return [];
@@ -810,9 +1286,9 @@ function persistentPass(router, persistent, method, path) {
810
1286
  const seen = /* @__PURE__ */ new Set();
811
1287
  const middleware = expand(route.gatherMiddleware(), aliases, groups).filter((entry) => {
812
1288
  if (!ids.has(entry.cls)) ids.set(entry.cls, ids.size);
813
- const key = `${ids.get(entry.cls)}:${entry.parameters.join(",")}`;
814
- if (!wanted.has(entry.cls) || excluded.has(entry.cls) || seen.has(key)) return false;
815
- 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);
816
1292
  return true;
817
1293
  });
818
1294
  const early = new Set(expand(["web", "api"], aliases, groups).map((entry) => entry.cls));
@@ -824,38 +1300,38 @@ function persistentPass(router, persistent, method, path) {
824
1300
  function persistentMiddleware(router, persistent, method, path) {
825
1301
  return [...persistentPass(router, persistent, method, path)?.middleware ?? []];
826
1302
  }
827
- function currentRoute(container) {
828
- if (!container.bound(CurrentRoute)) return void 0;
1303
+ function currentRoute(container2) {
1304
+ if (!container2.bound(CurrentRoute)) return void 0;
829
1305
  try {
830
- return container.make(CurrentRoute);
1306
+ return container2.make(CurrentRoute);
831
1307
  } catch {
832
1308
  return void 0;
833
1309
  }
834
1310
  }
835
- async function throughMiddleware(container, request, pass, destination) {
836
- const original = currentRoute(container);
1311
+ async function throughMiddleware(container2, request, pass, destination) {
1312
+ const original = currentRoute(container2);
837
1313
  const page = { route: pass.route, parameters: { ...pass.values }, middleware: [] };
838
1314
  let swapped = false;
839
1315
  const show = () => {
840
- container.instance(CurrentRoute, page);
1316
+ container2.instance(CurrentRoute, page);
841
1317
  swapped = true;
842
1318
  };
843
1319
  const restore = () => {
844
1320
  if (!swapped) return;
845
- if (original === void 0) container.forgetInstance(CurrentRoute);
846
- else container.instance(CurrentRoute, original);
1321
+ if (original === void 0) container2.forgetInstance(CurrentRoute);
1322
+ else container2.instance(CurrentRoute, original);
847
1323
  swapped = false;
848
1324
  };
849
1325
  let ran;
850
1326
  const run = async (index) => {
851
- 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));
852
1328
  if (index === pass.middleware.length) {
853
1329
  restore();
854
1330
  ran = { result: await destination() };
855
1331
  return new Response(null, { status: 204 });
856
1332
  }
857
1333
  const { cls, parameters } = pass.middleware[index];
858
- const instance = container.make(cls);
1334
+ const instance = container2.make(cls);
859
1335
  const next = () => run(index + 1);
860
1336
  return instance.handle(request, next, ...parameters);
861
1337
  };
@@ -929,18 +1405,19 @@ var livePaginationState = {
929
1405
 
930
1406
  // LiveComponentManager.ts
931
1407
  var renderContext = new AsyncLocalStorage2();
1408
+ var loading = /* @__PURE__ */ new WeakMap();
932
1409
  var pageRequest = new AsyncLocalStorage2();
933
- function optional(container, token2) {
934
- if (!container.bound(token2)) return void 0;
1410
+ function optional(container2, token2) {
1411
+ if (!container2.bound(token2)) return void 0;
935
1412
  try {
936
- return container.make(token2);
1413
+ return container2.make(token2);
937
1414
  } catch {
938
1415
  return void 0;
939
1416
  }
940
1417
  }
941
- var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
942
- function setOwn(object, key, value) {
943
- 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 });
944
1421
  }
945
1422
  async function hook2(instance, name, ...args) {
946
1423
  const method = instance[name];
@@ -953,7 +1430,7 @@ var LiveComponentManager = class {
953
1430
  app;
954
1431
  /** The request scope when there is one (sessions, the current route), else the application. */
955
1432
  container() {
956
- return currentContainer2() ?? this.app;
1433
+ return currentContainer3() ?? this.app;
957
1434
  }
958
1435
  registry() {
959
1436
  return this.container().make(ClassRegistry);
@@ -984,10 +1461,10 @@ var LiveComponentManager = class {
984
1461
  return request.header("X-Oriel-Live") === "true";
985
1462
  }
986
1463
  /** The first render: HTML with o:id, o:snapshot and o:effects on the root element. */
987
- async mount(component, params = {}, key) {
1464
+ async mount(component, params = {}, key2) {
988
1465
  const context = renderContext.getStore();
989
1466
  if (context === void 0) return (await this.mountComponent(component, params)).html;
990
- const childKey = key ?? `lw-${context.index++}`;
1467
+ const childKey = key2 ?? `lw-${context.index++}`;
991
1468
  const existing = Object.hasOwn(context.previous, childKey) ? context.previous[childKey] : void 0;
992
1469
  if (existing !== void 0) {
993
1470
  setOwn(context.children, childKey, existing);
@@ -997,7 +1474,11 @@ var LiveComponentManager = class {
997
1474
  setOwn(context.children, childKey, [result.tag, result.snapshot.memo.id]);
998
1475
  return result.html;
999
1476
  }
1000
- 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) {
1001
1482
  const cls = typeof component === "string" ? this.resolve(component) : component;
1002
1483
  if (!isComponentClass(cls)) throw new LiveError(`[${cls.name}] is not a Live component: it must extend Component.`);
1003
1484
  const name = this.nameOf(cls);
@@ -1005,7 +1486,10 @@ var LiveComponentManager = class {
1005
1486
  const state = stateOf(instance);
1006
1487
  state.id = randomBytes(15).toString("base64url");
1007
1488
  state.name = name;
1489
+ const { lazy: _lazy, ...params } = embedParams;
1490
+ const lazy = lazyMode(cls, embedParams);
1008
1491
  await hook2(instance, "boot");
1492
+ if (lazy !== void 0) return this.mountPlaceholder(instance, params, lazy);
1009
1493
  for (const [property, value] of Object.entries(params)) {
1010
1494
  if (Object.hasOwn(instance, property)) instance[property] = value;
1011
1495
  }
@@ -1022,6 +1506,30 @@ var LiveComponentManager = class {
1022
1506
  const attributes = { "o:snapshot": JSON.stringify(snapshot), "o:effects": JSON.stringify(effects), "o:id": state.id };
1023
1507
  return { instance, snapshot, effects, html: insertAttributesIntoRoot(html, attributes), tag };
1024
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
+ }
1025
1533
  /** Applies each component's updates and calls, re-renders it and returns its new snapshot and effects. */
1026
1534
  async update(request) {
1027
1535
  const components = [];
@@ -1034,6 +1542,7 @@ var LiveComponentManager = class {
1034
1542
  }
1035
1543
  async updateComponent(snapshotJson, updates, calls) {
1036
1544
  const snapshot = this.verify(snapshotJson);
1545
+ const load = await this.lazyLoad(snapshot.memo, updates, calls);
1037
1546
  const cls = this.resolve(snapshot.memo.name);
1038
1547
  const instance = this.container().make(cls);
1039
1548
  const state = stateOf(instance);
@@ -1048,13 +1557,21 @@ var LiveComponentManager = class {
1048
1557
  this.prepareComputed(instance);
1049
1558
  const returns = [];
1050
1559
  await renderPaginated(instance, this.pageUrl(snapshot.memo.path), async () => {
1051
- await hook2(instance, "hydrate");
1560
+ if (load === void 0) await hook2(instance, "hydrate");
1052
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
+ }
1053
1570
  for (const [path, value] of Object.entries(updates)) await this.applyUpdate(instance, path, value);
1054
- 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));
1055
1572
  } catch (error) {
1056
1573
  const errors = error.errors;
1057
- if (error.kind !== "Validation" || !isRecord(errors)) throw error;
1574
+ if (error.kind !== "Validation" || !isRecord2(errors)) throw error;
1058
1575
  state.errors = { ...errors };
1059
1576
  }
1060
1577
  });
@@ -1065,12 +1582,24 @@ var LiveComponentManager = class {
1065
1582
  if (!state.skipRender) {
1066
1583
  ;
1067
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);
1068
1587
  html = insertAttributesIntoRoot(html, { "o:id": state.id });
1069
1588
  effects.html = html;
1070
1589
  }
1071
1590
  if (calls.length > 0) effects.returns = returns;
1072
1591
  return { instance, snapshot: await this.snapshot(instance, memo), effects, html, tag };
1073
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
+ }
1074
1603
  /**
1075
1604
  * The route action of POST /oriel/live/update. Each component's update runs behind the persistent middleware of the
1076
1605
  * route that rendered its page (Livewire's): `auth`, `verified`, `guest` and `can`, with the page route's parameters
@@ -1080,17 +1609,17 @@ var LiveComponentManager = class {
1080
1609
  */
1081
1610
  async handle(request) {
1082
1611
  if (!this.isLiveRequest(request)) throw new InvalidLiveRequestError("Live update requests need the X-Oriel-Live header.");
1083
- const container = this.container();
1084
- 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;
1085
1614
  const components = [];
1086
- const flashed = trackFlash(optional(container, Session2));
1615
+ const flashed = trackFlash(optional(container2, Session2));
1087
1616
  for (const component of this.payload(request).components) {
1088
1617
  const { memo } = this.verify(component.snapshot);
1089
- const pass = persistentPass(container.make(Router), persistent, memo.method, memo.path);
1618
+ const pass = persistentPass(container2.make(Router), persistent, memo.method, memo.path);
1090
1619
  const update = () => this.updateComponent(component.snapshot, component.updates, component.calls);
1091
1620
  const result = await pageRequest.run(
1092
1621
  { path: memo.path, method: memo.method },
1093
- () => 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)
1094
1623
  );
1095
1624
  if (result instanceof Response) return result;
1096
1625
  components.push({ snapshot: JSON.stringify(result.snapshot), effects: result.effects });
@@ -1100,19 +1629,19 @@ var LiveComponentManager = class {
1100
1629
  }
1101
1630
  /** A full-page component (Route.get('/counter', Counter)): mounted with the route parameters, inside the layout. */
1102
1631
  async page(component) {
1103
- const container = this.container();
1104
- const params = { ...optional(container, CurrentRoute2)?.parameters ?? {} };
1632
+ const container2 = this.container();
1633
+ const params = { ...optional(container2, CurrentRoute2)?.parameters ?? {} };
1105
1634
  const cls = component.constructor;
1106
1635
  const result = await this.mountComponent(cls, params, component);
1107
1636
  const redirect = result.effects.redirect;
1108
1637
  if (redirect !== void 0) {
1109
- 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 } });
1110
1639
  }
1111
- 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";
1112
1641
  const html = result.html;
1113
1642
  const data = { children: { toHtml: () => html } };
1114
1643
  if (cls.title !== void 0) data.title = cls.title;
1115
- return container.make(ViewFactory).make(layout, data);
1644
+ return container2.make(ViewFactory2).make(layout, data);
1116
1645
  }
1117
1646
  /** Parses and checks a snapshot: a bad checksum throws before anything is hydrated. */
1118
1647
  verify(snapshotJson) {
@@ -1122,13 +1651,13 @@ var LiveComponentManager = class {
1122
1651
  } catch {
1123
1652
  throw new CorruptComponentPayloadError();
1124
1653
  }
1125
- 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();
1126
1655
  const memo = snapshot.memo;
1127
1656
  const name = typeof memo.name === "string" ? memo.name : "unknown";
1128
- const key = this.key();
1657
+ const key2 = this.key();
1129
1658
  let valid;
1130
1659
  try {
1131
- valid = verifyChecksum(key, { data: snapshot.data, memo: snapshot.memo }, snapshot.checksum);
1660
+ valid = verifyChecksum(key2, { data: snapshot.data, memo: snapshot.memo }, snapshot.checksum);
1132
1661
  } catch {
1133
1662
  valid = false;
1134
1663
  }
@@ -1136,7 +1665,7 @@ var LiveComponentManager = class {
1136
1665
  return snapshot;
1137
1666
  }
1138
1667
  key() {
1139
- return this.container().make(Encrypter).getKey();
1668
+ return this.container().make(Encrypter2).getKey();
1140
1669
  }
1141
1670
  /** The request body, read from the raw JSON when the request has it, so TrimStrings can't alter typed values. */
1142
1671
  payload(request) {
@@ -1147,9 +1676,9 @@ var LiveComponentManager = class {
1147
1676
  } catch {
1148
1677
  throw new InvalidLiveRequestError("The Live update request body is not valid JSON.");
1149
1678
  }
1150
- const components = isRecord(body) ? body.components : void 0;
1679
+ const components = isRecord2(body) ? body.components : void 0;
1151
1680
  const valid = Array.isArray(components) && components.every(
1152
- (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))
1153
1682
  );
1154
1683
  if (!valid) throw new InvalidLiveRequestError("The Live update request body is malformed.");
1155
1684
  return body;
@@ -1168,10 +1697,10 @@ var LiveComponentManager = class {
1168
1697
  async applyUpdate(instance, path, value) {
1169
1698
  if (!isUpdatable(instance, path)) throw new PropertyNotUpdatableError(path, stateOf(instance).name);
1170
1699
  const [root, ...rest] = path.split(".");
1171
- const key = rest.length > 0 ? rest.join(".") : void 0;
1172
- const studly = Str2.studly(root);
1700
+ const key2 = rest.length > 0 ? rest.join(".") : void 0;
1701
+ const studly = Str4.studly(root);
1173
1702
  await hook2(instance, "updating", path, value);
1174
- await hook2(instance, `updating${studly}`, value, key);
1703
+ await hook2(instance, `updating${studly}`, value, key2);
1175
1704
  const target = instance;
1176
1705
  if (rest.length === 0) target[root] = value;
1177
1706
  else {
@@ -1179,20 +1708,20 @@ var LiveComponentManager = class {
1179
1708
  if (Array.isArray(node2) && !isArraySlot(node2, segment)) throw new PropertyNotUpdatableError(path, stateOf(instance).name);
1180
1709
  };
1181
1710
  let node = target[root];
1182
- if (!isRecord(node) && !Array.isArray(node)) target[root] = node = {};
1711
+ if (!isRecord2(node) && !Array.isArray(node)) target[root] = node = {};
1183
1712
  for (const segment of rest.slice(0, -1)) {
1184
1713
  assertSlot(node, segment);
1185
1714
  const next = node[segment];
1186
- node = isRecord(next) || Array.isArray(next) ? next : node[segment] = {};
1715
+ node = isRecord2(next) || Array.isArray(next) ? next : node[segment] = {};
1187
1716
  }
1188
1717
  const last = rest.at(-1);
1189
1718
  assertSlot(node, last);
1190
1719
  Object.defineProperty(node, last, { value, enumerable: true, writable: true, configurable: true });
1191
1720
  }
1192
1721
  await hook2(instance, "updated", path, value);
1193
- await hook2(instance, `updated${studly}`, value, key);
1722
+ await hook2(instance, `updated${studly}`, value, key2);
1194
1723
  }
1195
- /** 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. */
1196
1725
  async call(instance, method, params) {
1197
1726
  const name = stateOf(instance).name;
1198
1727
  switch (method) {
@@ -1202,7 +1731,7 @@ var LiveComponentManager = class {
1202
1731
  return this.applyUpdate(instance, String(params[0]), params[1]);
1203
1732
  case "$toggle": {
1204
1733
  const path = String(params[0]);
1205
- 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);
1206
1735
  return this.applyUpdate(instance, path, !current);
1207
1736
  }
1208
1737
  case "__dispatch": {
@@ -1210,11 +1739,12 @@ var LiveComponentManager = class {
1210
1739
  const listeners = listenersOf(instance.constructor);
1211
1740
  for (const listener of Object.hasOwn(listeners, String(event)) ? listeners[String(event)] : []) {
1212
1741
  if (!isCallable(instance, listener)) throw new MethodNotCallableError(listener, name);
1213
- await instance[listener](isRecord(detail) ? detail : {});
1742
+ await instance[listener](isRecord2(detail) ? detail : {});
1214
1743
  }
1215
1744
  return void 0;
1216
1745
  }
1217
1746
  }
1747
+ if (UPLOAD_CALLS.has(method)) return runUploadCall(instance, method, params, (path, value) => this.applyUpdate(instance, path, value));
1218
1748
  if (!isCallable(instance, method)) throw new MethodNotCallableError(method, name);
1219
1749
  return instance[method](...params);
1220
1750
  }
@@ -1230,7 +1760,7 @@ var LiveComponentManager = class {
1230
1760
  context,
1231
1761
  () => renderPaginated(instance, this.pageUrl(memo.path), async () => {
1232
1762
  const output = await instance.render();
1233
- return typeof output === "string" ? output : this.container().make(ViewFactory).renderToString(output);
1763
+ return typeof output === "string" ? output : this.container().make(ViewFactory2).renderToString(output);
1234
1764
  })
1235
1765
  );
1236
1766
  await hook2(instance, "rendered", html);
@@ -1275,16 +1805,16 @@ var LiveComponentManager = class {
1275
1805
  };
1276
1806
 
1277
1807
  // LiveServiceProvider.ts
1278
- import { ConfigRepository as ConfigRepository3, ConsoleKernel, HttpRequest as HttpRequest2, LiveManager as LiveManager4, PaginationState, Router as Router2 } from "@orieljs/contracts";
1279
- 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";
1280
1810
  import { ServiceProvider } from "@orieljs/support";
1281
1811
 
1282
1812
  // commands.ts
1283
1813
  import { existsSync as existsSync2 } from "fs";
1284
1814
  import { mkdir, writeFile } from "fs/promises";
1285
1815
  import { dirname, join as join2, relative as relative2, sep as sep2 } from "path";
1286
- import { ConfigRepository as ConfigRepository2 } from "@orieljs/contracts";
1287
- import { Str as Str3 } from "@orieljs/support";
1816
+ import { ConfigRepository as ConfigRepository5 } from "@orieljs/contracts";
1817
+ import { Str as Str5 } from "@orieljs/support";
1288
1818
  var COMPONENT_STUB = `import { Component } from '@orieljs/framework'
1289
1819
 
1290
1820
  export default class {{ class }} extends Component {
@@ -1312,13 +1842,13 @@ function importPath(from, to) {
1312
1842
  }
1313
1843
  function registerLiveCommands(kernel, app) {
1314
1844
  kernel.command("make:live {name : The component name, e.g. Posts/CreatePost} {--test : Also create a test} {--force : Overwrite existing files}", async function() {
1315
- 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));
1316
1846
  if (segments.length === 0 || !segments.every((segment) => CLASS_NAME.test(segment))) {
1317
1847
  this.error(`Component name [${String(this.argument("name"))}] is not valid: use names like Posts/CreatePost.`);
1318
1848
  return 1;
1319
1849
  }
1320
1850
  const className = segments.at(-1);
1321
- const config = app.bound(ConfigRepository2) ? app.make(ConfigRepository2) : void 0;
1851
+ const config = app.bound(ConfigRepository5) ? app.make(ConfigRepository5) : void 0;
1322
1852
  const directory = config?.get("live.class_path") ?? app.appPath("Live");
1323
1853
  const file = join2(directory, ...segments.slice(0, -1), `${className}.tsx`);
1324
1854
  const force = this.option("force") === true;
@@ -1335,7 +1865,7 @@ function registerLiveCommands(kernel, app) {
1335
1865
  this.error(`Test [${relative2(app.basePath(), testFile)}] already exists.`);
1336
1866
  return 1;
1337
1867
  }
1338
- const name = segments.map((segment) => Str3.kebab(segment)).join(".");
1868
+ const name = segments.map((segment) => Str5.kebab(segment)).join(".");
1339
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);
1340
1870
  await mkdir(dirname(testFile), { recursive: true });
1341
1871
  await writeFile(testFile, test);
@@ -1351,16 +1881,19 @@ var LiveServiceProvider = class extends ServiceProvider {
1351
1881
  register() {
1352
1882
  this.mergeConfigFrom({ class_path: this.app.appPath("Live"), layout: "components.layouts.app", persistent_middleware: [...PERSISTENT_MIDDLEWARE] }, "live");
1353
1883
  this.app.singleton(LiveComponentManager, () => new LiveComponentManager(this.app));
1354
- this.app.singleton(LiveManager4, (container) => container.make(LiveComponentManager));
1884
+ this.app.singleton(LiveManager4, (container2) => container2.make(LiveComponentManager));
1355
1885
  this.app.singleton(PaginationState, () => livePaginationState);
1356
1886
  }
1357
1887
  async boot() {
1358
1888
  const manager = this.app.make(LiveComponentManager);
1359
1889
  if (this.app.bound(Router2)) {
1360
- 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");
1361
1894
  }
1362
1895
  if (this.app.bound(ConsoleKernel)) registerLiveCommands(this.app.make(ConsoleKernel), this.app);
1363
- 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"));
1364
1897
  for (const [name, exported] of await this.app.run(() => discover(directory))) {
1365
1898
  if (isComponentClass(exported)) manager.register(exported, name);
1366
1899
  }
@@ -1407,7 +1940,9 @@ export {
1407
1940
  Computed,
1408
1941
  CorruptComponentPayloadError,
1409
1942
  ErrorBag,
1943
+ FileNotPreviewableError,
1410
1944
  InvalidLiveRequestError,
1945
+ InvalidUploadSignatureError,
1411
1946
  Live,
1412
1947
  LiveComponentManager,
1413
1948
  LiveComponentNotFoundError,
@@ -1417,12 +1952,19 @@ export {
1417
1952
  MethodNotCallableError,
1418
1953
  On,
1419
1954
  PERSISTENT_MIDDLEWARE,
1955
+ PREVIEW_URI,
1420
1956
  Pagination,
1421
1957
  PendingEvent,
1422
1958
  PropertyNotUpdatableError,
1959
+ TEMPORARY_UPLOAD_DEFAULTS,
1960
+ TemporaryFileNotFoundError,
1961
+ TemporaryUploadedFile,
1423
1962
  TestRequestScope,
1424
1963
  Testable,
1425
1964
  UPDATE_URI,
1965
+ UPLOAD_URI,
1966
+ UploadValidationError,
1967
+ WithFileUploads,
1426
1968
  WithPagination,
1427
1969
  componentName,
1428
1970
  live,