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