2022-06-27 12:10:29 +00:00
|
|
|
import { extname, normalize, relative, resolve } from 'pathe'
|
2021-01-18 12:22:38 +00:00
|
|
|
import { encodePath } from 'ufo'
|
2022-12-11 21:44:52 +00:00
|
|
|
import type { NuxtPage } from '@nuxt/schema'
|
2022-01-25 12:29:11 +00:00
|
|
|
import { resolveFiles, useNuxt } from '@nuxt/kit'
|
2022-06-10 13:12:21 +00:00
|
|
|
import { genImport, genDynamicImport, genArrayFromRaw, genSafeVariableName } from 'knitwork'
|
2022-01-27 11:13:32 +00:00
|
|
|
import escapeRE from 'escape-string-regexp'
|
2022-06-27 12:10:29 +00:00
|
|
|
import { uniqueBy } from '../core/utils'
|
2020-08-18 18:34:08 +00:00
|
|
|
|
2021-03-18 14:26:41 +00:00
|
|
|
enum SegmentParserState {
|
|
|
|
initial,
|
|
|
|
static,
|
|
|
|
dynamic,
|
2022-04-26 16:10:05 +00:00
|
|
|
optional,
|
2021-06-21 12:09:08 +00:00
|
|
|
catchall,
|
2021-03-18 14:26:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
enum SegmentTokenType {
|
|
|
|
static,
|
|
|
|
dynamic,
|
2022-04-26 16:10:05 +00:00
|
|
|
optional,
|
2021-06-21 12:09:08 +00:00
|
|
|
catchall,
|
2021-03-18 14:26:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
interface SegmentToken {
|
|
|
|
type: SegmentTokenType
|
|
|
|
value: string
|
|
|
|
}
|
|
|
|
|
2022-03-22 18:12:54 +00:00
|
|
|
export async function resolvePagesRoutes (): Promise<NuxtPage[]> {
|
|
|
|
const nuxt = useNuxt()
|
|
|
|
|
|
|
|
const pagesDirs = nuxt.options._layers.map(
|
|
|
|
layer => resolve(layer.config.srcDir, layer.config.dir?.pages || 'pages')
|
|
|
|
)
|
2020-08-18 18:34:08 +00:00
|
|
|
|
2022-03-22 18:12:54 +00:00
|
|
|
const allRoutes = (await Promise.all(
|
|
|
|
pagesDirs.map(async (dir) => {
|
|
|
|
const files = await resolveFiles(dir, `**/*{${nuxt.options.extensions.join(',')}}`)
|
|
|
|
// Sort to make sure parent are listed first
|
|
|
|
files.sort()
|
|
|
|
return generateRoutesFromFiles(files, dir)
|
|
|
|
})
|
|
|
|
)).flat()
|
2021-05-20 11:42:41 +00:00
|
|
|
|
2022-03-25 11:55:05 +00:00
|
|
|
return uniqueBy(allRoutes, 'path')
|
2021-01-18 12:22:38 +00:00
|
|
|
}
|
|
|
|
|
2022-01-17 18:27:23 +00:00
|
|
|
export function generateRoutesFromFiles (files: string[], pagesDir: string): NuxtPage[] {
|
|
|
|
const routes: NuxtPage[] = []
|
2020-08-18 18:34:08 +00:00
|
|
|
|
|
|
|
for (const file of files) {
|
2021-01-18 12:22:38 +00:00
|
|
|
const segments = relative(pagesDir, file)
|
2022-01-27 11:13:32 +00:00
|
|
|
.replace(new RegExp(`${escapeRE(extname(file))}$`), '')
|
2020-08-18 18:34:08 +00:00
|
|
|
.split('/')
|
|
|
|
|
2022-01-17 18:27:23 +00:00
|
|
|
const route: NuxtPage = {
|
2020-08-18 18:34:08 +00:00
|
|
|
name: '',
|
|
|
|
path: '',
|
2021-01-18 12:22:38 +00:00
|
|
|
file,
|
|
|
|
children: []
|
2020-08-18 18:34:08 +00:00
|
|
|
}
|
|
|
|
|
2021-05-20 11:42:41 +00:00
|
|
|
// Array where routes should be added, useful when adding child routes
|
2020-08-18 18:34:08 +00:00
|
|
|
let parent = routes
|
|
|
|
|
2021-01-18 12:22:38 +00:00
|
|
|
for (let i = 0; i < segments.length; i++) {
|
|
|
|
const segment = segments[i]
|
2020-08-18 18:34:08 +00:00
|
|
|
|
2021-01-18 12:22:38 +00:00
|
|
|
const tokens = parseSegment(segment)
|
|
|
|
const segmentName = tokens.map(({ value }) => value).join('')
|
2020-08-18 18:34:08 +00:00
|
|
|
|
2021-01-18 12:22:38 +00:00
|
|
|
// ex: parent/[slug].vue -> parent-slug
|
|
|
|
route.name += (route.name && '-') + segmentName
|
|
|
|
|
|
|
|
// ex: parent.vue + parent/child.vue
|
2022-08-01 07:51:46 +00:00
|
|
|
const child = parent.find(parentRoute => parentRoute.name === route.name && !parentRoute.path.endsWith('(.*)*'))
|
|
|
|
|
2022-08-12 17:47:58 +00:00
|
|
|
if (child && child.children) {
|
2020-08-18 18:34:08 +00:00
|
|
|
parent = child.children
|
|
|
|
route.path = ''
|
2021-01-18 12:22:38 +00:00
|
|
|
} else if (segmentName === 'index' && !route.path) {
|
2020-08-18 18:34:08 +00:00
|
|
|
route.path += '/'
|
2021-01-18 12:22:38 +00:00
|
|
|
} else if (segmentName !== 'index') {
|
|
|
|
route.path += getRoutePath(tokens)
|
2020-08-18 18:34:08 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
parent.push(route)
|
|
|
|
}
|
|
|
|
|
|
|
|
return prepareRoutes(routes)
|
|
|
|
}
|
|
|
|
|
2021-01-18 12:22:38 +00:00
|
|
|
function getRoutePath (tokens: SegmentToken[]): string {
|
|
|
|
return tokens.reduce((path, token) => {
|
|
|
|
return (
|
|
|
|
path +
|
2022-04-26 16:10:05 +00:00
|
|
|
(token.type === SegmentTokenType.optional
|
|
|
|
? `:${token.value}?`
|
|
|
|
: token.type === SegmentTokenType.dynamic
|
|
|
|
? `:${token.value}`
|
|
|
|
: token.type === SegmentTokenType.catchall
|
|
|
|
? `:${token.value}(.*)*`
|
|
|
|
: encodePath(token.value))
|
2021-01-18 12:22:38 +00:00
|
|
|
)
|
|
|
|
}, '/')
|
|
|
|
}
|
|
|
|
|
2021-06-21 12:09:08 +00:00
|
|
|
const PARAM_CHAR_RE = /[\w\d_.]/
|
2021-01-18 12:22:38 +00:00
|
|
|
|
|
|
|
function parseSegment (segment: string) {
|
2021-06-21 12:09:08 +00:00
|
|
|
let state: SegmentParserState = SegmentParserState.initial
|
2021-01-18 12:22:38 +00:00
|
|
|
let i = 0
|
|
|
|
|
|
|
|
let buffer = ''
|
|
|
|
const tokens: SegmentToken[] = []
|
|
|
|
|
|
|
|
function consumeBuffer () {
|
|
|
|
if (!buffer) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if (state === SegmentParserState.initial) {
|
|
|
|
throw new Error('wrong state')
|
|
|
|
}
|
|
|
|
|
|
|
|
tokens.push({
|
|
|
|
type:
|
|
|
|
state === SegmentParserState.static
|
|
|
|
? SegmentTokenType.static
|
2021-06-21 12:09:08 +00:00
|
|
|
: state === SegmentParserState.dynamic
|
|
|
|
? SegmentTokenType.dynamic
|
2022-04-26 16:10:05 +00:00
|
|
|
: state === SegmentParserState.optional
|
|
|
|
? SegmentTokenType.optional
|
|
|
|
: SegmentTokenType.catchall,
|
2021-01-18 12:22:38 +00:00
|
|
|
value: buffer
|
|
|
|
})
|
|
|
|
|
|
|
|
buffer = ''
|
|
|
|
}
|
|
|
|
|
|
|
|
while (i < segment.length) {
|
|
|
|
const c = segment[i]
|
|
|
|
|
|
|
|
switch (state) {
|
|
|
|
case SegmentParserState.initial:
|
|
|
|
buffer = ''
|
|
|
|
if (c === '[') {
|
|
|
|
state = SegmentParserState.dynamic
|
|
|
|
} else {
|
|
|
|
i--
|
|
|
|
state = SegmentParserState.static
|
|
|
|
}
|
|
|
|
break
|
|
|
|
|
|
|
|
case SegmentParserState.static:
|
|
|
|
if (c === '[') {
|
|
|
|
consumeBuffer()
|
|
|
|
state = SegmentParserState.dynamic
|
|
|
|
} else {
|
|
|
|
buffer += c
|
|
|
|
}
|
|
|
|
break
|
|
|
|
|
2021-06-21 12:09:08 +00:00
|
|
|
case SegmentParserState.catchall:
|
2021-01-18 12:22:38 +00:00
|
|
|
case SegmentParserState.dynamic:
|
2022-04-26 16:10:05 +00:00
|
|
|
case SegmentParserState.optional:
|
2021-06-21 12:09:08 +00:00
|
|
|
if (buffer === '...') {
|
|
|
|
buffer = ''
|
|
|
|
state = SegmentParserState.catchall
|
|
|
|
}
|
2022-04-26 16:10:05 +00:00
|
|
|
if (c === '[' && state === SegmentParserState.dynamic) {
|
|
|
|
state = SegmentParserState.optional
|
|
|
|
}
|
|
|
|
if (c === ']' && (state !== SegmentParserState.optional || buffer[buffer.length - 1] === ']')) {
|
2021-10-20 18:12:55 +00:00
|
|
|
if (!buffer) {
|
|
|
|
throw new Error('Empty param')
|
|
|
|
} else {
|
|
|
|
consumeBuffer()
|
|
|
|
}
|
2021-01-18 12:22:38 +00:00
|
|
|
state = SegmentParserState.initial
|
|
|
|
} else if (PARAM_CHAR_RE.test(c)) {
|
|
|
|
buffer += c
|
|
|
|
} else {
|
|
|
|
// eslint-disable-next-line no-console
|
2021-10-20 18:12:55 +00:00
|
|
|
// console.debug(`[pages]Ignored character "${c}" while building param "${buffer}" from "segment"`)
|
2021-01-18 12:22:38 +00:00
|
|
|
}
|
|
|
|
break
|
|
|
|
}
|
|
|
|
i++
|
|
|
|
}
|
|
|
|
|
|
|
|
if (state === SegmentParserState.dynamic) {
|
|
|
|
throw new Error(`Unfinished param "${buffer}"`)
|
|
|
|
}
|
|
|
|
|
|
|
|
consumeBuffer()
|
|
|
|
|
|
|
|
return tokens
|
|
|
|
}
|
|
|
|
|
2022-01-17 18:27:23 +00:00
|
|
|
function prepareRoutes (routes: NuxtPage[], parent?: NuxtPage) {
|
2020-08-18 18:34:08 +00:00
|
|
|
for (const route of routes) {
|
2021-01-18 12:22:38 +00:00
|
|
|
// Remove -index
|
2020-08-18 18:34:08 +00:00
|
|
|
if (route.name) {
|
|
|
|
route.name = route.name.replace(/-index$/, '')
|
|
|
|
}
|
|
|
|
|
2021-01-18 12:22:38 +00:00
|
|
|
// Remove leading / if children route
|
|
|
|
if (parent && route.path.startsWith('/')) {
|
|
|
|
route.path = route.path.slice(1)
|
2020-08-18 18:34:08 +00:00
|
|
|
}
|
|
|
|
|
2022-08-12 17:47:58 +00:00
|
|
|
if (route.children?.length) {
|
2021-01-18 12:22:38 +00:00
|
|
|
route.children = prepareRoutes(route.children, route)
|
|
|
|
}
|
|
|
|
|
2022-08-12 17:47:58 +00:00
|
|
|
if (route.children?.find(childRoute => childRoute.path === '')) {
|
2020-08-18 18:34:08 +00:00
|
|
|
delete route.name
|
|
|
|
}
|
|
|
|
}
|
2021-01-18 12:22:38 +00:00
|
|
|
|
2020-08-18 18:34:08 +00:00
|
|
|
return routes
|
|
|
|
}
|
2021-06-30 16:32:22 +00:00
|
|
|
|
2022-02-07 13:45:47 +00:00
|
|
|
export function normalizeRoutes (routes: NuxtPage[], metaImports: Set<string> = new Set()): { imports: Set<string>, routes: string } {
|
2022-01-17 18:27:23 +00:00
|
|
|
return {
|
|
|
|
imports: metaImports,
|
2022-02-07 13:45:47 +00:00
|
|
|
routes: genArrayFromRaw(routes.map((route) => {
|
2022-01-21 09:54:11 +00:00
|
|
|
const file = normalize(route.file)
|
2022-06-10 13:12:21 +00:00
|
|
|
const metaImportName = genSafeVariableName(file) + 'Meta'
|
2022-11-02 10:28:41 +00:00
|
|
|
metaImports.add(genImport(`${file}?macro=true`, [{ name: 'default', as: metaImportName }]))
|
2022-09-05 07:53:01 +00:00
|
|
|
|
|
|
|
let aliasCode = `${metaImportName}?.alias || []`
|
|
|
|
if (Array.isArray(route.alias) && route.alias.length) {
|
|
|
|
aliasCode = `${JSON.stringify(route.alias)}.concat(${aliasCode})`
|
|
|
|
}
|
|
|
|
|
2022-01-17 18:27:23 +00:00
|
|
|
return {
|
2022-02-07 13:45:47 +00:00
|
|
|
...Object.fromEntries(Object.entries(route).map(([key, value]) => [key, JSON.stringify(value)])),
|
2022-11-03 14:05:38 +00:00
|
|
|
name: `${metaImportName}?.name ?? ${route.name ? JSON.stringify(route.name) : 'undefined'}`,
|
|
|
|
path: `${metaImportName}?.path ?? ${JSON.stringify(route.path)}`,
|
2022-01-17 18:27:23 +00:00
|
|
|
children: route.children ? normalizeRoutes(route.children, metaImports).routes : [],
|
2022-03-11 11:52:05 +00:00
|
|
|
meta: route.meta ? `{...(${metaImportName} || {}), ...${JSON.stringify(route.meta)}}` : metaImportName,
|
2022-09-05 07:53:01 +00:00
|
|
|
alias: aliasCode,
|
2022-09-22 13:54:34 +00:00
|
|
|
redirect: route.redirect ? JSON.stringify(route.redirect) : `${metaImportName}?.redirect || undefined`,
|
2022-08-09 18:25:35 +00:00
|
|
|
component: genDynamicImport(file, { interopDefault: true })
|
2022-01-17 18:27:23 +00:00
|
|
|
}
|
2022-02-07 13:45:47 +00:00
|
|
|
}))
|
2022-01-17 18:27:23 +00:00
|
|
|
}
|
2021-10-20 18:49:15 +00:00
|
|
|
}
|