mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-22 05:35:13 +00:00
feat(nuxt): automatically generate unique keys for keyed composables (#4955)
Co-authored-by: Pooya Parsa <pyapar@gmail.com>
This commit is contained in:
parent
4d607080f5
commit
23546a270c
@ -22,6 +22,7 @@
|
|||||||
"jsdoc/require-param": "off",
|
"jsdoc/require-param": "off",
|
||||||
"jsdoc/require-returns": "off",
|
"jsdoc/require-returns": "off",
|
||||||
"jsdoc/require-param-type": "off",
|
"jsdoc/require-param-type": "off",
|
||||||
|
"no-redeclare": "off",
|
||||||
"import/no-restricted-paths": [
|
"import/no-restricted-paths": [
|
||||||
"error",
|
"error",
|
||||||
{
|
{
|
||||||
|
@ -5,13 +5,17 @@ Within your pages, components, and plugins you can use useAsyncData to get acces
|
|||||||
## Type
|
## Type
|
||||||
|
|
||||||
```ts [Signature]
|
```ts [Signature]
|
||||||
|
function useAsyncData(
|
||||||
|
handler: (nuxtApp?: NuxtApp) => Promise<DataT>,
|
||||||
|
options?: AsyncDataOptions<DataT>
|
||||||
|
): AsyncData<DataT>
|
||||||
function useAsyncData(
|
function useAsyncData(
|
||||||
key: string,
|
key: string,
|
||||||
handler: (nuxtApp?: NuxtApp) => Promise<DataT>,
|
handler: (nuxtApp?: NuxtApp) => Promise<DataT>,
|
||||||
options?: AsyncDataOptions
|
options?: AsyncDataOptions<DataT>
|
||||||
): Promise<DataT>
|
): Promise<AsyncData<DataT>>
|
||||||
|
|
||||||
type AsyncDataOptions = {
|
type AsyncDataOptions<DataT> = {
|
||||||
server?: boolean
|
server?: boolean
|
||||||
lazy?: boolean
|
lazy?: boolean
|
||||||
default?: () => DataT | Ref<DataT>
|
default?: () => DataT | Ref<DataT>
|
||||||
@ -21,7 +25,7 @@ type AsyncDataOptions = {
|
|||||||
initialCache?: boolean
|
initialCache?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type DataT = {
|
type AsyncData<DataT> = {
|
||||||
data: Ref<DataT>
|
data: Ref<DataT>
|
||||||
pending: Ref<boolean>
|
pending: Ref<boolean>
|
||||||
refresh: () => Promise<void>
|
refresh: () => Promise<void>
|
||||||
@ -31,7 +35,7 @@ type DataT = {
|
|||||||
|
|
||||||
## Params
|
## Params
|
||||||
|
|
||||||
* **key**: a unique key to ensure that data fetching can be properly de-duplicated across requests
|
* **key**: a unique key to ensure that data fetching can be properly de-duplicated across requests. If you do not provide a key, then a key that is unique to the file name and line number of the instance of `useAsyncData` will be generated for you.
|
||||||
* **handler**: an asynchronous function that returns a value
|
* **handler**: an asynchronous function that returns a value
|
||||||
* **options**:
|
* **options**:
|
||||||
* _lazy_: whether to resolve the async function after loading the route, instead of blocking navigation (defaults to `false`)
|
* _lazy_: whether to resolve the async function after loading the route, instead of blocking navigation (defaults to `false`)
|
||||||
|
@ -6,9 +6,9 @@ This composable provides a convenient wrapper around [`useAsyncData`](/api/compo
|
|||||||
|
|
||||||
```ts [Signature]
|
```ts [Signature]
|
||||||
function useFetch(
|
function useFetch(
|
||||||
url: string | Request,
|
url: string | Request | Ref<string | Request> | () => string | Request,
|
||||||
options?: UseFetchOptions
|
options?: UseFetchOptions<DataT>
|
||||||
): Promise<DataT>
|
): Promise<AsyncData<DataT>>
|
||||||
|
|
||||||
type UseFetchOptions = {
|
type UseFetchOptions = {
|
||||||
key?: string,
|
key?: string,
|
||||||
@ -25,7 +25,7 @@ type UseFetchOptions = {
|
|||||||
watch?: WatchSource[]
|
watch?: WatchSource[]
|
||||||
}
|
}
|
||||||
|
|
||||||
type DataT = {
|
type AsyncData<DataT> = {
|
||||||
data: Ref<DataT>
|
data: Ref<DataT>
|
||||||
pending: Ref<boolean>
|
pending: Ref<boolean>
|
||||||
refresh: () => Promise<void>
|
refresh: () => Promise<void>
|
||||||
@ -51,6 +51,10 @@ type DataT = {
|
|||||||
* `watch`: watch reactive sources to auto-refresh
|
* `watch`: watch reactive sources to auto-refresh
|
||||||
* `transform`: A function that can be used to alter `handler` function result after resolving.
|
* `transform`: A function that can be used to alter `handler` function result after resolving.
|
||||||
|
|
||||||
|
::alert{type=warning}
|
||||||
|
If you provide a function or ref as the `url` parameter, or if you provide functions as arguments to the `options` parameter, then the `useFetch` call will not match other `useFetch` calls elsewhere in your codebase, even if the options seem to be identical. If you wish to force a match, you may provide your own key in `options`.
|
||||||
|
::
|
||||||
|
|
||||||
## Return values
|
## Return values
|
||||||
|
|
||||||
* **data**: the result of the asynchronous function that is passed in
|
* **data**: the result of the asynchronous function that is passed in
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
# `useState`
|
# `useState`
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
|
useState<T>(init?: () => T | Ref<T>): Ref<T>
|
||||||
useState<T>(key: string, init?: () => T | Ref<T>): Ref<T>
|
useState<T>(key: string, init?: () => T | Ref<T>): Ref<T>
|
||||||
```
|
```
|
||||||
|
|
||||||
* **key**: A unique key ensuring that data fetching is properly de-duplicated across requests
|
* **key**: A unique key ensuring that data fetching is properly de-duplicated across requests. If you do not provide a key, then a key that is unique to the file and line number of the instance of `useState` will be generated for you.
|
||||||
* **init**: A function that provides initial value for the state when not initiated. This function can also return a `Ref`.
|
* **init**: A function that provides initial value for the state when not initiated. This function can also return a `Ref`.
|
||||||
* **T**: (typescript only) Specify the type of state
|
* **T**: (typescript only) Specify the type of state
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
const ctr = ref(0)
|
const ctr = ref(0)
|
||||||
const { data, pending, refresh } = await useAsyncData('/api/hello', () => $fetch(`/api/hello/${ctr.value}`), { watch: [ctr] })
|
const { data, pending, refresh } = await useAsyncData(() => $fetch(`/api/hello/${ctr.value}`), { watch: [ctr] })
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -20,8 +20,8 @@
|
|||||||
"lint": "eslint --ext .vue,.ts,.js,.mjs .",
|
"lint": "eslint --ext .vue,.ts,.js,.mjs .",
|
||||||
"lint:docs": "markdownlint ./docs/content && case-police 'docs/content**/*.md'",
|
"lint:docs": "markdownlint ./docs/content && case-police 'docs/content**/*.md'",
|
||||||
"lint:docs:fix": "markdownlint ./docs/content --fix && case-police 'docs/content**/*.md' --fix",
|
"lint:docs:fix": "markdownlint ./docs/content --fix && case-police 'docs/content**/*.md' --fix",
|
||||||
"nuxi": "NUXT_TELEMETRY_DISABLED=1 node ./packages/nuxi/bin/nuxi.mjs",
|
"nuxi": "NUXT_TELEMETRY_DISABLED=1 JITI_ESM_RESOLVE=1 node ./packages/nuxi/bin/nuxi.mjs",
|
||||||
"nuxt": "NUXT_TELEMETRY_DISABLED=1 node ./packages/nuxi/bin/nuxi.mjs",
|
"nuxt": "NUXT_TELEMETRY_DISABLED=1 JITI_ESM_RESOLVE=1 node ./packages/nuxi/bin/nuxi.mjs",
|
||||||
"play": "echo use yarn dev && exit 1",
|
"play": "echo use yarn dev && exit 1",
|
||||||
"release": "yarn && yarn lint && FORCE_COLOR=1 lerna publish -m \"chore: release\" && yarn stub",
|
"release": "yarn && yarn lint && FORCE_COLOR=1 lerna publish -m \"chore: release\" && yarn stub",
|
||||||
"stub": "lerna run prepack -- --stub",
|
"stub": "lerna run prepack -- --stub",
|
||||||
|
@ -46,7 +46,15 @@ export interface _AsyncData<DataT, ErrorT> {
|
|||||||
export type AsyncData<Data, Error> = _AsyncData<Data, Error> & Promise<_AsyncData<Data, Error>>
|
export type AsyncData<Data, Error> = _AsyncData<Data, Error> & Promise<_AsyncData<Data, Error>>
|
||||||
|
|
||||||
const getDefault = () => null
|
const getDefault = () => null
|
||||||
|
export function useAsyncData<
|
||||||
|
DataT,
|
||||||
|
DataE = Error,
|
||||||
|
Transform extends _Transform<DataT> = _Transform<DataT, DataT>,
|
||||||
|
PickKeys extends KeyOfRes<Transform> = KeyOfRes<Transform>
|
||||||
|
> (
|
||||||
|
handler: (ctx?: NuxtApp) => Promise<DataT>,
|
||||||
|
options?: AsyncDataOptions<DataT, Transform, PickKeys>
|
||||||
|
): AsyncData<PickFrom<ReturnType<Transform>, PickKeys>, DataE | null | true>
|
||||||
export function useAsyncData<
|
export function useAsyncData<
|
||||||
DataT,
|
DataT,
|
||||||
DataE = Error,
|
DataE = Error,
|
||||||
@ -55,14 +63,26 @@ export function useAsyncData<
|
|||||||
> (
|
> (
|
||||||
key: string,
|
key: string,
|
||||||
handler: (ctx?: NuxtApp) => Promise<DataT>,
|
handler: (ctx?: NuxtApp) => Promise<DataT>,
|
||||||
options: AsyncDataOptions<DataT, Transform, PickKeys> = {}
|
options?: AsyncDataOptions<DataT, Transform, PickKeys>
|
||||||
): AsyncData<PickFrom<ReturnType<Transform>, PickKeys>, DataE | null | true> {
|
): AsyncData<PickFrom<ReturnType<Transform>, PickKeys>, DataE | null | true>
|
||||||
|
export function useAsyncData<
|
||||||
|
DataT,
|
||||||
|
DataE = Error,
|
||||||
|
Transform extends _Transform<DataT> = _Transform<DataT, DataT>,
|
||||||
|
PickKeys extends KeyOfRes<Transform> = KeyOfRes<Transform>
|
||||||
|
> (...args): AsyncData<PickFrom<ReturnType<Transform>, PickKeys>, DataE | null | true> {
|
||||||
|
const autoKey = typeof args[args.length - 1] === 'string' ? args.pop() : undefined
|
||||||
|
if (typeof args[0] !== 'string') { args.unshift(autoKey) }
|
||||||
|
|
||||||
|
// eslint-disable-next-line prefer-const
|
||||||
|
let [key, handler, options = {}] = args as [string, (ctx?: NuxtApp) => Promise<DataT>, AsyncDataOptions<DataT, Transform, PickKeys>]
|
||||||
|
|
||||||
// Validate arguments
|
// Validate arguments
|
||||||
if (typeof key !== 'string') {
|
if (typeof key !== 'string') {
|
||||||
throw new TypeError('asyncData key must be a string')
|
throw new TypeError('[nuxt] [asyncData] key must be a string.')
|
||||||
}
|
}
|
||||||
if (typeof handler !== 'function') {
|
if (typeof handler !== 'function') {
|
||||||
throw new TypeError('asyncData handler must be a function')
|
throw new TypeError('[nuxt] [asyncData] handler must be a function.')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply defaults
|
// Apply defaults
|
||||||
@ -180,7 +200,15 @@ export function useAsyncData<
|
|||||||
|
|
||||||
return asyncDataPromise as AsyncData<PickFrom<ReturnType<Transform>, PickKeys>, DataE>
|
return asyncDataPromise as AsyncData<PickFrom<ReturnType<Transform>, PickKeys>, DataE>
|
||||||
}
|
}
|
||||||
|
export function useLazyAsyncData<
|
||||||
|
DataT,
|
||||||
|
DataE = Error,
|
||||||
|
Transform extends _Transform<DataT> = _Transform<DataT, DataT>,
|
||||||
|
PickKeys extends KeyOfRes<Transform> = KeyOfRes<Transform>
|
||||||
|
> (
|
||||||
|
handler: (ctx?: NuxtApp) => Promise<DataT>,
|
||||||
|
options?: Omit<AsyncDataOptions<DataT, Transform, PickKeys>, 'lazy'>
|
||||||
|
): AsyncData<PickFrom<ReturnType<Transform>, PickKeys>, DataE | null | true>
|
||||||
export function useLazyAsyncData<
|
export function useLazyAsyncData<
|
||||||
DataT,
|
DataT,
|
||||||
DataE = Error,
|
DataE = Error,
|
||||||
@ -189,9 +217,19 @@ export function useLazyAsyncData<
|
|||||||
> (
|
> (
|
||||||
key: string,
|
key: string,
|
||||||
handler: (ctx?: NuxtApp) => Promise<DataT>,
|
handler: (ctx?: NuxtApp) => Promise<DataT>,
|
||||||
options: Omit<AsyncDataOptions<DataT, Transform, PickKeys>, 'lazy'> = {}
|
options?: Omit<AsyncDataOptions<DataT, Transform, PickKeys>, 'lazy'>
|
||||||
): AsyncData<PickFrom<ReturnType<Transform>, PickKeys>, DataE | null | true> {
|
): AsyncData<PickFrom<ReturnType<Transform>, PickKeys>, DataE | null | true>
|
||||||
return useAsyncData(key, handler, { ...options, lazy: true })
|
export function useLazyAsyncData<
|
||||||
|
DataT,
|
||||||
|
DataE = Error,
|
||||||
|
Transform extends _Transform<DataT> = _Transform<DataT, DataT>,
|
||||||
|
PickKeys extends KeyOfRes<Transform> = KeyOfRes<Transform>
|
||||||
|
> (...args): AsyncData<PickFrom<ReturnType<Transform>, PickKeys>, DataE | null | true> {
|
||||||
|
const autoKey = typeof args[args.length - 1] === 'string' ? args.pop() : undefined
|
||||||
|
if (typeof args[0] !== 'string') { args.unshift(autoKey) }
|
||||||
|
const [key, handler, options] = args as [string, (ctx?: NuxtApp) => Promise<DataT>, AsyncDataOptions<DataT, Transform, PickKeys>]
|
||||||
|
// @ts-ignore
|
||||||
|
return useAsyncData(key, handler, { ...options, lazy: true }, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function refreshNuxtData (keys?: string | string[]): Promise<void> {
|
export function refreshNuxtData (keys?: string | string[]): Promise<void> {
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import type { FetchOptions, FetchRequest } from 'ohmyfetch'
|
import type { FetchOptions, FetchRequest } from 'ohmyfetch'
|
||||||
import type { TypedInternalResponse, NitroFetchRequest } from 'nitropack'
|
import type { TypedInternalResponse, NitroFetchRequest } from 'nitropack'
|
||||||
import { hash } from 'ohash'
|
|
||||||
import { computed, isRef, Ref } from 'vue'
|
import { computed, isRef, Ref } from 'vue'
|
||||||
import type { AsyncDataOptions, _Transform, KeyOfRes } from './asyncData'
|
import type { AsyncDataOptions, _Transform, KeyOfRes, AsyncData, PickFrom } from './asyncData'
|
||||||
import { useAsyncData } from './asyncData'
|
import { useAsyncData } from './asyncData'
|
||||||
|
|
||||||
export type FetchResult<ReqT extends NitroFetchRequest> = TypedInternalResponse<ReqT, unknown>
|
export type FetchResult<ReqT extends NitroFetchRequest> = TypedInternalResponse<ReqT, unknown>
|
||||||
@ -24,12 +23,30 @@ export function useFetch<
|
|||||||
PickKeys extends KeyOfRes<Transform> = KeyOfRes<Transform>
|
PickKeys extends KeyOfRes<Transform> = KeyOfRes<Transform>
|
||||||
> (
|
> (
|
||||||
request: Ref<ReqT> | ReqT | (() => ReqT),
|
request: Ref<ReqT> | ReqT | (() => ReqT),
|
||||||
opts: UseFetchOptions<_ResT, Transform, PickKeys> = {}
|
opts?: UseFetchOptions<_ResT, Transform, PickKeys>
|
||||||
|
): AsyncData<PickFrom<ReturnType<Transform>, PickKeys>, ErrorT | null | true>
|
||||||
|
export function useFetch<
|
||||||
|
ResT = void,
|
||||||
|
ErrorT = Error,
|
||||||
|
ReqT extends NitroFetchRequest = NitroFetchRequest,
|
||||||
|
_ResT = ResT extends void ? FetchResult<ReqT> : ResT,
|
||||||
|
Transform extends (res: _ResT) => any = (res: _ResT) => _ResT,
|
||||||
|
PickKeys extends KeyOfRes<Transform> = KeyOfRes<Transform>
|
||||||
|
> (
|
||||||
|
request: Ref<ReqT> | ReqT | (() => ReqT),
|
||||||
|
arg1?: string | UseFetchOptions<_ResT, Transform, PickKeys>,
|
||||||
|
arg2?: string
|
||||||
) {
|
) {
|
||||||
if (process.dev && !opts.key && Object.values(opts).some(v => typeof v === 'function' || v instanceof Blob)) {
|
const [opts, autoKey] = typeof arg1 === 'string' ? [{}, arg1] : [arg1, arg2]
|
||||||
console.warn('[nuxt] [useFetch] You should provide a key when passing options that are not serializable to JSON:', opts)
|
const _key = opts.key || autoKey
|
||||||
|
if (!_key || typeof _key !== 'string') {
|
||||||
|
throw new TypeError('[nuxt] [useFetch] key must be a string: ' + _key)
|
||||||
}
|
}
|
||||||
const key = '$f_' + (opts.key || hash([request, { ...opts, transform: null }]))
|
if (!request) {
|
||||||
|
throw new Error('[nuxt] [useFetch] request is missing.')
|
||||||
|
}
|
||||||
|
const key = '$f' + _key
|
||||||
|
|
||||||
const _request = computed(() => {
|
const _request = computed(() => {
|
||||||
let r = request as Ref<FetchRequest> | FetchRequest | (() => FetchRequest)
|
let r = request as Ref<FetchRequest> | FetchRequest | (() => FetchRequest)
|
||||||
if (typeof r === 'function') {
|
if (typeof r === 'function') {
|
||||||
@ -83,10 +100,26 @@ export function useLazyFetch<
|
|||||||
PickKeys extends KeyOfRes<Transform> = KeyOfRes<Transform>
|
PickKeys extends KeyOfRes<Transform> = KeyOfRes<Transform>
|
||||||
> (
|
> (
|
||||||
request: Ref<ReqT> | ReqT | (() => ReqT),
|
request: Ref<ReqT> | ReqT | (() => ReqT),
|
||||||
opts: Omit<UseFetchOptions<_ResT, Transform, PickKeys>, 'lazy'> = {}
|
opts?: Omit<UseFetchOptions<_ResT, Transform, PickKeys>, 'lazy'>
|
||||||
|
): AsyncData<PickFrom<ReturnType<Transform>, PickKeys>, ErrorT | null | true>
|
||||||
|
export function useLazyFetch<
|
||||||
|
ResT = void,
|
||||||
|
ErrorT = Error,
|
||||||
|
ReqT extends NitroFetchRequest = NitroFetchRequest,
|
||||||
|
_ResT = ResT extends void ? FetchResult<ReqT> : ResT,
|
||||||
|
Transform extends (res: _ResT) => any = (res: _ResT) => _ResT,
|
||||||
|
PickKeys extends KeyOfRes<Transform> = KeyOfRes<Transform>
|
||||||
|
> (
|
||||||
|
request: Ref<ReqT> | ReqT | (() => ReqT),
|
||||||
|
arg1?: string | Omit<UseFetchOptions<_ResT, Transform, PickKeys>, 'lazy'>,
|
||||||
|
arg2?: string
|
||||||
) {
|
) {
|
||||||
|
const [opts, autoKey] = typeof arg1 === 'string' ? [{}, arg1] : [arg1, arg2]
|
||||||
|
|
||||||
return useFetch<ResT, ErrorT, ReqT, _ResT, Transform, PickKeys>(request, {
|
return useFetch<ResT, ErrorT, ReqT, _ResT, Transform, PickKeys>(request, {
|
||||||
...opts,
|
...opts,
|
||||||
lazy: true
|
lazy: true
|
||||||
})
|
},
|
||||||
|
// @ts-ignore
|
||||||
|
autoKey)
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,20 @@ import { useNuxtApp } from '#app'
|
|||||||
* @param key a unique key ensuring that data fetching can be properly de-duplicated across requests
|
* @param key a unique key ensuring that data fetching can be properly de-duplicated across requests
|
||||||
* @param init a function that provides initial value for the state when it's not initiated
|
* @param init a function that provides initial value for the state when it's not initiated
|
||||||
*/
|
*/
|
||||||
export const useState = <T> (key: string, init?: (() => T | Ref<T>)): Ref<T> => {
|
export function useState <T> (key?: string, init?: (() => T | Ref<T>)): Ref<T>
|
||||||
|
export function useState <T> (init?: (() => T | Ref<T>)): Ref<T>
|
||||||
|
export function useState <T> (...args): Ref<T> {
|
||||||
|
const autoKey = typeof args[args.length - 1] === 'string' ? args.pop() : undefined
|
||||||
|
if (typeof args[0] !== 'string') { args.unshift(autoKey) }
|
||||||
|
const [_key, init] = args as [string, (() => T | Ref<T>)]
|
||||||
|
if (!_key || typeof _key !== 'string') {
|
||||||
|
throw new TypeError('[nuxt] [useState] key must be a string: ' + _key)
|
||||||
|
}
|
||||||
|
if (init !== undefined && typeof init !== 'function') {
|
||||||
|
throw new Error('[nuxt] [useState] init must be a function: ' + init)
|
||||||
|
}
|
||||||
|
const key = '$s' + _key
|
||||||
|
|
||||||
const nuxt = useNuxtApp()
|
const nuxt = useNuxtApp()
|
||||||
const state = toRef(nuxt.payload.state, key)
|
const state = toRef(nuxt.payload.state, key)
|
||||||
if (state.value === undefined && init) {
|
if (state.value === undefined && init) {
|
||||||
|
@ -29,6 +29,7 @@
|
|||||||
"defu": "^6.0.0",
|
"defu": "^6.0.0",
|
||||||
"esbuild": "^0.14.48",
|
"esbuild": "^0.14.48",
|
||||||
"escape-string-regexp": "^5.0.0",
|
"escape-string-regexp": "^5.0.0",
|
||||||
|
"estree-walker": "^3.0.1",
|
||||||
"externality": "^0.2.2",
|
"externality": "^0.2.2",
|
||||||
"fs-extra": "^10.1.0",
|
"fs-extra": "^10.1.0",
|
||||||
"get-port-please": "^2.5.0",
|
"get-port-please": "^2.5.0",
|
||||||
@ -36,6 +37,7 @@
|
|||||||
"knitwork": "^0.1.2",
|
"knitwork": "^0.1.2",
|
||||||
"magic-string": "^0.26.2",
|
"magic-string": "^0.26.2",
|
||||||
"mlly": "^0.5.4",
|
"mlly": "^0.5.4",
|
||||||
|
"ohash": "^0.1.0",
|
||||||
"pathe": "^0.3.2",
|
"pathe": "^0.3.2",
|
||||||
"perfect-debounce": "^0.1.3",
|
"perfect-debounce": "^0.1.3",
|
||||||
"postcss": "^8.4.14",
|
"postcss": "^8.4.14",
|
||||||
|
55
packages/vite/src/plugins/composable-keys.ts
Normal file
55
packages/vite/src/plugins/composable-keys.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { pathToFileURL } from 'node:url'
|
||||||
|
import { createUnplugin } from 'unplugin'
|
||||||
|
import { isAbsolute, relative } from 'pathe'
|
||||||
|
import { walk } from 'estree-walker'
|
||||||
|
import MagicString from 'magic-string'
|
||||||
|
import { hash } from 'ohash'
|
||||||
|
import type { CallExpression } from 'estree'
|
||||||
|
import { parseURL } from 'ufo'
|
||||||
|
|
||||||
|
export interface ComposableKeysOptions {
|
||||||
|
sourcemap?: boolean
|
||||||
|
rootDir?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyedFunctions = [
|
||||||
|
'useState', 'useFetch', 'useAsyncData', 'useLazyAsyncData', 'useLazyFetch'
|
||||||
|
]
|
||||||
|
const KEYED_FUNCTIONS_RE = new RegExp(`(${keyedFunctions.join('|')})`)
|
||||||
|
|
||||||
|
export const composableKeysPlugin = createUnplugin((options: ComposableKeysOptions = {}) => {
|
||||||
|
return {
|
||||||
|
name: 'nuxt:composable-keys',
|
||||||
|
enforce: 'post',
|
||||||
|
transform (code, id) {
|
||||||
|
const { pathname } = parseURL(decodeURIComponent(pathToFileURL(id).href))
|
||||||
|
if (!pathname.match(/\.(m?[jt]sx?|vue)/)) { return }
|
||||||
|
if (!KEYED_FUNCTIONS_RE.test(code)) { return }
|
||||||
|
const { 0: script = code, index: codeIndex = 0 } = code.match(/(?<=<script[^>]*>)[\S\s.]*?(?=<\/script>)/) || []
|
||||||
|
const s = new MagicString(code)
|
||||||
|
// https://github.com/unjs/unplugin/issues/90
|
||||||
|
const relativeID = isAbsolute(id) ? relative(options.rootDir, id) : id
|
||||||
|
walk(this.parse(script, {
|
||||||
|
sourceType: 'module',
|
||||||
|
ecmaVersion: 'latest'
|
||||||
|
}), {
|
||||||
|
enter (node: CallExpression) {
|
||||||
|
if (node.type !== 'CallExpression' || node.callee.type !== 'Identifier') { return }
|
||||||
|
if (keyedFunctions.includes(node.callee.name) && node.arguments.length < 4) {
|
||||||
|
const end = (node as any).end
|
||||||
|
s.appendLeft(
|
||||||
|
codeIndex + end - 1,
|
||||||
|
(node.arguments.length ? ', ' : '') + "'$" + hash(`${relativeID}-${codeIndex + end}`) + "'"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (s.hasChanged()) {
|
||||||
|
return {
|
||||||
|
code: s.toString(),
|
||||||
|
map: options.sourcemap && s.generateMap({ source: id, includeContent: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
@ -1,5 +1,5 @@
|
|||||||
import { createHash } from 'node:crypto'
|
|
||||||
import { promises as fsp, readdirSync, statSync } from 'node:fs'
|
import { promises as fsp, readdirSync, statSync } from 'node:fs'
|
||||||
|
import { hash } from 'ohash'
|
||||||
import { join } from 'pathe'
|
import { join } from 'pathe'
|
||||||
|
|
||||||
export function uniq<T> (arr: T[]): T[] {
|
export function uniq<T> (arr: T[]): T[] {
|
||||||
@ -28,13 +28,6 @@ export function hashId (id: string) {
|
|||||||
return '$id_' + hash(id)
|
return '$id_' + hash(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hash (input: string, length = 8) {
|
|
||||||
return createHash('sha256')
|
|
||||||
.update(input)
|
|
||||||
.digest('hex')
|
|
||||||
.slice(0, length)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function readDirRecursively (dir: string) {
|
export function readDirRecursively (dir: string) {
|
||||||
return readdirSync(dir).reduce((files, file) => {
|
return readdirSync(dir).reduce((files, file) => {
|
||||||
const name = join(dir, file)
|
const name = join(dir, file)
|
||||||
|
@ -13,6 +13,7 @@ import virtual from './plugins/virtual'
|
|||||||
import { DynamicBasePlugin } from './plugins/dynamic-base'
|
import { DynamicBasePlugin } from './plugins/dynamic-base'
|
||||||
import { warmupViteServer } from './utils/warmup'
|
import { warmupViteServer } from './utils/warmup'
|
||||||
import { resolveCSSOptions } from './css'
|
import { resolveCSSOptions } from './css'
|
||||||
|
import { composableKeysPlugin } from './plugins/composable-keys'
|
||||||
|
|
||||||
export interface ViteOptions extends InlineConfig {
|
export interface ViteOptions extends InlineConfig {
|
||||||
vue?: Options
|
vue?: Options
|
||||||
@ -65,6 +66,7 @@ export async function bundle (nuxt: Nuxt) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
|
composableKeysPlugin.vite({ sourcemap: nuxt.options.sourcemap, rootDir: nuxt.options.rootDir }),
|
||||||
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
|
||||||
|
@ -25,6 +25,7 @@
|
|||||||
"cssnano": "^5.1.12",
|
"cssnano": "^5.1.12",
|
||||||
"esbuild-loader": "^2.19.0",
|
"esbuild-loader": "^2.19.0",
|
||||||
"escape-string-regexp": "^5.0.0",
|
"escape-string-regexp": "^5.0.0",
|
||||||
|
"estree-walker": "^3.0.1",
|
||||||
"file-loader": "^6.2.0",
|
"file-loader": "^6.2.0",
|
||||||
"fork-ts-checker-webpack-plugin": "^7.2.11",
|
"fork-ts-checker-webpack-plugin": "^7.2.11",
|
||||||
"fs-extra": "^10.1.0",
|
"fs-extra": "^10.1.0",
|
||||||
|
@ -9,6 +9,7 @@ import type { Nuxt } from '@nuxt/schema'
|
|||||||
import { joinURL } from 'ufo'
|
import { joinURL } from 'ufo'
|
||||||
import { logger, useNuxt } from '@nuxt/kit'
|
import { logger, useNuxt } from '@nuxt/kit'
|
||||||
import { DynamicBasePlugin } from '../../vite/src/plugins/dynamic-base'
|
import { DynamicBasePlugin } from '../../vite/src/plugins/dynamic-base'
|
||||||
|
import { composableKeysPlugin } from '../../vite/src/plugins/composable-keys'
|
||||||
import { createMFS } from './utils/mfs'
|
import { createMFS } from './utils/mfs'
|
||||||
import { registerVirtualModules } from './virtual-modules'
|
import { registerVirtualModules } from './virtual-modules'
|
||||||
import { client, server } from './configs'
|
import { client, server } from './configs'
|
||||||
@ -37,6 +38,10 @@ export async function bundle (nuxt: Nuxt) {
|
|||||||
sourcemap: nuxt.options.sourcemap,
|
sourcemap: nuxt.options.sourcemap,
|
||||||
globalPublicPath: '__webpack_public_path__'
|
globalPublicPath: '__webpack_public_path__'
|
||||||
}))
|
}))
|
||||||
|
config.plugins.push(composableKeysPlugin.webpack({
|
||||||
|
sourcemap: nuxt.options.sourcemap,
|
||||||
|
rootDir: nuxt.options.rootDir
|
||||||
|
}))
|
||||||
|
|
||||||
// Create compiler
|
// Create compiler
|
||||||
const compiler = webpack(config)
|
const compiler = webpack(config)
|
||||||
|
@ -326,6 +326,14 @@ describe('extends support', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('automatically keyed composables', () => {
|
||||||
|
it('should automatically generate keys', async () => {
|
||||||
|
const html = await $fetch('/keyed-composables')
|
||||||
|
expect(html).toContain('true')
|
||||||
|
expect(html).not.toContain('false')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('dynamic paths', () => {
|
describe('dynamic paths', () => {
|
||||||
if (process.env.NUXT_TEST_DEV) {
|
if (process.env.NUXT_TEST_DEV) {
|
||||||
// TODO:
|
// TODO:
|
||||||
|
31
test/fixtures/basic/pages/keyed-composables.vue
vendored
Normal file
31
test/fixtures/basic/pages/keyed-composables.vue
vendored
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const useLocalState = () => useState(() => ({ foo: Math.random() }))
|
||||||
|
const useStateTest1 = useLocalState()
|
||||||
|
const useStateTest2 = useLocalState()
|
||||||
|
|
||||||
|
const useLocalAsyncData = () => useAsyncData(() => Promise.resolve({ foo: Math.random() }), { transform: data => data.foo })
|
||||||
|
const { data: useAsyncDataTest1 } = await useLocalAsyncData()
|
||||||
|
const { data: useAsyncDataTest2 } = await useLocalAsyncData()
|
||||||
|
|
||||||
|
const useLocalLazyAsyncData = () => useLazyAsyncData(() => Promise.resolve({ foo: Math.random() }), { transform: data => data.foo })
|
||||||
|
const { data: useLazyAsyncDataTest1 } = await useLocalLazyAsyncData()
|
||||||
|
const { data: useLazyAsyncDataTest2 } = await useLocalLazyAsyncData()
|
||||||
|
|
||||||
|
const useLocalFetch = () => useFetch('/api/counter', { transform: data => data.count })
|
||||||
|
const { data: useFetchTest1 } = await useLocalFetch()
|
||||||
|
const { data: useFetchTest2 } = await useLocalFetch()
|
||||||
|
|
||||||
|
const useLocalLazyFetch = () => useLazyFetch(() => '/api/counter')
|
||||||
|
const { data: useLazyFetchTest1 } = await useLocalLazyFetch()
|
||||||
|
const { data: useLazyFetchTest2 } = await useLocalLazyFetch()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<pre>
|
||||||
|
{{ useStateTest1 === useStateTest2 }}
|
||||||
|
{{ useAsyncDataTest1 === useAsyncDataTest2 }}
|
||||||
|
{{ useLazyAsyncDataTest1 === useLazyAsyncDataTest2 }}
|
||||||
|
{{ useFetchTest1 === useFetchTest2 }}
|
||||||
|
{{ useLazyFetchTest1 === useLazyFetchTest2 }}
|
||||||
|
</pre>
|
||||||
|
</template>
|
15
test/fixtures/basic/types.ts
vendored
15
test/fixtures/basic/types.ts
vendored
@ -134,4 +134,19 @@ describe('composables', () => {
|
|||||||
expectTypeOf(useFetch('/test', { default: () => ref(500) }).data).toMatchTypeOf<Ref<number>>()
|
expectTypeOf(useFetch('/test', { default: () => ref(500) }).data).toMatchTypeOf<Ref<number>>()
|
||||||
expectTypeOf(useFetch('/test', { default: () => 500 }).data).toMatchTypeOf<Ref<number>>()
|
expectTypeOf(useFetch('/test', { default: () => 500 }).data).toMatchTypeOf<Ref<number>>()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('provides proper type support when using overloads', () => {
|
||||||
|
expectTypeOf(useState('test')).toMatchTypeOf(useState())
|
||||||
|
expectTypeOf(useState('test', () => ({ foo: Math.random() }))).toMatchTypeOf(useState(() => ({ foo: Math.random() })))
|
||||||
|
|
||||||
|
expectTypeOf(useAsyncData('test', () => Promise.resolve({ foo: Math.random() })))
|
||||||
|
.toMatchTypeOf(useAsyncData(() => Promise.resolve({ foo: Math.random() })))
|
||||||
|
expectTypeOf(useAsyncData('test', () => Promise.resolve({ foo: Math.random() }), { transform: data => data.foo }))
|
||||||
|
.toMatchTypeOf(useAsyncData(() => Promise.resolve({ foo: Math.random() }), { transform: data => data.foo }))
|
||||||
|
|
||||||
|
expectTypeOf(useLazyAsyncData('test', () => Promise.resolve({ foo: Math.random() })))
|
||||||
|
.toMatchTypeOf(useLazyAsyncData(() => Promise.resolve({ foo: Math.random() })))
|
||||||
|
expectTypeOf(useLazyAsyncData('test', () => Promise.resolve({ foo: Math.random() }), { transform: data => data.foo }))
|
||||||
|
.toMatchTypeOf(useLazyAsyncData(() => Promise.resolve({ foo: Math.random() }), { transform: data => data.foo }))
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
10
yarn.lock
10
yarn.lock
@ -1775,6 +1775,7 @@ __metadata:
|
|||||||
defu: ^6.0.0
|
defu: ^6.0.0
|
||||||
esbuild: ^0.14.48
|
esbuild: ^0.14.48
|
||||||
escape-string-regexp: ^5.0.0
|
escape-string-regexp: ^5.0.0
|
||||||
|
estree-walker: ^3.0.1
|
||||||
externality: ^0.2.2
|
externality: ^0.2.2
|
||||||
fs-extra: ^10.1.0
|
fs-extra: ^10.1.0
|
||||||
get-port-please: ^2.5.0
|
get-port-please: ^2.5.0
|
||||||
@ -1782,6 +1783,7 @@ __metadata:
|
|||||||
knitwork: ^0.1.2
|
knitwork: ^0.1.2
|
||||||
magic-string: ^0.26.2
|
magic-string: ^0.26.2
|
||||||
mlly: ^0.5.4
|
mlly: ^0.5.4
|
||||||
|
ohash: ^0.1.0
|
||||||
pathe: ^0.3.2
|
pathe: ^0.3.2
|
||||||
perfect-debounce: ^0.1.3
|
perfect-debounce: ^0.1.3
|
||||||
postcss: ^8.4.14
|
postcss: ^8.4.14
|
||||||
@ -1820,6 +1822,7 @@ __metadata:
|
|||||||
cssnano: ^5.1.12
|
cssnano: ^5.1.12
|
||||||
esbuild-loader: ^2.19.0
|
esbuild-loader: ^2.19.0
|
||||||
escape-string-regexp: ^5.0.0
|
escape-string-regexp: ^5.0.0
|
||||||
|
estree-walker: ^3.0.1
|
||||||
file-loader: ^6.2.0
|
file-loader: ^6.2.0
|
||||||
fork-ts-checker-webpack-plugin: ^7.2.11
|
fork-ts-checker-webpack-plugin: ^7.2.11
|
||||||
fs-extra: ^10.1.0
|
fs-extra: ^10.1.0
|
||||||
@ -6278,6 +6281,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"estree-walker@npm:^3.0.1":
|
||||||
|
version: 3.0.1
|
||||||
|
resolution: "estree-walker@npm:3.0.1"
|
||||||
|
checksum: 674096950819041f1ee471e63f7aa987f2ed3a3a441cc41a5176e9ed01ea5cfd6487822c3b9c2cddd0e2c8f9d7ef52d32d06147a19b5a9ca9f8ab0c094bd43b9
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"esutils@npm:^2.0.2":
|
"esutils@npm:^2.0.2":
|
||||||
version: 2.0.3
|
version: 2.0.3
|
||||||
resolution: "esutils@npm:2.0.3"
|
resolution: "esutils@npm:2.0.3"
|
||||||
|
Loading…
Reference in New Issue
Block a user