@dokebi/template-inertia 0.4.6 → 0.4.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/AI.md +1 -0
- package/dist/i18n.js +6 -2
- package/dist/index.js +1 -1
- package/dist/responder.js +38 -7
- package/package.json +6 -6
package/AI.md
CHANGED
|
@@ -59,6 +59,7 @@ route.post('/orders', async (request, reply, page) => {
|
|
|
59
59
|
- The Vue side is type-checked too: `dokebi app add --inertia` makes `npm run typecheck` run `vue-tsc` after `tsc` (`node typecheck-vue.mjs -p tsconfig.vue.json`), which checks the `<script>` and the `<template>` of every `.vue` file. Write pages as `<script setup lang="ts">` with `defineProps<{ … }>()`; the shared props (`$page.props`, `usePage().props`) are typed in `src/infra/inertia/page.d.ts` (add a key there for what `src/infra/inertia/shared.ts` returns), and a prop only some pages get with `usePage<{ … }>()`. A Vite build is not a Vue type check.
|
|
60
60
|
- `redirect(request, reply, localUrl)`: 302 for GET/HEAD, 303 for mutations; an Inertia `#` navigation gets 409 + `X-Inertia-Redirect`.
|
|
61
61
|
- `location(request, reply, url)`: a local or external http(s) navigation. Inertia requests get 409 + `X-Inertia-Location`.
|
|
62
|
+
- Both write the URL into a response header: characters outside printable ASCII (a Korean path or query value) are percent-encoded as UTF-8 for you, and what is already encoded (`%EA%B0%80`) stays as it is; text that is not well formed (a lone surrogate) is `INVALID_URL`.
|
|
62
63
|
|
|
63
64
|
### 3.1 deferred, merge and once props (D-0052)
|
|
64
65
|
|
package/dist/i18n.js
CHANGED
|
@@ -267,7 +267,10 @@ export class I18n {
|
|
|
267
267
|
const formats = this.#formatsOf(locale);
|
|
268
268
|
let formatter = formats.numbers.get(format);
|
|
269
269
|
if (!formatter) {
|
|
270
|
-
|
|
270
|
+
// Own names only: the settings are parsed JSON, where `constructor` or `toString` would
|
|
271
|
+
// name an Object.prototype member and format with the default (audit R9-g13-7).
|
|
272
|
+
const presets = this.#settings.numberFormats;
|
|
273
|
+
const preset = format === 'default' ? {} : Object.hasOwn(presets, format) ? presets[format] : undefined;
|
|
271
274
|
if (!preset)
|
|
272
275
|
throw new PageI18nError('INVALID_FORMAT', 'Unknown number format');
|
|
273
276
|
try {
|
|
@@ -294,7 +297,8 @@ export class I18n {
|
|
|
294
297
|
const formats = this.#formatsOf(locale);
|
|
295
298
|
let formatter = formats.dates.get(format);
|
|
296
299
|
if (!formatter) {
|
|
297
|
-
const
|
|
300
|
+
const presets = this.#settings.dateFormats;
|
|
301
|
+
const preset = format === 'default' ? {} : Object.hasOwn(presets, format) ? presets[format] : undefined;
|
|
298
302
|
if (!preset)
|
|
299
303
|
throw new PageI18nError('INVALID_FORMAT', 'Unknown date format');
|
|
300
304
|
try {
|
package/dist/index.js
CHANGED
package/dist/responder.js
CHANGED
|
@@ -41,6 +41,36 @@ function localUrl(value) {
|
|
|
41
41
|
throw new InertiaUsageError('INVALID_URL', 'Expected a local absolute URL');
|
|
42
42
|
return value;
|
|
43
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* A checked redirect or location URL as a response header can carry it (audit R9-g13-3). Node
|
|
46
|
+
* refuses a header value with a character above U+00FF, and Fastify then destroys the socket:
|
|
47
|
+
* a redirect to a path with Korean text was a dropped connection, not a redirect.
|
|
48
|
+
* Printable ASCII (`%XX` included) is written as it is; anything else is percent-encoded as
|
|
49
|
+
* UTF-8, as a browser does with the same text in a link (an external URL through the URL
|
|
50
|
+
* parser, so its host becomes punycode). Text that is not well formed (a lone surrogate) is
|
|
51
|
+
* INVALID_URL.
|
|
52
|
+
*/
|
|
53
|
+
const LONE_SURROGATE = /[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]/;
|
|
54
|
+
function headerUrl(url) {
|
|
55
|
+
if (/^[\x21-\x7e]*$/.test(url))
|
|
56
|
+
return url;
|
|
57
|
+
if (LONE_SURROGATE.test(url))
|
|
58
|
+
throw new InertiaUsageError('INVALID_URL', 'Expected a URL of well-formed text');
|
|
59
|
+
return url.startsWith('/')
|
|
60
|
+
? url.replace(/[^\x21-\x7e]+/g, (text) => encodeURIComponent(text))
|
|
61
|
+
: new URL(url).href;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The location of a version mismatch (409): the request's own path and query. A request target
|
|
65
|
+
* is client input: `//host/x` or `/\host/x` as a location would send the browser to that host
|
|
66
|
+
* (and was refused with INVALID_URL, a 500 any client could ask for; audit R9-g13-4), so the
|
|
67
|
+
* leading slashes become one and a backslash is written `%5C`. A target that is no path (`*`,
|
|
68
|
+
* an absolute form) goes to `/`.
|
|
69
|
+
*/
|
|
70
|
+
function ownLocation(url) {
|
|
71
|
+
const path = url.replace(/^\/+/, '/').replace(/\\/g, '%5C');
|
|
72
|
+
return path.startsWith('/') ? path : '/';
|
|
73
|
+
}
|
|
44
74
|
export function vary(reply) {
|
|
45
75
|
const values = String(reply.getHeader('vary') ?? '')
|
|
46
76
|
.split(',')
|
|
@@ -301,7 +331,7 @@ export class InertiaResponder {
|
|
|
301
331
|
request.headers['x-inertia-version'] !== this.options.version) {
|
|
302
332
|
return reply
|
|
303
333
|
.code(409)
|
|
304
|
-
.header('X-Inertia-Location',
|
|
334
|
+
.header('X-Inertia-Location', ownLocation(request.url))
|
|
305
335
|
.header('X-Inertia-Version', this.options.version)
|
|
306
336
|
.send();
|
|
307
337
|
}
|
|
@@ -553,11 +583,11 @@ export class InertiaResponder {
|
|
|
553
583
|
}
|
|
554
584
|
redirect(request, reply, url) {
|
|
555
585
|
this.#check();
|
|
556
|
-
localUrl(url);
|
|
586
|
+
const target = headerUrl(localUrl(url));
|
|
557
587
|
vary(reply);
|
|
558
|
-
if (request.headers['x-inertia'] === 'true' &&
|
|
559
|
-
return reply.code(409).header('X-Inertia-Redirect',
|
|
560
|
-
return reply.redirect(
|
|
588
|
+
if (request.headers['x-inertia'] === 'true' && target.includes('#'))
|
|
589
|
+
return reply.code(409).header('X-Inertia-Redirect', target).send();
|
|
590
|
+
return reply.redirect(target, ['GET', 'HEAD'].includes(request.method) ? 302 : 303);
|
|
561
591
|
}
|
|
562
592
|
location(request, reply, url) {
|
|
563
593
|
this.#check();
|
|
@@ -568,10 +598,11 @@ export class InertiaResponder {
|
|
|
568
598
|
!/^https?:$/.test(new URL(url).protocol) ||
|
|
569
599
|
/[\r\n]/.test(url))
|
|
570
600
|
throw new InertiaUsageError('INVALID_URL', 'Invalid location');
|
|
601
|
+
const target = headerUrl(url);
|
|
571
602
|
vary(reply);
|
|
572
603
|
return request.headers['x-inertia'] === 'true'
|
|
573
|
-
? reply.code(409).header('X-Inertia-Location',
|
|
574
|
-
: reply.redirect(
|
|
604
|
+
? reply.code(409).header('X-Inertia-Location', target).send()
|
|
605
|
+
: reply.redirect(target, 302);
|
|
575
606
|
}
|
|
576
607
|
}
|
|
577
608
|
const RESCUED = Symbol('rescued');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dokebi/template-inertia",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.7",
|
|
4
4
|
"description": "Inertia v3 server adapter for Dokebi: page JSON, Vite manifest assets and optional server-side rendering.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dokebi",
|
|
@@ -135,11 +135,11 @@
|
|
|
135
135
|
]
|
|
136
136
|
},
|
|
137
137
|
"devDependencies": {
|
|
138
|
-
"@dokebi/i18n": "0.2.
|
|
139
|
-
"@dokebi/session": "0.4.
|
|
140
|
-
"@dokebi/web": "0.4.
|
|
141
|
-
"@dokebi/websocket": "0.4.
|
|
142
|
-
"@dokebi/websocket-hub": "0.4.
|
|
138
|
+
"@dokebi/i18n": "0.2.4",
|
|
139
|
+
"@dokebi/session": "0.4.7",
|
|
140
|
+
"@dokebi/web": "0.4.7",
|
|
141
|
+
"@dokebi/websocket": "0.4.3",
|
|
142
|
+
"@dokebi/websocket-hub": "0.4.3",
|
|
143
143
|
"@inertiajs/vue3": "3.9.1",
|
|
144
144
|
"vue": "3.5.44"
|
|
145
145
|
},
|