diff --git a/src/codegen/__snapshots__/generateRouteRecords.spec.ts.snap b/src/codegen/__snapshots__/generateRouteRecords.spec.ts.snap index 9fbc1a83..d18eca07 100644 --- a/src/codegen/__snapshots__/generateRouteRecords.spec.ts.snap +++ b/src/codegen/__snapshots__/generateRouteRecords.spec.ts.snap @@ -99,9 +99,8 @@ exports[`generateRouteRecord > generate custom imports 1`] = ` `; exports[`generateRouteRecord > generate custom imports 2`] = ` -Map { - "_page_0" => "a.vue", -} +"import _page_0 from 'a.vue' +" `; exports[`generateRouteRecord > generate static imports 1`] = ` @@ -142,11 +141,10 @@ exports[`generateRouteRecord > generate static imports 1`] = ` `; exports[`generateRouteRecord > generate static imports 2`] = ` -Map { - "_page_0" => "a.vue", - "_page_1" => "b.vue", - "_page_2" => "nested/file/c.vue", -} +"import _page_0 from 'a.vue' +import _page_1 from 'b.vue' +import _page_2 from 'nested/file/c.vue' +" `; exports[`generateRouteRecord > handles multiple named views 1`] = ` diff --git a/src/codegen/generateRouteRecords.spec.ts b/src/codegen/generateRouteRecords.spec.ts index 754da602..d4322415 100644 --- a/src/codegen/generateRouteRecords.spec.ts +++ b/src/codegen/generateRouteRecords.spec.ts @@ -3,12 +3,13 @@ import { describe, expect, it } from 'vitest' import { createPrefixTree, TreeNode } from '../core/tree' import { ResolvedOptions, resolveOptions } from '../options' import { generateRouteRecord } from './generateRouteRecords' +import { ImportsMap } from '../core/utils' const DEFAULT_OPTIONS = resolveOptions({}) describe('generateRouteRecord', () => { function generateRouteRecordSimple(tree: TreeNode) { - return generateRouteRecord(tree, DEFAULT_OPTIONS, new Map()) + return generateRouteRecord(tree, DEFAULT_OPTIONS, new ImportsMap()) } it('works with an empty tree', () => { @@ -106,10 +107,10 @@ describe('generateRouteRecord', () => { tree.insert('a.vue') tree.insert('b.vue') tree.insert('nested/file/c.vue') - const importList = new Map() + const importList = new ImportsMap() expect(generateRouteRecord(tree, options, importList)).toMatchSnapshot() - expect(importList).toMatchSnapshot() + expect(importList.toString()).toMatchSnapshot() }) it('generate custom imports', () => { @@ -123,10 +124,10 @@ describe('generateRouteRecord', () => { tree.insert('a.vue') tree.insert('b.vue') tree.insert('nested/file/c.vue') - const importList = new Map() + const importList = new ImportsMap() expect(generateRouteRecord(tree, options, importList)).toMatchSnapshot() - expect(importList).toMatchSnapshot() + expect(importList.toString()).toMatchSnapshot() }) describe('names', () => { diff --git a/src/codegen/generateRouteRecords.ts b/src/codegen/generateRouteRecords.ts index e93518bf..f454e53e 100644 --- a/src/codegen/generateRouteRecords.ts +++ b/src/codegen/generateRouteRecords.ts @@ -1,10 +1,11 @@ import type { TreeNode } from '../core/tree' +import { ImportsMap } from '../core/utils' import { ResolvedOptions, _OptionsImportMode } from '../options' export function generateRouteRecord( node: TreeNode, options: ResolvedOptions, - importList: Map, + importsMap: ImportsMap, indent = 0 ): string { // root @@ -12,7 +13,7 @@ export function generateRouteRecord( return `[ ${node .getSortedChildren() - .map((child) => generateRouteRecord(child, options, importList, indent + 1)) + .map((child) => generateRouteRecord(child, options, importsMap, indent + 1)) .join(',\n')} ]` } @@ -43,7 +44,7 @@ ${ node, indentStr, options.importMode, - importList + importsMap ) : '/* no component */' } @@ -59,7 +60,7 @@ ${overrides.props != null ? indentStr + `props: ${overrides.props},\n` : ''}${ ? `children: [ ${node .getSortedChildren() - .map((child) => generateRouteRecord(child, options, importList, indent + 2)) + .map((child) => generateRouteRecord(child, options, importsMap, indent + 2)) .join(',\n')} ${indentStr}],` : '/* no children */' @@ -69,12 +70,13 @@ ${startIndent}}` if (node.hasDefinePage) { const definePageDataList: string[] = [] for (const [name, filePath] of node.value.components) { - const pageDataImport = `_definePage_${name}_${importList.size}` + const pageDataImport = `_definePage_${name}_${importsMap.size}` definePageDataList.push(pageDataImport) - importList.set(pageDataImport, `${filePath}?definePage&vue`) + importsMap.addDefault(`${filePath}?definePage&vue`, pageDataImport) } if (definePageDataList.length) { + importsMap.add('unplugin-vue-router/runtime', '_mergeRouteRecord') return ` _mergeRouteRecord( ${routeRecord}, ${definePageDataList.join(',\n')} @@ -89,12 +91,12 @@ function generateRouteRecordComponent( node: TreeNode, indentStr: string, importMode: _OptionsImportMode, - importList: Map + importsMap: ImportsMap ): string { const files = Array.from(node.value.components) const isDefaultExport = files.length === 1 && files[0][0] === 'default' return isDefaultExport - ? `component: ${generatePageImport(files[0][1], importMode, importList)},` + ? `component: ${generatePageImport(files[0][1], importMode, importsMap)},` : // files has at least one entry `components: { ${files @@ -103,7 +105,7 @@ ${files `${indentStr + ' '}'${key}': ${generatePageImport( path, importMode, - importList + importsMap )}` ) .join(',\n')} @@ -114,21 +116,21 @@ ${indentStr}},` * Generate the import (dynamic or static) for the given filepath. If the filepath is a static import, add it to the * @param filepath - the filepath to the file * @param importMode - the import mode to use - * @param importList - the import list to fill + * @param importsMap - the import list to fill * @returns */ function generatePageImport( filepath: string, importMode: _OptionsImportMode, - importList: Map + importsMap: ImportsMap ) { const mode = typeof importMode === 'function' ? importMode(filepath) : importMode if (mode === 'async') { return `() => import('${filepath}')` } else { - const importName = `_page_${importList.size}` - importList.set(importName, filepath) + const importName = `_page_${importsMap.size}` + importsMap.addDefault(filepath, importName) return importName } } diff --git a/src/core/context.ts b/src/core/context.ts index d4dcf0b9..035959ae 100644 --- a/src/core/context.ts +++ b/src/core/context.ts @@ -4,6 +4,7 @@ import { promises as fs } from 'fs' import { appendExtensionListToPattern, asRoutePath, + ImportsMap, logTree, throttle, } from './utils' @@ -167,25 +168,20 @@ export function createRoutesContext(options: ResolvedOptions) { } function generateRoutes() { - // keys are import names while values are paths import __ from __ - // TODO: reverse the order and make a list of named imports and another for defaults? - const importList = new Map() + const importsMap = new ImportsMap() const routesExport = `export const routes = ${generateRouteRecord( routeTree, options, - importList + importsMap )}` - // generate the list of imports - let imports = '' if (options.dataFetching) { - imports += `import { _HasDataLoaderMeta, _mergeRouteRecord } from 'unplugin-vue-router/runtime'\n` - } - for (const [name, path] of importList) { - imports += `import ${name} from '${path}'\n` + importsMap.add('unplugin-vue-router/runtime', '_HasDataLoaderMeta') } + // generate the list of imports + let imports = `${importsMap}` // add an empty line for readability if (imports) { imports += '\n' diff --git a/src/core/utils.ts b/src/core/utils.ts index a0949f30..a5115d4c 100644 --- a/src/core/utils.ts +++ b/src/core/utils.ts @@ -274,3 +274,73 @@ export function appendExtensionListToPattern( ? filePatterns.map((filePattern) => `${filePattern}${extensionPattern}`) : `${filePatterns}${extensionPattern}` } + +export interface ImportEntry { + // name of the variable to import + name: string + // optional name to use when importing + as?: string +} + +export class ImportsMap { + // path -> import as -> import name + // e.g map['vue-router']['myUseRouter'] = 'useRouter' -> import { useRouter as myUseRouter } from 'vue-router' + private map = new Map>() + + constructor() {} + + add(path: string, importEntry: ImportEntry): this + add(path: string, importEntry: string): this + add(path: string, importEntry: string | ImportEntry): this { + if (!this.map.has(path)) { + this.map.set(path, new Map()) + } + const imports = this.map.get(path)! + if (typeof importEntry === 'string') { + imports.set(importEntry, importEntry) + } else { + imports.set(importEntry.as || importEntry.name, importEntry.name) + } + + return this + } + + addDefault(path: string, as: string): this { + return this.add(path, { name: 'default', as }) + } + + getImportList(path: string): Required[] { + if (!this.map.has(path)) return [] + return Array.from(this.map.get(path)!).map(([as, name]) => ({ + as: as || name, + name, + })) + } + + toString(): string { + let importStatements = '' + for (const [path, imports] of this.map) { + if (!imports.size) continue + + // only one import and it's the default one + if (imports.size === 1) { + // we extract the first and only entry + const [[importName, maybeDefault]] = [...imports.entries()] + // we only care if this is the default import + if (maybeDefault === 'default') { + importStatements += `import ${importName} from '${path}'\n` + continue + } + } + importStatements += `import { ${Array.from(imports) + .map(([as, name]) => (as === name ? name : `${name} as ${as}`)) + .join(', ')} } from '${path}'\n` + } + + return importStatements + } + + get size(): number { + return this.map.size + } +}