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