Skip to content

Commit

Permalink
feat(nuxt): add experimental typedPages option (nuxt#20367)
Browse files Browse the repository at this point in the history
  • Loading branch information
posva authored May 9, 2023
1 parent 80d7899 commit 5781cf1
Show file tree
Hide file tree
Showing 23 changed files with 339 additions and 35 deletions.
4 changes: 4 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@
{
"pattern": "@nuxt/test-utils",
"group": "external"
},
{
"pattern": "#vue-router",
"group": "external"
}
]
}
Expand Down
1 change: 1 addition & 0 deletions packages/nuxt/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
"unenv": "^1.4.1",
"unimport": "^3.0.6",
"unplugin": "^1.3.1",
"unplugin-vue-router": "^0.6.4",
"untyped": "^1.3.2",
"vue": "^3.2.47",
"vue-bundle-renderer": "^1.0.3",
Expand Down
10 changes: 5 additions & 5 deletions packages/nuxt/src/app/components/nuxt-link.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { ComputedRef, DefineComponent, PropType } from 'vue'
import { computed, defineComponent, h, onBeforeUnmount, onMounted, ref, resolveComponent } from 'vue'
import type { RouteLocation, RouteLocationRaw } from 'vue-router'
import type { RouteLocation, RouteLocationRaw } from '#vue-router'
import { hasProtocol, parseQuery, parseURL, withTrailingSlash, withoutTrailingSlash } from 'ufo'

import { preloadRouteComponents } from '../composables/preload'
Expand All @@ -24,8 +24,8 @@ export type NuxtLinkOptions = {

export type NuxtLinkProps = {
// Routing
to?: string | RouteLocationRaw
href?: string | RouteLocationRaw
to?: RouteLocationRaw
href?: RouteLocationRaw
external?: boolean
replace?: boolean
custom?: boolean
Expand Down Expand Up @@ -81,12 +81,12 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
props: {
// Routing
to: {
type: [String, Object] as PropType<string | RouteLocationRaw>,
type: [String, Object] as PropType<RouteLocationRaw>,
default: undefined,
required: false
},
href: {
type: [String, Object] as PropType<string | RouteLocationRaw>,
type: [String, Object] as PropType<RouteLocationRaw>,
default: undefined,
required: false
},
Expand Down
2 changes: 1 addition & 1 deletion packages/nuxt/src/app/composables/preload.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Component } from 'vue'
import type { RouteLocationRaw, Router } from 'vue-router'
import type { RouteLocationRaw, Router } from '#vue-router'
import { useNuxtApp } from '../nuxt'
import { useRouter } from './router'

Expand Down
6 changes: 3 additions & 3 deletions packages/nuxt/src/app/composables/router.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { getCurrentInstance, inject, onUnmounted } from 'vue'
import type { Ref } from 'vue'
import type { NavigationFailure, NavigationGuard, RouteLocationNormalized, RouteLocationNormalizedLoaded, RouteLocationPathRaw, RouteLocationRaw, Router } from 'vue-router'
import type { NavigationFailure, NavigationGuard, RouteLocationNormalized, RouteLocationPathRaw, RouteLocationRaw, Router, useRoute as _useRoute, useRouter as _useRouter } from '#vue-router'
import { sanitizeStatusCode } from 'h3'
import { hasProtocol, joinURL, parseURL } from 'ufo'

Expand All @@ -11,11 +11,11 @@ import { useState } from './state'

import type { PageMeta } from '#app'

export const useRouter = () => {
export const useRouter: typeof _useRouter = () => {
return useNuxtApp()?.$router as Router
}

export const useRoute = (): RouteLocationNormalizedLoaded => {
export const useRoute: typeof _useRoute = () => {
if (process.dev && isProcessingMiddleware()) {
console.warn('[nuxt] Calling `useRoute` within middleware may lead to misleading results. Instead, use the (to, from) arguments passed to the middleware to access the new and old routes.')
}
Expand Down
2 changes: 1 addition & 1 deletion packages/nuxt/src/app/nuxt.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable no-use-before-define */
import { getCurrentInstance, reactive } from 'vue'
import type { App, Ref, VNode, onErrorCaptured } from 'vue'
import type { RouteLocationNormalizedLoaded } from 'vue-router'
import type { RouteLocationNormalizedLoaded } from '#vue-router'
import type { HookCallback, Hookable } from 'hookable'
import { createHooks } from 'hookable'
import { getContext } from 'unctx'
Expand Down
95 changes: 90 additions & 5 deletions packages/nuxt/src/pages/module.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { existsSync, readdirSync } from 'node:fs'
import { mkdir, readFile } from 'node:fs/promises'
import { addComponent, addPlugin, addTemplate, addVitePlugin, addWebpackPlugin, defineNuxtModule, findPath, updateTemplates } from '@nuxt/kit'
import { join, relative, resolve } from 'pathe'
import { dirname, join, relative, resolve } from 'pathe'
import { genImport, genObjectFromRawEntries, genString } from 'knitwork'
import escapeRE from 'escape-string-regexp'
import { joinURL } from 'ufo'
import type { NuxtApp, NuxtPage } from 'nuxt/schema'
import { createRoutesContext } from 'unplugin-vue-router'
import { resolveOptions } from 'unplugin-vue-router/options'
import type { EditableTreeNode, Options as TypedRouterOptions } from 'unplugin-vue-router'

import { distDir } from '../dirs'
import { normalizeRoutes, resolvePagesRoutes } from './utils'
Expand All @@ -15,7 +19,9 @@ export default defineNuxtModule({
meta: {
name: 'pages'
},
setup (_options, nuxt) {
async setup (_options, nuxt) {
const useExperimentalTypedPages = nuxt.options.experimental.typedPages

const pagesDirs = nuxt.options._layers.map(
layer => resolve(layer.config.srcDir, layer.config.dir?.pages || 'pages')
)
Expand Down Expand Up @@ -67,18 +73,82 @@ export default defineNuxtModule({
return
}

if (useExperimentalTypedPages) {
const declarationFile = './types/typed-router.d.ts'

const options: TypedRouterOptions = {
routesFolder: [],
dts: resolve(nuxt.options.buildDir, declarationFile),
logs: nuxt.options.debug,
async beforeWriteFiles (rootPage) {
rootPage.children.forEach(child => child.delete())
const pages = await resolvePagesRoutes()
await nuxt.callHook('pages:extend', pages)
function addPage (parent: EditableTreeNode, page: NuxtPage) {
// @ts-expect-error TODO: either fix types upstream or figure out another
// way to add a route without a file, which must be possible
const route = parent.insert(page.path, page.file)
if (page.meta) {
route.addToMeta(page.meta)
}
if (page.alias) {
route.addAlias(page.alias)
}
if (page.name) {
route.name = page.name
}
// TODO: implement redirect support
// if (page.redirect) {}
if (page.children) {
page.children.forEach(child => addPage(route, child))
}
}

for (const page of pages) {
addPage(rootPage, page)
}
}
}

nuxt.hook('prepare:types', ({ references }) => {
// This file will be generated by unplugin-vue-router
references.push({ path: declarationFile })
})

const context = createRoutesContext(resolveOptions(options))
const dtsFile = resolve(nuxt.options.buildDir, declarationFile)
await mkdir(dirname(dtsFile), { recursive: true })
await context.scanPages(false)

if (nuxt.options._prepare) {
// TODO: could we generate this from context instead?
const dts = await readFile(dtsFile, 'utf-8')
addTemplate({
filename: 'types/typed-router.d.ts',
getContents: () => dts
})
}

// Regenerate types/typed-router.d.ts when adding or removing pages
nuxt.hook('builder:generateApp', async (options) => {
if (!options?.filter || options.filter({ filename: 'routes.mjs' } as any)) {
await context.scanPages()
}
})
}

const runtimeDir = resolve(distDir, 'pages/runtime')

// Add $router types
nuxt.hook('prepare:types', ({ references }) => {
references.push({ types: 'vue-router' })
references.push({ types: useExperimentalTypedPages ? 'vue-router/auto' : 'vue-router' })
})

// Add vue-router route guard imports
nuxt.hook('imports:sources', (sources) => {
const routerImports = sources.find(s => s.from === '#app' && s.imports.includes('onBeforeRouteLeave'))
if (routerImports) {
routerImports.from = 'vue-router'
routerImports.from = '#vue-router'
}
})

Expand Down Expand Up @@ -144,7 +214,7 @@ export default defineNuxtModule({
nuxt.hook('imports:extend', (imports) => {
imports.push(
{ name: 'definePageMeta', as: 'definePageMeta', from: resolve(runtimeDir, 'composables') },
{ name: 'useLink', as: 'useLink', from: 'vue-router' }
{ name: 'useLink', as: 'useLink', from: '#vue-router' }
)
})

Expand Down Expand Up @@ -177,6 +247,7 @@ export default defineNuxtModule({
await nuxt.callHook('pages:extend', pages)

const sourceFiles = getSources(pages)

for (const key in manifest) {
if (manifest[key].isEntry) {
manifest[key].dynamicImports =
Expand All @@ -185,6 +256,17 @@ export default defineNuxtModule({
}
})

// adds support for #vue-router alias
addTemplate({
filename: 'vue-router.mjs',
// TODO: use `vue-router/auto` when we have support for page metadata
getContents: () => 'export * from \'vue-router\';'
})
addTemplate({
filename: 'vue-router.d.ts',
getContents: () => `export * from '${useExperimentalTypedPages ? 'vue-router/auto' : 'vue-router'}'`
})

// Add routes template
addTemplate({
filename: 'routes.mjs',
Expand Down Expand Up @@ -278,10 +360,13 @@ export default defineNuxtModule({
filePath: resolve(distDir, 'pages/runtime/page')
})

nuxt.options.alias['#vue-router'] = join(nuxt.options.buildDir, 'vue-router')

// Add declarations for middleware keys
nuxt.hook('prepare:types', ({ references }) => {
references.push({ path: resolve(nuxt.options.buildDir, 'types/middleware.d.ts') })
references.push({ path: resolve(nuxt.options.buildDir, 'types/layouts.d.ts') })
references.push({ path: resolve(nuxt.options.buildDir, 'vue-router.d.ts') })
})
}
})
2 changes: 1 addition & 1 deletion packages/nuxt/src/pages/runtime/composables.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { KeepAliveProps, TransitionProps, UnwrapRef } from 'vue'
import { getCurrentInstance } from 'vue'
import type { RouteLocationNormalized, RouteLocationNormalizedLoaded, RouteRecordRedirectOption } from 'vue-router'
import type { RouteLocationNormalized, RouteLocationNormalizedLoaded, RouteRecordRedirectOption } from '#vue-router'
import { useRoute } from 'vue-router'
import type { NuxtError } from '#app'

Expand Down
4 changes: 2 additions & 2 deletions packages/nuxt/src/pages/runtime/page.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Suspense, Transition, computed, defineComponent, h, nextTick, onMounted, provide, reactive } from 'vue'
import type { KeepAliveProps, TransitionProps, VNode } from 'vue'
import { RouterView } from 'vue-router'
import { RouterView } from '#vue-router'
import { defu } from 'defu'
import type { RouteLocation, RouteLocationNormalized, RouteLocationNormalizedLoaded } from 'vue-router'
import type { RouteLocation, RouteLocationNormalized, RouteLocationNormalizedLoaded } from '#vue-router'

import type { RouterViewSlotProps } from './utils'
import { generateRouteKey, wrapInKeepAlive } from './utils'
Expand Down
10 changes: 6 additions & 4 deletions packages/nuxt/src/pages/runtime/plugins/router.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { computed, isReadonly, reactive, shallowRef } from 'vue'
import type { Ref } from 'vue'
import type { RouteLocation, Router } from 'vue-router'
import type { RouteLocation, Router } from '#vue-router'
import {
createMemoryHistory,
createRouter,
createWebHashHistory,
createWebHistory
} from 'vue-router'
} from '#vue-router'
import { createError } from 'h3'
import { withoutBase } from 'ufo'

Expand Down Expand Up @@ -44,7 +44,7 @@ function createCurrentLocation (
return path + search + hash
}

export default defineNuxtPlugin({
const plugin: Plugin<{ router: Router }> = defineNuxtPlugin({
name: 'nuxt:router',
enforce: 'pre',
async setup (nuxtApp) {
Expand Down Expand Up @@ -201,4 +201,6 @@ export default defineNuxtPlugin({

return { provide: { router } }
}
}) as Plugin<{ router: Router }>
})

export default plugin
2 changes: 1 addition & 1 deletion packages/nuxt/src/pages/runtime/router.options.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { RouteLocationNormalized, RouterScrollBehavior } from 'vue-router'
import type { RouteLocationNormalized, RouterScrollBehavior } from '#vue-router'
import { nextTick } from 'vue'
import type { RouterConfig } from 'nuxt/schema'
import { useNuxtApp } from '#app/nuxt'
Expand Down
2 changes: 1 addition & 1 deletion packages/nuxt/src/pages/runtime/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { KeepAlive, h } from 'vue'
import type { RouteLocationMatched, RouteLocationNormalizedLoaded, RouterView } from 'vue-router'
import type { RouteLocationMatched, RouteLocationNormalizedLoaded, RouterView } from '#vue-router'

type InstanceOf<T> = T extends new (...args: any[]) => infer R ? R : never
type RouterViewSlot = Exclude<InstanceOf<typeof RouterView>['$slots']['default'], undefined>
Expand Down
3 changes: 3 additions & 0 deletions packages/schema/src/config/experimental.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,9 @@ export default defineUntypedSchema({
/** Resolve `~`, `~~`, `@` and `@@` aliases located within layers with respect to their layer source and root directories. */
localLayerAliases: true,

/** Enable the new experimental typed router using [unplugin-vue-router](https://github.com/posva/unplugin-vue-router). */
typedPages: false,

/**
* Set an alternative watcher that will be used as the watching service for Nuxt.
*
Expand Down
Loading

0 comments on commit 5781cf1

Please sign in to comment.