@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.
Files changed (103) hide show
  1. package/README.md +9 -1
  2. package/dist/cancellation.js +2 -1
  3. package/dist/config.js +11 -7
  4. package/dist/derived-outputs.js +6 -113
  5. package/dist/dev-modules.js +3 -2
  6. package/dist/discovery.js +19 -12
  7. package/dist/fs-sync.js +1 -2
  8. package/dist/internal/guards.d.ts +5 -0
  9. package/dist/internal/guards.js +22 -0
  10. package/dist/internal/locale-catalogs.d.ts +6 -0
  11. package/dist/internal/locale-catalogs.js +115 -0
  12. package/dist/layer-aliases.js +2 -1
  13. package/dist/layers.js +2 -1
  14. package/dist/locale-files.js +9 -4
  15. package/dist/manifest-secrets.js +11 -4
  16. package/dist/manifest.js +11 -5
  17. package/dist/materialize.js +6 -5
  18. package/dist/module-set.js +2 -4
  19. package/dist/ports.js +8 -1
  20. package/dist/private-values.js +26 -17
  21. package/dist/project-modules.js +17 -11
  22. package/dist/renderer-range.js +2 -1
  23. package/dist/server-process.js +17 -16
  24. package/dist/update-check.js +23 -16
  25. package/dist/verify-source-runner.js +2 -2
  26. package/dist/workspace-setup.js +1 -2
  27. package/dist/workspace.js +11 -10
  28. package/package.json +7 -4
  29. package/skills/dms-frontend/SKILL.md +1 -1
  30. package/skills/dms-frontend/cases.json +77 -0
  31. package/templates/vue/app-runtime.ts +88 -7
  32. package/templates/vue/client-chunking.mjs +0 -5
  33. package/templates/vue/client-server-guard.mjs +10 -13
  34. package/templates/vue/entry-weight.mjs +12 -14
  35. package/templates/vue/frontend-module.ts +159 -3563
  36. package/templates/vue/frontend-runtime/async-data-entry.ts +320 -0
  37. package/templates/vue/frontend-runtime/async-data.ts +404 -0
  38. package/templates/vue/frontend-runtime/components.ts +386 -0
  39. package/templates/vue/frontend-runtime/cookies.ts +125 -0
  40. package/templates/vue/frontend-runtime/data-access.ts +132 -0
  41. package/templates/vue/frontend-runtime/dms-error.ts +11 -0
  42. package/templates/vue/frontend-runtime/errors.ts +145 -0
  43. package/templates/vue/frontend-runtime/fetch.ts +306 -0
  44. package/templates/vue/frontend-runtime/module-registration.ts +247 -0
  45. package/templates/vue/frontend-runtime/paged-data.ts +191 -0
  46. package/templates/vue/frontend-runtime/payload.ts +172 -0
  47. package/templates/vue/frontend-runtime/route.ts +328 -0
  48. package/templates/vue/frontend-runtime/sdk.ts +300 -0
  49. package/templates/vue/frontend-runtime/state.ts +167 -0
  50. package/templates/vue/frontend-runtime/types.ts +361 -0
  51. package/templates/vue/frontend-runtime/ui.ts +247 -0
  52. package/templates/vue/globals.d.ts +5 -0
  53. package/templates/vue/head-order.mjs +11 -13
  54. package/templates/vue/icon-bundle.ts +31 -0
  55. package/templates/vue/message-subset.ts +65 -0
  56. package/templates/vue/network-error.ts +4 -0
  57. package/templates/vue/prepare-types.mjs +0 -8
  58. package/templates/vue/server/auth/backend.mjs +4 -2
  59. package/templates/vue/server/auth/establish-endpoints.mjs +14 -9
  60. package/templates/vue/server/auth/session.mjs +4 -3
  61. package/templates/vue/server/auth-hooks.mjs +6 -6
  62. package/templates/vue/server/backend-proxies.mjs +6 -6
  63. package/templates/vue/server/backend-response.mjs +15 -14
  64. package/templates/vue/server/built-files.mjs +10 -8
  65. package/templates/vue/server/cli-report.mjs +7 -5
  66. package/templates/vue/server/client-error.mjs +11 -8
  67. package/templates/vue/server/client-manifest.mjs +9 -8
  68. package/templates/vue/server/content.mjs +2 -1
  69. package/templates/vue/server/dev-private-files.mjs +20 -15
  70. package/templates/vue/server/dev-styles.mjs +22 -25
  71. package/templates/vue/server/email-bundle-watch.mjs +0 -12
  72. package/templates/vue/server/email.mjs +20 -15
  73. package/templates/vue/server/favicon.mjs +12 -11
  74. package/templates/vue/server/homepage.mjs +8 -10
  75. package/templates/vue/server/icons.mjs +18 -9
  76. package/templates/vue/server/in-place-render.mjs +6 -8
  77. package/templates/vue/server/listen-host.mjs +2 -2
  78. package/templates/vue/server/module-event.mjs +5 -5
  79. package/templates/vue/server/module-routes.mjs +11 -7
  80. package/templates/vue/server/module-server.mjs +8 -7
  81. package/templates/vue/server/module-testing.mjs +7 -7
  82. package/templates/vue/server/page-props.mjs +14 -14
  83. package/templates/vue/server/path-patterns.mjs +4 -4
  84. package/templates/vue/server/proxy-compression.mjs +21 -13
  85. package/templates/vue/server/proxy-headers.mjs +5 -3
  86. package/templates/vue/server/proxy-redirects.mjs +7 -6
  87. package/templates/vue/server/proxy-transfers.mjs +15 -11
  88. package/templates/vue/server/public-endpoints.mjs +27 -19
  89. package/templates/vue/server/public-options.mjs +9 -7
  90. package/templates/vue/server/request-failure.mjs +4 -6
  91. package/templates/vue/server/request-routing.mjs +8 -11
  92. package/templates/vue/server/request-target.mjs +6 -6
  93. package/templates/vue/server/route-rules.mjs +7 -5
  94. package/templates/vue/server/static-files.mjs +19 -10
  95. package/templates/vue/server/web-body.mjs +3 -4
  96. package/templates/vue/server.mjs +32 -19
  97. package/templates/vue/sfc-global-types.mjs +27 -24
  98. package/templates/vue/ssr-renderer.ts +10 -1
  99. package/templates/vue/ui-inertia-page.mjs +7 -7
  100. package/templates/vue/ui-kbd.mjs +10 -10
  101. package/templates/vue/ui-lazy-icons.mjs +44 -0
  102. package/templates/vue/ui-request-state.mjs +9 -9
  103. package/templates/vue/vite.config.ts +2 -0
@@ -0,0 +1,145 @@
1
+ import { router as inertiaRouter } from "@inertiajs/vue3";
2
+ import { ofetch } from "ofetch";
3
+ import { computed, nextTick, type Ref, readonly } from "vue";
4
+ import {
5
+ type DmsErrorData,
6
+ type DmsPageProps,
7
+ type DmsSession,
8
+ type DmsUser,
9
+ type DmsUserSession,
10
+ } from "./types";
11
+ import { createDmsError } from "./dms-error";
12
+ import { useDmsRuntime } from "./state";
13
+ import { navigateDms } from "./route";
14
+ import { endHydration, preloadDmsPage } from "./components";
15
+ import { useDmsState } from "./payload";
16
+
17
+ export function showDmsError(
18
+ input: string | DmsErrorData,
19
+ ): Error & DmsErrorData {
20
+ const error = createDmsError(input);
21
+ useDmsRuntime().currentError.value = error;
22
+ return error;
23
+ }
24
+ export function useDmsError(): Ref<(Error & DmsErrorData) | null> {
25
+ return useDmsRuntime().currentError as Ref<(Error & DmsErrorData) | null>;
26
+ }
27
+ export async function clearDmsError(options?: {
28
+ redirect?: string;
29
+ }): Promise<void> {
30
+ useDmsRuntime().currentError.value = null;
31
+ if (options?.redirect) await navigateDms(options.redirect);
32
+ }
33
+
34
+ function sessionIdentity(session: unknown): string {
35
+ const { accountId, activeTenantId } = (session ?? {}) as Partial<DmsSession>;
36
+ return `${accountId ?? ""}\n${activeTenantId ?? ""}`;
37
+ }
38
+
39
+ /**
40
+ * Drops the pages Inertia prefetched (a hovered link) once the session is
41
+ * another account's or workspace's, or none: each carries the user, the page
42
+ * and the site layout of the session it was fetched with, and a visit would
43
+ * show them as current.
44
+ */
45
+ function forgetPrefetchedPages(): void {
46
+ if (typeof window !== "undefined") inertiaRouter.flushAll();
47
+ }
48
+
49
+ /**
50
+ * Warms what a prefetched page renders (its page, layout and block
51
+ * components) once Inertia has its props, so following the link does not wait
52
+ * for those chunks after the props.
53
+ */
54
+ export function preloadPrefetchedDmsPages(): () => void {
55
+ return inertiaRouter.on("prefetched", (event) => {
56
+ const data = event.detail.response.data as unknown;
57
+ let page: { props?: DmsPageProps } | undefined;
58
+ try {
59
+ page = (typeof data === "string" ? JSON.parse(data) : data) as
60
+ | { props?: DmsPageProps }
61
+ | undefined;
62
+ } catch {
63
+ return;
64
+ }
65
+ if (page?.props?.page) void preloadDmsPage(page.props).catch(() => {});
66
+ });
67
+ }
68
+
69
+ export function useDmsUserSession<
70
+ TUser = DmsUser,
71
+ TSession = DmsSession,
72
+ >(): DmsUserSession<TUser, TSession> {
73
+ const user = useDmsState<TUser | null>("dms-user", () => null);
74
+ const session = useDmsState<TSession | null>("dms-session", () => null);
75
+ return {
76
+ user,
77
+ session,
78
+ loggedIn: computed(() => user.value !== null),
79
+ fetch: async () => {
80
+ const before = sessionIdentity(session.value);
81
+ const value = await ofetch<{
82
+ user?: TUser;
83
+ session?: TSession;
84
+ }>("/api/_auth/session", { method: "POST" });
85
+ user.value = value.user ?? null;
86
+ session.value = value.session ?? null;
87
+ if (sessionIdentity(session.value) !== before) forgetPrefetchedPages();
88
+ },
89
+ clear: async () => {
90
+ await ofetch("/api/_auth/session", { method: "DELETE" });
91
+ user.value = null;
92
+ session.value = null;
93
+ forgetPrefetchedPages();
94
+ },
95
+ };
96
+ }
97
+
98
+ /**
99
+ * Whether the page loaded by the current navigation is still pending: its
100
+ * chunk, or an async `setup` it awaits, has not resolved yet. A layout reads
101
+ * it to show its own placeholder in place of the page. Always `false` on the
102
+ * server and during hydration, which render the page resolved.
103
+ */
104
+ export function useDmsPageLoading(): Readonly<Ref<boolean>> {
105
+ return readonly(useDmsRuntime().pageLoading);
106
+ }
107
+
108
+ /**
109
+ * Whether the browser is hydrating the page the server rendered, whose first
110
+ * render must match the server's HTML: from the start of the application to
111
+ * the tick the page's `<Suspense>` resolves. False on the server, in a page
112
+ * the server did not render, and after a client navigation. As Nuxt's
113
+ * `nuxtApp.isHydrating`; read once, it is not reactive.
114
+ */
115
+ export function isDmsHydrating(): boolean {
116
+ return useDmsRuntime().hydrating;
117
+ }
118
+
119
+ /**
120
+ * The props of the `<Suspense>` around the page that keep `useDmsPageLoading`
121
+ * in step with it. Cleared before the boundary unmounts, so a page left while
122
+ * pending never leaves the state set.
123
+ *
124
+ * They also end the hydration: the page is on screen once its suspense
125
+ * resolves, and the components after it once the tick is over, so the
126
+ * browser has then hydrated what the server rendered. A hydrating suspense
127
+ * never reports itself pending: the server rendered the page resolved.
128
+ */
129
+ export function dmsPageSuspenseProps(): Record<string, () => void> {
130
+ const runtime = useDmsRuntime();
131
+ const loading = runtime.pageLoading;
132
+ const settle = () => {
133
+ loading.value = false;
134
+ };
135
+ return {
136
+ onPending: () => {
137
+ loading.value = true;
138
+ },
139
+ onResolve: () => {
140
+ settle();
141
+ if (runtime.hydrating) void nextTick(() => endHydration(runtime));
142
+ },
143
+ onVnodeBeforeUnmount: settle,
144
+ };
145
+ }
@@ -0,0 +1,306 @@
1
+ import { type FetchOptions, ofetch } from "ofetch";
2
+ import { isRef, type MaybeRefOrGetter, toValue, watch } from "vue";
3
+ import { type DmsFrontendRuntime } from "./module-registration";
4
+ import { useDmsRuntime } from "./state";
5
+ import {
6
+ type DmsAsyncDataHandler,
7
+ type DmsAsyncDataResult,
8
+ type UseAsyncDataOptions,
9
+ type UseFetchOptions,
10
+ } from "./types";
11
+ import { useDmsAsyncData } from "./async-data";
12
+
13
+ /**
14
+ * The options of a request a runtime makes. A server render does not retry a
15
+ * failed read, as ofetch would once: the page waits for it, and a backend
16
+ * failing once mostly fails twice. A browser keeps ofetch's retries, and a
17
+ * caller that names its own keeps them everywhere.
18
+ */
19
+ function runtimeFetchOptions(
20
+ runtime: DmsFrontendRuntime,
21
+ options: FetchOptions,
22
+ ): FetchOptions {
23
+ return runtime.isServer && options.retry === undefined
24
+ ? { ...options, retry: 0 }
25
+ : options;
26
+ }
27
+
28
+ const warnedServerReads = new Set<string>();
29
+
30
+ /**
31
+ * Warns, in a development server render, about a backend read no async data
32
+ * handler makes: an immediate watcher's, a setup's own `await $fetch`, a
33
+ * middleware's. The server waits for it, but the page carries none of it, so
34
+ * the browser reads it again and may hydrate with other data. Code that hands
35
+ * the answer over itself (`useDmsState` with `transfer: true`) looks the same
36
+ * from here, so the message names that exception rather than guess.
37
+ */
38
+ function warnUntransferredRead(
39
+ runtime: DmsFrontendRuntime,
40
+ request: string,
41
+ ): void {
42
+ if (
43
+ !runtime.isServer ||
44
+ !runtime.devWarnings ||
45
+ !runtime.inAsyncDataHandler ||
46
+ runtime.inAsyncDataHandler() ||
47
+ warnedServerReads.has(request)
48
+ )
49
+ return;
50
+ warnedServerReads.add(request);
51
+ console.warn(
52
+ `DMS: ${request} was read during a server render outside useDmsFetch and useDmsAsyncData ` +
53
+ "(an immediate watcher, a setup's own await, a middleware): unless the code hands the " +
54
+ "answer over (useDmsState with transfer: true), the page does not carry it and the " +
55
+ "browser reads it again. Load it with useDmsFetch or useDmsAsyncData, in a middleware " +
56
+ "or a plugin too, or leave it to the browser (server: false, onMounted).",
57
+ );
58
+ }
59
+
60
+ function createDmsFetch(defaults: FetchOptions = {}): typeof ofetch {
61
+ const configuredFetch = ofetch.create(defaults);
62
+ const execute = (request: string, options?: FetchOptions) => {
63
+ const runtime = useDmsRuntime();
64
+ warnUntransferredRead(runtime, request);
65
+ return (runtime.serverFetch ?? ofetch)(
66
+ request,
67
+ runtimeFetchOptions(runtime, { ...defaults, ...options }),
68
+ );
69
+ };
70
+ return Object.assign(execute, {
71
+ create: (options: FetchOptions) =>
72
+ createDmsFetch({ ...defaults, ...options }),
73
+ native: configuredFetch.native,
74
+ raw: configuredFetch.raw,
75
+ }) as typeof ofetch;
76
+ }
77
+
78
+ export const $fetch = createDmsFetch();
79
+
80
+ /** JSON with sorted keys, or nothing for a value JSON cannot stand for. */
81
+ function stableJson(value: unknown): string | undefined {
82
+ if (value === undefined || value === null) return "null";
83
+ if (value instanceof Date) return JSON.stringify(value);
84
+ if (typeof value !== "object")
85
+ return ["string", "number", "boolean"].includes(typeof value)
86
+ ? JSON.stringify(value)
87
+ : undefined;
88
+ if (Array.isArray(value)) {
89
+ const items = value.map(stableJson);
90
+ return items.includes(undefined) ? undefined : `[${items.join(",")}]`;
91
+ }
92
+ const prototype = Object.getPrototypeOf(value) as unknown;
93
+ if (prototype !== Object.prototype && prototype !== null) return undefined;
94
+ const record = value as Record<string, unknown>;
95
+ const members = Object.keys(record)
96
+ .filter((name) => record[name] !== undefined)
97
+ .sort()
98
+ .map((name) => {
99
+ const member = stableJson(record[name]);
100
+ return member === undefined
101
+ ? undefined
102
+ : `${JSON.stringify(name)}:${member}`;
103
+ });
104
+ return members.includes(undefined) ? undefined : `{${members.join(",")}}`;
105
+ }
106
+
107
+ const FETCH_KEY_PREFIX = "$dmsfetch:";
108
+ const DEFAULT_FETCH_METHOD = "GET";
109
+
110
+ // cyrb53's mixing constants, with its two 32-bit lanes both kept: a 64-bit
111
+ // digest, as short as Nuxt's keys and as unlikely to collide within a page.
112
+ const HASH_SEEDS = [0xdeadbeef, 0x41c6ce57];
113
+ const HASH_MULTIPLIERS = [2654435761, 1597334677];
114
+ const HASH_FINALIZERS = [2246822507, 3266489909];
115
+ const HASH_SHIFTS = [16, 13];
116
+ const HASH_RADIX = 36;
117
+ const HASH_LANE_LENGTH = 7;
118
+
119
+ /**
120
+ * A short digest of a text, the same in a server and in a browser: pure
121
+ * JavaScript, so no `node:crypto` reaches a browser bundle.
122
+ */
123
+ function hashText(text: string): string {
124
+ let [first, second] = HASH_SEEDS;
125
+ for (let index = 0; index < text.length; index++) {
126
+ const code = text.charCodeAt(index);
127
+ first = Math.imul(first ^ code, HASH_MULTIPLIERS[0]);
128
+ second = Math.imul(second ^ code, HASH_MULTIPLIERS[1]);
129
+ }
130
+ first = Math.imul(first ^ (first >>> HASH_SHIFTS[0]), HASH_FINALIZERS[0]);
131
+ first ^= Math.imul(second ^ (second >>> HASH_SHIFTS[1]), HASH_FINALIZERS[1]);
132
+ second = Math.imul(second ^ (second >>> HASH_SHIFTS[0]), HASH_FINALIZERS[0]);
133
+ second ^= Math.imul(first ^ (first >>> HASH_SHIFTS[1]), HASH_FINALIZERS[1]);
134
+ return [second, first]
135
+ .map((lane) =>
136
+ (lane >>> 0).toString(HASH_RADIX).padStart(HASH_LANE_LENGTH, "0"),
137
+ )
138
+ .join("");
139
+ }
140
+
141
+ /**
142
+ * The key a request's response is shared and transferred under: a digest of
143
+ * its method, base URL, URL, query and params, and body, the headers aside. A
144
+ * server render writes the key into its HTML, which must not show the URL, a
145
+ * token in its query or a body. A body only a request can read, such as a
146
+ * FormData, gives none.
147
+ */
148
+ function requestKey(url: string, options: FetchOptions): string | undefined {
149
+ // ofetch sends the params and the query together, the query winning.
150
+ const query =
151
+ options.query || options.params
152
+ ? { ...options.params, ...options.query }
153
+ : null;
154
+ const request = stableJson([
155
+ (options.method ?? DEFAULT_FETCH_METHOD).toUpperCase(),
156
+ options.baseURL ?? "",
157
+ url,
158
+ query,
159
+ options.body ?? null,
160
+ ]);
161
+ return request === undefined
162
+ ? undefined
163
+ : `${FETCH_KEY_PREFIX}${hashText(request)}`;
164
+ }
165
+
166
+ /** The parts of a request its key is computed from. */
167
+ export type DmsRequestKeyOptions = Pick<
168
+ FetchOptions,
169
+ "method" | "baseURL" | "query" | "params" | "body"
170
+ >;
171
+
172
+ /**
173
+ * The key `useDmsFetch` loads a request's response under, without the scope
174
+ * (`setDmsAsyncDataScope`): what `refreshDmsData(keys)` takes, and what a
175
+ * test compares with the keys it finds. `undefined` for a body only a
176
+ * request can read, such as a FormData.
177
+ *
178
+ * @param url The URL as given to `useDmsFetch`, before the base URL and the query
179
+ * @param options The method, base URL, query, params and body of the request
180
+ */
181
+ export function dmsRequestKey(
182
+ url: string,
183
+ options: DmsRequestKeyOptions = {},
184
+ ): string | undefined {
185
+ return requestKey(url, options);
186
+ }
187
+
188
+ /** A URL to request, or nothing to request while it is null or undefined. */
189
+ type DmsFetchRequest = string | null | undefined;
190
+
191
+ function isReactiveSource(value: unknown): boolean {
192
+ return isRef(value) || typeof value === "function";
193
+ }
194
+
195
+ /** The options given, without the ones left undefined. */
196
+ function definedOptions(options: FetchOptions): FetchOptions {
197
+ return Object.fromEntries(
198
+ Object.entries(options).filter(([, value]) => value !== undefined),
199
+ ) as FetchOptions;
200
+ }
201
+
202
+ /**
203
+ * Fetches a URL as `useDmsAsyncData` loads its data, under the key of the
204
+ * request, so that the callers of one request share it and a server render
205
+ * transfers it. A ref or a getter for the URL, the method, the query, the
206
+ * params or the body moves the caller to the key of each request; a URL that
207
+ * is null or undefined requests nothing.
208
+ */
209
+ export function useDmsFetch<T>(
210
+ request: MaybeRefOrGetter<DmsFetchRequest>,
211
+ options: UseFetchOptions<T> = {},
212
+ ): DmsAsyncDataResult<T> {
213
+ const runtime = useDmsRuntime();
214
+ const {
215
+ key,
216
+ immediate,
217
+ lazy,
218
+ server,
219
+ watch: sources,
220
+ default: fallback,
221
+ transform,
222
+ pick,
223
+ deep,
224
+ dedupe,
225
+ refreshOnPageRefresh,
226
+ refreshInterval,
227
+ $fetch: fetcher,
228
+ method,
229
+ query,
230
+ params,
231
+ body,
232
+ ...fixedOptions
233
+ } = options;
234
+ const url = () => toValue(request);
235
+ const requestOptions = (): FetchOptions =>
236
+ definedOptions({
237
+ ...fixedOptions,
238
+ method: toValue(method),
239
+ query: toValue(query),
240
+ params: toValue(params),
241
+ body: toValue(body),
242
+ });
243
+ // A `$fetch` of the caller's own applies its own retries. The transform
244
+ // runs here, where the request a response answers is known: a reactive URL
245
+ // may have changed since it was sent.
246
+ const handler: DmsAsyncDataHandler<T> = async ({ signal }) => {
247
+ const target = url() as string;
248
+ const sent = requestOptions();
249
+ const runKey =
250
+ key === undefined
251
+ ? requestKey(target, sent)
252
+ : (toValue(key) ?? undefined);
253
+ const value: unknown = await (fetcher
254
+ ? fetcher(target, { ...sent, signal })
255
+ : (runtime.serverFetch ?? ofetch)(
256
+ target,
257
+ runtimeFetchOptions(runtime, { ...sent, signal }),
258
+ ));
259
+ if (!transform) return value as T;
260
+ return transform(value, { key: runKey, url: target });
261
+ };
262
+ const asyncOptions: UseAsyncDataOptions<T> = {
263
+ immediate,
264
+ lazy,
265
+ server,
266
+ watch: sources,
267
+ default: fallback,
268
+ pick,
269
+ deep,
270
+ dedupe,
271
+ refreshOnPageRefresh,
272
+ refreshInterval,
273
+ };
274
+ const reactiveSources = [request, method, query, params, body].filter(
275
+ isReactiveSource,
276
+ );
277
+ const isReactive = reactiveSources.length > 0;
278
+ // The request gives its key, which follows the request as it changes.
279
+ const derived = (): string | null | undefined => {
280
+ const target = url();
281
+ return target == null ? null : requestKey(target, requestOptions());
282
+ };
283
+ if (key === undefined && derived() !== undefined)
284
+ return useDmsAsyncData(
285
+ isReactive ? derived : derived(),
286
+ handler,
287
+ asyncOptions,
288
+ );
289
+ // The key the caller names, or none for a body without one, stays put while
290
+ // the request changes: the request is watched instead.
291
+ const watched =
292
+ isReactive && sources !== false
293
+ ? [...reactiveSources, ...(sources ?? [])]
294
+ : sources;
295
+ if (key === undefined)
296
+ return useDmsAsyncData(handler, undefined, {
297
+ ...asyncOptions,
298
+ watch: watched,
299
+ });
300
+ const named = () => (url() == null ? null : toValue(key));
301
+ const followsKey = isReactiveSource(request) || isReactiveSource(key);
302
+ return useDmsAsyncData(followsKey ? named : named(), handler, {
303
+ ...asyncOptions,
304
+ watch: watched,
305
+ });
306
+ }
@@ -0,0 +1,247 @@
1
+ import { usePage } from "@inertiajs/vue3";
2
+ import { ofetch } from "ofetch";
3
+ import {
4
+ type Component,
5
+ type EffectScope,
6
+ type InjectionKey,
7
+ type Plugin,
8
+ type Ref,
9
+ } from "vue";
10
+ import type { DmsPublicOptionsById } from "../public-options";
11
+ import {
12
+ type DmsAppContext,
13
+ type DmsAsyncData,
14
+ type DmsComponentPreloader,
15
+ type DmsComponentRegistrationOptions,
16
+ type DmsErrorData,
17
+ type DmsMiddlewareRegistrationOptions,
18
+ type DmsModuleOptions,
19
+ type DmsPluginRegistrationOptions,
20
+ type DmsPluginSetup,
21
+ type DmsRoute,
22
+ type DmsSharedPagePayload,
23
+ type RouteLocationRaw,
24
+ } from "./types";
25
+
26
+ export interface DmsFrontendSdk {
27
+ options: DmsModuleOptions;
28
+ registerComponent(
29
+ name: string,
30
+ component: Component,
31
+ options?: DmsComponentRegistrationOptions,
32
+ ): void;
33
+ registerPage(
34
+ name: string,
35
+ component: Component,
36
+ preload?: DmsComponentPreloader,
37
+ ): void;
38
+ registerDynamicPage(
39
+ name: string,
40
+ component: Component,
41
+ preload?: DmsComponentPreloader,
42
+ ): void;
43
+ registerLayout(
44
+ name: string,
45
+ component: Component,
46
+ preload?: DmsComponentPreloader,
47
+ ): void;
48
+ registerErrorPage(
49
+ component: Component,
50
+ preload?: DmsComponentPreloader,
51
+ ): void;
52
+ registerPlugin(
53
+ setup: DmsPluginSetup,
54
+ options?: DmsPluginRegistrationOptions,
55
+ ): void;
56
+ registerMiddleware(
57
+ name: string,
58
+ handler: DmsMiddleware,
59
+ options?: DmsMiddlewareRegistrationOptions,
60
+ ): void;
61
+ /**
62
+ * Sends a page visit the backend refuses with a typed 403 (its body is
63
+ * `code`) to `path` instead of the error page. The server answers with the
64
+ * redirect before anything renders, so a reload lands on `path` directly.
65
+ */
66
+ registerAccessRedirect(code: string, path: string): void;
67
+ provide<T>(key: string | InjectionKey<T>, value: T): void;
68
+ use(plugin: Plugin): void;
69
+ }
70
+ export interface DmsFrontendModule {
71
+ /**
72
+ * Put in front of the name of every component the module registers, so
73
+ * the components of two modules never collide: with `"Acme"`,
74
+ * `registerComponent("Banner", ...)` registers `AcmeBanner`, the name the
75
+ * backend sends and templates use.
76
+ */
77
+ componentPrefix?: string;
78
+ setup(sdk: DmsFrontendSdk): void | Promise<void>;
79
+ }
80
+ export interface DmsFrontendModuleRegistration {
81
+ /** The module's id in the generated module registry. */
82
+ id?: string;
83
+ /** The module's manifest name, which collision warnings name it by. */
84
+ name?: string;
85
+ module: DmsFrontendModule;
86
+ options: DmsModuleOptions;
87
+ }
88
+
89
+ /**
90
+ * The registrations with the public options the server runs with in place of
91
+ * the build's: `ajs dms start` computes them from the project's configuration
92
+ * and the deployment's variables. Unset, the build's options stand.
93
+ */
94
+ export function withPublicOptions(
95
+ registrations: DmsFrontendModuleRegistration[],
96
+ byId: DmsPublicOptionsById | undefined,
97
+ ): DmsFrontendModuleRegistration[] {
98
+ if (!byId) return registrations;
99
+ return registrations.map((registration) => {
100
+ const options = registration.id ? byId[registration.id] : undefined;
101
+ return options
102
+ ? {
103
+ ...registration,
104
+ options: { ...registration.options, public: options },
105
+ }
106
+ : registration;
107
+ });
108
+ }
109
+ /** A layer's `app/app.config.ts`, as `app-configs.generated.ts` lists them. */
110
+ export interface DmsLayerAppConfig {
111
+ /** The id of the frontend module the layer belongs to. */
112
+ module: string;
113
+ config: Record<string, unknown>;
114
+ }
115
+ // Async middleware without a redirect naturally infers Promise<void>.
116
+ // biome-ignore lint/suspicious/noConfusingVoidType: preserve that valid handler signature.
117
+ export type DmsMiddlewareResult = void | false | RouteLocationRaw;
118
+ export type DmsMiddleware = (
119
+ to: DmsRoute,
120
+ from: DmsRoute,
121
+ ) => DmsMiddlewareResult | Promise<DmsMiddlewareResult>;
122
+ export type DmsRuntimeHook = (...args: unknown[]) => void | Promise<void>;
123
+ export interface DmsRuntimeHooks {
124
+ hook(name: string, callback: DmsRuntimeHook): () => void;
125
+ callHook(name: string, ...args: unknown[]): Promise<void>;
126
+ }
127
+
128
+ export interface DmsComponentRegistration {
129
+ name: string;
130
+ component: Component;
131
+ /** The registering module, for a collision warning. */
132
+ module?: string;
133
+ /** Registered without the module's prefix, to replace another module's. */
134
+ unprefixed?: boolean;
135
+ }
136
+
137
+ export interface DmsFrontendEntry {
138
+ component: Component;
139
+ preload?: DmsComponentPreloader;
140
+ }
141
+
142
+ export interface DmsAsyncComponent {
143
+ __asyncLoader?: DmsComponentPreloader;
144
+ }
145
+
146
+ interface DmsPluginRegistration {
147
+ setup: DmsPluginSetup;
148
+ clientOnly: boolean;
149
+ enforce: NonNullable<DmsPluginRegistrationOptions["enforce"]>;
150
+ }
151
+
152
+ export const PLUGIN_ORDER = { pre: 0, default: 1, post: 2 } as const;
153
+
154
+ /**
155
+ * What the frontend modules registered through their SDK. Modules are set up
156
+ * by descending priority, so the first one to claim a name keeps it.
157
+ */
158
+ export interface DmsFrontendRegistry {
159
+ components: Map<string, DmsComponentRegistration>;
160
+ pages: Map<string, DmsFrontendEntry>;
161
+ dynamicPages: Map<string, DmsFrontendEntry>;
162
+ layouts: Map<string, DmsFrontendEntry>;
163
+ errorPage?: DmsFrontendEntry;
164
+ plugins: Plugin[];
165
+ pluginSetups: DmsPluginRegistration[];
166
+ middleware: DmsMiddleware[];
167
+ namedMiddleware: Map<string, DmsMiddleware>;
168
+ /** The middlewares registered with `parallel: true`. */
169
+ parallelMiddleware: WeakSet<DmsMiddleware>;
170
+ accessRedirects: Map<string, string>;
171
+ injections: Map<string | symbol, unknown>;
172
+ }
173
+
174
+ export interface DmsFrontendRuntime {
175
+ sharedState: Map<string, Ref<unknown>>;
176
+ /** Reactive: `useDmsData` follows a key as it appears and is dropped. */
177
+ asyncData: Map<string, DmsAsyncData<unknown>>;
178
+ /** The server's data, which the browser takes over once per key. */
179
+ hydratedAsyncData: Map<string, unknown>;
180
+ /** The server's failures, which the browser takes over once per key. */
181
+ hydratedAsyncErrors: Map<string, DmsErrorData>;
182
+ /** The state keys a server render hands to the browser. */
183
+ transferredState: Set<string>;
184
+ /**
185
+ * Whether the browser is still hydrating the page the server rendered,
186
+ * whose first render must match the server's HTML.
187
+ */
188
+ hydrating: boolean;
189
+ /**
190
+ * The runs a server render started outside any component, which no
191
+ * `onServerPrefetch` waits for.
192
+ */
193
+ serverPrefetches: Set<Promise<void>>;
194
+ /** The runs in flight, which a server render aborts once it is over. */
195
+ asyncDataRuns: Set<AbortController>;
196
+ /** The data some component or caller shows: `refreshDmsPageData`'s. */
197
+ boundAsyncData: Set<{ pageRefresh(): Promise<void> | undefined }>;
198
+ /** Orders the data no caller shows, the least recently shown first. */
199
+ asyncDataClock: number;
200
+ /**
201
+ * The account and workspace the keys belong to (`setDmsAsyncDataScope`):
202
+ * the same key of another scope is another entry.
203
+ */
204
+ asyncDataScope?: string;
205
+ /**
206
+ * Runs an async data handler; a server render marks the requests it makes
207
+ * as transferred (see `inAsyncDataHandler`).
208
+ */
209
+ runAsyncDataHandler?: <R>(run: () => R) => R;
210
+ /** Whether the code running is an async data handler's. */
211
+ inAsyncDataHandler?: () => boolean;
212
+ /** Whether to warn about what a development render does wrong. */
213
+ devWarnings?: boolean;
214
+ runtimeHooks: Map<string, DmsRuntimeHook[]>;
215
+ serverFetch?: typeof ofetch;
216
+ currentError: Ref<Error | null>;
217
+ route: DmsRoute;
218
+ currentRoutePattern?: string;
219
+ appContext?: DmsAppContext;
220
+ isServer: boolean;
221
+ serverRedirect?: string;
222
+ pendingNavigation?: Promise<void>;
223
+ hasNavigationListener: boolean;
224
+ /** Whether the page `<Suspense>` waits on the page being navigated to. */
225
+ pageLoading: Ref<boolean>;
226
+ /** The site layout last copied into the state, as the page props hold it. */
227
+ appliedShared?: DmsSharedPagePayload;
228
+ /** Whether Inertia is putting back a page from the browser history. */
229
+ restoringHistory?: boolean;
230
+ /**
231
+ * Identifies the page on screen (its path, or the error page shown at that
232
+ * path), whose meta `route.meta` holds.
233
+ */
234
+ pageKey?: string;
235
+ /** The `Cookie` header of the request a server runtime renders. */
236
+ requestCookies?: string;
237
+ /**
238
+ * Owns the effects plugins create while they set up. A server render stops
239
+ * it once done: nothing else would, since a server app is never unmounted.
240
+ */
241
+ scope: EffectScope;
242
+ /** The Inertia page a server runtime renders: see `useDmsInertiaPage`. */
243
+ inertiaPage?: DmsInertiaPage;
244
+ }
245
+
246
+ /** Inertia's page, as `usePage()` returns it. */
247
+ export type DmsInertiaPage = ReturnType<typeof usePage>;