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 +21 -0
- package/README.md +587 -0
- package/package.json +43 -0
- package/src/HistoryManager.js +95 -0
- package/src/Link.js +97 -0
- package/src/MiddlewareChain.js +47 -0
- package/src/RouteMatcher.js +156 -0
- package/src/RouteTree.js +187 -0
- package/src/SafaRouter.js +464 -0
- package/src/constants.js +47 -0
- package/src/errors.js +46 -0
- package/src/index.js +16 -0
- package/src/link-helper.js +24 -0
- package/src/utils.js +122 -0
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> ·
|
|
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) © 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) © 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
|
+
}
|