Nuxt/packages/nuxt3/src/pages/utils.ts

266 lines
7.4 KiB
TypeScript
Raw Normal View History

import { basename, extname, normalize, relative, resolve } from 'pathe'
import { encodePath } from 'ufo'
import type { Nuxt, NuxtMiddleware, NuxtPage } from '@nuxt/schema'
import { resolveFiles, useNuxt } from '@nuxt/kit'
import { kebabCase, pascalCase } from 'scule'
import { genImport, genDynamicImport, genArrayFromRaw } from 'knitwork'
import escapeRE from 'escape-string-regexp'
2020-08-18 18:34:08 +00:00
2021-03-18 14:26:41 +00:00
enum SegmentParserState {
initial,
static,
dynamic,
catchall,
2021-03-18 14:26:41 +00:00
}
enum SegmentTokenType {
static,
dynamic,
catchall,
2021-03-18 14:26:41 +00:00
}
interface SegmentToken {
type: SegmentTokenType
value: string
}
export async function resolvePagesRoutes (nuxt: Nuxt) {
const pagesDir = resolve(nuxt.options.srcDir, nuxt.options.dir.pages)
const files = await resolveFiles(pagesDir, `**/*{${nuxt.options.extensions.join(',')}}`)
2020-08-18 18:34:08 +00:00
// Sort to make sure parent are listed first
files.sort()
return generateRoutesFromFiles(files, pagesDir)
}
export function generateRoutesFromFiles (files: string[], pagesDir: string): NuxtPage[] {
const routes: NuxtPage[] = []
2020-08-18 18:34:08 +00:00
for (const file of files) {
const segments = relative(pagesDir, file)
.replace(new RegExp(`${escapeRE(extname(file))}$`), '')
2020-08-18 18:34:08 +00:00
.split('/')
const route: NuxtPage = {
2020-08-18 18:34:08 +00:00
name: '',
path: '',
file,
children: []
2020-08-18 18:34:08 +00:00
}
// Array where routes should be added, useful when adding child routes
2020-08-18 18:34:08 +00:00
let parent = routes
for (let i = 0; i < segments.length; i++) {
const segment = segments[i]
2020-08-18 18:34:08 +00:00
const tokens = parseSegment(segment)
const segmentName = tokens.map(({ value }) => value).join('')
const isSingleSegment = segments.length === 1
const isLastSegment = i === segments.length - 1
2020-08-18 18:34:08 +00:00
// ex: parent/[slug].vue -> parent-slug
route.name += (route.name && '-') + segmentName
// ex: parent.vue + parent/child.vue
const child = parent.find(parentRoute => parentRoute.name === route.name)
2020-08-18 18:34:08 +00:00
if (child) {
parent = child.children
route.path = ''
} else if (segmentName === '404' && isSingleSegment) {
route.path += '/:catchAll(.*)*'
} else if (segmentName === 'index' && !route.path) {
2020-08-18 18:34:08 +00:00
route.path += '/'
} else if (segmentName !== 'index') {
route.path += getRoutePath(tokens)
if (isLastSegment && tokens.length === 1 && tokens[0].type === SegmentTokenType.dynamic) {
route.path += '?'
2020-08-18 18:34:08 +00:00
}
}
}
parent.push(route)
}
return prepareRoutes(routes)
}
function getRoutePath (tokens: SegmentToken[]): string {
return tokens.reduce((path, token) => {
return (
path +
(token.type === SegmentTokenType.dynamic
? `:${token.value}`
: token.type === SegmentTokenType.catchall
? `:${token.value}(.*)*`
: encodePath(token.value))
)
}, '/')
}
const PARAM_CHAR_RE = /[\w\d_.]/
function parseSegment (segment: string) {
let state: SegmentParserState = SegmentParserState.initial
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
: state === SegmentParserState.dynamic
? SegmentTokenType.dynamic
: SegmentTokenType.catchall,
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
case SegmentParserState.catchall:
case SegmentParserState.dynamic:
if (buffer === '...') {
buffer = ''
state = SegmentParserState.catchall
}
if (c === ']') {
if (!buffer) {
throw new Error('Empty param')
} else {
consumeBuffer()
}
state = SegmentParserState.initial
} else if (PARAM_CHAR_RE.test(c)) {
buffer += c
} else {
// eslint-disable-next-line no-console
// console.debug(`[pages]Ignored character "${c}" while building param "${buffer}" from "segment"`)
}
break
}
i++
}
if (state === SegmentParserState.dynamic) {
throw new Error(`Unfinished param "${buffer}"`)
}
consumeBuffer()
return tokens
}
function prepareRoutes (routes: NuxtPage[], parent?: NuxtPage) {
2020-08-18 18:34:08 +00:00
for (const route of routes) {
// Remove -index
2020-08-18 18:34:08 +00:00
if (route.name) {
route.name = route.name.replace(/-index$/, '')
}
if (route.path === '/') {
// Remove ? suffix when index page at same level
routes.forEach((siblingRoute) => {
if (siblingRoute.path.endsWith('?')) {
siblingRoute.path = siblingRoute.path.slice(0, -1)
}
})
}
// Remove leading / if children route
if (parent && route.path.startsWith('/')) {
route.path = route.path.slice(1)
2020-08-18 18:34:08 +00:00
}
if (route.children.length) {
route.children = prepareRoutes(route.children, route)
}
if (route.children.find(childRoute => childRoute.path === '')) {
2020-08-18 18:34:08 +00:00
delete route.name
}
}
2020-08-18 18:34:08 +00:00
return routes
}
2021-06-30 16:32:22 +00:00
export async function resolveLayouts (nuxt: Nuxt) {
const layoutDir = resolve(nuxt.options.srcDir, nuxt.options.dir.layouts)
const files = await resolveFiles(layoutDir, `*{${nuxt.options.extensions.join(',')}}`)
const layouts = files.map(file => ({ name: getNameFromPath(file), file }))
await nuxt.callHook('pages:layouts:extend', layouts)
return layouts
2021-06-30 16:32:22 +00:00
}
export function normalizeRoutes (routes: NuxtPage[], metaImports: Set<string> = new Set()): { imports: Set<string>, routes: string } {
return {
imports: metaImports,
routes: genArrayFromRaw(routes.map((route) => {
const file = normalize(route.file)
const metaImportName = getImportName(file) + 'Meta'
metaImports.add(genImport(`${file}?macro=true`, [{ name: 'meta', as: metaImportName }]))
return {
...Object.fromEntries(Object.entries(route).map(([key, value]) => [key, JSON.stringify(value)])),
children: route.children ? normalizeRoutes(route.children, metaImports).routes : [],
meta: route.meta ? JSON.stringify(route.meta) : metaImportName,
component: genDynamicImport(file)
}
}))
}
}
export async function resolveMiddleware (): Promise<NuxtMiddleware[]> {
const nuxt = useNuxt()
const middlewareDir = resolve(nuxt.options.srcDir, nuxt.options.dir.middleware)
const files = await resolveFiles(middlewareDir, `*{${nuxt.options.extensions.join(',')}}`)
const middleware = files.map(path => ({ name: getNameFromPath(path), path, global: hasSuffix(path, '.global') }))
await nuxt.callHook('pages:middleware:extend', middleware)
return middleware
}
function getNameFromPath (path: string) {
return kebabCase(basename(path).replace(extname(path), '')).replace(/["']/g, '').replace('.global', '')
}
function hasSuffix (path: string, suffix: string) {
return basename(path).replace(extname(path), '').endsWith(suffix)
}
export function getImportName (name: string) {
return pascalCase(name).replace(/[^\w]/g, '')
}