Skip to content

Commit a113a2d

Browse files
committed
feat: allow imported values in definePage
Close #317
1 parent 7a57597 commit a113a2d

File tree

5 files changed

+283
-13
lines changed

5 files changed

+283
-13
lines changed

playground/src/pages/[name].vue

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ export default {}
4242
</script>
4343

4444
<script lang="ts" setup>
45+
import { dummy, dummy_id, dummy_number } from '@/utils'
46+
import * as dummy_star from '@/utils'
4547
import {
4648
onBeforeRouteLeave,
4749
onBeforeRouteUpdate,
@@ -102,10 +104,22 @@ definePage({
102104
// name: 'my-name',
103105
alias: ['/n/:name'],
104106
meta: {
107+
[dummy_id]: 'id',
108+
fixed: dummy_number,
105109
// hello: 'there',
106110
mySymbol: Symbol(),
107-
test: (to: RouteLocationNormalized) =>
108-
console.log(to.name === '/[name]' ? to.params.name : 'nope'),
111+
['hello' + 'expr']: true,
112+
test: (to: RouteLocationNormalized) => {
113+
// this one should crash it
114+
// anyRoute.params
115+
const shadow = 'nope'
116+
// dummy(shadow)
117+
dummy_star
118+
if (Math.random()) {
119+
console.log(typeof dummy)
120+
}
121+
console.log(to.name === '/[name]' ? to.params.name : shadow)
122+
},
109123
},
110124
})
111125

playground/src/utils.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,10 @@ export function useParamMatcher<Name extends keyof RouteNamedMap>(
3434

3535
onUnmounted(removeGuard)
3636
}
37+
38+
export function dummy(arg: unknown) {
39+
return 'ok'
40+
}
41+
42+
export const dummy_id = 'dummy_id'
43+
export const dummy_number = 42

src/core/__snapshots__/definePage.spec.ts.snap

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,50 @@
11
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
22

3+
exports[`definePage > imports > keeps used default imports 1`] = `
4+
"import my_var from './lib'
5+
export default {
6+
meta: {
7+
[my_var]: 'hello',
8+
}
9+
}"
10+
`;
11+
12+
exports[`definePage > imports > keeps used named imports 1`] = `
13+
"import {my_var, my_func, my_num} from './lib'
14+
export default {
15+
meta: {
16+
[my_var]: 'hello',
17+
other: my_func,
18+
custom() {
19+
return my_num
20+
}
21+
}
22+
}"
23+
`;
24+
25+
exports[`definePage > imports > removes default import if not used 1`] = `"export default {name: 'ok'}"`;
26+
27+
exports[`definePage > imports > removes star imports if not used 1`] = `"export default {name: 'ok'}"`;
28+
29+
exports[`definePage > imports > works when combining named and default imports 1`] = `
30+
"import my_var, {my_func} from './lib'
31+
export default {
32+
meta: {
33+
[my_var]: 'hello',
34+
other: my_func,
35+
}
36+
}"
37+
`;
38+
39+
exports[`definePage > imports > works with star imports 1`] = `
40+
"import * as lib from './my-lib'
41+
export default {
42+
meta: {
43+
[lib.my_var]: 'hello',
44+
}
45+
}"
46+
`;
47+
348
exports[`definePage > removes definePage 1`] = `
449
"
550
<script setup>

src/core/definePage.spec.ts

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,113 @@ describe('definePage', () => {
2828
expect(result?.code).toMatchSnapshot()
2929
})
3030

31+
describe('imports', () => {
32+
it('keeps used named imports', async () => {
33+
const result = (await definePageTransform({
34+
code: `
35+
<script setup>
36+
import { my_var, not_used, my_func, my_num } from './lib'
37+
definePage({
38+
meta: {
39+
[my_var]: 'hello',
40+
other: my_func,
41+
custom() {
42+
return my_num
43+
}
44+
}
45+
})
46+
</script>
47+
`,
48+
id: 'src/pages/with-imports.vue&definePage&vue&lang.ts',
49+
})) as Exclude<TransformResult, string>
50+
expect(result).toHaveProperty('code')
51+
expect(result?.code).toMatchSnapshot()
52+
})
53+
54+
it('keeps used default imports', async () => {
55+
const result = (await definePageTransform({
56+
code: `
57+
<script setup>
58+
import my_var from './lib'
59+
definePage({
60+
meta: {
61+
[my_var]: 'hello',
62+
}
63+
})
64+
</script>
65+
`,
66+
id: 'src/pages/with-imports.vue&definePage&vue&lang.ts',
67+
})) as Exclude<TransformResult, string>
68+
expect(result).toHaveProperty('code')
69+
expect(result?.code).toMatchSnapshot()
70+
})
71+
72+
it('removes default import if not used', async () => {
73+
const result = (await definePageTransform({
74+
code: `
75+
<script setup>
76+
import my_var from './lib'
77+
definePage({name: 'ok'})
78+
</script>
79+
`,
80+
id: 'src/pages/with-imports.vue&definePage&vue&lang.ts',
81+
})) as Exclude<TransformResult, string>
82+
expect(result).toHaveProperty('code')
83+
expect(result?.code).toMatchSnapshot()
84+
})
85+
86+
it('works with star imports', async () => {
87+
const result = (await definePageTransform({
88+
code: `
89+
<script setup>
90+
import * as lib from './my-lib'
91+
definePage({
92+
meta: {
93+
[lib.my_var]: 'hello',
94+
}
95+
})
96+
</script>
97+
`,
98+
id: 'src/pages/with-imports.vue&definePage&vue&lang.ts',
99+
})) as Exclude<TransformResult, string>
100+
expect(result).toHaveProperty('code')
101+
expect(result?.code).toMatchSnapshot()
102+
})
103+
104+
it('removes star imports if not used', async () => {
105+
const result = (await definePageTransform({
106+
code: `
107+
<script setup>
108+
import * as lib from './my-lib'
109+
definePage({name: 'ok'})
110+
</script>
111+
`,
112+
id: 'src/pages/with-imports.vue&definePage&vue&lang.ts',
113+
})) as Exclude<TransformResult, string>
114+
expect(result).toHaveProperty('code')
115+
expect(result?.code).toMatchSnapshot()
116+
})
117+
118+
it('works when combining named and default imports', async () => {
119+
const result = (await definePageTransform({
120+
code: `
121+
<script setup>
122+
import my_var, { not_used, my_func, not_used_either } from './lib'
123+
definePage({
124+
meta: {
125+
[my_var]: 'hello',
126+
other: my_func,
127+
}
128+
})
129+
</script>
130+
`,
131+
id: 'src/pages/with-imports.vue&definePage&vue&lang.ts',
132+
})) as Exclude<TransformResult, string>
133+
expect(result).toHaveProperty('code')
134+
expect(result?.code).toMatchSnapshot()
135+
})
136+
})
137+
31138
it.todo('works with jsx', async () => {
32139
const code = `
33140
const a = 1
@@ -112,7 +219,7 @@ const b = 1
112219
expect(
113220
await definePageTransform({
114221
code: sampleCode,
115-
id: 'src/pages/definePage?definePage.vue',
222+
id: 'src/pages/definePage.vue?definePage&vue',
116223
})
117224
).toMatchObject({
118225
code: `\

src/core/definePage.ts

Lines changed: 107 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
MagicString,
66
checkInvalidScopeReference,
77
} from '@vue-macros/common'
8-
import { Thenable, TransformResult } from 'unplugin'
8+
import type { Thenable, TransformResult } from 'unplugin'
99
import type {
1010
CallExpression,
1111
Node,
@@ -16,6 +16,7 @@ import type {
1616
import { walkAST } from 'ast-walker-scope'
1717
import { CustomRouteBlock } from './customBlock'
1818
import { warn } from './utils'
19+
import { ParsedStaticImport, findStaticImports, parseStaticImport } from 'mlly'
1920

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

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

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

88-
// NOTE: this doesn't seem to be any faster than using MagicString
89-
// return (
90-
// 'export default ' +
91-
// code.slice(
92-
// setupOffset + routeRecord.start!,
93-
// setupOffset + routeRecord.end!
94-
// )
95-
// )
96-
9790
s.remove(setupOffset + routeRecord.end!, code.length)
9891
s.remove(0, setupOffset + routeRecord.start!)
9992
s.prepend(`export default `)
10093

94+
// find all static imports and filter out the ones that are not used
95+
const staticImports = findStaticImports(code)
96+
97+
const usedIds = new Set<string>()
98+
const localIds = new Set<string>()
99+
100+
walkAST(routeRecord, {
101+
enter(node) {
102+
// skip literal keys from object properties
103+
if (
104+
this.parent?.type === 'ObjectProperty' &&
105+
this.parent.key === node &&
106+
// still track computed keys [a + b]: 1
107+
!this.parent.computed &&
108+
node.type === 'Identifier'
109+
) {
110+
this.skip()
111+
} else if (
112+
// filter out things like 'log' in console.log
113+
this.parent?.type === 'MemberExpression' &&
114+
this.parent.property === node &&
115+
!this.parent.computed &&
116+
node.type === 'Identifier'
117+
) {
118+
this.skip()
119+
// types are stripped off so we can skip them
120+
} else if (node.type === 'TSTypeAnnotation') {
121+
this.skip()
122+
// track everything else
123+
} else if (node.type === 'Identifier' && !localIds.has(node.name)) {
124+
usedIds.add(node.name)
125+
// track local ids that could shadow an import
126+
} else if ('scopeIds' in node && node.scopeIds instanceof Set) {
127+
// avoid adding them to the usedIds list
128+
for (const id of node.scopeIds as Set<string>) {
129+
localIds.add(id)
130+
}
131+
}
132+
},
133+
leave(node) {
134+
if ('scopeIds' in node && node.scopeIds instanceof Set) {
135+
// clear out local ids
136+
for (const id of node.scopeIds as Set<string>) {
137+
localIds.delete(id)
138+
}
139+
}
140+
},
141+
})
142+
143+
for (const imp of staticImports) {
144+
const importCode = generateFilteredImportStatement(
145+
parseStaticImport(imp),
146+
usedIds
147+
)
148+
if (importCode) {
149+
s.prepend(importCode + '\n')
150+
}
151+
}
152+
101153
return generateTransform(s, id)
102154
} else {
103155
// console.log('!!!', definePageNode)
@@ -219,3 +271,48 @@ const getIdentifiers = (stmts: Statement[]) => {
219271

220272
return ids
221273
}
274+
275+
/**
276+
* Generate a filtere import statement based on a set of identifiers that should be kept.
277+
*
278+
* @param parsedImports - parsed imports with mlly
279+
* @param usedIds - set of used identifiers
280+
* @returns `null` if no import statement should be generated, otherwise the import statement as a string without a newline
281+
*/
282+
function generateFilteredImportStatement(
283+
parsedImports: ParsedStaticImport,
284+
usedIds: Set<string>
285+
) {
286+
if (!parsedImports || usedIds.size < 1) return null
287+
288+
const { namedImports, defaultImport, namespacedImport } = parsedImports
289+
290+
if (namespacedImport && usedIds.has(namespacedImport)) {
291+
return `import * as ${namespacedImport} from '${parsedImports.specifier}'`
292+
}
293+
294+
let importListCode = ''
295+
if (defaultImport && usedIds.has(defaultImport)) {
296+
importListCode += defaultImport
297+
}
298+
299+
let namedImportListCode = ''
300+
for (const importName in namedImports) {
301+
if (usedIds.has(importName)) {
302+
// add comma if we have more than one named import
303+
namedImportListCode += namedImportListCode ? `, ` : ''
304+
305+
namedImportListCode +=
306+
importName === namedImports[importName]
307+
? importName
308+
: `${importName} as ${namedImports[importName]}`
309+
}
310+
}
311+
312+
importListCode += importListCode && namedImportListCode ? ', ' : ''
313+
importListCode += namedImportListCode ? `{${namedImportListCode}}` : ''
314+
315+
if (!importListCode) return null
316+
317+
return `import ${importListCode} from '${parsedImports.specifier}'`
318+
}

0 commit comments

Comments
 (0)