Skip to content

feat(extendRoutes): allow relative path overrides #519

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
Apr 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
7e16960
fix(extendRoutes): override child with relative paths
robertmoura Jun 25, 2024
87f4855
fix(extendRoutes): flip parent check
robertmoura Jun 25, 2024
1c77a15
test(extendRoutes): add tests and fix set path
robertmoura Jun 25, 2024
b940c15
test(extendRoutes): improve set path tests
robertmoura Jun 25, 2024
4a82dc4
chore: lint extend routes
robertmoura Jun 25, 2024
b811c72
refactor(tree): update full path override
robertmoura Jun 28, 2024
98bfe76
chore(extendRoutes): update warning message
robertmoura Jun 28, 2024
a8c529a
test(extendRoutes): remove set root node path
robertmoura Jun 28, 2024
964e9cd
refactor(tree): remove fullPath
robertmoura Jun 28, 2024
52147cf
Merge branch 'posva:main' into main
robertmoura Jun 28, 2024
3135bb5
fix: reintroduce absolute paths
robertmoura Jun 28, 2024
ea2c323
Merge branch 'main' of https://github.com/robertmoura/unplugin-vue-ro…
robertmoura Jun 28, 2024
b260d9b
chore: lint code
robertmoura Jun 28, 2024
43ed75f
test(extendRoutes): improve tests
robertmoura Jun 28, 2024
652507a
fix(tree): relative child of absolute route
robertmoura Jun 28, 2024
16807d7
Merge branch 'posva:main' into main
robertmoura Jun 28, 2024
3ec0b22
feat(routeBlock): improve relative path overrides
robertmoura Jul 1, 2024
74a18ec
Merge branch 'posva:main' into main
robertmoura Oct 4, 2024
5930b91
Merge branch 'main' of https://github.com/robertmoura/unplugin-vue-ro…
robertmoura Oct 4, 2024
a930a2d
fix: handle empty string
robertmoura Oct 4, 2024
53b21b3
Merge branch 'main' into feat/relative-route-block-paths
posva Apr 22, 2025
ee95ab5
refactor: replace `path` with `fullPath` in editable tree node
posva Apr 23, 2025
8ef4d18
chore: inline variable
posva Apr 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/core/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export function createRoutesContext(options: ResolvedOptions) {
const routeBlock = getRouteBlock(filePath, content, options)
// TODO: should warn if hasDefinePage and customRouteBlock
// if (routeBlock) logger.log(routeBlock)

node.setCustomRouteBlock(filePath, {
...routeBlock,
...definedPageNameAndPath,
Expand Down
13 changes: 1 addition & 12 deletions src/core/customBlock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,7 @@ export function getRouteBlock(
const parsedSFC = parse(content, { pad: 'space' }).descriptor
const blockStr = parsedSFC?.customBlocks.find((b) => b.type === 'route')

if (!blockStr) return

let result = parseCustomBlock(blockStr, path, options)

// validation
if (result) {
if (result.path != null && !result.path.startsWith('/')) {
warn(`Overridden path must start with "/". Found in "${path}".`)
}
}

return result
if (blockStr) return parseCustomBlock(blockStr, path, options)
}

export interface CustomRouteBlock
Expand Down
49 changes: 48 additions & 1 deletion src/core/extendRoutes.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { expect, describe, it } from 'vitest'
import { expect, describe, it, beforeAll } from 'vitest'
import { PrefixTree } from './tree'
import { DEFAULT_OPTIONS, resolveOptions } from '../options'
import { EditableTreeNode } from './extendRoutes'
import { mockWarn } from '../../tests/vitest-mock-warn'

describe('EditableTreeNode', () => {
beforeAll(() => {
mockWarn()
})

const RESOLVED_OPTIONS = resolveOptions(DEFAULT_OPTIONS)
it('creates an editable tree node', () => {
const tree = new PrefixTree(RESOLVED_OPTIONS)
Expand Down Expand Up @@ -251,4 +256,46 @@ describe('EditableTreeNode', () => {
},
])
})

it('can override children path with relative ones', () => {
const tree = new PrefixTree(RESOLVED_OPTIONS)
const editable = new EditableTreeNode(tree)
const parent = editable.insert('parent', 'file.vue')
const child = parent.insert('child', 'file.vue')
const grandChild = child.insert('grandchild', 'file.vue')

child.path = 'relative'
expect(child.path).toBe('relative')
expect(child.fullPath).toBe('/parent/relative')
expect(grandChild.fullPath).toBe('/parent/relative/grandchild')

child.path = '/absolute'
expect(child.path).toBe('/absolute')
expect(child.fullPath).toBe('/absolute')
expect(grandChild.fullPath).toBe('/absolute/grandchild')
})

it('can override paths at tho root', () => {
const tree = new PrefixTree(RESOLVED_OPTIONS)
const editable = new EditableTreeNode(tree)
const parent = editable.insert('parent', 'file.vue')
const child = parent.insert('child', 'child.vue')

parent.path = '/p'
expect(parent.path).toBe('/p')
expect(parent.fullPath).toBe('/p')
expect(child.fullPath).toBe('/p/child')
})

it('still creates valid paths if the path misses a leading slash', () => {
const tree = new PrefixTree(RESOLVED_OPTIONS)
const editable = new EditableTreeNode(tree)
const parent = editable.insert('parent', 'file.vue')
const child = parent.insert('child', 'file.vue')

parent.path = 'bar'
expect(parent.path).toBe('/bar')
expect(parent.fullPath).toBe('/bar')
expect(child.fullPath).toBe('/bar/child')
})
})
13 changes: 7 additions & 6 deletions src/core/extendRoutes.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { RouteMeta } from 'vue-router'
import { CustomRouteBlock } from './customBlock'
import { type TreeNode } from './tree'
import { warn } from './utils'

/**
* A route node that can be modified by the user. The tree can be iterated to be traversed.
Expand Down Expand Up @@ -142,11 +141,13 @@ export class EditableTreeNode {
* Override the path of the route. You must ensure `params` match with the existing path.
*/
set path(path: string) {
if (!path.startsWith('/')) {
warn(
`Only absolute paths are supported. Make sure that "${path}" starts with a slash "/".`
)
return
// automatically prefix the path with `/` if the route is at the root of the tree
// that matches the behavior of node.insert('path', 'file.vue') that also adds it
if (
(!this.node.parent || this.node.parent.isRoot()) &&
!path.startsWith('/')
) {
path = '/' + path
}
this.node.value.addEditOverride({ path })
}
Expand Down
30 changes: 15 additions & 15 deletions src/core/tree.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ describe('Tree', () => {
expect(child).toBeDefined()
expect(child.value).toMatchObject({
rawSegment: 'foo',
path: '/foo',
fullPath: '/foo',
_type: TreeNodeType.static,
})
expect(child.children.size).toBe(0)
Expand All @@ -37,7 +37,7 @@ describe('Tree', () => {
expect(child.value).toMatchObject({
rawSegment: '[id]',
params: [{ paramName: 'id' }],
path: '/:id',
fullPath: '/:id',
_type: TreeNodeType.param,
})
expect(child.children.size).toBe(0)
Expand All @@ -50,14 +50,14 @@ describe('Tree', () => {
expect(tree.children.get('[id]_a')!.value).toMatchObject({
rawSegment: '[id]_a',
params: [{ paramName: 'id' }],
path: '/:id()_a',
fullPath: '/:id()_a',
_type: TreeNodeType.param,
})

expect(tree.children.get('[a]e[b]f')!.value).toMatchObject({
rawSegment: '[a]e[b]f',
params: [{ paramName: 'a' }, { paramName: 'b' }],
path: '/:a()e:b()f',
fullPath: '/:a()e:b()f',
_type: TreeNodeType.param,
})
})
Expand Down Expand Up @@ -155,7 +155,7 @@ describe('Tree', () => {
modifier: '+',
},
],
path: '/:id+',
fullPath: '/:id+',
_type: TreeNodeType.param,
})
})
Expand All @@ -173,7 +173,7 @@ describe('Tree', () => {
modifier: '*',
},
],
path: '/:id*',
fullPath: '/:id*',
_type: TreeNodeType.param,
})
})
Expand All @@ -191,7 +191,7 @@ describe('Tree', () => {
modifier: '?',
},
],
path: '/:id?',
fullPath: '/:id?',
_type: TreeNodeType.param,
})
})
Expand Down Expand Up @@ -292,30 +292,30 @@ describe('Tree', () => {
expect(index.value).toMatchObject({
rawSegment: 'index',
// the root should have a '/' instead of '' for the autocompletion
path: '/',
fullPath: '/',
})
expect(index).toBeDefined()
const a = tree.children.get('a')!
expect(a).toBeDefined()
expect(a.value.components.get('default')).toBeUndefined()
expect(a.value).toMatchObject({
rawSegment: 'a',
path: '/a',
fullPath: '/a',
})
expect(Array.from(a.children.keys())).toEqual(['index', 'b'])
const aIndex = a.children.get('index')!
expect(aIndex).toBeDefined()
expect(Array.from(aIndex.children.keys())).toEqual([])
expect(aIndex.value).toMatchObject({
rawSegment: 'index',
path: '/a',
fullPath: '/a',
})

tree.insert('a', 'a.vue')
expect(a.value.components.get('default')).toBe('a.vue')
expect(a.value).toMatchObject({
rawSegment: 'a',
path: '/a',
fullPath: '/a',
})
})

Expand All @@ -328,7 +328,7 @@ describe('Tree', () => {
expect(child.value).toMatchObject({
rawSegment: '[id]+',
params: [{ paramName: 'id', modifier: '+' }],
path: '/:id+',
fullPath: '/:id+',
pathSegment: ':id+',
_type: TreeNodeType.param,
})
Expand All @@ -346,7 +346,7 @@ describe('Tree', () => {
expect(child.value).toMatchObject({
rawSegment: '[id]',
params: [{ paramName: 'id' }],
path: '/:id',
fullPath: '/:id',
pathSegment: ':id',
})
expect(child.children.size).toBe(0)
Expand Down Expand Up @@ -543,7 +543,7 @@ describe('Tree', () => {
expect(users.value).toMatchObject({
rawSegment: 'users.new',
pathSegment: 'users/new',
path: '/users/new',
fullPath: '/users/new',
_type: TreeNodeType.static,
})
})
Expand All @@ -562,7 +562,7 @@ describe('Tree', () => {
expect(lesson.value).toMatchObject({
rawSegment: '1.2.3-lesson',
pathSegment: '1.2.3-lesson',
path: '/1.2.3-lesson',
fullPath: '/1.2.3-lesson',
_type: TreeNodeType.static,
})
})
Expand Down
4 changes: 2 additions & 2 deletions src/core/tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ export class TreeNode {
* Returns the route path of the node including parent paths.
*/
get fullPath() {
return this.value.overrides.path ?? this.value.path
return this.value.fullPath
}

/**
Expand Down Expand Up @@ -247,7 +247,7 @@ export class TreeNode {
*/
isRoot() {
return (
!this.parent && this.value.path === '/' && !this.value.components.size
!this.parent && this.value.fullPath === '/' && !this.value.components.size
)
}

Expand Down
23 changes: 16 additions & 7 deletions src/core/treeNodeValue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,15 +68,24 @@ class _TreeNodeValueBase {
}

/**
* fullPath of the node based on parent nodes
* Path of the node. Can be absolute or not. If it has been overridden, it
* will return the overridden path.
*/
get path(): string {
const parentPath = this.parent?.path
// both the root record and the index record have a path of /
const pathSegment = this.overrides.path ?? this.pathSegment
return (!parentPath || parentPath === '/') && pathSegment === ''
? '/'
: joinPath(parentPath || '', pathSegment)
return this.overrides.path ?? this.pathSegment
}

/**
* Full path of the node including parent nodes.
*/
get fullPath(): string {
const pathSegment = this.path
// if the path is absolute, we don't need to join it with the parent
if (pathSegment.startsWith('/')) {
return pathSegment
}

return joinPath(this.parent?.fullPath ?? '', pathSegment)
}

toString(): string {
Expand Down
42 changes: 41 additions & 1 deletion src/core/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { trimExtension } from './utils'
import { joinPath, trimExtension } from './utils'

describe('utils', () => {
describe('trimExtension', () => {
Expand All @@ -15,4 +15,44 @@ describe('utils', () => {
expect(trimExtension('foo.page.vue', ['.vue'])).toBe('foo.page')
})
})

describe('joinPath', () => {
it('joins paths', () => {
expect(joinPath('/foo', 'bar')).toBe('/foo/bar')
expect(joinPath('/foo', 'bar', 'baz')).toBe('/foo/bar/baz')
expect(joinPath('/foo', 'bar', 'baz', 'qux')).toBe('/foo/bar/baz/qux')
expect(joinPath('/foo', 'bar', 'baz', 'qux', 'quux')).toBe(
'/foo/bar/baz/qux/quux'
)
})

it('adds a leading slash if missing', () => {
expect(joinPath('foo')).toBe('/foo')
expect(joinPath('foo', '')).toBe('/foo')
expect(joinPath('foo', 'bar')).toBe('/foo/bar')
expect(joinPath('foo', 'bar', 'baz')).toBe('/foo/bar/baz')
})

it('works with empty paths', () => {
expect(joinPath('', '', '', '')).toBe('/')
expect(joinPath('', '/', '', '')).toBe('/')
expect(joinPath('', '/', '', '/')).toBe('/')
expect(joinPath('', '/', '/', '/')).toBe('/')
expect(joinPath('/', '', '', '')).toBe('/')
})

it('collapses slashes', () => {
expect(joinPath('/foo/', 'bar')).toBe('/foo/bar')
expect(joinPath('/foo', 'bar')).toBe('/foo/bar')
expect(joinPath('/foo', 'bar/', 'foo')).toBe('/foo/bar/foo')
expect(joinPath('/foo', 'bar', 'foo')).toBe('/foo/bar/foo')
})

it('keeps trailing slashes', () => {
expect(joinPath('/foo', 'bar/')).toBe('/foo/bar/')
expect(joinPath('/foo/', 'bar/')).toBe('/foo/bar/')
expect(joinPath('/foo/', 'bar', 'baz/')).toBe('/foo/bar/baz/')
expect(joinPath('/foo/', 'bar/', 'baz/')).toBe('/foo/bar/baz/')
})
})
})
2 changes: 1 addition & 1 deletion src/core/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export function joinPath(...paths: string[]): string {
// check path to avoid adding a trailing slash when joining an empty string
(path && '/' + path.replace(LEADING_SLASH_RE, ''))
}
return result
return result || '/'
}

function paramToName({ paramName, modifier, isSplat }: TreeRouteParam) {
Expand Down