feat(auto-imports): allow extending with config and hooks (#1167)

This commit is contained in:
pooya parsa 2021-10-18 15:39:53 +02:00 committed by GitHub
parent 807f4a325f
commit 0ab477cad0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 231 additions and 180 deletions

View File

@ -1,82 +1,28 @@
import { installModule, useNuxt } from '@nuxt/kit' import { installModule, useNuxt } from '@nuxt/kit'
import autoImports from '../../nuxt3/src/auto-imports/module' import autoImports from '../../nuxt3/src/auto-imports/module'
// TODO: implement these: https://github.com/nuxt/framework/issues/549 const UnsupportedImports = new Set(['useAsyncData', 'useFetch'])
const disabled = [
'useAsyncData',
'asyncData',
'useFetch'
]
const identifiers = { const ImportRewrites = {
'#app': [ vue: '@vue/composition-api',
'defineNuxtComponent', 'vue-router': '#app'
'useNuxtApp',
'defineNuxtPlugin',
'useRoute',
'useRouter',
'useRuntimeConfig',
'useState'
],
'#meta': [
'useMeta'
],
'@vue/composition-api': [
// lifecycle
'onActivated',
'onBeforeMount',
'onBeforeUnmount',
'onBeforeUpdate',
'onDeactivated',
'onErrorCaptured',
'onMounted',
'onServerPrefetch',
'onUnmounted',
'onUpdated',
// reactivity,
'computed',
'customRef',
'isReadonly',
'isRef',
'markRaw',
'reactive',
'readonly',
'ref',
'shallowReactive',
'shallowReadonly',
'shallowRef',
'toRaw',
'toRef',
'toRefs',
'triggerRef',
'unref',
'watch',
'watchEffect',
// component
'defineComponent',
'defineAsyncComponent',
'getCurrentInstance',
'h',
'inject',
'nextTick',
'provide',
'useCssModule'
]
}
const defaultIdentifiers = {}
for (const pkg in identifiers) {
for (const id of identifiers[pkg]) {
defaultIdentifiers[id] = pkg
}
} }
export async function setupAutoImports () { export async function setupAutoImports () {
const nuxt = useNuxt() const nuxt = useNuxt()
nuxt.options.autoImports = nuxt.options.autoImports || {}
nuxt.options.autoImports.disabled = nuxt.options.autoImports.disabled || disabled nuxt.hook('autoImports:extend', (autoImports) => {
nuxt.options.autoImports.identifiers = Object.assign({}, defaultIdentifiers, nuxt.options.autoImports.identifiers) for (const autoImport of autoImports) {
// Rewrite imports
if (autoImport.from in ImportRewrites) {
autoImport.from = ImportRewrites[autoImport.from]
}
// Disable unsupported imports
if (UnsupportedImports.has(autoImport.name)) {
autoImport.disabled = true
}
}
})
await installModule(nuxt, autoImports) await installModule(nuxt, autoImports)
} }

View File

@ -21,3 +21,4 @@ export * from './types/hooks'
export * from './types/module' export * from './types/module'
export * from './types/nuxt' export * from './types/nuxt'
export * from './types/components' export * from './types/components'
export * from './types/imports'

View File

@ -1,6 +1,7 @@
import { ConfigSchema as _ConfigSchema } from '../../schema/config' import { ConfigSchema as _ConfigSchema } from '../../schema/config'
import { ModuleInstallOptions } from './module' import { ModuleInstallOptions } from './module'
import { NuxtHooks } from './hooks' import { NuxtHooks } from './hooks'
import { AutoImportsOptions } from './imports'
import { ComponentsOptions } from './components' import { ComponentsOptions } from './components'
export interface ConfigSchema extends _ConfigSchema { export interface ConfigSchema extends _ConfigSchema {
@ -12,6 +13,7 @@ export interface ConfigSchema extends _ConfigSchema {
// TODO: Move to schema when untyped supports type annotation // TODO: Move to schema when untyped supports type annotation
vite: boolean | import('vite').InlineConfig vite: boolean | import('vite').InlineConfig
autoImports: AutoImportsOptions
} }
export interface NuxtOptions extends ConfigSchema { } export interface NuxtOptions extends ConfigSchema { }

View File

@ -1,9 +1,10 @@
import type { IncomingMessage, ServerResponse } from 'http' import type { IncomingMessage, ServerResponse } from 'http'
import type { Compiler, Configuration, Stats } from 'webpack' import type { Compiler, Configuration, Stats } from 'webpack'
import type { TSConfig } from 'pkg-types' import type { TSConfig } from 'pkg-types'
import type { NuxtConfig, NuxtOptions } from '..'
import type { ModuleContainer } from '../module/container' import type { ModuleContainer } from '../module/container'
import type { NuxtTemplate, Nuxt, NuxtApp } from './nuxt' import type { NuxtTemplate, Nuxt, NuxtApp } from '../types/nuxt'
import type { AutoImport, AutoImportSource } from '../types/imports'
import type { NuxtConfig, NuxtOptions } from './config'
import type { Component, ComponentsDir, ScanDir, ComponentsOptions } from './components' import type { Component, ComponentsDir, ScanDir, ComponentsOptions } from './components'
type HookResult = Promise<void> | void type HookResult = Promise<void> | void
@ -37,7 +38,11 @@ export interface NuxtHooks {
'app:templatesGenerated': (app: NuxtApp) => HookResult 'app:templatesGenerated': (app: NuxtApp) => HookResult
'builder:generateApp': () => HookResult 'builder:generateApp': () => HookResult
// components // Auto imports
'autoImports:sources': (autoImportSources: AutoImportSource[]) => HookResult
'autoImports:extend': (autoImports: AutoImport[]) => HookResult
// Components
'components:dirs': (dirs: ComponentsOptions['dirs']) => HookResult 'components:dirs': (dirs: ComponentsOptions['dirs']) => HookResult
'components:extend': (components: (Component | ComponentsDir | ScanDir)[]) => HookResult 'components:extend': (components: (Component | ComponentsDir | ScanDir)[]) => HookResult

View File

@ -0,0 +1,49 @@
export type IdentifierMap = Record<string, string>
export type Identifiers = [string, string][]
export interface AutoImport {
/**
* 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
}

View File

@ -1,68 +0,0 @@
const identifiers = {
'#app': [
'useAsyncData',
'defineNuxtComponent',
'useNuxtApp',
'defineNuxtPlugin',
'useRuntimeConfig',
'useState',
'useFetch'
],
'#meta': [
'useMeta'
],
vue: [
// lifecycle
'onActivated',
'onBeforeMount',
'onBeforeUnmount',
'onBeforeUpdate',
'onDeactivated',
'onErrorCaptured',
'onMounted',
'onServerPrefetch',
'onUnmounted',
'onUpdated',
// reactivity,
'computed',
'customRef',
'isReadonly',
'isRef',
'markRaw',
'reactive',
'readonly',
'ref',
'shallowReactive',
'shallowReadonly',
'shallowRef',
'toRaw',
'toRef',
'toRefs',
'triggerRef',
'unref',
'watch',
'watchEffect',
// component
'defineComponent',
'defineAsyncComponent',
'getCurrentInstance',
'h',
'inject',
'nextTick',
'provide',
'useCssModule'
],
'vue-router': [
'useRoute',
'useRouter'
]
}
export const defaultIdentifiers = {}
for (const pkg in identifiers) {
for (const id of identifiers[pkg]) {
defaultIdentifiers[id] = pkg
}
}

View File

@ -0,0 +1,79 @@
import type { AutoImportSource } from '@nuxt/kit'
export const Nuxt3AutoImports: AutoImportSource[] = [
// #app
{
from: '#app',
names: [
'useAsyncData',
'defineNuxtComponent',
'useNuxtApp',
'defineNuxtPlugin',
'useRuntimeConfig',
'useState',
'useFetch'
]
},
// #meta
{
from: '#meta',
names: [
'useMeta'
]
},
// vue-router
{
from: 'vue-router',
names: [
'useRoute',
'useRouter'
]
},
// vue
{
from: 'vue',
names: [
// Lifecycle
'onActivated',
'onBeforeMount',
'onBeforeUnmount',
'onBeforeUpdate',
'onDeactivated',
'onErrorCaptured',
'onMounted',
'onServerPrefetch',
'onUnmounted',
'onUpdated',
// Reactivity
'computed',
'customRef',
'isReadonly',
'isRef',
'markRaw',
'reactive',
'readonly',
'ref',
'shallowReactive',
'shallowReadonly',
'shallowRef',
'toRaw',
'toRef',
'toRefs',
'triggerRef',
'unref',
'watch',
'watchEffect',
// Component
'defineComponent',
'defineAsyncComponent',
'getCurrentInstance',
'h',
'inject',
'nextTick',
'provide',
'useCssModule'
]
}
]

View File

@ -1,35 +1,69 @@
import { addVitePlugin, addWebpackPlugin, defineNuxtModule, addTemplate, resolveAlias, addPluginTemplate } from '@nuxt/kit' import { addVitePlugin, addWebpackPlugin, defineNuxtModule, addTemplate, resolveAlias, addPluginTemplate, AutoImport } from '@nuxt/kit'
import type { AutoImportsOptions } from '@nuxt/kit'
import { isAbsolute, relative, resolve } from 'pathe' import { isAbsolute, relative, resolve } from 'pathe'
import type { Identifiers, AutoImportsOptions } from './types'
import { TransformPlugin } from './transform' import { TransformPlugin } from './transform'
import { defaultIdentifiers } from './identifiers' import { Nuxt3AutoImports } from './imports'
export default defineNuxtModule<AutoImportsOptions>({ export default defineNuxtModule<AutoImportsOptions>({
name: 'auto-imports', name: 'auto-imports',
configKey: 'autoImports', configKey: 'autoImports',
defaults: { identifiers: defaultIdentifiers }, defaults: {
setup ({ disabled = [], identifiers }, nuxt) { sources: Nuxt3AutoImports,
for (const key of disabled) { global: false
delete identifiers[key] },
async setup (options, nuxt) {
// Allow modules extending sources
await nuxt.callHook('autoImports:sources', options.sources)
// Filter disabled sources
options.sources = options.sources.filter(source => source.disabled !== true)
// Resolve autoimports from sources
let autoImports: AutoImport[] = []
for (const source of options.sources) {
for (const importName of source.names) {
if (typeof importName === 'string') {
autoImports.push({ name: importName, as: importName, from: source.from })
} else {
autoImports.push({ name: importName.name, as: importName.as || importName.name, from: source.from })
}
}
} }
// Allow modules extending resolved imports
await nuxt.callHook('autoImports:extend', autoImports)
// Disable duplicate auto imports
const usedNames = new Set()
for (const autoImport of 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 disabled imports
autoImports = autoImports.filter(i => i.disabled !== true)
// temporary disable #746 // temporary disable #746
// eslint-disable-next-line no-constant-condition // @ts-ignore
if (nuxt.options.dev && false) { if (nuxt.options.dev && options.global) {
// Add all imports to globalThis in development mode // Add all imports to globalThis in development mode
addPluginTemplate({ addPluginTemplate({
filename: 'auto-imports.mjs', filename: 'auto-imports.mjs',
src: '', src: '',
getContents: () => { getContents: () => {
const imports = toImports(Object.entries(identifiers)) const imports = toImports(autoImports)
const globalThisSet = Object.keys(identifiers).map(name => `globalThis.${name} = ${name};`).join('\n') const globalThisSet = autoImports.map(i => `globalThis.${i.as} = ${i.as};`).join('\n')
return `${imports}\n\n${globalThisSet}\n\nexport default () => {};` return `${imports}\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(identifiers)) addVitePlugin(TransformPlugin.vite(autoImports))
addWebpackPlugin(TransformPlugin.webpack(identifiers)) addWebpackPlugin(TransformPlugin.webpack(autoImports))
} }
// Add types // Add types
@ -51,7 +85,7 @@ export default defineNuxtModule<AutoImportsOptions>({
write: true, write: true,
getContents: () => `// Generated by auto imports getContents: () => `// Generated by auto imports
declare global { declare global {
${Object.entries(identifiers).map(([api, moduleName]) => ` const ${api}: typeof import('${r(moduleName)}')['${api}']`).join('\n')} ${autoImports.map(i => ` const ${i.as}: typeof import('${r(i.as)}')['${i.name}']`).join('\n')}
}\nexport {}` }\nexport {}`
}) })
nuxt.hook('prepare:types', ({ references }) => { nuxt.hook('prepare:types', ({ references }) => {
@ -60,16 +94,14 @@ ${Object.entries(identifiers).map(([api, moduleName]) => ` const ${api}: typeof
} }
}) })
function toImports (identifiers: Identifiers) { function toImports (autoImports: AutoImport[]) {
const map: Record<string, Set<string>> = {} const map: Record<string, Set<string>> = {}
for (const autoImport of autoImports) {
identifiers.forEach(([name, moduleName]) => { if (!map[autoImport.from]) {
if (!map[moduleName]) { map[autoImport.from] = new Set()
map[moduleName] = new Set() }
map[autoImport.from].add(autoImport.as)
} }
map[moduleName].add(name)
})
return Object.entries(map) return Object.entries(map)
.map(([name, imports]) => `import { ${Array.from(imports).join(', ')} } from '${name}';`) .map(([name, imports]) => `import { ${Array.from(imports).join(', ')} } from '${name}';`)
.join('\n') .join('\n')

View File

@ -1,6 +1,6 @@
import { createUnplugin } from 'unplugin' import { createUnplugin } from 'unplugin'
import { parseQuery, parseURL } from 'ufo' import { parseQuery, parseURL } from 'ufo'
import { IdentifierMap } from './types' import type { AutoImport } from '@nuxt/kit'
const excludeRE = [ const excludeRE = [
// imported from other module // imported from other module
@ -22,8 +22,14 @@ function stripeComments (code: string) {
.replace(singlelineCommentsRE, '') .replace(singlelineCommentsRE, '')
} }
export const TransformPlugin = createUnplugin((map: IdentifierMap) => { export const TransformPlugin = createUnplugin((autoImports: AutoImport[]) => {
const matchRE = new RegExp(`\\b(${Object.keys(map).join('|')})\\b`, 'g') const matchRE = new RegExp(`\\b(${autoImports.map(i => i.as).join('|')})\\b`, 'g')
// Create an internal map for faster lookup
const autoImportMap = new Map<string, AutoImport>()
for (const autoImport of autoImports) {
autoImportMap.set(autoImport.as, autoImport)
}
return { return {
name: 'nuxt-auto-imports-transform', name: 'nuxt-auto-imports-transform',
@ -76,7 +82,7 @@ export const TransformPlugin = createUnplugin((map: IdentifierMap) => {
// group by module name // group by module name
Array.from(matched).forEach((name) => { Array.from(matched).forEach((name) => {
const moduleName = map[name]! const moduleName = autoImportMap.get(name).from
if (!modules[moduleName]) { if (!modules[moduleName]) {
modules[moduleName] = [] modules[moduleName] = []
} }

View File

@ -1,7 +0,0 @@
export type IdentifierMap = Record<string, string>
export type Identifiers = [string, string][]
export interface AutoImportsOptions {
identifiers?: IdentifierMap
disabled?: string[]
}

View File

@ -1,13 +1,19 @@
import type { AutoImport } from '@nuxt/kit'
import { expect } from 'chai' import { expect } from 'chai'
import { TransformPlugin } from '../src/auto-imports/transform' import { TransformPlugin } from '../src/auto-imports/transform'
describe('module:auto-imports:build', () => { describe('auto-imports:transform', () => {
const { transform: _transform } = TransformPlugin.raw({ ref: 'vue', computed: 'bar' }, {} as any) const autoImports: AutoImport[] = [
const transform = (code: string) => _transform.call({} as any, code, '') { name: 'ref', as: 'ref', from: 'vue' },
{ name: 'computed', as: 'computed', from: 'bar' }
]
const transformPlugin = TransformPlugin.raw(autoImports, { framework: 'rollup' })
const transform = (code: string) => transformPlugin.transform.call({ error: null, warn: null }, 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)')).to.equal('import { ref } from \'vue\';const a = ref(0)')
expect(await transform('import { computed as ref } from "foo"; const a = ref(0)')).to.includes('import { computed } from \'bar\';') expect(await transform('import { computed as ref } from "foo"; const a = ref(0)')).to.include('import { computed } from \'bar\';')
}) })
it('should ignore existing imported', async () => { it('should ignore existing imported', async () => {