Skip to content
This repository has been archived by the owner on Apr 6, 2023. It is now read-only.

feat: auto-import for composables #1176

Merged
merged 15 commits into from
Oct 20, 2021
Merged
39 changes: 38 additions & 1 deletion docs/content/3.docs/2.directory-structure/5.composables.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,41 @@ head.title: Composables directory

# Composables directory

Nuxt will soon support a `composables/` directory to auto import your Vue composables into your application when used, learn more on the [open issue](https://github.com/nuxt/framework/issues/639).
Nuxt 3 supports `composables/` directory to auto import your Vue composables into your application and use using auto imports!


Example: (using named exports)

```js [composables/useFoo.ts]
import { useState } from '#app'

export const useFoo () {
return useState('foo', () => 'bar')
}
```

Example: (using default export)

```js [composables/use-foo.ts or composables/useFoo.ts]
import { useState } from '#app'

// It will be available as useFoo()
export default function () {
return 'foo'
}
```

You can now auto import it:

```vue [app.vue]
<template>
<div>
{{ foo }}
</div>
</template>

<script setup>
const foo = useFoo()
</script>
```

17 changes: 17 additions & 0 deletions examples/with-composables/app.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<template>
<div>
{{ a }}
{{ b }}
{{ c }}
{{ d }}
{{ foo }}
</div>
</template>

<script setup>
const a = useA()
const b = useB()
const c = useC()
const d = useD()
const foo = useFoo()
</script>
23 changes: 23 additions & 0 deletions examples/with-composables/composables/use-foo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useState } from '#app'

export function useA () {
return 'a'
}

function useB () {
return 'b'
}

function _useC () {
return 'c'
}

export const useD = () => {
return 'd'
}

export { useB, _useC as useC }

export default function () {
return useState('foo', () => 'bar')
}
4 changes: 4 additions & 0 deletions examples/with-composables/nuxt.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { defineNuxtConfig } from 'nuxt3'

export default defineNuxtConfig({
})
12 changes: 12 additions & 0 deletions examples/with-composables/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "example-with-composables",
"private": true,
"devDependencies": {
"nuxt3": "latest"
},
"scripts": {
"dev": "nuxt dev",
"build": "nuxt build",
"start": "node .output/server/index.mjs"
}
}
24 changes: 15 additions & 9 deletions packages/nuxi/src/commands/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,20 +74,26 @@ export default defineNuxtCommand({
// TODO: Watcher service, modules, and requireTree
const dLoad = debounce(load, 250)
const watcher = chokidar.watch([rootDir], { ignoreInitial: true, depth: 1 })
watcher.on('all', (_event, file) => {
watcher.on('all', (event, file) => {
if (!currentNuxt) { return }
if (file.startsWith(currentNuxt.options.buildDir)) { return }
if (file.match(/nuxt\.config\.(js|ts|mjs|cjs)$/)) {
dLoad(true, `${relative(rootDir, file)} updated`)
}
if (['addDir', 'unlinkDir'].includes(_event) && file.match(/pages$/)) {
dLoad(true, `Directory \`pages/\` ${_event === 'addDir' ? 'created' : 'removed'}`)
}
if (['addDir', 'unlinkDir'].includes(_event) && file.match(/components$/)) {
dLoad(true, `Directory \`components/\` ${_event === 'addDir' ? 'created' : 'removed'}`)
}
if (['add', 'unlink'].includes(_event) && file.match(/app\.(js|ts|mjs|jsx|tsx|vue)$/)) {
dLoad(true, `\`${relative(rootDir, file)}\` ${_event === 'add' ? 'created' : 'removed'}`)

const isDirChange = ['addDir', 'unlinkDir'].includes(event)
const isFileChange = ['add', 'unlink'].includes(event)
const reloadDirs = ['pages', 'components', 'composables']

if (isDirChange) {
const dir = reloadDirs.find(dir => file.endsWith(dir))
if (dir) {
dLoad(true, `Directory \`${dir}/\` ${event === 'addDir' ? 'created' : 'removed'}`)
}
} else if (isFileChange) {
if (file.match(/app\.(js|ts|mjs|jsx|tsx|vue)$/)) {
dLoad(true, `\`${relative(rootDir, file)}\` ${event === 'add' ? 'created' : 'removed'}`)
}
}
})

Expand Down
39 changes: 39 additions & 0 deletions packages/nuxt3/src/auto-imports/composables.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { promises as fsp, existsSync } from 'fs'
import { parse as parsePath, join } from 'pathe'
import globby from 'globby'
import { findExports } from 'mlly'
import { camelCase } from 'scule'
import { AutoImport } from '@nuxt/kit'
import { filterInPlace } from './utils'

export async function scanForComposables (dir: string, autoImports: AutoImport[]) {
if (!existsSync(dir)) { return }

const files = await globby(['*.{ts,js,tsx,jsx,mjs,cjs,mts,cts}'], { cwd: dir })

await Promise.all(
files.map(async (file) => {
const importPath = join(dir, file)

// Remove original entries from the same import (for build watcher)
filterInPlace(autoImports, i => i.from !== importPath)

const code = await fsp.readFile(join(dir, file), 'utf-8')
const exports = findExports(code)
const defaultExport = exports.find(i => i.type === 'default')

if (defaultExport) {
autoImports.push({ name: 'default', as: camelCase(parsePath(file).name), from: importPath })
}
for (const exp of exports) {
if (exp.type === 'named') {
for (const name of exp.names) {
autoImports.push({ name, as: name, from: importPath })
}
} else if (exp.type === 'declaration') {
autoImports.push({ name: exp.name, as: exp.name, from: importPath })
}
}
})
)
}
42 changes: 42 additions & 0 deletions packages/nuxt3/src/auto-imports/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { AutoImport } from '@nuxt/kit'

export interface AutoImportContext {
autoImports: AutoImport[]
matchRE: RegExp
map: Map<string, AutoImport>
}

export function createAutoImportContext (): AutoImportContext {
return {
autoImports: [],
map: new Map(),
matchRE: /__never__/
}
}

export function updateAutoImportContext (ctx: AutoImportContext) {
pi0 marked this conversation as resolved.
Show resolved Hide resolved
// 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 => i.as).join('|')})\\b`, 'g')

// Create map
ctx.map.clear()
for (const autoImport of ctx.autoImports) {
ctx.map.set(autoImport.as, autoImport)
}

return ctx
}
116 changes: 58 additions & 58 deletions packages/nuxt3/src/auto-imports/module.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { addVitePlugin, addWebpackPlugin, defineNuxtModule, addTemplate, resolveAlias, addPluginTemplate, AutoImport } from '@nuxt/kit'
import { addVitePlugin, addWebpackPlugin, defineNuxtModule, addTemplate, resolveAlias, addPluginTemplate, useNuxt } from '@nuxt/kit'
import type { AutoImportsOptions } from '@nuxt/kit'
import { isAbsolute, relative, resolve } from 'pathe'
import { isAbsolute, join, relative, resolve } from 'pathe'
import { TransformPlugin } from './transform'
import { Nuxt3AutoImports } from './imports'
import { scanForComposables } from './composables'
import { toImports } from './utils'
import { AutoImportContext, createAutoImportContext, updateAutoImportContext } from './context'

export default defineNuxtModule<AutoImportsOptions>({
name: 'auto-imports',
Expand All @@ -18,91 +21,88 @@ export default defineNuxtModule<AutoImportsOptions>({
// Filter disabled sources
options.sources = options.sources.filter(source => source.disabled !== true)

// Create a context to share state between module internals
const ctx = createAutoImportContext()

// 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 })
ctx.autoImports.push({ name: importName, as: importName, from: source.from })
} else {
autoImports.push({ name: importName.name, as: importName.as || importName.name, from: source.from })
ctx.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)
}
}
// User composables/ dir
const composablesDir = join(nuxt.options.srcDir, 'composables')

// Filter disabled imports
autoImports = autoImports.filter(i => i.disabled !== true)

// temporary disable #746
// @ts-ignore
// Transpile and injection
// @ts-ignore temporary disabled due to #746
if (nuxt.options.dev && options.global) {
// Add all imports to globalThis in development mode
addPluginTemplate({
filename: 'auto-imports.mjs',
src: '',
getContents: () => {
const imports = toImports(autoImports)
const globalThisSet = autoImports.map(i => `globalThis.${i.as} = ${i.as};`).join('\n')
const imports = toImports(ctx.autoImports)
const globalThisSet = ctx.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(autoImports))
addWebpackPlugin(TransformPlugin.webpack(autoImports))
addVitePlugin(TransformPlugin.vite(ctx))
addWebpackPlugin(TransformPlugin.webpack(ctx))
}

// Add types
const resolved = {}
const r = (id: string) => {
if (resolved[id]) { return resolved[id] }
let path = resolveAlias(id, nuxt.options.alias)
if (isAbsolute(path)) {
path = relative(nuxt.options.buildDir, path)
}
// Remove file extension for benefit of TypeScript
path = path.replace(/\.[a-z]+$/, '')
resolved[id] = path
return path
const updateAutoImports = async () => {
// Scan composables/
await scanForComposables(composablesDir, ctx.autoImports)
// Allow modules extending
await nuxt.callHook('autoImports:extend', ctx.autoImports)
// Update context
updateAutoImportContext(ctx)
// Generate types
generateDts(ctx)
}
await updateAutoImports()

addTemplate({
filename: 'auto-imports.d.ts',
write: true,
getContents: () => `// Generated by auto imports
declare global {
${autoImports.map(i => ` const ${i.as}: typeof import('${r(i.from)}')['${i.name}']`).join('\n')}
}\nexport {}`
})
nuxt.hook('prepare:types', ({ references }) => {
references.push({ path: resolve(nuxt.options.buildDir, 'auto-imports.d.ts') })
// Watch composables/ directory
nuxt.hook('builder:watch', async (_, path) => {
if (resolve(nuxt.options.srcDir, path).startsWith(composablesDir)) {
await updateAutoImports()
}
})
}
})

function toImports (autoImports: AutoImport[]) {
const map: Record<string, Set<string>> = {}
for (const autoImport of autoImports) {
if (!map[autoImport.from]) {
map[autoImport.from] = new Set()
function generateDts (ctx: AutoImportContext) {
const nuxt = useNuxt()

const resolved = {}
const r = (id: string) => {
if (resolved[id]) { return resolved[id] }
let path = resolveAlias(id, nuxt.options.alias)
if (isAbsolute(path)) {
path = relative(nuxt.options.buildDir, path)
}
map[autoImport.from].add(autoImport.as)
// Remove file extension for benefit of TypeScript
path = path.replace(/\.[a-z]+$/, '')
resolved[id] = path
return path
}
return Object.entries(map)
.map(([name, imports]) => `import { ${Array.from(imports).join(', ')} } from '${name}';`)
.join('\n')

addTemplate({
filename: 'auto-imports.d.ts',
write: true,
getContents: () => `// Generated by auto imports
declare global {
${ctx.autoImports.map(i => ` const ${i.as}: typeof import('${r(i.from)}')['${i.name}']`).join('\n')}
}

export {}
`
})
}
Loading