feat: auto global imports (#410)

Co-authored-by: Pooya Parsa <pyapar@gmail.com>
This commit is contained in:
Anthony Fu 2021-08-10 08:27:23 +08:00 committed by GitHub
parent 50acd3dda5
commit b2b4c64807
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 274 additions and 25 deletions

View File

@ -51,6 +51,9 @@ jobs:
- name: Stub
run: yarn stub
- name: Test (unit)
run: yarn test:unit
- name: Test (presets)
run: yarn test:presets

View File

@ -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
@ -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'))
@ -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>

View File

@ -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({

View File

@ -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>

View File

@ -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'))

View File

@ -9,9 +9,6 @@
</template>
<script lang="ts">
import { defineNuxtComponent } from '@nuxt/app'
import { ref } from 'vue'
export default defineNuxtComponent({
fetchKey: 'custom',
asyncData ({ route }) {

View File

@ -15,8 +15,6 @@
</template>
<script lang="ts">
import { ref } from 'vue'
import { useMeta } from '@nuxt/app'
export default {
setup () {

View File

@ -12,8 +12,6 @@
</template>
<script>
import { defineNuxtComponent } from '@nuxt/app'
export default defineNuxtComponent({
layout: 'custom'
})

View File

@ -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 () {

View File

@ -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": {

View File

@ -0,0 +1,8 @@
import { defineBuildConfig } from 'unbuild'
export default defineBuildConfig({
declaration: true,
entries: [
'src/module'
]
})

View File

@ -0,0 +1 @@
module.exports = require('./dist/module')

View File

@ -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"
}
}

View File

@ -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'])
])

View File

@ -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')
}

View File

@ -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
}
}
})

View File

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

View File

@ -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)')
})
})

View File

@ -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
@ -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))
}

View File

@ -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",

View File

@ -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)

View File

@ -1,7 +1,13 @@
// Declarations auto generated by `nuxt prepare`. Please do not manually modify this file.
// This file is auto generated by `nuxt prepare`
// Please do not manually modify this file.
/// <reference types="@nuxt/kit" />
/// <reference types="@nuxt/app" />
/// <reference types="@nuxt/nitro" />
/// <reference types="@nuxt/pages" />
/// <reference types="@nuxt/meta" />
/// <reference types="@nuxt/component-discovery" />
/// <reference types="@nuxt/global-imports" />
/// <reference path=".nuxt/components.d.ts" />
/// <reference path=".nuxt/global-imports.d.ts" />
export {}

View File

@ -5,8 +5,6 @@
</template>
<script>
import { defineNuxtComponent } from '@nuxt/app'
export default defineNuxtComponent({
})
</script>

View File

@ -14,7 +14,7 @@
"mocha",
"chai",
"@nuxt/app",
"@nuxt/nitro",
"@nuxt/nitro"
]
},
"exclude": ["./packages/*/dist/*"]

View File

@ -1335,6 +1335,18 @@ __metadata:
languageName: node
linkType: hard
"@nuxt/global-imports@^0.1.0, @nuxt/global-imports@workspace:packages/global-imports":
version: 0.0.0-use.local
resolution: "@nuxt/global-imports@workspace:packages/global-imports"
dependencies:
"@nuxt/kit": ^0.6.4
ufo: ^0.7.7
unbuild: ^0.4.2
unplugin: ^0.0.5
upath: ^2.0.1
languageName: unknown
linkType: soft
"@nuxt/kit@^0.6.4, @nuxt/kit@workspace:packages/kit":
version: 0.0.0-use.local
resolution: "@nuxt/kit@workspace:packages/kit"
@ -8735,6 +8747,7 @@ fsevents@~2.3.2:
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