Skip to content

Commit

Permalink
feat: wildcard, unicode, and modifier changes
Browse files Browse the repository at this point in the history
  • Loading branch information
bent10 committed Aug 28, 2024
1 parent 8dc94fb commit 17834ef
Show file tree
Hide file tree
Showing 12 changed files with 116 additions and 83 deletions.
1 change: 1 addition & 0 deletions packages/core/example/foo/bar/bar.data.cjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// eslint-disable-next-line no-undef
exports.default = {
dataBar: 'data'
}
1 change: 1 addition & 0 deletions packages/core/example/foo/bar/baz/baz.data.cjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// eslint-disable-next-line no-undef
exports.default = {
dataBaz: 'data'
}
1 change: 1 addition & 0 deletions packages/core/example/foo/foo.data.cjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// eslint-disable-next-line no-undef
exports.default = {
dataFoo: 'data'
}
2 changes: 1 addition & 1 deletion packages/core/src/async/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export async function createRoutes(options: Options = {}): Promise<Route[]> {
const { dir = '.', cache } = options
const cwd = process.cwd()
const resolvedDir = dir.startsWith(cwd)
? dir.replace(cwd, '').replace(/^[\\\/]+/, '') || '.'
? dir.replace(cwd, '').replace(/^[\\/]+/, '') || '.'
: dir

if (cache && cacheRoute[dir]) {
Expand Down
40 changes: 18 additions & 22 deletions packages/core/src/async/visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,33 +25,29 @@ export async function visit(
handler
} = options

try {
const files: Dirent[] = await readdir(dir, { withFileTypes: true })
const files: Dirent[] = await readdir(dir, { withFileTypes: true })

await Promise.all(
files.map(async file => {
if (!(await filter(file))) return
await Promise.all(
files.map(async file => {
if (!(await filter(file))) return

const id = normalizePath(join(dir, file.name))
const id = normalizePath(join(dir, file.name))

if (file.isDirectory()) {
await visit({ ...options, dir: id }, routes)
} else {
const fileExtension = extname(id)
if (file.isDirectory()) {
await visit({ ...options, dir: id }, routes)
} else {
const fileExtension = extname(id)

if (!isValidExtension(extensions, fileExtension) || isIgnored(id))
return
if (!isValidExtension(extensions, fileExtension) || isIgnored(id))
return

const route = createRoute(id, { root, urlSuffix, urlPrefix })
const route = createRoute(id, { root, urlSuffix, urlPrefix })

// call handler fn, useful to expand each route
await handler?.(route, root)
// call handler fn, useful to expand each route
await handler?.(route, root)

routes.push(route)
}
})
)
} catch (error) {
throw error
}
routes.push(route)
}
})
)
}
53 changes: 33 additions & 20 deletions packages/core/src/segments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,9 @@ import { extname } from 'node:path'
* @returns An array of segments representing the path of the route relative to the root directory.
*/
export function routeSegments(id: string, root = '') {
const fileExtension = extname(id)
const routePath = id
.replace(new RegExp(`^${escapeRegExp(root)}`), '')
.replace(new RegExp(`${escapeRegExp(fileExtension)}$`), '')
.replace(new RegExp(`${escapeRegExp(extname(id))}$`), '')

return parseRoutePath(routePath)
}
Expand All @@ -38,40 +37,53 @@ export function escapeRegExp(str: string) {
*/
export function isDynamicRouteSegment(segment: string) {
return (
isWildcard(segment) ||
isNonPartialDynamicRouteSegment(segment) ||
isPartialDynamicRouteSegment(segment)
)
}

/**
* Checks if a segment of a route path is a non-partial dynamic segment.
* Non-partial dynamic segments are indicated by a leading colon (`:`) or dollar
* sign (`$`) and not ending with a question mark (`?`).
* Determines if a route path segment is a wildcard segment. Wildcard
* segments are represented by `*` or end with `.*`.
*
* @param segment The segment of the route path to check.
* @returns `true` if the segment is a non-partial dynamic segment, `false` otherwise.
* @param segment - The route path segment to check.
* @returns `true` if the segment is a wildcard segment,
* `false` otherwise.
*/
function isWildcard(segment: string): boolean {
return segment === '*' || segment.endsWith('.*')
}

/**
* Determines if a route path segment is a non-partial dynamic segment.
* Non-partial dynamic segments start with `:` or `$` and do not end with
* `?`, `*`, or `+`.
*
* @param segment The route path segment to check.
* @returns `true` if it's a non-partial dynamic segment,
* otherwise `false`.
*/
function isNonPartialDynamicRouteSegment(segment: string) {
return (
(segment.startsWith(':') || segment.startsWith('$')) &&
!segment.endsWith('?')
!/[?*+]$/.test(segment)
)
}

/**
* Checks if a segment of a route path is a partial dynamic segment. Partial
* dynamic segments are indicated by a leading colon (`:`) or dollar sign (`$`)
* and ending with a question mark (`?`), or enclosed in square brackets
* (`[]`).
* Determines if a route path segment is a partial dynamic segment. Partial
* dynamic segments are enclosed in `[]`, or start with `:`, `$` and end
* with `?`, `*`, or `+`.
*
* @param segment The segment of the route path to check.
* @returns `true` if the segment is a partial dynamic segment, `false` otherwise.
* @param segment The route path segment to examine.
* @returns `true` if it's a partial dynamic segment, otherwise `false`.
*/
function isPartialDynamicRouteSegment(segment: string) {
return (
(segment.startsWith('[') && segment.endsWith(']')) ||
((segment.startsWith(':') || segment.startsWith('$')) &&
segment.endsWith('?'))
/[?*+]$/.test(segment))
)
}

Expand Down Expand Up @@ -124,20 +136,21 @@ export function normalizeSegment(segment: string) {
function getValidSegment(segment: string) {
// segment may include file names ordered numerically (e.g., 01-foo, 02-bar, etc.),
// used for organizing routes in a specific order.
const validSegment = segment.replace(/^\d+[\-\_]/, '')
const validSegment = segment.replace(/^\d+[-_]/, '')

if (isNonPartialDynamicRouteSegment(validSegment)) {
return `:${validSegment.slice(1)}`
}

if (isPartialDynamicRouteSegment(validSegment)) {
return `:${validSegment.slice(1, -1)}?`
const mod = /[*+]$/.test(validSegment) ? validSegment.slice(-1) : '?'
return `:${validSegment.slice(1, -1)}${mod}`
}

// handle unknown splats segment
if (validSegment === '*') {
return ':splats*'
}
// if (validSegment === '*') {
// return ':splats*'
// }

return validSegment
}
2 changes: 1 addition & 1 deletion packages/core/src/sync/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export function createRoutesSync(options: OptionsSync = {}): Route[] {
const { dir = '.', cache } = options
const cwd = process.cwd()
const resolvedDir = dir.startsWith(cwd)
? dir.replace(cwd, '').replace(/^[\\\/]+/, '') || '.'
? dir.replace(cwd, '').replace(/^[/\\]+/, '') || '.'
: dir

if (cache && cacheRouteSync[dir]) {
Expand Down
27 changes: 23 additions & 4 deletions packages/core/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,10 @@ export function createRoute(
options: { root: string; urlPrefix?: string; urlSuffix?: string }
): Route {
const { root, urlPrefix = '/', urlSuffix = '' } = options
const normalizedUrlPrefix =
urlPrefix === '' ? './' : urlPrefix.replace(/\/+$/g, '') + '/'

const segments = routeSegments(id, root)
const stem = segments.join('/')
const url = normalizedUrlPrefix + stem + urlSuffix
const url = constructUrl(segments, urlPrefix, urlSuffix)
const index = url.endsWith('/index' + urlSuffix)

const route: Route = { id, stem, url, index, isDynamic: false }
Expand All @@ -79,6 +77,27 @@ export function createRoute(
return route
}

// construct URL from segments
function constructUrl(segments: string[], prefix: string, suffix: string) {
const normalizedPrefix = normalizeUrlPrefix(prefix)

return (
segments
.reduce((url, segment, index) => {
if (segment.startsWith(':') && /[?*+]$/.test(segment)) {
return url + `{/${segment.slice(0, -1)}}${segment.slice(-1)}`
}
return url + (index === 0 ? segment : `/${segment}`)
}, normalizedPrefix)
.replace(/^\/{\//, '{/') + suffix
)
}

// normalize URL prefix
function normalizeUrlPrefix(prefix: string) {
return prefix ? `${prefix.replace(/\/+$/g, '')}/` : './'
}

/**
* Creates a parent route object based on the provided segment and parent
* route.
Expand Down Expand Up @@ -147,7 +166,7 @@ export function findRoute(requestUrl: string, routes: Route[]) {
export function applyDynamicRouteProps(route: Route) {
const regexp = pathToRegexp(route.url)
const fnMatch = match(route.url, {
encode: encodeURI
encodePath: encodeURI
})
const fnCompile = compile(route.url, {
encode: encodeURIComponent
Expand Down
37 changes: 19 additions & 18 deletions packages/core/test/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ it('should create a base route with absolute url prefix', () => {
it('should create a base route with full url prefix', () => {
const route = createRoute('pages/foo/bar', {
root: 'pages',
urlPrefix: 'https://example.com'
urlPrefix: 'https://example.com/'
})

expect(route).toMatchInlineSnapshot(`
Expand Down Expand Up @@ -126,7 +126,7 @@ it('should create a dynamic route', () => {
})

it('should create optional dynamic routes', () => {
const files: string[] = ['pages/:a?/:b', 'pages/$c?/$d', 'pages/[e]/f']
const files: string[] = ['pages/:a?/:b', 'pages/$c+/$d*', 'pages/:e/[f]']
const routes = files.map(file =>
createRoute(file, { root: 'pages', urlSuffix: '.html' })
)
Expand All @@ -138,21 +138,21 @@ it('should create optional dynamic routes', () => {
"index": false,
"isDynamic": true,
"stem": ":a?/:b",
"url": "/:a?/:b.html",
"url": "{/:a}?/:b.html",
},
{
"id": "pages/$c?/$d",
"id": "pages/$c+/$d*",
"index": false,
"isDynamic": true,
"stem": ":c?/:d",
"url": "/:c?/:d.html",
"stem": ":c+/:d*",
"url": "{/:c}+{/:d}*.html",
},
{
"id": "pages/[e]/f",
"id": "pages/:e/[f]",
"index": false,
"isDynamic": true,
"stem": ":e?/f",
"url": "/:e?/f.html",
"stem": ":e/:f?",
"url": "/:e{/:f}?.html",
},
]
`)
Expand Down Expand Up @@ -262,25 +262,26 @@ it('should find an optional route from routes object based on the request URL',
it('should find a splats route from routes object based on the request URL', () => {
const config = { root: 'pages' }
const routes = [
// splat or "catchall" or "star" segments (zero or more params)
// wildcard route
createRoute('pages/files/*.md', config),
// named splat segments (zero or more params)
// zero or more modifier
createRoute('pages/foo/:ids*.md', config),
// required splat segments (one or more params)
// one or more modifier
createRoute('pages/bar/:ids+.md', config)
]

// match splat segments
expect(findRoute('/files', routes)).toEqual(routes[0])
// match wildcard route
expect(findRoute('/files', routes)).toBeUndefined()
expect(findRoute('/files/', routes)).toEqual(routes[0])
expect(findRoute('/files/a', routes)).toEqual(routes[0])
expect(findRoute('/files/a/b', routes)).toEqual(routes[0])
expect(findRoute('/files/a/b/123', routes)).toEqual(routes[0])

const splatRoute = findRoute('/files/a/b/123', routes)
if (splatRoute?.isDynamic) {
const params = splatRoute.matchParams('/files/a/b/123')
const wildcardRoute = findRoute('/files/a/b/123', routes)
if (wildcardRoute?.isDynamic) {
const params = wildcardRoute.matchParams('/files/a/b/123')

expect(params).toEqual({ splats: ['a', 'b', '123'] })
expect(params).toEqual({ '0': ['a', 'b', '123'] })
}

// match named splat segments
Expand Down
6 changes: 4 additions & 2 deletions packages/core/test/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@ export function loadDatafiles(route: Route, root: string) {
const localData = loadFileSync(currSegment + datafile)

Object.assign(context, localData)
} catch {}
} catch {
/* empty */
}
})
})

route.context = context
Object.assign(route, { context })
}
18 changes: 7 additions & 11 deletions packages/vite/src/DataStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,11 @@ export class DataStore<V = unknown> extends InMemoryStore<V> {
const { dir, extensions, localDataDir, localDataSuffix } = this.config
const exts = `(${extensions.map(e => e.replace(/^\./, '')).join('|')})`

const source = [`${dir.replace(/[\/\\]+$/, '')}/**/*.${exts}`]
const source = [`${dir.replace(/[/\\]+$/, '')}/**/*.${exts}`]

if (localDataDir) {
source.push(
`${localDataDir.replace(/[\/\\]+$/, '')}/**/*${localDataSuffix}.${exts}`
`${localDataDir.replace(/[/\\]+$/, '')}/**/*${localDataSuffix}.${exts}`
)
}

Expand Down Expand Up @@ -132,17 +132,13 @@ export class DataStore<V = unknown> extends InMemoryStore<V> {
* @throws Error if an error occurs during data loading.
*/
async init() {
try {
const datasources = this.datasources
const datasources = this.datasources

this.clear()
this.clear()

for (const id of datasources) {
const data = await this.load(id)
this.set(id, data)
}
} catch (error) {
throw error
for (const id of datasources) {
const data = await this.load(id)
this.set(id, data)
}
}

Expand Down
11 changes: 7 additions & 4 deletions packages/vite/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,17 @@ export function buildNavigation<T extends object = object>(
const segments = route.stem.split('/').slice(1)
const lastSegment = String(
segments.length ? segments.pop() : route.stem
).replace(/\-/g, ' ')
).replace(/-/g, ' ')
const text =
lastSegment.slice(0, 1).toUpperCase() + lastSegment.slice(1).toLowerCase()

if ('children' in route) {
segments.length
? Object.assign(route, { id: route.stem.replace(/\//g, '-') })
: Object.assign(route, { type: 'group' })
Object.assign(
route,
segments.length
? { id: route.stem.replace(/\//g, '-') }
: { type: 'group' }
)
}

Object.assign(route, { text }, navMeta[route.stem])
Expand Down

0 comments on commit 17834ef

Please # to comment.