From 6b1ba9149f519742ffafe91497bc3feec905b1b7 Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Sun, 29 Jan 2023 13:59:35 +0100 Subject: [PATCH] feat: support `injectAtLast` option --- src/context.ts | 63 ++++++++++++++++---------- src/types.ts | 9 +++- src/utils.ts | 26 +++++++++-- test/{index.test.ts => inject.test.ts} | 55 ++++++++++++++++++++++ 4 files changed, 125 insertions(+), 28 deletions(-) rename test/{index.test.ts => inject.test.ts} (58%) diff --git a/src/context.ts b/src/context.ts index db3efab4..6b22eade 100644 --- a/src/context.ts +++ b/src/context.ts @@ -118,7 +118,10 @@ export function createUnimport (opts: Partial) { } 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) { @@ -179,26 +182,29 @@ async function detectImports (code: string | MagicString, ctx: UnimportContext, const isCJSContext = syntax.hasCJS && !syntax.hasESM let matchedImports: Import[] = [] + const occurrenceMap = new Map() + 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) { @@ -206,14 +212,22 @@ async function detectImports (code: string | MagicString, ctx: UnimportContext, 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 @@ -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 } } @@ -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))) { @@ -278,7 +295,7 @@ async function injectImports ( } return { - ...addImportToCode(s, imports, isCJSContext, options?.mergeExisting), + ...addImportToCode(s, imports, isCJSContext, options?.mergeExisting, options?.injectAtEnd, firstOccurrence), imports } } diff --git a/src/types.ts b/src/types.ts index 92658a16..be6bfe2c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -103,7 +103,7 @@ export interface AddonsOptions { vueTemplate?: boolean } -export interface UnimportOptions { +export interface UnimportOptions extends Pick { /** * Auto import items */ @@ -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 = Promise | T diff --git a/src/utils.ts b/src/utils.ts index 4508a924..5619a69f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -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() 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) } @@ -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 { diff --git a/test/index.test.ts b/test/inject.test.ts similarity index 58% rename from test/index.test.ts rename to test/inject.test.ts index e2aae358..9fb5d188 100644 --- a/test/index.test.ts +++ b/test/inject.test.ts @@ -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'" + `) + }) })