Skip to content

Commit

Permalink
feat: support injectAtLast option
Browse files Browse the repository at this point in the history
  • Loading branch information
antfu committed Jan 29, 2023
1 parent 8fc28a5 commit 6b1ba91
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 28 deletions.
63 changes: 40 additions & 23 deletions src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,10 @@ export function createUnimport (opts: Partial<UnimportOptions>) {
}

async function injectImportsWithContext (code: string | MagicString, id?: string, options?: InjectImportsOptions) {
const result = await injectImports(code, id, ctx, options)
const result = await injectImports(code, id, ctx, {
...opts,
...options
})

// Collect metadata
if (metadata) {
Expand Down Expand Up @@ -179,41 +182,52 @@ async function detectImports (code: string | MagicString, ctx: UnimportContext,
const isCJSContext = syntax.hasCJS && !syntax.hasESM
let matchedImports: Import[] = []

const occurrenceMap = new Map<string, number>()

const map = await ctx.getImportMap()
// Auto import, search for unreferenced usages
if (options?.autoImport !== false) {
// Find all possible injection
const identifiers = new Set(
Array.from(strippedCode.matchAll(matchRE))
.map((i) => {
// Remove dot access, but keep destructuring
if (i[1] === '.') {
return ''
}
// Remove property, but keep `case x:` and `? x :`
const end = strippedCode[i.index! + i[0].length]
if (end === ':' && !['?', 'case'].includes(i[1].trim())) {
return ''
}
return i[2]
})
.filter(Boolean)
)
Array.from(strippedCode.matchAll(matchRE))
.forEach((i) => {
// Remove dot access, but keep destructuring
if (i[1] === '.') {
return null
}
// Remove property, but keep `case x:` and `? x :`
const end = strippedCode[i.index! + i[0].length]
if (end === ':' && !['?', 'case'].includes(i[1].trim())) {
return null
}
const name = i[2]
const occurrence = i.index! + i[1].length
if (occurrenceMap.get(name) || Infinity > occurrence) {
occurrenceMap.set(name, occurrence)
}
})

// Remove those already defined
for (const regex of excludeRE) {
for (const match of strippedCode.matchAll(regex)) {
const segments = [...match[1]?.split(separatorRE) || [], ...match[2]?.split(separatorRE) || []]
for (const segment of segments) {
const identifier = segment.replace(importAsRE, '').trim()
identifiers.delete(identifier)
occurrenceMap.delete(identifier)
}
}
}

const identifiers = new Set(occurrenceMap.keys())
matchedImports = Array.from(identifiers)
.map(name => map.get(name))
.filter(i => i && !i.disabled) as Import[]
.map((name) => {
const item = map.get(name)
if (item && !item.disabled) {
return item
}
occurrenceMap.delete(name)
return null
})
.filter(Boolean) as Import[]

for (const addon of ctx.addons) {
matchedImports = await addon.matchImports?.call(ctx, identifiers, matchedImports) || matchedImports
Expand All @@ -240,11 +254,14 @@ async function detectImports (code: string | MagicString, ctx: UnimportContext,
})
}

const firstOccurrence = Math.min(...Array.from(occurrenceMap.entries()).map(i => i[1]))

return {
s,
strippedCode,
isCJSContext,
matchedImports
matchedImports,
firstOccurrence
}
}

Expand All @@ -268,7 +285,7 @@ async function injectImports (
await addon.transform?.call(ctx, s, id)
}

const { isCJSContext, matchedImports } = await detectImports(s, ctx, options)
const { isCJSContext, matchedImports, firstOccurrence } = await detectImports(s, ctx, options)
const imports = await resolveImports(ctx, matchedImports, id)

if (ctx.options.commentsDebug?.some(c => s.original.includes(c))) {
Expand All @@ -278,7 +295,7 @@ async function injectImports (
}

return {
...addImportToCode(s, imports, isCJSContext, options?.mergeExisting),
...addImportToCode(s, imports, isCJSContext, options?.mergeExisting, options?.injectAtEnd, firstOccurrence),
imports
}
}
Expand Down
9 changes: 8 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ export interface AddonsOptions {
vueTemplate?: boolean
}

export interface UnimportOptions {
export interface UnimportOptions extends Pick<InjectImportsOptions, 'injectAtEnd' | 'mergeExisting'> {
/**
* Auto import items
*/
Expand Down Expand Up @@ -240,6 +240,13 @@ export interface InjectImportsOptions {

/** @deprecated use `virtualImports` instead */
transformVirtualImoports?: boolean

/**
* Inject the imports at the end of other imports
*
* @default false
*/
injectAtEnd?: boolean
}

export type Thenable<T> = Promise<T> | T
Expand Down
26 changes: 22 additions & 4 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,17 +201,27 @@ export function addImportToCode (
code: string | MagicString,
imports: Import[],
isCJS = false,
mergeExisting = false
mergeExisting = false,
injectAtLast = false,
firstOccurrence = Infinity
): MagicStringResult {
let newImports: Import[] = []
const s = getMagicString(code)

let _staticImports: StaticImport[] | undefined
function findStaticImportsLazy () {
if (!_staticImports) {
_staticImports = findStaticImports(s.original).map(i => parseStaticImport(i))
}
return _staticImports
}

if (mergeExisting && !isCJS) {
const existing = findStaticImports(s.original).map(i => parseStaticImport(i))
const existingImports = findStaticImportsLazy()
const map = new Map<StaticImport, Import[]>()

imports.forEach((i) => {
const target = existing.find(e => e.specifier === i.from && e.imports.startsWith('{'))
const target = existingImports.find(e => e.specifier === i.from && e.imports.startsWith('{'))
if (!target) {
return newImports.push(i)
}
Expand All @@ -234,7 +244,15 @@ export function addImportToCode (

const newEntries = toImports(newImports, isCJS)
if (newEntries) {
s.prepend(newEntries + '\n')
const insertionIndex = injectAtLast
? findStaticImportsLazy().reverse().find(i => i.end <= firstOccurrence)?.end ?? 0
: 0

if (insertionIndex === 0) {
s.prepend(newEntries + '\n')
} else {
s.appendRight(insertionIndex, '\n' + newEntries + '\n')
}
}

return {
Expand Down
55 changes: 55 additions & 0 deletions test/index.test.ts → test/inject.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,59 @@ describe('inject import', () => {
}
`)
})

test('mergeExisting', async () => {
const { injectImports } = createUnimport({
imports: [{ name: 'fooBar', from: 'test-id' }],
mergeExisting: true
})
expect((await injectImports(`
import { foo } from 'test-id'
console.log(fooBar())
`.trim())).code)
.toMatchInlineSnapshot(`
"import { fooBar, foo } from 'test-id'
console.log(fooBar())"
`)
})

test('injection at end', async () => {
const { injectImports } = createUnimport({
imports: [{ name: 'fooBar', from: 'test-id' }],
injectAtEnd: true
})
expect((await injectImports(`
import { foo } from 'foo'
console.log(fooBar())
`.trim())).code)
.toMatchInlineSnapshot(`
"import { foo } from 'foo'
import { fooBar } from 'test-id';
console.log(fooBar())"
`)
})

test('injection at end with mixed imports', async () => {
const { injectImports } = createUnimport({
imports: [{ name: 'fooBar', from: 'test-id' }],
injectAtEnd: true
})
expect((await injectImports(`
import { foo } from 'foo'
console.log(nonAutoImport())
import { bar } from 'bar'
console.log(fooBar())
import { baz } from 'baz'
`.trim())).code)
.toMatchInlineSnapshot(`
"import { foo } from 'foo'
console.log(nonAutoImport())
import { bar } from 'bar'
import { fooBar } from 'test-id';
console.log(fooBar())
import { baz } from 'baz'"
`)
})
})

0 comments on commit 6b1ba91

Please sign in to comment.