From 4a3ce4cc1dd676e86b0ae377b47d38fee80619b0 Mon Sep 17 00:00:00 2001 From: Ovidiu U Date: Sat, 11 Apr 2026 16:28:36 +0100 Subject: [PATCH] docs: add advanced skills for Vitest, Pinia, and Vue built-ins Add comprehensive reference documentation for: - Vitest: environments, projects/workspaces, type testing, vi utilities - Pinia: HMR, Nuxt integration, SSR setup - Vue: built-in components (Transition, Teleport, Suspense, KeepAlive) and advanced directives --- .agents/skills/pinia/GENERATION.md | 5 + .agents/skills/pinia/SKILL.md | 59 ++ .../skills/pinia/references/advanced-hmr.md | 61 ++ .../skills/pinia/references/advanced-nuxt.md | 119 ++++ .../skills/pinia/references/advanced-ssr.md | 121 ++++ .../best-practices-outside-component.md | 115 ++++ .../references/best-practices-testing.md | 212 +++++++ .../skills/pinia/references/core-stores.md | 389 +++++++++++++ .../pinia/references/features-composables.md | 114 ++++ .../references/features-composing-stores.md | 134 +++++ .../pinia/references/features-plugins.md | 203 +++++++ .agents/skills/vite/GENERATION.md | 5 + .agents/skills/vite/SKILL.md | 72 +++ .../skills/vite/references/build-and-ssr.md | 164 ++++++ .agents/skills/vite/references/core-config.md | 162 ++++++ .../skills/vite/references/core-features.md | 205 +++++++ .../skills/vite/references/core-plugin-api.md | 235 ++++++++ .../skills/vite/references/environment-api.md | 108 ++++ .../vite/references/rolldown-migration.md | 157 +++++ .agents/skills/vitest/GENERATION.md | 5 + .agents/skills/vitest/SKILL.md | 52 ++ .../references/advanced-environments.md | 264 +++++++++ .../vitest/references/advanced-projects.md | 300 ++++++++++ .../references/advanced-type-testing.md | 237 ++++++++ .../skills/vitest/references/advanced-vi.md | 249 ++++++++ .agents/skills/vitest/references/core-cli.md | 166 ++++++ .../skills/vitest/references/core-config.md | 174 ++++++ .../skills/vitest/references/core-describe.md | 193 +++++++ .../skills/vitest/references/core-expect.md | 219 +++++++ .../skills/vitest/references/core-hooks.md | 244 ++++++++ .../skills/vitest/references/core-test-api.md | 233 ++++++++ .../vitest/references/features-concurrency.md | 250 ++++++++ .../vitest/references/features-context.md | 238 ++++++++ .../vitest/references/features-coverage.md | 207 +++++++ .../vitest/references/features-filtering.md | 211 +++++++ .../vitest/references/features-mocking.md | 265 +++++++++ .../vitest/references/features-snapshots.md | 207 +++++++ .agents/skills/vue-best-practices/LICENSE.md | 21 + .agents/skills/vue-best-practices/SKILL.md | 154 +++++ .agents/skills/vue-best-practices/SYNC.md | 5 + .../animation-class-based-technique.md | 254 ++++++++ .../animation-state-driven-technique.md | 291 ++++++++++ .../references/component-async.md | 97 ++++ .../references/component-data-flow.md | 307 ++++++++++ .../references/component-fallthrough-attrs.md | 174 ++++++ .../references/component-keep-alive.md | 137 +++++ .../references/component-slots.md | 216 +++++++ .../references/component-suspense.md | 228 ++++++++ .../references/component-teleport.md | 108 ++++ .../references/component-transition-group.md | 128 ++++ .../references/component-transition.md | 125 ++++ .../references/composables.md | 290 ++++++++++ .../references/directives.md | 162 ++++++ ...rf-avoid-component-abstraction-in-lists.md | 159 +++++ .../perf-v-once-v-memo-directives.md | 182 ++++++ .../references/perf-virtualize-large-lists.md | 187 ++++++ .../vue-best-practices/references/plugins.md | 166 ++++++ .../references/reactivity.md | 344 +++++++++++ .../references/render-functions.md | 201 +++++++ .../vue-best-practices/references/sfc.md | 310 ++++++++++ .../references/state-management.md | 135 +++++ .../references/updated-hook-performance.md | 187 ++++++ .../vue-router-best-practices/LICENSE.md | 21 + .../skills/vue-router-best-practices/SKILL.md | 23 + .../skills/vue-router-best-practices/SYNC.md | 5 + .../router-beforeenter-no-param-trigger.md | 167 ++++++ .../router-beforerouteenter-no-this.md | 176 ++++++ .../router-guard-async-await-pattern.md | 227 ++++++++ .../router-navigation-guard-infinite-loop.md | 187 ++++++ ...router-navigation-guard-next-deprecated.md | 150 +++++ .../router-param-change-no-lifecycle.md | 181 ++++++ .../router-simple-routing-cleanup.md | 209 +++++++ .../router-use-vue-router-for-production.md | 183 ++++++ .../vue-testing-best-practices/LICENSE.md | 21 + .../vue-testing-best-practices/SKILL.md | 29 + .../skills/vue-testing-best-practices/SYNC.md | 5 + .../reference/async-component-testing.md | 163 ++++++ .../reference/teleport-testing-complexity.md | 158 +++++ .../testing-async-await-flushpromises.md | 175 ++++++ .../testing-browser-vs-node-runners.md | 208 +++++++ .../testing-component-blackbox-approach.md | 144 +++++ .../testing-composables-helper-wrapper.md | 238 ++++++++ .../testing-e2e-playwright-recommended.md | 242 ++++++++ .../reference/testing-no-snapshot-only.md | 197 +++++++ .../reference/testing-pinia-store-setup.md | 228 ++++++++ .../testing-suspense-async-components.md | 229 ++++++++ .../testing-vitest-recommended-for-vue.md | 204 +++++++ .agents/skills/vue/GENERATION.md | 5 + .agents/skills/vue/SKILL.md | 84 +++ .../vue/references/advanced-patterns.md | 314 ++++++++++ .../skills/vue/references/core-new-apis.md | 264 +++++++++ .../vue/references/script-setup-macros.md | 204 +++++++ .agents/skills/vueuse-functions/LICENSE.md | 21 + .agents/skills/vueuse-functions/SKILL.md | 419 ++++++++++++++ .agents/skills/vueuse-functions/SYNC.md | 5 + .../references/computedAsync.md | 195 +++++++ .../references/computedEager.md | 62 ++ .../references/computedInject.md | 137 +++++ .../references/computedWithControl.md | 98 ++++ .../references/createEventHook.md | 86 +++ .../references/createGenericProjection.md | 25 + .../references/createGlobalState.md | 95 +++ .../references/createInjectionState.md | 215 +++++++ .../references/createProjection.md | 31 + .../vueuse-functions/references/createRef.md | 54 ++ .../references/createReusableTemplate.md | 357 ++++++++++++ .../references/createSharedComposable.md | 42 ++ .../references/createTemplatePromise.md | 306 ++++++++++ .../references/createUnrefFn.md | 51 ++ .../vueuse-functions/references/extendRef.md | 76 +++ .../vueuse-functions/references/from.md | 80 +++ .../skills/vueuse-functions/references/get.md | 30 + .../references/injectLocal.md | 35 ++ .../vueuse-functions/references/isDefined.md | 31 + .../vueuse-functions/references/logicAnd.md | 40 ++ .../vueuse-functions/references/logicNot.md | 36 ++ .../vueuse-functions/references/logicOr.md | 40 ++ .../references/makeDestructurable.md | 41 ++ .../references/onClickOutside.md | 228 ++++++++ .../references/onElementRemoval.md | 88 +++ .../references/onKeyStroke.md | 211 +++++++ .../references/onLongPress.md | 229 ++++++++ .../references/onStartTyping.md | 53 ++ .../references/provideLocal.md | 37 ++ .../vueuse-functions/references/reactify.md | 144 +++++ .../references/reactifyObject.md | 62 ++ .../references/reactiveComputed.md | 34 ++ .../references/reactiveOmit.md | 86 +++ .../references/reactivePick.md | 106 ++++ .../references/refAutoReset.md | 46 ++ .../references/refDebounced.md | 81 +++ .../vueuse-functions/references/refDefault.md | 36 ++ .../references/refManualReset.md | 44 ++ .../references/refThrottled.md | 99 ++++ .../references/refWithControl.md | 146 +++++ .../skills/vueuse-functions/references/set.md | 30 + .../vueuse-functions/references/syncRef.md | 195 +++++++ .../vueuse-functions/references/syncRefs.md | 128 ++++ .../references/templateRef.md | 86 +++ .../vueuse-functions/references/toObserver.md | 38 ++ .../vueuse-functions/references/toReactive.md | 41 ++ .../vueuse-functions/references/toRef.md | 75 +++ .../vueuse-functions/references/toRefs.md | 81 +++ .../references/tryOnBeforeMount.md | 34 ++ .../references/tryOnBeforeUnmount.md | 32 + .../references/tryOnMounted.md | 34 ++ .../references/tryOnScopeDispose.md | 31 + .../references/tryOnUnmounted.md | 32 + .../references/unrefElement.md | 54 ++ .../vueuse-functions/references/until.md | 161 ++++++ .../vueuse-functions/references/useAbs.md | 31 + .../references/useActiveElement.md | 86 +++ .../vueuse-functions/references/useAnimate.md | 180 ++++++ .../references/useArrayDifference.md | 84 +++ .../references/useArrayEvery.md | 59 ++ .../references/useArrayFilter.md | 63 ++ .../references/useArrayFind.md | 50 ++ .../references/useArrayFindIndex.md | 59 ++ .../references/useArrayFindLast.md | 52 ++ .../references/useArrayIncludes.md | 63 ++ .../references/useArrayJoin.md | 74 +++ .../references/useArrayMap.md | 59 ++ .../references/useArrayReduce.md | 81 +++ .../references/useArraySome.md | 59 ++ .../references/useArrayUnique.md | 76 +++ .../references/useAsyncQueue.md | 136 +++++ .../references/useAsyncState.md | 185 ++++++ .../references/useAsyncValidator.md | 70 +++ .../vueuse-functions/references/useAuth.md | 123 ++++ .../vueuse-functions/references/useAverage.md | 36 ++ .../vueuse-functions/references/useAxios.md | 325 +++++++++++ .../vueuse-functions/references/useBase64.md | 136 +++++ .../vueuse-functions/references/useBattery.md | 80 +++ .../references/useBluetooth.md | 174 ++++++ .../references/useBreakpoints.md | 176 ++++++ .../references/useBroadcastChannel.md | 73 +++ .../references/useBrowserLocation.md | 56 ++ .../vueuse-functions/references/useCached.md | 52 ++ .../vueuse-functions/references/useCeil.md | 31 + .../references/useChangeCase.md | 79 +++ .../vueuse-functions/references/useClamp.md | 85 +++ .../references/useClipboard.md | 122 ++++ .../references/useClipboardItems.md | 93 +++ .../vueuse-functions/references/useCloned.md | 91 +++ .../references/useColorMode.md | 172 ++++++ .../references/useConfirmDialog.md | 159 +++++ .../vueuse-functions/references/useCookies.md | 162 ++++++ .../references/useCountdown.md | 105 ++++ .../vueuse-functions/references/useCounter.md | 86 +++ .../references/useCssSupports.md | 33 ++ .../vueuse-functions/references/useCssVar.md | 50 ++ .../references/useCurrentElement.md | 61 ++ .../references/useCycleList.md | 75 +++ .../vueuse-functions/references/useDark.md | 142 +++++ .../references/useDateFormat.md | 145 +++++ .../references/useDebounceFn.md | 100 ++++ .../references/useDebouncedRefHistory.md | 40 ++ .../references/useDeviceMotion.md | 80 +++ .../references/useDeviceOrientation.md | 64 ++ .../references/useDevicePixelRatio.md | 47 ++ .../references/useDevicesList.md | 89 +++ .../references/useDisplayMedia.md | 67 +++ .../references/useDocumentVisibility.md | 44 ++ .../references/useDraggable.md | 289 +++++++++ .../vueuse-functions/references/useDrauu.md | 65 +++ .../references/useDropZone.md | 83 +++ .../references/useElementBounding.md | 131 +++++ .../references/useElementByPoint.md | 46 ++ .../references/useElementHover.md | 79 +++ .../references/useElementSize.md | 79 +++ .../references/useElementVisibility.md | 123 ++++ .../references/useEventBus.md | 101 ++++ .../references/useEventListener.md | 226 ++++++++ .../references/useEventSource.md | 204 +++++++ .../references/useExtractedObservable.md | 198 +++++++ .../references/useEyeDropper.md | 72 +++ .../vueuse-functions/references/useFavicon.md | 69 +++ .../vueuse-functions/references/useFetch.md | 546 ++++++++++++++++++ .../references/useFileDialog.md | 91 +++ .../references/useFileSystemAccess.md | 161 ++++++ .../references/useFirestore.md | 129 +++++ .../vueuse-functions/references/useFloor.md | 31 + .../vueuse-functions/references/useFocus.md | 99 ++++ .../references/useFocusTrap.md | 245 ++++++++ .../references/useFocusWithin.md | 57 ++ .../vueuse-functions/references/useFps.md | 28 + .../references/useFullscreen.md | 74 +++ .../vueuse-functions/references/useFuse.md | 75 +++ .../vueuse-functions/references/useGamepad.md | 178 ++++++ .../references/useGeolocation.md | 63 ++ .../references/useIDBKeyval.md | 93 +++ .../vueuse-functions/references/useIdle.md | 88 +++ .../vueuse-functions/references/useImage.md | 90 +++ .../references/useInfiniteScroll.md | 156 +++++ .../references/useIntersectionObserver.md | 117 ++++ .../references/useInterval.md | 112 ++++ .../references/useIntervalFn.md | 50 ++ .../references/useIpcRenderer.md | 144 +++++ .../references/useIpcRendererInvoke.md | 58 ++ .../references/useIpcRendererOn.md | 52 ++ .../vueuse-functions/references/useJwt.md | 57 ++ .../references/useKeyModifier.md | 87 +++ .../references/useLastChanged.md | 63 ++ .../references/useLocalStorage.md | 41 ++ .../references/useMagicKeys.md | 245 ++++++++ .../references/useManualRefHistory.md | 204 +++++++ .../vueuse-functions/references/useMath.md | 47 ++ .../vueuse-functions/references/useMax.md | 36 ++ .../references/useMediaControls.md | 201 +++++++ .../references/useMediaQuery.md | 53 ++ .../vueuse-functions/references/useMemoize.md | 175 ++++++ .../vueuse-functions/references/useMemory.md | 70 +++ .../vueuse-functions/references/useMin.md | 36 ++ .../vueuse-functions/references/useMounted.md | 38 ++ .../vueuse-functions/references/useMouse.md | 113 ++++ .../references/useMouseInElement.md | 132 +++++ .../references/useMousePressed.md | 116 ++++ .../references/useMutationObserver.md | 60 ++ .../references/useNProgress.md | 78 +++ .../references/useNavigatorLanguage.md | 57 ++ .../vueuse-functions/references/useNetwork.md | 106 ++++ .../vueuse-functions/references/useNow.md | 83 +++ .../references/useObjectUrl.md | 55 ++ .../references/useObservable.md | 91 +++ .../references/useOffsetPagination.md | 199 +++++++ .../vueuse-functions/references/useOnline.md | 41 ++ .../references/usePageLeave.md | 43 ++ .../references/useParallax.md | 58 ++ .../references/useParentElement.md | 54 ++ .../references/usePerformanceObserver.md | 48 ++ .../references/usePermission.md | 78 +++ .../vueuse-functions/references/usePointer.md | 91 +++ .../references/usePointerLock.md | 59 ++ .../references/usePointerSwipe.md | 80 +++ .../references/usePrecision.md | 49 ++ .../references/usePreferredColorScheme.md | 42 ++ .../references/usePreferredContrast.md | 42 ++ .../references/usePreferredDark.md | 41 ++ .../references/usePreferredLanguages.md | 41 ++ .../references/usePreferredReducedMotion.md | 42 ++ .../usePreferredReducedTransparency.md | 42 ++ .../references/usePrevious.md | 40 ++ .../references/useProjection.md | 38 ++ .../vueuse-functions/references/useQRCode.md | 53 ++ .../vueuse-functions/references/useRTDB.md | 83 +++ .../vueuse-functions/references/useRafFn.md | 68 +++ .../references/useRefHistory.md | 285 +++++++++ .../references/useResizeObserver.md | 108 ++++ .../vueuse-functions/references/useRound.md | 31 + .../references/useRouteHash.md | 27 + .../references/useRouteParams.md | 38 ++ .../references/useRouteQuery.md | 79 +++ .../references/useSSRWidth.md | 47 ++ .../references/useScreenOrientation.md | 98 ++++ .../references/useScreenSafeArea.md | 60 ++ .../references/useScriptTag.md | 116 ++++ .../vueuse-functions/references/useScroll.md | 238 ++++++++ .../references/useScrollLock.md | 66 +++ .../references/useSessionStorage.md | 41 ++ .../vueuse-functions/references/useShare.md | 67 +++ .../references/useSortable.md | 235 ++++++++ .../vueuse-functions/references/useSorted.md | 90 +++ .../references/useSpeechRecognition.md | 90 +++ .../references/useSpeechSynthesis.md | 101 ++++ .../vueuse-functions/references/useStepper.md | 137 +++++ .../vueuse-functions/references/useStorage.md | 278 +++++++++ .../references/useStorageAsync.md | 136 +++++ .../references/useStyleTag.md | 131 +++++ .../vueuse-functions/references/useSubject.md | 77 +++ .../references/useSubscription.md | 33 ++ .../vueuse-functions/references/useSum.md | 36 ++ .../references/useSupported.md | 29 + .../vueuse-functions/references/useSwipe.md | 75 +++ .../references/useTemplateRefsList.md | 37 ++ .../references/useTextDirection.md | 75 +++ .../references/useTextSelection.md | 43 ++ .../references/useTextareaAutosize.md | 94 +++ .../references/useThrottleFn.md | 57 ++ .../references/useThrottledRefHistory.md | 47 ++ .../vueuse-functions/references/useTimeAgo.md | 154 +++++ .../references/useTimeAgoIntl.md | 117 ++++ .../vueuse-functions/references/useTimeout.md | 113 ++++ .../references/useTimeoutFn.md | 51 ++ .../references/useTimeoutPoll.md | 47 ++ .../references/useTimestamp.md | 93 +++ .../vueuse-functions/references/useTitle.md | 115 ++++ .../references/useToNumber.md | 54 ++ .../references/useToString.md | 34 ++ .../vueuse-functions/references/useToggle.md | 103 ++++ .../references/useTransition.md | 265 +++++++++ .../vueuse-functions/references/useTrunc.md | 33 ++ .../references/useUrlSearchParams.md | 121 ++++ .../references/useUserMedia.md | 96 +++ .../vueuse-functions/references/useVModel.md | 182 ++++++ .../vueuse-functions/references/useVModels.md | 67 +++ .../vueuse-functions/references/useVibrate.md | 86 +++ .../references/useVirtualList.md | 182 ++++++ .../references/useWakeLock.md | 51 ++ .../references/useWebNotification.md | 175 ++++++ .../references/useWebSocket.md | 299 ++++++++++ .../references/useWebWorker.md | 60 ++ .../references/useWebWorkerFn.md | 102 ++++ .../references/useWindowFocus.md | 46 ++ .../references/useWindowScroll.md | 46 ++ .../references/useWindowSize.md | 78 +++ .../references/useZoomFactor.md | 53 ++ .../references/useZoomLevel.md | 53 ++ .../vueuse-functions/references/watchArray.md | 53 ++ .../references/watchAtMost.md | 55 ++ .../references/watchDebounced.md | 101 ++++ .../vueuse-functions/references/watchDeep.md | 54 ++ .../references/watchExtractedObservable.md | 192 ++++++ .../references/watchIgnorable.md | 120 ++++ .../references/watchImmediate.md | 44 ++ .../vueuse-functions/references/watchOnce.md | 41 ++ .../references/watchPausable.md | 86 +++ .../references/watchThrottled.md | 108 ++++ .../references/watchTriggerable.md | 98 ++++ .../references/watchWithFilter.md | 54 ++ .../vueuse-functions/references/whenever.md | 100 ++++ .agents/skills/web-design-guidelines/SKILL.md | 39 ++ .agents/skills/web-design-guidelines/SYNC.md | 5 + .claude/skills/pinia | 1 + .claude/skills/vite | 1 + .claude/skills/vitest | 1 + .claude/skills/vue | 1 + .claude/skills/vue-best-practices | 1 + .claude/skills/vue-router-best-practices | 1 + .claude/skills/vue-testing-best-practices | 1 + .claude/skills/vueuse-functions | 1 + .claude/skills/web-design-guidelines | 1 + .continue/skills/antfu | 1 + .continue/skills/nuxt | 1 + .continue/skills/pinia | 1 + .continue/skills/pnpm | 1 + .continue/skills/slidev | 1 + .continue/skills/tsdown | 1 + .continue/skills/turborepo | 1 + .continue/skills/unocss | 1 + .continue/skills/vite | 1 + .continue/skills/vitepress | 1 + .continue/skills/vitest | 1 + .continue/skills/vue | 1 + .continue/skills/vue-best-practices | 1 + .continue/skills/vue-router-best-practices | 1 + .continue/skills/vue-testing-best-practices | 1 + .continue/skills/vueuse-functions | 1 + .continue/skills/web-design-guidelines | 1 + .kiro/skills/antfu | 1 + .kiro/skills/nuxt | 1 + .kiro/skills/pinia | 1 + .kiro/skills/pnpm | 1 + .kiro/skills/slidev | 1 + .kiro/skills/tsdown | 1 + .kiro/skills/turborepo | 1 + .kiro/skills/unocss | 1 + .kiro/skills/vite | 1 + .kiro/skills/vitepress | 1 + .kiro/skills/vitest | 1 + .kiro/skills/vue | 1 + .kiro/skills/vue-best-practices | 1 + .kiro/skills/vue-router-best-practices | 1 + .kiro/skills/vue-testing-best-practices | 1 + .kiro/skills/vueuse-functions | 1 + .kiro/skills/web-design-guidelines | 1 + 405 files changed, 41122 insertions(+) create mode 100644 .agents/skills/pinia/GENERATION.md create mode 100644 .agents/skills/pinia/SKILL.md create mode 100644 .agents/skills/pinia/references/advanced-hmr.md create mode 100644 .agents/skills/pinia/references/advanced-nuxt.md create mode 100644 .agents/skills/pinia/references/advanced-ssr.md create mode 100644 .agents/skills/pinia/references/best-practices-outside-component.md create mode 100644 .agents/skills/pinia/references/best-practices-testing.md create mode 100644 .agents/skills/pinia/references/core-stores.md create mode 100644 .agents/skills/pinia/references/features-composables.md create mode 100644 .agents/skills/pinia/references/features-composing-stores.md create mode 100644 .agents/skills/pinia/references/features-plugins.md create mode 100644 .agents/skills/vite/GENERATION.md create mode 100644 .agents/skills/vite/SKILL.md create mode 100644 .agents/skills/vite/references/build-and-ssr.md create mode 100644 .agents/skills/vite/references/core-config.md create mode 100644 .agents/skills/vite/references/core-features.md create mode 100644 .agents/skills/vite/references/core-plugin-api.md create mode 100644 .agents/skills/vite/references/environment-api.md create mode 100644 .agents/skills/vite/references/rolldown-migration.md create mode 100644 .agents/skills/vitest/GENERATION.md create mode 100644 .agents/skills/vitest/SKILL.md create mode 100644 .agents/skills/vitest/references/advanced-environments.md create mode 100644 .agents/skills/vitest/references/advanced-projects.md create mode 100644 .agents/skills/vitest/references/advanced-type-testing.md create mode 100644 .agents/skills/vitest/references/advanced-vi.md create mode 100644 .agents/skills/vitest/references/core-cli.md create mode 100644 .agents/skills/vitest/references/core-config.md create mode 100644 .agents/skills/vitest/references/core-describe.md create mode 100644 .agents/skills/vitest/references/core-expect.md create mode 100644 .agents/skills/vitest/references/core-hooks.md create mode 100644 .agents/skills/vitest/references/core-test-api.md create mode 100644 .agents/skills/vitest/references/features-concurrency.md create mode 100644 .agents/skills/vitest/references/features-context.md create mode 100644 .agents/skills/vitest/references/features-coverage.md create mode 100644 .agents/skills/vitest/references/features-filtering.md create mode 100644 .agents/skills/vitest/references/features-mocking.md create mode 100644 .agents/skills/vitest/references/features-snapshots.md create mode 100644 .agents/skills/vue-best-practices/LICENSE.md create mode 100644 .agents/skills/vue-best-practices/SKILL.md create mode 100644 .agents/skills/vue-best-practices/SYNC.md create mode 100644 .agents/skills/vue-best-practices/references/animation-class-based-technique.md create mode 100644 .agents/skills/vue-best-practices/references/animation-state-driven-technique.md create mode 100644 .agents/skills/vue-best-practices/references/component-async.md create mode 100644 .agents/skills/vue-best-practices/references/component-data-flow.md create mode 100644 .agents/skills/vue-best-practices/references/component-fallthrough-attrs.md create mode 100644 .agents/skills/vue-best-practices/references/component-keep-alive.md create mode 100644 .agents/skills/vue-best-practices/references/component-slots.md create mode 100644 .agents/skills/vue-best-practices/references/component-suspense.md create mode 100644 .agents/skills/vue-best-practices/references/component-teleport.md create mode 100644 .agents/skills/vue-best-practices/references/component-transition-group.md create mode 100644 .agents/skills/vue-best-practices/references/component-transition.md create mode 100644 .agents/skills/vue-best-practices/references/composables.md create mode 100644 .agents/skills/vue-best-practices/references/directives.md create mode 100644 .agents/skills/vue-best-practices/references/perf-avoid-component-abstraction-in-lists.md create mode 100644 .agents/skills/vue-best-practices/references/perf-v-once-v-memo-directives.md create mode 100644 .agents/skills/vue-best-practices/references/perf-virtualize-large-lists.md create mode 100644 .agents/skills/vue-best-practices/references/plugins.md create mode 100644 .agents/skills/vue-best-practices/references/reactivity.md create mode 100644 .agents/skills/vue-best-practices/references/render-functions.md create mode 100644 .agents/skills/vue-best-practices/references/sfc.md create mode 100644 .agents/skills/vue-best-practices/references/state-management.md create mode 100644 .agents/skills/vue-best-practices/references/updated-hook-performance.md create mode 100644 .agents/skills/vue-router-best-practices/LICENSE.md create mode 100644 .agents/skills/vue-router-best-practices/SKILL.md create mode 100644 .agents/skills/vue-router-best-practices/SYNC.md create mode 100644 .agents/skills/vue-router-best-practices/reference/router-beforeenter-no-param-trigger.md create mode 100644 .agents/skills/vue-router-best-practices/reference/router-beforerouteenter-no-this.md create mode 100644 .agents/skills/vue-router-best-practices/reference/router-guard-async-await-pattern.md create mode 100644 .agents/skills/vue-router-best-practices/reference/router-navigation-guard-infinite-loop.md create mode 100644 .agents/skills/vue-router-best-practices/reference/router-navigation-guard-next-deprecated.md create mode 100644 .agents/skills/vue-router-best-practices/reference/router-param-change-no-lifecycle.md create mode 100644 .agents/skills/vue-router-best-practices/reference/router-simple-routing-cleanup.md create mode 100644 .agents/skills/vue-router-best-practices/reference/router-use-vue-router-for-production.md create mode 100644 .agents/skills/vue-testing-best-practices/LICENSE.md create mode 100644 .agents/skills/vue-testing-best-practices/SKILL.md create mode 100644 .agents/skills/vue-testing-best-practices/SYNC.md create mode 100644 .agents/skills/vue-testing-best-practices/reference/async-component-testing.md create mode 100644 .agents/skills/vue-testing-best-practices/reference/teleport-testing-complexity.md create mode 100644 .agents/skills/vue-testing-best-practices/reference/testing-async-await-flushpromises.md create mode 100644 .agents/skills/vue-testing-best-practices/reference/testing-browser-vs-node-runners.md create mode 100644 .agents/skills/vue-testing-best-practices/reference/testing-component-blackbox-approach.md create mode 100644 .agents/skills/vue-testing-best-practices/reference/testing-composables-helper-wrapper.md create mode 100644 .agents/skills/vue-testing-best-practices/reference/testing-e2e-playwright-recommended.md create mode 100644 .agents/skills/vue-testing-best-practices/reference/testing-no-snapshot-only.md create mode 100644 .agents/skills/vue-testing-best-practices/reference/testing-pinia-store-setup.md create mode 100644 .agents/skills/vue-testing-best-practices/reference/testing-suspense-async-components.md create mode 100644 .agents/skills/vue-testing-best-practices/reference/testing-vitest-recommended-for-vue.md create mode 100644 .agents/skills/vue/GENERATION.md create mode 100644 .agents/skills/vue/SKILL.md create mode 100644 .agents/skills/vue/references/advanced-patterns.md create mode 100644 .agents/skills/vue/references/core-new-apis.md create mode 100644 .agents/skills/vue/references/script-setup-macros.md create mode 100644 .agents/skills/vueuse-functions/LICENSE.md create mode 100644 .agents/skills/vueuse-functions/SKILL.md create mode 100644 .agents/skills/vueuse-functions/SYNC.md create mode 100644 .agents/skills/vueuse-functions/references/computedAsync.md create mode 100644 .agents/skills/vueuse-functions/references/computedEager.md create mode 100644 .agents/skills/vueuse-functions/references/computedInject.md create mode 100644 .agents/skills/vueuse-functions/references/computedWithControl.md create mode 100644 .agents/skills/vueuse-functions/references/createEventHook.md create mode 100644 .agents/skills/vueuse-functions/references/createGenericProjection.md create mode 100644 .agents/skills/vueuse-functions/references/createGlobalState.md create mode 100644 .agents/skills/vueuse-functions/references/createInjectionState.md create mode 100644 .agents/skills/vueuse-functions/references/createProjection.md create mode 100644 .agents/skills/vueuse-functions/references/createRef.md create mode 100644 .agents/skills/vueuse-functions/references/createReusableTemplate.md create mode 100644 .agents/skills/vueuse-functions/references/createSharedComposable.md create mode 100644 .agents/skills/vueuse-functions/references/createTemplatePromise.md create mode 100644 .agents/skills/vueuse-functions/references/createUnrefFn.md create mode 100644 .agents/skills/vueuse-functions/references/extendRef.md create mode 100644 .agents/skills/vueuse-functions/references/from.md create mode 100644 .agents/skills/vueuse-functions/references/get.md create mode 100644 .agents/skills/vueuse-functions/references/injectLocal.md create mode 100644 .agents/skills/vueuse-functions/references/isDefined.md create mode 100644 .agents/skills/vueuse-functions/references/logicAnd.md create mode 100644 .agents/skills/vueuse-functions/references/logicNot.md create mode 100644 .agents/skills/vueuse-functions/references/logicOr.md create mode 100644 .agents/skills/vueuse-functions/references/makeDestructurable.md create mode 100644 .agents/skills/vueuse-functions/references/onClickOutside.md create mode 100644 .agents/skills/vueuse-functions/references/onElementRemoval.md create mode 100644 .agents/skills/vueuse-functions/references/onKeyStroke.md create mode 100644 .agents/skills/vueuse-functions/references/onLongPress.md create mode 100644 .agents/skills/vueuse-functions/references/onStartTyping.md create mode 100644 .agents/skills/vueuse-functions/references/provideLocal.md create mode 100644 .agents/skills/vueuse-functions/references/reactify.md create mode 100644 .agents/skills/vueuse-functions/references/reactifyObject.md create mode 100644 .agents/skills/vueuse-functions/references/reactiveComputed.md create mode 100644 .agents/skills/vueuse-functions/references/reactiveOmit.md create mode 100644 .agents/skills/vueuse-functions/references/reactivePick.md create mode 100644 .agents/skills/vueuse-functions/references/refAutoReset.md create mode 100644 .agents/skills/vueuse-functions/references/refDebounced.md create mode 100644 .agents/skills/vueuse-functions/references/refDefault.md create mode 100644 .agents/skills/vueuse-functions/references/refManualReset.md create mode 100644 .agents/skills/vueuse-functions/references/refThrottled.md create mode 100644 .agents/skills/vueuse-functions/references/refWithControl.md create mode 100644 .agents/skills/vueuse-functions/references/set.md create mode 100644 .agents/skills/vueuse-functions/references/syncRef.md create mode 100644 .agents/skills/vueuse-functions/references/syncRefs.md create mode 100644 .agents/skills/vueuse-functions/references/templateRef.md create mode 100644 .agents/skills/vueuse-functions/references/toObserver.md create mode 100644 .agents/skills/vueuse-functions/references/toReactive.md create mode 100644 .agents/skills/vueuse-functions/references/toRef.md create mode 100644 .agents/skills/vueuse-functions/references/toRefs.md create mode 100644 .agents/skills/vueuse-functions/references/tryOnBeforeMount.md create mode 100644 .agents/skills/vueuse-functions/references/tryOnBeforeUnmount.md create mode 100644 .agents/skills/vueuse-functions/references/tryOnMounted.md create mode 100644 .agents/skills/vueuse-functions/references/tryOnScopeDispose.md create mode 100644 .agents/skills/vueuse-functions/references/tryOnUnmounted.md create mode 100644 .agents/skills/vueuse-functions/references/unrefElement.md create mode 100644 .agents/skills/vueuse-functions/references/until.md create mode 100644 .agents/skills/vueuse-functions/references/useAbs.md create mode 100644 .agents/skills/vueuse-functions/references/useActiveElement.md create mode 100644 .agents/skills/vueuse-functions/references/useAnimate.md create mode 100644 .agents/skills/vueuse-functions/references/useArrayDifference.md create mode 100644 .agents/skills/vueuse-functions/references/useArrayEvery.md create mode 100644 .agents/skills/vueuse-functions/references/useArrayFilter.md create mode 100644 .agents/skills/vueuse-functions/references/useArrayFind.md create mode 100644 .agents/skills/vueuse-functions/references/useArrayFindIndex.md create mode 100644 .agents/skills/vueuse-functions/references/useArrayFindLast.md create mode 100644 .agents/skills/vueuse-functions/references/useArrayIncludes.md create mode 100644 .agents/skills/vueuse-functions/references/useArrayJoin.md create mode 100644 .agents/skills/vueuse-functions/references/useArrayMap.md create mode 100644 .agents/skills/vueuse-functions/references/useArrayReduce.md create mode 100644 .agents/skills/vueuse-functions/references/useArraySome.md create mode 100644 .agents/skills/vueuse-functions/references/useArrayUnique.md create mode 100644 .agents/skills/vueuse-functions/references/useAsyncQueue.md create mode 100644 .agents/skills/vueuse-functions/references/useAsyncState.md create mode 100644 .agents/skills/vueuse-functions/references/useAsyncValidator.md create mode 100644 .agents/skills/vueuse-functions/references/useAuth.md create mode 100644 .agents/skills/vueuse-functions/references/useAverage.md create mode 100644 .agents/skills/vueuse-functions/references/useAxios.md create mode 100644 .agents/skills/vueuse-functions/references/useBase64.md create mode 100644 .agents/skills/vueuse-functions/references/useBattery.md create mode 100644 .agents/skills/vueuse-functions/references/useBluetooth.md create mode 100644 .agents/skills/vueuse-functions/references/useBreakpoints.md create mode 100644 .agents/skills/vueuse-functions/references/useBroadcastChannel.md create mode 100644 .agents/skills/vueuse-functions/references/useBrowserLocation.md create mode 100644 .agents/skills/vueuse-functions/references/useCached.md create mode 100644 .agents/skills/vueuse-functions/references/useCeil.md create mode 100644 .agents/skills/vueuse-functions/references/useChangeCase.md create mode 100644 .agents/skills/vueuse-functions/references/useClamp.md create mode 100644 .agents/skills/vueuse-functions/references/useClipboard.md create mode 100644 .agents/skills/vueuse-functions/references/useClipboardItems.md create mode 100644 .agents/skills/vueuse-functions/references/useCloned.md create mode 100644 .agents/skills/vueuse-functions/references/useColorMode.md create mode 100644 .agents/skills/vueuse-functions/references/useConfirmDialog.md create mode 100644 .agents/skills/vueuse-functions/references/useCookies.md create mode 100644 .agents/skills/vueuse-functions/references/useCountdown.md create mode 100644 .agents/skills/vueuse-functions/references/useCounter.md create mode 100644 .agents/skills/vueuse-functions/references/useCssSupports.md create mode 100644 .agents/skills/vueuse-functions/references/useCssVar.md create mode 100644 .agents/skills/vueuse-functions/references/useCurrentElement.md create mode 100644 .agents/skills/vueuse-functions/references/useCycleList.md create mode 100644 .agents/skills/vueuse-functions/references/useDark.md create mode 100644 .agents/skills/vueuse-functions/references/useDateFormat.md create mode 100644 .agents/skills/vueuse-functions/references/useDebounceFn.md create mode 100644 .agents/skills/vueuse-functions/references/useDebouncedRefHistory.md create mode 100644 .agents/skills/vueuse-functions/references/useDeviceMotion.md create mode 100644 .agents/skills/vueuse-functions/references/useDeviceOrientation.md create mode 100644 .agents/skills/vueuse-functions/references/useDevicePixelRatio.md create mode 100644 .agents/skills/vueuse-functions/references/useDevicesList.md create mode 100644 .agents/skills/vueuse-functions/references/useDisplayMedia.md create mode 100644 .agents/skills/vueuse-functions/references/useDocumentVisibility.md create mode 100644 .agents/skills/vueuse-functions/references/useDraggable.md create mode 100644 .agents/skills/vueuse-functions/references/useDrauu.md create mode 100644 .agents/skills/vueuse-functions/references/useDropZone.md create mode 100644 .agents/skills/vueuse-functions/references/useElementBounding.md create mode 100644 .agents/skills/vueuse-functions/references/useElementByPoint.md create mode 100644 .agents/skills/vueuse-functions/references/useElementHover.md create mode 100644 .agents/skills/vueuse-functions/references/useElementSize.md create mode 100644 .agents/skills/vueuse-functions/references/useElementVisibility.md create mode 100644 .agents/skills/vueuse-functions/references/useEventBus.md create mode 100644 .agents/skills/vueuse-functions/references/useEventListener.md create mode 100644 .agents/skills/vueuse-functions/references/useEventSource.md create mode 100644 .agents/skills/vueuse-functions/references/useExtractedObservable.md create mode 100644 .agents/skills/vueuse-functions/references/useEyeDropper.md create mode 100644 .agents/skills/vueuse-functions/references/useFavicon.md create mode 100644 .agents/skills/vueuse-functions/references/useFetch.md create mode 100644 .agents/skills/vueuse-functions/references/useFileDialog.md create mode 100644 .agents/skills/vueuse-functions/references/useFileSystemAccess.md create mode 100644 .agents/skills/vueuse-functions/references/useFirestore.md create mode 100644 .agents/skills/vueuse-functions/references/useFloor.md create mode 100644 .agents/skills/vueuse-functions/references/useFocus.md create mode 100644 .agents/skills/vueuse-functions/references/useFocusTrap.md create mode 100644 .agents/skills/vueuse-functions/references/useFocusWithin.md create mode 100644 .agents/skills/vueuse-functions/references/useFps.md create mode 100644 .agents/skills/vueuse-functions/references/useFullscreen.md create mode 100644 .agents/skills/vueuse-functions/references/useFuse.md create mode 100644 .agents/skills/vueuse-functions/references/useGamepad.md create mode 100644 .agents/skills/vueuse-functions/references/useGeolocation.md create mode 100644 .agents/skills/vueuse-functions/references/useIDBKeyval.md create mode 100644 .agents/skills/vueuse-functions/references/useIdle.md create mode 100644 .agents/skills/vueuse-functions/references/useImage.md create mode 100644 .agents/skills/vueuse-functions/references/useInfiniteScroll.md create mode 100644 .agents/skills/vueuse-functions/references/useIntersectionObserver.md create mode 100644 .agents/skills/vueuse-functions/references/useInterval.md create mode 100644 .agents/skills/vueuse-functions/references/useIntervalFn.md create mode 100644 .agents/skills/vueuse-functions/references/useIpcRenderer.md create mode 100644 .agents/skills/vueuse-functions/references/useIpcRendererInvoke.md create mode 100644 .agents/skills/vueuse-functions/references/useIpcRendererOn.md create mode 100644 .agents/skills/vueuse-functions/references/useJwt.md create mode 100644 .agents/skills/vueuse-functions/references/useKeyModifier.md create mode 100644 .agents/skills/vueuse-functions/references/useLastChanged.md create mode 100644 .agents/skills/vueuse-functions/references/useLocalStorage.md create mode 100644 .agents/skills/vueuse-functions/references/useMagicKeys.md create mode 100644 .agents/skills/vueuse-functions/references/useManualRefHistory.md create mode 100644 .agents/skills/vueuse-functions/references/useMath.md create mode 100644 .agents/skills/vueuse-functions/references/useMax.md create mode 100644 .agents/skills/vueuse-functions/references/useMediaControls.md create mode 100644 .agents/skills/vueuse-functions/references/useMediaQuery.md create mode 100644 .agents/skills/vueuse-functions/references/useMemoize.md create mode 100644 .agents/skills/vueuse-functions/references/useMemory.md create mode 100644 .agents/skills/vueuse-functions/references/useMin.md create mode 100644 .agents/skills/vueuse-functions/references/useMounted.md create mode 100644 .agents/skills/vueuse-functions/references/useMouse.md create mode 100644 .agents/skills/vueuse-functions/references/useMouseInElement.md create mode 100644 .agents/skills/vueuse-functions/references/useMousePressed.md create mode 100644 .agents/skills/vueuse-functions/references/useMutationObserver.md create mode 100644 .agents/skills/vueuse-functions/references/useNProgress.md create mode 100644 .agents/skills/vueuse-functions/references/useNavigatorLanguage.md create mode 100644 .agents/skills/vueuse-functions/references/useNetwork.md create mode 100644 .agents/skills/vueuse-functions/references/useNow.md create mode 100644 .agents/skills/vueuse-functions/references/useObjectUrl.md create mode 100644 .agents/skills/vueuse-functions/references/useObservable.md create mode 100644 .agents/skills/vueuse-functions/references/useOffsetPagination.md create mode 100644 .agents/skills/vueuse-functions/references/useOnline.md create mode 100644 .agents/skills/vueuse-functions/references/usePageLeave.md create mode 100644 .agents/skills/vueuse-functions/references/useParallax.md create mode 100644 .agents/skills/vueuse-functions/references/useParentElement.md create mode 100644 .agents/skills/vueuse-functions/references/usePerformanceObserver.md create mode 100644 .agents/skills/vueuse-functions/references/usePermission.md create mode 100644 .agents/skills/vueuse-functions/references/usePointer.md create mode 100644 .agents/skills/vueuse-functions/references/usePointerLock.md create mode 100644 .agents/skills/vueuse-functions/references/usePointerSwipe.md create mode 100644 .agents/skills/vueuse-functions/references/usePrecision.md create mode 100644 .agents/skills/vueuse-functions/references/usePreferredColorScheme.md create mode 100644 .agents/skills/vueuse-functions/references/usePreferredContrast.md create mode 100644 .agents/skills/vueuse-functions/references/usePreferredDark.md create mode 100644 .agents/skills/vueuse-functions/references/usePreferredLanguages.md create mode 100644 .agents/skills/vueuse-functions/references/usePreferredReducedMotion.md create mode 100644 .agents/skills/vueuse-functions/references/usePreferredReducedTransparency.md create mode 100644 .agents/skills/vueuse-functions/references/usePrevious.md create mode 100644 .agents/skills/vueuse-functions/references/useProjection.md create mode 100644 .agents/skills/vueuse-functions/references/useQRCode.md create mode 100644 .agents/skills/vueuse-functions/references/useRTDB.md create mode 100644 .agents/skills/vueuse-functions/references/useRafFn.md create mode 100644 .agents/skills/vueuse-functions/references/useRefHistory.md create mode 100644 .agents/skills/vueuse-functions/references/useResizeObserver.md create mode 100644 .agents/skills/vueuse-functions/references/useRound.md create mode 100644 .agents/skills/vueuse-functions/references/useRouteHash.md create mode 100644 .agents/skills/vueuse-functions/references/useRouteParams.md create mode 100644 .agents/skills/vueuse-functions/references/useRouteQuery.md create mode 100644 .agents/skills/vueuse-functions/references/useSSRWidth.md create mode 100644 .agents/skills/vueuse-functions/references/useScreenOrientation.md create mode 100644 .agents/skills/vueuse-functions/references/useScreenSafeArea.md create mode 100644 .agents/skills/vueuse-functions/references/useScriptTag.md create mode 100644 .agents/skills/vueuse-functions/references/useScroll.md create mode 100644 .agents/skills/vueuse-functions/references/useScrollLock.md create mode 100644 .agents/skills/vueuse-functions/references/useSessionStorage.md create mode 100644 .agents/skills/vueuse-functions/references/useShare.md create mode 100644 .agents/skills/vueuse-functions/references/useSortable.md create mode 100644 .agents/skills/vueuse-functions/references/useSorted.md create mode 100644 .agents/skills/vueuse-functions/references/useSpeechRecognition.md create mode 100644 .agents/skills/vueuse-functions/references/useSpeechSynthesis.md create mode 100644 .agents/skills/vueuse-functions/references/useStepper.md create mode 100644 .agents/skills/vueuse-functions/references/useStorage.md create mode 100644 .agents/skills/vueuse-functions/references/useStorageAsync.md create mode 100644 .agents/skills/vueuse-functions/references/useStyleTag.md create mode 100644 .agents/skills/vueuse-functions/references/useSubject.md create mode 100644 .agents/skills/vueuse-functions/references/useSubscription.md create mode 100644 .agents/skills/vueuse-functions/references/useSum.md create mode 100644 .agents/skills/vueuse-functions/references/useSupported.md create mode 100644 .agents/skills/vueuse-functions/references/useSwipe.md create mode 100644 .agents/skills/vueuse-functions/references/useTemplateRefsList.md create mode 100644 .agents/skills/vueuse-functions/references/useTextDirection.md create mode 100644 .agents/skills/vueuse-functions/references/useTextSelection.md create mode 100644 .agents/skills/vueuse-functions/references/useTextareaAutosize.md create mode 100644 .agents/skills/vueuse-functions/references/useThrottleFn.md create mode 100644 .agents/skills/vueuse-functions/references/useThrottledRefHistory.md create mode 100644 .agents/skills/vueuse-functions/references/useTimeAgo.md create mode 100644 .agents/skills/vueuse-functions/references/useTimeAgoIntl.md create mode 100644 .agents/skills/vueuse-functions/references/useTimeout.md create mode 100644 .agents/skills/vueuse-functions/references/useTimeoutFn.md create mode 100644 .agents/skills/vueuse-functions/references/useTimeoutPoll.md create mode 100644 .agents/skills/vueuse-functions/references/useTimestamp.md create mode 100644 .agents/skills/vueuse-functions/references/useTitle.md create mode 100644 .agents/skills/vueuse-functions/references/useToNumber.md create mode 100644 .agents/skills/vueuse-functions/references/useToString.md create mode 100644 .agents/skills/vueuse-functions/references/useToggle.md create mode 100644 .agents/skills/vueuse-functions/references/useTransition.md create mode 100644 .agents/skills/vueuse-functions/references/useTrunc.md create mode 100644 .agents/skills/vueuse-functions/references/useUrlSearchParams.md create mode 100644 .agents/skills/vueuse-functions/references/useUserMedia.md create mode 100644 .agents/skills/vueuse-functions/references/useVModel.md create mode 100644 .agents/skills/vueuse-functions/references/useVModels.md create mode 100644 .agents/skills/vueuse-functions/references/useVibrate.md create mode 100644 .agents/skills/vueuse-functions/references/useVirtualList.md create mode 100644 .agents/skills/vueuse-functions/references/useWakeLock.md create mode 100644 .agents/skills/vueuse-functions/references/useWebNotification.md create mode 100644 .agents/skills/vueuse-functions/references/useWebSocket.md create mode 100644 .agents/skills/vueuse-functions/references/useWebWorker.md create mode 100644 .agents/skills/vueuse-functions/references/useWebWorkerFn.md create mode 100644 .agents/skills/vueuse-functions/references/useWindowFocus.md create mode 100644 .agents/skills/vueuse-functions/references/useWindowScroll.md create mode 100644 .agents/skills/vueuse-functions/references/useWindowSize.md create mode 100644 .agents/skills/vueuse-functions/references/useZoomFactor.md create mode 100644 .agents/skills/vueuse-functions/references/useZoomLevel.md create mode 100644 .agents/skills/vueuse-functions/references/watchArray.md create mode 100644 .agents/skills/vueuse-functions/references/watchAtMost.md create mode 100644 .agents/skills/vueuse-functions/references/watchDebounced.md create mode 100644 .agents/skills/vueuse-functions/references/watchDeep.md create mode 100644 .agents/skills/vueuse-functions/references/watchExtractedObservable.md create mode 100644 .agents/skills/vueuse-functions/references/watchIgnorable.md create mode 100644 .agents/skills/vueuse-functions/references/watchImmediate.md create mode 100644 .agents/skills/vueuse-functions/references/watchOnce.md create mode 100644 .agents/skills/vueuse-functions/references/watchPausable.md create mode 100644 .agents/skills/vueuse-functions/references/watchThrottled.md create mode 100644 .agents/skills/vueuse-functions/references/watchTriggerable.md create mode 100644 .agents/skills/vueuse-functions/references/watchWithFilter.md create mode 100644 .agents/skills/vueuse-functions/references/whenever.md create mode 100644 .agents/skills/web-design-guidelines/SKILL.md create mode 100644 .agents/skills/web-design-guidelines/SYNC.md create mode 120000 .claude/skills/pinia create mode 120000 .claude/skills/vite create mode 120000 .claude/skills/vitest create mode 120000 .claude/skills/vue create mode 120000 .claude/skills/vue-best-practices create mode 120000 .claude/skills/vue-router-best-practices create mode 120000 .claude/skills/vue-testing-best-practices create mode 120000 .claude/skills/vueuse-functions create mode 120000 .claude/skills/web-design-guidelines create mode 120000 .continue/skills/antfu create mode 120000 .continue/skills/nuxt create mode 120000 .continue/skills/pinia create mode 120000 .continue/skills/pnpm create mode 120000 .continue/skills/slidev create mode 120000 .continue/skills/tsdown create mode 120000 .continue/skills/turborepo create mode 120000 .continue/skills/unocss create mode 120000 .continue/skills/vite create mode 120000 .continue/skills/vitepress create mode 120000 .continue/skills/vitest create mode 120000 .continue/skills/vue create mode 120000 .continue/skills/vue-best-practices create mode 120000 .continue/skills/vue-router-best-practices create mode 120000 .continue/skills/vue-testing-best-practices create mode 120000 .continue/skills/vueuse-functions create mode 120000 .continue/skills/web-design-guidelines create mode 120000 .kiro/skills/antfu create mode 120000 .kiro/skills/nuxt create mode 120000 .kiro/skills/pinia create mode 120000 .kiro/skills/pnpm create mode 120000 .kiro/skills/slidev create mode 120000 .kiro/skills/tsdown create mode 120000 .kiro/skills/turborepo create mode 120000 .kiro/skills/unocss create mode 120000 .kiro/skills/vite create mode 120000 .kiro/skills/vitepress create mode 120000 .kiro/skills/vitest create mode 120000 .kiro/skills/vue create mode 120000 .kiro/skills/vue-best-practices create mode 120000 .kiro/skills/vue-router-best-practices create mode 120000 .kiro/skills/vue-testing-best-practices create mode 120000 .kiro/skills/vueuse-functions create mode 120000 .kiro/skills/web-design-guidelines diff --git a/.agents/skills/pinia/GENERATION.md b/.agents/skills/pinia/GENERATION.md new file mode 100644 index 0000000..a2c0f71 --- /dev/null +++ b/.agents/skills/pinia/GENERATION.md @@ -0,0 +1,5 @@ +# Generation Info + +- **Source:** `sources/pinia` +- **Git SHA:** `55dbfc5c20d4461748996aa74d8c0913e89fb98e` +- **Generated:** 2026-01-28 diff --git a/.agents/skills/pinia/SKILL.md b/.agents/skills/pinia/SKILL.md new file mode 100644 index 0000000..89cfa7d --- /dev/null +++ b/.agents/skills/pinia/SKILL.md @@ -0,0 +1,59 @@ +--- +name: pinia +description: Pinia official Vue state management library, type-safe and extensible. Use when defining stores, working with state/getters/actions, or implementing store patterns in Vue apps. +metadata: + author: Anthony Fu + version: "2026.1.28" + source: Generated from https://github.com/vuejs/pinia, scripts located at https://github.com/antfu/skills +--- + +# Pinia + +Pinia is the official state management library for Vue, designed to be intuitive and type-safe. It supports both Options API and Composition API styles, with first-class TypeScript support and devtools integration. + +> The skill is based on Pinia v3.0.4, generated at 2026-01-28. + +## Core References + +| Topic | Description | Reference | +|-------|-------------|-----------| +| Stores | Defining stores, state, getters, actions, storeToRefs, subscriptions | [core-stores](references/core-stores.md) | + +## Features + +### Extensibility + +| Topic | Description | Reference | +|-------|-------------|-----------| +| Plugins | Extend stores with custom properties, state, and behavior | [features-plugins](references/features-plugins.md) | + +### Composability + +| Topic | Description | Reference | +|-------|-------------|-----------| +| Composables | Using Vue composables within stores (VueUse, etc.) | [features-composables](references/features-composables.md) | +| Composing Stores | Store-to-store communication, avoiding circular dependencies | [features-composing-stores](references/features-composing-stores.md) | + +## Best Practices + +| Topic | Description | Reference | +|-------|-------------|-----------| +| Testing | Unit testing with @pinia/testing, mocking, stubbing | [best-practices-testing](references/best-practices-testing.md) | +| Outside Components | Using stores in navigation guards, plugins, middlewares | [best-practices-outside-component](references/best-practices-outside-component.md) | + +## Advanced + +| Topic | Description | Reference | +|-------|-------------|-----------| +| SSR | Server-side rendering, state hydration | [advanced-ssr](references/advanced-ssr.md) | +| Nuxt | Nuxt integration, auto-imports, SSR best practices | [advanced-nuxt](references/advanced-nuxt.md) | +| HMR | Hot module replacement for development | [advanced-hmr](references/advanced-hmr.md) | + +## Key Recommendations + +- **Prefer Setup Stores** for complex logic, composables, and watchers +- **Use `storeToRefs()`** when destructuring state/getters to preserve reactivity +- **Actions can be destructured directly** - they're bound to the store +- **Call stores inside functions** not at module scope, especially for SSR +- **Add HMR support** to each store for better development experience +- **Use `@pinia/testing`** for component tests with mocked stores diff --git a/.agents/skills/pinia/references/advanced-hmr.md b/.agents/skills/pinia/references/advanced-hmr.md new file mode 100644 index 0000000..3eef5c7 --- /dev/null +++ b/.agents/skills/pinia/references/advanced-hmr.md @@ -0,0 +1,61 @@ +--- +name: hot-module-replacement +description: Enable HMR to preserve store state during development +--- + +# Hot Module Replacement (HMR) + +Pinia supports HMR to edit stores without page reload, preserving existing state. + +## Setup + +Add this snippet after each store definition: + +```ts +import { defineStore, acceptHMRUpdate } from 'pinia' + +export const useAuth = defineStore('auth', { + // store options... +}) + +if (import.meta.hot) { + import.meta.hot.accept(acceptHMRUpdate(useAuth, import.meta.hot)) +} +``` + +## Setup Store Example + +```ts +import { defineStore, acceptHMRUpdate } from 'pinia' + +export const useCounterStore = defineStore('counter', () => { + const count = ref(0) + const increment = () => count.value++ + return { count, increment } +}) + +if (import.meta.hot) { + import.meta.hot.accept(acceptHMRUpdate(useCounterStore, import.meta.hot)) +} +``` + +## Bundler Support + +- **Vite:** Officially supported via `import.meta.hot` +- **Webpack:** Uses `import.meta.webpackHot` +- Any bundler implementing the `import.meta.hot` spec should work + +## Nuxt + +With `@pinia/nuxt`, `acceptHMRUpdate` is auto-imported but you still need to add the HMR snippet manually. + +## Benefits + +- Edit store logic without losing state +- Add/remove state, actions, and getters on the fly +- Faster development iteration + + diff --git a/.agents/skills/pinia/references/advanced-nuxt.md b/.agents/skills/pinia/references/advanced-nuxt.md new file mode 100644 index 0000000..569da63 --- /dev/null +++ b/.agents/skills/pinia/references/advanced-nuxt.md @@ -0,0 +1,119 @@ +--- +name: nuxt-integration +description: Using Pinia with Nuxt - auto-imports, SSR, and best practices +--- + +# Nuxt Integration + +Pinia works seamlessly with Nuxt 3/4, handling SSR, serialization, and XSS protection automatically. + +## Installation + +```bash +npx nuxi@latest module add pinia +``` + +This installs both `@pinia/nuxt` and `pinia`. If `pinia` isn't installed, add it manually. + +> **npm users:** If you get `ERESOLVE unable to resolve dependency tree`, add to `package.json`: +> ```json +> "overrides": { "vue": "latest" } +> ``` + +## Configuration + +```ts +// nuxt.config.ts +export default defineNuxtConfig({ + modules: ['@pinia/nuxt'], +}) +``` + +## Auto Imports + +These are automatically available: +- `usePinia()` - get pinia instance +- `defineStore()` - define stores +- `storeToRefs()` - extract reactive refs +- `acceptHMRUpdate()` - HMR support + +**All stores in `app/stores/` (Nuxt 4) or `stores/` are auto-imported.** + +### Custom Store Directories + +```ts +// nuxt.config.ts +export default defineNuxtConfig({ + modules: ['@pinia/nuxt'], + pinia: { + storesDirs: ['./stores/**', './custom-folder/stores/**'], + }, +}) +``` + +## Fetching Data in Pages + +Use `callOnce()` for SSR-friendly data fetching: + +```vue + +``` + +### Refetch on Navigation + +```vue + +``` + +## Using Stores Outside Components + +In navigation guards, middlewares, or other stores, pass the `pinia` instance: + +```ts +// middleware/auth.ts +export default defineNuxtRouteMiddleware((to) => { + const nuxtApp = useNuxtApp() + const store = useStore(nuxtApp.$pinia) + + if (to.meta.requiresAuth && !store.isLoggedIn) { + return navigateTo('/login') + } +}) +``` + +Most of the time, you don't need this - just use stores in components or other injection-aware contexts. + +## Pinia Plugins with Nuxt + +Create a Nuxt plugin: + +```ts +// plugins/myPiniaPlugin.ts +import { PiniaPluginContext } from 'pinia' + +function MyPiniaPlugin({ store }: PiniaPluginContext) { + store.$subscribe((mutation) => { + console.log(`[🍍 ${mutation.storeId}]: ${mutation.type}`) + }) + return { creationTime: new Date() } +} + +export default defineNuxtPlugin(({ $pinia }) => { + $pinia.use(MyPiniaPlugin) +}) +``` + + diff --git a/.agents/skills/pinia/references/advanced-ssr.md b/.agents/skills/pinia/references/advanced-ssr.md new file mode 100644 index 0000000..2972f3a --- /dev/null +++ b/.agents/skills/pinia/references/advanced-ssr.md @@ -0,0 +1,121 @@ +--- +name: server-side-rendering +description: SSR setup, state hydration, and avoiding cross-request state pollution +--- + +# Server Side Rendering (SSR) + +Pinia works with SSR when stores are called at the top of `setup`, getters, or actions. + +> **Using Nuxt?** See the [Nuxt integration](advanced-nuxt.md) instead. + +## Basic Usage + +```vue + +``` + +## Using Store Outside setup() + +Pass the `pinia` instance explicitly: + +```ts +const pinia = createPinia() +const app = createApp(App) +app.use(router) +app.use(pinia) + +router.beforeEach((to) => { + // ✅ Pass pinia for correct SSR context + const main = useMainStore(pinia) + + if (to.meta.requiresAuth && !main.isLoggedIn) { + return '/login' + } +}) +``` + +## serverPrefetch() + +Access pinia via `this.$pinia`: + +```ts +export default { + serverPrefetch() { + const store = useStore(this.$pinia) + return store.fetchData() + }, +} +``` + +## onServerPrefetch() + +Works normally: + +```vue + +``` + +## State Hydration + +Serialize state on server and hydrate on client. + +### Server Side + +Use [devalue](https://github.com/Rich-Harris/devalue) for XSS-safe serialization: + +```ts +import devalue from 'devalue' +import { createPinia } from 'pinia' + +const pinia = createPinia() +const app = createApp(App) +app.use(router) +app.use(pinia) + +// After rendering, state is available +const serializedState = devalue(pinia.state.value) +// Inject into HTML as global variable +``` + +### Client Side + +Hydrate before any `useStore()` call: + +```ts +const pinia = createPinia() +const app = createApp(App) +app.use(pinia) + +// Hydrate from serialized state (e.g., from window.__pinia) +if (typeof window !== 'undefined') { + pinia.state.value = JSON.parse(window.__pinia) +} +``` + +## SSR Examples + +- [Vitesse template](https://github.com/antfu/vitesse/blob/main/src/modules/pinia.ts) +- [vite-plugin-ssr](https://vite-plugin-ssr.com/pinia) + +## Key Points + +1. Call stores inside functions, not at module scope +2. Pass `pinia` instance when using stores outside components in SSR +3. Hydrate state before calling any `useStore()` +4. Use `devalue` or similar for safe serialization +5. Avoid cross-request state pollution by creating fresh pinia per request + + diff --git a/.agents/skills/pinia/references/best-practices-outside-component.md b/.agents/skills/pinia/references/best-practices-outside-component.md new file mode 100644 index 0000000..126f7a6 --- /dev/null +++ b/.agents/skills/pinia/references/best-practices-outside-component.md @@ -0,0 +1,115 @@ +--- +name: using-stores-outside-components +description: Correctly using stores in navigation guards, plugins, and other non-component contexts +--- + +# Using Stores Outside Components + +Stores need the `pinia` instance, which is automatically injected in components. Outside components, you may need to provide it manually. + +## Single Page Applications + +Call stores **after** pinia is installed: + +```ts +import { useUserStore } from '@/stores/user' +import { createPinia } from 'pinia' +import { createApp } from 'vue' +import App from './App.vue' + +// ❌ Fails - pinia not created yet +const userStore = useUserStore() + +const pinia = createPinia() +const app = createApp(App) +app.use(pinia) + +// ✅ Works - pinia is active +const userStore = useUserStore() +``` + +## Navigation Guards + +**Wrong:** Call at module level + +```ts +import { createRouter } from 'vue-router' +const router = createRouter({ /* ... */ }) + +// ❌ May fail depending on import order +const store = useUserStore() + +router.beforeEach((to) => { + if (store.isLoggedIn) { /* ... */ } +}) +``` + +**Correct:** Call inside the guard + +```ts +router.beforeEach((to) => { + // ✅ Called after pinia is installed + const store = useUserStore() + + if (to.meta.requiresAuth && !store.isLoggedIn) { + return '/login' + } +}) +``` + +## SSR Applications + +Always pass the `pinia` instance to `useStore()`: + +```ts +const pinia = createPinia() +const app = createApp(App) +app.use(router) +app.use(pinia) + +router.beforeEach((to) => { + // ✅ Pass pinia instance + const main = useMainStore(pinia) + + if (to.meta.requiresAuth && !main.isLoggedIn) { + return '/login' + } +}) +``` + +## serverPrefetch() + +Access pinia via `this.$pinia`: + +```ts +export default { + serverPrefetch() { + const store = useStore(this.$pinia) + return store.fetchData() + }, +} +``` + +## onServerPrefetch() + +Works normally in ` +``` + +## Key Takeaway + +Defer `useStore()` calls to functions that run after pinia is installed, rather than calling at module scope. + + diff --git a/.agents/skills/pinia/references/best-practices-testing.md b/.agents/skills/pinia/references/best-practices-testing.md new file mode 100644 index 0000000..7227cd4 --- /dev/null +++ b/.agents/skills/pinia/references/best-practices-testing.md @@ -0,0 +1,212 @@ +--- +name: testing +description: Unit testing stores and components with @pinia/testing +--- + +# Testing Stores + +## Unit Testing Stores + +Create a fresh pinia instance for each test: + +```ts +import { setActivePinia, createPinia } from 'pinia' +import { useCounterStore } from '../src/stores/counter' + +describe('Counter Store', () => { + beforeEach(() => { + setActivePinia(createPinia()) + }) + + it('increments', () => { + const counter = useCounterStore() + expect(counter.n).toBe(0) + counter.increment() + expect(counter.n).toBe(1) + }) +}) +``` + +### With Plugins + +```ts +import { setActivePinia, createPinia } from 'pinia' +import { createApp } from 'vue' +import { somePlugin } from '../src/stores/plugin' + +const app = createApp({}) + +beforeEach(() => { + const pinia = createPinia().use(somePlugin) + app.use(pinia) + setActivePinia(pinia) +}) +``` + +## Testing Components + +Install `@pinia/testing`: + +```bash +npm i -D @pinia/testing +``` + +Use `createTestingPinia()`: + +```ts +import { mount } from '@vue/test-utils' +import { createTestingPinia } from '@pinia/testing' +import { useSomeStore } from '@/stores/myStore' + +const wrapper = mount(Counter, { + global: { + plugins: [createTestingPinia()], + }, +}) + +const store = useSomeStore() + +// Manipulate state directly +store.name = 'new name' +store.$patch({ name: 'new name' }) + +// Actions are stubbed by default +store.someAction() +expect(store.someAction).toHaveBeenCalledTimes(1) +``` + +## Initial State + +Set initial state for tests: + +```ts +const wrapper = mount(Counter, { + global: { + plugins: [ + createTestingPinia({ + initialState: { + counter: { n: 20 }, // Store name → initial state + }, + }), + ], + }, +}) +``` + +## Action Stubbing + +### Execute Real Actions + +```ts +createTestingPinia({ stubActions: false }) +``` + +### Selective Stubbing + +```ts +// Only stub specific actions +createTestingPinia({ + stubActions: ['increment', 'reset'], +}) + +// Or use a function +createTestingPinia({ + stubActions: (actionName, store) => { + if (actionName.startsWith('set')) return true + return false + }, +}) +``` + +### Mock Action Return Values + +```ts +import type { Mock } from 'vitest' + +// After getting store +store.someAction.mockResolvedValue('mocked value') +``` + +## Mocking Getters + +Getters are writable in tests: + +```ts +const pinia = createTestingPinia() +const counter = useCounterStore(pinia) + +counter.double = 3 // Override computed value + +// Reset to default behavior +counter.double = undefined +counter.double // Now computed normally +``` + +## Custom Spy Function + +If not using Jest/Vitest with globals: + +```ts +import { vi } from 'vitest' + +createTestingPinia({ + createSpy: vi.fn, +}) +``` + +With Sinon: + +```ts +import sinon from 'sinon' + +createTestingPinia({ + createSpy: sinon.spy, +}) +``` + +## Pinia Plugins in Tests + +Pass plugins to `createTestingPinia()`: + +```ts +import { somePlugin } from '../src/stores/plugin' + +createTestingPinia({ + stubActions: false, + plugins: [somePlugin], +}) +``` + +**Don't use** `testingPinia.use(MyPlugin)` - pass plugins in options. + +## Type-Safe Mocked Store + +```ts +import type { Mock } from 'vitest' +import type { Store, StoreDefinition } from 'pinia' + +function mockedStore unknown>( + useStore: TStoreDef +): TStoreDef extends StoreDefinition + ? Store, { + [K in keyof Actions]: Actions[K] extends (...args: any[]) => any + ? Mock + : Actions[K] + }> + : ReturnType { + return useStore() as any +} + +// Usage +const store = mockedStore(useSomeStore) +store.someAction.mockResolvedValue('value') // Typed! +``` + +## E2E Tests + +No special handling needed - Pinia works normally. + + diff --git a/.agents/skills/pinia/references/core-stores.md b/.agents/skills/pinia/references/core-stores.md new file mode 100644 index 0000000..ea6a72b --- /dev/null +++ b/.agents/skills/pinia/references/core-stores.md @@ -0,0 +1,389 @@ +--- +name: stores +description: Defining stores, state, getters, and actions in Pinia +--- + +# Pinia Stores + +Stores are defined using `defineStore()` with a unique name. Each store has three core concepts: **state**, **getters**, and **actions**. + +## Defining Stores + +### Option Stores + +Similar to Vue's Options API: + +```ts +import { defineStore } from 'pinia' + +export const useCounterStore = defineStore('counter', { + state: () => ({ + count: 0, + name: 'Eduardo', + }), + getters: { + doubleCount: (state) => state.count * 2, + }, + actions: { + increment() { + this.count++ + }, + }, +}) +``` + +Think of `state` as `data`, `getters` as `computed`, and `actions` as `methods`. + +### Setup Stores (Recommended) + +Uses Composition API syntax - more flexible and powerful: + +```ts +import { ref, computed } from 'vue' +import { defineStore } from 'pinia' + +export const useCounterStore = defineStore('counter', () => { + const count = ref(0) + const name = ref('Eduardo') + const doubleCount = computed(() => count.value * 2) + + function increment() { + count.value++ + } + + return { count, name, doubleCount, increment } +}) +``` + +In Setup Stores: `ref()` → state, `computed()` → getters, `function()` → actions. + +**Important:** You must return all state properties for Pinia to track them. + +### Using Stores + +```vue + +``` + +### Destructuring with storeToRefs + +```vue + +``` + +--- + +## State + +State is defined as a function returning the initial state. + +### TypeScript + +Type inference works automatically. For complex types: + +```ts +interface UserInfo { + name: string + age: number +} + +export const useUserStore = defineStore('user', { + state: () => ({ + userList: [] as UserInfo[], + user: null as UserInfo | null, + }), +}) +``` + +Or use an interface for the return type: + +```ts +interface State { + userList: UserInfo[] + user: UserInfo | null +} + +export const useUserStore = defineStore('user', { + state: (): State => ({ + userList: [], + user: null, + }), +}) +``` + +### Accessing and Modifying + +```ts +const store = useStore() +store.count++ +``` + +```vue + +``` + +### Mutating with $patch + +Apply multiple changes at once: + +```ts +// Object syntax +store.$patch({ + count: store.count + 1, + name: 'DIO', +}) + +// Function syntax (for complex mutations) +store.$patch((state) => { + state.items.push({ name: 'shoes', quantity: 1 }) + state.hasChanged = true +}) +``` + +### Resetting State + +Option Stores have built-in `$reset()`. For Setup Stores, implement your own: + +```ts +export const useCounterStore = defineStore('counter', () => { + const count = ref(0) + + function $reset() { + count.value = 0 + } + + return { count, $reset } +}) +``` + +### Subscribing to State Changes + +```ts +cartStore.$subscribe((mutation, state) => { + mutation.type // 'direct' | 'patch object' | 'patch function' + mutation.storeId // 'cart' + mutation.payload // patch object (only for 'patch object') + + localStorage.setItem('cart', JSON.stringify(state)) +}) + +// Options +cartStore.$subscribe(callback, { flush: 'sync' }) // Immediate +cartStore.$subscribe(callback, { detached: true }) // Keep after unmount +``` + +--- + +## Getters + +Getters are computed values, equivalent to Vue's `computed()`. + +### Basic Getters + +```ts +getters: { + doubleCount: (state) => state.count * 2, +} +``` + +### Accessing Other Getters + +Use `this` with explicit return type: + +```ts +getters: { + doubleCount: (state) => state.count * 2, + doublePlusOne(): number { + return this.doubleCount + 1 + }, +}, +``` + +### Getters with Arguments + +Return a function (note: loses caching): + +```ts +getters: { + getUserById: (state) => { + return (userId: string) => state.users.find((user) => user.id === userId) + }, +}, +``` + +Cache within parameterized getters: + +```ts +getters: { + getActiveUserById(state) { + const activeUsers = state.users.filter((user) => user.active) + return (userId: string) => activeUsers.find((user) => user.id === userId) + }, +}, +``` + +### Accessing Other Stores in Getters + +```ts +import { useOtherStore } from './other-store' + +getters: { + combined(state) { + const otherStore = useOtherStore() + return state.localData + otherStore.data + }, +}, +``` + +--- + +## Actions + +Actions are methods for business logic. Unlike getters, they can be asynchronous. + +### Defining Actions + +```ts +actions: { + increment() { + this.count++ + }, + randomizeCounter() { + this.count = Math.round(100 * Math.random()) + }, +}, +``` + +### Async Actions + +```ts +actions: { + async registerUser(login: string, password: string) { + try { + this.userData = await api.post({ login, password }) + } catch (error) { + return error + } + }, +}, +``` + +### Accessing Other Stores in Actions + +```ts +import { useAuthStore } from './auth-store' + +actions: { + async fetchUserPreferences() { + const auth = useAuthStore() + if (auth.isAuthenticated) { + this.preferences = await fetchPreferences() + } + }, +}, +``` + +**SSR:** Call all `useStore()` before any `await`: + +```ts +async orderCart() { + // ✅ Call stores before await + const user = useUserStore() + + await apiOrderCart(user.token, this.items) + // ❌ Don't call useStore() after await in SSR +} +``` + +### Subscribing to Actions + +```ts +const unsubscribe = someStore.$onAction( + ({ name, store, args, after, onError }) => { + const startTime = Date.now() + console.log(`Start "${name}" with params [${args.join(', ')}]`) + + after((result) => { + console.log(`Finished "${name}" after ${Date.now() - startTime}ms`) + }) + + onError((error) => { + console.warn(`Failed "${name}": ${error}`) + }) + } +) + +unsubscribe() // Cleanup +``` + +Keep subscription after component unmount: + +```ts +someStore.$onAction(callback, true) +``` + +--- + +## Options API Helpers + +```ts +import { mapState, mapWritableState, mapActions } from 'pinia' +import { useCounterStore } from '../stores/counter' + +export default { + computed: { + // Readonly state/getters + ...mapState(useCounterStore, ['count', 'doubleCount']), + // Writable state + ...mapWritableState(useCounterStore, ['count']), + }, + methods: { + ...mapActions(useCounterStore, ['increment']), + }, +} +``` + +--- + +## Accessing Global Providers in Setup Stores + +```ts +import { inject } from 'vue' +import { useRoute } from 'vue-router' +import { defineStore } from 'pinia' + +export const useSearchFilters = defineStore('search-filters', () => { + const route = useRoute() + const appProvided = inject('appProvided') + + // Don't return these - access them directly in components + return { /* ... */ } +}) +``` + + diff --git a/.agents/skills/pinia/references/features-composables.md b/.agents/skills/pinia/references/features-composables.md new file mode 100644 index 0000000..79f7d94 --- /dev/null +++ b/.agents/skills/pinia/references/features-composables.md @@ -0,0 +1,114 @@ +--- +name: composables-in-stores +description: Using Vue composables within Pinia stores +--- + +# Composables in Stores + +Pinia stores can leverage Vue composables for reusable stateful logic. + +## Option Stores + +Call composables inside the `state` property, but only those returning writable refs: + +```ts +import { defineStore } from 'pinia' +import { useLocalStorage } from '@vueuse/core' + +export const useAuthStore = defineStore('auth', { + state: () => ({ + user: useLocalStorage('pinia/auth/login', 'bob'), + }), +}) +``` + +**Works:** Composables returning `ref()`: +- `useLocalStorage` +- `useAsyncState` + +**Doesn't work in Option Stores:** +- Composables exposing functions +- Composables exposing readonly data + +## Setup Stores + +More flexible - can use almost any composable: + +```ts +import { defineStore } from 'pinia' +import { useMediaControls } from '@vueuse/core' +import { ref } from 'vue' + +export const useVideoPlayer = defineStore('video', () => { + const videoElement = ref() + const src = ref('/data/video.mp4') + const { playing, volume, currentTime, togglePictureInPicture } = + useMediaControls(videoElement, { src }) + + function loadVideo(element: HTMLVideoElement, newSrc: string) { + videoElement.value = element + src.value = newSrc + } + + return { + src, + playing, + volume, + currentTime, + loadVideo, + togglePictureInPicture, + } +}) +``` + +**Note:** Don't return non-serializable DOM refs like `videoElement` - they're internal implementation details. + +## SSR Considerations + +### Option Stores with hydrate() + +Define a `hydrate()` function to handle client-side hydration: + +```ts +import { defineStore } from 'pinia' +import { useLocalStorage } from '@vueuse/core' + +export const useAuthStore = defineStore('auth', { + state: () => ({ + user: useLocalStorage('pinia/auth/login', 'bob'), + }), + + hydrate(state, initialState) { + // Ignore server state, read from browser + state.user = useLocalStorage('pinia/auth/login', 'bob') + }, +}) +``` + +### Setup Stores with skipHydrate() + +Mark state that shouldn't hydrate from server: + +```ts +import { defineStore, skipHydrate } from 'pinia' +import { useEyeDropper, useLocalStorage } from '@vueuse/core' + +export const useColorStore = defineStore('colors', () => { + const { isSupported, open, sRGBHex } = useEyeDropper() + const lastColor = useLocalStorage('lastColor', sRGBHex) + + return { + // Skip hydration for client-only state + lastColor: skipHydrate(lastColor), + open, // Function - no hydration needed + isSupported, // Boolean - not reactive + } +}) +``` + +`skipHydrate()` only applies to state properties (refs), not functions or non-reactive values. + + diff --git a/.agents/skills/pinia/references/features-composing-stores.md b/.agents/skills/pinia/references/features-composing-stores.md new file mode 100644 index 0000000..948f8b5 --- /dev/null +++ b/.agents/skills/pinia/references/features-composing-stores.md @@ -0,0 +1,134 @@ +--- +name: composing-stores +description: Store-to-store communication and avoiding circular dependencies +--- + +# Composing Stores + +Stores can use each other for shared state and logic. + +## Rule: Avoid Circular Dependencies + +Two stores cannot directly read each other's state during setup: + +```ts +// ❌ Infinite loop +const useX = defineStore('x', () => { + const y = useY() + y.name // Don't read here! + return { name: ref('X') } +}) + +const useY = defineStore('y', () => { + const x = useX() + x.name // Don't read here! + return { name: ref('Y') } +}) +``` + +**Solution:** Read in getters, computed, or actions: + +```ts +const useX = defineStore('x', () => { + const y = useY() + + // ✅ Read in computed/actions + function doSomething() { + const yName = y.name + } + + return { name: ref('X'), doSomething } +}) +``` + +## Setup Stores: Use Store at Top + +```ts +import { defineStore } from 'pinia' +import { useUserStore } from './user' + +export const useCartStore = defineStore('cart', () => { + const user = useUserStore() + const list = ref([]) + + const summary = computed(() => { + return `Hi ${user.name}, you have ${list.value.length} items` + }) + + function purchase() { + return apiPurchase(user.id, list.value) + } + + return { list, summary, purchase } +}) +``` + +## Shared Getters + +Call `useStore()` inside a getter: + +```ts +import { useUserStore } from './user' + +export const useCartStore = defineStore('cart', { + getters: { + summary(state) { + const user = useUserStore() + return `Hi ${user.name}, you have ${state.list.length} items` + }, + }, +}) +``` + +## Shared Actions + +Call `useStore()` inside an action: + +```ts +import { useUserStore } from './user' +import { apiOrderCart } from './api' + +export const useCartStore = defineStore('cart', { + actions: { + async orderCart() { + const user = useUserStore() + + try { + await apiOrderCart(user.token, this.items) + this.emptyCart() + } catch (err) { + displayError(err) + } + }, + }, +}) +``` + +## SSR: Call Stores Before Await + +In async actions, call all stores before any `await`: + +```ts +actions: { + async orderCart() { + // ✅ All useStore() calls before await + const user = useUserStore() + const analytics = useAnalyticsStore() + + try { + await apiOrderCart(user.token, this.items) + // ❌ Don't call useStore() after await (SSR issue) + // const otherStore = useOtherStore() + } catch (err) { + displayError(err) + } + }, +} +``` + +This ensures the correct Pinia instance is used during SSR. + + diff --git a/.agents/skills/pinia/references/features-plugins.md b/.agents/skills/pinia/references/features-plugins.md new file mode 100644 index 0000000..c4355ac --- /dev/null +++ b/.agents/skills/pinia/references/features-plugins.md @@ -0,0 +1,203 @@ +--- +name: plugins +description: Extend stores with custom properties, methods, and behavior +--- + +# Plugins + +Plugins extend all stores with custom properties, methods, or behavior. + +## Basic Plugin + +```ts +import { createPinia } from 'pinia' + +function SecretPiniaPlugin() { + return { secret: 'the cake is a lie' } +} + +const pinia = createPinia() +pinia.use(SecretPiniaPlugin) + +// In any store +const store = useStore() +store.secret // 'the cake is a lie' +``` + +## Plugin Context + +Plugins receive a context object: + +```ts +import { PiniaPluginContext } from 'pinia' + +export function myPiniaPlugin(context: PiniaPluginContext) { + context.pinia // pinia instance + context.app // Vue app instance + context.store // store being augmented + context.options // store definition options +} +``` + +## Adding Properties + +Return an object to add properties (tracked in devtools): + +```ts +pinia.use(() => ({ hello: 'world' })) +``` + +Or set directly on store: + +```ts +pinia.use(({ store }) => { + store.hello = 'world' + // For devtools visibility in dev mode + if (process.env.NODE_ENV === 'development') { + store._customProperties.add('hello') + } +}) +``` + +## Adding State + +Add to both `store` and `store.$state` for SSR/devtools: + +```ts +import { toRef, ref } from 'vue' + +pinia.use(({ store }) => { + if (!store.$state.hasOwnProperty('hasError')) { + const hasError = ref(false) + store.$state.hasError = hasError + } + store.hasError = toRef(store.$state, 'hasError') +}) +``` + +## Adding External Properties + +Wrap non-reactive objects with `markRaw()`: + +```ts +import { markRaw } from 'vue' +import { router } from './router' + +pinia.use(({ store }) => { + store.router = markRaw(router) +}) +``` + +## Custom Store Options + +Define custom options consumed by plugins: + +```ts +// Store definition +defineStore('search', { + actions: { + searchContacts() { /* ... */ }, + }, + debounce: { + searchContacts: 300, + }, +}) + +// Plugin reads custom option +import debounce from 'lodash/debounce' + +pinia.use(({ options, store }) => { + if (options.debounce) { + return Object.keys(options.debounce).reduce((acc, action) => { + acc[action] = debounce(store[action], options.debounce[action]) + return acc + }, {}) + } +}) +``` + +For Setup Stores, pass options as third argument: + +```ts +defineStore( + 'search', + () => { /* ... */ }, + { + debounce: { searchContacts: 300 }, + } +) +``` + +## TypeScript Augmentation + +### Custom Properties + +```ts +import 'pinia' +import type { Router } from 'vue-router' + +declare module 'pinia' { + export interface PiniaCustomProperties { + router: Router + hello: string + } +} +``` + +### Custom State + +```ts +declare module 'pinia' { + export interface PiniaCustomStateProperties { + hasError: boolean + } +} +``` + +### Custom Options + +```ts +declare module 'pinia' { + export interface DefineStoreOptionsBase { + debounce?: Partial, number>> + } +} +``` + +## Subscribe in Plugins + +```ts +pinia.use(({ store }) => { + store.$subscribe(() => { + // React to state changes + }) + store.$onAction(() => { + // React to actions + }) +}) +``` + +## Nuxt Plugin + +Create a Nuxt plugin to add Pinia plugins: + +```ts +// plugins/myPiniaPlugin.ts +import { PiniaPluginContext } from 'pinia' + +function MyPiniaPlugin({ store }: PiniaPluginContext) { + store.$subscribe((mutation) => { + console.log(`[🍍 ${mutation.storeId}]: ${mutation.type}`) + }) + return { creationTime: new Date() } +} + +export default defineNuxtPlugin(({ $pinia }) => { + $pinia.use(MyPiniaPlugin) +}) +``` + + diff --git a/.agents/skills/vite/GENERATION.md b/.agents/skills/vite/GENERATION.md new file mode 100644 index 0000000..a4b1c4a --- /dev/null +++ b/.agents/skills/vite/GENERATION.md @@ -0,0 +1,5 @@ +# Generation Info + +- **Source:** `sources/vite` +- **Git SHA:** `c47015eba4f0de255218c35769628d87152216ca` +- **Generated:** 2026-01-31 diff --git a/.agents/skills/vite/SKILL.md b/.agents/skills/vite/SKILL.md new file mode 100644 index 0000000..0a00766 --- /dev/null +++ b/.agents/skills/vite/SKILL.md @@ -0,0 +1,72 @@ +--- +name: vite +description: Vite build tool configuration, plugin API, SSR, and Vite 8 Rolldown migration. Use when working with Vite projects, vite.config.ts, Vite plugins, or building libraries/SSR apps with Vite. +metadata: + author: Anthony Fu + version: "2026.1.31" + source: Generated from https://github.com/vitejs/vite, scripts at https://github.com/antfu/skills +--- + +# Vite + +> Based on Vite 8 beta (Rolldown-powered). Vite 8 uses Rolldown bundler and Oxc transformer. + +Vite is a next-generation frontend build tool with fast dev server (native ESM + HMR) and optimized production builds. + +## Preferences + +- Use TypeScript: prefer `vite.config.ts` +- Always use ESM, avoid CommonJS + +## Core + +| Topic | Description | Reference | +|-------|-------------|-----------| +| Configuration | `vite.config.ts`, `defineConfig`, conditional configs, `loadEnv` | [core-config](references/core-config.md) | +| Features | `import.meta.glob`, asset queries (`?raw`, `?url`), `import.meta.env`, HMR API | [core-features](references/core-features.md) | +| Plugin API | Vite-specific hooks, virtual modules, plugin ordering | [core-plugin-api](references/core-plugin-api.md) | + +## Build & SSR + +| Topic | Description | Reference | +|-------|-------------|-----------| +| Build & SSR | Library mode, SSR middleware mode, `ssrLoadModule`, JavaScript API | [build-and-ssr](references/build-and-ssr.md) | + +## Advanced + +| Topic | Description | Reference | +|-------|-------------|-----------| +| Environment API | Vite 6+ multi-environment support, custom runtimes | [environment-api](references/environment-api.md) | +| Rolldown Migration | Vite 8 changes: Rolldown bundler, Oxc transformer, config migration | [rolldown-migration](references/rolldown-migration.md) | + +## Quick Reference + +### CLI Commands + +```bash +vite # Start dev server +vite build # Production build +vite preview # Preview production build +vite build --ssr # SSR build +``` + +### Common Config + +```ts +import { defineConfig } from 'vite' + +export default defineConfig({ + plugins: [], + resolve: { alias: { '@': '/src' } }, + server: { port: 3000, proxy: { '/api': 'http://localhost:8080' } }, + build: { target: 'esnext', outDir: 'dist' }, +}) +``` + +### Official Plugins + +- `@vitejs/plugin-vue` - Vue 3 SFC support +- `@vitejs/plugin-vue-jsx` - Vue 3 JSX +- `@vitejs/plugin-react` - React with Oxc/Babel +- `@vitejs/plugin-react-swc` - React with SWC +- `@vitejs/plugin-legacy` - Legacy browser support diff --git a/.agents/skills/vite/references/build-and-ssr.md b/.agents/skills/vite/references/build-and-ssr.md new file mode 100644 index 0000000..ac46a9a --- /dev/null +++ b/.agents/skills/vite/references/build-and-ssr.md @@ -0,0 +1,164 @@ +--- +name: vite-build-ssr +description: Vite library mode, multi-page apps, JavaScript API, and SSR guidance +--- + +# Build and SSR + +## Library Mode + +Build a library for distribution: + +```ts +// vite.config.ts +import { resolve } from 'node:path' +import { defineConfig } from 'vite' + +export default defineConfig({ + build: { + lib: { + entry: resolve(import.meta.dirname, 'lib/main.ts'), + name: 'MyLib', + fileName: 'my-lib', + }, + rolldownOptions: { + external: ['vue', 'react'], + output: { + globals: { + vue: 'Vue', + react: 'React', + }, + }, + }, + }, +}) +``` + +### Multiple Entries + +```ts +build: { + lib: { + entry: { + 'my-lib': resolve(import.meta.dirname, 'lib/main.ts'), + secondary: resolve(import.meta.dirname, 'lib/secondary.ts'), + }, + name: 'MyLib', + }, +} +``` + +### Output Formats + +- Single entry: `es` and `umd` +- Multiple entries: `es` and `cjs` + +### Package.json Setup + +```json +{ + "name": "my-lib", + "type": "module", + "files": ["dist"], + "main": "./dist/my-lib.umd.cjs", + "module": "./dist/my-lib.js", + "exports": { + ".": { + "import": "./dist/my-lib.js", + "require": "./dist/my-lib.umd.cjs" + }, + "./style.css": "./dist/my-lib.css" + } +} +``` + +## Multi-Page App + +```ts +export default defineConfig({ + build: { + rolldownOptions: { + input: { + main: resolve(import.meta.dirname, 'index.html'), + nested: resolve(import.meta.dirname, 'nested/index.html'), + }, + }, + }, +}) +``` + +## SSR Development + +**Note:** Vite's SSR support is **low-level** and designed mostly for meta-framework authors, not application developers. If you need SSR for your app, use a Vite-based meta-framework instead: + +- **Nuxt** (Vue) - https://nuxt.com +- **SvelteKit** (Svelte) - https://svelte.dev/docs/kit +- **SolidStart** (Solid) - https://start.solidjs.com +- **TanStack Start** (React) - https://tanstack.com/start + +These frameworks build on top of Vite's SSR primitives so you don't have to wire them up yourself. + +**Need a server?** Consider [Nitro](https://nitro.build) -- think of it as "Vite for servers." Nitro provides a portable, framework-agnostic server layer with file-based API routing, auto-imports, and deployment presets for dozens of platforms (Node.js, Deno, Bun, Cloudflare Workers, Vercel, Netlify, etc.). It integrates naturally with Vite and is what powers Nuxt's server engine. See the [Nitro docs](https://nitro.build) for more details. + +## JavaScript API + +### createServer + +```ts +import { createServer } from 'vite' + +const server = await createServer({ + configFile: false, + root: import.meta.dirname, + server: { port: 1337 }, +}) + +await server.listen() +server.printUrls() +``` + +### build + +```ts +import { build } from 'vite' + +await build({ + root: './project', + build: { outDir: 'dist' }, +}) +``` + +### preview + +```ts +import { preview } from 'vite' + +const previewServer = await preview({ + preview: { port: 8080, open: true }, +}) +previewServer.printUrls() +``` + +### resolveConfig + +```ts +import { resolveConfig } from 'vite' + +const config = await resolveConfig({}, 'build') +``` + +### loadEnv + +```ts +import { loadEnv } from 'vite' + +const env = loadEnv('development', process.cwd(), '') +// Loads all env vars (empty prefix = no filtering) +``` + + diff --git a/.agents/skills/vite/references/core-config.md b/.agents/skills/vite/references/core-config.md new file mode 100644 index 0000000..039ba52 --- /dev/null +++ b/.agents/skills/vite/references/core-config.md @@ -0,0 +1,162 @@ +--- +name: vite-config +description: Vite configuration patterns using vite.config.ts +--- + +# Vite Configuration + +## Basic Setup + +```ts +// vite.config.ts +import { defineConfig } from 'vite' + +export default defineConfig({ + // config options +}) +``` + +Vite auto-resolves `vite.config.ts` from project root. Supports ES modules syntax regardless of `package.json` type. + +## Conditional Config + +Export a function to access command and mode: + +```ts +export default defineConfig(({ command, mode, isSsrBuild, isPreview }) => { + if (command === 'serve') { + return { /* dev config */ } + } else { + return { /* build config */ } + } +}) +``` + +- `command`: `'serve'` during dev, `'build'` for production +- `mode`: `'development'` or `'production'` (or custom via `--mode`) + +## Async Config + +```ts +export default defineConfig(async ({ command, mode }) => { + const data = await fetchSomething() + return { /* config */ } +}) +``` + +## Using Environment Variables in Config + +`.env` files are loaded **after** config resolution. Use `loadEnv` to access them in config: + +```ts +import { defineConfig, loadEnv } from 'vite' + +export default defineConfig(({ mode }) => { + // Load env files from cwd, include all vars (empty prefix) + const env = loadEnv(mode, process.cwd(), '') + + return { + define: { + __APP_ENV__: JSON.stringify(env.APP_ENV), + }, + server: { + port: env.APP_PORT ? Number(env.APP_PORT) : 5173, + }, + } +}) +``` + +## Key Config Options + +### resolve.alias + +```ts +export default defineConfig({ + resolve: { + alias: { + '@': '/src', + '~': '/src', + }, + }, +}) +``` + +### define (Global Constants) + +```ts +export default defineConfig({ + define: { + __APP_VERSION__: JSON.stringify('1.0.0'), + __API_URL__: 'window.__backend_api_url', + }, +}) +``` + +Values must be JSON-serializable or single identifiers. Non-strings auto-wrapped with `JSON.stringify`. + +### plugins + +```ts +import vue from '@vitejs/plugin-vue' + +export default defineConfig({ + plugins: [vue()], +}) +``` + +Plugins array is flattened; falsy values ignored. + +### server.proxy + +```ts +export default defineConfig({ + server: { + proxy: { + '/api': { + target: 'http://localhost:3000', + changeOrigin: true, + rewrite: (path) => path.replace(/^\/api/, ''), + }, + }, + }, +}) +``` + +### build.target + +Default: Baseline Widely Available browsers. Customize: + +```ts +export default defineConfig({ + build: { + target: 'esnext', // or 'es2020', ['chrome90', 'firefox88'] + }, +}) +``` + +## TypeScript Intellisense + +For plain JS config files: + +```js +/** @type {import('vite').UserConfig} */ +export default { + // ... +} +``` + +Or use `satisfies`: + +```ts +import type { UserConfig } from 'vite' + +export default { + // ... +} satisfies UserConfig +``` + + diff --git a/.agents/skills/vite/references/core-features.md b/.agents/skills/vite/references/core-features.md new file mode 100644 index 0000000..1403ac9 --- /dev/null +++ b/.agents/skills/vite/references/core-features.md @@ -0,0 +1,205 @@ +--- +name: vite-features +description: Vite-specific import patterns and runtime features +--- + +# Vite Features + +## Glob Import + +Import multiple modules matching a pattern: + +```ts +const modules = import.meta.glob('./dir/*.ts') +// { './dir/foo.ts': () => import('./dir/foo.ts'), ... } + +for (const path in modules) { + modules[path]().then((mod) => { + console.log(path, mod) + }) +} +``` + +### Eager Loading + +```ts +const modules = import.meta.glob('./dir/*.ts', { eager: true }) +// Modules loaded immediately, no dynamic import +``` + +### Named Imports + +```ts +const modules = import.meta.glob('./dir/*.ts', { import: 'setup' }) +// Only imports the 'setup' export from each module + +const defaults = import.meta.glob('./dir/*.ts', { import: 'default', eager: true }) +``` + +### Multiple Patterns + +```ts +const modules = import.meta.glob(['./dir/*.ts', './another/*.ts']) +``` + +### Negative Patterns + +```ts +const modules = import.meta.glob(['./dir/*.ts', '!**/ignored.ts']) +``` + +### Custom Queries + +```ts +const svgRaw = import.meta.glob('./icons/*.svg', { query: '?raw', import: 'default' }) +const svgUrls = import.meta.glob('./icons/*.svg', { query: '?url', import: 'default' }) +``` + +## Asset Import Queries + +### URL Import + +```ts +import imgUrl from './img.png' +// Returns resolved URL: '/src/img.png' (dev) or '/assets/img.2d8efhg.png' (build) +``` + +### Explicit URL + +```ts +import workletUrl from './worklet.js?url' +``` + +### Raw String + +```ts +import shaderCode from './shader.glsl?raw' +``` + +### Inline/No-Inline + +```ts +import inlined from './small.png?inline' // Force base64 inline +import notInlined from './large.png?no-inline' // Force separate file +``` + +### Web Workers + +```ts +import Worker from './worker.ts?worker' +const worker = new Worker() + +// Or inline: +import InlineWorker from './worker.ts?worker&inline' +``` + +Preferred pattern using constructor: + +```ts +const worker = new Worker(new URL('./worker.ts', import.meta.url), { + type: 'module', +}) +``` + +## Environment Variables + +### Built-in Constants + +```ts +import.meta.env.MODE // 'development' | 'production' | custom +import.meta.env.BASE_URL // Base URL from config +import.meta.env.PROD // true in production +import.meta.env.DEV // true in development +import.meta.env.SSR // true when running in server +``` + +### Custom Variables + +Only `VITE_` prefixed vars exposed to client: + +``` +# .env +VITE_API_URL=https://api.example.com +DB_PASSWORD=secret # NOT exposed to client +``` + +```ts +console.log(import.meta.env.VITE_API_URL) // works +console.log(import.meta.env.DB_PASSWORD) // undefined +``` + +### Mode-specific Files + +``` +.env # always loaded +.env.local # always loaded, gitignored +.env.[mode] # only in specified mode +.env.[mode].local # only in specified mode, gitignored +``` + +### TypeScript Support + +```ts +// vite-env.d.ts +interface ImportMetaEnv { + readonly VITE_API_URL: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} +``` + +### HTML Replacement + +```html +

Running in %MODE%

+ +``` + +## CSS Modules + +Any `.module.css` file treated as CSS module: + +```ts +import styles from './component.module.css' +element.className = styles.button +``` + +With camelCase conversion: + +```ts +// .my-class -> myClass (if css.modules.localsConvention configured) +import { myClass } from './component.module.css' +``` + +## JSON Import + +```ts +import pkg from './package.json' +import { version } from './package.json' // Named import with tree-shaking +``` + +## HMR API + +```ts +if (import.meta.hot) { + import.meta.hot.accept((newModule) => { + // Handle update + }) + + import.meta.hot.dispose((data) => { + // Cleanup before module is replaced + }) + + import.meta.hot.invalidate() // Force full reload +} +``` + + diff --git a/.agents/skills/vite/references/core-plugin-api.md b/.agents/skills/vite/references/core-plugin-api.md new file mode 100644 index 0000000..c115851 --- /dev/null +++ b/.agents/skills/vite/references/core-plugin-api.md @@ -0,0 +1,235 @@ +--- +name: vite-plugin-api +description: Vite plugin authoring with Vite-specific hooks +--- + +# Vite Plugin API + +Vite plugins extend Rolldown's plugin interface with Vite-specific hooks. + +## Basic Structure + +```ts +function myPlugin(): Plugin { + return { + name: 'my-plugin', + // hooks... + } +} +``` + +## Vite-Specific Hooks + +### config + +Modify config before resolution: + +```ts +const plugin = () => ({ + name: 'add-alias', + config: () => ({ + resolve: { + alias: { foo: 'bar' }, + }, + }), +}) +``` + +### configResolved + +Access final resolved config: + +```ts +const plugin = () => { + let config: ResolvedConfig + return { + name: 'read-config', + configResolved(resolvedConfig) { + config = resolvedConfig + }, + transform(code, id) { + if (config.command === 'serve') { /* dev */ } + }, + } +} +``` + +### configureServer + +Add custom middleware to dev server: + +```ts +const plugin = () => ({ + name: 'custom-middleware', + configureServer(server) { + server.middlewares.use((req, res, next) => { + // handle request + next() + }) + }, +}) +``` + +Return function to run **after** internal middlewares: + +```ts +configureServer(server) { + return () => { + server.middlewares.use((req, res, next) => { + // runs after Vite's middlewares + }) + } +} +``` + +### transformIndexHtml + +Transform HTML entry files: + +```ts +const plugin = () => ({ + name: 'html-transform', + transformIndexHtml(html) { + return html.replace(/(.*?)<\/title>/, '<title>New Title') + }, +}) +``` + +Inject tags: + +```ts +transformIndexHtml() { + return [ + { tag: 'script', attrs: { src: '/inject.js' }, injectTo: 'body' }, + ] +} +``` + +### handleHotUpdate + +Custom HMR handling: + +```ts +handleHotUpdate({ server, modules, timestamp }) { + server.ws.send({ type: 'custom', event: 'special-update', data: {} }) + return [] // empty = skip default HMR +} +``` + +## Virtual Modules + +Serve virtual content without files on disk: + +```ts +const plugin = () => { + const virtualModuleId = 'virtual:my-module' + const resolvedId = '\0' + virtualModuleId + + return { + name: 'virtual-module', + resolveId(id) { + if (id === virtualModuleId) return resolvedId + }, + load(id) { + if (id === resolvedId) { + return `export const msg = "from virtual module"` + } + }, + } +} +``` + +Usage: + +```ts +import { msg } from 'virtual:my-module' +``` + +Convention: prefix user-facing path with `virtual:`, prefix resolved id with `\0`. + +## Plugin Ordering + +Use `enforce` to control execution order: + +```ts +{ + name: 'pre-plugin', + enforce: 'pre', // runs before core plugins +} + +{ + name: 'post-plugin', + enforce: 'post', // runs after build plugins +} +``` + +Order: Alias → `enforce: 'pre'` → Core → User (no enforce) → Build → `enforce: 'post'` → Post-build + +## Conditional Application + +```ts +{ + name: 'build-only', + apply: 'build', // or 'serve' +} + +// Function form: +{ + apply(config, { command }) { + return command === 'build' && !config.build.ssr + } +} +``` + +## Universal Hooks (from Rolldown) + +These work in both dev and build: + +- `resolveId(id, importer)` - Resolve import paths +- `load(id)` - Load module content +- `transform(code, id)` - Transform module code + +```ts +transform(code, id) { + if (id.endsWith('.custom')) { + return { code: compile(code), map: null } + } +} +``` + +## Client-Server Communication + +Server to client: + +```ts +configureServer(server) { + server.ws.send('my:event', { msg: 'hello' }) +} +``` + +Client side: + +```ts +if (import.meta.hot) { + import.meta.hot.on('my:event', (data) => { + console.log(data.msg) + }) +} +``` + +Client to server: + +```ts +// Client +import.meta.hot.send('my:from-client', { msg: 'Hey!' }) + +// Server +server.ws.on('my:from-client', (data, client) => { + client.send('my:ack', { msg: 'Got it!' }) +}) +``` + + diff --git a/.agents/skills/vite/references/environment-api.md b/.agents/skills/vite/references/environment-api.md new file mode 100644 index 0000000..006ff7f --- /dev/null +++ b/.agents/skills/vite/references/environment-api.md @@ -0,0 +1,108 @@ +--- +name: vite-environment-api +description: Vite 6+ Environment API for multiple runtime environments +--- + +# Environment API (Vite 6+) + +The Environment API formalizes multiple runtime environments beyond the traditional client/SSR split. + +## Concept + +Before Vite 6: Two implicit environments (`client` and `ssr`). + +Vite 6+: Configure as many environments as needed (browser, node server, edge server, etc.). + +## Basic Configuration + +For SPA/MPA, nothing changes—options apply to the implicit `client` environment: + +```ts +export default defineConfig({ + build: { sourcemap: false }, + optimizeDeps: { include: ['lib'] }, +}) +``` + +## Multiple Environments + +```ts +export default defineConfig({ + build: { sourcemap: false }, // Inherited by all environments + optimizeDeps: { include: ['lib'] }, // Client only + environments: { + // SSR environment + server: {}, + // Edge runtime environment + edge: { + resolve: { noExternal: true }, + }, + }, +}) +``` + +Environments inherit top-level config. Some options (like `optimizeDeps`) only apply to `client` by default. + +## Environment Options + +```ts +interface EnvironmentOptions { + define?: Record + resolve?: EnvironmentResolveOptions + optimizeDeps: DepOptimizationOptions + consumer?: 'client' | 'server' + dev: DevOptions + build: BuildOptions +} +``` + +## Custom Environment Instances + +Runtime providers can define custom environments: + +```ts +import { customEnvironment } from 'vite-environment-provider' + +export default defineConfig({ + environments: { + ssr: customEnvironment({ + build: { outDir: '/dist/ssr' }, + }), + }, +}) +``` + +Example: Cloudflare's Vite plugin runs code in `workerd` runtime during development. + +## Backward Compatibility + +- `server.moduleGraph` returns mixed client/SSR view +- `ssrLoadModule` still works +- Existing SSR apps work unchanged + +## When to Use + +- **End users**: Usually don't need to configure—frameworks handle it +- **Plugin authors**: Use for environment-aware transformations +- **Framework authors**: Create custom environments for their runtime needs + +## Plugin Environment Access + +Plugins can access environment in hooks: + +```ts +{ + name: 'env-aware', + transform(code, id, options) { + if (options?.ssr) { + // SSR-specific transform + } + }, +} +``` + + diff --git a/.agents/skills/vite/references/rolldown-migration.md b/.agents/skills/vite/references/rolldown-migration.md new file mode 100644 index 0000000..28d6d76 --- /dev/null +++ b/.agents/skills/vite/references/rolldown-migration.md @@ -0,0 +1,157 @@ +--- +name: vite-rolldown +description: Vite 8 Rolldown bundler and Oxc transformer migration +--- + +# Rolldown Migration (Vite 8) + +Vite 8 replaces esbuild+Rollup with Rolldown, a unified Rust-based bundler. + +## What Changed + +| Before (Vite 7) | After (Vite 8) | +|-----------------|----------------| +| esbuild (dev transform) | Oxc Transformer | +| esbuild (dep pre-bundling) | Rolldown | +| Rollup (production build) | Rolldown | +| `rollupOptions` | `rolldownOptions` | +| `esbuild` option | `oxc` option | + +## Performance Impact + +- 10-30x faster than Rollup for production builds +- Matches esbuild's dev performance +- Unified behavior between dev and build + +## Config Migration + +### rollupOptions → rolldownOptions + +```ts +// Before (Vite 7) +export default defineConfig({ + build: { + rollupOptions: { + external: ['vue'], + output: { globals: { vue: 'Vue' } }, + }, + }, +}) + +// After (Vite 8) +export default defineConfig({ + build: { + rolldownOptions: { + external: ['vue'], + output: { globals: { vue: 'Vue' } }, + }, + }, +}) +``` + +### esbuild → oxc + +```ts +// Before (Vite 7) +export default defineConfig({ + esbuild: { + jsxFactory: 'h', + jsxFragment: 'Fragment', + }, +}) + +// After (Vite 8) +export default defineConfig({ + oxc: { + jsx: { + runtime: 'classic', + pragma: 'h', + pragmaFrag: 'Fragment', + }, + }, +}) +``` + +### JSX Configuration + +```ts +export default defineConfig({ + oxc: { + jsx: { + runtime: 'automatic', // or 'classic' + importSource: 'react', // for automatic runtime + }, + jsxInject: `import React from 'react'`, // auto-inject + }, +}) +``` + +### Custom Transform Targets + +```ts +export default defineConfig({ + oxc: { + include: ['**/*.ts', '**/*.tsx'], + exclude: ['node_modules/**'], + }, +}) +``` + +## Plugin Compatibility + +Most Vite plugins work unchanged. Rolldown supports Rollup's plugin API. + +If a plugin only works during build: + +```ts +{ + ...rollupPlugin(), + enforce: 'post', + apply: 'build', +} +``` + +## New Capabilities + +Rolldown unlocks features not possible before: + +- Full bundle mode (experimental) +- Module-level persistent cache +- More flexible chunk splitting +- Module Federation support + +## Gradual Migration + +For large projects, migrate via `rolldown-vite` first: + +```bash +# Step 1: Test with rolldown-vite +pnpm add -D rolldown-vite + +# Replace vite import in config +import { defineConfig } from 'rolldown-vite' + +# Step 2: Once stable, upgrade to Vite 8 +pnpm add -D vite@8 +``` + +## Overriding Vite in Frameworks + +When framework depends on older Vite: + +```json +{ + "pnpm": { + "overrides": { + "vite": "8.0.0" + } + } +} +``` + + diff --git a/.agents/skills/vitest/GENERATION.md b/.agents/skills/vitest/GENERATION.md new file mode 100644 index 0000000..9bc7664 --- /dev/null +++ b/.agents/skills/vitest/GENERATION.md @@ -0,0 +1,5 @@ +# Generation Info + +- **Source:** `sources/vitest` +- **Git SHA:** `4a7321e10672f00f0bb698823a381c2cc245b8f7` +- **Generated:** 2026-01-28 diff --git a/.agents/skills/vitest/SKILL.md b/.agents/skills/vitest/SKILL.md new file mode 100644 index 0000000..0578bdc --- /dev/null +++ b/.agents/skills/vitest/SKILL.md @@ -0,0 +1,52 @@ +--- +name: vitest +description: Vitest fast unit testing framework powered by Vite with Jest-compatible API. Use when writing tests, mocking, configuring coverage, or working with test filtering and fixtures. +metadata: + author: Anthony Fu + version: "2026.1.28" + source: Generated from https://github.com/vitest-dev/vitest, scripts located at https://github.com/antfu/skills +--- + +Vitest is a next-generation testing framework powered by Vite. It provides a Jest-compatible API with native ESM, TypeScript, and JSX support out of the box. Vitest shares the same config, transformers, resolvers, and plugins with your Vite app. + +**Key Features:** +- Vite-native: Uses Vite's transformation pipeline for fast HMR-like test updates +- Jest-compatible: Drop-in replacement for most Jest test suites +- Smart watch mode: Only reruns affected tests based on module graph +- Native ESM, TypeScript, JSX support without configuration +- Multi-threaded workers for parallel test execution +- Built-in coverage via V8 or Istanbul +- Snapshot testing, mocking, and spy utilities + +> The skill is based on Vitest 3.x, generated at 2026-01-28. + +## Core + +| Topic | Description | Reference | +|-------|-------------|-----------| +| Configuration | Vitest and Vite config integration, defineConfig usage | [core-config](references/core-config.md) | +| CLI | Command line interface, commands and options | [core-cli](references/core-cli.md) | +| Test API | test/it function, modifiers like skip, only, concurrent | [core-test-api](references/core-test-api.md) | +| Describe API | describe/suite for grouping tests and nested suites | [core-describe](references/core-describe.md) | +| Expect API | Assertions with toBe, toEqual, matchers and asymmetric matchers | [core-expect](references/core-expect.md) | +| Hooks | beforeEach, afterEach, beforeAll, afterAll, aroundEach | [core-hooks](references/core-hooks.md) | + +## Features + +| Topic | Description | Reference | +|-------|-------------|-----------| +| Mocking | Mock functions, modules, timers, dates with vi utilities | [features-mocking](references/features-mocking.md) | +| Snapshots | Snapshot testing with toMatchSnapshot and inline snapshots | [features-snapshots](references/features-snapshots.md) | +| Coverage | Code coverage with V8 or Istanbul providers | [features-coverage](references/features-coverage.md) | +| Test Context | Test fixtures, context.expect, test.extend for custom fixtures | [features-context](references/features-context.md) | +| Concurrency | Concurrent tests, parallel execution, sharding | [features-concurrency](references/features-concurrency.md) | +| Filtering | Filter tests by name, file patterns, tags | [features-filtering](references/features-filtering.md) | + +## Advanced + +| Topic | Description | Reference | +|-------|-------------|-----------| +| Vi Utilities | vi helper: mock, spyOn, fake timers, hoisted, waitFor | [advanced-vi](references/advanced-vi.md) | +| Environments | Test environments: node, jsdom, happy-dom, custom | [advanced-environments](references/advanced-environments.md) | +| Type Testing | Type-level testing with expectTypeOf and assertType | [advanced-type-testing](references/advanced-type-testing.md) | +| Projects | Multi-project workspaces, different configs per project | [advanced-projects](references/advanced-projects.md) | diff --git a/.agents/skills/vitest/references/advanced-environments.md b/.agents/skills/vitest/references/advanced-environments.md new file mode 100644 index 0000000..25a1d5b --- /dev/null +++ b/.agents/skills/vitest/references/advanced-environments.md @@ -0,0 +1,264 @@ +--- +name: test-environments +description: Configure environments like jsdom, happy-dom for browser APIs +--- + +# Test Environments + +## Available Environments + +- `node` (default) - Node.js environment +- `jsdom` - Browser-like with DOM APIs +- `happy-dom` - Faster alternative to jsdom +- `edge-runtime` - Vercel Edge Runtime + +## Configuration + +```ts +// vitest.config.ts +defineConfig({ + test: { + environment: 'jsdom', + + // Environment-specific options + environmentOptions: { + jsdom: { + url: 'http://localhost', + }, + }, + }, +}) +``` + +## Installing Environment Packages + +```bash +# jsdom +npm i -D jsdom + +# happy-dom (faster, fewer APIs) +npm i -D happy-dom +``` + +## Per-File Environment + +Use magic comment at top of file: + +```ts +// @vitest-environment jsdom + +import { expect, test } from 'vitest' + +test('DOM test', () => { + const div = document.createElement('div') + expect(div).toBeInstanceOf(HTMLDivElement) +}) +``` + +## jsdom Environment + +Full browser environment simulation: + +```ts +// @vitest-environment jsdom + +test('DOM manipulation', () => { + document.body.innerHTML = '
' + + const app = document.getElementById('app') + app.textContent = 'Hello' + + expect(app.textContent).toBe('Hello') +}) + +test('window APIs', () => { + expect(window.location.href).toBeDefined() + expect(localStorage).toBeDefined() +}) +``` + +### jsdom Options + +```ts +defineConfig({ + test: { + environmentOptions: { + jsdom: { + url: 'http://localhost:3000', + html: '', + userAgent: 'custom-agent', + resources: 'usable', + }, + }, + }, +}) +``` + +## happy-dom Environment + +Faster but fewer APIs: + +```ts +// @vitest-environment happy-dom + +test('basic DOM', () => { + const el = document.createElement('div') + el.className = 'test' + expect(el.className).toBe('test') +}) +``` + +## Multiple Environments per Project + +Use projects for different environments: + +```ts +defineConfig({ + test: { + projects: [ + { + test: { + name: 'unit', + include: ['tests/unit/**/*.test.ts'], + environment: 'node', + }, + }, + { + test: { + name: 'dom', + include: ['tests/dom/**/*.test.ts'], + environment: 'jsdom', + }, + }, + ], + }, +}) +``` + +## Custom Environment + +Create custom environment package: + +```ts +// vitest-environment-custom/index.ts +import type { Environment } from 'vitest/runtime' + +export default { + name: 'custom', + viteEnvironment: 'ssr', // or 'client' + + setup() { + // Setup global state + globalThis.myGlobal = 'value' + + return { + teardown() { + delete globalThis.myGlobal + }, + } + }, +} +``` + +Use with: + +```ts +defineConfig({ + test: { + environment: 'custom', + }, +}) +``` + +## Environment with VM + +For full isolation: + +```ts +export default { + name: 'isolated', + viteEnvironment: 'ssr', + + async setupVM() { + const vm = await import('node:vm') + const context = vm.createContext() + + return { + getVmContext() { + return context + }, + teardown() {}, + } + }, + + setup() { + return { teardown() {} } + }, +} +``` + +## Browser Mode (Separate from Environments) + +For real browser testing, use Vitest Browser Mode: + +```ts +defineConfig({ + test: { + browser: { + enabled: true, + name: 'chromium', // or 'firefox', 'webkit' + provider: 'playwright', + }, + }, +}) +``` + +## CSS and Assets + +In jsdom/happy-dom, configure CSS handling: + +```ts +defineConfig({ + test: { + css: true, // Process CSS + + // Or with options + css: { + include: /\.module\.css$/, + modules: { + classNameStrategy: 'non-scoped', + }, + }, + }, +}) +``` + +## Fixing External Dependencies + +If external deps fail with CSS/asset errors: + +```ts +defineConfig({ + test: { + server: { + deps: { + inline: ['problematic-package'], + }, + }, + }, +}) +``` + +## Key Points + +- Default is `node` - no browser APIs +- Use `jsdom` for full browser simulation +- Use `happy-dom` for faster tests with basic DOM +- Per-file environment via `// @vitest-environment` comment +- Use projects for multiple environment configurations +- Browser Mode is for real browser testing, not environment + + diff --git a/.agents/skills/vitest/references/advanced-projects.md b/.agents/skills/vitest/references/advanced-projects.md new file mode 100644 index 0000000..57b9a73 --- /dev/null +++ b/.agents/skills/vitest/references/advanced-projects.md @@ -0,0 +1,300 @@ +--- +name: projects-workspaces +description: Multi-project configuration for monorepos and different test types +--- + +# Projects + +Run different test configurations in the same Vitest process. + +## Basic Projects Setup + +```ts +// vitest.config.ts +defineConfig({ + test: { + projects: [ + // Glob patterns for config files + 'packages/*', + + // Inline config + { + test: { + name: 'unit', + include: ['tests/unit/**/*.test.ts'], + environment: 'node', + }, + }, + { + test: { + name: 'integration', + include: ['tests/integration/**/*.test.ts'], + environment: 'jsdom', + }, + }, + ], + }, +}) +``` + +## Monorepo Pattern + +```ts +defineConfig({ + test: { + projects: [ + // Each package has its own vitest.config.ts + 'packages/core', + 'packages/cli', + 'packages/utils', + ], + }, +}) +``` + +Package config: + +```ts +// packages/core/vitest.config.ts +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + name: 'core', + include: ['src/**/*.test.ts'], + environment: 'node', + }, +}) +``` + +## Different Environments + +Run same tests in different environments: + +```ts +defineConfig({ + test: { + projects: [ + { + test: { + name: 'happy-dom', + root: './shared-tests', + environment: 'happy-dom', + setupFiles: ['./setup.happy-dom.ts'], + }, + }, + { + test: { + name: 'node', + root: './shared-tests', + environment: 'node', + setupFiles: ['./setup.node.ts'], + }, + }, + ], + }, +}) +``` + +## Browser + Node Projects + +```ts +defineConfig({ + test: { + projects: [ + { + test: { + name: 'unit', + include: ['tests/unit/**/*.test.ts'], + environment: 'node', + }, + }, + { + test: { + name: 'browser', + include: ['tests/browser/**/*.test.ts'], + browser: { + enabled: true, + name: 'chromium', + provider: 'playwright', + }, + }, + }, + ], + }, +}) +``` + +## Shared Configuration + +```ts +// vitest.shared.ts +export const sharedConfig = { + testTimeout: 10000, + setupFiles: ['./tests/setup.ts'], +} + +// vitest.config.ts +import { sharedConfig } from './vitest.shared' + +defineConfig({ + test: { + projects: [ + { + test: { + ...sharedConfig, + name: 'unit', + include: ['tests/unit/**/*.test.ts'], + }, + }, + { + test: { + ...sharedConfig, + name: 'e2e', + include: ['tests/e2e/**/*.test.ts'], + }, + }, + ], + }, +}) +``` + +## Project-Specific Dependencies + +Each project can have different dependencies inlined: + +```ts +defineConfig({ + test: { + projects: [ + { + test: { + name: 'project-a', + server: { + deps: { + inline: ['package-a'], + }, + }, + }, + }, + ], + }, +}) +``` + +## Running Specific Projects + +```bash +# Run specific project +vitest --project unit +vitest --project integration + +# Multiple projects +vitest --project unit --project e2e + +# Exclude project +vitest --project.ignore browser +``` + +## Providing Values to Projects + +Share values from config to tests: + +```ts +// vitest.config.ts +defineConfig({ + test: { + projects: [ + { + test: { + name: 'staging', + provide: { + apiUrl: 'https://staging.api.com', + debug: true, + }, + }, + }, + { + test: { + name: 'production', + provide: { + apiUrl: 'https://api.com', + debug: false, + }, + }, + }, + ], + }, +}) + +// In tests, use inject +import { inject } from 'vitest' + +test('uses correct api', () => { + const url = inject('apiUrl') + expect(url).toContain('api.com') +}) +``` + +## With Fixtures + +```ts +const test = base.extend({ + apiUrl: ['/default', { injected: true }], +}) + +test('uses injected url', ({ apiUrl }) => { + // apiUrl comes from project's provide config +}) +``` + +## Project Isolation + +Each project runs in its own thread pool by default: + +```ts +defineConfig({ + test: { + projects: [ + { + test: { + name: 'isolated', + isolate: true, // Full isolation + pool: 'forks', + }, + }, + ], + }, +}) +``` + +## Global Setup per Project + +```ts +defineConfig({ + test: { + projects: [ + { + test: { + name: 'with-db', + globalSetup: ['./tests/db-setup.ts'], + }, + }, + ], + }, +}) +``` + +## Key Points + +- Projects run in same Vitest process +- Each project can have different environment, config +- Use glob patterns for monorepo packages +- Run specific projects with `--project` flag +- Use `provide` to inject config values into tests +- Projects inherit from root config unless overridden + + diff --git a/.agents/skills/vitest/references/advanced-type-testing.md b/.agents/skills/vitest/references/advanced-type-testing.md new file mode 100644 index 0000000..f67a034 --- /dev/null +++ b/.agents/skills/vitest/references/advanced-type-testing.md @@ -0,0 +1,237 @@ +--- +name: type-testing +description: Test TypeScript types with expectTypeOf and assertType +--- + +# Type Testing + +Test TypeScript types without runtime execution. + +## Setup + +Type tests use `.test-d.ts` extension: + +```ts +// math.test-d.ts +import { expectTypeOf } from 'vitest' +import { add } from './math' + +test('add returns number', () => { + expectTypeOf(add).returns.toBeNumber() +}) +``` + +## Configuration + +```ts +defineConfig({ + test: { + typecheck: { + enabled: true, + + // Only type check + only: false, + + // Checker: 'tsc' or 'vue-tsc' + checker: 'tsc', + + // Include patterns + include: ['**/*.test-d.ts'], + + // tsconfig to use + tsconfig: './tsconfig.json', + }, + }, +}) +``` + +## expectTypeOf API + +```ts +import { expectTypeOf } from 'vitest' + +// Basic type checks +expectTypeOf().toBeString() +expectTypeOf().toBeNumber() +expectTypeOf().toBeBoolean() +expectTypeOf().toBeNull() +expectTypeOf().toBeUndefined() +expectTypeOf().toBeVoid() +expectTypeOf().toBeNever() +expectTypeOf().toBeAny() +expectTypeOf().toBeUnknown() +expectTypeOf().toBeObject() +expectTypeOf().toBeFunction() +expectTypeOf<[]>().toBeArray() +expectTypeOf().toBeSymbol() +``` + +## Value Type Checking + +```ts +const value = 'hello' +expectTypeOf(value).toBeString() + +const obj = { name: 'test', count: 42 } +expectTypeOf(obj).toMatchTypeOf<{ name: string }>() +expectTypeOf(obj).toHaveProperty('name') +``` + +## Function Types + +```ts +function greet(name: string): string { + return `Hello, ${name}` +} + +expectTypeOf(greet).toBeFunction() +expectTypeOf(greet).parameters.toEqualTypeOf<[string]>() +expectTypeOf(greet).returns.toBeString() + +// Parameter checking +expectTypeOf(greet).parameter(0).toBeString() +``` + +## Object Types + +```ts +interface User { + id: number + name: string + email?: string +} + +expectTypeOf().toHaveProperty('id') +expectTypeOf().toHaveProperty('name').toBeString() + +// Check shape +expectTypeOf({ id: 1, name: 'test' }).toMatchTypeOf() +``` + +## Equality vs Matching + +```ts +interface A { x: number } +interface B { x: number; y: string } + +// toMatchTypeOf - subset matching +expectTypeOf().toMatchTypeOf() // B extends A + +// toEqualTypeOf - exact match +expectTypeOf().not.toEqualTypeOf() // Not exact match +expectTypeOf().toEqualTypeOf<{ x: number }>() // Exact match +``` + +## Branded Types + +```ts +type UserId = number & { __brand: 'UserId' } +type PostId = number & { __brand: 'PostId' } + +expectTypeOf().not.toEqualTypeOf() +expectTypeOf().not.toEqualTypeOf() +``` + +## Generic Types + +```ts +function identity(value: T): T { + return value +} + +expectTypeOf(identity).returns.toBeString() +expectTypeOf(identity).returns.toBeNumber() +``` + +## Nullable Types + +```ts +type MaybeString = string | null | undefined + +expectTypeOf().toBeNullable() +expectTypeOf().not.toBeNullable() +``` + +## assertType + +Assert a value matches a type (no assertion at runtime): + +```ts +import { assertType } from 'vitest' + +function getUser(): User | null { + return { id: 1, name: 'test' } +} + +test('returns user', () => { + const result = getUser() + + // @ts-expect-error - should fail type check + assertType(result) + + // Correct type + assertType(result) +}) +``` + +## Using @ts-expect-error + +Test that code produces type error: + +```ts +test('rejects wrong types', () => { + function requireString(s: string) {} + + // @ts-expect-error - number not assignable to string + requireString(123) +}) +``` + +## Running Type Tests + +```bash +# Run type tests +vitest typecheck + +# Run alongside unit tests +vitest --typecheck + +# Type tests only +vitest --typecheck.only +``` + +## Mixed Test Files + +Combine runtime and type tests: + +```ts +// user.test.ts +import { describe, expect, expectTypeOf, test } from 'vitest' +import { createUser } from './user' + +describe('createUser', () => { + test('runtime: creates user', () => { + const user = createUser('John') + expect(user.name).toBe('John') + }) + + test('types: returns User type', () => { + expectTypeOf(createUser).returns.toMatchTypeOf<{ name: string }>() + }) +}) +``` + +## Key Points + +- Use `.test-d.ts` for type-only tests +- `expectTypeOf` for type assertions +- `toMatchTypeOf` for subset matching +- `toEqualTypeOf` for exact type matching +- Use `@ts-expect-error` to test type errors +- Run with `vitest typecheck` or `--typecheck` + + diff --git a/.agents/skills/vitest/references/advanced-vi.md b/.agents/skills/vitest/references/advanced-vi.md new file mode 100644 index 0000000..57a4784 --- /dev/null +++ b/.agents/skills/vitest/references/advanced-vi.md @@ -0,0 +1,249 @@ +--- +name: vi-utilities +description: vi helper for mocking, timers, utilities +--- + +# Vi Utilities + +The `vi` helper provides mocking and utility functions. + +```ts +import { vi } from 'vitest' +``` + +## Mock Functions + +```ts +// Create mock +const fn = vi.fn() +const fnWithImpl = vi.fn((x) => x * 2) + +// Check if mock +vi.isMockFunction(fn) // true + +// Mock methods +fn.mockReturnValue(42) +fn.mockReturnValueOnce(1) +fn.mockResolvedValue(data) +fn.mockRejectedValue(error) +fn.mockImplementation(() => 'result') +fn.mockImplementationOnce(() => 'once') + +// Clear/reset +fn.mockClear() // Clear call history +fn.mockReset() // Clear history + implementation +fn.mockRestore() // Restore original (for spies) +``` + +## Spying + +```ts +const obj = { method: () => 'original' } + +const spy = vi.spyOn(obj, 'method') +obj.method() + +expect(spy).toHaveBeenCalled() + +// Mock implementation +spy.mockReturnValue('mocked') + +// Spy on getter/setter +vi.spyOn(obj, 'prop', 'get').mockReturnValue('value') +``` + +## Module Mocking + +```ts +// Hoisted to top of file +vi.mock('./module', () => ({ + fn: vi.fn(), +})) + +// Partial mock +vi.mock('./module', async (importOriginal) => ({ + ...(await importOriginal()), + specificFn: vi.fn(), +})) + +// Spy mode - keep implementation +vi.mock('./module', { spy: true }) + +// Import actual module inside mock +const actual = await vi.importActual('./module') + +// Import as mock +const mocked = await vi.importMock('./module') +``` + +## Dynamic Mocking + +```ts +// Not hoisted - use with dynamic imports +vi.doMock('./config', () => ({ key: 'value' })) +const config = await import('./config') + +// Unmock +vi.doUnmock('./config') +vi.unmock('./module') // Hoisted +``` + +## Reset Modules + +```ts +// Clear module cache +vi.resetModules() + +// Wait for dynamic imports +await vi.dynamicImportSettled() +``` + +## Fake Timers + +```ts +vi.useFakeTimers() + +setTimeout(() => console.log('done'), 1000) + +// Advance time +vi.advanceTimersByTime(1000) +vi.advanceTimersByTimeAsync(1000) // For async callbacks +vi.advanceTimersToNextTimer() +vi.advanceTimersToNextFrame() // requestAnimationFrame + +// Run all timers +vi.runAllTimers() +vi.runAllTimersAsync() +vi.runOnlyPendingTimers() + +// Clear timers +vi.clearAllTimers() + +// Check state +vi.getTimerCount() +vi.isFakeTimers() + +// Restore +vi.useRealTimers() +``` + +## Mock Date/Time + +```ts +vi.setSystemTime(new Date('2024-01-01')) +expect(new Date().getFullYear()).toBe(2024) + +vi.getMockedSystemTime() // Get mocked date +vi.getRealSystemTime() // Get real time (ms) +``` + +## Global/Env Mocking + +```ts +// Stub global +vi.stubGlobal('fetch', vi.fn()) +vi.unstubAllGlobals() + +// Stub environment +vi.stubEnv('API_KEY', 'test') +vi.stubEnv('NODE_ENV', 'test') +vi.unstubAllEnvs() +``` + +## Hoisted Code + +Run code before imports: + +```ts +const mock = vi.hoisted(() => vi.fn()) + +vi.mock('./module', () => ({ + fn: mock, // Can reference hoisted variable +})) +``` + +## Waiting Utilities + +```ts +// Wait for callback to succeed +await vi.waitFor(async () => { + const el = document.querySelector('.loaded') + expect(el).toBeTruthy() +}, { timeout: 5000, interval: 100 }) + +// Wait for truthy value +const element = await vi.waitUntil( + () => document.querySelector('.loaded'), + { timeout: 5000 } +) +``` + +## Mock Object + +Mock all methods of an object: + +```ts +const original = { + method: () => 'real', + nested: { fn: () => 'nested' }, +} + +const mocked = vi.mockObject(original) +mocked.method() // undefined (mocked) +mocked.method.mockReturnValue('mocked') + +// Spy mode +const spied = vi.mockObject(original, { spy: true }) +spied.method() // 'real' +expect(spied.method).toHaveBeenCalled() +``` + +## Test Configuration + +```ts +vi.setConfig({ + testTimeout: 10_000, + hookTimeout: 10_000, +}) + +vi.resetConfig() +``` + +## Global Mock Management + +```ts +vi.clearAllMocks() // Clear all mock call history +vi.resetAllMocks() // Reset + clear implementation +vi.restoreAllMocks() // Restore originals (spies) +``` + +## vi.mocked Type Helper + +TypeScript helper for mocked values: + +```ts +import { myFn } from './module' +vi.mock('./module') + +// Type as mock +vi.mocked(myFn).mockReturnValue('typed') + +// Deep mocking +vi.mocked(myModule, { deep: true }) + +// Partial mock typing +vi.mocked(fn, { partial: true }).mockResolvedValue({ ok: true }) +``` + +## Key Points + +- `vi.mock` is hoisted - use `vi.doMock` for dynamic mocking +- `vi.hoisted` lets you reference variables in mock factories +- Use `vi.spyOn` to spy on existing methods +- Fake timers require explicit setup and teardown +- `vi.waitFor` retries until assertion passes + + diff --git a/.agents/skills/vitest/references/core-cli.md b/.agents/skills/vitest/references/core-cli.md new file mode 100644 index 0000000..7a05c04 --- /dev/null +++ b/.agents/skills/vitest/references/core-cli.md @@ -0,0 +1,166 @@ +--- +name: vitest-cli +description: Command line interface commands and options +--- + +# Command Line Interface + +## Commands + +### `vitest` + +Start Vitest in watch mode (dev) or run mode (CI): + +```bash +vitest # Watch mode in dev, run mode in CI +vitest foobar # Run tests containing "foobar" in path +vitest basic/foo.test.ts:10 # Run specific test by file and line number +``` + +### `vitest run` + +Run tests once without watch mode: + +```bash +vitest run +vitest run --coverage +``` + +### `vitest watch` + +Explicitly start watch mode: + +```bash +vitest watch +``` + +### `vitest related` + +Run tests that import specific files (useful with lint-staged): + +```bash +vitest related src/index.ts src/utils.ts --run +``` + +### `vitest bench` + +Run only benchmark tests: + +```bash +vitest bench +``` + +### `vitest list` + +List all matching tests without running them: + +```bash +vitest list # List test names +vitest list --json # Output as JSON +vitest list --filesOnly # List only test files +``` + +### `vitest init` + +Initialize project setup: + +```bash +vitest init browser # Set up browser testing +``` + +## Common Options + +```bash +# Configuration +--config # Path to config file +--project # Run specific project + +# Filtering +--testNamePattern, -t # Run tests matching pattern +--changed # Run tests for changed files +--changed HEAD~1 # Tests for last commit changes + +# Reporters +--reporter # default, verbose, dot, json, html +--reporter=html --outputFile=report.html + +# Coverage +--coverage # Enable coverage +--coverage.provider v8 # Use v8 provider +--coverage.reporter text,html + +# Execution +--shard / # Split tests across machines +--bail # Stop after n failures +--retry # Retry failed tests n times +--sequence.shuffle # Randomize test order + +# Watch mode +--no-watch # Disable watch mode +--standalone # Start without running tests + +# Environment +--environment # jsdom, happy-dom, node +--globals # Enable global APIs + +# Debugging +--inspect # Enable Node inspector +--inspect-brk # Break on start + +# Output +--silent # Suppress console output +--no-color # Disable colors +``` + +## Package.json Scripts + +```json +{ + "scripts": { + "test": "vitest", + "test:run": "vitest run", + "test:ui": "vitest --ui", + "coverage": "vitest run --coverage" + } +} +``` + +## Sharding for CI + +Split tests across multiple machines: + +```bash +# Machine 1 +vitest run --shard=1/3 --reporter=blob + +# Machine 2 +vitest run --shard=2/3 --reporter=blob + +# Machine 3 +vitest run --shard=3/3 --reporter=blob + +# Merge reports +vitest --merge-reports --reporter=junit +``` + +## Watch Mode Keyboard Shortcuts + +In watch mode, press: +- `a` - Run all tests +- `f` - Run only failed tests +- `u` - Update snapshots +- `p` - Filter by filename pattern +- `t` - Filter by test name pattern +- `q` - Quit + +## Key Points + +- Watch mode is default in dev, run mode in CI (when `process.env.CI` is set) +- Use `--run` flag to ensure single run (important for lint-staged) +- Both camelCase (`--testTimeout`) and kebab-case (`--test-timeout`) work +- Boolean options can be negated with `--no-` prefix + + diff --git a/.agents/skills/vitest/references/core-config.md b/.agents/skills/vitest/references/core-config.md new file mode 100644 index 0000000..76002a5 --- /dev/null +++ b/.agents/skills/vitest/references/core-config.md @@ -0,0 +1,174 @@ +--- +name: vitest-configuration +description: Configure Vitest with vite.config.ts or vitest.config.ts +--- + +# Configuration + +Vitest reads configuration from `vitest.config.ts` or `vite.config.ts`. It shares the same config format as Vite. + +## Basic Setup + +```ts +// vitest.config.ts +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + // test options + }, +}) +``` + +## Using with Existing Vite Config + +Add Vitest types reference and use the `test` property: + +```ts +// vite.config.ts +/// +import { defineConfig } from 'vite' + +export default defineConfig({ + test: { + globals: true, + environment: 'jsdom', + }, +}) +``` + +## Merging Configs + +If you have separate config files, use `mergeConfig`: + +```ts +// vitest.config.ts +import { defineConfig, mergeConfig } from 'vitest/config' +import viteConfig from './vite.config' + +export default mergeConfig(viteConfig, defineConfig({ + test: { + environment: 'jsdom', + }, +})) +``` + +## Common Options + +```ts +defineConfig({ + test: { + // Enable global APIs (describe, it, expect) without imports + globals: true, + + // Test environment: 'node', 'jsdom', 'happy-dom' + environment: 'node', + + // Setup files to run before each test file + setupFiles: ['./tests/setup.ts'], + + // Include patterns for test files + include: ['**/*.{test,spec}.{js,ts,jsx,tsx}'], + + // Exclude patterns + exclude: ['**/node_modules/**', '**/dist/**'], + + // Test timeout in ms + testTimeout: 5000, + + // Hook timeout in ms + hookTimeout: 10000, + + // Enable watch mode by default + watch: true, + + // Coverage configuration + coverage: { + provider: 'v8', // or 'istanbul' + reporter: ['text', 'html'], + include: ['src/**/*.ts'], + }, + + // Run tests in isolation (each file in separate process) + isolate: true, + + // Pool for running tests: 'threads', 'forks', 'vmThreads' + pool: 'threads', + + // Number of threads/processes + poolOptions: { + threads: { + maxThreads: 4, + minThreads: 1, + }, + }, + + // Automatically clear mocks between tests + clearMocks: true, + + // Restore mocks between tests + restoreMocks: true, + + // Retry failed tests + retry: 0, + + // Stop after first failure + bail: 0, + }, +}) +``` + +## Conditional Configuration + +Use `mode` or `process.env.VITEST` for test-specific config: + +```ts +export default defineConfig(({ mode }) => ({ + plugins: mode === 'test' ? [] : [myPlugin()], + test: { + // test options + }, +})) +``` + +## Projects (Monorepos) + +Run different configurations in the same Vitest process: + +```ts +defineConfig({ + test: { + projects: [ + 'packages/*', + { + test: { + name: 'unit', + include: ['tests/unit/**/*.test.ts'], + environment: 'node', + }, + }, + { + test: { + name: 'integration', + include: ['tests/integration/**/*.test.ts'], + environment: 'jsdom', + }, + }, + ], + }, +}) +``` + +## Key Points + +- Vitest uses Vite's transformation pipeline - same `resolve.alias`, plugins work +- `vitest.config.ts` takes priority over `vite.config.ts` +- Use `--config` flag to specify a custom config path +- `process.env.VITEST` is set to `true` when running tests +- Test config uses `test` property, rest is Vite config + + diff --git a/.agents/skills/vitest/references/core-describe.md b/.agents/skills/vitest/references/core-describe.md new file mode 100644 index 0000000..3f7f3fe --- /dev/null +++ b/.agents/skills/vitest/references/core-describe.md @@ -0,0 +1,193 @@ +--- +name: describe-api +description: describe/suite for grouping tests into logical blocks +--- + +# Describe API + +Group related tests into suites for organization and shared setup. + +## Basic Usage + +```ts +import { describe, expect, test } from 'vitest' + +describe('Math', () => { + test('adds numbers', () => { + expect(1 + 1).toBe(2) + }) + + test('subtracts numbers', () => { + expect(3 - 1).toBe(2) + }) +}) + +// Alias: suite +import { suite } from 'vitest' +suite('equivalent to describe', () => {}) +``` + +## Nested Suites + +```ts +describe('User', () => { + describe('when logged in', () => { + test('shows dashboard', () => {}) + test('can update profile', () => {}) + }) + + describe('when logged out', () => { + test('shows login page', () => {}) + }) +}) +``` + +## Suite Options + +```ts +// All tests inherit options +describe('slow tests', { timeout: 30_000 }, () => { + test('test 1', () => {}) // 30s timeout + test('test 2', () => {}) // 30s timeout +}) +``` + +## Suite Modifiers + +### Skip Suites + +```ts +describe.skip('skipped suite', () => { + test('wont run', () => {}) +}) + +// Conditional +describe.skipIf(process.env.CI)('not in CI', () => {}) +describe.runIf(!process.env.CI)('only local', () => {}) +``` + +### Focus Suites + +```ts +describe.only('only this suite runs', () => { + test('runs', () => {}) +}) +``` + +### Todo Suites + +```ts +describe.todo('implement later') +``` + +### Concurrent Suites + +```ts +// All tests run in parallel +describe.concurrent('parallel tests', () => { + test('test 1', async ({ expect }) => {}) + test('test 2', async ({ expect }) => {}) +}) +``` + +### Sequential in Concurrent + +```ts +describe.concurrent('parallel', () => { + test('concurrent 1', async () => {}) + + describe.sequential('must be sequential', () => { + test('step 1', async () => {}) + test('step 2', async () => {}) + }) +}) +``` + +### Shuffle Tests + +```ts +describe.shuffle('random order', () => { + test('test 1', () => {}) + test('test 2', () => {}) + test('test 3', () => {}) +}) + +// Or with option +describe('random', { shuffle: true }, () => {}) +``` + +## Parameterized Suites + +### describe.each + +```ts +describe.each([ + { name: 'Chrome', version: 100 }, + { name: 'Firefox', version: 90 }, +])('$name browser', ({ name, version }) => { + test('has version', () => { + expect(version).toBeGreaterThan(0) + }) +}) +``` + +### describe.for + +```ts +describe.for([ + ['Chrome', 100], + ['Firefox', 90], +])('%s browser', ([name, version]) => { + test('has version', () => { + expect(version).toBeGreaterThan(0) + }) +}) +``` + +## Hooks in Suites + +```ts +describe('Database', () => { + let db + + beforeAll(async () => { + db = await createDb() + }) + + afterAll(async () => { + await db.close() + }) + + beforeEach(async () => { + await db.clear() + }) + + test('insert works', async () => { + await db.insert({ name: 'test' }) + expect(await db.count()).toBe(1) + }) +}) +``` + +## Modifier Combinations + +All modifiers can be chained: + +```ts +describe.skip.concurrent('skipped concurrent', () => {}) +describe.only.shuffle('only and shuffled', () => {}) +describe.concurrent.skip('equivalent', () => {}) +``` + +## Key Points + +- Top-level tests belong to an implicit file suite +- Nested suites inherit parent's options (timeout, retry, etc.) +- Hooks are scoped to their suite and nested suites +- Use `describe.concurrent` with context's `expect` for snapshots +- Shuffle order depends on `sequence.seed` config + + diff --git a/.agents/skills/vitest/references/core-expect.md b/.agents/skills/vitest/references/core-expect.md new file mode 100644 index 0000000..91de00a --- /dev/null +++ b/.agents/skills/vitest/references/core-expect.md @@ -0,0 +1,219 @@ +--- +name: expect-api +description: Assertions with matchers, asymmetric matchers, and custom matchers +--- + +# Expect API + +Vitest uses Chai assertions with Jest-compatible API. + +## Basic Assertions + +```ts +import { expect, test } from 'vitest' + +test('assertions', () => { + // Equality + expect(1 + 1).toBe(2) // Strict equality (===) + expect({ a: 1 }).toEqual({ a: 1 }) // Deep equality + + // Truthiness + expect(true).toBeTruthy() + expect(false).toBeFalsy() + expect(null).toBeNull() + expect(undefined).toBeUndefined() + expect('value').toBeDefined() + + // Numbers + expect(10).toBeGreaterThan(5) + expect(10).toBeGreaterThanOrEqual(10) + expect(5).toBeLessThan(10) + expect(0.1 + 0.2).toBeCloseTo(0.3, 5) + + // Strings + expect('hello world').toMatch(/world/) + expect('hello').toContain('ell') + + // Arrays + expect([1, 2, 3]).toContain(2) + expect([{ a: 1 }]).toContainEqual({ a: 1 }) + expect([1, 2, 3]).toHaveLength(3) + + // Objects + expect({ a: 1, b: 2 }).toHaveProperty('a') + expect({ a: 1, b: 2 }).toHaveProperty('a', 1) + expect({ a: { b: 1 } }).toHaveProperty('a.b', 1) + expect({ a: 1 }).toMatchObject({ a: 1 }) + + // Types + expect('string').toBeTypeOf('string') + expect(new Date()).toBeInstanceOf(Date) +}) +``` + +## Negation + +```ts +expect(1).not.toBe(2) +expect({ a: 1 }).not.toEqual({ a: 2 }) +``` + +## Error Assertions + +```ts +// Sync errors - wrap in function +expect(() => throwError()).toThrow() +expect(() => throwError()).toThrow('message') +expect(() => throwError()).toThrow(/pattern/) +expect(() => throwError()).toThrow(CustomError) + +// Async errors - use rejects +await expect(asyncThrow()).rejects.toThrow('error') +``` + +## Promise Assertions + +```ts +// Resolves +await expect(Promise.resolve(1)).resolves.toBe(1) +await expect(fetchData()).resolves.toEqual({ data: true }) + +// Rejects +await expect(Promise.reject('error')).rejects.toBe('error') +await expect(failingFetch()).rejects.toThrow() +``` + +## Spy/Mock Assertions + +```ts +const fn = vi.fn() +fn('arg1', 'arg2') +fn('arg3') + +expect(fn).toHaveBeenCalled() +expect(fn).toHaveBeenCalledTimes(2) +expect(fn).toHaveBeenCalledWith('arg1', 'arg2') +expect(fn).toHaveBeenLastCalledWith('arg3') +expect(fn).toHaveBeenNthCalledWith(1, 'arg1', 'arg2') + +expect(fn).toHaveReturned() +expect(fn).toHaveReturnedWith(value) +``` + +## Asymmetric Matchers + +Use inside `toEqual`, `toHaveBeenCalledWith`, etc: + +```ts +expect({ id: 1, name: 'test' }).toEqual({ + id: expect.any(Number), + name: expect.any(String), +}) + +expect({ a: 1, b: 2, c: 3 }).toEqual( + expect.objectContaining({ a: 1 }) +) + +expect([1, 2, 3, 4]).toEqual( + expect.arrayContaining([1, 3]) +) + +expect('hello world').toEqual( + expect.stringContaining('world') +) + +expect('hello world').toEqual( + expect.stringMatching(/world$/) +) + +expect({ value: null }).toEqual({ + value: expect.anything() // Matches anything except null/undefined +}) + +// Negate with expect.not +expect([1, 2]).toEqual( + expect.not.arrayContaining([3]) +) +``` + +## Soft Assertions + +Continue test after failure: + +```ts +expect.soft(1).toBe(2) // Marks test failed but continues +expect.soft(2).toBe(3) // Also runs +// All failures reported at end +``` + +## Poll Assertions + +Retry until passes: + +```ts +await expect.poll(() => fetchStatus()).toBe('ready') + +await expect.poll( + () => document.querySelector('.element'), + { interval: 100, timeout: 5000 } +).toBeTruthy() +``` + +## Assertion Count + +```ts +test('async assertions', async () => { + expect.assertions(2) // Exactly 2 assertions must run + + await doAsync((data) => { + expect(data).toBeDefined() + expect(data.id).toBe(1) + }) +}) + +test('at least one', () => { + expect.hasAssertions() // At least 1 assertion must run +}) +``` + +## Extending Matchers + +```ts +expect.extend({ + toBeWithinRange(received, floor, ceiling) { + const pass = received >= floor && received <= ceiling + return { + pass, + message: () => + `expected ${received} to be within range ${floor} - ${ceiling}`, + } + }, +}) + +test('custom matcher', () => { + expect(100).toBeWithinRange(90, 110) +}) +``` + +## Snapshot Assertions + +```ts +expect(data).toMatchSnapshot() +expect(data).toMatchInlineSnapshot(`{ "id": 1 }`) +await expect(result).toMatchFileSnapshot('./expected.json') + +expect(() => throw new Error('fail')).toThrowErrorMatchingSnapshot() +``` + +## Key Points + +- Use `toBe` for primitives, `toEqual` for objects/arrays +- `toStrictEqual` checks undefined properties and array sparseness +- Always `await` async assertions (`resolves`, `rejects`, `poll`) +- Use context's `expect` in concurrent tests for correct tracking +- `toThrow` requires wrapping sync code in a function + + diff --git a/.agents/skills/vitest/references/core-hooks.md b/.agents/skills/vitest/references/core-hooks.md new file mode 100644 index 0000000..d0c2bfa --- /dev/null +++ b/.agents/skills/vitest/references/core-hooks.md @@ -0,0 +1,244 @@ +--- +name: lifecycle-hooks +description: beforeEach, afterEach, beforeAll, afterAll, and around hooks +--- + +# Lifecycle Hooks + +## Basic Hooks + +```ts +import { afterAll, afterEach, beforeAll, beforeEach, test } from 'vitest' + +beforeAll(async () => { + // Runs once before all tests in file/suite + await setupDatabase() +}) + +afterAll(async () => { + // Runs once after all tests in file/suite + await teardownDatabase() +}) + +beforeEach(async () => { + // Runs before each test + await clearTestData() +}) + +afterEach(async () => { + // Runs after each test + await cleanupMocks() +}) +``` + +## Cleanup Return Pattern + +Return cleanup function from `before*` hooks: + +```ts +beforeAll(async () => { + const server = await startServer() + + // Returned function runs as afterAll + return async () => { + await server.close() + } +}) + +beforeEach(async () => { + const connection = await connect() + + // Runs as afterEach + return () => connection.close() +}) +``` + +## Scoped Hooks + +Hooks apply to current suite and nested suites: + +```ts +describe('outer', () => { + beforeEach(() => console.log('outer before')) + + test('test 1', () => {}) // outer before → test + + describe('inner', () => { + beforeEach(() => console.log('inner before')) + + test('test 2', () => {}) // outer before → inner before → test + }) +}) +``` + +## Hook Timeout + +```ts +beforeAll(async () => { + await slowSetup() +}, 30_000) // 30 second timeout +``` + +## Around Hooks + +Wrap tests with setup/teardown context: + +```ts +import { aroundEach, test } from 'vitest' + +// Wrap each test in database transaction +aroundEach(async (runTest) => { + await db.beginTransaction() + await runTest() // Must be called! + await db.rollback() +}) + +test('insert user', async () => { + await db.insert({ name: 'Alice' }) + // Automatically rolled back after test +}) +``` + +### aroundAll + +Wrap entire suite: + +```ts +import { aroundAll, test } from 'vitest' + +aroundAll(async (runSuite) => { + console.log('before all tests') + await runSuite() // Must be called! + console.log('after all tests') +}) +``` + +### Multiple Around Hooks + +Nested like onion layers: + +```ts +aroundEach(async (runTest) => { + console.log('outer before') + await runTest() + console.log('outer after') +}) + +aroundEach(async (runTest) => { + console.log('inner before') + await runTest() + console.log('inner after') +}) + +// Order: outer before → inner before → test → inner after → outer after +``` + +## Test Hooks + +Inside test body: + +```ts +import { onTestFailed, onTestFinished, test } from 'vitest' + +test('with cleanup', () => { + const db = connect() + + // Runs after test finishes (pass or fail) + onTestFinished(() => db.close()) + + // Only runs if test fails + onTestFailed(({ task }) => { + console.log('Failed:', task.result?.errors) + }) + + db.query('SELECT * FROM users') +}) +``` + +### Reusable Cleanup Pattern + +```ts +function useTestDb() { + const db = connect() + onTestFinished(() => db.close()) + return db +} + +test('query users', () => { + const db = useTestDb() + expect(db.query('SELECT * FROM users')).toBeDefined() +}) + +test('query orders', () => { + const db = useTestDb() // Fresh connection, auto-closed + expect(db.query('SELECT * FROM orders')).toBeDefined() +}) +``` + +## Concurrent Test Hooks + +For concurrent tests, use context's hooks: + +```ts +test.concurrent('concurrent', ({ onTestFinished }) => { + const resource = allocate() + onTestFinished(() => resource.release()) +}) +``` + +## Extended Test Hooks + +With `test.extend`, hooks are type-aware: + +```ts +const test = base.extend<{ db: Database }>({ + db: async ({}, use) => { + const db = await createDb() + await use(db) + await db.close() + }, +}) + +// These hooks know about `db` fixture +test.beforeEach(({ db }) => { + db.seed() +}) + +test.afterEach(({ db }) => { + db.clear() +}) +``` + +## Hook Execution Order + +Default order (stack): +1. `beforeAll` (in order) +2. `beforeEach` (in order) +3. Test +4. `afterEach` (reverse order) +5. `afterAll` (reverse order) + +Configure with `sequence.hooks`: + +```ts +defineConfig({ + test: { + sequence: { + hooks: 'list', // 'stack' (default), 'list', 'parallel' + }, + }, +}) +``` + +## Key Points + +- Hooks are not called during type checking +- Return cleanup function from `before*` to avoid `after*` duplication +- `aroundEach`/`aroundAll` must call `runTest()`/`runSuite()` +- `onTestFinished` always runs, even if test fails +- Use context hooks for concurrent tests + + diff --git a/.agents/skills/vitest/references/core-test-api.md b/.agents/skills/vitest/references/core-test-api.md new file mode 100644 index 0000000..1f3c932 --- /dev/null +++ b/.agents/skills/vitest/references/core-test-api.md @@ -0,0 +1,233 @@ +--- +name: test-api +description: test/it function for defining tests with modifiers +--- + +# Test API + +## Basic Test + +```ts +import { expect, test } from 'vitest' + +test('adds numbers', () => { + expect(1 + 1).toBe(2) +}) + +// Alias: it +import { it } from 'vitest' + +it('works the same', () => { + expect(true).toBe(true) +}) +``` + +## Async Tests + +```ts +test('async test', async () => { + const result = await fetchData() + expect(result).toBeDefined() +}) + +// Promises are automatically awaited +test('returns promise', () => { + return fetchData().then(result => { + expect(result).toBeDefined() + }) +}) +``` + +## Test Options + +```ts +// Timeout (default: 5000ms) +test('slow test', async () => { + // ... +}, 10_000) + +// Or with options object +test('with options', { timeout: 10_000, retry: 2 }, async () => { + // ... +}) +``` + +## Test Modifiers + +### Skip Tests + +```ts +test.skip('skipped test', () => { + // Won't run +}) + +// Conditional skip +test.skipIf(process.env.CI)('not in CI', () => {}) +test.runIf(process.env.CI)('only in CI', () => {}) + +// Dynamic skip via context +test('dynamic skip', ({ skip }) => { + skip(someCondition, 'reason') + // ... +}) +``` + +### Focus Tests + +```ts +test.only('only this runs', () => { + // Other tests in file are skipped +}) +``` + +### Todo Tests + +```ts +test.todo('implement later') + +test.todo('with body', () => { + // Not run, shows in report +}) +``` + +### Failing Tests + +```ts +test.fails('expected to fail', () => { + expect(1).toBe(2) // Test passes because assertion fails +}) +``` + +### Concurrent Tests + +```ts +// Run tests in parallel +test.concurrent('test 1', async ({ expect }) => { + // Use context.expect for concurrent tests + expect(await fetch1()).toBe('result') +}) + +test.concurrent('test 2', async ({ expect }) => { + expect(await fetch2()).toBe('result') +}) +``` + +### Sequential Tests + +```ts +// Force sequential in concurrent context +test.sequential('must run alone', async () => {}) +``` + +## Parameterized Tests + +### test.each + +```ts +test.each([ + [1, 1, 2], + [1, 2, 3], + [2, 1, 3], +])('add(%i, %i) = %i', (a, b, expected) => { + expect(a + b).toBe(expected) +}) + +// With objects +test.each([ + { a: 1, b: 1, expected: 2 }, + { a: 1, b: 2, expected: 3 }, +])('add($a, $b) = $expected', ({ a, b, expected }) => { + expect(a + b).toBe(expected) +}) + +// Template literal +test.each` + a | b | expected + ${1} | ${1} | ${2} + ${1} | ${2} | ${3} +`('add($a, $b) = $expected', ({ a, b, expected }) => { + expect(a + b).toBe(expected) +}) +``` + +### test.for + +Preferred over `.each` - doesn't spread arrays: + +```ts +test.for([ + [1, 1, 2], + [1, 2, 3], +])('add(%i, %i) = %i', ([a, b, expected], { expect }) => { + // Second arg is TestContext + expect(a + b).toBe(expected) +}) +``` + +## Test Context + +First argument provides context utilities: + +```ts +test('with context', ({ expect, skip, task }) => { + console.log(task.name) // Test name + skip(someCondition) // Skip dynamically + expect(1).toBe(1) // Context-bound expect +}) +``` + +## Custom Test with Fixtures + +```ts +import { test as base } from 'vitest' + +const test = base.extend({ + db: async ({}, use) => { + const db = await createDb() + await use(db) + await db.close() + }, +}) + +test('query', async ({ db }) => { + const users = await db.query('SELECT * FROM users') + expect(users).toBeDefined() +}) +``` + +## Retry Configuration + +```ts +test('flaky test', { retry: 3 }, async () => { + // Retries up to 3 times on failure +}) + +// Advanced retry options +test('with delay', { + retry: { + count: 3, + delay: 1000, + condition: /timeout/i, // Only retry on timeout errors + }, +}, async () => {}) +``` + +## Tags + +```ts +test('database test', { tags: ['db', 'slow'] }, async () => {}) + +// Run with: vitest --tags db +``` + +## Key Points + +- Tests with no body are marked as `todo` +- `test.only` throws in CI unless `allowOnly: true` +- Use context's `expect` for concurrent tests and snapshots +- Function name is used as test name if passed as first arg + + diff --git a/.agents/skills/vitest/references/features-concurrency.md b/.agents/skills/vitest/references/features-concurrency.md new file mode 100644 index 0000000..412f60d --- /dev/null +++ b/.agents/skills/vitest/references/features-concurrency.md @@ -0,0 +1,250 @@ +--- +name: concurrency-parallelism +description: Concurrent tests, parallel execution, and sharding +--- + +# Concurrency & Parallelism + +## File Parallelism + +By default, Vitest runs test files in parallel across workers: + +```ts +defineConfig({ + test: { + // Run files in parallel (default: true) + fileParallelism: true, + + // Number of worker threads + maxWorkers: 4, + minWorkers: 1, + + // Pool type: 'threads', 'forks', 'vmThreads' + pool: 'threads', + }, +}) +``` + +## Concurrent Tests + +Run tests within a file in parallel: + +```ts +// Individual concurrent tests +test.concurrent('test 1', async ({ expect }) => { + expect(await fetch1()).toBe('result') +}) + +test.concurrent('test 2', async ({ expect }) => { + expect(await fetch2()).toBe('result') +}) + +// All tests in suite concurrent +describe.concurrent('parallel suite', () => { + test('test 1', async ({ expect }) => {}) + test('test 2', async ({ expect }) => {}) +}) +``` + +**Important:** Use `{ expect }` from context for concurrent tests. + +## Sequential in Concurrent Context + +Force sequential execution: + +```ts +describe.concurrent('mostly parallel', () => { + test('parallel 1', async () => {}) + test('parallel 2', async () => {}) + + test.sequential('must run alone 1', async () => {}) + test.sequential('must run alone 2', async () => {}) +}) + +// Or entire suite +describe.sequential('sequential suite', () => { + test('first', () => {}) + test('second', () => {}) +}) +``` + +## Max Concurrency + +Limit concurrent tests: + +```ts +defineConfig({ + test: { + maxConcurrency: 5, // Max concurrent tests per file + }, +}) +``` + +## Isolation + +Each file runs in isolated environment by default: + +```ts +defineConfig({ + test: { + // Disable isolation for faster runs (less safe) + isolate: false, + }, +}) +``` + +## Sharding + +Split tests across machines: + +```bash +# Machine 1 +vitest run --shard=1/3 + +# Machine 2 +vitest run --shard=2/3 + +# Machine 3 +vitest run --shard=3/3 +``` + +### CI Example (GitHub Actions) + +```yaml +jobs: + test: + strategy: + matrix: + shard: [1, 2, 3] + steps: + - run: vitest run --shard=${{ matrix.shard }}/3 --reporter=blob + + merge: + needs: test + steps: + - run: vitest --merge-reports --reporter=junit +``` + +### Merge Reports + +```bash +# Each shard outputs blob +vitest run --shard=1/3 --reporter=blob --coverage +vitest run --shard=2/3 --reporter=blob --coverage + +# Merge all blobs +vitest --merge-reports --reporter=json --coverage +``` + +## Test Sequence + +Control test order: + +```ts +defineConfig({ + test: { + sequence: { + // Run tests in random order + shuffle: true, + + // Seed for reproducible shuffle + seed: 12345, + + // Hook execution order + hooks: 'stack', // 'stack', 'list', 'parallel' + + // All tests concurrent by default + concurrent: true, + }, + }, +}) +``` + +## Shuffle Tests + +Randomize to catch hidden dependencies: + +```ts +// Via CLI +vitest --sequence.shuffle + +// Per suite +describe.shuffle('random order', () => { + test('test 1', () => {}) + test('test 2', () => {}) + test('test 3', () => {}) +}) +``` + +## Pool Options + +### Threads (Default) + +```ts +defineConfig({ + test: { + pool: 'threads', + poolOptions: { + threads: { + maxThreads: 8, + minThreads: 2, + isolate: true, + }, + }, + }, +}) +``` + +### Forks + +Better isolation, slower: + +```ts +defineConfig({ + test: { + pool: 'forks', + poolOptions: { + forks: { + maxForks: 4, + isolate: true, + }, + }, + }, +}) +``` + +### VM Threads + +Full VM isolation per file: + +```ts +defineConfig({ + test: { + pool: 'vmThreads', + }, +}) +``` + +## Bail on Failure + +Stop after first failure: + +```bash +vitest --bail 1 # Stop after 1 failure +vitest --bail # Stop on first failure (same as --bail 1) +``` + +## Key Points + +- Files run in parallel by default +- Use `.concurrent` for parallel tests within file +- Always use context's `expect` in concurrent tests +- Sharding splits tests across CI machines +- Use `--merge-reports` to combine sharded results +- Shuffle tests to find hidden dependencies + + diff --git a/.agents/skills/vitest/references/features-context.md b/.agents/skills/vitest/references/features-context.md new file mode 100644 index 0000000..a9db0a1 --- /dev/null +++ b/.agents/skills/vitest/references/features-context.md @@ -0,0 +1,238 @@ +--- +name: test-context-fixtures +description: Test context, custom fixtures with test.extend +--- + +# Test Context & Fixtures + +## Built-in Context + +Every test receives context as first argument: + +```ts +test('context', ({ task, expect, skip }) => { + console.log(task.name) // Test name + expect(1).toBe(1) // Context-bound expect + skip() // Skip test dynamically +}) +``` + +### Context Properties + +- `task` - Test metadata (name, file, etc.) +- `expect` - Expect bound to this test (important for concurrent tests) +- `skip(condition?, message?)` - Skip the test +- `onTestFinished(fn)` - Cleanup after test +- `onTestFailed(fn)` - Run on failure only + +## Custom Fixtures with test.extend + +Create reusable test utilities: + +```ts +import { test as base } from 'vitest' + +// Define fixture types +interface Fixtures { + db: Database + user: User +} + +// Create extended test +export const test = base.extend({ + // Fixture with setup/teardown + db: async ({}, use) => { + const db = await createDatabase() + await use(db) // Provide to test + await db.close() // Cleanup + }, + + // Fixture depending on another fixture + user: async ({ db }, use) => { + const user = await db.createUser({ name: 'Test' }) + await use(user) + await db.deleteUser(user.id) + }, +}) +``` + +Using fixtures: + +```ts +test('query user', async ({ db, user }) => { + const found = await db.findUser(user.id) + expect(found).toEqual(user) +}) +``` + +## Fixture Initialization + +Fixtures only initialize when accessed: + +```ts +const test = base.extend({ + expensive: async ({}, use) => { + console.log('initializing') // Only runs if test uses it + await use('value') + }, +}) + +test('no fixture', () => {}) // expensive not called +test('uses fixture', ({ expensive }) => {}) // expensive called +``` + +## Auto Fixtures + +Run fixture for every test: + +```ts +const test = base.extend({ + setup: [ + async ({}, use) => { + await globalSetup() + await use() + await globalTeardown() + }, + { auto: true } // Always run + ], +}) +``` + +## Scoped Fixtures + +### File Scope + +Initialize once per file: + +```ts +const test = base.extend({ + connection: [ + async ({}, use) => { + const conn = await connect() + await use(conn) + await conn.close() + }, + { scope: 'file' } + ], +}) +``` + +### Worker Scope + +Initialize once per worker: + +```ts +const test = base.extend({ + sharedResource: [ + async ({}, use) => { + await use(globalResource) + }, + { scope: 'worker' } + ], +}) +``` + +## Injected Fixtures (from Config) + +Override fixtures per project: + +```ts +// test file +const test = base.extend({ + apiUrl: ['/default', { injected: true }], +}) + +// vitest.config.ts +defineConfig({ + test: { + projects: [ + { + test: { + name: 'prod', + provide: { apiUrl: 'https://api.prod.com' }, + }, + }, + ], + }, +}) +``` + +## Scoped Values per Suite + +Override fixture for specific suite: + +```ts +const test = base.extend({ + environment: 'development', +}) + +describe('production tests', () => { + test.scoped({ environment: 'production' }) + + test('uses production', ({ environment }) => { + expect(environment).toBe('production') + }) +}) + +test('uses default', ({ environment }) => { + expect(environment).toBe('development') +}) +``` + +## Extended Test Hooks + +Type-aware hooks with fixtures: + +```ts +const test = base.extend<{ db: Database }>({ + db: async ({}, use) => { + const db = await createDb() + await use(db) + await db.close() + }, +}) + +// Hooks know about fixtures +test.beforeEach(({ db }) => { + db.seed() +}) + +test.afterEach(({ db }) => { + db.clear() +}) +``` + +## Composing Fixtures + +Extend from another extended test: + +```ts +// base-test.ts +export const test = base.extend<{ db: Database }>({ + db: async ({}, use) => { /* ... */ }, +}) + +// admin-test.ts +import { test as dbTest } from './base-test' + +export const test = dbTest.extend<{ admin: User }>({ + admin: async ({ db }, use) => { + const admin = await db.createAdmin() + await use(admin) + }, +}) +``` + +## Key Points + +- Use `{ }` destructuring to access fixtures +- Fixtures are lazy - only initialize when accessed +- Return cleanup function from fixtures +- Use `{ auto: true }` for setup fixtures +- Use `{ scope: 'file' }` for expensive shared resources +- Fixtures compose - extend from extended tests + + diff --git a/.agents/skills/vitest/references/features-coverage.md b/.agents/skills/vitest/references/features-coverage.md new file mode 100644 index 0000000..aaf44cf --- /dev/null +++ b/.agents/skills/vitest/references/features-coverage.md @@ -0,0 +1,207 @@ +--- +name: code-coverage +description: Code coverage with V8 or Istanbul providers +--- + +# Code Coverage + +## Setup + +```bash +# Run tests with coverage +vitest run --coverage +``` + +## Configuration + +```ts +// vitest.config.ts +defineConfig({ + test: { + coverage: { + // Provider: 'v8' (default, faster) or 'istanbul' (more compatible) + provider: 'v8', + + // Enable coverage + enabled: true, + + // Reporters + reporter: ['text', 'json', 'html'], + + // Files to include + include: ['src/**/*.{ts,tsx}'], + + // Files to exclude + exclude: [ + 'node_modules/', + 'tests/', + '**/*.d.ts', + '**/*.test.ts', + ], + + // Report uncovered files + all: true, + + // Thresholds + thresholds: { + lines: 80, + functions: 80, + branches: 80, + statements: 80, + }, + }, + }, +}) +``` + +## Providers + +### V8 (Default) + +```bash +npm i -D @vitest/coverage-v8 +``` + +- Faster, no pre-instrumentation +- Uses V8's native coverage +- Recommended for most projects + +### Istanbul + +```bash +npm i -D @vitest/coverage-istanbul +``` + +- Pre-instruments code +- Works in any JS runtime +- More overhead but widely compatible + +## Reporters + +```ts +coverage: { + reporter: [ + 'text', // Terminal output + 'text-summary', // Summary only + 'json', // JSON file + 'html', // HTML report + 'lcov', // For CI tools + 'cobertura', // XML format + ], + reportsDirectory: './coverage', +} +``` + +## Thresholds + +Fail tests if coverage is below threshold: + +```ts +coverage: { + thresholds: { + // Global thresholds + lines: 80, + functions: 75, + branches: 70, + statements: 80, + + // Per-file thresholds + perFile: true, + + // Auto-update thresholds (for gradual improvement) + autoUpdate: true, + }, +} +``` + +## Ignoring Code + +### V8 + +```ts +/* v8 ignore next -- @preserve */ +function ignored() { + return 'not covered' +} + +/* v8 ignore start -- @preserve */ +// All code here ignored +/* v8 ignore stop -- @preserve */ +``` + +### Istanbul + +```ts +/* istanbul ignore next -- @preserve */ +function ignored() {} + +/* istanbul ignore if -- @preserve */ +if (condition) { + // ignored +} +``` + +Note: `@preserve` keeps comments through esbuild. + +## Package.json Scripts + +```json +{ + "scripts": { + "test": "vitest", + "test:coverage": "vitest run --coverage", + "test:coverage:watch": "vitest --coverage" + } +} +``` + +## Vitest UI Coverage + +Enable HTML coverage in Vitest UI: + +```ts +coverage: { + enabled: true, + reporter: ['text', 'html'], +} +``` + +Run with `vitest --ui` to view coverage visually. + +## CI Integration + +```yaml +# GitHub Actions +- name: Run tests with coverage + run: npm run test:coverage + +- name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + files: ./coverage/lcov.info +``` + +## Coverage with Sharding + +Merge coverage from sharded runs: + +```bash +vitest run --shard=1/3 --coverage --reporter=blob +vitest run --shard=2/3 --coverage --reporter=blob +vitest run --shard=3/3 --coverage --reporter=blob + +vitest --merge-reports --coverage --reporter=json +``` + +## Key Points + +- V8 is faster, Istanbul is more compatible +- Use `--coverage` flag or `coverage.enabled: true` +- Include `all: true` to see uncovered files +- Set thresholds to enforce minimum coverage +- Use `@preserve` comment to keep ignore hints + + diff --git a/.agents/skills/vitest/references/features-filtering.md b/.agents/skills/vitest/references/features-filtering.md new file mode 100644 index 0000000..24a41cb --- /dev/null +++ b/.agents/skills/vitest/references/features-filtering.md @@ -0,0 +1,211 @@ +--- +name: test-filtering +description: Filter tests by name, file patterns, and tags +--- + +# Test Filtering + +## CLI Filtering + +### By File Path + +```bash +# Run files containing "user" +vitest user + +# Multiple patterns +vitest user auth + +# Specific file +vitest src/user.test.ts + +# By line number +vitest src/user.test.ts:25 +``` + +### By Test Name + +```bash +# Tests matching pattern +vitest -t "login" +vitest --testNamePattern "should.*work" + +# Regex patterns +vitest -t "/user|auth/" +``` + +## Changed Files + +```bash +# Uncommitted changes +vitest --changed + +# Since specific commit +vitest --changed HEAD~1 +vitest --changed abc123 + +# Since branch +vitest --changed origin/main +``` + +## Related Files + +Run tests that import specific files: + +```bash +vitest related src/utils.ts src/api.ts --run +``` + +Useful with lint-staged: + +```js +// .lintstagedrc.js +export default { + '*.{ts,tsx}': 'vitest related --run', +} +``` + +## Focus Tests (.only) + +```ts +test.only('only this runs', () => {}) + +describe.only('only this suite', () => { + test('runs', () => {}) +}) +``` + +In CI, `.only` throws error unless configured: + +```ts +defineConfig({ + test: { + allowOnly: true, // Allow .only in CI + }, +}) +``` + +## Skip Tests + +```ts +test.skip('skipped', () => {}) + +// Conditional +test.skipIf(process.env.CI)('not in CI', () => {}) +test.runIf(!process.env.CI)('local only', () => {}) + +// Dynamic skip +test('dynamic', ({ skip }) => { + skip(someCondition, 'reason') +}) +``` + +## Tags + +Filter by custom tags: + +```ts +test('database test', { tags: ['db'] }, () => {}) +test('slow test', { tags: ['slow', 'integration'] }, () => {}) +``` + +Run tagged tests: + +```bash +vitest --tags db +vitest --tags "db,slow" # OR +vitest --tags db --tags slow # OR +``` + +Configure allowed tags: + +```ts +defineConfig({ + test: { + tags: ['db', 'slow', 'integration'], + strictTags: true, // Fail on unknown tags + }, +}) +``` + +## Include/Exclude Patterns + +```ts +defineConfig({ + test: { + // Test file patterns + include: ['**/*.{test,spec}.{ts,tsx}'], + + // Exclude patterns + exclude: [ + '**/node_modules/**', + '**/e2e/**', + '**/*.skip.test.ts', + ], + + // Include source for in-source testing + includeSource: ['src/**/*.ts'], + }, +}) +``` + +## Watch Mode Filtering + +In watch mode, press: +- `p` - Filter by filename pattern +- `t` - Filter by test name pattern +- `a` - Run all tests +- `f` - Run only failed tests + +## Projects Filtering + +Run specific project: + +```bash +vitest --project unit +vitest --project integration --project e2e +``` + +## Environment-based Filtering + +```ts +const isDev = process.env.NODE_ENV === 'development' +const isCI = process.env.CI + +describe.skipIf(isCI)('local only tests', () => {}) +describe.runIf(isDev)('dev tests', () => {}) +``` + +## Combining Filters + +```bash +# File pattern + test name + changed +vitest user -t "login" --changed + +# Related files + run mode +vitest related src/auth.ts --run +``` + +## List Tests Without Running + +```bash +vitest list # Show all test names +vitest list -t "user" # Filter by name +vitest list --filesOnly # Show only file paths +vitest list --json # JSON output +``` + +## Key Points + +- Use `-t` for test name pattern filtering +- `--changed` runs only tests affected by changes +- `--related` runs tests importing specific files +- Tags provide semantic test grouping +- Use `.only` for debugging, but configure CI to reject it +- Watch mode has interactive filtering + + diff --git a/.agents/skills/vitest/references/features-mocking.md b/.agents/skills/vitest/references/features-mocking.md new file mode 100644 index 0000000..e351efe --- /dev/null +++ b/.agents/skills/vitest/references/features-mocking.md @@ -0,0 +1,265 @@ +--- +name: mocking +description: Mock functions, modules, timers, and dates with vi utilities +--- + +# Mocking + +## Mock Functions + +```ts +import { expect, vi } from 'vitest' + +// Create mock function +const fn = vi.fn() +fn('hello') + +expect(fn).toHaveBeenCalled() +expect(fn).toHaveBeenCalledWith('hello') + +// With implementation +const add = vi.fn((a, b) => a + b) +expect(add(1, 2)).toBe(3) + +// Mock return values +fn.mockReturnValue(42) +fn.mockReturnValueOnce(1).mockReturnValueOnce(2) +fn.mockResolvedValue({ data: true }) +fn.mockRejectedValue(new Error('fail')) + +// Mock implementation +fn.mockImplementation((x) => x * 2) +fn.mockImplementationOnce(() => 'first call') +``` + +## Spying on Objects + +```ts +const cart = { + getTotal: () => 100, +} + +const spy = vi.spyOn(cart, 'getTotal') +cart.getTotal() + +expect(spy).toHaveBeenCalled() + +// Mock implementation +spy.mockReturnValue(200) +expect(cart.getTotal()).toBe(200) + +// Restore original +spy.mockRestore() +``` + +## Module Mocking + +```ts +// vi.mock is hoisted to top of file +vi.mock('./api', () => ({ + fetchUser: vi.fn(() => ({ id: 1, name: 'Mock' })), +})) + +import { fetchUser } from './api' + +test('mocked module', () => { + expect(fetchUser()).toEqual({ id: 1, name: 'Mock' }) +}) +``` + +### Partial Mock + +```ts +vi.mock('./utils', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + specificFunction: vi.fn(), + } +}) +``` + +### Auto-mock with Spy + +```ts +// Keep implementation but spy on calls +vi.mock('./calculator', { spy: true }) + +import { add } from './calculator' + +test('spy on module', () => { + const result = add(1, 2) // Real implementation + expect(result).toBe(3) + expect(add).toHaveBeenCalledWith(1, 2) +}) +``` + +### Manual Mocks (__mocks__) + +``` +src/ + __mocks__/ + axios.ts # Mocks 'axios' + api/ + __mocks__/ + client.ts # Mocks './client' + client.ts +``` + +```ts +// Just call vi.mock with no factory +vi.mock('axios') +vi.mock('./api/client') +``` + +## Dynamic Mocking (vi.doMock) + +Not hoisted - use for dynamic imports: + +```ts +test('dynamic mock', async () => { + vi.doMock('./config', () => ({ + apiUrl: 'http://test.local', + })) + + const { apiUrl } = await import('./config') + expect(apiUrl).toBe('http://test.local') + + vi.doUnmock('./config') +}) +``` + +## Mock Timers + +```ts +import { afterEach, beforeEach, vi } from 'vitest' + +beforeEach(() => { + vi.useFakeTimers() +}) + +afterEach(() => { + vi.useRealTimers() +}) + +test('timers', () => { + const fn = vi.fn() + setTimeout(fn, 1000) + + expect(fn).not.toHaveBeenCalled() + + vi.advanceTimersByTime(1000) + expect(fn).toHaveBeenCalled() +}) + +// Other timer methods +vi.runAllTimers() // Run all pending timers +vi.runOnlyPendingTimers() // Run only currently pending +vi.advanceTimersToNextTimer() // Advance to next timer +``` + +### Async Timer Methods + +```ts +test('async timers', async () => { + vi.useFakeTimers() + + let resolved = false + setTimeout(() => Promise.resolve().then(() => { resolved = true }), 100) + + await vi.advanceTimersByTimeAsync(100) + expect(resolved).toBe(true) +}) +``` + +## Mock Dates + +```ts +vi.setSystemTime(new Date('2024-01-01')) +expect(new Date().getFullYear()).toBe(2024) + +vi.useRealTimers() // Restore +``` + +## Mock Globals + +```ts +vi.stubGlobal('fetch', vi.fn(() => + Promise.resolve({ json: () => ({ data: 'mock' }) }) +)) + +// Restore +vi.unstubAllGlobals() +``` + +## Mock Environment Variables + +```ts +vi.stubEnv('API_KEY', 'test-key') +expect(import.meta.env.API_KEY).toBe('test-key') + +// Restore +vi.unstubAllEnvs() +``` + +## Clearing Mocks + +```ts +const fn = vi.fn() +fn() + +fn.mockClear() // Clear call history +fn.mockReset() // Clear history + implementation +fn.mockRestore() // Restore original (for spies) + +// Global +vi.clearAllMocks() +vi.resetAllMocks() +vi.restoreAllMocks() +``` + +## Config Auto-Reset + +```ts +// vitest.config.ts +defineConfig({ + test: { + clearMocks: true, // Clear before each test + mockReset: true, // Reset before each test + restoreMocks: true, // Restore after each test + unstubEnvs: true, // Restore env vars + unstubGlobals: true, // Restore globals + }, +}) +``` + +## Hoisted Variables for Mocks + +```ts +const mockFn = vi.hoisted(() => vi.fn()) + +vi.mock('./module', () => ({ + getData: mockFn, +})) + +import { getData } from './module' + +test('hoisted mock', () => { + mockFn.mockReturnValue('test') + expect(getData()).toBe('test') +}) +``` + +## Key Points + +- `vi.mock` is hoisted - called before imports +- Use `vi.doMock` for dynamic, non-hoisted mocking +- Always restore mocks to avoid test pollution +- Use `{ spy: true }` to keep implementation but track calls +- `vi.hoisted` lets you reference variables in mock factories + + diff --git a/.agents/skills/vitest/references/features-snapshots.md b/.agents/skills/vitest/references/features-snapshots.md new file mode 100644 index 0000000..6868fb1 --- /dev/null +++ b/.agents/skills/vitest/references/features-snapshots.md @@ -0,0 +1,207 @@ +--- +name: snapshot-testing +description: Snapshot testing with file, inline, and file snapshots +--- + +# Snapshot Testing + +Snapshot tests capture output and compare against stored references. + +## Basic Snapshot + +```ts +import { expect, test } from 'vitest' + +test('snapshot', () => { + const result = generateOutput() + expect(result).toMatchSnapshot() +}) +``` + +First run creates `.snap` file: + +```js +// __snapshots__/test.spec.ts.snap +exports['snapshot 1'] = ` +{ + "id": 1, + "name": "test" +} +` +``` + +## Inline Snapshots + +Stored directly in test file: + +```ts +test('inline snapshot', () => { + const data = { foo: 'bar' } + expect(data).toMatchInlineSnapshot() +}) +``` + +Vitest updates the test file: + +```ts +test('inline snapshot', () => { + const data = { foo: 'bar' } + expect(data).toMatchInlineSnapshot(` + { + "foo": "bar", + } + `) +}) +``` + +## File Snapshots + +Compare against explicit file: + +```ts +test('render html', async () => { + const html = renderComponent() + await expect(html).toMatchFileSnapshot('./expected/component.html') +}) +``` + +## Snapshot Hints + +Add descriptive hints: + +```ts +test('multiple snapshots', () => { + expect(header).toMatchSnapshot('header') + expect(body).toMatchSnapshot('body content') + expect(footer).toMatchSnapshot('footer') +}) +``` + +## Object Shape Matching + +Match partial structure: + +```ts +test('shape snapshot', () => { + const data = { + id: Math.random(), + created: new Date(), + name: 'test' + } + + expect(data).toMatchSnapshot({ + id: expect.any(Number), + created: expect.any(Date), + }) +}) +``` + +## Error Snapshots + +```ts +test('error message', () => { + expect(() => { + throw new Error('Something went wrong') + }).toThrowErrorMatchingSnapshot() +}) + +test('inline error', () => { + expect(() => { + throw new Error('Bad input') + }).toThrowErrorMatchingInlineSnapshot(`[Error: Bad input]`) +}) +``` + +## Updating Snapshots + +```bash +# Update all snapshots +vitest -u +vitest --update + +# In watch mode, press 'u' to update failed snapshots +``` + +## Custom Serializers + +Add custom snapshot formatting: + +```ts +expect.addSnapshotSerializer({ + test(val) { + return val && typeof val.toJSON === 'function' + }, + serialize(val, config, indentation, depth, refs, printer) { + return printer(val.toJSON(), config, indentation, depth, refs) + }, +}) +``` + +Or via config: + +```ts +// vitest.config.ts +defineConfig({ + test: { + snapshotSerializers: ['./my-serializer.ts'], + }, +}) +``` + +## Snapshot Format Options + +```ts +defineConfig({ + test: { + snapshotFormat: { + printBasicPrototype: false, // Don't print Array/Object prototypes + escapeString: false, + }, + }, +}) +``` + +## Concurrent Test Snapshots + +Use context's expect: + +```ts +test.concurrent('concurrent 1', async ({ expect }) => { + expect(await getData()).toMatchSnapshot() +}) + +test.concurrent('concurrent 2', async ({ expect }) => { + expect(await getOther()).toMatchSnapshot() +}) +``` + +## Snapshot File Location + +Default: `__snapshots__/.snap` + +Customize: + +```ts +defineConfig({ + test: { + resolveSnapshotPath: (testPath, snapExtension) => { + return testPath.replace('__tests__', '__snapshots__') + snapExtension + }, + }, +}) +``` + +## Key Points + +- Commit snapshot files to version control +- Review snapshot changes in code review +- Use hints for multiple snapshots in one test +- Use `toMatchFileSnapshot` for large outputs (HTML, JSON) +- Inline snapshots auto-update in test file +- Use context's `expect` for concurrent tests + + diff --git a/.agents/skills/vue-best-practices/LICENSE.md b/.agents/skills/vue-best-practices/LICENSE.md new file mode 100644 index 0000000..3f08a54 --- /dev/null +++ b/.agents/skills/vue-best-practices/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 hyf0, SerKo + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/.agents/skills/vue-best-practices/SKILL.md b/.agents/skills/vue-best-practices/SKILL.md new file mode 100644 index 0000000..feacd70 --- /dev/null +++ b/.agents/skills/vue-best-practices/SKILL.md @@ -0,0 +1,154 @@ +--- +name: vue-best-practices +description: MUST be used for Vue.js tasks. Strongly recommends Composition API with ` + + +``` + +## Common Animation Patterns + +### Pulse on Success + +```vue + + + + + +``` + +### Highlight on Change + +```vue + + + + + +``` + +### Bounce Attention + +```vue + + + + + +``` + +## Using animationend Event + +Instead of `setTimeout`, use the `animationend` event for cleaner code: + +```vue + + + +``` + +## Composable for Reusable Animations + +```javascript +// composables/useAnimation.js +import { ref } from 'vue' + +export function useAnimation(duration = 500) { + const isAnimating = ref(false) + + function trigger() { + isAnimating.value = true + setTimeout(() => { + isAnimating.value = false + }, duration) + } + + return { + isAnimating, + trigger + } +} +``` + +```vue + + + +``` diff --git a/.agents/skills/vue-best-practices/references/animation-state-driven-technique.md b/.agents/skills/vue-best-practices/references/animation-state-driven-technique.md new file mode 100644 index 0000000..26b0120 --- /dev/null +++ b/.agents/skills/vue-best-practices/references/animation-state-driven-technique.md @@ -0,0 +1,291 @@ +--- +title: State-driven Animations with CSS Transitions and Style Bindings +impact: LOW +impactDescription: Combining Vue's reactive style bindings with CSS transitions creates smooth, interactive animations +type: best-practice +tags: [vue3, animation, css, transition, style-binding, state, interactive] +--- + +# State-driven Animations with CSS Transitions and Style Bindings + +**Impact: LOW** - For responsive, interactive animations that react to user input or state changes, combine Vue's dynamic style bindings with CSS transitions. This creates smooth animations that interpolate values in real-time based on state. + +## Task List + +- Use `:style` binding for dynamic properties that change frequently +- Add CSS `transition` property to smoothly animate between values +- Consider using `transform` and `opacity` for GPU-accelerated animations +- For complex value interpolation, use watchers with animation libraries + +## Basic Pattern + +```vue + + + + + +``` + +## Common Use Cases + +### Following Mouse Position + +```vue + + + + + +``` + +### Progress Animation + +```vue + + + + + +``` + +### Scroll-based Animation + +```vue + + + + + +``` + +### Color Theme Transition + +```vue + + + + + +``` + +## Advanced: Numerical Tweening with Watchers + +For smooth number animations (counters, stats), use watchers with animation libraries: + +```vue + + + +``` + +## Performance Considerations + +```vue + +``` diff --git a/.agents/skills/vue-best-practices/references/component-async.md b/.agents/skills/vue-best-practices/references/component-async.md new file mode 100644 index 0000000..b39310d --- /dev/null +++ b/.agents/skills/vue-best-practices/references/component-async.md @@ -0,0 +1,97 @@ +--- +title: Async Component Best Practices +impact: MEDIUM +impactDescription: Poor async component strategy can delay interactivity in SSR apps and create loading UI flicker +type: best-practice +tags: [vue3, async-components, ssr, hydration, performance, ux] +--- + +# Async Component Best Practices + +**Impact: MEDIUM** - Async components should reduce JavaScript cost without degrading perceived performance. Focus on hydration timing in SSR and stable loading UX. + +## Task List + +- Use lazy hydration strategies for non-critical SSR component trees +- Import only the hydration helpers you actually use +- Keep `loadingComponent` delay near the default `200ms` unless real UX data suggests otherwise +- Configure `delay` and `timeout` together for predictable loading behavior + +## Use Lazy Hydration Strategies in SSR + +In Vue 3.5+, async components can delay hydration until idle time, visibility, media query match, or user interaction. + +**BAD:** +```vue + +``` + +**GOOD:** +```vue + +``` + +## Prevent Loading Spinner Flicker + +Avoid showing loading UI immediately for components that usually resolve quickly. + +**BAD:** +```vue + +``` + +**GOOD:** +```vue + +``` + +## Delay Guidelines + +| Scenario | Recommended Delay | +|----------|-------------------| +| Small component, fast network | `200ms` | +| Known heavy component | `100ms` | +| Background or non-critical UI | `300-500ms` | diff --git a/.agents/skills/vue-best-practices/references/component-data-flow.md b/.agents/skills/vue-best-practices/references/component-data-flow.md new file mode 100644 index 0000000..e1add1e --- /dev/null +++ b/.agents/skills/vue-best-practices/references/component-data-flow.md @@ -0,0 +1,307 @@ +--- +title: Component Data Flow Best Practices +impact: HIGH +impactDescription: Clear data flow between components prevents state bugs, stale UI, and brittle coupling +type: best-practice +tags: [vue3, props, emits, v-model, provide-inject, data-flow, typescript] +--- + +# Component Data Flow Best Practices + +**Impact: HIGH** - Vue components stay reliable when data flow is explicit: props go down, events go up, `v-model` handles two-way bindings, and provide/inject supports cross-tree dependencies. Blurring these boundaries leads to stale state, hidden coupling, and hard-to-debug UI. + +The main principle of data flow in Vue.js is **Props Down / Events Up**. This is the most maintainable default, and one-way flow scales well. + +## Task List + +- Treat props as read-only inputs +- Use props/emit for component communication; reserve refs for imperative actions +- When refs are required for imperative APIs, type them with template refs +- Emit events instead of mutating parent state directly +- Use `defineModel` for v-model in modern Vue (3.4+) +- Handle v-model modifiers deliberately in child components +- Use symbols for provide/inject keys to avoid props drilling (over ~3 layers) +- Keep mutations in the provider or expose explicit actions +- In TypeScript projects, prefer type-based `defineProps`, `defineEmits`, and `InjectionKey` + +## Props: One-Way Data Down + +Props are inputs. Do not mutate them in the child. + +**BAD:** +```vue + +``` + +**GOOD:** + +If state needs to change, emit an event, use `v-model` or create a local copy. + +## Prefer props/emit over component refs + +**BAD:** +```vue + + + +``` + +**GOOD:** +```vue + + + +``` + +## Type component refs when imperative access is required + +Prefer props/emits by default. When a parent must call an exposed child method, type the ref explicitly and expose only the intended API from the child with `defineExpose`. + +**BAD:** +```vue + + + +``` + +**GOOD:** +```vue + + +``` + +```vue + + + + +``` + +## Emits: Explicit Events Up + +Component events do not bubble. If a parent needs to know about an event, re-emit it explicitly. + +**BAD:** +```vue + + +``` + +**GOOD:** +```vue + + + + +``` + +**Event naming:** use kebab-case in templates and camelCase in script: +```vue + + + +``` + +## `v-model`: Predictable Two-Way Bindings + +Use `defineModel` by default for component bindings and emit updates on input. Only use the `modelValue` + `update:modelValue` pattern if you are on Vue < 3.4. + +**BAD:** +```vue + + + +``` + +**GOOD (Vue 3.4+):** +```vue + + + +``` + +**GOOD (Vue < 3.4):** +```vue + + + +``` + +If you need the updated value immediately after a change, use the input event value or `nextTick` in the parent. + +## Provide/Inject: Shared Context Without Prop Drilling + +Use provide/inject for cross-tree state, but keep mutations centralized in the provider and expose explicit actions. + +**BAD:** +```vue +// Provider.vue +provide('theme', reactive({ dark: false })) + +// Consumer.vue +const theme = inject('theme') +// Mutating shared state from any depth becomes hard to track +theme.dark = true +``` + +**GOOD:** +```vue +// Provider.vue +const theme = reactive({ dark: false }) +const toggleTheme = () => { theme.dark = !theme.dark } + +provide(themeKey, readonly(theme)) +provide(themeActionsKey, { toggleTheme }) + +// Consumer.vue +const theme = inject(themeKey) +const { toggleTheme } = inject(themeActionsKey) +``` + +Use symbols for keys to avoid collisions in large apps: +```ts +export const themeKey = Symbol('theme') +export const themeActionsKey = Symbol('theme-actions') +``` + +## Use TypeScript Contracts for Public Component APIs + +In TypeScript projects, type component boundaries directly with `defineProps`, `defineEmits`, and `InjectionKey` so invalid payloads and mismatched injections fail at compile time. + +**BAD:** +```vue + +``` + +**GOOD:** +```vue + +``` diff --git a/.agents/skills/vue-best-practices/references/component-fallthrough-attrs.md b/.agents/skills/vue-best-practices/references/component-fallthrough-attrs.md new file mode 100644 index 0000000..5362fa4 --- /dev/null +++ b/.agents/skills/vue-best-practices/references/component-fallthrough-attrs.md @@ -0,0 +1,174 @@ +--- +title: Component Fallthrough Attributes Best Practices +impact: MEDIUM +impactDescription: Incorrect $attrs access and reactivity assumptions can cause undefined values and watchers that never run +type: best-practice +tags: [vue3, attrs, fallthrough-attributes, composition-api, reactivity] +--- + +# Component Fallthrough Attributes Best Practices + +**Impact: MEDIUM** - Fallthrough attributes are straightforward once you follow Vue's conventions: hyphenated names use bracket notation, listener keys are camelCase `onX`, and `useAttrs()` is current-but-not-reactive. + +## Task List + +- Access hyphenated attribute names with bracket notation (for example `attrs['data-testid']`) +- Access event listeners with camelCase `onX` keys (for example `attrs.onClick`) +- Do not `watch()` values returned from `useAttrs()`; those watchers do not trigger on attr changes +- Use `onUpdated()` for attr-driven side effects +- Promote frequently observed attrs to props when reactive observation is required + +## Access Attribute and Listener Keys Correctly + +Hyphenated attribute names preserve their original casing in JavaScript, so dot notation does not work for keys that include `-`. + +**BAD:** +```vue + +``` + +**GOOD:** +```vue + +``` + +### Naming Reference + +| Parent Usage | Access in `attrs` | +|--------------|-------------------| +| `class="foo"` | `attrs.class` | +| `data-id="123"` | `attrs['data-id']` | +| `aria-label="..."` | `attrs['aria-label']` | +| `foo-bar="baz"` | `attrs['foo-bar']` | +| `@click="fn"` | `attrs.onClick` | +| `@custom-event="fn"` | `attrs.onCustomEvent` | +| `@update:modelValue="fn"` | `attrs['onUpdate:modelValue']` | + +## `useAttrs()` Is Not Reactive + +`useAttrs()` always reflects the latest values, but it is intentionally not reactive for watcher tracking. + +**BAD:** +```vue + +``` + +**GOOD:** +```vue + +``` + +**GOOD:** +```vue + +``` + +## Common Patterns + +### Check for optional attrs safely + +```vue + +``` + +### Forward listeners after internal logic + +```vue + + + +``` + +## TypeScript Notes + +`useAttrs()` is typed as `Record`, so cast individual keys when needed. + +```vue + +``` diff --git a/.agents/skills/vue-best-practices/references/component-keep-alive.md b/.agents/skills/vue-best-practices/references/component-keep-alive.md new file mode 100644 index 0000000..f887691 --- /dev/null +++ b/.agents/skills/vue-best-practices/references/component-keep-alive.md @@ -0,0 +1,137 @@ +--- +title: KeepAlive Component Best Practices +impact: HIGH +impactDescription: KeepAlive caches component instances; misuse causes stale data, memory growth, or unexpected lifecycle behavior +type: best-practice +tags: [vue3, keepalive, cache, performance, router, dynamic-components] +--- + +# KeepAlive Component Best Practices + +**Impact: HIGH** - `` caches component instances instead of destroying them. Use it to preserve state across switches, but manage cache size and freshness explicitly to avoid memory growth or stale UI. + +## Task List + +- Use KeepAlive only where state preservation improves UX +- Set a reasonable `max` to cap cache size +- Declare component names for include/exclude matching +- Use `onActivated`/`onDeactivated` for cache-aware logic +- Decide how and when cached views refresh their data +- Avoid caching memory-heavy or security-sensitive views + +## When to Use KeepAlive + +Use KeepAlive when switching between views where state should persist (tabs, multi-step forms, dashboards). Avoid it when each visit should start fresh. + +**BAD:** +```vue + +``` + +**GOOD:** +```vue + +``` + +## When NOT to Use KeepAlive + +- Search or filter pages where users expect fresh results +- Memory-heavy components (maps, large tables, media players) +- Sensitive flows where data must be cleared on exit +- Components with heavy background activity you cannot pause + +## Limit and Control the Cache + +Always cap cache size with `max` and restrict caching to specific components when possible. + +```vue + +``` + +## Ensure Component Names Match include/exclude + +`include` and `exclude` match the component `name` option. Explicitly set names for reliable caching. + +```vue + + +``` + +```vue + +``` + +## Cache Invalidation Strategies + +Vue 3 has no direct API to remove a specific cached instance. Use keys or dynamic include/exclude to force refreshes. + +```vue + + + +``` + +## Lifecycle Hooks for Cached Components + +Cached components are not destroyed on switch. Use activation hooks for refresh and cleanup. + +```vue + +``` + +## Router Caching and Freshness + +Decide whether navigation should show cached state or a fresh view. A common pattern is to key by route when params change. + +```vue + +``` + +If you want cache reuse but fresh data, refresh in `onActivated` and compare query/params before fetching. diff --git a/.agents/skills/vue-best-practices/references/component-slots.md b/.agents/skills/vue-best-practices/references/component-slots.md new file mode 100644 index 0000000..f77a91c --- /dev/null +++ b/.agents/skills/vue-best-practices/references/component-slots.md @@ -0,0 +1,216 @@ +--- +title: Component Slots Best Practices +impact: MEDIUM +impactDescription: Poor slot API design causes empty DOM wrappers, weak TypeScript safety, brittle defaults, and unnecessary component overhead +type: best-practice +tags: [vue3, slots, components, typescript, composables] +--- + +# Component Slots Best Practices + +**Impact: MEDIUM** - Slots are a core component API surface in Vue. Structure them intentionally so templates stay predictable, typed, and performant. + +## Task List + +- Use shorthand syntax for named slots (`#` instead of `v-slot:`) +- Render optional slot wrapper elements only when slot content exists (`$slots` checks) +- Type scoped slot contracts with `defineSlots` in TypeScript components +- Provide fallback content for optional slots +- Prefer composables over renderless components for pure logic reuse + +## Shorthand syntax for named slots + +**BAD:** +```vue + + + +``` + +**GOOD:** +```vue + + + +``` + +## Conditionally Render Optional Slot Wrappers + +Use `$slots` checks when wrapper elements add spacing, borders, or layout constraints. + +**BAD:** +```vue + + +``` + +**GOOD:** +```vue + + +``` + +## Type Scoped Slot Props with defineSlots + +In ` + + +``` + +**GOOD:** +```vue + + + + +``` + +## Provide Slot Fallback Content + +Fallback content makes components resilient when parents omit optional slots. + +**BAD:** +```vue + + +``` + +**GOOD:** +```vue + + +``` + +## Prefer Composables for Pure Logic Reuse + +Renderless components are still useful for slot-driven composition, but composables are usually cleaner for logic-only reuse. + +**BAD:** +```vue + + + + +``` + +**GOOD:** +```ts +// composables/useMouse.ts +import { ref, onMounted, onUnmounted } from 'vue' + +export function useMouse() { + const x = ref(0) + const y = ref(0) + + function onMove(event: MouseEvent) { + x.value = event.pageX + y.value = event.pageY + } + + onMounted(() => window.addEventListener('mousemove', onMove)) + onUnmounted(() => window.removeEventListener('mousemove', onMove)) + + return { x, y } +} +``` + +```vue + + + + +``` diff --git a/.agents/skills/vue-best-practices/references/component-suspense.md b/.agents/skills/vue-best-practices/references/component-suspense.md new file mode 100644 index 0000000..4d9ecab --- /dev/null +++ b/.agents/skills/vue-best-practices/references/component-suspense.md @@ -0,0 +1,228 @@ +--- +title: Suspense Component Best Practices +impact: MEDIUM +impactDescription: Suspense coordinates async dependencies with fallback UI; misconfiguration leads to missing loading states or confusing UX +type: best-practice +tags: [vue3, suspense, async-components, async-setup, loading, fallback, router, transition, keepalive] +--- + +# Suspense Component Best Practices + +**Impact: MEDIUM** - `` coordinates async dependencies (async components or async setup) and renders a fallback while they resolve. Misconfiguration leads to missing loading states, empty renders, or subtle UX bugs. + +## Task List + +- Wrap default and fallback slot content in a single root node +- Use `timeout` when you need the fallback to appear on reverts +- Force root replacement with `:key` when you need Suspense to re-trigger +- Add `suspensible` to nested Suspense boundaries (Vue 3.3+) +- Use `@pending`, `@resolve`, and `@fallback` for programmatic loading state +- Nest `RouterView` -> `Transition` -> `KeepAlive` -> `Suspense` in that order +- Keep Suspense usage centralized and documented in production + +## Single Root in Default and Fallback Slots + +Suspense tracks a single immediate child in both slots. Wrap multiple elements in a single element or component. + +**BAD:** +```vue + +``` + +**GOOD:** +```vue + +``` + +## Fallback Timing on Reverts (`timeout`) + +When Suspense is already resolved and new async work starts, the previous content remains visible until the timeout elapses. Use `timeout="0"` for immediate fallback or a short delay to avoid flicker. + +**BAD:** +```vue + +``` + +**GOOD:** +```vue + +``` + +## Pending State Only Re-triggers on Root Replacement + +Once resolved, Suspense only re-enters pending when the root node of the default slot changes. If async work happens deeper in the tree, no fallback appears. + +**BAD:** +```vue + +``` + +**GOOD:** +```vue + +``` + +## Use `suspensible` for Nested Suspense (Vue 3.3+) + +Nested Suspense boundaries need `suspensible` on the inner boundary so the parent can coordinate loading state. Without it, inner async content may render empty nodes until resolved. + +**BAD:** +```vue + +``` + +**GOOD:** +```vue + +``` + +## Track Loading with Suspense Events + +Use `@pending`, `@resolve`, and `@fallback` for analytics, global loading indicators, or coordinating UI outside the Suspense boundary. + +```vue + + + +``` + +## Recommended Nesting with RouterView, Transition, KeepAlive + +When combining these components, the nesting order should be `RouterView` -> `Transition` -> `KeepAlive` -> `Suspense` so each wrapper works correctly. + +**BAD:** +```vue + +``` + +**GOOD:** +```vue + +``` + +## Treat Suspense Cautiously in Production + +In production code, keep Suspense boundaries minimal, document where they are used, and have a fallback loading strategy if you ever need to replace or refactor them. diff --git a/.agents/skills/vue-best-practices/references/component-teleport.md b/.agents/skills/vue-best-practices/references/component-teleport.md new file mode 100644 index 0000000..db48db2 --- /dev/null +++ b/.agents/skills/vue-best-practices/references/component-teleport.md @@ -0,0 +1,108 @@ +--- +title: Teleport Component Best Practices +impact: MEDIUM +impactDescription: Teleport renders content outside the component's DOM position, which is essential for overlays but affects styling and layout +type: best-practice +tags: [vue3, teleport, modal, overlay, positioning, responsive] +--- + +# Teleport Component Best Practices + +**Impact: MEDIUM** - `` renders part of a component's template in a different place in the DOM while preserving the Vue component hierarchy. Use it for overlays (modals, toasts, tooltips) or any UI that must escape stacking contexts, overflow, or fixed positioning constraints. + +## Task List + +- Teleport overlays to `body` or a dedicated container outside the app root +- Keep a shared target for similar UI (`#modals`, `#notifications`) and control layering with order or z-index +- Use `:disabled` for responsive layouts that should render inline on small screens +- Remember props, emits, and provide/inject still work through teleport +- Avoid relying on parent stacking contexts or transforms for teleported UI + +## Teleport Overlays Out of Transformed Containers + +When an ancestor has `transform`, `filter`, or `perspective`, fixed-position overlays can behave like they are locally positioned. Teleport escapes that context. + +**BAD:** +```vue + + + +``` + +**GOOD:** +```vue + +``` + +## Responsive Layouts with `disabled` + +Use `:disabled` to render inline on mobile and teleport on larger screens: + +```vue + + + +``` + +## Logical Hierarchy Is Preserved + +Teleport changes DOM position, not the Vue component tree. Props, emits, slots, and provide/inject still work: + +```vue + +``` + +## Multiple Teleports to the Same Target + +Teleports to the same target append in declaration order: + +```vue + +``` + +Use a shared container to keep stacking predictable, and apply z-index only when you need explicit layering. diff --git a/.agents/skills/vue-best-practices/references/component-transition-group.md b/.agents/skills/vue-best-practices/references/component-transition-group.md new file mode 100644 index 0000000..d0339ff --- /dev/null +++ b/.agents/skills/vue-best-practices/references/component-transition-group.md @@ -0,0 +1,128 @@ +--- +title: TransitionGroup Component Best Practices +impact: MEDIUM +impactDescription: TransitionGroup animates list items; missing keys or misuse leads to broken list transitions +type: best-practice +tags: [vue3, transition-group, animation, lists, keys] +--- + +# TransitionGroup Component Best Practices + +**Impact: MEDIUM** - `` animates lists of items entering, leaving, and moving. Use it for `v-for` lists or dynamic collections where individual items change over time. + +## Task List + +- Use `` only for lists and repeated items +- Provide unique, stable keys for every direct child +- Use `tag` when you need semantic or layout wrappers +- Avoid the `mode` prop (not supported) +- Use JavaScript hooks for staggered effects + +## Use TransitionGroup for Lists + +`` is designed for list items. Use `tag` to control the wrapper element when needed. + +**BAD:** +```vue + +``` + +**GOOD:** +```vue + +``` + +## Always Provide Stable Keys + +Keys are required. Without stable keys, Vue cannot track item positions and animations break. + +**BAD:** +```vue + +``` + +**GOOD:** +```vue + +``` + +## Do Not Use `mode` on TransitionGroup + +`mode` is only for `` because it swaps a single element. Use `` if you need in/out sequencing. + +**BAD:** +```vue + +``` + +**GOOD:** +```vue + +``` + +## Stagger List Animations with Data Attributes + +For cascading list animations, pass the index to JavaScript hooks and compute delay per item. + +```vue + + + +``` diff --git a/.agents/skills/vue-best-practices/references/component-transition.md b/.agents/skills/vue-best-practices/references/component-transition.md new file mode 100644 index 0000000..e6abed7 --- /dev/null +++ b/.agents/skills/vue-best-practices/references/component-transition.md @@ -0,0 +1,125 @@ +--- +title: Transition Component Best Practices +impact: MEDIUM +impactDescription: Transition animates a single element or component; incorrect structure or keys prevent animations +type: best-practice +tags: [vue3, transition, animation, performance, keys] +--- + +# Transition Component Best Practices + +**Impact: MEDIUM** - `` animates entering/leaving of a single element or component. It is ideal for toggling UI states, swapping views, or animating one component at a time. + +## Task List + +- Wrap a single element or component inside `` +- Provide a `key` when switching between same element types +- Use `mode="out-in"` when you need sequential swaps +- Prefer `transform` and `opacity` for smooth animations + +## Use Transition for a Single Root Element + +`` only supports one direct child. Wrap multiple nodes in a single element or component. + +**BAD:** +```vue + +``` + +**GOOD:** +```vue + +``` + +## Force Transitions Between Same Element Types + +Vue reuses the same DOM element when the tag type does not change. Add `key` so Vue treats it as a new element and triggers enter/leave. + +**BAD:** +```vue + +``` + +**GOOD:** +```vue + +``` + +## Use `mode` to Avoid Overlap During Swaps + +When swapping components or views, use `mode="out-in"` to prevent both from being visible at the same time. + +**BAD:** +```vue + +``` + +**GOOD:** +```vue + +``` + +## Animate `transform` and `opacity` for Performance + +Avoid layout-triggering properties such as `height`, `margin`, or `top`. Use `transform` and `opacity` for smooth, GPU-friendly transitions. + +**BAD:** +```css +.slide-enter-active, +.slide-leave-active { + transition: height 0.3s ease; +} + +.slide-enter-from, +.slide-leave-to { + height: 0; +} +``` + +**GOOD:** +```css +.slide-enter-active, +.slide-leave-active { + transition: transform 0.3s ease, opacity 0.3s ease; +} + +.slide-enter-from { + transform: translateX(-12px); + opacity: 0; +} + +.slide-leave-to { + transform: translateX(12px); + opacity: 0; +} +``` diff --git a/.agents/skills/vue-best-practices/references/composables.md b/.agents/skills/vue-best-practices/references/composables.md new file mode 100644 index 0000000..cb18a6f --- /dev/null +++ b/.agents/skills/vue-best-practices/references/composables.md @@ -0,0 +1,290 @@ +--- +title: Composable Organization Patterns +impact: MEDIUM +impactDescription: Well-structured composables improve maintainability, reusability, and update performance +type: best-practice +tags: [vue3, composables, composition-api, code-organization, api-design, readonly, utilities] +--- + +# Composable Organization Patterns + +**Impact: MEDIUM** - Treat composables as reusable, stateful building blocks and keep their code organized by feature concern. This keeps large components maintainable and prevents hard-to-debug mutation and API design issues. + +## Task List + +- Compose complex behavior from small, focused composables +- Use options objects for composables with multiple optional parameters +- Return readonly state when updates must flow through explicit actions +- Keep pure utility functions as plain utilities, not composables +- Organize composable and component code by feature concern, and extract composables when components grow + +## Compose Composables from Smaller Primitives + +**BAD:** +```vue + +``` + +**GOOD:** +```javascript +// composables/useEventListener.js +import { onMounted, onUnmounted, toValue } from 'vue' + +export function useEventListener(target, event, callback) { + onMounted(() => toValue(target).addEventListener(event, callback)) + onUnmounted(() => toValue(target).removeEventListener(event, callback)) +} +``` + +```javascript +// composables/useMouse.js +import { ref } from 'vue' +import { useEventListener } from './useEventListener' + +export function useMouse() { + const x = ref(0) + const y = ref(0) + + useEventListener(window, 'mousemove', (e) => { + x.value = e.pageX + y.value = e.pageY + }) + + return { x, y } +} +``` + +```javascript +// composables/useMouseInElement.js +import { computed } from 'vue' +import { useMouse } from './useMouse' + +export function useMouseInElement(elementRef) { + const { x, y } = useMouse() + + const isOutside = computed(() => { + if (!elementRef.value) return true + const rect = elementRef.value.getBoundingClientRect() + return x.value < rect.left || x.value > rect.right || + y.value < rect.top || y.value > rect.bottom + }) + + return { x, y, isOutside } +} +``` + +## Use Options Object Pattern for Composable Parameters + +**BAD:** +```javascript +export function useFetch(url, method, headers, timeout, retries, immediate) { + // hard to read and easy to misorder +} + +useFetch('/api/users', 'GET', null, 5000, 3, true) +``` + +**GOOD:** +```javascript +export function useFetch(url, options = {}) { + const { + method = 'GET', + headers = {}, + timeout = 30000, + retries = 0, + immediate = true + } = options + + // implementation + return { method, headers, timeout, retries, immediate } +} + +useFetch('/api/users', { + method: 'POST', + timeout: 5000, + retries: 3 +}) +``` + +```typescript +interface UseCounterOptions { + initial?: number + min?: number + max?: number + step?: number +} + +export function useCounter(options: UseCounterOptions = {}) { + const { initial = 0, min = -Infinity, max = Infinity, step = 1 } = options + // implementation +} +``` + +## Return Readonly State with Explicit Actions + +**BAD:** +```javascript +export function useCart() { + const items = ref([]) + const total = computed(() => items.value.reduce((sum, item) => sum + item.price, 0)) + return { items, total } // any consumer can mutate directly +} + +const { items } = useCart() +items.value.push({ id: 1, price: 10 }) +``` + +**GOOD:** +```javascript +import { ref, computed, readonly } from 'vue' + +export function useCart() { + const _items = ref([]) + + const total = computed(() => + _items.value.reduce((sum, item) => sum + item.price * item.quantity, 0) + ) + + function addItem(product, quantity = 1) { + const existing = _items.value.find(item => item.id === product.id) + if (existing) { + existing.quantity += quantity + return + } + _items.value.push({ ...product, quantity }) + } + + function removeItem(productId) { + _items.value = _items.value.filter(item => item.id !== productId) + } + + return { + items: readonly(_items), + total, + addItem, + removeItem + } +} +``` + +## Keep Utilities as Utilities + +**BAD:** +```javascript +export function useFormatters() { + const formatDate = (date) => new Intl.DateTimeFormat('en-US').format(date) + const formatCurrency = (amount) => + new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(amount) + return { formatDate, formatCurrency } +} + +const { formatDate } = useFormatters() +``` + +**GOOD:** +```javascript +// utils/formatters.js +export function formatDate(date) { + return new Intl.DateTimeFormat('en-US').format(date) +} + +export function formatCurrency(amount) { + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD' + }).format(amount) +} +``` + +```javascript +// composables/useInvoiceSummary.js +import { computed } from 'vue' +import { formatCurrency } from '@/utils/formatters' + +export function useInvoiceSummary(invoiceRef) { + const totalLabel = computed(() => formatCurrency(invoiceRef.value.total)) + return { totalLabel } +} +``` + +## Organize Composable and Component Code by Feature Concern + +**BAD:** +```vue + +``` + +**GOOD:** +```vue + +``` + +```javascript +// composables/useItems.js +import { ref, onMounted } from 'vue' + +export function useItems() { + const items = ref([]) + const loading = ref(false) + + async function fetchItems() { + loading.value = true + try { + items.value = await api.getItems() + } finally { + loading.value = false + } + } + + onMounted(fetchItems) + return { items, loading, fetchItems } +} +``` diff --git a/.agents/skills/vue-best-practices/references/directives.md b/.agents/skills/vue-best-practices/references/directives.md new file mode 100644 index 0000000..8412fbc --- /dev/null +++ b/.agents/skills/vue-best-practices/references/directives.md @@ -0,0 +1,162 @@ +--- +title: Directive Best Practices +impact: MEDIUM +impactDescription: Custom directives are powerful but easy to misuse; following patterns prevents leaks, invalid usage, and unclear abstractions +type: best-practice +tags: [vue3, directives, custom-directives, composition, typescript] +--- + +# Directive Best Practices + +**Impact: MEDIUM** - Directives are for low-level DOM access. Use them sparingly, keep them side-effect safe, and prefer components or composables when you need stateful or reusable UI behavior. + +## Task List + +- Use directives only when you need direct DOM access +- Do not mutate directive arguments or binding objects +- Clean up timers, listeners, and observers in `unmounted` +- Register directives in ` + + +``` + +## Clean Up Side Effects in `unmounted` + +Any timers, listeners, or observers must be removed to avoid leaks. + +```ts +const vResize = { + mounted(el) { + const observer = new ResizeObserver(() => {}) + observer.observe(el) + el._observer = observer + }, + unmounted(el) { + el._observer?.disconnect() + } +} +``` + +## Prefer Function Shorthand for Single-Hook Directives + +If you only need `mounted`/`updated`, use the function form. + +```ts +const vAutofocus = (el) => el.focus() +``` + +## Use the `v-` Prefix and Script Setup Registration + +```vue + + + +``` + +## Type Custom Directives in TypeScript Projects + +Use `Directive` so `binding.value` is typed, and augment Vue's template types so directives are recognized in SFC templates. + +**BAD:** +```ts +// Untyped directive value and no template type augmentation +export const vHighlight = { + mounted(el, binding) { + el.style.backgroundColor = binding.value + } +} +``` + +**GOOD:** +```ts +import type { Directive } from 'vue' + +type HighlightValue = string + +export const vHighlight = { + mounted(el, binding) { + el.style.backgroundColor = binding.value + } +} satisfies Directive + +declare module 'vue' { + interface ComponentCustomProperties { + vHighlight: typeof vHighlight + } +} +``` + +## Handle SSR with `getSSRProps` + +Directive hooks such as `mounted` and `updated` do not run during SSR. If a directive sets attributes/classes that affect rendered HTML, provide an SSR equivalent via `getSSRProps` to avoid hydration mismatches. + +**BAD:** +```ts +const vTooltip = { + mounted(el, binding) { + el.setAttribute('data-tooltip', binding.value) + el.classList.add('has-tooltip') + } +} +``` + +**GOOD:** +```ts +const vTooltip = { + mounted(el, binding) { + el.setAttribute('data-tooltip', binding.value) + el.classList.add('has-tooltip') + }, + getSSRProps(binding) { + return { + 'data-tooltip': binding.value, + class: 'has-tooltip' + } + } +} +``` + +## Prefer Declarative Templates When Possible + +If a standard attribute or binding works, use it instead of a directive. + +## Decide Between Directives and Components + +Use a directive for DOM-level behavior. Use a component when behavior affects structure, state, or rendering. diff --git a/.agents/skills/vue-best-practices/references/perf-avoid-component-abstraction-in-lists.md b/.agents/skills/vue-best-practices/references/perf-avoid-component-abstraction-in-lists.md new file mode 100644 index 0000000..44f98ff --- /dev/null +++ b/.agents/skills/vue-best-practices/references/perf-avoid-component-abstraction-in-lists.md @@ -0,0 +1,159 @@ +--- +title: Avoid Excessive Component Abstraction in Large Lists +impact: MEDIUM +impactDescription: Each component instance has memory and render overhead - abstractions multiply this in lists +type: efficiency +tags: [vue3, performance, components, abstraction, lists, optimization] +--- + +# Avoid Excessive Component Abstraction in Large Lists + +**Impact: MEDIUM** - Component instances are more expensive than plain DOM nodes. While abstractions improve code organization, unnecessary nesting creates overhead. In large lists, this overhead multiplies - 100 items with 3 levels of abstraction means 300+ component instances instead of 100. + +Don't avoid abstraction entirely, but be mindful of component depth in frequently-rendered elements like list items. + +## Task List + +- Review list item components for unnecessary wrapper components +- Consider flattening component hierarchies in hot paths +- Use native elements when a component adds no value +- Profile component counts using Vue DevTools +- Focus optimization efforts on the most-rendered components + +**BAD:** +```vue + + + + + + + +``` + +**GOOD:** +```vue + + + + + + + + + +``` + +## When Abstraction Is Still Worth It + +```vue + + + + + + + + + + + + + + + +``` + +## Measuring Component Overhead + +```javascript +// In development, profile component counts +import { onMounted, getCurrentInstance } from 'vue' + +onMounted(() => { + const instance = getCurrentInstance() + let count = 0 + + function countComponents(vnode) { + if (vnode.component) count++ + if (vnode.children) { + vnode.children.forEach(child => { + if (child.component || child.children) countComponents(child) + }) + } + } + + // Use Vue DevTools instead for accurate counts + console.log('Check Vue DevTools Components tab for instance counts') +}) +``` + +## Alternatives to Wrapper Components + +```vue + + + + +{{ content }} + + +
+ +
+ + +``` + +## Impact Calculation + +| List Size | Components per Item | Total Instances | Memory Impact | +|-----------|---------------------|-----------------|---------------| +| 100 items | 1 (flat) | 100 | Baseline | +| 100 items | 3 (nested) | 300 | ~3x memory | +| 100 items | 5 (deeply nested) | 500 | ~5x memory | +| 1000 items | 1 (flat) | 1000 | High | +| 1000 items | 5 (deeply nested) | 5000 | Very High | diff --git a/.agents/skills/vue-best-practices/references/perf-v-once-v-memo-directives.md b/.agents/skills/vue-best-practices/references/perf-v-once-v-memo-directives.md new file mode 100644 index 0000000..ce5f688 --- /dev/null +++ b/.agents/skills/vue-best-practices/references/perf-v-once-v-memo-directives.md @@ -0,0 +1,182 @@ +--- +title: Use v-once and v-memo to Skip Unnecessary Updates +impact: MEDIUM +impactDescription: v-once skips all future updates for static content; v-memo conditionally memoizes subtrees +type: efficiency +tags: [vue3, performance, v-once, v-memo, optimization, directives] +--- + +# Use v-once and v-memo to Skip Unnecessary Updates + +**Impact: MEDIUM** - Vue re-evaluates templates on every reactive change. For content that never changes or changes infrequently, `v-once` and `v-memo` tell Vue to skip updates, reducing render work. + +Use `v-once` for truly static content and `v-memo` for conditionally-static content in lists. + +## Task List + +- Apply `v-once` to elements that use runtime data but never need updating +- Apply `v-memo` to list items that should only update on specific condition changes +- Verify memoized content doesn't need to respond to other state changes +- Profile with Vue DevTools to confirm update skipping + +## v-once: Render Once, Never Update + +**BAD:** +```vue + +``` + +**GOOD:** +```vue + + + +``` + +## v-memo: Conditional Memoization for Lists + +**BAD:** +```vue + +``` + +**GOOD:** +```vue + + + +``` + +## v-memo with Multiple Dependencies + +```vue + + + +``` + +## v-memo with Empty Array = v-once + +```vue + +``` + +## When NOT to Use These Directives + +```vue + +``` + +## Performance Comparison + +| Scenario | Without Directive | With v-once/v-memo | +|----------|-------------------|-------------------| +| Static header, parent re-renders 100x | Re-evaluated 100x | Evaluated 1x | +| 1000 items, selection changes | 1000 items re-render | 2 items re-render | +| Complex child component | Full re-render | Skipped if memoized | + +## Debugging Memoized Components + +```vue + +``` diff --git a/.agents/skills/vue-best-practices/references/perf-virtualize-large-lists.md b/.agents/skills/vue-best-practices/references/perf-virtualize-large-lists.md new file mode 100644 index 0000000..78a8a1c --- /dev/null +++ b/.agents/skills/vue-best-practices/references/perf-virtualize-large-lists.md @@ -0,0 +1,187 @@ +--- +title: Virtualize Large Lists to Avoid DOM Overload +impact: HIGH +impactDescription: Rendering thousands of list items creates excessive DOM nodes, causing slow renders and high memory usage +type: efficiency +tags: [vue3, performance, virtual-list, large-data, dom, optimization] +--- + +# Virtualize Large Lists to Avoid DOM Overload + +**Impact: HIGH** - Rendering all items in a large list (hundreds or thousands) creates massive amounts of DOM nodes. Each node consumes memory, slows down initial render, and makes updates expensive. List virtualization only renders visible items, dramatically improving performance. + +Use a virtualization library when dealing with lists that could exceed 50-100 items, especially if items have complex content. + +## Task List + +- Identify lists that render more than 50-100 items +- Install a virtualization library (vue-virtual-scroller, @tanstack/vue-virtual) +- Replace standard `v-for` with virtualized component +- Ensure list items have consistent or estimable heights +- Test with realistic data volumes during development + +## Recommended Libraries + +| Library | Best For | Notes | +|---------|----------|-------| +| `vue-virtual-scroller` | General use, easy setup | Most popular, good defaults | +| `@tanstack/vue-virtual` | Complex layouts, headless | Framework-agnostic, flexible | +| `vue-virtual-scroll-grid` | Grid layouts | 2D virtualization | +| `vueuc/VVirtualList` | Naive UI projects | Part of Naive UI ecosystem | + +**BAD:** +```vue + + + +``` + +**GOOD:** +```vue + + + + + +``` + +## Using @tanstack/vue-virtual + +```vue + + + + + +``` + +## Dynamic Heights with vue-virtual-scroller + +```vue + + + +``` + +## Performance Comparison + +| Approach | 100 Items | 1,000 Items | 10,000 Items | +|----------|-----------|-------------|--------------| +| Regular v-for | ~100 DOM nodes | ~1,000 DOM nodes | ~10,000 DOM nodes | +| Virtualized | ~20 DOM nodes | ~20 DOM nodes | ~20 DOM nodes | +| Initial render | Fast | Slow | Very slow / crashes | +| Virtualized render | Fast | Fast | Fast | + +## When NOT to Virtualize + +- Lists under 50 items with simple content +- Lists where all items must be accessible to screen readers simultaneously +- Print layouts where all content must render +- SEO-critical content that must be in initial HTML diff --git a/.agents/skills/vue-best-practices/references/plugins.md b/.agents/skills/vue-best-practices/references/plugins.md new file mode 100644 index 0000000..190cee8 --- /dev/null +++ b/.agents/skills/vue-best-practices/references/plugins.md @@ -0,0 +1,166 @@ +--- +title: Vue Plugin Best Practices +impact: MEDIUM +impactDescription: Incorrect plugin structure or injection key strategy causes install failures, collisions, and unsafe APIs +type: best-practice +tags: [vue3, plugins, provide-inject, typescript, dependency-injection] +--- + +# Vue Plugin Best Practices + +**Impact: MEDIUM** - Vue plugins should follow the `app.use()` contract, expose explicit capabilities, and use collision-safe injection keys. This keeps plugin setup predictable and composable across large apps. + +## Task List + +- Export plugins as an object with `install()` or as an install function +- Use the `app` instance in `install()` to register components/directives/provides +- Type plugin APIs with `Plugin` (and options tuple types when needed) +- Use symbol keys (prefer `InjectionKey`) for `provide/inject` in plugins +- Add a small typed composable wrapper for required injections to fail fast + +## Structure Plugins for `app.use()` + +A Vue plugin must be either: +- An object with `install(app, options?)` +- A function with the same signature + +**BAD:** +```ts +const notAPlugin = { + doSomething() {} +} + +app.use(notAPlugin) +``` + +**GOOD:** +```ts +import type { App } from 'vue' + +interface PluginOptions { + prefix?: string + debug?: boolean +} + +const myPlugin = { + install(app: App, options: PluginOptions = {}) { + const { prefix = 'my', debug = false } = options + + if (debug) { + console.log('Installing myPlugin with prefix:', prefix) + } + + app.provide('myPlugin', { prefix }) + } +} + +app.use(myPlugin, { prefix: 'custom', debug: true }) +``` + +**GOOD:** +```ts +import type { App } from 'vue' + +function simplePlugin(app: App, options?: { message: string }) { + app.config.globalProperties.$greet = () => options?.message ?? 'Hello!' +} + +app.use(simplePlugin, { message: 'Welcome!' }) +``` + +## Register Capabilities Explicitly in `install()` + +Inside `install()`, wire behavior through Vue application APIs: +- `app.component()` for global components +- `app.directive()` for global directives +- `app.provide()` for injectable services and config +- `app.config.globalProperties` for optional global helpers (sparingly) + +**BAD:** +```ts +const uselessPlugin = { + install(app, options) { + const service = createService(options) + } +} +``` + +**GOOD:** +```ts +const usefulPlugin = { + install(app, options) { + const service = createService(options) + app.provide(serviceKey, service) + } +} +``` + +## Type Plugin Contracts + +Use Vue's `Plugin` type to keep install signatures and options type-safe. + +```ts +import type { App, Plugin } from 'vue' + +interface MyOptions { + apiKey: string +} + +const myPlugin: Plugin<[MyOptions]> = { + install(app: App, options: MyOptions) { + app.provide(apiKeyKey, options.apiKey) + } +} +``` + +## Use Symbol Injection Keys in Plugins + +String keys can collide (`'http'`, `'config'`, `'i18n'`). Use symbol keys with `InjectionKey` so injections are unique and typed. + +**BAD:** +```ts +export default { + install(app) { + app.provide('http', axios) + app.provide('config', appConfig) + } +} +``` + +**GOOD:** +```ts +import type { InjectionKey } from 'vue' +import type { AxiosInstance } from 'axios' + +interface AppConfig { + apiUrl: string + timeout: number +} + +export const httpKey: InjectionKey = Symbol('http') +export const configKey: InjectionKey = Symbol('appConfig') + +export default { + install(app) { + app.provide(httpKey, axios) + app.provide(configKey, { apiUrl: '/api', timeout: 5000 }) + } +} +``` + +## Provide Required Injection Helpers + +Wrap required injections in composables that throw clear setup errors. + +```ts +import { inject } from 'vue' +import { authKey, type AuthService } from '@/injection-keys' + +export function useAuth(): AuthService { + const auth = inject(authKey) + if (!auth) { + throw new Error('Auth plugin not installed. Did you forget app.use(authPlugin)?') + } + return auth +} +``` diff --git a/.agents/skills/vue-best-practices/references/reactivity.md b/.agents/skills/vue-best-practices/references/reactivity.md new file mode 100644 index 0000000..4cf0ad3 --- /dev/null +++ b/.agents/skills/vue-best-practices/references/reactivity.md @@ -0,0 +1,344 @@ +--- +title: Reactivity Core Patterns (ref, reactive, shallowRef, computed, watch) +impact: MEDIUM +impactDescription: Clear reactivity choices keep state predictable and reduce unnecessary updates in Vue 3 apps +type: efficiency +tags: [vue3, reactivity, ref, reactive, shallowRef, computed, watch, watchEffect, external-state, best-practice] +--- + +# Reactivity Core Patterns (ref, reactive, shallowRef, computed, watch) + +**Impact: MEDIUM** - Choose the right reactive primitive first, derive with `computed`, and use watchers only for side effects. + +This reference covers the core reactivity decisions for local state, external data, derived values, and effects. + +## Task List + +- Declare reactive state correctly + - Always use `shallowRef()` instead of `ref()` for primitive values + - Choose the correct reactive declaration method for objects/arrays/map/set +- Follow best practices for `reactive` + - Avoid destructuring from `reactive()` directly + - Watch correctly for `reactive` +- Follow best practices for `computed` + - Prefer `computed` over watcher-assigned derived refs + - Keep filtered/sorted derivations out of templates + - Use `computed` for reusable class/style logic + - Keep computed getters pure (no side effects) and put side effects in watchers +- Follow best practices for watchers + - Use `immediate: true` instead of duplicate initial calls + - Clean up async effects for watchers + +## Declare reactive state correctly + +### Always use `shallowRef()` instead of `ref()` for primitive values (string, number, boolean, null, etc.) for better performance. + +**Incorrect:** +```ts +import { ref } from 'vue' +const count = ref(0) +``` + +**Correct:** +```ts +import { shallowRef } from 'vue' +const count = shallowRef(0) +``` + +### Choose the correct reactive declaration method for objects/arrays/map/set + +Use `ref()` when you often **replace the entire value** (`state.value = newObj`) and still want deep reactivity inside it, usually used for: + +- Frequently reassigned state (replace fetched object/list, reset to defaults, switch presets). +- Composable return values where updates happen mostly via `.value` reassignment. + +Use `reactive()` when you mainly **mutate properties** and full replacement is uncommon, usually used for: + +- “Single state object” patterns (stores/forms): `state.count++`, `state.items.push(...)`, `state.user.name = ...`. +- Situations where you want to avoid `.value` and update nested fields in place. + +```ts +import { reactive } from 'vue' + +const state = reactive({ + count: 0, + user: { name: 'Alice', age: 30 } +}) + +state.count++ // ✅ reactive +state.user.age = 31 // ✅ reactive +// ❌ avoid replacing the reactive object reference: +// state = reactive({ count: 1 }) +``` + +Use `shallowRef()` when the value is **opaque / should not be proxied** (class instances, external library objects, very large nested data) and you only want updates to trigger when you **replace** `state.value` (no deep tracking), usually used for: + +- Storing external instances/handles (SDK clients, class instances) without Vue proxying internals. +- Large data where you update by replacing the root reference (immutable-style updates). + +```ts +import { shallowRef } from 'vue' + +const user = shallowRef({ name: 'Alice', age: 30 }) + +user.value.age = 31 // ❌ not reactive +user.value = { name: 'Bob', age: 25 } // ✅ triggers update +``` + +Use `shallowReactive()` when you want **only top-level properties** reactive; nested objects remain raw, usually used for: + +- Container objects where only top-level keys change and nested payloads should stay unmanaged/unproxied. +- Mixed structures where Vue tracks the wrapper object, but not deeply nested or foreign objects. + +```ts +import { shallowReactive } from 'vue' + +const state = shallowReactive({ + count: 0, + user: { name: 'Alice', age: 30 } +}) + +state.count++ // ✅ reactive +state.user.age = 31 // ❌ not reactive +``` + +## Best practices for `reactive` + +### Avoid destructuring from `reactive()` directly + +**BAD:** + +```ts +import { reactive } from 'vue' + +const state = reactive({ count: 0 }) +const { count } = state // ❌ disconnected from reactivity +``` + +### Watch correctly for reactive + +**BAD:** + +passing a non-getter value into `watch()` + +```ts +import { reactive, watch } from 'vue' + +const state = reactive({ count: 0 }) + +// ❌ watch expects a getter, ref, reactive object, or array of these +watch(state.count, () => { /* ... */ }) +``` + +**GOOD:** + +preserve reactivity with `toRefs()` and use a getter for `watch()` + +```ts +import { reactive, toRefs, watch } from 'vue' + +const state = reactive({ count: 0 }) +const { count } = toRefs(state) // ✅ count is a ref + +watch(count, () => { /* ... */ }) // ✅ +watch(() => state.count, () => { /* ... */ }) // ✅ +``` + +## Best practices for `computed` + +### Prefer `computed` over watcher-assigned derived refs + +**BAD:** +```ts +import { ref, watchEffect } from 'vue' + +const items = ref([{ price: 10 }, { price: 20 }]) +const total = ref(0) + +watchEffect(() => { + total.value = items.value.reduce((sum, item) => sum + item.price, 0) +}) +``` + +**GOOD:** +```ts +import { ref, computed } from 'vue' + +const items = ref([{ price: 10 }, { price: 20 }]) +const total = computed(() => + items.value.reduce((sum, item) => sum + item.price, 0) +) +``` + +### Keep filtered/sorted derivations out of templates + +**BAD:** +```vue + + + +``` + +**GOOD:** +```vue + + + +``` + +### Use `computed` for reusable class/style logic + +**BAD:** +```vue + +``` + +**GOOD:** +```vue + + + +``` + +### Keep computed getters pure (no side effects) and put side effects in watchers instead + +A computed getter should only derive a value. No mutation, no API calls, no storage writes, no event emits. +([Reference](https://vuejs.org/guide/essentials/computed.html#best-practices)) + +**BAD:** + +side effects inside computed + +```ts +const count = ref(0) + +const doubled = computed(() => { + // ❌ side effect + if (count.value > 10) console.warn('Too big!') + return count.value * 2 +}) +``` + +**GOOD:** + +pure computed + `watch()` for side effects + +```ts +const count = ref(0) +const doubled = computed(() => count.value * 2) + +watch(count, (value) => { + if (value > 10) console.warn('Too big!') +}) +``` + +## Best practices for watchers + +### Use `immediate: true` instead of duplicate initial calls + +**BAD:** +```ts +import { ref, watch, onMounted } from 'vue' + +const userId = ref(1) + +function loadUser(id) { + // ... +} + +onMounted(() => loadUser(userId.value)) +watch(userId, (id) => loadUser(id)) +``` + +**GOOD:** +```ts +import { ref, watch } from 'vue' + +const userId = ref(1) + +watch( + userId, + (id) => loadUser(id), + { immediate: true } +) +``` + +### Clean up async effects for watchers + +When reacting to rapid changes (search boxes, filters), cancel the previous request. + +**GOOD:** + +```ts +const query = ref('') +const results = ref([]) + +watch(query, async (q, _prev, onCleanup) => { + const controller = new AbortController() + onCleanup(() => controller.abort()) + + const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`, { + signal: controller.signal, + }) + + results.value = await res.json() +}) +``` diff --git a/.agents/skills/vue-best-practices/references/render-functions.md b/.agents/skills/vue-best-practices/references/render-functions.md new file mode 100644 index 0000000..b64942c --- /dev/null +++ b/.agents/skills/vue-best-practices/references/render-functions.md @@ -0,0 +1,201 @@ +--- +title: Render Function Patterns and Performance +impact: MEDIUM +impactDescription: Render functions require explicit patterns for lists, events, v-model, and performance to stay correct and maintainable +type: best-practice +tags: [vue3, render-function, h, v-model, directives, performance, jsx] +--- + +# Render Function Patterns and Performance + +**Impact: MEDIUM** - Render functions are powerful but opt out of template compiler optimizations. Use them intentionally and apply the key patterns below to keep output correct and performant. + +## Task List + +- Prefer templates; use render functions only when templates cannot express the logic +- Always add stable keys when rendering lists with `h()`/JSX +- Use `withModifiers` / `withKeys` for event modifiers +- Implement `v-model` via `modelValue` + `onUpdate:modelValue` +- Apply custom directives with `withDirectives` +- Use functional components for stateless presentational UI + +## Prefer templates over render functions + +**BAD:** +```vue + +``` + +**GOOD:** +```vue + + + +``` + +## Always add keys for list rendering + +**BAD:** +```javascript +import { h, ref } from 'vue' + +export default { + setup() { + const items = ref([{ id: 1, name: 'Apple' }]) + + return () => h('ul', + items.value.map(item => h('li', item.name)) + ) + } +} +``` + +**GOOD:** +```javascript +import { h, ref } from 'vue' + +export default { + setup() { + const items = ref([{ id: 1, name: 'Apple' }]) + + return () => h('ul', + items.value.map(item => h('li', { key: item.id }, item.name)) + ) + } +} +``` + +## Use `withModifiers` / `withKeys` for event modifiers + +**BAD:** +```javascript +import { h } from 'vue' + +export default { + setup() { + const handleClick = (e) => { + e.stopPropagation() + e.preventDefault() + } + + return () => h('button', { onClick: handleClick }, 'Click') + } +} +``` + +**GOOD:** +```javascript +import { h, withModifiers, withKeys } from 'vue' + +export default { + setup() { + const handleClick = () => {} + const handleEnter = () => {} + + return () => h('div', [ + h('button', { + onClick: withModifiers(handleClick, ['stop', 'prevent']) + }, 'Click'), + h('input', { + onKeyup: withKeys(handleEnter, ['enter']) + }) + ]) + } +} +``` + +## Implement `v-model` explicitly + +**BAD:** +```javascript +import { h, ref } from 'vue' +import CustomInput from './CustomInput.vue' + +export default { + setup() { + const text = ref('') + return () => h(CustomInput, { modelValue: text.value }) + } +} +``` + +**GOOD:** +```javascript +import { h, ref } from 'vue' +import CustomInput from './CustomInput.vue' + +export default { + setup() { + const text = ref('') + return () => h(CustomInput, { + modelValue: text.value, + 'onUpdate:modelValue': (value) => { text.value = value } + }) + } +} +``` + +## Use `withDirectives` for custom directives + +**BAD:** +```javascript +import { h } from 'vue' + +const vFocus = { mounted: (el) => el.focus() } + +export default { + setup() { + return () => h('input', { 'v-focus': true }) + } +} +``` + +**GOOD:** +```javascript +import { h, withDirectives } from 'vue' + +const vFocus = { mounted: (el) => el.focus() } + +export default { + setup() { + return () => withDirectives(h('input'), [[vFocus]]) + } +} +``` + +## Prefer functional components for stateless UI + +**BAD:** +```javascript +import { h } from 'vue' + +export default { + setup() { + return () => h('span', { class: 'badge' }, 'New') + } +} +``` + +**GOOD:** +```javascript +import { h } from 'vue' + +function Badge(props, { slots }) { + return h('span', { class: 'badge' }, slots.default?.()) +} + +Badge.props = ['variant'] + +export default Badge +``` diff --git a/.agents/skills/vue-best-practices/references/sfc.md b/.agents/skills/vue-best-practices/references/sfc.md new file mode 100644 index 0000000..d1c3981 --- /dev/null +++ b/.agents/skills/vue-best-practices/references/sfc.md @@ -0,0 +1,310 @@ +--- +title: Single-File Component Structure, Styling, and Template Patterns +impact: MEDIUM +impactDescription: Consistent SFC structure and styling choices improve maintainability, tooling support, and render performance +type: best-practice +tags: [vue3, sfc, scoped-css, styles, build-tools, performance, template, v-html, v-for, computed, v-if, v-show] +--- + +# Single-File Component Structure, Styling, and Template Patterns + +**Impact: MEDIUM** - Using SFCs with consistent structure and performant styling keeps components easier to maintain and avoids unnecessary render overhead. + +## Task List + +- Use `.vue` SFCs instead of separate `.js`/`.ts` and `.css` files for components +- Colocate template, script, and styles in the same SFC by default +- Use PascalCase for component names in templates and filenames +- Prefer component-scoped styles +- Prefer class selectors (not element selectors) in scoped CSS for performance +- Access DOM / component refs with `useTemplateRef()` in Vue 3.5+ +- Use camelCase keys in `:style` bindings for consistency and IDE support +- Use `v-for` and `v-if` correctly +- Never use `v-html` with untrusted/user-provided content +- Choose `v-if` vs `v-show` based on toggle frequency and initial render cost + +## Colocate template, script, and styles + +**BAD:** +``` +components/ +├── UserCard.vue +├── UserCard.js +└── UserCard.css +``` + +**GOOD:** +```vue + + + + + + +``` + +## Use PascalCase for component names + +**BAD:** +```vue + + + +``` + +**GOOD:** +```vue + + + +``` + +## Best practices for ` +``` + +**GOOD:** + +```vue + +``` + +**GOOD:** + +```css +/* src/assets/main.css */ +/* ✅ resets, tokens, typography, app-wide rules */ +:root { --radius: 999px; } +``` + +### Use class selectors in scoped CSS + +**BAD:** +```vue + + + +``` + +**GOOD:** +```vue + + + +``` + +## Access DOM / component refs with `useTemplateRef()` + +For Vue 3.5+: use `useTemplateRef()` to access template refs. + +```vue + + + +``` + +## Use camelCase in `:style` bindings + +**BAD:** +```vue + +``` + +**GOOD:** +```vue + +``` + +## Use `v-for` and `v-if` correctly + +### Always provide a stable `:key` + +- Prefer primitive keys (`string | number`). +- Avoid using objects as keys. + +**GOOD:** + +```vue +
  • + +
  • +``` + +### Avoid `v-if` and `v-for` on the same element + +It leads to unclear intent and unnecessary work. +([Reference](https://vuejs.org/guide/essentials/list.html#v-for-with-v-if)) + +**To filter items** +**BAD:** + +```vue +
  • + {{ user.name }} +
  • +``` + +**GOOD:** + +```vue + + + +``` + +**To conditionally show/hide the entire list** +**GOOD:** + +```vue +
      +
    • + {{ user.name }} +
    • +
    +``` + +## Never render untrusted HTML with `v-html` + +**BAD:** +```vue + +``` + +**GOOD:** +```vue + + + +``` + +## Choose `v-if` vs `v-show` by toggle behavior + +**BAD:** +```vue + +``` + +**GOOD:** +```vue + +``` diff --git a/.agents/skills/vue-best-practices/references/state-management.md b/.agents/skills/vue-best-practices/references/state-management.md new file mode 100644 index 0000000..02423ab --- /dev/null +++ b/.agents/skills/vue-best-practices/references/state-management.md @@ -0,0 +1,135 @@ +--- +title: State Management Strategy +impact: HIGH +impactDescription: Choosing the wrong store pattern can cause SSR request leaks, brittle mutation flows, and poor scaling +type: best-practice +tags: [vue3, state-management, pinia, composables, ssr, vueuse] +--- + +# State Management Strategy + +**Impact: HIGH** - Use the lightest state solution that fits your app architecture. SPA-only apps can use lightweight global composables, while SSR/Nuxt apps should default to Pinia for request-safe isolation and predictable tooling. + +## Task List + +- Keep state local first, then promote to shared/global only when needed +- Use singleton composables only in non-SSR applications +- Expose global state as readonly and mutate through explicit actions +- Prefer Pinia for SSR/Nuxt, large apps, and advanced debugging/plugin needs +- Avoid exporting mutable module-level reactive state directly + +## Choose the Lightest Store Approach + +- **Feature composable:** Default for reusable logic with local/feature-level state. +- **Singleton composable or VueUse `createGlobalState`:** Small non-SSR apps needing shared app state. +- **Pinia:** SSR/Nuxt apps, medium-to-large apps, and cases requiring DevTools, plugins, or action tracing. + +## Avoid Exporting Mutable Module State + +**BAD:** +```ts +// store/cart.ts +import { reactive } from 'vue' + +export const cart = reactive({ + items: [] as Array<{ id: string; qty: number }> +}) +``` + +**GOOD:** +```ts +// composables/useCartStore.ts +import { reactive, readonly } from 'vue' + +let _store: ReturnType | null = null + +function createCartStore() { + const state = reactive({ + items: [] as Array<{ id: string; qty: number }> + }) + + function addItem(id: string, qty = 1) { + const existing = state.items.find((item) => item.id === id) + if (existing) { + existing.qty += qty + return + } + state.items.push({ id, qty }) + } + + return { + state: readonly(state), + addItem + } +} + +export function useCartStore() { + if (!_store) _store = createCartStore() + return _store +} +``` + +## Do Not Use Runtime Singletons in SSR + +Module singletons live for the runtime lifetime. In SSR this can leak state between requests. + +**BAD:** +```ts +// shared singleton reused across requests +const cartStore = useCartStore() + +export function useServerCart() { + return cartStore +} +``` + +**GOOD:** + +> `pinia` dependency required. + +```ts +// stores/cart.ts +import { defineStore } from 'pinia' + +export const useCartStore = defineStore('cart', { + state: () => ({ + items: [] as Array<{ id: string; qty: number }> + }), + actions: { + addItem(id: string, qty = 1) { + const existing = this.items.find((item) => item.id === id) + if (existing) { + existing.qty += qty + return + } + this.items.push({ id, qty }) + } + } +}) +``` + +## Use `createGlobalState` for Small SPA Global State + +> `@vueuse/core` dependency required. + +If the app is non-SSR and already uses VueUse, `createGlobalState` removes singleton boilerplate. + +```ts +import { createGlobalState } from '@vueuse/core' +import { computed, ref } from 'vue' + +export const useAuthState = createGlobalState(() => { + const token = ref(null) + const isAuthenticated = computed(() => token.value !== null) + + function setToken(next: string | null) { + token.value = next + } + + return { + token, + isAuthenticated, + setToken + } +}) +``` diff --git a/.agents/skills/vue-best-practices/references/updated-hook-performance.md b/.agents/skills/vue-best-practices/references/updated-hook-performance.md new file mode 100644 index 0000000..6375e86 --- /dev/null +++ b/.agents/skills/vue-best-practices/references/updated-hook-performance.md @@ -0,0 +1,187 @@ +--- +title: Avoid Expensive Operations in Updated Hook +impact: MEDIUM +impactDescription: Heavy computations in updated hook cause performance bottlenecks and potential infinite loops +type: capability +tags: [vue3, vue2, lifecycle, updated, performance, optimization, reactivity] +--- + +# Avoid Expensive Operations in Updated Hook + +**Impact: MEDIUM** - The `updated` hook runs after every reactive state change that causes a re-render. Placing expensive operations, API calls, or state mutations here can cause severe performance degradation, infinite loops, and dropped frames below the optimal 60fps threshold. + +Use `updated`/`onUpdated` sparingly for post-DOM-update operations that cannot be handled by watchers or computed properties. For most reactive data handling, prefer watchers (`watch`/`watchEffect`) which provide more control over what triggers the callback. + +## Task List + +- Never perform API calls in updated hook +- Never mutate reactive state inside updated (causes infinite loops) +- Use conditional checks to verify updates are relevant before acting +- Prefer `watch` or `watchEffect` for reacting to specific data changes +- Use throttling/debouncing if updated operations are expensive +- Reserve updated for low-level DOM synchronization tasks + +**BAD:** +```javascript +// BAD: API call in updated - fires on every re-render +export default { + data() { + return { items: [], lastUpdate: null } + }, + updated() { + // This runs after every single state change! + fetch('/api/sync', { + method: 'POST', + body: JSON.stringify(this.items) + }) + } +} +``` + +```javascript +// BAD: State mutation in updated - infinite loop +export default { + data() { + return { renderCount: 0 } + }, + updated() { + // This causes another update, which triggers updated again! + this.renderCount++ // Infinite loop + } +} +``` + +```javascript +// BAD: Heavy computation on every update +export default { + updated() { + // Expensive operation runs on every keystroke, every state change + this.processedData = this.heavyComputation(this.rawData) + this.analytics = this.calculateMetrics(this.allData) + } +} +``` + +**GOOD:** +```javascript +import debounce from 'lodash-es/debounce' + +// GOOD: Use watcher for specific data changes +export default { + data() { + return { items: [] } + }, + watch: { + // Only fires when items actually changes + items: { + handler(newItems) { + this.syncToServer(newItems) + }, + deep: true + } + }, + methods: { + syncToServer: debounce(function(items) { + fetch('/api/sync', { + method: 'POST', + body: JSON.stringify(items) + }) + }, 500) + } +} +``` + +```vue + + +``` + +```javascript +// GOOD: Conditional check in updated hook +export default { + data() { + return { + content: '', + lastSyncedContent: '' + } + }, + updated() { + // Only act if specific condition is met + if (this.content !== this.lastSyncedContent) { + this.syncContent() + this.lastSyncedContent = this.content + } + }, + methods: { + syncContent: debounce(function() { + // Sync logic + }, 300) + } +} +``` + +## Valid Use Cases for Updated Hook + +```javascript +// GOOD: Low-level DOM synchronization +export default { + updated() { + // Sync third-party library with Vue's DOM + this.thirdPartyWidget.refresh() + + // Update scroll position after content change + this.$nextTick(() => { + this.maintainScrollPosition() + }) + } +} +``` + +## Prefer Computed Properties for Derived Data + +```javascript +// BAD: Calculating derived data in updated +export default { + data() { + return { numbers: [1, 2, 3, 4, 5] } + }, + updated() { + this.sum = this.numbers.reduce((a, b) => a + b, 0) // Causes another update! + } +} + +// GOOD: Use computed property instead +export default { + data() { + return { numbers: [1, 2, 3, 4, 5] } + }, + computed: { + sum() { + return this.numbers.reduce((a, b) => a + b, 0) + } + } +} +``` diff --git a/.agents/skills/vue-router-best-practices/LICENSE.md b/.agents/skills/vue-router-best-practices/LICENSE.md new file mode 100644 index 0000000..3f08a54 --- /dev/null +++ b/.agents/skills/vue-router-best-practices/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 hyf0, SerKo + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/.agents/skills/vue-router-best-practices/SKILL.md b/.agents/skills/vue-router-best-practices/SKILL.md new file mode 100644 index 0000000..6488d42 --- /dev/null +++ b/.agents/skills/vue-router-best-practices/SKILL.md @@ -0,0 +1,23 @@ +--- +name: vue-router-best-practices +description: "Vue Router 4 patterns, navigation guards, route params, and route-component lifecycle interactions." +version: 1.0.0 +license: MIT +author: github.com/vuejs-ai +--- + +Vue Router best practices, common gotchas, and navigation patterns. + +### Navigation Guards +- Navigating between same route with different params → See [router-beforeenter-no-param-trigger](reference/router-beforeenter-no-param-trigger.md) +- Accessing component instance in beforeRouteEnter guard → See [router-beforerouteenter-no-this](reference/router-beforerouteenter-no-this.md) +- Navigation guard making API calls without awaiting → See [router-guard-async-await-pattern](reference/router-guard-async-await-pattern.md) +- Users trapped in infinite redirect loops → See [router-navigation-guard-infinite-loop](reference/router-navigation-guard-infinite-loop.md) +- Navigation guard using deprecated next() function → See [router-navigation-guard-next-deprecated](reference/router-navigation-guard-next-deprecated.md) + +### Route Lifecycle +- Stale data when navigating between same route → See [router-param-change-no-lifecycle](reference/router-param-change-no-lifecycle.md) +- Event listeners persisting after component unmounts → See [router-simple-routing-cleanup](reference/router-simple-routing-cleanup.md) + +### Setup +- Building production single-page application → See [router-use-vue-router-for-production](reference/router-use-vue-router-for-production.md) diff --git a/.agents/skills/vue-router-best-practices/SYNC.md b/.agents/skills/vue-router-best-practices/SYNC.md new file mode 100644 index 0000000..c549070 --- /dev/null +++ b/.agents/skills/vue-router-best-practices/SYNC.md @@ -0,0 +1,5 @@ +# Sync Info + +- **Source:** `vendor/vuejs-ai/skills/vue-router-best-practices` +- **Git SHA:** `f3dd1bf4d3ac78331bdc903e4519d561c538ca6a` +- **Synced:** 2026-03-16 diff --git a/.agents/skills/vue-router-best-practices/reference/router-beforeenter-no-param-trigger.md b/.agents/skills/vue-router-best-practices/reference/router-beforeenter-no-param-trigger.md new file mode 100644 index 0000000..1693a13 --- /dev/null +++ b/.agents/skills/vue-router-best-practices/reference/router-beforeenter-no-param-trigger.md @@ -0,0 +1,167 @@ +--- +title: Per-Route beforeEnter Guards Ignore Param/Query Changes +impact: MEDIUM +impactDescription: Route-level beforeEnter guards don't fire when only params, query, or hash change, causing unexpected bypasses of validation logic +type: gotcha +tags: [vue3, vue-router, navigation-guards, params, query] +--- + +# Per-Route beforeEnter Guards Ignore Param/Query Changes + +**Impact: MEDIUM** - The `beforeEnter` guard defined in route configuration only triggers when entering a route from a DIFFERENT route. Changes to params, query strings, or hash within the same route do NOT trigger `beforeEnter`, potentially bypassing important validation logic. + +## Task Checklist + +- [ ] Use in-component `onBeforeRouteUpdate` for param/query changes +- [ ] Or use global `beforeEach` with route.params/query checks +- [ ] Document which guards protect which scenarios +- [ ] Test navigation between same route with different params + +## The Problem + +```javascript +// router.js +const routes = [ + { + path: '/orders/:id', + component: OrderDetail, + beforeEnter: async (to, from) => { + // This runs when entering from /products + // But NOT when navigating from /orders/1 to /orders/2! + const order = await checkOrderAccess(to.params.id) + if (!order.canView) { + return '/unauthorized' + } + } + } +] +``` + +**Scenario:** +1. User navigates from `/products` to `/orders/1` - beforeEnter runs, access checked +2. User navigates from `/orders/1` to `/orders/2` - beforeEnter DOES NOT run! +3. User might access order they don't have permission for! + +## What Triggers beforeEnter vs. What Doesn't + +| Navigation | beforeEnter fires? | +|------------|-------------------| +| `/products` → `/orders/1` | YES | +| `/orders/1` → `/orders/2` | NO | +| `/orders/1` → `/orders/1?tab=details` | NO | +| `/orders/1#section` → `/orders/1#other` | NO | +| `/orders/1` → `/products` → `/orders/2` | YES (leaving and re-entering) | + +## Solution 1: Add In-Component Guard + +```vue + + +``` + +## Solution 2: Use Global beforeEach Instead + +```javascript +// router.js +router.beforeEach(async (to, from) => { + // Handle all order access checks globally + if (to.name === 'OrderDetail') { + // This runs on EVERY navigation to this route, including param changes + const order = await checkOrderAccess(to.params.id) + if (!order.canView) { + return '/unauthorized' + } + } +}) +``` + +## Solution 3: Combine Both Guards + +```javascript +// router.js - For entering from different route +const routes = [ + { + path: '/orders/:id', + component: OrderDetail, + beforeEnter: (to) => validateOrderAccess(to.params.id) + } +] + +// In component - For param changes within route +// OrderDetail.vue +onBeforeRouteUpdate((to) => validateOrderAccess(to.params.id)) + +// Shared validation function +async function validateOrderAccess(orderId) { + const order = await checkOrderAccess(orderId) + if (!order.canView) { + return '/unauthorized' + } +} +``` + +## Solution 4: Use beforeEnter with Array of Guards + +```javascript +// guards/orderGuards.js +export const orderAccessGuard = async (to) => { + const order = await checkOrderAccess(to.params.id) + if (!order.canView) { + return '/unauthorized' + } +} + +// router.js +const routes = [ + { + path: '/orders/:id', + component: OrderDetail, + beforeEnter: [orderAccessGuard] // Can add multiple guards + } +] + +// Still need in-component guard for param changes! +``` + +## Full Navigation Guard Execution Order + +Understanding when each guard type fires: + +``` +1. beforeRouteLeave (in-component, leaving component) +2. beforeEach (global) +3. beforeEnter (per-route, ONLY when entering from different route) +4. beforeRouteEnter (in-component, entering component) +5. beforeResolve (global) +6. afterEach (global, after navigation confirmed) + +For param/query changes on same route: +1. beforeRouteUpdate (in-component) - ONLY this fires! +2. beforeEach (global) +3. beforeResolve (global) +4. afterEach (global) +``` + +## Key Points + +1. **beforeEnter is for route ENTRY only** - Not for within-route changes +2. **Use onBeforeRouteUpdate for param changes** - This is the in-component solution +3. **Global beforeEach always runs** - Good for centralized validation +4. **Test param change scenarios** - Easy to miss during development +5. **Consider security implications** - Param-based access control needs both guards + +## Reference +- [Vue Router Navigation Guards](https://router.vuejs.org/guide/advanced/navigation-guards.html) +- [Vue Router Per-Route Guards](https://router.vuejs.org/guide/advanced/navigation-guards.html#per-route-guard) diff --git a/.agents/skills/vue-router-best-practices/reference/router-beforerouteenter-no-this.md b/.agents/skills/vue-router-best-practices/reference/router-beforerouteenter-no-this.md new file mode 100644 index 0000000..795d6ee --- /dev/null +++ b/.agents/skills/vue-router-best-practices/reference/router-beforerouteenter-no-this.md @@ -0,0 +1,176 @@ +--- +title: beforeRouteEnter Cannot Access Component Instance +impact: MEDIUM +impactDescription: The beforeRouteEnter guard runs before component creation, so 'this' is undefined; use the next callback to access the instance +type: gotcha +tags: [vue3, vue-router, navigation-guards, lifecycle, this] +--- + +# beforeRouteEnter Cannot Access Component Instance + +**Impact: MEDIUM** - The `beforeRouteEnter` in-component navigation guard executes BEFORE the component is created, meaning you cannot access `this` or any component instance properties. This is the ONLY navigation guard that supports a callback in the `next()` function to access the component instance after navigation. + +## Task Checklist + +- [ ] Use next(vm => ...) callback to access component instance +- [ ] Or use composition API guards which have different patterns +- [ ] Move data fetching logic appropriately based on timing needs +- [ ] Consider using global guards for data that doesn't need component access + +## The Problem + +```javascript +// Options API - WRONG: this is undefined +export default { + data() { + return { user: null } + }, + beforeRouteEnter(to, from, next) { + // BUG: this is undefined here - component doesn't exist yet! + this.user = await fetchUser(to.params.id) // ERROR! + next() + } +} +``` + +## Solution: Use next() Callback (Options API) + +```javascript +// Options API - CORRECT: Use callback to access vm +export default { + data() { + return { + user: null, + loading: true + } + }, + + beforeRouteEnter(to, from, next) { + // Fetch data before component exists + fetchUser(to.params.id) + .then(user => { + // Pass callback to next() - receives component instance as 'vm' + next(vm => { + vm.user = user + vm.loading = false + }) + }) + .catch(error => { + next(vm => { + vm.error = error + vm.loading = false + }) + }) + } +} +``` + +## Solution: Async beforeRouteEnter (Options API) + +```javascript +export default { + data() { + return { userData: null } + }, + + async beforeRouteEnter(to, from, next) { + try { + const user = await fetchUser(to.params.id) + + // Still need callback for component access + next(vm => { + vm.userData = user + }) + } catch (error) { + // Redirect on error + next('/error') + } + } +} +``` + +## Composition API Alternative + +In Composition API with ` +``` + +## Route-Level Data Fetching + +For data that should load BEFORE navigation, use route-level guards: + +```javascript +// router.js +const routes = [ + { + path: '/users/:id', + component: () => import('./UserProfile.vue'), + beforeEnter: async (to, from) => { + try { + // Store data for component to access + const user = await fetchUser(to.params.id) + to.meta.user = user // Attach to route meta + } catch (error) { + return '/error' + } + } + } +] +``` + +```vue + + +``` + +## Comparison of Navigation Guards + +| Guard | Has `this`/component? | Can delay navigation? | Use case | +|-------|----------------------|----------------------|----------| +| beforeRouteEnter | NO (use next callback) | YES | Pre-fetch, redirect if data missing | +| beforeRouteUpdate | YES | YES | React to param changes | +| beforeRouteLeave | YES | YES | Unsaved changes warning | +| Global beforeEach | NO | YES | Auth checks | +| Route beforeEnter | NO | YES | Route-specific validation | + +## Key Points + +1. **beforeRouteEnter runs before component creation** - No access to `this` +2. **Use next(vm => ...) callback** - Only way to access component instance +3. **Composition API has limitations** - Use onMounted or global guards instead +4. **Consider route meta for pre-fetched data** - Clean separation of concerns +5. **beforeRouteUpdate and beforeRouteLeave have component access** - They run when component exists + +## Reference +- [Vue Router In-Component Guards](https://router.vuejs.org/guide/advanced/navigation-guards.html#in-component-guards) +- [Vue Router Navigation Resolution Flow](https://router.vuejs.org/guide/advanced/navigation-guards.html#the-full-navigation-resolution-flow) diff --git a/.agents/skills/vue-router-best-practices/reference/router-guard-async-await-pattern.md b/.agents/skills/vue-router-best-practices/reference/router-guard-async-await-pattern.md new file mode 100644 index 0000000..185dcda --- /dev/null +++ b/.agents/skills/vue-router-best-practices/reference/router-guard-async-await-pattern.md @@ -0,0 +1,227 @@ +--- +title: Async Navigation Guards Require Proper Promise Handling +impact: MEDIUM +impactDescription: Unawaited promises in guards cause navigation to complete before async checks finish, allowing unauthorized access or missing data +type: gotcha +tags: [vue3, vue-router, navigation-guards, async, promises] +--- + +# Async Navigation Guards Require Proper Promise Handling + +**Impact: MEDIUM** - Navigation guards that perform async operations (API calls, auth checks) must properly handle promises. If you don't await async operations or return the promise, navigation completes before your check finishes, potentially allowing unauthorized access or navigating with incomplete data. + +## Task Checklist + +- [ ] Use async/await in navigation guards +- [ ] Return the promise if not using async/await +- [ ] Add loading states for long async operations +- [ ] Implement timeouts for slow API calls +- [ ] Handle errors to prevent navigation hanging + +## The Problem + +```javascript +// WRONG: Not awaiting - navigation proceeds immediately +router.beforeEach((to, from) => { + if (to.meta.requiresAuth) { + checkAuth() // This returns a Promise but we're not waiting! + // Navigation continues before checkAuth completes + } +}) + +// WRONG: Async function but forgot return +router.beforeEach(async (to, from) => { + if (to.meta.requiresAuth) { + const isValid = await checkAuth() + if (!isValid) { + // This redirect might happen after navigation already completed! + return '/login' + } + } + // Missing return - implicitly returns undefined, allowing navigation +}) +``` + +## Solution: Proper Async/Await Pattern + +```javascript +// CORRECT: Async function with proper returns +router.beforeEach(async (to, from) => { + if (to.meta.requiresAuth) { + try { + const isAuthenticated = await checkAuth() + + if (!isAuthenticated) { + return { name: 'Login', query: { redirect: to.fullPath } } + } + } catch (error) { + console.error('Auth check failed:', error) + return { name: 'Error', params: { message: 'Authentication failed' } } + } + } + // Explicitly return nothing to proceed + return true +}) +``` + +## Solution: Promise-Based Pattern (Alternative) + +```javascript +// CORRECT: Return promise explicitly +router.beforeEach((to, from) => { + if (to.meta.requiresAuth) { + return checkAuth() + .then(isAuthenticated => { + if (!isAuthenticated) { + return { name: 'Login' } + } + }) + .catch(error => { + console.error('Auth check failed:', error) + return { name: 'Error' } + }) + } +}) +``` + +## Loading State During Async Guards + +```javascript +// app/composables/useNavigationLoading.js +import { ref } from 'vue' + +const isNavigating = ref(false) + +export function useNavigationLoading() { + return { isNavigating } +} + +export function setupNavigationLoading(router) { + router.beforeEach(() => { + isNavigating.value = true + }) + + router.afterEach(() => { + isNavigating.value = false + }) + + router.onError(() => { + isNavigating.value = false + }) +} +``` + +```vue + + + + +``` + +## Timeout Pattern for Slow APIs + +```javascript +// CORRECT: Add timeout to prevent indefinite waiting +function withTimeout(promise, ms = 5000) { + return Promise.race([ + promise, + new Promise((_, reject) => + setTimeout(() => reject(new Error('Request timeout')), ms) + ) + ]) +} + +router.beforeEach(async (to, from) => { + if (to.meta.requiresAuth) { + try { + const isValid = await withTimeout(checkAuth(), 5000) + if (!isValid) { + return '/login' + } + } catch (error) { + if (error.message === 'Request timeout') { + // Let user through but show warning + console.warn('Auth check timed out') + } else { + return '/login' + } + } + } +}) +``` + +## Multiple Async Checks + +```javascript +// CORRECT: Run independent checks in parallel +router.beforeEach(async (to, from) => { + if (to.meta.requiresAuth && to.meta.requiresSubscription) { + try { + const [isAuthenticated, hasSubscription] = await Promise.all([ + checkAuth(), + checkSubscription() + ]) + + if (!isAuthenticated) { + return '/login' + } + + if (!hasSubscription) { + return '/subscribe' + } + } catch (error) { + return '/error' + } + } +}) +``` + +## Error Handling Best Practices + +```javascript +router.beforeEach(async (to, from) => { + try { + // Your async logic here + await performChecks(to) + } catch (error) { + // Always handle errors to prevent navigation from hanging + + if (error.response?.status === 401) { + return '/login' + } + + if (error.response?.status === 403) { + return '/forbidden' + } + + if (error.code === 'NETWORK_ERROR') { + // Offline - maybe allow navigation but show warning + return true + } + + // Unknown error - redirect to error page + console.error('Navigation guard error:', error) + return { name: 'Error', state: { error: error.message } } + } +}) +``` + +## Key Points + +1. **Always await async operations** - Otherwise navigation proceeds immediately +2. **Return values matter** - Return route to redirect, false to cancel, true/undefined to proceed +3. **Handle all error cases** - Uncaught errors can hang navigation +4. **Add timeouts** - Slow APIs shouldn't block navigation indefinitely +5. **Show loading state** - Users need feedback during async checks +6. **Parallelize independent checks** - Use Promise.all for better performance + +## Reference +- [Vue Router Navigation Guards](https://router.vuejs.org/guide/advanced/navigation-guards.html) +- [Vue Router Navigation Failures](https://router.vuejs.org/guide/advanced/navigation-failures.html) diff --git a/.agents/skills/vue-router-best-practices/reference/router-navigation-guard-infinite-loop.md b/.agents/skills/vue-router-best-practices/reference/router-navigation-guard-infinite-loop.md new file mode 100644 index 0000000..38bb577 --- /dev/null +++ b/.agents/skills/vue-router-best-practices/reference/router-navigation-guard-infinite-loop.md @@ -0,0 +1,187 @@ +--- +title: Navigation Guard Infinite Redirect Loops +impact: HIGH +impactDescription: Misconfigured navigation guards can trap users in infinite redirect loops, crashing the browser or making the app unusable +type: gotcha +tags: [vue3, vue-router, navigation-guards, redirect, debugging] +--- + +# Navigation Guard Infinite Redirect Loops + +**Impact: HIGH** - A common mistake in navigation guards is creating conditions that cause infinite redirects. Vue Router will detect this and show a warning, but in production, it can crash the browser or create a broken user experience. + +## Task Checklist + +- [ ] Always check if already on target route before redirecting +- [ ] Test guard logic with all possible navigation scenarios +- [ ] Add route meta to control which routes need protection +- [ ] Use Vue Router devtools to debug redirect chains + +## The Problem + +```javascript +// WRONG: Infinite loop - always redirects to login, even when on login! +router.beforeEach((to, from) => { + if (!isAuthenticated()) { + return '/login' // Redirects to /login, which triggers guard again... + } +}) + +// WRONG: Circular redirect between two routes +router.beforeEach((to, from) => { + if (to.path === '/dashboard' && !hasProfile()) { + return '/profile' + } + if (to.path === '/profile' && !isVerified()) { + return '/dashboard' // Back to dashboard, which goes to profile... + } +}) +``` + +**Error you'll see:** +``` +[Vue Router warn]: Detected an infinite redirection in a navigation guard when going from "/" to "/login". Aborting to avoid a Stack Overflow. +``` + +## Solution 1: Exclude Target Route + +```javascript +// CORRECT: Don't redirect if already going to login +router.beforeEach((to, from) => { + if (!isAuthenticated() && to.path !== '/login') { + return '/login' + } +}) + +// CORRECT: Use route name for cleaner check +router.beforeEach((to, from) => { + const publicPages = ['Login', 'Register', 'ForgotPassword'] + + if (!isAuthenticated() && !publicPages.includes(to.name)) { + return { name: 'Login' } + } +}) +``` + +## Solution 2: Use Route Meta Fields + +```javascript +// router.js +const routes = [ + { + path: '/login', + name: 'Login', + component: Login, + meta: { requiresAuth: false } + }, + { + path: '/dashboard', + name: 'Dashboard', + component: Dashboard, + meta: { requiresAuth: true } + }, + { + path: '/public', + name: 'PublicPage', + component: PublicPage, + meta: { requiresAuth: false } + } +] + +// Guard checks meta field +router.beforeEach((to, from) => { + // Only redirect if route requires auth + if (to.meta.requiresAuth && !isAuthenticated()) { + return { name: 'Login', query: { redirect: to.fullPath } } + } +}) +``` + +## Solution 3: Handle Redirect Chains Carefully + +```javascript +// CORRECT: Break potential circular redirects +router.beforeEach((to, from) => { + // Prevent redirect loops by tracking redirect depth + const redirectCount = to.query._redirectCount || 0 + + if (redirectCount > 3) { + console.error('Too many redirects, stopping at:', to.path) + return '/error' // Escape hatch + } + + if (needsRedirect(to)) { + return { + path: getRedirectTarget(to), + query: { ...to.query, _redirectCount: redirectCount + 1 } + } + } +}) +``` + +## Solution 4: Centralized Redirect Logic + +```javascript +// guards/auth.js +export function createAuthGuard(router) { + const publicRoutes = new Set(['Login', 'Register', 'ForgotPassword', 'ResetPassword']) + const guestOnlyRoutes = new Set(['Login', 'Register']) + + router.beforeEach((to, from) => { + const isPublic = publicRoutes.has(to.name) + const isGuestOnly = guestOnlyRoutes.has(to.name) + const isLoggedIn = isAuthenticated() + + // Not logged in, trying to access protected route + if (!isLoggedIn && !isPublic) { + return { name: 'Login', query: { redirect: to.fullPath } } + } + + // Logged in, trying to access guest-only route (like login page) + if (isLoggedIn && isGuestOnly) { + return { name: 'Dashboard' } + } + + // All other cases: proceed + }) +} +``` + +## Debugging Redirect Loops + +```javascript +// Add logging to understand the redirect chain +router.beforeEach((to, from) => { + console.log(`Navigation: ${from.path} -> ${to.path}`) + console.log('Auth state:', isAuthenticated()) + console.log('Route meta:', to.meta) + + // Your guard logic here +}) + +// Or use afterEach for confirmed navigations +router.afterEach((to, from) => { + console.log(`Navigated: ${from.path} -> ${to.path}`) +}) +``` + +## Common Redirect Loop Patterns + +| Pattern | Problem | Fix | +|---------|---------|-----| +| Auth check without exclusion | Login redirects to login | Exclude `/login` from check | +| Role-based with circular deps | Admin -> User -> Admin | Use single source of truth for role requirements | +| Onboarding flow | Step 1 -> Step 2 -> Step 1 | Track completion state properly | +| Redirect query handling | Reading redirect creates new redirect | Process redirect only once | + +## Key Points + +1. **Always exclude the target route** - Never redirect to a route that would trigger the same redirect +2. **Use route meta fields** - Cleaner than path string comparisons +3. **Test edge cases** - Direct URL access, refresh, back button +4. **Add logging during development** - Helps trace redirect chains +5. **Have an escape hatch** - Error page or max redirect count + +## Reference +- [Vue Router Navigation Guards](https://router.vuejs.org/guide/advanced/navigation-guards.html) +- [Vue Router Route Meta Fields](https://router.vuejs.org/guide/advanced/meta.html) diff --git a/.agents/skills/vue-router-best-practices/reference/router-navigation-guard-next-deprecated.md b/.agents/skills/vue-router-best-practices/reference/router-navigation-guard-next-deprecated.md new file mode 100644 index 0000000..9cabf03 --- /dev/null +++ b/.agents/skills/vue-router-best-practices/reference/router-navigation-guard-next-deprecated.md @@ -0,0 +1,150 @@ +--- +title: Vue Router Navigation Guard next() Function Deprecated +impact: HIGH +impactDescription: Using the deprecated next() function incorrectly causes navigation to hang, infinite loops, or silent failures +type: gotcha +tags: [vue3, vue-router, navigation-guards, migration, async] +--- + +# Vue Router Navigation Guard next() Function Deprecated + +**Impact: HIGH** - The third `next()` argument in navigation guards is deprecated in Vue Router 4. While still supported for backward compatibility, using it incorrectly is one of the most common sources of bugs: calling it multiple times, forgetting to call it, or calling it conditionally without proper logic. + +## Task Checklist + +- [ ] Refactor guards to use return-based syntax instead of next() +- [ ] Remove all next() calls from navigation guards +- [ ] Use async/await pattern for asynchronous checks +- [ ] Return false to cancel, return route to redirect, return nothing to proceed + +## The Problem + +```javascript +// WRONG: Using deprecated next() function +router.beforeEach((to, from, next) => { + if (!isAuthenticated) { + next('/login') // Easy to forget this call + } + // BUG: next() not called when authenticated - navigation hangs! +}) + +// WRONG: Multiple next() calls +router.beforeEach((to, from, next) => { + if (!isAuthenticated) { + next('/login') + } + next() // BUG: Called twice when not authenticated! +}) + +// WRONG: next() in async code without proper handling +router.beforeEach(async (to, from, next) => { + const user = await fetchUser() + if (!user) { + next('/login') + } + next() // Still gets called even after redirect! +}) +``` + +## Solution: Use Return-Based Guards + +```javascript +// CORRECT: Return-based syntax (modern Vue Router 4+) +router.beforeEach((to, from) => { + if (!isAuthenticated) { + return '/login' // Redirect + } + // Return nothing (undefined) to proceed +}) + +// CORRECT: Return false to cancel navigation +router.beforeEach((to, from) => { + if (hasUnsavedChanges) { + return false // Cancel navigation + } +}) + +// CORRECT: Async with return-based syntax +router.beforeEach(async (to, from) => { + const user = await fetchUser() + if (!user) { + return { name: 'Login', query: { redirect: to.fullPath } } + } + // Proceed with navigation +}) +``` + +## Return Values Explained + +```javascript +router.beforeEach((to, from) => { + // Return nothing/undefined - allow navigation + return + + // Return false - cancel navigation, stay on current route + return false + + // Return string path - redirect to path + return '/login' + + // Return route object - redirect with full control + return { name: 'Login', query: { redirect: to.fullPath } } + + // Return Error - cancel and trigger router.onError() + return new Error('Navigation cancelled') +}) +``` + +## If You Must Use next() (Legacy Code) + +If maintaining legacy code that uses `next()`, follow these rules strictly: + +```javascript +// CORRECT: Exactly one next() call per code path +router.beforeEach((to, from, next) => { + if (!isAuthenticated) { + next('/login') + return // CRITICAL: Exit after calling next() + } + + if (!hasPermission(to)) { + next('/forbidden') + return // CRITICAL: Exit after calling next() + } + + next() // Only reached if all checks pass +}) +``` + +## Error Handling Pattern + +```javascript +router.beforeEach(async (to, from) => { + try { + await validateAccess(to) + // Proceed + } catch (error) { + if (error.status === 401) { + return '/login' + } + if (error.status === 403) { + return '/forbidden' + } + // Log error and proceed anyway (or return false) + console.error('Access validation failed:', error) + return false + } +}) +``` + +## Key Points + +1. **Prefer return-based syntax** - Cleaner, less error-prone, modern standard +2. **next() must be called exactly once** - If using legacy syntax, ensure single call per path +3. **Always return/exit after redirect** - Prevent multiple navigation actions +4. **Async guards work naturally** - Just return the redirect route or nothing +5. **Test all code paths** - Each branch must result in either return or next() + +## Reference +- [Vue Router Navigation Guards](https://router.vuejs.org/guide/advanced/navigation-guards.html) +- [RFC: Remove next() from Navigation Guards](https://github.com/vuejs/rfcs/discussions/302) diff --git a/.agents/skills/vue-router-best-practices/reference/router-param-change-no-lifecycle.md b/.agents/skills/vue-router-best-practices/reference/router-param-change-no-lifecycle.md new file mode 100644 index 0000000..12457d9 --- /dev/null +++ b/.agents/skills/vue-router-best-practices/reference/router-param-change-no-lifecycle.md @@ -0,0 +1,181 @@ +--- +title: Route Param Changes Do Not Trigger Lifecycle Hooks +impact: HIGH +impactDescription: Navigating between routes with different params reuses the component instance, skipping created/mounted hooks and leaving stale data +type: gotcha +tags: [vue3, vue-router, lifecycle, params, reactivity] +--- + +# Route Param Changes Do Not Trigger Lifecycle Hooks + +**Impact: HIGH** - When navigating between routes that use the same component (e.g., `/users/1` to `/users/2`), Vue Router reuses the existing component instance for performance. This means `onMounted`, `created`, and other lifecycle hooks do NOT fire, leaving you with stale data from the previous route. + +## Task Checklist + +- [ ] Use `watch` on route params for data fetching +- [ ] Or use `onBeforeRouteUpdate` in-component guard +- [ ] Or use `:key="route.params.id"` to force re-creation (less efficient) +- [ ] Never rely solely on `onMounted` for route-param-dependent data + +## The Problem + +```vue + + + + +``` + +**Scenario:** +1. Visit `/users/1` - Component mounts, fetches User 1 data +2. Navigate to `/users/2` - Component is REUSED, onMounted doesn't run +3. UI still shows User 1's data! + +## Solution 1: Watch Route Params (Recommended) + +```vue + +``` + +## Solution 2: Use onBeforeRouteUpdate Guard + +```vue + +``` + +## Solution 3: Force Component Re-creation with Key + +```vue + + +``` + +**Tradeoffs:** +- Simple but less performant +- Destroys and recreates component on every param change +- Loses component state +- Use only when component state should reset completely + +## Solution 4: Composable for Route-Reactive Data + +```javascript +// composables/useRouteData.js +import { ref, watch } from 'vue' +import { useRoute } from 'vue-router' + +export function useRouteData(paramName, fetcher) { + const route = useRoute() + const data = ref(null) + const loading = ref(false) + const error = ref(null) + + watch( + () => route.params[paramName], + async (id) => { + if (!id) return + + loading.value = true + error.value = null + + try { + data.value = await fetcher(id) + } catch (e) { + error.value = e + } finally { + loading.value = false + } + }, + { immediate: true } + ) + + return { data, loading, error } +} +``` + +```vue + + +``` + +## What Triggers vs. What Doesn't + +| Navigation Type | Lifecycle Hooks | beforeRouteUpdate | Watch on params | +|----------------|-----------------|-------------------|-----------------| +| `/users/1` to `/posts/1` | YES | NO | YES | +| `/users/1` to `/users/2` | NO | YES | YES | +| `/users/1?tab=a` to `/users/1?tab=b` | NO | YES | NO (different watch) | +| `/users/1` to `/users/1` (same) | NO | NO | NO | + +## Key Points + +1. **Same route, different params = same component instance** - This is a performance optimization +2. **Lifecycle hooks only fire once** - When component first mounts +3. **Use `watch` with `immediate: true`** - Covers both initial load and updates +4. **`onBeforeRouteUpdate` is navigation-aware** - Good for data that must load before view updates +5. **`:key="route.fullPath"` is a sledgehammer** - Use only when necessary + +## Reference +- [Vue Router Dynamic Route Matching](https://router.vuejs.org/guide/essentials/dynamic-matching.html#reacting-to-params-changes) +- [Vue School: Reacting to Param Changes](https://vueschool.io/lessons/reacting-to-param-changes) diff --git a/.agents/skills/vue-router-best-practices/reference/router-simple-routing-cleanup.md b/.agents/skills/vue-router-best-practices/reference/router-simple-routing-cleanup.md new file mode 100644 index 0000000..ef31bef --- /dev/null +++ b/.agents/skills/vue-router-best-practices/reference/router-simple-routing-cleanup.md @@ -0,0 +1,209 @@ +--- +title: Simple Hash Routing Requires Event Listener Cleanup +impact: MEDIUM +impactDescription: When implementing basic routing without Vue Router, forgetting to remove hashchange listeners causes memory leaks and multiple handler execution +type: gotcha +tags: [vue3, routing, events, memory-leak, cleanup] +--- + +# Simple Hash Routing Requires Event Listener Cleanup + +**Impact: MEDIUM** - When implementing basic client-side routing without Vue Router (using hash-based routing with `hashchange` events), you must clean up event listeners when the component unmounts. Failure to do so causes memory leaks and can result in multiple handlers firing after the component is recreated. + +## Task Checklist + +- [ ] Store event listener reference for cleanup +- [ ] Use onUnmounted to remove event listener +- [ ] Consider using Vue Router instead for production apps +- [ ] Test component mount/unmount cycles + +## The Problem + +```vue + +``` + +**What happens:** +1. Component mounts, adds listener +2. Component unmounts (e.g., route change, v-if toggle) +3. Component mounts again, adds ANOTHER listener +4. Now TWO listeners respond to each hash change +5. Eventually causes performance issues and memory leaks + +## Solution: Proper Cleanup with onUnmounted + +```vue + +``` + +## Solution: Using Options API + +```vue + +``` + +## Solution: Composable for Reusable Hash Routing + +```javascript +// composables/useHashRouter.js +import { ref, computed, onUnmounted } from 'vue' + +export function useHashRouter(routes, notFoundComponent = null) { + const currentPath = ref(window.location.hash) + + function handleHashChange() { + currentPath.value = window.location.hash + } + + // Setup + window.addEventListener('hashchange', handleHashChange) + + // Cleanup - handled automatically when component unmounts + onUnmounted(() => { + window.removeEventListener('hashchange', handleHashChange) + }) + + const currentView = computed(() => { + const path = currentPath.value.slice(1) || '/' + return routes[path] || notFoundComponent + }) + + function navigate(path) { + window.location.hash = path + } + + return { + currentPath, + currentView, + navigate + } +} +``` + +```vue + + + + +``` + +## When to Use Simple Routing vs Vue Router + +| Use Simple Hash Routing | Use Vue Router | +|------------------------|----------------| +| Learning/prototyping | Production apps | +| Very simple apps (2-3 pages) | Nested routes needed | +| No build step available | Navigation guards needed | +| Bundle size critical | Lazy loading needed | +| Static hosting only | History mode (clean URLs) | + +## Key Points + +1. **Always clean up event listeners** - Use onUnmounted or beforeUnmount +2. **Store handler reference** - Anonymous functions can't be removed +3. **Consider Vue Router for real apps** - It handles cleanup automatically +4. **Test unmount scenarios** - v-if toggling, hot module replacement +5. **Composables help encapsulate cleanup logic** - Reusable and automatic + +## Reference +- [Vue.js Routing Documentation](https://vuejs.org/guide/scaling-up/routing.html) +- [Vue Router Official Library](https://router.vuejs.org/) diff --git a/.agents/skills/vue-router-best-practices/reference/router-use-vue-router-for-production.md b/.agents/skills/vue-router-best-practices/reference/router-use-vue-router-for-production.md new file mode 100644 index 0000000..1e60304 --- /dev/null +++ b/.agents/skills/vue-router-best-practices/reference/router-use-vue-router-for-production.md @@ -0,0 +1,183 @@ +--- +title: Use Vue Router Library for Production Applications +impact: LOW +impactDescription: Simple hash routing lacks essential features for production SPAs; Vue Router provides navigation guards, lazy loading, and proper history management +type: best-practice +tags: [vue3, vue-router, spa, production, architecture] +--- + +# Use Vue Router Library for Production Applications + +**Impact: LOW** - While you can implement basic routing with hash changes and dynamic components, the official Vue Router library should be used for any production single-page application. It provides essential features like navigation guards, nested routes, lazy loading, and proper browser history integration that are tedious and error-prone to implement manually. + +## Task Checklist + +- [ ] Install Vue Router for production SPAs +- [ ] Use simple routing only for learning or tiny prototypes +- [ ] Leverage built-in features: guards, lazy loading, meta fields +- [ ] Consider router-based state and data loading patterns + +## When Simple Routing is Acceptable + +```vue + + + + +``` + +## Why Vue Router for Production + +### Features You'd Have to Implement Manually + +| Feature | Simple Routing | Vue Router | +|---------|---------------|------------| +| Navigation guards | Manual, error-prone | Built-in, composable | +| Nested routes | Complex to implement | Native support | +| Route params | Parse manually | Automatic extraction | +| Lazy loading | DIY with dynamic imports | Built-in with code splitting | +| History mode (clean URLs) | Requires server config + manual | Built-in | +| Scroll behavior | Manual | Configurable | +| Route transitions | DIY | Integrated with Transition | +| Active link styling | Manual class toggling | `router-link-active` class | +| Programmatic navigation | `location.hash = ...` | `router.push()`, `router.replace()` | +| Route meta fields | N/A | Built-in | + +## Production Setup with Vue Router + +```javascript +// router/index.js +import { createRouter, createWebHistory } from 'vue-router' + +const routes = [ + { + path: '/', + name: 'Home', + component: () => import('@/views/Home.vue'), // Lazy loaded + meta: { requiresAuth: false } + }, + { + path: '/dashboard', + name: 'Dashboard', + component: () => import('@/views/Dashboard.vue'), + meta: { requiresAuth: true }, + children: [ + { + path: 'settings', + name: 'Settings', + component: () => import('@/views/Settings.vue') + } + ] + }, + { + path: '/users/:id', + name: 'UserProfile', + component: () => import('@/views/UserProfile.vue'), + props: true // Pass params as props + }, + { + path: '/:pathMatch(.*)*', + name: 'NotFound', + component: () => import('@/views/NotFound.vue') + } +] + +const router = createRouter({ + history: createWebHistory(), + routes, + scrollBehavior(to, from, savedPosition) { + return savedPosition || { top: 0 } + } +}) + +// Global navigation guard +router.beforeEach((to, from) => { + if (to.meta.requiresAuth && !isAuthenticated()) { + return { name: 'Login', query: { redirect: to.fullPath } } + } +}) + +export default router +``` + +```javascript +// main.js +import { createApp } from 'vue' +import App from './App.vue' +import router from './router' + +createApp(App) + .use(router) + .mount('#app') +``` + +```vue + + +``` + +## Modern Vue Router Features (2025+) + +```javascript +// Data Loading API (Vue Router 4.2+) +const routes = [ + { + path: '/users/:id', + component: UserProfile, + // Load data at route level + loader: async (route) => { + return { user: await fetchUser(route.params.id) } + } + } +] + +// View Transitions API integration +const router = createRouter({ + // Enable native browser view transitions + // Requires browser support (Chrome 111+) +}) +``` + +## Key Points + +1. **Use Vue Router for any app beyond a prototype** - The features are essential +2. **Simple routing is for learning** - Understand the concepts, then use the library +3. **Lazy loading is critical for bundle size** - Vue Router makes it trivial +4. **Navigation guards prevent security issues** - Hard to get right manually +5. **History mode requires Vue Router** - Clean URLs need proper handling +6. **New features keep coming** - Data Loading API, View Transitions + +## Reference +- [Vue.js Routing Guide](https://vuejs.org/guide/scaling-up/routing.html) +- [Vue Router Documentation](https://router.vuejs.org/) +- [Vue Router Getting Started](https://router.vuejs.org/guide/) diff --git a/.agents/skills/vue-testing-best-practices/LICENSE.md b/.agents/skills/vue-testing-best-practices/LICENSE.md new file mode 100644 index 0000000..3f08a54 --- /dev/null +++ b/.agents/skills/vue-testing-best-practices/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 hyf0, SerKo + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/.agents/skills/vue-testing-best-practices/SKILL.md b/.agents/skills/vue-testing-best-practices/SKILL.md new file mode 100644 index 0000000..c600b52 --- /dev/null +++ b/.agents/skills/vue-testing-best-practices/SKILL.md @@ -0,0 +1,29 @@ +--- +name: vue-testing-best-practices +version: 1.0.0 +license: MIT +author: github.com/vuejs-ai +description: Use for Vue.js testing. Covers Vitest, Vue Test Utils, component testing, mocking, testing patterns, and Playwright for E2E testing. +--- + +Vue.js testing best practices, patterns, and common gotchas. + +### Testing +- Setting up test infrastructure for Vue 3 projects → See [testing-vitest-recommended-for-vue](reference/testing-vitest-recommended-for-vue.md) +- Tests keep breaking when refactoring component internals → See [testing-component-blackbox-approach](reference/testing-component-blackbox-approach.md) +- Tests fail intermittently with race conditions → See [testing-async-await-flushpromises](reference/testing-async-await-flushpromises.md) +- Composables using lifecycle hooks or inject fail to test → See [testing-composables-helper-wrapper](reference/testing-composables-helper-wrapper.md) +- Getting "injection Symbol(pinia) not found" errors in tests → See [testing-pinia-store-setup](reference/testing-pinia-store-setup.md) +- Components with async setup won't render in tests → See [testing-suspense-async-components](reference/testing-suspense-async-components.md) +- Snapshot tests keep passing despite broken functionality → See [testing-no-snapshot-only](reference/testing-no-snapshot-only.md) +- Choosing end-to-end testing framework for Vue apps → See [testing-e2e-playwright-recommended](reference/testing-e2e-playwright-recommended.md) +- Tests need to verify computed styles or real DOM events → See [testing-browser-vs-node-runners](reference/testing-browser-vs-node-runners.md) +- Testing components created with defineAsyncComponent fails → See [async-component-testing](reference/async-component-testing.md) +- Teleported modal content can't be found in wrapper queries → See [teleport-testing-complexity](reference/teleport-testing-complexity.md) + +## Reference + +- [Vue.js Testing Guide](https://vuejs.org/guide/scaling-up/testing) +- [Vue Test Utils](https://test-utils.vuejs.org/) +- [Vitest Documentation](https://vitest.dev/) +- [Playwright Documentation](https://playwright.dev/) diff --git a/.agents/skills/vue-testing-best-practices/SYNC.md b/.agents/skills/vue-testing-best-practices/SYNC.md new file mode 100644 index 0000000..6e968f2 --- /dev/null +++ b/.agents/skills/vue-testing-best-practices/SYNC.md @@ -0,0 +1,5 @@ +# Sync Info + +- **Source:** `vendor/vuejs-ai/skills/vue-testing-best-practices` +- **Git SHA:** `f3dd1bf4d3ac78331bdc903e4519d561c538ca6a` +- **Synced:** 2026-03-16 diff --git a/.agents/skills/vue-testing-best-practices/reference/async-component-testing.md b/.agents/skills/vue-testing-best-practices/reference/async-component-testing.md new file mode 100644 index 0000000..a4d2c49 --- /dev/null +++ b/.agents/skills/vue-testing-best-practices/reference/async-component-testing.md @@ -0,0 +1,163 @@ +--- +title: Use flushPromises for Testing Async Components +impact: HIGH +impactDescription: Without awaiting async operations, tests make assertions before the component has rendered, causing false negatives +type: gotcha +tags: [vue3, testing, async, defineAsyncComponent, flushPromises, vitest] +--- + +# Use flushPromises for Testing Async Components + +**Impact: HIGH** - When testing async components created with `defineAsyncComponent`, you must use `await flushPromises()` to ensure the component has loaded before making assertions. Vue updates asynchronously, so tests that don't account for this will make assertions before the component has rendered. + +## Task Checklist + +- [ ] Use `async/await` in test functions for async components +- [ ] Call `await flushPromises()` after mounting async components +- [ ] Test loading states by making assertions before `flushPromises()` +- [ ] Test error states using rejected promises in `defineAsyncComponent` +- [ ] Use `trigger()` with `await` as it returns a Promise + +**Incorrect:** + +```javascript +import { mount } from '@vue/test-utils' +import { defineAsyncComponent } from 'vue' + +const AsyncWidget = defineAsyncComponent(() => + import('./Widget.vue') +) + +test('renders async component', () => { + const wrapper = mount(AsyncWidget) + + // FAILS: Component hasn't loaded yet + expect(wrapper.text()).toContain('Widget Content') +}) +``` + +**Correct:** + +```javascript +import { mount, flushPromises } from '@vue/test-utils' +import { defineAsyncComponent, nextTick } from 'vue' + +const AsyncWidget = defineAsyncComponent(() => + import('./Widget.vue') +) + +test('renders async component', async () => { + const wrapper = mount(AsyncWidget) + + // Wait for async component to load + await flushPromises() + + expect(wrapper.text()).toContain('Widget Content') +}) + +test('shows loading state initially', async () => { + const AsyncWithLoading = defineAsyncComponent({ + loader: () => import('./Widget.vue'), + loadingComponent: { template: '
    Loading...
    ' }, + delay: 0 + }) + + const wrapper = mount(AsyncWithLoading) + + // Check loading state immediately + expect(wrapper.text()).toContain('Loading...') + + // Wait for component to load + await flushPromises() + + // Check final state + expect(wrapper.text()).toContain('Widget Content') +}) +``` + +## Testing with Suspense + +```javascript +import { mount, flushPromises } from '@vue/test-utils' +import { Suspense, defineAsyncComponent, h } from 'vue' + +const AsyncWidget = defineAsyncComponent(() => + import('./Widget.vue') +) + +test('renders async component with Suspense', async () => { + const wrapper = mount({ + components: { AsyncWidget }, + template: ` + + + + + ` + }) + + // Initially shows fallback + expect(wrapper.text()).toContain('Loading...') + + // Wait for async resolution + await flushPromises() + + // Now shows actual content + expect(wrapper.text()).toContain('Widget Content') +}) +``` + +## Testing Error States + +```javascript +import { mount, flushPromises } from '@vue/test-utils' +import { defineAsyncComponent } from 'vue' + +test('shows error component on load failure', async () => { + const AsyncWithError = defineAsyncComponent({ + loader: () => Promise.reject(new Error('Failed to load')), + errorComponent: { template: '
    Error loading component
    ' } + }) + + const wrapper = mount(AsyncWithError) + + await flushPromises() + + expect(wrapper.text()).toContain('Error loading component') +}) +``` + +## Utilities Reference + +| Utility | Purpose | +|---------|---------| +| `await flushPromises()` | Resolves all pending promises | +| `await nextTick()` | Waits for Vue's next DOM update cycle | +| `await wrapper.trigger('click')` | Triggers event and waits for update | + +## Dynamic Import Handling + +**Note:** Dynamic imports (`import('./File.vue')`) may require additional handling beyond `flushPromises()` in test environments. Test runners like Vitest handle module resolution differently than runtime bundlers, which can cause timing issues with dynamic imports. If `flushPromises()` alone doesn't resolve the component, consider: + +- Mocking the dynamic import to return the component synchronously +- Using multiple `await flushPromises()` calls in sequence +- Wrapping assertions in `waitFor()` or retry utilities +- Configuring your test runner's module resolution settings + +```javascript +// If flushPromises() isn't sufficient, mock the import +vi.mock('./Widget.vue', () => ({ + default: { template: '
    Widget Content
    ' } +})) + +// Or use multiple flush calls for nested async operations +await flushPromises() +await flushPromises() +``` + +## References + +- [Vue Test Utils - Asynchronous Behavior](https://test-utils.vuejs.org/guide/advanced/async-suspense) +- [Vue.js Async Components Documentation](https://vuejs.org/guide/components/async) diff --git a/.agents/skills/vue-testing-best-practices/reference/teleport-testing-complexity.md b/.agents/skills/vue-testing-best-practices/reference/teleport-testing-complexity.md new file mode 100644 index 0000000..887836f --- /dev/null +++ b/.agents/skills/vue-testing-best-practices/reference/teleport-testing-complexity.md @@ -0,0 +1,158 @@ +--- +title: Teleported Content Requires Special Testing Approach +impact: MEDIUM +impactDescription: Vue Test Utils cannot find teleported content using standard wrapper.find() methods +type: gotcha +tags: [vue3, teleport, testing, vue-test-utils] +--- + +# Teleported Content Requires Special Testing Approach + +**Impact: MEDIUM** - Vue Test Utils scopes queries to the mounted component. Teleported content renders outside the component's DOM tree, so `wrapper.find()` cannot locate it. This leads to failing tests and confusion. + +## Task Checklist + +- [ ] Stub Teleport in unit tests to keep content in component tree +- [ ] Use `document.body` queries for integration tests with real Teleport +- [ ] Consider using `getComponent()` instead of DOM queries for teleported components + +**Problem - Standard Testing Fails:** +```vue + + +``` + +```ts +// Modal.spec.ts - BROKEN +import { mount } from '@vue/test-utils' +import Modal from './Modal.vue' + +test('modal input exists', async () => { + const wrapper = mount(Modal) + await wrapper.find('button').trigger('click') + + // FAILS: Teleported content is not in wrapper's DOM tree + expect(wrapper.find('[data-testid="modal-input"]').exists()).toBe(true) +}) +``` + +**Solution 1 - Stub Teleport:** +```ts +import { mount } from '@vue/test-utils' +import Modal from './Modal.vue' + +test('modal input exists', async () => { + const wrapper = mount(Modal, { + global: { + stubs: { + // Stub teleport to render content inline + Teleport: true + } + } + }) + + await wrapper.find('button').trigger('click') + + // Works: Content renders inside wrapper + expect(wrapper.find('[data-testid="modal-input"]').exists()).toBe(true) +}) +``` + +**Solution 2 - Query Document Body:** +```ts +import { mount } from '@vue/test-utils' +import Modal from './Modal.vue' + +test('modal renders to body', async () => { + const wrapper = mount(Modal, { + attachTo: document.body // Required for Teleport to work + }) + + await wrapper.find('button').trigger('click') + + // Query the actual DOM + const modal = document.querySelector('[data-testid="modal"]') + expect(modal).toBeTruthy() + + const input = document.querySelector('[data-testid="modal-input"]') + expect(input).toBeTruthy() + + // Cleanup + wrapper.unmount() +}) +``` + +**Solution 3 - Custom Teleport Stub with Content Access:** +```ts +import { mount, config } from '@vue/test-utils' +import { h, Teleport } from 'vue' +import Modal from './Modal.vue' + +// Custom stub that renders content in a testable way +const TeleportStub = { + setup(props, { slots }) { + return () => h('div', { class: 'teleport-stub' }, slots.default?.()) + } +} + +test('modal with custom stub', async () => { + const wrapper = mount(Modal, { + global: { + stubs: { + Teleport: TeleportStub + } + } + }) + + await wrapper.find('button').trigger('click') + + // Content is inside .teleport-stub + expect(wrapper.find('.teleport-stub [data-testid="modal-input"]').exists()).toBe(true) +}) +``` + +## Testing Vue Final Modal and UI Libraries + +Libraries like Vue Final Modal use Teleport internally, causing test failures: + +```ts +// Problem: Vue Final Modal teleports to body +import { VueFinalModal } from 'vue-final-modal' + +test('modal content', async () => { + const wrapper = mount(MyComponent, { + global: { + stubs: { + // Stub the modal component to avoid teleport issues + VueFinalModal: true + } + } + }) +}) +``` + +## E2E Testing (Cypress, Playwright) + +E2E tests query the real DOM, so Teleport works naturally: + +```ts +// Cypress +it('opens modal', () => { + cy.visit('/page-with-modal') + cy.get('button').click() + + // Works: Cypress queries the real DOM + cy.get('[data-testid="modal"]').should('be.visible') +}) +``` + +## Reference +- [Vue Test Utils - Teleport](https://test-utils.vuejs.org/guide/advanced/teleport) +- [Vue Test Utils - Stubs](https://test-utils.vuejs.org/guide/advanced/stubs-shallow-mount) diff --git a/.agents/skills/vue-testing-best-practices/reference/testing-async-await-flushpromises.md b/.agents/skills/vue-testing-best-practices/reference/testing-async-await-flushpromises.md new file mode 100644 index 0000000..597d9c8 --- /dev/null +++ b/.agents/skills/vue-testing-best-practices/reference/testing-async-await-flushpromises.md @@ -0,0 +1,175 @@ +--- +title: Properly Handle Async Updates with nextTick and flushPromises +impact: HIGH +impactDescription: Race conditions and flaky tests occur when async DOM updates or API calls complete after assertions run +type: gotcha +tags: [vue3, testing, async, flushPromises, nextTick, vitest, vue-test-utils, race-condition] +--- + +# Properly Handle Async Updates with nextTick and flushPromises + +**Impact: HIGH** - Vue updates the DOM asynchronously. Without properly awaiting these updates, tests may assert against stale DOM state, causing intermittent failures and false negatives. + +Use `await` with triggers and `setValue`, use `nextTick` for reactive updates, and use `flushPromises` for external async operations like API calls. + +## Task Checklist + +- [ ] Always await `trigger()` and `setValue()` calls +- [ ] Use `await nextTick()` after programmatic reactive state changes +- [ ] Use `await flushPromises()` for external async operations (API calls, timers) +- [ ] Don't chain multiple `nextTick` calls - use `flushPromises` instead +- [ ] Consider using `waitFor` from testing-library for polling assertions + +**Incorrect:** +```javascript +import { mount } from '@vue/test-utils' +import SearchComponent from './SearchComponent.vue' + +// BAD: Not awaiting trigger - assertion runs before DOM updates +test('search filters results', () => { + const wrapper = mount(SearchComponent) + + wrapper.find('input').setValue('vue') // Missing await! + wrapper.find('button').trigger('click') // Missing await! + + // This assertion likely fails - DOM hasn't updated yet + expect(wrapper.findAll('.result').length).toBe(3) +}) + +// BAD: Using nextTick for API calls +test('loads data from API', async () => { + const wrapper = mount(DataLoader) + + await nextTick() // This won't wait for the API call! + + // Assertion runs before fetch completes + expect(wrapper.find('.data').text()).toBe('Loaded data') +}) +``` + +**Correct:** +```javascript +import { mount, flushPromises } from '@vue/test-utils' +import { nextTick } from 'vue' +import SearchComponent from './SearchComponent.vue' +import DataLoader from './DataLoader.vue' + +// CORRECT: Await trigger and setValue +test('search filters results', async () => { + const wrapper = mount(SearchComponent) + + await wrapper.find('input').setValue('vue') + await wrapper.find('button').trigger('click') + + expect(wrapper.findAll('.result').length).toBe(3) +}) + +// CORRECT: Use flushPromises for API calls +test('loads data from API', async () => { + const wrapper = mount(DataLoader) + + // Wait for all pending promises to resolve + await flushPromises() + + expect(wrapper.find('.data').text()).toBe('Loaded data') +}) +``` + +## When to Use Each Method + +### `await trigger()` / `await setValue()` - User Interactions +```javascript +// These methods return nextTick internally +await wrapper.find('button').trigger('click') +await wrapper.find('input').setValue('new value') +await wrapper.find('form').trigger('submit') +``` + +### `await nextTick()` - Programmatic Reactive Updates +```javascript +import { nextTick } from 'vue' + +test('reflects programmatic state changes', async () => { + const wrapper = mount(Counter) + + // Direct state modification (when testing with exposed internals) + wrapper.vm.count = 5 + + await nextTick() // Wait for Vue to update DOM + + expect(wrapper.find('.count').text()).toBe('5') +}) +``` + +### `await flushPromises()` - External Async Operations +```javascript +import { flushPromises } from '@vue/test-utils' + +test('displays fetched data', async () => { + const wrapper = mount(UserProfile, { + props: { userId: 1 } + }) + + // Wait for component's API call to complete + await flushPromises() + + expect(wrapper.find('.username').text()).toBe('John') +}) + +// Sometimes you need multiple flushPromises for chained async operations +test('processes data after fetch', async () => { + const wrapper = mount(DataProcessor) + + await flushPromises() // Wait for fetch + await flushPromises() // Wait for processing triggered by fetch + + expect(wrapper.find('.processed').exists()).toBe(true) +}) +``` + +## Common Pattern: Combining Methods +```javascript +test('submits form and shows success', async () => { + const wrapper = mount(ContactForm) + + // Fill form (awaiting each interaction) + await wrapper.find('#name').setValue('John') + await wrapper.find('#email').setValue('john@example.com') + + // Submit form + await wrapper.find('form').trigger('submit') + + // Wait for API submission to complete + await flushPromises() + + // Assert success state + expect(wrapper.find('.success-message').exists()).toBe(true) +}) +``` + +## Testing with MSW or Mock APIs +```javascript +import { flushPromises } from '@vue/test-utils' +import { rest } from 'msw' +import { setupServer } from 'msw/node' + +const server = setupServer( + rest.get('/api/user', (req, res, ctx) => { + return res(ctx.json({ name: 'John' })) + }) +) + +test('displays user data', async () => { + const wrapper = mount(UserCard) + + // MSW might require multiple flushPromises + await flushPromises() + await flushPromises() + + expect(wrapper.find('.name').text()).toBe('John') +}) +``` + +## Reference +- [Vue Test Utils - Asynchronous Behavior](https://test-utils.vuejs.org/guide/advanced/async-suspense) +- [Vue.js Testing Guide](https://vuejs.org/guide/scaling-up/testing) diff --git a/.agents/skills/vue-testing-best-practices/reference/testing-browser-vs-node-runners.md b/.agents/skills/vue-testing-best-practices/reference/testing-browser-vs-node-runners.md new file mode 100644 index 0000000..a112a42 --- /dev/null +++ b/.agents/skills/vue-testing-best-practices/reference/testing-browser-vs-node-runners.md @@ -0,0 +1,208 @@ +--- +title: Choose Browser-Based Runner for Style and DOM Event Testing +impact: MEDIUM +impactDescription: Node-based runners cannot test real CSS behavior, native DOM events, cookies, or computed styles +type: capability +tags: [vue3, testing, component-testing, vitest, browser, jsdom] +--- + +# Choose Browser-Based Runner for Style and DOM Event Testing + +**Impact: MEDIUM** - Node-based test runners (Vitest with jsdom/happy-dom) simulate the DOM but cannot test real CSS rendering, native browser events, cookies, computed styles, or cross-browser behavior. Use browser-based runners when these matter. + +Use Vitest for most component tests (fast), but use Vitest Browser Mode when testing visual/DOM-dependent features. + +## Task Checklist + +- [ ] Use Vitest (node) for logic-focused component tests +- [ ] Use Vitest Browser Mode for style-dependent tests +- [ ] Use Vitest Browser Mode for native events (focus, drag, resize) +- [ ] Use Vitest Browser Mode for cookies and computed CSS styles +- [ ] Accept slower speed tradeoff for browser accuracy + +## When to Use Each Approach + +### Node-Based Runner (Vitest + happy-dom/jsdom) +Best for: +- Pure logic testing +- State management +- Event emission +- Props/slots behavior +- Most component interactions +- Fast CI/CD pipelines + +```javascript +// vitest.config.js +export default defineConfig({ + test: { + environment: 'happy-dom', // or 'jsdom' + } +}) +``` + +```javascript +// Fast but limited - fine for most tests +test('button emits click event', async () => { + const wrapper = mount(Button) + await wrapper.trigger('click') + expect(wrapper.emitted('click')).toBeTruthy() +}) +``` + +### Vitest Browser Mode +Required for: +- CSS computed styles verification +- CSS transitions/animations +- Real focus/blur behavior +- Drag and drop +- Cookie operations +- Viewport-dependent behavior +- Cross-browser validation + +## Vitest Browser Mode Setup + +```bash +npm install -D @vitest/browser playwright +``` + +```javascript +// vitest.config.js +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + browser: { + enabled: true, + name: 'chromium', + provider: 'playwright', + }, + }, +}) +``` + +```javascript +// Button.browser.test.js +import { render } from 'vitest-browser-vue' +import Button from './Button.vue' + +test('has correct hover styling', async () => { + const { getByRole } = render(Button, { props: { label: 'Click me' } }) + + const button = getByRole('button') + + // Check initial style + await expect.element(button).toHaveStyle({ + backgroundColor: 'rgb(59, 130, 246)' // blue + }) +}) + +test('maintains focus after click', async () => { + const { getByRole } = render(Button) + + const button = getByRole('button') + await button.click() + + await expect.element(button).toHaveFocus() +}) +``` + +## Examples: What Each Runner Can/Cannot Test + +### Styles - Browser Required +```javascript +// Node runner: CANNOT verify actual CSS +test('danger button has red background', () => { + const wrapper = mount(Button, { props: { variant: 'danger' } }) + // This only checks class exists, not actual color + expect(wrapper.classes()).toContain('bg-red-500') +}) + +// Vitest Browser Mode: CAN verify computed styles +test('danger button renders red', async () => { + const { getByRole } = render(Button, { props: { variant: 'danger' } }) + await expect.element(getByRole('button')).toHaveStyle({ + backgroundColor: 'rgb(239, 68, 68)' + }) +}) +``` + +### Computed CSS Styles - Browser Required +```javascript +// Node runner: CANNOT get real computed styles +test('button has correct padding', () => { + const wrapper = mount(Button) + // getComputedStyle returns empty/default values in jsdom + const style = window.getComputedStyle(wrapper.element) + // style.padding will be empty string, not actual computed value +}) + +// Vitest Browser Mode: Real computed styles +test('button has correct padding', async () => { + const { getByRole } = render(Button) + const button = getByRole('button') + + await expect.element(button).toHaveStyle({ + padding: '12px 24px' + }) +}) +``` + +### Native Events - Browser Required +```javascript +// Node runner: Synthetic events only +test('handles drag and drop', async () => { + const wrapper = mount(DraggableList) + // trigger('dragstart') is synthetic - may not work as expected + await wrapper.find('.item').trigger('dragstart') +}) + +// Vitest Browser Mode: Real native events via userEvent +import { userEvent } from '@vitest/browser/context' + +test('reorders items on drag', async () => { + const { getByTestId } = render(DraggableList) + + const item = getByTestId('item-1') + const target = getByTestId('item-3') + + await userEvent.dragAndDrop(item, target) + + // Assert reordering +}) +``` + +## Recommended Testing Strategy + +```javascript +// vitest.config.js - Separate test configurations + +export default defineConfig({ + test: { + // Default: Node environment for speed + environment: 'happy-dom', + + // Browser tests in separate directory + include: ['src/**/*.test.{js,ts}'], + }, +}) + +// Run browser tests separately +// npx vitest --browser.enabled +``` + +### Directory Structure +``` +tests/ +├── unit/ # Fast node-based tests +│ ├── Button.test.js +│ └── useCounter.test.js +├── component/ # Slower browser-based tests +│ ├── Button.browser.test.js +│ └── DragDrop.browser.test.js +└── e2e/ # Full E2E tests (Playwright) + └── user-flow.spec.ts +``` + +## Reference +- [Vue.js Testing - Component Testing](https://vuejs.org/guide/scaling-up/testing#component-testing) +- [Vitest Browser Mode](https://vitest.dev/guide/browser.html) diff --git a/.agents/skills/vue-testing-best-practices/reference/testing-component-blackbox-approach.md b/.agents/skills/vue-testing-best-practices/reference/testing-component-blackbox-approach.md new file mode 100644 index 0000000..e3a869c --- /dev/null +++ b/.agents/skills/vue-testing-best-practices/reference/testing-component-blackbox-approach.md @@ -0,0 +1,144 @@ +--- +title: Test Components Using Blackbox Approach - Focus on Behavior Not Implementation +impact: HIGH +impactDescription: Implementation-aware tests become brittle and break during refactoring, leading to high maintenance burden +type: best-practice +tags: [vue3, testing, component-testing, vitest, vue-test-utils, blackbox] +--- + +# Test Components Using Blackbox Approach - Focus on Behavior Not Implementation + +**Impact: HIGH** - Tests that rely on implementation details (internal state, private methods, component structure) break during refactoring even when functionality remains correct. This leads to false negatives and high test maintenance burden. + +Follow Kent C. Dodds' testing philosophy: "The more your tests resemble how your software is used, the more confidence they can give you." + +## Task Checklist + +- [ ] Test what the component does, not how it does it +- [ ] Query elements by user-visible attributes (text, role, testid) +- [ ] Simulate user interactions (click, type) rather than calling methods directly +- [ ] Assert on rendered output, emitted events, and visible state changes +- [ ] Avoid accessing component internal state or private methods +- [ ] Use data-testid attributes for elements without semantic meaning + +**Incorrect:** +```javascript +import { mount } from '@vue/test-utils' +import Counter from './Counter.vue' + +// BAD: Testing implementation details +test('counter increments', async () => { + const wrapper = mount(Counter) + + // Accessing internal state directly + expect(wrapper.vm.count).toBe(0) + + // Calling internal method instead of simulating user action + wrapper.vm.increment() + + // Checking internal state instead of visible output + expect(wrapper.vm.count).toBe(1) +}) + +// BAD: Testing component structure +test('has increment button', () => { + const wrapper = mount(Counter) + + // Testing implementation detail - what if button becomes an anchor? + expect(wrapper.find('button').exists()).toBe(true) +}) +``` + +**Correct:** +```javascript +import { mount } from '@vue/test-utils' +import Counter from './Counter.vue' + +// CORRECT: Testing behavior like a user would +test('counter displays updated value after clicking increment', async () => { + const wrapper = mount(Counter, { + props: { max: 10 } + }) + + // Assert initial visible state + expect(wrapper.find('[data-testid="counter-value"]').text()).toContain('0') + + // Simulate user action + await wrapper.find('[data-testid="increment-button"]').trigger('click') + + // Assert visible result + expect(wrapper.find('[data-testid="counter-value"]').text()).toContain('1') +}) + +// CORRECT: Testing emitted events (public API) +test('emits change event with new value when incremented', async () => { + const wrapper = mount(Counter) + + await wrapper.find('[data-testid="increment-button"]').trigger('click') + + expect(wrapper.emitted('change')).toHaveLength(1) + expect(wrapper.emitted('change')[0]).toEqual([1]) +}) +``` + +## Using @testing-library/vue for Better Blackbox Tests + +```javascript +import { render, screen, fireEvent } from '@testing-library/vue' +import Counter from './Counter.vue' + +// Testing Library encourages accessible, user-centric queries +test('increments counter on button click', async () => { + render(Counter) + + // Query by role - how screen readers see it + const button = screen.getByRole('button', { name: /increment/i }) + const display = screen.getByText('0') + + await fireEvent.click(button) + + expect(screen.getByText('1')).toBeInTheDocument() +}) +``` + +## What to Test vs What Not to Test + +### DO Test (Public Interface) +```javascript +// Props affect rendered output +test('shows title from props', () => { + const wrapper = mount(Card, { + props: { title: 'Hello World' } + }) + expect(wrapper.text()).toContain('Hello World') +}) + +// Slots render correctly +test('renders slot content', () => { + const wrapper = mount(Card, { + slots: { default: '

    Slot content

    ' } + }) + expect(wrapper.text()).toContain('Slot content') +}) + +// Emitted events +test('emits close event when X clicked', async () => { + const wrapper = mount(Modal) + await wrapper.find('[data-testid="close-button"]').trigger('click') + expect(wrapper.emitted('close')).toBeTruthy() +}) +``` + +### DON'T Test (Implementation Details) +```javascript +// Don't test internal computed properties +// Don't test internal methods +// Don't test component options/setup internals +// Don't test that specific child components are rendered (unless critical) +// Don't rely exclusively on snapshot tests for correctness +``` + +## Reference +- [Vue.js Testing Guide](https://vuejs.org/guide/scaling-up/testing) +- [Vue Test Utils - Testing Philosophy](https://test-utils.vuejs.org/guide/) +- [Testing Library Guiding Principles](https://testing-library.com/docs/guiding-principles) diff --git a/.agents/skills/vue-testing-best-practices/reference/testing-composables-helper-wrapper.md b/.agents/skills/vue-testing-best-practices/reference/testing-composables-helper-wrapper.md new file mode 100644 index 0000000..f6e4f07 --- /dev/null +++ b/.agents/skills/vue-testing-best-practices/reference/testing-composables-helper-wrapper.md @@ -0,0 +1,238 @@ +--- +title: Test Complex Composables with Host Component Wrapper +impact: MEDIUM +impactDescription: Composables using lifecycle hooks or provide/inject fail when tested directly without a component context +type: capability +tags: [vue3, testing, composables, vitest, lifecycle-hooks, provide-inject] +--- + +# Test Complex Composables with Host Component Wrapper + +**Impact: MEDIUM** - Composables that use Vue lifecycle hooks (`onMounted`, `onUnmounted`) or dependency injection (`inject`) require a component context to function. Testing them directly will cause errors or incorrect behavior. + +Simple composables using only reactivity APIs can be tested directly. Complex composables need a helper function that creates a host component context. + +## Task Checklist + +- [ ] Identify if composable uses lifecycle hooks or inject +- [ ] For simple composables (refs, computed only): test directly +- [ ] For complex composables: use `withSetup` helper pattern +- [ ] Clean up by unmounting the test app after each test +- [ ] Use `app.provide()` to mock injected dependencies + +**Simple Composable - Test Directly:** +```javascript +// composables/useCounter.js +import { ref, computed } from 'vue' + +export function useCounter(initialValue = 0) { + const count = ref(initialValue) + const doubled = computed(() => count.value * 2) + const increment = () => count.value++ + + return { count, doubled, increment } +} +``` + +```javascript +// useCounter.test.js +import { describe, it, expect } from 'vitest' +import { useCounter } from './useCounter' + +// CORRECT: Simple composable can be tested directly +describe('useCounter', () => { + it('initializes with default value', () => { + const { count } = useCounter() + expect(count.value).toBe(0) + }) + + it('increments count', () => { + const { count, increment } = useCounter() + increment() + expect(count.value).toBe(1) + }) + + it('computes doubled value', () => { + const { count, doubled, increment } = useCounter(5) + expect(doubled.value).toBe(10) + increment() + expect(doubled.value).toBe(12) + }) +}) +``` + +**Complex Composable - Use Host Wrapper:** +```javascript +// composables/useFetch.js +import { ref, onMounted, onUnmounted, inject } from 'vue' + +export function useFetch(url) { + const data = ref(null) + const error = ref(null) + const loading = ref(true) + let controller = null + + // Uses inject - needs component context + const apiClient = inject('apiClient') + + // Uses lifecycle hooks - needs component context + onMounted(async () => { + controller = new AbortController() + try { + const response = await apiClient.get(url, { signal: controller.signal }) + data.value = response.data + } catch (e) { + if (e.name !== 'AbortError') error.value = e + } finally { + loading.value = false + } + }) + + onUnmounted(() => { + controller?.abort() + }) + + return { data, error, loading } +} +``` + +```javascript +// test-utils.js +import { createApp } from 'vue' + +/** + * Helper to test composables that need component context + */ +export function withSetup(composable) { + let result + + const app = createApp({ + setup() { + result = composable() + // Return a render function to suppress warnings + return () => {} + } + }) + + app.mount(document.createElement('div')) + + return [result, app] +} +``` + +```javascript +// useFetch.test.js +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { flushPromises } from '@vue/test-utils' +import { withSetup } from './test-utils' +import { useFetch } from './useFetch' + +describe('useFetch', () => { + let app + const mockApiClient = { + get: vi.fn() + } + + afterEach(() => { + // IMPORTANT: Clean up to trigger onUnmounted + app?.unmount() + }) + + it('fetches data on mount', async () => { + mockApiClient.get.mockResolvedValue({ data: { id: 1, name: 'Test' } }) + + const [result, testApp] = withSetup(() => useFetch('/api/test')) + app = testApp + + // Provide mocked dependency + app.provide('apiClient', mockApiClient) + + // Wait for async operations + await flushPromises() + + expect(result.data.value).toEqual({ id: 1, name: 'Test' }) + expect(result.loading.value).toBe(false) + expect(result.error.value).toBeNull() + }) + + it('handles errors', async () => { + const testError = new Error('Network error') + mockApiClient.get.mockRejectedValue(testError) + + const [result, testApp] = withSetup(() => useFetch('/api/test')) + app = testApp + app.provide('apiClient', mockApiClient) + + await flushPromises() + + expect(result.error.value).toBe(testError) + expect(result.data.value).toBeNull() + }) +}) +``` + +## Enhanced withSetup Helper with Provide Support +```javascript +// test-utils.js +export function withSetup(composable, options = {}) { + let result + + const app = createApp({ + setup() { + result = composable() + return () => {} + } + }) + + // Apply global provides before mounting + if (options.provide) { + Object.entries(options.provide).forEach(([key, value]) => { + app.provide(key, value) + }) + } + + app.mount(document.createElement('div')) + + return [result, app] +} + +// Usage +const [result, app] = withSetup(() => useMyComposable(), { + provide: { + apiClient: mockApiClient, + currentUser: { id: 1, name: 'Test User' } + } +}) +``` + +## Testing with @vue/test-utils mount +```javascript +import { mount } from '@vue/test-utils' +import { defineComponent } from 'vue' +import { useFetch } from './useFetch' + +test('useFetch in component context', async () => { + const TestComponent = defineComponent({ + setup() { + const { data, loading } = useFetch('/api/users') + return { data, loading } + }, + template: '
    {{ loading ? "Loading..." : data }}
    ' + }) + + const wrapper = mount(TestComponent, { + global: { + provide: { + apiClient: mockApiClient + } + } + }) + + await flushPromises() + expect(wrapper.text()).toContain('Test data') +}) +``` + +## Reference +- [Vue.js Testing Guide - Testing Composables](https://vuejs.org/guide/scaling-up/testing#testing-composables) +- [Vue Test Utils - Mounting Components](https://test-utils.vuejs.org/guide/) diff --git a/.agents/skills/vue-testing-best-practices/reference/testing-e2e-playwright-recommended.md b/.agents/skills/vue-testing-best-practices/reference/testing-e2e-playwright-recommended.md new file mode 100644 index 0000000..3df2ff0 --- /dev/null +++ b/.agents/skills/vue-testing-best-practices/reference/testing-e2e-playwright-recommended.md @@ -0,0 +1,242 @@ +--- +title: Use Playwright for E2E Testing - Cross-Browser Support and Better DX +impact: MEDIUM +impactDescription: Cypress has browser limitations and some features require paid subscriptions +type: best-practice +tags: [vue3, testing, e2e, playwright, cypress, end-to-end] +--- + +# Use Playwright for E2E Testing - Cross-Browser Support and Better DX + +**Impact: MEDIUM** - Playwright offers superior cross-browser testing (Chromium, WebKit, Firefox), excellent debugging tools, and is fully open source. Cypress has limitations with WebKit support and requires paid subscriptions for some features. + +Use Playwright for new E2E testing setups. Consider Cypress if team already has expertise or for its visual debugging UI. + +## Task Checklist + +- [ ] Install Playwright with browsers for your target platforms +- [ ] Configure for Vue dev server integration +- [ ] Set up projects for different browsers +- [ ] Use locator strategies that match component test patterns +- [ ] Configure CI for parallel test execution +- [ ] Use trace and screenshot features for debugging + +## Quick Setup + +```bash +# Install Playwright +npm init playwright@latest + +# This will create: +# - playwright.config.ts +# - tests/ directory +# - tests-examples/ directory +``` + +**playwright.config.ts:** +```typescript +import { defineConfig, devices } from '@playwright/test' + +export default defineConfig({ + testDir: './e2e', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: 'html', + + use: { + // Base URL for navigation + baseURL: 'http://localhost:5173', + // Capture trace on first retry + trace: 'on-first-retry', + // Screenshot on failure + screenshot: 'only-on-failure', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + // Mobile viewports + { + name: 'Mobile Chrome', + use: { ...devices['Pixel 5'] }, + }, + ], + + // Run local dev server before tests + webServer: { + command: 'npm run dev', + url: 'http://localhost:5173', + reuseExistingServer: !process.env.CI, + }, +}) +``` + +## E2E Test Example + +```typescript +// e2e/user-flow.spec.ts +import { test, expect } from '@playwright/test' + +test.describe('User Authentication', () => { + test('user can log in and see dashboard', async ({ page }) => { + // Navigate to login + await page.goto('/login') + + // Fill login form + await page.getByLabel('Email').fill('user@example.com') + await page.getByLabel('Password').fill('password123') + await page.getByRole('button', { name: 'Sign In' }).click() + + // Verify redirect to dashboard + await expect(page).toHaveURL('/dashboard') + await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible() + }) + + test('shows error for invalid credentials', async ({ page }) => { + await page.goto('/login') + + await page.getByLabel('Email').fill('wrong@example.com') + await page.getByLabel('Password').fill('wrongpassword') + await page.getByRole('button', { name: 'Sign In' }).click() + + await expect(page.getByRole('alert')).toContainText('Invalid credentials') + await expect(page).toHaveURL('/login') + }) +}) +``` + +## Playwright vs Cypress Comparison + +| Feature | Playwright | Cypress | +|---------|------------|---------| +| Browsers | Chromium, Firefox, WebKit | Chromium, Firefox, Electron (WebKit experimental) | +| Cross-browser | Full support | Limited | +| Parallelization | Built-in | Requires Cypress Cloud | +| Open source | Fully | Core only | +| Mobile testing | Device emulation | Limited | +| Debugging | Inspector, trace viewer | Time-travel UI | +| API testing | Built-in | Plugin required | +| Iframes | Full support | Limited | + +## Testing Vue Components with Data-Testid + +```typescript +// e2e/product-list.spec.ts +import { test, expect } from '@playwright/test' + +test('user can add product to cart', async ({ page }) => { + await page.goto('/products') + + // Use data-testid for reliable selectors + await page.getByTestId('product-card').first().click() + + // Verify product detail page + await expect(page.getByTestId('product-title')).toBeVisible() + + // Add to cart + await page.getByTestId('add-to-cart-button').click() + + // Verify cart updated + await expect(page.getByTestId('cart-count')).toHaveText('1') +}) +``` + +## Page Object Pattern for Vue Apps + +```typescript +// e2e/pages/LoginPage.ts +import { Page, Locator } from '@playwright/test' + +export class LoginPage { + readonly page: Page + readonly emailInput: Locator + readonly passwordInput: Locator + readonly submitButton: Locator + readonly errorMessage: Locator + + constructor(page: Page) { + this.page = page + this.emailInput = page.getByLabel('Email') + this.passwordInput = page.getByLabel('Password') + this.submitButton = page.getByRole('button', { name: 'Sign In' }) + this.errorMessage = page.getByRole('alert') + } + + async goto() { + await this.page.goto('/login') + } + + async login(email: string, password: string) { + await this.emailInput.fill(email) + await this.passwordInput.fill(password) + await this.submitButton.click() + } +} +``` + +```typescript +// e2e/auth.spec.ts +import { test, expect } from '@playwright/test' +import { LoginPage } from './pages/LoginPage' + +test('successful login', async ({ page }) => { + const loginPage = new LoginPage(page) + await loginPage.goto() + await loginPage.login('user@example.com', 'password123') + + await expect(page).toHaveURL('/dashboard') +}) +``` + +## Visual Regression Testing + +```typescript +test('homepage visual regression', async ({ page }) => { + await page.goto('/') + + // Full page screenshot comparison + await expect(page).toHaveScreenshot('homepage.png') + + // Element-specific screenshot + await expect(page.getByTestId('hero-section')).toHaveScreenshot('hero.png') +}) +``` + +## Running Tests + +```bash +# Run all tests +npx playwright test + +# Run in headed mode (see browser) +npx playwright test --headed + +# Run specific file +npx playwright test e2e/auth.spec.ts + +# Run in specific browser +npx playwright test --project=chromium + +# Debug mode +npx playwright test --debug + +# Generate test from actions +npx playwright codegen localhost:5173 +``` + +## Reference +- [Playwright Documentation](https://playwright.dev/) +- [Vue.js E2E Testing Recommendations](https://vuejs.org/guide/scaling-up/testing#e2e-testing) +- [Playwright Best Practices](https://playwright.dev/docs/best-practices) diff --git a/.agents/skills/vue-testing-best-practices/reference/testing-no-snapshot-only.md b/.agents/skills/vue-testing-best-practices/reference/testing-no-snapshot-only.md new file mode 100644 index 0000000..e44f437 --- /dev/null +++ b/.agents/skills/vue-testing-best-practices/reference/testing-no-snapshot-only.md @@ -0,0 +1,197 @@ +--- +title: Avoid Snapshot-Only Tests - They Don't Prove Correctness +impact: MEDIUM +impactDescription: Snapshot tests verify structure but not functionality, leading to false confidence and brittle tests +type: best-practice +tags: [vue3, testing, snapshot, vitest, vue-test-utils, anti-pattern] +--- + +# Avoid Snapshot-Only Tests - They Don't Prove Correctness + +**Impact: MEDIUM** - Snapshot tests only verify that HTML structure hasn't changed - they don't verify that the component works correctly. Relying exclusively on snapshots leads to false confidence and tests that break on any refactoring, even when functionality is preserved. + +Use snapshots sparingly for regression detection. Prefer behavioral assertions that test what the component does. + +## Task Checklist + +- [ ] Don't use snapshots as the only assertion for component behavior +- [ ] Use snapshots for regression detection on stable UI components +- [ ] Always pair snapshots with behavioral assertions +- [ ] Keep snapshots small and focused (avoid full component snapshots) +- [ ] Review snapshot diffs carefully - don't blindly update +- [ ] Consider inline snapshots for small, critical structures + +**Incorrect:** +```javascript +import { mount } from '@vue/test-utils' +import UserCard from './UserCard.vue' + +// BAD: Snapshot-only test proves nothing about functionality +test('UserCard renders correctly', () => { + const wrapper = mount(UserCard, { + props: { user: { name: 'John', email: 'john@example.com' } } + }) + + expect(wrapper.html()).toMatchSnapshot() +}) + +// This test passes even if: +// - The email isn't clickable +// - The avatar doesn't load +// - User actions are completely broken +// - Accessibility is broken +``` + +**Correct:** +```javascript +import { mount } from '@vue/test-utils' +import UserCard from './UserCard.vue' + +// CORRECT: Test actual behavior +test('UserCard displays user information', () => { + const wrapper = mount(UserCard, { + props: { user: { name: 'John', email: 'john@example.com' } } + }) + + expect(wrapper.find('[data-testid="user-name"]').text()).toBe('John') + expect(wrapper.find('[data-testid="user-email"]').text()).toBe('john@example.com') +}) + +test('UserCard email link is clickable', async () => { + const wrapper = mount(UserCard, { + props: { user: { name: 'John', email: 'john@example.com' } } + }) + + const emailLink = wrapper.find('a[href^="mailto:"]') + expect(emailLink.exists()).toBe(true) + expect(emailLink.attributes('href')).toBe('mailto:john@example.com') +}) + +test('UserCard emits select event when clicked', async () => { + const wrapper = mount(UserCard, { + props: { user: { id: 1, name: 'John' } } + }) + + await wrapper.trigger('click') + + expect(wrapper.emitted('select')).toBeTruthy() + expect(wrapper.emitted('select')[0]).toEqual([{ id: 1, name: 'John' }]) +}) +``` + +## When Snapshots ARE Useful + +### Regression Detection for Stable Components +```javascript +// ACCEPTABLE: Snapshot as additional check, not the only check +test('ErrorBoundary renders error message', () => { + const wrapper = mount(ErrorBoundary, { + props: { error: new Error('Something went wrong') } + }) + + // Primary assertions - verify behavior + expect(wrapper.find('.error-title').text()).toBe('Error') + expect(wrapper.find('.error-message').text()).toContain('Something went wrong') + + // Secondary snapshot - catches unexpected structural changes + expect(wrapper.find('.error-container').html()).toMatchSnapshot() +}) +``` + +### Inline Snapshots for Small Structures +```javascript +// ACCEPTABLE: Inline snapshot for small, critical structure +test('generates correct list markup', () => { + const wrapper = mount(ListItem, { props: { item: 'Test' } }) + + expect(wrapper.html()).toMatchInlineSnapshot(` + "
  • Test
  • " + `) +}) +``` + +### Complex SVG or Icon Output +```javascript +// ACCEPTABLE: Snapshot for complex generated content +test('renders correct chart SVG', () => { + const wrapper = mount(PieChart, { + props: { data: [30, 40, 30] } + }) + + // Verify key behavior + expect(wrapper.findAll('path').length).toBe(3) + + // Snapshot for full SVG structure + expect(wrapper.find('svg').html()).toMatchSnapshot() +}) +``` + +## Better Alternatives to Snapshots + +### Test Specific Elements +```javascript +// Instead of snapshotting entire component +test('renders product with all required fields', () => { + const wrapper = mount(ProductCard, { + props: { product: { name: 'Widget', price: 9.99, inStock: true } } + }) + + expect(wrapper.find('.product-name').text()).toBe('Widget') + expect(wrapper.find('.product-price').text()).toContain('9.99') + expect(wrapper.find('.in-stock-badge').exists()).toBe(true) +}) +``` + +### Test CSS Classes for Styling +```javascript +test('applies danger styling for errors', () => { + const wrapper = mount(Alert, { + props: { type: 'error', message: 'Failed!' } + }) + + expect(wrapper.classes()).toContain('alert-danger') + expect(wrapper.find('.alert-icon').classes()).toContain('icon-error') +}) +``` + +### Use Testing Library Queries +```javascript +import { render, screen } from '@testing-library/vue' + +test('form has accessible labels', () => { + render(LoginForm) + + // Testing Library queries verify accessibility + expect(screen.getByLabelText('Email')).toBeInTheDocument() + expect(screen.getByLabelText('Password')).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'Sign In' })).toBeInTheDocument() +}) +``` + +## Snapshot Anti-Patterns + +```javascript +// ANTI-PATTERN: Giant component snapshot +test('page renders', () => { + const wrapper = mount(EntirePageComponent) + expect(wrapper.html()).toMatchSnapshot() // 500+ lines of HTML +}) + +// ANTI-PATTERN: Snapshot with dynamic content +test('shows current date', () => { + const wrapper = mount(DateDisplay) + expect(wrapper.html()).toMatchSnapshot() // Fails every day! +}) + +// ANTI-PATTERN: Snapshot after every test +test('button works', async () => { + const wrapper = mount(Counter) + await wrapper.find('button').trigger('click') + expect(wrapper.html()).toMatchSnapshot() // Redundant +}) +``` + +## Reference +- [Vue.js Testing Guide - What Not to Test](https://vuejs.org/guide/scaling-up/testing) +- [Effective Snapshot Testing](https://kentcdodds.com/blog/effective-snapshot-testing) +- [Vitest Snapshot Testing](https://vitest.dev/guide/snapshot.html) diff --git a/.agents/skills/vue-testing-best-practices/reference/testing-pinia-store-setup.md b/.agents/skills/vue-testing-best-practices/reference/testing-pinia-store-setup.md new file mode 100644 index 0000000..3f8d440 --- /dev/null +++ b/.agents/skills/vue-testing-best-practices/reference/testing-pinia-store-setup.md @@ -0,0 +1,228 @@ +--- +title: Configure Pinia Testing with createTestingPinia and setActivePinia +impact: HIGH +impactDescription: Missing Pinia configuration causes 'injection Symbol(pinia) not found' errors and failing tests +type: gotcha +tags: [vue3, testing, pinia, vitest, store, mocking, createTestingPinia] +--- + +# Configure Pinia Testing with createTestingPinia and setActivePinia + +**Impact: HIGH** - Testing components or composables that use Pinia stores without proper configuration results in "[Vue warn]: injection Symbol(pinia) not found" errors. Tests will fail or behave unexpectedly. + +Use `@pinia/testing` package with `createTestingPinia` for component tests and `setActivePinia(createPinia())` for unit testing stores directly. + +## Task Checklist + +- [ ] Install `@pinia/testing` as a dev dependency +- [ ] Use `createTestingPinia` in component tests with `global.plugins` +- [ ] Use `setActivePinia(createPinia())` in `beforeEach` for store unit tests +- [ ] Configure `createSpy: vi.fn` when NOT using `globals: true` in Vitest +- [ ] Initialize store inside each test to get fresh state +- [ ] Use `stubActions: false` when you need real action execution + +**Incorrect:** +```javascript +import { mount } from '@vue/test-utils' +import UserProfile from './UserProfile.vue' + +// BAD: Missing Pinia - causes injection error +test('displays user name', () => { + const wrapper = mount(UserProfile) // ERROR: injection "Symbol(pinia)" not found + expect(wrapper.text()).toContain('John') +}) +``` + +```javascript +import { useUserStore } from '@/stores/user' + +// BAD: No active Pinia instance +test('user store actions', () => { + const store = useUserStore() // ERROR: no active Pinia + store.login('john', 'password') +}) +``` + +**Correct - Component Testing:** +```javascript +import { mount } from '@vue/test-utils' +import { createTestingPinia } from '@pinia/testing' +import { vi } from 'vitest' +import UserProfile from './UserProfile.vue' +import { useUserStore } from '@/stores/user' + +// CORRECT: Provide testing pinia with stubbed actions +test('displays user name', () => { + const wrapper = mount(UserProfile, { + global: { + plugins: [ + createTestingPinia({ + createSpy: vi.fn, // Required if not using globals: true + initialState: { + user: { name: 'John', email: 'john@example.com' } + } + }) + ] + } + }) + + expect(wrapper.text()).toContain('John') +}) + +// CORRECT: Test with stubbed actions (default behavior) +test('calls logout action', async () => { + const wrapper = mount(UserProfile, { + global: { + plugins: [createTestingPinia({ createSpy: vi.fn })] + } + }) + + // Get store AFTER mounting with createTestingPinia + const store = useUserStore() + + await wrapper.find('[data-testid="logout"]').trigger('click') + + // Actions are stubbed and wrapped in spies + expect(store.logout).toHaveBeenCalled() +}) +``` + +**Correct - Store Unit Testing:** +```javascript +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { setActivePinia, createPinia } from 'pinia' +import { useUserStore } from '@/stores/user' + +describe('User Store', () => { + beforeEach(() => { + // Create fresh Pinia instance for each test + setActivePinia(createPinia()) + }) + + it('initializes with empty user', () => { + const store = useUserStore() + expect(store.user).toBeNull() + expect(store.isLoggedIn).toBe(false) + }) + + it('updates user on login', async () => { + const store = useUserStore() + + // Real action executes - not stubbed + await store.login('john', 'password') + + expect(store.user).toEqual({ name: 'John' }) + expect(store.isLoggedIn).toBe(true) + }) + + it('clears user on logout', () => { + const store = useUserStore() + store.user = { name: 'John' } // Set initial state + + store.logout() + + expect(store.user).toBeNull() + }) +}) +``` + +## Testing with Real Actions vs Stubbed Actions + +```javascript +import { createTestingPinia } from '@pinia/testing' + +// Stubbed actions (default) - for isolation +const wrapper = mount(Component, { + global: { + plugins: [ + createTestingPinia({ + createSpy: vi.fn, + // stubActions: true (default) - actions are mocked + }) + ] + } +}) + +// Real actions - for integration testing +const wrapper = mount(Component, { + global: { + plugins: [ + createTestingPinia({ + createSpy: vi.fn, + stubActions: false // Actions execute normally + }) + ] + } +}) +``` + +## Mocking Specific Action Implementations + +```javascript +import { mount } from '@vue/test-utils' +import { createTestingPinia } from '@pinia/testing' +import { vi } from 'vitest' +import { useCartStore } from '@/stores/cart' + +test('handles checkout failure', async () => { + const wrapper = mount(Checkout, { + global: { + plugins: [createTestingPinia({ createSpy: vi.fn })] + } + }) + + const cartStore = useCartStore() + + // Mock specific action behavior + cartStore.checkout.mockRejectedValue(new Error('Payment failed')) + + await wrapper.find('[data-testid="checkout"]').trigger('click') + await flushPromises() + + expect(wrapper.find('.error').text()).toContain('Payment failed') +}) +``` + +## Spying on Actions with vi.spyOn + +```javascript +import { setActivePinia, createPinia } from 'pinia' +import { vi } from 'vitest' +import { useUserStore } from '@/stores/user' + +test('tracks action calls', async () => { + setActivePinia(createPinia()) + const store = useUserStore() + + const loginSpy = vi.spyOn(store, 'login') + loginSpy.mockResolvedValue({ success: true }) + + await store.login('john', 'password') + + expect(loginSpy).toHaveBeenCalledWith('john', 'password') +}) +``` + +## Testing Store $subscribe + +```javascript +import { setActivePinia, createPinia } from 'pinia' +import { useUserStore } from '@/stores/user' + +test('subscription triggers on state change', () => { + setActivePinia(createPinia()) + const store = useUserStore() + + const callback = vi.fn() + store.$subscribe(callback) + + store.user = { name: 'John' } + + expect(callback).toHaveBeenCalled() +}) +``` + +## Reference +- [Pinia Testing Guide](https://pinia.vuejs.org/cookbook/testing.html) +- [@pinia/testing Package](https://www.npmjs.com/package/@pinia/testing) +- [Vue Test Utils - Plugins](https://test-utils.vuejs.org/guide/advanced/plugins.html) diff --git a/.agents/skills/vue-testing-best-practices/reference/testing-suspense-async-components.md b/.agents/skills/vue-testing-best-practices/reference/testing-suspense-async-components.md new file mode 100644 index 0000000..f935533 --- /dev/null +++ b/.agents/skills/vue-testing-best-practices/reference/testing-suspense-async-components.md @@ -0,0 +1,229 @@ +--- +title: Wrap Async Setup Components in Suspense for Testing +impact: HIGH +impactDescription: Components with async setup() fail to render in tests without Suspense wrapper, causing cryptic errors +type: gotcha +tags: [vue3, testing, suspense, async-setup, vue-test-utils, vitest] +--- + +# Wrap Async Setup Components in Suspense for Testing + +**Impact: HIGH** - Components using `async setup()` require a `` wrapper to function correctly. Testing them without Suspense causes the component to never render, leading to test failures and confusing errors. + +Create a test wrapper component with Suspense or use a `mountSuspense` helper function for testing async components. + +## Task Checklist + +- [ ] Identify components with async setup (uses `await` in ` + + +``` + +### Key Imports + +```ts +// Reactivity +import { ref, shallowRef, computed, reactive, readonly, toRef, toRefs, toValue } from 'vue' + +// Watchers +import { watch, watchEffect, watchPostEffect, onWatcherCleanup } from 'vue' + +// Lifecycle +import { onMounted, onUpdated, onUnmounted, onBeforeMount, onBeforeUpdate, onBeforeUnmount } from 'vue' + +// Utilities +import { nextTick, defineComponent, defineAsyncComponent } from 'vue' +``` diff --git a/.agents/skills/vue/references/advanced-patterns.md b/.agents/skills/vue/references/advanced-patterns.md new file mode 100644 index 0000000..243d040 --- /dev/null +++ b/.agents/skills/vue/references/advanced-patterns.md @@ -0,0 +1,314 @@ +--- +name: advanced-patterns +description: Vue 3 built-in components (Transition, Teleport, Suspense, KeepAlive) and advanced directives +--- + +# Built-in Components & Directives + +## Transition + +Animate enter/leave of a single element or component. + +```vue + + + +``` + +### CSS Classes + +| Class | When | +|-------|------| +| `{name}-enter-from` | Start state for enter | +| `{name}-enter-active` | Active state for enter (add transitions here) | +| `{name}-enter-to` | End state for enter | +| `{name}-leave-from` | Start state for leave | +| `{name}-leave-active` | Active state for leave | +| `{name}-leave-to` | End state for leave | + +### Transition Modes + +```vue + + + + +``` + +### JavaScript Hooks + +```vue + +
    Content
    +
    + + +``` + +### Appear on Initial Render + +```vue + +
    Shows with animation on mount
    +
    +``` + +## TransitionGroup + +Animate list items. Each child must have a unique `key`. + +```vue + + + +``` + +## Teleport + +Render content to a different DOM location. + +```vue + +``` + +### Props + +```vue + + + + + + + + + + + +``` + +## Suspense + +Handle async dependencies with loading states. **Experimental feature.** + +```vue + +``` + +### Async Dependencies + +Suspense waits for: +- Components with `async setup()` +- Components using top-level `await` in ` +``` + +### Events + +```vue + + ... + +``` + +## KeepAlive + +Cache component instances when toggled. + +```vue + +``` + +### Include/Exclude + +```vue + + + + + + + + + + +``` + +### Lifecycle Hooks + +```ts +import { onActivated, onDeactivated } from 'vue' + +onActivated(() => { + // Called when component is inserted from cache + fetchLatestData() +}) + +onDeactivated(() => { + // Called when component is removed to cache + pauseTimers() +}) +``` + +## v-memo + +Skip re-renders when dependencies unchanged. Use for performance optimization. + +```vue + +``` + +Equivalent to `v-once` when empty: +```vue +
    Never updates
    +``` + +## v-once + +Render once, skip all future updates. + +```vue +Static: {{ neverChanges }} +``` + +## Custom Directives + +Create reusable DOM manipulations. + +```ts +// Directive definition +const vFocus: Directive = { + mounted: (el) => el.focus() +} + +// Full hooks +const vColor: Directive = { + created(el, binding, vnode, prevVnode) {}, + beforeMount(el, binding) {}, + mounted(el, binding) { + el.style.color = binding.value + }, + beforeUpdate(el, binding) {}, + updated(el, binding) { + el.style.color = binding.value + }, + beforeUnmount(el, binding) {}, + unmounted(el, binding) {} +} +``` + +### Directive Arguments & Modifiers + +```vue +
    + + +``` + +### Global Registration + +```ts +// main.ts +app.directive('focus', { + mounted: (el) => el.focus() +}) +``` + + diff --git a/.agents/skills/vue/references/core-new-apis.md b/.agents/skills/vue/references/core-new-apis.md new file mode 100644 index 0000000..4c6a9b8 --- /dev/null +++ b/.agents/skills/vue/references/core-new-apis.md @@ -0,0 +1,264 @@ +--- +name: core-new-apis +description: Vue 3 reactivity system, lifecycle hooks, and composable patterns +--- + +# Reactivity, Lifecycle & Composables + +## Reactivity + +### ref vs shallowRef + +```ts +import { ref, shallowRef } from 'vue' + +// ref - deep reactivity (tracks nested changes) +const user = ref({ name: 'John', profile: { age: 30 } }) +user.value.profile.age = 31 // Triggers reactivity + +// shallowRef - only .value assignment triggers reactivity (better performance) +const data = shallowRef({ items: [] }) +data.value.items.push('new') // Does NOT trigger reactivity +data.value = { items: ['new'] } // Triggers reactivity +``` + +**Prefer `shallowRef`** for large data structures or when deep reactivity is unnecessary. + +### computed + +```ts +import { ref, computed } from 'vue' + +const count = ref(0) + +// Read-only computed +const doubled = computed(() => count.value * 2) + +// Writable computed +const plusOne = computed({ + get: () => count.value + 1, + set: (val) => { count.value = val - 1 } +}) +``` + +### reactive & readonly + +```ts +import { reactive, readonly } from 'vue' + +const state = reactive({ count: 0, nested: { value: 1 } }) +state.count++ // Reactive + +const readonlyState = readonly(state) +readonlyState.count++ // Warning, mutation blocked +``` + +Note: `reactive()` loses reactivity on destructuring. Use `ref()` or `toRefs()`. + +## Watchers + +### watch + +```ts +import { ref, watch } from 'vue' + +const count = ref(0) + +// Watch single ref +watch(count, (newVal, oldVal) => { + console.log(`Changed from ${oldVal} to ${newVal}`) +}) + +// Watch getter +watch( + () => props.id, + (id) => fetchData(id), + { immediate: true } +) + +// Watch multiple sources +watch([firstName, lastName], ([first, last]) => { + fullName.value = `${first} ${last}` +}) + +// Deep watch with depth limit (Vue 3.5+) +watch(state, callback, { deep: 2 }) + +// Once (Vue 3.4+) +watch(source, callback, { once: true }) +``` + +### watchEffect + +Runs immediately and auto-tracks dependencies. + +```ts +import { ref, watchEffect, onWatcherCleanup } from 'vue' + +const id = ref(1) + +watchEffect(async () => { + const controller = new AbortController() + + // Cleanup on re-run or unmount (Vue 3.5+) + onWatcherCleanup(() => controller.abort()) + + const res = await fetch(`/api/${id.value}`, { signal: controller.signal }) + data.value = await res.json() +}) + +// Pause/resume (Vue 3.5+) +const { pause, resume, stop } = watchEffect(() => {}) +pause() +resume() +stop() +``` + +### Flush Timing + +```ts +// 'pre' (default) - before component update +// 'post' - after component update (access updated DOM) +// 'sync' - immediate, use with caution + +watch(source, callback, { flush: 'post' }) +watchPostEffect(() => {}) // Alias for flush: 'post' +``` + +## Lifecycle Hooks + +```ts +import { + onBeforeMount, + onMounted, + onBeforeUpdate, + onUpdated, + onBeforeUnmount, + onUnmounted, + onErrorCaptured, + onActivated, // KeepAlive + onDeactivated, // KeepAlive + onServerPrefetch // SSR only +} from 'vue' + +onMounted(() => { + console.log('DOM is ready') +}) + +onUnmounted(() => { + // Cleanup timers, listeners, etc. +}) + +// Error boundary +onErrorCaptured((err, instance, info) => { + console.error(err) + return false // Stop propagation +}) +``` + +## Effect Scope + +Group reactive effects for batch disposal. + +```ts +import { effectScope, onScopeDispose } from 'vue' + +const scope = effectScope() + +scope.run(() => { + const count = ref(0) + const doubled = computed(() => count.value * 2) + + watch(count, () => console.log(count.value)) + + // Cleanup when scope stops + onScopeDispose(() => { + console.log('Scope disposed') + }) +}) + +// Dispose all effects +scope.stop() +``` + +## Composables + +Composables are functions that encapsulate stateful logic using Composition API. + +### Naming Convention + +- Start with `use`: `useMouse`, `useFetch`, `useCounter` + +### Pattern + +```ts +// composables/useMouse.ts +import { ref, onMounted, onUnmounted } from 'vue' + +export function useMouse() { + const x = ref(0) + const y = ref(0) + + const update = (e: MouseEvent) => { + x.value = e.pageX + y.value = e.pageY + } + + onMounted(() => window.addEventListener('mousemove', update)) + onUnmounted(() => window.removeEventListener('mousemove', update)) + + return { x, y } +} +``` + +### Accept Reactive Input + +Use `toValue()` (Vue 3.3+) to normalize refs, getters, or plain values. + +```ts +import { ref, watchEffect, toValue, type MaybeRefOrGetter } from 'vue' + +export function useFetch(url: MaybeRefOrGetter) { + const data = ref(null) + const error = ref(null) + + watchEffect(async () => { + data.value = null + error.value = null + + try { + const res = await fetch(toValue(url)) + data.value = await res.json() + } catch (e) { + error.value = e + } + }) + + return { data, error } +} + +// Usage - all work: +useFetch('/api/users') +useFetch(urlRef) +useFetch(() => `/api/users/${props.id}`) +``` + +### Return Refs (Not Reactive) + +Always return plain object with refs for destructuring compatibility. + +```ts +// Good - preserves reactivity when destructured +return { x, y } + +// Bad - loses reactivity when destructured +return reactive({ x, y }) +``` + + diff --git a/.agents/skills/vue/references/script-setup-macros.md b/.agents/skills/vue/references/script-setup-macros.md new file mode 100644 index 0000000..a0caaa9 --- /dev/null +++ b/.agents/skills/vue/references/script-setup-macros.md @@ -0,0 +1,204 @@ +--- +name: script-setup-macros +description: Vue 3 script setup syntax and compiler macros for defining props, emits, models, and more +--- + +# Script Setup & Macros + +` + + +``` + +## defineProps + +Declare component props with full TypeScript support. + +```ts +// Type-based declaration (recommended) +const props = defineProps<{ + title: string + count?: number + items: string[] +}>() + +// With defaults (Vue 3.5+) +const { title, count = 0 } = defineProps<{ + title: string + count?: number +}>() + +// With defaults (Vue 3.4 and below) +const props = withDefaults(defineProps<{ + title: string + items?: string[] +}>(), { + items: () => [] // Use factory for arrays/objects +}) +``` + +## defineEmits + +Declare emitted events with typed payloads. + +```ts +// Named tuple syntax (recommended) +const emit = defineEmits<{ + update: [value: string] + change: [id: number, name: string] + close: [] +}>() + +emit('update', 'new value') +emit('change', 1, 'name') +emit('close') +``` + +## defineModel + +Two-way binding prop consumed via `v-model`. Available in Vue 3.4+. + +```ts +// Basic usage - creates "modelValue" prop +const model = defineModel() +model.value = 'hello' // Emits "update:modelValue" + +// Named model - consumed via v-model:name +const count = defineModel('count', { default: 0 }) + +// With modifiers +const [value, modifiers] = defineModel() +if (modifiers.trim) { + // Handle trim modifier +} + +// With transformers +const [value, modifiers] = defineModel({ + get(val) { return val?.toLowerCase() }, + set(val) { return modifiers.trim ? val?.trim() : val } +}) +``` + +Parent usage: +```vue + + + +``` + +## defineExpose + +Explicitly expose properties to parent via template refs. Components are closed by default. + +```ts +import { ref } from 'vue' + +const count = ref(0) +const reset = () => { count.value = 0 } + +defineExpose({ + count, + reset +}) +``` + +Parent access: +```ts +const childRef = ref<{ count: number; reset: () => void }>() +childRef.value?.reset() +``` + +## defineOptions + +Declare component options without a separate ` +``` + +Multiple generics with constraints: +```vue + +``` + +## Local Custom Directives + +Use `vNameOfDirective` naming convention. + +```ts +const vFocus = { + mounted: (el: HTMLElement) => el.focus() +} + +// Or import and rename +import { myDirective as vMyDirective } from './directives' +``` + +```vue + +``` + +## Top-level await + +Use `await` directly in ` +``` + + diff --git a/.agents/skills/vueuse-functions/LICENSE.md b/.agents/skills/vueuse-functions/LICENSE.md new file mode 100644 index 0000000..8a1060d --- /dev/null +++ b/.agents/skills/vueuse-functions/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 SerKo + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/.agents/skills/vueuse-functions/SKILL.md b/.agents/skills/vueuse-functions/SKILL.md new file mode 100644 index 0000000..cb8cdb0 --- /dev/null +++ b/.agents/skills/vueuse-functions/SKILL.md @@ -0,0 +1,419 @@ +--- +name: vueuse-functions +description: Apply VueUse composables where appropriate to build concise, maintainable Vue.js / Nuxt features. +license: MIT +metadata: + author: SerKo + version: "1.0" +compatibility: Requires Vue 3 (or above) or Nuxt 3 (or above) project +--- + +# VueUse Functions + +This skill is a decision-and-implementation guide for VueUse composables in Vue.js / Nuxt projects. It maps requirements to the most suitable VueUse function, applies the correct usage pattern, and prefers composable-based solutions over bespoke code to keep implementations concise, maintainable, and performant. + +## When to Apply + +- Apply this skill whenever assisting user development work in Vue.js / Nuxt. +- Always check first whether a VueUse function can implement the requirement. +- Prefer VueUse composables over custom code to improve readability, maintainability, and performance. +- Map requirements to the most appropriate VueUse function and follow the function’s invocation rule. +- Please refer to the `Invocation` field in the below functions table. For example: + - `AUTO`: Use automatically when applicable. + - `EXTERNAL`: Use only if the user already installed the required external dependency; otherwise reconsider, and ask to install only if truly needed. + - `EXPLICIT_ONLY`: Use only when explicitly requested by the user. + > *NOTE* User instructions in the prompt or `AGENTS.md` may override a function’s default `Invocation` rule. + +## Functions + +All functions listed below are part of the [VueUse](https://vueuse.org/) library, each section categorizes functions based on their functionality. + +IMPORTANT: Each function entry includes a short `Description` and a detailed `Reference`. When using any function, always consult the corresponding document in `./references` for Usage details and Type Declarations. + +### State + +| Function | Description | Invocation | +|----------|-------------|------------| +| [`createGlobalState`](references/createGlobalState.md) | Keep states in the global scope to be reusable across Vue instances | AUTO | +| [`createInjectionState`](references/createInjectionState.md) | Create global state that can be injected into components | AUTO | +| [`createSharedComposable`](references/createSharedComposable.md) | Make a composable function usable with multiple Vue instances | AUTO | +| [`injectLocal`](references/injectLocal.md) | Extended `inject` with ability to call `provideLocal` to provide the value in the same component | AUTO | +| [`provideLocal`](references/provideLocal.md) | Extended `provide` with ability to call `injectLocal` to obtain the value in the same component | AUTO | +| [`useAsyncState`](references/useAsyncState.md) | Reactive async state | AUTO | +| [`useDebouncedRefHistory`](references/useDebouncedRefHistory.md) | Shorthand for `useRefHistory` with debounced filter | AUTO | +| [`useLastChanged`](references/useLastChanged.md) | Records the timestamp of the last change | AUTO | +| [`useLocalStorage`](references/useLocalStorage.md) | Reactive [LocalStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) | AUTO | +| [`useManualRefHistory`](references/useManualRefHistory.md) | Manually track the change history of a ref when the using calls `commit()` | AUTO | +| [`useRefHistory`](references/useRefHistory.md) | Track the change history of a ref | AUTO | +| [`useSessionStorage`](references/useSessionStorage.md) | Reactive [SessionStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage) | AUTO | +| [`useStorage`](references/useStorage.md) | Create a reactive ref that can be used to access & modify [LocalStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) or [SessionStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage) | AUTO | +| [`useStorageAsync`](references/useStorageAsync.md) | Reactive Storage in with async support | AUTO | +| [`useThrottledRefHistory`](references/useThrottledRefHistory.md) | Shorthand for `useRefHistory` with throttled filter | AUTO | + +### Elements + +| Function | Description | Invocation | +|----------|-------------|------------| +| [`useActiveElement`](references/useActiveElement.md) | Reactive `document.activeElement` | AUTO | +| [`useDocumentVisibility`](references/useDocumentVisibility.md) | Reactively track [`document.visibilityState`](https://developer.mozilla.org/en-US/docs/Web/API/Document/visibilityState) | AUTO | +| [`useDraggable`](references/useDraggable.md) | Make elements draggable | AUTO | +| [`useDropZone`](references/useDropZone.md) | Create a zone where files can be dropped | AUTO | +| [`useElementBounding`](references/useElementBounding.md) | Reactive [bounding box](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect) of an HTML element | AUTO | +| [`useElementSize`](references/useElementSize.md) | Reactive size of an HTML element | AUTO | +| [`useElementVisibility`](references/useElementVisibility.md) | Tracks the visibility of an element within the viewport | AUTO | +| [`useIntersectionObserver`](references/useIntersectionObserver.md) | Detects that a target element's visibility | AUTO | +| [`useMouseInElement`](references/useMouseInElement.md) | Reactive mouse position related to an element | AUTO | +| [`useMutationObserver`](references/useMutationObserver.md) | Watch for changes being made to the DOM tree | AUTO | +| [`useParentElement`](references/useParentElement.md) | Get parent element of the given element | AUTO | +| [`useResizeObserver`](references/useResizeObserver.md) | Reports changes to the dimensions of an Element's content or the border-box | AUTO | +| [`useWindowFocus`](references/useWindowFocus.md) | Reactively track window focus with `window.onfocus` and `window.onblur` events | AUTO | +| [`useWindowScroll`](references/useWindowScroll.md) | Reactive window scroll | AUTO | +| [`useWindowSize`](references/useWindowSize.md) | Reactive window size | AUTO | + +### Browser + +| Function | Description | Invocation | +|----------|-------------|------------| +| [`useBluetooth`](references/useBluetooth.md) | Reactive [Web Bluetooth API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API) | AUTO | +| [`useBreakpoints`](references/useBreakpoints.md) | Reactive viewport breakpoints | AUTO | +| [`useBroadcastChannel`](references/useBroadcastChannel.md) | Reactive [BroadcastChannel API](https://developer.mozilla.org/en-US/docs/Web/API/BroadcastChannel) | AUTO | +| [`useBrowserLocation`](references/useBrowserLocation.md) | Reactive browser location | AUTO | +| [`useClipboard`](references/useClipboard.md) | Reactive [Clipboard API](https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API) | AUTO | +| [`useClipboardItems`](references/useClipboardItems.md) | Reactive [Clipboard API](https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API) | AUTO | +| [`useColorMode`](references/useColorMode.md) | Reactive color mode (dark / light / customs) with auto data persistence | AUTO | +| [`useCssSupports`](references/useCssSupports.md) | SSR compatible and reactive [`CSS.supports`](https://developer.mozilla.org/docs/Web/API/CSS/supports_static) | AUTO | +| [`useCssVar`](references/useCssVar.md) | Manipulate CSS variables | AUTO | +| [`useDark`](references/useDark.md) | Reactive dark mode with auto data persistence | AUTO | +| [`useEventListener`](references/useEventListener.md) | Use EventListener with ease | AUTO | +| [`useEyeDropper`](references/useEyeDropper.md) | Reactive [EyeDropper API](https://developer.mozilla.org/en-US/docs/Web/API/EyeDropper_API) | AUTO | +| [`useFavicon`](references/useFavicon.md) | Reactive favicon | AUTO | +| [`useFileDialog`](references/useFileDialog.md) | Open file dialog with ease | AUTO | +| [`useFileSystemAccess`](references/useFileSystemAccess.md) | Create and read and write local files with [FileSystemAccessAPI](https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API) | AUTO | +| [`useFullscreen`](references/useFullscreen.md) | Reactive [Fullscreen API](https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API) | AUTO | +| [`useGamepad`](references/useGamepad.md) | Provides reactive bindings for the [Gamepad API](https://developer.mozilla.org/en-US/docs/Web/API/Gamepad_API) | AUTO | +| [`useImage`](references/useImage.md) | Reactive load an image in the browser | AUTO | +| [`useMediaControls`](references/useMediaControls.md) | Reactive media controls for both `audio` and `video` elements | AUTO | +| [`useMediaQuery`](references/useMediaQuery.md) | Reactive [Media Query](https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries/Testing_media_queries) | AUTO | +| [`useMemory`](references/useMemory.md) | Reactive Memory Info | AUTO | +| [`useObjectUrl`](references/useObjectUrl.md) | Reactive URL representing an object | AUTO | +| [`usePerformanceObserver`](references/usePerformanceObserver.md) | Observe performance metrics | AUTO | +| [`usePermission`](references/usePermission.md) | Reactive [Permissions API](https://developer.mozilla.org/en-US/docs/Web/API/Permissions_API) | AUTO | +| [`usePreferredColorScheme`](references/usePreferredColorScheme.md) | Reactive [prefers-color-scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme) media query | AUTO | +| [`usePreferredContrast`](references/usePreferredContrast.md) | Reactive [prefers-contrast](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-contrast) media query | AUTO | +| [`usePreferredDark`](references/usePreferredDark.md) | Reactive dark theme preference | AUTO | +| [`usePreferredLanguages`](references/usePreferredLanguages.md) | Reactive [Navigator Languages](https://developer.mozilla.org/en-US/docs/Web/API/NavigatorLanguage/languages) | AUTO | +| [`usePreferredReducedMotion`](references/usePreferredReducedMotion.md) | Reactive [prefers-reduced-motion](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion) media query | AUTO | +| [`usePreferredReducedTransparency`](references/usePreferredReducedTransparency.md) | Reactive [prefers-reduced-transparency](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-transparency) media query | AUTO | +| [`useScreenOrientation`](references/useScreenOrientation.md) | Reactive [Screen Orientation API](https://developer.mozilla.org/en-US/docs/Web/API/Screen_Orientation_API) | AUTO | +| [`useScreenSafeArea`](references/useScreenSafeArea.md) | Reactive `env(safe-area-inset-*)` | AUTO | +| [`useScriptTag`](references/useScriptTag.md) | Creates a script tag | AUTO | +| [`useShare`](references/useShare.md) | Reactive [Web Share API](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/share) | AUTO | +| [`useSSRWidth`](references/useSSRWidth.md) | Used to set a global viewport width which will be used when rendering SSR components that rely on the viewport width like [`useMediaQuery`](../useMediaQuery/index.md) or [`useBreakpoints`](../useBreakpoints/index.md) | AUTO | +| [`useStyleTag`](references/useStyleTag.md) | Inject reactive `style` element in head | AUTO | +| [`useTextareaAutosize`](references/useTextareaAutosize.md) | Automatically update the height of a textarea depending on the content | AUTO | +| [`useTextDirection`](references/useTextDirection.md) | Reactive [dir](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/dir) of the element's text | AUTO | +| [`useTitle`](references/useTitle.md) | Reactive document title | AUTO | +| [`useUrlSearchParams`](references/useUrlSearchParams.md) | Reactive [URLSearchParams](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams) | AUTO | +| [`useVibrate`](references/useVibrate.md) | Reactive [Vibration API](https://developer.mozilla.org/en-US/docs/Web/API/Vibration_API) | AUTO | +| [`useWakeLock`](references/useWakeLock.md) | Reactive [Screen Wake Lock API](https://developer.mozilla.org/en-US/docs/Web/API/Screen_Wake_Lock_API) | AUTO | +| [`useWebNotification`](references/useWebNotification.md) | Reactive [Notification](https://developer.mozilla.org/en-US/docs/Web/API/notification) | AUTO | +| [`useWebWorker`](references/useWebWorker.md) | Simple [Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers) registration and communication | AUTO | +| [`useWebWorkerFn`](references/useWebWorkerFn.md) | Run expensive functions without blocking the UI | AUTO | + +### Sensors + +| Function | Description | Invocation | +|----------|-------------|------------| +| [`onClickOutside`](references/onClickOutside.md) | Listen for clicks outside of an element | AUTO | +| [`onElementRemoval`](references/onElementRemoval.md) | Fires when the element or any element containing it is removed from the DOM | AUTO | +| [`onKeyStroke`](references/onKeyStroke.md) | Listen for keyboard keystrokes | AUTO | +| [`onLongPress`](references/onLongPress.md) | Listen for a long press on an element | AUTO | +| [`onStartTyping`](references/onStartTyping.md) | Fires when users start typing on non-editable elements | AUTO | +| [`useBattery`](references/useBattery.md) | Reactive [Battery Status API](https://developer.mozilla.org/en-US/docs/Web/API/Battery_Status_API) | AUTO | +| [`useDeviceMotion`](references/useDeviceMotion.md) | Reactive [DeviceMotionEvent](https://developer.mozilla.org/en-US/docs/Web/API/DeviceMotionEvent) | AUTO | +| [`useDeviceOrientation`](references/useDeviceOrientation.md) | Reactive [DeviceOrientationEvent](https://developer.mozilla.org/en-US/docs/Web/API/DeviceOrientationEvent) | AUTO | +| [`useDevicePixelRatio`](references/useDevicePixelRatio.md) | Reactively track [`window.devicePixelRatio`](https://developer.mozilla.org/docs/Web/API/Window/devicePixelRatio) | AUTO | +| [`useDevicesList`](references/useDevicesList.md) | Reactive [enumerateDevices](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/enumerateDevices) listing available input/output devices | AUTO | +| [`useDisplayMedia`](references/useDisplayMedia.md) | Reactive [`mediaDevices.getDisplayMedia`](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia) streaming | AUTO | +| [`useElementByPoint`](references/useElementByPoint.md) | Reactive element by point | AUTO | +| [`useElementHover`](references/useElementHover.md) | Reactive element's hover state | AUTO | +| [`useFocus`](references/useFocus.md) | Reactive utility to track or set the focus state of a DOM element | AUTO | +| [`useFocusWithin`](references/useFocusWithin.md) | Reactive utility to track if an element or one of its decendants has focus | AUTO | +| [`useFps`](references/useFps.md) | Reactive FPS (frames per second) | AUTO | +| [`useGeolocation`](references/useGeolocation.md) | Reactive [Geolocation API](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API) | AUTO | +| [`useIdle`](references/useIdle.md) | Tracks whether the user is being inactive | AUTO | +| [`useInfiniteScroll`](references/useInfiniteScroll.md) | Infinite scrolling of the element | AUTO | +| [`useKeyModifier`](references/useKeyModifier.md) | Reactive [Modifier State](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/getModifierState) | AUTO | +| [`useMagicKeys`](references/useMagicKeys.md) | Reactive keys pressed state | AUTO | +| [`useMouse`](references/useMouse.md) | Reactive mouse position | AUTO | +| [`useMousePressed`](references/useMousePressed.md) | Reactive mouse pressing state | AUTO | +| [`useNavigatorLanguage`](references/useNavigatorLanguage.md) | Reactive [navigator.language](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/language) | AUTO | +| [`useNetwork`](references/useNetwork.md) | Reactive [Network status](https://developer.mozilla.org/en-US/docs/Web/API/Network_Information_API) | AUTO | +| [`useOnline`](references/useOnline.md) | Reactive online state | AUTO | +| [`usePageLeave`](references/usePageLeave.md) | Reactive state to show whether the mouse leaves the page | AUTO | +| [`useParallax`](references/useParallax.md) | Create parallax effect easily | AUTO | +| [`usePointer`](references/usePointer.md) | Reactive [pointer state](https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events) | AUTO | +| [`usePointerLock`](references/usePointerLock.md) | Reactive [pointer lock](https://developer.mozilla.org/en-US/docs/Web/API/Pointer_Lock_API) | AUTO | +| [`usePointerSwipe`](references/usePointerSwipe.md) | Reactive swipe detection based on [PointerEvents](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent) | AUTO | +| [`useScroll`](references/useScroll.md) | Reactive scroll position and state | AUTO | +| [`useScrollLock`](references/useScrollLock.md) | Lock scrolling of the element | AUTO | +| [`useSpeechRecognition`](references/useSpeechRecognition.md) | Reactive [SpeechRecognition](https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognition) | AUTO | +| [`useSpeechSynthesis`](references/useSpeechSynthesis.md) | Reactive [SpeechSynthesis](https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesis) | AUTO | +| [`useSwipe`](references/useSwipe.md) | Reactive swipe detection based on [`TouchEvents`](https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent) | AUTO | +| [`useTextSelection`](references/useTextSelection.md) | Reactively track user text selection based on [`Window.getSelection`](https://developer.mozilla.org/en-US/docs/Web/API/Window/getSelection) | AUTO | +| [`useUserMedia`](references/useUserMedia.md) | Reactive [`mediaDevices.getUserMedia`](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia) streaming | AUTO | + +### Network + +| Function | Description | Invocation | +|----------|-------------|------------| +| [`useEventSource`](references/useEventSource.md) | An [EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource) or [Server-Sent-Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) instance opens a persistent connection to an HTTP server | AUTO | +| [`useFetch`](references/useFetch.md) | Reactive [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) provides the ability to abort requests | AUTO | +| [`useWebSocket`](references/useWebSocket.md) | Reactive [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket) client | AUTO | + +### Animation + +| Function | Description | Invocation | +|----------|-------------|------------| +| [`useAnimate`](references/useAnimate.md) | Reactive [Web Animations API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API) | AUTO | +| [`useInterval`](references/useInterval.md) | Reactive counter that increases on every interval | AUTO | +| [`useIntervalFn`](references/useIntervalFn.md) | Wrapper for `setInterval` with controls | AUTO | +| [`useNow`](references/useNow.md) | Reactive current Date instance | AUTO | +| [`useRafFn`](references/useRafFn.md) | Call function on every `requestAnimationFrame` | AUTO | +| [`useTimeout`](references/useTimeout.md) | Reactive value that becomes `true` after a given time | AUTO | +| [`useTimeoutFn`](references/useTimeoutFn.md) | Wrapper for `setTimeout` with controls | AUTO | +| [`useTimestamp`](references/useTimestamp.md) | Reactive current timestamp | AUTO | +| [`useTransition`](references/useTransition.md) | Transition between values | AUTO | + +### Component + +| Function | Description | Invocation | +|----------|-------------|------------| +| [`computedInject`](references/computedInject.md) | Combine `computed` and `inject` | AUTO | +| [`createReusableTemplate`](references/createReusableTemplate.md) | Define and reuse template inside the component scope | AUTO | +| [`createTemplatePromise`](references/createTemplatePromise.md) | Template as Promise | AUTO | +| [`templateRef`](references/templateRef.md) | Shorthand for binding ref to template element | AUTO | +| [`tryOnBeforeMount`](references/tryOnBeforeMount.md) | Safe `onBeforeMount` | AUTO | +| [`tryOnBeforeUnmount`](references/tryOnBeforeUnmount.md) | Safe `onBeforeUnmount` | AUTO | +| [`tryOnMounted`](references/tryOnMounted.md) | Safe `onMounted` | AUTO | +| [`tryOnScopeDispose`](references/tryOnScopeDispose.md) | Safe `onScopeDispose` | AUTO | +| [`tryOnUnmounted`](references/tryOnUnmounted.md) | Safe `onUnmounted` | AUTO | +| [`unrefElement`](references/unrefElement.md) | Retrieves the underlying DOM element from a Vue ref or component instance | AUTO | +| [`useCurrentElement`](references/useCurrentElement.md) | Get the DOM element of current component as a ref | AUTO | +| [`useMounted`](references/useMounted.md) | Mounted state in ref | AUTO | +| [`useTemplateRefsList`](references/useTemplateRefsList.md) | Shorthand for binding refs to template elements and components inside `v-for` | AUTO | +| [`useVirtualList`](references/useVirtualList.md) | Create virtual lists with ease | AUTO | +| [`useVModel`](references/useVModel.md) | Shorthand for v-model binding | AUTO | +| [`useVModels`](references/useVModels.md) | Shorthand for props v-model binding | AUTO | + +### Watch + +| Function | Description | Invocation | +|----------|-------------|------------| +| [`until`](references/until.md) | Promised one-time watch for changes | AUTO | +| [`watchArray`](references/watchArray.md) | Watch for an array with additions and removals | AUTO | +| [`watchAtMost`](references/watchAtMost.md) | `watch` with the number of times triggered | AUTO | +| [`watchDebounced`](references/watchDebounced.md) | Debounced watch | AUTO | +| [`watchDeep`](references/watchDeep.md) | Shorthand for watching value with `{deep: true}` | AUTO | +| [`watchIgnorable`](references/watchIgnorable.md) | Ignorable watch | AUTO | +| [`watchImmediate`](references/watchImmediate.md) | Shorthand for watching value with `{immediate: true}` | AUTO | +| [`watchOnce`](references/watchOnce.md) | Shorthand for watching value with `{ once: true }` | AUTO | +| [`watchPausable`](references/watchPausable.md) | Pausable watch | AUTO | +| [`watchThrottled`](references/watchThrottled.md) | Throttled watch | AUTO | +| [`watchTriggerable`](references/watchTriggerable.md) | Watch that can be triggered manually | AUTO | +| [`watchWithFilter`](references/watchWithFilter.md) | `watch` with additional EventFilter control | AUTO | +| [`whenever`](references/whenever.md) | Shorthand for watching value to be truthy | AUTO | + +### Reactivity + +| Function | Description | Invocation | +|----------|-------------|------------| +| [`computedAsync`](references/computedAsync.md) | Computed for async functions | AUTO | +| [`computedEager`](references/computedEager.md) | Eager computed without lazy evaluation | AUTO | +| [`computedWithControl`](references/computedWithControl.md) | Explicitly define the dependencies of computed | AUTO | +| [`createRef`](references/createRef.md) | Returns a `deepRef` or `shallowRef` depending on the `deep` param | AUTO | +| [`extendRef`](references/extendRef.md) | Add extra attributes to Ref | AUTO | +| [`reactify`](references/reactify.md) | Converts plain functions into reactive functions | AUTO | +| [`reactifyObject`](references/reactifyObject.md) | Apply `reactify` to an object | AUTO | +| [`reactiveComputed`](references/reactiveComputed.md) | Computed reactive object | AUTO | +| [`reactiveOmit`](references/reactiveOmit.md) | Reactively omit fields from a reactive object | AUTO | +| [`reactivePick`](references/reactivePick.md) | Reactively pick fields from a reactive object | AUTO | +| [`refAutoReset`](references/refAutoReset.md) | A ref which will be reset to the default value after some time | AUTO | +| [`refDebounced`](references/refDebounced.md) | Debounce execution of a ref value | AUTO | +| [`refDefault`](references/refDefault.md) | Apply default value to a ref | AUTO | +| [`refManualReset`](references/refManualReset.md) | Create a ref with manual reset functionality | AUTO | +| [`refThrottled`](references/refThrottled.md) | Throttle changing of a ref value | AUTO | +| [`refWithControl`](references/refWithControl.md) | Fine-grained controls over ref and its reactivity | AUTO | +| [`syncRef`](references/syncRef.md) | Two-way refs synchronization | AUTO | +| [`syncRefs`](references/syncRefs.md) | Keep target refs in sync with a source ref | AUTO | +| [`toReactive`](references/toReactive.md) | Converts ref to reactive | AUTO | +| [`toRef`](references/toRef.md) | Normalize value/ref/getter to `ref` or `computed` | EXPLICIT_ONLY | +| [`toRefs`](references/toRefs.md) | Extended [`toRefs`](https://vuejs.org/api/reactivity-utilities.html#torefs) that also accepts refs of an object | AUTO | + +### Array + +| Function | Description | Invocation | +|----------|-------------|------------| +| [`useArrayDifference`](references/useArrayDifference.md) | Reactive get array difference of two arrays | AUTO | +| [`useArrayEvery`](references/useArrayEvery.md) | Reactive `Array.every` | AUTO | +| [`useArrayFilter`](references/useArrayFilter.md) | Reactive `Array.filter` | AUTO | +| [`useArrayFind`](references/useArrayFind.md) | Reactive `Array.find` | AUTO | +| [`useArrayFindIndex`](references/useArrayFindIndex.md) | Reactive `Array.findIndex` | AUTO | +| [`useArrayFindLast`](references/useArrayFindLast.md) | Reactive `Array.findLast` | AUTO | +| [`useArrayIncludes`](references/useArrayIncludes.md) | Reactive `Array.includes` | AUTO | +| [`useArrayJoin`](references/useArrayJoin.md) | Reactive `Array.join` | AUTO | +| [`useArrayMap`](references/useArrayMap.md) | Reactive `Array.map` | AUTO | +| [`useArrayReduce`](references/useArrayReduce.md) | Reactive `Array.reduce` | AUTO | +| [`useArraySome`](references/useArraySome.md) | Reactive `Array.some` | AUTO | +| [`useArrayUnique`](references/useArrayUnique.md) | Reactive unique array | AUTO | +| [`useSorted`](references/useSorted.md) | Reactive sort array | AUTO | + +### Time + +| Function | Description | Invocation | +|----------|-------------|------------| +| [`useCountdown`](references/useCountdown.md) | Reactive countdown timer in seconds | AUTO | +| [`useDateFormat`](references/useDateFormat.md) | Get the formatted date according to the string of tokens passed in | AUTO | +| [`useTimeAgo`](references/useTimeAgo.md) | Reactive time ago | AUTO | +| [`useTimeAgoIntl`](references/useTimeAgoIntl.md) | Reactive time ago with i18n supported | AUTO | + +### Utilities + +| Function | Description | Invocation | +|----------|-------------|------------| +| [`createEventHook`](references/createEventHook.md) | Utility for creating event hooks | AUTO | +| [`createUnrefFn`](references/createUnrefFn.md) | Make a plain function accepting ref and raw values as arguments | AUTO | +| [`get`](references/get.md) | Shorthand for accessing `ref.value` | EXPLICIT_ONLY | +| [`isDefined`](references/isDefined.md) | Non-nullish checking type guard for Ref | AUTO | +| [`makeDestructurable`](references/makeDestructurable.md) | Make isomorphic destructurable for object and array at the same time | AUTO | +| [`set`](references/set.md) | Shorthand for `ref.value = x` | EXPLICIT_ONLY | +| [`useAsyncQueue`](references/useAsyncQueue.md) | Executes each asynchronous task sequentially and passes the current task result to the next task | AUTO | +| [`useBase64`](references/useBase64.md) | Reactive base64 transforming | AUTO | +| [`useCached`](references/useCached.md) | Cache a ref with a custom comparator | AUTO | +| [`useCloned`](references/useCloned.md) | Reactive clone of a ref | AUTO | +| [`useConfirmDialog`](references/useConfirmDialog.md) | Creates event hooks to support modals and confirmation dialog chains | AUTO | +| [`useCounter`](references/useCounter.md) | Basic counter with utility functions | AUTO | +| [`useCycleList`](references/useCycleList.md) | Cycle through a list of items | AUTO | +| [`useDebounceFn`](references/useDebounceFn.md) | Debounce execution of a function | AUTO | +| [`useEventBus`](references/useEventBus.md) | A basic event bus | AUTO | +| [`useMemoize`](references/useMemoize.md) | Cache results of functions depending on arguments and keep it reactive | AUTO | +| [`useOffsetPagination`](references/useOffsetPagination.md) | Reactive offset pagination | AUTO | +| [`usePrevious`](references/usePrevious.md) | Holds the previous value of a ref | AUTO | +| [`useStepper`](references/useStepper.md) | Provides helpers for building a multi-step wizard interface | AUTO | +| [`useSupported`](references/useSupported.md) | SSR compatibility `isSupported` | AUTO | +| [`useThrottleFn`](references/useThrottleFn.md) | Throttle execution of a function | AUTO | +| [`useTimeoutPoll`](references/useTimeoutPoll.md) | Use timeout to poll something | AUTO | +| [`useToggle`](references/useToggle.md) | A boolean switcher with utility functions | AUTO | +| [`useToNumber`](references/useToNumber.md) | Reactively convert a string ref to number | AUTO | +| [`useToString`](references/useToString.md) | Reactively convert a ref to string | AUTO | + +### @Electron + +| Function | Description | Invocation | +|----------|-------------|------------| +| [`useIpcRenderer`](references/useIpcRenderer.md) | Provides [ipcRenderer](https://www.electronjs.org/docs/api/ipc-renderer) and all of its APIs with Vue reactivity | EXTERNAL | +| [`useIpcRendererInvoke`](references/useIpcRendererInvoke.md) | Reactive [ipcRenderer.invoke API](https://www.electronjs.org/docs/api/ipc-renderer#ipcrendererinvokechannel-args) result | EXTERNAL | +| [`useIpcRendererOn`](references/useIpcRendererOn.md) | Use [ipcRenderer.on](https://www.electronjs.org/docs/api/ipc-renderer#ipcrendereronchannel-listener) with ease and [ipcRenderer.removeListener](https://www.electronjs.org/docs/api/ipc-renderer#ipcrendererremovelistenerchannel-listener) automatically on unmounted | EXTERNAL | +| [`useZoomFactor`](references/useZoomFactor.md) | Reactive [WebFrame](https://www.electronjs.org/docs/api/web-frame#webframe) zoom factor | EXTERNAL | +| [`useZoomLevel`](references/useZoomLevel.md) | Reactive [WebFrame](https://www.electronjs.org/docs/api/web-frame#webframe) zoom level | EXTERNAL | + +### @Firebase + +| Function | Description | Invocation | +|----------|-------------|------------| +| [`useAuth`](references/useAuth.md) | Reactive [Firebase Auth](https://firebase.google.com/docs/auth) binding | EXTERNAL | +| [`useFirestore`](references/useFirestore.md) | Reactive [Firestore](https://firebase.google.com/docs/firestore) binding | EXTERNAL | +| [`useRTDB`](references/useRTDB.md) | Reactive [Firebase Realtime Database](https://firebase.google.com/docs/database) binding | EXTERNAL | + +### @Head + +| Function | Description | Invocation | +|----------|-------------|------------| +| [`createHead`](https://github.com/vueuse/head#api) | Create the head manager instance. | EXTERNAL | +| [`useHead`](https://github.com/vueuse/head#api) | Update head meta tags reactively. | EXTERNAL | + +### @Integrations + +| Function | Description | Invocation | +|----------|-------------|------------| +| [`useAsyncValidator`](references/useAsyncValidator.md) | Wrapper for [`async-validator`](https://github.com/yiminghe/async-validator) | EXTERNAL | +| [`useAxios`](references/useAxios.md) | Wrapper for [`axios`](https://github.com/axios/axios) | EXTERNAL | +| [`useChangeCase`](references/useChangeCase.md) | Reactive wrapper for [`change-case`](https://github.com/blakeembrey/change-case) | EXTERNAL | +| [`useCookies`](references/useCookies.md) | Wrapper for [`universal-cookie`](https://www.npmjs.com/package/universal-cookie) | EXTERNAL | +| [`useDrauu`](references/useDrauu.md) | Reactive instance for [drauu](https://github.com/antfu/drauu) | EXTERNAL | +| [`useFocusTrap`](references/useFocusTrap.md) | Reactive wrapper for [`focus-trap`](https://github.com/focus-trap/focus-trap) | EXTERNAL | +| [`useFuse`](references/useFuse.md) | Easily implement fuzzy search using a composable with [Fuse.js](https://github.com/krisk/fuse) | EXTERNAL | +| [`useIDBKeyval`](references/useIDBKeyval.md) | Wrapper for [`idb-keyval`](https://www.npmjs.com/package/idb-keyval) | EXTERNAL | +| [`useJwt`](references/useJwt.md) | Wrapper for [`jwt-decode`](https://github.com/auth0/jwt-decode) | EXTERNAL | +| [`useNProgress`](references/useNProgress.md) | Reactive wrapper for [`nprogress`](https://github.com/rstacruz/nprogress) | EXTERNAL | +| [`useQRCode`](references/useQRCode.md) | Wrapper for [`qrcode`](https://github.com/soldair/node-qrcode) | EXTERNAL | +| [`useSortable`](references/useSortable.md) | Wrapper for [`sortable`](https://github.com/SortableJS/Sortable) | EXTERNAL | + +### @Math + +| Function | Description | Invocation | +|----------|-------------|------------| +| [`createGenericProjection`](references/createGenericProjection.md) | Generic version of `createProjection` | EXTERNAL | +| [`createProjection`](references/createProjection.md) | Reactive numeric projection from one domain to another | EXTERNAL | +| [`logicAnd`](references/logicAnd.md) | `AND` condition for refs | EXTERNAL | +| [`logicNot`](references/logicNot.md) | `NOT` condition for ref | EXTERNAL | +| [`logicOr`](references/logicOr.md) | `OR` conditions for refs | EXTERNAL | +| [`useAbs`](references/useAbs.md) | Reactive `Math.abs` | EXTERNAL | +| [`useAverage`](references/useAverage.md) | Get the average of an array reactively | EXTERNAL | +| [`useCeil`](references/useCeil.md) | Reactive `Math.ceil` | EXTERNAL | +| [`useClamp`](references/useClamp.md) | Reactively clamp a value between two other values | EXTERNAL | +| [`useFloor`](references/useFloor.md) | Reactive `Math.floor` | EXTERNAL | +| [`useMath`](references/useMath.md) | Reactive `Math` methods | EXTERNAL | +| [`useMax`](references/useMax.md) | Reactive `Math.max` | EXTERNAL | +| [`useMin`](references/useMin.md) | Reactive `Math.min` | EXTERNAL | +| [`usePrecision`](references/usePrecision.md) | Reactively set the precision of a number | EXTERNAL | +| [`useProjection`](references/useProjection.md) | Reactive numeric projection from one domain to another | EXTERNAL | +| [`useRound`](references/useRound.md) | Reactive `Math.round` | EXTERNAL | +| [`useSum`](references/useSum.md) | Get the sum of an array reactively | EXTERNAL | +| [`useTrunc`](references/useTrunc.md) | Reactive `Math.trunc` | EXTERNAL | + +### @Motion + +| Function | Description | Invocation | +|----------|-------------|------------| +| [`useElementStyle`](https://motion.vueuse.org/api/use-element-style) | Sync a reactive object to a target element CSS styling | EXTERNAL | +| [`useElementTransform`](https://motion.vueuse.org/api/use-element-transform) | Sync a reactive object to a target element CSS transform. | EXTERNAL | +| [`useMotion`](https://motion.vueuse.org/api/use-motion) | Putting your components in motion. | EXTERNAL | +| [`useMotionProperties`](https://motion.vueuse.org/api/use-motion-properties) | Access Motion Properties for a target element. | EXTERNAL | +| [`useMotionVariants`](https://motion.vueuse.org/api/use-motion-variants) | Handle the Variants state and selection. | EXTERNAL | +| [`useSpring`](https://motion.vueuse.org/api/use-spring) | Spring animations. | EXTERNAL | + +### @Router + +| Function | Description | Invocation | +|----------|-------------|------------| +| [`useRouteHash`](references/useRouteHash.md) | Shorthand for a reactive `route.hash` | EXTERNAL | +| [`useRouteParams`](references/useRouteParams.md) | Shorthand for a reactive `route.params` | EXTERNAL | +| [`useRouteQuery`](references/useRouteQuery.md) | Shorthand for a reactive `route.query` | EXTERNAL | + +### @RxJS + +| Function | Description | Invocation | +|----------|-------------|------------| +| [`from`](references/from.md) | Wrappers around RxJS's [`from()`](https://rxjs.dev/api/index/function/from) and [`fromEvent()`](https://rxjs.dev/api/index/function/fromEvent) to allow them to accept `ref`s | EXTERNAL | +| [`toObserver`](references/toObserver.md) | Sugar function to convert a `ref` into an RxJS [Observer](https://rxjs.dev/guide/observer) | EXTERNAL | +| [`useExtractedObservable`](references/useExtractedObservable.md) | Use an RxJS [`Observable`](https://rxjs.dev/guide/observable) as extracted from one or more composables | EXTERNAL | +| [`useObservable`](references/useObservable.md) | Use an RxJS [`Observable`](https://rxjs.dev/guide/observable) | EXTERNAL | +| [`useSubject`](references/useSubject.md) | Bind an RxJS [`Subject`](https://rxjs.dev/guide/subject) to a `ref` and propagate value changes both ways | EXTERNAL | +| [`useSubscription`](references/useSubscription.md) | Use an RxJS [`Subscription`](https://rxjs.dev/guide/subscription) without worrying about unsubscribing from it or creating memory leaks | EXTERNAL | +| [`watchExtractedObservable`](references/watchExtractedObservable.md) | Watch the values of an RxJS [`Observable`](https://rxjs.dev/guide/observable) as extracted from one or more composables | EXTERNAL | + +### @SchemaOrg + +| Function | Description | Invocation | +|----------|-------------|------------| +| [`createSchemaOrg`](https://vue-schema-org.netlify.app/api/core/create-schema-org.html) | Create the schema.org manager instance. | EXTERNAL | +| [`useSchemaOrg`](https://vue-schema-org.netlify.app/api/core/use-schema-org.html) | Update schema.org reactively. | EXTERNAL | + +### @Sound + +| Function | Description | Invocation | +|----------|-------------|------------| +| [`useSound`](https://github.com/vueuse/sound#examples) | Play sound effects reactively. | EXTERNAL | + + diff --git a/.agents/skills/vueuse-functions/SYNC.md b/.agents/skills/vueuse-functions/SYNC.md new file mode 100644 index 0000000..b818d1b --- /dev/null +++ b/.agents/skills/vueuse-functions/SYNC.md @@ -0,0 +1,5 @@ +# Sync Info + +- **Source:** `vendor/vueuse/skills/vueuse-functions` +- **Git SHA:** `075b0d6d558cc5ca7d5ffe72a56b5fd92bbef2d1` +- **Synced:** 2026-03-16 diff --git a/.agents/skills/vueuse-functions/references/computedAsync.md b/.agents/skills/vueuse-functions/references/computedAsync.md new file mode 100644 index 0000000..34059d0 --- /dev/null +++ b/.agents/skills/vueuse-functions/references/computedAsync.md @@ -0,0 +1,195 @@ +--- +category: Reactivity +alias: asyncComputed +--- + +# computedAsync + +Computed for async functions. + +## Usage + +```ts +import { computedAsync } from '@vueuse/core' +import { shallowRef } from 'vue' + +const name = shallowRef('jack') + +const userInfo = computedAsync( + async () => { + return await mockLookUp(name.value) + }, + null, // initial state +) +``` + +### Evaluation State + +Pass a ref to track if the async function is currently evaluating. + +```ts +import { computedAsync } from '@vueuse/core' +import { shallowRef } from 'vue' + +const evaluating = shallowRef(false) + +const userInfo = computedAsync( + async () => { /* your logic */ }, + null, + evaluating, // can also be passed via options: { evaluating } +) +``` + +### onCancel + +When the computed source changes before the previous async function resolves, you may want to cancel the previous one. Here is an example showing how to incorporate with the fetch API. + +```ts +import { computedAsync } from '@vueuse/core' +import { shallowRef } from 'vue' + +const packageName = shallowRef('@vueuse/core') + +const downloads = computedAsync(async (onCancel) => { + const abortController = new AbortController() + + onCancel(() => abortController.abort()) + + return await fetch( + `https://api.npmjs.org/downloads/point/last-week/${packageName.value}`, + { signal: abortController.signal }, + ) + .then(response => response.ok ? response.json() : { downloads: '—' }) + .then(result => result.downloads) +}, 0) +``` + +### Lazy + +By default, `computedAsync` will start resolving immediately on creation. Specify `lazy: true` to make it start resolving on the first access. + +```ts +import { computedAsync } from '@vueuse/core' +import { shallowRef } from 'vue' + +const evaluating = shallowRef(false) + +const userInfo = computedAsync( + async () => { /* your logic */ }, + null, + { lazy: true, evaluating }, +) +``` + +### Error Handling + +Use the `onError` callback to handle errors from the async function. + +```ts +import { computedAsync } from '@vueuse/core' +import { shallowRef } from 'vue' + +const name = shallowRef('jack') + +const userInfo = computedAsync( + async () => { + return await mockLookUp(name.value) + }, + null, + { + onError(e) { + console.error('Failed to fetch user info', e) + }, + }, +) +``` + +### Shallow Ref + +By default, `computedAsync` uses `shallowRef` internally. Set `shallow: false` to use a deep ref instead. + +```ts +import { computedAsync } from '@vueuse/core' +import { shallowRef } from 'vue' + +const name = shallowRef('jack') + +const userInfo = computedAsync( + async () => { + return await fetchNestedData(name.value) + }, + null, + { shallow: false }, // enables deep reactivity +) +``` + +## Caveats + +- Just like Vue's built-in `computed` function, `computedAsync` does dependency tracking and is automatically re-evaluated when dependencies change. Note however that only dependencies referenced in the first call stack are considered for this. In other words: **Dependencies that are accessed asynchronously will not trigger re-evaluation of the async computed value.** + +- As opposed to Vue's built-in `computed` function, re-evaluation of the async computed value is triggered whenever dependencies are changing, regardless of whether its result is currently being tracked or not. + +## Type Declarations + +```ts +/** + * Handle overlapping async evaluations. + * + * @param cancelCallback The provided callback is invoked when a re-evaluation of the computed value is triggered before the previous one finished + */ +export type AsyncComputedOnCancel = (cancelCallback: Fn) => void +export interface AsyncComputedOptions< + Lazy = boolean, +> extends ConfigurableFlushSync { + /** + * Should value be evaluated lazily + * + * @default false + */ + lazy?: Lazy + /** + * Ref passed to receive the updated of async evaluation + */ + evaluating?: Ref + /** + * Use shallowRef + * + * @default true + */ + shallow?: boolean + /** + * Callback when error is caught. + */ + onError?: (e: unknown) => void +} +/** + * Create an asynchronous computed dependency. + * + * @see https://vueuse.org/computedAsync + * @param evaluationCallback The promise-returning callback which generates the computed value + * @param initialState The initial state, used until the first evaluation finishes + * @param optionsOrRef Additional options or a ref passed to receive the updates of the async evaluation + */ +export declare function computedAsync( + evaluationCallback: (onCancel: AsyncComputedOnCancel) => T | Promise, + initialState: T, + optionsOrRef: AsyncComputedOptions, +): ComputedRef +export declare function computedAsync( + evaluationCallback: (onCancel: AsyncComputedOnCancel) => T | Promise, + initialState: undefined, + optionsOrRef: AsyncComputedOptions, +): ComputedRef +export declare function computedAsync( + evaluationCallback: (onCancel: AsyncComputedOnCancel) => T | Promise, + initialState: T, + optionsOrRef?: Ref | AsyncComputedOptions, +): Ref +export declare function computedAsync( + evaluationCallback: (onCancel: AsyncComputedOnCancel) => T | Promise, + initialState?: undefined, + optionsOrRef?: Ref | AsyncComputedOptions, +): Ref +/** @deprecated use `computedAsync` instead */ +export declare const asyncComputed: typeof computedAsync +``` diff --git a/.agents/skills/vueuse-functions/references/computedEager.md b/.agents/skills/vueuse-functions/references/computedEager.md new file mode 100644 index 0000000..225dc9a --- /dev/null +++ b/.agents/skills/vueuse-functions/references/computedEager.md @@ -0,0 +1,62 @@ +--- +category: Reactivity +alias: eagerComputed +--- + +# computedEager + +Eager computed without lazy evaluation. + +::: info +This function will be removed in future version. +::: + +::: tip +Note💡: If you are using Vue 3.4+, you can use `computed` right away, you no longer need this function. +In Vue 3.4+, if the computed new value does not change, `computed`, `effect`, `watch`, `watchEffect`, `render` dependencies will not be triggered. +See: https://github.com/vuejs/core/pull/5912 +::: + +Learn more at [Vue: When a computed property can be the wrong tool](https://dev.to/linusborg/vue-when-a-computed-property-can-be-the-wrong-tool-195j). + +- Use `computed()` when you have a complex calculation going on, which can actually profit from caching and lazy evaluation and should only be (re-)calculated if really necessary. +- Use `computedEager()` when you have a simple operation, with a rarely changing return value – often a boolean. + +## Usage + +```ts +import { computedEager } from '@vueuse/core' + +const todos = ref([]) +const hasOpenTodos = computedEager(() => !!todos.length) + +console.log(hasOpenTodos.value) // false +toTodos.value.push({ title: 'Learn Vue' }) +console.log(hasOpenTodos.value) // true +``` + +## Type Declarations + +```ts +export type ComputedEagerOptions = WatchOptionsBase +export type ComputedEagerReturn = Readonly> +/** + * + * @deprecated This function will be removed in future version. + * + * Note: If you are using Vue 3.4+, you can straight use computed instead. + * Because in Vue 3.4+, if computed new value does not change, + * computed, effect, watch, watchEffect, render dependencies will not be triggered. + * refer: https://github.com/vuejs/core/pull/5912 + * + * @param fn effect function + * @param options WatchOptionsBase + * @returns readonly shallowRef + */ +export declare function computedEager( + fn: () => T, + options?: ComputedEagerOptions, +): ComputedEagerReturn +/** @deprecated use `computedEager` instead */ +export declare const eagerComputed: typeof computedEager +``` diff --git a/.agents/skills/vueuse-functions/references/computedInject.md b/.agents/skills/vueuse-functions/references/computedInject.md new file mode 100644 index 0000000..5e01016 --- /dev/null +++ b/.agents/skills/vueuse-functions/references/computedInject.md @@ -0,0 +1,137 @@ +--- +category: Component +--- + +# computedInject + +Combine `computed` and `inject`. Useful for creating a computed property based on an injected value. + +## Usage + +In Provider Component + +```ts twoslash include main +import type { InjectionKey, Ref } from 'vue' +import { provide, ref } from 'vue' + +interface Item { + key: number + value: string +} + +export const ArrayKey: InjectionKey> = Symbol('symbol-key') + +const array = ref([{ key: 1, value: '1' }, { key: 2, value: '2' }, { key: 3, value: '3' }]) + +provide(ArrayKey, array) +``` + +In Receiver Component + +```ts +// @filename: provider.ts +// @include: main +// ---cut--- +import { computedInject } from '@vueuse/core' + +import { ArrayKey } from './provider' + +const computedArray = computedInject(ArrayKey, (source) => { + const arr = [...source.value] + arr.unshift({ key: 0, value: 'all' }) + return arr +}) +``` + +### Default Value + +You can provide a default value that will be used if the injection key is not provided by a parent component. + +```ts +import { computedInject } from '@vueuse/core' + +const computedArray = computedInject( + ArrayKey, + (source) => { + return source.value.map(item => item.value) + }, + ref([]), // default source value +) +``` + +### Factory Default + +Pass `true` as the fourth argument to treat the default value as a factory function. + +```ts +import { computedInject } from '@vueuse/core' + +const computedArray = computedInject( + ArrayKey, + (source) => { + return source.value.map(item => item.value) + }, + () => ref([]), // factory function for default + true, // treat default as factory +) +``` + +### Writable Computed + +You can also create a writable computed property by passing an object with `get` and `set` functions. + +```ts +import { computedInject } from '@vueuse/core' + +const computedArray = computedInject(ArrayKey, { + get(source) { + return source.value.map(item => item.value) + }, + set(value) { + // handle setting the value + console.log('Setting value:', value) + }, +}) +``` + +## Type Declarations + +```ts +export type ComputedInjectGetter = ( + source: T | undefined, + oldValue?: K, +) => K +export type ComputedInjectGetterWithDefault = ( + source: T, + oldValue?: K, +) => K +export type ComputedInjectSetter = (v: T) => void +export interface WritableComputedInjectOptions { + get: ComputedInjectGetter + set: ComputedInjectSetter +} +export interface WritableComputedInjectOptionsWithDefault { + get: ComputedInjectGetterWithDefault + set: ComputedInjectSetter +} +export declare function computedInject( + key: InjectionKey | string, + getter: ComputedInjectGetter, +): ComputedRef +export declare function computedInject( + key: InjectionKey | string, + options: WritableComputedInjectOptions, +): ComputedRef +export declare function computedInject( + key: InjectionKey | string, + getter: ComputedInjectGetterWithDefault, + defaultSource: T, + treatDefaultAsFactory?: false, +): ComputedRef +export declare function computedInject( + key: InjectionKey | string, + options: WritableComputedInjectOptionsWithDefault, + defaultSource: T | (() => T), + treatDefaultAsFactory: true, +): ComputedRef +``` diff --git a/.agents/skills/vueuse-functions/references/computedWithControl.md b/.agents/skills/vueuse-functions/references/computedWithControl.md new file mode 100644 index 0000000..59e9704 --- /dev/null +++ b/.agents/skills/vueuse-functions/references/computedWithControl.md @@ -0,0 +1,98 @@ +--- +category: Reactivity +alias: controlledComputed +--- + +# computedWithControl + +Explicitly define the dependencies of computed. + +## Usage + +```ts twoslash include main +import { computedWithControl } from '@vueuse/core' + +const source = ref('foo') +const counter = ref(0) + +const computedRef = computedWithControl( + () => source.value, // watch source, same as `watch` + () => counter.value, // computed getter, same as `computed` +) +``` + +With this, the changes of `counter` won't trigger `computedRef` to update but the `source` ref does. + +```ts +// @include: main +// ---cut--- +console.log(computedRef.value) // 0 + +counter.value += 1 + +console.log(computedRef.value) // 0 + +source.value = 'bar' + +console.log(computedRef.value) // 1 +``` + +### Manual Triggering + +You can also manually trigger the update of the computed by: + +```ts +// @include: main +// ---cut--- +const computedRef = computedWithControl( + () => source.value, + () => counter.value, +) + +computedRef.trigger() +``` + +### Deep Watch + +Unlike `computed`, `computedWithControl` is shallow by default. +You can specify the same options as `watch` to control the behavior: + +```ts +const source = ref({ name: 'foo' }) + +const computedRef = computedWithControl( + source, + () => counter.value, + { deep: true }, +) +``` + +## Type Declarations + +```ts +export interface ComputedWithControlRefExtra { + /** + * Force update the computed value. + */ + trigger: () => void +} +export interface ComputedRefWithControl + extends ComputedRef, ComputedWithControlRefExtra {} +export interface WritableComputedRefWithControl + extends WritableComputedRef, ComputedWithControlRefExtra {} +export type ComputedWithControlRef = + | ComputedRefWithControl + | WritableComputedRefWithControl +export declare function computedWithControl( + source: WatchSource | MultiWatchSources, + fn: ComputedGetter, + options?: WatchOptions, +): ComputedRefWithControl +export declare function computedWithControl( + source: WatchSource | MultiWatchSources, + fn: WritableComputedOptions, + options?: WatchOptions, +): WritableComputedRefWithControl +/** @deprecated use `computedWithControl` instead */ +export declare const controlledComputed: typeof computedWithControl +``` diff --git a/.agents/skills/vueuse-functions/references/createEventHook.md b/.agents/skills/vueuse-functions/references/createEventHook.md new file mode 100644 index 0000000..a01f218 --- /dev/null +++ b/.agents/skills/vueuse-functions/references/createEventHook.md @@ -0,0 +1,86 @@ +--- +category: Utilities +--- + +# createEventHook + +Utility for creating event hooks + +## Usage + +Creating a function that uses `createEventHook` + +```ts +import { createEventHook } from '@vueuse/core' + +export function useMyFetch(url) { + const fetchResult = createEventHook() + const fetchError = createEventHook() + + fetch(url) + .then(result => fetchResult.trigger(result)) + .catch(error => fetchError.trigger(error.message)) + + return { + onResult: fetchResult.on, + onError: fetchError.on, + } +} +``` + +Using a function that uses `createEventHook` + +```vue + +``` + +## Type Declarations + +```ts +/** + * The source code for this function was inspired by vue-apollo's `useEventHook` util + * https://github.com/vuejs/vue-apollo/blob/v4/packages/vue-apollo-composable/src/util/useEventHook.ts + */ +type Callback = + IsAny extends true + ? (...param: any) => void + : [T] extends [void] + ? (...param: unknown[]) => void + : [T] extends [any[]] + ? (...param: T) => void + : (...param: [T, ...unknown[]]) => void +export type EventHookOn = (fn: Callback) => { + off: () => void +} +export type EventHookOff = (fn: Callback) => void +export type EventHookTrigger = ( + ...param: Parameters> +) => Promise +export interface EventHook { + on: EventHookOn + off: EventHookOff + trigger: EventHookTrigger + clear: () => void +} +export type EventHookReturn = EventHook +/** + * Utility for creating event hooks + * + * @see https://vueuse.org/createEventHook + * + * @__NO_SIDE_EFFECTS__ + */ +export declare function createEventHook(): EventHookReturn +``` diff --git a/.agents/skills/vueuse-functions/references/createGenericProjection.md b/.agents/skills/vueuse-functions/references/createGenericProjection.md new file mode 100644 index 0000000..c452031 --- /dev/null +++ b/.agents/skills/vueuse-functions/references/createGenericProjection.md @@ -0,0 +1,25 @@ +--- +category: '@Math' +--- + +# createGenericProjection + +Generic version of `createProjection`. Accepts a custom projector function to map arbitrary type of domains. + +Refer to `createProjection` and `useProjection` + +## Type Declarations + +```ts +export type ProjectorFunction = ( + input: F, + from: readonly [F, F], + to: readonly [T, T], +) => T +export type UseProjection = (input: MaybeRefOrGetter) => ComputedRef +export declare function createGenericProjection( + fromDomain: MaybeRefOrGetter, + toDomain: MaybeRefOrGetter, + projector: ProjectorFunction, +): UseProjection +``` diff --git a/.agents/skills/vueuse-functions/references/createGlobalState.md b/.agents/skills/vueuse-functions/references/createGlobalState.md new file mode 100644 index 0000000..d0909fc --- /dev/null +++ b/.agents/skills/vueuse-functions/references/createGlobalState.md @@ -0,0 +1,95 @@ +--- +category: State +related: createSharedComposable +--- + +# createGlobalState + +Keep states in the global scope to be reusable across Vue instances. + +## Usage + +### Without Persistence (Store in Memory) + +```ts +// store.ts +import { createGlobalState } from '@vueuse/core' +import { shallowRef } from 'vue' + +export const useGlobalState = createGlobalState( + () => { + const count = shallowRef(0) + return { count } + } +) +``` + +A bigger example: + +```ts +// store.ts +import { createGlobalState } from '@vueuse/core' +import { computed, shallowRef } from 'vue' + +export const useGlobalState = createGlobalState( + () => { + // state + const count = shallowRef(0) + + // getters + const doubleCount = computed(() => count.value * 2) + + // actions + function increment() { + count.value++ + } + + return { count, doubleCount, increment } + } +) +``` + +### With Persistence + +Store in `localStorage` with `useStorage`: + +```ts twoslash include store +// store.ts +import { createGlobalState, useStorage } from '@vueuse/core' + +export const useGlobalState = createGlobalState( + () => useStorage('vueuse-local-storage', 'initialValue'), +) +``` + +```ts +// @filename: store.ts +// @include: store +// ---cut--- +// component.ts +import { useGlobalState } from './store' + +export default defineComponent({ + setup() { + const state = useGlobalState() + return { state } + }, +}) +``` + +## Type Declarations + +```ts +export type CreateGlobalStateReturn = Fn +/** + * Keep states in the global scope to be reusable across Vue instances. + * + * @see https://vueuse.org/createGlobalState + * @param stateFactory A factory function to create the state + * + * @__NO_SIDE_EFFECTS__ + */ +export declare function createGlobalState( + stateFactory: Fn, +): CreateGlobalStateReturn +``` diff --git a/.agents/skills/vueuse-functions/references/createInjectionState.md b/.agents/skills/vueuse-functions/references/createInjectionState.md new file mode 100644 index 0000000..5aa7331 --- /dev/null +++ b/.agents/skills/vueuse-functions/references/createInjectionState.md @@ -0,0 +1,215 @@ +--- +category: State +--- + +# createInjectionState + +Create global state that can be injected into components. + +## Usage + +```ts twoslash include useCounterStore +// useCounterStore.ts +import { createInjectionState } from '@vueuse/core' +import { computed, shallowRef } from 'vue' + +const [useProvideCounterStore, useCounterStore] = createInjectionState((initialValue: number) => { + // state + const count = shallowRef(initialValue) + + // getters + const double = computed(() => count.value * 2) + + // actions + function increment() { + count.value++ + } + + return { count, double, increment } +}) + +export { useProvideCounterStore } + +// If you want to hide `useCounterStore` and wrap it in default value logic or throw error logic, please don't export `useCounterStore` +export { useCounterStore } + +export function useCounterStoreWithDefaultValue() { + return useCounterStore() ?? { + count: shallowRef(0), + double: shallowRef(0), + increment: () => {}, + } +} + +export function useCounterStoreOrThrow() { + const counterStore = useCounterStore() + if (counterStore == null) + throw new Error('Please call `useProvideCounterStore` on the appropriate parent component') + return counterStore +} +``` + +```vue + + + + +``` + +```vue + + + + +``` + +```vue + + + + +``` + +## Provide a custom InjectionKey + +```ts +// useCounterStore.ts +import { createInjectionState } from '@vueuse/core' +import { computed, shallowRef } from 'vue' + +// custom injectionKey +const CounterStoreKey = 'counter-store' + +const [useProvideCounterStore, useCounterStore] = createInjectionState((initialValue: number) => { + // state + const count = shallowRef(initialValue) + + // getters + const double = computed(() => count.value * 2) + + // actions + function increment() { + count.value++ + } + + return { count, double, increment } +}, { injectionKey: CounterStoreKey }) +``` + +## Provide a custom default value + +```ts +// useCounterStore.ts +import { createInjectionState } from '@vueuse/core' +import { computed, shallowRef } from 'vue' + +const [useProvideCounterStore, useCounterStore] = createInjectionState((initialValue: number) => { + // state + const count = shallowRef(initialValue) + + // getters + const double = computed(() => count.value * 2) + + // actions + function increment() { + count.value++ + } + + return { count, double, increment } +}, { defaultValue: 0 }) +``` + +## Type Declarations + +```ts +export type CreateInjectionStateReturn< + Arguments extends Array, + Return, +> = Readonly< + [ + /** + * Call this function in a provider component to create and provide the state. + * + * @param args Arguments passed to the composable + * @returns The state returned by the composable + */ + useProvidingState: (...args: Arguments) => Return, + /** + * Call this function in a consumer component to inject the state. + * + * @returns The injected state, or `undefined` if not provided and no default value was set. + */ + useInjectedState: () => Return | undefined, + ] +> +export interface CreateInjectionStateOptions { + /** + * Custom injectionKey for InjectionState + */ + injectionKey?: string | InjectionKey + /** + * Default value for the InjectionState + */ + defaultValue?: Return +} +/** + * Create global state that can be injected into components. + * + * @see https://vueuse.org/createInjectionState + * + * @__NO_SIDE_EFFECTS__ + */ +export declare function createInjectionState< + Arguments extends Array, + Return, +>( + composable: (...args: Arguments) => Return, + options?: CreateInjectionStateOptions, +): CreateInjectionStateReturn +``` diff --git a/.agents/skills/vueuse-functions/references/createProjection.md b/.agents/skills/vueuse-functions/references/createProjection.md new file mode 100644 index 0000000..11bfcee --- /dev/null +++ b/.agents/skills/vueuse-functions/references/createProjection.md @@ -0,0 +1,31 @@ +--- +category: '@Math' +related: useProjection, createGenericProjection +--- + +# createProjection + +Reactive numeric projection from one domain to another. + +## Usage + +```ts +import { createProjection } from '@vueuse/math' + +const useProjector = createProjection([0, 10], [0, 100]) +const input = ref(0) +const projected = useProjector(input) // projected.value === 0 + +input.value = 5 // projected.value === 50 +input.value = 10 // projected.value === 100 +``` + +## Type Declarations + +```ts +export declare function createProjection( + fromDomain: MaybeRefOrGetter, + toDomain: MaybeRefOrGetter, + projector?: ProjectorFunction, +): UseProjection +``` diff --git a/.agents/skills/vueuse-functions/references/createRef.md b/.agents/skills/vueuse-functions/references/createRef.md new file mode 100644 index 0000000..07f0cac --- /dev/null +++ b/.agents/skills/vueuse-functions/references/createRef.md @@ -0,0 +1,54 @@ +--- +category: Reactivity +--- + +# createRef + +Returns a `deepRef` or `shallowRef` depending on the `deep` param. + +## Usage + +```ts +import { createRef } from '@vueuse/core' +import { isShallow, ref } from 'vue' + +const initialData = 1 + +const shallowData = createRef(initialData) +const deepData = createRef(initialData, true) + +isShallow(shallowData) // true +isShallow(deepData) // false +``` + +## Type Declarations + +```ts +export type CreateRefReturn< + T = any, + D extends boolean = false, +> = ShallowOrDeepRef +export type ShallowOrDeepRef< + T = any, + D extends boolean = false, +> = D extends true ? Ref : ShallowRef +/** + * Returns a `deepRef` or `shallowRef` depending on the `deep` param. + * + * @example createRef(1) // ShallowRef + * @example createRef(1, false) // ShallowRef + * @example createRef(1, true) // Ref + * @example createRef("string") // ShallowRef + * @example createRef<"A"|"B">("A", true) // Ref<"A"|"B"> + * + * @param value + * @param deep + * @returns the `deepRef` or `shallowRef` + * + * @__NO_SIDE_EFFECTS__ + */ +export declare function createRef( + value: T, + deep?: D, +): CreateRefReturn +``` diff --git a/.agents/skills/vueuse-functions/references/createReusableTemplate.md b/.agents/skills/vueuse-functions/references/createReusableTemplate.md new file mode 100644 index 0000000..f82fb51 --- /dev/null +++ b/.agents/skills/vueuse-functions/references/createReusableTemplate.md @@ -0,0 +1,357 @@ +--- +category: Component +outline: deep +--- + +# createReusableTemplate + +Define and reuse template inside the component scope. + +## Motivation + +It's common to have the need to reuse some part of the template. For example: + +```vue + +``` + +We'd like to reuse our code as much as possible. So normally we might need to extract those duplicated parts into a component. However, in a separated component you lose the ability to access the local bindings. Defining props and emits for them can be tedious sometimes. + +So this function is made to provide a way for defining and reusing templates inside the component scope. + +## Usage + +In the previous example, we could refactor it to: + +```vue + + + +``` + +- `` will register the template and renders nothing. +- `` will render the template provided by ``. +- `` must be used before ``. + +> **Note**: It's recommended to extract as separate components whenever possible. Abusing this function might lead to bad practices for your codebase. + +### Options API + +When using with [Options API](https://vuejs.org/guide/introduction.html#api-styles), you will need to define `createReusableTemplate` outside of the component setup and pass to the `components` option in order to use them in the template. + +```vue + + + +``` + +### Passing Data + +You can also pass data to the template using slots: + +- Use `v-slot="..."` to access the data on `` +- Directly bind the data on `` to pass them to the template + +```vue + + + +``` + +### TypeScript Support + +`createReusableTemplate` accepts a generic type to provide type support for the data passed to the template: + +```vue + + + +``` + +Optionally, if you are not a fan of array destructuring, the following usages are also legal: + +```vue + + + +``` + +```vue + + + +``` + +::: warning +Passing boolean props without `v-bind` is not supported. See the [Caveats](#boolean-props) section for more details. +::: + +### Props and Attributes + +By default, all props and attributes passed to `` will be passed to the template. If you don't want certain props to be passed to the DOM, you need to define the runtime props: + +```ts +import { createReusableTemplate } from '@vueuse/core' + +const [DefineTemplate, ReuseTemplate] = createReusableTemplate({ + props: { + msg: String, + enable: Boolean, + } +}) +``` + +If you don't want to pass any props to the template, you can pass the `inheritAttrs` option: + +```ts +import { createReusableTemplate } from '@vueuse/core' + +const [DefineTemplate, ReuseTemplate] = createReusableTemplate({ + inheritAttrs: false, +}) +``` + +### Passing Slots + +It's also possible to pass slots back from ``. You can access the slots on `` from `$slots`: + +```vue + + + +``` + +## Caveats + +### Boolean props + +As opposed to Vue's behavior, props defined as `boolean` that were passed without `v-bind` or absent will be resolved into an empty string or `undefined` respectively: + +```vue + + + +``` + +## References + +This function is migrated from [vue-reuse-template](https://github.com/antfu/vue-reuse-template). + +Existing Vue discussions/issues about reusing template: + +- [Discussion on Reusing Templates](https://github.com/vuejs/core/discussions/6898) + +Alternative Approaches: + +- [Vue Macros - `namedTemplate`](https://vue-macros.sxzz.moe/features/named-template.html) +- [`unplugin-vue-reuse-template`](https://github.com/liulinboyi/unplugin-vue-reuse-template) + +## Type Declarations + +```ts +type ObjectLiteralWithPotentialObjectLiterals = Record< + string, + Record | undefined +> +type GenerateSlotsFromSlotMap< + T extends ObjectLiteralWithPotentialObjectLiterals, +> = { + [K in keyof T]: Slot +} +export type DefineTemplateComponent< + Bindings extends Record, + MapSlotNameToSlotProps extends ObjectLiteralWithPotentialObjectLiterals, +> = DefineComponent & { + new (): { + $slots: { + default: ( + _: Bindings & { + $slots: GenerateSlotsFromSlotMap + }, + ) => any + } + } +} +export type ReuseTemplateComponent< + Bindings extends Record, + MapSlotNameToSlotProps extends ObjectLiteralWithPotentialObjectLiterals, +> = DefineComponent & { + new (): { + $slots: GenerateSlotsFromSlotMap + } +} +export type ReusableTemplatePair< + Bindings extends Record, + MapSlotNameToSlotProps extends ObjectLiteralWithPotentialObjectLiterals, +> = [ + DefineTemplateComponent, + ReuseTemplateComponent, +] & { + define: DefineTemplateComponent + reuse: ReuseTemplateComponent +} +export interface CreateReusableTemplateOptions< + Props extends Record, +> { + /** + * Inherit attrs from reuse component. + * + * @default true + */ + inheritAttrs?: boolean + /** + * Props definition for reuse component. + */ + props?: ComponentObjectPropsOptions +} +/** + * This function creates `define` and `reuse` components in pair, + * It also allow to pass a generic to bind with type. + * + * @see https://vueuse.org/createReusableTemplate + * + * @__NO_SIDE_EFFECTS__ + */ +export declare function createReusableTemplate< + Bindings extends Record, + MapSlotNameToSlotProps extends ObjectLiteralWithPotentialObjectLiterals = + Record<"default", undefined>, +>( + options?: CreateReusableTemplateOptions, +): ReusableTemplatePair +``` diff --git a/.agents/skills/vueuse-functions/references/createSharedComposable.md b/.agents/skills/vueuse-functions/references/createSharedComposable.md new file mode 100644 index 0000000..19e6308 --- /dev/null +++ b/.agents/skills/vueuse-functions/references/createSharedComposable.md @@ -0,0 +1,42 @@ +--- +category: State +related: createGlobalState +--- + +# createSharedComposable + +Make a composable function usable with multiple Vue instances. + +> [!WARNING] +> When used in a **SSR** environment, `createSharedComposable` will **automatically fallback** to a non-shared version. +> This means every call will create a fresh instance in SSR to avoid [cross-request state pollution](https://vuejs.org/guide/scaling-up/ssr.html#cross-request-state-pollution). + +## Usage + +```ts +import { createSharedComposable, useMouse } from '@vueuse/core' + +const useSharedMouse = createSharedComposable(useMouse) + +// CompA.vue +const { x, y } = useSharedMouse() + +// CompB.vue - will reuse the previous state and no new event listeners will be registered +const { x, y } = useSharedMouse() +``` + +## Type Declarations + +```ts +export type SharedComposableReturn = T +/** + * Make a composable function usable with multiple Vue instances. + * + * @see https://vueuse.org/createSharedComposable + * + * @__NO_SIDE_EFFECTS__ + */ +export declare function createSharedComposable( + composable: Fn, +): SharedComposableReturn +``` diff --git a/.agents/skills/vueuse-functions/references/createTemplatePromise.md b/.agents/skills/vueuse-functions/references/createTemplatePromise.md new file mode 100644 index 0000000..c27d6b4 --- /dev/null +++ b/.agents/skills/vueuse-functions/references/createTemplatePromise.md @@ -0,0 +1,306 @@ +--- +category: Component +outline: deep +--- + +# createTemplatePromise + +Template as Promise. Useful for constructing custom Dialogs, Modals, Toasts, etc. + +## Usage + +```vue + + + +``` + +## Features + +- **Programmatic** - call your UI as a promise +- **Template** - use Vue template to render, not a new DSL +- **TypeScript** - full type safety via generic type +- **Renderless** - you take full control of the UI +- **Transition** - use support Vue transition + +This function is migrated from [vue-template-promise](https://github.com/antfu/vue-template-promise) + +## Usage + +`createTemplatePromise` returns a **Vue Component** that you can directly use in your template with ` + + + + +``` + +Learn more about [Vue Transition](https://vuejs.org/guide/built-ins/transition.html). + +### Slot Props + +The slot provides the following props: + +| Prop | Type | Description | +| ------------- | ---------------------------------------- | --------------------------------------------------------- | +| `promise` | `Promise \| undefined` | The current promise instance | +| `resolve` | `(v: Return \| Promise) => void` | Resolve the promise with a value | +| `reject` | `(v: any) => void` | Reject the promise | +| `args` | `Args` | Arguments passed to `start()` | +| `isResolving` | `boolean` | `true` when resolving another promise passed to `resolve` | +| `key` | `number` | Unique key for list rendering | + +```vue + +``` + +## Motivation + +The common approach to call a dialog or a modal programmatically would be like this: + +```ts +const dialog = useDialog() +const result = await dialog.open({ + title: 'Hello', + content: 'World', +}) +``` + +This would work by sending these information to the top-level component and let it render the dialog. However, it limits the flexibility you could express in the UI. For example, you could want the title to be red, or have extra buttons, etc. You would end up with a lot of options like: + +```ts +const result = await dialog.open({ + title: 'Hello', + titleClass: 'text-red', + content: 'World', + contentClass: 'text-blue text-sm', + buttons: [ + { text: 'OK', class: 'bg-red', onClick: () => {} }, + { text: 'Cancel', class: 'bg-blue', onClick: () => {} }, + ], + // ... +}) +``` + +Even this is not flexible enough. If you want more, you might end up with manual render function. + +```ts +const result = await dialog.open({ + title: 'Hello', + contentSlot: () => h(MyComponent, { content }), +}) +``` + +This is like reinventing a new DSL in the script to express the UI template. + +So this function allows **expressing the UI in templates instead of scripts**, where it is supposed to be, while still being able to be manipulated programmatically. + +## Type Declarations + +```ts +export interface TemplatePromiseProps { + /** + * The promise instance. + */ + promise: Promise | undefined + /** + * Resolve the promise. + */ + resolve: (v: Return | Promise) => void + /** + * Reject the promise. + */ + reject: (v: any) => void + /** + * Arguments passed to TemplatePromise.start() + */ + args: Args + /** + * Indicates if the promise is resolving. + * When passing another promise to `resolve`, this will be set to `true` until the promise is resolved. + */ + isResolving: boolean + /** + * Options passed to createTemplatePromise() + */ + options: TemplatePromiseOptions + /** + * Unique key for list rendering. + */ + key: number +} +export interface TemplatePromiseOptions { + /** + * Determines if the promise can be called only once at a time. + * + * @default false + */ + singleton?: boolean + /** + * Transition props for the promise. + */ + transition?: TransitionGroupProps +} +export type TemplatePromise< + Return, + Args extends any[] = [], +> = DefineComponent & { + new (): { + $slots: { + default: (_: TemplatePromiseProps) => any + } + } +} & { + start: (...args: Args) => Promise +} +/** + * Creates a template promise component. + * + * @see https://vueuse.org/createTemplatePromise + * + * @__NO_SIDE_EFFECTS__ + */ +export declare function createTemplatePromise( + options?: TemplatePromiseOptions, +): TemplatePromise +``` diff --git a/.agents/skills/vueuse-functions/references/createUnrefFn.md b/.agents/skills/vueuse-functions/references/createUnrefFn.md new file mode 100644 index 0000000..0335b85 --- /dev/null +++ b/.agents/skills/vueuse-functions/references/createUnrefFn.md @@ -0,0 +1,51 @@ +--- +category: Utilities +related: reactify +--- + +# createUnrefFn + +Make a plain function accepting ref and raw values as arguments. +Returns the same value the unconverted function returns, with proper typing. + +::: tip +Make sure you're using the right tool for the job. Using `reactify` +might be more pertinent in some cases where you want to evaluate the function on each changes of it's arguments. +::: + +## Usage + +```ts +import { createUnrefFn } from '@vueuse/core' +import { shallowRef } from 'vue' + +const url = shallowRef('https://httpbin.org/post') +const data = shallowRef({ foo: 'bar' }) + +function post(url, data) { + return fetch(url, { data }) +} +const unrefPost = createUnrefFn(post) + +post(url, data) /* ❌ Will throw an error because the arguments are refs */ +unrefPost(url, data) /* ✔️ Will Work because the arguments will be auto unref */ +``` + +## Type Declarations + +```ts +export type UnrefFn = T extends (...args: infer A) => infer R + ? ( + ...args: { + [K in keyof A]: MaybeRef + } + ) => R + : never +/** + * Make a plain function accepting ref and raw values as arguments. + * Returns the same value the unconverted function returns, with proper typing. + * + * @__NO_SIDE_EFFECTS__ + */ +export declare function createUnrefFn(fn: T): UnrefFn +``` diff --git a/.agents/skills/vueuse-functions/references/extendRef.md b/.agents/skills/vueuse-functions/references/extendRef.md new file mode 100644 index 0000000..478eebb --- /dev/null +++ b/.agents/skills/vueuse-functions/references/extendRef.md @@ -0,0 +1,76 @@ +--- +category: Reactivity +--- + +# extendRef + +Add extra attributes to Ref. + +## Usage + +> Please note the extra attribute will not be accessible in Vue's template. + +```ts +import { extendRef } from '@vueuse/core' +import { shallowRef } from 'vue' + +const myRef = shallowRef('content') + +const extended = extendRef(myRef, { foo: 'extra data' }) + +extended.value === 'content' +extended.foo === 'extra data' +``` + +Refs will be unwrapped and be reactive + +```ts +import { extendRef } from '@vueuse/core' +// ---cut--- +const myRef = shallowRef('content') +const extraRef = shallowRef('extra') + +const extended = extendRef(myRef, { extra: extraRef }) + +extended.value === 'content' +extended.extra === 'extra' + +extended.extra = 'new data' // will trigger update +extraRef.value === 'new data' +``` + +## Type Declarations + +```ts +export type ExtendRefReturn = Ref +export interface ExtendRefOptions { + /** + * Is the extends properties enumerable + * + * @default false + */ + enumerable?: boolean + /** + * Unwrap for Ref properties + * + * @default true + */ + unwrap?: Unwrap +} +/** + * Overload 1: Unwrap set to false + */ +export declare function extendRef< + R extends Ref, + Extend extends object, + Options extends ExtendRefOptions, +>(ref: R, extend: Extend, options?: Options): ShallowUnwrapRef & R +/** + * Overload 2: Unwrap unset or set to true + */ +export declare function extendRef< + R extends Ref, + Extend extends object, + Options extends ExtendRefOptions, +>(ref: R, extend: Extend, options?: Options): Extend & R +``` diff --git a/.agents/skills/vueuse-functions/references/from.md b/.agents/skills/vueuse-functions/references/from.md new file mode 100644 index 0000000..81a80c2 --- /dev/null +++ b/.agents/skills/vueuse-functions/references/from.md @@ -0,0 +1,80 @@ +--- +category: '@RxJS' +--- + +# from / fromEvent + +Wrappers around RxJS's [`from()`](https://rxjs.dev/api/index/function/from) and [`fromEvent()`](https://rxjs.dev/api/index/function/fromEvent) to allow them to accept `ref`s. + +## Usage + + + +```ts no-twoslash +import { from, fromEvent, toObserver, useSubscription } from '@vueuse/rxjs' +import { interval } from 'rxjs' +import { map, mapTo, takeUntil, withLatestFrom } from 'rxjs/operators' +import { shallowRef, useTemplateRef } from 'vue' + +const count = shallowRef(0) +const button = useTemplateRef('buttonRef') + +useSubscription( + interval(1000) + .pipe( + mapTo(1), + takeUntil(fromEvent(button, 'click')), + withLatestFrom(from(count, { + immediate: true, + deep: false, + })), + map(([curr, total]) => curr + total), + ) + .subscribe(toObserver(count)), // same as ).subscribe(val => (count.value = val)) +) +``` + +## from + +The `from` function can accept either a standard RxJS `ObservableInput` or a Vue `ref`. When passed a ref, it creates an Observable that emits whenever the ref's value changes. + +### Watch Options + +When using `from` with a ref, you can pass Vue's `WatchOptions`: + +| Option | Type | Description | +| ----------- | --------------------------- | ---------------------------------- | +| `immediate` | `boolean` | Emit the current value immediately | +| `deep` | `boolean` | Deeply watch nested objects | +| `flush` | `'pre' \| 'post' \| 'sync'` | Timing of the callback flush | + +## fromEvent + +The `fromEvent` function extends RxJS's `fromEvent` to accept a ref to an element. When the ref's value changes (e.g., after the component mounts), it automatically subscribes to the new element. + +```ts no-twoslash +import { fromEvent, useSubscription } from '@vueuse/rxjs' +import { useTemplateRef } from 'vue' + +const button = useTemplateRef('buttonRef') + +// Will automatically subscribe when the button element becomes available +useSubscription( + fromEvent(button, 'click').subscribe(() => { + console.log('clicked!') + }) +) +``` + +## Type Declarations + +```ts +export declare function from( + value: ObservableInput | Ref, + watchOptions?: WatchOptions, +): Observable +export declare function fromEvent( + value: MaybeRef, + event: string, +): Observable +``` diff --git a/.agents/skills/vueuse-functions/references/get.md b/.agents/skills/vueuse-functions/references/get.md new file mode 100644 index 0000000..36971f5 --- /dev/null +++ b/.agents/skills/vueuse-functions/references/get.md @@ -0,0 +1,30 @@ +--- +category: Utilities +--- + +# get + +Shorthand for accessing `ref.value` + +## Usage + +```ts +import { get } from '@vueuse/core' + +const a = ref(42) + +console.log(get(a)) // 42 +``` + +## Type Declarations + +```ts +/** + * Shorthand for accessing `ref.value` + */ +export declare function get(ref: MaybeRef): T +export declare function get( + ref: MaybeRef, + key: K, +): T[K] +``` diff --git a/.agents/skills/vueuse-functions/references/injectLocal.md b/.agents/skills/vueuse-functions/references/injectLocal.md new file mode 100644 index 0000000..165acb3 --- /dev/null +++ b/.agents/skills/vueuse-functions/references/injectLocal.md @@ -0,0 +1,35 @@ +--- +category: State +--- + +# injectLocal + +Extended `inject` with ability to call `provideLocal` to provide the value in the same component. + +## Usage + +```vue + +``` + +## Type Declarations + +```ts +/** + * On the basis of `inject`, it is allowed to directly call inject to obtain the value after call provide in the same component. + * + * @example + * ```ts + * injectLocal('MyInjectionKey', 1) + * const injectedValue = injectLocal('MyInjectionKey') // injectedValue === 1 + * ``` + * + * @__NO_SIDE_EFFECTS__ + */ +export declare const injectLocal: typeof inject +``` diff --git a/.agents/skills/vueuse-functions/references/isDefined.md b/.agents/skills/vueuse-functions/references/isDefined.md new file mode 100644 index 0000000..d8a9866 --- /dev/null +++ b/.agents/skills/vueuse-functions/references/isDefined.md @@ -0,0 +1,31 @@ +--- +category: Utilities +--- + +# isDefined + +Non-nullish checking type guard for Ref. + +## Usage + +```ts +import { isDefined } from '@vueuse/core' + +const example = ref(Math.random() ? 'example' : undefined) // Ref + +if (isDefined(example)) + example // Ref +``` + +## Type Declarations + +```ts +export type IsDefinedReturn = boolean +export declare function isDefined( + v: ComputedRef, +): v is ComputedRef> +export declare function isDefined( + v: Ref, +): v is Ref> +export declare function isDefined(v: T): v is Exclude +``` diff --git a/.agents/skills/vueuse-functions/references/logicAnd.md b/.agents/skills/vueuse-functions/references/logicAnd.md new file mode 100644 index 0000000..4b4a6ce --- /dev/null +++ b/.agents/skills/vueuse-functions/references/logicAnd.md @@ -0,0 +1,40 @@ +--- +category: '@Math' +alias: and +related: logicNot, logicOr +--- + +# logicAnd + +`AND` condition for refs. + +## Usage + +```ts +import { whenever } from '@vueuse/core' +import { logicAnd } from '@vueuse/math' + +const a = ref(true) +const b = ref(false) + +whenever(logicAnd(a, b), () => { + console.log('both a and b are now truthy!') +}) +``` + +## Type Declarations + +```ts +/** + * `AND` conditions for refs. + * + * @see https://vueuse.org/logicAnd + * + * @__NO_SIDE_EFFECTS__ + */ +export declare function logicAnd( + ...args: MaybeRefOrGetter[] +): ComputedRef +/** @deprecated use `logicAnd` instead */ +export declare const and: typeof logicAnd +``` diff --git a/.agents/skills/vueuse-functions/references/logicNot.md b/.agents/skills/vueuse-functions/references/logicNot.md new file mode 100644 index 0000000..b5b324c --- /dev/null +++ b/.agents/skills/vueuse-functions/references/logicNot.md @@ -0,0 +1,36 @@ +--- +category: '@Math' +alias: not +--- + +# logicNot + +`NOT` condition for ref. + +## Usage + +```ts +import { whenever } from '@vueuse/core' +import { logicNot } from '@vueuse/math' + +const a = ref(true) + +whenever(logicNot(a), () => { + console.log('a is now falsy!') +}) +``` + +## Type Declarations + +```ts +/** + * `NOT` conditions for refs. + * + * @see https://vueuse.org/logicNot + * + * @__NO_SIDE_EFFECTS__ + */ +export declare function logicNot(v: MaybeRefOrGetter): ComputedRef +/** @deprecated use `logicNot` instead */ +export declare const not: typeof logicNot +``` diff --git a/.agents/skills/vueuse-functions/references/logicOr.md b/.agents/skills/vueuse-functions/references/logicOr.md new file mode 100644 index 0000000..4fa8bb1 --- /dev/null +++ b/.agents/skills/vueuse-functions/references/logicOr.md @@ -0,0 +1,40 @@ +--- +category: '@Math' +alias: or +related: logicAnd, logicNot +--- + +# logicOr + +`OR` conditions for refs. + +## Usage + +```ts +import { whenever } from '@vueuse/core' +import { logicOr } from '@vueuse/math' + +const a = ref(true) +const b = ref(false) + +whenever(logicOr(a, b), () => { + console.log('either a or b is truthy!') +}) +``` + +## Type Declarations + +```ts +/** + * `OR` conditions for refs. + * + * @see https://vueuse.org/logicOr + * + * @__NO_SIDE_EFFECTS__ + */ +export declare function logicOr( + ...args: MaybeRefOrGetter[] +): ComputedRef +/** @deprecated use `logicOr` instead */ +export declare const or: typeof logicOr +``` diff --git a/.agents/skills/vueuse-functions/references/makeDestructurable.md b/.agents/skills/vueuse-functions/references/makeDestructurable.md new file mode 100644 index 0000000..261d423 --- /dev/null +++ b/.agents/skills/vueuse-functions/references/makeDestructurable.md @@ -0,0 +1,41 @@ +--- +category: Utilities +--- + +# makeDestructurable + +Make isomorphic destructurable for object and array at the same time. See [this blog](https://antfu.me/posts/destructuring-with-object-or-array/) for more details. + +## Usage + +TypeScript Example: + +```ts twoslash include main +import { makeDestructurable } from '@vueuse/core' + +const foo = { name: 'foo' } +const bar = 1024 + +const obj = makeDestructurable( + { foo, bar } as const, + [foo, bar] as const, +) +``` + +Usage: + +```ts twoslash +// @include: main +// ---cut--- +let { foo, bar } = obj +let [foo, bar] = obj +``` + +## Type Declarations + +```ts +export declare function makeDestructurable< + T extends Record, + A extends readonly any[], +>(obj: T, arr: A): T & A +``` diff --git a/.agents/skills/vueuse-functions/references/onClickOutside.md b/.agents/skills/vueuse-functions/references/onClickOutside.md new file mode 100644 index 0000000..f148778 --- /dev/null +++ b/.agents/skills/vueuse-functions/references/onClickOutside.md @@ -0,0 +1,228 @@ +--- +category: Sensors +--- + +# onClickOutside + +Listen for clicks outside of an element. Useful for modals or dropdowns. + +## Usage + +```vue + + + +``` + +### Return Value + +By default, `onClickOutside` returns a `stop` function to remove the event listeners. + +```ts +const stop = onClickOutside(target, handler) + +// Later, stop listening +stop() +``` + +### Controls + +If you need more control over triggering the handler, you can use the `controls` option. This returns an object with `stop`, `cancel`, and `trigger` functions. + +```ts +const { stop, cancel, trigger } = onClickOutside( + modalRef, + (event) => { + modal.value = false + }, + { controls: true }, +) + +// cancel prevents the next click from triggering the handler +cancel() + +// trigger manually fires the handler +trigger(event) + +// stop removes all event listeners +stop() +``` + +### Ignore Elements + +Use the `ignore` option to prevent certain elements from triggering the handler. Provide elements as an array of Refs or CSS selectors. + +```ts +const ignoreElRef = useTemplateRef('ignoreEl') + +onClickOutside( + target, + event => console.log(event), + { ignore: [ignoreElRef, '.ignore-class', '#ignore-id'] }, +) +``` + +### Capture Phase + +By default, the event listener uses the capture phase (`capture: true`). Set `capture: false` to use the bubbling phase instead. + +```ts +onClickOutside(target, handler, { capture: false }) +``` + +### Detect Iframe Clicks + +Clicks inside an iframe are not detected by default. Enable `detectIframe` to also trigger the handler when focus moves to an iframe. + +```ts +onClickOutside(target, handler, { detectIframe: true }) +``` + +## Component Usage + +```vue + +``` + +## Directive Usage + +```vue + + + +``` + +You can also set the handler as an array to set the configuration items of the instruction. + +```vue + + + +``` + +## Type Declarations + +```ts +export interface OnClickOutsideOptions< + Controls extends boolean = false, +> extends ConfigurableWindow { + /** + * List of elements that should not trigger the event, + * provided as Refs or CSS Selectors. + */ + ignore?: MaybeRefOrGetter<(MaybeElementRef | string)[]> + /** + * Use capturing phase for internal event listener. + * @default true + */ + capture?: boolean + /** + * Run handler function if focus moves to an iframe. + * @default false + */ + detectIframe?: boolean + /** + * Use controls to cancel/trigger listener. + * @default false + */ + controls?: Controls +} +export type OnClickOutsideHandler< + T extends OnClickOutsideOptions = OnClickOutsideOptions, +> = ( + event: + | (T["detectIframe"] extends true ? FocusEvent : never) + | (T["controls"] extends true ? Event : never) + | PointerEvent, +) => void +export type OnClickOutsideReturn = + Controls extends false + ? Fn + : { + stop: Fn + cancel: Fn + trigger: (event: Event) => void + } +/** + * Listen for clicks outside of an element. + * + * @see https://vueuse.org/onClickOutside + * @param target + * @param handler + * @param options + */ +export declare function onClickOutside( + target: MaybeComputedElementRef, + handler: OnClickOutsideHandler, + options?: T, +): Fn +export declare function onClickOutside>( + target: MaybeComputedElementRef, + handler: OnClickOutsideHandler, + options: T, +): { + stop: Fn + cancel: Fn + trigger: (event: Event) => void +} +``` diff --git a/.agents/skills/vueuse-functions/references/onElementRemoval.md b/.agents/skills/vueuse-functions/references/onElementRemoval.md new file mode 100644 index 0000000..6fb9ed5 --- /dev/null +++ b/.agents/skills/vueuse-functions/references/onElementRemoval.md @@ -0,0 +1,88 @@ +--- +category: Sensors +--- + +# onElementRemoval + +Fires when the element or any element containing it is removed from the DOM. + +## Usage + +```vue {13} + + + +``` + +### Callback with Mutation Records + +The callback receives an array of `MutationRecord` objects that triggered the removal. + +```ts +import { onElementRemoval } from '@vueuse/core' + +onElementRemoval(targetRef, (mutationRecords) => { + console.log('Element removed', mutationRecords) +}) +``` + +### Return Value + +Returns a stop function to stop observing. + +```ts +const stop = onElementRemoval(targetRef, callback) + +// Later, stop observing +stop() +``` + +## Type Declarations + +```ts +export interface OnElementRemovalOptions + extends + ConfigurableWindow, + ConfigurableDocumentOrShadowRoot, + WatchOptionsBase {} +/** + * Fires when the element or any element containing it is removed. + * + * @param target + * @param callback + * @param options + */ +export declare function onElementRemoval( + target: MaybeElementRef, + callback: (mutationRecords: MutationRecord[]) => void, + options?: OnElementRemovalOptions, +): Fn +``` diff --git a/.agents/skills/vueuse-functions/references/onKeyStroke.md b/.agents/skills/vueuse-functions/references/onKeyStroke.md new file mode 100644 index 0000000..9ef55fe --- /dev/null +++ b/.agents/skills/vueuse-functions/references/onKeyStroke.md @@ -0,0 +1,211 @@ +--- +category: Sensors +--- + +# onKeyStroke + +Listen for keyboard keystrokes. By default, listens on `keydown` events on `window`. + +## Usage + +```ts +import { onKeyStroke } from '@vueuse/core' + +onKeyStroke('ArrowDown', (e) => { + e.preventDefault() +}) +``` + +See [this table](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values) for all key codes. + +### Return Value + +Returns a stop function to remove the event listener. + +```ts +const stop = onKeyStroke('Escape', handler) + +// Later, stop listening +stop() +``` + +### Listen To Multiple Keys + +```ts +import { onKeyStroke } from '@vueuse/core' + +onKeyStroke(['s', 'S', 'ArrowDown'], (e) => { + e.preventDefault() +}) + +// listen to all keys by passing `true` or skipping the key parameter +onKeyStroke(true, (e) => { + e.preventDefault() +}) +onKeyStroke((e) => { + e.preventDefault() +}) +``` + +### Custom Key Predicate + +You can pass a custom function to determine which keys should trigger the handler. + +```ts +import { onKeyStroke } from '@vueuse/core' + +onKeyStroke( + e => e.key === 'A' && e.shiftKey, + (e) => { + console.log('Shift+A pressed') + }, +) +``` + +### Custom Event Target + +```ts +import { onKeyStroke } from '@vueuse/core' + +onKeyStroke('A', (e) => { + console.log('Key A pressed on document') +}, { target: document }) +``` + +### Ignore Repeated Events + +The callback will trigger only once when pressing `A` and **holding down**. The `dedupe` option can also be a reactive ref. + +```ts +import { onKeyStroke } from '@vueuse/core' + +onKeyStroke('A', (e) => { + console.log('Key A pressed') +}, { dedupe: true }) +``` + +Reference: [KeyboardEvent.repeat](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/repeat) + +### Passive Mode + +Set `passive: true` to use a passive event listener. + +```ts +import { onKeyStroke } from '@vueuse/core' + +onKeyStroke('A', handler, { passive: true }) +``` + +## Directive Usage + +```vue + + + +``` + +### Custom Keyboard Event + +```ts +import { onKeyStroke } from '@vueuse/core' +// ---cut--- +onKeyStroke('Shift', (e) => { + console.log('Shift key up') +}, { eventName: 'keyup' }) +``` + +Or + +```ts +import { onKeyUp } from '@vueuse/core' +// ---cut--- +onKeyUp('Shift', () => console.log('Shift key up')) +``` + +## Shorthands + +- `onKeyDown` - alias for `onKeyStroke(key, handler, {eventName: 'keydown'})` +- `onKeyPressed` - alias for `onKeyStroke(key, handler, {eventName: 'keypress'})` +- `onKeyUp` - alias for `onKeyStroke(key, handler, {eventName: 'keyup'})` + +## Type Declarations + +```ts +export type KeyPredicate = (event: KeyboardEvent) => boolean +export type KeyFilter = true | string | string[] | KeyPredicate +export type KeyStrokeEventName = "keydown" | "keypress" | "keyup" +export interface OnKeyStrokeOptions { + eventName?: KeyStrokeEventName + target?: MaybeRefOrGetter + passive?: boolean + /** + * Set to `true` to ignore repeated events when the key is being held down. + * + * @default false + */ + dedupe?: MaybeRefOrGetter +} +/** + * Listen for keyboard keystrokes. + * + * @see https://vueuse.org/onKeyStroke + */ +export declare function onKeyStroke( + key: KeyFilter, + handler: (event: KeyboardEvent) => void, + options?: OnKeyStrokeOptions, +): () => void +export declare function onKeyStroke( + handler: (event: KeyboardEvent) => void, + options?: OnKeyStrokeOptions, +): () => void +/** + * Listen to the keydown event of the given key. + * + * @see https://vueuse.org/onKeyStroke + * @param key + * @param handler + * @param options + */ +export declare function onKeyDown( + key: KeyFilter, + handler: (event: KeyboardEvent) => void, + options?: Omit, +): () => void +/** + * Listen to the keypress event of the given key. + * + * @see https://vueuse.org/onKeyStroke + * @param key + * @param handler + * @param options + */ +export declare function onKeyPressed( + key: KeyFilter, + handler: (event: KeyboardEvent) => void, + options?: Omit, +): () => void +/** + * Listen to the keyup event of the given key. + * + * @see https://vueuse.org/onKeyStroke + * @param key + * @param handler + * @param options + */ +export declare function onKeyUp( + key: KeyFilter, + handler: (event: KeyboardEvent) => void, + options?: Omit, +): () => void +``` diff --git a/.agents/skills/vueuse-functions/references/onLongPress.md b/.agents/skills/vueuse-functions/references/onLongPress.md new file mode 100644 index 0000000..20e5e05 --- /dev/null +++ b/.agents/skills/vueuse-functions/references/onLongPress.md @@ -0,0 +1,229 @@ +--- +category: Sensors +--- + +# onLongPress + +Listen for a long press on an element. Returns a stop function. + +## Usage + +```vue + + + +``` + +### Custom Delay + +By default, the handler fires after 500ms. You can customize this with the `delay` option. It can be a number or a function that receives the `PointerEvent`. + +```ts +import { onLongPress } from '@vueuse/core' + +// Fixed delay +onLongPress(target, handler, { delay: 1000 }) + +// Dynamic delay based on event +onLongPress(target, handler, { + delay: ev => ev.pointerType === 'touch' ? 800 : 500, +}) +``` + +### Distance Threshold + +The long press will be canceled if the pointer moves more than the threshold (default: 10 pixels). Set to `false` to disable movement detection. + +```ts +import { onLongPress } from '@vueuse/core' + +// Custom threshold +onLongPress(target, handler, { distanceThreshold: 20 }) + +// Disable movement detection +onLongPress(target, handler, { distanceThreshold: false }) +``` + +### On Mouse Up Callback + +You can provide an `onMouseUp` callback to be notified when the pointer is released. + +```ts +import { onLongPress } from '@vueuse/core' + +onLongPress(target, handler, { + onMouseUp(duration, distance, isLongPress) { + console.log(`Held for ${duration}ms, moved ${distance}px, long press: ${isLongPress}`) + }, +}) +``` + +### Modifiers + +The following modifiers are available: + +| Modifier | Description | +| --------- | -------------------------------------------- | +| `stop` | Calls `event.stopPropagation()` | +| `once` | Removes event listener after first trigger | +| `prevent` | Calls `event.preventDefault()` | +| `capture` | Uses capture mode for event listener | +| `self` | Only trigger if target is the element itself | + +```ts +onLongPress(target, handler, { + modifiers: { + prevent: true, + stop: true, + }, +}) +``` + +## Component Usage + +```vue + + + +``` + +## Directive Usage + +```vue + + + +``` + +## Type Declarations + +```ts +export interface OnLongPressOptions { + /** + * Time in ms till `longpress` gets called + * + * @default 500 + */ + delay?: number | ((ev: PointerEvent) => number) + modifiers?: OnLongPressModifiers + /** + * Allowance of moving distance in pixels, + * The action will get canceled When moving too far from the pointerdown position. + * @default 10 + */ + distanceThreshold?: number | false + /** + * Function called when the ref element is released. + * @param duration how long the element was pressed in ms + * @param distance distance from the pointerdown position + * @param isLongPress whether the action was a long press or not + */ + onMouseUp?: (duration: number, distance: number, isLongPress: boolean) => void +} +export interface OnLongPressModifiers { + stop?: boolean + once?: boolean + prevent?: boolean + capture?: boolean + self?: boolean +} +export type OnLongPressReturn = () => void +/** @deprecated use {@link OnLongPressReturn} instead */ +export type UseOnLongPressReturn = OnLongPressReturn +export declare function onLongPress( + target: MaybeElementRef, + handler: (evt: PointerEvent) => void, + options?: OnLongPressOptions, +): OnLongPressReturn +``` diff --git a/.agents/skills/vueuse-functions/references/onStartTyping.md b/.agents/skills/vueuse-functions/references/onStartTyping.md new file mode 100644 index 0000000..d7527b5 --- /dev/null +++ b/.agents/skills/vueuse-functions/references/onStartTyping.md @@ -0,0 +1,53 @@ +--- +category: Sensors +--- + +# onStartTyping + +Fires when users start typing on non-editable elements. Useful for auto-focusing an input field when the user starts typing anywhere on the page. + +## Usage + +```vue + + + +``` + +## How It Works + +The callback only fires when: + +- No editable element (``, `