@queryweave/vue-router 0.1.0-alpha.1 → 0.1.0-beta.2
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 +17 -8
- package/dist/index.d.ts +7 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +47 -46
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
# `@queryweave/vue-router`
|
|
2
2
|
|
|
3
|
-
A `QueryAdapter` backed by Vue Router.
|
|
3
|
+
A `QueryAdapter` backed by Vue Router 4.4+ or 5.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
pnpm add @queryweave/vue-router
|
|
7
|
+
```
|
|
4
8
|
|
|
5
9
|
```ts
|
|
6
10
|
import { createVueRouterAdapter } from "@queryweave/vue-router";
|
|
7
11
|
|
|
8
|
-
const adapter = createVueRouterAdapter(router
|
|
9
|
-
|
|
10
|
-
});
|
|
12
|
+
const adapter = createVueRouterAdapter(router);
|
|
13
|
+
|
|
14
|
+
const result = await runtime.update({ page: 9 });
|
|
15
|
+
result.outcome; // "committed" | "refused" | "redirected" | "unchanged"
|
|
16
|
+
result.reason; // Vue Router's NavigationFailure when a guard refused
|
|
11
17
|
```
|
|
12
18
|
|
|
13
|
-
It normalizes router query values, keeps path and hash,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
without the Vue
|
|
19
|
+
It normalizes router query values, keeps path and hash, waits for `router.isReady()`, observes
|
|
20
|
+
route changes in a scope it owns, and reports a refused or redirected navigation as the transition's
|
|
21
|
+
outcome instead of throwing. It owns no codec logic, no validation logic, and no Vue binding logic,
|
|
22
|
+
and depends on `@queryweave/core` alone — router synchronization can be adopted without the Vue
|
|
23
|
+
binding.
|
|
17
24
|
|
|
18
25
|
Vue and Vue Router are peer dependencies.
|
|
26
|
+
|
|
27
|
+
Documentation: https://queryweave-docs.vercel.app/frameworks/vue-router/
|
package/dist/index.d.ts
CHANGED
|
@@ -1,24 +1,13 @@
|
|
|
1
|
+
import { Router } from "vue-router";
|
|
1
2
|
import { QueryAdapter } from "@queryweave/core";
|
|
2
|
-
import { NavigationFailure, Router } from "vue-router";
|
|
3
3
|
//#region src/index.d.ts
|
|
4
4
|
/**
|
|
5
5
|
* Vue Router adapter.
|
|
6
6
|
*
|
|
7
7
|
* This package owns synchronization only. It never decodes, validates, applies defaults, or
|
|
8
|
-
* duplicates the Vue binding.
|
|
8
|
+
* duplicates the Vue binding. Vue Router owns the text of the URL: values are handed over as
|
|
9
|
+
* entries and the router writes them in its own encoding and key order.
|
|
9
10
|
*/
|
|
10
|
-
/** Reported when a router transition is rejected or redirected. */
|
|
11
|
-
type VueRouterNavigationOutcome = {
|
|
12
|
-
readonly ok: true;
|
|
13
|
-
} | {
|
|
14
|
-
readonly ok: false;
|
|
15
|
-
readonly failure: NavigationFailure | Error;
|
|
16
|
-
};
|
|
17
|
-
/** Options accepted by {@link createVueRouterAdapter}. */
|
|
18
|
-
interface VueRouterAdapterOptions {
|
|
19
|
-
/** Invoked instead of throwing when the router rejects a transition. */
|
|
20
|
-
readonly onNavigationFailure?: ((outcome: VueRouterNavigationOutcome) => void) | undefined;
|
|
21
|
-
}
|
|
22
11
|
/** A Vue Router adapter with deterministic cleanup. */
|
|
23
12
|
interface VueRouterQueryAdapter extends QueryAdapter {
|
|
24
13
|
readonly router: Router;
|
|
@@ -28,9 +17,10 @@ interface VueRouterQueryAdapter extends QueryAdapter {
|
|
|
28
17
|
* Create an adapter backed by a Vue Router instance.
|
|
29
18
|
*
|
|
30
19
|
* Path and hash are preserved on every transition; unmanaged query keys are preserved by the
|
|
31
|
-
* runtime that owns the model.
|
|
20
|
+
* runtime that owns the model. A navigation guard that refuses or redirects is reported as the
|
|
21
|
+
* navigation result, never thrown; an error thrown by a guard is propagated.
|
|
32
22
|
*/
|
|
33
|
-
declare function createVueRouterAdapter(router: Router
|
|
23
|
+
declare function createVueRouterAdapter(router: Router): VueRouterQueryAdapter;
|
|
34
24
|
//#endregion
|
|
35
|
-
export {
|
|
25
|
+
export { VueRouterQueryAdapter, createVueRouterAdapter };
|
|
36
26
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":";;;;;;;;;;;UAyBiB,8BAA8B;WACpC,QAAQ;EACjB;;;;;;;;;iBAgDc,uBAAuB,QAAQ,SAAS"}
|
package/dist/index.js
CHANGED
|
@@ -1,35 +1,27 @@
|
|
|
1
|
-
import { watch } from "vue";
|
|
1
|
+
import { effectScope, watch } from "vue";
|
|
2
|
+
import { NavigationFailureType, isNavigationFailure } from "vue-router";
|
|
2
3
|
|
|
3
4
|
//#region src/index.ts
|
|
5
|
+
/**
|
|
6
|
+
* Vue Router parses into a plain object, so a key such as `constructor` picks up an inherited
|
|
7
|
+
* member as its first "value". Anything that is not a string or a bare key is skipped.
|
|
8
|
+
*/
|
|
4
9
|
function toEntries(query) {
|
|
5
10
|
const entries = [];
|
|
6
11
|
for (const [key, value] of Object.entries(query)) {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
if (typeof value === "string") {
|
|
12
|
-
entries.push([key, value]);
|
|
13
|
-
continue;
|
|
14
|
-
}
|
|
15
|
-
if (value === void 0) continue;
|
|
16
|
-
for (const item of value) entries.push([key, item ?? ""]);
|
|
12
|
+
const items = Array.isArray(value) ? value : [value];
|
|
13
|
+
for (const item of items) if (item === null) entries.push([key, ""]);
|
|
14
|
+
else if (typeof item === "string") entries.push([key, item]);
|
|
17
15
|
}
|
|
18
16
|
return entries;
|
|
19
17
|
}
|
|
20
18
|
function toRawQuery(output) {
|
|
21
|
-
const query =
|
|
19
|
+
const query = Object.create(null);
|
|
22
20
|
for (const [key, value] of output) {
|
|
23
21
|
const existing = query[key];
|
|
24
|
-
if (existing === void 0)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
if (typeof existing === "string") {
|
|
29
|
-
query[key] = [existing, value];
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
32
|
-
existing.push(value);
|
|
22
|
+
if (existing === void 0) query[key] = value;
|
|
23
|
+
else if (typeof existing === "string") query[key] = [existing, value];
|
|
24
|
+
else existing.push(value);
|
|
33
25
|
}
|
|
34
26
|
return query;
|
|
35
27
|
}
|
|
@@ -37,45 +29,53 @@ function toRawQuery(output) {
|
|
|
37
29
|
* Create an adapter backed by a Vue Router instance.
|
|
38
30
|
*
|
|
39
31
|
* Path and hash are preserved on every transition; unmanaged query keys are preserved by the
|
|
40
|
-
* runtime that owns the model.
|
|
32
|
+
* runtime that owns the model. A navigation guard that refuses or redirects is reported as the
|
|
33
|
+
* navigation result, never thrown; an error thrown by a guard is propagated.
|
|
41
34
|
*/
|
|
42
|
-
function createVueRouterAdapter(router
|
|
35
|
+
function createVueRouterAdapter(router) {
|
|
43
36
|
const listeners = /* @__PURE__ */ new Set();
|
|
44
|
-
let
|
|
37
|
+
let scope;
|
|
45
38
|
let disposed = false;
|
|
39
|
+
const assertActive = () => {
|
|
40
|
+
if (disposed) throw new Error("This Vue Router query adapter was disposed.");
|
|
41
|
+
};
|
|
46
42
|
const read = () => toEntries(router.currentRoute.value.query);
|
|
47
43
|
const notify = () => {
|
|
48
44
|
const input = read();
|
|
49
45
|
for (const listener of [...listeners]) listener(input);
|
|
50
46
|
};
|
|
51
|
-
const report = (outcome) => {
|
|
52
|
-
options.onNavigationFailure?.(outcome);
|
|
53
|
-
};
|
|
54
47
|
const navigate = async (next, mode) => {
|
|
55
|
-
|
|
48
|
+
assertActive();
|
|
49
|
+
await router.isReady();
|
|
56
50
|
const route = router.currentRoute.value;
|
|
57
|
-
const target = {
|
|
51
|
+
const target = router.resolve({
|
|
58
52
|
path: route.path,
|
|
59
53
|
hash: route.hash,
|
|
60
54
|
query: toRawQuery(next)
|
|
55
|
+
});
|
|
56
|
+
const failure = await (mode === "replace" ? router.replace(target) : router.push(target));
|
|
57
|
+
if (failure) return isNavigationFailure(failure, NavigationFailureType.duplicated) ? { outcome: "committed" } : {
|
|
58
|
+
outcome: "refused",
|
|
59
|
+
reason: failure
|
|
61
60
|
};
|
|
62
|
-
|
|
63
|
-
const failure = await (mode === "replace" ? router.replace(target) : router.push(target));
|
|
64
|
-
report(failure === void 0 || failure === null ? { ok: true } : {
|
|
65
|
-
ok: false,
|
|
66
|
-
failure
|
|
67
|
-
});
|
|
68
|
-
} catch (error) {
|
|
69
|
-
report({
|
|
70
|
-
ok: false,
|
|
71
|
-
failure: error instanceof Error ? error : new Error(String(error))
|
|
72
|
-
});
|
|
73
|
-
}
|
|
61
|
+
return router.currentRoute.value.fullPath === target.fullPath ? { outcome: "committed" } : { outcome: "redirected" };
|
|
74
62
|
};
|
|
63
|
+
/**
|
|
64
|
+
* The watcher lives in a scope the adapter owns, not in whichever component subscribed first,
|
|
65
|
+
* so it survives that component and stops only when the last subscriber leaves.
|
|
66
|
+
*/
|
|
75
67
|
const attach = () => {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
68
|
+
if (scope !== void 0) return;
|
|
69
|
+
scope = effectScope(true);
|
|
70
|
+
scope.run(() => {
|
|
71
|
+
watch(() => router.currentRoute.value.fullPath, () => {
|
|
72
|
+
notify();
|
|
73
|
+
}, { flush: "post" });
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
const detach = () => {
|
|
77
|
+
scope?.stop();
|
|
78
|
+
scope = void 0;
|
|
79
79
|
};
|
|
80
80
|
return {
|
|
81
81
|
router,
|
|
@@ -83,17 +83,18 @@ function createVueRouterAdapter(router, options = {}) {
|
|
|
83
83
|
push: async (next) => navigate(next, "push"),
|
|
84
84
|
replace: async (next) => navigate(next, "replace"),
|
|
85
85
|
subscribe: (listener) => {
|
|
86
|
+
assertActive();
|
|
86
87
|
attach();
|
|
87
88
|
listeners.add(listener);
|
|
88
89
|
return () => {
|
|
89
90
|
listeners.delete(listener);
|
|
91
|
+
if (listeners.size === 0) detach();
|
|
90
92
|
};
|
|
91
93
|
},
|
|
92
94
|
dispose: () => {
|
|
93
95
|
disposed = true;
|
|
94
96
|
listeners.clear();
|
|
95
|
-
|
|
96
|
-
stopWatching = void 0;
|
|
97
|
+
detach();
|
|
97
98
|
}
|
|
98
99
|
};
|
|
99
100
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import type {
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import type {\n QueryAdapter,\n QueryChangeListener,\n QueryEntry,\n QueryNavigationResult,\n QueryOutput,\n} from \"@queryweave/core\";\nimport { effectScope, watch, type EffectScope } from \"vue\";\nimport {\n isNavigationFailure,\n NavigationFailureType,\n type LocationQuery,\n type LocationQueryRaw,\n type Router,\n} from \"vue-router\";\n\n/**\n * Vue Router adapter.\n *\n * This package owns synchronization only. It never decodes, validates, applies defaults, or\n * duplicates the Vue binding. Vue Router owns the text of the URL: values are handed over as\n * entries and the router writes them in its own encoding and key order.\n */\n\n/** A Vue Router adapter with deterministic cleanup. */\nexport interface VueRouterQueryAdapter extends QueryAdapter {\n readonly router: Router;\n dispose(): void;\n}\n\n/**\n * Vue Router parses into a plain object, so a key such as `constructor` picks up an inherited\n * member as its first \"value\". Anything that is not a string or a bare key is skipped.\n */\nfunction toEntries(query: LocationQuery): QueryOutput {\n const entries: QueryEntry[] = [];\n for (const [key, value] of Object.entries(query)) {\n const items: readonly unknown[] = Array.isArray(value) ? value : [value];\n for (const item of items) {\n if (item === null) {\n entries.push([key, \"\"]);\n } else if (typeof item === \"string\") {\n entries.push([key, item]);\n }\n }\n }\n return entries;\n}\n\nfunction toRawQuery(output: QueryOutput): LocationQueryRaw {\n // A null prototype keeps a key such as `constructor` from resolving to an inherited member.\n const query: Record<string, string | string[]> = Object.create(null) as Record<\n string,\n string | string[]\n >;\n for (const [key, value] of output) {\n const existing = query[key];\n if (existing === undefined) {\n query[key] = value;\n } else if (typeof existing === \"string\") {\n query[key] = [existing, value];\n } else {\n existing.push(value);\n }\n }\n return query;\n}\n\n/**\n * Create an adapter backed by a Vue Router instance.\n *\n * Path and hash are preserved on every transition; unmanaged query keys are preserved by the\n * runtime that owns the model. A navigation guard that refuses or redirects is reported as the\n * navigation result, never thrown; an error thrown by a guard is propagated.\n */\nexport function createVueRouterAdapter(router: Router): VueRouterQueryAdapter {\n const listeners = new Set<QueryChangeListener>();\n let scope: EffectScope | undefined;\n let disposed = false;\n\n const assertActive = (): void => {\n if (disposed) {\n throw new Error(\"This Vue Router query adapter was disposed.\");\n }\n };\n\n const read = (): QueryOutput => toEntries(router.currentRoute.value.query);\n\n const notify = (): void => {\n const input = read();\n for (const listener of [...listeners]) {\n listener(input);\n }\n };\n\n const navigate = async (\n next: QueryOutput,\n mode: \"push\" | \"replace\",\n ): Promise<QueryNavigationResult> => {\n assertActive();\n // Before the initial navigation the current route is a placeholder with no path.\n await router.isReady();\n const route = router.currentRoute.value;\n const target = router.resolve({ path: route.path, hash: route.hash, query: toRawQuery(next) });\n const failure = await (mode === \"replace\" ? router.replace(target) : router.push(target));\n if (failure) {\n // The router already sits on the requested route; nothing was lost.\n return isNavigationFailure(failure, NavigationFailureType.duplicated)\n ? { outcome: \"committed\" }\n : { outcome: \"refused\", reason: failure };\n }\n return router.currentRoute.value.fullPath === target.fullPath\n ? { outcome: \"committed\" }\n : { outcome: \"redirected\" };\n };\n\n /**\n * The watcher lives in a scope the adapter owns, not in whichever component subscribed first,\n * so it survives that component and stops only when the last subscriber leaves.\n */\n const attach = (): void => {\n if (scope !== undefined) {\n return;\n }\n scope = effectScope(true);\n scope.run(() => {\n watch(\n () => router.currentRoute.value.fullPath,\n () => {\n notify();\n },\n { flush: \"post\" },\n );\n });\n };\n\n const detach = (): void => {\n scope?.stop();\n scope = undefined;\n };\n\n return {\n router,\n read,\n push: async (next) => navigate(next, \"push\"),\n replace: async (next) => navigate(next, \"replace\"),\n subscribe: (listener) => {\n assertActive();\n attach();\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n if (listeners.size === 0) {\n detach();\n }\n };\n },\n dispose: () => {\n disposed = true;\n listeners.clear();\n detach();\n },\n };\n}\n"],"mappings":";;;;;;;;AAkCA,SAAS,UAAU,OAAmC;CACpD,MAAM,UAAwB,CAAC;CAC/B,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,KAAK,GAAG;EAChD,MAAM,QAA4B,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC,KAAK;EACvE,KAAK,MAAM,QAAQ,OACjB,IAAI,SAAS,MACX,QAAQ,KAAK,CAAC,KAAK,EAAE,CAAC;OACjB,IAAI,OAAO,SAAS,UACzB,QAAQ,KAAK,CAAC,KAAK,IAAI,CAAC;CAG9B;CACA,OAAO;AACT;AAEA,SAAS,WAAW,QAAuC;CAEzD,MAAM,QAA2C,OAAO,OAAO,IAAI;CAInE,KAAK,MAAM,CAAC,KAAK,UAAU,QAAQ;EACjC,MAAM,WAAW,MAAM;EACvB,IAAI,aAAa,QACf,MAAM,OAAO;OACR,IAAI,OAAO,aAAa,UAC7B,MAAM,OAAO,CAAC,UAAU,KAAK;OAE7B,SAAS,KAAK,KAAK;CAEvB;CACA,OAAO;AACT;;;;;;;;AASA,SAAgB,uBAAuB,QAAuC;CAC5E,MAAM,4BAAY,IAAI,IAAyB;CAC/C,IAAI;CACJ,IAAI,WAAW;CAEf,MAAM,qBAA2B;EAC/B,IAAI,UACF,MAAM,IAAI,MAAM,6CAA6C;CAEjE;CAEA,MAAM,aAA0B,UAAU,OAAO,aAAa,MAAM,KAAK;CAEzE,MAAM,eAAqB;EACzB,MAAM,QAAQ,KAAK;EACnB,KAAK,MAAM,YAAY,CAAC,GAAG,SAAS,GAClC,SAAS,KAAK;CAElB;CAEA,MAAM,WAAW,OACf,MACA,SACmC;EACnC,aAAa;EAEb,MAAM,OAAO,QAAQ;EACrB,MAAM,QAAQ,OAAO,aAAa;EAClC,MAAM,SAAS,OAAO,QAAQ;GAAE,MAAM,MAAM;GAAM,MAAM,MAAM;GAAM,OAAO,WAAW,IAAI;EAAE,CAAC;EAC7F,MAAM,UAAU,OAAO,SAAS,YAAY,OAAO,QAAQ,MAAM,IAAI,OAAO,KAAK,MAAM;EACvF,IAAI,SAEF,OAAO,oBAAoB,SAAS,sBAAsB,UAAU,IAChE,EAAE,SAAS,YAAY,IACvB;GAAE,SAAS;GAAW,QAAQ;EAAQ;EAE5C,OAAO,OAAO,aAAa,MAAM,aAAa,OAAO,WACjD,EAAE,SAAS,YAAY,IACvB,EAAE,SAAS,aAAa;CAC9B;;;;;CAMA,MAAM,eAAqB;EACzB,IAAI,UAAU,QACZ;EAEF,QAAQ,YAAY,IAAI;EACxB,MAAM,UAAU;GACd,YACQ,OAAO,aAAa,MAAM,gBAC1B;IACJ,OAAO;GACT,GACA,EAAE,OAAO,OAAO,CAClB;EACF,CAAC;CACH;CAEA,MAAM,eAAqB;EACzB,OAAO,KAAK;EACZ,QAAQ;CACV;CAEA,OAAO;EACL;EACA;EACA,MAAM,OAAO,SAAS,SAAS,MAAM,MAAM;EAC3C,SAAS,OAAO,SAAS,SAAS,MAAM,SAAS;EACjD,YAAY,aAAa;GACvB,aAAa;GACb,OAAO;GACP,UAAU,IAAI,QAAQ;GACtB,aAAa;IACX,UAAU,OAAO,QAAQ;IACzB,IAAI,UAAU,SAAS,GACrB,OAAO;GAEX;EACF;EACA,eAAe;GACb,WAAW;GACX,UAAU,MAAM;GAChB,OAAO;EACT;CACF;AACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@queryweave/vue-router",
|
|
3
|
-
"version": "0.1.0-
|
|
3
|
+
"version": "0.1.0-beta.2",
|
|
4
4
|
"description": "Vue Router adapter contracts for QueryWeave.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"query",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"provenance": true
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@queryweave/core": "0.1.0-
|
|
41
|
+
"@queryweave/core": "0.1.0-beta.2"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"vue": "3.5.40",
|
|
@@ -47,10 +47,10 @@
|
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
49
|
"vue": ">=3.5.0 <4",
|
|
50
|
-
"vue-router": ">=
|
|
50
|
+
"vue-router": ">=4.4.0 <6"
|
|
51
51
|
},
|
|
52
52
|
"engines": {
|
|
53
|
-
"node": ">=
|
|
53
|
+
"node": ">=22.12.0"
|
|
54
54
|
},
|
|
55
55
|
"scripts": {
|
|
56
56
|
"build": "tsdown",
|