From dffcc613462e8165c7c676f40a7da6d5554d1e8b Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Tue, 3 Jan 2023 14:36:06 +0100 Subject: [PATCH] fix: read name and path from definePage Fix #74 --- playground/src/pages/[name].vue | 23 +------- src/core/context.ts | 36 +++++++------ src/core/definePage.spec.ts | 37 ++++++++++++- src/core/definePage.ts | 93 ++++++++++++++++++++++++++++++++- 4 files changed, 149 insertions(+), 40 deletions(-) diff --git a/playground/src/pages/[name].vue b/playground/src/pages/[name].vue index a66aa0e2c..85d3c4b4b 100644 --- a/playground/src/pages/[name].vue +++ b/playground/src/pages/[name].vue @@ -84,29 +84,8 @@ useRoute<'/[name]'>().params.name // @ts-expect-error: /about doesn't have params useRoute<'/about'>('/about').params.never -function defineRoute>( - routeModifier: (route: RouteRecordRaw) => T -): T -function defineRoute>(route: T): T -function defineRoute>( - route: T | ((route: RouteRecordRaw) => T) -): T { - return {} as T -} - -defineRoute({ - path: '/:name(\\d+)', - name: 'my-name', -}) -defineRoute((route) => ({ - ...route, - children: [ - ...(route.children ? route.children : []), - { path: '/cosa', name: 'cosa', component: {} }, - ], -})) - definePage({ + // name: 'my-name', alias: ['/n/:name'], }) diff --git a/src/core/context.ts b/src/core/context.ts index e70f6fdb2..1bff36787 100644 --- a/src/core/context.ts +++ b/src/core/context.ts @@ -13,7 +13,7 @@ import { RoutesFolderWatcher, HandlerContext } from './RoutesFolderWatcher' import { generateDTS as _generateDTS } from '../codegen/generateDTS' import { generateVueRouterProxy as _generateVueRouterProxy } from '../codegen/vueRouterModule' import { hasNamedExports } from '../data-fetching/parse' -import { definePageTransform } from './definePage' +import { definePageTransform, extractDefinePageNameAndPath } from './definePage' export function createRoutesContext(options: ResolvedOptions) { const { dts: preferDTS, root, routesFolder } = options @@ -85,24 +85,33 @@ export function createRoutesContext(options: ResolvedOptions) { await _writeConfigFiles() } + async function writeRouteInfoToNode(node: TreeNode, path: string) { + const content = await fs.readFile(path, 'utf8') + // TODO: cache the result of parsing the SFC so the transform can reuse the parsing + node.hasDefinePage = content.includes('definePage') + const [definedPageNameAndPath, routeBlock] = await Promise.all([ + extractDefinePageNameAndPath(content, path), + getRouteBlock(path, options), + ]) + // TODO: should warn if hasDefinePage and customRouteBlock + // if (routeBlock) log(routeBlock) + node.setCustomRouteBlock(path, { ...routeBlock, ...definedPageNameAndPath }) + node.value.includeLoaderGuard = + options.dataFetching && (await hasNamedExports(path)) + } + async function addPage({ filePath: path, routePath }: HandlerContext) { - const routeBlock = await getRouteBlock(path, options) log(`added "${routePath}" for "${path}"`) - if (routeBlock) log(routeBlock) // TODO: handle top level named view HMR const node = routeTree.insert( routePath, // './' + path resolve(root, path) ) - node.setCustomRouteBlock(path, routeBlock) - node.value.includeLoaderGuard = - options.dataFetching && (await hasNamedExports(path)) + + await writeRouteInfoToNode(node, path) routeMap.set(path, node) - // FIXME: do once - const content = await fs.readFile(path, 'utf8') - node.hasDefinePage = content.includes('definePage') } async function updatePage({ filePath: path, routePath }: HandlerContext) { @@ -112,12 +121,7 @@ export function createRoutesContext(options: ResolvedOptions) { console.warn(`Cannot update "${path}": Not found.`) return } - // FIXME: do once - const content = await fs.readFile(path, 'utf8') - node.hasDefinePage = content.includes('definePage') - node.setCustomRouteBlock(path, await getRouteBlock(path, options)) - node.value.includeLoaderGuard = - options.dataFetching && (await hasNamedExports(path)) + writeRouteInfoToNode(node, path) } function removePage({ filePath: path, routePath }: HandlerContext) { @@ -193,7 +197,7 @@ ${routesExport} let lastDTS: string | undefined async function _writeConfigFiles() { - log('writing') + log('💾 writing...') logTree(routeTree, log) if (dts) { const content = generateDTS() diff --git a/src/core/definePage.spec.ts b/src/core/definePage.spec.ts index 5bbf8638a..8b17fa039 100644 --- a/src/core/definePage.spec.ts +++ b/src/core/definePage.spec.ts @@ -1,5 +1,5 @@ import { expect, describe, it } from 'vitest' -import { definePageTransform } from './definePage' +import { definePageTransform, extractDefinePageNameAndPath } from './definePage' describe('definePage', () => { it('removes definePage', async () => { @@ -29,4 +29,39 @@ const b = 1 " `) }) + + it('extracts name and path', async () => { + expect( + await extractDefinePageNameAndPath( + ` + + `, + 'src/pages/basic.vue' + ) + ).toEqual({ + name: 'custom', + path: '/custom', + }) + }) + + it('extract name skipped when non existent', async () => { + expect( + await extractDefinePageNameAndPath( + ` + + `, + 'src/pages/basic.vue' + ) + ).toEqual(undefined) + }) }) diff --git a/src/core/definePage.ts b/src/core/definePage.ts index 98d4dfc5b..2f826698f 100644 --- a/src/core/definePage.ts +++ b/src/core/definePage.ts @@ -6,11 +6,22 @@ import { checkInvalidScopeReference, } from '@vue-macros/common' import { Thenable, TransformResult } from 'unplugin' -import type { CallExpression, Node, Statement } from '@babel/types' +import type { + CallExpression, + Node, + ObjectProperty, + Statement, + StringLiteral, +} from '@babel/types' import { walkAST } from 'ast-walker-scope' +import { CustomRouteBlock } from './customBlock' const MACRO_DEFINE_PAGE = 'definePage' +function isStringLiteral(node: Node | null | undefined): node is StringLiteral { + return node?.type === 'StringLiteral' +} + export function definePageTransform({ code, id, @@ -84,6 +95,86 @@ export function definePageTransform({ } } +export function extractDefinePageNameAndPath( + sfcCode: string, + id: string +): { name?: string; path?: string } | null | undefined { + if (!sfcCode.includes(MACRO_DEFINE_PAGE)) return + + const sfc = parseSFC(sfcCode, id) + + if (!sfc.scriptSetup) return + + const { script, scriptSetup, scriptCompiled } = sfc + + const definePageNodes = (scriptCompiled.scriptSetupAst as Node[]) + .map((node) => { + if (node.type === 'ExpressionStatement') node = node.expression + return isCallOf(node, MACRO_DEFINE_PAGE) ? node : null + }) + .filter((node): node is CallExpression => !!node) + + if (!definePageNodes.length) { + return + } else if (definePageNodes.length > 1) { + throw new SyntaxError(`duplicate definePage() call`) + } + + const definePageNode = definePageNodes[0] + const setupOffset = scriptSetup.loc.start.offset + + const routeRecord = definePageNode.arguments[0] + if (routeRecord.type !== 'ObjectExpression') { + throw new SyntaxError( + `[${id}]: definePage() expects an object expression as its only argument` + ) + } + + const routeInfo: Pick = {} + + for (const prop of routeRecord.properties) { + if (prop.type === 'ObjectProperty' && prop.key.type === 'Identifier') { + if (prop.key.name === 'name') { + if (prop.value.type !== 'StringLiteral') { + console.warn( + `[unplugin-vue-router]: route name must be a string literal. Found in "${id}".` + ) + } else { + routeInfo.name = prop.value.value + } + } else if (prop.key.name === 'path') { + if (prop.value.type !== 'StringLiteral') { + console.warn( + `[unplugin-vue-router]: route path must be a string literal. Found in "${id}".` + ) + } else { + routeInfo.path = prop.value.value + } + } + } + } + + return routeInfo +} + +function extractRouteAlias( + aliasValue: ObjectProperty['value'], + id: string +): string[] | undefined { + if ( + aliasValue.type !== 'StringLiteral' && + aliasValue.type !== 'ArrayExpression' + ) { + console.warn( + `[unplugin-vue-router]: route alias must be a string literal. Found in "${id}".` + ) + } else { + return aliasValue.type === 'StringLiteral' + ? [aliasValue.value] + : aliasValue.elements.filter(isStringLiteral).map((el) => el.value) + } +} + const getIdentifiers = (stmts: Statement[]) => { let ids: string[] = [] walkAST(