@alxia/react-router 0.9.0 → 0.11.0

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.
Files changed (100) hide show
  1. package/README.md +301 -13
  2. package/dist/chunks/index-59vhfj6n.js +10 -0
  3. package/dist/chunks/index-59vhfj6n.js.map +10 -0
  4. package/dist/chunks/index-5gqcpyyg.js +34 -0
  5. package/dist/chunks/index-5gqcpyyg.js.map +10 -0
  6. package/dist/chunks/{index-zqa30gr7.js → index-9xv2a8et.js} +7 -38
  7. package/dist/chunks/index-9xv2a8et.js.map +11 -0
  8. package/dist/chunks/index-mppz0806.js +119 -0
  9. package/dist/chunks/index-mppz0806.js.map +11 -0
  10. package/dist/chunks/index-rfr02vg4.js +40 -0
  11. package/dist/chunks/index-rfr02vg4.js.map +10 -0
  12. package/dist/chunks/index-zdbqzcjp.js +97 -0
  13. package/dist/chunks/index-zdbqzcjp.js.map +12 -0
  14. package/dist/errors/api-error.d.ts +86 -0
  15. package/dist/errors/api-error.d.ts.map +1 -0
  16. package/dist/errors/api-errors.d.ts +96 -0
  17. package/dist/errors/api-errors.d.ts.map +1 -0
  18. package/dist/errors/call-failure.d.ts +45 -0
  19. package/dist/errors/call-failure.d.ts.map +1 -0
  20. package/dist/errors/catalogue-check.d.ts +24 -0
  21. package/dist/errors/catalogue-check.d.ts.map +1 -0
  22. package/dist/errors/catalogue-read.d.ts +33 -0
  23. package/dist/errors/catalogue-read.d.ts.map +1 -0
  24. package/dist/errors/catalogue.d.ts +59 -0
  25. package/dist/errors/catalogue.d.ts.map +1 -0
  26. package/dist/errors/code-resolver.d.ts +44 -0
  27. package/dist/errors/code-resolver.d.ts.map +1 -0
  28. package/dist/errors/describe-refusal.d.ts +6 -0
  29. package/dist/errors/describe-refusal.d.ts.map +1 -0
  30. package/dist/errors/describe.d.ts +3 -0
  31. package/dist/errors/describe.d.ts.map +1 -0
  32. package/dist/errors/fetcher-result.d.ts +21 -0
  33. package/dist/errors/fetcher-result.d.ts.map +1 -0
  34. package/dist/errors/form-errors.d.ts +8 -0
  35. package/dist/errors/form-errors.d.ts.map +1 -0
  36. package/dist/errors/index.d.ts +15 -0
  37. package/dist/errors/index.d.ts.map +1 -0
  38. package/dist/errors/index.js +602 -0
  39. package/dist/errors/index.js.map +29 -0
  40. package/dist/errors/json-actions.d.ts +38 -0
  41. package/dist/errors/json-actions.d.ts.map +1 -0
  42. package/dist/errors/log.d.ts +8 -0
  43. package/dist/errors/log.d.ts.map +1 -0
  44. package/dist/errors/message-values.d.ts +11 -0
  45. package/dist/errors/message-values.d.ts.map +1 -0
  46. package/dist/errors/messages.d.ts +63 -0
  47. package/dist/errors/messages.d.ts.map +1 -0
  48. package/dist/errors/page-path.d.ts +9 -0
  49. package/dist/errors/page-path.d.ts.map +1 -0
  50. package/dist/errors/provider.d.ts +20 -0
  51. package/dist/errors/provider.d.ts.map +1 -0
  52. package/dist/errors/request-id.d.ts +5 -0
  53. package/dist/errors/request-id.d.ts.map +1 -0
  54. package/dist/errors/route-error.d.ts +31 -0
  55. package/dist/errors/route-error.d.ts.map +1 -0
  56. package/dist/errors/standard-schema.d.ts +34 -0
  57. package/dist/errors/standard-schema.d.ts.map +1 -0
  58. package/dist/errors/submit-form.d.ts +16 -0
  59. package/dist/errors/submit-form.d.ts.map +1 -0
  60. package/dist/errors/use-json-fetcher.d.ts +18 -0
  61. package/dist/errors/use-json-fetcher.d.ts.map +1 -0
  62. package/dist/pwa/build/build-worker.d.ts +41 -0
  63. package/dist/pwa/build/build-worker.d.ts.map +1 -0
  64. package/dist/pwa/build/index.d.ts +9 -0
  65. package/dist/pwa/build/index.d.ts.map +1 -0
  66. package/dist/pwa/build/index.js +9 -0
  67. package/dist/pwa/build/index.js.map +9 -0
  68. package/dist/pwa/build/precache.d.ts +17 -0
  69. package/dist/pwa/build/precache.d.ts.map +1 -0
  70. package/dist/pwa/index.js +34 -134
  71. package/dist/pwa/index.js.map +4 -8
  72. package/dist/pwa/react/index.js +1 -1
  73. package/dist/pwa/worker/index.d.ts +9 -0
  74. package/dist/pwa/worker/index.d.ts.map +1 -0
  75. package/dist/pwa/worker/index.js +144 -0
  76. package/dist/pwa/worker/index.js.map +12 -0
  77. package/dist/pwa/worker/injected.d.ts +15 -0
  78. package/dist/pwa/worker/injected.d.ts.map +1 -0
  79. package/dist/pwa/worker/plan.d.ts +65 -0
  80. package/dist/pwa/worker/plan.d.ts.map +1 -0
  81. package/dist/pwa/worker/scope.d.ts +18 -0
  82. package/dist/pwa/worker/scope.d.ts.map +1 -0
  83. package/dist/pwa/worker/start-worker.d.ts +17 -0
  84. package/dist/pwa/worker/start-worker.d.ts.map +1 -0
  85. package/dist/vite/index.d.ts +10 -0
  86. package/dist/vite/index.d.ts.map +1 -1
  87. package/dist/vite/index.js +49 -3
  88. package/dist/vite/index.js.map +6 -5
  89. package/dist/vite/runtime.d.ts +1 -1
  90. package/dist/vite/runtime.d.ts.map +1 -1
  91. package/dist/vite/worker.d.ts +16 -0
  92. package/dist/vite/worker.d.ts.map +1 -0
  93. package/docs/README.md +3 -2
  94. package/docs/guide/errors.md +838 -0
  95. package/docs/guide/how-it-works.md +7 -1
  96. package/docs/guide/pwa.md +372 -211
  97. package/docs/roadmap.md +38 -5
  98. package/docs/troubleshooting.md +689 -3
  99. package/package.json +56 -4
  100. package/dist/chunks/index-zqa30gr7.js.map +0 -12
@@ -0,0 +1,602 @@
1
+ // src/errors/api-error.ts
2
+ var isNamed = (error, name) => error instanceof Error && error.name === name;
3
+ function isApiErrorLike(error) {
4
+ return isNamed(error, "ApiError") && Array.isArray(error.errors) && typeof error.status === "number";
5
+ }
6
+ function waitMinutes(extensions) {
7
+ const minutes = extensions?.params?.minutes ?? extensions?.minutes;
8
+ if (minutes !== undefined)
9
+ return Math.max(1, Number(minutes) || 1);
10
+ const seconds = extensions?.params?.retryAfter ?? extensions?.retryAfter;
11
+ return Math.max(1, Math.ceil(Number(seconds) / 60) || 1);
12
+ }
13
+ function valuesOf(extensions) {
14
+ if (!extensions)
15
+ return;
16
+ return extensions.params ? { ...extensions, ...extensions.params } : extensions;
17
+ }
18
+ var isStatusError = (error) => isNamed(error, "ApiStatusError") && typeof error.status === "number";
19
+ var isUnavailableError = (error) => isNamed(error, "ApiUnavailableError") && typeof error.reason === "string";
20
+ // src/errors/code-resolver.ts
21
+ var API_CODE = /^[a-z-]+\.errors\.[a-z0-9-]+$/;
22
+ function paramsCode(issue) {
23
+ const code = issue.params?.code;
24
+ return typeof code === "string" ? code : undefined;
25
+ }
26
+ function apiCodeResolver(messages, options) {
27
+ const isApiCode = (key) => options.isApiCode ? options.isApiCode(key) : API_CODE.test(key);
28
+ return {
29
+ translate(key, context, provideLanguage) {
30
+ if (!isApiCode(key))
31
+ return options.translate(key, context, provideLanguage);
32
+ const language = typeof provideLanguage === "function" ? provideLanguage() : provideLanguage ?? options.language();
33
+ return messages.apiMessage(key, context, language) ?? key;
34
+ },
35
+ codeOf(issue) {
36
+ return options.codeOf ? options.codeOf(issue) : paramsCode(issue);
37
+ }
38
+ };
39
+ }
40
+
41
+ // src/errors/describe-refusal.ts
42
+ function refusalOf(errors, error, language) {
43
+ if (isApiErrorLike(error)) {
44
+ return {
45
+ message: errors.apiErrorMessage(error, language),
46
+ status: error.status
47
+ };
48
+ }
49
+ if (error instanceof Response && error.status === 503) {
50
+ return { message: errors.unavailableMessage(language), status: 503 };
51
+ }
52
+ return;
53
+ }
54
+ function fieldErrorsOf(errors, error, language) {
55
+ const extensions = error.errors[0]?.extensions;
56
+ const fields = Array.isArray(extensions?.fields) ? extensions.fields : [];
57
+ if (!fields.length && extensions?.field) {
58
+ return { [extensions.field]: errors.apiErrorMessage(error, language) };
59
+ }
60
+ return Object.fromEntries(fields.map(({ path, code, message, params }) => [
61
+ path,
62
+ errors.translateCode(code, message, language, params)
63
+ ]));
64
+ }
65
+
66
+ // src/errors/fetcher-result.ts
67
+ import { data as data2 } from "react-router";
68
+
69
+ // src/errors/route-error.ts
70
+ import { data } from "react-router";
71
+
72
+ // src/errors/request-id.ts
73
+ var REQUEST_ID_HEADER = "x-request-id";
74
+ function requestIdOf(response) {
75
+ return response.headers.get(REQUEST_ID_HEADER) || undefined;
76
+ }
77
+
78
+ // src/errors/route-error.ts
79
+ var ROUTED = new Set([401, 403, 404, 503]);
80
+ function routeError(error, { statuses = ROUTED } = {}) {
81
+ if (isApiErrorLike(error)) {
82
+ const { requestId } = error;
83
+ if (statuses.has(error.status)) {
84
+ return data({ code: error.code, message: error.message, requestId }, { status: error.status });
85
+ }
86
+ return requestId ? data({ requestId }, { status: 500 }) : error;
87
+ }
88
+ if (error instanceof Response && statuses.has(error.status)) {
89
+ const requestId = requestIdOf(error);
90
+ if (requestId) {
91
+ return data({ requestId }, { status: error.status });
92
+ }
93
+ }
94
+ return error;
95
+ }
96
+ function errorRequestId(error) {
97
+ if (typeof error !== "object" || error === null)
98
+ return;
99
+ const { data: body } = error;
100
+ if (typeof body !== "object" || body === null)
101
+ return;
102
+ const { requestId } = body;
103
+ return typeof requestId === "string" && requestId ? requestId : undefined;
104
+ }
105
+
106
+ // src/errors/fetcher-result.ts
107
+ function fetcherRefusal(errors, error, language, route) {
108
+ const refusal = isApiErrorLike(error) && error.status === 401 ? undefined : errors.apiRefusal(error, language);
109
+ if (!refusal)
110
+ throw routeError(error, route);
111
+ return refusal;
112
+ }
113
+ async function fetcherResult(errors, language, about, run, route) {
114
+ try {
115
+ return { ok: true, ...about, ...await run() };
116
+ } catch (error) {
117
+ const { message, status } = fetcherRefusal(errors, error, language, route);
118
+ return data2({ ok: false, ...about, error: message }, { status });
119
+ }
120
+ }
121
+
122
+ // src/errors/submit-form.ts
123
+ import { data as data3 } from "react-router";
124
+ async function submitForm(errors, language, run, fieldOf, route) {
125
+ try {
126
+ return { ok: true, value: await run() };
127
+ } catch (error) {
128
+ if (!isApiErrorLike(error) || error.status === 401) {
129
+ throw routeError(error, route);
130
+ }
131
+ const field = error.errors[0]?.extensions?.field;
132
+ if (error.status === 404 && !(field && fieldOf(field))) {
133
+ throw routeError(error, route);
134
+ }
135
+ const byField = {};
136
+ const unplaced = [];
137
+ for (const [path, message] of Object.entries(errors.fieldErrors(error, language))) {
138
+ const field = fieldOf(path);
139
+ if (field)
140
+ byField[field] = message;
141
+ else
142
+ unplaced.push(message);
143
+ }
144
+ const formError = Object.keys(byField).length && !unplaced.length ? undefined : [errors.apiErrorMessage(error, language), ...unplaced].join(" ");
145
+ return data3({ fieldErrors: byField, formError }, { status: error.status });
146
+ }
147
+ }
148
+
149
+ // src/errors/describe.ts
150
+ function describeApiErrors({
151
+ messages,
152
+ unavailable,
153
+ refused,
154
+ byCode = {},
155
+ isApiCode = (code) => API_CODE.test(code)
156
+ }) {
157
+ const translateCode = (code, fallback, language, values) => {
158
+ if (!code)
159
+ return fallback;
160
+ return messages.apiMessage(code, values, language) ?? fallback;
161
+ };
162
+ const apiErrorMessage = (error, language) => {
163
+ const code = error.code;
164
+ if (refused && (!code || !isApiCode(code)))
165
+ return refused(language);
166
+ const own = code && Object.hasOwn(byCode, code) ? byCode[code] : undefined;
167
+ if (own)
168
+ return own(error, language);
169
+ return translateCode(code, error.message, language, valuesOf(error.errors[0]?.extensions));
170
+ };
171
+ const unavailableMessage = (language) => unavailable(language);
172
+ const apiRefusal = (error, language) => refusalOf({ apiErrorMessage, unavailableMessage }, error, language);
173
+ const fieldErrors = (error, language) => fieldErrorsOf({ apiErrorMessage, translateCode }, error, language);
174
+ const parts = { apiRefusal, apiErrorMessage, fieldErrors };
175
+ return {
176
+ translateCode,
177
+ apiErrorMessage,
178
+ unavailableMessage,
179
+ fieldErrors,
180
+ apiRefusal,
181
+ fetcherRefusal: (error, language, route) => fetcherRefusal(parts, error, language, route),
182
+ fetcherResult: (language, about, run, route) => fetcherResult(parts, language, about, run, route),
183
+ submitForm: (language, run, fieldOf, route) => submitForm(parts, language, run, fieldOf, route)
184
+ };
185
+ }
186
+
187
+ // src/errors/api-errors.ts
188
+ function createApiErrors(options) {
189
+ return describeApiErrors(options);
190
+ }
191
+ // src/errors/log.ts
192
+ function quietLog(log, methods) {
193
+ const quiet = {};
194
+ for (const method of methods) {
195
+ quiet[method] = (...args) => {
196
+ try {
197
+ log[method](...args);
198
+ } catch {}
199
+ };
200
+ }
201
+ return quiet;
202
+ }
203
+
204
+ // src/errors/call-failure.ts
205
+ var PASSED_THROUGH = new Set([401, 403, 404]);
206
+ var NO_BODY = new Set([101, 204, 205, 304]);
207
+ function apiCallFailure(error, {
208
+ api,
209
+ operation = "operation",
210
+ requestId,
211
+ signal,
212
+ passedThrough = PASSED_THROUGH,
213
+ log: appLog = console
214
+ }) {
215
+ const log = quietLog(appLog, ["warn", "error"]);
216
+ const call = `${api}: ${operation}${requestId ? ` (request ${requestId})` : ""}`;
217
+ const headers = requestId ? { [REQUEST_ID_HEADER]: requestId } : {};
218
+ const unavailable = (reason, cause) => {
219
+ log.error(`${call} ${reason}`, cause ?? "");
220
+ return new Response(null, { status: 503, headers });
221
+ };
222
+ if (isApiErrorLike(error)) {
223
+ if (requestId)
224
+ error.requestId = requestId;
225
+ const line = `${call} answered ${error.status}${error.code ? ` ${error.code}` : ""}`;
226
+ if (error.status >= 500)
227
+ log.error(line);
228
+ else
229
+ log.warn(line);
230
+ return error;
231
+ }
232
+ if (isStatusError(error)) {
233
+ if (passedThrough.has(error.status)) {
234
+ log.warn(`${call} answered ${error.status}`);
235
+ const statusText = typeof error.statusText === "string" ? error.statusText : "";
236
+ if (NO_BODY.has(error.status)) {
237
+ return new Response(null, {
238
+ status: error.status,
239
+ statusText,
240
+ headers
241
+ });
242
+ }
243
+ return new Response(statusText, {
244
+ status: error.status,
245
+ statusText,
246
+ headers
247
+ });
248
+ }
249
+ return unavailable(`answered ${error.status} without GraphQL`, error.body);
250
+ }
251
+ if (isUnavailableError(error))
252
+ return unavailable(error.reason, error.cause);
253
+ if (signal?.aborted)
254
+ return unavailable("aborted", error);
255
+ return error;
256
+ }
257
+ function operationName(document) {
258
+ for (const definition of document.definitions) {
259
+ if (definition.kind === "OperationDefinition") {
260
+ return definition.name?.value ?? definition.operation ?? "operation";
261
+ }
262
+ }
263
+ return "operation";
264
+ }
265
+ // src/errors/catalogue-check.ts
266
+ async function checkVersions({ reader, versions, ttl, retryMs, log }, now) {
267
+ try {
268
+ const latest = await versions();
269
+ for (const [language, entry] of [...reader.held]) {
270
+ if (latest[language] !== entry.version) {
271
+ await reader.load(language, now);
272
+ } else {
273
+ entry.checkedAt = now;
274
+ }
275
+ }
276
+ } catch (error) {
277
+ log.error("alxia-react-router: the error catalogue was not checked, keeping what is held", error);
278
+ for (const entry of reader.held.values()) {
279
+ entry.checkedAt = now - ttl() + retryMs;
280
+ }
281
+ }
282
+ }
283
+ function safeTtl(ttlMs, fallback, log) {
284
+ return () => {
285
+ try {
286
+ const ttl = typeof ttlMs === "function" ? ttlMs() : ttlMs;
287
+ if (typeof ttl === "number" && Number.isFinite(ttl) && ttl >= 0) {
288
+ return ttl;
289
+ }
290
+ log.error("alxia-react-router: ttlMs is not a duration, using retryMs");
291
+ } catch (error) {
292
+ log.error("alxia-react-router: ttlMs threw, using retryMs", error);
293
+ }
294
+ return fallback;
295
+ };
296
+ }
297
+
298
+ // src/errors/catalogue-read.ts
299
+ async function boundedWait(pending, firstReadWaitMs) {
300
+ const left = firstReadWaitMs - (performance.now() - pending.started);
301
+ if (left <= 0)
302
+ return;
303
+ let timer;
304
+ const limit = new Promise((resolve) => {
305
+ timer = setTimeout(resolve, left);
306
+ });
307
+ await Promise.race([pending.read, limit]);
308
+ clearTimeout(timer);
309
+ }
310
+ function createCatalogueReader({
311
+ messages,
312
+ read,
313
+ retryMs,
314
+ firstReadWaitMs,
315
+ log
316
+ }) {
317
+ const held = new Map;
318
+ const loading = new Map;
319
+ const failedAt = new Map;
320
+ async function load(language, now) {
321
+ const reply = await read(language);
322
+ const entries = Object.create(null);
323
+ for (const { code, message, params } of reply.messages) {
324
+ entries[code] = { message, params };
325
+ }
326
+ messages.set(language, entries);
327
+ held.set(language, { version: reply.version, checkedAt: now });
328
+ }
329
+ function loadOnce(language, now) {
330
+ let pending = loading.get(language);
331
+ if (!pending) {
332
+ const started = performance.now();
333
+ const reading = load(language, now).then(() => {
334
+ failedAt.delete(language);
335
+ }).catch((error) => {
336
+ failedAt.set(language, now + (performance.now() - started));
337
+ log.error(`alxia-react-router: the error catalogue (${language}) was not read`, error);
338
+ }).finally(() => {
339
+ loading.delete(language);
340
+ });
341
+ pending = { read: reading, started };
342
+ loading.set(language, pending);
343
+ }
344
+ return pending;
345
+ }
346
+ return {
347
+ held,
348
+ load,
349
+ async first(language, now) {
350
+ const failed = failedAt.get(language);
351
+ if (failed === undefined || now - failed >= retryMs) {
352
+ await boundedWait(loadOnce(language, now), firstReadWaitMs);
353
+ }
354
+ },
355
+ reset() {
356
+ held.clear();
357
+ loading.clear();
358
+ failedAt.clear();
359
+ }
360
+ };
361
+ }
362
+
363
+ // src/errors/catalogue.ts
364
+ function clientMessages(messages, languages, clientCodes) {
365
+ const byLanguage = {};
366
+ for (const language of languages) {
367
+ const entries = Object.create(null);
368
+ for (const code of clientCodes) {
369
+ const entry = messages.messageOf(language, code);
370
+ if (entry)
371
+ entries[code] = entry;
372
+ }
373
+ byLanguage[language] = entries;
374
+ }
375
+ return byLanguage;
376
+ }
377
+ function createErrorCatalogue({
378
+ messages,
379
+ languages,
380
+ read,
381
+ versions,
382
+ ttlMs,
383
+ retryMs = 5000,
384
+ firstReadWaitMs = 1500,
385
+ clientCodes,
386
+ log: appLog = console
387
+ }) {
388
+ if (languages.length === 0) {
389
+ throw new TypeError("createErrorCatalogue(): needs at least one language.");
390
+ }
391
+ const log = quietLog(appLog, ["error"]);
392
+ const ttl = safeTtl(ttlMs, retryMs, log);
393
+ const reader = createCatalogueReader({
394
+ messages,
395
+ read,
396
+ retryMs,
397
+ firstReadWaitMs,
398
+ log
399
+ });
400
+ const context = { reader, versions, ttl, retryMs, log };
401
+ let checking;
402
+ async function fresh(language, now = Date.now()) {
403
+ const entry = reader.held.get(language);
404
+ if (!entry)
405
+ return reader.first(language, now);
406
+ if (now - entry.checkedAt >= ttl() && !checking) {
407
+ checking = checkVersions(context, now).finally(() => {
408
+ checking = undefined;
409
+ }).catch(() => {});
410
+ }
411
+ }
412
+ const freshAll = async (now = Date.now()) => {
413
+ await Promise.all(languages.map((language) => fresh(language, now)));
414
+ };
415
+ return {
416
+ fresh,
417
+ freshAll,
418
+ warm() {
419
+ freshAll().catch((error) => {
420
+ log.error("alxia-react-router: the error catalogue was not warmed", error);
421
+ });
422
+ },
423
+ clientMessages: () => clientMessages(messages, languages, clientCodes),
424
+ reset() {
425
+ reader.reset();
426
+ checking = undefined;
427
+ messages.clear();
428
+ },
429
+ heldVersion: (language) => reader.held.get(language)?.version,
430
+ settled: () => checking ?? Promise.resolve()
431
+ };
432
+ }
433
+ // src/errors/json-actions.ts
434
+ import { data as data4, redirect } from "react-router";
435
+
436
+ // src/errors/page-path.ts
437
+ function nextOf(url) {
438
+ const { pathname, searchParams } = new URL(url);
439
+ const trimmed = pathname.endsWith("/_.data") ? pathname.slice(0, -"_.data".length) : pathname.replace(/\.data$/, "");
440
+ const path = trimmed.replace(/^\/{2,}/, "/");
441
+ const params = new URLSearchParams(searchParams);
442
+ params.delete("_routes");
443
+ for (const value of params.getAll("index")) {
444
+ if (!value)
445
+ params.delete("index", value);
446
+ }
447
+ const search = params.toString();
448
+ return search ? `${path}?${search}` : path;
449
+ }
450
+
451
+ // src/errors/json-actions.ts
452
+ function jsonActions(options = {}) {
453
+ const ownOrigin = (request) => {
454
+ const { origin } = options;
455
+ const set = typeof origin === "function" ? origin() : origin;
456
+ return set ?? new URL(request.url).origin;
457
+ };
458
+ const isJson = (request) => request.headers.get("content-type")?.startsWith("application/json");
459
+ async function readJson(request) {
460
+ const origin = request.headers.get("origin");
461
+ const site = request.headers.get("sec-fetch-site");
462
+ if (origin && origin !== ownOrigin(request) || site && site !== "same-origin" && site !== "none") {
463
+ throw data4("Cross-site request refused", { status: 403 });
464
+ }
465
+ if (!isJson(request)) {
466
+ throw data4("Expected a JSON body", { status: 415 });
467
+ }
468
+ return request.json().catch(() => {
469
+ throw data4("Malformed JSON body", { status: 400 });
470
+ });
471
+ }
472
+ return {
473
+ readJson,
474
+ requireJsonPost(request) {
475
+ if (!isJson(request))
476
+ throw redirect(nextOf(request.url));
477
+ },
478
+ async readInput(request, schema) {
479
+ const body = await readJson(request);
480
+ const result = await schema["~standard"].validate(body?.input);
481
+ if (result.issues) {
482
+ throw data4("The form sent an input the API would refuse", {
483
+ status: 400
484
+ });
485
+ }
486
+ return result.value;
487
+ }
488
+ };
489
+ }
490
+ // src/errors/message-values.ts
491
+ var DAY = /^(\d{4})-(\d{2})-(\d{2})$/;
492
+ function dayOf(value) {
493
+ const parts = DAY.exec(value);
494
+ if (!parts)
495
+ return;
496
+ return new Date(Number(parts[1]), Number(parts[2]) - 1, Number(parts[3]));
497
+ }
498
+ function valuesFor(params, values) {
499
+ const worded = { ...values };
500
+ for (const { name, kind } of params) {
501
+ const value = values[name];
502
+ if (typeof value !== "string")
503
+ continue;
504
+ if (kind === "DATE") {
505
+ const day = dayOf(value);
506
+ if (day)
507
+ worded[name] = day;
508
+ } else if ((kind === "NUMBER" || kind === "PLURAL") && value.trim() !== "" && !Number.isNaN(Number(value))) {
509
+ worded[name] = Number(value);
510
+ }
511
+ }
512
+ return worded;
513
+ }
514
+
515
+ // src/errors/messages.ts
516
+ function createApiMessages({
517
+ format
518
+ }) {
519
+ const catalogues = new Map;
520
+ const messageOf = (language, code) => catalogues.get(language)?.get(code);
521
+ function formatEntry(entry, values, language) {
522
+ const worded = valuesFor(entry.params, values ?? {});
523
+ let failed = false;
524
+ try {
525
+ const text = format(entry.message, language, worded, {
526
+ onError: () => {
527
+ failed = true;
528
+ }
529
+ });
530
+ return failed ? undefined : text;
531
+ } catch {
532
+ return;
533
+ }
534
+ }
535
+ return {
536
+ set(language, messages) {
537
+ catalogues.set(language, new Map(Object.entries(messages)));
538
+ },
539
+ add(language, messages) {
540
+ const held = catalogues.get(language) ?? new Map;
541
+ for (const [code, message] of Object.entries(messages)) {
542
+ held.set(code, message);
543
+ }
544
+ catalogues.set(language, held);
545
+ },
546
+ clear() {
547
+ catalogues.clear();
548
+ },
549
+ messageOf,
550
+ format: formatEntry,
551
+ apiMessage(code, values, language) {
552
+ const entry = messageOf(language, code);
553
+ return entry ? formatEntry(entry, values, language) : undefined;
554
+ }
555
+ };
556
+ }
557
+ // src/errors/provider.ts
558
+ function ApiMessagesProvider({
559
+ messages,
560
+ value,
561
+ children
562
+ }) {
563
+ const inBrowser = typeof globalThis.document !== "undefined";
564
+ if (value && inBrowser) {
565
+ for (const [language, held] of Object.entries(value)) {
566
+ messages.add(language, held);
567
+ }
568
+ }
569
+ return children;
570
+ }
571
+ // src/errors/use-json-fetcher.ts
572
+ import { useFetcher } from "react-router";
573
+ function useJsonFetcher(key, action) {
574
+ const fetcher = useFetcher(key ? { key } : undefined);
575
+ const post = (body, to = action) => fetcher.submit(body, {
576
+ method: "post",
577
+ encType: "application/json",
578
+ ...to ? { action: to } : {}
579
+ });
580
+ return { fetcher, post, busy: fetcher.state !== "idle" };
581
+ }
582
+ export {
583
+ ApiMessagesProvider,
584
+ REQUEST_ID_HEADER,
585
+ apiCallFailure,
586
+ apiCodeResolver,
587
+ createApiErrors,
588
+ createApiMessages,
589
+ createErrorCatalogue,
590
+ errorRequestId,
591
+ isApiErrorLike,
592
+ jsonActions,
593
+ nextOf,
594
+ operationName,
595
+ requestIdOf,
596
+ routeError,
597
+ useJsonFetcher,
598
+ waitMinutes
599
+ };
600
+
601
+ //# debugId=0AF5BB2424365EAA64756E2164756E21
602
+ //# sourceMappingURL=index.js.map