From d9bb922f9ee136bbaad9ba165eff8ca57708f514 Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Fri, 22 Dec 2023 11:51:32 +0100 Subject: [PATCH] refactor: cleanup codebase --- src/addons/vue-template.ts | 4 +- src/context.ts | 294 +++++++------------- src/detect.ts | 119 ++++++++ src/index.ts | 14 +- src/{extract.ts => node/extract-package.ts} | 2 +- src/{ => node}/scan-dirs.ts | 2 +- src/preset.ts | 2 +- src/presets/README.md | 1 - src/regexp.ts | 30 ++ src/types.ts | 47 +++- src/utils.ts | 41 +-- test/existing-scanning.test.ts | 4 +- test/public-api.test.ts | 40 +++ test/scan-dirs.test.ts | 4 +- test/to-imports.test.ts | 26 +- 15 files changed, 368 insertions(+), 262 deletions(-) create mode 100644 src/detect.ts rename src/{extract.ts => node/extract-package.ts} (97%) rename src/{ => node}/scan-dirs.ts (98%) create mode 100644 src/regexp.ts create mode 100644 test/public-api.test.ts diff --git a/src/addons/vue-template.ts b/src/addons/vue-template.ts index d15a1aec..a99ab1b7 100644 --- a/src/addons/vue-template.ts +++ b/src/addons/vue-template.ts @@ -1,5 +1,5 @@ import type { Addon, Import } from '../types' -import { toImports } from '../utils' +import { stringifyImports } from '../utils' const contextRE = /\b_ctx\.([\w_]+)\b/g const UNREF_KEY = '__unimport_unref_' @@ -47,7 +47,7 @@ export function vueTemplateAddon(): Addon { targets = await addon.injectImportsResolved?.call(this, targets, s, id) ?? targets } - let injection = toImports(targets) + let injection = stringifyImports(targets) for (const addon of this.addons) { if (addon === self) continue diff --git a/src/context.ts b/src/context.ts index 25def35f..7ac3695e 100644 --- a/src/context.ts +++ b/src/context.ts @@ -1,15 +1,102 @@ -import { detectSyntax, findStaticImports, parseStaticImport } from 'mlly' import type MagicString from 'magic-string' import { version } from '../package.json' -import type { Addon, Import, ImportInjectionResult, InjectImportsOptions, Thenable, TypeDeclarationOptions, UnimportContext, UnimportMeta, UnimportOptions } from './types' -import { addImportToCode, dedupeImports, excludeRE, getMagicString, importAsRE, matchRE, normalizeImports, separatorRE, stripCommentsAndStrings, toExports, toTypeDeclarationFile, toTypeReExports } from './utils' +import type { Addon, Import, ImportInjectionResult, InjectImportsOptions, Thenable, TypeDeclarationOptions, Unimport, UnimportContext, UnimportMeta, UnimportOptions } from './types' +import { addImportToCode, dedupeImports, getMagicString, normalizeImports, toExports, toTypeDeclarationFile, toTypeReExports } from './utils' import { resolveBuiltinPresets } from './preset' import { vueTemplateAddon } from './addons' -import { dedupeDtsExports, scanExports, scanFilesFromDir } from './scan-dirs' +import { dedupeDtsExports, scanExports, scanFilesFromDir } from './node/scan-dirs' +import { detectImports, parseVirtualImports } from './detect' -export type Unimport = ReturnType +export function createUnimport(opts: Partial): Unimport { + const ctx = createInternalContext(opts) -export function createUnimport(opts: Partial) { + async function generateTypeDeclarations(options?: TypeDeclarationOptions) { + const opts: TypeDeclarationOptions = { + resolvePath: i => i.typeFrom || i.from, + ...options, + } + const { + typeReExports = true, + } = opts + const imports = await ctx.getImports() + let dts = toTypeDeclarationFile(imports.filter(i => !i.type && !i.dtsDisabled), opts) + const typeOnly = imports.filter(i => i.type) + if (typeReExports && typeOnly.length) + dts += `\n${toTypeReExports(typeOnly, opts)}` + + for (const addon of ctx.addons) + dts = await addon.declaration?.call(ctx, dts, opts) ?? dts + + return dts + } + + async function scanImportsFromFile(filepath: string, includeTypes = true) { + const additions = await scanExports(filepath, includeTypes) + await ctx.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) + const includeTypes = options?.types ?? true + const imports = (await Promise.all(files.map(dir => scanExports(dir, includeTypes)))).flat() + const deduped = dedupeDtsExports(imports) + await ctx.modifyDynamicImports(imports => imports.filter(i => !files.includes(i.from)).concat(deduped)) + return imports + } + + async function injectImportsWithContext(code: string | MagicString, id?: string, options?: InjectImportsOptions) { + const result = await injectImports(code, id, ctx, { + ...opts, + ...options, + }) + + // Collect metadata + const metadata = ctx.getMetadata() + 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 + } + + /** + * Initialize unimport: + * - scan imports from dirs + */ + async function init() { + if (ctx.options.dirs?.length) + await scanImportsFromDir() + } + + // Public API + return { + version, + init, + clearDynamicImports: () => ctx.clearDynamicImports(), + modifyDynamicImports: fn => ctx.modifyDynamicImports(fn), + scanImportsFromDir, + scanImportsFromFile, + getImports: () => ctx.getImports(), + getImportMap: () => ctx.getImportMap(), + detectImports: (code: string | MagicString) => detectImports(code, ctx), + injectImports: injectImportsWithContext, + parseVirtualImports: (code: string) => parseVirtualImports(code, ctx), + generateTypeDeclarations: (options?: TypeDeclarationOptions) => generateTypeDeclarations(options), + getMetadata: () => ctx.getMetadata(), + getInternalContext: () => ctx, + + // Deprecated + toExports: async (filepath?: string, includeTypes = false) => toExports(await ctx.getImports(), filepath, includeTypes), + } +} + +function createInternalContext(opts: Partial) { // Cache for combine imports let _combinedImports: Import[] | undefined const _map = new Map() @@ -37,11 +124,12 @@ export function createUnimport(opts: Partial) { const ctx: UnimportContext = { version, - options: opts, addons, staticImports: [...(opts.imports || [])].filter(Boolean), dynamicImports: [], + modifyDynamicImports, + clearDynamicImports, async getImports() { await resolvePromise return updateImports() @@ -102,7 +190,6 @@ export function createUnimport(opts: Partial) { const result = await fn(ctx.dynamicImports) if (Array.isArray(result)) ctx.dynamicImports = result - ctx.invalidate() } @@ -111,196 +198,7 @@ export function createUnimport(opts: Partial) { ctx.invalidate() } - async function generateTypeDeclarations(options?: TypeDeclarationOptions) { - const opts: TypeDeclarationOptions = { - resolvePath: i => i.typeFrom || i.from, - ...options, - } - const { - typeReExports = true, - } = opts - const imports = await ctx.getImports() - let dts = toTypeDeclarationFile(imports.filter(i => !i.type && !i.dtsDisabled), opts) - const typeOnly = imports.filter(i => i.type) - if (typeReExports && typeOnly.length) - dts += `\n${toTypeReExports(typeOnly, opts)}` - - for (const addon of ctx.addons) - dts = await addon.declaration?.call(ctx, dts, opts) ?? dts - - return dts - } - - 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) - const includeTypes = options?.types ?? true - const imports = (await Promise.all(files.map(dir => scanExports(dir, includeTypes)))).flat() - const deduped = dedupeDtsExports(imports) - await modifyDynamicImports(imports => imports.filter(i => !files.includes(i.from)).concat(deduped)) - return imports - } - - async function injectImportsWithContext(code: string | MagicString, id?: string, options?: InjectImportsOptions) { - const result = await injectImports(code, id, ctx, { - ...opts, - ...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 - } - - /** - * Initialize unimport: - * - scan imports from dirs - */ - async function init() { - if (ctx.options.dirs?.length) - await scanImportsFromDir() - } - - // Public API - return { - init, - clearDynamicImports, - modifyDynamicImports, - scanImportsFromDir, - scanImportsFromFile, - getImports: () => ctx.getImports(), - getImportMap: () => ctx.getImportMap(), - detectImports: (code: string | MagicString) => detectImports(code, ctx), - injectImports: injectImportsWithContext, - toExports: async (filepath?: string, includeTypes = false) => toExports(await ctx.getImports(), filepath, includeTypes), - parseVirtualImports: (code: string) => parseVirtualImports(code, ctx), - generateTypeDeclarations: (options?: TypeDeclarationOptions) => generateTypeDeclarations(options), - getMetadata: () => ctx.getMetadata(), - getInternalContext: () => ctx, - } -} - -function parseVirtualImports(code: string, ctx: UnimportContext) { - if (ctx.options.virtualImports?.length) { - return findStaticImports(code) - .filter(i => ctx.options.virtualImports!.includes(i.specifier)) - .map(i => parseStaticImport(i)) - } - return [] -} - -async function detectImports(code: string | MagicString, ctx: UnimportContext, options?: InjectImportsOptions) { - const s = getMagicString(code) - // Strip comments so we don't match on them - const original = s.original - const strippedCode = stripCommentsAndStrings( - original, - // Do not strip comments if they are virtual import names - options?.transformVirtualImports !== false && ctx.options.virtualImports?.length - ? { - filter: i => !(ctx.options.virtualImports!.includes(i)), - fillChar: '-', - } - : undefined, - ) - const syntax = detectSyntax(strippedCode) - 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 - 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] - // also keeps deep ternary like `true ? false ? a : b : c` - const before = strippedCode[i.index! - 1] - if (end === ':' && !['?', 'case'].includes(i[1].trim()) && before !== ':') - return null - - const name = i[2] - const occurrence = i.index! + i[1].length - if (occurrenceMap.get(name) || Number.POSITIVE_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() - occurrenceMap.delete(identifier) - } - } - } - - const identifiers = new Set(occurrenceMap.keys()) - matchedImports = Array.from(identifiers) - .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 - } - - // Transform virtual imports like `import { foo } from '#imports'` - if (options?.transformVirtualImports !== false && options?.transformVirtualImoports !== false && ctx.options.virtualImports?.length) { - const virtualImports = parseVirtualImports(strippedCode, ctx) - virtualImports.forEach((i) => { - s.remove(i.start, i.end) - Object.entries(i.namedImports || {}) - .forEach(([name, as]) => { - const original = map.get(name) - if (!original) - throw new Error(`[unimport] failed to find "${name}" imported from "${i.specifier}"`) - - matchedImports.push({ - from: original.from, - name: original.name, - as, - }) - }) - }) - } - - const firstOccurrence = Math.min(...Array.from(occurrenceMap.entries()).map(i => i[1])) - - return { - s, - strippedCode, - isCJSContext, - matchedImports, - firstOccurrence, - } + return ctx } async function injectImports( diff --git a/src/detect.ts b/src/detect.ts new file mode 100644 index 00000000..9e951c10 --- /dev/null +++ b/src/detect.ts @@ -0,0 +1,119 @@ +import { detectSyntax, findStaticImports, parseStaticImport } from 'mlly' +import type MagicString from 'magic-string' +import type { DetectImportResult, Import, InjectImportsOptions, UnimportContext } from './types' +import { getMagicString } from './utils' +import { excludeRE, importAsRE, matchRE, separatorRE, stripCommentsAndStrings } from './regexp' + +export async function detectImports( + code: string | MagicString, + ctx: UnimportContext, + options?: InjectImportsOptions, +): Promise { + const s = getMagicString(code) + // Strip comments so we don't match on them + const original = s.original + const strippedCode = stripCommentsAndStrings( + original, + // Do not strip comments if they are virtual import names + options?.transformVirtualImports !== false && ctx.options.virtualImports?.length + ? { + filter: i => !(ctx.options.virtualImports!.includes(i)), + fillChar: '-', + } + : undefined, + ) + const syntax = detectSyntax(strippedCode) + 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 + 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] + // also keeps deep ternary like `true ? false ? a : b : c` + const before = strippedCode[i.index! - 1] + if (end === ':' && !['?', 'case'].includes(i[1].trim()) && before !== ':') + return null + + const name = i[2] + const occurrence = i.index! + i[1].length + if (occurrenceMap.get(name) || Number.POSITIVE_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() + occurrenceMap.delete(identifier) + } + } + } + + const identifiers = new Set(occurrenceMap.keys()) + matchedImports = Array.from(identifiers) + .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 + } + + // Transform virtual imports like `import { foo } from '#imports'` + if (options?.transformVirtualImports !== false && ctx.options.virtualImports?.length) { + const virtualImports = parseVirtualImports(strippedCode, ctx) + virtualImports.forEach((i) => { + s.remove(i.start, i.end) + Object.entries(i.namedImports || {}) + .forEach(([name, as]) => { + const original = map.get(name) + if (!original) + throw new Error(`[unimport] failed to find "${name}" imported from "${i.specifier}"`) + + matchedImports.push({ + from: original.from, + name: original.name, + as, + }) + }) + }) + } + + const firstOccurrence = Math.min(...Array.from(occurrenceMap.entries()).map(i => i[1])) + + return { + s, + strippedCode, + isCJSContext, + matchedImports, + firstOccurrence, + } +} + +export function parseVirtualImports(code: string, ctx: UnimportContext) { + if (ctx.options.virtualImports?.length) { + return findStaticImports(code) + .filter(i => ctx.options.virtualImports!.includes(i.specifier)) + .map(i => parseStaticImport(i)) + } + return [] +} diff --git a/src/index.ts b/src/index.ts index 54f8813c..c0b99ee4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,12 +1,18 @@ -export * from './preset' +export { + resolveBuiltinPresets, + resolvePreset, +} from './preset' export * from './utils' +export * from './regexp' export * from './types' -export * from './scan-dirs' -export * from './context' -export * from './global' + +export { createUnimport } from './context' +export { installGlobalAutoImports } from './global' export { builtinPresets } from './presets' export type { BuiltinPresetName } from './presets' +export * from './node/scan-dirs' + export { vueTemplateAddon } from './addons/vue-template' export { version } from '../package.json' diff --git a/src/extract.ts b/src/node/extract-package.ts similarity index 97% rename from src/extract.ts rename to src/node/extract-package.ts index 335eca1c..e7217323 100644 --- a/src/extract.ts +++ b/src/node/extract-package.ts @@ -3,7 +3,7 @@ import { accessSync, constants, existsSync, promises as fsp } from 'node:fs' import { resolveModuleExportNames } from 'mlly' import { readPackageJSON, resolvePackageJSON } from 'pkg-types' import { dirname, join } from 'pathe' -import type { Import, PackagePreset } from './types' +import type { Import, PackagePreset } from '../types' const CACHE_PATH = /* #__PURE__ */ join(os.tmpdir(), 'unimport') let CACHE_WRITEABLE: boolean | undefined diff --git a/src/scan-dirs.ts b/src/node/scan-dirs.ts similarity index 98% rename from src/scan-dirs.ts rename to src/node/scan-dirs.ts index 67fd79ba..043b1230 100644 --- a/src/scan-dirs.ts +++ b/src/node/scan-dirs.ts @@ -6,7 +6,7 @@ import { dirname, extname, join, normalize, parse as parsePath, resolve } from ' import type { ESMExport } from 'mlly' import { findExports, findTypeExports } from 'mlly' import { camelCase } from 'scule' -import type { Import, ScanDirExportsOptions } from './types' +import type { Import, ScanDirExportsOptions } from '../types' export async function scanFilesFromDir(dir: string | string[], options?: ScanDirExportsOptions) { const dirs = (Array.isArray(dir) ? dir : [dir]).map(d => normalize(d)) diff --git a/src/preset.ts b/src/preset.ts index 95f0e5f4..e0ec5b8d 100644 --- a/src/preset.ts +++ b/src/preset.ts @@ -1,7 +1,7 @@ +import { resolvePackagePreset } from './node/extract-package' import type { BuiltinPresetName } from './presets' import { builtinPresets } from './presets' import type { Import, ImportCommon, InlinePreset, Preset } from './types' -import { resolvePackagePreset } from './extract' /** * Common propreties for import item and preset diff --git a/src/presets/README.md b/src/presets/README.md index 95fe80cb..fd9b781e 100644 --- a/src/presets/README.md +++ b/src/presets/README.md @@ -12,4 +12,3 @@ We are excited to hear you want to contribute to the presets! While we are happy The rules are not fixed and will adjust if needed. Thanks :) - diff --git a/src/regexp.ts b/src/regexp.ts new file mode 100644 index 00000000..bd3529c2 --- /dev/null +++ b/src/regexp.ts @@ -0,0 +1,30 @@ +import type { StripLiteralOptions } from 'strip-literal' +import { stripLiteral } from 'strip-literal' + +export const excludeRE = [ + // imported/exported from other module + /\b(import|export)\b([\s\w_$*{},]+)\sfrom\b/gs, + // defined as function + /\bfunction\s*([\w_$]+?)\s*\(/gs, + // defined as class + /\bclass\s*([\w_$]+?)\s*{/gs, + // defined as local variable + /\b(?:const|let|var)\s+?(\[.*?\]|\{.*?\}|.+?)\s*?[=;\n]/gs, +] + +export const importAsRE = /^.*\sas\s+/ +export const separatorRE = /[,[\]{}\n]|\bimport\b/g + +/** + * | | + * destructing case&ternary non-call inheritance | id | + * ↓ ↓ ↓ ↓ | | + */ +export const matchRE = /(^|\.\.\.|(?:\bcase|\?)\s+|[^\w_$\/)]|(?:\bextends)\s+)([\w_$]+)\s*(?=[.()[\]}}:;?+\-*&|`<>,\n]|\b(?:instanceof|in)\b|$|(?<=extends\s+\w+)\s+{)/g + +const regexRE = /\/[^\s]*?(? staticImports: Import[] @@ -91,12 +92,53 @@ export interface UnimportContext { getImportMap(): Promise> getMetadata(): UnimportMeta | undefined + modifyDynamicImports(fn: (imports: Import[]) => Thenable): Promise + clearDynamicImports(): void replaceImports(imports: UnimportOptions['imports']): Promise invalidate(): void resolveId(id: string, parentId?: string): Thenable } +export interface DetectImportResult { + s: MagicString + strippedCode: string + isCJSContext: boolean + matchedImports: Import[] + firstOccurrence: number +} + +export interface Unimport { + readonly version: string + init(): Promise + + clearDynamicImports: UnimportContext['clearDynamicImports'] + getImportMap: UnimportContext['getImportMap'] + getImports: UnimportContext['getImports'] + getInternalContext: () => UnimportContext + getMetadata: UnimportContext['getMetadata'] + modifyDynamicImports: UnimportContext['modifyDynamicImports'] + generateTypeDeclarations: (options?: TypeDeclarationOptions) => Promise + + /** + * Get un-imported usages from code + */ + detectImports(code: string | MagicString): Promise + /** + * Insert missing imports statements to code + */ + injectImports(code: string | MagicString, id?: string, options?: InjectImportsOptions): Promise + + scanImportsFromDir(dir?: string[], options?: ScanDirExportsOptions): Promise + scanImportsFromFile(file: string, includeTypes?: boolean): Promise + parseVirtualImports(code: string): ParsedStaticImport[] + + /** + * @deprecated + */ + toExports(filepath?: string, includeTypes?: boolean): Promise +} + export interface InjectionUsageRecord { import: Import count: number @@ -264,9 +306,6 @@ export interface InjectImportsOptions { */ transformVirtualImports?: boolean - /** @deprecated use `virtualImports` instead */ - transformVirtualImoports?: boolean - /** * Inject the imports at the end of other imports * diff --git a/src/utils.ts b/src/utils.ts index cb959e87..d9e95ebb 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -2,43 +2,14 @@ import { isAbsolute, relative } from 'pathe' import type { StaticImport } from 'mlly' import { findStaticImports, parseStaticImport, resolvePath } from 'mlly' import MagicString from 'magic-string' -import type { StripLiteralOptions } from 'strip-literal' -import { stripLiteral } from 'strip-literal' import type { Import, InlinePreset, MagicStringResult, TypeDeclarationOptions } from './types' - -export const excludeRE = [ - // imported/exported from other module - /\b(import|export)\b([\s\w_$*{},]+)\sfrom\b/gs, - // defined as function - /\bfunction\s*([\w_$]+?)\s*\(/gs, - // defined as class - /\bclass\s*([\w_$]+?)\s*{/gs, - // defined as local variable - /\b(?:const|let|var)\s+?(\[.*?\]|\{.*?\}|.+?)\s*?[=;\n]/gs, -] - -export const importAsRE = /^.*\sas\s+/ -export const separatorRE = /[,[\]{}\n]|\bimport\b/g - -/** - * | | - * destructing case&ternary non-call inheritance | id | - * ↓ ↓ ↓ ↓ | | - */ -export const matchRE = /(^|\.\.\.|(?:\bcase|\?)\s+|[^\w_$\/)]|(?:\bextends)\s+)([\w_$]+)\s*(?=[.()[\]}}:;?+\-*&|`<>,\n]|\b(?:instanceof|in)\b|$|(?<=extends\s+\w+)\s+{)/g - -const regexRE = /\/[^\s]*?(? { @@ -227,7 +198,6 @@ function toImportModuleMap(imports: Import[], includeType = false) { export function getString(code: string | MagicString) { if (typeof code === 'string') return code - return code.toString() } @@ -291,7 +261,7 @@ export function addImportToCode( newImports = onResolved?.(newImports) ?? newImports - let newEntries = toImports(newImports, isCJS) + let newEntries = stringifyImports(newImports, isCJS) newEntries = onStringified?.(newEntries, newImports) ?? newEntries if (newEntries) { @@ -327,3 +297,8 @@ export function resolveIdAbsolute(id: string, parentId?: string) { function isFilePath(path: string) { return path.startsWith('.') || isAbsolute(path) || path.includes('://') } + +/** + * @deprecated renamed to `stringifyImports` + */ +export const toImports = stringifyImports diff --git a/test/existing-scanning.test.ts b/test/existing-scanning.test.ts index a62dc428..6084ad3d 100644 --- a/test/existing-scanning.test.ts +++ b/test/existing-scanning.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { excludeRE, importAsRE, separatorRE, stripCommentsAndStrings } from '../src/utils' +import { excludeRE, importAsRE, separatorRE, stripCommentsAndStrings } from '../src/regexp' describe('regex for extract local variable', () => { const cases: { input: string, output: string[] }[] = [ @@ -7,7 +7,7 @@ describe('regex for extract local variable', () => { { input: 'const { ref, computed,watch} = Vue', output: ['ref', 'computed', 'watch'] }, { input: 'const { } = Vue', output: [] }, { input: 'const { ref} = Vue', output: ['ref'] }, - { input: 'const { mabye_test, $test} = Vue', output: ['mabye_test', '$test'] }, + { input: 'const { maybe_test, $test} = Vue', output: ['maybe_test', '$test'] }, { input: 'const [state] = useState(1)', output: ['state'] }, // We may not able to handle these cases diff --git a/test/public-api.test.ts b/test/public-api.test.ts new file mode 100644 index 00000000..95e91926 --- /dev/null +++ b/test/public-api.test.ts @@ -0,0 +1,40 @@ +import { expect, it } from 'vitest' + +it('public-api', async () => { + const keys = await import('../src').then(r => Object.keys(r).sort()) + + expect(keys) + .toMatchInlineSnapshot(` + [ + "addImportToCode", + "builtinPresets", + "createUnimport", + "dedupeDtsExports", + "dedupeImports", + "defineUnimportPreset", + "excludeRE", + "getMagicString", + "getString", + "importAsRE", + "installGlobalAutoImports", + "matchRE", + "normalizeImports", + "resolveBuiltinPresets", + "resolveIdAbsolute", + "resolvePreset", + "scanDirExports", + "scanExports", + "scanFilesFromDir", + "separatorRE", + "stringifyImports", + "stripCommentsAndStrings", + "toExports", + "toImports", + "toTypeDeclarationFile", + "toTypeDeclarationItems", + "toTypeReExports", + "version", + "vueTemplateAddon", + ] + `) +}) diff --git a/test/scan-dirs.test.ts b/test/scan-dirs.test.ts index 3c4b8e7e..67eec327 100644 --- a/test/scan-dirs.test.ts +++ b/test/scan-dirs.test.ts @@ -1,6 +1,6 @@ import { join, relative } from 'pathe' import { describe, expect, it } from 'vitest' -import { scanDirExports, toImports } from '../src' +import { scanDirExports, stringifyImports } from '../src' describe('scan-dirs', () => { it('scanDirExports', async () => { @@ -160,7 +160,7 @@ describe('scan-dirs', () => { ] `) - expect(toImports(importsResult)).toMatchInlineSnapshot(` + expect(stringifyImports(importsResult)).toMatchInlineSnapshot(` "import { bar, named } from 'nested/bar/index.ts'; import { myBazFunction } from 'nested/bar/baz.ts'; import { subFoo } from 'nested/bar/sub/index.ts';" diff --git a/test/to-imports.test.ts b/test/to-imports.test.ts index 48126f16..8c703bac 100644 --- a/test/to-imports.test.ts +++ b/test/to-imports.test.ts @@ -1,21 +1,21 @@ import { describe, expect, it } from 'vitest' import type { Import } from '../src/types' -import { toImports } from '../src/utils' +import { stringifyImports } from '../src/utils' describe('toImports', () => { it('basic', () => { const imports: Import[] = [{ from: 'test-id', name: 'fooBar', as: 'fooBar' }] - expect(toImports(imports)) + expect(stringifyImports(imports)) .toMatchInlineSnapshot('"import { fooBar } from \'test-id\';"') - expect(toImports(imports, true)) + expect(stringifyImports(imports, true)) .toMatchInlineSnapshot('"const { fooBar } = require(\'test-id\');"') }) it('alias', () => { const imports: Import[] = [{ from: 'test-id', name: 'foo', as: 'bar' }] - expect(toImports(imports)) + expect(stringifyImports(imports)) .toMatchInlineSnapshot('"import { foo as bar } from \'test-id\';"') - expect(toImports(imports, true)) + expect(stringifyImports(imports, true)) .toMatchInlineSnapshot('"const { foo: bar } = require(\'test-id\');"') }) @@ -25,12 +25,12 @@ describe('toImports', () => { { from: 'test1', name: 'bar', as: 'bar' }, { from: 'test2', name: 'foobar', as: 'foobar' }, ] - expect(toImports(imports)) + expect(stringifyImports(imports)) .toMatchInlineSnapshot(` "import { foo, bar } from 'test1'; import { foobar } from 'test2';" `) - expect(toImports(imports, true)) + expect(stringifyImports(imports, true)) .toMatchInlineSnapshot(` "const { foo, bar } = require('test1'); const { foobar } = require('test2');" @@ -41,9 +41,9 @@ describe('toImports', () => { const imports: Import[] = [ { from: 'test1', name: 'default', as: 'foo' }, ] - expect(toImports(imports)) + expect(stringifyImports(imports)) .toMatchInlineSnapshot('"import foo from \'test1\';"') - expect(toImports(imports, true)) + expect(stringifyImports(imports, true)) .toMatchInlineSnapshot('"const { default: foo } = require(\'test1\');"') }) @@ -51,9 +51,9 @@ describe('toImports', () => { const imports: Import[] = [ { from: 'test1', name: '*', as: 'foo' }, ] - expect(toImports(imports)) + expect(stringifyImports(imports)) .toMatchInlineSnapshot('"import * as foo from \'test1\';"') - expect(toImports(imports, true)) + expect(stringifyImports(imports, true)) .toMatchInlineSnapshot('"const foo = require(\'test1\');"') }) @@ -67,7 +67,7 @@ describe('toImports', () => { { from: 'test2', name: 'default', as: 'defaultAlias' }, { from: 'sideeffects', name: '', as: '' }, ] - expect(toImports(imports)) + expect(stringifyImports(imports)) .toMatchInlineSnapshot(` "import * as foo from 'test1'; import * as bar from 'test1'; @@ -76,7 +76,7 @@ describe('toImports', () => { import { foobar } from 'test2'; import 'sideeffects';" `) - expect(toImports(imports, true)) + expect(stringifyImports(imports, true)) .toMatchInlineSnapshot(` "const foo = require('test1'); const bar = require('test1');