vue-nnn-router 0.0.1

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 ADDED
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+
3
+ **Versioning:** while in **`0.x`** the package is in early development and breaking changes may occur in any minor release. Starting from **`1.0.0`**, this package will use **semver major aligned with Vue Router** — **`vue-nnn-router` 4.x** will target **Vue Router 4.x**, and when Vue Router 5 exists, expect **`vue-nnn-router` 5.x** with a Vue Router 5 peer.
4
+
5
+ ## [0.0.1] - 2026-05-14
6
+
7
+ Initial public release.
8
+
9
+ - SPA file-based routing: `index.vue`, `_layout.vue`, dynamic `[param]` and `[param].vue`.
10
+ - Cascading `_middleware.ts` and optional per-route `middleware` export (eager glob).
11
+ - Helpers: `simplifyGlobKey`, `pathNoExt`, `segmentUrlFromFs`, `mwPrefixesForPathNoExt`.
12
+
13
+ [0.0.1]: https://www.npmjs.com/package/vue-nnn-router/v/0.0.1
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 vue-nnn-router contributors
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,448 @@
1
+ # vue-nnn-router
2
+
3
+ File-based routing for **[Vue Router](https://router.vuejs.org/)** (**4.x** or **5.x**): **SPA-style** pages (`index.vue`, optional **`_layout.vue`** per folder), dynamic segments **`[param]` → `:param`**, cascading **`_middleware.ts`**, powered by a Vite **`import.meta.glob`** map (or any equivalent `Record<string, unknown>`).
4
+
5
+ **English** | [Tiếng Việt](README.vi.md)
6
+
7
+ ## Contents
8
+
9
+ 1. [Why glob, not filesystem?](#why-glob-not-filesystem)
10
+ 2. [Install & requirements](#install--requirements)
11
+ 3. [Quick start](#quick-start)
12
+ 4. [Folder layout & URL mapping](#folder-layout--url-mapping)
13
+ 5. [Glob patterns and `routesRoot`](#glob-patterns-and-routesroot) — three common setups
14
+ 6. [`createNnnRoutes` options (with examples)](#creatennroutes-options-with-examples)
15
+ 7. [Middleware (directory + per-route)](#middleware-directory--per-route)
16
+ 8. [Eager vs lazy `import.meta.glob`](#eager-vs-lazy-importmetaglob)
17
+ 9. [Route meta & utilities](#route-meta--utilities)
18
+ 10. [Run the demo](#run-the-demo-this-repo)
19
+ 11. [Package & license](#package)
20
+
21
+ ## Why glob, not filesystem?
22
+
23
+ There is no usable filesystem API inside the browser bundle for your route tree, so this library turns a **glob map** (resolved at build time) into **`RouteRecordRaw[]`**.
24
+
25
+ ## Install & requirements
26
+
27
+ ```bash
28
+ npm install vue-nnn-router
29
+ ```
30
+
31
+ - **Vue** `^3.3` (with **Vue Router 5**, follow that release’s peer: typically **Vue `^3.5`**).
32
+ - **Vue Router** `^4.2` **or** `^5.0` — this package only builds plain **`RouteRecordRaw[]`** and **`beforeEnter`** guards compatible with both lines.
33
+ - **Vite** `import.meta.glob` (or any object shaped like the glob result)
34
+
35
+ Guards exported from **`_middleware.ts`** and per-route **`middleware`** use the same **`next()` callback style** Vue Router accepts on both majors; Vue Router **5 may log deprecations** encouraging return-based guards — behavior is unchanged for your users until they refactor.
36
+
37
+ ## Quick start
38
+
39
+ **1. Put pages under `src/pages/`** (recommended name; any folder works).
40
+
41
+ **2. Build a module map and call `createNnnRoutes`:**
42
+
43
+ ```ts
44
+ // e.g. src/router/index.ts
45
+ import { createRouter, createWebHistory } from "vue-router";
46
+ import { createNnnRoutes } from "vue-nnn-router";
47
+
48
+ const modules = import.meta.glob(
49
+ [
50
+ "/src/pages/**/*.{vue,tsx,jsx,ts,js}",
51
+ "/src/pages/**/_middleware.ts",
52
+ ],
53
+ { eager: true },
54
+ );
55
+
56
+ const routes = createNnnRoutes(modules as Record<string, unknown>, {
57
+ routesRoot: "src/pages",
58
+ });
59
+
60
+ export const router = createRouter({
61
+ history: createWebHistory(),
62
+ routes,
63
+ });
64
+ ```
65
+
66
+ **3. Mount the router** in `main.ts` as usual.
67
+
68
+ ## Folder layout & URL mapping
69
+
70
+ ```
71
+ pages/
72
+ _middleware.ts # applies to all pages under pages/
73
+ index.vue # URL /
74
+ about/
75
+ index.vue # /about
76
+ users/
77
+ _layout.vue # layout for /users/** — must render <RouterView />
78
+ _middleware.ts
79
+ index.vue # /users
80
+ add.vue # /users/add
81
+ [id].vue # /users/:id (shorthand; same idea as users/[id]/index.vue)
82
+ ```
83
+
84
+ - **Allowed extensions:** `.vue`, `.tsx`, `.jsx`, `.ts`, `.js`.
85
+ - **`index.*`:** index URL for that folder.
86
+ - **Other basenames (`add.vue`, …):** one more segment in the URL.
87
+ - **`[param].*` or `[param]/` folders:** **`[id]` → `:id`** in the URL.
88
+ - **`_layout.vue`:** wraps child routes (same **`_`** prefix rule as **`_middleware.ts`**); nested views need **`<RouterView />`**.
89
+
90
+ ## Glob patterns and `routesRoot`
91
+
92
+ Keys in the glob object must be normalized consistently: this library runs each key through **`simplifyGlobKey`** (drops leading `./`, drops a single leading **`/`**) and then **`stripRoutesRoot`** when you pass **`routesRoot`**.
93
+
94
+ You must pass **`routesRoot`** exactly matching the prefix of those normalized keys — the part **before** the path that drives the route tree (`about/index.vue`, `users/[id].vue`, …).
95
+
96
+ ### Case A — Root-relative glob (recommended)
97
+
98
+ Pattern starts with **`/`** → resolved from the Vite **project root** (folder that contains **`vite.config`**).
99
+
100
+ ```ts
101
+ const modules = import.meta.glob(
102
+ [
103
+ "/src/pages/**/*.{vue,tsx,jsx,ts,js}",
104
+ "/src/pages/**/_middleware.ts",
105
+ ],
106
+ { eager: true },
107
+ );
108
+
109
+ createNnnRoutes(modules as Record<string, unknown>, {
110
+ routesRoot: "src/pages",
111
+ });
112
+ ```
113
+
114
+ Keys look like **`src/pages/about/index.vue`** → strip **`src/pages`** → **`about/index.vue`**.
115
+
116
+ ### Case B — Relative to the file that calls `import.meta.glob`
117
+
118
+ Useful when you keep **`src/router/index.ts`** next to **`src/pages/`**:
119
+
120
+ ```ts
121
+ const modules = import.meta.glob(
122
+ [
123
+ "../pages/**/*.{vue,tsx,jsx,ts,js}",
124
+ "../pages/**/_middleware.ts",
125
+ ],
126
+ { eager: true },
127
+ );
128
+
129
+ createNnnRoutes(modules as Record<string, unknown>, {
130
+ routesRoot: "../pages",
131
+ });
132
+ ```
133
+
134
+ Keys look like **`../pages/about/index.vue`**.
135
+
136
+ ### Case C — Manual map, no strip
137
+
138
+ If **you omit `routesRoot`**, normalized keys must already be rooted at the routing tree (**no stray `src/pages` prefix**). Handy for tests or codegen.
139
+
140
+ ```ts
141
+ createNnnRoutes(
142
+ {
143
+ "about/index.vue": { default: AboutPage },
144
+ "index.vue": { default: HomePage },
145
+ } as Record<string, unknown>,
146
+ {
147
+ /** no routesRoot — keys ARE the tree paths after normalize */
148
+ },
149
+ );
150
+ ```
151
+
152
+ ❌ Typical mistake: **`routesRoot: "pages"`** while using **`Case A`** — keys become **`src/pages/...`**; the prefix **`pages`** alone does **not** match. Prefer **`routesRoot: "src/pages"`** or switch to **`Case B`**.
153
+
154
+ ---
155
+
156
+ ## `createNnnRoutes` options (with examples)
157
+
158
+ | Option | Purpose |
159
+ |----------------|---------|
160
+ | `routesRoot` | Strip filesystem prefix from each glob key (after `simplifyGlobKey`). |
161
+ | `prefix` | Leading URL segment for **all** generated paths. |
162
+ | `onDuplicate` | Two files resolving to the same URL path. |
163
+ | `verbose` | Print path ↔ source file table after build. |
164
+ | `logger` | Custom printer for **`verbose`** (defaults to **`console.log`**). |
165
+ | `silent` | Suppress warnings and disable **`verbose`** output. |
166
+
167
+ **Default duplicate resolution:** if you do nothing, the **first** matching leaf (by stable internal ordering) wins. Set **`onDuplicate: "last-wins"`** to keep the opposite. Duplicate sets still **`console.warn`** unless **`silent: true`** or you pass a **callback** (callback replaces built-in duplicate warnings).
168
+
169
+ ### `routesRoot`
170
+
171
+ Already covered above. Use **`warnIfRoutesRootLikelyWrong`** from the package export if you want to detect misaligned roots in tooling (see [Utilities](#route-meta--utilities)).
172
+
173
+ ### `prefix`
174
+
175
+ Adds a stable URL segment in front of every route (**leading/trailing slashes are normalized away**):
176
+
177
+ ```ts
178
+ createNnnRoutes(modules as Record<string, unknown>, {
179
+ routesRoot: "src/pages",
180
+ prefix: "/app", // or "app"
181
+ });
182
+ ```
183
+
184
+ - Page that was **`/`** becomes **`/app`** (or **`/app/`** normalized).
185
+ - **`/users`** becomes **`/app/users`**.
186
+
187
+ ### `onDuplicate`: `"first-wins"` \| `"last-wins"` \| callback
188
+
189
+ When two files map to the **same URL** (for example two `index.vue` files both resolving to **`/`**).
190
+
191
+ ```ts
192
+ // Keep the chronologically later file after internal ordering:
193
+ createNnnRoutes(modules as Record<string, unknown>, {
194
+ routesRoot: "src/pages",
195
+ onDuplicate: "last-wins",
196
+ silent: true, // omit duplicate warnings if intentional
197
+ });
198
+
199
+ // Or handle yourself (no default duplicate warning for these):
200
+ createNnnRoutes(modules as Record<string, unknown>, {
201
+ routesRoot: "src/pages",
202
+ onDuplicate: (path, files) => {
203
+ console.error(`Duplicate URL ${path}`, files);
204
+ },
205
+ });
206
+ ```
207
+
208
+ ### `verbose` and `logger`
209
+
210
+ ```ts
211
+ createNnnRoutes(modules as Record<string, unknown>, {
212
+ routesRoot: "src/pages",
213
+ verbose: true,
214
+ logger: (...args) => {
215
+ // e.g. send to tooling instead of stdout
216
+ args.forEach((a) => myTooling.log(String(a)));
217
+ },
218
+ });
219
+ ```
220
+
221
+ If **`silent: true`**, **`verbose`** has no channel (logging is disabled).
222
+
223
+ ### `silent`
224
+
225
+ Turns **off**:
226
+
227
+ - Duplicate-path **`console.warn`**
228
+ - Other library warnings (`routesRoot` mismatch, duplicate middleware keys, …)
229
+ - **`verbose`** printing
230
+
231
+ ---
232
+
233
+ ## Middleware (directory + per-route)
234
+
235
+ ### At a glance
236
+
237
+ | Goal | What to use |
238
+ |------|-------------|
239
+ | Run before **all** routes under a folder (and its subfolders) | That folder’s **`_middleware.ts`** — `export default` one guard or `export default [a, b]` |
240
+ | Run for **one URL** only | In the page module: **`export function middleware`** (or `export { … as middleware }`) — glob for that file must be **`eager`** |
241
+ | Avoid per-page `middleware` with lazy code-splitting | Keep **`_middleware.ts`** in a separate **`import.meta.glob(..., { eager: true })`** |
242
+
243
+ ### File layout
244
+
245
+ Place **`_middleware.ts`** (or **`_middleware.js`**) inside a page directory. It applies to the **entire URL subtree** under that folder (including deeper pages that do not define their own `_middleware`).
246
+
247
+ ```text
248
+ src/pages/
249
+ _middleware.ts ← root guard: every URL under pages/
250
+ index.vue ← /
251
+ users/
252
+ _middleware.ts ← extra guard for every URL under /users/...
253
+ index.vue ← /users
254
+ add.vue ← /users/add
255
+ ```
256
+
257
+ ### Order when you navigate to a URL
258
+
259
+ Each **leaf** `RouteRecordRaw` gets a single composed **`beforeEnter`** chain. This library concatenates:
260
+
261
+ 1. The **`_middleware.ts`** at the routing root (`pages/`, empty prefix),
262
+ 2. Then each deeper **`_middleware.ts`** along the path (`users/`, …),
263
+ 3. Finally the page module’s **`middleware`** export (if any and the module was loaded **eagerly**).
264
+
265
+ **Example:** navigating to **`/users/add`** with the tree above:
266
+
267
+ 1. Default export from **`pages/_middleware.ts`**
268
+ 2. Default export from **`pages/users/_middleware.ts`**
269
+ 3. (If present) **`middleware`** from **`pages/users/add.vue`**
270
+
271
+ If any guard calls `next(false)`, `throw`, or redirects with `next('/somewhere')`, later steps follow normal Vue Router rules (may never run).
272
+
273
+ ### Directory middleware — default export, single guard
274
+
275
+ ```ts
276
+ // src/pages/_middleware.ts
277
+ import type {
278
+ NavigationGuardNext,
279
+ RouteLocationNormalized,
280
+ } from "vue-router";
281
+
282
+ export default function rootGuard(
283
+ to: RouteLocationNormalized,
284
+ _from: RouteLocationNormalized,
285
+ next: NavigationGuardNext
286
+ ) {
287
+ // e.g. if (!token && to.meta.requiresAuth) return next('/login')
288
+ next();
289
+ }
290
+ ```
291
+
292
+ A nested folder guard is identical in shape:
293
+
294
+ ```ts
295
+ // src/pages/users/_middleware.ts
296
+ import type {
297
+ NavigationGuardNext,
298
+ RouteLocationNormalized,
299
+ } from "vue-router";
300
+
301
+ export default function usersGuard(
302
+ to: RouteLocationNormalized,
303
+ _from: RouteLocationNormalized,
304
+ next: NavigationGuardNext
305
+ ) {
306
+ next();
307
+ }
308
+ ```
309
+
310
+ ### Directory middleware — default export, **array** of guards
311
+
312
+ Guards run **left-to-right** within that folder’s layer **before** child-folder `_middleware` or the page `middleware`:
313
+
314
+ ```ts
315
+ import type {
316
+ NavigationGuardNext,
317
+ RouteLocationNormalized,
318
+ } from "vue-router";
319
+
320
+ function logVisit(
321
+ to: RouteLocationNormalized,
322
+ _from: RouteLocationNormalized,
323
+ next: NavigationGuardNext
324
+ ) {
325
+ console.log(to.fullPath);
326
+ next();
327
+ }
328
+
329
+ function checkSomething(
330
+ to: RouteLocationNormalized,
331
+ _from: RouteLocationNormalized,
332
+ next: NavigationGuardNext
333
+ ) {
334
+ next();
335
+ }
336
+
337
+ export default [logVisit, checkSomething];
338
+ ```
339
+
340
+ ### Per-page `middleware` in a `.vue` SFC
341
+
342
+ Use this when **one URL** needs logic that doesn’t belong in a whole-folder `_middleware`.
343
+
344
+ With **`<script setup>`**, keep a **plain `<script lang="ts">` block** (no setup) dedicated to **`middleware`**, plus **`<script setup>`** for the component — this avoids fighting `export default` rules:
345
+
346
+ ```vue
347
+ <!-- src/pages/users/add.vue -->
348
+ <script lang="ts">
349
+ import type { NavigationGuardNext, RouteLocationNormalized } from "vue-router";
350
+
351
+ /** Runs after all pages/ and pages/users/ _middleware.ts guards. */
352
+ export function middleware(
353
+ to: RouteLocationNormalized,
354
+ _from: RouteLocationNormalized,
355
+ next: NavigationGuardNext
356
+ ) {
357
+ next();
358
+ }
359
+ </script>
360
+
361
+ <script setup lang="ts">
362
+ // page component as usual
363
+ </script>
364
+
365
+ <template>
366
+ <section>…</section>
367
+ </template>
368
+ ```
369
+
370
+ Equivalent: **`export { myGuard as middleware }`** in the same file.
371
+
372
+ **Requirement:** the merged glob must **eagerly** load any page module that exports **`middleware`**, e.g. `import.meta.glob('/src/pages/**/*.vue', { eager: true })` for those routes, or list them in a separate eager pattern. Otherwise the export is not available when `createNnnRoutes` runs.
373
+
374
+ ### `.ts`/`.js` route modules
375
+
376
+ The same **`middleware`** export applies; those entries must still be **eager** in the merged glob map when you rely on composed `middleware`.
377
+
378
+ ### Lazy glob caveat
379
+
380
+ ⚠️ If the page file is only imported through a **lazy** glob (no **`eager: true`**), **`createNnnRoutes`** never sees **`middleware`** exports at generation time — the leaf **`beforeEnter`** contains **only** directory `_middleware.ts` guards that were eager-loaded.
381
+
382
+ **Reliable split:** eagerly glob **`/_middleware.ts`** only, keep **`*.vue`** lazy. See [Eager vs lazy `import.meta.glob`](#eager-vs-lazy-importmetaglob).
383
+
384
+ ---
385
+
386
+ ## Eager vs lazy `import.meta.glob`
387
+
388
+ ### All eager (simplest — matches quick start)
389
+
390
+ ```ts
391
+ const modules = import.meta.glob(["/src/pages/**/*.vue", "/src/pages/**/_middleware.ts"], {
392
+ eager: true,
393
+ });
394
+
395
+ createNnnRoutes(modules as Record<string, unknown>, { routesRoot: "src/pages" });
396
+ ```
397
+
398
+ ### Lazy pages + eager middleware (recommended split)
399
+
400
+ Keeps **`_middleware.ts`** synchronous so cascading guards attach correctly:
401
+
402
+ ```ts
403
+ const lazyViews = import.meta.glob("/src/pages/**/*.vue"); // eager: false implied
404
+ const eagerMw = import.meta.glob("/src/pages/**/_middleware.ts", { eager: true });
405
+
406
+ const modules = {
407
+ ...(lazyViews as Record<string, unknown>),
408
+ ...(eagerMw as Record<string, unknown>),
409
+ };
410
+
411
+ const routes = createNnnRoutes(modules, {
412
+ routesRoot: "src/pages",
413
+ });
414
+ ```
415
+
416
+ **Note:** Middleware files under **`/src/pages/**`** are usually separate keys from **`.vue`** files — no key collision unless you overlap patterns carelessly.
417
+
418
+ ---
419
+
420
+ ## Route meta & utilities
421
+
422
+ Each generated leaf sets **`meta.nnnFile`** to the **original** glob key (before strip), which helps editors and debugger linking.
423
+
424
+ **Exported helpers:** `createSpaNnnRoutes`, `pathNoExt`, `segmentUrlFromFs`, `mwPrefixesForPathNoExt`, **`warnIfRoutesRootLikelyWrong`**, `simplifyGlobKey`, **`stripRoutesRoot`**, `normalizePath`, `pathFromSegments`, **`isMiddlewareKey`**, **`middlewareDirFromNormKey`**, **`middlewareLogicalKey`**, **`dynamicScore`**.
425
+
426
+ ---
427
+
428
+ ## Run the demo (this repo)
429
+
430
+ The `demo/` app aliases **`vue-nnn-router`** to **`../src`**.
431
+
432
+ ```bash
433
+ npm install
434
+ npm run demo:install
435
+ npm run demo:dev
436
+ ```
437
+
438
+ Demo pages live in **`demo/src/pages/`**.
439
+
440
+ ---
441
+
442
+ ## Package
443
+
444
+ [**vue-nnn-router on npm**](https://www.npmjs.com/package/vue-nnn-router) · [CHANGELOG.md](CHANGELOG.md)
445
+
446
+ ## License
447
+
448
+ MIT — see [LICENSE](LICENSE).