@orieljs/live 0.1.0-alpha.10

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 ADDED
@@ -0,0 +1,1991 @@
1
+ // Component.ts
2
+ import {
3
+ LiveManager,
4
+ UrlGenerator as UrlGenerator2,
5
+ ValidationFactory
6
+ } from "@orieljs/contracts";
7
+ import { inject } from "@orieljs/container";
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
+
86
+ // metadata.ts
87
+ Symbol.metadata ??= /* @__PURE__ */ Symbol.for("Symbol.metadata");
88
+ var LISTENERS = /* @__PURE__ */ Symbol.for("oriel:live.listeners");
89
+ var LOCKED = /* @__PURE__ */ Symbol.for("oriel:live.locked");
90
+ var COMPUTED = /* @__PURE__ */ Symbol.for("oriel:live.computed");
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];
94
+ }
95
+ function On(event) {
96
+ return function(_method, context) {
97
+ const listeners = own(
98
+ context.metadata,
99
+ LISTENERS,
100
+ () => ({}),
101
+ (inherited) => Object.fromEntries(Object.entries(inherited).map(([name, methods2]) => [name, [...methods2]]))
102
+ );
103
+ const methods = listeners[event] ??= [];
104
+ if (!methods.includes(String(context.name))) methods.push(String(context.name));
105
+ };
106
+ }
107
+ function Locked(_target, context) {
108
+ own(context.metadata, LOCKED, () => [], (inherited) => [...inherited]).push(String(context.name));
109
+ }
110
+ var computedCache = /* @__PURE__ */ new WeakMap();
111
+ function cachedGetter(name, getter) {
112
+ return function() {
113
+ let cache = computedCache.get(this);
114
+ if (cache === void 0) computedCache.set(this, cache = /* @__PURE__ */ new Map());
115
+ if (!cache.has(name)) cache.set(name, getter.call(this));
116
+ return cache.get(name);
117
+ };
118
+ }
119
+ function Computed(getter, context) {
120
+ own(context.metadata, COMPUTED, () => [], (inherited) => [...inherited]).push(String(context.name));
121
+ return cachedGetter(String(context.name), getter);
122
+ }
123
+ function forgetComputed(instance) {
124
+ computedCache.delete(instance);
125
+ }
126
+ function metadataOf(cls) {
127
+ return cls[Symbol.metadata] ?? {};
128
+ }
129
+ function chainOf(cls) {
130
+ const chain = [];
131
+ for (let c = cls; c !== null && c !== Function.prototype; c = Object.getPrototypeOf(c)) {
132
+ chain.unshift(c);
133
+ }
134
+ return chain;
135
+ }
136
+ function staticsOf(cls, key2) {
137
+ return chainOf(cls).flatMap((c) => Object.hasOwn(c, key2) && c[key2] !== void 0 ? [c[key2]] : []);
138
+ }
139
+ function listenersOf(cls) {
140
+ const result = {};
141
+ for (const [event, methods] of Object.entries(metadataOf(cls)[LISTENERS] ?? {})) {
142
+ result[event] = [...methods];
143
+ }
144
+ for (const listeners of staticsOf(cls, "listeners")) {
145
+ for (const [event, method] of Object.entries(listeners)) {
146
+ const methods = result[event] ??= [];
147
+ if (!methods.includes(method)) methods.push(method);
148
+ }
149
+ }
150
+ return result;
151
+ }
152
+ function lockedOf(cls) {
153
+ return /* @__PURE__ */ new Set([...metadataOf(cls)[LOCKED] ?? [], ...staticsOf(cls, "locked").flat()]);
154
+ }
155
+ function computedOf(cls) {
156
+ return /* @__PURE__ */ new Set([...metadataOf(cls)[COMPUTED] ?? [], ...staticsOf(cls, "computed").flat()]);
157
+ }
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
+
445
+ // Component.ts
446
+ var COMPONENT = /* @__PURE__ */ Symbol.for("oriel:live.component");
447
+ var states = /* @__PURE__ */ new WeakMap();
448
+ function stateOf(component) {
449
+ let state = states.get(component);
450
+ if (state === void 0) states.set(component, state = { id: "", name: "", errors: {}, dispatches: [], skipRender: false });
451
+ return state;
452
+ }
453
+ function isComponentClass(value) {
454
+ return typeof value === "function" && value[COMPONENT] === true;
455
+ }
456
+ var ErrorBag = class {
457
+ constructor(messages) {
458
+ this.messages = messages;
459
+ }
460
+ messages;
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 });
470
+ }
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) ?? []];
479
+ }
480
+ all() {
481
+ return Object.values(this.messages).flat();
482
+ }
483
+ any() {
484
+ return !this.isEmpty();
485
+ }
486
+ isEmpty() {
487
+ return this.count() === 0;
488
+ }
489
+ count() {
490
+ return this.all().length;
491
+ }
492
+ toObject() {
493
+ return Object.fromEntries(Object.entries(this.messages).map(([key2, messages]) => [key2, [...messages]]));
494
+ }
495
+ };
496
+ var PendingEvent = class {
497
+ constructor(event) {
498
+ this.event = event;
499
+ }
500
+ event;
501
+ /** Only this component's own listeners receive the event. */
502
+ self() {
503
+ this.event.self = true;
504
+ delete this.event.to;
505
+ return this;
506
+ }
507
+ /** Only components of this class (or registered name) receive the event. */
508
+ to(component) {
509
+ this.event.to = component;
510
+ delete this.event.self;
511
+ return this;
512
+ }
513
+ };
514
+ var Component = class {
515
+ static [COMPONENT] = true;
516
+ /** The view that wraps a full-page component (default: config `live.layout`). */
517
+ static layout;
518
+ /** The page title passed to the layout as `title`. */
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;
527
+ /** Static fallbacks for runtimes without decorators: { event: 'method' }, locked names, computed getter names. */
528
+ static listeners;
529
+ static locked;
530
+ static computed;
531
+ getId() {
532
+ return stateOf(this).id;
533
+ }
534
+ getName() {
535
+ return stateOf(this).name;
536
+ }
537
+ /** Dispatches a browser event; every component listening for it (@On) runs its handler. */
538
+ dispatch(name, params = {}) {
539
+ const event = { name, params };
540
+ stateOf(this).dispatches.push(event);
541
+ return new PendingEvent(event);
542
+ }
543
+ /** Sends the browser to `url` after this request; `navigate` uses o:navigate instead of a full page load. */
544
+ redirect(url, options = {}) {
545
+ const state = stateOf(this);
546
+ state.redirect = url;
547
+ if (options.navigate === true) state.redirectUsingNavigate = true;
548
+ else delete state.redirectUsingNavigate;
549
+ }
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);
557
+ }
558
+ /**
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`).
562
+ */
563
+ async validate(rules, messages, attributes) {
564
+ const validator = inject(ValidationFactory).make(Object.fromEntries(Object.entries(this)), rules, messages, attributes);
565
+ try {
566
+ const data = await validator.validated();
567
+ stateOf(this).errors = {};
568
+ return data;
569
+ } catch (error) {
570
+ const errors = error.errors;
571
+ if (error.kind === "Validation" && errors !== void 0) stateOf(this).errors = { ...errors };
572
+ throw error;
573
+ }
574
+ }
575
+ addError(key2, message) {
576
+ this.errors.add(key2, message);
577
+ }
578
+ /** Clears every error, or only those of `keys`. */
579
+ resetErrorBag(...keys) {
580
+ const state = stateOf(this);
581
+ if (keys.length === 0) state.errors = {};
582
+ else for (const key2 of keys) delete state.errors[key2];
583
+ }
584
+ get errors() {
585
+ return new ErrorBag(stateOf(this).errors);
586
+ }
587
+ /** Puts public properties (all of them when none are named) back to their initial values. */
588
+ reset(...properties) {
589
+ const fresh = new this.constructor();
590
+ const names = properties.length > 0 ? properties : Object.keys(this);
591
+ for (const name of names) if (Object.hasOwn(fresh, name)) this[name] = fresh[name];
592
+ forgetComputed(this);
593
+ }
594
+ /** Keeps the current HTML: the response carries the new state but no `html` effect. */
595
+ skipRender() {
596
+ stateOf(this).skipRender = true;
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
+ }
615
+ /** A full-page component as a route action: Route.get('/counter', Counter). Route parameters go to mount(). */
616
+ async __invoke() {
617
+ return inject(LiveManager).page(this);
618
+ }
619
+ };
620
+
621
+ // embed.ts
622
+ import { LiveManager as LiveManager3 } from "@orieljs/contracts";
623
+ import { inject as inject5 } from "@orieljs/container";
624
+
625
+ // Testable.ts
626
+ import { AssertionError } from "assert";
627
+ import { isDeepStrictEqual } from "util";
628
+ import { LiveManager as LiveManager2, Session, token } from "@orieljs/contracts";
629
+ import { Container, currentContainer as currentContainer2, inject as inject4 } from "@orieljs/container";
630
+
631
+ // flash.ts
632
+ function trackFlash(session, { saved = true } = {}) {
633
+ if (session === void 0) return () => void 0;
634
+ const before = new Set(session.get("_flash.new", []));
635
+ return (redirected) => {
636
+ if (redirected) return;
637
+ const flashed = session.get("_flash.new", []);
638
+ const fresh = flashed.filter((key2) => !before.has(key2));
639
+ if (fresh.length === 0) return;
640
+ session.put("_flash.new", flashed.filter((key2) => before.has(key2)));
641
+ if (!saved) return session.forget(...fresh);
642
+ const old = session.get("_flash.old", []);
643
+ session.put("_flash.old", [...old, ...fresh.filter((key2) => !old.includes(key2))]);
644
+ };
645
+ }
646
+
647
+ // html.ts
648
+ var ESCAPES = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#039;" };
649
+ function escapeAttribute(value) {
650
+ return value.replace(/[&<>"']/g, (char) => ESCAPES[char]);
651
+ }
652
+ function stripTags(html) {
653
+ return html.replace(/<[^>]*>/g, "");
654
+ }
655
+ var VOID = /* @__PURE__ */ new Set(["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "source", "track", "wbr"]);
656
+ var RAW_TEXT = /* @__PURE__ */ new Set(["script", "style", "textarea", "title"]);
657
+ var TOKEN = /<\/([A-Za-z][\w:-]*)\s*>|<([A-Za-z][\w:-]*)(?:\s+[^\s"'<>/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'=<>`]+))?)*\s*(\/?)>/g;
658
+ function rootTag(html, component) {
659
+ const fail2 = () => {
660
+ throw new LiveError(`Live only supports one HTML element per component. Multiple root elements were detected for component [${component}].`);
661
+ };
662
+ const source = html.replace(/<!--[\s\S]*?-->/g, "").trim();
663
+ let depth = 0;
664
+ let root;
665
+ let rootEnd = -1;
666
+ TOKEN.lastIndex = 0;
667
+ for (let match = TOKEN.exec(source); match !== null; match = TOKEN.exec(source)) {
668
+ const [token2, closing, opening, selfClosing] = match;
669
+ if (root === void 0 && match.index !== 0) fail2();
670
+ if (depth === 0 && root !== void 0) fail2();
671
+ if (opening !== void 0) {
672
+ const tag = opening.toLowerCase();
673
+ root ??= tag;
674
+ if (selfClosing === "/" || VOID.has(tag)) {
675
+ if (depth === 0) rootEnd = match.index + token2.length;
676
+ continue;
677
+ }
678
+ if (RAW_TEXT.has(tag)) {
679
+ const closer = new RegExp(`</${tag}`, "gi");
680
+ closer.lastIndex = TOKEN.lastIndex;
681
+ const end = closer.exec(source);
682
+ if (end !== null) TOKEN.lastIndex = end.index;
683
+ }
684
+ depth++;
685
+ } else if (closing !== void 0) {
686
+ if (--depth < 0) fail2();
687
+ if (depth === 0) rootEnd = match.index + token2.length;
688
+ }
689
+ }
690
+ if (root === void 0 || depth !== 0 || source.slice(rootEnd).trim() !== "") fail2();
691
+ return root;
692
+ }
693
+ function insertAttributesIntoRoot(html, attributes) {
694
+ const source = html.trim();
695
+ const match = /^((?:<!--[\s\S]*?-->\s*)*<[A-Za-z][\w:-]*)/.exec(source);
696
+ if (match === null) throw new LiveError("Live could not find the root element of the component.");
697
+ const rendered = Object.entries(attributes).map(([name, value]) => ` ${name}="${escapeAttribute(value)}"`).join("");
698
+ return match[1] + rendered + source.slice(match[1].length);
699
+ }
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
+
945
+ // Testable.ts
946
+ var fail = (message, actual, expected) => {
947
+ throw new AssertionError({ message, actual, expected, operator: "live" });
948
+ };
949
+ var getPath = (target, path) => path.split(".").reduce((node, segment) => typeof node === "object" && node !== null ? node[segment] : void 0, target);
950
+ var TestRequestScope = token("Testing.RequestScope");
951
+ function requestScope() {
952
+ const container2 = currentContainer2() ?? Container.getInstance();
953
+ return container2?.bound(TestRequestScope) === true ? container2.make(TestRequestScope) : (run) => run();
954
+ }
955
+ function currentSession() {
956
+ const container2 = currentContainer2();
957
+ if (container2?.bound(Session) !== true) return void 0;
958
+ try {
959
+ return container2.make(Session);
960
+ } catch {
961
+ return void 0;
962
+ }
963
+ }
964
+ var Testable = class _Testable {
965
+ constructor(manager, scope, result) {
966
+ this.manager = manager;
967
+ this.scope = scope;
968
+ this.#snapshot = JSON.stringify(result.snapshot);
969
+ this.#html = result.html ?? "";
970
+ this.#effects = result.effects;
971
+ this.#instance = result.instance;
972
+ }
973
+ manager;
974
+ scope;
975
+ #snapshot;
976
+ #html;
977
+ #effects;
978
+ #instance;
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
+ */
983
+ static async create(component, params = {}) {
984
+ const scope = requestScope();
985
+ const manager = inject4(LiveManager2);
986
+ return new _Testable(manager, scope, await scope(() => manager.mountComponent(component, { lazy: false, ...params })));
987
+ }
988
+ /** The component instance after the last request. */
989
+ instance() {
990
+ return this.#instance;
991
+ }
992
+ /** A public property (dotted paths reach into arrays and objects). */
993
+ get(path) {
994
+ return getPath(this.#instance, path);
995
+ }
996
+ /** The last rendered HTML, without the o:id, o:snapshot and o:effects attributes (Livewire strips them too). */
997
+ html() {
998
+ return this.#html.replace(/ o:(id|snapshot|effects)="[^"]*"/g, "");
999
+ }
1000
+ /** The effects of the last request. */
1001
+ effects() {
1002
+ return this.#effects;
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
+ */
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));
1012
+ return this.#update(typeof path === "string" ? { [path]: value } : path, []);
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
+ }
1022
+ async call(method, ...params) {
1023
+ return this.#update({}, [{ method, params }]);
1024
+ }
1025
+ async toggle(path) {
1026
+ return this.#update({}, [{ method: "$toggle", params: [path] }]);
1027
+ }
1028
+ /** Dispatches an event to this component, as the browser does for its listeners. */
1029
+ async dispatch(event, params = {}) {
1030
+ return this.#update({}, [{ method: "__dispatch", params: [event, params] }]);
1031
+ }
1032
+ async refresh() {
1033
+ return this.#update({}, [{ method: "$refresh", params: [] }]);
1034
+ }
1035
+ async #update(updates, calls) {
1036
+ const result = await this.scope(async () => {
1037
+ const flashed = trackFlash(currentSession(), { saved: false });
1038
+ const updated = await this.manager.updateComponent(this.#snapshot, updates, calls);
1039
+ flashed(updated.effects.redirect !== void 0);
1040
+ return updated;
1041
+ });
1042
+ this.#snapshot = JSON.stringify(result.snapshot);
1043
+ if (result.html !== void 0) this.#html = result.html;
1044
+ this.#effects = result.effects;
1045
+ this.#instance = result.instance;
1046
+ return this;
1047
+ }
1048
+ assertSet(path, value) {
1049
+ const actual = this.get(path);
1050
+ if (!isDeepStrictEqual(actual, value)) fail(`Failed asserting that property [${path}] is set to the expected value.`, actual, value);
1051
+ return this;
1052
+ }
1053
+ assertNotSet(path, value) {
1054
+ if (isDeepStrictEqual(this.get(path), value)) fail(`Failed asserting that property [${path}] is not set to the given value.`, this.get(path), value);
1055
+ return this;
1056
+ }
1057
+ /** Each value, HTML-escaped, appears in the rendered HTML. */
1058
+ assertSee(...values) {
1059
+ for (const value of values) if (!this.html().includes(escapeAttribute(value))) fail(`Failed asserting that [${value}] is in the rendered HTML.`, this.html(), value);
1060
+ return this;
1061
+ }
1062
+ assertDontSee(...values) {
1063
+ for (const value of values) if (this.html().includes(escapeAttribute(value))) fail(`Failed asserting that [${value}] is not in the rendered HTML.`, this.html(), value);
1064
+ return this;
1065
+ }
1066
+ /** Each value appears in the rendered HTML as written (not escaped). */
1067
+ assertSeeHtml(...values) {
1068
+ for (const value of values) if (!this.html().includes(value)) fail(`Failed asserting that [${value}] is in the rendered HTML.`, this.html(), value);
1069
+ return this;
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
+ }
1082
+ assertDispatched(name, params) {
1083
+ const dispatched = (this.#effects.dispatches ?? []).filter((event) => event.name === name);
1084
+ if (dispatched.length === 0) fail(`Failed asserting that an event [${name}] was dispatched.`, this.#effects.dispatches ?? [], name);
1085
+ if (params !== void 0 && !dispatched.some((event) => isDeepStrictEqual(event.params, params))) {
1086
+ fail(`Failed asserting that event [${name}] was dispatched with the expected params.`, dispatched.map((event) => event.params), params);
1087
+ }
1088
+ return this;
1089
+ }
1090
+ assertNotDispatched(name) {
1091
+ if ((this.#effects.dispatches ?? []).some((event) => event.name === name)) fail(`Failed asserting that an event [${name}] was not dispatched.`);
1092
+ return this;
1093
+ }
1094
+ /** The error bag has errors for every key given (or any error at all with no keys). */
1095
+ assertHasErrors(keys = []) {
1096
+ const errors = stateOf(this.#instance).errors;
1097
+ const wanted = [keys].flat();
1098
+ if (wanted.length === 0 && Object.keys(errors).length === 0) fail("Component has no errors.", errors);
1099
+ for (const key2 of wanted) if ((errors[key2]?.length ?? 0) === 0) fail(`Component missing error: ${key2}`, errors, key2);
1100
+ return this;
1101
+ }
1102
+ /** No errors for the keys given (or none at all with no keys). */
1103
+ assertHasNoErrors(keys = []) {
1104
+ const errors = stateOf(this.#instance).errors;
1105
+ const wanted = [keys].flat();
1106
+ const found = wanted.length === 0 ? Object.keys(errors).filter((key2) => (errors[key2]?.length ?? 0) > 0) : wanted.filter((key2) => (errors[key2]?.length ?? 0) > 0);
1107
+ if (found.length > 0) fail(`Component has errors: "${found.join('", "')}"`, errors);
1108
+ return this;
1109
+ }
1110
+ assertRedirect(url) {
1111
+ const redirect = this.#effects.redirect;
1112
+ if (redirect === void 0) fail("Component did not perform a redirect.");
1113
+ if (url !== void 0 && redirect !== url) fail("Component redirected to an unexpected URL.", redirect, url);
1114
+ return this;
1115
+ }
1116
+ assertNoRedirect() {
1117
+ if (this.#effects.redirect !== void 0) fail("Component performed a redirect.", this.#effects.redirect);
1118
+ return this;
1119
+ }
1120
+ /** The last call returned `value`. */
1121
+ assertReturned(value) {
1122
+ const returned = this.#effects.returns?.at(-1);
1123
+ if (!isDeepStrictEqual(returned, value)) fail("Failed asserting that the method returned the expected value.", returned, value);
1124
+ return this;
1125
+ }
1126
+ };
1127
+
1128
+ // embed.ts
1129
+ async function live(component, params = {}, key2) {
1130
+ const html = await inject5(LiveManager3).mount(component, params, key2 === void 0 ? void 0 : String(key2));
1131
+ return { toHtml: () => html };
1132
+ }
1133
+ function LiveComponent({ component, "o:key": key2, children: _children, ...params }) {
1134
+ return live(component, params, key2);
1135
+ }
1136
+ var Live = Object.assign(LiveComponent, {
1137
+ /** Registers a component under a name (components in app/Live are registered at boot). */
1138
+ component(name, component) {
1139
+ inject5(LiveManager3).register(component, name);
1140
+ },
1141
+ /** Livewire::test(): mounts `component` for assertions. */
1142
+ test(component, params = {}) {
1143
+ return Testable.create(component, params);
1144
+ }
1145
+ });
1146
+
1147
+ // LiveComponentManager.ts
1148
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
1149
+ import { randomBytes } from "crypto";
1150
+ import {
1151
+ ClassRegistry,
1152
+ ConfigRepository as ConfigRepository4,
1153
+ CurrentRoute as CurrentRoute2,
1154
+ Encrypter as Encrypter2,
1155
+ HttpRequest,
1156
+ Redirector,
1157
+ Router,
1158
+ Session as Session2,
1159
+ ViewFactory as ViewFactory2
1160
+ } from "@orieljs/contracts";
1161
+ import { currentContainer as currentContainer3 } from "@orieljs/container";
1162
+ import { Str as Str4 } from "@orieljs/support";
1163
+
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;
1172
+ }
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();
1182
+ }
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
+ }
1198
+ }
1199
+
1200
+ // naming.ts
1201
+ import { existsSync, readdirSync } from "fs";
1202
+ import { join, relative, sep } from "path";
1203
+ import { pathToFileURL } from "url";
1204
+ import { Str as Str3 } from "@orieljs/support";
1205
+ function componentName(file) {
1206
+ return file.replace(/\.(tsx|ts|jsx|js|mts|mjs)$/, "").split(/[\\/]/).map((segment) => Str3.kebab(segment)).join(".");
1207
+ }
1208
+ function defaultName(cls) {
1209
+ const name = cls.name;
1210
+ return typeof name === "string" && name !== "" ? Str3.kebab(name) : void 0;
1211
+ }
1212
+ function componentFiles(directory) {
1213
+ if (!existsSync(directory)) return [];
1214
+ return readdirSync(directory, { recursive: true, encoding: "utf8" }).filter((file) => /\.(tsx|ts|jsx|js|mts|mjs)$/.test(file) && !/\.(test|spec|d)\.[^.]+$/.test(file)).map((file) => file.split(sep).join("/")).sort();
1215
+ }
1216
+ async function discover(directory) {
1217
+ const found = [];
1218
+ for (const file of componentFiles(directory)) {
1219
+ const module = await import(
1220
+ /* @vite-ignore */
1221
+ pathToFileURL(join(directory, file)).href
1222
+ );
1223
+ found.push([componentName(relative(directory, join(directory, file))), module.default]);
1224
+ }
1225
+ return found;
1226
+ }
1227
+
1228
+ // persistent.ts
1229
+ import { CurrentRoute } from "@orieljs/contracts";
1230
+ var PERSISTENT_MIDDLEWARE = ["auth", "verified", "guest", "can"];
1231
+ var ROUTER_METHODS = ["getRoutesByMatchOrder", "getPatterns", "getPatternVersion", "getMiddleware", "getMiddlewareGroups", "substituteBindings"];
1232
+ function canMatch(router) {
1233
+ const methods = router;
1234
+ return ROUTER_METHODS.every((name) => typeof methods[name] === "function");
1235
+ }
1236
+ function isMatchingRoute(route) {
1237
+ const candidate = route;
1238
+ return typeof candidate === "object" && candidate !== null && Array.isArray(candidate.methods) && ["match", "gatherMiddleware", "excludedMiddleware"].every((name) => typeof candidate[name] === "function");
1239
+ }
1240
+ function routePath(path) {
1241
+ const trimmed = path.replace(/^\/+|\/+$/g, "").replace(/\/{2,}/g, "/");
1242
+ return trimmed === "" ? "" : `/${trimmed}`;
1243
+ }
1244
+ function expand(references, aliases, groups, out = [], visiting = /* @__PURE__ */ new Set()) {
1245
+ for (const reference of references) {
1246
+ if (typeof reference === "function") {
1247
+ out.push({ cls: reference, parameters: [] });
1248
+ continue;
1249
+ }
1250
+ const index = reference.indexOf(":");
1251
+ const name = index === -1 ? reference : reference.slice(0, index);
1252
+ if (index === -1 && Object.hasOwn(groups, name)) {
1253
+ if (visiting.has(name)) continue;
1254
+ visiting.add(name);
1255
+ expand(groups[name], aliases, groups, out, visiting);
1256
+ visiting.delete(name);
1257
+ } else if (Object.hasOwn(aliases, name)) {
1258
+ out.push({ cls: aliases[name], parameters: index === -1 ? [] : reference.slice(index + 1).split(",") });
1259
+ }
1260
+ }
1261
+ return out;
1262
+ }
1263
+ function persistentPass(router, persistent, method, path) {
1264
+ if (!canMatch(router)) throw new LiveError("Live re-applies the page route's middleware to updates, which needs Oriel's router.");
1265
+ const routes = router.getRoutesByMatchOrder();
1266
+ if (!Array.isArray(routes) || !routes.every(isMatchingRoute)) throw new LiveError("Live re-applies the page route's middleware to updates, which needs Oriel's routes.");
1267
+ const verb = method.toUpperCase();
1268
+ const target = routePath(path);
1269
+ const patterns = router.getPatterns();
1270
+ const version = router.getPatternVersion();
1271
+ let matched;
1272
+ for (const candidate of routes) {
1273
+ const values2 = candidate.methods.includes(verb) ? candidate.match(target, patterns, version) : void 0;
1274
+ if (values2 !== void 0) {
1275
+ matched = { route: candidate, values: values2 };
1276
+ break;
1277
+ }
1278
+ }
1279
+ if (matched === void 0) return void 0;
1280
+ const { route, values } = matched;
1281
+ const aliases = router.getMiddleware();
1282
+ const groups = router.getMiddlewareGroups();
1283
+ const wanted = new Set(persistent.flatMap((name) => Object.hasOwn(aliases, name) ? [aliases[name]] : []));
1284
+ const excluded = new Set(expand(route.excludedMiddleware(), aliases, groups).map((entry) => entry.cls));
1285
+ const ids = /* @__PURE__ */ new Map();
1286
+ const seen = /* @__PURE__ */ new Set();
1287
+ const middleware = expand(route.gatherMiddleware(), aliases, groups).filter((entry) => {
1288
+ if (!ids.has(entry.cls)) ids.set(entry.cls, ids.size);
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);
1292
+ return true;
1293
+ });
1294
+ const early = new Set(expand(["web", "api"], aliases, groups).map((entry) => entry.cls));
1295
+ for (const name of ["auth", "guest"]) if (Object.hasOwn(aliases, name)) early.add(aliases[name]);
1296
+ const before = middleware.filter((entry) => early.has(entry.cls));
1297
+ const after = middleware.filter((entry) => !early.has(entry.cls));
1298
+ return { router, route, values, middleware: [...before, ...after], at: before.length };
1299
+ }
1300
+ function persistentMiddleware(router, persistent, method, path) {
1301
+ return [...persistentPass(router, persistent, method, path)?.middleware ?? []];
1302
+ }
1303
+ function currentRoute(container2) {
1304
+ if (!container2.bound(CurrentRoute)) return void 0;
1305
+ try {
1306
+ return container2.make(CurrentRoute);
1307
+ } catch {
1308
+ return void 0;
1309
+ }
1310
+ }
1311
+ async function throughMiddleware(container2, request, pass, destination) {
1312
+ const original = currentRoute(container2);
1313
+ const page = { route: pass.route, parameters: { ...pass.values }, middleware: [] };
1314
+ let swapped = false;
1315
+ const show = () => {
1316
+ container2.instance(CurrentRoute, page);
1317
+ swapped = true;
1318
+ };
1319
+ const restore = () => {
1320
+ if (!swapped) return;
1321
+ if (original === void 0) container2.forgetInstance(CurrentRoute);
1322
+ else container2.instance(CurrentRoute, original);
1323
+ swapped = false;
1324
+ };
1325
+ let ran;
1326
+ const run = async (index) => {
1327
+ if (index === pass.at) Object.assign(page.parameters, await pass.router.substituteBindings(pass.route, pass.values, container2));
1328
+ if (index === pass.middleware.length) {
1329
+ restore();
1330
+ ran = { result: await destination() };
1331
+ return new Response(null, { status: 204 });
1332
+ }
1333
+ const { cls, parameters } = pass.middleware[index];
1334
+ const instance = container2.make(cls);
1335
+ const next = () => run(index + 1);
1336
+ return instance.handle(request, next, ...parameters);
1337
+ };
1338
+ show();
1339
+ try {
1340
+ const response = await run(0);
1341
+ return ran ? ran.result : response;
1342
+ } finally {
1343
+ restore();
1344
+ }
1345
+ }
1346
+
1347
+ // WithPagination.ts
1348
+ import { AsyncLocalStorage } from "async_hooks";
1349
+ var PAGINATED = /* @__PURE__ */ Symbol.for("oriel:live.pagination");
1350
+ function toPage(value) {
1351
+ const page = typeof value === "number" ? value : typeof value === "string" && value.trim() !== "" ? Number(value) : Number.NaN;
1352
+ return Number.isFinite(page) && page >= 1 ? Math.min(Math.floor(page), Number.MAX_SAFE_INTEGER) : 1;
1353
+ }
1354
+ async function hook(instance, name, ...args) {
1355
+ const method = instance[name];
1356
+ if (typeof method === "function") await method.apply(instance, args);
1357
+ }
1358
+ function WithPagination(Base) {
1359
+ class WithPaginationMixin extends Base {
1360
+ page = 1;
1361
+ nextPage() {
1362
+ return this.setPage(toPage(this.page) + 1);
1363
+ }
1364
+ previousPage() {
1365
+ return this.setPage(Math.max(toPage(this.page) - 1, 1));
1366
+ }
1367
+ gotoPage(page) {
1368
+ return this.setPage(page);
1369
+ }
1370
+ resetPage() {
1371
+ return this.setPage(1);
1372
+ }
1373
+ async setPage(page) {
1374
+ const next = toPage(page);
1375
+ await hook(this, "updatingPage", next);
1376
+ this.page = next;
1377
+ await hook(this, "updatedPage", next);
1378
+ }
1379
+ }
1380
+ Object.defineProperty(WithPaginationMixin.prototype, PAGINATED, { value: true });
1381
+ return WithPaginationMixin;
1382
+ }
1383
+ function isPaginated(component) {
1384
+ return component[PAGINATED] === true;
1385
+ }
1386
+ var rendering = new AsyncLocalStorage();
1387
+ function renderPaginated(component, url, run) {
1388
+ return rendering.run(isPaginated(component) ? { component, url } : void 0, run);
1389
+ }
1390
+ function outsidePagination(run) {
1391
+ return rendering.run(void 0, run);
1392
+ }
1393
+ var livePaginationState = {
1394
+ currentPage(pageName) {
1395
+ const current = rendering.getStore();
1396
+ if (current === void 0 || pageName !== "page") return void 0;
1397
+ const page = toPage(current.component.page);
1398
+ current.component.page = page;
1399
+ return page;
1400
+ },
1401
+ currentPath() {
1402
+ return rendering.getStore()?.url;
1403
+ }
1404
+ };
1405
+
1406
+ // LiveComponentManager.ts
1407
+ var renderContext = new AsyncLocalStorage2();
1408
+ var loading = /* @__PURE__ */ new WeakMap();
1409
+ var pageRequest = new AsyncLocalStorage2();
1410
+ function optional(container2, token2) {
1411
+ if (!container2.bound(token2)) return void 0;
1412
+ try {
1413
+ return container2.make(token2);
1414
+ } catch {
1415
+ return void 0;
1416
+ }
1417
+ }
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 });
1421
+ }
1422
+ async function hook2(instance, name, ...args) {
1423
+ const method = instance[name];
1424
+ if (typeof method === "function") await method.apply(instance, args);
1425
+ }
1426
+ var LiveComponentManager = class {
1427
+ constructor(app) {
1428
+ this.app = app;
1429
+ }
1430
+ app;
1431
+ /** The request scope when there is one (sessions, the current route), else the application. */
1432
+ container() {
1433
+ return currentContainer3() ?? this.app;
1434
+ }
1435
+ registry() {
1436
+ return this.container().make(ClassRegistry);
1437
+ }
1438
+ /** Live.component('counter', Counter): registers a component under a name the snapshots use. */
1439
+ register(component, name) {
1440
+ if (!isComponentClass(component)) throw new LiveError(`[${component.name}] is not a Live component: it must extend Component.`);
1441
+ const resolved = name ?? defaultName(component);
1442
+ if (resolved === void 0) throw new LiveError("Anonymous component classes need a name: Live.component(name, Class).");
1443
+ this.registry().register(component, resolved);
1444
+ }
1445
+ /** The registered name of a component class, registering it under its kebab-case class name the first time. */
1446
+ nameOf(component) {
1447
+ const registry = this.registry();
1448
+ const existing = registry.nameOf(component);
1449
+ if (existing !== void 0) return existing;
1450
+ this.register(component);
1451
+ return registry.nameOf(component);
1452
+ }
1453
+ resolve(name) {
1454
+ const registry = this.registry();
1455
+ if (!registry.has(name)) throw new LiveComponentNotFoundError(name);
1456
+ const cls = registry.resolve(name);
1457
+ if (!isComponentClass(cls)) throw new LiveComponentNotFoundError(name);
1458
+ return cls;
1459
+ }
1460
+ isLiveRequest(request) {
1461
+ return request.header("X-Oriel-Live") === "true";
1462
+ }
1463
+ /** The first render: HTML with o:id, o:snapshot and o:effects on the root element. */
1464
+ async mount(component, params = {}, key2) {
1465
+ const context = renderContext.getStore();
1466
+ if (context === void 0) return (await this.mountComponent(component, params)).html;
1467
+ const childKey = key2 ?? `lw-${context.index++}`;
1468
+ const existing = Object.hasOwn(context.previous, childKey) ? context.previous[childKey] : void 0;
1469
+ if (existing !== void 0) {
1470
+ setOwn(context.children, childKey, existing);
1471
+ return `<${existing[0]} o:id="${escapeAttribute(existing[1])}"></${existing[0]}>`;
1472
+ }
1473
+ const result = await outsidePagination(() => this.mountComponent(component, params));
1474
+ setOwn(context.children, childKey, [result.tag, result.snapshot.memo.id]);
1475
+ return result.html;
1476
+ }
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) {
1482
+ const cls = typeof component === "string" ? this.resolve(component) : component;
1483
+ if (!isComponentClass(cls)) throw new LiveError(`[${cls.name}] is not a Live component: it must extend Component.`);
1484
+ const name = this.nameOf(cls);
1485
+ const instance = existing ?? this.container().make(cls);
1486
+ const state = stateOf(instance);
1487
+ state.id = randomBytes(15).toString("base64url");
1488
+ state.name = name;
1489
+ const { lazy: _lazy, ...params } = embedParams;
1490
+ const lazy = lazyMode(cls, embedParams);
1491
+ await hook2(instance, "boot");
1492
+ if (lazy !== void 0) return this.mountPlaceholder(instance, params, lazy);
1493
+ for (const [property, value] of Object.entries(params)) {
1494
+ if (Object.hasOwn(instance, property)) instance[property] = value;
1495
+ }
1496
+ this.prepareComputed(instance);
1497
+ const request = optional(this.container(), HttpRequest);
1498
+ const page = pageRequest.getStore() ?? { path: request?.path() ?? "", method: request?.method() ?? "GET" };
1499
+ await renderPaginated(instance, this.pageUrl(page.path), () => hook2(instance, "mount", params));
1500
+ const memo = { id: state.id, name, path: page.path, method: page.method, children: {}, errors: {} };
1501
+ const { html, tag } = await this.render(instance, memo, {});
1502
+ const effects = this.effects(instance);
1503
+ const listeners = Object.keys(listenersOf(cls));
1504
+ if (listeners.length > 0) effects.listeners = listeners;
1505
+ const snapshot = await this.snapshot(instance, memo);
1506
+ const attributes = { "o:snapshot": JSON.stringify(snapshot), "o:effects": JSON.stringify(effects), "o:id": state.id };
1507
+ return { instance, snapshot, effects, html: insertAttributesIntoRoot(html, attributes), tag };
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
+ }
1533
+ /** Applies each component's updates and calls, re-renders it and returns its new snapshot and effects. */
1534
+ async update(request) {
1535
+ const components = [];
1536
+ for (const component of request.components) {
1537
+ const { memo } = this.verify(component.snapshot);
1538
+ const result = await pageRequest.run({ path: memo.path, method: memo.method }, () => this.updateComponent(component.snapshot, component.updates, component.calls));
1539
+ components.push({ snapshot: JSON.stringify(result.snapshot), effects: result.effects });
1540
+ }
1541
+ return { components };
1542
+ }
1543
+ async updateComponent(snapshotJson, updates, calls) {
1544
+ const snapshot = this.verify(snapshotJson);
1545
+ const load = await this.lazyLoad(snapshot.memo, updates, calls);
1546
+ const cls = this.resolve(snapshot.memo.name);
1547
+ const instance = this.container().make(cls);
1548
+ const state = stateOf(instance);
1549
+ state.id = snapshot.memo.id;
1550
+ state.name = snapshot.memo.name;
1551
+ state.errors = { ...snapshot.memo.errors };
1552
+ await hook2(instance, "boot");
1553
+ const registry = this.registry();
1554
+ for (const [property, value] of Object.entries(snapshot.data)) {
1555
+ Object.defineProperty(instance, property, { value: await hydrate(value, registry), enumerable: true, writable: true, configurable: true });
1556
+ }
1557
+ this.prepareComputed(instance);
1558
+ const returns = [];
1559
+ await renderPaginated(instance, this.pageUrl(snapshot.memo.path), async () => {
1560
+ if (load === void 0) await hook2(instance, "hydrate");
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
+ }
1570
+ for (const [path, value] of Object.entries(updates)) await this.applyUpdate(instance, path, value);
1571
+ for (const call of load === void 0 ? calls : []) returns.push(await this.call(instance, call.method, call.params));
1572
+ } catch (error) {
1573
+ const errors = error.errors;
1574
+ if (error.kind !== "Validation" || !isRecord2(errors)) throw error;
1575
+ state.errors = { ...errors };
1576
+ }
1577
+ });
1578
+ const memo = { ...snapshot.memo, children: { ...snapshot.memo.children } };
1579
+ const effects = this.effects(instance);
1580
+ let html;
1581
+ let tag;
1582
+ if (!state.skipRender) {
1583
+ ;
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);
1587
+ html = insertAttributesIntoRoot(html, { "o:id": state.id });
1588
+ effects.html = html;
1589
+ }
1590
+ if (calls.length > 0) effects.returns = returns;
1591
+ return { instance, snapshot: await this.snapshot(instance, memo), effects, html, tag };
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
+ }
1603
+ /**
1604
+ * The route action of POST /oriel/live/update. Each component's update runs behind the persistent middleware of the
1605
+ * route that rendered its page (Livewire's): `auth`, `verified`, `guest` and `can`, with the page route's parameters
1606
+ * bound again. A signed-out tab gets a 401, a revoked ability a 403 and a deleted model a 404, before the component
1607
+ * hydrates. When a middleware answers instead (a redirect), that is the response. A key first flashed during the
1608
+ * update shows in this response only, unless the update redirects.
1609
+ */
1610
+ async handle(request) {
1611
+ if (!this.isLiveRequest(request)) throw new InvalidLiveRequestError("Live update requests need the X-Oriel-Live header.");
1612
+ const container2 = this.container();
1613
+ const persistent = optional(container2, ConfigRepository4)?.get("live.persistent_middleware", PERSISTENT_MIDDLEWARE) ?? PERSISTENT_MIDDLEWARE;
1614
+ const components = [];
1615
+ const flashed = trackFlash(optional(container2, Session2));
1616
+ for (const component of this.payload(request).components) {
1617
+ const { memo } = this.verify(component.snapshot);
1618
+ const pass = persistentPass(container2.make(Router), persistent, memo.method, memo.path);
1619
+ const update = () => this.updateComponent(component.snapshot, component.updates, component.calls);
1620
+ const result = await pageRequest.run(
1621
+ { path: memo.path, method: memo.method },
1622
+ () => pass === void 0 || pass.middleware.length === 0 ? update() : throughMiddleware(container2, request, pass, update)
1623
+ );
1624
+ if (result instanceof Response) return result;
1625
+ components.push({ snapshot: JSON.stringify(result.snapshot), effects: result.effects });
1626
+ }
1627
+ flashed(components.some((component) => component.effects.redirect !== void 0));
1628
+ return { components };
1629
+ }
1630
+ /** A full-page component (Route.get('/counter', Counter)): mounted with the route parameters, inside the layout. */
1631
+ async page(component) {
1632
+ const container2 = this.container();
1633
+ const params = { ...optional(container2, CurrentRoute2)?.parameters ?? {} };
1634
+ const cls = component.constructor;
1635
+ const result = await this.mountComponent(cls, params, component);
1636
+ const redirect = result.effects.redirect;
1637
+ if (redirect !== void 0) {
1638
+ return optional(container2, Redirector)?.to(redirect) ?? new Response(null, { status: 302, headers: { Location: redirect } });
1639
+ }
1640
+ const layout = cls.layout ?? optional(container2, ConfigRepository4)?.get("live.layout") ?? "components.layouts.app";
1641
+ const html = result.html;
1642
+ const data = { children: { toHtml: () => html } };
1643
+ if (cls.title !== void 0) data.title = cls.title;
1644
+ return container2.make(ViewFactory2).make(layout, data);
1645
+ }
1646
+ /** Parses and checks a snapshot: a bad checksum throws before anything is hydrated. */
1647
+ verify(snapshotJson) {
1648
+ let snapshot;
1649
+ try {
1650
+ snapshot = typeof snapshotJson === "string" ? JSON.parse(snapshotJson) : void 0;
1651
+ } catch {
1652
+ throw new CorruptComponentPayloadError();
1653
+ }
1654
+ if (!isRecord2(snapshot) || !isRecord2(snapshot.data) || !isRecord2(snapshot.memo)) throw new CorruptComponentPayloadError();
1655
+ const memo = snapshot.memo;
1656
+ const name = typeof memo.name === "string" ? memo.name : "unknown";
1657
+ const key2 = this.key();
1658
+ let valid;
1659
+ try {
1660
+ valid = verifyChecksum(key2, { data: snapshot.data, memo: snapshot.memo }, snapshot.checksum);
1661
+ } catch {
1662
+ valid = false;
1663
+ }
1664
+ if (!valid) throw new CorruptComponentPayloadError(name);
1665
+ return snapshot;
1666
+ }
1667
+ key() {
1668
+ return this.container().make(Encrypter2).getKey();
1669
+ }
1670
+ /** The request body, read from the raw JSON when the request has it, so TrimStrings can't alter typed values. */
1671
+ payload(request) {
1672
+ let body;
1673
+ try {
1674
+ const content = request.content;
1675
+ body = typeof content === "function" ? JSON.parse(content.call(request)) : request.all();
1676
+ } catch {
1677
+ throw new InvalidLiveRequestError("The Live update request body is not valid JSON.");
1678
+ }
1679
+ const components = isRecord2(body) ? body.components : void 0;
1680
+ const valid = Array.isArray(components) && components.every(
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))
1682
+ );
1683
+ if (!valid) throw new InvalidLiveRequestError("The Live update request body is malformed.");
1684
+ return body;
1685
+ }
1686
+ /** Makes every computed getter (static `computed` too) cache its value for the rest of the request. */
1687
+ prepareComputed(instance) {
1688
+ for (const name of computedOf(instance.constructor)) {
1689
+ for (let proto = Object.getPrototypeOf(instance); proto !== null; proto = Object.getPrototypeOf(proto)) {
1690
+ const getter = Object.getOwnPropertyDescriptor(proto, name)?.get;
1691
+ if (getter === void 0) continue;
1692
+ Object.defineProperty(instance, name, { get: cachedGetter(name, getter), enumerable: false, configurable: true });
1693
+ break;
1694
+ }
1695
+ }
1696
+ }
1697
+ async applyUpdate(instance, path, value) {
1698
+ if (!isUpdatable(instance, path)) throw new PropertyNotUpdatableError(path, stateOf(instance).name);
1699
+ const [root, ...rest] = path.split(".");
1700
+ const key2 = rest.length > 0 ? rest.join(".") : void 0;
1701
+ const studly = Str4.studly(root);
1702
+ await hook2(instance, "updating", path, value);
1703
+ await hook2(instance, `updating${studly}`, value, key2);
1704
+ const target = instance;
1705
+ if (rest.length === 0) target[root] = value;
1706
+ else {
1707
+ const assertSlot = (node2, segment) => {
1708
+ if (Array.isArray(node2) && !isArraySlot(node2, segment)) throw new PropertyNotUpdatableError(path, stateOf(instance).name);
1709
+ };
1710
+ let node = target[root];
1711
+ if (!isRecord2(node) && !Array.isArray(node)) target[root] = node = {};
1712
+ for (const segment of rest.slice(0, -1)) {
1713
+ assertSlot(node, segment);
1714
+ const next = node[segment];
1715
+ node = isRecord2(next) || Array.isArray(next) ? next : node[segment] = {};
1716
+ }
1717
+ const last = rest.at(-1);
1718
+ assertSlot(node, last);
1719
+ Object.defineProperty(node, last, { value, enumerable: true, writable: true, configurable: true });
1720
+ }
1721
+ await hook2(instance, "updated", path, value);
1722
+ await hook2(instance, `updated${studly}`, value, key2);
1723
+ }
1724
+ /** Runs one call from the browser: a magic action, an event, an upload call, or a public method. Returns its return value. */
1725
+ async call(instance, method, params) {
1726
+ const name = stateOf(instance).name;
1727
+ switch (method) {
1728
+ case "$refresh":
1729
+ return void 0;
1730
+ case "$set":
1731
+ return this.applyUpdate(instance, String(params[0]), params[1]);
1732
+ case "$toggle": {
1733
+ const path = String(params[0]);
1734
+ const current = path.split(".").reduce((node, segment) => isRecord2(node) || Array.isArray(node) ? node[segment] : void 0, instance);
1735
+ return this.applyUpdate(instance, path, !current);
1736
+ }
1737
+ case "__dispatch": {
1738
+ const [event, detail] = params;
1739
+ const listeners = listenersOf(instance.constructor);
1740
+ for (const listener of Object.hasOwn(listeners, String(event)) ? listeners[String(event)] : []) {
1741
+ if (!isCallable(instance, listener)) throw new MethodNotCallableError(listener, name);
1742
+ await instance[listener](isRecord2(detail) ? detail : {});
1743
+ }
1744
+ return void 0;
1745
+ }
1746
+ }
1747
+ if (UPLOAD_CALLS.has(method)) return runUploadCall(instance, method, params, (path, value) => this.applyUpdate(instance, path, value));
1748
+ if (!isCallable(instance, method)) throw new MethodNotCallableError(method, name);
1749
+ return instance[method](...params);
1750
+ }
1751
+ /**
1752
+ * Renders the component (rendering/rendered hooks, nested children tracked) and checks for a single root. A
1753
+ * WithPagination component's page and page URL are what paginate() reads while it renders, as in its mount() and in
1754
+ * its update's hooks and actions.
1755
+ */
1756
+ async render(instance, memo, previous) {
1757
+ await hook2(instance, "rendering");
1758
+ const context = { previous, children: {}, index: 0 };
1759
+ const html = await renderContext.run(
1760
+ context,
1761
+ () => renderPaginated(instance, this.pageUrl(memo.path), async () => {
1762
+ const output = await instance.render();
1763
+ return typeof output === "string" ? output : this.container().make(ViewFactory2).renderToString(output);
1764
+ })
1765
+ );
1766
+ await hook2(instance, "rendered", html);
1767
+ memo.children = context.children;
1768
+ return { html: html.trim(), tag: rootTag(html, memo.name) };
1769
+ }
1770
+ /** The absolute URL of the page a memo path belongs to, on the current request's origin ('/' + path without one). */
1771
+ pageUrl(path) {
1772
+ const relative3 = `/${path.replace(/^\/+/, "")}`;
1773
+ const url = optional(this.container(), HttpRequest)?.url();
1774
+ try {
1775
+ return url === void 0 ? relative3 : new URL(relative3, url).href;
1776
+ } catch {
1777
+ return relative3;
1778
+ }
1779
+ }
1780
+ /** Dispatches, redirects: the effects every response carries. */
1781
+ effects(instance) {
1782
+ const state = stateOf(instance);
1783
+ const effects = {};
1784
+ if (state.dispatches.length > 0) {
1785
+ effects.dispatches = state.dispatches.map(({ name, params, self, to }) => ({
1786
+ name,
1787
+ params,
1788
+ ...self ? { self } : {},
1789
+ ...to === void 0 ? {} : { to: typeof to === "string" ? to : this.nameOf(to) }
1790
+ }));
1791
+ }
1792
+ if (state.redirect !== void 0) effects.redirect = state.redirect;
1793
+ if (state.redirectUsingNavigate) effects.redirectUsingNavigate = true;
1794
+ return effects;
1795
+ }
1796
+ /** dehydrate() hook, then the state as snapshot data, the error bag, and the checksum over both. */
1797
+ async snapshot(instance, memo) {
1798
+ await hook2(instance, "dehydrate");
1799
+ const registry = this.registry();
1800
+ const data = {};
1801
+ for (const [property, value] of Object.entries(instance)) data[property] = dehydrate(value, registry, `${memo.name}.${property}`);
1802
+ const signed = { data, memo: { ...memo, errors: { ...stateOf(instance).errors } } };
1803
+ return { ...signed, checksum: checksum(this.key(), signed) };
1804
+ }
1805
+ };
1806
+
1807
+ // LiveServiceProvider.ts
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";
1810
+ import { ServiceProvider } from "@orieljs/support";
1811
+
1812
+ // commands.ts
1813
+ import { existsSync as existsSync2 } from "fs";
1814
+ import { mkdir, writeFile } from "fs/promises";
1815
+ import { dirname, join as join2, relative as relative2, sep as sep2 } from "path";
1816
+ import { ConfigRepository as ConfigRepository5 } from "@orieljs/contracts";
1817
+ import { Str as Str5 } from "@orieljs/support";
1818
+ var COMPONENT_STUB = `import { Component } from '@orieljs/framework'
1819
+
1820
+ export default class {{ class }} extends Component {
1821
+ render() {
1822
+ return <div></div>
1823
+ }
1824
+ }
1825
+ `;
1826
+ var TEST_STUB = `import { Live } from '@orieljs/framework'
1827
+ import { expect, test } from 'vitest'
1828
+ import {{ class }} from '{{ component }}'
1829
+ import { TestCase } from '{{ testCase }}'
1830
+
1831
+ TestCase.boot()
1832
+
1833
+ test('{{ name }} renders', async () => {
1834
+ const component = await Live.test({{ class }})
1835
+ expect(component.html()).toContain('<div')
1836
+ })
1837
+ `;
1838
+ var CLASS_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
1839
+ function importPath(from, to) {
1840
+ const path = relative2(dirname(from), to).split(sep2).join("/").replace(/\.tsx?$/, "");
1841
+ return path.startsWith(".") ? path : `./${path}`;
1842
+ }
1843
+ function registerLiveCommands(kernel, app) {
1844
+ kernel.command("make:live {name : The component name, e.g. Posts/CreatePost} {--test : Also create a test} {--force : Overwrite existing files}", async function() {
1845
+ const segments = String(this.argument("name") ?? "").split(/[/\\.]+/).filter((segment) => segment !== "").map((segment) => Str5.studly(segment));
1846
+ if (segments.length === 0 || !segments.every((segment) => CLASS_NAME.test(segment))) {
1847
+ this.error(`Component name [${String(this.argument("name"))}] is not valid: use names like Posts/CreatePost.`);
1848
+ return 1;
1849
+ }
1850
+ const className = segments.at(-1);
1851
+ const config = app.bound(ConfigRepository5) ? app.make(ConfigRepository5) : void 0;
1852
+ const directory = config?.get("live.class_path") ?? app.appPath("Live");
1853
+ const file = join2(directory, ...segments.slice(0, -1), `${className}.tsx`);
1854
+ const force = this.option("force") === true;
1855
+ if (existsSync2(file) && !force) {
1856
+ this.error(`Component [${relative2(app.basePath(), file)}] already exists.`);
1857
+ return 1;
1858
+ }
1859
+ await mkdir(dirname(file), { recursive: true });
1860
+ await writeFile(file, COMPONENT_STUB.replace("{{ class }}", () => className));
1861
+ this.info(`Component [${relative2(app.basePath(), file)}] created successfully.`);
1862
+ if (this.option("test") === true) {
1863
+ const testFile = app.basePath(join2("tests", "Feature", "Live", ...segments.slice(0, -1), `${className}.test.ts`));
1864
+ if (existsSync2(testFile) && !force) {
1865
+ this.error(`Test [${relative2(app.basePath(), testFile)}] already exists.`);
1866
+ return 1;
1867
+ }
1868
+ const name = segments.map((segment) => Str5.kebab(segment)).join(".");
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);
1870
+ await mkdir(dirname(testFile), { recursive: true });
1871
+ await writeFile(testFile, test);
1872
+ this.info(`Test [${relative2(app.basePath(), testFile)}] created successfully.`);
1873
+ }
1874
+ return 0;
1875
+ }).purpose("Create a new Live component");
1876
+ }
1877
+
1878
+ // LiveServiceProvider.ts
1879
+ var UPDATE_URI = "/oriel/live/update";
1880
+ var LiveServiceProvider = class extends ServiceProvider {
1881
+ register() {
1882
+ this.mergeConfigFrom({ class_path: this.app.appPath("Live"), layout: "components.layouts.app", persistent_middleware: [...PERSISTENT_MIDDLEWARE] }, "live");
1883
+ this.app.singleton(LiveComponentManager, () => new LiveComponentManager(this.app));
1884
+ this.app.singleton(LiveManager4, (container2) => container2.make(LiveComponentManager));
1885
+ this.app.singleton(PaginationState, () => livePaginationState);
1886
+ }
1887
+ async boot() {
1888
+ const manager = this.app.make(LiveComponentManager);
1889
+ if (this.app.bound(Router2)) {
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");
1894
+ }
1895
+ if (this.app.bound(ConsoleKernel)) registerLiveCommands(this.app.make(ConsoleKernel), this.app);
1896
+ const directory = this.app.make(ConfigRepository6).get("live.class_path", this.app.appPath("Live"));
1897
+ for (const [name, exported] of await this.app.run(() => discover(directory))) {
1898
+ if (isComponentClass(exported)) manager.register(exported, name);
1899
+ }
1900
+ }
1901
+ };
1902
+
1903
+ // describe.ts
1904
+ import { currentContainer as currentContainer4 } from "@orieljs/container";
1905
+ function describeComponent(cls) {
1906
+ const container2 = currentContainer4();
1907
+ const instance = container2 ? container2.make(cls) : new cls();
1908
+ const methods = /* @__PURE__ */ new Set();
1909
+ const isBase = (proto) => proto === Component.prototype || proto === Object.prototype;
1910
+ for (let proto = Object.getPrototypeOf(instance); proto !== null && !isBase(proto); proto = Object.getPrototypeOf(proto)) {
1911
+ for (const name of Object.getOwnPropertyNames(proto)) methods.add(name);
1912
+ }
1913
+ return {
1914
+ actions: [...methods].filter((name) => isCallable(instance, name)).sort(),
1915
+ properties: Object.keys(instance).filter((name) => isUpdatable(instance, name)).sort()
1916
+ };
1917
+ }
1918
+
1919
+ // Pagination.ts
1920
+ function urlWindow(current, last, onEachSide) {
1921
+ const range = (from, to) => Array.from({ length: Math.max(to - from + 1, 0) }, (_, index) => from + index);
1922
+ if (last < onEachSide * 2 + 8) return range(1, last);
1923
+ const size = onEachSide + 4;
1924
+ if (current <= size) return [...range(1, size + onEachSide), "...", ...range(last - 1, last)];
1925
+ if (current > last - size) return [...range(1, 2), "...", ...range(last - (size + (onEachSide - 1)), last)];
1926
+ return [...range(1, 2), "...", ...range(current - onEachSide, current + onEachSide), "...", ...range(last - 1, last)];
1927
+ }
1928
+ function button(label, action, attributes = "") {
1929
+ const live2 = action === void 0 ? " disabled" : ` o:click="${escapeAttribute(action)}"`;
1930
+ return `<button type="button" class="btn btn-secondary"${attributes}${live2}>${escapeAttribute(label)}</button>`;
1931
+ }
1932
+ function Pagination({ paginator, class: className }) {
1933
+ if (typeof paginator.nextCursor === "function") {
1934
+ throw new LiveError("<Pagination> needs paginate() or simplePaginate(): a cursor paginator has no page numbers.");
1935
+ }
1936
+ const current = paginator.currentPage();
1937
+ const onFirst = paginator.onFirstPage();
1938
+ const hasMore = paginator.hasMorePages();
1939
+ if (onFirst && !hasMore) return { toHtml: () => "" };
1940
+ const parts = [button("\xAB Previous", onFirst ? void 0 : "previousPage")];
1941
+ if (typeof paginator.lastPage === "function") {
1942
+ const onEachSide = Number.isInteger(paginator.onEachSideCount) && paginator.onEachSideCount >= 0 ? paginator.onEachSideCount : 3;
1943
+ for (const page of urlWindow(current, paginator.lastPage(), onEachSide)) {
1944
+ if (page === "...") parts.push('<span aria-hidden="true">...</span>');
1945
+ else if (page === current) parts.push(`<button type="button" class="btn" aria-current="page" disabled>${page}</button>`);
1946
+ else parts.push(button(String(page), `gotoPage(${page})`, ` aria-label="Go to page ${page}"`));
1947
+ }
1948
+ }
1949
+ parts.push(button("Next \xBB", hasMore ? "nextPage" : void 0));
1950
+ const classes = className === void 0 || className === "" ? "" : ` class="${escapeAttribute(className)}"`;
1951
+ const html = `<nav aria-label="Pagination"${classes}>${parts.join("")}</nav>`;
1952
+ return { toHtml: () => html };
1953
+ }
1954
+ export {
1955
+ Component,
1956
+ Computed,
1957
+ CorruptComponentPayloadError,
1958
+ ErrorBag,
1959
+ FileNotPreviewableError,
1960
+ InvalidLiveRequestError,
1961
+ InvalidUploadSignatureError,
1962
+ Live,
1963
+ LiveComponentManager,
1964
+ LiveComponentNotFoundError,
1965
+ LiveError,
1966
+ LiveServiceProvider,
1967
+ Locked,
1968
+ MethodNotCallableError,
1969
+ On,
1970
+ PERSISTENT_MIDDLEWARE,
1971
+ PREVIEW_URI,
1972
+ Pagination,
1973
+ PendingEvent,
1974
+ PropertyNotUpdatableError,
1975
+ TEMPORARY_UPLOAD_DEFAULTS,
1976
+ TemporaryFileNotFoundError,
1977
+ TemporaryUploadedFile,
1978
+ TestRequestScope,
1979
+ Testable,
1980
+ UPDATE_URI,
1981
+ UPLOAD_URI,
1982
+ UploadValidationError,
1983
+ WithFileUploads,
1984
+ WithPagination,
1985
+ componentFiles,
1986
+ componentName,
1987
+ describeComponent,
1988
+ live,
1989
+ persistentMiddleware
1990
+ };
1991
+ //# sourceMappingURL=index.js.map