vue-router-citadel 0.1.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/CHANGELOG.md +139 -0
- package/LICENSE +21 -0
- package/README.md +104 -0
- package/dist/chunk-BPEUPHLM.js +1 -0
- package/dist/devtools-EZI2AQWB.js +2 -0
- package/dist/index.cjs +2 -0
- package/dist/index.d.cts +372 -0
- package/dist/index.d.ts +372 -0
- package/dist/index.js +1 -0
- package/package.json +106 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
|
|
6
|
+
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2026-02-17
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
#### Core
|
|
13
|
+
|
|
14
|
+
- `createNavigationCitadel(router, options?)` — main factory function
|
|
15
|
+
- Global and route-scoped navigation outposts
|
|
16
|
+
- Support for `beforeEach`, `beforeResolve`, `afterEach` hooks
|
|
17
|
+
- Verdict system: `ALLOW`, `BLOCK`, redirect
|
|
18
|
+
- TypeScript support with full type definitions
|
|
19
|
+
- Peer dependencies: `vue@^3.0.0`, `vue-router@^4.0.0 || ^5.0.0`
|
|
20
|
+
|
|
21
|
+
#### API Methods
|
|
22
|
+
|
|
23
|
+
- `citadel.deployOutpost(options)` — deploy one or multiple outposts (scope defaults to `'global'`)
|
|
24
|
+
- `citadel.abandonOutpost(scope, name)` — abandon outposts by scope and name
|
|
25
|
+
- `citadel.getOutpostNames(scope)` — get deployed outpost names
|
|
26
|
+
- `citadel.assignOutpostToRoute(routeName, outpostNames)` — dynamically assign outposts to routes
|
|
27
|
+
- `citadel.revokeOutpostFromRoute(routeName, outpostNames)` — dynamically remove outposts from
|
|
28
|
+
routes
|
|
29
|
+
- `citadel.destroy()` — remove all hooks and clear registry
|
|
30
|
+
|
|
31
|
+
#### Features
|
|
32
|
+
|
|
33
|
+
- Priority-based processing order for global and route outposts
|
|
34
|
+
- Route outposts inheritance from parent routes
|
|
35
|
+
- Route outposts deduplication with warning log
|
|
36
|
+
- Route validation for redirect returns
|
|
37
|
+
- Default error handler (`console.error` + `BLOCK`)
|
|
38
|
+
- Timeout support (`defaultTimeout`, `timeout`, `onTimeout`)
|
|
39
|
+
- Lazy outposts (`lazy: true`) — load handler modules on-demand for code splitting
|
|
40
|
+
- Type-safe outpost names via declaration merging (`GlobalOutpostRegistry`, `RouteOutpostRegistry`)
|
|
41
|
+
- Optional `scope` in outpost config (defaults to `'global'`)
|
|
42
|
+
|
|
43
|
+
#### Developer Experience
|
|
44
|
+
|
|
45
|
+
- `log` option — enable/disable non-critical logging (default: `__DEV__`)
|
|
46
|
+
- `logger` option — custom logger with `CitadelLogger` interface (default: `createDefaultLogger()`)
|
|
47
|
+
- Critical events (errors, timeouts, missing routes) always logged regardless of `log` setting
|
|
48
|
+
- `createDefaultLogger()` — factory for default console logger with emoji prefixes
|
|
49
|
+
- `debug` option — logging + debugger breakpoints (default: `false`)
|
|
50
|
+
- Colored console output: 🔵 info, 🟡 warn, 🔴 error, 🟣 debug
|
|
51
|
+
- Named debug breakpoints: `navigation-start`, `before-outpost`, `patrol-stopped`, `timeout`,
|
|
52
|
+
`error-caught`, `devtools-init`, `devtools-inspector`
|
|
53
|
+
- Optimized processing — outposts sorted at deploy, direct calls from registry
|
|
54
|
+
|
|
55
|
+
#### NPM Scripts
|
|
56
|
+
|
|
57
|
+
- `lint` / `lint:fix` — ESLint check / auto-fix
|
|
58
|
+
- `check:lint` — ESLint check (alias)
|
|
59
|
+
- `check:types` — TypeScript type checking (`tsc --noEmit`)
|
|
60
|
+
- `check:format` — format check alias
|
|
61
|
+
- `check:size` — bundle size check ([size-limit](https://github.com/ai/size-limit), ≤4 KB)
|
|
62
|
+
- `check:all` — full validation chain (format + lint + types + tests + build + size)
|
|
63
|
+
- `release:check` — pre-release verification (check:all + pack --dry-run)
|
|
64
|
+
- `release:publish` — publish to npm with full checks
|
|
65
|
+
- `release:publish:beta` — publish beta version
|
|
66
|
+
|
|
67
|
+
#### Vue DevTools Integration
|
|
68
|
+
|
|
69
|
+
- `devtools` option — enable/disable Vue DevTools integration (default: `__DEV__`)
|
|
70
|
+
- Custom inspector with outpost tree (Global/Route groups)
|
|
71
|
+
- Tags showing priority and hooks count
|
|
72
|
+
- State panel with outpost details (name, scope, priority, hooks, timeout)
|
|
73
|
+
- Auto-refresh on deploy/abandon
|
|
74
|
+
- Vue Plugin API integration via `app.use(citadel)`
|
|
75
|
+
- Tree-shakeable via dynamic import — devtools code eliminated when `devtools: false`
|
|
76
|
+
- **Settings panel** — runtime Log level selector (`Off | Log | Log + Debug`)
|
|
77
|
+
- Settings persist in `localStorage` with priority: `localStorage → citadel options → defaults`
|
|
78
|
+
|
|
79
|
+
#### Debug Handler
|
|
80
|
+
|
|
81
|
+
- `debugHandler` option — custom debug handler for reliable breakpoints (default:
|
|
82
|
+
`createDefaultDebugHandler()`)
|
|
83
|
+
- Exports: `DebugHandler`, `DebugPoint`, `DebugPoints`, `createDefaultDebugHandler`
|
|
84
|
+
- Solves bundler issue where `debugger` statements are stripped from dependencies
|
|
85
|
+
|
|
86
|
+
#### Types
|
|
87
|
+
|
|
88
|
+
- `NavigationOutpost` — outpost configuration interface (scope optional, defaults to `'global'`)
|
|
89
|
+
- `NavigationOutpostHandler` — handler function type
|
|
90
|
+
- `LazyOutpostLoader` — lazy loader function type for code splitting
|
|
91
|
+
- `NavigationOutpostContext` — handler context with verdicts, to, from, router, hook
|
|
92
|
+
- `NavigationCitadelOptions` — citadel creation options
|
|
93
|
+
- `NavigationCitadelAPI` — public API interface
|
|
94
|
+
|
|
95
|
+
#### Testing
|
|
96
|
+
|
|
97
|
+
- Vitest + happy-dom test setup
|
|
98
|
+
- 140 tests across 9 test files
|
|
99
|
+
- `__tests__/navigationCitadel.test.ts` — citadel creation, hooks, destroy
|
|
100
|
+
- `__tests__/navigationRegistry.test.ts` — registry CRUD, priority sorting
|
|
101
|
+
- `__tests__/navigationOutposts.test.ts` — patrol logic, verdicts, redirects
|
|
102
|
+
- `__tests__/timeout.test.ts` — timeout handling, onTimeout callback
|
|
103
|
+
- `__tests__/integration.test.ts` — end-to-end navigation scenarios
|
|
104
|
+
- `__tests__/lazy.test.ts` — lazy loading, caching, retry, timeout behavior
|
|
105
|
+
- `__tests__/devtools-settings.test.ts` — DevTools settings, localStorage persistence
|
|
106
|
+
- `__tests__/debugHandler.test.ts` — debugHandler invocation, custom handlers
|
|
107
|
+
- VitePress testing guide (`docs/contributing/testing`) and test case reference
|
|
108
|
+
(`docs/contributing/test-cases`)
|
|
109
|
+
|
|
110
|
+
#### CI/CD
|
|
111
|
+
|
|
112
|
+
- GitHub Actions CI workflow (`ci.yml`) — `check:all` on push/PR to main/release
|
|
113
|
+
- GitHub Actions Release workflow (`release.yml`) — `check:all` + npm publish with provenance on
|
|
114
|
+
`v*` tags
|
|
115
|
+
|
|
116
|
+
#### Documentation
|
|
117
|
+
|
|
118
|
+
- `README.md` — concise project overview with quick start example, links to full docs
|
|
119
|
+
- `CONTRIBUTING.md` — concise contributor guide with link to full VitePress docs
|
|
120
|
+
- VitePress documentation site (`docs/`) — guides, API reference, examples, advanced patterns
|
|
121
|
+
- Error Handling — dedicated guide page with error flow diagram, `onError`, `onTimeout`
|
|
122
|
+
- Mermaid diagram legend with emoji markers (🟢🟡🔴🔵🟣) across all diagram pages
|
|
123
|
+
- API types documentation aligned with source code (lazy generics, async return types)
|
|
124
|
+
- Contributing guide — code style, naming conventions, architecture guidelines with examples
|
|
125
|
+
- Source docs consolidated into VitePress (`internals.md`, `testing.md` deleted, content
|
|
126
|
+
distributed)
|
|
127
|
+
- `examples/` directory removed — examples live in `docs/examples/` only
|
|
128
|
+
|
|
129
|
+
#### Infrastructure
|
|
130
|
+
|
|
131
|
+
- [commitlint](https://commitlint.js.org/) — commit message validation via husky `commit-msg` hook
|
|
132
|
+
- [Conventional Commits](https://www.conventionalcommits.org/) specification enforced
|
|
133
|
+
- Prettier `proseWrap: "preserve"` override for `docs/**/*.md` to preserve VitePress containers
|
|
134
|
+
- [ESLint](https://eslint.org/) 9 with flat config (`eslint.config.ts`) and `defineConfig`
|
|
135
|
+
- `typescript-eslint` with type-aware linting (`projectService: true`)
|
|
136
|
+
- `eslint-config-prettier` for conflict-free coexistence with Prettier
|
|
137
|
+
- 3 custom local rules: `switch-case-braces`, `jsdoc-comment-style`, `prefer-arrow-without-this`
|
|
138
|
+
- npm scripts: `lint`, `lint:fix`, `check:lint`; integrated into `check:all` and `lint-staged`
|
|
139
|
+
- [size-limit](https://github.com/ai/size-limit) — bundle size control (≤4 KB, minified + brotli)
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 - Present Dmytro Symonov "Kassaila"
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# 🏰 Vue Router Citadel
|
|
2
|
+
|
|
3
|
+
> _Place guards at the gates. Outposts along the way._
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/vue-router-citadel)
|
|
6
|
+
[](https://github.com/ai/size-limit)
|
|
7
|
+
[](https://github.com/Kassaila/vue-router-citadel/blob/main/LICENSE)
|
|
8
|
+
[](https://kassaila.github.io/vue-router-citadel/)
|
|
9
|
+
|
|
10
|
+
**Structured navigation defense for Vue Router 4 & 5.**
|
|
11
|
+
|
|
12
|
+
Citadel is a middleware-driven navigation control system for Vue Router that lets you build
|
|
13
|
+
**layered, predictable, and scalable route protection**.
|
|
14
|
+
|
|
15
|
+
Where Vue Router gives you guards at the entrance, Citadel introduces **navigation outposts** ---
|
|
16
|
+
internal checkpoints that control access, preload data, enforce permissions, and orchestrate complex
|
|
17
|
+
navigation flows.
|
|
18
|
+
|
|
19
|
+
Think of it as turning your router into a fortress.
|
|
20
|
+
|
|
21
|
+
🏰 Citadel → ✋ Outposts (🛡 Guards) → 📍 Final point
|
|
22
|
+
|
|
23
|
+
## ✨ Features
|
|
24
|
+
|
|
25
|
+
- 📜 **Verdict system** — return-based API — ALLOW, BLOCK, or redirect. No next() callback chains.
|
|
26
|
+
Clean, predictable control flow.
|
|
27
|
+
- ⭕ **Outpost scopes** — global for every navigation, route-scoped for specific pages
|
|
28
|
+
- 📋 **Priority-based execution** — deterministic outpost ordering with numeric priorities
|
|
29
|
+
- 🪝 **All navigation hooks** — beforeEach, beforeResolve, afterEach support per outpost
|
|
30
|
+
- 🔄 **Dynamic management** — deploy, abandon, and reassign outposts at runtime
|
|
31
|
+
- ⏱️ **Timeout control & error handling** — global and per-outpost timeout configuration. Custom
|
|
32
|
+
timeout and error handlers with redirect or block verdicts.
|
|
33
|
+
- 🔒 **Type-safe** — full TypeScript support with declaration merging for outpost names. IDE
|
|
34
|
+
autocomplete and compile-time validation.
|
|
35
|
+
- 🦥 **Lazy outposts** — dynamic imports with automatic caching for code splitting
|
|
36
|
+
- 🛠️ **Vue DevTools** — custom inspector for real-time outpost monitoring and debug modes
|
|
37
|
+
- 🔍 **Logging & debug** — configurable logger, custom implementations, and debug breakpoints
|
|
38
|
+
|
|
39
|
+
## 📦 Installation
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm install vue-router-citadel
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## 🚀 Quick Start
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
import { createRouter, createWebHistory } from 'vue-router';
|
|
49
|
+
import { createNavigationCitadel } from 'vue-router-citadel';
|
|
50
|
+
import { createApp } from 'vue';
|
|
51
|
+
import App from './App.vue';
|
|
52
|
+
|
|
53
|
+
const routes = [
|
|
54
|
+
{ path: '/', name: 'home', component: () => import('./pages/Home.vue') },
|
|
55
|
+
{ path: '/login', name: 'login', component: () => import('./pages/Login.vue') },
|
|
56
|
+
{
|
|
57
|
+
path: '/dashboard',
|
|
58
|
+
name: 'dashboard',
|
|
59
|
+
component: () => import('./pages/Dashboard.vue'),
|
|
60
|
+
meta: { requiresAuth: true },
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
const router = createRouter({
|
|
65
|
+
history: createWebHistory(),
|
|
66
|
+
routes,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const citadel = createNavigationCitadel(router, {
|
|
70
|
+
outposts: [
|
|
71
|
+
{
|
|
72
|
+
name: 'auth',
|
|
73
|
+
handler: ({ verdicts, to }) => {
|
|
74
|
+
const isAuthenticated = Boolean(localStorage.getItem('token'));
|
|
75
|
+
|
|
76
|
+
if (to.meta.requiresAuth && !isAuthenticated) {
|
|
77
|
+
return { name: 'login' };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return verdicts.ALLOW;
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const app = createApp(App);
|
|
87
|
+
|
|
88
|
+
app.use(router);
|
|
89
|
+
app.use(citadel);
|
|
90
|
+
app.mount('#app');
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## 📖 Documentation
|
|
94
|
+
|
|
95
|
+
**[View full documentation](https://kassaila.github.io/vue-router-citadel/)** — guides, API
|
|
96
|
+
reference, examples, and advanced patterns.
|
|
97
|
+
|
|
98
|
+
## 🤝 Contributing
|
|
99
|
+
|
|
100
|
+
Contributions are welcome! See **[CONTRIBUTING.md](./CONTRIBUTING.md)** for guidelines.
|
|
101
|
+
|
|
102
|
+
## 📄 License
|
|
103
|
+
|
|
104
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var n={BEFORE_EACH:"beforeEach",BEFORE_RESOLVE:"beforeResolve",AFTER_EACH:"afterEach"},r={ALLOW:"allow",BLOCK:"block"},s={GLOBAL:"global",ROUTE:"route"},p={NAVIGATION_START:"navigation-start",BEFORE_OUTPOST:"before-outpost",PATROL_STOPPED:"patrol-stopped",ERROR_CAUGHT:"error-caught",TIMEOUT:"timeout",DEVTOOLS_INIT:"devtools-init",DEVTOOLS_INSPECTOR:"devtools-inspector"};var g=typeof import.meta?.env<"u"?!!import.meta.env.DEV:globalThis.process?.env?.NODE_ENV!=="production",t="[\u{1F3F0} NavigationCitadel]",O=100;var d=()=>({info:(...o)=>console.info(`\u{1F535} ${t}`,...o),warn:(...o)=>console.log(`\u{1F7E1} ${t}`,...o),error:(...o)=>console.error(`\u{1F534} ${t}`,...o),debug:(...o)=>console.log(`\u{1F7E3} ${t} [DEBUG]`,...o)}),c=()=>()=>{debugger},m=(o,e,a,i)=>{e&&(a.debug(o),i?.(o));};export{n as a,r as b,s as c,p as d,g as e,t as f,O as g,d as h,c as i,m as j};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import {c,j as j$1,d as d$1,g as g$1,a,e}from'./chunk-BPEUPHLM.js';import {setupDevToolsPlugin}from'@vue/devtools-api';var L="navigation.citadel",O="Navigation Citadel",R="castle",b="https://kassaila.github.io/vue-router-citadel/logo_devtools.svg",u=L+".inspector",N="citadel-root",C="citadel-"+c.GLOBAL,D="citadel-"+c.ROUTE,p=16777215,x=4372867,y=3900150,A=9133302,P=16096779,B=15485081,d="vue-router-citadel:settings:",_="logLevel";var U=e=>e.priority??g$1,V=e=>e.hooks??[a.BEFORE_EACH],z=e=>({label:`priority: ${e}`,textColor:p,backgroundColor:x}),Y=e=>({label:e.length===1?e[0]:`${e.length} hooks`,textColor:p,backgroundColor:y}),K=()=>({label:"lazy",textColor:p,backgroundColor:B}),h=(e,o,n)=>{let t=[z(U(o)),Y(V(o))];return o.lazy&&t.push(K()),{id:`outpost-${n}-${e}`,label:e,tags:t}},X=e=>{let o=[],n=[];for(let t of e.globalSorted){let s=e.global.get(t);s&&o.push(h(t,s,c.GLOBAL));}for(let t of e.routeSorted){let s=e.route.get(t);s&&n.push(h(t,s,c.ROUTE));}return [{id:N,label:"Outposts",children:[{id:C,label:`Global (${o.length})`,tags:[{label:c.GLOBAL,textColor:p,backgroundColor:A}],children:o},{id:D,label:`Route (${n.length})`,tags:[{label:c.ROUTE,textColor:p,backgroundColor:P}],children:n}]}]},Z=(e,o)=>{let n=e.match(`^outpost-(${c.GLOBAL}|${c.ROUTE})-(.+)$`);if(!n)return null;let[,t,s]=n,i=(t===c.GLOBAL?o.global:o.route).get(s);return i?{"Outpost Details":[{key:"name",value:s},{key:"scope",value:t},{key:"priority",value:U(i)},{key:"hooks",value:V(i)},{key:"timeout",value:i.timeout??"none (uses default)"},{key:"lazy",value:i.lazy}]}:null},k=(e,o,n,t=false,s)=>{e.addInspector({id:u,label:O,icon:R}),e.on.getInspectorTree(a=>{a.inspectorId===u&&(a.rootNodes=X(o));}),e.on.getInspectorState(a=>{if(a.inspectorId!==u)return;let i=Z(a.nodeId,o);i&&(a.state=i);}),j$1(d$1.DEVTOOLS_INSPECTOR,t,n,s);},F=e=>{e.sendInspectorTree(u),e.sendInspectorState(u);};var r={OFF:"off",LOG:"log",DEBUG:"debug"};var M=()=>{if(typeof window>"u"||!window.localStorage)return null;try{let e=localStorage.getItem(d+_);return e===null?null:e===r.OFF||e===r.LOG||e===r.DEBUG?e:null}catch{return null}},j=e=>{if(!(typeof window>"u"||!window.localStorage))try{localStorage.setItem(d+_,e);}catch{}},q=(e,o,n)=>o?r.DEBUG:e??n?r.LOG:r.OFF,T=e=>{switch(e){case r.OFF:return {log:false,debug:false};case r.LOG:return {log:true,debug:false};case r.DEBUG:return {log:true,debug:true}}},J=e=>e.debug?r.DEBUG:e.log?r.LOG:r.OFF,w=(e,o,n)=>{let t=M();if(t!==null)return T(t);let s=q(e,o,n);return T(s)},H=(e,o)=>{let n=T(o);e.log=n.log,e.debug=n.debug,j(o);},$=e=>({logLevel:{label:"Log level",type:"choice",defaultValue:J(e),options:[{label:"Off",value:r.OFF},{label:"Log",value:r.LOG},{label:"Log + Debug",value:r.DEBUG}],component:"button-group"}});var g=null,de=(e$1,o,n,t,s,a,i)=>{let f=w(s,a,e);t.log=f.log,t.debug=f.debug,setupDevToolsPlugin({id:L,label:O,logo:b,packageName:"vue-router-citadel",homepage:"https://github.com/Kassaila/vue-router-citadel",enableEarlyProxy:true,app:e$1,settings:$(t)},c=>{g=c,c.on.setPluginSettings(S=>{S.key==="logLevel"&&H(t,S.newValue);}),k(c,o,n,t.debug,i);});},_e=()=>{g&&F(g);},Te=()=>{g=null;};
|
|
2
|
+
export{Te as clearDevtoolsApi,_e as notifyDevtoolsRefresh,de as setupDevtools};
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
'use strict';var devtoolsApi=require('@vue/devtools-api');var Vt=Object.defineProperty;var D=(t,o)=>()=>(t&&(o=t(t=0)),o);var Ut=(t,o)=>{for(var n in o)Vt(t,n,{get:o[n],enumerable:true});};exports.NavigationHooks=void 0;exports.NavigationOutpostVerdicts=void 0;exports.NavigationOutpostScopes=void 0;exports.DebugPoints=void 0;var G=D(()=>{exports.NavigationHooks={BEFORE_EACH:"beforeEach",BEFORE_RESOLVE:"beforeResolve",AFTER_EACH:"afterEach"},exports.NavigationOutpostVerdicts={ALLOW:"allow",BLOCK:"block"},exports.NavigationOutpostScopes={GLOBAL:"global",ROUTE:"route"},exports.DebugPoints={NAVIGATION_START:"navigation-start",BEFORE_OUTPOST:"before-outpost",PATROL_STOPPED:"patrol-stopped",ERROR_CAUGHT:"error-caught",TIMEOUT:"timeout",DEVTOOLS_INIT:"devtools-init",DEVTOOLS_INSPECTOR:"devtools-inspector"};});var k,I,V,P=D(()=>{k=typeof undefined<"u"?!!undefined.DEV:globalThis.process?.env?.NODE_ENV!=="production",I="[\u{1F3F0} NavigationCitadel]",V=100;});exports.createDefaultLogger=void 0;exports.createDefaultDebugHandler=void 0;var y,w=D(()=>{P();exports.createDefaultLogger=()=>({info:(...t)=>console.info(`\u{1F535} ${I}`,...t),warn:(...t)=>console.log(`\u{1F7E1} ${I}`,...t),error:(...t)=>console.error(`\u{1F534} ${I}`,...t),debug:(...t)=>console.log(`\u{1F7E3} ${I} [DEBUG]`,...t)}),exports.createDefaultDebugHandler=()=>()=>{debugger},y=(t,o,n,e)=>{o&&(n.debug(t),e?.(t));};});var Z,F,dt,Ot,h,ft,vt,mt,H,Nt,Lt,bt,Rt,yt,q,Q,z=D(()=>{G();Z="navigation.citadel",F="Navigation Citadel",dt="castle",Ot="https://kassaila.github.io/vue-router-citadel/logo_devtools.svg",h=Z+".inspector",ft="citadel-root",vt="citadel-"+exports.NavigationOutpostScopes.GLOBAL,mt="citadel-"+exports.NavigationOutpostScopes.ROUTE,H=16777215,Nt=4372867,Lt=3900150,bt=9133302,Rt=16096779,yt=15485081,q="vue-router-citadel:settings:",Q="logLevel";});var St,Tt,Kt,Yt,Wt,Et,Xt,jt,_t,Ct,Dt=D(()=>{G();P();w();z();St=t=>t.priority??V,Tt=t=>t.hooks??[exports.NavigationHooks.BEFORE_EACH],Kt=t=>({label:`priority: ${t}`,textColor:H,backgroundColor:Nt}),Yt=t=>({label:t.length===1?t[0]:`${t.length} hooks`,textColor:H,backgroundColor:Lt}),Wt=()=>({label:"lazy",textColor:H,backgroundColor:yt}),Et=(t,o,n)=>{let e=[Kt(St(o)),Yt(Tt(o))];return o.lazy&&e.push(Wt()),{id:`outpost-${n}-${t}`,label:t,tags:e}},Xt=t=>{let o=[],n=[];for(let e of t.globalSorted){let r=t.global.get(e);r&&o.push(Et(e,r,exports.NavigationOutpostScopes.GLOBAL));}for(let e of t.routeSorted){let r=t.route.get(e);r&&n.push(Et(e,r,exports.NavigationOutpostScopes.ROUTE));}return [{id:ft,label:"Outposts",children:[{id:vt,label:`Global (${o.length})`,tags:[{label:exports.NavigationOutpostScopes.GLOBAL,textColor:H,backgroundColor:bt}],children:o},{id:mt,label:`Route (${n.length})`,tags:[{label:exports.NavigationOutpostScopes.ROUTE,textColor:H,backgroundColor:Rt}],children:n}]}]},jt=(t,o)=>{let n=t.match(`^outpost-(${exports.NavigationOutpostScopes.GLOBAL}|${exports.NavigationOutpostScopes.ROUTE})-(.+)$`);if(!n)return null;let[,e,r]=n,a=(e===exports.NavigationOutpostScopes.GLOBAL?o.global:o.route).get(r);return a?{"Outpost Details":[{key:"name",value:r},{key:"scope",value:e},{key:"priority",value:St(a)},{key:"hooks",value:Tt(a)},{key:"timeout",value:a.timeout??"none (uses default)"},{key:"lazy",value:a.lazy}]}:null},_t=(t,o,n,e=false,r)=>{t.addInspector({id:h,label:F,icon:dt}),t.on.getInspectorTree(l=>{l.inspectorId===h&&(l.rootNodes=Xt(o));}),t.on.getInspectorState(l=>{if(l.inspectorId!==h)return;let a=jt(l.nodeId,o);a&&(l.state=a);}),y(exports.DebugPoints.DEVTOOLS_INSPECTOR,e,n,r);},Ct=t=>{t.sendInspectorTree(h),t.sendInspectorState(h);};});var f,xt=D(()=>{f={OFF:"off",LOG:"log",DEBUG:"debug"};});var Jt,Zt,qt,tt,Qt,It,At,Gt,Pt=D(()=>{xt();z();Jt=()=>{if(typeof window>"u"||!window.localStorage)return null;try{let t=localStorage.getItem(q+Q);return t===null?null:t===f.OFF||t===f.LOG||t===f.DEBUG?t:null}catch{return null}},Zt=t=>{if(!(typeof window>"u"||!window.localStorage))try{localStorage.setItem(q+Q,t);}catch{}},qt=(t,o,n)=>o?f.DEBUG:t??n?f.LOG:f.OFF,tt=t=>{switch(t){case f.OFF:return {log:false,debug:false};case f.LOG:return {log:true,debug:false};case f.DEBUG:return {log:true,debug:true}}},Qt=t=>t.debug?f.DEBUG:t.log?f.LOG:f.OFF,It=(t,o,n)=>{let e=Jt();if(e!==null)return tt(e);let r=qt(t,o,n);return tt(r)},At=(t,o)=>{let n=tt(o);t.log=n.log,t.debug=n.debug,Zt(o);},Gt=t=>({logLevel:{label:"Log level",type:"choice",defaultValue:Qt(t),options:[{label:"Off",value:f.OFF},{label:"Log",value:f.LOG},{label:"Log + Debug",value:f.DEBUG}],component:"button-group"}});});var ht={};Ut(ht,{clearDevtoolsApi:()=>no,notifyDevtoolsRefresh:()=>eo,setupDevtools:()=>oo});var M,oo,eo,no,Ht=D(()=>{P();z();Dt();Pt();M=null,oo=(t,o,n,e,r,l,a)=>{let v=It(r,l,k);e.log=v.log,e.debug=v.debug,devtoolsApi.setupDevToolsPlugin({id:Z,label:F,logo:Ot,packageName:"vue-router-citadel",homepage:"https://github.com/Kassaila/vue-router-citadel",enableEarlyProxy:true,app:t,settings:Gt(e)},p=>{M=p,p.on.setPluginSettings(g=>{g.key==="logLevel"&&At(e,g.newValue);}),_t(p,o,n,e.debug,a);});},eo=()=>{M&&Ct(M);},no=()=>{M=null;};});G();w();G();P();w();var it=()=>({global:new Map,route:new Map,globalSorted:[],routeSorted:[]}),rt=(t,o,n)=>{let e=t[o],r=`${o}Sorted`;t[r]=Array.from(e.keys()).sort((l,a)=>{let v=e.get(l)?.priority??n,p=e.get(a)?.priority??n;return v-p});},st=(t,o,n,e,r)=>{t[o].has(n.name)&&r.warn(`${o} outpost "${n.name}" already exists, replacing...`),t[o].set(n.name,n),rt(t,o,e);},ut=(t,o,n,e)=>{let r=t[o].delete(n);return r&&rt(t,o,e),r},lt=(t,o)=>Array.from(t[o].keys());G();P();w();var Ft=t=>{if(typeof t=="string")return true;if(typeof t=="object"&&t!==null){let o=t;return "name"in o||"path"in o}return false},j=(t,o)=>{if(t instanceof Error)throw t;if(Object.values(exports.NavigationOutpostVerdicts).includes(t))return t;let n=`${I} Invalid outpost outcome: ${JSON.stringify(t)}.`;if(Ft(t)){if(o.resolve(t).matched.length===0)throw new Error(n+` Route not found: ${JSON.stringify(t)}`);return t}throw new Error(n+" Expected: verdicts.ALLOW, verdicts.BLOCK, or RouteLocationRaw (string path or object with name/path).")},U=(t,o)=>(t.hooks??[exports.NavigationHooks.BEFORE_EACH]).includes(o),gt=Symbol("timeout"),zt=t=>new Promise((o,n)=>{setTimeout(()=>{let e=new Error(`Timeout after ${t}ms`);e[gt]=true,n(e);},t);}),Mt=t=>t instanceof Error&> in t,pt=async(t,o,n,e,r)=>{let{onError:l,defaultTimeout:a,onTimeout:v}=n,{router:p}=o,g=t.timeout??a;y(exports.DebugPoints.BEFORE_OUTPOST,r.debug,e,n.debugHandler);try{let m=await t.getHandler(),d=g?await Promise.race([m(o),zt(g)]):await m(o);return j(d,p)}catch(m){if(Mt(m)){if(e.warn(`Outpost "${t.name}" timed out after ${g}ms`),y(exports.DebugPoints.TIMEOUT,r.debug,e,n.debugHandler),v){let d=await v(t.name,o);return j(d,p)}return exports.NavigationOutpostVerdicts.BLOCK}if(l&&m instanceof Error){let d=await l(m,o);return j(d,p)}return e.error(`Outpost "${t.name}" threw error:`,m),y(exports.DebugPoints.ERROR_CAUGHT,r.debug,e,n.debugHandler),exports.NavigationOutpostVerdicts.BLOCK}},J=async(t,o,n,e,r)=>{let{hook:l,to:a,from:v}=o,p=r.log||r.debug,g=a.matched.flatMap(c=>c.meta?.outposts??[]),m=new Set(g);g.length!==m.size&&e.warn(`Duplicate outposts detected on route "${String(a.name??a.path)}"`);let d=0,x=t.globalSorted.filter(c=>U(t.global.get(c),l)).length,$=t.routeSorted.filter(c=>m.has(c)&&U(t.route.get(c),l)).length,E=x+$;if(E===0)return exports.NavigationOutpostVerdicts.ALLOW;p&&e.info(`${l}: ${v.path} -> ${a.path} (${E} outposts)`),y(exports.DebugPoints.NAVIGATION_START,r.debug,e,n.debugHandler);for(let c of t.globalSorted){let T=t.global.get(c);if(!T||!U(T,l))continue;d++,p&&e.info(`Processing outpost ${d}/${E}: "${c}" [${l}]`);let S=await pt(T,o,n,e,r);if(S!==exports.NavigationOutpostVerdicts.ALLOW)return p&&e.warn(`Patrol stopped by outpost "${c}":`,S),y(exports.DebugPoints.PATROL_STOPPED,r.debug,e,n.debugHandler),S}for(let c of t.routeSorted){if(!m.has(c))continue;let T=t.route.get(c);if(!T){e.warn(`Route outpost "${c}" not found in registry`);continue}if(!U(T,l))continue;d++,p&&e.info(`Processing outpost ${d}/${E}: "${c}" [${l}]`);let S=await pt(T,o,n,e,r);if(S!==exports.NavigationOutpostVerdicts.ALLOW)return p&&e.warn(`Patrol stopped by outpost "${c}":`,S),y(exports.DebugPoints.PATROL_STOPPED,r.debug,e,n.debugHandler),S}return exports.NavigationOutpostVerdicts.ALLOW},ct=t=>{switch(t){case exports.NavigationOutpostVerdicts.ALLOW:return true;case exports.NavigationOutpostVerdicts.BLOCK:return false;default:return t}};var ot=null,kt=false,K=async()=>{if(kt)return null;if(!ot)try{ot=await Promise.resolve().then(()=>(Ht(),ht));}catch{return kt=true,null}return ot},ao=(t,o={})=>{let{log:n,debug:e,devtools:r=k,defaultPriority:l=V}=o,a=o.logger??exports.createDefaultLogger(),v=o.debugHandler??exports.createDefaultDebugHandler(),p=r&&typeof window<"u",g=it(),m={...o,debugHandler:v},d={log:n??k,debug:e??false},x=[],$=(i,s,u)=>({verdicts:exports.NavigationOutpostVerdicts,to:i,from:s,router:t,hook:u}),E=()=>d.log||d.debug,c=i=>async(s,u)=>{let O=$(s,u,i),N=await J(g,O,m,a,d);return ct(N)};x.push(t.beforeEach(c(exports.NavigationHooks.BEFORE_EACH))),x.push(t.beforeResolve(c(exports.NavigationHooks.BEFORE_RESOLVE)));let T=t.afterEach(async(i,s)=>{let u=$(i,s,exports.NavigationHooks.AFTER_EACH);try{await J(g,u,m,a,d);}catch(O){a.error("Error in afterEach outpost:",O),y(exports.DebugPoints.ERROR_CAUGHT,d.debug,a,v);}});x.push(T);let S=i=>{let{scope:s="global",name:u,handler:O,priority:N,hooks:wt,timeout:$t,lazy:Y=false}=i,A=null,B=null,Bt=async()=>A||(Y?(B||(B=O().then(_=>{if(!_.default||typeof _.default!="function")throw new Error(`Lazy outpost "${u}" must export default handler`);return A=_.default,A}).catch(_=>{throw B=null,_ instanceof Error?_:new Error(String(_))})),B):(A=O,A));E()&&a.info(`Deploying ${s} outpost: ${u}${Y?" (lazy)":""}`),st(g,s,{name:u,getHandler:Bt,lazy:Y,priority:N,hooks:wt,timeout:$t},l,a),p&&K().then(_=>_?.notifyDevtoolsRefresh());},et=(i,s)=>{E()&&a.info(`Abandoning ${i} outpost: ${s}`);let u=ut(g,i,s,l);return p&&u&&K().then(O=>O?.notifyDevtoolsRefresh()),u},nt=i=>t.getRoutes().find(s=>s.name===i),at={install(i){p&&K().then(s=>{s&&(s.setupDevtools(i,g,a,d,n,e,v),y(exports.DebugPoints.DEVTOOLS_INIT,d.debug,a,v),E()&&a.info("DevTools initialized via app.use(citadel)"));});},deployOutpost(i){if(Array.isArray(i))for(let s of i)S(s);else S(i);},abandonOutpost(i,s){if(Array.isArray(s)){let u=true;for(let O of s)et(i,O)||(u=false);return u}else return et(i,s)},getOutpostNames(i){return lt(g,i)},assignOutpostToRoute(i,s){let u=nt(i);if(!u)return a.warn(`Route "${i}" not found`),false;let O=Array.isArray(s)?s:[s];u.meta.outposts||(u.meta.outposts=[]);for(let N of O)u.meta.outposts.includes(N)||u.meta.outposts.push(N);return E()&&a.info(`Assigned outposts [${O.join(", ")}] to route "${i}"`),true},revokeOutpostFromRoute(i,s){let u=nt(i);if(!u)return a.warn(`Route "${i}" not found`),false;let O=Array.isArray(s)?s:[s];if(!u.meta.outposts){for(let N of O)a.warn(`Outpost "${N}" not found in route "${i}"`);return true}for(let N of O)u.meta.outposts.includes(N)||a.warn(`Outpost "${N}" not found in route "${i}"`);return u.meta.outposts=u.meta.outposts.filter(N=>!O.includes(N)),E()&&a.info(`Revoked outposts [${O.join(", ")}] from route "${i}"`),true},destroy(){E()&&a.info("Destroying citadel");for(let i of x)i();x.length=0,g.global.clear(),g.route.clear(),g.globalSorted.length=0,g.routeSorted.length=0,p&&K().then(i=>i?.clearDevtoolsApi());}};return o.outposts&&at.deployOutpost(o.outposts),at};
|
|
2
|
+
exports.createNavigationCitadel=ao;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import { App } from 'vue';
|
|
2
|
+
import { RouteLocationNormalized, Router, RouteLocationRaw } from 'vue-router';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Navigation hooks supported by the citadel
|
|
6
|
+
*/
|
|
7
|
+
declare const NavigationHooks: {
|
|
8
|
+
readonly BEFORE_EACH: "beforeEach";
|
|
9
|
+
readonly BEFORE_RESOLVE: "beforeResolve";
|
|
10
|
+
readonly AFTER_EACH: "afterEach";
|
|
11
|
+
};
|
|
12
|
+
type NavigationHook = (typeof NavigationHooks)[keyof typeof NavigationHooks];
|
|
13
|
+
/**
|
|
14
|
+
* Navigation outpost verdict constants
|
|
15
|
+
*/
|
|
16
|
+
declare const NavigationOutpostVerdicts: {
|
|
17
|
+
readonly ALLOW: "allow";
|
|
18
|
+
readonly BLOCK: "block";
|
|
19
|
+
};
|
|
20
|
+
type NavigationOutpostVerdict = (typeof NavigationOutpostVerdicts)[keyof typeof NavigationOutpostVerdicts];
|
|
21
|
+
/**
|
|
22
|
+
* Navigation outpost scope constants
|
|
23
|
+
*/
|
|
24
|
+
declare const NavigationOutpostScopes: {
|
|
25
|
+
readonly GLOBAL: "global";
|
|
26
|
+
readonly ROUTE: "route";
|
|
27
|
+
};
|
|
28
|
+
type NavigationOutpostScope = (typeof NavigationOutpostScopes)[keyof typeof NavigationOutpostScopes];
|
|
29
|
+
/**
|
|
30
|
+
* Outpost Registries (user-extensible via declaration merging)
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* Global outpost registry — extend this interface to enable type-safe global outpost names.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```typescript
|
|
37
|
+
* declare module 'vue-router-citadel' {
|
|
38
|
+
* interface GlobalOutpostRegistry {
|
|
39
|
+
* 'auth': true;
|
|
40
|
+
* 'maintenance': true;
|
|
41
|
+
* }
|
|
42
|
+
* }
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
interface GlobalOutpostRegistry {
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Route outpost registry — extend this interface to enable type-safe route outpost names.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```typescript
|
|
52
|
+
* declare module 'vue-router-citadel' {
|
|
53
|
+
* interface RouteOutpostRegistry {
|
|
54
|
+
* 'admin-only': true;
|
|
55
|
+
* 'premium': true;
|
|
56
|
+
* }
|
|
57
|
+
* }
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
interface RouteOutpostRegistry {
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Global outpost name type — inferred from GlobalOutpostRegistry or falls back to string
|
|
64
|
+
*/
|
|
65
|
+
type GlobalOutpostName = keyof GlobalOutpostRegistry extends never ? string : keyof GlobalOutpostRegistry;
|
|
66
|
+
/**
|
|
67
|
+
* Route outpost name type — inferred from RouteOutpostRegistry or falls back to string
|
|
68
|
+
*/
|
|
69
|
+
type RouteOutpostName = keyof RouteOutpostRegistry extends never ? string : keyof RouteOutpostRegistry;
|
|
70
|
+
/**
|
|
71
|
+
* Combined outpost name type (global | route)
|
|
72
|
+
*/
|
|
73
|
+
type OutpostName = GlobalOutpostName | RouteOutpostName;
|
|
74
|
+
/**
|
|
75
|
+
* Helper type to get outpost name type by scope
|
|
76
|
+
*/
|
|
77
|
+
type OutpostNameByScope<S extends NavigationOutpostScope> = S extends 'global' ? GlobalOutpostName : S extends 'route' ? RouteOutpostName : never;
|
|
78
|
+
/**
|
|
79
|
+
* Debug point names for debugger breakpoints
|
|
80
|
+
*/
|
|
81
|
+
declare const DebugPoints: {
|
|
82
|
+
readonly NAVIGATION_START: "navigation-start";
|
|
83
|
+
readonly BEFORE_OUTPOST: "before-outpost";
|
|
84
|
+
readonly PATROL_STOPPED: "patrol-stopped";
|
|
85
|
+
readonly ERROR_CAUGHT: "error-caught";
|
|
86
|
+
readonly TIMEOUT: "timeout";
|
|
87
|
+
readonly DEVTOOLS_INIT: "devtools-init";
|
|
88
|
+
readonly DEVTOOLS_INSPECTOR: "devtools-inspector";
|
|
89
|
+
};
|
|
90
|
+
type DebugPoint = (typeof DebugPoints)[keyof typeof DebugPoints];
|
|
91
|
+
/**
|
|
92
|
+
* Debug handler function signature.
|
|
93
|
+
* Called at debug points when debug mode is enabled.
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* ```typescript
|
|
97
|
+
* // Custom debug handler with debugger statement
|
|
98
|
+
* const debugHandler: DebugHandler = (name) => {
|
|
99
|
+
* console.trace(`Debug point: ${name}`);
|
|
100
|
+
* debugger; // Will work because it's in your code, not library code
|
|
101
|
+
* };
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
type DebugHandler = (name: DebugPoint) => void;
|
|
105
|
+
/**
|
|
106
|
+
* Logger interface for citadel.
|
|
107
|
+
* Implement this interface to provide custom logging behavior.
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* ```typescript
|
|
111
|
+
* // Use with pino for SSR
|
|
112
|
+
* import pino from 'pino';
|
|
113
|
+
* const pinoLogger = pino();
|
|
114
|
+
*
|
|
115
|
+
* const logger: CitadelLogger = {
|
|
116
|
+
* info: (...args) => pinoLogger.info(args),
|
|
117
|
+
* warn: (...args) => pinoLogger.warn(args),
|
|
118
|
+
* error: (...args) => pinoLogger.error(args),
|
|
119
|
+
* debug: (...args) => pinoLogger.debug(args),
|
|
120
|
+
* };
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
interface CitadelLogger {
|
|
124
|
+
info: (...args: unknown[]) => void;
|
|
125
|
+
warn: (...args: unknown[]) => void;
|
|
126
|
+
error: (...args: unknown[]) => void;
|
|
127
|
+
debug: (...args: unknown[]) => void;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Context passed to navigation outpost functions
|
|
131
|
+
*/
|
|
132
|
+
interface NavigationOutpostContext {
|
|
133
|
+
/**
|
|
134
|
+
* Verdict constants for outpost return
|
|
135
|
+
*/
|
|
136
|
+
verdicts: typeof NavigationOutpostVerdicts;
|
|
137
|
+
/**
|
|
138
|
+
* Target route
|
|
139
|
+
*/
|
|
140
|
+
to: RouteLocationNormalized;
|
|
141
|
+
/**
|
|
142
|
+
* Current route
|
|
143
|
+
*/
|
|
144
|
+
from: RouteLocationNormalized;
|
|
145
|
+
/**
|
|
146
|
+
* Router instance
|
|
147
|
+
*/
|
|
148
|
+
router: Router;
|
|
149
|
+
/**
|
|
150
|
+
* Current hook being processed
|
|
151
|
+
*/
|
|
152
|
+
hook: NavigationHook;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Outcome returned from navigation outpost
|
|
156
|
+
* - NavigationOutpostVerdicts.ALLOW: continue to next outpost
|
|
157
|
+
* - NavigationOutpostVerdicts.BLOCK: cancel navigation
|
|
158
|
+
* - RouteLocationRaw: redirect to specified route
|
|
159
|
+
* - Error: throw error (will be caught by onError handler)
|
|
160
|
+
*/
|
|
161
|
+
type NavigationOutpostOutcome = NavigationOutpostVerdict | RouteLocationRaw | Error;
|
|
162
|
+
/**
|
|
163
|
+
* Navigation outpost handler function signature
|
|
164
|
+
*/
|
|
165
|
+
type NavigationOutpostHandler = (ctx: NavigationOutpostContext) => NavigationOutpostOutcome | Promise<NavigationOutpostOutcome>;
|
|
166
|
+
/**
|
|
167
|
+
* Lazy outpost loader — returns a module with default export
|
|
168
|
+
*/
|
|
169
|
+
type LazyOutpostLoader = () => Promise<{
|
|
170
|
+
default: NavigationOutpostHandler;
|
|
171
|
+
}>;
|
|
172
|
+
/**
|
|
173
|
+
* Navigation outpost configuration.
|
|
174
|
+
* Generic parameter S constrains the name field based on scope.
|
|
175
|
+
* Generic parameter L constrains handler type based on lazy flag.
|
|
176
|
+
*/
|
|
177
|
+
interface NavigationOutpost<S extends NavigationOutpostScope = 'global', L extends boolean = false> {
|
|
178
|
+
/**
|
|
179
|
+
* Outpost scope. Default: 'global'
|
|
180
|
+
*/
|
|
181
|
+
scope?: S;
|
|
182
|
+
/**
|
|
183
|
+
* Unique outpost name (type-safe when registries are extended)
|
|
184
|
+
*/
|
|
185
|
+
name: OutpostNameByScope<S>;
|
|
186
|
+
/**
|
|
187
|
+
* Outpost handler function.
|
|
188
|
+
* When lazy: true, must be a function returning Promise<{ default: NavigationOutpostHandler }>.
|
|
189
|
+
* When lazy: false (default), must be a NavigationOutpostHandler.
|
|
190
|
+
*/
|
|
191
|
+
handler: L extends true ? LazyOutpostLoader : NavigationOutpostHandler;
|
|
192
|
+
/**
|
|
193
|
+
* Priority for outposts (lower = processed first). Default: 100
|
|
194
|
+
*/
|
|
195
|
+
priority?: number;
|
|
196
|
+
/**
|
|
197
|
+
* Hooks this outpost should run on. Default: ['beforeEach']
|
|
198
|
+
*/
|
|
199
|
+
hooks?: NavigationHook[];
|
|
200
|
+
/**
|
|
201
|
+
* Timeout for this outpost in milliseconds. Overrides defaultTimeout.
|
|
202
|
+
* Note: For lazy outposts, timeout applies only to handler execution, not module loading.
|
|
203
|
+
*/
|
|
204
|
+
timeout?: number;
|
|
205
|
+
/**
|
|
206
|
+
* Mark handler as lazy-loaded. Default: false.
|
|
207
|
+
* When true, handler must return Promise<{ default: NavigationOutpostHandler }>.
|
|
208
|
+
*/
|
|
209
|
+
lazy?: L;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Options for creating navigation citadel
|
|
213
|
+
*/
|
|
214
|
+
interface NavigationCitadelOptions {
|
|
215
|
+
/**
|
|
216
|
+
* Initial outposts to deploy on citadel creation
|
|
217
|
+
*/
|
|
218
|
+
outposts?: NavigationOutpost<NavigationOutpostScope, boolean>[];
|
|
219
|
+
/**
|
|
220
|
+
* Enable logging for non-critical events. Default: __DEV__
|
|
221
|
+
* Critical events (errors, timeouts) are always logged regardless of this setting.
|
|
222
|
+
*/
|
|
223
|
+
log?: boolean;
|
|
224
|
+
/**
|
|
225
|
+
* Custom logger implementation. Default: createDefaultLogger() (console with emoji prefixes)
|
|
226
|
+
*
|
|
227
|
+
* @example
|
|
228
|
+
* ```typescript
|
|
229
|
+
* createNavigationCitadel(router, {
|
|
230
|
+
* logger: myCustomLogger,
|
|
231
|
+
* });
|
|
232
|
+
* ```
|
|
233
|
+
*/
|
|
234
|
+
logger?: CitadelLogger;
|
|
235
|
+
/**
|
|
236
|
+
* Enable debug mode (logging + debugger breakpoints at key points). Default: false
|
|
237
|
+
*/
|
|
238
|
+
debug?: boolean;
|
|
239
|
+
/**
|
|
240
|
+
* Custom debug handler called at debug points when debug mode is enabled.
|
|
241
|
+
* Use this to add your own debugger statement (Vite won't strip it from your code).
|
|
242
|
+
*
|
|
243
|
+
* @example
|
|
244
|
+
* ```typescript
|
|
245
|
+
* createNavigationCitadel(router, {
|
|
246
|
+
* debug: true,
|
|
247
|
+
* debugHandler: (name) => {
|
|
248
|
+
* console.trace(`Debug: ${name}`);
|
|
249
|
+
* debugger; // Works because it's in your code
|
|
250
|
+
* },
|
|
251
|
+
* });
|
|
252
|
+
* ```
|
|
253
|
+
*/
|
|
254
|
+
debugHandler?: DebugHandler;
|
|
255
|
+
/**
|
|
256
|
+
* Enable Vue DevTools integration. Default: __DEV__
|
|
257
|
+
* When enabled, registers a custom inspector showing deployed outposts.
|
|
258
|
+
*/
|
|
259
|
+
devtools?: boolean;
|
|
260
|
+
/**
|
|
261
|
+
* Global error handler
|
|
262
|
+
*/
|
|
263
|
+
onError?: (error: Error, ctx: NavigationOutpostContext) => NavigationOutpostOutcome | Promise<NavigationOutpostOutcome>;
|
|
264
|
+
/**
|
|
265
|
+
* Default priority for outposts. Default: 100
|
|
266
|
+
*/
|
|
267
|
+
defaultPriority?: number;
|
|
268
|
+
/**
|
|
269
|
+
* Default timeout for outposts in milliseconds. Default: undefined (no timeout)
|
|
270
|
+
*/
|
|
271
|
+
defaultTimeout?: number;
|
|
272
|
+
/**
|
|
273
|
+
* Handler called when outpost times out
|
|
274
|
+
*/
|
|
275
|
+
onTimeout?: (outpostName: string, ctx: NavigationOutpostContext) => NavigationOutpostOutcome | Promise<NavigationOutpostOutcome>;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Public API returned by createNavigationCitadel
|
|
279
|
+
*/
|
|
280
|
+
interface NavigationCitadelAPI {
|
|
281
|
+
/**
|
|
282
|
+
* Install method for Vue Plugin API
|
|
283
|
+
* @internal
|
|
284
|
+
*/
|
|
285
|
+
install: (app: App) => void;
|
|
286
|
+
/**
|
|
287
|
+
* Deploy one or multiple outposts
|
|
288
|
+
*/
|
|
289
|
+
deployOutpost: <S extends NavigationOutpostScope = 'global', L extends boolean = false>(options: NavigationOutpost<S, L> | NavigationOutpost<S, L>[]) => void;
|
|
290
|
+
/**
|
|
291
|
+
* Remove one or multiple global outposts by name(s)
|
|
292
|
+
*/
|
|
293
|
+
abandonOutpost(scope: 'global', name: GlobalOutpostName | GlobalOutpostName[]): boolean;
|
|
294
|
+
/**
|
|
295
|
+
* Remove one or multiple route outposts by name(s)
|
|
296
|
+
*/
|
|
297
|
+
abandonOutpost(scope: 'route', name: RouteOutpostName | RouteOutpostName[]): boolean;
|
|
298
|
+
/**
|
|
299
|
+
* Get all deployed global outpost names
|
|
300
|
+
*/
|
|
301
|
+
getOutpostNames(scope: 'global'): GlobalOutpostName[];
|
|
302
|
+
/**
|
|
303
|
+
* Get all deployed route outpost names
|
|
304
|
+
*/
|
|
305
|
+
getOutpostNames(scope: 'route'): RouteOutpostName[];
|
|
306
|
+
/**
|
|
307
|
+
* Assign route outpost(s) to an existing route by route name
|
|
308
|
+
*/
|
|
309
|
+
assignOutpostToRoute: (routeName: string, outpostNames: RouteOutpostName | RouteOutpostName[]) => boolean;
|
|
310
|
+
/**
|
|
311
|
+
* Revoke route outpost(s) from an existing route by route name
|
|
312
|
+
*/
|
|
313
|
+
revokeOutpostFromRoute: (routeName: string, outpostNames: RouteOutpostName | RouteOutpostName[]) => boolean;
|
|
314
|
+
/**
|
|
315
|
+
* Destroy the citadel and remove navigation hooks
|
|
316
|
+
*/
|
|
317
|
+
destroy: () => void;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Extended route meta with navigation outpost support
|
|
321
|
+
*/
|
|
322
|
+
declare module 'vue-router' {
|
|
323
|
+
interface RouteMeta {
|
|
324
|
+
/**
|
|
325
|
+
* Route outposts to process for this route (type-safe when RouteOutpostRegistry is extended)
|
|
326
|
+
*/
|
|
327
|
+
outposts?: RouteOutpostName[];
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Creates default console logger with prefix and emoji indicators.
|
|
333
|
+
*
|
|
334
|
+
* Log levels:
|
|
335
|
+
* - 🔵 info: Navigation flow, outpost deployment
|
|
336
|
+
* - 🟡 warn: Blocked navigation, missing routes, duplicates
|
|
337
|
+
* - 🔴 error: Outpost errors, timeouts
|
|
338
|
+
* - 🟣 debug: Debug breakpoints
|
|
339
|
+
*/
|
|
340
|
+
declare const createDefaultLogger: () => CitadelLogger;
|
|
341
|
+
/**
|
|
342
|
+
* Default debug handler - triggers debugger statement.
|
|
343
|
+
* Note: Bundlers in consuming projects (Vite/esbuild) may strip this.
|
|
344
|
+
* For reliable breakpoints, provide your own debugHandler in options.
|
|
345
|
+
*/
|
|
346
|
+
declare const createDefaultDebugHandler: () => DebugHandler;
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Creates a navigation citadel for Vue Router
|
|
350
|
+
*
|
|
351
|
+
* @example
|
|
352
|
+
* ```typescript
|
|
353
|
+
* const citadel = createNavigationCitadel(router, {
|
|
354
|
+
* outposts: [
|
|
355
|
+
* {
|
|
356
|
+
* name: 'auth', // scope defaults to 'global'
|
|
357
|
+
* priority: 10,
|
|
358
|
+
* handler: async ({ verdicts, to }) => {
|
|
359
|
+
* if (!isAuthenticated && to.meta.requiresAuth) {
|
|
360
|
+
* return { name: 'login' };
|
|
361
|
+
* }
|
|
362
|
+
* return verdicts.ALLOW;
|
|
363
|
+
* },
|
|
364
|
+
* },
|
|
365
|
+
* ],
|
|
366
|
+
* onError: (error, ctx) => ({ name: 'error' }),
|
|
367
|
+
* });
|
|
368
|
+
* ```
|
|
369
|
+
*/
|
|
370
|
+
declare const createNavigationCitadel: (router: Router, options?: NavigationCitadelOptions) => NavigationCitadelAPI;
|
|
371
|
+
|
|
372
|
+
export { type CitadelLogger, type DebugHandler, type DebugPoint, DebugPoints, type GlobalOutpostName, type GlobalOutpostRegistry, type LazyOutpostLoader, type NavigationCitadelAPI, type NavigationCitadelOptions, type NavigationHook, NavigationHooks, type NavigationOutpost, type NavigationOutpostContext, type NavigationOutpostHandler, type NavigationOutpostScope, NavigationOutpostScopes, NavigationOutpostVerdicts, type OutpostName, type RouteOutpostName, type RouteOutpostRegistry, createDefaultDebugHandler, createDefaultLogger, createNavigationCitadel };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import { App } from 'vue';
|
|
2
|
+
import { RouteLocationNormalized, Router, RouteLocationRaw } from 'vue-router';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Navigation hooks supported by the citadel
|
|
6
|
+
*/
|
|
7
|
+
declare const NavigationHooks: {
|
|
8
|
+
readonly BEFORE_EACH: "beforeEach";
|
|
9
|
+
readonly BEFORE_RESOLVE: "beforeResolve";
|
|
10
|
+
readonly AFTER_EACH: "afterEach";
|
|
11
|
+
};
|
|
12
|
+
type NavigationHook = (typeof NavigationHooks)[keyof typeof NavigationHooks];
|
|
13
|
+
/**
|
|
14
|
+
* Navigation outpost verdict constants
|
|
15
|
+
*/
|
|
16
|
+
declare const NavigationOutpostVerdicts: {
|
|
17
|
+
readonly ALLOW: "allow";
|
|
18
|
+
readonly BLOCK: "block";
|
|
19
|
+
};
|
|
20
|
+
type NavigationOutpostVerdict = (typeof NavigationOutpostVerdicts)[keyof typeof NavigationOutpostVerdicts];
|
|
21
|
+
/**
|
|
22
|
+
* Navigation outpost scope constants
|
|
23
|
+
*/
|
|
24
|
+
declare const NavigationOutpostScopes: {
|
|
25
|
+
readonly GLOBAL: "global";
|
|
26
|
+
readonly ROUTE: "route";
|
|
27
|
+
};
|
|
28
|
+
type NavigationOutpostScope = (typeof NavigationOutpostScopes)[keyof typeof NavigationOutpostScopes];
|
|
29
|
+
/**
|
|
30
|
+
* Outpost Registries (user-extensible via declaration merging)
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* Global outpost registry — extend this interface to enable type-safe global outpost names.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```typescript
|
|
37
|
+
* declare module 'vue-router-citadel' {
|
|
38
|
+
* interface GlobalOutpostRegistry {
|
|
39
|
+
* 'auth': true;
|
|
40
|
+
* 'maintenance': true;
|
|
41
|
+
* }
|
|
42
|
+
* }
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
interface GlobalOutpostRegistry {
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Route outpost registry — extend this interface to enable type-safe route outpost names.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```typescript
|
|
52
|
+
* declare module 'vue-router-citadel' {
|
|
53
|
+
* interface RouteOutpostRegistry {
|
|
54
|
+
* 'admin-only': true;
|
|
55
|
+
* 'premium': true;
|
|
56
|
+
* }
|
|
57
|
+
* }
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
interface RouteOutpostRegistry {
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Global outpost name type — inferred from GlobalOutpostRegistry or falls back to string
|
|
64
|
+
*/
|
|
65
|
+
type GlobalOutpostName = keyof GlobalOutpostRegistry extends never ? string : keyof GlobalOutpostRegistry;
|
|
66
|
+
/**
|
|
67
|
+
* Route outpost name type — inferred from RouteOutpostRegistry or falls back to string
|
|
68
|
+
*/
|
|
69
|
+
type RouteOutpostName = keyof RouteOutpostRegistry extends never ? string : keyof RouteOutpostRegistry;
|
|
70
|
+
/**
|
|
71
|
+
* Combined outpost name type (global | route)
|
|
72
|
+
*/
|
|
73
|
+
type OutpostName = GlobalOutpostName | RouteOutpostName;
|
|
74
|
+
/**
|
|
75
|
+
* Helper type to get outpost name type by scope
|
|
76
|
+
*/
|
|
77
|
+
type OutpostNameByScope<S extends NavigationOutpostScope> = S extends 'global' ? GlobalOutpostName : S extends 'route' ? RouteOutpostName : never;
|
|
78
|
+
/**
|
|
79
|
+
* Debug point names for debugger breakpoints
|
|
80
|
+
*/
|
|
81
|
+
declare const DebugPoints: {
|
|
82
|
+
readonly NAVIGATION_START: "navigation-start";
|
|
83
|
+
readonly BEFORE_OUTPOST: "before-outpost";
|
|
84
|
+
readonly PATROL_STOPPED: "patrol-stopped";
|
|
85
|
+
readonly ERROR_CAUGHT: "error-caught";
|
|
86
|
+
readonly TIMEOUT: "timeout";
|
|
87
|
+
readonly DEVTOOLS_INIT: "devtools-init";
|
|
88
|
+
readonly DEVTOOLS_INSPECTOR: "devtools-inspector";
|
|
89
|
+
};
|
|
90
|
+
type DebugPoint = (typeof DebugPoints)[keyof typeof DebugPoints];
|
|
91
|
+
/**
|
|
92
|
+
* Debug handler function signature.
|
|
93
|
+
* Called at debug points when debug mode is enabled.
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* ```typescript
|
|
97
|
+
* // Custom debug handler with debugger statement
|
|
98
|
+
* const debugHandler: DebugHandler = (name) => {
|
|
99
|
+
* console.trace(`Debug point: ${name}`);
|
|
100
|
+
* debugger; // Will work because it's in your code, not library code
|
|
101
|
+
* };
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
type DebugHandler = (name: DebugPoint) => void;
|
|
105
|
+
/**
|
|
106
|
+
* Logger interface for citadel.
|
|
107
|
+
* Implement this interface to provide custom logging behavior.
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* ```typescript
|
|
111
|
+
* // Use with pino for SSR
|
|
112
|
+
* import pino from 'pino';
|
|
113
|
+
* const pinoLogger = pino();
|
|
114
|
+
*
|
|
115
|
+
* const logger: CitadelLogger = {
|
|
116
|
+
* info: (...args) => pinoLogger.info(args),
|
|
117
|
+
* warn: (...args) => pinoLogger.warn(args),
|
|
118
|
+
* error: (...args) => pinoLogger.error(args),
|
|
119
|
+
* debug: (...args) => pinoLogger.debug(args),
|
|
120
|
+
* };
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
interface CitadelLogger {
|
|
124
|
+
info: (...args: unknown[]) => void;
|
|
125
|
+
warn: (...args: unknown[]) => void;
|
|
126
|
+
error: (...args: unknown[]) => void;
|
|
127
|
+
debug: (...args: unknown[]) => void;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Context passed to navigation outpost functions
|
|
131
|
+
*/
|
|
132
|
+
interface NavigationOutpostContext {
|
|
133
|
+
/**
|
|
134
|
+
* Verdict constants for outpost return
|
|
135
|
+
*/
|
|
136
|
+
verdicts: typeof NavigationOutpostVerdicts;
|
|
137
|
+
/**
|
|
138
|
+
* Target route
|
|
139
|
+
*/
|
|
140
|
+
to: RouteLocationNormalized;
|
|
141
|
+
/**
|
|
142
|
+
* Current route
|
|
143
|
+
*/
|
|
144
|
+
from: RouteLocationNormalized;
|
|
145
|
+
/**
|
|
146
|
+
* Router instance
|
|
147
|
+
*/
|
|
148
|
+
router: Router;
|
|
149
|
+
/**
|
|
150
|
+
* Current hook being processed
|
|
151
|
+
*/
|
|
152
|
+
hook: NavigationHook;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Outcome returned from navigation outpost
|
|
156
|
+
* - NavigationOutpostVerdicts.ALLOW: continue to next outpost
|
|
157
|
+
* - NavigationOutpostVerdicts.BLOCK: cancel navigation
|
|
158
|
+
* - RouteLocationRaw: redirect to specified route
|
|
159
|
+
* - Error: throw error (will be caught by onError handler)
|
|
160
|
+
*/
|
|
161
|
+
type NavigationOutpostOutcome = NavigationOutpostVerdict | RouteLocationRaw | Error;
|
|
162
|
+
/**
|
|
163
|
+
* Navigation outpost handler function signature
|
|
164
|
+
*/
|
|
165
|
+
type NavigationOutpostHandler = (ctx: NavigationOutpostContext) => NavigationOutpostOutcome | Promise<NavigationOutpostOutcome>;
|
|
166
|
+
/**
|
|
167
|
+
* Lazy outpost loader — returns a module with default export
|
|
168
|
+
*/
|
|
169
|
+
type LazyOutpostLoader = () => Promise<{
|
|
170
|
+
default: NavigationOutpostHandler;
|
|
171
|
+
}>;
|
|
172
|
+
/**
|
|
173
|
+
* Navigation outpost configuration.
|
|
174
|
+
* Generic parameter S constrains the name field based on scope.
|
|
175
|
+
* Generic parameter L constrains handler type based on lazy flag.
|
|
176
|
+
*/
|
|
177
|
+
interface NavigationOutpost<S extends NavigationOutpostScope = 'global', L extends boolean = false> {
|
|
178
|
+
/**
|
|
179
|
+
* Outpost scope. Default: 'global'
|
|
180
|
+
*/
|
|
181
|
+
scope?: S;
|
|
182
|
+
/**
|
|
183
|
+
* Unique outpost name (type-safe when registries are extended)
|
|
184
|
+
*/
|
|
185
|
+
name: OutpostNameByScope<S>;
|
|
186
|
+
/**
|
|
187
|
+
* Outpost handler function.
|
|
188
|
+
* When lazy: true, must be a function returning Promise<{ default: NavigationOutpostHandler }>.
|
|
189
|
+
* When lazy: false (default), must be a NavigationOutpostHandler.
|
|
190
|
+
*/
|
|
191
|
+
handler: L extends true ? LazyOutpostLoader : NavigationOutpostHandler;
|
|
192
|
+
/**
|
|
193
|
+
* Priority for outposts (lower = processed first). Default: 100
|
|
194
|
+
*/
|
|
195
|
+
priority?: number;
|
|
196
|
+
/**
|
|
197
|
+
* Hooks this outpost should run on. Default: ['beforeEach']
|
|
198
|
+
*/
|
|
199
|
+
hooks?: NavigationHook[];
|
|
200
|
+
/**
|
|
201
|
+
* Timeout for this outpost in milliseconds. Overrides defaultTimeout.
|
|
202
|
+
* Note: For lazy outposts, timeout applies only to handler execution, not module loading.
|
|
203
|
+
*/
|
|
204
|
+
timeout?: number;
|
|
205
|
+
/**
|
|
206
|
+
* Mark handler as lazy-loaded. Default: false.
|
|
207
|
+
* When true, handler must return Promise<{ default: NavigationOutpostHandler }>.
|
|
208
|
+
*/
|
|
209
|
+
lazy?: L;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Options for creating navigation citadel
|
|
213
|
+
*/
|
|
214
|
+
interface NavigationCitadelOptions {
|
|
215
|
+
/**
|
|
216
|
+
* Initial outposts to deploy on citadel creation
|
|
217
|
+
*/
|
|
218
|
+
outposts?: NavigationOutpost<NavigationOutpostScope, boolean>[];
|
|
219
|
+
/**
|
|
220
|
+
* Enable logging for non-critical events. Default: __DEV__
|
|
221
|
+
* Critical events (errors, timeouts) are always logged regardless of this setting.
|
|
222
|
+
*/
|
|
223
|
+
log?: boolean;
|
|
224
|
+
/**
|
|
225
|
+
* Custom logger implementation. Default: createDefaultLogger() (console with emoji prefixes)
|
|
226
|
+
*
|
|
227
|
+
* @example
|
|
228
|
+
* ```typescript
|
|
229
|
+
* createNavigationCitadel(router, {
|
|
230
|
+
* logger: myCustomLogger,
|
|
231
|
+
* });
|
|
232
|
+
* ```
|
|
233
|
+
*/
|
|
234
|
+
logger?: CitadelLogger;
|
|
235
|
+
/**
|
|
236
|
+
* Enable debug mode (logging + debugger breakpoints at key points). Default: false
|
|
237
|
+
*/
|
|
238
|
+
debug?: boolean;
|
|
239
|
+
/**
|
|
240
|
+
* Custom debug handler called at debug points when debug mode is enabled.
|
|
241
|
+
* Use this to add your own debugger statement (Vite won't strip it from your code).
|
|
242
|
+
*
|
|
243
|
+
* @example
|
|
244
|
+
* ```typescript
|
|
245
|
+
* createNavigationCitadel(router, {
|
|
246
|
+
* debug: true,
|
|
247
|
+
* debugHandler: (name) => {
|
|
248
|
+
* console.trace(`Debug: ${name}`);
|
|
249
|
+
* debugger; // Works because it's in your code
|
|
250
|
+
* },
|
|
251
|
+
* });
|
|
252
|
+
* ```
|
|
253
|
+
*/
|
|
254
|
+
debugHandler?: DebugHandler;
|
|
255
|
+
/**
|
|
256
|
+
* Enable Vue DevTools integration. Default: __DEV__
|
|
257
|
+
* When enabled, registers a custom inspector showing deployed outposts.
|
|
258
|
+
*/
|
|
259
|
+
devtools?: boolean;
|
|
260
|
+
/**
|
|
261
|
+
* Global error handler
|
|
262
|
+
*/
|
|
263
|
+
onError?: (error: Error, ctx: NavigationOutpostContext) => NavigationOutpostOutcome | Promise<NavigationOutpostOutcome>;
|
|
264
|
+
/**
|
|
265
|
+
* Default priority for outposts. Default: 100
|
|
266
|
+
*/
|
|
267
|
+
defaultPriority?: number;
|
|
268
|
+
/**
|
|
269
|
+
* Default timeout for outposts in milliseconds. Default: undefined (no timeout)
|
|
270
|
+
*/
|
|
271
|
+
defaultTimeout?: number;
|
|
272
|
+
/**
|
|
273
|
+
* Handler called when outpost times out
|
|
274
|
+
*/
|
|
275
|
+
onTimeout?: (outpostName: string, ctx: NavigationOutpostContext) => NavigationOutpostOutcome | Promise<NavigationOutpostOutcome>;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Public API returned by createNavigationCitadel
|
|
279
|
+
*/
|
|
280
|
+
interface NavigationCitadelAPI {
|
|
281
|
+
/**
|
|
282
|
+
* Install method for Vue Plugin API
|
|
283
|
+
* @internal
|
|
284
|
+
*/
|
|
285
|
+
install: (app: App) => void;
|
|
286
|
+
/**
|
|
287
|
+
* Deploy one or multiple outposts
|
|
288
|
+
*/
|
|
289
|
+
deployOutpost: <S extends NavigationOutpostScope = 'global', L extends boolean = false>(options: NavigationOutpost<S, L> | NavigationOutpost<S, L>[]) => void;
|
|
290
|
+
/**
|
|
291
|
+
* Remove one or multiple global outposts by name(s)
|
|
292
|
+
*/
|
|
293
|
+
abandonOutpost(scope: 'global', name: GlobalOutpostName | GlobalOutpostName[]): boolean;
|
|
294
|
+
/**
|
|
295
|
+
* Remove one or multiple route outposts by name(s)
|
|
296
|
+
*/
|
|
297
|
+
abandonOutpost(scope: 'route', name: RouteOutpostName | RouteOutpostName[]): boolean;
|
|
298
|
+
/**
|
|
299
|
+
* Get all deployed global outpost names
|
|
300
|
+
*/
|
|
301
|
+
getOutpostNames(scope: 'global'): GlobalOutpostName[];
|
|
302
|
+
/**
|
|
303
|
+
* Get all deployed route outpost names
|
|
304
|
+
*/
|
|
305
|
+
getOutpostNames(scope: 'route'): RouteOutpostName[];
|
|
306
|
+
/**
|
|
307
|
+
* Assign route outpost(s) to an existing route by route name
|
|
308
|
+
*/
|
|
309
|
+
assignOutpostToRoute: (routeName: string, outpostNames: RouteOutpostName | RouteOutpostName[]) => boolean;
|
|
310
|
+
/**
|
|
311
|
+
* Revoke route outpost(s) from an existing route by route name
|
|
312
|
+
*/
|
|
313
|
+
revokeOutpostFromRoute: (routeName: string, outpostNames: RouteOutpostName | RouteOutpostName[]) => boolean;
|
|
314
|
+
/**
|
|
315
|
+
* Destroy the citadel and remove navigation hooks
|
|
316
|
+
*/
|
|
317
|
+
destroy: () => void;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Extended route meta with navigation outpost support
|
|
321
|
+
*/
|
|
322
|
+
declare module 'vue-router' {
|
|
323
|
+
interface RouteMeta {
|
|
324
|
+
/**
|
|
325
|
+
* Route outposts to process for this route (type-safe when RouteOutpostRegistry is extended)
|
|
326
|
+
*/
|
|
327
|
+
outposts?: RouteOutpostName[];
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Creates default console logger with prefix and emoji indicators.
|
|
333
|
+
*
|
|
334
|
+
* Log levels:
|
|
335
|
+
* - 🔵 info: Navigation flow, outpost deployment
|
|
336
|
+
* - 🟡 warn: Blocked navigation, missing routes, duplicates
|
|
337
|
+
* - 🔴 error: Outpost errors, timeouts
|
|
338
|
+
* - 🟣 debug: Debug breakpoints
|
|
339
|
+
*/
|
|
340
|
+
declare const createDefaultLogger: () => CitadelLogger;
|
|
341
|
+
/**
|
|
342
|
+
* Default debug handler - triggers debugger statement.
|
|
343
|
+
* Note: Bundlers in consuming projects (Vite/esbuild) may strip this.
|
|
344
|
+
* For reliable breakpoints, provide your own debugHandler in options.
|
|
345
|
+
*/
|
|
346
|
+
declare const createDefaultDebugHandler: () => DebugHandler;
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Creates a navigation citadel for Vue Router
|
|
350
|
+
*
|
|
351
|
+
* @example
|
|
352
|
+
* ```typescript
|
|
353
|
+
* const citadel = createNavigationCitadel(router, {
|
|
354
|
+
* outposts: [
|
|
355
|
+
* {
|
|
356
|
+
* name: 'auth', // scope defaults to 'global'
|
|
357
|
+
* priority: 10,
|
|
358
|
+
* handler: async ({ verdicts, to }) => {
|
|
359
|
+
* if (!isAuthenticated && to.meta.requiresAuth) {
|
|
360
|
+
* return { name: 'login' };
|
|
361
|
+
* }
|
|
362
|
+
* return verdicts.ALLOW;
|
|
363
|
+
* },
|
|
364
|
+
* },
|
|
365
|
+
* ],
|
|
366
|
+
* onError: (error, ctx) => ({ name: 'error' }),
|
|
367
|
+
* });
|
|
368
|
+
* ```
|
|
369
|
+
*/
|
|
370
|
+
declare const createNavigationCitadel: (router: Router, options?: NavigationCitadelOptions) => NavigationCitadelAPI;
|
|
371
|
+
|
|
372
|
+
export { type CitadelLogger, type DebugHandler, type DebugPoint, DebugPoints, type GlobalOutpostName, type GlobalOutpostRegistry, type LazyOutpostLoader, type NavigationCitadelAPI, type NavigationCitadelOptions, type NavigationHook, NavigationHooks, type NavigationOutpost, type NavigationOutpostContext, type NavigationOutpostHandler, type NavigationOutpostScope, NavigationOutpostScopes, NavigationOutpostVerdicts, type OutpostName, type RouteOutpostName, type RouteOutpostRegistry, createDefaultDebugHandler, createDefaultLogger, createNavigationCitadel };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import {g,e,h,i,a,j as j$1,d,b,f}from'./chunk-BPEUPHLM.js';export{d as DebugPoints,a as NavigationHooks,c as NavigationOutpostScopes,b as NavigationOutpostVerdicts,i as createDefaultDebugHandler,h as createDefaultLogger}from'./chunk-BPEUPHLM.js';var U=()=>({global:new Map,route:new Map,globalSorted:[],routeSorted:[]}),j=(t,o,a)=>{let n=t[o],u=`${o}Sorted`;t[u]=Array.from(n.keys()).sort((f,s)=>{let N=n.get(f)?.priority??a,c=n.get(s)?.priority??a;return N-c});},K=(t,o,a,n,u)=>{t[o].has(a.name)&&u.warn(`${o} outpost "${a.name}" already exists, replacing...`),t[o].set(a.name,a),j(t,o,n);},W=(t,o,a,n)=>{let u=t[o].delete(a);return u&&j(t,o,n),u},Y=(t,o)=>Array.from(t[o].keys());var nt=t=>{if(typeof t=="string")return true;if(typeof t=="object"&&t!==null){let o=t;return "name"in o||"path"in o}return false},x=(t,o)=>{if(t instanceof Error)throw t;if(Object.values(b).includes(t))return t;let a=`${f} Invalid outpost outcome: ${JSON.stringify(t)}.`;if(nt(t)){if(o.resolve(t).matched.length===0)throw new Error(a+` Route not found: ${JSON.stringify(t)}`);return t}throw new Error(a+" Expected: verdicts.ALLOW, verdicts.BLOCK, or RouteLocationRaw (string path or object with name/path).")},H=(t,o)=>(t.hooks??[a.BEFORE_EACH]).includes(o),X=Symbol("timeout"),it=t=>new Promise((o,a)=>{setTimeout(()=>{let n=new Error(`Timeout after ${t}ms`);n[X]=true,a(n);},t);}),at=t=>t instanceof Error&&X in t,J=async(t,o,a,n,u)=>{let{onError:f,defaultTimeout:s,onTimeout:N}=a,{router:c}=o,g=t.timeout??s;j$1(d.BEFORE_OUTPOST,u.debug,n,a.debugHandler);try{let O=await t.getHandler(),d=g?await Promise.race([O(o),it(g)]):await O(o);return x(d,c)}catch(O){if(at(O)){if(n.warn(`Outpost "${t.name}" timed out after ${g}ms`),j$1(d.TIMEOUT,u.debug,n,a.debugHandler),N){let d=await N(t.name,o);return x(d,c)}return b.BLOCK}if(f&&O instanceof Error){let d=await f(O,o);return x(d,c)}return n.error(`Outpost "${t.name}" threw error:`,O),j$1(d.ERROR_CAUGHT,u.debug,n,a.debugHandler),b.BLOCK}},I=async(t,o,a,n,u)=>{let{hook:f,to:s,from:N}=o,c=u.log||u.debug,g=s.matched.flatMap(l=>l.meta?.outposts??[]),O=new Set(g);g.length!==O.size&&n.warn(`Duplicate outposts detected on route "${String(s.name??s.path)}"`);let d$1=0,w=t.globalSorted.filter(l=>H(t.global.get(l),f)).length,$=t.routeSorted.filter(l=>O.has(l)&&H(t.route.get(l),f)).length,R=w+$;if(R===0)return b.ALLOW;c&&n.info(`${f}: ${N.path} -> ${s.path} (${R} outposts)`),j$1(d.NAVIGATION_START,u.debug,n,a.debugHandler);for(let l of t.globalSorted){let L=t.global.get(l);if(!L||!H(L,f))continue;d$1++,c&&n.info(`Processing outpost ${d$1}/${R}: "${l}" [${f}]`);let b$1=await J(L,o,a,n,u);if(b$1!==b.ALLOW)return c&&n.warn(`Patrol stopped by outpost "${l}":`,b$1),j$1(d.PATROL_STOPPED,u.debug,n,a.debugHandler),b$1}for(let l of t.routeSorted){if(!O.has(l))continue;let L=t.route.get(l);if(!L){n.warn(`Route outpost "${l}" not found in registry`);continue}if(!H(L,f))continue;d$1++,c&&n.info(`Processing outpost ${d$1}/${R}: "${l}" [${f}]`);let b$1=await J(L,o,a,n,u);if(b$1!==b.ALLOW)return c&&n.warn(`Patrol stopped by outpost "${l}":`,b$1),j$1(d.PATROL_STOPPED,u.debug,n,a.debugHandler),b$1}return b.ALLOW},q=t=>{switch(t){case b.ALLOW:return true;case b.BLOCK:return false;default:return t}};var z=null,Q=false,P=async()=>{if(Q)return null;if(!z)try{z=await import('./devtools-EZI2AQWB.js');}catch{return Q=true,null}return z},rt=(t,o={})=>{let{log:a$1,debug:n,devtools:u=e,defaultPriority:f=g}=o,s=o.logger??h(),N=o.debugHandler??i(),c=u&&typeof window<"u",g$1=U(),O={...o,debugHandler:N},d$1={log:a$1??e,debug:n??false},w=[],$=(e,i,r)=>({verdicts:b,to:e,from:i,router:t,hook:r}),R=()=>d$1.log||d$1.debug,l=e=>async(i,r)=>{let p=$(i,r,e),v=await I(g$1,p,O,s,d$1);return q(v)};w.push(t.beforeEach(l(a.BEFORE_EACH))),w.push(t.beforeResolve(l(a.BEFORE_RESOLVE)));let L=t.afterEach(async(e,i)=>{let r=$(e,i,a.AFTER_EACH);try{await I(g$1,r,O,s,d$1);}catch(p){s.error("Error in afterEach outpost:",p),j$1(d.ERROR_CAUGHT,d$1.debug,s,N);}});w.push(L);let b$1=e=>{let{scope:i="global",name:r,handler:p,priority:v,hooks:Z,timeout:tt,lazy:T=false}=e,C=null,S=null,ot=async()=>C||(T?(S||(S=p().then(E=>{if(!E.default||typeof E.default!="function")throw new Error(`Lazy outpost "${r}" must export default handler`);return C=E.default,C}).catch(E=>{throw S=null,E instanceof Error?E:new Error(String(E))})),S):(C=p,C));R()&&s.info(`Deploying ${i} outpost: ${r}${T?" (lazy)":""}`),K(g$1,i,{name:r,getHandler:ot,lazy:T,priority:v,hooks:Z,timeout:tt},f,s),c&&P().then(E=>E?.notifyDevtoolsRefresh());},B=(e,i)=>{R()&&s.info(`Abandoning ${e} outpost: ${i}`);let r=W(g$1,e,i,f);return c&&r&&P().then(p=>p?.notifyDevtoolsRefresh()),r},G=e=>t.getRoutes().find(i=>i.name===e),M={install(e){c&&P().then(i=>{i&&(i.setupDevtools(e,g$1,s,d$1,a$1,n,N),j$1(d.DEVTOOLS_INIT,d$1.debug,s,N),R()&&s.info("DevTools initialized via app.use(citadel)"));});},deployOutpost(e){if(Array.isArray(e))for(let i of e)b$1(i);else b$1(e);},abandonOutpost(e,i){if(Array.isArray(i)){let r=true;for(let p of i)B(e,p)||(r=false);return r}else return B(e,i)},getOutpostNames(e){return Y(g$1,e)},assignOutpostToRoute(e,i){let r=G(e);if(!r)return s.warn(`Route "${e}" not found`),false;let p=Array.isArray(i)?i:[i];r.meta.outposts||(r.meta.outposts=[]);for(let v of p)r.meta.outposts.includes(v)||r.meta.outposts.push(v);return R()&&s.info(`Assigned outposts [${p.join(", ")}] to route "${e}"`),true},revokeOutpostFromRoute(e,i){let r=G(e);if(!r)return s.warn(`Route "${e}" not found`),false;let p=Array.isArray(i)?i:[i];if(!r.meta.outposts){for(let v of p)s.warn(`Outpost "${v}" not found in route "${e}"`);return true}for(let v of p)r.meta.outposts.includes(v)||s.warn(`Outpost "${v}" not found in route "${e}"`);return r.meta.outposts=r.meta.outposts.filter(v=>!p.includes(v)),R()&&s.info(`Revoked outposts [${p.join(", ")}] from route "${e}"`),true},destroy(){R()&&s.info("Destroying citadel");for(let e of w)e();w.length=0,g$1.global.clear(),g$1.route.clear(),g$1.globalSorted.length=0,g$1.routeSorted.length=0,c&&P().then(e=>e?.clearDevtoolsApi());}};return o.outposts&&M.deployOutpost(o.outposts),M};export{rt as createNavigationCitadel};
|
package/package.json
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vue-router-citadel",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Structured navigation defense for Vue Router",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"main": "dist/index.cjs",
|
|
8
|
+
"module": "dist/index.js",
|
|
9
|
+
"types": "dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"require": {
|
|
17
|
+
"types": "./dist/index.d.cts",
|
|
18
|
+
"default": "./dist/index.cjs"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"CHANGELOG.md"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsup",
|
|
28
|
+
"build:dev": "NODE_ENV=development tsup",
|
|
29
|
+
"test": "vitest",
|
|
30
|
+
"test:run": "vitest run",
|
|
31
|
+
"test:coverage": "vitest run --coverage",
|
|
32
|
+
"lint": "eslint",
|
|
33
|
+
"lint:fix": "eslint --fix",
|
|
34
|
+
"format": "prettier --write .",
|
|
35
|
+
"format:check": "prettier --check .",
|
|
36
|
+
"check:lint": "eslint",
|
|
37
|
+
"check:types": "tsc --noEmit",
|
|
38
|
+
"check:format": "npm run format:check",
|
|
39
|
+
"check:size": "size-limit",
|
|
40
|
+
"check:all": "npm run check:format && npm run check:lint && npm run check:types && npm run test:run && npm run build && npm run check:size",
|
|
41
|
+
"release:check": "npm run check:all && npm pack --dry-run",
|
|
42
|
+
"release:publish": "npm run release:check && npm publish",
|
|
43
|
+
"release:publish:beta": "npm run release:check && npm publish --tag beta",
|
|
44
|
+
"prepare": "husky",
|
|
45
|
+
"prepublishOnly": "npm run check:types && npm run test:run",
|
|
46
|
+
"docs:dev": "vitepress dev docs",
|
|
47
|
+
"docs:build": "vitepress build docs",
|
|
48
|
+
"docs:preview": "vitepress preview docs"
|
|
49
|
+
},
|
|
50
|
+
"lint-staged": {
|
|
51
|
+
"*.{ts,js}": "eslint --fix",
|
|
52
|
+
"*.{ts,js,json,md}": "prettier --write"
|
|
53
|
+
},
|
|
54
|
+
"keywords": [
|
|
55
|
+
"vue",
|
|
56
|
+
"vue-router",
|
|
57
|
+
"router",
|
|
58
|
+
"guard",
|
|
59
|
+
"middleware",
|
|
60
|
+
"navigation",
|
|
61
|
+
"typescript"
|
|
62
|
+
],
|
|
63
|
+
"author": "Kassaila",
|
|
64
|
+
"license": "MIT",
|
|
65
|
+
"engines": {
|
|
66
|
+
"node": ">=22.0.0"
|
|
67
|
+
},
|
|
68
|
+
"repository": {
|
|
69
|
+
"type": "git",
|
|
70
|
+
"url": "git+https://github.com/Kassaila/vue-router-citadel.git"
|
|
71
|
+
},
|
|
72
|
+
"bugs": {
|
|
73
|
+
"url": "https://github.com/Kassaila/vue-router-citadel/issues"
|
|
74
|
+
},
|
|
75
|
+
"homepage": "https://github.com/Kassaila/vue-router-citadel#readme",
|
|
76
|
+
"peerDependencies": {
|
|
77
|
+
"vue": "^3.0.0",
|
|
78
|
+
"vue-router": "^4.0.0 || ^5.0.0"
|
|
79
|
+
},
|
|
80
|
+
"dependencies": {
|
|
81
|
+
"@vue/devtools-api": "^8.0.5"
|
|
82
|
+
},
|
|
83
|
+
"devDependencies": {
|
|
84
|
+
"@commitlint/cli": "^20.4.1",
|
|
85
|
+
"@commitlint/config-conventional": "^20.4.1",
|
|
86
|
+
"@eslint/js": "^9.39.2",
|
|
87
|
+
"@size-limit/preset-small-lib": "^12.0.0",
|
|
88
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
89
|
+
"@vue/test-utils": "^2.4.6",
|
|
90
|
+
"eslint": "^9.39.2",
|
|
91
|
+
"eslint-config-prettier": "^10.1.8",
|
|
92
|
+
"happy-dom": "^20.4.0",
|
|
93
|
+
"husky": "^9.0.0",
|
|
94
|
+
"lint-staged": "^16.0.0",
|
|
95
|
+
"prettier": "^3.0.0",
|
|
96
|
+
"size-limit": "^12.0.0",
|
|
97
|
+
"tsup": "^8.0.0",
|
|
98
|
+
"typescript": "^5.0.0",
|
|
99
|
+
"typescript-eslint": "^8.55.0",
|
|
100
|
+
"vitepress": "^1.6.4",
|
|
101
|
+
"vitepress-plugin-mermaid": "^2.0.17",
|
|
102
|
+
"vitest": "^4.0.18",
|
|
103
|
+
"vue": "^3.5.27",
|
|
104
|
+
"vue-router": "^5.0.2"
|
|
105
|
+
}
|
|
106
|
+
}
|