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 autoImports from '../../nuxt3/src/auto-imports/module'
// TODO: implement these: https://github.com/nuxt/framework/issues/549
const disabled = [
'useAsyncData',
'asyncData',
'useFetch'
]
const UnsupportedImports = new Set(['useAsyncData', 'useFetch'])
const identifiers = {
'#app': [
'defineNuxtComponent',
'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
}
const ImportRewrites = {
vue: '@vue/composition-api',
'vue-router': '#app'
}
export async function setupAutoImports () {
const nuxt = useNuxt()
nuxt.options.autoImports = nuxt.options.autoImports || {}
nuxt.options.autoImports.disabled = nuxt.options.autoImports.disabled || disabled
nuxt.options.autoImports.identifiers = Object.assign({}, defaultIdentifiers, nuxt.options.autoImports.identifiers)
nuxt.hook('autoImports:extend', (autoImports) => {
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)
}

View File

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

View File

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

View File

@ -1,9 +1,10 @@
import type { IncomingMessage, ServerResponse } from 'http'
import type { Compiler, Configuration, Stats } from 'webpack'
import type { TSConfig } from 'pkg-types'
import type { NuxtConfig, NuxtOptions } from '..'
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'
type HookResult = Promise<void> | void
@ -37,7 +38,11 @@ export interface NuxtHooks {
'app:templatesGenerated': (app: NuxtApp) => 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: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 type { Identifiers, AutoImportsOptions } from './types'
import { TransformPlugin } from './transform'
import { defaultIdentifiers } from './identifiers'
import { Nuxt3AutoImports } from './imports'
export default defineNuxtModule<AutoImportsOptions>({
name: 'auto-imports',
configKey: 'autoImports',
defaults: { identifiers: defaultIdentifiers },
setup ({ disabled = [], identifiers }, nuxt) {
for (const key of disabled) {
delete identifiers[key]
defaults: {
sources: Nuxt3AutoImports,
global: false
},
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
// eslint-disable-next-line no-constant-condition
if (nuxt.options.dev && false) {
// @ts-ignore
if (nuxt.options.dev && options.global) {
// Add all imports to globalThis in development mode
addPluginTemplate({
filename: 'auto-imports.mjs',
src: '',
getContents: () => {
const imports = toImports(Object.entries(identifiers))
const globalThisSet = Object.keys(identifiers).map(name => `globalThis.${name} = ${name};`).join('\n')
const imports = toImports(autoImports)
const globalThisSet = autoImports.map(i => `globalThis.${i.as} = ${i.as};`).join('\n')
return `${imports}\n\n${globalThisSet}\n\nexport default () => {};`
}
})
} else {
// Transform to inject imports in production mode
addVitePlugin(TransformPlugin.vite(identifiers))
addWebpackPlugin(TransformPlugin.webpack(identifiers))
addVitePlugin(TransformPlugin.vite(autoImports))
addWebpackPlugin(TransformPlugin.webpack(autoImports))
}
// Add types
@ -51,7 +85,7 @@ export default defineNuxtModule<AutoImportsOptions>({
write: true,
getContents: () => `// Generated by auto imports
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 {}`
})
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>> = {}
identifiers.forEach(([name, moduleName]) => {
if (!map[moduleName]) {
map[moduleName] = new Set()
for (const autoImport of autoImports) {
if (!map[autoImport.from]) {
map[autoImport.from] = new Set()
}
map[moduleName].add(name)
})
map[autoImport.from].add(autoImport.as)
}
return Object.entries(map)
.map(([name, imports]) => `import { ${Array.from(imports).join(', ')} } from '${name}';`)
.join('\n')

View File

@ -1,6 +1,6 @@
import { createUnplugin } from 'unplugin'
import { parseQuery, parseURL } from 'ufo'
import { IdentifierMap } from './types'
import type { AutoImport } from '@nuxt/kit'
const excludeRE = [
// imported from other module
@ -22,8 +22,14 @@ function stripeComments (code: string) {
.replace(singlelineCommentsRE, '')
}
export const TransformPlugin = createUnplugin((map: IdentifierMap) => {
const matchRE = new RegExp(`\\b(${Object.keys(map).join('|')})\\b`, 'g')
export const TransformPlugin = createUnplugin((autoImports: AutoImport[]) => {
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 {
name: 'nuxt-auto-imports-transform',
@ -76,7 +82,7 @@ export const TransformPlugin = createUnplugin((map: IdentifierMap) => {
// group by module name
Array.from(matched).forEach((name) => {
const moduleName = map[name]!
const moduleName = autoImportMap.get(name).from
if (!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 { TransformPlugin } from '../src/auto-imports/transform'
describe('module:auto-imports:build', () => {
const { transform: _transform } = TransformPlugin.raw({ ref: 'vue', computed: 'bar' }, {} as any)
const transform = (code: string) => _transform.call({} as any, code, '')
describe('auto-imports:transform', () => {
const autoImports: AutoImport[] = [
{ 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 () => {
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 () => {