@zerotal/inertia 1.8.0 → 1.9.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.
- package/CHANGELOG.md +44 -0
- package/api-surface.md +0 -20
- package/package.json +2 -2
- package/src/inertia.ts +66 -16
- package/src/props/PropTypes.ts +2 -0
- package/src/props/propBoundary.ts +161 -0
- package/src/props/resolveProps.ts +5 -1
- package/src/types.ts +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,50 @@ follows the Zerotal monorepo's unified versioning.
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [1.9.0] — 2026-08-29
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- **A rebuilt bundle no longer 404s on a chunk the browser asks for.** `resources/js/app.tsx`
|
|
16
|
+
builds to `/assets/app.js` under that name every time, while `splitting: true` names each chunk
|
|
17
|
+
after its content. A rebuild therefore rewrites `app.js` to import `chunk-NEW.js` and prunes
|
|
18
|
+
`chunk-OLD.js` — and a browser holding a cached `app.js` asks for the pruned one and gets
|
|
19
|
+
|
|
20
|
+
GET /assets/chunk-hrnspqda.js status=404
|
|
21
|
+
|
|
22
|
+
from a page that renders and a server that is healthy. Nothing in that line leads back to the
|
|
23
|
+
template.
|
|
24
|
+
|
|
25
|
+
The template hardcodes `/assets/app.js` rather than calling `asset()`, so the version token the
|
|
26
|
+
rest of the framework appends never reached it, and cache-busting had only ever been
|
|
27
|
+
implemented for `--dev-worker`. It now applies everywhere: the file's mtime in dev, where a
|
|
28
|
+
rebuild happens without a restart, and the boot-derived asset version otherwise, memoised
|
|
29
|
+
because a deploy restarts the process. An unchanged asset keeps a stable URL and stays cached,
|
|
30
|
+
which is why the token is derived rather than random.
|
|
31
|
+
|
|
32
|
+
### Documented
|
|
33
|
+
|
|
34
|
+
- **Every promised export is documented.** The `docs-coverage` gate reads `maturity: stable` as a
|
|
35
|
+
promise about a package's exports, and measures how much of that promise is written down. It
|
|
36
|
+
was 798 gaps across the suite; it is now zero. This package's share is covered on its own
|
|
37
|
+
pages — types named, options shapes described, and the decisions behind them recorded where
|
|
38
|
+
somebody looking for them will find them.
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- **A warning when a model crosses into page props having declared no boundary.** Page props
|
|
43
|
+
are page source: everything handed to `inertia()` is serialised into the document, and
|
|
44
|
+
`return inertia("Trips/Show", { trip })` is what a newcomer writes on their first afternoon
|
|
45
|
+
and ships the whole row — the internal cost, the margin, the note about the customer, on
|
|
46
|
+
the customer's own screen. Nothing fails and the page looks right, which makes it the one
|
|
47
|
+
mistake here that never announces itself.
|
|
48
|
+
|
|
49
|
+
The ORM's `hidden` / `visible` lists were already honoured, since they are applied by
|
|
50
|
+
`toJSON()` and that is what serialises a prop — nothing said so. In development, passing a
|
|
51
|
+
model that declares neither list now names the model and the number of fields it is about
|
|
52
|
+
to publish. It fires once per model class and goes quiet as soon as either list exists, so
|
|
53
|
+
the normal case of passing models stays quiet.
|
|
54
|
+
|
|
11
55
|
## [1.8.0] — 2026-08-24
|
|
12
56
|
|
|
13
57
|
### Fixed
|
package/api-surface.md
CHANGED
|
@@ -256,12 +256,6 @@ interface InertiaDevtoolsConfig = {
|
|
|
256
256
|
|
|
257
257
|
interface InertiaPageRegistry = {}
|
|
258
258
|
|
|
259
|
-
interface InertiaProviderOptions = {
|
|
260
|
-
assetsUrl?: string
|
|
261
|
-
htmlTemplate?: string
|
|
262
|
-
version?: string
|
|
263
|
-
}
|
|
264
|
-
|
|
265
259
|
interface MergeConfig = {
|
|
266
260
|
appendPaths: string[]
|
|
267
261
|
deep: boolean
|
|
@@ -303,18 +297,6 @@ interface PaginatorLike = {
|
|
|
303
297
|
total?: number
|
|
304
298
|
}
|
|
305
299
|
|
|
306
|
-
interface ResolvedPage = {
|
|
307
|
-
deepMergeProps?: string[]
|
|
308
|
-
deferredProps?: Record<string, string[]>
|
|
309
|
-
matchPropsOn?: string[]
|
|
310
|
-
mergeProps?: string[]
|
|
311
|
-
onceProps?: Record<string, { prop: string; expiresAt: number | null;}>
|
|
312
|
-
prependProps?: string[]
|
|
313
|
-
props: Record<string, unknown>
|
|
314
|
-
rescuedProps?: string[]
|
|
315
|
-
scrollProps?: Record<string, ScrollConfig>
|
|
316
|
-
}
|
|
317
|
-
|
|
318
300
|
interface ScrollConfig = {
|
|
319
301
|
currentPage: number | null
|
|
320
302
|
nextPage: number | null
|
|
@@ -328,8 +310,6 @@ type PageName = never
|
|
|
328
310
|
|
|
329
311
|
type PageTarget = string
|
|
330
312
|
|
|
331
|
-
type PropFactory = () => T | Promise<T>
|
|
332
|
-
|
|
333
313
|
type PropInput = T | (() => T | Promise<T>) | AlwaysProp<T> | MergeProp<T> | (T extends PaginatorLike ? InfiniteScrollProp : never) | (undefined extends T ? OptionalProp<T> | DeferProp<T> : never)
|
|
334
314
|
|
|
335
315
|
type PropsOf = PageComponent<N> extends (props: infer Props, ...rest: any[]) => any ? Props : PageComponent<N> extends abstract new (props: infer Props, ...rest: any[]) => any ? Props : Record<string, unknown>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zerotal/inertia",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"maturity": "stable",
|
|
6
6
|
"private": false,
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"typecheck": "tsc --noEmit"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@zerotal/core": "1.
|
|
36
|
+
"@zerotal/core": "1.9.0"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"react": "^18 || ^19",
|
package/src/inertia.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { config, RequestContext } from "@zerotal/core";
|
|
2
|
+
import { assetVersion as coreAssetVersion } from "@zerotal/core/assets";
|
|
2
3
|
import { statSync } from "node:fs";
|
|
3
4
|
import { InertiaTemplateNotLoadedError, InvalidComponentError } from "./errors.ts";
|
|
4
5
|
import { DEFAULT_PAGES_DIR } from "./config.ts";
|
|
5
6
|
import { sharedProps } from "./SharedProps.ts";
|
|
6
7
|
import { assetVersion } from "./version.ts";
|
|
7
8
|
import { resolveProps } from "./props/resolveProps.ts";
|
|
9
|
+
import { checkPropBoundary } from "./props/propBoundary.ts";
|
|
8
10
|
import { readHistoryFlags } from "./historyState.ts";
|
|
9
11
|
import { allSharedKeys } from "./share.ts";
|
|
10
12
|
import { recordPage } from "./devtools/recorder.ts";
|
|
@@ -81,6 +83,12 @@ export async function buildPageObject(
|
|
|
81
83
|
// a no-op unless the DevTools recorder opened a recording for this request.
|
|
82
84
|
recordPage(component, merged, resolved.props, resolved.rescuedProps);
|
|
83
85
|
|
|
86
|
+
// Everything below this line is page source. Last chance to say so — and the
|
|
87
|
+
// only place that sees the props as they will actually be serialised, after
|
|
88
|
+
// partial reloads and lazy wrappers have decided what is really going out.
|
|
89
|
+
// Development only; a no-op on a served request.
|
|
90
|
+
checkPropBoundary(resolved.props);
|
|
91
|
+
|
|
84
92
|
const history = readHistoryFlags();
|
|
85
93
|
if (history.encryptHistory) page.encryptHistory = true;
|
|
86
94
|
if (history.clearHistory) page.clearHistory = true;
|
|
@@ -103,6 +111,9 @@ let _pagesDir = "";
|
|
|
103
111
|
*/
|
|
104
112
|
export function _setHtmlTemplate(html: string): void {
|
|
105
113
|
_htmlTemplate = html;
|
|
114
|
+
// The busted copy was derived from the previous template. Keeping it would
|
|
115
|
+
// serve the old markup for as long as the asset token happened to match.
|
|
116
|
+
_resetBustedTemplate();
|
|
106
117
|
}
|
|
107
118
|
|
|
108
119
|
/**
|
|
@@ -136,31 +147,70 @@ export function _getPagesDir(): string {
|
|
|
136
147
|
return _pagesDir || `${process.cwd()}/${config.safe("inertia.pagesDir", DEFAULT_PAGES_DIR)}`;
|
|
137
148
|
}
|
|
138
149
|
|
|
150
|
+
/** Local `href="/…"` / `src="/…"` JS and CSS URLs that carry no query already. */
|
|
151
|
+
const _LOCAL_ASSET_URL = /((?:href|src)=")(\/[^"?]+\.(?:js|css))(")/g;
|
|
152
|
+
|
|
139
153
|
/**
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
154
|
+
* Append a cache-busting `?v=…` to the template's local JS/CSS URLs.
|
|
155
|
+
*
|
|
156
|
+
* **The entry point is not content-hashed and the chunks are**, which is the
|
|
157
|
+
* whole reason this exists. `app.tsx` builds to `/assets/app.js` under that name
|
|
158
|
+
* every time, while `splitting: true` names each chunk after its content — so a
|
|
159
|
+
* rebuild rewrites `app.js` to import `chunk-NEW.js` and prunes `chunk-OLD.js`.
|
|
160
|
+
* A browser holding a cached `app.js` then asks for a chunk that is no longer on
|
|
161
|
+
* disk and gets a 404, from a page that renders fine and a server that is
|
|
162
|
+
* perfectly healthy. The stack says `GET /assets/chunk-….js 404` and names
|
|
163
|
+
* nothing that would lead you here.
|
|
164
|
+
*
|
|
165
|
+
* The template hardcodes `/assets/app.js` rather than calling `asset()`, so the
|
|
166
|
+
* version token the rest of the framework appends never reached it. Two token
|
|
167
|
+
* sources, because the two runtimes invalidate at different moments:
|
|
143
168
|
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
169
|
+
* - **Dev (`--dev-worker`)**: the file's mtime, read per request. A rebuild
|
|
170
|
+
* happens without a restart, so a token fixed at boot would be stale exactly
|
|
171
|
+
* when it matters.
|
|
172
|
+
* - **Everywhere else**: the boot-derived asset version, memoised. A deploy
|
|
173
|
+
* restarts the process — the pipeline says so in as many words — so the token
|
|
174
|
+
* is computed once and reused, rather than stat-ing files on every render.
|
|
175
|
+
*
|
|
176
|
+
* Unchanged assets keep a stable URL and stay cached; that is the point of a
|
|
177
|
+
* derived token rather than a random one.
|
|
147
178
|
*
|
|
148
179
|
* @internal
|
|
149
180
|
*/
|
|
150
|
-
function
|
|
151
|
-
if (
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
/((?:href|src)=")(\/[^"?]+\.(?:js|css))(")/g,
|
|
155
|
-
(match, pre: string, url: string, post: string) => {
|
|
181
|
+
export function _bustAssets(html: string): string {
|
|
182
|
+
if (process.argv.includes("--dev-worker")) {
|
|
183
|
+
const root = `${process.cwd()}/public`;
|
|
184
|
+
return html.replace(_LOCAL_ASSET_URL, (match, pre: string, url: string, post: string) => {
|
|
156
185
|
try {
|
|
157
186
|
const mtime = Math.floor(statSync(`${root}${url}`).mtimeMs);
|
|
158
187
|
return `${pre}${url}?v=${mtime}${post}`;
|
|
159
188
|
} catch {
|
|
160
189
|
return match; // asset not found under public/ — leave the URL untouched
|
|
161
190
|
}
|
|
162
|
-
}
|
|
163
|
-
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const token = coreAssetVersion();
|
|
195
|
+
// No token means nothing derived one — an app serving no built assets, or a
|
|
196
|
+
// boot order that has not reached the conventions phase. Leave the URLs alone
|
|
197
|
+
// rather than stamping `?v=` and inventing a second URL for the same file.
|
|
198
|
+
if (!token) return html;
|
|
199
|
+
if (_bustedFor === token) return _bustedHtml;
|
|
200
|
+
|
|
201
|
+
_bustedHtml = html.replace(_LOCAL_ASSET_URL, `$1$2?v=${token}$3`);
|
|
202
|
+
_bustedFor = token;
|
|
203
|
+
return _bustedHtml;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Memoised output of {@link _bustAssets}, keyed on the token it was built with. */
|
|
207
|
+
let _bustedHtml = "";
|
|
208
|
+
let _bustedFor = "";
|
|
209
|
+
|
|
210
|
+
/** Drop the memoised template. Tests, and any caller that replaces the template. @internal */
|
|
211
|
+
export function _resetBustedTemplate(): void {
|
|
212
|
+
_bustedHtml = "";
|
|
213
|
+
_bustedFor = "";
|
|
164
214
|
}
|
|
165
215
|
|
|
166
216
|
/**
|
|
@@ -215,7 +265,7 @@ async function _inertiaStream(component: string, props: Record<string, unknown>)
|
|
|
215
265
|
|
|
216
266
|
const pageObject = await buildPageObject(component, props);
|
|
217
267
|
|
|
218
|
-
const [prefix = "", suffix = ""] =
|
|
268
|
+
const [prefix = "", suffix = ""] = _bustAssets(_htmlTemplate).split("<!-- @inertia -->");
|
|
219
269
|
|
|
220
270
|
const { modPath, framework } = await resolvePageModule(_getPagesDir(), component);
|
|
221
271
|
const encoder = new TextEncoder();
|
|
@@ -394,7 +444,7 @@ async function _inertia(component: string, props: Record<string, unknown>): Prom
|
|
|
394
444
|
.replace(/&/g, "\\u0026")
|
|
395
445
|
.replace(/\//g, "\\/");
|
|
396
446
|
|
|
397
|
-
const html =
|
|
447
|
+
const html = _bustAssets(_htmlTemplate).replace(
|
|
398
448
|
"<!-- @inertia -->",
|
|
399
449
|
`<div id="app"></div>\n ` +
|
|
400
450
|
`<script type="application/json" data-page="app">${safeJson}</script>`,
|
package/src/props/PropTypes.ts
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
* Generic so a wrapper can carry what it will resolve to — that is what lets
|
|
15
15
|
* `Inertia.render` check `defer(() => stats())` against the `stats` prop the
|
|
16
16
|
* page component declares, instead of checking that *something* was passed.
|
|
17
|
+
*
|
|
18
|
+
* @internal
|
|
17
19
|
*/
|
|
18
20
|
export type PropFactory<T = unknown> = () => T | Promise<T>;
|
|
19
21
|
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The boundary between a model and a page prop.
|
|
3
|
+
*
|
|
4
|
+
* Inertia page props are page source. Everything handed to `inertia()` is
|
|
5
|
+
* serialised into the HTML document — or returned as JSON on an XHR visit — and
|
|
6
|
+
* anybody who views source reads all of it. That is not a leak in itself; it is
|
|
7
|
+
* how the protocol works. It becomes a leak because of what the obvious code does:
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* return inertia("Trips/Show", { trip }); // the first thing anyone writes
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* `trip` is a model, and a model serialises the row. Every column. The internal
|
|
14
|
+
* cost, the margin, the note somebody left about the customer — all of it, in the
|
|
15
|
+
* page, on the customer's own screen. Nothing fails, nothing logs, and the page
|
|
16
|
+
* looks right.
|
|
17
|
+
*
|
|
18
|
+
* The ORM already has the answer: `static hidden` and `static visible` are honoured
|
|
19
|
+
* by `toJSON()`, which is what serialises a prop. Declaring the dangerous columns
|
|
20
|
+
* once at the model is strictly better than remembering a projection at every call
|
|
21
|
+
* site. What was missing is anything that says so at the moment it matters.
|
|
22
|
+
*
|
|
23
|
+
* So this warns — in development only, once per model class — when a model reaches
|
|
24
|
+
* page props having declared neither list. Not "you passed a model", which is
|
|
25
|
+
* normal and fine, but "you passed a model that has never said which of its columns
|
|
26
|
+
* are safe to publish". A model with either list declared is silent forever after.
|
|
27
|
+
*
|
|
28
|
+
* @module
|
|
29
|
+
*/
|
|
30
|
+
import { deployEnv, isDevSurfaceAllowed } from "@zerotal/core";
|
|
31
|
+
|
|
32
|
+
/** A value that serialises like an ORM model. */
|
|
33
|
+
interface ModelLike {
|
|
34
|
+
toJSON(): unknown;
|
|
35
|
+
constructor: { name: string; hidden?: unknown; visible?: unknown };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Model classes already reported. Keyed by the class itself, so two models with the
|
|
40
|
+
* same name are two findings and a hot path never re-formats a warning nobody
|
|
41
|
+
* needs to read twice.
|
|
42
|
+
*/
|
|
43
|
+
const _warned = new WeakSet<object>();
|
|
44
|
+
|
|
45
|
+
/** How deep to look for models inside props. */
|
|
46
|
+
const MAX_DEPTH = 4;
|
|
47
|
+
|
|
48
|
+
/** How many values to look at per level, so a large collection cannot cost the request. */
|
|
49
|
+
const MAX_BREADTH = 50;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* How many fields a value publishes when serialised, or `null` when it is not the
|
|
53
|
+
* kind of thing this check is about.
|
|
54
|
+
*
|
|
55
|
+
* Structural, not `instanceof BaseModel`: `@zerotal/inertia` must not depend on
|
|
56
|
+
* `@zerotal/orm`, and an app can serve Inertia pages with no ORM installed at all.
|
|
57
|
+
* Three conditions together are specific enough — a class instance (not a plain
|
|
58
|
+
* object, which is already a projection), carrying its own `toJSON`, whose
|
|
59
|
+
* `toJSON` returns an object with fields in it.
|
|
60
|
+
*
|
|
61
|
+
* That last one is doing real work. `Date` is a class instance with a `toJSON`,
|
|
62
|
+
* and so are `URL` and the Temporal types; every one of them serialises to a
|
|
63
|
+
* string, and a string has no columns to leak. Requiring an object result is what
|
|
64
|
+
* separates "a row went into the page" from "a timestamp did".
|
|
65
|
+
*/
|
|
66
|
+
function serialisedFieldCount(value: unknown): number | null {
|
|
67
|
+
if (value === null || typeof value !== "object") return null;
|
|
68
|
+
if (Array.isArray(value)) return null;
|
|
69
|
+
const prototype = Object.getPrototypeOf(value) as object | null;
|
|
70
|
+
if (prototype === null || prototype === Object.prototype) return null;
|
|
71
|
+
if (typeof (value as { toJSON?: unknown }).toJSON !== "function") return null;
|
|
72
|
+
let json: unknown;
|
|
73
|
+
try {
|
|
74
|
+
json = (value as ModelLike).toJSON();
|
|
75
|
+
} catch {
|
|
76
|
+
// A model whose serialisation throws — an unloaded relation, most likely —
|
|
77
|
+
// has a problem this check is not the right place to report.
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
if (json === null || typeof json !== "object" || Array.isArray(json)) return null;
|
|
81
|
+
const fields = Object.keys(json).length;
|
|
82
|
+
return fields > 0 ? fields : null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Whether a model's class has said anything at all about what is safe to publish. */
|
|
86
|
+
function declaresBoundary(model: ModelLike): boolean {
|
|
87
|
+
const { hidden, visible } = model.constructor;
|
|
88
|
+
return (
|
|
89
|
+
(Array.isArray(hidden) && hidden.length > 0) || (Array.isArray(visible) && visible.length > 0)
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The warning text. Names the model, says how much of it is about to be published,
|
|
95
|
+
* and gives the one-line fix rather than a principle.
|
|
96
|
+
*/
|
|
97
|
+
export function propBoundaryWarning(name: string, columns: number, propKey: string): string {
|
|
98
|
+
return (
|
|
99
|
+
`[inertia] \`${name}\` was passed as the \`${propKey}\` prop and declares neither ` +
|
|
100
|
+
`\`hidden\` nor \`visible\`, so all ${columns} of its serialised fields are written into ` +
|
|
101
|
+
`page source — readable by anyone who views source on this page.\n` +
|
|
102
|
+
` If that is intended, say so once and this goes quiet:\n` +
|
|
103
|
+
` static hidden: Columns<${name}>[] = ["cost_cents", "internal_notes"];\n` +
|
|
104
|
+
` static visible: Columns<${name}>[] = ["id", "title"]; // or an allow-list\n` +
|
|
105
|
+
` Both are honoured by toJSON(), which is what serialises this prop.`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Warn about models in page props that have never declared a boundary.
|
|
111
|
+
*
|
|
112
|
+
* Development only, and a no-op everywhere else — it walks the resolved prop tree,
|
|
113
|
+
* which is not work a served request should be doing.
|
|
114
|
+
*
|
|
115
|
+
* @param props - The resolved props, as they will be serialised.
|
|
116
|
+
* @param report - Where to write findings. Injectable for tests.
|
|
117
|
+
*/
|
|
118
|
+
export function checkPropBoundary(
|
|
119
|
+
props: Record<string, unknown>,
|
|
120
|
+
report: (message: string) => void = (m) => console.warn(m),
|
|
121
|
+
): void {
|
|
122
|
+
if (!isDevSurfaceAllowed(deployEnv())) return;
|
|
123
|
+
|
|
124
|
+
const visit = (value: unknown, propKey: string, depth: number, seen: WeakSet<object>): void => {
|
|
125
|
+
if (depth > MAX_DEPTH || value === null || typeof value !== "object") return;
|
|
126
|
+
// Props can hold the same model twice, and a loaded relation can point back at
|
|
127
|
+
// its parent. Either would otherwise be walked forever.
|
|
128
|
+
if (seen.has(value)) return;
|
|
129
|
+
seen.add(value);
|
|
130
|
+
|
|
131
|
+
if (Array.isArray(value)) {
|
|
132
|
+
for (const item of value.slice(0, MAX_BREADTH)) visit(item, propKey, depth + 1, seen);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const fields = serialisedFieldCount(value);
|
|
137
|
+
if (fields !== null) {
|
|
138
|
+
const model = value as ModelLike;
|
|
139
|
+
if (!declaresBoundary(model) && !_warned.has(model.constructor)) {
|
|
140
|
+
_warned.add(model.constructor);
|
|
141
|
+
report(propBoundaryWarning(model.constructor.name, fields, propKey));
|
|
142
|
+
}
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// A plain object — a paginator, a `{ data: [...] }` envelope, a hand-built
|
|
147
|
+
// projection that happens to carry a model on one key.
|
|
148
|
+
for (const nested of Object.values(value).slice(0, MAX_BREADTH)) {
|
|
149
|
+
visit(nested, propKey, depth + 1, seen);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
for (const [key, value] of Object.entries(props)) {
|
|
154
|
+
visit(value, key, 0, new WeakSet<object>());
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Forget every class already reported. @internal For tests. */
|
|
159
|
+
export function _resetPropBoundaryWarnings(classes: object[]): void {
|
|
160
|
+
for (const cls of classes) _warned.delete(cls);
|
|
161
|
+
}
|
|
@@ -7,7 +7,11 @@ import {
|
|
|
7
7
|
type ScrollConfig,
|
|
8
8
|
} from "./PropTypes.ts";
|
|
9
9
|
|
|
10
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* The resolved props plus the page-object metadata the client needs to merge/defer correctly.
|
|
12
|
+
*
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
11
15
|
export interface ResolvedPage {
|
|
12
16
|
props: Record<string, unknown>;
|
|
13
17
|
deferredProps?: Record<string, string[]>;
|
package/src/types.ts
CHANGED
|
@@ -42,7 +42,11 @@ export interface PageObject {
|
|
|
42
42
|
sharedProps?: string[];
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
/**
|
|
45
|
+
/**
|
|
46
|
+
* Options passed to `InertiaProvider` to configure the adapter at boot.
|
|
47
|
+
*
|
|
48
|
+
* @internal
|
|
49
|
+
*/
|
|
46
50
|
export interface InertiaProviderOptions {
|
|
47
51
|
/** Path to the HTML template. Default: 'resources/app.html' */
|
|
48
52
|
htmlTemplate?: string;
|