@dokebi/template-inertia 0.4.0 → 0.4.1

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 CHANGED
@@ -1,6 +1,6 @@
1
- # AI guide — @dokebi/template-inertia 0.4.0
1
+ # AI guide — @dokebi/template-inertia 0.4.1
2
2
 
3
- An Inertia (v3) server adapter. The first request is answered with HTML (plus `script[data-page]` JSON), later `X-Inertia` requests with page JSON. It reads the Vite manifest to fill in scripts, styles and the version, and serves the build files itself. Optionally it server-renders the first HTML in the same process (SSR, D-0039). Vue and Vite are not server dependencies (they are the app's dev dependencies). Node >=24.0.0, core ^0.4.0, @dokebi/web (peer). Details are in the [README](README.md).
3
+ An Inertia (v3) server adapter. The first request is answered with HTML (plus `script[data-page]` JSON), later `X-Inertia` requests with page JSON. It reads the Vite manifest to fill in scripts, styles and the version, and serves the build files itself. Optionally it server-renders the first HTML in the same process (SSR, D-0039). Vue and Vite are not server dependencies (they are the app's dev dependencies). Node >=24.0.0, core ^0.4.0, @dokebi/web ^0.4.1 (peer). Details are in the [README](README.md).
4
4
 
5
5
  ## 1. Install and project layout
6
6
 
@@ -46,6 +46,7 @@ route.post('/orders', async (request, reply, page) => {
46
46
 
47
47
  - `render(request, reply, component, props?, { encryptHistory?, clearHistory? }?)`: GET/HEAD only. The fifth argument sets history handling for this response (otherwise the scope setting and the session signal). Return or await it in the handler. component is `/`-separated parts matching `[A-Za-z0-9][A-Za-z0-9_-]*`.
48
48
  - props: JSON values, async lazy functions or the helpers of §3.1. A partial reload (`only`, `except`) evaluates only the selected ones; `errors` is always included. Non-JSON values (`undefined`, functions, symbols, `NaN`, `Infinity`) fail with `TypeError: Page props must be JSON serializable`, BigInt and cycles with `JSON.stringify`'s `TypeError`. Development and tests (`NODE_ENV` not `production`) check every value of the page; production checks only the top-level props and leaves the rest to `JSON.stringify` (D-0068; a 30 KB page went from 112 to 24 µs), so this mistake is caught during development.
49
+ - Prop types in TypeScript: describe props with `type` aliases (`type Post = { id: number; title: string }`), not `interface`. An interface has no index signature, so TypeScript refuses it where `Json` is required (`npm run typecheck` fails with "Index signature for type 'string' is missing"); a `type` with the same fields is accepted (AI-M04).
49
50
  - `redirect(request, reply, localUrl)`: 302 for GET/HEAD, 303 for mutations; an Inertia `#` navigation gets 409 + `X-Inertia-Redirect`.
50
51
  - `location(request, reply, url)`: a local or external http(s) navigation. Inertia requests get 409 + `X-Inertia-Location`.
51
52
 
@@ -93,7 +94,7 @@ route.post('/orders', async (request, reply, page) => {
93
94
 
94
95
  **Refuse input with a validation error, never with your own 422.** A refused route schema (`schema.body`, `schema.querystring`) and `throw validationError({ name: 'Already taken' })` (from `@dokebi/web`) in the handler are the same 422 `VALIDATION_FAILED` with `fields` and `errors`. For an Inertia request (`X-Inertia`) this plugin answers it like Laravel does:
95
96
 
96
- - **With @dokebi/session**: the `errors` (`{ field: message }`) and the `old` input (the body's top-level strings, numbers and booleans, without password-, token-, secret- or card-like fields; left out above 8 KB) go into the session flash, and the answer is **303 back** to the `Referer` when it is this host (another origin is never followed), otherwise to `/` for a POST/PUT/PATCH/DELETE. The next page gets the flashed `errors` as its **`errors` prop** (under errors the handler passed itself) and `old` stays in the page `flash` (`usePage().flash.old`). The client keeps the form's own state on a non-GET visit, so `useForm().errors.name` just shows the message. A GET refused by its query schema without a usable referer (a redirect to itself would loop) gets the JSON below.
97
+ - **With @dokebi/session**: the `errors` (`{ field: message }`) and the `old` input (the body's top-level strings, numbers and booleans, without password-, token-, secret- or card-like fields; left out above 8 KB) go into the session flash, and the answer is **303 back** to the `Referer` when it is this host (another origin is never followed), otherwise to `/` for a POST/PUT/PATCH/DELETE. The next page gets the flashed `errors` as its **`errors` prop** (under errors the handler passed itself) and `old` stays in the page `flash` (`usePage().flash.old`). The client keeps the form's own state on a non-GET visit, so `useForm().errors.name` just shows the message. A GET refused by its query schema without a usable referer (a redirect to itself would loop) gets the JSON below. The target and the old input come from @dokebi/web's form helpers (`formBackTarget`, `formOldInput`, 0.4.1), the same rules @dokebi/app uses for HTML forms.
97
98
  - **Without a session** the answer is the 422 JSON with `errors`: Inertia hands a non-page answer to `onHttpException`, so read it there: `form.post(url, { onHttpException: (response) => { form.setError(response.data.errors); return false } })`.
98
99
  - **Submit with `only`.** The GET after the 303 is a new visit, and a full visit must send every prop of the page again (Inertia replaces all props, so the server cannot trim it by itself). Pass the props the submit changes and Inertia keeps the partial headers across the redirect; `errors` always comes along:
99
100
 
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # @dokebi/template-inertia 0.4.0
1
+ # @dokebi/template-inertia 0.4.1
2
2
 
3
3
  An optional package for Node >=24.0.0 and core ^0.4.0 (public on npm). Its ID is `dokebi-template-inertia` and it depends on `dokebi-web`. `@dokebi/web` is a peer dependency (not the pinned dependency it used to be). EJS, Vue and Vite are not installed as required dependencies of the server package.
4
4
 
@@ -47,7 +47,7 @@ Supported: the initial HTML, page navigation, shared/lazy/partial props, deferre
47
47
 
48
48
  ## Forms and validation
49
49
 
50
- Validation follows [D-0074](https://git.nyx-zone.com/dokebi/framework/-/blob/main/docs/decisions/D-0074-error-responses.md). A refused route schema and `throw validationError({ field: 'message' })` (from `@dokebi/web`) are the same 422 `VALIDATION_FAILED` answer. For an Inertia request with [@dokebi/session](https://git.nyx-zone.com/dokebi/framework/-/blob/main/plugins/dokebi-session/README.md) this plugin flashes `errors` (and `old` input without password-like fields) and answers 303 back to the same-origin `Referer`; the next page gets them in its `errors` prop, so `useForm().errors.name` shows the message. Without a session the answer is the 422 JSON with `errors` (read it in `onHttpException`).
50
+ Validation follows [D-0074](https://git.nyx-zone.com/dokebi/framework/-/blob/main/docs/decisions/D-0074-error-responses.md). A refused route schema and `throw validationError({ field: 'message' })` (from `@dokebi/web`) are the same 422 `VALIDATION_FAILED` answer. For an Inertia request with [@dokebi/session](https://git.nyx-zone.com/dokebi/framework/-/blob/main/plugins/dokebi-session/README.md) this plugin flashes `errors` (and `old` input without password-like fields) and answers 303 back to the same-origin `Referer`; the next page gets them in its `errors` prop, so `useForm().errors.name` shows the message. Without a session the answer is the 422 JSON with `errors` (read it in `onHttpException`). The target and the old input follow @dokebi/web's form helpers (0.4.1, shared with @dokebi/app's HTML forms), so this version needs @dokebi/web ^0.4.1.
51
51
 
52
52
  Submit forms with `only` (the props the submit changes, for example `form.post('/posts', { only: ['posts'] })`): the GET after the redirect is a new visit, and a full visit must send every prop again because Inertia replaces all props, so the server cannot trim it by itself. Inertia keeps the partial headers across the redirect, and `errors` is always included. `dokebi check` warns about a submit without `only` (`inertia-form-only`).
53
53
 
package/dist/index.js CHANGED
@@ -4,9 +4,9 @@ import { readFile, stat } from 'node:fs/promises';
4
4
  import { basename, dirname, extname, join, resolve } from 'node:path';
5
5
  import { pathToFileURL } from 'node:url';
6
6
  import { types } from 'node:util';
7
- import { definePlugin, optionalDependency, } from '@dokebi/core';
7
+ import { definePlugin, optionalDependency, providerDependency, } from '@dokebi/core';
8
8
  import { diagnostics } from '@dokebi/core/libraries';
9
- import { dependency as web } from '@dokebi/web/dependency';
9
+ import { formBackTarget, formOldInput } from '@dokebi/web';
10
10
  import { viewEngines } from '@dokebi/web/points';
11
11
  import { InertiaError, InertiaUsageError } from './errors.js';
12
12
  import { options } from './options.js';
@@ -154,54 +154,16 @@ function hasCookie(header, name) {
154
154
  return text.split(';').some((part) => part.slice(0, part.indexOf('=')).trim() === name);
155
155
  }
156
156
  /**
157
- * Where a validation redirect goes (D-0074 §3): the `Referer` when it is this host (another
158
- * origin is never followed); otherwise `/` for a POST, PUT, PATCH or DELETE. A GET whose referer
159
- * is missing or is itself has no way back (a redirect to itself would fail again, forever).
157
+ * dokebi-web 0.4.1 or later: the validation redirect uses its form helpers (`formBackTarget`,
158
+ * `formOldInput`, D-0074 addendum 1), shared with @dokebi/app. A local descriptor instead of
159
+ * '@dokebi/web/dependency' (whose range stays ^0.4.0 for the plugins that need nothing newer).
160
160
  */
161
- function backTarget(request, host) {
162
- const referer = request.headers.referer;
163
- if (typeof referer === 'string' && referer.length <= 4096) {
164
- let url;
165
- if (URL.canParse(referer))
166
- url = new URL(referer);
167
- if (url &&
168
- (url.protocol === 'http:' || url.protocol === 'https:') &&
169
- url.host.toLowerCase() === host.toLowerCase()) {
170
- const target = url.pathname + url.search;
171
- if (!(['GET', 'HEAD'].includes(request.method) && target === request.url))
172
- return target;
173
- }
174
- }
175
- return ['GET', 'HEAD'].includes(request.method) ? undefined : '/';
176
- }
177
- /** Field names whose values never go into the session as old input. */
178
- const SECRET_FIELD = /pass(?:word|wd|phrase)?|secret|token|api[_-]?key|card|cvc|cvv|otp|pin$/i;
179
- /** Old input kept at most (as JSON text): a larger body is not flashed. */
180
- const OLD_MAX = 8192;
181
- /**
182
- * The old input of a refused form (D-0074 §3): the body's top-level strings, numbers, booleans
183
- * and nulls, without password-like fields; undefined when nothing is left or it is too large.
184
- */
185
- function oldInput(body) {
186
- if (body === null || typeof body !== 'object' || Array.isArray(body))
187
- return undefined;
188
- const old = {};
189
- let kept = 0;
190
- for (const [key, value] of Object.entries(body)) {
191
- if (key === '__proto__' || SECRET_FIELD.test(key))
192
- continue;
193
- if (value === null ||
194
- typeof value === 'boolean' ||
195
- (typeof value === 'number' && Number.isFinite(value)) ||
196
- typeof value === 'string') {
197
- old[key] = value;
198
- kept++;
199
- }
200
- }
201
- if (!kept || JSON.stringify(old).length > OLD_MAX)
202
- return undefined;
203
- return old;
204
- }
161
+ const web = providerDependency({
162
+ name: 'dokebi-web',
163
+ version: '^0.4.1',
164
+ packageName: '@dokebi/web',
165
+ load: () => import('@dokebi/web'),
166
+ });
205
167
  const session = optionalDependency({
206
168
  name: 'dokebi-session',
207
169
  version: '^0.4.0',
@@ -292,7 +254,7 @@ class InertiaRuntime {
292
254
  // D-0074 §3: flash errors and old input, 303 back; the next page gets `errors`.
293
255
  validation: async (request, reply, errors) => {
294
256
  const current = sessionOf(request);
295
- const target = backTarget(request, server.requestHost(request));
257
+ const target = formBackTarget(request, server.requestHost(request), '/');
296
258
  if (!current?.flash || target === undefined) {
297
259
  if (logger.isEnabled('debug'))
298
260
  logger.log('debug', 'Inertia validation answered as JSON', {
@@ -303,7 +265,7 @@ class InertiaRuntime {
303
265
  return false;
304
266
  }
305
267
  await current.flash('errors', { ...errors });
306
- const old = oldInput(request.body);
268
+ const old = formOldInput(request.body);
307
269
  if (old)
308
270
  await current.flash('old', old);
309
271
  if (logger.isEnabled('debug'))
@@ -622,7 +584,7 @@ class InertiaRuntime {
622
584
  }
623
585
  export const createPlugin = definePlugin({
624
586
  name: 'dokebi-template-inertia',
625
- version: '0.4.0',
587
+ version: '0.4.1',
626
588
  core: '^0.4.0',
627
589
  options,
628
590
  entry: parseEntry,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dokebi/template-inertia",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
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",
@@ -41,7 +41,7 @@
41
41
  },
42
42
  "peerDependencies": {
43
43
  "@dokebi/core": "^0.4.0",
44
- "@dokebi/web": "^0.4.0",
44
+ "@dokebi/web": "^0.4.1",
45
45
  "fastify": "^5.12.7"
46
46
  },
47
47
  "dokebi": {
@@ -51,7 +51,7 @@
51
51
  "dependencies": [
52
52
  {
53
53
  "name": "dokebi-web",
54
- "version": "^0.4.0"
54
+ "version": "^0.4.1"
55
55
  },
56
56
  {
57
57
  "name": "dokebi-session",
@@ -114,7 +114,7 @@
114
114
  },
115
115
  "devDependencies": {
116
116
  "@dokebi/session": "0.4.0",
117
- "@dokebi/web": "0.4.0"
117
+ "@dokebi/web": "0.4.1"
118
118
  },
119
119
  "publishConfig": {
120
120
  "access": "public",