@orieljs/framework 0.1.0-alpha.1 → 0.1.0-alpha.3

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.
@@ -2,10 +2,11 @@ import {
2
2
  Arr,
3
3
  ClassRegistry as ClassRegistry2,
4
4
  Container,
5
+ ContainerNotAvailableError,
5
6
  Str,
6
7
  currentContainer,
7
8
  facade
8
- } from "./chunk-AI2RFMBT.js";
9
+ } from "./chunk-IRHK7GH7.js";
9
10
  import {
10
11
  Application,
11
12
  AuthFactory,
@@ -20,6 +21,7 @@ import {
20
21
  ExceptionHandler,
21
22
  FilesystemManager,
22
23
  HttpKernel,
24
+ HttpRequest,
23
25
  MailManager,
24
26
  MiddlewareCallbacks,
25
27
  NotificationDispatcher,
@@ -28,7 +30,7 @@ import {
28
30
  Session,
29
31
  UrlGenerator,
30
32
  ViewFactory
31
- } from "./chunk-K6MAA3RM.js";
33
+ } from "./chunk-3ZRPBRAE.js";
32
34
 
33
35
  // ../Config/dist/index.js
34
36
  import { existsSync, readFileSync } from "fs";
@@ -111,8 +113,11 @@ async function loadConfiguration(configPath) {
111
113
  }
112
114
 
113
115
  // ../Foundation/dist/index.js
116
+ import { join as join3 } from "path";
117
+ import { isAbsolute, join as join4 } from "path";
118
+ import { existsSync as existsSync3, readFileSync as readFileSync2, statSync } from "fs";
119
+ import { createHash, randomBytes } from "crypto";
114
120
  import { join as join22 } from "path";
115
- import { isAbsolute, join as join3 } from "path";
116
121
  var ApplicationBuilder = class {
117
122
  constructor(app) {
118
123
  this.app = app;
@@ -127,7 +132,7 @@ var ApplicationBuilder = class {
127
132
  return this;
128
133
  }
129
134
  withRouting(options) {
130
- const abs = (p) => p === void 0 ? void 0 : isAbsolute(p) ? p : join3(this.app.basePath(), p);
135
+ const abs = (p) => p === void 0 ? void 0 : isAbsolute(p) ? p : join4(this.app.basePath(), p);
131
136
  const paths = { apiPrefix: options.apiPrefix ?? "api" };
132
137
  if (options.web !== void 0) paths.web = abs(options.web);
133
138
  if (options.api !== void 0) paths.api = abs(options.api);
@@ -178,6 +183,280 @@ var ProviderRegisterError = class extends Error {
178
183
  provider;
179
184
  name = "ProviderRegisterError";
180
185
  };
186
+ var ViteError = class extends Error {
187
+ kind = "Server";
188
+ status = 500;
189
+ constructor(message) {
190
+ super(message);
191
+ this.name = "ViteError";
192
+ }
193
+ };
194
+ var ViteHtml = class {
195
+ constructor(html) {
196
+ this.html = html;
197
+ }
198
+ html;
199
+ toHtml() {
200
+ return this.html;
201
+ }
202
+ toString() {
203
+ return this.html;
204
+ }
205
+ };
206
+ var CSS_PATH = /\.(css|less|sass|scss|styl|stylus|pcss|postcss)(\?[^.]*)?$/;
207
+ var manifests = /* @__PURE__ */ new Map();
208
+ var escapeAttribute = (value) => value.replace(/&/g, "&").replace(/"/g, """);
209
+ var Vite = class {
210
+ constructor(app) {
211
+ this.app = app;
212
+ }
213
+ app;
214
+ #hotFile;
215
+ #buildDirectory = "build";
216
+ #manifestFilename = "manifest.json";
217
+ #nonce;
218
+ #integrityKey = "integrity";
219
+ #entryPoints = [];
220
+ #scriptResolvers = [];
221
+ #styleResolvers = [];
222
+ #preloadResolvers = [];
223
+ #assetPathResolver;
224
+ #preloaded = {};
225
+ // Configuration --------------------------------------------------------------------------------------------
226
+ useCspNonce(nonce) {
227
+ this.#nonce = nonce ?? randomBytes(30).toString("base64url").slice(0, 40);
228
+ return this.#nonce;
229
+ }
230
+ cspNonce() {
231
+ return this.#nonce;
232
+ }
233
+ useIntegrityKey(key) {
234
+ this.#integrityKey = key;
235
+ return this;
236
+ }
237
+ withEntryPoints(entryPoints) {
238
+ this.#entryPoints = entryPoints;
239
+ return this;
240
+ }
241
+ useHotFile(path) {
242
+ this.#hotFile = path;
243
+ return this;
244
+ }
245
+ useBuildDirectory(path) {
246
+ this.#buildDirectory = path;
247
+ return this;
248
+ }
249
+ useManifestFilename(filename) {
250
+ this.#manifestFilename = filename;
251
+ return this;
252
+ }
253
+ useScriptTagAttributes(attributes) {
254
+ this.#scriptResolvers.push(typeof attributes === "function" ? attributes : () => attributes);
255
+ return this;
256
+ }
257
+ useStyleTagAttributes(attributes) {
258
+ this.#styleResolvers.push(typeof attributes === "function" ? attributes : () => attributes);
259
+ return this;
260
+ }
261
+ /** Extra preload attributes; a resolver returning false drops that preload. */
262
+ usePreloadTagAttributes(attributes) {
263
+ this.#preloadResolvers.push(typeof attributes === "function" ? attributes : () => attributes);
264
+ return this;
265
+ }
266
+ createAssetPathsUsing(resolver) {
267
+ this.#assetPathResolver = resolver;
268
+ return this;
269
+ }
270
+ // Dev server -----------------------------------------------------------------------------------------------
271
+ hotFile() {
272
+ return this.#hotFile ?? this.app.publicPath("hot");
273
+ }
274
+ isRunningHot() {
275
+ return existsSync3(this.hotFile());
276
+ }
277
+ /** The dev server URL written by @orieljs/vite, or undefined when it isn't running. */
278
+ devServerUrl() {
279
+ return this.isRunningHot() ? readFileSync2(this.hotFile(), "utf8").trimEnd() : void 0;
280
+ }
281
+ // Tags -----------------------------------------------------------------------------------------------------
282
+ /** Laravel's @vite([...]): preloads (styles first), then stylesheets, then scripts. */
283
+ tags(entryPoints, buildDirectory = this.#buildDirectory) {
284
+ const entries = [entryPoints].flat();
285
+ if (this.isRunningHot()) {
286
+ return new ViteHtml(["@vite/client", ...entries].map((entry) => this.#tagForChunk(entry, this.#hotAsset(entry), void 0, void 0)).join(""));
287
+ }
288
+ const manifest = this.manifest(buildDirectory);
289
+ const tags = [];
290
+ const preloads = [];
291
+ const cssChunk = (css) => {
292
+ const found = Object.entries(manifest).find(([, chunk2]) => chunk2.file === css);
293
+ return found === void 0 ? [css, void 0] : [found[0], found[1]];
294
+ };
295
+ for (const entry of entries) {
296
+ const chunk2 = this.#chunk(manifest, entry);
297
+ preloads.push([chunk2.src ?? entry, this.#assetPath(`${buildDirectory}/${chunk2.file}`), chunk2]);
298
+ for (const name of this.#resolveImports(manifest, chunk2)) {
299
+ const imported = manifest[name];
300
+ preloads.push([name, this.#assetPath(`${buildDirectory}/${imported.file}`), imported]);
301
+ for (const css of imported.css ?? []) {
302
+ const [key, partial] = cssChunk(css);
303
+ preloads.push([key, this.#assetPath(`${buildDirectory}/${css}`), partial]);
304
+ tags.push(this.#tagForChunk(key, this.#assetPath(`${buildDirectory}/${css}`), partial, manifest));
305
+ }
306
+ }
307
+ tags.push(this.#tagForChunk(entry, this.#assetPath(`${buildDirectory}/${chunk2.file}`), chunk2, manifest));
308
+ for (const css of chunk2.css ?? []) {
309
+ const [key, partial] = cssChunk(css);
310
+ preloads.push([key, this.#assetPath(`${buildDirectory}/${css}`), partial]);
311
+ tags.push(this.#tagForChunk(key, this.#assetPath(`${buildDirectory}/${css}`), partial, manifest));
312
+ }
313
+ }
314
+ const uniqueTags = [...new Set(tags)];
315
+ const stylesheets = uniqueTags.filter((tag) => tag.startsWith("<link"));
316
+ const scripts = uniqueTags.filter((tag) => !tag.startsWith("<link"));
317
+ const seen = /* @__PURE__ */ new Set();
318
+ const uniquePreloads = preloads.filter(([, url]) => seen.has(url) ? false : (seen.add(url), true));
319
+ const sorted = [...uniquePreloads.filter(([, url]) => CSS_PATH.test(url)), ...uniquePreloads.filter(([, url]) => !CSS_PATH.test(url))];
320
+ const preloadTags = sorted.map(([src, url, chunk2]) => this.#preloadTag(src, url, chunk2, manifest));
321
+ return new ViteHtml(preloadTags.join("") + stylesheets.join("") + scripts.join(""));
322
+ }
323
+ /** Laravel's @viteReactRefresh: the React Refresh preamble while the dev server runs; nothing otherwise. */
324
+ reactRefresh() {
325
+ if (!this.isRunningHot()) return void 0;
326
+ const attributes = this.#parseAttributes({ nonce: this.#nonce ?? false });
327
+ return new ViteHtml(
328
+ [
329
+ `<script type="module" ${attributes.join(" ")}>`,
330
+ ` import RefreshRuntime from '${this.#hotAsset("@react-refresh")}'`,
331
+ " RefreshRuntime.injectIntoGlobalHook(window)",
332
+ " window.$RefreshReg$ = () => {}",
333
+ " window.$RefreshSig$ = () => (type) => type",
334
+ " window.__vite_plugin_react_preamble_installed__ = true",
335
+ "</script>"
336
+ ].join("\n")
337
+ );
338
+ }
339
+ /** The URL of a processed asset (Vite::asset('resources/images/logo.svg')). */
340
+ asset(path, buildDirectory = this.#buildDirectory) {
341
+ if (this.isRunningHot()) return this.#hotAsset(path);
342
+ return this.#assetPath(`${buildDirectory}/${this.#chunk(this.manifest(buildDirectory), path).file}`);
343
+ }
344
+ /** The built file's contents (Vite::content()). */
345
+ content(path, buildDirectory = this.#buildDirectory) {
346
+ const chunk2 = this.#chunk(this.manifest(buildDirectory), path);
347
+ const file = this.app.publicPath(join22(buildDirectory, chunk2.file));
348
+ if (!existsSync3(file)) throw new ViteError(`Unable to locate file from Vite manifest: ${file}.`);
349
+ return readFileSync2(file, "utf8");
350
+ }
351
+ /** md5 of the manifest (a cache-busting asset version); undefined while hot or without a build. */
352
+ manifestHash(buildDirectory = this.#buildDirectory) {
353
+ if (this.isRunningHot()) return void 0;
354
+ const path = this.manifestPath(buildDirectory);
355
+ return existsSync3(path) ? createHash("md5").update(readFileSync2(path)).digest("hex") : void 0;
356
+ }
357
+ manifestPath(buildDirectory = this.#buildDirectory) {
358
+ return this.app.publicPath(join22(buildDirectory, this.#manifestFilename));
359
+ }
360
+ manifest(buildDirectory = this.#buildDirectory) {
361
+ const path = this.manifestPath(buildDirectory);
362
+ if (!existsSync3(path)) throw new ViteError(`Vite manifest not found at: ${path}`);
363
+ const { mtimeMs } = statSync(path);
364
+ const cached = manifests.get(path);
365
+ if (cached !== void 0 && cached.mtimeMs === mtimeMs) return cached.manifest;
366
+ const manifest = JSON.parse(readFileSync2(path, "utf8"));
367
+ manifests.set(path, { mtimeMs, manifest });
368
+ return manifest;
369
+ }
370
+ /** URL → the attributes of the preload tags rendered so far (without href), for Link headers. */
371
+ preloadedAssets() {
372
+ return { ...this.#preloaded };
373
+ }
374
+ /** Renders withEntryPoints() (Vite is Htmlable, like Laravel's). */
375
+ toHtml() {
376
+ return this.tags(this.#entryPoints).toHtml();
377
+ }
378
+ // Internals ------------------------------------------------------------------------------------------------
379
+ #hotAsset(path) {
380
+ return `${this.devServerUrl() ?? ""}/${path}`;
381
+ }
382
+ #assetPath(path) {
383
+ if (this.#assetPathResolver !== void 0) return this.#assetPathResolver(path);
384
+ if (this.app.bound(UrlGenerator)) return this.app.make(UrlGenerator).asset(path);
385
+ const base = this.app.bound(ConfigRepository) ? this.app.make(ConfigRepository).get("app.asset_url", null) : null;
386
+ return `${(base ?? "").replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
387
+ }
388
+ #chunk(manifest, file) {
389
+ const chunk2 = manifest[file];
390
+ if (chunk2 === void 0) throw new ViteError(`Unable to locate file in Vite manifest: ${file}.`);
391
+ return chunk2;
392
+ }
393
+ /** Every static import, depth first, each once. */
394
+ #resolveImports(manifest, chunk2, seen = /* @__PURE__ */ new Set()) {
395
+ const imports = [];
396
+ for (const name of chunk2.imports ?? []) {
397
+ if (seen.has(name)) continue;
398
+ seen.add(name);
399
+ imports.push(name);
400
+ const imported = manifest[name];
401
+ if (imported !== void 0) imports.push(...this.#resolveImports(manifest, imported, seen));
402
+ }
403
+ return imports;
404
+ }
405
+ #tagForChunk(src, url, chunk2, manifest) {
406
+ if (CSS_PATH.test(url)) {
407
+ const attributes2 = this.#parseAttributes({ rel: "stylesheet", href: url, nonce: this.#nonce ?? false, ...this.#styleAttributes(src, url, chunk2, manifest) });
408
+ return `<link ${attributes2.join(" ")} />`;
409
+ }
410
+ const attributes = this.#parseAttributes({ type: "module", src: url, nonce: this.#nonce ?? false, ...this.#scriptAttributes(src, url, chunk2, manifest) });
411
+ return `<script ${attributes.join(" ")}></script>`;
412
+ }
413
+ #preloadTag(src, url, chunk2, manifest) {
414
+ const css = CSS_PATH.test(url);
415
+ let attributes = {
416
+ rel: css ? "preload" : "modulepreload",
417
+ as: css ? "style" : "script",
418
+ href: url,
419
+ nonce: this.#nonce ?? false,
420
+ crossorigin: (css ? this.#styleAttributes(src, url, chunk2, manifest) : this.#scriptAttributes(src, url, chunk2, manifest)).crossorigin ?? false
421
+ };
422
+ if (this.#integrityKey !== false) attributes.integrity = chunk2?.[this.#integrityKey] ?? false;
423
+ for (const resolver of this.#preloadResolvers) {
424
+ const resolved = resolver(src, url, chunk2, manifest);
425
+ if (resolved === false) return "";
426
+ attributes = { ...attributes, ...resolved };
427
+ }
428
+ const { href: _href, ...rest } = attributes;
429
+ this.#preloaded[url] = this.#parseAttributes(rest);
430
+ return `<link ${this.#parseAttributes(attributes).join(" ")} />`;
431
+ }
432
+ #scriptAttributes(src, url, chunk2, manifest) {
433
+ let attributes = this.#integrityKey !== false ? { integrity: chunk2?.[this.#integrityKey] ?? false } : {};
434
+ for (const resolver of this.#scriptResolvers) attributes = { ...attributes, ...resolver(src, url, chunk2, manifest) };
435
+ return attributes;
436
+ }
437
+ #styleAttributes(src, url, chunk2, manifest) {
438
+ let attributes = this.#integrityKey !== false ? { integrity: chunk2?.[this.#integrityKey] ?? false } : {};
439
+ for (const resolver of this.#styleResolvers) attributes = { ...attributes, ...resolver(src, url, chunk2, manifest) };
440
+ return attributes;
441
+ }
442
+ /** false/null/undefined are dropped, true is a bare attribute, anything else name="value". */
443
+ #parseAttributes(attributes) {
444
+ return Object.entries(attributes).filter(([, value]) => value !== false && value !== null && value !== void 0).map(([name, value]) => value === true ? name : `${name}="${escapeAttribute(String(value))}"`);
445
+ }
446
+ };
447
+ function currentVite() {
448
+ const container = currentContainer();
449
+ if (container === void 0) throw new ContainerNotAvailableError("Vite");
450
+ if (container.bound(HttpRequest)) return container.make(Vite);
451
+ return new Vite(container.make(Application));
452
+ }
453
+ function vite(entryPoints, buildDirectory) {
454
+ const instance = currentVite();
455
+ return entryPoints === void 0 ? instance : instance.tags(entryPoints, buildDirectory);
456
+ }
457
+ function viteReactRefresh() {
458
+ return currentVite().reactRefresh();
459
+ }
181
460
  var Application2 = class _Application extends Container {
182
461
  constructor(base) {
183
462
  super();
@@ -186,6 +465,7 @@ var Application2 = class _Application extends Container {
186
465
  this.instance(Application, this);
187
466
  this.instance(_Application, this);
188
467
  this.singleton(ClassRegistry, ClassRegistry2);
468
+ this.scoped(Vite, () => new Vite(this));
189
469
  }
190
470
  base;
191
471
  version = "0.1.0-alpha.1";
@@ -201,28 +481,28 @@ var Application2 = class _Application extends Container {
201
481
  return new ApplicationBuilder(new _Application(options.basePath));
202
482
  }
203
483
  basePath(path = "") {
204
- return join22(this.base, path);
484
+ return join3(this.base, path);
205
485
  }
206
486
  appPath(path = "") {
207
- return this.basePath(join22("app", path));
487
+ return this.basePath(join3("app", path));
208
488
  }
209
489
  bootstrapPath(path = "") {
210
- return this.basePath(join22("bootstrap", path));
490
+ return this.basePath(join3("bootstrap", path));
211
491
  }
212
492
  configPath(path = "") {
213
- return this.basePath(join22("config", path));
493
+ return this.basePath(join3("config", path));
214
494
  }
215
495
  databasePath(path = "") {
216
- return this.basePath(join22("database", path));
496
+ return this.basePath(join3("database", path));
217
497
  }
218
498
  publicPath(path = "") {
219
- return this.basePath(join22("public", path));
499
+ return this.basePath(join3("public", path));
220
500
  }
221
501
  resourcePath(path = "") {
222
- return this.basePath(join22("resources", path));
502
+ return this.basePath(join3("resources", path));
223
503
  }
224
504
  storagePath(path = "") {
225
- return this.basePath(join22("storage", path));
505
+ return this.basePath(join3("storage", path));
226
506
  }
227
507
  environment(...environments) {
228
508
  const fallback = process.env.APP_ENV ?? "production";
@@ -322,11 +602,13 @@ function toResponse(output) {
322
602
  // ../Testing/dist/index.js
323
603
  import { AssertionError } from "assert";
324
604
  import { isDeepStrictEqual } from "util";
605
+ import { existsSync as existsSync4 } from "fs";
606
+ import { join as join5 } from "path";
325
607
  import { createHmac } from "crypto";
326
608
  import { randomUUID } from "crypto";
327
609
  import { randomUUID as randomUUID2 } from "crypto";
328
610
  import { randomUUID as randomUUID3 } from "crypto";
329
- import { randomBytes } from "crypto";
611
+ import { randomBytes as randomBytes2 } from "crypto";
330
612
  import { deflateSync } from "zlib";
331
613
  function fail(message, details = {}) {
332
614
  const error = new AssertionError({ message, actual: details.actual, expected: details.expected, operator: "fail" });
@@ -608,6 +890,395 @@ function clonePlain2(value) {
608
890
  }
609
891
  return value;
610
892
  }
893
+ function typeOf(value) {
894
+ if (value === null) return ["null"];
895
+ if (Array.isArray(value)) return ["array"];
896
+ switch (typeof value) {
897
+ case "string":
898
+ return ["string"];
899
+ case "boolean":
900
+ return ["boolean"];
901
+ case "number":
902
+ return Number.isInteger(value) ? ["integer", "number"] : ["double", "float", "number"];
903
+ case "object":
904
+ return ["array", "object"];
905
+ default:
906
+ return [typeof value];
907
+ }
908
+ }
909
+ var AssertableJson = class _AssertableJson {
910
+ constructor(props, path = void 0) {
911
+ this.props = props;
912
+ this.path = path;
913
+ }
914
+ props;
915
+ path;
916
+ interactedKeys = [];
917
+ static fromJson(json) {
918
+ return new _AssertableJson(json);
919
+ }
920
+ /** A child scope of the same class (subclasses override to keep their extra state out of scopes). */
921
+ newScope(props, path) {
922
+ return new _AssertableJson(props, path);
923
+ }
924
+ dotPath(key = "") {
925
+ if (this.path === void 0) return key;
926
+ return key === "" ? this.path : `${this.path}.${key}`;
927
+ }
928
+ /** The value at `key` in this scope, or the whole scope. */
929
+ prop(key) {
930
+ if (key !== void 0 && this.hasOwnKey(key)) return this.props[key];
931
+ return Arr.get(this.props, key);
932
+ }
933
+ /** A literal key of this scope wins over a dot path, like Laravel's Arr::get (keys such as 'items.0.name'). */
934
+ hasOwnKey(key) {
935
+ return typeof this.props === "object" && this.props !== null && Object.hasOwn(this.props, key);
936
+ }
937
+ exists(key) {
938
+ return this.hasOwnKey(key) || Arr.has(this.props, key);
939
+ }
940
+ interactsWith(key) {
941
+ const prop = this.hasOwnKey(String(key)) ? String(key) : String(key).split(".")[0];
942
+ if (!this.interactedKeys.includes(prop)) this.interactedKeys.push(prop);
943
+ }
944
+ /** Every property of the scope was checked (called when a scope ends). */
945
+ interacted() {
946
+ const props = this.prop();
947
+ const keys = typeof props === "object" && props !== null ? Object.keys(props) : [];
948
+ const unexpected = keys.filter((key) => !this.interactedKeys.includes(key));
949
+ assert(
950
+ unexpected.length === 0,
951
+ this.path === void 0 ? "Unexpected properties were found on the root level." : `Unexpected properties were found in scope [${this.path}].`,
952
+ { actual: unexpected, expected: [] }
953
+ );
954
+ }
955
+ /** Allow the properties this scope didn't check. */
956
+ etc() {
957
+ const props = this.prop();
958
+ this.interactedKeys = typeof props === "object" && props !== null ? Object.keys(props) : [];
959
+ return this;
960
+ }
961
+ // Has ------------------------------------------------------------------------------------------------------
962
+ /** has('key'), has('key', 3), has('key', json => …), has('key', 3, first => …), or has(3) for the root size. */
963
+ has(key, length, callback) {
964
+ if (typeof key === "number" && length === void 0) return this.count(key);
965
+ const name = String(key);
966
+ assert(this.exists(name), `Property [${this.dotPath(name)}] does not exist.`);
967
+ this.interactsWith(name);
968
+ if (callback !== void 0) {
969
+ return this.has(name, (scope) => {
970
+ if (typeof length === "number") scope.count(length);
971
+ scope.first(callback).etc();
972
+ });
973
+ }
974
+ if (typeof length === "function") return this.scope(name, length);
975
+ if (typeof length === "number") return this.count(name, length);
976
+ return this;
977
+ }
978
+ /** hasAll(['a', 'b']) or hasAll({ items: 3 }). */
979
+ hasAll(keys) {
980
+ if (typeof keys === "string" || Array.isArray(keys)) {
981
+ for (const key of [keys].flat()) this.has(key);
982
+ return this;
983
+ }
984
+ for (const [key, length] of Object.entries(keys)) this.has(key, length);
985
+ return this;
986
+ }
987
+ hasAny(keys) {
988
+ const list = [keys].flat();
989
+ const present = list.filter((key) => this.exists(key));
990
+ assert(present.length > 0, `None of properties [${list.map((key) => this.dotPath(key)).join(", ")}] exist.`);
991
+ for (const key of present) this.interactsWith(key);
992
+ return this;
993
+ }
994
+ missing(key) {
995
+ assert(!this.exists(key), `Property [${this.dotPath(key)}] was found while it was expected to be missing.`);
996
+ return this;
997
+ }
998
+ missingAll(keys) {
999
+ for (const key of [keys].flat()) this.missing(key);
1000
+ return this;
1001
+ }
1002
+ /** count(3) checks this scope's size; count('items', 3) a property's. */
1003
+ count(key, length) {
1004
+ if (typeof key === "number") {
1005
+ const size2 = sizeOf(this.prop());
1006
+ assert(size2 === key, this.path === void 0 ? "Root level does not have the expected size." : `Property [${this.path}] does not have the expected size.`, { actual: size2, expected: key });
1007
+ return this;
1008
+ }
1009
+ this.has(key);
1010
+ const size = sizeOf(this.prop(key));
1011
+ assert(size === length, `Property [${this.dotPath(key)}] does not have the expected size.`, { actual: size, expected: length });
1012
+ return this;
1013
+ }
1014
+ countBetween(key, min, max) {
1015
+ this.has(key);
1016
+ const size = sizeOf(this.prop(key));
1017
+ assert(size >= min && size <= max, `Property [${this.dotPath(key)}] size is not between ${min} and ${max}.`, { actual: size });
1018
+ return this;
1019
+ }
1020
+ // Matching -------------------------------------------------------------------------------------------------
1021
+ /** where('name', 'Ann'), or where('name', value => value.startsWith('A')). Objects compare ignoring key order. */
1022
+ where(key, expected) {
1023
+ this.has(key);
1024
+ const actual = this.prop(key);
1025
+ if (typeof expected === "function") {
1026
+ assert(expected(actual), `Property [${this.dotPath(key)}] was marked as invalid using a closure.`);
1027
+ return this;
1028
+ }
1029
+ const normalized = toPlain(expected);
1030
+ assert(equals(normalized, actual), `Property [${this.dotPath(key)}] does not match the expected value.`, { actual, expected: normalized });
1031
+ return this;
1032
+ }
1033
+ whereNot(key, expected) {
1034
+ this.has(key);
1035
+ const actual = this.prop(key);
1036
+ if (typeof expected === "function") {
1037
+ assert(!expected(actual), `Property [${this.dotPath(key)}] was marked as invalid using a closure.`);
1038
+ return this;
1039
+ }
1040
+ assert(!equals(toPlain(expected), actual), `Property [${this.dotPath(key)}] contains a value that should be missing: [${key}, ${format(expected)}]`);
1041
+ return this;
1042
+ }
1043
+ whereNull(key) {
1044
+ this.has(key);
1045
+ const actual = this.prop(key);
1046
+ assert(actual === null, `Property [${this.dotPath(key)}] should be null.`, { actual, expected: null });
1047
+ return this;
1048
+ }
1049
+ whereNotNull(key) {
1050
+ this.has(key);
1051
+ assert(this.prop(key) !== null, `Property [${this.dotPath(key)}] should not be null.`);
1052
+ return this;
1053
+ }
1054
+ whereAll(bindings) {
1055
+ for (const [key, value] of Object.entries(bindings)) this.where(key, value);
1056
+ return this;
1057
+ }
1058
+ /** whereType('id', 'integer'), whereType('name', 'string|null'), whereType('tags', ['array', 'null']). */
1059
+ whereType(key, expected) {
1060
+ this.has(key);
1061
+ const types = typeof expected === "string" ? expected.split("|") : expected;
1062
+ const actual = typeOf(this.prop(key));
1063
+ assert(
1064
+ types.some((type) => actual.includes(type)),
1065
+ `Property [${this.dotPath(key)}] is not of expected type [${types.join("|")}].`,
1066
+ { actual: actual[0], expected: types.join("|") }
1067
+ );
1068
+ return this;
1069
+ }
1070
+ whereAllType(bindings) {
1071
+ for (const [key, type] of Object.entries(bindings)) this.whereType(key, type);
1072
+ return this;
1073
+ }
1074
+ /** The list contains every expected value (or one passing the callback); a string contains the substring. */
1075
+ whereContains(key, expected) {
1076
+ this.has(key);
1077
+ const actual = this.prop(key);
1078
+ if (typeof actual === "string") {
1079
+ assert(actual.includes(String(expected)), `Property [${this.dotPath(key)}] does not contain [${format(expected)}].`);
1080
+ return this;
1081
+ }
1082
+ const values = typeof actual === "object" && actual !== null ? Object.values(actual) : [actual];
1083
+ const wanted = Array.isArray(expected) ? expected : [expected];
1084
+ const missing = wanted.filter(
1085
+ (item) => typeof item === "function" ? !values.some((value) => item(value)) : !values.some((value) => equals(toPlain(item), value))
1086
+ );
1087
+ assert(missing.length === 0, `Property [${this.dotPath(key)}] does not contain [${missing.map(format).join(", ")}].`, { actual });
1088
+ return this;
1089
+ }
1090
+ // Scoping --------------------------------------------------------------------------------------------------
1091
+ scope(key, callback) {
1092
+ const props = this.prop(key);
1093
+ const path = this.dotPath(key);
1094
+ assert(typeof props === "object" && props !== null, `Property [${path}] is not scopeable.`);
1095
+ const scope = this.newScope(props, path);
1096
+ callback(scope);
1097
+ scope.interacted();
1098
+ return this;
1099
+ }
1100
+ first(callback) {
1101
+ const props = this.prop();
1102
+ const keys = typeof props === "object" && props !== null ? Object.keys(props) : [];
1103
+ if (keys.length === 0) {
1104
+ fail(this.path === void 0 ? "Cannot scope directly onto the first element of the root level because it is empty." : `Cannot scope directly onto the first element of property [${this.path}] because it is empty.`);
1105
+ }
1106
+ this.interactsWith(keys[0]);
1107
+ return this.scope(keys[0], callback);
1108
+ }
1109
+ each(callback) {
1110
+ const props = this.prop();
1111
+ const keys = typeof props === "object" && props !== null ? Object.keys(props) : [];
1112
+ if (keys.length === 0) {
1113
+ fail(this.path === void 0 ? "Cannot scope directly onto each element of the root level because it is empty." : `Cannot scope directly onto each element of property [${this.path}] because it is empty.`);
1114
+ }
1115
+ for (const key of keys) {
1116
+ this.interactsWith(key);
1117
+ this.scope(key, callback);
1118
+ }
1119
+ return this;
1120
+ }
1121
+ // Helpers --------------------------------------------------------------------------------------------------
1122
+ when(condition, callback, otherwise) {
1123
+ if (condition) callback(this);
1124
+ else otherwise?.(this);
1125
+ return this;
1126
+ }
1127
+ dump(key) {
1128
+ console.log(format(this.prop(key)));
1129
+ return this;
1130
+ }
1131
+ toArray() {
1132
+ return this.prop();
1133
+ }
1134
+ };
1135
+ function sizeOf(value) {
1136
+ if (Array.isArray(value)) return value.length;
1137
+ if (typeof value === "object" && value !== null) return Object.keys(value).length;
1138
+ if (typeof value === "string") return value.length;
1139
+ return 0;
1140
+ }
1141
+ function toPlain(value) {
1142
+ return value === void 0 ? void 0 : JSON.parse(JSON.stringify(value));
1143
+ }
1144
+ function inertiaPageOf(response) {
1145
+ let page;
1146
+ try {
1147
+ if (response.headers.has("X-Inertia")) page = JSON.parse(response.content);
1148
+ else {
1149
+ const match = /<script data-page="[^"]*" type="application\/json">([\s\S]*?)<\/script>/.exec(response.content);
1150
+ page = match === null ? void 0 : JSON.parse(match[1]);
1151
+ }
1152
+ } catch {
1153
+ page = void 0;
1154
+ }
1155
+ const valid = typeof page === "object" && page !== null && ["component", "props", "url", "version"].every((key) => Object.hasOwn(page, key));
1156
+ if (!valid) fail("Not a valid Inertia response.");
1157
+ return page;
1158
+ }
1159
+ var AssertableInertia = class _AssertableInertia extends AssertableJson {
1160
+ #page;
1161
+ #response;
1162
+ static fromTestResponse(response) {
1163
+ const page = inertiaPageOf(response);
1164
+ const instance = new _AssertableInertia(page.props);
1165
+ instance.#page = page;
1166
+ instance.#response = response;
1167
+ return instance;
1168
+ }
1169
+ /** Scopes are plain AssertableJson: the page-level methods only make sense at the root. */
1170
+ newScope(props, path) {
1171
+ return new AssertableJson(props, path);
1172
+ }
1173
+ get page() {
1174
+ if (this.#page === void 0) fail("This assertion is only available on the page root.");
1175
+ return this.#page;
1176
+ }
1177
+ /** The component name; the page file must exist unless `shouldExist` is false (inertia.testing.ensure_pages_exist). */
1178
+ component(value, shouldExist) {
1179
+ const actual = this.page.component;
1180
+ assert(actual === value, "Unexpected Inertia page component.", { actual, expected: value });
1181
+ const app = this.#app();
1182
+ const config = app?.bound(ConfigRepository) ? app.make(ConfigRepository) : void 0;
1183
+ if (shouldExist === true || shouldExist === void 0 && config?.get("inertia.testing.ensure_pages_exist", true) !== false) {
1184
+ const paths = config?.get("inertia.pages.paths", []) ?? [];
1185
+ const extensions = config?.get("inertia.pages.extensions", []) ?? [];
1186
+ const exists = paths.some((path) => extensions.some((extension) => existsSync4(join5(path, `${value}.${extension}`))));
1187
+ if (!exists) fail(`Inertia page component file [${value}] does not exist.`);
1188
+ }
1189
+ return this;
1190
+ }
1191
+ url(value) {
1192
+ assert(this.page.url === value, "Unexpected Inertia page url.", { actual: this.page.url, expected: value });
1193
+ return this;
1194
+ }
1195
+ version(value) {
1196
+ assert(this.page.version === value, "Unexpected Inertia asset version.", { actual: this.page.version, expected: value });
1197
+ return this;
1198
+ }
1199
+ /** The flash data has `key` (dot paths allowed), equal to `expected` when given. */
1200
+ hasFlash(key, ...expected) {
1201
+ _AssertableInertia.assertFlashHas(this.page.flash ?? {}, key, ...expected);
1202
+ return this;
1203
+ }
1204
+ missingFlash(key) {
1205
+ _AssertableInertia.assertFlashMissing(this.page.flash ?? {}, key);
1206
+ return this;
1207
+ }
1208
+ static assertFlashHas(flash, key, ...expected) {
1209
+ assert(Arr.has(flash, key), `Inertia Flash Data is missing key [${key}].`);
1210
+ if (expected.length > 0) {
1211
+ const actual = Arr.get(flash, key);
1212
+ assert(equals(actual, expected[0]), `Inertia Flash Data [${key}] does not match expected value.`, { actual, expected: expected[0] });
1213
+ }
1214
+ }
1215
+ static assertFlashMissing(flash, key) {
1216
+ assert(!Arr.has(flash, key), `Inertia Flash Data has unexpected key [${key}].`);
1217
+ }
1218
+ /**
1219
+ * Requests the page again as a partial reload (only/except) and checks it is the same page. Uses the test's
1220
+ * HTTP client, so the session and cookies carry over.
1221
+ */
1222
+ async reload(callback, only, except) {
1223
+ const client = this.#response?.context.client;
1224
+ if (client === void 0) fail("reload() needs a response from the test HTTP client.");
1225
+ const headers = {
1226
+ "X-Inertia": "true",
1227
+ "X-Requested-With": "XMLHttpRequest",
1228
+ "X-Inertia-Version": this.page.version
1229
+ };
1230
+ const onlyList = only === void 0 ? void 0 : [only].flat().join(",");
1231
+ const exceptList = except === void 0 ? void 0 : [except].flat().join(",");
1232
+ if (onlyList !== void 0 || exceptList !== void 0) headers["X-Inertia-Partial-Component"] = this.page.component;
1233
+ if (onlyList !== void 0) headers["X-Inertia-Partial-Data"] = onlyList;
1234
+ if (exceptList !== void 0) headers["X-Inertia-Partial-Except"] = exceptList;
1235
+ const response = await client.request("GET", this.page.url, { headers });
1236
+ const reloaded = _AssertableInertia.fromTestResponse(response);
1237
+ reloaded.component(this.page.component, false).url(this.page.url).version(this.page.version);
1238
+ await callback?.(reloaded);
1239
+ return this;
1240
+ }
1241
+ reloadOnly(only, callback) {
1242
+ return this.reload(
1243
+ async (page) => {
1244
+ page.hasAll(typeof only === "string" ? only.split(",") : only);
1245
+ await callback?.(page);
1246
+ },
1247
+ only
1248
+ );
1249
+ }
1250
+ reloadExcept(except, callback) {
1251
+ return this.reload(
1252
+ async (page) => {
1253
+ page.missingAll(typeof except === "string" ? except.split(",") : except);
1254
+ await callback?.(page);
1255
+ },
1256
+ void 0,
1257
+ except
1258
+ );
1259
+ }
1260
+ /** Loads the deferred props of every group (or the given groups) in one partial reload. */
1261
+ loadDeferredProps(groupsOrCallback, callback) {
1262
+ const deferred = this.page.deferredProps ?? {};
1263
+ const groups = typeof groupsOrCallback === "function" ? Object.keys(deferred) : [groupsOrCallback].flat();
1264
+ const props = groups.flatMap((group) => deferred[group] ?? []);
1265
+ return this.reloadOnly(props.join(","), typeof groupsOrCallback === "function" ? groupsOrCallback : callback);
1266
+ }
1267
+ toArray() {
1268
+ return {
1269
+ component: this.page.component,
1270
+ props: this.prop(),
1271
+ url: this.page.url,
1272
+ version: this.page.version,
1273
+ flash: this.page.flash ?? {},
1274
+ ...this.page.encryptHistory === true ? { encryptHistory: true } : {},
1275
+ ...this.page.clearHistory === true ? { clearHistory: true } : {}
1276
+ };
1277
+ }
1278
+ #app() {
1279
+ return this.#response?.context.app;
1280
+ }
1281
+ };
611
1282
  function cookiePrefix(name, key) {
612
1283
  return `${createHmac("sha1", key).update(`${name}v2`).digest("hex")}|`;
613
1284
  }
@@ -880,12 +1551,44 @@ ${this.excerpt(text)}`);
880
1551
  return this.inOrder(stripTags(this.content), this.needles(values, escape));
881
1552
  }
882
1553
  // JSON --------------------------------------------------------------------------------------------------
883
- /** The response JSON contains `data` (a subset match: extra keys in the response are fine). */
1554
+ /**
1555
+ * The response JSON contains `data` (a subset match: extra keys in the response are fine), or fluent
1556
+ * assertions: assertJson(json => json.has('data', 3).etc()).
1557
+ */
884
1558
  assertJson(data) {
1559
+ if (typeof data === "function") {
1560
+ const json = AssertableJson.fromJson(this.json());
1561
+ data(json);
1562
+ if (!Array.isArray(json.toArray())) json.interacted();
1563
+ return this;
1564
+ }
885
1565
  const actual = this.json();
886
1566
  assert(isSubset(data, actual), "Unable to find the expected JSON subset in the response.", { actual, expected: data });
887
1567
  return this;
888
1568
  }
1569
+ assertInertia(callback) {
1570
+ const page = AssertableInertia.fromTestResponse(this);
1571
+ const result = callback?.(page);
1572
+ if (typeof result?.then === "function") return Promise.resolve(result).then(() => this);
1573
+ return this;
1574
+ }
1575
+ /** The Inertia page object of this response. */
1576
+ inertiaPage() {
1577
+ return inertiaPageOf(this);
1578
+ }
1579
+ /** The page props, or one prop by dot path. */
1580
+ inertiaProps(key) {
1581
+ return Arr.get(inertiaPageOf(this).props, key);
1582
+ }
1583
+ /** Inertia flash data waiting in the session for the next page (after a redirect). */
1584
+ assertInertiaFlash(key, ...expected) {
1585
+ AssertableInertia.assertFlashHas(this.session().get("inertia.flash_data", {}), key, ...expected);
1586
+ return this;
1587
+ }
1588
+ assertInertiaFlashMissing(key) {
1589
+ AssertableInertia.assertFlashMissing(this.session().get("inertia.flash_data", {}), key);
1590
+ return this;
1591
+ }
889
1592
  /** The response JSON equals `data` exactly (object key order is ignored). */
890
1593
  assertExactJson(data) {
891
1594
  const actual = this.json();
@@ -1303,7 +2006,7 @@ var HttpClient = class {
1303
2006
  await app.boot();
1304
2007
  setClientAddress(request, "127.0.0.1");
1305
2008
  const scope = app.createScope();
1306
- const context = { request, app };
2009
+ const context = { request, app, client: this };
1307
2010
  const response = await scope.run(async () => {
1308
2011
  await this.beforeRequest(scope, context);
1309
2012
  this.previousUrl = void 0;
@@ -2585,7 +3288,7 @@ function guessMimeType(path) {
2585
3288
  }
2586
3289
  function hashName(file) {
2587
3290
  const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
2588
- const name = [...randomBytes(40)].map((byte) => alphabet[byte % alphabet.length]).join("");
3291
+ const name = [...randomBytes2(40)].map((byte) => alphabet[byte % alphabet.length]).join("");
2589
3292
  const extension = file.extension();
2590
3293
  return extension === "" ? name : `${name}.${extension}`;
2591
3294
  }
@@ -2894,6 +3597,12 @@ export {
2894
3597
  ApplicationBuilder,
2895
3598
  ProviderBootError,
2896
3599
  ProviderRegisterError,
3600
+ ViteError,
3601
+ ViteHtml,
3602
+ Vite,
3603
+ currentVite,
3604
+ vite,
3605
+ viteReactRefresh,
2897
3606
  Application2 as Application,
2898
3607
  ClosureKernel,
2899
3608
  PendingCommand,
@@ -2905,6 +3614,9 @@ export {
2905
3614
  DatabaseTransactions,
2906
3615
  TESTING_ENVIRONMENT,
2907
3616
  applyEnvironment,
3617
+ AssertableJson,
3618
+ inertiaPageOf,
3619
+ AssertableInertia,
2908
3620
  cookiePrefix,
2909
3621
  encryptCookie,
2910
3622
  decryptCookie,
@@ -2938,4 +3650,4 @@ export {
2938
3650
  StorageFake,
2939
3651
  FakeFile
2940
3652
  };
2941
- //# sourceMappingURL=chunk-BZPCRKFQ.js.map
3653
+ //# sourceMappingURL=chunk-UPZAML2Q.js.map