refactor!: migrate to `unimport` (#3386)

This commit is contained in:
Anthony Fu 2022-03-11 16:09:11 +08:00 committed by GitHub
parent c942465f79
commit cff2f37cc8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 310 additions and 479 deletions

View File

@ -1,35 +1,29 @@
import { installModule, useNuxt } from '@nuxt/kit' import { installModule, useNuxt } from '@nuxt/kit'
import * as CompositionApi from '@vue/composition-api' import * as CompositionApi from '@vue/composition-api'
import type { Preset } from 'unimport'
import autoImports from '../../nuxt3/src/auto-imports/module' import autoImports from '../../nuxt3/src/auto-imports/module'
import { vuePreset, commonPresets, appPreset } from '../../nuxt3/src/auto-imports/presets'
const UnsupportedImports = new Set(['useAsyncData', 'useFetch']) const UnsupportedImports = new Set(['useAsyncData', 'useFetch'])
const CapiHelpers = new Set(Object.keys(CompositionApi)) const CapiHelpers = new Set(Object.keys(CompositionApi))
const ImportRewrites = {
vue: '@vue/composition-api'
}
export function setupAutoImports () { export function setupAutoImports () {
const nuxt = useNuxt() const nuxt = useNuxt()
nuxt.hook('autoImports:extend', (autoImports) => { const bridgePresets: Preset[] = [
for (const autoImport of autoImports) { ...commonPresets,
// Rewrite imports {
if (autoImport.from in ImportRewrites) { from: '#app',
autoImport.from = ImportRewrites[autoImport.from] imports: [
} ...appPreset.imports.filter(i => !UnsupportedImports.has(i as string)),
// Disable unsupported imports 'useNuxt2Meta'
if (UnsupportedImports.has(autoImport.name)) { ]
autoImport.disabled = true },
} {
if (autoImport.from === '@vue/composition-api' && !CapiHelpers.has(autoImport.name)) { from: '@vue/composition-api',
autoImport.disabled = true imports: vuePreset.imports.filter(i => CapiHelpers.has(i as string))
}
} }
]
// Add bridge-only auto-imports nuxt.hook('modules:done', () => installModule(autoImports, { presets: bridgePresets }))
autoImports.push({ name: 'useNuxt2Meta', as: 'useNuxt2Meta', from: '#app' })
})
nuxt.hook('modules:done', () => installModule(autoImports))
} }

View File

@ -1,30 +0,0 @@
import * as CompositionApi from '@vue/composition-api'
import { expect, describe, it } from 'vitest'
import { Nuxt3AutoImports } from '../../nuxt3/src/auto-imports/imports'
const excludedVueHelpers = [
'EffectScope',
'createApp',
'createRef',
'default',
'del',
'isRaw',
'set',
'useCSSModule',
'version',
'warn',
'watchPostEffect',
'watchSyncEffect'
]
describe('auto-imports:vue', () => {
for (const name of Object.keys(CompositionApi)) {
if (excludedVueHelpers.includes(name)) {
continue
}
it(`should register ${name} globally`, () => {
expect(Nuxt3AutoImports.find(a => a.from === 'vue').names).to.include(name)
})
}
})

View File

@ -29,6 +29,7 @@
"scule": "^0.2.1", "scule": "^0.2.1",
"semver": "^7.3.5", "semver": "^7.3.5",
"unctx": "^1.0.2", "unctx": "^1.0.2",
"unimport": "0.0.8",
"untyped": "^0.4.2" "untyped": "^0.4.2"
}, },
"devDependencies": { "devDependencies": {

View File

@ -1,14 +1,12 @@
import type { AutoImport } from '../../schema/src/types/imports' import { Import } from 'unimport'
import { useNuxt } from './context' import { useNuxt } from './context'
import { assertNuxtCompatibility } from './compatibility' import { assertNuxtCompatibility } from './compatibility'
export function addAutoImport (_autoImports: AutoImport | AutoImport[]) { export function addAutoImport (imports: Import | Import[]) {
assertNuxtCompatibility({ bridge: true }) assertNuxtCompatibility({ bridge: true })
useNuxt().hook('autoImports:extend', (autoImports: AutoImport[]) => { useNuxt().hook('autoImports:extend', (autoImports) => {
for (const composable of (Array.isArray(_autoImports) ? _autoImports : [_autoImports])) { autoImports.push(...(Array.isArray(imports) ? imports : [imports]))
autoImports.push(composable)
}
}) })
} }

View File

@ -58,6 +58,7 @@
"pathe": "^0.2.0", "pathe": "^0.2.0",
"scule": "^0.2.1", "scule": "^0.2.1",
"ufo": "^0.7.11", "ufo": "^0.7.11",
"unimport": "0.0.8",
"unplugin": "^0.4.0", "unplugin": "^0.4.0",
"untyped": "^0.4.2", "untyped": "^0.4.2",
"vue": "^3.2.31", "vue": "^3.2.31",

View File

@ -2,44 +2,45 @@ import { promises as fsp, existsSync } from 'fs'
import { parse as parsePath } from 'pathe' import { parse as parsePath } from 'pathe'
import { findExports } from 'mlly' import { findExports } from 'mlly'
import { camelCase } from 'scule' import { camelCase } from 'scule'
import { AutoImport } from '@nuxt/schema'
import { resolveFiles } from '@nuxt/kit' import { resolveFiles } from '@nuxt/kit'
import { filterInPlace } from './utils' import { Unimport } from 'unimport'
export async function scanForComposables (dir: string | string[], autoImports: AutoImport[]) { export async function scanForComposables (dir: string | string[], ctx: Unimport) {
const performScan = async (entry: string) => { const performScan = async (entry: string) => {
const files = await resolveFiles(entry, [ const files = await resolveFiles(entry, [
'*.{ts,js,mjs,cjs,mts,cts}', '*.{ts,js,mjs,cjs,mts,cts}',
'*/index.{ts,js,mjs,cjs,mts,cts}' '*/index.{ts,js,mjs,cjs,mts,cts}'
]) ])
await Promise.all( await ctx.modifyDynamicImports(async (dynamicImports) => {
files.map(async (path) => { await Promise.all(
// Remove original entries from the same import (for build watcher) files.map(async (path) => {
filterInPlace(autoImports, i => i.from !== path) // Remove original entries from the same import (for build watcher)
filterInPlace(dynamicImports, i => i.from !== path)
const code = await fsp.readFile(path, 'utf-8') const code = await fsp.readFile(path, 'utf-8')
const exports = findExports(code) const exports = findExports(code)
const defaultExport = exports.find(i => i.type === 'default') const defaultExport = exports.find(i => i.type === 'default')
if (defaultExport) { if (defaultExport) {
let name = parsePath(path).name let name = parsePath(path).name
if (name === 'index') { if (name === 'index') {
name = parsePath(path.split('/').slice(0, -1).join('/')).name name = parsePath(path.split('/').slice(0, -1).join('/')).name
}
autoImports.push({ name: 'default', as: camelCase(name), from: path })
}
for (const exp of exports) {
if (exp.type === 'named') {
for (const name of exp.names) {
autoImports.push({ name, as: name, from: path })
} }
} else if (exp.type === 'declaration') { dynamicImports.push({ name: 'default', as: camelCase(name), from: path })
autoImports.push({ name: exp.name, as: exp.name, from: path })
} }
} for (const exp of exports) {
}) if (exp.type === 'named') {
) for (const name of exp.names) {
dynamicImports.push({ name, as: name, from: path })
}
} else if (exp.type === 'declaration') {
dynamicImports.push({ name: exp.name, as: exp.name, from: path })
}
}
})
)
})
} }
for (const entry of Array.isArray(dir) ? dir : [dir]) { for (const entry of Array.isArray(dir) ? dir : [dir]) {
@ -48,3 +49,12 @@ export async function scanForComposables (dir: string | string[], autoImports: A
await performScan(entry) await performScan(entry)
} }
} }
function filterInPlace<T> (arr: T[], predicate: (v: T) => any) {
let i = arr.length
while (i--) {
if (!predicate(arr[i])) {
arr.splice(i, 1)
}
}
}

View File

@ -1,49 +0,0 @@
import type { AutoImport } from '@nuxt/schema'
import escapeRE from 'escape-string-regexp'
export interface AutoImportContext {
autoImports: AutoImport[]
matchRE: RegExp
transform: {
exclude: RegExp[]
}
map: Map<string, AutoImport>
}
export function createAutoImportContext (opts): AutoImportContext {
return {
autoImports: [],
map: new Map(),
matchRE: /__never__/,
transform: {
exclude: opts.transform.exclude || [/node_modules/]
}
}
}
export function updateAutoImportContext (ctx: AutoImportContext) {
// Detect duplicates
const usedNames = new Set()
for (const autoImport of ctx.autoImports) {
if (usedNames.has(autoImport.as)) {
autoImport.disabled = true
console.warn(`Disabling duplicate auto import '${autoImport.as}' (imported from '${autoImport.from}')`)
} else {
usedNames.add(autoImport.as)
}
}
// Filter out disabled auto imports
ctx.autoImports = ctx.autoImports.filter(i => i.disabled !== true)
// Create regex
ctx.matchRE = new RegExp(`\\b(${ctx.autoImports.map(i => escapeRE(i.as)).join('|')})\\b`, 'g')
// Create map
ctx.map.clear()
for (const autoImport of ctx.autoImports) {
ctx.map.set(autoImport.as, autoImport)
}
return ctx
}

View File

@ -1,113 +0,0 @@
import type { AutoImportSource } from '@nuxt/schema'
export const Nuxt3AutoImports: AutoImportSource[] = [
// #app
{
from: '#app',
names: [
'useAsyncData',
'useLazyAsyncData',
'defineNuxtComponent',
'useNuxtApp',
'defineNuxtPlugin',
'useRuntimeConfig',
'useState',
'useFetch',
'useLazyFetch',
'useCookie',
'useRequestHeaders',
'useRouter',
'useRoute',
'defineNuxtRouteMiddleware',
'navigateTo',
'abortNavigation',
'addRouteMiddleware'
]
},
// #meta
{
from: '#meta',
names: [
'useMeta'
]
},
// vue-demi (mocked)
{
from: 'vue-demi',
names: [
'isVue2',
'isVue3'
]
},
// vue
{
from: 'vue',
names: [
// <script setup>
'withCtx',
'withDirectives',
'withKeys',
'withMemo',
'withModifiers',
'withScopeId',
// Lifecycle
'onActivated',
'onBeforeMount',
'onBeforeUnmount',
'onBeforeUpdate',
'onDeactivated',
'onErrorCaptured',
'onMounted',
'onRenderTracked',
'onRenderTriggered',
'onServerPrefetch',
'onUnmounted',
'onUpdated',
// Reactivity
'computed',
'customRef',
'isProxy',
'isReactive',
'isReadonly',
'isRef',
'markRaw',
'proxyRefs',
'reactive',
'readonly',
'ref',
'shallowReactive',
'shallowReadonly',
'shallowRef',
'toRaw',
'toRef',
'toRefs',
'triggerRef',
'unref',
'watch',
'watchEffect',
'isShallow',
// effect
'effect',
'effectScope',
'getCurrentScope',
'onScopeDispose',
// Component
'defineComponent',
'defineAsyncComponent',
'getCurrentInstance',
'h',
'inject',
'nextTick',
'provide',
'useAttrs',
'useCssModule',
'useCssVars',
'useSlots',
'useTransitionState'
] as Array<keyof typeof import('vue')>
}
]

View File

@ -1,21 +1,20 @@
import { addVitePlugin, addWebpackPlugin, defineNuxtModule, addTemplate, resolveAlias, addPluginTemplate, useNuxt } from '@nuxt/kit' import { addVitePlugin, addWebpackPlugin, defineNuxtModule, addTemplate, resolveAlias, useNuxt, addPluginTemplate, logger } from '@nuxt/kit'
import type { AutoImportsOptions } from '@nuxt/schema'
import { isAbsolute, join, relative, resolve, normalize } from 'pathe' import { isAbsolute, join, relative, resolve, normalize } from 'pathe'
import { genDynamicImport } from 'knitwork' import { createUnimport, Import, toImports, Unimport } from 'unimport'
import { AutoImportsOptions, ImportPresetWithDeperection } from '@nuxt/schema'
import { TransformPlugin } from './transform' import { TransformPlugin } from './transform'
import { Nuxt3AutoImports } from './imports' import { defaultPresets } from './presets'
import { scanForComposables } from './composables' import { scanForComposables } from './composables'
import { toExports, toImports } from './utils'
import { AutoImportContext, createAutoImportContext, updateAutoImportContext } from './context'
export default defineNuxtModule<AutoImportsOptions>({ export default defineNuxtModule<Partial<AutoImportsOptions>>({
meta: { meta: {
name: 'auto-imports', name: 'auto-imports',
configKey: 'autoImports' configKey: 'autoImports'
}, },
defaults: { defaults: {
sources: Nuxt3AutoImports, presets: defaultPresets,
global: false, global: false,
imports: [],
dirs: [], dirs: [],
transform: { transform: {
exclude: undefined exclude: undefined
@ -23,13 +22,23 @@ export default defineNuxtModule<AutoImportsOptions>({
}, },
async setup (options, nuxt) { async setup (options, nuxt) {
// Allow modules extending sources // Allow modules extending sources
await nuxt.callHook('autoImports:sources', options.sources) await nuxt.callHook('autoImports:sources', options.presets as ImportPresetWithDeperection[])
options.presets.forEach((i: ImportPresetWithDeperection) => {
if (typeof i !== 'string' && i.names && !i.imports) {
i.imports = i.names
logger.warn('auto-imports: presets.names is deprecated, use presets.imports instead')
}
})
// Filter disabled sources // Filter disabled sources
options.sources = options.sources.filter(source => source.disabled !== true) // options.sources = options.sources.filter(source => source.disabled !== true)
// Create a context to share state between module internals // Create a context to share state between module internals
const ctx = createAutoImportContext(options) const ctx = createUnimport({
presets: defaultPresets,
imports: options.imports
})
// composables/ dirs // composables/ dirs
let composablesDirs = [ let composablesDirs = [
@ -51,7 +60,7 @@ export default defineNuxtModule<AutoImportsOptions>({
// Support for importing from '#imports' // Support for importing from '#imports'
addTemplate({ addTemplate({
filename: 'imports.mjs', filename: 'imports.mjs',
getContents: () => toExports(ctx.autoImports) getContents: () => ctx.toExports()
}) })
nuxt.options.alias['#imports'] = join(nuxt.options.buildDir, 'imports') nuxt.options.alias['#imports'] = join(nuxt.options.buildDir, 'imports')
@ -62,30 +71,25 @@ export default defineNuxtModule<AutoImportsOptions>({
addPluginTemplate({ addPluginTemplate({
filename: 'auto-imports.mjs', filename: 'auto-imports.mjs',
getContents: () => { getContents: () => {
const imports = toImports(ctx.autoImports) const imports = ctx.getImports()
const globalThisSet = ctx.autoImports.map(i => `globalThis.${i.as} = ${i.as};`).join('\n') const importStatement = toImports(imports)
return `${imports}\n\n${globalThisSet}\n\nexport default () => {};` const globalThisSet = imports.map(i => `globalThis.${i.as} = ${i.as};`).join('\n')
return `${importStatement}\n\n${globalThisSet}\n\nexport default () => {};`
} }
}) })
} else { } else {
// Transform to inject imports in production mode // Transform to inject imports in production mode
addVitePlugin(TransformPlugin.vite(ctx)) addVitePlugin(TransformPlugin.vite({ ctx, options }))
addWebpackPlugin(TransformPlugin.webpack(ctx)) addWebpackPlugin(TransformPlugin.webpack({ ctx, options }))
} }
const regenerateAutoImports = async () => { const regenerateAutoImports = async () => {
// Resolve autoimports from sources
ctx.autoImports = options.sources.flatMap(source => source.names.map(
importName => typeof importName === 'string'
? { name: importName, as: importName, from: source.from }
: { name: importName.name, as: importName.as || importName.name, from: source.from }
))
// Scan composables/ // Scan composables/
await scanForComposables(composablesDirs, ctx.autoImports) await scanForComposables(composablesDirs, ctx)
// Allow modules extending // Allow modules extending
await nuxt.callHook('autoImports:extend', ctx.autoImports) await ctx.modifyDynamicImports(async (imports) => {
// Update context await nuxt.callHook('autoImports:extend', imports)
updateAutoImportContext(ctx) })
} }
await regenerateAutoImports() await regenerateAutoImports()
@ -113,37 +117,34 @@ export default defineNuxtModule<AutoImportsOptions>({
} }
}) })
function addDeclarationTemplates (ctx: AutoImportContext) { function addDeclarationTemplates (ctx: Unimport) {
const nuxt = useNuxt() const nuxt = useNuxt()
// Remove file extension for benefit of TypeScript // Remove file extension for benefit of TypeScript
const stripExtension = (path: string) => path.replace(/\.[a-z]+$/, '') const stripExtension = (path: string) => path.replace(/\.[a-z]+$/, '')
const resolved = {} const resolved = {}
const r = (id: string) => { const r = ({ from }: Import) => {
if (resolved[id]) { return resolved[id] } if (resolved[from]) {
let path = resolveAlias(id) return resolved[from]
}
let path = resolveAlias(from)
if (isAbsolute(path)) { if (isAbsolute(path)) {
path = relative(join(nuxt.options.buildDir, 'types'), path) path = relative(join(nuxt.options.buildDir, 'types'), path)
} }
path = stripExtension(path) path = stripExtension(path)
resolved[id] = path resolved[from] = path
return path return path
} }
addTemplate({ addTemplate({
filename: 'imports.d.ts', filename: 'imports.d.ts',
getContents: () => toExports(ctx.autoImports.map(i => ({ ...i, from: stripExtension(i.from) }))) getContents: () => ctx.toExports()
}) })
addTemplate({ addTemplate({
filename: 'types/auto-imports.d.ts', filename: 'types/auto-imports.d.ts',
getContents: () => `// Generated by auto imports getContents: () => '// Generated by auto imports\n' + ctx.generateTypeDecarations(r)
declare global {
${ctx.autoImports.map(i => ` const ${i.as}: typeof ${genDynamicImport(r(i.from), { wrapper: false })}['${i.name}']`).join('\n')}
}
export {}
`
}) })
} }

View File

@ -0,0 +1,120 @@
import { defineUnimportPreset } from 'unimport'
export const commonPresets = [
// #meta
defineUnimportPreset({
from: '#meta',
imports: [
'useMeta'
]
}),
// vue-demi (mocked)
defineUnimportPreset({
from: 'vue-demi',
imports: [
'isVue2',
'isVue3'
]
})
]
export const appPreset = defineUnimportPreset({
from: '#app',
imports: [
'useAsyncData',
'useLazyAsyncData',
'defineNuxtComponent',
'useNuxtApp',
'defineNuxtPlugin',
'useRuntimeConfig',
'useState',
'useFetch',
'useLazyFetch',
'useCookie',
'useRequestHeaders',
'useRouter',
'useRoute',
'defineNuxtRouteMiddleware',
'navigateTo',
'abortNavigation',
'addRouteMiddleware'
]
})
// vue
export const vuePreset = defineUnimportPreset({
from: 'vue',
imports: [
// <script setup>
'withCtx',
'withDirectives',
'withKeys',
'withMemo',
'withModifiers',
'withScopeId',
// Lifecycle
'onActivated',
'onBeforeMount',
'onBeforeUnmount',
'onBeforeUpdate',
'onDeactivated',
'onErrorCaptured',
'onMounted',
'onRenderTracked',
'onRenderTriggered',
'onServerPrefetch',
'onUnmounted',
'onUpdated',
// Reactivity
'computed',
'customRef',
'isProxy',
'isReactive',
'isReadonly',
'isRef',
'markRaw',
'proxyRefs',
'reactive',
'readonly',
'ref',
'shallowReactive',
'shallowReadonly',
'shallowRef',
'toRaw',
'toRef',
'toRefs',
'triggerRef',
'unref',
'watch',
'watchEffect',
'isShallow',
// effect
'effect',
'effectScope',
'getCurrentScope',
'onScopeDispose',
// Component
'defineComponent',
'defineAsyncComponent',
'getCurrentInstance',
'h',
'inject',
'nextTick',
'provide',
'useAttrs',
'useCssModule',
'useCssVars',
'useSlots',
'useTransitionState'
] as Array<keyof typeof import('vue')>
})
export const defaultPresets = [
...commonPresets,
appPreset,
vuePreset
]

View File

@ -1,37 +1,9 @@
import { createUnplugin } from 'unplugin' import { createUnplugin } from 'unplugin'
import { parseQuery, parseURL } from 'ufo' import { parseQuery, parseURL } from 'ufo'
import { toImports } from './utils' import { Unimport } from 'unimport'
import { AutoImportContext } from './context' import { AutoImportsOptions } from '@nuxt/schema'
const excludeRE = [ export const TransformPlugin = createUnplugin(({ ctx, options }: {ctx: Unimport, options: Partial<AutoImportsOptions> }) => {
// imported from other module
/\bimport\s*([\s\S]+?)\s*from\b/g,
// defined as function
/\bfunction\s*([\w_$]+?)\s*\(/g,
// defined as local variable
/\b(?:const|let|var)\s+?(\[[\s\S]*?\]|\{[\s\S]*?\}|[\s\S]+?)\s*?[=;\n]/g
]
const importAsRE = /^.*\sas\s+/
const separatorRE = /[,[\]{}\n]/g
const multilineCommentsRE = /\/\*\s(.|[\r\n])*?\*\//gm
const singlelineCommentsRE = /\/\/\s.*$/gm
const templateLiteralRE = /\$\{(.*)\}/g
const quotesRE = [
/(["'])((?:\\\1|(?!\1)|.|\r)*?)\1/gm,
/([`])((?:\\\1|(?!\1)|.|\n|\r)*?)\1/gm
]
function stripCommentsAndStrings (code: string) {
return code
.replace(multilineCommentsRE, '')
.replace(singlelineCommentsRE, '')
.replace(templateLiteralRE, '` + $1 + `')
.replace(quotesRE[0], '""')
.replace(quotesRE[1], '``')
}
export const TransformPlugin = createUnplugin((ctx: AutoImportContext) => {
return { return {
name: 'nuxt:auto-imports-transform', name: 'nuxt:auto-imports-transform',
enforce: 'post', enforce: 'post',
@ -39,8 +11,10 @@ export const TransformPlugin = createUnplugin((ctx: AutoImportContext) => {
const { pathname, search } = parseURL(id) const { pathname, search } = parseURL(id)
const { type, macro } = parseQuery(search) const { type, macro } = parseQuery(search)
const exclude = options.transform?.exclude || [/[\\/]node_modules[\\/]/]
// Exclude node_modules by default // Exclude node_modules by default
if (ctx.transform.exclude.some(pattern => id.match(pattern))) { if (exclude.some(pattern => id.match(pattern))) {
return false return false
} }
@ -57,35 +31,15 @@ export const TransformPlugin = createUnplugin((ctx: AutoImportContext) => {
return true return true
} }
}, },
transform (code) { async transform (_code, id) {
// strip comments so we don't match on them const { code, s } = await ctx.injectImports(_code)
const stripped = stripCommentsAndStrings(code) if (code === _code) {
return
// find all possible injection
const matched = new Set(Array.from(stripped.matchAll(ctx.matchRE)).map(i => i[1]))
// remove those already defined
for (const regex of excludeRE) {
Array.from(stripped.matchAll(regex))
.flatMap(i => [
...(i[1]?.split(separatorRE) || []),
...(i[2]?.split(separatorRE) || [])
])
.map(i => i.replace(importAsRE, '').trim())
.forEach(i => matched.delete(i))
} }
return {
if (!matched.size) { code,
return null map: s.generateMap({ source: id, includeContent: true })
} }
// For webpack4/bridge support
const isCJSContext = stripped.includes('require(')
const matchedImports = Array.from(matched).map(name => ctx.map.get(name)).filter(Boolean)
const imports = toImports(matchedImports, isCJSContext)
return imports + code
} }
} }
}) })

View File

@ -1,47 +0,0 @@
import type { AutoImport } from '@nuxt/schema'
import { genExport, genImport, genString } from 'knitwork'
export function toImportModuleMap (autoImports: AutoImport[], isCJS = false) {
const aliasKeyword = isCJS ? ' : ' : ' as '
const map: Record<string, Set<string>> = {}
for (const autoImport of autoImports) {
if (!map[autoImport.from]) {
map[autoImport.from] = new Set()
}
map[autoImport.from].add(
autoImport.name === autoImport.as
? autoImport.name
: autoImport.name + aliasKeyword + autoImport.as
)
}
return map
}
export function toImports (autoImports: AutoImport[], isCJS = false) {
const map = toImportModuleMap(autoImports, isCJS)
if (isCJS) {
return Object.entries(map)
.map(([name, imports]) => `const { ${Array.from(imports).join(', ')} } = require(${genString(name)});`)
.join('\n')
} else {
return Object.entries(map)
.map(([name, imports]) => genImport(name, Array.from(imports)))
.join('\n')
}
}
export function toExports (autoImports: AutoImport[]) {
const map = toImportModuleMap(autoImports, false)
return Object.entries(map)
.map(([name, imports]) => genExport(name, Array.from(imports)))
.join('\n')
}
export function filterInPlace<T> (arr: T[], predicate: (v: T) => any) {
let i = arr.length
while (i--) {
if (!predicate(arr[i])) {
arr.splice(i, 1)
}
}
}

View File

@ -1,35 +1,32 @@
import { readFileSync } from 'fs' import { readFileSync } from 'fs'
import type { AutoImport } from '@nuxt/schema'
import { expect, describe, it } from 'vitest' import { expect, describe, it } from 'vitest'
import { join } from 'pathe' import { join } from 'pathe'
import { createCommonJS, findExports } from 'mlly' import { createCommonJS, findExports } from 'mlly'
import * as VueFunctions from 'vue' import * as VueFunctions from 'vue'
import { AutoImportContext, updateAutoImportContext } from '../src/auto-imports/context' import { createUnimport, Import } from 'unimport'
import { TransformPlugin } from '../src/auto-imports/transform' import { TransformPlugin } from '../src/auto-imports/transform'
import { Nuxt3AutoImports } from '../src/auto-imports/imports' import { defaultPresets } from '../src/auto-imports/presets'
describe('auto-imports:transform', () => { describe('auto-imports:transform', () => {
const autoImports: AutoImport[] = [ const imports: Import[] = [
{ name: 'ref', as: 'ref', from: 'vue' }, { name: 'ref', as: 'ref', from: 'vue' },
{ name: 'computed', as: 'computed', from: 'bar' }, { name: 'computed', as: 'computed', from: 'bar' },
{ name: 'foo', as: 'foo', from: 'excluded' } { name: 'foo', as: 'foo', from: 'excluded' }
] ]
const ctx = { const ctx = createUnimport({
autoImports, imports
map: new Map(), })
transform: {
exclude: [/excluded/]
}
} as AutoImportContext
updateAutoImportContext(ctx)
const transformPlugin = TransformPlugin.raw(ctx, { framework: 'rollup' }) const transformPlugin = TransformPlugin.raw({ ctx, options: { transform: { exclude: [/node_modules/] } } }, { framework: 'rollup' })
const transform = (code: string) => transformPlugin.transform.call({ error: null, warn: null }, code, '') const transform = async (source: string) => {
const { code } = await transformPlugin.transform.call({ error: null, warn: null }, source, '') || { code: null }
return code
}
it('should correct inject', async () => { it('should correct inject', async () => {
expect(await transform('const a = ref(0)')).to.equal('import { ref } from "vue";const a = ref(0)') expect(await transform('const a = ref(0)')).toMatchInlineSnapshot('"import { ref } from \'vue\';const a = ref(0)"')
expect(await transform('import { computed as ref } from "foo"; const a = ref(0)')).to.include('import { computed } from "bar";') expect(await transform('import { computed as ref } from "foo"; const a = ref(0)')).to.toMatchInlineSnapshot('"import { computed } from \'bar\';import { computed as ref } from \\"foo\\"; const a = ref(0)"')
}) })
it('should ignore existing imported', async () => { it('should ignore existing imported', async () => {
@ -43,11 +40,14 @@ describe('auto-imports:transform', () => {
it('should ignore comments', async () => { it('should ignore comments', async () => {
const result = await transform('// import { computed } from "foo"\n;const a = computed(0)') const result = await transform('// import { computed } from "foo"\n;const a = computed(0)')
expect(result).to.equal('import { computed } from "bar";// import { computed } from "foo"\n;const a = computed(0)') expect(result).toMatchInlineSnapshot(`
"import { computed } from 'bar';// import { computed } from \\"foo\\"
;const a = computed(0)"
`)
}) })
it('should exclude files from transform', () => { it('should exclude files from transform', async () => {
expect(transformPlugin.transformInclude.call({ error: null, warn: null }, 'excluded')).to.equal(false) expect(await transform('excluded')).toEqual(null)
}) })
}) })
@ -64,7 +64,7 @@ describe('auto-imports:nuxt3', () => {
continue continue
} }
it(`should register ${name} globally`, () => { it(`should register ${name} globally`, () => {
expect(Nuxt3AutoImports.find(a => a.from === '#app').names).to.include(name) expect(defaultPresets.find(a => a.from === '#app').imports).to.include(name)
}) })
} }
} catch (e) { } catch (e) {
@ -175,7 +175,7 @@ describe('auto-imports:vue', () => {
continue continue
} }
it(`should register ${name} globally`, () => { it(`should register ${name} globally`, () => {
expect(Nuxt3AutoImports.find(a => a.from === 'vue').names).toContain(name) expect(defaultPresets.find(a => a.from === 'vue').imports).toContain(name)
}) })
} }
}) })

View File

@ -27,7 +27,8 @@
"postcss-import-resolver": "^2.0.0", "postcss-import-resolver": "^2.0.0",
"scule": "^0.2.1", "scule": "^0.2.1",
"std-env": "^3.0.1", "std-env": "^3.0.1",
"ufo": "^0.7.11" "ufo": "^0.7.11",
"unimport": "0.0.5"
}, },
"engines": { "engines": {
"node": "^14.16.0 || ^16.11.0 || ^17.0.0" "node": "^14.16.0 || ^16.11.0 || ^17.0.0"

View File

@ -4,7 +4,7 @@ import type { Compiler, Configuration, Stats } from 'webpack'
import type { TSConfig } from 'pkg-types' import type { TSConfig } from 'pkg-types'
import type { ModuleContainer } from './module' import type { ModuleContainer } from './module'
import type { NuxtTemplate, Nuxt, NuxtApp } from './nuxt' import type { NuxtTemplate, Nuxt, NuxtApp } from './nuxt'
import type { AutoImport, AutoImportSource } from './imports' import type { Preset as ImportPreset, Import } from 'unimport'
import type { NuxtConfig, NuxtOptions } from './config' import type { NuxtConfig, NuxtOptions } from './config'
import type { Component, ComponentsDir, ScanDir, ComponentsOptions } from './components' import type { Component, ComponentsDir, ScanDir, ComponentsOptions } from './components'
import { NuxtCompatibility, NuxtCompatibilityIssues } from '..' import { NuxtCompatibility, NuxtCompatibilityIssues } from '..'
@ -52,6 +52,13 @@ export type NuxtLayout = {
file: string file: string
} }
export interface ImportPresetWithDeperection extends ImportPreset {
/**
* @deprecated renamed to `imports`
*/
names?: string[]
}
export interface NuxtHooks { export interface NuxtHooks {
// Kit // Kit
'kit:compatibility': (compatibility: NuxtCompatibility, issues: NuxtCompatibilityIssues) => HookResult 'kit:compatibility': (compatibility: NuxtCompatibility, issues: NuxtCompatibilityIssues) => HookResult
@ -66,8 +73,8 @@ export interface NuxtHooks {
'pages:layouts:extend': (layouts: NuxtLayout[]) => HookResult 'pages:layouts:extend': (layouts: NuxtLayout[]) => HookResult
// Auto imports // Auto imports
'autoImports:sources': (autoImportSources: AutoImportSource[]) => HookResult 'autoImports:sources': (presets: ImportPresetWithDeperection[]) => HookResult
'autoImports:extend': (autoImports: AutoImport[]) => HookResult 'autoImports:extend': (imports: Import[]) => HookResult
'autoImports:dirs': (dirs: string[]) => HookResult 'autoImports:dirs': (dirs: string[]) => HookResult
// Components // Components

View File

@ -1,66 +1,9 @@
export type IdentifierMap = Record<string, string> import { UnimportOptions } from 'unimport'
export type Identifiers = [string, string][]
export interface AutoImport { export interface AutoImportsOptions extends UnimportOptions {
/**
* Export name to be imported
*
*/
name: string
/**
* Import as this name
*/
as: string
/**
* Module specifier to import from
*/
from: string
/**
* Disable auto import
*/
disabled?: boolean
}
export interface AutoImportSource {
/**
* Exports from module for auto-import
*
*/
names: (string | { name: string, as?: string })[]
/**
* Module specifier to import from
*/
from: string
/**
* Disable auto import source
*/
disabled?: boolean
}
export interface AutoImportsOptions {
/**
* Auto import sources
*/
sources?: AutoImportSource[]
/**
* [experimental] Use globalThis injection instead of transform for development
*/
global?: boolean
/**
* Additional directories to scan composables from
*
* By default <rootDir>/composables is added
*/
dirs?: string[] dirs?: string[]
global?: boolean,
transform: { transform?: {
/**
* Exclusion patterns for transforming files
*
* By default [/node_modules/] will be used
*/
exclude?: RegExp[] exclude?: RegExp[]
} }
} }

View File

@ -2986,6 +2986,7 @@ __metadata:
semver: ^7.3.5 semver: ^7.3.5
unbuild: latest unbuild: latest
unctx: ^1.0.2 unctx: ^1.0.2
unimport: 0.0.8
untyped: ^0.4.2 untyped: ^0.4.2
languageName: unknown languageName: unknown
linkType: soft linkType: soft
@ -3148,6 +3149,7 @@ __metadata:
std-env: ^3.0.1 std-env: ^3.0.1
ufo: ^0.7.11 ufo: ^0.7.11
unbuild: latest unbuild: latest
unimport: 0.0.5
languageName: unknown languageName: unknown
linkType: soft linkType: soft
@ -13829,6 +13831,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"magic-string@npm:^0.26.0":
version: 0.26.0
resolution: "magic-string@npm:0.26.0"
dependencies:
sourcemap-codec: ^1.4.8
checksum: 2065de79cdbb76fe1b073244d51a2094cabf317e95a4ce385d7ddbe1d1f16b864c0f2a1ae514872251e64edc44bf56c8bb6a7dbe5eff9c9c236f877bcdfec9f9
languageName: node
linkType: hard
"magic-string@npm:^0.26.1": "magic-string@npm:^0.26.1":
version: 0.26.1 version: 0.26.1
resolution: "magic-string@npm:0.26.1" resolution: "magic-string@npm:0.26.1"
@ -15496,6 +15507,7 @@ __metadata:
scule: ^0.2.1 scule: ^0.2.1
ufo: ^0.7.11 ufo: ^0.7.11
unbuild: latest unbuild: latest
unimport: 0.0.8
unplugin: ^0.4.0 unplugin: ^0.4.0
untyped: ^0.4.2 untyped: ^0.4.2
vue: ^3.2.31 vue: ^3.2.31
@ -21184,6 +21196,34 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"unimport@npm:0.0.5":
version: 0.0.5
resolution: "unimport@npm:0.0.5"
dependencies:
"@rollup/pluginutils": ^4.1.2
escape-string-regexp: ^5.0.0
local-pkg: ^0.4.1
magic-string: ^0.26.0
mlly: ^0.4.3
unplugin: ^0.3.3
checksum: 6514320ff1771f699dc6c69b7a7086fe6a62ca1ece7a4877798e89964bf3b9b2450631266a49dfa5785ba8939e4f30517333e4639ecf44e97077fb3128a946d5
languageName: node
linkType: hard
"unimport@npm:0.0.8":
version: 0.0.8
resolution: "unimport@npm:0.0.8"
dependencies:
"@rollup/pluginutils": ^4.1.2
escape-string-regexp: ^5.0.0
local-pkg: ^0.4.1
magic-string: ^0.26.0
mlly: ^0.4.3
unplugin: ^0.3.3
checksum: ac6e2ef860b274c28093117c23071aaa35f484f88ff23cd25a4f4d114c24e72969f46f729b26023546ffdc1016ef42e5fc399d14389c0c0374f4a74b17ff1955
languageName: node
linkType: hard
"union-value@npm:^1.0.0": "union-value@npm:^1.0.0":
version: 1.0.1 version: 1.0.1
resolution: "union-value@npm:1.0.1" resolution: "union-value@npm:1.0.1"
@ -21303,7 +21343,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"unplugin@npm:^0.3.0, unplugin@npm:^0.3.2": "unplugin@npm:^0.3.0, unplugin@npm:^0.3.2, unplugin@npm:^0.3.3":
version: 0.3.3 version: 0.3.3
resolution: "unplugin@npm:0.3.3" resolution: "unplugin@npm:0.3.3"
dependencies: dependencies: