diff --git a/client.d.ts b/client.d.ts index e6afb9f3b..266ece901 100644 --- a/client.d.ts +++ b/client.d.ts @@ -4,13 +4,16 @@ declare module 'vue-router/auto-routes' { /** * Array of routes generated by unplugin-vue-router */ - export const routes: RouteRecordRaw[] + export const routes: readonly RouteRecordRaw[] /** * Setups hot module replacement for routes. + * * @param router - The router instance * @param hotUpdateCallback - Callback to be called after replacing the routes and before the navigation + * * @example + * * ```ts * import { createRouter, createWebHistory } from 'vue-router' * import { routes, handleHotUpdate } from 'vue-router/auto-routes' @@ -19,7 +22,7 @@ declare module 'vue-router/auto-routes' { * routes, * }) * if (import.meta.hot) { - * handleHotUpdate(router) + * handleHotUpdate(router) * } * ``` */ @@ -31,9 +34,11 @@ declare module 'vue-router/auto-routes' { declare module 'vue-router' { import type { RouteNamedMap } from 'vue-router/auto-routes' + import type { ParamParserCustom } from 'vue-router/auto-resolver' export interface TypesConfig { RouteNamedMap: RouteNamedMap + ParamParsers: ParamParserCustom } } diff --git a/examples/nuxt/package.json b/examples/nuxt/package.json index bcb1cb9e0..4079ab5c1 100644 --- a/examples/nuxt/package.json +++ b/examples/nuxt/package.json @@ -8,7 +8,7 @@ }, "devDependencies": { "@pinia/colada-nuxt": "^0.2.1", - "@pinia/nuxt": "^0.11.1", + "@pinia/nuxt": "^0.11.2", "nuxt": "^3.17.5", "unplugin-vue-router": "workspace:*" }, diff --git a/package.json b/package.json index 1e77f56c2..b6bf3d5c8 100644 --- a/package.json +++ b/package.json @@ -139,7 +139,8 @@ ] }, "dependencies": { - "@vue-macros/common": "3.0.0", + "@babel/generator": "^7.28.3", + "@vue-macros/common": "^3.0.0", "@vue/language-core": "^3.0.7", "ast-walker-scope": "^0.8.2", "chokidar": "^4.0.3", @@ -171,7 +172,8 @@ "@posva/prompts": "^2.4.4", "@shikijs/vitepress-twoslash": "3.13.0", "@tanstack/vue-query": "^5.89.0", - "@types/node": "^22.18.6", + "@types/babel__generator": "^7.27.0", + "@types/node": "^24.3.0", "@types/picomatch": "^4.0.2", "@vitest/coverage-v8": "^3.2.4", "@vitest/ui": "^3.2.4", @@ -203,7 +205,7 @@ "vitepress-plugin-llms": "^1.7.5", "vitest": "^3.2.4", "vue": "^3.5.21", - "vue-router": "^4.5.1", + "vue-router": "https://pkg.pr.new/vue-router@4f1a37a", "vue-router-mock": "^2.0.0", "vue-tsc": "^3.0.7", "vuefire": "^3.2.2", diff --git a/playground-experimental/.gitignore b/playground-experimental/.gitignore new file mode 100644 index 000000000..cc1b7f164 --- /dev/null +++ b/playground-experimental/.gitignore @@ -0,0 +1 @@ +tsconfig.tsbuildinfo diff --git a/playground-experimental/auto-imports.d.ts b/playground-experimental/auto-imports.d.ts new file mode 100644 index 000000000..f77a9b47b --- /dev/null +++ b/playground-experimental/auto-imports.d.ts @@ -0,0 +1,14 @@ +/* eslint-disable */ +/* prettier-ignore */ +// @ts-nocheck +// noinspection JSUnusedGlobalSymbols +// Generated by unplugin-auto-import +// biome-ignore lint: disable +export {} +declare global { + const defineBasicLoader: typeof import('../src/data-loaders/entries/basic')['defineBasicLoader'] + const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave'] + const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate'] + const useRoute: typeof import('vue-router')['useRoute'] + const useRouter: typeof import('vue-router')['useRouter'] +} diff --git a/playground-experimental/db.json b/playground-experimental/db.json new file mode 100644 index 000000000..53e4f7413 --- /dev/null +++ b/playground-experimental/db.json @@ -0,0 +1,24 @@ +{ + "todos": [ + { + "id": 2, + "title": "Walking", + "completed": true + }, + { + "id": 3, + "title": "Cleaning", + "completed": false + }, + { + "id": 4, + "title": "Cooking", + "completed": true + }, + { + "title": "hello", + "completed": false, + "id": 7 + } + ] +} \ No newline at end of file diff --git a/playground-experimental/env.d.ts b/playground-experimental/env.d.ts new file mode 100644 index 000000000..dabd0deba --- /dev/null +++ b/playground-experimental/env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/playground-experimental/index.html b/playground-experimental/index.html new file mode 100644 index 000000000..e69792de0 --- /dev/null +++ b/playground-experimental/index.html @@ -0,0 +1,22 @@ + + + + + + + + + visit /__inspect/ to inspect the intermediate state +
+ + + diff --git a/playground-experimental/package.json b/playground-experimental/package.json new file mode 100644 index 000000000..03ec9010f --- /dev/null +++ b/playground-experimental/package.json @@ -0,0 +1,23 @@ +{ + "private": true, + "type": "module", + "scripts": { + "dev": "nodemon -w '../src/**/*.ts' -e .ts -x vite", + "json-server": "json-server --watch db.json --port 4000", + "playground:build": "vite build" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^6.0.1", + "@vue/compiler-sfc": "^3.5.18", + "@vue/tsconfig": "^0.7.0", + "json-server": "^0.17.4", + "unplugin-vue-router": "workspace:*", + "vite": "^7.1.2" + }, + "dependencies": { + "mande": "^2.0.9", + "pinia": "^3.0.3", + "vue": "^3.5.18", + "vue-router": "https://pkg.pr.new/vue-router@4f1a37a" + } +} diff --git a/playground-experimental/src/App.vue b/playground-experimental/src/App.vue new file mode 100644 index 000000000..19683d635 --- /dev/null +++ b/playground-experimental/src/App.vue @@ -0,0 +1,72 @@ + + + + + diff --git a/playground-experimental/src/main.ts b/playground-experimental/src/main.ts new file mode 100644 index 000000000..13178acb5 --- /dev/null +++ b/playground-experimental/src/main.ts @@ -0,0 +1,29 @@ +import { createApp } from 'vue' +import App from './App.vue' +import { createPinia } from 'pinia' +import { PiniaColada } from '@pinia/colada' +import { router } from './router' +import { DataLoaderPlugin } from 'unplugin-vue-router/data-loaders' +import { RouterLink, RouterView } from 'vue-router' + +const app = createApp(App) + +app.use(createPinia()) +app.use(PiniaColada, {}) +// @ts-expect-error: FIXME: should be doable +app.use(DataLoaderPlugin, { router }) +app.component('RouterLink', RouterLink) +app.component('RouterView', RouterView) +app.use(router) + +// @ts-expect-error: for debugging on browser +window.$router = router + +app.mount('#app') + +// small logger for navigations, useful to check HMR +router.isReady().then(() => { + router.beforeEach((to, from) => { + console.log('🧭', from.fullPath, '->', to.fullPath) + }) +}) diff --git a/playground-experimental/src/page-outside.vue b/playground-experimental/src/page-outside.vue new file mode 100644 index 000000000..2848777a2 --- /dev/null +++ b/playground-experimental/src/page-outside.vue @@ -0,0 +1,5 @@ + + + diff --git a/playground-experimental/src/pages/(home).vue b/playground-experimental/src/pages/(home).vue new file mode 100644 index 000000000..746ed526b --- /dev/null +++ b/playground-experimental/src/pages/(home).vue @@ -0,0 +1,7 @@ + + + diff --git a/playground-experimental/src/pages/[...path].vue b/playground-experimental/src/pages/[...path].vue new file mode 100644 index 000000000..4b83b0349 --- /dev/null +++ b/playground-experimental/src/pages/[...path].vue @@ -0,0 +1,71 @@ + + + + + +{ + "meta": { + "from block": true + } +} + diff --git a/playground-experimental/src/pages/a.[b].c.[d].vue b/playground-experimental/src/pages/a.[b].c.[d].vue new file mode 100644 index 000000000..29b5f2e77 --- /dev/null +++ b/playground-experimental/src/pages/a.[b].c.[d].vue @@ -0,0 +1,5 @@ + + + diff --git a/playground-experimental/src/pages/b.vue b/playground-experimental/src/pages/b.vue new file mode 100644 index 000000000..2ec140b33 --- /dev/null +++ b/playground-experimental/src/pages/b.vue @@ -0,0 +1,5 @@ + + + diff --git a/playground-experimental/src/pages/blog/[[slugOptional]]+.vue b/playground-experimental/src/pages/blog/[[slugOptional]]+.vue new file mode 100644 index 000000000..76cf19383 --- /dev/null +++ b/playground-experimental/src/pages/blog/[[slugOptional]]+.vue @@ -0,0 +1,7 @@ + + + diff --git a/playground-experimental/src/pages/blog/[slug]+.vue b/playground-experimental/src/pages/blog/[slug]+.vue new file mode 100644 index 000000000..76cf19383 --- /dev/null +++ b/playground-experimental/src/pages/blog/[slug]+.vue @@ -0,0 +1,7 @@ + + + diff --git a/playground-experimental/src/pages/blog/info/(info).vue b/playground-experimental/src/pages/blog/info/(info).vue new file mode 100644 index 000000000..42c010722 --- /dev/null +++ b/playground-experimental/src/pages/blog/info/(info).vue @@ -0,0 +1,30 @@ + + + diff --git a/playground-experimental/src/pages/blog/info/[[section]].vue b/playground-experimental/src/pages/blog/info/[[section]].vue new file mode 100644 index 000000000..ca35246d7 --- /dev/null +++ b/playground-experimental/src/pages/blog/info/[[section]].vue @@ -0,0 +1,7 @@ + + + diff --git a/playground-experimental/src/pages/events/[when=date].vue b/playground-experimental/src/pages/events/[when=date].vue new file mode 100644 index 000000000..b3f7a9a8a --- /dev/null +++ b/playground-experimental/src/pages/events/[when=date].vue @@ -0,0 +1,8 @@ + + + diff --git a/playground-experimental/src/pages/events/repeat/[when=date]+.vue b/playground-experimental/src/pages/events/repeat/[when=date]+.vue new file mode 100644 index 000000000..c34fc7056 --- /dev/null +++ b/playground-experimental/src/pages/events/repeat/[when=date]+.vue @@ -0,0 +1,9 @@ + + + diff --git a/playground-experimental/src/pages/tests/[[optional]]/end.vue b/playground-experimental/src/pages/tests/[[optional]]/end.vue new file mode 100644 index 000000000..4f5b429ed --- /dev/null +++ b/playground-experimental/src/pages/tests/[[optional]]/end.vue @@ -0,0 +1,7 @@ + + + diff --git a/playground-experimental/src/pages/u[name].vue b/playground-experimental/src/pages/u[name].vue new file mode 100644 index 000000000..2d5ad6847 --- /dev/null +++ b/playground-experimental/src/pages/u[name].vue @@ -0,0 +1,8 @@ + + + diff --git a/playground-experimental/src/pages/u[name]/24.vue b/playground-experimental/src/pages/u[name]/24.vue new file mode 100644 index 000000000..b5778f70e --- /dev/null +++ b/playground-experimental/src/pages/u[name]/24.vue @@ -0,0 +1,6 @@ + + + diff --git a/playground-experimental/src/pages/u[name]/[userId=int].vue b/playground-experimental/src/pages/u[name]/[userId=int].vue new file mode 100644 index 000000000..b8e8f105c --- /dev/null +++ b/playground-experimental/src/pages/u[name]/[userId=int].vue @@ -0,0 +1,7 @@ + + + diff --git a/playground-experimental/src/pages/users/[userId=int].vue b/playground-experimental/src/pages/users/[userId=int].vue new file mode 100644 index 000000000..4e7524625 --- /dev/null +++ b/playground-experimental/src/pages/users/[userId=int].vue @@ -0,0 +1,32 @@ + + + diff --git a/playground-experimental/src/pages/users/sub-[first]-[second].vue b/playground-experimental/src/pages/users/sub-[first]-[second].vue new file mode 100644 index 000000000..29b5f2e77 --- /dev/null +++ b/playground-experimental/src/pages/users/sub-[first]-[second].vue @@ -0,0 +1,5 @@ + + + diff --git a/playground-experimental/src/params/date.ts b/playground-experimental/src/params/date.ts new file mode 100644 index 000000000..eb85e9bdc --- /dev/null +++ b/playground-experimental/src/params/date.ts @@ -0,0 +1,31 @@ +import { defineParamParser, miss } from 'vue-router/experimental' + +function toDate(value: string): Date { + const asDate = new Date(value) + if (Number.isNaN(asDate.getTime())) { + throw miss(`Invalid date: "${value}"`) + } + + return asDate +} + +function toString(value: Date): string { + return ( + value + .toISOString() + // allows keeping simple dates like 2023-10-01 without time + // while still being able to parse full dates like 2023-10-01T12:00:00.000Z + .replace('T00:00:00.000Z', '') + ) +} + +export const parser = defineParamParser({ + get: (value: string | string[] | null) => { + if (!value) { + throw miss() + } + return Array.isArray(value) ? value.map(toDate) : toDate(value) + }, + set: (value: Date | Date[]) => + Array.isArray(value) ? value.map(toString) : toString(value), +}) diff --git a/playground-experimental/src/router.ts b/playground-experimental/src/router.ts new file mode 100644 index 000000000..13d6dc9ab --- /dev/null +++ b/playground-experimental/src/router.ts @@ -0,0 +1,37 @@ +import { experimental_createRouter } from 'vue-router/experimental' +import { resolver, handleHotUpdate } from 'vue-router/auto-resolver' + +import { + type RouteRecordInfo, + type ParamValue, + createWebHistory, +} from 'vue-router' + +export const router = experimental_createRouter({ + history: createWebHistory(), + resolver, +}) + +if (import.meta.hot) { + handleHotUpdate(router) +} + +// manual extension of route types +declare module 'vue-router/auto-routes' { + export interface RouteNamedMap { + 'custom-dynamic-name': RouteRecordInfo< + 'custom-dynamic-name', + '/added-during-runtime/[...path]', + { path: ParamValue }, + { path: ParamValue }, + 'custom-dynamic-child-name' + > + 'custom-dynamic-child-name': RouteRecordInfo< + 'custom-dynamic-child-name', + '/added-during-runtime/[...path]/child', + { path: ParamValue }, + { path: ParamValue }, + never + > + } +} diff --git a/playground-experimental/tsconfig.config.json b/playground-experimental/tsconfig.config.json new file mode 100644 index 000000000..fdcee2d99 --- /dev/null +++ b/playground-experimental/tsconfig.config.json @@ -0,0 +1,8 @@ +{ + "extends": "@vue/tsconfig/tsconfig.json", + "include": ["vite.config.*", "vitest.config.*", "cypress.config.*"], + "compilerOptions": { + "composite": true, + "types": ["node"] + } +} diff --git a/playground-experimental/tsconfig.json b/playground-experimental/tsconfig.json new file mode 100644 index 000000000..7106ea827 --- /dev/null +++ b/playground-experimental/tsconfig.json @@ -0,0 +1,47 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "include": [ + "./env.d.ts", + "./src/**/*.ts", + "./src/**/*.vue", + "./typed-router.d.ts", + "./auto-imports.d.ts", + "../src" + ], + "compilerOptions": { + "baseUrl": ".", + "composite": true, + "moduleResolution": "Bundler", + "paths": { + "@/*": [ + "./src/*" + ], + "unplugin-vue-router/runtime": [ + "../src/runtime.ts" + ], + "unplugin-vue-router/types": [ + "../src/types.ts" + ], + "unplugin-vue-router/data-loaders": [ + "../src/data-loaders/entries/index.ts" + ], + "unplugin-vue-router/data-loaders/basic": [ + "../src/data-loaders/entries/basic.ts" + ], + "unplugin-vue-router/data-loaders/pinia-colada": [ + "../src/data-loaders/entries/pinia-colada.ts" + ] + } + }, + "vueCompilerOptions": { + "plugins": [ + "unplugin-vue-router/volar/sfc-route-blocks", + "unplugin-vue-router/volar/sfc-typed-router" + ] + }, + "references": [ + { + "path": "./tsconfig.config.json" + } + ] +} diff --git a/playground-experimental/typed-router.d.ts b/playground-experimental/typed-router.d.ts new file mode 100644 index 000000000..575766309 --- /dev/null +++ b/playground-experimental/typed-router.d.ts @@ -0,0 +1,138 @@ +/* eslint-disable */ +/* prettier-ignore */ +// @ts-nocheck +// Generated by unplugin-vue-router. ‼️ DO NOT MODIFY THIS FILE ‼️ +// It's recommended to commit this file. +// Make sure to add this file to your tsconfig.json file as an "includes" or "files" entry. + +// Custom route params parsers +type Param_date = ReturnType> + +declare module 'vue-router/auto-resolver' { + export type ParamParserCustom = 'date' +} + +declare module 'vue-router/auto-routes' { + import type { + RouteRecordInfo, + ParamValue, + ParamValueOneOrMore, + ParamValueZeroOrMore, + ParamValueZeroOrOne, + } from 'vue-router' + + /** + * Route name map generated by unplugin-vue-router + */ + export interface RouteNamedMap { + '/(home)': RouteRecordInfo<'/(home)', '/', Record, Record>, + 'not-found': RouteRecordInfo<'not-found', '/:path(.*)', { path: string, page?: number, other?: boolean, active?: boolean, multi: string[], req?: number, when?: Exclude }, { path: string, page: number, other: boolean, active: boolean, multi: string[], req: number, when: Exclude }>, + '/a.[b].c.[d]': RouteRecordInfo<'/a.[b].c.[d]', '/a/:b/c/:d', { b: string, d: string }, { b: string, d: string }>, + '/b': RouteRecordInfo<'/b', '/b', Record, Record>, + '/blog/[slug]+': RouteRecordInfo<'/blog/[slug]+', '/blog/:slug+', { slug: string[] }, { slug: string[] }>, + '/blog/[[slugOptional]]+': RouteRecordInfo<'/blog/[[slugOptional]]+', '/blog/:slugOptional*', { slugOptional?: string[] }, { slugOptional: string[] }>, + '/blog/info/(info)': RouteRecordInfo<'/blog/info/(info)', '/blog/info', Record, Record>, + '/blog/info/[[section]]': RouteRecordInfo<'/blog/info/[[section]]', '/blog/info/:section?', { section?: string | null }, { section: string | null }>, + '/events/[when=date]': RouteRecordInfo<'/events/[when=date]', '/events/:when', { when: Exclude }, { when: Exclude }>, + '/events/repeat/[when=date]+': RouteRecordInfo<'/events/repeat/[when=date]+', '/events/repeat/:when+', { when: Extract }, { when: Extract }>, + '/manually-added': RouteRecordInfo<'/manually-added', '/manually-added', Record, Record>, + '/tests/[[optional]]/end': RouteRecordInfo<'/tests/[[optional]]/end', '/tests/:optional?/end', { optional?: string | null }, { optional: string | null }>, + '/u[name]': RouteRecordInfo<'/u[name]', '/u:name', { name: string }, { name: string }, '/u[name]/24' | '/u[name]/[userId=int]'>, + '/u[name]/[userId=int]': RouteRecordInfo<'/u[name]/[userId=int]', '/u:name/:userId', { name: string, userId: number }, { name: string, userId: number }>, + '/u[name]/24': RouteRecordInfo<'/u[name]/24', '/u:name/24', { name: string }, { name: string }>, + '/users/[userId=int]': RouteRecordInfo<'/users/[userId=int]', '/users/:userId', { userId: number, anyParam?: string, page?: number }, { userId: number, anyParam: string, page: number }>, + '/users/sub-[first]-[second]': RouteRecordInfo<'/users/sub-[first]-[second]', '/users/sub-:first-:second', { first: string, second: string }, { first: string, second: string }>, + } + + /** + * Route file to route info map by unplugin-vue-router. + * Used by the volar plugin to automatically type useRoute() + * + * Each key is a file path relative to the project root with 2 properties: + * - routes: union of route names of the possible routes when in this page (passed to useRoute<...>()) + * - views: names of nested views (can be passed to ) + * + * @internal + */ + export interface _RouteFileInfoMap { + 'src/pages/(home).vue': { + routes: '/(home)' + views: never + } + 'src/pages/[...path].vue': { + routes: 'not-found' + views: never + } + 'src/pages/a.[b].c.[d].vue': { + routes: '/a.[b].c.[d]' + views: never + } + 'src/pages/b.vue': { + routes: '/b' + views: never + } + 'src/pages/blog/[slug]+.vue': { + routes: '/blog/[slug]+' + views: never + } + 'src/pages/blog/[[slugOptional]]+.vue': { + routes: '/blog/[[slugOptional]]+' + views: never + } + 'src/pages/blog/info/(info).vue': { + routes: '/blog/info/(info)' + views: never + } + 'src/pages/blog/info/[[section]].vue': { + routes: '/blog/info/[[section]]' + views: never + } + 'src/pages/events/[when=date].vue': { + routes: '/events/[when=date]' + views: never + } + 'src/pages/events/repeat/[when=date]+.vue': { + routes: '/events/repeat/[when=date]+' + views: never + } + 'src/page-outside.vue': { + routes: '/manually-added' + views: never + } + 'src/pages/tests/[[optional]]/end.vue': { + routes: '/tests/[[optional]]/end' + views: never + } + 'src/pages/u[name].vue': { + routes: '/u[name]' | '/u[name]/[userId=int]' | '/u[name]/24' + views: 'default' + } + 'src/pages/u[name]/[userId=int].vue': { + routes: '/u[name]/[userId=int]' + views: never + } + 'src/pages/u[name]/24.vue': { + routes: '/u[name]/24' + views: never + } + 'src/pages/users/[userId=int].vue': { + routes: '/users/[userId=int]' + views: never + } + 'src/pages/users/sub-[first]-[second].vue': { + routes: '/users/sub-[first]-[second]' + views: never + } + } + + /** + * Get a union of possible route names in a certain route component file. + * Used by the volar plugin to automatically type useRoute() + * + * @internal + */ + export type _RouteNamesForFilePath = + _RouteFileInfoMap extends Record + ? Info['routes'] + : keyof RouteNamedMap +} diff --git a/playground-experimental/vite.config.ts b/playground-experimental/vite.config.ts new file mode 100644 index 000000000..896ff0333 --- /dev/null +++ b/playground-experimental/vite.config.ts @@ -0,0 +1,129 @@ +import { fileURLToPath, URL } from 'node:url' +import { defineConfig } from 'vite' +import Markdown from 'unplugin-vue-markdown/vite' +// @ts-ignore: the plugin should not be checked in the playground +import VueRouter from '../src/vite' +import { VueRouterAutoImports } from '../src' +import Vue from '@vitejs/plugin-vue' +import AutoImport from 'unplugin-auto-import/vite' +import VueDevtools from 'vite-plugin-vue-devtools' +import { join, relative } from 'node:path' + +export default defineConfig({ + clearScreen: false, + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + '~': fileURLToPath(new URL('./src', import.meta.url)), + 'unplugin-vue-router/runtime': fileURLToPath( + new URL('../src/runtime.ts', import.meta.url) + ), + 'unplugin-vue-router/types': fileURLToPath( + new URL('../src/types.ts', import.meta.url) + ), + 'unplugin-vue-router/data-loaders/basic': fileURLToPath( + new URL('../src/data-loaders/entries/basic.ts', import.meta.url) + ), + 'unplugin-vue-router/data-loaders/pinia-colada': fileURLToPath( + new URL('../src/data-loaders/entries/pinia-colada.ts', import.meta.url) + ), + 'unplugin-vue-router/data-loaders': fileURLToPath( + new URL('../src/data-loaders/entries/index.ts', import.meta.url) + ), + }, + }, + build: { + sourcemap: true, + }, + optimizeDeps: { + exclude: [ + // easier to test with yalc + '@pinia/colada', + ], + }, + + plugins: [ + VueRouter({ + extensions: ['.page.vue', '.vue'], + importMode: 'async', + logs: true, + // getRouteName: getPascalCaseRouteName, + experimental: { + autoExportsDataLoaders: ['src/loaders/**/*', '@/loaders/**/*'], + paramParsers: true, + }, + extendRoute(route) { + // example of deleting routes + // if (route.name.startsWith('/users')) { + // route.delete() + // } + + if (route.name === '/[name]') { + // TODO: implement aliases + // route.addAlias('/hello-vite-:name') + } + + // TODO: implement insertions + // const newRoute = root.insert( + // '/custom/page', + // route.components.get('default')! + // ) + // newRoute.components.set('default', route.components.get('default')!) + // newRoute.meta = { + // 'custom-meta': 'works', + // } + // } + }, + beforeWriteFiles(root) { + root.insert( + '/manually-added', + join(__dirname, './src/page-outside.vue') + ) + }, + routesFolder: [ + // can add multiple routes folders + { + src: 'src/pages', + }, + { + src: 'src/docs', + path: 'docs/[lang]/', + // doesn't take into account files directly at src/docs, only subfolders + filePatterns: ['*/**'], + // ignores .vue files + extensions: ['.md'], + }, + { + src: 'src/features', + filePatterns: '*/pages/**/*', + path: (file) => { + return relative('src/features', file).replace(/^pages\//, '') + }, + }, + ], + exclude: ['**/ignored/**', '**/__*', '**/__**/*', '**/*.component.vue'], + }), + Vue({ + include: [/\.vue$/, /\.md$/], + }), + Markdown({}), + AutoImport({ + imports: [ + VueRouterAutoImports, + { + // NOTE: we need to match the resolved paths to local files for development + // instead of just 'unplugin-vue-router/data-loaders/basic': ['defineBasicLoader'], + [fileURLToPath( + new URL('../src/data-loaders/entries/basic.ts', import.meta.url) + )]: ['defineBasicLoader'], + // [fileURLToPath( + // new URL('../src/data-loaders/entries/pinia-colada.ts', import.meta.url) + // )]: ['defineColadaLoader'], + }, + ], + }), + // currently the devtools use 0.8.8 but we care more about + // inspecting virtual files + VueDevtools(), + ], +}) diff --git a/playground/src/pages/(some-layout).vue b/playground/src/pages/(some-layout).vue index cf0fff14d..ba4dd01c9 100644 --- a/playground/src/pages/(some-layout).vue +++ b/playground/src/pages/(some-layout).vue @@ -1,6 +1,6 @@ diff --git a/playground/src/params/date.ts b/playground/src/params/date.ts new file mode 100644 index 000000000..490dbb617 --- /dev/null +++ b/playground/src/params/date.ts @@ -0,0 +1,18 @@ +// NOTE: should be imported from vue-router +const invalid = (...args: ConstructorParameters) => + new Error(...args) + +export const parse = (value: string): Date => { + const asDate = new Date(value) + if (Number.isNaN(asDate.getTime())) { + throw invalid(`Invalid date: "${value}"`) + } + + return asDate +} + +export const toString = (value: Date): string => + value + .toISOString() + // allows keeping simple dates like 2023-10-01 without time + .replace('T00:00:00.000Z', '') diff --git a/playground/src/params/number.ts b/playground/src/params/number.ts new file mode 100644 index 000000000..3d70f3913 --- /dev/null +++ b/playground/src/params/number.ts @@ -0,0 +1,14 @@ +// NOTE: should be imported from vue-router +const invalid = (...args: ConstructorParameters) => + new Error(...args) + +export const parse = (value: string): number => { + const asNumber = Number(value) + if (Number.isFinite(asNumber)) { + return asNumber + } + throw invalid(`Expected a number, but received: ${value}`) +} + +// Same as default serializer +export const toString = (value: number): string => String(value) diff --git a/playground/tsconfig.json b/playground/tsconfig.json index 3bfd00252..a2baae089 100644 --- a/playground/tsconfig.json +++ b/playground/tsconfig.json @@ -5,23 +5,16 @@ "./src/**/*.ts", "./src/**/*.vue", "./typed-router.d.ts", - "./auto-imports.d.ts", - "../src" + "./auto-imports.d.ts" ], "compilerOptions": { "baseUrl": ".", "composite": true, "moduleResolution": "Bundler", "paths": { - "@/*": [ - "./src/*" - ], - "unplugin-vue-router/runtime": [ - "../src/runtime.ts" - ], - "unplugin-vue-router/types": [ - "../src/types.ts" - ], + "@/*": ["./src/*"], + "unplugin-vue-router/runtime": ["../src/runtime.ts"], + "unplugin-vue-router/types": ["../src/types.ts"], "unplugin-vue-router/data-loaders": [ "../src/data-loaders/entries/index.ts" ], @@ -30,8 +23,8 @@ ], "unplugin-vue-router/data-loaders/pinia-colada": [ "../src/data-loaders/entries/pinia-colada.ts" - ], - }, + ] + } }, "vueCompilerOptions": { "plugins": [ diff --git a/playground/typed-router.d.ts b/playground/typed-router.d.ts index c4b6e56f2..46d7baf0a 100644 --- a/playground/typed-router.d.ts +++ b/playground/typed-router.d.ts @@ -5,6 +5,10 @@ // It's recommended to commit this file. // Make sure to add this file to your tsconfig.json file as an "includes" or "files" entry. +declare module 'vue-router/auto-resolver' { + export type ParamParserCustom = never +} + declare module 'vue-router/auto-routes' { import type { RouteRecordInfo, diff --git a/playground/vite.config.ts b/playground/vite.config.ts index 6f93f16b4..69d2c7a30 100644 --- a/playground/vite.config.ts +++ b/playground/vite.config.ts @@ -9,6 +9,8 @@ import Vue from '@vitejs/plugin-vue' import AutoImport from 'unplugin-auto-import/vite' import VueDevtools from 'vite-plugin-vue-devtools' +const __dirname = fileURLToPath(new URL('.', import.meta.url)) + export default defineConfig({ clearScreen: false, resolve: { @@ -50,12 +52,14 @@ export default defineConfig({ // getRouteName: getPascalCaseRouteName, experimental: { autoExportsDataLoaders: ['src/loaders/**/*', '@/loaders/**/*'], + paramParsers: false, }, extendRoute(route) { route.params.forEach((param) => { // transform kebab-case to camelCase - param.paramName = param.paramName.replace(/-([a-z])/g, (g) => - g[1].toUpperCase() + param.paramName = param.paramName.replace( + /-([a-z])/g, + (g) => g[1]?.toUpperCase() || '' ) }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7596d5c61..012ef95ba 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,12 @@ importers: .: dependencies: + '@babel/generator': + specifier: ^7.28.3 + version: 7.28.3 '@vue-macros/common': - specifier: 3.0.0 - version: 3.0.0(vue@3.5.21(typescript@5.9.2)) + specifier: ^3.0.0 + version: 3.1.0(vue@3.5.21(typescript@5.9.2)) '@vue/compiler-sfc': specifier: ^3.5.17 version: 3.5.18 @@ -75,9 +78,12 @@ importers: '@tanstack/vue-query': specifier: ^5.89.0 version: 5.89.0(vue@3.5.21(typescript@5.9.2)) + '@types/babel__generator': + specifier: ^7.27.0 + version: 7.27.0 '@types/node': - specifier: ^22.18.6 - version: 22.18.6 + specifier: ^24.3.0 + version: 24.3.0 '@types/picomatch': specifier: ^4.0.2 version: 4.0.2 @@ -128,7 +134,7 @@ importers: version: 6.0.1 rollup: specifier: ^4.52.0 - version: 4.52.0 + version: 4.52.2 semver: specifier: ^7.7.2 version: 7.7.2 @@ -146,34 +152,34 @@ importers: version: 20.1.0(@vueuse/core@12.5.0(typescript@5.9.2)) unplugin-vue-markdown: specifier: ^29.1.0 - version: 29.1.0(vite@7.1.6(@types/node@22.18.6)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1)) + version: 29.1.0(vite@7.1.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1)) unplugin-vue-router: specifier: workspace:* version: 'link:' vite: specifier: ^7.1.6 - version: 7.1.6(@types/node@22.18.6)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1) + version: 7.1.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1) vite-plugin-vue-devtools: specifier: ^8.0.2 - version: 8.0.2(vite@7.1.6(@types/node@22.18.6)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2)) + version: 8.0.2(vite@7.1.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2)) vitepress: specifier: ^1.6.4 - version: 1.6.4(@algolia/client-search@5.20.0)(@types/node@22.18.6)(change-case@5.4.4)(fuse.js@7.1.0)(jwt-decode@4.0.0)(lightningcss@1.29.3)(postcss@8.5.6)(search-insights@2.17.2)(terser@5.40.0)(typescript@5.9.2) + version: 1.6.4(@algolia/client-search@5.20.0)(@types/node@24.3.0)(change-case@5.4.4)(fuse.js@7.1.0)(jwt-decode@4.0.0)(lightningcss@1.29.3)(postcss@8.5.6)(search-insights@2.17.2)(terser@5.43.1)(typescript@5.9.2) vitepress-plugin-llms: specifier: ^1.7.5 version: 1.7.5 vitest: specifier: ^3.2.4 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.6)(@vitest/ui@3.2.4)(happy-dom@18.0.1)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(@vitest/ui@3.2.4)(happy-dom@18.0.1)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1) vue: specifier: ^3.5.21 version: 3.5.21(typescript@5.9.2) vue-router: - specifier: ^4.5.1 - version: 4.5.1(vue@3.5.21(typescript@5.9.2)) + specifier: https://pkg.pr.new/vue-router@4f1a37a + version: https://pkg.pr.new/vue-router@4f1a37a(vue@3.5.21(typescript@5.9.2)) vue-router-mock: specifier: ^2.0.0 - version: 2.0.0(vue-router@4.5.1(vue@3.5.21(typescript@5.9.2)))(vue@3.5.21(typescript@5.9.2)) + version: 2.0.0(vue-router@https://pkg.pr.new/vue-router@4f1a37a(vue@3.5.21(typescript@5.9.2)))(vue@3.5.21(typescript@5.9.2)) vue-tsc: specifier: ^3.0.7 version: 3.0.7(typescript@5.9.2) @@ -200,11 +206,11 @@ importers: specifier: ^0.2.1 version: 0.2.1(@pinia/colada@0.17.1(pinia@3.0.3(typescript@5.9.2)(vue@3.5.21(typescript@5.9.2))))(magicast@0.3.5) '@pinia/nuxt': - specifier: ^0.11.1 - version: 0.11.1(magicast@0.3.5)(pinia@3.0.3(typescript@5.9.2)(vue@3.5.21(typescript@5.9.2))) + specifier: ^0.11.2 + version: 0.11.2(magicast@0.3.5)(pinia@3.0.3(typescript@5.9.2)(vue@3.5.21(typescript@5.9.2))) nuxt: specifier: ^3.17.5 - version: 3.17.6(@netlify/blobs@8.2.0)(@parcel/watcher@2.5.1)(@types/node@22.18.6)(@vue/compiler-sfc@3.5.21)(db0@0.3.2)(encoding@0.1.13)(ioredis@5.6.1)(lightningcss@1.29.3)(magicast@0.3.5)(meow@13.2.0)(rolldown@1.0.0-beta.10-commit.87188ed)(rollup@4.52.0)(terser@5.40.0)(typescript@5.9.2)(vite@7.1.6(@types/node@22.18.6)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1))(vue-tsc@2.2.10(typescript@5.9.2))(yaml@2.8.1) + version: 3.17.6(@netlify/blobs@8.2.0)(@parcel/watcher@2.5.1)(@types/node@24.3.0)(@vue/compiler-sfc@3.5.21)(db0@0.3.2)(encoding@0.1.13)(ioredis@5.6.1)(lightningcss@1.29.3)(magicast@0.3.5)(meow@13.2.0)(rolldown@1.0.0-beta.10-commit.87188ed)(rollup@4.52.2)(terser@5.43.1)(typescript@5.9.2)(vite@7.1.6(@types/node@24.3.0)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1))(vue-tsc@2.2.10(typescript@5.9.2))(yaml@2.8.1) unplugin-vue-router: specifier: workspace:* version: link:../.. @@ -232,7 +238,7 @@ importers: version: 5.89.0(@tanstack/vue-query@5.89.0(vue@3.5.21(typescript@5.9.2)))(vue@3.5.21(typescript@5.9.2)) '@vitejs/plugin-vue': specifier: ^6.0.1 - version: 6.0.1(vite@7.1.6(@types/node@22.18.6)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2)) + version: 6.0.1(vite@7.1.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2)) '@vue/compiler-sfc': specifier: ^3.5.21 version: 3.5.21 @@ -247,7 +253,41 @@ importers: version: link:.. vite: specifier: ^7.1.6 - version: 7.1.6(@types/node@22.18.6)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1) + version: 7.1.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1) + + playground-experimental: + dependencies: + mande: + specifier: ^2.0.9 + version: 2.0.9 + pinia: + specifier: ^3.0.3 + version: 3.0.3(typescript@5.9.2)(vue@3.5.21(typescript@5.9.2)) + vue: + specifier: ^3.5.18 + version: 3.5.21(typescript@5.9.2) + vue-router: + specifier: https://pkg.pr.new/vue-router@4f1a37a + version: https://pkg.pr.new/vue-router@4f1a37a(vue@3.5.21(typescript@5.9.2)) + devDependencies: + '@vitejs/plugin-vue': + specifier: ^6.0.1 + version: 6.0.1(vite@7.1.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2)) + '@vue/compiler-sfc': + specifier: ^3.5.18 + version: 3.5.21 + '@vue/tsconfig': + specifier: ^0.7.0 + version: 0.7.0(typescript@5.9.2)(vue@3.5.21(typescript@5.9.2)) + json-server: + specifier: ^0.17.4 + version: 0.17.4 + unplugin-vue-router: + specifier: workspace:* + version: link:.. + vite: + specifier: ^7.1.2 + version: 7.1.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1) packages: @@ -1410,6 +1450,9 @@ packages: '@jridgewell/gen-mapping@0.3.12': resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==} + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + '@jridgewell/remapping@2.3.5': resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} @@ -1417,6 +1460,9 @@ packages: resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} + '@jridgewell/source-map@0.3.11': + resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} + '@jridgewell/source-map@0.3.6': resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} @@ -1429,6 +1475,9 @@ packages: '@jridgewell/trace-mapping@0.3.29': resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==} + '@jridgewell/trace-mapping@0.3.30': + resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==} + '@kwsites/file-exists@1.1.1': resolution: {integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==} @@ -1530,6 +1579,10 @@ packages: resolution: {integrity: sha512-8PKRwoEF70IXVrpGEJZ4g4V2WtE9RjSMgSZLLa0HZCoyT+QczJcJe3kho/XKnJOnNnHep4WqciTD7p4qRRtBqw==} engines: {node: '>=18.12.0'} + '@nuxt/kit@3.18.1': + resolution: {integrity: sha512-z6w1Fzv27CIKFlhct05rndkJSfoslplWH5fJ9dtusEvpYScLXp5cATWIbWkte9e9zFSmQTgDQJjNs3geQHE7og==} + engines: {node: '>=18.12.0'} + '@nuxt/schema@3.17.6': resolution: {integrity: sha512-ahm0yz6CrSaZ4pS0iuVod9lVRXNDNIidKWLLBx2naGNM6rW+sdFV9gxjvUS3+rLW+swa4HCKE6J5bjOl//oyqQ==} engines: {node: ^14.18.0 || >=16.10.0} @@ -1647,8 +1700,8 @@ packages: '@oxc-project/types@0.75.1': resolution: {integrity: sha512-7ZJy+51qWpZRvynaQUezeYfjCtaSdiXIWFUZIlOuTSfDXpXqnSl/m1IUPLx6XrOy6s0SFv3CLE14vcZy63bz7g==} - '@oxc-project/types@0.89.0': - resolution: {integrity: sha512-yuo+ECPIW5Q9mSeNmCDC2im33bfKuwW18mwkaHMQh8KakHYDzj4ci/q7wxf2qS3dMlVVCIyrs3kFtH5LmnlYnw==} + '@oxc-project/types@0.92.0': + resolution: {integrity: sha512-PDLfCbwgXjGdTBxzcuDOUxJYNBl6P8dOp3eDKWw54dYvqONan9rwGDRQU0zrkdEMiItfXQQUOI17uOcMX5Zm7A==} '@parcel/watcher-android-arm64@2.5.1': resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} @@ -1754,8 +1807,8 @@ packages: pinia: ^2.2.6 || ^3.0.0 vue: ^3.5.17 - '@pinia/nuxt@0.11.1': - resolution: {integrity: sha512-tCD8ioWhhIHKwm8Y9VvyhBAV/kK4W5uGBIYbI5iM4N1t7duOqK6ECBUavrMxMolELayqqMLb9+evegrh3S7s2A==} + '@pinia/nuxt@0.11.2': + resolution: {integrity: sha512-CgvSWpbktxxWBV7ModhAcsExsQZqpPq6vMYEe9DexmmY6959ev8ukL4iFhr/qov2Nb9cQAWd7niFDnaWkN+FHg==} peerDependencies: pinia: ^3.0.3 @@ -1814,8 +1867,8 @@ packages: '@quansync/fs@0.1.5': resolution: {integrity: sha512-lNS9hL2aS2NZgNW7BBj+6EBl4rOf8l+tQ0eRY6JWCI8jI2kc53gSoqbjojU0OnAWhzoXiOjFyGsHcDGePB3lhA==} - '@rolldown/binding-android-arm64@1.0.0-beta.38': - resolution: {integrity: sha512-AE3HFQrjWCKLFZD1Vpiy+qsqTRwwoil1oM5WsKPSmfQ5fif/A+ZtOZetF32erZdsR7qyvns6qHEteEsF6g6rsQ==} + '@rolldown/binding-android-arm64@1.0.0-beta.40': + resolution: {integrity: sha512-9Ii9phC7QU6Lb+ncMfG1Xlosq0NBB1N/4sw+EGZ3y0BBWGy02TOb5ghWZalphAKv9rn1goqo5WkBjyd2YvsLmA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] @@ -1825,8 +1878,8 @@ packages: cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-arm64@1.0.0-beta.38': - resolution: {integrity: sha512-RaoWOKc0rrFsVmKOjQpebMY6c6/I7GR1FBc25v7L/R7NlM0166mUotwGEv7vxu7ruXH4SJcFeVrfADFUUXUmmQ==} + '@rolldown/binding-darwin-arm64@1.0.0-beta.40': + resolution: {integrity: sha512-5O6d0y2tBQTL+ecQY3qXIwSnF1/Zik8q7LZMKeyF+VJ9l194d0IdMhl2zUF0cqWbYHuF4Pnxplk4OhurPQ/Z9Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] @@ -1836,8 +1889,8 @@ packages: cpu: [x64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.0-beta.38': - resolution: {integrity: sha512-Ymojqc2U35iUc8NFU2XX1WQPfBRRHN6xHcrxAf9WS8BFFBn8pDrH5QPvH1tYs3lDkw6UGGbanr1RGzARqdUp1g==} + '@rolldown/binding-darwin-x64@1.0.0-beta.40': + resolution: {integrity: sha512-izB9jygt3miPQbOTZfSu5K51isUplqa8ysByOKQqcJHgrBWmbTU8TM9eouv6tRmBR0kjcEcID9xhmA1CeZ1VIg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] @@ -1847,8 +1900,8 @@ packages: cpu: [x64] os: [freebsd] - '@rolldown/binding-freebsd-x64@1.0.0-beta.38': - resolution: {integrity: sha512-0ermTQ//WzSI0nOL3z/LUWMNiE9xeM5cLGxjewPFEexqxV/0uM8/lNp9QageQ8jfc/VO1OURsGw34HYO5PaL8w==} + '@rolldown/binding-freebsd-x64@1.0.0-beta.40': + resolution: {integrity: sha512-2fdpEpKT+wwP0vig9dqxu+toTeWmVSjo3psJQVDeLJ51rO+GXcCJ1IkCXjhMKVEevNtZS7B8T8Z2vvmRV9MAdA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] @@ -1858,8 +1911,8 @@ packages: cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.38': - resolution: {integrity: sha512-GADxzVUTCTp6EWI52831A29Tt7PukFe94nhg/SUsfkI33oTiNQtPxyLIT/3oRegizGuPSZSlrdBurkjDwxyEUQ==} + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.40': + resolution: {integrity: sha512-HP2lo78OWULN+8TewpLbS9PS00jh0CaF04tA2u8z2I+6QgVgrYOYKvX+T0hlO5smgso4+qb3YchzumWJl3yCPQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] @@ -1869,8 +1922,8 @@ packages: cpu: [arm64] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.38': - resolution: {integrity: sha512-SKO7Exl5Yem/OSNoA5uLHzyrptUQ8Hg70kHDxuwEaH0+GUg+SQe9/7PWmc4hFKBMrJGdQtii8WZ0uIz9Dofg5Q==} + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.40': + resolution: {integrity: sha512-ng00gfr9BhA2NPAOU5RWAlTiL+JcwAD+L+4yUD1sbBy6tgHdLiNBOvKtHISIF9RM9/eQeS0tAiWOYZGIH9JMew==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] @@ -1880,8 +1933,8 @@ packages: cpu: [arm64] os: [linux] - '@rolldown/binding-linux-arm64-musl@1.0.0-beta.38': - resolution: {integrity: sha512-SOo6+WqhXPBaShLxLT0eCgH17d3Yu1lMAe4mFP0M9Bvr/kfMSOPQXuLxBcbBU9IFM9w3N6qP9xWOHO+oUJvi8Q==} + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.40': + resolution: {integrity: sha512-mF0R1l9kLcaag/9cLEiYYdNZ4v1uuX4jklSDZ1s6vJE4RB3LirUney0FavdVRwCJ5sDvfvsPgXgtBXWYr2M2tQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] @@ -1891,8 +1944,8 @@ packages: cpu: [x64] os: [linux] - '@rolldown/binding-linux-x64-gnu@1.0.0-beta.38': - resolution: {integrity: sha512-yvsQ3CyrodOX+lcoi+lejZGCOvJZa9xTsNB8OzpMDmHeZq3QzJfpYjXSAS6vie70fOkLVJb77UqYO193Cl8XBQ==} + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.40': + resolution: {integrity: sha512-+wi08S7wT5iLPHRZb0USrS6n+T6m+yY++dePYedE5uvKIpWCJJioFTaRtWjpm0V6dVNLcq2OukrvfdlGtH9Wgg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] @@ -1902,14 +1955,14 @@ packages: cpu: [x64] os: [linux] - '@rolldown/binding-linux-x64-musl@1.0.0-beta.38': - resolution: {integrity: sha512-84qzKMwUwikfYeOuJ4Kxm/3z15rt0nFGGQArHYIQQNSTiQdxGHxOkqXtzPFqrVfBJUdxBAf+jYzR1pttFJuWyg==} + '@rolldown/binding-linux-x64-musl@1.0.0-beta.40': + resolution: {integrity: sha512-W5qBGAemUocIBKCcOsDjlV9GUt28qhl/+M6etWBeLS5gQK0J6XDg0YVzfOQdvq57ZGjYNP0NvhYzqhOOnEx+4g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@rolldown/binding-openharmony-arm64@1.0.0-beta.38': - resolution: {integrity: sha512-QrNiWlce01DYH0rL8K3yUBu+lNzY+B0DyCbIc2Atan6/S6flxOL0ow5DLQvMamOI/oKhrJ4xG+9MkMb9dDHbLQ==} + '@rolldown/binding-openharmony-arm64@1.0.0-beta.40': + resolution: {integrity: sha512-vJwoDehtt+yqj2zacq1AqNc2uE/oh7mnRGqAUbuldV6pgvU01OSQUJ7Zu+35hTopnjFoDNN6mIezkYlGAv5RFA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] @@ -1919,8 +1972,8 @@ packages: engines: {node: '>=14.21.3'} cpu: [wasm32] - '@rolldown/binding-wasm32-wasi@1.0.0-beta.38': - resolution: {integrity: sha512-fnLtHyjwEsG4/aNV3Uv3Qd1ZbdH+CopwJNoV0RgBqrcQB8V6/Qdikd5JKvnO23kb3QvIpP+dAMGZMv1c2PJMzw==} + '@rolldown/binding-wasm32-wasi@1.0.0-beta.40': + resolution: {integrity: sha512-Oj3YyqVUPurr1FlMpEE/bJmMC+VWAWPM/SGUfklO5KUX97bk5Q/733nPg4RykK8q8/TluJoQYvRc05vL/B74dw==} engines: {node: '>=14.0.0'} cpu: [wasm32] @@ -1929,8 +1982,8 @@ packages: cpu: [arm64] os: [win32] - '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.38': - resolution: {integrity: sha512-19cTfnGedem+RY+znA9J6ARBOCEFD4YSjnx0p5jiTm9tR6pHafRfFIfKlTXhun+NL0WWM/M0eb2IfPPYUa8+wg==} + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.40': + resolution: {integrity: sha512-0ZtO6yN8XjVoFfN4HDWQj4nDu3ndMybr7jIM00DJqOmc+yFhly7rdOy7fNR9Sky3leCpBtsXfepVqRmVpYKPVA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] @@ -1940,8 +1993,8 @@ packages: cpu: [ia32] os: [win32] - '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.38': - resolution: {integrity: sha512-HcICm4YzFJZV+fI0O0bFLVVlsWvRNo/AB9EfUXvNYbtAxakCnQZ15oq22deFdz6sfi9Y4/SagH2kPU723dhCFA==} + '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.40': + resolution: {integrity: sha512-BPl1inoJXPpIe38Ja46E4y11vXlJyuleo+9Rmu//pYL5fIDYJkXUj/oAXqjSuwLcssrcwnuPgzvzvlz9++cr3w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] @@ -1951,8 +2004,8 @@ packages: cpu: [x64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.0-beta.38': - resolution: {integrity: sha512-4Qx6cgEPXLb0XsCyLoQcUgYBpfL0sjugftob+zhUH0EOk/NVCAIT+h0NJhY+jn7pFpeKxhNMqhvTNx3AesxIAQ==} + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.40': + resolution: {integrity: sha512-UguA4ltbAk+nbwHRxqaUP/etpTbR0HjyNlsu4Zjbh/ytNbFsbw8CA4tEBkwDyjgI5NIPea6xY11zpl7R2/ddVA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -1963,8 +2016,11 @@ packages: '@rolldown/pluginutils@1.0.0-beta.29': resolution: {integrity: sha512-NIJgOsMjbxAXvoGq/X0gD7VPMQ8j9g0BiDaNjVNVjvl+iKXxL3Jre0v31RmBYeLEmkbj2s02v8vFTbUXi5XS2Q==} - '@rolldown/pluginutils@1.0.0-beta.38': - resolution: {integrity: sha512-N/ICGKleNhA5nc9XXQG/kkKHJ7S55u0x0XUJbbkmdCnFuoRkM1Il12q9q0eX19+M7KKUEPw/daUPIRnxhcxAIw==} + '@rolldown/pluginutils@1.0.0-beta.33': + resolution: {integrity: sha512-she25NCG6NoEPC/SEB4pHs5STcnfI4VBFOzjeI63maSPrWME5J2XC8ogrBgp8NaE/xzj28/kbpSaebiMvFRj+w==} + + '@rolldown/pluginutils@1.0.0-beta.40': + resolution: {integrity: sha512-s3GeJKSQOwBlzdUrj4ISjJj5SfSh+aqn0wjOar4Bx95iV1ETI7F6S/5hLcfAxZ9kXDcyrAkxPlqmd1ZITttf+w==} '@rollup/plugin-alias@5.1.1': resolution: {integrity: sha512-PR9zDb+rOzkRb2VD+EuKB7UC41vU5DIwZ5qqCpk0KJudcWAyi8rvYOhS7+L5aZCspw1stTViLgN5v6FF1p5cgQ==} @@ -2038,113 +2094,113 @@ packages: rollup: optional: true - '@rollup/rollup-android-arm-eabi@4.52.0': - resolution: {integrity: sha512-VxDYCDqOaR7NXzAtvRx7G1u54d2kEHopb28YH/pKzY6y0qmogP3gG7CSiWsq9WvDFxOQMpNEyjVAHZFXfH3o/A==} + '@rollup/rollup-android-arm-eabi@4.52.2': + resolution: {integrity: sha512-o3pcKzJgSGt4d74lSZ+OCnHwkKBeAbFDmbEm5gg70eA8VkyCuC/zV9TwBnmw6VjDlRdF4Pshfb+WE9E6XY1PoQ==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.52.0': - resolution: {integrity: sha512-pqDirm8koABIKvzL59YI9W9DWbRlTX7RWhN+auR8HXJxo89m4mjqbah7nJZjeKNTNYopqL+yGg+0mhCpf3xZtQ==} + '@rollup/rollup-android-arm64@4.52.2': + resolution: {integrity: sha512-cqFSWO5tX2vhC9hJTK8WAiPIm4Q8q/cU8j2HQA0L3E1uXvBYbOZMhE2oFL8n2pKB5sOCHY6bBuHaRwG7TkfJyw==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.52.0': - resolution: {integrity: sha512-YCdWlY/8ltN6H78HnMsRHYlPiKvqKagBP1r+D7SSylxX+HnsgXGCmLiV3Y4nSyY9hW8qr8U9LDUx/Lo7M6MfmQ==} + '@rollup/rollup-darwin-arm64@4.52.2': + resolution: {integrity: sha512-vngduywkkv8Fkh3wIZf5nFPXzWsNsVu1kvtLETWxTFf/5opZmflgVSeLgdHR56RQh71xhPhWoOkEBvbehwTlVA==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.52.0': - resolution: {integrity: sha512-z4nw6y1j+OOSGzuVbSWdIp1IUks9qNw4dc7z7lWuWDKojY38VMWBlEN7F9jk5UXOkUcp97vA1N213DF+Lz8BRg==} + '@rollup/rollup-darwin-x64@4.52.2': + resolution: {integrity: sha512-h11KikYrUCYTrDj6h939hhMNlqU2fo/X4NB0OZcys3fya49o1hmFaczAiJWVAFgrM1NCP6RrO7lQKeVYSKBPSQ==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.52.0': - resolution: {integrity: sha512-Q/dv9Yvyr5rKlK8WQJZVrp5g2SOYeZUs9u/t2f9cQ2E0gJjYB/BWoedXfUT0EcDJefi2zzVfhcOj8drWCzTviw==} + '@rollup/rollup-freebsd-arm64@4.52.2': + resolution: {integrity: sha512-/eg4CI61ZUkLXxMHyVlmlGrSQZ34xqWlZNW43IAU4RmdzWEx0mQJ2mN/Cx4IHLVZFL6UBGAh+/GXhgvGb+nVxw==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.52.0': - resolution: {integrity: sha512-kdBsLs4Uile/fbjZVvCRcKB4q64R+1mUq0Yd7oU1CMm1Av336ajIFqNFovByipciuUQjBCPMxwJhCgfG2re3rg==} + '@rollup/rollup-freebsd-x64@4.52.2': + resolution: {integrity: sha512-QOWgFH5X9+p+S1NAfOqc0z8qEpJIoUHf7OWjNUGOeW18Mx22lAUOiA9b6r2/vpzLdfxi/f+VWsYjUOMCcYh0Ng==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.52.0': - resolution: {integrity: sha512-aL6hRwu0k7MTUESgkg7QHY6CoqPgr6gdQXRJI1/VbFlUMwsSzPGSR7sG5d+MCbYnJmJwThc2ol3nixj1fvI/zQ==} + '@rollup/rollup-linux-arm-gnueabihf@4.52.2': + resolution: {integrity: sha512-kDWSPafToDd8LcBYd1t5jw7bD5Ojcu12S3uT372e5HKPzQt532vW+rGFFOaiR0opxePyUkHrwz8iWYEyH1IIQA==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.52.0': - resolution: {integrity: sha512-BTs0M5s1EJejgIBJhCeiFo7GZZ2IXWkFGcyZhxX4+8usnIo5Mti57108vjXFIQmmJaRyDwmV59Tw64Ap1dkwMw==} + '@rollup/rollup-linux-arm-musleabihf@4.52.2': + resolution: {integrity: sha512-gKm7Mk9wCv6/rkzwCiUC4KnevYhlf8ztBrDRT9g/u//1fZLapSRc+eDZj2Eu2wpJ+0RzUKgtNijnVIB4ZxyL+w==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.52.0': - resolution: {integrity: sha512-uj672IVOU9m08DBGvoPKPi/J8jlVgjh12C9GmjjBxCTQc3XtVmRkRKyeHSmIKQpvJ7fIm1EJieBUcnGSzDVFyw==} + '@rollup/rollup-linux-arm64-gnu@4.52.2': + resolution: {integrity: sha512-66lA8vnj5mB/rtDNwPgrrKUOtCLVQypkyDa2gMfOefXK6rcZAxKLO9Fy3GkW8VkPnENv9hBkNOFfGLf6rNKGUg==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.52.0': - resolution: {integrity: sha512-/+IVbeDMDCtB/HP/wiWsSzduD10SEGzIZX2945KSgZRNi4TSkjHqRJtNTVtVb8IRwhJ65ssI56krlLik+zFWkw==} + '@rollup/rollup-linux-arm64-musl@4.52.2': + resolution: {integrity: sha512-s+OPucLNdJHvuZHuIz2WwncJ+SfWHFEmlC5nKMUgAelUeBUnlB4wt7rXWiyG4Zn07uY2Dd+SGyVa9oyLkVGOjA==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loong64-gnu@4.52.0': - resolution: {integrity: sha512-U1vVzvSWtSMWKKrGoROPBXMh3Vwn93TA9V35PldokHGqiUbF6erSzox/5qrSMKp6SzakvyjcPiVF8yB1xKr9Pg==} + '@rollup/rollup-linux-loong64-gnu@4.52.2': + resolution: {integrity: sha512-8wTRM3+gVMDLLDdaT6tKmOE3lJyRy9NpJUS/ZRWmLCmOPIJhVyXwjBo+XbrrwtV33Em1/eCTd5TuGJm4+DmYjw==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-ppc64-gnu@4.52.0': - resolution: {integrity: sha512-X/4WfuBAdQRH8cK3DYl8zC00XEE6aM472W+QCycpQJeLWVnHfkv7RyBFVaTqNUMsTgIX8ihMjCvFF9OUgeABzw==} + '@rollup/rollup-linux-ppc64-gnu@4.52.2': + resolution: {integrity: sha512-6yqEfgJ1anIeuP2P/zhtfBlDpXUb80t8DpbYwXQ3bQd95JMvUaqiX+fKqYqUwZXqdJDd8xdilNtsHM2N0cFm6A==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.52.0': - resolution: {integrity: sha512-xIRYc58HfWDBZoLmWfWXg2Sq8VCa2iJ32B7mqfWnkx5mekekl0tMe7FHpY8I72RXEcUkaWawRvl3qA55og+cwQ==} + '@rollup/rollup-linux-riscv64-gnu@4.52.2': + resolution: {integrity: sha512-sshYUiYVSEI2B6dp4jMncwxbrUqRdNApF2c3bhtLAU0qA8Lrri0p0NauOsTWh3yCCCDyBOjESHMExonp7Nzc0w==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-riscv64-musl@4.52.0': - resolution: {integrity: sha512-mbsoUey05WJIOz8U1WzNdf+6UMYGwE3fZZnQqsM22FZ3wh1N887HT6jAOjXs6CNEK3Ntu2OBsyQDXfIjouI4dw==} + '@rollup/rollup-linux-riscv64-musl@4.52.2': + resolution: {integrity: sha512-duBLgd+3pqC4MMwBrKkFxaZerUxZcYApQVC5SdbF5/e/589GwVvlRUnyqMFbM8iUSb1BaoX/3fRL7hB9m2Pj8Q==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.52.0': - resolution: {integrity: sha512-qP6aP970bucEi5KKKR4AuPFd8aTx9EF6BvutvYxmZuWLJHmnq4LvBfp0U+yFDMGwJ+AIJEH5sIP+SNypauMWzg==} + '@rollup/rollup-linux-s390x-gnu@4.52.2': + resolution: {integrity: sha512-tzhYJJidDUVGMgVyE+PmxENPHlvvqm1KILjjZhB8/xHYqAGeizh3GBGf9u6WdJpZrz1aCpIIHG0LgJgH9rVjHQ==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.52.0': - resolution: {integrity: sha512-nmSVN+F2i1yKZ7rJNKO3G7ZzmxJgoQBQZ/6c4MuS553Grmr7WqR7LLDcYG53Z2m9409z3JLt4sCOhLdbKQ3HmA==} + '@rollup/rollup-linux-x64-gnu@4.52.2': + resolution: {integrity: sha512-opH8GSUuVcCSSyHHcl5hELrmnk4waZoVpgn/4FDao9iyE4WpQhyWJ5ryl5M3ocp4qkRuHfyXnGqg8M9oKCEKRA==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.52.0': - resolution: {integrity: sha512-2d0qRo33G6TfQVjaMR71P+yJVGODrt5V6+T0BDYH4EMfGgdC/2HWDVjSSFw888GSzAZUwuska3+zxNUCDco6rQ==} + '@rollup/rollup-linux-x64-musl@4.52.2': + resolution: {integrity: sha512-LSeBHnGli1pPKVJ79ZVJgeZWWZXkEe/5o8kcn23M8eMKCUANejchJbF/JqzM4RRjOJfNRhKJk8FuqL1GKjF5oQ==} cpu: [x64] os: [linux] - '@rollup/rollup-openharmony-arm64@4.52.0': - resolution: {integrity: sha512-A1JalX4MOaFAAyGgpO7XP5khquv/7xKzLIyLmhNrbiCxWpMlnsTYr8dnsWM7sEeotNmxvSOEL7F65j0HXFcFsw==} + '@rollup/rollup-openharmony-arm64@4.52.2': + resolution: {integrity: sha512-uPj7MQ6/s+/GOpolavm6BPo+6CbhbKYyZHUDvZ/SmJM7pfDBgdGisFX3bY/CBDMg2ZO4utfhlApkSfZ92yXw7Q==} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.52.0': - resolution: {integrity: sha512-YQugafP/rH0eOOHGjmNgDURrpYHrIX0yuojOI8bwCyXwxC9ZdTd3vYkmddPX0oHONLXu9Rb1dDmT0VNpjkzGGw==} + '@rollup/rollup-win32-arm64-msvc@4.52.2': + resolution: {integrity: sha512-Z9MUCrSgIaUeeHAiNkm3cQyst2UhzjPraR3gYYfOjAuZI7tcFRTOD+4cHLPoS/3qinchth+V56vtqz1Tv+6KPA==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.52.0': - resolution: {integrity: sha512-zYdUYhi3Qe2fndujBqL5FjAFzvNeLxtIqfzNEVKD1I7C37/chv1VxhscWSQHTNfjPCrBFQMnynwA3kpZpZ8w4A==} + '@rollup/rollup-win32-ia32-msvc@4.52.2': + resolution: {integrity: sha512-+GnYBmpjldD3XQd+HMejo+0gJGwYIOfFeoBQv32xF/RUIvccUz20/V6Otdv+57NE70D5pa8W/jVGDoGq0oON4A==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.52.0': - resolution: {integrity: sha512-fGk03kQylNaCOQ96HDMeT7E2n91EqvCDd3RwvT5k+xNdFCeMGnj5b5hEgTGrQuyidqSsD3zJDQ21QIaxXqTBJw==} + '@rollup/rollup-win32-x64-gnu@4.52.2': + resolution: {integrity: sha512-ApXFKluSB6kDQkAqZOKXBjiaqdF1BlKi+/eqnYe9Ee7U2K3pUDKsIyr8EYm/QDHTJIM+4X+lI0gJc3TTRhd+dA==} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.52.0': - resolution: {integrity: sha512-6iKDCVSIUQ8jPMoIV0OytRKniaYyy5EbY/RRydmLW8ZR3cEBhxbWl5ro0rkUNe0ef6sScvhbY79HrjRm8i3vDQ==} + '@rollup/rollup-win32-x64-msvc@4.52.2': + resolution: {integrity: sha512-ARz+Bs8kY6FtitYM96PqPEVvPXqEZmPZsSkXvyX19YzDqkCaIlhCieLLMI5hxO9SRZ2XtCtm8wxhy0iJ2jxNfw==} cpu: [x64] os: [win32] @@ -2263,6 +2319,9 @@ packages: '@tybys/wasm-util@0.9.0': resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==} + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + '@types/chai@5.2.2': resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} @@ -2302,11 +2361,11 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} - '@types/node@20.19.4': - resolution: {integrity: sha512-OP+We5WV8Xnbuvw0zC2m4qfB/BJvjyCwtNjhHdJxV1639SGSKrLmJkc3fMnp2Qy8nJyHp8RO6umxELN/dS1/EA==} + '@types/node@20.19.11': + resolution: {integrity: sha512-uug3FEEGv0r+jrecvUUpbY8lLisvIjg6AAic6a2bSP5OEOLeJsDSnvhCDov7ipFFMXS3orMpzlmi0ZcuGkBbow==} - '@types/node@22.18.6': - resolution: {integrity: sha512-r8uszLPpeIWbNKtvWRt/DbVi5zbqZyj1PTmhRMqBMvDnaz1QpmSKujUtJLrqGZeoM8v72MfYggDceY4K1itzWQ==} + '@types/node@24.3.0': + resolution: {integrity: sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==} '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} @@ -2459,18 +2518,18 @@ packages: '@volar/typescript@2.4.23': resolution: {integrity: sha512-lAB5zJghWxVPqfcStmAP1ZqQacMpe90UrP5RJ3arDyrhy4aCUQqmxPPLB2PWDKugvylmO41ljK7vZ+t6INMTag==} - '@vue-macros/common@3.0.0': - resolution: {integrity: sha512-WwzEv0Ky/HSGR/oZnh+pHJi8yvxW83XRQGIsKsn6t5MiF+KsHoMPuqJdm1VoGNJAuTqJtd1ZGSznnZKPMSKC8w==} - engines: {node: '>=20.19.0'} + '@vue-macros/common@3.0.0-beta.15': + resolution: {integrity: sha512-DMgq/rIh1H20WYNWU7krIbEfJRYDDhy7ix64GlT4AVUJZZWCZ5pxiYVJR3A3GmWQPkn7Pg7i3oIiGqu4JGC65w==} + engines: {node: '>=20.18.0'} peerDependencies: vue: ^2.7.0 || ^3.2.25 peerDependenciesMeta: vue: optional: true - '@vue-macros/common@3.0.0-beta.15': - resolution: {integrity: sha512-DMgq/rIh1H20WYNWU7krIbEfJRYDDhy7ix64GlT4AVUJZZWCZ5pxiYVJR3A3GmWQPkn7Pg7i3oIiGqu4JGC65w==} - engines: {node: '>=20.18.0'} + '@vue-macros/common@3.1.0': + resolution: {integrity: sha512-YGpEYYfFkjQY0ZbtOX90KuxRs0JHLsMizxvArzemn4Uwc0X7iLIs/+8RETgnQgGyBenJU5CZW5qVGiedKA3j2g==} + engines: {node: '>=20.19.0'} peerDependencies: vue: ^2.7.0 || ^3.2.25 peerDependenciesMeta: @@ -2526,8 +2585,8 @@ packages: '@vue/devtools-api@7.7.6': resolution: {integrity: sha512-b2Xx0KvXZObePpXPYHvBRRJLDQn5nhKjXh7vUhMEtWxz1AYNFOVIsh5+HLP8xDGL7sy+Q7hXeUxPHB/KgbtsPw==} - '@vue/devtools-api@8.0.1': - resolution: {integrity: sha512-YBvjfpM7LEp5+b7ZDm4+mFrC+TgGjUmN8ff9lZcbHQ1MKhmftT/urCTZP0y1j26YQWr25l9TPaEbNLbILRiGoQ==} + '@vue/devtools-api@8.0.2': + resolution: {integrity: sha512-RdwsaYoSTumwZ7XOt5yIPP1/T4O0bTs+c5XaEjmUB6f9x+FvDSL9AekxW1vuhK1lmA9TfewpXVt2r5LIax3LHw==} '@vue/devtools-core@7.7.7': resolution: {integrity: sha512-9z9TLbfC+AjAi1PQyWX+OErjIaJmdFlbDHcD+cAMYKY6Bh5VlsAtCeGyRMrXwIlMEQPukvnWt3gZBLwTAIMKzQ==} @@ -2542,18 +2601,12 @@ packages: '@vue/devtools-kit@7.7.7': resolution: {integrity: sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA==} - '@vue/devtools-kit@8.0.1': - resolution: {integrity: sha512-7kiPhgTKNtNeXltEHnJJjIDlndlJP4P+UJvCw54uVHNDlI6JzwrSiRmW4cxKTug2wDbc/dkGaMnlZghcwV+aWA==} - '@vue/devtools-kit@8.0.2': resolution: {integrity: sha512-yjZKdEmhJzQqbOh4KFBfTOQjDPMrjjBNCnHBvnTGJX+YLAqoUtY2J+cg7BE+EA8KUv8LprECq04ts75wCoIGWA==} '@vue/devtools-shared@7.7.7': resolution: {integrity: sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==} - '@vue/devtools-shared@8.0.1': - resolution: {integrity: sha512-PqtWqPPRpMwZ9FjTzyugb5KeV9kmg2C3hjxZHwjl0lijT4QIJDd0z6AWcnbM9w2nayjDymyTt0+sbdTv3pVeNg==} - '@vue/devtools-shared@8.0.2': resolution: {integrity: sha512-mLU0QVdy5Lp40PMGSixDw/Kbd6v5dkQXltd2r+mdVQV7iUog2NlZuLxFZApFZ/mObUBDhoCpf0T3zF2FWWdeHw==} @@ -2604,15 +2657,23 @@ packages: '@vue/shared@3.5.18': resolution: {integrity: sha512-cZy8Dq+uuIXbxCZpuLd2GJdeSO/lIzIspC2WtkqIpje5QyFbvLaI5wZtdUjLHjGZrlVX6GilejatWwVYYRc8tA==} - '@vue/shared@3.5.19': - resolution: {integrity: sha512-IhXCOn08wgKrLQxRFKKlSacWg4Goi1BolrdEeLYn6tgHjJNXVrWJ5nzoxZqNwl5p88aLlQ8LOaoMa3AYvaKJ/Q==} - '@vue/shared@3.5.21': resolution: {integrity: sha512-+2k1EQpnYuVuu3N7atWyG3/xoFWIVJZq4Mz8XNOdScFI0etES75fbny/oU4lKWk/577P1zmg0ioYvpGEDZ3DLw==} '@vue/test-utils@2.4.6': resolution: {integrity: sha512-FMxEjOpYNYiFe0GkaHsnJPXFHxQ6m4t8vI/ElPGpMWxZKpmRvQ33OIrvRXemy6yha03RxhOlQuy+gZMC3CQSow==} + '@vue/tsconfig@0.7.0': + resolution: {integrity: sha512-ku2uNz5MaZ9IerPPUyOHzyjhXoX2kVJaVf7hL315DC17vS6IiZRmmCPfggNbU16QTvM80+uYYy3eYJB59WCtvg==} + peerDependencies: + typescript: 5.x + vue: ^3.4.0 + peerDependenciesMeta: + typescript: + optional: true + vue: + optional: true + '@vue/tsconfig@0.8.1': resolution: {integrity: sha512-aK7feIWPXFSUhsCP9PFqPyFOcz4ENkb8hZ2pneL6m2UjCkccvaOhC/5KCKluuBufvp2KzkbdA2W2pk20vLzu3g==} peerDependencies: @@ -2936,8 +2997,8 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.25.0: - resolution: {integrity: sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==} + browserslist@4.25.2: + resolution: {integrity: sha512-0si2SJK3ooGzIawRu61ZdPCO1IncZwS8IzuX73sPZsXW6EQ/w/DAfPyKI8l1ETTCr2MnvqWitmlCUxgdul45jA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -2978,6 +3039,14 @@ packages: magicast: optional: true + c12@3.2.0: + resolution: {integrity: sha512-ixkEtbYafL56E6HiFuonMm1ZjoKtIo7TH68/uiEq4DAwv9NcUX2nJ95F8TrbMeNjqIkZpruo3ojXQJ+MGG5gcQ==} + peerDependencies: + magicast: ^0.3.5 + peerDependenciesMeta: + magicast: + optional: true + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -2999,6 +3068,9 @@ packages: caniuse-lite@1.0.30001720: resolution: {integrity: sha512-Ec/2yV2nNPwb4DnTANEV99ZWwm3ZWfdlfkQbWSDDt+PsXEVYwlhPH8tdMaPunYTKKmz7AnHi2oNEi1GcmKCD8g==} + caniuse-lite@1.0.30001735: + resolution: {integrity: sha512-EV/laoX7Wq2J9TQlyIXRxTJqIw4sxfXS4OYgudGxBYRuTv0q7AM6yMEpU/Vo1I94thg9U6EZ2NfZx9GJq83u7w==} + ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -3056,9 +3128,9 @@ packages: resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} engines: {node: '>=18'} - cli-truncate@4.0.0: - resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==} - engines: {node: '>=18'} + cli-truncate@5.1.0: + resolution: {integrity: sha512-7JDGG+4Zp0CsknDCedl0DYdaeOhc46QNpXi3NLQblkZpXXgA6LncLDUUyvrjSvZeF3VRQa+KiMGomazQrC1V8g==} + engines: {node: '>=20'} clipboardy@4.0.0: resolution: {integrity: sha512-5mOlNS0mhX0707P2I0aZ2V/cmHUEO/fL7VFLqszkhUsxt7RwnmrInf/eEQKlf5GzvYeHIjT+Ov1HRfNmymlG0w==} @@ -3556,6 +3628,10 @@ packages: resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==} engines: {node: '>=12'} + dotenv@17.2.1: + resolution: {integrity: sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==} + engines: {node: '>=12'} + dts-resolver@2.1.2: resolution: {integrity: sha512-xeXHBQkn2ISSXxbJWD828PFjtyg+/UrMDo7W4Ffcs7+YWCquxU8YjV1KoxuiL+eJ5pg3ll+bC6flVv61L3LKZg==} engines: {node: '>=20.18.0'} @@ -3583,8 +3659,8 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - electron-to-chromium@1.5.161: - resolution: {integrity: sha512-hwtetwfKNZo/UlwHIVBlKZVdy7o8bIZxxKs0Mv/ROPiQQQmDgdm5a+KvKtBsxM8ZjFzTaCeLoodZ8jiBE3o9rA==} + electron-to-chromium@1.5.202: + resolution: {integrity: sha512-NxbYjRmiHcHXV1Ws3fWUW+SLb62isauajk45LUJ/HgIOkUA7jLZu/X2Iif+X9FBNK8QkF9Zb4Q2mcwXCcY30mg==} emoji-regex-xs@1.0.0: resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==} @@ -3619,8 +3695,8 @@ packages: end-of-stream@1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} - enhanced-resolve@5.18.1: - resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} + enhanced-resolve@5.18.3: + resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} engines: {node: '>=10.13.0'} entities@4.5.0: @@ -3816,6 +3892,14 @@ packages: fd-slicer@1.1.0: resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + fdir@6.4.6: + resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -4222,10 +4306,6 @@ packages: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} - is-fullwidth-code-point@4.0.0: - resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} - engines: {node: '>=12'} - is-fullwidth-code-point@5.0.0: resolution: {integrity: sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==} engines: {node: '>=18'} @@ -4537,8 +4617,8 @@ packages: resolution: {integrity: sha512-I8oW2+QL5KJo8zXNWX046M134WchxsXC7SawLPvRQpogCbkyQIaFxPE89A2HiwR7vAK2Dm2ERBAmyjTYGYEpBg==} hasBin: true - listr2@9.0.3: - resolution: {integrity: sha512-0aeh5HHHgmq1KRdMMDHfhMWQmIT/m7nRDTlxlFqni2Sp0had9baqsjJRvDGdlvgd6NmPE0nPloOipiQJGFtTHQ==} + listr2@9.0.4: + resolution: {integrity: sha512-1wd/kpAdKRLwv7/3OKC8zZ5U8e/fajCfWMxacUvB79S5nLrYGPtUI/8chMQhn3LQjsRVErTb9i1ECAwW0ZIHnQ==} engines: {node: '>=20.0.0'} loader-runner@4.3.0: @@ -4630,9 +4710,6 @@ packages: magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} - magic-string@0.30.18: - resolution: {integrity: sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ==} - magic-string@0.30.19: resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} @@ -5543,8 +5620,8 @@ packages: proto-list@1.2.4: resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} - protobufjs@7.5.0: - resolution: {integrity: sha512-Z2E/kOY1QjoMlCytmexzYfDm/w5fKAiRwpSzGtdnXW1zC88Z2yXazHHrOtwCzn+7wSxyE8PYM4rvVcMphF9sOA==} + protobufjs@7.5.3: + resolution: {integrity: sha512-sildjKwVqOI2kmFDiXQ6aEB0fjYTafpEvIBs8tOR8qI4spuL9OPROLVu2qZqi/xgCfsHIwVqlaF8JBjWFHnKbw==} engines: {node: '>=12.0.0'} protocols@2.0.2: @@ -5715,8 +5792,8 @@ packages: engines: {node: 20 || >=22} hasBin: true - rolldown-plugin-dts@0.16.7: - resolution: {integrity: sha512-9iDzS4MHXMyieisFbWxuz96i/idGJNpvWILqCH06mrEZvn8Q2el3Q63xxjOt7HJjTOUNFhB1isvZFy4dA87lPQ==} + rolldown-plugin-dts@0.16.8: + resolution: {integrity: sha512-lsx7yrYA0ZXfARLEcPKgHIw8DX4fLQOhmMChgZbn5eFhqibY2Bav1+/Yn5WNm+ATtw+cefXYgEYO/7njeHsgAA==} engines: {node: '>=20.18.0'} peerDependencies: '@ts-macro/tsc': ^0.3.6 @@ -5738,8 +5815,8 @@ packages: resolution: {integrity: sha512-D+iim+DHIwK9kbZvubENmtnYFqHfFV0OKwzT8yU/W+xyUK1A71+iRFmJYBGqNUo3fJ2Ob4oIQfan63mhzh614A==} hasBin: true - rolldown@1.0.0-beta.38: - resolution: {integrity: sha512-58frPNX55Je1YsyrtPJv9rOSR3G5efUZpRqok94Efsj0EUa8dnqJV3BldShyI7A+bVPleucOtzXHwVpJRcR0kQ==} + rolldown@1.0.0-beta.40: + resolution: {integrity: sha512-VqEHbKpOgTPmQrZ4fVn4eshDQS/6g/fRpNE7cFSJY+eQLDZn4B9X61J6L+hnlt1u2uRI+pF7r1USs6S5fuWCvw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -5756,8 +5833,8 @@ packages: rollup: optional: true - rollup@4.52.0: - resolution: {integrity: sha512-+IuescNkTJQgX7AkIDtITipZdIGcWF0pnVvZTWStiazUmcGA2ag8dfg0urest2XlXUi9kuhfQ+qmdc5Stc3z7g==} + rollup@4.52.2: + resolution: {integrity: sha512-I25/2QgoROE1vYV+NQ1En9T9UFB9Cmfm2CJ83zZOlaDpvz29wGQSZXWKw7MiNXau7wYgB/T9fVIdIuEQ+KbiiA==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -5912,10 +5989,6 @@ packages: resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} engines: {node: '>=14.16'} - slice-ansi@5.0.0: - resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} - engines: {node: '>=12'} - slice-ansi@7.1.0: resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==} engines: {node: '>=18'} @@ -5998,6 +6071,10 @@ packages: resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} engines: {node: '>=18'} + string-width@8.1.0: + resolution: {integrity: sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==} + engines: {node: '>=20'} + string_decoder@1.1.1: resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} @@ -6123,6 +6200,11 @@ packages: engines: {node: '>=10'} hasBin: true + terser@5.43.1: + resolution: {integrity: sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==} + engines: {node: '>=10'} + hasBin: true + test-exclude@7.0.1: resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} engines: {node: '>=18'} @@ -6300,6 +6382,9 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@7.10.0: + resolution: {integrity: sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==} + unenv@2.0.0-rc.18: resolution: {integrity: sha512-O0oVQVJ2X3Q8H4HITJr4e2cWxMYBeZ+p8S25yoKCxVCgDWtIJDcgwWNonYz12tI3ylVQCRyPV/Bdq0KJeXo7AA==} @@ -6593,8 +6678,8 @@ packages: vite: ^6.0.0 || ^7.0.0 vue: ^3.5.0 - vite@5.4.20: - resolution: {integrity: sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==} + vite@5.4.19: + resolution: {integrity: sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -6624,8 +6709,8 @@ packages: terser: optional: true - vite@6.3.6: - resolution: {integrity: sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==} + vite@6.3.5: + resolution: {integrity: sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true peerDependencies: @@ -6777,6 +6862,7 @@ packages: vue-router-mock@2.0.0: resolution: {integrity: sha512-UmfJ9C4odcC8P2d8+yZWGPnjK7MMc1Uk3bmchpq+8lcGEdpwrO18RPQOMUEiwAjqjTVN5Z955Weaz2Ev9UrXMw==} + version: 2.0.0 peerDependencies: vue: ^3.2.23 vue-router: ^4.0.12 @@ -6786,6 +6872,12 @@ packages: peerDependencies: vue: ^3.2.0 + vue-router@https://pkg.pr.new/vue-router@4f1a37a: + resolution: {tarball: https://pkg.pr.new/vue-router@4f1a37a} + version: 4.5.1 + peerDependencies: + vue: ^3.5.0 + vue-tsc@2.2.10: resolution: {integrity: sha512-jWZ1xSaNbabEV3whpIDMbjVSVawjAyW+x1n3JeGQo7S0uv2n9F/JMgWW90tGWNFRKya4YwKMZgCtr0vRAM7DeQ==} hasBin: true @@ -7129,7 +7221,7 @@ snapshots: '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-module-transforms': 7.27.3(@babel/core@7.27.4) '@babel/helpers': 7.27.4 - '@babel/parser': 7.28.4 + '@babel/parser': 7.28.3 '@babel/template': 7.27.2 '@babel/traverse': 7.27.4 '@babel/types': 7.28.4 @@ -7145,8 +7237,8 @@ snapshots: dependencies: '@babel/parser': 7.28.4 '@babel/types': 7.28.4 - '@jridgewell/gen-mapping': 0.3.12 - '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.30 jsesc: 3.1.0 '@babel/helper-annotate-as-pure@7.27.1': @@ -7157,7 +7249,7 @@ snapshots: dependencies: '@babel/compat-data': 7.27.2 '@babel/helper-validator-option': 7.27.1 - browserslist: 4.25.0 + browserslist: 4.25.2 lru-cache: 5.1.1 semver: 6.3.1 @@ -7290,14 +7382,14 @@ snapshots: '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 - '@babel/parser': 7.28.4 + '@babel/parser': 7.28.3 '@babel/types': 7.28.4 '@babel/traverse@7.27.4': dependencies: '@babel/code-frame': 7.27.1 '@babel/generator': 7.28.3 - '@babel/parser': 7.28.4 + '@babel/parser': 7.28.3 '@babel/template': 7.27.2 '@babel/types': 7.28.4 debug: 4.4.1(supports-color@5.5.0) @@ -8028,13 +8120,13 @@ snapshots: '@grpc/grpc-js@1.9.15': dependencies: '@grpc/proto-loader': 0.7.15 - '@types/node': 22.18.6 + '@types/node': 24.3.0 '@grpc/proto-loader@0.7.15': dependencies: lodash.camelcase: 4.3.0 long: 5.3.2 - protobufjs: 7.5.0 + protobufjs: 7.5.3 yargs: 17.7.2 '@hutson/parse-repository-url@5.0.0': {} @@ -8070,26 +8162,41 @@ snapshots: '@jridgewell/gen-mapping@0.3.12': dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/sourcemap-codec': 1.5.0 '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.30 + '@jridgewell/remapping@2.3.5': dependencies: - '@jridgewell/gen-mapping': 0.3.12 - '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.30 '@jridgewell/resolve-uri@3.1.2': {} + '@jridgewell/source-map@0.3.11': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.30 + '@jridgewell/source-map@0.3.6': dependencies: - '@jridgewell/gen-mapping': 0.3.12 - '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.30 '@jridgewell/sourcemap-codec@1.5.0': {} '@jridgewell/sourcemap-codec@1.5.5': {} '@jridgewell/trace-mapping@0.3.29': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@jridgewell/trace-mapping@0.3.30': dependencies: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 @@ -8167,12 +8274,12 @@ snapshots: uuid: 11.1.0 write-file-atomic: 6.0.0 - '@netlify/functions@3.1.10(encoding@0.1.13)(rollup@4.52.0)': + '@netlify/functions@3.1.10(encoding@0.1.13)(rollup@4.52.2)': dependencies: '@netlify/blobs': 9.1.2 '@netlify/dev-utils': 2.2.0 '@netlify/serverless-functions-api': 1.41.2 - '@netlify/zip-it-and-ship-it': 12.1.1(encoding@0.1.13)(rollup@4.52.0) + '@netlify/zip-it-and-ship-it': 12.1.1(encoding@0.1.13)(rollup@4.52.2) cron-parser: 4.9.0 decache: 4.6.2 extract-zip: 2.0.1 @@ -8192,13 +8299,13 @@ snapshots: '@netlify/serverless-functions-api@1.41.2': {} - '@netlify/zip-it-and-ship-it@12.1.1(encoding@0.1.13)(rollup@4.52.0)': + '@netlify/zip-it-and-ship-it@12.1.1(encoding@0.1.13)(rollup@4.52.2)': dependencies: - '@babel/parser': 7.28.4 + '@babel/parser': 7.28.3 '@babel/types': 7.27.3 '@netlify/binary-info': 1.0.0 '@netlify/serverless-functions-api': 1.41.2 - '@vercel/nft': 0.29.3(encoding@0.1.13)(rollup@4.52.0) + '@vercel/nft': 0.29.3(encoding@0.1.13)(rollup@4.52.2) archiver: 7.0.1 common-path-prefix: 3.0.0 copy-file: 11.0.0 @@ -8276,11 +8383,11 @@ snapshots: '@nuxt/devalue@2.0.2': {} - '@nuxt/devtools-kit@2.6.2(magicast@0.3.5)(vite@7.1.6(@types/node@22.18.6)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1))': + '@nuxt/devtools-kit@2.6.2(magicast@0.3.5)(vite@7.1.6(@types/node@24.3.0)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1))': dependencies: - '@nuxt/kit': 3.17.6(magicast@0.3.5) + '@nuxt/kit': 3.18.1(magicast@0.3.5) execa: 8.0.1 - vite: 7.1.6(@types/node@22.18.6)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1) + vite: 7.1.6(@types/node@24.3.0)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1) transitivePeerDependencies: - magicast @@ -8295,12 +8402,12 @@ snapshots: prompts: 2.4.2 semver: 7.7.2 - '@nuxt/devtools@2.6.2(vite@7.1.6(@types/node@22.18.6)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1))(vue@3.5.18(typescript@5.9.2))': + '@nuxt/devtools@2.6.2(vite@7.1.6(@types/node@24.3.0)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1))(vue@3.5.18(typescript@5.9.2))': dependencies: - '@nuxt/devtools-kit': 2.6.2(magicast@0.3.5)(vite@7.1.6(@types/node@22.18.6)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1)) + '@nuxt/devtools-kit': 2.6.2(magicast@0.3.5)(vite@7.1.6(@types/node@24.3.0)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1)) '@nuxt/devtools-wizard': 2.6.2 '@nuxt/kit': 3.17.6(magicast@0.3.5) - '@vue/devtools-core': 7.7.7(vite@7.1.6(@types/node@22.18.6)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1))(vue@3.5.18(typescript@5.9.2)) + '@vue/devtools-core': 7.7.7(vite@7.1.6(@types/node@24.3.0)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1))(vue@3.5.18(typescript@5.9.2)) '@vue/devtools-kit': 7.7.7 birpc: 2.5.0 consola: 3.4.2 @@ -8325,9 +8432,9 @@ snapshots: sirv: 3.0.1 structured-clone-es: 1.0.0 tinyglobby: 0.2.15 - vite: 7.1.6(@types/node@22.18.6)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1) - vite-plugin-inspect: 11.3.0(@nuxt/kit@3.17.6(magicast@0.3.5))(vite@7.1.6(@types/node@22.18.6)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1)) - vite-plugin-vue-tracer: 1.0.0(vite@7.1.6(@types/node@22.18.6)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1))(vue@3.5.18(typescript@5.9.2)) + vite: 7.1.6(@types/node@24.3.0)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1) + vite-plugin-inspect: 11.3.0(@nuxt/kit@3.17.6(magicast@0.3.5))(vite@7.1.6(@types/node@24.3.0)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1)) + vite-plugin-vue-tracer: 1.0.0(vite@7.1.6(@types/node@24.3.0)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1))(vue@3.5.18(typescript@5.9.2)) which: 5.0.0 ws: 8.18.3 transitivePeerDependencies: @@ -8363,9 +8470,36 @@ snapshots: transitivePeerDependencies: - magicast + '@nuxt/kit@3.18.1(magicast@0.3.5)': + dependencies: + c12: 3.2.0(magicast@0.3.5) + consola: 3.4.2 + defu: 6.1.4 + destr: 2.0.5 + errx: 0.1.0 + exsolve: 1.0.7 + ignore: 7.0.5 + jiti: 2.5.1 + klona: 2.0.6 + knitwork: 1.2.0 + mlly: 1.8.0 + ohash: 2.0.11 + pathe: 2.0.3 + pkg-types: 2.2.0 + scule: 1.3.0 + semver: 7.7.2 + std-env: 3.9.0 + tinyglobby: 0.2.15 + ufo: 1.6.1 + unctx: 2.4.1 + unimport: 5.2.0 + untyped: 2.0.0 + transitivePeerDependencies: + - magicast + '@nuxt/schema@3.17.6': dependencies: - '@vue/shared': 3.5.19 + '@vue/shared': 3.5.18 consola: 3.4.2 defu: 6.1.4 pathe: 2.0.3 @@ -8388,17 +8522,17 @@ snapshots: transitivePeerDependencies: - magicast - '@nuxt/vite-builder@3.17.6(@types/node@22.18.6)(lightningcss@1.29.3)(magicast@0.3.5)(meow@13.2.0)(rolldown@1.0.0-beta.10-commit.87188ed)(rollup@4.52.0)(terser@5.40.0)(typescript@5.9.2)(vue-tsc@2.2.10(typescript@5.9.2))(vue@3.5.18(typescript@5.9.2))(yaml@2.8.1)': + '@nuxt/vite-builder@3.17.6(@types/node@24.3.0)(lightningcss@1.29.3)(magicast@0.3.5)(meow@13.2.0)(rolldown@1.0.0-beta.10-commit.87188ed)(rollup@4.52.2)(terser@5.43.1)(typescript@5.9.2)(vue-tsc@2.2.10(typescript@5.9.2))(vue@3.5.18(typescript@5.9.2))(yaml@2.8.1)': dependencies: '@nuxt/kit': 3.17.6(magicast@0.3.5) - '@rollup/plugin-replace': 6.0.2(rollup@4.52.0) - '@vitejs/plugin-vue': 5.2.4(vite@6.3.6(@types/node@22.18.6)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1))(vue@3.5.18(typescript@5.9.2)) - '@vitejs/plugin-vue-jsx': 4.2.0(vite@6.3.6(@types/node@22.18.6)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1))(vue@3.5.18(typescript@5.9.2)) + '@rollup/plugin-replace': 6.0.2(rollup@4.52.2) + '@vitejs/plugin-vue': 5.2.4(vite@6.3.5(@types/node@24.3.0)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1))(vue@3.5.18(typescript@5.9.2)) + '@vitejs/plugin-vue-jsx': 4.2.0(vite@6.3.5(@types/node@24.3.0)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1))(vue@3.5.18(typescript@5.9.2)) autoprefixer: 10.4.21(postcss@8.5.6) consola: 3.4.2 cssnano: 7.0.7(postcss@8.5.6) defu: 6.1.4 - esbuild: 0.25.9 + esbuild: 0.25.5 escape-string-regexp: 5.0.0 exsolve: 1.0.7 externality: 1.0.2 @@ -8414,13 +8548,13 @@ snapshots: perfect-debounce: 1.0.0 pkg-types: 2.2.0 postcss: 8.5.6 - rollup-plugin-visualizer: 6.0.3(rolldown@1.0.0-beta.10-commit.87188ed)(rollup@4.52.0) + rollup-plugin-visualizer: 6.0.3(rolldown@1.0.0-beta.10-commit.87188ed)(rollup@4.52.2) std-env: 3.9.0 ufo: 1.6.1 unenv: 2.0.0-rc.18 - vite: 6.3.6(@types/node@22.18.6)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1) - vite-node: 3.2.4(@types/node@22.18.6)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1) - vite-plugin-checker: 0.9.3(meow@13.2.0)(typescript@5.9.2)(vite@6.3.6(@types/node@22.18.6)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1))(vue-tsc@2.2.10(typescript@5.9.2)) + vite: 6.3.5(@types/node@24.3.0)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1) + vite-node: 3.2.4(@types/node@24.3.0)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1) + vite-plugin-checker: 0.9.3(meow@13.2.0)(typescript@5.9.2)(vite@6.3.5(@types/node@24.3.0)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1))(vue-tsc@2.2.10(typescript@5.9.2)) vue: 3.5.18(typescript@5.9.2) vue-bundle-renderer: 2.1.1 transitivePeerDependencies: @@ -8505,7 +8639,7 @@ snapshots: '@oxc-project/types@0.75.1': {} - '@oxc-project/types@0.89.0': {} + '@oxc-project/types@0.92.0': {} '@parcel/watcher-android-arm64@2.5.1': optional: true @@ -8586,13 +8720,13 @@ snapshots: '@pinia/colada@0.17.5(pinia@3.0.3(typescript@5.9.2)(vue@3.5.21(typescript@5.9.2)))(vue@3.5.21(typescript@5.9.2))': dependencies: - '@vue/devtools-api': 8.0.1 + '@vue/devtools-api': 8.0.2 pinia: 3.0.3(typescript@5.9.2)(vue@3.5.21(typescript@5.9.2)) vue: 3.5.21(typescript@5.9.2) - '@pinia/nuxt@0.11.1(magicast@0.3.5)(pinia@3.0.3(typescript@5.9.2)(vue@3.5.21(typescript@5.9.2)))': + '@pinia/nuxt@0.11.2(magicast@0.3.5)(pinia@3.0.3(typescript@5.9.2)(vue@3.5.21(typescript@5.9.2)))': dependencies: - '@nuxt/kit': 3.17.6(magicast@0.3.5) + '@nuxt/kit': 3.18.1(magicast@0.3.5) pinia: 3.0.3(typescript@5.9.2)(vue@3.5.21(typescript@5.9.2)) transitivePeerDependencies: - magicast @@ -8646,64 +8780,64 @@ snapshots: dependencies: quansync: 0.2.11 - '@rolldown/binding-android-arm64@1.0.0-beta.38': + '@rolldown/binding-android-arm64@1.0.0-beta.40': optional: true '@rolldown/binding-darwin-arm64@1.0.0-beta.10-commit.87188ed': optional: true - '@rolldown/binding-darwin-arm64@1.0.0-beta.38': + '@rolldown/binding-darwin-arm64@1.0.0-beta.40': optional: true '@rolldown/binding-darwin-x64@1.0.0-beta.10-commit.87188ed': optional: true - '@rolldown/binding-darwin-x64@1.0.0-beta.38': + '@rolldown/binding-darwin-x64@1.0.0-beta.40': optional: true '@rolldown/binding-freebsd-x64@1.0.0-beta.10-commit.87188ed': optional: true - '@rolldown/binding-freebsd-x64@1.0.0-beta.38': + '@rolldown/binding-freebsd-x64@1.0.0-beta.40': optional: true '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.10-commit.87188ed': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.38': + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.40': optional: true '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.10-commit.87188ed': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.38': + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.40': optional: true '@rolldown/binding-linux-arm64-musl@1.0.0-beta.10-commit.87188ed': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.0-beta.38': + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.40': optional: true '@rolldown/binding-linux-x64-gnu@1.0.0-beta.10-commit.87188ed': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.0-beta.38': + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.40': optional: true '@rolldown/binding-linux-x64-musl@1.0.0-beta.10-commit.87188ed': optional: true - '@rolldown/binding-linux-x64-musl@1.0.0-beta.38': + '@rolldown/binding-linux-x64-musl@1.0.0-beta.40': optional: true - '@rolldown/binding-openharmony-arm64@1.0.0-beta.38': + '@rolldown/binding-openharmony-arm64@1.0.0-beta.40': optional: true '@rolldown/binding-wasm32-wasi@1.0.0-beta.10-commit.87188ed': optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-beta.38': + '@rolldown/binding-wasm32-wasi@1.0.0-beta.40': dependencies: '@napi-rs/wasm-runtime': 1.0.5 optional: true @@ -8711,34 +8845,37 @@ snapshots: '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.10-commit.87188ed': optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.38': + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.40': optional: true '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.10-commit.87188ed': optional: true - '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.38': + '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.40': optional: true '@rolldown/binding-win32-x64-msvc@1.0.0-beta.10-commit.87188ed': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.0-beta.38': + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.40': optional: true - '@rolldown/pluginutils@1.0.0-beta.10-commit.87188ed': {} + '@rolldown/pluginutils@1.0.0-beta.10-commit.87188ed': + optional: true '@rolldown/pluginutils@1.0.0-beta.29': {} - '@rolldown/pluginutils@1.0.0-beta.38': {} + '@rolldown/pluginutils@1.0.0-beta.33': {} + + '@rolldown/pluginutils@1.0.0-beta.40': {} - '@rollup/plugin-alias@5.1.1(rollup@4.52.0)': + '@rollup/plugin-alias@5.1.1(rollup@4.52.2)': optionalDependencies: - rollup: 4.52.0 + rollup: 4.52.2 - '@rollup/plugin-commonjs@28.0.6(rollup@4.52.0)': + '@rollup/plugin-commonjs@28.0.6(rollup@4.52.2)': dependencies: - '@rollup/pluginutils': 5.1.4(rollup@4.52.0) + '@rollup/pluginutils': 5.1.4(rollup@4.52.2) commondir: 1.0.1 estree-walker: 2.0.2 fdir: 6.5.0(picomatch@4.0.3) @@ -8746,119 +8883,119 @@ snapshots: magic-string: 0.30.19 picomatch: 4.0.3 optionalDependencies: - rollup: 4.52.0 + rollup: 4.52.2 - '@rollup/plugin-inject@5.0.5(rollup@4.52.0)': + '@rollup/plugin-inject@5.0.5(rollup@4.52.2)': dependencies: - '@rollup/pluginutils': 5.1.4(rollup@4.52.0) + '@rollup/pluginutils': 5.1.4(rollup@4.52.2) estree-walker: 2.0.2 magic-string: 0.30.19 optionalDependencies: - rollup: 4.52.0 + rollup: 4.52.2 - '@rollup/plugin-json@6.1.0(rollup@4.52.0)': + '@rollup/plugin-json@6.1.0(rollup@4.52.2)': dependencies: - '@rollup/pluginutils': 5.1.4(rollup@4.52.0) + '@rollup/pluginutils': 5.1.4(rollup@4.52.2) optionalDependencies: - rollup: 4.52.0 + rollup: 4.52.2 - '@rollup/plugin-node-resolve@16.0.1(rollup@4.52.0)': + '@rollup/plugin-node-resolve@16.0.1(rollup@4.52.2)': dependencies: - '@rollup/pluginutils': 5.1.4(rollup@4.52.0) + '@rollup/pluginutils': 5.1.4(rollup@4.52.2) '@types/resolve': 1.20.2 deepmerge: 4.3.1 is-module: 1.0.0 resolve: 1.22.10 optionalDependencies: - rollup: 4.52.0 + rollup: 4.52.2 - '@rollup/plugin-replace@6.0.2(rollup@4.52.0)': + '@rollup/plugin-replace@6.0.2(rollup@4.52.2)': dependencies: - '@rollup/pluginutils': 5.1.4(rollup@4.52.0) + '@rollup/pluginutils': 5.1.4(rollup@4.52.2) magic-string: 0.30.19 optionalDependencies: - rollup: 4.52.0 + rollup: 4.52.2 - '@rollup/plugin-terser@0.4.4(rollup@4.52.0)': + '@rollup/plugin-terser@0.4.4(rollup@4.52.2)': dependencies: serialize-javascript: 6.0.2 smob: 1.5.0 terser: 5.40.0 optionalDependencies: - rollup: 4.52.0 + rollup: 4.52.2 - '@rollup/pluginutils@5.1.4(rollup@4.52.0)': + '@rollup/pluginutils@5.1.4(rollup@4.52.2)': dependencies: '@types/estree': 1.0.8 estree-walker: 2.0.2 picomatch: 4.0.3 optionalDependencies: - rollup: 4.52.0 + rollup: 4.52.2 - '@rollup/rollup-android-arm-eabi@4.52.0': + '@rollup/rollup-android-arm-eabi@4.52.2': optional: true - '@rollup/rollup-android-arm64@4.52.0': + '@rollup/rollup-android-arm64@4.52.2': optional: true - '@rollup/rollup-darwin-arm64@4.52.0': + '@rollup/rollup-darwin-arm64@4.52.2': optional: true - '@rollup/rollup-darwin-x64@4.52.0': + '@rollup/rollup-darwin-x64@4.52.2': optional: true - '@rollup/rollup-freebsd-arm64@4.52.0': + '@rollup/rollup-freebsd-arm64@4.52.2': optional: true - '@rollup/rollup-freebsd-x64@4.52.0': + '@rollup/rollup-freebsd-x64@4.52.2': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.52.0': + '@rollup/rollup-linux-arm-gnueabihf@4.52.2': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.52.0': + '@rollup/rollup-linux-arm-musleabihf@4.52.2': optional: true - '@rollup/rollup-linux-arm64-gnu@4.52.0': + '@rollup/rollup-linux-arm64-gnu@4.52.2': optional: true - '@rollup/rollup-linux-arm64-musl@4.52.0': + '@rollup/rollup-linux-arm64-musl@4.52.2': optional: true - '@rollup/rollup-linux-loong64-gnu@4.52.0': + '@rollup/rollup-linux-loong64-gnu@4.52.2': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.52.0': + '@rollup/rollup-linux-ppc64-gnu@4.52.2': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.52.0': + '@rollup/rollup-linux-riscv64-gnu@4.52.2': optional: true - '@rollup/rollup-linux-riscv64-musl@4.52.0': + '@rollup/rollup-linux-riscv64-musl@4.52.2': optional: true - '@rollup/rollup-linux-s390x-gnu@4.52.0': + '@rollup/rollup-linux-s390x-gnu@4.52.2': optional: true - '@rollup/rollup-linux-x64-gnu@4.52.0': + '@rollup/rollup-linux-x64-gnu@4.52.2': optional: true - '@rollup/rollup-linux-x64-musl@4.52.0': + '@rollup/rollup-linux-x64-musl@4.52.2': optional: true - '@rollup/rollup-openharmony-arm64@4.52.0': + '@rollup/rollup-openharmony-arm64@4.52.2': optional: true - '@rollup/rollup-win32-arm64-msvc@4.52.0': + '@rollup/rollup-win32-arm64-msvc@4.52.2': optional: true - '@rollup/rollup-win32-ia32-msvc@4.52.0': + '@rollup/rollup-win32-ia32-msvc@4.52.2': optional: true - '@rollup/rollup-win32-x64-gnu@4.52.0': + '@rollup/rollup-win32-x64-gnu@4.52.2': optional: true - '@rollup/rollup-win32-x64-msvc@4.52.0': + '@rollup/rollup-win32-x64-msvc@4.52.2': optional: true '@sec-ant/readable-stream@0.4.1': {} @@ -9030,6 +9167,10 @@ snapshots: tslib: 2.8.1 optional: true + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.28.4 + '@types/chai@5.2.2': dependencies: '@types/deep-eql': 4.0.2 @@ -9073,13 +9214,13 @@ snapshots: '@types/ms@2.1.0': {} - '@types/node@20.19.4': + '@types/node@20.19.11': dependencies: undici-types: 6.21.0 - '@types/node@22.18.6': + '@types/node@24.3.0': dependencies: - undici-types: 6.21.0 + undici-types: 7.10.0 '@types/normalize-package-data@2.4.4': {} @@ -9103,14 +9244,14 @@ snapshots: '@types/yauzl@2.10.3': dependencies: - '@types/node': 22.18.6 + '@types/node': 24.3.0 optional: true '@typescript-eslint/project-service@8.33.0(typescript@5.9.2)': dependencies: '@typescript-eslint/tsconfig-utils': 8.33.0(typescript@5.9.2) '@typescript-eslint/types': 8.33.0 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.3 transitivePeerDependencies: - supports-color - typescript @@ -9127,7 +9268,7 @@ snapshots: '@typescript-eslint/tsconfig-utils': 8.33.0(typescript@5.9.2) '@typescript-eslint/types': 8.33.0 '@typescript-eslint/visitor-keys': 8.33.0 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.3 fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 @@ -9144,7 +9285,7 @@ snapshots: '@typescript/vfs@1.6.1(typescript@5.9.2)': dependencies: - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.3 typescript: 5.9.2 transitivePeerDependencies: - supports-color @@ -9157,10 +9298,10 @@ snapshots: unhead: 2.0.11 vue: 3.5.18(typescript@5.9.2) - '@vercel/nft@0.29.3(encoding@0.1.13)(rollup@4.52.0)': + '@vercel/nft@0.29.3(encoding@0.1.13)(rollup@4.52.2)': dependencies: '@mapbox/node-pre-gyp': 2.0.0(encoding@0.1.13) - '@rollup/pluginutils': 5.1.4(rollup@4.52.0) + '@rollup/pluginutils': 5.1.4(rollup@4.52.2) acorn: 8.15.0 acorn-import-attributes: 1.9.5(acorn@8.15.0) async-sema: 3.1.1 @@ -9176,10 +9317,10 @@ snapshots: - rollup - supports-color - '@vercel/nft@0.29.4(encoding@0.1.13)(rollup@4.52.0)': + '@vercel/nft@0.29.4(encoding@0.1.13)(rollup@4.52.2)': dependencies: '@mapbox/node-pre-gyp': 2.0.0(encoding@0.1.13) - '@rollup/pluginutils': 5.1.4(rollup@4.52.0) + '@rollup/pluginutils': 5.1.4(rollup@4.52.2) acorn: 8.15.0 acorn-import-attributes: 1.9.5(acorn@8.15.0) async-sema: 3.1.1 @@ -9195,31 +9336,31 @@ snapshots: - rollup - supports-color - '@vitejs/plugin-vue-jsx@4.2.0(vite@6.3.6(@types/node@22.18.6)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1))(vue@3.5.18(typescript@5.9.2))': + '@vitejs/plugin-vue-jsx@4.2.0(vite@6.3.5(@types/node@24.3.0)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1))(vue@3.5.18(typescript@5.9.2))': dependencies: '@babel/core': 7.27.4 '@babel/plugin-transform-typescript': 7.27.1(@babel/core@7.27.4) - '@rolldown/pluginutils': 1.0.0-beta.10-commit.87188ed + '@rolldown/pluginutils': 1.0.0-beta.33 '@vue/babel-plugin-jsx': 1.4.0(@babel/core@7.27.4) - vite: 6.3.6(@types/node@22.18.6)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1) + vite: 6.3.5(@types/node@24.3.0)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1) vue: 3.5.18(typescript@5.9.2) transitivePeerDependencies: - supports-color - '@vitejs/plugin-vue@5.2.4(vite@5.4.20(@types/node@22.18.6)(lightningcss@1.29.3)(terser@5.40.0))(vue@3.5.21(typescript@5.9.2))': + '@vitejs/plugin-vue@5.2.4(vite@5.4.19(@types/node@24.3.0)(lightningcss@1.29.3)(terser@5.43.1))(vue@3.5.21(typescript@5.9.2))': dependencies: - vite: 5.4.20(@types/node@22.18.6)(lightningcss@1.29.3)(terser@5.40.0) + vite: 5.4.19(@types/node@24.3.0)(lightningcss@1.29.3)(terser@5.43.1) vue: 3.5.21(typescript@5.9.2) - '@vitejs/plugin-vue@5.2.4(vite@6.3.6(@types/node@22.18.6)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1))(vue@3.5.18(typescript@5.9.2))': + '@vitejs/plugin-vue@5.2.4(vite@6.3.5(@types/node@24.3.0)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1))(vue@3.5.18(typescript@5.9.2))': dependencies: - vite: 6.3.6(@types/node@22.18.6)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1) + vite: 6.3.5(@types/node@24.3.0)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1) vue: 3.5.18(typescript@5.9.2) - '@vitejs/plugin-vue@6.0.1(vite@7.1.6(@types/node@22.18.6)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2))': + '@vitejs/plugin-vue@6.0.1(vite@7.1.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2))': dependencies: '@rolldown/pluginutils': 1.0.0-beta.29 - vite: 7.1.6(@types/node@22.18.6)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1) + vite: 7.1.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1) vue: 3.5.21(typescript@5.9.2) '@vitest/coverage-v8@3.2.4(vitest@3.2.4)': @@ -9237,7 +9378,7 @@ snapshots: std-env: 3.9.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.6)(@vitest/ui@3.2.4)(happy-dom@18.0.1)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(@vitest/ui@3.2.4)(happy-dom@18.0.1)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1) transitivePeerDependencies: - supports-color @@ -9249,13 +9390,13 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@7.1.6(@types/node@22.18.6)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1))': + '@vitest/mocker@3.2.4(vite@7.1.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.19 optionalDependencies: - vite: 7.1.6(@types/node@22.18.6)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1) + vite: 7.1.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1) '@vitest/pretty-format@3.2.4': dependencies: @@ -9286,7 +9427,7 @@ snapshots: sirv: 3.0.1 tinyglobby: 0.2.15 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.6)(@vitest/ui@3.2.4)(happy-dom@18.0.1)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(@vitest/ui@3.2.4)(happy-dom@18.0.1)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1) '@vitest/utils@3.2.4': dependencies: @@ -9306,25 +9447,25 @@ snapshots: path-browserify: 1.0.1 vscode-uri: 3.1.0 - '@vue-macros/common@3.0.0(vue@3.5.21(typescript@5.9.2))': + '@vue-macros/common@3.0.0-beta.15(vue@3.5.18(typescript@5.9.2))': dependencies: '@vue/compiler-sfc': 3.5.21 ast-kit: 2.1.2 local-pkg: 1.1.2 magic-string-ast: 1.0.2 - unplugin-utils: 0.3.0 + unplugin-utils: 0.2.5 optionalDependencies: - vue: 3.5.21(typescript@5.9.2) + vue: 3.5.18(typescript@5.9.2) - '@vue-macros/common@3.0.0-beta.15(vue@3.5.18(typescript@5.9.2))': + '@vue-macros/common@3.1.0(vue@3.5.21(typescript@5.9.2))': dependencies: '@vue/compiler-sfc': 3.5.21 ast-kit: 2.1.2 local-pkg: 1.1.2 magic-string-ast: 1.0.2 - unplugin-utils: 0.2.5 + unplugin-utils: 0.3.0 optionalDependencies: - vue: 3.5.18(typescript@5.9.2) + vue: 3.5.21(typescript@5.9.2) '@vue/babel-helper-vue-transform-on@1.4.0': {} @@ -9338,7 +9479,7 @@ snapshots: '@babel/types': 7.28.4 '@vue/babel-helper-vue-transform-on': 1.4.0 '@vue/babel-plugin-resolve-type': 1.4.0(@babel/core@7.27.4) - '@vue/shared': 3.5.21 + '@vue/shared': 3.5.18 optionalDependencies: '@babel/core': 7.27.4 transitivePeerDependencies: @@ -9350,14 +9491,14 @@ snapshots: '@babel/core': 7.27.4 '@babel/helper-module-imports': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 - '@babel/parser': 7.28.4 + '@babel/parser': 7.28.3 '@vue/compiler-sfc': 3.5.21 transitivePeerDependencies: - supports-color '@vue/compiler-core@3.5.18': dependencies: - '@babel/parser': 7.28.3 + '@babel/parser': 7.28.0 '@vue/shared': 3.5.18 entities: 4.5.0 estree-walker: 2.0.2 @@ -9401,7 +9542,7 @@ snapshots: '@vue/compiler-ssr': 3.5.21 '@vue/shared': 3.5.21 estree-walker: 2.0.2 - magic-string: 0.30.18 + magic-string: 0.30.19 postcss: 8.5.6 source-map-js: 1.2.1 @@ -9426,30 +9567,30 @@ snapshots: dependencies: '@vue/devtools-kit': 7.7.7 - '@vue/devtools-api@8.0.1': + '@vue/devtools-api@8.0.2': dependencies: - '@vue/devtools-kit': 8.0.1 + '@vue/devtools-kit': 8.0.2 - '@vue/devtools-core@7.7.7(vite@7.1.6(@types/node@22.18.6)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1))(vue@3.5.18(typescript@5.9.2))': + '@vue/devtools-core@7.7.7(vite@7.1.6(@types/node@24.3.0)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1))(vue@3.5.18(typescript@5.9.2))': dependencies: '@vue/devtools-kit': 7.7.7 '@vue/devtools-shared': 7.7.7 mitt: 3.0.1 nanoid: 5.1.5 pathe: 2.0.3 - vite-hot-client: 2.1.0(vite@7.1.6(@types/node@22.18.6)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1)) + vite-hot-client: 2.1.0(vite@7.1.6(@types/node@24.3.0)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1)) vue: 3.5.18(typescript@5.9.2) transitivePeerDependencies: - vite - '@vue/devtools-core@8.0.2(vite@7.1.6(@types/node@22.18.6)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2))': + '@vue/devtools-core@8.0.2(vite@7.1.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2))': dependencies: '@vue/devtools-kit': 8.0.2 '@vue/devtools-shared': 8.0.2 mitt: 3.0.1 nanoid: 5.1.5 pathe: 2.0.3 - vite-hot-client: 2.1.0(vite@7.1.6(@types/node@22.18.6)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1)) + vite-hot-client: 2.1.0(vite@7.1.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1)) vue: 3.5.21(typescript@5.9.2) transitivePeerDependencies: - vite @@ -9464,16 +9605,6 @@ snapshots: speakingurl: 14.0.1 superjson: 2.2.2 - '@vue/devtools-kit@8.0.1': - dependencies: - '@vue/devtools-shared': 8.0.1 - birpc: 2.5.0 - hookable: 5.5.3 - mitt: 3.0.1 - perfect-debounce: 1.0.0 - speakingurl: 14.0.1 - superjson: 2.2.2 - '@vue/devtools-kit@8.0.2': dependencies: '@vue/devtools-shared': 8.0.2 @@ -9488,10 +9619,6 @@ snapshots: dependencies: rfdc: 1.4.1 - '@vue/devtools-shared@8.0.1': - dependencies: - rfdc: 1.4.1 - '@vue/devtools-shared@8.0.2': dependencies: rfdc: 1.4.1 @@ -9513,9 +9640,9 @@ snapshots: '@vue/language-core@3.0.7(typescript@5.9.2)': dependencies: '@volar/language-core': 2.4.23 - '@vue/compiler-dom': 3.5.21 + '@vue/compiler-dom': 3.5.18 '@vue/compiler-vue2': 2.7.16 - '@vue/shared': 3.5.21 + '@vue/shared': 3.5.18 alien-signals: 2.0.5 muggle-string: 0.4.1 path-browserify: 1.0.1 @@ -9569,8 +9696,6 @@ snapshots: '@vue/shared@3.5.18': {} - '@vue/shared@3.5.19': {} - '@vue/shared@3.5.21': {} '@vue/test-utils@2.4.6': @@ -9578,6 +9703,11 @@ snapshots: js-beautify: 1.15.1 vue-component-type-helpers: 2.0.22 + '@vue/tsconfig@0.7.0(typescript@5.9.2)(vue@3.5.21(typescript@5.9.2))': + optionalDependencies: + typescript: 5.9.2 + vue: 3.5.21(typescript@5.9.2) + '@vue/tsconfig@0.8.1(typescript@5.9.2)(vue@3.5.21(typescript@5.9.2))': optionalDependencies: typescript: 5.9.2 @@ -9840,7 +9970,7 @@ snapshots: ast-kit@2.1.2: dependencies: - '@babel/parser': 7.28.4 + '@babel/parser': 7.28.3 pathe: 2.0.3 ast-module-types@6.0.1: {} @@ -9862,7 +9992,7 @@ snapshots: autoprefixer@10.4.21(postcss@8.5.6): dependencies: - browserslist: 4.25.0 + browserslist: 4.25.2 caniuse-lite: 1.0.30001720 fraction.js: 4.3.7 normalize-range: 0.1.2 @@ -9925,12 +10055,12 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist@4.25.0: + browserslist@4.25.2: dependencies: - caniuse-lite: 1.0.30001720 - electron-to-chromium: 1.5.161 + caniuse-lite: 1.0.30001735 + electron-to-chromium: 1.5.202 node-releases: 2.0.19 - update-browserslist-db: 1.1.3(browserslist@4.25.0) + update-browserslist-db: 1.1.3(browserslist@4.25.2) buffer-crc32@0.2.13: {} @@ -9970,6 +10100,23 @@ snapshots: optionalDependencies: magicast: 0.3.5 + c12@3.2.0(magicast@0.3.5): + dependencies: + chokidar: 4.0.3 + confbox: 0.2.2 + defu: 6.1.4 + dotenv: 17.2.1 + exsolve: 1.0.7 + giget: 2.0.0 + jiti: 2.5.1 + ohash: 2.0.11 + pathe: 2.0.3 + perfect-debounce: 1.0.0 + pkg-types: 2.2.0 + rc9: 2.1.2 + optionalDependencies: + magicast: 0.3.5 + cac@6.7.14: {} call-bind-apply-helpers@1.0.2: @@ -9986,13 +10133,15 @@ snapshots: caniuse-api@3.0.0: dependencies: - browserslist: 4.25.0 - caniuse-lite: 1.0.30001720 + browserslist: 4.25.2 + caniuse-lite: 1.0.30001735 lodash.memoize: 4.1.2 lodash.uniq: 4.5.0 caniuse-lite@1.0.30001720: {} + caniuse-lite@1.0.30001735: {} + ccount@2.0.1: {} chai@5.2.0: @@ -10051,10 +10200,10 @@ snapshots: dependencies: restore-cursor: 5.1.0 - cli-truncate@4.0.0: + cli-truncate@5.1.0: dependencies: - slice-ansi: 5.0.0 - string-width: 7.2.0 + slice-ansi: 7.1.0 + string-width: 8.1.0 clipboardy@4.0.0: dependencies: @@ -10334,7 +10483,7 @@ snapshots: cssnano-preset-default@7.0.7(postcss@8.5.6): dependencies: - browserslist: 4.25.0 + browserslist: 4.25.2 css-declaration-sorter: 7.2.0(postcss@8.5.6) cssnano-utils: 5.0.1(postcss@8.5.6) postcss: 8.5.6 @@ -10537,6 +10686,8 @@ snapshots: dotenv@16.5.0: {} + dotenv@17.2.1: {} + dts-resolver@2.1.2: {} dunder-proto@1.0.1: @@ -10558,7 +10709,7 @@ snapshots: ee-first@1.1.1: {} - electron-to-chromium@1.5.161: {} + electron-to-chromium@1.5.202: {} emoji-regex-xs@1.0.0: {} @@ -10585,7 +10736,7 @@ snapshots: dependencies: once: 1.4.0 - enhanced-resolve@5.18.1: + enhanced-resolve@5.18.3: dependencies: graceful-fs: 4.2.11 tapable: 2.2.2 @@ -10865,14 +11016,14 @@ snapshots: externality@1.0.2: dependencies: - enhanced-resolve: 5.18.1 + enhanced-resolve: 5.18.3 mlly: 1.8.0 pathe: 1.1.2 ufo: 1.6.1 extract-zip@2.0.1: dependencies: - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.3 get-stream: 5.2.0 yauzl: 2.10.0 optionalDependencies: @@ -10912,6 +11063,10 @@ snapshots: dependencies: pend: 1.2.0 + fdir@6.4.6(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 @@ -11203,7 +11358,7 @@ snapshots: happy-dom@18.0.1: dependencies: - '@types/node': 20.19.4 + '@types/node': 20.19.11 '@types/whatwg-mimetype': 3.0.2 whatwg-mimetype: 3.0.0 @@ -11262,7 +11417,7 @@ snapshots: https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.3 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -11318,7 +11473,7 @@ snapshots: dependencies: '@ioredis/commands': 1.2.0 cluster-key-slot: 1.1.2 - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.3 denque: 2.1.0 lodash.defaults: 4.2.0 lodash.isarguments: 3.1.0 @@ -11360,8 +11515,6 @@ snapshots: is-fullwidth-code-point@3.0.0: {} - is-fullwidth-code-point@4.0.0: {} - is-fullwidth-code-point@5.0.0: dependencies: get-east-asian-width: 1.3.0 @@ -11472,7 +11625,7 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 22.18.6 + '@types/node': 24.3.0 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -11628,7 +11781,7 @@ snapshots: commander: 14.0.0 debug: 4.4.1(supports-color@5.5.0) lilconfig: 3.1.3 - listr2: 9.0.3 + listr2: 9.0.4 micromatch: 4.0.8 nano-spawn: 1.0.2 pidtree: 0.6.0 @@ -11658,9 +11811,9 @@ snapshots: untun: 0.1.3 uqr: 0.1.2 - listr2@9.0.3: + listr2@9.0.4: dependencies: - cli-truncate: 4.0.0 + cli-truncate: 5.1.0 colorette: 2.0.20 eventemitter3: 5.0.1 log-update: 6.1.0 @@ -11753,10 +11906,6 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 - magic-string@0.30.18: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - magic-string@0.30.19: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -12221,15 +12370,15 @@ snapshots: nitropack@2.11.13(@netlify/blobs@8.2.0)(encoding@0.1.13)(rolldown@1.0.0-beta.10-commit.87188ed): dependencies: '@cloudflare/kv-asset-handler': 0.4.0 - '@netlify/functions': 3.1.10(encoding@0.1.13)(rollup@4.52.0) - '@rollup/plugin-alias': 5.1.1(rollup@4.52.0) - '@rollup/plugin-commonjs': 28.0.6(rollup@4.52.0) - '@rollup/plugin-inject': 5.0.5(rollup@4.52.0) - '@rollup/plugin-json': 6.1.0(rollup@4.52.0) - '@rollup/plugin-node-resolve': 16.0.1(rollup@4.52.0) - '@rollup/plugin-replace': 6.0.2(rollup@4.52.0) - '@rollup/plugin-terser': 0.4.4(rollup@4.52.0) - '@vercel/nft': 0.29.4(encoding@0.1.13)(rollup@4.52.0) + '@netlify/functions': 3.1.10(encoding@0.1.13)(rollup@4.52.2) + '@rollup/plugin-alias': 5.1.1(rollup@4.52.2) + '@rollup/plugin-commonjs': 28.0.6(rollup@4.52.2) + '@rollup/plugin-inject': 5.0.5(rollup@4.52.2) + '@rollup/plugin-json': 6.1.0(rollup@4.52.2) + '@rollup/plugin-node-resolve': 16.0.1(rollup@4.52.2) + '@rollup/plugin-replace': 6.0.2(rollup@4.52.2) + '@rollup/plugin-terser': 0.4.4(rollup@4.52.2) + '@vercel/nft': 0.29.4(encoding@0.1.13)(rollup@4.52.2) archiver: 7.0.1 c12: 3.0.4(magicast@0.3.5) chokidar: 4.0.3 @@ -12244,7 +12393,7 @@ snapshots: defu: 6.1.4 destr: 2.0.5 dot-prop: 9.0.0 - esbuild: 0.25.9 + esbuild: 0.25.5 escape-string-regexp: 5.0.0 etag: 1.8.1 exsolve: 1.0.7 @@ -12271,8 +12420,8 @@ snapshots: pkg-types: 2.2.0 pretty-bytes: 6.1.1 radix3: 1.1.2 - rollup: 4.52.0 - rollup-plugin-visualizer: 6.0.3(rolldown@1.0.0-beta.10-commit.87188ed)(rollup@4.52.0) + rollup: 4.52.2 + rollup-plugin-visualizer: 6.0.3(rolldown@1.0.0-beta.10-commit.87188ed)(rollup@4.52.2) scule: 1.3.0 semver: 7.7.2 serve-placeholder: 2.0.2 @@ -12346,7 +12495,7 @@ snapshots: node-source-walk@7.0.1: dependencies: - '@babel/parser': 7.28.4 + '@babel/parser': 7.28.3 nodemon@3.1.10: dependencies: @@ -12402,15 +12551,15 @@ snapshots: dependencies: boolbase: 1.0.0 - nuxt@3.17.6(@netlify/blobs@8.2.0)(@parcel/watcher@2.5.1)(@types/node@22.18.6)(@vue/compiler-sfc@3.5.21)(db0@0.3.2)(encoding@0.1.13)(ioredis@5.6.1)(lightningcss@1.29.3)(magicast@0.3.5)(meow@13.2.0)(rolldown@1.0.0-beta.10-commit.87188ed)(rollup@4.52.0)(terser@5.40.0)(typescript@5.9.2)(vite@7.1.6(@types/node@22.18.6)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1))(vue-tsc@2.2.10(typescript@5.9.2))(yaml@2.8.1): + nuxt@3.17.6(@netlify/blobs@8.2.0)(@parcel/watcher@2.5.1)(@types/node@24.3.0)(@vue/compiler-sfc@3.5.21)(db0@0.3.2)(encoding@0.1.13)(ioredis@5.6.1)(lightningcss@1.29.3)(magicast@0.3.5)(meow@13.2.0)(rolldown@1.0.0-beta.10-commit.87188ed)(rollup@4.52.2)(terser@5.43.1)(typescript@5.9.2)(vite@7.1.6(@types/node@24.3.0)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1))(vue-tsc@2.2.10(typescript@5.9.2))(yaml@2.8.1): dependencies: '@nuxt/cli': 3.25.1(magicast@0.3.5) '@nuxt/devalue': 2.0.2 - '@nuxt/devtools': 2.6.2(vite@7.1.6(@types/node@22.18.6)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1))(vue@3.5.18(typescript@5.9.2)) + '@nuxt/devtools': 2.6.2(vite@7.1.6(@types/node@24.3.0)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1))(vue@3.5.18(typescript@5.9.2)) '@nuxt/kit': 3.17.6(magicast@0.3.5) '@nuxt/schema': 3.17.6 '@nuxt/telemetry': 2.6.6(magicast@0.3.5) - '@nuxt/vite-builder': 3.17.6(@types/node@22.18.6)(lightningcss@1.29.3)(magicast@0.3.5)(meow@13.2.0)(rolldown@1.0.0-beta.10-commit.87188ed)(rollup@4.52.0)(terser@5.40.0)(typescript@5.9.2)(vue-tsc@2.2.10(typescript@5.9.2))(vue@3.5.18(typescript@5.9.2))(yaml@2.8.1) + '@nuxt/vite-builder': 3.17.6(@types/node@24.3.0)(lightningcss@1.29.3)(magicast@0.3.5)(meow@13.2.0)(rolldown@1.0.0-beta.10-commit.87188ed)(rollup@4.52.2)(terser@5.43.1)(typescript@5.9.2)(vue-tsc@2.2.10(typescript@5.9.2))(vue@3.5.18(typescript@5.9.2))(yaml@2.8.1) '@unhead/vue': 2.0.11(vue@3.5.18(typescript@5.9.2)) '@vue/shared': 3.5.18 c12: 3.0.4(magicast@0.3.5) @@ -12467,7 +12616,7 @@ snapshots: vue-router: 4.5.1(vue@3.5.21(typescript@5.9.2)) optionalDependencies: '@parcel/watcher': 2.5.1 - '@types/node': 22.18.6 + '@types/node': 24.3.0 transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -12772,7 +12921,7 @@ snapshots: postcss-colormin@7.0.3(postcss@8.5.6): dependencies: - browserslist: 4.25.0 + browserslist: 4.25.2 caniuse-api: 3.0.0 colord: 2.9.3 postcss: 8.5.6 @@ -12780,7 +12929,7 @@ snapshots: postcss-convert-values@7.0.5(postcss@8.5.6): dependencies: - browserslist: 4.25.0 + browserslist: 4.25.2 postcss: 8.5.6 postcss-value-parser: 4.2.0 @@ -12809,7 +12958,7 @@ snapshots: postcss-merge-rules@7.0.5(postcss@8.5.6): dependencies: - browserslist: 4.25.0 + browserslist: 4.25.2 caniuse-api: 3.0.0 cssnano-utils: 5.0.1(postcss@8.5.6) postcss: 8.5.6 @@ -12829,7 +12978,7 @@ snapshots: postcss-minify-params@7.0.3(postcss@8.5.6): dependencies: - browserslist: 4.25.0 + browserslist: 4.25.2 cssnano-utils: 5.0.1(postcss@8.5.6) postcss: 8.5.6 postcss-value-parser: 4.2.0 @@ -12871,7 +13020,7 @@ snapshots: postcss-normalize-unicode@7.0.3(postcss@8.5.6): dependencies: - browserslist: 4.25.0 + browserslist: 4.25.2 postcss: 8.5.6 postcss-value-parser: 4.2.0 @@ -12893,7 +13042,7 @@ snapshots: postcss-reduce-initial@7.0.3(postcss@8.5.6): dependencies: - browserslist: 4.25.0 + browserslist: 4.25.2 caniuse-api: 3.0.0 postcss: 8.5.6 @@ -12978,7 +13127,7 @@ snapshots: proto-list@1.2.4: {} - protobufjs@7.5.0: + protobufjs@7.5.3: dependencies: '@protobufjs/aspromise': 1.1.2 '@protobufjs/base64': 1.1.2 @@ -12990,7 +13139,7 @@ snapshots: '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 '@protobufjs/utf8': 1.1.0 - '@types/node': 22.18.6 + '@types/node': 24.3.0 long: 5.3.2 protocols@2.0.2: {} @@ -13191,7 +13340,7 @@ snapshots: glob: 11.0.0 package-json-from-dist: 1.0.0 - rolldown-plugin-dts@0.16.7(rolldown@1.0.0-beta.38)(typescript@5.9.2)(vue-tsc@3.0.7(typescript@5.9.2)): + rolldown-plugin-dts@0.16.8(rolldown@1.0.0-beta.40)(typescript@5.9.2)(vue-tsc@3.0.7(typescript@5.9.2)): dependencies: '@babel/generator': 7.28.3 '@babel/parser': 7.28.4 @@ -13202,7 +13351,7 @@ snapshots: dts-resolver: 2.1.2 get-tsconfig: 4.10.1 magic-string: 0.30.19 - rolldown: 1.0.0-beta.38 + rolldown: 1.0.0-beta.40 optionalDependencies: typescript: 5.9.2 vue-tsc: 3.0.7(typescript@5.9.2) @@ -13231,28 +13380,28 @@ snapshots: '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.10-commit.87188ed optional: true - rolldown@1.0.0-beta.38: + rolldown@1.0.0-beta.40: dependencies: - '@oxc-project/types': 0.89.0 - '@rolldown/pluginutils': 1.0.0-beta.38 + '@oxc-project/types': 0.92.0 + '@rolldown/pluginutils': 1.0.0-beta.40 ansis: 4.1.0 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.0-beta.38 - '@rolldown/binding-darwin-arm64': 1.0.0-beta.38 - '@rolldown/binding-darwin-x64': 1.0.0-beta.38 - '@rolldown/binding-freebsd-x64': 1.0.0-beta.38 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.38 - '@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.38 - '@rolldown/binding-linux-arm64-musl': 1.0.0-beta.38 - '@rolldown/binding-linux-x64-gnu': 1.0.0-beta.38 - '@rolldown/binding-linux-x64-musl': 1.0.0-beta.38 - '@rolldown/binding-openharmony-arm64': 1.0.0-beta.38 - '@rolldown/binding-wasm32-wasi': 1.0.0-beta.38 - '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.38 - '@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.38 - '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.38 - - rollup-plugin-visualizer@6.0.3(rolldown@1.0.0-beta.10-commit.87188ed)(rollup@4.52.0): + '@rolldown/binding-android-arm64': 1.0.0-beta.40 + '@rolldown/binding-darwin-arm64': 1.0.0-beta.40 + '@rolldown/binding-darwin-x64': 1.0.0-beta.40 + '@rolldown/binding-freebsd-x64': 1.0.0-beta.40 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.40 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.40 + '@rolldown/binding-linux-arm64-musl': 1.0.0-beta.40 + '@rolldown/binding-linux-x64-gnu': 1.0.0-beta.40 + '@rolldown/binding-linux-x64-musl': 1.0.0-beta.40 + '@rolldown/binding-openharmony-arm64': 1.0.0-beta.40 + '@rolldown/binding-wasm32-wasi': 1.0.0-beta.40 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.40 + '@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.40 + '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.40 + + rollup-plugin-visualizer@6.0.3(rolldown@1.0.0-beta.10-commit.87188ed)(rollup@4.52.2): dependencies: open: 8.4.2 picomatch: 4.0.3 @@ -13260,34 +13409,34 @@ snapshots: yargs: 17.7.2 optionalDependencies: rolldown: 1.0.0-beta.10-commit.87188ed - rollup: 4.52.0 + rollup: 4.52.2 - rollup@4.52.0: + rollup@4.52.2: dependencies: '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.52.0 - '@rollup/rollup-android-arm64': 4.52.0 - '@rollup/rollup-darwin-arm64': 4.52.0 - '@rollup/rollup-darwin-x64': 4.52.0 - '@rollup/rollup-freebsd-arm64': 4.52.0 - '@rollup/rollup-freebsd-x64': 4.52.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.52.0 - '@rollup/rollup-linux-arm-musleabihf': 4.52.0 - '@rollup/rollup-linux-arm64-gnu': 4.52.0 - '@rollup/rollup-linux-arm64-musl': 4.52.0 - '@rollup/rollup-linux-loong64-gnu': 4.52.0 - '@rollup/rollup-linux-ppc64-gnu': 4.52.0 - '@rollup/rollup-linux-riscv64-gnu': 4.52.0 - '@rollup/rollup-linux-riscv64-musl': 4.52.0 - '@rollup/rollup-linux-s390x-gnu': 4.52.0 - '@rollup/rollup-linux-x64-gnu': 4.52.0 - '@rollup/rollup-linux-x64-musl': 4.52.0 - '@rollup/rollup-openharmony-arm64': 4.52.0 - '@rollup/rollup-win32-arm64-msvc': 4.52.0 - '@rollup/rollup-win32-ia32-msvc': 4.52.0 - '@rollup/rollup-win32-x64-gnu': 4.52.0 - '@rollup/rollup-win32-x64-msvc': 4.52.0 + '@rollup/rollup-android-arm-eabi': 4.52.2 + '@rollup/rollup-android-arm64': 4.52.2 + '@rollup/rollup-darwin-arm64': 4.52.2 + '@rollup/rollup-darwin-x64': 4.52.2 + '@rollup/rollup-freebsd-arm64': 4.52.2 + '@rollup/rollup-freebsd-x64': 4.52.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.52.2 + '@rollup/rollup-linux-arm-musleabihf': 4.52.2 + '@rollup/rollup-linux-arm64-gnu': 4.52.2 + '@rollup/rollup-linux-arm64-musl': 4.52.2 + '@rollup/rollup-linux-loong64-gnu': 4.52.2 + '@rollup/rollup-linux-ppc64-gnu': 4.52.2 + '@rollup/rollup-linux-riscv64-gnu': 4.52.2 + '@rollup/rollup-linux-riscv64-musl': 4.52.2 + '@rollup/rollup-linux-s390x-gnu': 4.52.2 + '@rollup/rollup-linux-x64-gnu': 4.52.2 + '@rollup/rollup-linux-x64-musl': 4.52.2 + '@rollup/rollup-openharmony-arm64': 4.52.2 + '@rollup/rollup-win32-arm64-msvc': 4.52.2 + '@rollup/rollup-win32-ia32-msvc': 4.52.2 + '@rollup/rollup-win32-x64-gnu': 4.52.2 + '@rollup/rollup-win32-x64-msvc': 4.52.2 fsevents: 2.3.3 run-applescript@7.0.0: {} @@ -13346,7 +13495,7 @@ snapshots: send@1.2.0: dependencies: - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.3 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 @@ -13492,11 +13641,6 @@ snapshots: slash@5.1.0: {} - slice-ansi@5.0.0: - dependencies: - ansi-styles: 6.2.1 - is-fullwidth-code-point: 4.0.0 - slice-ansi@7.1.0: dependencies: ansi-styles: 6.2.1 @@ -13576,6 +13720,11 @@ snapshots: get-east-asian-width: 1.3.0 strip-ansi: 7.1.0 + string-width@8.1.0: + dependencies: + get-east-asian-width: 1.3.0 + strip-ansi: 7.1.0 + string_decoder@1.1.1: dependencies: safe-buffer: 5.1.2 @@ -13615,7 +13764,7 @@ snapshots: stylehacks@7.0.5(postcss@8.5.6): dependencies: - browserslist: 4.25.0 + browserslist: 4.25.2 postcss: 8.5.6 postcss-selector-parser: 7.1.0 @@ -13678,11 +13827,11 @@ snapshots: terser-webpack-plugin@5.3.14(webpack@5.101.3): dependencies: - '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/trace-mapping': 0.3.30 jest-worker: 27.5.1 schema-utils: 4.3.2 serialize-javascript: 6.0.2 - terser: 5.40.0 + terser: 5.43.1 webpack: 5.101.3 terser@5.40.0: @@ -13692,6 +13841,13 @@ snapshots: commander: 2.20.3 source-map-support: 0.5.21 + terser@5.43.1: + dependencies: + '@jridgewell/source-map': 0.3.11 + acorn: 8.15.0 + commander: 2.20.3 + source-map-support: 0.5.21 + test-exclude@7.0.1: dependencies: '@istanbuljs/schema': 0.1.3 @@ -13714,7 +13870,7 @@ snapshots: tinyglobby@0.2.14: dependencies: - fdir: 6.5.0(picomatch@4.0.3) + fdir: 6.4.6(picomatch@4.0.3) picomatch: 4.0.3 tinyglobby@0.2.15: @@ -13773,8 +13929,8 @@ snapshots: diff: 8.0.2 empathic: 2.0.0 hookable: 5.5.3 - rolldown: 1.0.0-beta.38 - rolldown-plugin-dts: 0.16.7(rolldown@1.0.0-beta.38)(typescript@5.9.2)(vue-tsc@3.0.7(typescript@5.9.2)) + rolldown: 1.0.0-beta.40 + rolldown-plugin-dts: 0.16.8(rolldown@1.0.0-beta.40)(typescript@5.9.2)(vue-tsc@3.0.7(typescript@5.9.2)) semver: 7.7.2 tinyexec: 1.0.1 tinyglobby: 0.2.15 @@ -13848,6 +14004,8 @@ snapshots: undici-types@6.21.0: {} + undici-types@7.10.0: {} + unenv@2.0.0-rc.18: dependencies: defu: 6.1.4 @@ -13901,7 +14059,7 @@ snapshots: mlly: 1.8.0 pathe: 2.0.3 picomatch: 4.0.3 - pkg-types: 2.3.0 + pkg-types: 2.2.0 scule: 1.3.0 strip-literal: 3.0.0 tinyglobby: 0.2.15 @@ -13964,7 +14122,7 @@ snapshots: pathe: 2.0.3 picomatch: 4.0.3 - unplugin-vue-markdown@29.1.0(vite@7.1.6(@types/node@22.18.6)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1)): + unplugin-vue-markdown@29.1.0(vite@7.1.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1)): dependencies: '@mdit-vue/plugin-component': 2.1.4 '@mdit-vue/plugin-frontmatter': 2.1.4 @@ -13974,7 +14132,7 @@ snapshots: markdown-it-async: 2.2.0 unplugin: 2.3.10 unplugin-utils: 0.2.5 - vite: 7.1.6(@types/node@22.18.6)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1) + vite: 7.1.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1) unplugin-vue-router@0.14.0(@vue/compiler-sfc@3.5.21)(vue-router@4.5.1(vue@3.5.18(typescript@5.9.2)))(vue@3.5.18(typescript@5.9.2)): dependencies: @@ -14054,9 +14212,9 @@ snapshots: pkg-types: 1.3.1 unplugin: 1.16.1 - update-browserslist-db@1.1.3(browserslist@4.25.0): + update-browserslist-db@1.1.3(browserslist@4.25.2): dependencies: - browserslist: 4.25.0 + browserslist: 4.25.2 escalade: 3.2.0 picocolors: 1.1.1 @@ -14089,33 +14247,33 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 - vite-dev-rpc@1.1.0(vite@7.1.6(@types/node@22.18.6)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1)): + vite-dev-rpc@1.1.0(vite@7.1.6(@types/node@24.3.0)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1)): dependencies: birpc: 2.5.0 - vite: 7.1.6(@types/node@22.18.6)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1) - vite-hot-client: 2.1.0(vite@7.1.6(@types/node@22.18.6)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1)) + vite: 7.1.6(@types/node@24.3.0)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1) + vite-hot-client: 2.1.0(vite@7.1.6(@types/node@24.3.0)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1)) - vite-dev-rpc@1.1.0(vite@7.1.6(@types/node@22.18.6)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1)): + vite-dev-rpc@1.1.0(vite@7.1.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1)): dependencies: birpc: 2.5.0 - vite: 7.1.6(@types/node@22.18.6)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1) - vite-hot-client: 2.1.0(vite@7.1.6(@types/node@22.18.6)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1)) + vite: 7.1.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1) + vite-hot-client: 2.1.0(vite@7.1.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1)) - vite-hot-client@2.1.0(vite@7.1.6(@types/node@22.18.6)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1)): + vite-hot-client@2.1.0(vite@7.1.6(@types/node@24.3.0)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1)): dependencies: - vite: 7.1.6(@types/node@22.18.6)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1) + vite: 7.1.6(@types/node@24.3.0)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1) - vite-hot-client@2.1.0(vite@7.1.6(@types/node@22.18.6)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1)): + vite-hot-client@2.1.0(vite@7.1.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1)): dependencies: - vite: 7.1.6(@types/node@22.18.6)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1) + vite: 7.1.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1) - vite-node@3.2.4(@types/node@22.18.6)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1): + vite-node@3.2.4(@types/node@24.3.0)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1): dependencies: cac: 6.7.14 debug: 4.4.1(supports-color@5.5.0) es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.1.6(@types/node@22.18.6)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1) + vite: 7.1.6(@types/node@24.3.0)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1) transitivePeerDependencies: - '@types/node' - jiti @@ -14130,13 +14288,13 @@ snapshots: - tsx - yaml - vite-node@3.2.4(@types/node@22.18.6)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1): + vite-node@3.2.4(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1): dependencies: cac: 6.7.14 debug: 4.4.1(supports-color@5.5.0) es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.1.6(@types/node@22.18.6)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1) + vite: 7.1.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1) transitivePeerDependencies: - '@types/node' - jiti @@ -14151,7 +14309,7 @@ snapshots: - tsx - yaml - vite-plugin-checker@0.9.3(meow@13.2.0)(typescript@5.9.2)(vite@6.3.6(@types/node@22.18.6)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1))(vue-tsc@2.2.10(typescript@5.9.2)): + vite-plugin-checker@0.9.3(meow@13.2.0)(typescript@5.9.2)(vite@6.3.5(@types/node@24.3.0)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1))(vue-tsc@2.2.10(typescript@5.9.2)): dependencies: '@babel/code-frame': 7.27.1 chokidar: 4.0.3 @@ -14161,14 +14319,14 @@ snapshots: strip-ansi: 7.1.0 tiny-invariant: 1.3.3 tinyglobby: 0.2.15 - vite: 6.3.6(@types/node@22.18.6)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1) + vite: 6.3.5(@types/node@24.3.0)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1) vscode-uri: 3.1.0 optionalDependencies: meow: 13.2.0 typescript: 5.9.2 vue-tsc: 2.2.10(typescript@5.9.2) - vite-plugin-inspect@11.3.0(@nuxt/kit@3.17.6(magicast@0.3.5))(vite@7.1.6(@types/node@22.18.6)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1)): + vite-plugin-inspect@11.3.0(@nuxt/kit@3.17.6(magicast@0.3.5))(vite@7.1.6(@types/node@24.3.0)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1)): dependencies: ansis: 4.1.0 debug: 4.4.1(supports-color@5.5.0) @@ -14178,14 +14336,14 @@ snapshots: perfect-debounce: 1.0.0 sirv: 3.0.1 unplugin-utils: 0.2.5 - vite: 7.1.6(@types/node@22.18.6)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1) - vite-dev-rpc: 1.1.0(vite@7.1.6(@types/node@22.18.6)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1)) + vite: 7.1.6(@types/node@24.3.0)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1) + vite-dev-rpc: 1.1.0(vite@7.1.6(@types/node@24.3.0)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1)) optionalDependencies: '@nuxt/kit': 3.17.6(magicast@0.3.5) transitivePeerDependencies: - supports-color - vite-plugin-inspect@11.3.3(vite@7.1.6(@types/node@22.18.6)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1)): + vite-plugin-inspect@11.3.3(vite@7.1.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1)): dependencies: ansis: 4.1.0 debug: 4.4.1(supports-color@5.5.0) @@ -14195,27 +14353,27 @@ snapshots: perfect-debounce: 2.0.0 sirv: 3.0.2 unplugin-utils: 0.3.0 - vite: 7.1.6(@types/node@22.18.6)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1) - vite-dev-rpc: 1.1.0(vite@7.1.6(@types/node@22.18.6)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1)) + vite: 7.1.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1) + vite-dev-rpc: 1.1.0(vite@7.1.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1)) transitivePeerDependencies: - supports-color - vite-plugin-vue-devtools@8.0.2(vite@7.1.6(@types/node@22.18.6)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2)): + vite-plugin-vue-devtools@8.0.2(vite@7.1.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2)): dependencies: - '@vue/devtools-core': 8.0.2(vite@7.1.6(@types/node@22.18.6)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2)) + '@vue/devtools-core': 8.0.2(vite@7.1.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2)) '@vue/devtools-kit': 8.0.2 '@vue/devtools-shared': 8.0.2 execa: 9.6.0 sirv: 3.0.2 - vite: 7.1.6(@types/node@22.18.6)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1) - vite-plugin-inspect: 11.3.3(vite@7.1.6(@types/node@22.18.6)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1)) - vite-plugin-vue-inspector: 5.3.2(vite@7.1.6(@types/node@22.18.6)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1)) + vite: 7.1.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1) + vite-plugin-inspect: 11.3.3(vite@7.1.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1)) + vite-plugin-vue-inspector: 5.3.2(vite@7.1.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1)) transitivePeerDependencies: - '@nuxt/kit' - supports-color - vue - vite-plugin-vue-inspector@5.3.2(vite@7.1.6(@types/node@22.18.6)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1)): + vite-plugin-vue-inspector@5.3.2(vite@7.1.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1)): dependencies: '@babel/core': 7.27.4 '@babel/plugin-proposal-decorators': 7.25.9(@babel/core@7.27.4) @@ -14223,80 +14381,80 @@ snapshots: '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.27.4) '@babel/plugin-transform-typescript': 7.27.1(@babel/core@7.27.4) '@vue/babel-plugin-jsx': 1.4.0(@babel/core@7.27.4) - '@vue/compiler-dom': 3.5.21 + '@vue/compiler-dom': 3.5.18 kolorist: 1.8.0 magic-string: 0.30.19 - vite: 7.1.6(@types/node@22.18.6)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1) + vite: 7.1.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1) transitivePeerDependencies: - supports-color - vite-plugin-vue-tracer@1.0.0(vite@7.1.6(@types/node@22.18.6)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1))(vue@3.5.18(typescript@5.9.2)): + vite-plugin-vue-tracer@1.0.0(vite@7.1.6(@types/node@24.3.0)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1))(vue@3.5.18(typescript@5.9.2)): dependencies: estree-walker: 3.0.3 exsolve: 1.0.7 magic-string: 0.30.19 pathe: 2.0.3 source-map-js: 1.2.1 - vite: 7.1.6(@types/node@22.18.6)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1) + vite: 7.1.6(@types/node@24.3.0)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1) vue: 3.5.18(typescript@5.9.2) - vite@5.4.20(@types/node@22.18.6)(lightningcss@1.29.3)(terser@5.40.0): + vite@5.4.19(@types/node@24.3.0)(lightningcss@1.29.3)(terser@5.43.1): dependencies: esbuild: 0.21.5 postcss: 8.5.6 - rollup: 4.52.0 + rollup: 4.52.2 optionalDependencies: - '@types/node': 22.18.6 + '@types/node': 24.3.0 fsevents: 2.3.3 lightningcss: 1.29.3 - terser: 5.40.0 + terser: 5.43.1 - vite@6.3.6(@types/node@22.18.6)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1): + vite@6.3.5(@types/node@24.3.0)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1): dependencies: esbuild: 0.25.9 fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 postcss: 8.5.6 - rollup: 4.52.0 + rollup: 4.52.2 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 22.18.6 + '@types/node': 24.3.0 fsevents: 2.3.3 jiti: 2.4.2 lightningcss: 1.29.3 - terser: 5.40.0 + terser: 5.43.1 yaml: 2.8.1 - vite@7.1.6(@types/node@22.18.6)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1): + vite@7.1.6(@types/node@24.3.0)(jiti@2.4.2)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1): dependencies: esbuild: 0.25.9 fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 postcss: 8.5.6 - rollup: 4.52.0 + rollup: 4.52.2 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 22.18.6 + '@types/node': 24.3.0 fsevents: 2.3.3 jiti: 2.4.2 lightningcss: 1.29.3 - terser: 5.40.0 + terser: 5.43.1 yaml: 2.8.1 - vite@7.1.6(@types/node@22.18.6)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1): + vite@7.1.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1): dependencies: esbuild: 0.25.9 fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 postcss: 8.5.6 - rollup: 4.52.0 + rollup: 4.52.2 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 22.18.6 + '@types/node': 24.3.0 fsevents: 2.3.3 jiti: 2.5.1 lightningcss: 1.29.3 - terser: 5.40.0 + terser: 5.43.1 yaml: 2.8.1 vitepress-plugin-llms@1.7.5: @@ -14317,7 +14475,7 @@ snapshots: transitivePeerDependencies: - supports-color - vitepress@1.6.4(@algolia/client-search@5.20.0)(@types/node@22.18.6)(change-case@5.4.4)(fuse.js@7.1.0)(jwt-decode@4.0.0)(lightningcss@1.29.3)(postcss@8.5.6)(search-insights@2.17.2)(terser@5.40.0)(typescript@5.9.2): + vitepress@1.6.4(@algolia/client-search@5.20.0)(@types/node@24.3.0)(change-case@5.4.4)(fuse.js@7.1.0)(jwt-decode@4.0.0)(lightningcss@1.29.3)(postcss@8.5.6)(search-insights@2.17.2)(terser@5.43.1)(typescript@5.9.2): dependencies: '@docsearch/css': 3.8.2 '@docsearch/js': 3.8.2(@algolia/client-search@5.20.0)(search-insights@2.17.2) @@ -14326,7 +14484,7 @@ snapshots: '@shikijs/transformers': 2.1.0 '@shikijs/types': 2.3.2 '@types/markdown-it': 14.1.2 - '@vitejs/plugin-vue': 5.2.4(vite@5.4.20(@types/node@22.18.6)(lightningcss@1.29.3)(terser@5.40.0))(vue@3.5.21(typescript@5.9.2)) + '@vitejs/plugin-vue': 5.2.4(vite@5.4.19(@types/node@24.3.0)(lightningcss@1.29.3)(terser@5.43.1))(vue@3.5.21(typescript@5.9.2)) '@vue/devtools-api': 7.7.6 '@vue/shared': 3.5.18 '@vueuse/core': 12.5.0(typescript@5.9.2) @@ -14335,7 +14493,7 @@ snapshots: mark.js: 8.11.1 minisearch: 7.1.1 shiki: 2.3.2 - vite: 5.4.20(@types/node@22.18.6)(lightningcss@1.29.3)(terser@5.40.0) + vite: 5.4.19(@types/node@24.3.0)(lightningcss@1.29.3)(terser@5.43.1) vue: 3.5.21(typescript@5.9.2) optionalDependencies: postcss: 8.5.6 @@ -14366,11 +14524,11 @@ snapshots: - typescript - universal-cookie - vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.18.6)(@vitest/ui@3.2.4)(happy-dom@18.0.1)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(@vitest/ui@3.2.4)(happy-dom@18.0.1)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.1.6(@types/node@22.18.6)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1)) + '@vitest/mocker': 3.2.4(vite@7.1.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -14388,12 +14546,12 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.1.6(@types/node@22.18.6)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1) - vite-node: 3.2.4(@types/node@22.18.6)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.40.0)(yaml@2.8.1) + vite: 7.1.6(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1) + vite-node: 3.2.4(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 - '@types/node': 22.18.6 + '@types/node': 24.3.0 '@vitest/ui': 3.2.4(vitest@3.2.4) happy-dom: 18.0.1 transitivePeerDependencies: @@ -14428,16 +14586,21 @@ snapshots: dependencies: vue: 3.5.21(typescript@5.9.2) - vue-router-mock@2.0.0(vue-router@4.5.1(vue@3.5.21(typescript@5.9.2)))(vue@3.5.21(typescript@5.9.2)): + vue-router-mock@2.0.0(vue-router@https://pkg.pr.new/vue-router@4f1a37a(vue@3.5.21(typescript@5.9.2)))(vue@3.5.21(typescript@5.9.2)): dependencies: vue: 3.5.21(typescript@5.9.2) - vue-router: 4.5.1(vue@3.5.21(typescript@5.9.2)) + vue-router: https://pkg.pr.new/vue-router@4f1a37a(vue@3.5.21(typescript@5.9.2)) vue-router@4.5.1(vue@3.5.21(typescript@5.9.2)): dependencies: '@vue/devtools-api': 6.6.4 vue: 3.5.21(typescript@5.9.2) + vue-router@https://pkg.pr.new/vue-router@4f1a37a(vue@3.5.21(typescript@5.9.2)): + dependencies: + '@vue/devtools-api': 6.6.4 + vue: 3.5.21(typescript@5.9.2) + vue-tsc@2.2.10(typescript@5.9.2): dependencies: '@volar/typescript': 2.4.23 @@ -14504,9 +14667,9 @@ snapshots: '@webassemblyjs/wasm-parser': 1.14.1 acorn: 8.15.0 acorn-import-phases: 1.0.4(acorn@8.15.0) - browserslist: 4.25.0 + browserslist: 4.25.2 chrome-trace-event: 1.0.4 - enhanced-resolve: 5.18.1 + enhanced-resolve: 5.18.3 es-module-lexer: 1.7.0 eslint-scope: 5.1.1 events: 3.3.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 09b39bb23..016b9ff5e 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,5 +1,6 @@ packages: - playground + - playground-experimental - examples/* ignoredBuiltDependencies: - '@firebase/util' diff --git a/src/codegen/__snapshots__/generateRouteRecords.spec.ts.snap b/src/codegen/__snapshots__/generateRouteRecords.spec.ts.snap index 9005ff119..145d26c78 100644 --- a/src/codegen/__snapshots__/generateRouteRecords.spec.ts.snap +++ b/src/codegen/__snapshots__/generateRouteRecords.spec.ts.snap @@ -654,7 +654,7 @@ exports[`generateRouteRecord > route block > adds meta data 1`] = ` meta: { "auth": true, "title": "Home" - } + }, } ]" `; @@ -672,7 +672,7 @@ exports[`generateRouteRecord > route block > handles named views with empty rout meta: { "auth": true, "title": "Home" - } + }, } ]" `; @@ -695,7 +695,7 @@ exports[`generateRouteRecord > route block > merges deep meta properties 1`] = ` 3 ] } - } + }, } ]" `; @@ -710,7 +710,7 @@ exports[`generateRouteRecord > route block > merges multiple meta properties 1`] meta: { "one": true, "two": true - } + }, } ]" `; diff --git a/src/codegen/generateDTS.ts b/src/codegen/generateDTS.ts index 09a6e2b8c..f61df7fae 100644 --- a/src/codegen/generateDTS.ts +++ b/src/codegen/generateDTS.ts @@ -19,11 +19,14 @@ export function generateDTS({ routesModule, routeNamedMap, routeFileInfoMap, + paramsTypesDeclaration, + customParamsType, }: { - vueRouterModule: string routesModule: string routeNamedMap: string routeFileInfoMap: string + paramsTypesDeclaration: string + customParamsType: string }) { return ts` /* eslint-disable */ @@ -33,6 +36,18 @@ export function generateDTS({ // It's recommended to commit this file. // Make sure to add this file to your tsconfig.json file as an "includes" or "files" entry. +${ + paramsTypesDeclaration + ? ` +// Custom route params parsers +${paramsTypesDeclaration} + +`.trimStart() + : '' +}declare module 'vue-router/auto-resolver' { + export type ParamParserCustom = ${customParamsType} +} + declare module '${routesModule}' { import type { RouteRecordInfo, diff --git a/src/codegen/generateParamParsers.spec.ts b/src/codegen/generateParamParsers.spec.ts new file mode 100644 index 000000000..9a36879d0 --- /dev/null +++ b/src/codegen/generateParamParsers.spec.ts @@ -0,0 +1,570 @@ +import { describe, expect, it } from 'vitest' +import { + warnMissingParamParsers, + generateParamParsersTypesDeclarations, + generateParamsTypes, + generateParamParserOptions, + generatePathParamsOptions, + generateParamParserCustomType, + type ParamParsersMap, +} from './generateParamParsers' +import { PrefixTree } from '../core/tree' +import { resolveOptions } from '../options' +import { ImportsMap } from '../core/utils' +import type { TreePathParam } from '../core/treeNodeValue' +import { mockWarn } from '../../tests/vitest-mock-warn' + +const DEFAULT_OPTIONS = resolveOptions({}) + +describe('warnMissingParamParsers', () => { + mockWarn() + it('shows no warnings for routes without param parsers', () => { + const tree = new PrefixTree(DEFAULT_OPTIONS) + tree.insert('users', 'users.vue') + tree.insert('posts/[id]', 'posts/[id].vue') + + const paramParsers: ParamParsersMap = new Map() + + warnMissingParamParsers(tree, paramParsers) + }) + + it('shows no warnings for native parsers', () => { + const tree = new PrefixTree(DEFAULT_OPTIONS) + tree.insert('users/[id=int]', 'users/[id=int].vue') + tree.insert('posts/[active=bool]', 'posts/[active=bool].vue') + + const paramParsers: ParamParsersMap = new Map() + + warnMissingParamParsers(tree, paramParsers) + }) + + it('warns for missing custom parsers', () => { + const tree = new PrefixTree(DEFAULT_OPTIONS) + tree.insert('users/[id=uuid]', 'users/[id=uuid].vue') + + const paramParsers: ParamParsersMap = new Map() + + warnMissingParamParsers(tree, paramParsers) + + expect( + 'Parameter parser "uuid" not found for route "/users/:id".' + ).toHaveBeenWarned() + }) + + it('shows no warnings when custom parsers exist in map', () => { + const tree = new PrefixTree(DEFAULT_OPTIONS) + tree.insert('users/[id=uuid]', 'users/[id=uuid].vue') + + const paramParsers: ParamParsersMap = new Map([ + [ + 'uuid', + { + name: 'uuid', + typeName: 'Param_uuid', + relativePath: 'parsers/uuid', + absolutePath: '/path/to/parsers/uuid', + }, + ], + ]) + + warnMissingParamParsers(tree, paramParsers) + }) +}) + +describe('generateParamParsersTypesDeclarations', () => { + it('returns empty string for empty param parsers map', () => { + const paramParsers: ParamParsersMap = new Map() + const result = generateParamParsersTypesDeclarations(paramParsers) + expect(result).toBe('') + }) + + it('generates single param parser type declaration', () => { + const paramParsers: ParamParsersMap = new Map([ + [ + 'uuid', + { + name: 'uuid', + typeName: 'Param_uuid', + relativePath: 'parsers/uuid', + absolutePath: '/path/to/parsers/uuid', + }, + ], + ]) + + const result = generateParamParsersTypesDeclarations(paramParsers) + expect(result).toBe( + `type Param_uuid = ReturnType>` + ) + }) + + it('generates multiple param parsers type declarations', () => { + const paramParsers: ParamParsersMap = new Map([ + [ + 'uuid', + { + name: 'uuid', + typeName: 'Param_uuid', + relativePath: 'parsers/uuid', + absolutePath: '/path/to/parsers/uuid', + }, + ], + [ + 'slug', + { + name: 'slug', + typeName: 'Param_slug', + relativePath: 'parsers/slug', + absolutePath: '/path/to/parsers/slug', + }, + ], + ]) + + const result = generateParamParsersTypesDeclarations(paramParsers) + expect(result).toMatchInlineSnapshot(` + "type Param_uuid = ReturnType> + type Param_slug = ReturnType>" + `) + }) +}) + +describe('generateParamsTypes', () => { + it('returns null for params without parsers', () => { + const params: TreePathParam[] = [ + { + paramName: 'id', + modifier: '', + optional: false, + repeatable: false, + isSplat: false, + parser: null, + }, + ] + const paramParsers: ParamParsersMap = new Map() + + const result = generateParamsTypes(params, paramParsers) + expect(result).toEqual([null]) + }) + + it('returns correct type names for custom parsers', () => { + const params: TreePathParam[] = [ + { + paramName: 'id', + modifier: '', + optional: false, + repeatable: false, + isSplat: false, + parser: 'uuid', + }, + ] + const paramParsers: ParamParsersMap = new Map([ + [ + 'uuid', + { + name: 'uuid', + typeName: 'Param_uuid', + relativePath: 'parsers/uuid', + absolutePath: '/path/to/parsers/uuid', + }, + ], + ]) + + const result = generateParamsTypes(params, paramParsers) + expect(result).toEqual(['Param_uuid']) + }) + + it('returns correct types for native parsers', () => { + const params: TreePathParam[] = [ + { + paramName: 'id', + modifier: '', + optional: false, + repeatable: false, + isSplat: false, + parser: 'int', + }, + { + paramName: 'active', + modifier: '', + optional: false, + repeatable: false, + isSplat: false, + parser: 'bool', + }, + ] + const paramParsers: ParamParsersMap = new Map() + + const result = generateParamsTypes(params, paramParsers) + expect(result).toEqual(['number', 'boolean']) + }) + + it('handles mixed params with and without parsers', () => { + const params: TreePathParam[] = [ + { + paramName: 'id', + modifier: '', + optional: false, + repeatable: false, + isSplat: false, + parser: 'uuid', + }, + { + paramName: 'page', + modifier: '', + optional: false, + repeatable: false, + isSplat: false, + parser: null, + }, + { + paramName: 'count', + modifier: '', + optional: false, + repeatable: false, + isSplat: false, + parser: 'int', + }, + ] + const paramParsers: ParamParsersMap = new Map([ + [ + 'uuid', + { + name: 'uuid', + typeName: 'Param_uuid', + relativePath: 'parsers/uuid', + absolutePath: '/path/to/parsers/uuid', + }, + ], + ]) + + const result = generateParamsTypes(params, paramParsers) + expect(result).toEqual(['Param_uuid', null, 'number']) + }) +}) + +describe('generateParamParserOptions', () => { + it('returns empty string for param without parser', () => { + const param: TreePathParam = { + paramName: 'id', + modifier: '', + optional: false, + repeatable: false, + isSplat: false, + parser: null, + } + const importsMap = new ImportsMap() + const paramParsers: ParamParsersMap = new Map() + + const result = generateParamParserOptions(param, importsMap, paramParsers) + expect(result).toBe('') + }) + + it('generates import and returns variable for custom parser', () => { + const param: TreePathParam = { + paramName: 'id', + modifier: '', + optional: false, + repeatable: false, + isSplat: false, + parser: 'uuid', + } + const importsMap = new ImportsMap() + const paramParsers: ParamParsersMap = new Map([ + [ + 'uuid', + { + name: 'uuid', + typeName: 'Param_uuid', + relativePath: 'parsers/uuid', + absolutePath: '/path/to/parsers/uuid', + }, + ], + ]) + + const result = generateParamParserOptions(param, importsMap, paramParsers) + expect(result).toBe('PARAM_PARSER__uuid') + expect(importsMap.toString()).toContain( + `import { parser as PARAM_PARSER__uuid } from '/path/to/parsers/uuid'` + ) + }) + + it('generates correct import for native int parser', () => { + const param: TreePathParam = { + paramName: 'id', + modifier: '', + optional: false, + repeatable: false, + isSplat: false, + parser: 'int', + } + const importsMap = new ImportsMap() + const paramParsers: ParamParsersMap = new Map() + + const result = generateParamParserOptions(param, importsMap, paramParsers) + expect(result).toBe('PARAM_PARSER_INT') + expect(importsMap.toString()).toContain( + `import { PARAM_PARSER_INT } from 'vue-router/experimental'` + ) + }) + + it('generates correct import for native bool parser', () => { + const param: TreePathParam = { + paramName: 'active', + modifier: '', + optional: false, + repeatable: false, + isSplat: false, + parser: 'bool', + } + const importsMap = new ImportsMap() + const paramParsers: ParamParsersMap = new Map() + + const result = generateParamParserOptions(param, importsMap, paramParsers) + expect(result).toBe('PARAM_PARSER_BOOL') + expect(importsMap.toString()).toContain( + `import { PARAM_PARSER_BOOL } from 'vue-router/experimental'` + ) + }) + + it('returns empty string for missing parser', () => { + const param: TreePathParam = { + paramName: 'id', + modifier: '', + optional: false, + repeatable: false, + isSplat: false, + parser: 'missing', + } + const importsMap = new ImportsMap() + const paramParsers: ParamParsersMap = new Map() + + const result = generateParamParserOptions(param, importsMap, paramParsers) + expect(result).toBe('') + }) +}) + +describe('generatePathParamsOptions', () => { + it('returns empty object for empty params array', () => { + const params: TreePathParam[] = [] + const importsMap = new ImportsMap() + const paramParsers: ParamParsersMap = new Map() + + const result = generatePathParamsOptions(params, importsMap, paramParsers) + expect(result).toBe(`{}`) + }) + + it('generates options for single param with parser', () => { + const params: TreePathParam[] = [ + { + paramName: 'id', + modifier: '', + optional: false, + repeatable: false, + isSplat: false, + parser: 'int', + }, + ] + const importsMap = new ImportsMap() + const paramParsers: ParamParsersMap = new Map() + + const result = generatePathParamsOptions(params, importsMap, paramParsers) + expect(result).toContain('id: [PARAM_PARSER_INT]') + }) + + it('generates options for param without parser', () => { + const params: TreePathParam[] = [ + { + paramName: 'slug', + modifier: '', + optional: false, + repeatable: false, + isSplat: false, + parser: null, + }, + ] + const importsMap = new ImportsMap() + const paramParsers: ParamParsersMap = new Map() + + const result = generatePathParamsOptions(params, importsMap, paramParsers) + expect(result).toContain('slug: [/* no parser */]') + }) + + it('includes repeatable and optional flags when present', () => { + const params: TreePathParam[] = [ + { + paramName: 'tags', + modifier: '+', + optional: false, + repeatable: true, + isSplat: false, + parser: null, + }, + { + paramName: 'category', + modifier: '?', + optional: true, + repeatable: false, + isSplat: false, + parser: null, + }, + ] + const importsMap = new ImportsMap() + const paramParsers: ParamParsersMap = new Map() + + const result = generatePathParamsOptions(params, importsMap, paramParsers) + expect(result).toContain('tags: [/* no parser */, /* repeatable: */ true]') + expect(result).toContain( + 'category: [/* no parser */, /* repeatable: false */, /* optional: */ true]' + ) + }) + + it('handles multiple params with different configurations', () => { + const params: TreePathParam[] = [ + { + paramName: 'id', + modifier: '', + optional: false, + repeatable: false, + isSplat: false, + parser: 'uuid', + }, + { + paramName: 'page', + modifier: '?', + optional: true, + repeatable: false, + isSplat: false, + parser: 'int', + }, + { + paramName: 'tags', + modifier: '+', + optional: false, + repeatable: true, + isSplat: false, + parser: null, + }, + ] + const importsMap = new ImportsMap() + const paramParsers: ParamParsersMap = new Map([ + [ + 'uuid', + { + name: 'uuid', + typeName: 'Param_uuid', + relativePath: 'parsers/uuid', + absolutePath: '/path/to/parsers/uuid', + }, + ], + ]) + + const result = generatePathParamsOptions(params, importsMap, paramParsers) + expect(result).toContain('id: [PARAM_PARSER__uuid]') + expect(result).toContain( + 'page: [PARAM_PARSER_INT, /* repeatable: false */, /* optional: */ true]' + ) + expect(result).toContain('tags: [/* no parser */, /* repeatable: */ true]') + }) +}) + +describe('generateParamParserCustomType', () => { + it('returns never for empty param parsers map', () => { + const paramParsers: ParamParsersMap = new Map() + const result = generateParamParserCustomType(paramParsers) + expect(result).toBe('never') + }) + + it('returns single quoted parser name for one parser', () => { + const paramParsers: ParamParsersMap = new Map([ + [ + 'date', + { + name: 'date', + typeName: 'Param_date', + relativePath: 'parsers/date', + absolutePath: '/path/to/parsers/date', + }, + ], + ]) + + const result = generateParamParserCustomType(paramParsers) + expect(result).toBe("'date'") + }) + + it('returns union of quoted parser names for multiple parsers in alphabetical order on separate lines', () => { + const paramParsers: ParamParsersMap = new Map([ + [ + 'uuid', + { + name: 'uuid', + typeName: 'Param_uuid', + relativePath: 'parsers/uuid', + absolutePath: '/path/to/parsers/uuid', + }, + ], + [ + 'date', + { + name: 'date', + typeName: 'Param_date', + relativePath: 'parsers/date', + absolutePath: '/path/to/parsers/date', + }, + ], + ]) + + const result = generateParamParserCustomType(paramParsers) + expect(result).toBe(" | 'date'\n | 'uuid'") + }) + + it('handles parser names with special characters correctly', () => { + const paramParsers: ParamParsersMap = new Map([ + [ + 'custom-parser', + { + name: 'custom-parser', + typeName: 'Param_custom-parser', + relativePath: 'parsers/custom-parser', + absolutePath: '/path/to/parsers/custom-parser', + }, + ], + ]) + + const result = generateParamParserCustomType(paramParsers) + expect(result).toBe("'custom-parser'") + }) + + it('formats multiple parsers with proper indentation for three or more types', () => { + const paramParsers: ParamParsersMap = new Map([ + [ + 'uuid', + { + name: 'uuid', + typeName: 'Param_uuid', + relativePath: 'parsers/uuid', + absolutePath: '/path/to/parsers/uuid', + }, + ], + [ + 'date', + { + name: 'date', + typeName: 'Param_date', + relativePath: 'parsers/date', + absolutePath: '/path/to/parsers/date', + }, + ], + [ + 'slug', + { + name: 'slug', + typeName: 'Param_slug', + relativePath: 'parsers/slug', + absolutePath: '/path/to/parsers/slug', + }, + ], + ]) + + const result = generateParamParserCustomType(paramParsers) + expect(result).toBe(" | 'date'\n | 'slug'\n | 'uuid'") + }) +}) diff --git a/src/codegen/generateParamParsers.ts b/src/codegen/generateParamParsers.ts new file mode 100644 index 000000000..ef56e0e76 --- /dev/null +++ b/src/codegen/generateParamParsers.ts @@ -0,0 +1,136 @@ +import { TreePathParam, TreeQueryParam } from '../core/treeNodeValue' +import { ImportsMap } from '../core/utils' +import { PrefixTree } from '../core/tree' + +export type ParamParsersMap = Map< + string, + { + name: string + typeName: `Param_${string}` + relativePath: string + absolutePath: string + } +> + +// just for type strictness +const _NATIVE_PARAM_PARSERS = ['int', 'bool'] as const +const NATIVE_PARAM_PARSERS = _NATIVE_PARAM_PARSERS as readonly string[] +const NATIVE_PARAM_PARSERS_TYPES = { + int: 'number', + bool: 'boolean', +} satisfies Record<(typeof _NATIVE_PARAM_PARSERS)[number], string> + +export function warnMissingParamParsers( + tree: PrefixTree, + paramParsers: ParamParsersMap +) { + for (const node of tree.getChildrenDeepSorted()) { + for (const param of node.params) { + if (param.parser && !paramParsers.has(param.parser)) { + if (!NATIVE_PARAM_PARSERS.includes(param.parser)) { + console.warn( + `Parameter parser "${param.parser}" not found for route "${node.fullPath}".` + ) + } + } + } + } +} + +export function generateParamParsersTypesDeclarations( + paramParsers: ParamParsersMap +) { + return Array.from(paramParsers.values()) + .map( + ({ typeName, relativePath }) => + `type ${typeName} = ReturnType>` + ) + .join('\n') +} + +export function generateParamsTypes( + params: (TreePathParam | TreeQueryParam)[], + parparsersMap: ParamParsersMap +): Array { + return params.map((param) => { + if (param.parser) { + if (parparsersMap.has(param.parser)) { + return parparsersMap.get(param.parser)!.typeName + } else if (param.parser in NATIVE_PARAM_PARSERS_TYPES) { + return NATIVE_PARAM_PARSERS_TYPES[ + param.parser as keyof typeof NATIVE_PARAM_PARSERS_TYPES + ] + } + } + return null + }) +} + +export function generateParamParserOptions( + param: TreePathParam | TreeQueryParam, + importsMap: ImportsMap, + paramParsers: ParamParsersMap +): string { + if (!param.parser) return '' + + // we prioritize custom parsers to let users override them + if (paramParsers.has(param.parser)) { + const { name, absolutePath } = paramParsers.get(param.parser)! + const varName = `PARAM_PARSER__${name}` + importsMap.add(absolutePath, { name: 'parser', as: varName }) + return varName + } else if (NATIVE_PARAM_PARSERS.includes(param.parser)) { + const varName = `PARAM_PARSER_${param.parser.toUpperCase()}` + importsMap.add('vue-router/experimental', varName) + return varName + } + return '' +} + +export function generateParamParserCustomType( + paramParsers: ParamParsersMap +): string { + const parserNames = Array.from(paramParsers.keys()).sort() + + if (parserNames.length === 0) { + return 'never' + } + + if (parserNames.length === 1) { + return `'${parserNames[0]}'` + } + + return parserNames.map((name) => ` | '${name}'`).join('\n') +} + +export function generatePathParamsOptions( + params: TreePathParam[], + importsMap: ImportsMap, + paramParsers: ParamParsersMap +) { + const paramOptions = params.map((param) => { + // build a lean option list without any optional value + const optionList: string[] = [] + const parser = generateParamParserOptions(param, importsMap, paramParsers) + optionList.push(parser || `/* no parser */`) + if (param.optional || param.repeatable) { + optionList.push( + `/* repeatable: ` + (param.repeatable ? `*/ true` : `false */`) + ) + } + if (param.optional) { + optionList.push( + `/* optional: ` + (param.optional ? `*/ true` : `false */`) + ) + } + return ` +${param.paramName}: [${optionList.join(', ')}], +`.slice(1, -1) + }) + + return paramOptions.length === 0 + ? '{}' + : `{ + ${paramOptions.join('\n ')} + }` +} diff --git a/src/codegen/generateRouteMap.spec.ts b/src/codegen/generateRouteMap.spec.ts index 99f517615..43ecaeba2 100644 --- a/src/codegen/generateRouteMap.spec.ts +++ b/src/codegen/generateRouteMap.spec.ts @@ -19,7 +19,9 @@ describe('generateRouteNamedMap', () => { tree.insert('a', 'a.vue') tree.insert('b', 'b.vue') tree.insert('c', 'c.vue') - expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(` + expect( + formatExports(generateRouteNamedMap(tree, DEFAULT_OPTIONS, new Map())) + ).toMatchInlineSnapshot(` "export interface RouteNamedMap { '/': RouteRecordInfo<'/', '/', Record, Record>, '/a': RouteRecordInfo<'/a', '/a', Record, Record>, @@ -40,7 +42,9 @@ describe('generateRouteNamedMap', () => { tree.insert('[...a]', '[...a].vue') // splat tree.insert('[[...a]]', '[[...a]].vue') // splat tree.insert('[[...a]]+', '[[...a]]+.vue') // splat - expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(` + expect( + formatExports(generateRouteNamedMap(tree, DEFAULT_OPTIONS, new Map())) + ).toMatchInlineSnapshot(` "export interface RouteNamedMap { '/[a]': RouteRecordInfo<'/[a]', '/:a', { a: ParamValue }, { a: ParamValue }>, '/[[a]]': RouteRecordInfo<'/[[a]]', '/:a?', { a?: ParamValueZeroOrOne }, { a?: ParamValueZeroOrOne }>, @@ -61,7 +65,9 @@ describe('generateRouteNamedMap', () => { const b = tree.insertParsedPath(':b()', 'a.vue') expect(a.name).toBe('/:a') expect(b.name).toBe('/:b()') - expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(` + expect( + formatExports(generateRouteNamedMap(tree, DEFAULT_OPTIONS, new Map())) + ).toMatchInlineSnapshot(` "export interface RouteNamedMap { '/:a': RouteRecordInfo<'/:a', '/:a', { a: ParamValue }, { a: ParamValue }>, '/:b()': RouteRecordInfo<'/:b()', '/:b()', { b: ParamValue }, { b: ParamValue }>, @@ -75,7 +81,9 @@ describe('generateRouteNamedMap', () => { tree.insert('n/[a]/other', 'n/[a]/other.vue') tree.insert('n/[a]/[b]', 'n/[a]/[b].vue') tree.insert('n/[a]/[c]/other-[d]', 'n/[a]/[c]/other-[d].vue') - expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(` + expect( + formatExports(generateRouteNamedMap(tree, DEFAULT_OPTIONS, new Map())) + ).toMatchInlineSnapshot(` "export interface RouteNamedMap { '/n/[a]/': RouteRecordInfo<'/n/[a]/', '/n/:a', { a: ParamValue }, { a: ParamValue }>, '/n/[a]/[b]': RouteRecordInfo<'/n/[a]/[b]', '/n/:a/:b', { a: ParamValue, b: ParamValue }, { a: ParamValue, b: ParamValue }>, @@ -93,7 +101,9 @@ describe('generateRouteNamedMap', () => { tree.insert('n/[a]+', 'n/[a]+.vue') // repeated tree.insert('n/[[a]]+', 'n/[[a]]+.vue') // optional repeated tree.insert('n/[...a]', 'n/[...a].vue') // splat - expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(` + expect( + formatExports(generateRouteNamedMap(tree, DEFAULT_OPTIONS, new Map())) + ).toMatchInlineSnapshot(` "export interface RouteNamedMap { '/n/[a]': RouteRecordInfo<'/n/[a]', '/n/:a', { a: ParamValue }, { a: ParamValue }>, '/n/[[a]]': RouteRecordInfo<'/n/[[a]]', '/n/:a?', { a?: ParamValueZeroOrOne }, { a?: ParamValueZeroOrOne }>, @@ -115,7 +125,9 @@ describe('generateRouteNamedMap', () => { tree.insert('[lang]/a', 'src/pages/a.vue') tree.insert('[lang]/[id]', 'src/pages/[id].vue') - expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(` + expect( + formatExports(generateRouteNamedMap(tree, DEFAULT_OPTIONS, new Map())) + ).toMatchInlineSnapshot(` "export interface RouteNamedMap { '/[lang]/': RouteRecordInfo<'/[lang]/', '/:lang', { lang: ParamValue }, { lang: ParamValue }>, '/[lang]/[id]': RouteRecordInfo<'/[lang]/[id]', '/:lang/:id', { lang: ParamValue, id: ParamValue }, { lang: ParamValue, id: ParamValue }>, @@ -134,7 +146,9 @@ describe('generateRouteNamedMap', () => { tree.insert('b/d', 'b/d.vue') tree.insert('c', 'c.vue') tree.insert('d', 'd.vue') - expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(` + expect( + formatExports(generateRouteNamedMap(tree, DEFAULT_OPTIONS, new Map())) + ).toMatchInlineSnapshot(` "export interface RouteNamedMap { '/a/a': RouteRecordInfo<'/a/a', '/a/a', Record, Record>, '/a/b': RouteRecordInfo<'/a/b', '/a/b', Record, Record>, @@ -154,7 +168,9 @@ describe('generateRouteNamedMap', () => { tree.insert('a/index', 'a/index.vue') tree.insert('a/[id]', 'a/[id].vue') tree.insert('a/[id]/index', 'a/[id]/index.vue') - expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(` + expect( + formatExports(generateRouteNamedMap(tree, DEFAULT_OPTIONS, new Map())) + ).toMatchInlineSnapshot(` "export interface RouteNamedMap { '/a': RouteRecordInfo<'/a', '/a', Record, Record, '/a/' | '/a/[id]' | '/a/[id]/'>, '/a/': RouteRecordInfo<'/a/', '/a', Record, Record>, @@ -170,7 +186,9 @@ describe('generateRouteNamedMap', () => { const child = tree.insert('parent/child', 'parent/child.vue') parent.value.setOverride('parent', { path: '/' }) expect(child.fullPath).toBe('/child') - expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(` + expect( + formatExports(generateRouteNamedMap(tree, DEFAULT_OPTIONS, new Map())) + ).toMatchInlineSnapshot(` "export interface RouteNamedMap { '/parent': RouteRecordInfo<'/parent', '/', Record, Record, '/parent/child'>, '/parent/child': RouteRecordInfo<'/parent/child', '/child', Record, Record>, @@ -188,7 +206,9 @@ describe('generateRouteNamedMap', () => { 'parent/child/subchild/grandchild.vue' ) tree.insert('parent/other-child', 'parent/other-child.vue') - expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(` + expect( + formatExports(generateRouteNamedMap(tree, DEFAULT_OPTIONS, new Map())) + ).toMatchInlineSnapshot(` "export interface RouteNamedMap { '/parent': RouteRecordInfo<'/parent', '/parent', Record, Record, '/parent/child' | '/parent/child/subchild' | '/parent/child/subchild/grandchild' | '/parent/other-child'>, '/parent/child': RouteRecordInfo<'/parent/child', '/parent/child', Record, Record, '/parent/child/subchild' | '/parent/child/subchild/grandchild'>, @@ -203,7 +223,9 @@ describe('generateRouteNamedMap', () => { const tree = new PrefixTree(DEFAULT_OPTIONS) tree.insert('parent', 'parent.vue') tree.insert('parent/child/a/b/c', 'parent/child/a/b/c.vue') - expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(` + expect( + formatExports(generateRouteNamedMap(tree, DEFAULT_OPTIONS, new Map())) + ).toMatchInlineSnapshot(` "export interface RouteNamedMap { '/parent': RouteRecordInfo<'/parent', '/parent', Record, Record, '/parent/child/a/b/c'>, '/parent/child/a/b/c': RouteRecordInfo<'/parent/child/a/b/c', '/parent/child/a/b/c', Record, Record>, @@ -215,7 +237,9 @@ describe('generateRouteNamedMap', () => { const tree = new PrefixTree(DEFAULT_OPTIONS) tree.insert('parent/index', 'parent/index.vue') tree.insert('parent/child', 'parent/child.vue') - expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(` + expect( + formatExports(generateRouteNamedMap(tree, DEFAULT_OPTIONS, new Map())) + ).toMatchInlineSnapshot(` "export interface RouteNamedMap { '/parent/': RouteRecordInfo<'/parent/', '/parent', Record, Record>, '/parent/child': RouteRecordInfo<'/parent/child', '/parent/child', Record, Record>, @@ -230,7 +254,9 @@ describe('generateRouteNamedMap', () => { tree.insert('parent/a/b', 'parent/a/b.vue') tree.insert('parent/a/b/index', 'parent/a/b/index.vue') tree.insert('parent/a/b/c', 'parent/a/b/c.vue') - expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(` + expect( + formatExports(generateRouteNamedMap(tree, DEFAULT_OPTIONS, new Map())) + ).toMatchInlineSnapshot(` "export interface RouteNamedMap { '/parent/': RouteRecordInfo<'/parent/', '/parent', Record, Record>, '/parent/a/': RouteRecordInfo<'/parent/a/', '/parent/a', Record, Record>, @@ -252,7 +278,9 @@ describe('generateRouteNamedMap', () => { tree.insert('[lang]/a', 'src/pages/a.vue') tree.insert('[lang]/[id]', 'src/pages/[id].vue') - expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(` + expect( + formatExports(generateRouteNamedMap(tree, DEFAULT_OPTIONS, new Map())) + ).toMatchInlineSnapshot(` "export interface RouteNamedMap { '/[lang]/': RouteRecordInfo<'/[lang]/', '/:lang', { lang: ParamValue }, { lang: ParamValue }>, '/[lang]/[id]': RouteRecordInfo<'/[lang]/[id]', '/:lang/:id', { lang: ParamValue, id: ParamValue }, { lang: ParamValue, id: ParamValue }>, @@ -266,7 +294,9 @@ describe('generateRouteNamedMap', () => { tree.insert('(group)/a', 'a.vue') - expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(` + expect( + formatExports(generateRouteNamedMap(tree, DEFAULT_OPTIONS, new Map())) + ).toMatchInlineSnapshot(` "export interface RouteNamedMap { '/(group)/a': RouteRecordInfo<'/(group)/a', '/a', Record, Record>, }" @@ -278,7 +308,9 @@ describe('generateRouteNamedMap', () => { tree.insert('(group)/(subgroup)/c', 'c.vue') - expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(` + expect( + formatExports(generateRouteNamedMap(tree, DEFAULT_OPTIONS, new Map())) + ).toMatchInlineSnapshot(` "export interface RouteNamedMap { '/(group)/(subgroup)/c': RouteRecordInfo<'/(group)/(subgroup)/c', '/c', Record, Record>, }" @@ -290,7 +322,9 @@ describe('generateRouteNamedMap', () => { tree.insert('folder/(group)', 'folder/(group).vue') - expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(` + expect( + formatExports(generateRouteNamedMap(tree, DEFAULT_OPTIONS, new Map())) + ).toMatchInlineSnapshot(` "export interface RouteNamedMap { '/folder/(group)': RouteRecordInfo<'/folder/(group)', '/folder', Record, Record>, }" @@ -308,7 +342,9 @@ describe('generateRouteNamedMap', () => { tree.insert(route, `${route}.vue`) }) - return formatExports(generateRouteNamedMap(tree)) + return formatExports( + generateRouteNamedMap(tree, DEFAULT_OPTIONS, new Map()) + ) } // Same routes, different insertion orders @@ -340,7 +376,9 @@ describe('generateRouteNamedMap', () => { const parentNode = tree.children.get('parent')! parentNode.value.setOverride('parent', { name: '' }) - expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(` + expect( + formatExports(generateRouteNamedMap(tree, DEFAULT_OPTIONS, new Map())) + ).toMatchInlineSnapshot(` "export interface RouteNamedMap { '/child': RouteRecordInfo<'/child', '/child', Record, Record>, '/parent/child': RouteRecordInfo<'/parent/child', '/parent/child', Record, Record>, @@ -359,7 +397,9 @@ describe('generateRouteNamedMap', () => { const child2Node = tree.children.get('parent')!.children.get('child2')! child2Node.value.setOverride('parent/child2', { name: '' }) - expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(` + expect( + formatExports(generateRouteNamedMap(tree, DEFAULT_OPTIONS, new Map())) + ).toMatchInlineSnapshot(` "export interface RouteNamedMap { '/parent': RouteRecordInfo<'/parent', '/parent', Record, Record, '/parent/child1' | '/parent/child3'>, '/parent/child1': RouteRecordInfo<'/parent/child1', '/parent/child1', Record, Record>, diff --git a/src/codegen/generateRouteMap.ts b/src/codegen/generateRouteMap.ts index 0454f9908..da4908964 100644 --- a/src/codegen/generateRouteMap.ts +++ b/src/codegen/generateRouteMap.ts @@ -1,30 +1,58 @@ import type { TreeNode } from '../core/tree' -import { generateRouteParams } from './generateRouteParams' +import type { ResolvedOptions } from '../options' +import { generateParamsTypes, ParamParsersMap } from './generateParamParsers' +import { + EXPERIMENTAL_generateRouteParams, + generateRouteParams, +} from './generateRouteParams' -export function generateRouteNamedMap(node: TreeNode): string { +export function generateRouteNamedMap( + node: TreeNode, + options: ResolvedOptions, + paramParsersMap: ParamParsersMap +): string { if (node.isRoot()) { return `export interface RouteNamedMap { -${node.getChildrenSorted().map(generateRouteNamedMap).join('')}}` +${node + .getChildrenSorted() + .map((n) => generateRouteNamedMap(n, options, paramParsersMap)) + .join('')}}` } return ( // if the node has a filePath, it's a component, it has a routeName and it should be referenced in the RouteNamedMap // otherwise it should be skipped to avoid navigating to a route that doesn't render anything (node.value.components.size > 0 && node.name - ? ` '${node.name}': ${generateRouteRecordInfo(node)},\n` + ? ` '${node.name}': ${generateRouteRecordInfo(node, options, paramParsersMap)},\n` : '') + (node.children.size > 0 - ? node.getChildrenSorted().map(generateRouteNamedMap).join('\n') + ? node + .getChildrenSorted() + .map((n) => generateRouteNamedMap(n, options, paramParsersMap)) + .join('\n') : '') ) } -export function generateRouteRecordInfo(node: TreeNode) { +export function generateRouteRecordInfo( + node: TreeNode, + options: ResolvedOptions, + paramParsersMap: ParamParsersMap +): string { + let paramParsers: Array = [] + + if (options.experimental.paramParsers) { + paramParsers = generateParamsTypes(node.params, paramParsersMap) + } const typeParams = [ `'${node.name}'`, `'${node.fullPath}'`, - generateRouteParams(node, true), - generateRouteParams(node, false), + options.experimental.paramParsers + ? EXPERIMENTAL_generateRouteParams(node, paramParsers, true) + : generateRouteParams(node, true), + options.experimental.paramParsers + ? EXPERIMENTAL_generateRouteParams(node, paramParsers, false) + : generateRouteParams(node, false), ] if (node.children.size > 0) { diff --git a/src/codegen/generateRouteParams.ts b/src/codegen/generateRouteParams.ts index 28e94e64d..5d85c75eb 100644 --- a/src/codegen/generateRouteParams.ts +++ b/src/codegen/generateRouteParams.ts @@ -1,8 +1,14 @@ import { TreeNode } from '../core/tree' +import { + isTreeParamOptional, + isTreeParamRepeatable, + isTreePathParam, +} from '../core/treeNodeValue' export function generateRouteParams(node: TreeNode, isRaw: boolean): string { - // node.params is a getter so we compute it once - const nodeParams = node.params + // node.pathParams is a getter so we compute it once + // this version does not support query params + const nodeParams = node.pathParams return nodeParams.length > 0 ? `{ ${nodeParams .map( @@ -21,7 +27,44 @@ export function generateRouteParams(node: TreeNode, isRaw: boolean): string { 'Record' } -// TODO: refactor to ParamValueRaw and ParamValue ? +export function EXPERIMENTAL_generateRouteParams( + node: TreeNode, + types: Array, + isRaw: boolean +) { + // node.params is a getter so we compute it once + const nodeParams = node.params + return nodeParams.length > 0 + ? `{ ${nodeParams + .map((param, i) => { + const isOptional = isTreeParamOptional(param) + const isRepeatable = isTreeParamRepeatable(param) + + const type = types[i] + + let extractedType: string + + if (type?.startsWith('Param_')) { + extractedType = `${isRepeatable ? 'Extract' : 'Exclude'}<${type}, unknown[]>` + } else { + extractedType = `${type ?? 'string'}${isRepeatable ? '[]' : ''}` + } + + extractedType += + isTreePathParam(param) && isOptional && !isRepeatable + ? ' | null' + : '' + + return `${param.paramName}${isRaw && isOptional ? '?' : ''}: ${ + extractedType + }` + }) + .join(', ')} }` + : // no params allowed + 'Record' +} + +// TODO: Remove in favor of inline types because it's easier to read /** * Utility type for raw and non raw params like :id+ diff --git a/src/codegen/generateRouteRecords.ts b/src/codegen/generateRouteRecords.ts index 3dca451b4..ed0ebc75f 100644 --- a/src/codegen/generateRouteRecords.ts +++ b/src/codegen/generateRouteRecords.ts @@ -51,9 +51,6 @@ ${node const startIndent = ' '.repeat(indent * 2) const indentStr = ' '.repeat((indent + 1) * 2) - // TODO: should meta be defined a different way to allow preserving imports? - // const meta = node.value.overrides.meta - // compute once since it's a getter const overrides = node.value.overrides @@ -145,7 +142,7 @@ ${indentStr}},` * @param importsMap - the import list to fill * @returns */ -function generatePageImport( +export function generatePageImport( filepath: string, importMode: ResolvedOptions['importMode'], importsMap: ImportsMap @@ -168,14 +165,14 @@ function generatePageImport( return importName } -function formatMeta(node: TreeNode, indent: string): string { +export function formatMeta(node: TreeNode, indent: string): string { const meta = node.meta const formatted = meta && meta .split('\n') .map((line) => indent + line) - .join('\n') + .join('\n') + ',' return formatted ? '\n' + indent + 'meta: ' + formatted.trimStart() : '' } diff --git a/src/codegen/generateRouteResolver.spec.ts b/src/codegen/generateRouteResolver.spec.ts new file mode 100644 index 000000000..2808183b2 --- /dev/null +++ b/src/codegen/generateRouteResolver.spec.ts @@ -0,0 +1,765 @@ +import { beforeEach, describe, expect, it } from 'vitest' +import { PrefixTree } from '../core/tree' +import { resolveOptions } from '../options' +import { + generateRouteResolver, + generateRouteRecord, + generateRouteRecordQuery, +} from './generateRouteResolver' +import { ImportsMap } from '../core/utils' +import { ParamParsersMap } from './generateParamParsers' + +const DEFAULT_OPTIONS = resolveOptions({}) +let DEFAULT_STATE: Parameters[0]['state'] = { + id: 0, + matchableRecords: [], +} + +beforeEach(() => { + DEFAULT_STATE = { + id: 0, + matchableRecords: [], + } +}) + +describe('generateRouteRecordQuery', () => { + let importsMap!: ImportsMap + beforeEach(() => { + importsMap = new ImportsMap() + }) + + it('returns empty string for non-matchable nodes without query params', () => { + const tree = new PrefixTree(DEFAULT_OPTIONS) + const node = tree.insert('a/b', 'a/b.vue').parent! // non-matchable parent + expect( + generateRouteRecordQuery({ importsMap, node, paramParsersMap: new Map() }) + ).toBe('') + }) + + it('returns empty string when no query params in a matchable node', () => { + const node = new PrefixTree(DEFAULT_OPTIONS).insert('a', 'a.vue') + expect( + generateRouteRecordQuery({ importsMap, node, paramParsersMap: new Map() }) + ).toBe('') + }) + + it('generates query params for non-matchable nodes when they have query params', () => { + const tree = new PrefixTree(DEFAULT_OPTIONS) + const node = tree.insert('a/b', 'a/b.vue').parent! // non-matchable parent + // Add query params to the non-matchable parent + node.value.setEditOverride('params', { + query: { search: {} }, + }) + expect( + generateRouteRecordQuery({ importsMap, node, paramParsersMap: new Map() }) + ).toMatchInlineSnapshot(` + "query: [ + new MatcherPatternQueryParam('search', 'search', 'value') + ]," + `) + }) + + it('does not includes query params from parent nodes', () => { + const tree = new PrefixTree(DEFAULT_OPTIONS) + const parentNode = tree.insert('parent', 'parent.vue') + const childNode = tree.insert('parent/child', 'parent/child.vue') + + // Add query params to parent + parentNode.value.setEditOverride('params', { + query: { + parentParam: {}, + }, + }) + + // Add query params to child + childNode.value.setEditOverride('params', { + query: { + childParam: { parser: 'int' }, + }, + }) + + expect( + generateRouteRecordQuery({ + importsMap, + node: childNode, + paramParsersMap: new Map(), + }) + ).toMatchInlineSnapshot(` + "query: [ + new MatcherPatternQueryParam('childParam', 'childParam', 'value', PARAM_PARSER_INT) + ]," + `) + }) + + it('generates query property with single query param', () => { + const node = new PrefixTree(DEFAULT_OPTIONS).insert('a', 'a.vue') + // Mock the queryParams getter + node.value.setEditOverride('params', { + query: { search: {} }, + }) + expect( + generateRouteRecordQuery({ importsMap, node, paramParsersMap: new Map() }) + ).toMatchInlineSnapshot(` + "query: [ + new MatcherPatternQueryParam('search', 'search', 'value') + ]," + `) + }) + + it('generates query property with multiple query params', () => { + const node = new PrefixTree(DEFAULT_OPTIONS).insert('a', 'a.vue') + node.value.setEditOverride('params', { + query: { + search: {}, + page: { parser: 'int' }, + active: { parser: 'bool' }, + }, + }) + expect( + generateRouteRecordQuery({ importsMap, node, paramParsersMap: new Map() }) + ).toMatchInlineSnapshot(` + "query: [ + new MatcherPatternQueryParam('search', 'search', 'value'), + new MatcherPatternQueryParam('page', 'page', 'value', PARAM_PARSER_INT), + new MatcherPatternQueryParam('active', 'active', 'value', PARAM_PARSER_BOOL) + ]," + `) + }) + + it('adds MatcherPatternQueryParam import', () => { + const node = new PrefixTree(DEFAULT_OPTIONS).insert('a', 'a.vue') + node.value.setEditOverride('params', { + query: { search: {} }, + }) + + generateRouteRecordQuery({ importsMap, node, paramParsersMap: new Map() }) + + expect( + importsMap.has('vue-router/experimental', 'MatcherPatternQueryParam') + ).toBe(true) + }) + + it('generates query param with format "value"', () => { + const node = new PrefixTree(DEFAULT_OPTIONS).insert('a', 'a.vue') + node.value.setEditOverride('params', { + query: { search: { format: 'value' } }, + }) + expect( + generateRouteRecordQuery({ importsMap, node, paramParsersMap: new Map() }) + ).toMatchInlineSnapshot(` + "query: [ + new MatcherPatternQueryParam('search', 'search', 'value') + ]," + `) + }) + + it('generates query param with format "array"', () => { + const node = new PrefixTree(DEFAULT_OPTIONS).insert('a', 'a.vue') + node.value.setEditOverride('params', { + query: { tags: { format: 'array' } }, + }) + expect( + generateRouteRecordQuery({ importsMap, node, paramParsersMap: new Map() }) + ).toMatchInlineSnapshot(` + "query: [ + new MatcherPatternQueryParam('tags', 'tags', 'array') + ]," + `) + }) + + it('generates query param with default value', () => { + const node = new PrefixTree(DEFAULT_OPTIONS).insert('a', 'a.vue') + node.value.setEditOverride('params', { + query: { limit: { default: '10' } }, + }) + expect( + generateRouteRecordQuery({ importsMap, node, paramParsersMap: new Map() }) + ).toMatchInlineSnapshot(` + "query: [ + new MatcherPatternQueryParam('limit', 'limit', 'value', {}, 10) + ]," + `) + }) + + it('generates query param with format and default value', () => { + const node = new PrefixTree(DEFAULT_OPTIONS).insert('a', 'a.vue') + node.value.setEditOverride('params', { + query: { page: { parser: 'int', format: 'array', default: '1' } }, + }) + expect( + generateRouteRecordQuery({ importsMap, node, paramParsersMap: new Map() }) + ).toMatchInlineSnapshot(` + "query: [ + new MatcherPatternQueryParam('page', 'page', 'array', PARAM_PARSER_INT, 1) + ]," + `) + }) + + it('generates mixed query params with different configurations', () => { + const node = new PrefixTree(DEFAULT_OPTIONS).insert('a', 'a.vue') + node.value.setEditOverride('params', { + query: { + q: { format: 'value' }, + tags: { format: 'array' }, + limit: { parser: 'int', default: '20' }, + active: { default: 'true' }, + }, + }) + expect( + generateRouteRecordQuery({ importsMap, node, paramParsersMap: new Map() }) + ).toMatchInlineSnapshot(` + "query: [ + new MatcherPatternQueryParam('q', 'q', 'value'), + new MatcherPatternQueryParam('tags', 'tags', 'array'), + new MatcherPatternQueryParam('limit', 'limit', 'value', PARAM_PARSER_INT, 20), + new MatcherPatternQueryParam('active', 'active', 'value', {}, true) + ]," + `) + }) +}) + +describe('generateRouteRecord', () => { + it('serializes a simple static path', () => { + const tree = new PrefixTree(DEFAULT_OPTIONS) + const importsMap = new ImportsMap() + const paramParsersMap: ParamParsersMap = new Map() + expect( + generateRouteRecord({ + node: tree.insert('a', 'a.vue'), + parentVar: null, + state: DEFAULT_STATE, + options: DEFAULT_OPTIONS, + importsMap, + paramParsersMap, + }) + ).toMatchInlineSnapshot(` + "const r_0 = normalizeRouteRecord({ + name: '/a', + path: new MatcherPatternPathStatic('/a'), + components: { + 'default': () => import('a.vue') + }, + })" + `) + expect( + generateRouteRecord({ + node: tree.insert('a/b/c', 'a/b/c.vue'), + parentVar: null, + state: DEFAULT_STATE, + options: DEFAULT_OPTIONS, + importsMap, + paramParsersMap, + }) + ).toMatchInlineSnapshot(` + "const r_1 = normalizeRouteRecord({ + name: '/a/b/c', + path: new MatcherPatternPathStatic('/a/b/c'), + components: { + 'default': () => import('a/b/c.vue') + }, + })" + `) + }) +}) + +describe('generateRouteResolver', () => { + it('generates a resolver for a simple tree', () => { + const tree = new PrefixTree(DEFAULT_OPTIONS) + const importsMap = new ImportsMap() + tree.insert('a', 'a.vue') + tree.insert('b/c', 'b/c.vue') + tree.insert('b/c/d', 'b/c/d.vue') + tree.insert('b/e/f', 'b/c/f.vue') + const resolver = generateRouteResolver( + tree, + DEFAULT_OPTIONS, + importsMap, + new Map() + ) + + expect(resolver).toMatchInlineSnapshot(` + " + const r_0 = normalizeRouteRecord({ + name: '/a', + path: new MatcherPatternPathStatic('/a'), + components: { + 'default': () => import('a.vue') + }, + }) + + const r_1 = normalizeRouteRecord({ + name: '/b/c', + path: new MatcherPatternPathStatic('/b/c'), + components: { + 'default': () => import('b/c.vue') + }, + }) + const r_2 = normalizeRouteRecord({ + name: '/b/c/d', + path: new MatcherPatternPathStatic('/b/c/d'), + components: { + 'default': () => import('b/c/d.vue') + }, + parent: r_1, + }) + const r_3 = normalizeRouteRecord({ + name: '/b/e/f', + path: new MatcherPatternPathStatic('/b/e/f'), + components: { + 'default': () => import('b/c/f.vue') + }, + }) + + export const resolver = createFixedResolver([ + r_2, // /b/c/d + r_3, // /b/e/f + r_1, // /b/c + r_0, // /a + ]) + " + `) + }) + + it('generates correct nested layouts', () => { + const tree = new PrefixTree(DEFAULT_OPTIONS) + const importsMap = new ImportsMap() + tree.insert('a', 'a.vue') + tree.insert('a/(a-home)', 'a/(a-home).vue') + tree.insert('a/b', 'a/b.vue') + tree.insert('a/b/c', 'a/b/c.vue') + tree.insert('a/b/(b-home)', 'a/b/(b-home).vue') + tree.insert('a/d', 'a/d.vue') + tree.insert('a/b/e', 'a/b/e.vue') + + const resolver = generateRouteResolver( + tree, + DEFAULT_OPTIONS, + importsMap, + new Map() + ) + + // FIXME: there are conflicting paths here. The order is correct as nested routes appear higher but + // it should appeand a trailing slash to the children route or the parent + // Adding it to the parent makes the routing stable but also inconsistent trailing slash + // I think it's better to not have a stable routing to preserve stable trailing slash + + expect(resolver).toMatchInlineSnapshot(` + " + const r_0 = normalizeRouteRecord({ + name: '/a', + path: new MatcherPatternPathStatic('/a'), + components: { + 'default': () => import('a.vue') + }, + }) + const r_1 = normalizeRouteRecord({ + name: '/a/(a-home)', + path: new MatcherPatternPathStatic('/a'), + components: { + 'default': () => import('a/(a-home).vue') + }, + parent: r_0, + }) + const r_2 = normalizeRouteRecord({ + name: '/a/b', + path: new MatcherPatternPathStatic('/a/b'), + components: { + 'default': () => import('a/b.vue') + }, + parent: r_0, + }) + const r_3 = normalizeRouteRecord({ + name: '/a/b/(b-home)', + path: new MatcherPatternPathStatic('/a/b'), + components: { + 'default': () => import('a/b/(b-home).vue') + }, + parent: r_2, + }) + const r_4 = normalizeRouteRecord({ + name: '/a/b/c', + path: new MatcherPatternPathStatic('/a/b/c'), + components: { + 'default': () => import('a/b/c.vue') + }, + parent: r_2, + }) + const r_5 = normalizeRouteRecord({ + name: '/a/b/e', + path: new MatcherPatternPathStatic('/a/b/e'), + components: { + 'default': () => import('a/b/e.vue') + }, + parent: r_2, + }) + const r_6 = normalizeRouteRecord({ + name: '/a/d', + path: new MatcherPatternPathStatic('/a/d'), + components: { + 'default': () => import('a/d.vue') + }, + parent: r_0, + }) + + export const resolver = createFixedResolver([ + r_3, // /a/b + r_4, // /a/b/c + r_5, // /a/b/e + r_1, // /a + r_2, // /a/b + r_6, // /a/d + r_0, // /a + ]) + " + `) + }) + + describe('route prioritization in resolver', () => { + function getRouteOrderFromResolver(tree: PrefixTree): string[] { + const resolver = generateRouteResolver( + tree, + DEFAULT_OPTIONS, + new ImportsMap(), + new Map() + ) + + // Extract the order from the resolver output + const lines = resolver.split('\n').filter((line) => line.includes('// /')) + return lines.map((line) => line.split('// ')[1] || '') + } + + it('orders records based on specificity of paths', () => { + const tree = new PrefixTree(DEFAULT_OPTIONS) + // static at root + tree.insert('prefix', 'prefix.vue') + + // params in the end + tree.insert('prefix/sub-end', 'prefix/sub-end.vue') + tree.insert('prefix/sub-[id]', 'prefix/sub-[id].vue') + // repeat can only be the whole segment + // tree.insert('prefix/sub-[repeat]+', 'prefix/sub-[repeat]+.vue') + tree.insert('prefix/sub-[[opt]]', 'prefix/sub-[[opt]].vue') + // repeat can only be the whole segment + // tree.insert('prefix/sub-[[optRepeat]]+', 'prefix/c/d.vue') + tree.insert('prefix/[id]', 'prefix/[id].vue') + tree.insert('prefix/[repeat]+', 'prefix/[repeat]+.vue') + tree.insert('prefix/[[opt]]', 'prefix/[[opt]].vue') + tree.insert('prefix/[[optRepeat]]+', 'prefix/[[optRepeat]]+.vue') + tree.insert('[...splat]', '[...splat].vue') + + // params at root level + tree.insert('[id]', '[id].vue') + tree.insert('[[optional]]', '[[optional]].vue') + tree.insert('prefix-[id]-suffix', 'prefix-[id]-suffix.vue') + + // params in the middle path parts + tree.insert('prefix/[id]/suffix', 'prefix/[id]/suffix.vue') + tree.insert('prefix/[[opt]]/suffix', 'prefix/[[opt]]/suffix.vue') + tree.insert('prefix/[repeat]+/suffix', 'prefix/[repeat]+/suffix.vue') + tree.insert( + 'prefix/[[optRepeat]]+/suffix', + 'prefix/[[optRepeat]]+/suffix.vue' + ) + tree.insert('prefix/static/suffix', 'prefix/static/suffix.vue') + // sub-segments + tree.insert('prefix/[id]-end/suffix', 'prefix/[id]-end/suffix.vue') + tree.insert('prefix/[[opt]]-end/suffix', 'prefix/[[opt]]-end/suffix.vue') + tree.insert( + 'prefix/sub-[id]-end/suffix', + 'prefix/sub-[id]-end/suffix.vue' + ) + tree.insert( + 'prefix/sub-[[opt]]-end/suffix', + 'prefix/sub-[[opt]]-end/suffix.vue' + ) + tree.insert('prefix/sub-[id]/suffix', 'prefix/sub-[id]/suffix.vue') + tree.insert('prefix/sub-[[opt]]/suffix', 'prefix/sub-[[opt]]/suffix.vue') + + expect(getRouteOrderFromResolver(tree)).toEqual([ + '/prefix/static/suffix', + '/prefix/sub-end', + '/prefix/sub-:id-end/suffix', + '/prefix/:id-end/suffix', + '/prefix/sub-:id/suffix', + '/prefix/sub-:id', + '/prefix/:id/suffix', + '/prefix/:id', + // this could be before /prefix/:id, but since it has a suffix, it works too + '/prefix/sub-:opt?-end/suffix', + '/prefix/:opt?-end/suffix', + '/prefix/sub-:opt?/suffix', + '/prefix/sub-:opt?', // FIXME: should be before /prefix/:id + '/prefix/:opt?/suffix', + '/prefix/:opt?', + '/prefix/:repeat+/suffix', + '/prefix/:repeat+', + '/prefix/:optRepeat*/suffix', + '/prefix/:optRepeat*', + '/prefix', + '/prefix-:id-suffix', + '/:id', + // one should never have both a regular id and an optional id in the same position + // because the optional one will never match + '/:optional?', + '/:splat(.*)', + ]) + }) + + it.todo('warns on invalid repeatable params') + }) + + it('strips off empty parent records', () => { + const tree = new PrefixTree(DEFAULT_OPTIONS) + const importsMap = new ImportsMap() + tree.insert('a', 'a.vue') + tree.insert('b/c', 'b/c.vue') + tree.insert('b/c/d', 'b/c/d.vue') + tree.insert('b/e/f', 'b/c/f.vue') + const resolver = generateRouteResolver( + tree, + DEFAULT_OPTIONS, + importsMap, + new Map() + ) + + expect(resolver).toMatchInlineSnapshot(` + " + const r_0 = normalizeRouteRecord({ + name: '/a', + path: new MatcherPatternPathStatic('/a'), + components: { + 'default': () => import('a.vue') + }, + }) + + const r_1 = normalizeRouteRecord({ + name: '/b/c', + path: new MatcherPatternPathStatic('/b/c'), + components: { + 'default': () => import('b/c.vue') + }, + }) + const r_2 = normalizeRouteRecord({ + name: '/b/c/d', + path: new MatcherPatternPathStatic('/b/c/d'), + components: { + 'default': () => import('b/c/d.vue') + }, + parent: r_1, + }) + const r_3 = normalizeRouteRecord({ + name: '/b/e/f', + path: new MatcherPatternPathStatic('/b/e/f'), + components: { + 'default': () => import('b/c/f.vue') + }, + }) + + export const resolver = createFixedResolver([ + r_2, // /b/c/d + r_3, // /b/e/f + r_1, // /b/c + r_0, // /a + ]) + " + `) + }) + + it('retains parent chain when skipping empty intermediate nodes', () => { + const tree = new PrefixTree(DEFAULT_OPTIONS) + // Create a meaningful parent + tree.insert('a', 'a.vue') + // Create a deeply nested child with empty intermediate nodes b and c + tree.insert('a/b/c/e', 'a/b/c/e.vue') + const resolver = generateRouteResolver( + tree, + DEFAULT_OPTIONS, + new ImportsMap(), + new Map() + ) + + expect(resolver).toMatchInlineSnapshot(` + " + const r_0 = normalizeRouteRecord({ + name: '/a', + path: new MatcherPatternPathStatic('/a'), + components: { + 'default': () => import('a.vue') + }, + }) + const r_1 = normalizeRouteRecord({ + name: '/a/b/c/e', + path: new MatcherPatternPathStatic('/a/b/c/e'), + components: { + 'default': () => import('a/b/c/e.vue') + }, + parent: r_0, + }) + + export const resolver = createFixedResolver([ + r_1, // /a/b/c/e + r_0, // /a + ]) + " + `) + }) + + it('preserves parent nodes with meta data', () => { + const tree = new PrefixTree(DEFAULT_OPTIONS) + // Create a nested route + tree.insert('a/b/c', 'a/b/c.vue') + // Add meta to the intermediate b node (no components, but has meta) + const aNode = tree.children.get('a')! + const bNode = aNode.children.get('b')! + bNode.value.setEditOverride('meta', { requiresAuth: true }) + + const resolver = generateRouteResolver( + tree, + DEFAULT_OPTIONS, + new ImportsMap(), + new Map() + ) + + expect(resolver).toMatchInlineSnapshot(` + " + const r_0 = normalizeRouteRecord({ + /* internal name: '/a/b' */ + meta: { + "requiresAuth": true + }, + }) + const r_1 = normalizeRouteRecord({ + name: '/a/b/c', + path: new MatcherPatternPathStatic('/a/b/c'), + components: { + 'default': () => import('a/b/c.vue') + }, + parent: r_0, + }) + + export const resolver = createFixedResolver([ + r_1, // /a/b/c + ]) + " + `) + }) + + it('includes meta in route records with components', () => { + const tree = new PrefixTree(DEFAULT_OPTIONS) + // Create a route with both component and meta + tree.insert('users', 'users.vue') + const usersNode = tree.children.get('users')! + usersNode.value.setEditOverride('meta', { + requiresAuth: true, + title: 'Users', + }) + + const resolver = generateRouteResolver( + tree, + DEFAULT_OPTIONS, + new ImportsMap(), + new Map() + ) + + expect(resolver).toMatchInlineSnapshot(` + " + const r_0 = normalizeRouteRecord({ + name: '/users', + path: new MatcherPatternPathStatic('/users'), + meta: { + "requiresAuth": true, + "title": "Users" + }, + components: { + 'default': () => import('users.vue') + }, + }) + + export const resolver = createFixedResolver([ + r_0, // /users + ]) + " + `) + }) + + it('handles definePage imports correctly', () => { + const tree = new PrefixTree(DEFAULT_OPTIONS) + // Create a route with a component + tree.insert('profile', 'profile.vue') + const profileNode = tree.children.get('profile')! + + // Mark it as having definePage (this would normally be set by the plugin when parsing the file) + profileNode.hasDefinePage = true + + const resolver = generateRouteResolver( + tree, + DEFAULT_OPTIONS, + new ImportsMap(), + new Map() + ) + + expect(resolver).toMatchInlineSnapshot(` + " + + const r_0 = normalizeRouteRecord( + _mergeRouteRecord( + { + name: '/profile', + path: new MatcherPatternPathStatic('/profile'), + components: { + 'default': () => import('profile.vue') + }, + }, + _definePage_default_0 + ) + ) + + + export const resolver = createFixedResolver([ + r_0, // /profile + ]) + " + `) + }) + + it('includes query property in route records with query params', () => { + const tree = new PrefixTree(DEFAULT_OPTIONS) + tree.insert('search', 'search.vue') + const searchNode = tree.children.get('search')! + + // Add query params + searchNode.value.setEditOverride('params', { + query: { + q: {}, + limit: { parser: 'int' }, + }, + }) + + const resolver = generateRouteResolver( + tree, + DEFAULT_OPTIONS, + new ImportsMap(), + new Map() + ) + + expect(resolver).toMatchInlineSnapshot(` + " + const r_0 = normalizeRouteRecord({ + name: '/search', + path: new MatcherPatternPathStatic('/search'), + query: [ + new MatcherPatternQueryParam('q', 'q', 'value'), + new MatcherPatternQueryParam('limit', 'limit', 'value', PARAM_PARSER_INT) + ], + components: { + 'default': () => import('search.vue') + }, + }) + + export const resolver = createFixedResolver([ + r_0, // /search + ]) + " + `) + }) +}) diff --git a/src/codegen/generateRouteResolver.ts b/src/codegen/generateRouteResolver.ts new file mode 100644 index 000000000..f43f32755 --- /dev/null +++ b/src/codegen/generateRouteResolver.ts @@ -0,0 +1,345 @@ +import { getLang } from '@vue-macros/common' +import { PrefixTree, type TreeNode } from '../core/tree' +import { ImportsMap } from '../core/utils' +import { type ResolvedOptions } from '../options' +import { ts } from '../utils' +import { + generatePathParamsOptions, + generateParamParserOptions, + ParamParsersMap, +} from './generateParamParsers' +import { generatePageImport, formatMeta } from './generateRouteRecords' + +/** + * Compare two score arrays for sorting routes by priority. + * Higher scores should come first (more specific routes). + */ +function compareRouteScore(a: number[][], b: number[][]): number { + const maxLength = Math.max(a.length, b.length) + + for (let i = 0; i < maxLength; i++) { + const aSegment = a[i] || [] + const bSegment = b[i] || [] + + // Compare segment by segment, but consider the "minimum" score of each segment + // since mixed segments with params should rank lower than pure static + const aMinScore = aSegment.length > 0 ? Math.min(...aSegment) : 0 + const bMinScore = bSegment.length > 0 ? Math.min(...bSegment) : 0 + + if (aMinScore !== bMinScore) { + return bMinScore - aMinScore // Higher minimum score wins + } + + // If minimum scores are equal, compare average scores + const aAvgScore = + aSegment.length > 0 + ? aSegment.reduce((sum, s) => sum + s, 0) / aSegment.length + : 0 + const bAvgScore = + bSegment.length > 0 + ? bSegment.reduce((sum, s) => sum + s, 0) / bSegment.length + : 0 + + if (aAvgScore !== bAvgScore) { + return bAvgScore - aAvgScore // Higher average score wins + } + + // If averages are equal, prefer fewer subsegments (less complexity) + if (aSegment.length !== bSegment.length) { + return aSegment.length - bSegment.length + } + } + + // If all segments are equal, prefer fewer segments (shorter paths) + return a.length - b.length +} + +interface GenerateRouteResolverState { + id: number + matchableRecords: { + path: string + varName: string + score: number[][] + }[] +} + +export function generateRouteResolver( + tree: PrefixTree, + options: ResolvedOptions, + importsMap: ImportsMap, + paramParsersMap: ParamParsersMap +): string { + const state: GenerateRouteResolverState = { id: 0, matchableRecords: [] } + const records = tree.getChildrenSorted().map((node) => + generateRouteRecord({ + node, + parentVar: null, + state, + options, + importsMap, + paramParsersMap, + }) + ) + + importsMap.add('vue-router/experimental', 'createFixedResolver') + importsMap.add('vue-router/experimental', 'MatcherPatternPathStatic') + importsMap.add('vue-router/experimental', 'MatcherPatternPathDynamic') + importsMap.add('vue-router/experimental', 'normalizeRouteRecord') + + return ts` +${records.join('\n\n')} + +export const resolver = createFixedResolver([ +${state.matchableRecords + .sort((a, b) => compareRouteScore(a.score, b.score)) + .map( + ({ varName, path }) => + ` ${varName}, ${' '.repeat(String(state.id).length - varName.length + 2)}// ${path}` + ) + .join('\n')} +]) +` +} + +/** + * Generates the route record in the format expected by the static resolver. + */ +export function generateRouteRecord({ + node, + parentVar, + state, + options, + importsMap, + paramParsersMap, +}: { + node: TreeNode + parentVar: string | null | undefined + state: GenerateRouteResolverState + options: ResolvedOptions + importsMap: ImportsMap + paramParsersMap: ParamParsersMap +}): string { + const isMatchable = node.isMatchable() + + // we want to skip adding routes that add no options (components, meta, props, etc) + // that simplifies the generated tree + const shouldSkipNode = !isMatchable && !node.meta + + let varName: string | null = null + let recordDeclaration = '' + + // Handle definePage imports + const definePageDataList: string[] = [] + if (node.hasDefinePage) { + for (const [name, filePath] of node.value.components) { + const pageDataImport = `_definePage_${name}_${importsMap.size}` + definePageDataList.push(pageDataImport) + const lang = getLang(filePath) + importsMap.addDefault( + // TODO: apply the language used in the sfc + `${filePath}?definePage&` + + (lang === 'vue' ? 'vue&lang.tsx' : `lang.${lang}`), + pageDataImport + ) + } + } + + if (!shouldSkipNode) { + varName = `r_${state.id++}` + + let recordName: string + let recordComponents: string + + if (isMatchable) { + state.matchableRecords.push({ + path: node.fullPath, + varName, + score: node.score, + }) + recordName = `name: '${node.name}',` + recordComponents = generateRouteRecordComponent( + node, + ' ', + options.importMode, + importsMap + ) + } else { + recordName = node.name ? `/* internal name: '${node.name}' */` : `` + recordComponents = '' + } + + const queryProperty = generateRouteRecordQuery({ + node, + importsMap, + paramParsersMap, + }) + const routeRecordObject = `{ + ${recordName} + ${generateRouteRecordPath({ node, importsMap, paramParsersMap })}${ + queryProperty ? `\n ${queryProperty}` : '' + }${formatMeta(node, ' ')} + ${recordComponents}${parentVar ? `\n parent: ${parentVar},` : ''} +}` + + recordDeclaration = + definePageDataList.length > 0 + ? ` +const ${varName} = normalizeRouteRecord( + ${generateRouteRecordMerge(routeRecordObject, definePageDataList, importsMap)} +) +` + : ` +const ${varName} = normalizeRouteRecord(${routeRecordObject}) +` + .trim() + .split('\n') + // remove empty lines + .filter((l) => l.trimStart().length > 0) + .join('\n') + } + + const children = node.getChildrenSorted().map((child) => + generateRouteRecord({ + node: child, + // If we skipped this node, pass the parent var from above, otherwise use our var + parentVar: shouldSkipNode ? parentVar : varName, + state, + options, + importsMap, + paramParsersMap, + }) + ) + + return ( + recordDeclaration + + (children.length + ? (recordDeclaration ? '\n' : '') + children.join('\n') + : '') + ) +} + +function generateRouteRecordComponent( + node: TreeNode, + indentStr: string, + importMode: ResolvedOptions['importMode'], + importsMap: ImportsMap +): string { + const files = Array.from(node.value.components) + return `components: { +${files + .map( + ([key, path]) => + `${indentStr + ' '}'${key}': ${generatePageImport( + path, + importMode, + importsMap + )}` + ) + .join(',\n')} +${indentStr}},` +} + +/** + * Generates the `path` property of a route record for the static resolver. + */ +export function generateRouteRecordPath({ + node, + importsMap, + paramParsersMap, +}: { + node: TreeNode + importsMap: ImportsMap + paramParsersMap: ParamParsersMap +}) { + if (!node.isMatchable()) { + return '' + } + const params = node.pathParams + if (params.length > 0) { + return `path: new MatcherPatternPathDynamic( + ${node.regexp}, + ${generatePathParamsOptions(params, importsMap, paramParsersMap)}, + ${JSON.stringify(node.matcherPatternPathDynamicParts)}, + ${node.isSplat ? 'null,' : '/* trailingSlash */'} + ),` + } else { + return `path: new MatcherPatternPathStatic('${node.fullPath}'),` + } +} + +/** + * Generates the `query` property of a route record for the static resolver. + */ +export function generateRouteRecordQuery({ + node, + importsMap, + paramParsersMap, +}: { + node: TreeNode + importsMap: ImportsMap + paramParsersMap: ParamParsersMap +}) { + const queryParams = node.queryParams + if (queryParams.length === 0) { + return '' + } + + importsMap.add('vue-router/experimental', 'MatcherPatternQueryParam') + + return `query: [ +${queryParams + .map((param) => { + const parserOptions = generateParamParserOptions( + param, + importsMap, + paramParsersMap + ) + + const args = [ + `'${param.paramName}'`, + // TODO: allow param.queryKey + `'${param.paramName}'`, + `'${param.format}'`, + ] + + if (parserOptions || param.defaultValue !== undefined) { + args.push(parserOptions || '{}') + } + + if (param.defaultValue !== undefined) { + args.push(param.defaultValue) + } + + return ` new MatcherPatternQueryParam(${args.join(', ')})` + }) + .join(',\n')} + ],` +} + +/** + * Generates a merge call for route records with definePage data in the experimental resolver format. + */ +function generateRouteRecordMerge( + routeRecordObject: string, + definePageDataList: string[], + importsMap: ImportsMap +): string { + if (definePageDataList.length === 0) { + return routeRecordObject + } + + importsMap.add('vue-router/experimental', '_mergeRouteRecord') + + // Re-indent the route object to be 4 spaces (2 levels from normalizeRouteRecord) + const indentedRouteObject = routeRecordObject + .split('\n') + .map((line) => { + return line && ` ${line}` + }) + .join('\n') + + return `_mergeRouteRecord( +${indentedRouteObject}, +${definePageDataList.map((name) => ` ${name}`).join(',\n')} + )` +} diff --git a/src/core/__snapshots__/definePage.spec.ts.snap b/src/core/__snapshots__/definePage.spec.ts.snap index 0d160ad0b..6b1dee935 100644 --- a/src/core/__snapshots__/definePage.spec.ts.snap +++ b/src/core/__snapshots__/definePage.spec.ts.snap @@ -22,9 +22,9 @@ export default { }" `; -exports[`definePage > imports > removes default import if not used 1`] = `"export default {name: 'ok'}"`; +exports[`definePage > imports > removes default unused imports 1`] = `"export default {name: 'ok'}"`; -exports[`definePage > imports > removes star imports if not used 1`] = `"export default {name: 'ok'}"`; +exports[`definePage > imports > removes unused star imports 1`] = `"export default {name: 'ok'}"`; exports[`definePage > imports > works when combining named and default imports 1`] = ` "import my_var, {my_func} from './lib' diff --git a/src/core/context.ts b/src/core/context.ts index f07494aae..a3fb8bf2b 100644 --- a/src/core/context.ts +++ b/src/core/context.ts @@ -1,13 +1,13 @@ import { ResolvedOptions } from '../options' import { TreeNode, PrefixTree } from './tree' -import { promises as fs } from 'fs' +import { promises as fs } from 'node:fs' import { asRoutePath, ImportsMap, logTree, throttle } from './utils' import { generateRouteNamedMap } from '../codegen/generateRouteMap' import { generateRouteFileInfoMap } from '../codegen/generateRouteFileInfoMap' -import { MODULE_ROUTES_PATH, MODULE_VUE_ROUTER_AUTO } from './moduleConstants' +import { MODULE_ROUTES_PATH } from './moduleConstants' import { generateRouteRecord } from '../codegen/generateRouteRecords' import { glob } from 'tinyglobby' -import { dirname, relative, resolve } from 'pathe' +import { dirname, parse as parsePathe, relative, resolve } from 'pathe' import { ServerContext } from '../options' import { getRouteBlock } from './customBlock' import { @@ -17,10 +17,19 @@ import { } from './RoutesFolderWatcher' import { generateDTS as _generateDTS } from '../codegen/generateDTS' import { generateVueRouterProxy as _generateVueRouterProxy } from '../codegen/vueRouterModule' -import { definePageTransform, extractDefinePageNameAndPath } from './definePage' +import { definePageTransform, extractDefinePageInfo } from './definePage' import { EditableTreeNode } from './extendRoutes' import { isPackageExists as isPackageInstalled } from 'local-pkg' import { ts } from '../utils' +import { generateRouteResolver } from '../codegen/generateRouteResolver' +import { type FSWatcher, watch as fsWatch } from 'chokidar' +import { + generateParamParsersTypesDeclarations, + generateParamParserCustomType, + ParamParsersMap, + warnMissingParamParsers, +} from '../codegen/generateParamParsers' +import picomatch from 'picomatch' export function createRoutesContext(options: ResolvedOptions) { const { dts: preferDTS, root, routesFolder } = options @@ -45,7 +54,8 @@ export function createRoutesContext(options: ResolvedOptions) { }) // populated by the initial scan pages - const watchers: RoutesFolderWatcher[] = [] + const watchers: Array = [] + const paramParsersMap: ParamParsersMap = new Map() async function scanPages(startWatchers = true) { if (options.extensions.length < 1) { @@ -59,9 +69,12 @@ export function createRoutesContext(options: ResolvedOptions) { return } + const PARAM_PARSER_GLOB = '*.{ts,js}' + const isParamParserMatch = picomatch(PARAM_PARSER_GLOB) + // get the initial list of pages - await Promise.all( - routesFolder + await Promise.all([ + ...routesFolder .map((folder) => resolveFolderOptions(options, folder)) .map((folder) => { if (startWatchers) { @@ -93,8 +106,52 @@ export function createRoutesContext(options: ResolvedOptions) { ) ) ) + }), + ...(options.experimental.paramParsers?.dir.map((folder) => { + if (startWatchers) { + watchers.push( + setupParamParserWatcher( + fsWatch('.', { + cwd: folder, + ignoreInitial: true, + ignorePermissionErrors: true, + ignored: (filePath, stats) => { + // let folders pass, they are ignored by the glob pattern + if (!stats || stats.isDirectory()) { + return false + } + + return !isParamParserMatch(relative(folder, filePath)) + }, + }), + folder + ) + ) + } + + return glob(PARAM_PARSER_GLOB, { + cwd: folder, + onlyFiles: true, + expandDirectories: false, + }).then((paramParserFiles) => { + for (const file of paramParserFiles) { + const name = parsePathe(file).name + // TODO: could be simplified to only one import that starts with / for vite + const absolutePath = resolve(folder, file) + paramParsersMap.set(name, { + name, + typeName: `Param_${name}`, + absolutePath, + relativePath: relative(options.root, absolutePath), + }) + } + logger.log( + 'Parsed param parsers', + [...paramParsersMap].map((p) => p[0]) + ) }) - ) + }) || []), + ]) for (const route of editableRoutes) { await options.extendRoute?.(route) @@ -109,10 +166,7 @@ export function createRoutesContext(options: ResolvedOptions) { // TODO: cache the result of parsing the SFC (in the extractDefinePageAndName) so the transform can reuse the parsing node.hasDefinePage ||= content.includes('definePage') // TODO: track if it changed and to not always trigger HMR - const definedPageNameAndPath = extractDefinePageNameAndPath( - content, - filePath - ) + const definedPageInfo = extractDefinePageInfo(content, filePath) // TODO: track if it changed and if generateRoutes should be called again const routeBlock = getRouteBlock(filePath, content, options) // TODO: should warn if hasDefinePage and customRouteBlock @@ -120,7 +174,7 @@ export function createRoutesContext(options: ResolvedOptions) { node.setCustomRouteBlock(filePath, { ...routeBlock, - ...definedPageNameAndPath, + ...definedPageInfo, }) } @@ -135,9 +189,6 @@ export function createRoutesContext(options: ResolvedOptions) { if (triggerExtendRoute) { await options.extendRoute?.(new EditableTreeNode(node)) } - - // TODO: trigger HMR vue-router/auto - server?.updateRoutes() } async function updatePage({ filePath, routePath }: HandlerContext) { @@ -156,8 +207,26 @@ export function createRoutesContext(options: ResolvedOptions) { function removePage({ filePath, routePath }: HandlerContext) { logger.log(`remove "${routePath}" for "${filePath}"`) routeTree.removeChild(filePath) - // TODO: HMR vue-router/auto - server?.updateRoutes() + } + + function setupParamParserWatcher(watcher: FSWatcher, cwd: string) { + logger.log(`🤖 Scanning param parsers in ${cwd}`) + return watcher + .on('add', (file) => { + const name = parsePathe(file).name + const absolutePath = resolve(cwd, file) + paramParsersMap.set(name, { + name, + typeName: `Param_${name}`, + absolutePath, + relativePath: './' + relative(options.root, absolutePath), + }) + writeConfigFiles() + }) + .on('unlink', (file) => { + paramParsersMap.delete(parsePathe(file).name) + writeConfigFiles() + }) } function setupWatcher(watcher: RoutesFolderWatcher) { @@ -181,6 +250,57 @@ export function createRoutesContext(options: ResolvedOptions) { // unlinkDir event } + function generateResolver() { + const importsMap = new ImportsMap() + + const resolverCode = generateRouteResolver( + routeTree, + options, + importsMap, + paramParsersMap + ) + + // generate the list of imports + let imports = importsMap.toString() + // add an empty line for readability + if (imports) { + imports += '\n' + } + + const hmr = ts` +export function handleHotUpdate(_router, _hotUpdateCallback) { + if (import.meta.hot) { + import.meta.hot.data.router = _router + import.meta.hot.data.router_hotUpdateCallback = _hotUpdateCallback + } +} + +if (import.meta.hot) { + import.meta.hot.accept((mod) => { + const router = import.meta.hot.data.router + if (!router) { + import.meta.hot.invalidate('[unplugin-vue-router:HMR] Cannot replace the resolver because there is no active router. Reloading.') + return + } + router._hmrReplaceResolver(mod.resolver) + // call the hotUpdateCallback for custom updates + import.meta.hot.data.router_hotUpdateCallback?.(mod.resolver) + const route = router.currentRoute.value + router.replace({ + path: route.path, + query: route.query, + hash: route.hash, + force: true + }) + }) +}` + + const newAutoRoutes = `${imports}${resolverCode}\n${hmr}` + + // prepend it to the code + return newAutoRoutes + } + function generateRoutes() { const importsMap = new ImportsMap() @@ -190,7 +310,7 @@ export function createRoutesContext(options: ResolvedOptions) { importsMap )}\n` - let hmr = ts` + const hmr = ts` export function handleHotUpdate(_router, _hotUpdateCallback) { if (import.meta.hot) { import.meta.hot.data.router = _router @@ -238,15 +358,26 @@ if (import.meta.hot) { return newAutoRoutes } - function generateDTS(): string { - return _generateDTS({ - vueRouterModule: MODULE_VUE_ROUTER_AUTO, + function generateDTS() { + if (options.experimental.paramParsers?.dir.length) { + warnMissingParamParsers(routeTree, paramParsersMap) + } + + const autoRoutes = _generateDTS({ routesModule: MODULE_ROUTES_PATH, - routeNamedMap: generateRouteNamedMap(routeTree), + routeNamedMap: generateRouteNamedMap(routeTree, options, paramParsersMap), routeFileInfoMap: generateRouteFileInfoMap(routeTree, { root, }), + paramsTypesDeclaration: + generateParamParsersTypesDeclarations(paramParsersMap), + customParamsType: generateParamParserCustomType(paramParsersMap), }) + + // TODO: parser auto copmlete for definePage + // const paramParserListType = generateParamParserListTypes([...paramParsers]) + + return autoRoutes } // NOTE: this code needs to be generated because otherwise it doesn't go through transforms and `vue-router/auto-routes` @@ -275,6 +406,10 @@ if (import.meta.hot) { await fs.writeFile(dts, content, 'utf-8') logger.timeLog('writeConfigFiles', 'wrote dts file') lastDTS = content + // TODO: only update routes if routes changed (this includes definePage changes) + // but do not update routes if only the component want updated + // currently, this doesn't trigger if definePage meta properties changed + server?.updateRoutes() } } logger.timeEnd('writeConfigFiles') @@ -305,6 +440,7 @@ if (import.meta.hot) { stopWatcher, generateRoutes, + generateResolver, generateVueRouterProxy, definePageTransform(code: string, id: string) { diff --git a/src/core/customBlock.ts b/src/core/customBlock.ts index 75c30d72d..27a34e922 100644 --- a/src/core/customBlock.ts +++ b/src/core/customBlock.ts @@ -4,6 +4,7 @@ import JSON5 from 'json5' import { parse as YAMLParser } from 'yaml' import { RouteRecordRaw } from 'vue-router' import { warn } from './utils' +import type { DefinePageQueryParamOptions } from '../runtime' export function getRouteBlock( path: string, @@ -24,6 +25,19 @@ export interface CustomRouteBlock > > { name?: string | undefined | false + + params?: { + path?: Record + + query?: Record + } +} + +export interface CustomRouteBlockQueryParamOptions { + parser?: string + format?: DefinePageQueryParamOptions['format'] + // TODO: queryKey?: string + default?: string } function parseCustomBlock( diff --git a/src/core/definePage.spec.ts b/src/core/definePage.spec.ts index 07d92d4f8..214a84a87 100644 --- a/src/core/definePage.spec.ts +++ b/src/core/definePage.spec.ts @@ -1,8 +1,11 @@ import { TransformResult } from 'vite' import { expect, describe, it } from 'vitest' -import { definePageTransform, extractDefinePageNameAndPath } from './definePage' +import { definePageTransform, extractDefinePageInfo } from './definePage' +import { ts } from '../utils' -const sampleCode = ` +const vue = String.raw + +const sampleCode = vue` `, id: 'src/pages/with-imports.vue&definePage&vue&lang.ts', })) as Exclude - expect(result).toHaveProperty('code') - expect(result?.code).toMatchSnapshot() + expect(resultStar).toHaveProperty('code') + expect(resultStar?.code).toMatchSnapshot() }) - it('removes star imports if not used', async () => { + it('works with star imports', async () => { const result = (await definePageTransform({ - code: ` + code: vue` `, id: 'src/pages/with-imports.vue&definePage&vue&lang.ts', @@ -117,7 +120,7 @@ definePage({name: 'ok'}) it('works when combining named and default imports', async () => { const result = (await definePageTransform({ - code: ` + code: vue` +` + expect( + extractDefinePageInfo(codeWithAllParams, 'src/pages/test.vue') + ).toEqual({ + params: { + path: { + userId: 'int', + isActive: 'bool', + }, + query: { + page: { + parser: 'int', + default: '1', + format: 'value', + }, + enabled: { + parser: 'bool', + }, + count: { + parser: 'int', + default: '42', + }, + active: { + default: "'none'", + }, + }, + }, + }) + }) + it('extract name skipped when non existent', async () => { expect( - await extractDefinePageNameAndPath( - ` + extractDefinePageInfo( + vue` @@ -240,15 +298,15 @@ const b = 1 id: 'src/pages/definePage.vue?definePage&vue', }) ).toMatchObject({ - code: `\ + code: ts` export default { name: 'custom', path: '/custom', -}`, +}`.trim(), }) expect( - await extractDefinePageNameAndPath(sampleCode, 'src/pages/definePage.vue') + extractDefinePageInfo(sampleCode, 'src/pages/definePage.vue') ).toEqual({ name: 'custom', path: '/custom', diff --git a/src/core/definePage.ts b/src/core/definePage.ts index ca420eb1e..fe5bdfb21 100644 --- a/src/core/definePage.ts +++ b/src/core/definePage.ts @@ -11,15 +11,18 @@ import type { Thenable, TransformResult } from 'unplugin' import type { CallExpression, Node, + ObjectExpression, ObjectProperty, Program, Statement, StringLiteral, } from '@babel/types' +import { generate } from '@babel/generator' import { walkAST } from 'ast-walker-scope' -import { CustomRouteBlock } from './customBlock' import { warn } from './utils' import { ParsedStaticImport, findStaticImports, parseStaticImport } from 'mlly' +import type { ParamParserType } from 'unplugin-vue-router/runtime' +import { CustomRouteBlock } from './customBlock' const MACRO_DEFINE_PAGE = 'definePage' export const MACRO_DEFINE_PAGE_QUERY = /[?&]definePage\b/ @@ -186,10 +189,18 @@ export function definePageTransform({ } } -export function extractDefinePageNameAndPath( +type DefinePageParamsInfo = NonNullable + +export interface DefinePageInfo { + name?: string | false + path?: string + params?: CustomRouteBlock['params'] +} + +export function extractDefinePageInfo( sfcCode: string, id: string -): { name?: string | false; path?: string } | null | undefined { +): DefinePageInfo | null | undefined { if (!sfcCode.includes(MACRO_DEFINE_PAGE)) return const { ast, definePageNodes } = getCodeAst(sfcCode, id) @@ -216,7 +227,7 @@ export function extractDefinePageNameAndPath( ) } - const routeInfo: Pick = {} + const routeInfo: DefinePageInfo = {} for (const prop of routeRecord.properties) { if (prop.type === 'ObjectProperty' && prop.key.type === 'Identifier') { @@ -238,6 +249,10 @@ export function extractDefinePageNameAndPath( } else { routeInfo.path = prop.value.value } + } else if (prop.key.name === 'params') { + if (prop.value.type === 'ObjectExpression') { + routeInfo.params = extractParamsInfo(prop.value, id) + } } } } @@ -245,6 +260,122 @@ export function extractDefinePageNameAndPath( return routeInfo } +function extractParamsInfo( + paramsObj: ObjectExpression, + id: string +): DefinePageParamsInfo { + const params: DefinePageParamsInfo = {} + + for (const prop of paramsObj.properties) { + if (prop.type === 'ObjectProperty' && prop.key.type === 'Identifier') { + if (prop.key.name === 'query' && prop.value.type === 'ObjectExpression') { + params.query = extractQueryParams(prop.value, id) + } else if ( + prop.key.name === 'path' && + prop.value.type === 'ObjectExpression' + ) { + params.path = extractPathParams(prop.value, id) + } + } + } + + return params +} + +function extractQueryParams( + queryObj: ObjectExpression, + _id: string +): NonNullable['query'] { + const queryParams: NonNullable['query'] = {} + + for (const prop of queryObj.properties) { + if (prop.type === 'ObjectProperty' && prop.key.type === 'Identifier') { + const paramName = prop.key.name + + // we normalize short form for convenience + if (prop.value.type === 'StringLiteral') { + queryParams[paramName] = { + parser: prop.value.value as ParamParserType, + } + } else if (prop.value.type === 'ObjectExpression') { + // Full form: param: { parser: 'int', default: 1, format: 'value' } + const paramInfo: (typeof queryParams)[string] = {} + + for (const paramProp of prop.value.properties) { + if ( + paramProp.type === 'ObjectProperty' && + paramProp.key.type === 'Identifier' + ) { + if ( + paramProp.key.name === 'parser' && + paramProp.value.type === 'StringLiteral' + ) { + paramInfo.parser = paramProp.value.value as ParamParserType + } else if ( + paramProp.key.name === 'format' && + paramProp.value.type === 'StringLiteral' + ) { + paramInfo.format = paramProp.value.value as 'value' | 'array' + } else if (paramProp.key.name === 'default') { + if (typeof paramProp.value.extra?.raw === 'string') { + paramInfo.default = paramProp.value.extra.raw + } else if (paramProp.value.type === 'NumericLiteral') { + paramInfo.default = String(paramProp.value.value) + } else if (paramProp.value.type === 'StringLiteral') { + paramInfo.default = JSON.stringify(paramProp.value.value) + } else if (paramProp.value.type === 'BooleanLiteral') { + paramInfo.default = String(paramProp.value.value) + } else if (paramProp.value.type === 'NullLiteral') { + paramInfo.default = 'null' + } else if ( + paramProp.value.type === 'UnaryExpression' && + (paramProp.value.operator === '-' || + paramProp.value.operator === '+' || + paramProp.value.operator === '!' || + paramProp.value.operator === '~') && + paramProp.value.argument.type === 'NumericLiteral' + ) { + // support negative numeric literals: -1, -1.5 + paramInfo.default = `${paramProp.value.operator}${paramProp.value.argument.value}` + } else if (paramProp.value.type === 'ArrowFunctionExpression') { + paramInfo.default = generate(paramProp.value).code + } else { + warn( + `Unrecognized default value in definePage() for query param "${paramName}". Typeof value: "${paramProp.value.type}". This is a bug or a missing type of value, open an issue on https://github.com/posva/unplugin-vue-router and provide the definePage() code.` + ) + } + } + } + } + + queryParams[paramName] = paramInfo + } + } + } + + return queryParams +} + +function extractPathParams( + pathObj: ObjectExpression, + _id: string +): NonNullable['path'] { + const pathParams: NonNullable['path'] = {} + + for (const prop of pathObj.properties) { + if ( + prop.type === 'ObjectProperty' && + prop.key.type === 'Identifier' && + prop.value.type === 'StringLiteral' + ) { + // TODO: we should check if the value is a valid parser type + pathParams[prop.key.name] = prop.value.value as ParamParserType + } + } + + return pathParams +} + // TODO: use export function extractRouteAlias( aliasValue: ObjectProperty['value'], diff --git a/src/core/extendRoutes.spec.ts b/src/core/extendRoutes.spec.ts index 608bcb8c4..3417efd1f 100644 --- a/src/core/extendRoutes.spec.ts +++ b/src/core/extendRoutes.spec.ts @@ -69,12 +69,13 @@ describe('EditableTreeNode', () => { const child = tree.children.get(':id')! expect(child.fullPath).toBe('/:id') expect(child.path).toBe('/:id') - expect(child.params).toEqual([ + expect(child.params).toMatchObject([ { paramName: 'id', modifier: '', optional: false, repeatable: false, + parser: null, isSplat: false, }, ]) @@ -89,9 +90,10 @@ describe('EditableTreeNode', () => { const child = tree.children.get(':id+')! expect(child.fullPath).toBe('/:id+') expect(child.path).toBe('/:id+') - expect(child.params).toEqual([ + expect(child.params).toMatchObject([ { paramName: 'id', + parser: null, modifier: '+', optional: false, repeatable: true, @@ -109,7 +111,7 @@ describe('EditableTreeNode', () => { const node = tree.children.get(':foo/:bar')! expect(node.fullPath).toBe('/:foo/:bar') expect(node.path).toBe('/:foo/:bar') - expect(node.params).toEqual([ + expect(node.params).toMatchObject([ { paramName: 'foo', modifier: '', @@ -136,7 +138,7 @@ describe('EditableTreeNode', () => { const node = tree.children.get(':foo/:bar+_:o(\\d+)')! expect(node.fullPath).toBe('/:foo/:bar+_:o(\\d+)') expect(node.path).toBe('/:foo/:bar+_:o(\\d+)') - expect(node.params).toEqual([ + expect(node.params).toMatchObject([ { paramName: 'foo', modifier: '', @@ -169,7 +171,7 @@ describe('EditableTreeNode', () => { const node = tree.children.get(':id(\\d+)')! expect(node.fullPath).toBe('/:id(\\d+)') expect(node.path).toBe('/:id(\\d+)') - expect(node.params).toEqual([ + expect(node.params).toMatchObject([ { paramName: 'id', modifier: '', @@ -188,7 +190,7 @@ describe('EditableTreeNode', () => { const node = tree.children.get(':id()')! expect(node.fullPath).toBe('/:id()') expect(node.path).toBe('/:id()') - expect(node.params).toEqual([ + expect(node.params).toMatchObject([ { paramName: 'id', modifier: '', @@ -207,7 +209,7 @@ describe('EditableTreeNode', () => { const node = tree.children.get(':id(\\d+)+')! expect(node.fullPath).toBe('/:id(\\d+)+') expect(node.path).toBe('/:id(\\d+)+') - expect(node.params).toEqual([ + expect(node.params).toMatchObject([ { paramName: 'id', modifier: '+', @@ -226,7 +228,7 @@ describe('EditableTreeNode', () => { const node = tree.children.get(':id()+')! expect(node.fullPath).toBe('/:id()+') expect(node.path).toBe('/:id()+') - expect(node.params).toEqual([ + expect(node.params).toMatchObject([ { paramName: 'id', modifier: '+', @@ -246,7 +248,7 @@ describe('EditableTreeNode', () => { const child = tree.children.get(':path(.*)')! expect(child.fullPath).toBe('/:path(.*)') expect(child.path).toBe('/:path(.*)') - expect(child.params).toEqual([ + expect(child.params).toMatchObject([ { paramName: 'path', modifier: '', diff --git a/src/core/extendRoutes.ts b/src/core/extendRoutes.ts index 4bf0514cd..23b2fa1fd 100644 --- a/src/core/extendRoutes.ts +++ b/src/core/extendRoutes.ts @@ -47,6 +47,8 @@ export class EditableTreeNode { // but in other places we need to instruct the path is at the root so we change it afterwards addBackLeadingSlash = !this.node.isRoot() } + // TODO: if options.experimental.paramParsers, should insert the raw path as [thing] + // and warn if a path contains a : const node = this.node.insertParsedPath(path, filePath) const editable = new EditableTreeNode(node) if (addBackLeadingSlash) { @@ -169,11 +171,11 @@ export class EditableTreeNode { } /** - * Array of the route params and all of its parent's params. Note that changing the params will not update the path, - * you need to update both. + * Array of the route params and all of its parent's params. Note that + * changing the params will not update the path, you need to update both. */ get params() { - return this.node.params + return this.node.pathParams } /** diff --git a/src/core/moduleConstants.ts b/src/core/moduleConstants.ts index c7dfa03d0..a21969340 100644 --- a/src/core/moduleConstants.ts +++ b/src/core/moduleConstants.ts @@ -4,6 +4,7 @@ export const MODULE_VUE_ROUTER_AUTO = 'vue-router/auto' // vue-router/auto/routes was more natural but didn't work well with TS export const MODULE_ROUTES_PATH = `${MODULE_VUE_ROUTER_AUTO}-routes` +export const MODULE_RESOLVER_PATH = `vue-router/auto-resolver` // NOTE: not sure if needed. Used for HMR the virtual routes let time = Date.now() @@ -27,7 +28,11 @@ export const VIRTUAL_PREFIX = '/__' // allows removing the route block from the code export const ROUTE_BLOCK_ID = `${VIRTUAL_PREFIX}/vue-router/auto/route-block` -export const MODULES_ID_LIST = [MODULE_VUE_ROUTER_AUTO, MODULE_ROUTES_PATH] +export const MODULES_ID_LIST = [ + MODULE_VUE_ROUTER_AUTO, + MODULE_ROUTES_PATH, + MODULE_RESOLVER_PATH, +] export function getVirtualId(id: string) { return id.startsWith(VIRTUAL_PREFIX) ? id.slice(VIRTUAL_PREFIX.length) : null diff --git a/src/core/tree.spec.ts b/src/core/tree.spec.ts index f3f696090..c8688ff69 100644 --- a/src/core/tree.spec.ts +++ b/src/core/tree.spec.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest' import { DEFAULT_OPTIONS, resolveOptions } from '../options' import { PrefixTree } from './tree' -import { TreeNodeType, type TreeRouteParam } from './treeNodeValue' +import { TreeNodeType, type TreePathParam } from './treeNodeValue' import { resolve } from 'pathe' import { mockWarn } from '../../tests/vitest-mock-warn' @@ -43,6 +43,103 @@ describe('Tree', () => { expect(child.children.size).toBe(0) }) + it('parses a custom param type', () => { + const tree = new PrefixTree(RESOLVED_OPTIONS) + tree.insert('[id=int]', '[id=int].vue') + const child = tree.children.get('[id=int]')! + expect(child).toBeDefined() + expect(child.value).toMatchObject({ + rawSegment: '[id=int]', + params: [ + { + paramName: 'id', + parser: 'int', + }, + ], + fullPath: '/:id', + _type: TreeNodeType.param, + }) + }) + + it('parses a repeatable custom param type', () => { + const tree = new PrefixTree(RESOLVED_OPTIONS) + tree.insert('[id=int]+', '[id=int]+.vue') + const child = tree.children.get('[id=int]+')! + expect(child).toBeDefined() + expect(child.value).toMatchObject({ + rawSegment: '[id=int]+', + params: [ + { + paramName: 'id', + parser: 'int', + repeatable: true, + modifier: '+', + }, + ], + fullPath: '/:id+', + _type: TreeNodeType.param, + }) + }) + + it('parses an optional custom param type', () => { + const tree = new PrefixTree(RESOLVED_OPTIONS) + tree.insert('[[id=int]]', '[[id=int]].vue') + const child = tree.children.get('[[id=int]]')! + expect(child).toBeDefined() + expect(child.value).toMatchObject({ + rawSegment: '[[id=int]]', + params: [ + { + paramName: 'id', + parser: 'int', + optional: true, + modifier: '?', + }, + ], + fullPath: '/:id?', + _type: TreeNodeType.param, + }) + }) + + it('parses a repeatable optional custom param type', () => { + const tree = new PrefixTree(RESOLVED_OPTIONS) + tree.insert('[[id=int]]+', '[[id=int]]+.vue') + const child = tree.children.get('[[id=int]]+')! + expect(child).toBeDefined() + expect(child.value).toMatchObject({ + rawSegment: '[[id=int]]+', + params: [ + { + paramName: 'id', + parser: 'int', + repeatable: true, + optional: true, + modifier: '*', + }, + ], + fullPath: '/:id*', + _type: TreeNodeType.param, + }) + }) + + it('parses a custom param type with sub segments', () => { + const tree = new PrefixTree(RESOLVED_OPTIONS) + tree.insert('a-[id=int]-b', 'file.vue') + const child = tree.children.get('a-[id=int]-b')! + expect(child).toBeDefined() + expect(child.value).toMatchObject({ + rawSegment: 'a-[id=int]-b', + params: [ + { + paramName: 'id', + parser: 'int', + }, + ], + fullPath: '/a-:id-b', + _type: TreeNodeType.param, + }) + }) + it('separate param names from static segments', () => { const tree = new PrefixTree(RESOLVED_OPTIONS) tree.insert('[id]_a', '[id]_a.vue') @@ -452,13 +549,13 @@ describe('Tree', () => { path: '/:a()/new-b', }) expect(node.params).toHaveLength(1) - expect(node.params[0]).toEqual({ + expect(node.params[0]).toMatchObject({ paramName: 'a', isSplat: false, modifier: '', optional: false, repeatable: false, - } satisfies TreeRouteParam) + } satisfies Partial) }) it('removes trailing slash from path but not from name', () => { @@ -554,6 +651,223 @@ describe('Tree', () => { expect(`"(home" is missing the closing ")"`).toHaveBeenWarned() }) + describe('path regexp', () => { + it('generates static paths', () => { + const node = new PrefixTree(RESOLVED_OPTIONS).insert('a', 'a.vue') + expect(node.regexp).toBe('/^\\/a$/i') + }) + + it('works with multiple segments', () => { + const node = new PrefixTree(RESOLVED_OPTIONS).insert('a/b/c', 'a/b/c.vue') + expect(node.regexp).toBe('/^\\/a\\/b\\/c$/i') + }) + + describe('basic params [id] in all positions', () => { + it('only segment', () => { + const node = new PrefixTree(RESOLVED_OPTIONS).insert('[id]', '[id].vue') + expect(node.regexp).toBe('/^\\/([^/]+?)$/i') + expect(node.matcherPatternPathDynamicParts).toEqual([1]) + }) + + it('first position', () => { + const node = new PrefixTree(RESOLVED_OPTIONS).insert( + '[id]/static', + '[id]/static.vue' + ) + expect(node.regexp).toBe('/^\\/([^/]+?)\\/static$/i') + expect(node.matcherPatternPathDynamicParts).toEqual([1, 'static']) + }) + + it('middle position', () => { + const node = new PrefixTree(RESOLVED_OPTIONS).insert( + 'static/[id]/more', + 'static/[id]/more.vue' + ) + expect(node.regexp).toBe('/^\\/static\\/([^/]+?)\\/more$/i') + expect(node.matcherPatternPathDynamicParts).toEqual([ + 'static', + 1, + 'more', + ]) + }) + + it('last position', () => { + const node = new PrefixTree(RESOLVED_OPTIONS).insert( + 'static/[id]', + 'static/[id].vue' + ) + expect(node.regexp).toBe('/^\\/static\\/([^/]+?)$/i') + expect(node.matcherPatternPathDynamicParts).toEqual(['static', 1]) + }) + }) + + describe('optional params [[id]] in all positions', () => { + it('only segment', () => { + const node = new PrefixTree(RESOLVED_OPTIONS).insert( + '[[id]]', + '[[id]].vue' + ) + expect(node.regexp).toBe('/^\\/([^/]+?)?$/i') + expect(node.matcherPatternPathDynamicParts).toEqual([1]) + }) + + it('first position', () => { + const node = new PrefixTree(RESOLVED_OPTIONS).insert( + '[[id]]/static', + '[[id]]/static.vue' + ) + expect(node.regexp).toBe('/^(?:\\/([^/]+?))?\\/static$/i') + expect(node.matcherPatternPathDynamicParts).toEqual([1, 'static']) + }) + + it('middle position', () => { + const node = new PrefixTree(RESOLVED_OPTIONS).insert( + 'static/[[id]]/more', + 'static/[[id]]/more.vue' + ) + expect(node.regexp).toBe('/^\\/static(?:\\/([^/]+?))?\\/more$/i') + expect(node.matcherPatternPathDynamicParts).toEqual([ + 'static', + 1, + 'more', + ]) + }) + + it('last position', () => { + const node = new PrefixTree(RESOLVED_OPTIONS).insert( + 'static/[[id]]', + 'static/[[id]].vue' + ) + expect(node.regexp).toBe('/^\\/static(?:\\/([^/]+?))?$/i') + expect(node.matcherPatternPathDynamicParts).toEqual(['static', 1]) + }) + }) + + describe('repeatable params [id]+ in all positions', () => { + it('only segment', () => { + const node = new PrefixTree(RESOLVED_OPTIONS).insert( + '[id]+', + '[id]+.vue' + ) + expect(node.regexp).toBe('/^\\/(.+?)$/i') + expect(node.matcherPatternPathDynamicParts).toEqual([1]) + }) + + it('first position', () => { + const node = new PrefixTree(RESOLVED_OPTIONS).insert( + '[id]+/static', + '[id]+/static.vue' + ) + expect(node.regexp).toBe('/^\\/(.+?)\\/static$/i') + expect(node.matcherPatternPathDynamicParts).toEqual([1, 'static']) + }) + + it('middle position', () => { + const node = new PrefixTree(RESOLVED_OPTIONS).insert( + 'static/[id]+/more', + 'static/[id]+/more.vue' + ) + expect(node.regexp).toBe('/^\\/static\\/(.+?)\\/more$/i') + expect(node.matcherPatternPathDynamicParts).toEqual([ + 'static', + 1, + 'more', + ]) + }) + + it('last position', () => { + const node = new PrefixTree(RESOLVED_OPTIONS).insert( + 'static/[id]+', + 'static/[id]+.vue' + ) + expect(node.regexp).toBe('/^\\/static\\/(.+?)$/i') + expect(node.matcherPatternPathDynamicParts).toEqual(['static', 1]) + }) + }) + + describe('optional repeatable params [[id]]+ in all positions', () => { + it('only segment', () => { + const node = new PrefixTree(RESOLVED_OPTIONS).insert( + '[[id]]+', + '[[id]]+.vue' + ) + expect(node.regexp).toBe('/^\\/(.+?)?$/i') + expect(node.matcherPatternPathDynamicParts).toEqual([1]) + }) + + it('first position', () => { + const node = new PrefixTree(RESOLVED_OPTIONS).insert( + '[[id]]+/static', + '[[id]]+/static.vue' + ) + expect(node.regexp).toBe('/^(?:\\/(.+?))?\\/static$/i') + expect(node.matcherPatternPathDynamicParts).toEqual([1, 'static']) + }) + + it('middle position', () => { + const node = new PrefixTree(RESOLVED_OPTIONS).insert( + 'static/[[id]]+/more', + 'static/[[id]]+/more.vue' + ) + expect(node.regexp).toBe('/^\\/static(?:\\/(.+?))?\\/more$/i') + expect(node.matcherPatternPathDynamicParts).toEqual([ + 'static', + 1, + 'more', + ]) + }) + + it('last position', () => { + const node = new PrefixTree(RESOLVED_OPTIONS).insert( + 'static/[[id]]+', + 'static/[[id]]+.vue' + ) + expect(node.regexp).toBe('/^\\/static(?:\\/(.+?))?$/i') + expect(node.matcherPatternPathDynamicParts).toEqual(['static', 1]) + }) + }) + + it('works with multiple params', () => { + const node = new PrefixTree(RESOLVED_OPTIONS).insert('a/[b]/[c]', 'a.vue') + expect(node.regexp).toBe('/^\\/a\\/([^/]+?)\\/([^/]+?)$/i') + expect(node.matcherPatternPathDynamicParts).toEqual(['a', 1, 1]) + }) + + it('works with segments', () => { + const node = new PrefixTree(RESOLVED_OPTIONS).insert( + 'a/a-[b]-c-[d]', + 'a.vue' + ) + expect(node.regexp).toBe('/^\\/a\\/a-([^/]+?)-c-([^/]+?)$/i') + expect(node.matcherPatternPathDynamicParts).toEqual([ + 'a', + ['a-', 1, '-c-', 1], + ]) + }) + + it('works with a catch all route', () => { + const node = new PrefixTree(RESOLVED_OPTIONS).insert( + '[...all]', + '[...all].vue' + ) + expect(node.regexp).toBe('/^\\/(.*)$/i') + expect(node.matcherPatternPathDynamicParts).toEqual([0]) + }) + + it('works with a splat param with a prefix', () => { + const node = new PrefixTree(RESOLVED_OPTIONS).insert( + 'a/some-[id]/[...all]', + 'a/some-[id]/[...all].vue' + ) + expect(node.regexp).toBe('/^\\/a\\/some-([^/]+?)\\/(.*)$/i') + expect(node.matcherPatternPathDynamicParts).toEqual([ + 'a', + ['some-', 1], + 0, + ]) + }) + }) + // TODO: check warns with different order it.todo(`warns when a group's path conflicts with an existing file`) @@ -590,4 +904,87 @@ describe('Tree', () => { }) }) }) + + describe('Query params from definePage', () => { + it('extracts query params from route overrides', () => { + const tree = new PrefixTree(RESOLVED_OPTIONS) + const node = tree.insert('users', 'users.vue') + + // Simulate definePage params extraction + node.setCustomRouteBlock('users.vue', { + params: { + query: { + search: {}, + limit: { parser: 'int', default: '10' }, + tags: { parser: 'bool' }, + other: { default: '"defaultValue"' }, + }, + }, + }) + + expect(node.queryParams).toEqual([ + { + paramName: 'search', + parser: null, + format: 'value', + defaultValue: undefined, + }, + { + paramName: 'limit', + parser: 'int', + format: 'value', + defaultValue: '10', + }, + { + paramName: 'tags', + parser: 'bool', + format: 'value', + defaultValue: undefined, + }, + { + paramName: 'other', + parser: null, + format: 'value', + defaultValue: '"defaultValue"', + }, + ]) + }) + + it('returns empty array when no query params defined', () => { + const tree = new PrefixTree(RESOLVED_OPTIONS) + const node = tree.insert('about', 'about.vue') + + expect(node.queryParams).toEqual([]) + }) + + it('params includes both path and query params', () => { + const tree = new PrefixTree(RESOLVED_OPTIONS) + const node = tree.insert('posts/[id]', 'posts/[id].vue') + + node.setCustomRouteBlock('posts/[id].vue', { + params: { + query: { + tab: {}, + expand: { parser: 'bool', default: 'false' }, + }, + }, + }) + + // Should have 1 path param + 2 query params + expect(node.params).toHaveLength(3) + expect(node.params[0]).toMatchObject({ paramName: 'id' }) // path param + expect(node.params[1]).toMatchObject({ + paramName: 'tab', + parser: null, + format: 'value', + defaultValue: undefined, + }) // query param + expect(node.params[2]).toMatchObject({ + paramName: 'expand', + parser: 'bool', + format: 'value', + defaultValue: 'false', + }) // query param + }) + }) }) diff --git a/src/core/tree.ts b/src/core/tree.ts index 25fff6644..66633fada 100644 --- a/src/core/tree.ts +++ b/src/core/tree.ts @@ -1,8 +1,10 @@ import { type ResolvedOptions } from '../options' import { createTreeNodeValue, + escapeRegex, TreeNodeValueOptions, - TreeRouteParam, + TreePathParam, + TreeQueryParam, } from './treeNodeValue' import type { TreeNodeValue } from './treeNodeValue' import { CustomRouteBlock } from './customBlock' @@ -12,6 +14,10 @@ export interface TreeNodeOptions extends ResolvedOptions { treeNodeOptions?: TreeNodeValueOptions } +export type TreeNodeValueMatcherPart = Array< + string | number | Array +> + export class TreeNode { /** * value of the node @@ -265,13 +271,31 @@ export class TreeNode { : '' } - get params(): TreeRouteParam[] { - const params = this.value.isParam() ? [...this.value.params] : [] + /** + * Array of route params for this node. It includes **all** the params from the parents as well. + */ + get params(): (TreePathParam | TreeQueryParam)[] { + const params = [...this.value.params] + let node = this.parent + // add all the params from the parents + while (node) { + params.unshift(...node.value.params) + node = node.parent + } + + return params + } + + /** + * Array of route params coming from the path. It includes all the params from the parents as well. + */ + get pathParams(): TreePathParam[] { + const params = this.value.isParam() ? [...this.value.pathParams] : [] let node = this.parent // add all the params from the parents while (node) { if (node.value.isParam()) { - params.unshift(...node.value.params) + params.unshift(...node.value.pathParams) } node = node.parent } @@ -279,6 +303,123 @@ export class TreeNode { return params } + /** + * Array of query params extracted from definePage. Only returns query params from this specific node. + */ + get queryParams(): TreeQueryParam[] { + return this.value.queryParams + } + + /** + * Generates a regexp based on this node and its parents. This regexp is used by the custom resolver + */ + get regexp(): string { + let node: TreeNode | undefined = this + // we build the node list from parent to child + const nodeList: TreeNode[] = [] + while (node && !node.isRoot()) { + nodeList.unshift(node) + node = node.parent + } + + let re = '' + for (var i = 0; i < nodeList.length; i++) { + node = nodeList[i]! + if (node.value.isParam()) { + var nodeRe = node.value.re + // Ensure we add a connecting slash + // if we already have something in the regexp and if the only part of + // the segment is an optional param, then the / must be put inside the + // non-capturing group + if ( + // if we have a segment before or after + (re || i < nodeList.length - 1) && + // if the only part of the segment is an optional (can be repeatable) param + node.value.subSegments.length === 1 && + (node.value.subSegments.at(0) as TreePathParam).optional + ) { + // TODO: tweak if trailingSlash + re += `(?:\\/${ + // we remove the ? at the end because we add it later + nodeRe.slice(0, -1) + })?` + } else { + re += (re ? '\\/' : '') + nodeRe + } + } else { + re += (re ? '\\/' : '') + escapeRegex(node.value.pathSegment) + } + } + + // TODO: trailingSlash + return ( + '/^' + + // Avoid adding a leading slash if the first segment + // is an optional segment that already includes it + (re.startsWith('(?:\\/') ? '' : '\\/') + + re + + '$/i' + ) + } + + get score(): number[][] { + const scores: number[][] = [] + let node: TreeNode | undefined = this + + while (node && !node.isRoot()) { + scores.unshift(node.value.score) + node = node.parent + } + + return scores + } + + /** + * Is this node a splat (catch-all) param + */ + get isSplat(): boolean { + return this.value.isParam() && this.value.pathParams.some((p) => p.isSplat) + } + + /** + * Returns an array of matcher parts that is consumed by + * MatcherPatternPathDynamic to render the path. + */ + get matcherPatternPathDynamicParts(): TreeNodeValueMatcherPart { + const parts: TreeNodeValueMatcherPart = [] + let node: TreeNode | undefined = this + + while (node && !node.isRoot()) { + const subSegments = node.value.subSegments.map((segment) => + typeof segment === 'string' + ? segment + : // param + segment.isSplat + ? 0 + : 1 + ) + + if (subSegments.length > 1) { + parts.unshift(subSegments) + } else if (subSegments.length === 1) { + parts.unshift(subSegments[0]!) + } + node = node.parent + } + + return parts + } + + /** + * Is this tree node matchable? A matchable node has at least one component + * and a name. + */ + isMatchable(): this is TreeNode & { name: string } { + // a node is matchable if it has at least one component + // and the name is not false + return this.value.components.size > 0 && this.name !== false + } + /** * Returns wether this tree node is the root node of the tree. * diff --git a/src/core/treeNodeValue.ts b/src/core/treeNodeValue.ts index e2aabecc2..60616d78c 100644 --- a/src/core/treeNodeValue.ts +++ b/src/core/treeNodeValue.ts @@ -1,5 +1,8 @@ import type { RouteRecordRaw } from 'vue-router' -import { CustomRouteBlock } from './customBlock' +import { + CustomRouteBlock, + CustomRouteBlockQueryParamOptions, +} from './customBlock' import { joinPath, mergeRouteRecordOverride, warn } from './utils' export const enum TreeNodeType { @@ -11,9 +14,23 @@ export const enum TreeNodeType { export interface RouteRecordOverride extends Partial> { name?: string | undefined | false + + /** + * Param Parsers information. + */ + params?: { + path?: Record + + query?: Record + } +} + +export interface RouteRecordOverrideQueryParamOptions + extends CustomRouteBlockQueryParamOptions { + default?: string } -export type SubSegment = string | TreeRouteParam +export type SubSegment = string | TreePathParam // internal name used for overrides done by the user at build time export const EDITS_OVERRIDE_NAME = '@@edits' @@ -45,7 +62,7 @@ class _TreeNodeValueBase { * Overrides defined by each file. The map is necessary to handle named views. */ private _overrides = new Map() - // TODO: cache the overrides generation + // TODO: measure perf bottlenecks with large trees and use caching if it can potentially improve /** * View name (Vue Router feature) mapped to their corresponding file. By default, the view name is `default` unless @@ -88,6 +105,48 @@ class _TreeNodeValueBase { return joinPath(this.parent?.fullPath ?? '', pathSegment) } + /** + * Gets all the query params for the node. This does not include params from parent nodes. + */ + get queryParams(): TreeQueryParam[] { + const paramsQuery = this.overrides.params?.query + if (!paramsQuery) { + return [] + } + + const queryParams: TreeQueryParam[] = [] + + for (var paramName in paramsQuery) { + var config = paramsQuery[paramName] + // shouldn't happen + if (!config) continue + if (typeof config === 'string') { + queryParams.push({ + paramName, + parser: config, + format: 'value', + }) + } else { + queryParams.push({ + paramName, + parser: config.parser || null, + format: config.format || 'value', + defaultValue: config.default, + }) + } + } + + return queryParams + } + + /** + * Gets all the params for the node including path and query params. This + * does not include params from parent nodes. + */ + get params(): (TreePathParam | TreeQueryParam)[] { + return [...(this.isParam() ? this.pathParams : []), ...this.queryParams] + } + toString(): string { return this.pathSegment || '' } @@ -179,9 +238,24 @@ class _TreeNodeValueBase { } } +/** + * - Static + * - Static + Custom Param (subSegments) + * - Static + Param (subSegments) + * - Custom Param + * - Param + * - CatchAll + */ + +/** + * Static path like `/users`, `/users/list`, etc + * @extends _TreeNodeValueBase + */ export class TreeNodeValueStatic extends _TreeNodeValueBase { override _type: TreeNodeType.static = TreeNodeType.static + readonly score = [300] + constructor( rawSegment: string, parent: TreeNodeValue | undefined, @@ -195,6 +269,8 @@ export class TreeNodeValueGroup extends _TreeNodeValueBase { override _type: TreeNodeType.group = TreeNodeType.group groupName: string + readonly score = [300] + constructor( rawSegment: string, parent: TreeNodeValue | undefined, @@ -206,27 +282,154 @@ export class TreeNodeValueGroup extends _TreeNodeValueBase { } } -export interface TreeRouteParam { +export interface TreePathParam { paramName: string modifier: string optional: boolean repeatable: boolean isSplat: boolean + parser: string | null +} + +export interface TreeQueryParam { + paramName: string + + queryKey?: string + + parser: string | null + + format: 'value' | 'array' + + /** + * Expression to be passed as is to the default value of the param. + */ + defaultValue?: string +} + +/** + * Checks if a TreePathParam or TreeQueryParam is optional. + * + * @internal + */ +export function isTreeParamOptional( + param: TreePathParam | TreeQueryParam +): boolean { + if ('optional' in param) { + return param.optional + } + return param.defaultValue !== undefined +} + +/** + * Checks if a TreePathParam or TreeQueryParam is repeatable (array). + * + * @internal + */ +export function isTreeParamRepeatable( + param: TreePathParam | TreeQueryParam +): boolean { + if ('repeatable' in param) { + return param.repeatable + } + return param.format === 'array' +} + +/** + * Checks if a param is a TreePathParam. + * + * @internal + */ +export function isTreePathParam( + param: TreePathParam | TreeQueryParam +): param is TreePathParam { + return 'modifier' in param } +/** + * To escape regex characters in the path segment. + * @internal + */ +const REGEX_CHARS_RE = /[.+*?^${}()[\]/\\]/g + +/** + * Escapes regex characters in a string to be used in a regex pattern. + * @param str - The string to escape. + * + * @internal + */ +export const escapeRegex = (str: string): string => + str.replace(REGEX_CHARS_RE, '\\$&') + export class TreeNodeValueParam extends _TreeNodeValueBase { - params: TreeRouteParam[] override _type: TreeNodeType.param = TreeNodeType.param constructor( rawSegment: string, parent: TreeNodeValue | undefined, - params: TreeRouteParam[], + public pathParams: TreePathParam[], pathSegment: string, subSegments: SubSegment[] ) { super(rawSegment, parent, pathSegment, subSegments) - this.params = params + } + + // Calculate score for each subsegment to handle mixed static/param parts + get score(): number[] { + return this.subSegments.map((segment) => { + if (typeof segment === 'string') { + // Static subsegment gets highest score + return 300 + } else { + // Parameter subsegment - calculate malus based on param properties + const malus = segment.isSplat + ? 500 + : (segment.optional ? 10 : 0) + (segment.repeatable ? 20 : 0) + + return 80 - malus + } + }) + } + + get re(): string { + const paramRe = this.subSegments + .filter(Boolean) + .map((segment) => { + if (typeof segment === 'string') { + return escapeRegex(segment) + } + + if (segment.isSplat) { + return '(.*)' + } + + let re = segment.repeatable ? '(.+?)' : '([^/]+?)' + + if (segment.optional) { + re += '?' + } + + return re + }) + .join('') + + return paramRe + } + + override toString(): string { + const params = + this.params.length > 0 + ? ` 𝑥(` + + this.params + .map( + (p) => + ('format' in p ? '?' : '') + + `${p.paramName}${'modifier' in p ? p.modifier : ''}` + + (p.parser ? '=' + p.parser : '') + ) + .join(', ') + + ')' + : '' + return `${this.pathSegment}` + params } } @@ -308,17 +511,17 @@ export function createTreeNodeValue( } } - const [pathSegment, params, subSegments] = + const [pathSegment, pathParams, subSegments] = options.format === 'path' ? parseRawPathSegment(segment) : // by default, we use the file format parseFileSegment(segment, options) - if (params.length) { + if (pathParams.length) { return new TreeNodeValueParam( segment, parent, - params, + pathParams, pathSegment, subSegments ) @@ -331,6 +534,7 @@ const enum ParseFileSegmentState { static, paramOptional, // within [[]] or [] param, // within [] + paramParser, // [param=type] modifier, // after the ] } @@ -359,13 +563,14 @@ const IS_VARIABLE_CHAR_RE = /[0-9a-zA-Z_]/ function parseFileSegment( segment: string, { dotNesting = true }: ParseSegmentOptions = {} -): [string, TreeRouteParam[], SubSegment[]] { +): [string, TreePathParam[], SubSegment[]] { let buffer = '' + let paramParserBuffer = '' let state: ParseFileSegmentState = ParseFileSegmentState.static - const params: TreeRouteParam[] = [] + const params: TreePathParam[] = [] let pathSegment = '' const subSegments: SubSegment[] = [] - let currentTreeRouteParam: TreeRouteParam = createEmptyRouteParam() + let currentTreeRouteParam: TreePathParam = createEmptyRouteParam() // position in segment let pos = 0 @@ -379,6 +584,7 @@ function parseFileSegment( subSegments.push(buffer) } else if (state === ParseFileSegmentState.modifier) { currentTreeRouteParam.paramName = buffer + currentTreeRouteParam.parser = paramParserBuffer || null currentTreeRouteParam.modifier = currentTreeRouteParam.optional ? currentTreeRouteParam.repeatable ? '*' @@ -386,7 +592,11 @@ function parseFileSegment( : currentTreeRouteParam.repeatable ? '+' : '' + + // reset the buffers buffer = '' + paramParserBuffer = '' + pathSegment += `:${currentTreeRouteParam.paramName}${ currentTreeRouteParam.isSplat ? '(.*)' @@ -409,7 +619,10 @@ function parseFileSegment( if (state === ParseFileSegmentState.static) { if (c === '[') { - consumeBuffer() + // avoid adding the leading empty string for segments that start with a param + if (buffer) { + consumeBuffer() + } // check if it's an optional param or not state = ParseFileSegmentState.paramOptional } else { @@ -438,6 +651,9 @@ function parseFileSegment( } else if (c === '.') { currentTreeRouteParam.isSplat = true pos += 2 // skip the other 2 dots + } else if (c === '=') { + state = ParseFileSegmentState.paramParser + paramParserBuffer = '' } else { buffer += c } @@ -451,12 +667,23 @@ function parseFileSegment( consumeBuffer() // start again state = ParseFileSegmentState.static + } else if (state === ParseFileSegmentState.paramParser) { + if (c === ']') { + if (currentTreeRouteParam.optional) { + // skip the next ] + pos++ + } + state = ParseFileSegmentState.modifier + } else { + paramParserBuffer += c + } } } if ( state === ParseFileSegmentState.param || - state === ParseFileSegmentState.paramOptional + state === ParseFileSegmentState.paramOptional || + state === ParseFileSegmentState.paramParser ) { throw new Error(`Invalid segment: "${segment}"`) } @@ -487,12 +714,12 @@ const IS_MODIFIER_RE = /[+*?]/ */ function parseRawPathSegment( segment: string -): [string, TreeRouteParam[], SubSegment[]] { +): [string, TreePathParam[], SubSegment[]] { let buffer = '' let state: ParseRawPathSegmentState = ParseRawPathSegmentState.static - const params: TreeRouteParam[] = [] + const params: TreePathParam[] = [] const subSegments: SubSegment[] = [] - let currentTreeRouteParam: TreeRouteParam = createEmptyRouteParam() + let currentTreeRouteParam: TreePathParam = createEmptyRouteParam() // position in segment let pos = 0 @@ -618,9 +845,10 @@ function parseRawPathSegment( * * @returns an empty route param */ -function createEmptyRouteParam(): TreeRouteParam { +function createEmptyRouteParam(): TreePathParam { return { paramName: '', + parser: null, modifier: '', optional: false, repeatable: false, diff --git a/src/core/utils.ts b/src/core/utils.ts index 63f269b49..cd69c96f4 100644 --- a/src/core/utils.ts +++ b/src/core/utils.ts @@ -1,5 +1,5 @@ import { TreeNode } from './tree' -import type { RouteRecordOverride, TreeRouteParam } from './treeNodeValue' +import type { RouteRecordOverride, TreePathParam } from './treeNodeValue' import { pascalCase } from 'scule' import { ResolvedOptions, @@ -119,7 +119,7 @@ export function joinPath(...paths: string[]): string { return result || '/' } -function paramToName({ paramName, modifier, isSplat }: TreeRouteParam) { +function paramToName({ paramName, modifier, isSplat }: TreePathParam) { return `${isSplat ? '$' : ''}${ paramName.charAt(0).toUpperCase() + paramName.slice(1) }${ @@ -199,6 +199,17 @@ export function mergeRouteRecordOverride( merged[key] = newAlias.concat(a.alias || [], b.alias || []) } else if (key === 'meta') { merged[key] = mergeDeep(a[key] || {}, b[key] || {}) + } else if (key === 'params') { + merged[key] = { + path: { + ...a[key]?.path, + ...b[key]?.path, + }, + query: { + ...a[key]?.query, + ...b[key]?.query, + }, + } } else { // @ts-expect-error: TS cannot see it's the same key merged[key] = b[key] ?? a[key] @@ -319,6 +330,22 @@ export class ImportsMap { return this } + /** + * Check if the given path has the given import name. + * + * @param path - the path to check + * @param name - the import name to check + */ + has(path: string, name: string): boolean { + return this.map.has(path) && this.map.get(path)!.has(name) + } + + /** + * Add a default import. Alias for `add(path, { name: 'default', as })`. + * + * @param path - the path to import from + * @param as - the name to import as + */ addDefault(path: string, as: string): this { return this.add(path, { name: 'default', as }) } diff --git a/src/core/vite/index.ts b/src/core/vite/index.ts index 1fa1cb974..f59b79782 100644 --- a/src/core/vite/index.ts +++ b/src/core/vite/index.ts @@ -1,33 +1,17 @@ import { type ViteDevServer } from 'vite' import { type ServerContext } from '../../options' -import { MODULE_ROUTES_PATH, asVirtualId } from '../moduleConstants' +import { + MODULE_RESOLVER_PATH, + MODULE_ROUTES_PATH, + asVirtualId, +} from '../moduleConstants' export function createViteContext(server: ViteDevServer): ServerContext { function invalidate(path: string) { - const { moduleGraph } = server - const foundModule = moduleGraph.getModuleById(path) + const foundModule = server.moduleGraph.getModuleById(path) + // console.log(`🟣 Invalidating module: ${path}, found: ${!!foundModule}`) if (foundModule) { - moduleGraph.invalidateModule(foundModule, undefined, undefined, true) - // for (const mod of foundModule.importers) { - // console.log(`Invalidating ${mod.url}`) - // moduleGraph.invalidateModule(mod) - // } - setTimeout(() => { - console.log(`Sending update for ${foundModule.url}`) - server.ws.send({ - type: 'update', - updates: [ - { - acceptedPath: path, - path: path, - // NOTE: this was in the - // timestamp: ROUTES_LAST_LOAD_TIME.value, - timestamp: Date.now(), - type: 'js-update', - }, - ], - }) - }, 100) + return server.reloadModule(foundModule) } return !!foundModule } @@ -43,11 +27,17 @@ export function createViteContext(server: ViteDevServer): ServerContext { * Triggers HMR for the vue-router/auto-routes module. */ async function updateRoutes() { - const modId = asVirtualId(MODULE_ROUTES_PATH) - const mod = server.moduleGraph.getModuleById(modId) - if (mod) { - return server.reloadModule(mod) - } + const autoRoutesMod = server.moduleGraph.getModuleById( + asVirtualId(MODULE_ROUTES_PATH) + ) + const autoResolvedMod = server.moduleGraph.getModuleById( + asVirtualId(MODULE_RESOLVER_PATH) + ) + + await Promise.all([ + autoRoutesMod && server.reloadModule(autoRoutesMod), + autoResolvedMod && server.reloadModule(autoResolvedMod), + ]) } return { diff --git a/src/data-loaders/meta-extensions.ts b/src/data-loaders/meta-extensions.ts index 5835aa53c..0949cd7c5 100644 --- a/src/data-loaders/meta-extensions.ts +++ b/src/data-loaders/meta-extensions.ts @@ -10,6 +10,7 @@ import type { IS_SSR_KEY, } from './symbols' import { type NavigationResult } from './navigation-guard' +import { type RouteLocationNormalizedLoaded } from 'vue-router' /** * Map type for the entries used by data loaders. diff --git a/src/index.ts b/src/index.ts index 623d5304b..573ee818d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,7 @@ import { ROUTES_LAST_LOAD_TIME, VIRTUAL_PREFIX, DEFINE_PAGE_QUERY_RE, + MODULE_RESOLVER_PATH, } from './core/moduleConstants' import { Options, @@ -20,7 +21,6 @@ import { import { createViteContext } from './core/vite' import { join } from 'pathe' import { appendExtensionListToPattern } from './core/utils' -import { MACRO_DEFINE_PAGE_QUERY } from './core/definePage' import { createAutoExportPlugin } from './data-loaders/auto-exports' export type * from './types' @@ -66,6 +66,7 @@ export default createUnplugin((opt = {}, _meta) => { include: [ new RegExp(`^${MODULE_VUE_ROUTER_AUTO}$`), new RegExp(`^${MODULE_ROUTES_PATH}$`), + new RegExp(`^${MODULE_RESOLVER_PATH}$`), routeBlockQueryRE, ], }, @@ -73,7 +74,11 @@ export default createUnplugin((opt = {}, _meta) => { handler(id) { // vue-router/auto // vue-router/auto-routes - if (id === MODULE_ROUTES_PATH || id === MODULE_VUE_ROUTER_AUTO) { + if ( + id === MODULE_ROUTES_PATH || + id === MODULE_VUE_ROUTER_AUTO || + id === MODULE_RESOLVER_PATH + ) { // must be a virtual module return asVirtualId(id) } @@ -84,8 +89,8 @@ export default createUnplugin((opt = {}, _meta) => { }, }, - buildStart() { - return ctx.scanPages(options.watch) + async buildStart() { + await ctx.scanPages(options.watch) }, buildEnd() { @@ -113,6 +118,7 @@ export default createUnplugin((opt = {}, _meta) => { new RegExp(`^${ROUTE_BLOCK_ID}$`), new RegExp(`^${VIRTUAL_PREFIX}${MODULE_VUE_ROUTER_AUTO}$`), new RegExp(`^${VIRTUAL_PREFIX}${MODULE_ROUTES_PATH}$`), + new RegExp(`^${VIRTUAL_PREFIX}${MODULE_RESOLVER_PATH}$`), ], }, }, @@ -136,6 +142,12 @@ export default createUnplugin((opt = {}, _meta) => { return ctx.generateRoutes() } + // vue-router/auto-resolver + if (resolvedId === MODULE_RESOLVER_PATH) { + ROUTES_LAST_LOAD_TIME.update() + return ctx.generateResolver() + } + // vue-router/auto if (resolvedId === MODULE_VUE_ROUTER_AUTO) { return ctx.generateVueRouterProxy() @@ -145,46 +157,11 @@ export default createUnplugin((opt = {}, _meta) => { }, }, - // improves DX + // for HMR vite: { configureServer(server) { ctx.setServerContext(createViteContext(server)) }, - - handleHotUpdate: { - order: 'post', - handler({ server, file, modules }) { - // console.log(`🔥 HMR ${file}`) - const moduleList = server.moduleGraph.getModulesByFile(file) - const definePageModule = Array.from(moduleList || []).find( - (mod) => { - return mod?.id && MACRO_DEFINE_PAGE_QUERY.test(mod.id) - } - ) - - if (definePageModule) { - // console.log(`Updating ${definePageModule.file}`) - const routesModule = server.moduleGraph.getModuleById( - asVirtualId(MODULE_ROUTES_PATH) - ) - - if (!routesModule) { - console.error('🔥 HMR routes module not found') - return - } - - return [ - ...modules, - // TODO: only if the definePage changed - definePageModule, - // TODO: only if ether the definePage or the route block changed - routesModule, - ] - } - - return // for ts - }, - }, }, }, ] diff --git a/src/options.ts b/src/options.ts index 7fe1f7e0f..0209385ca 100644 --- a/src/options.ts +++ b/src/options.ts @@ -225,9 +225,27 @@ export interface Options { * page component. */ autoExportsDataLoaders?: string | string[] + + /** + * Enable experimental support for the new custom resolvers. + */ + paramParsers?: boolean | ParamParsersOptions } } +export interface ParamParsersOptions { + /** + * Folder(s) to scan for param matchers. Set to an empty array to disable the feature. + * + * @default `['src/params']` + */ + dir?: string | string[] +} + +export const DEFAULT_PARAM_PARSERS_OPTIONS = { + dir: ['src/params'], +} satisfies Required + export const DEFAULT_OPTIONS = { extensions: ['.vue'], exclude: [], @@ -303,14 +321,39 @@ export function resolveOptions(options: Options) { src: resolve(root, routeOption.src), })) - const experimental = { ...options.experimental } - - if (experimental.autoExportsDataLoaders) { - experimental.autoExportsDataLoaders = ( - Array.isArray(experimental.autoExportsDataLoaders) - ? experimental.autoExportsDataLoaders - : [experimental.autoExportsDataLoaders] - ).map((path) => resolve(root, path)) + const paramParsers = options.experimental?.paramParsers + ? options.experimental.paramParsers === true + ? DEFAULT_PARAM_PARSERS_OPTIONS + : { + ...DEFAULT_PARAM_PARSERS_OPTIONS, + ...options.experimental.paramParsers, + } + : // this way we can do paramParsers?.dir + undefined + + const paramParsersDir = ( + paramParsers?.dir + ? isArray(paramParsers.dir) + ? paramParsers.dir + : [paramParsers.dir] + : [] + ).map((dir) => resolve(root, dir)) + + const autoExportsDataLoaders = options.experimental?.autoExportsDataLoaders + ? (isArray(options.experimental.autoExportsDataLoaders) + ? options.experimental.autoExportsDataLoaders + : [options.experimental.autoExportsDataLoaders] + ).map((path) => resolve(root, path)) + : undefined + + const experimental = { + ...options.experimental, + autoExportsDataLoaders, + // keep undefined if paramParsers is not set + paramParsers: paramParsers && { + ...paramParsers, + dir: paramParsersDir, + }, } if (options.extensions) { @@ -351,6 +394,9 @@ export function resolveOptions(options: Options) { } } +/** + * @internal + */ export type ResolvedOptions = ReturnType /** diff --git a/src/runtime.ts b/src/runtime.ts index 03e97b865..f34adb9b0 100644 --- a/src/runtime.ts +++ b/src/runtime.ts @@ -1,12 +1,4 @@ -import type { RouteRecordRaw } from 'vue-router' - -/** - * Defines properties of the route for the current page component. - * - * @param route - route information to be added to this page - * @deprecated - use `definePage` instead - */ -export const _definePage = (route: DefinePage) => route +import type { RouteRecordRaw, TypesConfig } from 'vue-router' /** * Defines properties of the route for the current page component. @@ -58,4 +50,65 @@ export interface DefinePage * Can be set to `false` to remove the name from types. */ name?: string | false + + /** + * Custom parameters for the route. Requires `experimental.paramParsers` enabled. + * + * @experimental + */ + params?: { + path?: Record + + /** + * Parameters extracted from the query. + */ + query?: Record + } +} + +export type ParamParserType_Native = 'int' | 'bool' + +export type ParamParserType = + | (TypesConfig extends Record<'ParamParsers', infer ParamParsers> + ? ParamParsers + : never) + | ParamParserType_Native + +/** + * Configures how to extract a route param from a specific query parameter. + */ +export interface DefinePageQueryParamOptions { + /** + * The type of the query parameter. Allowed values are native param parsers + * and any parser in the {@link https://uvr.esm.is/TODO | params folder }. If + * not provided, the value will kept as is. + */ + parser?: ParamParserType + + // TODO: allow customizing the name in the query string + // queryKey?: string + + /** + * Default value if the query parameter is missing or if the match fails + * (e.g. a invalid number is passed to the int param parser). If not provided + * and the param parser throws, the route will not match. + */ + default?: (() => T) | T + + /** + * How to format the query parameter value. + * + * - 'value' - keep the first value only and pass that to parser + * - 'array' - keep all values (even one or none) as an array and pass that to parser + * + * @default 'value' + */ + format?: 'value' | 'array' } + +/** + * TODO: native parsers ideas: + * - json -> just JSON.parse(value) + * - boolean -> 'true' | 'false' -> boolean + * - number -> Number(value) -> NaN if not a number + */ diff --git a/src/types.ts b/src/types.ts index af4b1104c..db3554a38 100644 --- a/src/types.ts +++ b/src/types.ts @@ -6,7 +6,9 @@ export type { Options } from './options' export type { TreeNode } from './core/tree' export type { - TreeNodeValueParam, + TreeNodeValue, TreeNodeValueStatic, + TreeNodeValueParam, + TreeNodeValueGroup, } from './core/treeNodeValue' export type { EditableTreeNode } from './core/extendRoutes' diff --git a/src/utils/index.ts b/src/utils/index.ts index c74ccc691..ce07fa035 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,3 +1,5 @@ +import { access, constants } from 'node:fs/promises' + /** * Maybe a promise maybe not * @internal @@ -14,3 +16,12 @@ export type LiteralStringUnion = // for highlighting export const ts = String.raw + +export async function fileExists(filePath: string) { + try { + await access(filePath, constants.F_OK) + return true + } catch { + return false + } +} diff --git a/tsconfig.json b/tsconfig.json index 83731b6cd..75cb476c5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,15 +14,13 @@ "dist" ], "compilerOptions": { - "baseUrl": ".", + // this makes auto import canonical (e.g. 'src/utils' instead of '../utils') + // "baseUrl": ".", "rootDir": ".", "jsx": "preserve", "target": "ESNext", "module": "ESNext", - "lib": [ - "ESNext", - "DOM" - ], + "lib": ["ESNext", "DOM"], "moduleResolution": "Bundler", "skipDefaultLibCheck": true, "skipLibCheck": true, @@ -42,25 +40,16 @@ "strictNullChecks": true, "resolveJsonModule": true, "paths": { - "unplugin-vue-router": [ - "./src/index.ts" - ], - "unplugin-vue-router/types": [ - "./src/types.ts" - ], - "unplugin-vue-router/runtime": [ - "./src/runtime.ts" - ], + "unplugin-vue-router": ["./src/index.ts"], + "unplugin-vue-router/types": ["./src/types.ts"], + "unplugin-vue-router/runtime": ["./src/runtime.ts"], "unplugin-vue-router/data-loaders": [ "./src/data-loaders/entries/index.ts" ] }, - "types": [ - "node", - "vite/client" - ] + "types": ["node", "vite/client"] }, "vueCompilerOptions": { "plugins": [] - }, + } } diff --git a/vitest.config.ts b/vitest.config.ts index acd118418..37ace1e22 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -2,7 +2,6 @@ import { defineConfig } from 'vitest/config' import Vue from '@vitejs/plugin-vue' import { fileURLToPath, URL } from 'url' -const __dirname = new URL('.', import.meta.url).pathname export default defineConfig({ resolve: { alias: [ @@ -20,6 +19,7 @@ export default defineConfig({ }, ], }, + plugins: [Vue()], test: { diff --git a/vitest.workspace.js b/vitest.workspace.js deleted file mode 100644 index 45759c07c..000000000 --- a/vitest.workspace.js +++ /dev/null @@ -1,3 +0,0 @@ -import { defineWorkspace } from 'vitest/config' - -export default defineWorkspace(['./vitest.config.ts'])