├── .github
├── funding.yml
└── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── demo
├── src
│ ├── views
│ │ ├── ManageCards.vue
│ │ ├── ReviewPaymentMethod.vue
│ │ ├── CancelSubscription.vue
│ │ ├── About.vue
│ │ ├── Home.vue
│ │ ├── StartUpgrade.vue
│ │ ├── SelectNewPlan.vue
│ │ ├── UpgradeSubscription.vue
│ │ ├── ManageAccount.vue
│ │ └── ViewSubscription.vue
│ ├── main.css
│ ├── assets
│ │ └── logo.png
│ ├── App.vue
│ └── main.js
├── .gitignore
├── public
│ └── favicon.ico
├── postcss.config.js
├── vite.config.js
├── tailwind.config.js
├── index.html
└── package.json
├── .gitignore
├── src
├── errors
│ ├── guard-error.js
│ └── type-assertion-error.js
├── utilities
│ ├── string-populated.js
│ ├── index.js
│ ├── trim-character.js
│ ├── clean-route-param.js
│ └── filter-object.js
├── main.js
├── route
│ ├── bag.js
│ ├── compiler.js
│ ├── route.js
│ └── factory.js
├── assertions
│ └── guard.js
└── guard.js
├── .npmignore
├── .editorconfig
├── license.md
├── package.json
├── readme.md
├── upgrading.md
└── changelog.md
/.github/funding.yml:
--------------------------------------------------------------------------------
1 | custom: ['https://rockett.pw/donate']
2 |
--------------------------------------------------------------------------------
/demo/src/views/ManageCards.vue:
--------------------------------------------------------------------------------
1 |
2 | Cards
3 |
4 |
--------------------------------------------------------------------------------
/demo/src/main.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/demo/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | dist
4 | dist-ssr
5 | *.local
6 | .vscode
7 |
--------------------------------------------------------------------------------
/demo/src/views/ReviewPaymentMethod.vue:
--------------------------------------------------------------------------------
1 |
2 | Payment Method
3 |
4 |
--------------------------------------------------------------------------------
/demo/src/views/CancelSubscription.vue:
--------------------------------------------------------------------------------
1 |
2 | Cancel Subscription
3 |
4 |
--------------------------------------------------------------------------------
/demo/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mikerockett/vue-routisan/HEAD/demo/public/favicon.ico
--------------------------------------------------------------------------------
/demo/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mikerockett/vue-routisan/HEAD/demo/src/assets/logo.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.vscode
3 | /dist
4 | /node_modules
5 | npm-debug.log
6 | yarn-error.log
7 | yarn.lock
8 |
--------------------------------------------------------------------------------
/src/errors/guard-error.js:
--------------------------------------------------------------------------------
1 | export function guardError(reason) {
2 | return new Error(`GuardError: ${reason}`)
3 | }
4 |
--------------------------------------------------------------------------------
/src/utilities/string-populated.js:
--------------------------------------------------------------------------------
1 | export function stringPopulated(string) {
2 | return string.trim() !== ''
3 | }
4 |
--------------------------------------------------------------------------------
/demo/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | export { Factory } from './route/factory'
2 | export { Route } from './route/route'
3 | export { Guard } from './guard'
4 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | /.github
2 | /.vscode
3 | /demo
4 | .editorconfig
5 | .gitignore
6 | changelog.md
7 | license.md
8 | readme.md
9 | upgrading.md
10 |
--------------------------------------------------------------------------------
/demo/src/views/About.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
About
4 |
5 |
6 |
--------------------------------------------------------------------------------
/demo/src/views/Home.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Home
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/errors/type-assertion-error.js:
--------------------------------------------------------------------------------
1 | export function typeAssertionError(context, expected) {
2 | return new TypeError(`${context} must be of type ${expected}`)
3 | }
4 |
--------------------------------------------------------------------------------
/demo/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import vue from '@vitejs/plugin-vue'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [vue()]
7 | })
8 |
--------------------------------------------------------------------------------
/demo/src/views/StartUpgrade.vue:
--------------------------------------------------------------------------------
1 |
2 | Select New Plan
3 |
4 |
--------------------------------------------------------------------------------
/demo/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | mode: 'jit',
3 | purge: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
4 | darkMode: false,
5 | theme: {
6 | extend: {},
7 | },
8 | plugins: [],
9 | }
10 |
--------------------------------------------------------------------------------
/src/utilities/index.js:
--------------------------------------------------------------------------------
1 | export { cleanRouteParam } from './clean-route-param'
2 | export { filterObject } from './filter-object'
3 | export { stringPopulated } from './string-populated'
4 | export { trim } from './trim-character'
5 |
--------------------------------------------------------------------------------
/demo/src/views/SelectNewPlan.vue:
--------------------------------------------------------------------------------
1 |
2 | Review Payment Method
3 |
4 |
--------------------------------------------------------------------------------
/src/utilities/trim-character.js:
--------------------------------------------------------------------------------
1 | export function trim(input, mask = '/') {
2 | while (~mask.indexOf(input[0])) input = input.slice(1)
3 | while (~mask.indexOf(input[input.length - 1])) input = input.slice(0, -1)
4 |
5 | return input
6 | }
7 |
--------------------------------------------------------------------------------
/demo/src/views/UpgradeSubscription.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Upgrade Subscription
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/route/bag.js:
--------------------------------------------------------------------------------
1 | export class RouteBag {
2 | constructor() {
3 | this.routes = []
4 | }
5 |
6 | pushRoute(route) {
7 | this.routes.push(route)
8 | }
9 |
10 | compiled() {
11 | return this.routes.map((route) => route.compile())
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/assertions/guard.js:
--------------------------------------------------------------------------------
1 | import { typeAssertionError } from '../errors/type-assertion-error'
2 | import { Guard } from '../guard'
3 |
4 | export function assertGuard(instance, context) {
5 | if (!(instance instanceof Guard)) throw typeAssertionError(context, 'Guard')
6 |
7 | return instance
8 | }
9 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_size = 2
6 | indent_style = space
7 | end_of_line = lf
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [{package.json}]
12 | indent_size = 2
13 |
14 | [*.md]
15 | trim_trailing_whitespace = false
16 |
--------------------------------------------------------------------------------
/src/utilities/clean-route-param.js:
--------------------------------------------------------------------------------
1 | export function cleanRouteParam(string) {
2 | return string
3 | .replace(/\}\{/, '}/{')
4 | .replace(/\{all\}/, '(.*)')
5 | .replace(/\(number\)/, '(\\d+)')
6 | .replace(/\(string\)/, '(\\w+)')
7 | .replace(/\{(\w+)\}/, ':$1')
8 | .trim()
9 | }
10 |
--------------------------------------------------------------------------------
/src/utilities/filter-object.js:
--------------------------------------------------------------------------------
1 | export function filterObject(object, predicate) {
2 | let key
3 | let filteredObject = {}
4 |
5 | for (key in object) {
6 | if (object.hasOwnProperty(key) && !predicate(object[key])) {
7 | filteredObject[key] = object[key]
8 | }
9 | }
10 |
11 | return filteredObject
12 | }
13 |
--------------------------------------------------------------------------------
/demo/src/views/ManageAccount.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Manage Account
4 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite App
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/demo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "demo",
3 | "version": "0.0.0",
4 | "scripts": {
5 | "dev": "vite",
6 | "build": "vite build",
7 | "serve": "vite serve"
8 | },
9 | "dependencies": {
10 | "vue": "^3.2.13",
11 | "vue-router": "^4.0.11",
12 | "vue-routisan": "link:../"
13 | },
14 | "devDependencies": {
15 | "@vitejs/plugin-vue": "^1.9.0",
16 | "autoprefixer": "^10.3.5",
17 | "postcss": "^8.3.8",
18 | "tailwindcss": "^2.2.15",
19 | "vite": "^2.5.10"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/demo/src/views/ViewSubscription.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Subscription
4 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: 'bug: unconfirmed'
6 | assignees: mikerockett
7 |
8 | ---
9 |
10 | **Describe the bug**
11 |
12 |
13 | **To Reproduce**
14 |
15 |
16 | **Expected behavior**
17 |
18 |
19 | **Versions:**
20 |
21 | - Vue Routisan:
22 | - Vue Router:
23 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: mikerockett
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 |
12 |
13 | **Describe the solution you'd like**
14 |
15 |
16 | **Describe alternatives you've considered**
17 |
18 |
19 | **Additional context**
20 |
21 |
--------------------------------------------------------------------------------
/demo/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
12 |
{{ $route.matched }}
13 |
14 |
15 |
16 |
17 |
20 |
--------------------------------------------------------------------------------
/license.md:
--------------------------------------------------------------------------------
1 | # ISC Licence
2 |
3 | Copyright © 2020 by Mike Rockétt
4 |
5 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
6 |
7 | THE SOFTWARE IS PROVIDED “AS IS” AND ISC DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
8 |
9 | ## Learn More
10 |
11 | https://www.isc.org/licenses/
12 |
--------------------------------------------------------------------------------
/src/guard.js:
--------------------------------------------------------------------------------
1 | export class Guard {
2 | constructor(name) {
3 | this.name = name || 'UntitledGuard'
4 | }
5 |
6 | promise(context) {
7 | return new Promise((resolve, reject) => {
8 | this.handle(resolve, reject, context)
9 | })
10 | }
11 |
12 | get canLog() {
13 | const logMethod = 'logPromiseOutcomes'
14 | return this[logMethod] && typeof this[logMethod] === 'function' && this[logMethod]()
15 | }
16 |
17 | logResolution({ from, to }) {
18 | this.canLog && console.info(`${this.name}.resolved: ${from.path} → ${to.path}`)
19 | }
20 |
21 | logRejection({ from, to }, rejection) {
22 | this.canLog && console.warn(`${this.name}.rejected: ${from.path} → ${to.path} with:`, rejection)
23 | }
24 |
25 | handle(resolve) {
26 | console.warn('Guards must implement the handle() method.')
27 | resolve()
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-routisan",
3 | "description": "Elegant, fluent route definitions for Vue Router, inspired by Laravel.",
4 | "version": "3.0.0-beta.5",
5 | "author": "@mikerockett",
6 | "license": "ISC",
7 | "homepage": "https://vue-routisan.rockett.pw",
8 | "bugs": "https://github.com/mikerockett/vue-routisan/issues",
9 | "source": "src/main.js",
10 | "main": "dist/vue-routisan.js",
11 | "keywords": [
12 | "vue",
13 | "vuejs",
14 | "vue-router",
15 | "laravel",
16 | "guard",
17 | "middleware",
18 | "artisan"
19 | ],
20 | "repository": {
21 | "type": "git",
22 | "url": "https://github.com/mikerockett/vue-routisan.git"
23 | },
24 | "mangle": {
25 | "regex": "^_"
26 | },
27 | "scripts": {
28 | "clean": "rimraf dist/*",
29 | "bundle": "esbuild src/main.js --platform=node --bundle --format=esm --minify --outfile=dist/vue-routisan.js",
30 | "watch": "yarn clean && esbuild src/main.js --platform=node --bundle --format=esm --sourcemap --watch --outfile=dist/vue-routisan.js",
31 | "build": "yarn clean && yarn bundle",
32 | "prepublishOnly": "yarn build"
33 | },
34 | "devDependencies": {
35 | "esbuild": "^0.13.2",
36 | "rimraf": "^3.0.2"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/demo/src/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue'
2 | import { createRouter, createWebHistory } from 'vue-router'
3 | import { Factory, Route, Guard } from 'vue-routisan'
4 | import App from './App.vue'
5 |
6 | import './main.css'
7 |
8 | class MyGuard extends Guard {
9 | handle(resolve, reject, context) {
10 | console.log('MyGuard triggered')
11 | resolve()
12 | }
13 | }
14 |
15 | const views = import.meta.globEager('./views/**/*.vue')
16 | const view = (path) => views[`./views/${path}.vue`].default
17 |
18 | Factory.usingResolver(view).withGuards({ MyGuard })
19 |
20 | Route.view('/', 'Home').name('home')
21 | Route.view('/about', 'About').guard('MyGuard').name('about')
22 |
23 | Route.group({ prefix: 'account', name: 'account' }, () => {
24 | Route.view('/', 'ManageAccount').name('manage')
25 |
26 | Route.group({ prefix: 'subscription', name: 'subscription' }, () => {
27 | Route.view('/', 'ViewSubscription').name('view')
28 | Route.view('cancel', 'CancelSubscription').name('cancel')
29 |
30 | Route.view('upgrade', 'UpgradeSubscription').name('upgrade').children(() => {
31 | Route.view('/', 'StartUpgrade').name('start')
32 | Route.group({ prefix: 'steps' }, () => {
33 | Route.view('select-new-plan', 'SelectNewPlan').name('select-new-plan')
34 | Route.view('review-payment-method', 'ReviewPaymentMethod').name('review-payment-method')
35 | })
36 | })
37 | })
38 |
39 | Route.view('cards', 'ManageCards').name('cards')
40 | })
41 |
42 | Factory.dump()
43 | Route.dump()
44 |
45 | const router = createRouter({
46 | routes: Factory.routes(),
47 | history: createWebHistory(),
48 | })
49 |
50 | const app = createApp(App)
51 |
52 | app.use(router)
53 |
54 | app.mount('#app')
55 |
--------------------------------------------------------------------------------
/src/route/compiler.js:
--------------------------------------------------------------------------------
1 | import { Factory } from './factory'
2 | import { guardError } from '../errors/guard-error'
3 | import { trim, filterObject } from '../utilities'
4 |
5 | export class Compiler {
6 | constructor(route) {
7 | this.route = route
8 | }
9 |
10 | compile(nested = false) {
11 | const children = this.route._children
12 | const hasChildren = children.length > 0
13 |
14 | const compiled = {
15 | components: {},
16 | path: this.compilePath(nested),
17 | redirect: this.route._redirect,
18 | children: children.map((child) => child.compile(true)),
19 | name: hasChildren ? undefined : this.compileName(),
20 | alias: hasChildren ? undefined : this.route._alias,
21 | meta: hasChildren ? undefined : this.route._meta,
22 | props: hasChildren ? undefined : this.route._props,
23 | }
24 |
25 | this.components(compiled)
26 |
27 | if (this.route._guards.size > 0) {
28 | this.beforeEnter(compiled)
29 | }
30 |
31 | return filterObject(
32 | compiled,
33 | (item) => item instanceof Function
34 | ? false
35 | : item === undefined || (item instanceof Object && !Object.keys(item).length)
36 | )
37 | }
38 |
39 | components(compiled) {
40 | for (const [name, component] of Object.entries(this.route._components)) {
41 | compiled.components[name] = component
42 | }
43 | }
44 |
45 | compilePath(nested) {
46 | const cleanedPath = trim([this.route._prefixClamp, this.route._path].join('/')) || ''
47 | return nested ? cleanedPath : `/${cleanedPath}`
48 | }
49 |
50 | compileName() {
51 | const separator = Factory.nameSeparator
52 | const compiledName = [this.route._nameClamp, this.route._name].join(separator)
53 |
54 | return trim(compiledName, separator)
55 | }
56 |
57 | beforeEnter(compiled) {
58 | compiled.beforeEnter = (to, from, next) => {
59 | Array.from(this.route._guards)
60 | .reduce(this.guardChain({ from, to }), Promise.resolve())
61 | .then(this.guardResolver({ from, to }, next))
62 | .catch(this.guardRejector({ from, to }, next))
63 | }
64 | }
65 |
66 | guardChain({ from, to }) {
67 | return (chain, current) => {
68 | this.current = current
69 | return chain.then(() => current.promise({ from, to }))
70 | }
71 | }
72 |
73 | guardResolver(context, next) {
74 | return () => {
75 | this.current.logResolution(context)
76 | next()
77 | }
78 | }
79 |
80 | guardRejector(context, next) {
81 | return (rejection) => {
82 | this.current.logRejection(context, rejection)
83 |
84 | if (context.to.name == rejection.name || context.to.path === rejection.path) {
85 | throw guardError('rejection loop detected.')
86 | }
87 |
88 | rejection = rejection === undefined
89 | ? guardError('rejection handler missing.')
90 | : this.compileRejection(rejection)
91 |
92 | next(rejection)
93 | }
94 | }
95 |
96 | compileRejection(rejection) {
97 | return rejection instanceof Function ? rejection() : rejection
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/src/route/route.js:
--------------------------------------------------------------------------------
1 | import { Compiler } from './compiler'
2 | import { Factory } from './factory'
3 | import { trim } from '../utilities'
4 |
5 | export class Route {
6 | constructor(path, view, additionalViews, redirectTo) {
7 | this.setDefaults()
8 | Factory.linkRoute(this)
9 | this._path = Factory.cleanPath(path)
10 |
11 | if (view) this._components = Factory.resolveComponents(view, additionalViews)
12 | else this._redirect = redirectTo
13 | }
14 |
15 | static view(path, view, additionalViews) {
16 | return new this(path, view, additionalViews)
17 | }
18 |
19 | static redirect(source, destination) {
20 | return new this(source, null, null, destination)
21 | }
22 |
23 | static dump() {
24 | console.log('Compiled Routes:')
25 | console.dir(Factory.compile().compiled)
26 | }
27 |
28 | name(name) {
29 | this._name = trim(name, Factory.nameSeparator)
30 | return this
31 | }
32 |
33 | alias(alias) {
34 | this._alias = Factory.cleanPath(alias)
35 | return this
36 | }
37 |
38 | meta(key, value) {
39 | return this.setObject(this._meta, 'meta', key, value)
40 | }
41 |
42 | props(key, value) {
43 | return this.setObject(this._props, 'props', key, value)
44 | }
45 |
46 | prop(key, value) {
47 | return this.props({ [key]: value })
48 | }
49 |
50 | guard(...guards) {
51 | for (const guard of guards) {
52 | this._guards.add(Factory.guard(guard))
53 | }
54 |
55 | return this
56 | }
57 |
58 | children(callable) {
59 | return Factory.withChildren(this, callable)
60 | }
61 |
62 | static group(options, callable) {
63 | Factory.withinGroup(callable ? options : {}, callable || options)
64 | }
65 |
66 | setDefaults() {
67 | this._path = undefined
68 | this._components = {}
69 | this._redirect = undefined
70 | this._nameClamp = undefined
71 | this._prefixClamp = undefined
72 | this._name = undefined
73 | this._children = []
74 | this._guards = new Set()
75 | this._alias = undefined
76 | this._meta = {}
77 | this._props = {}
78 | }
79 |
80 | clampName(clampName) {
81 | const separator = Factory.nameSeparator
82 |
83 | this._nameClamp = trim(
84 | [this._nameClamp || undefined, this._name || undefined, clampName]
85 | .filter((clamp) => clamp !== undefined)
86 | .join(separator),
87 | separator
88 | )
89 |
90 | return this
91 | }
92 |
93 | clampPrefix(clampPrefix) {
94 |
95 | this._prefixClamp = Factory.cleanPath(
96 | [this._prefixClamp || undefined, this._path || undefined, clampPrefix]
97 | .filter((clamp) => clamp !== undefined)
98 | .join('/')
99 | )
100 |
101 | return this
102 | }
103 |
104 | setObject(on, context, key, value) {
105 | if (key instanceof Object) {
106 | Object.assign(on, key)
107 | } else if (value) {
108 | on[key] = value
109 | } else {
110 | throw `${context} must be passed as key,value or a key-value object.`
111 | }
112 |
113 | return this
114 | }
115 |
116 | compile(nested) {
117 | return new Compiler(this).compile(nested)
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | ## ⚠️ Retired and Archived
2 |
3 | Vue Routisan is being retired in favour of a TypeScript rewrite under a new name, which is yet to be announced. This repository is being archived as a result.
4 |
5 | The new package will employ the same principles of Routisan, however the interface will be simpler and a lot easier to digest, even for those with minimal Vue experience. When the new package is released, it is recommended that all users of v2 and v3 move to the new package. An migration guide will be made available when this happens.
6 |
7 | **Note:** The [current documentation site](https://vue-routisan.rockett.pw/) for v3 will remain available until such time as the new package has been released. The new package will also get a brand new docs site, powered by [VitePress](https://vitepress.dev/).
8 |
9 |
10 | ---
11 |
12 | ## Readme
13 |
14 |
15 |
16 | Elegant, fluent route definitions for [Vue Router](https://router.vuejs.org/), inspired by [Laravel](https://laravel.com).
17 |
18 | 
19 | 
20 | 
21 | 
22 |
23 | **Routisan 3 is currently in beta. Stable release around the corner!**
24 |
25 |
26 |
27 | ```sh
28 | npm i vue-routisan@next # or yarn add vue-routisan@next
29 | ```
30 |
31 | ---
32 |
33 | Routisan provides you with a friendlier way to declare route definitions for Vue Router. Inspired by Laravel, it uses chained calls to build up your routes, allowing you to group and nest as deeply as you like.
34 |
35 | ```js
36 | Route.view('blog', 'Blog').name('blog').children(() => {
37 | // All Posts
38 | Route.view('/', 'Blog/Posts').name('posts')
39 |
40 | // Single Post
41 | Route.view('{post}', 'Blog/Post').name('single-post').children(() => {
42 | Route.view('edit', 'Blog/Post/Edit').name('edit')
43 | Route.view('stats', 'Blog/Post/Stats').name('stats')
44 | })
45 | })
46 | ```
47 |
48 | This produces an array of routes in the format Vue Router expects to see, and follows a behaviour somewhat similar to Laravel’s router, such as:
49 |
50 | - Using callbacks to iteratively collect routes
51 | - Correctly joining nested routes together, regardles of prefixed slashes
52 | - Correctly joining the names of nested routes, using a separator of your choice
53 |
54 | ## Documentation
55 |
56 | You can read the docs on the [Vue Routisan 3 site](https://vue-routisan.rockett.pw/).
57 |
58 | ## Upgrading from v2.x
59 |
60 | If you are upgrading a project to Routisan 3, please consult the [upgrade guide](upgrading.md).
61 |
62 | Keep in mind that Routisan 3 is currently in beta. It is suitable for production use, but it would be wise to wait for the stable release before using it in large projects where potential breaking changes might make the upgrade path unnecessarily complex.
63 |
64 | ## Changelog
65 |
66 | Routisan’s changelog is maintained [here](changelog.md).
67 |
68 | ## License
69 |
70 | Vue Routisan is licensed under the ISC license, which is more permissive variant of the MIT license. You can read the license [here](license.md).
71 |
72 | ## Contributing
73 |
74 | If you would like to contribute code to Vue Routisan, simply open a Pull Request containing the changes you would like to see. Please provide a proper description of the changes, whether they fix a bug, enhance an existing feature, or add a new feature.
75 |
76 | If you spot a bug and don’t know how to fix it (or just don’t have the time), simply [open an issue](https://github.com/mikerockett/vue-routisan/issues/new). Please ensure the issue is descriptive, and contains a link to a reproduction of the issue. Additionally, please prefix the title with the applicable version of Routisan, such as `[3.0]`.
77 |
78 | Feature requests may also be submitted by opening an issue – please prefix the title with "Feature Request"
79 |
--------------------------------------------------------------------------------
/src/route/factory.js:
--------------------------------------------------------------------------------
1 | import { assertGuard } from '../assertions/guard'
2 | import { RouteBag } from './bag'
3 | import { stringPopulated, cleanRouteParam } from '../utilities'
4 |
5 | export class Factory {
6 |
7 | static compiled = []
8 | static nameSeparator = '.'
9 | static guards = new Map()
10 | static childContexts = []
11 | static groupContexts = []
12 | static previousGroupContexts = []
13 | static resolver = (component) => component
14 |
15 | static usingResolver(resolver) {
16 | this.resolver = resolver
17 | return this
18 | }
19 |
20 | static withNameSeparator(separator) {
21 | this.nameSeparator = separator
22 | return this
23 | }
24 |
25 | static withGuards(guards) {
26 | for (const [name, guardClass] of Object.entries(guards)) {
27 | const guard = new guardClass(name)
28 | assertGuard(guard, 'guard[]')
29 | this.guards.set(name, guard)
30 | }
31 |
32 | return this
33 | }
34 |
35 | static guard(name) {
36 | return this.guards.get(name)
37 | }
38 |
39 | static resolveComponents(view, additionalViews, root = true) {
40 | const defaultComponent = this.resolver(view)
41 | const additionalComponents = {}
42 |
43 | if (additionalViews) for (const viewName in additionalViews) {
44 | components[viewName] = this.resolveComponents(additionalViews[viewName], null, false)
45 | }
46 |
47 | return root
48 | ? Object.assign({ default: defaultComponent }, additionalComponents)
49 | : defaultComponent
50 | }
51 |
52 | static cleanPath(path) {
53 | const separator = '/'
54 |
55 | const routePath = path
56 | .split(separator)
57 | .filter(stringPopulated)
58 | .map(cleanRouteParam)
59 | .join(separator)
60 |
61 | return this.childContexts.length || this.groupContexts.length
62 | ? routePath
63 | : separator + routePath
64 | }
65 |
66 | static linkRoute(route) {
67 | if (!this.routeBag) {
68 | this.routeBag = new RouteBag()
69 | }
70 |
71 | if (this.groupContexts.length) {
72 | const clampableName = this.groupContexts
73 | .map((options) => options.name)
74 | .join(this.nameSeparator)
75 | .trim()
76 |
77 | const clampablePrefix = this.groupContexts
78 | .map((options) => options.prefix)
79 | .join('/')
80 | .trim()
81 |
82 | const clampableGuards = this.groupContexts
83 | .map((options) => {
84 | const guards = Array.isArray(options.guard)
85 | ? options.guard
86 | : Array.of(options.guard)
87 |
88 | return guards.filter(guard => guard !== undefined)
89 | })
90 |
91 | if (clampableName) route.clampName(clampableName)
92 | if (clampablePrefix) route.clampPrefix(clampablePrefix)
93 | if (clampableGuards.length) route.guard(
94 | ...clampableGuards.reduce((flat, next) => flat.concat(next), [])
95 | )
96 | }
97 |
98 | if (this.childContexts.length) {
99 | route.clampName(this.childContexts.map((route) => route._name).join(this.nameSeparator))
100 | this.childContexts[this.childContexts.length - 1]._children.push(route)
101 | } else this.routeBag.pushRoute(route)
102 | }
103 |
104 | static withChildren(route, callable) {
105 | this.childContexts.push(route)
106 |
107 | if (this.groupContexts.length) {
108 | this.previousGroupContexts = Array.from(this.groupContexts)
109 | this.groupContexts = this.groupContexts.map(({ name, guard }) => ({ name, guard }))
110 | }
111 |
112 | callable()
113 |
114 | if (this.previousGroupContexts.length) {
115 | this.groupContexts = Array.from(this.previousGroupContexts)
116 | }
117 |
118 | this.childContexts.pop()
119 |
120 | return route
121 | }
122 |
123 | static withinGroup(options, callable) {
124 | this.groupContexts.push(options)
125 | callable()
126 | this.groupContexts.pop()
127 | }
128 |
129 | static compile() {
130 | this.compiled = this.routeBag
131 | ? this.routeBag.compiled()
132 | : []
133 |
134 | return this
135 | }
136 |
137 | static flush() {
138 | if (!this.compiled.length) this.compile()
139 |
140 | const compiled = this.compiled
141 | this.routeBag = new RouteBag()
142 | this.compiled = []
143 |
144 | return compiled
145 | }
146 |
147 | static routes() {
148 | return this.flush()
149 | }
150 |
151 | static dump() {
152 | console.log('Route Bag:')
153 | console.dir(this.routeBag ? this.routeBag.routes : [])
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/upgrading.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Upgrade Guide
4 |
5 | ## `v3.0.0-alpha.*` or `v3.0.0-beta.1`
6 |
7 | In the first v3 alpha, a `fallback` option was introduced to provide a clean syntax for defining a catch-all route. However, Vue Router 4 removed catch-alls and replaced them with named regex-based paramaters.
8 |
9 | Given that Routisan does not force a specific version of Vue Router (it does not depend on it, nor does it make it a peer dependency), it is unable to determine the correct course of action for fallback routes.
10 |
11 | ### Migration Path
12 |
13 | In the unlikely event that you are using Routisan 3 alpha or beta in a production project and making use of fallbacks, you will need to revert to a normal route that handles this:
14 |
15 | ```diff
16 | -- Route.fallback('PageNotFound')
17 | ++ Route.view('/:fallback(.*)*', 'PageNotFound')
18 | ```
19 |
20 | If you prefer the [curly syntax](https://vue-routisan.rockett.pw/guide/parameter-matching.html#alternative-curly-syntax):
21 |
22 | ```js
23 | Route.view('/{fallback}(.*)*', 'PageNotFound')
24 | ```
25 |
26 | At a later stage, Routisan may introduce Vue Router 4 as a peer dependency, at which time `fallback` may become available again.
27 |
28 | ## `v2.x` → `v3.0.0-alpha.1`
29 |
30 | ### Importing `Route`
31 |
32 | `Route` is now a named export:
33 |
34 | ```diff
35 | -- import Route from 'vue-routisan'
36 | ++ import { Route } from 'vue-routisan'
37 | ```
38 |
39 | ### View Resolvers
40 |
41 | Calls to `Route.setViewResolver` must be changed to `Factory.usingResolver`:
42 |
43 | ```diff
44 | -- Route.setViewResolver(view => () => import(`@/views/${view}`))
45 | ++ Factory.usingResolver(view => () => import(`@/views/${view}`))
46 | ```
47 |
48 | ### Compiled Routes
49 |
50 | `Route.all()` has been replaced with `Factory.routes()`:
51 |
52 | ```diff
53 | -- new Router({
54 | -- routes: Route.all()
55 | ++ routes: Factory.routes()
56 | -- })
57 | ```
58 |
59 | If you are using Vue Router 4, the syntax for creating routers has changed:
60 |
61 | ```js
62 | import { createRouter, createWebHistory } from 'vue-router'
63 |
64 | createRouter({
65 | history: createWebHistory(),
66 | routes: Factory.routes(),
67 | })
68 | ```
69 |
70 | ### Named Views
71 |
72 | These are now declared as an optional third argument to `view`:
73 |
74 | ```diff
75 | -- Route.view('path', {
76 | -- default: 'DefaultComponent',
77 | -- other: 'OtherComponent',
78 | -- })
79 | ++ Route.view('path', 'DefaultComponent', {
80 | ++ other: 'OtherComponent',
81 | ++ })
82 | ```
83 |
84 | ### Named Routes
85 |
86 | In v2, named routes did not cascade to their child routes, which meant redeclaring the parent name in each child, where necessary.
87 |
88 | ```diff
89 | Route.view('parent', 'Parent').name('parent').children(() => {
90 | -- Route.view('child', 'Child').name('parent.child')
91 | ++ Route.view('child', 'Child').name('child')
92 | })
93 | ```
94 |
95 | ### Guards
96 |
97 | Navigation-guarding is now done through a Promise-chain, which means an underlying Promise-based class must be used to build guards. These classes must extend Routisan's `Guard` and implement the `handle` method, used to resolve or reject the underlying Promise. This method also accepts the current route context as its third argument, in case your guard needs to reference those.
98 |
99 | Here's a simple example of the transition:
100 |
101 | > Whilst it's recommended to declare your guards in dedicated files, they are shown inline below for brevity.
102 |
103 | **Before:**
104 |
105 | ```js
106 | // Declaration:
107 | const authenticated = (to, from, next) => {
108 | return isAuthenticated()
109 | ? next()
110 | : next({ name: 'login' })
111 | }
112 |
113 | // Usage:
114 | Route.view('/account/settings', 'Settings').guard(auth);
115 | ```
116 |
117 | **After:**
118 |
119 | ```js
120 | // Declaration:
121 | import { Guard } from 'vue-routisan'
122 |
123 | class AuthenticationGuard extends Guard {
124 | handle(resolve, reject, { from, to }) {
125 | isAuthenticated()
126 | ? resolve()
127 | : reject({ name: 'login' }) // refer to the readme for more info.
128 | }
129 | }
130 |
131 | // Usage:
132 | Factory.withGuards({ 'auth': AuthenticationGuard })
133 | Route.view('/account/settings', 'Settings').guard('auth');
134 | ```
135 |
136 | The method used to set more than one guard on a route has also changed. Previously, you would pass in an array to the `guard` method. Now, you may pass in an arbitrary number of arguments.
137 |
138 | > Aliasing guards is also optional. If you pass in a guard without a name (as shown with the `AdminGuard` below), the class-name of the guard will be used instead.
139 |
140 | ```js
141 | Factory.withGuards({
142 | 'auth': AuthenticationGuard,
143 | AdminGuard,
144 | })
145 |
146 | Route.view('/account/settings', 'Settings').guard('auth', 'AdminGuard');
147 | ```
148 |
149 | ### Fallback Routes
150 |
151 | > ⚠️ This has been removed in Beta 2
152 |
153 | If you were declaring a catch-all/fallback route, you may now take advantage of the `fallback` method:
154 |
155 | ```js
156 | Route.fallback('PageNotFound')
157 | ```
158 |
159 | If you need dedicated fallbacks for different type of routes, you'll need to stick to the `view` method:
160 |
161 | ```js
162 | Route.view('users/*', 'UserRouteNotFound')
163 | ```
164 |
165 | **Vue Router Reference:** [router.vuejs.org/guide/essentials/dynamic-matching.html](https://router.vuejs.org/guide/essentials/dynamic-matching.html#catch-all-404-not-found-route)
166 |
167 | ### Route Options
168 |
169 | Per the [changelog](changelog.md), route options are no longer supported. Instead, use the applicable methods to set up your routes. These include:
170 |
171 | - `meta(key, value)`
172 | - `props(object)`
173 | - `prop(key, value)` (defers to `props`)
174 | - `guard` (now fully replaces the old `beforeEnter` option)
175 |
--------------------------------------------------------------------------------
/changelog.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Changelog
4 |
5 | ## `v3.0.0-beta-5`
6 |
7 | This is a reversal of the previous beta, however it now uses esbuild, and targets ESM as browsers are capable of running modules. Certain bundlers may do a pass over dependencies, converting them where required.
8 |
9 | This release also fixes an issue where using `'/'` in a nested route would result in pointing to the root. This is a new feature in Vue Router 4. Now, when using `''` or `'/'` in a nested route, Routisan will compile an empty string. Whilst this may counter the feature in Vue Router 4, it does stray away from the purpose of this package, which is to produce routes using a Laravel-like approach, which does not support the feature (which itself is counter-intuitive, in my opionion).
10 |
11 | Lastly, this release includes a Vite-powered demo in the `demo` directory. To play with it, simply clone the repo and spin up the demo app using `npm run serve` or `yarn serve`.
12 |
13 | ## `v3.0.0-beta.4`
14 |
15 | The internal build of Vue Routisan has been removed. This has been done for a few reasons:
16 |
17 | 1. Most bundlers can compile the code, and would do so with the dist bundle anyway.
18 | 2. Several dependency security issues, and quite a lot of open issues in microbundle. Whilst development is active on that project, it's actually not required.
19 |
20 | At a later stage, alternative options will be explored to make sure that Vue Routisan can be used in all environments (Node, Deno, and the browser).
21 |
22 | ## `v3.0.0-beta.3`
23 |
24 | Fixing the missing meta and props fields (#55)
25 |
26 | ## `v3.0.0-beta.2`
27 |
28 | The `fallback` helper has been removed. See the [upgrade guide](upgrading.md) for more information.
29 |
30 | ## `v3.0.0-alpha.1`
31 |
32 | This is a complete rewrite of Routisan, from the ground up. The aim was to provide a more context-aware and efficient way of building up a route-tree without changing the vast majority of the API, which makes the upgrade process relatively straight-forward.
33 |
34 | Note, however, that this rebuild brings Vue routing a little closer to Laravel’s routing syntax and, as such, has a few **breaking changes**.
35 |
36 | Your setup will determine upgrade-complexity, however the [upgrade guide](upgrading.md) will help you out.
37 |
38 | **So, what's new under the hood?**
39 |
40 | In v2, Routes were built up using a `Route` instance-helper and a shared container through an iterative, recursive merging process, building route config on the fly. The v3 approach is quite different, where a `Factory` delegate is used to build up a tree of routes, which is then recursively compiled to a Vue Router-compatible config and flushed from memory.
41 |
42 | Apart from this, here's a list that details everything that's been added, changed, and fixed. Again, there are breaking changes here, so please review the below and the upgrade guide.
43 |
44 | ### Added
45 |
46 | - **Routing:**
47 | - Name-cascading, which allows names to cascade or funnel down from route to route.
48 | - Names are joined together using the separator specified in `Factory.withNameSeparator(string)`, which defaults to a period (`.`).
49 |
50 | - **Semantics and Utilities:**
51 | - Helper functions for `view`, `redirect`, `group` were added, in the case you don't want to use `Route` everywhere.
52 | - A `fallback` option was added to `Route`, along with the corresponding helper function.
53 | - Route parameters may now be declared using `{curly braces}` in addition to `:colons`.
54 | - Additionally, if two parameters are `{joined}{together}`, they will be separated by a slash (`/`).
55 | - The `{all}` paramater, which translates to `(.*)`, was added.
56 | - The `(number)` and `(string)` constraint-aliases, which translate to `(\\d+)` and `(\\w+)` respectively, were added.
57 | - For debugging, you can call `Route.dump()` to see all compiled routes in the console. `Factory.dump()` is also available and shows what the routes look like before compilation. Note, though, that internal variable names are mangled by Terser, so you'll need to figure out your way around the tree structure.
58 |
59 | ### Changed
60 |
61 | - **Views and Resolvers:**
62 | - View resolvers are now set on the `Factory` with the `usingResolver` method.
63 | - Named views are now passed in as additional views, the third argument to `view`.
64 | - Likewise, the second argument to `view` no longer compiles down to `component`, but rather `components.default`, which is what [Vue Router does](https://github.com/vuejs/vue-router/blob/7d7e048490e46f4d433ec6cf897468409d158c9b/src/create-route-map.js#L86) with `RouteRecord.component`.
65 |
66 | - **Guards:**
67 | - The v2 `vue-router-multiguard` setup has been replaced with a Promise-based class system. Amoung other benefits, this change affords guards the ability to run an optional callback on navigation-rejection, the return value of which will be passed to `next`. All guards must extend the `Guard` class and implement the `handle(resolve, reject, [context])` method.
68 | - Guards must registered through the `Factory`, using the `withGuards` method. They are then used in route definitions by name, which may or may not be aliased when registered.
69 | - When rejecting navigation, Routisan will detect rejection loops and warn you when it finds them.
70 | - When applying multiple guards to a route, an array is no longer required – simply add each guard as an argument to the `guard` method.
71 | - Useful for debugging, guards may log the outcome of their inner promises by declaring a method that returns a boolean, called `logPromiseOutcomes`.
72 |
73 | - **Internal:**
74 | - Builds are now performed by [microbundle](https://github.com/developit/microbundle) using the CJS, UMD, ESM and modern output formats, compressed with Terser.
75 |
76 | ### Fixed
77 |
78 | These are not fixes, technically. Rather, they are existing issues that the rewrite solves.
79 |
80 | - Any issues pertaining to route concatenation are gone.
81 | - Groups and children now play nicely together.
82 |
83 | ### Removed
84 |
85 | - To reduce duplication issues, the `Route.options` method is no longer required and has been removed. You should use the corresponding route-builder methods instead.
86 |
--------------------------------------------------------------------------------