From a113a2d765aac884ca5a11adb4cba644d1515cc1 Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Tue, 18 Jun 2024 12:12:04 +0200 Subject: [PATCH] feat: allow imported values in definePage Close #317 --- playground/src/pages/[name].vue | 18 ++- playground/src/utils.ts | 7 ++ .../__snapshots__/definePage.spec.ts.snap | 45 +++++++ src/core/definePage.spec.ts | 109 +++++++++++++++- src/core/definePage.ts | 117 ++++++++++++++++-- 5 files changed, 283 insertions(+), 13 deletions(-) diff --git a/playground/src/pages/[name].vue b/playground/src/pages/[name].vue index 5ebe33086..da675bf33 100644 --- a/playground/src/pages/[name].vue +++ b/playground/src/pages/[name].vue @@ -42,6 +42,8 @@ export default {} +`, + id: 'src/pages/with-imports.vue&definePage&vue&lang.ts', + })) as Exclude + expect(result).toHaveProperty('code') + expect(result?.code).toMatchSnapshot() + }) + + it('keeps used default imports', async () => { + const result = (await definePageTransform({ + code: ` + +`, + id: 'src/pages/with-imports.vue&definePage&vue&lang.ts', + })) as Exclude + expect(result).toHaveProperty('code') + expect(result?.code).toMatchSnapshot() + }) + + it('removes default import if not used', async () => { + const result = (await definePageTransform({ + code: ` + +`, + id: 'src/pages/with-imports.vue&definePage&vue&lang.ts', + })) as Exclude + expect(result).toHaveProperty('code') + expect(result?.code).toMatchSnapshot() + }) + + it('works with star imports', async () => { + const result = (await definePageTransform({ + code: ` + +`, + id: 'src/pages/with-imports.vue&definePage&vue&lang.ts', + })) as Exclude + expect(result).toHaveProperty('code') + expect(result?.code).toMatchSnapshot() + }) + + it('removes star imports if not used', async () => { + const result = (await definePageTransform({ + code: ` + +`, + id: 'src/pages/with-imports.vue&definePage&vue&lang.ts', + })) as Exclude + expect(result).toHaveProperty('code') + expect(result?.code).toMatchSnapshot() + }) + + it('works when combining named and default imports', async () => { + const result = (await definePageTransform({ + code: ` + +`, + id: 'src/pages/with-imports.vue&definePage&vue&lang.ts', + })) as Exclude + expect(result).toHaveProperty('code') + expect(result?.code).toMatchSnapshot() + }) + }) + it.todo('works with jsx', async () => { const code = ` const a = 1 @@ -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: `\ diff --git a/src/core/definePage.ts b/src/core/definePage.ts index 80ac25157..4612fb2c2 100644 --- a/src/core/definePage.ts +++ b/src/core/definePage.ts @@ -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, @@ -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/ @@ -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() + const localIds = new Set() + + 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) { + 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) { + 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) @@ -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 +) { + 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}'` +}