Skip to content

Commit

Permalink
fix: read name and path from definePage
Browse files Browse the repository at this point in the history
Fix #74
  • Loading branch information
posva committed Jan 3, 2023
1 parent 6838eb1 commit dffcc61
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 40 deletions.
23 changes: 1 addition & 22 deletions playground/src/pages/[name].vue
Expand Up @@ -84,29 +84,8 @@ useRoute<'/[name]'>().params.name
// @ts-expect-error: /about doesn't have params
useRoute<'/about'>('/about').params.never
function defineRoute<T extends Partial<RouteRecordRaw>>(
routeModifier: (route: RouteRecordRaw) => T
): T
function defineRoute<T extends Partial<RouteRecordRaw>>(route: T): T
function defineRoute<T extends Partial<RouteRecordRaw>>(
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'],
})
Expand Down
36 changes: 20 additions & 16 deletions src/core/context.ts
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -193,7 +197,7 @@ ${routesExport}

let lastDTS: string | undefined
async function _writeConfigFiles() {
log('writing')
log('💾 writing...')
logTree(routeTree, log)
if (dts) {
const content = generateDTS()
Expand Down
37 changes: 36 additions & 1 deletion 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 () => {
Expand Down Expand Up @@ -29,4 +29,39 @@ const b = 1
"
`)
})

it('extracts name and path', async () => {
expect(
await extractDefinePageNameAndPath(
`
<script setup>
const a = 1
definePage({
name: 'custom',
path: '/custom',
})
const b = 1
</script>
`,
'src/pages/basic.vue'
)
).toEqual({
name: 'custom',
path: '/custom',
})
})

it('extract name skipped when non existent', async () => {
expect(
await extractDefinePageNameAndPath(
`
<script setup>
const a = 1
const b = 1
</script>
`,
'src/pages/basic.vue'
)
).toEqual(undefined)
})
})
93 changes: 92 additions & 1 deletion src/core/definePage.ts
Expand Up @@ -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,
Expand Down Expand Up @@ -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<CustomRouteBlock, 'name' | 'path'> = {}

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(
Expand Down

0 comments on commit dffcc61

Please sign in to comment.