Skip to content

Commit

Permalink
feat: auto global imports (nuxt#410)
Browse files Browse the repository at this point in the history
Co-authored-by: Pooya Parsa <[email protected]>
  • Loading branch information
antfu and pi0 authored Aug 10, 2021
1 parent 50acd3d commit b2b4c64
Show file tree
Hide file tree
Showing 25 changed files with 274 additions and 25 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ jobs:
- name: Stub
run: yarn stub

- name: Test (unit)
run: yarn test:unit

- name: Test (presets)
run: yarn test:presets

Expand Down
6 changes: 1 addition & 5 deletions docs/content/2.app/3.data-fetching.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Nuxt provides `asyncData` to handle data fetching within you application.

## `asyncData`

Within your pages and components you can use `asyncData` to get access to data that resolves asynchronously.
Within your pages and components you can use `asyncData` to get access to data that resolves asynchronously.

### Usage

Expand Down Expand Up @@ -32,8 +32,6 @@ This helper only works with:
</template>
<script>
import { asyncData, defineNuxtComponent } from '@nuxt/app'
export default defineNuxtComponent({
setup () {
const { data } = asyncData('time', () => $fetch('/api/count'))
Expand All @@ -47,8 +45,6 @@ When using with the `<script setup>` syntax, an addition attribute `nuxt` is req

```vue
<script setup nuxt>
import { asyncData } from '@nuxt/app'
const { data } = asyncData('time', () => $fetch('/api/count'))
</script>
Expand Down
3 changes: 0 additions & 3 deletions docs/content/2.app/4.meta-tags.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@ Within your `setup()` function you can call `useMeta` with an object of meta pro

For example:
```ts
import { ref } from 'vue'
import { useMeta } from '@nuxt/app'

export default {
setup () {
useMeta({
Expand Down
2 changes: 0 additions & 2 deletions examples/async-data-setup/pages/index.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
<script nuxt setup lang="ts">
import { asyncData } from '@nuxt/app'
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { data } = asyncData('time', () => $fetch('/api/count'))
</script>
Expand Down
2 changes: 0 additions & 2 deletions examples/async-data/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
</template>

<script lang="ts">
import { defineNuxtComponent, asyncData } from '@nuxt/app'
export default defineNuxtComponent({
setup () {
const { data } = asyncData('time', () => $fetch('/api/count'))
Expand Down
3 changes: 0 additions & 3 deletions examples/async-data/pages/options.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@
</template>

<script lang="ts">
import { defineNuxtComponent } from '@nuxt/app'
import { ref } from 'vue'
export default defineNuxtComponent({
fetchKey: 'custom',
asyncData ({ route }) {
Expand Down
2 changes: 0 additions & 2 deletions examples/meta/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@
</template>

<script lang="ts">
import { ref } from 'vue'
import { useMeta } from '@nuxt/app'
export default {
setup () {
Expand Down
2 changes: 0 additions & 2 deletions examples/with-layouts/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@
</template>

<script>
import { defineNuxtComponent } from '@nuxt/app'
export default defineNuxtComponent({
layout: 'custom'
})
Expand Down
2 changes: 0 additions & 2 deletions examples/with-vue-content-loader/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@

<script lang="ts">
import { ContentLoader } from 'vue-content-loader'
import { defineNuxtComponent, asyncData } from '@nuxt/app'
export default defineNuxtComponent({
components: { ContentLoader },
setup () {
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"test": "yarn lint && yarn test:presets",
"test:presets": "mocha test/presets/*.mjs",
"test:compat": "TEST_COMPAT=1 yarn test:presets",
"test:unit": "mocha -r jiti/register packages/**/test/*.test.*",
"version": "yarn && git add yarn.lock"
},
"resolutions": {
Expand Down
8 changes: 8 additions & 0 deletions packages/global-imports/build.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { defineBuildConfig } from 'unbuild'

export default defineBuildConfig({
declaration: true,
entries: [
'src/module'
]
})
1 change: 1 addition & 0 deletions packages/global-imports/module.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('./dist/module')
28 changes: 28 additions & 0 deletions packages/global-imports/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "@nuxt/global-imports",
"version": "0.1.0",
"repository": "nuxt/framework",
"license": "MIT",
"exports": {
"./module": {
"import": "./dist/module.mjs",
"require": "./dist/module.js"
}
},
"types": "./dist/module.d.ts",
"files": [
"dist"
],
"scripts": {
"prepack": "unbuild"
},
"dependencies": {
"@nuxt/kit": "^0.6.4",
"ufo": "^0.7.7",
"unplugin": "^0.0.5",
"upath": "^2.0.1"
},
"devDependencies": {
"unbuild": "^0.4.2"
}
}
57 changes: 57 additions & 0 deletions packages/global-imports/src/identifiers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
const VueAPIs = [
// 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 nuxtComposition = [
'useAsyncData',
'asyncData',
'defineNuxtComponent',
'useNuxt',
'defineNuxtPlugin',
'useMeta'
]

export const defaultIdentifiers = Object.fromEntries([
...VueAPIs.map(name => [name, 'vue']),
...nuxtComposition.map(name => [name, '@nuxt/app'])
])
57 changes: 57 additions & 0 deletions packages/global-imports/src/module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { addVitePlugin, addWebpackPlugin, defineNuxtModule, addTemplate, addPluginTemplate } from '@nuxt/kit'
import { resolve } from 'upath'
import type { Identifiers, GlobalImportsOptions } from './types'
import { TrsnsformPlugin } from './transform'
import { defaultIdentifiers } from './identifiers'

export default defineNuxtModule<GlobalImportsOptions>({
name: 'global-imports',
configKey: 'globalImports',
defaults: { identifiers: defaultIdentifiers },
setup ({ identifiers }, nuxt) {
if (nuxt.options.dev) {
// Add all imports to globalThis in development mode
addPluginTemplate({
filename: 'global-imports.mjs',
src: '',
getContents: () => {
const imports = toImports(Object.entries(identifiers))
const globalThisSet = Object.keys(identifiers).map(name => `globalThis.${name} = ${name};`).join('\n')
return `${imports}\n\n${globalThisSet}\n\nexport default () => {};`
}
})
} else {
// Transform to inject imports in production mode
addVitePlugin(TrsnsformPlugin.vite(identifiers))
addWebpackPlugin(TrsnsformPlugin.webpack(identifiers))
}

// Add types
addTemplate({
filename: 'global-imports.d.ts',
write: true,
getContents: () => `// Generated by global imports
declare global {
${Object.entries(identifiers).map(([api, moduleName]) => ` const ${api}: typeof import('${moduleName}')['${api}']`).join('\n')}
}\nexport {}`
})
nuxt.hook('prepare:types', ({ references }) => {
references.push({ path: resolve(nuxt.options.buildDir, 'global-imports.d.ts') })
})
}
})

function toImports (identifiers: Identifiers) {
const map: Record<string, Set<string>> = {}

identifiers.forEach(([name, moduleName]) => {
if (!map[moduleName]) {
map[moduleName] = new Set()
}
map[moduleName].add(name)
})

return Object.entries(map)
.map(([name, imports]) => `import { ${Array.from(imports).join(', ')} } from '${name}';`)
.join('\n')
}
68 changes: 68 additions & 0 deletions packages/global-imports/src/transform.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { createUnplugin } from 'unplugin'
import { parseQuery, parseURL } from 'ufo'
import { IdentifierMap } from './types'

const excludeRegex = [
// imported from other module
/\bimport\s*\{([\s\S]*?)\}\s*from\b/g,
// defined as function
/\bfunction\s*([\s\S]+?)\s*\(/g,
// defined as local variable
/\b(?:const|let|var)\s*([\w\d_$]+?)\b/g
]

export const TrsnsformPlugin = createUnplugin((map: IdentifierMap) => {
const regex = new RegExp('\\b(' + (Object.keys(map).join('|')) + ')\\b', 'g')

return {
name: 'nuxt-global-imports-transform',
enforce: 'post',
transformInclude (id) {
const { pathname, search } = parseURL(id)
const query = parseQuery(search)

if (id.includes('node_modules')) {
return false
}

// vue files
if (pathname.endsWith('.vue') && (query.type === 'template' || !search)) {
return true
}

// js files
if (pathname.match(/\.((c|m)?j|t)sx?/g)) {
return true
}
},
transform (code) {
// find all possible injection
const matched = new Set(Array.from(code.matchAll(regex)).map(i => i[1]))

// remove those already defined
for (const regex of excludeRegex) {
Array.from(code.matchAll(regex))
.flatMap(i => i[1]?.split(',') || [])
.forEach(i => matched.delete(i.trim()))
}

const modules: Record<string, string[]> = {}

// group by module name
Array.from(matched).forEach((name) => {
const moduleName = map[name]!
if (!modules[moduleName]) {
modules[moduleName] = []
}
modules[moduleName].push(name)
})

// stringify import
const imports = Object.entries(modules)
.map(([moduleName, names]) => `import { ${names.join(',')} } from '${moduleName}';`)
.join('')

return imports + code
}
}
})
6 changes: 6 additions & 0 deletions packages/global-imports/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type IdentifierMap = Record<string, string>
export type Identifiers = [string, string][]

export interface GlobalImportsOptions {
identifiers?: IdentifierMap
}
16 changes: 16 additions & 0 deletions packages/global-imports/test/build.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { expect } from 'chai'
import { TrsnsformPlugin } from '../src/transform'

describe('module:global-imports:build', () => {
const { transform } = TrsnsformPlugin.raw({ ref: 'vue' })

it('should correct inject', () => {
expect(transform('const a = ref(0)', ''))
.to.equal('import { ref } from \'vue\';const a = ref(0)')
})

it('should ignore imported', () => {
expect(transform('import { ref } from "foo";const a = ref(0)', ''))
.to.equal('import { ref } from "foo";const a = ref(0)')
})
})
5 changes: 5 additions & 0 deletions packages/kit/src/module/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ export function normalizeTemplate (template: NuxtTemplate | string): NuxtTemplat
// Normalize
if (typeof template === 'string') {
template = { src: template }
} else {
template = { ...template }
}

// Use src if provided
Expand Down Expand Up @@ -74,7 +76,10 @@ export function normalizePlugin (plugin: NuxtPlugin | string): NuxtPlugin {
// Normalize src
if (typeof plugin === 'string') {
plugin = { src: plugin }
} else {
plugin = { ...plugin }
}

if (!plugin.src) {
throw new Error('Invalid plugin. src option is required: ' + JSON.stringify(plugin))
}
Expand Down
1 change: 1 addition & 0 deletions packages/nuxt3/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"dependencies": {
"@nuxt/app": "^0.5.0",
"@nuxt/component-discovery": "^0.2.0",
"@nuxt/global-imports": "^0.1.0",
"@nuxt/kit": "^0.6.4",
"@nuxt/meta": "^0.1.0",
"@nuxt/nitro": "^0.9.1",
Expand Down
1 change: 1 addition & 0 deletions packages/nuxt3/src/nuxt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export async function loadNuxt (opts: LoadNuxtOptions): Promise<Nuxt> {
options.buildModules.push(normalize(require.resolve('@nuxt/pages/module')))
options.buildModules.push(normalize(require.resolve('@nuxt/meta/module')))
options.buildModules.push(normalize(require.resolve('@nuxt/component-discovery/module')))
options.buildModules.push(normalize(require.resolve('@nuxt/global-imports/module')))

const nuxt = createNuxt(options)

Expand Down
Loading

0 comments on commit b2b4c64

Please sign in to comment.