diff --git a/packages/bridge/src/auto-imports.ts b/packages/bridge/src/auto-imports.ts index 30b90f9158..145a5c9f33 100644 --- a/packages/bridge/src/auto-imports.ts +++ b/packages/bridge/src/auto-imports.ts @@ -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) } diff --git a/packages/kit/src/index.ts b/packages/kit/src/index.ts index 6df45e514c..4ef25390f2 100644 --- a/packages/kit/src/index.ts +++ b/packages/kit/src/index.ts @@ -21,3 +21,4 @@ export * from './types/hooks' export * from './types/module' export * from './types/nuxt' export * from './types/components' +export * from './types/imports' diff --git a/packages/kit/src/types/config.ts b/packages/kit/src/types/config.ts index 01903df424..2003f1ad04 100644 --- a/packages/kit/src/types/config.ts +++ b/packages/kit/src/types/config.ts @@ -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 { } diff --git a/packages/kit/src/types/hooks.ts b/packages/kit/src/types/hooks.ts index 77f2a52ea9..e27b71f537 100644 --- a/packages/kit/src/types/hooks.ts +++ b/packages/kit/src/types/hooks.ts @@ -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 @@ -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 diff --git a/packages/kit/src/types/imports.ts b/packages/kit/src/types/imports.ts new file mode 100644 index 0000000000..c46655dff4 --- /dev/null +++ b/packages/kit/src/types/imports.ts @@ -0,0 +1,49 @@ +export type IdentifierMap = Record +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 +} diff --git a/packages/nuxt3/src/auto-imports/identifiers.ts b/packages/nuxt3/src/auto-imports/identifiers.ts deleted file mode 100644 index 5ad7d45056..0000000000 --- a/packages/nuxt3/src/auto-imports/identifiers.ts +++ /dev/null @@ -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 - } -} diff --git a/packages/nuxt3/src/auto-imports/imports.ts b/packages/nuxt3/src/auto-imports/imports.ts new file mode 100644 index 0000000000..d05761ac7c --- /dev/null +++ b/packages/nuxt3/src/auto-imports/imports.ts @@ -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' + ] + } +] diff --git a/packages/nuxt3/src/auto-imports/module.ts b/packages/nuxt3/src/auto-imports/module.ts index 3cd9f8e437..e4f0e8516d 100644 --- a/packages/nuxt3/src/auto-imports/module.ts +++ b/packages/nuxt3/src/auto-imports/module.ts @@ -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({ 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({ 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> = {} - - 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') diff --git a/packages/nuxt3/src/auto-imports/transform.ts b/packages/nuxt3/src/auto-imports/transform.ts index f6d73abd2e..233bcee231 100644 --- a/packages/nuxt3/src/auto-imports/transform.ts +++ b/packages/nuxt3/src/auto-imports/transform.ts @@ -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() + 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] = [] } diff --git a/packages/nuxt3/src/auto-imports/types.ts b/packages/nuxt3/src/auto-imports/types.ts deleted file mode 100644 index 9e0ee41c18..0000000000 --- a/packages/nuxt3/src/auto-imports/types.ts +++ /dev/null @@ -1,7 +0,0 @@ -export type IdentifierMap = Record -export type Identifiers = [string, string][] - -export interface AutoImportsOptions { - identifiers?: IdentifierMap - disabled?: string[] -} diff --git a/packages/nuxt3/test/auto-imports.test.ts b/packages/nuxt3/test/auto-imports.test.ts index 6139a5f781..8bb19b29ca 100644 --- a/packages/nuxt3/test/auto-imports.test.ts +++ b/packages/nuxt3/test/auto-imports.test.ts @@ -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 () => {