diff --git a/src/context.ts b/src/context.ts index 905dd804..05609290 100644 --- a/src/context.ts +++ b/src/context.ts @@ -1,6 +1,6 @@ import { detectSyntax, findStaticImports, parseStaticImport } from 'mlly' import MagicString from 'magic-string' -import type { Addon, Import, ImportInjectionResult, InjectImportsOptions, Thenable, TypeDeclarationOptions, UnimportContext, UnimportOptions } from './types' +import type { Addon, Import, ImportInjectionResult, InjectImportsOptions, Thenable, TypeDeclarationOptions, UnimportContext, UnimportMeta, UnimportOptions } from './types' import { excludeRE, stripCommentsAndStrings, separatorRE, importAsRE, toTypeDeclarationFile, addImportToCode, dedupeImports, toExports, normalizeImports, matchRE, getMagicString } from './utils' import { resolveBuiltinPresets } from './preset' import { vueTemplateAddon } from './addons' @@ -24,6 +24,14 @@ export function createUnimport (opts: Partial) { opts.commentsDisable = opts.commentsDisable ?? ['@unimport-disable', '@imports-disable'] opts.commentsDebug = opts.commentsDebug ?? ['@unimport-debug', '@imports-debug'] + let metadata: UnimportMeta | undefined + + if (opts.collectMeta) { + metadata = { + injectionUsage: {} + } + } + const ctx: UnimportContext = { staticImports: [...(opts.imports || [])].filter(Boolean), dynamicImports: [], @@ -38,6 +46,9 @@ export function createUnimport (opts: Partial) { invalidate () { _combinedImports = undefined }, + getMetadata () { + return metadata + }, resolveId: (id, parentId) => opts.resolveId?.(id, parentId), addons, options: opts @@ -99,10 +110,26 @@ export function createUnimport (opts: Partial) { modifyDynamicImports, getImports: () => ctx.getImports(), detectImports: (code: string | MagicString) => detectImports(code, ctx), - injectImports: (code: string | MagicString, id?: string, options?: InjectImportsOptions) => injectImports(code, id, ctx, options), + injectImports: async (code: string | MagicString, id?: string, options?: InjectImportsOptions) => { + const result = await injectImports(code, id, ctx, options) + + // Collect metadata + if (metadata) { + result.imports.forEach((i) => { + metadata!.injectionUsage[i.name] = metadata!.injectionUsage[i.name] || { import: i, count: 0, moduleIds: [] } + metadata!.injectionUsage[i.name].count++ + if (id && !metadata!.injectionUsage[i.name].moduleIds.includes(id)) { + metadata!.injectionUsage[i.name].moduleIds.push(id) + } + }) + } + + return result + }, toExports: async (filepath?: string) => toExports(await ctx.getImports(), filepath), parseVirtualImports: (code: string) => parseVirtualImports(code, ctx), - generateTypeDeclarations + generateTypeDeclarations, + getMetadata: () => ctx.getMetadata() } } @@ -193,13 +220,19 @@ async function detectImports (code: string | MagicString, ctx: UnimportContext, } } -async function injectImports (code: string | MagicString, id: string | undefined, ctx: UnimportContext, options?: InjectImportsOptions): Promise { +async function injectImports ( + code: string | MagicString, + id: string | undefined, + ctx: UnimportContext, + options?: InjectImportsOptions +): Promise { const s = getMagicString(code) if (ctx.options.commentsDisable?.some(c => s.original.includes(c))) { return { s, - get code () { return s.toString() } + get code () { return s.toString() }, + imports: [] } } @@ -216,7 +249,10 @@ async function injectImports (code: string | MagicString, id: string | undefined log(`[unimport] ${imports.length} imports detected in "${id}"${imports.length ? ': ' + imports.map(i => i.name).join(', ') : ''}`) } - return addImportToCode(s, imports, isCJSContext, options?.mergeExisting) + return { + ...addImportToCode(s, imports, isCJSContext, options?.mergeExisting), + imports + } } async function resolveImports (ctx: UnimportContext, imports: Import[], id: string | undefined) { diff --git a/src/types.ts b/src/types.ts index 786d8dd0..3c1119b0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -68,6 +68,17 @@ export interface UnimportContext { invalidate(): void resolveId(id: string, parentId?: string): Thenable options: Partial + getMetadata(): UnimportMeta | undefined +} + +export interface InjectionUsageRecord { + import: Import, + count: number + moduleIds: string[] +} + +export interface UnimportMeta { + injectionUsage: Record } export interface AddonsOptions { @@ -137,6 +148,11 @@ export interface UnimportOptions { * @default ['@unimport-debug', '@imports-debug'] */ commentsDebug?: string[] + + /** + * Collect meta data for each auto import. Accessible via `ctx.meta` + */ + collectMeta?: boolean } export type PathFromResolver = (_import: Import) => string | undefined @@ -223,7 +239,11 @@ export interface InstallGlobalOptions { overrides?: boolean } -export interface ImportInjectionResult { +export interface MagicStringResult { s: MagicString code: string } + +export interface ImportInjectionResult extends MagicStringResult { + imports: Import[] +} diff --git a/src/utils.ts b/src/utils.ts index d08236f0..4508a924 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -3,7 +3,7 @@ import { isAbsolute, relative } from 'pathe' import { findStaticImports, parseStaticImport, StaticImport, resolvePath } from 'mlly' import MagicString from 'magic-string' import { stripLiteral } from 'strip-literal' -import type { Import, ImportInjectionResult, InlinePreset, TypeDeclarationOptions } from './types' +import type { Import, InlinePreset, MagicStringResult, TypeDeclarationOptions } from './types' export const excludeRE = [ // imported/exported from other module @@ -197,7 +197,12 @@ export function getMagicString (code:string | MagicString) { return code } -export function addImportToCode (code: string | MagicString, imports: Import[], isCJS = false, mergeExisting = false): ImportInjectionResult { +export function addImportToCode ( + code: string | MagicString, + imports: Import[], + isCJS = false, + mergeExisting = false +): MagicStringResult { let newImports: Import[] = [] const s = getMagicString(code) diff --git a/test/index.test.ts b/test/index.test.ts index 4f38ea23..e2aae358 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -12,6 +12,7 @@ describe('inject import', () => { console.log(fooBar())" `) }) + test('should not match export', async () => { const { injectImports } = createUnimport({ imports: [{ name: 'fooBar', from: 'test-id' }] @@ -19,4 +20,52 @@ describe('inject import', () => { expect((await injectImports('export { fooBar } from "test-id"')).code) .toMatchInlineSnapshot('"export { fooBar } from \\"test-id\\""') }) + + test('metadata', async () => { + const ctx = createUnimport({ + imports: [ + { name: 'import1', from: 'specifier1' }, + { name: 'import2', from: 'specifier2' }, + { name: 'import3', from: 'specifier3' }, + { name: 'import4', from: 'specifier4' }, + { name: 'foo', as: 'import5', from: 'specifier5' }, + { name: 'import10', from: 'specifier10' } + ], + collectMeta: true + }) + await ctx.injectImports('console.log(import1())', 'foo') + await ctx.injectImports('console.log(import1())', 'foo') + await ctx.injectImports('console.log(import2())', 'bar') + await ctx.injectImports('console.log(import1())', 'gar') + + expect(ctx.getMetadata()).toMatchInlineSnapshot(` + { + "injectionUsage": { + "import1": { + "count": 3, + "import": { + "as": "import1", + "from": "specifier1", + "name": "import1", + }, + "moduleIds": [ + "foo", + "gar", + ], + }, + "import2": { + "count": 1, + "import": { + "as": "import2", + "from": "specifier2", + "name": "import2", + }, + "moduleIds": [ + "bar", + ], + }, + }, + } + `) + }) })