mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-11 08:33:53 +00:00
feat: initial work for pages routing (#113)
* feat: initial work for routing Co-Authored-By: Eduardo San Martin Morote <posva@users.noreply.github.com> * test: test for page builder * test: add tests for names * feat(routes): stringify routes * feat(routes): support multiple slugs * feat(routes): encode paths * test: remove nuxt test utils * refactor: app.template.* * chore: fix lint errors * simplify and fix jest/test * refactor: move hook todo to app.ts (since pages/ might be optional) * add todo for __file exposing Co-authored-by: Sébastien Chopin <seb@nuxtjs.com> Co-authored-by: Pooya Parsa <pyapar@gmail.com>
This commit is contained in:
parent
0091dba181
commit
a6f9fb4c7a
@ -8,13 +8,17 @@ export interface NuxtApp {
|
|||||||
routes: NuxtRoute[]
|
routes: NuxtRoute[]
|
||||||
dir: string
|
dir: string
|
||||||
extensions: string[]
|
extensions: string[]
|
||||||
|
templates: Record<string, string>
|
||||||
pages?: {
|
pages?: {
|
||||||
dir: string
|
dir: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scan project structure
|
// Scan project structure
|
||||||
export async function createApp (builder: Builder, options: Partial<NuxtApp> = {}): Promise<NuxtApp> {
|
export async function createApp (
|
||||||
|
builder: Builder,
|
||||||
|
options: Partial<NuxtApp> = {}
|
||||||
|
): Promise<NuxtApp> {
|
||||||
const { nuxt } = builder
|
const { nuxt } = builder
|
||||||
|
|
||||||
// Create base app object
|
// Create base app object
|
||||||
@ -22,6 +26,7 @@ export async function createApp (builder: Builder, options: Partial<NuxtApp> = {
|
|||||||
dir: nuxt.options.srcDir,
|
dir: nuxt.options.srcDir,
|
||||||
extensions: nuxt.options.extensions,
|
extensions: nuxt.options.extensions,
|
||||||
routes: [],
|
routes: [],
|
||||||
|
templates: {},
|
||||||
pages: {
|
pages: {
|
||||||
dir: 'pages'
|
dir: 'pages'
|
||||||
}
|
}
|
||||||
@ -29,13 +34,18 @@ export async function createApp (builder: Builder, options: Partial<NuxtApp> = {
|
|||||||
|
|
||||||
// Resolve app.main
|
// Resolve app.main
|
||||||
if (!app.main) {
|
if (!app.main) {
|
||||||
app.main = nuxt.resolver.tryResolvePath('~/App') ||
|
app.main =
|
||||||
|
nuxt.resolver.tryResolvePath('~/App') ||
|
||||||
nuxt.resolver.tryResolvePath('~/app')
|
nuxt.resolver.tryResolvePath('~/app')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve pages/
|
// Resolve pages/
|
||||||
if (app.pages) {
|
if (app.pages) {
|
||||||
app.routes.push(...await resolvePagesRoutes(builder, app))
|
app.routes.push(...(await resolvePagesRoutes(builder, app)))
|
||||||
|
}
|
||||||
|
// TODO: Hook to extend routes
|
||||||
|
if (app.routes.length) {
|
||||||
|
app.templates.routes = serializeRoutes(app.routes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback app.main
|
// Fallback app.main
|
||||||
@ -47,3 +57,22 @@ export async function createApp (builder: Builder, options: Partial<NuxtApp> = {
|
|||||||
|
|
||||||
return app
|
return app
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function serializeRoutes (routes: NuxtRoute[]) {
|
||||||
|
return JSON.stringify(
|
||||||
|
routes.map(formatRoute),
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
).replace(/"{(.+)}"/g, '$1')
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatRoute (route: NuxtRoute) {
|
||||||
|
return {
|
||||||
|
name: route.name,
|
||||||
|
path: route.path,
|
||||||
|
children: route.children.map(formatRoute),
|
||||||
|
// TODO: avoid exposing to prod
|
||||||
|
__file: route.file,
|
||||||
|
component: `{() => import('${route.file}' /* webpackChunkName: '${route.name}' */)}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
import { resolve, extname, relative } from 'path'
|
import { resolve, extname, relative } from 'path'
|
||||||
|
import { encodePath } from 'ufo'
|
||||||
import { NuxtApp } from './app'
|
import { NuxtApp } from './app'
|
||||||
import { resolveFiles } from './utils'
|
import { resolveFiles } from './utils'
|
||||||
|
|
||||||
const isDynamicRoute = (s: string) => /^\[.+\]$/.test(s)
|
// Check if name has [slug]
|
||||||
|
|
||||||
export interface NuxtRoute {
|
export interface NuxtRoute {
|
||||||
name?: string
|
name: string
|
||||||
path: string
|
path: string
|
||||||
file: string
|
file: string
|
||||||
children?: NuxtRoute[]
|
children: NuxtRoute[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function resolvePagesRoutes (builder, app: NuxtApp) {
|
export async function resolvePagesRoutes (builder, app: NuxtApp) {
|
||||||
@ -16,53 +16,55 @@ export async function resolvePagesRoutes (builder, app: NuxtApp) {
|
|||||||
const pagesPattern = `${app.pages!.dir}/**/*.{${app.extensions.join(',')}}`
|
const pagesPattern = `${app.pages!.dir}/**/*.{${app.extensions.join(',')}}`
|
||||||
const files = await resolveFiles(builder, pagesPattern, app.dir)
|
const files = await resolveFiles(builder, pagesPattern, app.dir)
|
||||||
|
|
||||||
|
// Sort to make sure parent are listed first
|
||||||
|
return generateRoutesFromFiles(files.sort(), pagesDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateRoutesFromFiles (
|
||||||
|
files: string[],
|
||||||
|
pagesDir: string
|
||||||
|
): NuxtRoute[] {
|
||||||
const routes: NuxtRoute[] = []
|
const routes: NuxtRoute[] = []
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const pathParts = relative(pagesDir, file)
|
const segments = relative(pagesDir, file)
|
||||||
.replace(new RegExp(`${extname(file)}$`), '')
|
.replace(new RegExp(`${extname(file)}$`), '')
|
||||||
.split('/')
|
.split('/')
|
||||||
|
|
||||||
const route: NuxtRoute = {
|
const route: NuxtRoute = {
|
||||||
name: '',
|
name: '',
|
||||||
path: '',
|
path: '',
|
||||||
file
|
file,
|
||||||
|
children: []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// array where routes should be added, useful when adding child routes
|
||||||
let parent = routes
|
let parent = routes
|
||||||
|
|
||||||
for (let i = 0; i < pathParts.length; i++) {
|
for (let i = 0; i < segments.length; i++) {
|
||||||
const part = pathParts[i]
|
const segment = segments[i]
|
||||||
// Remove square brackets at the start and end.
|
|
||||||
const isDynamicPart = isDynamicRoute(part)
|
|
||||||
const normalizedPart = (isDynamicPart
|
|
||||||
? part.replace(/^\[(\.{3})?/, '').replace(/\]$/, '')
|
|
||||||
: part
|
|
||||||
).toLowerCase()
|
|
||||||
|
|
||||||
route.name += route.name ? `-${normalizedPart}` : normalizedPart
|
const tokens = parseSegment(segment)
|
||||||
|
const segmentName = tokens.map(({ value }) => value).join('')
|
||||||
|
const isSingleSegment = segments.length === 1
|
||||||
|
const isLastSegment = i === segments.length - 1
|
||||||
|
|
||||||
const child = parent.find(
|
// ex: parent/[slug].vue -> parent-slug
|
||||||
parentRoute => parentRoute.name === route.name
|
route.name += (route.name && '-') + segmentName
|
||||||
)
|
|
||||||
|
// ex: parent.vue + parent/child.vue
|
||||||
|
const child = parent.find(parentRoute => parentRoute.name === route.name)
|
||||||
if (child) {
|
if (child) {
|
||||||
child.children = child.children || []
|
|
||||||
parent = child.children
|
parent = child.children
|
||||||
route.path = ''
|
route.path = ''
|
||||||
} else if (normalizedPart === 'index' && !route.path) {
|
} else if (segmentName === '404' && isSingleSegment) {
|
||||||
|
route.path += '/:catchAll(.*)*'
|
||||||
|
} else if (segmentName === 'index' && !route.path) {
|
||||||
route.path += '/'
|
route.path += '/'
|
||||||
} else if (normalizedPart !== 'index') {
|
} else if (segmentName !== 'index') {
|
||||||
if (isDynamicPart) {
|
route.path += getRoutePath(tokens)
|
||||||
route.path += `/:${normalizedPart}`
|
if (isLastSegment && tokens.length === 1 && tokens[0].type === SegmentTokenType.dynamic) {
|
||||||
|
route.path += '?'
|
||||||
// Catch-all route
|
|
||||||
if (/^\[\.{3}/.test(part)) {
|
|
||||||
route.path += '(.*)'
|
|
||||||
} else if (i === pathParts.length - 1) {
|
|
||||||
// route.path += '?'
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
route.path += `/${normalizedPart}`
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -73,20 +75,138 @@ export async function resolvePagesRoutes (builder, app: NuxtApp) {
|
|||||||
return prepareRoutes(routes)
|
return prepareRoutes(routes)
|
||||||
}
|
}
|
||||||
|
|
||||||
function prepareRoutes (routes: NuxtRoute[], hasParent = false) {
|
function getRoutePath (tokens: SegmentToken[]): string {
|
||||||
|
return tokens.reduce((path, token) => {
|
||||||
|
return (
|
||||||
|
path +
|
||||||
|
(token.type === SegmentTokenType.dynamic
|
||||||
|
? `:${token.value}`
|
||||||
|
: encodePath(token.value))
|
||||||
|
)
|
||||||
|
}, '/')
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: should be const
|
||||||
|
enum SegmentParserState {
|
||||||
|
initial,
|
||||||
|
static,
|
||||||
|
dynamic,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: should be const
|
||||||
|
enum SegmentTokenType {
|
||||||
|
static,
|
||||||
|
dynamic,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SegmentToken {
|
||||||
|
type: SegmentTokenType
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const PARAM_CHAR_RE = /[\w\d_]/
|
||||||
|
|
||||||
|
function parseSegment (segment: string) {
|
||||||
|
let state = 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
|
||||||
|
: SegmentTokenType.dynamic,
|
||||||
|
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.dynamic:
|
||||||
|
if (c === ']') {
|
||||||
|
consumeBuffer()
|
||||||
|
state = SegmentParserState.initial
|
||||||
|
} else if (PARAM_CHAR_RE.test(c)) {
|
||||||
|
buffer += c
|
||||||
|
} else {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(`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: NuxtRoute[], parent?: NuxtRoute) {
|
||||||
for (const route of routes) {
|
for (const route of routes) {
|
||||||
|
// Remove -index
|
||||||
if (route.name) {
|
if (route.name) {
|
||||||
route.name = route.name.replace(/-index$/, '')
|
route.name = route.name.replace(/-index$/, '')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasParent) {
|
if (route.path === '/') {
|
||||||
route.path = route.path.replace(/^\//, '').replace(/\?$/, '')
|
// 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (route.children) {
|
if (route.children.length) {
|
||||||
|
route.children = prepareRoutes(route.children, route)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (route.children.find(childRoute => childRoute.path === '')) {
|
||||||
delete route.name
|
delete route.name
|
||||||
route.children = prepareRoutes(route.children, true)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return routes
|
return routes
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user