Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(nuxt): add experimental typedPages option #20367

Merged
merged 61 commits into from May 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
f1afae9
feat: add experimental typedPages option
posva Apr 12, 2023
316a288
refactor: contain changes within pages
posva Apr 12, 2023
6289223
feat: allow vite plugins to be prepended
posva Apr 12, 2023
2f54325
chore: add temporary playground to test
posva Apr 12, 2023
6ef6fd3
style: lint
posva Apr 12, 2023
bd79b91
refactor: add router plugin
posva Apr 12, 2023
2622b06
feat: add types for unplugin-vue-router
posva Apr 12, 2023
4d8c1e1
wip: editable page
posva Apr 12, 2023
6aa00e0
refactor: add #build/vue-router
posva Apr 18, 2023
792e1ab
refactor: fix types
posva Apr 18, 2023
d2a0ff2
more shared
posva Apr 18, 2023
e2cf3c1
chore: use npm version
posva Apr 18, 2023
3e03638
refactor: use custom `#vue-router` alias
danielroe Apr 19, 2023
ada04ed
refactor: merge back into single plugin
danielroe Apr 19, 2023
20aae8d
Merge remote-tracking branch 'origin/main' into pr/posva/20367
danielroe Apr 19, 2023
325a818
fix: write types when preparing
danielroe Apr 19, 2023
f38cace
feat: add webpack plugin
danielroe Apr 19, 2023
c32738f
fix: use buildDir
danielroe Apr 19, 2023
a0e5574
refactor: use `RouterLinkProps` to benefit from typings (later)
danielroe Apr 19, 2023
1591bb1
chore: update lockfile
danielroe Apr 19, 2023
51db0dc
style: lint
danielroe Apr 19, 2023
2af7b9a
style: reduce visible diff
danielroe Apr 19, 2023
5484cd8
[autofix.ci] apply automated fixes
autofix-ci[bot] Apr 19, 2023
e22dd02
fix: add todo note
danielroe Apr 19, 2023
ab80137
[autofix.ci] apply automated fixes
autofix-ci[bot] Apr 19, 2023
2204242
Merge remote-tracking branch 'origin/main' into feat/unplugin-vue-router
danielroe Apr 19, 2023
2e34a3b
fix(nuxt): use different syntax to keep type in build
danielroe Apr 19, 2023
dc34ed9
chore: use minor constraint
danielroe Apr 19, 2023
2c5b6c4
chore: minimise diff
danielroe Apr 19, 2023
ebbe880
refactor: move templates into module
danielroe Apr 19, 2023
21f5376
refactor: move to types folder
danielroe Apr 19, 2023
9708b7c
refactor: more small changes
danielroe Apr 19, 2023
6df21bf
Merge remote-tracking branch 'origin/main' into feat/unplugin-vue-router
danielroe Apr 26, 2023
5c164a5
test: add type fixture
danielroe Apr 26, 2023
4c19b4b
chore: update lockfile
danielroe Apr 26, 2023
d19ddf2
refactor: use native nuxt + `pages:extend` rather than unplugin hooks
danielroe Apr 26, 2023
edfdad4
chore: ignore type error
danielroe Apr 26, 2023
35fbd6b
chore: add `page.redirect`
danielroe Apr 26, 2023
25830a2
Update packages/kit/src/build.ts [skip ci]
posva May 1, 2023
8c64788
chore: update unplugin version
danielroe May 1, 2023
88dfc3b
Merge remote-tracking branch 'origin/main' into feat/unplugin-vue-router
danielroe May 1, 2023
2dbee98
fix: remove customisations to kit utils
danielroe May 1, 2023
8d6fdf9
fix: workaround for dts generation time
danielroe May 1, 2023
a837c24
fix: create directory first
danielroe May 1, 2023
a78b797
chore: bump unplugin version
danielroe May 2, 2023
d9ac7ca
Merge remote-tracking branch 'origin/main' into feat/unplugin-vue-router
danielroe May 2, 2023
9350e93
fix: use unplugin just for types, not for router
danielroe May 2, 2023
10ea7e7
fix: use `RouteLocationRaw` directly for nuxtlink props
danielroe May 2, 2023
d31b7b7
test: test for invalid params, not missing ones
danielroe May 2, 2023
0a97086
test: add typed usage of useRoute throughout suite
danielroe May 2, 2023
c4111c2
chore: restore playground
danielroe May 2, 2023
1c2f263
chore: remove comment
danielroe May 2, 2023
ed4e2a6
fix(nuxt): remove unplugin and handle hmr in nuxt
danielroe May 2, 2023
6e79f81
fix: disable logs by default
danielroe May 2, 2023
b851b4f
Merge remote-tracking branch 'origin/main' into feat/unplugin-vue-router
danielroe May 2, 2023
efbf166
Merge remote-tracking branch 'origin/main' into feat/unplugin-vue-router
danielroe May 5, 2023
f2689b9
test: update test import
danielroe May 5, 2023
02e7cb8
Merge remote-tracking branch 'origin/main' into feat/unplugin-vue-router
danielroe May 9, 2023
dd13ca4
fix: update reference
danielroe May 9, 2023
9976c62
chore: update version
danielroe May 9, 2023
7258433
Merge branch 'main' into feat/unplugin-vue-router
danielroe May 9, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions .eslintrc
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
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
@@ -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
Comment on lines 85 to 86
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW are these intentional? It should be the exact same without them

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's get rid of them then πŸ˜†

},
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
@@ -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
@@ -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
@@ -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
@@ -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
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I will need to add this before it gets merged

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will it make a difference to the types? Because at the moment we are not using the unplugin for actual routes generation (until it can support route metadata).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you are right, it doesn't make any difference in terms of types

// 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))
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to take a look, I think the options should be resolved internally anyway πŸ€”

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?
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could add a method to expose the generation of the dts content. I imagine that having an option not to write to the filesystem could also be helpful in this case. Let me know what you think

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be perfect πŸ‘Œ

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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
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