diff --git a/netlify.toml b/netlify.toml index e5a9e8809be..57a45386dbf 100755 --- a/netlify.toml +++ b/netlify.toml @@ -3,7 +3,7 @@ publish = "packages/.vitepress/dist" command = "pnpm run install-fonts && pnpm run docs:build" [build.environment] -NODE_OPTIONS = "--max_old_space_size=4096" +NODE_OPTIONS = "--max-old-space-size=5120" NODE_VERSION = "22" [[redirects]] diff --git a/package.json b/package.json index 6c7fc636776..8d8d91b00d5 100644 --- a/package.json +++ b/package.json @@ -3,19 +3,20 @@ "type": "module", "version": "13.6.0", "private": true, - "packageManager": "pnpm@10.13.1", + "packageManager": "pnpm@10.14.0", "description": "Collection of essential Vue Composition Utilities", "author": "Anthony Fu", "license": "MIT", "scripts": { "up": "nlx taze && nr size", - "build": "nr update && pnpm -r run build", + "build": "nr update && nr build:packages", + "build:packages": "pnpm -r run build", "build:redirects": "tsx scripts/redirects.ts", "build:types": "vue-tsc --emitDeclarationOnly", "clean": "tsx scripts/clean.ts", "dev": "nr update && nr docs", "docs": "vitepress dev packages --open", - "docs:build": "nr update:full && nr build && vitepress build packages && nr build:redirects", + "docs:build": "nr update:full && nr build:packages && NODE_OPTIONS=\"--max-old-space-size=5120\" ROLLDOWN_OPTIONS_VALIDATION=loose vitepress build packages && nr build:redirects", "docs:serve": "vitepress serve packages", "lint": "eslint --cache .", "lint:fix": "nr lint --fix", @@ -90,6 +91,7 @@ "postcss-nested": "catalog:dev", "prettier": "catalog:dev", "remove-markdown": "catalog:docs", + "rolldown-vite": "catalog:dev", "rollup": "catalog:dev", "rollup-plugin-dts": "catalog:dev", "rollup-plugin-esbuild": "catalog:dev", @@ -106,6 +108,7 @@ "unplugin-vue-components": "catalog:docs", "vite": "catalog:dev", "vite-plugin-inspect": "catalog:dev", + "vite-plugin-pwa": "catalog:docs", "vitepress": "catalog:docs", "vitest": "catalog:test", "vitest-browser-vue": "catalog:test", @@ -126,6 +129,7 @@ "rollup": "catalog:dev", "vite": "catalog:dev", "vite-plugin-pwa": "catalog:docs", + "vitepress>vite": "npm:rolldown-vite@latest", "vue": "catalog:dev" }, "simple-git-hooks": { diff --git a/packages/.vitepress/config.ts b/packages/.vitepress/config.ts index 0e36d40a46d..65a198a40d6 100644 --- a/packages/.vitepress/config.ts +++ b/packages/.vitepress/config.ts @@ -1,11 +1,13 @@ import { resolve } from 'node:path' import { transformerTwoslash } from '@shikijs/vitepress-twoslash' +import { createFileSystemTypesCache } from '@shikijs/vitepress-twoslash/cache-fs' import { withPwa } from '@vite-pwa/vitepress' import { defineConfig } from 'vitepress' import { currentVersion, versions } from '../../meta/versions' import { addonCategoryNames, categoryNames, coreCategoryNames, metadata } from '../metadata/metadata' import { PWAVirtual } from './plugins/pwa-virtual' import { transformHead } from './transformHead' +import { FILE_IMPORTS } from './twoslash' import viteConfig from './vite.config' const Guide = [ @@ -72,7 +74,17 @@ export default withPwa(defineConfig({ dark: 'vitesse-dark', }, codeTransformers: [ - transformerTwoslash(), + transformerTwoslash({ + twoslashOptions: { + handbookOptions: { + noErrors: true, + }, + }, + includesMap: new Map([['imports', `// ---cut-start---\n${FILE_IMPORTS}\n// ---cut-end---`]]), + typesCache: createFileSystemTypesCache({ + dir: resolve(__dirname, 'cache', 'twoslash'), + }), + }), ], languages: ['js', 'ts'], }, diff --git a/packages/.vitepress/plugins/markdownTransform.ts b/packages/.vitepress/plugins/markdownTransform.ts index ea23182e617..563c921cfe4 100644 --- a/packages/.vitepress/plugins/markdownTransform.ts +++ b/packages/.vitepress/plugins/markdownTransform.ts @@ -2,6 +2,7 @@ import type { Plugin } from 'vite' import { existsSync } from 'node:fs' import { join, resolve } from 'node:path' import { format } from 'prettier' +import { createTwoslasher } from 'twoslash' import ts from 'typescript' import { packages } from '../../../meta/packages' import { version as currentVersion } from '../../../package.json' @@ -15,6 +16,11 @@ export function MarkdownTransform(): Plugin { if (!hasTypes) console.warn('No types dist found, run `npm run build:types` first.') + const twoslasher = createTwoslasher({ + handbookOptions: { noErrors: true }, + customTags: ['include'], + }) + return { name: 'vueuse-md-transform', enforce: 'pre', @@ -44,16 +50,46 @@ export function MarkdownTransform(): Plugin { const firstHeader = code.search(/\n#{2,6}\s.+/) const sliceIndex = firstHeader < 0 ? frontmatterEnds < 0 ? 0 : frontmatterEnds + 4 : firstHeader + // Add vue code blocks to twoslash by default + code = await replaceAsync(code, /\n```vue( [^\n]+)?\n(.+?)\n```\n/gs, async (_, meta = '', snippet = '') => { + meta = replaceToDefaultTwoslashMeta(meta) + + if (isMetaTwoslash(meta)) { + snippet = injectCodeToTsVue(snippet, '// @include: imports') + } + + return ` +\`\`\`vue ${meta.trim()} +${snippet} +\`\`\` +` + }) + // Insert JS/TS code blocks - code = await replaceAsync(code, /\n```ts( [^\n]+)?\n(.+?)\n```\n/gs, async (_, meta = '', snippet = '') => { - const formattedTS = (await format(snippet.replace(/\n+/g, '\n'), { semi: false, singleQuote: true, parser: 'typescript' })).trim() + code = await replaceAsync(code, /\n```(?:ts|typescript)( [^\n]+)?\n(.+?)\n```\n/gs, async (_, meta = '', snippet = '') => { + meta = replaceToDefaultTwoslashMeta(meta) + + let snippetForCompare = snippet + if (isMetaTwoslash(meta)) { + // remove twoslash notations + snippetForCompare = twoslasher(snippet, 'ts').code + + // add vue auto imports + snippet = `// @include: imports\n${snippet}` + } + const formattedTS = (await format(snippetForCompare.replace(/\n+/g, '\n'), { semi: false, singleQuote: true, parser: 'typescript' })).trim() const js = ts.transpileModule(formattedTS, { compilerOptions: { target: 99 }, }) const formattedJS = (await format(js.outputText, { semi: false, singleQuote: true, parser: 'typescript' })) .trim() - if (formattedJS === formattedTS) - return _ + if (formattedJS === formattedTS) { + return ` +\`\`\`ts ${meta} +${snippet} +\`\`\` +` + } return `
@@ -107,7 +143,10 @@ export async function getFunctionMarkdown(pkg: string, name: string) { let typingSection = '' if (types) { - const code = `\`\`\`typescript\n${types.trim()}\n\`\`\`` + const code = `\`\`\`ts twoslash +// @include: imports +${types.trim()} +\`\`\`` typingSection = types.length > 1000 ? ` ## Type Declarations @@ -224,3 +263,58 @@ function replaceAsync(str: string, match: RegExp, replacer: (substring: string, }) return Promise.all(promises).then(replacements => str.replace(match, () => replacements.shift()!)) } + +const reLineHighlightMeta = /^\{[\d\-,]*\}$/ + +/** + * Replaces the given meta string with a default "twoslash" if it is empty or modifies it based on certain conditions. + * + * @param meta - The meta string to be processed. + * @returns The processed meta string. + * + * If the meta string is empty or only contains whitespace, it returns "twoslash". + * If the meta string contains "no-twoslash" (case insensitive), it removes "no-twoslash" and returns the remaining string. + * If the remaining string is empty after removing "no-twoslash", it returns an empty string. + * If the meta string matches the `reLineHighlightMeta` regex, it appends "twoslash" to the meta string. + * Otherwise, it returns the trimmed meta string. + */ +function replaceToDefaultTwoslashMeta(meta: string) { + const trimmed = meta.trim() + if (!trimmed) { + return 'twoslash' + } + const hasNoTwoslash = /no-twoslash/i.test(trimmed) + if (hasNoTwoslash) { + const leftover = trimmed.replace(/no-twoslash/i, '').trim() + if (!leftover) { + return '' + } + return leftover + } + if (reLineHighlightMeta.test(trimmed)) { + return `${trimmed} twoslash` + } + return trimmed +} + +function isMetaTwoslash(meta: string) { + return meta.includes('twoslash') && !meta.includes('no-twoslash') +} + +const scriptTagRegex = /]+\blang=["']ts["'][^>]*>/i + +function injectCodeToTsVue(vueContent: string, code: string): string { + const match = vueContent.match(scriptTagRegex) + if (!match) { + return vueContent + } + + const scriptTagStart = match.index! + const scriptTagLength = match[0].length + const insertPosition = scriptTagStart + scriptTagLength + const updatedContent + = `${vueContent.slice(0, insertPosition) + }\n${code}${vueContent.slice(insertPosition)}` + + return updatedContent +} diff --git a/packages/.vitepress/twoslash.ts b/packages/.vitepress/twoslash.ts new file mode 100644 index 00000000000..7f549e4625e --- /dev/null +++ b/packages/.vitepress/twoslash.ts @@ -0,0 +1,55 @@ +// eslint-disable-next-line no-restricted-imports +import * as vue from 'vue' + +function getModuleExports(module: any, exclude?: string[]) { + const exports = Object.keys(module) + return exports.filter(name => !exclude?.includes(name)) +} + +export function generateGlobalDeclsFromModule(moduleName: string, exports: string[] = [], exportTypes: string[] = []) { + const output: string[] = [] + + if (exports.length > 0) { + output.push(`declare global { +${exports + .map(name => `const ${name}: typeof import('${moduleName}').${name};`) + .join('\n')} +}`) + } + + if (exportTypes.length > 0) { + output.push(`declare global { + export type { ${exportTypes.join(', ')} } from 'vue'; + import '${moduleName}'; +}`) + } + + output.push('export {};') + + return output.join('\n\n') +} + +export function generateFileImports(moduleName: string, exports: string[] = [], exportTypes: string[] = []) { + const output: string[] = [] + + if (exports.length > 0) { + output.push(`import { ${exports.join(', ')} } from '${moduleName}';`) + } + + if (exportTypes.length > 0) { + output.push(`import type { ${exportTypes.join(', ')} } from '${moduleName}';`) + } + + return output.join('\n') +} + +const vueExports = getModuleExports(vue) + +// TODO: auto generate types +const vueTypes = ['Component', 'ComponentPublicInstance', 'ComputedRef', 'DirectiveBinding', 'ExtractDefaultPropTypes', 'ExtractPropTypes', 'ExtractPublicPropTypes', 'InjectionKey', 'PropType', 'Ref', 'MaybeRef', 'MaybeRefOrGetter', 'VNode', 'WritableComputedRef'] + +export const EXTRA_FILES = { + 'global-vue.ts': generateGlobalDeclsFromModule('vue', vueExports, vueTypes), +} + +export const FILE_IMPORTS = generateFileImports('vue', vueExports, vueTypes) diff --git a/packages/.vitepress/vite.config.ts b/packages/.vitepress/vite.config.ts index bb2b9b3ac80..71ae01ad99b 100644 --- a/packages/.vitepress/vite.config.ts +++ b/packages/.vitepress/vite.config.ts @@ -2,11 +2,11 @@ import { createRequire } from 'node:module' import { resolve } from 'node:path' import process from 'node:process' import { fileURLToPath } from 'node:url' +import { defineConfig } from 'rolldown-vite' import UnoCSS from 'unocss/vite' import IconsResolver from 'unplugin-icons/resolver' import Icons from 'unplugin-icons/vite' import Components from 'unplugin-vue-components/vite' -import { defineConfig } from 'vite' import Inspect from 'vite-plugin-inspect' import { getChangeLog, getFunctionContributors } from '../../scripts/changelog' import { ChangeLog } from './plugins/changelog' @@ -60,8 +60,7 @@ export default defineConfig({ '@vueuse/shared': resolve(__dirname, '../shared/index.ts'), '@vueuse/core': resolve(__dirname, '../core/index.ts'), '@vueuse/math': resolve(__dirname, '../math/index.ts'), - '@vueuse/integrations/useFocusTrap': resolve(__dirname, '../integrations/useFocusTrap/index.ts'), - '@vueuse/integrations': resolve(__dirname, '../integrations/index.ts'), + '@vueuse/integrations': resolve(__dirname, '../integrations'), '@vueuse/components': resolve(__dirname, '../components/index.ts'), '@vueuse/metadata': resolve(__dirname, '../metadata/index.ts'), }, @@ -97,7 +96,10 @@ export default defineConfig({ return 'vue' }, }, + /* TODO: unsupported options for Rolldown */ + // maxParallelFileOps: 5, }, + sourcemap: false, }, css: { postcss: { diff --git a/packages/core/computedAsync/demo.vue b/packages/core/computedAsync/demo.vue new file mode 100644 index 00000000000..1af6942f96e --- /dev/null +++ b/packages/core/computedAsync/demo.vue @@ -0,0 +1,52 @@ + + + diff --git a/packages/core/computedAsync/index.md b/packages/core/computedAsync/index.md index 5f08717a699..8d486d37207 100644 --- a/packages/core/computedAsync/index.md +++ b/packages/core/computedAsync/index.md @@ -9,7 +9,7 @@ Computed for async functions ## Usage -```js +```ts import { computedAsync } from '@vueuse/core' import { shallowRef } from 'vue' @@ -19,7 +19,7 @@ const userInfo = computedAsync( async () => { return await mockLookUp(name.value) }, - null, // initial state + null, /* initial state */ ) ``` @@ -27,7 +27,7 @@ const userInfo = computedAsync( You will need to pass a ref to track if the async function is evaluating. -```js +```ts import { computedAsync } from '@vueuse/core' import { shallowRef } from 'vue' @@ -44,7 +44,9 @@ const userInfo = computedAsync( When the computed source changed before the previous async function gets resolved, you may want to cancel the previous one. Here is an example showing how to incorporate with the fetch API. -```js +```ts +import { computedAsync } from '@vueuse/core' +// ---cut--- const packageName = shallowRef('@vueuse/core') const downloads = computedAsync(async (onCancel) => { @@ -65,7 +67,7 @@ const downloads = computedAsync(async (onCancel) => { By default, `computedAsync` will start resolving immediately on creation, specify `lazy: true` to make it start resolving on the first accessing. -```js +```ts import { computedAsync } from '@vueuse/core' import { shallowRef } from 'vue' diff --git a/packages/core/computedAsync/index.test.ts b/packages/core/computedAsync/index.test.ts index 9e193449df0..cd67dd1d6c8 100644 --- a/packages/core/computedAsync/index.test.ts +++ b/packages/core/computedAsync/index.test.ts @@ -55,6 +55,28 @@ describe('computedAsync', () => { expectTypeOf(data2).toEqualTypeOf>() }) + it('default onError in computedAsync uses globalThis.reportError', async () => { + const originalReportError = globalThis.reportError + const mockReportError = vi.fn() + globalThis.reportError = mockReportError + + const error = new Error('An Error Message') + const func = vi.fn(async () => { + throw error + }) + + const data = computedAsync(func, undefined) + + expect(func).toBeCalledTimes(1) + + expect(data.value).toBeUndefined() + + await nextTick() + expect(data.value).toBeUndefined() + expect(mockReportError).toHaveBeenCalledWith(error) + globalThis.reportError = originalReportError + }) + it('call onError when error is thrown', async () => { const errorMessage = shallowRef() const func = vi.fn(async () => { diff --git a/packages/core/computedAsync/index.ts b/packages/core/computedAsync/index.ts index dce7018cc0c..67ebd3ce1d4 100644 --- a/packages/core/computedAsync/index.ts +++ b/packages/core/computedAsync/index.ts @@ -101,7 +101,7 @@ export function computedAsync( flush = 'pre', evaluating = undefined, shallow = true, - onError = noop, + onError = globalThis.reportError ?? noop, } = options const started = shallowRef(!lazy) diff --git a/packages/core/computedInject/index.md b/packages/core/computedInject/index.md index 6526ae134c6..5a50d77662f 100644 --- a/packages/core/computedInject/index.md +++ b/packages/core/computedInject/index.md @@ -10,7 +10,7 @@ Combine computed and inject In Provider Component -```ts +```ts twoslash include main import type { InjectionKey, Ref } from 'vue' import { provide } from 'vue' @@ -29,6 +29,9 @@ provide(ArrayKey, array) In Receiver Component ```ts +// @filename: provider.ts +// @include: main +// ---cut--- import { computedInject } from '@vueuse/core' import { ArrayKey } from './provider' diff --git a/packages/core/createReusableTemplate/index.md b/packages/core/createReusableTemplate/index.md index b6fb47c5fab..1f786076e9a 100644 --- a/packages/core/createReusableTemplate/index.md +++ b/packages/core/createReusableTemplate/index.md @@ -31,7 +31,7 @@ So this function is made to provide a way for defining and reusing templates ins In the previous example, we could refactor it to: ```vue - - return { - input, - } - }, -} + ``` diff --git a/packages/core/unrefElement/index.md b/packages/core/unrefElement/index.md index 24f93e28e9e..690aaf2a44e 100644 --- a/packages/core/unrefElement/index.md +++ b/packages/core/unrefElement/index.md @@ -9,7 +9,7 @@ Retrieves the underlying DOM element from a Vue ref or component instance ## Usage ```vue - -```vue