From 474596f30b422aa817c47b56394e0cfa0c7a61e7 Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Tue, 25 Jun 2024 15:56:55 +0200 Subject: [PATCH 01/40] feat: wip new matcher --- packages/router/src/encoding.ts | 4 +- packages/router/src/matcher/index.ts | 12 +- packages/router/src/new-matcher/index.ts | 1 + .../src/new-matcher/matcher-location.ts | 32 ++ .../router/src/new-matcher/matcher-pattern.ts | 144 +++++++++ .../router/src/new-matcher/matcher.spec.ts | 105 +++++++ .../router/src/new-matcher/matcher.test-d.ts | 16 + packages/router/src/new-matcher/matcher.ts | 292 ++++++++++++++++++ packages/router/src/utils/index.ts | 5 +- 9 files changed, 602 insertions(+), 9 deletions(-) create mode 100644 packages/router/src/new-matcher/index.ts create mode 100644 packages/router/src/new-matcher/matcher-location.ts create mode 100644 packages/router/src/new-matcher/matcher-pattern.ts create mode 100644 packages/router/src/new-matcher/matcher.spec.ts create mode 100644 packages/router/src/new-matcher/matcher.test-d.ts create mode 100644 packages/router/src/new-matcher/matcher.ts diff --git a/packages/router/src/encoding.ts b/packages/router/src/encoding.ts index 69b338a65..74d304928 100644 --- a/packages/router/src/encoding.ts +++ b/packages/router/src/encoding.ts @@ -22,7 +22,7 @@ import { warn } from './warning' const HASH_RE = /#/g // %23 const AMPERSAND_RE = /&/g // %26 -const SLASH_RE = /\//g // %2F +export const SLASH_RE = /\//g // %2F const EQUAL_RE = /=/g // %3D const IM_RE = /\?/g // %3F export const PLUS_RE = /\+/g // %2B @@ -58,7 +58,7 @@ const ENC_SPACE_RE = /%20/g // } * @param text - string to encode * @returns encoded string */ -function commonEncode(text: string | number): string { +export function commonEncode(text: string | number): string { return encodeURI('' + text) .replace(ENC_PIPE_RE, '|') .replace(ENC_BRACKET_OPEN_RE, '[') diff --git a/packages/router/src/matcher/index.ts b/packages/router/src/matcher/index.ts index 9d787ddbc..fe951f7ad 100644 --- a/packages/router/src/matcher/index.ts +++ b/packages/router/src/matcher/index.ts @@ -271,7 +271,7 @@ export function createRouterMatcher( name = matcher.record.name params = assign( // paramsFromLocation is a new object - paramsFromLocation( + pickParams( currentLocation.params, // only keep params that exist in the resolved location // only keep optional params coming from a parent record @@ -285,7 +285,7 @@ export function createRouterMatcher( // discard any existing params in the current location that do not exist here // #1497 this ensures better active/exact matching location.params && - paramsFromLocation( + pickParams( location.params, matcher.keys.map(k => k.name) ) @@ -365,7 +365,13 @@ export function createRouterMatcher( } } -function paramsFromLocation( +/** + * Picks an object param to contain only specified keys. + * + * @param params - params object to pick from + * @param keys - keys to pick + */ +function pickParams( params: MatcherLocation['params'], keys: string[] ): MatcherLocation['params'] { diff --git a/packages/router/src/new-matcher/index.ts b/packages/router/src/new-matcher/index.ts new file mode 100644 index 000000000..17910f62f --- /dev/null +++ b/packages/router/src/new-matcher/index.ts @@ -0,0 +1 @@ +export { createCompiledMatcher } from './matcher' diff --git a/packages/router/src/new-matcher/matcher-location.ts b/packages/router/src/new-matcher/matcher-location.ts new file mode 100644 index 000000000..bb44326b2 --- /dev/null +++ b/packages/router/src/new-matcher/matcher-location.ts @@ -0,0 +1,32 @@ +import type { LocationQueryRaw } from '../query' +import type { MatcherName } from './matcher' + +// the matcher can serialize and deserialize params +export type MatcherParamsFormatted = Record + +export interface MatcherLocationAsName { + name: MatcherName + params: MatcherParamsFormatted + query?: LocationQueryRaw + hash?: string + + path?: undefined +} + +export interface MatcherLocationAsPath { + path: string + query?: LocationQueryRaw + hash?: string + + name?: undefined + params?: undefined +} + +export interface MatcherLocationAsRelative { + params?: MatcherParamsFormatted + query?: LocationQueryRaw + hash?: string + + name?: undefined + path?: undefined +} diff --git a/packages/router/src/new-matcher/matcher-pattern.ts b/packages/router/src/new-matcher/matcher-pattern.ts new file mode 100644 index 000000000..021b975c0 --- /dev/null +++ b/packages/router/src/new-matcher/matcher-pattern.ts @@ -0,0 +1,144 @@ +import type { + MatcherName, + MatcherPathParams, + MatcherQueryParams, + MatcherQueryParamsValue, +} from './matcher' +import type { MatcherParamsFormatted } from './matcher-location' + +export interface MatcherPattern { + /** + * Name of the matcher. Unique across all matchers. + */ + name: MatcherName + + /** + * Extracts from a formatted, unencoded params object the ones belonging to the path, query, and hash. If any of them is missing, returns `null`. TODO: throw instead? + * @param params - Params to extract from. + */ + unformatParams( + params: MatcherParamsFormatted + ): [path: MatcherPathParams, query: MatcherQueryParams, hash: string | null] + + /** + * Extracts the defined params from an encoded path, query, and hash parsed from a URL. Does not apply formatting or + * decoding. If the URL does not match the pattern, returns `null`. + * + * @example + * ```ts + * const pattern = createPattern('/foo', { + * path: {}, // nothing is used from the path + * query: { used: String }, // we require a `used` query param + * }) + * // /?used=2 + * pattern.parseLocation({ path: '/', query: { used: '' }, hash: '' }) // null + * // /foo?used=2¬Used¬Used=2#hello + * pattern.parseLocation({ path: '/foo', query: { used: '2', notUsed: [null, '2']}, hash: '#hello' }) + * // { used: '2' } // we extract the required params + * // /foo?used=2#hello + * pattern.parseLocation({ path: '/foo', query: {}, hash: '#hello' }) + * // null // the query param is missing + * ``` + */ + matchLocation(location: { + path: string + query: MatcherQueryParams + hash: string + }): [path: MatcherPathParams, query: MatcherQueryParams, hash: string | null] + + /** + * Takes encoded params object to form the `path`, + * @param path - encoded path params + */ + buildPath(path: MatcherPathParams): string + + /** + * Runs the decoded params through the formatting functions if any. + * @param params - Params to format. + */ + formatParams( + path: MatcherPathParams, + query: MatcherQueryParams, + hash: string | null + ): MatcherParamsFormatted +} + +interface PatternParamOptions_Base { + get: (value: MatcherQueryParamsValue) => T + set?: (value: T) => MatcherQueryParamsValue + default?: T | (() => T) +} + +export interface PatternParamOptions extends PatternParamOptions_Base {} + +export interface PatternQueryParamOptions + extends PatternParamOptions_Base { + get: (value: MatcherQueryParamsValue) => T + set?: (value: T) => MatcherQueryParamsValue +} + +// TODO: allow more than strings +export interface PatternHashParamOptions + extends PatternParamOptions_Base {} + +export interface MatcherPatternPath { + match(path: string): MatcherPathParams + format(params: MatcherPathParams): MatcherParamsFormatted +} + +export interface MatcherPatternQuery { + match(query: MatcherQueryParams): MatcherQueryParams + format(params: MatcherQueryParams): MatcherParamsFormatted +} + +export interface MatcherPatternHash { + /** + * Check if the hash matches a pattern and returns it, still encoded with its leading `#`. + * @param hash - encoded hash + */ + match(hash: string): string + format(hash: string): MatcherParamsFormatted +} + +export class MatcherPatternImpl implements MatcherPattern { + constructor( + public name: MatcherName, + private path: MatcherPatternPath, + private query?: MatcherPatternQuery, + private hash?: MatcherPatternHash + ) {} + + matchLocation(location: { + path: string + query: MatcherQueryParams + hash: string + }): [path: MatcherPathParams, query: MatcherQueryParams, hash: string] { + return [ + this.path.match(location.path), + this.query?.match(location.query) ?? {}, + this.hash?.match(location.hash) ?? '', + ] + } + + formatParams( + path: MatcherPathParams, + query: MatcherQueryParams, + hash: string + ): MatcherParamsFormatted { + return { + ...this.path.format(path), + ...this.query?.format(query), + ...this.hash?.format(hash), + } + } + + buildPath(path: MatcherPathParams): string { + return '' + } + + unformatParams( + params: MatcherParamsFormatted + ): [path: MatcherPathParams, query: MatcherQueryParams, hash: string | null] { + throw new Error('Method not implemented.') + } +} diff --git a/packages/router/src/new-matcher/matcher.spec.ts b/packages/router/src/new-matcher/matcher.spec.ts new file mode 100644 index 000000000..2660abdda --- /dev/null +++ b/packages/router/src/new-matcher/matcher.spec.ts @@ -0,0 +1,105 @@ +import { describe, expect, it } from 'vitest' +import { MatcherPatternImpl, MatcherPatternPath } from './matcher-pattern' +import { createCompiledMatcher } from './matcher' + +function createMatcherPattern( + ...args: ConstructorParameters +) { + return new MatcherPatternImpl(...args) +} + +const EMPTY_PATH_PATTERN_MATCHER = { + match: (path: string) => ({}), + format: (params: {}) => ({}), +} satisfies MatcherPatternPath + +describe('Matcher', () => { + describe('resolve()', () => { + it('resolves string locations with no params', () => { + const matcher = createCompiledMatcher() + matcher.addRoute( + createMatcherPattern(Symbol('foo'), EMPTY_PATH_PATTERN_MATCHER) + ) + + expect(matcher.resolve('/foo?a=a&b=b#h')).toMatchObject({ + path: '/foo', + params: {}, + query: { a: 'a', b: 'b' }, + hash: '#h', + }) + }) + + it('resolves string locations with params', () => { + const matcher = createCompiledMatcher() + matcher.addRoute( + // /users/:id + createMatcherPattern(Symbol('foo'), { + match: (path: string) => { + const match = path.match(/^\/foo\/([^/]+?)$/) + if (!match) throw new Error('no match') + return { id: match[1] } + }, + format: (params: { id: string }) => ({ id: Number(params.id) }), + }) + ) + + expect(matcher.resolve('/foo/1')).toMatchObject({ + path: '/foo/1', + params: { id: 1 }, + query: {}, + hash: '', + }) + expect(matcher.resolve('/foo/54')).toMatchObject({ + path: '/foo/54', + params: { id: 54 }, + query: {}, + hash: '', + }) + }) + + it('resolve string locations with query', () => { + const matcher = createCompiledMatcher() + matcher.addRoute( + createMatcherPattern(Symbol('foo'), EMPTY_PATH_PATTERN_MATCHER, { + match: query => ({ + id: Array.isArray(query.id) ? query.id[0] : query.id, + }), + format: (params: { id: string }) => ({ id: Number(params.id) }), + }) + ) + + expect(matcher.resolve('/foo?id=100')).toMatchObject({ + hash: '', + params: { + id: 100, + }, + path: '/foo', + query: { + id: '100', + }, + }) + }) + + it('resolves string locations with hash', () => { + const matcher = createCompiledMatcher() + matcher.addRoute( + createMatcherPattern( + Symbol('foo'), + EMPTY_PATH_PATTERN_MATCHER, + undefined, + { + match: hash => hash, + format: hash => ({ a: hash.slice(1) }), + } + ) + ) + + expect(matcher.resolve('/foo#bar')).toMatchObject({ + hash: '#bar', + params: { a: 'bar' }, + path: '/foo', + query: {}, + }) + }) + }) +}) diff --git a/packages/router/src/new-matcher/matcher.test-d.ts b/packages/router/src/new-matcher/matcher.test-d.ts new file mode 100644 index 000000000..fbf150e2e --- /dev/null +++ b/packages/router/src/new-matcher/matcher.test-d.ts @@ -0,0 +1,16 @@ +import { describe, it } from 'vitest' +import { NEW_MatcherLocationResolved, createCompiledMatcher } from './matcher' + +describe('Matcher', () => { + it('resolves locations', () => { + const matcher = createCompiledMatcher() + matcher.resolve('/foo') + // @ts-expect-error: needs currentLocation + matcher.resolve('foo') + matcher.resolve('foo', {} as NEW_MatcherLocationResolved) + matcher.resolve({ name: 'foo', params: {} }) + // @ts-expect-error: needs currentLocation + matcher.resolve({ params: { id: 1 } }) + matcher.resolve({ params: { id: 1 } }, {} as NEW_MatcherLocationResolved) + }) +}) diff --git a/packages/router/src/new-matcher/matcher.ts b/packages/router/src/new-matcher/matcher.ts new file mode 100644 index 000000000..bd48a1246 --- /dev/null +++ b/packages/router/src/new-matcher/matcher.ts @@ -0,0 +1,292 @@ +import { type LocationQuery, parseQuery, normalizeQuery } from '../query' +import type { MatcherPattern } from './matcher-pattern' +import { warn } from '../warning' +import { + SLASH_RE, + encodePath, + encodeQueryValue as _encodeQueryValue, +} from '../encoding' +import { parseURL } from '../location' +import type { + MatcherLocationAsName, + MatcherLocationAsRelative, + MatcherParamsFormatted, +} from './matcher-location' + +export type MatcherName = string | symbol + +/** + * Matcher capable of resolving route locations. + */ +export interface NEW_Matcher_Resolve { + /** + * Resolves an absolute location (like `/path/to/somewhere`). + */ + resolve(absoluteLocation: `/${string}`): NEW_MatcherLocationResolved + + /** + * Resolves a string location relative to another location. A relative location can be `./same-folder`, + * `../parent-folder`, or even `same-folder`. + */ + resolve( + relativeLocation: string, + currentLocation: NEW_MatcherLocationResolved + ): NEW_MatcherLocationResolved + + /** + * Resolves a location by its name. Any required params or query must be passed in the `options` argument. + */ + resolve(location: MatcherLocationAsName): NEW_MatcherLocationResolved + + /** + * Resolves a location by its path. Any required query must be passed. + * @param location - The location to resolve. + */ + // resolve(location: MatcherLocationAsPath): NEW_MatcherLocationResolved + // NOTE: in practice, this overload can cause bugs. It's better to use named locations + + /** + * Resolves a location relative to another location. It reuses existing properties in the `currentLocation` like + * `params`, `query`, and `hash`. + */ + resolve( + relativeLocation: MatcherLocationAsRelative, + currentLocation: NEW_MatcherLocationResolved + ): NEW_MatcherLocationResolved + + addRoute(matcher: MatcherPattern, parent?: MatcherPattern): void + removeRoute(matcher: MatcherPattern): void + clearRoutes(): void +} + +type MatcherResolveArgs = + | [absoluteLocation: `/${string}`] + | [relativeLocation: string, currentLocation: NEW_MatcherLocationResolved] + | [location: MatcherLocationAsName] + | [ + relativeLocation: MatcherLocationAsRelative, + currentLocation: NEW_MatcherLocationResolved + ] + +/** + * Matcher capable of adding and removing routes at runtime. + */ +export interface NEW_Matcher_Dynamic { + addRoute(record: TODO, parent?: TODO): () => void + + removeRoute(record: TODO): void + removeRoute(name: MatcherName): void + + clearRoutes(): void +} + +type TODO = any + +export interface NEW_MatcherLocationResolved { + name: MatcherName + path: string + // TODO: generics? + params: MatcherParamsFormatted + query: LocationQuery + hash: string + + matched: TODO[] +} + +export type MatcherPathParamsValue = string | null | string[] +/** + * Params in a string format so they can be encoded/decoded and put into a URL. + */ +export type MatcherPathParams = Record + +export type MatcherQueryParamsValue = string | null | Array +export type MatcherQueryParams = Record + +export function applyToParams( + fn: (v: string | number | null | undefined) => R, + params: MatcherPathParams | LocationQuery | undefined +): Record { + const newParams: Record = {} + + for (const key in params) { + const value = params[key] + newParams[key] = Array.isArray(value) ? value.map(fn) : fn(value) + } + + return newParams +} + +/** + * Decode text using `decodeURIComponent`. Returns the original text if it + * fails. + * + * @param text - string to decode + * @returns decoded string + */ +export function decode(text: string | number): string +export function decode(text: null | undefined): null +export function decode(text: string | number | null | undefined): string | null +export function decode( + text: string | number | null | undefined +): string | null { + if (text == null) return null + try { + return decodeURIComponent('' + text) + } catch (err) { + __DEV__ && warn(`Error decoding "${text}". Using original value`) + } + return '' + text +} + +interface FnStableNull { + (value: null | undefined): null + (value: string | number): string + // needed for the general case and must be last + (value: string | number | null | undefined): string | null +} + +function encodeParam(text: null | undefined, encodeSlash?: boolean): null +function encodeParam(text: string | number, encodeSlash?: boolean): string +function encodeParam( + text: string | number | null | undefined, + encodeSlash?: boolean +): string | null +function encodeParam( + text: string | number | null | undefined, + encodeSlash = true +): string | null { + if (text == null) return null + text = encodePath(text) + return encodeSlash ? text.replace(SLASH_RE, '%2F') : text +} + +// @ts-expect-error: overload are not correctly identified +const encodeQueryValue: FnStableNull = + // for ts + value => (value == null ? null : _encodeQueryValue(value)) + +// // @ts-expect-error: overload are not correctly identified +// const encodeQueryKey: FnStableNull = +// // for ts +// value => (value == null ? null : _encodeQueryKey(value)) + +function transformObject( + fnKey: (value: string | number) => string, + fnValue: FnStableNull, + query: T +): T { + const encoded: any = {} + + for (const key in query) { + const value = query[key] + encoded[fnKey(key)] = Array.isArray(value) + ? value.map(fnValue) + : fnValue(value as string | number | null | undefined) + } + + return encoded +} + +export function createCompiledMatcher(): NEW_Matcher_Resolve { + const matchers = new Map() + + // TODO: allow custom encode/decode functions + // const encodeParams = applyToParams.bind(null, encodeParam) + // const decodeParams = transformObject.bind(null, String, decode) + // const encodeQuery = transformObject.bind( + // null, + // _encodeQueryKey, + // encodeQueryValue + // ) + // const decodeQuery = transformObject.bind(null, decode, decode) + + function resolve(...args: MatcherResolveArgs): NEW_MatcherLocationResolved { + const [location, currentLocation] = args + if (typeof location === 'string') { + // string location, e.g. '/foo', '../bar', 'baz' + const url = parseURL(parseQuery, location, currentLocation?.path) + + let matcher: MatcherPattern | undefined + let parsedParams: MatcherParamsFormatted | null | undefined + + for (matcher of matchers.values()) { + const params = matcher.matchLocation(url) + if (params) { + parsedParams = matcher.formatParams( + transformObject(String, decode, params[0]), + transformObject(decode, decode, params[1]), + decode(params[2]) + ) + if (parsedParams) break + } + } + if (!parsedParams || !matcher) { + throw new Error(`No matcher found for location "${location}"`) + } + // TODO: build fullPath + return { + name: matcher.name, + path: url.path, + params: parsedParams, + query: transformObject(decode, decode, url.query), + hash: decode(url.hash), + matched: [], + } + } else { + // relative location or by name + const name = location.name ?? currentLocation!.name + const matcher = matchers.get(name) + if (!matcher) { + throw new Error(`Matcher "${String(location.name)}" not found`) + } + + // unencoded params in a formatted form that the user came up with + const params = location.params ?? currentLocation!.params + const mixedUnencodedParams = matcher.unformatParams(params) + + // TODO: they could just throw? + if (!mixedUnencodedParams) { + throw new Error(`Missing params for matcher "${String(name)}"`) + } + + const path = matcher.buildPath( + // encode the values before building the path + transformObject(String, encodeParam, mixedUnencodedParams[0]) + ) + + return { + name, + path, + params, + hash: mixedUnencodedParams[2] ?? location.hash ?? '', + // TODO: should pick query from the params but also from the location and merge them + query: { + ...normalizeQuery(location.query), + // ...matcher.extractQuery(mixedUnencodedParams[1]) + }, + matched: [], + } + } + } + + function addRoute(matcher: MatcherPattern, parent?: MatcherPattern) { + matchers.set(matcher.name, matcher) + } + + function removeRoute(matcher: MatcherPattern) { + matchers.delete(matcher.name) + // TODO: delete children and aliases + } + + function clearRoutes() { + matchers.clear() + } + + return { + resolve, + + addRoute, + removeRoute, + clearRoutes, + } +} diff --git a/packages/router/src/utils/index.ts b/packages/router/src/utils/index.ts index b63f9dbb3..a7c42f4cf 100644 --- a/packages/router/src/utils/index.ts +++ b/packages/router/src/utils/index.ts @@ -2,7 +2,6 @@ import { RouteParamsGeneric, RouteComponent, RouteParamsRawGeneric, - RouteParamValueRaw, RawRouteComponent, } from '../types' @@ -45,9 +44,7 @@ export function applyToParams( for (const key in params) { const value = params[key] - newParams[key] = isArray(value) - ? value.map(fn) - : fn(value as Exclude) + newParams[key] = isArray(value) ? value.map(fn) : fn(value) } return newParams From 745dabcaa76859e5f75abd818332cc8a07c70721 Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Tue, 25 Jun 2024 15:59:05 +0200 Subject: [PATCH 02/40] test: check parsed urls --- .../router/src/new-matcher/matcher.spec.ts | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/packages/router/src/new-matcher/matcher.spec.ts b/packages/router/src/new-matcher/matcher.spec.ts index 2660abdda..29f6a40a3 100644 --- a/packages/router/src/new-matcher/matcher.spec.ts +++ b/packages/router/src/new-matcher/matcher.spec.ts @@ -43,17 +43,17 @@ describe('Matcher', () => { }) ) - expect(matcher.resolve('/foo/1')).toMatchObject({ + expect(matcher.resolve('/foo/1?a=a&b=b#h')).toMatchObject({ path: '/foo/1', params: { id: 1 }, - query: {}, - hash: '', + query: { a: 'a', b: 'b' }, + hash: '#h', }) - expect(matcher.resolve('/foo/54')).toMatchObject({ + expect(matcher.resolve('/foo/54?a=a&b=b#h')).toMatchObject({ path: '/foo/54', params: { id: 54 }, - query: {}, - hash: '', + query: { a: 'a', b: 'b' }, + hash: '#h', }) }) @@ -68,15 +68,14 @@ describe('Matcher', () => { }) ) - expect(matcher.resolve('/foo?id=100')).toMatchObject({ - hash: '', - params: { - id: 100, - }, + expect(matcher.resolve('/foo?id=100&b=b#h')).toMatchObject({ + params: { id: 100 }, path: '/foo', query: { id: '100', + b: 'b', }, + hash: '#h', }) }) @@ -94,11 +93,11 @@ describe('Matcher', () => { ) ) - expect(matcher.resolve('/foo#bar')).toMatchObject({ + expect(matcher.resolve('/foo?a=a&b=b#bar')).toMatchObject({ hash: '#bar', params: { a: 'bar' }, path: '/foo', - query: {}, + query: { a: 'a', b: 'b' }, }) }) }) From 3b893cfdcd1b96eaa2bb085c9cc5fc8d798655df Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Tue, 25 Jun 2024 17:22:01 +0200 Subject: [PATCH 03/40] chore: build location --- .../router/src/new-matcher/matcher-pattern.ts | 22 +- .../router/src/new-matcher/matcher.spec.ts | 255 +++++++++++++----- packages/router/src/new-matcher/matcher.ts | 23 +- 3 files changed, 218 insertions(+), 82 deletions(-) diff --git a/packages/router/src/new-matcher/matcher-pattern.ts b/packages/router/src/new-matcher/matcher-pattern.ts index 021b975c0..bb993658c 100644 --- a/packages/router/src/new-matcher/matcher-pattern.ts +++ b/packages/router/src/new-matcher/matcher-pattern.ts @@ -13,12 +13,12 @@ export interface MatcherPattern { name: MatcherName /** - * Extracts from a formatted, unencoded params object the ones belonging to the path, query, and hash. If any of them is missing, returns `null`. TODO: throw instead? + * Extracts from a formatted, unencoded params object the ones belonging to the path, query, and hash. * @param params - Params to extract from. */ unformatParams( params: MatcherParamsFormatted - ): [path: MatcherPathParams, query: MatcherQueryParams, hash: string | null] + ): [path: MatcherPathParams, query: MatcherQueryParams, hash: string] /** * Extracts the defined params from an encoded path, query, and hash parsed from a URL. Does not apply formatting or @@ -44,7 +44,7 @@ export interface MatcherPattern { path: string query: MatcherQueryParams hash: string - }): [path: MatcherPathParams, query: MatcherQueryParams, hash: string | null] + }): [path: MatcherPathParams, query: MatcherQueryParams, hash: string] /** * Takes encoded params object to form the `path`, @@ -59,7 +59,7 @@ export interface MatcherPattern { formatParams( path: MatcherPathParams, query: MatcherQueryParams, - hash: string | null + hash: string ): MatcherParamsFormatted } @@ -82,13 +82,16 @@ export interface PatternHashParamOptions extends PatternParamOptions_Base {} export interface MatcherPatternPath { + build(path: MatcherPathParams): string match(path: string): MatcherPathParams format(params: MatcherPathParams): MatcherParamsFormatted + unformat(params: MatcherParamsFormatted): MatcherPathParams } export interface MatcherPatternQuery { match(query: MatcherQueryParams): MatcherQueryParams format(params: MatcherQueryParams): MatcherParamsFormatted + unformat(params: MatcherParamsFormatted): MatcherQueryParams } export interface MatcherPatternHash { @@ -98,6 +101,7 @@ export interface MatcherPatternHash { */ match(hash: string): string format(hash: string): MatcherParamsFormatted + unformat(params: MatcherParamsFormatted): string } export class MatcherPatternImpl implements MatcherPattern { @@ -133,12 +137,16 @@ export class MatcherPatternImpl implements MatcherPattern { } buildPath(path: MatcherPathParams): string { - return '' + return this.path.build(path) } unformatParams( params: MatcherParamsFormatted - ): [path: MatcherPathParams, query: MatcherQueryParams, hash: string | null] { - throw new Error('Method not implemented.') + ): [path: MatcherPathParams, query: MatcherQueryParams, hash: string] { + return [ + this.path.unformat(params), + this.query?.unformat(params) ?? {}, + this.hash?.unformat(params) ?? '', + ] } } diff --git a/packages/router/src/new-matcher/matcher.spec.ts b/packages/router/src/new-matcher/matcher.spec.ts index 29f6a40a3..9c6ccb2fe 100644 --- a/packages/router/src/new-matcher/matcher.spec.ts +++ b/packages/router/src/new-matcher/matcher.spec.ts @@ -11,93 +11,212 @@ function createMatcherPattern( const EMPTY_PATH_PATTERN_MATCHER = { match: (path: string) => ({}), format: (params: {}) => ({}), + unformat: (params: {}) => ({}), + build: () => '/', } satisfies MatcherPatternPath describe('Matcher', () => { describe('resolve()', () => { - it('resolves string locations with no params', () => { - const matcher = createCompiledMatcher() - matcher.addRoute( - createMatcherPattern(Symbol('foo'), EMPTY_PATH_PATTERN_MATCHER) - ) - - expect(matcher.resolve('/foo?a=a&b=b#h')).toMatchObject({ - path: '/foo', - params: {}, - query: { a: 'a', b: 'b' }, - hash: '#h', + describe('absolute locationss as strings', () => { + it('resolves string locations with no params', () => { + const matcher = createCompiledMatcher() + matcher.addRoute( + createMatcherPattern(Symbol('foo'), EMPTY_PATH_PATTERN_MATCHER) + ) + + expect(matcher.resolve('/foo?a=a&b=b#h')).toMatchObject({ + path: '/foo', + params: {}, + query: { a: 'a', b: 'b' }, + hash: '#h', + }) }) - }) - it('resolves string locations with params', () => { - const matcher = createCompiledMatcher() - matcher.addRoute( - // /users/:id - createMatcherPattern(Symbol('foo'), { - match: (path: string) => { - const match = path.match(/^\/foo\/([^/]+?)$/) - if (!match) throw new Error('no match') - return { id: match[1] } + it('resolves string locations with params', () => { + const matcher = createCompiledMatcher() + matcher.addRoute( + // /users/:id + createMatcherPattern(Symbol('foo'), { + match: (path: string) => { + const match = path.match(/^\/foo\/([^/]+?)$/) + if (!match) throw new Error('no match') + return { id: match[1] } + }, + format: (params: { id: string }) => ({ id: Number(params.id) }), + unformat: (params: { id: number }) => ({ id: String(params.id) }), + build: params => `/foo/${params.id}`, + }) + ) + + expect(matcher.resolve('/foo/1?a=a&b=b#h')).toMatchObject({ + path: '/foo/1', + params: { id: 1 }, + query: { a: 'a', b: 'b' }, + hash: '#h', + }) + expect(matcher.resolve('/foo/54?a=a&b=b#h')).toMatchObject({ + path: '/foo/54', + params: { id: 54 }, + query: { a: 'a', b: 'b' }, + hash: '#h', + }) + }) + + it('resolve string locations with query', () => { + const matcher = createCompiledMatcher() + matcher.addRoute( + createMatcherPattern(Symbol('foo'), EMPTY_PATH_PATTERN_MATCHER, { + match: query => ({ + id: Array.isArray(query.id) ? query.id[0] : query.id, + }), + format: (params: { id: string }) => ({ id: Number(params.id) }), + unformat: (params: { id: number }) => ({ id: String(params.id) }), + }) + ) + + expect(matcher.resolve('/foo?id=100&b=b#h')).toMatchObject({ + params: { id: 100 }, + path: '/foo', + query: { + id: '100', + b: 'b', }, - format: (params: { id: string }) => ({ id: Number(params.id) }), + hash: '#h', + }) + }) + + it('resolves string locations with hash', () => { + const matcher = createCompiledMatcher() + matcher.addRoute( + createMatcherPattern( + Symbol('foo'), + EMPTY_PATH_PATTERN_MATCHER, + undefined, + { + match: hash => hash, + format: hash => ({ a: hash.slice(1) }), + unformat: ({ a }) => '#a', + } + ) + ) + + expect(matcher.resolve('/foo?a=a&b=b#bar')).toMatchObject({ + hash: '#bar', + params: { a: 'bar' }, + path: '/foo', + query: { a: 'a', b: 'b' }, }) - ) + }) - expect(matcher.resolve('/foo/1?a=a&b=b#h')).toMatchObject({ - path: '/foo/1', - params: { id: 1 }, - query: { a: 'a', b: 'b' }, - hash: '#h', + it('returns a valid location with an empty `matched` array if no match', () => { + const matcher = createCompiledMatcher() + expect(matcher.resolve('/bar')).toMatchInlineSnapshot( + { + hash: '', + matched: [], + params: {}, + path: '/bar', + query: {}, + }, + ` + { + "fullPath": "/bar", + "hash": "", + "matched": [], + "name": Symbol(no-match), + "params": {}, + "path": "/bar", + "query": {}, + } + ` + ) }) - expect(matcher.resolve('/foo/54?a=a&b=b#h')).toMatchObject({ - path: '/foo/54', - params: { id: 54 }, - query: { a: 'a', b: 'b' }, - hash: '#h', + + it('resolves string locations with all', () => { + const matcher = createCompiledMatcher() + matcher.addRoute( + createMatcherPattern( + Symbol('foo'), + { + build: params => `/foo/${params.id}`, + match: path => { + const match = path.match(/^\/foo\/([^/]+?)$/) + if (!match) throw new Error('no match') + return { id: match[1] } + }, + format: params => ({ id: Number(params.id) }), + unformat: params => ({ id: String(params.id) }), + }, + { + match: query => ({ + id: Array.isArray(query.id) ? query.id[0] : query.id, + }), + format: params => ({ q: Number(params.id) }), + unformat: params => ({ id: String(params.q) }), + }, + { + match: hash => hash, + format: hash => ({ a: hash.slice(1) }), + unformat: ({ a }) => '#a', + } + ) + ) + + expect(matcher.resolve('/foo/1?id=100#bar')).toMatchObject({ + hash: '#bar', + params: { id: 1, q: 100, a: 'bar' }, + }) }) }) - it('resolve string locations with query', () => { - const matcher = createCompiledMatcher() - matcher.addRoute( - createMatcherPattern(Symbol('foo'), EMPTY_PATH_PATTERN_MATCHER, { - match: query => ({ - id: Array.isArray(query.id) ? query.id[0] : query.id, - }), - format: (params: { id: string }) => ({ id: Number(params.id) }), + describe('relative locations as strings', () => { + it('resolves a simple relative location', () => { + const matcher = createCompiledMatcher() + matcher.addRoute( + createMatcherPattern(Symbol('foo'), EMPTY_PATH_PATTERN_MATCHER) + ) + + expect( + matcher.resolve('foo', matcher.resolve('/nested/')) + ).toMatchObject({ + params: {}, + path: '/nested/foo', + query: {}, + hash: '', + }) + expect( + matcher.resolve('../foo', matcher.resolve('/nested/')) + ).toMatchObject({ + params: {}, + path: '/foo', + query: {}, + hash: '', + }) + expect( + matcher.resolve('./foo', matcher.resolve('/nested/')) + ).toMatchObject({ + params: {}, + path: '/nested/foo', + query: {}, + hash: '', }) - ) - - expect(matcher.resolve('/foo?id=100&b=b#h')).toMatchObject({ - params: { id: 100 }, - path: '/foo', - query: { - id: '100', - b: 'b', - }, - hash: '#h', }) }) - it('resolves string locations with hash', () => { - const matcher = createCompiledMatcher() - matcher.addRoute( - createMatcherPattern( - Symbol('foo'), - EMPTY_PATH_PATTERN_MATCHER, - undefined, - { - match: hash => hash, - format: hash => ({ a: hash.slice(1) }), - } + describe('named locations', () => { + it('resolves named locations with no params', () => { + const matcher = createCompiledMatcher() + matcher.addRoute( + createMatcherPattern('home', EMPTY_PATH_PATTERN_MATCHER) ) - ) - expect(matcher.resolve('/foo?a=a&b=b#bar')).toMatchObject({ - hash: '#bar', - params: { a: 'bar' }, - path: '/foo', - query: { a: 'a', b: 'b' }, + expect(matcher.resolve({ name: 'home', params: {} })).toMatchObject({ + name: 'home', + path: '/', + params: {}, + query: {}, + hash: '', + }) }) }) }) diff --git a/packages/router/src/new-matcher/matcher.ts b/packages/router/src/new-matcher/matcher.ts index bd48a1246..5d204f7bc 100644 --- a/packages/router/src/new-matcher/matcher.ts +++ b/packages/router/src/new-matcher/matcher.ts @@ -187,6 +187,12 @@ function transformObject( return encoded } +export const NO_MATCH_LOCATION = { + name: Symbol('no-match'), + params: {}, + matched: [], +} satisfies Omit + export function createCompiledMatcher(): NEW_Matcher_Resolve { const matchers = new Map() @@ -220,13 +226,21 @@ export function createCompiledMatcher(): NEW_Matcher_Resolve { if (parsedParams) break } } + + // No match location if (!parsedParams || !matcher) { - throw new Error(`No matcher found for location "${location}"`) + return { + ...url, + ...NO_MATCH_LOCATION, + query: transformObject(decode, decode, url.query), + hash: decode(url.hash), + } } + // TODO: build fullPath return { + ...url, name: matcher.name, - path: url.path, params: parsedParams, query: transformObject(decode, decode, url.query), hash: decode(url.hash), @@ -244,11 +258,6 @@ export function createCompiledMatcher(): NEW_Matcher_Resolve { const params = location.params ?? currentLocation!.params const mixedUnencodedParams = matcher.unformatParams(params) - // TODO: they could just throw? - if (!mixedUnencodedParams) { - throw new Error(`Missing params for matcher "${String(name)}"`) - } - const path = matcher.buildPath( // encode the values before building the path transformObject(String, encodeParam, mixedUnencodedParams[0]) From f89a842f580a632bdb60a7ef84fd119f4bf3e199 Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Wed, 26 Jun 2024 10:49:57 +0200 Subject: [PATCH 04/40] perf: parseURL minor improvements --- packages/router/__tests__/location.spec.ts | 68 +++++++++++++++++++++- packages/router/__tests__/router.spec.ts | 4 +- packages/router/src/location.ts | 43 ++++++++------ packages/router/src/query.ts | 3 +- 4 files changed, 93 insertions(+), 25 deletions(-) diff --git a/packages/router/__tests__/location.spec.ts b/packages/router/__tests__/location.spec.ts index 8ccd8a425..98dadf1e8 100644 --- a/packages/router/__tests__/location.spec.ts +++ b/packages/router/__tests__/location.spec.ts @@ -134,7 +134,7 @@ describe('parseURL', () => { }) }) - it('parses ? after the hash', () => { + it('avoids ? after the hash', () => { expect(parseURL('/foo#?a=one')).toEqual({ fullPath: '/foo#?a=one', path: '/foo', @@ -149,11 +149,75 @@ describe('parseURL', () => { }) }) + it('works with empty query', () => { + expect(parseURL('/foo?#hash')).toEqual({ + fullPath: '/foo?#hash', + path: '/foo', + hash: '#hash', + query: {}, + }) + expect(parseURL('/foo?')).toEqual({ + fullPath: '/foo?', + path: '/foo', + hash: '', + query: {}, + }) + }) + + it('works with empty hash', () => { + expect(parseURL('/foo#')).toEqual({ + fullPath: '/foo#', + path: '/foo', + hash: '#', + query: {}, + }) + expect(parseURL('/foo?#')).toEqual({ + fullPath: '/foo?#', + path: '/foo', + hash: '#', + query: {}, + }) + }) + + it('works with a relative paths', () => { + expect(parseURL('foo', '/parent/bar')).toEqual({ + fullPath: '/parent/foo', + path: '/parent/foo', + hash: '', + query: {}, + }) + expect(parseURL('./foo', '/parent/bar')).toEqual({ + fullPath: '/parent/foo', + path: '/parent/foo', + hash: '', + query: {}, + }) + expect(parseURL('../foo', '/parent/bar')).toEqual({ + fullPath: '/foo', + path: '/foo', + hash: '', + query: {}, + }) + + expect(parseURL('#foo', '/parent/bar')).toEqual({ + fullPath: '/parent/bar#foo', + path: '/parent/bar', + hash: '#foo', + query: {}, + }) + expect(parseURL('?o=o', '/parent/bar')).toEqual({ + fullPath: '/parent/bar?o=o', + path: '/parent/bar', + hash: '', + query: { o: 'o' }, + }) + }) + it('calls parseQuery', () => { const parseQuery = vi.fn() originalParseURL(parseQuery, '/?é=é&é=a') expect(parseQuery).toHaveBeenCalledTimes(1) - expect(parseQuery).toHaveBeenCalledWith('é=é&é=a') + expect(parseQuery).toHaveBeenCalledWith('?é=é&é=a') }) }) diff --git a/packages/router/__tests__/router.spec.ts b/packages/router/__tests__/router.spec.ts index bf11f31ba..f835f41e3 100644 --- a/packages/router/__tests__/router.spec.ts +++ b/packages/router/__tests__/router.spec.ts @@ -14,8 +14,6 @@ import { START_LOCATION_NORMALIZED } from '../src/location' import { vi, describe, expect, it, beforeAll } from 'vitest' import { mockWarn } from './vitest-mock-warn' -declare var __DEV__: boolean - const routes: RouteRecordRaw[] = [ { path: '/', component: components.Home, name: 'home' }, { path: '/home', redirect: '/' }, @@ -173,7 +171,7 @@ describe('Router', () => { const parseQuery = vi.fn(_ => ({})) const { router } = await newRouter({ parseQuery }) const to = router.resolve('/foo?bar=baz') - expect(parseQuery).toHaveBeenCalledWith('bar=baz') + expect(parseQuery).toHaveBeenCalledWith('?bar=baz') expect(to.query).toEqual({}) }) diff --git a/packages/router/src/location.ts b/packages/router/src/location.ts index c786e0ae9..a8d5f9192 100644 --- a/packages/router/src/location.ts +++ b/packages/router/src/location.ts @@ -50,37 +50,43 @@ export function parseURL( searchString = '', hash = '' - // Could use URL and URLSearchParams but IE 11 doesn't support it - // TODO: move to new URL() + // NOTE: we could use URL and URLSearchParams but they are 2 to 5 times slower than this method const hashPos = location.indexOf('#') - let searchPos = location.indexOf('?') - // the hash appears before the search, so it's not part of the search string - if (hashPos < searchPos && hashPos >= 0) { - searchPos = -1 - } + // let searchPos = location.indexOf('?') + let searchPos = + hashPos >= 0 + ? // find the query string before the hash to avoid including a ? in the hash + // e.g. /foo#hash?query -> has no query + location.lastIndexOf('?', hashPos) + : location.indexOf('?') - if (searchPos > -1) { + if (searchPos >= 0) { path = location.slice(0, searchPos) - searchString = location.slice( - searchPos + 1, - hashPos > -1 ? hashPos : location.length - ) + searchString = + '?' + + location.slice(searchPos + 1, hashPos > 0 ? hashPos : location.length) query = parseQuery(searchString) } - if (hashPos > -1) { + if (hashPos >= 0) { + // TODO(major): path ||= path = path || location.slice(0, hashPos) // keep the # character hash = location.slice(hashPos, location.length) } - // no search and no query - path = resolveRelativePath(path != null ? path : location, currentLocation) - // empty path means a relative query or hash `?foo=f`, `#thing` + // TODO(major): path ?? location + path = resolveRelativePath( + path != null + ? path + : // empty path means a relative query or hash `?foo=f`, `#thing` + location, + currentLocation + ) return { - fullPath: path + (searchString && '?') + searchString + hash, + fullPath: path + searchString + hash, path, query, hash: decode(hash), @@ -207,11 +213,12 @@ export function resolveRelativePath(to: string, from: string): string { return to } + // resolve '' with '/anything' -> '/anything' if (!to) return from const fromSegments = from.split('/') const toSegments = to.split('/') - const lastToSegment = toSegments[toSegments.length - 1] + const lastToSegment: string | undefined = toSegments[toSegments.length - 1] // make . and ./ the same (../ === .., ../../ === ../..) // this is the same behavior as new URL() diff --git a/packages/router/src/query.ts b/packages/router/src/query.ts index 35e2e9f8a..f2f5c655c 100644 --- a/packages/router/src/query.ts +++ b/packages/router/src/query.ts @@ -56,8 +56,7 @@ export function parseQuery(search: string): LocationQuery { // avoid creating an object with an empty key and empty value // because of split('&') if (search === '' || search === '?') return query - const hasLeadingIM = search[0] === '?' - const searchParams = (hasLeadingIM ? search.slice(1) : search).split('&') + const searchParams = (search[0] === '?' ? search.slice(1) : search).split('&') for (let i = 0; i < searchParams.length; ++i) { // pre decode the + into space const searchParam = searchParams[i].replace(PLUS_RE, ' ') From 38606b9224d61c8fe3a3736df126e1295db00acd Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Wed, 26 Jun 2024 11:13:06 +0200 Subject: [PATCH 05/40] refactor: avoid double decoding --- .../router/src/new-matcher/matcher-pattern.ts | 2 +- packages/router/src/new-matcher/matcher.ts | 15 +++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/router/src/new-matcher/matcher-pattern.ts b/packages/router/src/new-matcher/matcher-pattern.ts index bb993658c..f368a04f8 100644 --- a/packages/router/src/new-matcher/matcher-pattern.ts +++ b/packages/router/src/new-matcher/matcher-pattern.ts @@ -44,7 +44,7 @@ export interface MatcherPattern { path: string query: MatcherQueryParams hash: string - }): [path: MatcherPathParams, query: MatcherQueryParams, hash: string] + }): [path: MatcherPathParams, query: MatcherQueryParams, hash: string] | null /** * Takes encoded params object to form the `path`, diff --git a/packages/router/src/new-matcher/matcher.ts b/packages/router/src/new-matcher/matcher.ts index 5d204f7bc..ec3b8d6cf 100644 --- a/packages/router/src/new-matcher/matcher.ts +++ b/packages/router/src/new-matcher/matcher.ts @@ -220,8 +220,9 @@ export function createCompiledMatcher(): NEW_Matcher_Resolve { if (params) { parsedParams = matcher.formatParams( transformObject(String, decode, params[0]), - transformObject(decode, decode, params[1]), - decode(params[2]) + // already decoded + params[1], + params[2] ) if (parsedParams) break } @@ -232,8 +233,9 @@ export function createCompiledMatcher(): NEW_Matcher_Resolve { return { ...url, ...NO_MATCH_LOCATION, - query: transformObject(decode, decode, url.query), - hash: decode(url.hash), + // already decoded + query: url.query, + hash: url.hash, } } @@ -242,8 +244,9 @@ export function createCompiledMatcher(): NEW_Matcher_Resolve { ...url, name: matcher.name, params: parsedParams, - query: transformObject(decode, decode, url.query), - hash: decode(url.hash), + // already decoded + query: url.query, + hash: url.hash, matched: [], } } else { From af7afb50686a88de0425c4cbe57d76982f52b1a4 Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Wed, 26 Jun 2024 11:13:37 +0200 Subject: [PATCH 06/40] refactor: add fullPath --- packages/router/src/new-matcher/matcher.ts | 33 +++++++++++++++------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/packages/router/src/new-matcher/matcher.ts b/packages/router/src/new-matcher/matcher.ts index ec3b8d6cf..9e39f44e6 100644 --- a/packages/router/src/new-matcher/matcher.ts +++ b/packages/router/src/new-matcher/matcher.ts @@ -1,4 +1,9 @@ -import { type LocationQuery, parseQuery, normalizeQuery } from '../query' +import { + type LocationQuery, + parseQuery, + normalizeQuery, + stringifyQuery, +} from '../query' import type { MatcherPattern } from './matcher-pattern' import { warn } from '../warning' import { @@ -6,7 +11,7 @@ import { encodePath, encodeQueryValue as _encodeQueryValue, } from '../encoding' -import { parseURL } from '../location' +import { parseURL, stringifyURL } from '../location' import type { MatcherLocationAsName, MatcherLocationAsRelative, @@ -84,6 +89,7 @@ type TODO = any export interface NEW_MatcherLocationResolved { name: MatcherName + fullPath: string path: string // TODO: generics? params: MatcherParamsFormatted @@ -137,6 +143,7 @@ export function decode( } return '' + text } +// TODO: just add the null check to the original function in encoding.ts interface FnStableNull { (value: null | undefined): null @@ -191,7 +198,10 @@ export const NO_MATCH_LOCATION = { name: Symbol('no-match'), params: {}, matched: [], -} satisfies Omit +} satisfies Omit< + NEW_MatcherLocationResolved, + 'path' | 'hash' | 'query' | 'fullPath' +> export function createCompiledMatcher(): NEW_Matcher_Resolve { const matchers = new Map() @@ -239,7 +249,6 @@ export function createCompiledMatcher(): NEW_Matcher_Resolve { } } - // TODO: build fullPath return { ...url, name: matcher.name, @@ -266,16 +275,20 @@ export function createCompiledMatcher(): NEW_Matcher_Resolve { transformObject(String, encodeParam, mixedUnencodedParams[0]) ) + // TODO: should pick query from the params but also from the location and merge them + const query = { + ...normalizeQuery(location.query), + // ...matcher.extractQuery(mixedUnencodedParams[1]) + } + const hash = mixedUnencodedParams[2] ?? location.hash ?? '' + return { name, + fullPath: stringifyURL(stringifyQuery, { path, query: {}, hash }), path, params, - hash: mixedUnencodedParams[2] ?? location.hash ?? '', - // TODO: should pick query from the params but also from the location and merge them - query: { - ...normalizeQuery(location.query), - // ...matcher.extractQuery(mixedUnencodedParams[1]) - }, + hash, + query, matched: [], } } From f26e919db31538c48ef6b2f96a2d38de2cfeb312 Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Wed, 26 Jun 2024 15:52:51 +0200 Subject: [PATCH 07/40] chore: static path matcher --- .../index.ts | 0 .../matcher-location.ts | 0 .../matcher-pattern.ts | 109 ++++++++++++------ .../matcher.spec.ts | 34 +++--- .../matcher.test-d.ts | 6 +- .../matcher.ts | 45 +++++--- .../new-route-resolver/matchers/path-param.ts | 48 ++++++++ .../matchers/path-static.ts | 15 +++ 8 files changed, 181 insertions(+), 76 deletions(-) rename packages/router/src/{new-matcher => new-route-resolver}/index.ts (100%) rename packages/router/src/{new-matcher => new-route-resolver}/matcher-location.ts (100%) rename packages/router/src/{new-matcher => new-route-resolver}/matcher-pattern.ts (53%) rename packages/router/src/{new-matcher => new-route-resolver}/matcher.spec.ts (85%) rename packages/router/src/{new-matcher => new-route-resolver}/matcher.test-d.ts (64%) rename packages/router/src/{new-matcher => new-route-resolver}/matcher.ts (87%) create mode 100644 packages/router/src/new-route-resolver/matchers/path-param.ts create mode 100644 packages/router/src/new-route-resolver/matchers/path-static.ts diff --git a/packages/router/src/new-matcher/index.ts b/packages/router/src/new-route-resolver/index.ts similarity index 100% rename from packages/router/src/new-matcher/index.ts rename to packages/router/src/new-route-resolver/index.ts diff --git a/packages/router/src/new-matcher/matcher-location.ts b/packages/router/src/new-route-resolver/matcher-location.ts similarity index 100% rename from packages/router/src/new-matcher/matcher-location.ts rename to packages/router/src/new-route-resolver/matcher-location.ts diff --git a/packages/router/src/new-matcher/matcher-pattern.ts b/packages/router/src/new-route-resolver/matcher-pattern.ts similarity index 53% rename from packages/router/src/new-matcher/matcher-pattern.ts rename to packages/router/src/new-route-resolver/matcher-pattern.ts index f368a04f8..a9f8f5e83 100644 --- a/packages/router/src/new-matcher/matcher-pattern.ts +++ b/packages/router/src/new-route-resolver/matcher-pattern.ts @@ -6,19 +6,34 @@ import type { } from './matcher' import type { MatcherParamsFormatted } from './matcher-location' +/** + * Allows to match, extract, parse and build a path. Tailored to iterate through route records and check if a location + * matches. When it cannot match, it returns `null` instead of throwing to not force a try/catch block around each + * iteration in for loops. + */ export interface MatcherPattern { /** * Name of the matcher. Unique across all matchers. */ name: MatcherName + // TODO: add route record to be able to build the matched + /** - * Extracts from a formatted, unencoded params object the ones belonging to the path, query, and hash. - * @param params - Params to extract from. + * Extracts from an unencoded, parsed params object the ones belonging to the path, query, and hash in their + * serialized format but still unencoded. e.g. `{ id: 2 }` -> `{ id: '2' }`. If any params are missing, return `null`. + * + * @param params - Params to extract from. If any params are missing, throws */ - unformatParams( + matchParams( params: MatcherParamsFormatted - ): [path: MatcherPathParams, query: MatcherQueryParams, hash: string] + ): + | readonly [ + pathParams: MatcherPathParams, + queryParams: MatcherQueryParams, + hashParam: string + ] + | null /** * Extracts the defined params from an encoded path, query, and hash parsed from a URL. Does not apply formatting or @@ -44,23 +59,34 @@ export interface MatcherPattern { path: string query: MatcherQueryParams hash: string - }): [path: MatcherPathParams, query: MatcherQueryParams, hash: string] | null + }): + | readonly [ + pathParams: MatcherPathParams, + queryParams: MatcherQueryParams, + hashParam: string + ] + | null /** * Takes encoded params object to form the `path`, - * @param path - encoded path params + * + * @param pathParams - encoded path params */ - buildPath(path: MatcherPathParams): string + buildPath(pathParams: MatcherPathParams): string /** - * Runs the decoded params through the formatting functions if any. - * @param params - Params to format. + * Runs the decoded params through the parsing functions if any, allowing them to be in be of a type other than a + * string. + * + * @param pathParams - decoded path params + * @param queryParams - decoded query params + * @param hashParam - decoded hash param */ - formatParams( - path: MatcherPathParams, - query: MatcherQueryParams, - hash: string - ): MatcherParamsFormatted + parseParams( + pathParams: MatcherPathParams, + queryParams: MatcherQueryParams, + hashParam: string + ): MatcherParamsFormatted | null } interface PatternParamOptions_Base { @@ -69,7 +95,11 @@ interface PatternParamOptions_Base { default?: T | (() => T) } -export interface PatternParamOptions extends PatternParamOptions_Base {} +export interface PatternPathParamOptions + extends PatternParamOptions_Base { + re: RegExp + keys: string[] +} export interface PatternQueryParamOptions extends PatternParamOptions_Base { @@ -82,16 +112,16 @@ export interface PatternHashParamOptions extends PatternParamOptions_Base {} export interface MatcherPatternPath { - build(path: MatcherPathParams): string + buildPath(path: MatcherPathParams): string match(path: string): MatcherPathParams - format(params: MatcherPathParams): MatcherParamsFormatted - unformat(params: MatcherParamsFormatted): MatcherPathParams + parse?(params: MatcherPathParams): MatcherParamsFormatted + serialize?(params: MatcherParamsFormatted): MatcherPathParams } export interface MatcherPatternQuery { match(query: MatcherQueryParams): MatcherQueryParams - format(params: MatcherQueryParams): MatcherParamsFormatted - unformat(params: MatcherParamsFormatted): MatcherQueryParams + parse(params: MatcherQueryParams): MatcherParamsFormatted + serialize(params: MatcherParamsFormatted): MatcherQueryParams } export interface MatcherPatternHash { @@ -100,8 +130,8 @@ export interface MatcherPatternHash { * @param hash - encoded hash */ match(hash: string): string - format(hash: string): MatcherParamsFormatted - unformat(params: MatcherParamsFormatted): string + parse(hash: string): MatcherParamsFormatted + serialize(params: MatcherParamsFormatted): string } export class MatcherPatternImpl implements MatcherPattern { @@ -116,37 +146,42 @@ export class MatcherPatternImpl implements MatcherPattern { path: string query: MatcherQueryParams hash: string - }): [path: MatcherPathParams, query: MatcherQueryParams, hash: string] { - return [ - this.path.match(location.path), - this.query?.match(location.query) ?? {}, - this.hash?.match(location.hash) ?? '', - ] + }) { + // TODO: is this performant? Compare to a check with `null + try { + return [ + this.path.match(location.path), + this.query?.match(location.query) ?? {}, + this.hash?.match(location.hash) ?? '', + ] as const + } catch { + return null + } } - formatParams( + parseParams( path: MatcherPathParams, query: MatcherQueryParams, hash: string ): MatcherParamsFormatted { return { - ...this.path.format(path), - ...this.query?.format(query), - ...this.hash?.format(hash), + ...this.path.parse?.(path), + ...this.query?.parse(query), + ...this.hash?.parse(hash), } } buildPath(path: MatcherPathParams): string { - return this.path.build(path) + return this.path.buildPath(path) } - unformatParams( + matchParams( params: MatcherParamsFormatted ): [path: MatcherPathParams, query: MatcherQueryParams, hash: string] { return [ - this.path.unformat(params), - this.query?.unformat(params) ?? {}, - this.hash?.unformat(params) ?? '', + this.path.serialize?.(params) ?? {}, + this.query?.serialize(params) ?? {}, + this.hash?.serialize(params) ?? '', ] } } diff --git a/packages/router/src/new-matcher/matcher.spec.ts b/packages/router/src/new-route-resolver/matcher.spec.ts similarity index 85% rename from packages/router/src/new-matcher/matcher.spec.ts rename to packages/router/src/new-route-resolver/matcher.spec.ts index 9c6ccb2fe..52d8b208a 100644 --- a/packages/router/src/new-matcher/matcher.spec.ts +++ b/packages/router/src/new-route-resolver/matcher.spec.ts @@ -10,9 +10,9 @@ function createMatcherPattern( const EMPTY_PATH_PATTERN_MATCHER = { match: (path: string) => ({}), - format: (params: {}) => ({}), - unformat: (params: {}) => ({}), - build: () => '/', + parse: (params: {}) => ({}), + serialize: (params: {}) => ({}), + buildPath: () => '/', } satisfies MatcherPatternPath describe('Matcher', () => { @@ -42,9 +42,9 @@ describe('Matcher', () => { if (!match) throw new Error('no match') return { id: match[1] } }, - format: (params: { id: string }) => ({ id: Number(params.id) }), - unformat: (params: { id: number }) => ({ id: String(params.id) }), - build: params => `/foo/${params.id}`, + parse: (params: { id: string }) => ({ id: Number(params.id) }), + serialize: (params: { id: number }) => ({ id: String(params.id) }), + buildPath: params => `/foo/${params.id}`, }) ) @@ -69,8 +69,8 @@ describe('Matcher', () => { match: query => ({ id: Array.isArray(query.id) ? query.id[0] : query.id, }), - format: (params: { id: string }) => ({ id: Number(params.id) }), - unformat: (params: { id: number }) => ({ id: String(params.id) }), + parse: (params: { id: string }) => ({ id: Number(params.id) }), + serialize: (params: { id: number }) => ({ id: String(params.id) }), }) ) @@ -94,8 +94,8 @@ describe('Matcher', () => { undefined, { match: hash => hash, - format: hash => ({ a: hash.slice(1) }), - unformat: ({ a }) => '#a', + parse: hash => ({ a: hash.slice(1) }), + serialize: ({ a }) => '#a', } ) ) @@ -138,26 +138,26 @@ describe('Matcher', () => { createMatcherPattern( Symbol('foo'), { - build: params => `/foo/${params.id}`, + buildPath: params => `/foo/${params.id}`, match: path => { const match = path.match(/^\/foo\/([^/]+?)$/) if (!match) throw new Error('no match') return { id: match[1] } }, - format: params => ({ id: Number(params.id) }), - unformat: params => ({ id: String(params.id) }), + parse: params => ({ id: Number(params.id) }), + serialize: params => ({ id: String(params.id) }), }, { match: query => ({ id: Array.isArray(query.id) ? query.id[0] : query.id, }), - format: params => ({ q: Number(params.id) }), - unformat: params => ({ id: String(params.q) }), + parse: params => ({ q: Number(params.id) }), + serialize: params => ({ id: String(params.q) }), }, { match: hash => hash, - format: hash => ({ a: hash.slice(1) }), - unformat: ({ a }) => '#a', + parse: hash => ({ a: hash.slice(1) }), + serialize: ({ a }) => '#a', } ) ) diff --git a/packages/router/src/new-matcher/matcher.test-d.ts b/packages/router/src/new-route-resolver/matcher.test-d.ts similarity index 64% rename from packages/router/src/new-matcher/matcher.test-d.ts rename to packages/router/src/new-route-resolver/matcher.test-d.ts index fbf150e2e..412cb0719 100644 --- a/packages/router/src/new-matcher/matcher.test-d.ts +++ b/packages/router/src/new-route-resolver/matcher.test-d.ts @@ -1,5 +1,5 @@ import { describe, it } from 'vitest' -import { NEW_MatcherLocationResolved, createCompiledMatcher } from './matcher' +import { NEW_LocationResolved, createCompiledMatcher } from './matcher' describe('Matcher', () => { it('resolves locations', () => { @@ -7,10 +7,10 @@ describe('Matcher', () => { matcher.resolve('/foo') // @ts-expect-error: needs currentLocation matcher.resolve('foo') - matcher.resolve('foo', {} as NEW_MatcherLocationResolved) + matcher.resolve('foo', {} as NEW_LocationResolved) matcher.resolve({ name: 'foo', params: {} }) // @ts-expect-error: needs currentLocation matcher.resolve({ params: { id: 1 } }) - matcher.resolve({ params: { id: 1 } }, {} as NEW_MatcherLocationResolved) + matcher.resolve({ params: { id: 1 } }, {} as NEW_LocationResolved) }) }) diff --git a/packages/router/src/new-matcher/matcher.ts b/packages/router/src/new-route-resolver/matcher.ts similarity index 87% rename from packages/router/src/new-matcher/matcher.ts rename to packages/router/src/new-route-resolver/matcher.ts index 9e39f44e6..4aa742e93 100644 --- a/packages/router/src/new-matcher/matcher.ts +++ b/packages/router/src/new-route-resolver/matcher.ts @@ -21,13 +21,13 @@ import type { export type MatcherName = string | symbol /** - * Matcher capable of resolving route locations. + * Manage and resolve routes. Also handles the encoding, decoding, parsing and serialization of params, query, and hash. */ -export interface NEW_Matcher_Resolve { +export interface RouteResolver { /** * Resolves an absolute location (like `/path/to/somewhere`). */ - resolve(absoluteLocation: `/${string}`): NEW_MatcherLocationResolved + resolve(absoluteLocation: `/${string}`): NEW_LocationResolved /** * Resolves a string location relative to another location. A relative location can be `./same-folder`, @@ -35,13 +35,13 @@ export interface NEW_Matcher_Resolve { */ resolve( relativeLocation: string, - currentLocation: NEW_MatcherLocationResolved - ): NEW_MatcherLocationResolved + currentLocation: NEW_LocationResolved + ): NEW_LocationResolved /** * Resolves a location by its name. Any required params or query must be passed in the `options` argument. */ - resolve(location: MatcherLocationAsName): NEW_MatcherLocationResolved + resolve(location: MatcherLocationAsName): NEW_LocationResolved /** * Resolves a location by its path. Any required query must be passed. @@ -56,8 +56,8 @@ export interface NEW_Matcher_Resolve { */ resolve( relativeLocation: MatcherLocationAsRelative, - currentLocation: NEW_MatcherLocationResolved - ): NEW_MatcherLocationResolved + currentLocation: NEW_LocationResolved + ): NEW_LocationResolved addRoute(matcher: MatcherPattern, parent?: MatcherPattern): void removeRoute(matcher: MatcherPattern): void @@ -66,11 +66,11 @@ export interface NEW_Matcher_Resolve { type MatcherResolveArgs = | [absoluteLocation: `/${string}`] - | [relativeLocation: string, currentLocation: NEW_MatcherLocationResolved] + | [relativeLocation: string, currentLocation: NEW_LocationResolved] | [location: MatcherLocationAsName] | [ relativeLocation: MatcherLocationAsRelative, - currentLocation: NEW_MatcherLocationResolved + currentLocation: NEW_LocationResolved ] /** @@ -87,7 +87,7 @@ export interface NEW_Matcher_Dynamic { type TODO = any -export interface NEW_MatcherLocationResolved { +export interface NEW_LocationResolved { name: MatcherName fullPath: string path: string @@ -198,12 +198,9 @@ export const NO_MATCH_LOCATION = { name: Symbol('no-match'), params: {}, matched: [], -} satisfies Omit< - NEW_MatcherLocationResolved, - 'path' | 'hash' | 'query' | 'fullPath' -> +} satisfies Omit -export function createCompiledMatcher(): NEW_Matcher_Resolve { +export function createCompiledMatcher(): RouteResolver { const matchers = new Map() // TODO: allow custom encode/decode functions @@ -216,7 +213,7 @@ export function createCompiledMatcher(): NEW_Matcher_Resolve { // ) // const decodeQuery = transformObject.bind(null, decode, decode) - function resolve(...args: MatcherResolveArgs): NEW_MatcherLocationResolved { + function resolve(...args: MatcherResolveArgs): NEW_LocationResolved { const [location, currentLocation] = args if (typeof location === 'string') { // string location, e.g. '/foo', '../bar', 'baz' @@ -228,7 +225,7 @@ export function createCompiledMatcher(): NEW_Matcher_Resolve { for (matcher of matchers.values()) { const params = matcher.matchLocation(url) if (params) { - parsedParams = matcher.formatParams( + parsedParams = matcher.parseParams( transformObject(String, decode, params[0]), // already decoded params[1], @@ -268,7 +265,17 @@ export function createCompiledMatcher(): NEW_Matcher_Resolve { // unencoded params in a formatted form that the user came up with const params = location.params ?? currentLocation!.params - const mixedUnencodedParams = matcher.unformatParams(params) + const mixedUnencodedParams = matcher.matchParams(params) + + if (!mixedUnencodedParams) { + throw new Error( + `Invalid params for matcher "${String(name)}":\n${JSON.stringify( + params, + null, + 2 + )}` + ) + } const path = matcher.buildPath( // encode the values before building the path diff --git a/packages/router/src/new-route-resolver/matchers/path-param.ts b/packages/router/src/new-route-resolver/matchers/path-param.ts new file mode 100644 index 000000000..e17e78068 --- /dev/null +++ b/packages/router/src/new-route-resolver/matchers/path-param.ts @@ -0,0 +1,48 @@ +import type { MatcherPathParams } from '../matcher' +import { MatcherParamsFormatted } from '../matcher-location' +import type { + MatcherPatternPath, + PatternPathParamOptions, +} from '../matcher-pattern' + +export class PatterParamPath implements MatcherPatternPath { + options: Required, 'default'>> & { + default: undefined | (() => T) | T + } + + constructor(options: PatternPathParamOptions) { + this.options = { + set: String, + default: undefined, + ...options, + } + } + + match(path: string): MatcherPathParams { + const match = this.options.re.exec(path)?.groups ?? {} + if (!match) { + throw new Error( + `Path "${path}" does not match the pattern "${String( + this.options.re + )}"}` + ) + } + const params: MatcherPathParams = {} + for (let i = 0; i < this.options.keys.length; i++) { + params[this.options.keys[i]] = match[i + 1] ?? null + } + return params + } + + buildPath(path: MatcherPathParams): string { + throw new Error('Method not implemented.') + } + + parse(params: MatcherPathParams): MatcherParamsFormatted { + throw new Error('Method not implemented.') + } + + serialize(params: MatcherParamsFormatted): MatcherPathParams { + throw new Error('Method not implemented.') + } +} diff --git a/packages/router/src/new-route-resolver/matchers/path-static.ts b/packages/router/src/new-route-resolver/matchers/path-static.ts new file mode 100644 index 000000000..0d6ebd3fe --- /dev/null +++ b/packages/router/src/new-route-resolver/matchers/path-static.ts @@ -0,0 +1,15 @@ +import type { MatcherPatternPath } from '../matcher-pattern' + +export class PathMatcherStatic implements MatcherPatternPath { + constructor(private path: string) {} + + match(path: string) { + if (this.path === path) return {} + throw new Error() + // return this.path === path ? {} : null + } + + buildPath() { + return this.path + } +} From b7204faed54936a1341336dcbc96d4dc8cfdc6fb Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Wed, 26 Jun 2024 16:12:40 +0200 Subject: [PATCH 08/40] chore: error matches --- .../src/new-route-resolver/matcher-pattern.ts | 12 +++++++++--- .../src/new-route-resolver/matchers/errors.ts | 13 +++++++++++++ .../src/new-route-resolver/matchers/path-static.ts | 6 +++--- packages/router/vue-router-auto.d.ts | 5 +---- 4 files changed, 26 insertions(+), 10 deletions(-) create mode 100644 packages/router/src/new-route-resolver/matchers/errors.ts diff --git a/packages/router/src/new-route-resolver/matcher-pattern.ts b/packages/router/src/new-route-resolver/matcher-pattern.ts index a9f8f5e83..2e066bf87 100644 --- a/packages/router/src/new-route-resolver/matcher-pattern.ts +++ b/packages/router/src/new-route-resolver/matcher-pattern.ts @@ -9,7 +9,8 @@ import type { MatcherParamsFormatted } from './matcher-location' /** * Allows to match, extract, parse and build a path. Tailored to iterate through route records and check if a location * matches. When it cannot match, it returns `null` instead of throwing to not force a try/catch block around each - * iteration in for loops. + * iteration in for loops. Not meant to handle encoding/decoding. It expects different parts of the URL to be either + * encoded or decoded depending on the method. */ export interface MatcherPattern { /** @@ -36,8 +37,8 @@ export interface MatcherPattern { | null /** - * Extracts the defined params from an encoded path, query, and hash parsed from a URL. Does not apply formatting or - * decoding. If the URL does not match the pattern, returns `null`. + * Extracts the defined params from an encoded path, decoded query, and decoded hash parsed from a URL. Does not apply + * formatting or decoding. If the URL does not match the pattern, returns `null`. * * @example * ```ts @@ -54,6 +55,11 @@ export interface MatcherPattern { * pattern.parseLocation({ path: '/foo', query: {}, hash: '#hello' }) * // null // the query param is missing * ``` + * + * @param location - URL parts to extract from + * @param location.path - encoded path + * @param location.query - decoded query + * @param location.hash - decoded hash */ matchLocation(location: { path: string diff --git a/packages/router/src/new-route-resolver/matchers/errors.ts b/packages/router/src/new-route-resolver/matchers/errors.ts new file mode 100644 index 000000000..51c5574a8 --- /dev/null +++ b/packages/router/src/new-route-resolver/matchers/errors.ts @@ -0,0 +1,13 @@ +export class MatchMiss extends Error { + name = 'MatchMiss' +} + +export const miss = () => new MatchMiss() + +export class ParamInvalid extends Error { + name = 'ParamInvalid' + constructor(public param: string) { + super() + } +} +export const invalid = (param: string) => new ParamInvalid(param) diff --git a/packages/router/src/new-route-resolver/matchers/path-static.ts b/packages/router/src/new-route-resolver/matchers/path-static.ts index 0d6ebd3fe..7d5e968ff 100644 --- a/packages/router/src/new-route-resolver/matchers/path-static.ts +++ b/packages/router/src/new-route-resolver/matchers/path-static.ts @@ -1,12 +1,12 @@ import type { MatcherPatternPath } from '../matcher-pattern' +import { miss } from './errors' -export class PathMatcherStatic implements MatcherPatternPath { +export class MatcherPathStatic implements MatcherPatternPath { constructor(private path: string) {} match(path: string) { if (this.path === path) return {} - throw new Error() - // return this.path === path ? {} : null + throw miss() } buildPath() { diff --git a/packages/router/vue-router-auto.d.ts b/packages/router/vue-router-auto.d.ts index 56e8a0979..797a70599 100644 --- a/packages/router/vue-router-auto.d.ts +++ b/packages/router/vue-router-auto.d.ts @@ -1,4 +1 @@ -/** - * Extended by unplugin-vue-router to create typed routes. - */ -export interface RouteNamedMap {} +// augmented by unplugin-vue-router From 684e9705af0fe7654875efbb5c952f96c95f49ae Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Wed, 26 Jun 2024 16:22:24 +0200 Subject: [PATCH 09/40] test: static matcher --- .../src/new-route-resolver/matcher-pattern.ts | 2 +- .../matchers/path-static.spec.ts | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 packages/router/src/new-route-resolver/matchers/path-static.spec.ts diff --git a/packages/router/src/new-route-resolver/matcher-pattern.ts b/packages/router/src/new-route-resolver/matcher-pattern.ts index 2e066bf87..3b2bbdbfd 100644 --- a/packages/router/src/new-route-resolver/matcher-pattern.ts +++ b/packages/router/src/new-route-resolver/matcher-pattern.ts @@ -153,7 +153,7 @@ export class MatcherPatternImpl implements MatcherPattern { query: MatcherQueryParams hash: string }) { - // TODO: is this performant? Compare to a check with `null + // TODO: is this performant? bench compare to a check with `null try { return [ this.path.match(location.path), diff --git a/packages/router/src/new-route-resolver/matchers/path-static.spec.ts b/packages/router/src/new-route-resolver/matchers/path-static.spec.ts new file mode 100644 index 000000000..aae50551c --- /dev/null +++ b/packages/router/src/new-route-resolver/matchers/path-static.spec.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from 'vitest' +import { MatcherPathStatic } from './path-static' + +describe('PathStaticMatcher', () => { + it('matches', () => { + expect(new MatcherPathStatic('/').match('/')).toEqual({}) + expect(() => new MatcherPathStatic('/').match('/no')).toThrowError() + expect(new MatcherPathStatic('/ok/ok').match('/ok/ok')).toEqual({}) + expect(() => new MatcherPathStatic('/ok/ok').match('/ok/no')).toThrowError() + }) + + it('builds path', () => { + expect(new MatcherPathStatic('/').buildPath()).toBe('/') + expect(new MatcherPathStatic('/ok').buildPath()).toBe('/ok') + expect(new MatcherPathStatic('/ok/ok').buildPath()).toEqual('/ok/ok') + }) +}) From 7cec10b06b6f2289e82173e2222a1360f51109a4 Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Tue, 9 Jul 2024 09:54:37 +0200 Subject: [PATCH 10/40] refactor: unused code --- packages/router/src/typed-routes/route-location.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/router/src/typed-routes/route-location.ts b/packages/router/src/typed-routes/route-location.ts index d370c99ab..4eb431803 100644 --- a/packages/router/src/typed-routes/route-location.ts +++ b/packages/router/src/typed-routes/route-location.ts @@ -2,7 +2,6 @@ import type { RouteLocationOptions, RouteQueryAndHash, _RouteLocationBase, - RouteParamsGeneric, RouteLocationMatched, RouteParamsRawGeneric, } from '../types' @@ -50,7 +49,6 @@ export type RouteLocationTypedList< */ export interface RouteLocationNormalizedGeneric extends _RouteLocationBase { name: RouteRecordNameGeneric - params: RouteParamsGeneric /** * Array of {@link RouteRecordNormalized} */ From b0e0f0def045efc9b1e550f45cd3661bba5968bf Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Wed, 4 Dec 2024 16:51:03 +0100 Subject: [PATCH 11/40] chore: ignore temp tsconfig --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index faa347817..9053a1101 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ local.log _selenium-server.log packages/*/LICENSE tracing_output +tsconfig.vitest-temp.json From 42bd1acb10c00e2875dabb14bfe11ea219068ec6 Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Thu, 5 Dec 2024 11:16:12 +0100 Subject: [PATCH 12/40] test: better IM after hash --- packages/router/__tests__/location.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/router/__tests__/location.spec.ts b/packages/router/__tests__/location.spec.ts index 98dadf1e8..0511ad89f 100644 --- a/packages/router/__tests__/location.spec.ts +++ b/packages/router/__tests__/location.spec.ts @@ -134,18 +134,18 @@ describe('parseURL', () => { }) }) - it('avoids ? after the hash', () => { + it('correctly parses a ? after the hash', () => { expect(parseURL('/foo#?a=one')).toEqual({ fullPath: '/foo#?a=one', path: '/foo', hash: '#?a=one', query: {}, }) - expect(parseURL('/foo/#?a=one')).toEqual({ - fullPath: '/foo/#?a=one', + expect(parseURL('/foo/?a=two#?a=one')).toEqual({ + fullPath: '/foo/?a=two#?a=one', path: '/foo/', hash: '#?a=one', - query: {}, + query: { a: 'two' }, }) }) From 82047444c7db6b7886e7195be73656bc8e62912d Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Thu, 5 Dec 2024 11:24:40 +0100 Subject: [PATCH 13/40] test: url parsing --- packages/router/__tests__/location.spec.ts | 31 ++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/packages/router/__tests__/location.spec.ts b/packages/router/__tests__/location.spec.ts index 0511ad89f..7b7497687 100644 --- a/packages/router/__tests__/location.spec.ts +++ b/packages/router/__tests__/location.spec.ts @@ -156,12 +156,24 @@ describe('parseURL', () => { hash: '#hash', query: {}, }) + expect(parseURL('/foo#hash')).toEqual({ + fullPath: '/foo#hash', + path: '/foo', + hash: '#hash', + query: {}, + }) expect(parseURL('/foo?')).toEqual({ fullPath: '/foo?', path: '/foo', hash: '', query: {}, }) + expect(parseURL('/foo')).toEqual({ + fullPath: '/foo', + path: '/foo', + hash: '', + query: {}, + }) }) it('works with empty hash', () => { @@ -177,6 +189,12 @@ describe('parseURL', () => { hash: '#', query: {}, }) + expect(parseURL('/foo')).toEqual({ + fullPath: '/foo', + path: '/foo', + hash: '', + query: {}, + }) }) it('works with a relative paths', () => { @@ -198,7 +216,20 @@ describe('parseURL', () => { hash: '', query: {}, }) + // cannot go below root + expect(parseURL('../../foo', '/parent/bar')).toEqual({ + fullPath: '/foo', + path: '/foo', + hash: '', + query: {}, + }) + expect(parseURL('', '/parent/bar')).toEqual({ + fullPath: '/parent/bar', + path: '/parent/bar', + hash: '', + query: {}, + }) expect(parseURL('#foo', '/parent/bar')).toEqual({ fullPath: '/parent/bar#foo', path: '/parent/bar', From ca95567e08d8de4adb518a6e23a4dd590d23dd36 Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Fri, 6 Dec 2024 09:40:16 +0100 Subject: [PATCH 14/40] refactor: simplify parseURL --- packages/router/src/location.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/router/src/location.ts b/packages/router/src/location.ts index a8d5f9192..ad317332c 100644 --- a/packages/router/src/location.ts +++ b/packages/router/src/location.ts @@ -52,19 +52,19 @@ export function parseURL( // NOTE: we could use URL and URLSearchParams but they are 2 to 5 times slower than this method const hashPos = location.indexOf('#') - // let searchPos = location.indexOf('?') - let searchPos = - hashPos >= 0 - ? // find the query string before the hash to avoid including a ? in the hash - // e.g. /foo#hash?query -> has no query - location.lastIndexOf('?', hashPos) - : location.indexOf('?') + let searchPos = location.indexOf('?') + + // This ensures that the ? is not part of the hash + // e.g. /foo#hash?query -> has no query + searchPos = hashPos >= 0 && searchPos > hashPos ? -1 : searchPos if (searchPos >= 0) { path = location.slice(0, searchPos) - searchString = - '?' + - location.slice(searchPos + 1, hashPos > 0 ? hashPos : location.length) + // keep the ? char + searchString = location.slice( + searchPos, + hashPos > 0 ? hashPos : location.length + ) query = parseQuery(searchString) } @@ -213,7 +213,7 @@ export function resolveRelativePath(to: string, from: string): string { return to } - // resolve '' with '/anything' -> '/anything' + // resolve to: '' with from: '/anything' -> '/anything' if (!to) return from const fromSegments = from.split('/') From 17bb729303a9b806eff8c9cb270b393388eba63d Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Fri, 6 Dec 2024 09:41:57 +0100 Subject: [PATCH 15/40] chore: comment [skip ci] --- packages/router/src/location.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/router/src/location.ts b/packages/router/src/location.ts index ad317332c..af2444de3 100644 --- a/packages/router/src/location.ts +++ b/packages/router/src/location.ts @@ -63,6 +63,7 @@ export function parseURL( // keep the ? char searchString = location.slice( searchPos, + // hashPos cannot be 0 because there is a search section in the location hashPos > 0 ? hashPos : location.length ) From f8cdff0fdeb8fa415fd49a45757dc3d456a21507 Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Fri, 6 Dec 2024 10:03:39 +0100 Subject: [PATCH 16/40] chore: comments --- .../router/src/new-route-resolver/matcher-location.ts | 7 ++++++- .../router/src/new-route-resolver/matchers/errors.ts | 11 +++++++++++ packages/router/tsconfig.json | 11 +++-------- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/packages/router/src/new-route-resolver/matcher-location.ts b/packages/router/src/new-route-resolver/matcher-location.ts index bb44326b2..1bfcd9a16 100644 --- a/packages/router/src/new-route-resolver/matcher-location.ts +++ b/packages/router/src/new-route-resolver/matcher-location.ts @@ -1,7 +1,9 @@ import type { LocationQueryRaw } from '../query' import type { MatcherName } from './matcher' -// the matcher can serialize and deserialize params +/** + * Generic object of params that can be passed to a matcher. + */ export type MatcherParamsFormatted = Record export interface MatcherLocationAsName { @@ -10,6 +12,9 @@ export interface MatcherLocationAsName { query?: LocationQueryRaw hash?: string + /** + * A path is ignored if `name` is provided. + */ path?: undefined } diff --git a/packages/router/src/new-route-resolver/matchers/errors.ts b/packages/router/src/new-route-resolver/matchers/errors.ts index 51c5574a8..4ad69cc4c 100644 --- a/packages/router/src/new-route-resolver/matchers/errors.ts +++ b/packages/router/src/new-route-resolver/matchers/errors.ts @@ -1,9 +1,20 @@ +/** + * NOTE: for these classes to keep the same code we need to tell TS with `"useDefineForClassFields": true` in the `tsconfig.json` + */ + +/** + * Error throw when a matcher miss + */ export class MatchMiss extends Error { name = 'MatchMiss' } +// NOTE: not sure about having a helper. Using `new MatchMiss(description?)` is good enough export const miss = () => new MatchMiss() +/** + * Error throw when a param is invalid when parsing params from path, query, or hash. + */ export class ParamInvalid extends Error { name = 'ParamInvalid' constructor(public param: string) { diff --git a/packages/router/tsconfig.json b/packages/router/tsconfig.json index 41fc6c378..318f5c658 100644 --- a/packages/router/tsconfig.json +++ b/packages/router/tsconfig.json @@ -22,19 +22,14 @@ "noImplicitReturns": true, "strict": true, "skipLibCheck": true, + "useDefineForClassFields": true, // "noUncheckedIndexedAccess": true, "experimentalDecorators": true, "resolveJsonModule": true, "esModuleInterop": true, "removeComments": false, "jsx": "preserve", - "lib": [ - "esnext", - "dom" - ], - "types": [ - "node", - "vite/client" - ] + "lib": ["esnext", "dom"], + "types": ["node", "vite/client"] } } From 0d86f5a061d116234f272b5459d906bdcacfce84 Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Fri, 6 Dec 2024 22:01:44 +0100 Subject: [PATCH 17/40] refactor: renames and minor changes --- .../new-route-resolver/matcher-location.ts | 2 +- .../src/new-route-resolver/matcher-pattern.ts | 9 ++-- .../src/new-route-resolver/matcher.test-d.ts | 48 ++++++++++++++----- .../router/src/new-route-resolver/matcher.ts | 37 ++++++++++---- 4 files changed, 72 insertions(+), 24 deletions(-) diff --git a/packages/router/src/new-route-resolver/matcher-location.ts b/packages/router/src/new-route-resolver/matcher-location.ts index 1bfcd9a16..c205a4564 100644 --- a/packages/router/src/new-route-resolver/matcher-location.ts +++ b/packages/router/src/new-route-resolver/matcher-location.ts @@ -6,7 +6,7 @@ import type { MatcherName } from './matcher' */ export type MatcherParamsFormatted = Record -export interface MatcherLocationAsName { +export interface MatcherLocationAsNamed { name: MatcherName params: MatcherParamsFormatted query?: LocationQueryRaw diff --git a/packages/router/src/new-route-resolver/matcher-pattern.ts b/packages/router/src/new-route-resolver/matcher-pattern.ts index 3b2bbdbfd..049109e11 100644 --- a/packages/router/src/new-route-resolver/matcher-pattern.ts +++ b/packages/router/src/new-route-resolver/matcher-pattern.ts @@ -47,11 +47,11 @@ export interface MatcherPattern { * query: { used: String }, // we require a `used` query param * }) * // /?used=2 - * pattern.parseLocation({ path: '/', query: { used: '' }, hash: '' }) // null + * pattern.parseLocation({ path: '/', query: { used: '' }, hash: '' }) // null becauso no /foo * // /foo?used=2¬Used¬Used=2#hello * pattern.parseLocation({ path: '/foo', query: { used: '2', notUsed: [null, '2']}, hash: '#hello' }) - * // { used: '2' } // we extract the required params - * // /foo?used=2#hello + * // [{}, { used: '2' }, {}]// we extract the required params + * // /foo?other=2#hello * pattern.parseLocation({ path: '/foo', query: {}, hash: '#hello' }) * // null // the query param is missing * ``` @@ -109,6 +109,7 @@ export interface PatternPathParamOptions export interface PatternQueryParamOptions extends PatternParamOptions_Base { + // FIXME: can be removed? seems to be the same as above get: (value: MatcherQueryParamsValue) => T set?: (value: T) => MatcherQueryParamsValue } @@ -153,7 +154,7 @@ export class MatcherPatternImpl implements MatcherPattern { query: MatcherQueryParams hash: string }) { - // TODO: is this performant? bench compare to a check with `null + // TODO: is this performant? bench compare to a check with `null` try { return [ this.path.match(location.path), diff --git a/packages/router/src/new-route-resolver/matcher.test-d.ts b/packages/router/src/new-route-resolver/matcher.test-d.ts index 412cb0719..bb45c5129 100644 --- a/packages/router/src/new-route-resolver/matcher.test-d.ts +++ b/packages/router/src/new-route-resolver/matcher.test-d.ts @@ -1,16 +1,42 @@ -import { describe, it } from 'vitest' +import { describe, expectTypeOf, it } from 'vitest' import { NEW_LocationResolved, createCompiledMatcher } from './matcher' describe('Matcher', () => { - it('resolves locations', () => { - const matcher = createCompiledMatcher() - matcher.resolve('/foo') - // @ts-expect-error: needs currentLocation - matcher.resolve('foo') - matcher.resolve('foo', {} as NEW_LocationResolved) - matcher.resolve({ name: 'foo', params: {} }) - // @ts-expect-error: needs currentLocation - matcher.resolve({ params: { id: 1 } }) - matcher.resolve({ params: { id: 1 } }, {} as NEW_LocationResolved) + const matcher = createCompiledMatcher() + + describe('matcher.resolve()', () => { + it('resolves absolute string locations', () => { + expectTypeOf( + matcher.resolve('/foo') + ).toEqualTypeOf() + }) + + it('fails on non absolute location without a currentLocation', () => { + // @ts-expect-error: needs currentLocation + matcher.resolve('foo') + }) + + it('resolves relative locations', () => { + expectTypeOf( + matcher.resolve('foo', {} as NEW_LocationResolved) + ).toEqualTypeOf() + }) + + it('resolved named locations', () => { + expectTypeOf( + matcher.resolve({ name: 'foo', params: {} }) + ).toEqualTypeOf() + }) + + it('fails on object relative location without a currentLocation', () => { + // @ts-expect-error: needs currentLocation + matcher.resolve({ params: { id: 1 } }) + }) + + it('resolves object relative locations with a currentLocation', () => { + expectTypeOf( + matcher.resolve({ params: { id: 1 } }, {} as NEW_LocationResolved) + ).toEqualTypeOf() + }) }) }) diff --git a/packages/router/src/new-route-resolver/matcher.ts b/packages/router/src/new-route-resolver/matcher.ts index 4aa742e93..31b1d0319 100644 --- a/packages/router/src/new-route-resolver/matcher.ts +++ b/packages/router/src/new-route-resolver/matcher.ts @@ -13,7 +13,7 @@ import { } from '../encoding' import { parseURL, stringifyURL } from '../location' import type { - MatcherLocationAsName, + MatcherLocationAsNamed, MatcherLocationAsRelative, MatcherParamsFormatted, } from './matcher-location' @@ -31,7 +31,7 @@ export interface RouteResolver { /** * Resolves a string location relative to another location. A relative location can be `./same-folder`, - * `../parent-folder`, or even `same-folder`. + * `../parent-folder`, `same-folder`, or even `?page=2`. */ resolve( relativeLocation: string, @@ -41,7 +41,7 @@ export interface RouteResolver { /** * Resolves a location by its name. Any required params or query must be passed in the `options` argument. */ - resolve(location: MatcherLocationAsName): NEW_LocationResolved + resolve(location: MatcherLocationAsNamed): NEW_LocationResolved /** * Resolves a location by its path. Any required query must be passed. @@ -67,7 +67,7 @@ export interface RouteResolver { type MatcherResolveArgs = | [absoluteLocation: `/${string}`] | [relativeLocation: string, currentLocation: NEW_LocationResolved] - | [location: MatcherLocationAsName] + | [location: MatcherLocationAsNamed] | [ relativeLocation: MatcherLocationAsRelative, currentLocation: NEW_LocationResolved @@ -108,7 +108,11 @@ export type MatcherPathParams = Record export type MatcherQueryParamsValue = string | null | Array export type MatcherQueryParams = Record -export function applyToParams( +/** + * Apply a function to all properties in an object. It's used to encode/decode params and queries. + * @internal + */ +export function applyFnToObject( fn: (v: string | number | null | undefined) => R, params: MatcherPathParams | LocationQuery | undefined ): Record { @@ -195,7 +199,7 @@ function transformObject( } export const NO_MATCH_LOCATION = { - name: Symbol('no-match'), + name: __DEV__ ? Symbol('no-match') : Symbol(), params: {}, matched: [], } satisfies Omit @@ -215,8 +219,9 @@ export function createCompiledMatcher(): RouteResolver { function resolve(...args: MatcherResolveArgs): NEW_LocationResolved { const [location, currentLocation] = args + + // string location, e.g. '/foo', '../bar', 'baz', '?page=1' if (typeof location === 'string') { - // string location, e.g. '/foo', '../bar', 'baz' const url = parseURL(parseQuery, location, currentLocation?.path) let matcher: MatcherPattern | undefined @@ -257,6 +262,21 @@ export function createCompiledMatcher(): RouteResolver { } } else { // relative location or by name + if (__DEV__ && location.name == null && currentLocation == null) { + console.warn( + `Cannot resolve an unnamed relative location without a current location. This will throw in production.`, + location + ) + return { + ...NO_MATCH_LOCATION, + fullPath: '/', + path: '/', + query: {}, + hash: '', + } + } + + // either one of them must be defined and is catched by the dev only warn above const name = location.name ?? currentLocation!.name const matcher = matchers.get(name) if (!matcher) { @@ -264,7 +284,8 @@ export function createCompiledMatcher(): RouteResolver { } // unencoded params in a formatted form that the user came up with - const params = location.params ?? currentLocation!.params + const params: MatcherParamsFormatted = + location.params ?? currentLocation!.params const mixedUnencodedParams = matcher.matchParams(params) if (!mixedUnencodedParams) { From c18e14fef310684fc37130fcdac71b172c34e85e Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Mon, 9 Dec 2024 21:57:13 +0100 Subject: [PATCH 18/40] refactor: simplify matcher interfaces --- .../new-route-resolver/matcher-location.ts | 6 + .../src/new-route-resolver/matcher.spec.ts | 250 +++++++++--------- .../src/new-route-resolver/matcher.test-d.ts | 4 +- .../router/src/new-route-resolver/matcher.ts | 143 +++++++--- .../new-route-resolver/new-matcher-pattern.ts | 197 ++++++++++++++ packages/router/src/query.ts | 3 +- 6 files changed, 438 insertions(+), 165 deletions(-) create mode 100644 packages/router/src/new-route-resolver/new-matcher-pattern.ts diff --git a/packages/router/src/new-route-resolver/matcher-location.ts b/packages/router/src/new-route-resolver/matcher-location.ts index c205a4564..3744e8cec 100644 --- a/packages/router/src/new-route-resolver/matcher-location.ts +++ b/packages/router/src/new-route-resolver/matcher-location.ts @@ -6,8 +6,14 @@ import type { MatcherName } from './matcher' */ export type MatcherParamsFormatted = Record +/** + * Empty object in TS. + */ +export type EmptyParams = Record + export interface MatcherLocationAsNamed { name: MatcherName + // FIXME: should this be optional? params: MatcherParamsFormatted query?: LocationQueryRaw hash?: string diff --git a/packages/router/src/new-route-resolver/matcher.spec.ts b/packages/router/src/new-route-resolver/matcher.spec.ts index 52d8b208a..3cb67af19 100644 --- a/packages/router/src/new-route-resolver/matcher.spec.ts +++ b/packages/router/src/new-route-resolver/matcher.spec.ts @@ -1,6 +1,14 @@ import { describe, expect, it } from 'vitest' -import { MatcherPatternImpl, MatcherPatternPath } from './matcher-pattern' -import { createCompiledMatcher } from './matcher' +import { MatcherPatternImpl } from './matcher-pattern' +import { createCompiledMatcher, NO_MATCH_LOCATION } from './matcher' +import { + MatcherPatternParams_Base, + MatcherPattern, + MatcherPatternPath, + MatcherPatternQuery, +} from './new-matcher-pattern' +import { miss } from './matchers/errors' +import { EmptyParams } from './matcher-location' function createMatcherPattern( ...args: ConstructorParameters @@ -8,54 +16,121 @@ function createMatcherPattern( return new MatcherPatternImpl(...args) } -const EMPTY_PATH_PATTERN_MATCHER = { - match: (path: string) => ({}), - parse: (params: {}) => ({}), - serialize: (params: {}) => ({}), - buildPath: () => '/', -} satisfies MatcherPatternPath +const ANY_PATH_PATTERN_MATCHER: MatcherPatternPath<{ pathMatch: string }> = { + match(path) { + return { pathMatch: path } + }, + build({ pathMatch }) { + return pathMatch + }, +} + +const EMPTY_PATH_PATTERN_MATCHER: MatcherPatternPath = { + match: path => { + if (path !== '/') { + throw miss() + } + return {} + }, + build: () => '/', +} + +const USER_ID_PATH_PATTERN_MATCHER: MatcherPatternPath<{ id: number }> = { + match(value) { + const match = value.match(/^\/users\/(\d+)$/) + if (!match?.[1]) { + throw miss() + } + const id = Number(match[1]) + if (Number.isNaN(id)) { + throw miss() + } + return { id } + }, + build({ id }) { + return `/users/${id}` + }, +} + +const PAGE_QUERY_PATTERN_MATCHER: MatcherPatternQuery<{ page: number }> = { + match: query => { + const page = Number(query.page) + return { + page: Number.isNaN(page) ? 1 : page, + } + }, + build: params => ({ page: String(params.page) }), +} satisfies MatcherPatternQuery<{ page: number }> + +const ANY_HASH_PATTERN_MATCHER: MatcherPatternParams_Base< + string, + { hash: string | null } +> = { + match: hash => ({ hash: hash ? hash.slice(1) : null }), + build: ({ hash }) => (hash ? `#${hash}` : ''), +} + +const EMPTY_PATH_ROUTE = { + name: 'no params', + path: EMPTY_PATH_PATTERN_MATCHER, +} satisfies MatcherPattern + +const USER_ID_ROUTE = { + name: 'user-id', + path: USER_ID_PATH_PATTERN_MATCHER, +} satisfies MatcherPattern describe('Matcher', () => { + describe('adding and removing', () => { + it('add static path', () => { + const matcher = createCompiledMatcher() + matcher.addRoute(EMPTY_PATH_ROUTE) + }) + + it('adds dynamic path', () => { + const matcher = createCompiledMatcher() + matcher.addRoute(USER_ID_ROUTE) + }) + }) + describe('resolve()', () => { describe('absolute locationss as strings', () => { it('resolves string locations with no params', () => { const matcher = createCompiledMatcher() - matcher.addRoute( - createMatcherPattern(Symbol('foo'), EMPTY_PATH_PATTERN_MATCHER) - ) + matcher.addRoute(EMPTY_PATH_ROUTE) - expect(matcher.resolve('/foo?a=a&b=b#h')).toMatchObject({ - path: '/foo', + expect(matcher.resolve('/?a=a&b=b#h')).toMatchObject({ + path: '/', params: {}, query: { a: 'a', b: 'b' }, hash: '#h', }) }) + it('resolves a not found string', () => { + const matcher = createCompiledMatcher() + expect(matcher.resolve('/bar?q=1#hash')).toEqual({ + ...NO_MATCH_LOCATION, + fullPath: '/bar?q=1#hash', + path: '/bar', + query: { q: '1' }, + hash: '#hash', + matched: [], + }) + }) + it('resolves string locations with params', () => { const matcher = createCompiledMatcher() - matcher.addRoute( - // /users/:id - createMatcherPattern(Symbol('foo'), { - match: (path: string) => { - const match = path.match(/^\/foo\/([^/]+?)$/) - if (!match) throw new Error('no match') - return { id: match[1] } - }, - parse: (params: { id: string }) => ({ id: Number(params.id) }), - serialize: (params: { id: number }) => ({ id: String(params.id) }), - buildPath: params => `/foo/${params.id}`, - }) - ) - - expect(matcher.resolve('/foo/1?a=a&b=b#h')).toMatchObject({ - path: '/foo/1', + matcher.addRoute(USER_ID_ROUTE) + + expect(matcher.resolve('/users/1?a=a&b=b#h')).toMatchObject({ + path: '/users/1', params: { id: 1 }, query: { a: 'a', b: 'b' }, hash: '#h', }) - expect(matcher.resolve('/foo/54?a=a&b=b#h')).toMatchObject({ - path: '/foo/54', + expect(matcher.resolve('/users/54?a=a&b=b#h')).toMatchObject({ + path: '/users/54', params: { id: 54 }, query: { a: 'a', b: 'b' }, hash: '#h', @@ -64,21 +139,16 @@ describe('Matcher', () => { it('resolve string locations with query', () => { const matcher = createCompiledMatcher() - matcher.addRoute( - createMatcherPattern(Symbol('foo'), EMPTY_PATH_PATTERN_MATCHER, { - match: query => ({ - id: Array.isArray(query.id) ? query.id[0] : query.id, - }), - parse: (params: { id: string }) => ({ id: Number(params.id) }), - serialize: (params: { id: number }) => ({ id: String(params.id) }), - }) - ) - - expect(matcher.resolve('/foo?id=100&b=b#h')).toMatchObject({ - params: { id: 100 }, + matcher.addRoute({ + path: ANY_PATH_PATTERN_MATCHER, + query: PAGE_QUERY_PATTERN_MATCHER, + }) + + expect(matcher.resolve('/foo?page=100&b=b#h')).toMatchObject({ + params: { page: 100 }, path: '/foo', query: { - id: '100', + page: '100', b: 'b', }, hash: '#h', @@ -87,84 +157,29 @@ describe('Matcher', () => { it('resolves string locations with hash', () => { const matcher = createCompiledMatcher() - matcher.addRoute( - createMatcherPattern( - Symbol('foo'), - EMPTY_PATH_PATTERN_MATCHER, - undefined, - { - match: hash => hash, - parse: hash => ({ a: hash.slice(1) }), - serialize: ({ a }) => '#a', - } - ) - ) + matcher.addRoute({ + path: ANY_PATH_PATTERN_MATCHER, + hash: ANY_HASH_PATTERN_MATCHER, + }) expect(matcher.resolve('/foo?a=a&b=b#bar')).toMatchObject({ hash: '#bar', - params: { a: 'bar' }, + params: { hash: 'bar' }, path: '/foo', query: { a: 'a', b: 'b' }, }) }) - it('returns a valid location with an empty `matched` array if no match', () => { + it('combines path, query and hash params', () => { const matcher = createCompiledMatcher() - expect(matcher.resolve('/bar')).toMatchInlineSnapshot( - { - hash: '', - matched: [], - params: {}, - path: '/bar', - query: {}, - }, - ` - { - "fullPath": "/bar", - "hash": "", - "matched": [], - "name": Symbol(no-match), - "params": {}, - "path": "/bar", - "query": {}, - } - ` - ) - }) + matcher.addRoute({ + path: USER_ID_PATH_PATTERN_MATCHER, + query: PAGE_QUERY_PATTERN_MATCHER, + hash: ANY_HASH_PATTERN_MATCHER, + }) - it('resolves string locations with all', () => { - const matcher = createCompiledMatcher() - matcher.addRoute( - createMatcherPattern( - Symbol('foo'), - { - buildPath: params => `/foo/${params.id}`, - match: path => { - const match = path.match(/^\/foo\/([^/]+?)$/) - if (!match) throw new Error('no match') - return { id: match[1] } - }, - parse: params => ({ id: Number(params.id) }), - serialize: params => ({ id: String(params.id) }), - }, - { - match: query => ({ - id: Array.isArray(query.id) ? query.id[0] : query.id, - }), - parse: params => ({ q: Number(params.id) }), - serialize: params => ({ id: String(params.q) }), - }, - { - match: hash => hash, - parse: hash => ({ a: hash.slice(1) }), - serialize: ({ a }) => '#a', - } - ) - ) - - expect(matcher.resolve('/foo/1?id=100#bar')).toMatchObject({ - hash: '#bar', - params: { id: 1, q: 100, a: 'bar' }, + expect(matcher.resolve('/users/24?page=100#bar')).toMatchObject({ + params: { id: 24, page: 100, hash: 'bar' }, }) }) }) @@ -172,9 +187,7 @@ describe('Matcher', () => { describe('relative locations as strings', () => { it('resolves a simple relative location', () => { const matcher = createCompiledMatcher() - matcher.addRoute( - createMatcherPattern(Symbol('foo'), EMPTY_PATH_PATTERN_MATCHER) - ) + matcher.addRoute({ path: ANY_PATH_PATTERN_MATCHER }) expect( matcher.resolve('foo', matcher.resolve('/nested/')) @@ -206,9 +219,10 @@ describe('Matcher', () => { describe('named locations', () => { it('resolves named locations with no params', () => { const matcher = createCompiledMatcher() - matcher.addRoute( - createMatcherPattern('home', EMPTY_PATH_PATTERN_MATCHER) - ) + matcher.addRoute({ + name: 'home', + path: EMPTY_PATH_PATTERN_MATCHER, + }) expect(matcher.resolve({ name: 'home', params: {} })).toMatchObject({ name: 'home', diff --git a/packages/router/src/new-route-resolver/matcher.test-d.ts b/packages/router/src/new-route-resolver/matcher.test-d.ts index bb45c5129..c50731a1e 100644 --- a/packages/router/src/new-route-resolver/matcher.test-d.ts +++ b/packages/router/src/new-route-resolver/matcher.test-d.ts @@ -1,8 +1,8 @@ import { describe, expectTypeOf, it } from 'vitest' -import { NEW_LocationResolved, createCompiledMatcher } from './matcher' +import { NEW_LocationResolved, RouteResolver } from './matcher' describe('Matcher', () => { - const matcher = createCompiledMatcher() + const matcher: RouteResolver = {} as any describe('matcher.resolve()', () => { it('resolves absolute string locations', () => { diff --git a/packages/router/src/new-route-resolver/matcher.ts b/packages/router/src/new-route-resolver/matcher.ts index 31b1d0319..c6af61e98 100644 --- a/packages/router/src/new-route-resolver/matcher.ts +++ b/packages/router/src/new-route-resolver/matcher.ts @@ -4,7 +4,12 @@ import { normalizeQuery, stringifyQuery, } from '../query' -import type { MatcherPattern } from './matcher-pattern' +import type { + MatcherPattern, + MatcherPatternHash, + MatcherPatternPath, + MatcherPatternQuery, +} from './new-matcher-pattern' import { warn } from '../warning' import { SLASH_RE, @@ -17,13 +22,17 @@ import type { MatcherLocationAsRelative, MatcherParamsFormatted, } from './matcher-location' +import { RouteRecordRaw } from 'test-dts' +/** + * Allowed types for a matcher name. + */ export type MatcherName = string | symbol /** * Manage and resolve routes. Also handles the encoding, decoding, parsing and serialization of params, query, and hash. */ -export interface RouteResolver { +export interface RouteResolver { /** * Resolves an absolute location (like `/path/to/somewhere`). */ @@ -59,8 +68,8 @@ export interface RouteResolver { currentLocation: NEW_LocationResolved ): NEW_LocationResolved - addRoute(matcher: MatcherPattern, parent?: MatcherPattern): void - removeRoute(matcher: MatcherPattern): void + addRoute(matcher: Matcher, parent?: MatcherNormalized): MatcherNormalized + removeRoute(matcher: MatcherNormalized): void clearRoutes(): void } @@ -204,7 +213,39 @@ export const NO_MATCH_LOCATION = { matched: [], } satisfies Omit -export function createCompiledMatcher(): RouteResolver { +// FIXME: later on, the MatcherRecord should be compatible with RouteRecordRaw (which can miss a path, have children, etc) + +export interface MatcherRecordRaw { + name?: MatcherName + + path: MatcherPatternPath + + query?: MatcherPatternQuery + + hash?: MatcherPatternHash + + children?: MatcherRecordRaw[] +} + +// const a: RouteRecordRaw = {} as any + +/** + * Build the `matched` array of a record that includes all parent records from the root to the current one. + */ +function buildMatched(record: MatcherPattern): MatcherPattern[] { + const matched: MatcherPattern[] = [] + let node: MatcherPattern | undefined = record + while (node) { + matched.unshift(node) + node = node.parent + } + return matched +} + +export function createCompiledMatcher(): RouteResolver< + MatcherRecordRaw, + MatcherPattern +> { const matchers = new Map() // TODO: allow custom encode/decode functions @@ -225,23 +266,39 @@ export function createCompiledMatcher(): RouteResolver { const url = parseURL(parseQuery, location, currentLocation?.path) let matcher: MatcherPattern | undefined + let matched: NEW_LocationResolved['matched'] | undefined let parsedParams: MatcherParamsFormatted | null | undefined for (matcher of matchers.values()) { - const params = matcher.matchLocation(url) - if (params) { - parsedParams = matcher.parseParams( - transformObject(String, decode, params[0]), - // already decoded - params[1], - params[2] + // match the path because the path matcher only needs to be matched here + // match the hash because only the deepest child matters + // End up by building up the matched array, (reversed so it goes from + // root to child) and then match and merge all queries + try { + const pathParams = matcher.path.match(url.path) + const hashParams = matcher.hash?.match(url.hash) + matched = buildMatched(matcher) + const queryParams: MatcherQueryParams = Object.assign( + {}, + ...matched.map(matcher => matcher.query?.match(url.query)) ) + // TODO: test performance + // for (const matcher of matched) { + // Object.assign(queryParams, matcher.query?.match(url.query)) + // } + + parsedParams = { ...pathParams, ...queryParams, ...hashParams } + // console.log('parsedParams', parsedParams) + if (parsedParams) break + } catch (e) { + // for debugging tests + // console.log('❌ ERROR matching', e) } } // No match location - if (!parsedParams || !matcher) { + if (!parsedParams || !matched) { return { ...url, ...NO_MATCH_LOCATION, @@ -253,12 +310,13 @@ export function createCompiledMatcher(): RouteResolver { return { ...url, - name: matcher.name, + // matcher exists if matched exists + name: matcher!.name, params: parsedParams, // already decoded query: url.query, hash: url.hash, - matched: [], + matched, } } else { // relative location or by name @@ -284,46 +342,43 @@ export function createCompiledMatcher(): RouteResolver { } // unencoded params in a formatted form that the user came up with - const params: MatcherParamsFormatted = - location.params ?? currentLocation!.params - const mixedUnencodedParams = matcher.matchParams(params) - - if (!mixedUnencodedParams) { - throw new Error( - `Invalid params for matcher "${String(name)}":\n${JSON.stringify( - params, - null, - 2 - )}` - ) + const params: MatcherParamsFormatted = { + ...currentLocation?.params, + ...location.params, } - - const path = matcher.buildPath( - // encode the values before building the path - transformObject(String, encodeParam, mixedUnencodedParams[0]) + const path = matcher.path.build(params) + const hash = matcher.hash?.build(params) ?? '' + const matched = buildMatched(matcher) + const query = Object.assign( + { + ...currentLocation?.query, + ...normalizeQuery(location.query), + }, + ...matched.map(matcher => matcher.query?.build(params)) ) - // TODO: should pick query from the params but also from the location and merge them - const query = { - ...normalizeQuery(location.query), - // ...matcher.extractQuery(mixedUnencodedParams[1]) - } - const hash = mixedUnencodedParams[2] ?? location.hash ?? '' - return { name, - fullPath: stringifyURL(stringifyQuery, { path, query: {}, hash }), + fullPath: stringifyURL(stringifyQuery, { path, query, hash }), path, - params, - hash, query, - matched: [], + hash, + params, + matched, } } } - function addRoute(matcher: MatcherPattern, parent?: MatcherPattern) { - matchers.set(matcher.name, matcher) + function addRoute(record: MatcherRecordRaw, parent?: MatcherPattern) { + const name = record.name ?? (__DEV__ ? Symbol('unnamed-route') : Symbol()) + // FIXME: proper normalization of the record + const normalizedRecord: MatcherPattern = { + ...record, + name, + parent, + } + matchers.set(name, normalizedRecord) + return normalizedRecord } function removeRoute(matcher: MatcherPattern) { diff --git a/packages/router/src/new-route-resolver/new-matcher-pattern.ts b/packages/router/src/new-route-resolver/new-matcher-pattern.ts new file mode 100644 index 000000000..f231490cd --- /dev/null +++ b/packages/router/src/new-route-resolver/new-matcher-pattern.ts @@ -0,0 +1,197 @@ +import { MatcherName, MatcherQueryParams } from './matcher' +import { EmptyParams, MatcherParamsFormatted } from './matcher-location' +import { MatchMiss, miss } from './matchers/errors' + +export interface MatcherLocation { + /** + * Encoded path + */ + path: string + + /** + * Decoded query. + */ + query: MatcherQueryParams + + /** + * Decoded hash. + */ + hash: string +} + +export interface OLD_MatcherPattern { + /** + * Name of the matcher. Unique across all matchers. + */ + name: MatcherName + + match(location: MatcherLocation): TParams | null + + toLocation(params: TParams): MatcherLocation +} + +export interface MatcherPattern { + /** + * Name of the matcher. Unique across all matchers. + */ + name: MatcherName + + path: MatcherPatternPath + query?: MatcherPatternQuery + hash?: MatcherPatternHash + + parent?: MatcherPattern +} + +export interface MatcherPatternParams_Base< + TIn = string, + TOut extends MatcherParamsFormatted = MatcherParamsFormatted +> { + match(value: TIn): TOut + + build(params: TOut): TIn + // get: (value: MatcherQueryParamsValue) => T + // set?: (value: T) => MatcherQueryParamsValue + // default?: T | (() => T) +} + +export interface MatcherPatternPath< + TParams extends MatcherParamsFormatted = MatcherParamsFormatted +> extends MatcherPatternParams_Base {} + +export class MatcherPatternPathStatic + implements MatcherPatternPath +{ + constructor(private path: string) {} + + match(path: string): EmptyParams { + if (path !== this.path) { + throw miss() + } + return {} + } + + build(): string { + return this.path + } +} +// example of a static matcher built at runtime +// new MatcherPatternPathStatic('/') + +// example of a generated matcher at build time +const HomePathMatcher = { + match: path => { + if (path !== '/') { + throw miss() + } + return {} + }, + build: () => '/', +} satisfies MatcherPatternPath + +export interface MatcherPatternQuery< + TParams extends MatcherParamsFormatted = MatcherParamsFormatted +> extends MatcherPatternParams_Base {} + +const PaginationQueryMatcher = { + match: query => { + const page = Number(query.page) + return { + page: Number.isNaN(page) ? 1 : page, + } + }, + build: params => ({ page: String(params.page) }), +} satisfies MatcherPatternQuery<{ page: number }> + +export interface MatcherPatternHash< + TParams extends MatcherParamsFormatted = MatcherParamsFormatted +> extends MatcherPatternParams_Base {} + +const HeaderHashMatcher = { + match: hash => + hash.startsWith('#') + ? { + header: hash.slice(1), + } + : {}, // null also works + build: ({ header }) => (header ? `#${header}` : ''), +} satisfies MatcherPatternHash<{ header?: string }> + +export class MatcherPatternImpl< + PathParams extends MatcherParamsFormatted, + QueryParams extends MatcherParamsFormatted = EmptyParams, + HashParams extends MatcherParamsFormatted = EmptyParams +> implements OLD_MatcherPattern +{ + parent: MatcherPatternImpl | null = null + children: MatcherPatternImpl[] = [] + + constructor( + public name: MatcherName, + private path: MatcherPatternPath, + private query?: MatcherPatternQuery, + private hash?: MatcherPatternHash + ) {} + + /** + * Matches a parsed query against the matcher and all of the parents. + * @param query - query to match + * @returns matched + * @throws {MatchMiss} if the query does not match + */ + queryMatch(query: MatcherQueryParams): QParams { + // const queryParams: QParams = {} as QParams + const queryParams: QParams[] = [] + let current: MatcherPatternImpl< + MatcherParamsFormatted, + MatcherParamsFormatted, + MatcherParamsFormatted + > | null = this + + while (current) { + queryParams.push(current.query?.match(query) as QParams) + current = current.parent + } + // we give the later matchers precedence + return Object.assign({}, ...queryParams.reverse()) + } + + queryBuild(params: QParams): MatcherQueryParams { + const query: MatcherQueryParams = {} + let current: MatcherPatternImpl< + MatcherParamsFormatted, + MatcherParamsFormatted, + MatcherParamsFormatted + > | null = this + while (current) { + Object.assign(query, current.query?.build(params)) + current = current.parent + } + return query + } + + match( + location: MatcherLocation + ): (PathParams & QParams & HashParams) | null { + try { + const pathParams = this.path.match(location.path) + const queryParams = this.queryMatch(location.query) + const hashParams = this.hash?.match(location.hash) ?? ({} as HashParams) + + return { ...pathParams, ...queryParams, ...hashParams } + } catch (err) {} + + return null + } + + toLocation(params: PathParams & QueryParams & HashParams): MatcherLocation { + return { + path: this.path.build(params), + query: this.query?.build(params) ?? {}, + hash: this.hash?.build(params) ?? '', + } + } +} + +// const matcher = new MatcherPatternImpl('name', HomePathMatcher, PaginationQueryMatcher, HeaderHashMatcher) +// matcher.match({ path: '/', query: {}, hash: '' })!.page diff --git a/packages/router/src/query.ts b/packages/router/src/query.ts index f2f5c655c..365d55262 100644 --- a/packages/router/src/query.ts +++ b/packages/router/src/query.ts @@ -89,9 +89,10 @@ export function parseQuery(search: string): LocationQuery { * @param query - query object to stringify * @returns string version of the query without the leading `?` */ -export function stringifyQuery(query: LocationQueryRaw): string { +export function stringifyQuery(query: LocationQueryRaw | undefined): string { let search = '' for (let key in query) { + // FIXME: we could do search ||= '?' so that the returned value already has the leading ? const value = query[key] key = encodeQueryKey(key) if (value == null) { From e1de9e6c7844943f8c41219cf5dbd287d5e3f2dd Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Mon, 9 Dec 2024 22:12:37 +0100 Subject: [PATCH 19/40] refactor: remove unused code --- .../src/new-route-resolver/matcher-pattern.ts | 194 ------------------ .../src/new-route-resolver/matcher.spec.ts | 7 - .../router/src/new-route-resolver/matcher.ts | 52 ++--- .../new-route-resolver/matchers/path-param.ts | 48 ----- .../matchers/path-static.spec.ts | 17 -- .../matchers/path-static.ts | 15 -- .../new-route-resolver/new-matcher-pattern.ts | 148 +------------ 7 files changed, 19 insertions(+), 462 deletions(-) delete mode 100644 packages/router/src/new-route-resolver/matcher-pattern.ts delete mode 100644 packages/router/src/new-route-resolver/matchers/path-param.ts delete mode 100644 packages/router/src/new-route-resolver/matchers/path-static.spec.ts delete mode 100644 packages/router/src/new-route-resolver/matchers/path-static.ts diff --git a/packages/router/src/new-route-resolver/matcher-pattern.ts b/packages/router/src/new-route-resolver/matcher-pattern.ts deleted file mode 100644 index 049109e11..000000000 --- a/packages/router/src/new-route-resolver/matcher-pattern.ts +++ /dev/null @@ -1,194 +0,0 @@ -import type { - MatcherName, - MatcherPathParams, - MatcherQueryParams, - MatcherQueryParamsValue, -} from './matcher' -import type { MatcherParamsFormatted } from './matcher-location' - -/** - * Allows to match, extract, parse and build a path. Tailored to iterate through route records and check if a location - * matches. When it cannot match, it returns `null` instead of throwing to not force a try/catch block around each - * iteration in for loops. Not meant to handle encoding/decoding. It expects different parts of the URL to be either - * encoded or decoded depending on the method. - */ -export interface MatcherPattern { - /** - * Name of the matcher. Unique across all matchers. - */ - name: MatcherName - - // TODO: add route record to be able to build the matched - - /** - * Extracts from an unencoded, parsed params object the ones belonging to the path, query, and hash in their - * serialized format but still unencoded. e.g. `{ id: 2 }` -> `{ id: '2' }`. If any params are missing, return `null`. - * - * @param params - Params to extract from. If any params are missing, throws - */ - matchParams( - params: MatcherParamsFormatted - ): - | readonly [ - pathParams: MatcherPathParams, - queryParams: MatcherQueryParams, - hashParam: string - ] - | null - - /** - * Extracts the defined params from an encoded path, decoded query, and decoded hash parsed from a URL. Does not apply - * formatting or decoding. If the URL does not match the pattern, returns `null`. - * - * @example - * ```ts - * const pattern = createPattern('/foo', { - * path: {}, // nothing is used from the path - * query: { used: String }, // we require a `used` query param - * }) - * // /?used=2 - * pattern.parseLocation({ path: '/', query: { used: '' }, hash: '' }) // null becauso no /foo - * // /foo?used=2¬Used¬Used=2#hello - * pattern.parseLocation({ path: '/foo', query: { used: '2', notUsed: [null, '2']}, hash: '#hello' }) - * // [{}, { used: '2' }, {}]// we extract the required params - * // /foo?other=2#hello - * pattern.parseLocation({ path: '/foo', query: {}, hash: '#hello' }) - * // null // the query param is missing - * ``` - * - * @param location - URL parts to extract from - * @param location.path - encoded path - * @param location.query - decoded query - * @param location.hash - decoded hash - */ - matchLocation(location: { - path: string - query: MatcherQueryParams - hash: string - }): - | readonly [ - pathParams: MatcherPathParams, - queryParams: MatcherQueryParams, - hashParam: string - ] - | null - - /** - * Takes encoded params object to form the `path`, - * - * @param pathParams - encoded path params - */ - buildPath(pathParams: MatcherPathParams): string - - /** - * Runs the decoded params through the parsing functions if any, allowing them to be in be of a type other than a - * string. - * - * @param pathParams - decoded path params - * @param queryParams - decoded query params - * @param hashParam - decoded hash param - */ - parseParams( - pathParams: MatcherPathParams, - queryParams: MatcherQueryParams, - hashParam: string - ): MatcherParamsFormatted | null -} - -interface PatternParamOptions_Base { - get: (value: MatcherQueryParamsValue) => T - set?: (value: T) => MatcherQueryParamsValue - default?: T | (() => T) -} - -export interface PatternPathParamOptions - extends PatternParamOptions_Base { - re: RegExp - keys: string[] -} - -export interface PatternQueryParamOptions - extends PatternParamOptions_Base { - // FIXME: can be removed? seems to be the same as above - get: (value: MatcherQueryParamsValue) => T - set?: (value: T) => MatcherQueryParamsValue -} - -// TODO: allow more than strings -export interface PatternHashParamOptions - extends PatternParamOptions_Base {} - -export interface MatcherPatternPath { - buildPath(path: MatcherPathParams): string - match(path: string): MatcherPathParams - parse?(params: MatcherPathParams): MatcherParamsFormatted - serialize?(params: MatcherParamsFormatted): MatcherPathParams -} - -export interface MatcherPatternQuery { - match(query: MatcherQueryParams): MatcherQueryParams - parse(params: MatcherQueryParams): MatcherParamsFormatted - serialize(params: MatcherParamsFormatted): MatcherQueryParams -} - -export interface MatcherPatternHash { - /** - * Check if the hash matches a pattern and returns it, still encoded with its leading `#`. - * @param hash - encoded hash - */ - match(hash: string): string - parse(hash: string): MatcherParamsFormatted - serialize(params: MatcherParamsFormatted): string -} - -export class MatcherPatternImpl implements MatcherPattern { - constructor( - public name: MatcherName, - private path: MatcherPatternPath, - private query?: MatcherPatternQuery, - private hash?: MatcherPatternHash - ) {} - - matchLocation(location: { - path: string - query: MatcherQueryParams - hash: string - }) { - // TODO: is this performant? bench compare to a check with `null` - try { - return [ - this.path.match(location.path), - this.query?.match(location.query) ?? {}, - this.hash?.match(location.hash) ?? '', - ] as const - } catch { - return null - } - } - - parseParams( - path: MatcherPathParams, - query: MatcherQueryParams, - hash: string - ): MatcherParamsFormatted { - return { - ...this.path.parse?.(path), - ...this.query?.parse(query), - ...this.hash?.parse(hash), - } - } - - buildPath(path: MatcherPathParams): string { - return this.path.buildPath(path) - } - - matchParams( - params: MatcherParamsFormatted - ): [path: MatcherPathParams, query: MatcherQueryParams, hash: string] { - return [ - this.path.serialize?.(params) ?? {}, - this.query?.serialize(params) ?? {}, - this.hash?.serialize(params) ?? '', - ] - } -} diff --git a/packages/router/src/new-route-resolver/matcher.spec.ts b/packages/router/src/new-route-resolver/matcher.spec.ts index 3cb67af19..f368647c8 100644 --- a/packages/router/src/new-route-resolver/matcher.spec.ts +++ b/packages/router/src/new-route-resolver/matcher.spec.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from 'vitest' -import { MatcherPatternImpl } from './matcher-pattern' import { createCompiledMatcher, NO_MATCH_LOCATION } from './matcher' import { MatcherPatternParams_Base, @@ -10,12 +9,6 @@ import { import { miss } from './matchers/errors' import { EmptyParams } from './matcher-location' -function createMatcherPattern( - ...args: ConstructorParameters -) { - return new MatcherPatternImpl(...args) -} - const ANY_PATH_PATTERN_MATCHER: MatcherPatternPath<{ pathMatch: string }> = { match(path) { return { pathMatch: path } diff --git a/packages/router/src/new-route-resolver/matcher.ts b/packages/router/src/new-route-resolver/matcher.ts index c6af61e98..11cf6b098 100644 --- a/packages/router/src/new-route-resolver/matcher.ts +++ b/packages/router/src/new-route-resolver/matcher.ts @@ -11,18 +11,13 @@ import type { MatcherPatternQuery, } from './new-matcher-pattern' import { warn } from '../warning' -import { - SLASH_RE, - encodePath, - encodeQueryValue as _encodeQueryValue, -} from '../encoding' +import { encodeQueryValue as _encodeQueryValue } from '../encoding' import { parseURL, stringifyURL } from '../location' import type { MatcherLocationAsNamed, MatcherLocationAsRelative, MatcherParamsFormatted, } from './matcher-location' -import { RouteRecordRaw } from 'test-dts' /** * Allowed types for a matcher name. @@ -165,20 +160,20 @@ interface FnStableNull { (value: string | number | null | undefined): string | null } -function encodeParam(text: null | undefined, encodeSlash?: boolean): null -function encodeParam(text: string | number, encodeSlash?: boolean): string -function encodeParam( - text: string | number | null | undefined, - encodeSlash?: boolean -): string | null -function encodeParam( - text: string | number | null | undefined, - encodeSlash = true -): string | null { - if (text == null) return null - text = encodePath(text) - return encodeSlash ? text.replace(SLASH_RE, '%2F') : text -} +// function encodeParam(text: null | undefined, encodeSlash?: boolean): null +// function encodeParam(text: string | number, encodeSlash?: boolean): string +// function encodeParam( +// text: string | number | null | undefined, +// encodeSlash?: boolean +// ): string | null +// function encodeParam( +// text: string | number | null | undefined, +// encodeSlash = true +// ): string | null { +// if (text == null) return null +// text = encodePath(text) +// return encodeSlash ? text.replace(SLASH_RE, '%2F') : text +// } // @ts-expect-error: overload are not correctly identified const encodeQueryValue: FnStableNull = @@ -190,23 +185,6 @@ const encodeQueryValue: FnStableNull = // // for ts // value => (value == null ? null : _encodeQueryKey(value)) -function transformObject( - fnKey: (value: string | number) => string, - fnValue: FnStableNull, - query: T -): T { - const encoded: any = {} - - for (const key in query) { - const value = query[key] - encoded[fnKey(key)] = Array.isArray(value) - ? value.map(fnValue) - : fnValue(value as string | number | null | undefined) - } - - return encoded -} - export const NO_MATCH_LOCATION = { name: __DEV__ ? Symbol('no-match') : Symbol(), params: {}, diff --git a/packages/router/src/new-route-resolver/matchers/path-param.ts b/packages/router/src/new-route-resolver/matchers/path-param.ts deleted file mode 100644 index e17e78068..000000000 --- a/packages/router/src/new-route-resolver/matchers/path-param.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { MatcherPathParams } from '../matcher' -import { MatcherParamsFormatted } from '../matcher-location' -import type { - MatcherPatternPath, - PatternPathParamOptions, -} from '../matcher-pattern' - -export class PatterParamPath implements MatcherPatternPath { - options: Required, 'default'>> & { - default: undefined | (() => T) | T - } - - constructor(options: PatternPathParamOptions) { - this.options = { - set: String, - default: undefined, - ...options, - } - } - - match(path: string): MatcherPathParams { - const match = this.options.re.exec(path)?.groups ?? {} - if (!match) { - throw new Error( - `Path "${path}" does not match the pattern "${String( - this.options.re - )}"}` - ) - } - const params: MatcherPathParams = {} - for (let i = 0; i < this.options.keys.length; i++) { - params[this.options.keys[i]] = match[i + 1] ?? null - } - return params - } - - buildPath(path: MatcherPathParams): string { - throw new Error('Method not implemented.') - } - - parse(params: MatcherPathParams): MatcherParamsFormatted { - throw new Error('Method not implemented.') - } - - serialize(params: MatcherParamsFormatted): MatcherPathParams { - throw new Error('Method not implemented.') - } -} diff --git a/packages/router/src/new-route-resolver/matchers/path-static.spec.ts b/packages/router/src/new-route-resolver/matchers/path-static.spec.ts deleted file mode 100644 index aae50551c..000000000 --- a/packages/router/src/new-route-resolver/matchers/path-static.spec.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { MatcherPathStatic } from './path-static' - -describe('PathStaticMatcher', () => { - it('matches', () => { - expect(new MatcherPathStatic('/').match('/')).toEqual({}) - expect(() => new MatcherPathStatic('/').match('/no')).toThrowError() - expect(new MatcherPathStatic('/ok/ok').match('/ok/ok')).toEqual({}) - expect(() => new MatcherPathStatic('/ok/ok').match('/ok/no')).toThrowError() - }) - - it('builds path', () => { - expect(new MatcherPathStatic('/').buildPath()).toBe('/') - expect(new MatcherPathStatic('/ok').buildPath()).toBe('/ok') - expect(new MatcherPathStatic('/ok/ok').buildPath()).toEqual('/ok/ok') - }) -}) diff --git a/packages/router/src/new-route-resolver/matchers/path-static.ts b/packages/router/src/new-route-resolver/matchers/path-static.ts deleted file mode 100644 index 7d5e968ff..000000000 --- a/packages/router/src/new-route-resolver/matchers/path-static.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { MatcherPatternPath } from '../matcher-pattern' -import { miss } from './errors' - -export class MatcherPathStatic implements MatcherPatternPath { - constructor(private path: string) {} - - match(path: string) { - if (this.path === path) return {} - throw miss() - } - - buildPath() { - return this.path - } -} diff --git a/packages/router/src/new-route-resolver/new-matcher-pattern.ts b/packages/router/src/new-route-resolver/new-matcher-pattern.ts index f231490cd..25d7c22ec 100644 --- a/packages/router/src/new-route-resolver/new-matcher-pattern.ts +++ b/packages/router/src/new-route-resolver/new-matcher-pattern.ts @@ -1,34 +1,6 @@ import { MatcherName, MatcherQueryParams } from './matcher' import { EmptyParams, MatcherParamsFormatted } from './matcher-location' -import { MatchMiss, miss } from './matchers/errors' - -export interface MatcherLocation { - /** - * Encoded path - */ - path: string - - /** - * Decoded query. - */ - query: MatcherQueryParams - - /** - * Decoded hash. - */ - hash: string -} - -export interface OLD_MatcherPattern { - /** - * Name of the matcher. Unique across all matchers. - */ - name: MatcherName - - match(location: MatcherLocation): TParams | null - - toLocation(params: TParams): MatcherLocation -} +import { miss } from './matchers/errors' export interface MatcherPattern { /** @@ -48,15 +20,13 @@ export interface MatcherPatternParams_Base< TOut extends MatcherParamsFormatted = MatcherParamsFormatted > { match(value: TIn): TOut - build(params: TOut): TIn - // get: (value: MatcherQueryParamsValue) => T - // set?: (value: T) => MatcherQueryParamsValue - // default?: T | (() => T) } export interface MatcherPatternPath< - TParams extends MatcherParamsFormatted = MatcherParamsFormatted + TParams extends MatcherParamsFormatted = // | undefined // | void // so it might be a bit more convenient // TODO: should we allow to not return anything? It's valid to spread null and undefined + // | null + MatcherParamsFormatted > extends MatcherPatternParams_Base {} export class MatcherPatternPathStatic @@ -78,120 +48,10 @@ export class MatcherPatternPathStatic // example of a static matcher built at runtime // new MatcherPatternPathStatic('/') -// example of a generated matcher at build time -const HomePathMatcher = { - match: path => { - if (path !== '/') { - throw miss() - } - return {} - }, - build: () => '/', -} satisfies MatcherPatternPath - export interface MatcherPatternQuery< TParams extends MatcherParamsFormatted = MatcherParamsFormatted > extends MatcherPatternParams_Base {} -const PaginationQueryMatcher = { - match: query => { - const page = Number(query.page) - return { - page: Number.isNaN(page) ? 1 : page, - } - }, - build: params => ({ page: String(params.page) }), -} satisfies MatcherPatternQuery<{ page: number }> - export interface MatcherPatternHash< TParams extends MatcherParamsFormatted = MatcherParamsFormatted > extends MatcherPatternParams_Base {} - -const HeaderHashMatcher = { - match: hash => - hash.startsWith('#') - ? { - header: hash.slice(1), - } - : {}, // null also works - build: ({ header }) => (header ? `#${header}` : ''), -} satisfies MatcherPatternHash<{ header?: string }> - -export class MatcherPatternImpl< - PathParams extends MatcherParamsFormatted, - QueryParams extends MatcherParamsFormatted = EmptyParams, - HashParams extends MatcherParamsFormatted = EmptyParams -> implements OLD_MatcherPattern -{ - parent: MatcherPatternImpl | null = null - children: MatcherPatternImpl[] = [] - - constructor( - public name: MatcherName, - private path: MatcherPatternPath, - private query?: MatcherPatternQuery, - private hash?: MatcherPatternHash - ) {} - - /** - * Matches a parsed query against the matcher and all of the parents. - * @param query - query to match - * @returns matched - * @throws {MatchMiss} if the query does not match - */ - queryMatch(query: MatcherQueryParams): QParams { - // const queryParams: QParams = {} as QParams - const queryParams: QParams[] = [] - let current: MatcherPatternImpl< - MatcherParamsFormatted, - MatcherParamsFormatted, - MatcherParamsFormatted - > | null = this - - while (current) { - queryParams.push(current.query?.match(query) as QParams) - current = current.parent - } - // we give the later matchers precedence - return Object.assign({}, ...queryParams.reverse()) - } - - queryBuild(params: QParams): MatcherQueryParams { - const query: MatcherQueryParams = {} - let current: MatcherPatternImpl< - MatcherParamsFormatted, - MatcherParamsFormatted, - MatcherParamsFormatted - > | null = this - while (current) { - Object.assign(query, current.query?.build(params)) - current = current.parent - } - return query - } - - match( - location: MatcherLocation - ): (PathParams & QParams & HashParams) | null { - try { - const pathParams = this.path.match(location.path) - const queryParams = this.queryMatch(location.query) - const hashParams = this.hash?.match(location.hash) ?? ({} as HashParams) - - return { ...pathParams, ...queryParams, ...hashParams } - } catch (err) {} - - return null - } - - toLocation(params: PathParams & QueryParams & HashParams): MatcherLocation { - return { - path: this.path.build(params), - query: this.query?.build(params) ?? {}, - hash: this.hash?.build(params) ?? '', - } - } -} - -// const matcher = new MatcherPatternImpl('name', HomePathMatcher, PaginationQueryMatcher, HeaderHashMatcher) -// matcher.match({ path: '/', query: {}, hash: '' })!.page From 00eef1509fb4093a3b7f2a868dbcee5036c42f4e Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Mon, 9 Dec 2024 22:20:06 +0100 Subject: [PATCH 20/40] refactor: rename matcher-pattern --- .../{new-matcher-pattern.ts => matcher-pattern.ts} | 0 packages/router/src/new-route-resolver/matcher.spec.ts | 2 +- packages/router/src/new-route-resolver/matcher.ts | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename packages/router/src/new-route-resolver/{new-matcher-pattern.ts => matcher-pattern.ts} (100%) diff --git a/packages/router/src/new-route-resolver/new-matcher-pattern.ts b/packages/router/src/new-route-resolver/matcher-pattern.ts similarity index 100% rename from packages/router/src/new-route-resolver/new-matcher-pattern.ts rename to packages/router/src/new-route-resolver/matcher-pattern.ts diff --git a/packages/router/src/new-route-resolver/matcher.spec.ts b/packages/router/src/new-route-resolver/matcher.spec.ts index f368647c8..15ca09f83 100644 --- a/packages/router/src/new-route-resolver/matcher.spec.ts +++ b/packages/router/src/new-route-resolver/matcher.spec.ts @@ -5,7 +5,7 @@ import { MatcherPattern, MatcherPatternPath, MatcherPatternQuery, -} from './new-matcher-pattern' +} from './matcher-pattern' import { miss } from './matchers/errors' import { EmptyParams } from './matcher-location' diff --git a/packages/router/src/new-route-resolver/matcher.ts b/packages/router/src/new-route-resolver/matcher.ts index 11cf6b098..26805ddaf 100644 --- a/packages/router/src/new-route-resolver/matcher.ts +++ b/packages/router/src/new-route-resolver/matcher.ts @@ -9,7 +9,7 @@ import type { MatcherPatternHash, MatcherPatternPath, MatcherPatternQuery, -} from './new-matcher-pattern' +} from './matcher-pattern' import { warn } from '../warning' import { encodeQueryValue as _encodeQueryValue } from '../encoding' import { parseURL, stringifyURL } from '../location' From d061304b3827f1fab7cbee8f0281b04abe07348d Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Tue, 10 Dec 2024 14:41:31 +0100 Subject: [PATCH 21/40] refactor: add methods needed by router --- .../new-route-resolver/matcher-location.ts | 20 ++++++++- .../src/new-route-resolver/matcher.spec.ts | 14 ++++++ .../src/new-route-resolver/matcher.test-d.ts | 12 ++++++ .../router/src/new-route-resolver/matcher.ts | 43 +++++++++++++++++-- 4 files changed, 84 insertions(+), 5 deletions(-) diff --git a/packages/router/src/new-route-resolver/matcher-location.ts b/packages/router/src/new-route-resolver/matcher-location.ts index 3744e8cec..b9ca1ab0c 100644 --- a/packages/router/src/new-route-resolver/matcher-location.ts +++ b/packages/router/src/new-route-resolver/matcher-location.ts @@ -19,25 +19,41 @@ export interface MatcherLocationAsNamed { hash?: string /** - * A path is ignored if `name` is provided. + * @deprecated This is ignored when `name` is provided */ path?: undefined } -export interface MatcherLocationAsPath { +export interface MatcherLocationAsPathRelative { path: string query?: LocationQueryRaw hash?: string + /** + * @deprecated This is ignored when `path` is provided + */ name?: undefined + /** + * @deprecated This is ignored when `path` (instead of `name`) is provided + */ params?: undefined } +export interface MatcherLocationAsPathAbsolute + extends MatcherLocationAsPathRelative { + path: `/${string}` +} export interface MatcherLocationAsRelative { params?: MatcherParamsFormatted query?: LocationQueryRaw hash?: string + /** + * @deprecated This location is relative to the next parameter. This `name` will be ignored. + */ name?: undefined + /** + * @deprecated This location is relative to the next parameter. This `path` will be ignored. + */ path?: undefined } diff --git a/packages/router/src/new-route-resolver/matcher.spec.ts b/packages/router/src/new-route-resolver/matcher.spec.ts index 15ca09f83..c15561f53 100644 --- a/packages/router/src/new-route-resolver/matcher.spec.ts +++ b/packages/router/src/new-route-resolver/matcher.spec.ts @@ -209,6 +209,20 @@ describe('Matcher', () => { }) }) + describe('absolute locations as objects', () => { + it('resolves an object location', () => { + const matcher = createCompiledMatcher() + matcher.addRoute(EMPTY_PATH_ROUTE) + expect(matcher.resolve({ path: '/' })).toMatchObject({ + fullPath: '/', + path: '/', + params: {}, + query: {}, + hash: '', + }) + }) + }) + describe('named locations', () => { it('resolves named locations with no params', () => { const matcher = createCompiledMatcher() diff --git a/packages/router/src/new-route-resolver/matcher.test-d.ts b/packages/router/src/new-route-resolver/matcher.test-d.ts index c50731a1e..a60874518 100644 --- a/packages/router/src/new-route-resolver/matcher.test-d.ts +++ b/packages/router/src/new-route-resolver/matcher.test-d.ts @@ -39,4 +39,16 @@ describe('Matcher', () => { ).toEqualTypeOf() }) }) + + it('does not allow a name + path', () => { + matcher.resolve({ + // ...({} as NEW_LocationResolved), + name: 'foo', + params: {}, + // @ts-expect-error: name + path + path: '/e', + }) + // @ts-expect-error: name + currentLocation + matcher.resolve({ name: 'a', params: {} }, {} as NEW_LocationResolved) + }) }) diff --git a/packages/router/src/new-route-resolver/matcher.ts b/packages/router/src/new-route-resolver/matcher.ts index 26805ddaf..f9fa1c6f8 100644 --- a/packages/router/src/new-route-resolver/matcher.ts +++ b/packages/router/src/new-route-resolver/matcher.ts @@ -15,6 +15,8 @@ import { encodeQueryValue as _encodeQueryValue } from '../encoding' import { parseURL, stringifyURL } from '../location' import type { MatcherLocationAsNamed, + MatcherLocationAsPathAbsolute, + MatcherLocationAsPathRelative, MatcherLocationAsRelative, MatcherParamsFormatted, } from './matcher-location' @@ -48,10 +50,16 @@ export interface RouteResolver { resolve(location: MatcherLocationAsNamed): NEW_LocationResolved /** - * Resolves a location by its path. Any required query must be passed. + * Resolves a location by its absolute path (starts with `/`). Any required query must be passed. * @param location - The location to resolve. */ - // resolve(location: MatcherLocationAsPath): NEW_MatcherLocationResolved + resolve(location: MatcherLocationAsPathAbsolute): NEW_LocationResolved + + resolve( + location: MatcherLocationAsPathRelative, + currentLocation: NEW_LocationResolved + ): NEW_LocationResolved + // NOTE: in practice, this overload can cause bugs. It's better to use named locations /** @@ -66,11 +74,28 @@ export interface RouteResolver { addRoute(matcher: Matcher, parent?: MatcherNormalized): MatcherNormalized removeRoute(matcher: MatcherNormalized): void clearRoutes(): void + + /** + * Get a list of all matchers. + * Previously named `getRoutes()` + */ + getMatchers(): MatcherNormalized[] + + /** + * Get a matcher by its name. + * Previously named `getRecordMatcher()` + */ + getMatcher(name: MatcherName): MatcherNormalized | undefined } type MatcherResolveArgs = | [absoluteLocation: `/${string}`] | [relativeLocation: string, currentLocation: NEW_LocationResolved] + | [absoluteLocation: MatcherLocationAsPathAbsolute] + | [ + relativeLocation: MatcherLocationAsPathRelative, + currentLocation: NEW_LocationResolved + ] | [location: MatcherLocationAsNamed] | [ relativeLocation: MatcherLocationAsRelative, @@ -224,6 +249,7 @@ export function createCompiledMatcher(): RouteResolver< MatcherRecordRaw, MatcherPattern > { + // TODO: we also need an array that has the correct order const matchers = new Map() // TODO: allow custom encode/decode functions @@ -241,6 +267,7 @@ export function createCompiledMatcher(): RouteResolver< // string location, e.g. '/foo', '../bar', 'baz', '?page=1' if (typeof location === 'string') { + // parseURL handles relative paths const url = parseURL(parseQuery, location, currentLocation?.path) let matcher: MatcherPattern | undefined @@ -266,7 +293,6 @@ export function createCompiledMatcher(): RouteResolver< // } parsedParams = { ...pathParams, ...queryParams, ...hashParams } - // console.log('parsedParams', parsedParams) if (parsedParams) break } catch (e) { @@ -296,6 +322,7 @@ export function createCompiledMatcher(): RouteResolver< hash: url.hash, matched, } + // TODO: handle object location { path, query, hash } } else { // relative location or by name if (__DEV__ && location.name == null && currentLocation == null) { @@ -368,11 +395,21 @@ export function createCompiledMatcher(): RouteResolver< matchers.clear() } + function getMatchers() { + return Array.from(matchers.values()) + } + + function getMatcher(name: MatcherName) { + return matchers.get(name) + } + return { resolve, addRoute, removeRoute, clearRoutes, + getMatcher, + getMatchers, } } From 89035ea51b80b9f83b7f9d2267fa96b2a5ff5106 Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Mon, 16 Dec 2024 15:35:49 +0100 Subject: [PATCH 22/40] feat: new dynamic path matcher --- .../src/new-route-resolver/matcher-pattern.ts | 159 +- .../matcher-resolve.spec.ts | 1492 +++++++++++++++++ .../src/new-route-resolver/matcher.spec.ts | 118 +- .../router/src/new-route-resolver/matcher.ts | 46 +- .../new-route-resolver/matchers/test-utils.ts | 76 + packages/router/src/types/utils.ts | 10 + 6 files changed, 1860 insertions(+), 41 deletions(-) create mode 100644 packages/router/src/new-route-resolver/matcher-resolve.spec.ts create mode 100644 packages/router/src/new-route-resolver/matchers/test-utils.ts diff --git a/packages/router/src/new-route-resolver/matcher-pattern.ts b/packages/router/src/new-route-resolver/matcher-pattern.ts index 25d7c22ec..ad582bb8d 100644 --- a/packages/router/src/new-route-resolver/matcher-pattern.ts +++ b/packages/router/src/new-route-resolver/matcher-pattern.ts @@ -1,4 +1,4 @@ -import { MatcherName, MatcherQueryParams } from './matcher' +import { decode, MatcherName, MatcherQueryParams } from './matcher' import { EmptyParams, MatcherParamsFormatted } from './matcher-location' import { miss } from './matchers/errors' @@ -19,14 +19,28 @@ export interface MatcherPatternParams_Base< TIn = string, TOut extends MatcherParamsFormatted = MatcherParamsFormatted > { + /** + * Matches a serialized params value against the pattern. + * + * @param value - params value to parse + * @throws {MatchMiss} if the value doesn't match + * @returns parsed params + */ match(value: TIn): TOut + + /** + * Build a serializable value from parsed params. Should apply encoding if the + * returned value is a string (e.g path and hash should be encoded but query + * shouldn't). + * + * @param value - params value to parse + */ build(params: TOut): TIn } export interface MatcherPatternPath< - TParams extends MatcherParamsFormatted = // | undefined // | void // so it might be a bit more convenient // TODO: should we allow to not return anything? It's valid to spread null and undefined - // | null - MatcherParamsFormatted + // TODO: should we allow to not return anything? It's valid to spread null and undefined + TParams extends MatcherParamsFormatted = MatcherParamsFormatted // | null // | undefined // | void // so it might be a bit more convenient > extends MatcherPatternParams_Base {} export class MatcherPatternPathStatic @@ -48,6 +62,143 @@ export class MatcherPatternPathStatic // example of a static matcher built at runtime // new MatcherPatternPathStatic('/') +export interface Param_GetSet< + TIn extends string | string[] = string | string[], + TOut = TIn +> { + get?: (value: NoInfer) => TOut + set?: (value: NoInfer) => TIn +} + +export type ParamParser_Generic = + | Param_GetSet + | Param_GetSet +// TODO: these are possible values for optional params +// | null | undefined + +/** + * Type safe helper to define a param parser. + * + * @param parser - the parser to define. Will be returned as is. + */ +/*! #__NO_SIDE_EFFECTS__ */ +export function defineParamParser(parser: { + get?: (value: TIn) => TOut + set?: (value: TOut) => TIn +}): Param_GetSet { + return parser +} + +const PATH_PARAM_DEFAULT_GET = (value: string | string[]) => value +const PATH_PARAM_DEFAULT_SET = (value: unknown) => + value && Array.isArray(value) ? value.map(String) : String(value) +// TODO: `(value an null | undefined)` for types + +/** + * NOTE: I tried to make this generic and infer the types from the params but failed. This is what I tried: + * ```ts + * export type ParamsFromParsers

> = { + * [K in keyof P]: P[K] extends Param_GetSet + * ? unknown extends TOut // if any or unknown, use the value of TIn, which defaults to string | string[] + * ? TIn + * : TOut + * : never + * } + * + * export class MatcherPatternPathDynamic< + * ParamsParser extends Record + * > implements MatcherPatternPath> + * { + * private params: Record> = {} + * constructor( + * private re: RegExp, + * params: ParamsParser, + * public build: (params: ParamsFromParsers) => string + * ) {} + * ``` + * It ended up not working in one place or another. It could probably be fixed by + */ + +export type ParamsFromParsers

> = { + [K in keyof P]: P[K] extends Param_GetSet + ? unknown extends TOut // if any or unknown, use the value of TIn, which defaults to string | string[] + ? TIn + : TOut + : never +} + +export class MatcherPatternPathDynamic< + TParams extends MatcherParamsFormatted = MatcherParamsFormatted +> implements MatcherPatternPath +{ + private params: Record> = {} + constructor( + private re: RegExp, + params: Record, + public build: (params: TParams) => string, + private opts: { repeat?: boolean; optional?: boolean } = {} + ) { + for (const paramName in params) { + const param = params[paramName] + this.params[paramName] = { + get: param.get || PATH_PARAM_DEFAULT_GET, + // @ts-expect-error FIXME: should work + set: param.set || PATH_PARAM_DEFAULT_SET, + } + } + } + + /** + * Match path against the pattern and return + * + * @param path - path to match + * @throws if the patch doesn't match + * @returns matched decoded params + */ + match(path: string): TParams { + const match = path.match(this.re) + if (!match) { + throw miss() + } + let i = 1 // index in match array + const params = {} as TParams + for (const paramName in this.params) { + const currentParam = this.params[paramName] + const currentMatch = match[i++] + let value: string | null | string[] = + this.opts.optional && currentMatch == null ? null : currentMatch + value = this.opts.repeat && value ? value.split('/') : value + + params[paramName as keyof typeof params] = currentParam.get( + // @ts-expect-error: FIXME: the type of currentParam['get'] is wrong + value && (Array.isArray(value) ? value.map(decode) : decode(value)) + ) as (typeof params)[keyof typeof params] + } + + if (__DEV__ && i !== match.length) { + console.warn( + `Regexp matched ${match.length} params, but ${i} params are defined` + ) + } + return params + } + + // build(params: TParams): string { + // let path = this.re.source + // for (const param of this.params) { + // const value = params[param.name as keyof TParams] + // if (value == null) { + // throw new Error(`Matcher build: missing param ${param.name}`) + // } + // path = path.replace( + // /([^\\]|^)\([^?]*\)/, + // `$1${encodeParam(param.set(value))}` + // ) + // } + // return path + // } +} + export interface MatcherPatternQuery< TParams extends MatcherParamsFormatted = MatcherParamsFormatted > extends MatcherPatternParams_Base {} diff --git a/packages/router/src/new-route-resolver/matcher-resolve.spec.ts b/packages/router/src/new-route-resolver/matcher-resolve.spec.ts new file mode 100644 index 000000000..b4799bbec --- /dev/null +++ b/packages/router/src/new-route-resolver/matcher-resolve.spec.ts @@ -0,0 +1,1492 @@ +import { createRouterMatcher, normalizeRouteRecord } from '../matcher' +import { RouteComponent, RouteRecordRaw, MatcherLocation } from '../types' +import { MatcherLocationNormalizedLoose } from '../../__tests__/utils' +import { defineComponent } from 'vue' +import { START_LOCATION_NORMALIZED } from '../location' +import { describe, expect, it } from 'vitest' +import { mockWarn } from '../../__tests__/vitest-mock-warn' +import { + createCompiledMatcher, + MatcherLocationRaw, + MatcherRecordRaw, + NEW_LocationResolved, +} from './matcher' +import { PathParams, tokensToParser } from '../matcher/pathParserRanker' +import { tokenizePath } from '../matcher/pathTokenizer' +import { miss } from './matchers/errors' +import { MatcherPatternPath } from './matcher-pattern' + +// for raw route record +const component: RouteComponent = defineComponent({}) +// for normalized route records +const components = { default: component } + +function compileRouteRecord( + record: RouteRecordRaw, + parentRecord?: RouteRecordRaw +): MatcherRecordRaw { + // we adapt the path to ensure they are absolute + // TODO: aliases? they could be handled directly in the path matcher + const path = record.path.startsWith('/') + ? record.path + : (parentRecord?.path || '') + record.path + record.path = path + const parser = tokensToParser(tokenizePath(record.path), { + // start: true, + end: record.end, + sensitive: record.sensitive, + strict: record.strict, + }) + + return { + name: record.name, + + path: { + match(value) { + const params = parser.parse(value) + if (params) { + return params + } + throw miss() + }, + build(params) { + // TODO: normalize params? + return parser.stringify(params) + }, + } satisfies MatcherPatternPath, + + children: record.children?.map(childRecord => + compileRouteRecord(childRecord, record) + ), + } +} + +describe('RouterMatcher.resolve', () => { + mockWarn() + type Matcher = ReturnType + type MatcherResolvedLocation = ReturnType + + const START_LOCATION: NEW_LocationResolved = { + name: Symbol('START'), + fullPath: '/', + path: '/', + params: {}, + query: {}, + hash: '', + matched: [], + } + + function isMatcherLocationResolved( + location: unknown + ): location is NEW_LocationResolved { + return !!( + location && + typeof location === 'object' && + 'matched' in location && + 'fullPath' in location && + Array.isArray(location.matched) + ) + } + + // TODO: rework with object param for clarity + + function assertRecordMatch( + record: RouteRecordRaw | RouteRecordRaw[], + toLocation: MatcherLocationRaw, + expectedLocation: Partial, + fromLocation: + | NEW_LocationResolved + | Exclude + | `/${string}` = START_LOCATION + ) { + const records = (Array.isArray(record) ? record : [record]).map( + (record): MatcherRecordRaw => compileRouteRecord(record) + ) + const matcher = createCompiledMatcher() + for (const record of records) { + matcher.addRoute(record) + } + + const resolved: MatcherResolvedLocation = { + // FIXME: to add later + // meta: records[0].meta || {}, + path: + typeof toLocation === 'string' ? toLocation : toLocation.path || '/', + name: expect.any(Symbol) as symbol, + matched: [], // FIXME: build up + params: (typeof toLocation === 'object' && toLocation.params) || {}, + ...expectedLocation, + } + + Object.defineProperty(resolved, 'matched', { + writable: true, + configurable: true, + enumerable: false, + value: [], + }) + + fromLocation = isMatcherLocationResolved(fromLocation) + ? fromLocation + : matcher.resolve(fromLocation) + + expect(matcher.resolve(toLocation, fromLocation)).toMatchObject({ + // avoid undesired properties + query: {}, + hash: '', + ...resolved, + }) + } + + function _assertRecordMatch( + record: RouteRecordRaw | RouteRecordRaw[], + location: MatcherLocationRaw, + resolved: Partial, + start: MatcherLocation = START_LOCATION_NORMALIZED + ) { + record = Array.isArray(record) ? record : [record] + const matcher = createRouterMatcher(record, {}) + + if (!('meta' in resolved)) { + resolved.meta = record[0].meta || {} + } + + if (!('name' in resolved)) { + resolved.name = undefined + } + + // add location if provided as it should be the same value + if ('path' in location && !('path' in resolved)) { + resolved.path = location.path + } + + if ('redirect' in record) { + throw new Error('not handled') + } else { + // use one single record + if (!resolved.matched) resolved.matched = record.map(normalizeRouteRecord) + // allow passing an expect.any(Array) + else if (Array.isArray(resolved.matched)) + resolved.matched = resolved.matched.map(m => ({ + ...normalizeRouteRecord(m as any), + aliasOf: m.aliasOf, + })) + } + + // allows not passing params + resolved.params = + resolved.params || ('params' in location ? location.params : {}) + + const startCopy: MatcherLocation = { + ...start, + matched: start.matched.map(m => ({ + ...normalizeRouteRecord(m), + aliasOf: m.aliasOf, + })) as MatcherLocation['matched'], + } + + // make matched non enumerable + Object.defineProperty(startCopy, 'matched', { enumerable: false }) + + const result = matcher.resolve(location, startCopy) + expect(result).toEqual(resolved) + } + + /** + * + * @param record - Record or records we are testing the matcher against + * @param location - location we want to resolve against + * @param [start] Optional currentLocation used when resolving + * @returns error + */ + function assertErrorMatch( + record: RouteRecordRaw | RouteRecordRaw[], + location: MatcherLocationRaw, + start: MatcherLocation = START_LOCATION_NORMALIZED + ) { + assertRecordMatch(record, location, {}, start) + } + + describe.skip('LocationAsPath', () => { + it('resolves a normal path', () => { + assertRecordMatch({ path: '/', name: 'Home', components }, '/', { + name: 'Home', + path: '/', + params: {}, + }) + }) + + it('resolves a normal path without name', () => { + assertRecordMatch( + { path: '/', components }, + { path: '/' }, + { name: undefined, path: '/', params: {} } + ) + }) + + it('resolves a path with params', () => { + assertRecordMatch( + { path: '/users/:id', name: 'User', components }, + { path: '/users/posva' }, + { name: 'User', params: { id: 'posva' } } + ) + }) + + it('resolves an array of params for a repeatable params', () => { + assertRecordMatch( + { path: '/a/:p+', name: 'a', components }, + { name: 'a', params: { p: ['b', 'c', 'd'] } }, + { name: 'a', path: '/a/b/c/d', params: { p: ['b', 'c', 'd'] } } + ) + }) + + it('resolves single params for a repeatable params', () => { + assertRecordMatch( + { path: '/a/:p+', name: 'a', components }, + { name: 'a', params: { p: 'b' } }, + { name: 'a', path: '/a/b', params: { p: 'b' } } + ) + }) + + it('keeps repeated params as a single one when provided through path', () => { + assertRecordMatch( + { path: '/a/:p+', name: 'a', components }, + { path: '/a/b/c' }, + { name: 'a', params: { p: ['b', 'c'] } } + ) + }) + + it('resolves a path with multiple params', () => { + assertRecordMatch( + { path: '/users/:id/:other', name: 'User', components }, + { path: '/users/posva/hey' }, + { name: 'User', params: { id: 'posva', other: 'hey' } } + ) + }) + + it('resolves a path with multiple params but no name', () => { + assertRecordMatch( + { path: '/users/:id/:other', components }, + { path: '/users/posva/hey' }, + { name: undefined, params: { id: 'posva', other: 'hey' } } + ) + }) + + it('returns an empty match when the path does not exist', () => { + assertRecordMatch( + { path: '/', components }, + { path: '/foo' }, + { name: undefined, params: {}, path: '/foo', matched: [] } + ) + }) + + it('allows an optional trailing slash', () => { + assertRecordMatch( + { path: '/home/', name: 'Home', components }, + { path: '/home/' }, + { name: 'Home', path: '/home/', matched: expect.any(Array) } + ) + }) + + it('allows an optional trailing slash with optional param', () => { + assertRecordMatch( + { path: '/:a', components, name: 'a' }, + { path: '/a/' }, + { path: '/a/', params: { a: 'a' }, name: 'a' } + ) + assertRecordMatch( + { path: '/a/:a', components, name: 'a' }, + { path: '/a/a/' }, + { path: '/a/a/', params: { a: 'a' }, name: 'a' } + ) + }) + + it('allows an optional trailing slash with missing optional param', () => { + assertRecordMatch( + { path: '/:a?', components, name: 'a' }, + { path: '/' }, + { path: '/', params: { a: '' }, name: 'a' } + ) + assertRecordMatch( + { path: '/a/:a?', components, name: 'a' }, + { path: '/a/' }, + { path: '/a/', params: { a: '' }, name: 'a' } + ) + }) + + it('keeps required trailing slash (strict: true)', () => { + const record = { + path: '/home/', + name: 'Home', + components, + options: { strict: true }, + } + assertErrorMatch(record, { path: '/home' }) + assertRecordMatch( + record, + { path: '/home/' }, + { name: 'Home', path: '/home/', matched: expect.any(Array) } + ) + }) + + it('rejects a trailing slash when strict', () => { + const record = { + path: '/home', + name: 'Home', + components, + options: { strict: true }, + } + assertRecordMatch( + record, + { path: '/home' }, + { name: 'Home', path: '/home', matched: expect.any(Array) } + ) + assertErrorMatch(record, { path: '/home/' }) + }) + }) + + describe('LocationAsName', () => { + it('matches a name', () => { + assertRecordMatch( + { path: '/home', name: 'Home', components }, + // TODO: allow a name only without the params? + { name: 'Home', params: {} }, + { name: 'Home', path: '/home' } + ) + }) + + it('matches a name and fill params', () => { + assertRecordMatch( + { path: '/users/:id/m/:role', name: 'UserEdit', components }, + { name: 'UserEdit', params: { id: 'posva', role: 'admin' } }, + { + name: 'UserEdit', + path: '/users/posva/m/admin', + params: { id: 'posva', role: 'admin' }, + } + ) + }) + + it('throws if the named route does not exists', () => { + expect(() => + assertErrorMatch( + { path: '/', components }, + { name: 'Home', params: {} } + ) + ).toThrowError('Matcher "Home" not found') + }) + + it('merges params', () => { + assertRecordMatch( + { path: '/:a/:b', name: 'p', components }, + { params: { b: 'b' } }, + { name: 'p', path: '/A/b', params: { a: 'A', b: 'b' } }, + '/A/B' + ) + }) + + // TODO: new matcher no longer allows implicit param merging + it.todo('only keep existing params', () => { + assertRecordMatch( + { path: '/:a/:b', name: 'p', components }, + { name: 'p', params: { b: 'b' } }, + { name: 'p', path: '/a/b', params: { a: 'a', b: 'b' } }, + '/a/c' + ) + }) + + // TODO: implement parent children + it.todo('keep optional params from parent record', () => { + const Child_A = { path: 'a', name: 'child_a', components } + const Child_B = { path: 'b', name: 'child_b', components } + const Parent = { + path: '/:optional?/parent', + name: 'parent', + components, + children: [Child_A, Child_B], + } + assertRecordMatch( + Parent, + { name: 'child_b' }, + { + name: 'child_b', + path: '/foo/parent/b', + params: { optional: 'foo' }, + matched: [ + Parent as any, + { + ...Child_B, + path: `${Parent.path}/${Child_B.path}`, + }, + ], + }, + { + params: { optional: 'foo' }, + path: '/foo/parent/a', + matched: [], + meta: {}, + name: undefined, + } + ) + }) + + // TODO: check if needed by the active matching, if not just test that the param is dropped + it.todo('discards non existent params', () => { + assertRecordMatch( + { path: '/', name: 'home', components }, + { name: 'home', params: { a: 'a', b: 'b' } }, + { name: 'home', path: '/', params: {} } + ) + expect('invalid param(s) "a", "b" ').toHaveBeenWarned() + assertRecordMatch( + { path: '/:b', name: 'a', components }, + { name: 'a', params: { a: 'a', b: 'b' } }, + { name: 'a', path: '/b', params: { b: 'b' } } + ) + expect('invalid param(s) "a"').toHaveBeenWarned() + }) + + it('drops optional params in absolute location', () => { + assertRecordMatch( + { path: '/:a/:b?', name: 'p', components }, + { name: 'p', params: { a: 'b' } }, + { name: 'p', path: '/b', params: { a: 'b' } } + ) + }) + + it('keeps optional params passed as empty strings', () => { + assertRecordMatch( + { path: '/:a/:b?', name: 'p', components }, + { name: 'p', params: { a: 'b', b: '' } }, + { name: 'p', path: '/b', params: { a: 'b', b: '' } } + ) + }) + + it('resolves root path with optional params', () => { + assertRecordMatch( + { path: '/:tab?', name: 'h', components }, + { name: 'h', params: {} }, + { name: 'h', path: '/', params: {} } + ) + assertRecordMatch( + { path: '/:tab?/:other?', name: 'h', components }, + { name: 'h', params: {} }, + { name: 'h', path: '/', params: {} } + ) + }) + }) + + describe.skip('LocationAsRelative', () => { + it('warns if a path isn not absolute', () => { + const record = { + path: '/parent', + components, + } + const matcher = createRouterMatcher([record], {}) + matcher.resolve( + { path: 'two' }, + { + path: '/parent/one', + name: undefined, + params: {}, + matched: [] as any, + meta: {}, + } + ) + expect('received "two"').toHaveBeenWarned() + }) + + it('matches with nothing', () => { + const record = { path: '/home', name: 'Home', components } + assertRecordMatch( + record, + {}, + { name: 'Home', path: '/home' }, + { + name: 'Home', + params: {}, + path: '/home', + matched: [record] as any, + meta: {}, + } + ) + }) + + it('replace params even with no name', () => { + const record = { path: '/users/:id/m/:role', components } + assertRecordMatch( + record, + { params: { id: 'posva', role: 'admin' } }, + { name: undefined, path: '/users/posva/m/admin' }, + { + path: '/users/ed/m/user', + name: undefined, + params: { id: 'ed', role: 'user' }, + matched: [record] as any, + meta: {}, + } + ) + }) + + it('replace params', () => { + const record = { + path: '/users/:id/m/:role', + name: 'UserEdit', + components, + } + assertRecordMatch( + record, + { params: { id: 'posva', role: 'admin' } }, + { name: 'UserEdit', path: '/users/posva/m/admin' }, + { + path: '/users/ed/m/user', + name: 'UserEdit', + params: { id: 'ed', role: 'user' }, + matched: [], + meta: {}, + } + ) + }) + + it('keep params if not provided', () => { + const record = { + path: '/users/:id/m/:role', + name: 'UserEdit', + components, + } + assertRecordMatch( + record, + {}, + { + name: 'UserEdit', + path: '/users/ed/m/user', + params: { id: 'ed', role: 'user' }, + }, + { + path: '/users/ed/m/user', + name: 'UserEdit', + params: { id: 'ed', role: 'user' }, + matched: [record] as any, + meta: {}, + } + ) + }) + + it('keep params if not provided even with no name', () => { + const record = { path: '/users/:id/m/:role', components } + assertRecordMatch( + record, + {}, + { + name: undefined, + path: '/users/ed/m/user', + params: { id: 'ed', role: 'user' }, + }, + { + path: '/users/ed/m/user', + name: undefined, + params: { id: 'ed', role: 'user' }, + matched: [record] as any, + meta: {}, + } + ) + }) + + it('merges params', () => { + assertRecordMatch( + { path: '/:a/:b?', name: 'p', components }, + { params: { b: 'b' } }, + { name: 'p', path: '/a/b', params: { a: 'a', b: 'b' } }, + { + name: 'p', + params: { a: 'a' }, + path: '/a', + matched: [], + meta: {}, + } + ) + }) + + it('keep optional params', () => { + assertRecordMatch( + { path: '/:a/:b?', name: 'p', components }, + {}, + { name: 'p', path: '/a/b', params: { a: 'a', b: 'b' } }, + { + name: 'p', + params: { a: 'a', b: 'b' }, + path: '/a/b', + matched: [], + meta: {}, + } + ) + }) + + it('merges optional params', () => { + assertRecordMatch( + { path: '/:a/:b?', name: 'p', components }, + { params: { a: 'c' } }, + { name: 'p', path: '/c/b', params: { a: 'c', b: 'b' } }, + { + name: 'p', + params: { a: 'a', b: 'b' }, + path: '/a/b', + matched: [], + meta: {}, + } + ) + }) + + it('throws if the current named route does not exists', () => { + const record = { path: '/', components } + const start = { + name: 'home', + params: {}, + path: '/', + matched: [record], + } + // the property should be non enumerable + Object.defineProperty(start, 'matched', { enumerable: false }) + expect( + assertErrorMatch( + record, + { params: { a: 'foo' } }, + { + ...start, + matched: start.matched.map(normalizeRouteRecord), + meta: {}, + } + ) + ).toMatchSnapshot() + }) + + it('avoids records with children without a component nor name', () => { + assertErrorMatch( + { + path: '/articles', + children: [{ path: ':id', components }], + }, + { path: '/articles' } + ) + }) + + it('avoid deeply nested records with children without a component nor name', () => { + assertErrorMatch( + { + path: '/app', + components, + children: [ + { + path: '/articles', + children: [{ path: ':id', components }], + }, + ], + }, + { path: '/articles' } + ) + }) + + it('can reach a named route with children and no component if named', () => { + assertRecordMatch( + { + path: '/articles', + name: 'ArticlesParent', + children: [{ path: ':id', components }], + }, + { name: 'ArticlesParent' }, + { name: 'ArticlesParent', path: '/articles' } + ) + }) + }) + + describe.skip('alias', () => { + it('resolves an alias', () => { + assertRecordMatch( + { + path: '/', + alias: '/home', + name: 'Home', + components, + meta: { foo: true }, + }, + { path: '/home' }, + { + name: 'Home', + path: '/home', + params: {}, + meta: { foo: true }, + matched: [ + { + path: '/home', + name: 'Home', + components, + aliasOf: expect.objectContaining({ name: 'Home', path: '/' }), + meta: { foo: true }, + }, + ], + } + ) + }) + + it('multiple aliases', () => { + const record = { + path: '/', + alias: ['/home', '/start'], + name: 'Home', + components, + meta: { foo: true }, + } + + assertRecordMatch( + record, + { path: '/' }, + { + name: 'Home', + path: '/', + params: {}, + meta: { foo: true }, + matched: [ + { + path: '/', + name: 'Home', + components, + aliasOf: undefined, + meta: { foo: true }, + }, + ], + } + ) + assertRecordMatch( + record, + { path: '/home' }, + { + name: 'Home', + path: '/home', + params: {}, + meta: { foo: true }, + matched: [ + { + path: '/home', + name: 'Home', + components, + aliasOf: expect.objectContaining({ name: 'Home', path: '/' }), + meta: { foo: true }, + }, + ], + } + ) + assertRecordMatch( + record, + { path: '/start' }, + { + name: 'Home', + path: '/start', + params: {}, + meta: { foo: true }, + matched: [ + { + path: '/start', + name: 'Home', + components, + aliasOf: expect.objectContaining({ name: 'Home', path: '/' }), + meta: { foo: true }, + }, + ], + } + ) + }) + + it('resolves the original record by name', () => { + assertRecordMatch( + { + path: '/', + alias: '/home', + name: 'Home', + components, + meta: { foo: true }, + }, + { name: 'Home' }, + { + name: 'Home', + path: '/', + params: {}, + meta: { foo: true }, + matched: [ + { + path: '/', + name: 'Home', + components, + aliasOf: undefined, + meta: { foo: true }, + }, + ], + } + ) + }) + + it('resolves an alias with children to the alias when using the path', () => { + const children = [{ path: 'one', component, name: 'nested' }] + assertRecordMatch( + { + path: '/parent', + alias: '/p', + component, + children, + }, + { path: '/p/one' }, + { + path: '/p/one', + name: 'nested', + params: {}, + matched: [ + { + path: '/p', + children, + components, + aliasOf: expect.objectContaining({ path: '/parent' }), + }, + { + path: '/p/one', + name: 'nested', + components, + aliasOf: expect.objectContaining({ path: '/parent/one' }), + }, + ], + } + ) + }) + + describe('nested aliases', () => { + const children = [ + { + path: 'one', + component, + name: 'nested', + alias: 'o', + children: [ + { path: 'two', alias: 't', name: 'nestednested', component }, + ], + }, + { + path: 'other', + alias: 'otherAlias', + component, + name: 'other', + }, + ] + const record = { + path: '/parent', + name: 'parent', + alias: '/p', + component, + children, + } + + it('resolves the parent as an alias', () => { + assertRecordMatch( + record, + { path: '/p' }, + expect.objectContaining({ + path: '/p', + name: 'parent', + matched: [ + expect.objectContaining({ + path: '/p', + aliasOf: expect.objectContaining({ path: '/parent' }), + }), + ], + }) + ) + }) + + describe('multiple children', () => { + // tests concerning the /parent/other path and its aliases + + it('resolves the alias parent', () => { + assertRecordMatch( + record, + { path: '/p/other' }, + expect.objectContaining({ + path: '/p/other', + name: 'other', + matched: [ + expect.objectContaining({ + path: '/p', + aliasOf: expect.objectContaining({ path: '/parent' }), + }), + expect.objectContaining({ + path: '/p/other', + aliasOf: expect.objectContaining({ path: '/parent/other' }), + }), + ], + }) + ) + }) + + it('resolves the alias child', () => { + assertRecordMatch( + record, + { path: '/parent/otherAlias' }, + expect.objectContaining({ + path: '/parent/otherAlias', + name: 'other', + matched: [ + expect.objectContaining({ + path: '/parent', + aliasOf: undefined, + }), + expect.objectContaining({ + path: '/parent/otherAlias', + aliasOf: expect.objectContaining({ path: '/parent/other' }), + }), + ], + }) + ) + }) + + it('resolves the alias parent and child', () => { + assertRecordMatch( + record, + { path: '/p/otherAlias' }, + expect.objectContaining({ + path: '/p/otherAlias', + name: 'other', + matched: [ + expect.objectContaining({ + path: '/p', + aliasOf: expect.objectContaining({ path: '/parent' }), + }), + expect.objectContaining({ + path: '/p/otherAlias', + aliasOf: expect.objectContaining({ path: '/parent/other' }), + }), + ], + }) + ) + }) + }) + + it('resolves the original one with no aliases', () => { + assertRecordMatch( + record, + { path: '/parent/one/two' }, + expect.objectContaining({ + path: '/parent/one/two', + name: 'nestednested', + matched: [ + expect.objectContaining({ + path: '/parent', + aliasOf: undefined, + }), + expect.objectContaining({ + path: '/parent/one', + aliasOf: undefined, + }), + expect.objectContaining({ + path: '/parent/one/two', + aliasOf: undefined, + }), + ], + }) + ) + }) + + it.todo('resolves when parent is an alias and child has an absolute path') + + it('resolves when parent is an alias', () => { + assertRecordMatch( + record, + { path: '/p/one/two' }, + expect.objectContaining({ + path: '/p/one/two', + name: 'nestednested', + matched: [ + expect.objectContaining({ + path: '/p', + aliasOf: expect.objectContaining({ path: '/parent' }), + }), + expect.objectContaining({ + path: '/p/one', + aliasOf: expect.objectContaining({ path: '/parent/one' }), + }), + expect.objectContaining({ + path: '/p/one/two', + aliasOf: expect.objectContaining({ path: '/parent/one/two' }), + }), + ], + }) + ) + }) + + it('resolves a different child when parent is an alias', () => { + assertRecordMatch( + record, + { path: '/p/other' }, + expect.objectContaining({ + path: '/p/other', + name: 'other', + matched: [ + expect.objectContaining({ + path: '/p', + aliasOf: expect.objectContaining({ path: '/parent' }), + }), + expect.objectContaining({ + path: '/p/other', + aliasOf: expect.objectContaining({ path: '/parent/other' }), + }), + ], + }) + ) + }) + + it('resolves when the first child is an alias', () => { + assertRecordMatch( + record, + { path: '/parent/o/two' }, + expect.objectContaining({ + path: '/parent/o/two', + name: 'nestednested', + matched: [ + expect.objectContaining({ + path: '/parent', + aliasOf: undefined, + }), + expect.objectContaining({ + path: '/parent/o', + aliasOf: expect.objectContaining({ path: '/parent/one' }), + }), + expect.objectContaining({ + path: '/parent/o/two', + aliasOf: expect.objectContaining({ path: '/parent/one/two' }), + }), + ], + }) + ) + }) + + it('resolves when the second child is an alias', () => { + assertRecordMatch( + record, + { path: '/parent/one/t' }, + expect.objectContaining({ + path: '/parent/one/t', + name: 'nestednested', + matched: [ + expect.objectContaining({ + path: '/parent', + aliasOf: undefined, + }), + expect.objectContaining({ + path: '/parent/one', + aliasOf: undefined, + }), + expect.objectContaining({ + path: '/parent/one/t', + aliasOf: expect.objectContaining({ path: '/parent/one/two' }), + }), + ], + }) + ) + }) + + it('resolves when the two last children are aliases', () => { + assertRecordMatch( + record, + { path: '/parent/o/t' }, + expect.objectContaining({ + path: '/parent/o/t', + name: 'nestednested', + matched: [ + expect.objectContaining({ + path: '/parent', + aliasOf: undefined, + }), + expect.objectContaining({ + path: '/parent/o', + aliasOf: expect.objectContaining({ path: '/parent/one' }), + }), + expect.objectContaining({ + path: '/parent/o/t', + aliasOf: expect.objectContaining({ path: '/parent/one/two' }), + }), + ], + }) + ) + }) + + it('resolves when all are aliases', () => { + assertRecordMatch( + record, + { path: '/p/o/t' }, + expect.objectContaining({ + path: '/p/o/t', + name: 'nestednested', + matched: [ + expect.objectContaining({ + path: '/p', + aliasOf: expect.objectContaining({ path: '/parent' }), + }), + expect.objectContaining({ + path: '/p/o', + aliasOf: expect.objectContaining({ path: '/parent/one' }), + }), + expect.objectContaining({ + path: '/p/o/t', + aliasOf: expect.objectContaining({ path: '/parent/one/two' }), + }), + ], + }) + ) + }) + + it('resolves when first and last are aliases', () => { + assertRecordMatch( + record, + { path: '/p/one/t' }, + expect.objectContaining({ + path: '/p/one/t', + name: 'nestednested', + matched: [ + expect.objectContaining({ + path: '/p', + aliasOf: expect.objectContaining({ path: '/parent' }), + }), + expect.objectContaining({ + path: '/p/one', + aliasOf: expect.objectContaining({ path: '/parent/one' }), + }), + expect.objectContaining({ + path: '/p/one/t', + aliasOf: expect.objectContaining({ path: '/parent/one/two' }), + }), + ], + }) + ) + }) + }) + + it('resolves the original path of the named children of a route with an alias', () => { + const children = [{ path: 'one', component, name: 'nested' }] + assertRecordMatch( + { + path: '/parent', + alias: '/p', + component, + children, + }, + { name: 'nested' }, + { + path: '/parent/one', + name: 'nested', + params: {}, + matched: [ + { + path: '/parent', + children, + components, + aliasOf: undefined, + }, + { path: '/parent/one', name: 'nested', components }, + ], + } + ) + }) + }) + + describe.skip('children', () => { + const ChildA = { path: 'a', name: 'child-a', components } + const ChildB = { path: 'b', name: 'child-b', components } + const ChildC = { path: 'c', name: 'child-c', components } + const ChildD = { path: '/absolute', name: 'absolute', components } + const ChildWithParam = { path: ':p', name: 'child-params', components } + const NestedChildWithParam = { + ...ChildWithParam, + name: 'nested-child-params', + } + const NestedChildA = { ...ChildA, name: 'nested-child-a' } + const NestedChildB = { ...ChildB, name: 'nested-child-b' } + const NestedChildC = { ...ChildC, name: 'nested-child-c' } + const Nested = { + path: 'nested', + name: 'nested', + components, + children: [NestedChildA, NestedChildB, NestedChildC], + } + const NestedWithParam = { + path: 'nested/:n', + name: 'nested', + components, + children: [NestedChildWithParam], + } + + it('resolves children', () => { + const Foo = { + path: '/foo', + name: 'Foo', + components, + children: [ChildA, ChildB, ChildC], + } + assertRecordMatch( + Foo, + { path: '/foo/b' }, + { + name: 'child-b', + path: '/foo/b', + params: {}, + matched: [Foo, { ...ChildB, path: `${Foo.path}/${ChildB.path}` }], + } + ) + }) + + it('resolves children with empty paths', () => { + const Nested = { path: '', name: 'nested', components } + const Foo = { + path: '/foo', + name: 'Foo', + components, + children: [Nested], + } + assertRecordMatch( + Foo, + { path: '/foo' }, + { + name: 'nested', + path: '/foo', + params: {}, + matched: [Foo as any, { ...Nested, path: `${Foo.path}` }], + } + ) + }) + + it('resolves nested children with empty paths', () => { + const NestedNested = { path: '', name: 'nested', components } + const Nested = { + path: '', + name: 'nested-nested', + components, + children: [NestedNested], + } + const Foo = { + path: '/foo', + name: 'Foo', + components, + children: [Nested], + } + assertRecordMatch( + Foo, + { path: '/foo' }, + { + name: 'nested', + path: '/foo', + params: {}, + matched: [ + Foo as any, + { ...Nested, path: `${Foo.path}` }, + { ...NestedNested, path: `${Foo.path}` }, + ], + } + ) + }) + + it('resolves nested children', () => { + const Foo = { + path: '/foo', + name: 'Foo', + components, + children: [Nested], + } + assertRecordMatch( + Foo, + { path: '/foo/nested/a' }, + { + name: 'nested-child-a', + path: '/foo/nested/a', + params: {}, + matched: [ + Foo as any, + { ...Nested, path: `${Foo.path}/${Nested.path}` }, + { + ...NestedChildA, + path: `${Foo.path}/${Nested.path}/${NestedChildA.path}`, + }, + ], + } + ) + }) + + it('resolves nested children with named location', () => { + const Foo = { + path: '/foo', + name: 'Foo', + components, + children: [Nested], + } + assertRecordMatch( + Foo, + { name: 'nested-child-a' }, + { + name: 'nested-child-a', + path: '/foo/nested/a', + params: {}, + matched: [ + Foo as any, + { ...Nested, path: `${Foo.path}/${Nested.path}` }, + { + ...NestedChildA, + path: `${Foo.path}/${Nested.path}/${NestedChildA.path}`, + }, + ], + } + ) + }) + + it('resolves nested children with relative location', () => { + const Foo = { + path: '/foo', + name: 'Foo', + components, + children: [Nested], + } + assertRecordMatch( + Foo, + {}, + { + name: 'nested-child-a', + path: '/foo/nested/a', + params: {}, + matched: [ + Foo as any, + { ...Nested, path: `${Foo.path}/${Nested.path}` }, + { + ...NestedChildA, + path: `${Foo.path}/${Nested.path}/${NestedChildA.path}`, + }, + ], + }, + { + name: 'nested-child-a', + matched: [], + params: {}, + path: '/foo/nested/a', + meta: {}, + } + ) + }) + + it('resolves nested children with params', () => { + const Foo = { + path: '/foo', + name: 'Foo', + components, + children: [NestedWithParam], + } + assertRecordMatch( + Foo, + { path: '/foo/nested/a/b' }, + { + name: 'nested-child-params', + path: '/foo/nested/a/b', + params: { p: 'b', n: 'a' }, + matched: [ + Foo as any, + { + ...NestedWithParam, + path: `${Foo.path}/${NestedWithParam.path}`, + }, + { + ...NestedChildWithParam, + path: `${Foo.path}/${NestedWithParam.path}/${NestedChildWithParam.path}`, + }, + ], + } + ) + }) + + it('resolves nested children with params with named location', () => { + const Foo = { + path: '/foo', + name: 'Foo', + components, + children: [NestedWithParam], + } + assertRecordMatch( + Foo, + { name: 'nested-child-params', params: { p: 'a', n: 'b' } }, + { + name: 'nested-child-params', + path: '/foo/nested/b/a', + params: { p: 'a', n: 'b' }, + matched: [ + Foo as any, + { + ...NestedWithParam, + path: `${Foo.path}/${NestedWithParam.path}`, + }, + { + ...NestedChildWithParam, + path: `${Foo.path}/${NestedWithParam.path}/${NestedChildWithParam.path}`, + }, + ], + } + ) + }) + + it('resolves absolute path children', () => { + const Foo = { + path: '/foo', + name: 'Foo', + components, + children: [ChildA, ChildD], + } + assertRecordMatch( + Foo, + { path: '/absolute' }, + { + name: 'absolute', + path: '/absolute', + params: {}, + matched: [Foo, ChildD], + } + ) + }) + + it('resolves children with root as the parent', () => { + const Nested = { path: 'nested', name: 'nested', components } + const Parent = { + path: '/', + name: 'parent', + components, + children: [Nested], + } + assertRecordMatch( + Parent, + { path: '/nested' }, + { + name: 'nested', + path: '/nested', + params: {}, + matched: [Parent as any, { ...Nested, path: `/nested` }], + } + ) + }) + + it('resolves children with parent with trailing slash', () => { + const Nested = { path: 'nested', name: 'nested', components } + const Parent = { + path: '/parent/', + name: 'parent', + components, + children: [Nested], + } + assertRecordMatch( + Parent, + { path: '/parent/nested' }, + { + name: 'nested', + path: '/parent/nested', + params: {}, + matched: [Parent as any, { ...Nested, path: `/parent/nested` }], + } + ) + }) + }) +}) diff --git a/packages/router/src/new-route-resolver/matcher.spec.ts b/packages/router/src/new-route-resolver/matcher.spec.ts index c15561f53..f508fe113 100644 --- a/packages/router/src/new-route-resolver/matcher.spec.ts +++ b/packages/router/src/new-route-resolver/matcher.spec.ts @@ -1,10 +1,17 @@ import { describe, expect, it } from 'vitest' -import { createCompiledMatcher, NO_MATCH_LOCATION } from './matcher' +import { + createCompiledMatcher, + NO_MATCH_LOCATION, + pathEncoded, +} from './matcher' import { MatcherPatternParams_Base, MatcherPattern, MatcherPatternPath, MatcherPatternQuery, + MatcherPatternPathStatic, + MatcherPatternPathDynamic, + defineParamParser, } from './matcher-pattern' import { miss } from './matchers/errors' import { EmptyParams } from './matcher-location' @@ -73,7 +80,52 @@ const USER_ID_ROUTE = { path: USER_ID_PATH_PATTERN_MATCHER, } satisfies MatcherPattern -describe('Matcher', () => { +describe('RouterMatcher', () => { + describe('new matchers', () => { + it('static path', () => { + const matcher = createCompiledMatcher([ + { path: new MatcherPatternPathStatic('/') }, + { path: new MatcherPatternPathStatic('/users') }, + ]) + + expect(matcher.resolve('/')).toMatchObject({ + fullPath: '/', + path: '/', + params: {}, + query: {}, + hash: '', + }) + + expect(matcher.resolve('/users')).toMatchObject({ + fullPath: '/users', + path: '/users', + params: {}, + query: {}, + hash: '', + }) + }) + + it('dynamic path', () => { + const matcher = createCompiledMatcher([ + { + path: new MatcherPatternPathDynamic<{ id: string }>( + /^\/users\/([^\/]+)$/, + { + id: {}, + }, + ({ id }) => pathEncoded`/users/${id}` + ), + }, + ]) + + expect(matcher.resolve('/users/1')).toMatchObject({ + fullPath: '/users/1', + path: '/users/1', + params: { id: '1' }, + }) + }) + }) + describe('adding and removing', () => { it('add static path', () => { const matcher = createCompiledMatcher() @@ -87,10 +139,9 @@ describe('Matcher', () => { }) describe('resolve()', () => { - describe('absolute locationss as strings', () => { + describe('absolute locations as strings', () => { it('resolves string locations with no params', () => { - const matcher = createCompiledMatcher() - matcher.addRoute(EMPTY_PATH_ROUTE) + const matcher = createCompiledMatcher([EMPTY_PATH_ROUTE]) expect(matcher.resolve('/?a=a&b=b#h')).toMatchObject({ path: '/', @@ -113,8 +164,7 @@ describe('Matcher', () => { }) it('resolves string locations with params', () => { - const matcher = createCompiledMatcher() - matcher.addRoute(USER_ID_ROUTE) + const matcher = createCompiledMatcher([USER_ID_ROUTE]) expect(matcher.resolve('/users/1?a=a&b=b#h')).toMatchObject({ path: '/users/1', @@ -131,11 +181,12 @@ describe('Matcher', () => { }) it('resolve string locations with query', () => { - const matcher = createCompiledMatcher() - matcher.addRoute({ - path: ANY_PATH_PATTERN_MATCHER, - query: PAGE_QUERY_PATTERN_MATCHER, - }) + const matcher = createCompiledMatcher([ + { + path: ANY_PATH_PATTERN_MATCHER, + query: PAGE_QUERY_PATTERN_MATCHER, + }, + ]) expect(matcher.resolve('/foo?page=100&b=b#h')).toMatchObject({ params: { page: 100 }, @@ -149,11 +200,12 @@ describe('Matcher', () => { }) it('resolves string locations with hash', () => { - const matcher = createCompiledMatcher() - matcher.addRoute({ - path: ANY_PATH_PATTERN_MATCHER, - hash: ANY_HASH_PATTERN_MATCHER, - }) + const matcher = createCompiledMatcher([ + { + path: ANY_PATH_PATTERN_MATCHER, + hash: ANY_HASH_PATTERN_MATCHER, + }, + ]) expect(matcher.resolve('/foo?a=a&b=b#bar')).toMatchObject({ hash: '#bar', @@ -164,12 +216,13 @@ describe('Matcher', () => { }) it('combines path, query and hash params', () => { - const matcher = createCompiledMatcher() - matcher.addRoute({ - path: USER_ID_PATH_PATTERN_MATCHER, - query: PAGE_QUERY_PATTERN_MATCHER, - hash: ANY_HASH_PATTERN_MATCHER, - }) + const matcher = createCompiledMatcher([ + { + path: USER_ID_PATH_PATTERN_MATCHER, + query: PAGE_QUERY_PATTERN_MATCHER, + hash: ANY_HASH_PATTERN_MATCHER, + }, + ]) expect(matcher.resolve('/users/24?page=100#bar')).toMatchObject({ params: { id: 24, page: 100, hash: 'bar' }, @@ -179,8 +232,9 @@ describe('Matcher', () => { describe('relative locations as strings', () => { it('resolves a simple relative location', () => { - const matcher = createCompiledMatcher() - matcher.addRoute({ path: ANY_PATH_PATTERN_MATCHER }) + const matcher = createCompiledMatcher([ + { path: ANY_PATH_PATTERN_MATCHER }, + ]) expect( matcher.resolve('foo', matcher.resolve('/nested/')) @@ -211,8 +265,7 @@ describe('Matcher', () => { describe('absolute locations as objects', () => { it('resolves an object location', () => { - const matcher = createCompiledMatcher() - matcher.addRoute(EMPTY_PATH_ROUTE) + const matcher = createCompiledMatcher([EMPTY_PATH_ROUTE]) expect(matcher.resolve({ path: '/' })).toMatchObject({ fullPath: '/', path: '/', @@ -225,11 +278,12 @@ describe('Matcher', () => { describe('named locations', () => { it('resolves named locations with no params', () => { - const matcher = createCompiledMatcher() - matcher.addRoute({ - name: 'home', - path: EMPTY_PATH_PATTERN_MATCHER, - }) + const matcher = createCompiledMatcher([ + { + name: 'home', + path: EMPTY_PATH_PATTERN_MATCHER, + }, + ]) expect(matcher.resolve({ name: 'home', params: {} })).toMatchObject({ name: 'home', diff --git a/packages/router/src/new-route-resolver/matcher.ts b/packages/router/src/new-route-resolver/matcher.ts index f9fa1c6f8..cabb296ef 100644 --- a/packages/router/src/new-route-resolver/matcher.ts +++ b/packages/router/src/new-route-resolver/matcher.ts @@ -11,7 +11,7 @@ import type { MatcherPatternQuery, } from './matcher-pattern' import { warn } from '../warning' -import { encodeQueryValue as _encodeQueryValue } from '../encoding' +import { encodeQueryValue as _encodeQueryValue, encodeParam } from '../encoding' import { parseURL, stringifyURL } from '../location' import type { MatcherLocationAsNamed, @@ -102,6 +102,17 @@ type MatcherResolveArgs = currentLocation: NEW_LocationResolved ] +/** + * Allowed location objects to be passed to {@link RouteResolver['resolve']} + */ +export type MatcherLocationRaw = + | `/${string}` + | string + | MatcherLocationAsNamed + | MatcherLocationAsPathAbsolute + | MatcherLocationAsPathRelative + | MatcherLocationAsRelative + /** * Matcher capable of adding and removing routes at runtime. */ @@ -230,6 +241,28 @@ export interface MatcherRecordRaw { children?: MatcherRecordRaw[] } +/** + * Tagged template helper to encode params into a path. Doesn't work with null + */ +export function pathEncoded( + parts: TemplateStringsArray, + ...params: Array +): string { + return parts.reduce((result, part, i) => { + return ( + result + + part + + (Array.isArray(params[i]) + ? params[i].map(encodeParam).join('/') + : encodeParam(params[i])) + ) + }) +} + +// pathEncoded`/users/${1}` +// TODO: +// pathEncoded`/users/${null}/end` + // const a: RouteRecordRaw = {} as any /** @@ -245,10 +278,9 @@ function buildMatched(record: MatcherPattern): MatcherPattern[] { return matched } -export function createCompiledMatcher(): RouteResolver< - MatcherRecordRaw, - MatcherPattern -> { +export function createCompiledMatcher( + records: MatcherRecordRaw[] = [] +): RouteResolver { // TODO: we also need an array that has the correct order const matchers = new Map() @@ -386,6 +418,10 @@ export function createCompiledMatcher(): RouteResolver< return normalizedRecord } + for (const record of records) { + addRoute(record) + } + function removeRoute(matcher: MatcherPattern) { matchers.delete(matcher.name) // TODO: delete children and aliases diff --git a/packages/router/src/new-route-resolver/matchers/test-utils.ts b/packages/router/src/new-route-resolver/matchers/test-utils.ts new file mode 100644 index 000000000..f40ce00a5 --- /dev/null +++ b/packages/router/src/new-route-resolver/matchers/test-utils.ts @@ -0,0 +1,76 @@ +import { EmptyParams } from '../matcher-location' +import { + MatcherPatternPath, + MatcherPatternQuery, + MatcherPatternParams_Base, + MatcherPattern, +} from '../matcher-pattern' +import { miss } from './errors' + +export const ANY_PATH_PATTERN_MATCHER: MatcherPatternPath<{ + pathMatch: string +}> = { + match(path) { + return { pathMatch: path } + }, + build({ pathMatch }) { + return pathMatch + }, +} + +export const EMPTY_PATH_PATTERN_MATCHER: MatcherPatternPath = { + match: path => { + if (path !== '/') { + throw miss() + } + return {} + }, + build: () => '/', +} + +export const USER_ID_PATH_PATTERN_MATCHER: MatcherPatternPath<{ id: number }> = + { + match(value) { + const match = value.match(/^\/users\/(\d+)$/) + if (!match?.[1]) { + throw miss() + } + const id = Number(match[1]) + if (Number.isNaN(id)) { + throw miss() + } + return { id } + }, + build({ id }) { + return `/users/${id}` + }, + } + +export const PAGE_QUERY_PATTERN_MATCHER: MatcherPatternQuery<{ page: number }> = + { + match: query => { + const page = Number(query.page) + return { + page: Number.isNaN(page) ? 1 : page, + } + }, + build: params => ({ page: String(params.page) }), + } satisfies MatcherPatternQuery<{ page: number }> + +export const ANY_HASH_PATTERN_MATCHER: MatcherPatternParams_Base< + string, + { hash: string | null } +> = { + match: hash => ({ hash: hash ? hash.slice(1) : null }), + build: ({ hash }) => (hash ? `#${hash}` : ''), +} + +export const EMPTY_PATH_ROUTE = { + name: 'no params', + path: EMPTY_PATH_PATTERN_MATCHER, +} satisfies MatcherPattern + +export const USER_ID_ROUTE = { + name: 'user-id', + path: USER_ID_PATH_PATTERN_MATCHER, +} satisfies MatcherPattern diff --git a/packages/router/src/types/utils.ts b/packages/router/src/types/utils.ts index e7d163184..2d443f69e 100644 --- a/packages/router/src/types/utils.ts +++ b/packages/router/src/types/utils.ts @@ -6,6 +6,16 @@ export type _LiteralUnion = | LiteralType | (BaseType & Record) +export type IsNull = + // avoid distributive conditional types + [T] extends [null] ? true : false + +export type IsUnknown = unknown extends T // `T` can be `unknown` or `any` + ? IsNull extends false // `any` can be `null`, but `unknown` can't be + ? true + : false + : false + /** * Maybe a promise maybe not * @internal From 84ac19e06e77d06e9889d99d185cf15840fddf8b Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Tue, 17 Dec 2024 15:32:50 +0100 Subject: [PATCH 23/40] refactor: reorganize types and add initial experimental router --- packages/router/src/errors.ts | 22 +- packages/router/src/experimental/router.ts | 1351 +++++++++++++++++ packages/router/src/index.ts | 3 +- packages/router/src/navigationGuards.ts | 40 + .../src/new-route-resolver/matcher.spec.ts | 1 - packages/router/src/router.ts | 349 +---- packages/router/src/scrollBehavior.ts | 16 + packages/router/src/types/utils.ts | 2 + 8 files changed, 1451 insertions(+), 333 deletions(-) create mode 100644 packages/router/src/experimental/router.ts diff --git a/packages/router/src/errors.ts b/packages/router/src/errors.ts index 877a0de21..63abf5f8a 100644 --- a/packages/router/src/errors.ts +++ b/packages/router/src/errors.ts @@ -1,5 +1,9 @@ import type { MatcherLocationRaw, MatcherLocation } from './types' -import type { RouteLocationRaw, RouteLocationNormalized } from './typed-routes' +import type { + RouteLocationRaw, + RouteLocationNormalized, + RouteLocationNormalizedLoaded, +} from './typed-routes' import { assign } from './utils' /** @@ -199,3 +203,19 @@ function stringifyRoute(to: RouteLocationRaw): string { } return JSON.stringify(location, null, 2) } +/** + * Internal type to define an ErrorHandler + * + * @param error - error thrown + * @param to - location we were navigating to when the error happened + * @param from - location we were navigating from when the error happened + * @internal + */ + +export interface _ErrorListener { + ( + error: any, + to: RouteLocationNormalized, + from: RouteLocationNormalizedLoaded + ): any +} diff --git a/packages/router/src/experimental/router.ts b/packages/router/src/experimental/router.ts new file mode 100644 index 000000000..ac44da155 --- /dev/null +++ b/packages/router/src/experimental/router.ts @@ -0,0 +1,1351 @@ +import { + createRouterError, + ErrorTypes, + isNavigationFailure, + NavigationRedirectError, + type _ErrorListener, + type NavigationFailure, +} from '../errors' +import { + nextTick, + shallowReactive, + shallowRef, + unref, + warn, + type App, + type Ref, +} from 'vue' +import { RouterLink } from '../RouterLink' +import { RouterView } from '../RouterView' +import { + NavigationType, + type HistoryState, + type RouterHistory, +} from '../history/common' +import type { PathParserOptions } from '../matcher' +import type { RouteResolver } from '../new-route-resolver/matcher' +import { + LocationQuery, + normalizeQuery, + parseQuery as originalParseQuery, + stringifyQuery as originalStringifyQuery, +} from '../query' +import type { Router } from '../router' +import { + _ScrollPositionNormalized, + computeScrollPosition, + getSavedScrollPosition, + getScrollKey, + saveScrollPosition, + scrollToPosition, + type RouterScrollBehavior, +} from '../scrollBehavior' +import type { + NavigationGuardWithThis, + NavigationHookAfter, + RouteLocation, + RouteLocationAsPath, + RouteLocationAsRelative, + RouteLocationAsRelativeTyped, + RouteLocationAsString, + RouteLocationNormalized, + RouteLocationNormalizedLoaded, + RouteLocationRaw, + RouteLocationResolved, + RouteMap, + RouteParams, + RouteRecordNameGeneric, +} from '../typed-routes' +import { + isRouteLocation, + isRouteName, + Lazy, + MatcherLocationRaw, + RouteLocationOptions, + type RouteRecordRaw, +} from '../types' +import { useCallbacks } from '../utils/callbacks' +import { + isSameRouteLocation, + parseURL, + START_LOCATION_NORMALIZED, + stringifyURL, +} from '../location' +import { applyToParams, assign, isArray, isBrowser, noop } from '../utils' +import { decode, encodeHash, encodeParam } from '../encoding' +import { + extractChangingRecords, + extractComponentsGuards, + guardToPromiseFn, +} from '../navigationGuards' +import { addDevtools } from '../devtools' +import { + routeLocationKey, + routerKey, + routerViewLocationKey, +} from '../injectionSymbols' + +/** + * resolve, reject arguments of Promise constructor + * @internal + */ +export type _OnReadyCallback = [() => void, (reason?: any) => void] + +/** + * Options to initialize a {@link Router} instance. + */ +export interface EXPERIMENTAL_RouterOptions_Base extends PathParserOptions { + /** + * History implementation used by the router. Most web applications should use + * `createWebHistory` but it requires the server to be properly configured. + * You can also use a _hash_ based history with `createWebHashHistory` that + * does not require any configuration on the server but isn't handled at all + * by search engines and does poorly on SEO. + * + * @example + * ```js + * createRouter({ + * history: createWebHistory(), + * // other options... + * }) + * ``` + */ + history: RouterHistory + + /** + * Function to control scrolling when navigating between pages. Can return a + * Promise to delay scrolling. + * + * @see {@link RouterScrollBehavior}. + * + * @example + * ```js + * function scrollBehavior(to, from, savedPosition) { + * // `to` and `from` are both route locations + * // `savedPosition` can be null if there isn't one + * } + * ``` + */ + + scrollBehavior?: RouterScrollBehavior + /** + * Custom implementation to parse a query. See its counterpart, + * {@link EXPERIMENTAL_RouterOptions_Base.stringifyQuery}. + * + * @example + * Let's say you want to use the [qs package](https://github.com/ljharb/qs) + * to parse queries, you can provide both `parseQuery` and `stringifyQuery`: + * ```js + * import qs from 'qs' + * + * createRouter({ + * // other options... + * parseQuery: qs.parse, + * stringifyQuery: qs.stringify, + * }) + * ``` + */ + + parseQuery?: typeof originalParseQuery + /** + * Custom implementation to stringify a query object. Should not prepend a leading `?`. + * {@link EXPERIMENTAL_RouterOptions_Base.parseQuery | parseQuery} counterpart to handle query parsing. + */ + + stringifyQuery?: typeof originalStringifyQuery + /** + * Default class applied to active {@link RouterLink}. If none is provided, + * `router-link-active` will be applied. + */ + + linkActiveClass?: string + /** + * Default class applied to exact active {@link RouterLink}. If none is provided, + * `router-link-exact-active` will be applied. + */ + + linkExactActiveClass?: string + /** + * Default class applied to non-active {@link RouterLink}. If none is provided, + * `router-link-inactive` will be applied. + */ + // linkInactiveClass?: string +} + +/** + * Options to initialize an experimental {@link EXPERIMENTAL_Router} instance. + * @experimental + */ +export interface EXPERIMENTAL_RouterOptions + extends EXPERIMENTAL_RouterOptions_Base { + /** + * Initial list of routes that should be added to the router. + */ + routes?: Readonly + + /** + * Matcher to use to resolve routes. + * @experimental + */ + matcher: RouteResolver +} + +/** + * Router instance. + * @experimental This version is not stable, it's meant to replace {@link Router} in the future. + */ +export interface EXPERIMENTAL_Router_Base { + /** + * Current {@link RouteLocationNormalized} + */ + readonly currentRoute: Ref + + /** + * Allows turning off the listening of history events. This is a low level api for micro-frontend. + */ + listening: boolean + + /** + * Add a new {@link RouteRecordRaw | route record} as the child of an existing route. + * + * @param parentName - Parent Route Record where `route` should be appended at + * @param route - Route Record to add + */ + addRoute( + // NOTE: it could be `keyof RouteMap` but the point of dynamic routes is not knowing the routes at build + parentName: NonNullable, + route: RouteRecordRaw + ): () => void + /** + * Add a new {@link RouteRecordRaw | route record} to the router. + * + * @param route - Route Record to add + */ + addRoute(route: TRouteRecordRaw): () => void + + /** + * Remove an existing route by its name. + * + * @param name - Name of the route to remove + */ + removeRoute(name: NonNullable): void + + /** + * Checks if a route with a given name exists + * + * @param name - Name of the route to check + */ + hasRoute(name: NonNullable): boolean + + /** + * Get a full list of all the {@link RouteRecord | route records}. + */ + getRoutes(): TRouteRecord[] + + /** + * Delete all routes from the router matcher. + */ + clearRoutes(): void + + /** + * Returns the {@link RouteLocation | normalized version} of a + * {@link RouteLocationRaw | route location}. Also includes an `href` property + * that includes any existing `base`. By default, the `currentLocation` used is + * `router.currentRoute` and should only be overridden in advanced use cases. + * + * @param to - Raw route location to resolve + * @param currentLocation - Optional current location to resolve against + */ + resolve( + to: RouteLocationAsRelativeTyped, + // NOTE: This version doesn't work probably because it infers the type too early + // | RouteLocationAsRelative + currentLocation?: RouteLocationNormalizedLoaded + ): RouteLocationResolved + resolve( + // not having the overload produces errors in RouterLink calls to router.resolve() + to: RouteLocationAsString | RouteLocationAsRelative | RouteLocationAsPath, + currentLocation?: RouteLocationNormalizedLoaded + ): RouteLocationResolved + + /** + * Programmatically navigate to a new URL by pushing an entry in the history + * stack. + * + * @param to - Route location to navigate to + */ + push(to: RouteLocationRaw): Promise + + /** + * Programmatically navigate to a new URL by replacing the current entry in + * the history stack. + * + * @param to - Route location to navigate to + */ + replace(to: RouteLocationRaw): Promise + + /** + * Go back in history if possible by calling `history.back()`. Equivalent to + * `router.go(-1)`. + */ + back(): void + + /** + * Go forward in history if possible by calling `history.forward()`. + * Equivalent to `router.go(1)`. + */ + forward(): void + + /** + * Allows you to move forward or backward through the history. Calls + * `history.go()`. + * + * @param delta - The position in the history to which you want to move, + * relative to the current page + */ + go(delta: number): void + + /** + * Add a navigation guard that executes before any navigation. Returns a + * function that removes the registered guard. + * + * @param guard - navigation guard to add + */ + beforeEach(guard: NavigationGuardWithThis): () => void + + /** + * Add a navigation guard that executes before navigation is about to be + * resolved. At this state all component have been fetched and other + * navigation guards have been successful. Returns a function that removes the + * registered guard. + * + * @param guard - navigation guard to add + * @returns a function that removes the registered guard + * + * @example + * ```js + * router.beforeResolve(to => { + * if (to.meta.requiresAuth && !isAuthenticated) return false + * }) + * ``` + * + */ + beforeResolve(guard: NavigationGuardWithThis): () => void + + /** + * Add a navigation hook that is executed after every navigation. Returns a + * function that removes the registered hook. + * + * @param guard - navigation hook to add + * @returns a function that removes the registered hook + * + * @example + * ```js + * router.afterEach((to, from, failure) => { + * if (isNavigationFailure(failure)) { + * console.log('failed navigation', failure) + * } + * }) + * ``` + */ + afterEach(guard: NavigationHookAfter): () => void + + /** + * Adds an error handler that is called every time a non caught error happens + * during navigation. This includes errors thrown synchronously and + * asynchronously, errors returned or passed to `next` in any navigation + * guard, and errors occurred when trying to resolve an async component that + * is required to render a route. + * + * @param handler - error handler to register + */ + onError(handler: _ErrorListener): () => void + + /** + * Returns a Promise that resolves when the router has completed the initial + * navigation, which means it has resolved all async enter hooks and async + * components that are associated with the initial route. If the initial + * navigation already happened, the promise resolves immediately. + * + * This is useful in server-side rendering to ensure consistent output on both + * the server and the client. Note that on server side, you need to manually + * push the initial location while on client side, the router automatically + * picks it up from the URL. + */ + isReady(): Promise + + /** + * Called automatically by `app.use(router)`. Should not be called manually by + * the user. This will trigger the initial navigation when on client side. + * + * @internal + * @param app - Application that uses the router + */ + install(app: App): void +} + +export interface EXPERIMENTAL_Router + extends EXPERIMENTAL_Router_Base { + /** + * Original options object passed to create the Router + */ + readonly options: EXPERIMENTAL_RouterOptions +} + +interface EXPERIMENTAL_RouteRecordRaw {} +interface EXPERIMENTAL_RouteRecord {} + +export function experimental_createRouter( + options: EXPERIMENTAL_RouterOptions< + EXPERIMENTAL_RouteRecordRaw, + EXPERIMENTAL_RouteRecord + > +): EXPERIMENTAL_Router { + const { + matcher, + parseQuery = originalParseQuery, + stringifyQuery = originalStringifyQuery, + history: routerHistory, + } = options + + if (__DEV__ && !routerHistory) + throw new Error( + 'Provide the "history" option when calling "createRouter()":' + + ' https://router.vuejs.org/api/interfaces/RouterOptions.html#history' + ) + + const beforeGuards = useCallbacks>() + const beforeResolveGuards = useCallbacks>() + const afterGuards = useCallbacks() + const currentRoute = shallowRef( + START_LOCATION_NORMALIZED + ) + let pendingLocation: RouteLocation = START_LOCATION_NORMALIZED + + // leave the scrollRestoration if no scrollBehavior is provided + if (isBrowser && options.scrollBehavior && 'scrollRestoration' in history) { + history.scrollRestoration = 'manual' + } + + const normalizeParams = applyToParams.bind( + null, + paramValue => '' + paramValue + ) + const encodeParams = applyToParams.bind(null, encodeParam) + const decodeParams: (params: RouteParams | undefined) => RouteParams = + // @ts-expect-error: intentionally avoid the type check + applyToParams.bind(null, decode) + + function addRoute( + parentOrRoute: NonNullable | RouteRecordRaw, + route?: RouteRecordRaw + ) { + let parent: Parameters<(typeof matcher)['addRoute']>[1] | undefined + let record: RouteRecordRaw + if (isRouteName(parentOrRoute)) { + parent = matcher.getMatcher(parentOrRoute) + if (__DEV__ && !parent) { + warn( + `Parent route "${String( + parentOrRoute + )}" not found when adding child route`, + route + ) + } + record = route! + } else { + record = parentOrRoute + } + + return matcher.addRoute(record, parent) + } + + function removeRoute(name: NonNullable) { + const recordMatcher = matcher.getMatcher(name) + if (recordMatcher) { + matcher.removeRoute(recordMatcher) + } else if (__DEV__) { + warn(`Cannot remove non-existent route "${String(name)}"`) + } + } + + function getRoutes() { + return matcher.getMatchers().map(routeMatcher => routeMatcher.record) + } + + function hasRoute(name: NonNullable): boolean { + return !!matcher.getMatcher(name) + } + + function resolve( + rawLocation: RouteLocationRaw, + currentLocation?: RouteLocationNormalizedLoaded + ): RouteLocationResolved { + // const resolve: Router['resolve'] = (rawLocation: RouteLocationRaw, currentLocation) => { + // const objectLocation = routerLocationAsObject(rawLocation) + // we create a copy to modify it later + currentLocation = assign({}, currentLocation || currentRoute.value) + if (typeof rawLocation === 'string') { + const locationNormalized = parseURL( + parseQuery, + rawLocation, + currentLocation.path + ) + const matchedRoute = matcher.resolve( + { path: locationNormalized.path }, + currentLocation + ) + + const href = routerHistory.createHref(locationNormalized.fullPath) + if (__DEV__) { + if (href.startsWith('//')) + warn( + `Location "${rawLocation}" resolved to "${href}". A resolved location cannot start with multiple slashes.` + ) + else if (!matchedRoute.matched.length) { + warn(`No match found for location with path "${rawLocation}"`) + } + } + + // locationNormalized is always a new object + return assign(locationNormalized, matchedRoute, { + params: decodeParams(matchedRoute.params), + hash: decode(locationNormalized.hash), + redirectedFrom: undefined, + href, + }) + } + + if (__DEV__ && !isRouteLocation(rawLocation)) { + warn( + `router.resolve() was passed an invalid location. This will fail in production.\n- Location:`, + rawLocation + ) + return resolve({}) + } + + let matcherLocation: MatcherLocationRaw + + // path could be relative in object as well + if (rawLocation.path != null) { + if ( + __DEV__ && + 'params' in rawLocation && + !('name' in rawLocation) && + // @ts-expect-error: the type is never + Object.keys(rawLocation.params).length + ) { + warn( + `Path "${rawLocation.path}" was passed with params but they will be ignored. Use a named route alongside params instead.` + ) + } + matcherLocation = assign({}, rawLocation, { + path: parseURL(parseQuery, rawLocation.path, currentLocation.path).path, + }) + } else { + // remove any nullish param + const targetParams = assign({}, rawLocation.params) + for (const key in targetParams) { + if (targetParams[key] == null) { + delete targetParams[key] + } + } + // pass encoded values to the matcher, so it can produce encoded path and fullPath + matcherLocation = assign({}, rawLocation, { + params: encodeParams(targetParams), + }) + // current location params are decoded, we need to encode them in case the + // matcher merges the params + currentLocation.params = encodeParams(currentLocation.params) + } + + const matchedRoute = matcher.resolve(matcherLocation, currentLocation) + const hash = rawLocation.hash || '' + + if (__DEV__ && hash && !hash.startsWith('#')) { + warn( + `A \`hash\` should always start with the character "#". Replace "${hash}" with "#${hash}".` + ) + } + + // the matcher might have merged current location params, so + // we need to run the decoding again + matchedRoute.params = normalizeParams(decodeParams(matchedRoute.params)) + + const fullPath = stringifyURL( + stringifyQuery, + assign({}, rawLocation, { + hash: encodeHash(hash), + path: matchedRoute.path, + }) + ) + + const href = routerHistory.createHref(fullPath) + if (__DEV__) { + if (href.startsWith('//')) { + warn( + `Location "${rawLocation}" resolved to "${href}". A resolved location cannot start with multiple slashes.` + ) + } else if (!matchedRoute.matched.length) { + warn( + `No match found for location with path "${ + rawLocation.path != null ? rawLocation.path : rawLocation + }"` + ) + } + } + + return assign( + { + fullPath, + // keep the hash encoded so fullPath is effectively path + encodedQuery + + // hash + hash, + query: + // if the user is using a custom query lib like qs, we might have + // nested objects, so we keep the query as is, meaning it can contain + // numbers at `$route.query`, but at the point, the user will have to + // use their own type anyway. + // https://github.com/vuejs/router/issues/328#issuecomment-649481567 + stringifyQuery === originalStringifyQuery + ? normalizeQuery(rawLocation.query) + : ((rawLocation.query || {}) as LocationQuery), + }, + matchedRoute, + { + redirectedFrom: undefined, + href, + } + ) + } + + function locationAsObject( + to: RouteLocationRaw | RouteLocationNormalized + ): Exclude | RouteLocationNormalized { + return typeof to === 'string' + ? parseURL(parseQuery, to, currentRoute.value.path) + : assign({}, to) + } + + function checkCanceledNavigation( + to: RouteLocationNormalized, + from: RouteLocationNormalized + ): NavigationFailure | void { + if (pendingLocation !== to) { + return createRouterError( + ErrorTypes.NAVIGATION_CANCELLED, + { + from, + to, + } + ) + } + } + + function push(to: RouteLocationRaw) { + return pushWithRedirect(to) + } + + function replace(to: RouteLocationRaw) { + return push(assign(locationAsObject(to), { replace: true })) + } + + function handleRedirectRecord(to: RouteLocation): RouteLocationRaw | void { + const lastMatched = to.matched[to.matched.length - 1] + if (lastMatched && lastMatched.redirect) { + const { redirect } = lastMatched + let newTargetLocation = + typeof redirect === 'function' ? redirect(to) : redirect + + if (typeof newTargetLocation === 'string') { + newTargetLocation = + newTargetLocation.includes('?') || newTargetLocation.includes('#') + ? (newTargetLocation = locationAsObject(newTargetLocation)) + : // force empty params + { path: newTargetLocation } + // @ts-expect-error: force empty params when a string is passed to let + // the router parse them again + newTargetLocation.params = {} + } + + if ( + __DEV__ && + newTargetLocation.path == null && + !('name' in newTargetLocation) + ) { + warn( + `Invalid redirect found:\n${JSON.stringify( + newTargetLocation, + null, + 2 + )}\n when navigating to "${ + to.fullPath + }". A redirect must contain a name or path. This will break in production.` + ) + throw new Error('Invalid redirect') + } + + return assign( + { + query: to.query, + hash: to.hash, + // avoid transferring params if the redirect has a path + params: newTargetLocation.path != null ? {} : to.params, + }, + newTargetLocation + ) + } + } + + function pushWithRedirect( + to: RouteLocationRaw | RouteLocation, + redirectedFrom?: RouteLocation + ): Promise { + const targetLocation: RouteLocation = (pendingLocation = resolve(to)) + const from = currentRoute.value + const data: HistoryState | undefined = (to as RouteLocationOptions).state + const force: boolean | undefined = (to as RouteLocationOptions).force + // to could be a string where `replace` is a function + const replace = (to as RouteLocationOptions).replace === true + + const shouldRedirect = handleRedirectRecord(targetLocation) + + if (shouldRedirect) + return pushWithRedirect( + assign(locationAsObject(shouldRedirect), { + state: + typeof shouldRedirect === 'object' + ? assign({}, data, shouldRedirect.state) + : data, + force, + replace, + }), + // keep original redirectedFrom if it exists + redirectedFrom || targetLocation + ) + + // if it was a redirect we already called `pushWithRedirect` above + const toLocation = targetLocation as RouteLocationNormalized + + toLocation.redirectedFrom = redirectedFrom + let failure: NavigationFailure | void | undefined + + if (!force && isSameRouteLocation(stringifyQuery, from, targetLocation)) { + failure = createRouterError( + ErrorTypes.NAVIGATION_DUPLICATED, + { to: toLocation, from } + ) + // trigger scroll to allow scrolling to the same anchor + handleScroll( + from, + from, + // this is a push, the only way for it to be triggered from a + // history.listen is with a redirect, which makes it become a push + true, + // This cannot be the first navigation because the initial location + // cannot be manually navigated to + false + ) + } + + return (failure ? Promise.resolve(failure) : navigate(toLocation, from)) + .catch((error: NavigationFailure | NavigationRedirectError) => + isNavigationFailure(error) + ? // navigation redirects still mark the router as ready + isNavigationFailure(error, ErrorTypes.NAVIGATION_GUARD_REDIRECT) + ? error + : markAsReady(error) // also returns the error + : // reject any unknown error + triggerError(error, toLocation, from) + ) + .then((failure: NavigationFailure | NavigationRedirectError | void) => { + if (failure) { + if ( + isNavigationFailure(failure, ErrorTypes.NAVIGATION_GUARD_REDIRECT) + ) { + if ( + __DEV__ && + // we are redirecting to the same location we were already at + isSameRouteLocation( + stringifyQuery, + resolve(failure.to), + toLocation + ) && + // and we have done it a couple of times + redirectedFrom && + // @ts-expect-error: added only in dev + (redirectedFrom._count = redirectedFrom._count + ? // @ts-expect-error + redirectedFrom._count + 1 + : 1) > 30 + ) { + warn( + `Detected a possibly infinite redirection in a navigation guard when going from "${from.fullPath}" to "${toLocation.fullPath}". Aborting to avoid a Stack Overflow.\n Are you always returning a new location within a navigation guard? That would lead to this error. Only return when redirecting or aborting, that should fix this. This might break in production if not fixed.` + ) + return Promise.reject( + new Error('Infinite redirect in navigation guard') + ) + } + + return pushWithRedirect( + // keep options + assign( + { + // preserve an existing replacement but allow the redirect to override it + replace, + }, + locationAsObject(failure.to), + { + state: + typeof failure.to === 'object' + ? assign({}, data, failure.to.state) + : data, + force, + } + ), + // preserve the original redirectedFrom if any + redirectedFrom || toLocation + ) + } + } else { + // if we fail we don't finalize the navigation + failure = finalizeNavigation( + toLocation as RouteLocationNormalizedLoaded, + from, + true, + replace, + data + ) + } + triggerAfterEach( + toLocation as RouteLocationNormalizedLoaded, + from, + failure + ) + return failure + }) + } + + /** + * Helper to reject and skip all navigation guards if a new navigation happened + * @param to + * @param from + */ + function checkCanceledNavigationAndReject( + to: RouteLocationNormalized, + from: RouteLocationNormalized + ): Promise { + const error = checkCanceledNavigation(to, from) + return error ? Promise.reject(error) : Promise.resolve() + } + + function runWithContext(fn: () => T): T { + const app: App | undefined = installedApps.values().next().value + // support Vue < 3.3 + return app && typeof app.runWithContext === 'function' + ? app.runWithContext(fn) + : fn() + } + + // TODO: refactor the whole before guards by internally using router.beforeEach + + function navigate( + to: RouteLocationNormalized, + from: RouteLocationNormalizedLoaded + ): Promise { + let guards: Lazy[] + + const [leavingRecords, updatingRecords, enteringRecords] = + extractChangingRecords(to, from) + + // all components here have been resolved once because we are leaving + guards = extractComponentsGuards( + leavingRecords.reverse(), + 'beforeRouteLeave', + to, + from + ) + + // leavingRecords is already reversed + for (const record of leavingRecords) { + record.leaveGuards.forEach(guard => { + guards.push(guardToPromiseFn(guard, to, from)) + }) + } + + const canceledNavigationCheck = checkCanceledNavigationAndReject.bind( + null, + to, + from + ) + + guards.push(canceledNavigationCheck) + + // run the queue of per route beforeRouteLeave guards + return ( + runGuardQueue(guards) + .then(() => { + // check global guards beforeEach + guards = [] + for (const guard of beforeGuards.list()) { + guards.push(guardToPromiseFn(guard, to, from)) + } + guards.push(canceledNavigationCheck) + + return runGuardQueue(guards) + }) + .then(() => { + // check in components beforeRouteUpdate + guards = extractComponentsGuards( + updatingRecords, + 'beforeRouteUpdate', + to, + from + ) + + for (const record of updatingRecords) { + record.updateGuards.forEach(guard => { + guards.push(guardToPromiseFn(guard, to, from)) + }) + } + guards.push(canceledNavigationCheck) + + // run the queue of per route beforeEnter guards + return runGuardQueue(guards) + }) + .then(() => { + // check the route beforeEnter + guards = [] + for (const record of enteringRecords) { + // do not trigger beforeEnter on reused views + if (record.beforeEnter) { + if (isArray(record.beforeEnter)) { + for (const beforeEnter of record.beforeEnter) + guards.push(guardToPromiseFn(beforeEnter, to, from)) + } else { + guards.push(guardToPromiseFn(record.beforeEnter, to, from)) + } + } + } + guards.push(canceledNavigationCheck) + + // run the queue of per route beforeEnter guards + return runGuardQueue(guards) + }) + .then(() => { + // NOTE: at this point to.matched is normalized and does not contain any () => Promise + + // clear existing enterCallbacks, these are added by extractComponentsGuards + to.matched.forEach(record => (record.enterCallbacks = {})) + + // check in-component beforeRouteEnter + guards = extractComponentsGuards( + enteringRecords, + 'beforeRouteEnter', + to, + from, + runWithContext + ) + guards.push(canceledNavigationCheck) + + // run the queue of per route beforeEnter guards + return runGuardQueue(guards) + }) + .then(() => { + // check global guards beforeResolve + guards = [] + for (const guard of beforeResolveGuards.list()) { + guards.push(guardToPromiseFn(guard, to, from)) + } + guards.push(canceledNavigationCheck) + + return runGuardQueue(guards) + }) + // catch any navigation canceled + .catch(err => + isNavigationFailure(err, ErrorTypes.NAVIGATION_CANCELLED) + ? err + : Promise.reject(err) + ) + ) + } + + function triggerAfterEach( + to: RouteLocationNormalizedLoaded, + from: RouteLocationNormalizedLoaded, + failure?: NavigationFailure | void + ): void { + // navigation is confirmed, call afterGuards + // TODO: wrap with error handlers + afterGuards + .list() + .forEach(guard => runWithContext(() => guard(to, from, failure))) + } + + /** + * - Cleans up any navigation guards + * - Changes the url if necessary + * - Calls the scrollBehavior + */ + function finalizeNavigation( + toLocation: RouteLocationNormalizedLoaded, + from: RouteLocationNormalizedLoaded, + isPush: boolean, + replace?: boolean, + data?: HistoryState + ): NavigationFailure | void { + // a more recent navigation took place + const error = checkCanceledNavigation(toLocation, from) + if (error) return error + + // only consider as push if it's not the first navigation + const isFirstNavigation = from === START_LOCATION_NORMALIZED + const state: Partial | null = !isBrowser ? {} : history.state + + // change URL only if the user did a push/replace and if it's not the initial navigation because + // it's just reflecting the url + if (isPush) { + // on the initial navigation, we want to reuse the scroll position from + // history state if it exists + if (replace || isFirstNavigation) + routerHistory.replace( + toLocation.fullPath, + assign( + { + scroll: isFirstNavigation && state && state.scroll, + }, + data + ) + ) + else routerHistory.push(toLocation.fullPath, data) + } + + // accept current navigation + currentRoute.value = toLocation + handleScroll(toLocation, from, isPush, isFirstNavigation) + + markAsReady() + } + + let removeHistoryListener: undefined | null | (() => void) + // attach listener to history to trigger navigations + function setupListeners() { + // avoid setting up listeners twice due to an invalid first navigation + if (removeHistoryListener) return + removeHistoryListener = routerHistory.listen((to, _from, info) => { + if (!router.listening) return + // cannot be a redirect route because it was in history + const toLocation = resolve(to) as RouteLocationNormalized + + // due to dynamic routing, and to hash history with manual navigation + // (manually changing the url or calling history.hash = '#/somewhere'), + // there could be a redirect record in history + const shouldRedirect = handleRedirectRecord(toLocation) + if (shouldRedirect) { + pushWithRedirect( + assign(shouldRedirect, { replace: true, force: true }), + toLocation + ).catch(noop) + return + } + + pendingLocation = toLocation + const from = currentRoute.value + + // TODO: should be moved to web history? + if (isBrowser) { + saveScrollPosition( + getScrollKey(from.fullPath, info.delta), + computeScrollPosition() + ) + } + + navigate(toLocation, from) + .catch((error: NavigationFailure | NavigationRedirectError) => { + if ( + isNavigationFailure( + error, + ErrorTypes.NAVIGATION_ABORTED | ErrorTypes.NAVIGATION_CANCELLED + ) + ) { + return error + } + if ( + isNavigationFailure(error, ErrorTypes.NAVIGATION_GUARD_REDIRECT) + ) { + // Here we could call if (info.delta) routerHistory.go(-info.delta, + // false) but this is bug prone as we have no way to wait the + // navigation to be finished before calling pushWithRedirect. Using + // a setTimeout of 16ms seems to work but there is no guarantee for + // it to work on every browser. So instead we do not restore the + // history entry and trigger a new navigation as requested by the + // navigation guard. + + // the error is already handled by router.push we just want to avoid + // logging the error + pushWithRedirect( + assign(locationAsObject((error as NavigationRedirectError).to), { + force: true, + }), + toLocation + // avoid an uncaught rejection, let push call triggerError + ) + .then(failure => { + // manual change in hash history #916 ending up in the URL not + // changing, but it was changed by the manual url change, so we + // need to manually change it ourselves + if ( + isNavigationFailure( + failure, + ErrorTypes.NAVIGATION_ABORTED | + ErrorTypes.NAVIGATION_DUPLICATED + ) && + !info.delta && + info.type === NavigationType.pop + ) { + routerHistory.go(-1, false) + } + }) + .catch(noop) + // avoid the then branch + return Promise.reject() + } + // do not restore history on unknown direction + if (info.delta) { + routerHistory.go(-info.delta, false) + } + // unrecognized error, transfer to the global handler + return triggerError(error, toLocation, from) + }) + .then((failure: NavigationFailure | void) => { + failure = + failure || + finalizeNavigation( + // after navigation, all matched components are resolved + toLocation as RouteLocationNormalizedLoaded, + from, + false + ) + + // revert the navigation + if (failure) { + if ( + info.delta && + // a new navigation has been triggered, so we do not want to revert, that will change the current history + // entry while a different route is displayed + !isNavigationFailure(failure, ErrorTypes.NAVIGATION_CANCELLED) + ) { + routerHistory.go(-info.delta, false) + } else if ( + info.type === NavigationType.pop && + isNavigationFailure( + failure, + ErrorTypes.NAVIGATION_ABORTED | ErrorTypes.NAVIGATION_DUPLICATED + ) + ) { + // manual change in hash history #916 + // it's like a push but lacks the information of the direction + routerHistory.go(-1, false) + } + } + + triggerAfterEach( + toLocation as RouteLocationNormalizedLoaded, + from, + failure + ) + }) + // avoid warnings in the console about uncaught rejections, they are logged by triggerErrors + .catch(noop) + }) + } + + // Initialization and Errors + + let readyHandlers = useCallbacks<_OnReadyCallback>() + let errorListeners = useCallbacks<_ErrorListener>() + let ready: boolean + + /** + * Trigger errorListeners added via onError and throws the error as well + * + * @param error - error to throw + * @param to - location we were navigating to when the error happened + * @param from - location we were navigating from when the error happened + * @returns the error as a rejected promise + */ + function triggerError( + error: any, + to: RouteLocationNormalized, + from: RouteLocationNormalizedLoaded + ): Promise { + markAsReady(error) + const list = errorListeners.list() + if (list.length) { + list.forEach(handler => handler(error, to, from)) + } else { + if (__DEV__) { + warn('uncaught error during route navigation:') + } + console.error(error) + } + // reject the error no matter there were error listeners or not + return Promise.reject(error) + } + + function isReady(): Promise { + if (ready && currentRoute.value !== START_LOCATION_NORMALIZED) + return Promise.resolve() + return new Promise((resolve, reject) => { + readyHandlers.add([resolve, reject]) + }) + } + + /** + * Mark the router as ready, resolving the promised returned by isReady(). Can + * only be called once, otherwise does nothing. + * @param err - optional error + */ + function markAsReady(err: E): E + function markAsReady(): void + function markAsReady(err?: E): E | void { + if (!ready) { + // still not ready if an error happened + ready = !err + setupListeners() + readyHandlers + .list() + .forEach(([resolve, reject]) => (err ? reject(err) : resolve())) + readyHandlers.reset() + } + return err + } + + // Scroll behavior + function handleScroll( + to: RouteLocationNormalizedLoaded, + from: RouteLocationNormalizedLoaded, + isPush: boolean, + isFirstNavigation: boolean + ): // the return is not meant to be used + Promise { + const { scrollBehavior } = options + if (!isBrowser || !scrollBehavior) return Promise.resolve() + + const scrollPosition: _ScrollPositionNormalized | null = + (!isPush && getSavedScrollPosition(getScrollKey(to.fullPath, 0))) || + ((isFirstNavigation || !isPush) && + (history.state as HistoryState) && + history.state.scroll) || + null + + return nextTick() + .then(() => scrollBehavior(to, from, scrollPosition)) + .then(position => position && scrollToPosition(position)) + .catch(err => triggerError(err, to, from)) + } + + const go = (delta: number) => routerHistory.go(delta) + + let started: boolean | undefined + const installedApps = new Set() + + const router: Router = { + currentRoute, + listening: true, + + addRoute, + removeRoute, + clearRoutes: matcher.clearRoutes, + hasRoute, + getRoutes, + resolve, + options, + + push, + replace, + go, + back: () => go(-1), + forward: () => go(1), + + beforeEach: beforeGuards.add, + beforeResolve: beforeResolveGuards.add, + afterEach: afterGuards.add, + + onError: errorListeners.add, + isReady, + + install(app: App) { + const router = this + app.component('RouterLink', RouterLink) + app.component('RouterView', RouterView) + + app.config.globalProperties.$router = router + Object.defineProperty(app.config.globalProperties, '$route', { + enumerable: true, + get: () => unref(currentRoute), + }) + + // this initial navigation is only necessary on client, on server it doesn't + // make sense because it will create an extra unnecessary navigation and could + // lead to problems + if ( + isBrowser && + // used for the initial navigation client side to avoid pushing + // multiple times when the router is used in multiple apps + !started && + currentRoute.value === START_LOCATION_NORMALIZED + ) { + // see above + started = true + push(routerHistory.location).catch(err => { + if (__DEV__) warn('Unexpected error when starting the router:', err) + }) + } + + const reactiveRoute = {} as RouteLocationNormalizedLoaded + for (const key in START_LOCATION_NORMALIZED) { + Object.defineProperty(reactiveRoute, key, { + get: () => currentRoute.value[key as keyof RouteLocationNormalized], + enumerable: true, + }) + } + + app.provide(routerKey, router) + app.provide(routeLocationKey, shallowReactive(reactiveRoute)) + app.provide(routerViewLocationKey, currentRoute) + + const unmountApp = app.unmount + installedApps.add(app) + app.unmount = function () { + installedApps.delete(app) + // the router is not attached to an app anymore + if (installedApps.size < 1) { + // invalidate the current navigation + pendingLocation = START_LOCATION_NORMALIZED + removeHistoryListener && removeHistoryListener() + removeHistoryListener = null + currentRoute.value = START_LOCATION_NORMALIZED + started = false + ready = false + } + unmountApp() + } + + // TODO: this probably needs to be updated so it can be used by vue-termui + if ((__DEV__ || __FEATURE_PROD_DEVTOOLS__) && isBrowser) { + addDevtools(app, router, matcher) + } + }, + } + + // TODO: type this as NavigationGuardReturn or similar instead of any + function runGuardQueue(guards: Lazy[]): Promise { + return guards.reduce( + (promise, guard) => promise.then(() => runWithContext(guard)), + Promise.resolve() + ) + } + + return router +} diff --git a/packages/router/src/index.ts b/packages/router/src/index.ts index 2b27d8329..71899d34a 100644 --- a/packages/router/src/index.ts +++ b/packages/router/src/index.ts @@ -138,7 +138,8 @@ export type { } from './typed-routes' export { createRouter } from './router' -export type { Router, RouterOptions, RouterScrollBehavior } from './router' +export type { Router, RouterOptions } from './router' +export type { RouterScrollBehavior } from './scrollBehavior' export { NavigationFailureType, isNavigationFailure } from './errors' export type { diff --git a/packages/router/src/navigationGuards.ts b/packages/router/src/navigationGuards.ts index db53c3dc1..0582ce47b 100644 --- a/packages/router/src/navigationGuards.ts +++ b/packages/router/src/navigationGuards.ts @@ -22,6 +22,7 @@ import { matchedRouteKey } from './injectionSymbols' import { RouteRecordNormalized } from './matcher/types' import { isESModule, isRouteComponent } from './utils' import { warn } from './warning' +import { isSameRouteRecord } from './location' function registerGuard( record: RouteRecordNormalized, @@ -398,3 +399,42 @@ export function loadRouteLocation( ) ).then(() => route as RouteLocationNormalizedLoaded) } + +/** + * Split the leaving, updating, and entering records. + * @internal + * + * @param to - Location we are navigating to + * @param from - Location we are navigating from + */ +export function extractChangingRecords( + to: RouteLocationNormalized, + from: RouteLocationNormalizedLoaded +): [ + leavingRecords: RouteRecordNormalized[], + updatingRecords: RouteRecordNormalized[], + enteringRecords: RouteRecordNormalized[] +] { + const leavingRecords: RouteRecordNormalized[] = [] + const updatingRecords: RouteRecordNormalized[] = [] + const enteringRecords: RouteRecordNormalized[] = [] + + const len = Math.max(from.matched.length, to.matched.length) + for (let i = 0; i < len; i++) { + const recordFrom = from.matched[i] + if (recordFrom) { + if (to.matched.find(record => isSameRouteRecord(record, recordFrom))) + updatingRecords.push(recordFrom) + else leavingRecords.push(recordFrom) + } + const recordTo = to.matched[i] + if (recordTo) { + // the type doesn't matter because we are comparing per reference + if (!from.matched.find(record => isSameRouteRecord(record, recordTo))) { + enteringRecords.push(recordTo) + } + } + } + + return [leavingRecords, updatingRecords, enteringRecords] +} diff --git a/packages/router/src/new-route-resolver/matcher.spec.ts b/packages/router/src/new-route-resolver/matcher.spec.ts index f508fe113..22fb3e511 100644 --- a/packages/router/src/new-route-resolver/matcher.spec.ts +++ b/packages/router/src/new-route-resolver/matcher.spec.ts @@ -11,7 +11,6 @@ import { MatcherPatternQuery, MatcherPatternPathStatic, MatcherPatternPathDynamic, - defineParamParser, } from './matcher-pattern' import { miss } from './matchers/errors' import { EmptyParams } from './matcher-location' diff --git a/packages/router/src/router.ts b/packages/router/src/router.ts index bc448bbd5..5746d396c 100644 --- a/packages/router/src/router.ts +++ b/packages/router/src/router.ts @@ -15,14 +15,10 @@ import type { NavigationGuardWithThis, NavigationHookAfter, RouteLocationResolved, - RouteLocationAsRelative, - RouteLocationAsPath, - RouteLocationAsString, RouteRecordNameGeneric, } from './typed-routes' -import { RouterHistory, HistoryState, NavigationType } from './history/common' +import { HistoryState, NavigationType } from './history/common' import { - ScrollPosition, getSavedScrollPosition, getScrollKey, saveScrollPosition, @@ -30,13 +26,14 @@ import { scrollToPosition, _ScrollPositionNormalized, } from './scrollBehavior' -import { createRouterMatcher, PathParserOptions } from './matcher' +import { createRouterMatcher } from './matcher' import { createRouterError, ErrorTypes, NavigationFailure, NavigationRedirectError, isNavigationFailure, + _ErrorListener, } from './errors' import { applyToParams, isBrowser, assign, noop, isArray } from './utils' import { useCallbacks } from './utils/callbacks' @@ -47,16 +44,19 @@ import { stringifyQuery as originalStringifyQuery, LocationQuery, } from './query' -import { shallowRef, Ref, nextTick, App, unref, shallowReactive } from 'vue' -import { RouteRecord, RouteRecordNormalized } from './matcher/types' +import { shallowRef, nextTick, App, unref, shallowReactive } from 'vue' +import { RouteRecordNormalized } from './matcher/types' import { parseURL, stringifyURL, isSameRouteLocation, - isSameRouteRecord, START_LOCATION_NORMALIZED, } from './location' -import { extractComponentsGuards, guardToPromiseFn } from './navigationGuards' +import { + extractChangingRecords, + extractComponentsGuards, + guardToPromiseFn, +} from './navigationGuards' import { warn } from './warning' import { RouterLink } from './RouterLink' import { RouterView } from './RouterView' @@ -67,314 +67,31 @@ import { } from './injectionSymbols' import { addDevtools } from './devtools' import { _LiteralUnion } from './types/utils' -import { RouteLocationAsRelativeTyped } from './typed-routes/route-location' -import { RouteMap } from './typed-routes/route-map' - -/** - * Internal type to define an ErrorHandler - * - * @param error - error thrown - * @param to - location we were navigating to when the error happened - * @param from - location we were navigating from when the error happened - * @internal - */ -export interface _ErrorListener { - ( - error: any, - to: RouteLocationNormalized, - from: RouteLocationNormalizedLoaded - ): any -} -// resolve, reject arguments of Promise constructor -type OnReadyCallback = [() => void, (reason?: any) => void] - -type Awaitable = T | Promise - -/** - * Type of the `scrollBehavior` option that can be passed to `createRouter`. - */ -export interface RouterScrollBehavior { - /** - * @param to - Route location where we are navigating to - * @param from - Route location where we are navigating from - * @param savedPosition - saved position if it exists, `null` otherwise - */ - ( - to: RouteLocationNormalized, - from: RouteLocationNormalizedLoaded, - savedPosition: _ScrollPositionNormalized | null - ): Awaitable -} +import { + EXPERIMENTAL_RouterOptions_Base, + EXPERIMENTAL_Router_Base, + _OnReadyCallback, +} from './experimental/router' /** * Options to initialize a {@link Router} instance. */ -export interface RouterOptions extends PathParserOptions { - /** - * History implementation used by the router. Most web applications should use - * `createWebHistory` but it requires the server to be properly configured. - * You can also use a _hash_ based history with `createWebHashHistory` that - * does not require any configuration on the server but isn't handled at all - * by search engines and does poorly on SEO. - * - * @example - * ```js - * createRouter({ - * history: createWebHistory(), - * // other options... - * }) - * ``` - */ - history: RouterHistory +export interface RouterOptions extends EXPERIMENTAL_RouterOptions_Base { /** * Initial list of routes that should be added to the router. */ routes: Readonly - /** - * Function to control scrolling when navigating between pages. Can return a - * Promise to delay scrolling. Check {@link ScrollBehavior}. - * - * @example - * ```js - * function scrollBehavior(to, from, savedPosition) { - * // `to` and `from` are both route locations - * // `savedPosition` can be null if there isn't one - * } - * ``` - */ - scrollBehavior?: RouterScrollBehavior - /** - * Custom implementation to parse a query. See its counterpart, - * {@link RouterOptions.stringifyQuery}. - * - * @example - * Let's say you want to use the [qs package](https://github.com/ljharb/qs) - * to parse queries, you can provide both `parseQuery` and `stringifyQuery`: - * ```js - * import qs from 'qs' - * - * createRouter({ - * // other options... - * parseQuery: qs.parse, - * stringifyQuery: qs.stringify, - * }) - * ``` - */ - parseQuery?: typeof originalParseQuery - /** - * Custom implementation to stringify a query object. Should not prepend a leading `?`. - * {@link RouterOptions.parseQuery | parseQuery} counterpart to handle query parsing. - */ - stringifyQuery?: typeof originalStringifyQuery - /** - * Default class applied to active {@link RouterLink}. If none is provided, - * `router-link-active` will be applied. - */ - linkActiveClass?: string - /** - * Default class applied to exact active {@link RouterLink}. If none is provided, - * `router-link-exact-active` will be applied. - */ - linkExactActiveClass?: string - /** - * Default class applied to non-active {@link RouterLink}. If none is provided, - * `router-link-inactive` will be applied. - */ - // linkInactiveClass?: string } /** * Router instance. */ -export interface Router { - /** - * @internal - */ - // readonly history: RouterHistory - /** - * Current {@link RouteLocationNormalized} - */ - readonly currentRoute: Ref +export interface Router + extends EXPERIMENTAL_Router_Base { /** * Original options object passed to create the Router */ readonly options: RouterOptions - - /** - * Allows turning off the listening of history events. This is a low level api for micro-frontend. - */ - listening: boolean - - /** - * Add a new {@link RouteRecordRaw | route record} as the child of an existing route. - * - * @param parentName - Parent Route Record where `route` should be appended at - * @param route - Route Record to add - */ - addRoute( - // NOTE: it could be `keyof RouteMap` but the point of dynamic routes is not knowing the routes at build - parentName: NonNullable, - route: RouteRecordRaw - ): () => void - /** - * Add a new {@link RouteRecordRaw | route record} to the router. - * - * @param route - Route Record to add - */ - addRoute(route: RouteRecordRaw): () => void - /** - * Remove an existing route by its name. - * - * @param name - Name of the route to remove - */ - removeRoute(name: NonNullable): void - /** - * Checks if a route with a given name exists - * - * @param name - Name of the route to check - */ - hasRoute(name: NonNullable): boolean - /** - * Get a full list of all the {@link RouteRecord | route records}. - */ - getRoutes(): RouteRecord[] - - /** - * Delete all routes from the router matcher. - */ - clearRoutes(): void - - /** - * Returns the {@link RouteLocation | normalized version} of a - * {@link RouteLocationRaw | route location}. Also includes an `href` property - * that includes any existing `base`. By default, the `currentLocation` used is - * `router.currentRoute` and should only be overridden in advanced use cases. - * - * @param to - Raw route location to resolve - * @param currentLocation - Optional current location to resolve against - */ - resolve( - to: RouteLocationAsRelativeTyped, - // NOTE: This version doesn't work probably because it infers the type too early - // | RouteLocationAsRelative - currentLocation?: RouteLocationNormalizedLoaded - ): RouteLocationResolved - resolve( - // not having the overload produces errors in RouterLink calls to router.resolve() - to: RouteLocationAsString | RouteLocationAsRelative | RouteLocationAsPath, - currentLocation?: RouteLocationNormalizedLoaded - ): RouteLocationResolved - - /** - * Programmatically navigate to a new URL by pushing an entry in the history - * stack. - * - * @param to - Route location to navigate to - */ - push(to: RouteLocationRaw): Promise - - /** - * Programmatically navigate to a new URL by replacing the current entry in - * the history stack. - * - * @param to - Route location to navigate to - */ - replace(to: RouteLocationRaw): Promise - - /** - * Go back in history if possible by calling `history.back()`. Equivalent to - * `router.go(-1)`. - */ - back(): ReturnType - /** - * Go forward in history if possible by calling `history.forward()`. - * Equivalent to `router.go(1)`. - */ - forward(): ReturnType - /** - * Allows you to move forward or backward through the history. Calls - * `history.go()`. - * - * @param delta - The position in the history to which you want to move, - * relative to the current page - */ - go(delta: number): void - - /** - * Add a navigation guard that executes before any navigation. Returns a - * function that removes the registered guard. - * - * @param guard - navigation guard to add - */ - beforeEach(guard: NavigationGuardWithThis): () => void - /** - * Add a navigation guard that executes before navigation is about to be - * resolved. At this state all component have been fetched and other - * navigation guards have been successful. Returns a function that removes the - * registered guard. - * - * @param guard - navigation guard to add - * @returns a function that removes the registered guard - * - * @example - * ```js - * router.beforeResolve(to => { - * if (to.meta.requiresAuth && !isAuthenticated) return false - * }) - * ``` - * - */ - beforeResolve(guard: NavigationGuardWithThis): () => void - - /** - * Add a navigation hook that is executed after every navigation. Returns a - * function that removes the registered hook. - * - * @param guard - navigation hook to add - * @returns a function that removes the registered hook - * - * @example - * ```js - * router.afterEach((to, from, failure) => { - * if (isNavigationFailure(failure)) { - * console.log('failed navigation', failure) - * } - * }) - * ``` - */ - afterEach(guard: NavigationHookAfter): () => void - - /** - * Adds an error handler that is called every time a non caught error happens - * during navigation. This includes errors thrown synchronously and - * asynchronously, errors returned or passed to `next` in any navigation - * guard, and errors occurred when trying to resolve an async component that - * is required to render a route. - * - * @param handler - error handler to register - */ - onError(handler: _ErrorListener): () => void - /** - * Returns a Promise that resolves when the router has completed the initial - * navigation, which means it has resolved all async enter hooks and async - * components that are associated with the initial route. If the initial - * navigation already happened, the promise resolves immediately. - * - * This is useful in server-side rendering to ensure consistent output on both - * the server and the client. Note that on server side, you need to manually - * push the initial location while on client side, the router automatically - * picks it up from the URL. - */ - isReady(): Promise - - /** - * Called automatically by `app.use(router)`. Should not be called manually by - * the user. This will trigger the initial navigation when on client side. - * - * @internal - * @param app - Application that uses the router - */ - install(app: App): void } /** @@ -1141,7 +858,7 @@ export function createRouter(options: RouterOptions): Router { // Initialization and Errors - let readyHandlers = useCallbacks() + let readyHandlers = useCallbacks<_OnReadyCallback>() let errorListeners = useCallbacks<_ErrorListener>() let ready: boolean @@ -1327,31 +1044,3 @@ export function createRouter(options: RouterOptions): Router { return router } - -function extractChangingRecords( - to: RouteLocationNormalized, - from: RouteLocationNormalizedLoaded -) { - const leavingRecords: RouteRecordNormalized[] = [] - const updatingRecords: RouteRecordNormalized[] = [] - const enteringRecords: RouteRecordNormalized[] = [] - - const len = Math.max(from.matched.length, to.matched.length) - for (let i = 0; i < len; i++) { - const recordFrom = from.matched[i] - if (recordFrom) { - if (to.matched.find(record => isSameRouteRecord(record, recordFrom))) - updatingRecords.push(recordFrom) - else leavingRecords.push(recordFrom) - } - const recordTo = to.matched[i] - if (recordTo) { - // the type doesn't matter because we are comparing per reference - if (!from.matched.find(record => isSameRouteRecord(record, recordTo))) { - enteringRecords.push(recordTo) - } - } - } - - return [leavingRecords, updatingRecords, enteringRecords] -} diff --git a/packages/router/src/scrollBehavior.ts b/packages/router/src/scrollBehavior.ts index 642556452..8124a9fb0 100644 --- a/packages/router/src/scrollBehavior.ts +++ b/packages/router/src/scrollBehavior.ts @@ -29,6 +29,22 @@ export type _ScrollPositionNormalized = { top: number } +/** + * Type of the `scrollBehavior` option that can be passed to `createRouter`. + */ +export interface RouterScrollBehavior { + /** + * @param to - Route location where we are navigating to + * @param from - Route location where we are navigating from + * @param savedPosition - saved position if it exists, `null` otherwise + */ + ( + to: RouteLocationNormalized, + from: RouteLocationNormalizedLoaded, + savedPosition: _ScrollPositionNormalized | null + ): Awaitable +} + export interface ScrollPositionElement extends ScrollToOptions { /** * A valid CSS selector. Note some characters must be escaped in id selectors (https://mathiasbynens.be/notes/css-escapes). diff --git a/packages/router/src/types/utils.ts b/packages/router/src/types/utils.ts index 2d443f69e..34881aca5 100644 --- a/packages/router/src/types/utils.ts +++ b/packages/router/src/types/utils.ts @@ -94,3 +94,5 @@ export type _AlphaNumeric = | '8' | '9' | '_' + +export type Awaitable = T | Promise From 9af14417b821b0e993d1296a25833627470dd56c Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Mon, 23 Dec 2024 11:44:01 +0100 Subject: [PATCH 24/40] chore: comments --- packages/router/src/experimental/router.ts | 20 +++++++++---------- .../router/src/new-route-resolver/matcher.ts | 2 -- packages/router/src/types/index.ts | 6 ++++-- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/router/src/experimental/router.ts b/packages/router/src/experimental/router.ts index ac44da155..cc73bc9c3 100644 --- a/packages/router/src/experimental/router.ts +++ b/packages/router/src/experimental/router.ts @@ -126,8 +126,8 @@ export interface EXPERIMENTAL_RouterOptions_Base extends PathParserOptions { * } * ``` */ - scrollBehavior?: RouterScrollBehavior + /** * Custom implementation to parse a query. See its counterpart, * {@link EXPERIMENTAL_RouterOptions_Base.stringifyQuery}. @@ -145,26 +145,27 @@ export interface EXPERIMENTAL_RouterOptions_Base extends PathParserOptions { * }) * ``` */ - parseQuery?: typeof originalParseQuery + /** * Custom implementation to stringify a query object. Should not prepend a leading `?`. - * {@link EXPERIMENTAL_RouterOptions_Base.parseQuery | parseQuery} counterpart to handle query parsing. + * {@link parseQuery} counterpart to handle query parsing. */ stringifyQuery?: typeof originalStringifyQuery + /** * Default class applied to active {@link RouterLink}. If none is provided, * `router-link-active` will be applied. */ - linkActiveClass?: string + /** * Default class applied to exact active {@link RouterLink}. If none is provided, * `router-link-exact-active` will be applied. */ - linkExactActiveClass?: string + /** * Default class applied to non-active {@link RouterLink}. If none is provided, * `router-link-inactive` will be applied. @@ -191,7 +192,7 @@ export interface EXPERIMENTAL_RouterOptions } /** - * Router instance. + * Router base instance. * @experimental This version is not stable, it's meant to replace {@link Router} in the future. */ export interface EXPERIMENTAL_Router_Base { @@ -1161,7 +1162,6 @@ export function experimental_createRouter( } // Initialization and Errors - let readyHandlers = useCallbacks<_OnReadyCallback>() let errorListeners = useCallbacks<_ErrorListener>() let ready: boolean @@ -1206,9 +1206,9 @@ export function experimental_createRouter( * only be called once, otherwise does nothing. * @param err - optional error */ - function markAsReady(err: E): E - function markAsReady(): void - function markAsReady(err?: E): E | void { + function markAsReady(err: E): E + function markAsReady(): void + function markAsReady(err?: E): E | void { if (!ready) { // still not ready if an error happened ready = !err diff --git a/packages/router/src/new-route-resolver/matcher.ts b/packages/router/src/new-route-resolver/matcher.ts index cabb296ef..54ea4cba1 100644 --- a/packages/router/src/new-route-resolver/matcher.ts +++ b/packages/router/src/new-route-resolver/matcher.ts @@ -325,8 +325,6 @@ export function createCompiledMatcher( // } parsedParams = { ...pathParams, ...queryParams, ...hashParams } - - if (parsedParams) break } catch (e) { // for debugging tests // console.log('❌ ERROR matching', e) diff --git a/packages/router/src/types/index.ts b/packages/router/src/types/index.ts index bf8f7fb6c..caa95a6e2 100644 --- a/packages/router/src/types/index.ts +++ b/packages/router/src/types/index.ts @@ -185,7 +185,7 @@ export type RouteComponent = Component | DefineComponent */ export type RawRouteComponent = RouteComponent | Lazy -// TODO: could this be moved to matcher? +// TODO: could this be moved to matcher? YES, it's on the way /** * Internal type for common properties among all kind of {@link RouteRecordRaw}. */ @@ -278,7 +278,9 @@ export interface RouteRecordSingleView extends _RouteRecordBase { } /** - * Route Record defining one single component with a nested view. + * Route Record defining one single component with a nested view. Differently + * from {@link RouteRecordSingleView}, this record has children and allows a + * `redirect` option. */ export interface RouteRecordSingleViewWithChildren extends _RouteRecordBase { /** From e080bff685673d1f19a444287d9a11c739e634aa Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Mon, 23 Dec 2024 15:28:35 +0100 Subject: [PATCH 25/40] refactor: simplify router resolve --- packages/router/src/experimental/router.ts | 309 ++++++++---------- .../src/new-route-resolver/matcher-pattern.ts | 15 +- .../matcher-resolve.spec.ts | 6 +- .../src/new-route-resolver/matcher.spec.ts | 57 +++- .../src/new-route-resolver/matcher.test-d.ts | 36 +- .../router/src/new-route-resolver/matcher.ts | 185 +++++++---- .../new-route-resolver/matchers/test-utils.ts | 6 +- packages/router/src/types/typeGuards.ts | 4 +- 8 files changed, 359 insertions(+), 259 deletions(-) diff --git a/packages/router/src/experimental/router.ts b/packages/router/src/experimental/router.ts index cc73bc9c3..3c79d2c69 100644 --- a/packages/router/src/experimental/router.ts +++ b/packages/router/src/experimental/router.ts @@ -9,11 +9,11 @@ import { import { nextTick, shallowReactive, + ShallowRef, shallowRef, unref, warn, type App, - type Ref, } from 'vue' import { RouterLink } from '../RouterLink' import { RouterView } from '../RouterView' @@ -23,10 +23,13 @@ import { type RouterHistory, } from '../history/common' import type { PathParserOptions } from '../matcher' -import type { RouteResolver } from '../new-route-resolver/matcher' +import type { + NEW_LocationResolved, + NEW_MatcherRecord, + NEW_MatcherRecordRaw, + NEW_RouterMatcher, +} from '../new-route-resolver/matcher' import { - LocationQuery, - normalizeQuery, parseQuery as originalParseQuery, stringifyQuery as originalStringifyQuery, } from '../query' @@ -48,6 +51,7 @@ import type { RouteLocationAsRelative, RouteLocationAsRelativeTyped, RouteLocationAsString, + RouteLocationGeneric, RouteLocationNormalized, RouteLocationNormalizedLoaded, RouteLocationRaw, @@ -60,19 +64,17 @@ import { isRouteLocation, isRouteName, Lazy, - MatcherLocationRaw, RouteLocationOptions, - type RouteRecordRaw, + RouteMeta, } from '../types' import { useCallbacks } from '../utils/callbacks' import { isSameRouteLocation, parseURL, START_LOCATION_NORMALIZED, - stringifyURL, } from '../location' import { applyToParams, assign, isArray, isBrowser, noop } from '../utils' -import { decode, encodeHash, encodeParam } from '../encoding' +import { decode, encodeParam } from '../encoding' import { extractChangingRecords, extractComponentsGuards, @@ -177,18 +179,19 @@ export interface EXPERIMENTAL_RouterOptions_Base extends PathParserOptions { * Options to initialize an experimental {@link EXPERIMENTAL_Router} instance. * @experimental */ -export interface EXPERIMENTAL_RouterOptions - extends EXPERIMENTAL_RouterOptions_Base { +export interface EXPERIMENTAL_RouterOptions< + TMatcherRecord extends NEW_MatcherRecord +> extends EXPERIMENTAL_RouterOptions_Base { /** * Initial list of routes that should be added to the router. */ - routes?: Readonly + routes?: Readonly /** * Matcher to use to resolve routes. * @experimental */ - matcher: RouteResolver + matcher: NEW_RouterMatcher } /** @@ -199,7 +202,7 @@ export interface EXPERIMENTAL_Router_Base { /** * Current {@link RouteLocationNormalized} */ - readonly currentRoute: Ref + readonly currentRoute: ShallowRef /** * Allows turning off the listening of history events. This is a low level api for micro-frontend. @@ -207,7 +210,7 @@ export interface EXPERIMENTAL_Router_Base { listening: boolean /** - * Add a new {@link RouteRecordRaw | route record} as the child of an existing route. + * Add a new {@link EXPERIMENTAL_RouteRecordRaw | route record} as the child of an existing route. * * @param parentName - Parent Route Record where `route` should be appended at * @param route - Route Record to add @@ -215,10 +218,10 @@ export interface EXPERIMENTAL_Router_Base { addRoute( // NOTE: it could be `keyof RouteMap` but the point of dynamic routes is not knowing the routes at build parentName: NonNullable, - route: RouteRecordRaw + route: TRouteRecordRaw ): () => void /** - * Add a new {@link RouteRecordRaw | route record} to the router. + * Add a new {@link EXPERIMENTAL_RouteRecordRaw | route record} to the router. * * @param route - Route Record to add */ @@ -385,23 +388,45 @@ export interface EXPERIMENTAL_Router_Base { install(app: App): void } -export interface EXPERIMENTAL_Router - extends EXPERIMENTAL_Router_Base { +export interface EXPERIMENTAL_Router< + TRouteRecordRaw, // extends NEW_MatcherRecordRaw, + TRouteRecord extends NEW_MatcherRecord +> extends EXPERIMENTAL_Router_Base { /** * Original options object passed to create the Router */ - readonly options: EXPERIMENTAL_RouterOptions + readonly options: EXPERIMENTAL_RouterOptions +} + +export interface EXPERIMENTAL_RouteRecordRaw extends NEW_MatcherRecordRaw { + /** + * Arbitrary data attached to the record. + */ + meta?: RouteMeta +} + +// TODO: is it worth to have 2 types for the undefined values? +export interface EXPERIMENTAL_RouteRecordNormalized extends NEW_MatcherRecord { + meta: RouteMeta } -interface EXPERIMENTAL_RouteRecordRaw {} -interface EXPERIMENTAL_RouteRecord {} +function normalizeRouteRecord( + record: EXPERIMENTAL_RouteRecordRaw +): EXPERIMENTAL_RouteRecordNormalized { + // FIXME: implementation + return { + name: __DEV__ ? Symbol('anonymous route record') : Symbol(), + meta: {}, + ...record, + } +} export function experimental_createRouter( - options: EXPERIMENTAL_RouterOptions< - EXPERIMENTAL_RouteRecordRaw, - EXPERIMENTAL_RouteRecord - > -): EXPERIMENTAL_Router { + options: EXPERIMENTAL_RouterOptions +): EXPERIMENTAL_Router< + EXPERIMENTAL_RouteRecordRaw, + EXPERIMENTAL_RouteRecordNormalized +> { const { matcher, parseQuery = originalParseQuery, @@ -438,11 +463,14 @@ export function experimental_createRouter( applyToParams.bind(null, decode) function addRoute( - parentOrRoute: NonNullable | RouteRecordRaw, - route?: RouteRecordRaw + parentOrRoute: + | NonNullable + | EXPERIMENTAL_RouteRecordRaw, + route?: EXPERIMENTAL_RouteRecordRaw ) { let parent: Parameters<(typeof matcher)['addRoute']>[1] | undefined - let record: RouteRecordRaw + let rawRecord: EXPERIMENTAL_RouteRecordRaw + if (isRouteName(parentOrRoute)) { parent = matcher.getMatcher(parentOrRoute) if (__DEV__ && !parent) { @@ -453,12 +481,19 @@ export function experimental_createRouter( route ) } - record = route! + rawRecord = route! } else { - record = parentOrRoute + rawRecord = parentOrRoute } - return matcher.addRoute(record, parent) + const addedRecord = matcher.addRoute( + normalizeRouteRecord(rawRecord), + parent + ) + + return () => { + matcher.removeRoute(addedRecord) + } } function removeRoute(name: NonNullable) { @@ -471,7 +506,7 @@ export function experimental_createRouter( } function getRoutes() { - return matcher.getMatchers().map(routeMatcher => routeMatcher.record) + return matcher.getMatchers() } function hasRoute(name: NonNullable): boolean { @@ -485,139 +520,66 @@ export function experimental_createRouter( // const resolve: Router['resolve'] = (rawLocation: RouteLocationRaw, currentLocation) => { // const objectLocation = routerLocationAsObject(rawLocation) // we create a copy to modify it later - currentLocation = assign({}, currentLocation || currentRoute.value) - if (typeof rawLocation === 'string') { - const locationNormalized = parseURL( - parseQuery, - rawLocation, - currentLocation.path - ) - const matchedRoute = matcher.resolve( - { path: locationNormalized.path }, - currentLocation - ) + // TODO: in the experimental version, allow configuring this + currentLocation = + currentLocation && assign({}, currentLocation || currentRoute.value) + // currentLocation = assign({}, currentLocation || currentRoute.value) - const href = routerHistory.createHref(locationNormalized.fullPath) - if (__DEV__) { - if (href.startsWith('//')) - warn( - `Location "${rawLocation}" resolved to "${href}". A resolved location cannot start with multiple slashes.` - ) - else if (!matchedRoute.matched.length) { - warn(`No match found for location with path "${rawLocation}"`) - } + if (__DEV__) { + if (!isRouteLocation(rawLocation)) { + warn( + `router.resolve() was passed an invalid location. This will fail in production.\n- Location:`, + rawLocation + ) + return resolve({}) } - // locationNormalized is always a new object - return assign(locationNormalized, matchedRoute, { - params: decodeParams(matchedRoute.params), - hash: decode(locationNormalized.hash), - redirectedFrom: undefined, - href, - }) - } - - if (__DEV__ && !isRouteLocation(rawLocation)) { - warn( - `router.resolve() was passed an invalid location. This will fail in production.\n- Location:`, - rawLocation - ) - return resolve({}) - } - - let matcherLocation: MatcherLocationRaw - - // path could be relative in object as well - if (rawLocation.path != null) { if ( - __DEV__ && - 'params' in rawLocation && - !('name' in rawLocation) && - // @ts-expect-error: the type is never - Object.keys(rawLocation.params).length + typeof rawLocation === 'object' && + rawLocation.hash?.startsWith('#') ) { warn( - `Path "${rawLocation.path}" was passed with params but they will be ignored. Use a named route alongside params instead.` + `A \`hash\` should always start with the character "#". Replace "${hash}" with "#${hash}".` ) } - matcherLocation = assign({}, rawLocation, { - path: parseURL(parseQuery, rawLocation.path, currentLocation.path).path, - }) - } else { - // remove any nullish param - const targetParams = assign({}, rawLocation.params) - for (const key in targetParams) { - if (targetParams[key] == null) { - delete targetParams[key] - } - } - // pass encoded values to the matcher, so it can produce encoded path and fullPath - matcherLocation = assign({}, rawLocation, { - params: encodeParams(targetParams), - }) - // current location params are decoded, we need to encode them in case the - // matcher merges the params - currentLocation.params = encodeParams(currentLocation.params) } - const matchedRoute = matcher.resolve(matcherLocation, currentLocation) - const hash = rawLocation.hash || '' - - if (__DEV__ && hash && !hash.startsWith('#')) { - warn( - `A \`hash\` should always start with the character "#". Replace "${hash}" with "#${hash}".` - ) - } - - // the matcher might have merged current location params, so - // we need to run the decoding again - matchedRoute.params = normalizeParams(decodeParams(matchedRoute.params)) - - const fullPath = stringifyURL( - stringifyQuery, - assign({}, rawLocation, { - hash: encodeHash(hash), - path: matchedRoute.path, - }) + // FIXME: is this achieved by matchers? + // remove any nullish param + // if ('params' in rawLocation) { + // const targetParams = assign({}, rawLocation.params) + // for (const key in targetParams) { + // if (targetParams[key] == null) { + // delete targetParams[key] + // } + // } + // rawLocation.params = targetParams + // } + + const matchedRoute = matcher.resolve( + rawLocation, + currentLocation satisfies NEW_LocationResolved ) + const href = routerHistory.createHref(matchedRoute.fullPath) - const href = routerHistory.createHref(fullPath) if (__DEV__) { if (href.startsWith('//')) { warn( `Location "${rawLocation}" resolved to "${href}". A resolved location cannot start with multiple slashes.` ) - } else if (!matchedRoute.matched.length) { - warn( - `No match found for location with path "${ - rawLocation.path != null ? rawLocation.path : rawLocation - }"` - ) + } + if (!matchedRoute.matched.length) { + warn(`No match found for location with path "${rawLocation}"`) } } - return assign( - { - fullPath, - // keep the hash encoded so fullPath is effectively path + encodedQuery + - // hash - hash, - query: - // if the user is using a custom query lib like qs, we might have - // nested objects, so we keep the query as is, meaning it can contain - // numbers at `$route.query`, but at the point, the user will have to - // use their own type anyway. - // https://github.com/vuejs/router/issues/328#issuecomment-649481567 - stringifyQuery === originalStringifyQuery - ? normalizeQuery(rawLocation.query) - : ((rawLocation.query || {}) as LocationQuery), - }, - matchedRoute, - { - redirectedFrom: undefined, - href, - } - ) + // TODO: can this be refactored at the very end + // matchedRoute is always a new object + return assign(matchedRoute, { + redirectedFrom: undefined, + href, + meta: mergeMetaFields(matchedRoute.matched), + }) } function locationAsObject( @@ -648,7 +610,7 @@ export function experimental_createRouter( } function replace(to: RouteLocationRaw) { - return push(assign(locationAsObject(to), { replace: true })) + return pushWithRedirect(to, true) } function handleRedirectRecord(to: RouteLocation): RouteLocationRaw | void { @@ -700,14 +662,14 @@ export function experimental_createRouter( function pushWithRedirect( to: RouteLocationRaw | RouteLocation, + _replace?: boolean, redirectedFrom?: RouteLocation ): Promise { const targetLocation: RouteLocation = (pendingLocation = resolve(to)) const from = currentRoute.value const data: HistoryState | undefined = (to as RouteLocationOptions).state const force: boolean | undefined = (to as RouteLocationOptions).force - // to could be a string where `replace` is a function - const replace = (to as RouteLocationOptions).replace === true + const replace = (to as RouteLocationOptions).replace ?? _replace const shouldRedirect = handleRedirectRecord(targetLocation) @@ -719,8 +681,8 @@ export function experimental_createRouter( ? assign({}, data, shouldRedirect.state) : data, force, - replace, }), + replace, // keep original redirectedFrom if it exists redirectedFrom || targetLocation ) @@ -790,20 +752,15 @@ export function experimental_createRouter( return pushWithRedirect( // keep options - assign( - { - // preserve an existing replacement but allow the redirect to override it - replace, - }, - locationAsObject(failure.to), - { - state: - typeof failure.to === 'object' - ? assign({}, data, failure.to.state) - : data, - force, - } - ), + assign(locationAsObject(failure.to), { + state: + typeof failure.to === 'object' + ? assign({}, data, failure.to.state) + : data, + force, + }), + // preserve an existing replacement but allow the redirect to override it + replace, // preserve the original redirectedFrom if any redirectedFrom || toLocation ) @@ -842,6 +799,7 @@ export function experimental_createRouter( function runWithContext(fn: () => T): T { const app: App | undefined = installedApps.values().next().value + // TODO: remove safeguard and bump required minimum version of Vue // support Vue < 3.3 return app && typeof app.runWithContext === 'function' ? app.runWithContext(fn) @@ -1044,7 +1002,8 @@ export function experimental_createRouter( const shouldRedirect = handleRedirectRecord(toLocation) if (shouldRedirect) { pushWithRedirect( - assign(shouldRedirect, { replace: true, force: true }), + assign(shouldRedirect, { force: true }), + true, toLocation ).catch(noop) return @@ -1088,6 +1047,7 @@ export function experimental_createRouter( assign(locationAsObject((error as NavigationRedirectError).to), { force: true, }), + undefined, toLocation // avoid an uncaught rejection, let push call triggerError ) @@ -1250,7 +1210,10 @@ export function experimental_createRouter( let started: boolean | undefined const installedApps = new Set() - const router: Router = { + const router: EXPERIMENTAL_Router< + EXPERIMENTAL_RouteRecordRaw, + EXPERIMENTAL_RouteRecordNormalized + > = { currentRoute, listening: true, @@ -1280,6 +1243,7 @@ export function experimental_createRouter( app.component('RouterLink', RouterLink) app.component('RouterView', RouterView) + // @ts-expect-error: FIXME: refactor with new types once it's possible app.config.globalProperties.$router = router Object.defineProperty(app.config.globalProperties, '$route', { enumerable: true, @@ -1311,6 +1275,7 @@ export function experimental_createRouter( }) } + // @ts-expect-error: FIXME: refactor with new types once it's possible app.provide(routerKey, router) app.provide(routeLocationKey, shallowReactive(reactiveRoute)) app.provide(routerViewLocationKey, currentRoute) @@ -1334,6 +1299,7 @@ export function experimental_createRouter( // TODO: this probably needs to be updated so it can be used by vue-termui if ((__DEV__ || __FEATURE_PROD_DEVTOOLS__) && isBrowser) { + // @ts-expect-error: FIXME: refactor with new types once it's possible addDevtools(app, router, matcher) } }, @@ -1349,3 +1315,14 @@ export function experimental_createRouter( return router } + +/** + * Merge meta fields of an array of records + * + * @param matched - array of matched records + */ +function mergeMetaFields( + matched: NEW_LocationResolved['matched'] +): RouteMeta { + return assign({} as RouteMeta, ...matched.map(r => r.meta)) +} diff --git a/packages/router/src/new-route-resolver/matcher-pattern.ts b/packages/router/src/new-route-resolver/matcher-pattern.ts index ad582bb8d..c627c3bff 100644 --- a/packages/router/src/new-route-resolver/matcher-pattern.ts +++ b/packages/router/src/new-route-resolver/matcher-pattern.ts @@ -1,20 +1,7 @@ -import { decode, MatcherName, MatcherQueryParams } from './matcher' +import { decode, MatcherQueryParams } from './matcher' import { EmptyParams, MatcherParamsFormatted } from './matcher-location' import { miss } from './matchers/errors' -export interface MatcherPattern { - /** - * Name of the matcher. Unique across all matchers. - */ - name: MatcherName - - path: MatcherPatternPath - query?: MatcherPatternQuery - hash?: MatcherPatternHash - - parent?: MatcherPattern -} - export interface MatcherPatternParams_Base< TIn = string, TOut extends MatcherParamsFormatted = MatcherParamsFormatted diff --git a/packages/router/src/new-route-resolver/matcher-resolve.spec.ts b/packages/router/src/new-route-resolver/matcher-resolve.spec.ts index b4799bbec..91fb8fb24 100644 --- a/packages/router/src/new-route-resolver/matcher-resolve.spec.ts +++ b/packages/router/src/new-route-resolver/matcher-resolve.spec.ts @@ -8,7 +8,7 @@ import { mockWarn } from '../../__tests__/vitest-mock-warn' import { createCompiledMatcher, MatcherLocationRaw, - MatcherRecordRaw, + NEW_MatcherRecordRaw, NEW_LocationResolved, } from './matcher' import { PathParams, tokensToParser } from '../matcher/pathParserRanker' @@ -24,7 +24,7 @@ const components = { default: component } function compileRouteRecord( record: RouteRecordRaw, parentRecord?: RouteRecordRaw -): MatcherRecordRaw { +): NEW_MatcherRecordRaw { // we adapt the path to ensure they are absolute // TODO: aliases? they could be handled directly in the path matcher const path = record.path.startsWith('/') @@ -100,7 +100,7 @@ describe('RouterMatcher.resolve', () => { | `/${string}` = START_LOCATION ) { const records = (Array.isArray(record) ? record : [record]).map( - (record): MatcherRecordRaw => compileRouteRecord(record) + (record): NEW_MatcherRecordRaw => compileRouteRecord(record) ) const matcher = createCompiledMatcher() for (const record of records) { diff --git a/packages/router/src/new-route-resolver/matcher.spec.ts b/packages/router/src/new-route-resolver/matcher.spec.ts index 22fb3e511..07695b598 100644 --- a/packages/router/src/new-route-resolver/matcher.spec.ts +++ b/packages/router/src/new-route-resolver/matcher.spec.ts @@ -6,12 +6,12 @@ import { } from './matcher' import { MatcherPatternParams_Base, - MatcherPattern, MatcherPatternPath, MatcherPatternQuery, MatcherPatternPathStatic, MatcherPatternPathDynamic, } from './matcher-pattern' +import { NEW_MatcherRecord } from './matcher' import { miss } from './matchers/errors' import { EmptyParams } from './matcher-location' @@ -72,12 +72,17 @@ const ANY_HASH_PATTERN_MATCHER: MatcherPatternParams_Base< const EMPTY_PATH_ROUTE = { name: 'no params', path: EMPTY_PATH_PATTERN_MATCHER, -} satisfies MatcherPattern +} satisfies NEW_MatcherRecord + +const ANY_PATH_ROUTE = { + name: 'any path', + path: ANY_PATH_PATTERN_MATCHER, +} satisfies NEW_MatcherRecord const USER_ID_ROUTE = { name: 'user-id', path: USER_ID_PATH_PATTERN_MATCHER, -} satisfies MatcherPattern +} satisfies NEW_MatcherRecord describe('RouterMatcher', () => { describe('new matchers', () => { @@ -135,6 +140,20 @@ describe('RouterMatcher', () => { const matcher = createCompiledMatcher() matcher.addRoute(USER_ID_ROUTE) }) + + it('removes static path', () => { + const matcher = createCompiledMatcher() + matcher.addRoute(EMPTY_PATH_ROUTE) + matcher.removeRoute(EMPTY_PATH_ROUTE) + // Add assertions to verify the route was removed + }) + + it('removes dynamic path', () => { + const matcher = createCompiledMatcher() + matcher.addRoute(USER_ID_ROUTE) + matcher.removeRoute(USER_ID_ROUTE) + // Add assertions to verify the route was removed + }) }) describe('resolve()', () => { @@ -293,5 +312,37 @@ describe('RouterMatcher', () => { }) }) }) + + describe('encoding', () => { + it('handles encoded string path', () => { + const matcher = createCompiledMatcher([ANY_PATH_ROUTE]) + console.log(matcher.resolve('/%23%2F%3F')) + expect(matcher.resolve('/%23%2F%3F')).toMatchObject({ + fullPath: '/%23%2F%3F', + path: '/%23%2F%3F', + query: {}, + params: {}, + hash: '', + }) + }) + + it('decodes query from a string', () => { + const matcher = createCompiledMatcher([ANY_PATH_ROUTE]) + expect(matcher.resolve('/foo?foo=%23%2F%3F')).toMatchObject({ + path: '/foo', + fullPath: '/foo?foo=%23%2F%3F', + query: { foo: '#/?' }, + }) + }) + + it('decodes hash from a string', () => { + const matcher = createCompiledMatcher([ANY_PATH_ROUTE]) + expect(matcher.resolve('/foo#h-%23%2F%3F')).toMatchObject({ + path: '/foo', + fullPath: '/foo#h-%23%2F%3F', + hash: '#h-#/?', + }) + }) + }) }) }) diff --git a/packages/router/src/new-route-resolver/matcher.test-d.ts b/packages/router/src/new-route-resolver/matcher.test-d.ts index a60874518..8ea5b771d 100644 --- a/packages/router/src/new-route-resolver/matcher.test-d.ts +++ b/packages/router/src/new-route-resolver/matcher.test-d.ts @@ -1,14 +1,23 @@ import { describe, expectTypeOf, it } from 'vitest' -import { NEW_LocationResolved, RouteResolver } from './matcher' +import { + NEW_LocationResolved, + NEW_MatcherRecordRaw, + NEW_RouterMatcher, +} from './matcher' +import { EXPERIMENTAL_RouteRecordNormalized } from '../experimental/router' describe('Matcher', () => { - const matcher: RouteResolver = {} as any + type TMatcherRecordRaw = NEW_MatcherRecordRaw + type TMatcherRecord = EXPERIMENTAL_RouteRecordNormalized + + const matcher: NEW_RouterMatcher = + {} as any describe('matcher.resolve()', () => { it('resolves absolute string locations', () => { - expectTypeOf( - matcher.resolve('/foo') - ).toEqualTypeOf() + expectTypeOf(matcher.resolve('/foo')).toEqualTypeOf< + NEW_LocationResolved + >() }) it('fails on non absolute location without a currentLocation', () => { @@ -18,14 +27,14 @@ describe('Matcher', () => { it('resolves relative locations', () => { expectTypeOf( - matcher.resolve('foo', {} as NEW_LocationResolved) - ).toEqualTypeOf() + matcher.resolve('foo', {} as NEW_LocationResolved) + ).toEqualTypeOf>() }) it('resolved named locations', () => { - expectTypeOf( - matcher.resolve({ name: 'foo', params: {} }) - ).toEqualTypeOf() + expectTypeOf(matcher.resolve({ name: 'foo', params: {} })).toEqualTypeOf< + NEW_LocationResolved + >() }) it('fails on object relative location without a currentLocation', () => { @@ -35,8 +44,11 @@ describe('Matcher', () => { it('resolves object relative locations with a currentLocation', () => { expectTypeOf( - matcher.resolve({ params: { id: 1 } }, {} as NEW_LocationResolved) - ).toEqualTypeOf() + matcher.resolve( + { params: { id: 1 } }, + {} as NEW_LocationResolved + ) + ).toEqualTypeOf>() }) }) diff --git a/packages/router/src/new-route-resolver/matcher.ts b/packages/router/src/new-route-resolver/matcher.ts index 54ea4cba1..69ddc5540 100644 --- a/packages/router/src/new-route-resolver/matcher.ts +++ b/packages/router/src/new-route-resolver/matcher.ts @@ -5,7 +5,6 @@ import { stringifyQuery, } from '../query' import type { - MatcherPattern, MatcherPatternHash, MatcherPatternPath, MatcherPatternQuery, @@ -20,6 +19,7 @@ import type { MatcherLocationAsRelative, MatcherParamsFormatted, } from './matcher-location' +import { _RouteRecordProps } from '../typed-routes' /** * Allowed types for a matcher name. @@ -28,12 +28,17 @@ export type MatcherName = string | symbol /** * Manage and resolve routes. Also handles the encoding, decoding, parsing and serialization of params, query, and hash. + * `TMatcherRecordRaw` represents the raw record type passed to {@link addRoute}. + * `TMatcherRecord` represents the normalized record type. */ -export interface RouteResolver { +export interface NEW_RouterMatcher { /** * Resolves an absolute location (like `/path/to/somewhere`). */ - resolve(absoluteLocation: `/${string}`): NEW_LocationResolved + resolve( + absoluteLocation: `/${string}`, + currentLocation?: undefined | NEW_LocationResolved + ): NEW_LocationResolved /** * Resolves a string location relative to another location. A relative location can be `./same-folder`, @@ -41,24 +46,28 @@ export interface RouteResolver { */ resolve( relativeLocation: string, - currentLocation: NEW_LocationResolved - ): NEW_LocationResolved + currentLocation: NEW_LocationResolved + ): NEW_LocationResolved /** * Resolves a location by its name. Any required params or query must be passed in the `options` argument. */ - resolve(location: MatcherLocationAsNamed): NEW_LocationResolved + resolve( + location: MatcherLocationAsNamed + ): NEW_LocationResolved /** * Resolves a location by its absolute path (starts with `/`). Any required query must be passed. * @param location - The location to resolve. */ - resolve(location: MatcherLocationAsPathAbsolute): NEW_LocationResolved + resolve( + location: MatcherLocationAsPathAbsolute + ): NEW_LocationResolved resolve( location: MatcherLocationAsPathRelative, - currentLocation: NEW_LocationResolved - ): NEW_LocationResolved + currentLocation: NEW_LocationResolved + ): NEW_LocationResolved // NOTE: in practice, this overload can cause bugs. It's better to use named locations @@ -68,42 +77,28 @@ export interface RouteResolver { */ resolve( relativeLocation: MatcherLocationAsRelative, - currentLocation: NEW_LocationResolved - ): NEW_LocationResolved + currentLocation: NEW_LocationResolved + ): NEW_LocationResolved - addRoute(matcher: Matcher, parent?: MatcherNormalized): MatcherNormalized - removeRoute(matcher: MatcherNormalized): void + addRoute(matcher: TMatcherRecordRaw, parent?: TMatcherRecord): TMatcherRecord + removeRoute(matcher: TMatcherRecord): void clearRoutes(): void /** * Get a list of all matchers. * Previously named `getRoutes()` */ - getMatchers(): MatcherNormalized[] + getMatchers(): TMatcherRecord[] /** * Get a matcher by its name. * Previously named `getRecordMatcher()` */ - getMatcher(name: MatcherName): MatcherNormalized | undefined + getMatcher(name: MatcherName): TMatcherRecord | undefined } -type MatcherResolveArgs = - | [absoluteLocation: `/${string}`] - | [relativeLocation: string, currentLocation: NEW_LocationResolved] - | [absoluteLocation: MatcherLocationAsPathAbsolute] - | [ - relativeLocation: MatcherLocationAsPathRelative, - currentLocation: NEW_LocationResolved - ] - | [location: MatcherLocationAsNamed] - | [ - relativeLocation: MatcherLocationAsRelative, - currentLocation: NEW_LocationResolved - ] - /** - * Allowed location objects to be passed to {@link RouteResolver['resolve']} + * Allowed location objects to be passed to {@link NEW_RouterMatcher['resolve']} */ export type MatcherLocationRaw = | `/${string}` @@ -127,16 +122,18 @@ export interface NEW_Matcher_Dynamic { type TODO = any -export interface NEW_LocationResolved { - name: MatcherName - fullPath: string - path: string +export interface NEW_LocationResolved { + // FIXME: remove `undefined` + name: MatcherName | undefined // TODO: generics? params: MatcherParamsFormatted + + fullPath: string + path: string query: LocationQuery hash: string - matched: TODO[] + matched: TMatched[] } export type MatcherPathParamsValue = string | null | string[] @@ -221,24 +218,69 @@ const encodeQueryValue: FnStableNull = // // for ts // value => (value == null ? null : _encodeQueryKey(value)) +/** + * Common properties for a location that couldn't be matched. This ensures + * having the same name while having a `path`, `query` and `hash` that change. + */ export const NO_MATCH_LOCATION = { name: __DEV__ ? Symbol('no-match') : Symbol(), params: {}, matched: [], -} satisfies Omit +} satisfies Omit< + NEW_LocationResolved, + 'path' | 'hash' | 'query' | 'fullPath' +> // FIXME: later on, the MatcherRecord should be compatible with RouteRecordRaw (which can miss a path, have children, etc) -export interface MatcherRecordRaw { +/** + * Experiment new matcher record base type. + * + * @experimental + */ +export interface NEW_MatcherRecordRaw { + path: MatcherPatternPath + query?: MatcherPatternQuery + hash?: MatcherPatternHash + + // NOTE: matchers do not handle `redirect` the redirect option, the router + // does. They can still match the correct record but they will let the router + // retrigger a whole navigation to the new location. + + // TODO: probably as `aliasOf`. Maybe a different format with the path, query and has matchers? + /** + * Aliases for the record. Allows defining extra paths that will behave like a + * copy of the record. Allows having paths shorthands like `/users/:id` and + * `/u/:id`. All `alias` and `path` values must share the same params. + */ + // alias?: string | string[] + + /** + * Name for the route record. Must be unique. Will be set to `Symbol()` if + * not set. + */ name?: MatcherName - path: MatcherPatternPath + /** + * Array of nested routes. + */ + children?: NEW_MatcherRecordRaw[] +} - query?: MatcherPatternQuery +/** + * Normalized version of a {@link NEW_MatcherRecordRaw} record. + */ +export interface NEW_MatcherRecord { + /** + * Name of the matcher. Unique across all matchers. + */ + name: MatcherName + path: MatcherPatternPath + query?: MatcherPatternQuery hash?: MatcherPatternHash - children?: MatcherRecordRaw[] + parent?: NEW_MatcherRecord } /** @@ -268,9 +310,9 @@ export function pathEncoded( /** * Build the `matched` array of a record that includes all parent records from the root to the current one. */ -function buildMatched(record: MatcherPattern): MatcherPattern[] { - const matched: MatcherPattern[] = [] - let node: MatcherPattern | undefined = record +function buildMatched(record: NEW_MatcherRecord): NEW_MatcherRecord[] { + const matched: NEW_MatcherRecord[] = [] + let node: NEW_MatcherRecord | undefined = record while (node) { matched.unshift(node) node = node.parent @@ -279,10 +321,10 @@ function buildMatched(record: MatcherPattern): MatcherPattern[] { } export function createCompiledMatcher( - records: MatcherRecordRaw[] = [] -): RouteResolver { + records: NEW_MatcherRecordRaw[] = [] +): NEW_RouterMatcher { // TODO: we also need an array that has the correct order - const matchers = new Map() + const matchers = new Map() // TODO: allow custom encode/decode functions // const encodeParams = applyToParams.bind(null, encodeParam) @@ -294,7 +336,30 @@ export function createCompiledMatcher( // ) // const decodeQuery = transformObject.bind(null, decode, decode) - function resolve(...args: MatcherResolveArgs): NEW_LocationResolved { + // NOTE: because of the overloads, we need to manually type the arguments + type MatcherResolveArgs = + | [ + absoluteLocation: `/${string}`, + currentLocation?: undefined | NEW_LocationResolved + ] + | [ + relativeLocation: string, + currentLocation: NEW_LocationResolved + ] + | [absoluteLocation: MatcherLocationAsPathAbsolute] + | [ + relativeLocation: MatcherLocationAsPathRelative, + currentLocation: NEW_LocationResolved + ] + | [location: MatcherLocationAsNamed] + | [ + relativeLocation: MatcherLocationAsRelative, + currentLocation: NEW_LocationResolved + ] + + function resolve( + ...args: MatcherResolveArgs + ): NEW_LocationResolved { const [location, currentLocation] = args // string location, e.g. '/foo', '../bar', 'baz', '?page=1' @@ -302,8 +367,10 @@ export function createCompiledMatcher( // parseURL handles relative paths const url = parseURL(parseQuery, location, currentLocation?.path) - let matcher: MatcherPattern | undefined - let matched: NEW_LocationResolved['matched'] | undefined + let matcher: NEW_MatcherRecord | undefined + let matched: + | NEW_LocationResolved['matched'] + | undefined let parsedParams: MatcherParamsFormatted | null | undefined for (matcher of matchers.values()) { @@ -360,18 +427,22 @@ export function createCompiledMatcher( `Cannot resolve an unnamed relative location without a current location. This will throw in production.`, location ) + const query = normalizeQuery(location.query) + const hash = location.hash ?? '' + const path = location.path ?? '/' return { ...NO_MATCH_LOCATION, - fullPath: '/', - path: '/', - query: {}, - hash: '', + fullPath: stringifyURL(stringifyQuery, { path, query, hash }), + path, + query, + hash, } } // either one of them must be defined and is catched by the dev only warn above const name = location.name ?? currentLocation!.name - const matcher = matchers.get(name) + // FIXME: remove once name cannot be null + const matcher = name != null && matchers.get(name) if (!matcher) { throw new Error(`Matcher "${String(location.name)}" not found`) } @@ -404,10 +475,10 @@ export function createCompiledMatcher( } } - function addRoute(record: MatcherRecordRaw, parent?: MatcherPattern) { + function addRoute(record: NEW_MatcherRecordRaw, parent?: NEW_MatcherRecord) { const name = record.name ?? (__DEV__ ? Symbol('unnamed-route') : Symbol()) // FIXME: proper normalization of the record - const normalizedRecord: MatcherPattern = { + const normalizedRecord: NEW_MatcherRecord = { ...record, name, parent, @@ -420,7 +491,7 @@ export function createCompiledMatcher( addRoute(record) } - function removeRoute(matcher: MatcherPattern) { + function removeRoute(matcher: NEW_MatcherRecord) { matchers.delete(matcher.name) // TODO: delete children and aliases } diff --git a/packages/router/src/new-route-resolver/matchers/test-utils.ts b/packages/router/src/new-route-resolver/matchers/test-utils.ts index f40ce00a5..e922e7217 100644 --- a/packages/router/src/new-route-resolver/matchers/test-utils.ts +++ b/packages/router/src/new-route-resolver/matchers/test-utils.ts @@ -3,8 +3,8 @@ import { MatcherPatternPath, MatcherPatternQuery, MatcherPatternParams_Base, - MatcherPattern, } from '../matcher-pattern' +import { NEW_MatcherRecord } from '../matcher' import { miss } from './errors' export const ANY_PATH_PATTERN_MATCHER: MatcherPatternPath<{ @@ -68,9 +68,9 @@ export const ANY_HASH_PATTERN_MATCHER: MatcherPatternParams_Base< export const EMPTY_PATH_ROUTE = { name: 'no params', path: EMPTY_PATH_PATTERN_MATCHER, -} satisfies MatcherPattern +} satisfies NEW_MatcherRecord export const USER_ID_ROUTE = { name: 'user-id', path: USER_ID_PATH_PATTERN_MATCHER, -} satisfies MatcherPattern +} satisfies NEW_MatcherRecord diff --git a/packages/router/src/types/typeGuards.ts b/packages/router/src/types/typeGuards.ts index ba30bd9b6..9ecbf3a3c 100644 --- a/packages/router/src/types/typeGuards.ts +++ b/packages/router/src/types/typeGuards.ts @@ -4,6 +4,8 @@ export function isRouteLocation(route: any): route is RouteLocationRaw { return typeof route === 'string' || (route && typeof route === 'object') } -export function isRouteName(name: any): name is RouteRecordNameGeneric { +export function isRouteName( + name: unknown +): name is NonNullable { return typeof name === 'string' || typeof name === 'symbol' } From 047858de6e6f4f5f48c29895b7d84f52d2e8c11c Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Tue, 24 Dec 2024 10:45:50 +0100 Subject: [PATCH 26/40] chore: wip encoding --- packages/router/src/location.ts | 21 ++++++- .../src/new-route-resolver/matcher.spec.ts | 61 ++++++++++++------- .../router/src/new-route-resolver/matcher.ts | 6 +- 3 files changed, 62 insertions(+), 26 deletions(-) diff --git a/packages/router/src/location.ts b/packages/router/src/location.ts index af2444de3..2f8b1ba86 100644 --- a/packages/router/src/location.ts +++ b/packages/router/src/location.ts @@ -3,7 +3,7 @@ import { RouteParamValue, RouteParamsGeneric } from './types' import { RouteRecord } from './matcher/types' import { warn } from './warning' import { isArray } from './utils' -import { decode } from './encoding' +import { decode, encodeHash } from './encoding' import { RouteLocation, RouteLocationNormalizedLoaded } from './typed-routes' /** @@ -94,6 +94,25 @@ export function parseURL( } } +/** + * Creates a `fullPath` property from the `path`, `query` and `hash` properties + * + * @param stringifyQuery - custom function to stringify the query object. It should handle encoding values + * @param path - An encdoded path + * @param query - A decoded query object + * @param hash - A decoded hash + * @returns a valid `fullPath` + */ +export function NEW_stringifyURL( + stringifyQuery: (query?: LocationQueryRaw) => string, + path: LocationPartial['path'], + query?: LocationPartial['query'], + hash: LocationPartial['hash'] = '' +): string { + const searchText = stringifyQuery(query) + return path + (searchText && '?') + searchText + encodeHash(hash) +} + /** * Stringifies a URL object * diff --git a/packages/router/src/new-route-resolver/matcher.spec.ts b/packages/router/src/new-route-resolver/matcher.spec.ts index 07695b598..a0c59e6d9 100644 --- a/packages/router/src/new-route-resolver/matcher.spec.ts +++ b/packages/router/src/new-route-resolver/matcher.spec.ts @@ -314,33 +314,50 @@ describe('RouterMatcher', () => { }) describe('encoding', () => { - it('handles encoded string path', () => { - const matcher = createCompiledMatcher([ANY_PATH_ROUTE]) - console.log(matcher.resolve('/%23%2F%3F')) - expect(matcher.resolve('/%23%2F%3F')).toMatchObject({ - fullPath: '/%23%2F%3F', - path: '/%23%2F%3F', - query: {}, - params: {}, - hash: '', + const matcher = createCompiledMatcher([ANY_PATH_ROUTE]) + describe('decodes', () => { + it('handles encoded string path', () => { + expect(matcher.resolve('/%23%2F%3F')).toMatchObject({ + fullPath: '/%23%2F%3F', + path: '/%23%2F%3F', + query: {}, + params: {}, + hash: '', + }) }) - }) - it('decodes query from a string', () => { - const matcher = createCompiledMatcher([ANY_PATH_ROUTE]) - expect(matcher.resolve('/foo?foo=%23%2F%3F')).toMatchObject({ - path: '/foo', - fullPath: '/foo?foo=%23%2F%3F', - query: { foo: '#/?' }, + it('decodes query from a string', () => { + expect(matcher.resolve('/foo?foo=%23%2F%3F')).toMatchObject({ + path: '/foo', + fullPath: '/foo?foo=%23%2F%3F', + query: { foo: '#/?' }, + }) + }) + + it('decodes hash from a string', () => { + expect(matcher.resolve('/foo#%22')).toMatchObject({ + path: '/foo', + fullPath: '/foo#%22', + hash: '#"', + }) }) }) - it('decodes hash from a string', () => { - const matcher = createCompiledMatcher([ANY_PATH_ROUTE]) - expect(matcher.resolve('/foo#h-%23%2F%3F')).toMatchObject({ - path: '/foo', - fullPath: '/foo#h-%23%2F%3F', - hash: '#h-#/?', + describe('encodes', () => { + it('encodes the query', () => { + expect( + matcher.resolve({ path: '/foo', query: { foo: '"' } }) + ).toMatchObject({ + fullPath: '/foo?foo=%22', + query: { foo: '"' }, + }) + }) + + it('encodes the hash', () => { + expect(matcher.resolve({ path: '/foo', hash: '#"' })).toMatchObject({ + fullPath: '/foo#%22', + hash: '#"', + }) }) }) }) diff --git a/packages/router/src/new-route-resolver/matcher.ts b/packages/router/src/new-route-resolver/matcher.ts index 69ddc5540..c0ba504ce 100644 --- a/packages/router/src/new-route-resolver/matcher.ts +++ b/packages/router/src/new-route-resolver/matcher.ts @@ -11,7 +11,7 @@ import type { } from './matcher-pattern' import { warn } from '../warning' import { encodeQueryValue as _encodeQueryValue, encodeParam } from '../encoding' -import { parseURL, stringifyURL } from '../location' +import { parseURL, NEW_stringifyURL } from '../location' import type { MatcherLocationAsNamed, MatcherLocationAsPathAbsolute, @@ -432,7 +432,7 @@ export function createCompiledMatcher( const path = location.path ?? '/' return { ...NO_MATCH_LOCATION, - fullPath: stringifyURL(stringifyQuery, { path, query, hash }), + fullPath: NEW_stringifyURL(stringifyQuery, path, query, hash), path, query, hash, @@ -465,7 +465,7 @@ export function createCompiledMatcher( return { name, - fullPath: stringifyURL(stringifyQuery, { path, query, hash }), + fullPath: NEW_stringifyURL(stringifyQuery, path, query, hash), path, query, hash, From a8e01d3fa2cf92640fa8eebded124604675ec3ce Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Tue, 7 Jan 2025 14:22:51 +0100 Subject: [PATCH 27/40] chore: small fix --- packages/router/src/experimental/router.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/router/src/experimental/router.ts b/packages/router/src/experimental/router.ts index 3c79d2c69..95762e899 100644 --- a/packages/router/src/experimental/router.ts +++ b/packages/router/src/experimental/router.ts @@ -536,10 +536,10 @@ export function experimental_createRouter( if ( typeof rawLocation === 'object' && - rawLocation.hash?.startsWith('#') + !rawLocation.hash?.startsWith('#') ) { warn( - `A \`hash\` should always start with the character "#". Replace "${hash}" with "#${hash}".` + `A \`hash\` should always start with the character "#". Replace "${rawLocation.hash}" with "#${rawLocation.hash}".` ) } } From 0efc390436d674aaeae1f3c0656dec10e304f627 Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Wed, 8 Jan 2025 10:48:35 +0100 Subject: [PATCH 28/40] refactor: rename matcher to resolver --- packages/router/src/experimental/router.ts | 20 ++-- packages/router/src/matcher/index.ts | 27 ++--- .../router/src/matcher/pathParserRanker.ts | 5 + .../router/src/new-route-resolver/index.ts | 2 +- .../new-route-resolver/matcher-location.ts | 2 +- .../src/new-route-resolver/matcher-pattern.ts | 2 +- .../matcher-resolve.spec.ts | 75 ++---------- .../src/new-route-resolver/matcher.spec.ts | 16 +-- .../src/new-route-resolver/matcher.test-d.ts | 6 +- .../new-route-resolver/matchers/test-utils.ts | 2 +- .../{matcher.ts => resolver.ts} | 107 ++++++++++-------- packages/router/src/utils/index.ts | 12 ++ 12 files changed, 121 insertions(+), 155 deletions(-) rename packages/router/src/new-route-resolver/{matcher.ts => resolver.ts} (85%) diff --git a/packages/router/src/experimental/router.ts b/packages/router/src/experimental/router.ts index 95762e899..9cf885cc1 100644 --- a/packages/router/src/experimental/router.ts +++ b/packages/router/src/experimental/router.ts @@ -27,8 +27,8 @@ import type { NEW_LocationResolved, NEW_MatcherRecord, NEW_MatcherRecordRaw, - NEW_RouterMatcher, -} from '../new-route-resolver/matcher' + NEW_RouterResolver, +} from '../new-route-resolver/resolver' import { parseQuery as originalParseQuery, stringifyQuery as originalStringifyQuery, @@ -51,7 +51,6 @@ import type { RouteLocationAsRelative, RouteLocationAsRelativeTyped, RouteLocationAsString, - RouteLocationGeneric, RouteLocationNormalized, RouteLocationNormalizedLoaded, RouteLocationRaw, @@ -191,7 +190,7 @@ export interface EXPERIMENTAL_RouterOptions< * Matcher to use to resolve routes. * @experimental */ - matcher: NEW_RouterMatcher + matcher: NEW_RouterResolver } /** @@ -407,6 +406,9 @@ export interface EXPERIMENTAL_RouteRecordRaw extends NEW_MatcherRecordRaw { // TODO: is it worth to have 2 types for the undefined values? export interface EXPERIMENTAL_RouteRecordNormalized extends NEW_MatcherRecord { + /** + * Arbitrary data attached to the record. + */ meta: RouteMeta } @@ -468,7 +470,7 @@ export function experimental_createRouter( | EXPERIMENTAL_RouteRecordRaw, route?: EXPERIMENTAL_RouteRecordRaw ) { - let parent: Parameters<(typeof matcher)['addRoute']>[1] | undefined + let parent: Parameters<(typeof matcher)['addMatcher']>[1] | undefined let rawRecord: EXPERIMENTAL_RouteRecordRaw if (isRouteName(parentOrRoute)) { @@ -486,20 +488,20 @@ export function experimental_createRouter( rawRecord = parentOrRoute } - const addedRecord = matcher.addRoute( + const addedRecord = matcher.addMatcher( normalizeRouteRecord(rawRecord), parent ) return () => { - matcher.removeRoute(addedRecord) + matcher.removeMatcher(addedRecord) } } function removeRoute(name: NonNullable) { const recordMatcher = matcher.getMatcher(name) if (recordMatcher) { - matcher.removeRoute(recordMatcher) + matcher.removeMatcher(recordMatcher) } else if (__DEV__) { warn(`Cannot remove non-existent route "${String(name)}"`) } @@ -1219,7 +1221,7 @@ export function experimental_createRouter( addRoute, removeRoute, - clearRoutes: matcher.clearRoutes, + clearRoutes: matcher.clearMatchers, hasRoute, getRoutes, resolve, diff --git a/packages/router/src/matcher/index.ts b/packages/router/src/matcher/index.ts index fe951f7ad..1a2541d72 100644 --- a/packages/router/src/matcher/index.ts +++ b/packages/router/src/matcher/index.ts @@ -14,10 +14,13 @@ import type { _PathParserOptions, } from './pathParserRanker' -import { comparePathParserScore } from './pathParserRanker' +import { + comparePathParserScore, + PATH_PARSER_OPTIONS_DEFAULTS, +} from './pathParserRanker' import { warn } from '../warning' -import { assign, noop } from '../utils' +import { assign, mergeOptions, noop } from '../utils' import type { RouteRecordNameGeneric, _RouteRecordProps } from '../typed-routes' /** @@ -64,8 +67,8 @@ export function createRouterMatcher( NonNullable, RouteRecordMatcher >() - globalOptions = mergeOptions( - { strict: false, end: true, sensitive: false } as PathParserOptions, + globalOptions = mergeOptions( + PATH_PARSER_OPTIONS_DEFAULTS, globalOptions ) @@ -429,7 +432,7 @@ export function normalizeRouteRecord( * components. Also accept a boolean for components. * @param record */ -function normalizeRecordProps( +export function normalizeRecordProps( record: RouteRecordRaw ): Record { const propsObject = {} as Record @@ -472,18 +475,6 @@ function mergeMetaFields(matched: MatcherLocation['matched']) { ) } -function mergeOptions( - defaults: T, - partialOptions: Partial -): T { - const options = {} as T - for (const key in defaults) { - options[key] = key in partialOptions ? partialOptions[key]! : defaults[key] - } - - return options -} - type ParamKey = RouteRecordMatcher['keys'][number] function isSameParam(a: ParamKey, b: ParamKey): boolean { @@ -521,7 +512,7 @@ function checkSameParams(a: RouteRecordMatcher, b: RouteRecordMatcher) { * @param mainNormalizedRecord - RouteRecordNormalized * @param parent - RouteRecordMatcher */ -function checkChildMissingNameWithEmptyPath( +export function checkChildMissingNameWithEmptyPath( mainNormalizedRecord: RouteRecordNormalized, parent?: RouteRecordMatcher ) { diff --git a/packages/router/src/matcher/pathParserRanker.ts b/packages/router/src/matcher/pathParserRanker.ts index 81b077642..b2c0b40a0 100644 --- a/packages/router/src/matcher/pathParserRanker.ts +++ b/packages/router/src/matcher/pathParserRanker.ts @@ -367,3 +367,8 @@ function isLastScoreNegative(score: PathParser['score']): boolean { const last = score[score.length - 1] return score.length > 0 && last[last.length - 1] < 0 } +export const PATH_PARSER_OPTIONS_DEFAULTS: PathParserOptions = { + strict: false, + end: true, + sensitive: false, +} diff --git a/packages/router/src/new-route-resolver/index.ts b/packages/router/src/new-route-resolver/index.ts index 17910f62f..4c07b32cc 100644 --- a/packages/router/src/new-route-resolver/index.ts +++ b/packages/router/src/new-route-resolver/index.ts @@ -1 +1 @@ -export { createCompiledMatcher } from './matcher' +export { createCompiledMatcher } from './resolver' diff --git a/packages/router/src/new-route-resolver/matcher-location.ts b/packages/router/src/new-route-resolver/matcher-location.ts index b9ca1ab0c..f597df07f 100644 --- a/packages/router/src/new-route-resolver/matcher-location.ts +++ b/packages/router/src/new-route-resolver/matcher-location.ts @@ -1,5 +1,5 @@ import type { LocationQueryRaw } from '../query' -import type { MatcherName } from './matcher' +import type { MatcherName } from './resolver' /** * Generic object of params that can be passed to a matcher. diff --git a/packages/router/src/new-route-resolver/matcher-pattern.ts b/packages/router/src/new-route-resolver/matcher-pattern.ts index c627c3bff..0f7d8c192 100644 --- a/packages/router/src/new-route-resolver/matcher-pattern.ts +++ b/packages/router/src/new-route-resolver/matcher-pattern.ts @@ -1,4 +1,4 @@ -import { decode, MatcherQueryParams } from './matcher' +import { decode, MatcherQueryParams } from './resolver' import { EmptyParams, MatcherParamsFormatted } from './matcher-location' import { miss } from './matchers/errors' diff --git a/packages/router/src/new-route-resolver/matcher-resolve.spec.ts b/packages/router/src/new-route-resolver/matcher-resolve.spec.ts index 91fb8fb24..6f9914af2 100644 --- a/packages/router/src/new-route-resolver/matcher-resolve.spec.ts +++ b/packages/router/src/new-route-resolver/matcher-resolve.spec.ts @@ -1,6 +1,5 @@ import { createRouterMatcher, normalizeRouteRecord } from '../matcher' import { RouteComponent, RouteRecordRaw, MatcherLocation } from '../types' -import { MatcherLocationNormalizedLoose } from '../../__tests__/utils' import { defineComponent } from 'vue' import { START_LOCATION_NORMALIZED } from '../location' import { describe, expect, it } from 'vitest' @@ -10,7 +9,8 @@ import { MatcherLocationRaw, NEW_MatcherRecordRaw, NEW_LocationResolved, -} from './matcher' + NEW_MatcherRecord, +} from './resolver' import { PathParams, tokensToParser } from '../matcher/pathParserRanker' import { tokenizePath } from '../matcher/pathTokenizer' import { miss } from './matchers/errors' @@ -63,22 +63,23 @@ function compileRouteRecord( describe('RouterMatcher.resolve', () => { mockWarn() - type Matcher = ReturnType + type Matcher = ReturnType type MatcherResolvedLocation = ReturnType - const START_LOCATION: NEW_LocationResolved = { + const START_LOCATION: MatcherResolvedLocation = { name: Symbol('START'), - fullPath: '/', - path: '/', params: {}, + path: '/', + fullPath: '/', query: {}, hash: '', matched: [], + // meta: {}, } function isMatcherLocationResolved( location: unknown - ): location is NEW_LocationResolved { + ): location is NEW_LocationResolved { return !!( location && typeof location === 'object' && @@ -95,16 +96,16 @@ describe('RouterMatcher.resolve', () => { toLocation: MatcherLocationRaw, expectedLocation: Partial, fromLocation: - | NEW_LocationResolved + | NEW_LocationResolved | Exclude | `/${string}` = START_LOCATION ) { const records = (Array.isArray(record) ? record : [record]).map( (record): NEW_MatcherRecordRaw => compileRouteRecord(record) ) - const matcher = createCompiledMatcher() + const matcher = createCompiledMatcher() for (const record of records) { - matcher.addRoute(record) + matcher.addMatcher(record) } const resolved: MatcherResolvedLocation = { @@ -137,60 +138,6 @@ describe('RouterMatcher.resolve', () => { }) } - function _assertRecordMatch( - record: RouteRecordRaw | RouteRecordRaw[], - location: MatcherLocationRaw, - resolved: Partial, - start: MatcherLocation = START_LOCATION_NORMALIZED - ) { - record = Array.isArray(record) ? record : [record] - const matcher = createRouterMatcher(record, {}) - - if (!('meta' in resolved)) { - resolved.meta = record[0].meta || {} - } - - if (!('name' in resolved)) { - resolved.name = undefined - } - - // add location if provided as it should be the same value - if ('path' in location && !('path' in resolved)) { - resolved.path = location.path - } - - if ('redirect' in record) { - throw new Error('not handled') - } else { - // use one single record - if (!resolved.matched) resolved.matched = record.map(normalizeRouteRecord) - // allow passing an expect.any(Array) - else if (Array.isArray(resolved.matched)) - resolved.matched = resolved.matched.map(m => ({ - ...normalizeRouteRecord(m as any), - aliasOf: m.aliasOf, - })) - } - - // allows not passing params - resolved.params = - resolved.params || ('params' in location ? location.params : {}) - - const startCopy: MatcherLocation = { - ...start, - matched: start.matched.map(m => ({ - ...normalizeRouteRecord(m), - aliasOf: m.aliasOf, - })) as MatcherLocation['matched'], - } - - // make matched non enumerable - Object.defineProperty(startCopy, 'matched', { enumerable: false }) - - const result = matcher.resolve(location, startCopy) - expect(result).toEqual(resolved) - } - /** * * @param record - Record or records we are testing the matcher against diff --git a/packages/router/src/new-route-resolver/matcher.spec.ts b/packages/router/src/new-route-resolver/matcher.spec.ts index a0c59e6d9..335ddb83d 100644 --- a/packages/router/src/new-route-resolver/matcher.spec.ts +++ b/packages/router/src/new-route-resolver/matcher.spec.ts @@ -3,7 +3,7 @@ import { createCompiledMatcher, NO_MATCH_LOCATION, pathEncoded, -} from './matcher' +} from './resolver' import { MatcherPatternParams_Base, MatcherPatternPath, @@ -11,7 +11,7 @@ import { MatcherPatternPathStatic, MatcherPatternPathDynamic, } from './matcher-pattern' -import { NEW_MatcherRecord } from './matcher' +import { NEW_MatcherRecord } from './resolver' import { miss } from './matchers/errors' import { EmptyParams } from './matcher-location' @@ -133,25 +133,25 @@ describe('RouterMatcher', () => { describe('adding and removing', () => { it('add static path', () => { const matcher = createCompiledMatcher() - matcher.addRoute(EMPTY_PATH_ROUTE) + matcher.addMatcher(EMPTY_PATH_ROUTE) }) it('adds dynamic path', () => { const matcher = createCompiledMatcher() - matcher.addRoute(USER_ID_ROUTE) + matcher.addMatcher(USER_ID_ROUTE) }) it('removes static path', () => { const matcher = createCompiledMatcher() - matcher.addRoute(EMPTY_PATH_ROUTE) - matcher.removeRoute(EMPTY_PATH_ROUTE) + matcher.addMatcher(EMPTY_PATH_ROUTE) + matcher.removeMatcher(EMPTY_PATH_ROUTE) // Add assertions to verify the route was removed }) it('removes dynamic path', () => { const matcher = createCompiledMatcher() - matcher.addRoute(USER_ID_ROUTE) - matcher.removeRoute(USER_ID_ROUTE) + matcher.addMatcher(USER_ID_ROUTE) + matcher.removeMatcher(USER_ID_ROUTE) // Add assertions to verify the route was removed }) }) diff --git a/packages/router/src/new-route-resolver/matcher.test-d.ts b/packages/router/src/new-route-resolver/matcher.test-d.ts index 8ea5b771d..26060c3a6 100644 --- a/packages/router/src/new-route-resolver/matcher.test-d.ts +++ b/packages/router/src/new-route-resolver/matcher.test-d.ts @@ -2,15 +2,15 @@ import { describe, expectTypeOf, it } from 'vitest' import { NEW_LocationResolved, NEW_MatcherRecordRaw, - NEW_RouterMatcher, -} from './matcher' + NEW_RouterResolver, +} from './resolver' import { EXPERIMENTAL_RouteRecordNormalized } from '../experimental/router' describe('Matcher', () => { type TMatcherRecordRaw = NEW_MatcherRecordRaw type TMatcherRecord = EXPERIMENTAL_RouteRecordNormalized - const matcher: NEW_RouterMatcher = + const matcher: NEW_RouterResolver = {} as any describe('matcher.resolve()', () => { diff --git a/packages/router/src/new-route-resolver/matchers/test-utils.ts b/packages/router/src/new-route-resolver/matchers/test-utils.ts index e922e7217..250efafd9 100644 --- a/packages/router/src/new-route-resolver/matchers/test-utils.ts +++ b/packages/router/src/new-route-resolver/matchers/test-utils.ts @@ -4,7 +4,7 @@ import { MatcherPatternQuery, MatcherPatternParams_Base, } from '../matcher-pattern' -import { NEW_MatcherRecord } from '../matcher' +import { NEW_MatcherRecord } from '../resolver' import { miss } from './errors' export const ANY_PATH_PATTERN_MATCHER: MatcherPatternPath<{ diff --git a/packages/router/src/new-route-resolver/matcher.ts b/packages/router/src/new-route-resolver/resolver.ts similarity index 85% rename from packages/router/src/new-route-resolver/matcher.ts rename to packages/router/src/new-route-resolver/resolver.ts index c0ba504ce..17ceecf02 100644 --- a/packages/router/src/new-route-resolver/matcher.ts +++ b/packages/router/src/new-route-resolver/resolver.ts @@ -27,11 +27,13 @@ import { _RouteRecordProps } from '../typed-routes' export type MatcherName = string | symbol /** - * Manage and resolve routes. Also handles the encoding, decoding, parsing and serialization of params, query, and hash. - * `TMatcherRecordRaw` represents the raw record type passed to {@link addRoute}. - * `TMatcherRecord` represents the normalized record type. + * Manage and resolve routes. Also handles the encoding, decoding, parsing and + * serialization of params, query, and hash. + * + * - `TMatcherRecordRaw` represents the raw record type passed to {@link addMatcher}. + * - `TMatcherRecord` represents the normalized record type returned by {@link getMatchers}. */ -export interface NEW_RouterMatcher { +export interface NEW_RouterResolver { /** * Resolves an absolute location (like `/path/to/somewhere`). */ @@ -80,9 +82,26 @@ export interface NEW_RouterMatcher { currentLocation: NEW_LocationResolved ): NEW_LocationResolved - addRoute(matcher: TMatcherRecordRaw, parent?: TMatcherRecord): TMatcherRecord - removeRoute(matcher: TMatcherRecord): void - clearRoutes(): void + /** + * Add a matcher record. Previously named `addRoute()`. + * @param matcher - The matcher record to add. + * @param parent - The parent matcher record if this is a child. + */ + addMatcher( + matcher: TMatcherRecordRaw, + parent?: TMatcherRecord + ): TMatcherRecord + + /** + * Remove a matcher by its name. Previously named `removeRoute()`. + * @param matcher - The matcher (returned by {@link addMatcher}) to remove. + */ + removeMatcher(matcher: TMatcherRecord): void + + /** + * Remove all matcher records. Prevoisly named `clearRoutes()`. + */ + clearMatchers(): void /** * Get a list of all matchers. @@ -98,7 +117,7 @@ export interface NEW_RouterMatcher { } /** - * Allowed location objects to be passed to {@link NEW_RouterMatcher['resolve']} + * Allowed location objects to be passed to {@link NEW_RouterResolver['resolve']} */ export type MatcherLocationRaw = | `/${string}` @@ -108,20 +127,6 @@ export type MatcherLocationRaw = | MatcherLocationAsPathRelative | MatcherLocationAsRelative -/** - * Matcher capable of adding and removing routes at runtime. - */ -export interface NEW_Matcher_Dynamic { - addRoute(record: TODO, parent?: TODO): () => void - - removeRoute(record: TODO): void - removeRoute(name: MatcherName): void - - clearRoutes(): void -} - -type TODO = any - export interface NEW_LocationResolved { // FIXME: remove `undefined` name: MatcherName | undefined @@ -234,7 +239,7 @@ export const NO_MATCH_LOCATION = { // FIXME: later on, the MatcherRecord should be compatible with RouteRecordRaw (which can miss a path, have children, etc) /** - * Experiment new matcher record base type. + * Experimental new matcher record base type. * * @experimental */ @@ -267,10 +272,7 @@ export interface NEW_MatcherRecordRaw { children?: NEW_MatcherRecordRaw[] } -/** - * Normalized version of a {@link NEW_MatcherRecordRaw} record. - */ -export interface NEW_MatcherRecord { +export interface NEW_MatcherRecordBase { /** * Name of the matcher. Unique across all matchers. */ @@ -280,9 +282,15 @@ export interface NEW_MatcherRecord { query?: MatcherPatternQuery hash?: MatcherPatternHash - parent?: NEW_MatcherRecord + parent?: T } +/** + * Normalized version of a {@link NEW_MatcherRecordRaw} record. + */ +export interface NEW_MatcherRecord + extends NEW_MatcherRecordBase {} + /** * Tagged template helper to encode params into a path. Doesn't work with null */ @@ -310,9 +318,9 @@ export function pathEncoded( /** * Build the `matched` array of a record that includes all parent records from the root to the current one. */ -function buildMatched(record: NEW_MatcherRecord): NEW_MatcherRecord[] { - const matched: NEW_MatcherRecord[] = [] - let node: NEW_MatcherRecord | undefined = record +function buildMatched>(record: T): T[] { + const matched: T[] = [] + let node: T | undefined = record while (node) { matched.unshift(node) node = node.parent @@ -320,11 +328,13 @@ function buildMatched(record: NEW_MatcherRecord): NEW_MatcherRecord[] { return matched } -export function createCompiledMatcher( +export function createCompiledMatcher< + TMatcherRecord extends NEW_MatcherRecordBase +>( records: NEW_MatcherRecordRaw[] = [] -): NEW_RouterMatcher { +): NEW_RouterResolver { // TODO: we also need an array that has the correct order - const matchers = new Map() + const matchers = new Map() // TODO: allow custom encode/decode functions // const encodeParams = applyToParams.bind(null, encodeParam) @@ -340,26 +350,26 @@ export function createCompiledMatcher( type MatcherResolveArgs = | [ absoluteLocation: `/${string}`, - currentLocation?: undefined | NEW_LocationResolved + currentLocation?: undefined | NEW_LocationResolved ] | [ relativeLocation: string, - currentLocation: NEW_LocationResolved + currentLocation: NEW_LocationResolved ] | [absoluteLocation: MatcherLocationAsPathAbsolute] | [ relativeLocation: MatcherLocationAsPathRelative, - currentLocation: NEW_LocationResolved + currentLocation: NEW_LocationResolved ] | [location: MatcherLocationAsNamed] | [ relativeLocation: MatcherLocationAsRelative, - currentLocation: NEW_LocationResolved + currentLocation: NEW_LocationResolved ] function resolve( ...args: MatcherResolveArgs - ): NEW_LocationResolved { + ): NEW_LocationResolved { const [location, currentLocation] = args // string location, e.g. '/foo', '../bar', 'baz', '?page=1' @@ -367,10 +377,8 @@ export function createCompiledMatcher( // parseURL handles relative paths const url = parseURL(parseQuery, location, currentLocation?.path) - let matcher: NEW_MatcherRecord | undefined - let matched: - | NEW_LocationResolved['matched'] - | undefined + let matcher: TMatcherRecord | undefined + let matched: NEW_LocationResolved['matched'] | undefined let parsedParams: MatcherParamsFormatted | null | undefined for (matcher of matchers.values()) { @@ -475,10 +483,11 @@ export function createCompiledMatcher( } } - function addRoute(record: NEW_MatcherRecordRaw, parent?: NEW_MatcherRecord) { + function addRoute(record: NEW_MatcherRecordRaw, parent?: TMatcherRecord) { const name = record.name ?? (__DEV__ ? Symbol('unnamed-route') : Symbol()) // FIXME: proper normalization of the record - const normalizedRecord: NEW_MatcherRecord = { + // @ts-expect-error: we are not properly normalizing the record yet + const normalizedRecord: TMatcherRecord = { ...record, name, parent, @@ -491,7 +500,7 @@ export function createCompiledMatcher( addRoute(record) } - function removeRoute(matcher: NEW_MatcherRecord) { + function removeRoute(matcher: TMatcherRecord) { matchers.delete(matcher.name) // TODO: delete children and aliases } @@ -511,9 +520,9 @@ export function createCompiledMatcher( return { resolve, - addRoute, - removeRoute, - clearRoutes, + addMatcher: addRoute, + removeMatcher: removeRoute, + clearMatchers: clearRoutes, getMatcher, getMatchers, } diff --git a/packages/router/src/utils/index.ts b/packages/router/src/utils/index.ts index a7c42f4cf..c6d622095 100644 --- a/packages/router/src/utils/index.ts +++ b/packages/router/src/utils/index.ts @@ -58,3 +58,15 @@ export const noop = () => {} */ export const isArray: (arg: ArrayLike | any) => arg is ReadonlyArray = Array.isArray + +export function mergeOptions( + defaults: T, + partialOptions: Partial +): T { + const options = {} as T + for (const key in defaults) { + options[key] = key in partialOptions ? partialOptions[key]! : defaults[key] + } + + return options +} From 2d17e5b5d2e72165ddc5802b0fc0d5e13af39edb Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Wed, 8 Jan 2025 10:54:03 +0100 Subject: [PATCH 29/40] chore: remove unused --- packages/router/src/experimental/router.ts | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/packages/router/src/experimental/router.ts b/packages/router/src/experimental/router.ts index 9cf885cc1..09e7a46af 100644 --- a/packages/router/src/experimental/router.ts +++ b/packages/router/src/experimental/router.ts @@ -56,7 +56,6 @@ import type { RouteLocationRaw, RouteLocationResolved, RouteMap, - RouteParams, RouteRecordNameGeneric, } from '../typed-routes' import { @@ -72,8 +71,7 @@ import { parseURL, START_LOCATION_NORMALIZED, } from '../location' -import { applyToParams, assign, isArray, isBrowser, noop } from '../utils' -import { decode, encodeParam } from '../encoding' +import { assign, isArray, isBrowser, noop } from '../utils' import { extractChangingRecords, extractComponentsGuards, @@ -455,15 +453,6 @@ export function experimental_createRouter( history.scrollRestoration = 'manual' } - const normalizeParams = applyToParams.bind( - null, - paramValue => '' + paramValue - ) - const encodeParams = applyToParams.bind(null, encodeParam) - const decodeParams: (params: RouteParams | undefined) => RouteParams = - // @ts-expect-error: intentionally avoid the type check - applyToParams.bind(null, decode) - function addRoute( parentOrRoute: | NonNullable From 1471a07e4044b1a49385e28e0050834510743bfd Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Wed, 8 Jan 2025 11:53:53 +0100 Subject: [PATCH 30/40] test: fix ts errors --- .../matcher-resolve.spec.ts | 291 ++++++++++-------- 1 file changed, 162 insertions(+), 129 deletions(-) diff --git a/packages/router/src/new-route-resolver/matcher-resolve.spec.ts b/packages/router/src/new-route-resolver/matcher-resolve.spec.ts index 6f9914af2..4ea1a00cd 100644 --- a/packages/router/src/new-route-resolver/matcher-resolve.spec.ts +++ b/packages/router/src/new-route-resolver/matcher-resolve.spec.ts @@ -1,7 +1,7 @@ -import { createRouterMatcher, normalizeRouteRecord } from '../matcher' -import { RouteComponent, RouteRecordRaw, MatcherLocation } from '../types' +import { createRouterMatcher } from '../matcher' +import { RouteComponent, RouteRecordRaw } from '../types' import { defineComponent } from 'vue' -import { START_LOCATION_NORMALIZED } from '../location' +import { stringifyURL } from '../location' import { describe, expect, it } from 'vitest' import { mockWarn } from '../../__tests__/vitest-mock-warn' import { @@ -15,6 +15,12 @@ import { PathParams, tokensToParser } from '../matcher/pathParserRanker' import { tokenizePath } from '../matcher/pathTokenizer' import { miss } from './matchers/errors' import { MatcherPatternPath } from './matcher-pattern' +import { EXPERIMENTAL_RouteRecordRaw } from '../experimental/router' +import { stringifyQuery } from '../query' +import { + MatcherLocationAsNamed, + MatcherLocationAsPathAbsolute, +} from './matcher-location' // for raw route record const component: RouteComponent = defineComponent({}) @@ -89,33 +95,58 @@ describe('RouterMatcher.resolve', () => { ) } + function isExperimentalRouteRecordRaw( + record: Record + ): record is EXPERIMENTAL_RouteRecordRaw { + return typeof record.path !== 'string' + } + // TODO: rework with object param for clarity function assertRecordMatch( - record: RouteRecordRaw | RouteRecordRaw[], - toLocation: MatcherLocationRaw, + record: + | EXPERIMENTAL_RouteRecordRaw + | EXPERIMENTAL_RouteRecordRaw[] + | RouteRecordRaw + | RouteRecordRaw[], + toLocation: Exclude | `/${string}`, expectedLocation: Partial, fromLocation: | NEW_LocationResolved - | Exclude - | `/${string}` = START_LOCATION + // absolute locations only that can be resolved for convenience + | `/${string}` + | MatcherLocationAsNamed + | MatcherLocationAsPathAbsolute = START_LOCATION ) { const records = (Array.isArray(record) ? record : [record]).map( - (record): NEW_MatcherRecordRaw => compileRouteRecord(record) + (record): EXPERIMENTAL_RouteRecordRaw => + isExperimentalRouteRecordRaw(record) + ? record + : compileRouteRecord(record) ) const matcher = createCompiledMatcher() for (const record of records) { matcher.addMatcher(record) } - const resolved: MatcherResolvedLocation = { + const path = + typeof toLocation === 'string' ? toLocation : toLocation.path || '/' + + const resolved: Omit = { // FIXME: to add later // meta: records[0].meta || {}, - path: - typeof toLocation === 'string' ? toLocation : toLocation.path || '/', + path, + query: {}, + hash: '', name: expect.any(Symbol) as symbol, - matched: [], // FIXME: build up + // must non enumerable + // matched: [], params: (typeof toLocation === 'object' && toLocation.params) || {}, + fullPath: stringifyURL(stringifyQuery, { + path: expectedLocation.path || '/', + query: expectedLocation.query, + hash: expectedLocation.hash, + }), ...expectedLocation, } @@ -123,17 +154,24 @@ describe('RouterMatcher.resolve', () => { writable: true, configurable: true, enumerable: false, + // FIXME: build it value: [], }) - fromLocation = isMatcherLocationResolved(fromLocation) + const resolvedFrom = isMatcherLocationResolved(fromLocation) ? fromLocation - : matcher.resolve(fromLocation) + : // FIXME: is this a ts bug? + // @ts-expect-error + matcher.resolve(fromLocation) - expect(matcher.resolve(toLocation, fromLocation)).toMatchObject({ - // avoid undesired properties - query: {}, - hash: '', + expect( + matcher.resolve( + // FIXME: WTF? + // @ts-expect-error + toLocation, + resolvedFrom + ) + ).toMatchObject({ ...resolved, }) } @@ -147,10 +185,15 @@ describe('RouterMatcher.resolve', () => { */ function assertErrorMatch( record: RouteRecordRaw | RouteRecordRaw[], - location: MatcherLocationRaw, - start: MatcherLocation = START_LOCATION_NORMALIZED + toLocation: Exclude | `/${string}`, + fromLocation: + | NEW_LocationResolved + // absolute locations only + | `/${string}` + | MatcherLocationAsNamed + | MatcherLocationAsPathAbsolute = START_LOCATION ) { - assertRecordMatch(record, location, {}, start) + assertRecordMatch(record, toLocation, {}, fromLocation) } describe.skip('LocationAsPath', () => { @@ -353,7 +396,7 @@ describe('RouterMatcher.resolve', () => { } assertRecordMatch( Parent, - { name: 'child_b' }, + {}, { name: 'child_b', path: '/foo/parent/b', @@ -368,15 +411,13 @@ describe('RouterMatcher.resolve', () => { }, { params: { optional: 'foo' }, - path: '/foo/parent/a', - matched: [], - meta: {}, - name: undefined, + // matched: [], + name: 'child_a', } ) }) - // TODO: check if needed by the active matching, if not just test that the param is dropped + it.todo('discards non existent params', () => { assertRecordMatch( { path: '/', name: 'home', components }, @@ -451,9 +492,6 @@ describe('RouterMatcher.resolve', () => { { name: 'Home', params: {}, - path: '/home', - matched: [record] as any, - meta: {}, } ) }) @@ -466,10 +504,8 @@ describe('RouterMatcher.resolve', () => { { name: undefined, path: '/users/posva/m/admin' }, { path: '/users/ed/m/user', - name: undefined, - params: { id: 'ed', role: 'user' }, - matched: [record] as any, - meta: {}, + // params: { id: 'ed', role: 'user' }, + // matched: [record] as any, } ) }) @@ -485,11 +521,10 @@ describe('RouterMatcher.resolve', () => { { params: { id: 'posva', role: 'admin' } }, { name: 'UserEdit', path: '/users/posva/m/admin' }, { - path: '/users/ed/m/user', + // path: '/users/ed/m/user', name: 'UserEdit', params: { id: 'ed', role: 'user' }, - matched: [], - meta: {}, + // matched: [], } ) }) @@ -509,11 +544,10 @@ describe('RouterMatcher.resolve', () => { params: { id: 'ed', role: 'user' }, }, { - path: '/users/ed/m/user', + // path: '/users/ed/m/user', name: 'UserEdit', params: { id: 'ed', role: 'user' }, matched: [record] as any, - meta: {}, } ) }) @@ -530,10 +564,9 @@ describe('RouterMatcher.resolve', () => { }, { path: '/users/ed/m/user', - name: undefined, - params: { id: 'ed', role: 'user' }, - matched: [record] as any, - meta: {}, + // name: undefined, + // params: { id: 'ed', role: 'user' }, + // matched: [record] as any, } ) }) @@ -546,9 +579,8 @@ describe('RouterMatcher.resolve', () => { { name: 'p', params: { a: 'a' }, - path: '/a', - matched: [], - meta: {}, + // path: '/a', + // matched: [], } ) }) @@ -561,9 +593,8 @@ describe('RouterMatcher.resolve', () => { { name: 'p', params: { a: 'a', b: 'b' }, - path: '/a/b', + // path: '/a/b', matched: [], - meta: {}, } ) }) @@ -576,9 +607,8 @@ describe('RouterMatcher.resolve', () => { { name: 'p', params: { a: 'a', b: 'b' }, - path: '/a/b', + // path: '/a/b', matched: [], - meta: {}, } ) }) @@ -598,9 +628,10 @@ describe('RouterMatcher.resolve', () => { record, { params: { a: 'foo' } }, { - ...start, - matched: start.matched.map(normalizeRouteRecord), - meta: {}, + name: 'home', + params: {}, + // matched: start.matched.map(normalizeRouteRecord), + // meta: {}, } ) ).toMatchSnapshot() @@ -639,7 +670,7 @@ describe('RouterMatcher.resolve', () => { name: 'ArticlesParent', children: [{ path: ':id', components }], }, - { name: 'ArticlesParent' }, + { name: 'ArticlesParent', params: {} }, { name: 'ArticlesParent', path: '/articles' } ) }) @@ -660,15 +691,15 @@ describe('RouterMatcher.resolve', () => { name: 'Home', path: '/home', params: {}, - meta: { foo: true }, matched: [ - { - path: '/home', - name: 'Home', - components, - aliasOf: expect.objectContaining({ name: 'Home', path: '/' }), - meta: { foo: true }, - }, + // TODO: + // { + // path: '/home', + // name: 'Home', + // components, + // aliasOf: expect.objectContaining({ name: 'Home', path: '/' }), + // meta: { foo: true }, + // }, ], } ) @@ -690,15 +721,15 @@ describe('RouterMatcher.resolve', () => { name: 'Home', path: '/', params: {}, - meta: { foo: true }, matched: [ - { - path: '/', - name: 'Home', - components, - aliasOf: undefined, - meta: { foo: true }, - }, + // TODO: + // { + // path: '/', + // name: 'Home', + // components, + // aliasOf: undefined, + // meta: { foo: true }, + // }, ], } ) @@ -709,15 +740,15 @@ describe('RouterMatcher.resolve', () => { name: 'Home', path: '/home', params: {}, - meta: { foo: true }, matched: [ - { - path: '/home', - name: 'Home', - components, - aliasOf: expect.objectContaining({ name: 'Home', path: '/' }), - meta: { foo: true }, - }, + // TODO: + // { + // path: '/home', + // name: 'Home', + // components, + // aliasOf: expect.objectContaining({ name: 'Home', path: '/' }), + // meta: { foo: true }, + // }, ], } ) @@ -728,15 +759,15 @@ describe('RouterMatcher.resolve', () => { name: 'Home', path: '/start', params: {}, - meta: { foo: true }, matched: [ - { - path: '/start', - name: 'Home', - components, - aliasOf: expect.objectContaining({ name: 'Home', path: '/' }), - meta: { foo: true }, - }, + // TODO: + // { + // path: '/start', + // name: 'Home', + // components, + // aliasOf: expect.objectContaining({ name: 'Home', path: '/' }), + // meta: { foo: true }, + // }, ], } ) @@ -751,20 +782,20 @@ describe('RouterMatcher.resolve', () => { components, meta: { foo: true }, }, - { name: 'Home' }, + { name: 'Home', params: {} }, { name: 'Home', path: '/', params: {}, - meta: { foo: true }, matched: [ - { - path: '/', - name: 'Home', - components, - aliasOf: undefined, - meta: { foo: true }, - }, + // TODO: + // { + // path: '/', + // name: 'Home', + // components, + // aliasOf: undefined, + // meta: { foo: true }, + // }, ], } ) @@ -785,18 +816,19 @@ describe('RouterMatcher.resolve', () => { name: 'nested', params: {}, matched: [ - { - path: '/p', - children, - components, - aliasOf: expect.objectContaining({ path: '/parent' }), - }, - { - path: '/p/one', - name: 'nested', - components, - aliasOf: expect.objectContaining({ path: '/parent/one' }), - }, + // TODO: + // { + // path: '/p', + // children, + // components, + // aliasOf: expect.objectContaining({ path: '/parent' }), + // }, + // { + // path: '/p/one', + // name: 'nested', + // components, + // aliasOf: expect.objectContaining({ path: '/parent/one' }), + // }, ], } ) @@ -1120,19 +1152,20 @@ describe('RouterMatcher.resolve', () => { component, children, }, - { name: 'nested' }, + { name: 'nested', params: {} }, { path: '/parent/one', name: 'nested', params: {}, matched: [ - { - path: '/parent', - children, - components, - aliasOf: undefined, - }, - { path: '/parent/one', name: 'nested', components }, + // TODO: + // { + // path: '/parent', + // children, + // components, + // aliasOf: undefined, + // }, + // { path: '/parent/one', name: 'nested', components }, ], } ) @@ -1179,7 +1212,8 @@ describe('RouterMatcher.resolve', () => { name: 'child-b', path: '/foo/b', params: {}, - matched: [Foo, { ...ChildB, path: `${Foo.path}/${ChildB.path}` }], + // TODO: + // matched: [Foo, { ...ChildB, path: `${Foo.path}/${ChildB.path}` }], } ) }) @@ -1269,19 +1303,20 @@ describe('RouterMatcher.resolve', () => { } assertRecordMatch( Foo, - { name: 'nested-child-a' }, + { name: 'nested-child-a', params: {} }, { name: 'nested-child-a', path: '/foo/nested/a', params: {}, - matched: [ - Foo as any, - { ...Nested, path: `${Foo.path}/${Nested.path}` }, - { - ...NestedChildA, - path: `${Foo.path}/${Nested.path}/${NestedChildA.path}`, - }, - ], + // TODO: + // matched: [ + // Foo as any, + // { ...Nested, path: `${Foo.path}/${Nested.path}` }, + // { + // ...NestedChildA, + // path: `${Foo.path}/${Nested.path}/${NestedChildA.path}`, + // }, + // ], } ) }) @@ -1311,10 +1346,7 @@ describe('RouterMatcher.resolve', () => { }, { name: 'nested-child-a', - matched: [], params: {}, - path: '/foo/nested/a', - meta: {}, } ) }) @@ -1391,7 +1423,8 @@ describe('RouterMatcher.resolve', () => { name: 'absolute', path: '/absolute', params: {}, - matched: [Foo, ChildD], + // TODO: + // matched: [Foo, ChildD], } ) }) From 9780e914e27da3684b7cecc1b1e17bfdb9f67b93 Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Wed, 8 Jan 2025 12:00:15 +0100 Subject: [PATCH 31/40] test: remove old matcher refs --- .../matcher-resolve.spec.ts | 47 ++++++++----------- 1 file changed, 19 insertions(+), 28 deletions(-) diff --git a/packages/router/src/new-route-resolver/matcher-resolve.spec.ts b/packages/router/src/new-route-resolver/matcher-resolve.spec.ts index 4ea1a00cd..6ad889394 100644 --- a/packages/router/src/new-route-resolver/matcher-resolve.spec.ts +++ b/packages/router/src/new-route-resolver/matcher-resolve.spec.ts @@ -1,26 +1,27 @@ -import { createRouterMatcher } from '../matcher' -import { RouteComponent, RouteRecordRaw } from '../types' +import { describe, expect, it } from 'vitest' import { defineComponent } from 'vue' +import { RouteComponent, RouteRecordRaw } from '../types' import { stringifyURL } from '../location' -import { describe, expect, it } from 'vitest' import { mockWarn } from '../../__tests__/vitest-mock-warn' import { createCompiledMatcher, - MatcherLocationRaw, - NEW_MatcherRecordRaw, - NEW_LocationResolved, - NEW_MatcherRecord, + type MatcherLocationRaw, + type NEW_MatcherRecordRaw, + type NEW_LocationResolved, + type NEW_MatcherRecord, } from './resolver' -import { PathParams, tokensToParser } from '../matcher/pathParserRanker' -import { tokenizePath } from '../matcher/pathTokenizer' import { miss } from './matchers/errors' -import { MatcherPatternPath } from './matcher-pattern' -import { EXPERIMENTAL_RouteRecordRaw } from '../experimental/router' +import { MatcherPatternPath, MatcherPatternPathStatic } from './matcher-pattern' +import { type EXPERIMENTAL_RouteRecordRaw } from '../experimental/router' import { stringifyQuery } from '../query' -import { +import type { MatcherLocationAsNamed, MatcherLocationAsPathAbsolute, } from './matcher-location' +// TODO: should be moved to a different test file +// used to check backward compatible paths +import { PathParams, tokensToParser } from '../matcher/pathParserRanker' +import { tokenizePath } from '../matcher/pathTokenizer' // for raw route record const component: RouteComponent = defineComponent({}) @@ -464,22 +465,12 @@ describe('RouterMatcher.resolve', () => { }) describe.skip('LocationAsRelative', () => { - it('warns if a path isn not absolute', () => { - const record = { - path: '/parent', - components, - } - const matcher = createRouterMatcher([record], {}) - matcher.resolve( - { path: 'two' }, - { - path: '/parent/one', - name: undefined, - params: {}, - matched: [] as any, - meta: {}, - } - ) + // TODO: not sure where this warning should appear now + it.todo('warns if a path isn not absolute', () => { + const matcher = createCompiledMatcher([ + { path: new MatcherPatternPathStatic('/') }, + ]) + matcher.resolve('two', matcher.resolve('/')) expect('received "two"').toHaveBeenWarned() }) From 800721fa374007f4cf6f7ed0455fcb26eb1c775f Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Wed, 8 Jan 2025 12:34:34 +0100 Subject: [PATCH 32/40] chore: remove last ts errors --- packages/router/src/experimental/router.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/router/src/experimental/router.ts b/packages/router/src/experimental/router.ts index 09e7a46af..32f8a0f0f 100644 --- a/packages/router/src/experimental/router.ts +++ b/packages/router/src/experimental/router.ts @@ -90,6 +90,12 @@ import { */ export type _OnReadyCallback = [() => void, (reason?: any) => void] +// NOTE: we could override each type with the new matched array but this would +// interface RouteLocationResolved +// extends Omit<_RouteLocationResolved, 'matched'> { +// matched: EXPERIMENTAL_RouteRecordNormalized[] +// } + /** * Options to initialize a {@link Router} instance. */ @@ -548,8 +554,10 @@ export function experimental_createRouter( // } const matchedRoute = matcher.resolve( + // FIXME: should be ok + // @ts-expect-error: too many overlads rawLocation, - currentLocation satisfies NEW_LocationResolved + currentLocation ) const href = routerHistory.createHref(matchedRoute.fullPath) @@ -564,8 +572,8 @@ export function experimental_createRouter( } } - // TODO: can this be refactored at the very end // matchedRoute is always a new object + // @ts-expect-error: the `matched` property is different return assign(matchedRoute, { redirectedFrom: undefined, href, From e9ba885e0cb9e71700e9f6d925514f73d60d9a63 Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Wed, 8 Jan 2025 17:50:23 +0100 Subject: [PATCH 33/40] feat: support partial locations --- packages/router/src/experimental/router.ts | 47 ++-- packages/router/src/location.ts | 2 +- .../new-route-resolver/matcher-location.ts | 3 + .../matcher-resolve.spec.ts | 171 +++++++------- .../src/new-route-resolver/matcher.spec.ts | 48 ++-- .../src/new-route-resolver/matcher.test-d.ts | 7 +- .../router/src/new-route-resolver/resolver.ts | 215 ++++++++++-------- packages/router/src/query.ts | 2 +- 8 files changed, 276 insertions(+), 219 deletions(-) diff --git a/packages/router/src/experimental/router.ts b/packages/router/src/experimental/router.ts index 32f8a0f0f..1b252336e 100644 --- a/packages/router/src/experimental/router.ts +++ b/packages/router/src/experimental/router.ts @@ -83,6 +83,7 @@ import { routerKey, routerViewLocationKey, } from '../injectionSymbols' +import { MatcherLocationAsPathAbsolute } from '../new-route-resolver/matcher-location' /** * resolve, reject arguments of Promise constructor @@ -406,6 +407,11 @@ export interface EXPERIMENTAL_RouteRecordRaw extends NEW_MatcherRecordRaw { * Arbitrary data attached to the record. */ meta?: RouteMeta + + components?: Record + component?: unknown + + redirect?: unknown } // TODO: is it worth to have 2 types for the undefined values? @@ -510,6 +516,15 @@ export function experimental_createRouter( return !!matcher.getMatcher(name) } + function locationAsObject( + to: RouteLocationRaw | RouteLocationNormalized, + currentLocation: string = currentRoute.value.path + ): Exclude | RouteLocationNormalized { + return typeof to === 'string' + ? parseURL(parseQuery, to, currentLocation) + : to + } + function resolve( rawLocation: RouteLocationRaw, currentLocation?: RouteLocationNormalizedLoaded @@ -522,6 +537,11 @@ export function experimental_createRouter( currentLocation && assign({}, currentLocation || currentRoute.value) // currentLocation = assign({}, currentLocation || currentRoute.value) + const locationObject = locationAsObject( + rawLocation, + currentRoute.value.path + ) + if (__DEV__) { if (!isRouteLocation(rawLocation)) { warn( @@ -531,12 +551,9 @@ export function experimental_createRouter( return resolve({}) } - if ( - typeof rawLocation === 'object' && - !rawLocation.hash?.startsWith('#') - ) { + if (!locationObject.hash?.startsWith('#')) { warn( - `A \`hash\` should always start with the character "#". Replace "${rawLocation.hash}" with "#${rawLocation.hash}".` + `A \`hash\` should always start with the character "#". Replace "${locationObject.hash}" with "#${locationObject.hash}".` ) } } @@ -555,16 +572,20 @@ export function experimental_createRouter( const matchedRoute = matcher.resolve( // FIXME: should be ok - // @ts-expect-error: too many overlads - rawLocation, - currentLocation + // locationObject as MatcherLocationAsPathRelative, + // locationObject as MatcherLocationAsRelative, + // locationObject as MatcherLocationAsName, // TODO: this one doesn't allow an undefined currentLocation, the other ones work + locationObject as MatcherLocationAsPathAbsolute, + currentLocation as unknown as NEW_LocationResolved ) const href = routerHistory.createHref(matchedRoute.fullPath) if (__DEV__) { if (href.startsWith('//')) { warn( - `Location "${rawLocation}" resolved to "${href}". A resolved location cannot start with multiple slashes.` + `Location ${JSON.stringify( + rawLocation + )} resolved to "${href}". A resolved location cannot start with multiple slashes.` ) } if (!matchedRoute.matched.length) { @@ -581,14 +602,6 @@ export function experimental_createRouter( }) } - function locationAsObject( - to: RouteLocationRaw | RouteLocationNormalized - ): Exclude | RouteLocationNormalized { - return typeof to === 'string' - ? parseURL(parseQuery, to, currentRoute.value.path) - : assign({}, to) - } - function checkCanceledNavigation( to: RouteLocationNormalized, from: RouteLocationNormalized diff --git a/packages/router/src/location.ts b/packages/router/src/location.ts index 2f8b1ba86..9c896e1e4 100644 --- a/packages/router/src/location.ts +++ b/packages/router/src/location.ts @@ -77,8 +77,8 @@ export function parseURL( hash = location.slice(hashPos, location.length) } - // TODO(major): path ?? location path = resolveRelativePath( + // TODO(major): path ?? location path != null ? path : // empty path means a relative query or hash `?foo=f`, `#thing` diff --git a/packages/router/src/new-route-resolver/matcher-location.ts b/packages/router/src/new-route-resolver/matcher-location.ts index f597df07f..e05fdf7b3 100644 --- a/packages/router/src/new-route-resolver/matcher-location.ts +++ b/packages/router/src/new-route-resolver/matcher-location.ts @@ -38,6 +38,9 @@ export interface MatcherLocationAsPathRelative { */ params?: undefined } + +// TODO: does it make sense to support absolute paths objects? + export interface MatcherLocationAsPathAbsolute extends MatcherLocationAsPathRelative { path: `/${string}` diff --git a/packages/router/src/new-route-resolver/matcher-resolve.spec.ts b/packages/router/src/new-route-resolver/matcher-resolve.spec.ts index 6ad889394..a73a05841 100644 --- a/packages/router/src/new-route-resolver/matcher-resolve.spec.ts +++ b/packages/router/src/new-route-resolver/matcher-resolve.spec.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest' import { defineComponent } from 'vue' import { RouteComponent, RouteRecordRaw } from '../types' -import { stringifyURL } from '../location' +import { NEW_stringifyURL } from '../location' import { mockWarn } from '../../__tests__/vitest-mock-warn' import { createCompiledMatcher, @@ -9,6 +9,7 @@ import { type NEW_MatcherRecordRaw, type NEW_LocationResolved, type NEW_MatcherRecord, + NO_MATCH_LOCATION, } from './resolver' import { miss } from './matchers/errors' import { MatcherPatternPath, MatcherPatternPathStatic } from './matcher-pattern' @@ -20,14 +21,27 @@ import type { } from './matcher-location' // TODO: should be moved to a different test file // used to check backward compatible paths -import { PathParams, tokensToParser } from '../matcher/pathParserRanker' +import { + PATH_PARSER_OPTIONS_DEFAULTS, + PathParams, + tokensToParser, +} from '../matcher/pathParserRanker' import { tokenizePath } from '../matcher/pathTokenizer' +import { mergeOptions } from '../utils' // for raw route record const component: RouteComponent = defineComponent({}) // for normalized route records const components = { default: component } +function isMatchable(record: RouteRecordRaw): boolean { + return !!( + record.name || + (record.components && Object.keys(record.components).length) || + record.redirect + ) +} + function compileRouteRecord( record: RouteRecordRaw, parentRecord?: RouteRecordRaw @@ -38,14 +52,15 @@ function compileRouteRecord( ? record.path : (parentRecord?.path || '') + record.path record.path = path - const parser = tokensToParser(tokenizePath(record.path), { - // start: true, - end: record.end, - sensitive: record.sensitive, - strict: record.strict, - }) + const parser = tokensToParser( + tokenizePath(record.path), + mergeOptions(PATH_PARSER_OPTIONS_DEFAULTS, record) + ) + + // console.log({ record, parser }) return { + group: !isMatchable(record), name: record.name, path: { @@ -122,7 +137,7 @@ describe('RouterMatcher.resolve', () => { const records = (Array.isArray(record) ? record : [record]).map( (record): EXPERIMENTAL_RouteRecordRaw => isExperimentalRouteRecordRaw(record) - ? record + ? { components, ...record } : compileRouteRecord(record) ) const matcher = createCompiledMatcher() @@ -139,15 +154,17 @@ describe('RouterMatcher.resolve', () => { path, query: {}, hash: '', + // by default we have a symbol on every route name: expect.any(Symbol) as symbol, // must non enumerable // matched: [], params: (typeof toLocation === 'object' && toLocation.params) || {}, - fullPath: stringifyURL(stringifyQuery, { - path: expectedLocation.path || '/', - query: expectedLocation.query, - hash: expectedLocation.hash, - }), + fullPath: NEW_stringifyURL( + stringifyQuery, + expectedLocation.path || path || '/', + expectedLocation.query, + expectedLocation.hash + ), ...expectedLocation, } @@ -161,43 +178,29 @@ describe('RouterMatcher.resolve', () => { const resolvedFrom = isMatcherLocationResolved(fromLocation) ? fromLocation - : // FIXME: is this a ts bug? - // @ts-expect-error - matcher.resolve(fromLocation) + : matcher.resolve( + // FIXME: is this a ts bug? + // @ts-expect-error + typeof fromLocation === 'string' + ? { path: fromLocation } + : fromLocation + ) + + // console.log({ toLocation, resolved, expectedLocation, resolvedFrom }) expect( matcher.resolve( - // FIXME: WTF? + // FIXME: should work now // @ts-expect-error - toLocation, - resolvedFrom + typeof toLocation === 'string' ? { path: toLocation } : toLocation, + resolvedFrom === START_LOCATION ? undefined : resolvedFrom ) ).toMatchObject({ ...resolved, }) } - /** - * - * @param record - Record or records we are testing the matcher against - * @param location - location we want to resolve against - * @param [start] Optional currentLocation used when resolving - * @returns error - */ - function assertErrorMatch( - record: RouteRecordRaw | RouteRecordRaw[], - toLocation: Exclude | `/${string}`, - fromLocation: - | NEW_LocationResolved - // absolute locations only - | `/${string}` - | MatcherLocationAsNamed - | MatcherLocationAsPathAbsolute = START_LOCATION - ) { - assertRecordMatch(record, toLocation, {}, fromLocation) - } - - describe.skip('LocationAsPath', () => { + describe('LocationAsPath', () => { it('resolves a normal path', () => { assertRecordMatch({ path: '/', name: 'Home', components }, '/', { name: 'Home', @@ -207,10 +210,14 @@ describe('RouterMatcher.resolve', () => { }) it('resolves a normal path without name', () => { + assertRecordMatch({ path: '/', components }, '/', { + path: '/', + params: {}, + }) assertRecordMatch( { path: '/', components }, { path: '/' }, - { name: undefined, path: '/', params: {} } + { path: '/', params: {} } ) }) @@ -258,7 +265,7 @@ describe('RouterMatcher.resolve', () => { assertRecordMatch( { path: '/users/:id/:other', components }, { path: '/users/posva/hey' }, - { name: undefined, params: { id: 'posva', other: 'hey' } } + { name: expect.any(Symbol), params: { id: 'posva', other: 'hey' } } ) }) @@ -266,7 +273,7 @@ describe('RouterMatcher.resolve', () => { assertRecordMatch( { path: '/', components }, { path: '/foo' }, - { name: undefined, params: {}, path: '/foo', matched: [] } + { params: {}, path: '/foo', matched: [] } ) }) @@ -274,7 +281,7 @@ describe('RouterMatcher.resolve', () => { assertRecordMatch( { path: '/home/', name: 'Home', components }, { path: '/home/' }, - { name: 'Home', path: '/home/', matched: expect.any(Array) } + { name: 'Home', path: '/home/' } ) }) @@ -309,13 +316,13 @@ describe('RouterMatcher.resolve', () => { path: '/home/', name: 'Home', components, - options: { strict: true }, + strict: true, } - assertErrorMatch(record, { path: '/home' }) + assertRecordMatch(record, { path: '/home' }, NO_MATCH_LOCATION) assertRecordMatch( record, { path: '/home/' }, - { name: 'Home', path: '/home/', matched: expect.any(Array) } + { name: 'Home', path: '/home/' } ) }) @@ -324,14 +331,14 @@ describe('RouterMatcher.resolve', () => { path: '/home', name: 'Home', components, - options: { strict: true }, + strict: true, } assertRecordMatch( record, { path: '/home' }, - { name: 'Home', path: '/home', matched: expect.any(Array) } + { name: 'Home', path: '/home' } ) - assertErrorMatch(record, { path: '/home/' }) + assertRecordMatch(record, { path: '/home/' }, NO_MATCH_LOCATION) }) }) @@ -358,12 +365,10 @@ describe('RouterMatcher.resolve', () => { }) it('throws if the named route does not exists', () => { - expect(() => - assertErrorMatch( - { path: '/', components }, - { name: 'Home', params: {} } - ) - ).toThrowError('Matcher "Home" not found') + const matcher = createCompiledMatcher([]) + expect(() => matcher.resolve({ name: 'Home', params: {} })).toThrowError( + 'Matcher "Home" not found' + ) }) it('merges params', () => { @@ -375,8 +380,9 @@ describe('RouterMatcher.resolve', () => { ) }) - // TODO: new matcher no longer allows implicit param merging - it.todo('only keep existing params', () => { + // TODO: this test doesn't seem useful, it's the same as the test above + // maybe remove it? + it('only keep existing params', () => { assertRecordMatch( { path: '/:a/:b', name: 'p', components }, { name: 'p', params: { b: 'b' } }, @@ -464,13 +470,13 @@ describe('RouterMatcher.resolve', () => { }) }) - describe.skip('LocationAsRelative', () => { + describe('LocationAsRelative', () => { // TODO: not sure where this warning should appear now it.todo('warns if a path isn not absolute', () => { const matcher = createCompiledMatcher([ { path: new MatcherPatternPathStatic('/') }, ]) - matcher.resolve('two', matcher.resolve('/')) + matcher.resolve({ path: 'two' }, matcher.resolve({ path: '/' })) expect('received "two"').toHaveBeenWarned() }) @@ -492,7 +498,7 @@ describe('RouterMatcher.resolve', () => { assertRecordMatch( record, { params: { id: 'posva', role: 'admin' } }, - { name: undefined, path: '/users/posva/m/admin' }, + { path: '/users/posva/m/admin' }, { path: '/users/ed/m/user', // params: { id: 'ed', role: 'user' }, @@ -549,7 +555,6 @@ describe('RouterMatcher.resolve', () => { record, {}, { - name: undefined, path: '/users/ed/m/user', params: { id: 'ed', role: 'user' }, }, @@ -605,41 +610,36 @@ describe('RouterMatcher.resolve', () => { }) it('throws if the current named route does not exists', () => { - const record = { path: '/', components } - const start = { - name: 'home', - params: {}, - path: '/', - matched: [record], - } - // the property should be non enumerable - Object.defineProperty(start, 'matched', { enumerable: false }) - expect( - assertErrorMatch( - record, - { params: { a: 'foo' } }, + const matcher = createCompiledMatcher([]) + expect(() => + matcher.resolve( + {}, { - name: 'home', + name: 'ko', params: {}, - // matched: start.matched.map(normalizeRouteRecord), - // meta: {}, + fullPath: '/', + hash: '', + matched: [], + path: '/', + query: {}, } ) - ).toMatchSnapshot() + ).toThrowError('Matcher "ko" not found') }) it('avoids records with children without a component nor name', () => { - assertErrorMatch( + assertRecordMatch( { path: '/articles', children: [{ path: ':id', components }], }, - { path: '/articles' } + { path: '/articles' }, + NO_MATCH_LOCATION ) }) - it('avoid deeply nested records with children without a component nor name', () => { - assertErrorMatch( + it('avoids deeply nested records with children without a component nor name', () => { + assertRecordMatch( { path: '/app', components, @@ -650,7 +650,8 @@ describe('RouterMatcher.resolve', () => { }, ], }, - { path: '/articles' } + { path: '/articles' }, + NO_MATCH_LOCATION ) }) diff --git a/packages/router/src/new-route-resolver/matcher.spec.ts b/packages/router/src/new-route-resolver/matcher.spec.ts index 335ddb83d..ecc2d5e39 100644 --- a/packages/router/src/new-route-resolver/matcher.spec.ts +++ b/packages/router/src/new-route-resolver/matcher.spec.ts @@ -92,7 +92,7 @@ describe('RouterMatcher', () => { { path: new MatcherPatternPathStatic('/users') }, ]) - expect(matcher.resolve('/')).toMatchObject({ + expect(matcher.resolve({ path: '/' })).toMatchObject({ fullPath: '/', path: '/', params: {}, @@ -100,7 +100,7 @@ describe('RouterMatcher', () => { hash: '', }) - expect(matcher.resolve('/users')).toMatchObject({ + expect(matcher.resolve({ path: '/users' })).toMatchObject({ fullPath: '/users', path: '/users', params: {}, @@ -122,7 +122,7 @@ describe('RouterMatcher', () => { }, ]) - expect(matcher.resolve('/users/1')).toMatchObject({ + expect(matcher.resolve({ path: '/users/1' })).toMatchObject({ fullPath: '/users/1', path: '/users/1', params: { id: '1' }, @@ -157,11 +157,11 @@ describe('RouterMatcher', () => { }) describe('resolve()', () => { - describe('absolute locations as strings', () => { + describe.todo('absolute locations as strings', () => { it('resolves string locations with no params', () => { const matcher = createCompiledMatcher([EMPTY_PATH_ROUTE]) - expect(matcher.resolve('/?a=a&b=b#h')).toMatchObject({ + expect(matcher.resolve({ path: '/?a=a&b=b#h' })).toMatchObject({ path: '/', params: {}, query: { a: 'a', b: 'b' }, @@ -171,7 +171,7 @@ describe('RouterMatcher', () => { it('resolves a not found string', () => { const matcher = createCompiledMatcher() - expect(matcher.resolve('/bar?q=1#hash')).toEqual({ + expect(matcher.resolve({ path: '/bar?q=1#hash' })).toEqual({ ...NO_MATCH_LOCATION, fullPath: '/bar?q=1#hash', path: '/bar', @@ -184,13 +184,13 @@ describe('RouterMatcher', () => { it('resolves string locations with params', () => { const matcher = createCompiledMatcher([USER_ID_ROUTE]) - expect(matcher.resolve('/users/1?a=a&b=b#h')).toMatchObject({ + expect(matcher.resolve({ path: '/users/1?a=a&b=b#h' })).toMatchObject({ path: '/users/1', params: { id: 1 }, query: { a: 'a', b: 'b' }, hash: '#h', }) - expect(matcher.resolve('/users/54?a=a&b=b#h')).toMatchObject({ + expect(matcher.resolve({ path: '/users/54?a=a&b=b#h' })).toMatchObject({ path: '/users/54', params: { id: 54 }, query: { a: 'a', b: 'b' }, @@ -206,7 +206,7 @@ describe('RouterMatcher', () => { }, ]) - expect(matcher.resolve('/foo?page=100&b=b#h')).toMatchObject({ + expect(matcher.resolve({ path: '/foo?page=100&b=b#h' })).toMatchObject({ params: { page: 100 }, path: '/foo', query: { @@ -225,7 +225,7 @@ describe('RouterMatcher', () => { }, ]) - expect(matcher.resolve('/foo?a=a&b=b#bar')).toMatchObject({ + expect(matcher.resolve({ path: '/foo?a=a&b=b#bar' })).toMatchObject({ hash: '#bar', params: { hash: 'bar' }, path: '/foo', @@ -242,7 +242,9 @@ describe('RouterMatcher', () => { }, ]) - expect(matcher.resolve('/users/24?page=100#bar')).toMatchObject({ + expect( + matcher.resolve({ path: '/users/24?page=100#bar' }) + ).toMatchObject({ params: { id: 24, page: 100, hash: 'bar' }, }) }) @@ -255,7 +257,10 @@ describe('RouterMatcher', () => { ]) expect( - matcher.resolve('foo', matcher.resolve('/nested/')) + matcher.resolve( + { path: 'foo' }, + matcher.resolve({ path: '/nested/' }) + ) ).toMatchObject({ params: {}, path: '/nested/foo', @@ -263,7 +268,10 @@ describe('RouterMatcher', () => { hash: '', }) expect( - matcher.resolve('../foo', matcher.resolve('/nested/')) + matcher.resolve( + { path: '../foo' }, + matcher.resolve({ path: '/nested/' }) + ) ).toMatchObject({ params: {}, path: '/foo', @@ -271,7 +279,10 @@ describe('RouterMatcher', () => { hash: '', }) expect( - matcher.resolve('./foo', matcher.resolve('/nested/')) + matcher.resolve( + { path: './foo' }, + matcher.resolve({ path: '/nested/' }) + ) ).toMatchObject({ params: {}, path: '/nested/foo', @@ -317,7 +328,7 @@ describe('RouterMatcher', () => { const matcher = createCompiledMatcher([ANY_PATH_ROUTE]) describe('decodes', () => { it('handles encoded string path', () => { - expect(matcher.resolve('/%23%2F%3F')).toMatchObject({ + expect(matcher.resolve({ path: '/%23%2F%3F' })).toMatchObject({ fullPath: '/%23%2F%3F', path: '/%23%2F%3F', query: {}, @@ -326,7 +337,9 @@ describe('RouterMatcher', () => { }) }) - it('decodes query from a string', () => { + // TODO: move to the router as the matcher dosen't handle a plain string + it.todo('decodes query from a string', () => { + // @ts-expect-error: does not suppor fullPath expect(matcher.resolve('/foo?foo=%23%2F%3F')).toMatchObject({ path: '/foo', fullPath: '/foo?foo=%23%2F%3F', @@ -334,7 +347,8 @@ describe('RouterMatcher', () => { }) }) - it('decodes hash from a string', () => { + it.todo('decodes hash from a string', () => { + // @ts-expect-error: does not suppor fullPath expect(matcher.resolve('/foo#%22')).toMatchObject({ path: '/foo', fullPath: '/foo#%22', diff --git a/packages/router/src/new-route-resolver/matcher.test-d.ts b/packages/router/src/new-route-resolver/matcher.test-d.ts index 26060c3a6..c04dfad31 100644 --- a/packages/router/src/new-route-resolver/matcher.test-d.ts +++ b/packages/router/src/new-route-resolver/matcher.test-d.ts @@ -15,7 +15,7 @@ describe('Matcher', () => { describe('matcher.resolve()', () => { it('resolves absolute string locations', () => { - expectTypeOf(matcher.resolve('/foo')).toEqualTypeOf< + expectTypeOf(matcher.resolve({ path: '/foo' })).toEqualTypeOf< NEW_LocationResolved >() }) @@ -27,7 +27,10 @@ describe('Matcher', () => { it('resolves relative locations', () => { expectTypeOf( - matcher.resolve('foo', {} as NEW_LocationResolved) + matcher.resolve( + { path: 'foo' }, + {} as NEW_LocationResolved + ) ).toEqualTypeOf>() }) diff --git a/packages/router/src/new-route-resolver/resolver.ts b/packages/router/src/new-route-resolver/resolver.ts index 17ceecf02..93b235c79 100644 --- a/packages/router/src/new-route-resolver/resolver.ts +++ b/packages/router/src/new-route-resolver/resolver.ts @@ -1,9 +1,4 @@ -import { - type LocationQuery, - parseQuery, - normalizeQuery, - stringifyQuery, -} from '../query' +import { type LocationQuery, normalizeQuery, stringifyQuery } from '../query' import type { MatcherPatternHash, MatcherPatternPath, @@ -11,7 +6,7 @@ import type { } from './matcher-pattern' import { warn } from '../warning' import { encodeQueryValue as _encodeQueryValue, encodeParam } from '../encoding' -import { parseURL, NEW_stringifyURL } from '../location' +import { NEW_stringifyURL, resolveRelativePath } from '../location' import type { MatcherLocationAsNamed, MatcherLocationAsPathAbsolute, @@ -37,25 +32,27 @@ export interface NEW_RouterResolver { /** * Resolves an absolute location (like `/path/to/somewhere`). */ - resolve( - absoluteLocation: `/${string}`, - currentLocation?: undefined | NEW_LocationResolved - ): NEW_LocationResolved + // resolve( + // absoluteLocation: `/${string}`, + // currentLocation?: undefined | NEW_LocationResolved + // ): NEW_LocationResolved /** * Resolves a string location relative to another location. A relative location can be `./same-folder`, * `../parent-folder`, `same-folder`, or even `?page=2`. */ - resolve( - relativeLocation: string, - currentLocation: NEW_LocationResolved - ): NEW_LocationResolved + // resolve( + // relativeLocation: string, + // currentLocation: NEW_LocationResolved + // ): NEW_LocationResolved /** * Resolves a location by its name. Any required params or query must be passed in the `options` argument. */ resolve( - location: MatcherLocationAsNamed + location: MatcherLocationAsNamed, + // TODO: is this useful? + currentLocation?: undefined ): NEW_LocationResolved /** @@ -63,7 +60,10 @@ export interface NEW_RouterResolver { * @param location - The location to resolve. */ resolve( - location: MatcherLocationAsPathAbsolute + location: MatcherLocationAsPathAbsolute, + // TODO: is this useful? + currentLocation?: undefined + // currentLocation?: NEW_LocationResolved ): NEW_LocationResolved resolve( @@ -120,8 +120,8 @@ export interface NEW_RouterResolver { * Allowed location objects to be passed to {@link NEW_RouterResolver['resolve']} */ export type MatcherLocationRaw = - | `/${string}` - | string + // | `/${string}` + // | string | MatcherLocationAsNamed | MatcherLocationAsPathAbsolute | MatcherLocationAsPathRelative @@ -270,6 +270,11 @@ export interface NEW_MatcherRecordRaw { * Array of nested routes. */ children?: NEW_MatcherRecordRaw[] + + /** + * Is this a record that groups children. Cannot be matched + */ + group?: boolean } export interface NEW_MatcherRecordBase { @@ -282,6 +287,8 @@ export interface NEW_MatcherRecordBase { query?: MatcherPatternQuery hash?: MatcherPatternHash + group?: boolean + parent?: T } @@ -348,20 +355,23 @@ export function createCompiledMatcher< // NOTE: because of the overloads, we need to manually type the arguments type MatcherResolveArgs = + // | [ + // absoluteLocation: `/${string}`, + // currentLocation?: undefined | NEW_LocationResolved + // ] + // | [ + // relativeLocation: string, + // currentLocation: NEW_LocationResolved + // ] | [ - absoluteLocation: `/${string}`, - currentLocation?: undefined | NEW_LocationResolved - ] - | [ - relativeLocation: string, - currentLocation: NEW_LocationResolved + absoluteLocation: MatcherLocationAsPathAbsolute, + currentLocation?: undefined ] - | [absoluteLocation: MatcherLocationAsPathAbsolute] | [ relativeLocation: MatcherLocationAsPathRelative, currentLocation: NEW_LocationResolved ] - | [location: MatcherLocationAsNamed] + | [location: MatcherLocationAsNamed, currentLocation?: undefined] | [ relativeLocation: MatcherLocationAsRelative, currentLocation: NEW_LocationResolved @@ -370,12 +380,76 @@ export function createCompiledMatcher< function resolve( ...args: MatcherResolveArgs ): NEW_LocationResolved { - const [location, currentLocation] = args + const [to, currentLocation] = args + + if (to.name || to.path == null) { + // relative location or by name + if (__DEV__ && to.name == null && currentLocation == null) { + console.warn( + `Cannot resolve an unnamed relative location without a current location. This will throw in production.`, + to + ) + // NOTE: normally there is no query, hash or path but this helps debug + // what kind of object location was passed + // @ts-expect-error: to is never + const query = normalizeQuery(to.query) + // @ts-expect-error: to is never + const hash = to.hash ?? '' + // @ts-expect-error: to is never + const path = to.path ?? '/' + return { + ...NO_MATCH_LOCATION, + fullPath: NEW_stringifyURL(stringifyQuery, path, query, hash), + path, + query, + hash, + } + } - // string location, e.g. '/foo', '../bar', 'baz', '?page=1' - if (typeof location === 'string') { + // either one of them must be defined and is catched by the dev only warn above + const name = to.name ?? currentLocation?.name + // FIXME: remove once name cannot be null + const matcher = name != null && matchers.get(name) + if (!matcher) { + throw new Error(`Matcher "${String(name)}" not found`) + } + + // unencoded params in a formatted form that the user came up with + const params: MatcherParamsFormatted = { + ...currentLocation?.params, + ...to.params, + } + const path = matcher.path.build(params) + const hash = matcher.hash?.build(params) ?? '' + const matched = buildMatched(matcher) + const query = Object.assign( + { + ...currentLocation?.query, + ...normalizeQuery(to.query), + }, + ...matched.map(matcher => matcher.query?.build(params)) + ) + + return { + name, + fullPath: NEW_stringifyURL(stringifyQuery, path, query, hash), + path, + query, + hash, + params, + matched, + } + // string location, e.g. '/foo', '../bar', 'baz', '?page=1' + } else { // parseURL handles relative paths - const url = parseURL(parseQuery, location, currentLocation?.path) + // parseURL(to.path, currentLocation?.path) + const query = normalizeQuery(to.query) + const url = { + fullPath: NEW_stringifyURL(stringifyQuery, to.path, query, to.hash), + path: resolveRelativePath(to.path, currentLocation?.path || '/'), + query, + hash: to.hash || '', + } let matcher: TMatcherRecord | undefined let matched: NEW_LocationResolved['matched'] | undefined @@ -412,8 +486,8 @@ export function createCompiledMatcher< ...url, ...NO_MATCH_LOCATION, // already decoded - query: url.query, - hash: url.hash, + // query: url.query, + // hash: url.hash, } } @@ -422,68 +496,13 @@ export function createCompiledMatcher< // matcher exists if matched exists name: matcher!.name, params: parsedParams, - // already decoded - query: url.query, - hash: url.hash, matched, } // TODO: handle object location { path, query, hash } - } else { - // relative location or by name - if (__DEV__ && location.name == null && currentLocation == null) { - console.warn( - `Cannot resolve an unnamed relative location without a current location. This will throw in production.`, - location - ) - const query = normalizeQuery(location.query) - const hash = location.hash ?? '' - const path = location.path ?? '/' - return { - ...NO_MATCH_LOCATION, - fullPath: NEW_stringifyURL(stringifyQuery, path, query, hash), - path, - query, - hash, - } - } - - // either one of them must be defined and is catched by the dev only warn above - const name = location.name ?? currentLocation!.name - // FIXME: remove once name cannot be null - const matcher = name != null && matchers.get(name) - if (!matcher) { - throw new Error(`Matcher "${String(location.name)}" not found`) - } - - // unencoded params in a formatted form that the user came up with - const params: MatcherParamsFormatted = { - ...currentLocation?.params, - ...location.params, - } - const path = matcher.path.build(params) - const hash = matcher.hash?.build(params) ?? '' - const matched = buildMatched(matcher) - const query = Object.assign( - { - ...currentLocation?.query, - ...normalizeQuery(location.query), - }, - ...matched.map(matcher => matcher.query?.build(params)) - ) - - return { - name, - fullPath: NEW_stringifyURL(stringifyQuery, path, query, hash), - path, - query, - hash, - params, - matched, - } } } - function addRoute(record: NEW_MatcherRecordRaw, parent?: TMatcherRecord) { + function addMatcher(record: NEW_MatcherRecordRaw, parent?: TMatcherRecord) { const name = record.name ?? (__DEV__ ? Symbol('unnamed-route') : Symbol()) // FIXME: proper normalization of the record // @ts-expect-error: we are not properly normalizing the record yet @@ -492,20 +511,24 @@ export function createCompiledMatcher< name, parent, } - matchers.set(name, normalizedRecord) + // TODO: + // record.children + if (!normalizedRecord.group) { + matchers.set(name, normalizedRecord) + } return normalizedRecord } for (const record of records) { - addRoute(record) + addMatcher(record) } - function removeRoute(matcher: TMatcherRecord) { + function removeMatcher(matcher: TMatcherRecord) { matchers.delete(matcher.name) // TODO: delete children and aliases } - function clearRoutes() { + function clearMatchers() { matchers.clear() } @@ -520,9 +543,9 @@ export function createCompiledMatcher< return { resolve, - addMatcher: addRoute, - removeMatcher: removeRoute, - clearMatchers: clearRoutes, + addMatcher, + removeMatcher, + clearMatchers, getMatcher, getMatchers, } diff --git a/packages/router/src/query.ts b/packages/router/src/query.ts index 365d55262..00cfb9dd6 100644 --- a/packages/router/src/query.ts +++ b/packages/router/src/query.ts @@ -16,7 +16,7 @@ import { isArray } from './utils' */ export type LocationQueryValue = string | null /** - * Possible values when defining a query. + * Possible values when defining a query. `undefined` allows to remove a value. * * @internal */ From 3fe030a1fcef51eb3ecf7953109cd2e68165b2f1 Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Thu, 9 Jan 2025 11:18:40 +0100 Subject: [PATCH 34/40] chore: rename --- .../src/new-route-resolver/{matcher.spec.ts => resolver.spec.ts} | 0 .../new-route-resolver/{matcher.test-d.ts => resolver.test-d.ts} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename packages/router/src/new-route-resolver/{matcher.spec.ts => resolver.spec.ts} (100%) rename packages/router/src/new-route-resolver/{matcher.test-d.ts => resolver.test-d.ts} (100%) diff --git a/packages/router/src/new-route-resolver/matcher.spec.ts b/packages/router/src/new-route-resolver/resolver.spec.ts similarity index 100% rename from packages/router/src/new-route-resolver/matcher.spec.ts rename to packages/router/src/new-route-resolver/resolver.spec.ts diff --git a/packages/router/src/new-route-resolver/matcher.test-d.ts b/packages/router/src/new-route-resolver/resolver.test-d.ts similarity index 100% rename from packages/router/src/new-route-resolver/matcher.test-d.ts rename to packages/router/src/new-route-resolver/resolver.test-d.ts From b7b0bbf66475126834e27f96ba66beec5db8ae44 Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Thu, 9 Jan 2025 11:54:20 +0100 Subject: [PATCH 35/40] feat: allow string in matcher resolve --- packages/router/src/experimental/router.ts | 23 +++--- packages/router/src/location.ts | 2 +- .../src/new-route-resolver/resolver.spec.ts | 7 +- .../src/new-route-resolver/resolver.test-d.ts | 22 +++++- .../router/src/new-route-resolver/resolver.ts | 77 ++++++++++++------- 5 files changed, 78 insertions(+), 53 deletions(-) diff --git a/packages/router/src/experimental/router.ts b/packages/router/src/experimental/router.ts index 1b252336e..a6c70afff 100644 --- a/packages/router/src/experimental/router.ts +++ b/packages/router/src/experimental/router.ts @@ -83,7 +83,6 @@ import { routerKey, routerViewLocationKey, } from '../injectionSymbols' -import { MatcherLocationAsPathAbsolute } from '../new-route-resolver/matcher-location' /** * resolve, reject arguments of Promise constructor @@ -537,11 +536,6 @@ export function experimental_createRouter( currentLocation && assign({}, currentLocation || currentRoute.value) // currentLocation = assign({}, currentLocation || currentRoute.value) - const locationObject = locationAsObject( - rawLocation, - currentRoute.value.path - ) - if (__DEV__) { if (!isRouteLocation(rawLocation)) { warn( @@ -551,9 +545,12 @@ export function experimental_createRouter( return resolve({}) } - if (!locationObject.hash?.startsWith('#')) { + if ( + typeof rawLocation === 'object' && + rawLocation.hash?.startsWith('#') + ) { warn( - `A \`hash\` should always start with the character "#". Replace "${locationObject.hash}" with "#${locationObject.hash}".` + `A \`hash\` should always start with the character "#". Replace "${rawLocation.hash}" with "#${rawLocation.hash}".` ) } } @@ -571,12 +568,10 @@ export function experimental_createRouter( // } const matchedRoute = matcher.resolve( - // FIXME: should be ok - // locationObject as MatcherLocationAsPathRelative, - // locationObject as MatcherLocationAsRelative, - // locationObject as MatcherLocationAsName, // TODO: this one doesn't allow an undefined currentLocation, the other ones work - locationObject as MatcherLocationAsPathAbsolute, - currentLocation as unknown as NEW_LocationResolved + // incompatible types + rawLocation as any, + // incompatible `matched` requires casting + currentLocation as any ) const href = routerHistory.createHref(matchedRoute.fullPath) diff --git a/packages/router/src/location.ts b/packages/router/src/location.ts index 9c896e1e4..77c1666cc 100644 --- a/packages/router/src/location.ts +++ b/packages/router/src/location.ts @@ -10,7 +10,7 @@ import { RouteLocation, RouteLocationNormalizedLoaded } from './typed-routes' * Location object returned by {@link `parseURL`}. * @internal */ -interface LocationNormalized { +export interface LocationNormalized { path: string fullPath: string hash: string diff --git a/packages/router/src/new-route-resolver/resolver.spec.ts b/packages/router/src/new-route-resolver/resolver.spec.ts index ecc2d5e39..436349a04 100644 --- a/packages/router/src/new-route-resolver/resolver.spec.ts +++ b/packages/router/src/new-route-resolver/resolver.spec.ts @@ -337,9 +337,7 @@ describe('RouterMatcher', () => { }) }) - // TODO: move to the router as the matcher dosen't handle a plain string - it.todo('decodes query from a string', () => { - // @ts-expect-error: does not suppor fullPath + it('decodes query from a string', () => { expect(matcher.resolve('/foo?foo=%23%2F%3F')).toMatchObject({ path: '/foo', fullPath: '/foo?foo=%23%2F%3F', @@ -347,8 +345,7 @@ describe('RouterMatcher', () => { }) }) - it.todo('decodes hash from a string', () => { - // @ts-expect-error: does not suppor fullPath + it('decodes hash from a string', () => { expect(matcher.resolve('/foo#%22')).toMatchObject({ path: '/foo', fullPath: '/foo#%22', diff --git a/packages/router/src/new-route-resolver/resolver.test-d.ts b/packages/router/src/new-route-resolver/resolver.test-d.ts index c04dfad31..6da64da51 100644 --- a/packages/router/src/new-route-resolver/resolver.test-d.ts +++ b/packages/router/src/new-route-resolver/resolver.test-d.ts @@ -18,11 +18,16 @@ describe('Matcher', () => { expectTypeOf(matcher.resolve({ path: '/foo' })).toEqualTypeOf< NEW_LocationResolved >() + expectTypeOf(matcher.resolve('/foo')).toEqualTypeOf< + NEW_LocationResolved + >() }) it('fails on non absolute location without a currentLocation', () => { // @ts-expect-error: needs currentLocation matcher.resolve('foo') + // @ts-expect-error: needs currentLocation + matcher.resolve({ path: 'foo' }) }) it('resolves relative locations', () => { @@ -32,6 +37,9 @@ describe('Matcher', () => { {} as NEW_LocationResolved ) ).toEqualTypeOf>() + expectTypeOf( + matcher.resolve('foo', {} as NEW_LocationResolved) + ).toEqualTypeOf>() }) it('resolved named locations', () => { @@ -42,7 +50,9 @@ describe('Matcher', () => { it('fails on object relative location without a currentLocation', () => { // @ts-expect-error: needs currentLocation - matcher.resolve({ params: { id: 1 } }) + matcher.resolve({ params: { id: '1' } }) + // @ts-expect-error: needs currentLocation + matcher.resolve({ query: { id: '1' } }) }) it('resolves object relative locations with a currentLocation', () => { @@ -57,13 +67,17 @@ describe('Matcher', () => { it('does not allow a name + path', () => { matcher.resolve({ - // ...({} as NEW_LocationResolved), + // ...({} as NEW_LocationResolved), name: 'foo', params: {}, // @ts-expect-error: name + path path: '/e', }) - // @ts-expect-error: name + currentLocation - matcher.resolve({ name: 'a', params: {} }, {} as NEW_LocationResolved) + matcher.resolve( + // @ts-expect-error: name + currentLocation + { name: 'a', params: {} }, + // + {} as NEW_LocationResolved + ) }) }) diff --git a/packages/router/src/new-route-resolver/resolver.ts b/packages/router/src/new-route-resolver/resolver.ts index 93b235c79..060aee34c 100644 --- a/packages/router/src/new-route-resolver/resolver.ts +++ b/packages/router/src/new-route-resolver/resolver.ts @@ -1,4 +1,9 @@ -import { type LocationQuery, normalizeQuery, stringifyQuery } from '../query' +import { + type LocationQuery, + normalizeQuery, + parseQuery, + stringifyQuery, +} from '../query' import type { MatcherPatternHash, MatcherPatternPath, @@ -6,7 +11,12 @@ import type { } from './matcher-pattern' import { warn } from '../warning' import { encodeQueryValue as _encodeQueryValue, encodeParam } from '../encoding' -import { NEW_stringifyURL, resolveRelativePath } from '../location' +import { + LocationNormalized, + NEW_stringifyURL, + parseURL, + resolveRelativePath, +} from '../location' import type { MatcherLocationAsNamed, MatcherLocationAsPathAbsolute, @@ -32,19 +42,19 @@ export interface NEW_RouterResolver { /** * Resolves an absolute location (like `/path/to/somewhere`). */ - // resolve( - // absoluteLocation: `/${string}`, - // currentLocation?: undefined | NEW_LocationResolved - // ): NEW_LocationResolved + resolve( + absoluteLocation: `/${string}`, + currentLocation?: undefined + ): NEW_LocationResolved /** * Resolves a string location relative to another location. A relative location can be `./same-folder`, * `../parent-folder`, `same-folder`, or even `?page=2`. */ - // resolve( - // relativeLocation: string, - // currentLocation: NEW_LocationResolved - // ): NEW_LocationResolved + resolve( + relativeLocation: string, + currentLocation: NEW_LocationResolved + ): NEW_LocationResolved /** * Resolves a location by its name. Any required params or query must be passed in the `options` argument. @@ -53,6 +63,7 @@ export interface NEW_RouterResolver { location: MatcherLocationAsNamed, // TODO: is this useful? currentLocation?: undefined + // currentLocation?: undefined | NEW_LocationResolved ): NEW_LocationResolved /** @@ -63,7 +74,7 @@ export interface NEW_RouterResolver { location: MatcherLocationAsPathAbsolute, // TODO: is this useful? currentLocation?: undefined - // currentLocation?: NEW_LocationResolved + // currentLocation?: NEW_LocationResolved | undefined ): NEW_LocationResolved resolve( @@ -121,7 +132,7 @@ export interface NEW_RouterResolver { */ export type MatcherLocationRaw = // | `/${string}` - // | string + | string | MatcherLocationAsNamed | MatcherLocationAsPathAbsolute | MatcherLocationAsPathRelative @@ -355,23 +366,27 @@ export function createCompiledMatcher< // NOTE: because of the overloads, we need to manually type the arguments type MatcherResolveArgs = - // | [ - // absoluteLocation: `/${string}`, - // currentLocation?: undefined | NEW_LocationResolved - // ] - // | [ - // relativeLocation: string, - // currentLocation: NEW_LocationResolved - // ] + | [absoluteLocation: `/${string}`, currentLocation?: undefined] + | [ + relativeLocation: string, + currentLocation: NEW_LocationResolved + ] | [ absoluteLocation: MatcherLocationAsPathAbsolute, + // Same as above + // currentLocation?: NEW_LocationResolved | undefined currentLocation?: undefined ] | [ relativeLocation: MatcherLocationAsPathRelative, currentLocation: NEW_LocationResolved ] - | [location: MatcherLocationAsNamed, currentLocation?: undefined] + | [ + location: MatcherLocationAsNamed, + // Same as above + // currentLocation?: NEW_LocationResolved | undefined + currentLocation?: undefined + ] | [ relativeLocation: MatcherLocationAsRelative, currentLocation: NEW_LocationResolved @@ -382,7 +397,7 @@ export function createCompiledMatcher< ): NEW_LocationResolved { const [to, currentLocation] = args - if (to.name || to.path == null) { + if (typeof to === 'object' && (to.name || to.path == null)) { // relative location or by name if (__DEV__ && to.name == null && currentLocation == null) { console.warn( @@ -442,13 +457,17 @@ export function createCompiledMatcher< // string location, e.g. '/foo', '../bar', 'baz', '?page=1' } else { // parseURL handles relative paths - // parseURL(to.path, currentLocation?.path) - const query = normalizeQuery(to.query) - const url = { - fullPath: NEW_stringifyURL(stringifyQuery, to.path, query, to.hash), - path: resolveRelativePath(to.path, currentLocation?.path || '/'), - query, - hash: to.hash || '', + let url: LocationNormalized + if (typeof to === 'string') { + url = parseURL(parseQuery, to, currentLocation?.path) + } else { + const query = normalizeQuery(to.query) + url = { + fullPath: NEW_stringifyURL(stringifyQuery, to.path, query, to.hash), + path: resolveRelativePath(to.path, currentLocation?.path || '/'), + query, + hash: to.hash || '', + } } let matcher: TMatcherRecord | undefined From a218b243e3c7fc85f07c681ebcc7d8741fb491af Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Thu, 9 Jan 2025 12:04:28 +0100 Subject: [PATCH 36/40] test: stricter no match test --- .../matcher-resolve.spec.ts | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/packages/router/src/new-route-resolver/matcher-resolve.spec.ts b/packages/router/src/new-route-resolver/matcher-resolve.spec.ts index a73a05841..66ca21a89 100644 --- a/packages/router/src/new-route-resolver/matcher-resolve.spec.ts +++ b/packages/router/src/new-route-resolver/matcher-resolve.spec.ts @@ -188,16 +188,21 @@ describe('RouterMatcher.resolve', () => { // console.log({ toLocation, resolved, expectedLocation, resolvedFrom }) - expect( - matcher.resolve( - // FIXME: should work now - // @ts-expect-error - typeof toLocation === 'string' ? { path: toLocation } : toLocation, - resolvedFrom === START_LOCATION ? undefined : resolvedFrom - ) - ).toMatchObject({ - ...resolved, - }) + const result = matcher.resolve( + // FIXME: should work now + // @ts-expect-error + typeof toLocation === 'string' ? { path: toLocation } : toLocation, + resolvedFrom === START_LOCATION ? undefined : resolvedFrom + ) + + if ( + expectedLocation.name === undefined || + expectedLocation.name !== NO_MATCH_LOCATION.name + ) { + expect(result.name).not.toBe(NO_MATCH_LOCATION.name) + } + + expect(result).toMatchObject(resolved) } describe('LocationAsPath', () => { @@ -273,7 +278,7 @@ describe('RouterMatcher.resolve', () => { assertRecordMatch( { path: '/', components }, { path: '/foo' }, - { params: {}, path: '/foo', matched: [] } + NO_MATCH_LOCATION ) }) From ef995f44b334ad8d2638cb3e7cf66b4ea8d7227f Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Thu, 9 Jan 2025 15:47:33 +0100 Subject: [PATCH 37/40] feat: handle children --- .../__tests__/matcher/pathRanking.spec.ts | 10 -- packages/router/src/experimental/router.ts | 44 ++++--- .../router/src/matcher/pathParserRanker.ts | 5 +- .../matcher-resolve.spec.ts | 63 ++++++--- .../new-route-resolver/matchers/test-utils.ts | 14 ++ .../src/new-route-resolver/resolver.spec.ts | 32 ++--- .../router/src/new-route-resolver/resolver.ts | 122 ++++++++++++++++-- 7 files changed, 208 insertions(+), 82 deletions(-) diff --git a/packages/router/__tests__/matcher/pathRanking.spec.ts b/packages/router/__tests__/matcher/pathRanking.spec.ts index 230c3a182..417d3229e 100644 --- a/packages/router/__tests__/matcher/pathRanking.spec.ts +++ b/packages/router/__tests__/matcher/pathRanking.spec.ts @@ -13,19 +13,9 @@ describe('Path ranking', () => { return comparePathParserScore( { score: a, - re: /a/, - // @ts-expect-error - stringify: v => v, - // @ts-expect-error - parse: v => v, - keys: [], }, { score: b, - re: /a/, - stringify: v => v, - parse: v => v, - keys: [], } ) } diff --git a/packages/router/src/experimental/router.ts b/packages/router/src/experimental/router.ts index a6c70afff..2347a5f9b 100644 --- a/packages/router/src/experimental/router.ts +++ b/packages/router/src/experimental/router.ts @@ -23,11 +23,12 @@ import { type RouterHistory, } from '../history/common' import type { PathParserOptions } from '../matcher' -import type { - NEW_LocationResolved, - NEW_MatcherRecord, - NEW_MatcherRecordRaw, - NEW_RouterResolver, +import { + type NEW_MatcherRecordBase, + type NEW_LocationResolved, + type NEW_MatcherRecord, + type NEW_MatcherRecordRaw, + type NEW_RouterResolver, } from '../new-route-resolver/resolver' import { parseQuery as originalParseQuery, @@ -194,7 +195,7 @@ export interface EXPERIMENTAL_RouterOptions< * Matcher to use to resolve routes. * @experimental */ - matcher: NEW_RouterResolver + resolver: NEW_RouterResolver } /** @@ -411,14 +412,18 @@ export interface EXPERIMENTAL_RouteRecordRaw extends NEW_MatcherRecordRaw { component?: unknown redirect?: unknown + score: Array } // TODO: is it worth to have 2 types for the undefined values? -export interface EXPERIMENTAL_RouteRecordNormalized extends NEW_MatcherRecord { +export interface EXPERIMENTAL_RouteRecordNormalized + extends NEW_MatcherRecordBase { /** * Arbitrary data attached to the record. */ meta: RouteMeta + group?: boolean + score: Array } function normalizeRouteRecord( @@ -429,6 +434,7 @@ function normalizeRouteRecord( name: __DEV__ ? Symbol('anonymous route record') : Symbol(), meta: {}, ...record, + children: (record.children || []).map(normalizeRouteRecord), } } @@ -439,7 +445,7 @@ export function experimental_createRouter( EXPERIMENTAL_RouteRecordNormalized > { const { - matcher, + resolver, parseQuery = originalParseQuery, stringifyQuery = originalStringifyQuery, history: routerHistory, @@ -470,11 +476,11 @@ export function experimental_createRouter( | EXPERIMENTAL_RouteRecordRaw, route?: EXPERIMENTAL_RouteRecordRaw ) { - let parent: Parameters<(typeof matcher)['addMatcher']>[1] | undefined + let parent: Parameters<(typeof resolver)['addMatcher']>[1] | undefined let rawRecord: EXPERIMENTAL_RouteRecordRaw if (isRouteName(parentOrRoute)) { - parent = matcher.getMatcher(parentOrRoute) + parent = resolver.getMatcher(parentOrRoute) if (__DEV__ && !parent) { warn( `Parent route "${String( @@ -488,31 +494,31 @@ export function experimental_createRouter( rawRecord = parentOrRoute } - const addedRecord = matcher.addMatcher( + const addedRecord = resolver.addMatcher( normalizeRouteRecord(rawRecord), parent ) return () => { - matcher.removeMatcher(addedRecord) + resolver.removeMatcher(addedRecord) } } function removeRoute(name: NonNullable) { - const recordMatcher = matcher.getMatcher(name) + const recordMatcher = resolver.getMatcher(name) if (recordMatcher) { - matcher.removeMatcher(recordMatcher) + resolver.removeMatcher(recordMatcher) } else if (__DEV__) { warn(`Cannot remove non-existent route "${String(name)}"`) } } function getRoutes() { - return matcher.getMatchers() + return resolver.getMatchers() } function hasRoute(name: NonNullable): boolean { - return !!matcher.getMatcher(name) + return !!resolver.getMatcher(name) } function locationAsObject( @@ -567,7 +573,7 @@ export function experimental_createRouter( // rawLocation.params = targetParams // } - const matchedRoute = matcher.resolve( + const matchedRoute = resolver.resolve( // incompatible types rawLocation as any, // incompatible `matched` requires casting @@ -1226,7 +1232,7 @@ export function experimental_createRouter( addRoute, removeRoute, - clearRoutes: matcher.clearMatchers, + clearRoutes: resolver.clearMatchers, hasRoute, getRoutes, resolve, @@ -1307,7 +1313,7 @@ export function experimental_createRouter( // TODO: this probably needs to be updated so it can be used by vue-termui if ((__DEV__ || __FEATURE_PROD_DEVTOOLS__) && isBrowser) { // @ts-expect-error: FIXME: refactor with new types once it's possible - addDevtools(app, router, matcher) + addDevtools(app, router, resolver) } }, } diff --git a/packages/router/src/matcher/pathParserRanker.ts b/packages/router/src/matcher/pathParserRanker.ts index b2c0b40a0..df9bf172e 100644 --- a/packages/router/src/matcher/pathParserRanker.ts +++ b/packages/router/src/matcher/pathParserRanker.ts @@ -331,7 +331,10 @@ function compareScoreArray(a: number[], b: number[]): number { * @param b - second PathParser * @returns 0 if both are equal, < 0 if a should be sorted first, > 0 if b */ -export function comparePathParserScore(a: PathParser, b: PathParser): number { +export function comparePathParserScore( + a: Pick, + b: Pick +): number { let i = 0 const aScore = a.score const bScore = b.score diff --git a/packages/router/src/new-route-resolver/matcher-resolve.spec.ts b/packages/router/src/new-route-resolver/matcher-resolve.spec.ts index 66ca21a89..77b37489d 100644 --- a/packages/router/src/new-route-resolver/matcher-resolve.spec.ts +++ b/packages/router/src/new-route-resolver/matcher-resolve.spec.ts @@ -42,15 +42,25 @@ function isMatchable(record: RouteRecordRaw): boolean { ) } +function joinPaths(a: string | undefined, b: string) { + if (a?.endsWith('/')) { + return a + b + } + return a + '/' + b +} + function compileRouteRecord( record: RouteRecordRaw, parentRecord?: RouteRecordRaw ): NEW_MatcherRecordRaw { // we adapt the path to ensure they are absolute // TODO: aliases? they could be handled directly in the path matcher + if (!parentRecord && !record.path.startsWith('/')) { + throw new Error(`Record without parent must have an absolute path`) + } const path = record.path.startsWith('/') ? record.path - : (parentRecord?.path || '') + record.path + : joinPaths(parentRecord?.path, record.path) record.path = path const parser = tokensToParser( tokenizePath(record.path), @@ -62,10 +72,12 @@ function compileRouteRecord( return { group: !isMatchable(record), name: record.name, + score: parser.score, path: { match(value) { const params = parser.parse(value) + // console.log('🌟', parser.re, value, params) if (params) { return params } @@ -181,20 +193,21 @@ describe('RouterMatcher.resolve', () => { : matcher.resolve( // FIXME: is this a ts bug? // @ts-expect-error - typeof fromLocation === 'string' - ? { path: fromLocation } - : fromLocation + fromLocation ) + // console.log(matcher.getMatchers()) // console.log({ toLocation, resolved, expectedLocation, resolvedFrom }) const result = matcher.resolve( // FIXME: should work now // @ts-expect-error - typeof toLocation === 'string' ? { path: toLocation } : toLocation, + toLocation, resolvedFrom === START_LOCATION ? undefined : resolvedFrom ) + // console.log(result) + if ( expectedLocation.name === undefined || expectedLocation.name !== NO_MATCH_LOCATION.name @@ -479,7 +492,7 @@ describe('RouterMatcher.resolve', () => { // TODO: not sure where this warning should appear now it.todo('warns if a path isn not absolute', () => { const matcher = createCompiledMatcher([ - { path: new MatcherPatternPathStatic('/') }, + { path: new MatcherPatternPathStatic('/'), score: [[80]] }, ]) matcher.resolve({ path: 'two' }, matcher.resolve({ path: '/' })) expect('received "two"').toHaveBeenWarned() @@ -1169,26 +1182,34 @@ describe('RouterMatcher.resolve', () => { }) }) - describe.skip('children', () => { - const ChildA = { path: 'a', name: 'child-a', components } - const ChildB = { path: 'b', name: 'child-b', components } - const ChildC = { path: 'c', name: 'child-c', components } - const ChildD = { path: '/absolute', name: 'absolute', components } - const ChildWithParam = { path: ':p', name: 'child-params', components } - const NestedChildWithParam = { + describe('children', () => { + const ChildA: RouteRecordRaw = { path: 'a', name: 'child-a', components } + const ChildB: RouteRecordRaw = { path: 'b', name: 'child-b', components } + const ChildC: RouteRecordRaw = { path: 'c', name: 'child-c', components } + const ChildD: RouteRecordRaw = { + path: '/absolute', + name: 'absolute', + components, + } + const ChildWithParam: RouteRecordRaw = { + path: ':p', + name: 'child-params', + components, + } + const NestedChildWithParam: RouteRecordRaw = { ...ChildWithParam, name: 'nested-child-params', } - const NestedChildA = { ...ChildA, name: 'nested-child-a' } - const NestedChildB = { ...ChildB, name: 'nested-child-b' } - const NestedChildC = { ...ChildC, name: 'nested-child-c' } - const Nested = { + const NestedChildA: RouteRecordRaw = { ...ChildA, name: 'nested-child-a' } + const NestedChildB: RouteRecordRaw = { ...ChildB, name: 'nested-child-b' } + const NestedChildC: RouteRecordRaw = { ...ChildC, name: 'nested-child-c' } + const Nested: RouteRecordRaw = { path: 'nested', name: 'nested', components, children: [NestedChildA, NestedChildB, NestedChildC], } - const NestedWithParam = { + const NestedWithParam: RouteRecordRaw = { path: 'nested/:n', name: 'nested', components, @@ -1196,7 +1217,7 @@ describe('RouterMatcher.resolve', () => { } it('resolves children', () => { - const Foo = { + const Foo: RouteRecordRaw = { path: '/foo', name: 'Foo', components, @@ -1216,8 +1237,8 @@ describe('RouterMatcher.resolve', () => { }) it('resolves children with empty paths', () => { - const Nested = { path: '', name: 'nested', components } - const Foo = { + const Nested: RouteRecordRaw = { path: '', name: 'nested', components } + const Foo: RouteRecordRaw = { path: '/foo', name: 'Foo', components, diff --git a/packages/router/src/new-route-resolver/matchers/test-utils.ts b/packages/router/src/new-route-resolver/matchers/test-utils.ts index 250efafd9..4c72d8331 100644 --- a/packages/router/src/new-route-resolver/matchers/test-utils.ts +++ b/packages/router/src/new-route-resolver/matchers/test-utils.ts @@ -68,9 +68,23 @@ export const ANY_HASH_PATTERN_MATCHER: MatcherPatternParams_Base< export const EMPTY_PATH_ROUTE = { name: 'no params', path: EMPTY_PATH_PATTERN_MATCHER, + score: [[80]], + children: [], + parent: undefined, +} satisfies NEW_MatcherRecord + +export const ANY_PATH_ROUTE = { + name: 'any path', + path: ANY_PATH_PATTERN_MATCHER, + score: [[-10]], + children: [], + parent: undefined, } satisfies NEW_MatcherRecord export const USER_ID_ROUTE = { name: 'user-id', path: USER_ID_PATH_PATTERN_MATCHER, + score: [[80], [70]], + children: [], + parent: undefined, } satisfies NEW_MatcherRecord diff --git a/packages/router/src/new-route-resolver/resolver.spec.ts b/packages/router/src/new-route-resolver/resolver.spec.ts index 436349a04..da7b388e7 100644 --- a/packages/router/src/new-route-resolver/resolver.spec.ts +++ b/packages/router/src/new-route-resolver/resolver.spec.ts @@ -11,9 +11,13 @@ import { MatcherPatternPathStatic, MatcherPatternPathDynamic, } from './matcher-pattern' -import { NEW_MatcherRecord } from './resolver' import { miss } from './matchers/errors' import { EmptyParams } from './matcher-location' +import { + EMPTY_PATH_ROUTE, + USER_ID_ROUTE, + ANY_PATH_ROUTE, +} from './matchers/test-utils' const ANY_PATH_PATTERN_MATCHER: MatcherPatternPath<{ pathMatch: string }> = { match(path) { @@ -69,27 +73,12 @@ const ANY_HASH_PATTERN_MATCHER: MatcherPatternParams_Base< build: ({ hash }) => (hash ? `#${hash}` : ''), } -const EMPTY_PATH_ROUTE = { - name: 'no params', - path: EMPTY_PATH_PATTERN_MATCHER, -} satisfies NEW_MatcherRecord - -const ANY_PATH_ROUTE = { - name: 'any path', - path: ANY_PATH_PATTERN_MATCHER, -} satisfies NEW_MatcherRecord - -const USER_ID_ROUTE = { - name: 'user-id', - path: USER_ID_PATH_PATTERN_MATCHER, -} satisfies NEW_MatcherRecord - describe('RouterMatcher', () => { describe('new matchers', () => { it('static path', () => { const matcher = createCompiledMatcher([ - { path: new MatcherPatternPathStatic('/') }, - { path: new MatcherPatternPathStatic('/users') }, + { path: new MatcherPatternPathStatic('/'), score: [[80]] }, + { path: new MatcherPatternPathStatic('/users'), score: [[80]] }, ]) expect(matcher.resolve({ path: '/' })).toMatchObject({ @@ -112,6 +101,7 @@ describe('RouterMatcher', () => { it('dynamic path', () => { const matcher = createCompiledMatcher([ { + score: [[80], [70]], path: new MatcherPatternPathDynamic<{ id: string }>( /^\/users\/([^\/]+)$/, { @@ -202,6 +192,7 @@ describe('RouterMatcher', () => { const matcher = createCompiledMatcher([ { path: ANY_PATH_PATTERN_MATCHER, + score: [[100, -10]], query: PAGE_QUERY_PATTERN_MATCHER, }, ]) @@ -220,6 +211,7 @@ describe('RouterMatcher', () => { it('resolves string locations with hash', () => { const matcher = createCompiledMatcher([ { + score: [[100, -10]], path: ANY_PATH_PATTERN_MATCHER, hash: ANY_HASH_PATTERN_MATCHER, }, @@ -236,6 +228,7 @@ describe('RouterMatcher', () => { it('combines path, query and hash params', () => { const matcher = createCompiledMatcher([ { + score: [[200, 80], [72]], path: USER_ID_PATH_PATTERN_MATCHER, query: PAGE_QUERY_PATTERN_MATCHER, hash: ANY_HASH_PATTERN_MATCHER, @@ -253,7 +246,7 @@ describe('RouterMatcher', () => { describe('relative locations as strings', () => { it('resolves a simple relative location', () => { const matcher = createCompiledMatcher([ - { path: ANY_PATH_PATTERN_MATCHER }, + { path: ANY_PATH_PATTERN_MATCHER, score: [[-10]] }, ]) expect( @@ -311,6 +304,7 @@ describe('RouterMatcher', () => { { name: 'home', path: EMPTY_PATH_PATTERN_MATCHER, + score: [[80]], }, ]) diff --git a/packages/router/src/new-route-resolver/resolver.ts b/packages/router/src/new-route-resolver/resolver.ts index 060aee34c..e9d198b0d 100644 --- a/packages/router/src/new-route-resolver/resolver.ts +++ b/packages/router/src/new-route-resolver/resolver.ts @@ -25,6 +25,7 @@ import type { MatcherParamsFormatted, } from './matcher-location' import { _RouteRecordProps } from '../typed-routes' +import { comparePathParserScore } from '../matcher/pathParserRanker' /** * Allowed types for a matcher name. @@ -286,6 +287,8 @@ export interface NEW_MatcherRecordRaw { * Is this a record that groups children. Cannot be matched */ group?: boolean + + score: Array } export interface NEW_MatcherRecordBase { @@ -298,9 +301,12 @@ export interface NEW_MatcherRecordBase { query?: MatcherPatternQuery hash?: MatcherPatternHash - group?: boolean - parent?: T + children: T[] + + group?: boolean + aliasOf?: NEW_MatcherRecord + score: Array } /** @@ -352,7 +358,8 @@ export function createCompiledMatcher< records: NEW_MatcherRecordRaw[] = [] ): NEW_RouterResolver { // TODO: we also need an array that has the correct order - const matchers = new Map() + const matcherMap = new Map() + const matchers: TMatcherRecord[] = [] // TODO: allow custom encode/decode functions // const encodeParams = applyToParams.bind(null, encodeParam) @@ -424,7 +431,7 @@ export function createCompiledMatcher< // either one of them must be defined and is catched by the dev only warn above const name = to.name ?? currentLocation?.name // FIXME: remove once name cannot be null - const matcher = name != null && matchers.get(name) + const matcher = name != null && matcherMap.get(name) if (!matcher) { throw new Error(`Matcher "${String(name)}" not found`) } @@ -474,7 +481,7 @@ export function createCompiledMatcher< let matched: NEW_LocationResolved['matched'] | undefined let parsedParams: MatcherParamsFormatted | null | undefined - for (matcher of matchers.values()) { + for (matcher of matchers) { // match the path because the path matcher only needs to be matched here // match the hash because only the deepest child matters // End up by building up the matched array, (reversed so it goes from @@ -493,6 +500,8 @@ export function createCompiledMatcher< // } parsedParams = { ...pathParams, ...queryParams, ...hashParams } + // we found our match! + break } catch (e) { // for debugging tests // console.log('❌ ERROR matching', e) @@ -529,12 +538,23 @@ export function createCompiledMatcher< ...record, name, parent, + children: [], } - // TODO: - // record.children + + // insert the matcher if it's matchable if (!normalizedRecord.group) { - matchers.set(name, normalizedRecord) + const index = findInsertionIndex(normalizedRecord, matchers) + matchers.splice(index, 0, normalizedRecord) + // only add the original record to the name map + if (normalizedRecord.name && !isAliasRecord(normalizedRecord)) + matcherMap.set(normalizedRecord.name, normalizedRecord) + // matchers.set(name, normalizedRecord) } + + record.children?.forEach(childRecord => + normalizedRecord.children.push(addMatcher(childRecord, normalizedRecord)) + ) + return normalizedRecord } @@ -543,20 +563,25 @@ export function createCompiledMatcher< } function removeMatcher(matcher: TMatcherRecord) { - matchers.delete(matcher.name) + matcherMap.delete(matcher.name) + for (const child of matcher.children) { + removeMatcher(child) + } + // TODO: delete from matchers // TODO: delete children and aliases } function clearMatchers() { - matchers.clear() + matchers.splice(0, matchers.length) + matcherMap.clear() } function getMatchers() { - return Array.from(matchers.values()) + return matchers } function getMatcher(name: MatcherName) { - return matchers.get(name) + return matcherMap.get(name) } return { @@ -569,3 +594,76 @@ export function createCompiledMatcher< getMatchers, } } + +/** + * Performs a binary search to find the correct insertion index for a new matcher. + * + * Matchers are primarily sorted by their score. If scores are tied then we also consider parent/child relationships, + * with descendants coming before ancestors. If there's still a tie, new routes are inserted after existing routes. + * + * @param matcher - new matcher to be inserted + * @param matchers - existing matchers + */ +function findInsertionIndex>( + matcher: T, + matchers: T[] +) { + // First phase: binary search based on score + let lower = 0 + let upper = matchers.length + + while (lower !== upper) { + const mid = (lower + upper) >> 1 + const sortOrder = comparePathParserScore(matcher, matchers[mid]) + + if (sortOrder < 0) { + upper = mid + } else { + lower = mid + 1 + } + } + + // Second phase: check for an ancestor with the same score + const insertionAncestor = getInsertionAncestor(matcher) + + if (insertionAncestor) { + upper = matchers.lastIndexOf(insertionAncestor, upper - 1) + + if (__DEV__ && upper < 0) { + // This should never happen + warn( + // TODO: fix stringifying new matchers + `Finding ancestor route "${insertionAncestor.path}" failed for "${matcher.path}"` + ) + } + } + + return upper +} + +function getInsertionAncestor>(matcher: T) { + let ancestor: T | undefined = matcher + + while ((ancestor = ancestor.parent)) { + if (!ancestor.group && comparePathParserScore(matcher, ancestor) === 0) { + return ancestor + } + } + + return +} + +/** + * Checks if a record or any of its parent is an alias + * @param record + */ +function isAliasRecord>( + record: T | undefined +): boolean { + while (record) { + if (record.aliasOf) return true + record = record.parent + } + + return false +} From 4b8ac59c21ea0f70d30bdac399a954a3021efd06 Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Thu, 17 Jul 2025 22:22:51 +0200 Subject: [PATCH 38/40] refactor: simplify new resolver to be static --- packages/router/src/experimental/router.ts | 248 +++++++----------- packages/router/src/navigationGuards.ts | 2 +- .../new-route-resolver/matcher-location.ts | 6 +- .../src/new-route-resolver/matcher-pattern.ts | 49 +++- .../matcher-resolve.spec.ts | 21 +- .../src/new-route-resolver/matchers/errors.ts | 5 + .../new-route-resolver/matchers/test-utils.ts | 15 +- .../src/new-route-resolver/resolver-static.ts | 198 ++++++++++++++ .../src/new-route-resolver/resolver.spec.ts | 52 +--- .../router/src/new-route-resolver/resolver.ts | 137 ++++++---- packages/router/src/router.ts | 32 ++- packages/router/test-dts/index.d.ts | 4 +- 12 files changed, 488 insertions(+), 281 deletions(-) create mode 100644 packages/router/src/new-route-resolver/resolver-static.ts diff --git a/packages/router/src/experimental/router.ts b/packages/router/src/experimental/router.ts index 2347a5f9b..e6a933c7a 100644 --- a/packages/router/src/experimental/router.ts +++ b/packages/router/src/experimental/router.ts @@ -16,20 +16,13 @@ import { type App, } from 'vue' import { RouterLink } from '../RouterLink' -import { RouterView } from '../RouterView' import { NavigationType, type HistoryState, type RouterHistory, } from '../history/common' import type { PathParserOptions } from '../matcher' -import { - type NEW_MatcherRecordBase, - type NEW_LocationResolved, - type NEW_MatcherRecord, - type NEW_MatcherRecordRaw, - type NEW_RouterResolver, -} from '../new-route-resolver/resolver' +import { type NEW_LocationResolved } from '../new-route-resolver/resolver' import { parseQuery as originalParseQuery, stringifyQuery as originalStringifyQuery, @@ -45,6 +38,7 @@ import { type RouterScrollBehavior, } from '../scrollBehavior' import type { + _RouteRecordProps, NavigationGuardWithThis, NavigationHookAfter, RouteLocation, @@ -61,8 +55,8 @@ import type { } from '../typed-routes' import { isRouteLocation, - isRouteName, Lazy, + RawRouteComponent, RouteLocationOptions, RouteMeta, } from '../types' @@ -84,6 +78,10 @@ import { routerKey, routerViewLocationKey, } from '../injectionSymbols' +import { + EXPERIMENTAL_ResolverStatic, + EXPERIMENTAL_ResolverStaticRecord, +} from '../new-route-resolver/resolver-static' /** * resolve, reject arguments of Promise constructor @@ -179,30 +177,58 @@ export interface EXPERIMENTAL_RouterOptions_Base extends PathParserOptions { // linkInactiveClass?: string } +// TODO: is it worth to have 2 types for the undefined values? +export interface EXPERIMENTAL_RouteRecordNormalized + extends EXPERIMENTAL_ResolverStaticRecord { + /** + * Arbitrary data attached to the record. + */ + meta: RouteMeta + + // TODO: + redirect?: unknown + + /** + * Allow passing down params as props to the component rendered by `router-view`. + */ + props: Record + + /** + * {@inheritDoc RouteRecordMultipleViews.components} + */ + components: Record + + /** + * Contains the original modules for lazy loaded components. + * @internal + */ + mods: Record +} + /** * Options to initialize an experimental {@link EXPERIMENTAL_Router} instance. * @experimental */ export interface EXPERIMENTAL_RouterOptions< - TMatcherRecord extends NEW_MatcherRecord -> extends EXPERIMENTAL_RouterOptions_Base { - /** - * Initial list of routes that should be added to the router. - */ - routes?: Readonly - + // TODO: probably need some generic types + // TResolver extends NEW_RouterResolver_Base, +>extends EXPERIMENTAL_RouterOptions_Base { /** * Matcher to use to resolve routes. + * * @experimental */ - resolver: NEW_RouterResolver + resolver: EXPERIMENTAL_ResolverStatic } /** * Router base instance. + * * @experimental This version is not stable, it's meant to replace {@link Router} in the future. */ -export interface EXPERIMENTAL_Router_Base { +export interface EXPERIMENTAL_Router_Base { + // NOTE: for dynamic routing we need this + // /** * Current {@link RouteLocationNormalized} */ @@ -213,31 +239,6 @@ export interface EXPERIMENTAL_Router_Base { */ listening: boolean - /** - * Add a new {@link EXPERIMENTAL_RouteRecordRaw | route record} as the child of an existing route. - * - * @param parentName - Parent Route Record where `route` should be appended at - * @param route - Route Record to add - */ - addRoute( - // NOTE: it could be `keyof RouteMap` but the point of dynamic routes is not knowing the routes at build - parentName: NonNullable, - route: TRouteRecordRaw - ): () => void - /** - * Add a new {@link EXPERIMENTAL_RouteRecordRaw | route record} to the router. - * - * @param route - Route Record to add - */ - addRoute(route: TRouteRecordRaw): () => void - - /** - * Remove an existing route by its name. - * - * @param name - Name of the route to remove - */ - removeRoute(name: NonNullable): void - /** * Checks if a route with a given name exists * @@ -248,12 +249,7 @@ export interface EXPERIMENTAL_Router_Base { /** * Get a full list of all the {@link RouteRecord | route records}. */ - getRoutes(): TRouteRecord[] - - /** - * Delete all routes from the router matcher. - */ - clearRoutes(): void + getRoutes(): TRecord[] /** * Returns the {@link RouteLocation | normalized version} of a @@ -392,58 +388,49 @@ export interface EXPERIMENTAL_Router_Base { install(app: App): void } -export interface EXPERIMENTAL_Router< - TRouteRecordRaw, // extends NEW_MatcherRecordRaw, - TRouteRecord extends NEW_MatcherRecord -> extends EXPERIMENTAL_Router_Base { +export interface EXPERIMENTAL_Router + // TODO: dynamic routing + // < + // TRouteRecordRaw, // extends NEW_MatcherRecordRaw, + // TRouteRecord extends NEW_MatcherRecord, + // > + extends EXPERIMENTAL_Router_Base { /** * Original options object passed to create the Router */ - readonly options: EXPERIMENTAL_RouterOptions -} - -export interface EXPERIMENTAL_RouteRecordRaw extends NEW_MatcherRecordRaw { - /** - * Arbitrary data attached to the record. - */ - meta?: RouteMeta - - components?: Record - component?: unknown - - redirect?: unknown - score: Array + readonly options: EXPERIMENTAL_RouterOptions } -// TODO: is it worth to have 2 types for the undefined values? -export interface EXPERIMENTAL_RouteRecordNormalized - extends NEW_MatcherRecordBase { - /** - * Arbitrary data attached to the record. - */ - meta: RouteMeta - group?: boolean - score: Array -} - -function normalizeRouteRecord( - record: EXPERIMENTAL_RouteRecordRaw -): EXPERIMENTAL_RouteRecordNormalized { - // FIXME: implementation - return { - name: __DEV__ ? Symbol('anonymous route record') : Symbol(), - meta: {}, - ...record, - children: (record.children || []).map(normalizeRouteRecord), - } -} +// export interface EXPERIMENTAL_RouteRecordRaw extends NEW_MatcherRecordRaw { +// /** +// * Arbitrary data attached to the record. +// */ +// meta?: RouteMeta +// +// components?: Record +// component?: unknown +// +// redirect?: unknown +// // TODO: Not needed +// score: Array +// } +// +// +// function normalizeRouteRecord( +// record: EXPERIMENTAL_RouteRecordRaw +// ): EXPERIMENTAL_RouteRecordNormalized { +// // FIXME: implementation +// return { +// name: __DEV__ ? Symbol('anonymous route record') : Symbol(), +// meta: {}, +// ...record, +// children: (record.children || []).map(normalizeRouteRecord), +// } +// } export function experimental_createRouter( - options: EXPERIMENTAL_RouterOptions -): EXPERIMENTAL_Router< - EXPERIMENTAL_RouteRecordRaw, - EXPERIMENTAL_RouteRecordNormalized -> { + options: EXPERIMENTAL_RouterOptions +): EXPERIMENTAL_Router { const { resolver, parseQuery = originalParseQuery, @@ -451,6 +438,7 @@ export function experimental_createRouter( history: routerHistory, } = options + // FIXME: can be removed, it was for migration purposes if (__DEV__ && !routerHistory) throw new Error( 'Provide the "history" option when calling "createRouter()":' + @@ -466,59 +454,16 @@ export function experimental_createRouter( let pendingLocation: RouteLocation = START_LOCATION_NORMALIZED // leave the scrollRestoration if no scrollBehavior is provided - if (isBrowser && options.scrollBehavior && 'scrollRestoration' in history) { + if (isBrowser && options.scrollBehavior) { history.scrollRestoration = 'manual' } - function addRoute( - parentOrRoute: - | NonNullable - | EXPERIMENTAL_RouteRecordRaw, - route?: EXPERIMENTAL_RouteRecordRaw - ) { - let parent: Parameters<(typeof resolver)['addMatcher']>[1] | undefined - let rawRecord: EXPERIMENTAL_RouteRecordRaw - - if (isRouteName(parentOrRoute)) { - parent = resolver.getMatcher(parentOrRoute) - if (__DEV__ && !parent) { - warn( - `Parent route "${String( - parentOrRoute - )}" not found when adding child route`, - route - ) - } - rawRecord = route! - } else { - rawRecord = parentOrRoute - } - - const addedRecord = resolver.addMatcher( - normalizeRouteRecord(rawRecord), - parent - ) - - return () => { - resolver.removeMatcher(addedRecord) - } - } - - function removeRoute(name: NonNullable) { - const recordMatcher = resolver.getMatcher(name) - if (recordMatcher) { - resolver.removeMatcher(recordMatcher) - } else if (__DEV__) { - warn(`Cannot remove non-existent route "${String(name)}"`) - } - } - function getRoutes() { - return resolver.getMatchers() + return resolver.getRecords() } function hasRoute(name: NonNullable): boolean { - return !!resolver.getMatcher(name) + return !!resolver.getRecord(name) } function locationAsObject( @@ -812,9 +757,10 @@ export function experimental_createRouter( function runWithContext(fn: () => T): T { const app: App | undefined = installedApps.values().next().value + // FIXME: remove safeguard and ensure // TODO: remove safeguard and bump required minimum version of Vue // support Vue < 3.3 - return app && typeof app.runWithContext === 'function' + return typeof app?.runWithContext === 'function' ? app.runWithContext(fn) : fn() } @@ -1223,16 +1169,10 @@ export function experimental_createRouter( let started: boolean | undefined const installedApps = new Set() - const router: EXPERIMENTAL_Router< - EXPERIMENTAL_RouteRecordRaw, - EXPERIMENTAL_RouteRecordNormalized - > = { + const router: EXPERIMENTAL_Router = { currentRoute, listening: true, - addRoute, - removeRoute, - clearRoutes: resolver.clearMatchers, hasRoute, getRoutes, resolve, @@ -1252,9 +1192,9 @@ export function experimental_createRouter( isReady, install(app: App) { - const router = this - app.component('RouterLink', RouterLink) - app.component('RouterView', RouterView) + // Must be done by user for vapor variants + // app.component('RouterLink', RouterLink) + // app.component('RouterView', RouterView) // @ts-expect-error: FIXME: refactor with new types once it's possible app.config.globalProperties.$router = router @@ -1293,9 +1233,8 @@ export function experimental_createRouter( app.provide(routeLocationKey, shallowReactive(reactiveRoute)) app.provide(routerViewLocationKey, currentRoute) - const unmountApp = app.unmount installedApps.add(app) - app.unmount = function () { + app.onUnmount(() => { installedApps.delete(app) // the router is not attached to an app anymore if (installedApps.size < 1) { @@ -1307,8 +1246,7 @@ export function experimental_createRouter( started = false ready = false } - unmountApp() - } + }) // TODO: this probably needs to be updated so it can be used by vue-termui if ((__DEV__ || __FEATURE_PROD_DEVTOOLS__) && isBrowser) { diff --git a/packages/router/src/navigationGuards.ts b/packages/router/src/navigationGuards.ts index 0582ce47b..e0389cd7c 100644 --- a/packages/router/src/navigationGuards.ts +++ b/packages/router/src/navigationGuards.ts @@ -413,7 +413,7 @@ export function extractChangingRecords( ): [ leavingRecords: RouteRecordNormalized[], updatingRecords: RouteRecordNormalized[], - enteringRecords: RouteRecordNormalized[] + enteringRecords: RouteRecordNormalized[], ] { const leavingRecords: RouteRecordNormalized[] = [] const updatingRecords: RouteRecordNormalized[] = [] diff --git a/packages/router/src/new-route-resolver/matcher-location.ts b/packages/router/src/new-route-resolver/matcher-location.ts index e05fdf7b3..ec1431cf8 100644 --- a/packages/router/src/new-route-resolver/matcher-location.ts +++ b/packages/router/src/new-route-resolver/matcher-location.ts @@ -1,5 +1,7 @@ import type { LocationQueryRaw } from '../query' -import type { MatcherName } from './resolver' +import type { RecordName } from './resolver' + +// FIXME: rename to ResolverLocation... instead of MatcherLocation... since they are returned by a resolver /** * Generic object of params that can be passed to a matcher. @@ -12,7 +14,7 @@ export type MatcherParamsFormatted = Record export type EmptyParams = Record export interface MatcherLocationAsNamed { - name: MatcherName + name: RecordName // FIXME: should this be optional? params: MatcherParamsFormatted query?: LocationQueryRaw diff --git a/packages/router/src/new-route-resolver/matcher-pattern.ts b/packages/router/src/new-route-resolver/matcher-pattern.ts index 0f7d8c192..e0efb2eea 100644 --- a/packages/router/src/new-route-resolver/matcher-pattern.ts +++ b/packages/router/src/new-route-resolver/matcher-pattern.ts @@ -2,16 +2,28 @@ import { decode, MatcherQueryParams } from './resolver' import { EmptyParams, MatcherParamsFormatted } from './matcher-location' import { miss } from './matchers/errors' -export interface MatcherPatternParams_Base< +/** + * Base interface for matcher patterns that extract params from a URL. + * + * @template TIn - type of the input value to match against the pattern + * @template TOut - type of the output value after matching + * + * In the case of the `path`, the `TIn` is a `string`, but in the case of the + * query, it's the object of query params. + * + * @internal this is the base interface for all matcher patterns, it shouldn't + * be used directly + */ +export interface MatcherPattern< TIn = string, - TOut extends MatcherParamsFormatted = MatcherParamsFormatted + TOut extends MatcherParamsFormatted = MatcherParamsFormatted, > { /** * Matches a serialized params value against the pattern. * * @param value - params value to parse * @throws {MatchMiss} if the value doesn't match - * @returns parsed params + * @returns parsed params object */ match(value: TIn): TOut @@ -21,14 +33,19 @@ export interface MatcherPatternParams_Base< * shouldn't). * * @param value - params value to parse + * @returns serialized params value */ build(params: TOut): TIn } +/** + * Handles the `path` part of a URL. It can transform a path string into an + * object of params and vice versa. + */ export interface MatcherPatternPath< // TODO: should we allow to not return anything? It's valid to spread null and undefined - TParams extends MatcherParamsFormatted = MatcherParamsFormatted // | null // | undefined // | void // so it might be a bit more convenient -> extends MatcherPatternParams_Base {} + TParams extends MatcherParamsFormatted = MatcherParamsFormatted, // | null // | undefined // | void // so it might be a bit more convenient +> extends MatcherPattern {} export class MatcherPatternPathStatic implements MatcherPatternPath @@ -48,10 +65,11 @@ export class MatcherPatternPathStatic } // example of a static matcher built at runtime // new MatcherPatternPathStatic('/') +// new MatcherPatternPathStatic('/team') export interface Param_GetSet< TIn extends string | string[] = string | string[], - TOut = TIn + TOut = TIn, > { get?: (value: NoInfer) => TOut set?: (value: NoInfer) => TIn @@ -115,10 +133,11 @@ export type ParamsFromParsers

> = { } export class MatcherPatternPathDynamic< - TParams extends MatcherParamsFormatted = MatcherParamsFormatted + TParams extends MatcherParamsFormatted = MatcherParamsFormatted, > implements MatcherPatternPath { private params: Record> = {} + constructor( private re: RegExp, params: Record, @@ -186,10 +205,18 @@ export class MatcherPatternPathDynamic< // } } +/** + * Handles the `query` part of a URL. It can transform a query object into an + * object of params and vice versa. + */ export interface MatcherPatternQuery< - TParams extends MatcherParamsFormatted = MatcherParamsFormatted -> extends MatcherPatternParams_Base {} + TParams extends MatcherParamsFormatted = MatcherParamsFormatted, +> extends MatcherPattern {} +/** + * Handles the `hash` part of a URL. It can transform a hash string into an + * object of params and vice versa. + */ export interface MatcherPatternHash< - TParams extends MatcherParamsFormatted = MatcherParamsFormatted -> extends MatcherPatternParams_Base {} + TParams extends MatcherParamsFormatted = MatcherParamsFormatted, +> extends MatcherPattern {} diff --git a/packages/router/src/new-route-resolver/matcher-resolve.spec.ts b/packages/router/src/new-route-resolver/matcher-resolve.spec.ts index 77b37489d..af02741eb 100644 --- a/packages/router/src/new-route-resolver/matcher-resolve.spec.ts +++ b/packages/router/src/new-route-resolver/matcher-resolve.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { defineComponent } from 'vue' -import { RouteComponent, RouteRecordRaw } from '../types' +import { RouteComponent, RouteMeta, RouteRecordRaw } from '../types' import { NEW_stringifyURL } from '../location' import { mockWarn } from '../../__tests__/vitest-mock-warn' import { @@ -13,7 +13,7 @@ import { } from './resolver' import { miss } from './matchers/errors' import { MatcherPatternPath, MatcherPatternPathStatic } from './matcher-pattern' -import { type EXPERIMENTAL_RouteRecordRaw } from '../experimental/router' +import { EXPERIMENTAL_RouterOptions } from '../experimental/router' import { stringifyQuery } from '../query' import type { MatcherLocationAsNamed, @@ -29,6 +29,21 @@ import { import { tokenizePath } from '../matcher/pathTokenizer' import { mergeOptions } from '../utils' +// FIXME: this type was removed, it will be a new one once a dynamic resolver is implemented +export interface EXPERIMENTAL_RouteRecordRaw extends NEW_MatcherRecordRaw { + /** + * Arbitrary data attached to the record. + */ + meta?: RouteMeta + + components?: Record + component?: unknown + + redirect?: unknown + score: Array + readonly options: EXPERIMENTAL_RouterOptions +} + // for raw route record const component: RouteComponent = defineComponent({}) // for normalized route records @@ -147,7 +162,7 @@ describe('RouterMatcher.resolve', () => { | MatcherLocationAsPathAbsolute = START_LOCATION ) { const records = (Array.isArray(record) ? record : [record]).map( - (record): EXPERIMENTAL_RouteRecordRaw => + (record): NEW_MatcherRecordRaw => isExperimentalRouteRecordRaw(record) ? { components, ...record } : compileRouteRecord(record) diff --git a/packages/router/src/new-route-resolver/matchers/errors.ts b/packages/router/src/new-route-resolver/matchers/errors.ts index 4ad69cc4c..142b37ff8 100644 --- a/packages/router/src/new-route-resolver/matchers/errors.ts +++ b/packages/router/src/new-route-resolver/matchers/errors.ts @@ -2,6 +2,8 @@ * NOTE: for these classes to keep the same code we need to tell TS with `"useDefineForClassFields": true` in the `tsconfig.json` */ +// TODO: document helpers if kept. The helpers could also be moved to the generated code to reduce bundle size. After all, user is unlikely to write these manually + /** * Error throw when a matcher miss */ @@ -11,6 +13,9 @@ export class MatchMiss extends Error { // NOTE: not sure about having a helper. Using `new MatchMiss(description?)` is good enough export const miss = () => new MatchMiss() +// TODO: which one?, the return type of never makes types work anyway +// export const throwMiss = () => { throw new MatchMiss() } +// export const throwMiss = (...args: ConstructorParameters) => { throw new MatchMiss(...args) } /** * Error throw when a param is invalid when parsing params from path, query, or hash. diff --git a/packages/router/src/new-route-resolver/matchers/test-utils.ts b/packages/router/src/new-route-resolver/matchers/test-utils.ts index 4c72d8331..b48b9362e 100644 --- a/packages/router/src/new-route-resolver/matchers/test-utils.ts +++ b/packages/router/src/new-route-resolver/matchers/test-utils.ts @@ -2,10 +2,10 @@ import { EmptyParams } from '../matcher-location' import { MatcherPatternPath, MatcherPatternQuery, - MatcherPatternParams_Base, + MatcherPatternHash, } from '../matcher-pattern' import { NEW_MatcherRecord } from '../resolver' -import { miss } from './errors' +import { invalid, miss } from './errors' export const ANY_PATH_PATTERN_MATCHER: MatcherPatternPath<{ pathMatch: string @@ -37,7 +37,8 @@ export const USER_ID_PATH_PATTERN_MATCHER: MatcherPatternPath<{ id: number }> = } const id = Number(match[1]) if (Number.isNaN(id)) { - throw miss() + throw invalid('id') + // throw miss() } return { id } }, @@ -55,12 +56,10 @@ export const PAGE_QUERY_PATTERN_MATCHER: MatcherPatternQuery<{ page: number }> = } }, build: params => ({ page: String(params.page) }), - } satisfies MatcherPatternQuery<{ page: number }> + } -export const ANY_HASH_PATTERN_MATCHER: MatcherPatternParams_Base< - string, - { hash: string | null } -> = { +export const ANY_HASH_PATTERN_MATCHER: MatcherPatternHash = { match: hash => ({ hash: hash ? hash.slice(1) : null }), build: ({ hash }) => (hash ? `#${hash}` : ''), } diff --git a/packages/router/src/new-route-resolver/resolver-static.ts b/packages/router/src/new-route-resolver/resolver-static.ts new file mode 100644 index 000000000..1558fa8c2 --- /dev/null +++ b/packages/router/src/new-route-resolver/resolver-static.ts @@ -0,0 +1,198 @@ +import { normalizeQuery, parseQuery, stringifyQuery } from '../query' +import { + LocationNormalized, + NEW_stringifyURL, + parseURL, + resolveRelativePath, +} from '../location' +import { + MatcherLocationAsNamed, + MatcherLocationAsPathAbsolute, + MatcherLocationAsPathRelative, + MatcherLocationAsRelative, + MatcherParamsFormatted, +} from './matcher-location' +import { + buildMatched, + EXPERIMENTAL_ResolverRecord_Base, + RecordName, + MatcherQueryParams, + NEW_LocationResolved, + NEW_RouterResolver_Base, + NO_MATCH_LOCATION, +} from './resolver' + +export interface EXPERIMENTAL_ResolverStaticRecord + extends EXPERIMENTAL_ResolverRecord_Base {} + +export interface EXPERIMENTAL_ResolverStatic + extends NEW_RouterResolver_Base {} + +export function createStaticResolver< + TRecord extends EXPERIMENTAL_ResolverStaticRecord, +>(records: TRecord[]): EXPERIMENTAL_ResolverStatic { + // allows fast access to a matcher by name + const recordMap = new Map() + for (const record of records) { + recordMap.set(record.name, record) + } + + // NOTE: because of the overloads, we need to manually type the arguments + type _resolveArgs = + | [absoluteLocation: `/${string}`, currentLocation?: undefined] + | [relativeLocation: string, currentLocation: NEW_LocationResolved] + | [ + absoluteLocation: MatcherLocationAsPathAbsolute, + // Same as above + // currentLocation?: NEW_LocationResolved | undefined + currentLocation?: undefined, + ] + | [ + relativeLocation: MatcherLocationAsPathRelative, + currentLocation: NEW_LocationResolved, + ] + | [ + location: MatcherLocationAsNamed, + // Same as above + // currentLocation?: NEW_LocationResolved | undefined + currentLocation?: undefined, + ] + | [ + relativeLocation: MatcherLocationAsRelative, + currentLocation: NEW_LocationResolved, + ] + + function resolve( + ...[to, currentLocation]: _resolveArgs + ): NEW_LocationResolved { + if (typeof to === 'object' && (to.name || to.path == null)) { + // relative location by path or by name + if (__DEV__ && to.name == null && currentLocation == null) { + console.warn( + `Cannot resolve relative location "${JSON.stringify(to)}"without a current location. This will throw in production.`, + to + ) + // NOTE: normally there is no query, hash or path but this helps debug + // what kind of object location was passed + // @ts-expect-error: to is never + const query = normalizeQuery(to.query) + // @ts-expect-error: to is never + const hash = to.hash ?? '' + // @ts-expect-error: to is never + const path = to.path ?? '/' + return { + ...NO_MATCH_LOCATION, + fullPath: NEW_stringifyURL(stringifyQuery, path, query, hash), + path, + query, + hash, + } + } + + // either one of them must be defined and is catched by the dev only warn above + const name = to.name ?? currentLocation!.name + const record = recordMap.get(name)! + if (__DEV__ && (!record || !name)) { + throw new Error(`Record "${String(name)}" not found`) + } + + // unencoded params in a formatted form that the user came up with + const params: MatcherParamsFormatted = { + ...currentLocation?.params, + ...to.params, + } + const path = record.path.build(params) + const hash = record.hash?.build(params) ?? '' + const matched = buildMatched(record) + const query = Object.assign( + { + ...currentLocation?.query, + ...normalizeQuery(to.query), + }, + ...matched.map(record => record.query?.build(params)) + ) + + return { + name, + fullPath: NEW_stringifyURL(stringifyQuery, path, query, hash), + path, + query, + hash, + params, + matched, + } + // string location, e.g. '/foo', '../bar', 'baz', '?page=1' + } else { + // parseURL handles relative paths + let url: LocationNormalized + if (typeof to === 'string') { + url = parseURL(parseQuery, to, currentLocation?.path) + } else { + const query = normalizeQuery(to.query) + url = { + fullPath: NEW_stringifyURL(stringifyQuery, to.path, query, to.hash), + path: resolveRelativePath(to.path, currentLocation?.path || '/'), + query, + hash: to.hash || '', + } + } + + let record: TRecord | undefined + let matched: NEW_LocationResolved['matched'] | undefined + let parsedParams: MatcherParamsFormatted | null | undefined + + for (record of records) { + // match the path because the path matcher only needs to be matched here + // match the hash because only the deepest child matters + // End up by building up the matched array, (reversed so it goes from + // root to child) and then match and merge all queries + try { + const pathParams = record.path.match(url.path) + const hashParams = record.hash?.match(url.hash) + matched = buildMatched(record) + const queryParams: MatcherQueryParams = Object.assign( + {}, + ...matched.map(record => record.query?.match(url.query)) + ) + // TODO: test performance + // for (const record of matched) { + // Object.assign(queryParams, record.query?.match(url.query)) + // } + + parsedParams = { ...pathParams, ...queryParams, ...hashParams } + // we found our match! + break + } catch (e) { + // for debugging tests + // console.log('❌ ERROR matching', e) + } + } + + // No match location + if (!parsedParams || !matched) { + return { + ...url, + ...NO_MATCH_LOCATION, + // already decoded + // query: url.query, + // hash: url.hash, + } + } + + return { + ...url, + // record exists if matched exists + name: record!.name, + params: parsedParams, + matched, + } + // TODO: handle object location { path, query, hash } + } + } + + return { + resolve, + getRecords: () => records, + getRecord: name => recordMap.get(name), + } +} diff --git a/packages/router/src/new-route-resolver/resolver.spec.ts b/packages/router/src/new-route-resolver/resolver.spec.ts index da7b388e7..93a269c96 100644 --- a/packages/router/src/new-route-resolver/resolver.spec.ts +++ b/packages/router/src/new-route-resolver/resolver.spec.ts @@ -5,56 +5,20 @@ import { pathEncoded, } from './resolver' import { - MatcherPatternParams_Base, - MatcherPatternPath, MatcherPatternQuery, MatcherPatternPathStatic, MatcherPatternPathDynamic, } from './matcher-pattern' -import { miss } from './matchers/errors' -import { EmptyParams } from './matcher-location' import { EMPTY_PATH_ROUTE, USER_ID_ROUTE, ANY_PATH_ROUTE, + ANY_PATH_PATTERN_MATCHER, + EMPTY_PATH_PATTERN_MATCHER, + USER_ID_PATH_PATTERN_MATCHER, + ANY_HASH_PATTERN_MATCHER, } from './matchers/test-utils' -const ANY_PATH_PATTERN_MATCHER: MatcherPatternPath<{ pathMatch: string }> = { - match(path) { - return { pathMatch: path } - }, - build({ pathMatch }) { - return pathMatch - }, -} - -const EMPTY_PATH_PATTERN_MATCHER: MatcherPatternPath = { - match: path => { - if (path !== '/') { - throw miss() - } - return {} - }, - build: () => '/', -} - -const USER_ID_PATH_PATTERN_MATCHER: MatcherPatternPath<{ id: number }> = { - match(value) { - const match = value.match(/^\/users\/(\d+)$/) - if (!match?.[1]) { - throw miss() - } - const id = Number(match[1]) - if (Number.isNaN(id)) { - throw miss() - } - return { id } - }, - build({ id }) { - return `/users/${id}` - }, -} - const PAGE_QUERY_PATTERN_MATCHER: MatcherPatternQuery<{ page: number }> = { match: query => { const page = Number(query.page) @@ -65,14 +29,6 @@ const PAGE_QUERY_PATTERN_MATCHER: MatcherPatternQuery<{ page: number }> = { build: params => ({ page: String(params.page) }), } satisfies MatcherPatternQuery<{ page: number }> -const ANY_HASH_PATTERN_MATCHER: MatcherPatternParams_Base< - string, - { hash: string | null } -> = { - match: hash => ({ hash: hash ? hash.slice(1) : null }), - build: ({ hash }) => (hash ? `#${hash}` : ''), -} - describe('RouterMatcher', () => { describe('new matchers', () => { it('static path', () => { diff --git a/packages/router/src/new-route-resolver/resolver.ts b/packages/router/src/new-route-resolver/resolver.ts index e9d198b0d..fde6f38f7 100644 --- a/packages/router/src/new-route-resolver/resolver.ts +++ b/packages/router/src/new-route-resolver/resolver.ts @@ -5,6 +5,7 @@ import { stringifyQuery, } from '../query' import type { + MatcherPattern, MatcherPatternHash, MatcherPatternPath, MatcherPatternQuery, @@ -30,23 +31,26 @@ import { comparePathParserScore } from '../matcher/pathParserRanker' /** * Allowed types for a matcher name. */ -export type MatcherName = string | symbol +export type RecordName = string | symbol /** * Manage and resolve routes. Also handles the encoding, decoding, parsing and * serialization of params, query, and hash. * * - `TMatcherRecordRaw` represents the raw record type passed to {@link addMatcher}. - * - `TMatcherRecord` represents the normalized record type returned by {@link getMatchers}. + * - `TMatcherRecord` represents the normalized record type returned by {@link getRecords}. */ -export interface NEW_RouterResolver { +export interface NEW_RouterResolver_Base { /** * Resolves an absolute location (like `/path/to/somewhere`). + * + * @param absoluteLocation - The absolute location to resolve. + * @param currentLocation - This value is ignored and should not be passed if the location is absolute. */ resolve( absoluteLocation: `/${string}`, currentLocation?: undefined - ): NEW_LocationResolved + ): NEW_LocationResolved /** * Resolves a string location relative to another location. A relative location can be `./same-folder`, @@ -54,8 +58,8 @@ export interface NEW_RouterResolver { */ resolve( relativeLocation: string, - currentLocation: NEW_LocationResolved - ): NEW_LocationResolved + currentLocation: NEW_LocationResolved + ): NEW_LocationResolved /** * Resolves a location by its name. Any required params or query must be passed in the `options` argument. @@ -65,7 +69,7 @@ export interface NEW_RouterResolver { // TODO: is this useful? currentLocation?: undefined // currentLocation?: undefined | NEW_LocationResolved - ): NEW_LocationResolved + ): NEW_LocationResolved /** * Resolves a location by its absolute path (starts with `/`). Any required query must be passed. @@ -76,12 +80,12 @@ export interface NEW_RouterResolver { // TODO: is this useful? currentLocation?: undefined // currentLocation?: NEW_LocationResolved | undefined - ): NEW_LocationResolved + ): NEW_LocationResolved resolve( location: MatcherLocationAsPathRelative, - currentLocation: NEW_LocationResolved - ): NEW_LocationResolved + currentLocation: NEW_LocationResolved + ): NEW_LocationResolved // NOTE: in practice, this overload can cause bugs. It's better to use named locations @@ -91,9 +95,31 @@ export interface NEW_RouterResolver { */ resolve( relativeLocation: MatcherLocationAsRelative, - currentLocation: NEW_LocationResolved - ): NEW_LocationResolved + currentLocation: NEW_LocationResolved + ): NEW_LocationResolved + /** + * Get a list of all resolver records. + * Previously named `getRoutes()` + */ + getRecords(): TRecord[] + + /** + * Get a resolver record by its name. + * Previously named `getRecordMatcher()` + */ + getRecord(name: RecordName): TRecord | undefined +} + +/** + * Manage and resolve routes. Also handles the encoding, decoding, parsing and + * serialization of params, query, and hash. + * + * - `TMatcherRecordRaw` represents the raw record type passed to {@link addMatcher}. + * - `TMatcherRecord` represents the normalized record type returned by {@link getRecords}. + */ +export interface NEW_RouterResolver + extends NEW_RouterResolver_Base { /** * Add a matcher record. Previously named `addRoute()`. * @param matcher - The matcher record to add. @@ -114,18 +140,6 @@ export interface NEW_RouterResolver { * Remove all matcher records. Prevoisly named `clearRoutes()`. */ clearMatchers(): void - - /** - * Get a list of all matchers. - * Previously named `getRoutes()` - */ - getMatchers(): TMatcherRecord[] - - /** - * Get a matcher by its name. - * Previously named `getRecordMatcher()` - */ - getMatcher(name: MatcherName): TMatcherRecord | undefined } /** @@ -139,10 +153,9 @@ export type MatcherLocationRaw = | MatcherLocationAsPathRelative | MatcherLocationAsRelative +// TODO: ResolverLocationResolved export interface NEW_LocationResolved { - // FIXME: remove `undefined` - name: MatcherName | undefined - // TODO: generics? + name: RecordName params: MatcherParamsFormatted fullPath: string @@ -159,6 +172,7 @@ export type MatcherPathParamsValue = string | null | string[] */ export type MatcherPathParams = Record +// TODO: move to matcher-pattern export type MatcherQueryParamsValue = string | null | Array export type MatcherQueryParams = Record @@ -276,7 +290,7 @@ export interface NEW_MatcherRecordRaw { * Name for the route record. Must be unique. Will be set to `Symbol()` if * not set. */ - name?: MatcherName + name?: RecordName /** * Array of nested routes. @@ -291,29 +305,50 @@ export interface NEW_MatcherRecordRaw { score: Array } -export interface NEW_MatcherRecordBase { +export interface EXPERIMENTAL_ResolverRecord_Base { /** * Name of the matcher. Unique across all matchers. */ - name: MatcherName + name: RecordName + /** + * {@link MatcherPattern} for the path section of the URI. + */ path: MatcherPatternPath + + /** + * {@link MatcherPattern} for the query section of the URI. + */ query?: MatcherPatternQuery + + /** + * {@link MatcherPattern} for the hash section of the URI. + */ hash?: MatcherPatternHash - parent?: T - children: T[] + // TODO: here or in router + // redirect?: RouteRecordRedirectOption + parent?: this + children: this[] + aliasOf?: this + + /** + * Is this a record that groups children. Cannot be matched + */ group?: boolean - aliasOf?: NEW_MatcherRecord +} + +export interface NEW_MatcherDynamicRecord + extends EXPERIMENTAL_ResolverRecord_Base { + // TODO: the score shouldn't be always needed, it's only needed with dynamic routing score: Array } /** * Normalized version of a {@link NEW_MatcherRecordRaw} record. */ -export interface NEW_MatcherRecord - extends NEW_MatcherRecordBase {} +export interface NEW_MatcherRecord extends NEW_MatcherDynamicRecord {} /** * Tagged template helper to encode params into a path. Doesn't work with null @@ -342,7 +377,9 @@ export function pathEncoded( /** * Build the `matched` array of a record that includes all parent records from the root to the current one. */ -function buildMatched>(record: T): T[] { +export function buildMatched( + record: T +): T[] { const matched: T[] = [] let node: T | undefined = record while (node) { @@ -353,12 +390,12 @@ function buildMatched>(record: T): T[] { } export function createCompiledMatcher< - TMatcherRecord extends NEW_MatcherRecordBase + TMatcherRecord extends NEW_MatcherDynamicRecord, >( records: NEW_MatcherRecordRaw[] = [] ): NEW_RouterResolver { // TODO: we also need an array that has the correct order - const matcherMap = new Map() + const matcherMap = new Map() const matchers: TMatcherRecord[] = [] // TODO: allow custom encode/decode functions @@ -376,27 +413,27 @@ export function createCompiledMatcher< | [absoluteLocation: `/${string}`, currentLocation?: undefined] | [ relativeLocation: string, - currentLocation: NEW_LocationResolved + currentLocation: NEW_LocationResolved, ] | [ absoluteLocation: MatcherLocationAsPathAbsolute, // Same as above // currentLocation?: NEW_LocationResolved | undefined - currentLocation?: undefined + currentLocation?: undefined, ] | [ relativeLocation: MatcherLocationAsPathRelative, - currentLocation: NEW_LocationResolved + currentLocation: NEW_LocationResolved, ] | [ location: MatcherLocationAsNamed, // Same as above // currentLocation?: NEW_LocationResolved | undefined - currentLocation?: undefined + currentLocation?: undefined, ] | [ relativeLocation: MatcherLocationAsRelative, - currentLocation: NEW_LocationResolved + currentLocation: NEW_LocationResolved, ] function resolve( @@ -576,11 +613,11 @@ export function createCompiledMatcher< matcherMap.clear() } - function getMatchers() { + function getRecords() { return matchers } - function getMatcher(name: MatcherName) { + function getRecord(name: RecordName) { return matcherMap.get(name) } @@ -590,8 +627,8 @@ export function createCompiledMatcher< addMatcher, removeMatcher, clearMatchers, - getMatcher, - getMatchers, + getRecord, + getRecords, } } @@ -604,7 +641,7 @@ export function createCompiledMatcher< * @param matcher - new matcher to be inserted * @param matchers - existing matchers */ -function findInsertionIndex>( +function findInsertionIndex( matcher: T, matchers: T[] ) { @@ -641,7 +678,7 @@ function findInsertionIndex>( return upper } -function getInsertionAncestor>(matcher: T) { +function getInsertionAncestor(matcher: T) { let ancestor: T | undefined = matcher while ((ancestor = ancestor.parent)) { @@ -657,7 +694,7 @@ function getInsertionAncestor>(matcher: T) { * Checks if a record or any of its parent is an alias * @param record */ -function isAliasRecord>( +function isAliasRecord( record: T | undefined ): boolean { while (record) { diff --git a/packages/router/src/router.ts b/packages/router/src/router.ts index 5746d396c..01884ea2a 100644 --- a/packages/router/src/router.ts +++ b/packages/router/src/router.ts @@ -87,11 +87,41 @@ export interface RouterOptions extends EXPERIMENTAL_RouterOptions_Base { * Router instance. */ export interface Router - extends EXPERIMENTAL_Router_Base { + extends EXPERIMENTAL_Router_Base { /** * Original options object passed to create the Router */ readonly options: RouterOptions + + /** + * Add a new {@link EXPERIMENTAL_RouteRecordRaw | route record} as the child of an existing route. + * + * @param parentName - Parent Route Record where `route` should be appended at + * @param route - Route Record to add + */ + addRoute( + // NOTE: it could be `keyof RouteMap` but the point of dynamic routes is not knowing the routes at build + parentName: NonNullable, + route: RouteRecordRaw + ): () => void + /** + * Add a new {@link EXPERIMENTAL_RouteRecordRaw | route record} to the router. + * + * @param route - Route Record to add + */ + addRoute(route: RouteRecordRaw): () => void + + /** + * Remove an existing route by its name. + * + * @param name - Name of the route to remove + */ + removeRoute(name: NonNullable): void + + /** + * Delete all routes from the router. + */ + clearRoutes(): void } /** diff --git a/packages/router/test-dts/index.d.ts b/packages/router/test-dts/index.d.ts index e7c8be7b6..62b091993 100644 --- a/packages/router/test-dts/index.d.ts +++ b/packages/router/test-dts/index.d.ts @@ -1,2 +1,2 @@ -export * from '../dist/vue-router' -// export * from '../src' +// export * from '../dist/vue-router' +export * from '../src' From 46aa26d0669058b21d7f7d6309c4a2a6a51c288e Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Fri, 18 Jul 2025 18:13:30 +0200 Subject: [PATCH 39/40] build: use tsdown --- package.json | 16 +- packages/docs/package.json | 4 +- packages/playground/package.json | 12 +- packages/router/package.json | 29 +- packages/router/rollup.config.mjs | 7 - packages/router/src/experimental/router.ts | 8 +- packages/router/src/global.d.ts | 2 - packages/router/test-dts/index.d.ts | 5 +- packages/router/tsdown.config.ts | 109 + pnpm-lock.yaml | 3380 ++++++++++++++------ 10 files changed, 2609 insertions(+), 963 deletions(-) create mode 100644 packages/router/tsdown.config.ts diff --git a/package.json b/package.json index 83c776139..ecb70d01c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@vue/router-root", "private": true, - "packageManager": "pnpm@9.10.0", + "packageManager": "pnpm@10.13.1", "type": "module", "engines": { "node": ">=20.9.0" @@ -36,7 +36,7 @@ "brotli": "^1.3.3", "chalk": "^5.4.1", "enquirer": "^2.4.1", - "execa": "^9.5.2", + "execa": "^9.6.0", "globby": "^14.1.0", "lint-staged": "^15.5.1", "minimist": "^1.2.8", @@ -44,9 +44,9 @@ "prettier": "^3.5.3", "semver": "^7.7.1", "simple-git-hooks": "^2.13.0", - "typedoc": "^0.26.11", - "typedoc-plugin-markdown": "^4.2.10", - "typescript": "~5.6.3", + "typedoc": "^0.28.7", + "typedoc-plugin-markdown": "^4.7.0", + "typescript": "~5.8.3", "vitest": "^2.1.9" }, "simple-git-hooks": { @@ -71,7 +71,11 @@ "@types/react", "react-dom" ] - } + }, + "onlyBuiltDependencies": [ + "chromedriver", + "geckodriver" + ] }, "volta": { "node": "20.11.1" diff --git a/packages/docs/package.json b/packages/docs/package.json index fae6fe1f4..4765889c2 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -13,8 +13,8 @@ "docs:preview": "vitepress preview ." }, "dependencies": { - "simple-git": "^3.27.0", - "vitepress": "1.5.0", + "simple-git": "^3.28.0", + "vitepress": "1.6.3", "vitepress-translation-helper": "^0.2.1", "vue-router": "workspace:*" } diff --git a/packages/playground/package.json b/packages/playground/package.json index 86fb91867..30219916b 100644 --- a/packages/playground/package.json +++ b/packages/playground/package.json @@ -9,15 +9,15 @@ "preview": "vite preview --port 4173" }, "dependencies": { - "vue": "~3.5.13" + "vue": "~3.5.17" }, "devDependencies": { "@types/node": "^20.17.31", - "@vitejs/plugin-vue": "^5.2.3", - "@vue/compiler-sfc": "~3.5.13", - "@vue/tsconfig": "^0.6.0", - "vite": "^5.4.18", + "@vitejs/plugin-vue": "^5.2.4", + "@vue/compiler-sfc": "~3.5.17", + "@vue/tsconfig": "^0.7.0", + "vite": "^5.4.19", "vue-router": "workspace:*", - "vue-tsc": "^2.2.10" + "vue-tsc": "^2.2.12" } } diff --git a/packages/router/package.json b/packages/router/package.json index ba06a10be..0ccbbdeed 100644 --- a/packages/router/package.json +++ b/packages/router/package.json @@ -5,10 +5,10 @@ "unpkg": "dist/vue-router.global.js", "jsdelivr": "dist/vue-router.global.js", "module": "dist/vue-router.mjs", - "types": "dist/vue-router.d.ts", + "types": "dist/vue-router.d.mts", "exports": { ".": { - "types": "./dist/vue-router.d.ts", + "types": "./dist/vue-router.d.mts", "node": { "import": { "production": "./dist/vue-router.node.mjs", @@ -80,7 +80,7 @@ "files": [ "index.js", "dist/*.{js,cjs,mjs}", - "dist/vue-router.d.ts", + "dist/vue-router.d.{ts,mts}", "vue-router-auto.d.ts", "vue-router-auto-routes.d.ts", "vetur/tags.json", @@ -90,8 +90,8 @@ "scripts": { "dev": "vitest --ui", "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 1", - "build": "rimraf dist && rollup -c rollup.config.mjs", - "build:dts": "api-extractor run --local --verbose && tail -n +10 src/globalExtensions.ts >> dist/vue-router.d.ts", + "build": "tsdown", + "build:dts": "tail -n +10 src/globalExtensions.ts >> dist/vue-router.d.mts", "build:playground": "vue-tsc --noEmit && vite build --config playground/vite.config.ts", "build:e2e": "vue-tsc --noEmit && vite build --config e2e/vite.config.mjs", "build:size": "pnpm run build && rollup -c size-checks/rollup.config.mjs", @@ -117,7 +117,7 @@ "@vue/devtools-api": "^6.6.4" }, "devDependencies": { - "@microsoft/api-extractor": "^7.48.0", + "@microsoft/api-extractor": "^7.52.8", "@rollup/plugin-alias": "^5.1.1", "@rollup/plugin-commonjs": "^25.0.8", "@rollup/plugin-node-resolve": "^15.3.1", @@ -125,17 +125,18 @@ "@rollup/plugin-terser": "^0.4.4", "@types/jsdom": "^21.1.7", "@types/nightwatch": "^2.3.32", - "@vitejs/plugin-vue": "^5.2.3", - "@vue/compiler-sfc": "~3.5.13", - "@vue/server-renderer": "~3.5.13", + "@typescript/native-preview": "7.0.0-dev.20250718.1", + "@vitejs/plugin-vue": "^5.2.4", + "@vue/compiler-sfc": "~3.5.17", + "@vue/server-renderer": "~3.5.17", "@vue/test-utils": "^2.4.6", "browserstack-local": "^1.5.6", - "chromedriver": "^131.0.5", + "chromedriver": "^138.0.3", "connect-history-api-fallback": "^1.6.0", "conventional-changelog-cli": "^2.2.2", "dotenv": "^16.5.0", "faked-promise": "^2.2.2", - "geckodriver": "^4.5.1", + "geckodriver": "^5.0.0", "happy-dom": "^15.11.7", "nightwatch": "^2.6.25", "nightwatch-helpers": "^1.2.0", @@ -143,7 +144,9 @@ "rollup": "^3.29.5", "rollup-plugin-analyzer": "^4.0.0", "rollup-plugin-typescript2": "^0.36.0", - "vite": "^5.4.18", - "vue": "~3.5.13" + "tsdown": "^0.12.9", + "tsup": "^8.5.0", + "vite": "^5.4.19", + "vue": "~3.5.17" } } diff --git a/packages/router/rollup.config.mjs b/packages/router/rollup.config.mjs index fc8aaa3b0..ff2e4b0b1 100644 --- a/packages/router/rollup.config.mjs +++ b/packages/router/rollup.config.mjs @@ -182,8 +182,6 @@ function createReplacePlugin( isNodeBuild ) { const replacements = { - __COMMIT__: `"${process.env.COMMIT}"`, - __VERSION__: `"${pkg.version}"`, __DEV__: isBundlerESMBuild ? // preserve to be handled by bundlers `(process.env.NODE_ENV !== 'production')` @@ -196,11 +194,6 @@ function createReplacePlugin( __FEATURE_PROD_DEVTOOLS__: isBundlerESMBuild ? `__VUE_PROD_DEVTOOLS__` : 'false', - // is targeting bundlers? - __BUNDLER__: JSON.stringify(isBundlerESMBuild), - __GLOBAL__: JSON.stringify(isGlobalBuild), - // is targeting Node (SSR)? - __NODE_JS__: JSON.stringify(isNodeBuild), } // allow inline overrides like //__RUNTIME_COMPILE__=true yarn build diff --git a/packages/router/src/experimental/router.ts b/packages/router/src/experimental/router.ts index e6a933c7a..8083492e2 100644 --- a/packages/router/src/experimental/router.ts +++ b/packages/router/src/experimental/router.ts @@ -205,14 +205,14 @@ export interface EXPERIMENTAL_RouteRecordNormalized mods: Record } +// TODO: probably need some generic types +// , /** * Options to initialize an experimental {@link EXPERIMENTAL_Router} instance. * @experimental */ -export interface EXPERIMENTAL_RouterOptions< - // TODO: probably need some generic types - // TResolver extends NEW_RouterResolver_Base, ->extends EXPERIMENTAL_RouterOptions_Base { +export interface EXPERIMENTAL_RouterOptions + extends EXPERIMENTAL_RouterOptions_Base { /** * Matcher to use to resolve routes. * diff --git a/packages/router/src/global.d.ts b/packages/router/src/global.d.ts index e5206cd2d..a3477174e 100644 --- a/packages/router/src/global.d.ts +++ b/packages/router/src/global.d.ts @@ -3,5 +3,3 @@ declare var __DEV__: boolean declare var __TEST__: boolean declare var __FEATURE_PROD_DEVTOOLS__: boolean declare var __BROWSER__: boolean -declare var __NODE_JS__: boolean -declare var __CI__: boolean diff --git a/packages/router/test-dts/index.d.ts b/packages/router/test-dts/index.d.ts index 62b091993..09f58a38f 100644 --- a/packages/router/test-dts/index.d.ts +++ b/packages/router/test-dts/index.d.ts @@ -1,2 +1,3 @@ -// export * from '../dist/vue-router' -export * from '../src' +export * from '../dist/vue-router.mjs' +// Uncomment for testing local changes without having to build +// export * from '../src' diff --git a/packages/router/tsdown.config.ts b/packages/router/tsdown.config.ts new file mode 100644 index 000000000..23822ed91 --- /dev/null +++ b/packages/router/tsdown.config.ts @@ -0,0 +1,109 @@ +import { type Options } from 'tsdown' +import pkg from './package.json' with { type: 'json' } + +const banner = ` +/*! + * ${pkg.name} v${pkg.version} + * (c) ${new Date().getFullYear()} Eduardo San Martin Morote + * @license MIT + */ +`.trim() + +const commonOptions = { + sourcemap: false, + format: ['esm'], + entry: { + 'vue-router': './src/index.ts', + }, + outputOptions: { + banner, + name: 'VueRouter', + globals: { + vue: 'Vue', + '@vue/devtools-api': 'VueDevtoolsApi', + }, + }, + define: { + __DEV__: `(process.env.NODE_ENV !== 'production')`, + // this is only used during tests + __TEST__: 'false', + // If the build is expected to run directly in the browser (global / esm builds) + // FIXME: makes no sense anymore + __BROWSER__: 'true', + // is replaced by the vite vue plugin + __FEATURE_PROD_DEVTOOLS__: `__VUE_PROD_DEVTOOLS__`, + }, + dts: false, +} satisfies Options + +const esm = { + ...commonOptions, + platform: 'neutral', + dts: true, + // sourcemap: true, +} satisfies Options + +const cjs = { + ...commonOptions, + format: 'cjs', + outputOptions: { + ...commonOptions.outputOptions, + dir: undefined, // must be unset with file + file: 'dist/vue-router.cjs', + }, + define: { + ...commonOptions.define, + // TODO: what is the right value + __BROWSER__: 'false', + __FEATURE_PROD_DEVTOOLS__: `false`, + }, +} satisfies Options + +const cjsProd = { + ...cjs, + minify: true, + outputOptions: { + ...cjs.outputOptions, + file: 'dist/vue-router.prod.cjs', + }, +} satisfies Options + +const iife = { + ...commonOptions, + format: 'iife', + // TODO: remove when upgrading to devtools-api v7 because it's too big + noExternal: ['@vue/devtools-api'], + outputOptions: { + ...commonOptions.outputOptions, + dir: undefined, // must be unset with file + file: 'dist/vue-router.global.js', + }, + define: { + ...commonOptions.define, + __DEV__: 'true', + __FEATURE_PROD_DEVTOOLS__: `true`, + }, +} satisfies Options + +const iifeProd = { + ...iife, + minify: true, + outputOptions: { + ...iife.outputOptions, + file: 'dist/vue-router.global.prod.js', + }, + define: { + ...iife.define, + __DEV__: 'false', + __FEATURE_PROD_DEVTOOLS__: `false`, + }, +} satisfies Options + +export default [ + // + esm, + cjs, + cjsProd, + iife, + iifeProd, +] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9e238c902..ca41b93a5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,7 +10,7 @@ importers: devDependencies: '@vitest/coverage-v8': specifier: ^2.1.9 - version: 2.1.9(vitest@2.1.9(@types/node@22.15.2)(@vitest/ui@2.1.9)(happy-dom@15.11.7)(jsdom@19.0.0)(terser@5.32.0)) + version: 2.1.9(vitest@2.1.9) '@vitest/ui': specifier: ^2.1.9 version: 2.1.9(vitest@2.1.9) @@ -24,8 +24,8 @@ importers: specifier: ^2.4.1 version: 2.4.1 execa: - specifier: ^9.5.2 - version: 9.5.2 + specifier: ^9.6.0 + version: 9.6.0 globby: specifier: ^14.1.0 version: 14.1.0 @@ -48,29 +48,29 @@ importers: specifier: ^2.13.0 version: 2.13.0 typedoc: - specifier: ^0.26.11 - version: 0.26.11(typescript@5.6.3) + specifier: ^0.28.7 + version: 0.28.7(typescript@5.8.3) typedoc-plugin-markdown: - specifier: ^4.2.10 - version: 4.2.10(typedoc@0.26.11(typescript@5.6.3)) + specifier: ^4.7.0 + version: 4.7.0(typedoc@0.28.7(typescript@5.8.3)) typescript: - specifier: ~5.6.3 - version: 5.6.3 + specifier: ~5.8.3 + version: 5.8.3 vitest: specifier: ^2.1.9 - version: 2.1.9(@types/node@22.15.2)(@vitest/ui@2.1.9)(happy-dom@15.11.7)(jsdom@19.0.0)(terser@5.32.0) + version: 2.1.9(@types/node@24.0.14)(@vitest/ui@2.1.9)(happy-dom@18.0.1)(jsdom@26.1.0)(terser@5.43.1) packages/docs: dependencies: simple-git: - specifier: ^3.27.0 - version: 3.27.0 + specifier: ^3.28.0 + version: 3.28.0 vitepress: - specifier: 1.5.0 - version: 1.5.0(@algolia/client-search@5.15.0)(@types/node@22.15.2)(axios@1.7.7)(postcss@8.4.49)(search-insights@2.17.1)(terser@5.32.0)(typescript@5.6.3) + specifier: 1.6.3 + version: 1.6.3(@algolia/client-search@5.34.0)(@types/node@24.0.14)(axios@1.10.0)(postcss@8.5.6)(search-insights@2.17.3)(terser@5.43.1)(typescript@5.8.3) vitepress-translation-helper: specifier: ^0.2.1 - version: 0.2.1(vitepress@1.5.0(@algolia/client-search@5.15.0)(@types/node@22.15.2)(axios@1.7.7)(postcss@8.4.49)(search-insights@2.17.1)(terser@5.32.0)(typescript@5.6.3))(vue@3.5.13(typescript@5.6.3)) + version: 0.2.1(vitepress@1.6.3(@algolia/client-search@5.34.0)(@types/node@24.0.14)(axios@1.10.0)(postcss@8.5.6)(search-insights@2.17.3)(terser@5.43.1)(typescript@5.8.3))(vue@3.5.17(typescript@5.8.3)) vue-router: specifier: workspace:* version: link:../router @@ -78,30 +78,30 @@ importers: packages/playground: dependencies: vue: - specifier: ~3.5.13 - version: 3.5.13(typescript@5.6.3) + specifier: ~3.5.17 + version: 3.5.17(typescript@5.8.3) devDependencies: '@types/node': specifier: ^20.17.31 - version: 20.17.31 + version: 20.19.8 '@vitejs/plugin-vue': - specifier: ^5.2.3 - version: 5.2.3(vite@5.4.18(@types/node@20.17.31)(terser@5.32.0))(vue@3.5.13(typescript@5.6.3)) + specifier: ^5.2.4 + version: 5.2.4(vite@5.4.19(@types/node@20.19.8)(terser@5.43.1))(vue@3.5.17(typescript@5.8.3)) '@vue/compiler-sfc': - specifier: ~3.5.13 - version: 3.5.13 + specifier: ~3.5.17 + version: 3.5.17 '@vue/tsconfig': - specifier: ^0.6.0 - version: 0.6.0(typescript@5.6.3)(vue@3.5.13(typescript@5.6.3)) + specifier: ^0.7.0 + version: 0.7.0(typescript@5.8.3)(vue@3.5.17(typescript@5.8.3)) vite: - specifier: ^5.4.18 - version: 5.4.18(@types/node@20.17.31)(terser@5.32.0) + specifier: ^5.4.19 + version: 5.4.19(@types/node@20.19.8)(terser@5.43.1) vue-router: specifier: workspace:* version: link:../router vue-tsc: - specifier: ^2.2.10 - version: 2.2.10(typescript@5.6.3) + specifier: ^2.2.12 + version: 2.2.12(typescript@5.8.3) packages/router: dependencies: @@ -110,8 +110,8 @@ importers: version: 6.6.4 devDependencies: '@microsoft/api-extractor': - specifier: ^7.48.0 - version: 7.48.0(@types/node@22.15.2) + specifier: ^7.52.8 + version: 7.52.8(@types/node@24.0.14) '@rollup/plugin-alias': specifier: ^5.1.1 version: 5.1.1(rollup@3.29.5) @@ -133,15 +133,18 @@ importers: '@types/nightwatch': specifier: ^2.3.32 version: 2.3.32 + '@typescript/native-preview': + specifier: 7.0.0-dev.20250718.1 + version: 7.0.0-dev.20250718.1 '@vitejs/plugin-vue': - specifier: ^5.2.3 - version: 5.2.3(vite@5.4.18(@types/node@22.15.2)(terser@5.32.0))(vue@3.5.13(typescript@5.6.3)) + specifier: ^5.2.4 + version: 5.2.4(vite@5.4.19(@types/node@24.0.14)(terser@5.43.1))(vue@3.5.17(typescript@5.8.3)) '@vue/compiler-sfc': - specifier: ~3.5.13 - version: 3.5.13 + specifier: ~3.5.17 + version: 3.5.17 '@vue/server-renderer': - specifier: ~3.5.13 - version: 3.5.13(vue@3.5.13(typescript@5.6.3)) + specifier: ~3.5.17 + version: 3.5.17(vue@3.5.17(typescript@5.8.3)) '@vue/test-utils': specifier: ^2.4.6 version: 2.4.6 @@ -149,8 +152,8 @@ importers: specifier: ^1.5.6 version: 1.5.6 chromedriver: - specifier: ^131.0.5 - version: 131.0.5 + specifier: ^138.0.3 + version: 138.0.3 connect-history-api-fallback: specifier: ^1.6.0 version: 1.6.0 @@ -164,14 +167,14 @@ importers: specifier: ^2.2.2 version: 2.2.2 geckodriver: - specifier: ^4.5.1 - version: 4.5.1 + specifier: ^5.0.0 + version: 5.0.0 happy-dom: specifier: ^15.11.7 version: 15.11.7 nightwatch: specifier: ^2.6.25 - version: 2.6.25(chromedriver@131.0.5)(geckodriver@4.5.1) + version: 2.6.25(chromedriver@138.0.3)(geckodriver@5.0.0) nightwatch-helpers: specifier: ^1.2.0 version: 1.2.0 @@ -186,13 +189,19 @@ importers: version: 4.0.0 rollup-plugin-typescript2: specifier: ^0.36.0 - version: 0.36.0(rollup@3.29.5)(typescript@5.6.3) + version: 0.36.0(rollup@3.29.5)(typescript@5.8.3) + tsdown: + specifier: ^0.12.9 + version: 0.12.9(@typescript/native-preview@7.0.0-dev.20250718.1)(typescript@5.8.3)(vue-tsc@2.2.12(typescript@5.8.3)) + tsup: + specifier: ^8.5.0 + version: 8.5.0(@microsoft/api-extractor@7.52.8(@types/node@24.0.14))(jiti@2.4.2)(postcss@8.5.6)(typescript@5.8.3)(yaml@2.8.0) vite: - specifier: ^5.4.18 - version: 5.4.18(@types/node@22.15.2)(terser@5.32.0) + specifier: ^5.4.19 + version: 5.4.19(@types/node@24.0.14)(terser@5.43.1) vue: - specifier: ~3.5.13 - version: 3.5.13(typescript@5.6.3) + specifier: ~3.5.17 + version: 3.5.17(typescript@5.8.3) packages: @@ -216,85 +225,92 @@ packages: '@algolia/client-search': '>= 4.9.1 < 6' algoliasearch: '>= 4.9.1 < 6' - '@algolia/client-abtesting@5.15.0': - resolution: {integrity: sha512-FaEM40iuiv1mAipYyiptP4EyxkJ8qHfowCpEeusdHUC4C7spATJYArD2rX3AxkVeREkDIgYEOuXcwKUbDCr7Nw==} + '@algolia/client-abtesting@5.34.0': + resolution: {integrity: sha512-d6ardhDtQsnMpyr/rPrS3YuIE9NYpY4rftkC7Ap9tyuhZ/+V3E/LH+9uEewPguKzVqduApdwJzYq2k+vAXVEbQ==} engines: {node: '>= 14.0.0'} - '@algolia/client-analytics@5.15.0': - resolution: {integrity: sha512-lho0gTFsQDIdCwyUKTtMuf9nCLwq9jOGlLGIeQGKDxXF7HbiAysFIu5QW/iQr1LzMgDyM9NH7K98KY+BiIFriQ==} + '@algolia/client-analytics@5.34.0': + resolution: {integrity: sha512-WXIByjHNA106JO1Dj6b4viSX/yMN3oIB4qXr2MmyEmNq0MgfuPfPw8ayLRIZPa9Dp27hvM3G8MWJ4RG978HYFw==} engines: {node: '>= 14.0.0'} - '@algolia/client-common@5.15.0': - resolution: {integrity: sha512-IofrVh213VLsDkPoSKMeM9Dshrv28jhDlBDLRcVJQvlL8pzue7PEB1EZ4UoJFYS3NSn7JOcJ/V+olRQzXlJj1w==} + '@algolia/client-common@5.34.0': + resolution: {integrity: sha512-JeN1XJLZIkkv6yK0KT93CIXXk+cDPUGNg5xeH4fN9ZykYFDWYRyqgaDo+qvg4RXC3WWkdQ+hogQuuCk4Y3Eotw==} engines: {node: '>= 14.0.0'} - '@algolia/client-insights@5.15.0': - resolution: {integrity: sha512-bDDEQGfFidDi0UQUCbxXOCdphbVAgbVmxvaV75cypBTQkJ+ABx/Npw7LkFGw1FsoVrttlrrQbwjvUB6mLVKs/w==} + '@algolia/client-insights@5.34.0': + resolution: {integrity: sha512-gdFlcQa+TWXJUsihHDlreFWniKPFIQ15i5oynCY4m9K3DCex5g5cVj9VG4Hsquxf2t6Y0yv8w6MvVTGDO8oRLw==} engines: {node: '>= 14.0.0'} - '@algolia/client-personalization@5.15.0': - resolution: {integrity: sha512-LfaZqLUWxdYFq44QrasCDED5bSYOswpQjSiIL7Q5fYlefAAUO95PzBPKCfUhSwhb4rKxigHfDkd81AvEicIEoA==} + '@algolia/client-personalization@5.34.0': + resolution: {integrity: sha512-g91NHhIZDkh1IUeNtsUd8V/ZxuBc2ByOfDqhCkoQY3Z/mZszhpn3Czn6AR5pE81fx793vMaiOZvQVB5QttArkQ==} engines: {node: '>= 14.0.0'} - '@algolia/client-query-suggestions@5.15.0': - resolution: {integrity: sha512-wu8GVluiZ5+il8WIRsGKu8VxMK9dAlr225h878GGtpTL6VBvwyJvAyLdZsfFIpY0iN++jiNb31q2C1PlPL+n/A==} + '@algolia/client-query-suggestions@5.34.0': + resolution: {integrity: sha512-cvRApDfFrlJ3Vcn37U4Nd/7S6T8cx7FW3mVLJPqkkzixv8DQ/yV+x4VLirxOtGDdq3KohcIbIGWbg1QuyOZRvQ==} engines: {node: '>= 14.0.0'} - '@algolia/client-search@5.15.0': - resolution: {integrity: sha512-Z32gEMrRRpEta5UqVQA612sLdoqY3AovvUPClDfMxYrbdDAebmGDVPtSogUba1FZ4pP5dx20D3OV3reogLKsRA==} + '@algolia/client-search@5.34.0': + resolution: {integrity: sha512-m9tK4IqJmn+flEPRtuxuHgiHmrKV0su5fuVwVpq8/es4DMjWMgX1a7Lg1PktvO8AbKaTp9kTtBAPnwXpuCwmEg==} engines: {node: '>= 14.0.0'} - '@algolia/ingestion@1.15.0': - resolution: {integrity: sha512-MkqkAxBQxtQ5if/EX2IPqFA7LothghVyvPoRNA/meS2AW2qkHwcxjuiBxv4H6mnAVEPfJlhu9rkdVz9LgCBgJg==} + '@algolia/ingestion@1.34.0': + resolution: {integrity: sha512-2rxy4XoeRtIpzxEh5u5UgDC5HY4XbNdjzNgFx1eDrfFkSHpEVjirtLhISMy2N5uSFqYu1uUby5/NC1Soq8J7iw==} engines: {node: '>= 14.0.0'} - '@algolia/monitoring@1.15.0': - resolution: {integrity: sha512-QPrFnnGLMMdRa8t/4bs7XilPYnoUXDY8PMQJ1sf9ZFwhUysYYhQNX34/enoO0LBjpoOY6rLpha39YQEFbzgKyQ==} + '@algolia/monitoring@1.34.0': + resolution: {integrity: sha512-OJiDhlJX8ZdWAndc50Z6aUEW/YmnhFK2ul3rahMw5/c9Damh7+oY9SufoK2LimJejy+65Qka06YPG29v2G/vww==} engines: {node: '>= 14.0.0'} - '@algolia/recommend@5.15.0': - resolution: {integrity: sha512-5eupMwSqMLDObgSMF0XG958zR6GJP3f7jHDQ3/WlzCM9/YIJiWIUoJFGsko9GYsA5xbLDHE/PhWtq4chcCdaGQ==} + '@algolia/recommend@5.34.0': + resolution: {integrity: sha512-fzNQZAdVxu/Gnbavy8KW5gurApwdYcPW6+pjO7Pw8V5drCR3eSqnOxSvp79rhscDX8ezwqMqqK4F3Hsq+KpRzg==} engines: {node: '>= 14.0.0'} - '@algolia/requester-browser-xhr@5.15.0': - resolution: {integrity: sha512-Po/GNib6QKruC3XE+WKP1HwVSfCDaZcXu48kD+gwmtDlqHWKc7Bq9lrS0sNZ456rfCKhXksOmMfUs4wRM/Y96w==} + '@algolia/requester-browser-xhr@5.34.0': + resolution: {integrity: sha512-gEI0xjzA/xvMpEdYmgQnf6AQKllhgKRtnEWmwDrnct+YPIruEHlx1dd7nRJTy/33MiYcCxkB4khXpNrHuqgp3Q==} engines: {node: '>= 14.0.0'} - '@algolia/requester-fetch@5.15.0': - resolution: {integrity: sha512-rOZ+c0P7ajmccAvpeeNrUmEKoliYFL8aOR5qGW5pFq3oj3Iept7Y5mEtEsOBYsRt6qLnaXn4zUKf+N8nvJpcIw==} + '@algolia/requester-fetch@5.34.0': + resolution: {integrity: sha512-5SwGOttpbACT4jXzfSJ3mnTcF46SVNSnZ1JjxC3qBa3qKi4U0CJGzuVVy3L798u8dG5H0SZ2MAB5v7180Gnqew==} engines: {node: '>= 14.0.0'} - '@algolia/requester-node-http@5.15.0': - resolution: {integrity: sha512-b1jTpbFf9LnQHEJP5ddDJKE2sAlhYd7EVSOWgzo/27n/SfCoHfqD0VWntnWYD83PnOKvfe8auZ2+xCb0TXotrQ==} + '@algolia/requester-node-http@5.34.0': + resolution: {integrity: sha512-409XlyIyEXrxyGjWxd0q5RASizHSRVUU0AXPCEdqnbcGEzbCgL1n7oYI8YxzE/RqZLha+PNwWCcTVn7EE5tyyQ==} engines: {node: '>= 14.0.0'} '@ampproject/remapping@2.3.0': resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} + '@asamuzakjp/css-color@3.2.0': + resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} + '@babel/code-frame@7.22.10': resolution: {integrity: sha512-/KKIMG4UEL35WmI9OlvMhurwtytjvXoFcGNrOvyG9zIzA8YmPjVtIZUf7b05+TPO7G7/GEmLHDaoCgACHl9hhA==} engines: {node: '>=6.9.0'} - '@babel/helper-string-parser@7.25.9': - resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} + '@babel/generator@7.28.0': + resolution: {integrity: sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.25.9': - resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} engines: {node: '>=6.9.0'} '@babel/highlight@7.22.10': resolution: {integrity: sha512-78aUtVcT7MUscr0K5mIEnkwxPE0MaxkR5RxRwuHaQ+JuU5AmTPhY+do2mdzVTnIJJpyBglql2pehuBIWHug+WQ==} engines: {node: '>=6.9.0'} - '@babel/parser@7.26.2': - resolution: {integrity: sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==} + '@babel/parser@7.28.0': + resolution: {integrity: sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==} engines: {node: '>=6.0.0'} hasBin: true - '@babel/types@7.26.0': - resolution: {integrity: sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==} + '@babel/types@7.28.1': + resolution: {integrity: sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==} engines: {node: '>=6.9.0'} '@bcoe/v8-coverage@0.2.3': @@ -304,14 +320,42 @@ packages: resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} - '@docsearch/css@3.8.0': - resolution: {integrity: sha512-pieeipSOW4sQ0+bE5UFC51AOZp9NGxg89wAlZ1BAQFaiRAGK1IKUaPQ0UGZeNctJXyqZ1UvBtOQh2HH+U5GtmA==} + '@csstools/color-helpers@5.0.2': + resolution: {integrity: sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==} + engines: {node: '>=18'} + + '@csstools/css-calc@2.1.4': + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-color-parser@3.0.10': + resolution: {integrity: sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + + '@docsearch/css@3.8.2': + resolution: {integrity: sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ==} - '@docsearch/js@3.8.0': - resolution: {integrity: sha512-PVuV629f5UcYRtBWqK7ID6vNL5647+2ADJypwTjfeBIrJfwPuHtzLy39hMGMfFK+0xgRyhTR0FZ83EkdEraBlg==} + '@docsearch/js@3.8.2': + resolution: {integrity: sha512-Q5wY66qHn0SwA7Taa0aDbHiJvaFJLOJyHmooQ7y8hlwwQLQ/5WwCcoX0g7ii04Qi2DJlHsd0XXzJ8Ypw9+9YmQ==} - '@docsearch/react@3.8.0': - resolution: {integrity: sha512-WnFK720+iwTVt94CxY3u+FgX6exb3BfN5kE9xUY6uuAH/9W/UFboBZFLlrw/zxFRHoHZCOXRtOylsXF+6LHI+Q==} + '@docsearch/react@3.8.2': + resolution: {integrity: sha512-xCRrJQlTt8N9GU0DG4ptwHRkfnSnD/YpdeaXe02iKfqs97TkZJv60yE+1eq/tjPcVnTW8dP5qLP7itifFVV5eg==} peerDependencies: '@types/react': '>= 16.8.0 < 19.0.0' react: '>= 16.8.0 < 19.0.0' @@ -327,150 +371,318 @@ packages: search-insights: optional: true + '@emnapi/core@1.4.5': + resolution: {integrity: sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==} + + '@emnapi/runtime@1.4.5': + resolution: {integrity: sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==} + + '@emnapi/wasi-threads@1.0.4': + resolution: {integrity: sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==} + '@esbuild/aix-ppc64@0.21.5': resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} engines: {node: '>=12'} cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.25.6': + resolution: {integrity: sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.21.5': resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} engines: {node: '>=12'} cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.25.6': + resolution: {integrity: sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.21.5': resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} engines: {node: '>=12'} cpu: [arm] os: [android] + '@esbuild/android-arm@0.25.6': + resolution: {integrity: sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.21.5': resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} engines: {node: '>=12'} cpu: [x64] os: [android] + '@esbuild/android-x64@0.25.6': + resolution: {integrity: sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.21.5': resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} engines: {node: '>=12'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.25.6': + resolution: {integrity: sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.21.5': resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} engines: {node: '>=12'} cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.25.6': + resolution: {integrity: sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.21.5': resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} engines: {node: '>=12'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.25.6': + resolution: {integrity: sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.21.5': resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} engines: {node: '>=12'} cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.25.6': + resolution: {integrity: sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.21.5': resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} engines: {node: '>=12'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.25.6': + resolution: {integrity: sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.21.5': resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} engines: {node: '>=12'} cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.25.6': + resolution: {integrity: sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.21.5': resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} engines: {node: '>=12'} cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.25.6': + resolution: {integrity: sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.21.5': resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} engines: {node: '>=12'} cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.25.6': + resolution: {integrity: sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.21.5': resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} engines: {node: '>=12'} cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.25.6': + resolution: {integrity: sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.21.5': resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} engines: {node: '>=12'} cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.25.6': + resolution: {integrity: sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.21.5': resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} engines: {node: '>=12'} cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.25.6': + resolution: {integrity: sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.21.5': resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} engines: {node: '>=12'} cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.25.6': + resolution: {integrity: sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.21.5': resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} engines: {node: '>=12'} cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.25.6': + resolution: {integrity: sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.6': + resolution: {integrity: sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.21.5': resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} engines: {node: '>=12'} cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.25.6': + resolution: {integrity: sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.6': + resolution: {integrity: sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.21.5': resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} engines: {node: '>=12'} cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.25.6': + resolution: {integrity: sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.6': + resolution: {integrity: sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.21.5': resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} engines: {node: '>=12'} cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.25.6': + resolution: {integrity: sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.21.5': resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} engines: {node: '>=12'} cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.25.6': + resolution: {integrity: sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.21.5': resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} engines: {node: '>=12'} cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.25.6': + resolution: {integrity: sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.21.5': resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} engines: {node: '>=12'} cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.25.6': + resolution: {integrity: sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@gerrit0/mini-shiki@3.8.0': + resolution: {integrity: sha512-tloLVqvvoyv636PilYZwNhCmZ+xxgRicysMvpKdZ4Y6+9IH6v4lp7GodbDDncApNQjflwTSnXuYQoe3el5C59w==} + '@hutson/parse-repository-url@3.0.2': resolution: {integrity: sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q==} engines: {node: '>=6.9.0'} - '@iconify-json/simple-icons@1.2.13': - resolution: {integrity: sha512-rRQjMoIt/kPfaD+fnBC9YZQpso3hkn8xPeadl+YWhscJ5SVUCdB9oTeR9VIpt+/5Yi8vEkh2UOWFPq4lz3ee2A==} + '@iconify-json/simple-icons@1.2.43': + resolution: {integrity: sha512-JERgKGFRfZdyjGyTvVBVW5rftahy9tNUX+P+0QUnbaAEWvEMexXHE9863YVMVrIRhoj/HybGsibg8ZWieo/NDg==} '@iconify/types@2.0.0': resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} @@ -483,26 +695,21 @@ packages: resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} engines: {node: '>=8'} - '@jridgewell/gen-mapping@0.3.5': - resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} - engines: {node: '>=6.0.0'} - - '@jridgewell/resolve-uri@3.1.1': - resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} - engines: {node: '>=6.0.0'} + '@jridgewell/gen-mapping@0.3.12': + resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==} - '@jridgewell/set-array@1.2.1': - resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} - '@jridgewell/source-map@0.3.6': - resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} + '@jridgewell/source-map@0.3.10': + resolution: {integrity: sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q==} - '@jridgewell/sourcemap-codec@1.5.0': - resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + '@jridgewell/sourcemap-codec@1.5.4': + resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==} - '@jridgewell/trace-mapping@0.3.25': - resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@jridgewell/trace-mapping@0.3.29': + resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==} '@kwsites/file-exists@1.1.1': resolution: {integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==} @@ -510,11 +717,11 @@ packages: '@kwsites/promise-deferred@1.1.1': resolution: {integrity: sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==} - '@microsoft/api-extractor-model@7.30.0': - resolution: {integrity: sha512-26/LJZBrsWDKAkOWRiQbdVgcfd1F3nyJnAiJzsAgpouPk7LtOIj7PK9aJtBaw/pUXrkotEg27RrT+Jm/q0bbug==} + '@microsoft/api-extractor-model@7.30.6': + resolution: {integrity: sha512-znmFn69wf/AIrwHya3fxX6uB5etSIn6vg4Q4RB/tb5VDDs1rqREc+AvMC/p19MUN13CZ7+V/8pkYPTj7q8tftg==} - '@microsoft/api-extractor@7.48.0': - resolution: {integrity: sha512-FMFgPjoilMUWeZXqYRlJ3gCVRhB7WU/HN88n8OLqEsmsG4zBdX/KQdtJfhq95LQTQ++zfu0Em1LLb73NqRCLYQ==} + '@microsoft/api-extractor@7.52.8': + resolution: {integrity: sha512-cszYIcjiNscDoMB1CIKZ3My61+JOhpERGlGr54i6bocvGLrcL/wo9o+RNXMBrb7XgLtKaizZWUpqRduQuHQLdg==} hasBin: true '@microsoft/tsdoc-config@0.17.1': @@ -523,6 +730,9 @@ packages: '@microsoft/tsdoc@0.15.1': resolution: {integrity: sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==} + '@napi-rs/wasm-runtime@1.0.0': + resolution: {integrity: sha512-OInwPIZhcQ+aWOBFMUXzv95RLDTBRPaNPm5kSFJaL3gVAMVxrzc0YXNsVeLPHf+4sTviOy2e5wZdvKILb7dC/w==} + '@nightwatch/chai@5.0.2': resolution: {integrity: sha512-yzILJFCcE75OPoRfBlJ80Y3Ky06ljsdrK4Ld92yhmM477vxO2GEguwnd+ldl7pdSYTcg1gSJ1bPPQrA+/Hrn+A==} engines: {node: '>=12'} @@ -545,6 +755,13 @@ packages: '@one-ini/wasm@0.1.1': resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} + '@oxc-project/runtime@0.77.2': + resolution: {integrity: sha512-oqzN82vVbqK6BnUuYDlBMlMr8mEeysMn/P8HbiB3j5rD04JvIfONCfh6SbtJTxhp1C4cjLi1evrtVTIptrln7Q==} + engines: {node: '>=6.9.0'} + + '@oxc-project/types@0.77.2': + resolution: {integrity: sha512-+ZFWJF8ZBTOIO5PiNohNIw7JBzJCybScfrhLh65tcHCAtqaQkVDonjRD1HmMV/RF3rtt3r88hzSyTqvXs4j7vw==} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -552,6 +769,83 @@ packages: '@polka/url@1.0.0-next.28': resolution: {integrity: sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==} + '@quansync/fs@0.1.3': + resolution: {integrity: sha512-G0OnZbMWEs5LhDyqy2UL17vGhSVHkQIfVojMtEWVenvj0V5S84VBgy86kJIuNsGDp2p7sTKlpSIpBUWdC35OKg==} + engines: {node: '>=20.0.0'} + + '@rolldown/binding-android-arm64@1.0.0-beta.28': + resolution: {integrity: sha512-hLb7k11KBXtO8xc7DO1OWriXWM/2FKv/R510NChqpzoI6au2aJbGUQTKJw4D8Mj7oHfY2Nwzy+sJBgWx/P8IKw==} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-beta.28': + resolution: {integrity: sha512-yRhjS3dcjfAasnJ2pTyCVm5rtfOmkGIglrFh+n9J7Zi4owJFsVVpbY7dOE3T1Op3mQ94apGN+Twtv6CIk6GFIQ==} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-beta.28': + resolution: {integrity: sha512-eOX0pjz++yVdqcDqnoZeVXUHxak2AcEgQBlEKJYaeJj+O5V3r3wSnlDVSkgD6YEAHo2IlIa89+qFHv529esY6w==} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-beta.28': + resolution: {integrity: sha512-WV1QYVMkkp/568iaEBoZhD1axFLhSO+ybCJlbmHkTFMub4wb5bmKtfuaBgjUVDDSB6JfZ6UL3Z0Q9VVHENOgsg==} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.28': + resolution: {integrity: sha512-ug/Wh9Tz4XB/CsYvaI2r5uC3vE3zrP5iDIsD+uEgFPV71BOQOfXFgZbC1zv+J1adkzWACr578aGQqW9jRj0gVA==} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.28': + resolution: {integrity: sha512-h3hzQuP+5l47wxn9+A39n1Q3i4mAvbNFJCZ8EZLrkqfsecfeZ5btIbDJTVAIQTy+uPr7uluAHIf11Jw+YkWjOQ==} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.28': + resolution: {integrity: sha512-oW5LydGtfdT8TI5HTybxi1DdMCXCmVE1ak4VrSmVKsbBZyE0bDgL1UvTS1OOvuq4PM24zQHIuSNOpgLXgVj4vQ==} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-arm64-ohos@1.0.0-beta.28': + resolution: {integrity: sha512-yeAAPMgssEkTCouUSYLrSWm+EXYBFI+ZTe8BVQkY5le51OCbqFNibtYkKZNHZBdhNRjWcSKSIuXN4MAXBz1j+g==} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.28': + resolution: {integrity: sha512-xWsylmva9L4ZFc28A9VGlF9fnrFpVxVC+kKqrBoqz2l/p5b4zRoFNtnSecivnxuPeR5Ga6W6lnpwGeWDvqBZ1Q==} + cpu: [x64] + os: [linux] + + '@rolldown/binding-linux-x64-musl@1.0.0-beta.28': + resolution: {integrity: sha512-IdtRNm70EH7/1mjqXJc4pa2MoAxo/xl9hN8VySG9BQuYfhQz+JDC+FZBc+krlVUO3cTJz/o4xI/x4kA+rLKTwA==} + cpu: [x64] + os: [linux] + + '@rolldown/binding-wasm32-wasi@1.0.0-beta.28': + resolution: {integrity: sha512-jS2G0+GtUCVcglCefScxgXeLJal0UAvVwvpy3reoC07K16k8WM/lXoYsZdpw34d5ONg0XcZpcokzA9R5K2o0lQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.28': + resolution: {integrity: sha512-K6SO4e48aqpE/E6iEaXYG1kVX3owLierZUngP44f7s6WcnNUXsX8aborZZkKDKjgfk654M/EjSI7riPQXfynIA==} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.28': + resolution: {integrity: sha512-IIAecHvlUY/oxADfA6sZFfmRx0ajY+U1rAPFT77COp11kf7irUJeD9GskFzCm+7Wm+q8Vogyh0KWqqd6f5Azgg==} + cpu: [ia32] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.28': + resolution: {integrity: sha512-eMGdPBhNkylib+7eaeC69axEjg5Y1Vie5LoKDBVaZ71jYTmtrUdna9PTUblkCIChNTQKlgxpi/eCaYmhId0aYA==} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-beta.28': + resolution: {integrity: sha512-fe3/1HZ3qJmXvkGv1kacKq2b+x9gbcyF1hnmLBVrRFEQWoOcRapQjXf8+hgyxI0EJAbnKEtrp5yhohQCFCjycw==} + '@rollup/plugin-alias@5.1.1': resolution: {integrity: sha512-PR9zDb+rOzkRb2VD+EuKB7UC41vU5DIwZ5qqCpk0KJudcWAyi8rvYOhS7+L5aZCspw1stTViLgN5v6FF1p5cgQ==} engines: {node: '>=14.0.0'} @@ -615,93 +909,193 @@ packages: cpu: [arm] os: [android] + '@rollup/rollup-android-arm-eabi@4.45.1': + resolution: {integrity: sha512-NEySIFvMY0ZQO+utJkgoMiCAjMrGvnbDLHvcmlA33UXJpYBCvlBEbMMtV837uCkS+plG2umfhn0T5mMAxGrlRA==} + cpu: [arm] + os: [android] + '@rollup/rollup-android-arm64@4.27.4': resolution: {integrity: sha512-wzKRQXISyi9UdCVRqEd0H4cMpzvHYt1f/C3CoIjES6cG++RHKhrBj2+29nPF0IB5kpy9MS71vs07fvrNGAl/iA==} cpu: [arm64] os: [android] + '@rollup/rollup-android-arm64@4.45.1': + resolution: {integrity: sha512-ujQ+sMXJkg4LRJaYreaVx7Z/VMgBBd89wGS4qMrdtfUFZ+TSY5Rs9asgjitLwzeIbhwdEhyj29zhst3L1lKsRQ==} + cpu: [arm64] + os: [android] + '@rollup/rollup-darwin-arm64@4.27.4': resolution: {integrity: sha512-PlNiRQapift4LNS8DPUHuDX/IdXiLjf8mc5vdEmUR0fF/pyy2qWwzdLjB+iZquGr8LuN4LnUoSEvKRwjSVYz3Q==} cpu: [arm64] os: [darwin] + '@rollup/rollup-darwin-arm64@4.45.1': + resolution: {integrity: sha512-FSncqHvqTm3lC6Y13xncsdOYfxGSLnP+73k815EfNmpewPs+EyM49haPS105Rh4aF5mJKywk9X0ogzLXZzN9lA==} + cpu: [arm64] + os: [darwin] + '@rollup/rollup-darwin-x64@4.27.4': resolution: {integrity: sha512-o9bH2dbdgBDJaXWJCDTNDYa171ACUdzpxSZt+u/AAeQ20Nk5x+IhA+zsGmrQtpkLiumRJEYef68gcpn2ooXhSQ==} cpu: [x64] os: [darwin] + '@rollup/rollup-darwin-x64@4.45.1': + resolution: {integrity: sha512-2/vVn/husP5XI7Fsf/RlhDaQJ7x9zjvC81anIVbr4b/f0xtSmXQTFcGIQ/B1cXIYM6h2nAhJkdMHTnD7OtQ9Og==} + cpu: [x64] + os: [darwin] + '@rollup/rollup-freebsd-arm64@4.27.4': resolution: {integrity: sha512-NBI2/i2hT9Q+HySSHTBh52da7isru4aAAo6qC3I7QFVsuhxi2gM8t/EI9EVcILiHLj1vfi+VGGPaLOUENn7pmw==} cpu: [arm64] os: [freebsd] + '@rollup/rollup-freebsd-arm64@4.45.1': + resolution: {integrity: sha512-4g1kaDxQItZsrkVTdYQ0bxu4ZIQ32cotoQbmsAnW1jAE4XCMbcBPDirX5fyUzdhVCKgPcrwWuucI8yrVRBw2+g==} + cpu: [arm64] + os: [freebsd] + '@rollup/rollup-freebsd-x64@4.27.4': resolution: {integrity: sha512-wYcC5ycW2zvqtDYrE7deary2P2UFmSh85PUpAx+dwTCO9uw3sgzD6Gv9n5X4vLaQKsrfTSZZ7Z7uynQozPVvWA==} cpu: [x64] os: [freebsd] + '@rollup/rollup-freebsd-x64@4.45.1': + resolution: {integrity: sha512-L/6JsfiL74i3uK1Ti2ZFSNsp5NMiM4/kbbGEcOCps99aZx3g8SJMO1/9Y0n/qKlWZfn6sScf98lEOUe2mBvW9A==} + cpu: [x64] + os: [freebsd] + '@rollup/rollup-linux-arm-gnueabihf@4.27.4': resolution: {integrity: sha512-9OwUnK/xKw6DyRlgx8UizeqRFOfi9mf5TYCw1uolDaJSbUmBxP85DE6T4ouCMoN6pXw8ZoTeZCSEfSaYo+/s1w==} cpu: [arm] os: [linux] + '@rollup/rollup-linux-arm-gnueabihf@4.45.1': + resolution: {integrity: sha512-RkdOTu2jK7brlu+ZwjMIZfdV2sSYHK2qR08FUWcIoqJC2eywHbXr0L8T/pONFwkGukQqERDheaGTeedG+rra6Q==} + cpu: [arm] + os: [linux] + '@rollup/rollup-linux-arm-musleabihf@4.27.4': resolution: {integrity: sha512-Vgdo4fpuphS9V24WOV+KwkCVJ72u7idTgQaBoLRD0UxBAWTF9GWurJO9YD9yh00BzbkhpeXtm6na+MvJU7Z73A==} cpu: [arm] os: [linux] + '@rollup/rollup-linux-arm-musleabihf@4.45.1': + resolution: {integrity: sha512-3kJ8pgfBt6CIIr1o+HQA7OZ9mp/zDk3ctekGl9qn/pRBgrRgfwiffaUmqioUGN9hv0OHv2gxmvdKOkARCtRb8Q==} + cpu: [arm] + os: [linux] + '@rollup/rollup-linux-arm64-gnu@4.27.4': resolution: {integrity: sha512-pleyNgyd1kkBkw2kOqlBx+0atfIIkkExOTiifoODo6qKDSpnc6WzUY5RhHdmTdIJXBdSnh6JknnYTtmQyobrVg==} cpu: [arm64] os: [linux] + '@rollup/rollup-linux-arm64-gnu@4.45.1': + resolution: {integrity: sha512-k3dOKCfIVixWjG7OXTCOmDfJj3vbdhN0QYEqB+OuGArOChek22hn7Uy5A/gTDNAcCy5v2YcXRJ/Qcnm4/ma1xw==} + cpu: [arm64] + os: [linux] + '@rollup/rollup-linux-arm64-musl@4.27.4': resolution: {integrity: sha512-caluiUXvUuVyCHr5DxL8ohaaFFzPGmgmMvwmqAITMpV/Q+tPoaHZ/PWa3t8B2WyoRcIIuu1hkaW5KkeTDNSnMA==} cpu: [arm64] os: [linux] + '@rollup/rollup-linux-arm64-musl@4.45.1': + resolution: {integrity: sha512-PmI1vxQetnM58ZmDFl9/Uk2lpBBby6B6rF4muJc65uZbxCs0EA7hhKCk2PKlmZKuyVSHAyIw3+/SiuMLxKxWog==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loongarch64-gnu@4.45.1': + resolution: {integrity: sha512-9UmI0VzGmNJ28ibHW2GpE2nF0PBQqsyiS4kcJ5vK+wuwGnV5RlqdczVocDSUfGX/Na7/XINRVoUgJyFIgipoRg==} + cpu: [loong64] + os: [linux] + '@rollup/rollup-linux-powerpc64le-gnu@4.27.4': resolution: {integrity: sha512-FScrpHrO60hARyHh7s1zHE97u0KlT/RECzCKAdmI+LEoC1eDh/RDji9JgFqyO+wPDb86Oa/sXkily1+oi4FzJQ==} cpu: [ppc64] os: [linux] + '@rollup/rollup-linux-powerpc64le-gnu@4.45.1': + resolution: {integrity: sha512-7nR2KY8oEOUTD3pBAxIBBbZr0U7U+R9HDTPNy+5nVVHDXI4ikYniH1oxQz9VoB5PbBU1CZuDGHkLJkd3zLMWsg==} + cpu: [ppc64] + os: [linux] + '@rollup/rollup-linux-riscv64-gnu@4.27.4': resolution: {integrity: sha512-qyyprhyGb7+RBfMPeww9FlHwKkCXdKHeGgSqmIXw9VSUtvyFZ6WZRtnxgbuz76FK7LyoN8t/eINRbPUcvXB5fw==} cpu: [riscv64] os: [linux] + '@rollup/rollup-linux-riscv64-gnu@4.45.1': + resolution: {integrity: sha512-nlcl3jgUultKROfZijKjRQLUu9Ma0PeNv/VFHkZiKbXTBQXhpytS8CIj5/NfBeECZtY2FJQubm6ltIxm/ftxpw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.45.1': + resolution: {integrity: sha512-HJV65KLS51rW0VY6rvZkiieiBnurSzpzore1bMKAhunQiECPuxsROvyeaot/tcK3A3aGnI+qTHqisrpSgQrpgA==} + cpu: [riscv64] + os: [linux] + '@rollup/rollup-linux-s390x-gnu@4.27.4': resolution: {integrity: sha512-PFz+y2kb6tbh7m3A7nA9++eInGcDVZUACulf/KzDtovvdTizHpZaJty7Gp0lFwSQcrnebHOqxF1MaKZd7psVRg==} cpu: [s390x] os: [linux] + '@rollup/rollup-linux-s390x-gnu@4.45.1': + resolution: {integrity: sha512-NITBOCv3Qqc6hhwFt7jLV78VEO/il4YcBzoMGGNxznLgRQf43VQDae0aAzKiBeEPIxnDrACiMgbqjuihx08OOw==} + cpu: [s390x] + os: [linux] + '@rollup/rollup-linux-x64-gnu@4.27.4': resolution: {integrity: sha512-Ni8mMtfo+o/G7DVtweXXV/Ol2TFf63KYjTtoZ5f078AUgJTmaIJnj4JFU7TK/9SVWTaSJGxPi5zMDgK4w+Ez7Q==} cpu: [x64] os: [linux] + '@rollup/rollup-linux-x64-gnu@4.45.1': + resolution: {integrity: sha512-+E/lYl6qu1zqgPEnTrs4WysQtvc/Sh4fC2nByfFExqgYrqkKWp1tWIbe+ELhixnenSpBbLXNi6vbEEJ8M7fiHw==} + cpu: [x64] + os: [linux] + '@rollup/rollup-linux-x64-musl@4.27.4': resolution: {integrity: sha512-5AeeAF1PB9TUzD+3cROzFTnAJAcVUGLuR8ng0E0WXGkYhp6RD6L+6szYVX+64Rs0r72019KHZS1ka1q+zU/wUw==} cpu: [x64] os: [linux] + '@rollup/rollup-linux-x64-musl@4.45.1': + resolution: {integrity: sha512-a6WIAp89p3kpNoYStITT9RbTbTnqarU7D8N8F2CV+4Cl9fwCOZraLVuVFvlpsW0SbIiYtEnhCZBPLoNdRkjQFw==} + cpu: [x64] + os: [linux] + '@rollup/rollup-win32-arm64-msvc@4.27.4': resolution: {integrity: sha512-yOpVsA4K5qVwu2CaS3hHxluWIK5HQTjNV4tWjQXluMiiiu4pJj4BN98CvxohNCpcjMeTXk/ZMJBRbgRg8HBB6A==} cpu: [arm64] os: [win32] + '@rollup/rollup-win32-arm64-msvc@4.45.1': + resolution: {integrity: sha512-T5Bi/NS3fQiJeYdGvRpTAP5P02kqSOpqiopwhj0uaXB6nzs5JVi2XMJb18JUSKhCOX8+UE1UKQufyD6Or48dJg==} + cpu: [arm64] + os: [win32] + '@rollup/rollup-win32-ia32-msvc@4.27.4': resolution: {integrity: sha512-KtwEJOaHAVJlxV92rNYiG9JQwQAdhBlrjNRp7P9L8Cb4Rer3in+0A+IPhJC9y68WAi9H0sX4AiG2NTsVlmqJeQ==} cpu: [ia32] os: [win32] + '@rollup/rollup-win32-ia32-msvc@4.45.1': + resolution: {integrity: sha512-lxV2Pako3ujjuUe9jiU3/s7KSrDfH6IgTSQOnDWr9aJ92YsFd7EurmClK0ly/t8dzMkDtd04g60WX6yl0sGfdw==} + cpu: [ia32] + os: [win32] + '@rollup/rollup-win32-x64-msvc@4.27.4': resolution: {integrity: sha512-3j4jx1TppORdTAoBJRd+/wJRGCPC0ETWkXOecJ6PPZLj6SptXkrXcNqdj0oclbKML6FkQltdz7bBA3rUSirZug==} cpu: [x64] os: [win32] - '@rushstack/node-core-library@5.10.0': - resolution: {integrity: sha512-2pPLCuS/3x7DCd7liZkqOewGM0OzLyCacdvOe8j6Yrx9LkETGnxul1t7603bIaB8nUAooORcct9fFDOQMbWAgw==} + '@rollup/rollup-win32-x64-msvc@4.45.1': + resolution: {integrity: sha512-M/fKi4sasCdM8i0aWJjCSFm2qEnYRR8AMLG2kxp6wD13+tMGA4Z1tVAuHkNRjud5SW2EM3naLuK35w9twvf6aA==} + cpu: [x64] + os: [win32] + + '@rushstack/node-core-library@5.13.1': + resolution: {integrity: sha512-5yXhzPFGEkVc9Fu92wsNJ9jlvdwz4RNb2bMso+/+TH0nMm1jDDDsOIf4l8GAkPxGuwPw5DH24RliWVfSPhlW/Q==} peerDependencies: '@types/node': '*' peerDependenciesMeta: @@ -711,37 +1105,55 @@ packages: '@rushstack/rig-package@0.5.3': resolution: {integrity: sha512-olzSSjYrvCNxUFZowevC3uz8gvKr3WTpHQ7BkpjtRpA3wK+T0ybep/SRUMfr195gBzJm5gaXw0ZMgjIyHqJUow==} - '@rushstack/terminal@0.14.3': - resolution: {integrity: sha512-csXbZsAdab/v8DbU1sz7WC2aNaKArcdS/FPmXMOXEj/JBBZMvDK0+1b4Qao0kkG0ciB1Qe86/Mb68GjH6/TnMw==} + '@rushstack/terminal@0.15.3': + resolution: {integrity: sha512-DGJ0B2Vm69468kZCJkPj3AH5nN+nR9SPmC0rFHtzsS4lBQ7/dgOwtwVxYP7W9JPDMuRBkJ4KHmWKr036eJsj9g==} peerDependencies: '@types/node': '*' peerDependenciesMeta: '@types/node': optional: true - '@rushstack/ts-command-line@4.23.1': - resolution: {integrity: sha512-40jTmYoiu/xlIpkkRsVfENtBq4CW3R4azbL0Vmda+fMwHWqss6wwf/Cy/UJmMqIzpfYc2OTnjYP1ZLD3CmyeCA==} + '@rushstack/ts-command-line@5.0.1': + resolution: {integrity: sha512-bsbUucn41UXrQK7wgM8CNM/jagBytEyJqXw/umtI8d68vFm1Jwxh1OtLrlW7uGZgjCWiiPH6ooUNa1aVsuVr3Q==} '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} - '@shikijs/core@1.23.1': - resolution: {integrity: sha512-NuOVgwcHgVC6jBVH5V7iblziw6iQbWWHrj5IlZI3Fqu2yx9awH7OIQkXIcsHsUmY19ckwSgUMgrqExEyP5A0TA==} + '@shikijs/core@2.5.0': + resolution: {integrity: sha512-uu/8RExTKtavlpH7XqnVYBrfBkUc20ngXiX9NSrBhOVZYv/7XQRKUyhtkeflY5QsxC0GbJThCerruZfsUaSldg==} + + '@shikijs/engine-javascript@2.5.0': + resolution: {integrity: sha512-VjnOpnQf8WuCEZtNUdjjwGUbtAVKuZkVQ/5cHy/tojVVRIRtlWMYVjyWhxOmIq05AlSOv72z7hRNRGVBgQOl0w==} + + '@shikijs/engine-oniguruma@2.5.0': + resolution: {integrity: sha512-pGd1wRATzbo/uatrCIILlAdFVKdxImWJGQ5rFiB5VZi2ve5xj3Ax9jny8QvkaV93btQEwR/rSz5ERFpC5mKNIw==} + + '@shikijs/engine-oniguruma@3.8.0': + resolution: {integrity: sha512-Tx7kR0oFzqa+rY7t80LjN8ZVtHO3a4+33EUnBVx2qYP3fGxoI9H0bvnln5ySelz9SIUTsS0/Qn+9dg5zcUMsUw==} + + '@shikijs/langs@2.5.0': + resolution: {integrity: sha512-Qfrrt5OsNH5R+5tJ/3uYBBZv3SuGmnRPejV9IlIbFH3HTGLDlkqgHymAlzklVmKBjAaVmkPkyikAV/sQ1wSL+w==} - '@shikijs/engine-javascript@1.23.1': - resolution: {integrity: sha512-i/LdEwT5k3FVu07SiApRFwRcSJs5QM9+tod5vYCPig1Ywi8GR30zcujbxGQFJHwYD7A5BUqagi8o5KS+LEVgBg==} + '@shikijs/langs@3.8.0': + resolution: {integrity: sha512-mfGYuUgjQ5GgXinB5spjGlBVhG2crKRpKkfADlp8r9k/XvZhtNXxyOToSnCEnF0QNiZnJjlt5MmU9PmhRdwAbg==} - '@shikijs/engine-oniguruma@1.23.1': - resolution: {integrity: sha512-KQ+lgeJJ5m2ISbUZudLR1qHeH3MnSs2mjFg7bnencgs5jDVPeJ2NVDJ3N5ZHbcTsOIh0qIueyAJnwg7lg7kwXQ==} + '@shikijs/themes@2.5.0': + resolution: {integrity: sha512-wGrk+R8tJnO0VMzmUExHR+QdSaPUl/NKs+a4cQQRWyoc3YFbUzuLEi/KWK1hj+8BfHRKm2jNhhJck1dfstJpiw==} - '@shikijs/transformers@1.23.1': - resolution: {integrity: sha512-yQ2Cn0M9i46p30KwbyIzLvKDk+dQNU+lj88RGO0XEj54Hn4Cof1bZoDb9xBRWxFE4R8nmK63w7oHnJwvOtt0NQ==} + '@shikijs/themes@3.8.0': + resolution: {integrity: sha512-yaZiLuyO23sXe16JFU76KyUMTZCJi4EMQKIrdQt7okoTzI4yAaJhVXT2Uy4k8yBIEFRiia5dtD7gC1t8m6y3oQ==} - '@shikijs/types@1.23.1': - resolution: {integrity: sha512-98A5hGyEhzzAgQh2dAeHKrWW4HfCMeoFER2z16p5eJ+vmPeF6lZ/elEne6/UCU551F/WqkopqRsr1l2Yu6+A0g==} + '@shikijs/transformers@2.5.0': + resolution: {integrity: sha512-SI494W5X60CaUwgi8u4q4m4s3YAFSxln3tzNjOSYqq54wlVgz0/NbbXEb3mdLbqMBztcmS7bVTaEd2w0qMmfeg==} - '@shikijs/vscode-textmate@9.3.0': - resolution: {integrity: sha512-jn7/7ky30idSkd/O5yDBfAnVt+JJpepofP/POZ1iMOxK59cOfqIgg/Dj0eFsjOTMw+4ycJN0uhZH/Eb0bs/EUA==} + '@shikijs/types@2.5.0': + resolution: {integrity: sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw==} + + '@shikijs/types@3.8.0': + resolution: {integrity: sha512-I/b/aNg0rP+kznVDo7s3UK8jMcqEGTtoPDdQ+JlQ2bcJIyu/e2iRvl42GLIDMK03/W1YOHOuhlhQ7aM+XbKUeg==} + + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} '@sindresorhus/merge-streams@2.3.0': resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} @@ -761,6 +1173,9 @@ packages: '@tootallnate/quickjs-emscripten@0.23.0': resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} + '@tybys/wasm-util@0.10.0': + resolution: {integrity: sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==} + '@types/argparse@1.0.38': resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} @@ -773,6 +1188,9 @@ packages: '@types/estree@1.0.7': resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} @@ -797,14 +1215,11 @@ packages: '@types/nightwatch@2.3.32': resolution: {integrity: sha512-RXAWpe83AERF0MbRHXaEJlMQGDtA6BW5sgbn2jO0z04yzbxc4gUvzaJwHpGULBSa2QKUHfBZoLwe/tuQx0PWLg==} - '@types/node@20.17.31': - resolution: {integrity: sha512-quODOCNXQAbNf1Q7V+fI8WyErOCh0D5Yd31vHnKu4GkSztGQ7rlltAaqXhHhLl33tlVyUXs2386MkANSwgDn6A==} + '@types/node@20.19.8': + resolution: {integrity: sha512-HzbgCY53T6bfu4tT7Aq3TvViJyHjLjPNaAS3HOuMc9pw97KHsUtXNX4L+wu59g1WnjsZSko35MbEqnO58rihhw==} - '@types/node@20.17.7': - resolution: {integrity: sha512-sZXXnpBFMKbao30dUAvzKbdwA2JM1fwUtVEq/kxKuPI5mMwZiRElCpTXb0Biq/LMEVpXDZL5G5V0RPnxKeyaYg==} - - '@types/node@22.15.2': - resolution: {integrity: sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A==} + '@types/node@24.0.14': + resolution: {integrity: sha512-4zXMWD91vBLGRtHK3YbIoFMia+1nqEz72coM42C5ETjnNCa/heoj7NT1G67iAfOqMmcfhuCZ4uNpyz8EjlAejw==} '@types/normalize-package-data@2.4.1': resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==} @@ -821,8 +1236,11 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} - '@types/web-bluetooth@0.0.20': - resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==} + '@types/web-bluetooth@0.0.21': + resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} + + '@types/whatwg-mimetype@3.0.2': + resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==} '@types/ws@8.5.10': resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==} @@ -830,21 +1248,61 @@ packages: '@types/yauzl@2.10.3': resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20250718.1': + resolution: {integrity: sha512-sdlVVkQcIY9r3wfYX8MBIQ/ioA4C09IlBrlI6ZAjoG/bcw9xq1QeOSFyP0zmY8OIUGW/ZJ6tXgnQ2lDfq2BBYA==} + engines: {node: '>=20.6.0'} + cpu: [arm64] + os: [darwin] + + '@typescript/native-preview-darwin-x64@7.0.0-dev.20250718.1': + resolution: {integrity: sha512-VUH+f7ZilFyDSMdcJPWCD7aHn01z1zYL2b+lXhIERSxT/46QrSO+otHTxe9IvRkj/W2tbPOokbjHm+clZkWA8w==} + engines: {node: '>=20.6.0'} + cpu: [x64] + os: [darwin] + + '@typescript/native-preview-linux-arm64@7.0.0-dev.20250718.1': + resolution: {integrity: sha512-rz8RQOmCps0F68IBlEDC9fNhQfI7HPl5YGRKUlJTJj+u/2or84iU3+J0wtH8Yc14wqXDOF7g1DUh8hfdxmtlrw==} + engines: {node: '>=20.6.0'} + cpu: [arm64] + os: [linux] + + '@typescript/native-preview-linux-arm@7.0.0-dev.20250718.1': + resolution: {integrity: sha512-i7sgp1jdDJnlDRY0w8MiuILtpuHpRfBAso3XuvfQVW637gDNjp/3o1/8vw6mKgSXmkeicjki0fWbRBK6FNcaSg==} + engines: {node: '>=20.6.0'} + cpu: [arm] + os: [linux] + + '@typescript/native-preview-linux-x64@7.0.0-dev.20250718.1': + resolution: {integrity: sha512-YTKSZVeB9mAEd4xb1j3TXEUDYDZNMWXoH/8FnKAmBJ40RrZMOxXqzIJUzoYnuxx3p/HCVONPcWdQkLvWQ+hOVQ==} + engines: {node: '>=20.6.0'} + cpu: [x64] + os: [linux] + + '@typescript/native-preview-win32-arm64@7.0.0-dev.20250718.1': + resolution: {integrity: sha512-C9rdeH42doHfXoYpi/zTiWmsijGUYF36476ohGdSKa0E4QhaVrbUPttd8sY/vkqiKiqVXmBA/j03uzdnXznGZg==} + engines: {node: '>=20.6.0'} + cpu: [arm64] + os: [win32] + + '@typescript/native-preview-win32-x64@7.0.0-dev.20250718.1': + resolution: {integrity: sha512-ZVFYhLB+0p37yuLSIVuO2Rng1Y43wjNIRX3yoPpKwoasm+d8XEVA3nNHBHrFFm7VKJV/SqjWvo3OPyTj5VbW6Q==} + engines: {node: '>=20.6.0'} + cpu: [x64] + os: [win32] + + '@typescript/native-preview@7.0.0-dev.20250718.1': + resolution: {integrity: sha512-T74SOKwgUvf77HmNffmceqobVdhhV638u6uiPtm1BPFHbHeCVoK3nA1wXZ1XKcFYt603fCA5QfzmGhrEnkdzRA==} + engines: {node: '>=20.6.0'} + hasBin: true + '@ungap/promise-all-settled@1.1.2': resolution: {integrity: sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==} - '@ungap/structured-clone@1.2.0': - resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} - '@vitejs/plugin-vue@5.2.0': - resolution: {integrity: sha512-7n7KdUEtx/7Yl7I/WVAMZ1bEb0eVvXF3ummWTeLcs/9gvo9pJhuLdouSXGjdZ/MKD1acf1I272+X0RMua4/R3g==} - engines: {node: ^18.0.0 || >=20.0.0} - peerDependencies: - vite: ^5.0.0 - vue: ^3.2.25 - - '@vitejs/plugin-vue@5.2.3': - resolution: {integrity: sha512-IYSLEQj4LgZZuoVpdSUCw3dIynTWQgPlaRP6iAvMle4My0HdYwr5g5wQAfwOeHQBmYwEkqF70nRpSilr6PoUDg==} + '@vitejs/plugin-vue@5.2.4': + resolution: {integrity: sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==} engines: {node: ^18.0.0 || >=20.0.0} peerDependencies: vite: ^5.0.0 || ^6.0.0 @@ -893,26 +1351,26 @@ packages: '@vitest/utils@2.1.9': resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} - '@volar/language-core@2.4.12': - resolution: {integrity: sha512-RLrFdXEaQBWfSnYGVxvR2WrO6Bub0unkdHYIdC31HzIEqATIuuhRRzYu76iGPZ6OtA4Au1SnW0ZwIqPP217YhA==} + '@volar/language-core@2.4.15': + resolution: {integrity: sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==} - '@volar/source-map@2.4.12': - resolution: {integrity: sha512-bUFIKvn2U0AWojOaqf63ER0N/iHIBYZPpNGogfLPQ68F5Eet6FnLlyho7BS0y2HJ1jFhSif7AcuTx1TqsCzRzw==} + '@volar/source-map@2.4.15': + resolution: {integrity: sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==} - '@volar/typescript@2.4.12': - resolution: {integrity: sha512-HJB73OTJDgPc80K30wxi3if4fSsZZAOScbj2fcicMuOPoOkcf9NNAINb33o+DzhBdF9xTKC1gnPmIRDous5S0g==} + '@volar/typescript@2.4.15': + resolution: {integrity: sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==} - '@vue/compiler-core@3.5.13': - resolution: {integrity: sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==} + '@vue/compiler-core@3.5.17': + resolution: {integrity: sha512-Xe+AittLbAyV0pabcN7cP7/BenRBNcteM4aSDCtRvGw0d9OL+HG1u/XHLY/kt1q4fyMeZYXyIYrsHuPSiDPosA==} - '@vue/compiler-dom@3.5.13': - resolution: {integrity: sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==} + '@vue/compiler-dom@3.5.17': + resolution: {integrity: sha512-+2UgfLKoaNLhgfhV5Ihnk6wB4ljyW1/7wUIog2puUqajiC29Lp5R/IKDdkebh9jTbTogTbsgB+OY9cEWzG95JQ==} - '@vue/compiler-sfc@3.5.13': - resolution: {integrity: sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==} + '@vue/compiler-sfc@3.5.17': + resolution: {integrity: sha512-rQQxbRJMgTqwRugtjw0cnyQv9cP4/4BxWfTdRBkqsTfLOHWykLzbOc3C4GGzAmdMDxhzU/1Ija5bTjMVrddqww==} - '@vue/compiler-ssr@3.5.13': - resolution: {integrity: sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==} + '@vue/compiler-ssr@3.5.17': + resolution: {integrity: sha512-hkDbA0Q20ZzGgpj5uZjb9rBzQtIHLS78mMilwrlpWk2Ep37DYntUz0PonQ6kr113vfOEdM+zTBuJDaceNIW0tQ==} '@vue/compiler-vue2@2.7.16': resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==} @@ -920,59 +1378,59 @@ packages: '@vue/devtools-api@6.6.4': resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} - '@vue/devtools-api@7.6.4': - resolution: {integrity: sha512-5AaJ5ELBIuevmFMZYYLuOO9HUuY/6OlkOELHE7oeDhy4XD/hSODIzktlsvBOsn+bto3aD0psj36LGzwVu5Ip8w==} + '@vue/devtools-api@7.7.7': + resolution: {integrity: sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg==} - '@vue/devtools-kit@7.6.4': - resolution: {integrity: sha512-Zs86qIXXM9icU0PiGY09PQCle4TI750IPLmAJzW5Kf9n9t5HzSYf6Rz6fyzSwmfMPiR51SUKJh9sXVZu78h2QA==} + '@vue/devtools-kit@7.7.7': + resolution: {integrity: sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA==} - '@vue/devtools-shared@7.6.4': - resolution: {integrity: sha512-nD6CUvBEel+y7zpyorjiUocy0nh77DThZJ0k1GRnJeOmY3ATq2fWijEp7wk37gb023Cb0R396uYh5qMSBQ5WFg==} + '@vue/devtools-shared@7.7.7': + resolution: {integrity: sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==} - '@vue/language-core@2.2.10': - resolution: {integrity: sha512-+yNoYx6XIKuAO8Mqh1vGytu8jkFEOH5C8iOv3i8Z/65A7x9iAOXA97Q+PqZ3nlm2lxf5rOJuIGI/wDtx/riNYw==} + '@vue/language-core@2.2.12': + resolution: {integrity: sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==} peerDependencies: typescript: '*' peerDependenciesMeta: typescript: optional: true - '@vue/reactivity@3.5.13': - resolution: {integrity: sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==} + '@vue/reactivity@3.5.17': + resolution: {integrity: sha512-l/rmw2STIscWi7SNJp708FK4Kofs97zc/5aEPQh4bOsReD/8ICuBcEmS7KGwDj5ODQLYWVN2lNibKJL1z5b+Lw==} - '@vue/runtime-core@3.5.13': - resolution: {integrity: sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==} + '@vue/runtime-core@3.5.17': + resolution: {integrity: sha512-QQLXa20dHg1R0ri4bjKeGFKEkJA7MMBxrKo2G+gJikmumRS7PTD4BOU9FKrDQWMKowz7frJJGqBffYMgQYS96Q==} - '@vue/runtime-dom@3.5.13': - resolution: {integrity: sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==} + '@vue/runtime-dom@3.5.17': + resolution: {integrity: sha512-8El0M60TcwZ1QMz4/os2MdlQECgGoVHPuLnQBU3m9h3gdNRW9xRmI8iLS4t/22OQlOE6aJvNNlBiCzPHur4H9g==} - '@vue/server-renderer@3.5.13': - resolution: {integrity: sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==} + '@vue/server-renderer@3.5.17': + resolution: {integrity: sha512-BOHhm8HalujY6lmC3DbqF6uXN/K00uWiEeF22LfEsm9Q93XeJ/plHTepGwf6tqFcF7GA5oGSSAAUock3VvzaCA==} peerDependencies: - vue: 3.5.13 + vue: 3.5.17 - '@vue/shared@3.5.13': - resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==} + '@vue/shared@3.5.17': + resolution: {integrity: sha512-CabR+UN630VnsJO/jHWYBC1YVXyMq94KKp6iF5MQgZJs5I8cmjw6oVMO1oDbtBkENSHSSn/UadWlW/OAgdmKrg==} '@vue/test-utils@2.4.6': resolution: {integrity: sha512-FMxEjOpYNYiFe0GkaHsnJPXFHxQ6m4t8vI/ElPGpMWxZKpmRvQ33OIrvRXemy6yha03RxhOlQuy+gZMC3CQSow==} - '@vue/tsconfig@0.6.0': - resolution: {integrity: sha512-MHXNd6lzugsEHvuA6l1GqrF5jROqUon8sP/HInLPnthJiYvB0VvpHMywg7em1dBZfFZNBSkR68qH37zOdRHmCw==} + '@vue/tsconfig@0.7.0': + resolution: {integrity: sha512-ku2uNz5MaZ9IerPPUyOHzyjhXoX2kVJaVf7hL315DC17vS6IiZRmmCPfggNbU16QTvM80+uYYy3eYJB59WCtvg==} peerDependencies: typescript: 5.x - vue: ^3.3.0 + vue: ^3.4.0 peerDependenciesMeta: typescript: optional: true vue: optional: true - '@vueuse/core@11.3.0': - resolution: {integrity: sha512-7OC4Rl1f9G8IT6rUfi9JrKiXy4bfmHhZ5x2Ceojy0jnd3mHNEvV4JaRygH362ror6/NZ+Nl+n13LPzGiPN8cKA==} + '@vueuse/core@12.8.2': + resolution: {integrity: sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==} - '@vueuse/integrations@11.3.0': - resolution: {integrity: sha512-5fzRl0apQWrDezmobchoiGTkGw238VWESxZHazfhP3RM7pDSiyXy18QbfYkILoYNTd23HPAfQTJpkUc5QbkwTw==} + '@vueuse/integrations@12.8.2': + resolution: {integrity: sha512-fbGYivgK5uBTRt7p5F3zy6VrETlV9RtZjBqd1/HxGdjdckBgBM4ugP8LHpjolqTj14TXTxSK1ZfgPbHYyGuH7g==} peerDependencies: async-validator: ^4 axios: ^1 @@ -1012,18 +1470,18 @@ packages: universal-cookie: optional: true - '@vueuse/metadata@11.3.0': - resolution: {integrity: sha512-pwDnDspTqtTo2HwfLw4Rp6yywuuBdYnPYDq+mO38ZYKGebCUQC/nVj/PXSiK9HX5otxLz8Fn7ECPbjiRz2CC3g==} + '@vueuse/metadata@12.8.2': + resolution: {integrity: sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==} - '@vueuse/shared@11.3.0': - resolution: {integrity: sha512-P8gSSWQeucH5821ek2mn/ciCk+MS/zoRKqdQIM3bHq6p7GXDAJLmnRRKmF5F65sAVJIfzQlwR3aDzwCn10s8hA==} + '@vueuse/shared@12.8.2': + resolution: {integrity: sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==} - '@wdio/logger@9.0.8': - resolution: {integrity: sha512-uIyYIDBwLczmsp9JE5hN3ME8Xg+9WNBfSNXD69ICHrY9WPTzFf94UeTuavK7kwSKF3ro2eJbmNZItYOfnoovnw==} + '@wdio/logger@9.18.0': + resolution: {integrity: sha512-HdzDrRs+ywAqbXGKqe1i/bLtCv47plz4TvsHFH3j729OooT5VH38ctFn5aLXgECmiAKDkmH/A6kOq2Zh5DIxww==} engines: {node: '>=18.20.0'} - '@zip.js/zip.js@2.7.52': - resolution: {integrity: sha512-+5g7FQswvrCHwYKNMd/KFxZSObctLSsQOgqBSi0LzwHo3li9Eh1w5cF5ndjQw9Zbr3ajVnd2+XyiX85gAetx1Q==} + '@zip.js/zip.js@2.7.64': + resolution: {integrity: sha512-x6BDiDTKeZWbeHgHlVzV1sqReGuz4b1R4cVStUDua8OSq4bRreZeiJKPFFlvJs67FvCnetq33aYb/XH73EQhdA==} engines: {bun: '>=0.7.0', deno: '>=1.0.0', node: '>=16.5.0'} JSONStream@1.3.5: @@ -1050,8 +1508,8 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - acorn@8.14.0: - resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} engines: {node: '>=0.4.0'} hasBin: true @@ -1062,8 +1520,8 @@ packages: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} - agent-base@7.1.1: - resolution: {integrity: sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==} + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} ajv-draft-04@1.0.0: @@ -1088,8 +1546,8 @@ packages: ajv@8.13.0: resolution: {integrity: sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==} - algoliasearch@5.15.0: - resolution: {integrity: sha512-Yf3Swz1s63hjvBVZ/9f2P1Uu48GjmjCN+Esxb6MAONMGtZB1fRX8/S1AhUTtsuTlcGovbYLxpHgc7wEzstDZBw==} + algoliasearch@5.34.0: + resolution: {integrity: sha512-wioVnf/8uuG8Bmywhk5qKIQ3wzCCtmdvicPRb0fa3kKYGGoewfgDqLEaET1MV2NbTc3WGpPv+AgauLVBp1nB9A==} engines: {node: '>= 14.0.0'} alien-signals@1.0.13: @@ -1135,6 +1593,13 @@ packages: engines: {node: '>=8.0.0'} hasBin: true + ansis@4.1.0: + resolution: {integrity: sha512-BGcItUBWSMRgOCe+SVZJ+S7yTRG0eGt9cXAHev72yuGcY23hnLA7Bky5L/xLyPINoSN95geovfBkqoTlNZYa7w==} + engines: {node: '>=14'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + anymatch@3.1.3: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} @@ -1159,6 +1624,10 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + ast-kit@2.1.1: + resolution: {integrity: sha512-mfh6a7gKXE8pDlxTvqIc/syH/P3RkzbOF6LeHdcKztLEzYe6IMsRCL7N8vI7hqTGWNxpkCuuRTpT21xNWqhRtQ==} + engines: {node: '>=20.18.0'} + ast-types@0.13.4: resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} engines: {node: '>=4'} @@ -1173,29 +1642,44 @@ packages: resolution: {integrity: sha512-zIURGIS1E1Q4pcrMjp+nnEh+16G56eG/MUllJH8yEvw7asDo7Ac9uhC9KIH5jzpITueEZolfYglnCGIuSBz39g==} engines: {node: '>=4'} - axios@1.7.7: - resolution: {integrity: sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==} + axios@1.10.0: + resolution: {integrity: sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==} - b4a@1.6.6: - resolution: {integrity: sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==} + b4a@1.6.7: + resolution: {integrity: sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==} balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - bare-events@2.4.2: - resolution: {integrity: sha512-qMKFd2qG/36aA4GwvKq8MxnPgCQAmBWmSyLWsJcbn8v03wvIPQ/hG1Ms8bPzndZxMDoHpxez5VOS+gC9Yi24/Q==} + bare-events@2.6.0: + resolution: {integrity: sha512-EKZ5BTXYExaNqi3I3f9RtEsaI/xBSGjE0XZCZilPzFAV/goswFHuPd9jEZlPIZ/iNZJwDSao9qRiScySz7MbQg==} - bare-fs@2.3.3: - resolution: {integrity: sha512-7RYKL+vZVCyAsMLi5SPu7QGauGGT8avnP/HO571ndEuV4MYdGXvLhtW67FuLPeEI8EiIY7zbbRR9x7x7HU0kgw==} + bare-fs@4.1.6: + resolution: {integrity: sha512-25RsLF33BqooOEFNdMcEhMpJy8EoR88zSMrnOQOaM3USnOK2VmaJ1uaQEwPA6AQjrv1lXChScosN6CzbwbO9OQ==} + engines: {bare: '>=1.16.0'} + peerDependencies: + bare-buffer: '*' + peerDependenciesMeta: + bare-buffer: + optional: true - bare-os@2.4.2: - resolution: {integrity: sha512-HZoJwzC+rZ9lqEemTMiO0luOePoGYNBgsLLgegKR/cljiJvcDNhDZQkzC+NC5Oh0aHbdBNSOHpghwMuB5tqhjg==} + bare-os@3.6.1: + resolution: {integrity: sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g==} + engines: {bare: '>=1.14.0'} - bare-path@2.1.3: - resolution: {integrity: sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA==} + bare-path@3.0.0: + resolution: {integrity: sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==} - bare-stream@2.2.1: - resolution: {integrity: sha512-YTB47kHwBW9zSG8LD77MIBAAQXjU2WjAkMHeeb7hUplVs6+IoM5I7uEVQNPMB7lj9r8I76UMdoMkGnCodHOLqg==} + bare-stream@2.6.5: + resolution: {integrity: sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==} + peerDependencies: + bare-buffer: '*' + bare-events: '*' + peerDependenciesMeta: + bare-buffer: + optional: true + bare-events: + optional: true base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -1208,8 +1692,8 @@ packages: resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} engines: {node: '>=8'} - birpc@0.2.19: - resolution: {integrity: sha512-5WeXXAvTmitV1RqJFppT5QtUiz2p1mRSYU000Jkft5ZUCLJIk4uQriYNO50HknxKwM6jd8utNc66K1qGIwwWBQ==} + birpc@2.5.0: + resolution: {integrity: sha512-VSWO/W6nNQdyP520F1mhf+Lc2f8pjGQOtoHHm7Ze8Go1kX7akpVIrtTa0fn+HB0QJEDVacl6aO08YE0PgXfdnQ==} bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} @@ -1249,10 +1733,20 @@ packages: buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + bundle-require@5.1.0: + resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.18' + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + camelcase-keys@6.2.2: resolution: {integrity: sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==} engines: {node: '>=8'} @@ -1305,9 +1799,13 @@ packages: resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} engines: {node: '>= 8.10.0'} - chromedriver@131.0.5: - resolution: {integrity: sha512-OQY4BHUe9JedxH4aAsPZJcf8Y0lMlE7y+3tiCvQSCQ6qDz2b99R0qsqyqzUUSW2DFx0bg4YxmK8CDVMKb9u5kg==} - engines: {node: '>=18'} + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + chromedriver@138.0.3: + resolution: {integrity: sha512-RKcfzbUthmQzFmy91F9StQQwNZ72khp3febF/RntpkDKhhCkwor0cgop00diwzAVSUq1s2e8B54Iema9FQnynw==} + engines: {node: '>=20'} hasBin: true ci-info@3.3.0: @@ -1378,6 +1876,10 @@ packages: commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + commondir@1.0.1: resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} @@ -1390,6 +1892,9 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + config-chain@1.1.13: resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} @@ -1397,6 +1902,10 @@ packages: resolution: {integrity: sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==} engines: {node: '>=0.8'} + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + conventional-changelog-angular@5.0.13: resolution: {integrity: sha512-i/gipMxs7s8L/QeuavPF2hLnJgH6pEZAttySB6aiQLWcX3puWDL3ACVmvBhJGxnAy52Qc15ua26BufY6KpmrVA==} engines: {node: '>=10'} @@ -1471,8 +1980,8 @@ packages: core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - cross-spawn@7.0.3: - resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} cssom@0.3.8: @@ -1485,6 +1994,10 @@ packages: resolution: {integrity: sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==} engines: {node: '>=8'} + cssstyle@4.6.0: + resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} + engines: {node: '>=18'} + csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} @@ -1504,6 +2017,10 @@ packages: resolution: {integrity: sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==} engines: {node: '>=12'} + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + dateformat@3.0.3: resolution: {integrity: sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==} @@ -1528,17 +2045,8 @@ packages: supports-color: optional: true - debug@4.3.7: - resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - debug@4.4.0: - resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} engines: {node: '>=6.0'} peerDependencies: supports-color: '*' @@ -1562,8 +2070,8 @@ packages: resolution: {integrity: sha512-Fv96DCsdOgB6mdGl67MT5JaTNKRzrzill5OH5s8bjYJXVlcXyPYGyPsUkWyGV5p1TXI5esYIYMMeDJL0hEIwaA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - decimal.js@10.4.3: - resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} deep-eql@4.0.1: resolution: {integrity: sha512-D/Oxqobjr+kxaHsgiQBZq9b6iAWdEj5W/JdJm8deNduAPc9CwXQ3BJJCuEqlrPXcy45iOMkGPZ0T81Dnz7UDCA==} @@ -1587,6 +2095,9 @@ packages: resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} engines: {node: '>=8'} + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + degenerator@5.0.1: resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==} engines: {node: '>= 14'} @@ -1612,6 +2123,10 @@ packages: resolution: {integrity: sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==} engines: {node: '>=0.3.1'} + diff@8.0.2: + resolution: {integrity: sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==} + engines: {node: '>=0.3.1'} + domexception@4.0.0: resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==} engines: {node: '>=12'} @@ -1629,6 +2144,19 @@ packages: resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==} engines: {node: '>=12'} + dts-resolver@2.1.1: + resolution: {integrity: sha512-3BiGFhB6mj5Kv+W2vdJseQUYW+SKVzAFJL6YNP6ursbrwy1fXHRotfHi3xLNxe4wZl/K8qbAFeCDjZLjzqxxRw==} + engines: {node: '>=20.18.0'} + peerDependencies: + oxc-resolver: '>=11.0.0' + peerDependenciesMeta: + oxc-resolver: + optional: true + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} @@ -1657,8 +2185,12 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - end-of-stream@1.4.4: - resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + empathic@2.0.0: + resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} + engines: {node: '>=14'} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} enquirer@2.4.1: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} @@ -1671,6 +2203,10 @@ packages: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + envinfo@7.8.1: resolution: {integrity: sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==} engines: {node: '>=4'} @@ -1683,14 +2219,35 @@ packages: error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + esbuild@0.21.5: resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} engines: {node: '>=12'} hasBin: true + esbuild@0.25.6: + resolution: {integrity: sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==} + engines: {node: '>=18'} + hasBin: true + escalade@3.1.1: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} engines: {node: '>=6'} @@ -1737,8 +2294,8 @@ packages: resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} engines: {node: '>=16.17'} - execa@9.5.2: - resolution: {integrity: sha512-EHlpxMCpHWSAh1dgS6bVeoLAXGnJNdR93aabr4QCGbzOM73o5XmRfM/e5FUqsw3aagP8S8XEWUWFAxnRBnAF0Q==} + execa@9.6.0: + resolution: {integrity: sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw==} engines: {node: ^18.19.0 || >=20.5.0} expect-type@1.1.0: @@ -1777,6 +2334,14 @@ packages: picomatch: optional: true + fdir@6.4.6: + resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + fetch-blob@3.2.0: resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} engines: {node: ^12.20 || >= 14.13} @@ -1811,6 +2376,9 @@ packages: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} + fix-dts-default-cjs-exports@1.0.1: + resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} + flat@5.0.2: resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} hasBin: true @@ -1818,8 +2386,8 @@ packages: flatted@3.3.2: resolution: {integrity: sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==} - focus-trap@7.6.2: - resolution: {integrity: sha512-9FhUxK1hVju2+AiQIDJ5Dd//9R2n2RAfJ0qfhF4IHGHgcoEUTMpbTeG/zbEuwaiYXfuAH6XE0/aCyxDdRM+W5w==} + focus-trap@7.6.5: + resolution: {integrity: sha512-7Ke1jyybbbPZyZXFxEftUtxFGLMpE2n6A+z//m4CRDlj0hW+o3iYSmh8nFlYMurOiJVDmJRilUQtJr08KfIxlg==} follow-redirects@1.15.9: resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} @@ -1834,8 +2402,8 @@ packages: resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} engines: {node: '>=14'} - form-data@4.0.1: - resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} + form-data@4.0.4: + resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} engines: {node: '>= 6'} formdata-polyfill@4.0.10: @@ -1849,14 +2417,10 @@ packages: resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} engines: {node: '>=12'} - fs-extra@11.2.0: - resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==} + fs-extra@11.3.0: + resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==} engines: {node: '>=14.14'} - fs-extra@7.0.1: - resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} - engines: {node: '>=6 <7 || >=8'} - fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} @@ -1868,9 +2432,9 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - geckodriver@4.5.1: - resolution: {integrity: sha512-lGCRqPMuzbRNDWJOQcUqhNqPvNsIFu6yzXF8J/6K3WCYFd2r5ckbeF7h1cxsnjA7YLSEiWzERCt6/gjZ3tW0ug==} - engines: {node: ^16.13 || >=18 || >=20} + geckodriver@5.0.0: + resolution: {integrity: sha512-vn7TtQ3b9VMJtVXsyWtQQl1fyBVFhQy7UvJF96kPuuJ0or5THH496AD3eUyaDD11+EqCxH9t6V+EP9soZQk4YQ==} + engines: {node: '>=18.0.0'} hasBin: true get-caller-file@2.0.5: @@ -1884,11 +2448,19 @@ packages: get-func-name@2.0.2: resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + get-pkg-repo@4.2.1: resolution: {integrity: sha512-2+QbHjFRfGB74v/pYWjd5OhU3TDIC2Gv/YKUTk/tCvAz0pkn/Mz6P3uByuBimLOcPvN2jYdScl3xGFSrx0jEcA==} engines: {node: '>=6.9.0'} hasBin: true + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + get-stream@5.2.0: resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} engines: {node: '>=8'} @@ -1901,8 +2473,11 @@ packages: resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} engines: {node: '>=18'} - get-uri@6.0.3: - resolution: {integrity: sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==} + get-tsconfig@4.10.1: + resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} + + get-uri@6.0.5: + resolution: {integrity: sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==} engines: {node: '>= 14'} git-raw-commits@2.0.11: @@ -1953,6 +2528,10 @@ packages: resolution: {integrity: sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==} engines: {node: '>=18'} + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -1969,6 +2548,10 @@ packages: resolution: {integrity: sha512-KyrFvnl+J9US63TEzwoiJOQzZBJY7KgBushJA8X61DMbNsH+2ONkDuLDnCnwUiPTF42tLoEmrPyoqbenVA5zrg==} engines: {node: '>=18.0.0'} + happy-dom@18.0.1: + resolution: {integrity: sha512-qn+rKOW7KWpVTtgIUi6RVmTBZJSe2k0Db0vh1f7CWrWclkkc7/Q+FrOfkZIb2eiErLyqu5AXEzE7XthO9JVxRA==} + engines: {node: '>=20.0.0'} + hard-rejection@2.1.0: resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==} engines: {node: '>=6'} @@ -1981,12 +2564,20 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} - hasown@2.0.0: - resolution: {integrity: sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==} + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} - hast-util-to-html@9.0.3: - resolution: {integrity: sha512-M17uBDzMJ9RPCqLMO92gNNUDuBSq10a25SDBI08iCCxmorf4Yy6sYHK57n9WAbRAAaU+DuR4W6GN9K4DFZesYg==} + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} hast-util-whitespace@3.0.0: resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} @@ -2009,6 +2600,10 @@ packages: resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} engines: {node: '>=12'} + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -2027,16 +2622,16 @@ packages: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} - https-proxy-agent@7.0.5: - resolution: {integrity: sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==} + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} human-signals@5.0.0: resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} engines: {node: '>=16.17.0'} - human-signals@8.0.0: - resolution: {integrity: sha512-/1/GPCpDUCCYwlERiYjxoczfP0zfvZMU/OWgQPMya9AbAE24vseigFdhAMObpc8Q4lc/kjutPfUddDYyAmejnA==} + human-signals@8.0.1: + resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} engines: {node: '>=18.18.0'} iconv-lite@0.6.3: @@ -2224,9 +2819,17 @@ packages: engines: {node: '>=10'} hasBin: true + jiti@2.4.2: + resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} + hasBin: true + jju@1.4.0: resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + js-beautify@1.15.1: resolution: {integrity: sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA==} engines: {node: '>=14'} @@ -2255,6 +2858,20 @@ packages: canvas: optional: true + jsdom@26.1.0: + resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + json-parse-better-errors@1.0.2: resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} @@ -2267,9 +2884,6 @@ packages: json-stringify-safe@5.0.1: resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} - jsonfile@4.0.0: - resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} - jsonfile@6.1.0: resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} @@ -2310,6 +2924,10 @@ packages: resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==} engines: {node: '>=4'} + load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + locate-path@2.0.0: resolution: {integrity: sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==} engines: {node: '>=4'} @@ -2351,6 +2969,7 @@ packages: lodash.clone@3.0.3: resolution: {integrity: sha512-yVYPpFTdZDCLG2p07gVRTvcwN5X04oj2hu4gG6r0fer58JA08wAVxXzWM+CmmxO2bzOH8u8BkZTZqgX6juVF7A==} + deprecated: This package is deprecated. Use structuredClone instead. lodash.defaultsdeep@4.6.1: resolution: {integrity: sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA==} @@ -2375,6 +2994,10 @@ packages: lodash.pick@4.4.0: resolution: {integrity: sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q==} + deprecated: This package is deprecated. Use destructuring assignment syntax instead. + + lodash.sortby@4.7.0: + resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} @@ -2419,8 +3042,8 @@ packages: lunr@2.3.9: resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==} - magic-string@0.30.13: - resolution: {integrity: sha512-8rYBO+MsWkgjDSOvLomYnzhdwEG51olQ4zL5KXnNJWV5MNmrb4rTZdrtkhxjnD/QyZUqR/Z/XDsUs/4ej2nx0g==} + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} magicast@0.3.5: resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} @@ -2451,6 +3074,10 @@ packages: resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} hasBin: true + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + mdast-util-to-hast@13.2.0: resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==} @@ -2480,8 +3107,8 @@ packages: micromark-util-symbol@2.0.1: resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} - micromark-util-types@2.0.1: - resolution: {integrity: sha512-534m2WhVTddrcKVepwmVEVnUAmtrx9bfIjNoQHRqfnvdaHQiFytEhJoTgpWJvDEXCO5gLTQh3wYC1PgOJA4NSQ==} + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} @@ -2551,12 +3178,15 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} - minisearch@7.1.1: - resolution: {integrity: sha512-b3YZEYCEH4EdCAtYP7OlDyx7FdPwNzuNwLQ34SfJpM9dlbBZzeXndGavTrC+VCiRWomL21SWfMc6SCKO/U2ZNw==} + minisearch@7.1.2: + resolution: {integrity: sha512-R1Pd9eF+MD5JYDDSPAp/q1ougKglm14uEkPMvQ/05RGmx6G9wvmLTrTI/Q5iPNJLYqNdsDQ7qTGIcNWR+FrHmA==} mitt@3.0.1: resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + mlly@1.7.4: + resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==} + mocha@9.2.2: resolution: {integrity: sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g==} engines: {node: '>= 12.0.0'} @@ -2579,13 +3209,16 @@ packages: muggle-string@0.4.1: resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + nanoid@3.3.1: resolution: {integrity: sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - nanoid@3.3.7: - resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -2621,6 +3254,7 @@ packages: node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead node-fetch@3.3.2: resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} @@ -2650,8 +3284,12 @@ packages: resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} engines: {node: '>=18'} - nwsapi@2.2.13: - resolution: {integrity: sha512-cTGB9ptp9dY9A5VbMSe7fQBcl/tt22Vcqdq8+eN93rblOuE0aCFu4aZ2vMwct/2t+lFnosm8RkQW1I0Omb1UtQ==} + nwsapi@2.2.20: + resolution: {integrity: sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -2668,8 +3306,8 @@ packages: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} - oniguruma-to-es@0.4.1: - resolution: {integrity: sha512-rNcEohFz095QKGRovP/yqPIKc+nP+Sjs4YTHMv33nMePGKrq/r2eu9Yh4646M5XluGJsUnmwoXuiXE69KDs+fQ==} + oniguruma-to-es@3.1.1: + resolution: {integrity: sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ==} open@8.4.0: resolution: {integrity: sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==} @@ -2715,8 +3353,8 @@ packages: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} - pac-proxy-agent@7.0.2: - resolution: {integrity: sha512-BFi3vZnO9X5Qt6NRz7ZOaPja3ic0PhlsmCRYLOpN11+mWBCR6XJDqW5RF3j8jm4WGGQZtBA+bTfxYzeKW73eHg==} + pac-proxy-agent@7.2.0: + resolution: {integrity: sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==} engines: {node: '>= 14'} pac-resolver@7.0.1: @@ -2744,8 +3382,8 @@ packages: parse5@6.0.1: resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} - parse5@7.2.1: - resolution: {integrity: sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==} + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} @@ -2792,6 +3430,9 @@ packages: pathe@1.1.2: resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pathval@1.1.1: resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} @@ -2832,37 +3473,62 @@ packages: resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==} engines: {node: '>=4'} + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + pkg-dir@4.2.0: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} - postcss@8.4.49: - resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} - preact@10.25.0: - resolution: {integrity: sha512-6bYnzlLxXV3OSpUxLdaxBmE7PMOu0aR3pG6lryK/0jmvcDFPlcXGQAt5DpK3RITWiDrfYZRI0druyaK/S9kYLg==} + preact@10.26.9: + resolution: {integrity: sha512-SSjF9vcnF27mJK1XyFMNJzFd5u3pQiATFqoaDy03XuN00u4ziveVVEGt5RKJrDR8MHE/wJo9Nnad56RLzS2RMA==} prettier@3.5.3: resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==} engines: {node: '>=14'} hasBin: true - pretty-ms@9.1.0: - resolution: {integrity: sha512-o1piW0n3tgKIKCwk2vpM/vOV13zjJzvP37Ioze54YlTHE06m4tjEbzg9WsKkvTuyYln2DHjo5pY4qrZGI0otpw==} + pretty-ms@9.2.0: + resolution: {integrity: sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==} engines: {node: '>=18'} process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} - property-information@6.5.0: - resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} proto-list@1.2.4: resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} - proxy-agent@6.4.0: - resolution: {integrity: sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==} + proxy-agent@6.5.0: + resolution: {integrity: sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==} engines: {node: '>= 14'} proxy-from-env@1.1.0: @@ -2876,8 +3542,8 @@ packages: psl@1.13.0: resolution: {integrity: sha512-BFwmFXiJoFqlUpZ5Qssolv15DMyc84gTBds1BjsV1BfXEo1UyyD7GsmN67n7J77uRhoSNW1AXtXKPLcBFQn9Aw==} - pump@3.0.2: - resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} + pump@3.0.3: + resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} punycode.js@2.3.1: resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} @@ -2895,15 +3561,15 @@ packages: (For a CapTP with native promises, see @endo/eventual-send and @endo/captp) + quansync@0.2.10: + resolution: {integrity: sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==} + querystringify@2.2.0: resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - queue-tick@1.0.1: - resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==} - quick-lru@4.0.1: resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==} engines: {node: '>=8'} @@ -2938,18 +3604,22 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + redent@3.0.0: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} - regex-recursion@4.2.1: - resolution: {integrity: sha512-QHNZyZAeKdndD1G3bKAbBEKOSSK4KOHQrAJ01N1LJeb0SoH4DJIeFhp0uUpETgONifS4+P3sOgoA1dhzgrQvhA==} + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} regex-utilities@2.3.0: resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} - regex@5.0.2: - resolution: {integrity: sha512-/pczGbKIQgfTMRV0XjABvc5RzLqQmwqxLHdQao2RTXPk+pmTXB2P0IaUHYdYyk412YLwUIkaeMd5T+RzVgTqnQ==} + regex@6.0.1: + resolution: {integrity: sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==} require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} @@ -2962,6 +3632,13 @@ packages: requires-port@1.0.0: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve@1.22.8: resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} hasBin: true @@ -2974,6 +3651,10 @@ packages: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} + ret@0.5.0: + resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==} + engines: {node: '>=10'} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -2996,6 +3677,26 @@ packages: engines: {node: 20 || >=22} hasBin: true + rolldown-plugin-dts@0.13.14: + resolution: {integrity: sha512-wjNhHZz9dlN6PTIXyizB6u/mAg1wEFMW9yw7imEVe3CxHSRnNHVyycIX0yDEOVJfDNISLPbkCIPEpFpizy5+PQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + '@typescript/native-preview': '>=7.0.0-dev.20250601.1' + rolldown: ^1.0.0-beta.9 + typescript: ^5.0.0 + vue-tsc: ^2.2.0 || ^3.0.0 + peerDependenciesMeta: + '@typescript/native-preview': + optional: true + typescript: + optional: true + vue-tsc: + optional: true + + rolldown@1.0.0-beta.28: + resolution: {integrity: sha512-QOANlVluwwrLP5snQqKfC2lv/KJphMkjh4V0gpw0K40GdKmhd8eShIGOJNAC51idk5cn3xI08SZTRWj0R2XlDw==} + hasBin: true + rollup-plugin-analyzer@4.0.0: resolution: {integrity: sha512-LL9GEt3bkXp6Wa19SNR5MWcvHNMvuTFYg+eYBZN2OIFhSWN+pEJUQXEKu5BsOeABob3x9PDaLKW7w5iOJnsESQ==} engines: {node: '>=8.0.0'} @@ -3016,6 +3717,14 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rollup@4.45.1: + resolution: {integrity: sha512-4iya7Jb76fVpQyLoiVpzUrsjQ12r3dM7fIVz+4NwoYvZOShknRmiv+iu9CClZml5ZLGb0XMcYLutK6w9tgxHDw==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + rrweb-cssom@0.8.0: + resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -3025,6 +3734,9 @@ packages: safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-regex2@5.0.0: + resolution: {integrity: sha512-YwJwe5a51WlK7KbOJREPdjNrpViQBI3p4T50lfwPuDhZnE3XGVTlGvi+aolc5+RvxDD6bnUmjVsU9n1eboLUYw==} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -3032,8 +3744,12 @@ packages: resolution: {integrity: sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==} engines: {node: '>=10'} - search-insights@2.17.1: - resolution: {integrity: sha512-HHFjYH/0AqXacETlIbe9EYc3UNlQYGNNTY0fZ/sWl6SweX+GDxq9NB5+RVoPLgEFuOtCz7M9dhYxqDnhbbF0eQ==} + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + search-insights@2.17.3: + resolution: {integrity: sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==} selenium-webdriver@4.6.1: resolution: {integrity: sha512-FT8Dw0tbzaTp8YYLuwhaCnve/nw03HKrOJrA3aUmTKmxaIFSP4kT2R5fN3K0RpV5kbR0ZnM4FGVI2vANBvekaA==} @@ -3057,13 +3773,13 @@ packages: engines: {node: '>=10'} hasBin: true - semver@7.6.3: - resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + semver@7.7.1: + resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} engines: {node: '>=10'} hasBin: true - semver@7.7.1: - resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} engines: {node: '>=10'} hasBin: true @@ -3084,8 +3800,8 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - shiki@1.23.1: - resolution: {integrity: sha512-8kxV9TH4pXgdKGxNOkrSMydn1Xf6It8lsle0fiqxf7a1149K1WGtdOu3Zb91T5r1JpvRPxqxU3C2XdZZXQnrig==} + shiki@2.5.0: + resolution: {integrity: sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ==} siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -3101,8 +3817,8 @@ packages: resolution: {integrity: sha512-N+goiLxlkHJlyaYEglFypzVNMaNplPAk5syu0+OPp/Bk6dwVoXF6FfOw2vO0Dp+JHsBaI+w6cm8TnFl2Hw6tDA==} hasBin: true - simple-git@3.27.0: - resolution: {integrity: sha512-ivHoFS9Yi9GY49ogc6/YAi3Fl9ROnF4VyubNylgCkA+RVqLaKWnDSzXOVzya8csELIaWaYNutsEuAhZrtOjozA==} + simple-git@3.28.0: + resolution: {integrity: sha512-Rs/vQRwsn1ILH1oBUy8NucJlXmnnLeLCfcvbSehkPzbv3wwoFWIdtfd6Ndo6ZPhlPsCZ60CPI4rxurnwAa+a2w==} sirv@3.0.0: resolution: {integrity: sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg==} @@ -3127,12 +3843,12 @@ packages: smob@1.4.0: resolution: {integrity: sha512-MqR3fVulhjWuRNSMydnTlweu38UhQ0HXM4buStD/S3mc/BzX3CuM9OmhyQpmtYCvoYdl5ris6TI0ZqH355Ymqg==} - socks-proxy-agent@8.0.4: - resolution: {integrity: sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==} + socks-proxy-agent@8.0.5: + resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} engines: {node: '>= 14'} - socks@2.8.3: - resolution: {integrity: sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==} + socks@2.8.6: + resolution: {integrity: sha512-pe4Y2yzru68lXCb38aAqRf5gvN8YdjP1lok5o0J7BOHljkyCGKVz7H3vpVIXKD27rj2giOJ7DwVyk/GWrPHDWA==} engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} source-map-js@1.2.1: @@ -3146,6 +3862,10 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + source-map@0.8.0-beta.0: + resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} + engines: {node: '>= 8'} + space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} @@ -3193,8 +3913,8 @@ packages: stream-combiner@0.0.4: resolution: {integrity: sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==} - streamx@2.20.0: - resolution: {integrity: sha512-ZGd1LhDeGFucr1CUCTBOS58ZhEendd0ttpGT3usTvosS4ntIwKN9LJFp+OeCSprsCPL14BXVRZlHGRY1V9PVzQ==} + streamx@2.22.1: + resolution: {integrity: sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==} string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} @@ -3249,8 +3969,13 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} - superjson@2.2.1: - resolution: {integrity: sha512-8iGv75BYOa0xRJHK5vRLEjE2H/i4lulTjzpUXic3Eg8akftYjkmQDa8JARQ42rlczXyFR3IeRoeFCc7RxHsYZA==} + sucrase@3.35.0: + resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + superjson@2.2.2: + resolution: {integrity: sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==} engines: {node: '>=16'} supports-color@5.5.0: @@ -3275,8 +4000,8 @@ packages: tabbable@6.2.0: resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} - tar-fs@3.0.6: - resolution: {integrity: sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==} + tar-fs@3.1.0: + resolution: {integrity: sha512-5Mty5y/sOF1YWj1J6GiBodjlDc05CUR8PKXrsnFAiSG0xA+GHeWLovaZPYUDXkH/1iKRf2+M5+OrRgzC7O9b7w==} tar-stream@3.1.7: resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} @@ -3296,8 +4021,8 @@ packages: resolution: {integrity: sha512-uNFCg478XovRi85iD42egu+eSFUmmka750Jy7L5tfHI5hQKKtbPnxaSaXAbBqCDYrw3wx4tXjKwci4/QmsZJxw==} engines: {node: '>=8'} - terser@5.32.0: - resolution: {integrity: sha512-v3Gtw3IzpBJ0ugkxEX8U0W6+TnPKRRCWGh1jC/iM/e3Ki5+qvO1L1EAZ56bZasc64aXHwRHNIQEzm6//i5cemQ==} + terser@5.43.1: + resolution: {integrity: sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==} engines: {node: '>=10'} hasBin: true @@ -3305,13 +4030,20 @@ packages: resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} engines: {node: '>=18'} - text-decoder@1.1.1: - resolution: {integrity: sha512-8zll7REEv4GDD3x4/0pW+ppIxSNs7H1J10IKFZsuOMscumCdM2a+toDGLPA3T+1+fLBql4zbt5z83GEQGGV5VA==} + text-decoder@1.2.3: + resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==} text-extensions@1.9.0: resolution: {integrity: sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==} engines: {node: '>=0.10'} + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + through2@2.0.5: resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} @@ -3327,10 +4059,20 @@ packages: tinyexec@0.3.1: resolution: {integrity: sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==} + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyexec@1.0.1: + resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==} + tinyglobby@0.2.10: resolution: {integrity: sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew==} engines: {node: '>=12.0.0'} + tinyglobby@0.2.14: + resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} + engines: {node: '>=12.0.0'} + tinypool@1.0.2: resolution: {integrity: sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -3343,6 +4085,13 @@ packages: resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} engines: {node: '>=14.0.0'} + tldts-core@6.1.86: + resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} + + tldts@6.1.86: + resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} + hasBin: true + tmp@0.2.1: resolution: {integrity: sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==} engines: {node: '>=8.17.0'} @@ -3359,10 +4108,25 @@ packages: resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} engines: {node: '>=6'} + tough-cookie@5.1.2: + resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} + engines: {node: '>=16'} + + tr46@1.0.1: + resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} + tr46@3.0.0: resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==} engines: {node: '>=12'} + tr46@5.1.1: + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} + engines: {node: '>=18'} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -3370,9 +4134,53 @@ packages: resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==} engines: {node: '>=8'} + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + tsdown@0.12.9: + resolution: {integrity: sha512-MfrXm9PIlT3saovtWKf/gCJJ/NQCdE0SiREkdNC+9Qy6UHhdeDPxnkFaBD7xttVUmgp0yUHtGirpoLB+OVLuLA==} + engines: {node: '>=18.0.0'} + hasBin: true + peerDependencies: + '@arethetypeswrong/core': ^0.18.1 + publint: ^0.3.0 + typescript: ^5.0.0 + unplugin-lightningcss: ^0.4.0 + unplugin-unused: ^0.5.0 + peerDependenciesMeta: + '@arethetypeswrong/core': + optional: true + publint: + optional: true + typescript: + optional: true + unplugin-lightningcss: + optional: true + unplugin-unused: + optional: true + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsup@8.5.0: + resolution: {integrity: sha512-VmBp77lWNQq6PfuMqCHD3xWl22vEoWsKajkF8t+yMBawlUS8JzEI+vOVMeuNZIuMML8qXRizFKi9oD5glKQVcQ==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + '@microsoft/api-extractor': ^7.36.0 + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.5.0' + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + type-detect@4.0.8: resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} engines: {node: '>=4'} @@ -3397,43 +4205,49 @@ packages: resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} engines: {node: '>=8'} - typedoc-plugin-markdown@4.2.10: - resolution: {integrity: sha512-PLX3pc1/7z13UJm4TDE9vo9jWGcClFUErXXtd5LdnoLjV6mynPpqZLU992DwMGFSRqJFZeKbVyqlNNeNHnk2tQ==} + typedoc-plugin-markdown@4.7.0: + resolution: {integrity: sha512-PitbnAps2vpcqK2gargKoiFXLWFttvwUbyns/E6zGIFG5Gz8ZQJGttHnYR9csOlcSjB/uyjd8tnoayrtsXG17w==} engines: {node: '>= 18'} peerDependencies: - typedoc: 0.26.x + typedoc: 0.28.x - typedoc@0.26.11: - resolution: {integrity: sha512-sFEgRRtrcDl2FxVP58Ze++ZK2UQAEvtvvH8rRlig1Ja3o7dDaMHmaBfvJmdGnNEFaLTpQsN8dpvZaTqJSu/Ugw==} - engines: {node: '>= 18'} + typedoc@0.28.7: + resolution: {integrity: sha512-lpz0Oxl6aidFkmS90VQDQjk/Qf2iw0IUvFqirdONBdj7jPSN9mGXhy66BcGNDxx5ZMyKKiBVAREvPEzT6Uxipw==} + engines: {node: '>= 18', pnpm: '>= 10'} hasBin: true peerDependencies: - typescript: 4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x + typescript: 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x - typescript@5.4.2: - resolution: {integrity: sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==} + typescript@5.8.2: + resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==} engines: {node: '>=14.17'} hasBin: true - typescript@5.6.3: - resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} engines: {node: '>=14.17'} hasBin: true uc.micro@2.1.0: resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + ufo@1.6.1: + resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} + uglify-js@3.17.4: resolution: {integrity: sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==} engines: {node: '>=0.8.0'} hasBin: true - undici-types@6.19.8: - resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + unconfig@7.3.2: + resolution: {integrity: sha512-nqG5NNL2wFVGZ0NA/aCFw0oJ2pxSf1lwg4Z5ill8wd7K4KX/rQbHlwbh+bjctXL5Ly1xtzHenHGOK0b+lG6JVg==} undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@7.8.0: + resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==} + unicorn-magic@0.3.0: resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} engines: {node: '>=18'} @@ -3453,10 +4267,6 @@ packages: unist-util-visit@5.0.0: resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} - universalify@0.1.2: - resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} - engines: {node: '>= 4.0.0'} - universalify@0.2.0: resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} engines: {node: '>= 4.0.0'} @@ -3501,39 +4311,8 @@ packages: engines: {node: ^18.0.0 || >=20.0.0} hasBin: true - vite@5.4.11: - resolution: {integrity: sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@types/node': ^18.0.0 || >=20.0.0 - less: '*' - lightningcss: ^1.21.0 - sass: '*' - sass-embedded: '*' - stylus: '*' - sugarss: '*' - terser: ^5.4.0 - peerDependenciesMeta: - '@types/node': - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - - vite@5.4.18: - resolution: {integrity: sha512-1oDcnEp3lVyHCuQ2YFelM4Alm2o91xNoMncRm1U7S+JdYfYOvbiGZ3/CxGttrOu2M/KcGz7cRC2DoNUA6urmMA==} + vite@5.4.19: + resolution: {integrity: sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -3570,8 +4349,8 @@ packages: vitepress: ^1.0.0 vue: ^3.4.8 - vitepress@1.5.0: - resolution: {integrity: sha512-q4Q/G2zjvynvizdB3/bupdYkCJe2umSAMv9Ju4d92E6/NXJ59z70xB0q5p/4lpRyAwflDsbwy1mLV9Q5+nlB+g==} + vitepress@1.6.3: + resolution: {integrity: sha512-fCkfdOk8yRZT8GD9BFqusW3+GggWYZ/rYncOfmgcDtP3ualNHCAg+Robxp2/6xfH1WwPHtGpPwv7mbA3qomtBw==} hasBin: true peerDependencies: markdown-it-mathjax3: ^4 @@ -3613,25 +4392,14 @@ packages: vue-component-type-helpers@2.0.21: resolution: {integrity: sha512-3NaicyZ7N4B6cft4bfb7dOnPbE9CjLcx+6wZWAg5zwszfO4qXRh+U52dN5r5ZZfc6iMaxKCEcoH9CmxxoFZHLg==} - vue-demi@0.14.10: - resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} - engines: {node: '>=12'} - hasBin: true - peerDependencies: - '@vue/composition-api': ^1.0.0-rc.1 - vue: ^3.0.0-0 || ^2.6.0 - peerDependenciesMeta: - '@vue/composition-api': - optional: true - - vue-tsc@2.2.10: - resolution: {integrity: sha512-jWZ1xSaNbabEV3whpIDMbjVSVawjAyW+x1n3JeGQo7S0uv2n9F/JMgWW90tGWNFRKya4YwKMZgCtr0vRAM7DeQ==} + vue-tsc@2.2.12: + resolution: {integrity: sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==} hasBin: true peerDependencies: typescript: '>=5.0.0' - vue@3.5.13: - resolution: {integrity: sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==} + vue@3.5.17: + resolution: {integrity: sha512-LbHV3xPN9BeljML+Xctq4lbz2lVHCR6DtbpTf5XIO6gugpXUN49j2QQPcMj086r9+AkJ0FfUT8xjulKKBkkr9g==} peerDependencies: typescript: '*' peerDependenciesMeta: @@ -3646,6 +4414,10 @@ packages: resolution: {integrity: sha512-3WFqGEgSXIyGhOmAFtlicJNMjEps8b1MG31NCA0/vOF9+nKMUW1ckhi9cnNHmf88Rzw5V+dwIwsm2C7X8k9aQg==} engines: {node: '>=12'} + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} @@ -3653,6 +4425,9 @@ packages: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} + webidl-conversions@4.0.2: + resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} + webidl-conversions@7.0.0: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} @@ -3661,10 +4436,18 @@ packages: resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} engines: {node: '>=12'} + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + whatwg-mimetype@3.0.0: resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} engines: {node: '>=12'} + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + whatwg-url@10.0.0: resolution: {integrity: sha512-CLxxCmdUby142H5FZzn4D8ikO1cmypvXVQktsgosNy4a4BHrDHeciBBGZhb0bNoR5/MltoCatso+vFjjGx8t0w==} engines: {node: '>=12'} @@ -3673,14 +4456,21 @@ packages: resolution: {integrity: sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==} engines: {node: '>=12'} + whatwg-url@14.2.0: + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} + + whatwg-url@7.1.0: + resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} hasBin: true - which@4.0.0: - resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==} - engines: {node: ^16.13.0 || >=18.0.0} + which@5.0.0: + resolution: {integrity: sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==} + engines: {node: ^18.17.0 || >=20.5.0} hasBin: true why-is-node-running@2.3.0: @@ -3713,8 +4503,8 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - ws@8.18.0: - resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -3729,6 +4519,10 @@ packages: resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} engines: {node: '>=12'} + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} @@ -3743,14 +4537,9 @@ packages: yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - yaml@2.6.1: - resolution: {integrity: sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==} - engines: {node: '>= 14'} - hasBin: true - - yaml@2.7.1: - resolution: {integrity: sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==} - engines: {node: '>= 14'} + yaml@2.8.0: + resolution: {integrity: sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==} + engines: {node: '>= 14.6'} hasBin: true yargs-parser@20.2.4: @@ -3785,151 +4574,193 @@ packages: snapshots: - '@algolia/autocomplete-core@1.17.7(@algolia/client-search@5.15.0)(algoliasearch@5.15.0)(search-insights@2.17.1)': + '@algolia/autocomplete-core@1.17.7(@algolia/client-search@5.34.0)(algoliasearch@5.34.0)(search-insights@2.17.3)': dependencies: - '@algolia/autocomplete-plugin-algolia-insights': 1.17.7(@algolia/client-search@5.15.0)(algoliasearch@5.15.0)(search-insights@2.17.1) - '@algolia/autocomplete-shared': 1.17.7(@algolia/client-search@5.15.0)(algoliasearch@5.15.0) + '@algolia/autocomplete-plugin-algolia-insights': 1.17.7(@algolia/client-search@5.34.0)(algoliasearch@5.34.0)(search-insights@2.17.3) + '@algolia/autocomplete-shared': 1.17.7(@algolia/client-search@5.34.0)(algoliasearch@5.34.0) transitivePeerDependencies: - '@algolia/client-search' - algoliasearch - search-insights - '@algolia/autocomplete-plugin-algolia-insights@1.17.7(@algolia/client-search@5.15.0)(algoliasearch@5.15.0)(search-insights@2.17.1)': + '@algolia/autocomplete-plugin-algolia-insights@1.17.7(@algolia/client-search@5.34.0)(algoliasearch@5.34.0)(search-insights@2.17.3)': dependencies: - '@algolia/autocomplete-shared': 1.17.7(@algolia/client-search@5.15.0)(algoliasearch@5.15.0) - search-insights: 2.17.1 + '@algolia/autocomplete-shared': 1.17.7(@algolia/client-search@5.34.0)(algoliasearch@5.34.0) + search-insights: 2.17.3 transitivePeerDependencies: - '@algolia/client-search' - algoliasearch - '@algolia/autocomplete-preset-algolia@1.17.7(@algolia/client-search@5.15.0)(algoliasearch@5.15.0)': + '@algolia/autocomplete-preset-algolia@1.17.7(@algolia/client-search@5.34.0)(algoliasearch@5.34.0)': dependencies: - '@algolia/autocomplete-shared': 1.17.7(@algolia/client-search@5.15.0)(algoliasearch@5.15.0) - '@algolia/client-search': 5.15.0 - algoliasearch: 5.15.0 + '@algolia/autocomplete-shared': 1.17.7(@algolia/client-search@5.34.0)(algoliasearch@5.34.0) + '@algolia/client-search': 5.34.0 + algoliasearch: 5.34.0 - '@algolia/autocomplete-shared@1.17.7(@algolia/client-search@5.15.0)(algoliasearch@5.15.0)': + '@algolia/autocomplete-shared@1.17.7(@algolia/client-search@5.34.0)(algoliasearch@5.34.0)': dependencies: - '@algolia/client-search': 5.15.0 - algoliasearch: 5.15.0 + '@algolia/client-search': 5.34.0 + algoliasearch: 5.34.0 - '@algolia/client-abtesting@5.15.0': + '@algolia/client-abtesting@5.34.0': dependencies: - '@algolia/client-common': 5.15.0 - '@algolia/requester-browser-xhr': 5.15.0 - '@algolia/requester-fetch': 5.15.0 - '@algolia/requester-node-http': 5.15.0 + '@algolia/client-common': 5.34.0 + '@algolia/requester-browser-xhr': 5.34.0 + '@algolia/requester-fetch': 5.34.0 + '@algolia/requester-node-http': 5.34.0 - '@algolia/client-analytics@5.15.0': + '@algolia/client-analytics@5.34.0': dependencies: - '@algolia/client-common': 5.15.0 - '@algolia/requester-browser-xhr': 5.15.0 - '@algolia/requester-fetch': 5.15.0 - '@algolia/requester-node-http': 5.15.0 + '@algolia/client-common': 5.34.0 + '@algolia/requester-browser-xhr': 5.34.0 + '@algolia/requester-fetch': 5.34.0 + '@algolia/requester-node-http': 5.34.0 - '@algolia/client-common@5.15.0': {} + '@algolia/client-common@5.34.0': {} - '@algolia/client-insights@5.15.0': + '@algolia/client-insights@5.34.0': dependencies: - '@algolia/client-common': 5.15.0 - '@algolia/requester-browser-xhr': 5.15.0 - '@algolia/requester-fetch': 5.15.0 - '@algolia/requester-node-http': 5.15.0 + '@algolia/client-common': 5.34.0 + '@algolia/requester-browser-xhr': 5.34.0 + '@algolia/requester-fetch': 5.34.0 + '@algolia/requester-node-http': 5.34.0 - '@algolia/client-personalization@5.15.0': + '@algolia/client-personalization@5.34.0': dependencies: - '@algolia/client-common': 5.15.0 - '@algolia/requester-browser-xhr': 5.15.0 - '@algolia/requester-fetch': 5.15.0 - '@algolia/requester-node-http': 5.15.0 + '@algolia/client-common': 5.34.0 + '@algolia/requester-browser-xhr': 5.34.0 + '@algolia/requester-fetch': 5.34.0 + '@algolia/requester-node-http': 5.34.0 - '@algolia/client-query-suggestions@5.15.0': + '@algolia/client-query-suggestions@5.34.0': dependencies: - '@algolia/client-common': 5.15.0 - '@algolia/requester-browser-xhr': 5.15.0 - '@algolia/requester-fetch': 5.15.0 - '@algolia/requester-node-http': 5.15.0 + '@algolia/client-common': 5.34.0 + '@algolia/requester-browser-xhr': 5.34.0 + '@algolia/requester-fetch': 5.34.0 + '@algolia/requester-node-http': 5.34.0 - '@algolia/client-search@5.15.0': + '@algolia/client-search@5.34.0': dependencies: - '@algolia/client-common': 5.15.0 - '@algolia/requester-browser-xhr': 5.15.0 - '@algolia/requester-fetch': 5.15.0 - '@algolia/requester-node-http': 5.15.0 + '@algolia/client-common': 5.34.0 + '@algolia/requester-browser-xhr': 5.34.0 + '@algolia/requester-fetch': 5.34.0 + '@algolia/requester-node-http': 5.34.0 - '@algolia/ingestion@1.15.0': + '@algolia/ingestion@1.34.0': dependencies: - '@algolia/client-common': 5.15.0 - '@algolia/requester-browser-xhr': 5.15.0 - '@algolia/requester-fetch': 5.15.0 - '@algolia/requester-node-http': 5.15.0 + '@algolia/client-common': 5.34.0 + '@algolia/requester-browser-xhr': 5.34.0 + '@algolia/requester-fetch': 5.34.0 + '@algolia/requester-node-http': 5.34.0 - '@algolia/monitoring@1.15.0': + '@algolia/monitoring@1.34.0': dependencies: - '@algolia/client-common': 5.15.0 - '@algolia/requester-browser-xhr': 5.15.0 - '@algolia/requester-fetch': 5.15.0 - '@algolia/requester-node-http': 5.15.0 + '@algolia/client-common': 5.34.0 + '@algolia/requester-browser-xhr': 5.34.0 + '@algolia/requester-fetch': 5.34.0 + '@algolia/requester-node-http': 5.34.0 - '@algolia/recommend@5.15.0': + '@algolia/recommend@5.34.0': dependencies: - '@algolia/client-common': 5.15.0 - '@algolia/requester-browser-xhr': 5.15.0 - '@algolia/requester-fetch': 5.15.0 - '@algolia/requester-node-http': 5.15.0 + '@algolia/client-common': 5.34.0 + '@algolia/requester-browser-xhr': 5.34.0 + '@algolia/requester-fetch': 5.34.0 + '@algolia/requester-node-http': 5.34.0 - '@algolia/requester-browser-xhr@5.15.0': + '@algolia/requester-browser-xhr@5.34.0': dependencies: - '@algolia/client-common': 5.15.0 + '@algolia/client-common': 5.34.0 - '@algolia/requester-fetch@5.15.0': + '@algolia/requester-fetch@5.34.0': dependencies: - '@algolia/client-common': 5.15.0 + '@algolia/client-common': 5.34.0 - '@algolia/requester-node-http@5.15.0': + '@algolia/requester-node-http@5.34.0': dependencies: - '@algolia/client-common': 5.15.0 + '@algolia/client-common': 5.34.0 '@ampproject/remapping@2.3.0': dependencies: - '@jridgewell/gen-mapping': 0.3.5 - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/trace-mapping': 0.3.29 + + '@asamuzakjp/css-color@3.2.0': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + lru-cache: 10.4.3 + optional: true '@babel/code-frame@7.22.10': dependencies: '@babel/highlight': 7.22.10 chalk: 2.4.2 - '@babel/helper-string-parser@7.25.9': {} + '@babel/generator@7.28.0': + dependencies: + '@babel/parser': 7.28.0 + '@babel/types': 7.28.1 + '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/trace-mapping': 0.3.29 + jsesc: 3.1.0 + + '@babel/helper-string-parser@7.27.1': {} - '@babel/helper-validator-identifier@7.25.9': {} + '@babel/helper-validator-identifier@7.27.1': {} '@babel/highlight@7.22.10': dependencies: - '@babel/helper-validator-identifier': 7.25.9 + '@babel/helper-validator-identifier': 7.27.1 chalk: 2.4.2 js-tokens: 4.0.0 - '@babel/parser@7.26.2': + '@babel/parser@7.28.0': dependencies: - '@babel/types': 7.26.0 + '@babel/types': 7.28.1 - '@babel/types@7.26.0': + '@babel/types@7.28.1': dependencies: - '@babel/helper-string-parser': 7.25.9 - '@babel/helper-validator-identifier': 7.25.9 + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 '@bcoe/v8-coverage@0.2.3': {} '@colors/colors@1.5.0': optional: true - '@docsearch/css@3.8.0': {} + '@csstools/color-helpers@5.0.2': + optional: true + + '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + optional: true + + '@csstools/css-color-parser@3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/color-helpers': 5.0.2 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + optional: true + + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + optional: true - '@docsearch/js@3.8.0(@algolia/client-search@5.15.0)(search-insights@2.17.1)': + '@csstools/css-tokenizer@3.0.4': + optional: true + + '@docsearch/css@3.8.2': {} + + '@docsearch/js@3.8.2(@algolia/client-search@5.34.0)(search-insights@2.17.3)': dependencies: - '@docsearch/react': 3.8.0(@algolia/client-search@5.15.0)(search-insights@2.17.1) - preact: 10.25.0 + '@docsearch/react': 3.8.2(@algolia/client-search@5.34.0)(search-insights@2.17.3) + preact: 10.26.9 transitivePeerDependencies: - '@algolia/client-search' - '@types/react' @@ -3937,89 +4768,191 @@ snapshots: - react-dom - search-insights - '@docsearch/react@3.8.0(@algolia/client-search@5.15.0)(search-insights@2.17.1)': + '@docsearch/react@3.8.2(@algolia/client-search@5.34.0)(search-insights@2.17.3)': dependencies: - '@algolia/autocomplete-core': 1.17.7(@algolia/client-search@5.15.0)(algoliasearch@5.15.0)(search-insights@2.17.1) - '@algolia/autocomplete-preset-algolia': 1.17.7(@algolia/client-search@5.15.0)(algoliasearch@5.15.0) - '@docsearch/css': 3.8.0 - algoliasearch: 5.15.0 + '@algolia/autocomplete-core': 1.17.7(@algolia/client-search@5.34.0)(algoliasearch@5.34.0)(search-insights@2.17.3) + '@algolia/autocomplete-preset-algolia': 1.17.7(@algolia/client-search@5.34.0)(algoliasearch@5.34.0) + '@docsearch/css': 3.8.2 + algoliasearch: 5.34.0 optionalDependencies: - search-insights: 2.17.1 + search-insights: 2.17.3 transitivePeerDependencies: - '@algolia/client-search' + '@emnapi/core@1.4.5': + dependencies: + '@emnapi/wasi-threads': 1.0.4 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.4.5': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.0.4': + dependencies: + tslib: 2.8.1 + optional: true + '@esbuild/aix-ppc64@0.21.5': optional: true + '@esbuild/aix-ppc64@0.25.6': + optional: true + '@esbuild/android-arm64@0.21.5': optional: true + '@esbuild/android-arm64@0.25.6': + optional: true + '@esbuild/android-arm@0.21.5': optional: true + '@esbuild/android-arm@0.25.6': + optional: true + '@esbuild/android-x64@0.21.5': optional: true + '@esbuild/android-x64@0.25.6': + optional: true + '@esbuild/darwin-arm64@0.21.5': optional: true + '@esbuild/darwin-arm64@0.25.6': + optional: true + '@esbuild/darwin-x64@0.21.5': optional: true + '@esbuild/darwin-x64@0.25.6': + optional: true + '@esbuild/freebsd-arm64@0.21.5': optional: true + '@esbuild/freebsd-arm64@0.25.6': + optional: true + '@esbuild/freebsd-x64@0.21.5': optional: true + '@esbuild/freebsd-x64@0.25.6': + optional: true + '@esbuild/linux-arm64@0.21.5': optional: true + '@esbuild/linux-arm64@0.25.6': + optional: true + '@esbuild/linux-arm@0.21.5': optional: true + '@esbuild/linux-arm@0.25.6': + optional: true + '@esbuild/linux-ia32@0.21.5': optional: true + '@esbuild/linux-ia32@0.25.6': + optional: true + '@esbuild/linux-loong64@0.21.5': optional: true + '@esbuild/linux-loong64@0.25.6': + optional: true + '@esbuild/linux-mips64el@0.21.5': optional: true + '@esbuild/linux-mips64el@0.25.6': + optional: true + '@esbuild/linux-ppc64@0.21.5': optional: true + '@esbuild/linux-ppc64@0.25.6': + optional: true + '@esbuild/linux-riscv64@0.21.5': optional: true + '@esbuild/linux-riscv64@0.25.6': + optional: true + '@esbuild/linux-s390x@0.21.5': optional: true + '@esbuild/linux-s390x@0.25.6': + optional: true + '@esbuild/linux-x64@0.21.5': optional: true + '@esbuild/linux-x64@0.25.6': + optional: true + + '@esbuild/netbsd-arm64@0.25.6': + optional: true + '@esbuild/netbsd-x64@0.21.5': optional: true + '@esbuild/netbsd-x64@0.25.6': + optional: true + + '@esbuild/openbsd-arm64@0.25.6': + optional: true + '@esbuild/openbsd-x64@0.21.5': optional: true + '@esbuild/openbsd-x64@0.25.6': + optional: true + + '@esbuild/openharmony-arm64@0.25.6': + optional: true + '@esbuild/sunos-x64@0.21.5': optional: true + '@esbuild/sunos-x64@0.25.6': + optional: true + '@esbuild/win32-arm64@0.21.5': optional: true + '@esbuild/win32-arm64@0.25.6': + optional: true + '@esbuild/win32-ia32@0.21.5': optional: true + '@esbuild/win32-ia32@0.25.6': + optional: true + '@esbuild/win32-x64@0.21.5': optional: true + '@esbuild/win32-x64@0.25.6': + optional: true + + '@gerrit0/mini-shiki@3.8.0': + dependencies: + '@shikijs/engine-oniguruma': 3.8.0 + '@shikijs/langs': 3.8.0 + '@shikijs/themes': 3.8.0 + '@shikijs/types': 3.8.0 + '@shikijs/vscode-textmate': 10.0.2 + '@hutson/parse-repository-url@3.0.2': {} - '@iconify-json/simple-icons@1.2.13': + '@iconify-json/simple-icons@1.2.43': dependencies: '@iconify/types': 2.0.0 @@ -4036,59 +4969,56 @@ snapshots: '@istanbuljs/schema@0.1.3': {} - '@jridgewell/gen-mapping@0.3.5': + '@jridgewell/gen-mapping@0.3.12': dependencies: - '@jridgewell/set-array': 1.2.1 - '@jridgewell/sourcemap-codec': 1.5.0 - '@jridgewell/trace-mapping': 0.3.25 - - '@jridgewell/resolve-uri@3.1.1': {} + '@jridgewell/sourcemap-codec': 1.5.4 + '@jridgewell/trace-mapping': 0.3.29 - '@jridgewell/set-array@1.2.1': {} + '@jridgewell/resolve-uri@3.1.2': {} - '@jridgewell/source-map@0.3.6': + '@jridgewell/source-map@0.3.10': dependencies: - '@jridgewell/gen-mapping': 0.3.5 - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/trace-mapping': 0.3.29 - '@jridgewell/sourcemap-codec@1.5.0': {} + '@jridgewell/sourcemap-codec@1.5.4': {} - '@jridgewell/trace-mapping@0.3.25': + '@jridgewell/trace-mapping@0.3.29': dependencies: - '@jridgewell/resolve-uri': 3.1.1 - '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.4 '@kwsites/file-exists@1.1.1': dependencies: - debug: 4.3.7 + debug: 4.4.1 transitivePeerDependencies: - supports-color '@kwsites/promise-deferred@1.1.1': {} - '@microsoft/api-extractor-model@7.30.0(@types/node@22.15.2)': + '@microsoft/api-extractor-model@7.30.6(@types/node@24.0.14)': dependencies: '@microsoft/tsdoc': 0.15.1 '@microsoft/tsdoc-config': 0.17.1 - '@rushstack/node-core-library': 5.10.0(@types/node@22.15.2) + '@rushstack/node-core-library': 5.13.1(@types/node@24.0.14) transitivePeerDependencies: - '@types/node' - '@microsoft/api-extractor@7.48.0(@types/node@22.15.2)': + '@microsoft/api-extractor@7.52.8(@types/node@24.0.14)': dependencies: - '@microsoft/api-extractor-model': 7.30.0(@types/node@22.15.2) + '@microsoft/api-extractor-model': 7.30.6(@types/node@24.0.14) '@microsoft/tsdoc': 0.15.1 '@microsoft/tsdoc-config': 0.17.1 - '@rushstack/node-core-library': 5.10.0(@types/node@22.15.2) + '@rushstack/node-core-library': 5.13.1(@types/node@24.0.14) '@rushstack/rig-package': 0.5.3 - '@rushstack/terminal': 0.14.3(@types/node@22.15.2) - '@rushstack/ts-command-line': 4.23.1(@types/node@22.15.2) + '@rushstack/terminal': 0.15.3(@types/node@24.0.14) + '@rushstack/ts-command-line': 5.0.1(@types/node@24.0.14) lodash: 4.17.21 minimatch: 3.0.8 resolve: 1.22.8 semver: 7.5.4 source-map: 0.6.1 - typescript: 5.4.2 + typescript: 5.8.2 transitivePeerDependencies: - '@types/node' @@ -4101,6 +5031,13 @@ snapshots: '@microsoft/tsdoc@0.15.1': {} + '@napi-rs/wasm-runtime@1.0.0': + dependencies: + '@emnapi/core': 1.4.5 + '@emnapi/runtime': 1.4.5 + '@tybys/wasm-util': 0.10.0 + optional: true + '@nightwatch/chai@5.0.2': dependencies: assertion-error: 1.1.0 @@ -4126,11 +5063,65 @@ snapshots: '@one-ini/wasm@0.1.1': {} + '@oxc-project/runtime@0.77.2': {} + + '@oxc-project/types@0.77.2': {} + '@pkgjs/parseargs@0.11.0': optional: true '@polka/url@1.0.0-next.28': {} + '@quansync/fs@0.1.3': + dependencies: + quansync: 0.2.10 + + '@rolldown/binding-android-arm64@1.0.0-beta.28': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-beta.28': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-beta.28': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-beta.28': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.28': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.28': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.28': + optional: true + + '@rolldown/binding-linux-arm64-ohos@1.0.0-beta.28': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.28': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-beta.28': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-beta.28': + dependencies: + '@napi-rs/wasm-runtime': 1.0.0 + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.28': + optional: true + + '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.28': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.28': + optional: true + + '@rolldown/pluginutils@1.0.0-beta.28': {} + '@rollup/plugin-alias@5.1.1(rollup@3.29.5)': optionalDependencies: rollup: 3.29.5 @@ -4142,7 +5133,7 @@ snapshots: estree-walker: 2.0.2 glob: 8.1.0 is-reference: 1.2.1 - magic-string: 0.30.13 + magic-string: 0.30.17 optionalDependencies: rollup: 3.29.5 @@ -4159,7 +5150,7 @@ snapshots: '@rollup/plugin-replace@5.0.7(rollup@3.29.5)': dependencies: '@rollup/pluginutils': 5.1.0(rollup@3.29.5) - magic-string: 0.30.13 + magic-string: 0.30.17 optionalDependencies: rollup: 3.29.5 @@ -4167,7 +5158,7 @@ snapshots: dependencies: serialize-javascript: 6.0.1 smob: 1.4.0 - terser: 5.32.0 + terser: 5.43.1 optionalDependencies: rollup: 3.29.5 @@ -4178,7 +5169,7 @@ snapshots: '@rollup/pluginutils@5.1.0(rollup@3.29.5)': dependencies: - '@types/estree': 1.0.6 + '@types/estree': 1.0.7 estree-walker: 2.0.2 picomatch: 2.3.1 optionalDependencies: @@ -4187,123 +5178,210 @@ snapshots: '@rollup/rollup-android-arm-eabi@4.27.4': optional: true + '@rollup/rollup-android-arm-eabi@4.45.1': + optional: true + '@rollup/rollup-android-arm64@4.27.4': optional: true + '@rollup/rollup-android-arm64@4.45.1': + optional: true + '@rollup/rollup-darwin-arm64@4.27.4': optional: true + '@rollup/rollup-darwin-arm64@4.45.1': + optional: true + '@rollup/rollup-darwin-x64@4.27.4': optional: true + '@rollup/rollup-darwin-x64@4.45.1': + optional: true + '@rollup/rollup-freebsd-arm64@4.27.4': optional: true + '@rollup/rollup-freebsd-arm64@4.45.1': + optional: true + '@rollup/rollup-freebsd-x64@4.27.4': optional: true + '@rollup/rollup-freebsd-x64@4.45.1': + optional: true + '@rollup/rollup-linux-arm-gnueabihf@4.27.4': optional: true + '@rollup/rollup-linux-arm-gnueabihf@4.45.1': + optional: true + '@rollup/rollup-linux-arm-musleabihf@4.27.4': optional: true + '@rollup/rollup-linux-arm-musleabihf@4.45.1': + optional: true + '@rollup/rollup-linux-arm64-gnu@4.27.4': optional: true + '@rollup/rollup-linux-arm64-gnu@4.45.1': + optional: true + '@rollup/rollup-linux-arm64-musl@4.27.4': optional: true + '@rollup/rollup-linux-arm64-musl@4.45.1': + optional: true + + '@rollup/rollup-linux-loongarch64-gnu@4.45.1': + optional: true + '@rollup/rollup-linux-powerpc64le-gnu@4.27.4': optional: true + '@rollup/rollup-linux-powerpc64le-gnu@4.45.1': + optional: true + '@rollup/rollup-linux-riscv64-gnu@4.27.4': optional: true + '@rollup/rollup-linux-riscv64-gnu@4.45.1': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.45.1': + optional: true + '@rollup/rollup-linux-s390x-gnu@4.27.4': optional: true + '@rollup/rollup-linux-s390x-gnu@4.45.1': + optional: true + '@rollup/rollup-linux-x64-gnu@4.27.4': optional: true + '@rollup/rollup-linux-x64-gnu@4.45.1': + optional: true + '@rollup/rollup-linux-x64-musl@4.27.4': optional: true + '@rollup/rollup-linux-x64-musl@4.45.1': + optional: true + '@rollup/rollup-win32-arm64-msvc@4.27.4': optional: true + '@rollup/rollup-win32-arm64-msvc@4.45.1': + optional: true + '@rollup/rollup-win32-ia32-msvc@4.27.4': optional: true + '@rollup/rollup-win32-ia32-msvc@4.45.1': + optional: true + '@rollup/rollup-win32-x64-msvc@4.27.4': optional: true - '@rushstack/node-core-library@5.10.0(@types/node@22.15.2)': + '@rollup/rollup-win32-x64-msvc@4.45.1': + optional: true + + '@rushstack/node-core-library@5.13.1(@types/node@24.0.14)': dependencies: ajv: 8.13.0 ajv-draft-04: 1.0.0(ajv@8.13.0) ajv-formats: 3.0.1(ajv@8.13.0) - fs-extra: 7.0.1 + fs-extra: 11.3.0 import-lazy: 4.0.0 jju: 1.4.0 resolve: 1.22.8 semver: 7.5.4 optionalDependencies: - '@types/node': 22.15.2 + '@types/node': 24.0.14 '@rushstack/rig-package@0.5.3': dependencies: resolve: 1.22.8 strip-json-comments: 3.1.1 - '@rushstack/terminal@0.14.3(@types/node@22.15.2)': + '@rushstack/terminal@0.15.3(@types/node@24.0.14)': + dependencies: + '@rushstack/node-core-library': 5.13.1(@types/node@24.0.14) + supports-color: 8.1.1 + optionalDependencies: + '@types/node': 24.0.14 + + '@rushstack/ts-command-line@5.0.1(@types/node@24.0.14)': + dependencies: + '@rushstack/terminal': 0.15.3(@types/node@24.0.14) + '@types/argparse': 1.0.38 + argparse: 1.0.10 + string-argv: 0.3.2 + transitivePeerDependencies: + - '@types/node' + + '@sec-ant/readable-stream@0.4.1': {} + + '@shikijs/core@2.5.0': + dependencies: + '@shikijs/engine-javascript': 2.5.0 + '@shikijs/engine-oniguruma': 2.5.0 + '@shikijs/types': 2.5.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + + '@shikijs/engine-javascript@2.5.0': + dependencies: + '@shikijs/types': 2.5.0 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 3.1.1 + + '@shikijs/engine-oniguruma@2.5.0': + dependencies: + '@shikijs/types': 2.5.0 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/engine-oniguruma@3.8.0': dependencies: - '@rushstack/node-core-library': 5.10.0(@types/node@22.15.2) - supports-color: 8.1.1 - optionalDependencies: - '@types/node': 22.15.2 + '@shikijs/types': 3.8.0 + '@shikijs/vscode-textmate': 10.0.2 - '@rushstack/ts-command-line@4.23.1(@types/node@22.15.2)': + '@shikijs/langs@2.5.0': dependencies: - '@rushstack/terminal': 0.14.3(@types/node@22.15.2) - '@types/argparse': 1.0.38 - argparse: 1.0.10 - string-argv: 0.3.2 - transitivePeerDependencies: - - '@types/node' + '@shikijs/types': 2.5.0 - '@sec-ant/readable-stream@0.4.1': {} + '@shikijs/langs@3.8.0': + dependencies: + '@shikijs/types': 3.8.0 - '@shikijs/core@1.23.1': + '@shikijs/themes@2.5.0': dependencies: - '@shikijs/engine-javascript': 1.23.1 - '@shikijs/engine-oniguruma': 1.23.1 - '@shikijs/types': 1.23.1 - '@shikijs/vscode-textmate': 9.3.0 - '@types/hast': 3.0.4 - hast-util-to-html: 9.0.3 + '@shikijs/types': 2.5.0 - '@shikijs/engine-javascript@1.23.1': + '@shikijs/themes@3.8.0': dependencies: - '@shikijs/types': 1.23.1 - '@shikijs/vscode-textmate': 9.3.0 - oniguruma-to-es: 0.4.1 + '@shikijs/types': 3.8.0 - '@shikijs/engine-oniguruma@1.23.1': + '@shikijs/transformers@2.5.0': dependencies: - '@shikijs/types': 1.23.1 - '@shikijs/vscode-textmate': 9.3.0 + '@shikijs/core': 2.5.0 + '@shikijs/types': 2.5.0 - '@shikijs/transformers@1.23.1': + '@shikijs/types@2.5.0': dependencies: - shiki: 1.23.1 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 - '@shikijs/types@1.23.1': + '@shikijs/types@3.8.0': dependencies: - '@shikijs/vscode-textmate': 9.3.0 + '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 - '@shikijs/vscode-textmate@9.3.0': {} + '@shikijs/vscode-textmate@10.0.2': {} '@sindresorhus/merge-streams@2.3.0': {} @@ -4315,6 +5393,11 @@ snapshots: '@tootallnate/quickjs-emscripten@0.23.0': {} + '@tybys/wasm-util@0.10.0': + dependencies: + tslib: 2.8.1 + optional: true + '@types/argparse@1.0.38': {} '@types/chai@4.3.16': {} @@ -4323,15 +5406,17 @@ snapshots: '@types/estree@1.0.7': {} + '@types/estree@1.0.8': {} + '@types/hast@3.0.4': dependencies: '@types/unist': 3.0.3 '@types/jsdom@21.1.7': dependencies: - '@types/node': 20.17.7 + '@types/node': 20.19.8 '@types/tough-cookie': 4.0.5 - parse5: 7.2.1 + parse5: 7.3.0 '@types/linkify-it@5.0.0': {} @@ -4351,21 +5436,17 @@ snapshots: '@types/nightwatch@2.3.32': dependencies: '@types/chai': 4.3.16 - '@types/node': 20.17.7 + '@types/node': 20.19.8 '@types/selenium-webdriver': 4.1.23 devtools-protocol: 0.0.1025565 - '@types/node@20.17.31': - dependencies: - undici-types: 6.19.8 - - '@types/node@20.17.7': + '@types/node@20.19.8': dependencies: - undici-types: 6.19.8 + undici-types: 6.21.0 - '@types/node@22.15.2': + '@types/node@24.0.14': dependencies: - undici-types: 6.21.0 + undici-types: 7.8.0 optional: true '@types/normalize-package-data@2.4.1': {} @@ -4374,58 +5455,87 @@ snapshots: '@types/selenium-webdriver@4.1.23': dependencies: - '@types/node': 20.17.7 + '@types/node': 20.19.8 '@types/ws': 8.5.10 '@types/tough-cookie@4.0.5': {} '@types/unist@3.0.3': {} - '@types/web-bluetooth@0.0.20': {} + '@types/web-bluetooth@0.0.21': {} + + '@types/whatwg-mimetype@3.0.2': + optional: true '@types/ws@8.5.10': dependencies: - '@types/node': 20.17.7 + '@types/node': 20.19.8 '@types/yauzl@2.10.3': dependencies: - '@types/node': 22.15.2 + '@types/node': 24.0.14 optional: true - '@ungap/promise-all-settled@1.1.2': {} + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20250718.1': + optional: true - '@ungap/structured-clone@1.2.0': {} + '@typescript/native-preview-darwin-x64@7.0.0-dev.20250718.1': + optional: true - '@vitejs/plugin-vue@5.2.0(vite@5.4.11(@types/node@22.15.2)(terser@5.32.0))(vue@3.5.13(typescript@5.6.3))': - dependencies: - vite: 5.4.11(@types/node@22.15.2)(terser@5.32.0) - vue: 3.5.13(typescript@5.6.3) + '@typescript/native-preview-linux-arm64@7.0.0-dev.20250718.1': + optional: true + + '@typescript/native-preview-linux-arm@7.0.0-dev.20250718.1': + optional: true + + '@typescript/native-preview-linux-x64@7.0.0-dev.20250718.1': + optional: true - '@vitejs/plugin-vue@5.2.3(vite@5.4.18(@types/node@20.17.31)(terser@5.32.0))(vue@3.5.13(typescript@5.6.3))': + '@typescript/native-preview-win32-arm64@7.0.0-dev.20250718.1': + optional: true + + '@typescript/native-preview-win32-x64@7.0.0-dev.20250718.1': + optional: true + + '@typescript/native-preview@7.0.0-dev.20250718.1': + optionalDependencies: + '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20250718.1 + '@typescript/native-preview-darwin-x64': 7.0.0-dev.20250718.1 + '@typescript/native-preview-linux-arm': 7.0.0-dev.20250718.1 + '@typescript/native-preview-linux-arm64': 7.0.0-dev.20250718.1 + '@typescript/native-preview-linux-x64': 7.0.0-dev.20250718.1 + '@typescript/native-preview-win32-arm64': 7.0.0-dev.20250718.1 + '@typescript/native-preview-win32-x64': 7.0.0-dev.20250718.1 + + '@ungap/promise-all-settled@1.1.2': {} + + '@ungap/structured-clone@1.3.0': {} + + '@vitejs/plugin-vue@5.2.4(vite@5.4.19(@types/node@20.19.8)(terser@5.43.1))(vue@3.5.17(typescript@5.8.3))': dependencies: - vite: 5.4.18(@types/node@20.17.31)(terser@5.32.0) - vue: 3.5.13(typescript@5.6.3) + vite: 5.4.19(@types/node@20.19.8)(terser@5.43.1) + vue: 3.5.17(typescript@5.8.3) - '@vitejs/plugin-vue@5.2.3(vite@5.4.18(@types/node@22.15.2)(terser@5.32.0))(vue@3.5.13(typescript@5.6.3))': + '@vitejs/plugin-vue@5.2.4(vite@5.4.19(@types/node@24.0.14)(terser@5.43.1))(vue@3.5.17(typescript@5.8.3))': dependencies: - vite: 5.4.18(@types/node@22.15.2)(terser@5.32.0) - vue: 3.5.13(typescript@5.6.3) + vite: 5.4.19(@types/node@24.0.14)(terser@5.43.1) + vue: 3.5.17(typescript@5.8.3) - '@vitest/coverage-v8@2.1.9(vitest@2.1.9(@types/node@22.15.2)(@vitest/ui@2.1.9)(happy-dom@15.11.7)(jsdom@19.0.0)(terser@5.32.0))': + '@vitest/coverage-v8@2.1.9(vitest@2.1.9)': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 0.2.3 - debug: 4.3.7 + debug: 4.4.1 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 istanbul-lib-source-maps: 5.0.6 istanbul-reports: 3.1.7 - magic-string: 0.30.13 + magic-string: 0.30.17 magicast: 0.3.5 std-env: 3.8.0 test-exclude: 7.0.1 tinyrainbow: 1.2.0 - vitest: 2.1.9(@types/node@22.15.2)(@vitest/ui@2.1.9)(happy-dom@15.11.7)(jsdom@19.0.0)(terser@5.32.0) + vitest: 2.1.9(@types/node@24.0.14)(@vitest/ui@2.1.9)(happy-dom@18.0.1)(jsdom@26.1.0)(terser@5.43.1) transitivePeerDependencies: - supports-color @@ -4436,13 +5546,13 @@ snapshots: chai: 5.1.2 tinyrainbow: 1.2.0 - '@vitest/mocker@2.1.9(vite@5.4.18(@types/node@22.15.2)(terser@5.32.0))': + '@vitest/mocker@2.1.9(vite@5.4.19(@types/node@24.0.14)(terser@5.43.1))': dependencies: '@vitest/spy': 2.1.9 estree-walker: 3.0.3 - magic-string: 0.30.13 + magic-string: 0.30.17 optionalDependencies: - vite: 5.4.18(@types/node@22.15.2)(terser@5.32.0) + vite: 5.4.19(@types/node@24.0.14)(terser@5.43.1) '@vitest/pretty-format@2.1.9': dependencies: @@ -4456,7 +5566,7 @@ snapshots: '@vitest/snapshot@2.1.9': dependencies: '@vitest/pretty-format': 2.1.9 - magic-string: 0.30.13 + magic-string: 0.30.17 pathe: 1.1.2 '@vitest/spy@2.1.9': @@ -4472,7 +5582,7 @@ snapshots: sirv: 3.0.0 tinyglobby: 0.2.10 tinyrainbow: 1.2.0 - vitest: 2.1.9(@types/node@22.15.2)(@vitest/ui@2.1.9)(happy-dom@15.11.7)(jsdom@19.0.0)(terser@5.32.0) + vitest: 2.1.9(@types/node@24.0.14)(@vitest/ui@2.1.9)(happy-dom@18.0.1)(jsdom@26.1.0)(terser@5.43.1) '@vitest/utils@2.1.9': dependencies: @@ -4480,47 +5590,47 @@ snapshots: loupe: 3.1.3 tinyrainbow: 1.2.0 - '@volar/language-core@2.4.12': + '@volar/language-core@2.4.15': dependencies: - '@volar/source-map': 2.4.12 + '@volar/source-map': 2.4.15 - '@volar/source-map@2.4.12': {} + '@volar/source-map@2.4.15': {} - '@volar/typescript@2.4.12': + '@volar/typescript@2.4.15': dependencies: - '@volar/language-core': 2.4.12 + '@volar/language-core': 2.4.15 path-browserify: 1.0.1 vscode-uri: 3.1.0 - '@vue/compiler-core@3.5.13': + '@vue/compiler-core@3.5.17': dependencies: - '@babel/parser': 7.26.2 - '@vue/shared': 3.5.13 + '@babel/parser': 7.28.0 + '@vue/shared': 3.5.17 entities: 4.5.0 estree-walker: 2.0.2 source-map-js: 1.2.1 - '@vue/compiler-dom@3.5.13': + '@vue/compiler-dom@3.5.17': dependencies: - '@vue/compiler-core': 3.5.13 - '@vue/shared': 3.5.13 + '@vue/compiler-core': 3.5.17 + '@vue/shared': 3.5.17 - '@vue/compiler-sfc@3.5.13': + '@vue/compiler-sfc@3.5.17': dependencies: - '@babel/parser': 7.26.2 - '@vue/compiler-core': 3.5.13 - '@vue/compiler-dom': 3.5.13 - '@vue/compiler-ssr': 3.5.13 - '@vue/shared': 3.5.13 + '@babel/parser': 7.28.0 + '@vue/compiler-core': 3.5.17 + '@vue/compiler-dom': 3.5.17 + '@vue/compiler-ssr': 3.5.17 + '@vue/shared': 3.5.17 estree-walker: 2.0.2 - magic-string: 0.30.13 - postcss: 8.4.49 + magic-string: 0.30.17 + postcss: 8.5.6 source-map-js: 1.2.1 - '@vue/compiler-ssr@3.5.13': + '@vue/compiler-ssr@3.5.17': dependencies: - '@vue/compiler-dom': 3.5.13 - '@vue/shared': 3.5.13 + '@vue/compiler-dom': 3.5.17 + '@vue/shared': 3.5.17 '@vue/compiler-vue2@2.7.16': dependencies: @@ -4529,110 +5639,108 @@ snapshots: '@vue/devtools-api@6.6.4': {} - '@vue/devtools-api@7.6.4': + '@vue/devtools-api@7.7.7': dependencies: - '@vue/devtools-kit': 7.6.4 + '@vue/devtools-kit': 7.7.7 - '@vue/devtools-kit@7.6.4': + '@vue/devtools-kit@7.7.7': dependencies: - '@vue/devtools-shared': 7.6.4 - birpc: 0.2.19 + '@vue/devtools-shared': 7.7.7 + birpc: 2.5.0 hookable: 5.5.3 mitt: 3.0.1 perfect-debounce: 1.0.0 speakingurl: 14.0.1 - superjson: 2.2.1 + superjson: 2.2.2 - '@vue/devtools-shared@7.6.4': + '@vue/devtools-shared@7.7.7': dependencies: rfdc: 1.4.1 - '@vue/language-core@2.2.10(typescript@5.6.3)': + '@vue/language-core@2.2.12(typescript@5.8.3)': dependencies: - '@volar/language-core': 2.4.12 - '@vue/compiler-dom': 3.5.13 + '@volar/language-core': 2.4.15 + '@vue/compiler-dom': 3.5.17 '@vue/compiler-vue2': 2.7.16 - '@vue/shared': 3.5.13 + '@vue/shared': 3.5.17 alien-signals: 1.0.13 minimatch: 9.0.5 muggle-string: 0.4.1 path-browserify: 1.0.1 optionalDependencies: - typescript: 5.6.3 + typescript: 5.8.3 - '@vue/reactivity@3.5.13': + '@vue/reactivity@3.5.17': dependencies: - '@vue/shared': 3.5.13 + '@vue/shared': 3.5.17 - '@vue/runtime-core@3.5.13': + '@vue/runtime-core@3.5.17': dependencies: - '@vue/reactivity': 3.5.13 - '@vue/shared': 3.5.13 + '@vue/reactivity': 3.5.17 + '@vue/shared': 3.5.17 - '@vue/runtime-dom@3.5.13': + '@vue/runtime-dom@3.5.17': dependencies: - '@vue/reactivity': 3.5.13 - '@vue/runtime-core': 3.5.13 - '@vue/shared': 3.5.13 + '@vue/reactivity': 3.5.17 + '@vue/runtime-core': 3.5.17 + '@vue/shared': 3.5.17 csstype: 3.1.3 - '@vue/server-renderer@3.5.13(vue@3.5.13(typescript@5.6.3))': + '@vue/server-renderer@3.5.17(vue@3.5.17(typescript@5.8.3))': dependencies: - '@vue/compiler-ssr': 3.5.13 - '@vue/shared': 3.5.13 - vue: 3.5.13(typescript@5.6.3) + '@vue/compiler-ssr': 3.5.17 + '@vue/shared': 3.5.17 + vue: 3.5.17(typescript@5.8.3) - '@vue/shared@3.5.13': {} + '@vue/shared@3.5.17': {} '@vue/test-utils@2.4.6': dependencies: js-beautify: 1.15.1 vue-component-type-helpers: 2.0.21 - '@vue/tsconfig@0.6.0(typescript@5.6.3)(vue@3.5.13(typescript@5.6.3))': + '@vue/tsconfig@0.7.0(typescript@5.8.3)(vue@3.5.17(typescript@5.8.3))': optionalDependencies: - typescript: 5.6.3 - vue: 3.5.13(typescript@5.6.3) + typescript: 5.8.3 + vue: 3.5.17(typescript@5.8.3) - '@vueuse/core@11.3.0(vue@3.5.13(typescript@5.6.3))': + '@vueuse/core@12.8.2(typescript@5.8.3)': dependencies: - '@types/web-bluetooth': 0.0.20 - '@vueuse/metadata': 11.3.0 - '@vueuse/shared': 11.3.0(vue@3.5.13(typescript@5.6.3)) - vue-demi: 0.14.10(vue@3.5.13(typescript@5.6.3)) + '@types/web-bluetooth': 0.0.21 + '@vueuse/metadata': 12.8.2 + '@vueuse/shared': 12.8.2(typescript@5.8.3) + vue: 3.5.17(typescript@5.8.3) transitivePeerDependencies: - - '@vue/composition-api' - - vue + - typescript - '@vueuse/integrations@11.3.0(axios@1.7.7)(focus-trap@7.6.2)(vue@3.5.13(typescript@5.6.3))': + '@vueuse/integrations@12.8.2(axios@1.10.0)(focus-trap@7.6.5)(typescript@5.8.3)': dependencies: - '@vueuse/core': 11.3.0(vue@3.5.13(typescript@5.6.3)) - '@vueuse/shared': 11.3.0(vue@3.5.13(typescript@5.6.3)) - vue-demi: 0.14.10(vue@3.5.13(typescript@5.6.3)) + '@vueuse/core': 12.8.2(typescript@5.8.3) + '@vueuse/shared': 12.8.2(typescript@5.8.3) + vue: 3.5.17(typescript@5.8.3) optionalDependencies: - axios: 1.7.7 - focus-trap: 7.6.2 + axios: 1.10.0 + focus-trap: 7.6.5 transitivePeerDependencies: - - '@vue/composition-api' - - vue + - typescript - '@vueuse/metadata@11.3.0': {} + '@vueuse/metadata@12.8.2': {} - '@vueuse/shared@11.3.0(vue@3.5.13(typescript@5.6.3))': + '@vueuse/shared@12.8.2(typescript@5.8.3)': dependencies: - vue-demi: 0.14.10(vue@3.5.13(typescript@5.6.3)) + vue: 3.5.17(typescript@5.8.3) transitivePeerDependencies: - - '@vue/composition-api' - - vue + - typescript - '@wdio/logger@9.0.8': + '@wdio/logger@9.18.0': dependencies: chalk: 5.4.1 loglevel: 1.9.2 loglevel-plugin-prefix: 0.8.4 + safe-regex2: 5.0.0 strip-ansi: 7.1.0 - '@zip.js/zip.js@2.7.52': {} + '@zip.js/zip.js@2.7.64': {} JSONStream@1.3.5: dependencies: @@ -4652,21 +5760,17 @@ snapshots: acorn@7.4.1: {} - acorn@8.14.0: {} + acorn@8.15.0: {} add-stream@1.0.0: {} agent-base@6.0.2: dependencies: - debug: 4.3.7 + debug: 4.4.1 transitivePeerDependencies: - supports-color - agent-base@7.1.1: - dependencies: - debug: 4.3.7 - transitivePeerDependencies: - - supports-color + agent-base@7.1.4: {} ajv-draft-04@1.0.0(ajv@8.13.0): optionalDependencies: @@ -4690,21 +5794,21 @@ snapshots: require-from-string: 2.0.2 uri-js: 4.4.1 - algoliasearch@5.15.0: - dependencies: - '@algolia/client-abtesting': 5.15.0 - '@algolia/client-analytics': 5.15.0 - '@algolia/client-common': 5.15.0 - '@algolia/client-insights': 5.15.0 - '@algolia/client-personalization': 5.15.0 - '@algolia/client-query-suggestions': 5.15.0 - '@algolia/client-search': 5.15.0 - '@algolia/ingestion': 1.15.0 - '@algolia/monitoring': 1.15.0 - '@algolia/recommend': 5.15.0 - '@algolia/requester-browser-xhr': 5.15.0 - '@algolia/requester-fetch': 5.15.0 - '@algolia/requester-node-http': 5.15.0 + algoliasearch@5.34.0: + dependencies: + '@algolia/client-abtesting': 5.34.0 + '@algolia/client-analytics': 5.34.0 + '@algolia/client-common': 5.34.0 + '@algolia/client-insights': 5.34.0 + '@algolia/client-personalization': 5.34.0 + '@algolia/client-query-suggestions': 5.34.0 + '@algolia/client-search': 5.34.0 + '@algolia/ingestion': 1.34.0 + '@algolia/monitoring': 1.34.0 + '@algolia/recommend': 5.34.0 + '@algolia/requester-browser-xhr': 5.34.0 + '@algolia/requester-fetch': 5.34.0 + '@algolia/requester-node-http': 5.34.0 alien-signals@1.0.13: {} @@ -4738,6 +5842,10 @@ snapshots: dependencies: entities: 2.2.0 + ansis@4.1.0: {} + + any-promise@1.3.0: {} + anymatch@3.1.3: dependencies: normalize-path: 3.0.0 @@ -4757,6 +5865,11 @@ snapshots: assertion-error@2.0.1: {} + ast-kit@2.1.1: + dependencies: + '@babel/parser': 7.28.0 + pathe: 2.0.3 + ast-types@0.13.4: dependencies: tslib: 2.8.1 @@ -4767,40 +5880,41 @@ snapshots: axe-core@4.7.2: {} - axios@1.7.7: + axios@1.10.0: dependencies: follow-redirects: 1.15.9 - form-data: 4.0.1 + form-data: 4.0.4 proxy-from-env: 1.1.0 transitivePeerDependencies: - debug - b4a@1.6.6: {} + b4a@1.6.7: {} balanced-match@1.0.2: {} - bare-events@2.4.2: + bare-events@2.6.0: optional: true - bare-fs@2.3.3: + bare-fs@4.1.6: dependencies: - bare-events: 2.4.2 - bare-path: 2.1.3 - bare-stream: 2.2.1 + bare-events: 2.6.0 + bare-path: 3.0.0 + bare-stream: 2.6.5(bare-events@2.6.0) optional: true - bare-os@2.4.2: + bare-os@3.6.1: optional: true - bare-path@2.1.3: + bare-path@3.0.0: dependencies: - bare-os: 2.4.2 + bare-os: 3.6.1 optional: true - bare-stream@2.2.1: + bare-stream@2.6.5(bare-events@2.6.0): dependencies: - b4a: 1.6.6 - streamx: 2.20.0 + streamx: 2.22.1 + optionalDependencies: + bare-events: 2.6.0 optional: true base64-js@1.5.1: {} @@ -4809,7 +5923,7 @@ snapshots: binary-extensions@2.2.0: {} - birpc@0.2.19: {} + birpc@2.5.0: {} bl@4.1.0: dependencies: @@ -4868,8 +5982,18 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + bundle-require@5.1.0(esbuild@0.25.6): + dependencies: + esbuild: 0.25.6 + load-tsconfig: 0.2.5 + cac@6.7.14: {} + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + camelcase-keys@6.2.2: dependencies: camelcase: 5.3.1 @@ -4927,13 +6051,17 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - chromedriver@131.0.5: + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + chromedriver@138.0.3: dependencies: '@testim/chrome-version': 1.1.4 - axios: 1.7.7 + axios: 1.10.0 compare-versions: 6.1.1 extract-zip: 2.0.1 - proxy-agent: 6.4.0 + proxy-agent: 6.5.0 proxy-from-env: 1.1.0 tcp-port-used: 1.0.2 transitivePeerDependencies: @@ -4999,6 +6127,8 @@ snapshots: commander@2.20.3: {} + commander@4.1.1: {} + commondir@1.0.1: {} compare-func@2.0.0: @@ -5010,6 +6140,8 @@ snapshots: concat-map@0.0.1: {} + confbox@0.1.8: {} + config-chain@1.1.13: dependencies: ini: 1.3.8 @@ -5017,6 +6149,8 @@ snapshots: connect-history-api-fallback@1.6.0: {} + consola@3.4.2: {} + conventional-changelog-angular@5.0.13: dependencies: compare-func: 2.0.0 @@ -5130,7 +6264,7 @@ snapshots: core-util-is@1.0.3: {} - cross-spawn@7.0.3: + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 shebang-command: 2.0.0 @@ -5144,6 +6278,12 @@ snapshots: dependencies: cssom: 0.3.8 + cssstyle@4.6.0: + dependencies: + '@asamuzakjp/css-color': 3.2.0 + rrweb-cssom: 0.8.0 + optional: true + csstype@3.1.3: {} dargs@7.0.0: {} @@ -5158,6 +6298,12 @@ snapshots: whatwg-mimetype: 3.0.0 whatwg-url: 11.0.0 + data-urls@5.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + optional: true + dateformat@3.0.3: {} de-indent@1.0.2: {} @@ -5172,11 +6318,7 @@ snapshots: optionalDependencies: supports-color: 8.1.1 - debug@4.3.7: - dependencies: - ms: 2.1.3 - - debug@4.4.0: + debug@4.4.1: dependencies: ms: 2.1.3 @@ -5191,7 +6333,7 @@ snapshots: decamelize@6.0.0: {} - decimal.js@10.4.3: {} + decimal.js@10.6.0: {} deep-eql@4.0.1: dependencies: @@ -5209,6 +6351,8 @@ snapshots: define-lazy-prop@2.0.0: {} + defu@6.1.4: {} + degenerator@5.0.1: dependencies: ast-types: 0.13.4 @@ -5229,6 +6373,8 @@ snapshots: diff@5.0.0: {} + diff@8.0.2: {} + domexception@4.0.0: dependencies: webidl-conversions: 7.0.0 @@ -5241,6 +6387,14 @@ snapshots: dotenv@16.5.0: {} + dts-resolver@2.1.1: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + duplexer@0.1.2: {} eastasianwidth@0.2.0: {} @@ -5264,7 +6418,9 @@ snapshots: emoji-regex@9.2.2: {} - end-of-stream@1.4.4: + empathic@2.0.0: {} + + end-of-stream@1.4.5: dependencies: once: 1.4.0 @@ -5277,6 +6433,8 @@ snapshots: entities@4.5.0: {} + entities@6.0.1: {} + envinfo@7.8.1: {} environment@1.1.0: {} @@ -5285,8 +6443,23 @@ snapshots: dependencies: is-arrayish: 0.2.1 + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + es-module-lexer@1.7.0: {} + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + esbuild@0.21.5: optionalDependencies: '@esbuild/aix-ppc64': 0.21.5 @@ -5313,6 +6486,35 @@ snapshots: '@esbuild/win32-ia32': 0.21.5 '@esbuild/win32-x64': 0.21.5 + esbuild@0.25.6: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.6 + '@esbuild/android-arm': 0.25.6 + '@esbuild/android-arm64': 0.25.6 + '@esbuild/android-x64': 0.25.6 + '@esbuild/darwin-arm64': 0.25.6 + '@esbuild/darwin-x64': 0.25.6 + '@esbuild/freebsd-arm64': 0.25.6 + '@esbuild/freebsd-x64': 0.25.6 + '@esbuild/linux-arm': 0.25.6 + '@esbuild/linux-arm64': 0.25.6 + '@esbuild/linux-ia32': 0.25.6 + '@esbuild/linux-loong64': 0.25.6 + '@esbuild/linux-mips64el': 0.25.6 + '@esbuild/linux-ppc64': 0.25.6 + '@esbuild/linux-riscv64': 0.25.6 + '@esbuild/linux-s390x': 0.25.6 + '@esbuild/linux-x64': 0.25.6 + '@esbuild/netbsd-arm64': 0.25.6 + '@esbuild/netbsd-x64': 0.25.6 + '@esbuild/openbsd-arm64': 0.25.6 + '@esbuild/openbsd-x64': 0.25.6 + '@esbuild/openharmony-arm64': 0.25.6 + '@esbuild/sunos-x64': 0.25.6 + '@esbuild/win32-arm64': 0.25.6 + '@esbuild/win32-ia32': 0.25.6 + '@esbuild/win32-x64': 0.25.6 + escalade@3.1.1: {} escape-string-regexp@1.0.5: {} @@ -5353,7 +6555,7 @@ snapshots: execa@8.0.1: dependencies: - cross-spawn: 7.0.3 + cross-spawn: 7.0.6 get-stream: 8.0.1 human-signals: 5.0.0 is-stream: 3.0.0 @@ -5363,17 +6565,17 @@ snapshots: signal-exit: 4.1.0 strip-final-newline: 3.0.0 - execa@9.5.2: + execa@9.6.0: dependencies: '@sindresorhus/merge-streams': 4.0.0 - cross-spawn: 7.0.3 + cross-spawn: 7.0.6 figures: 6.1.0 get-stream: 9.0.1 - human-signals: 8.0.0 + human-signals: 8.0.1 is-plain-obj: 4.1.0 is-stream: 4.0.1 npm-run-path: 6.0.0 - pretty-ms: 9.1.0 + pretty-ms: 9.2.0 signal-exit: 4.1.0 strip-final-newline: 4.0.0 yoctocolors: 2.1.1 @@ -5382,7 +6584,7 @@ snapshots: extract-zip@2.0.1: dependencies: - debug: 4.3.7 + debug: 4.4.1 get-stream: 5.2.0 yauzl: 2.10.0 optionalDependencies: @@ -5416,6 +6618,10 @@ snapshots: optionalDependencies: picomatch: 4.0.2 + fdir@6.4.6(picomatch@4.0.2): + optionalDependencies: + picomatch: 4.0.2 + fetch-blob@3.2.0: dependencies: node-domexception: 1.0.0 @@ -5455,11 +6661,17 @@ snapshots: locate-path: 6.0.0 path-exists: 4.0.0 + fix-dts-default-cjs-exports@1.0.1: + dependencies: + magic-string: 0.30.17 + mlly: 1.7.4 + rollup: 4.45.1 + flat@5.0.2: {} flatted@3.3.2: {} - focus-trap@7.6.2: + focus-trap@7.6.5: dependencies: tabbable: 6.2.0 @@ -5467,13 +6679,15 @@ snapshots: foreground-child@3.3.0: dependencies: - cross-spawn: 7.0.3 + cross-spawn: 7.0.6 signal-exit: 4.1.0 - form-data@4.0.1: + form-data@4.0.4: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 mime-types: 2.1.35 formdata-polyfill@4.0.10: @@ -5488,18 +6702,12 @@ snapshots: jsonfile: 6.1.0 universalify: 2.0.1 - fs-extra@11.2.0: + fs-extra@11.3.0: dependencies: graceful-fs: 4.2.11 jsonfile: 6.1.0 universalify: 2.0.1 - fs-extra@7.0.1: - dependencies: - graceful-fs: 4.2.11 - jsonfile: 4.0.0 - universalify: 0.1.2 - fs.realpath@1.0.0: {} fsevents@2.3.3: @@ -5507,17 +6715,18 @@ snapshots: function-bind@1.1.2: {} - geckodriver@4.5.1: + geckodriver@5.0.0: dependencies: - '@wdio/logger': 9.0.8 - '@zip.js/zip.js': 2.7.52 + '@wdio/logger': 9.18.0 + '@zip.js/zip.js': 2.7.64 decamelize: 6.0.0 http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.5 + https-proxy-agent: 7.0.6 node-fetch: 3.3.2 - tar-fs: 3.0.6 - which: 4.0.0 + tar-fs: 3.1.0 + which: 5.0.0 transitivePeerDependencies: + - bare-buffer - supports-color get-caller-file@2.0.5: {} @@ -5526,6 +6735,19 @@ snapshots: get-func-name@2.0.2: {} + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + get-pkg-repo@4.2.1: dependencies: '@hutson/parse-repository-url': 3.0.2 @@ -5533,9 +6755,14 @@ snapshots: through2: 2.0.5 yargs: 16.2.0 + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + get-stream@5.2.0: dependencies: - pump: 3.0.2 + pump: 3.0.3 get-stream@8.0.1: {} @@ -5544,12 +6771,15 @@ snapshots: '@sec-ant/readable-stream': 0.4.1 is-stream: 4.0.1 - get-uri@6.0.3: + get-tsconfig@4.10.1: + dependencies: + resolve-pkg-maps: 1.0.0 + + get-uri@6.0.5: dependencies: basic-ftp: 5.0.5 data-uri-to-buffer: 6.0.2 - debug: 4.3.7 - fs-extra: 11.2.0 + debug: 4.4.1 transitivePeerDependencies: - supports-color @@ -5632,6 +6862,8 @@ snapshots: slash: 5.1.0 unicorn-magic: 0.3.0 + gopd@1.2.0: {} + graceful-fs@4.2.11: {} growl@1.10.5: {} @@ -5651,17 +6883,30 @@ snapshots: webidl-conversions: 7.0.0 whatwg-mimetype: 3.0.0 + happy-dom@18.0.1: + dependencies: + '@types/node': 20.19.8 + '@types/whatwg-mimetype': 3.0.2 + whatwg-mimetype: 3.0.0 + optional: true + hard-rejection@2.1.0: {} has-flag@3.0.0: {} has-flag@4.0.0: {} - hasown@2.0.0: + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: dependencies: function-bind: 1.1.2 - hast-util-to-html@9.0.3: + hast-util-to-html@9.0.5: dependencies: '@types/hast': 3.0.4 '@types/unist': 3.0.3 @@ -5670,7 +6915,7 @@ snapshots: hast-util-whitespace: 3.0.0 html-void-elements: 3.0.0 mdast-util-to-hast: 13.2.0 - property-information: 6.5.0 + property-information: 7.1.0 space-separated-tokens: 2.0.2 stringify-entities: 4.0.4 zwitch: 2.0.4 @@ -5693,6 +6938,11 @@ snapshots: dependencies: whatwg-encoding: 2.0.0 + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + optional: true + html-escaper@2.0.2: {} html-void-elements@3.0.0: {} @@ -5701,34 +6951,34 @@ snapshots: dependencies: '@tootallnate/once': 2.0.0 agent-base: 6.0.2 - debug: 4.4.0 + debug: 4.4.1 transitivePeerDependencies: - supports-color http-proxy-agent@7.0.2: dependencies: - agent-base: 7.1.1 - debug: 4.3.7 + agent-base: 7.1.4 + debug: 4.4.1 transitivePeerDependencies: - supports-color https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 - debug: 4.3.7 + debug: 4.4.1 transitivePeerDependencies: - supports-color - https-proxy-agent@7.0.5: + https-proxy-agent@7.0.6: dependencies: - agent-base: 7.1.1 - debug: 4.3.7 + agent-base: 7.1.4 + debug: 4.4.1 transitivePeerDependencies: - supports-color human-signals@5.0.0: {} - human-signals@8.0.0: {} + human-signals@8.0.1: {} iconv-lite@0.6.3: dependencies: @@ -5768,7 +7018,7 @@ snapshots: is-core-module@2.13.1: dependencies: - hasown: 2.0.0 + hasown: 2.0.2 is-docker@2.2.1: {} @@ -5804,7 +7054,7 @@ snapshots: is-reference@1.2.1: dependencies: - '@types/estree': 1.0.6 + '@types/estree': 1.0.7 is-running@2.1.0: {} @@ -5850,8 +7100,8 @@ snapshots: istanbul-lib-source-maps@5.0.6: dependencies: - '@jridgewell/trace-mapping': 0.3.25 - debug: 4.3.7 + '@jridgewell/trace-mapping': 0.3.29 + debug: 4.4.1 istanbul-lib-coverage: 3.2.2 transitivePeerDependencies: - supports-color @@ -5880,8 +7130,12 @@ snapshots: filelist: 1.0.4 minimatch: 3.1.2 + jiti@2.4.2: {} + jju@1.4.0: {} + joycon@3.1.1: {} + js-beautify@1.15.1: dependencies: config-chain: 1.1.13 @@ -5903,20 +7157,20 @@ snapshots: jsdom@19.0.0: dependencies: abab: 2.0.6 - acorn: 8.14.0 + acorn: 8.15.0 acorn-globals: 6.0.0 cssom: 0.5.0 cssstyle: 2.3.0 data-urls: 3.0.2 - decimal.js: 10.4.3 + decimal.js: 10.6.0 domexception: 4.0.0 escodegen: 2.1.0 - form-data: 4.0.1 + form-data: 4.0.4 html-encoding-sniffer: 3.0.0 http-proxy-agent: 5.0.0 https-proxy-agent: 5.0.1 is-potential-custom-element-name: 1.0.1 - nwsapi: 2.2.13 + nwsapi: 2.2.20 parse5: 6.0.1 saxes: 5.0.1 symbol-tree: 3.2.4 @@ -5927,13 +7181,43 @@ snapshots: whatwg-encoding: 2.0.0 whatwg-mimetype: 3.0.0 whatwg-url: 10.0.0 - ws: 8.18.0 + ws: 8.18.3 xml-name-validator: 4.0.0 transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate + jsdom@26.1.0: + dependencies: + cssstyle: 4.6.0 + data-urls: 5.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.20 + parse5: 7.3.0 + rrweb-cssom: 0.8.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 5.1.2 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + ws: 8.18.3 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + optional: true + + jsesc@3.1.0: {} + json-parse-better-errors@1.0.2: {} json-parse-even-better-errors@2.3.1: {} @@ -5942,10 +7226,6 @@ snapshots: json-stringify-safe@5.0.1: {} - jsonfile@4.0.0: - optionalDependencies: - graceful-fs: 4.2.11 - jsonfile@6.1.0: dependencies: universalify: 2.0.1 @@ -5979,14 +7259,14 @@ snapshots: dependencies: chalk: 5.4.1 commander: 13.1.0 - debug: 4.4.0 + debug: 4.4.1 execa: 8.0.1 lilconfig: 3.1.3 listr2: 8.3.2 micromatch: 4.0.8 pidtree: 0.6.0 string-argv: 0.3.2 - yaml: 2.7.1 + yaml: 2.8.0 transitivePeerDependencies: - supports-color @@ -6006,6 +7286,8 @@ snapshots: pify: 3.0.0 strip-bom: 3.0.0 + load-tsconfig@0.2.5: {} + locate-path@2.0.0: dependencies: p-locate: 2.0.0 @@ -6073,6 +7355,8 @@ snapshots: lodash.pick@4.4.0: {} + lodash.sortby@4.7.0: {} + lodash@4.17.21: {} log-symbols@4.1.0: @@ -6110,14 +7394,14 @@ snapshots: lunr@2.3.9: {} - magic-string@0.30.13: + magic-string@0.30.17: dependencies: - '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/sourcemap-codec': 1.5.4 magicast@0.3.5: dependencies: - '@babel/parser': 7.26.2 - '@babel/types': 7.26.0 + '@babel/parser': 7.28.0 + '@babel/types': 7.28.1 source-map-js: 1.2.1 make-dir@3.1.0: @@ -6145,11 +7429,13 @@ snapshots: punycode.js: 2.3.1 uc.micro: 2.1.0 + math-intrinsics@1.1.0: {} + mdast-util-to-hast@13.2.0: dependencies: '@types/hast': 3.0.4 '@types/mdast': 4.0.4 - '@ungap/structured-clone': 1.2.0 + '@ungap/structured-clone': 1.3.0 devlop: 1.1.0 micromark-util-sanitize-uri: 2.0.1 trim-lines: 3.0.1 @@ -6180,7 +7466,7 @@ snapshots: micromark-util-character@2.1.1: dependencies: micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.1 + micromark-util-types: 2.0.2 micromark-util-encode@2.0.1: {} @@ -6192,7 +7478,7 @@ snapshots: micromark-util-symbol@2.0.1: {} - micromark-util-types@2.0.1: {} + micromark-util-types@2.0.2: {} micromatch@4.0.8: dependencies: @@ -6253,10 +7539,17 @@ snapshots: minipass@7.1.2: {} - minisearch@7.1.1: {} + minisearch@7.1.2: {} mitt@3.0.1: {} + mlly@1.7.4: + dependencies: + acorn: 8.15.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.1 + mocha@9.2.2: dependencies: '@ungap/promise-all-settled': 1.1.2 @@ -6294,9 +7587,15 @@ snapshots: muggle-string@0.4.1: {} + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + nanoid@3.3.1: {} - nanoid@3.3.7: {} + nanoid@3.3.11: {} neo-async@2.6.2: {} @@ -6308,7 +7607,7 @@ snapshots: nightwatch-helpers@1.2.0: {} - nightwatch@2.6.25(chromedriver@131.0.5)(geckodriver@4.5.1): + nightwatch@2.6.25(chromedriver@138.0.3)(geckodriver@5.0.0): dependencies: '@nightwatch/chai': 5.0.2 '@nightwatch/html-reporter-template': 0.2.1 @@ -6343,8 +7642,8 @@ snapshots: untildify: 4.0.0 uuid: 8.3.2 optionalDependencies: - chromedriver: 131.0.5 - geckodriver: 4.5.1 + chromedriver: 138.0.3 + geckodriver: 5.0.0 transitivePeerDependencies: - bufferutil - canvas @@ -6388,7 +7687,9 @@ snapshots: path-key: 4.0.0 unicorn-magic: 0.3.0 - nwsapi@2.2.13: {} + nwsapi@2.2.20: {} + + object-assign@4.1.1: {} once@1.4.0: dependencies: @@ -6406,11 +7707,11 @@ snapshots: dependencies: mimic-function: 5.0.1 - oniguruma-to-es@0.4.1: + oniguruma-to-es@3.1.1: dependencies: emoji-regex-xs: 1.0.0 - regex: 5.0.2 - regex-recursion: 4.2.1 + regex: 6.0.1 + regex-recursion: 6.0.2 open@8.4.0: dependencies: @@ -6460,16 +7761,16 @@ snapshots: p-try@2.2.0: {} - pac-proxy-agent@7.0.2: + pac-proxy-agent@7.2.0: dependencies: '@tootallnate/quickjs-emscripten': 0.23.0 - agent-base: 7.1.1 - debug: 4.3.7 - get-uri: 6.0.3 + agent-base: 7.1.4 + debug: 4.4.1 + get-uri: 6.0.5 http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.5 + https-proxy-agent: 7.0.6 pac-resolver: 7.0.1 - socks-proxy-agent: 8.0.4 + socks-proxy-agent: 8.0.5 transitivePeerDependencies: - supports-color @@ -6498,9 +7799,9 @@ snapshots: parse5@6.0.1: {} - parse5@7.2.1: + parse5@7.3.0: dependencies: - entities: 4.5.0 + entities: 6.0.1 path-browserify@1.0.1: {} @@ -6534,6 +7835,8 @@ snapshots: pathe@1.1.2: {} + pathe@2.0.3: {} + pathval@1.1.1: {} pathval@2.0.0: {} @@ -6558,40 +7861,56 @@ snapshots: pify@3.0.0: {} + pirates@4.0.7: {} + pkg-dir@4.2.0: dependencies: find-up: 4.1.0 - postcss@8.4.49: + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.7.4 + pathe: 2.0.3 + + postcss-load-config@6.0.1(jiti@2.4.2)(postcss@8.5.6)(yaml@2.8.0): dependencies: - nanoid: 3.3.7 + lilconfig: 3.1.3 + optionalDependencies: + jiti: 2.4.2 + postcss: 8.5.6 + yaml: 2.8.0 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 picocolors: 1.1.1 source-map-js: 1.2.1 - preact@10.25.0: {} + preact@10.26.9: {} prettier@3.5.3: {} - pretty-ms@9.1.0: + pretty-ms@9.2.0: dependencies: parse-ms: 4.0.0 process-nextick-args@2.0.1: {} - property-information@6.5.0: {} + property-information@7.1.0: {} proto-list@1.2.4: {} - proxy-agent@6.4.0: + proxy-agent@6.5.0: dependencies: - agent-base: 7.1.1 - debug: 4.3.7 + agent-base: 7.1.4 + debug: 4.4.1 http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.5 + https-proxy-agent: 7.0.6 lru-cache: 7.18.3 - pac-proxy-agent: 7.0.2 + pac-proxy-agent: 7.2.0 proxy-from-env: 1.1.0 - socks-proxy-agent: 8.0.4 + socks-proxy-agent: 8.0.5 transitivePeerDependencies: - supports-color @@ -6605,9 +7924,9 @@ snapshots: dependencies: punycode: 2.3.1 - pump@3.0.2: + pump@3.0.3: dependencies: - end-of-stream: 1.4.4 + end-of-stream: 1.4.5 once: 1.4.0 punycode.js@2.3.1: {} @@ -6616,12 +7935,12 @@ snapshots: q@1.5.1: {} + quansync@0.2.10: {} + querystringify@2.2.0: {} queue-microtask@1.2.3: {} - queue-tick@1.0.1: {} - quick-lru@4.0.1: {} randombytes@2.1.0: @@ -6672,18 +7991,20 @@ snapshots: dependencies: picomatch: 2.3.1 + readdirp@4.1.2: {} + redent@3.0.0: dependencies: indent-string: 4.0.0 strip-indent: 3.0.0 - regex-recursion@4.2.1: + regex-recursion@6.0.2: dependencies: regex-utilities: 2.3.0 regex-utilities@2.3.0: {} - regex@5.0.2: + regex@6.0.1: dependencies: regex-utilities: 2.3.0 @@ -6693,6 +8014,10 @@ snapshots: requires-port@1.0.0: {} + resolve-from@5.0.0: {} + + resolve-pkg-maps@1.0.0: {} + resolve@1.22.8: dependencies: is-core-module: 2.13.1 @@ -6709,6 +8034,8 @@ snapshots: onetime: 7.0.0 signal-exit: 4.1.0 + ret@0.5.0: {} + reusify@1.1.0: {} rfdc@1.4.1: {} @@ -6726,17 +8053,58 @@ snapshots: glob: 11.0.0 package-json-from-dist: 1.0.0 + rolldown-plugin-dts@0.13.14(@typescript/native-preview@7.0.0-dev.20250718.1)(rolldown@1.0.0-beta.28)(typescript@5.8.3)(vue-tsc@2.2.12(typescript@5.8.3)): + dependencies: + '@babel/generator': 7.28.0 + '@babel/parser': 7.28.0 + '@babel/types': 7.28.1 + ast-kit: 2.1.1 + birpc: 2.5.0 + debug: 4.4.1 + dts-resolver: 2.1.1 + get-tsconfig: 4.10.1 + rolldown: 1.0.0-beta.28 + optionalDependencies: + '@typescript/native-preview': 7.0.0-dev.20250718.1 + typescript: 5.8.3 + vue-tsc: 2.2.12(typescript@5.8.3) + transitivePeerDependencies: + - oxc-resolver + - supports-color + + rolldown@1.0.0-beta.28: + dependencies: + '@oxc-project/runtime': 0.77.2 + '@oxc-project/types': 0.77.2 + '@rolldown/pluginutils': 1.0.0-beta.28 + ansis: 4.1.0 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-beta.28 + '@rolldown/binding-darwin-arm64': 1.0.0-beta.28 + '@rolldown/binding-darwin-x64': 1.0.0-beta.28 + '@rolldown/binding-freebsd-x64': 1.0.0-beta.28 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.28 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.28 + '@rolldown/binding-linux-arm64-musl': 1.0.0-beta.28 + '@rolldown/binding-linux-arm64-ohos': 1.0.0-beta.28 + '@rolldown/binding-linux-x64-gnu': 1.0.0-beta.28 + '@rolldown/binding-linux-x64-musl': 1.0.0-beta.28 + '@rolldown/binding-wasm32-wasi': 1.0.0-beta.28 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.28 + '@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.28 + '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.28 + rollup-plugin-analyzer@4.0.0: {} - rollup-plugin-typescript2@0.36.0(rollup@3.29.5)(typescript@5.6.3): + rollup-plugin-typescript2@0.36.0(rollup@3.29.5)(typescript@5.8.3): dependencies: '@rollup/pluginutils': 4.2.1 find-cache-dir: 3.3.2 fs-extra: 10.1.0 rollup: 3.29.5 - semver: 7.6.3 + semver: 7.7.1 tslib: 2.8.1 - typescript: 5.6.3 + typescript: 5.8.3 rollup@3.29.5: optionalDependencies: @@ -6766,6 +8134,35 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.27.4 fsevents: 2.3.3 + rollup@4.45.1: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.45.1 + '@rollup/rollup-android-arm64': 4.45.1 + '@rollup/rollup-darwin-arm64': 4.45.1 + '@rollup/rollup-darwin-x64': 4.45.1 + '@rollup/rollup-freebsd-arm64': 4.45.1 + '@rollup/rollup-freebsd-x64': 4.45.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.45.1 + '@rollup/rollup-linux-arm-musleabihf': 4.45.1 + '@rollup/rollup-linux-arm64-gnu': 4.45.1 + '@rollup/rollup-linux-arm64-musl': 4.45.1 + '@rollup/rollup-linux-loongarch64-gnu': 4.45.1 + '@rollup/rollup-linux-powerpc64le-gnu': 4.45.1 + '@rollup/rollup-linux-riscv64-gnu': 4.45.1 + '@rollup/rollup-linux-riscv64-musl': 4.45.1 + '@rollup/rollup-linux-s390x-gnu': 4.45.1 + '@rollup/rollup-linux-x64-gnu': 4.45.1 + '@rollup/rollup-linux-x64-musl': 4.45.1 + '@rollup/rollup-win32-arm64-msvc': 4.45.1 + '@rollup/rollup-win32-ia32-msvc': 4.45.1 + '@rollup/rollup-win32-x64-msvc': 4.45.1 + fsevents: 2.3.3 + + rrweb-cssom@0.8.0: + optional: true + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -6774,19 +8171,28 @@ snapshots: safe-buffer@5.2.1: {} + safe-regex2@5.0.0: + dependencies: + ret: 0.5.0 + safer-buffer@2.1.2: {} saxes@5.0.1: dependencies: xmlchars: 2.2.0 - search-insights@2.17.1: {} + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + optional: true + + search-insights@2.17.3: {} selenium-webdriver@4.6.1: dependencies: jszip: 3.10.1 tmp: 0.2.1 - ws: 8.18.0 + ws: 8.18.3 transitivePeerDependencies: - bufferutil - utf-8-validate @@ -6803,10 +8209,10 @@ snapshots: dependencies: lru-cache: 6.0.0 - semver@7.6.3: {} - semver@7.7.1: {} + semver@7.7.2: {} + serialize-javascript@6.0.0: dependencies: randombytes: 2.1.0 @@ -6823,13 +8229,15 @@ snapshots: shebang-regex@3.0.0: {} - shiki@1.23.1: + shiki@2.5.0: dependencies: - '@shikijs/core': 1.23.1 - '@shikijs/engine-javascript': 1.23.1 - '@shikijs/engine-oniguruma': 1.23.1 - '@shikijs/types': 1.23.1 - '@shikijs/vscode-textmate': 9.3.0 + '@shikijs/core': 2.5.0 + '@shikijs/engine-javascript': 2.5.0 + '@shikijs/engine-oniguruma': 2.5.0 + '@shikijs/langs': 2.5.0 + '@shikijs/themes': 2.5.0 + '@shikijs/types': 2.5.0 + '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 siginfo@2.0.0: {} @@ -6840,11 +8248,11 @@ snapshots: simple-git-hooks@2.13.0: {} - simple-git@3.27.0: + simple-git@3.28.0: dependencies: '@kwsites/file-exists': 1.1.1 '@kwsites/promise-deferred': 1.1.1 - debug: 4.3.7 + debug: 4.4.1 transitivePeerDependencies: - supports-color @@ -6870,15 +8278,15 @@ snapshots: smob@1.4.0: {} - socks-proxy-agent@8.0.4: + socks-proxy-agent@8.0.5: dependencies: - agent-base: 7.1.1 - debug: 4.3.7 - socks: 2.8.3 + agent-base: 7.1.4 + debug: 4.4.1 + socks: 2.8.6 transitivePeerDependencies: - supports-color - socks@2.8.3: + socks@2.8.6: dependencies: ip-address: 9.0.5 smart-buffer: 4.2.0 @@ -6892,6 +8300,10 @@ snapshots: source-map@0.6.1: {} + source-map@0.8.0-beta.0: + dependencies: + whatwg-url: 7.1.0 + space-separated-tokens@2.0.2: {} spdx-correct@3.2.0: @@ -6938,13 +8350,12 @@ snapshots: dependencies: duplexer: 0.1.2 - streamx@2.20.0: + streamx@2.22.1: dependencies: fast-fifo: 1.3.2 - queue-tick: 1.0.1 - text-decoder: 1.1.1 + text-decoder: 1.2.3 optionalDependencies: - bare-events: 2.4.2 + bare-events: 2.6.0 string-argv@0.3.2: {} @@ -6999,7 +8410,17 @@ snapshots: strip-json-comments@3.1.1: {} - superjson@2.2.1: + sucrase@3.35.0: + dependencies: + '@jridgewell/gen-mapping': 0.3.12 + commander: 4.1.1 + glob: 10.4.2 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + ts-interface-checker: 0.1.13 + + superjson@2.2.2: dependencies: copy-anything: 3.0.5 @@ -7021,19 +8442,21 @@ snapshots: tabbable@6.2.0: {} - tar-fs@3.0.6: + tar-fs@3.1.0: dependencies: - pump: 3.0.2 + pump: 3.0.3 tar-stream: 3.1.7 optionalDependencies: - bare-fs: 2.3.3 - bare-path: 2.1.3 + bare-fs: 4.1.6 + bare-path: 3.0.0 + transitivePeerDependencies: + - bare-buffer tar-stream@3.1.7: dependencies: - b4a: 1.6.6 + b4a: 1.6.7 fast-fifo: 1.3.2 - streamx: 2.20.0 + streamx: 2.22.1 tcp-port-used@1.0.2: dependencies: @@ -7053,10 +8476,10 @@ snapshots: temp-dir: 2.0.0 uuid: 3.4.0 - terser@5.32.0: + terser@5.43.1: dependencies: - '@jridgewell/source-map': 0.3.6 - acorn: 8.14.0 + '@jridgewell/source-map': 0.3.10 + acorn: 8.15.0 commander: 2.20.3 source-map-support: 0.5.21 @@ -7066,12 +8489,20 @@ snapshots: glob: 10.4.2 minimatch: 9.0.5 - text-decoder@1.1.1: + text-decoder@1.2.3: dependencies: - b4a: 1.6.6 + b4a: 1.6.7 text-extensions@1.9.0: {} + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + through2@2.0.5: dependencies: readable-stream: 2.3.8 @@ -7087,17 +8518,34 @@ snapshots: tinyexec@0.3.1: {} + tinyexec@0.3.2: {} + + tinyexec@1.0.1: {} + tinyglobby@0.2.10: dependencies: fdir: 6.4.2(picomatch@4.0.2) picomatch: 4.0.2 + tinyglobby@0.2.14: + dependencies: + fdir: 6.4.6(picomatch@4.0.2) + picomatch: 4.0.2 + tinypool@1.0.2: {} tinyrainbow@1.2.0: {} tinyspy@3.0.2: {} + tldts-core@6.1.86: + optional: true + + tldts@6.1.86: + dependencies: + tldts-core: 6.1.86 + optional: true + tmp@0.2.1: dependencies: rimraf: 3.0.2 @@ -7115,16 +8563,86 @@ snapshots: universalify: 0.2.0 url-parse: 1.5.10 + tough-cookie@5.1.2: + dependencies: + tldts: 6.1.86 + optional: true + + tr46@1.0.1: + dependencies: + punycode: 2.3.1 + tr46@3.0.0: dependencies: punycode: 2.3.1 + tr46@5.1.1: + dependencies: + punycode: 2.3.1 + optional: true + + tree-kill@1.2.2: {} + trim-lines@3.0.1: {} trim-newlines@3.0.1: {} + ts-interface-checker@0.1.13: {} + + tsdown@0.12.9(@typescript/native-preview@7.0.0-dev.20250718.1)(typescript@5.8.3)(vue-tsc@2.2.12(typescript@5.8.3)): + dependencies: + ansis: 4.1.0 + cac: 6.7.14 + chokidar: 4.0.3 + debug: 4.4.1 + diff: 8.0.2 + empathic: 2.0.0 + hookable: 5.5.3 + rolldown: 1.0.0-beta.28 + rolldown-plugin-dts: 0.13.14(@typescript/native-preview@7.0.0-dev.20250718.1)(rolldown@1.0.0-beta.28)(typescript@5.8.3)(vue-tsc@2.2.12(typescript@5.8.3)) + semver: 7.7.2 + tinyexec: 1.0.1 + tinyglobby: 0.2.14 + unconfig: 7.3.2 + optionalDependencies: + typescript: 5.8.3 + transitivePeerDependencies: + - '@typescript/native-preview' + - oxc-resolver + - supports-color + - vue-tsc + tslib@2.8.1: {} + tsup@8.5.0(@microsoft/api-extractor@7.52.8(@types/node@24.0.14))(jiti@2.4.2)(postcss@8.5.6)(typescript@5.8.3)(yaml@2.8.0): + dependencies: + bundle-require: 5.1.0(esbuild@0.25.6) + cac: 6.7.14 + chokidar: 4.0.3 + consola: 3.4.2 + debug: 4.4.1 + esbuild: 0.25.6 + fix-dts-default-cjs-exports: 1.0.1 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1(jiti@2.4.2)(postcss@8.5.6)(yaml@2.8.0) + resolve-from: 5.0.0 + rollup: 4.45.1 + source-map: 0.8.0-beta.0 + sucrase: 3.35.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.14 + tree-kill: 1.2.2 + optionalDependencies: + '@microsoft/api-extractor': 7.52.8(@types/node@24.0.14) + postcss: 8.5.6 + typescript: 5.8.3 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + type-detect@4.0.8: {} type-fest@0.18.1: {} @@ -7137,31 +8655,40 @@ snapshots: type-fest@0.8.1: {} - typedoc-plugin-markdown@4.2.10(typedoc@0.26.11(typescript@5.6.3)): + typedoc-plugin-markdown@4.7.0(typedoc@0.28.7(typescript@5.8.3)): dependencies: - typedoc: 0.26.11(typescript@5.6.3) + typedoc: 0.28.7(typescript@5.8.3) - typedoc@0.26.11(typescript@5.6.3): + typedoc@0.28.7(typescript@5.8.3): dependencies: + '@gerrit0/mini-shiki': 3.8.0 lunr: 2.3.9 markdown-it: 14.1.0 minimatch: 9.0.5 - shiki: 1.23.1 - typescript: 5.6.3 - yaml: 2.6.1 + typescript: 5.8.3 + yaml: 2.8.0 - typescript@5.4.2: {} + typescript@5.8.2: {} - typescript@5.6.3: {} + typescript@5.8.3: {} uc.micro@2.1.0: {} + ufo@1.6.1: {} + uglify-js@3.17.4: optional: true - undici-types@6.19.8: {} + unconfig@7.3.2: + dependencies: + '@quansync/fs': 0.1.3 + defu: 6.1.4 + jiti: 2.4.2 + quansync: 0.2.10 - undici-types@6.21.0: + undici-types@6.21.0: {} + + undici-types@7.8.0: optional: true unicorn-magic@0.3.0: {} @@ -7189,8 +8716,6 @@ snapshots: unist-util-is: 6.0.0 unist-util-visit-parents: 6.0.1 - universalify@0.1.2: {} - universalify@0.2.0: {} universalify@2.0.1: {} @@ -7227,13 +8752,13 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 - vite-node@2.1.9(@types/node@22.15.2)(terser@5.32.0): + vite-node@2.1.9(@types/node@24.0.14)(terser@5.43.1): dependencies: cac: 6.7.14 - debug: 4.3.7 + debug: 4.4.1 es-module-lexer: 1.7.0 pathe: 1.1.2 - vite: 5.4.18(@types/node@22.15.2)(terser@5.32.0) + vite: 5.4.19(@types/node@24.0.14)(terser@5.43.1) transitivePeerDependencies: - '@types/node' - less @@ -7245,72 +8770,61 @@ snapshots: - supports-color - terser - vite@5.4.11(@types/node@22.15.2)(terser@5.32.0): - dependencies: - esbuild: 0.21.5 - postcss: 8.4.49 - rollup: 4.27.4 - optionalDependencies: - '@types/node': 22.15.2 - fsevents: 2.3.3 - terser: 5.32.0 - - vite@5.4.18(@types/node@20.17.31)(terser@5.32.0): + vite@5.4.19(@types/node@20.19.8)(terser@5.43.1): dependencies: esbuild: 0.21.5 - postcss: 8.4.49 + postcss: 8.5.6 rollup: 4.27.4 optionalDependencies: - '@types/node': 20.17.31 + '@types/node': 20.19.8 fsevents: 2.3.3 - terser: 5.32.0 + terser: 5.43.1 - vite@5.4.18(@types/node@22.15.2)(terser@5.32.0): + vite@5.4.19(@types/node@24.0.14)(terser@5.43.1): dependencies: esbuild: 0.21.5 - postcss: 8.4.49 + postcss: 8.5.6 rollup: 4.27.4 optionalDependencies: - '@types/node': 22.15.2 + '@types/node': 24.0.14 fsevents: 2.3.3 - terser: 5.32.0 + terser: 5.43.1 - vitepress-translation-helper@0.2.1(vitepress@1.5.0(@algolia/client-search@5.15.0)(@types/node@22.15.2)(axios@1.7.7)(postcss@8.4.49)(search-insights@2.17.1)(terser@5.32.0)(typescript@5.6.3))(vue@3.5.13(typescript@5.6.3)): + vitepress-translation-helper@0.2.1(vitepress@1.6.3(@algolia/client-search@5.34.0)(@types/node@24.0.14)(axios@1.10.0)(postcss@8.5.6)(search-insights@2.17.3)(terser@5.43.1)(typescript@5.8.3))(vue@3.5.17(typescript@5.8.3)): dependencies: minimist: 1.2.8 - simple-git: 3.27.0 - vitepress: 1.5.0(@algolia/client-search@5.15.0)(@types/node@22.15.2)(axios@1.7.7)(postcss@8.4.49)(search-insights@2.17.1)(terser@5.32.0)(typescript@5.6.3) - vue: 3.5.13(typescript@5.6.3) + simple-git: 3.28.0 + vitepress: 1.6.3(@algolia/client-search@5.34.0)(@types/node@24.0.14)(axios@1.10.0)(postcss@8.5.6)(search-insights@2.17.3)(terser@5.43.1)(typescript@5.8.3) + vue: 3.5.17(typescript@5.8.3) transitivePeerDependencies: - supports-color - vitepress@1.5.0(@algolia/client-search@5.15.0)(@types/node@22.15.2)(axios@1.7.7)(postcss@8.4.49)(search-insights@2.17.1)(terser@5.32.0)(typescript@5.6.3): + vitepress@1.6.3(@algolia/client-search@5.34.0)(@types/node@24.0.14)(axios@1.10.0)(postcss@8.5.6)(search-insights@2.17.3)(terser@5.43.1)(typescript@5.8.3): dependencies: - '@docsearch/css': 3.8.0 - '@docsearch/js': 3.8.0(@algolia/client-search@5.15.0)(search-insights@2.17.1) - '@iconify-json/simple-icons': 1.2.13 - '@shikijs/core': 1.23.1 - '@shikijs/transformers': 1.23.1 - '@shikijs/types': 1.23.1 + '@docsearch/css': 3.8.2 + '@docsearch/js': 3.8.2(@algolia/client-search@5.34.0)(search-insights@2.17.3) + '@iconify-json/simple-icons': 1.2.43 + '@shikijs/core': 2.5.0 + '@shikijs/transformers': 2.5.0 + '@shikijs/types': 2.5.0 '@types/markdown-it': 14.1.2 - '@vitejs/plugin-vue': 5.2.0(vite@5.4.11(@types/node@22.15.2)(terser@5.32.0))(vue@3.5.13(typescript@5.6.3)) - '@vue/devtools-api': 7.6.4 - '@vue/shared': 3.5.13 - '@vueuse/core': 11.3.0(vue@3.5.13(typescript@5.6.3)) - '@vueuse/integrations': 11.3.0(axios@1.7.7)(focus-trap@7.6.2)(vue@3.5.13(typescript@5.6.3)) - focus-trap: 7.6.2 + '@vitejs/plugin-vue': 5.2.4(vite@5.4.19(@types/node@24.0.14)(terser@5.43.1))(vue@3.5.17(typescript@5.8.3)) + '@vue/devtools-api': 7.7.7 + '@vue/shared': 3.5.17 + '@vueuse/core': 12.8.2(typescript@5.8.3) + '@vueuse/integrations': 12.8.2(axios@1.10.0)(focus-trap@7.6.5)(typescript@5.8.3) + focus-trap: 7.6.5 mark.js: 8.11.1 - minisearch: 7.1.1 - shiki: 1.23.1 - vite: 5.4.11(@types/node@22.15.2)(terser@5.32.0) - vue: 3.5.13(typescript@5.6.3) + minisearch: 7.1.2 + shiki: 2.5.0 + vite: 5.4.19(@types/node@24.0.14)(terser@5.43.1) + vue: 3.5.17(typescript@5.8.3) optionalDependencies: - postcss: 8.4.49 + postcss: 8.5.6 transitivePeerDependencies: - '@algolia/client-search' - '@types/node' - '@types/react' - - '@vue/composition-api' - async-validator - axios - change-case @@ -7334,33 +8848,33 @@ snapshots: - typescript - universal-cookie - vitest@2.1.9(@types/node@22.15.2)(@vitest/ui@2.1.9)(happy-dom@15.11.7)(jsdom@19.0.0)(terser@5.32.0): + vitest@2.1.9(@types/node@24.0.14)(@vitest/ui@2.1.9)(happy-dom@18.0.1)(jsdom@26.1.0)(terser@5.43.1): dependencies: '@vitest/expect': 2.1.9 - '@vitest/mocker': 2.1.9(vite@5.4.18(@types/node@22.15.2)(terser@5.32.0)) + '@vitest/mocker': 2.1.9(vite@5.4.19(@types/node@24.0.14)(terser@5.43.1)) '@vitest/pretty-format': 2.1.9 '@vitest/runner': 2.1.9 '@vitest/snapshot': 2.1.9 '@vitest/spy': 2.1.9 '@vitest/utils': 2.1.9 chai: 5.1.2 - debug: 4.3.7 + debug: 4.4.1 expect-type: 1.1.0 - magic-string: 0.30.13 + magic-string: 0.30.17 pathe: 1.1.2 std-env: 3.8.0 tinybench: 2.9.0 tinyexec: 0.3.1 tinypool: 1.0.2 tinyrainbow: 1.2.0 - vite: 5.4.18(@types/node@22.15.2)(terser@5.32.0) - vite-node: 2.1.9(@types/node@22.15.2)(terser@5.32.0) + vite: 5.4.19(@types/node@24.0.14)(terser@5.43.1) + vite-node: 2.1.9(@types/node@24.0.14)(terser@5.43.1) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 22.15.2 + '@types/node': 24.0.14 '@vitest/ui': 2.1.9(vitest@2.1.9) - happy-dom: 15.11.7 - jsdom: 19.0.0 + happy-dom: 18.0.1 + jsdom: 26.1.0 transitivePeerDependencies: - less - lightningcss @@ -7376,25 +8890,21 @@ snapshots: vue-component-type-helpers@2.0.21: {} - vue-demi@0.14.10(vue@3.5.13(typescript@5.6.3)): - dependencies: - vue: 3.5.13(typescript@5.6.3) - - vue-tsc@2.2.10(typescript@5.6.3): + vue-tsc@2.2.12(typescript@5.8.3): dependencies: - '@volar/typescript': 2.4.12 - '@vue/language-core': 2.2.10(typescript@5.6.3) - typescript: 5.6.3 + '@volar/typescript': 2.4.15 + '@vue/language-core': 2.2.12(typescript@5.8.3) + typescript: 5.8.3 - vue@3.5.13(typescript@5.6.3): + vue@3.5.17(typescript@5.8.3): dependencies: - '@vue/compiler-dom': 3.5.13 - '@vue/compiler-sfc': 3.5.13 - '@vue/runtime-dom': 3.5.13 - '@vue/server-renderer': 3.5.13(vue@3.5.13(typescript@5.6.3)) - '@vue/shared': 3.5.13 + '@vue/compiler-dom': 3.5.17 + '@vue/compiler-sfc': 3.5.17 + '@vue/runtime-dom': 3.5.17 + '@vue/server-renderer': 3.5.17(vue@3.5.17(typescript@5.8.3)) + '@vue/shared': 3.5.17 optionalDependencies: - typescript: 5.6.3 + typescript: 5.8.3 w3c-hr-time@1.0.2: dependencies: @@ -7404,20 +8914,35 @@ snapshots: dependencies: xml-name-validator: 4.0.0 + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + optional: true + wcwidth@1.0.1: dependencies: defaults: 1.0.4 web-streams-polyfill@3.3.3: {} + webidl-conversions@4.0.2: {} + webidl-conversions@7.0.0: {} whatwg-encoding@2.0.0: dependencies: iconv-lite: 0.6.3 + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + optional: true + whatwg-mimetype@3.0.0: {} + whatwg-mimetype@4.0.0: + optional: true + whatwg-url@10.0.0: dependencies: tr46: 3.0.0 @@ -7428,11 +8953,23 @@ snapshots: tr46: 3.0.0 webidl-conversions: 7.0.0 + whatwg-url@14.2.0: + dependencies: + tr46: 5.1.1 + webidl-conversions: 7.0.0 + optional: true + + whatwg-url@7.1.0: + dependencies: + lodash.sortby: 4.7.0 + tr46: 1.0.1 + webidl-conversions: 4.0.2 + which@2.0.2: dependencies: isexe: 2.0.0 - which@4.0.0: + which@5.0.0: dependencies: isexe: 3.1.1 @@ -7469,10 +9006,13 @@ snapshots: wrappy@1.0.2: {} - ws@8.18.0: {} + ws@8.18.3: {} xml-name-validator@4.0.0: {} + xml-name-validator@5.0.0: + optional: true + xmlchars@2.2.0: {} xtend@4.0.2: {} @@ -7481,9 +9021,7 @@ snapshots: yallist@4.0.0: {} - yaml@2.6.1: {} - - yaml@2.7.1: {} + yaml@2.8.0: {} yargs-parser@20.2.4: {} From bce94866f2f87162b1f2d0cf0f53c25e1be477c7 Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Fri, 18 Jul 2025 18:35:15 +0200 Subject: [PATCH 40/40] docs: upgrade typedoc --- packages/docs/.gitignore | 1 + packages/docs/.vitepress/config/en.ts | 14 +++++++------- packages/docs/.vitepress/config/zh.ts | 15 ++++++++------- packages/docs/package.json | 3 ++- packages/docs/run-typedoc.mjs | 16 +++++++++++----- packages/docs/typedoc-markdown.mjs | 17 +++++++++++------ packages/docs/typedoc.tsconfig.json | 2 +- pnpm-lock.yaml | 22 +++++++++++++++++----- 8 files changed, 58 insertions(+), 32 deletions(-) diff --git a/packages/docs/.gitignore b/packages/docs/.gitignore index 4f2fc4ac0..45f548031 100644 --- a/packages/docs/.gitignore +++ b/packages/docs/.gitignore @@ -1 +1,2 @@ .vitepress/cache +api diff --git a/packages/docs/.vitepress/config/en.ts b/packages/docs/.vitepress/config/en.ts index b4e7f0e4b..e93f51697 100644 --- a/packages/docs/.vitepress/config/en.ts +++ b/packages/docs/.vitepress/config/en.ts @@ -1,4 +1,5 @@ import type { DefaultTheme, LocaleSpecificConfig } from 'vitepress' +import typedocSidebar from '../../api/typedoc-sidebar.json' export const META_URL = 'https://router.vuejs.org' export const META_TITLE = 'Vue Router' @@ -53,6 +54,12 @@ export const enConfig: LocaleSpecificConfig = { ], sidebar: { + '/api/': [ + { + text: 'API', + items: typedocSidebar, + }, + ], // catch-all fallback '/': [ { @@ -179,13 +186,6 @@ export const enConfig: LocaleSpecificConfig = { ], }, ], - - '/api/': [ - { - text: 'packages', - items: [{ text: 'vue-router', link: '/api/' }], - }, - ], }, }, } diff --git a/packages/docs/.vitepress/config/zh.ts b/packages/docs/.vitepress/config/zh.ts index dbc6ba4ab..312f9784e 100644 --- a/packages/docs/.vitepress/config/zh.ts +++ b/packages/docs/.vitepress/config/zh.ts @@ -1,4 +1,6 @@ import type { DefaultTheme, LocaleSpecificConfig } from 'vitepress' +import typedocSidebar from '../../api/typedoc-sidebar.json' +// TODO: rework the typedoc sidebar to include the /zh/ prefix export const META_URL = 'https://router.vuejs.org' export const META_TITLE = 'Vue Router' @@ -57,6 +59,12 @@ export const zhConfig: LocaleSpecificConfig = { ], sidebar: { + '/zh/api/': [ + { + text: 'API', + items: typedocSidebar, + }, + ], '/zh/': [ { text: '设置', @@ -186,13 +194,6 @@ export const zhConfig: LocaleSpecificConfig = { ], }, ], - - '/zh/api/': [ - { - text: 'packages', - items: [{ text: 'vue-router', link: '/zh/api/' }], - }, - ], }, }, } diff --git a/packages/docs/package.json b/packages/docs/package.json index 4765889c2..05815c563 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -14,8 +14,9 @@ }, "dependencies": { "simple-git": "^3.28.0", + "typedoc-vitepress-theme": "^1.1.2", "vitepress": "1.6.3", - "vitepress-translation-helper": "^0.2.1", + "vitepress-translation-helper": "^0.2.2", "vue-router": "workspace:*" } } diff --git a/packages/docs/run-typedoc.mjs b/packages/docs/run-typedoc.mjs index 790b14b9a..60c398e37 100644 --- a/packages/docs/run-typedoc.mjs +++ b/packages/docs/run-typedoc.mjs @@ -1,15 +1,21 @@ import path from 'node:path' -import { fileURLToPath } from 'node:url' import { createTypeDocApp } from './typedoc-markdown.mjs' -const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const __dirname = path.dirname(new URL(import.meta.url).pathname) createTypeDocApp({ - name: 'API Documentation', + textContentMappings: { + 'title.indexPage': 'API Reference', + 'title.memberPage': '{name}', + }, tsconfig: path.resolve(__dirname, './typedoc.tsconfig.json'), + // entryPointStrategy: 'packages', categorizeByGroup: true, githubPages: false, - disableSources: true, // some links are in node_modules and it's ugly - plugin: ['typedoc-plugin-markdown'], + readme: 'none', + indexFormat: 'table', + disableSources: true, + plugin: ['typedoc-plugin-markdown', 'typedoc-vitepress-theme'], + useCodeBlocks: true, entryPoints: [path.resolve(__dirname, '../router/src/index.ts')], }).then(app => app.build()) diff --git a/packages/docs/typedoc-markdown.mjs b/packages/docs/typedoc-markdown.mjs index 3c84b4e33..dcae3a8a2 100644 --- a/packages/docs/typedoc-markdown.mjs +++ b/packages/docs/typedoc-markdown.mjs @@ -1,13 +1,12 @@ // @ts-check import fs from 'node:fs/promises' import path from 'node:path' -import { fileURLToPath } from 'node:url' -import { Application, PageEvent, TSConfigReader } from 'typedoc' +import { Application, TSConfigReader, PageEvent } from 'typedoc' -const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const __dirname = path.dirname(new URL(import.meta.url).pathname) +/** @satisfies {Partial} */ const DEFAULT_OPTIONS = { - // disableOutputCheck: true, cleanOutputDir: true, excludeInternal: false, readme: 'none', @@ -72,7 +71,7 @@ export async function createTypeDocApp(config = {}) { if (project) { // Rendered docs try { - await app.generateDocs(project, options.out) + await app.generateOutputs(project) app.logger.info(`generated at ${options.out}.`) } catch (error) { app.logger.error(error) @@ -88,6 +87,12 @@ export async function createTypeDocApp(config = {}) { } } +/** + * checks if a path exists + * + * @async + * @param {string} path + */ async function exists(path) { try { await fs.access(path) @@ -108,7 +113,7 @@ async function exists(path) { */ function prependYAML(contents, vars) { return contents - .replace(/^/, `${toYAML(vars)}\n\n`) + .replace(/^/, toYAML(vars) + '\n\n') .replace(/[\r\n]{3,}/g, '\n\n') } diff --git a/packages/docs/typedoc.tsconfig.json b/packages/docs/typedoc.tsconfig.json index 3ca328f83..4601fbf47 100644 --- a/packages/docs/typedoc.tsconfig.json +++ b/packages/docs/typedoc.tsconfig.json @@ -31,6 +31,6 @@ "removeComments": false, "jsx": "preserve", "lib": ["esnext", "dom"], - "types": ["node"] + "types": ["vitest", "node", "vite/client"] } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ca41b93a5..828d69a78 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -65,12 +65,15 @@ importers: simple-git: specifier: ^3.28.0 version: 3.28.0 + typedoc-vitepress-theme: + specifier: ^1.1.2 + version: 1.1.2(typedoc-plugin-markdown@4.7.0(typedoc@0.28.7(typescript@5.8.3))) vitepress: specifier: 1.6.3 version: 1.6.3(@algolia/client-search@5.34.0)(@types/node@24.0.14)(axios@1.10.0)(postcss@8.5.6)(search-insights@2.17.3)(terser@5.43.1)(typescript@5.8.3) vitepress-translation-helper: - specifier: ^0.2.1 - version: 0.2.1(vitepress@1.6.3(@algolia/client-search@5.34.0)(@types/node@24.0.14)(axios@1.10.0)(postcss@8.5.6)(search-insights@2.17.3)(terser@5.43.1)(typescript@5.8.3))(vue@3.5.17(typescript@5.8.3)) + specifier: ^0.2.2 + version: 0.2.2(vitepress@1.6.3(@algolia/client-search@5.34.0)(@types/node@24.0.14)(axios@1.10.0)(postcss@8.5.6)(search-insights@2.17.3)(terser@5.43.1)(typescript@5.8.3))(vue@3.5.17(typescript@5.8.3)) vue-router: specifier: workspace:* version: link:../router @@ -4211,6 +4214,11 @@ packages: peerDependencies: typedoc: 0.28.x + typedoc-vitepress-theme@1.1.2: + resolution: {integrity: sha512-hQvCZRr5uKDqY1bRuY1+eNTNn6d4TE4OP5pnw65Y7WGgajkJW9X1/lVJK2UJpcwCmwkdjw1QIO49H9JQlxWhhw==} + peerDependencies: + typedoc-plugin-markdown: '>=4.4.0' + typedoc@0.28.7: resolution: {integrity: sha512-lpz0Oxl6aidFkmS90VQDQjk/Qf2iw0IUvFqirdONBdj7jPSN9mGXhy66BcGNDxx5ZMyKKiBVAREvPEzT6Uxipw==} engines: {node: '>= 18', pnpm: '>= 10'} @@ -4342,8 +4350,8 @@ packages: terser: optional: true - vitepress-translation-helper@0.2.1: - resolution: {integrity: sha512-zYjakGIdVDonT1P85OkeQcdE6e8vdmKiVclHB7DGcTzFUwb2D0w+hcC31AGneB5wa5IiqEoipycSTYNKM0YKJg==} + vitepress-translation-helper@0.2.2: + resolution: {integrity: sha512-xqE4p1iUmsADKyA8W/02POtEwL0ZMcY2Ogj4Shuh70392UslB5JhrgCdF1j61NIQhgy/wgAGhn33QZdksN6IqQ==} hasBin: true peerDependencies: vitepress: ^1.0.0 @@ -8659,6 +8667,10 @@ snapshots: dependencies: typedoc: 0.28.7(typescript@5.8.3) + typedoc-vitepress-theme@1.1.2(typedoc-plugin-markdown@4.7.0(typedoc@0.28.7(typescript@5.8.3))): + dependencies: + typedoc-plugin-markdown: 4.7.0(typedoc@0.28.7(typescript@5.8.3)) + typedoc@0.28.7(typescript@5.8.3): dependencies: '@gerrit0/mini-shiki': 3.8.0 @@ -8790,7 +8802,7 @@ snapshots: fsevents: 2.3.3 terser: 5.43.1 - vitepress-translation-helper@0.2.1(vitepress@1.6.3(@algolia/client-search@5.34.0)(@types/node@24.0.14)(axios@1.10.0)(postcss@8.5.6)(search-insights@2.17.3)(terser@5.43.1)(typescript@5.8.3))(vue@3.5.17(typescript@5.8.3)): + vitepress-translation-helper@0.2.2(vitepress@1.6.3(@algolia/client-search@5.34.0)(@types/node@24.0.14)(axios@1.10.0)(postcss@8.5.6)(search-insights@2.17.3)(terser@5.43.1)(typescript@5.8.3))(vue@3.5.17(typescript@5.8.3)): dependencies: minimist: 1.2.8 simple-git: 3.28.0