safa-router 1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SafaRouter
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,587 @@
1
+ <div align="center">
2
+ <h1>SafaRouter</h1>
3
+ <p><strong>A standalone frontend router inspired by Next.js App Router</strong></p>
4
+ <p>
5
+ <img src="https://img.shields.io/badge/version-1.0.1-blue" alt="Version">
6
+ <img src="https://img.shields.io/badge/license-MIT-green" alt="License">
7
+ <img src="https://img.shields.io/badge/dependencies-0-brightgreen" alt="Zero Dependencies">
8
+ <img src="https://img.shields.io/badge/size-%3C5%20KB-gold" alt="Size">
9
+ </p>
10
+ <p>
11
+ <a href="#english">English</a> &middot;
12
+ <a href="#persian">فارسی</a>
13
+ </p>
14
+ <br>
15
+ </div>
16
+
17
+ ---
18
+
19
+ <a id="english"></a>
20
+
21
+ ## English
22
+
23
+ **SafaRouter** brings the App Router development pattern — nested layouts, dynamic routes, middleware — to any frontend project without requiring Next.js or a specific framework.
24
+
25
+ Use it with React, Vue, Svelte, or vanilla JavaScript. Pure JS, zero dependencies, under 5 KB gzipped.
26
+
27
+ ### Quick Start (1 minute)
28
+
29
+ **1. Install**
30
+ ```bash
31
+ npm install safa-router
32
+ ```
33
+
34
+ **2. Create HTML pages**
35
+ ```
36
+ my-project/
37
+ ├── pages/
38
+ │ ├── index.html ← /
39
+ │ └── about.html ← /about
40
+ ├── index.html
41
+ └── main.js
42
+ ```
43
+
44
+ **3. Initialize router**
45
+ ```js
46
+ import { SafaRouter } from 'safa-router'
47
+
48
+ new SafaRouter({
49
+ target: '#app',
50
+ pageDir: 'pages', // auto-resolve .html files
51
+ }).start()
52
+ ```
53
+
54
+ That's it. Each `.html` file in `pages/` becomes a route automatically.
55
+
56
+ ---
57
+
58
+ ### Features
59
+
60
+ - **HTML-first** — Write plain `.html` files, no JS components needed
61
+ - **Auto routing** — Files in `pageDir` become routes automatically
62
+ - **Dynamic segments** — `[slug]`, `[...catchAll]`, `[[...optionalCatchAll]]`
63
+ - **Route groups** — `(groupName)` for logical organisation
64
+ - **Nested layouts** — Layouts wrap pages and compose parent → child
65
+ - **Client-side navigation** — History API with push, replace, back, forward
66
+ - **Middleware** — Auth, redirects, logging on every navigation
67
+ - **404 & error pages** — Built-in fallbacks with custom overrides
68
+ - **Event system** — Subscribe to navigation lifecycle events
69
+ - **Zero dependencies** — Pure JavaScript, ESM, ~5 KB gzipped
70
+ - **Framework agnostic** — Works with React, Vue, Svelte, or vanilla JS
71
+
72
+ ---
73
+
74
+ ### Table of Contents
75
+
76
+ - [Installation](#installation)
77
+ - [Simple Usage (HTML only)](#simple-usage-html-only)
78
+ - [Route Config Usage (JS components)](#route-config-usage-js-components)
79
+ - [API Reference](#api-reference)
80
+ - [Examples](#examples)
81
+ - [Running the Demo](#running-the-demo)
82
+
83
+ ---
84
+
85
+ ### Installation
86
+
87
+ ```bash
88
+ npm install safa-router
89
+ ```
90
+
91
+ Or copy the `src/` folder directly into your project.
92
+
93
+ ---
94
+
95
+ ### Simple Usage (HTML only)
96
+
97
+ Create a folder with `.html` files. Each file becomes a route:
98
+
99
+ ```
100
+ pages/
101
+ index.html → /
102
+ about.html → /about
103
+ contact.html → /contact
104
+ blog/
105
+ index.html → /blog
106
+ post.html → /blog/post
107
+ ```
108
+
109
+ ```js
110
+ import { SafaRouter } from 'safa-router'
111
+
112
+ new SafaRouter({
113
+ target: '#app',
114
+ pageDir: 'pages',
115
+ layout: ({ children }) => `
116
+ <header><nav>...</nav></header>
117
+ <main>${children}</main>
118
+ `,
119
+ }).start()
120
+ ```
121
+
122
+ The `layout` option wraps all pages with a global template.
123
+
124
+ #### Links in HTML pages
125
+
126
+ Use `data-safa-link` attribute for client-side navigation:
127
+
128
+ ```html
129
+ <a href="/about" data-safa-link>About</a>
130
+ <a href="/contact" data-safa-link>Contact</a>
131
+ ```
132
+
133
+ ---
134
+
135
+ ### Route Config Usage (JS components)
136
+
137
+ For advanced features (dynamic routes, middleware, per-route layouts), define routes explicitly:
138
+
139
+ ```js
140
+ import { SafaRouter } from 'safa-router'
141
+
142
+ const router = new SafaRouter({
143
+ target: '#app',
144
+ pageDir: 'pages', // fallback for simple pages
145
+
146
+ layout: ({ children }) => `
147
+ <nav>
148
+ <a href="/" data-safa-link>Home</a>
149
+ <a href="/blog" data-safa-link>Blog</a>
150
+ </nav>
151
+ <main>${children}</main>
152
+ `,
153
+
154
+ routes: {
155
+ '/': { page: () => `<h1>Home</h1>` },
156
+
157
+ '/blog': {
158
+ page: () => `<h1>Blog</h1>`,
159
+ children: {
160
+ '[slug]': {
161
+ page: ({ params }) => `<h1>Post: ${params.slug}</h1>`,
162
+ },
163
+ },
164
+ },
165
+
166
+ '/dashboard': {
167
+ layout: ({ children }) => `<aside>Sidebar</aside><div>${children}</div>`,
168
+ page: () => `<h1>Dashboard</h1>`,
169
+ },
170
+ },
171
+
172
+ notFound: ({ path }) => `<h1>404 — ${path} not found</h1>`,
173
+ error: ({ error }) => `<h1>Error</h1><pre>${error.message}</pre>`,
174
+ })
175
+
176
+ router.use(async (ctx, next) => {
177
+ if (ctx.path.startsWith('/dashboard') && !isLoggedIn()) {
178
+ ctx.redirect = '/login'
179
+ return
180
+ }
181
+ return next()
182
+ })
183
+
184
+ router.start()
185
+ ```
186
+
187
+ ---
188
+
189
+ ### API Reference
190
+
191
+ #### `new SafaRouter(options)`
192
+
193
+ | Option | Type | Default | Description |
194
+ |--------|------|---------|-------------|
195
+ | `target` | `string\|Element` | `'#app'` | CSS selector or element to render into |
196
+ | `pageDir` | `string` | `undefined` | Folder with `.html` files for auto-routing |
197
+ | `layout` | `Function\|string` | `null` | Global layout wrapping all pages |
198
+ | `routes` | `object` | `{}` | Explicit route definitions (overrides `pageDir`) |
199
+ | `notFound` | `Function\|string` | `null` | Custom 404 page |
200
+ | `error` | `Function\|string` | `null` | Custom error page |
201
+ | `basePath` | `string` | `''` | Base path when app is served from subdirectory |
202
+ | `useHash` | `boolean` | `false` | Use `#hash` routing instead of History API |
203
+ | `scrollToTop` | `boolean` | `true` | Scroll to top on navigation |
204
+ | `cacheRoutes` | `boolean` | `true` | Cache loaded components |
205
+ | `titleTemplate` | `string` | `null` | Document title template (e.g. `'%s — My App'`) |
206
+ | `transitionDuration` | `number` | `0` | Fade transition duration in ms |
207
+
208
+ #### Router methods
209
+
210
+ | Method | Description |
211
+ |--------|-------------|
212
+ | `router.start(target?)` | Initialize and start the router |
213
+ | `router.push(url, state?)` | Navigate to URL (new history entry) |
214
+ | `router.replace(url, state?)` | Navigate (replace current history entry) |
215
+ | `router.back()` | Go back |
216
+ | `router.forward()` | Go forward |
217
+ | `router.reload()` | Re-render current route |
218
+ | `router.use(fn)` | Add middleware |
219
+ | `router.on(event, fn)` | Subscribe to event |
220
+ | `router.off(event, fn)` | Unsubscribe |
221
+ | `router.createLink(config)` | Create a Link instance |
222
+ | `router.prefetch(path)` | Preload a page |
223
+ | `router.clearCache()` | Clear component cache |
224
+ | `router.getRoute(path)` | Inspect a route definition |
225
+ | `router.destroy()` | Clean up and stop |
226
+
227
+ #### Router properties
228
+
229
+ | Property | Type | Description |
230
+ |----------|------|-------------|
231
+ | `router.pathname` | `string` | Current URL path |
232
+ | `router.params` | `object` | Dynamic route parameters |
233
+ | `router.query` | `object` | URL query parameters |
234
+ | `router.loading` | `boolean` | Navigation in progress |
235
+ | `router.matchedRoute` | `object\|null` | Current matched route info |
236
+ | `router.currentRoute` | `object\|null` | Current route data |
237
+
238
+ #### Route tree structure
239
+
240
+ Each key in `routes` represents a URL segment. Values can be:
241
+
242
+ ```js
243
+ {
244
+ page: Function|string, // Component: function returning HTML, or path to HTML/JS file
245
+ layout: Function|string, // Layout wrapping children: ({ children, params, router }) => HTML
246
+ children: { ... }, // Nested route definitions
247
+ loading: Function|string, // Loading component shown during page load
248
+ error: Function|string, // Per-route error component
249
+ notFound: Function|string, // Per-route 404 component
250
+ meta: { title: string }, // Route metadata (used for document title)
251
+ }
252
+ ```
253
+
254
+ **Special segments:**
255
+
256
+ | Pattern | Example | Description |
257
+ |---------|---------|-------------|
258
+ | `[param]` | `[slug]` | Matches one path segment |
259
+ | `[...param]` | `[...path]` | Matches one or more segments |
260
+ | `[[...param]]` | `[[...tags]]` | Matches zero or more segments |
261
+ | `(group)` | `(auth)` | Route group — doesn't affect URL |
262
+
263
+ #### Component signature
264
+
265
+ Every component receives an object:
266
+
267
+ ```js
268
+ {
269
+ params, // {} — dynamic route parameters
270
+ query, // {} — URL query parameters
271
+ router, // SafaRouter instance
272
+ children, // string — (layout only) content to wrap
273
+ path, // string — (notFound/error only) attempted path
274
+ error, // Error — (error only) the caught error
275
+ }
276
+ ```
277
+
278
+ Components must return an **HTML string**.
279
+
280
+ #### Events
281
+
282
+ | Event | Payload | Description |
283
+ |-------|---------|-------------|
284
+ | `beforenavigate` | `{ path, method }` | Before navigation starts |
285
+ | `routechange` | `{ pathname, params, query }` | Route changed |
286
+ | `afternavigate` | `{ pathname }` | After render completes |
287
+ | `loading` | `{ path, loading }` | Loading state changed |
288
+ | `notfound` | `{ path }` | Route not found |
289
+ | `error` | `{ path, error }` | Navigation error |
290
+ | `ready` | `{ pathname }` | Router initialized |
291
+ | `destroy` | `{}` | Router destroyed |
292
+
293
+ #### Middleware
294
+
295
+ ```js
296
+ router.use(async (ctx, next) => {
297
+ // ctx = { path, method, query, cancelled, redirect }
298
+
299
+ if (ctx.path.startsWith('/admin') && !isAdmin()) {
300
+ ctx.redirect = '/login' // redirect somewhere else
301
+ return
302
+ }
303
+
304
+ if (!featureEnabled) {
305
+ ctx.cancelled = true // cancel navigation
306
+ return
307
+ }
308
+
309
+ return next() // continue
310
+ })
311
+ ```
312
+
313
+ ---
314
+
315
+ ### Examples
316
+
317
+ #### With global layout and HTML pages
318
+
319
+ ```
320
+ pages/
321
+ index.html
322
+ about.html
323
+ contact.html
324
+ ```
325
+
326
+ ```js
327
+ new SafaRouter({
328
+ target: '#app',
329
+ pageDir: 'pages',
330
+ layout: ({ children, router }) => `
331
+ <header>
332
+ <nav>
333
+ <a href="/" data-safa-link>Home</a>
334
+ <a href="/about" data-safa-link>About</a>
335
+ <a href="/contact" data-safa-link>Contact</a>
336
+ </nav>
337
+ </header>
338
+ <main>${children}</main>
339
+ `,
340
+ }).start()
341
+ ```
342
+
343
+ #### With React
344
+
345
+ ```jsx
346
+ import { useEffect, useState } from 'react'
347
+ import { SafaRouter } from 'safa-router'
348
+
349
+ const router = new SafaRouter({ pageDir: 'pages' })
350
+
351
+ function App() {
352
+ const [path, setPath] = useState(router.pathname)
353
+ useEffect(() => router.on('routechange', () => setPath(router.pathname)), [])
354
+
355
+ return (
356
+ <div>
357
+ <nav>
358
+ <Link href="/">Home</Link>
359
+ <Link href="/about">About</Link>
360
+ </nav>
361
+ <div id="app" />
362
+ </div>
363
+ )
364
+ }
365
+
366
+ function Link({ href, children }) {
367
+ return <a href={href} onClick={e => { e.preventDefault(); router.push(href) }}>{children}</a>
368
+ }
369
+ ```
370
+
371
+ #### Dynamic route: blog with [slug]
372
+
373
+ ```js
374
+ const router = new SafaRouter({
375
+ target: '#app',
376
+ routes: {
377
+ '/': { page: () => `<h1>Home</h1>` },
378
+ '/blog': {
379
+ page: () => `<h1>Blog</h1>`,
380
+ children: {
381
+ '[slug]': {
382
+ page: ({ params }) => `<h1>Post: ${params.slug}</h1>`,
383
+ },
384
+ },
385
+ },
386
+ },
387
+ })
388
+ ```
389
+
390
+ #### Nested layouts
391
+
392
+ ```js
393
+ const router = new SafaRouter({
394
+ target: '#app',
395
+ routes: {
396
+ '/': {
397
+ layout: ({ children }) => `<div id="root">${children}</div>`,
398
+ page: () => `<h1>Home</h1>`,
399
+ },
400
+ '/dashboard': {
401
+ layout: ({ children }) => `<aside>Sidebar</aside><main>${children}</main>`,
402
+ page: () => `<h1>Dashboard</h1>`,
403
+ children: {
404
+ settings: {
405
+ page: () => `<h1>Settings</h1>`,
406
+ },
407
+ },
408
+ },
409
+ },
410
+ })
411
+ ```
412
+
413
+ Render order: `rootLayout > dashboardLayout > page`
414
+
415
+ #### Middleware (auth guard)
416
+
417
+ ```js
418
+ router.use(async (ctx, next) => {
419
+ const protectedPaths = ['/dashboard', '/profile']
420
+ if (protectedPaths.some(p => ctx.path.startsWith(p)) && !isAuthenticated()) {
421
+ ctx.redirect = '/login'
422
+ return
423
+ }
424
+ return next()
425
+ })
426
+ ```
427
+
428
+ ---
429
+
430
+ ### Running the Demo
431
+
432
+ ```bash
433
+ cd safa-router
434
+ node dev-server.js
435
+ ```
436
+
437
+ Then open `http://localhost:3000/test-app/`
438
+
439
+ The demo shows the full feature set: HTML pages (`pageDir`), JS components, nested layouts, dynamic routes, middleware, loading state, and 404/error handling.
440
+
441
+ ---
442
+
443
+ ### Contributing
444
+
445
+ Contributions are welcome! Please open an issue or submit a PR on [GitHub](https://github.com/Karan-Safaie-Qadi/SafaRouter).
446
+
447
+ ---
448
+
449
+ ### License
450
+
451
+ [MIT](LICENSE) &copy; 2026 SafaRouter
452
+
453
+ ---
454
+
455
+ <a id="persian"></a>
456
+
457
+ ## فارسی
458
+
459
+ **صفا روتر** الگوی مسیریابی اپ روتِر نکست‌جی‌اس — لایه‌بندی‌های تو در تو، مسیرهای پویا، میان‌افزار — را به هر پروژه فرانت‌اندی می‌آورد.
460
+
461
+ ### شروع سریع (۱ دقیقه‌ای)
462
+
463
+ ```bash
464
+ npm install safa-router
465
+ ```
466
+
467
+ فایل‌های HTML بسازید:
468
+ ```
469
+ my-project/
470
+ ├── pages/
471
+ │ ├── index.html ← /
472
+ │ └── about.html ← /about
473
+ ├── index.html
474
+ └── main.js
475
+ ```
476
+
477
+ روتر را مقداردهی کنید:
478
+ ```js
479
+ import { SafaRouter } from 'safa-router'
480
+
481
+ new SafaRouter({
482
+ target: '#app',
483
+ pageDir: 'pages',
484
+ }).start()
485
+ ```
486
+
487
+ ### ویژگی‌ها
488
+
489
+ - **HTML-first** — فایل `.html` خالص بنویسید، نیازی به کامپوننت JS نیست
490
+ - **مسیریابی خودکار** — فایل‌های داخل `pageDir` خودکار تبدیل به مسیر می‌شوند
491
+ - **بخش‌های پویا** — `[slug]` و `[...catchAll]`
492
+ - **لایه‌بندی تو در تو** — صفحات را با لِی‌اوت محصور کنید
493
+ - **میان‌افزار** — احراز هویت، تغییر مسیر، لاگ‌گیری
494
+ - **صفحات ۴۰۴ و خطا** — پیش‌فرض داخلی با امکان سفارشی‌سازی
495
+ - **بدون وابستگی** — جاواسکریپت خالص، کمتر از ۵ کیلوبایت
496
+
497
+ ### آموزش کامل
498
+
499
+ #### استفاده ساده (فقط HTML)
500
+
501
+ فایل‌های HTML داخل یک پوشه. هر فایل یک مسیر می‌شود:
502
+
503
+ ```
504
+ pages/
505
+ index.html → /
506
+ about.html → /about
507
+ contact.html → /contact
508
+ ```
509
+
510
+ ```js
511
+ import { SafaRouter } from 'safa-router'
512
+
513
+ new SafaRouter({
514
+ target: '#app',
515
+ pageDir: 'pages',
516
+ layout: ({ children }) => `
517
+ <header>
518
+ <nav>
519
+ <a href="/" data-safa-link>خانه</a>
520
+ <a href="/about" data-safa-link>درباره</a>
521
+ </nav>
522
+ </header>
523
+ <main>${children}</main>
524
+ `,
525
+ }).start()
526
+ ```
527
+
528
+ برای لینک‌ها از `data-safa-link` استفاده کنید:
529
+ ```html
530
+ <a href="/about" data-safa-link>درباره ما</a>
531
+ ```
532
+
533
+ #### استفاده پیشرفته (با کامپوننت JS)
534
+
535
+ ```js
536
+ import { SafaRouter } from 'safa-router'
537
+
538
+ const router = new SafaRouter({
539
+ target: '#app',
540
+ layout: ({ children }) => `<nav>...</nav><main>${children}</main>`,
541
+
542
+ routes: {
543
+ '/': { page: () => `<h1>خانه</h1>` },
544
+ '/blog': {
545
+ page: () => `<h1>وبلاگ</h1>`,
546
+ children: {
547
+ '[slug]': {
548
+ page: ({ params }) => `<h1>مطلب: ${params.slug}</h1>`,
549
+ },
550
+ },
551
+ },
552
+ },
553
+ })
554
+
555
+ router.start()
556
+ ```
557
+
558
+ ### راهنمای کامل API
559
+
560
+ | گزینه | نوع | پیش‌فرض | توضیح |
561
+ |-------|-----|---------|-------|
562
+ | `target` | `string\|Element` | `'#app'` | محل رندر |
563
+ | `pageDir` | `string` | `undefined` | پوشه فایل‌های HTML |
564
+ | `layout` | `Function\|string` | `null` | لِی‌اوت سراسری |
565
+ | `routes` | `object` | `{}` | تعریف مسیرها (دستی) |
566
+ | `notFound` | `Function\|string` | `null` | صفحه ۴۰۴ |
567
+ | `error` | `Function\|string` | `null` | صفحه خطا |
568
+ | `basePath` | `string` | `''` | مسیر پایه |
569
+ | `scrollToTop` | `boolean` | `true` | اسکرول به بالا |
570
+ | `titleTemplate` | `string` | `null` | قالب عنوان صفحه |
571
+
572
+ ### اجرای دمو
573
+
574
+ ```bash
575
+ cd safa-router
576
+ node dev-server.js
577
+ ```
578
+
579
+ باز کنید: `http://localhost:3000/test-app/`
580
+
581
+ ### مشارکت
582
+
583
+ مشارکت شما خوش‌آمد است. لطفاً issue یا PR در [GitHub](https://github.com/Karan-Safaie-Qadi/SafaRouter) ثبت کنید.
584
+
585
+ ### مجوز
586
+
587
+ [MIT](LICENSE) &copy; 2026 SafaRouter
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "safa-router",
3
+ "version": "1.0.1",
4
+ "description": "A professional standalone frontend router inspired by Next.js App Router — works with any framework or vanilla JS",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js",
9
+ "./*": "./src/*.js"
10
+ },
11
+ "files": [
12
+ "src/"
13
+ ],
14
+ "scripts": {
15
+ "start": "npx serve test-app -l 3000",
16
+ "test": "echo \"Tests coming soon\"",
17
+ "lint": "echo \"Add lint tool\"",
18
+ "prepublishOnly": "npm test"
19
+ },
20
+ "keywords": [
21
+ "router",
22
+ "frontend-router",
23
+ "spa-router",
24
+ "app-router",
25
+ "nextjs-router",
26
+ "vanilla-js",
27
+ "client-side-routing",
28
+ "single-page-app",
29
+ "nested-layouts",
30
+ "middleware",
31
+ "dynamic-routes"
32
+ ],
33
+ "author": "",
34
+ "license": "MIT",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/pishro-dev/safa-router.git"
38
+ },
39
+ "bugs": {
40
+ "url": "https://github.com/pishro-dev/safa-router/issues"
41
+ },
42
+ "homepage": "https://github.com/pishro-dev/safa-router#readme"
43
+ }