@antelopejs/dms-frontend 0.9.1 → 0.9.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.
- package/README.md +9 -1
- package/dist/cancellation.js +2 -1
- package/dist/config.js +11 -7
- package/dist/derived-outputs.js +6 -113
- package/dist/dev-modules.js +3 -2
- package/dist/discovery.js +19 -12
- package/dist/fs-sync.js +1 -2
- package/dist/internal/guards.d.ts +5 -0
- package/dist/internal/guards.js +22 -0
- package/dist/internal/locale-catalogs.d.ts +6 -0
- package/dist/internal/locale-catalogs.js +115 -0
- package/dist/layer-aliases.js +2 -1
- package/dist/layers.js +2 -1
- package/dist/locale-files.js +9 -4
- package/dist/manifest-secrets.js +11 -4
- package/dist/manifest.js +11 -5
- package/dist/materialize.js +6 -5
- package/dist/module-set.js +2 -4
- package/dist/ports.js +8 -1
- package/dist/private-values.js +26 -17
- package/dist/project-modules.js +17 -11
- package/dist/renderer-range.js +2 -1
- package/dist/server-process.js +17 -16
- package/dist/update-check.js +23 -16
- package/dist/verify-source-runner.js +2 -2
- package/dist/workspace-setup.js +1 -2
- package/dist/workspace.js +11 -10
- package/package.json +7 -4
- package/skills/dms-frontend/SKILL.md +1 -1
- package/skills/dms-frontend/cases.json +77 -0
- package/templates/vue/app-runtime.ts +88 -7
- package/templates/vue/client-chunking.mjs +0 -5
- package/templates/vue/client-server-guard.mjs +10 -13
- package/templates/vue/entry-weight.mjs +12 -14
- package/templates/vue/frontend-module.ts +159 -3563
- package/templates/vue/frontend-runtime/async-data-entry.ts +320 -0
- package/templates/vue/frontend-runtime/async-data.ts +404 -0
- package/templates/vue/frontend-runtime/components.ts +386 -0
- package/templates/vue/frontend-runtime/cookies.ts +125 -0
- package/templates/vue/frontend-runtime/data-access.ts +132 -0
- package/templates/vue/frontend-runtime/dms-error.ts +11 -0
- package/templates/vue/frontend-runtime/errors.ts +145 -0
- package/templates/vue/frontend-runtime/fetch.ts +306 -0
- package/templates/vue/frontend-runtime/module-registration.ts +247 -0
- package/templates/vue/frontend-runtime/paged-data.ts +191 -0
- package/templates/vue/frontend-runtime/payload.ts +172 -0
- package/templates/vue/frontend-runtime/route.ts +328 -0
- package/templates/vue/frontend-runtime/sdk.ts +300 -0
- package/templates/vue/frontend-runtime/state.ts +167 -0
- package/templates/vue/frontend-runtime/types.ts +361 -0
- package/templates/vue/frontend-runtime/ui.ts +247 -0
- package/templates/vue/globals.d.ts +5 -0
- package/templates/vue/head-order.mjs +11 -13
- package/templates/vue/icon-bundle.ts +31 -0
- package/templates/vue/message-subset.ts +65 -0
- package/templates/vue/network-error.ts +4 -0
- package/templates/vue/prepare-types.mjs +0 -8
- package/templates/vue/server/auth/backend.mjs +4 -2
- package/templates/vue/server/auth/establish-endpoints.mjs +14 -9
- package/templates/vue/server/auth/session.mjs +4 -3
- package/templates/vue/server/auth-hooks.mjs +6 -6
- package/templates/vue/server/backend-proxies.mjs +6 -6
- package/templates/vue/server/backend-response.mjs +15 -14
- package/templates/vue/server/built-files.mjs +10 -8
- package/templates/vue/server/cli-report.mjs +7 -5
- package/templates/vue/server/client-error.mjs +11 -8
- package/templates/vue/server/client-manifest.mjs +9 -8
- package/templates/vue/server/content.mjs +2 -1
- package/templates/vue/server/dev-private-files.mjs +20 -15
- package/templates/vue/server/dev-styles.mjs +22 -25
- package/templates/vue/server/email-bundle-watch.mjs +0 -12
- package/templates/vue/server/email.mjs +20 -15
- package/templates/vue/server/favicon.mjs +12 -11
- package/templates/vue/server/homepage.mjs +8 -10
- package/templates/vue/server/icons.mjs +18 -9
- package/templates/vue/server/in-place-render.mjs +6 -8
- package/templates/vue/server/listen-host.mjs +2 -2
- package/templates/vue/server/module-event.mjs +5 -5
- package/templates/vue/server/module-routes.mjs +11 -7
- package/templates/vue/server/module-server.mjs +8 -7
- package/templates/vue/server/module-testing.mjs +7 -7
- package/templates/vue/server/page-props.mjs +14 -14
- package/templates/vue/server/path-patterns.mjs +4 -4
- package/templates/vue/server/proxy-compression.mjs +21 -13
- package/templates/vue/server/proxy-headers.mjs +5 -3
- package/templates/vue/server/proxy-redirects.mjs +7 -6
- package/templates/vue/server/proxy-transfers.mjs +15 -11
- package/templates/vue/server/public-endpoints.mjs +27 -19
- package/templates/vue/server/public-options.mjs +9 -7
- package/templates/vue/server/request-failure.mjs +4 -6
- package/templates/vue/server/request-routing.mjs +8 -11
- package/templates/vue/server/request-target.mjs +6 -6
- package/templates/vue/server/route-rules.mjs +7 -5
- package/templates/vue/server/static-files.mjs +19 -10
- package/templates/vue/server/web-body.mjs +3 -4
- package/templates/vue/server.mjs +32 -19
- package/templates/vue/sfc-global-types.mjs +27 -24
- package/templates/vue/ssr-renderer.ts +10 -1
- package/templates/vue/ui-inertia-page.mjs +7 -7
- package/templates/vue/ui-kbd.mjs +10 -10
- package/templates/vue/ui-lazy-icons.mjs +44 -0
- package/templates/vue/ui-request-state.mjs +9 -9
- package/templates/vue/vite.config.ts +2 -0
|
@@ -1,3564 +1,160 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
} from "
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
export
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
export
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
export interface DmsRuntimeConfig extends RuntimeConfig {
|
|
120
|
-
public: PublicRuntimeConfig;
|
|
121
|
-
}
|
|
122
|
-
export interface DmsModuleOptions {
|
|
123
|
-
public: Record<string, unknown>;
|
|
124
|
-
}
|
|
125
|
-
export type DmsLocaleLoader = (
|
|
126
|
-
locale: string,
|
|
127
|
-
) => Promise<Record<string, unknown>>;
|
|
128
|
-
export type DmsNavigationOptions = Pick<
|
|
129
|
-
InertiaLinkProps,
|
|
130
|
-
| "async"
|
|
131
|
-
| "component"
|
|
132
|
-
| "data"
|
|
133
|
-
| "except"
|
|
134
|
-
| "headers"
|
|
135
|
-
| "method"
|
|
136
|
-
| "onBefore"
|
|
137
|
-
| "onCancel"
|
|
138
|
-
| "onCancelToken"
|
|
139
|
-
| "onError"
|
|
140
|
-
| "onFinish"
|
|
141
|
-
| "onProgress"
|
|
142
|
-
| "onStart"
|
|
143
|
-
| "onSuccess"
|
|
144
|
-
| "only"
|
|
145
|
-
| "preserveScroll"
|
|
146
|
-
| "preserveState"
|
|
147
|
-
| "preserveUrl"
|
|
148
|
-
| "queryStringArrayFormat"
|
|
149
|
-
| "replace"
|
|
150
|
-
| "viewTransition"
|
|
151
|
-
>;
|
|
152
|
-
export interface RouteLocationObject {
|
|
153
|
-
path?: string;
|
|
154
|
-
query?: Record<string, unknown>;
|
|
155
|
-
hash?: string;
|
|
156
|
-
}
|
|
157
|
-
export type LocationQueryValue = string | null;
|
|
158
|
-
export type LocationQuery = Record<
|
|
159
|
-
string,
|
|
160
|
-
LocationQueryValue | LocationQueryValue[]
|
|
161
|
-
>;
|
|
162
|
-
export type LocationQueryRaw = Record<
|
|
163
|
-
string,
|
|
164
|
-
| LocationQueryValue
|
|
165
|
-
| number
|
|
166
|
-
| undefined
|
|
167
|
-
| (LocationQueryValue | number | undefined)[]
|
|
168
|
-
>;
|
|
169
|
-
export type RouteLocationRaw = string | RouteLocationObject;
|
|
170
|
-
export interface DmsRoute {
|
|
171
|
-
name?: string;
|
|
172
|
-
fullPath: string;
|
|
173
|
-
path: string;
|
|
174
|
-
query: Record<string, string>;
|
|
175
|
-
params: Record<string, string>;
|
|
176
|
-
meta: Record<string, unknown>;
|
|
177
|
-
matched: unknown[];
|
|
178
|
-
}
|
|
179
|
-
export interface DmsRouter {
|
|
180
|
-
currentRoute: Ref<DmsRoute>;
|
|
181
|
-
push(to: RouteLocationRaw, options?: DmsNavigationOptions): Promise<void>;
|
|
182
|
-
replace(to: RouteLocationRaw, options?: DmsNavigationOptions): Promise<void>;
|
|
183
|
-
back(): void;
|
|
184
|
-
}
|
|
185
|
-
export type DmsAsyncDataStatus = "idle" | "pending" | "success" | "error";
|
|
186
|
-
/**
|
|
187
|
-
* What a new run does with the one still in flight: `cancel` aborts it and
|
|
188
|
-
* starts over, `defer` waits for it instead.
|
|
189
|
-
*/
|
|
190
|
-
export type DmsAsyncDataDedupe = "cancel" | "defer";
|
|
191
|
-
export interface DmsAsyncDataRefreshOptions {
|
|
192
|
-
dedupe?: DmsAsyncDataDedupe;
|
|
193
|
-
}
|
|
194
|
-
export interface DmsAsyncData<T> {
|
|
195
|
-
data: Ref<T | null>;
|
|
196
|
-
/**
|
|
197
|
-
* The key the data is loaded under, a reactive key's current one; none for
|
|
198
|
-
* a call without a key.
|
|
199
|
-
*/
|
|
200
|
-
key: Readonly<Ref<string | undefined>>;
|
|
201
|
-
/**
|
|
202
|
-
* `true` while `data` is still the previous key's, shown in place of the
|
|
203
|
-
* default until the current key's first answer (a reactive key only).
|
|
204
|
-
*/
|
|
205
|
-
placeholder: Readonly<Ref<boolean>>;
|
|
206
|
-
/**
|
|
207
|
-
* The last run's failure. It stays while the next run is in flight, and
|
|
208
|
-
* goes back to `null` once a run answers.
|
|
209
|
-
*/
|
|
210
|
-
error: Ref<unknown>;
|
|
211
|
-
pending: Ref<boolean>;
|
|
212
|
-
status: Ref<DmsAsyncDataStatus>;
|
|
213
|
-
execute(options?: DmsAsyncDataRefreshOptions): Promise<void>;
|
|
214
|
-
refresh(options?: DmsAsyncDataRefreshOptions): Promise<void>;
|
|
215
|
-
/** Aborts the run in flight and puts the data back to its default. */
|
|
216
|
-
clear(): void;
|
|
217
|
-
}
|
|
218
|
-
/** Usable at once, or awaited until its data is there. */
|
|
219
|
-
export type DmsAsyncDataResult<T> = DmsAsyncData<T> & Promise<DmsAsyncData<T>>;
|
|
220
|
-
export interface DmsAsyncDataContext {
|
|
221
|
-
/**
|
|
222
|
-
* Aborted once the result is no longer wanted: a newer run replaced it, it
|
|
223
|
-
* was cleared, or no component shows its key any more.
|
|
224
|
-
*/
|
|
225
|
-
signal: AbortSignal;
|
|
226
|
-
}
|
|
227
|
-
export type DmsAsyncDataHandler<T> = (
|
|
228
|
-
context: DmsAsyncDataContext,
|
|
229
|
-
) => Promise<T>;
|
|
230
|
-
/** A key, or `null` (or `undefined`) while there is nothing to load. */
|
|
231
|
-
export type DmsAsyncDataKey = string | null | undefined;
|
|
232
|
-
/** What `useDmsData` gives of a key's data. */
|
|
233
|
-
export interface DmsData<T> {
|
|
234
|
-
/**
|
|
235
|
-
* The data loaded under the key, shared with every caller showing it:
|
|
236
|
-
* `undefined` while the key has none of its own (no caller loaded it yet,
|
|
237
|
-
* or it shows the previous key's data meanwhile). Assigning it replaces the
|
|
238
|
-
* key's data; a key nobody loaded keeps none.
|
|
239
|
-
*/
|
|
240
|
-
data: Ref<T | undefined>;
|
|
241
|
-
}
|
|
242
|
-
/** What `transform` knows of the run whose result it transforms. */
|
|
243
|
-
export interface DmsAsyncDataTransformContext {
|
|
244
|
-
/** The key the data is loaded under; none for a call without a key. */
|
|
245
|
-
key: string | undefined;
|
|
246
|
-
}
|
|
247
|
-
/** What `useDmsFetch`'s `transform` knows of the request it answers. */
|
|
248
|
-
export interface DmsFetchTransformContext extends DmsAsyncDataTransformContext {
|
|
249
|
-
/**
|
|
250
|
-
* The URL requested, as given (before the base URL and the query), even
|
|
251
|
-
* when a reactive URL changed since.
|
|
252
|
-
*/
|
|
253
|
-
url: string;
|
|
254
|
-
}
|
|
255
|
-
/**
|
|
256
|
-
* The request options a ref or a getter may stand for: read again for each
|
|
257
|
-
* request, and part of the key the response is shared under.
|
|
258
|
-
*/
|
|
259
|
-
type DmsFetchRequestSources = "method" | "query" | "params" | "body";
|
|
260
|
-
export interface UseAsyncDataOptions<T> {
|
|
261
|
-
default?: () => T;
|
|
262
|
-
/** `false` waits for `execute()` (a dialog, a drawer) before the first run. */
|
|
263
|
-
immediate?: boolean;
|
|
264
|
-
/**
|
|
265
|
-
* A client navigation does not wait for the data. A server render still
|
|
266
|
-
* does: its HTML shows the data, and the browser takes it over.
|
|
267
|
-
*/
|
|
268
|
-
lazy?: boolean;
|
|
269
|
-
/** `false` leaves the data to the browser: the server renders the default. */
|
|
270
|
-
server?: boolean;
|
|
271
|
-
watch?: false | unknown[];
|
|
272
|
-
transform?: (
|
|
273
|
-
value: unknown,
|
|
274
|
-
context: DmsAsyncDataTransformContext,
|
|
275
|
-
) => T | Promise<T>;
|
|
276
|
-
pick?: string[];
|
|
277
|
-
/**
|
|
278
|
-
* `true` tracks changes inside the value too. By default only the
|
|
279
|
-
* replacement of the whole value triggers, as in Nuxt 4.
|
|
280
|
-
*/
|
|
281
|
-
deep?: boolean;
|
|
282
|
-
dedupe?: DmsAsyncDataDedupe;
|
|
283
|
-
/**
|
|
284
|
-
* `false` leaves the data out of `refreshDmsPageData()`, which reads again
|
|
285
|
-
* the data of the components on screen once the page changed (a save, a
|
|
286
|
-
* realtime event).
|
|
287
|
-
*/
|
|
288
|
-
refreshOnPageRefresh?: boolean;
|
|
289
|
-
/**
|
|
290
|
-
* Reads again every this many milliseconds, in the browser, while the
|
|
291
|
-
* caller shows the data and the page is visible. A ref or a getter changes
|
|
292
|
-
* the interval as it changes; `0`, `null` or `undefined` stops the polling
|
|
293
|
-
* until it has a value again. A call outside a component (a plugin, a
|
|
294
|
-
* callback) polls for as long as the application runs.
|
|
295
|
-
*/
|
|
296
|
-
refreshInterval?: MaybeRefOrGetter<number | null | undefined>;
|
|
297
|
-
}
|
|
298
|
-
/**
|
|
299
|
-
* `useDmsFetch`'s options: ofetch's request options, of which the method, the
|
|
300
|
-
* query, the params and the body may be refs or getters, and the async data
|
|
301
|
-
* options.
|
|
302
|
-
*/
|
|
303
|
-
export interface UseFetchOptions<T>
|
|
304
|
-
extends
|
|
305
|
-
Omit<FetchOptions, DmsFetchRequestSources>,
|
|
306
|
-
Omit<UseAsyncDataOptions<T>, "transform"> {
|
|
307
|
-
/**
|
|
308
|
-
* The key the response is shared and transferred under, in place of the
|
|
309
|
-
* one the request gives: its method, base URL, URL, query and body. A ref
|
|
310
|
-
* or a getter moves the caller to the data of each key it takes; `null` or
|
|
311
|
-
* `undefined` requests nothing until it has a value.
|
|
312
|
-
*/
|
|
313
|
-
key?: MaybeRefOrGetter<DmsAsyncDataKey>;
|
|
314
|
-
method?: MaybeRefOrGetter<FetchOptions["method"]>;
|
|
315
|
-
query?: MaybeRefOrGetter<FetchOptions["query"]>;
|
|
316
|
-
params?: MaybeRefOrGetter<FetchOptions["params"]>;
|
|
317
|
-
body?: MaybeRefOrGetter<FetchOptions["body"]>;
|
|
318
|
-
transform?: (
|
|
319
|
-
value: unknown,
|
|
320
|
-
context: DmsFetchTransformContext,
|
|
321
|
-
) => T | Promise<T>;
|
|
322
|
-
/** Replaces the fetch function; it receives the run's signal. */
|
|
323
|
-
$fetch?: typeof ofetch;
|
|
324
|
-
}
|
|
325
|
-
export interface DmsErrorData {
|
|
326
|
-
statusCode?: number;
|
|
327
|
-
statusMessage?: string;
|
|
328
|
-
message?: string;
|
|
329
|
-
data?: unknown;
|
|
330
|
-
fatal?: boolean;
|
|
331
|
-
}
|
|
332
|
-
/** What a server render hands the browser along with its HTML. */
|
|
333
|
-
export interface DmsPayload {
|
|
334
|
-
data: Record<string, unknown>;
|
|
335
|
-
errors: Record<string, DmsErrorData>;
|
|
336
|
-
state: Record<string, unknown>;
|
|
337
|
-
}
|
|
338
|
-
export interface DmsStateOptions {
|
|
339
|
-
/**
|
|
340
|
-
* Hands the server's value to the browser, which then starts from it
|
|
341
|
-
* instead of calling `init` again. Never for tokens or secrets: the value
|
|
342
|
-
* is written into the page.
|
|
343
|
-
*/
|
|
344
|
-
transfer?: boolean;
|
|
345
|
-
}
|
|
346
|
-
export interface DmsUserSession<TUser = DmsUser, TSession = DmsSession> {
|
|
347
|
-
user: Ref<TUser | null>;
|
|
348
|
-
session: Ref<TSession | null>;
|
|
349
|
-
loggedIn: Ref<boolean>;
|
|
350
|
-
fetch(): Promise<void>;
|
|
351
|
-
clear(): Promise<void>;
|
|
352
|
-
}
|
|
353
|
-
export interface DmsCookieOptions<T> {
|
|
354
|
-
default?: () => T;
|
|
355
|
-
maxAge?: number;
|
|
356
|
-
path?: string;
|
|
357
|
-
sameSite?: "strict" | "lax" | "none";
|
|
358
|
-
secure?: boolean;
|
|
359
|
-
}
|
|
360
|
-
type DmsLinkPrefetchMode = "mount" | "hover" | "click";
|
|
361
|
-
type DmsLinkPrefetch = boolean | DmsLinkPrefetchMode | DmsLinkPrefetchMode[];
|
|
362
|
-
export interface DmsAppContext {
|
|
363
|
-
provide<T>(key: string, value: T): void;
|
|
364
|
-
vueApp: App;
|
|
365
|
-
runWithContext<T>(callback: () => T): T;
|
|
366
|
-
hook(name: string, callback: () => void | Promise<void>): void;
|
|
367
|
-
$i18n: DmsI18n;
|
|
368
|
-
}
|
|
369
|
-
export interface DmsLocale {
|
|
370
|
-
code: string;
|
|
371
|
-
name?: string;
|
|
372
|
-
}
|
|
373
|
-
export type DmsI18n = Composer & {
|
|
374
|
-
locales: ComputedRef<DmsLocale[]>;
|
|
375
|
-
setLocale(locale: string): Promise<void>;
|
|
376
|
-
};
|
|
377
|
-
export type DmsPluginSetup = (context: DmsAppContext) => void | Promise<void>;
|
|
378
|
-
export interface DmsMiddlewareRegistrationOptions {
|
|
379
|
-
global?: boolean;
|
|
380
|
-
/**
|
|
381
|
-
* Runs at the same time as the parallel middlewares next to it, for one
|
|
382
|
-
* that reads nothing another middleware sets (a check against the backend,
|
|
383
|
-
* say). Their results still apply in registration order: the first that
|
|
384
|
-
* redirects or aborts wins, and the next middlewares wait for all of them.
|
|
385
|
-
*/
|
|
386
|
-
parallel?: boolean;
|
|
387
|
-
}
|
|
388
|
-
export interface DmsPluginRegistrationOptions {
|
|
389
|
-
clientOnly?: boolean;
|
|
390
|
-
/**
|
|
391
|
-
* When the plugin runs, as Nuxt's `enforce`: every module's `"pre"`
|
|
392
|
-
* plugins first, then the `"default"` ones, then the `"post"` ones, each
|
|
393
|
-
* group in the modules' priority order. A plugin that the others depend on
|
|
394
|
-
* whatever their module's priority, such as the one setting the async data
|
|
395
|
-
* scope (`setDmsAsyncDataScope`), is `"pre"`.
|
|
396
|
-
*/
|
|
397
|
-
enforce?: "pre" | "default" | "post";
|
|
398
|
-
}
|
|
399
|
-
export type DmsComponentPreloader = () => Promise<unknown>;
|
|
400
|
-
export interface DmsComponentRegistrationOptions {
|
|
401
|
-
/**
|
|
402
|
-
* Registers the component under `name` as given, without the module's
|
|
403
|
-
* `componentPrefix`: the way to replace a component another module
|
|
404
|
-
* registers, from a module of higher priority. Defaults to `true`.
|
|
405
|
-
*/
|
|
406
|
-
prefix?: boolean;
|
|
407
|
-
}
|
|
408
|
-
export interface DmsFrontendSdk {
|
|
409
|
-
options: DmsModuleOptions;
|
|
410
|
-
registerComponent(
|
|
411
|
-
name: string,
|
|
412
|
-
component: Component,
|
|
413
|
-
options?: DmsComponentRegistrationOptions,
|
|
414
|
-
): void;
|
|
415
|
-
registerPage(
|
|
416
|
-
name: string,
|
|
417
|
-
component: Component,
|
|
418
|
-
preload?: DmsComponentPreloader,
|
|
419
|
-
): void;
|
|
420
|
-
registerDynamicPage(
|
|
421
|
-
name: string,
|
|
422
|
-
component: Component,
|
|
423
|
-
preload?: DmsComponentPreloader,
|
|
424
|
-
): void;
|
|
425
|
-
registerLayout(
|
|
426
|
-
name: string,
|
|
427
|
-
component: Component,
|
|
428
|
-
preload?: DmsComponentPreloader,
|
|
429
|
-
): void;
|
|
430
|
-
registerErrorPage(
|
|
431
|
-
component: Component,
|
|
432
|
-
preload?: DmsComponentPreloader,
|
|
433
|
-
): void;
|
|
434
|
-
registerPlugin(
|
|
435
|
-
setup: DmsPluginSetup,
|
|
436
|
-
options?: DmsPluginRegistrationOptions,
|
|
437
|
-
): void;
|
|
438
|
-
registerMiddleware(
|
|
439
|
-
name: string,
|
|
440
|
-
handler: DmsMiddleware,
|
|
441
|
-
options?: DmsMiddlewareRegistrationOptions,
|
|
442
|
-
): void;
|
|
443
|
-
/**
|
|
444
|
-
* Sends a page visit the backend refuses with a typed 403 (its body is
|
|
445
|
-
* `code`) to `path` instead of the error page. The server answers with the
|
|
446
|
-
* redirect before anything renders, so a reload lands on `path` directly.
|
|
447
|
-
*/
|
|
448
|
-
registerAccessRedirect(code: string, path: string): void;
|
|
449
|
-
provide<T>(key: string | InjectionKey<T>, value: T): void;
|
|
450
|
-
use(plugin: Plugin): void;
|
|
451
|
-
}
|
|
452
|
-
export interface DmsFrontendModule {
|
|
453
|
-
/**
|
|
454
|
-
* Put in front of the name of every component the module registers, so
|
|
455
|
-
* the components of two modules never collide: with `"Acme"`,
|
|
456
|
-
* `registerComponent("Banner", ...)` registers `AcmeBanner`, the name the
|
|
457
|
-
* backend sends and templates use.
|
|
458
|
-
*/
|
|
459
|
-
componentPrefix?: string;
|
|
460
|
-
setup(sdk: DmsFrontendSdk): void | Promise<void>;
|
|
461
|
-
}
|
|
462
|
-
export interface DmsFrontendModuleRegistration {
|
|
463
|
-
/** The module's id in the generated module registry. */
|
|
464
|
-
id?: string;
|
|
465
|
-
/** The module's manifest name, which collision warnings name it by. */
|
|
466
|
-
name?: string;
|
|
467
|
-
module: DmsFrontendModule;
|
|
468
|
-
options: DmsModuleOptions;
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
/**
|
|
472
|
-
* The registrations with the public options the server runs with in place of
|
|
473
|
-
* the build's: `ajs dms start` computes them from the project's configuration
|
|
474
|
-
* and the deployment's variables. Unset, the build's options stand.
|
|
475
|
-
*/
|
|
476
|
-
export function withPublicOptions(
|
|
477
|
-
registrations: DmsFrontendModuleRegistration[],
|
|
478
|
-
byId: DmsPublicOptionsById | undefined,
|
|
479
|
-
): DmsFrontendModuleRegistration[] {
|
|
480
|
-
if (!byId) return registrations;
|
|
481
|
-
return registrations.map((registration) => {
|
|
482
|
-
const options = registration.id ? byId[registration.id] : undefined;
|
|
483
|
-
return options
|
|
484
|
-
? {
|
|
485
|
-
...registration,
|
|
486
|
-
options: { ...registration.options, public: options },
|
|
487
|
-
}
|
|
488
|
-
: registration;
|
|
489
|
-
});
|
|
490
|
-
}
|
|
491
|
-
/** A layer's `app/app.config.ts`, as `app-configs.generated.ts` lists them. */
|
|
492
|
-
export interface DmsLayerAppConfig {
|
|
493
|
-
/** The id of the frontend module the layer belongs to. */
|
|
494
|
-
module: string;
|
|
495
|
-
config: Record<string, unknown>;
|
|
496
|
-
}
|
|
497
|
-
// Async middleware without a redirect naturally infers Promise<void>.
|
|
498
|
-
// biome-ignore lint/suspicious/noConfusingVoidType: preserve that valid handler signature.
|
|
499
|
-
export type DmsMiddlewareResult = void | false | RouteLocationRaw;
|
|
500
|
-
export type DmsMiddleware = (
|
|
501
|
-
to: DmsRoute,
|
|
502
|
-
from: DmsRoute,
|
|
503
|
-
) => DmsMiddlewareResult | Promise<DmsMiddlewareResult>;
|
|
504
|
-
export type DmsRuntimeHook = (...args: unknown[]) => void | Promise<void>;
|
|
505
|
-
export interface DmsRuntimeHooks {
|
|
506
|
-
hook(name: string, callback: DmsRuntimeHook): () => void;
|
|
507
|
-
callHook(name: string, ...args: unknown[]): Promise<void>;
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
interface DmsComponentRegistration {
|
|
511
|
-
name: string;
|
|
512
|
-
component: Component;
|
|
513
|
-
/** The registering module, for a collision warning. */
|
|
514
|
-
module?: string;
|
|
515
|
-
/** Registered without the module's prefix, to replace another module's. */
|
|
516
|
-
unprefixed?: boolean;
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
interface DmsFrontendEntry {
|
|
520
|
-
component: Component;
|
|
521
|
-
preload?: DmsComponentPreloader;
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
interface DmsAsyncComponent {
|
|
525
|
-
__asyncLoader?: DmsComponentPreloader;
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
interface DmsPluginRegistration {
|
|
529
|
-
setup: DmsPluginSetup;
|
|
530
|
-
clientOnly: boolean;
|
|
531
|
-
enforce: NonNullable<DmsPluginRegistrationOptions["enforce"]>;
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
const PLUGIN_ORDER = { pre: 0, default: 1, post: 2 } as const;
|
|
535
|
-
|
|
536
|
-
/**
|
|
537
|
-
* What the frontend modules registered through their SDK. Modules are set up
|
|
538
|
-
* by descending priority, so the first one to claim a name keeps it.
|
|
539
|
-
*/
|
|
540
|
-
interface DmsFrontendRegistry {
|
|
541
|
-
components: Map<string, DmsComponentRegistration>;
|
|
542
|
-
pages: Map<string, DmsFrontendEntry>;
|
|
543
|
-
dynamicPages: Map<string, DmsFrontendEntry>;
|
|
544
|
-
layouts: Map<string, DmsFrontendEntry>;
|
|
545
|
-
errorPage?: DmsFrontendEntry;
|
|
546
|
-
plugins: Plugin[];
|
|
547
|
-
pluginSetups: DmsPluginRegistration[];
|
|
548
|
-
middleware: DmsMiddleware[];
|
|
549
|
-
namedMiddleware: Map<string, DmsMiddleware>;
|
|
550
|
-
/** The middlewares registered with `parallel: true`. */
|
|
551
|
-
parallelMiddleware: WeakSet<DmsMiddleware>;
|
|
552
|
-
accessRedirects: Map<string, string>;
|
|
553
|
-
injections: Map<string | symbol, unknown>;
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
export interface DmsFrontendRuntime {
|
|
557
|
-
sharedState: Map<string, Ref<unknown>>;
|
|
558
|
-
/** Reactive: `useDmsData` follows a key as it appears and is dropped. */
|
|
559
|
-
asyncData: Map<string, DmsAsyncData<unknown>>;
|
|
560
|
-
/** The server's data, which the browser takes over once per key. */
|
|
561
|
-
hydratedAsyncData: Map<string, unknown>;
|
|
562
|
-
/** The server's failures, which the browser takes over once per key. */
|
|
563
|
-
hydratedAsyncErrors: Map<string, DmsErrorData>;
|
|
564
|
-
/** The state keys a server render hands to the browser. */
|
|
565
|
-
transferredState: Set<string>;
|
|
566
|
-
/**
|
|
567
|
-
* Whether the browser is still hydrating the page the server rendered,
|
|
568
|
-
* whose first render must match the server's HTML.
|
|
569
|
-
*/
|
|
570
|
-
hydrating: boolean;
|
|
571
|
-
/**
|
|
572
|
-
* The runs a server render started outside any component, which no
|
|
573
|
-
* `onServerPrefetch` waits for.
|
|
574
|
-
*/
|
|
575
|
-
serverPrefetches: Set<Promise<void>>;
|
|
576
|
-
/** The runs in flight, which a server render aborts once it is over. */
|
|
577
|
-
asyncDataRuns: Set<AbortController>;
|
|
578
|
-
/** The data some component or caller shows: `refreshDmsPageData`'s. */
|
|
579
|
-
boundAsyncData: Set<{ pageRefresh(): Promise<void> | undefined }>;
|
|
580
|
-
/** Orders the data no caller shows, the least recently shown first. */
|
|
581
|
-
asyncDataClock: number;
|
|
582
|
-
/**
|
|
583
|
-
* The account and workspace the keys belong to (`setDmsAsyncDataScope`):
|
|
584
|
-
* the same key of another scope is another entry.
|
|
585
|
-
*/
|
|
586
|
-
asyncDataScope?: string;
|
|
587
|
-
/**
|
|
588
|
-
* Runs an async data handler; a server render marks the requests it makes
|
|
589
|
-
* as transferred (see `inAsyncDataHandler`).
|
|
590
|
-
*/
|
|
591
|
-
runAsyncDataHandler?: <R>(run: () => R) => R;
|
|
592
|
-
/** Whether the code running is an async data handler's. */
|
|
593
|
-
inAsyncDataHandler?: () => boolean;
|
|
594
|
-
/** Whether to warn about what a development render does wrong. */
|
|
595
|
-
devWarnings?: boolean;
|
|
596
|
-
runtimeHooks: Map<string, DmsRuntimeHook[]>;
|
|
597
|
-
serverFetch?: typeof ofetch;
|
|
598
|
-
currentError: Ref<Error | null>;
|
|
599
|
-
route: DmsRoute;
|
|
600
|
-
currentRoutePattern?: string;
|
|
601
|
-
appContext?: DmsAppContext;
|
|
602
|
-
isServer: boolean;
|
|
603
|
-
serverRedirect?: string;
|
|
604
|
-
pendingNavigation?: Promise<void>;
|
|
605
|
-
hasNavigationListener: boolean;
|
|
606
|
-
/** Whether the page `<Suspense>` waits on the page being navigated to. */
|
|
607
|
-
pageLoading: Ref<boolean>;
|
|
608
|
-
/** The site layout last copied into the state, as the page props hold it. */
|
|
609
|
-
appliedShared?: DmsSharedPagePayload;
|
|
610
|
-
/** Whether Inertia is putting back a page from the browser history. */
|
|
611
|
-
restoringHistory?: boolean;
|
|
612
|
-
/**
|
|
613
|
-
* Identifies the page on screen (its path, or the error page shown at that
|
|
614
|
-
* path), whose meta `route.meta` holds.
|
|
615
|
-
*/
|
|
616
|
-
pageKey?: string;
|
|
617
|
-
/** The `Cookie` header of the request a server runtime renders. */
|
|
618
|
-
requestCookies?: string;
|
|
619
|
-
/**
|
|
620
|
-
* Owns the effects plugins create while they set up. A server render stops
|
|
621
|
-
* it once done: nothing else would, since a server app is never unmounted.
|
|
622
|
-
*/
|
|
623
|
-
scope: EffectScope;
|
|
624
|
-
/** The Inertia page a server runtime renders: see `useDmsInertiaPage`. */
|
|
625
|
-
inertiaPage?: DmsInertiaPage;
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
/** Inertia's page, as `usePage()` returns it. */
|
|
629
|
-
export type DmsInertiaPage = ReturnType<typeof usePage>;
|
|
630
|
-
|
|
631
|
-
/** Creates the empty registry one setup of the frontend modules fills. */
|
|
632
|
-
function createDmsFrontendRegistry(): DmsFrontendRegistry {
|
|
633
|
-
return {
|
|
634
|
-
components: new Map(),
|
|
635
|
-
pages: new Map(),
|
|
636
|
-
dynamicPages: new Map(),
|
|
637
|
-
layouts: new Map(),
|
|
638
|
-
plugins: [],
|
|
639
|
-
pluginSetups: [],
|
|
640
|
-
middleware: [],
|
|
641
|
-
namedMiddleware: new Map(),
|
|
642
|
-
parallelMiddleware: new WeakSet(),
|
|
643
|
-
accessRedirects: new Map(),
|
|
644
|
-
injections: new Map(),
|
|
645
|
-
};
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
// Replaced, never refilled, by each setup of the modules: see
|
|
649
|
-
// `setupFrontendModules`.
|
|
650
|
-
let registry = createDmsFrontendRegistry();
|
|
651
|
-
const runtimeConfig = ref<DmsRuntimeConfig>({
|
|
652
|
-
public: {},
|
|
653
|
-
} as DmsRuntimeConfig);
|
|
654
|
-
const appConfig = ref<Record<string, unknown>>({});
|
|
655
|
-
// What the last setup of the modules changed in the layers' app configs, which
|
|
656
|
-
// a development update of those configs keeps: see `updateDmsAppConfig`.
|
|
657
|
-
let appConfigSetupChanges: Record<string, unknown> = {};
|
|
658
|
-
const DMS_RUNTIME_KEY: InjectionKey<DmsFrontendRuntime> = Symbol("dms-runtime");
|
|
659
|
-
type DmsRuntimeContext = DmsAppContext["runWithContext"];
|
|
660
|
-
const runtimeContexts = new WeakMap<DmsFrontendRuntime, DmsRuntimeContext>();
|
|
661
|
-
let serverRuntimeResolver: (() => DmsFrontendRuntime | undefined) | undefined;
|
|
662
|
-
let browserRuntime: DmsFrontendRuntime | undefined;
|
|
663
|
-
|
|
664
|
-
/** Creates mutable runtime state owned by one Vue application. */
|
|
665
|
-
export function createDmsFrontendRuntime(
|
|
666
|
-
serverFetch?: typeof ofetch,
|
|
667
|
-
hydratedAsyncData?: Record<string, unknown>,
|
|
668
|
-
isServer = false,
|
|
669
|
-
requestCookies?: string,
|
|
670
|
-
): DmsFrontendRuntime {
|
|
671
|
-
const runtime: DmsFrontendRuntime = {
|
|
672
|
-
requestCookies,
|
|
673
|
-
sharedState: new Map(),
|
|
674
|
-
asyncData: shallowReactive(new Map()),
|
|
675
|
-
hydratedAsyncData: new Map(Object.entries(hydratedAsyncData ?? {})),
|
|
676
|
-
hydratedAsyncErrors: new Map(),
|
|
677
|
-
transferredState: new Set(),
|
|
678
|
-
// A browser handed the server's data, even none, hydrates its HTML.
|
|
679
|
-
hydrating: !isServer && hydratedAsyncData !== undefined,
|
|
680
|
-
serverPrefetches: new Set(),
|
|
681
|
-
asyncDataRuns: new Set(),
|
|
682
|
-
boundAsyncData: new Set(),
|
|
683
|
-
asyncDataClock: 0,
|
|
684
|
-
runtimeHooks: new Map(),
|
|
685
|
-
serverFetch,
|
|
686
|
-
isServer,
|
|
687
|
-
currentError: ref<Error | null>(null),
|
|
688
|
-
route: reactive<DmsRoute>({
|
|
689
|
-
fullPath: "/",
|
|
690
|
-
path: "/",
|
|
691
|
-
query: {},
|
|
692
|
-
params: {},
|
|
693
|
-
meta: {},
|
|
694
|
-
matched: [],
|
|
695
|
-
}),
|
|
696
|
-
hasNavigationListener: false,
|
|
697
|
-
pageLoading: ref(false),
|
|
698
|
-
scope: effectScope(true),
|
|
699
|
-
};
|
|
700
|
-
// A server render stops its scope once it has answered: whatever it still
|
|
701
|
-
// fetches then would reach nobody.
|
|
702
|
-
runtime.scope.run(() =>
|
|
703
|
-
onScopeDispose(() => {
|
|
704
|
-
for (const run of runtime.asyncDataRuns) run.abort();
|
|
705
|
-
}),
|
|
706
|
-
);
|
|
707
|
-
return runtime;
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
/** Makes a runtime available before application plugins are installed. */
|
|
711
|
-
export function provideDmsFrontendRuntime(
|
|
712
|
-
app: App,
|
|
713
|
-
runtime: DmsFrontendRuntime,
|
|
714
|
-
): void {
|
|
715
|
-
app.provide(DMS_RUNTIME_KEY, runtime);
|
|
716
|
-
runtimeContexts.set(runtime, (callback) => app.runWithContext(callback));
|
|
717
|
-
if (typeof window !== "undefined") browserRuntime = runtime;
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
/** Registers the request-local runtime resolver used by the SSR entry point. */
|
|
721
|
-
export function setDmsServerRuntimeResolver(
|
|
722
|
-
resolver: () => DmsFrontendRuntime | undefined,
|
|
723
|
-
): void {
|
|
724
|
-
serverRuntimeResolver = resolver;
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
function useDmsRuntime(): DmsFrontendRuntime {
|
|
728
|
-
const instance = getCurrentInstance();
|
|
729
|
-
const provided = instance?.appContext.provides[DMS_RUNTIME_KEY as symbol] as
|
|
730
|
-
| DmsFrontendRuntime
|
|
731
|
-
| undefined;
|
|
732
|
-
if (provided) return provided;
|
|
733
|
-
const contextual = hasInjectionContext()
|
|
734
|
-
? inject(DMS_RUNTIME_KEY, undefined)
|
|
735
|
-
: undefined;
|
|
736
|
-
if (contextual) return contextual;
|
|
737
|
-
if (typeof window !== "undefined" && browserRuntime) return browserRuntime;
|
|
738
|
-
const serverRuntime = serverRuntimeResolver?.();
|
|
739
|
-
if (serverRuntime) return serverRuntime;
|
|
740
|
-
throw new Error("DMS runtime is unavailable outside a Vue application");
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
/** Returns lifecycle hooks isolated to the current application runtime. */
|
|
744
|
-
export function useDmsRuntimeHooks(): DmsRuntimeHooks {
|
|
745
|
-
const runtime = useDmsRuntime();
|
|
746
|
-
return {
|
|
747
|
-
hook(name, callback) {
|
|
748
|
-
runtime.runtimeHooks.set(name, [
|
|
749
|
-
...(runtime.runtimeHooks.get(name) ?? []),
|
|
750
|
-
callback,
|
|
751
|
-
]);
|
|
752
|
-
return () =>
|
|
753
|
-
runtime.runtimeHooks.set(
|
|
754
|
-
name,
|
|
755
|
-
(runtime.runtimeHooks.get(name) ?? []).filter(
|
|
756
|
-
(entry) => entry !== callback,
|
|
757
|
-
),
|
|
758
|
-
);
|
|
759
|
-
},
|
|
760
|
-
async callHook(name, ...args) {
|
|
761
|
-
for (const callback of runtime.runtimeHooks.get(name) ?? [])
|
|
762
|
-
await callback(...args);
|
|
763
|
-
},
|
|
764
|
-
};
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
function extractRouteParams(
|
|
768
|
-
pattern: string | undefined,
|
|
769
|
-
path: string,
|
|
770
|
-
): Record<string, string> {
|
|
771
|
-
if (!pattern) return {};
|
|
772
|
-
const patternSegments = pattern.split("/").filter(Boolean);
|
|
773
|
-
const pathSegments = path.split("/").filter(Boolean);
|
|
774
|
-
if (patternSegments.length !== pathSegments.length) return {};
|
|
775
|
-
const params: Record<string, string> = {};
|
|
776
|
-
const matches = patternSegments.every((segment, index) => {
|
|
777
|
-
if (!segment.startsWith(":")) return segment === pathSegments[index];
|
|
778
|
-
params[segment.slice(1)] = decodeURIComponent(pathSegments[index] ?? "");
|
|
779
|
-
return true;
|
|
780
|
-
});
|
|
781
|
-
return matches ? params : {};
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
function parseRoute(runtime: DmsFrontendRuntime, url: string): DmsRoute {
|
|
785
|
-
const parsed = new URL(
|
|
786
|
-
url,
|
|
787
|
-
typeof window === "undefined"
|
|
788
|
-
? "http://frontend.local"
|
|
789
|
-
: window.location.origin,
|
|
790
|
-
);
|
|
791
|
-
return {
|
|
792
|
-
name: parsed.pathname,
|
|
793
|
-
fullPath: `${parsed.pathname}${parsed.search}${parsed.hash}`,
|
|
794
|
-
path: parsed.pathname,
|
|
795
|
-
query: Object.fromEntries(parsed.searchParams),
|
|
796
|
-
params: extractRouteParams(runtime.currentRoutePattern, parsed.pathname),
|
|
797
|
-
meta: {},
|
|
798
|
-
matched: [{}],
|
|
799
|
-
};
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
function hasSameEntries(
|
|
803
|
-
left: Record<string, string>,
|
|
804
|
-
right: Record<string, string>,
|
|
805
|
-
): boolean {
|
|
806
|
-
const keys = Object.keys(left);
|
|
807
|
-
return (
|
|
808
|
-
keys.length === Object.keys(right).length &&
|
|
809
|
-
keys.every((key) => right[key] === left[key])
|
|
810
|
-
);
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
// Every parse builds a new query, params and matched: assigning them as they
|
|
814
|
-
// come would re-run each watcher of the route on every call of useDmsRoute,
|
|
815
|
-
// though the URL did not change. They are only replaced when their content did.
|
|
816
|
-
function updateRoute(runtime: DmsFrontendRuntime, url: string): void {
|
|
817
|
-
// The meta is the page's, not the URL's: a page declares it, then reads its
|
|
818
|
-
// route, and the middleware the meta selected must still find it.
|
|
819
|
-
const {
|
|
820
|
-
meta: _pageMeta,
|
|
821
|
-
query,
|
|
822
|
-
params,
|
|
823
|
-
matched,
|
|
824
|
-
...location
|
|
825
|
-
} = parseRoute(runtime, url);
|
|
826
|
-
const route = runtime.route;
|
|
827
|
-
if (!hasSameEntries(query, route.query)) route.query = query;
|
|
828
|
-
if (!hasSameEntries(params, route.params)) route.params = params;
|
|
829
|
-
if (matched.length !== route.matched.length) route.matched = matched;
|
|
830
|
-
Object.assign(route, location);
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
const URL_BASE = "http://frontend.local";
|
|
834
|
-
|
|
835
|
-
// The URL the browser shows: it may be ahead of the route, which follows
|
|
836
|
-
// Inertia's page, when a component wrote it in place (history.replaceState).
|
|
837
|
-
function currentUrl(runtime: DmsFrontendRuntime): string {
|
|
838
|
-
if (runtime.isServer || typeof window === "undefined")
|
|
839
|
-
return new URL(runtime.route.fullPath, URL_BASE).href;
|
|
840
|
-
return window.location.href;
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
// One spelling per URL: `a b`, `a+b` and `a%20b` are the same query, as are
|
|
844
|
-
// `:` and `%3A`. The origin is kept, so a URL of another site never matches
|
|
845
|
-
// the current one, whatever its path.
|
|
846
|
-
function normalizeUrl(url: string, base: string): string {
|
|
847
|
-
const parsed = new URL(url, base);
|
|
848
|
-
const search = parsed.searchParams.toString();
|
|
849
|
-
return `${parsed.origin}${parsed.pathname}${search ? `?${search}` : ""}${parsed.hash}`;
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
function locationToUrl(
|
|
853
|
-
runtime: DmsFrontendRuntime,
|
|
854
|
-
to: RouteLocationRaw,
|
|
855
|
-
): string {
|
|
856
|
-
if (typeof to === "string") return to;
|
|
857
|
-
const origin =
|
|
858
|
-
typeof window === "undefined"
|
|
859
|
-
? "http://frontend.local"
|
|
860
|
-
: window.location.origin;
|
|
861
|
-
const parsed = new URL(to.path ?? runtime.route.path, origin);
|
|
862
|
-
if (to.query !== undefined) {
|
|
863
|
-
parsed.search = Object.entries(to.query)
|
|
864
|
-
.flatMap(([key, value]) =>
|
|
865
|
-
(Array.isArray(value) ? value : [value])
|
|
866
|
-
.filter((item) => item !== undefined)
|
|
867
|
-
.map((item) =>
|
|
868
|
-
item === null
|
|
869
|
-
? encodeURIComponent(key)
|
|
870
|
-
: `${encodeURIComponent(key)}=${encodeURIComponent(String(item))}`,
|
|
871
|
-
),
|
|
872
|
-
)
|
|
873
|
-
.join("&");
|
|
874
|
-
}
|
|
875
|
-
parsed.hash = to.hash ?? "";
|
|
876
|
-
return `${parsed.pathname}${parsed.search}${parsed.hash}`;
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
async function runMiddleware(
|
|
880
|
-
runtime: DmsFrontendRuntime,
|
|
881
|
-
to: DmsRoute,
|
|
882
|
-
): Promise<DmsMiddlewareResult> {
|
|
883
|
-
const declared = Array.isArray(to.meta.middleware) ? to.meta.middleware : [];
|
|
884
|
-
const metadataNames = Object.keys(to.meta).filter((name) => to.meta[name]);
|
|
885
|
-
const declaredHandlers = [...declared, ...metadataNames]
|
|
886
|
-
.map((name) => registry.namedMiddleware.get(String(name)))
|
|
887
|
-
.filter((handler): handler is DmsMiddleware => !!handler);
|
|
888
|
-
const run = (handler: DmsMiddleware) => {
|
|
889
|
-
const runWithContext = runtimeContexts.get(runtime);
|
|
890
|
-
return runWithContext
|
|
891
|
-
? runWithContext(() => handler(to, runtime.route))
|
|
892
|
-
: handler(to, runtime.route);
|
|
893
|
-
};
|
|
894
|
-
for (const group of middlewareGroups([
|
|
895
|
-
...registry.middleware,
|
|
896
|
-
...declaredHandlers,
|
|
897
|
-
])) {
|
|
898
|
-
// A group of parallel middlewares starts together; its results apply in
|
|
899
|
-
// order, as a sequence's would.
|
|
900
|
-
const results = await Promise.all(group.map(run));
|
|
901
|
-
const result = results.find((value) => value !== undefined);
|
|
902
|
-
if (result !== undefined) return result;
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
/** The middlewares in order, each run of parallel ones as one group. */
|
|
907
|
-
function middlewareGroups(handlers: DmsMiddleware[]): DmsMiddleware[][] {
|
|
908
|
-
const groups: DmsMiddleware[][] = [];
|
|
909
|
-
for (const handler of handlers) {
|
|
910
|
-
const last = groups.at(-1);
|
|
911
|
-
const parallel = registry.parallelMiddleware.has(handler);
|
|
912
|
-
if (parallel && last && registry.parallelMiddleware.has(last[0]))
|
|
913
|
-
last.push(handler);
|
|
914
|
-
else groups.push([handler]);
|
|
915
|
-
}
|
|
916
|
-
return groups;
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
// What only a request to the server can honour: a client-side visit sends
|
|
920
|
-
// none and has none of these events.
|
|
921
|
-
const SERVER_VISIT_OPTIONS = [
|
|
922
|
-
"async",
|
|
923
|
-
"component",
|
|
924
|
-
"data",
|
|
925
|
-
"except",
|
|
926
|
-
"headers",
|
|
927
|
-
"method",
|
|
928
|
-
"onBefore",
|
|
929
|
-
"onCancel",
|
|
930
|
-
"onCancelToken",
|
|
931
|
-
"onProgress",
|
|
932
|
-
"onStart",
|
|
933
|
-
"only",
|
|
934
|
-
"preserveUrl",
|
|
935
|
-
"queryStringArrayFormat",
|
|
936
|
-
] as const satisfies readonly (keyof DmsNavigationOptions)[];
|
|
937
|
-
|
|
938
|
-
/**
|
|
939
|
-
* Whether a visit to another query or hash of the page on screen can stay in
|
|
940
|
-
* the browser. The page props depend on the path alone: the server would
|
|
941
|
-
* answer with the same page, the site layout included. An error page is left
|
|
942
|
-
* through the server, which renders the page in its place, and so is the
|
|
943
|
-
* same path on another site.
|
|
944
|
-
*/
|
|
945
|
-
function staysOnPage(
|
|
946
|
-
runtime: DmsFrontendRuntime,
|
|
947
|
-
url: string,
|
|
948
|
-
options: DmsNavigationOptions,
|
|
949
|
-
): boolean {
|
|
950
|
-
return (
|
|
951
|
-
new URL(url, currentUrl(runtime)).origin ===
|
|
952
|
-
new URL(currentUrl(runtime)).origin &&
|
|
953
|
-
!runtime.pageKey?.startsWith("error:") &&
|
|
954
|
-
runtime.currentError.value === null &&
|
|
955
|
-
SERVER_VISIT_OPTIONS.every((name) => options[name] === undefined)
|
|
956
|
-
);
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
/**
|
|
960
|
-
* An Inertia client-side visit: the URL and the history entry change, the
|
|
961
|
-
* page keeps its props, and no request is made.
|
|
962
|
-
*/
|
|
963
|
-
function visitInBrowser(
|
|
964
|
-
url: string,
|
|
965
|
-
options: DmsNavigationOptions,
|
|
966
|
-
): Promise<void> {
|
|
967
|
-
const clientVisit = options.replace
|
|
968
|
-
? inertiaRouter.replace
|
|
969
|
-
: inertiaRouter.push;
|
|
970
|
-
return new Promise<void>((resolve) => {
|
|
971
|
-
clientVisit.call(inertiaRouter, {
|
|
972
|
-
url,
|
|
973
|
-
preserveScroll: options.preserveScroll ?? true,
|
|
974
|
-
preserveState: options.preserveState ?? true,
|
|
975
|
-
viewTransition: options.viewTransition,
|
|
976
|
-
onSuccess: options.onSuccess,
|
|
977
|
-
onError: options.onError,
|
|
978
|
-
onFinish: (visit) => {
|
|
979
|
-
(options.onFinish as ((visit: unknown) => void) | undefined)?.(visit);
|
|
980
|
-
resolve();
|
|
981
|
-
},
|
|
982
|
-
} as Parameters<typeof inertiaRouter.push>[0]);
|
|
983
|
-
});
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
async function visit(
|
|
987
|
-
runtime: DmsFrontendRuntime,
|
|
988
|
-
to: RouteLocationRaw,
|
|
989
|
-
options: DmsNavigationOptions = {},
|
|
990
|
-
): Promise<void> {
|
|
991
|
-
const url = locationToUrl(runtime, to);
|
|
992
|
-
const target = parseRoute(runtime, url);
|
|
993
|
-
const result = await runMiddleware(runtime, target);
|
|
994
|
-
if (result === false) return;
|
|
995
|
-
if (result !== undefined) return visit(runtime, result, options);
|
|
996
|
-
if (runtime.isServer) {
|
|
997
|
-
runtime.serverRedirect = url;
|
|
998
|
-
return;
|
|
999
|
-
}
|
|
1000
|
-
// Like Vue Router, a navigation that only changes the query or hash keeps
|
|
1001
|
-
// the page mounted: without preserveState, Inertia remounts the page
|
|
1002
|
-
// component on every visit and its local state (open tabs, selection,
|
|
1003
|
-
// inputs) is lost.
|
|
1004
|
-
const samePath = target.path === runtime.route.path;
|
|
1005
|
-
if (samePath && staysOnPage(runtime, url, options)) {
|
|
1006
|
-
await visitInBrowser(url, options);
|
|
1007
|
-
return;
|
|
1008
|
-
}
|
|
1009
|
-
await new Promise<void>((resolve) =>
|
|
1010
|
-
inertiaRouter.visit(url, {
|
|
1011
|
-
...options,
|
|
1012
|
-
preserveState: options.preserveState ?? samePath,
|
|
1013
|
-
preserveScroll: options.preserveScroll ?? samePath,
|
|
1014
|
-
onFinish: (completedVisit) => {
|
|
1015
|
-
options.onFinish?.(completedVisit);
|
|
1016
|
-
resolve();
|
|
1017
|
-
},
|
|
1018
|
-
}),
|
|
1019
|
-
);
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
export function useDmsRoute(pattern?: string): DmsRoute {
|
|
1023
|
-
const runtime = useDmsRuntime();
|
|
1024
|
-
runtime.currentRoutePattern = pattern;
|
|
1025
|
-
// A server render answers one request, whose URL the runtime was given with
|
|
1026
|
-
// its page, and never changes it. Inertia's page is one ref for the whole
|
|
1027
|
-
// process, written by each render as it starts: after an await it may hold
|
|
1028
|
-
// another request's URL, and while the plugins set up, the previous one's.
|
|
1029
|
-
if (runtime.isServer) {
|
|
1030
|
-
updateRoute(runtime, runtime.route.fullPath);
|
|
1031
|
-
return runtime.route;
|
|
1032
|
-
}
|
|
1033
|
-
const page = usePage();
|
|
1034
|
-
// The browser's plugins set up before Inertia has a page: until then the
|
|
1035
|
-
// runtime's route is the initial page's.
|
|
1036
|
-
if (page.url !== undefined) updateRoute(runtime, page.url);
|
|
1037
|
-
watch(
|
|
1038
|
-
() => page.url,
|
|
1039
|
-
(url) => updateRoute(runtime, url),
|
|
1040
|
-
);
|
|
1041
|
-
return runtime.route;
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
/**
|
|
1045
|
-
* Inertia's page, as `usePage()` returns it, for the request being rendered.
|
|
1046
|
-
* Inertia keeps a single page for the whole process, written by each server
|
|
1047
|
-
* render as it starts: read after an await, it may be another request's. Nuxt
|
|
1048
|
-
* UI's Link and route stub read this one instead (see `vite.config.ts`).
|
|
1049
|
-
*/
|
|
1050
|
-
export function useDmsInertiaPage(): DmsInertiaPage {
|
|
1051
|
-
const runtime = useDmsRuntime();
|
|
1052
|
-
if (runtime.isServer && runtime.inertiaPage) return runtime.inertiaPage;
|
|
1053
|
-
return usePage();
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
export function useDmsRouter(): DmsRouter {
|
|
1057
|
-
const runtime = useDmsRuntime();
|
|
1058
|
-
const navigate = (to: RouteLocationRaw, options?: DmsNavigationOptions) => {
|
|
1059
|
-
const current = currentUrl(runtime);
|
|
1060
|
-
return normalizeUrl(locationToUrl(runtime, to), current) ===
|
|
1061
|
-
normalizeUrl(current, current)
|
|
1062
|
-
? Promise.resolve()
|
|
1063
|
-
: visit(runtime, to, options);
|
|
1064
|
-
};
|
|
1065
|
-
return {
|
|
1066
|
-
currentRoute: computed(() => runtime.route),
|
|
1067
|
-
push: navigate,
|
|
1068
|
-
replace: (to, options) => navigate(to, { ...options, replace: true }),
|
|
1069
|
-
back: () => window.history.back(),
|
|
1070
|
-
};
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
|
-
export function navigateDms(
|
|
1074
|
-
to: RouteLocationRaw,
|
|
1075
|
-
options?: DmsNavigationOptions,
|
|
1076
|
-
): Promise<void> {
|
|
1077
|
-
return visit(useDmsRuntime(), to, options);
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
/**
|
|
1081
|
-
* The options of a request a runtime makes. A server render does not retry a
|
|
1082
|
-
* failed read, as ofetch would once: the page waits for it, and a backend
|
|
1083
|
-
* failing once mostly fails twice. A browser keeps ofetch's retries, and a
|
|
1084
|
-
* caller that names its own keeps them everywhere.
|
|
1085
|
-
*/
|
|
1086
|
-
function runtimeFetchOptions(
|
|
1087
|
-
runtime: DmsFrontendRuntime,
|
|
1088
|
-
options: FetchOptions,
|
|
1089
|
-
): FetchOptions {
|
|
1090
|
-
return runtime.isServer && options.retry === undefined
|
|
1091
|
-
? { ...options, retry: 0 }
|
|
1092
|
-
: options;
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
const warnedServerReads = new Set<string>();
|
|
1096
|
-
|
|
1097
|
-
/**
|
|
1098
|
-
* Warns, in a development server render, about a backend read no async data
|
|
1099
|
-
* handler makes: an immediate watcher's, a setup's own `await $fetch`, a
|
|
1100
|
-
* middleware's. The server waits for it, but the page carries none of it, so
|
|
1101
|
-
* the browser reads it again and may hydrate with other data. Code that hands
|
|
1102
|
-
* the answer over itself (`useDmsState` with `transfer: true`) looks the same
|
|
1103
|
-
* from here, so the message names that exception rather than guess.
|
|
1104
|
-
*/
|
|
1105
|
-
function warnUntransferredRead(
|
|
1106
|
-
runtime: DmsFrontendRuntime,
|
|
1107
|
-
request: string,
|
|
1108
|
-
): void {
|
|
1109
|
-
if (
|
|
1110
|
-
!runtime.isServer ||
|
|
1111
|
-
!runtime.devWarnings ||
|
|
1112
|
-
!runtime.inAsyncDataHandler ||
|
|
1113
|
-
runtime.inAsyncDataHandler() ||
|
|
1114
|
-
warnedServerReads.has(request)
|
|
1115
|
-
)
|
|
1116
|
-
return;
|
|
1117
|
-
warnedServerReads.add(request);
|
|
1118
|
-
console.warn(
|
|
1119
|
-
`DMS: ${request} was read during a server render outside useDmsFetch and useDmsAsyncData ` +
|
|
1120
|
-
"(an immediate watcher, a setup's own await, a middleware): unless the code hands the " +
|
|
1121
|
-
"answer over (useDmsState with transfer: true), the page does not carry it and the " +
|
|
1122
|
-
"browser reads it again. Load it with useDmsFetch or useDmsAsyncData, in a middleware " +
|
|
1123
|
-
"or a plugin too, or leave it to the browser (server: false, onMounted).",
|
|
1124
|
-
);
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
function createDmsFetch(defaults: FetchOptions = {}): typeof ofetch {
|
|
1128
|
-
const execute = (request: string, options?: FetchOptions) => {
|
|
1129
|
-
const runtime = useDmsRuntime();
|
|
1130
|
-
warnUntransferredRead(runtime, request);
|
|
1131
|
-
return (runtime.serverFetch ?? ofetch)(
|
|
1132
|
-
request,
|
|
1133
|
-
runtimeFetchOptions(runtime, { ...defaults, ...options }),
|
|
1134
|
-
);
|
|
1135
|
-
};
|
|
1136
|
-
return Object.assign(execute, {
|
|
1137
|
-
create: (options: FetchOptions) =>
|
|
1138
|
-
createDmsFetch({ ...defaults, ...options }),
|
|
1139
|
-
native: ofetch.native,
|
|
1140
|
-
raw: ofetch.raw,
|
|
1141
|
-
}) as typeof ofetch;
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
export const $fetch = createDmsFetch();
|
|
1145
|
-
|
|
1146
|
-
function pickValue<T>(value: unknown, keys?: string[]): T {
|
|
1147
|
-
if (!keys || typeof value !== "object" || value === null) return value as T;
|
|
1148
|
-
return Object.fromEntries(
|
|
1149
|
-
keys
|
|
1150
|
-
.filter((key) => key in value)
|
|
1151
|
-
.map((key) => [key, Reflect.get(value, key)]),
|
|
1152
|
-
) as T;
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
/** JSON with sorted keys, or nothing for a value JSON cannot stand for. */
|
|
1156
|
-
function stableJson(value: unknown): string | undefined {
|
|
1157
|
-
if (value === undefined || value === null) return "null";
|
|
1158
|
-
if (value instanceof Date) return JSON.stringify(value);
|
|
1159
|
-
if (typeof value !== "object")
|
|
1160
|
-
return ["string", "number", "boolean"].includes(typeof value)
|
|
1161
|
-
? JSON.stringify(value)
|
|
1162
|
-
: undefined;
|
|
1163
|
-
if (Array.isArray(value)) {
|
|
1164
|
-
const items = value.map(stableJson);
|
|
1165
|
-
return items.includes(undefined) ? undefined : `[${items.join(",")}]`;
|
|
1166
|
-
}
|
|
1167
|
-
const prototype = Object.getPrototypeOf(value) as unknown;
|
|
1168
|
-
if (prototype !== Object.prototype && prototype !== null) return undefined;
|
|
1169
|
-
const record = value as Record<string, unknown>;
|
|
1170
|
-
const members = Object.keys(record)
|
|
1171
|
-
.filter((name) => record[name] !== undefined)
|
|
1172
|
-
.sort()
|
|
1173
|
-
.map((name) => {
|
|
1174
|
-
const member = stableJson(record[name]);
|
|
1175
|
-
return member === undefined
|
|
1176
|
-
? undefined
|
|
1177
|
-
: `${JSON.stringify(name)}:${member}`;
|
|
1178
|
-
});
|
|
1179
|
-
return members.includes(undefined) ? undefined : `{${members.join(",")}}`;
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
const FETCH_KEY_PREFIX = "$dmsfetch:";
|
|
1183
|
-
const DEFAULT_FETCH_METHOD = "GET";
|
|
1184
|
-
|
|
1185
|
-
// cyrb53's mixing constants, with its two 32-bit lanes both kept: a 64-bit
|
|
1186
|
-
// digest, as short as Nuxt's keys and as unlikely to collide within a page.
|
|
1187
|
-
const HASH_SEEDS = [0xdeadbeef, 0x41c6ce57];
|
|
1188
|
-
const HASH_MULTIPLIERS = [2654435761, 1597334677];
|
|
1189
|
-
const HASH_FINALIZERS = [2246822507, 3266489909];
|
|
1190
|
-
const HASH_SHIFTS = [16, 13];
|
|
1191
|
-
const HASH_RADIX = 36;
|
|
1192
|
-
const HASH_LANE_LENGTH = 7;
|
|
1193
|
-
|
|
1194
|
-
/**
|
|
1195
|
-
* A short digest of a text, the same in a server and in a browser: pure
|
|
1196
|
-
* JavaScript, so no `node:crypto` reaches a browser bundle.
|
|
1197
|
-
*/
|
|
1198
|
-
function hashText(text: string): string {
|
|
1199
|
-
let [first, second] = HASH_SEEDS;
|
|
1200
|
-
for (let index = 0; index < text.length; index++) {
|
|
1201
|
-
const code = text.charCodeAt(index);
|
|
1202
|
-
first = Math.imul(first ^ code, HASH_MULTIPLIERS[0]);
|
|
1203
|
-
second = Math.imul(second ^ code, HASH_MULTIPLIERS[1]);
|
|
1204
|
-
}
|
|
1205
|
-
first = Math.imul(first ^ (first >>> HASH_SHIFTS[0]), HASH_FINALIZERS[0]);
|
|
1206
|
-
first ^= Math.imul(second ^ (second >>> HASH_SHIFTS[1]), HASH_FINALIZERS[1]);
|
|
1207
|
-
second = Math.imul(second ^ (second >>> HASH_SHIFTS[0]), HASH_FINALIZERS[0]);
|
|
1208
|
-
second ^= Math.imul(first ^ (first >>> HASH_SHIFTS[1]), HASH_FINALIZERS[1]);
|
|
1209
|
-
return [second, first]
|
|
1210
|
-
.map((lane) =>
|
|
1211
|
-
(lane >>> 0).toString(HASH_RADIX).padStart(HASH_LANE_LENGTH, "0"),
|
|
1212
|
-
)
|
|
1213
|
-
.join("");
|
|
1214
|
-
}
|
|
1215
|
-
|
|
1216
|
-
/**
|
|
1217
|
-
* The key a request's response is shared and transferred under: a digest of
|
|
1218
|
-
* its method, base URL, URL, query and params, and body, the headers aside. A
|
|
1219
|
-
* server render writes the key into its HTML, which must not show the URL, a
|
|
1220
|
-
* token in its query or a body. A body only a request can read, such as a
|
|
1221
|
-
* FormData, gives none.
|
|
1222
|
-
*/
|
|
1223
|
-
function requestKey(url: string, options: FetchOptions): string | undefined {
|
|
1224
|
-
// ofetch sends the params and the query together, the query winning.
|
|
1225
|
-
const query =
|
|
1226
|
-
options.query || options.params
|
|
1227
|
-
? { ...options.params, ...options.query }
|
|
1228
|
-
: null;
|
|
1229
|
-
const request = stableJson([
|
|
1230
|
-
(options.method ?? DEFAULT_FETCH_METHOD).toUpperCase(),
|
|
1231
|
-
options.baseURL ?? "",
|
|
1232
|
-
url,
|
|
1233
|
-
query,
|
|
1234
|
-
options.body ?? null,
|
|
1235
|
-
]);
|
|
1236
|
-
return request === undefined
|
|
1237
|
-
? undefined
|
|
1238
|
-
: `${FETCH_KEY_PREFIX}${hashText(request)}`;
|
|
1239
|
-
}
|
|
1240
|
-
|
|
1241
|
-
/** The parts of a request its key is computed from. */
|
|
1242
|
-
export type DmsRequestKeyOptions = Pick<
|
|
1243
|
-
FetchOptions,
|
|
1244
|
-
"method" | "baseURL" | "query" | "params" | "body"
|
|
1245
|
-
>;
|
|
1246
|
-
|
|
1247
|
-
/**
|
|
1248
|
-
* The key `useDmsFetch` loads a request's response under, without the scope
|
|
1249
|
-
* (`setDmsAsyncDataScope`): what `refreshDmsData(keys)` takes, and what a
|
|
1250
|
-
* test compares with the keys it finds. `undefined` for a body only a
|
|
1251
|
-
* request can read, such as a FormData.
|
|
1252
|
-
*
|
|
1253
|
-
* @param url The URL as given to `useDmsFetch`, before the base URL and the query
|
|
1254
|
-
* @param options The method, base URL, query, params and body of the request
|
|
1255
|
-
*/
|
|
1256
|
-
export function dmsRequestKey(
|
|
1257
|
-
url: string,
|
|
1258
|
-
options: DmsRequestKeyOptions = {},
|
|
1259
|
-
): string | undefined {
|
|
1260
|
-
return requestKey(url, options);
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
|
-
/** A URL to request, or nothing to request while it is null or undefined. */
|
|
1264
|
-
type DmsFetchRequest = string | null | undefined;
|
|
1265
|
-
|
|
1266
|
-
function isReactiveSource(value: unknown): boolean {
|
|
1267
|
-
return isRef(value) || typeof value === "function";
|
|
1268
|
-
}
|
|
1269
|
-
|
|
1270
|
-
/** The options given, without the ones left undefined. */
|
|
1271
|
-
function definedOptions(options: FetchOptions): FetchOptions {
|
|
1272
|
-
return Object.fromEntries(
|
|
1273
|
-
Object.entries(options).filter(([, value]) => value !== undefined),
|
|
1274
|
-
) as FetchOptions;
|
|
1275
|
-
}
|
|
1276
|
-
|
|
1277
|
-
/**
|
|
1278
|
-
* Fetches a URL as `useDmsAsyncData` loads its data, under the key of the
|
|
1279
|
-
* request, so that the callers of one request share it and a server render
|
|
1280
|
-
* transfers it. A ref or a getter for the URL, the method, the query, the
|
|
1281
|
-
* params or the body moves the caller to the key of each request; a URL that
|
|
1282
|
-
* is null or undefined requests nothing.
|
|
1283
|
-
*/
|
|
1284
|
-
export function useDmsFetch<T>(
|
|
1285
|
-
request: MaybeRefOrGetter<DmsFetchRequest>,
|
|
1286
|
-
options: UseFetchOptions<T> = {},
|
|
1287
|
-
): DmsAsyncDataResult<T> {
|
|
1288
|
-
const runtime = useDmsRuntime();
|
|
1289
|
-
const {
|
|
1290
|
-
key,
|
|
1291
|
-
immediate,
|
|
1292
|
-
lazy,
|
|
1293
|
-
server,
|
|
1294
|
-
watch: sources,
|
|
1295
|
-
default: fallback,
|
|
1296
|
-
transform,
|
|
1297
|
-
pick,
|
|
1298
|
-
deep,
|
|
1299
|
-
dedupe,
|
|
1300
|
-
refreshOnPageRefresh,
|
|
1301
|
-
refreshInterval,
|
|
1302
|
-
$fetch: fetcher,
|
|
1303
|
-
method,
|
|
1304
|
-
query,
|
|
1305
|
-
params,
|
|
1306
|
-
body,
|
|
1307
|
-
...fixedOptions
|
|
1308
|
-
} = options;
|
|
1309
|
-
const url = () => toValue(request);
|
|
1310
|
-
const requestOptions = (): FetchOptions =>
|
|
1311
|
-
definedOptions({
|
|
1312
|
-
...fixedOptions,
|
|
1313
|
-
method: toValue(method),
|
|
1314
|
-
query: toValue(query),
|
|
1315
|
-
params: toValue(params),
|
|
1316
|
-
body: toValue(body),
|
|
1317
|
-
});
|
|
1318
|
-
// A `$fetch` of the caller's own applies its own retries. The transform
|
|
1319
|
-
// runs here, where the request a response answers is known: a reactive URL
|
|
1320
|
-
// may have changed since it was sent.
|
|
1321
|
-
const handler: DmsAsyncDataHandler<T> = async ({ signal }) => {
|
|
1322
|
-
const target = url() as string;
|
|
1323
|
-
const sent = requestOptions();
|
|
1324
|
-
const runKey =
|
|
1325
|
-
key === undefined
|
|
1326
|
-
? requestKey(target, sent)
|
|
1327
|
-
: (toValue(key) ?? undefined);
|
|
1328
|
-
const value: unknown = await (fetcher
|
|
1329
|
-
? fetcher(target, { ...sent, signal })
|
|
1330
|
-
: (runtime.serverFetch ?? ofetch)(
|
|
1331
|
-
target,
|
|
1332
|
-
runtimeFetchOptions(runtime, { ...sent, signal }),
|
|
1333
|
-
));
|
|
1334
|
-
if (!transform) return value as T;
|
|
1335
|
-
return transform(value, { key: runKey, url: target });
|
|
1336
|
-
};
|
|
1337
|
-
const asyncOptions: UseAsyncDataOptions<T> = {
|
|
1338
|
-
immediate,
|
|
1339
|
-
lazy,
|
|
1340
|
-
server,
|
|
1341
|
-
watch: sources,
|
|
1342
|
-
default: fallback,
|
|
1343
|
-
pick,
|
|
1344
|
-
deep,
|
|
1345
|
-
dedupe,
|
|
1346
|
-
refreshOnPageRefresh,
|
|
1347
|
-
refreshInterval,
|
|
1348
|
-
};
|
|
1349
|
-
const reactiveSources = [request, method, query, params, body].filter(
|
|
1350
|
-
isReactiveSource,
|
|
1351
|
-
);
|
|
1352
|
-
const isReactive = reactiveSources.length > 0;
|
|
1353
|
-
// The request gives its key, which follows the request as it changes.
|
|
1354
|
-
const derived = (): string | null | undefined => {
|
|
1355
|
-
const target = url();
|
|
1356
|
-
return target == null ? null : requestKey(target, requestOptions());
|
|
1357
|
-
};
|
|
1358
|
-
if (key === undefined && derived() !== undefined)
|
|
1359
|
-
return useDmsAsyncData(
|
|
1360
|
-
isReactive ? derived : derived(),
|
|
1361
|
-
handler,
|
|
1362
|
-
asyncOptions,
|
|
1363
|
-
);
|
|
1364
|
-
// The key the caller names, or none for a body without one, stays put while
|
|
1365
|
-
// the request changes: the request is watched instead.
|
|
1366
|
-
const watched =
|
|
1367
|
-
isReactive && sources !== false
|
|
1368
|
-
? [...reactiveSources, ...(sources ?? [])]
|
|
1369
|
-
: sources;
|
|
1370
|
-
if (key === undefined)
|
|
1371
|
-
return useDmsAsyncData(handler, undefined, {
|
|
1372
|
-
...asyncOptions,
|
|
1373
|
-
watch: watched,
|
|
1374
|
-
});
|
|
1375
|
-
const named = () => (url() == null ? null : toValue(key));
|
|
1376
|
-
const followsKey = isReactiveSource(request) || isReactiveSource(key);
|
|
1377
|
-
return useDmsAsyncData(followsKey ? named : named(), handler, {
|
|
1378
|
-
...asyncOptions,
|
|
1379
|
-
watch: watched,
|
|
1380
|
-
});
|
|
1381
|
-
}
|
|
1382
|
-
|
|
1383
|
-
interface AsyncDataBinding<T> {
|
|
1384
|
-
handler: DmsAsyncDataHandler<T>;
|
|
1385
|
-
options: UseAsyncDataOptions<T>;
|
|
1386
|
-
}
|
|
1387
|
-
|
|
1388
|
-
interface AsyncDataRun {
|
|
1389
|
-
promise: Promise<void>;
|
|
1390
|
-
controller: AbortController;
|
|
1391
|
-
}
|
|
1392
|
-
|
|
1393
|
-
interface AsyncDataBindingHandle {
|
|
1394
|
-
unbind(): void;
|
|
1395
|
-
}
|
|
1396
|
-
|
|
1397
|
-
/** What a key holds before any run of the browser's own. */
|
|
1398
|
-
interface AsyncDataInitialState<T> {
|
|
1399
|
-
data: T | null;
|
|
1400
|
-
error: unknown;
|
|
1401
|
-
status: DmsAsyncDataStatus;
|
|
1402
|
-
}
|
|
1403
|
-
|
|
1404
|
-
/** What the components showing one key share: its data and its runs. */
|
|
1405
|
-
interface AsyncDataEntry<T> {
|
|
1406
|
-
result: DmsAsyncData<T>;
|
|
1407
|
-
bind(binding: AsyncDataBinding<T>): AsyncDataBindingHandle;
|
|
1408
|
-
/** A run: the one in flight, or a new one. */
|
|
1409
|
-
load(): Promise<void>;
|
|
1410
|
-
/** The first run, once the current turn is over. */
|
|
1411
|
-
schedule(): void;
|
|
1412
|
-
/** Resolves once no run is in flight, including the runs that replace it. */
|
|
1413
|
-
settled(): Promise<void>;
|
|
1414
|
-
unused(): boolean;
|
|
1415
|
-
/** When a caller last stopped showing the data (`asyncDataClock`). */
|
|
1416
|
-
lastShown(): number;
|
|
1417
|
-
/**
|
|
1418
|
-
* Whether another caller shows the data and its last run answered within
|
|
1419
|
-
* `JOIN_WITHOUT_READ_MS`: a caller joining it then reads nothing again.
|
|
1420
|
-
*/
|
|
1421
|
-
justReadForAnother(): boolean;
|
|
1422
|
-
running(): boolean;
|
|
1423
|
-
/** `refreshDmsPageData`'s run, for data a caller shows and opted into. */
|
|
1424
|
-
pageRefresh(): Promise<void> | undefined;
|
|
1425
|
-
/**
|
|
1426
|
-
* Holds the server's state back, as pending, until the next turn or
|
|
1427
|
-
* `adoptNow`, whichever comes first.
|
|
1428
|
-
*/
|
|
1429
|
-
adoptLater(state: AsyncDataInitialState<T>): void;
|
|
1430
|
-
adoptNow(): void;
|
|
1431
|
-
/** Resolves once the state held back is adopted; nothing when none is. */
|
|
1432
|
-
adoption(): Promise<void> | undefined;
|
|
1433
|
-
}
|
|
1434
|
-
|
|
1435
|
-
const DEFAULT_DEDUPE: DmsAsyncDataDedupe = "cancel";
|
|
1436
|
-
/**
|
|
1437
|
-
* How many keys no component shows any more a browser keeps, the most
|
|
1438
|
-
* recently shown, for a return to them to show their data at once.
|
|
1439
|
-
*/
|
|
1440
|
-
const MAX_UNUSED_ASYNC_DATA = 50;
|
|
1441
|
-
/**
|
|
1442
|
-
* How long after a run answered a caller that joins a key another caller
|
|
1443
|
-
* shows takes the data as it is, instead of reading it again: two components
|
|
1444
|
-
* of one page sharing a read mount a moment apart.
|
|
1445
|
-
*/
|
|
1446
|
-
const JOIN_WITHOUT_READ_MS = 2000;
|
|
1447
|
-
|
|
1448
|
-
const asyncDataEntries = new WeakMap<
|
|
1449
|
-
DmsAsyncData<unknown>,
|
|
1450
|
-
AsyncDataEntry<unknown>
|
|
1451
|
-
>();
|
|
1452
|
-
|
|
1453
|
-
function createAsyncDataEntry<T>(
|
|
1454
|
-
runtime: DmsFrontendRuntime,
|
|
1455
|
-
key: string | undefined,
|
|
1456
|
-
initial: AsyncDataInitialState<T>,
|
|
1457
|
-
deep: boolean,
|
|
1458
|
-
): AsyncDataEntry<T> {
|
|
1459
|
-
const data = (
|
|
1460
|
-
deep ? ref(initial.data) : shallowRef(initial.data)
|
|
1461
|
-
) as Ref<T | null>;
|
|
1462
|
-
const error = ref<unknown>(initial.error);
|
|
1463
|
-
const status = ref<DmsAsyncDataStatus>(initial.status);
|
|
1464
|
-
const pending = computed(() => status.value === "pending");
|
|
1465
|
-
const bindings: AsyncDataBinding<T>[] = [];
|
|
1466
|
-
let run: AsyncDataRun | undefined;
|
|
1467
|
-
let scheduled: Promise<void> | undefined;
|
|
1468
|
-
let heldBack: AsyncDataInitialState<T> | undefined;
|
|
1469
|
-
let adoption: Promise<void> | undefined;
|
|
1470
|
-
let lastShown = 0;
|
|
1471
|
-
// When the last run answered, to spare a caller joining at once a second read.
|
|
1472
|
-
let answeredAt = 0;
|
|
1473
|
-
const adoptNow = () => {
|
|
1474
|
-
if (!heldBack) return;
|
|
1475
|
-
data.value = heldBack.data;
|
|
1476
|
-
error.value = heldBack.error;
|
|
1477
|
-
status.value = heldBack.status;
|
|
1478
|
-
heldBack = undefined;
|
|
1479
|
-
};
|
|
1480
|
-
const handle = (binding: AsyncDataBinding<T>, signal: AbortSignal) => {
|
|
1481
|
-
const call = () => binding.handler({ signal });
|
|
1482
|
-
return runtime.runAsyncDataHandler
|
|
1483
|
-
? runtime.runAsyncDataHandler(call)
|
|
1484
|
-
: call();
|
|
1485
|
-
};
|
|
1486
|
-
const start = (binding: AsyncDataBinding<T>): Promise<void> => {
|
|
1487
|
-
heldBack = undefined;
|
|
1488
|
-
run?.controller.abort();
|
|
1489
|
-
const controller = new AbortController();
|
|
1490
|
-
const current: AsyncDataRun = { controller, promise: Promise.resolve() };
|
|
1491
|
-
run = current;
|
|
1492
|
-
runtime.asyncDataRuns.add(controller);
|
|
1493
|
-
// The last failure stays until this run answers, as in Nuxt.
|
|
1494
|
-
status.value = "pending";
|
|
1495
|
-
// A run another one replaced, or that nobody waits for any more, settles
|
|
1496
|
-
// nothing, even when its handler ignores the signal.
|
|
1497
|
-
current.promise = (async () => {
|
|
1498
|
-
try {
|
|
1499
|
-
const value = await handle(binding, controller.signal);
|
|
1500
|
-
const transformed = binding.options.transform
|
|
1501
|
-
? await binding.options.transform(value, { key })
|
|
1502
|
-
: value;
|
|
1503
|
-
if (run !== current) return;
|
|
1504
|
-
data.value = pickValue<T>(transformed, binding.options.pick);
|
|
1505
|
-
error.value = null;
|
|
1506
|
-
status.value = "success";
|
|
1507
|
-
answeredAt = Date.now();
|
|
1508
|
-
} catch (reason) {
|
|
1509
|
-
if (run !== current) return;
|
|
1510
|
-
// The data of the last success stays on screen next to the failure.
|
|
1511
|
-
error.value = reason;
|
|
1512
|
-
status.value = "error";
|
|
1513
|
-
} finally {
|
|
1514
|
-
runtime.asyncDataRuns.delete(controller);
|
|
1515
|
-
if (run === current) run = undefined;
|
|
1516
|
-
}
|
|
1517
|
-
})();
|
|
1518
|
-
return current.promise;
|
|
1519
|
-
};
|
|
1520
|
-
const refresh = (options: DmsAsyncDataRefreshOptions = {}) => {
|
|
1521
|
-
const binding = bindings.at(-1);
|
|
1522
|
-
if (!binding) return Promise.resolve();
|
|
1523
|
-
const dedupe = options.dedupe ?? binding.options.dedupe ?? DEFAULT_DEDUPE;
|
|
1524
|
-
return run && dedupe === "defer" ? run.promise : start(binding);
|
|
1525
|
-
};
|
|
1526
|
-
const load = (): Promise<void> => {
|
|
1527
|
-
if (run) return run.promise;
|
|
1528
|
-
const binding = bindings.at(-1);
|
|
1529
|
-
return binding ? start(binding) : Promise.resolve();
|
|
1530
|
-
};
|
|
1531
|
-
const result: DmsAsyncData<T> = {
|
|
1532
|
-
data,
|
|
1533
|
-
key: computed(() => key),
|
|
1534
|
-
placeholder: computed(() => false),
|
|
1535
|
-
error,
|
|
1536
|
-
pending,
|
|
1537
|
-
status,
|
|
1538
|
-
execute: refresh,
|
|
1539
|
-
refresh,
|
|
1540
|
-
clear() {
|
|
1541
|
-
heldBack = undefined;
|
|
1542
|
-
run?.controller.abort();
|
|
1543
|
-
run = undefined;
|
|
1544
|
-
data.value = bindings.at(-1)?.options.default?.() ?? null;
|
|
1545
|
-
error.value = null;
|
|
1546
|
-
status.value = "idle";
|
|
1547
|
-
},
|
|
1548
|
-
};
|
|
1549
|
-
const entry: AsyncDataEntry<T> = {
|
|
1550
|
-
result,
|
|
1551
|
-
bind(binding) {
|
|
1552
|
-
bindings.push(binding);
|
|
1553
|
-
runtime.boundAsyncData.add(entry);
|
|
1554
|
-
const unbind = () => {
|
|
1555
|
-
const index = bindings.indexOf(binding);
|
|
1556
|
-
if (index === -1) return;
|
|
1557
|
-
bindings.splice(index, 1);
|
|
1558
|
-
if (bindings.length > 0) return;
|
|
1559
|
-
lastShown = ++runtime.asyncDataClock;
|
|
1560
|
-
runtime.boundAsyncData.delete(entry);
|
|
1561
|
-
if (!run) return;
|
|
1562
|
-
run.controller.abort();
|
|
1563
|
-
run = undefined;
|
|
1564
|
-
// Back to what the last answer left.
|
|
1565
|
-
status.value =
|
|
1566
|
-
error.value != null
|
|
1567
|
-
? "error"
|
|
1568
|
-
: data.value === null
|
|
1569
|
-
? "idle"
|
|
1570
|
-
: "success";
|
|
1571
|
-
};
|
|
1572
|
-
return { unbind };
|
|
1573
|
-
},
|
|
1574
|
-
load,
|
|
1575
|
-
schedule() {
|
|
1576
|
-
// Started after the caller's turn, an explicit refresh in that turn
|
|
1577
|
-
// replaces this first run instead of cancelling it mid-request.
|
|
1578
|
-
scheduled ??= Promise.resolve().then(() => {
|
|
1579
|
-
scheduled = undefined;
|
|
1580
|
-
return load();
|
|
1581
|
-
});
|
|
1582
|
-
},
|
|
1583
|
-
async settled() {
|
|
1584
|
-
await scheduled;
|
|
1585
|
-
await adoption;
|
|
1586
|
-
while (run) await run.promise;
|
|
1587
|
-
},
|
|
1588
|
-
unused: () => bindings.length === 0,
|
|
1589
|
-
lastShown: () => lastShown,
|
|
1590
|
-
justReadForAnother: () =>
|
|
1591
|
-
bindings.length > 1 && Date.now() - answeredAt < JOIN_WITHOUT_READ_MS,
|
|
1592
|
-
running: () => run !== undefined,
|
|
1593
|
-
pageRefresh() {
|
|
1594
|
-
const wanted = bindings.some(
|
|
1595
|
-
(binding) => binding.options.refreshOnPageRefresh !== false,
|
|
1596
|
-
);
|
|
1597
|
-
// Data a caller waits to `execute()` stays unread.
|
|
1598
|
-
if (!wanted || (status.value === "idle" && !run)) return undefined;
|
|
1599
|
-
return refresh();
|
|
1600
|
-
},
|
|
1601
|
-
adoptLater(state) {
|
|
1602
|
-
heldBack = state;
|
|
1603
|
-
status.value = "pending";
|
|
1604
|
-
adoption = Promise.resolve().then(adoptNow);
|
|
1605
|
-
},
|
|
1606
|
-
adoptNow,
|
|
1607
|
-
adoption: () => (heldBack ? adoption : undefined),
|
|
1608
|
-
};
|
|
1609
|
-
return entry;
|
|
1610
|
-
}
|
|
1611
|
-
|
|
1612
|
-
/**
|
|
1613
|
-
* The state the server's render left a key in, if it rendered the key: its
|
|
1614
|
-
* data, or its failure next to the default it rendered.
|
|
1615
|
-
*/
|
|
1616
|
-
function serverAsyncDataState<T>(
|
|
1617
|
-
runtime: DmsFrontendRuntime,
|
|
1618
|
-
key: string,
|
|
1619
|
-
fallback: AsyncDataInitialState<T>,
|
|
1620
|
-
): AsyncDataInitialState<T> | undefined {
|
|
1621
|
-
if (runtime.hydratedAsyncData.has(key))
|
|
1622
|
-
return {
|
|
1623
|
-
data: runtime.hydratedAsyncData.get(key) as T,
|
|
1624
|
-
error: null,
|
|
1625
|
-
status: "success",
|
|
1626
|
-
};
|
|
1627
|
-
const failure = runtime.hydratedAsyncErrors.get(key);
|
|
1628
|
-
if (!failure) return undefined;
|
|
1629
|
-
return {
|
|
1630
|
-
data: fallback.data,
|
|
1631
|
-
error: createDmsError(failure),
|
|
1632
|
-
status: "error",
|
|
1633
|
-
};
|
|
1634
|
-
}
|
|
1635
|
-
|
|
1636
|
-
/**
|
|
1637
|
-
* The key an entry is stored and transferred under: the caller's key, in
|
|
1638
|
-
* the runtime's scope (`setDmsAsyncDataScope`).
|
|
1639
|
-
*/
|
|
1640
|
-
function scopedKey(runtime: DmsFrontendRuntime, key: string): string {
|
|
1641
|
-
return runtime.asyncDataScope === undefined
|
|
1642
|
-
? key
|
|
1643
|
-
: `${runtime.asyncDataScope}|${key}`;
|
|
1644
|
-
}
|
|
1645
|
-
|
|
1646
|
-
function asyncDataEntry<T>(
|
|
1647
|
-
runtime: DmsFrontendRuntime,
|
|
1648
|
-
key: string | undefined,
|
|
1649
|
-
options: UseAsyncDataOptions<T>,
|
|
1650
|
-
inComponent: boolean,
|
|
1651
|
-
): AsyncDataEntry<T> {
|
|
1652
|
-
const stored = key === undefined ? undefined : scopedKey(runtime, key);
|
|
1653
|
-
const shared =
|
|
1654
|
-
stored === undefined ? undefined : runtime.asyncData.get(stored);
|
|
1655
|
-
if (shared) return asyncDataEntries.get(shared) as AsyncDataEntry<T>;
|
|
1656
|
-
const fallback: AsyncDataInitialState<T> = {
|
|
1657
|
-
data: options.default?.() ?? null,
|
|
1658
|
-
error: null,
|
|
1659
|
-
status: "idle",
|
|
1660
|
-
};
|
|
1661
|
-
const server =
|
|
1662
|
-
stored === undefined
|
|
1663
|
-
? undefined
|
|
1664
|
-
: serverAsyncDataState<T>(runtime, stored, fallback);
|
|
1665
|
-
const entry = createAsyncDataEntry<T>(
|
|
1666
|
-
runtime,
|
|
1667
|
-
key,
|
|
1668
|
-
server && !inComponent ? server : fallback,
|
|
1669
|
-
options.deep === true,
|
|
1670
|
-
);
|
|
1671
|
-
// A component's setup saw the server's run in flight, not its result: the
|
|
1672
|
-
// browser holds the result back as long, see `startAsyncData`.
|
|
1673
|
-
if (server && inComponent) entry.adoptLater(server);
|
|
1674
|
-
if (stored === undefined) return entry;
|
|
1675
|
-
runtime.hydratedAsyncData.delete(stored);
|
|
1676
|
-
runtime.hydratedAsyncErrors.delete(stored);
|
|
1677
|
-
const result = entry.result as DmsAsyncData<unknown>;
|
|
1678
|
-
runtime.asyncData.set(stored, result);
|
|
1679
|
-
asyncDataEntries.set(result, entry as AsyncDataEntry<unknown>);
|
|
1680
|
-
return entry;
|
|
1681
|
-
}
|
|
1682
|
-
|
|
1683
|
-
/**
|
|
1684
|
-
* Loads data once for every component that shows the same key, with Nuxt's
|
|
1685
|
-
* semantics: a server render waits for it before rendering, and transfers it,
|
|
1686
|
-
* whether or not the caller awaits it; `lazy` only spares client navigations
|
|
1687
|
-
* the wait; `server: false` leaves it to the browser.
|
|
1688
|
-
*
|
|
1689
|
-
* In the browser, data loaded before is shown at once and read again in the
|
|
1690
|
-
* background on each mount and each return to its key (stale while
|
|
1691
|
-
* revalidate), except while the page the server rendered hydrates: its data
|
|
1692
|
-
* is the server's, just read.
|
|
1693
|
-
*
|
|
1694
|
-
* A call without a key cannot be transferred: the server leaves it to the
|
|
1695
|
-
* browser, as `server: false` does, so the page hydrates with what it shows.
|
|
1696
|
-
*/
|
|
1697
|
-
export function useDmsAsyncData<T>(
|
|
1698
|
-
keyOrHandler: MaybeRefOrGetter<DmsAsyncDataKey> | DmsAsyncDataHandler<T>,
|
|
1699
|
-
handler?: DmsAsyncDataHandler<T>,
|
|
1700
|
-
callerOptions: UseAsyncDataOptions<T> = {},
|
|
1701
|
-
): DmsAsyncDataResult<T> {
|
|
1702
|
-
const runtime = useDmsRuntime();
|
|
1703
|
-
// A call outside any scope (a plugin past its first await, a callback)
|
|
1704
|
-
// belongs to the application: its watchers and its polling live as long.
|
|
1705
|
-
if (getCurrentScope() || !runtime.scope.active)
|
|
1706
|
-
return createAsyncDataCall(runtime, keyOrHandler, handler, callerOptions);
|
|
1707
|
-
return runtime.scope.run(() =>
|
|
1708
|
-
createAsyncDataCall(runtime, keyOrHandler, handler, callerOptions),
|
|
1709
|
-
)!;
|
|
1710
|
-
}
|
|
1711
|
-
|
|
1712
|
-
function createAsyncDataCall<T>(
|
|
1713
|
-
runtime: DmsFrontendRuntime,
|
|
1714
|
-
keyOrHandler: MaybeRefOrGetter<DmsAsyncDataKey> | DmsAsyncDataHandler<T>,
|
|
1715
|
-
handler: DmsAsyncDataHandler<T> | undefined,
|
|
1716
|
-
callerOptions: UseAsyncDataOptions<T>,
|
|
1717
|
-
): DmsAsyncDataResult<T> {
|
|
1718
|
-
// With a handler second, the first argument is the key: a string, or a ref
|
|
1719
|
-
// or getter whose changes move the caller to the data of another key.
|
|
1720
|
-
const keySource = handler
|
|
1721
|
-
? (keyOrHandler as MaybeRefOrGetter<DmsAsyncDataKey>)
|
|
1722
|
-
: undefined;
|
|
1723
|
-
const execute = handler ?? keyOrHandler;
|
|
1724
|
-
if (typeof execute !== "function")
|
|
1725
|
-
throw new Error("useDmsAsyncData requires a handler");
|
|
1726
|
-
const keyless = keySource === undefined;
|
|
1727
|
-
if (keyless && runtime.isServer && runtime.devWarnings)
|
|
1728
|
-
console.warn(
|
|
1729
|
-
"DMS: useDmsAsyncData without a key, or useDmsFetch with a body it cannot key, " +
|
|
1730
|
-
"is left to the browser: give it a key for the server to render it.",
|
|
1731
|
-
);
|
|
1732
|
-
const options: UseAsyncDataOptions<T> = keyless
|
|
1733
|
-
? { ...callerOptions, server: false }
|
|
1734
|
-
: callerOptions;
|
|
1735
|
-
const inComponent = getCurrentInstance() !== null;
|
|
1736
|
-
const consumer = shallowRef(
|
|
1737
|
-
attachAsyncData(
|
|
1738
|
-
runtime,
|
|
1739
|
-
keyless ? undefined : (toValue(keySource) ?? null),
|
|
1740
|
-
execute as DmsAsyncDataHandler<T>,
|
|
1741
|
-
options,
|
|
1742
|
-
inComponent,
|
|
1743
|
-
),
|
|
1744
|
-
);
|
|
1745
|
-
if (getCurrentScope()) onScopeDispose(() => consumer.value.unbind());
|
|
1746
|
-
if (options.watch && options.watch.length)
|
|
1747
|
-
refreshOnWatchedChange(consumer, options.watch);
|
|
1748
|
-
// Registered in the browser too, though only a server calls it: Vue makes
|
|
1749
|
-
// a component that prefetches an async boundary, which shifts the ids
|
|
1750
|
-
// `useId()` gives the components after it, and both sides must agree. It
|
|
1751
|
-
// waits for the key the caller is on by then, which its setup may have set.
|
|
1752
|
-
let prefetched = false;
|
|
1753
|
-
if (inComponent && options.immediate !== false && options.server !== false)
|
|
1754
|
-
onServerPrefetch(() => {
|
|
1755
|
-
prefetched = true;
|
|
1756
|
-
return consumer.value.entry.settled();
|
|
1757
|
-
});
|
|
1758
|
-
pollAsyncData(runtime, consumer, options);
|
|
1759
|
-
const { entry } = consumer.value;
|
|
1760
|
-
const waited = startAsyncData(runtime, entry, options, inComponent);
|
|
1761
|
-
const result =
|
|
1762
|
-
keySource == null || typeof keySource === "string"
|
|
1763
|
-
? entry.result
|
|
1764
|
-
: followAsyncDataKey(
|
|
1765
|
-
runtime,
|
|
1766
|
-
consumer,
|
|
1767
|
-
keySource,
|
|
1768
|
-
execute as DmsAsyncDataHandler<T>,
|
|
1769
|
-
options,
|
|
1770
|
-
inComponent,
|
|
1771
|
-
() => !prefetched,
|
|
1772
|
-
);
|
|
1773
|
-
return Object.assign(
|
|
1774
|
-
Promise.resolve(waited).then(() => result),
|
|
1775
|
-
result,
|
|
1776
|
-
);
|
|
1777
|
-
}
|
|
1778
|
-
|
|
1779
|
-
/**
|
|
1780
|
-
* Reads the data again when a watched source changes, but when the caller's
|
|
1781
|
-
* key moved in the same turn: the new key's own read already has the change.
|
|
1782
|
-
*/
|
|
1783
|
-
function refreshOnWatchedChange<T>(
|
|
1784
|
-
consumer: ShallowRef<AsyncDataConsumer<T>>,
|
|
1785
|
-
sources: unknown[],
|
|
1786
|
-
): void {
|
|
1787
|
-
let moved = false;
|
|
1788
|
-
watch(
|
|
1789
|
-
() => consumer.value.entry,
|
|
1790
|
-
() => {
|
|
1791
|
-
moved = true;
|
|
1792
|
-
void nextTick(() => {
|
|
1793
|
-
moved = false;
|
|
1794
|
-
});
|
|
1795
|
-
},
|
|
1796
|
-
{ flush: "sync" },
|
|
1797
|
-
);
|
|
1798
|
-
watch(sources as never[], () => {
|
|
1799
|
-
if (!moved) void consumer.value.entry.result.refresh();
|
|
1800
|
-
});
|
|
1801
|
-
}
|
|
1802
|
-
|
|
1803
|
-
/**
|
|
1804
|
-
* Reads the data again every `refreshInterval`, in the browser, following
|
|
1805
|
-
* the interval as it changes: none, `0` or `null` stops it.
|
|
1806
|
-
*/
|
|
1807
|
-
function pollAsyncData<T>(
|
|
1808
|
-
runtime: DmsFrontendRuntime,
|
|
1809
|
-
consumer: ShallowRef<AsyncDataConsumer<T>>,
|
|
1810
|
-
options: UseAsyncDataOptions<T>,
|
|
1811
|
-
): void {
|
|
1812
|
-
const interval = options.refreshInterval;
|
|
1813
|
-
if (runtime.isServer || interval === undefined) return;
|
|
1814
|
-
let timer: ReturnType<typeof setInterval> | undefined;
|
|
1815
|
-
const stop = () => {
|
|
1816
|
-
clearInterval(timer);
|
|
1817
|
-
timer = undefined;
|
|
1818
|
-
};
|
|
1819
|
-
watch(
|
|
1820
|
-
() => toValue(interval),
|
|
1821
|
-
(period) => {
|
|
1822
|
-
stop();
|
|
1823
|
-
if (period && period > 0)
|
|
1824
|
-
timer = setInterval(() => pollOnce(consumer, options), period);
|
|
1825
|
-
},
|
|
1826
|
-
{ immediate: true },
|
|
1827
|
-
);
|
|
1828
|
-
if (getCurrentScope()) onScopeDispose(stop);
|
|
1829
|
-
}
|
|
1830
|
-
|
|
1831
|
-
function pollOnce<T>(
|
|
1832
|
-
consumer: ShallowRef<AsyncDataConsumer<T>>,
|
|
1833
|
-
options: UseAsyncDataOptions<T>,
|
|
1834
|
-
): void {
|
|
1835
|
-
if (typeof document !== "undefined" && document.visibilityState === "hidden")
|
|
1836
|
-
return;
|
|
1837
|
-
const result = consumer.value.entry.result;
|
|
1838
|
-
// Data a caller waits to `execute()` is not read before it is.
|
|
1839
|
-
if (result.status.value === "idle" && options.immediate === false) return;
|
|
1840
|
-
void result.refresh({ dedupe: "defer" });
|
|
1841
|
-
}
|
|
1842
|
-
|
|
1843
|
-
/**
|
|
1844
|
-
* Starts the run a caller needs, where its side needs it, and returns what
|
|
1845
|
-
* awaiting the call waits for.
|
|
1846
|
-
*/
|
|
1847
|
-
function startAsyncData<T>(
|
|
1848
|
-
runtime: DmsFrontendRuntime,
|
|
1849
|
-
entry: AsyncDataEntry<T>,
|
|
1850
|
-
options: UseAsyncDataOptions<T>,
|
|
1851
|
-
inComponent: boolean,
|
|
1852
|
-
): Promise<void> | undefined {
|
|
1853
|
-
const immediate = options.immediate !== false;
|
|
1854
|
-
const onServer = options.server !== false;
|
|
1855
|
-
const idle = entry.result.status.value === "idle";
|
|
1856
|
-
if (runtime.isServer) {
|
|
1857
|
-
if (!immediate || !onServer) return undefined;
|
|
1858
|
-
// A render that already answers a redirect throws its HTML away: what it
|
|
1859
|
-
// still has to read would reach nobody.
|
|
1860
|
-
if (runtime.serverRedirect) return undefined;
|
|
1861
|
-
if (idle && options.lazy) entry.schedule();
|
|
1862
|
-
else if (idle) void entry.load();
|
|
1863
|
-
const settled = entry.settled();
|
|
1864
|
-
if (!inComponent) runtime.serverPrefetches.add(settled);
|
|
1865
|
-
return settled;
|
|
1866
|
-
}
|
|
1867
|
-
// The server's render had the data once the setup awaited it, and before
|
|
1868
|
-
// the component rendered: handed over at the same points, it gives what
|
|
1869
|
-
// the setup derives from it, and so the HTML, the same values.
|
|
1870
|
-
const adoption = entry.adoption();
|
|
1871
|
-
if (adoption) {
|
|
1872
|
-
if (inComponent) onBeforeMount(entry.adoptNow);
|
|
1873
|
-
return adoption;
|
|
1874
|
-
}
|
|
1875
|
-
if (!immediate) return undefined;
|
|
1876
|
-
const waitForData = () =>
|
|
1877
|
-
!options.lazy && entry.result.data.value === null
|
|
1878
|
-
? entry.settled()
|
|
1879
|
-
: undefined;
|
|
1880
|
-
if (entry.running()) return waitForData();
|
|
1881
|
-
if (!idle) {
|
|
1882
|
-
// Loaded before: shown at once and read again in the background, except
|
|
1883
|
-
// while the page the server rendered hydrates with the server's data, or
|
|
1884
|
-
// when another caller of the page has just read it.
|
|
1885
|
-
if (!runtime.hydrating && !entry.justReadForAnother()) void entry.load();
|
|
1886
|
-
return waitForData();
|
|
1887
|
-
}
|
|
1888
|
-
// The server rendered the default in place of this data: the browser's
|
|
1889
|
-
// first render must show it too, before the request leaves.
|
|
1890
|
-
if (runtime.hydrating && (options.lazy || !onServer)) {
|
|
1891
|
-
if (inComponent) onMounted(() => void entry.load());
|
|
1892
|
-
else entry.schedule();
|
|
1893
|
-
} else if (options.lazy) entry.schedule();
|
|
1894
|
-
else void entry.load();
|
|
1895
|
-
return !options.lazy && entry.result.pending.value
|
|
1896
|
-
? entry.settled()
|
|
1897
|
-
: undefined;
|
|
1898
|
-
}
|
|
1899
|
-
|
|
1900
|
-
interface AsyncDataConsumer<T> extends AsyncDataBindingHandle {
|
|
1901
|
-
entry: AsyncDataEntry<T>;
|
|
1902
|
-
/** Whether no other component showed the key when this one came. */
|
|
1903
|
-
alone: boolean;
|
|
1904
|
-
}
|
|
1905
|
-
|
|
1906
|
-
/**
|
|
1907
|
-
* Binds a caller to the data of its key. A `null` key has no data to load:
|
|
1908
|
-
* the caller holds its default, and nothing runs until it has a key.
|
|
1909
|
-
*/
|
|
1910
|
-
function attachAsyncData<T>(
|
|
1911
|
-
runtime: DmsFrontendRuntime,
|
|
1912
|
-
key: string | null | undefined,
|
|
1913
|
-
handler: DmsAsyncDataHandler<T>,
|
|
1914
|
-
options: UseAsyncDataOptions<T>,
|
|
1915
|
-
inComponent: boolean,
|
|
1916
|
-
): AsyncDataConsumer<T> {
|
|
1917
|
-
const entry = asyncDataEntry(runtime, key ?? undefined, options, inComponent);
|
|
1918
|
-
const alone = entry.unused();
|
|
1919
|
-
if (key === null) return { entry, alone, unbind() {} };
|
|
1920
|
-
return { entry, alone, ...entry.bind({ handler, options }) };
|
|
1921
|
-
}
|
|
1922
|
-
|
|
1923
|
-
/**
|
|
1924
|
-
* Moves a caller to the data of each key its reactive key takes, and returns
|
|
1925
|
-
* refs that follow it there. Until the new key's data is in, the previous
|
|
1926
|
-
* data stays on screen rather than the default flashing in its place: the
|
|
1927
|
-
* caller's refs show it, and `placeholder` says so, while the new key's own
|
|
1928
|
-
* data stays its own, so a run that fails or that a switch away aborts never
|
|
1929
|
-
* leaves it another key's data. A key loaded before shows its own data at
|
|
1930
|
-
* once, and is read again in the background.
|
|
1931
|
-
*
|
|
1932
|
-
* A new key is read where the first one would have been. A server reads it
|
|
1933
|
-
* neither for `server: false` nor for `immediate: false`, and otherwise only
|
|
1934
|
-
* while its render still waits for the caller: until the prefetch of the
|
|
1935
|
-
* caller's component, or, outside a component, as a call made then. Past
|
|
1936
|
-
* that point the server renders the default and the browser reads the key. A
|
|
1937
|
-
* browser hydrating the server's HTML reads a key the server did not render
|
|
1938
|
-
* once the component is mounted, so that its first render matches the HTML.
|
|
1939
|
-
*/
|
|
1940
|
-
function followAsyncDataKey<T>(
|
|
1941
|
-
runtime: DmsFrontendRuntime,
|
|
1942
|
-
consumer: ShallowRef<AsyncDataConsumer<T>>,
|
|
1943
|
-
keySource: MaybeRefOrGetter<DmsAsyncDataKey>,
|
|
1944
|
-
handler: DmsAsyncDataHandler<T>,
|
|
1945
|
-
options: UseAsyncDataOptions<T>,
|
|
1946
|
-
inComponent: boolean,
|
|
1947
|
-
serverWaits: () => boolean,
|
|
1948
|
-
): DmsAsyncData<T> {
|
|
1949
|
-
// A read held back until the hydrating component is mounted.
|
|
1950
|
-
let held = false;
|
|
1951
|
-
let mounted = !(inComponent && runtime.hydrating);
|
|
1952
|
-
if (!mounted)
|
|
1953
|
-
onMounted(() => {
|
|
1954
|
-
mounted = true;
|
|
1955
|
-
const { entry } = consumer.value;
|
|
1956
|
-
if (held && entry.result.status.value === "idle") void entry.load();
|
|
1957
|
-
held = false;
|
|
1958
|
-
});
|
|
1959
|
-
const startOnServer = (entry: AsyncDataEntry<T>) => {
|
|
1960
|
-
if (
|
|
1961
|
-
options.server === false ||
|
|
1962
|
-
options.immediate === false ||
|
|
1963
|
-
!serverWaits()
|
|
1964
|
-
)
|
|
1965
|
-
return;
|
|
1966
|
-
void entry.load();
|
|
1967
|
-
if (!inComponent) runtime.serverPrefetches.add(entry.settled());
|
|
1968
|
-
};
|
|
1969
|
-
const current = () => consumer.value.entry.result;
|
|
1970
|
-
// The previous key's data, shown until the current key settles.
|
|
1971
|
-
const carried = shallowRef<{ value: T | null } | undefined>();
|
|
1972
|
-
const settled = (result: DmsAsyncData<T>) =>
|
|
1973
|
-
result.status.value === "success" || result.status.value === "error";
|
|
1974
|
-
const shownData = (): T | null => {
|
|
1975
|
-
const result = current();
|
|
1976
|
-
return carried.value && !settled(result)
|
|
1977
|
-
? carried.value.value
|
|
1978
|
-
: result.data.value;
|
|
1979
|
-
};
|
|
1980
|
-
watch(
|
|
1981
|
-
() => settled(current()),
|
|
1982
|
-
(done) => {
|
|
1983
|
-
if (done) carried.value = undefined;
|
|
1984
|
-
},
|
|
1985
|
-
{ flush: "sync" },
|
|
1986
|
-
);
|
|
1987
|
-
// Data written to the current key from elsewhere (`useDmsData`, another
|
|
1988
|
-
// caller of the key) is its own: it replaces what is carried.
|
|
1989
|
-
watch(
|
|
1990
|
-
() => [current(), current().data.value] as const,
|
|
1991
|
-
([result], [previous]) => {
|
|
1992
|
-
if (result === previous) carried.value = undefined;
|
|
1993
|
-
},
|
|
1994
|
-
{ flush: "sync" },
|
|
1995
|
-
);
|
|
1996
|
-
watch(
|
|
1997
|
-
() => toValue(keySource) ?? null,
|
|
1998
|
-
(key) => {
|
|
1999
|
-
const previous = current();
|
|
2000
|
-
const shown = shownData();
|
|
2001
|
-
const wasRunning = previous.pending.value;
|
|
2002
|
-
// Past the setup, the server's data for the new key, if it rendered
|
|
2003
|
-
// any, is taken over at once.
|
|
2004
|
-
const next = attachAsyncData(runtime, key, handler, options, false);
|
|
2005
|
-
const { entry } = next;
|
|
2006
|
-
const carry =
|
|
2007
|
-
next.alone && entry.result.status.value === "idle" && shown !== null;
|
|
2008
|
-
// Unbound first: the previous key's run, aborted, settles it, which
|
|
2009
|
-
// must not drop what is carried to the next key.
|
|
2010
|
-
consumer.value.unbind();
|
|
2011
|
-
consumer.value = next;
|
|
2012
|
-
carried.value = carry ? { value: shown } : undefined;
|
|
2013
|
-
held = false;
|
|
2014
|
-
// `watch: false` keeps the caller on the key it names, without a run.
|
|
2015
|
-
if (key === null || options.watch === false) return;
|
|
2016
|
-
if (runtime.isServer) return startOnServer(entry);
|
|
2017
|
-
const wanted =
|
|
2018
|
-
options.immediate !== false || shown !== null || wasRunning;
|
|
2019
|
-
if (!wanted) return;
|
|
2020
|
-
if (!mounted) held = true;
|
|
2021
|
-
else if (!inComponent && runtime.hydrating) entry.schedule();
|
|
2022
|
-
else void entry.load();
|
|
2023
|
-
},
|
|
2024
|
-
{ flush: "sync" },
|
|
2025
|
-
);
|
|
2026
|
-
const through = <V>(read: (result: DmsAsyncData<T>) => Ref<V>): Ref<V> =>
|
|
2027
|
-
computed({
|
|
2028
|
-
get: () => read(current()).value,
|
|
2029
|
-
set: (value) => {
|
|
2030
|
-
read(current()).value = value;
|
|
2031
|
-
},
|
|
2032
|
-
});
|
|
2033
|
-
return {
|
|
2034
|
-
data: computed({
|
|
2035
|
-
get: shownData,
|
|
2036
|
-
set: (value) => {
|
|
2037
|
-
carried.value = undefined;
|
|
2038
|
-
current().data.value = value;
|
|
2039
|
-
},
|
|
2040
|
-
}),
|
|
2041
|
-
key: computed(() => current().key.value),
|
|
2042
|
-
placeholder: computed(
|
|
2043
|
-
() => carried.value !== undefined && !settled(current()),
|
|
2044
|
-
),
|
|
2045
|
-
error: through((result) => result.error),
|
|
2046
|
-
pending: computed(() => current().pending.value),
|
|
2047
|
-
status: through((result) => result.status),
|
|
2048
|
-
execute: (refreshOptions) => current().refresh(refreshOptions),
|
|
2049
|
-
refresh: (refreshOptions) => current().refresh(refreshOptions),
|
|
2050
|
-
clear: () => {
|
|
2051
|
-
carried.value = undefined;
|
|
2052
|
-
current().clear();
|
|
2053
|
-
},
|
|
2054
|
-
};
|
|
2055
|
-
}
|
|
2056
|
-
|
|
2057
|
-
/** What a paged read keeps under its key: the pages loaded, in order. */
|
|
2058
|
-
export interface DmsPagedDataValue<TPage, TCursor> {
|
|
2059
|
-
pages: TPage[];
|
|
2060
|
-
/** The cursor of the page after the last one; `null` once there is none. */
|
|
2061
|
-
next: TCursor | null;
|
|
2062
|
-
}
|
|
2063
|
-
/** What a paged read's handler receives. */
|
|
2064
|
-
export interface DmsPagedDataContext<TCursor> extends DmsAsyncDataContext {
|
|
2065
|
-
/** The page to read: `initialCursor` for the first one. */
|
|
2066
|
-
cursor: TCursor | undefined;
|
|
2067
|
-
/** Its position, from `0`. */
|
|
2068
|
-
index: number;
|
|
2069
|
-
}
|
|
2070
|
-
export type DmsPagedDataHandler<TPage, TCursor> = (
|
|
2071
|
-
context: DmsPagedDataContext<TCursor>,
|
|
2072
|
-
) => Promise<TPage>;
|
|
2073
|
-
export interface UseDmsPagedDataOptions<TPage, TCursor> extends Omit<
|
|
2074
|
-
UseAsyncDataOptions<DmsPagedDataValue<TPage, TCursor>>,
|
|
2075
|
-
"default" | "transform" | "pick"
|
|
2076
|
-
> {
|
|
2077
|
-
/** The cursor of the page after `page`, or `null` (or `undefined`) for none. */
|
|
2078
|
-
nextCursor(page: TPage, pages: TPage[]): TCursor | null | undefined;
|
|
2079
|
-
/** The first page's cursor; none by default. */
|
|
2080
|
-
initialCursor?: TCursor;
|
|
2081
|
-
}
|
|
2082
|
-
export interface DmsPagedData<TPage, TCursor> extends DmsAsyncData<
|
|
2083
|
-
DmsPagedDataValue<TPage, TCursor>
|
|
2084
|
-
> {
|
|
2085
|
-
/** The pages loaded so far, in order; none before the first answer. */
|
|
2086
|
-
pages: Readonly<Ref<TPage[]>>;
|
|
2087
|
-
/** Whether `loadMore()` has a page to read. */
|
|
2088
|
-
hasMore: Readonly<Ref<boolean>>;
|
|
2089
|
-
/** `true` while `loadMore()` reads its page. */
|
|
2090
|
-
loadingMore: Readonly<Ref<boolean>>;
|
|
2091
|
-
/**
|
|
2092
|
-
* Reads the next page and appends it under the same key. It waits for a
|
|
2093
|
-
* read in flight first, does nothing once there is no next page, and puts
|
|
2094
|
-
* a failure in `error`, keeping the pages loaded.
|
|
2095
|
-
*/
|
|
2096
|
-
loadMore(): Promise<void>;
|
|
2097
|
-
}
|
|
2098
|
-
export type DmsPagedDataResult<TPage, TCursor> = DmsPagedData<TPage, TCursor> &
|
|
2099
|
-
Promise<DmsPagedData<TPage, TCursor>>;
|
|
2100
|
-
|
|
2101
|
-
type PagedValue<TPage, TCursor> = DmsPagedDataValue<TPage, TCursor>;
|
|
2102
|
-
|
|
2103
|
-
/** Reads `count` pages from the first, as long as each has a next one. */
|
|
2104
|
-
async function readPages<TPage, TCursor>(
|
|
2105
|
-
handler: DmsPagedDataHandler<TPage, TCursor>,
|
|
2106
|
-
options: UseDmsPagedDataOptions<TPage, TCursor>,
|
|
2107
|
-
count: number,
|
|
2108
|
-
signal: AbortSignal,
|
|
2109
|
-
): Promise<PagedValue<TPage, TCursor>> {
|
|
2110
|
-
const pages: TPage[] = [];
|
|
2111
|
-
let cursor: TCursor | undefined = options.initialCursor;
|
|
2112
|
-
for (let index = 0; index < count; index++) {
|
|
2113
|
-
const page = await handler({ cursor, index, signal });
|
|
2114
|
-
pages.push(page);
|
|
2115
|
-
const next = options.nextCursor(page, pages);
|
|
2116
|
-
if (next == null) return { pages, next: null };
|
|
2117
|
-
cursor = next;
|
|
2118
|
-
}
|
|
2119
|
-
return { pages, next: cursor ?? null };
|
|
2120
|
-
}
|
|
2121
|
-
|
|
2122
|
-
/** Resolves once `result` has no run in flight. */
|
|
2123
|
-
function untilSettled<T>(result: DmsAsyncData<T>): Promise<void> {
|
|
2124
|
-
if (!result.pending.value) return Promise.resolve();
|
|
2125
|
-
return new Promise((resolve) => {
|
|
2126
|
-
const stop = watch(result.pending, (pending) => {
|
|
2127
|
-
if (pending) return;
|
|
2128
|
-
stop();
|
|
2129
|
-
resolve();
|
|
2130
|
-
});
|
|
2131
|
-
});
|
|
2132
|
-
}
|
|
2133
|
-
|
|
2134
|
-
/**
|
|
2135
|
-
* A list read page by page under one key: "load more", an infinite list.
|
|
2136
|
-
* The first page loads as `useDmsAsyncData` loads its data (a server render
|
|
2137
|
-
* waits for it and transfers it), and `loadMore()` appends the next one to
|
|
2138
|
-
* the same key, so the pages a component loaded show at once on a return to
|
|
2139
|
-
* it. A revalidation (a mount, `refresh()`, `refreshDmsPageData()`, the
|
|
2140
|
-
* polling) reads every page loaded so far again, from the first, and
|
|
2141
|
-
* replaces them once all are in. Put what filters the list in the key: a new
|
|
2142
|
-
* key starts again from its first page.
|
|
2143
|
-
*
|
|
2144
|
-
* @param key The key, or a ref or getter for it
|
|
2145
|
-
* @param handler Reads one page, from the cursor it receives
|
|
2146
|
-
* @param options `nextCursor`, `initialCursor` and the async data options
|
|
2147
|
-
*/
|
|
2148
|
-
export function useDmsPagedData<TPage, TCursor = unknown>(
|
|
2149
|
-
key: MaybeRefOrGetter<DmsAsyncDataKey>,
|
|
2150
|
-
handler: DmsPagedDataHandler<TPage, TCursor>,
|
|
2151
|
-
options: UseDmsPagedDataOptions<TPage, TCursor>,
|
|
2152
|
-
): DmsPagedDataResult<TPage, TCursor> {
|
|
2153
|
-
const {
|
|
2154
|
-
nextCursor: _next,
|
|
2155
|
-
initialCursor: _initial,
|
|
2156
|
-
...asyncOptions
|
|
2157
|
-
} = options;
|
|
2158
|
-
let shown: DmsAsyncData<PagedValue<TPage, TCursor>> | undefined;
|
|
2159
|
-
const loadedCount = () =>
|
|
2160
|
-
shown && !shown.placeholder.value
|
|
2161
|
-
? Math.max(1, shown.data.value?.pages.length ?? 0)
|
|
2162
|
-
: 1;
|
|
2163
|
-
const call = useDmsAsyncData<PagedValue<TPage, TCursor>>(
|
|
2164
|
-
key,
|
|
2165
|
-
({ signal }) => readPages(handler, options, loadedCount(), signal),
|
|
2166
|
-
asyncOptions,
|
|
2167
|
-
);
|
|
2168
|
-
shown = call;
|
|
2169
|
-
const result: DmsPagedData<TPage, TCursor> = Object.assign(
|
|
2170
|
-
{},
|
|
2171
|
-
call as DmsAsyncData<PagedValue<TPage, TCursor>>,
|
|
2172
|
-
pagedControls(call, handler, options),
|
|
2173
|
-
);
|
|
2174
|
-
return Object.assign(
|
|
2175
|
-
call.then(() => result),
|
|
2176
|
-
result,
|
|
2177
|
-
);
|
|
2178
|
-
}
|
|
2179
|
-
|
|
2180
|
-
function pagedControls<TPage, TCursor>(
|
|
2181
|
-
call: DmsAsyncData<PagedValue<TPage, TCursor>>,
|
|
2182
|
-
handler: DmsPagedDataHandler<TPage, TCursor>,
|
|
2183
|
-
options: UseDmsPagedDataOptions<TPage, TCursor>,
|
|
2184
|
-
) {
|
|
2185
|
-
const loadingMore = ref(false);
|
|
2186
|
-
let controller: AbortController | undefined;
|
|
2187
|
-
const abort = () => {
|
|
2188
|
-
controller?.abort();
|
|
2189
|
-
controller = undefined;
|
|
2190
|
-
loadingMore.value = false;
|
|
2191
|
-
};
|
|
2192
|
-
watch(call.key, abort);
|
|
2193
|
-
if (getCurrentScope()) onScopeDispose(abort);
|
|
2194
|
-
const hasMore = computed(
|
|
2195
|
-
() => !call.placeholder.value && (call.data.value?.next ?? null) !== null,
|
|
2196
|
-
);
|
|
2197
|
-
const loadMore = async (): Promise<void> => {
|
|
2198
|
-
if (call.pending.value) await untilSettled(call);
|
|
2199
|
-
const before = call.data.value;
|
|
2200
|
-
if (!before) return call.execute();
|
|
2201
|
-
if (before.next === null || controller) return;
|
|
2202
|
-
const current = new AbortController();
|
|
2203
|
-
controller = current;
|
|
2204
|
-
loadingMore.value = true;
|
|
2205
|
-
try {
|
|
2206
|
-
const page = await handler({
|
|
2207
|
-
cursor: before.next,
|
|
2208
|
-
index: before.pages.length,
|
|
2209
|
-
signal: current.signal,
|
|
2210
|
-
});
|
|
2211
|
-
if (controller !== current || call.data.value !== before) return;
|
|
2212
|
-
const pages = [...before.pages, page];
|
|
2213
|
-
call.data.value = {
|
|
2214
|
-
pages,
|
|
2215
|
-
next: options.nextCursor(page, pages) ?? null,
|
|
2216
|
-
};
|
|
2217
|
-
} catch (reason) {
|
|
2218
|
-
if (controller === current) call.error.value = reason;
|
|
2219
|
-
} finally {
|
|
2220
|
-
if (controller === current) abort();
|
|
2221
|
-
}
|
|
2222
|
-
};
|
|
2223
|
-
return {
|
|
2224
|
-
pages: computed(() => call.data.value?.pages ?? []),
|
|
2225
|
-
hasMore,
|
|
2226
|
-
loadingMore: readonly(loadingMore),
|
|
2227
|
-
loadMore,
|
|
2228
|
-
};
|
|
2229
|
-
}
|
|
2230
|
-
|
|
2231
|
-
/**
|
|
2232
|
-
* The data another caller loads under `key`, as Nuxt's `useNuxtData`: to
|
|
2233
|
-
* update it in place without reading it again, from what a write answered
|
|
2234
|
-
* (an optimistic update) or what a realtime event says changed. It follows
|
|
2235
|
-
* the key in the runtime's scope (`setDmsAsyncDataScope`): `undefined` until
|
|
2236
|
-
* a caller loads the key, its data once there, `undefined` again once the
|
|
2237
|
-
* browser lets the key go (past the most recent keys it keeps) or the scope
|
|
2238
|
-
* changes. It does not load the key, nor keep it.
|
|
2239
|
-
*/
|
|
2240
|
-
export function useDmsData<T>(
|
|
2241
|
-
key: MaybeRefOrGetter<DmsAsyncDataKey>,
|
|
2242
|
-
): DmsData<T> {
|
|
2243
|
-
const runtime = useDmsRuntime();
|
|
2244
|
-
const entry = () => {
|
|
2245
|
-
const name = toValue(key);
|
|
2246
|
-
const result =
|
|
2247
|
-
name == null
|
|
2248
|
-
? undefined
|
|
2249
|
-
: runtime.asyncData.get(scopedKey(runtime, String(name)));
|
|
2250
|
-
return result && asyncDataEntries.get(result);
|
|
2251
|
-
};
|
|
2252
|
-
// A component after the caller, in the page or in its subtree, renders the
|
|
2253
|
-
// data the browser hydrates it with: the server waits for the caller's run
|
|
2254
|
-
// too. One before it has none on either side. Whether the key is there as
|
|
2255
|
-
// the setup runs is the same on both sides, and so are the async
|
|
2256
|
-
// boundaries `useId()` counts.
|
|
2257
|
-
const loaded = entry();
|
|
2258
|
-
if (loaded && getCurrentInstance()) onServerPrefetch(() => loaded.settled());
|
|
2259
|
-
return {
|
|
2260
|
-
data: computed({
|
|
2261
|
-
get() {
|
|
2262
|
-
// An entry only ever holds its own key's data: a caller showing the
|
|
2263
|
-
// previous key's data meanwhile keeps it in its own refs.
|
|
2264
|
-
return (entry()?.result.data.value ?? undefined) as T | undefined;
|
|
2265
|
-
},
|
|
2266
|
-
set(value) {
|
|
2267
|
-
const current = entry();
|
|
2268
|
-
if (!current) return;
|
|
2269
|
-
current.result.data.value = value ?? null;
|
|
2270
|
-
},
|
|
2271
|
-
}),
|
|
2272
|
-
};
|
|
2273
|
-
}
|
|
2274
|
-
|
|
2275
|
-
/** Reads the data of the keys given, or of every key, again. */
|
|
2276
|
-
export async function refreshDmsData(keys?: string | string[]): Promise<void> {
|
|
2277
|
-
const runtime = useDmsRuntime();
|
|
2278
|
-
const selected = keys
|
|
2279
|
-
? (Array.isArray(keys) ? keys : [keys]).map((key) =>
|
|
2280
|
-
scopedKey(runtime, key),
|
|
2281
|
-
)
|
|
2282
|
-
: [...runtime.asyncData.keys()];
|
|
2283
|
-
await Promise.all(
|
|
2284
|
-
selected.map((key) => runtime.asyncData.get(key)?.refresh()),
|
|
2285
|
-
);
|
|
2286
|
-
}
|
|
2287
|
-
|
|
2288
|
-
/**
|
|
2289
|
-
* Reads again the data the components on screen show, but those that opted
|
|
2290
|
-
* out (`refreshOnPageRefresh: false`) and those waiting for `execute()`:
|
|
2291
|
-
* what a page calls once something changed what it shows, after a save or
|
|
2292
|
-
* on a realtime event, instead of each component refreshing its own.
|
|
2293
|
-
*/
|
|
2294
|
-
export async function refreshDmsPageData(): Promise<void> {
|
|
2295
|
-
const runtime = useDmsRuntime();
|
|
2296
|
-
await Promise.all(
|
|
2297
|
-
[...runtime.boundAsyncData].map((entry) => entry.pageRefresh()),
|
|
2298
|
-
);
|
|
2299
|
-
}
|
|
2300
|
-
|
|
2301
|
-
/**
|
|
2302
|
-
* Puts the keys of the async data in a scope, such as a digest of the
|
|
2303
|
-
* account and the workspace: the same key in another scope is another
|
|
2304
|
-
* entry, never the data of the previous one. Moving to another scope drops
|
|
2305
|
-
* the data of every key, aborting its runs; the components on screen then
|
|
2306
|
-
* show their default until they load again. Call it on both sides before
|
|
2307
|
-
* the page renders, with the same value, so the browser finds the data the
|
|
2308
|
-
* server transferred; the scope is written in the page with each key, so a
|
|
2309
|
-
* digest rather than an identifier. Call it from a plugin registered with
|
|
2310
|
-
* `{ enforce: "pre" }`: a plugin of another module that reads data at setup
|
|
2311
|
-
* would otherwise run first, and the new scope abort its read.
|
|
2312
|
-
*
|
|
2313
|
-
* @param scope The scope, or `undefined` for none
|
|
2314
|
-
*/
|
|
2315
|
-
export function setDmsAsyncDataScope(scope: string | undefined): void {
|
|
2316
|
-
const runtime = useDmsRuntime();
|
|
2317
|
-
if (runtime.asyncDataScope === scope) return;
|
|
2318
|
-
runtime.asyncDataScope = scope;
|
|
2319
|
-
for (const result of runtime.asyncData.values()) result.clear();
|
|
2320
|
-
runtime.asyncData.clear();
|
|
2321
|
-
}
|
|
2322
|
-
|
|
2323
|
-
/**
|
|
2324
|
-
* Drops the data no component shows any more, but the most recently shown:
|
|
2325
|
-
* a return to a key shows its data at once, and the data does not pile up
|
|
2326
|
-
* over a session.
|
|
2327
|
-
*/
|
|
2328
|
-
function dropUnusedAsyncData(runtime: DmsFrontendRuntime): void {
|
|
2329
|
-
const unused = [...runtime.asyncData]
|
|
2330
|
-
.map(([key, result]) => ({ key, entry: asyncDataEntries.get(result) }))
|
|
2331
|
-
.filter(({ entry }) => entry?.unused())
|
|
2332
|
-
.sort((left, right) => right.entry!.lastShown() - left.entry!.lastShown());
|
|
2333
|
-
for (const { key } of unused.slice(MAX_UNUSED_ASYNC_DATA))
|
|
2334
|
-
runtime.asyncData.delete(key);
|
|
2335
|
-
}
|
|
2336
|
-
|
|
2337
|
-
export function serializeDmsAsyncData(
|
|
2338
|
-
runtime: DmsFrontendRuntime,
|
|
2339
|
-
): Record<string, unknown> {
|
|
2340
|
-
return Object.fromEntries(
|
|
2341
|
-
[...runtime.asyncData.entries()]
|
|
2342
|
-
.filter(([, entry]) => entry.status.value === "success")
|
|
2343
|
-
.map(([key, entry]) => [key, toRaw(entry.data.value)]),
|
|
2344
|
-
);
|
|
2345
|
-
}
|
|
2346
|
-
|
|
2347
|
-
const HTTP_ERROR_STATUS = { min: 400, max: 599 };
|
|
2348
|
-
const SERVER_FAILURE_STATUS = 500;
|
|
2349
|
-
const FETCH_ERROR_NAME = "FetchError";
|
|
2350
|
-
|
|
2351
|
-
/** The fields of a failure, under the names `createDmsError` and ofetch use. */
|
|
2352
|
-
interface DmsFailure extends DmsErrorData {
|
|
2353
|
-
name?: string;
|
|
2354
|
-
status?: number;
|
|
2355
|
-
statusText?: string;
|
|
2356
|
-
}
|
|
2357
|
-
|
|
2358
|
-
/**
|
|
2359
|
-
* What of a failure a page may carry, by the rule the error page follows: an
|
|
2360
|
-
* HTTP failure keeps its status, status text and message, and its body if it
|
|
2361
|
-
* is a 4xx, anything else
|
|
2362
|
-
* is a server failure whose cause only reaches the server log. A fetch error
|
|
2363
|
-
* names the URL it requested, which on a server is the backend's own address:
|
|
2364
|
-
* its status text stands for it.
|
|
2365
|
-
*/
|
|
2366
|
-
function transferredError(key: string, reason: unknown): DmsErrorData {
|
|
2367
|
-
const failure = (reason ?? {}) as DmsFailure;
|
|
2368
|
-
const statusCode = failure.statusCode ?? failure.status;
|
|
2369
|
-
if (
|
|
2370
|
-
statusCode === undefined ||
|
|
2371
|
-
!Number.isInteger(statusCode) ||
|
|
2372
|
-
statusCode < HTTP_ERROR_STATUS.min ||
|
|
2373
|
-
statusCode > HTTP_ERROR_STATUS.max
|
|
2374
|
-
) {
|
|
2375
|
-
console.error(`DMS async data "${key}" failed`, reason);
|
|
2376
|
-
return { statusCode: SERVER_FAILURE_STATUS };
|
|
2377
|
-
}
|
|
2378
|
-
const statusMessage = failure.statusMessage ?? failure.statusText;
|
|
2379
|
-
const transferred: DmsErrorData = {
|
|
2380
|
-
statusCode,
|
|
2381
|
-
statusMessage,
|
|
2382
|
-
message:
|
|
2383
|
-
failure.name === FETCH_ERROR_NAME ? statusMessage : failure.message,
|
|
2384
|
-
};
|
|
2385
|
-
// A 5xx body is the backend's own account of its failure, a stack or a
|
|
2386
|
-
// query at times: only a 4xx body, written for the client, is transferred.
|
|
2387
|
-
if (statusCode < SERVER_FAILURE_STATUS) transferred.data = failure.data;
|
|
2388
|
-
return transferred;
|
|
2389
|
-
}
|
|
2390
|
-
|
|
2391
|
-
/**
|
|
2392
|
-
* What a page's error page shows of an expected failure (a 4xx or 5xx): its
|
|
2393
|
-
* status code, status text and message. A fetch error's message names the URL
|
|
2394
|
-
* it requested, on a server the backend's own address: its status text
|
|
2395
|
-
* stands for it.
|
|
2396
|
-
*/
|
|
2397
|
-
export function pageErrorData(failure: Error & DmsErrorData): DmsErrorData {
|
|
2398
|
-
return {
|
|
2399
|
-
statusCode: failure.statusCode,
|
|
2400
|
-
message:
|
|
2401
|
-
failure.name === FETCH_ERROR_NAME
|
|
2402
|
-
? failure.statusMessage
|
|
2403
|
-
: failure.message,
|
|
2404
|
-
statusMessage: failure.statusMessage,
|
|
2405
|
-
};
|
|
2406
|
-
}
|
|
2407
|
-
|
|
2408
|
-
/**
|
|
2409
|
-
* Everything a server render hands the browser: the data and the failures of
|
|
2410
|
-
* its keys, and the state keys opted into. Read as the HTML is complete.
|
|
2411
|
-
*/
|
|
2412
|
-
export function serializeDmsPayload(runtime: DmsFrontendRuntime): DmsPayload {
|
|
2413
|
-
const errors = [...runtime.asyncData.entries()]
|
|
2414
|
-
.filter(([, entry]) => entry.status.value === "error")
|
|
2415
|
-
.map(([key, entry]): [string, DmsErrorData] => [
|
|
2416
|
-
key,
|
|
2417
|
-
transferredError(key, entry.error.value),
|
|
2418
|
-
]);
|
|
2419
|
-
const state = [...runtime.transferredState]
|
|
2420
|
-
.map((key): [string, unknown] => [
|
|
2421
|
-
key,
|
|
2422
|
-
toRaw(runtime.sharedState.get(key)?.value),
|
|
2423
|
-
])
|
|
2424
|
-
.filter(([, value]) => value !== undefined);
|
|
2425
|
-
return {
|
|
2426
|
-
data: serializeDmsAsyncData(runtime),
|
|
2427
|
-
errors: Object.fromEntries(errors),
|
|
2428
|
-
state: Object.fromEntries(state),
|
|
2429
|
-
};
|
|
2430
|
-
}
|
|
2431
|
-
|
|
2432
|
-
/** Starts a browser runtime from what the server rendered. */
|
|
2433
|
-
export function restoreDmsPayload(
|
|
2434
|
-
runtime: DmsFrontendRuntime,
|
|
2435
|
-
payload: Partial<DmsPayload>,
|
|
2436
|
-
): void {
|
|
2437
|
-
runtime.hydrating = true;
|
|
2438
|
-
for (const [key, value] of Object.entries(payload.data ?? {}))
|
|
2439
|
-
runtime.hydratedAsyncData.set(key, value);
|
|
2440
|
-
for (const [key, failure] of Object.entries(payload.errors ?? {}))
|
|
2441
|
-
runtime.hydratedAsyncErrors.set(key, failure);
|
|
2442
|
-
for (const [key, value] of Object.entries(payload.state ?? {}))
|
|
2443
|
-
runtime.sharedState.set(key, ref(value));
|
|
2444
|
-
}
|
|
2445
|
-
|
|
2446
|
-
export function useDmsState<T>(
|
|
2447
|
-
key: string,
|
|
2448
|
-
init?: () => T,
|
|
2449
|
-
options: DmsStateOptions = {},
|
|
2450
|
-
): Ref<T> {
|
|
2451
|
-
const runtime = useDmsRuntime();
|
|
2452
|
-
if (options.transfer) runtime.transferredState.add(key);
|
|
2453
|
-
if (!runtime.sharedState.has(key))
|
|
2454
|
-
runtime.sharedState.set(key, ref(init?.()));
|
|
2455
|
-
return runtime.sharedState.get(key) as Ref<T>;
|
|
2456
|
-
}
|
|
2457
|
-
export function useDmsRuntimeConfig(): DmsRuntimeConfig {
|
|
2458
|
-
return runtimeConfig.value;
|
|
2459
|
-
}
|
|
2460
|
-
export function useDmsAppConfig(): DmsAppConfig {
|
|
2461
|
-
return appConfig.value as DmsAppConfig;
|
|
2462
|
-
}
|
|
2463
|
-
export function defineDmsAppConfig<T extends Record<string, unknown>>(
|
|
2464
|
-
config: T,
|
|
2465
|
-
): T {
|
|
2466
|
-
return config;
|
|
2467
|
-
}
|
|
2468
|
-
export function defineDmsMiddleware(handler: DmsMiddleware): DmsMiddleware {
|
|
2469
|
-
return handler;
|
|
2470
|
-
}
|
|
2471
|
-
export function defineDmsPageMeta(meta: Record<string, unknown>): void {
|
|
2472
|
-
const runtime = useDmsRuntime();
|
|
2473
|
-
Object.assign(runtime.route.meta, meta);
|
|
2474
|
-
// A middleware that throws fails the page, as in Nuxt: the error page
|
|
2475
|
-
// shows in its place with the error's status code, which a server render
|
|
2476
|
-
// answers with. A redirect another navigation decided still wins.
|
|
2477
|
-
const navigation = runMiddleware(runtime, runtime.route)
|
|
2478
|
-
.then(async (result) => {
|
|
2479
|
-
if (result !== undefined && result !== false)
|
|
2480
|
-
await visit(runtime, result);
|
|
2481
|
-
})
|
|
2482
|
-
.catch((reason) => failPage(runtime, reason));
|
|
2483
|
-
runtime.pendingNavigation = runtime.pendingNavigation
|
|
2484
|
-
? Promise.all([runtime.pendingNavigation, navigation]).then(() => undefined)
|
|
2485
|
-
: navigation;
|
|
2486
|
-
void runtime.pendingNavigation;
|
|
2487
|
-
// A server renders the page's subtree once its middleware is done, so the
|
|
2488
|
-
// state a middleware sets is the one the HTML shows and the payload carries.
|
|
2489
|
-
// A failure shows the error page in its place (`failPage`). Registered
|
|
2490
|
-
// in the browser too, where only a server calls it: both sides must agree
|
|
2491
|
-
// on the async boundaries `useId()` counts.
|
|
2492
|
-
if (getCurrentInstance())
|
|
2493
|
-
onServerPrefetch(() => navigation.then(ignoreSettlement, ignoreSettlement));
|
|
2494
|
-
}
|
|
2495
|
-
function ignoreSettlement(): void {}
|
|
2496
|
-
/**
|
|
2497
|
-
* What Vue's development build passes `errorHandler` for an error thrown by a
|
|
2498
|
-
* component's `setup`, sync or async (production passes a link instead; it has
|
|
2499
|
-
* no warnings to scope either).
|
|
2500
|
-
*/
|
|
2501
|
-
const SETUP_FUNCTION_ERROR = "setup function";
|
|
2502
|
-
/**
|
|
2503
|
-
* Whether a Vue warning comes from a component whose `setup` failed, or from
|
|
2504
|
-
* under it. Vue still renders such a component, against the setup state that
|
|
2505
|
-
* never came: in development, where a single-file component's template is
|
|
2506
|
-
* compiled apart from its setup, the template reads the missing bindings, and
|
|
2507
|
-
* every component it uses becomes `undefined` ("Invalid vnode type when
|
|
2508
|
-
* creating vnode: undefined", then "Component <Anonymous> is missing template
|
|
2509
|
-
* or render function" for the comment Vue renders in its place). The error
|
|
2510
|
-
* page replaces that output (a server renders the page again, the browser
|
|
2511
|
-
* swaps it), and the failure itself is already reported.
|
|
2512
|
-
*
|
|
2513
|
-
* Walks internal instances: a `<script setup>` component exposes itself, so
|
|
2514
|
-
* its children's `$parent` is an expose proxy, not the proxy `errorHandler`
|
|
2515
|
-
* received.
|
|
2516
|
-
*/
|
|
2517
|
-
function withinFailedSetup(
|
|
2518
|
-
instance: ComponentInternalInstance | null,
|
|
2519
|
-
failedSetups: WeakSet<ComponentInternalInstance>,
|
|
2520
|
-
): boolean {
|
|
2521
|
-
for (let current = instance; current; current = current.parent)
|
|
2522
|
-
if (failedSetups.has(current)) return true;
|
|
2523
|
-
return false;
|
|
2524
|
-
}
|
|
2525
|
-
/** Shows the error page in place of the page, keeping the first failure. */
|
|
2526
|
-
function failPage(runtime: DmsFrontendRuntime, reason: unknown): void {
|
|
2527
|
-
runtime.currentError.value ??=
|
|
2528
|
-
reason instanceof Error ? reason : new Error(String(reason));
|
|
2529
|
-
}
|
|
2530
|
-
export function abortDmsNavigation(): false {
|
|
2531
|
-
return false;
|
|
2532
|
-
}
|
|
2533
|
-
export const useHead = useUnhead;
|
|
2534
|
-
export const useSeoMeta = useUnheadSeoMeta;
|
|
2535
|
-
export function useI18n(): DmsI18n {
|
|
2536
|
-
return useVueI18n() as DmsI18n;
|
|
2537
|
-
}
|
|
2538
|
-
|
|
2539
|
-
function shouldNavigateDmsLink(event: MouseEvent): boolean {
|
|
2540
|
-
const element = event.currentTarget;
|
|
2541
|
-
return !(
|
|
2542
|
-
event.defaultPrevented ||
|
|
2543
|
-
event.button !== 0 ||
|
|
2544
|
-
event.altKey ||
|
|
2545
|
-
event.ctrlKey ||
|
|
2546
|
-
event.metaKey ||
|
|
2547
|
-
event.shiftKey ||
|
|
2548
|
-
(element instanceof HTMLAnchorElement &&
|
|
2549
|
-
Boolean(element.target) &&
|
|
2550
|
-
element.target !== "_self")
|
|
2551
|
-
);
|
|
2552
|
-
}
|
|
2553
|
-
|
|
2554
|
-
function dmsLinkNavigationOptions(
|
|
2555
|
-
attributes: Record<string, unknown>,
|
|
2556
|
-
): DmsNavigationOptions {
|
|
2557
|
-
const options = attributes as DmsNavigationOptions;
|
|
2558
|
-
return {
|
|
2559
|
-
async: options.async,
|
|
2560
|
-
component: options.component,
|
|
2561
|
-
data: options.data,
|
|
2562
|
-
except: options.except,
|
|
2563
|
-
headers: options.headers,
|
|
2564
|
-
method: options.method,
|
|
2565
|
-
onBefore: options.onBefore,
|
|
2566
|
-
onCancel: options.onCancel,
|
|
2567
|
-
onCancelToken: options.onCancelToken,
|
|
2568
|
-
onError: options.onError,
|
|
2569
|
-
onFinish: options.onFinish,
|
|
2570
|
-
onProgress: options.onProgress,
|
|
2571
|
-
onStart: options.onStart,
|
|
2572
|
-
onSuccess: options.onSuccess,
|
|
2573
|
-
only: options.only,
|
|
2574
|
-
preserveScroll: options.preserveScroll,
|
|
2575
|
-
preserveState: options.preserveState,
|
|
2576
|
-
preserveUrl: options.preserveUrl,
|
|
2577
|
-
queryStringArrayFormat: options.queryStringArrayFormat,
|
|
2578
|
-
replace: options.replace,
|
|
2579
|
-
viewTransition: options.viewTransition,
|
|
2580
|
-
};
|
|
2581
|
-
}
|
|
2582
|
-
|
|
2583
|
-
export const DmsLink: Component = defineComponent({
|
|
2584
|
-
name: "DmsLink",
|
|
2585
|
-
inheritAttrs: false,
|
|
2586
|
-
props: {
|
|
2587
|
-
to: {
|
|
2588
|
-
type: [String, Object] as PropType<RouteLocationRaw>,
|
|
2589
|
-
required: true,
|
|
2590
|
-
},
|
|
2591
|
-
},
|
|
2592
|
-
setup(props, { attrs, slots }) {
|
|
2593
|
-
const runtime = useDmsRuntime();
|
|
2594
|
-
const prefetch = attrs.prefetch as DmsLinkPrefetch | undefined;
|
|
2595
|
-
const onClick = (event: MouseEvent) => {
|
|
2596
|
-
if (typeof attrs.onClick === "function") attrs.onClick(event);
|
|
2597
|
-
if (!shouldNavigateDmsLink(event)) return;
|
|
2598
|
-
event.preventDefault();
|
|
2599
|
-
void visit(runtime, props.to, dmsLinkNavigationOptions(attrs));
|
|
2600
|
-
};
|
|
2601
|
-
return (): ReturnType<typeof h> =>
|
|
2602
|
-
h(
|
|
2603
|
-
InertiaLink,
|
|
2604
|
-
{
|
|
2605
|
-
...attrs,
|
|
2606
|
-
href: locationToUrl(runtime, props.to),
|
|
2607
|
-
onClick,
|
|
2608
|
-
prefetch: prefetch ?? "hover",
|
|
2609
|
-
},
|
|
2610
|
-
slots,
|
|
2611
|
-
);
|
|
2612
|
-
},
|
|
2613
|
-
});
|
|
2614
|
-
|
|
2615
|
-
/** How a keyboard hint prints its modifiers: macOS symbols, or PC key names. */
|
|
2616
|
-
export type DmsKbdPlatform = "mac" | "other";
|
|
2617
|
-
/**
|
|
2618
|
-
* Where Nuxt UI's key hints read the platform: called in the setup of each
|
|
2619
|
-
* component that prints one, it returns the platform that component renders.
|
|
2620
|
-
* It must read on the server the platform it renders there, and keep it while
|
|
2621
|
-
* its caller hydrates, so the browser's first render matches the server's.
|
|
2622
|
-
*/
|
|
2623
|
-
export type DmsKbdPlatformSource = () => Readonly<Ref<DmsKbdPlatform>>;
|
|
2624
|
-
|
|
2625
|
-
const KBD_PLATFORM_KEY: InjectionKey<DmsKbdPlatformSource> =
|
|
2626
|
-
Symbol("dms:kbd-platform");
|
|
2627
|
-
const MODIFIER_KBD_KEYS: Record<
|
|
1
|
+
export type {
|
|
2
|
+
DmsPageRoute,
|
|
3
|
+
DmsPagePayload,
|
|
4
|
+
DmsSharedPagePayload,
|
|
5
|
+
DmsLayoutDefinition,
|
|
6
|
+
DmsPageLayout,
|
|
7
|
+
DmsPageProps,
|
|
8
|
+
PublicRuntimeConfig,
|
|
9
|
+
RuntimeConfig,
|
|
10
|
+
DmsAppConfig,
|
|
11
|
+
DmsUser,
|
|
12
|
+
DmsSession,
|
|
13
|
+
DmsRuntimeConfig,
|
|
14
|
+
DmsModuleOptions,
|
|
15
|
+
DmsLocaleLoader,
|
|
16
|
+
DmsNavigationOptions,
|
|
17
|
+
RouteLocationObject,
|
|
18
|
+
LocationQueryValue,
|
|
19
|
+
LocationQuery,
|
|
20
|
+
LocationQueryRaw,
|
|
21
|
+
RouteLocationRaw,
|
|
22
|
+
DmsRoute,
|
|
23
|
+
DmsRouter,
|
|
24
|
+
DmsAsyncDataStatus,
|
|
25
|
+
DmsAsyncDataDedupe,
|
|
26
|
+
DmsAsyncDataRefreshOptions,
|
|
27
|
+
DmsAsyncData,
|
|
28
|
+
DmsAsyncDataResult,
|
|
29
|
+
DmsAsyncDataContext,
|
|
30
|
+
DmsAsyncDataHandler,
|
|
31
|
+
DmsAsyncDataKey,
|
|
32
|
+
DmsData,
|
|
33
|
+
DmsAsyncDataTransformContext,
|
|
34
|
+
DmsFetchTransformContext,
|
|
35
|
+
UseAsyncDataOptions,
|
|
36
|
+
UseFetchOptions,
|
|
37
|
+
DmsErrorData,
|
|
38
|
+
DmsPayload,
|
|
39
|
+
DmsStateOptions,
|
|
40
|
+
DmsUserSession,
|
|
41
|
+
DmsCookieOptions,
|
|
42
|
+
DmsAppContext,
|
|
43
|
+
DmsLocale,
|
|
44
|
+
DmsI18n,
|
|
45
|
+
DmsPluginSetup,
|
|
46
|
+
DmsMiddlewareRegistrationOptions,
|
|
47
|
+
DmsPluginRegistrationOptions,
|
|
48
|
+
DmsComponentPreloader,
|
|
49
|
+
DmsComponentRegistrationOptions,
|
|
50
|
+
} from "./frontend-runtime/types";
|
|
51
|
+
export { withPublicOptions } from "./frontend-runtime/module-registration";
|
|
52
|
+
export type {
|
|
53
|
+
DmsFrontendSdk,
|
|
54
|
+
DmsFrontendModule,
|
|
55
|
+
DmsFrontendModuleRegistration,
|
|
56
|
+
DmsLayerAppConfig,
|
|
57
|
+
DmsMiddlewareResult,
|
|
58
|
+
DmsMiddleware,
|
|
59
|
+
DmsRuntimeHook,
|
|
60
|
+
DmsRuntimeHooks,
|
|
61
|
+
DmsFrontendRuntime,
|
|
62
|
+
DmsInertiaPage,
|
|
63
|
+
} from "./frontend-runtime/module-registration";
|
|
64
|
+
export {
|
|
65
|
+
createDmsFrontendRuntime,
|
|
66
|
+
provideDmsFrontendRuntime,
|
|
67
|
+
setDmsServerRuntimeResolver,
|
|
68
|
+
useDmsRuntimeHooks,
|
|
69
|
+
} from "./frontend-runtime/state";
|
|
70
|
+
export {
|
|
71
|
+
useDmsRoute,
|
|
72
|
+
useDmsInertiaPage,
|
|
73
|
+
useDmsRouter,
|
|
74
|
+
navigateDms,
|
|
75
|
+
} from "./frontend-runtime/route";
|
|
76
|
+
export { $fetch, dmsRequestKey, useDmsFetch } from "./frontend-runtime/fetch";
|
|
77
|
+
export type { DmsRequestKeyOptions } from "./frontend-runtime/fetch";
|
|
78
|
+
export { useDmsAsyncData } from "./frontend-runtime/async-data";
|
|
79
|
+
export { useDmsPagedData } from "./frontend-runtime/paged-data";
|
|
80
|
+
export type {
|
|
81
|
+
DmsPagedDataValue,
|
|
82
|
+
DmsPagedDataContext,
|
|
83
|
+
DmsPagedDataHandler,
|
|
84
|
+
UseDmsPagedDataOptions,
|
|
85
|
+
DmsPagedData,
|
|
86
|
+
DmsPagedDataResult,
|
|
87
|
+
} from "./frontend-runtime/paged-data";
|
|
88
|
+
export {
|
|
89
|
+
useDmsData,
|
|
90
|
+
refreshDmsData,
|
|
91
|
+
refreshDmsPageData,
|
|
92
|
+
setDmsAsyncDataScope,
|
|
93
|
+
serializeDmsAsyncData,
|
|
94
|
+
} from "./frontend-runtime/data-access";
|
|
95
|
+
export {
|
|
96
|
+
pageErrorData,
|
|
97
|
+
serializeDmsPayload,
|
|
98
|
+
restoreDmsPayload,
|
|
99
|
+
useDmsState,
|
|
100
|
+
useDmsRuntimeConfig,
|
|
101
|
+
useDmsAppConfig,
|
|
102
|
+
defineDmsAppConfig,
|
|
103
|
+
defineDmsMiddleware,
|
|
104
|
+
defineDmsPageMeta,
|
|
105
|
+
} from "./frontend-runtime/payload";
|
|
106
|
+
export {
|
|
107
|
+
abortDmsNavigation,
|
|
108
|
+
useHead,
|
|
109
|
+
useSeoMeta,
|
|
110
|
+
useI18n,
|
|
111
|
+
DmsLink,
|
|
112
|
+
provideDmsKbdPlatform,
|
|
113
|
+
useDmsKbd,
|
|
114
|
+
DmsClientOnly,
|
|
115
|
+
defineDmsPlugin,
|
|
116
|
+
useDmsApp,
|
|
117
|
+
} from "./frontend-runtime/ui";
|
|
118
|
+
export type {
|
|
2628
119
|
DmsKbdPlatform,
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
export
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
const { macOS } = shared();
|
|
2671
|
-
const instance = getCurrentInstance();
|
|
2672
|
-
const source = instance ? inject(KBD_PLATFORM_KEY, null) : null;
|
|
2673
|
-
const platform = source ? source() : null;
|
|
2674
|
-
// A hydrating component's vnode is bound to its server node before setup.
|
|
2675
|
-
const hydrating = ref(!!instance?.vnode.el);
|
|
2676
|
-
if (hydrating.value) onMounted(() => (hydrating.value = false));
|
|
2677
|
-
return {
|
|
2678
|
-
macOS,
|
|
2679
|
-
getKbdKey(value) {
|
|
2680
|
-
if (!value) return undefined;
|
|
2681
|
-
if (!isModifierKbdKey(value)) return keys[value] || value;
|
|
2682
|
-
if (platform) return MODIFIER_KBD_KEYS[platform.value][value];
|
|
2683
|
-
if (typeof window === "undefined" || hydrating.value) return " ";
|
|
2684
|
-
return MODIFIER_KBD_KEYS[macOS.value ? "mac" : "other"][value];
|
|
2685
|
-
},
|
|
2686
|
-
};
|
|
2687
|
-
}
|
|
2688
|
-
|
|
2689
|
-
export const DmsClientOnly = defineComponent({
|
|
2690
|
-
name: "DmsClientOnly",
|
|
2691
|
-
setup(_, { slots }) {
|
|
2692
|
-
const isMounted = ref(false);
|
|
2693
|
-
onMounted(() => {
|
|
2694
|
-
isMounted.value = true;
|
|
2695
|
-
});
|
|
2696
|
-
return () =>
|
|
2697
|
-
isMounted.value ? slots.default?.() : (slots.fallback?.() ?? null);
|
|
2698
|
-
},
|
|
2699
|
-
});
|
|
2700
|
-
|
|
2701
|
-
export function defineDmsPlugin(setup: DmsPluginSetup): DmsPluginSetup {
|
|
2702
|
-
return setup;
|
|
2703
|
-
}
|
|
2704
|
-
export function useDmsApp(): DmsAppContext {
|
|
2705
|
-
const instance = getCurrentInstance();
|
|
2706
|
-
const contextual = instance?.appContext.config.globalProperties
|
|
2707
|
-
.$dms as DmsAppContext;
|
|
2708
|
-
return contextual ?? (useDmsRuntime().appContext as DmsAppContext);
|
|
2709
|
-
}
|
|
2710
|
-
|
|
2711
|
-
const COOKIE_STATE_PREFIX = "dms-cookie:";
|
|
2712
|
-
// A browser tab runs one application, so its cookie refs are shared by every
|
|
2713
|
-
// caller; a server shares them within the request it renders instead.
|
|
2714
|
-
const browserCookieRefs = new Map<string, Ref<unknown>>();
|
|
2715
|
-
|
|
2716
|
-
function readCookie(
|
|
2717
|
-
source: string | undefined,
|
|
2718
|
-
name: string,
|
|
2719
|
-
): string | undefined {
|
|
2720
|
-
const match = source
|
|
2721
|
-
?.split(/;\s*/)
|
|
2722
|
-
.find((entry) => entry.startsWith(`${name}=`));
|
|
2723
|
-
return match ? decodeURIComponent(match.slice(name.length + 1)) : undefined;
|
|
2724
|
-
}
|
|
2725
|
-
|
|
2726
|
-
function parseCookieValue<T>(
|
|
2727
|
-
stored: string | undefined,
|
|
2728
|
-
options: DmsCookieOptions<T>,
|
|
2729
|
-
): T {
|
|
2730
|
-
if (stored === undefined) return (options.default?.() ?? null) as T;
|
|
2731
|
-
try {
|
|
2732
|
-
return JSON.parse(stored) as T;
|
|
2733
|
-
} catch {
|
|
2734
|
-
return stored as T;
|
|
2735
|
-
}
|
|
2736
|
-
}
|
|
2737
|
-
|
|
2738
|
-
function writeCookie<T>(name: string, next: T, options: DmsCookieOptions<T>) {
|
|
2739
|
-
const attributes = [
|
|
2740
|
-
`path=${options.path ?? "/"}`,
|
|
2741
|
-
options.maxAge === undefined ? "" : `max-age=${options.maxAge}`,
|
|
2742
|
-
options.sameSite ? `samesite=${options.sameSite}` : "",
|
|
2743
|
-
options.secure ? "secure" : "",
|
|
2744
|
-
].filter(Boolean);
|
|
2745
|
-
// biome-ignore lint/suspicious/noDocumentCookie: reactive cookie refs require synchronous writes.
|
|
2746
|
-
document.cookie = `${name}=${encodeURIComponent(JSON.stringify(next ?? null))}; ${attributes.join("; ")}`;
|
|
2747
|
-
}
|
|
2748
|
-
|
|
2749
|
-
function createCookieRef<T>(
|
|
2750
|
-
name: string,
|
|
2751
|
-
source: string | undefined,
|
|
2752
|
-
options: DmsCookieOptions<T>,
|
|
2753
|
-
isServer: boolean,
|
|
2754
|
-
): Ref<T> {
|
|
2755
|
-
const value = ref(
|
|
2756
|
-
parseCookieValue(readCookie(source, name), options),
|
|
2757
|
-
) as Ref<T>;
|
|
2758
|
-
return computed({
|
|
2759
|
-
get: () => value.value,
|
|
2760
|
-
set: (next) => {
|
|
2761
|
-
value.value = next;
|
|
2762
|
-
if (!isServer) writeCookie(name, next, options);
|
|
2763
|
-
},
|
|
2764
|
-
});
|
|
2765
|
-
}
|
|
2766
|
-
|
|
2767
|
-
// The language a visitor picks, kept by the browser so that a reload renders
|
|
2768
|
-
// in it. A signed-in user's own language still comes first.
|
|
2769
|
-
const LOCALE_COOKIE = "dms_locale";
|
|
2770
|
-
const LOCALE_COOKIE_MAX_AGE = 60 * 60 * 24 * 365;
|
|
2771
|
-
|
|
2772
|
-
/**
|
|
2773
|
-
* The language this browser picked: read from `document.cookie`, or in a
|
|
2774
|
-
* server render from the request. Only one of `supportedLocales` counts.
|
|
2775
|
-
*/
|
|
2776
|
-
export function readDmsLocaleCookie(
|
|
2777
|
-
runtime: DmsFrontendRuntime,
|
|
2778
|
-
supportedLocales: string[],
|
|
2779
|
-
): string | undefined {
|
|
2780
|
-
const source =
|
|
2781
|
-
typeof document === "undefined" ? runtime.requestCookies : document.cookie;
|
|
2782
|
-
try {
|
|
2783
|
-
const locale = parseCookieValue<unknown>(
|
|
2784
|
-
readCookie(source, LOCALE_COOKIE),
|
|
2785
|
-
{},
|
|
2786
|
-
);
|
|
2787
|
-
return typeof locale === "string" && supportedLocales.includes(locale)
|
|
2788
|
-
? locale
|
|
2789
|
-
: undefined;
|
|
2790
|
-
} catch {
|
|
2791
|
-
// A value that is not URI-encoded: as if the browser had picked none.
|
|
2792
|
-
return undefined;
|
|
2793
|
-
}
|
|
2794
|
-
}
|
|
2795
|
-
|
|
2796
|
-
/**
|
|
2797
|
-
* A cookie as a ref, shared by every caller of the same name — so a plugin and
|
|
2798
|
-
* a page reading one preference see each other's writes — and read on the
|
|
2799
|
-
* server from the request being rendered, so the server HTML matches the client.
|
|
2800
|
-
*/
|
|
2801
|
-
export function useDmsCookie<T = string | null>(
|
|
2802
|
-
name: string,
|
|
2803
|
-
options: DmsCookieOptions<T> = {},
|
|
2804
|
-
): Ref<T> {
|
|
2805
|
-
if (typeof document !== "undefined") {
|
|
2806
|
-
if (!browserCookieRefs.has(name))
|
|
2807
|
-
browserCookieRefs.set(
|
|
2808
|
-
name,
|
|
2809
|
-
createCookieRef(name, document.cookie, options, false) as Ref<unknown>,
|
|
2810
|
-
);
|
|
2811
|
-
return browserCookieRefs.get(name) as Ref<T>;
|
|
2812
|
-
}
|
|
2813
|
-
const runtime = useDmsRuntime();
|
|
2814
|
-
const key = `${COOKIE_STATE_PREFIX}${name}`;
|
|
2815
|
-
if (!runtime.sharedState.has(key))
|
|
2816
|
-
runtime.sharedState.set(
|
|
2817
|
-
key,
|
|
2818
|
-
createCookieRef(
|
|
2819
|
-
name,
|
|
2820
|
-
runtime.requestCookies,
|
|
2821
|
-
options,
|
|
2822
|
-
true,
|
|
2823
|
-
) as Ref<unknown>,
|
|
2824
|
-
);
|
|
2825
|
-
return runtime.sharedState.get(key) as Ref<T>;
|
|
2826
|
-
}
|
|
2827
|
-
|
|
2828
|
-
export function createDmsError(
|
|
2829
|
-
input: string | DmsErrorData,
|
|
2830
|
-
): Error & DmsErrorData {
|
|
2831
|
-
const details = typeof input === "string" ? { message: input } : input;
|
|
2832
|
-
return Object.assign(
|
|
2833
|
-
new Error(details.message ?? details.statusMessage),
|
|
2834
|
-
details,
|
|
2835
|
-
);
|
|
2836
|
-
}
|
|
2837
|
-
export function showDmsError(
|
|
2838
|
-
input: string | DmsErrorData,
|
|
2839
|
-
): Error & DmsErrorData {
|
|
2840
|
-
const error = createDmsError(input);
|
|
2841
|
-
useDmsRuntime().currentError.value = error;
|
|
2842
|
-
return error;
|
|
2843
|
-
}
|
|
2844
|
-
export function useDmsError(): Ref<(Error & DmsErrorData) | null> {
|
|
2845
|
-
return useDmsRuntime().currentError as Ref<(Error & DmsErrorData) | null>;
|
|
2846
|
-
}
|
|
2847
|
-
export async function clearDmsError(options?: {
|
|
2848
|
-
redirect?: string;
|
|
2849
|
-
}): Promise<void> {
|
|
2850
|
-
useDmsRuntime().currentError.value = null;
|
|
2851
|
-
if (options?.redirect) await navigateDms(options.redirect);
|
|
2852
|
-
}
|
|
2853
|
-
|
|
2854
|
-
function sessionIdentity(session: unknown): string {
|
|
2855
|
-
const { accountId, activeTenantId } = (session ?? {}) as Partial<DmsSession>;
|
|
2856
|
-
return `${accountId ?? ""}\n${activeTenantId ?? ""}`;
|
|
2857
|
-
}
|
|
2858
|
-
|
|
2859
|
-
/**
|
|
2860
|
-
* Drops the pages Inertia prefetched (a hovered link) once the session is
|
|
2861
|
-
* another account's or workspace's, or none: each carries the user, the page
|
|
2862
|
-
* and the site layout of the session it was fetched with, and a visit would
|
|
2863
|
-
* show them as current.
|
|
2864
|
-
*/
|
|
2865
|
-
function forgetPrefetchedPages(): void {
|
|
2866
|
-
if (typeof window !== "undefined") inertiaRouter.flushAll();
|
|
2867
|
-
}
|
|
2868
|
-
|
|
2869
|
-
/**
|
|
2870
|
-
* Warms what a prefetched page renders (its page, layout and block
|
|
2871
|
-
* components) once Inertia has its props, so following the link does not wait
|
|
2872
|
-
* for those chunks after the props.
|
|
2873
|
-
*/
|
|
2874
|
-
export function preloadPrefetchedDmsPages(): () => void {
|
|
2875
|
-
return inertiaRouter.on("prefetched", (event) => {
|
|
2876
|
-
const data = event.detail.response.data as unknown;
|
|
2877
|
-
let page: { props?: DmsPageProps } | undefined;
|
|
2878
|
-
try {
|
|
2879
|
-
page = (typeof data === "string" ? JSON.parse(data) : data) as
|
|
2880
|
-
| { props?: DmsPageProps }
|
|
2881
|
-
| undefined;
|
|
2882
|
-
} catch {
|
|
2883
|
-
return;
|
|
2884
|
-
}
|
|
2885
|
-
if (page?.props?.page) void preloadDmsPage(page.props).catch(() => {});
|
|
2886
|
-
});
|
|
2887
|
-
}
|
|
2888
|
-
|
|
2889
|
-
export function useDmsUserSession<
|
|
2890
|
-
TUser = DmsUser,
|
|
2891
|
-
TSession = DmsSession,
|
|
2892
|
-
>(): DmsUserSession<TUser, TSession> {
|
|
2893
|
-
const user = useDmsState<TUser | null>("dms-user", () => null);
|
|
2894
|
-
const session = useDmsState<TSession | null>("dms-session", () => null);
|
|
2895
|
-
return {
|
|
2896
|
-
user,
|
|
2897
|
-
session,
|
|
2898
|
-
loggedIn: computed(() => user.value !== null),
|
|
2899
|
-
fetch: async () => {
|
|
2900
|
-
const before = sessionIdentity(session.value);
|
|
2901
|
-
const value = await ofetch<{
|
|
2902
|
-
user?: TUser;
|
|
2903
|
-
session?: TSession;
|
|
2904
|
-
}>("/api/_auth/session", { method: "POST" });
|
|
2905
|
-
user.value = value.user ?? null;
|
|
2906
|
-
session.value = value.session ?? null;
|
|
2907
|
-
if (sessionIdentity(session.value) !== before) forgetPrefetchedPages();
|
|
2908
|
-
},
|
|
2909
|
-
clear: async () => {
|
|
2910
|
-
await ofetch("/api/_auth/session", { method: "DELETE" });
|
|
2911
|
-
user.value = null;
|
|
2912
|
-
session.value = null;
|
|
2913
|
-
forgetPrefetchedPages();
|
|
2914
|
-
},
|
|
2915
|
-
};
|
|
2916
|
-
}
|
|
2917
|
-
|
|
2918
|
-
/**
|
|
2919
|
-
* Whether the page loaded by the current navigation is still pending: its
|
|
2920
|
-
* chunk, or an async `setup` it awaits, has not resolved yet. A layout reads
|
|
2921
|
-
* it to show its own placeholder in place of the page. Always `false` on the
|
|
2922
|
-
* server and during hydration, which render the page resolved.
|
|
2923
|
-
*/
|
|
2924
|
-
export function useDmsPageLoading(): Readonly<Ref<boolean>> {
|
|
2925
|
-
return readonly(useDmsRuntime().pageLoading);
|
|
2926
|
-
}
|
|
2927
|
-
|
|
2928
|
-
/**
|
|
2929
|
-
* Whether the browser is hydrating the page the server rendered, whose first
|
|
2930
|
-
* render must match the server's HTML: from the start of the application to
|
|
2931
|
-
* the tick the page's `<Suspense>` resolves. False on the server, in a page
|
|
2932
|
-
* the server did not render, and after a client navigation. As Nuxt's
|
|
2933
|
-
* `nuxtApp.isHydrating`; read once, it is not reactive.
|
|
2934
|
-
*/
|
|
2935
|
-
export function isDmsHydrating(): boolean {
|
|
2936
|
-
return useDmsRuntime().hydrating;
|
|
2937
|
-
}
|
|
2938
|
-
|
|
2939
|
-
/**
|
|
2940
|
-
* The props of the `<Suspense>` around the page that keep `useDmsPageLoading`
|
|
2941
|
-
* in step with it. Cleared before the boundary unmounts, so a page left while
|
|
2942
|
-
* pending never leaves the state set.
|
|
2943
|
-
*
|
|
2944
|
-
* They also end the hydration: the page is on screen once its suspense
|
|
2945
|
-
* resolves, and the components after it once the tick is over, so the
|
|
2946
|
-
* browser has then hydrated what the server rendered. A hydrating suspense
|
|
2947
|
-
* never reports itself pending: the server rendered the page resolved.
|
|
2948
|
-
*/
|
|
2949
|
-
export function dmsPageSuspenseProps(): Record<string, () => void> {
|
|
2950
|
-
const runtime = useDmsRuntime();
|
|
2951
|
-
const loading = runtime.pageLoading;
|
|
2952
|
-
const settle = () => {
|
|
2953
|
-
loading.value = false;
|
|
2954
|
-
};
|
|
2955
|
-
return {
|
|
2956
|
-
onPending: () => {
|
|
2957
|
-
loading.value = true;
|
|
2958
|
-
},
|
|
2959
|
-
onResolve: () => {
|
|
2960
|
-
settle();
|
|
2961
|
-
if (runtime.hydrating) void nextTick(() => endHydration(runtime));
|
|
2962
|
-
},
|
|
2963
|
-
onVnodeBeforeUnmount: settle,
|
|
2964
|
-
};
|
|
2965
|
-
}
|
|
2966
|
-
|
|
2967
|
-
/**
|
|
2968
|
-
* The key a component name is registered and resolved under: case, a leading
|
|
2969
|
-
* `lazy` and every non-alphanumeric character ignored, so PascalCase,
|
|
2970
|
-
* kebab-case and lowercase spellings match. Unlike `normalizeDmsName`, a
|
|
2971
|
-
* leading `Dms` is kept: it is the DMS module's prefix, and dropping it would
|
|
2972
|
-
* make `DmsBanner` and another module's `Banner` the same component.
|
|
2973
|
-
*/
|
|
2974
|
-
function normalizeDmsComponentName(name: string): string {
|
|
2975
|
-
return name
|
|
2976
|
-
.replace(/^lazy/i, "")
|
|
2977
|
-
.replace(/[^a-z0-9]/gi, "")
|
|
2978
|
-
.toLowerCase();
|
|
2979
|
-
}
|
|
2980
|
-
|
|
2981
|
-
function moduleLabel(module: string | undefined): string {
|
|
2982
|
-
return module ? `module "${module}"` : "an unnamed module";
|
|
2983
|
-
}
|
|
2984
|
-
|
|
2985
|
-
/**
|
|
2986
|
-
* A component registered under a name already taken is ignored: the first
|
|
2987
|
-
* registration, from the module of higher priority, keeps the name. That is
|
|
2988
|
-
* how a module replaces another's component, by registering its full name
|
|
2989
|
-
* without a prefix, and only a collision neither side asked for is reported.
|
|
2990
|
-
*/
|
|
2991
|
-
function registerDmsComponentEntry(
|
|
2992
|
-
entries: Map<string, DmsComponentRegistration>,
|
|
2993
|
-
registration: DmsComponentRegistration,
|
|
2994
|
-
): void {
|
|
2995
|
-
const key = normalizeDmsComponentName(registration.name);
|
|
2996
|
-
const existing = entries.get(key);
|
|
2997
|
-
if (!existing) {
|
|
2998
|
-
entries.set(key, registration);
|
|
2999
|
-
return;
|
|
3000
|
-
}
|
|
3001
|
-
if (existing.unprefixed || registration.unprefixed) return;
|
|
3002
|
-
console.warn(
|
|
3003
|
-
`[dms] Component "${registration.name}" from ${moduleLabel(registration.module)} is ignored: ` +
|
|
3004
|
-
`${moduleLabel(existing.module)} already registers "${existing.name}". ` +
|
|
3005
|
-
"Give the modules different component prefixes.",
|
|
3006
|
-
);
|
|
3007
|
-
}
|
|
3008
|
-
/** Registers a component unless a higher-priority module already owns its name. */
|
|
3009
|
-
export function registerDmsComponent(name: string, component: Component): void {
|
|
3010
|
-
registerDmsComponentEntry(registry.components, { name, component });
|
|
3011
|
-
}
|
|
3012
|
-
/**
|
|
3013
|
-
* Resolves a registered component by its full name, prefix included. Case,
|
|
3014
|
-
* a leading `lazy` and non-alphanumeric characters are ignored.
|
|
3015
|
-
*/
|
|
3016
|
-
export function resolveDmsComponent(name: string): Component | undefined {
|
|
3017
|
-
return registry.components.get(normalizeDmsComponentName(name))?.component;
|
|
3018
|
-
}
|
|
3019
|
-
|
|
3020
|
-
/**
|
|
3021
|
-
* Records, by registered name, every registered async component a server
|
|
3022
|
-
* render reaches, so the client can resolve the same ones before hydrating.
|
|
3023
|
-
* Inertia re-renders the tree once on boot; an async component still loading
|
|
3024
|
-
* at that moment makes Vue drop its server-rendered markup ("Skipping lazy
|
|
3025
|
-
* hydration") and leave it empty until its chunk arrives.
|
|
3026
|
-
*/
|
|
3027
|
-
export function trackDmsAsyncComponents(app: App): Set<string> {
|
|
3028
|
-
const names = new Map<Component, string>();
|
|
3029
|
-
registry.components.forEach(({ component, name }) => {
|
|
3030
|
-
if ((component as DmsAsyncComponent).__asyncLoader)
|
|
3031
|
-
names.set(component, name);
|
|
3032
|
-
});
|
|
3033
|
-
const rendered = new Set<string>();
|
|
3034
|
-
app.mixin({
|
|
3035
|
-
beforeCreate(this: ComponentPublicInstance) {
|
|
3036
|
-
const name = names.get(this.$.type as Component);
|
|
3037
|
-
if (name) rendered.add(name);
|
|
3038
|
-
},
|
|
3039
|
-
});
|
|
3040
|
-
return rendered;
|
|
3041
|
-
}
|
|
3042
|
-
|
|
3043
|
-
/**
|
|
3044
|
-
* Records the scope of every component a server render creates, and returns
|
|
3045
|
-
* the function that stops them once the render is over.
|
|
3046
|
-
*
|
|
3047
|
-
* A server app is never mounted, so it is never unmounted either: Vue leaves
|
|
3048
|
-
* the scope of each component it rendered running, and nothing handed to
|
|
3049
|
-
* `onScopeDispose` is ever released. Nuxt UI's `useRuntimeHook` registers in
|
|
3050
|
-
* a process-wide hook registry and only unregisters there, so every server
|
|
3051
|
-
* render of the dashboard sidebar stayed reachable, its whole component tree
|
|
3052
|
-
* and page props with it, until the server ran out of heap.
|
|
3053
|
-
*
|
|
3054
|
-
* The component's scope is read through `getCurrentScope`: Vue makes it the
|
|
3055
|
-
* active scope before it calls `beforeCreate`, and the instance's own `scope`
|
|
3056
|
-
* field is internal, absent from the typings a generated workspace checks.
|
|
3057
|
-
*/
|
|
3058
|
-
export function trackDmsServerScopes(app: App): () => void {
|
|
3059
|
-
const scopes: EffectScope[] = [];
|
|
3060
|
-
app.mixin({
|
|
3061
|
-
beforeCreate() {
|
|
3062
|
-
const scope = getCurrentScope();
|
|
3063
|
-
if (scope) scopes.push(scope);
|
|
3064
|
-
},
|
|
3065
|
-
});
|
|
3066
|
-
return () => {
|
|
3067
|
-
for (const scope of scopes.splice(0)) scope.stop();
|
|
3068
|
-
};
|
|
3069
|
-
}
|
|
3070
|
-
|
|
3071
|
-
export async function resolveDmsAsyncComponents(
|
|
3072
|
-
names: readonly string[],
|
|
3073
|
-
): Promise<void> {
|
|
3074
|
-
await Promise.all(
|
|
3075
|
-
names.map((name) =>
|
|
3076
|
-
(
|
|
3077
|
-
resolveDmsComponent(name) as DmsAsyncComponent | undefined
|
|
3078
|
-
)?.__asyncLoader?.(),
|
|
3079
|
-
),
|
|
3080
|
-
);
|
|
3081
|
-
}
|
|
3082
|
-
export async function preloadDmsComponents(names: string[]): Promise<void> {
|
|
3083
|
-
const registry = useDmsRuntime().appContext?.vueApp._context.components ?? {};
|
|
3084
|
-
await Promise.all(
|
|
3085
|
-
names.map((name) =>
|
|
3086
|
-
(registry[name] as DmsAsyncComponent | undefined)?.__asyncLoader?.(),
|
|
3087
|
-
),
|
|
3088
|
-
);
|
|
3089
|
-
}
|
|
3090
|
-
export function normalizeDmsName(name: string): string {
|
|
3091
|
-
return name
|
|
3092
|
-
.replace(/^lazy/i, "")
|
|
3093
|
-
.replace(/^dms[-_]?/i, "")
|
|
3094
|
-
.replace(/[^a-z0-9]/gi, "")
|
|
3095
|
-
.toLowerCase();
|
|
3096
|
-
}
|
|
3097
|
-
|
|
3098
|
-
function collectRenderedComponentNames(
|
|
3099
|
-
value: unknown,
|
|
3100
|
-
names: Set<string>,
|
|
3101
|
-
): void {
|
|
3102
|
-
if (!value || typeof value !== "object") return;
|
|
3103
|
-
if (Array.isArray(value)) {
|
|
3104
|
-
value.forEach((entry) => {
|
|
3105
|
-
collectRenderedComponentNames(entry, names);
|
|
3106
|
-
});
|
|
3107
|
-
return;
|
|
3108
|
-
}
|
|
3109
|
-
const definition = value as Record<string, unknown>;
|
|
3110
|
-
for (const key of ["component", "componentName"]) {
|
|
3111
|
-
if (typeof definition[key] === "string") names.add(definition[key]);
|
|
3112
|
-
}
|
|
3113
|
-
// A child places a component definition: `{ id, component: { … } }`.
|
|
3114
|
-
if (typeof definition.component === "object")
|
|
3115
|
-
collectRenderedComponentNames(definition.component, names);
|
|
3116
|
-
collectRenderedComponentNames(definition.children, names);
|
|
3117
|
-
}
|
|
3118
|
-
|
|
3119
|
-
async function preloadDmsLayoutComponents(
|
|
3120
|
-
layout: DmsPageLayout | undefined,
|
|
3121
|
-
): Promise<void> {
|
|
3122
|
-
const names = new Set<string>();
|
|
3123
|
-
collectRenderedComponentNames(layout?.layout, names);
|
|
3124
|
-
Object.values(layout?.components ?? {}).forEach((component) => {
|
|
3125
|
-
collectRenderedComponentNames(component, names);
|
|
3126
|
-
});
|
|
3127
|
-
await Promise.all(
|
|
3128
|
-
[...names].map((name) => {
|
|
3129
|
-
const component = resolveDmsComponent(name) as
|
|
3130
|
-
| DmsAsyncComponent
|
|
3131
|
-
| undefined;
|
|
3132
|
-
return component?.__asyncLoader?.();
|
|
3133
|
-
}),
|
|
3134
|
-
);
|
|
3135
|
-
}
|
|
3136
|
-
|
|
3137
|
-
export function normalizeDmsPageKey(name: string): string {
|
|
3138
|
-
return name.split(/[?#]/, 1)[0].replace(/^\/+|\/+$/g, "");
|
|
3139
|
-
}
|
|
3140
|
-
const CATCH_ALL_PAGE_KEY = "[...slug]";
|
|
3141
|
-
|
|
3142
|
-
function findDmsPageEntry(props: DmsPageProps): DmsFrontendEntry | undefined {
|
|
3143
|
-
const name = [
|
|
3144
|
-
props.page.componentName,
|
|
3145
|
-
props.page.route?.fullSlug,
|
|
3146
|
-
props.path,
|
|
3147
|
-
].find(
|
|
3148
|
-
(candidate) =>
|
|
3149
|
-
candidate && registry.pages.has(normalizeDmsPageKey(candidate)),
|
|
3150
|
-
);
|
|
3151
|
-
return name
|
|
3152
|
-
? registry.pages.get(normalizeDmsPageKey(name))
|
|
3153
|
-
: registry.dynamicPages.get(CATCH_ALL_PAGE_KEY);
|
|
3154
|
-
}
|
|
3155
|
-
|
|
3156
|
-
/** The path a frontend module registered for a typed backend refusal. */
|
|
3157
|
-
export function resolveDmsAccessRedirect(code: string): string | undefined {
|
|
3158
|
-
return registry.accessRedirects.get(code);
|
|
3159
|
-
}
|
|
3160
|
-
export function hasDmsPage(name: string): boolean {
|
|
3161
|
-
return registry.pages.has(normalizeDmsPageKey(name));
|
|
3162
|
-
}
|
|
3163
|
-
export function getDmsPage(props: DmsPageProps): Component | undefined {
|
|
3164
|
-
if (props.error) return registry.errorPage?.component;
|
|
3165
|
-
return findDmsPageEntry(props)?.component;
|
|
3166
|
-
}
|
|
3167
|
-
export function getDmsDynamicPage(name = "default"): Component | undefined {
|
|
3168
|
-
return registry.dynamicPages.get(name)?.component;
|
|
3169
|
-
}
|
|
3170
|
-
export function getDmsLayout(props: DmsPageProps): Component | undefined {
|
|
3171
|
-
const layout = props.page.layout;
|
|
3172
|
-
const name = layout?.layout?.componentName ?? layout?.componentName;
|
|
3173
|
-
return name
|
|
3174
|
-
? registry.layouts.get(normalizeDmsName(name))?.component
|
|
3175
|
-
: undefined;
|
|
3176
|
-
}
|
|
3177
|
-
export function getDmsLayoutProps(
|
|
3178
|
-
props: DmsPageProps,
|
|
3179
|
-
): Record<string, unknown> {
|
|
3180
|
-
const definition = props.page.layout?.layout;
|
|
3181
|
-
return {
|
|
3182
|
-
...(definition?.options ?? {}),
|
|
3183
|
-
icon: props.page.route?.icon,
|
|
3184
|
-
title: props.page.route?.displayName,
|
|
3185
|
-
description: props.page.route?.description,
|
|
3186
|
-
};
|
|
3187
|
-
}
|
|
3188
|
-
|
|
3189
|
-
/**
|
|
3190
|
-
* Whether a page carries an older session of the same account than the one
|
|
3191
|
-
* the app holds: Inertia restores a page from history, or serves it from the
|
|
3192
|
-
* prefetch cache, with the props it was fetched with, and those must not undo
|
|
3193
|
-
* a later `useDmsUserSession().fetch()`.
|
|
3194
|
-
*/
|
|
3195
|
-
function isStaleSession(
|
|
3196
|
-
incoming: DmsSession | undefined,
|
|
3197
|
-
current: DmsSession | null,
|
|
3198
|
-
): boolean {
|
|
3199
|
-
return (
|
|
3200
|
-
!!incoming &&
|
|
3201
|
-
!!current &&
|
|
3202
|
-
incoming.accountId === current.accountId &&
|
|
3203
|
-
(incoming.updatedAt ?? 0) < (current.updatedAt ?? 0)
|
|
3204
|
-
);
|
|
3205
|
-
}
|
|
3206
|
-
|
|
3207
|
-
/**
|
|
3208
|
-
* Copies the site layout of the page props into the state. A visit carries
|
|
3209
|
-
* the layout it was sent over from page to page (a once prop): the same copy
|
|
3210
|
-
* is applied once, so the state keeps what a later refresh wrote into it, and
|
|
3211
|
-
* `dms-lastRefreshTime` tells when the layout was last received.
|
|
3212
|
-
*/
|
|
3213
|
-
export function hydrateDmsSharedProps(
|
|
3214
|
-
shared: DmsSharedPagePayload | undefined,
|
|
3215
|
-
): void {
|
|
3216
|
-
const runtime = useDmsRuntime();
|
|
3217
|
-
if (!shared || shared === runtime.appliedShared) return;
|
|
3218
|
-
// A page from the history carries the layout it was shown with, which the
|
|
3219
|
-
// state has replaced since: see `keepSharedOverHistory`.
|
|
3220
|
-
if (runtime.restoringHistory) return;
|
|
3221
|
-
runtime.appliedShared = shared;
|
|
3222
|
-
useDmsState<unknown>("dms-siteLayout", () => undefined).value =
|
|
3223
|
-
shared.siteLayout;
|
|
3224
|
-
useDmsState<unknown>("dms-pageTree", () => undefined).value =
|
|
3225
|
-
shared.siteLayoutTree;
|
|
3226
|
-
useDmsState<unknown>("dms-quickActions", () => undefined).value =
|
|
3227
|
-
shared.quickActions;
|
|
3228
|
-
useDmsState<unknown>("dms-modules", () => undefined).value = shared.modules;
|
|
3229
|
-
useDmsState("dms-isOwner", () => false).value = shared.isOwner ?? false;
|
|
3230
|
-
useDmsState("dms-lastRefreshTime", () => 0).value = Date.now();
|
|
3231
|
-
}
|
|
3232
|
-
|
|
3233
|
-
/**
|
|
3234
|
-
* Keeps the newest site layout when Back or Forward puts a page back. The
|
|
3235
|
-
* page carries the layout it was shown with, which a later reload replaced
|
|
3236
|
-
* (a menu change the backend announced); it is not copied into the state, and
|
|
3237
|
-
* the page is given the current one, so the next visit carries that over
|
|
3238
|
-
* rather than the older one.
|
|
3239
|
-
*/
|
|
3240
|
-
export function keepSharedOverHistory(): () => void {
|
|
3241
|
-
const runtime = useDmsRuntime();
|
|
3242
|
-
const onPopstate = (event: PopStateEvent) => {
|
|
3243
|
-
// Inertia restores only the entries it wrote, which hold a page.
|
|
3244
|
-
if ((event.state as { page?: unknown } | null)?.page)
|
|
3245
|
-
runtime.restoringHistory = true;
|
|
3246
|
-
};
|
|
3247
|
-
window.addEventListener("popstate", onPopstate);
|
|
3248
|
-
const stopListening = inertiaRouter.on("navigate", (event) => {
|
|
3249
|
-
if (!runtime.restoringHistory) return;
|
|
3250
|
-
runtime.restoringHistory = false;
|
|
3251
|
-
const current = runtime.appliedShared;
|
|
3252
|
-
const restored = (event.detail.page.props as Partial<DmsPageProps>).shared;
|
|
3253
|
-
if (current && restored !== current)
|
|
3254
|
-
inertiaRouter.replaceProp("shared", current);
|
|
3255
|
-
});
|
|
3256
|
-
return () => {
|
|
3257
|
-
window.removeEventListener("popstate", onPopstate);
|
|
3258
|
-
stopListening();
|
|
3259
|
-
};
|
|
3260
|
-
}
|
|
3261
|
-
|
|
3262
|
-
export function hydrateDmsPageProps(props: DmsPageProps, url?: string): void {
|
|
3263
|
-
const runtime = useDmsRuntime();
|
|
3264
|
-
if (url) updateRoute(runtime, url);
|
|
3265
|
-
// Pages are keyed by path: another path mounts another page, which declares
|
|
3266
|
-
// its own meta, and so does an error page shown in place of the page. A
|
|
3267
|
-
// visit that only changes the query keeps the page, and so its meta.
|
|
3268
|
-
const pageKey = props.error ? `error:${props.path}` : props.path;
|
|
3269
|
-
if (pageKey !== runtime.pageKey) {
|
|
3270
|
-
runtime.pageKey = pageKey;
|
|
3271
|
-
runtime.route.meta = {};
|
|
3272
|
-
}
|
|
3273
|
-
dropUnusedAsyncData(runtime);
|
|
3274
|
-
runtime.currentError.value = null;
|
|
3275
|
-
const session = useDmsState<DmsSession | null>("dms-session", () => null);
|
|
3276
|
-
if (!isStaleSession(props.session, session.value)) {
|
|
3277
|
-
useDmsState<DmsUser | null>("dms-user", () => null).value =
|
|
3278
|
-
props.user ?? null;
|
|
3279
|
-
session.value = props.session ?? null;
|
|
3280
|
-
}
|
|
3281
|
-
hydrateDmsSharedProps(props.shared);
|
|
3282
|
-
const layoutUrl = props.page.route?.layoutUrl ?? props.page.layoutUrl;
|
|
3283
|
-
const pageLayouts = useDmsState<Record<string, DmsPageLayout>>(
|
|
3284
|
-
"dms-pageLayouts",
|
|
3285
|
-
() => ({}),
|
|
3286
|
-
);
|
|
3287
|
-
// Each navigation is a new authorization snapshot. Never retain layouts
|
|
3288
|
-
// from an earlier account, tenant, or permission context.
|
|
3289
|
-
pageLayouts.value =
|
|
3290
|
-
layoutUrl && props.page.layout ? { [layoutUrl]: props.page.layout } : {};
|
|
3291
|
-
}
|
|
3292
|
-
/**
|
|
3293
|
-
* Ends the hydration of the page the server rendered. The server's data no
|
|
3294
|
-
* component took over is dropped: a later page must not show it as current.
|
|
3295
|
-
*/
|
|
3296
|
-
function endHydration(runtime: DmsFrontendRuntime): void {
|
|
3297
|
-
runtime.hydrating = false;
|
|
3298
|
-
runtime.hydratedAsyncData.clear();
|
|
3299
|
-
runtime.hydratedAsyncErrors.clear();
|
|
3300
|
-
}
|
|
3301
|
-
|
|
3302
|
-
export function getDmsErrorPage(): Component | undefined {
|
|
3303
|
-
return registry.errorPage?.component;
|
|
3304
|
-
}
|
|
3305
|
-
|
|
3306
|
-
export async function preloadDmsPage(props: DmsPageProps): Promise<void> {
|
|
3307
|
-
const layoutName =
|
|
3308
|
-
props.page.layout?.layout?.componentName ??
|
|
3309
|
-
props.page.layout?.componentName;
|
|
3310
|
-
const page = findDmsPageEntry(props) ?? registry.dynamicPages.get("default");
|
|
3311
|
-
const layout = layoutName
|
|
3312
|
-
? registry.layouts.get(normalizeDmsName(layoutName))
|
|
3313
|
-
: undefined;
|
|
3314
|
-
const entries = props.error ? [registry.errorPage] : [page, layout];
|
|
3315
|
-
// `preload` only warms the module; the async wrapper stays unresolved until
|
|
3316
|
-
// its own loader runs. Hydrating an unresolved wrapper defers it, and the
|
|
3317
|
-
// first parent render (Inertia's initial swap) then makes Vue drop the
|
|
3318
|
-
// server-rendered markup and render the subtree from scratch.
|
|
3319
|
-
await Promise.all(
|
|
3320
|
-
entries.flatMap((entry) => [
|
|
3321
|
-
entry?.preload?.(),
|
|
3322
|
-
(entry?.component as DmsAsyncComponent | undefined)?.__asyncLoader?.(),
|
|
3323
|
-
]),
|
|
3324
|
-
);
|
|
3325
|
-
if (!props.error) await preloadDmsLayoutComponents(props.page.layout);
|
|
3326
|
-
}
|
|
3327
|
-
|
|
3328
|
-
export function useDmsInjection<T>(
|
|
3329
|
-
key: string | InjectionKey<T>,
|
|
3330
|
-
): T | undefined {
|
|
3331
|
-
return inject(key as InjectionKey<T>, undefined);
|
|
3332
|
-
}
|
|
3333
|
-
|
|
3334
|
-
function registerDmsFrontendEntry(
|
|
3335
|
-
entries: Map<string, DmsFrontendEntry>,
|
|
3336
|
-
name: string,
|
|
3337
|
-
component: Component,
|
|
3338
|
-
preload?: DmsComponentPreloader,
|
|
3339
|
-
): void {
|
|
3340
|
-
if (!entries.has(name)) entries.set(name, { component, preload });
|
|
3341
|
-
}
|
|
3342
|
-
|
|
3343
|
-
/** `name` behind `prefix`, as a PascalCase component name. */
|
|
3344
|
-
function prefixedComponentName(prefix: string, name: string): string {
|
|
3345
|
-
return prefix ? `${prefix}${capitalize(camelize(name))}` : name;
|
|
3346
|
-
}
|
|
3347
|
-
|
|
3348
|
-
function createSdk(
|
|
3349
|
-
target: DmsFrontendRegistry,
|
|
3350
|
-
{
|
|
3351
|
-
name: module,
|
|
3352
|
-
module: { componentPrefix = "" },
|
|
3353
|
-
options,
|
|
3354
|
-
}: DmsFrontendModuleRegistration,
|
|
3355
|
-
): DmsFrontendSdk {
|
|
3356
|
-
return {
|
|
3357
|
-
options,
|
|
3358
|
-
registerComponent: (name, component, { prefix = true } = {}) =>
|
|
3359
|
-
registerDmsComponentEntry(target.components, {
|
|
3360
|
-
name: prefix ? prefixedComponentName(componentPrefix, name) : name,
|
|
3361
|
-
component,
|
|
3362
|
-
module,
|
|
3363
|
-
unprefixed: !prefix,
|
|
3364
|
-
}),
|
|
3365
|
-
registerPage: (name, component, preload) => {
|
|
3366
|
-
const key = normalizeDmsPageKey(name);
|
|
3367
|
-
registerDmsFrontendEntry(target.pages, key, component, preload);
|
|
3368
|
-
},
|
|
3369
|
-
registerDynamicPage: (name, component, preload) =>
|
|
3370
|
-
registerDmsFrontendEntry(target.dynamicPages, name, component, preload),
|
|
3371
|
-
registerLayout: (name, component, preload) => {
|
|
3372
|
-
const key = normalizeDmsName(name);
|
|
3373
|
-
registerDmsFrontendEntry(target.layouts, key, component, preload);
|
|
3374
|
-
},
|
|
3375
|
-
registerErrorPage: (component, preload) => {
|
|
3376
|
-
target.errorPage ??= { component, preload };
|
|
3377
|
-
},
|
|
3378
|
-
registerPlugin: (setup, options = {}) =>
|
|
3379
|
-
target.pluginSetups.push({
|
|
3380
|
-
setup,
|
|
3381
|
-
clientOnly: options.clientOnly ?? false,
|
|
3382
|
-
enforce: options.enforce ?? "default",
|
|
3383
|
-
}),
|
|
3384
|
-
registerMiddleware: (name, handler, options = {}) => {
|
|
3385
|
-
if (!target.namedMiddleware.has(name))
|
|
3386
|
-
target.namedMiddleware.set(name, handler);
|
|
3387
|
-
if (options.global) target.middleware.push(handler);
|
|
3388
|
-
if (options.parallel) target.parallelMiddleware.add(handler);
|
|
3389
|
-
},
|
|
3390
|
-
registerAccessRedirect: (code, path) => {
|
|
3391
|
-
if (!target.accessRedirects.has(code))
|
|
3392
|
-
target.accessRedirects.set(code, path);
|
|
3393
|
-
},
|
|
3394
|
-
provide: (key, value) => {
|
|
3395
|
-
if (!target.injections.has(key)) target.injections.set(key, value);
|
|
3396
|
-
},
|
|
3397
|
-
use: (plugin) => target.plugins.push(plugin),
|
|
3398
|
-
};
|
|
3399
|
-
}
|
|
3400
|
-
|
|
3401
|
-
/**
|
|
3402
|
-
* Sets the frontend modules up in their generated priority order, into a
|
|
3403
|
-
* registry of their own that replaces the previous one once complete.
|
|
3404
|
-
*
|
|
3405
|
-
* The browser and a production server set the modules up once. A development
|
|
3406
|
-
* server sets them up again each time Vite evaluates the server renderer anew
|
|
3407
|
-
* after a layer changes, but keeps this module and its state: registering into
|
|
3408
|
-
* the previous registry, the new components lost their names to the ones they
|
|
3409
|
-
* replace, so the server kept rendering the old ones, and every list gained the
|
|
3410
|
-
* same entries once more. A render still in progress keeps reading a complete
|
|
3411
|
-
* registry: the new one is only published once every module is set up.
|
|
3412
|
-
*
|
|
3413
|
-
* `useDmsAppConfig()` holds the layers' app configs merged, highest precedence
|
|
3414
|
-
* first, before the first module is set up: a project's layer overrides the
|
|
3415
|
-
* DMS's, which fills in the rest. A setup may still read and extend it.
|
|
3416
|
-
*/
|
|
3417
|
-
export async function setupFrontendModules(
|
|
3418
|
-
registrations: DmsFrontendModuleRegistration[],
|
|
3419
|
-
layerAppConfigs: DmsLayerAppConfig[] = [],
|
|
3420
|
-
): Promise<void> {
|
|
3421
|
-
const next = createDmsFrontendRegistry();
|
|
3422
|
-
const configs = layerAppConfigs.map(({ config }) => config);
|
|
3423
|
-
// Modules read and extend these two while they are set up, so they restart
|
|
3424
|
-
// here rather than being published with the registry: the public options
|
|
3425
|
-
// empty, the app config from the layers alone.
|
|
3426
|
-
runtimeConfig.value.public = {} as PublicRuntimeConfig;
|
|
3427
|
-
appConfig.value = mergeLayerAppConfigs(configs);
|
|
3428
|
-
for (const registration of registrations) {
|
|
3429
|
-
runtimeConfig.value.public = defu(
|
|
3430
|
-
runtimeConfig.value.public,
|
|
3431
|
-
registration.options.public,
|
|
3432
|
-
);
|
|
3433
|
-
await registration.module.setup(createSdk(next, registration));
|
|
3434
|
-
}
|
|
3435
|
-
appConfigSetupChanges = appConfigChanges(
|
|
3436
|
-
mergeLayerAppConfigs(configs),
|
|
3437
|
-
appConfig.value,
|
|
3438
|
-
);
|
|
3439
|
-
registry = next;
|
|
3440
|
-
}
|
|
3441
|
-
|
|
3442
|
-
/**
|
|
3443
|
-
* Development only: applies the layers' app configs, edited while the page
|
|
3444
|
-
* is open, to `useDmsAppConfig()`, with what the setup of the modules changed
|
|
3445
|
-
* in the previous ones still on top. The object is updated in place, records
|
|
3446
|
-
* included, since components hold it: the modules are not set up again.
|
|
3447
|
-
*/
|
|
3448
|
-
export function updateDmsAppConfig(layerAppConfigs: DmsLayerAppConfig[]): void {
|
|
3449
|
-
const layered = mergeLayerAppConfigs(
|
|
3450
|
-
layerAppConfigs.map(({ config }) => config),
|
|
3451
|
-
);
|
|
3452
|
-
assignInPlace(
|
|
3453
|
-
appConfig.value,
|
|
3454
|
-
withAppConfigChanges(layered, appConfigSetupChanges),
|
|
3455
|
-
);
|
|
3456
|
-
}
|
|
3457
|
-
|
|
3458
|
-
export async function installDmsPlugins(
|
|
3459
|
-
app: App,
|
|
3460
|
-
i18n: Composer,
|
|
3461
|
-
runtime: DmsFrontendRuntime,
|
|
3462
|
-
loadLocaleMessages: DmsLocaleLoader,
|
|
3463
|
-
supportedLocales: string[],
|
|
3464
|
-
): Promise<() => Promise<void>> {
|
|
3465
|
-
const dmsI18n = i18n as DmsI18n;
|
|
3466
|
-
const hooks = new Map<string, Array<() => void | Promise<void>>>();
|
|
3467
|
-
const appContext: DmsAppContext = {
|
|
3468
|
-
vueApp: app,
|
|
3469
|
-
$i18n: dmsI18n,
|
|
3470
|
-
provide: (key, value) => {
|
|
3471
|
-
app.provide(key, value);
|
|
3472
|
-
app.config.globalProperties[`$${key}`] = value;
|
|
3473
|
-
},
|
|
3474
|
-
runWithContext: (callback) => app.runWithContext(callback),
|
|
3475
|
-
hook: (name, callback) =>
|
|
3476
|
-
hooks.set(name, [...(hooks.get(name) ?? []), callback]),
|
|
3477
|
-
};
|
|
3478
|
-
runtime.appContext = appContext;
|
|
3479
|
-
if (typeof window !== "undefined" && !runtime.hasNavigationListener) {
|
|
3480
|
-
runtime.hasNavigationListener = true;
|
|
3481
|
-
const stopNavigationListener = inertiaRouter.on("before", () => {
|
|
3482
|
-
runtime.currentError.value = null;
|
|
3483
|
-
});
|
|
3484
|
-
app.onUnmount(() => {
|
|
3485
|
-
stopNavigationListener();
|
|
3486
|
-
runtime.hasNavigationListener = false;
|
|
3487
|
-
if (browserRuntime === runtime) browserRuntime = undefined;
|
|
3488
|
-
});
|
|
3489
|
-
}
|
|
3490
|
-
const failedSetups = new WeakSet<ComponentInternalInstance>();
|
|
3491
|
-
const warnOutsideFailedSetups: AppConfig["warnHandler"] = (
|
|
3492
|
-
message,
|
|
3493
|
-
instance,
|
|
3494
|
-
trace,
|
|
3495
|
-
) => {
|
|
3496
|
-
// The comment Vue renders in place of an `undefined` component has no
|
|
3497
|
-
// public instance: the template rendering it is the one that failed.
|
|
3498
|
-
const source = instance?.$ ?? getCurrentInstance();
|
|
3499
|
-
if (!withinFailedSetup(source, failedSetups))
|
|
3500
|
-
console.warn(`[Vue warn]: ${message}${trace ? `\n${trace}` : ""}`);
|
|
3501
|
-
};
|
|
3502
|
-
app.config.errorHandler = (reason, instance, info) => {
|
|
3503
|
-
if (instance && info === SETUP_FUNCTION_ERROR) {
|
|
3504
|
-
failedSetups.add(instance.$);
|
|
3505
|
-
// From the first failure on only: until then Vue keeps its own output,
|
|
3506
|
-
// whose component trace shows the props. An app that already has a
|
|
3507
|
-
// handler keeps it. A server renders a single page per app; in the
|
|
3508
|
-
// browser the app outlives Inertia visits, so once a page has failed
|
|
3509
|
-
// the handler stays for the session and later warnings print as plain
|
|
3510
|
-
// text (no inspectable objects), still all of them.
|
|
3511
|
-
app.config.warnHandler ??= warnOutsideFailedSetups;
|
|
3512
|
-
}
|
|
3513
|
-
failPage(runtime, reason);
|
|
3514
|
-
};
|
|
3515
|
-
app.config.globalProperties.$dms = appContext;
|
|
3516
|
-
Reflect.set(app, "$dms", appContext);
|
|
3517
|
-
Reflect.set(app, "$i18n", i18n);
|
|
3518
|
-
Reflect.set(appContext.vueApp, "$dms", appContext);
|
|
3519
|
-
Reflect.set(app.config.globalProperties, "$i18n", i18n);
|
|
3520
|
-
dmsI18n.setLocale = async (locale: string) => {
|
|
3521
|
-
const messages = await loadLocaleMessages(locale);
|
|
3522
|
-
i18n.setLocaleMessage(locale, messages);
|
|
3523
|
-
i18n.locale.value = locale;
|
|
3524
|
-
if (typeof document !== "undefined" && supportedLocales.includes(locale))
|
|
3525
|
-
writeCookie(LOCALE_COOKIE, locale, {
|
|
3526
|
-
maxAge: LOCALE_COOKIE_MAX_AGE,
|
|
3527
|
-
sameSite: "lax",
|
|
3528
|
-
secure: globalThis.location?.protocol === "https:",
|
|
3529
|
-
});
|
|
3530
|
-
};
|
|
3531
|
-
dmsI18n.locales = computed(() =>
|
|
3532
|
-
supportedLocales.map((code) => ({
|
|
3533
|
-
code,
|
|
3534
|
-
name: new Intl.DisplayNames([code], { type: "language" }).of(code),
|
|
3535
|
-
})),
|
|
3536
|
-
);
|
|
3537
|
-
app.component("DmsLink", DmsLink);
|
|
3538
|
-
app.component("DmsClientOnly", DmsClientOnly);
|
|
3539
|
-
registry.components.forEach(({ component, name }) => {
|
|
3540
|
-
app.component(name, component);
|
|
3541
|
-
});
|
|
3542
|
-
registry.injections.forEach((value, key) => {
|
|
3543
|
-
app.provide(key, value);
|
|
3544
|
-
});
|
|
3545
|
-
registry.plugins.forEach((plugin) => {
|
|
3546
|
-
app.use(plugin);
|
|
3547
|
-
});
|
|
3548
|
-
// Stable: within a group, the modules' priority order stays.
|
|
3549
|
-
const pluginSetups = [...registry.pluginSetups].sort(
|
|
3550
|
-
(left, right) => PLUGIN_ORDER[left.enforce] - PLUGIN_ORDER[right.enforce],
|
|
3551
|
-
);
|
|
3552
|
-
for (const registration of pluginSetups) {
|
|
3553
|
-
if (registration.clientOnly && typeof window === "undefined") continue;
|
|
3554
|
-
await runtime.scope.run(() =>
|
|
3555
|
-
app.runWithContext(() => registration.setup(appContext)),
|
|
3556
|
-
);
|
|
3557
|
-
}
|
|
3558
|
-
// What plugins start without awaiting it is still part of the page a
|
|
3559
|
-
// server renders: its HTML shows it, so the render waits for it.
|
|
3560
|
-
if (runtime.isServer) await Promise.all(runtime.serverPrefetches);
|
|
3561
|
-
return async () => {
|
|
3562
|
-
for (const callback of hooks.get("app:mounted") ?? []) await callback();
|
|
3563
|
-
};
|
|
3564
|
-
}
|
|
120
|
+
DmsKbdPlatformSource,
|
|
121
|
+
} from "./frontend-runtime/ui";
|
|
122
|
+
export { readDmsLocaleCookie, useDmsCookie } from "./frontend-runtime/cookies";
|
|
123
|
+
export { createDmsError } from "./frontend-runtime/dms-error";
|
|
124
|
+
export {
|
|
125
|
+
showDmsError,
|
|
126
|
+
useDmsError,
|
|
127
|
+
clearDmsError,
|
|
128
|
+
preloadPrefetchedDmsPages,
|
|
129
|
+
useDmsUserSession,
|
|
130
|
+
useDmsPageLoading,
|
|
131
|
+
isDmsHydrating,
|
|
132
|
+
dmsPageSuspenseProps,
|
|
133
|
+
} from "./frontend-runtime/errors";
|
|
134
|
+
export {
|
|
135
|
+
registerDmsComponent,
|
|
136
|
+
resolveDmsComponent,
|
|
137
|
+
trackDmsAsyncComponents,
|
|
138
|
+
trackDmsServerScopes,
|
|
139
|
+
resolveDmsAsyncComponents,
|
|
140
|
+
preloadDmsComponents,
|
|
141
|
+
normalizeDmsName,
|
|
142
|
+
normalizeDmsPageKey,
|
|
143
|
+
resolveDmsAccessRedirect,
|
|
144
|
+
hasDmsPage,
|
|
145
|
+
getDmsPage,
|
|
146
|
+
getDmsDynamicPage,
|
|
147
|
+
getDmsLayout,
|
|
148
|
+
getDmsLayoutProps,
|
|
149
|
+
hydrateDmsSharedProps,
|
|
150
|
+
keepSharedOverHistory,
|
|
151
|
+
hydrateDmsPageProps,
|
|
152
|
+
getDmsErrorPage,
|
|
153
|
+
preloadDmsPage,
|
|
154
|
+
} from "./frontend-runtime/components";
|
|
155
|
+
export {
|
|
156
|
+
useDmsInjection,
|
|
157
|
+
setupFrontendModules,
|
|
158
|
+
updateDmsAppConfig,
|
|
159
|
+
installDmsPlugins,
|
|
160
|
+
} from "./frontend-runtime/sdk";
|