Skip to content

Commit

Permalink
feat: allow imported values in definePage
Browse files Browse the repository at this point in the history
Close #317
  • Loading branch information
posva committed Jun 18, 2024
1 parent 7a57597 commit a113a2d
Show file tree
Hide file tree
Showing 5 changed files with 283 additions and 13 deletions.
18 changes: 16 additions & 2 deletions playground/src/pages/[name].vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export default {}
</script>

<script lang="ts" setup>
import { dummy, dummy_id, dummy_number } from '@/utils'
import * as dummy_star from '@/utils'
import {
onBeforeRouteLeave,
onBeforeRouteUpdate,
Expand Down Expand Up @@ -102,10 +104,22 @@ definePage({
// name: 'my-name',
alias: ['/n/:name'],
meta: {
[dummy_id]: 'id',
fixed: dummy_number,
// hello: 'there',
mySymbol: Symbol(),
test: (to: RouteLocationNormalized) =>
console.log(to.name === '/[name]' ? to.params.name : 'nope'),
['hello' + 'expr']: true,
test: (to: RouteLocationNormalized) => {
// this one should crash it
// anyRoute.params
const shadow = 'nope'
// dummy(shadow)
dummy_star
if (Math.random()) {
console.log(typeof dummy)
}
console.log(to.name === '/[name]' ? to.params.name : shadow)
},
},
})
Expand Down
7 changes: 7 additions & 0 deletions playground/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,10 @@ export function useParamMatcher<Name extends keyof RouteNamedMap>(

onUnmounted(removeGuard)
}

export function dummy(arg: unknown) {
return 'ok'
}

export const dummy_id = 'dummy_id'
export const dummy_number = 42
45 changes: 45 additions & 0 deletions src/core/__snapshots__/definePage.spec.ts.snap
Original file line number Diff line number Diff line change
@@ -1,5 +1,50 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`definePage > imports > keeps used default imports 1`] = `
"import my_var from './lib'
export default {
meta: {
[my_var]: 'hello',
}
}"
`;

exports[`definePage > imports > keeps used named imports 1`] = `
"import {my_var, my_func, my_num} from './lib'
export default {
meta: {
[my_var]: 'hello',
other: my_func,
custom() {
return my_num
}
}
}"
`;

exports[`definePage > imports > removes default import if not used 1`] = `"export default {name: 'ok'}"`;

exports[`definePage > imports > removes star imports if not used 1`] = `"export default {name: 'ok'}"`;

exports[`definePage > imports > works when combining named and default imports 1`] = `
"import my_var, {my_func} from './lib'
export default {
meta: {
[my_var]: 'hello',
other: my_func,
}
}"
`;

exports[`definePage > imports > works with star imports 1`] = `
"import * as lib from './my-lib'
export default {
meta: {
[lib.my_var]: 'hello',
}
}"
`;

exports[`definePage > removes definePage 1`] = `
"
<script setup>
Expand Down
109 changes: 108 additions & 1 deletion src/core/definePage.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,113 @@ describe('definePage', () => {
expect(result?.code).toMatchSnapshot()
})

describe('imports', () => {
it('keeps used named imports', async () => {
const result = (await definePageTransform({
code: `
<script setup>
import { my_var, not_used, my_func, my_num } from './lib'
definePage({
meta: {
[my_var]: 'hello',
other: my_func,
custom() {
return my_num
}
}
})
</script>
`,
id: 'src/pages/with-imports.vue&definePage&vue&lang.ts',
})) as Exclude<TransformResult, string>
expect(result).toHaveProperty('code')
expect(result?.code).toMatchSnapshot()
})

it('keeps used default imports', async () => {
const result = (await definePageTransform({
code: `
<script setup>
import my_var from './lib'
definePage({
meta: {
[my_var]: 'hello',
}
})
</script>
`,
id: 'src/pages/with-imports.vue&definePage&vue&lang.ts',
})) as Exclude<TransformResult, string>
expect(result).toHaveProperty('code')
expect(result?.code).toMatchSnapshot()
})

it('removes default import if not used', async () => {
const result = (await definePageTransform({
code: `
<script setup>
import my_var from './lib'
definePage({name: 'ok'})
</script>
`,
id: 'src/pages/with-imports.vue&definePage&vue&lang.ts',
})) as Exclude<TransformResult, string>
expect(result).toHaveProperty('code')
expect(result?.code).toMatchSnapshot()
})

it('works with star imports', async () => {
const result = (await definePageTransform({
code: `
<script setup>
import * as lib from './my-lib'
definePage({
meta: {
[lib.my_var]: 'hello',
}
})
</script>
`,
id: 'src/pages/with-imports.vue&definePage&vue&lang.ts',
})) as Exclude<TransformResult, string>
expect(result).toHaveProperty('code')
expect(result?.code).toMatchSnapshot()
})

it('removes star imports if not used', async () => {
const result = (await definePageTransform({
code: `
<script setup>
import * as lib from './my-lib'
definePage({name: 'ok'})
</script>
`,
id: 'src/pages/with-imports.vue&definePage&vue&lang.ts',
})) as Exclude<TransformResult, string>
expect(result).toHaveProperty('code')
expect(result?.code).toMatchSnapshot()
})

it('works when combining named and default imports', async () => {
const result = (await definePageTransform({
code: `
<script setup>
import my_var, { not_used, my_func, not_used_either } from './lib'
definePage({
meta: {
[my_var]: 'hello',
other: my_func,
}
})
</script>
`,
id: 'src/pages/with-imports.vue&definePage&vue&lang.ts',
})) as Exclude<TransformResult, string>
expect(result).toHaveProperty('code')
expect(result?.code).toMatchSnapshot()
})
})

it.todo('works with jsx', async () => {
const code = `
const a = 1
Expand Down Expand Up @@ -112,7 +219,7 @@ const b = 1
expect(
await definePageTransform({
code: sampleCode,
id: 'src/pages/definePage?definePage.vue',
id: 'src/pages/definePage.vue?definePage&vue',
})
).toMatchObject({
code: `\
Expand Down
117 changes: 107 additions & 10 deletions src/core/definePage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
MagicString,
checkInvalidScopeReference,
} from '@vue-macros/common'
import { Thenable, TransformResult } from 'unplugin'
import type { Thenable, TransformResult } from 'unplugin'
import type {
CallExpression,
Node,
Expand All @@ -16,6 +16,7 @@ import type {
import { walkAST } from 'ast-walker-scope'
import { CustomRouteBlock } from './customBlock'
import { warn } from './utils'
import { ParsedStaticImport, findStaticImports, parseStaticImport } from 'mlly'

const MACRO_DEFINE_PAGE = 'definePage'
const MACRO_DEFINE_PAGE_QUERY = /[?&]definePage\b/
Expand Down Expand Up @@ -83,21 +84,72 @@ export function definePageTransform({

const scriptBindings = setupAst?.body ? getIdentifiers(setupAst.body) : []

// this will throw if a property from the script setup is used in definePage
checkInvalidScopeReference(routeRecord, MACRO_DEFINE_PAGE, scriptBindings)

// NOTE: this doesn't seem to be any faster than using MagicString
// return (
// 'export default ' +
// code.slice(
// setupOffset + routeRecord.start!,
// setupOffset + routeRecord.end!
// )
// )

s.remove(setupOffset + routeRecord.end!, code.length)
s.remove(0, setupOffset + routeRecord.start!)
s.prepend(`export default `)

// find all static imports and filter out the ones that are not used
const staticImports = findStaticImports(code)

const usedIds = new Set<string>()
const localIds = new Set<string>()

walkAST(routeRecord, {
enter(node) {
// skip literal keys from object properties
if (
this.parent?.type === 'ObjectProperty' &&
this.parent.key === node &&
// still track computed keys [a + b]: 1
!this.parent.computed &&
node.type === 'Identifier'
) {
this.skip()
} else if (
// filter out things like 'log' in console.log
this.parent?.type === 'MemberExpression' &&
this.parent.property === node &&
!this.parent.computed &&
node.type === 'Identifier'
) {
this.skip()
// types are stripped off so we can skip them
} else if (node.type === 'TSTypeAnnotation') {
this.skip()
// track everything else
} else if (node.type === 'Identifier' && !localIds.has(node.name)) {
usedIds.add(node.name)
// track local ids that could shadow an import
} else if ('scopeIds' in node && node.scopeIds instanceof Set) {
// avoid adding them to the usedIds list
for (const id of node.scopeIds as Set<string>) {
localIds.add(id)
}
}
},
leave(node) {
if ('scopeIds' in node && node.scopeIds instanceof Set) {
// clear out local ids
for (const id of node.scopeIds as Set<string>) {
localIds.delete(id)
}
}
},
})

for (const imp of staticImports) {
const importCode = generateFilteredImportStatement(
parseStaticImport(imp),
usedIds
)
if (importCode) {
s.prepend(importCode + '\n')
}
}

return generateTransform(s, id)
} else {
// console.log('!!!', definePageNode)
Expand Down Expand Up @@ -219,3 +271,48 @@ const getIdentifiers = (stmts: Statement[]) => {

return ids
}

/**
* Generate a filtere import statement based on a set of identifiers that should be kept.
*
* @param parsedImports - parsed imports with mlly
* @param usedIds - set of used identifiers
* @returns `null` if no import statement should be generated, otherwise the import statement as a string without a newline
*/
function generateFilteredImportStatement(
parsedImports: ParsedStaticImport,
usedIds: Set<string>
) {
if (!parsedImports || usedIds.size < 1) return null

const { namedImports, defaultImport, namespacedImport } = parsedImports

if (namespacedImport && usedIds.has(namespacedImport)) {
return `import * as ${namespacedImport} from '${parsedImports.specifier}'`
}

let importListCode = ''
if (defaultImport && usedIds.has(defaultImport)) {
importListCode += defaultImport
}

let namedImportListCode = ''
for (const importName in namedImports) {
if (usedIds.has(importName)) {
// add comma if we have more than one named import
namedImportListCode += namedImportListCode ? `, ` : ''

namedImportListCode +=
importName === namedImports[importName]
? importName
: `${importName} as ${namedImports[importName]}`
}
}

importListCode += importListCode && namedImportListCode ? ', ' : ''
importListCode += namedImportListCode ? `{${namedImportListCode}}` : ''

if (!importListCode) return null

return `import ${importListCode} from '${parsedImports.specifier}'`
}

0 comments on commit a113a2d

Please sign in to comment.