feat(nuxt): support custom keyed composables (#19490)

This commit is contained in:
Daniel Roe 2023-03-07 21:06:15 +00:00 committed by GitHub
parent faeffcb963
commit 60d07df4cc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 58 additions and 10 deletions

View File

@ -132,6 +132,27 @@ export default defineUntypedSchema({
* Build time optimization configuration. * Build time optimization configuration.
*/ */
optimization: { optimization: {
/**
* Functions to inject a key for.
*
* As long as the number of arguments passed to the function is less than `argumentLength`, an
* additional magic string will be injected that can be used to deduplicate requests between server
* and client. You will need to take steps to handle this additional key.
*
* The key will be unique based on the location of the function being invoked within the file.
*
* @type {Array<{ name: string, argumentLength: number }>}
*/
keyedComposables: {
$resolve: (val) => [
{ name: 'useState', argumentLength: 2 },
{ name: 'useFetch', argumentLength: 3 },
{ name: 'useAsyncData', argumentLength: 3 },
{ name: 'useLazyAsyncData', argumentLength: 3 },
{ name: 'useLazyFetch', argumentLength: 3 },
].concat(val).filter(Boolean)
},
/** /**
* Tree shake code from specific builds. * Tree shake code from specific builds.
*/ */

View File

@ -7,20 +7,24 @@ import MagicString from 'magic-string'
import { hash } from 'ohash' import { hash } from 'ohash'
import type { CallExpression } from 'estree' import type { CallExpression } from 'estree'
import { parseQuery, parseURL } from 'ufo' import { parseQuery, parseURL } from 'ufo'
import escapeRE from 'escape-string-regexp'
import { findStaticImports, parseStaticImport } from 'mlly' import { findStaticImports, parseStaticImport } from 'mlly'
export interface ComposableKeysOptions { export interface ComposableKeysOptions {
sourcemap: boolean sourcemap: boolean
rootDir: string rootDir: string
composables: Array<{ name: string, argumentLength: number }>
} }
const keyedFunctions = [
'useState', 'useFetch', 'useAsyncData', 'useLazyAsyncData', 'useLazyFetch'
]
const KEYED_FUNCTIONS_RE = new RegExp(`(${keyedFunctions.join('|')})`)
const stringTypes = ['Literal', 'TemplateLiteral'] const stringTypes = ['Literal', 'TemplateLiteral']
export const composableKeysPlugin = createUnplugin((options: ComposableKeysOptions) => { export const composableKeysPlugin = createUnplugin((options: ComposableKeysOptions) => {
const composableMeta = Object.fromEntries(options.composables.map(({ name, ...meta }) => [name, meta]))
const maxLength = Math.max(...options.composables.map(({ argumentLength }) => argumentLength))
const keyedFunctions = new Set(options.composables.map(({ name }) => name))
const KEYED_FUNCTIONS_RE = new RegExp(`\\b(${[...keyedFunctions].map(f => escapeRE(f)).join('|')})\\b`)
return { return {
name: 'nuxt:composable-keys', name: 'nuxt:composable-keys',
enforce: 'post', enforce: 'post',
@ -44,24 +48,28 @@ export const composableKeysPlugin = createUnplugin((options: ComposableKeysOptio
if (_node.type !== 'CallExpression' || (_node as CallExpression).callee.type !== 'Identifier') { return } if (_node.type !== 'CallExpression' || (_node as CallExpression).callee.type !== 'Identifier') { return }
const node: CallExpression = _node as CallExpression const node: CallExpression = _node as CallExpression
const name = 'name' in node.callee && node.callee.name const name = 'name' in node.callee && node.callee.name
if (!name || !keyedFunctions.includes(name) || node.arguments.length >= 4) { return } if (!name || !keyedFunctions.has(name) || node.arguments.length >= maxLength) { return }
imports = imports || detectImportNames(script) imports = imports || detectImportNames(script)
if (imports.has(name)) { return } if (imports.has(name)) { return }
const meta = composableMeta[name]
if (node.arguments.length >= meta.argumentLength) { return }
switch (name) { switch (name) {
case 'useState': case 'useState':
if (node.arguments.length >= 2 || stringTypes.includes(node.arguments[0]?.type)) { return } if (stringTypes.includes(node.arguments[0]?.type)) { return }
break break
case 'useFetch': case 'useFetch':
case 'useLazyFetch': case 'useLazyFetch':
if (node.arguments.length >= 3 || stringTypes.includes(node.arguments[1]?.type)) { return } if (stringTypes.includes(node.arguments[1]?.type)) { return }
break break
case 'useAsyncData': case 'useAsyncData':
case 'useLazyAsyncData': case 'useLazyAsyncData':
if (node.arguments.length >= 3 || stringTypes.includes(node.arguments[0]?.type) || stringTypes.includes(node.arguments[node.arguments.length - 1]?.type)) { return } if (stringTypes.includes(node.arguments[0]?.type) || stringTypes.includes(node.arguments[node.arguments.length - 1]?.type)) { return }
break break
} }

View File

@ -80,7 +80,11 @@ export async function bundle (nuxt: Nuxt) {
} }
}, },
plugins: [ plugins: [
composableKeysPlugin.vite({ sourcemap: nuxt.options.sourcemap.server || nuxt.options.sourcemap.client, rootDir: nuxt.options.rootDir }), composableKeysPlugin.vite({
sourcemap: nuxt.options.sourcemap.server || nuxt.options.sourcemap.client,
rootDir: nuxt.options.rootDir,
composables: nuxt.options.optimization.keyedComposables
}),
replace({ replace({
...Object.fromEntries([';', '(', '{', '}', ' ', '\t', '\n'].map(d => [`${d}global.`, `${d}globalThis.`])), ...Object.fromEntries([';', '(', '{', '}', ' ', '\t', '\n'].map(d => [`${d}global.`, `${d}globalThis.`])),
preventAssignment: true preventAssignment: true

View File

@ -46,7 +46,8 @@ export async function bundle (nuxt: Nuxt) {
} }
config.plugins!.push(composableKeysPlugin.webpack({ config.plugins!.push(composableKeysPlugin.webpack({
sourcemap: nuxt.options.sourcemap[config.name as 'client' | 'server'], sourcemap: nuxt.options.sourcemap[config.name as 'client' | 'server'],
rootDir: nuxt.options.rootDir rootDir: nuxt.options.rootDir,
composables: nuxt.options.optimization.keyedComposables
})) }))
// Create compiler // Create compiler

View File

@ -49,6 +49,14 @@ export default defineNuxtConfig({
] ]
} }
}, },
optimization: {
keyedComposables: [
{
name: 'useKeyedComposable',
argumentLength: 1
}
]
},
runtimeConfig: { runtimeConfig: {
baseURL: '', baseURL: '',
baseAPIToken: '', baseAPIToken: '',

View File

@ -32,6 +32,11 @@ const { data: useFetchTest2 } = await useLocalFetch()
const useLocalLazyFetch = () => useLazyFetch(() => '/api/counter') const useLocalLazyFetch = () => useLazyFetch(() => '/api/counter')
const { data: useLazyFetchTest1 } = await useLocalLazyFetch() const { data: useLazyFetchTest1 } = await useLocalLazyFetch()
const { data: useLazyFetchTest2 } = await useLocalLazyFetch() const { data: useLazyFetchTest2 } = await useLocalLazyFetch()
const useKeyedComposable = (arg?: string) => arg
const useLocalKeyedComposable = () => useKeyedComposable()
const useMyAsyncDataTest1 = useLocalKeyedComposable()
const useMyAsyncDataTest2 = useLocalKeyedComposable()
</script> </script>
<template> <template>
@ -41,6 +46,7 @@ const { data: useLazyFetchTest2 } = await useLocalLazyFetch()
{{ useLazyAsyncDataTest1 === useLazyAsyncDataTest2 }} {{ useLazyAsyncDataTest1 === useLazyAsyncDataTest2 }}
{{ useFetchTest1 === useFetchTest2 }} {{ useFetchTest1 === useFetchTest2 }}
{{ useLazyFetchTest1 === useLazyFetchTest2 }} {{ useLazyFetchTest1 === useLazyFetchTest2 }}
{{ !!useMyAsyncDataTest1 && useMyAsyncDataTest1 === useMyAsyncDataTest2 }}
</div> </div>
</template> </template>