@revojs/vue 0.2.64 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { type FetchOptions } from "@revojs/server";
2
2
  import { type RouterContext, type Scope } from "revojs";
3
+ import type { StandardSchemaV1 } from "@standard-schema/spec";
3
4
  import type { Awaitable } from "revojs/shared";
4
5
  import { type Component, type ComputedRef, type MaybeRefOrGetter, type Ref, type ShallowRef, type WatchSource, type WritableComputedRef } from "vue";
5
6
  export type AsyncStatus = "idle" | "pending" | "success" | "error";
@@ -78,9 +79,24 @@ export declare function useRouter(): {
78
79
  };
79
80
  export declare function useState<T>(name: MaybeRefOrGetter<string>): Ref<T | undefined>;
80
81
  export declare function useState<T>(name: MaybeRefOrGetter<string>, value: T): Ref<T>;
82
+ export declare function useStateVersion(scope: Scope, name: string): Ref<number, number>;
83
+ export declare function writeState<T>(scope: Scope, name: string, value: T): void;
81
84
  export declare function useAsync<T, TError = Error>(name: MaybeRefOrGetter<string>, invoke: (context: AsyncContext) => Awaitable<T>, options?: AsyncOptions<T, TError>): Promise<AsyncResult<T, TError>> & AsyncResult<T, TError>;
82
85
  export declare function useFetch<T, TError = Error>(input: MaybeRefOrGetter<string>, options?: FetchOptions & AsyncOptions<T, TError>): Promise<AsyncResult<T, TError>> & AsyncResult<T, TError>;
83
86
  export declare function refreshAsync(input?: string | Array<string>): Promise<void[] | undefined>;
87
+ export declare function useAction<TInput extends Array<unknown>, TResult>(invoke: (...input: TInput) => Promise<TResult>): {
88
+ isLoading: Ref<boolean, boolean>;
89
+ error: ShallowRef<unknown, unknown>;
90
+ execute: (...input: TInput) => Promise<TResult>;
91
+ };
92
+ export declare function useForm<TSchema extends StandardSchemaV1>(schema: TSchema, defaults: StandardSchemaV1.InferInput<TSchema>, invoke: (value: StandardSchemaV1.InferOutput<TSchema>) => Promise<void>): {
93
+ form: Ref<StandardSchemaV1.InferInput<TSchema>, StandardSchemaV1.InferInput<TSchema>>;
94
+ issues: ShallowRef<readonly StandardSchemaV1.Issue[] | undefined, readonly StandardSchemaV1.Issue[] | undefined>;
95
+ isLoading: Ref<boolean, boolean>;
96
+ error: ShallowRef<unknown, unknown>;
97
+ execute: () => Promise<void>;
98
+ reset: () => void;
99
+ };
84
100
  export declare function useWebSocket(url: MaybeRefOrGetter<string>, options?: WebSocketOptions): {
85
101
  status: Ref<WebSocketStatus, WebSocketStatus>;
86
102
  send: (data: string | Blob | BufferSource) => boolean;
@@ -92,4 +108,5 @@ export declare const NAVIGATE_HOOK: import("revojs").Descriptor<import("revojs")
92
108
  export declare const ASYNC_CONTEXT: import("revojs").Descriptor<AsyncRegistry>;
93
109
  export declare const REFRESH_ASYNC_HOOK: import("revojs").Descriptor<import("revojs").Hook<void | string[], Promise<void>>>;
94
110
  export declare const CLIENT_ERROR_CONTEXT: import("revojs").Descriptor<ShallowRef<Error>>;
111
+ export declare const STATE_VERSION_CONTEXT: import("revojs").Descriptor<Map<string, Ref<number, number>>>;
95
112
  export declare const CLIENT_ROUTER_CONTEXT: import("revojs").Descriptor<ShallowRef<RouterContext<Component>>>;
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import pages from "#pages";
2
2
  import { isClient, isServer, isServerSideRendering } from "#variables";
3
3
  import { fetch, getState, sendRedirect, setState, useUrl } from "@revojs/server";
4
- import { Router, defineContext, defineHook, dispatchHook, getContext, registerHook, setContext } from "revojs";
5
- import { Suspense, computed, createApp, createCommentVNode, createSSRApp, customRef, defineAsyncComponent, defineComponent, getCurrentInstance, getCurrentScope, h, inject, onErrorCaptured, onMounted, onScopeDispose, onServerPrefetch, ref, shallowRef, toValue, warn, watch } from "vue";
4
+ import { Router, defineContext, defineHook, dispatchHook, getContext, registerHook, setContext, useRouteResolverContext } from "revojs";
5
+ import { Suspense, computed, createApp, createCommentVNode, createSSRApp, customRef, defineAsyncComponent, defineComponent, getCurrentInstance, getCurrentScope, h, inject, onErrorCaptured, onMounted, onScopeDispose, onServerPrefetch, ref, shallowRef, toRaw, toValue, warn, watch } from "vue";
6
6
  //#region src/index.ts
7
7
  var AsyncEntry = class {
8
8
  scope;
@@ -24,7 +24,7 @@ var AsyncEntry = class {
24
24
  }
25
25
  write(value) {
26
26
  this.data.value = value;
27
- setState(this.scope, this.key, value);
27
+ writeState(this.scope, this.key, value);
28
28
  }
29
29
  cancel(reason) {
30
30
  const controller = this.controller;
@@ -86,8 +86,13 @@ function useRouter() {
86
86
  const navigate = (route) => {
87
87
  if (isClient) {
88
88
  const url = new URL(route, globalThis.location.href);
89
+ const pathname = globalThis.location.pathname;
89
90
  if (url.href !== globalThis.location.href) globalThis.history.pushState(globalThis.history.state, "", url);
90
- dispatchHook(scope, NAVIGATE_HOOK);
91
+ (async () => {
92
+ await dispatchHook(scope, NAVIGATE_HOOK);
93
+ if (url.hash) globalThis.document.getElementById(decodeURIComponent(url.hash.slice(1)))?.scrollIntoView();
94
+ else if (url.pathname !== pathname) globalThis.scrollTo(0, 0);
95
+ })();
91
96
  }
92
97
  if (isServer) throw sendRedirect(scope, route);
93
98
  };
@@ -108,14 +113,29 @@ function useState(name, value) {
108
113
  return customRef((track, trigger) => ({
109
114
  get() {
110
115
  track();
116
+ useStateVersion(scope, toValue(name)).value;
111
117
  return getState(scope, toValue(name)) ?? value;
112
118
  },
113
119
  set(update) {
114
- setState(scope, toValue(name), update);
120
+ writeState(scope, toValue(name), update);
115
121
  trigger();
116
122
  }
117
123
  }));
118
124
  }
125
+ function useStateVersion(scope, name) {
126
+ let versions = getContext(scope, STATE_VERSION_CONTEXT);
127
+ if (!Object.hasOwn(scope.context, STATE_VERSION_CONTEXT)) versions = setContext(scope, STATE_VERSION_CONTEXT, /* @__PURE__ */ new Map());
128
+ let version = versions.get(name);
129
+ if (version === void 0) {
130
+ version = ref(0);
131
+ versions.set(name, version);
132
+ }
133
+ return version;
134
+ }
135
+ function writeState(scope, name, value) {
136
+ setState(scope, name, value);
137
+ useStateVersion(scope, name).value += 1;
138
+ }
119
139
  function useAsync(name, invoke, options) {
120
140
  const scope = useScope();
121
141
  const registry = useAsyncContext(scope);
@@ -150,7 +170,6 @@ function useAsync(name, invoke, options) {
150
170
  }
151
171
  } catch (error) {
152
172
  if (entry.controller === controller) {
153
- entry.write(void 0);
154
173
  entry.error.value = error;
155
174
  entry.status.value = "error";
156
175
  await options?.catch?.(error);
@@ -233,6 +252,60 @@ async function refreshAsync(input) {
233
252
  const scope = useScope();
234
253
  if (isClient) return await dispatchHook(scope, REFRESH_ASYNC_HOOK, input === void 0 ? void 0 : Array.isArray(input) ? input : [input]);
235
254
  }
255
+ function useAction(invoke) {
256
+ const isLoading = ref(false);
257
+ const error = shallowRef();
258
+ let running;
259
+ const run = async (...input) => {
260
+ isLoading.value = true;
261
+ error.value = void 0;
262
+ try {
263
+ return await invoke(...input);
264
+ } catch (cause) {
265
+ error.value = cause;
266
+ throw cause;
267
+ } finally {
268
+ isLoading.value = false;
269
+ running = void 0;
270
+ }
271
+ };
272
+ const execute = async (...input) => {
273
+ running ??= run(...input);
274
+ return await running;
275
+ };
276
+ return {
277
+ isLoading,
278
+ error,
279
+ execute
280
+ };
281
+ }
282
+ function useForm(schema, defaults, invoke) {
283
+ const form = ref(structuredClone(defaults));
284
+ const issues = shallowRef();
285
+ const action = useAction(invoke);
286
+ const execute = async () => {
287
+ if (action.isLoading.value) return;
288
+ const result = await schema["~standard"].validate(toRaw(form.value));
289
+ if (result.issues) {
290
+ issues.value = result.issues;
291
+ return;
292
+ }
293
+ issues.value = void 0;
294
+ await action.execute(result.value);
295
+ };
296
+ const reset = () => {
297
+ form.value = structuredClone(defaults);
298
+ issues.value = void 0;
299
+ };
300
+ return {
301
+ form,
302
+ issues,
303
+ isLoading: action.isLoading,
304
+ error: action.error,
305
+ execute,
306
+ reset
307
+ };
308
+ }
236
309
  function useWebSocket(url, options = {}) {
237
310
  const status = ref("closed");
238
311
  const minDelay = options.minDelay ?? 1e3;
@@ -324,19 +397,44 @@ async function createVueApp(scope) {
324
397
  const router = new Router();
325
398
  const error = shallowRef();
326
399
  const context = shallowRef();
400
+ const loaders = /* @__PURE__ */ new Map();
327
401
  for (const entries of Object.values(pages)) for (const [path, resolve] of Object.entries(entries)) {
328
402
  const segments = path.split("/");
329
403
  for (const attribute of segments.pop()?.split(".") ?? []) {
330
404
  if (attribute === "page") continue;
331
405
  segments.push(attribute);
332
406
  }
333
- router.use(segments, defineAsyncComponent(resolve));
407
+ const component = defineAsyncComponent(resolve);
408
+ loaders.set(component, resolve);
409
+ router.use(segments, component);
334
410
  }
335
411
  const registry = new AsyncRegistry(isClient && isServerSideRendering);
412
+ let navigation = 0;
336
413
  const resolve = async () => {
414
+ navigation += 1;
415
+ const current = navigation;
337
416
  const path = useUrl(scope).pathname.replace(/^\/+/, "").replace(/\/+$/, "");
338
417
  const segments = path === "" ? [] : path.split("/");
339
- context.value = await router.resolve(scope, segments);
418
+ const resolved = await router.resolve(scope, segments);
419
+ if (current === navigation) {
420
+ error.value = void 0;
421
+ context.value = resolved;
422
+ }
423
+ };
424
+ const preload = async (href) => {
425
+ const path = new URL(href, globalThis.location.href).pathname.replace(/^\/+/, "").replace(/\/+$/, "");
426
+ const segments = path === "" ? [] : path.split("/");
427
+ const resolver = useRouteResolverContext(scope);
428
+ let resolved = segments;
429
+ if (typeof resolver === "function") resolved = await resolver(segments);
430
+ if (resolved === false) return;
431
+ const { match, hooks } = router.match(resolved);
432
+ const chunks = new Array();
433
+ for (const component of [...hooks, match]) {
434
+ const load = component ? loaders.get(component) : void 0;
435
+ if (load) chunks.push(load());
436
+ }
437
+ await Promise.allSettled(chunks);
340
438
  };
341
439
  const navigate = async () => {
342
440
  registry.isHydrating = false;
@@ -347,7 +445,13 @@ async function createVueApp(scope) {
347
445
  setContext(scope, CLIENT_ERROR_CONTEXT, error);
348
446
  setContext(scope, CLIENT_ROUTER_CONTEXT, context);
349
447
  await resolve();
350
- const app = (isServerSideRendering ? createSSRApp(h(Main, { popstate: navigate })) : createApp(h(Main, { popstate: navigate }))).provide("REVOJS_SCOPE", scope);
448
+ const app = (isServerSideRendering ? createSSRApp(h(Main, {
449
+ popstate: navigate,
450
+ preload
451
+ })) : createApp(h(Main, {
452
+ popstate: navigate,
453
+ preload
454
+ }))).provide("REVOJS_SCOPE", scope);
351
455
  app.config.warnHandler = (message, instance, trace) => {
352
456
  if (message.includes("Invalid vnode type when creating vnode") || message.includes("Component <Anonymous> is missing template or render function")) return;
353
457
  warn(message, instance, trace);
@@ -357,11 +461,17 @@ async function createVueApp(scope) {
357
461
  }
358
462
  var Main = defineComponent({
359
463
  name: "Main",
360
- props: { popstate: {
361
- type: Function,
362
- required: true
363
- } },
364
- setup({ popstate }) {
464
+ props: {
465
+ popstate: {
466
+ type: Function,
467
+ required: true
468
+ },
469
+ preload: {
470
+ type: Function,
471
+ required: true
472
+ }
473
+ },
474
+ setup({ popstate, preload }) {
365
475
  const scope = useScope();
366
476
  const error = useError(scope);
367
477
  const context = useClientRouterContext(scope);
@@ -378,24 +488,40 @@ var Main = defineComponent({
378
488
  event.preventDefault();
379
489
  navigate(url.pathname + url.search + url.hash);
380
490
  };
491
+ const preloaded = /* @__PURE__ */ new Set();
492
+ const preloadLink = (anchor) => {
493
+ const url = new URL(anchor.href);
494
+ if (url.origin !== globalThis.location.origin || anchor.hasAttribute("download") || anchor.relList.contains("external") || preloaded.has(url.pathname)) return;
495
+ preloaded.add(url.pathname);
496
+ preload(url.pathname);
497
+ };
498
+ const preloadVisible = () => {
499
+ for (const anchor of globalThis.document.querySelectorAll("a[href]")) if (anchor instanceof HTMLAnchorElement) preloadLink(anchor);
500
+ };
501
+ const schedulePreload = () => {
502
+ if ("requestIdleCallback" in globalThis) {
503
+ requestIdleCallback(preloadVisible);
504
+ return;
505
+ }
506
+ setTimeout(preloadVisible, 1e3);
507
+ };
508
+ const hover = (event) => {
509
+ const anchor = event.target instanceof Element ? event.target.closest("a[href]") : null;
510
+ if (anchor instanceof HTMLAnchorElement) preloadLink(anchor);
511
+ };
381
512
  globalThis.addEventListener("popstate", popstate);
382
513
  globalThis.document.addEventListener("click", click);
514
+ globalThis.document.addEventListener("pointerover", hover);
515
+ globalThis.document.addEventListener("focusin", hover);
516
+ const unregister = registerHook(scope, NAVIGATE_HOOK, schedulePreload);
383
517
  onScopeDispose(() => {
384
518
  globalThis.removeEventListener("popstate", popstate);
385
519
  globalThis.document.removeEventListener("click", click);
520
+ globalThis.document.removeEventListener("pointerover", hover);
521
+ globalThis.document.removeEventListener("focusin", hover);
522
+ unregister();
386
523
  });
387
- onMounted(() => {
388
- const preload = async () => {
389
- const chunks = new Array();
390
- for (const entries of Object.values(pages)) for (const load of Object.values(entries)) chunks.push(load());
391
- await Promise.allSettled(chunks);
392
- };
393
- if ("requestIdleCallback" in globalThis) {
394
- requestIdleCallback(() => void preload());
395
- return;
396
- }
397
- setTimeout(() => void preload(), 1e3);
398
- });
524
+ onMounted(schedulePreload);
399
525
  }
400
526
  onErrorCaptured((value) => {
401
527
  error.value = value;
@@ -408,6 +534,7 @@ var NAVIGATE_HOOK = defineHook("NAVIGATE_HOOK");
408
534
  var ASYNC_CONTEXT = defineContext("ASYNC_CONTEXT");
409
535
  var REFRESH_ASYNC_HOOK = defineHook("REFRESH_ASYNC_HOOK");
410
536
  var CLIENT_ERROR_CONTEXT = defineContext("CLIENT_ERROR_CONTEXT");
537
+ var STATE_VERSION_CONTEXT = defineContext("STATE_VERSION_CONTEXT");
411
538
  var CLIENT_ROUTER_CONTEXT = defineContext("CLIENT_ROUTER_CONTEXT");
412
539
  //#endregion
413
- export { ASYNC_CONTEXT, AsyncEntry, AsyncRegistry, CLIENT_ERROR_CONTEXT, CLIENT_ROUTER_CONTEXT, NAVIGATE_HOOK, REFRESH_ASYNC_HOOK, createVueApp, isRouterNavigation, refreshAsync, useAsync, useAsyncContext, useClientRouterContext, useError, useFetch, useRouter, useScope, useState, useWebSocket };
540
+ export { ASYNC_CONTEXT, AsyncEntry, AsyncRegistry, CLIENT_ERROR_CONTEXT, CLIENT_ROUTER_CONTEXT, NAVIGATE_HOOK, REFRESH_ASYNC_HOOK, STATE_VERSION_CONTEXT, createVueApp, isRouterNavigation, refreshAsync, useAction, useAsync, useAsyncContext, useClientRouterContext, useError, useFetch, useForm, useRouter, useScope, useState, useStateVersion, useWebSocket, writeState };
@@ -1,6 +1,6 @@
1
1
  import { createVueApp, useError } from "../../index.js";
2
2
  import { isServerSideRendering } from "#variables";
3
- import { defineRoute, sendOk, useServer } from "@revojs/server";
3
+ import { FetchError, defineRoute, sendOk, useServer } from "@revojs/server";
4
4
  import client from "#client";
5
5
  import manifest from "#client/manifest";
6
6
  import { renderToString } from "vue/server-renderer";
@@ -16,9 +16,11 @@ var get_default = defineRoute({ async fetch(scope) {
16
16
  const html = await renderToString(app, context);
17
17
  content = content.replace("<!-- APP -->", () => html);
18
18
  } catch (error) {
19
+ if (captured.value instanceof FetchError) throw captured.value.response;
19
20
  throw captured.value ?? error;
20
21
  }
21
22
  if (captured.value instanceof Response) throw captured.value;
23
+ if (captured.value instanceof FetchError) throw captured.value.response;
22
24
  let body = "";
23
25
  let container = "";
24
26
  for (const [target, teleport] of Object.entries(context.teleports ?? {})) if (target.toLowerCase() === "body") body += teleport;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@revojs/vue",
3
- "version": "0.2.64",
3
+ "version": "0.3.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -32,14 +32,14 @@
32
32
  "watch": "vite build --watch"
33
33
  },
34
34
  "dependencies": {
35
- "@revojs/client": "*",
36
- "@revojs/server": "*",
35
+ "@revojs/client": "0.3.0",
36
+ "@revojs/server": "0.3.0",
37
37
  "@standard-schema/spec": "^1.1.0",
38
38
  "@vitejs/plugin-vue": "^6.0.9",
39
- "revojs": "*",
39
+ "revojs": "0.3.0",
40
40
  "vue": "^3.5.43"
41
41
  },
42
42
  "devDependencies": {
43
- "@revojs/tsconfig": "*"
43
+ "@revojs/tsconfig": "0.3.0"
44
44
  }
45
45
  }