@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,404 @@
1
+ import {
2
+ computed,
3
+ getCurrentInstance,
4
+ getCurrentScope,
5
+ type MaybeRefOrGetter,
6
+ nextTick,
7
+ onBeforeMount,
8
+ onMounted,
9
+ onScopeDispose,
10
+ onServerPrefetch,
11
+ type Ref,
12
+ type ShallowRef,
13
+ shallowRef,
14
+ toValue,
15
+ watch,
16
+ } from "vue";
17
+ import {
18
+ type DmsAsyncData,
19
+ type DmsAsyncDataHandler,
20
+ type DmsAsyncDataKey,
21
+ type DmsAsyncDataResult,
22
+ type UseAsyncDataOptions,
23
+ } from "./types";
24
+ import { useDmsRuntime } from "./state";
25
+ import { type DmsFrontendRuntime } from "./module-registration";
26
+ import {
27
+ type AsyncDataBindingHandle,
28
+ asyncDataEntry,
29
+ type AsyncDataEntry,
30
+ } from "./async-data-entry";
31
+
32
+ /**
33
+ * Loads data once for every component that shows the same key, with Nuxt's
34
+ * semantics: a server render waits for it before rendering, and transfers it,
35
+ * whether or not the caller awaits it; `lazy` only spares client navigations
36
+ * the wait; `server: false` leaves it to the browser.
37
+ *
38
+ * In the browser, data loaded before is shown at once and read again in the
39
+ * background on each mount and each return to its key (stale while
40
+ * revalidate), except while the page the server rendered hydrates: its data
41
+ * is the server's, just read.
42
+ *
43
+ * A call without a key cannot be transferred: the server leaves it to the
44
+ * browser, as `server: false` does, so the page hydrates with what it shows.
45
+ */
46
+ export function useDmsAsyncData<T>(
47
+ keyOrHandler: MaybeRefOrGetter<DmsAsyncDataKey> | DmsAsyncDataHandler<T>,
48
+ handler?: DmsAsyncDataHandler<T>,
49
+ callerOptions: UseAsyncDataOptions<T> = {},
50
+ ): DmsAsyncDataResult<T> {
51
+ const runtime = useDmsRuntime();
52
+ // A call outside any scope (a plugin past its first await, a callback)
53
+ // belongs to the application: its watchers and its polling live as long.
54
+ if (getCurrentScope() || !runtime.scope.active)
55
+ return createAsyncDataCall(runtime, keyOrHandler, handler, callerOptions);
56
+ return runtime.scope.run(() =>
57
+ createAsyncDataCall(runtime, keyOrHandler, handler, callerOptions),
58
+ )!;
59
+ }
60
+
61
+ function createAsyncDataCall<T>(
62
+ runtime: DmsFrontendRuntime,
63
+ keyOrHandler: MaybeRefOrGetter<DmsAsyncDataKey> | DmsAsyncDataHandler<T>,
64
+ handler: DmsAsyncDataHandler<T> | undefined,
65
+ callerOptions: UseAsyncDataOptions<T>,
66
+ ): DmsAsyncDataResult<T> {
67
+ // With a handler second, the first argument is the key: a string, or a ref
68
+ // or getter whose changes move the caller to the data of another key.
69
+ const keySource = handler
70
+ ? (keyOrHandler as MaybeRefOrGetter<DmsAsyncDataKey>)
71
+ : undefined;
72
+ const execute = handler ?? keyOrHandler;
73
+ if (typeof execute !== "function")
74
+ throw new Error("useDmsAsyncData requires a handler");
75
+ const keyless = keySource === undefined;
76
+ if (keyless && runtime.isServer && runtime.devWarnings)
77
+ console.warn(
78
+ "DMS: useDmsAsyncData without a key, or useDmsFetch with a body it cannot key, " +
79
+ "is left to the browser: give it a key for the server to render it.",
80
+ );
81
+ const options: UseAsyncDataOptions<T> = keyless
82
+ ? { ...callerOptions, server: false }
83
+ : callerOptions;
84
+ const inComponent = getCurrentInstance() !== null;
85
+ const consumer = shallowRef(
86
+ attachAsyncData(
87
+ runtime,
88
+ keyless ? undefined : (toValue(keySource) ?? null),
89
+ execute as DmsAsyncDataHandler<T>,
90
+ options,
91
+ inComponent,
92
+ ),
93
+ );
94
+ if (getCurrentScope()) onScopeDispose(() => consumer.value.unbind());
95
+ if (options.watch && options.watch.length)
96
+ refreshOnWatchedChange(consumer, options.watch);
97
+ // Registered in the browser too, though only a server calls it: Vue makes
98
+ // a component that prefetches an async boundary, which shifts the ids
99
+ // `useId()` gives the components after it, and both sides must agree. It
100
+ // waits for the key the caller is on by then, which its setup may have set.
101
+ let prefetched = false;
102
+ if (inComponent && options.immediate !== false && options.server !== false)
103
+ onServerPrefetch(() => {
104
+ prefetched = true;
105
+ return consumer.value.entry.settled();
106
+ });
107
+ pollAsyncData(runtime, consumer, options);
108
+ const { entry } = consumer.value;
109
+ const waited = startAsyncData(runtime, entry, options, inComponent);
110
+ const result =
111
+ keySource == null || typeof keySource === "string"
112
+ ? entry.result
113
+ : followAsyncDataKey(
114
+ runtime,
115
+ consumer,
116
+ keySource,
117
+ execute as DmsAsyncDataHandler<T>,
118
+ options,
119
+ inComponent,
120
+ () => !prefetched,
121
+ );
122
+ return Object.assign(
123
+ Promise.resolve(waited).then(() => result),
124
+ result,
125
+ );
126
+ }
127
+
128
+ /**
129
+ * Reads the data again when a watched source changes, but when the caller's
130
+ * key moved in the same turn: the new key's own read already has the change.
131
+ */
132
+ function refreshOnWatchedChange<T>(
133
+ consumer: ShallowRef<AsyncDataConsumer<T>>,
134
+ sources: unknown[],
135
+ ): void {
136
+ let moved = false;
137
+ watch(
138
+ () => consumer.value.entry,
139
+ () => {
140
+ moved = true;
141
+ void nextTick(() => {
142
+ moved = false;
143
+ });
144
+ },
145
+ { flush: "sync" },
146
+ );
147
+ watch(sources as never[], () => {
148
+ if (!moved) void consumer.value.entry.result.refresh();
149
+ });
150
+ }
151
+
152
+ /**
153
+ * Reads the data again every `refreshInterval`, in the browser, following
154
+ * the interval as it changes: none, `0` or `null` stops it.
155
+ */
156
+ function pollAsyncData<T>(
157
+ runtime: DmsFrontendRuntime,
158
+ consumer: ShallowRef<AsyncDataConsumer<T>>,
159
+ options: UseAsyncDataOptions<T>,
160
+ ): void {
161
+ const interval = options.refreshInterval;
162
+ if (runtime.isServer || interval === undefined) return;
163
+ let timer: ReturnType<typeof setInterval> | undefined;
164
+ const stop = () => {
165
+ clearInterval(timer);
166
+ timer = undefined;
167
+ };
168
+ watch(
169
+ () => toValue(interval),
170
+ (period) => {
171
+ stop();
172
+ if (period && period > 0)
173
+ timer = setInterval(() => pollOnce(consumer, options), period);
174
+ },
175
+ { immediate: true },
176
+ );
177
+ if (getCurrentScope()) onScopeDispose(stop);
178
+ }
179
+
180
+ function pollOnce<T>(
181
+ consumer: ShallowRef<AsyncDataConsumer<T>>,
182
+ options: UseAsyncDataOptions<T>,
183
+ ): void {
184
+ if (typeof document !== "undefined" && document.visibilityState === "hidden")
185
+ return;
186
+ const result = consumer.value.entry.result;
187
+ // Data a caller waits to `execute()` is not read before it is.
188
+ if (result.status.value === "idle" && options.immediate === false) return;
189
+ void result.refresh({ dedupe: "defer" });
190
+ }
191
+
192
+ /**
193
+ * Starts the run a caller needs, where its side needs it, and returns what
194
+ * awaiting the call waits for.
195
+ */
196
+ function startAsyncData<T>(
197
+ runtime: DmsFrontendRuntime,
198
+ entry: AsyncDataEntry<T>,
199
+ options: UseAsyncDataOptions<T>,
200
+ inComponent: boolean,
201
+ ): Promise<void> | undefined {
202
+ const immediate = options.immediate !== false;
203
+ const onServer = options.server !== false;
204
+ const idle = entry.result.status.value === "idle";
205
+ if (runtime.isServer) {
206
+ if (!immediate || !onServer) return undefined;
207
+ // A render that already answers a redirect throws its HTML away: what it
208
+ // still has to read would reach nobody.
209
+ if (runtime.serverRedirect) return undefined;
210
+ if (idle && options.lazy) entry.schedule();
211
+ else if (idle) void entry.load();
212
+ const settled = entry.settled();
213
+ if (!inComponent) runtime.serverPrefetches.add(settled);
214
+ return settled;
215
+ }
216
+ // The server's render had the data once the setup awaited it, and before
217
+ // the component rendered: handed over at the same points, it gives what
218
+ // the setup derives from it, and so the HTML, the same values.
219
+ const adoption = entry.adoption();
220
+ if (adoption) {
221
+ if (inComponent) onBeforeMount(entry.adoptNow);
222
+ return adoption;
223
+ }
224
+ if (!immediate) return undefined;
225
+ const waitForData = () =>
226
+ !options.lazy && entry.result.data.value === null
227
+ ? entry.settled()
228
+ : undefined;
229
+ if (entry.running()) return waitForData();
230
+ if (!idle) {
231
+ // Loaded before: shown at once and read again in the background, except
232
+ // while the page the server rendered hydrates with the server's data, or
233
+ // when another caller of the page has just read it.
234
+ if (!runtime.hydrating && !entry.justReadForAnother()) void entry.load();
235
+ return waitForData();
236
+ }
237
+ // The server rendered the default in place of this data: the browser's
238
+ // first render must show it too, before the request leaves.
239
+ if (runtime.hydrating && (options.lazy || !onServer)) {
240
+ if (inComponent) onMounted(() => void entry.load());
241
+ else entry.schedule();
242
+ } else if (options.lazy) entry.schedule();
243
+ else void entry.load();
244
+ return !options.lazy && entry.result.pending.value
245
+ ? entry.settled()
246
+ : undefined;
247
+ }
248
+
249
+ interface AsyncDataConsumer<T> extends AsyncDataBindingHandle {
250
+ entry: AsyncDataEntry<T>;
251
+ /** Whether no other component showed the key when this one came. */
252
+ alone: boolean;
253
+ }
254
+
255
+ /**
256
+ * Binds a caller to the data of its key. A `null` key has no data to load:
257
+ * the caller holds its default, and nothing runs until it has a key.
258
+ */
259
+ function attachAsyncData<T>(
260
+ runtime: DmsFrontendRuntime,
261
+ key: string | null | undefined,
262
+ handler: DmsAsyncDataHandler<T>,
263
+ options: UseAsyncDataOptions<T>,
264
+ inComponent: boolean,
265
+ ): AsyncDataConsumer<T> {
266
+ const entry = asyncDataEntry(runtime, key ?? undefined, options, inComponent);
267
+ const alone = entry.unused();
268
+ if (key === null) return { entry, alone, unbind() {} };
269
+ return { entry, alone, ...entry.bind({ handler, options }) };
270
+ }
271
+
272
+ /**
273
+ * Moves a caller to the data of each key its reactive key takes, and returns
274
+ * refs that follow it there. Until the new key's data is in, the previous
275
+ * data stays on screen rather than the default flashing in its place: the
276
+ * caller's refs show it, and `placeholder` says so, while the new key's own
277
+ * data stays its own, so a run that fails or that a switch away aborts never
278
+ * leaves it another key's data. A key loaded before shows its own data at
279
+ * once, and is read again in the background.
280
+ *
281
+ * A new key is read where the first one would have been. A server reads it
282
+ * neither for `server: false` nor for `immediate: false`, and otherwise only
283
+ * while its render still waits for the caller: until the prefetch of the
284
+ * caller's component, or, outside a component, as a call made then. Past
285
+ * that point the server renders the default and the browser reads the key. A
286
+ * browser hydrating the server's HTML reads a key the server did not render
287
+ * once the component is mounted, so that its first render matches the HTML.
288
+ */
289
+ function followAsyncDataKey<T>(
290
+ runtime: DmsFrontendRuntime,
291
+ consumer: ShallowRef<AsyncDataConsumer<T>>,
292
+ keySource: MaybeRefOrGetter<DmsAsyncDataKey>,
293
+ handler: DmsAsyncDataHandler<T>,
294
+ options: UseAsyncDataOptions<T>,
295
+ inComponent: boolean,
296
+ serverWaits: () => boolean,
297
+ ): DmsAsyncData<T> {
298
+ // A read held back until the hydrating component is mounted.
299
+ let held = false;
300
+ let mounted = !(inComponent && runtime.hydrating);
301
+ if (!mounted)
302
+ onMounted(() => {
303
+ mounted = true;
304
+ const { entry } = consumer.value;
305
+ if (held && entry.result.status.value === "idle") void entry.load();
306
+ held = false;
307
+ });
308
+ const startOnServer = (entry: AsyncDataEntry<T>) => {
309
+ if (
310
+ options.server === false ||
311
+ options.immediate === false ||
312
+ !serverWaits()
313
+ )
314
+ return;
315
+ void entry.load();
316
+ if (!inComponent) runtime.serverPrefetches.add(entry.settled());
317
+ };
318
+ const current = () => consumer.value.entry.result;
319
+ // The previous key's data, shown until the current key settles.
320
+ const carried = shallowRef<{ value: T | null } | undefined>();
321
+ const settled = (result: DmsAsyncData<T>) =>
322
+ result.status.value === "success" || result.status.value === "error";
323
+ const shownData = (): T | null => {
324
+ const result = current();
325
+ return carried.value && !settled(result)
326
+ ? carried.value.value
327
+ : result.data.value;
328
+ };
329
+ watch(
330
+ () => settled(current()),
331
+ (done) => {
332
+ if (done) carried.value = undefined;
333
+ },
334
+ { flush: "sync" },
335
+ );
336
+ // Data written to the current key from elsewhere (`useDmsData`, another
337
+ // caller of the key) is its own: it replaces what is carried.
338
+ watch(
339
+ () => [current(), current().data.value] as const,
340
+ ([result], [previous]) => {
341
+ if (result === previous) carried.value = undefined;
342
+ },
343
+ { flush: "sync" },
344
+ );
345
+ watch(
346
+ () => toValue(keySource) ?? null,
347
+ (key) => {
348
+ const previous = current();
349
+ const shown = shownData();
350
+ const wasRunning = previous.pending.value;
351
+ // Past the setup, the server's data for the new key, if it rendered
352
+ // any, is taken over at once.
353
+ const next = attachAsyncData(runtime, key, handler, options, false);
354
+ const { entry } = next;
355
+ const carry =
356
+ next.alone && entry.result.status.value === "idle" && shown !== null;
357
+ // Unbound first: the previous key's run, aborted, settles it, which
358
+ // must not drop what is carried to the next key.
359
+ consumer.value.unbind();
360
+ consumer.value = next;
361
+ carried.value = carry ? { value: shown } : undefined;
362
+ held = false;
363
+ // `watch: false` keeps the caller on the key it names, without a run.
364
+ if (key === null || options.watch === false) return;
365
+ if (runtime.isServer) return startOnServer(entry);
366
+ const wanted =
367
+ options.immediate !== false || shown !== null || wasRunning;
368
+ if (!wanted) return;
369
+ if (!mounted) held = true;
370
+ else if (!inComponent && runtime.hydrating) entry.schedule();
371
+ else void entry.load();
372
+ },
373
+ { flush: "sync" },
374
+ );
375
+ const through = <V>(read: (result: DmsAsyncData<T>) => Ref<V>): Ref<V> =>
376
+ computed({
377
+ get: () => read(current()).value,
378
+ set: (value) => {
379
+ read(current()).value = value;
380
+ },
381
+ });
382
+ return {
383
+ data: computed({
384
+ get: shownData,
385
+ set: (value) => {
386
+ carried.value = undefined;
387
+ current().data.value = value;
388
+ },
389
+ }),
390
+ key: computed(() => current().key.value),
391
+ placeholder: computed(
392
+ () => carried.value !== undefined && !settled(current()),
393
+ ),
394
+ error: through((result) => result.error),
395
+ pending: computed(() => current().pending.value),
396
+ status: through((result) => result.status),
397
+ execute: (refreshOptions) => current().refresh(refreshOptions),
398
+ refresh: (refreshOptions) => current().refresh(refreshOptions),
399
+ clear: () => {
400
+ carried.value = undefined;
401
+ current().clear();
402
+ },
403
+ };
404
+ }