Skip to content

Commit

Permalink
feat: support scanning types export under dirs (#268)
Browse files Browse the repository at this point in the history
  • Loading branch information
antfu committed Sep 4, 2023
1 parent 39d51f9 commit a57d863
Show file tree
Hide file tree
Showing 7 changed files with 115 additions and 39 deletions.
6 changes: 6 additions & 0 deletions playground/composables/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,9 @@ export {
localA,
localB as localBAlias
}

export type CustomType1 = string | number

export interface CustomInterface1 {
name: string
}
2 changes: 2 additions & 0 deletions playground/composables/nested/bar/sub/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export function subFoo () {}

export type CustomType2 = ReturnType<typeof subFoo>
7 changes: 4 additions & 3 deletions src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,15 +132,16 @@ export function createUnimport (opts: Partial<UnimportOptions>) {
return dts
}

async function scanImportsFromFile (filepath: string) {
const additions = await scanExports(filepath)
async function scanImportsFromFile (filepath: string, includeTypes = true) {
const additions = await scanExports(filepath, includeTypes)
await modifyDynamicImports(imports => imports.filter(i => i.from !== filepath).concat(additions))
return additions
}

async function scanImportsFromDir (dirs = ctx.options.dirs || [], options = ctx.options.dirsScanOptions) {
const files = await scanFilesFromDir(dirs, options)
return (await Promise.all(files.map(scanImportsFromFile))).flat()
const includeTypes = options?.types ?? true
return (await Promise.all(files.map(dir => scanImportsFromFile(dir, includeTypes)))).flat()
}

async function injectImportsWithContext (code: string | MagicString, id?: string, options?: InjectImportsOptions) {
Expand Down
85 changes: 49 additions & 36 deletions src/scan-dirs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { readFile } from 'fs/promises'
import { existsSync } from 'fs'
import fg from 'fast-glob'
import { parse as parsePath, join, normalize, resolve, extname, dirname } from 'pathe'
import { findExports } from 'mlly'
import { ESMExport, findExports, findTypeExports } from 'mlly'
import { camelCase } from 'scule'
import { Import, ScanDirExportsOptions } from './types'

Expand Down Expand Up @@ -34,7 +34,8 @@ export async function scanFilesFromDir (dir: string | string[], options?: ScanDi

export async function scanDirExports (dir: string | string[], options?: ScanDirExportsOptions) {
const files = await scanFilesFromDir(dir, options)
const fileExports = await Promise.all(files.map(i => scanExports(i)))
const includeTypes = options?.types ?? true
const fileExports = await Promise.all(files.map(i => scanExports(i, includeTypes)))
return fileExports.flat()
}

Expand All @@ -47,7 +48,7 @@ const FileExtensionLookup = [
'.js'
]

export async function scanExports (filepath: string, seen = new Set<string>()): Promise<Import[]> {
export async function scanExports (filepath: string, includeTypes: boolean, seen = new Set<string>()): Promise<Import[]> {
if (seen.has(filepath)) {
// eslint-disable-next-line no-console
console.warn(`[unimport] "${filepath}" is already scanned, skipping`)
Expand All @@ -71,46 +72,58 @@ export async function scanExports (filepath: string, seen = new Set<string>()):
imports.push({ name: 'default', as, from: filepath })
}

for (const exp of exports) {
if (exp.type === 'named') {
for (const name of exp.names) {
imports.push({ name, as: name, from: filepath })
}
} else if (exp.type === 'declaration') {
if (exp.name) {
imports.push({ name: exp.name, as: exp.name, from: filepath })
}
} else if (exp.type === 'star' && exp.specifier) {
if (exp.name) {
// export * as foo from './foo'
imports.push({ name: exp.name, as: exp.name, from: filepath })
} else {
// export * from './foo', scan deeper
const subfile = exp.specifier
let subfilepath = resolve(dirname(filepath), subfile)

if (!extname(subfilepath)) {
for (const ext of FileExtensionLookup) {
if (existsSync(`${subfilepath}${ext}`)) {
subfilepath = `${subfilepath}${ext}`
break
} else if (existsSync(`${subfilepath}/index${ext}`)) {
subfilepath = `${subfilepath}/index${ext}`
break
async function toImport (exports: ESMExport[], additional?: Partial<Import>) {
for (const exp of exports) {
if (exp.type === 'named') {
for (const name of exp.names) {
imports.push({ name, as: name, from: filepath, ...additional })
}
} else if (exp.type === 'declaration') {
if (exp.name) {
imports.push({ name: exp.name, as: exp.name, from: filepath, ...additional })
}
} else if (exp.type === 'star' && exp.specifier) {
if (exp.name) {
// export * as foo from './foo'
imports.push({ name: exp.name, as: exp.name, from: filepath, ...additional })
} else {
// export * from './foo', scan deeper
const subfile = exp.specifier
let subfilepath = resolve(dirname(filepath), subfile)

if (!extname(subfilepath)) {
for (const ext of FileExtensionLookup) {
if (existsSync(`${subfilepath}${ext}`)) {
subfilepath = `${subfilepath}${ext}`
break
} else if (existsSync(`${subfilepath}/index${ext}`)) {
subfilepath = `${subfilepath}/index${ext}`
break
}
}
}
}

if (!existsSync(subfilepath)) {
// eslint-disable-next-line no-console
console.warn(`[unimport] failed to resolve "${subfilepath}", skip scanning`)
continue
}
if (!existsSync(subfilepath)) {
// eslint-disable-next-line no-console
console.warn(`[unimport] failed to resolve "${subfilepath}", skip scanning`)
continue
}

imports.push(...await scanExports(subfilepath, seen))
const nested = await scanExports(subfilepath, includeTypes, seen)
imports.push(...(additional
? nested.map(i => ({ ...i, ...additional }))
: nested
))
}
}
}
}

await toImport(exports)

if (includeTypes) {
await toImport(findTypeExports(code), { type: true })
}

return imports
}
7 changes: 7 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,13 @@ export interface ScanDirExportsOptions {
*/
fileFilter?: (file: string) => boolean

/**
* Register type exports
*
* @default true
*/
types?: boolean

/**
* Current working directory
*
Expand Down
4 changes: 4 additions & 0 deletions test/dts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ test('dts', async () => {
export type { Ref, ComputedRef } from 'vue'
// @ts-ignore
export type { JQuery } from 'jquery'
// @ts-ignore
export type { CustomType1, CustomInterface1 } from '<root>/playground/composables/index.ts'
// @ts-ignore
export type { CustomType2 } from '<root>/playground/composables/nested/bar/sub/index.ts'
}"
`)
})
43 changes: 43 additions & 0 deletions test/scan-dirs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@ describe('scan-dirs', () => {
"from": "index.ts",
"name": "bump",
},
{
"as": "CustomInterface1",
"from": "index.ts",
"name": "CustomInterface1",
"type": true,
},
{
"as": "CustomType1",
"from": "index.ts",
"name": "CustomType1",
"type": true,
},
{
"as": "foo",
"from": "foo.ts",
Expand Down Expand Up @@ -79,6 +91,37 @@ describe('scan-dirs', () => {
return i
})

expect(importsResult).toMatchInlineSnapshot(`
[
{
"as": "bar",
"from": "nested/bar/index.ts",
"name": "bar",
},
{
"as": "myBazFunction",
"from": "nested/bar/baz.ts",
"name": "myBazFunction",
},
{
"as": "named",
"from": "nested/bar/index.ts",
"name": "named",
},
{
"as": "subFoo",
"from": "nested/bar/sub/index.ts",
"name": "subFoo",
},
{
"as": "CustomType2",
"from": "nested/bar/sub/index.ts",
"name": "CustomType2",
"type": true,
},
]
`)

expect(toImports(importsResult)).toMatchInlineSnapshot(`
"import { bar, named } from 'nested/bar/index.ts';
import { myBazFunction } from 'nested/bar/baz.ts';
Expand Down

0 comments on commit a57d863

Please sign in to comment.