From 435a331e87aa05edfa3abe0d9994e806680d3aec Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Thu, 12 May 2022 14:06:30 +0100 Subject: [PATCH 01/21] feat(nuxt): automatically generate unique keys for keyed composables --- .eslintrc | 1 + .../nuxt/src/app/composables/asyncData.ts | 50 +++++++++++++-- packages/nuxt/src/app/composables/fetch.ts | 41 ++++++++++--- packages/nuxt/src/app/composables/state.ts | 5 +- packages/vite/src/plugins/magic-keys.ts | 61 +++++++++++++++++++ packages/vite/src/vite.ts | 2 + packages/webpack/src/webpack.ts | 5 ++ test/basic.test.ts | 8 +++ .../basic/pages/keyed-composables.vue | 31 ++++++++++ test/fixtures/basic/types.ts | 15 +++++ 10 files changed, 206 insertions(+), 13 deletions(-) create mode 100644 packages/vite/src/plugins/magic-keys.ts create mode 100644 test/fixtures/basic/pages/keyed-composables.vue diff --git a/.eslintrc b/.eslintrc index 4b6b21596a8..5b5d001fa7d 100644 --- a/.eslintrc +++ b/.eslintrc @@ -22,6 +22,7 @@ "jsdoc/require-param": "off", "jsdoc/require-returns": "off", "jsdoc/require-param-type": "off", + "no-redeclare": "off", "import/no-restricted-paths": [ "error", { diff --git a/packages/nuxt/src/app/composables/asyncData.ts b/packages/nuxt/src/app/composables/asyncData.ts index 40ab34494a1..e7786b860d4 100644 --- a/packages/nuxt/src/app/composables/asyncData.ts +++ b/packages/nuxt/src/app/composables/asyncData.ts @@ -39,7 +39,15 @@ export interface _AsyncData { export type AsyncData = _AsyncData & Promise<_AsyncData> const getDefault = () => null - +export function useAsyncData< + DataT, + DataE = Error, + Transform extends _Transform = _Transform, + PickKeys extends KeyOfRes = KeyOfRes +> ( + handler: (ctx?: NuxtApp) => Promise, + options?: AsyncDataOptions +): AsyncData, PickKeys>, DataE | null | true> export function useAsyncData< DataT, DataE = Error, @@ -48,8 +56,21 @@ export function useAsyncData< > ( key: string, handler: (ctx?: NuxtApp) => Promise, - options: AsyncDataOptions = {} + options?: AsyncDataOptions +): AsyncData, PickKeys>, DataE | null | true> +export function useAsyncData< + DataT, + DataE = Error, + Transform extends _Transform = _Transform, + PickKeys extends KeyOfRes = KeyOfRes +> ( + _key: string | ((ctx?: NuxtApp) => Promise), + _handler?: ((ctx?: NuxtApp) => Promise) | AsyncDataOptions | string, + _options?: AsyncDataOptions | string, + fallback?: string ): AsyncData, PickKeys>, DataE | null | true> { + // eslint-disable-next-line prefer-const + let [key, handler, options] = (typeof _key === 'string' ? [_key, _handler, _options] : typeof _options === 'string' ? [_options, _key, {}] : [fallback, _key, _options]) as [string, (ctx?: NuxtApp) => Promise, AsyncDataOptions] // Validate arguments if (typeof key !== 'string') { throw new TypeError('asyncData key must be a string') @@ -171,7 +192,15 @@ export function useAsyncData< return asyncDataPromise as AsyncData, PickKeys>, DataE> } - +export function useLazyAsyncData< + DataT, + DataE = Error, + Transform extends _Transform = _Transform, + PickKeys extends KeyOfRes = KeyOfRes +> ( + handler: (ctx?: NuxtApp) => Promise, + options?: Omit, 'lazy'> +): AsyncData, PickKeys>, DataE | null | true> export function useLazyAsyncData< DataT, DataE = Error, @@ -180,8 +209,21 @@ export function useLazyAsyncData< > ( key: string, handler: (ctx?: NuxtApp) => Promise, - options: Omit, 'lazy'> = {} + options?: Omit, 'lazy'> +): AsyncData, PickKeys>, DataE | null | true> +export function useLazyAsyncData< + DataT, + DataE = Error, + Transform extends _Transform = _Transform, + PickKeys extends KeyOfRes = KeyOfRes +> ( + _key: string | ((ctx?: NuxtApp) => Promise), + _handler?: ((ctx?: NuxtApp) => Promise) | AsyncDataOptions | string, + _options?: Omit, 'lazy'>, + fallback?: string ): AsyncData, PickKeys>, DataE | null | true> { + // eslint-disable-next-line prefer-const + let [key, handler, options] = (typeof _key === 'string' ? [_key, _handler, _options] : typeof _options === 'string' ? [_options, _key, {}] : [fallback, _key, _options]) as [string, (ctx?: NuxtApp) => Promise, AsyncDataOptions] return useAsyncData(key, handler, { ...options, lazy: true }) } diff --git a/packages/nuxt/src/app/composables/fetch.ts b/packages/nuxt/src/app/composables/fetch.ts index 0ff307c2326..2664e040459 100644 --- a/packages/nuxt/src/app/composables/fetch.ts +++ b/packages/nuxt/src/app/composables/fetch.ts @@ -2,7 +2,7 @@ import type { FetchOptions, FetchRequest } from 'ohmyfetch' import type { TypedInternalResponse, NitroFetchRequest } from 'nitropack' import { hash } from 'ohash' 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' export type FetchResult = TypedInternalResponse @@ -24,12 +24,22 @@ export function useFetch< PickKeys extends KeyOfRes = KeyOfRes > ( request: Ref | ReqT | (() => ReqT), - opts: UseFetchOptions<_ResT, Transform, PickKeys> = {} + opts?: UseFetchOptions<_ResT, Transform, PickKeys> +): AsyncData, PickKeys>, ErrorT | null | true> +export function useFetch< + ResT = void, + ErrorT = Error, + ReqT extends NitroFetchRequest = NitroFetchRequest, + _ResT = ResT extends void ? FetchResult : ResT, + Transform extends (res: _ResT) => any = (res: _ResT) => _ResT, + PickKeys extends KeyOfRes = KeyOfRes +> ( + request: Ref | ReqT | (() => ReqT), + _opts?: string | UseFetchOptions<_ResT, Transform, PickKeys>, + _fallback?: string ) { - if (process.dev && opts.transform && !opts.key) { - console.warn('[nuxt] You should provide a key for `useFetch` when using a custom transform function.') - } - const key = '$f_' + (opts.key || hash([request, { ...opts, transform: null }])) + const [opts, fallback] = (typeof _opts === 'string' ? [{}, _opts] : [_opts, _fallback]) + const key = '$f_' + (typeof opts.key === 'string' ? opts.key : typeof request === 'string' && !opts.transform && !opts.default ? hash([request, opts]) : fallback) const _request = computed(() => { let r = request as Ref | FetchRequest | (() => FetchRequest) if (typeof r === 'function') { @@ -67,10 +77,25 @@ export function useLazyFetch< PickKeys extends KeyOfRes = KeyOfRes > ( request: Ref | ReqT | (() => ReqT), - opts: Omit, 'lazy'> = {} + opts: Omit, 'lazy'> +): AsyncData, PickKeys>, ErrorT | null | true> +export function useLazyFetch< + ResT = void, + ErrorT = Error, + ReqT extends NitroFetchRequest = NitroFetchRequest, + _ResT = ResT extends void ? FetchResult : ResT, + Transform extends (res: _ResT) => any = (res: _ResT) => _ResT, + PickKeys extends KeyOfRes = KeyOfRes +> ( + request: Ref | ReqT | (() => ReqT), + _opts?: string | Omit, 'lazy'>, + _fallback?: string ) { + const [opts, fallback] = (typeof _opts === 'string' ? [{}, _opts] : [_opts, _fallback]) return useFetch(request, { ...opts, lazy: true - }) + }, + // @ts-ignore + fallback) } diff --git a/packages/nuxt/src/app/composables/state.ts b/packages/nuxt/src/app/composables/state.ts index 07ac7ac80a2..4e1b0b65c71 100644 --- a/packages/nuxt/src/app/composables/state.ts +++ b/packages/nuxt/src/app/composables/state.ts @@ -8,7 +8,10 @@ import { useNuxtApp } from '#app' * @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 */ -export const useState = (key: string, init?: (() => T | Ref)): Ref => { +export function useState (key: string, init?: (() => T | Ref)): Ref +export function useState (init?: (() => T | Ref)): Ref +export function useState (_key?: string | (() => T | Ref), _init?: string | (() => T | Ref)): Ref { + const [key, init] = (typeof _key === 'string' ? [_key, _init] : [_init, _key]) as [string, (() => T | Ref)] const nuxt = useNuxtApp() const state = toRef(nuxt.payload.state, key) if (state.value === undefined && init) { diff --git a/packages/vite/src/plugins/magic-keys.ts b/packages/vite/src/plugins/magic-keys.ts new file mode 100644 index 00000000000..e52433ab88d --- /dev/null +++ b/packages/vite/src/plugins/magic-keys.ts @@ -0,0 +1,61 @@ +import crypto from 'node:crypto' +import { pathToFileURL } from 'node:url' +import type { Plugin } from 'vite' +import { parse } from 'acorn' +import { walk } from 'estree-walker' +import MagicString from 'magic-string' +import type { CallExpression } from 'estree' +import { parseURL } from 'ufo' + +export interface MagicKeysOptions { + sourcemap?: boolean + useAcorn?: boolean +} + +function createKey (source: string) { + const hash = crypto.createHash('md5') + hash.update(source) + return hash.digest('base64').toString() +} + +const keyedFunctions = [ + 'useState', 'useFetch', 'useAsyncData', 'useLazyAsyncData', 'useLazyFetch' +] + +export const magicKeysPlugin = (options: MagicKeysOptions = {}): Plugin => { + return { + name: 'nuxt:magic-keys', + enforce: 'post', + transform (code, id) { + const { pathname } = parseURL(decodeURIComponent(pathToFileURL(id).href)) + if (!pathname.match(/\.([cm][jt]sx?|vue)/)) { return } + if (!keyedFunctions.some(f => code.includes(f))) { return } + const { 0: script = code, index: codeIndex = 0 } = code.match(/(?<=]*>)[\S\s.]*?(?=<\/script>)/) || [] + const s = new MagicString(code) + // https://github.com/unjs/unplugin/issues/90 + walk(options.useAcorn + ? parse(script, { + sourceType: 'module', + ecmaVersion: 'latest' + }) + : this.parse(script), { + enter (node: CallExpression) { + if (node.type !== 'CallExpression' || node.callee.type !== 'Identifier') { return } + if (keyedFunctions.includes(node.callee.name)) { + const end = (node as any).end + s.appendLeft( + codeIndex + end - 1, + (node.arguments.length ? ', ' : '') + "'" + createKey(`${id}-${codeIndex + end}`) + "'" + ) + } + } + }) + if (s.hasChanged()) { + return { + code: s.toString(), + map: options.sourcemap && s.generateMap({ source: id, includeContent: true }) + } + } + } + } +} diff --git a/packages/vite/src/vite.ts b/packages/vite/src/vite.ts index 552039c76de..6b577d8e5fd 100644 --- a/packages/vite/src/vite.ts +++ b/packages/vite/src/vite.ts @@ -12,6 +12,7 @@ import virtual from './plugins/virtual' import { DynamicBasePlugin } from './plugins/dynamic-base' import { warmupViteServer } from './utils/warmup' import { resolveCSSOptions } from './css' +import { magicKeysPlugin } from './plugins/magic-keys' export interface ViteOptions extends InlineConfig { vue?: Options @@ -64,6 +65,7 @@ export async function bundle (nuxt: Nuxt) { } }, plugins: [ + magicKeysPlugin({ sourcemap: nuxt.options.sourcemap }), virtual(nuxt.vfs), DynamicBasePlugin.vite({ sourcemap: nuxt.options.sourcemap }) ], diff --git a/packages/webpack/src/webpack.ts b/packages/webpack/src/webpack.ts index ab04e37aa6f..8de0b94e726 100644 --- a/packages/webpack/src/webpack.ts +++ b/packages/webpack/src/webpack.ts @@ -8,7 +8,9 @@ import type { Compiler, Watching } from 'webpack' import type { Nuxt } from '@nuxt/schema' import { joinURL } from 'ufo' import { logger, useNuxt } from '@nuxt/kit' +import { createUnplugin } from 'unplugin' import { DynamicBasePlugin } from '../../vite/src/plugins/dynamic-base' +import { magicKeysPlugin as _magicKeysPlugin } from '../../vite/src/plugins/magic-keys' import { createMFS } from './utils/mfs' import { registerVirtualModules } from './virtual-modules' import { client, server } from './configs' @@ -17,6 +19,8 @@ import { createWebpackConfigContext, applyPresets, getWebpackConfig } from './ut // TODO: Support plugins // const plugins: string[] = [] +const magicKeysPlugin = createUnplugin(_magicKeysPlugin as any) + export async function bundle (nuxt: Nuxt) { await registerVirtualModules() @@ -37,6 +41,7 @@ export async function bundle (nuxt: Nuxt) { sourcemap: nuxt.options.sourcemap, globalPublicPath: '__webpack_public_path__' })) + config.plugins.push(magicKeysPlugin.webpack({ useAcorn: true, sourcemap: nuxt.options.sourcemap })) // Create compiler const compiler = webpack(config) diff --git a/test/basic.test.ts b/test/basic.test.ts index 2379b0baaa0..bda067a8a3e 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -317,6 +317,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', () => { if (process.env.NUXT_TEST_DEV) { // TODO: diff --git a/test/fixtures/basic/pages/keyed-composables.vue b/test/fixtures/basic/pages/keyed-composables.vue new file mode 100644 index 00000000000..2879009aefb --- /dev/null +++ b/test/fixtures/basic/pages/keyed-composables.vue @@ -0,0 +1,31 @@ + + + diff --git a/test/fixtures/basic/types.ts b/test/fixtures/basic/types.ts index 819bb9d7d10..5b6afad4acb 100644 --- a/test/fixtures/basic/types.ts +++ b/test/fixtures/basic/types.ts @@ -134,4 +134,19 @@ describe('composables', () => { expectTypeOf(useFetch('/test', { default: () => ref(500) }).data).toMatchTypeOf>() expectTypeOf(useFetch('/test', { default: () => 500 }).data).toMatchTypeOf>() }) + + 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 })) + }) }) From fcbb16475cf0b5cdf3b60834441a339833150945 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Thu, 12 May 2022 14:23:58 +0100 Subject: [PATCH 02/21] docs: update and fix typings --- .../3.api/1.composables/use-async-data.md | 14 ++++++++----- docs/content/3.api/1.composables/use-fetch.md | 20 +++++++++++-------- docs/content/3.api/1.composables/use-state.md | 3 ++- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/docs/content/3.api/1.composables/use-async-data.md b/docs/content/3.api/1.composables/use-async-data.md index 17806001ea0..15268093e98 100644 --- a/docs/content/3.api/1.composables/use-async-data.md +++ b/docs/content/3.api/1.composables/use-async-data.md @@ -5,13 +5,17 @@ Within your pages, components, and plugins you can use useAsyncData to get acces ## Type ```ts [Signature] +function useAsyncData( + handler: (nuxtApp?: NuxtApp) => Promise, + options?: AsyncDataOptions +): AsyncData function useAsyncData( key: string, handler: (nuxtApp?: NuxtApp) => Promise, - options?: AsyncDataOptions -): Promise + options?: AsyncDataOptions +): AsyncData -type AsyncDataOptions = { +type AsyncDataOptions = { server?: boolean lazy?: boolean default?: () => DataT | Ref @@ -21,7 +25,7 @@ type AsyncDataOptions = { initialCache?: boolean } -type DataT = { +type AsyncData = { data: Ref pending: Ref refresh: () => Promise @@ -31,7 +35,7 @@ type DataT = { ## 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 and line number of the instance of `useAsyncData` will be generated for you. * **handler**: an asynchronous function that returns a value * **options**: * _lazy_: whether to resolve the async function after loading the route, instead of blocking navigation (defaults to `false`) diff --git a/docs/content/3.api/1.composables/use-fetch.md b/docs/content/3.api/1.composables/use-fetch.md index cb808940877..96f26e2bc62 100644 --- a/docs/content/3.api/1.composables/use-fetch.md +++ b/docs/content/3.api/1.composables/use-fetch.md @@ -6,15 +6,15 @@ This composable provides a convenient wrapper around [`useAsyncData`](/api/compo ```ts [Signature] function useFetch( - url: string | Request, - options?: UseFetchOptions -): Promise + url: string | Request | Ref | () => string | Request, + options?: UseFetchOptions +): AsyncData type UseFetchOptions = { - method?: string, - params?: SearchParams, - headers?: {key: string, value: string}[], - baseURL?: string, + method?: string + params?: SearchParams + headers?: {key: string, value: string}[] + baseURL?: string server?: boolean lazy?: boolean default?: () => DataT @@ -22,7 +22,7 @@ type UseFetchOptions = { pick?: string[] } -type DataT = { +type AsyncData = { data: Ref pending: Ref refresh: () => Promise @@ -45,6 +45,10 @@ type DataT = { * `pick`: Only pick specified keys in this array from the `handler` function result. * `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 * **data**: the result of the asynchronous function that is passed in diff --git a/docs/content/3.api/1.composables/use-state.md b/docs/content/3.api/1.composables/use-state.md index b6c46e48f32..30ac7d5c4e2 100644 --- a/docs/content/3.api/1.composables/use-state.md +++ b/docs/content/3.api/1.composables/use-state.md @@ -1,10 +1,11 @@ # `useState` ```ts +useState(init?: () => T | Ref): Ref useState(key: string, init?: () => T | Ref): Ref ``` -* **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`. * **T**: (typescript only) Specify the type of state From ca201906231155e3e1d12770a3bec6caecee8483 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Thu, 12 May 2022 14:38:47 +0100 Subject: [PATCH 03/21] fix: update lazy fetch types --- packages/nuxt/src/app/composables/fetch.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nuxt/src/app/composables/fetch.ts b/packages/nuxt/src/app/composables/fetch.ts index 2664e040459..86de03702bf 100644 --- a/packages/nuxt/src/app/composables/fetch.ts +++ b/packages/nuxt/src/app/composables/fetch.ts @@ -77,7 +77,7 @@ export function useLazyFetch< PickKeys extends KeyOfRes = KeyOfRes > ( request: Ref | ReqT | (() => ReqT), - opts: Omit, 'lazy'> + opts?: Omit, 'lazy'> ): AsyncData, PickKeys>, ErrorT | null | true> export function useLazyFetch< ResT = void, From 8f709e342539df01d10391e58b0ebd2932007df1 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Thu, 12 May 2022 14:42:09 +0100 Subject: [PATCH 04/21] fix: add acorn and estree-walker to dependencies --- packages/vite/build.config.ts | 1 + packages/vite/package.json | 1 + packages/webpack/package.json | 2 ++ yarn.lock | 10 ++++++++++ 4 files changed, 14 insertions(+) diff --git a/packages/vite/build.config.ts b/packages/vite/build.config.ts index e387f8754ce..a1d247d9b99 100644 --- a/packages/vite/build.config.ts +++ b/packages/vite/build.config.ts @@ -10,6 +10,7 @@ export default defineBuildConfig({ 'vue' ], externals: [ + 'acorn', '@nuxt/schema' ] }) diff --git a/packages/vite/package.json b/packages/vite/package.json index 83dadc43f7b..2e9ab121619 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -28,6 +28,7 @@ "defu": "^6.0.0", "esbuild": "^0.14.39", "escape-string-regexp": "^5.0.0", + "estree-walker": "^3.0.1", "externality": "^0.2.1", "fs-extra": "^10.1.0", "get-port-please": "^2.5.0", diff --git a/packages/webpack/package.json b/packages/webpack/package.json index a9b82da2b48..07cbb4a226c 100644 --- a/packages/webpack/package.json +++ b/packages/webpack/package.json @@ -19,12 +19,14 @@ "@babel/core": "^7.17.10", "@nuxt/friendly-errors-webpack-plugin": "^2.5.2", "@nuxt/kit": "^3.0.0-rc.3", + "acorn": "^8.7.1", "autoprefixer": "^10.4.7", "css-loader": "^6.7.1", "css-minimizer-webpack-plugin": "^3.4.1", "cssnano": "^5.1.7", "esbuild-loader": "^2.18.0", "escape-string-regexp": "^5.0.0", + "estree-walker": "^3.0.1", "file-loader": "^6.2.0", "fork-ts-checker-webpack-plugin": "^7.2.11", "fs-extra": "^10.1.0", diff --git a/yarn.lock b/yarn.lock index b822426efea..2adfa6e44a4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1637,6 +1637,7 @@ __metadata: defu: ^6.0.0 esbuild: ^0.14.39 escape-string-regexp: ^5.0.0 + estree-walker: ^3.0.1 externality: ^0.2.1 fs-extra: ^10.1.0 get-port-please: ^2.5.0 @@ -1676,12 +1677,14 @@ __metadata: "@types/webpack-dev-middleware": ^5.0.2 "@types/webpack-hot-middleware": ^2.25.6 "@types/webpack-virtual-modules": ^0 + acorn: ^8.7.1 autoprefixer: ^10.4.7 css-loader: ^6.7.1 css-minimizer-webpack-plugin: ^3.4.1 cssnano: ^5.1.7 esbuild-loader: ^2.18.0 escape-string-regexp: ^5.0.0 + estree-walker: ^3.0.1 file-loader: ^6.2.0 fork-ts-checker-webpack-plugin: ^7.2.11 fs-extra: ^10.1.0 @@ -6235,6 +6238,13 @@ __metadata: languageName: node 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": version: 2.0.3 resolution: "esutils@npm:2.0.3" From be98e926a0c4491233288db4caae1922e25ac4c2 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Fri, 13 May 2022 13:05:50 +0100 Subject: [PATCH 05/21] refactor: rename magic keys -> composable keys --- .../vite/src/plugins/{magic-keys.ts => composable-keys.ts} | 2 +- packages/vite/src/vite.ts | 4 ++-- packages/webpack/src/webpack.ts | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) rename packages/vite/src/plugins/{magic-keys.ts => composable-keys.ts} (95%) diff --git a/packages/vite/src/plugins/magic-keys.ts b/packages/vite/src/plugins/composable-keys.ts similarity index 95% rename from packages/vite/src/plugins/magic-keys.ts rename to packages/vite/src/plugins/composable-keys.ts index e52433ab88d..fb9137b00d2 100644 --- a/packages/vite/src/plugins/magic-keys.ts +++ b/packages/vite/src/plugins/composable-keys.ts @@ -22,7 +22,7 @@ const keyedFunctions = [ 'useState', 'useFetch', 'useAsyncData', 'useLazyAsyncData', 'useLazyFetch' ] -export const magicKeysPlugin = (options: MagicKeysOptions = {}): Plugin => { +export const composableKeysPlugin = (options: MagicKeysOptions = {}): Plugin => { return { name: 'nuxt:magic-keys', enforce: 'post', diff --git a/packages/vite/src/vite.ts b/packages/vite/src/vite.ts index 6b577d8e5fd..65c5134dc64 100644 --- a/packages/vite/src/vite.ts +++ b/packages/vite/src/vite.ts @@ -12,7 +12,7 @@ import virtual from './plugins/virtual' import { DynamicBasePlugin } from './plugins/dynamic-base' import { warmupViteServer } from './utils/warmup' import { resolveCSSOptions } from './css' -import { magicKeysPlugin } from './plugins/magic-keys' +import { composableKeysPlugin } from './plugins/composable-keys' export interface ViteOptions extends InlineConfig { vue?: Options @@ -65,7 +65,7 @@ export async function bundle (nuxt: Nuxt) { } }, plugins: [ - magicKeysPlugin({ sourcemap: nuxt.options.sourcemap }), + composableKeysPlugin({ sourcemap: nuxt.options.sourcemap }), virtual(nuxt.vfs), DynamicBasePlugin.vite({ sourcemap: nuxt.options.sourcemap }) ], diff --git a/packages/webpack/src/webpack.ts b/packages/webpack/src/webpack.ts index 8de0b94e726..73311a51d5d 100644 --- a/packages/webpack/src/webpack.ts +++ b/packages/webpack/src/webpack.ts @@ -10,7 +10,7 @@ import { joinURL } from 'ufo' import { logger, useNuxt } from '@nuxt/kit' import { createUnplugin } from 'unplugin' import { DynamicBasePlugin } from '../../vite/src/plugins/dynamic-base' -import { magicKeysPlugin as _magicKeysPlugin } from '../../vite/src/plugins/magic-keys' +import { composableKeysPlugin as _composableKeysPlugin } from '../../vite/src/plugins/composable-keys' import { createMFS } from './utils/mfs' import { registerVirtualModules } from './virtual-modules' import { client, server } from './configs' @@ -19,7 +19,7 @@ import { createWebpackConfigContext, applyPresets, getWebpackConfig } from './ut // TODO: Support plugins // const plugins: string[] = [] -const magicKeysPlugin = createUnplugin(_magicKeysPlugin as any) +const composableKeysPlugin = createUnplugin(_composableKeysPlugin as any) export async function bundle (nuxt: Nuxt) { await registerVirtualModules() @@ -41,7 +41,7 @@ export async function bundle (nuxt: Nuxt) { sourcemap: nuxt.options.sourcemap, globalPublicPath: '__webpack_public_path__' })) - config.plugins.push(magicKeysPlugin.webpack({ useAcorn: true, sourcemap: nuxt.options.sourcemap })) + config.plugins.push(composableKeysPlugin.webpack({ useAcorn: true, sourcemap: nuxt.options.sourcemap })) // Create compiler const compiler = webpack(config) From cf0e2daa6314124b68faca9d3fdbb291c63e528d Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Fri, 13 May 2022 13:08:00 +0100 Subject: [PATCH 06/21] perf: use regexp for early return --- packages/vite/src/plugins/composable-keys.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/vite/src/plugins/composable-keys.ts b/packages/vite/src/plugins/composable-keys.ts index fb9137b00d2..263b757351c 100644 --- a/packages/vite/src/plugins/composable-keys.ts +++ b/packages/vite/src/plugins/composable-keys.ts @@ -21,6 +21,7 @@ function createKey (source: string) { const keyedFunctions = [ 'useState', 'useFetch', 'useAsyncData', 'useLazyAsyncData', 'useLazyFetch' ] +const KEYED_FUNCTIONS_RE = new RegExp(`(${keyedFunctions.join('|')})`) export const composableKeysPlugin = (options: MagicKeysOptions = {}): Plugin => { return { @@ -29,7 +30,7 @@ export const composableKeysPlugin = (options: MagicKeysOptions = {}): Plugin => transform (code, id) { const { pathname } = parseURL(decodeURIComponent(pathToFileURL(id).href)) if (!pathname.match(/\.([cm][jt]sx?|vue)/)) { return } - if (!keyedFunctions.some(f => code.includes(f))) { return } + if (!KEYED_FUNCTIONS_RE.test(code)) { return } const { 0: script = code, index: codeIndex = 0 } = code.match(/(?<=]*>)[\S\s.]*?(?=<\/script>)/) || [] const s = new MagicString(code) // https://github.com/unjs/unplugin/issues/90 From 1c1bac4f88ca68cb90864d80efb6877abdd510d8 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Fri, 13 May 2022 13:14:01 +0100 Subject: [PATCH 07/21] fix: normalise id into relative path --- packages/vite/src/plugins/composable-keys.ts | 11 +++++++---- packages/vite/src/vite.ts | 2 +- packages/webpack/src/webpack.ts | 6 +++++- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/vite/src/plugins/composable-keys.ts b/packages/vite/src/plugins/composable-keys.ts index 263b757351c..724103f184e 100644 --- a/packages/vite/src/plugins/composable-keys.ts +++ b/packages/vite/src/plugins/composable-keys.ts @@ -1,15 +1,17 @@ import crypto from 'node:crypto' import { pathToFileURL } from 'node:url' import type { Plugin } from 'vite' +import { isAbsolute, relative } from 'pathe' import { parse } from 'acorn' import { walk } from 'estree-walker' import MagicString from 'magic-string' import type { CallExpression } from 'estree' import { parseURL } from 'ufo' -export interface MagicKeysOptions { +export interface ComposableKeysOptions { sourcemap?: boolean useAcorn?: boolean + rootDir?: string } function createKey (source: string) { @@ -23,9 +25,9 @@ const keyedFunctions = [ ] const KEYED_FUNCTIONS_RE = new RegExp(`(${keyedFunctions.join('|')})`) -export const composableKeysPlugin = (options: MagicKeysOptions = {}): Plugin => { +export const composableKeysPlugin = (options: ComposableKeysOptions = {}): Plugin => { return { - name: 'nuxt:magic-keys', + name: 'nuxt:composable-keys', enforce: 'post', transform (code, id) { const { pathname } = parseURL(decodeURIComponent(pathToFileURL(id).href)) @@ -34,6 +36,7 @@ export const composableKeysPlugin = (options: MagicKeysOptions = {}): Plugin => const { 0: script = code, index: codeIndex = 0 } = code.match(/(?<=]*>)[\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(options.useAcorn ? parse(script, { sourceType: 'module', @@ -46,7 +49,7 @@ export const composableKeysPlugin = (options: MagicKeysOptions = {}): Plugin => const end = (node as any).end s.appendLeft( codeIndex + end - 1, - (node.arguments.length ? ', ' : '') + "'" + createKey(`${id}-${codeIndex + end}`) + "'" + (node.arguments.length ? ', ' : '') + "'" + createKey(`${relativeID}-${codeIndex + end}`) + "'" ) } } diff --git a/packages/vite/src/vite.ts b/packages/vite/src/vite.ts index 65c5134dc64..7fb27c3205d 100644 --- a/packages/vite/src/vite.ts +++ b/packages/vite/src/vite.ts @@ -65,7 +65,7 @@ export async function bundle (nuxt: Nuxt) { } }, plugins: [ - composableKeysPlugin({ sourcemap: nuxt.options.sourcemap }), + composableKeysPlugin({ sourcemap: nuxt.options.sourcemap, rootDir: nuxt.options.rootDir }), virtual(nuxt.vfs), DynamicBasePlugin.vite({ sourcemap: nuxt.options.sourcemap }) ], diff --git a/packages/webpack/src/webpack.ts b/packages/webpack/src/webpack.ts index 73311a51d5d..6aa4670dc05 100644 --- a/packages/webpack/src/webpack.ts +++ b/packages/webpack/src/webpack.ts @@ -41,7 +41,11 @@ export async function bundle (nuxt: Nuxt) { sourcemap: nuxt.options.sourcemap, globalPublicPath: '__webpack_public_path__' })) - config.plugins.push(composableKeysPlugin.webpack({ useAcorn: true, sourcemap: nuxt.options.sourcemap })) + config.plugins.push(composableKeysPlugin.webpack({ + useAcorn: true, + sourcemap: nuxt.options.sourcemap, + rootDir: nuxt.options.rootDir + })) // Create compiler const compiler = webpack(config) From 52d531d1589f22bfc1fced79ed699ef6e6d81cdf Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Fri, 13 May 2022 13:20:00 +0100 Subject: [PATCH 08/21] fix: improve readability --- packages/nuxt/src/app/composables/asyncData.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/nuxt/src/app/composables/asyncData.ts b/packages/nuxt/src/app/composables/asyncData.ts index e7786b860d4..29d58e83f22 100644 --- a/packages/nuxt/src/app/composables/asyncData.ts +++ b/packages/nuxt/src/app/composables/asyncData.ts @@ -70,7 +70,12 @@ export function useAsyncData< fallback?: string ): AsyncData, PickKeys>, DataE | null | true> { // eslint-disable-next-line prefer-const - let [key, handler, options] = (typeof _key === 'string' ? [_key, _handler, _options] : typeof _options === 'string' ? [_options, _key, {}] : [fallback, _key, _options]) as [string, (ctx?: NuxtApp) => Promise, AsyncDataOptions] + let [key, handler, options] = (typeof _key === 'string' + ? [_key, _handler, _options] + : typeof _options === 'string' + ? [_options, _key, {}] + : [fallback, _key, _options] + ) as [string, (ctx?: NuxtApp) => Promise, AsyncDataOptions] // Validate arguments if (typeof key !== 'string') { throw new TypeError('asyncData key must be a string') @@ -222,8 +227,12 @@ export function useLazyAsyncData< _options?: Omit, 'lazy'>, fallback?: string ): AsyncData, PickKeys>, DataE | null | true> { - // eslint-disable-next-line prefer-const - let [key, handler, options] = (typeof _key === 'string' ? [_key, _handler, _options] : typeof _options === 'string' ? [_options, _key, {}] : [fallback, _key, _options]) as [string, (ctx?: NuxtApp) => Promise, AsyncDataOptions] + const [key, handler, options] = (typeof _key === 'string' + ? [_key, _handler, _options] + : typeof _options === 'string' + ? [_options, _key, {}] + : [fallback, _key, _options] + ) as [string, (ctx?: NuxtApp) => Promise, AsyncDataOptions] return useAsyncData(key, handler, { ...options, lazy: true }) } From ae61515cc60ddac3eaf2ad12654a91392219024f Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Fri, 13 May 2022 13:32:49 +0100 Subject: [PATCH 09/21] perf: use hash map for generating short keys --- .../nuxt/src/app/composables/asyncData.ts | 8 +++++--- packages/vite/src/plugins/composable-keys.ts | 19 +++++++++++++++---- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/packages/nuxt/src/app/composables/asyncData.ts b/packages/nuxt/src/app/composables/asyncData.ts index 29d58e83f22..7735d189a87 100644 --- a/packages/nuxt/src/app/composables/asyncData.ts +++ b/packages/nuxt/src/app/composables/asyncData.ts @@ -72,9 +72,11 @@ export function useAsyncData< // eslint-disable-next-line prefer-const let [key, handler, options] = (typeof _key === 'string' ? [_key, _handler, _options] - : typeof _options === 'string' - ? [_options, _key, {}] - : [fallback, _key, _options] + : typeof _handler === 'string' + ? [_handler, _key, {}] + : typeof _options === 'string' + ? [_options, _key, {}] + : [fallback, _key, _options] ) as [string, (ctx?: NuxtApp) => Promise, AsyncDataOptions] // Validate arguments if (typeof key !== 'string') { diff --git a/packages/vite/src/plugins/composable-keys.ts b/packages/vite/src/plugins/composable-keys.ts index 724103f184e..dc5cb7dc081 100644 --- a/packages/vite/src/plugins/composable-keys.ts +++ b/packages/vite/src/plugins/composable-keys.ts @@ -14,10 +14,21 @@ export interface ComposableKeysOptions { rootDir?: string } +const hashMap: Record = {} + function createKey (source: string) { - const hash = crypto.createHash('md5') - hash.update(source) - return hash.digest('base64').toString() + let outputLength = 3 + do { + const hash = crypto.createHash('md5') + hash.update(source) + const key = hash.digest('base64').toString().slice(0, outputLength) + if (key in hashMap && hashMap[key] !== source) { + outputLength++ + continue + } + hashMap[key] = source + return key + } while (true) } const keyedFunctions = [ @@ -49,7 +60,7 @@ export const composableKeysPlugin = (options: ComposableKeysOptions = {}): Plugi const end = (node as any).end s.appendLeft( codeIndex + end - 1, - (node.arguments.length ? ', ' : '') + "'" + createKey(`${relativeID}-${codeIndex + end}`) + "'" + (node.arguments.length ? ', ' : '') + "'$" + createKey(`${relativeID}-${codeIndex + end}`) + "'" ) } } From 231c5a513a9f34af0d74c2ad9856fd1ea2abf135 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Fri, 13 May 2022 13:44:32 +0100 Subject: [PATCH 10/21] fix: remove cjs from transformed extensions --- packages/vite/src/plugins/composable-keys.ts | 2 +- playground/app.tsx | 8 ++++++++ playground/app.vue | 12 ------------ 3 files changed, 9 insertions(+), 13 deletions(-) create mode 100644 playground/app.tsx delete mode 100644 playground/app.vue diff --git a/packages/vite/src/plugins/composable-keys.ts b/packages/vite/src/plugins/composable-keys.ts index dc5cb7dc081..b916a966555 100644 --- a/packages/vite/src/plugins/composable-keys.ts +++ b/packages/vite/src/plugins/composable-keys.ts @@ -42,7 +42,7 @@ export const composableKeysPlugin = (options: ComposableKeysOptions = {}): Plugi enforce: 'post', transform (code, id) { const { pathname } = parseURL(decodeURIComponent(pathToFileURL(id).href)) - if (!pathname.match(/\.([cm][jt]sx?|vue)/)) { return } + if (!pathname.match(/\.(m?[jt]sx?|vue)/)) { return } if (!KEYED_FUNCTIONS_RE.test(code)) { return } const { 0: script = code, index: codeIndex = 0 } = code.match(/(?<=]*>)[\S\s.]*?(?=<\/script>)/) || [] const s = new MagicString(code) diff --git a/playground/app.tsx b/playground/app.tsx new file mode 100644 index 00000000000..711474979da --- /dev/null +++ b/playground/app.tsx @@ -0,0 +1,8 @@ +type A = string +export default defineComponent({ +async setup() { + const { data } = await useAsyncData(() => Promise.resolve({ foo: 'bar' })) + +return () =>
{'value:' + JSON.stringify(data.value)}
+} +}) diff --git a/playground/app.vue b/playground/app.vue deleted file mode 100644 index c93a9dcaf5f..00000000000 --- a/playground/app.vue +++ /dev/null @@ -1,12 +0,0 @@ - - - - - From bf022621dac3c11cd499e7671378295e1d1068d5 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Fri, 13 May 2022 13:45:46 +0100 Subject: [PATCH 11/21] docs: apply suggestions --- docs/content/3.api/1.composables/use-async-data.md | 4 ++-- docs/content/3.api/1.composables/use-fetch.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/content/3.api/1.composables/use-async-data.md b/docs/content/3.api/1.composables/use-async-data.md index 15268093e98..a1632812a07 100644 --- a/docs/content/3.api/1.composables/use-async-data.md +++ b/docs/content/3.api/1.composables/use-async-data.md @@ -13,7 +13,7 @@ function useAsyncData( key: string, handler: (nuxtApp?: NuxtApp) => Promise, options?: AsyncDataOptions -): AsyncData +): Promise> type AsyncDataOptions = { server?: boolean @@ -35,7 +35,7 @@ type AsyncData = { ## Params -* **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 and line number of the instance of `useAsyncData` will be generated for you. +* **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 * **options**: * _lazy_: whether to resolve the async function after loading the route, instead of blocking navigation (defaults to `false`) diff --git a/docs/content/3.api/1.composables/use-fetch.md b/docs/content/3.api/1.composables/use-fetch.md index 96f26e2bc62..c20d42be61d 100644 --- a/docs/content/3.api/1.composables/use-fetch.md +++ b/docs/content/3.api/1.composables/use-fetch.md @@ -8,7 +8,7 @@ This composable provides a convenient wrapper around [`useAsyncData`](/api/compo function useFetch( url: string | Request | Ref | () => string | Request, options?: UseFetchOptions -): AsyncData +): Promise> type UseFetchOptions = { method?: string From 4b20d10612b54397f83483054f5b8d9511d21247 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Fri, 13 May 2022 14:03:01 +0100 Subject: [PATCH 12/21] fix: remove changed files --- playground/app.tsx | 8 -------- playground/app.vue | 12 ++++++++++++ 2 files changed, 12 insertions(+), 8 deletions(-) delete mode 100644 playground/app.tsx create mode 100644 playground/app.vue diff --git a/playground/app.tsx b/playground/app.tsx deleted file mode 100644 index 711474979da..00000000000 --- a/playground/app.tsx +++ /dev/null @@ -1,8 +0,0 @@ -type A = string -export default defineComponent({ -async setup() { - const { data } = await useAsyncData
(() => Promise.resolve({ foo: 'bar' })) - -return () =>
{'value:' + JSON.stringify(data.value)}
-} -}) diff --git a/playground/app.vue b/playground/app.vue new file mode 100644 index 00000000000..c93a9dcaf5f --- /dev/null +++ b/playground/app.vue @@ -0,0 +1,12 @@ + + + + + From 88cf9f0ce5116162bb15273800e518019083e87c Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Fri, 13 May 2022 14:07:02 +0100 Subject: [PATCH 13/21] docs: update counter example --- .../composables/use-async-data/components/CounterExample.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/composables/use-async-data/components/CounterExample.vue b/examples/composables/use-async-data/components/CounterExample.vue index 7b4756d630f..14ea1d01a5a 100644 --- a/examples/composables/use-async-data/components/CounterExample.vue +++ b/examples/composables/use-async-data/components/CounterExample.vue @@ -1,6 +1,6 @@ From 040acb28e4a7c4dacb9f1b89d7178bdde25b8666 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Fri, 20 May 2022 12:56:28 +0100 Subject: [PATCH 14/21] refactor: simplify handling of fallback --- .../nuxt/src/app/composables/asyncData.ts | 40 +++++++++---------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/packages/nuxt/src/app/composables/asyncData.ts b/packages/nuxt/src/app/composables/asyncData.ts index 7735d189a87..f207b98d8fa 100644 --- a/packages/nuxt/src/app/composables/asyncData.ts +++ b/packages/nuxt/src/app/composables/asyncData.ts @@ -64,20 +64,17 @@ export function useAsyncData< Transform extends _Transform = _Transform, PickKeys extends KeyOfRes = KeyOfRes > ( - _key: string | ((ctx?: NuxtApp) => Promise), - _handler?: ((ctx?: NuxtApp) => Promise) | AsyncDataOptions | string, - _options?: AsyncDataOptions | string, - fallback?: string + ...args: [ + _key: string | ((ctx?: NuxtApp) => Promise), + _handler?: ((ctx?: NuxtApp) => Promise) | AsyncDataOptions | string, + _options?: AsyncDataOptions | string, + fallback?: string + ] ): AsyncData, PickKeys>, DataE | null | true> { + if (typeof args[0] !== 'string') { args.unshift(args.pop()) } + // eslint-disable-next-line prefer-const - let [key, handler, options] = (typeof _key === 'string' - ? [_key, _handler, _options] - : typeof _handler === 'string' - ? [_handler, _key, {}] - : typeof _options === 'string' - ? [_options, _key, {}] - : [fallback, _key, _options] - ) as [string, (ctx?: NuxtApp) => Promise, AsyncDataOptions] + let [key, handler, options] = args as [string, (ctx?: NuxtApp) => Promise, AsyncDataOptions] // Validate arguments if (typeof key !== 'string') { throw new TypeError('asyncData key must be a string') @@ -224,17 +221,16 @@ export function useLazyAsyncData< Transform extends _Transform = _Transform, PickKeys extends KeyOfRes = KeyOfRes > ( - _key: string | ((ctx?: NuxtApp) => Promise), - _handler?: ((ctx?: NuxtApp) => Promise) | AsyncDataOptions | string, - _options?: Omit, 'lazy'>, - fallback?: string + ...args: [ + _key: string | ((ctx?: NuxtApp) => Promise), + _handler?: ((ctx?: NuxtApp) => Promise) | AsyncDataOptions | string, + _options?: Omit, 'lazy'>, + fallback?: string + ] ): AsyncData, PickKeys>, DataE | null | true> { - const [key, handler, options] = (typeof _key === 'string' - ? [_key, _handler, _options] - : typeof _options === 'string' - ? [_options, _key, {}] - : [fallback, _key, _options] - ) as [string, (ctx?: NuxtApp) => Promise, AsyncDataOptions] + if (typeof args[0] !== 'string') { args.unshift(args.pop()) } + + const [key, handler, options] = args as [string, (ctx?: NuxtApp) => Promise, AsyncDataOptions] return useAsyncData(key, handler, { ...options, lazy: true }) } From bf63061691d0d1954d637ca48851a2b1b7a86cb5 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Wed, 8 Jun 2022 14:19:54 +0100 Subject: [PATCH 15/21] refactor: use `ohash` for hashing --- packages/vite/package.json | 1 + packages/vite/src/plugins/composable-keys.ts | 21 ++------------------ packages/vite/src/utils/index.ts | 9 +-------- yarn.lock | 1 + 4 files changed, 5 insertions(+), 27 deletions(-) diff --git a/packages/vite/package.json b/packages/vite/package.json index fdfa9a3412c..7e2b0edbfa6 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -37,6 +37,7 @@ "knitwork": "^0.1.1", "magic-string": "^0.26.2", "mlly": "^0.5.2", + "ohash": "^0.1.0", "pathe": "^0.3.0", "perfect-debounce": "^0.1.3", "postcss": "^8.4.14", diff --git a/packages/vite/src/plugins/composable-keys.ts b/packages/vite/src/plugins/composable-keys.ts index b916a966555..95469f0d6e5 100644 --- a/packages/vite/src/plugins/composable-keys.ts +++ b/packages/vite/src/plugins/composable-keys.ts @@ -1,10 +1,10 @@ -import crypto from 'node:crypto' import { pathToFileURL } from 'node:url' import type { Plugin } from 'vite' import { isAbsolute, relative } from 'pathe' import { parse } from 'acorn' import { walk } from 'estree-walker' import MagicString from 'magic-string' +import { hash } from 'ohash' import type { CallExpression } from 'estree' import { parseURL } from 'ufo' @@ -14,23 +14,6 @@ export interface ComposableKeysOptions { rootDir?: string } -const hashMap: Record = {} - -function createKey (source: string) { - let outputLength = 3 - do { - const hash = crypto.createHash('md5') - hash.update(source) - const key = hash.digest('base64').toString().slice(0, outputLength) - if (key in hashMap && hashMap[key] !== source) { - outputLength++ - continue - } - hashMap[key] = source - return key - } while (true) -} - const keyedFunctions = [ 'useState', 'useFetch', 'useAsyncData', 'useLazyAsyncData', 'useLazyFetch' ] @@ -60,7 +43,7 @@ export const composableKeysPlugin = (options: ComposableKeysOptions = {}): Plugi const end = (node as any).end s.appendLeft( codeIndex + end - 1, - (node.arguments.length ? ', ' : '') + "'$" + createKey(`${relativeID}-${codeIndex + end}`) + "'" + (node.arguments.length ? ', ' : '') + "'$" + hash(`${relativeID}-${codeIndex + end}`) + "'" ) } } diff --git a/packages/vite/src/utils/index.ts b/packages/vite/src/utils/index.ts index 79792452ba2..94b2bd5b13b 100644 --- a/packages/vite/src/utils/index.ts +++ b/packages/vite/src/utils/index.ts @@ -1,5 +1,5 @@ -import { createHash } from 'node:crypto' import { promises as fsp, readdirSync, statSync } from 'node:fs' +import { hash } from 'ohash' import { join } from 'pathe' export function uniq (arr: T[]): T[] { @@ -28,13 +28,6 @@ export function hashId (id: string) { 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) { return readdirSync(dir).reduce((files, file) => { const name = join(dir, file) diff --git a/yarn.lock b/yarn.lock index c967c736ba3..470ac4da05c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1755,6 +1755,7 @@ __metadata: knitwork: ^0.1.1 magic-string: ^0.26.2 mlly: ^0.5.2 + ohash: ^0.1.0 pathe: ^0.3.0 perfect-debounce: ^0.1.3 postcss: ^8.4.14 From eab5ec36b140da5649f3df80756d71e340460234 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Thu, 7 Jul 2022 01:36:41 +0200 Subject: [PATCH 16/21] fix playground,, asyncData with key, improve runtime checks and small refactors --- package.json | 4 +-- .../nuxt/src/app/composables/asyncData.ts | 32 ++++++------------- packages/nuxt/src/app/composables/fetch.ts | 25 ++++++++++----- packages/nuxt/src/app/composables/state.ts | 8 +++-- 4 files changed, 35 insertions(+), 34 deletions(-) diff --git a/package.json b/package.json index 94b4d2b9c7c..2fb707c8cbb 100644 --- a/package.json +++ b/package.json @@ -20,8 +20,8 @@ "lint": "eslint --ext .vue,.ts,.js,.mjs .", "lint:docs": "markdownlint ./docs/content && case-police 'docs/content**/*.md'", "lint:docs:fix": "markdownlint ./docs/content --fix && case-police 'docs/content**/*.md' --fix", - "nuxi": "NUXT_TELEMETRY_DISABLED=1 node ./packages/nuxi/bin/nuxi.mjs", - "nuxt": "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 JITI_ESM_RESOLVE=1 node ./packages/nuxi/bin/nuxi.mjs", "play": "echo use yarn dev && exit 1", "release": "yarn && yarn lint && FORCE_COLOR=1 lerna publish -m \"chore: release\" && yarn stub", "stub": "lerna run prepack -- --stub", diff --git a/packages/nuxt/src/app/composables/asyncData.ts b/packages/nuxt/src/app/composables/asyncData.ts index 41bcbd35b8d..0bcb1ce7477 100644 --- a/packages/nuxt/src/app/composables/asyncData.ts +++ b/packages/nuxt/src/app/composables/asyncData.ts @@ -70,24 +70,19 @@ export function useAsyncData< DataE = Error, Transform extends _Transform = _Transform, PickKeys extends KeyOfRes = KeyOfRes -> ( - ...args: [ - _key: string | ((ctx?: NuxtApp) => Promise), - _handler?: ((ctx?: NuxtApp) => Promise) | AsyncDataOptions | string, - _options?: AsyncDataOptions | string, - fallback?: string - ] -): AsyncData, PickKeys>, DataE | null | true> { - if (typeof args[0] !== 'string') { args.unshift(args.pop()) } +> (...args): AsyncData, 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, AsyncDataOptions] + let [key, handler, options = {}] = args as [string, (ctx?: NuxtApp) => Promise, AsyncDataOptions] + // Validate arguments 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') { - throw new TypeError('asyncData handler must be a function') + throw new TypeError('[nuxt] [asyncData] handler must be a function.') } // Apply defaults @@ -229,16 +224,9 @@ export function useLazyAsyncData< DataE = Error, Transform extends _Transform = _Transform, PickKeys extends KeyOfRes = KeyOfRes -> ( - ...args: [ - _key: string | ((ctx?: NuxtApp) => Promise), - _handler?: ((ctx?: NuxtApp) => Promise) | AsyncDataOptions | string, - _options?: Omit, 'lazy'>, - fallback?: string - ] -): AsyncData, PickKeys>, DataE | null | true> { - if (typeof args[0] !== 'string') { args.unshift(args.pop()) } - +> (...args): AsyncData, 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, AsyncDataOptions] return useAsyncData(key, handler, { ...options, lazy: true }) } diff --git a/packages/nuxt/src/app/composables/fetch.ts b/packages/nuxt/src/app/composables/fetch.ts index 040806665bb..a868170f55c 100644 --- a/packages/nuxt/src/app/composables/fetch.ts +++ b/packages/nuxt/src/app/composables/fetch.ts @@ -35,11 +35,19 @@ export function useFetch< PickKeys extends KeyOfRes = KeyOfRes > ( request: Ref | ReqT | (() => ReqT), - _opts?: string | UseFetchOptions<_ResT, Transform, PickKeys>, - _fallback?: string + arg1?: string | UseFetchOptions<_ResT, Transform, PickKeys>, + arg2?: string ) { - const [opts, fallback] = (typeof _opts === 'string' ? [{}, _opts] : [_opts, _fallback]) - const key = '$f_' + (typeof opts.key === 'string' ? opts.key : typeof request === 'string' && !opts.transform && !opts.default ? hash([request, opts]) : fallback) + const [opts, autoKey] = typeof arg1 === 'string' ? [{}, arg1] : [arg1, arg2] + const _key = opts.key || autoKey + if (!_key || typeof _key !== 'string') { + throw new TypeError('[nuxt] [useFetch] key must be a string: ' + _key) + } + if (!request) { + throw new Error('[nuxt] [useFetch] request is missing.') + } + const key = '$f' + _key + const _request = computed(() => { let r = request as Ref | FetchRequest | (() => FetchRequest) if (typeof r === 'function') { @@ -104,14 +112,15 @@ export function useLazyFetch< PickKeys extends KeyOfRes = KeyOfRes > ( request: Ref | ReqT | (() => ReqT), - _opts?: string | Omit, 'lazy'>, - _fallback?: string + arg1?: string | Omit, 'lazy'>, + arg2?: string ) { - const [opts, fallback] = (typeof _opts === 'string' ? [{}, _opts] : [_opts, _fallback]) + const [opts, autoKey] = typeof arg1 === 'string' ? [{}, arg1] : [arg1, arg2] + return useFetch(request, { ...opts, lazy: true }, // @ts-ignore - fallback) + autoKey) } diff --git a/packages/nuxt/src/app/composables/state.ts b/packages/nuxt/src/app/composables/state.ts index 4e1b0b65c71..8cbb11dc267 100644 --- a/packages/nuxt/src/app/composables/state.ts +++ b/packages/nuxt/src/app/composables/state.ts @@ -10,8 +10,12 @@ import { useNuxtApp } from '#app' */ export function useState (key: string, init?: (() => T | Ref)): Ref export function useState (init?: (() => T | Ref)): Ref -export function useState (_key?: string | (() => T | Ref), _init?: string | (() => T | Ref)): Ref { - const [key, init] = (typeof _key === 'string' ? [_key, _init] : [_init, _key]) as [string, (() => T | Ref)] +export function useState (...args): Ref { + const [_key, init] = (typeof args[0] === 'string' ? args : [args[1] /* auto key */, args[0]]) as [string, (() => T | Ref)] + if (!_key || typeof _key !== 'string') { throw new TypeError('[nuxt] [useState] key must be a string: ' + _key) } + if (init && typeof init !== 'function') { throw new Error('[nuxt] [useState] init must be a function: ' + init) } + const key = '$s' + _key + const nuxt = useNuxtApp() const state = toRef(nuxt.payload.state, key) if (state.value === undefined && init) { From 502cc11938e413b96b0b6fce951781e535283233 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Thu, 7 Jul 2022 01:39:17 +0200 Subject: [PATCH 17/21] fix lint issue --- packages/nuxt/src/app/composables/fetch.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/nuxt/src/app/composables/fetch.ts b/packages/nuxt/src/app/composables/fetch.ts index a868170f55c..93ad83e5fdf 100644 --- a/packages/nuxt/src/app/composables/fetch.ts +++ b/packages/nuxt/src/app/composables/fetch.ts @@ -1,6 +1,5 @@ import type { FetchOptions, FetchRequest } from 'ohmyfetch' import type { TypedInternalResponse, NitroFetchRequest } from 'nitropack' -import { hash } from 'ohash' import { computed, isRef, Ref } from 'vue' import type { AsyncDataOptions, _Transform, KeyOfRes, AsyncData, PickFrom } from './asyncData' import { useAsyncData } from './asyncData' From f116ff6fa7e691a9f70eada48c711da1533ac804 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Thu, 7 Jul 2022 09:34:42 +0200 Subject: [PATCH 18/21] fix: useState with explicit key --- packages/nuxt/src/app/composables/state.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/nuxt/src/app/composables/state.ts b/packages/nuxt/src/app/composables/state.ts index 8cbb11dc267..451e6987b97 100644 --- a/packages/nuxt/src/app/composables/state.ts +++ b/packages/nuxt/src/app/composables/state.ts @@ -8,12 +8,18 @@ import { useNuxtApp } from '#app' * @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 */ -export function useState (key: string, init?: (() => T | Ref)): Ref +export function useState (key?: string, init?: (() => T | Ref)): Ref export function useState (init?: (() => T | Ref)): Ref export function useState (...args): Ref { - const [_key, init] = (typeof args[0] === 'string' ? args : [args[1] /* auto key */, args[0]]) as [string, (() => T | Ref)] - if (!_key || typeof _key !== 'string') { throw new TypeError('[nuxt] [useState] key must be a string: ' + _key) } - if (init && typeof init !== 'function') { throw new Error('[nuxt] [useState] init must be a function: ' + init) } + 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)] + 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() From faab8b172b9c0c6f28ab0575e03bcb700f7da4d6 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Thu, 7 Jul 2022 09:37:27 +0100 Subject: [PATCH 19/21] refactor: use unplugin's built-in parser --- packages/vite/build.config.ts | 1 - packages/vite/src/plugins/composable-keys.ts | 18 +++++++----------- packages/vite/src/vite.ts | 2 +- packages/webpack/package.json | 1 - packages/webpack/src/webpack.ts | 6 +----- 5 files changed, 9 insertions(+), 19 deletions(-) diff --git a/packages/vite/build.config.ts b/packages/vite/build.config.ts index a1d247d9b99..e387f8754ce 100644 --- a/packages/vite/build.config.ts +++ b/packages/vite/build.config.ts @@ -10,7 +10,6 @@ export default defineBuildConfig({ 'vue' ], externals: [ - 'acorn', '@nuxt/schema' ] }) diff --git a/packages/vite/src/plugins/composable-keys.ts b/packages/vite/src/plugins/composable-keys.ts index 95469f0d6e5..df242c80db0 100644 --- a/packages/vite/src/plugins/composable-keys.ts +++ b/packages/vite/src/plugins/composable-keys.ts @@ -1,7 +1,6 @@ import { pathToFileURL } from 'node:url' -import type { Plugin } from 'vite' +import { createUnplugin } from 'unplugin' import { isAbsolute, relative } from 'pathe' -import { parse } from 'acorn' import { walk } from 'estree-walker' import MagicString from 'magic-string' import { hash } from 'ohash' @@ -10,7 +9,6 @@ import { parseURL } from 'ufo' export interface ComposableKeysOptions { sourcemap?: boolean - useAcorn?: boolean rootDir?: string } @@ -19,7 +17,7 @@ const keyedFunctions = [ ] const KEYED_FUNCTIONS_RE = new RegExp(`(${keyedFunctions.join('|')})`) -export const composableKeysPlugin = (options: ComposableKeysOptions = {}): Plugin => { +export const composableKeysPlugin = createUnplugin((options: ComposableKeysOptions = {}) => { return { name: 'nuxt:composable-keys', enforce: 'post', @@ -31,12 +29,10 @@ export const composableKeysPlugin = (options: ComposableKeysOptions = {}): Plugi const s = new MagicString(code) // https://github.com/unjs/unplugin/issues/90 const relativeID = isAbsolute(id) ? relative(options.rootDir, id) : id - walk(options.useAcorn - ? parse(script, { - sourceType: 'module', - ecmaVersion: 'latest' - }) - : this.parse(script), { + 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)) { @@ -56,4 +52,4 @@ export const composableKeysPlugin = (options: ComposableKeysOptions = {}): Plugi } } } -} +}) diff --git a/packages/vite/src/vite.ts b/packages/vite/src/vite.ts index 9a9b4b87628..29187cd8579 100644 --- a/packages/vite/src/vite.ts +++ b/packages/vite/src/vite.ts @@ -66,7 +66,7 @@ export async function bundle (nuxt: Nuxt) { } }, plugins: [ - composableKeysPlugin({ sourcemap: nuxt.options.sourcemap, rootDir: nuxt.options.rootDir }), + composableKeysPlugin.vite({ sourcemap: nuxt.options.sourcemap, rootDir: nuxt.options.rootDir }), replace({ ...Object.fromEntries([';', '(', '{', '}', ' ', '\t', '\n'].map(d => [`${d}global.`, `${d}globalThis.`])), preventAssignment: true diff --git a/packages/webpack/package.json b/packages/webpack/package.json index e4eb4810b27..dff12c230ec 100644 --- a/packages/webpack/package.json +++ b/packages/webpack/package.json @@ -19,7 +19,6 @@ "@babel/core": "^7.18.6", "@nuxt/friendly-errors-webpack-plugin": "^2.5.2", "@nuxt/kit": "^3.0.0-rc.4", - "acorn": "^8.7.1", "autoprefixer": "^10.4.7", "css-loader": "^6.7.1", "css-minimizer-webpack-plugin": "^4.0.0", diff --git a/packages/webpack/src/webpack.ts b/packages/webpack/src/webpack.ts index 8994b94a2d8..4696a289012 100644 --- a/packages/webpack/src/webpack.ts +++ b/packages/webpack/src/webpack.ts @@ -8,9 +8,8 @@ import type { Compiler, Watching } from 'webpack' import type { Nuxt } from '@nuxt/schema' import { joinURL } from 'ufo' import { logger, useNuxt } from '@nuxt/kit' -import { createUnplugin } from 'unplugin' import { DynamicBasePlugin } from '../../vite/src/plugins/dynamic-base' -import { composableKeysPlugin as _composableKeysPlugin } from '../../vite/src/plugins/composable-keys' +import { composableKeysPlugin } from '../../vite/src/plugins/composable-keys' import { createMFS } from './utils/mfs' import { registerVirtualModules } from './virtual-modules' import { client, server } from './configs' @@ -19,8 +18,6 @@ import { createWebpackConfigContext, applyPresets, getWebpackConfig } from './ut // TODO: Support plugins // const plugins: string[] = [] -const composableKeysPlugin = createUnplugin(_composableKeysPlugin as any) - export async function bundle (nuxt: Nuxt) { registerVirtualModules() @@ -42,7 +39,6 @@ export async function bundle (nuxt: Nuxt) { globalPublicPath: '__webpack_public_path__' })) config.plugins.push(composableKeysPlugin.webpack({ - useAcorn: true, sourcemap: nuxt.options.sourcemap, rootDir: nuxt.options.rootDir })) From 5964216dae0cfe2a48f465bfd51d08ff7244b6a0 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Thu, 7 Jul 2022 09:39:31 +0100 Subject: [PATCH 20/21] chore: update lockfile --- yarn.lock | 1 - 1 file changed, 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index f77ecf619e1..8129e98d012 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1816,7 +1816,6 @@ __metadata: "@types/webpack-dev-middleware": ^5.0.2 "@types/webpack-hot-middleware": ^2.25.6 "@types/webpack-virtual-modules": ^0 - acorn: ^8.7.1 autoprefixer: ^10.4.7 css-loader: ^6.7.1 css-minimizer-webpack-plugin: ^4.0.0 From 4c42bc6e85cb2bb636aba290b71fa4362aeb78e4 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Thu, 7 Jul 2022 18:17:57 +0200 Subject: [PATCH 21/21] add workaround for autogenerated key --- packages/nuxt/src/app/composables/asyncData.ts | 3 ++- packages/vite/src/plugins/composable-keys.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/nuxt/src/app/composables/asyncData.ts b/packages/nuxt/src/app/composables/asyncData.ts index 0bcb1ce7477..a644d78f977 100644 --- a/packages/nuxt/src/app/composables/asyncData.ts +++ b/packages/nuxt/src/app/composables/asyncData.ts @@ -228,7 +228,8 @@ export function useLazyAsyncData< 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, AsyncDataOptions] - return useAsyncData(key, handler, { ...options, lazy: true }) + // @ts-ignore + return useAsyncData(key, handler, { ...options, lazy: true }, null) } export function refreshNuxtData (keys?: string | string[]): Promise { diff --git a/packages/vite/src/plugins/composable-keys.ts b/packages/vite/src/plugins/composable-keys.ts index df242c80db0..9b4e40b6360 100644 --- a/packages/vite/src/plugins/composable-keys.ts +++ b/packages/vite/src/plugins/composable-keys.ts @@ -35,7 +35,7 @@ export const composableKeysPlugin = createUnplugin((options: ComposableKeysOptio }), { enter (node: CallExpression) { if (node.type !== 'CallExpression' || node.callee.type !== 'Identifier') { return } - if (keyedFunctions.includes(node.callee.name)) { + if (keyedFunctions.includes(node.callee.name) && node.arguments.length < 4) { const end = (node as any).end s.appendLeft( codeIndex + end - 1,